diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..db0a440 Binary files /dev/null and b/.DS_Store differ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..80ef291 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,42 @@ +sudo: false + +language: php + +branches: + only: + - stable + +cache: + directories: + - $HOME/.composer/cache + +before_install: + - composer self-update + +install: + - composer install --no-dev --no-interaction --ignore-platform-reqs + - zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Core.zip . + - composer require --update-no-dev --no-interaction "topthink/think-image:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-migration:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-captcha:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-mongo:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-worker:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-helper:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-queue:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-angular:^1.0" + - composer require --dev --update-no-dev --no-interaction "topthink/think-testing:^1.0" + - zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Full.zip . + +script: + - php think unit + +deploy: + provider: releases + api_key: + secure: TSF6bnl2JYN72UQOORAJYL+CqIryP2gHVKt6grfveQ7d9rleAEoxlq6PWxbvTI4jZ5nrPpUcBUpWIJHNgVcs+bzLFtyh5THaLqm39uCgBbrW7M8rI26L8sBh/6nsdtGgdeQrO/cLu31QoTzbwuz1WfAVoCdCkOSZeXyT/CclH99qV6RYyQYqaD2wpRjrhA5O4fSsEkiPVuk0GaOogFlrQHx+C+lHnf6pa1KxEoN1A0UxxVfGX6K4y5g4WQDO5zT4bLeubkWOXK0G51XSvACDOZVIyLdjApaOFTwamPcD3S1tfvuxRWWvsCD5ljFvb2kSmx5BIBNwN80MzuBmrGIC27XLGOxyMerwKxB6DskNUO9PflKHDPI61DRq0FTy1fv70SFMSiAtUv9aJRT41NQh9iJJ0vC8dl+xcxrWIjU1GG6+l/ZcRqVx9V1VuGQsLKndGhja7SQ+X1slHl76fRq223sMOql7MFCd0vvvxVQ2V39CcFKao/LB1aPH3VhODDEyxwx6aXoTznvC/QPepgWsHOWQzKj9ftsgDbsNiyFlXL4cu8DWUty6rQy8zT2b4O8b1xjcwSUCsy+auEjBamzQkMJFNlZAIUrukL/NbUhQU37TAbwsFyz7X0E/u/VMle/nBCNAzgkMwAUjiHM6FqrKKBRWFbPrSIixjfjkCnrMEPw= + file: + - ThinkPHP_Core.zip + - ThinkPHP_Full.zip + skip_cleanup: true + on: + tags: true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8284d9e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,838 @@ + +## V5.1.34 LTS(2019-1-30) + +本次更新为常规更新,修正了一些反馈的问题。 + +* 改进Request类的`has`方法,支持`patch` +* 改进`unique`验证的多条件支持 +* 修复自定义上传验证,检测文件大小 +* 改进`in`查询支持表达式 +* 改进路由的`getBind`方法 +* 改进验证类的错误信息获取 +* 改进`response`助手函数默认值 +* 修正mysql的`regexp`查询 +* 改进模型类型强制转换写入对`Expression`对象的支持 + +## V5.1.33 LTS(2019-1-16) + +* 修复路由中存在多个相同替换的正则BUG +* 修正whereLike查询 +* join方法支持参数绑定 +* 改进union方法 +* 修正多对多关联的attach方法 +* 改进验证类的正则规则自定义 +* 改进Request类method方法 +* 改进File日志类型的CLI日志写入 +* 改进文件日志time_format配置对JSON格式的支持 + +## V5.1.32 LTS(2018-12-24) + +本次主要为常规更新,修正了一些反馈的问题。 + + +* 改进多对多关联的`attach`方法 +* 改进聚合查询的`field`处理 +* 改进关联的`save`方法 +* 修正模型`exists`方法返回值 +* 改进时间字段写入和输出 +* 改进控制器中间件的调用 +* 改进路由变量替换的性能 +* 改进缓存标签的处理机制 + +## V5.1.31 LTS (2018-12-9) + +本次版本包含一个安全更新,建议升级。 + +* 改进`field`方法 +* 改进`count`方法返回类型 +* `download`函数增加在浏览器中显示文件功能 +* 修正多对多模型的中间表数据写入 +* 改进`sqlsrv`驱动支持多个Schemas模式查询 +* 统一助手函数与\think\response\Download函数文件过期时间 +* 完善关联模型的`save`方法 增加`make`方法仅创建对象不保存 +* 修改条件表达式对静态变量的支持 +* 修正控制器名获取 +* 改进view方法的`field`解析 + +## V5.1.30 LTS(2018-11-30) + +该版本为常规更新,修正了一些社区反馈的问题。 + +主要更新如下: + +* 改进查询类的`execute`方法 +* 判断路由规则定义添加对请求类型的判断 +* 修复`orderRaw`异常 +* 修正 `optimize:autoload`指令 +* 改进软删除的`destroy`方法造成重复执行事件的问题 +* 改进验证类对扩展验证规则 始终验证 不管是否`require` +* 修复自定义验证`remove`所有规则的异常 +* 改进时间字段的自动写入支持微秒数据 +* 改进`Connection`类的`getrealsql`方法 +* 修正`https`地址的URL生成 +* 修复 `array_walk_recursive` 在低于PHP7.1消耗内部指针问题 +* 改进手动参数绑定使用 +* 改进聚合查询方法的`field`参数支持`Expression` + +## V5.1.29 LTS(2018-11-11) + +该版本主要改进了参数绑定的解析问题和提升性能,并修正了一些反馈的问题。 + +* 改进手动参数绑定 +* 修正MISS路由的分组参数无效问题 +* 行为支持对象的方法 +* 修正全局查询范围 +* 改进`belongsto`关联的`has`方法 +* 改进`hasMany`关联 +* 改进模型观察者多次注册的问题 +* 改进`query`类的默认查询参数处理 +* 修正`parseBetween`解析方法 +* 改进路由地址生成的本地域名支持 +* 改进参数绑定的实际URL解析性能 +* 改进`Env`类的`getEnv`和`get`方法 +* 改进模板缓存的生成优化 +* 修复验证类的多语言支持 +* 修复自定义场景验证`remove`规则异常 +* File类添加是否自动补全扩展名的选项 +* 改进`strpos`对子串是否存在的判断 +* 修复`choice`无法用值选择第一个选项问题 +* 验证器支持多维数组取值验证 +* 改进解析`extend`和`block`标签的正则 + +## V5.1.28 LTS(2018-10-29) + +该版本主要修正了上一个版本存在的一些问题,并改进了关联查询 + +* 改进聚合查询方法的字段支持DISTINCT +* 改进定义路由后url函数的端口生成 +* 改进控制器中间件对`swoole`等的支持 +* 改进Log类`save`方法 +* 改进验证类的闭包验证参数 +* 多对多关联支持指定中间表数据的名称 +* 关联聚合查询支持闭包方式指定聚合字段 +* 改进Lang类`get`方法 +* 多对多关联增加判断关联数据是否存在的方法 +* 改进关联查询使用`fetchsql`的情况 +* 改进修改器的是否已经执行判断 +* 增加`afterWith`和`beforeWith`验证规则 用于比较日期字段 + +## V5.1.27 LTS(2018-10-22) + +该版本主要修正了路由绑定的参数,改进了修改器的执行多次问题,并正式宣布为LTS版本! + + +* 修正路由绑定的参数丢失问题 +* 修正路由别名的参数获取 +* 改进修改器会执行多次的问题 + +## V5.1.26(2018-10-12) + +该版本主要修正了上一个版本的一些问题,并改进了全局查询范围的支持,同时包含了一个安全更新。 + + +* 修正单一模块下注解路由无效的问题 +* 改进数据库的聚合查询的字段处理 +* 模型类增加`globalScope`属性定义 用于指定全局的查询范围 +* 模型的`useGlobalScope`方法支持传入数组 用于指定当前查询需要使用的全局查询范围 +* 改进数据集的`order`方法对数字类型的支持 +* 修正上一个版本`order`方法解析的一处BUG +* 排序字段不合法或者错误的时候抛出异常 +* 改进`Request`类的`file`方法对上传文件的错误判断 + +## V5.1.25(2018-9-21) + +该版本主要改进了查询参数绑定的性能和对浮点型的支持,以及一些细节的完善。 + +* 修正一处命令行问题 +* 改进`Socketlog`日志驱动,支持自定义默认展开日志类别 +* 修正`MorphMany`一处bug +* 跳转到上次记住的url,并支持默认值 +* 改进模型的异常提示 +* 改进参数绑定对浮点型的支持 +* 改进`order`方法解析 +* 改进`json`字段数据的自动编码 +* 改进日志`log_write`可能造成的日志写入死循环 +* Log类增加`log_level`行为标签位置,用于对某个类型的日志进行处理 +* Route类增加`clear`方法清空路由规则 +* 分布式数据库配置支持使用数组 +* 单日志文件也支持`max_files`参数 +* 改进查询参数绑定的性能 +* 改进别名路由的URL后缀参数检测 +* 控制器前置方法和控制器中间件的`only`和`except`定义不区分大小写 + +## V5.1.24(2018-9-5) + +该版本主要增加了命令行的表格输出功能,并增加了查看路由定义的指令,以及修正了社区的一些反馈问题。 + +* 修正`Request`类的`file`方法 +* 修正路由的`cache`方法 +* 修正路由缓存的一处问题 +* 改进上传文件获取的异常处理 +* 改进`fetchCollection`方法支持传入数据集类名 +* 修正多级控制器的注解路由生成 +* 改进`Middleware`类`clear`方法 +* 增加`route:list`指令用于[查看定义的路由](752690) 并支持排序 +* 命令行增加`Table`输出类 +* `Command`类增加`table`方法用于输出表格 +* 改进搜索器查询方法支持别名定义 +* 命令行配置增加`auto_path`参数用于定义自动载入的命令类路径 +* 增加`make:command`指令用于[快速生成指令](354146) +* 改进`make:controller`指令对操作方法后缀的支持 +* 改进命令行的定义文件支持索引数组 用于指令对象的惰性加载 +* 改进`value`和`column`方法对后续查询结果的影响 +* 改进`RuleName`类的`setRule`方法 + +## V5.1.23(2018-8-23) + +该版本主要改进了数据集对象的处理,增加了`findOrEmpty`方法,并且修正了一些社区反馈的BUG。 + +* 数据集类增加`diff`/`intersect`方法用于获取差集和交集(默认根据主键值比较) +* 数据集类增加`order`方法支持指定字段排序 +* 数据集类增加`map`方法使用回调函数处理数据并返回新的数据集对象 +* Db增加`allowEmpty`方法允许`find`方法在没有数据的时候返回空数组或者空模型对象而不是null +* Db增加`findOrEmpty`方法 +* Db增加`fetchCollection`方法用于指定查询返回数据集对象 +* 改进`order`方法的数组方式解析,增强安全性 +* 改进`withSearch`方法,支持第三个参数传入字段前缀标识,用于多表查询字段搜索 +* 修正`optimize:route`指令开启类库后缀后的注解路由生成 +* 修正redis缓存及session驱动 +* 支持指定`Yaconf`的独立配置文件 +* 增加`yaconf`助手函数用于配置文件 + + +## V5.1.22(2018-8-9) + +该版本主要增加了模型搜索器和`withJoin`方法,完善了模型输出和对`Yaconf`的支持,修正了一些社区反馈的BUG。 + +* 改进一对一关联的`table`识别问题 +* 改进内置`Facade`类 +* 增加`withJoin`方法支持`join`方式的[一对一关联](一对一关联.md)查询 +* 改进`join`预载入查询的空数据问题 +* 改进`Config`类的`load`方法支持快速加载配置文件 +* 改进`execute`方法和事务的断线重连 +* 改进`memcache`驱动的`has`方法 +* 模型类支持定义[搜索器](搜索器.md)方法 +* 完善`Config`类对`Yaconf`的支持 +* 改进模型的`hidden/visible/append/withAttr`方法,支持在[查询前后调用](数组访问.md),以及支持数据集对象 +* 数据集对象增加`where`方法根据字段或者关联数据[过滤数据](模型数据集.md) +* 改进AJAX请求的`204`判断 + + +## V5.1.21(2018-8-2) + +该版本主要增加了下载响应对象和数组查询对象的支持,并修正了一些社区反馈的问题。 + +* 改进核心对象的无用信息调试输出 +* 改进模型的`isRelationAttr`方法判断 +* 模型类的`get`和`all`方法并入Db类 +* 增加[下载响应对象](文件下载.md)和`download`助手函数 +* 修正别名路由配置定义读取 +* 改进`resultToModel`方法 +* 修正开启类库后缀后的注解路由生成 +* `Response`类增加`noCache`快捷方法 +* 改进路由对象在`Swoole`/`Workerman`下面参数多次合并问题 +* 修正路由`ajax`/`pjax`参数后路由变量无法正确获取的问题 +* 增加清除中间件的方法 +* 改进依赖注入的参数规范自动识别(便于对接前端小写+下划线规范) +* 改进`hasWhere`的数组条件的字段判断 +* 增加[数组查询对象](高级查询.md)`Where`支持(喜欢数组查询的福音) +* 改进多对多关联的闭包支持 + +## V5.1.20(2018-7-25) + +该版本主要增加了Db和模型的动态获取器的支持,并修正了一些已知问题。 + +* Db类添加[获取器支持](703981) +* 支持模型及关联模型字段[动态定义获取器](354046) +* 动态获取器支持`JSON`字段 +* 改进路由的`before`行为执行(匹配后执行) +* `Config`类支持`Yaconf` +* 改进Url生成的端口问题 +* Request类增加`setUrl`和`setBaseUrl`方法 +* 改进页面trace的信息显示 +* 修正`MorphOne`关联 +* 命令行添加[查看版本指令](703994) + +## V5.1.19 (2018-7-13) + +该版本是一个小幅改进版本,针对`Swoole`和`Workerman`的`Cookie`支持做了一些改进,并修正了一些已知的问题。 + + +* 改进query类`delete`方法对软删除条件判断 +* 修正分表查询的软删除问题 +* 模型查询的时候同时传入`table`和`name`属性 +* 容器类增加`IteratorAggregate`和`Countable`接口支持 +* 路由分组支持对下面的资源路由统一设置`only/except/vars`参数 +* 改进Cookie类更好支持扩展 +* 改进Request类`post`方法 +* 改进模型自关联的自动识别 +* 改进Request类对`php://input`数据的处理 + + +## V5.1.18 (2018-6-30) + +该版本主要完善了对`Swoole`和`Workerman`的`HttpServer`运行支持,改进`Request`类,并修正了一些已知的问题。 + +* 改进关联`append`方法的处理 +* 路由初始化和检测方法分离 +* 修正`destroy`方法强制删除 +* `app_init`钩子位置移入`run`方法 +* `think-swoole`扩展更新到2.0版本 +* `think-worker`扩展更新到2.0版本 +* 改进Url生成的域名自动识别 +* `Request`类增加`setPathinfo`方法和`setHost`方法 +* `Request`类增加`withGet`/`withPost`/`withHeader`/`withServer`/`withCookie`/`withEnv`方法进行赋值操作 +* Route类改进`host`属性的获取 +* 解决注解路由配置不生效的问题 +* 取消Test日志驱动,改为使用`close`设置关闭全局日志写入 +* 修正路由的`response`参数 +* 修正204响应输出的判断 + +## V5.1.17 (2018-6-18) + +该版本主要增加了控制器中间件的支持,改进了路由功能,并且修正了社区反馈的一些问题。 + +* 修正软删除的`delete`方法 +* 修正Query类`Count`方法 +* 改进多对多`detach`方法 +* 改进Request类`Session`方法 +* 增加控制器中间件支持 +* 模型类增加`jsonAssoc`属性用于定义json数据是否返回数组 +* 修正Request类`method`方法的请求伪装 +* 改进静态路由的匹配 +* 分组首页路由自动完整匹配 +* 改进sqlsrv的`column`方法 +* 日志类的`apart_level`配置支持true自动生成对应类型的日志文件 +* 改进`204`输出判断 +* 修正cli下页面输出的BUG +* 验证类使用更高效的`ctype`验证机制 +* 改进Request类`cookie`方法 +* 修正软删除的`withTrashed`方法 +* 改进多态一对多的预载入查询 +* 改进Query类`column`方法的缓存读取 +* Query类增加`whereBetweenTimeField`方法 +* 改进分组下多个相同路由规则的合并匹配问题 +* 路由类增加`getRule`/`getRuleList`方法获取定义的路由 + +## V5.1.16 (2018-6-7) + +该版本主要修正了社区反馈的一些问题,并对Request类做了进一步规范和优化。 + +* 改进Session类的`boot`方法 +* App类的初始化方法可以单独执行 +* 改进Request类的`param`方法 +* 改进资源路由的变量替换 +* Request类增加`__isset`方法 +* 改进`useGlobalScope`方法对软删除的影响 +* 修正命令行调用 +* 改进Cookie类`init`方法 +* 改进多对多关联删除的返回值 +* 一对多关联写入支持`replace` +* 路由增加`filter`检测方法,用于通过请求参数检测路由是否匹配 +* 取消Request类`session/env/server`方法的`filter`参数 +* 改进关联的指定属性输出 +* 模型删除操作删除后不清空对象数据仅作标记 +* 调整模型的`save`方法返回值为布尔值 +* 修正Request类`isAjax`方法 +* 修正中间件的模块配置读取 +* 取消Request类的请求变量的设置功能 +* 取消请求变量获取的默认修饰符 +* Request类增加`setAction/setModule/setController`方法 +* 关联模型的`delete`方法调用Query类 +* 改进URL生成的域名识别 +* 改进URL检测对已定义路由的域名判断 +* 模型类增加`isExists`和`isForce`方法 +* 软删除的`destroy`和`restore`方法返回值调整为布尔值 + +## V5.1.15 (2018-6-1) + +该版本主要改进了路由缓存的性能和缓存方式设置,增加了JSON格式文件日志的支持,并修正了社区反馈的一些问题。 + +* 容器类增加`exists`方法 仅判断是否存在对象实例 +* 取消配置类的`autoload`方法 +* 改进路由缓存大小提高性能 +* 改进Dispatch类`init`方法 +* 增加`make:validate`指令生成验证器类 +* Config类`get`方法支持默认值参数 +* 修正字段缓存指令 +* 改进App类对`null`数据的返回 +* 改进模型类的`__isset`方法判断 +* 修正`Query`类的`withAggregate`方法 +* 改进`RuleItem`类的`setRuleName`方法 +* 修正依赖注入和参数的冲突问题 +* 修正Db类对第三方驱动的支持 +* 修正模型类查询对象问题 +* 修正File缓存驱动的`has`方法 +* 修正资源路由嵌套 +* 改进Request类对`$_SERVER`变量的读取 +* 改进请求缓存处理 +* 路由缓存支持指定单独的缓存方式和参数 +* 修正资源路由的中间件多次执行问题 +* 修正`optimize:config`指令 +* 文件日志支持`JSON`格式日志保存 +* 修正Db类`connect`方法 +* 改进Log类`write`方法不会自动写入之前日志 +* 模型的关联操作默认启用事务 +* 改进软删除的事件响应 + +## V5.1.14 (2018-5-18) + +该版本主要对底层容器进行了一些优化改进,并增加了路由缓存功能,可以进一步提升路由性能。 + +* 依赖注入的对象参数传入改进 +* 改进核心类的容器实例化 +* 改进日期字段的读取 +* 改进验证类的`getScene`方法 +* 模型的`create`方法和`save`方法支持`replace`操作 +* 改进`Db`类的调用机制 +* App类调整为容器类 +* 改进容器默认绑定 +* `Loader`类增加工厂类的实例化方法 +* 增加路由变量默认规则配置参数 +* 增加路由缓存设计 +* 错误处理机制改进 +* 增加清空路由缓存指令 + + +## V5.1.13 (2018-5-11) + +该版本主要增加了MySQL的XA事务支持,模型事件支持观察者,以及对Facade类的改进。 + +* 改进自动缓存 +* 改进Url生成 +* 修正数据缓存 +* 修正`value`方法的缓存 +* `join`方法和`view`方法的条件支持使用`Expression`对象 +* 改进驱动的`parseKey`方法 +* 改进Request类`host`方法和`domain`方法对端口的处理 +* 模型增加`withEvent`方法用于控制当前操作是否需要执行模型事件 +* 模型`setInc/setDec`方法支持更新事件 +* 模型添加`before_restore/after_restore`事件 +* 增加模型事件观察者 +* 路由增加`mobile`方法设置是否允许手机访问 +* 数据库XA事务支持 +* 改进索引数组查询对`IN`查询的支持 +* 修正`invokeMethod`方法 +* 修正空数据写入返回值的BUG +* redis驱动支持`predis` +* 改进`parseData`方法 +* 改进模块加载 +* App类初始化方法调整 +* 改进数组查询对表达式`Expression`对象支持 +* 改进闭包的依赖注入调用 +* 改进多对多关联的中间表模型更新 +* 增加容器中对象的自定义实例化 + +## V5.1.12 (2018-4-25) + +该版本主要改进了主从查询的及时性,并支持动态设置请求数据。 + +* 支持动态设置请求数据 +* 改进`comment`方法解析 +* 修正App类`__unset`方法 +* 改进url生成的域名绑定 +* 改进主从查询的及时性 +* 修正`value`的数据缓存功能 +* 改进分页类的集合对象方法调用 +* 改进Db类的代码提示 +* SQL日志增加主从标记 + +## V5.1.11 (2018-4-19) + +该版本为安全和修正版本,改进了JSON查询的参数绑定问题和容器类对象实例获取,并包含一处可能的安全隐患,建议更新。 + +* 支持指定JSON数据查询的字段类型 +* 修正`selectInsert`方法 +* `whereColumn`方法支持数组方式 +* 改进容器类`make`方法 +* 容器类`delete`方法支持数组 +* 改进`composer`自动加载 +* 改进模板引擎 +* 修正`like`查询的一处安全隐患 + +## V5.1.10 (2018-4-16) + +该版本为修正版本,修正上一个版本的一些BUG,并增强了`think clear`指令。 + +* 改进`orderField`方法 +* 改进`exists`查询 +* 修改cli模式入口文件位置计算 +* 修正`null`查询 +* 改进`parseTime`方法 +* 修正关联预载入查询 +* 改进`mysql`驱动 +* 改进`think clear`指令 支持 `-c -l -r `选项 +* 改进路由规则对`/`结尾的支持 + +## V5.1.9 (2018-4-12) + +该版本主要是一些改进和修正,并包含一个安全更新,是一个推荐更新版本。 + +* 默认模板渲染规则支持配置保持操作方法名 +* 改进`Request`类的`ip`方法 +* 支持模型软删除字段的默认值定义 +* 改进路由变量规则对中文的支持 +* 使用闭包查询的时候使用`cache(true)` 抛出异常提示 +* 改进`Loader`类`loadComposerAutoloadFiles`方法 +* 改进查询方法安全性 +* 修正路由地址中控制器名驼峰问题 +* 调整上一个版本的`module_init`和`app_begin`的钩子顺序问题 +* 改进CLI命令行执行的问题 +* 修正社区反馈的其它问题 + +## V5.1.8 (2018-4-5) + +该版本主要改进了中间件的域名和模块支持,并同时修正了几个已知问题。 + +* 增加`template.auto_rule` 参数设置默认模板渲染的操作名自动转换规则 +* 默认模板渲染规则改由视图驱动实现 +* 修正路由标识定义 +* 修正控制器路由方法 +* 改进Request类`ip`方法支持自定义代理IP参数 +* 路由注册中间件支持数组方式别名 +* 改进命令行执行下的`composer`自动加载 +* 添加域名中间件注册支持 +* 全局中间件支持模块定义文件 +* Log日志配置支持`close`参数可以全局关闭日志写入 +* 中间件方法中捕获`HttpResponseException`异常 +* 改进中间件的闭包参数传入 +* 改进分组路由的延迟解析 +* 改进URL生成对域名绑定的支持 +* 改进文件缓存和文件日志驱动的并发支持 + +## V5.1.7 (2018-3-28) + +该版本主要修正了路由的一些问题,并改进了查询的安全性。 + +* 支持`middleware`配置文件预先定义中间件别名方便路由调用 +* 修正资源路由 +* 改进`field`方法 自动识别`fieldRaw` +* 增加`Expression`类 +* Query类增加`raw`方法 +* Query类的`field`/ `order` 和` where`方法都支持使用`raw`表达式查询 +* 改进`inc/dec`查询 支持批量更新 +* 改进路由分组 +* 改进Response类`create`方法 +* 改进composer自动加载 +* 修正域名路由的`append`方法 +* 修正操作方法的初始化方法获取不到问题 + +## V5.1.6 (2018-3-26) + +该版本主要改进了路由规则的匹配算法,大幅提升了路由性能。并正式引入了中间件的支持,可以在路由中定义或者全局定义。另外包含了一个安全更新,是一个建议更新版本。 + +* 改进URL生成对路由`ext`方法的支持 +* 改进查询缓存对不同数据库相同表名的支持 +* 改进composer自动加载的性能 +* 改进空路由变量对默认参数的影响 +* mysql的`json`字段查询支持多级 +* Query类增加`option`方法 +* 优化路由匹配 +* 修复验证规则数字键名丢失问题 +* 改进路由Url生成 +* 改进一对一关联预载入查询 +* Request类增加`rootDomain`方法 +* 支持API资源控制器生成 `make:controller --api` +* 优化Template类的标签解析 +* 容器类增加删除和清除对象实例的方法 +* 修正MorphMany关联的`eagerlyMorphToMany`方法一处错误 +* Container类的异常捕获改进 +* Domain对象支持`bind`方法 +* 修正分页参数 +* 默认模板的输出规则不受URL影响 +* 注解路由支持多级控制器 +* Query类增加`getNumRows`方法获取前次操作影响的记录数 +* 改进查询条件的性能 +* 改进模型类`readTransform`方法对序列化类型的处理 +* Log类增加`close`方法可以临时关闭当前请求的日志写入 +* 文件日志方式增加自动清理功能(设置`max_files`参数) +* 修正Query类的`getPk`方法 +* 修正模板缓存的布局开关问题 +* 修正Query类`select`方法的缓存 +* 改进input助手函数 +* 改进断线重连的信息判断 +* 改进正则验证方法 +* 调整语言包的加载顺序 放到`app_init`之前 +* controller类`fetch`方法改为`final` +* 路由地址中的变量支持使用``方式 +* 改进XMLResponse 支持传入编码过的xml内容 +* 修正Query类`view`方法的数组表名支持 +* 改进路由的模型闭包绑定 +* 改进分组变量规则的继承 +* 改进`cli-server`模式下的`composer`自动加载 +* 路由变量规则异常捕获 +* 引入中间件支持 +* 路由定义增加`middleware`方法 +* 增加生成中间件指令`make:middleware` +* 增加全局中间件定义支持 +* 改进`optimize:config`指令对全局中间件的支持 +* 改进config类`has`方法 +* 改进时间查询的参数绑定 +* 改进`inc/dec/exp`查询的安全性 + + +## V5.1.5 (2018-1-31) + +该版本主要增强了数据库的JSON查询,并支持JSON字段的聚合查询,改进了一些性能问题,修正了路由的一些BUG,主要更新如下: + +* 改进数据集查询对`JSON`数据的支持 +* 改进聚合查询对`JSON`字段的支持 +* 模型类增加`getOrFail`方法 +* 改进数据库驱动的`parseKey`方法 +* 改进Query类`join`方法的自关联查询 +* 改进数据查询不存在不生成查询缓存 +* 增加`run`命令行指令启动内置服务器 +* `Request`类`pathinfo`方法改进对`cli-server`支持 +* `Session`类增加`use_lock`配置参数设置是否启用锁机制 +* 优化`File`缓存自动生成空目录的问题 +* 域名及分组路由支持`append`方法传递隐式参数 +* 改进日志的并发写入问题 +* 改进`Query`类的`where`方法支持传入`Query`对象 +* 支持设置单个日志文件的文件名 +* 修正路由规则的域名条件约束 +* `Request`类增加`subDomain`方法用于获取当前子域名 +* `Response`类增加`allowCache`方法控制是否允许请求缓存 +* `Request`类增加`sendData`方法便于扩展 +* 改进`Env`类不依赖`putenv`方法 +* 改进控制台`trace`显示错误 +* 改进`MorphTo`关联 +* 改进完整路由匹配后带斜线访问出错的情况 +* 改进路由的多级分组问题 +* 路由url地址生成支持多级分组 +* 改进路由Url生成的`url_convert`参数的影响 +* 改进`miss`和`auto`路由内部解析 +* 取消预载入关联查询缓存功能 + +## V5.1.4 (2018-1-19) + +该版本主要增强了数据库和模型操作,主要更新如下: + +* 支持设置 `deleteTime`属性为`false` 关闭软删除 +* 模型增加`getError`方法 +* 改进Query类的`getTableFields`/`getFieldsType`方法 支持表名自动获取 +* 模型类`toCollection`方法增加参数指定数据集类 +* 改进`union`查询 +* 关联预载入`with`方法增加缓存参数 +* 改进模型类的`get`和`all`方法的缓存 支持关联缓存 +* 支持`order by field`操作 +* 改进`insertAll`分批写入 +* 改进`json`字段数据支持 +* 增加JSON数据的模型对象化操作 +* 改进路由`ext`参数检测 +* 修正`rule`方法的`method`参数使用 `get|post` 方式注册路由的问题 + +## V5.1.3 (2018-1-12) + +该版本主要改进了路由及调整函数加载顺序,主要更新如下: + +* 增加`env`助手函数; +* 增加`route`助手函数; +* 增加视图路由方法; +* 增加路由重定向方法; +* 路由默认区分最后的目录斜杆(支持设置不区分); +* 调整公共文件和配置文件的加载顺序(可以在配置文件中直接使用助手函数); +* 视图类增加`filter`方法设置输出过滤; +* `view`助手函数增加`filter`参数; +* 改进缓存生成指令; +* Session类的`get`方法支持获取多级; +* Request类`only`方法支持指定默认值; +* 改进路由分组; +* 修正使用闭包查询的时候自动数据缓存出错的情况; +* 废除`view_filter`钩子位置; +* 修正分组下面的资源路由; +* 改进session驱动; + +## V5.1.2 (2018-1-8) + +该版本改进了配置类及数据库类,主要更新如下: + +* 修正嵌套路由分组; +* 修正自定义模板标签界定符后表达式语法出错的情况; +* 修正自关联的多次调用问题; +* 修正数组查询的`null`条件查询; +* 修正Query类的`order`及`field`的一处可能的BUG; +* 配置参数设置支持三级; +* 配置对象支持`ArrayAccess`; +* App类增加`path`方法用于设置应用目录; +* 关联定义增加`selfRelation`方法用于设置是否为自关联; + +## V5.1.1 (2018-1-3) + +修正一些反馈的BUG,包括: + +* 修正Cookie类存取数组的问题 +* 修正Controller的`fetch`方法 +* 改进跨域请求 +* 修正`insertAll`方法 +* 修正`chunk`方法 + +## V5.1.0 (2018-1-1) + +主要更新如下: + +* 增加注解路由支持 +* 路由支持跨域请求设置 +* 增加`app_dispatch`钩子位置 +* 修正多对多关联的`detach`方法 +* 修正软删除的`destroy`方法 +* Cookie类`httponly`参数默认为false +* 日志File驱动增加`single`参数配置记录同一个文件(不按日期生成) +* 路由的`ext`和`denyExt`方法支持不传任何参数 +* 改进模型的`save`方法对`oracle`的支持 +* Query类的`insertall`方法支持配合`data`和`limit`方法 +* 增加`whereOr`动态查询支持 +* 日志的ip地址记录改进 +* 模型`saveAll`方法支持`isUpdate`方法 +* 改进`Pivot`模型的实例化操作 +* 改进Model类的`data`方法 +* 改进多对多中间表模型类 +* 模型增加`force`方法强制更新所有数据 +* Hook类支持设置入口方法名称 +* 改进验证类 +* 改进`hasWhere`查询的数据重复问题 +* 模型的`saveall`方法返回数据集对象 +* 改进File缓存的`clear`方法 +* 缓存添加统一的序列化机制 +* 改进泛三级域名的绑定 +* 改进泛域名的传值和取值 +* Request类增加`panDomain`方法 +* 改进废弃字段判断 +* App类增加`create`方法用于实例化应用类库 +* 容器类增加`has`方法 +* 改进多数据库切换连接 +* 改进断线重连的异常捕获 +* 改进模型类`buildQuery`方法 +* Query类增加`unionAll`方法 +* 关联统计功能增强(支持Sum/Max/Min/Avg) +* 修正延迟写入 +* chunk方法支持复合主键 +* 改进JSON类型的写入 +* 改进Mysql的insertAll方法 +* Model类`save`方法改进复合主键包含自增的情况 +* 改进Query类`inc`和`dec`方法的关键字处理 +* File缓存inc和dec方法保持原来的有效期 +* 改进redis缓存的有效期判断 +* 增加checkRule方法用于单独数据的多个验证规则 +* 修正setDec方法的延迟写入 +* max和min方法增加force参数 +* 二级配置参数区分大小写 +* 改进join方法自关联的问题 +* 修正关联模型自定义表名的情况 +* Query类增加getFieldsType和getTableFields方法 +* 取消视图替换功能及view_replace_str配置参数 +* 改进域名绑定模块后的额外路由规则问题 +* 改进mysql的insertAll方法 +* 改进insertAll方法写入json字段数据的支持 +* 改进redis长连接多编号库的情况 + +## RC3版本(2017-11-6) + +主要更新如下: + +* 改进redis驱动的`get`方法 +* 修正Query类的`alias`方法 +* `File`类错误信息支持多语言 +* 修正路由的额外参数解析 +* 改进`whereTime`方法 +* 改进Model类`getAttr`方法 +* 改进App类的`controller`和`validate`方法支持多层 +* 改进`HasManyThrough`类 +* 修正软删除的`restore`方法 +* 改进`MorpthTo`关联 +* 改进数据库驱动类的`parseKey`方法 +* 增加`whereField`动态查询方法 +* 模型增加废弃字段功能 +* 改进路由的`after`行为检查和`before`行为机制 +* 改进路由分组的检查 +* 修正mysql的`json`字段查询 +* 取消Connection类的`quote`方法 +* 改进命令行的支持 +* 验证信息支持多语言 +* 修正路由模型绑定 +* 改进参数绑定类型对枚举类型的支持 +* 修正模板的`{$Think.version} `输出 +* 改进模板`date`函数解析 +* 改进`insertAll`方法支持分批执行 +* Request类`host`方法支持反向代理 +* 改进`JumpResponse`支持区分成功和错误模板 +* 改进开启类库后缀后的关联外键自动识别问题 +* 修正一对一关联的JOIN方式预载入查询问题 +* Query类增加`hidden`方法 + +## RC2版本(2017-10-17) + +主要更新如下: + +* 修正视图查询 +* 修正资源路由 +* 修正`HasMany`关联 修正`where`方法的闭包查询 +* 一对一关联绑定属性到父模型后 关联属性不再保留 +* 修正应用的命令行配置文件读取 +* 改进`Connection`类的`getCacheKey`方法 +* 改进文件上传的非法图像异常 +* 改进验证类的`unique`规则 +* Config类`get`方法支持获取一级配置 +* 修正count方法对`fetchSql`的支持 +* 修正mysql驱动对`socket`支持 +* 改进Connection类的`getRealSql`方法 +* 修正`view`助手函数 +* Query类增加`leftJoin` `rightJoin` 和 `fullJoin`方法 +* 改进app_namespace的获取 +* 改进`append`方法对一对一`bind`属性的支持 +* 改进关联的`saveall`方法的返回值 +* 路由标识设置异常修复 +* 改进Route类`rule`方法 +* 改进模型的`table`属性设置 +* 改进composer autofile的加载顺序 +* 改进`exception_handle`配置对闭包的支持 +* 改进app助手函数增加参数 +* 改进composer的加载路径判断 +* 修正路由组合变量的URL生成 +* 修正路由URL生成 +* 改进`whereTime`查询并支持扩展规则 +* File类的`move`方法第二个参数支持`false` +* 改进Config类 +* 改进缓存类`remember`方法 +* 惯例配置文件调整 Url类当普通模式参数的时候不做`urlencode`处理 +* 取消`ROOT_PATH`和`APP_PATH`常量定义 如需更改应用目录 自己重新定义入口文件 +* 增加`app_debug`的`Env`获取 +* 修正泛域名绑定 +* 改进查询表达式的解析机制 +* mysql增加`regexp`查询表达式 支持正则查询 +* 改进查询表达式的异常判断 +* 改进model类的`destroy`方法 +* 改进Builder类 取消`parseValue`方法 +* 修正like查询的参数绑定问题 +* console和start文件移出核心纳入应用库 +* 改进Db类主键删除方法 +* 改进泛域名绑定模块 +* 取消`BIND_MODULE`常量 改为在入口文件使用`bind`方法设置 +* 改进数组查询 +* 改进模板渲染的异常处理 +* 改进控制器基类的架构方法参数 +* 改进Controller类的`success`和`error`方法 +* 改进对浏览器`JSON-Handle`插件的支持 +* 优化跳转模板的移动端显示 +* 修正模型查询的`chunk`方法对时间字段的支持 +* 改进trace驱动 +* Collection类增加`push`方法 +* 改进Redis Session驱动 +* 增加JumpResponse驱动 + + +## RC1(2017-9-8) + +主要新特性为: + +* 引入容器和Facade支持 +* 依赖注入完善和支持更多场景 +* 重构的(对象化)路由 +* 配置和路由目录独立 +* 取消系统常量 +* 助手函数增强 +* 类库别名机制 +* 模型和数据库增强 +* 验证类增强 +* 模板引擎改进 +* 支持PSR-3日志规范 +* RC1版本取消了5.0多个字段批量数组查询的方式 \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..2255550 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,27 @@ +版权所有 (c) 2016~2017,河源市卓锐科技有限公司保留所有权利。 + +DolphinPHP 用户协议 + +版权所有 (c) 2016~2017,河源市卓锐科技有限公司保留所有权利。 + +感谢您选择DolphinPHP,希望我们的努力能为您提供一个极简极致极速的PHP快速开发解决方案。 + +用户须知:本协议是您与河源市卓锐科技有限公司(以下简称卓锐公司)之间关于您使用DolphinPHP产品及服务的法律协议。无论您是个人或组织、盈利与否、用途如何(包括以学习和研究为目的),均需仔细阅读本协议,包括免除或者限制卓锐公司责任的免责条款及对您的权利限制。请您审阅并接受或不接受本服务条款。如您不同意本服务条款及/或卓锐公司随时对其的修改,您应不使用或主动取消DolphinPHP产品。否则,您的任何对DolphinPHP的相关服务的注册、登陆、下载、查看等使用行为将被视为您对本服务条款全部的完全接受,包括接受卓锐公司对服务条款随时所做的任何修改。 + +本服务条款一旦发生变更, 卓锐公司将在产品官网上公布修改内容。修改后的服务条款一旦在网站公布即有效代替原来的服务条款。您可随时登陆官网查阅最新版服务条款。如果您选择接受本条款,即表示您同意接受协议各项条件的约束。如果您不同意本服务条款,则不能获得使用本服务的权利。您若有违反本条款规定,卓锐公司有权随时中止或终止您对DolphinPHP产品的使用资格并保留追究相关法律责任的权利。 + +在理解、同意、并遵守本协议的全部条款后,方可开始使用DolphinPHP产品。您也可能与卓锐公司直接签订另一书面协议,以补充或者取代本协议的全部或者任何部分。 + +卓锐公司拥有DolphinPHP的全部知识产权,包括商标和著作权。本软件只供许可协议,并非出售。卓锐公司只允许您在遵守本协议各项条款的情况下复制、下载、安装、使用或者以其他方式受益于本软件的功能或者知识产权。 + +个人非商业用途可免费使用(但不包括其衍生产品、插件或者服务),也可以根据个人实际情况选择是否购买授权。 + +非个人用户(泛指非个人的团体,如企业、政府单位、教育机构、协会团体、厂矿、工作室等)必须购买软件授权后方可使用。 + +您可以在协议规定的约束和限制范围内修改DolphinPHP以适应您的网站要求,但免费版必须保留软件版本信息的正常显示及相关连接正常。 + +禁止去除DolphinPHP源码里的版权信息,商业授权版本可去除后台界面及前台界面的相关版权信息。 + +禁止在DolphinPHP整体或任何部分基础上发展任何派生版本、修改版本或第三方版本用于重新分发。 + +未经官方许可,不得对本软件或与之关联的商业授权进行出租、出售、抵押或发放子许可证。 \ No newline at end of file diff --git a/application/.DS_Store b/application/.DS_Store new file mode 100644 index 0000000..f953888 Binary files /dev/null and b/application/.DS_Store differ diff --git a/application/.htaccess b/application/.htaccess new file mode 100644 index 0000000..3418e55 --- /dev/null +++ b/application/.htaccess @@ -0,0 +1 @@ +deny from all \ No newline at end of file diff --git a/application/activity/admin/Award.php b/application/activity/admin/Award.php new file mode 100644 index 0000000..cfe01b9 --- /dev/null +++ b/application/activity/admin/Award.php @@ -0,0 +1,191 @@ + +// +---------------------------------------------------------------------- +namespace app\activity\admin; + +use think\Db; +use app\admin\controller\Admin; +use app\common\builder\ZBuilder; +use app\activity\model\Award as AwardModel; +use app\member\model\Member as MemberModel; + + +class Award extends Admin +{ + //主播列表 + public function index(){ + // 获取查询条件 + $map = $this->getMap(); + + // 排序 + $order = $this->getOrder('sort asc'); + + // 数据列表 + $map[] = ['id','neq',9]; + $data_list = AwardModel::where($map)->order($order)->paginate(); + + //加载模板 + return ZBuilder::make('table') + ->setPageTitle('奖品设置')// 设置页面标题 + ->setTableName('award') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['sort', '排序(升序排序)','text.edit'], + ['thumb', '奖品图片','picture'], + ['title', '奖品名称'], + ['jp_number', '开奖累计次数'], + ['open_num', '已开奖次数'], + ['right_button', '操作', 'btn'] + ]) + //->addTopButton('add',['class'=>'btn btn-primary','icon' => 'fa fa-plus-circle','title'=>'新增', 'href' => url('add')],['area' => ['600px', '60%']]) + ->addTopButton('export',[ + 'title' => '设置抽奖扣除金币数量', + 'icon' => 'fa fa-fw fa-minus', + 'class' => 'btn btn-info export confirm', + 'href' => url('setgoldcoin'), + 'data-url' => url('setgoldcoin') + ],['area' => ['600px', '60%']]) // 导出 + ->addRightButton('edit',['title'=>'編輯','icon'=>'fa fa-pencil','href'=>url('edit',['id' => '__id__'])],['area' => ['600px', '60%']]) + //->addRightButton('delete', ['title' => '删除', 'icon' => 'fa fa-remove', 'href' => url('delete', ['id' => '__id__'])]) + ->setRowList($data_list)// 设置表格数据 + ->fetch(); // 渲染模板 + } + + public function setgoldcoin(){ + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['jp_number'])){ + return $this->error('请输入需要扣除的金币'); + } + + $data['update_time'] = time(); + $result = AwardModel::update($data); + if($result){ + $this->success('修改成功', url('index'),'_parent_reload'); + }else{ + $this->error('修改失败'); + } + } else { + $info = AwardModel::where('id',9)->find(); + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['hidden', 'id'], + ['text', 'jp_number', '扣除金币'], + ]) + ->setFormData($info) + ->fetch(); + } + } + + + //添加 + public function add(){ + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['title'])){ + return $this->error('奖品名称不能为空'); + } + + $result = AwardModel::create($data); + if($result){ + $this->success('新增成功', url('index'),'_parent_reload'); + }else{ + $this->error('新增失败'); + } + }else{ + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['image', 'thumb', '奖品图片', '必上传'], + ['text', 'title', '奖品名称'], + ['text', 'jp_number', '开奖累计次数'], + ['text', 'sort', '排序(升序排序)','',9999], + ]) + ->fetch(); + } + } + + //编辑 + public function edit($id=null){ + if ($id === null) return $this->error('缺少参数'); + + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['title'])){ + return $this->error('奖品名称不能为空'); + } + $data['update_time'] = time(); + $result = AwardModel::update($data); + if($result){ + $this->success('修改成功', url('index'),'_parent_reload'); + }else{ + $this->error('修改失败'); + } + } else { + $info = AwardModel::get($id); + + if($id==1){ + $formitems = [ + ['hidden', 'id'], + ['image', 'thumb', '奖品图片', '必上传'], + ['text', 'title', '奖品名称'], + ['text', 'jp_number', '开奖累计次数(谢谢参与中奖次数)'], + ['text', 'sort', '排序(升序排序)','',9999], + ]; + } + + if($id==2 || $id==3 || $id==4){ + $formitems = [ + ['hidden', 'id'], + ['image', 'thumb', '奖品图片', '必上传'], + ['text', 'title', '奖品名称'], + ['text', 'sort', '排序(升序排序)','',9999], + ]; + } + + if($id==5 || $id==6 || $id==7 || $id==8){ + $formitems = [ + ['hidden', 'id'], + ['image', 'thumb', '奖品图片', '必上传'], + ['text', 'title', '奖品名称'], + ['text', 'jp_number', '开奖累计次数(抽奖累次次数)'], + ['text', 'sort', '排序(升序排序)','',9999], + ]; + } + + return ZBuilder::make('form') + ->addFormItems($formitems) + ->setFormData($info) + ->fetch(); + } + } + + //删除 + public function delete($id = null) + { + if ($id === null) { + $this->error('缺少参数'); + } + + $data = AwardModel::where('id',$id)->find(); + if (empty($data)) {$this->error('删除成功!'); } + + $result = AwardModel::where('id',$id)->delete(); + if($result) { + return $this->success('删除成功!'); + }else{ + return $this->error('删除失败!'); + } + } + + +} \ No newline at end of file diff --git a/application/activity/admin/Awardmessage.php b/application/activity/admin/Awardmessage.php new file mode 100644 index 0000000..8f8a455 --- /dev/null +++ b/application/activity/admin/Awardmessage.php @@ -0,0 +1,125 @@ + +// +---------------------------------------------------------------------- +namespace app\activity\admin; + +use think\Db; +use app\admin\controller\Admin; +use app\common\builder\ZBuilder; +use app\activity\model\AwardMessage as AwardMessageModel; +use app\member\model\Member as MemberModel; + + +class Awardmessage extends Admin +{ + //列表 + public function index(){ + // 获取查询条件 + $map = $this->getMap(); + + // 排序 + $order = $this->getOrder('create_time dasc'); + + // 数据列表 + $data_list = AwardMessageModel::where($map)->order($order)->paginate(); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['daterange', 'create_time', '添加时间', 'between'], + ]) + ->setPageTitle('中奖信息列表')// 设置页面标题 + ->setTableName('award_message') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['title', '中奖信息'], + ['create_time', '添加时间','datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('add',['class'=>'btn btn-primary','icon' => 'fa fa-plus-circle','title'=>'新增', 'href' => url('add')],['area' => ['700px', '60%']]) + ->addRightButton('edit',['title'=>'編輯','icon'=>'fa fa-pencil','href'=>url('edit',['id' => '__id__'])],['area' => ['700px', '60%']]) + ->addRightButton('delete', ['title' => '删除', 'icon' => 'fa fa-remove', 'href' => url('delete', ['id' => '__id__'])]) + ->setRowList($data_list)// 设置表格数据 + ->fetch(); // 渲染模板 + } + + //添加主播 + public function add(){ + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['title'])){ + return $this->error('中奖信息不能为空'); + } + + $result = AwardMessageModel::create($data); + if($result){ + $this->success('新增成功', url('index'),'_parent_reload'); + }else{ + $this->error('新增失败'); + } + }else{ + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['textarea', 'title', '中奖信息'], + ]) + ->fetch(); + } + } + + //编辑主播 + public function edit($id=null){ + if ($id === null) return $this->error('缺少参数'); + + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['title'])){ + return $this->error('中奖信息不能为空'); + } + $data['update_time'] = time(); + $result = AwardMessageModel::update($data); + if($result){ + $this->success('修改成功', url('index'),'_parent_reload'); + }else{ + $this->error('修改失败'); + } + } else { + $info = AwardMessageModel::get($id); + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['hidden', 'id'], + ['textarea', 'title', '中奖信息'], + ]) + ->setFormData($info) + ->fetch(); + } + } + + //删除主播 + public function delete($id = null) + { + if ($id === null) { + $this->error('缺少参数'); + } + + $data = AwardMessageModel::where('id',$id)->find(); + if (empty($data)) {$this->error('删除成功!'); } + + $result = AwardMessageModel::where('id',$id)->delete(); + if($result) { + return $this->success('删除成功!'); + }else{ + return $this->error('删除失败!'); + } + } + + +} \ No newline at end of file diff --git a/application/activity/admin/Awardwinnings.php b/application/activity/admin/Awardwinnings.php new file mode 100644 index 0000000..3b42a2e --- /dev/null +++ b/application/activity/admin/Awardwinnings.php @@ -0,0 +1,83 @@ + +// +---------------------------------------------------------------------- +namespace app\activity\admin; + +use think\Db; +use app\admin\controller\Admin; +use app\common\builder\ZBuilder; +use app\activity\model\AwardWinnings as AwardWinningsModel; +use app\activity\model\Award as AwardModel; +use app\member\model\Member as MemberModel; + + +class Awardwinnings extends Admin +{ + public function index(){ + // 获取 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('id desc'); + //数据 + $data_list = AwardWinningsModel::where($map)->order($order)->paginate(); + if(!empty($data_list)){ + foreach ($data_list as $key=>$val){ + + $uinfo = MemberModel::where('id',$val['uid'])->find(); + $is_vip = '普通会员'; + switch ($uinfo['is_vip']) { + case 1: + $is_vip = '铜牌会员'; + break; + case 2: + $is_vip = '银牌会员'; + break; + case 3: + $is_vip = '金牌会员'; + break; + } + $data_list[$key]['nickname'] = $uinfo['nickname'].'('.$is_vip.')'; + if(!empty($val['address'])){ + $data_list[$key]['address'] = "姓名:".$val['name']."
电话:".$val['phone']."
邮寄地址:".$val['address']; + }else{ + $data_list[$key]['address'] = "暂无"; + } + + } + } + // 分页数据 + $page = $data_list->render(); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['select', 'uid', '所属用户', 'eq', '', MemberModel::where('is_delete',0)->column('id,nickname')], + ['select', 'award_id', '所属奖品', 'eq', '', AwardModel::column('id,title')], + ['text', 'name', '姓名', 'like'], + ['text', 'phone', '电话', 'like'], + ['text', 'address', '邮寄地址', 'like'], + ['daterange','create_time','抽奖时间','between'], + ]) + ->setPageTitle('金币产出记录') // 设置页面标题 + ->setTableName('member') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['nickname', '所属用户','text'], + ['award_title', '奖品','text'], + ['award_thumb', '奖品图片','picture'], + ['usegoldcoin', '消耗金币','text'], + ['create_time', '抽奖时间','datetime'], + ['address', '邮寄地址','text'], + ]) + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } +} \ No newline at end of file diff --git a/application/activity/admin/Driftbottle.php b/application/activity/admin/Driftbottle.php new file mode 100644 index 0000000..d685854 --- /dev/null +++ b/application/activity/admin/Driftbottle.php @@ -0,0 +1,131 @@ + +// +---------------------------------------------------------------------- +namespace app\activity\admin; + +use think\Db; +use app\admin\controller\Admin; +use app\common\builder\ZBuilder; +use app\activity\model\DriftBottle as DriftBottleModel; +use app\member\model\Member as MemberModel; +use app\member\model\MemberService as MemberServiceModel; + + +class Driftbottle extends Admin +{ + //主播列表 + public function index(){ + // 获取查询条件 + $map = $this->getMap(); + + // 排序 + $order = $this->getOrder('create_time desc'); + + // 数据列表 + //数据 + $data_list = DriftBottleModel::where($map)->order($order)->paginate(); + if(!empty($data_list)){ + foreach ($data_list as $key=>$val){ + + $uinfo = MemberModel::where('id',$val['uid'])->find(); + $is_vip = '普通会员'; + switch ($uinfo['is_vip']) { + case 1: + $is_vip = '铜牌会员'; + break; + case 2: + $is_vip = '银牌会员'; + break; + case 3: + $is_vip = '金牌会员'; + break; + } + $data_list[$key]['nickname'] = $uinfo['nickname'].'('.$is_vip.')'; + } + } + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['select', 'uid', '所属用户', 'eq', '', MemberModel::where('is_delete',0)->column('id,nickname')], + ['text', 'message', '漂流瓶信息', 'like'], + ['daterange','create_time','发布时间','between'], + ]) + ->setPageTitle('漂流瓶信息列表')// 设置页面标题 + ->setTableName('member_driftbottle') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['nickname', '所属用户','text'], + ['message', '漂流瓶信息'], + ['create_time', '发布时间','datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addRightButton('delete', ['title' => '删除', 'icon' => 'fa fa-remove', 'href' => url('delete', ['id' => '__id__'])]) + ->setRowList($data_list)// 设置表格数据 + ->fetch(); // 渲染模板 + } + + //删除 + public function delete($id = null) + { + if ($id === null) { + $this->error('缺少参数'); + } + + $data = DriftBottleModel::where('id',$id)->find(); + if (empty($data)) {$this->error('删除成功!'); } + + $result = DriftBottleModel::where('id',$id)->delete(); + if($result) { + return $this->success('删除成功!'); + }else{ + return $this->error('删除失败!'); + } + } + + + + public function portraitphotography(){ + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['title'])){ + return $this->error('标题不能为空'); + } + $data['update_time'] = time(); + $data['create_time'] = !empty($data['create_time'])?strtotime($data['create_time']):time(); + + $result = MemberServiceModel::update($data); + if($result){ + $this->success('修改成功', url('portraitphotography'),'_parent_reload'); + }else{ + $this->error('修改失败'); + } + } else { + $info = MemberServiceModel::get(12); + + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['hidden', 'id'], + ['image', 'thumb', '封面图', '必上传'], + ['text:6', 'title', '标题'], + ['text:6', 'price', '购买金额'], + ['textarea:4', 'box_url', '网盘地址'], + ['textarea:4', 'box_psw', '网盘密码(没有密码则不填写)'], + ['textarea:4', 'summary', '写真描述'], + ['datetime', 'create_time', '发布时间'], + ['ueditor', 'message', '写真详情说明'], + ]) + ->setFormData($info) + ->fetch(); + } + } + +} \ No newline at end of file diff --git a/application/activity/admin/Moneytree.php b/application/activity/admin/Moneytree.php new file mode 100644 index 0000000..031d4e7 --- /dev/null +++ b/application/activity/admin/Moneytree.php @@ -0,0 +1,222 @@ + +// +---------------------------------------------------------------------- +namespace app\activity\admin; + +use think\Db; +use app\admin\controller\Admin; +use app\common\builder\ZBuilder; +use app\activity\model\MoneyTree as MoneyTreeModel; +use app\activity\model\MoneyTreeLogs as MMoneyTreeLogsModel; +use app\member\model\Member as MemberModel; + + +class Moneytree extends Admin +{ + //主播列表 + public function index(){ + // 获取查询条件 + $map = $this->getMap(); + + // 排序 + $order = $this->getOrder('tree_grade asc'); + + // 数据列表 + $data_list = MoneyTreeModel::where($map)->order($order)->paginate(); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['daterange', 'create_time', '添加时间', 'between'], + ]) + ->setPageTitle('摇钱树等级')// 设置页面标题 + ->setTableName('moneytree') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['thumb', '摇钱树图片','picture'], + ['tree_grade', '等级'], + ['integral_num', '24小时产生的金币'], + ['share_num', '分享人数'], + ['create_time', '添加时间','datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('add',['class'=>'btn btn-primary','icon' => 'fa fa-plus-circle','title'=>'新增', 'href' => url('add')],['area' => ['600px', '60%']]) + ->addTopButton('export',[ + 'title' => '摇钱树规则说明', + 'icon' => 'fa fa-fw fa-file-text-o', + 'class' => 'btn btn-info export confirm', + 'href' => url('moneytreeequity'), + 'data-url' => url('moneytreeequity') + ],['area' => ['800px', '80%']]) // 导出 + ->addRightButton('edit',['title'=>'編輯','icon'=>'fa fa-pencil','href'=>url('edit',['id' => '__id__'])],['area' => ['600px', '60%']]) + ->addRightButton('delete', ['title' => '删除', 'icon' => 'fa fa-remove', 'href' => url('delete', ['id' => '__id__'])]) + ->setRowList($data_list)// 设置表格数据 + ->fetch(); // 渲染模板 + } + + //摇钱树规则说明 + public function moneytreeequity(){ + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['message'])){ + return $this->error('摇钱树规则说明不能为空'); + } + + $data['update_time'] = time(); + $result = MMoneyTreeLogsModel::update($data); + if($result){ + $this->success('修改成功', url('index'),'_parent_reload'); + }else{ + $this->error('修改失败'); + } + } else { + $info = MMoneyTreeLogsModel::where('id',1)->find(); + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['hidden', 'id'], + ['ueditor', 'message', '摇钱树规则说明'], + ]) + ->setFormData($info) + ->fetch(); + } + } + + //添加主播 + public function add(){ + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['tree_grade'])){ + return $this->error('等级不能为空'); + } + + $result = MoneyTreeModel::create($data); + if($result){ + $this->success('新增成功', url('index'),'_parent_reload'); + }else{ + $this->error('新增失败'); + } + }else{ + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['image', 'thumb', '摇钱树图片', '必上传'], + ['text', 'tree_grade', '等级'], + ['text', 'integral_num', '24小时产生的金币'], + ['text', 'share_num', '分享人数'], + ]) + ->fetch(); + } + } + + //编辑主播 + public function edit($id=null){ + if ($id === null) return $this->error('缺少参数'); + + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['tree_grade'])){ + return $this->error('等级不能为空'); + } + $data['update_time'] = time(); + $result = MoneyTreeModel::update($data); + if($result){ + $this->success('修改成功', url('index'),'_parent_reload'); + }else{ + $this->error('修改失败'); + } + } else { + $info = MoneyTreeModel::get($id); + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['hidden', 'id'], + ['image', 'thumb', '摇钱树图片', '必上传'], + ['text', 'tree_grade', '等级'], + ['text', 'integral_num', '24小时产生的金币'], + ['text', 'share_num', '分享人数'], + ]) + ->setFormData($info) + ->fetch(); + } + } + + //删除主播 + public function delete($id = null) + { + if ($id === null) { + $this->error('缺少参数'); + } + + $data = MoneyTreeModel::where('id',$id)->find(); + if (empty($data)) {$this->error('删除成功!'); } + + $result = MoneyTreeModel::where('id',$id)->delete(); + if($result) { + return $this->success('删除成功!'); + }else{ + return $this->error('删除失败!'); + } + } + + + + public function moneytreelogs(){ + // 获取 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('id desc'); + $map[] = ['id','neq',1]; + //数据 + $data_list = MMoneyTreeLogsModel::where($map)->order($order)->paginate(); + if(!empty($data_list)){ + foreach ($data_list as $key=>$val){ + + $uinfo = MemberModel::where('id',$val['uid'])->find(); + $is_vip = '普通会员'; + switch ($uinfo['is_vip']) { + case 1: + $is_vip = '铜牌会员'; + break; + case 2: + $is_vip = '银牌会员'; + break; + case 3: + $is_vip = '金牌会员'; + break; + } + $data_list[$key]['nickname'] = $uinfo['nickname'].'('.$is_vip.')'; + } + } + // 分页数据 + $page = $data_list->render(); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['select', 'uid', '所属用户', 'eq', '', MemberModel::where('is_delete',0)->column('id,nickname')], + ['daterange','create_time','时间','between'], + ]) + ->setPageTitle('金币产出记录') // 设置页面标题 + ->setTableName('member') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['uid', 'UID','text'], + ['nickname', '所属用户','text'], + ['tree_grade', '摇钱树等级','text'], + ['integral_num', '产出金币','text'], + ['create_time', '产出时间','datetime'], + ['status', '状态','text','',[1=>'待领取',2=>'已领取']], + ]) + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } +} \ No newline at end of file diff --git a/application/activity/common.php b/application/activity/common.php new file mode 100644 index 0000000..ea404c9 --- /dev/null +++ b/application/activity/common.php @@ -0,0 +1,13 @@ + 'activity', + // 模块标题[必填] + 'title' => '活动', + // 模块唯一标识[必填],格式:模块名.开发者标识.module + 'identifier' => 'activity.maurylee.module', + // 模块图标[选填] + 'icon' => 'fa fa-fw fa-font', + // 模块描述[选填] + 'description' => '活动模块', + // 开发者[必填] + 'author' => 'maurylee', + // 开发者网址[选填] + 'author_url' => '', + // 版本[必填],格式采用三段式:主版本号.次版本号.修订版本号 + 'version' => '1.0.0', + // 模块依赖[可选],格式[[模块名, 模块唯一标识, 依赖版本, 对比方式]] + 'need_module' => [], + // 插件依赖[可选],格式[[插件名, 插件唯一标识, 依赖版本, 对比方式]] + 'need_plugin' => [], + // 数据表[有数据库表时必填] + 'tables' => [], + // 原始数据库表前缀 + // 用于在导入模块sql时,将原有的表前缀转换成系统的表前缀 + // 一般模块自带sql文件时才需要配置 + 'database_prefix' => '', + + + //模块参数配置 + 'config' => [], + + + // 行为配置 + 'action' => [], + + + // 授权配置 + 'access' => [], +]; + diff --git a/application/activity/menus.php b/application/activity/menus.php new file mode 100644 index 0000000..fee4358 --- /dev/null +++ b/application/activity/menus.php @@ -0,0 +1,29 @@ + '活动', + 'icon' => 'fa fa-fw fa-font', + 'url_type' => 'module_admin', + 'url_value' => 'activity/index/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [], + ], + +]; + diff --git a/application/activity/model/Award.php b/application/activity/model/Award.php new file mode 100644 index 0000000..02bc144 --- /dev/null +++ b/application/activity/model/Award.php @@ -0,0 +1,26 @@ +select(); + if(!empty($list)){ + foreach ($list as $k=>$v){ + $share_num = intval($v['share_num']); + if($sharenum >= $share_num){ + $integralnum = $v['integral_num']; + } + } + } + + return $integralnum; + } + + //根据用户分享人数,获得摇钱树等级 + public static function gettreegrade($sharenum){ + $treegrade = 0; + + $list = self::order('tree_grade asc')->select(); + if(!empty($list)){ + foreach ($list as $k=>$v){ + $share_num = intval($v['share_num']); + if($sharenum >= $share_num){ + $treegrade = $v['tree_grade']; + } + } + } + + return $treegrade; + } +} diff --git a/application/activity/model/MoneyTreeLogs.php b/application/activity/model/MoneyTreeLogs.php new file mode 100644 index 0000000..237fc70 --- /dev/null +++ b/application/activity/model/MoneyTreeLogs.php @@ -0,0 +1,44 @@ + + */ + public static function addGetLog($uid,$tree_grade,$integral_num){ + $res = self::create([ + 'uid'=>$uid, + 'tree_grade'=>$tree_grade, + 'integral_num'=>$integral_num, + 'create_time'=>time(), + 'update_time'=>time(), + ]); + return $res; + } + +} diff --git a/application/activity/validate/Category.php b/application/activity/validate/Category.php new file mode 100644 index 0000000..6e11cb1 --- /dev/null +++ b/application/activity/validate/Category.php @@ -0,0 +1,31 @@ + +// +---------------------------------------------------------------------- + +namespace app\other\validate; + +use think\Validate; + +/** + * 商品分类 + * @author 王海鑫 + */ +class Category extends Validate +{ + //定义验证规则 + protected $rule = [ + 'title|分类名称' => 'require' + ]; + + protected $message = [ + 'title.require' => '分类名称不能为空', + ]; + +} \ No newline at end of file diff --git a/application/activity/view/admin/category/index.html b/application/activity/view/admin/category/index.html new file mode 100644 index 0000000..508f310 --- /dev/null +++ b/application/activity/view/admin/category/index.html @@ -0,0 +1,207 @@ +{extend name="$_admin_base_layout" /} + +{block name="plugins-css"} + +{/block} + +{block name="content"} +
+ +

提示:
1:按住表头可拖动类别,调整后点击【保存类别】。
2:类别只沿用到二级类别。

+
+ +
+
+
+ {notempty name="tab_nav"} + + {else/} +
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+

{$page_title}

+
+ {/notempty} +
+
+ {notempty name="menus"} +
+
+
+
+ 新增 + + + + + + + + +
+
+
+
+ + + {/notempty} + + {notempty name="modules"} +
+ +
+
+
+ {volist name="modules" id="module"} +
+ + {$module.title} +
+ {/volist} +
+
+
+
+ {/notempty} +
+
+
+
+
+ +{/block} + +{block name="script"} + + + +{/block} diff --git a/application/activity/view/admin/layout.html b/application/activity/view/admin/layout.html new file mode 100644 index 0000000..4880c5b --- /dev/null +++ b/application/activity/view/admin/layout.html @@ -0,0 +1,438 @@ + + + + + + {block name="page-title"}{$page_title|default='后台'} | {:config('web_site_title')} - CCU{/block} + + + + + + + + + + + {notempty name="_css_files"} + {eq name="Think.config.minify_status" value="1"} + + {else/} + {volist name="_css_files" id="css"} + {:load_assets($css)} + {/volist} + {/eq} + {/notempty} + {block name="plugins-css"}{/block} + + {eq name="Think.config.minify_status" value="1"} + + {else/} + + + + + + + + {/eq} + + {block name="style"}{/block} + {notempty name="Think.get._pop"} + + {/notempty} + + + + + + + + + + + + + + + +{eq name="Think.config.minify_status" value="1"} + +{else/} + + + + + + + + + + + + + + + + + + +{/eq} + + + +{notempty name="_js_files"} +{eq name="Think.config.minify_status" value="1"} + +{else/} +{volist name="_js_files" id="js"} +{:load_assets($js, 'js')} +{/volist} +{/eq} +{/notempty} + + + + + + +{block name="script"}{/block} + + + \ No newline at end of file diff --git a/application/admin/controller/Action.php b/application/admin/controller/Action.php new file mode 100644 index 0000000..f1a3c82 --- /dev/null +++ b/application/admin/controller/Action.php @@ -0,0 +1,73 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\exception\DbException + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 数据列表 + $data_list = ActionModel::where($map)->order('id desc')->paginate(); + // 所有模块的名称和标题 + $list_module = ModuleModel::getModule(); + + // 新增或编辑页面的字段 + $fields = [ + ['hidden', 'id'], + ['select', 'module', '所属模块', '', $list_module], + ['text', 'name', '行为标识', '由英文字母和下划线组成'], + ['text', 'title', '行为名称', ''], + ['textarea', 'remark', '行为描述'], + ['textarea', 'rule', '行为规则', '不写则只记录日志'], + ['textarea', 'log', '日志规则', '记录日志备注时按此规则来生成,支持[变量|函数]。目前变量有:user,time,model,record,data,details'], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('行为管理') // 设置页面标题 + ->setSearch(['name' => '标识', 'title' => '名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['name', '标识'], + ['title', '名称'], + ['remark', '描述'], + ['module', '所属模块', 'callback', function($module, $list_module){ + return isset($list_module[$module]) ? $list_module[$module] : '未知'; + }, $list_module], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->autoAdd($fields, '', true, true) // 添加自动新增按钮 + ->autoEdit($fields, '', true, true) // 添加自动编辑按钮 + ->addTopButtons('enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons('delete') // 批量添加右侧按钮 + ->addFilter('module', $list_module) + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } +} \ No newline at end of file diff --git a/application/admin/controller/Admin.php b/application/admin/controller/Admin.php new file mode 100644 index 0000000..26aa101 --- /dev/null +++ b/application/admin/controller/Admin.php @@ -0,0 +1,507 @@ + + * @throws \think\Exception + */ + protected function initialize() + { + parent::initialize(); + // 是否拒绝ie浏览器访问 + if (config('system.deny_ie') && get_browser_type() == 'ie') { + $this->redirect('admin/ie/index'); + } + + // 判断是否登录,并定义用户ID常量 + defined('UID') or define('UID', $this->isLogin()); + + // 设置当前角色菜单节点权限 + role_auth(); + + // 检查权限 + if (!RoleModel::checkAuth()) $this->error('权限不足!'); + + // 设置分页参数 + $this->setPageParam(); + + // 如果不是ajax请求,则读取菜单 + if (!$this->request->isAjax()) { + // 读取顶部菜单 + $this->assign('_top_menus', MenuModel::getTopMenu(config('top_menu_max'), '_top_menus')); + // 读取全部顶级菜单 + $this->assign('_top_menus_all', MenuModel::getTopMenu('', '_top_menus_all')); + // 获取侧边栏菜单 + $this->assign('_sidebar_menus', MenuModel::getSidebarMenu()); + // 获取面包屑导航 + $this->assign('_location', MenuModel::getLocation('', true)); + // 获取当前用户未读消息数量 + $this->assign('_message', MessageModel::getMessageCount()); + // 获取自定义图标 + $this->assign('_icons', IconModel::getUrls()); + // 构建侧栏 + $data = [ + 'table' => 'admin_config', // 表名或模型名 + 'prefix' => 1, + 'module' => 'admin', + 'controller' => 'system', + 'action' => 'quickedit', + ]; + $table_token = substr(sha1('_aside'), 0, 8); + session($table_token, $data); + $settings = [ + [ + 'title' => '站点开关', + 'tips' => '站点关闭后将不能访问', + 'checked' => Db::name('admin_config')->where('id', 1)->value('value'), + 'table' => $table_token, + 'id' => 1, + 'field' => 'value' + ] + ]; + ZBuilder::make('aside') + ->addBlock('switch', '系统设置', $settings); + } + } + + /** + * 获取当前操作模型 + * @author 蔡伟明 <314013107@qq.com> + * @return object|\think\db\Query + */ + final protected function getCurrModel() + { + $table_token = input('param._t', ''); + $module = $this->request->module(); + $controller = parse_name($this->request->controller()); + + $table_token == '' && $this->error('缺少参数'); + !session('?'.$table_token) && $this->error('参数错误'); + + $table_data = session($table_token); + $table = $table_data['table']; + + $Model = null; + if ($table_data['prefix'] == 2) { + // 使用模型 + try { + $Model = App::model($table); + } catch (\Exception $e) { + $this->error('找不到模型:'.$table); + } + } else { + // 使用DB类 + $table == '' && $this->error('缺少表名'); + if ($table_data['module'] != $module || $table_data['controller'] != $controller) { + $this->error('非法操作'); + } + + $Model = $table_data['prefix'] == 0 ? Db::table($table) : Db::name($table); + } + + return $Model; + } + + /** + * 设置分页参数 + * @author 蔡伟明 <314013107@qq.com> + */ + final protected function setPageParam() + { + _system_check(); + $list_rows = !empty(input('param.list_rows')) ? input('param.list_rows') : config('list_rows'); + config('paginate.list_rows', $list_rows); + config('paginate.query', input('get.')); + } + + /** + * 检查是否登录,没有登录则跳转到登录页面 + * @author 蔡伟明 <314013107@qq.com> + * @return int + */ + final protected function isLogin() + { + // 判断是否登录 + if ($uid = is_signin()) { + // 已登录 + return $uid; + } else { + // 未登录 + $this->redirect('user/publics/signin'); + } + } + + /** + * 禁用 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function disable($record = []) + { + return $this->setStatus('disable', $record); + } + + /** + * 启用 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function enable($record = []) + { + return $this->setStatus('enable', $record); + } + + /** + * 启用 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function delete($record = []) + { + return $this->setStatus('delete', $record); + } + + /** + * 快速编辑 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + */ + public function quickEdit($record = []) + { + $field = input('post.name', ''); + $value = input('post.value', ''); + $type = input('post.type', ''); + $id = input('post.pk', ''); + $validate = input('post.validate', ''); + $validate_fields = input('post.validate_fields', ''); + + $field == '' && $this->error('缺少字段名'); + $id == '' && $this->error('缺少主键值'); + + $Model = $this->getCurrModel(); + $protect_table = [ + '__ADMIN_USER__', + '__ADMIN_ROLE__', + config('database.prefix').'admin_user', + config('database.prefix').'admin_role', + ]; + + // 验证是否操作管理员 + if (in_array($Model->getTable(), $protect_table) && $id == 1) { + $this->error('禁止操作超级管理员'); + } + + // 验证器 + if ($validate != '') { + $validate_fields = array_flip(explode(',', $validate_fields)); + if (isset($validate_fields[$field])) { + $result = $this->validate([$field => $value], $validate.'.'.$field); + if (true !== $result) $this->error($result); + } + } + + switch ($type) { + // 日期时间需要转为时间戳 + case 'combodate': + $value = strtotime($value); + break; + // 开关 + case 'switch': + $value = $value == 'true' ? 1 : 0; + break; + // 开关 + case 'password': + $value = Hash::make((string)$value); + break; + } + + // 主键名 + $pk = $Model->getPk(); + $result = $Model->where($pk, $id)->setField($field, $value); + + cache('hook_plugins', null); + cache('system_config', null); + cache('access_menus', null); + if (false !== $result) { + // 记录行为日志 + if (!empty($record)) { + call_user_func_array('action_log', $record); + } + $this->success('操作成功'); + } else { + $this->error('操作失败'); + } + } + + /** + * 自动创建添加页面 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function add() + { + // 获取表单项 + $cache_name = $this->request->module().'/'.parse_name($this->request->controller()).'/add'; + $cache_name = strtolower($cache_name); + $form = Cache::get($cache_name, []); + if (!$form) { + $this->error('自动新增数据不存在,请重新打开此页面'); + } + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + $_pop = $this->request->get('_pop'); + + // 验证 + if ($form['validate'] != '') { + $result = $this->validate($data, $form['validate']); + if(true !== $result) $this->error($result); + } + + // 是否需要自动插入时间 + if ($form['auto_time'] != '') { + $now_time = $this->request->time(); + foreach ($form['auto_time'] as $item) { + if (strpos($item, '|')) { + list($item, $format) = explode('|', $item); + $data[$item] = date($format, $now_time); + } else { + $data[$item] = $form['format'] != '' ? date($form['format'], $now_time) : $now_time; + } + } + } + + // 插入数据 + if (Db::name($form['table'])->insert($data)) { + if ($_pop == 1) { + $this->success('新增成功', null, '_parent_reload'); + } else { + $this->success('新增成功', $form['go_back']); + } + } else { + $this->error('新增失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems($form['items']) + ->fetch(); + } + + /** + * 自动创建编辑页面 + * @param string $id 主键值 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + * @throws \think\exception\PDOException + */ + public function edit($id = '') + { + if ($id === '') $this->error('参数错误'); + + // 获取表单项 + $cache_name = $this->request->module().'/'.parse_name($this->request->controller()).'/edit'; + $cache_name = strtolower($cache_name); + $form = Cache::get($cache_name, []); + if (!$form) { + $this->error('自动编辑数据不存在,请重新打开此页面'); + } + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + $_pop = $this->request->get('_pop'); + + // 验证 + if ($form['validate'] != '') { + $result = $this->validate($data, $form['validate']); + if(true !== $result) $this->error($result); + } + + // 是否需要自动插入时间 + if ($form['auto_time'] != '') { + $now_time = $this->request->time(); + foreach ($form['auto_time'] as $item) { + if (strpos($item, '|')) { + list($item, $format) = explode('|', $item); + $data[$item] = date($format, $now_time); + } else { + $data[$item] = $form['format'] != '' ? date($form['format'], $now_time) : $now_time; + } + } + } + + // 更新数据 + if (false !== Db::name($form['table'])->update($data)) { + if ($_pop == 1) { + $this->success('编辑成功', null, '_parent_reload'); + } else { + $this->success('编辑成功', $form['go_back']); + } + } else { + $this->error('编辑失败'); + } + } + + // 获取数据 + $info = Db::name($form['table'])->find($id); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑') + ->addFormItems($form['items']) + ->setFormData($info) + ->fetch(); + } + + /** + * 设置状态 + * 禁用、启用、删除都是调用这个内部方法 + * @param string $type 操作类型:enable,disable,delete + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $ids = (array)$ids; + $field = input('param.field', 'status'); + + empty($ids) && $this->error('缺少主键'); + + $Model = $this->getCurrModel(); + $protect_table = [ + '__ADMIN_USER__', + '__ADMIN_ROLE__', + '__ADMIN_MODULE__', + config('database.prefix').'admin_user', + config('database.prefix').'admin_role', + config('database.prefix').'admin_module', + ]; + + // 禁止操作核心表的主要数据 + if (in_array($Model->getTable(), $protect_table) && in_array('1', $ids)) { + $this->error('禁止操作'); + } + + // 主键名称 + $pk = $Model->getPk(); + $map = [ + [$pk, 'in', $ids] + ]; + + $result = false; + switch ($type) { + case 'disable': // 禁用 + $result = $Model->where($map)->setField($field, 0); + break; + case 'enable': // 启用 + $result = $Model->where($map)->setField($field, 1); + break; + case 'delete': // 删除 + $result = $Model->where($map)->delete(); + break; + default: + $this->error('非法操作'); + break; + } + + if (false !== $result) { + Cache::clear(); + // 记录行为日志 + if (!empty($record)) { + call_user_func_array('action_log', $record); + } + $this->success('操作成功'); + } else { + $this->error('操作失败'); + } + } + + /** + * 模块设置 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function moduleConfig() + { + // 当前模块名 + $module = $this->request->module(); + + // 保存 + if ($this->request->isPost()) { + $data = $this->request->post(); + $data = json_encode($data); + + if (false !== ModuleModel::where('name', $module)->update(['config' => $data])) { + cache('module_config_'.$module, null); + $this->success('更新成功'); + } else { + $this->error('更新失败'); + } + } + + // 模块配置信息 + $module_info = ModuleModel::getInfoFromFile($module); + $config = $module_info['config']; + $trigger = isset($module_info['trigger']) ? $module_info['trigger'] : []; + + // 数据库内的模块信息 + $db_config = ModuleModel::where('name', $module)->value('config'); + $db_config = json_decode($db_config, true); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('模块设置') + ->addFormItems($config) + ->setFormdata($db_config) // 设置表格数据 + ->setTrigger($trigger) // 设置触发 + ->fetch(); + } +} diff --git a/application/admin/controller/Ajax.php b/application/admin/controller/Ajax.php new file mode 100644 index 0000000..bc88c8f --- /dev/null +++ b/application/admin/controller/Ajax.php @@ -0,0 +1,308 @@ + + * @return \think\response\Json + */ + public function getLevelData($token = '', $pid = 0, $pidkey = 'pid') + { + if ($token == '') { + return json(['code' => 0, 'msg' => '缺少Token']); + } + + $token_data = session($token); + $table = $token_data['table']; + $option = $token_data['option']; + $key = $token_data['key']; + + $data_list = Db::name($table)->where($pidkey, $pid)->column($option, $key); + + if ($data_list === false) { + return json(['code' => 0, 'msg' => '查询失败']); + } + + if ($data_list) { + $result = [ + 'code' => 1, + 'msg' => '请求成功', + 'list' => format_linkage($data_list) + ]; + return json($result); + } else { + return json(['code' => 0, 'msg' => '查询不到数据']); + } + } + + /** + * 获取筛选数据 + * @param string $token + * @param array $map 查询条件 + * @param string $options 选项,用于显示转换 + * @param string $list 选项缓存列表名称 + * @author 蔡伟明 <314013107@qq.com> + * @return \think\response\Json + */ + public function getFilterList($token = '', $map = [], $options = '', $list = '') + { + if ($list != '') { + $result = [ + 'code' => 1, + 'msg' => '请求成功', + 'list' => Cache::get($list) + ]; + return json($result); + } + if ($token == '') { + return json(['code' => 0, 'msg' => '缺少Token']); + } + + $token_data = session($token); + $table = $token_data['table']; + $field = $token_data['field']; + + if ($field == '') { + return json(['code' => 0, 'msg' => '缺少字段']); + } + if (!empty($map) && is_array($map)) { + foreach ($map as &$item) { + if (is_array($item)) { + foreach ($item as &$value) { + $value = trim($value); + } + } else { + $item = trim($item); + } + } + } + + if (strpos($table, '/')) { + $data_list = model($table)->where($map)->group($field)->column($field); + } else { + $data_list = Db::name($table)->where($map)->group($field)->column($field); + } + + if ($data_list === false) { + return json(['code' => 0, 'msg' => '查询失败']); + } + + if ($data_list) { + if ($options != '') { + // 从缓存获取选项数据 + $options = cache($options); + if ($options) { + $temp_data_list = []; + foreach ($data_list as $item) { + $temp_data_list[$item] = isset($options[$item]) ? $options[$item] : ''; + } + $data_list = $temp_data_list; + } else { + $data_list = parse_array($data_list); + } + } else { + $data_list = parse_array($data_list); + } + + $result = [ + 'code' => 1, + 'msg' => '请求成功', + 'list' => $data_list + ]; + return json($result); + } else { + return json(['code' => 0, 'msg' => '查询不到数据']); + } + } + + /** + * 获取指定模块的菜单 + * @param string $module 模块名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function getModuleMenus($module = '') + { + if (!is_signin()) { + $this->error('请先登录'); + } + $menus = MenuModel::getMenuTree(0, '', $module); + $result = [ + 'code' => 1, + 'msg' => '请求成功', + 'list' => format_linkage($menus) + ]; + return json($result); + } + + /** + * 设置配色方案 + * @param string $theme 配色名称 + * @author 蔡伟明 <314013107@qq.com> + */ + public function setTheme($theme = '') { + if (!is_signin()) { + $this->error('请先登录'); + } + $themes = ['default', 'amethyst', 'city', 'flat', 'modern', 'smooth']; + if (!in_array($theme, $themes)) { + $this->error('非法操作'); + } + $map['name'] = 'system_color'; + $map['group'] = 'system'; + + if (Db::name('admin_config')->where($map)->setField('value', $theme)) { + $this->success('设置成功'); + } else { + $this->error('设置失败,请重试'); + } + } + + /** + * 获取侧栏菜单 + * @param string $module_id 模块id + * @param string $module 模型名 + * @param string $controller 控制器名 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + public function getSidebarMenu($module_id = '', $module = '', $controller = '') + { + if (!is_signin()) { + $this->error('登录已失效,请重新登录', 'user/publics/signin'); + } + + role_auth(); + $menus = MenuModel::getSidebarMenu($module_id, $module, $controller); + + $output = ''; + foreach ($menus as $key => $menu) { + if (!empty($menu['url_value'])) { + $output = $menu['url_value']; + break; + } + if (!empty($menu['child'])) { + $output = $menu['child'][0]['url_value']; + break; + } + } + $this->success('获取成功', null, $output); + } + + /** + * 检查附件是否存在 + * @param string $md5 文件md5 + * @author 蔡伟明 <314013107@qq.com> + * @return \think\response\Json + */ + public function check($md5 = '') + { + $md5 == '' && $this->error('参数错误'); + + // 判断附件是否已存在 + if ($file_exists = AttachmentModel::get(['md5' => $md5])) { + if ($file_exists['driver'] == 'local') { + $file_path = PUBLIC_PATH.$file_exists['path']; + } else { + $file_path = $file_exists['path']; + } + return json([ + 'code' => 1, + 'info' => '上传成功', + 'class' => 'success', + 'id' => $file_exists['id'], + 'path' => $file_path + ]); + } else { + $this->error('文件不存在'); + } + } + + /** + * 获取我的角色集合 + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + * @author 蔡伟明 <314013107@qq.com> + */ + public function getMyRoles() + { + if (!is_signin()) { + $this->error('请先登录'); + } + + $user = Db::name('admin_user')->where('id', session('user_auth.uid'))->find(); + !$user && $this->error('获取失败'); + + $roles = [$user['role']]; + if ($user['roles'] != '') { + $roles = array_merge($roles, explode(',', $user['roles'])); + } + $roles = array_unique($roles); + $roles = Db::name('admin_role')->where('id', 'in', $roles)->column('id,name'); + $this->success('获取成功', null, [ + 'curr' => session('user_auth.role'), + 'roles' => $roles + ]); + } + + /** + * 设置我的当前角色 + * @param string $id + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + * @author 蔡伟明 <314013107@qq.com> + */ + public function setMyRole($id = '') + { + if (!is_signin()) { + $this->error('请先登录'); + } + + $id == '' && $this->error('请选择要设置的角色'); + + // 读取当前用户能设置的角色 + $user = Db::name('admin_user')->where('id', session('user_auth.uid'))->find(); + !$user && $this->error('设置失败'); + + $roles = [$user['role']]; + if ($user['roles'] != '') { + $roles = array_merge($roles, explode(',', $user['roles'])); + } + $roles = array_unique($roles); + + if (!in_array($id, $roles)) { + $this->error('无法设置当前角色'); + } + + cache('role_menu_auth_'.session('user_auth.role'), null); + session('user_auth.role', $id); + session('user_auth.role_name', Db::name('admin_role')->where('id', $id)->value('name')); + session('user_auth_sign', data_auth_sign(session('user_auth'))); + $this->success('设置成功'); + } +} \ No newline at end of file diff --git a/application/admin/controller/Attachment.php b/application/admin/controller/Attachment.php new file mode 100644 index 0000000..0758a97 --- /dev/null +++ b/application/admin/controller/Attachment.php @@ -0,0 +1,806 @@ + + */ + public function index() { + // 查询 + $map = $this->getMap(); + + // 数据列表 + $data_list = AttachmentModel::where($map)->order('sort asc,id desc')->paginate(); + foreach ($data_list as $key => &$value) { + if (in_array(strtolower($value['ext']), ['jpg', 'jpeg', 'png', 'gif', 'bmp'])) { + if ($value['driver'] == 'local') { + $thumb = $value['thumb'] != '' ? $value['thumb'] : $value['path']; + $value['type'] = ''; + } else { + $value['type'] = ''; + } + } else { + if ($value['driver'] == 'local') { + $path = PUBLIC_PATH . $value['path']; + } else { + $path = $value['path']; + } + if (is_file('.' . config('public_static_path') . 'admin/img/files/' . $value['ext'] . '.png')) { + $value['type'] = ' + '; + } else { + $value['type'] = ' + '; + } + } + } + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['name' => '名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['type', '类型'], + ['name', '名称'], + ['size', '大小', 'byte'], + ['driver', '上传驱动', parse_attr(Db::name('admin_config')->where('name', 'upload_driver')->value('options'))], + ['create_time', '上传时间', 'datetime'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'], + ]) + ->addTopButtons('enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons('delete') // 批量添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + /** + * 上传附件 + * @param string $dir 保存的目录:images,files,videos,voices + * @param string $from 来源,wangeditor:wangEditor编辑器, ueditor:ueditor编辑器, editormd:editormd编辑器等 + * @param string $module 来自哪个模块 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function upload($dir = '', $from = '', $module = '') { + // 临时取消执行时间限制 + set_time_limit(0); + if ($dir == '') { + $this->error('没有指定上传目录'); + } + + if ($from == 'ueditor') { + return $this->ueditor($module); + } + + if ($from == 'jcrop') { + return $this->jcrop(); + } + + return $this->saveFile($dir, $from, $module); + } + + /** + * 保存附件 + * @param string $dir 附件存放的目录 + * @param string $from 来源 + * @param string $module 来自哪个模块 + * @author 蔡伟明 <314013107@qq.com> + * @return string|\think\response\Json + */ + private function saveFile($dir = '', $from = '', $module = '') { + // 附件大小限制 + $size_limit = $dir == 'images' ? config('upload_image_size') : config('upload_file_size'); + $size_limit = $size_limit * 1024; + // 附件类型限制 + $ext_limit = $dir == 'images' ? config('upload_image_ext') : config('upload_file_ext'); + $ext_limit = $ext_limit != '' ? parse_attr($ext_limit) : ''; + // 缩略图参数 + $thumb = $this->request->post('thumb', ''); + // 水印参数 + $watermark = $this->request->post('watermark', ''); + + // 获取附件数据 + $callback = ''; + switch ($from) { + case 'editormd': + $file_input_name = 'editormd-image-file'; + break; + case 'ckeditor': + $file_input_name = 'upload'; + $callback = $this->request->get('CKEditorFuncNum'); + break; + case 'ueditor_scrawl': + return $this->saveScrawl(); + break; + default: + $file_input_name = 'file'; + } + $file = $this->request->file($file_input_name); + + // 判断附件是否已存在 + if ($file_exists = AttachmentModel::get(['md5' => $file->hash('md5')])) { + if ($file_exists['driver'] == 'local') { + $file_path = PUBLIC_PATH . $file_exists['path']; + } else { + $file_path = $file_exists['path']; + } + + // 附件已存在 + return $this->uploadSuccess($from, $file_path, $file_exists['name'], $file_exists['id'], $callback); + } + + // 判断附件大小是否超过限制 + if ($size_limit > 0 && ($file->getInfo('size') > $size_limit)) { + return $this->uploadError($from, '附件过大', $callback); + } + + // 判断附件格式是否符合 + $file_name = $file->getInfo('name'); + $file_ext = strtolower(substr($file_name, strrpos($file_name, '.') + 1)); + $error_msg = ''; + if ($ext_limit == '') { + $error_msg = '获取文件信息失败!'; + } + if ($file->getMime() == 'text/x-php' || $file->getMime() == 'text/html') { + $error_msg = '禁止上传非法文件!'; + } + if (preg_grep("/php/i", $ext_limit)) { + $error_msg = '禁止上传非法文件!'; + } + if (!preg_grep("/$file_ext/i", $ext_limit)) { + $error_msg = '附件类型不正确!'; + } + + if ($error_msg != '') { + // 上传错误 + return $this->uploadError($from, $error_msg, $callback); + } + + // 附件上传钩子,用于第三方文件上传扩展 + if (config('upload_driver') != 'local') { + $hook_result = Hook::listen('upload_attachment', ['file' => $file, 'from' => $from, 'module' => $module], true); + if (false !== $hook_result) { + return $hook_result; + } + } + + // 移动到框架应用根目录/uploads/ 目录下 + $info = $file->move(config('upload_path') . DIRECTORY_SEPARATOR . $dir); + if ($info) { + // 缩略图路径 + $thumb_path_name = ''; + // 图片宽度 + $img_width = ''; + // 图片高度 + $img_height = ''; + if ($dir == 'images') { + $img = Image::open($info); + $img_width = $img->width(); + $img_height = $img->height(); + // 水印功能 + if ($watermark == '') { + if (config('upload_thumb_water') == 1 && config('upload_thumb_water_pic') > 0) { + $this->create_water($info->getRealPath(), config('upload_thumb_water_pic')); + } + } else { + if (strtolower($watermark) != 'close') { + list($watermark_img, $watermark_pos, $watermark_alpha) = explode('|', $watermark); + $this->create_water($info->getRealPath(), $watermark_img, $watermark_pos, $watermark_alpha); + } + } + + // 生成缩略图 + if ($thumb == '') { + if (config('upload_image_thumb') != '') { + $thumb_path_name = $this->create_thumb($info, $info->getPathInfo()->getfileName(), $info->getFilename()); + } + } else { + if (strtolower($thumb) != 'close') { + list($thumb_size, $thumb_type) = explode('|', $thumb); + $thumb_path_name = $this->create_thumb($info, $info->getPathInfo()->getfileName(), $info->getFilename(), $thumb_size, $thumb_type); + } + } + } + + // 获取附件信息 + $file_info = [ + 'uid' => session('user_auth.uid'), + 'name' => $file->getInfo('name'), + 'mime' => $file->getInfo('type'), + 'path' => 'uploads/' . $dir . '/' . str_replace('\\', '/', $info->getSaveName()), + 'ext' => $info->getExtension(), + 'size' => $info->getSize(), + 'md5' => $info->hash('md5'), + 'sha1' => $info->hash('sha1'), + 'thumb' => $thumb_path_name, + 'module' => $module, + 'width' => $img_width, + 'height' => $img_height, + ]; + + // 写入数据库 + if ($file_add = AttachmentModel::create($file_info)) { + $file_path = PUBLIC_PATH . $file_info['path']; + return $this->uploadSuccess($from, $file_path, $file_info['name'], $file_add['id'], $callback); + } else { + return $this->uploadError($from, '上传失败', $callback); + } + } else { + return $this->uploadError($from, $file->getError(), $callback); + } + } + + /** + * 处理ueditor上传 + * @author 蔡伟明 <314013107@qq.com> + * @param string $module 来自哪个模块 + * @return string|\think\response\Json + */ + private function ueditor($module = '') { + $action = $this->request->get('action'); + $config_file = './static/libs/ueditor/php/config.json'; + $config = json_decode(preg_replace("/\/\*[\s\S]+?\*\//", "", file_get_contents($config_file)), true); + switch ($action) { + /* 获取配置信息 */ + case 'config': + $result = $config; + break; + + /* 上传图片 */ + case 'uploadimage': + return $this->saveFile('images', 'ueditor', $module); + break; + /* 上传涂鸦 */ + case 'uploadscrawl': + return $this->saveFile('images', 'ueditor_scrawl', $module); + break; + + /* 上传视频 */ + case 'uploadvideo': + return $this->saveFile('videos', 'ueditor', $module); + break; + + /* 上传附件 */ + case 'uploadfile': + return $this->saveFile('files', 'ueditor', $module); + break; + + /* 列出图片 */ + case 'listimage': + return $this->showFile('listimage', $config); + break; + + /* 列出附件 */ + case 'listfile': + return $this->showFile('listfile', $config); + break; + + /* 抓取远程附件 */ +// case 'catchimage': + // $result = include("action_crawler.php"); + // break; + + default: + $result = ['state' => '请求地址出错']; + break; + } + + /* 输出结果 */ + if (isset($_GET["callback"])) { + if (preg_match("/^[\w_]+$/", $_GET["callback"])) { + return htmlspecialchars($_GET["callback"]) . '(' . $result . ')'; + } else { + return json(['state' => 'callback参数不合法']); + } + } else { + return json($result); + } + } + + /** + * 保存涂鸦(ueditor) + * @author 蔡伟明 <314013107@qq.com> + * @return \think\response\Json + */ + private function saveScrawl() { + $file = $this->request->post('file'); + $file_content = base64_decode($file); + $file_name = md5($file) . '.jpg'; + $dir = config('upload_path') . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . date('Ymd', $this->request->time()); + $file_path = $dir . DIRECTORY_SEPARATOR . $file_name; + + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + + if (false === file_put_contents($file_path, $file_content)) { + return json(['state' => '涂鸦上传出错']); + } + + $file = new File($file_path); + $img = Image::open($file); + $file_info = [ + 'uid' => session('user_auth.uid'), + 'name' => $file_name, + 'mime' => 'image/png', + 'path' => 'uploads/images/' . date('Ymd', $this->request->time()) . '/' . $file_name, + 'ext' => 'png', + 'size' => $file->getSize(), + 'md5' => $file->hash('md5'), + 'sha1' => $file->hash('sha1'), + 'module' => $this->request->param('module', ''), + 'width' => $img->width(), + 'height' => $img->height(), + ]; + + if ($file_add = AttachmentModel::create($file_info)) { + // 返回成功信息 + return json([ + "state" => "SUCCESS", // 上传状态,上传成功时必须返回"SUCCESS" + "url" => PUBLIC_PATH . $file_info['path'], // 返回的地址 + "title" => $file_info['name'], // 附件名 + ]); + } else { + return json(['state' => '涂鸦上传出错']); + } + } + + /** + * 显示附件列表(ueditor) + * @param string $type 类型 + * @param $config + * @author 蔡伟明 <314013107@qq.com> + * @return \think\response\Json + */ + private function showFile($type, $config) { + /* 判断类型 */ + switch ($type) { + /* 列出附件 */ + case 'listfile': + $allowFiles = $config['fileManagerAllowFiles']; + $listSize = $config['fileManagerListSize']; + $path = realpath(config('upload_path') . '/files/'); + break; + /* 列出图片 */ + case 'listimage': + default: + $allowFiles = $config['imageManagerAllowFiles']; + $listSize = $config['imageManagerListSize']; + $path = realpath(config('upload_path') . '/images/'); + } + $allowFiles = substr(str_replace(".", "|", join("", $allowFiles)), 1); + + /* 获取参数 */ + $size = isset($_GET['size']) ? htmlspecialchars($_GET['size']) : $listSize; + $start = isset($_GET['start']) ? htmlspecialchars($_GET['start']) : 0; + $end = $start + $size; + + /* 获取附件列表 */ + $files = $this->getfiles($path, $allowFiles); + if (!count($files)) { + return json(array( + "state" => "no match file", + "list" => array(), + "start" => $start, + "total" => count($files), + )); + } + + /* 获取指定范围的列表 */ + $len = count($files); + for ($i = min($end, $len) - 1, $list = array(); $i < $len && $i >= 0 && $i >= $start; $i--) { + $list[] = $files[$i]; + } + //倒序 + //for ($i = $end, $list = array(); $i < $len && $i < $end; $i++){ + // $list[] = $files[$i]; + //} + + /* 返回数据 */ + $result = array( + "state" => "SUCCESS", + "list" => $list, + "start" => $start, + "total" => count($files), + ); + + return json($result); + } + + /** + * 处理Jcrop图片裁剪 + * @author 蔡伟明 <314013107@qq.com> + */ + private function jcrop() { + $file_path = $this->request->post('path', ''); + $cut_info = $this->request->post('cut', ''); + $thumb = $this->request->post('thumb', ''); + $watermark = $this->request->post('watermark', ''); + $module = $this->request->param('module', ''); + + // 上传图片 + if ($file_path == '') { + $file = $this->request->file('file'); + + // 附件类型限制 + $ext_limit = config('upload_image_ext'); + $ext_limit = $ext_limit != '' ? parse_attr($ext_limit) : ''; + + // 判断附件格式是否符合 + $file_name = $file->getInfo('name'); + $file_ext = strtolower(substr($file_name, strrpos($file_name, '.') + 1)); + + if ($ext_limit == '') { + $this->error('获取文件信息失败!'); + } + if (strtolower($file_ext) == 'php') { + $this->error('禁止上传非法文件!'); + } + if ($file->getMime() == 'text/x-php' || $file->getMime() == 'text/html') { + $this->error('禁止上传非法文件!'); + } + if (preg_grep("/php/i", $ext_limit)) { + $this->error('禁止上传非法文件!'); + } + if (!preg_grep("/$file_ext/i", $ext_limit)) { + $this->error('附件类型不正确!'); + } + + if (!is_dir(config('upload_temp_path'))) { + mkdir(config('upload_temp_path'), 0766, true); + } + $info = $file->move(config('upload_temp_path'), $file->hash('md5')); + if ($info) { + return json(['code' => 1, 'src' => PUBLIC_PATH . 'uploads/temp/' . $info->getFilename()]); + } else { + $this->error('上传失败'); + } + } + + $file_path = config('upload_temp_path') . str_replace(PUBLIC_PATH . 'uploads/temp/', '', $file_path); + + if (is_file($file_path)) { + // 获取裁剪信息 + $cut_info = explode(',', $cut_info); + + // 读取图片 + $image = Image::open($file_path); + + $dir_name = date('Ymd'); + $file_dir = config('upload_path') . DIRECTORY_SEPARATOR . 'images/' . $dir_name . '/'; + if (!is_dir($file_dir)) { + mkdir($file_dir, 0766, true); + } + $file_name = md5(microtime(true)) . '.' . $image->type(); + $new_file_path = $file_dir . $file_name; + + // 裁剪图片 + $image->crop($cut_info[0], $cut_info[1], $cut_info[2], $cut_info[3], $cut_info[4], $cut_info[5])->save($new_file_path); + + // 水印功能 + if ($watermark == '') { + if (config('upload_thumb_water') == 1 && config('upload_thumb_water_pic') > 0) { + $this->create_water($new_file_path, config('upload_thumb_water_pic')); + } + } else { + if (strtolower($watermark) != 'close') { + list($watermark_img, $watermark_pos, $watermark_alpha) = explode('|', $watermark); + $this->create_water($new_file_path, $watermark_img, $watermark_pos, $watermark_alpha); + } + } + + // 是否创建缩略图 + $thumb_path_name = ''; + if ($thumb == '') { + if (config('upload_image_thumb') != '') { + $thumb_path_name = $this->create_thumb($new_file_path, $dir_name, $file_name); + } + } else { + if (strtolower($thumb) != 'close') { + list($thumb_size, $thumb_type) = explode('|', $thumb); + $thumb_path_name = $this->create_thumb($new_file_path, $dir_name, $file_name, $thumb_size, $thumb_type); + } + } + + // 保存图片 + $file = new File($new_file_path); + $file_info = [ + 'uid' => session('user_auth.uid'), + 'name' => $file_name, + 'mime' => $image->mime(), + 'path' => 'uploads/images/' . $dir_name . '/' . $file_name, + 'ext' => $image->type(), + 'size' => $file->getSize(), + 'md5' => $file->hash('md5'), + 'sha1' => $file->hash('sha1'), + 'thumb' => $thumb_path_name, + 'module' => $module, + 'width' => $image->width(), + 'height' => $image->height(), + ]; + + if ($file_add = AttachmentModel::create($file_info)) { + // 删除临时图片 + unlink($file_path); + // 返回成功信息 + return json([ + 'code' => 1, + 'id' => $file_add['id'], + 'src' => PUBLIC_PATH . $file_info['path'], + 'thumb' => $thumb_path_name == '' ? '' : PUBLIC_PATH . $thumb_path_name, + ]); + } else { + $this->error('上传失败'); + } + } + $this->error('文件不存在'); + } + + /** + * 创建缩略图 + * @param string $file 目标文件,可以是文件对象或文件路径 + * @param string $dir 保存目录,即目标文件所在的目录名 + * @param string $save_name 缩略图名 + * @param string $thumb_size 尺寸 + * @param string $thumb_type 裁剪类型 + * @author 蔡伟明 <314013107@qq.com> + * @return string 缩略图路径 + */ + private function create_thumb($file = '', $dir = '', $save_name = '', $thumb_size = '', $thumb_type = '') { + // 获取要生成的缩略图最大宽度和高度 + $thumb_size = $thumb_size == '' ? config('upload_image_thumb') : $thumb_size; + list($thumb_max_width, $thumb_max_height) = explode(',', $thumb_size); + // 读取图片 + $image = Image::open($file); + // 生成缩略图 + $thumb_type = $thumb_type == '' ? config('upload_image_thumb_type') : $thumb_type; + $image->thumb($thumb_max_width, $thumb_max_height, $thumb_type); + // 保存缩略图 + $thumb_path = config('upload_path') . DIRECTORY_SEPARATOR . 'images/' . $dir . '/thumb/'; + if (!is_dir($thumb_path)) { + mkdir($thumb_path, 0766, true); + } + $thumb_path_name = $thumb_path . $save_name; + $image->save($thumb_path_name); + $thumb_path_name = 'uploads/images/' . $dir . '/thumb/' . $save_name; + return $thumb_path_name; + } + + /** + * 添加水印 + * @param string $file 要添加水印的文件路径 + * @param string $watermark_img 水印图片id + * @param string $watermark_pos 水印位置 + * @param string $watermark_alpha 水印透明度 + * @author 蔡伟明 <314013107@qq.com> + */ + private function create_water($file = '', $watermark_img = '', $watermark_pos = '', $watermark_alpha = '') { + $path = model('admin/attachment')->getFilePath($watermark_img, 1); + $thumb_water_pic = realpath(Env::get('root_path') . 'public/' . $path); + if (is_file($thumb_water_pic)) { + // 读取图片 + $image = Image::open($file); + // 添加水印 + $watermark_pos = $watermark_pos == '' ? config('upload_thumb_water_position') : $watermark_pos; + $watermark_alpha = $watermark_alpha == '' ? config('upload_thumb_water_alpha') : $watermark_alpha; + $image->water($thumb_water_pic, $watermark_pos, $watermark_alpha); + // 保存水印图片,覆盖原图 + $image->save($file); + } + } + + /** + * 上传成功信息 + * @param $from + * @param string $file_path + * @param string $file_name + * @param string $file_id + * @param string $callback + * @return string|\think\response\Json + * @author 蔡伟明 <314013107@qq.com> + */ + private function uploadSuccess($from, $file_path = '', $file_name = '', $file_id = '', $callback = '') { + switch ($from) { + case 'wangeditor': + return $file_path; + break; + case 'ueditor': + return json([ + "state" => "SUCCESS", // 上传状态,上传成功时必须返回"SUCCESS" + "url" => $file_path, // 返回的地址 + "title" => $file_name, // 附件名 + ]); + break; + case 'editormd': + return json([ + "success" => 1, + "message" => '上传成功', + "url" => $file_path, + ]); + break; + case 'ckeditor': + return ck_js($callback, $file_path); + break; + default: + return json([ + 'code' => 1, + 'info' => '上传成功', + 'class' => 'success', + 'id' => $file_id, + 'path' => $file_path, + ]); + } + } + + /** + * 上传错误信息 + * @param $from + * @param string $msg + * @param string $callback + * @return string|\think\response\Json + * @author 蔡伟明 <314013107@qq.com> + */ + private function uploadError($from, $msg = '', $callback = '') { + switch ($from) { + case 'wangeditor': + return "error|" . $msg; + break; + case 'ueditor': + return json(['state' => $msg]); + break; + case 'editormd': + return json(["success" => 0, "message" => $msg]); + break; + case 'ckeditor': + return ck_js($callback, '', $msg); + break; + default: + return json([ + 'code' => 0, + 'class' => 'danger', + 'info' => $msg, + ]); + } + } + + /** + * 遍历获取目录下的指定类型的附件 + * @param string $path 路径 + * @param string $allowFiles 允许查看的类型 + * @param array $files 文件列表 + * @author 蔡伟明 <314013107@qq.com> + * @return array|null + */ + public function getfiles($path = '', $allowFiles = '', &$files = array()) { + if (!is_dir($path)) { + return null; + } + + if (substr($path, strlen($path) - 1) != '/') { + $path .= '/'; + } + + $handle = opendir($path); + while (false !== ($file = readdir($handle))) { + if ($file != '.' && $file != '..') { + $path2 = $path . $file; + if (is_dir($path2)) { + $this->getfiles($path2, $allowFiles, $files); + } else { + if (preg_match("/\.(" . $allowFiles . ")$/i", $file)) { + $files[] = array( + 'url' => str_replace("\\", "/", substr($path2, strlen($_SERVER['DOCUMENT_ROOT']))), + 'mtime' => filemtime($path2), + ); + } + } + } + } + return $files; + } + + /** + * 启用附件 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) { + return $this->setStatus('enable'); + } + + /** + * 禁用附件 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) { + return $this->setStatus('disable'); + } + + /** + * 设置附件状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function setStatus($type = '', $record = []) { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $ids = is_array($ids) ? implode(',', $ids) : $ids; + return parent::setStatus($type, ['attachment_' . $type, 'admin_attachment', 0, UID, $ids]); + } + + /** + * 删除附件 + * @param string $ids 附件id + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function delete($ids = '') { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + if (empty($ids)) { + $this->error('缺少主键'); + } + + $files_path = AttachmentModel::where('id', 'in', $ids)->column('path,thumb', 'id'); + + foreach ($files_path as $value) { + $real_path = realpath(config('upload_path') . '/../' . $value['path']); + $real_path_thumb = realpath(config('upload_path') . '/../' . $value['thumb']); + + if (is_file($real_path) && !unlink($real_path)) { + $this->error('删除失败'); + } + if (is_file($real_path_thumb) && !unlink($real_path_thumb)) { + $this->error('删除缩略图失败'); + } + } + if (AttachmentModel::where('id', 'in', $ids)->delete()) { + // 记录行为 + $ids = is_array($ids) ? implode(',', $ids) : $ids; + action_log('attachment_delete', 'admin_attachment', 0, UID, $ids); + $this->success('删除成功'); + } else { + $this->error('删除失败'); + } + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) { + $id = input('post.pk', ''); + return parent::quickEdit(['attachment_edit', 'admin_attachment', 0, UID, $id]); + } +} \ No newline at end of file diff --git a/application/admin/controller/Config.php b/application/admin/controller/Config.php new file mode 100644 index 0000000..733072c --- /dev/null +++ b/application/admin/controller/Config.php @@ -0,0 +1,289 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\exception\DbException + */ + public function index($group = 'base') + { + cookie('__forward__', $_SERVER['REQUEST_URI']); + + // 配置分组信息 + $list_group = config('config_group'); + $tab_list = []; + foreach ($list_group as $key => $value) { + $tab_list[$key]['title'] = $value; + $tab_list[$key]['url'] = url('index', ['group' => $key]); + } + + // 查询 + $map = $this->getMap(); + $map[] = ['group', '=', $group]; + $map[] = ['status', 'egt', 0]; + + // 排序 + $order = $this->getOrder('sort asc,id asc'); + // 数据列表 + $data_list = ConfigModel::where($map)->order($order)->paginate(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('配置管理') // 设置页面标题 + ->setTabNav($tab_list, $group) // 设置tab分页 + ->setSearch(['name' => '名称', 'title' => '标题']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['name', '名称', 'text.edit'], + ['title', '标题', 'text.edit'], + ['type', '类型', 'select', config('form_item_type')], + ['status', '状态', 'switch'], + ['sort', '排序', 'text.edit'], + ['right_button', '操作', 'btn'] + ]) + ->addValidate('Config', 'name,title') // 添加快捷编辑的验证器 + ->addOrder('name,title,status') // 添加标题字段排序 + ->addFilter('name,title') // 添加标题字段筛选 + ->addFilter('type', config('form_item_type')) // 添加标题字段筛选 + ->addFilterMap('name,title', ['group' => $group]) // 添加标题字段筛选条件 + ->addTopButton('add', ['href' => url('add', ['group' => $group])], true) // 添加单个顶部按钮 + ->addTopButtons('enable,disable,delete') // 批量添加顶部按钮 + ->addRightButton('edit', [], true) + ->addRightButton('delete') // 批量添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + /** + * 新增配置项 + * @param string $group 分组 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function add($group = '') + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Config'); + if(true !== $result) $this->error($result); + + // 如果是快速联动 + if ($data['type'] == 'linkages') { + $data['key'] = $data['key'] == '' ? 'id' : $data['key']; + $data['pid'] = $data['pid'] == '' ? 'pid' : $data['pid']; + $data['level'] = $data['level'] == '' ? '2' : $data['level']; + $data['option'] = $data['option'] == '' ? 'name' : $data['option']; + } + + if ($config = ConfigModel::create($data)) { + cache('system_config', null); + $forward = $this->request->param('_pop') == 1 ? null : cookie('__forward__'); + // 记录行为 + $details = '详情:分组('.$data['group'].')、类型('.$data['type'].')、标题('.$data['title'].')、名称('.$data['name'].')'; + action_log('config_add', 'admin_config', $config['id'], UID, $details); + $this->success('新增成功', $forward); + } else { + $this->error('新增失败'); + } + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('新增') + ->addRadio('group', '配置分组', '', config('config_group'), $group) + ->addSelect('type', '配置类型', '', config('form_item_type')) + ->addText('title', '配置标题', '一般由中文组成,仅用于显示') + ->addText('name', '配置名称', '由英文字母和下划线组成,如 web_site_title,调用方法:config(\'web_site_title\')') + ->addTextarea('value', '配置值', '该配置的具体内容') + ->addTextarea('options', '配置项', '用于单选、多选、下拉、联动等类型') + ->addText('ajax_url', '异步请求地址', "如请求的地址是 url('ajax/getCity'),那么只需填写 ajax/getCity,或者直接填写以 http开头的url地址") + ->addText('next_items', '下一级联动下拉框的表单名', "与当前有关联的下级联动下拉框名,多个用逗号隔开,如:area,other") + ->addText('param', '请求参数名', "联动下拉框请求参数名,默认为配置名称") + ->addNumber('level', '级别', '需要显示的级别数量,默认为2', 2, 2, 4) + ->addText('table', '表名', '要查询的表,里面必须含有id、name、pid三个字段,其中id和name字段可在下面重新定义') + ->addText('pid', '父级id字段名', '即表中的父级ID字段名,如果表中的主键字段名为pid则可不填写') + ->addText('key', '键字段名', '即表中的主键字段名,如果表中的主键字段名为id则可不填写') + ->addText('option', '值字段名', '下拉菜单显示的字段名,如果表中的该字段名为name则可不填写') + ->addText('ak', 'APPKEY', '百度编辑器APPKEY') + ->addText('format', '格式') + ->addText('tips', '配置说明', '该配置的具体说明') + ->addText('sort', '排序', '', 100) + ->setTrigger('type', 'linkage', 'ajax_url,next_items,param') + ->setTrigger('type', 'linkages', 'table,pid,level,key,option') + ->setTrigger('type', 'bmap', 'ak') + ->setTrigger('type', 'masked,date,time,datetime', 'format') + ->fetch(); + } + + /** + * 编辑 + * @param int $id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function edit($id = 0) + { + if ($id === 0) $this->error('参数错误'); + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Config'); + if(true !== $result) $this->error($result); + + // 如果是快速联动 + if ($data['type'] == 'linkages') { + $data['key'] = $data['key'] == '' ? 'id' : $data['key']; + $data['pid'] = $data['pid'] == '' ? 'pid' : $data['pid']; + $data['level'] = $data['level'] == '' ? '2' : $data['level']; + $data['option'] = $data['option'] == '' ? 'name' : $data['option']; + } + + // 原配置内容 + $config = ConfigModel::where('id', $id)->find(); + $details = '原数据:分组('.$config['group'].')、类型('.$config['type'].')、标题('.$config['title'].')、名称('.$config['name'].')'; + + if ($config = ConfigModel::update($data)) { + cache('system_config', null); + $forward = $this->request->param('_pop') == 1 ? null : cookie('__forward__'); + // 记录行为 + action_log('config_edit', 'admin_config', $config['id'], UID, $details); + $this->success('编辑成功', $forward, '_parent_reload'); + } else { + $this->error('编辑失败'); + } + } + + // 获取数据 + $info = ConfigModel::get($id); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑') + ->addHidden('id') + ->addRadio('group', '配置分组', '', config('config_group')) + ->addSelect('type', '配置类型', '', config('form_item_type')) + ->addText('title', '配置标题', '一般由中文组成,仅用于显示') + ->addText('name', '配置名称', '由英文字母和下划线组成,如 web_site_title,调用方法:config(\'web_site_title\')') + ->addTextarea('value', '配置值', '该配置的具体内容') + ->addTextarea('options', '配置项', '用于单选、多选、下拉、联动等类型') + ->addText('ajax_url', '异步请求地址', "如请求的地址是 url('ajax/getCity'),那么只需填写 ajax/getCity,或者直接填写以 http开头的url地址") + ->addText('next_items', '下一级联动下拉框的表单名', "与当前有关联的下级联动下拉框名,多个用逗号隔开,如:area,other") + ->addText('param', '请求参数名', "联动下拉框请求参数名,默认为配置名称") + ->addNumber('level', '级别', '需要显示的级别数量,默认为2', 2, 2, 4) + ->addText('table', '表名', '要查询的表,里面必须含有id、name、pid三个字段,其中id和name字段可在下面重新定义') + ->addText('pid', '父级id字段名', '即表中的父级ID字段名,如果表中的主键字段名为pid则可不填写') + ->addText('key', '键字段名', '即表中的主键字段名,如果表中的主键字段名为id则可不填写') + ->addText('option', '值字段名', '下拉菜单显示的字段名,如果表中的该字段名为name则可不填写') + ->addText('ak', 'APPKEY', '百度编辑器APPKEY') + ->addText('format', '格式') + ->addText('tips', '配置说明', '该配置的具体说明') + ->addText('sort', '排序', '', 100) + ->setTrigger('type', 'linkage', 'ajax_url,next_items,param') + ->setTrigger('type', 'linkages', 'table,pid,level,key,option') + ->setTrigger('type', 'bmap', 'ak') + ->setTrigger('type', 'masked,date,time,datetime', 'format') + ->setFormData($info) + ->fetch(); + } + + /** + * 删除配置 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function delete($record = []) + { + return $this->setStatus('delete'); + } + + /** + * 启用配置 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用配置 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置配置状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $uid_delete = is_array($ids) ? '' : $ids; + $ids = ConfigModel::where('id', 'in', $ids)->column('title'); + return parent::setStatus($type, ['config_'.$type, 'admin_config', $uid_delete, UID, implode('、', $ids)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $config = ConfigModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $config . '),新值:(' . $value . ')'; + return parent::quickEdit(['config_edit', 'admin_config', $id, UID, $details]); + } +} \ No newline at end of file diff --git a/application/admin/controller/Database.php b/application/admin/controller/Database.php new file mode 100644 index 0000000..e6acdf3 --- /dev/null +++ b/application/admin/controller/Database.php @@ -0,0 +1,378 @@ + + * @return mixed + * @throws \think\Exception + */ + public function index($group = 'export') + { + // 配置分组信息 + $list_group = ['export' =>'备份数据库', 'import' => '还原数据库']; + $tab_list = []; + foreach ($list_group as $key => $value) { + $tab_list[$key]['title'] = $value; + $tab_list[$key]['url'] = url('index', ['group' => $key]); + } + + switch ($group) { + case 'export': + $data_list = Db::query("SHOW TABLE STATUS"); + $data_list = array_map('array_change_key_case', $data_list); + // 过滤掉视图表 + $data_list = array_filter($data_list, function($item) { + return $item['comment'] !== 'VIEW'; + }); + + // 自定义按钮 + $btn_export = [ + 'title' => '立即备份', + 'icon' => 'fa fa-fw fa-copy', + 'class' => 'btn btn-primary ajax-post confirm', + 'href' => url('export') + ]; + $btn_optimize_all = [ + 'title' => '优化表', + 'icon' => 'fa fa-fw fa-cogs', + 'class' => 'btn btn-success ajax-post', + 'href' => url('optimize') + ]; + $btn_repair_all = [ + 'title' => '修复表', + 'icon' => 'fa fa-fw fa-wrench', + 'class' => 'btn btn-success ajax-post', + 'href' => url('repair') + ]; + $btn_optimize = [ + 'title' => '优化表', + 'icon' => 'fa fa-fw fa-cogs', + 'class' => 'btn btn-xs btn-default ajax-get', + 'href' => url('optimize', ['ids' => '__id__']) + ]; + $btn_repair = [ + 'title' => '修复表', + 'icon' => 'fa fa-fw fa-wrench', + 'class' => 'btn btn-xs btn-default ajax-get', + 'href' => url('repair', ['ids' => '__id__']) + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('数据库管理') // 设置页面标题 + ->setPrimaryKey('name') + ->setTabNav($tab_list, $group) // 设置tab分页 + ->addColumns([ // 批量添加数据列 + ['name', '表名'], + ['rows', '行数'], + ['data_length', '大小', 'byte'], + ['data_free', '冗余', 'byte'], + ['comment', '备注'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('custom', $btn_export) // 添加单个顶部按钮 + ->addTopButton('custom', $btn_optimize_all) // 添加单个顶部按钮 + ->addTopButton('custom', $btn_repair_all) // 添加单个顶部按钮 + ->addRightButton('custom', $btn_optimize) // 添加右侧按钮 + ->addRightButton('custom', $btn_repair) // 添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + break; + case 'import': + // 列出备份文件列表 + $path = config('data_backup_path'); + if(!is_dir($path)){ + mkdir($path, 0755, true); + } + $path = realpath($path); + $flag = \FilesystemIterator::KEY_AS_FILENAME; + $glob = new \FilesystemIterator($path, $flag); + + $data_list = []; + foreach ($glob as $name => $file) { + if(preg_match('/^\d{8,8}-\d{6,6}-\d+\.sql(?:\.gz)?$/', $name)){ + $name = sscanf($name, '%4s%2s%2s-%2s%2s%2s-%d'); + + $date = "{$name[0]}-{$name[1]}-{$name[2]}"; + $time = "{$name[3]}:{$name[4]}:{$name[5]}"; + $part = $name[6]; + + if(isset($data_list["{$date} {$time}"])){ + $info = $data_list["{$date} {$time}"]; + $info['part'] = max($info['part'], $part); + $info['size'] = $info['size'] + $file->getSize(); + } else { + $info['part'] = $part; + $info['size'] = $file->getSize(); + } + $extension = strtoupper(pathinfo($file->getFilename(), PATHINFO_EXTENSION)); + $info['compress'] = ($extension === 'SQL') ? '-' : $extension; + $info['time'] = strtotime("{$date} {$time}"); + $info['name'] = $info['time']; + + $data_list["{$date} {$time}"] = $info; + } + } + + $data_list = !empty($data_list) ? array_values($data_list) : $data_list; + + // 自定义按钮 + $btn_import = [ + 'title' => '还原', + 'icon' => 'fa fa-fw fa-reply', + 'class' => 'btn btn-xs btn-default ajax-get confirm', + 'href' => url('import', ['time' => '__id__']) + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('数据库管理') // 设置页面标题 + ->setPrimaryKey('time') + ->hideCheckbox() + ->setTabNav($tab_list, $group) // 设置tab分页 + ->addColumns([ // 批量添加数据列 + ['name', '备份名称', 'datetime', '', 'Ymd-His'], + ['part', '卷数'], + ['compress', '压缩'], + ['size', '数据大小', 'byte'], + ['time', '备份时间', 'datetime', '', 'Y-m-d H:i:s'], + ['right_button', '操作', 'btn'] + ]) + ->addRightButton('custom', $btn_import) // 添加右侧按钮 + ->addRightButton('delete') // 添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + break; + } + } + + /** + * 备份数据库(参考onthink 麦当苗儿 ) + * @param null|array $ids 表名 + * @param integer $start 起始行数 + * @author 蔡伟明 <314013107@qq.com> + */ + public function export($ids = null, $start = 0) + { + $tables = $ids; + if ($this->request->isPost() && !empty($tables) && is_array($tables)) { + // 初始化 + $path = config('data_backup_path'); + if(!is_dir($path)){ + mkdir($path, 0755, true); + } + + // 读取备份配置 + $config = array( + 'path' => realpath($path) . DIRECTORY_SEPARATOR, + 'part' => config('data_backup_part_size'), + 'compress' => config('data_backup_compress'), + 'level' => config('data_backup_compress_level'), + ); + + // 检查是否有正在执行的任务 + $lock = "{$config['path']}backup.lock"; + if(is_file($lock)){ + $this->error('检测到有一个备份任务正在执行,请稍后再试!'); + } else { + // 创建锁文件 + file_put_contents($lock, $this->request->time()); + } + + // 检查备份目录是否可写 + is_writeable($config['path']) || $this->error('备份目录不存在或不可写,请检查后重试!'); + + // 生成备份文件信息 + $file = array( + 'name' => date('Ymd-His', $this->request->time()), + 'part' => 1, + ); + + // 创建备份文件 + $Database = new DatabaseModel($file, $config); + if(false !== $Database->create()){ + // 备份指定表 + foreach ($tables as $table) { + $start = $Database->backup($table, $start); + while (0 !== $start) { + if (false === $start) { // 出错 + $this->error('备份出错!'); + } + $start = $Database->backup($table, $start[0]); + } + } + + // 备份完成,删除锁定文件 + unlink($lock); + // 记录行为 + action_log('database_export', 'database', 0, UID, implode(',', $tables)); + $this->success('备份完成!'); + } else { + $this->error('初始化失败,备份文件创建失败!'); + } + } else { + $this->error('参数错误!'); + } + } + + /** + * 还原数据库(参考onthink 麦当苗儿 ) + * @param int $time 文件时间戳 + * @author 蔡伟明 <314013107@qq.com> + */ + public function import($time = 0) + { + if ($time === 0) $this->error('参数错误!'); + + // 初始化 + $name = date('Ymd-His', $time) . '-*.sql*'; + $path = realpath(config('data_backup_path')) . DIRECTORY_SEPARATOR . $name; + $files = glob($path); + $list = array(); + foreach($files as $name){ + $basename = basename($name); + $match = sscanf($basename, '%4s%2s%2s-%2s%2s%2s-%d'); + $gz = preg_match('/^\d{8,8}-\d{6,6}-\d+\.sql.gz$/', $basename); + $list[$match[6]] = array($match[6], $name, $gz); + } + ksort($list); + + // 检测文件正确性 + $last = end($list); + if(count($list) === $last[0]){ + foreach ($list as $item) { + $config = [ + 'path' => realpath(config('data_backup_path')) . DIRECTORY_SEPARATOR, + 'compress' => $item[2] + ]; + $Database = new DatabaseModel($item, $config); + $start = $Database->import(0); + + // 循环导入数据 + while (0 !== $start) { + if (false === $start) { // 出错 + $this->error('还原数据出错!'); + } + $start = $Database->import($start[0]); + } + } + // 记录行为 + action_log('database_import', 'database', 0, UID, date('Ymd-His', $time)); + $this->success('还原完成!'); + } else { + $this->error('备份文件可能已经损坏,请检查!'); + } + } + + /** + * 优化表 + * @param null|string|array $ids 表名 + * @author 蔡伟明 <314013107@qq.com> + */ + public function optimize($ids = null) + { + $tables = $ids; + if($tables) { + if(is_array($tables)){ + $tables = implode('`,`', $tables); + $list = Db::query("OPTIMIZE TABLE `{$tables}`"); + + if($list){ + // 记录行为 + action_log('database_optimize', 'database', 0, UID, "`{$tables}`"); + $this->success("数据表优化完成!"); + } else { + $this->error("数据表优化出错请重试!"); + } + } else { + $list = Db::query("OPTIMIZE TABLE `{$tables}`"); + if($list){ + // 记录行为 + action_log('database_optimize', 'database', 0, UID, $tables); + $this->success("数据表'{$tables}'优化完成!"); + } else { + $this->error("数据表'{$tables}'优化出错请重试!"); + } + } + } else { + $this->error("请选择要优化的表!"); + } + } + + /** + * 修复表 + * @param null|string|array $ids 表名 + * @author 蔡伟明 <314013107@qq.com> + */ + public function repair($ids = null) + { + $tables = $ids; + if($tables) { + if(is_array($tables)){ + $tables = implode('`,`', $tables); + $list = Db::query("REPAIR TABLE `{$tables}`"); + + if($list){ + // 记录行为 + action_log('database_repair', 'database', 0, UID, "`{$tables}`"); + $this->success("数据表修复完成!"); + } else { + $this->error("数据表修复出错请重试!"); + } + } else { + $list = Db::query("REPAIR TABLE `{$tables}`"); + if($list){ + // 记录行为 + action_log('database_repair', 'database', 0, UID, $tables); + $this->success("数据表'{$tables}'修复完成!"); + } else { + $this->error("数据表'{$tables}'修复出错请重试!"); + } + } + } else { + $this->error("请指定要修复的表!"); + } + } + + /** + * 删除备份文件 + * @param int $ids 备份时间 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($ids = 0) + { + if ($ids == 0) $this->error('参数错误!'); + + $name = date('Ymd-His', $ids) . '-*.sql*'; + $path = realpath(config('data_backup_path')) . DIRECTORY_SEPARATOR . $name; + array_map("unlink", glob($path)); + if(count(glob($path))){ + $this->error('备份文件删除失败,请检查权限!'); + } else { + // 记录行为 + action_log('database_backup_delete', 'database', 0, UID, date('Ymd-His', $ids)); + $this->success('备份文件删除成功!'); + } + } +} \ No newline at end of file diff --git a/application/admin/controller/Hook.php b/application/admin/controller/Hook.php new file mode 100644 index 0000000..6ac5f5a --- /dev/null +++ b/application/admin/controller/Hook.php @@ -0,0 +1,242 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\exception\DbException + */ + public function index() + { + $map = $this->getMap(); + $order = $this->getOrder(); + + // 数据列表 + $data_list = HookModel::where($map)->order($order)->paginate(); + + // 分页数据 + $page = $data_list->render(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('钩子管理') // 设置页面标题 + ->setSearch(['name' => '钩子名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['name', '名称'], + ['description', '描述'], + ['plugin', '所属插件', 'callback', function($plugin){ + return $plugin == '' ? '系统' : $plugin; + }], + ['system', '系统钩子', 'yesno'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addOrder('name,status') + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons('edit,delete') // 批量添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + $data['system'] = 1; + + // 验证 + $result = $this->validate($data, 'Hook'); + if(true !== $result) $this->error($result); + + if ($hook = HookModel::create($data)) { + cache('hook_plugins', null); + // 记录行为 + action_log('hook_add', 'admin_hook', $hook['id'], UID, $data['name']); + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('新增') + ->addText('name', '钩子名称', '由字母和下划线组成,如:page_tips') + ->addText('description', '钩子描述') + ->fetch(); + } + + /** + * 编辑 + * @param int $id 钩子id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function edit($id = 0) + { + if ($id === 0) $this->error('参数错误'); + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + // 验证 + $result = $this->validate($data, 'Hook'); + if(true !== $result) $this->error($result); + + if ($hook = HookModel::update($data)) { + // 调整插件顺序 + if ($data['sort'] != '') { + HookPluginModel::sort($data['name'], $data['sort']); + } + cache('hook_plugins', null); + // 记录行为 + action_log('hook_edit', 'admin_hook', $hook['id'], UID, $data['name']); + $this->success('编辑成功', 'index'); + } else { + $this->error('编辑失败'); + } + } + + // 获取数据 + $info = HookModel::get($id); + + // 该钩子的所有插件 + $hooks = HookPluginModel::where('hook', $info['name'])->order('sort')->column('plugin'); + $hooks = parse_array($hooks); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑') + ->addHidden('id') + ->addText('name', '钩子名称', '由字母和下划线组成,如:page_tips') + ->addText('description', '钩子描述') + ->addSort('sort', '插件排序', '', $hooks) + ->setFormData($info) + ->fetch(); + } + + /** + * 快速编辑(启用/禁用) + * @param string $status 状态 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($status = '') + { + $id = $this->request->post('pk'); + $status = $this->request->param('value'); + $hook_name = HookModel::where('id', $id)->value('name'); + + if (false === HookPluginModel::where('hook', $hook_name)->setField('status', $status == 'true' ? 1 : 0)) { + $this->error('操作失败,请重试'); + } + cache('hook_plugins', null); + $details = $status == 'true' ? '启用钩子' : '禁用钩子'; + return parent::quickEdit(['hook_edit', 'admin_hook', $id, UID, $details]); + } + + /** + * 启用 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + /** + * 禁用 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 删除钩子 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + * @throws \think\exception\PDOException + */ + public function delete($record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $map = [ + ['id', 'in', $ids], + ['system', '=', 1], + ]; + if (HookModel::where($map)->find()) { + $this->error('禁止删除系统钩子'); + } + return $this->setStatus('delete'); + } + + /** + * 设置状态 + * @param string $type 类型 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->param('ids/a'); + foreach ($ids as $id) { + $hook_name = HookModel::where('id', $id)->value('name'); + if (false === HookPluginModel::where('hook', $hook_name)->setField('status', $type == 'enable' ? 1 : 0)) { + $this->error('操作失败,请重试'); + } + } + cache('hook_plugins', null); + $hook_delete = is_array($ids) ? '' : $ids; + $hook_names = HookModel::where('id', 'in', $ids)->column('name'); + return parent::setStatus($type, ['hook_'.$type, 'admin_hook', $hook_delete, UID, implode('、', $hook_names)]); + } +} diff --git a/application/admin/controller/Icon.php b/application/admin/controller/Icon.php new file mode 100644 index 0000000..cbe518a --- /dev/null +++ b/application/admin/controller/Icon.php @@ -0,0 +1,268 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\exception\DbException + */ + public function index() + { + $data_list = IconModel::where($this->getMap()) + ->order($this->getOrder('id DESC')) + ->paginate(); + + return ZBuilder::make('table') + ->addTopButtons('add,enable,disable,delete') + ->addRightButton('list', [ + 'title' => '图标列表', + 'icon' => 'fa fa-list', + 'href' => url('items', ['id' => '__id__']) + ]) + ->addRightButton('reload', [ + 'title' => '更新图标', + 'icon' => 'fa fa-refresh', + 'class' => 'btn btn-xs btn-default ajax-get confirm', + 'href' => url('reload', ['id' => '__id__']) + ]) + ->addRightButton('delete') + ->setSearch('name') + ->addColumns([ + ['id', 'ID'], + ['name', '名称', 'text.edit'], + ['url', '链接', 'text.edit'], + ['status', '状态', 'switch'], + ['create_time', '创建时间', 'datetime'], + ['right_button', '操作', 'btn'], + ]) + ->setRowList($data_list) + ->fetch(); + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function add() + { + if ($this->request->isPost()) { + $data = $this->request->post('', null, 'trim'); + $data['name'] == '' && $this->error('请填写名称'); + $data['url'] == '' && $this->error('请填写链接'); + $data['create_time'] = $this->request->time(); + $data['update_time'] = $this->request->time(); + + // 组合图标地址 + $url = substr($data['url'], 0, 4) == 'http' ? $data['url'] : 'http:'.$data['url']; + + // 检查图标url是否合法 + $result = check_icon_url($url); + if (true !== $result) { + $this->error($result['msg']); + } + + // 获取图标内容 + try { + $content = file_get_contents($url); + } catch (\Exception $e) { + $this->error('图标获取失败,请确认地址是否正确'); + } + + // 获取字体名 + $font_family = ''; + $pattern = '/font-family: "(.*)";/'; + if (preg_match($pattern, $content, $match)) { + $font_family = $match[1]; + } else { + $this->error('无法获取字体名'); + } + + $IconModel = new IconModel(); + if ($id = $IconModel->insertGetId($data)) { + // 拉取图标列表 + $pattern = '/\.(.*):before/'; + if (preg_match_all($pattern, $content, $matches)) { + $icon_list = []; + foreach ($matches[1] as $match) { + $icon_list[] = [ + 'icon_id' => $id, + 'title' => $match, + 'class' => $font_family . ' ' . $match, + 'code' => $match, + ]; + } + $IconListModel = new IconListModel(); + if ($IconListModel->saveAll($icon_list)) { + $this->success('新增成功', 'index'); + } else { + $IconModel->where('id', $id)->delete(); + $this->error('图标添加失败'); + } + } + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + return ZBuilder::make('form') + ->addFormItems([ + ['text', 'name', '名称', '可填写中文'], + ['text', 'url', '链接', '如://at.alicdn.com/t/font_588968_z5hsg7xluoh41jor.css'], + ]) + ->fetch(); + } + + /** + * 图标列表 + * @param string $id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + * @throws \think\exception\DbException + */ + public function items($id = '') + { + $data_list = IconListModel::where($this->getMap()) + ->order($this->getOrder('id DESC')) + ->where('icon_id', $id) + ->paginate(); + + return ZBuilder::make('table') + ->setTableName('admin_icon_list') + ->addTopButtons('back') + ->addTopButton('add', [ + 'title' => '更新图标', + 'icon' => 'fa fa-refresh', + 'class' => 'btn btn-primary ajax-get confirm', + 'href' => url('reload', ['id' => $id]) + ]) + ->setSearch('title,code') + ->addColumns([ + ['icon', '图标', 'callback', function($data){ + return ''; + }, '__data__'], + ['title', '图标标题', 'text.edit'], + ['code', '图标关键词', 'text.edit'], + ['class', '图标类名'], + ]) + ->setRowList($data_list) + ->fetch(); + } + + /** + * 更新图标 + * @param string $id + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function reload($id = '') + { + $icon = IconModel::get($id); + // 获取图标信息 + $url = substr($icon['url'], 0, 4) == 'http' ? $icon['url'] : 'http:'.$icon['url']; + try { + $content = file_get_contents($url); + } catch (\Exception $e) { + $this->error('图标获取失败,请确认地址是否正确'); + } + + // 获取字体名 + $font_family = ''; + $pattern = '/font-family: "(.*)";/'; + if (preg_match($pattern, $content, $match)) { + $font_family = $match[1]; + } else { + $this->error('无法获取字体名'); + } + + // 拉取图标列表 + $pattern = '/\.(.*):before/'; + if (preg_match_all($pattern, $content, $matches)) { + $icon_list = []; + foreach ($matches[1] as $match) { + $icon_list[] = [ + 'icon_id' => $id, + 'title' => $match, + 'class' => $font_family . ' ' . $match, + 'code' => $match, + ]; + } + $IconListModel = new IconListModel(); + $IconListModel->where('icon_id', $id)->delete(); + if ($IconListModel->saveAll($icon_list)) { + $this->success('更新成功'); + } else { + $this->error('图标添加失败'); + } + } + $this->success('更新成功'); + } + + /** + * 删除图标库 + * @param string $ids + * @throws \think\Exception + * @throws \think\exception\PDOException + * @author 蔡伟明 <314013107@qq.com> + */ + public function delete($ids = '') + { + $ids == '' && $this->error('请选择要删除的数据'); + $ids = (array)$ids; + + // 删除图标列表 + if (false !== IconListModel::where('icon_id', 'in', $ids)->delete()) { + // 删除图标库 + if (false !== IconModel::where('id', 'in', $ids)->delete()) { + $this->success('删除成功'); + } + } + $this->error('删除失败'); + } + + /** + * 快捷编辑 + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + */ + public function quickEdit($record = []) + { + $url = $this->request->param('value', ''); + $url == '' && $this->error('请填写图标地址'); + // 组合图标地址 + $url = substr($url, 0, 4) == 'http' ? $url : 'http:'.$url; + + // 检查图标url是否合法 + $result = check_icon_url($url); + if (true !== $result) { + $this->error($result['msg']); + } + + parent::quickEdit($record); + } +} diff --git a/application/admin/controller/Ie.php b/application/admin/controller/Ie.php new file mode 100644 index 0000000..ed16636 --- /dev/null +++ b/application/admin/controller/Ie.php @@ -0,0 +1,33 @@ + + * @return mixed + */ + public function index(){ + // ie浏览器判断 + if (get_browser_type() == 'ie') { + return $this->fetch(); + } else { + $this->redirect('admin/index/index'); + } + } +} \ No newline at end of file diff --git a/application/admin/controller/Index.php b/application/admin/controller/Index.php new file mode 100644 index 0000000..77964c0 --- /dev/null +++ b/application/admin/controller/Index.php @@ -0,0 +1,165 @@ + + * @return string + */ + public function index() + { + $admin_pass = Db::name('admin_user')->where('id', 1)->value('password'); + + if (UID == 1 && $admin_pass && Hash::check('admin', $admin_pass)) { + $this->assign('default_pass', 1); + } + return $this->fetch(); + } + + /** + * 清空系统缓存 + * @author 蔡伟明 <314013107@qq.com> + */ + public function wipeCache() + { + $wipe_cache_type = config('wipe_cache_type'); + if (!empty($wipe_cache_type)) { + foreach ($wipe_cache_type as $item) { + switch ($item) { + case 'TEMP_PATH': + array_map('unlink', glob(Env::get('runtime_path'). 'temp/*.*')); + break; + case 'LOG_PATH': + $dirs = (array) glob(Env::get('runtime_path') . 'log/*'); + foreach ($dirs as $dir) { + array_map('unlink', glob($dir . '/*.log')); + } + array_map('rmdir', $dirs); + break; + case 'CACHE_PATH': + array_map('unlink', glob(Env::get('runtime_path'). 'cache/*.*')); + break; + } + } + Cache::clear(); + $this->success('清空成功'); + } else { + $this->error('请在系统设置中选择需要清除的缓存类型'); + } + } + + /** + * 个人设置 + * @author 蔡伟明 <314013107@qq.com> + */ + public function profile() + { + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + $data['nickname'] == '' && $this->error('昵称不能为空'); + $data['id'] = UID; + + // 如果没有填写密码,则不更新密码 + if ($data['password'] == '') { + unset($data['password']); + } + + $UserModel = new UserModel(); + if ($user = $UserModel->allowField(['nickname', 'email', 'password', 'mobile', 'avatar'])->update($data)) { + // 记录行为 + action_log('user_edit', 'admin_user', UID, UID, get_nickname(UID)); + $this->success('编辑成功'); + } else { + $this->error('编辑失败'); + } + } + + // 获取数据 + $info = UserModel::where('id', UID)->field('password', true)->find(); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->addFormItems([ // 批量添加表单项 + ['static', 'username', '用户名', '不可更改'], + ['text', 'nickname', '昵称', '可以是中文'], + ['text', 'email', '邮箱', ''], + ['password', 'password', '密码', '必填,6-20位'], + ['text', 'mobile', '手机号'], + ['image', 'avatar', '头像'] + ]) + ->setFormData($info) // 设置表单数据 + ->fetch(); + } + + /** + * 检查版本更新 + * @author 蔡伟明 <314013107@qq.com> + * @return \think\response\Json + * @throws \think\db\exception\BindParamException + * @throws \think\exception\PDOException + */ + public function checkUpdate() + { + $params = config('dolphin.'); + $params['domain'] = request()->domain(); + $params['website'] = config('web_site_title'); + $params['ip'] = $_SERVER['SERVER_ADDR']; + $params['php_os'] = PHP_OS; + $params['php_version'] = PHP_VERSION; + $params['mysql_version'] = db()->query('select version() as version')[0]['version']; + $params['server_software'] = $_SERVER['SERVER_SOFTWARE']; + $params = http_build_query($params); + + $opts = [ + CURLOPT_TIMEOUT => 20, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_URL => config('dolphin.product_update'), + CURLOPT_USERAGENT => $_SERVER['HTTP_USER_AGENT'], + CURLOPT_POST => 1, + CURLOPT_POSTFIELDS => $params + ]; + + // 初始化并执行curl请求 + $ch = curl_init(); + curl_setopt_array($ch, $opts); + $data = curl_exec($ch); + curl_close($ch); + + $result = json_decode($data, true); + + if ($result['code'] == 1) { + return json([ + 'update' => '有新版本:'.$result["version"].'', + 'auth' => $result['auth'] + ]); + } else { + return json([ + 'update' => '', + 'auth' => $result['auth'] + ]); + } + } +} \ No newline at end of file diff --git a/application/admin/controller/Log.php b/application/admin/controller/Log.php new file mode 100644 index 0000000..1bd0ed8 --- /dev/null +++ b/application/admin/controller/Log.php @@ -0,0 +1,92 @@ + + * @return mixed + * @throws \think\Exception + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('admin_log.id desc'); + // 数据列表 + $data_list = LogModel::getAll($map, $order); + // 分页数据 + $page = $data_list->render(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('系统日志') // 设置页面标题 + ->setSearch(['admin_action.title' => '行为名称', 'admin_user.username' => '执行者', 'admin_module.title' => '所属模块']) // 设置搜索框 + ->hideCheckbox() + ->addColumns([ // 批量添加数据列 + ['id', '编号'], + ['title', '行为名称'], + ['username', '执行者'], + ['action_ip', '执行IP', 'callback', function($value){ + return long2ip(intval($value)); + }], + ['module_title', '所属模块'], + ['create_time', '执行时间', 'datetime', '', 'Y-m-d H:i:s'], + ['right_button', '操作', 'btn'] + ]) + ->addOrder(['title' => 'admin_action', 'username' => 'admin_user', 'module_title' => 'admin_module.title']) + ->addFilter(['admin_action.title', 'admin_user.username', 'module_title' => 'admin_module.title']) + ->addRightButton('details', ['icon' => 'fa fa-eye', 'title' => '详情', 'href' => url('details', ['id' => '__id__'])]) + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } + + /** + * 日志详情 + * @param null $id 日志id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function details($id = null) + { + if ($id === null) $this->error('缺少参数'); + $info = LogModel::getAll(['admin_log.id' => $id]); + $info = $info[0]; + $info['action_ip'] = long2ip(intval($info['action_ip'])); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑') // 设置页面标题 + ->addFormItems([ // 批量添加表单项 + ['hidden', 'id'], + ['static', 'title', '行为名称'], + ['static', 'username', '执行者'], + ['static', 'record_id', '目标ID'], + ['static', 'action_ip', '执行IP'], + ['static', 'module_title', '所属模块'], + ['textarea', 'remark', '备注'], + ]) + ->hideBtn('submit') + ->setFormData($info) // 设置表单数据 + ->fetch(); + } +} \ No newline at end of file diff --git a/application/admin/controller/Menu.php b/application/admin/controller/Menu.php new file mode 100644 index 0000000..e515fab --- /dev/null +++ b/application/admin/controller/Menu.php @@ -0,0 +1,545 @@ + + * @return mixed + * @throws \Exception + */ + public function index($group = 'admin') + { + // 保存模块排序 + if ($this->request->isPost()) { + $modules = $this->request->post('sort/a'); + if ($modules) { + $data = []; + foreach ($modules as $key => $module) { + $data[] = [ + 'id' => $module, + 'sort' => $key + 1 + ]; + } + $MenuModel = new MenuModel(); + if (false !== $MenuModel->saveAll($data)) { + $this->success('保存成功'); + } else { + $this->error('保存失败'); + } + } + } + + cookie('__forward__', $_SERVER['REQUEST_URI']); + // 配置分组信息 + $list_group = MenuModel::getGroup(); + foreach ($list_group as $key => $value) { + $tab_list[$key]['title'] = $value; + $tab_list[$key]['url'] = url('index', ['group' => $key]); + } + + // 模块排序 + if ($group == 'module-sort') { + $map['status'] = 1; + $map['pid'] = 0; + $modules = MenuModel::where($map)->order('sort,id')->column('icon,title', 'id'); + $this->assign('modules', $modules); + } else { + // 获取节点数据 + $data_list = MenuModel::getMenusByGroup($group); + + $max_level = $this->request->get('max', 0); + + $this->assign('menus', $this->getNestMenu($data_list, $max_level)); + } + + $this->assign('tab_nav', ['tab_list' => $tab_list, 'curr_tab' => $group]); + $this->assign('page_title', '节点管理'); + return $this->fetch(); + } + + /** + * 新增节点 + * @param string $module 所属模块 + * @param string $pid 所属节点id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \Exception + */ + public function add($module = 'admin', $pid = '') + { + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post('', null, 'trim'); + + // 验证 + $result = $this->validate($data, 'Menu'); + // 验证失败 输出错误信息 + if(true !== $result) $this->error($result); + + // 顶部节点url检查 + if ($data['pid'] == 0 && $data['url_value'] == '' && ($data['url_type'] == 'module_admin' || $data['url_type'] == 'module_home')) { + $this->error('顶级节点的节点链接不能为空'); + } + + if ($menu = MenuModel::create($data)) { + // 自动创建子节点 + if ($data['auto_create'] == 1 && !empty($data['child_node'])) { + unset($data['icon']); + unset($data['params']); + $this->createChildNode($data, $menu['id']); + } + // 添加角色权限 + if (isset($data['role'])) { + $this->setRoleMenu($menu['id'], $data['role']); + } + Cache::clear(); + // 记录行为 + $details = '所属模块('.$data['module'].'),所属节点ID('.$data['pid'].'),节点标题('.$data['title'].'),节点链接('.$data['url_value'].')'; + action_log('menu_add', 'admin_menu', $menu['id'], UID, $details); + $this->success('新增成功', cookie('__forward__')); + } else { + $this->error('新增失败'); + } + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('新增节点') + ->addLinkage('module', '所属模块', '', ModuleModel::getModule(), $module, url('ajax/getModuleMenus'), 'pid') + ->addFormItems([ + ['select', 'pid', '所属节点', '所属上级节点', MenuModel::getMenuTree(0, '', $module), $pid], + ['text', 'title', '节点标题'], + ['radio', 'url_type', '链接类型', '', ['module_admin' => '模块链接(后台)', 'module_home' => '模块链接(前台)', 'link' => '普通链接'], 'module_admin'] + ]) + ->addFormItem( + 'text', + 'url_value', + '节点链接', + "可留空,如果是模块链接,请填写模块/控制器/操作,如:admin/menu/add。如果是普通链接,则直接填写url地址,如:http://www.dolphinphp.com" + ) + ->addText('params', '参数', '如:a=1&b=2') + ->addSelect('role', '角色', '除超级管理员外,拥有该节点权限的角色', RoleModel::where('id', 'neq', 1)->column('id,name'), '', 'multiple') + ->addRadio('auto_create', '自动添加子节点', '选择【是】则自动添加指定的子节点', ['否', '是'], 0) + ->addCheckbox('child_node', '子节点', '仅上面选项为【是】时起作用', ['add' => '新增', 'edit' => '编辑', 'delete' => '删除', 'enable' => '启用', 'disable' => '禁用', 'quickedit' => '快速编辑'], 'add,edit,delete,enable,disable,quickedit') + ->addRadio('url_target', '打开方式', '', ['_self' => '当前窗口', '_blank' => '新窗口'], '_self') + ->addIcon('icon', '图标', '导航图标') + ->addRadio('online_hide', '网站上线后隐藏', '关闭开发模式后,则隐藏该菜单节点', ['否', '是'], 0) + ->addText('sort', '排序', '', 100) + ->setTrigger('auto_create', '1', 'child_node', false) + ->fetch(); + } + + /** + * 编辑节点 + * @param int $id 节点ID + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \Exception + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function edit($id = 0) + { + if ($id === 0) $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post('', null, 'trim'); + + // 验证 + $result = $this->validate($data, 'Menu'); + // 验证失败 输出错误信息 + if(true !== $result) $this->error($result); + + // 顶部节点url检查 + if ($data['pid'] == 0 && $data['url_value'] == '' && ($data['url_type'] == 'module_admin' || $data['url_type'] == 'module_home')) { + $this->error('顶级节点的节点链接不能为空'); + } + + // 设置角色权限 + $this->setRoleMenu($data['id'], isset($data['role']) ? $data['role'] : []); + + // 验证是否更改所属模块,如果是,则该节点的所有子孙节点的模块都要修改 + $map['id'] = $data['id']; + $map['module'] = $data['module']; + if (!MenuModel::where($map)->find()) { + MenuModel::changeModule($data['id'], $data['module']); + } + + if (MenuModel::update($data)) { + Cache::clear(); + // 记录行为 + $details = '节点ID('.$id.')'; + action_log('menu_edit', 'admin_menu', $id, UID, $details); + $this->success('编辑成功', cookie('__forward__')); + } else { + $this->error('编辑失败'); + } + } + + // 获取数据 + $info = MenuModel::get($id); + // 拥有该节点权限的角色 + $info['role'] = RoleModel::getRoleWithMenu($id); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑节点') + ->addFormItem('hidden', 'id') + ->addLinkage('module', '所属模块', '', ModuleModel::getModule(), '', url('ajax/getModuleMenus'), 'pid') + ->addFormItem('select', 'pid', '所属节点', '所属上级节点', MenuModel::getMenuTree(0, '', $info['module'])) + ->addFormItem('text', 'title', '节点标题') + ->addFormItem('radio', 'url_type', '链接类型', '', ['module_admin' => '模块链接(后台)', 'module_home' => '模块链接(前台)', 'link' => '普通链接'], 'module_admin') + ->addFormItem( + 'text', + 'url_value', + '节点链接', + "可留空,如果是模块链接,请填写模块/控制器/操作,如:admin/menu/add。如果是普通链接,则直接填写url地址,如:http://www.dolphinphp.com" + ) + ->addText('params', '参数', '如:a=1&b=2') + ->addSelect('role', '角色', '除超级管理员外,拥有该节点权限的角色', RoleModel::where('id', 'neq', 1)->column('id,name'), '', 'multiple') + ->addRadio('url_target', '打开方式', '', ['_self' => '当前窗口', '_blank' => '新窗口'], '_self') + ->addIcon('icon', '图标', '导航图标') + ->addRadio('online_hide', '网站上线后隐藏', '关闭开发模式后,则隐藏该菜单节点', ['否', '是']) + ->addText('sort', '排序', '', 100) + ->setFormData($info) + ->fetch(); + } + + /** + * 设置角色权限 + * @param string $role_id 角色id + * @param array $roles 角色id + * @author 蔡伟明 <314013107@qq.com> + * @throws \Exception + */ + private function setRoleMenu($role_id = '', $roles = []) + { + $RoleModel = new RoleModel(); + + // 该节点的所有子节点,包括本身节点 + $menu_child = MenuModel::getChildsId($role_id); + $menu_child[] = (int)$role_id; + // 该节点的所有上下级节点 + $menu_all = MenuModel::getLinkIds($role_id); + $menu_all = array_map('strval', $menu_all); + + if (!empty($roles)) { + // 拥有该节点的所有角色id及节点权限 + $role_menu_auth = RoleModel::getRoleWithMenu($role_id, true); + // 已有该节点权限的角色id + $role_exists = array_keys($role_menu_auth); + // 新节点权限的角色 + $role_new = $roles; + // 原有权限角色差集 + $role_diff = array_diff($role_exists, $role_new); + // 新权限角色差集 + $role_diff_new = array_diff($role_new, $role_exists); + // 新节点角色权限 + $role_new_auth = RoleModel::getAuthWithRole($roles); + + // 删除原先角色的该节点权限 + if ($role_diff) { + $role_del_auth = []; + foreach ($role_diff as $role) { + $auth = json_decode($role_menu_auth[$role], true); + $auth_new = array_diff($auth, $menu_child); + $role_del_auth[] = [ + 'id' => $role, + 'menu_auth' => array_values($auth_new) + ]; + } + if ($role_del_auth) { + $RoleModel->saveAll($role_del_auth); + } + } + + // 新增权限角色 + if ($role_diff_new) { + $role_update_auth = []; + foreach ($role_new_auth as $role => $auth) { + $auth = json_decode($auth, true); + if (in_array($role, $role_diff_new)) { + $auth = array_unique(array_merge($auth, $menu_all)); + } + $role_update_auth[] = [ + 'id' => $role, + 'menu_auth' => array_values($auth) + ]; + } + if ($role_update_auth) { + $RoleModel->saveAll($role_update_auth); + } + } + } else { + $role_menu_auth = RoleModel::getRoleWithMenu($role_id, true); + $role_del_auth = []; + foreach ($role_menu_auth as $role => $auth) { + $auth = json_decode($auth, true); + $auth_new = array_diff($auth, $menu_child); + $role_del_auth[] = [ + 'id' => $role, + 'menu_auth' => array_values($auth_new) + ]; + } + if ($role_del_auth) { + $RoleModel->saveAll($role_del_auth); + } + } + } + + /** + * 删除节点 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function delete($record = []) + { + $id = $this->request->param('id'); + $menu = MenuModel::where('id', $id)->find(); + + if ($menu['system_menu'] == '1') $this->error('系统节点,禁止删除'); + + // 获取该节点的所有后辈节点id + $menu_childs = MenuModel::getChildsId($id); + + // 要删除的所有节点id + $all_ids = array_merge([(int)$id], $menu_childs); + + // 删除节点 + if (MenuModel::destroy($all_ids)) { + Cache::clear(); + // 记录行为 + $details = '节点ID('.$id.'),节点标题('.$menu['title'].'),节点链接('.$menu['url_value'].')'; + action_log('menu_delete', 'admin_menu', $id, UID, $details); + $this->success('删除成功'); + } else { + $this->error('删除失败'); + } + } + + /** + * 保存节点排序 + * @author 蔡伟明 <314013107@qq.com> + */ + public function save() + { + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!empty($data)) { + $menus = $this->parseMenu($data['menus']); + foreach ($menus as $menu) { + if ($menu['pid'] == 0) { + continue; + } + MenuModel::update($menu); + } + Cache::clear(); + $this->success('保存成功'); + } else { + $this->error('没有需要保存的节点'); + } + } + $this->error('非法请求'); + } + + /** + * 添加子节点 + * @param array $data 节点数据 + * @param string $pid 父节点id + * @author 蔡伟明 <314013107@qq.com> + */ + private function createChildNode($data = [], $pid = '') + { + $url_value = substr($data['url_value'], 0, strrpos($data['url_value'], '/')).'/'; + $child_node = []; + $data['pid'] = $pid; + + foreach ($data['child_node'] as $item) { + switch ($item) { + case 'add': + $data['title'] = '新增'; + break; + case 'edit': + $data['title'] = '编辑'; + break; + case 'delete': + $data['title'] = '删除'; + break; + case 'enable': + $data['title'] = '启用'; + break; + case 'disable': + $data['title'] = '禁用'; + break; + case 'quickedit': + $data['title'] = '快速编辑'; + break; + } + $data['url_value'] = $url_value.$item; + $data['create_time'] = $this->request->time(); + $data['update_time'] = $this->request->time(); + $child_node[] = $data; + } + + if ($child_node) { + $MenuModel = new MenuModel(); + $MenuModel->insertAll($child_node); + } + } + + /** + * 递归解析节点 + * @param array $menus 节点数据 + * @param int $pid 上级节点id + * @author 蔡伟明 <314013107@qq.com> + * @return array 解析成可以写入数据库的格式 + */ + private function parseMenu($menus = [], $pid = 0) + { + $sort = 1; + $result = []; + foreach ($menus as $menu) { + $result[] = [ + 'id' => (int)$menu['id'], + 'pid' => (int)$pid, + 'sort' => $sort, + ]; + if (isset($menu['children'])) { + $result = array_merge($result, $this->parseMenu($menu['children'], $menu['id'])); + } + $sort ++; + } + return $result; + } + + /** + * 获取嵌套式节点 + * @param array $lists 原始节点数组 + * @param int $pid 父级id + * @param int $max_level 最多返回多少层,0为不限制 + * @param int $curr_level 当前层数 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + private function getNestMenu($lists = [], $max_level = 0, $pid = 0, $curr_level = 1) + { + $result = ''; + foreach ($lists as $key => $value) { + if ($value['pid'] == $pid) { + $disable = $value['status'] == 0 ? 'dd-disable' : ''; + + // 组合节点 + $result .= '
  • '; + $result .= '
    拖拽
    '.$value['title']; + if ($value['url_value'] != '') { + $result .= ' '.$value['url_value'].''; + } + $result .= '
    '; + $result .= ''; + if ($value['status'] == 0) { + // 启用 + $result .= ''; + } else { + // 禁用 + $result .= ''; + } + $result .= '
    '; + $result .= '
    '; + + if ($max_level == 0 || $curr_level != $max_level) { + unset($lists[$key]); + // 下级节点 + $children = $this->getNestMenu($lists, $max_level, $value['id'], $curr_level + 1); + if ($children != '') { + $result .= '
      '.$children.'
    '; + } + } + + $result .= '
  • '; + } + } + return $result; + } + + /** + * 启用节点 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function enable($record = []) + { + $id = input('param.ids'); + $menu = MenuModel::where('id', $id)->find(); + $details = '节点ID('.$id.'),节点标题('.$menu['title'].'),节点链接('.$menu['url_value'].')'; + $this->setStatus('enable', ['menu_enable', 'admin_menu', $id, UID, $details]); + } + + /** + * 禁用节点 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function disable($record = []) + { + $id = input('param.ids'); + $menu = MenuModel::where('id', $id)->find(); + $details = '节点ID('.$id.'),节点标题('.$menu['title'].'),节点链接('.$menu['url_value'].')'; + $this->setStatus('disable', ['menu_disable', 'admin_menu', $id, UID, $details]); + } + + /** + * 设置状态 + * @param string $type 类型 + * @param array $record 行为日志 + * @author 小乌 <82950492@qq.com> + */ + public function setStatus($type = '', $record = []) + { + $id = input('param.ids'); + + $status = $type == 'enable' ? 1 : 0; + + if (false !== MenuModel::where('id', $id)->setField('status', $status)) { + Cache::clear(); + // 记录行为日志 + if (!empty($record)) { + call_user_func_array('action_log', $record); + } + $this->success('操作成功'); + } else { + $this->error('操作失败'); + } + } +} diff --git a/application/admin/controller/Message.php b/application/admin/controller/Message.php new file mode 100644 index 0000000..c7b5335 --- /dev/null +++ b/application/admin/controller/Message.php @@ -0,0 +1,78 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\exception\DbException + */ + public function index() + { + $data_list = MessageModel::where($this->getMap()) + ->where('uid_receive', UID) + ->order($this->getOrder('id DESC')) + ->paginate(); + + return ZBuilder::make('table') + ->setTableName('admin_message') + ->addTopButton('enable', ['title' => '设置已阅读']) + ->addTopButton('delete') + ->addRightButton('enable', ['title' => '设置已阅读']) + ->addRightButton('delete') + ->addColumns([ + ['uid_send', '发送者', 'callback', 'get_nickname'], + ['type', '分类'], + ['content', '内容'], + ['status', '状态', 'status', '', ['未读', '已读']], + ['create_time', '发送时间', 'datetime'], + ['read_time', '阅读时间', 'datetime'], + ['right_button', '操作', 'btn'], + ]) + ->addFilter('type') + ->addFilter('status', ['未读', '已读']) + ->setRowList($data_list) + ->fetch(); + } + + /** + * 设置已阅读 + * @param array $ids + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function enable($ids = []) + { + empty($ids) && $this->error('参数错误'); + $map = [ + ['uid_receive', '=', UID], + ['id', 'in', $ids] + ]; + $result = MessageModel::where($map) + ->update(['status' => 1, 'read_time' => $this->request->time()]); + if (false !== $result) { + $this->success('设置成功'); + } else { + $this->error('设置失败'); + } + } +} \ No newline at end of file diff --git a/application/admin/controller/Module.php b/application/admin/controller/Module.php new file mode 100644 index 0000000..198f819 --- /dev/null +++ b/application/admin/controller/Module.php @@ -0,0 +1,643 @@ + + * @return mixed + */ + public function index($group = 'local', $type = '') + { + // 配置分组信息 + $list_group = ['local' => '本地模块']; + $tab_list = []; + foreach ($list_group as $key => $value) { + $tab_list[$key]['title'] = $value; + $tab_list[$key]['url'] = url('index', ['group' => $key]); + } + + // 监听tab钩子 + Hook::listen('module_index_tab_list', $tab_list); + + switch ($group) { + case 'local': + // 查询条件 + $keyword = $this->request->get('keyword', ''); + + if (input('?param.status') && input('param.status') != '_all') { + $status = input('param.status'); + } else { + $status = ''; + } + + $ModuleModel = new ModuleModel(); + $result = $ModuleModel->getAll($keyword, $status); + + if ($result['modules'] === false) { + $this->error($ModuleModel->getError()); + } + + $type_show = Cache::get('module_type_show'); + $type_show = $type != '' ? $type : ($type_show == false ? 'block' : $type_show); + Cache::set('module_type_show', $type_show); + $type = $type_show == 'block' ? 'list' : 'block'; + + $this->assign('page_title', '模块管理'); + $this->assign('modules', $result['modules']); + $this->assign('total', $result['total']); + $this->assign('tab_nav', ['tab_list' => $tab_list, 'curr_tab' => $group]); + $this->assign('type', $type); + return $this->fetch(); + break; + case 'online': + return '

    正在建设中...

    '; + break; + default: + $this->error('非法操作'); + } + } + + /** + * 安装模块 + * @param string $name 模块标识 + * @param int $confirm 是否确认 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function install($name = '', $confirm = 0) + { + // 设置最大执行时间和内存大小 + ini_set('max_execution_time', '0'); + ini_set('memory_limit', '1024M'); + + if ($name == '') $this->error('模块不存在!'); + if ($name == 'admin' || $name == 'user') $this->error('禁止操作系统核心模块!'); + + // 模块配置信息 + $module_info = ModuleModel::getInfoFromFile($name); + if (empty($module_info)) { + $this->error('模块不存在!'); + } + + if ($confirm == 0) { + $need_module = []; + $need_plugin = []; + $table_check = []; + // 检查模块依赖 + if (isset($module_info['need_module']) && !empty($module_info['need_module'])) { + $need_module = $this->checkDependence('module', $module_info['need_module']); + } + + // 检查插件依赖 + if (isset($module_info['need_plugin']) && !empty($module_info['need_plugin'])) { + $need_plugin = $this->checkDependence('plugin', $module_info['need_plugin']); + } + + // 检查数据表 + if (isset($module_info['tables']) && !empty($module_info['tables'])) { + foreach ($module_info['tables'] as $table) { + if (Db::query("SHOW TABLES LIKE '".config('database.prefix')."{$table}'")) { + $table_check[] = [ + 'table' => config('database.prefix')."{$table}", + 'result' => '存在同名' + ]; + } else { + $table_check[] = [ + 'table' => config('database.prefix')."{$table}", + 'result' => '' + ]; + } + } + } + + $this->assign('need_module', $need_module); + $this->assign('need_plugin', $need_plugin); + $this->assign('table_check', $table_check); + $this->assign('name', $name); + $this->assign('page_title', '安装模块:'. $name); + return $this->fetch(); + } + + // 执行安装文件 + $install_file = realpath(Env::get('app_path').$name.'/install.php'); + if (file_exists($install_file)) { + @include($install_file); + } + + // 执行安装模块sql文件 + $sql_file = realpath(Env::get('app_path').$name.'/sql/install.sql'); + if (file_exists($sql_file)) { + if (isset($module_info['database_prefix']) && !empty($module_info['database_prefix'])) { + $sql_statement = Sql::getSqlFromFile($sql_file, false, [$module_info['database_prefix'] => config('database.prefix')]); + } else { + $sql_statement = Sql::getSqlFromFile($sql_file); + } + if (!empty($sql_statement)) { + foreach ($sql_statement as $value) { + try{ + Db::execute($value); + }catch(\Exception $e){ + $this->error('导入SQL失败,请检查install.sql的语句是否正确'); + } + } + } + } + + // 添加菜单 + $menus = ModuleModel::getMenusFromFile($name); + if (is_array($menus) && !empty($menus)) { + if (false === $this->addMenus($menus, $name)) { + $this->error('菜单添加失败,请重新安装'); + } + } + + // 检查是否有模块设置信息 + if (isset($module_info['config']) && !empty($module_info['config'])) { + $module_info['config'] = json_encode(parse_config($module_info['config'])); + } + + // 检查是否有模块授权配置 + if (isset($module_info['access']) && !empty($module_info['access'])) { + $module_info['access'] = json_encode($module_info['access']); + } + + // 检查是否有行为规则 + if (isset($module_info['action']) && !empty($module_info['action'])) { + $ActionModel = new ActionModel; + if (!$ActionModel->saveAll($module_info['action'])) { + MenuModel::where('module', $name)->delete(); + $this->error('行为添加失败,请重新安装'); + } + } + + // 将模块信息写入数据库 + $ModuleModel = new ModuleModel($module_info); + $allowField = ['name','title','icon','description','author','author_url','config','access','version','identifier','status']; + + if ($ModuleModel->allowField($allowField)->save()) { + // 复制静态资源目录 + File::copy_dir(Env::get('app_path'). $name. '/public', Env::get('root_path'). 'public'); + // 删除静态资源目录 + File::del_dir(Env::get('app_path'). $name. '/public'); + cache('modules', null); + cache('module_all', null); + // 记录行为 + action_log('module_install', 'admin_module', 0, UID, $module_info['title']); + $this->success('模块安装成功', 'index'); + } else { + MenuModel::where('module', $name)->delete(); + $this->error('模块安装失败'); + } + } + + /** + * 卸载模块 + * @param string $name 模块名 + * @param int $confirm 是否确认 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function uninstall($name = '', $confirm = 0) + { + if ($name == '') $this->error('模块不存在!'); + if ($name == 'admin') $this->error('禁止操作系统模块!'); + + // 模块配置信息 + $module_info = ModuleModel::getInfoFromFile($name); + if (empty($module_info)) { + $this->error('模块不存在!'); + } + + if ($confirm == 0) { + $this->assign('name', $name); + $this->assign('page_title', '卸载模块:'. $name); + return $this->fetch(); + } + + // 执行卸载文件 + $uninstall_file = realpath(Env::get('app_path').$name.'/uninstall.php'); + if (file_exists($uninstall_file)) { + @include($uninstall_file); + } + + // 执行卸载模块sql文件 + $clear = $this->request->get('clear'); + if ($clear == 1) { + $sql_file = realpath(Env::get('app_path').$name.'/sql/uninstall.sql'); + if (file_exists($sql_file)) { + if (isset($module_info['database_prefix']) && !empty($module_info['database_prefix'])) { + $sql_statement = Sql::getSqlFromFile($sql_file, false, [$module_info['database_prefix'] => config('database.prefix')]); + } else { + $sql_statement = Sql::getSqlFromFile($sql_file); + } + + if (!empty($sql_statement)) { + foreach ($sql_statement as $sql) { + try{ + Db::execute($sql); + }catch(\Exception $e){ + $this->error('卸载失败,请检查uninstall.sql的语句是否正确'); + } + } + } + } + } + + // 删除菜单 + if (false === MenuModel::where('module', $name)->delete()) { + $this->error('菜单删除失败,请重新卸载'); + } + + // 删除授权信息 + if (false === Db::name('admin_access')->where('module', $name)->delete()) { + $this->error('删除授权信息失败,请重新卸载'); + } + + // 删除行为规则 + if (false === Db::name('admin_action')->where('module', $name)->delete()) { + $this->error('删除行为信息失败,请重新卸载'); + } + + // 删除模块信息 + if (ModuleModel::where('name', $name)->delete()) { + // 复制静态资源目录 + File::copy_dir(Env::get('root_path'). 'public/static/'. $name, Env::get('app_path').$name.'/public/static/'. $name); + // 删除静态资源目录 + File::del_dir(Env::get('root_path'). 'public/static/'. $name); + cache('modules', null); + cache('module_all', null); + // 记录行为 + action_log('module_uninstall', 'admin_module', 0, UID, $module_info['title']); + $this->success('模块卸载成功', 'index'); + } else { + $this->error('模块卸载失败'); + } + } + + /** + * 更新模块配置 + * @param string $name 模块名 + * @author 蔡伟明 <314013107@qq.com> + */ + public function update($name = '') + { + $name == '' && $this->error('缺少模块名!'); + + $Module = ModuleModel::get(['name' => $name]); + !$Module && $this->error('模块不存在,或未安装'); + + // 模块配置信息 + $module_info = ModuleModel::getInfoFromFile($name); + unset($module_info['name']); + + // 检查是否有模块设置信息 + if (isset($module_info['config']) && !empty($module_info['config'])) { + $module_info['config'] = json_encode(parse_config($module_info['config'])); + } else { + $module_info['config'] = ''; + } + + // 检查是否有模块授权配置 + if (isset($module_info['access']) && !empty($module_info['access'])) { + $module_info['access'] = json_encode($module_info['access']); + } else { + $module_info['access'] = ''; + } + + // 更新模块信息 + if (false !== $Module->save($module_info)) { + $this->success('模块配置更新成功'); + } else { + $this->error('模块配置更新失败,请重试'); + } + } + + /** + * 导出模块 + * @param string $name 模块名 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function export($name = '') + { + if ($name == '') $this->error('缺少模块名'); + + $export_data = $this->request->get('export_data', ''); + if ($export_data == '') { + $this->assign('page_title', '导出模块:'. $name); + return $this->fetch(); + } + + // 模块信息 + $module = ModuleModel::where('name', $name)->find(); + if (!$module) { + $this->error('模块不存在'); + } + + // 模块导出目录 + $module_dir = Env::get('root_path'). 'export/module/'. $name; + + // 删除旧的导出数据 + if (is_dir($module_dir)) { + File::del_dir($module_dir); + } + + // 复制模块目录到导出目录 + File::copy_dir(Env::get('app_path'). $name, $module_dir); + // 复制静态资源目录 + File::copy_dir(Env::get('root_path'). 'public/static/'. $name, $module_dir.'/public/static/'. $name); + + // 模块本地配置信息 + $module_info = ModuleModel::getInfoFromFile($name); + + // 检查是否有模块设置信息 + if (isset($module_info['config'])) { + $db_config = ModuleModel::where('name', $name)->value('config'); + $db_config = json_decode($db_config, true); + // 获取最新的模块设置信息 + $module_info['config'] = set_config_value($module_info['config'], $db_config); + } + + // 检查是否有模块行为信息 + $action = Db::name('admin_action')->where('module', $name)->field('module,name,title,remark,rule,log,status')->select(); + if ($action) { + $module_info['action'] = $action; + } + + // 表前缀 + $module_info['database_prefix'] = config('database.prefix'); + + // 生成配置文件 + if (false === $this->buildInfoFile($module_info, $name)) { + $this->error('模块配置文件创建失败,请重新导出'); + } + + // 获取模型菜单并导出 + $fields = 'id,pid,title,icon,url_type,url_value,url_target,online_hide,sort,status'; + $menus = MenuModel::getMenusByGroup($name, $fields); + if (false === $this->buildMenuFile($menus, $name)) { + $this->error('模型菜单文件创建失败,请重新导出'); + } + + // 导出数据库表 + if (isset($module_info['tables']) && !empty($module_info['tables'])) { + if (!is_dir($module_dir. '/sql')) { + mkdir($module_dir. '/sql', 644, true); + } + if (!Database::export($module_info['tables'], $module_dir. '/sql/install.sql', config('database.prefix'), $export_data)) { + $this->error('数据库文件创建失败,请重新导出'); + } + if (!Database::exportUninstall($module_info['tables'], $module_dir. '/sql/uninstall.sql', config('database.prefix'))) { + $this->error('数据库文件创建失败,请重新导出'); + } + } + + // 记录行为 + action_log('module_export', 'admin_module', 0, UID, $module_info['title']); + + // 打包下载 + $archive = new PHPZip; + return $archive->ZipAndDownload($module_dir, $name); + } + + /** + * 创建模块菜单文件 + * @param array $menus 菜单 + * @param string $name 模块名 + * @author 蔡伟明 <314013107@qq.com> + * @return int + */ + private function buildMenuFile($menus = [], $name = '') + { + $menus = Tree::toLayer($menus); + + // 美化数组格式 + $menus = var_export($menus, true); + $menus = preg_replace("/(\d+|'id'|'pid') =>(.*)/", '', $menus); + $menus = preg_replace("/'child' => (.*)(\r\n|\r|\n)\s*array/", "'child' => $1array", $menus); + $menus = str_replace(['array (', ')'], ['[', ']'], $menus); + $menus = preg_replace("/(\s*?\r?\n\s*?)+/", "\n", $menus); + + $content = << + * @return int + */ + private function buildInfoFile($info = [], $name = '') + { + // 美化数组格式 + $info = var_export($info, true); + $info = preg_replace("/'(.*)' => (.*)(\r\n|\r|\n)\s*array/", "'$1' => array", $info); + $info = preg_replace("/(\d+) => (\s*)(\r\n|\r|\n)\s*array/", "array", $info); + $info = preg_replace("/(\d+ => )/", "", $info); + $info = preg_replace("/array \((\r\n|\r|\n)\s*\)/", "[)", $info); + $info = preg_replace("/array \(/", "[", $info); + $info = preg_replace("/\)/", "]", $info); + + $content = << + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function setStatus($type = '', $record = []) + { + $ids = input('param.ids'); + empty($ids) && $this->error('缺少主键'); + + $module = ModuleModel::where('id', $ids)->find(); + $module['system_module'] == 1 && $this->error('禁止操作系统内置模块'); + + $status = $type == 'enable' ? 1 : 0; + + // 将模块对应的菜单禁用或启用 + $map = [ + 'pid' => 0, + 'module' => $module['name'] + ]; + MenuModel::where($map)->setField('status', $status); + + if (false !== ModuleModel::where('id', $ids)->setField('status', $status)) { + // 记录日志 + call_user_func_array('action_log', ['module_'.$type, 'admin_module', 0, UID, $module['title']]); + $this->success('操作成功'); + } else { + $this->error('操作失败'); + } + } + + /** + * 禁用模块 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function disable($record = []) + { + $this->setStatus('disable'); + } + + /** + * 启用模块 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function enable($record = []) + { + $this->setStatus('enable'); + } + + /** + * 添加模型菜单 + * @param array $menus 菜单 + * @param string $module 模型名称 + * @param int $pid 父级ID + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + private function addMenus($menus = [], $module = '', $pid = 0) + { + foreach ($menus as $menu) { + $data = [ + 'pid' => $pid, + 'module' => $module, + 'title' => $menu['title'], + 'icon' => isset($menu['icon']) ? $menu['icon'] : 'fa fa-fw fa-puzzle-piece', + 'url_type' => isset($menu['url_type']) ? $menu['url_type'] : 'module_admin', + 'url_value' => isset($menu['url_value']) ? $menu['url_value'] : '', + 'url_target' => isset($menu['url_target']) ? $menu['url_target'] : '_self', + 'online_hide' => isset($menu['online_hide']) ? $menu['online_hide'] : 0, + 'status' => isset($menu['status']) ? $menu['status'] : 1 + ]; + + $result = MenuModel::create($data); + if (!$result) return false; + + if (isset($menu['child'])) { + $this->addMenus($menu['child'], $module, $result['id']); + } + } + + return true; + } + + /** + * 检查依赖 + * @param string $type 类型:module/plugin + * @param array $data 检查数据 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + private function checkDependence($type = '', $data = []) + { + $need = []; + foreach ($data as $key => $value) { + if (!isset($value[3])) { + $value[3] = '='; + } + // 当前版本 + if ($type == 'module') { + $curr_version = ModuleModel::where('identifier', $value[1])->value('version'); + } else { + $curr_version = PluginModel::where('identifier', $value[1])->value('version'); + } + + // 比对版本 + $result = version_compare($curr_version, $value[2], $value[3]); + $need[$key] = [ + $type => $value[0], + 'identifier' => $value[1], + 'version' => $curr_version ? $curr_version : '未安装', + 'version_need' => $value[3].$value[2], + 'result' => $result ? '' : '' + ]; + } + + return $need; + } +} \ No newline at end of file diff --git a/application/admin/controller/Packet.php b/application/admin/controller/Packet.php new file mode 100644 index 0000000..1b02a05 --- /dev/null +++ b/application/admin/controller/Packet.php @@ -0,0 +1,154 @@ + + * @return mixed|string + * @throws \think\Exception + */ + public function index($group = 'local') + { + // 配置分组信息 + $list_group = ['local' => '本地数据包']; + $tab_list = []; + foreach ($list_group as $key => $value) { + $tab_list[$key]['title'] = $value; + $tab_list[$key]['url'] = url('index', ['group' => $key]); + } + + $PacketModel = new PacketModel; + $data_list = $PacketModel->getAll(); + foreach ($data_list as &$value) { + if (isset($value['author_url']) && !empty($value['author_url'])) { + $value['author'] = ''. $value['author'] .''; + } + } + + if ($data_list === false) { + $this->error($PacketModel->getError()); + } + + // 自定义按钮 + $btn_install = [ + 'title' => '安装', + 'icon' => 'fa fa-fw fa-sign-in', + 'class' => 'btn btn-xs btn-default ajax-get confirm', + 'href' => url('install', ['name' => '__id__']) + ]; + $btn_uninstall = [ + 'title' => '卸载', + 'icon' => 'fa fa-fw fa-sign-out', + 'class' => 'btn btn-xs btn-default ajax-get confirm', + 'href' => url('uninstall', ['name' => '__id__']) + ]; + $btn_install_all = [ + 'title' => '安装', + 'icon' => 'fa fa-fw fa-sign-in', + 'class' => 'btn btn-primary ajax-post confirm', + 'href' => url('install') + ]; + $btn_uninstall_all = [ + 'title' => '卸载', + 'icon' => 'fa fa-fw fa-sign-out', + 'class' => 'btn btn-danger ajax-post confirm', + 'href' => url('uninstall') + ]; + + switch ($group) { + case 'local': + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('数据包管理') // 设置页面标题 + ->setPrimaryKey('name') + ->setTabNav($tab_list, $group) // 设置tab分页 + ->addColumns([ // 批量添加数据列 + ['name', '名称'], + ['title', '标题'], + ['author', '作者'], + ['version', '版本号'], + ['status', '是否安装', 'yesno'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('custom', $btn_install_all) + ->addTopButton('custom', $btn_uninstall_all) + ->addRightButton('custom', $btn_install) // 添加右侧按钮 + ->addRightButton('custom', $btn_uninstall) // 添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + break; + case 'online': + return '

    正在制作中...

    '; + break; + } + } + + /** + * 安装 + * @param string $name 数据包名 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function install($name = '') + { + $names = $name != '' ? (array)$name : $this->request->param('ids/a'); + + foreach ($names as $name) { + $result = PacketModel::install($name); + if ($result === true) { + if (!PacketModel::where('name', $name)->find()) { + $data = PacketModel::getInfoFromFile($name); + $data['status'] = 1; + $data['tables'] = json_encode($data['tables']); + PacketModel::create($data); + } + } else { + $this->error('安装失败:'. $result); + } + } + // 记录行为 + $packet_titles = PacketModel::where('name', 'in', $names)->column('title'); + action_log('packet_install', 'admin_packet', 0, UID, implode('、', $packet_titles)); + $this->success('安装成功'); + } + + /** + * 卸载 + * @param string $name 数据包名 + * @author 蔡伟明 <314013107@qq.com> + */ + public function uninstall($name = '') + { + $names = $name != '' ? (array)$name : $this->request->param('ids/a'); + + // 记录行为 + $packet_titles = PacketModel::where('name', 'in', $names)->column('title'); + action_log('packet_uninstall', 'admin_packet', 0, UID, implode('、', $packet_titles)); + + foreach ($names as $name) { + PacketModel::uninstall($name); + } + + $this->success('卸载成功'); + } +} \ No newline at end of file diff --git a/application/admin/controller/Plugin.php b/application/admin/controller/Plugin.php new file mode 100644 index 0000000..c430694 --- /dev/null +++ b/application/admin/controller/Plugin.php @@ -0,0 +1,620 @@ + + * @return mixed + */ + public function index($group = 'local', $type = '') + { + // 配置分组信息 + $list_group = ['local' => '本地插件']; + $tab_list = []; + foreach ($list_group as $key => $value) { + $tab_list[$key]['title'] = $value; + $tab_list[$key]['url'] = url('index', ['group' => $key]); + } + + // 监听tab钩子 + Hook::listen('plugin_index_tab_list', $tab_list); + + switch ($group) { + case 'local': + // 查询条件 + $keyword = $this->request->get('keyword', ''); + + if (input('?param.status') && input('param.status') != '_all') { + $status = input('param.status'); + } else { + $status = ''; + } + + $PluginModel = new PluginModel; + $result = $PluginModel->getAll($keyword, $status); + + if ($result['plugins'] === false) { + $this->error($PluginModel->getError()); + } + + $type_show = Cache::get('plugin_type_show'); + $type_show = $type != '' ? $type : ($type_show == false ? 'block' : $type_show); + Cache::set('plugin_type_show', $type_show); + $type = $type_show == 'block' ? 'list' : 'block'; + + $this->assign('page_title', '插件管理'); + $this->assign('plugins', $result['plugins']); + $this->assign('total', $result['total']); + $this->assign('tab_nav', ['tab_list' => $tab_list, 'curr_tab' => $group]); + $this->assign('type', $type); + return $this->fetch(); + break; + case 'online': + break; + } + } + + /** + * 安装插件 + * @param string $name 插件标识 + * @author 蔡伟明 <314013107@qq.com> + */ + public function install($name = '') + { + // 设置最大执行时间和内存大小 + ini_set('max_execution_time', '0'); + ini_set('memory_limit', '1024M'); + + $plug_name = trim($name); + if ($plug_name == '') $this->error('插件不存在!'); + + $plugin_class = get_plugin_class($plug_name); + + if (!class_exists($plugin_class)) { + $this->error('插件不存在!'); + } + + // 实例化插件 + $plugin = new $plugin_class; + // 插件预安装 + if(!$plugin->install()) { + $this->error('插件预安装失败!原因:'. $plugin->getError()); + } + + // 添加钩子 + if (isset($plugin->hooks) && !empty($plugin->hooks)) { + if (!HookPluginModel::addHooks($plugin->hooks, $name)) { + $this->error('安装插件钩子时出现错误,请重新安装'); + } + cache('hook_plugins', null); + } + + // 执行安装插件sql文件 + $sql_file = realpath(config('plugin_path').$name.'/install.sql'); + if (file_exists($sql_file)) { + if (isset($plugin->database_prefix) && $plugin->database_prefix != '') { + $sql_statement = Sql::getSqlFromFile($sql_file, false, [$plugin->database_prefix => config('database.prefix')]); + } else { + $sql_statement = Sql::getSqlFromFile($sql_file); + } + + if (!empty($sql_statement)) { + foreach ($sql_statement as $value) { + Db::execute($value); + } + } + } + + // 插件配置信息 + $plugin_info = $plugin->info; + + // 验证插件信息 + $result = $this->validate($plugin_info, 'Plugin'); + // 验证失败 输出错误信息 + if(true !== $result) $this->error($result); + + // 并入插件配置值 + $plugin_info['config'] = $plugin->getConfigValue(); + + // 将插件信息写入数据库 + if (PluginModel::create($plugin_info)) { + cache('plugin_all', null); + $this->success('插件安装成功'); + } else { + $this->error('插件安装失败'); + } + } + + /** + * 卸载插件 + * @param string $name 插件标识 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function uninstall($name = '') + { + $plug_name = trim($name); + if ($plug_name == '') $this->error('插件不存在!'); + + $class = get_plugin_class($plug_name); + if (!class_exists($class)) { + $this->error('插件不存在!'); + } + + // 实例化插件 + $plugin = new $class; + // 插件预卸 + if(!$plugin->uninstall()) { + $this->error('插件预卸载失败!原因:'. $plugin->getError()); + } + + // 卸载插件自带钩子 + if (isset($plugin->hooks) && !empty($plugin->hooks)) { + if (false === HookPluginModel::deleteHooks($plug_name)) { + $this->error('卸载插件钩子时出现错误,请重新卸载'); + } + cache('hook_plugins', null); + } + + // 执行卸载插件sql文件 + $sql_file = realpath(config('plugin_path').$plug_name.'/uninstall.sql'); + if (file_exists($sql_file)) { + if (isset($plugin->database_prefix) && $plugin->database_prefix != '') { + $sql_statement = Sql::getSqlFromFile($sql_file, true, [$plugin->database_prefix => config('database.prefix')]); + } else { + $sql_statement = Sql::getSqlFromFile($sql_file, true); + } + + if (!empty($sql_statement)) { + Db::execute($sql_statement); + } + } + + // 删除插件信息 + if (PluginModel::where('name', $plug_name)->delete()) { + cache('plugin_all', null); + $this->success('插件卸载成功'); + } else { + $this->error('插件卸载失败'); + } + } + + /** + * 插件管理 + * @param string $name 插件名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function manage($name = '') + { + cookie('__forward__', $_SERVER['REQUEST_URI']); + + // 加载自定义后台页面 + if (plugin_action_exists($name, 'Admin', 'index')) { + return plugin_action($name, 'Admin', 'index'); + } + + // 加载系统的后台页面 + $class = get_plugin_class($name); + if (!class_exists($class)) { + $this->error($name.'插件不存在!'); + } + + // 实例化插件 + $plugin = new $class; + + // 获取后台字段信息,并分析 + if (isset($plugin->admin)) { + $admin = $this->parseAdmin($plugin->admin); + } else { + $admin = $this->parseAdmin(); + } + + if (!plugin_model_exists($name)) { + $this->error('插件: '.$name.' 缺少模型文件!'); + } + + // 获取插件模型实例 + $PluginModel = get_plugin_model($name); + $order = $this->getOrder(); + $map = $this->getMap(); + $data_list = $PluginModel->where($map)->order($order)->paginate(); + $page = $data_list->render(); + + // 使用ZBuilder快速创建数据表格 + $builder = ZBuilder::make('table') + ->setPageTitle($admin['title']) // 设置页面标题 + ->setPluginName($name) + ->setTableName($admin['table_name']) + ->setSearch($admin['search_field'], $admin['search_title']) // 设置搜索框 + ->addOrder($admin['order']) + ->addTopButton('back', [ + 'title' => '返回插件列表', + 'icon' => 'fa fa-reply', + 'href' => url('index') + ]) + ->addTopButtons($admin['top_buttons']) // 批量添加顶部按钮 + ->addRightButtons($admin['right_buttons']); // 批量添加右侧按钮 + + // 自定义顶部按钮 + if (!empty($admin['custom_top_buttons'])) { + foreach ($admin['custom_top_buttons'] as $custom) { + $builder->addTopButton('custom', $custom); + } + } + // 自定义右侧按钮 + if (!empty($admin['custom_right_buttons'])) { + foreach ($admin['custom_right_buttons'] as $custom) { + $builder->addRightButton('custom', $custom); + } + } + + // 表头筛选 + if (is_array($admin['filter'])) { + foreach ($admin['filter'] as $column => $params) { + $options = isset($params[0]) ? $params[0] : []; + $default = isset($params[1]) ? $params[1] : []; + $type = isset($params[2]) ? $params[2] : 'checkbox'; + $builder->addFilter($column, $options, $default, $type); + } + } else { + $builder->addFilter($admin['filter']); + } + + return $builder + ->addColumns($admin['columns']) // 批量添加数据列 + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } + + /** + * 插件新增方法 + * @param string $plugin_name 插件名称 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function add($plugin_name = '') + { + // 如果存在自定义的新增方法,则优先执行 + if (plugin_action_exists($plugin_name, 'Admin', 'add')) { + $params = $this->request->param(); + return plugin_action($plugin_name, 'Admin', 'add', $params); + } + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + // 执行插件的验证器(如果存在的话) + if (plugin_validate_exists($plugin_name)) { + $plugin_validate = get_plugin_validate($plugin_name); + if (!$plugin_validate->check($data)) { + // 验证失败 输出错误信息 + $this->error($plugin_validate->getError()); + } + } + + // 实例化模型并添加数据 + $PluginModel = get_plugin_model($plugin_name); + if ($PluginModel->data($data)->save()) { + $this->success('新增成功', cookie('__forward__')); + } else { + $this->error('新增失败'); + } + } + + // 获取插件模型 + $class = get_plugin_class($plugin_name); + if (!class_exists($class)) { + $this->error('插件不存在!'); + } + + // 实例化插件 + $plugin = new $class; + if (!isset($plugin->fields)) { + $this->error('插件新增、编辑字段不存在!'); + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('新增') + ->addFormItems($plugin->fields) + ->fetch(); + } + + /** + * 编辑插件方法 + * @param string $id 数据id + * @param string $plugin_name 插件名称 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function edit($id = '', $plugin_name = '') + { + // 如果存在自定义的编辑方法,则优先执行 + if (plugin_action_exists($plugin_name, 'Admin', 'edit')) { + $params = $this->request->param(); + return plugin_action($plugin_name, 'Admin', 'edit', $params); + } + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + // 执行插件的验证器(如果存在的话) + if (plugin_validate_exists($plugin_name)) { + $plugin_validate = get_plugin_validate($plugin_name); + if (!$plugin_validate->check($data)) { + // 验证失败 输出错误信息 + $this->error($plugin_validate->getError()); + } + } + + // 实例化模型并添加数据 + $PluginModel = get_plugin_model($plugin_name); + if (false !== $PluginModel->isUpdate(true)->save($data)) { + $this->success('编辑成功', cookie('__forward__')); + } else { + $this->error('编辑失败'); + } + } + + // 获取插件类名 + $class = get_plugin_class($plugin_name); + if (!class_exists($class)) { + $this->error('插件不存在!'); + } + + // 实例化插件 + $plugin = new $class; + if (!isset($plugin->fields)) { + $this->error('插件新增、编辑字段不存在!'); + } + + // 获取数据 + $PluginModel = get_plugin_model($plugin_name); + $info = $PluginModel->find($id); + if (!$info) { + $this->error('找不到数据!'); + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑') + ->addHidden('id') + ->addFormItems($plugin->fields) + ->setFormData($info) + ->fetch(); + } + + /** + * 插件参数设置 + * @param string $name 插件名称 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + * @throws \think\exception\PDOException + */ + public function config($name = '') + { + // 更新配置 + if ($this->request->isPost()) { + $data = $this->request->except(config('zbuilder.form_token_name'), 'post'); + $data = json_encode($data); + + if (false !== PluginModel::where('name', $name)->update(['config' => $data])) { + $this->success('更新成功', 'index'); + } else { + $this->error('更新失败'); + } + } + + $plugin_class = get_plugin_class($name); + // 实例化插件 + $plugin = new $plugin_class; + $trigger = isset($plugin->trigger) ? $plugin->trigger : []; + + // 插件配置值 + $info = PluginModel::where('name', $name)->field('id,name,config')->find(); + $db_config = json_decode($info['config'], true); + + // 插件配置项 + $config = include config('plugin_path'). $name. '/config.php'; + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('插件设置') + ->addFormItems($config) + ->setFormData($db_config) + ->setTrigger($trigger) + ->fetch(); + } + + /** + * 设置状态 + * @param string $type 状态类型:enable/disable + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function setStatus($type = '', $record = []) + { + $_t = input('param._t', ''); + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + empty($ids) && $this->error('缺少主键'); + + $status = $type == 'enable' ? 1 : 0; + + if ($_t != '') { + parent::setStatus($type, $record); + } else { + $plugins = PluginModel::where('id', 'in', $ids)->value('name'); + if ($plugins) { + HookPluginModel::$type($plugins); + } + + if (false !== PluginModel::where('id', 'in', $ids)->setField('status', $status)) { + $this->success('操作成功'); + } else { + $this->error('操作失败'); + } + } + } + + /** + * 禁用插件/禁用插件数据 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function disable($record = []) + { + $this->setStatus('disable'); + } + + /** + * 启用插件/启用插件数据 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function enable($record = []) + { + $this->setStatus('enable'); + } + + /** + * 删除插件数据 + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function delete($record = []) + { + $this->setStatus('delete'); + } + + /** + * 执行插件内部方法 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function execute() + { + $plugin = input('param._plugin'); + $controller = input('param._controller'); + $action = input('param._action'); + $params = $this->request->except(['_plugin', '_controller', '_action'], 'param'); + + if (empty($plugin) || empty($controller) || empty($action)) { + $this->error('没有指定插件名称、控制器名称或操作名称'); + } + + if (!plugin_action_exists($plugin, $controller, $action)) { + $this->error("找不到方法:{$plugin}/{$controller}/{$action}"); + } + return plugin_action($plugin, $controller, $action, $params); + } + + /** + * 分析后台字段信息 + * @param array $data 字段信息 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + private function parseAdmin($data = []) + { + $admin = [ + 'title' => '数据列表', + 'search_title' => '', + 'search_field' => [], + 'order' => '', + 'filter' => '', + 'table_name' => '', + 'columns' => [], + 'right_buttons' => [], + 'top_buttons' => [], + 'customs' => [], + ]; + + if (empty($data)) { + return $admin; + } + + // 处理工具栏按钮链接 + if (isset($data['top_buttons']) && !empty($data['top_buttons'])) { + $this->parseButton('top_buttons', $data); + } + + // 处理右侧按钮链接 + if (isset($data['right_buttons']) && !empty($data['right_buttons'])) { + $this->parseButton('right_buttons', $data); + } + + return array_merge($admin, $data); + } + + /** + * 解析按钮链接 + * @param string $button 按钮名称 + * @param array $data 字段信息 + * @author 蔡伟明 <314013107@qq.com> + */ + private function parseButton($button, &$data) + { + foreach ($data[$button] as $key => &$value) { + // 处理自定义按钮 + if ($key === 'customs') { + if (!empty($value)) { + foreach ($value as &$custom) { + if (isset($custom['href']['url']) && $custom['href']['url'] != '') { + $params = isset($custom['href']['params']) ? $custom['href']['params'] : []; + $custom['href'] = plugin_url($custom['href']['url'], $params); + $data['custom_'.$button][] = $custom; + } + } + } + unset($data[$button][$key]); + } + if (!is_numeric($key) && isset($value['href']['url']) && $value['href']['url'] != '') { + $value['href'] = plugin_url($value['href']['url']); + } + } + } +} diff --git a/application/admin/controller/System.php b/application/admin/controller/System.php new file mode 100644 index 0000000..34493dc --- /dev/null +++ b/application/admin/controller/System.php @@ -0,0 +1,186 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function index($group = 'base') + { + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + if (isset(config('config_group')[$group])) { + // 查询该分组下所有的配置项名和类型 + $items = ConfigModel::where('group', $group)->where('status', 1)->column('name,type'); + + foreach ($items as $name => $type) { + if (!isset($data[$name])) { + switch ($type) { + // 开关 + case 'switch': + $data[$name] = 0; + break; + case 'checkbox': + $data[$name] = ''; + break; + } + } else { + // 如果值是数组则转换成字符串,适用于复选框等类型 + if (is_array($data[$name])) { + $data[$name] = implode(',', $data[$name]); + } + switch ($type) { + // 开关 + case 'switch': + $data[$name] = 1; + break; + // 日期时间 + case 'date': + case 'time': + case 'datetime': + $data[$name] = strtotime($data[$name]); + break; + } + } + ConfigModel::where('name', $name)->update(['value' => $data[$name]]); + } + } else { + // 保存模块配置 + if (false === ModuleModel::where('name', $group)->update(['config' => json_encode($data)])) { + $this->error('更新失败'); + } + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('module_config_'.$group, $data); + } + } + cache('system_config', null); + // 记录行为 + action_log('system_config_update', 'admin_config', 0, UID, "分组($group)"); + $this->success('更新成功', url('index', ['group' => $group])); + } else { + // 配置分组信息 + $list_group = config('config_group'); + + // 读取模型配置 + $modules = ModuleModel::where('config', 'neq', '') + ->where('status', 1) + ->column('config,title,name', 'name'); + foreach ($modules as $name => $module) { + $list_group[$name] = $module['title']; + } + + $tab_list = []; + foreach ($list_group as $key => $value) { + $tab_list[$key]['title'] = $value; + $tab_list[$key]['url'] = url('index', ['group' => $key]); + } + + if (isset(config('config_group')[$group])) { + // 查询条件 + $map['group'] = $group; + $map['status'] = 1; + + // 数据列表 + $data_list = ConfigModel::where($map) + ->order('sort asc,id asc') + ->field('group', true) + ->select(); + $data_list = $data_list->toArray(); + + foreach ($data_list as &$value) { + // 解析options + if ($value['options'] != '') { + $value['options'] = parse_attr($value['options']); + } + // 默认模块列表 + if ($value['name'] == 'home_default_module') { + $value['options'] = array_merge(['index' => '默认'], ModuleModel::getModule()); + } + switch ($value['type']) { + // 日期时间 + case 'date': + $value['value'] = $value['value'] != '' ? date('Y-m-d', $value['value']) : ''; + break; + case 'time': + $value['value'] = $value['value'] != '' ? date('H:i:s', $value['value']) : ''; + break; + case 'datetime': + $value['value'] = $value['value'] != '' ? date('Y-m-d H:i:s', $value['value']) : ''; + break; + case 'linkages': + $value['token'] = $this->createLinkagesToken($value['table'], $value['option'], $value['key']); + break; + case 'colorpicker': + $value['mode'] = 'rgba'; + break; + } + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('系统设置') + ->setTabNav($tab_list, $group) + ->setFormItems($data_list) + ->fetch(); + } else { + // 模块配置 + $module_info = ModuleModel::getInfoFromFile($group); + $config = $module_info['config']; + $trigger = isset($module_info['trigger']) ? $module_info['trigger'] : []; + + // 数据库内的模块信息 + $db_config = ModuleModel::where('name', $group)->value('config'); + $db_config = json_decode($db_config, true); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('模块设置') + ->setTabNav($tab_list, $group) + ->addFormItems($config) + ->setFormdata($db_config) // 设置表格数据 + ->setTrigger($trigger) // 设置触发 + ->fetch(); + } + } + } + + /** + * 创建快速多级联动Token + * @param string $table 表名 + * @param string $option + * @param string $key + * @author 蔡伟明 <314013107@qq.com> + * @return bool|string + */ + private function createLinkagesToken($table = '', $option = '', $key = '') + { + $table_token = substr(sha1($table.'-'.$option.'-'.$key.'-'.session('user_auth.last_login_ip').'-'.UID.'-'.session('user_auth.last_login_time')), 0, 8); + session($table_token, ['table' => $table, 'option' => $option, 'key' => $key]); + return $table_token; + } +} \ No newline at end of file diff --git a/application/admin/model/Access.php b/application/admin/model/Access.php new file mode 100644 index 0000000..69b03dc --- /dev/null +++ b/application/admin/model/Access.php @@ -0,0 +1,83 @@ + + * @return array|bool + */ + public function getAuthNode($uid = 0, $group = '') + { + if ($uid == 0 || $group == '') { + $this->error = '缺少参数'; + return false; + } + + if (strpos($group, '.')) { + list($module, $group) = explode('.', $group); + } else { + $module = Request::module(); + } + + $map = [ + 'module' => $module, + 'group' => $group, + 'uid' => $uid + ]; + + return $this->where($map)->column('nid'); + } + + /** + * 检查用户的某个节点是否授权 + * @param int $uid 用户id + * @param string $group $group 权限分组,可以以点分开模型名称和分组名称,如user.group + * @param int $node 需要检查的节点id + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function checkAuthNode($uid = 0, $group = '', $node = 0) + { + if ($uid == 0 || $group == '' || $node == 0) { + $this->error = '缺少参数'; + return false; + } + + // 获取该用户的所有授权节点 + $nodes = $this->getAuthNode($uid, $group); + if (!$nodes) { + $this->error = '该用户没有授权任何节点'; + return false; + } + + $nodes = array_flip($nodes); + if (isset($nodes[$node])) { + return true; + } else { + $this->error = '未授权'; + return false; + } + } +} diff --git a/application/admin/model/Action.php b/application/admin/model/Action.php new file mode 100644 index 0000000..b4f2659 --- /dev/null +++ b/application/admin/model/Action.php @@ -0,0 +1,25 @@ + + * @return array|bool|mixed|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getFilePath($id = '', $type = 0) + { + if (is_array($id)) { + $data_list = $this->where('id', 'in', $id)->select(); + $paths = []; + foreach ($data_list as $key => $value) { + if ($value['driver'] == 'local') { + $paths[$value['id']] = ($type == 0 ? PUBLIC_PATH : '').$value['path']; + } else { + $paths[$value['id']] = $value['path']; + } + } + return $paths; + } else { + $data = $this->where('id', $id)->find(); + if ($data) { + if ($data['driver'] == 'local') { + return ($type == 0 ? PUBLIC_PATH : '').$data['path']; + } else { + return $data['path']; + } + } else { + return false; + } + } + } + + /** + * 根据图片id获取缩略图路径,如果缩略图不存在,则返回原图路径 + * @param string $id 图片id + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed|string|Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getThumbPath($id = '') + { + $result = $this->where('id', $id)->field('path,driver,thumb')->find(); + if ($result) { + if ($result['driver'] == 'local') { + return $result['thumb'] != '' ? PUBLIC_PATH.$result['thumb'] : PUBLIC_PATH.$result['path']; + } else { + return $result['thumb'] != '' ? $result['thumb'] : $result['path']; + } + } else { + return $result; + } + } + + /** + * 根据附件id获取名称 + * @param string $id 附件id + * @return string 名称 + */ + public function getFileName($id = '') + { + return $this->where('id', $id)->value('name'); + } +} diff --git a/application/admin/model/Config.php b/application/admin/model/Config.php new file mode 100644 index 0000000..75cc807 --- /dev/null +++ b/application/admin/model/Config.php @@ -0,0 +1,56 @@ + + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public static function addHooks($hooks = [], $plugin_name = '') + { + if (!empty($hooks) && is_array($hooks)) { + $data = []; + foreach ($hooks as $name => $description) { + if (is_numeric($name)) { + $name = $description; + $description = ''; + } + if (self::where('name', $name)->find()) { + continue; + } + $data[] = [ + 'name' => $name, + 'plugin' => $plugin_name, + 'description' => $description, + 'create_time' => request()->time(), + 'update_time' => request()->time(), + ]; + } + if (!empty($data) && false === self::insertAll($data)) { + return false; + } + } + return true; + } + + /** + * 删除钩子 + * @param string $plugin_name 钩子名称 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public static function deleteHooks($plugin_name = '') + { + if (!empty($plugin_name)) { + if (false === self::where('plugin', $plugin_name)->delete()) { + return false; + } + } + return true; + } +} diff --git a/application/admin/model/HookPlugin.php b/application/admin/model/HookPlugin.php new file mode 100644 index 0000000..e6acdc3 --- /dev/null +++ b/application/admin/model/HookPlugin.php @@ -0,0 +1,130 @@ + + * @return bool + */ + public static function enable($plugin = '') + { + return self::where('plugin', $plugin)->setField('status', 1); + } + + /** + * 禁用插件钩子 + * @param string $plugin 插件名称 + * @author 蔡伟明 <314013107@qq.com> + * @return int + */ + public static function disable($plugin = '') + { + return self::where('plugin', $plugin)->setField('status', 0); + } + + /** + * 添加钩子-插件对照 + * @param array $hooks 钩子 + * @param string $plugin_name 插件名称 + * @author 蔡伟明 <314013107@qq.com> + * @return bool|int|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public static function addHooks($hooks = [], $plugin_name = '') + { + if (!empty($hooks) && is_array($hooks)) { + // 添加钩子 + if (!HookModel::addHooks($hooks, $plugin_name)) { + return false; + } + + $data = []; + foreach ($hooks as $name => $description) { + if (is_numeric($name)) { + $name = $description; + } + $data[] = [ + 'hook' => $name, + 'plugin' => $plugin_name, + 'create_time' => request()->time(), + 'update_time' => request()->time(), + ]; + } + + return self::insertAll($data); + } + return false; + } + + /** + * 删除钩子 + * @param string $plugin_name 钩子名称 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public static function deleteHooks($plugin_name = '') + { + if (!empty($plugin_name)) { + // 删除钩子 + if (!HookModel::deleteHooks($plugin_name)) { + return false; + } + if (false === self::where('plugin', $plugin_name)->delete()) { + return false; + } + } + return true; + } + + /** + * 钩子插件排序 + * @param string $hook 钩子 + * @param string $plugins 插件名 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public static function sort($hook = '', $plugins = '') + { + if ($hook != '' && $plugins != '') { + $plugins = is_array($plugins) ? $plugins : explode(',', $plugins); + + foreach ($plugins as $key => $plugin) { + $map = [ + 'hook' => $hook, + 'plugin' => $plugin + ]; + self::where($map)->setField('sort', $key + 1); + } + } + + return true; + } +} diff --git a/application/admin/model/Icon.php b/application/admin/model/Icon.php new file mode 100644 index 0000000..ae0d104 --- /dev/null +++ b/application/admin/model/Icon.php @@ -0,0 +1,72 @@ + + * @return \think\model\relation\HasMany + */ + public function icons() + { + return $this->hasMany('IconList', 'icon_id')->field('title,class,code'); + } + + /** + * 获取图标css链接 + * @author 蔡伟明 <314013107@qq.com> + * @return array|string|\think\Collection + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public static function getUrls() + { + $list = self::where('status', 1)->select(); + $list = $list->toArray(); + if ($list) { + foreach ($list as $key => $item) { + $url = substr($item['url'], 0, 4) == 'http' ? $item['url'] : 'http:'.$item['url']; + // 检查图标url是否合法 + $result = check_icon_url($url); + if (true !== $result) { + unset($list[$key]); + continue; + } + + if (isset($item['icons'])) { + $html = '
      '; + foreach ($item['icons'] as $icon) { + $html .= '
    • '.$icon['code'].'
    • '; + } + $html .= '
    '; + } else { + $html = '

    暂无图标

    '; + } + $list[$key]['html'] = $html; + } + } + return array_values($list); + } +} diff --git a/application/admin/model/IconList.php b/application/admin/model/IconList.php new file mode 100644 index 0000000..298bd43 --- /dev/null +++ b/application/admin/model/IconList.php @@ -0,0 +1,24 @@ + + * @return \think\Paginator + * @throws \think\exception\DbException + */ + public static function getAll($map = [], $order = '') + { + $data_list = self::view('admin_log', true) + ->view('admin_action', 'title,module', 'admin_action.id=admin_log.action_id', 'left') + ->view('admin_user', 'username', 'admin_user.id=admin_log.user_id', 'left') + ->view('admin_module', ['title' => 'module_title'], 'admin_module.name=admin_action.module') + ->where($map) + ->order($order) + ->paginate(); + return $data_list; + } +} diff --git a/application/admin/model/Menu.php b/application/admin/model/Menu.php new file mode 100644 index 0000000..2ee1ac2 --- /dev/null +++ b/application/admin/model/Menu.php @@ -0,0 +1,322 @@ + + * @return bool + */ + public static function changeModule($id = 0, $module = '') + { + if ($id > 0) { + $ids = self::where('pid', $id)->column('id'); + if ($ids) { + foreach ($ids as $id) { + self::where('id', $id)->setField('module', $module); + self::changeModule($id, $module); + } + } + } + return true; + } + + /** + * 获取树形节点 + * @param int $id 需要隐藏的节点id + * @param string $default 默认第一个节点项,默认为“顶级节点”,如果为false则不显示,也可传入其他名称 + * @param string $module 模型名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public static function getMenuTree($id = 0, $default = '', $module = '') + { + $result[0] = '顶级节点'; + $where = [ + ['status', 'egt', 0] + ]; + if ($module != '') { + $where[] = ['module', '=', $module]; + } + + // 排除指定节点及其子节点 + if ($id !== 0) { + $hide_ids = array_merge([$id], self::getChildsId($id)); + $where[] = ['id', 'not in', $hide_ids]; + } + + // 获取节点 + $menus = Tree::toList(self::where($where)->order('pid,id')->column('id,pid,title')); + foreach ($menus as $menu) { + $result[$menu['id']] = $menu['title_display']; + } + + // 设置默认节点项标题 + if ($default != '') { + $result[0] = $default; + } + + // 隐藏默认节点项 + if ($default === false) { + unset($result[0]); + } + + return $result; + } + + /** + * 获取顶部节点 + * @param string $max 最多返回多少个 + * @param string $cache_tag 缓存标签 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function getTopMenu($max = '', $cache_tag = '') + { + $cache_tag .= '_role_'.session('user_auth.role'); + $menus = cache($cache_tag); + if (!$menus) { + // 非开发模式,只显示可以显示的菜单 + if (config('develop_mode') == 0) { + $map['online_hide'] = 0; + } + $map['status'] = 1; + $map['pid'] = 0; + $list_menu = self::where($map)->order('sort,id')->column('id,pid,module,title,url_value,url_type,url_target,icon,params'); + $i = 0; + $menus = []; + foreach ($list_menu as $key => &$menu) { + if ($max != '' && $i >= $max) { + break; + } + // 没有访问权限的节点不显示 + if (!RoleModel::checkAuth($menu['id'])) { + continue; + } + if ($menu['url_value'] != '' && ($menu['url_type'] == 'module_admin' || $menu['url_type'] == 'module_home')) { + $url = explode('/', $menu['url_value']); + $menu['controller'] = $url[1]; + $menu['action'] = $url[2]; + $menu['url_value'] = $menu['url_type'] == 'module_admin' ? admin_url($menu['url_value'], $menu['params']) : home_url($menu['url_value'], $menu['params']); + } + $menus[$key] = $menu; + $i++; + } + // 非开发模式,缓存菜单 + if (config('develop_mode') == 0) { + cache($cache_tag, $menus); + } + } + return $menus; + } + + /** + * 获取侧栏节点 + * @param string $id 模块id + * @param string $module 模块名 + * @param string $controller 控制器名 + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed + */ + public static function getSidebarMenu($id = '', $module = '', $controller = '') + { + $module = $module == '' ? request()->module() : $module; + $controller = $controller == '' ? request()->controller() : $controller; + $cache_tag = strtolower('_sidebar_menus_' . $module . '_' . $controller).'_role_'.session('user_auth.role'); + $menus = cache($cache_tag); + + if (!$menus) { + // 获取当前节点地址 + $location = self::getLocation($id); + // 当前顶级节点id + $top_id = $location[0]['id']; + // 获取顶级节点下的所有节点 + $map = [ + 'status' => 1 + ]; + // 非开发模式,只显示可以显示的菜单 + if (config('develop_mode') == 0) { + $map['online_hide'] = 0; + } + $menus = self::where($map)->order('sort,id')->column('id,pid,module,title,url_value,url_type,url_target,icon,params'); + + // 解析模块链接 + foreach ($menus as $key => &$menu) { + // 没有访问权限的节点不显示 + if (!RoleModel::checkAuth($menu['id'])) { + unset($menus[$key]); + continue; + } + if ($menu['url_value'] != '' && ($menu['url_type'] == 'module_admin' || $menu['url_type'] == 'module_home')) { + $menu['url_value'] = $menu['url_type'] == 'module_admin' ? admin_url($menu['url_value'], $menu['params']) : home_url($menu['url_value'], $menu['params']); + } + } + $menus = Tree::toLayer($menus, $top_id, 2); + + // 非开发模式,缓存菜单 + if (config('develop_mode') == 0) { + cache($cache_tag, $menus); + } + } + return $menus; + } + + /** + * 获取指定节点ID的位置 + * @param string $id 节点id,如果没有指定,则取当前节点id + * @param bool $del_last_url 是否删除最后一个节点的url地址 + * @param bool $check 检查节点是否存在,不存在则抛出错误 + * @author 蔡伟明 <314013107@qq.com> + * @return array + * @throws \think\Exception + */ + public static function getLocation($id = '', $del_last_url = false, $check = true) + { + $model = request()->module(); + $controller = request()->controller(); + $action = request()->action(); + + if ($id != '') { + $cache_name = 'location_menu_'.$id; + } else { + $cache_name = 'location_'.$model.'_'.$controller.'_'.$action; + } + + $location = cache($cache_name); + + if (!$location) { + $map = [ + ['pid', '<>', 0], + ['url_value', '=', strtolower($model.'/'.trim(preg_replace("/[A-Z]/", "_\\0", $controller), "_").'/'.$action)] + ]; + + // 当前操作对应的节点ID + $curr_id = $id == '' ? self::where($map)->value('id') : $id; + + // 获取节点ID是所有父级节点 + $location = Tree::getParents(self::column('id,pid,title,url_value,params'), $curr_id); + + if ($check && empty($location)) { + throw new Exception('获取不到当前节点地址,可能未添加节点', 9001); + } + + // 剔除最后一个节点url + if ($del_last_url) { + $location[count($location) - 1]['url_value'] = ''; + } + + // 非开发模式,缓存菜单 + if (config('develop_mode') == 0) { + cache($cache_name, $location); + } + } + + return $location; + } + + /** + * 根据分组获取节点 + * @param string $group 分组名称 + * @param bool|string $fields 要返回的字段 + * @param array $map 查找条件 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function getMenusByGroup($group = '', $fields = true, $map = []) + { + $map['module'] = $group; + return self::where($map)->order('sort,id')->column($fields, 'id'); + } + + /** + * 获取节点分组 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function getGroup() + { + $map['status'] = 1; + $map['pid'] = 0; + $menus = self::where($map)->order('id,sort')->column('module,title'); + return $menus; + } + + /** + * 获取所有子节点id + * @param int $pid 父级id + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function getChildsId($pid = 0) + { + $ids = self::where('pid', $pid)->column('id'); + foreach ($ids as $value) { + $ids = array_merge($ids, self::getChildsId($value)); + } + return $ids; + } + + /** + * 获取所有父节点id + * @param int $id 节点id + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function getParentsId($id = 0) + { + $pid = self::where('id', $id)->value('pid'); + $pids = []; + if ($pid != 0) { + $pids[] = $pid; + $pids = array_merge($pids, self::getParentsId($pid)); + } + return $pids; + } + + /** + * 根据节点id获取上下级的所有id + * @param int $id 节点id + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function getLinkIds($id = 0) + { + $childs = self::getChildsId($id); + $parents = self::getParentsId($id); + return array_merge((array)(int)$id, $childs, $parents); + } +} diff --git a/application/admin/model/Module.php b/application/admin/model/Module.php new file mode 100644 index 0000000..5590a33 --- /dev/null +++ b/application/admin/model/Module.php @@ -0,0 +1,343 @@ + + * @return mixed + */ + public static function getModule() + { + $modules = cache('modules'); + if (!$modules) { + $modules = self::where('status', '>=', 0)->order('id')->column('name,title'); + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('modules', $modules); + } + } + return $modules; + } + + /** + * 获取所有模块信息 + * @param string $keyword 查找关键词 + * @param string $status 查找状态 + * @author 蔡伟明 <314013107@qq.com> + * @return array|bool + */ + public function getAll($keyword = '', $status = '') + { + $result = cache('module_all'); + if (!$result) { + $dirs = array_map('basename', glob(Env::get('app_path').'*', GLOB_ONLYDIR)); + if ($dirs === false || !file_exists(Env::get('app_path'))) { + $this->error = '模块目录不可读或者不存在'; + return false; + } + + // 不读取模块信息的目录 + $except_module = config('system.except_module'); + // 正常模块(包括已安装和未安装) + $dirs = array_diff($dirs, $except_module); + + // 读取数据库模块表 + $modules = $this->order('sort asc,id desc')->column(true, 'name'); + + // 读取未安装的模块 + foreach ($dirs as $module) { + if (!isset($modules[$module])) { + // 获取模块信息 + $info = self::getInfoFromFile($module); + + $modules[$module]['name'] = $module; + + // 模块模块信息缺失 + if (empty($info)) { + $modules[$module]['status'] = '-2'; + continue; + } + + // 模块模块信息不完整 + if (!$this->checkInfo($info)) { + $modules[$module]['status'] = '-3'; + continue; + } + + // 模块未安装 + $modules[$module] = $info; + $modules[$module]['status'] = '-1'; // 模块未安装 + } + } + + // 数量统计 + $total = [ + 'all' => count($modules), // 所有模块数量 + '-2' => 0, // 已损坏数量 + '-1' => 0, // 未安装数量 + '0' => 0, // 已禁用数量 + '1' => 0, // 已启用数量 + ]; + + // 过滤查询结果和统计数量 + foreach ($modules as $key => $value) { + // 统计数量 + if (in_array($value['status'], ['-2', '-3'])) { + // 已损坏数量 + $total['-2']++; + } else { + $total[(string)$value['status']]++; + } + + // 过滤查询 + if ($status != '') { + if ($status == '-2') { + // 过滤掉非已损坏的模块 + if (!in_array($value['status'], ['-2', '-3'])) { + unset($modules[$key]); + continue; + } + } else if ($value['status'] != $status) { + unset($modules[$key]); + continue; + } + } + if ($keyword != '') { + if (stristr($value['name'], $keyword) === false && (!isset($value['title']) || stristr($value['title'], $keyword) === false) && (!isset($value['author']) || stristr($value['author'], $keyword) === false)) { + unset($modules[$key]); + continue; + } + } + } + + // 处理状态及模块按钮 + foreach ($modules as &$module) { + // 系统核心模块 + if (isset($module['system_module']) && $module['system_module'] == '1') { + $module['actions'] = ''; + $module['status_class'] = 'text-success'; + $module['status_info'] = ' 已启用'; + $module['bg_color'] = 'success'; + continue; + } + + switch ($module['status']) { + case '-3': // 模块信息不完整 + $module['title'] = '模块信息不完整'; + $module['bg_color'] = 'danger'; + $module['status_class'] = 'text-danger'; + $module['status_info'] = ' 已损坏'; + $module['actions'] = ''; + break; + case '-2': // 模块信息缺失 + $module['title'] = '模块信息缺失'; + $module['bg_color'] = 'danger'; + $module['status_class'] = 'text-danger'; + $module['status_info'] = ' 已损坏'; + $module['actions'] = ''; + break; + case '-1': // 未安装 + $module['bg_color'] = 'info'; + $module['actions'] = '安装'; + $module['status_class'] = 'text-info'; + $module['status_info'] = ' 未安装'; + break; + case '0': // 禁用 + $module['bg_color'] = 'warning'; + $module['actions'] = '启用 '; + $module['actions'] .= '导出 '; + $module['actions'] .= '卸载 '; + $module['status_class'] = 'text-warning'; + $module['status_info'] = ' 已禁用'; + break; + case '1': // 启用 + $module['bg_color'] = 'success'; + $module['actions'] = '更新 '; + $module['actions'] .= '禁用 '; + $module['actions'] .= '导出 '; + $module['actions'] .= '卸载 '; + $module['status_class'] = 'text-success'; + $module['status_info'] = ' 已启用'; + break; + default: // 未知 + $module['title'] = '未知'; + break; + } + } + + $result = ['total' => $total, 'modules' => $modules]; + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('module_all', $result); + } + } + return $result; + } + + /** + * 从文件获取模块信息 + * @param string $name 模块名称 + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed + */ + public static function getInfoFromFile($name = '') + { + $info = []; + if ($name != '') { + // 从配置文件获取 + if (is_file(Env::get('app_path'). $name . '/info.php')) { + $info = include Env::get('app_path'). $name . '/info.php'; + } + } + return $info; + } + + /** + * 检查模块模块信息是否完整 + * @param string $info 模块模块信息 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + private function checkInfo($info = '') + { + $default_item = ['name','title','author','version']; + foreach ($default_item as $item) { + if (!isset($info[$item]) || $info[$item] == '') { + return false; + } + } + return true; + } + + /** + * 获取模型配置信息 + * @param string $name 模型名 + * @param string $item 指定返回的模块配置项 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public static function getConfig($name = '', $item = '') + { + $name = $name == '' ? request()->module() : $name; + + $config = cache('module_config_'.$name); + if (!$config) { + $config = self::where('name', $name)->value('config'); + if (!$config) { + return []; + } + + $config = json_decode($config, true); + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('module_config_'.$name, $config); + } + } + + if (!empty($item)) { + $items = explode(',', $item); + if (count($items) == 1) { + return isset($config[$item]) ? $config[$item] : ''; + } + + $result = []; + foreach ($items as $item) { + $result[$item] = isset($config[$item]) ? $config[$item] : ''; + } + return $result; + } + return $config; + } + + /** + * 获取模型配置信息 + * @param string $name 插件名.配置名 + * @param string $value 配置值 + * @author caiweiming <314013107@qq.com> + * @return bool + */ + public static function setConfig($name = '', $value = '') + { + $item = ''; + if (strpos($name, '.')) { + list($name, $item) = explode('.', $name); + } + + // 获取缓存 + $config = cache('module_config_'.$name); + + if (!$config) { + $config = self::where('name', $name)->value('config'); + if (!$config) { + return false; + } + + $config = json_decode($config, true); + } + + if ($item === '') { + // 批量更新 + if (!is_array($value) || empty($value)) { + // 值的格式错误,必须为数组 + return false; + } + + $config = array_merge($config, $value); + } else { + // 更新单个值 + $config[$item] = $value; + } + + if (false === self::where('name', $name)->setField('config', json_encode($config))) { + return false; + } + + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('module_config_'.$name, $config); + } + + return true; + } + + /** + * 从文件获取模块菜单 + * @param string $name 模块名称 + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed + */ + public static function getMenusFromFile($name = '') + { + $menus = []; + if ($name != '' && is_file(Env::get('app_path'). $name . '/menus.php')) { + // 从菜单文件获取 + $menus = include Env::get('app_path'). $name . '/menus.php'; + } + return $menus; + } +} \ No newline at end of file diff --git a/application/admin/model/Packet.php b/application/admin/model/Packet.php new file mode 100644 index 0000000..9af2f37 --- /dev/null +++ b/application/admin/model/Packet.php @@ -0,0 +1,125 @@ + + * @return array|bool + */ + public function getAll() + { + // 获取数据包目录下的所有插件目录 + $dirs = array_map('basename', glob(config('packet_path').'*', GLOB_ONLYDIR)); + if ($dirs === false || !file_exists(config('packet_path'))) { + $this->error = '插件目录不可读或者不存在'; + return false; + } + + // 读取数据库数据包表 + $packets = $this->column(true, 'name'); + + // 读取未安装的数据包 + foreach ($dirs as $packet) { + if (!isset($packets[$packet])) { + $info = $this->getInfoFromFile($packet); + $info['status'] = 0; + $packets[] = $info; + } + } + + return $packets; + } + + /** + * 从文件获取数据包信息 + * @param string $name 数据包名称 + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed + */ + public static function getInfoFromFile($name = '') + { + $info = []; + if ($name != '') { + // 从配置文件获取 + if (is_file(config('packet_path'). $name . '/info.php')) { + $info = include config('packet_path'). $name . '/info.php'; + } + } + return $info; + } + + /** + * 安装数据包 + * @param string $name 数据包名 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public static function install($name = '') + { + $info = self::getInfoFromFile($name); + + foreach ($info['tables'] as $table) { + $sql_file = realpath(config('packet_path').$name."/{$table}.sql"); + if (file_exists($sql_file)) { + if (isset($info['database_prefix']) && $info['database_prefix'] != '') { + $sql_statement = Sql::getSqlFromFile($sql_file, false, [$info['database_prefix'] => config('database.prefix')]); + } else { + $sql_statement = Sql::getSqlFromFile($sql_file); + } + + if (!empty($sql_statement)) { + foreach ($sql_statement as $value) { + Db::execute($value); + } + } + } else { + return "【{$table}.sql】文件不存在"; + } + } + + return true; + } + + /** + * 卸载数据包 + * @param string $name 数据包名 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public static function uninstall($name = '') + { + $info = self::getInfoFromFile($name); + foreach ($info['tables'] as $table) { + $sql = "DROP TABLE IF EXISTS `". config('database.prefix') ."{$table}`;"; + Db::execute($sql); + self::where('name', $name)->delete(); + } + return true; + } +} diff --git a/application/admin/model/Plugin.php b/application/admin/model/Plugin.php new file mode 100644 index 0000000..51aa80c --- /dev/null +++ b/application/admin/model/Plugin.php @@ -0,0 +1,307 @@ + + * @return array|bool + */ + public function getAll($keyword = '', $status = '') + { + $result = cache('plugin_all'); + if (!$result) { + // 获取插件目录下的所有插件目录 + $dirs = array_map('basename', glob(config('plugin_path').'*', GLOB_ONLYDIR)); + if ($dirs === false || !file_exists(config('plugin_path'))) { + $this->error = '插件目录不可读或者不存在'; + return false; + } + + // 读取数据库插件表 + $plugins = $this->order('sort asc,id desc')->column(true, 'name'); + + // 读取未安装的插件 + foreach ($dirs as $plugin) { + if (!isset($plugins[$plugin])) { + $plugins[$plugin]['name'] = $plugin; + + // 获取插件类名 + $class = get_plugin_class($plugin); + + // 插件类不存在则跳过实例化 + if (!class_exists($class)) { + // 插件的入口文件不存在! + $plugins[$plugin]['status'] = '-2'; + continue; + } + + // 实例化插件 + $obj = new $class; + + // 插件插件信息缺失 + if (!isset($obj->info) || empty($obj->info)) { + // 插件信息缺失! + $plugins[$plugin]['status'] = '-3'; + continue; + } + + // 插件插件信息不完整 + if (!$this->checkInfo($obj->info)) { + $plugins[$plugin]['status'] = '-4'; + continue; + } + + // 插件未安装 + $plugins[$plugin] = $obj->info; + $plugins[$plugin]['status'] = '-1'; + + } + } + + // 数量统计 + $total = [ + 'all' => count($plugins), // 所有插件数量 + '-2' => 0, // 错误插件数量 + '-1' => 0, // 未安装数量 + '0' => 0, // 未启用数量 + '1' => 0, // 已启用数量 + ]; + + // 过滤查询结果和统计数量 + foreach ($plugins as $key => $value) { + // 统计数量 + if (in_array($value['status'], ['-2', '-3', '-4'])) { + // 已损坏数量 + $total['-2']++; + } else { + $total[(string)$value['status']]++; + } + + // 过滤查询 + if ($status != '') { + if ($status == '-2') { + // 过滤掉非已损坏的插件 + if (!in_array($value['status'], ['-2', '-3', '-4'])) { + unset($plugins[$key]); + continue; + } + } else if ($value['status'] != $status) { + unset($plugins[$key]); + continue; + } + } + if ($keyword != '') { + if (stristr($value['name'], $keyword) === false && (!isset($value['title']) || stristr($value['title'], $keyword) === false) && (!isset($value['author']) || stristr($value['author'], $keyword) === false)) { + unset($plugins[$key]); + continue; + } + } + } + + // 处理状态及插件按钮 + foreach ($plugins as &$plugin) { + switch ($plugin['status']) { + case '-4': // 插件信息不完整 + $plugin['title'] = '插件信息不完整'; + $plugin['bg_color'] = 'danger'; + $plugin['status_class'] = 'text-danger'; + $plugin['status_info'] = ' 已损坏'; + $plugin['actions'] = ''; + break; + case '-3': // 插件信息缺失 + $plugin['title'] = '插件信息缺失'; + $plugin['bg_color'] = 'danger'; + $plugin['status_class'] = 'text-danger'; + $plugin['status_info'] = ' 已损坏'; + $plugin['actions'] = ''; + break; + case '-2': // 入口文件不存在 + $plugin['title'] = '入口文件不存在'; + $plugin['bg_color'] = 'danger'; + $plugin['status_class'] = 'text-danger'; + $plugin['status_info'] = ' 已损坏'; + $plugin['actions'] = ''; + break; + case '-1': // 未安装 + $plugin['bg_color'] = 'info'; + $plugin['actions'] = '安装'; + $plugin['status_class'] = 'text-info'; + $plugin['status_info'] = ' 未安装'; + break; + case '0': // 禁用 + $plugin['bg_color'] = 'warning'; + $plugin['actions'] = '启用 '; + $plugin['actions'] .= '卸载 '; + if (isset($plugin['config']) && $plugin['config'] != '') { + $plugin['actions'] .= '设置 '; + } + if ($plugin['admin'] != '0') { + $plugin['actions'] .= '管理 '; + } + $plugin['status_class'] = 'text-warning'; + $plugin['status_info'] = ' 已禁用'; + break; + case '1': // 启用 + $plugin['bg_color'] = 'success'; + $plugin['actions'] = '禁用 '; + $plugin['actions'] .= '卸载 '; + if (isset($plugin['config']) && $plugin['config'] != '') { + $plugin['actions'] .= '设置 '; + } + if ($plugin['admin'] != '0') { + $plugin['actions'] .= '管理 '; + } + $plugin['status_class'] = 'text-success'; + $plugin['status_info'] = ' 已启用'; + break; + default: // 未知 + $plugin['title'] = '未知'; + break; + } + } + + $result = ['total' => $total, 'plugins' => $plugins]; + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('plugin_all', $result); + } + } + return $result; + } + + /** + * 检查插件插件信息是否完整 + * @param string $info 插件插件信息 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + private function checkInfo($info = '') + { + $default_item = ['name','title','author','version']; + foreach ($default_item as $item) { + if (!isset($info[$item]) || $info[$item] == '') { + return false; + } + } + return true; + } + + /** + * 获取插件配置 + * @param string $name 插件名称 + * @param string $item 指定返回的插件配置项 + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed + */ + public function getConfig($name = '', $item = '') + { + $config = cache('plugin_config_'.$name); + if (!$config) { + $config = $this->where('name', $name)->value('config'); + if (!$config) { + return []; + } + + $config = json_decode($config, true); + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('plugin_config_'.$name, $config); + } + } + + if (!empty($item)) { + $items = explode(',', $item); + if (count($items) == 1) { + return isset($config[$item]) ? $config[$item] : ''; + } + + $result = []; + foreach ($items as $item) { + $result[$item] = isset($config[$item]) ? $config[$item] : ''; + } + return $result; + } + return $config; + } + + /** + * 设置插件配置 + * @param string $name 插件名.配置名 + * @param string $value 配置值 + * @author caiweiming <314013107@qq.com> + * @return bool + */ + public function setConfig($name = '', $value = '') + { + $item = ''; + if (strpos($name, '.')) { + list($name, $item) = explode('.', $name); + } + + // 获取缓存 + $config = cache('plugin_config_'.$name); + + if (!$config) { + $config = $this->where('name', $name)->value('config'); + if (!$config) { + return false; + } + + $config = json_decode($config, true); + } + + if ($item === '') { + // 批量更新 + if (!is_array($value) || empty($value)) { + // 值的格式错误,必须为数组 + return false; + } + + $config = array_merge($config, $value); + } else { + // 更新单个值 + $config[$item] = $value; + } + + if (false === $this->where('name', $name)->setField('config', json_encode($config))) { + return false; + } + + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('plugin_config_'.$name, $config); + } + + return true; + } +} diff --git a/application/admin/validate/Action.php b/application/admin/validate/Action.php new file mode 100644 index 0000000..60bb7d0 --- /dev/null +++ b/application/admin/validate/Action.php @@ -0,0 +1,33 @@ + + */ +class Action extends Validate +{ + //定义验证规则 + protected $rule = [ + 'module|所属模块' => 'require', + 'name|行为标识' => 'require|regex:^[a-zA-Z]\w{0,39}$|unique:admin_action,name^module', + 'title|行为名称' => 'require|length:1,80', + 'remark|行为描述' => 'require|length:1,128' + ]; + + //定义验证提示 + protected $message = [ + 'name.regex' => '行为标识由字母和下划线组成', + ]; +} diff --git a/application/admin/validate/Config.php b/application/admin/validate/Config.php new file mode 100644 index 0000000..25efdbd --- /dev/null +++ b/application/admin/validate/Config.php @@ -0,0 +1,39 @@ + + */ +class Config extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'group|配置分组' => 'require', + 'type|配置类型' => 'require', + 'name|配置名称' => 'require|regex:^[a-zA-Z]\w{0,39}$|unique:admin_config', + 'title|配置标题' => 'require', + ]; + + // 定义验证提示 + protected $message = [ + 'name.regex' => '配置名称由字母和下划线组成', + ]; + + // 定义场景,供快捷编辑时验证 + protected $scene = [ + 'name' => ['name'], + 'title' => ['title'], + ]; +} diff --git a/application/admin/validate/Hook.php b/application/admin/validate/Hook.php new file mode 100644 index 0000000..6dbeff8 --- /dev/null +++ b/application/admin/validate/Hook.php @@ -0,0 +1,30 @@ + + */ +class Hook extends Validate +{ + //定义验证规则 + protected $rule = [ + 'name|钩子名称' => 'require|regex:^[a-zA-Z]\w{0,39}$|unique:admin_hook' + ]; + + //定义验证提示 + protected $message = [ + 'name.regex' => '钩子名称由字母和下划线组成', + ]; +} diff --git a/application/admin/validate/Menu.php b/application/admin/validate/Menu.php new file mode 100644 index 0000000..a8df520 --- /dev/null +++ b/application/admin/validate/Menu.php @@ -0,0 +1,33 @@ + + */ +class Menu extends Validate +{ + //定义验证规则 + protected $rule = [ + 'module|所属模块' => 'require', + 'pid|所属节点' => 'require', + 'title|节点标题' => 'require', + ]; + + //定义验证提示 + protected $message = [ + 'module.require' => '请选择所属模块', + 'pid.require' => '请选择所属节点', + ]; +} diff --git a/application/admin/validate/Plugin.php b/application/admin/validate/Plugin.php new file mode 100644 index 0000000..809f271 --- /dev/null +++ b/application/admin/validate/Plugin.php @@ -0,0 +1,26 @@ + + */ +class Plugin extends Validate +{ + //定义验证规则 + protected $rule = [ + 'name|插件名称' => 'require|unique:admin_plugin', + 'title|插件标题' => 'require', + ]; +} diff --git a/application/admin/view/dispatch_jump.tpl b/application/admin/view/dispatch_jump.tpl new file mode 100644 index 0000000..fda9353 --- /dev/null +++ b/application/admin/view/dispatch_jump.tpl @@ -0,0 +1,77 @@ + + + + + + + 跳转提示 | {:config('web_site_title')} - DolphinPHP + + + + + + + + + + + + + + + + + + +
    +
    +
    + +

    +

    页面自动 跳转 等待时间:

    +
    + 立即跳转 + + 返回首页 +
    + + +
    +
    +
    + + + +
    + 极简 · 极速 · 极致
    + +
    + + + + + \ No newline at end of file diff --git a/application/admin/view/ie/index.html b/application/admin/view/ie/index.html new file mode 100644 index 0000000..9b18f2d --- /dev/null +++ b/application/admin/view/ie/index.html @@ -0,0 +1,74 @@ + + + + + 页面提示 - {:config('web_site_title')} + + + + + +
    +
    +
    +
    +
    +
    +

    温馨提示:在IE浏览器环境下浏览本系统体验不够友好,建议您使用下方的浏览器访问本系统。

    +
    +
    +
    + +
    +
    如果你正在使用的是双核浏览器,比如QQ浏览器、搜狗浏览器、猎豹浏览器、世界之窗浏览器、傲游浏览器、360浏览器等,可以使用浏览器的极速模式来继续访问本系统。查看详情 +
    +
    +
      +
    • 方法一,点击浏览器顶部地址栏右侧的浏览器兼容模式图标,,切换到极速模式 +

      +
    • +
    • 方法二,在当前页面中,点击鼠标右键,选择“切换到极速模式” +

      +
    • +
    • 方法三,在浏览器菜单栏中选择工具选项,打开“兼容性视图设置”,把设置框底部的“在兼容性视图显示”三个勾选框去掉 +

    • +
    +
    +
    +
    +
    + + + \ No newline at end of file diff --git a/application/admin/view/index/index.html b/application/admin/view/index/index.html new file mode 100644 index 0000000..937c5c1 --- /dev/null +++ b/application/admin/view/index/index.html @@ -0,0 +1,31 @@ +{extend name="layout" /} + +{block name="page-header"}{/block} + +{block name="content"} + {notempty name="default_pass"} +
    + +

    安全提示

    +

    超级管理员默认密码未修改,建议马上 修改

    +
    + {/notempty} + {// 后台首页钩子} +
    + {:hook('admin_index')} +
    +{/block} + +{block name="script"} + +{/block} \ No newline at end of file diff --git a/application/admin/view/layout.html b/application/admin/view/layout.html new file mode 100644 index 0000000..ac55694 --- /dev/null +++ b/application/admin/view/layout.html @@ -0,0 +1,559 @@ + + + + + + + {block name="page-title"}{$page_title|default='后台'} | {:config('web_site_title')} - DolphinPHP{/block} + + + + + + + + + + + + + + + + + {notempty name="_css_files"} + {eq name="Think.config.minify_status" value="1"} + + {else/} + {volist name="_css_files" id="css"} + {:load_assets($css)} + {/volist} + {/eq} + {/notempty} + + {notempty name="extend_css_list"} + {volist name="extend_css_list" id="vo"} + {volist name="vo" id="v"} + + {/volist} + {/volist} + {/notempty} + + {notempty name="_icons"} + {volist name="_icons" id="icon"} + + {/volist} + {/notempty} + + {block name="plugins-css"}{/block} + + + {eq name="Think.config.minify_status" value="1"} + + {else/} + + + + + + + {/eq} + + + + {block name="style"}{/block} + {notempty name="_pop"} + + {/notempty} + + + + {:hook('page_plugin_css')} + + + + + + + + + + + + + + + +{eq name="Think.config.minify_status" value="1"} + +{else/} + + + + + + + + + + + + + + + + + + +{/eq} + + + +{notempty name="_js_files"} + {eq name="Think.config.minify_status" value="1"} + + {else/} + {volist name="_js_files" id="js"} + {:load_assets($js, 'js')} + {/volist} + {/eq} +{/notempty} + +{notempty name="extend_js_list"} + {volist name="extend_js_list" id="vo"} + {volist name="vo" id="v"} + + {/volist} + {/volist} +{/notempty} + + + + +{block name="script"}{/block} + + +{:hook('page_plugin_js')} + +{// 额外HTML代码 } +{$extra_html|raw|default=''} + + \ No newline at end of file diff --git a/application/admin/view/menu/index.html b/application/admin/view/menu/index.html new file mode 100644 index 0000000..14b59f5 --- /dev/null +++ b/application/admin/view/menu/index.html @@ -0,0 +1,203 @@ +{extend name="layout" /} + +{block name="plugins-css"} + +{/block} + +{block name="content"} +
    + +

    提示:按住表头可拖动节点,调整后点击【保存节点】。

    +
    + +
    +
    +
    + {notempty name="tab_nav"} + + {else/} +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +

    {$page_title|raw}

    +
    + {/notempty} +
    +
    + {notempty name="menus"} +
    +
    +
    +
    + 新增 + + + + + + + + +
    +
    +
    +
    + + + {/notempty} + + {notempty name="modules"} +
    + +
    +
    +
    + {volist name="modules" id="module"} +
    + + {$module.title} +
    + {/volist} +
    +
    +
    +
    + {/notempty} +
    +
    +
    +
    +
    + +{/block} + +{block name="script"} + + + +{/block} diff --git a/application/admin/view/module/export.html b/application/admin/view/module/export.html new file mode 100644 index 0000000..802b918 --- /dev/null +++ b/application/admin/view/module/export.html @@ -0,0 +1,63 @@ +{extend name="layout" /} + +{block name="content"} +
    +
    +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +

    {$page_title|default=""}

    +
    + +
    +
    +
    +
    + +
    +

    是否导出数据

    +
    + + +
    + 选择“否”,只导出数据表结构。选择“是”,导出该模块的所有数据 +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +{/block} diff --git a/application/admin/view/module/index.html b/application/admin/view/module/index.html new file mode 100644 index 0000000..2337cb9 --- /dev/null +++ b/application/admin/view/module/index.html @@ -0,0 +1,163 @@ +{extend name="layout" /} + +{block name="content"} +
    +
    +
    + {notempty name="tab_nav"} + + {else/} +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +

    {$page_title}

    +
    + {/notempty} +
    +
    + +
    + {empty name="modules"} +
    +
    + 暂无数据
    +
    +
    + {/empty} + {eq name="type" value="block"} + {volist name="modules" id="module"} +
    +
    +
    +
    {$module.version|default='无版本号'}
    +
    + +
    +

    {$module.title|default='无模块标题'}

    +
    + {$module.name|default=''} +
    +
    +
    +
    {$module.status_info|raw}
    + +
    +
    +

    {$module.description|raw|default='暂无简介'}

    +
    +
    +
    + {$module.actions|raw} +
    +
    +
    +
    + {/volist} + {else/} + {notempty name="modules"} +
    +
    +
    + + + + + + + + + + + + + {volist name="modules" id="module"} + + + + + + + + + {/volist} + +
    名称图标版本作者简介操作
    {$module.title|default='无模块标题'}{$module.version|default='无版本号'}{$module.author|default=''}{$module.description|raw|default='暂无简介'}{$module.actions|raw}
    +
    +
    +
    + {/notempty} + {/eq} +
    +
    +
    +
    + {// 分页 } + {notempty name="pages"} + {$pages} + {/notempty} + {notempty name="row_list"} +
    +
    + + + + / {$row_list->lastPage()} 页,共 {$row_list->total()} 条数据,每页显示数量 +
    +
    + {/notempty} +
    +
    +
    +
    +
    +
    +
    +
    +{/block} \ No newline at end of file diff --git a/application/admin/view/module/install.html b/application/admin/view/module/install.html new file mode 100644 index 0000000..df7c8ae --- /dev/null +++ b/application/admin/view/module/install.html @@ -0,0 +1,182 @@ +{extend name="layout" /} + +{block name="content"} +
    +
    +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +

    {$page_title|default=""}

    +
    + +
    +
    +
    +
    + + +
    +

    模块依赖检查

    +
    + {empty name="need_module"} +
    无需依赖其他模块
    + {else/} + + + + + + + + + + + + {volist name="need_module" id="vo"} + + + + + + + + {/volist} + +
    模块唯一标识当前版本所需版本检查结果
    {$vo.module}{$vo.identifier}{$vo.version}{$vo.version_need} + {$vo.result|raw} +
    + {/empty} +
    +
    +
    +

    插件依赖检查

    +
    + {empty name="need_plugin"} +
    无需依赖其他插件
    + {else/} + + + + + + + + + + + + {volist name="need_plugin" id="vo"} + + + + + + + + {/volist} + +
    插件唯一标识当前版本所需版本检查结果
    {$vo.plugin}{$vo.identifier}{$vo.version}{$vo.version_need} + {$vo.result|raw} +
    + {/empty} +
    +
    +
    +

    数据表检查

    +
    + {empty name="table_check"} +
    该模块不需要数据表
    + {else/} + + + + + + + + + {volist name="table_check" id="vo"} + + + + + {/volist} + +
    数据表检查结果
    {$vo.table} + {$vo.result|raw} +
    + {/empty} +
    +
    +
    +

    是否清除旧数据

    +
    + + +
    + 选择“是”,将删除数据库中已存在的相同数据表 +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +{/block} + +{block name="script"} + +{/block} diff --git a/application/admin/view/module/uninstall.html b/application/admin/view/module/uninstall.html new file mode 100644 index 0000000..161f03c --- /dev/null +++ b/application/admin/view/module/uninstall.html @@ -0,0 +1,64 @@ +{extend name="layout" /} + +{block name="content"} +
    +
    +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +

    {$page_title|default=""}

    +
    + +
    +
    +
    +
    + + +
    +

    是否清除数据

    +
    + + +
    + 选择“是”,将删除数据库中的数据表 +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +{/block} diff --git a/application/admin/view/plugin/index.html b/application/admin/view/plugin/index.html new file mode 100644 index 0000000..8f7a631 --- /dev/null +++ b/application/admin/view/plugin/index.html @@ -0,0 +1,164 @@ +{extend name="layout" /} + +{block name="content"} +
    +
    +
    + {notempty name="tab_nav"} + + {else/} +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +

    {$page_title}

    +
    + {/notempty} +
    +
    + +
    + {empty name="plugins"} +
    +
    + 暂无数据
    +
    +
    + {/empty} + {eq name="type" value="block"} + {volist name="plugins" id="plugin"} +
    +
    +
    +
    {$plugin.version|default='无版本号'}
    +
    + +
    +

    {$plugin.title|default='无插件标题'}

    +
    + {$plugin.name|default=''} +
    +
    +
    +
    {$plugin.status_info|raw}
    + +
    +
    +

    {$plugin.description|raw|default='暂无简介'}

    +
    +
    +
    + {$plugin.actions|raw} +
    +
    +
    +
    + {/volist} + {else/} + {notempty name="plugins"} +
    +
    +
    + + + + + + + + + + + + + {volist name="plugins" id="plugin"} + + + + + + + + + {/volist} + +
    名称图标版本作者简介操作
    {$plugin.title|default='无插件标题'}{$plugin.version|default='无版本号'}{$plugin.author|default=''}{$plugin.description|raw|default='暂无简介'}{$plugin.actions|raw}
    +
    +
    +
    + {/notempty} + {/eq} + +
    +
    +
    +
    + {// 分页 } + {notempty name="pages"} + {$pages} + {/notempty} + {notempty name="row_list"} +
    +
    + + + + / {$row_list->lastPage()} 页,共 {$row_list->total()} 条数据,每页显示数量 +
    +
    + {/notempty} +
    +
    +
    +
    +
    +
    +
    +
    +{/block} diff --git a/application/api/.DS_Store b/application/api/.DS_Store new file mode 100644 index 0000000..fb08b9a Binary files /dev/null and b/application/api/.DS_Store differ diff --git a/application/api/common.php b/application/api/common.php new file mode 100644 index 0000000..8d423db --- /dev/null +++ b/application/api/common.php @@ -0,0 +1,73 @@ +$data]; + // 发送头部信息 + foreach ($header as $name => $val) { + if (is_null($val)) { + header($name); + } else { + header($name . ':' . $val); + } + } + exit(json_encode($return,JSON_UNESCAPED_UNICODE)); +} + +function checknum($len = 6) +{ + $chars = array( + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9" + ); + $charsLen = count($chars) - 1; + shuffle($chars); // 将数组打乱 + $output = ""; + for ($i = 0; $i < $len; $i ++) { + $output .= $chars[mt_rand(0, $charsLen)]; + } + return $output; +} + +/** + * 根据身份证号码获取性别 + * + * @param string $string + * 身份证号码 + * @return int $sex 性别 1男 2女 0未知 + * + */ +function getsex($idcard) +{ + $sexint = (int) substr($idcard, 16, 1); + return $sexint % 2 === 0 ? 2 : 1; +} + +/** + * 根据身份证号码获取生日 + * + * @param string $string + * 身份证号码 + * @return string + */ +function getbirthday($idcard) +{ + $bir = substr($idcard, 6, 8); + $year = (int) substr($bir, 0, 4); + $month = (int) substr($bir, 4, 2); + $day = (int) substr($bir, 6, 2); + return $year . "-" . $month . "-" . $day; +} \ No newline at end of file diff --git a/application/api/controller/.DS_Store b/application/api/controller/.DS_Store new file mode 100644 index 0000000..58f6407 Binary files /dev/null and b/application/api/controller/.DS_Store differ diff --git a/application/api/controller/Api.php b/application/api/controller/Api.php new file mode 100644 index 0000000..70a02bd --- /dev/null +++ b/application/api/controller/Api.php @@ -0,0 +1,361 @@ +params = $this->request->param(); //传参 + $this->base_url = 'https://' . $_SERVER['HTTP_HOST']; //当前域名 + + //获取头部信息 + $header = $this->request->header(); + + //非登录模块操作需要验证用户信息 + if ( $this->request->controller() != 'Login' && $this->request->controller() != 'Uploadfile' && $this->request->controller() != 'Wxpaynotify') { + + if($this->request->action()!='index' && $this->request->action()!='groupdetail' && $this->request->action()!='messageboard'){ + + //根据token获取用户信息 + if (!isset($header['user-token']) || empty($header['user-token'])) { + apiReturn(500,'token信息不存在'); + } + + $user_token = $header['user-token']; + + $this->user_info = MemberModel::where('userToken',$user_token)->find(); + + if (!$this->user_info || empty($user_token)) { + apiReturn(500,'token参数错误或登录超时'); + } + + // 检查账号有效性 + if (!MemberModel::where(['id' => $this->user_info['id'], 'is_delete' => 0])->value('id')) { + apiReturn(500,'账号不存在或已被禁用'); + } + + //登录是否超时(一周登录时间) + $time_compa = abs(time() - $this->user_info['lastlogin_time']); + if ($time_compa > 604800) { + //清空token + MemberModel::update(['user_token' => ''], ['id' => $this->user_info['id']]); + apiReturn(500,'登录超时'); + } + + //登录用户id + $this->user_id = $this->user_info['id']; + + }else{ + + if (isset($header['user-token']) && !empty($header['user-token'])) { + $user_token = $header['user-token']; + + $this->user_info = MemberModel::where('userToken',$user_token)->find(); + + if (!$this->user_info || empty($user_token)) { + apiReturn(500,'token参数错误或登录超时'); + } + + // 检查账号有效性 + if (!MemberModel::where(['id' => $this->user_info['id'], 'is_delete' => 0])->value('id')) { + apiReturn(500,'账号不存在或已被禁用'); + } + + //登录是否超时(一周登录时间) + $time_compa = abs(time() - $this->user_info['lastlogin_time']); + if ($time_compa > 604800) { + //清空token + MemberModel::update(['user_token' => ''], ['id' => $this->user_info['id']]); + apiReturn(500,'登录超时'); + } + + //登录用户id + $this->user_id = $this->user_info['id']; + } + } + } + } + + /** + * 获取附件地址 + */ + public function getFileUrl($attactId = 0) + { + $url = $this->base_url . get_file_path($attactId); + return $url; + } + + /** + * 产生随机字符串 + * 产生一个指定长度的随机字符串,并返回给用户 + * @access public + * @param int $len 产生字符串的位数 + * @return string + */ + function genNumberString($len = 6) + { + $chars = array( + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" + ); + $charsLen = count($chars) - 1; + shuffle($chars); // 将数组打乱 + $output = ""; + for ($i = 0; $i < $len; $i++) { + $output .= $chars[mt_rand(0, $charsLen)]; + } + return $output; + } + + //非法字符判断 + public function illegalcharacters($str) + { + $strarr = array( + '"', + '<', + '>', + '<>', + '(', + ')', + '()', + ',', + ',', + 'script', + 'svg', + 'alert', + 'confirm', + 'prompt', + 'onload', + 'onmouseover', + 'onfocus', + 'onerror', + 'xss', + ); + if (in_array($str, $strarr)) { + return true; + } else { + return false; + } + } + + //计算两个时间之前存在几个小时 + public function hours_min($start_time, $end_time) + { + + $sec = strtotime($end_time) - strtotime($start_time); + $sec = round($sec / 60); + $min = str_pad($sec % 60, 2, 0, STR_PAD_LEFT); + $hours_min = floor($sec / 60); + $min != 0 && $hours_min .= ':' . $min; + + return $hours_min; + } + + public function encryptPhone($phone) { + $maskedPhone = substr_replace($phone, '****', 3, 4); + return $maskedPhone; + } + + //更改时间显示 + protected function format_date($time) + { + $t = time() - $time; + $f = array( + //'31536000'=>'年', + //'2592000'=>'个月', + //'604800'=>'星期', + '86400' => '天', + '3600' => '小时', + '60' => '分钟', + '1' => '秒' + ); + foreach ($f as $k => $v) { + if (0 != $c = floor($t / (int)$k)) { + return $c . $v; + } + } + } + + /** + * 生成用户token信息 + * @param int uid 用户id + * @param string secret 盐值信息 + */ + public function getToken($uid, $secret = 'bodybreakthrough'){ + //将签名密钥拼接到签名字符串最后面 + $str = $uid . $secret . time(); + //通过md5算法为签名字符串生成一个md5签名,该签名就是我们要追加的sign参数值 + return md5($str); + } + + public function get_access_token() + { + $appid = 'wxd5a3c4538c00a549'; + $secret = '6f3cd9ac49b8501f1db38e2f5f11bfd6'; + $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" . $appid . "&secret=" . $secret; + $result = file_get_contents($url); + + $data = json_decode($result, true); + + if ($data['access_token']) { + return $data['access_token']; + } else { + return $data['errmsg']; + } + } + + public function _requestPost($url, $data, $ssl = true) + { + //curl完成 + $curl = curl_init(); + //设置curl选项 + curl_setopt($curl, CURLOPT_URL, $url); //URL + $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0 FirePHP/0.7.4'; + curl_setopt($curl, CURLOPT_USERAGENT, $user_agent); //user_agent,请求代理信息 + curl_setopt($curl, CURLOPT_AUTOREFERER, true); //referer头,请求来源 + curl_setopt($curl, CURLOPT_TIMEOUT, 30); //设置超时时间 + //SSL相关 + if ($ssl) { + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); //禁用后cURL将终止从服务端进行验证 + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2); //检查服务器SSL证书中是否存在一个公用名(common name)。 + } + // 处理post相关选项 + curl_setopt($curl, CURLOPT_POST, true); // 是否为POST请求 + curl_setopt($curl, CURLOPT_POSTFIELDS, $data); // 处理请求数据 + // 处理响应结果 + curl_setopt($curl, CURLOPT_HEADER, false); //是否处理响应头 + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); //curl_exec()是否返回响应结果 + // 发出请求 + $response = curl_exec($curl); + if (false === $response) { + echo '
    ', curl_error($curl), '
    '; + return false; + } + curl_close($curl); + return $response; + } + + /** + * 判断微信图片 + */ + public function judge_wx_pic($id) + { + $headimg = AttachmentModel::where('id', $id)->value('path'); + if (!strstr($headimg, 'http')) { + $headimg = $this->getFileUrl($id); + } + return $headimg; + } + + /** + * 根据经纬度和半径计算出范围 + * @param string $lat 纬度 + * @param String $lng 经度 + * @param float $radius 半径(单位米) + * @return Array 范围数组 + */ + public function calcScope($lat, $lng, $radius) + { + $radius = $radius * 1000; + $degree = (24901 * 1609) / 360.0; + $dpmLat = 1 / $degree; + + $radiusLat = $dpmLat * $radius; + $minLat = $lat - $radiusLat; // 最小纬度 + $maxLat = $lat + $radiusLat; // 最大纬度 + + $mpdLng = $degree * cos($lat * (pi() / 180)); + $dpmLng = 1 / $mpdLng; + $radiusLng = $dpmLng * $radius; + $minLng = $lng - $radiusLng; // 最小经度 + $maxLng = $lng + $radiusLng; // 最大经度 + + /** 返回范围数组 */ + $scope = array( + 'minLat' => $minLat, + 'maxLat' => $maxLat, + 'minLng' => $minLng, + 'maxLng' => $maxLng + ); + return $scope; + } + + /** + * 计算两个经纬度之间的距离 + */ + function distance($lat1, $lon1, $lat2, $lon2) + { + $radius = 6378.137; + $rad = floatval(M_PI / 180.0); + + $lat1 = floatval($lat1) * $rad; + $lon1 = floatval($lon1) * $rad; + $lat2 = floatval($lat2) * $rad; + $lon2 = floatval($lon2) * $rad; + + $theta = $lon2 - $lon1; + + $dist = acos(sin($lat1) * sin($lat2) + cos($lat1) * cos($lat2) * cos($theta)); + + if ($dist < 0) { + $dist += M_PI; + } + return $dist = $dist * $radius; + } + /** + * + * @param $latitude 纬度 + * @param $longitude 经度 + * @param $raidus 半径范围(单位:米) + * @return multitype:number + */ + public function getAround($latitude,$longitude,$raidus){ + $PI = 3.14159265; + $degree = (24901*1609)/360.0; + $dpmLat = 1/$degree; + $radiusLat = $dpmLat*$raidus; + $minLat = $latitude - $radiusLat; + $maxLat = $latitude + $radiusLat; + $mpdLng = $degree*cos($latitude * ($PI/180)); + $dpmLng = 1 / $mpdLng; + $radiusLng = $dpmLng*$raidus; + $minLng = $longitude - $radiusLng; + $maxLng = $longitude + $radiusLng; + + return [ + 'minLat'=>$minLat, + 'maxLat'=>$maxLat, + 'minLng'=>$minLng, + 'maxLng'=>$maxLng + ]; + } + + //根据秒数转换时分秒 + function secondsToHMS($seconds) { + $hours = gmdate("H", $seconds); + $minutes = gmdate("i", $seconds); + $seconds = gmdate("s", $seconds); + + $hours = !empty(intval($hours))?$hours.'时':null; + $minutes = !empty(intval($minutes))?$minutes.'分':null; + $seconds = !empty(intval($seconds))?$seconds.'秒':null; + + return $hours.$minutes.$seconds; + } +} diff --git a/application/api/controller/Index.php b/application/api/controller/Index.php new file mode 100644 index 0000000..0ee8ade --- /dev/null +++ b/application/api/controller/Index.php @@ -0,0 +1,493 @@ + + */ + public function index(){ + //传参数据 + $data_list = $this->params; + + //首页广告图片 + $indexadvert = AdvertModel::where(['type'=>1])->field('id,linkurl,thumb')->select()->toArray(); + if(!empty($indexadvert)){ + foreach ($indexadvert as $k=>$v){ + $indexadvert[$k]['thumb'] = $this->getFileUrl($v['thumb']); + } + } + + //公告 + $indexnotice = MemberServiceDetailModel::field('id,title')->select()->toArray(); + + //城市数据 + $citylist = CitysModel::where('is_delete',0)->order('sort asc,create_time desc')->field('id,thumb,title')->select()->toArray(); + if(!empty($citylist)){ + foreach ($citylist as $k=>$v){ + $citylist[$k]['thumb'] = $this->getFileUrl($v['thumb']); + } + } + + //置顶搭子群数据 + $topgroups = MemberServiceModel::where(['is_index'=>1,'is_delete'=>0])->order('sort asc,create_time desc')->field('id,index_thumb,index_title')->select()->toArray(); + if(!empty($topgroups)){ + foreach ($topgroups as $k=>$v){ + $topgroups[$k]['index_thumb'] = $this->getFileUrl($v['index_thumb']); + } + } + + //页数 + $page = 1; + if(isset($data_list['page'])){ + $page = intval($data_list['page']); + } + $page = ($page-1); + $page=$page*20; + $limit=$page.',20'; + + //搭子群数据 + $where = []; + $where[] = ['is_delete','eq',0]; + $where[] = ['id','neq',12]; + if(isset($data_list['keyword']) && !empty($data_list['keyword'])){ + $keyword = trim($data_list['keyword']); + $where2 = []; + $where2[] = ['is_delete','eq',0]; + $where2[] = ['title','like',"%$keyword%"]; + $ids = CitysModel::where($where2)->column('id'); + $ids = implode(',',$ids); + $where[] = ['cityid','in',$ids]; + } + if(isset($data_list['cityid']) && !empty($data_list['cityid'])){ + $cityid = intval($data_list['cityid']); + $where[] = ['cityid','eq',$cityid]; + } + $groups = MemberServiceModel::where($where)->order('sort asc,create_time desc')->field('id,title,summary,thumb')->limit($limit)->select()->toArray(); + if(!empty($groups)){ + foreach ($groups as $k=>$v){ + $groups[$k]['thumb'] = $this->getFileUrl($v['thumb']); + } + } + + //浏览数 + $numberviews = MemberServiceModel::where('is_delete',0)->sum('browse'); + //发布 + $sendnum = MemberServiceModel::where('is_delete',0)->count(); + //用户数 + $usernum = MemberModel::where('is_delete',0)->count(); + + $indexfootadvert = AdvertModel::where(['type'=>4])->field('id,linkurl,thumb')->find(); + $indexfootadvert['thumb'] = $this->getFileUrl($indexfootadvert['thumb']); + + apiReturn('200', '查询成功',[ + 'indexadvert'=>$indexadvert, + 'indexnotice'=>$indexnotice, + 'citylist'=>$citylist, + 'topgroups'=>$topgroups, + 'groups'=>$groups, + 'numberviews'=>$numberviews,//浏览数 + 'sendnum'=>$sendnum,//发布数 + 'usernum'=>$usernum,//用户数 + 'indexfootadvert' => $indexfootadvert, + ]); + } + + public function getindexfootadinfo(){ + + $indexfootadvert = AdvertModel::where(['type'=>4])->find(); + + $img2arr = explode(',',$indexfootadvert['img2']); + $randomElement = $img2arr[array_rand($img2arr)]; + + $randomElement = $this->getFileUrl($randomElement); + $backgroundimage = $this->getFileUrl($indexfootadvert['img1']); + + apiReturn('200', '查询成功',[ + 'randomElement'=>$randomElement, + 'backgroundimage'=>$backgroundimage, + ]); + } + + /** + * 搭子群详情 + * @param $groupid 群id + * @author loomis<2477365162@qq.com> + */ + public function groupdetail(){ + //传参数据 + $data_list = $this->params; + + if(!isset($data_list['groupid']) || empty($data_list['groupid'])){ + apiReturn('500', '群id不能为空'); + } + + $groupid = intval($data_list['groupid']); + $info = MemberServiceModel::where(['id'=>$groupid,'is_delete'=>0])->find(); + if(empty($info)){ + apiReturn('500', '未查询到相关群信息'); + } + + $type=1; + $qrcodeimg = ''; + if(!empty(MemberServiceOrderModel::where(['service_id'=>$groupid,'uid'=>$this->user_id,'status'=>2])->count())){ + $type = 2; + $qrcodeimg = $info['qrcodeimg']; + } + + $strs = 'https://'.$_SERVER['HTTP_HOST'].'/uploads/'; + $message = str_replace("/uploads/", $strs, $info['message']); + + $info->browse = $info->browse+1; + $info->save(); + + apiReturn('200', '查询成功',[ + 'id'=>$groupid, + 'type'=>$type, + 'qrcodeimg'=>$this->getFileUrl($qrcodeimg), + 'title'=>$info['title'], + 'thumb'=>$this->getFileUrl($info['thumb']), + 'create_time'=>date('Y-m-d H:i:s',$info['create_time']), + 'summary'=>$info['summary'], + 'standard'=>$info['standard'], + 'message'=>$message, + ]); + } + + /** + * 购买搭子群二维码 + * @param $groupid 群id + * @author loomis<2477365162@qq.com> + */ + public function buygroup(){ + //传参数据 + $data_list = $this->params; + + if(!isset($data_list['groupid']) || empty($data_list['groupid'])){ + apiReturn('500', '群id不能为空'); + } + + $groupid = intval($data_list['groupid']); + $info = MemberServiceModel::where(['id'=>$groupid,'is_delete'=>0])->find(); + if(empty($info)){ + apiReturn('500', '未查询到相关群信息'); + } + + $orderinfo = MemberServiceOrderModel::where(['type'=>1,'uid'=>$this->user_id,'service_id'=>intval($data_list['groupid']),'status'=>2])->find(); + if(!empty($orderinfo)){ + return json(['code' => 500, 'msg' => '此群二维码用户已经购买,请勿重复购买']); + }else{ + //生成订单号 + $numRes = true; + do{ + $ordernum = 'O'.date('ymd').$this->user_id.rand(100,999); + $count = MemberServiceOrderModel::where(['ordernum'=>$ordernum])->count(); + if(empty($count)) { + $numRes=false; + } + }while($numRes); + + $totalprice = $info['price']; + + $res = MemberServiceOrderModel::create([ + 'type'=>1, + 'uid'=>$this->user_id, + 'ordernum'=>$ordernum, + 'service_id'=>intval($data_list['groupid']), + 'totalprice'=>$totalprice, + 'status'=>1, + 'create_time'=>time(), + 'timeout_time'=>time()+600, + 'service_title'=>$info['title'], + 'service_summary'=>$info['summary'], + 'service_price'=>$info['price'], + 'service_thumb'=>$info['qrcodeimg'], + ]); + } + + if($res){ + //微信小程序支付回调地址 + $notify_url = 'https://' . $_SERVER['HTTP_HOST'] . url('api/wxpaynotify/index'); + + $config = [ + // 必要配置 + 'app_id' => config('smallprogram_appid'), + 'mch_id' => config('wxpay_mchid'), + 'key' => config('wxpay_paykey'), // API v2 密钥 (注意: 是v2密钥 是v2密钥 是v2密钥) + + // 如需使用敏感接口(如退款、发送红包等)需要配置 API 证书路径(登录商户平台下载 API 证书) + 'cert_path' => get_file_path(config('wxpay_appclient_cert')), // XXX: 绝对路径!!!! + 'key_path' => get_file_path(config('wxpay_appclient_key')), // XXX: 绝对路径!!!! + 'notify_url' => $notify_url, // 你也可以在下单时单独设置来覆盖它 + ]; + + $total_fee = $totalprice * 100; + $app = Factory::payment($config); + $jssdk = $app->jssdk; + + if(empty($total_fee)){ + $total_fee = 1; + } + + //下单 + $result = $app->order->unify([ + 'body' => 'DAZIQUN', + 'out_trade_no' => $ordernum, + 'total_fee' => $total_fee, + 'spbill_create_ip' => '', // 可选,如不传该参数,SDK 将会自动获取相应 IP 地址 + 'notify_url' => $notify_url, // 支付结果通知网址,如果不设置则会使用配置里的默认地址 + 'trade_type' => 'JSAPI', // 请对应换成你的支付方式对应的值类型 + 'openid' => MemberModel::where(['id' => $this->user_id])->value('openid'), + ]); + + //组合参数 + $pay = $jssdk->bridgeConfig($result['prepay_id'], false); // 返回数组 + + return json(['code' => 200, 'msg' => '微信支付信息','result'=>$pay]); + }else{ + return json(['code' => 500, 'msg' => '服务器繁忙,请稍后再试']); + } + } + + /** + * 留言板 + * @param $int $type 1男生通道 2女生通道 + * @param int $page 页数(默认为1,每页显示20条数据) + * @author loomis<2477365162@qq.com> + */ + public function messageboard(){ + //传参数据 + $data_list = $this->params; + + //页数 + $page = 1; + if(isset($data_list['page'])){ + $page = intval($data_list['page']); + } + $page = ($page-1); + $page=$page*20; + $limit=$page.',20'; + + //留言板数据 + $where = []; + $where[] = ['status','eq',1]; + + $type=1; + if(isset($data_list['type']) && !empty($data_list['type'])){ + $type = intval($data_list['type']); + } + $where[] = ['type','eq',$type]; + $list = MemberMessagesModel::where($where)->order('istop desc,create_time desc')->field('id,uid,message,thumbs,create_time,wechat_number')->limit($limit)->select()->toArray(); + if(!empty($list)){ + foreach ($list as $k=>$v){ + $thumbs = explode(',',$v['thumbs']); + foreach ($thumbs as $key=>$val){ + $thumbs[$key] = $this->getFileUrl($val); + } + $list[$k]['thumbs'] = $thumbs; + + //用户信息 + $userinfo = MemberModel::where('id',$v['uid'])->find(); + $list[$k]['headimg'] = $this->getFileUrl($userinfo['headimg']); + $list[$k]['nickname'] = $userinfo['nickname']; + + $list[$k]['create_time'] = date('Y-m-d H:i:s',$v['create_time']); + + //用户是否购买此留言的微信号 + $isbuy = MemberBalanceLogsModel::where(['uid'=>$this->user_id,'orderid'=>$v['id'],'mark'=>'购买留言板留言信息微信号'])->count(); + + if($v['uid']==$this->user_id){ + $isbuy = 1; + } + + $list[$k]['isbuy'] = !empty($isbuy)?1:0; + $list[$k]['wechat_number'] = !empty($isbuy)?$v['wechat_number']:""; + } + } + + //广告图片 + $advertlist = AdvertModel::where(['type'=>2])->field('id,linkurl,thumb')->select()->toArray(); + if(!empty($advertlist)){ + foreach ($advertlist as $k=>$v){ + $advertlist[$k]['thumb'] = $this->getFileUrl($v['thumb']); + } + } + + apiReturn('200', '查询成功',[ + 'list'=>$list, + 'advert'=>$advertlist, + ]); + } + + /** + * 发布留言-留言板置顶积分 + * @author loomis<2477365162@qq.com> + */ + public function gettoppoints(){ + apiReturn('200', '查询成功',[ + 'levemeesga_points'=>config('levemeesga_points'), + 'levemessage_toppoints'=>config('levemessage_toppoints'), + 'levemessage_lookwechnum'=>config('levemessage_lookwechnum'), + ]); + } + + /** + * 发布留言 + * @param $int $type 1男生通道 2女生通道 + * @param string $message 留言信息 + * @param string $thumbs 图片(多个图片请使用“,”进行拼接) + * @param $int $istop 0默认 1置顶 + * @author loomis<2477365162@qq.com> + */ + public function sendmessage(){ + //传参数据 + $data_list = $this->params; + + if (!isset($data_list['type']) || empty($data_list['type'])) { + return json(['code' => 500, 'msg' => '请选择留言通道']); + } + if (!isset($data_list['message']) || empty($data_list['message'])) { + return json(['code' => 500, 'msg' => '留言信息不能为空']); + } + if (!isset($data_list['thumbs']) || empty($data_list['thumbs'])) { + return json(['code' => 500, 'msg' => '图片不能为空']); + } + if (!isset($data_list['wechat_number']) || empty($data_list['wechat_number'])) { + return json(['code' => 500, 'msg' => '微信号不能为空']); + } + + $message = trim($data_list['message']); + if($this->illegalcharacters($message)){ + apiReturn('500', '非法参数'); + } + + $istop = 0; + if (isset($data_list['istop']) && !empty($data_list['istop'])) { + $istop = intval($data_list['istop']); + } + + //用户积分是否足够 + $userinfo = MemberModel::where('id',$this->user_id)->find(); + + $sendpoints = intval(config('levemeesga_points')); + if($istop==1){ + $sendpoints = $sendpoints + intval(config('levemessage_toppoints')); + } + if($userinfo['balance']<$sendpoints){ + apiReturn('500', '发布失败,积分不足'); + } + + $res = MemberMessagesModel::create([ + 'uid'=>$this->user_id, + 'type'=>intval($data_list['type']), + 'message'=>trim($data_list['message']), + 'thumbs'=>trim($data_list['thumbs']), + 'wechat_number'=>trim($data_list['wechat_number']), + 'istop'=>$istop, + ]); + + if ($res) { + + //插入积分记录 + MemberBalanceLogsModel::addGetLog($this->user_id,2,$sendpoints,$res['id'],'发布留言'); + + //扣除积分 + $userinfo->balance = $userinfo->balance - $sendpoints; + $userinfo->save(); + + apiReturn('200', '留言信息提交成功,等待平台审核'); + } else { + apiReturn('500', '留言信息提交失败'); + } + } + + //用户购买留言板微信号 + public function buywechatnumber(){ + //传参数据 + $data_list = $this->params; + + if (!isset($data_list['id']) || empty($data_list['id'])) { + return json(['code' => 500, 'msg' => '留言id不能为空']); + } + + $id = intval($data_list['id']); + $info = MemberMessagesModel::where('id',$id)->find(); + if(empty($info)){ + apiReturn('500', '未查询到留言板信息'); + } + + //用户是否购买此留言的微信号 + $isbuy = MemberBalanceLogsModel::where(['uid'=>$this->user_id,'orderid'=>$id,'mark'=>'查看留言板微信号'])->count(); + if(!empty($isbuy)){ + apiReturn('500', '您已购买过此留言的微信号'); + } + + //扣除积分 + $userinfo = MemberModel::where('id',$this->user_id)->find(); + $levemessage_lookwechnum = intval(config('levemessage_lookwechnum')); + if($userinfo['balance']<$levemessage_lookwechnum){ + apiReturn('500', '购买失败,积分不足'); + } + + //插入积分记录 + MemberBalanceLogsModel::addGetLog($this->user_id,2,$levemessage_lookwechnum,$id,'购买留言板留言信息微信号'); + + //扣除积分 + $userinfo->balance = $userinfo->balance - $levemessage_lookwechnum; + $userinfo->save(); + + //反馈积分 + $fankuio_points_num = $levemessage_lookwechnum * (config('levemessage_lookwechnumfan')/100); + $fankuio_points_num = round($fankuio_points_num); + if($fankuio_points_num<=0){ + $fankuio_points_num = 1; + } + + $message_userinfo = MemberModel::where('id',$info['uid'])->find(); + $message_userinfo->balance = $message_userinfo->balance + $fankuio_points_num; + $message_userinfo->save(); + + //插入积分记录 + MemberBalanceLogsModel::addGetLog($info['uid'],1,$fankuio_points_num,$id,'留言板微信号反馈积分'); + + apiReturn('200', '购买成功'); + } + + //获取购买微信好需要消耗的积分数 + public function getbuywxnumjf(){ + apiReturn('200', '查询成功',['jfnum'=>config('levemessage_lookwechnum')]); + } +} diff --git a/application/api/controller/Login.php b/application/api/controller/Login.php new file mode 100644 index 0000000..818b36a --- /dev/null +++ b/application/api/controller/Login.php @@ -0,0 +1,122 @@ + config('smallprogram_appid'), + 'secret' => config('smallprogram_appsecret'), + ]; + $this->app = Factory::miniProgram($config); + } + + /** + * 登录凭证校验 + * @param string $js_code 登录时获取的 code + */ + public function loginCodeCheck() + { + //传参数据 + $data_list = $this->params; + + if (!isset($data_list['js_code']) || empty($data_list['js_code'])) { + apiReturn(500,'js_code不能为空'); + } + + $config = [ + 'app_id' => config('smallprogram_appid'), + 'secret' => config('smallprogram_appsecret'), + // 下面为可选项 + // 指定 API 调用返回结果的类型:array(default)/collection/object/raw/自定义类名 + 'response_type' => 'array', + + 'log' => [ + 'level' => 'debug', + 'file' => __DIR__ . '/wechat.log', + ], + ]; + $app = Factory::miniProgram($config); + $wx_user_info = $app->auth->session($data_list['js_code']); + + $member_info = MemberModel::where(['openid' => $wx_user_info['openid']])->find(); + + //新用户 + if (empty($member_info)) { + + //生成会员编号 + $numRes = true; + do{ + $finallymember = MemberModel::order('create_time desc')->find(); + if(!empty($finallymember)){ + $member_number = $finallymember['member_number']+1; + }else{ + $member_number = 100001; + } + $count = MemberModel::where(['member_number'=>$member_number])->count(); + if(empty($count)) { + $numRes=false; + } + }while($numRes); + + //添加数据并返回主键 + $res = MemberModel::create([ + 'member_number'=>$member_number, + 'openid' => $wx_user_info['openid'], + 'lastlogin_time' => time(), + 'lastlogin_ip' => get_client_ip(0), + 'login_num' => 1, + ]); + + //生成token + $user_token = $this->getToken($res['id']); + + $res2 = MemberModel::update(['userToken' => $user_token], ['id' => $res['id']]); + + if ($res && $res2) { + apiReturn(200,'绑定成功',['user_token' => $user_token]); + } else { + apiReturn(500,'绑定失败'); + } + } else { + if ($member_info['is_delete'] != 0) { + apiReturn(500,'此账号被禁用或删除'); + } + + //登录是否超时(一周登录时间) + $time_compa= abs(time() - $member_info['lastlogin_time']); + if ($time_compa > 604800) { + + //生成token + $user_token = $this->getToken($member_info['id']); + // 更新登录信息及token信息 + $member_info->userToken = $user_token; + $member_info->lastlogin_time = time(); + $member_info->lastlogin_ip = get_client_ip(1); + $member_info->login_num = $member_info->login_num + 1; + $member_info->save(); + }else{ + $user_token = $member_info['userToken']; + } + + apiReturn(200,'登录成功',['user_token' => $user_token]); + } + } +} diff --git a/application/api/controller/Member.php b/application/api/controller/Member.php new file mode 100644 index 0000000..f26cae1 --- /dev/null +++ b/application/api/controller/Member.php @@ -0,0 +1,1001 @@ + + */ + public function setUserInfo() + { + //传参数据 + $data_list = $this->params; + + $insert_data = []; + if(isset($data_list['headimg'])){ + $insert_data['headimg'] = intval($data_list['headimg']); + } + if(isset($data_list['nickname'])){ + $insert_data['nickname'] = trim($data_list['nickname']); + } + if(isset($data_list['realname'])){ + $insert_data['realname'] = trim($data_list['realname']); + } + if(isset($data_list['realphone'])){ + $insert_data['realphone'] = trim($data_list['realphone']); + } + if(isset($data_list['wxskqrcode'])){ + $insert_data['wxskqrcode'] = intval($data_list['wxskqrcode']); + } + + //被邀请注册 + if (isset($data_list['invitation_uid']) && !empty($data_list['invitation_uid'])) { + + if(intval($data_list['invitation_uid']) != $this->user_id){ + $insert_data['invitation_uid'] = intval($data_list['invitation_uid']); + //进行积分发送 + MemberModel::where('id',$insert_data['invitation_uid'])->setInc('balance',intval(config('login_integral'))); + //插入积分记录、 + MemberBalanceLogsModel::addGetLog($insert_data['invitation_uid'],1,intval(config('login_integral')),'','邀请用户注册'); + } + } + + $res = MemberModel::where(['id' => $this->user_id])->update($insert_data); + + if ($res) { + apiReturn('200', '用户信息更新成功'); + } else { + apiReturn('500', '用户信息更新失败'); + } + } + + /** + * 意见反馈 + * @param int $phone 联系电话 + * @param int $message 反馈内容 + * @author loomis<2477365162@qq.com> + */ + public function addfeedback(){ + //传参数据 + $data_list = $this->params; + + if (!isset($data_list['phone']) || empty($data_list['phone'])) { + return json(['code' => 500, 'msg' => '联系电话不能为空']); + } + if (!isset($data_list['message']) || empty($data_list['message'])) { + return json(['code' => 500, 'msg' => '反馈内容不能为空']); + } + $phone = trim($data_list['phone']); + $message = trim($data_list['message']); + if($this->illegalcharacters($phone) || $this->illegalcharacters($message)){ + apiReturn('500', '非法参数'); + } + + $data_list['uid'] = $this->user_id; + $res = MemberFeedbackModel::create($data_list); + if ($res) { + apiReturn('200', '意见反馈提交成功,等待平台查看联系'); + } else { + apiReturn('500', '意见反馈提交失败'); + } + } + + /** + * 我的 + * @author loomis<2477365162@qq.com> + */ + public function usercenter(){ + $userinfo = MemberModel::where('id',$this->user_id)->find(); + + //生成小程序码 + if(empty($userinfo['share_qrcode'])){ + $app = Factory::miniProgram([ + 'app_id' => config('smallprogram_appid'), + 'secret' => config('smallprogram_appsecret'), + ]); + + $response = $app->app_code->get('pages/index/index?invitation_uid='.$this->user_id, []); + + //二维码 + $filename=time().'_wx.jpg'; + $qr_path = $_SERVER['DOCUMENT_ROOT'] .'/uploads/qrcode'; + // 保存小程序码到文件 + if ($response instanceof \EasyWeChat\Kernel\Http\StreamResponse){ + $resfilename = $response->saveAs($qr_path,$filename); + } + + // 保存成功后 获取信息 + $data['uid'] = 0; + $data['path'] = trim('uploads/qrcode/'.$resfilename,'.'); + $data["md5"] = md5($data['path']); + $data["sha1"] = sha1($data['path']); + $data['name'] = $resfilename; + $data['ext'] = 'jpg'; + $data['create_time'] = time(); + $id = AttachmentModel::create($data); + + //更新小程序邀请二维码 + $userinfo->share_qrcode = $id['id']; + $userinfo->save(); + + $invitation_qrcode = $this->getFileUrl($id['id']); + }else{ + $invitation_qrcode = $this->getFileUrl($userinfo['share_qrcode']); + } + + //海报背景图 + $haibao_advert = AdvertModel::where(['type'=>3])->order('sort asc,create_time desc')->find(); + + apiReturn('200', '查询成功',[ + 'id'=>$userinfo['id'], + 'invitation_uid'=>$userinfo['invitation_uid'], + 'headimg'=>$this->getFileUrl($userinfo['headimg']), + 'headimg_id'=>$userinfo['headimg'], + 'nickname'=>$userinfo['nickname'], + 'balance'=>$userinfo['balance'], + 'poster_img'=>$this->getFileUrl($haibao_advert['thumb']), + 'invitation_qrcode'=>$invitation_qrcode, + 'realname'=>$userinfo['realname'], + 'realphone'=>$userinfo['realphone'], + 'wxskqrcode'=>$this->getFileUrl($userinfo['wxskqrcode']), + 'wxskqrcode_id'=>$userinfo['wxskqrcode'], + 'vip_title'=>$userinfo['vip_title'], + ]); + } + + + /** + * 我的积分 + * @param int $page 页数(默认为1,每页显示20条数据) + * @param int $type 1积分明细 2兑换明细 + * @author loomis<2477365162@qq.com> + */ + public function myintegral(){ + //传参数据 + $data_list = $this->params; + + if(!isset($data_list['type']) || empty($data_list['type'])){ + apiReturn('500', 'type不能为空'); + } + $type = intval($data_list['type']); + + //页数 + $page = 1; + if(isset($data_list['page'])){ + $page = intval($data_list['page']); + } + $page = ($page-1); + $page=$page*20; + $limit=$page.',20'; + + $resdata = []; + if($type==1){//积分明细 + $list = MemberBalanceLogsModel::where('uid',$this->user_id)->order('id desc')->limit($limit)->select()->toArray(); + if(!empty($list)){ + foreach ($list as $k=>$v){ + $resdata[$k] = [ + 'category'=>$v['category'], + 'value'=>$v['value'], + 'mark'=>$v['mark'], + 'create_time'=>date('Y-m-d H:i:s',$v['create_time']), + ]; + } + } + }else{//兑换明细 + $list = BalanceWithdrawModel::where('uid',$this->user_id)->order('id desc')->limit($limit)->select()->toArray(); + if(!empty($list)){ + foreach ($list as $k=>$v){ + $resdata[$k] = [ + 'value'=>$v['price'], + 'status'=>$v['status'], + 'create_time'=>date('Y-m-d H:i:s',$v['create_time']), + ]; + } + } + } + + $userinfo = MemberModel::where('id',$this->user_id)->find(); + + apiReturn('200', '查询成功',[ + 'integral'=>$userinfo['balance'], + 'withdraw_price'=>BalanceWithdrawModel::where(['uid'=>$this->user_id,'status'=>2])->sum('price'), + 'list'=>$list, + ]); + } + + //获取积分提现比例 + public function getproportion(){ + apiReturn('200', '查询成功',[ + 'proportion'=>config('integral_price_bili'), + 'minprice'=>config('integral_minprice') + ]); + } + + /** + * 积分提现 + * @param int $integral_num + * @author loomis<2477365162@qq.com> + */ + public function addwithdraw(){ + //传参数据 + $data_list = $this->params; + + if(!isset($data_list['withdrawid']) || empty($data_list['withdrawid'])){ + apiReturn('500', '提现id不能为空'); + } + $withdrawid = intval($data_list['withdrawid']); + $withdrawinfo = MemberWithdrawModel::where('id',$withdrawid)->find(); + if(empty($withdrawinfo)){ + apiReturn('500', '未查询到提现信息'); + } + + $userinfo = MemberModel::where('id',$this->user_id)->find(); + + if(empty($userinfo['wxskqrcode'])){ + apiReturn('500', '请先完善收款信息'); + } + + $integral_num = $withdrawinfo['points_num']; + if($userinfo['vip_id']!=0){ + $integral_num = $withdrawinfo['vip_points_num']; + } + + if($userinfo['balance']<$integral_num){ + apiReturn('500', '提现失败,积分不足'); + } + + $res = BalanceWithdrawModel::create([ + 'uid'=>$this->user_id, + 'price'=>$withdrawinfo['price'], + 'integral_num'=>$integral_num + ]); + + if($res){ + + //插入积分记录、 + MemberBalanceLogsModel::addGetLog($this->user_id,2,$integral_num,$res['id'],'积分提现'); + + //扣除积分 + $userinfo->balance = $userinfo->balance - $integral_num; + $userinfo->save(); + + apiReturn('200', '提现申请已提交,等待平台审核打款'); + }else{ + apiReturn('500', '服务器繁忙,请稍后再试'); + } + } + + /** + * 推广团队 + * @param int $page 页数(默认为1,每页显示20条数据) + * @param int $type 1一级 2二级 + * @author loomis<2477365162@qq.com> + */ + public function promotionteam(){ + //传参数据 + $data_list = $this->params; + + if(!isset($data_list['type']) || empty($data_list['type'])){ + apiReturn('500', 'type不能为空'); + } + $type = intval($data_list['type']); + + //页数 + $page = 1; + if(isset($data_list['page'])){ + $page = intval($data_list['page']); + } + $page = ($page-1); + $page=$page*20; + $limit=$page.',20'; + + if($type==1){//一级 + $list = MemberModel::where('invitation_uid',$this->user_id) + ->field('id,headimg,nickname') + ->order('id desc') + ->limit($limit) + ->select()->toArray(); + if(!empty($list)){ + foreach ($list as $k=>$v){ + $list[$k]['headimg'] = $this->getFileUrl($v['headimg']); + + //用户消费金额 + $list[$k]['useprice'] = MemberServiceOrderModel::where(['type'=>1,'uid'=>$v['id'],'status'=>2])->sum('totalprice'); + //用户给我产生的积分 + $list[$k]['pointsnum'] = MemberBalanceLogsModel::where(['uid'=>$this->user_id,'to_uid'=>$v['id']])->sum('value'); + } + } + }else{//二级 + $ids = MemberModel::where('invitation_uid',$this->user_id)->column('id'); + if(!empty($ids)){ + $ids = implode(',',$ids); + $where = []; + $where[] = ['invitation_uid','in',$ids]; + + $list = MemberModel::where($where) + ->field('id,headimg,nickname') + ->order('id desc') + ->limit($limit) + ->select()->toArray(); + if(!empty($list)){ + foreach ($list as $k=>$v){ + $list[$k]['headimg'] = $this->getFileUrl($v['headimg']); + //用户消费金额 + $list[$k]['useprice'] = MemberServiceOrderModel::where(['type'=>1,'uid'=>$v['id'],'status'=>2])->sum('totalprice'); + //用户给我产生的积分 + $list[$k]['pointsnum'] = MemberBalanceLogsModel::where(['uid'=>$this->user_id,'to_uid'=>$v['id']])->sum('value'); + } + } + }else{ + $list = []; + } + } + + apiReturn('200', '查询成功',$list); + } + + /** + * 资源订单 + * @param int $page 页数(默认为1,每页显示20条数据) + * @param int $status 1待支付 2已支付 + * @author loomis<2477365162@qq.com> + */ + public function serviceorder(){ + //传参数据 + $data_list = $this->params; + + $status = 1; + if(isset($data_list['status']) && !empty($data_list['status'])){ + $status = intval($data_list['status']); + } + + //页数 + $page = 1; + if(isset($data_list['page'])){ + $page = intval($data_list['page']); + } + $page = ($page-1); + $page=$page*20; + $limit=$page.',20'; + + $where = []; + $where[] = ['uid','eq',$this->user_id]; + $where[] = ['status','eq',$status]; + $list = MemberServiceOrderModel::where($where) + ->field('id,service_id,ordernum,service_title,service_summary,service_thumb,totalprice,create_time') + ->order('id desc') + ->limit($limit) + ->select()->toArray(); + if(!empty($list)){ + foreach ($list as $k=>$v){ + $list[$k]['service_thumb'] = $this->getFileUrl($v['service_thumb']); + $list[$k]['create_time'] = date('Y-m-d H:i:s',$v['create_time']); + $list[$k]['groupid'] = $v['service_id']; + unset($list[$k]['service_id']); + } + } + + apiReturn('200', '查询成功',$list); + } + + /** + * 我的扩列墙 + * @param $int $type 1男生通道 2女生通道 + * @param int $page 页数(默认为1,每页显示20条数据) + * @author loomis<2477365162@qq.com> + */ + public function messageboard(){ + //传参数据 + $data_list = $this->params; + + //页数 + $page = 1; + if(isset($data_list['page'])){ + $page = intval($data_list['page']); + } + $page = ($page-1); + $page=$page*20; + $limit=$page.',20'; + + //留言板数据 + $where = []; + $where[] = ['uid','eq',$this->user_id]; + $where[] = ['status','eq',1]; + + $type=1; + if(isset($data_list['type']) && !empty($data_list['type'])){ + $type = intval($data_list['type']); + } + $where[] = ['type','eq',$type]; + $list = MemberMessagesModel::where($where)->order('create_time desc')->field('id,uid,message,thumbs,create_time')->limit($limit)->select()->toArray(); + if(!empty($list)){ + foreach ($list as $k=>$v){ + $thumbs = explode(',',$v['thumbs']); + foreach ($thumbs as $key=>$val){ + $thumbs[$key] = $this->getFileUrl($val); + } + $list[$k]['thumbs'] = $thumbs; + + //用户信息 + $userinfo = MemberModel::where('id',$v['uid'])->find(); + $list[$k]['headimg'] = $this->getFileUrl($userinfo['headimg']); + $list[$k]['nickname'] = $userinfo['nickname']; + + $list[$k]['create_time'] = date('Y-m-d H:i:s',$v['create_time']); + } + } + + apiReturn('200', '查询成功',$list); + } + + //获取充值数据 + public function getrecharge(){ + $list = MemberRechargeModel::field('id,points_num,price')->order('sort asc')->select()->toArray(); + apiReturn('200', '查询成功',$list); + } + + //充值 + public function addrecharge(){ + //传参数据 + $data_list = $this->params; + + if(!isset($data_list['rechargeid']) || empty($data_list['rechargeid'])){ + apiReturn('500', '充值id不能为空'); + } + $rechargeid = intval($data_list['rechargeid']); + + $info = MemberRechargeModel::where('id',$rechargeid)->find(); + if(empty($info)){ + apiReturn('500', '未查询到充值数据'); + } + + //生成订单号 + $numRes = true; + do{ + $ordernum = 'O'.date('ymd').$this->user_id.rand(100,999); + $count = MemberServiceOrderModel::where(['ordernum'=>$ordernum])->count(); + if(empty($count)) { + $numRes=false; + } + }while($numRes); + + $totalprice = $info['price']; + + $res = MemberServiceOrderModel::create([ + 'type'=>2, + 'uid'=>$this->user_id, + 'ordernum'=>$ordernum, + 'totalprice'=>$totalprice, + 'status'=>1, + 'create_time'=>time(), + 'timeout_time'=>time()+600, + 'service_title'=>$info['points_num'], + ]); + + if($res){ + //微信小程序支付回调地址 + $notify_url = 'https://' . $_SERVER['HTTP_HOST'] . url('api/wxpaynotify/index'); + + $config = [ + // 必要配置 + 'app_id' => config('smallprogram_appid'), + 'mch_id' => config('wxpay_mchid'), + 'key' => config('wxpay_paykey'), // API v2 密钥 (注意: 是v2密钥 是v2密钥 是v2密钥) + + // 如需使用敏感接口(如退款、发送红包等)需要配置 API 证书路径(登录商户平台下载 API 证书) + 'cert_path' => get_file_path(config('wxpay_appclient_cert')), // XXX: 绝对路径!!!! + 'key_path' => get_file_path(config('wxpay_appclient_key')), // XXX: 绝对路径!!!! + 'notify_url' => $notify_url, // 你也可以在下单时单独设置来覆盖它 + ]; + + $total_fee = $totalprice * 100; + $app = Factory::payment($config); + $jssdk = $app->jssdk; + + if(empty($total_fee)){ + $total_fee = 1; + } + + //下单 + $result = $app->order->unify([ + 'body' => 'DAZIQUN', + 'out_trade_no' => $ordernum, + 'total_fee' => $total_fee, + 'spbill_create_ip' => '', // 可选,如不传该参数,SDK 将会自动获取相应 IP 地址 + 'notify_url' => $notify_url, // 支付结果通知网址,如果不设置则会使用配置里的默认地址 + 'trade_type' => 'JSAPI', // 请对应换成你的支付方式对应的值类型 + 'openid' => MemberModel::where(['id' => $this->user_id])->value('openid'), + ]); + + //组合参数 + $pay = $jssdk->bridgeConfig($result['prepay_id'], false); // 返回数组 + + return json(['code' => 200, 'msg' => '微信支付信息','result'=>$pay]); + }else{ + return json(['code' => 500, 'msg' => '服务器繁忙,请稍后再试']); + } + + } + + //VIP权益说明 + public function getvipequity(){ + $info = MemberVipModel::where('id',1)->find(); + + $strs = 'https://'.$_SERVER['HTTP_HOST'].'/uploads/'; + $message = str_replace("/uploads/", $strs, $info['title']); + + apiReturn('200', '查询成功',['message'=>$message]); + } + + //获取VIP数据 + public function getvip(){ + $list = MemberVipModel::where('type',1)->field('id,title,price')->order('sort asc')->select()->toArray(); + apiReturn('200', '查询成功',$list); + } + + //用户购买vip + public function buyvip(){ + //传参数据 + $data_list = $this->params; + + if(!isset($data_list['vipid']) || empty($data_list['vipid'])){ + apiReturn('500', 'VIP id不能为空'); + } + $vipid = intval($data_list['vipid']); + + $info = MemberVipModel::where('id',$vipid)->find(); + if(empty($info)){ + apiReturn('500', '未查询到VIP数据'); + } + + //生成订单号 + $numRes = true; + do{ + $ordernum = 'O'.date('ymd').$this->user_id.rand(100,999); + $count = MemberServiceOrderModel::where(['ordernum'=>$ordernum])->count(); + if(empty($count)) { + $numRes=false; + } + }while($numRes); + + $totalprice = $info['price']; + + $res = MemberServiceOrderModel::create([ + 'type'=>3, + 'uid'=>$this->user_id, + 'ordernum'=>$ordernum, + 'totalprice'=>$totalprice, + 'status'=>1, + 'create_time'=>time(), + 'timeout_time'=>time()+600, + 'service_title'=>$info['title'], + 'service_thumb'=>$info['id'], + ]); + + if($res){ + //微信小程序支付回调地址 + $notify_url = 'https://' . $_SERVER['HTTP_HOST'] . url('api/wxpaynotify/index'); + + $config = [ + // 必要配置 + 'app_id' => config('smallprogram_appid'), + 'mch_id' => config('wxpay_mchid'), + 'key' => config('wxpay_paykey'), // API v2 密钥 (注意: 是v2密钥 是v2密钥 是v2密钥) + + // 如需使用敏感接口(如退款、发送红包等)需要配置 API 证书路径(登录商户平台下载 API 证书) + 'cert_path' => get_file_path(config('wxpay_appclient_cert')), // XXX: 绝对路径!!!! + 'key_path' => get_file_path(config('wxpay_appclient_key')), // XXX: 绝对路径!!!! + 'notify_url' => $notify_url, // 你也可以在下单时单独设置来覆盖它 + ]; + + $total_fee = $totalprice * 100; + $app = Factory::payment($config); + $jssdk = $app->jssdk; + + if(empty($total_fee)){ + $total_fee = 1; + } + + //下单 + $result = $app->order->unify([ + 'body' => 'DAZIQUN', + 'out_trade_no' => $ordernum, + 'total_fee' => $total_fee, + 'spbill_create_ip' => '', // 可选,如不传该参数,SDK 将会自动获取相应 IP 地址 + 'notify_url' => $notify_url, // 支付结果通知网址,如果不设置则会使用配置里的默认地址 + 'trade_type' => 'JSAPI', // 请对应换成你的支付方式对应的值类型 + 'openid' => MemberModel::where(['id' => $this->user_id])->value('openid'), + ]); + + //组合参数 + $pay = $jssdk->bridgeConfig($result['prepay_id'], false); // 返回数组 + + return json(['code' => 200, 'msg' => '微信支付信息','result'=>$pay]); + }else{ + return json(['code' => 500, 'msg' => '服务器繁忙,请稍后再试']); + } + + } + + //获取提现数据 + public function getwithdraw(){ + $list = MemberWithdrawModel::field('id,points_num,vip_points_num,price')->order('sort asc')->select()->toArray(); + apiReturn('200', '查询成功',$list); + } + + + //摇钱树 + public function moneytree(){ + //摇钱树等级 + $treegrade = MoneyTreeModel::field('thumb,tree_grade,integral_num,share_num')->order('tree_grade asc')->select()->toArray(); + if(!empty($treegrade)){ + foreach ($treegrade as $k=>$v){ + $treegrade[$k]['thumb'] = $this->getFileUrl($v['thumb']); + } + } + + //摇钱树说明 + $strs = 'https://'.$_SERVER['HTTP_HOST'].'/uploads/'; + $message = MoneyTreeLogsModel::where('id',1)->value('message'); + $message = str_replace("/uploads/", $strs, $message); + + //用户金币信息 + $goldcoin = MoneyTreeLogsModel::where(['uid'=>$this->user_id,'status'=>1])->find(); + if(!empty($goldcoin)){ + $goldcoin = ['id'=>$goldcoin['id'],'goldcoin_num'=>$goldcoin['integral_num']]; + }else{ + $goldcoin = []; + } + + $invitation_num = MemberModel::where(['invitation_uid'=>$this->user_id,'is_delete'=>0])->count(); + + //根据用户分享人数,获得摇钱树等级 + $tree_grade = MoneyTreeModel::gettreegrade($invitation_num); + + apiReturn('200', '查询成功',[ + 'treegrade'=>$treegrade, + 'goldcoin'=>$goldcoin, + 'message'=>$message, + 'tree_grade'=>$tree_grade, + 'goldcoin_num' => MemberModel::where('id',$this->user_id)->value('moneytree_num'), + ]); + + } + + //摇钱树 - 领取金币 + public function addgoldcoin(){ + //传参数据 + $data_list = $this->params; + + if(!isset($data_list['id']) || empty($data_list['id'])){ + apiReturn('500', '金币id不能为空'); + } + $id = intval($data_list['id']); + $goldcoin = MoneyTreeLogsModel::where(['id'=>$id,'uid'=>$this->user_id,'status'=>1])->find(); + if(empty($goldcoin)){ + apiReturn('500', '金币已领取或不存在'); + } + + $userinfo = MemberModel::where('id',$this->user_id)->find(); + $moneytree_num = $userinfo['moneytree_num'] + $goldcoin['integral_num']; + $res = MemberModel::where('id',$this->user_id)->update(['moneytree_num'=>$moneytree_num]); + + if($res){ + $goldcoin->status = 2; + $goldcoin->save(); + apiReturn('200', '领取成功'); + }else{ + apiReturn('500', '服务器繁忙,请稍后再试'); + } + } + + //小程序打开次数 + public function addopennum(){ + $userinfo = MemberModel::where('id',$this->user_id)->find(); + $userinfo->xcxopen_num = $userinfo->xcxopen_num + 1; + $userinfo->save(); + + apiReturn('200', '操作成功'); + } + + //抽奖活动 + public function raffle(){ + //用户当前的剩余金币 + $goldcoin_num = MemberModel::where('id',$this->user_id)->value('moneytree_num'); + + //奖品信息 + $where = []; + $where[] = ['id','neq',9]; + $award = AwardModel::where($where)->field('id,title,thumb')->order('sort asc')->select()->toArray(); + if(!empty($award)){ + foreach ($award as $k=>$v){ + $award[$k]['thumb'] = $this->getFileUrl($v['thumb']); + } + } + + //中奖信息 + $awardmessage = AwardMessageModel::field('title')->order('create_time desc')->select()->toArray(); + + apiReturn('200', '查询成功',[ + 'goldcoin_num'=>$goldcoin_num, + 'award'=>$award, + 'awardmessage'=>$awardmessage + ]); + } + + //抽奖活动 - 立即抽奖 + public function prizedraw(){ + //用户当前的剩余金币 + $userinfo = MemberModel::where('id',$this->user_id)->find(); + $cjjb_num = AwardModel::where('id',9)->value('jp_number'); + + if($userinfo['moneytree_num']<$cjjb_num){ + apiReturn('500', '抽奖失败,剩余金币不足'); + } + + //扣除用户金币 + $userinfo->moneytree_num = $userinfo->moneytree_num - $cjjb_num; + $userinfo->save(); + + //给 5 6 7 8号奖品增加一次按钮点击累计次数 + $where = []; + $where[] = ['id','in','5,6,7,8']; + AwardModel::where($where)->setInc('yj_number',1); + + //按逻辑进行抽奖 + $prizedraw_id = 1; + + //奖品5 + $jp5 = AwardModel::where('id',5)->find(); + if($jp5['yj_number']>=$jp5['jp_number']){ + $prizedraw_id = 5; + $jp5->yj_number = 0; + $jp5->save(); + } + + //奖品6 + $jp6 = AwardModel::where('id',6)->find(); + if($jp6['yj_number']>=$jp6['jp_number']){ + $prizedraw_id = 6; + $jp6->yj_number = 0; + $jp6->save(); + } + + //奖品7 + $jp7 = AwardModel::where('id',7)->find(); + if($jp7['yj_number']>=$jp7['jp_number']){ + $prizedraw_id = 7; + $jp7->yj_number = 0; + $jp7->save(); + } + + //奖品8 + $jp8 = AwardModel::where('id',8)->find(); + if($jp8['yj_number']>=$jp8['jp_number']){ + $prizedraw_id = 8; + $jp8->yj_number = 0; + $jp8->save(); + } + + + if($prizedraw_id==1){ + $jp1 = AwardModel::where('id',1)->find(); + $jp1_yj_number = $jp1->yj_number + 1; + if(($jp1['yj_number']+1)>=$jp1['jp_number']){ + $jp1_yj_number = 0; + + $prizedraw_array = [2,3,4]; + shuffle($prizedraw_array); + $prizedraw_id = $prizedraw_array[0]; + } + + $jp1->yj_number = $jp1_yj_number; + $jp1->save(); + } + + //插入抽奖记录 + $prizedraw_info = AwardModel::where('id',$prizedraw_id)->find(); + $res = AwardWinningsModel::create([ + 'uid'=>$this->user_id, + 'award_id'=>$prizedraw_id, + 'award_title'=>$prizedraw_info['title'], + 'award_thumb'=>$prizedraw_info['thumb'], + 'usegoldcoin'=>$cjjb_num, + 'create_time'=>time(), + 'update_time'=>time(), + ]); + + if($res){ + $prizedraw_info->open_num = $prizedraw_info->open_num+1; + $prizedraw_info->save(); + apiReturn('200', '抽奖成功',['prizedraw_id'=>$prizedraw_id]); + }else{ + apiReturn('500', '服务器繁忙,请稍后再试'); + } + } + + //抽奖活动 - 我的包 + public function prizedrawlogs(){ + + $where = []; + $where[] = ['uid','eq',$this->user_id]; + $where[] = ['award_id','neq',1]; + $where[] = ['address','eq',null]; + $list = AwardWinningsModel::where([$where])->field('id,award_title,award_thumb,create_time')->order('create_time asc')->select()->toArray(); + if(!empty($list)){ + foreach ($list as $k=>$v){ + $list[$k]['award_thumb'] = $this->getFileUrl($v['award_thumb']); + $list[$k]['create_time'] = date('Y-m-d H:i',$v['create_time']); + } + } + + apiReturn('200', '查询成功',$list); + } + + //提交邮寄地址 + public function addmailingaddress(){ + //传参数据 + $data_list = $this->params; + + if (!isset($data_list['id']) || empty($data_list['id'])) { + return json(['code' => 500, 'msg' => 'id不能为空']); + } + if (!isset($data_list['name']) || empty($data_list['name'])) { + return json(['code' => 500, 'msg' => '姓名不能为空']); + } + if (!isset($data_list['phone']) || empty($data_list['phone'])) { + return json(['code' => 500, 'msg' => '联系电话不能为空']); + } + if (!isset($data_list['address']) || empty($data_list['address'])) { + return json(['code' => 500, 'msg' => '邮寄地址不能为空']); + } + + $id = intval($data_list['id']); + $name = trim($data_list['name']); + $phone = trim($data_list['phone']); + $address = trim($data_list['address']); + + if($this->illegalcharacters($phone) || $this->illegalcharacters($name) || $this->illegalcharacters($address)){ + apiReturn('500', '非法参数'); + } + + $res = AwardWinningsModel::where(['id'=>$id,'uid'=>$this->user_id])->update([ + 'name'=>$name, + 'phone'=>$phone, + 'address'=>$address + ]); + + if ($res) { + apiReturn('200', '提交成功'); + } else { + apiReturn('500', '服务器繁忙,请稍后再试'); + } + } + + //漂流瓶 - 捡一个 + public function pickupdriftbottle(){ + $driftbottle = Db::name('member_driftbottle')->orderRaw('rand()')->find(); + + if(!empty($driftbottle)){ + $userinfo = MemberModel::where('id',$driftbottle['uid'])->find(); + apiReturn('200', '捡到漂流瓶',[ + 'nickname'=>$userinfo['nickname'], + 'headimg'=>$this->getFileUrl($userinfo['headimg']), + 'message'=>$driftbottle['message'], + 'create_time'=>date('Y-m-d H:i:s',$driftbottle['create_time']), + ]); + }else{ + apiReturn('500', '你的运气不好,没有捡到任何漂流瓶'); + } + + } + + //漂流瓶 - 扔一个 + public function throwdriftbottle(){ + //传参数据 + $data_list = $this->params; + + if (!isset($data_list['message']) || empty($data_list['message'])) { + return json(['code' => 500, 'msg' => '漂流瓶内容不能为空']); + } + + $message = trim($data_list['message']); + if($this->illegalcharacters($message)){ + apiReturn('500', '非法参数'); + } + + $res = DriftBottleModel::create([ + 'uid'=>$this->user_id, + 'message'=>$message, + 'create_time'=>time(), + ]); + + if ($res) { + apiReturn('200', '提交成功'); + } else { + apiReturn('500', '服务器繁忙,请稍后再试'); + } + } + + /** + * 写真 + * @param $groupid 群id + * @author loomis<2477365162@qq.com> + */ + public function portraitphotography(){ + + $info = MemberServiceModel::where(['id'=>12])->find(); + if(empty($info)){ + apiReturn('500', '未查询到相关写真信息'); + } + + $type=1; + $box_url = ''; + $box_psw = ''; + if(!empty(MemberServiceOrderModel::where(['service_id'=>12,'uid'=>$this->user_id,'status'=>2])->count())){ + $type = 2; + $box_url = $info['box_url']; + $box_psw = $info['box_psw']; + } + + $strs = 'https://'.$_SERVER['HTTP_HOST'].'/uploads/'; + $message = str_replace("/uploads/", $strs, $info['message']); + + $info->browse = $info->browse+1; + $info->save(); + + apiReturn('200', '查询成功',[ + 'id'=>12, + 'type'=>$type, + 'box_url'=>$box_url, + 'box_psw'=>$box_psw, + 'title'=>$info['title'], + 'thumb'=>$this->getFileUrl($info['thumb']), + 'create_time'=>date('Y-m-d H:i:s',$info['create_time']), + 'summary'=>$info['summary'], + 'message'=>$message, + ]); + } +} diff --git a/application/api/controller/Uploadfile.php b/application/api/controller/Uploadfile.php new file mode 100644 index 0000000..71e8e3d --- /dev/null +++ b/application/api/controller/Uploadfile.php @@ -0,0 +1,158 @@ + + */ + public function uploadimgfile(){ + + $file = request()->file('imgfile'); + // 移动到框架应用根目录 /public/uploads/ 目录下'size'=>15678, + $info = $file->validate(['ext' => 'jpg,png,gif,jpeg'])->move($_SERVER['DOCUMENT_ROOT'] . '/uploads/xcximg'); + if ($info) { + $source = $_SERVER['DOCUMENT_ROOT'] . '/uploads/xcximg/' . date('Ymd') . '/' . $info->getFilename(); + $percent = 0.5; #缩放比例 + (new Compress($source, $percent))->compressImg($source); //压缩 + + // 成功上传后 获取上传信息 + $data['uid'] = 0; + $data['path'] = 'uploads/xcximg/' . date('Ymd') . '/' . $info->getFilename(); + $data["md5"] = md5($data['path']); + $data["sha1"] = sha1($data['path']); + $data['name'] = $info->getFilename(); + $data['ext'] = $info->getExtension(); + $data['size'] = $info->getSize(); + $data['category'] = 2; + $data['create_time'] = time(); + $id = AttachmentModel::create($data); + + return json(['code' => 200, 'msg' => '上传成功', 'result' => [ + 'img_id' => $id['id'], + 'img_url' => $this->getFileUrl($id['id']), + ]]); + } else { + // 上传失败获取信息 + return json(['code' => 500, 'msg' => $file->getError()]); + } + } + + //上传视频 + public function uploadvideofile() { + + $file = request()->file('videofile'); + // 移动到框架应用根目录/public/uploads/ 目录下'size'=>15678, + $info = $file->validate(['ext' => 'mp4'])->move($_SERVER['DOCUMENT_ROOT'] . '/uploads/xcxvideo'); + if($info){ + // 成功上传后 获取上传信息 + $data['uid'] = 0; + $data['path'] = 'uploads/xcxvideo/' . date('Ymd') . '/' . $info->getFilename(); + $data["md5"] = md5($data['path']); + $data["sha1"] = sha1($data['path']); + $data['name'] = $info->getFilename(); + $data['ext'] = $info->getExtension(); + $data['size'] = $info->getSize(); + $data['category'] = 2; + $data['create_time'] = time(); + $id = AttachmentModel::create($data); + + return json(['code' => 200, 'msg' => '上传成功', 'result' => [ + 'video_id' => $id['id'], + 'video_url' => $this->getFileUrl($id['id']), + ]]); + } else { + // 上传失败获取信息 + return json(['code' => 500, 'msg' => $file->getError()]); + } + } + + //接收签名图片 + public function uploadsigninimgfile(){ + $filedata = $this->request->param(); //传参 + $file = $filedata['imgfile']; + + header('Content-type:text/html;charset=utf-8'); + $base64_image_content = trim($file); + //正则匹配出图片的格式 + if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $base64_image_content, $result)) { + $type = $result[2];//图片后缀 + + $new_file = $_SERVER['DOCUMENT_ROOT'] . '/uploads/xcximg/'.date('Ymd').'/'; + + + if (!file_exists($new_file)) { + //检查是否有该文件夹,如果没有就创建,并给予最高权限 + mkdir($new_file, 0700); + } + + $filename = time() . '_' . uniqid() . ".{$type}"; //文件名 + $new_file = $new_file . $filename; + + //写入操作 + if (file_put_contents($new_file, base64_decode(str_replace($result[1], '', $base64_image_content)))) { + + // 成功上传后 获取上传信息 + $data['uid'] = 0; + $data['path'] = 'uploads/xcximg/' . date('Ymd') . '/' . $filename; + $data["md5"] = md5($data['path']); + $data["sha1"] = sha1($data['path']); + $data['name'] = $filename; + $data['ext'] = $type; + $data['size'] = 0; + $data['category'] = 2; + $data['create_time'] = time(); + $id = AttachmentModel::create($data); + + return json(['code' => 200, 'msg' => '上传成功', 'result' => [ + 'img_id' => $id['id'], + 'img_url' => $this->getFileUrl($id['id']), + ]]); + } else { + return json(['code' => 500, 'msg' => '上传失败']); + } + } + } + + function getimg($imgsrc){ + list($src_w,$src_h,$src_info) = getimagesize($imgsrc); + switch ($src_info){ + case 2: + $createtype = 'imagecreatefromjpeg'; + $headertype = 'imagejpeg'; + break; + case 1: + $createtype = 'imagecreatefromgif'; + $headertype = 'imagegif'; + break; + case 3: + $createtype = 'imagecreatefrompng'; + $headertype = 'imagepng'; + break; + default: + $createtype = 'imagecreatefromjpeg'; + $headertype = 'imagejpeg'; + break; + } + $src=$createtype($imgsrc); + return $src; + } +} diff --git a/application/api/controller/Wxpaynotify.php b/application/api/controller/Wxpaynotify.php new file mode 100644 index 0000000..9dc349a --- /dev/null +++ b/application/api/controller/Wxpaynotify.php @@ -0,0 +1,247 @@ + + */ + public function index(){ + + $config = [ + // 必要配置 + 'app_id' => config('smallprogram_appid'), + 'mch_id' => config('wxpay_mchid'), + 'key' => config('wxpay_paykey'), // API v2 密钥 (注意: 是v2密钥 是v2密钥 是v2密钥) + + // 如需使用敏感接口(如退款、发送红包等)需要配置 API 证书路径(登录商户平台下载 API 证书) + 'cert_path' => get_file_path(config('wxpay_appclient_cert')), // XXX: 绝对路径!!!! + 'key_path' => get_file_path(config('wxpay_appclient_key')), // XXX: 绝对路径!!!! + ]; + $app = Factory::payment($config); + + $response = $app->handlePaidNotify(function ($message, $fail) { + + // 使用通知里的 "微信支付订单号" 或者 "商户订单号" 去自己的数据库找到订单 + $ordernum = $message['out_trade_no']; + $order = MemberServiceOrderModel::where(['ordernum'=>$ordernum])->find(); + if (!$order || $order['status'] == 2) { // 如果订单不存在 或者 订单已经支付过了 + return true; //已处理 + } + + //微信:【订单查询】接口,确认已支付 + $config = [ + // 必要配置 + 'app_id' => config('smallprogram_appid'), + 'mch_id' => config('wxpay_mchid'), + 'key' => config('wxpay_paykey'), + ]; + $app = Factory::payment($config); + $wx_res = $app->order->queryByTransactionId($message['transaction_id']); + + $weixinquery = serialize($wx_res); + + if ($message['return_code'] === 'SUCCESS') { // return_code 表示通信状态,不代表支付状态 + //// 用户是否支付成功 + if (array_key_exists('result_code', $message) && $message['result_code'] === 'SUCCESS') { + + //更新订单状态 + $orderinfo = MemberServiceOrderModel::where(['ordernum'=>$ordernum,'status'=>1])->find(); + if (!$orderinfo){ + return true; + } + if ($orderinfo['status'] != 1){ + return true; + } + + //购买群二维码 + if($orderinfo['type']==1){ + //进行积分发送 + $memberinfo = MemberModel::where('id',$orderinfo['uid'])->find(); + if(!empty($memberinfo['invitation_uid'])){ + + //一级发送积分 + if(!empty(config('one_integral'))){ + $one_invitation_uinfo = MemberModel::where('id',$memberinfo['invitation_uid'])->find(); + $points_num = $orderinfo['totalprice'] * (config('one_integral')/100); + $points_num = round($points_num); + if($points_num<=0){ + $points_num = 1; + } + MemberModel::where('id',$memberinfo['invitation_uid'])->setInc('balance',$points_num); + //插入积分记录、 + MemberBalanceLogsModel::addGetLog($memberinfo['invitation_uid'],1,$points_num,$orderinfo['ordernum'],'一级分销反馈积分',$orderinfo['uid']); + + //二级发送积分 + if(!empty($one_invitation_uinfo['invitation_uid']) && !empty(config('two_integral'))){ + $two_points_num = $orderinfo['totalprice'] * (config('two_integral')/100); + $two_points_num = round($two_points_num); + if($two_points_num<=0){ + $two_points_num = 1; + } + MemberModel::where('id',$one_invitation_uinfo['invitation_uid'])->setInc('balance',$two_points_num); + //插入积分记录、 + MemberBalanceLogsModel::addGetLog($one_invitation_uinfo['invitation_uid'],1,$two_points_num,$orderinfo['ordernum'],'二级分销反馈积分',$orderinfo['uid']); + } + } + } + } + + //积分充值 + if($orderinfo['type']==2){ + //充值成功,给用户增加积分 + MemberModel::where('id',$orderinfo['uid'])->setInc('balance',intval($orderinfo['service_title'])); + //插入积分记录、 + MemberBalanceLogsModel::addGetLog($orderinfo['uid'],1,intval($orderinfo['service_title']),$orderinfo['ordernum'],'用户充值积分'); + } + + //购买vip + if($orderinfo['type']==3){ + MemberModel::where('id',$orderinfo['uid'])->update([ + 'vip_id'=>$orderinfo['service_thumb'], + 'vip_title'=>$orderinfo['service_title'], + ]); + } + + $res = MemberServiceOrderModel::where(['ordernum'=>$ordernum])->update([ + 'status'=>2, + 'update_time'=>time(), + 'pay_time'=>time(), + 'pay_data' =>$weixinquery, + ]); + + } elseif (array_key_exists('result_code', $message) && $message['result_code'] === 'FAIL') { + // 用户支付失败 + return $fail('通信失败,请稍后再通知我'); + } + } else { + return $fail('通信失败,请稍后再通知我'); + } + + return true; // 返回处理完成 + + }); + + $response->send(); + + } + + + //取消超时未支付的订单 + public function cancelorder(){ + + $where = []; + $where[] = ['timeout_time','elt',time()]; + $where[] = ['status','eq',1]; + + if(empty(MemberServiceOrderModel::where($where)->count())){ + return 'ok'; + } + $res = MemberServiceOrderModel::where($where)->update(['status'=>3]); + if($res){ + return 'ok'; + }else{ + return 'fail'; + } + } + + + //用户协议 + public function useragreement(){ + + $useragreementmsg = config('useragreement'); + + //商品详情 + $strs = 'https://'.$_SERVER['HTTP_HOST'].'/uploads/'; + $useragreementmsg= str_replace("/uploads/", $strs, $useragreementmsg); + + return json(['code' => 200, 'msg' => '查询成功','result'=>$useragreementmsg]); + } + + //隐私政策 + public function privacypolicy(){ + + $useragreementmsg = config('privacypolicy'); + + //商品详情 + $strs = 'https://'.$_SERVER['HTTP_HOST'].'/uploads/'; + $useragreementmsg= str_replace("/uploads/", $strs, $useragreementmsg); + + return json(['code' => 200, 'msg' => '查询成功','result'=>$useragreementmsg]); + } + + //摇钱树产生金币 + public function moneytree(){ + $userlist = MemberModel::where('is_delete',0)->select(); + if(!empty($userlist)){ + foreach ($userlist as $k=>$v){ + + //存在未领取金币用户不持续产生金币 + $wlqjinbi = MMoneyTreeLogsModel::where(['uid'=>$v['id'],'status'=>1])->count(); + if(!empty($wlqjinbi)){ + continue; + } + + //无邀请用户的用户不产生金币 + $invitation_num = MemberModel::where(['invitation_uid'=>$v['id'],'is_delete'=>0])->count(); + if(empty($invitation_num)){ + continue; + } + + //根据用户分享人数,获得对应积分 + $integral_num = MoneyTreeModel::getintegralnum($invitation_num); + //根据用户分享人数,获得摇钱树等级 + $tree_grade = MoneyTreeModel::gettreegrade($invitation_num); + if(empty($integral_num) || empty($tree_grade)){ + continue; + } + + $is_generate = 0; + $create_time = MMoneyTreeLogsModel::where(['uid'=>$v['id']])->order('create_time desc')->value('create_time'); + if(!empty($create_time)){ + $create_time = $create_time + 86400; + if($create_time<=time()){ + $is_generate = 1; + } + }else{ + $is_generate = 1; + } + + if($is_generate==1){ + MMoneyTreeLogsModel::create([ + 'uid'=>$v['id'], + 'tree_grade'=>$tree_grade, + 'integral_num'=>$integral_num, + 'create_time'=>time(), + 'update_time'=>time(), + ]); + } + } + } + + echo 'ok'; + } +} diff --git a/application/api/controller/wechat.log b/application/api/controller/wechat.log new file mode 100644 index 0000000..8b308ce --- /dev/null +++ b/application/api/controller/wechat.log @@ -0,0 +1,5904 @@ +[2024-10-15T09:02:03.079481+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85__DBO4riSF_88krjSDRzpM3MSSU2MlFO6TjRmvaNVWptZcZuuLQQ3Cz83FzOfvt-zmohVUysw43IwexE4nu_eoYSxFxP-QDczQUJvqu3_J6dC6W4fgxjZC6cEcRcPWNfABAGNE&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=xxxxxxx&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 15 Oct 2024 01:01:37 GMT +Content-Length: 74 + +{"errcode":40029,"errmsg":"invalid code, rid: 670dbef1-784da15c-6bdbea7e"} +-------- +NULL +[2024-10-15T09:31:34.729441+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85__DBO4riSF_88krjSDRzpM3MSSU2MlFO6TjRmvaNVWptZcZuuLQQ3Cz83FzOfvt-zmohVUysw43IwexE4nu_eoYSxFxP-QDczQUJvqu3_J6dC6W4fgxjZC6cEcRcPWNfABAGNE&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e1KsFFa1n0SkI0rpIFa169VlD4KsFFs&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 15 Oct 2024 01:31:09 GMT +Content-Length: 82 + +{"session_key":"kXoSMfI8dxD8ZR0T9rKYbg==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-15T10:18:28.420858+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85__DBO4riSF_88krjSDRzpM3MSSU2MlFO6TjRmvaNVWptZcZuuLQQ3Cz83FzOfvt-zmohVUysw43IwexE4nu_eoYSxFxP-QDczQUJvqu3_J6dC6W4fgxjZC6cEcRcPWNfABAGNE&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a17txGa1lR4kI0nIHJa1zOVua37txGX&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 15 Oct 2024 02:18:03 GMT +Content-Length: 82 + +{"session_key":"fGlm8iEqDAV4HfhykiAzDg==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-15T10:21:17.176079+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85__DBO4riSF_88krjSDRzpM3MSSU2MlFO6TjRmvaNVWptZcZuuLQQ3Cz83FzOfvt-zmohVUysw43IwexE4nu_eoYSxFxP-QDczQUJvqu3_J6dC6W4fgxjZC6cEcRcPWNfABAGNE&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b1Mea1w3VLHE332nE0w3wnzXE0Mea1a&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 15 Oct 2024 02:20:51 GMT +Content-Length: 82 + +{"session_key":"SbF/W4upjRenvDchN+Yo2A==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-15T11:47:49.459731+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_fUELFI3-Y-RkQY055dTf87JQV4XCtHTd_oFvCESK29sgrJU-R5-7aQOrVlEUlteRFpBtYipDE4jR3P568q87AbXQboTPw0BWMuFHR4z1gPTx5cUwg_F37jAsQBsSABeAJAVDE&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a1NO4Ga1mYAlI0REEFa1xzrb90NO4GL&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 15 Oct 2024 03:47:24 GMT +Content-Length: 82 + +{"session_key":"Ed77huo5/RyRej7So2gjPg==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-15T16:39:46.688806+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_qQW6NFBFw7IWvdwrLhCA9REMjcOS3L3L8Gzhsn99qKY8HMTBDG0G003FVNb-yziJWhhMABVhGsVnYgGIuy_YYq6f_bTVFFStCHbdbbFn0gzU2KU7-gp1h99Dn9sWZOfAIAJMB&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d1wgg1w3Ol4F339BE3w3vUH6m1wgg1o&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 15 Oct 2024 08:39:21 GMT +Content-Length: 82 + +{"session_key":"D4UymJEGkyvEY4bs4RX/bA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-15T16:42:05.533925+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_qQW6NFBFw7IWvdwrLhCA9REMjcOS3L3L8Gzhsn99qKY8HMTBDG0G003FVNb-yziJWhhMABVhGsVnYgGIuy_YYq6f_bTVFFStCHbdbbFn0gzU2KU7-gp1h99Dn9sWZOfAIAJMB&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b1EUQkl2xFtle4UD9ol27YTsi3EUQk6&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 15 Oct 2024 08:41:40 GMT +Content-Length: 82 + +{"session_key":"5x6UShreZ8XQvCBdWLha4A==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-15T16:42:50.030065+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_qQW6NFBFw7IWvdwrLhCA9REMjcOS3L3L8Gzhsn99qKY8HMTBDG0G003FVNb-yziJWhhMABVhGsVnYgGIuy_YYq6f_bTVFFStCHbdbbFn0gzU2KU7-gp1h99Dn9sWZOfAIAJMB&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a18XbHa1myTjI0C1YFa1jzSoF18XbH7&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 15 Oct 2024 08:42:24 GMT +Content-Length: 82 + +{"session_key":"kpEHAa/dSDpQVMLEpw7s2w==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T10:31:51.108444+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_4jmN-7YPTJLNbnFC25e_2Gg4S0gieb1Wj6GCCYAdWMSQL6sdCUmRzPsESjtJ62wLD3Hk5J5zOXu_cCZFzrip_gX8fwxy4WIZB58oI8ojtXgBmRxgsEeEz5tc-w8YMSaADANBH&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d1wxh1000FUZS19Ra400zB9gp3wxh1C&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 02:31:25 GMT +Content-Length: 82 + +{"session_key":"8nMR0RA5h2LAKTxh9ZK42g==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T10:36:27.019001+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_4jmN-7YPTJLNbnFC25e_2Gg4S0gieb1Wj6GCCYAdWMSQL6sdCUmRzPsESjtJ62wLD3Hk5J5zOXu_cCZFzrip_gX8fwxy4WIZB58oI8ojtXgBmRxgsEeEz5tc-w8YMSaADANBH&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e1Ujmll2DS4le4X4sll2h6cqq3Ujml4&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 02:36:01 GMT +Content-Length: 82 + +{"session_key":"3MgZbbnjinnVhoq969BWGg==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T10:41:36.884922+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_4jmN-7YPTJLNbnFC25e_2Gg4S0gieb1Wj6GCCYAdWMSQL6sdCUmRzPsESjtJ62wLD3Hk5J5zOXu_cCZFzrip_gX8fwxy4WIZB58oI8ojtXgBmRxgsEeEz5tc-w8YMSaADANBH&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0f1PevFa1VUHlI0uk4Ia1wqIcW3PevF2&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 02:41:11 GMT +Content-Length: 82 + +{"session_key":"3MgZbbnjinnVhoq969BWGg==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T10:57:39.986419+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_4jmN-7YPTJLNbnFC25e_2Gg4S0gieb1Wj6GCCYAdWMSQL6sdCUmRzPsESjtJ62wLD3Hk5J5zOXu_cCZFzrip_gX8fwxy4WIZB58oI8ojtXgBmRxgsEeEz5tc-w8YMSaADANBH&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0f12Yt0006TP0T13Gb0003DbFR32Yt0m&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 02:57:14 GMT +Content-Length: 82 + +{"session_key":"6yECKyZ3TTJSQNMrPldlsg==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T10:59:26.109066+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_4jmN-7YPTJLNbnFC25e_2Gg4S0gieb1Wj6GCCYAdWMSQL6sdCUmRzPsESjtJ62wLD3Hk5J5zOXu_cCZFzrip_gX8fwxy4WIZB58oI8ojtXgBmRxgsEeEz5tc-w8YMSaADANBH&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c1NInll2Fible4x8Hkl22MUf83NInlP&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 02:59:00 GMT +Content-Length: 82 + +{"session_key":"fBL9vRF7j6BFsb98ctBvNQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T11:08:01.473238+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_4jmN-7YPTJLNbnFC25e_2Gg4S0gieb1Wj6GCCYAdWMSQL6sdCUmRzPsESjtJ62wLD3Hk5J5zOXu_cCZFzrip_gX8fwxy4WIZB58oI8ojtXgBmRxgsEeEz5tc-w8YMSaADANBH&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d1KW4Ga1rhflI0t0yIa1HzkxQ3KW4GT&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 03:07:35 GMT +Content-Length: 82 + +{"session_key":"7op68ZYKT5K1ca172XHwJA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T11:09:39.384962+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_4jmN-7YPTJLNbnFC25e_2Gg4S0gieb1Wj6GCCYAdWMSQL6sdCUmRzPsESjtJ62wLD3Hk5J5zOXu_cCZFzrip_gX8fwxy4WIZB58oI8ojtXgBmRxgsEeEz5tc-w8YMSaADANBH&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c1qN2100Jgh0T1xLt100F1u6w3qN21G&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 03:09:13 GMT +Content-Length: 82 + +{"session_key":"/bG0fXZ1dGX/y51VHYXK7g==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T11:21:08.408251+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_4jmN-7YPTJLNbnFC25e_2Gg4S0gieb1Wj6GCCYAdWMSQL6sdCUmRzPsESjtJ62wLD3Hk5J5zOXu_cCZFzrip_gX8fwxy4WIZB58oI8ojtXgBmRxgsEeEz5tc-w8YMSaADANBH&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e10Kq200ohUYS1bb9400PGFH620Kq2e&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 03:20:42 GMT +Content-Length: 82 + +{"session_key":"WZkTa9lgstLFZh6JflOTbg==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T11:22:06.794429+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_4jmN-7YPTJLNbnFC25e_2Gg4S0gieb1Wj6GCCYAdWMSQL6sdCUmRzPsESjtJ62wLD3Hk5J5zOXu_cCZFzrip_gX8fwxy4WIZB58oI8ojtXgBmRxgsEeEz5tc-w8YMSaADANBH&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b16XUGa12jqkI0pqzHa1LkWvQ36XUGb&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 03:21:41 GMT +Content-Length: 82 + +{"session_key":"WZkTa9lgstLFZh6JflOTbg==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T11:25:40.501541+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_4jmN-7YPTJLNbnFC25e_2Gg4S0gieb1Wj6GCCYAdWMSQL6sdCUmRzPsESjtJ62wLD3Hk5J5zOXu_cCZFzrip_gX8fwxy4WIZB58oI8ojtXgBmRxgsEeEz5tc-w8YMSaADANBH&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d1t16Ga1xuflI0bWAIa1Q2Zoj0t16Gq&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 03:25:15 GMT +Content-Length: 82 + +{"session_key":"6dbpbkWHjPRGeVXVEO0JJQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T11:35:59.313906+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_4jmN-7YPTJLNbnFC25e_2Gg4S0gieb1Wj6GCCYAdWMSQL6sdCUmRzPsESjtJ62wLD3Hk5J5zOXu_cCZFzrip_gX8fwxy4WIZB58oI8ojtXgBmRxgsEeEz5tc-w8YMSaADANBH&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b16Ba2009W9ZS15dK000JbL6m16Ba2L&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 03:35:33 GMT +Content-Length: 82 + +{"session_key":"i1OUH7urh9+wsMmPp032ig==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T11:44:49.017923+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_4jmN-7YPTJLNbnFC25e_2Gg4S0gieb1Wj6GCCYAdWMSQL6sdCUmRzPsESjtJ62wLD3Hk5J5zOXu_cCZFzrip_gX8fwxy4WIZB58oI8ojtXgBmRxgsEeEz5tc-w8YMSaADANBH&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c1lW4100iOg0T1aHN000WsBzm2lW41N&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 03:44:23 GMT +Content-Length: 82 + +{"session_key":"G5+L7YyzKSypCU2OoLlecQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T12:33:50.459664+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_BaJevpIoYFVMyByxjXyudKVpvU6ChBzulopjOCJy4gBvhoQ_PP810DBvhjC3PnvWxE3tum-wUvJ94Q0bKk-YQpsIsGUAKcKa4_dW75w-UP1-N-dXmOfnUONtqzEUALaABACIT&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c112lFa1nTDlI0tYrFa1xgskm412lFD&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 04:33:24 GMT +Content-Length: 82 + +{"session_key":"9V16vxqhOYJ6sBmxPiKa0w==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T12:34:38.478612+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_BaJevpIoYFVMyByxjXyudKVpvU6ChBzulopjOCJy4gBvhoQ_PP810DBvhjC3PnvWxE3tum-wUvJ94Q0bKk-YQpsIsGUAKcKa4_dW75w-UP1-N-dXmOfnUONtqzEUALaABACIT&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a1EVQ000q480T125f10041B5B4EVQ0m&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 04:34:12 GMT +Content-Length: 82 + +{"session_key":"07o9EWeuDc7vJt7GdrHRPA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T12:35:06.736890+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_BaJevpIoYFVMyByxjXyudKVpvU6ChBzulopjOCJy4gBvhoQ_PP810DBvhjC3PnvWxE3tum-wUvJ94Q0bKk-YQpsIsGUAKcKa4_dW75w-UP1-N-dXmOfnUONtqzEUALaABACIT&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d11U31w3jnaF33vK74w3lY4CF41U31B&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 04:34:41 GMT +Content-Length: 82 + +{"session_key":"07o9EWeuDc7vJt7GdrHRPA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T12:37:02.982480+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_BaJevpIoYFVMyByxjXyudKVpvU6ChBzulopjOCJy4gBvhoQ_PP810DBvhjC3PnvWxE3tum-wUvJ94Q0bKk-YQpsIsGUAKcKa4_dW75w-UP1-N-dXmOfnUONtqzEUALaABACIT&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c1HYi000UxG0T1Abn000vGk9j2HYi0E&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 04:36:37 GMT +Content-Length: 82 + +{"session_key":"mRnoj8dgDHRc/ZbnCteW3g==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T12:39:16.743272+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_BaJevpIoYFVMyByxjXyudKVpvU6ChBzulopjOCJy4gBvhoQ_PP810DBvhjC3PnvWxE3tum-wUvJ94Q0bKk-YQpsIsGUAKcKa4_dW75w-UP1-N-dXmOfnUONtqzEUALaABACIT&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0f1cLcll2oY1le4noNol2o35dW3cLclG&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 04:38:51 GMT +Content-Length: 82 + +{"session_key":"gL+N2vptt2IX2WpcmZvslA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T12:41:23.949720+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_BaJevpIoYFVMyByxjXyudKVpvU6ChBzulopjOCJy4gBvhoQ_PP810DBvhjC3PnvWxE3tum-wUvJ94Q0bKk-YQpsIsGUAKcKa4_dW75w-UP1-N-dXmOfnUONtqzEUALaABACIT&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b1grp100rGAZS1xbd300EvXhG3grp1Y&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 04:40:58 GMT +Content-Length: 82 + +{"session_key":"G3A/c4+ZY2QD/F+t+cq5/A==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T12:44:19.108961+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_BaJevpIoYFVMyByxjXyudKVpvU6ChBzulopjOCJy4gBvhoQ_PP810DBvhjC3PnvWxE3tum-wUvJ94Q0bKk-YQpsIsGUAKcKa4_dW75w-UP1-N-dXmOfnUONtqzEUALaABACIT&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b1d1Wkl22Kjle4juGll2pNH7N3d1WkT&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 04:43:53 GMT +Content-Length: 82 + +{"session_key":"aONG5XgtD9iVPfYVC/JTfw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T12:53:14.661463+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_BaJevpIoYFVMyByxjXyudKVpvU6ChBzulopjOCJy4gBvhoQ_PP810DBvhjC3PnvWxE3tum-wUvJ94Q0bKk-YQpsIsGUAKcKa4_dW75w-UP1-N-dXmOfnUONtqzEUALaABACIT&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0f1XyWkl2V9kle4KGynl2XwrK04XyWkj&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 04:52:49 GMT +Content-Length: 82 + +{"session_key":"u/9IgmC0FbLEFO4WpXCZ4w==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T12:55:02.046145+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_BaJevpIoYFVMyByxjXyudKVpvU6ChBzulopjOCJy4gBvhoQ_PP810DBvhjC3PnvWxE3tum-wUvJ94Q0bKk-YQpsIsGUAKcKa4_dW75w-UP1-N-dXmOfnUONtqzEUALaABACIT&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e1OOLll2GOuke4z0cml2eaEiQ0OOLlO&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 04:54:36 GMT +Content-Length: 82 + +{"session_key":"52C7b3VKJbXU5tdW13NjRQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T13:02:38.942117+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_BaJevpIoYFVMyByxjXyudKVpvU6ChBzulopjOCJy4gBvhoQ_PP810DBvhjC3PnvWxE3tum-wUvJ94Q0bKk-YQpsIsGUAKcKa4_dW75w-UP1-N-dXmOfnUONtqzEUALaABACIT&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c1xbell2784le4zC8ol2zUu134xbelP&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 05:02:13 GMT +Content-Length: 82 + +{"session_key":"BZlsptLsKwj300lhzWJyiA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T13:07:41.028336+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_BaJevpIoYFVMyByxjXyudKVpvU6ChBzulopjOCJy4gBvhoQ_PP810DBvhjC3PnvWxE3tum-wUvJ94Q0bKk-YQpsIsGUAKcKa4_dW75w-UP1-N-dXmOfnUONtqzEUALaABACIT&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a1tN3000YYY0T1pq7000FDjRU0tN30g&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 05:07:15 GMT +Content-Length: 82 + +{"session_key":"91rEq99KHbi18uoTWb2HXw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T13:11:20.802985+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_BaJevpIoYFVMyByxjXyudKVpvU6ChBzulopjOCJy4gBvhoQ_PP810DBvhjC3PnvWxE3tum-wUvJ94Q0bKk-YQpsIsGUAKcKa4_dW75w-UP1-N-dXmOfnUONtqzEUALaABACIT&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d1V0y0w3PqKF33JtZ3w3R43YE0V0y0b&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 05:10:55 GMT +Content-Length: 82 + +{"session_key":"yR/O04TY0Jo83NqLsFMA1g==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T13:11:20.809787+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_BaJevpIoYFVMyByxjXyudKVpvU6ChBzulopjOCJy4gBvhoQ_PP810DBvhjC3PnvWxE3tum-wUvJ94Q0bKk-YQpsIsGUAKcKa4_dW75w-UP1-N-dXmOfnUONtqzEUALaABACIT&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b1ZyKGa1HGikI0O9RHa1PtaDv2ZyKG2&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 05:10:55 GMT +Content-Length: 82 + +{"session_key":"yR/O04TY0Jo83NqLsFMA1g==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T13:12:00.923597+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_BaJevpIoYFVMyByxjXyudKVpvU6ChBzulopjOCJy4gBvhoQ_PP810DBvhjC3PnvWxE3tum-wUvJ94Q0bKk-YQpsIsGUAKcKa4_dW75w-UP1-N-dXmOfnUONtqzEUALaABACIT&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d1SEGkl2sUBle4p9Lol2SU4O21SEGku&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 05:11:35 GMT +Content-Length: 82 + +{"session_key":"qgYLBhUY9gCz9Ij1dAlv6g==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T13:15:24.014734+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_BaJevpIoYFVMyByxjXyudKVpvU6ChBzulopjOCJy4gBvhoQ_PP810DBvhjC3PnvWxE3tum-wUvJ94Q0bKk-YQpsIsGUAKcKa4_dW75w-UP1-N-dXmOfnUONtqzEUALaABACIT&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e1aznFa1eOylI0H8IFa1oCH1d1aznFA&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 05:14:58 GMT +Content-Length: 82 + +{"session_key":"ISU55e/guqSfYpMOe12CNQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T13:21:25.898774+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_BaJevpIoYFVMyByxjXyudKVpvU6ChBzulopjOCJy4gBvhoQ_PP810DBvhjC3PnvWxE3tum-wUvJ94Q0bKk-YQpsIsGUAKcKa4_dW75w-UP1-N-dXmOfnUONtqzEUALaABACIT&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d1XVnFa1gBylI01ueJa1ZccSU0XVnF3&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 05:21:00 GMT +Content-Length: 82 + +{"session_key":"X9jHhp7rLiFge3rx+ifQyA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T14:48:25.006205+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_tOn6z96pJyyPTqu491OLjlhUVjtf7f0J9BoVZHpAfUBufDt72mLWcXB98qpu7Tb_3Xw0nCamRezjfV3Fzv2PZcXkk0hLdQ_Sx8HhblMW9qgAD7q4x3HIJ1KCWFESTDaAFAXYI&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a1l5Z0003ti0T1BqD200nCP6G0l5Z05&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 06:47:59 GMT +Content-Length: 82 + +{"session_key":"2grPLg2DRgNEkIm9gi7BNw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T14:49:31.578814+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_tOn6z96pJyyPTqu491OLjlhUVjtf7f0J9BoVZHpAfUBufDt72mLWcXB98qpu7Tb_3Xw0nCamRezjfV3Fzv2PZcXkk0hLdQ_Sx8HhblMW9qgAD7q4x3HIJ1KCWFESTDaAFAXYI&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a18yQGa1VgrkI0nt9Ja161SQU08yQGB&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 06:49:06 GMT +Content-Length: 82 + +{"session_key":"crfhz5MZFBkeCAU9+g+HeQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T16:07:56.004944+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_tOn6z96pJyyPTqu491OLjlhUVjtf7f0J9BoVZHpAfUBufDt72mLWcXB98qpu7Tb_3Xw0nCamRezjfV3Fzv2PZcXkk0hLdQ_Sx8HhblMW9qgAD7q4x3HIJ1KCWFESTDaAFAXYI&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b1rb6Ga1HgblI0ce4Ga19UTKf2rb6Ge&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 08:07:30 GMT +Content-Length: 82 + +{"session_key":"XLH2XVQXHCxT/TCrSJsxqQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T16:19:45.139480+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_tOn6z96pJyyPTqu491OLjlhUVjtf7f0J9BoVZHpAfUBufDt72mLWcXB98qpu7Tb_3Xw0nCamRezjfV3Fzv2PZcXkk0hLdQ_Sx8HhblMW9qgAD7q4x3HIJ1KCWFESTDaAFAXYI&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d1hHP1w3oYGE33AXR1w3reeov4hHP1q&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 08:19:19 GMT +Content-Length: 82 + +{"session_key":"6gQ7M8eu1CAOzJQYVDCumQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T16:21:00.774677+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_tOn6z96pJyyPTqu491OLjlhUVjtf7f0J9BoVZHpAfUBufDt72mLWcXB98qpu7Tb_3Xw0nCamRezjfV3Fzv2PZcXkk0hLdQ_Sx8HhblMW9qgAD7q4x3HIJ1KCWFESTDaAFAXYI&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a1Y7WGa10glkI0V93Ia1yCyIE2Y7WGW&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 08:20:35 GMT +Content-Length: 82 + +{"session_key":"0H6DV0qvSn3SyY8pn2fPmw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T16:22:32.468843+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_tOn6z96pJyyPTqu491OLjlhUVjtf7f0J9BoVZHpAfUBufDt72mLWcXB98qpu7Tb_3Xw0nCamRezjfV3Fzv2PZcXkk0hLdQ_Sx8HhblMW9qgAD7q4x3HIJ1KCWFESTDaAFAXYI&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a14Gf000kL11T1EWF1000UP9444Gf0S&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 08:22:06 GMT +Content-Length: 82 + +{"session_key":"5Y6RtuiH78diQL6dEs2AUQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T16:24:27.222527+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_tOn6z96pJyyPTqu491OLjlhUVjtf7f0J9BoVZHpAfUBufDt72mLWcXB98qpu7Tb_3Xw0nCamRezjfV3Fzv2PZcXkk0hLdQ_Sx8HhblMW9qgAD7q4x3HIJ1KCWFESTDaAFAXYI&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d1VMJ0w3WrMF33rkXZv3lU6RK3VMJ0v&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 08:24:01 GMT +Content-Length: 82 + +{"session_key":"fWq3y2VbhPScS1CPCUBugw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T16:26:53.279017+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_tOn6z96pJyyPTqu491OLjlhUVjtf7f0J9BoVZHpAfUBufDt72mLWcXB98qpu7Tb_3Xw0nCamRezjfV3Fzv2PZcXkk0hLdQ_Sx8HhblMW9qgAD7q4x3HIJ1KCWFESTDaAFAXYI&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d1fAuHa1eNMjI0nbgGa1BfqaA2fAuHs&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 08:26:27 GMT +Content-Length: 82 + +{"session_key":"rRQEJPRZTl0IkZh7WUCILA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-16T16:35:29.894162+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_tOn6z96pJyyPTqu491OLjlhUVjtf7f0J9BoVZHpAfUBufDt72mLWcXB98qpu7Tb_3Xw0nCamRezjfV3Fzv2PZcXkk0hLdQ_Sx8HhblMW9qgAD7q4x3HIJ1KCWFESTDaAFAXYI&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c1J0XGa1GekkI00AvIa1BiYG33J0XGX&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 16 Oct 2024 08:35:04 GMT +Content-Length: 82 + +{"session_key":"B71ZvDUGmrJTirBnBhgnfw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-17T09:29:30.231364+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_iw6MpDZVSB1I1fLGLhCA9REMjcOS3L3L8GzhsqX_A-TQC_v4NLt3T2Kd2ZGUcKPKkhu1LLCpK_AM0ciJP38DBurE1PWvs7zo79lPky0r1QFvdsXVhg4KUNBUUsYRHGjACAXNM&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c1Qk12w3R3EI33oUXZv3OXKLc3Qk12L&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 17 Oct 2024 01:29:04 GMT +Content-Length: 82 + +{"session_key":"qymLCSZrdFAhKuUm9Hqu5g==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-17T11:58:55.574658+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_ETo2FIhQYQVc49-39oOE2TNLLAeqc9hCxYvlZuxqofPE98Qls1UI4bhdMSGOw39UsReOpyuAHv6MFirpxe6fvgvxorPf-jkxpCD-QC6_yqXu94cfSkRajh2aHr0RURgACACFC&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e1Vcj000lOA1T1URl40027FtY3Vcj0x&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 17 Oct 2024 03:58:29 GMT +Content-Length: 82 + +{"session_key":"fenC4CPcXQUaK7ZFgNEoqg==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-17T17:21:16.083342+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_WmzT_ozwGm12wWYOaQdlUYPTVVuiHGJVL8ZzjOV3PhgAzoQzzjou0Ay-4u5kyFsFxZ3RH2eptwa18VQ5YtIEiu0uvw4RWiFO_Is3Jf3Tm7cCP9Jx58lzbvOHM1sZTEeADAUOZ&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d1usfll2TKsme4SFRll2mHHja0usflp&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 17 Oct 2024 09:20:50 GMT +Content-Length: 82 + +{"session_key":"6jjBLpYxXMgWvhXQk0dvVQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-17T17:23:50.514736+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_WmzT_ozwGm12wWYOaQdlUYPTVVuiHGJVL8ZzjOV3PhgAzoQzzjou0Ay-4u5kyFsFxZ3RH2eptwa18VQ5YtIEiu0uvw4RWiFO_Is3Jf3Tm7cCP9Jx58lzbvOHM1sZTEeADAUOZ&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a1d1D000h1R1T1kZK1007Jd1C1d1D0p&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 17 Oct 2024 09:23:24 GMT +Content-Length: 82 + +{"session_key":"mhknbtarI1SomIIDBuu0Dw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-17T17:40:12.105886+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_WmzT_ozwGm12wWYOaQdlUYPTVVuiHGJVL8ZzjOV3PhgAzoQzzjou0Ay-4u5kyFsFxZ3RH2eptwa18VQ5YtIEiu0uvw4RWiFO_Is3Jf3Tm7cCP9Jx58lzbvOHM1sZTEeADAUOZ&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0f1DXQ0w3GzpH33B8S3w3vEkmN1DXQ0p&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 17 Oct 2024 09:39:46 GMT +Content-Length: 82 + +{"session_key":"MtR1PKsvTo/sqGPaWKEmQQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-17T19:20:33.658740+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_WmzT_ozwGm12wWYOaQdlUYPTVVuiHGJVL8ZzjOV3PhgAzoQzzjou0Ay-4u5kyFsFxZ3RH2eptwa18VQ5YtIEiu0uvw4RWiFO_Is3Jf3Tm7cCP9Jx58lzbvOHM1sZTEeADAUOZ&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b1xPUll2SPVme4ozqll21aMg54xPUlG&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 17 Oct 2024 11:20:07 GMT +Content-Length: 82 + +{"session_key":"2y9fM98lm+aBojcKNkJEvQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-17T19:21:07.788859+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_WmzT_ozwGm12wWYOaQdlUYPTVVuiHGJVL8ZzjOV3PhgAzoQzzjou0Ay-4u5kyFsFxZ3RH2eptwa18VQ5YtIEiu0uvw4RWiFO_Is3Jf3Tm7cCP9Jx58lzbvOHM1sZTEeADAUOZ&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d1cCSGa1PZgnI09yYFa1OAbwQ3cCSGK&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 17 Oct 2024 11:20:41 GMT +Content-Length: 82 + +{"session_key":"ULeizn4Bc4++Ia8Gd4eBgA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T09:00:46.993029+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_D3Vuivhe9ISLYfI9LhCA9REMjcOS3L3L8Gzhsr21s_re-AfmaxmAoHFqlYNdzNnRz9x_TNgrEJfynQUrT3fFUbk8Cw4pNgfw5-ruTqdMqf9G07r0sebWbS_ipEkNKNdAFASPS&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e1STa000Awq1T1TSe000FmJeH0STa06&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 01:00:20 GMT +Content-Length: 82 + +{"session_key":"Yzr3ntPb59Pbda2fKI7Wcw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T10:16:16.668551+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_D3Vuivhe9ISLYfI9LhCA9REMjcOS3L3L8Gzhsr21s_re-AfmaxmAoHFqlYNdzNnRz9x_TNgrEJfynQUrT3fFUbk8Cw4pNgfw5-ruTqdMqf9G07r0sebWbS_ipEkNKNdAFASPS&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e1Srs0w3oBuG33pWz0w3RiG3z2Srs0x&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 02:15:50 GMT +Content-Length: 82 + +{"session_key":"Ob4sHqxwCmqGxhHVTtIHoA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T10:17:26.045424+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_D3Vuivhe9ISLYfI9LhCA9REMjcOS3L3L8Gzhsr21s_re-AfmaxmAoHFqlYNdzNnRz9x_TNgrEJfynQUrT3fFUbk8Cw4pNgfw5-ruTqdMqf9G07r0sebWbS_ipEkNKNdAFASPS&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c1NLl1006By2T1bdO300mq5F11NLl1k&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 02:17:00 GMT +Content-Length: 82 + +{"session_key":"3QRi4XslhFClSA2goQ4RMg==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T10:18:33.217772+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_D3Vuivhe9ISLYfI9LhCA9REMjcOS3L3L8Gzhsr21s_re-AfmaxmAoHFqlYNdzNnRz9x_TNgrEJfynQUrT3fFUbk8Cw4pNgfw5-ruTqdMqf9G07r0sebWbS_ipEkNKNdAFASPS&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e1DhuHa1RxwnI0MjAIa14W1Mh4DhuHv&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 02:18:07 GMT +Content-Length: 82 + +{"session_key":"yEtcQSPQgP70zdEwLfiEsg==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T10:19:15.887384+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_D3Vuivhe9ISLYfI9LhCA9REMjcOS3L3L8Gzhsr21s_re-AfmaxmAoHFqlYNdzNnRz9x_TNgrEJfynQUrT3fFUbk8Cw4pNgfw5-ruTqdMqf9G07r0sebWbS_ipEkNKNdAFASPS&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e1ChSkl2b55me4GzJml2p663k4ChSk8&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 02:18:49 GMT +Content-Length: 82 + +{"session_key":"VG6isCOiHDcyPyPEKZgj1g==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T10:22:12.970246+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_D3Vuivhe9ISLYfI9LhCA9REMjcOS3L3L8Gzhsr21s_re-AfmaxmAoHFqlYNdzNnRz9x_TNgrEJfynQUrT3fFUbk8Cw4pNgfw5-ruTqdMqf9G07r0sebWbS_ipEkNKNdAFASPS&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e1Qyqll2mOCme4P7vnl29Ako93Qyqlf&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 02:21:46 GMT +Content-Length: 82 + +{"session_key":"75aWDHa5u5q3dauPPxxIJw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T10:23:10.739657+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_D3Vuivhe9ISLYfI9LhCA9REMjcOS3L3L8Gzhsr21s_re-AfmaxmAoHFqlYNdzNnRz9x_TNgrEJfynQUrT3fFUbk8Cw4pNgfw5-ruTqdMqf9G07r0sebWbS_ipEkNKNdAFASPS&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e1VOb0w3jtLG33Dux1w3ISerA0VOb09&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 02:22:44 GMT +Content-Length: 82 + +{"session_key":"ALziHr7pZGTVUoJfeZ4SmA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T10:25:24.212053+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_D3Vuivhe9ISLYfI9LhCA9REMjcOS3L3L8Gzhsr21s_re-AfmaxmAoHFqlYNdzNnRz9x_TNgrEJfynQUrT3fFUbk8Cw4pNgfw5-ruTqdMqf9G07r0sebWbS_ipEkNKNdAFASPS&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c1O6x000B5c1T1SQS200zAOlD4O6x0Y&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 02:24:58 GMT +Content-Length: 82 + +{"session_key":"80Fj4RmmyInufugfV68vBg==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T10:56:49.362275+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_D3Vuivhe9ISLYfI9LhCA9REMjcOS3L3L8Gzhsr21s_re-AfmaxmAoHFqlYNdzNnRz9x_TNgrEJfynQUrT3fFUbk8Cw4pNgfw5-ruTqdMqf9G07r0sebWbS_ipEkNKNdAFASPS&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b16kSFa1OfRmI0ttFIa18E1qq36kSF4&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 02:56:23 GMT +Content-Length: 82 + +{"session_key":"80I9ijZ4+2u71dpNupQFlw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T11:53:15.452022+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_l-ttB6rDh5ppn3yomOa7tsARq4SOmtoGZGB_eFr2DyPRw8wsQXj1u99T6WVg4DmUAyiON6SjgDoMP8m34DJsxfl857D-cROAbGnzvCz6reCdXgQ0JYzJv0tgkawNYNcACAXIX&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0f1SXGkl2ahcme46xXml2GEsi03SXGkd&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 03:52:49 GMT +Content-Length: 82 + +{"session_key":"bz3aDMmxsfNKHU/ZJ0vCFA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T11:56:08.916293+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_l-ttB6rDh5ppn3yomOa7tsARq4SOmtoGZGB_eFr2DyPRw8wsQXj1u99T6WVg4DmUAyiON6SjgDoMP8m34DJsxfl857D-cROAbGnzvCz6reCdXgQ0JYzJv0tgkawNYNcACAXIX&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b1RDC000PD72T115h10090xyG1RDC06&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 03:55:42 GMT +Content-Length: 82 + +{"session_key":"NTNyLheW63pYZmwWh15qnw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T11:57:20.813009+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_l-ttB6rDh5ppn3yomOa7tsARq4SOmtoGZGB_eFr2DyPRw8wsQXj1u99T6WVg4DmUAyiON6SjgDoMP8m34DJsxfl857D-cROAbGnzvCz6reCdXgQ0JYzJv0tgkawNYNcACAXIX&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0f1SXEFa1UjZlI09PyGa1Axgxk0SXEFG&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 03:56:54 GMT +Content-Length: 82 + +{"session_key":"PG7NCGoCz+WA/GQYg/wsnw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T12:04:19.388253+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_l-ttB6rDh5ppn3yomOa7tsARq4SOmtoGZGB_eFr2DyPRw8wsQXj1u99T6WVg4DmUAyiON6SjgDoMP8m34DJsxfl857D-cROAbGnzvCz6reCdXgQ0JYzJv0tgkawNYNcACAXIX&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e1dloFa1d5gmI0kmcJa1nlHCt0dloFI&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 04:03:53 GMT +Content-Length: 82 + +{"session_key":"hl2dIFjw+ZUWjDY2ruKYGg==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T12:05:07.795408+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_l-ttB6rDh5ppn3yomOa7tsARq4SOmtoGZGB_eFr2DyPRw8wsQXj1u99T6WVg4DmUAyiON6SjgDoMP8m34DJsxfl857D-cROAbGnzvCz6reCdXgQ0JYzJv0tgkawNYNcACAXIX&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c1SPwll2zNsme4ahmml2C8da44SPwlt&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 04:04:41 GMT +Content-Length: 82 + +{"session_key":"MFNP+j0suNbj/4rhB0nMtw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T12:05:44.059269+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_l-ttB6rDh5ppn3yomOa7tsARq4SOmtoGZGB_eFr2DyPRw8wsQXj1u99T6WVg4DmUAyiON6SjgDoMP8m34DJsxfl857D-cROAbGnzvCz6reCdXgQ0JYzJv0tgkawNYNcACAXIX&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c1HFLGa1C55nI0W42Ja1lRZ573HFLGL&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 04:05:18 GMT +Content-Length: 82 + +{"session_key":"R/AHxzA7lx2jdjnp+Fms6A==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T12:05:55.795033+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_l-ttB6rDh5ppn3yomOa7tsARq4SOmtoGZGB_eFr2DyPRw8wsQXj1u99T6WVg4DmUAyiON6SjgDoMP8m34DJsxfl857D-cROAbGnzvCz6reCdXgQ0JYzJv0tgkawNYNcACAXIX&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a1re71w3KKYH33f4b0w3dwj500re71Z&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 04:05:29 GMT +Content-Length: 82 + +{"session_key":"R/AHxzA7lx2jdjnp+Fms6A==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T12:14:45.241546+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_l-ttB6rDh5ppn3yomOa7tsARq4SOmtoGZGB_eFr2DyPRw8wsQXj1u99T6WVg4DmUAyiON6SjgDoMP8m34DJsxfl857D-cROAbGnzvCz6reCdXgQ0JYzJv0tgkawNYNcACAXIX&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a1OdMGa1sn7nI0qXMHa1WcKoF1OdMG9&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 04:14:19 GMT +Content-Length: 82 + +{"session_key":"llCrLDFSPameeiRGqLOT7w==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T13:43:58.358573+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_l-ttB6rDh5ppn3yomOa7tsARq4SOmtoGZGB_eFr2DyPRw8wsQXj1u99T6WVg4DmUAyiON6SjgDoMP8m34DJsxfl857D-cROAbGnzvCz6reCdXgQ0JYzJv0tgkawNYNcACAXIX&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e1UpuFa1dGbmI0TfqIa1Y8q822UpuFq&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 05:43:32 GMT +Content-Length: 82 + +{"session_key":"ZOvdyocGuNB0CdyWkq9Fsw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T14:00:45.065170+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_UrWcs-xZkefEU2mlH2MI9bfFwDAAnnIU91jer4u_JV9OvsRk-sP3g5l5Gl9lu4vE3eYn-Wt-nVzAIbDwUsuHhSGT8mmaSWkQ0QuBcZrVrJdMHfQG5px97wJoVnIIGWcAHALJZ&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e1ReK000XM22T1qjQ100zGfHs3ReK0c&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 06:00:19 GMT +Content-Length: 82 + +{"session_key":"TR3hVNBH5KKCIyAqD2RkFg==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T14:19:09.424581+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_UrWcs-xZkefEU2mlH2MI9bfFwDAAnnIU91jer4u_JV9OvsRk-sP3g5l5Gl9lu4vE3eYn-Wt-nVzAIbDwUsuHhSGT8mmaSWkQ0QuBcZrVrJdMHfQG5px97wJoVnIIGWcAHALJZ&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a1C9uml2GeKne4PjMnl2E5w451C9umn&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 06:18:43 GMT +Content-Length: 82 + +{"session_key":"lK+ymNdBzFXyR/9HbitrTQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T15:12:11.396716+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_UrWcs-xZkefEU2mlH2MI9bfFwDAAnnIU91jer4u_JV9OvsRk-sP3g5l5Gl9lu4vE3eYn-Wt-nVzAIbDwUsuHhSGT8mmaSWkQ0QuBcZrVrJdMHfQG5px97wJoVnIIGWcAHALJZ&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0f1qoc0w3yEgH333Sn3w3Y0d5V3qoc0q&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 07:11:45 GMT +Content-Length: 82 + +{"session_key":"wkW1/3P75s2DoA77G8pzrg==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T15:15:02.562439+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_UrWcs-xZkefEU2mlH2MI9bfFwDAAnnIU91jer4u_JV9OvsRk-sP3g5l5Gl9lu4vE3eYn-Wt-nVzAIbDwUsuHhSGT8mmaSWkQ0QuBcZrVrJdMHfQG5px97wJoVnIIGWcAHALJZ&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a1OsZll2zhFne4LOmnl2gC2bg3OsZl5&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 07:14:36 GMT +Content-Length: 82 + +{"session_key":"CXty1Y/544OqBu3cASd5wg==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T15:38:05.165200+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_UrWcs-xZkefEU2mlH2MI9bfFwDAAnnIU91jer4u_JV9OvsRk-sP3g5l5Gl9lu4vE3eYn-Wt-nVzAIbDwUsuHhSGT8mmaSWkQ0QuBcZrVrJdMHfQG5px97wJoVnIIGWcAHALJZ&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e1PaQ000Rur1T1Ng0300uD5Hs3PaQ0w&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 07:37:39 GMT +Content-Length: 82 + +{"session_key":"rxq+wGE4YHvb/VdLgVnGxA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T15:39:23.618524+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_UrWcs-xZkefEU2mlH2MI9bfFwDAAnnIU91jer4u_JV9OvsRk-sP3g5l5Gl9lu4vE3eYn-Wt-nVzAIbDwUsuHhSGT8mmaSWkQ0QuBcZrVrJdMHfQG5px97wJoVnIIGWcAHALJZ&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0f1k9M0w3tJIG330Kz0w3jrj944k9M0D&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 07:38:57 GMT +Content-Length: 82 + +{"session_key":"Q4Jqw5AF8gvlYTJWy7jpLQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T15:40:27.098284+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_UrWcs-xZkefEU2mlH2MI9bfFwDAAnnIU91jer4u_JV9OvsRk-sP3g5l5Gl9lu4vE3eYn-Wt-nVzAIbDwUsuHhSGT8mmaSWkQ0QuBcZrVrJdMHfQG5px97wJoVnIIGWcAHALJZ&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d1kFqGa16stnI0emOIa1TbcFH1kFqGg&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 07:40:01 GMT +Content-Length: 82 + +{"session_key":"M1aN0FGCzkuzUMRjwti72A==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T17:20:24.186333+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_VYGkGVfrjSXIckwDQ6h7DOP9MwXsxRsU7kHdJd5h8hP3roLBCtDA4VeXXxku17t2Nps8sMz-E_4vaYbNseBl6oRz78tMULBKvfKFuYP_ootAVcVjknxNyyUVXSEVDNcAIALTD&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e11zqFa1aMrmI0fxHGa1mNRMr11zqF1&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 09:19:58 GMT +Content-Length: 82 + +{"session_key":"GpdDm7l6OWER9TaGkq6XGA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T17:36:12.927090+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_VYGkGVfrjSXIckwDQ6h7DOP9MwXsxRsU7kHdJd5h8hP3roLBCtDA4VeXXxku17t2Nps8sMz-E_4vaYbNseBl6oRz78tMULBKvfKFuYP_ootAVcVjknxNyyUVXSEVDNcAIALTD&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c1WIxGa1IIynI0sY5Ga1JixdH0WIxGf&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 09:35:46 GMT +Content-Length: 82 + +{"session_key":"2ybvE/uEQOugdSad3tNxPA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T17:37:54.633027+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_VYGkGVfrjSXIckwDQ6h7DOP9MwXsxRsU7kHdJd5h8hP3roLBCtDA4VeXXxku17t2Nps8sMz-E_4vaYbNseBl6oRz78tMULBKvfKFuYP_ootAVcVjknxNyyUVXSEVDNcAIALTD&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a1LqG000xNd1T1Fc2000cI9ny3LqG0m&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 09:37:28 GMT +Content-Length: 82 + +{"session_key":"+9P4GjibjXA9vuRgz9bhwA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T17:41:49.411130+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_VYGkGVfrjSXIckwDQ6h7DOP9MwXsxRsU7kHdJd5h8hP3roLBCtDA4VeXXxku17t2Nps8sMz-E_4vaYbNseBl6oRz78tMULBKvfKFuYP_ootAVcVjknxNyyUVXSEVDNcAIALTD&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0f1ednHa16UnnI0atZHa19vgcR2ednHf&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 09:41:23 GMT +Content-Length: 82 + +{"session_key":"1ROedmYSLIaVbHGJ8ViQmA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T17:47:00.288127+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_VYGkGVfrjSXIckwDQ6h7DOP9MwXsxRsU7kHdJd5h8hP3roLBCtDA4VeXXxku17t2Nps8sMz-E_4vaYbNseBl6oRz78tMULBKvfKFuYP_ootAVcVjknxNyyUVXSEVDNcAIALTD&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c1DeJFa1YBPmI0v05Ja10GIZ02DeJF9&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 09:46:34 GMT +Content-Length: 82 + +{"session_key":"N83HICg2HQcB5dwZGHaaWw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T17:54:36.585443+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_VYGkGVfrjSXIckwDQ6h7DOP9MwXsxRsU7kHdJd5h8hP3roLBCtDA4VeXXxku17t2Nps8sMz-E_4vaYbNseBl6oRz78tMULBKvfKFuYP_ootAVcVjknxNyyUVXSEVDNcAIALTD&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e1X5Bll2byeme4NLull2sXsz74X5Blc&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 09:54:10 GMT +Content-Length: 82 + +{"session_key":"2a/2+I1WmnEnKzbAPSdYTw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T17:56:04.723292+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_VYGkGVfrjSXIckwDQ6h7DOP9MwXsxRsU7kHdJd5h8hP3roLBCtDA4VeXXxku17t2Nps8sMz-E_4vaYbNseBl6oRz78tMULBKvfKFuYP_ootAVcVjknxNyyUVXSEVDNcAIALTD&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c1zh9ml2hVSne4betml2c5jPj1zh9mz&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 09:55:38 GMT +Content-Length: 82 + +{"session_key":"MaLGjH9fEynjm5lA17SMhw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T17:57:33.171450+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_VYGkGVfrjSXIckwDQ6h7DOP9MwXsxRsU7kHdJd5h8hP3roLBCtDA4VeXXxku17t2Nps8sMz-E_4vaYbNseBl6oRz78tMULBKvfKFuYP_ootAVcVjknxNyyUVXSEVDNcAIALTD&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b1tPsFa17XzmI0jXQHa1GFXF62tPsF7&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 09:57:07 GMT +Content-Length: 82 + +{"session_key":"qCCaokvNYu/A3AjvzzY/Fw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T17:57:44.120443+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_VYGkGVfrjSXIckwDQ6h7DOP9MwXsxRsU7kHdJd5h8hP3roLBCtDA4VeXXxku17t2Nps8sMz-E_4vaYbNseBl6oRz78tMULBKvfKFuYP_ootAVcVjknxNyyUVXSEVDNcAIALTD&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a1j5QGa1mbXnI0UtMIa1MTmWu3j5QGt&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 09:57:18 GMT +Content-Length: 82 + +{"session_key":"qCCaokvNYu/A3AjvzzY/Fw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T18:12:28.886151+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_VYGkGVfrjSXIckwDQ6h7DOP9MwXsxRsU7kHdJd5h8hP3roLBCtDA4VeXXxku17t2Nps8sMz-E_4vaYbNseBl6oRz78tMULBKvfKFuYP_ootAVcVjknxNyyUVXSEVDNcAIALTD&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d12uV0w3iXTG33y6R1w31q5FR32uV04&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 10:12:02 GMT +Content-Length: 82 + +{"session_key":"LI+ivbhphc1sb3MH6k71AQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T18:14:18.706967+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_VYGkGVfrjSXIckwDQ6h7DOP9MwXsxRsU7kHdJd5h8hP3roLBCtDA4VeXXxku17t2Nps8sMz-E_4vaYbNseBl6oRz78tMULBKvfKFuYP_ootAVcVjknxNyyUVXSEVDNcAIALTD&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d1crMIa1o7RpI0pRjJa1BeLo93crMID&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 10:13:52 GMT +Content-Length: 82 + +{"session_key":"D2sMqIkUE1zqVxiLsVu/Rw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T18:31:44.842547+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_VYGkGVfrjSXIckwDQ6h7DOP9MwXsxRsU7kHdJd5h8hP3roLBCtDA4VeXXxku17t2Nps8sMz-E_4vaYbNseBl6oRz78tMULBKvfKFuYP_ootAVcVjknxNyyUVXSEVDNcAIALTD&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d1nRy100bKd3T1JLk0008h4Xp2nRy18&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 10:31:18 GMT +Content-Length: 82 + +{"session_key":"D+ORfbTDGE3JbkxCoo69OA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T18:31:47.477804+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_VYGkGVfrjSXIckwDQ6h7DOP9MwXsxRsU7kHdJd5h8hP3roLBCtDA4VeXXxku17t2Nps8sMz-E_4vaYbNseBl6oRz78tMULBKvfKFuYP_ootAVcVjknxNyyUVXSEVDNcAIALTD&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a1ujmll2zZUle4h9mll2BLbBD2ujmld&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 10:31:21 GMT +Content-Length: 82 + +{"session_key":"D+ORfbTDGE3JbkxCoo69OA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T18:31:49.601166+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_VYGkGVfrjSXIckwDQ6h7DOP9MwXsxRsU7kHdJd5h8hP3roLBCtDA4VeXXxku17t2Nps8sMz-E_4vaYbNseBl6oRz78tMULBKvfKFuYP_ootAVcVjknxNyyUVXSEVDNcAIALTD&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c16zo0w33QqH33raXZv3Ta0cA26zo0l&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 10:31:23 GMT +Content-Length: 82 + +{"session_key":"D+ORfbTDGE3JbkxCoo69OA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T18:31:55.919003+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_VYGkGVfrjSXIckwDQ6h7DOP9MwXsxRsU7kHdJd5h8hP3roLBCtDA4VeXXxku17t2Nps8sMz-E_4vaYbNseBl6oRz78tMULBKvfKFuYP_ootAVcVjknxNyyUVXSEVDNcAIALTD&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0f14Sy100hOd3T1T63000bgmZm34Sy1s&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 10:31:29 GMT +Content-Length: 82 + +{"session_key":"D+ORfbTDGE3JbkxCoo69OA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T18:36:12.786851+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_VYGkGVfrjSXIckwDQ6h7DOP9MwXsxRsU7kHdJd5h8hP3roLBCtDA4VeXXxku17t2Nps8sMz-E_4vaYbNseBl6oRz78tMULBKvfKFuYP_ootAVcVjknxNyyUVXSEVDNcAIALTD&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0f1ibQ100WEn2T1cLt100zQvIT0ibQ1r&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 10:35:46 GMT +Content-Length: 82 + +{"session_key":"yslH3hYYsFd7mLQVMRjDpw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T20:12:48.102307+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_sPRVsvDU8Bp2ZsoBzP8dOdTWsbKEY_RNsTnDHi_bxVaY84lpaDropC4AoKPsMIaMaykAda8fzyjPipKX4VNh21Njj-gv8OSf8beSGtfFEJ-xI5jClbpUwvx408MKSUdAGADLN&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b1kwJll2jhvne4FCyml26FAYV0kwJlk&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 12:12:21 GMT +Content-Length: 82 + +{"session_key":"yslH3hYYsFd7mLQVMRjDpw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T20:54:29.693749+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_sPRVsvDU8Bp2ZsoBzP8dOdTWsbKEY_RNsTnDHi_bxVaY84lpaDropC4AoKPsMIaMaykAda8fzyjPipKX4VNh21Njj-gv8OSf8beSGtfFEJ-xI5jClbpUwvx408MKSUdAGADLN&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b1oFUFa1heWlI0jzxFa1iESGH1oFUFL&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 12:54:03 GMT +Content-Length: 82 + +{"session_key":"JvldiDDIeQ6ycKgWzVVZKQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-18T22:28:46.464260+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_1NBkBboeo3rXjRZDvYYcynt9RdNvuxCJA2nyrfZIMZSzv_BXoIK_FfXOadgTOBl16VhkkVuZVotLpanUJFNlWN015QRSKTfnNq0Yq_wsDcuL7P2aE2cmRqlb8K4TNQjAIAICF&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d1JmJFa1oJUmI0CM4Ha1ZkLDP1JmJF3&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 18 Oct 2024 14:28:20 GMT +Content-Length: 82 + +{"session_key":"R2uw/NX7rYmS0eErS6jhvQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-19T10:26:40.089539+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_EZ4-PL07rtNQuYLVs1_vg9SEi0w00ta0r_56odJqn9-7SO2bGu5T0xYuHTh3Lw_PMrrmnuci4tUGl39VkzDq2azNt6FJsQrSL3OdQGAjku9UiE2xFg4e6NpMDQIIUMgAHAAME&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a10Q71000K33T1tbB200LaKbb20Q71j&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 19 Oct 2024 02:26:13 GMT +Content-Length: 82 + +{"session_key":"ZuYFFJxfDDGiAb/BVz9k/w==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-19T11:43:10.450892+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_EZ4-PL07rtNQuYLVs1_vg9SEi0w00ta0r_56odJqn9-7SO2bGu5T0xYuHTh3Lw_PMrrmnuci4tUGl39VkzDq2azNt6FJsQrSL3OdQGAjku9UiE2xFg4e6NpMDQIIUMgAHAAME&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c19mn000icO1T1Blg300mq5Nr19mn0d&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 19 Oct 2024 03:42:44 GMT +Content-Length: 82 + +{"session_key":"t5oiS8Gb4XvYu4m9dXb58w==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-19T11:52:06.720990+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_EZ4-PL07rtNQuYLVs1_vg9SEi0w00ta0r_56odJqn9-7SO2bGu5T0xYuHTh3Lw_PMrrmnuci4tUGl39VkzDq2azNt6FJsQrSL3OdQGAjku9UiE2xFg4e6NpMDQIIUMgAHAAME&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d100W000ejd1T1ND4100PgTIY100W0f&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 19 Oct 2024 03:51:40 GMT +Content-Length: 82 + +{"session_key":"82ivRJv3JVyG+5jh5VXayQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-19T14:18:08.651507+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_AqJ98r2VXGlE-1qcmltxzWozhM7sQKwlCoChmIVKsaa4tUOmvPTGD1r1kj80WPyGV3Qrme4ZV4SdSsDgdaGTUd-xvxJ8l5NwBydngeJ3XhiboeeZTyJcfuRmDI8KQNbAHAVBY&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e163U100PvG2T1HEd000pF6ql263U1o&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 19 Oct 2024 06:17:42 GMT +Content-Length: 82 + +{"session_key":"lMKYKa/5Z0Y0Vd69hi4ljA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-19T14:19:16.480038+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_AqJ98r2VXGlE-1qcmltxzWozhM7sQKwlCoChmIVKsaa4tUOmvPTGD1r1kj80WPyGV3Qrme4ZV4SdSsDgdaGTUd-xvxJ8l5NwBydngeJ3XhiboeeZTyJcfuRmDI8KQNbAHAVBY&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b1n4D100YvX2T1N6a400qbY2u1n4D1Z&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 19 Oct 2024 06:18:50 GMT +Content-Length: 82 + +{"session_key":"lMKYKa/5Z0Y0Vd69hi4ljA==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-19T23:26:35.746442+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_3uPldmR4zqj6EolqpfmCmRjzesZOXyHevY1X0s8fsPKC_d71krAzOsCMoRmCMpSUV00IiwgYc6U0zAHOLOVyYu8ZJRryIJGnU_YQI8F784u5d1GIXFtl-cE8tU8UWLcAAAJLA&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b1FZa0w39PjH33Q6k1w38jfA23FZa0w&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 19 Oct 2024 15:26:09 GMT +Content-Length: 82 + +{"session_key":"8XvPY+O0Rix3X1GVtVFJ6g==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-19T23:41:46.336030+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_3uPldmR4zqj6EolqpfmCmRjzesZOXyHevY1X0s8fsPKC_d71krAzOsCMoRmCMpSUV00IiwgYc6U0zAHOLOVyYu8ZJRryIJGnU_YQI8F784u5d1GIXFtl-cE8tU8UWLcAAAJLA&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0f16Ys0w3h82H33onQ1w374kzm26Ys0R&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 19 Oct 2024 15:41:19 GMT +Content-Length: 82 + +{"session_key":"aiN4Ymb5gNjgcrfOMDQRmw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-20T15:48:45.791372+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_T81EhMMY6cl6VoVsNtv1FQrbdmyd70Oiie35qi0ILjfpHrvoi0lJr71sZgAw8SG5LJtLBcn54ziMia7ne2s1uUTmKHrU71mfkKcrBcJ23XhxtSav3HOi67OTpNcAJMcADAVEH&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0d1wn4Ha182BmI0EodGa1b6oTC3wn4Ht&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 20 Oct 2024 07:48:19 GMT +Content-Length: 82 + +{"session_key":"bT9ELRk0cP5449bZw3OWuw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-20T19:58:11.992577+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_ZXVvOxavyyDN86SYo3tVspgC_cIY50lwMB3nN4BR_P2jv3G5-BmZbP3p7UM-NUEl5ycnAmbxIWUzPMHJSh96Uw24PbU_OSJ4EMb0CeMPmhOQqDK1XxT-7G_wokEKYUfAGABPL&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c1ZVTml2zYfpe4ELOol2TlFP72ZVTm2&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 20 Oct 2024 11:57:45 GMT +Content-Length: 82 + +{"session_key":"bT9ELRk0cP5449bZw3OWuw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-21T13:44:06.900865+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_oqXBOb8PFUfphfc3GoqrGl7keSYMbcIpFKe6GLGqYHN--phFql5amrn6wE46MdF2blOCoe7Qt3HeOfJDHOjn5KA05HAqinChfgDY_EdUY0zf3WT9CWAhtfn3M-IRQZbAFAUTC&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a16utll21gFne4m6Dll22bGAc06utlQ&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 21 Oct 2024 05:43:39 GMT +Content-Length: 82 + +{"session_key":"vHrjJPkWPV9ja+ftbdGPEw==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-21T14:48:24.651991+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_oqXBOb8PFUfphfc3GoqrGl7keSYMbcIpFKe6GLGqYHN--phFql5amrn6wE46MdF2blOCoe7Qt3HeOfJDHOjn5KA05HAqinChfgDY_EdUY0zf3WT9CWAhtfn3M-IRQZbAFAUTC&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a15Im000O1q2T1VrK000t7A3f35Im0c&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 21 Oct 2024 06:47:57 GMT +Content-Length: 82 + +{"session_key":"fdsHWXGLtysQ8ltJiOVQ4A==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-21T14:52:39.856162+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_oqXBOb8PFUfphfc3GoqrGl7keSYMbcIpFKe6GLGqYHN--phFql5amrn6wE46MdF2blOCoe7Qt3HeOfJDHOjn5KA05HAqinChfgDY_EdUY0zf3WT9CWAhtfn3M-IRQZbAFAUTC&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c1GgGFa1A76nI0cPSHa1iFARF2GgGFP&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 21 Oct 2024 06:52:12 GMT +Content-Length: 82 + +{"session_key":"/GYYeAaYLZ/kjPEaTvdLTg==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-21T15:21:00.125598+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_oqXBOb8PFUfphfc3GoqrGl7keSYMbcIpFKe6GLGqYHN--phFql5amrn6wE46MdF2blOCoe7Qt3HeOfJDHOjn5KA05HAqinChfgDY_EdUY0zf3WT9CWAhtfn3M-IRQZbAFAUTC&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0f1Ye5Ha1KFWlI0gbXIa1Oeea90Ye5HY&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 21 Oct 2024 07:20:33 GMT +Content-Length: 82 + +{"session_key":"77m2PBv0WPQCFez8a5xP5Q==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-21T16:10:17.581983+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_SFuOshQ4b_8hFm8ZC1DcqvlszQyvwAQj_eJIOi9oPacLA5QdQzxO1_kEfuDbWmHfSS-z3T5-cIJ_sclBI-MkeIY9Ar_PXbAMN1IiRls9iYPP9ujeGjDSzju876EUROgACADAX&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c19Bn0w3KQwI33kMV2w3zHni549Bn0j&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 21 Oct 2024 08:09:50 GMT +Content-Length: 82 + +{"session_key":"NWXrl6lWYKXxIrOKrfYcug==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-21T17:55:41.713547+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_SFuOshQ4b_8hFm8ZC1DcqvlszQyvwAQj_eJIOi9oPacLA5QdQzxO1_kEfuDbWmHfSS-z3T5-cIJ_sclBI-MkeIY9Ar_PXbAMN1IiRls9iYPP9ujeGjDSzju876EUROgACADAX&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0f18Mrll26Iane4Z3Dml27oCgO38Mrlh&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 21 Oct 2024 09:55:14 GMT +Content-Length: 82 + +{"session_key":"PLCYxHVw3OqLjN3MwCZ1eA==","openid":"o-6OK7WTHiq3znCHwG2WsEWf699I"} +-------- +NULL +[2024-10-21T17:55:50.187418+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_SFuOshQ4b_8hFm8ZC1DcqvlszQyvwAQj_eJIOi9oPacLA5QdQzxO1_kEfuDbWmHfSS-z3T5-cIJ_sclBI-MkeIY9Ar_PXbAMN1IiRls9iYPP9ujeGjDSzju876EUROgACADAX&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b1b5L0w3ZpRH33S6i4w3wZaKf2b5L0C&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 21 Oct 2024 09:55:23 GMT +Content-Length: 82 + +{"session_key":"PLCYxHVw3OqLjN3MwCZ1eA==","openid":"o-6OK7WTHiq3znCHwG2WsEWf699I"} +-------- +NULL +[2024-10-21T18:01:51.868266+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_SFuOshQ4b_8hFm8ZC1DcqvlszQyvwAQj_eJIOi9oPacLA5QdQzxO1_kEfuDbWmHfSS-z3T5-cIJ_sclBI-MkeIY9Ar_PXbAMN1IiRls9iYPP9ujeGjDSzju876EUROgACADAX&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0a1sB6100GCI1T1ByU000nBP9U1sB61G&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 21 Oct 2024 10:01:24 GMT +Content-Length: 82 + +{"session_key":"sOheYS3Sl61NoNzocv0BCQ==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-22T18:36:00.403383+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_wtXmjLgr-Em-Xj2dX07FhVTUjWdf0SLAWVrCU11wybouf5voJy4xVpY4VOur9Xz5ZS4MzmlCBdA129l2ccnWYfjnD6tOKCviybzbhNEWQuyLovmevCIOiPl-GVMRGFjADAMBA&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0b1RMwll29EUme4yW6ll2ZOPwa3RMwla&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 22 Oct 2024 10:35:33 GMT +Content-Length: 82 + +{"session_key":"hyJEhoMw1eEAS1PLaOW03w==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-22T18:40:30.548344+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_wtXmjLgr-Em-Xj2dX07FhVTUjWdf0SLAWVrCU11wybouf5voJy4xVpY4VOur9Xz5ZS4MzmlCBdA129l2ccnWYfjnD6tOKCviybzbhNEWQuyLovmevCIOiPl-GVMRGFjADAMBA&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0c1ojz0w3UG2I33Rg64w3gTdeM1ojz09&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 22 Oct 2024 10:40:03 GMT +Content-Length: 82 + +{"session_key":"AE9ftZkiKKZsj93fv7W1/w==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-22T18:44:14.092162+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_wtXmjLgr-Em-Xj2dX07FhVTUjWdf0SLAWVrCU11wybouf5voJy4xVpY4VOur9Xz5ZS4MzmlCBdA129l2ccnWYfjnD6tOKCviybzbhNEWQuyLovmevCIOiPl-GVMRGFjADAMBA&appid=wx191a50f881264dbc&secret=8a95ad46c143bbc1993b3c5e587dbada&js_code=0e1Rx50005Kh3T1UiT100lvAbl4Rx50y&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 22 Oct 2024 10:43:46 GMT +Content-Length: 82 + +{"session_key":"AE9ftZkiKKZsj93fv7W1/w==","openid":"o-6OK7Q0VhRjfff3fVGxHsdd1Vk0"} +-------- +NULL +[2024-10-24T13:21:27.159247+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_PWwL1js5v7fmrLjoh3x9nIvCvkx7acfgjO72VzxyVD073M1jk_McT5at63UbC3JJ_4LfS6mZTOifLBmDPduOHkcXKBE1PLgyLF7R6sOc0FHzobILRVPD545EAP4BPCaAJAMAN&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1t4p000dvv3T1eVo300c2eBy1t4p0D&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 24 Oct 2024 05:20:59 GMT +Content-Length: 82 + +{"session_key":"lh2FEc8v7fOuunXoBP24Og==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-24T13:24:40.575005+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_PWwL1js5v7fmrLjoh3x9nIvCvkx7acfgjO72VzxyVD073M1jk_McT5at63UbC3JJ_4LfS6mZTOifLBmDPduOHkcXKBE1PLgyLF7R6sOc0FHzobILRVPD545EAP4BPCaAJAMAN&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1a740w38W5J33r8a1w3TBMAc0a740S&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 24 Oct 2024 05:24:12 GMT +Content-Length: 82 + +{"session_key":"effHMbCCT7YWWk9Uy4XE2A==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-24T13:26:47.221308+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_PWwL1js5v7fmrLjoh3x9nIvCvkx7acfgjO72VzxyVD073M1jk_McT5at63UbC3JJ_4LfS6mZTOifLBmDPduOHkcXKBE1PLgyLF7R6sOc0FHzobILRVPD545EAP4BPCaAJAMAN&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1bKZFa11K1pI0O5EIa1yOr6c4bKZF4&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 24 Oct 2024 05:26:19 GMT +Content-Length: 82 + +{"session_key":"43AKG9nW+wEVNBjHMd2usw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-24T13:28:12.142767+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_PWwL1js5v7fmrLjoh3x9nIvCvkx7acfgjO72VzxyVD073M1jk_McT5at63UbC3JJ_4LfS6mZTOifLBmDPduOHkcXKBE1PLgyLF7R6sOc0FHzobILRVPD545EAP4BPCaAJAMAN&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1BVxGa1QEzpI05LUHa1iKeNm0BVxG6&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 24 Oct 2024 05:27:44 GMT +Content-Length: 82 + +{"session_key":"ZmDqko4zsKTWp0jYzFPhUQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-24T13:29:29.698964+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_PWwL1js5v7fmrLjoh3x9nIvCvkx7acfgjO72VzxyVD073M1jk_McT5at63UbC3JJ_4LfS6mZTOifLBmDPduOHkcXKBE1PLgyLF7R6sOc0FHzobILRVPD545EAP4BPCaAJAMAN&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1Syp000NJv3T13S2200Pe0m71Syp0q&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 24 Oct 2024 05:29:01 GMT +Content-Length: 82 + +{"session_key":"WlB0EJnbsASMaVEuARqdaA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-24T13:33:30.596192+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_PWwL1js5v7fmrLjoh3x9nIvCvkx7acfgjO72VzxyVD073M1jk_McT5at63UbC3JJ_4LfS6mZTOifLBmDPduOHkcXKBE1PLgyLF7R6sOc0FHzobILRVPD545EAP4BPCaAJAMAN&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1A90Ga1sYWoI00kpGa1LaYx01A90G-&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 24 Oct 2024 05:33:02 GMT +Content-Length: 82 + +{"session_key":"2LgILYHbr00Z4EmKPAL2rA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-24T13:38:31.818747+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_PWwL1js5v7fmrLjoh3x9nIvCvkx7acfgjO72VzxyVD073M1jk_McT5at63UbC3JJ_4LfS6mZTOifLBmDPduOHkcXKBE1PLgyLF7R6sOc0FHzobILRVPD545EAP4BPCaAJAMAN&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1Z39000StH3T1oem200Mv20s4Z390l&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 24 Oct 2024 05:38:03 GMT +Content-Length: 82 + +{"session_key":"CkbIYionrIh4eMfC/LDXnQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-24T13:39:36.228445+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_PWwL1js5v7fmrLjoh3x9nIvCvkx7acfgjO72VzxyVD073M1jk_McT5at63UbC3JJ_4LfS6mZTOifLBmDPduOHkcXKBE1PLgyLF7R6sOc0FHzobILRVPD545EAP4BPCaAJAMAN&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1D4m0w3sGJI338Je4w3G0zyr3D4m0d&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 24 Oct 2024 05:39:08 GMT +Content-Length: 82 + +{"session_key":"lyqvZryr/flo4RwAIDykCA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-24T13:47:06.104124+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_PWwL1js5v7fmrLjoh3x9nIvCvkx7acfgjO72VzxyVD073M1jk_McT5at63UbC3JJ_4LfS6mZTOifLBmDPduOHkcXKBE1PLgyLF7R6sOc0FHzobILRVPD545EAP4BPCaAJAMAN&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1vhkll22TRoe4iGVol2Szz3z2vhklG&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 24 Oct 2024 05:46:38 GMT +Content-Length: 82 + +{"session_key":"mCWhzX7NGI9TDcKhu4sanQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-24T13:53:59.191777+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_PWwL1js5v7fmrLjoh3x9nIvCvkx7acfgjO72VzxyVD073M1jk_McT5at63UbC3JJ_4LfS6mZTOifLBmDPduOHkcXKBE1PLgyLF7R6sOc0FHzobILRVPD545EAP4BPCaAJAMAN&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1WitFa1ypqoI0uTFIa1B3XF62WitFz&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 24 Oct 2024 05:53:31 GMT +Content-Length: 82 + +{"session_key":"zeHApYH7KLm8QcAe4l1oNg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-24T13:54:49.589646+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_PWwL1js5v7fmrLjoh3x9nIvCvkx7acfgjO72VzxyVD073M1jk_McT5at63UbC3JJ_4LfS6mZTOifLBmDPduOHkcXKBE1PLgyLF7R6sOc0FHzobILRVPD545EAP4BPCaAJAMAN&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1B3a000MFJ3T1WAA100I55kB2B3a0N&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 24 Oct 2024 05:54:21 GMT +Content-Length: 82 + +{"session_key":"OM6LmbKrBoOLa7ko5ILk3w==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-24T15:28:40.775025+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_z0Z5iFg1I0i0hHjbRyfwpnxUoOyZKG70f-LxMrW3vRLqjL3xODpl6e_2QeBP9litDm7AMXWGvnwTaiRM1_MZfCJfTnhP75LwPUIkE5K9USYJZWABg0NJe63FH_YRAKeAGANWG&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1U7QFa1qk3oI0NmBFa1fsPu81U7QFw&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 24 Oct 2024 07:28:12 GMT +Content-Length: 82 + +{"session_key":"Bkc/XEBZ0G7KdYw1WQpRSQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-24T15:37:14.442866+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_z0Z5iFg1I0i0hHjbRyfwpnxUoOyZKG70f-LxMrW3vRLqjL3xODpl6e_2QeBP9litDm7AMXWGvnwTaiRM1_MZfCJfTnhP75LwPUIkE5K9USYJZWABg0NJe63FH_YRAKeAGANWG&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1Mlx000cGl3T1tpb000dnV722Mlx0P&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 24 Oct 2024 07:36:46 GMT +Content-Length: 82 + +{"session_key":"Xt+0EOHUwUo2M8ZCDoxXgQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-25T09:38:42.699875+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_ar4bdkxU3VBtAlASFUTQITuoJF6w60fIiObrLHwoQBzvJB5ng1d3dpwMADfw_TZd-5Rt7KDG5dFC-rlk3jSmZgEcFcWstoTCUjKdKjHuxbIC3_ejPxmap8p-mSYWHYbAIASDU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1loOFa182ToI0isfIa1Cvwr13loOFq&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 25 Oct 2024 01:38:14 GMT +Content-Length: 82 + +{"session_key":"tM9BwvGONwoHfKmatQ69iQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-25T09:49:30.297719+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_ar4bdkxU3VBtAlASFUTQITuoJF6w60fIiObrLHwoQBzvJB5ng1d3dpwMADfw_TZd-5Rt7KDG5dFC-rlk3jSmZgEcFcWstoTCUjKdKjHuxbIC3_ejPxmap8p-mSYWHYbAIASDU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1FKv000oqc4T1PWL300SdoSA1FKv0c&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 25 Oct 2024 01:49:02 GMT +Content-Length: 82 + +{"session_key":"UO8Idt2Vrgqis2/K19VAcw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-25T17:17:07.628927+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_djDvQ5mAwUW6xxwijoPu9jcM5s4hXPkFoL1B_GBQHnQOtafWGZxaB6msQ01uXRim4yDqkGc91QpEdKW0kslmjVlRirj3jG3ZLw5liZIY01LBsl2iDxv66w9JKKgESKaAHAPDV&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1MDzll2UUYne4DNrnl2T5dSU0MDzlo&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 25 Oct 2024 09:16:39 GMT +Content-Length: 82 + +{"session_key":"AJwKprpocHKe1k/n6rSezQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-25T17:22:23.797484+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_djDvQ5mAwUW6xxwijoPu9jcM5s4hXPkFoL1B_GBQHnQOtafWGZxaB6msQ01uXRim4yDqkGc91QpEdKW0kslmjVlRirj3jG3ZLw5liZIY01LBsl2iDxv66w9JKKgESKaAHAPDV&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a19740w3uPuJ33y2D0w3k8ILV29740U&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 25 Oct 2024 09:21:55 GMT +Content-Length: 82 + +{"session_key":"osx26OQ5mRzbSpjnznWerA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-25T18:07:19.880650+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_djDvQ5mAwUW6xxwijoPu9jcM5s4hXPkFoL1B_GBQHnQOtafWGZxaB6msQ01uXRim4yDqkGc91QpEdKW0kslmjVlRirj3jG3ZLw5liZIY01LBsl2iDxv66w9JKKgESKaAHAPDV&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1Hi6200gmE4T1CKU000ivZOD0Hi62h&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 25 Oct 2024 10:06:51 GMT +Content-Length: 82 + +{"session_key":"SyIeJCOkkqXb2Q2YQyX3RQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-25T18:24:33.664786+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_djDvQ5mAwUW6xxwijoPu9jcM5s4hXPkFoL1B_GBQHnQOtafWGZxaB6msQ01uXRim4yDqkGc91QpEdKW0kslmjVlRirj3jG3ZLw5liZIY01LBsl2iDxv66w9JKKgESKaAHAPDV&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1bXo0w3GjpJ33A990w38fOMw2bXo0s&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 25 Oct 2024 10:24:05 GMT +Content-Length: 82 + +{"session_key":"SyIeJCOkkqXb2Q2YQyX3RQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-26T09:50:43.796185+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_hGHFRZL3C6jqbYmilmrrr3vCf6-h72HTH54wx-S0SmqSFpdwII9XlsO_3tRaSFcS9sjsgwzVCEnJnFEuSv_gptqDjmvKKUCwLQos2mkwV-SdkMnWxDMXeJbCq8sEKQgAGAZFL&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1fPGGa1nKnoI0gmHIa1GImcC4fPGG7&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 26 Oct 2024 01:50:15 GMT +Content-Length: 82 + +{"session_key":"+A3ySoDm3wN8rYC0wOEaeA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-26T14:07:36.345119+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_nrQE7oZhvCTPuH_PJditiuIE5nu4swotsM3ML-XJ5B7B-weq30L_TsuwZZf4aW2Yiq7rBePh7_tJnV7t_gKg3QwIc5dQZqGQxmFwZ-h_EGjJh1_x7huMo2OfWt8SSLjADAFFY&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1HA9ll2Ayrpe47Hrll2KtGz23HA9l0&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 26 Oct 2024 06:07:07 GMT +Content-Length: 82 + +{"session_key":"UZTiEdsG3WWAtCBNHaMZlw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-26T14:28:22.585790+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_nrQE7oZhvCTPuH_PJditiuIE5nu4swotsM3ML-XJ5B7B-weq30L_TsuwZZf4aW2Yiq7rBePh7_tJnV7t_gKg3QwIc5dQZqGQxmFwZ-h_EGjJh1_x7huMo2OfWt8SSLjADAFFY&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1GvAFa1dSxpI0dRbIa1kS06w3GvAFO&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 26 Oct 2024 06:27:54 GMT +Content-Length: 82 + +{"session_key":"puXAoTQZ0V5wEw3lqDgnFg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-26T14:38:32.784061+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_nrQE7oZhvCTPuH_PJditiuIE5nu4swotsM3ML-XJ5B7B-weq30L_TsuwZZf4aW2Yiq7rBePh7_tJnV7t_gKg3QwIc5dQZqGQxmFwZ-h_EGjJh1_x7huMo2OfWt8SSLjADAFFY&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1AM0000QQz4T1ZD0000SpFYk1AM00y&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 26 Oct 2024 06:38:04 GMT +Content-Length: 82 + +{"session_key":"ltWKUJds4s/v6+Qv9SzWRQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-26T15:01:53.364215+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_nrQE7oZhvCTPuH_PJditiuIE5nu4swotsM3ML-XJ5B7B-weq30L_TsuwZZf4aW2Yiq7rBePh7_tJnV7t_gKg3QwIc5dQZqGQxmFwZ-h_EGjJh1_x7huMo2OfWt8SSLjADAFFY&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1TiA000fYa5T1DmQ200RRLJT0TiA0e&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 26 Oct 2024 07:01:24 GMT +Content-Length: 82 + +{"session_key":"pO724w8gjXQOc9VK+P6lxg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-26T15:06:27.238914+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_nrQE7oZhvCTPuH_PJditiuIE5nu4swotsM3ML-XJ5B7B-weq30L_TsuwZZf4aW2Yiq7rBePh7_tJnV7t_gKg3QwIc5dQZqGQxmFwZ-h_EGjJh1_x7huMo2OfWt8SSLjADAFFY&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1O7hHa1Wq2pI04tYFa1YkoZr4O7hHA&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 26 Oct 2024 07:05:58 GMT +Content-Length: 82 + +{"session_key":"Mh9E3TGjomV7L/WGDJcnVA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-26T19:46:38.178267+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_F86z8GCZeC2b5RtoyI5Wj-pTxlf7EQjUVuKP1ypdgyJOmUHRDem_I-dG7zHQBZwc5RJ0Y90lP23WBZWfDe7Xk6T5xi4T1IWD3UeSU8l0Wyh5uXmsNxgTIGfdGM0SMIiAHAVPL&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1LUTFa1JXVpI0ZlQIa1y0h1i2LUTF7&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 26 Oct 2024 11:46:09 GMT +Content-Length: 82 + +{"session_key":"dVwBQqLO36E03PMPUfHZVw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-26T20:50:34.252038+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_F86z8GCZeC2b5RtoyI5Wj-pTxlf7EQjUVuKP1ypdgyJOmUHRDem_I-dG7zHQBZwc5RJ0Y90lP23WBZWfDe7Xk6T5xi4T1IWD3UeSU8l0Wyh5uXmsNxgTIGfdGM0SMIiAHAVPL&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1pIpFa1EwgpI0FkJGa1OPjzh1pIpFc&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 26 Oct 2024 12:50:05 GMT +Content-Length: 82 + +{"session_key":"rwf3eYuXg+IFELT941ZwWg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-26T22:25:10.326377+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_RP3oDImk4o23kNuQ8abgPqx7wEM8_1sfnrvdIj11DRbRzWSEuU37ORPiJFTJrpD-yd-91zeXAlnNuCngOHLEGhDw1Jw6OlHfaoAAt4G4_m_yewNsUWHr1nO9dgYKXMdAJAPCR&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1ZtvFa1rOcpI0okiJa1IfHkQ0ZtvFb&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 26 Oct 2024 14:24:41 GMT +Content-Length: 82 + +{"session_key":"xKwO0u2JPlfwUwozd0Hnlg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-26T22:54:35.573576+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_RP3oDImk4o23kNuQ8abgPqx7wEM8_1sfnrvdIj11DRbRzWSEuU37ORPiJFTJrpD-yd-91zeXAlnNuCngOHLEGhDw1Jw6OlHfaoAAt4G4_m_yewNsUWHr1nO9dgYKXMdAJAPCR&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1IYd000o5W4T1rSV200zPiUC3IYd0g&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 26 Oct 2024 14:54:07 GMT +Content-Length: 82 + +{"session_key":"r+8KN6TgrKpkgflRAkElVA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-27T16:05:13.006789+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_Z407Cl9DJtkUnl5lcW4Dji-YsmU4CyKriYQDLdYkNLdYbQ8hBgJJNm3DIqhRaSY7LoGQYLJLO_oHWe5E1tVLvMACp1MICYgMJCSqmUEslHuvc7FfjULJC5tStR4AEBeAJAHLI&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1GOe100fDJ3T1IPz000nkwzD2GOe1A&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 27 Oct 2024 08:04:44 GMT +Content-Length: 82 + +{"session_key":"USVS2uS2z7Wt9mpFvJYTqA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-27T16:12:53.957515+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_Z407Cl9DJtkUnl5lcW4Dji-YsmU4CyKriYQDLdYkNLdYbQ8hBgJJNm3DIqhRaSY7LoGQYLJLO_oHWe5E1tVLvMACp1MICYgMJCSqmUEslHuvc7FfjULJC5tStR4AEBeAJAHLI&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1GJ2ll2kUbpe4EDdol292HmN1GJ2ld&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 27 Oct 2024 08:12:25 GMT +Content-Length: 82 + +{"session_key":"WpBRItXINwiCzG/ZE5Jwpw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-27T16:52:46.968191+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_Z407Cl9DJtkUnl5lcW4Dji-YsmU4CyKriYQDLdYkNLdYbQ8hBgJJNm3DIqhRaSY7LoGQYLJLO_oHWe5E1tVLvMACp1MICYgMJCSqmUEslHuvc7FfjULJC5tStR4AEBeAJAHLI&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1sa5ll2d29pe4mpYnl2ly1oo1sa5lQ&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 27 Oct 2024 08:52:18 GMT +Content-Length: 82 + +{"session_key":"WQ/edh4uGwma1ui0oXjExQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-27T16:56:11.826826+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_Z407Cl9DJtkUnl5lcW4Dji-YsmU4CyKriYQDLdYkNLdYbQ8hBgJJNm3DIqhRaSY7LoGQYLJLO_oHWe5E1tVLvMACp1MICYgMJCSqmUEslHuvc7FfjULJC5tStR4AEBeAJAHLI&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1kMs000YPr5T1wbe3009ywY53kMs0-&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 27 Oct 2024 08:55:43 GMT +Content-Length: 82 + +{"session_key":"WQ/edh4uGwma1ui0oXjExQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-27T17:02:13.454306+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_Z407Cl9DJtkUnl5lcW4Dji-YsmU4CyKriYQDLdYkNLdYbQ8hBgJJNm3DIqhRaSY7LoGQYLJLO_oHWe5E1tVLvMACp1MICYgMJCSqmUEslHuvc7FfjULJC5tStR4AEBeAJAHLI&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1Nev1w3zfFJ33Fc60w344Hgp3Nev1z&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 27 Oct 2024 09:01:44 GMT +Content-Length: 82 + +{"session_key":"PFYY1N83fKKDtP7SuDaoVA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-27T17:06:00.978320+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_Z407Cl9DJtkUnl5lcW4Dji-YsmU4CyKriYQDLdYkNLdYbQ8hBgJJNm3DIqhRaSY7LoGQYLJLO_oHWe5E1tVLvMACp1MICYgMJCSqmUEslHuvc7FfjULJC5tStR4AEBeAJAHLI&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1w1nll2B2gpe4lLvol29RJON2w1nlI&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 27 Oct 2024 09:05:32 GMT +Content-Length: 82 + +{"session_key":"1t9wnjr4wDR163U6cyIGWw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-27T17:11:41.386039+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_Z407Cl9DJtkUnl5lcW4Dji-YsmU4CyKriYQDLdYkNLdYbQ8hBgJJNm3DIqhRaSY7LoGQYLJLO_oHWe5E1tVLvMACp1MICYgMJCSqmUEslHuvc7FfjULJC5tStR4AEBeAJAHLI&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1WLK000yBD4T1CuR100RuzAc0WLK0g&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 27 Oct 2024 09:11:12 GMT +Content-Length: 82 + +{"session_key":"1t9wnjr4wDR163U6cyIGWw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-27T17:24:22.832126+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_Z407Cl9DJtkUnl5lcW4Dji-YsmU4CyKriYQDLdYkNLdYbQ8hBgJJNm3DIqhRaSY7LoGQYLJLO_oHWe5E1tVLvMACp1MICYgMJCSqmUEslHuvc7FfjULJC5tStR4AEBeAJAHLI&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1wxf1w37YpJ33IH24w3hXcvf4wxf15&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 27 Oct 2024 09:23:54 GMT +Content-Length: 82 + +{"session_key":"1t9wnjr4wDR163U6cyIGWw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-27T18:06:09.541894+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_7XaZRC8a7x_zcG5pk7AqcryYvrnbRckXRvZ8bAeQUeWjClEy83C345ZgnFPNG89pUmWngyeRADNV1Tk9XUziRitdXYGpA44TKeEgiMW2jmZCoYWQ0ue3078rG9UJFNgAFATPX&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1o211w32fgK33Q3Z0w3PTpfn1o211x&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 27 Oct 2024 10:05:40 GMT +Content-Length: 82 + +{"session_key":"quRihkymkU/il0nPpbusNg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-27T18:48:16.258210+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_7XaZRC8a7x_zcG5pk7AqcryYvrnbRckXRvZ8bAeQUeWjClEy83C345ZgnFPNG89pUmWngyeRADNV1Tk9XUziRitdXYGpA44TKeEgiMW2jmZCoYWQ0ue3078rG9UJFNgAFATPX&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1Y6Ekl2vuCpe4j37ll2eEpWO2Y6EkM&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 27 Oct 2024 10:47:47 GMT +Content-Length: 82 + +{"session_key":"KhZf01MwmWWYfJJavySgxg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-27T18:48:19.654793+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_7XaZRC8a7x_zcG5pk7AqcryYvrnbRckXRvZ8bAeQUeWjClEy83C345ZgnFPNG89pUmWngyeRADNV1Tk9XUziRitdXYGpA44TKeEgiMW2jmZCoYWQ0ue3078rG9UJFNgAFATPX&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1FOkFa15BGpI0sfzHa19zJ3O0FOkFK&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 27 Oct 2024 10:47:50 GMT +Content-Length: 82 + +{"session_key":"KhZf01MwmWWYfJJavySgxg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-27T18:48:29.340174+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_7XaZRC8a7x_zcG5pk7AqcryYvrnbRckXRvZ8bAeQUeWjClEy83C345ZgnFPNG89pUmWngyeRADNV1Tk9XUziRitdXYGpA44TKeEgiMW2jmZCoYWQ0ue3078rG9UJFNgAFATPX&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1XOF100hBl3T1Eau100vr6Yk1XOF1H&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 27 Oct 2024 10:48:00 GMT +Content-Length: 82 + +{"session_key":"KhZf01MwmWWYfJJavySgxg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-27T19:21:33.347814+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_7XaZRC8a7x_zcG5pk7AqcryYvrnbRckXRvZ8bAeQUeWjClEy83C345ZgnFPNG89pUmWngyeRADNV1Tk9XUziRitdXYGpA44TKeEgiMW2jmZCoYWQ0ue3078rG9UJFNgAFATPX&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1hQmFa1piHpI0vuoGa1zaDiz0hQmFN&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 27 Oct 2024 11:21:04 GMT +Content-Length: 82 + +{"session_key":"003WavNfATnB5BJWB/fVfQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-27T19:30:32.337242+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_7XaZRC8a7x_zcG5pk7AqcryYvrnbRckXRvZ8bAeQUeWjClEy83C345ZgnFPNG89pUmWngyeRADNV1Tk9XUziRitdXYGpA44TKeEgiMW2jmZCoYWQ0ue3078rG9UJFNgAFATPX&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1K8l000ZMh5T1WmJ0006V1jG3K8l0D&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 27 Oct 2024 11:30:03 GMT +Content-Length: 82 + +{"session_key":"P8k+5OCLhAbxgWSa7vcOng==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-27T21:36:16.382445+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_AD7lpcdkmXDHteHdaNGA5dS3SHnSmvWiHZ-uyn7YVv9SHLiOHv4K-nGnUGnYjKrYeNRDxIS0e-xjQ265IgdMsS1LtnB2Jr1xZ-mwmERkbKlNkWkcKz-IfOGDi58EFWjAFAUVM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c12mOkl2EIRpe4Znjol2U01ex32mOk3&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 27 Oct 2024 13:35:47 GMT +Content-Length: 82 + +{"session_key":"xWmvA0TlT1j4Dps7TW9FOQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-27T22:10:11.153067+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_AD7lpcdkmXDHteHdaNGA5dS3SHnSmvWiHZ-uyn7YVv9SHLiOHv4K-nGnUGnYjKrYeNRDxIS0e-xjQ265IgdMsS1LtnB2Jr1xZ-mwmERkbKlNkWkcKz-IfOGDi58EFWjAFAUVM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1LaOFa1mK5qI05PuJa1pEcYp2LaOF8&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 27 Oct 2024 14:09:42 GMT +Content-Length: 82 + +{"session_key":"34TJoiBe/lzBaNYPn/Prlw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-27T22:44:43.074239+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_AD7lpcdkmXDHteHdaNGA5dS3SHnSmvWiHZ-uyn7YVv9SHLiOHv4K-nGnUGnYjKrYeNRDxIS0e-xjQ265IgdMsS1LtnB2Jr1xZ-mwmERkbKlNkWkcKz-IfOGDi58EFWjAFAUVM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1Pz9ll2go5qe4OmYol2LqSNS3Pz9lf&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 27 Oct 2024 14:44:14 GMT +Content-Length: 82 + +{"session_key":"VHlQTkyPiZ5F64IpCwbu5g==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-28T14:48:20.224925+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_lBmaWdtEf2C9LXhPGT2UUHgHMZS8YZ_laKFXbxYWBbBWeuDeVZU_06ybH97udNIuWB0WYZBj7rh6X0MGII4aXdHBIIppaBUXjgu6h-0K3eVxAfBFhricGWTC9ckNYYcAEARCC&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1SSXFa1AeupI0PSkHa1Yojkm4SSXF4&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 28 Oct 2024 06:47:51 GMT +Content-Length: 82 + +{"session_key":"cwvdMDkBJqFjLPDZDgJoXw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-28T14:55:12.512707+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_lBmaWdtEf2C9LXhPGT2UUHgHMZS8YZ_laKFXbxYWBbBWeuDeVZU_06ybH97udNIuWB0WYZBj7rh6X0MGII4aXdHBIIppaBUXjgu6h-0K3eVxAfBFhricGWTC9ckNYYcAEARCC&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1UTA0w3PN6K33EeF0w3W1dWn0UTA0x&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 28 Oct 2024 06:54:43 GMT +Content-Length: 82 + +{"session_key":"DTGh32KkIcRP+7Ang+VPSg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-28T14:55:18.708042+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_lBmaWdtEf2C9LXhPGT2UUHgHMZS8YZ_laKFXbxYWBbBWeuDeVZU_06ybH97udNIuWB0WYZBj7rh6X0MGII4aXdHBIIppaBUXjgu6h-0K3eVxAfBFhricGWTC9ckNYYcAEARCC&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1lpwGa1i8WoI0tOHFa1bRpss0lpwGi&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 28 Oct 2024 06:54:49 GMT +Content-Length: 82 + +{"session_key":"o3y/HGhQR6pkIiRT0Ie0gQ==","openid":"oSsSc7cS4vUprOfY0ZPulgf7_EQ4"} +-------- +NULL +[2024-10-28T15:00:06.726034+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_lBmaWdtEf2C9LXhPGT2UUHgHMZS8YZ_laKFXbxYWBbBWeuDeVZU_06ybH97udNIuWB0WYZBj7rh6X0MGII4aXdHBIIppaBUXjgu6h-0K3eVxAfBFhricGWTC9ckNYYcAEARCC&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1UiF000H1O4T1FWM100qo5Ws1UiF0T&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 28 Oct 2024 06:59:37 GMT +Content-Length: 82 + +{"session_key":"5gOv+32K+DR5y5BNlv2wjw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-28T15:05:31.538590+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_lBmaWdtEf2C9LXhPGT2UUHgHMZS8YZ_laKFXbxYWBbBWeuDeVZU_06ybH97udNIuWB0WYZBj7rh6X0MGII4aXdHBIIppaBUXjgu6h-0K3eVxAfBFhricGWTC9ckNYYcAEARCC&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1RSHFa1XsKpI0th9Ga130qCe2RSHFd&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 28 Oct 2024 07:05:02 GMT +Content-Length: 82 + +{"session_key":"LNxaLc2ZEdnOhidG0TFPQQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-28T15:10:04.122749+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_lBmaWdtEf2C9LXhPGT2UUHgHMZS8YZ_laKFXbxYWBbBWeuDeVZU_06ybH97udNIuWB0WYZBj7rh6X0MGII4aXdHBIIppaBUXjgu6h-0K3eVxAfBFhricGWTC9ckNYYcAEARCC&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c15ggGa1XQcpI0iZ1Ia1XsAUx25ggGa&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 28 Oct 2024 07:09:35 GMT +Content-Length: 82 + +{"session_key":"nIp51sezN99+/xUJ1Y50UQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-28T15:35:36.816749+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_lBmaWdtEf2C9LXhPGT2UUHgHMZS8YZ_laKFXbxYWBbBWeuDeVZU_06ybH97udNIuWB0WYZBj7rh6X0MGII4aXdHBIIppaBUXjgu6h-0K3eVxAfBFhricGWTC9ckNYYcAEARCC&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f15YLkl2Sy4qe4z4xml2lrEes25YLkx&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 28 Oct 2024 07:35:07 GMT +Content-Length: 82 + +{"session_key":"AnJ/qsE+Cz13pMhfsXvVZA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-28T17:53:31.918508+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_udVAAk1fJTZQPxwjJditiuIE5nu4swotsM3ML7z-n4TM4-k8Jpl7n3gOxZNX91li7qP26lRdl_Nknphi_pAzmTXOtnar8FOwb9CuXBlDFAtCoB5fk2y8vTt35jALNFjABAACU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b19tNHa1HXynI0oSYHa1wUNRo29tNHq&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 28 Oct 2024 09:53:02 GMT +Content-Length: 82 + +{"session_key":"lYszE06AuujJo2vlFmq94g==","openid":"oSsSc7dg92c93yIl9vuzjedLbDUs"} +-------- +NULL +[2024-10-28T17:53:42.919347+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_udVAAk1fJTZQPxwjJditiuIE5nu4swotsM3ML7z-n4TM4-k8Jpl7n3gOxZNX91li7qP26lRdl_Nknphi_pAzmTXOtnar8FOwb9CuXBlDFAtCoB5fk2y8vTt35jALNFjABAACU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1E8SFa1njupI0VRQGa1tHwd61E8SFJ&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 28 Oct 2024 09:53:13 GMT +Content-Length: 82 + +{"session_key":"lYszE06AuujJo2vlFmq94g==","openid":"oSsSc7dg92c93yIl9vuzjedLbDUs"} +-------- +NULL +[2024-10-28T17:55:34.518389+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_udVAAk1fJTZQPxwjJditiuIE5nu4swotsM3ML7z-n4TM4-k8Jpl7n3gOxZNX91li7qP26lRdl_Nknphi_pAzmTXOtnar8FOwb9CuXBlDFAtCoB5fk2y8vTt35jALNFjABAACU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1GNd0w3rVnK332VZZv3JVm5Q2GNd0O&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 28 Oct 2024 09:55:05 GMT +Content-Length: 82 + +{"session_key":"1DsPwLZ0RIevCKBe4X9cRg==","openid":"oSsSc7UjSAllIJgM7z4RLz8tvaoU"} +-------- +NULL +[2024-10-28T17:55:42.173790+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_udVAAk1fJTZQPxwjJditiuIE5nu4swotsM3ML7z-n4TM4-k8Jpl7n3gOxZNX91li7qP26lRdl_Nknphi_pAzmTXOtnar8FOwb9CuXBlDFAtCoB5fk2y8vTt35jALNFjABAACU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1eUL0w3GOPJ333Ke2w3BCRIE2eUL0r&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 28 Oct 2024 09:55:13 GMT +Content-Length: 82 + +{"session_key":"1DsPwLZ0RIevCKBe4X9cRg==","openid":"oSsSc7UjSAllIJgM7z4RLz8tvaoU"} +-------- +NULL +[2024-10-28T17:55:49.291040+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_udVAAk1fJTZQPxwjJditiuIE5nu4swotsM3ML7z-n4TM4-k8Jpl7n3gOxZNX91li7qP26lRdl_Nknphi_pAzmTXOtnar8FOwb9CuXBlDFAtCoB5fk2y8vTt35jALNFjABAACU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1BOd0w30RnK33pjw3w3WIogi0BOd0s&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 28 Oct 2024 09:55:20 GMT +Content-Length: 82 + +{"session_key":"1DsPwLZ0RIevCKBe4X9cRg==","openid":"oSsSc7UjSAllIJgM7z4RLz8tvaoU"} +-------- +NULL +[2024-10-28T17:55:56.715500+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_udVAAk1fJTZQPxwjJditiuIE5nu4swotsM3ML7z-n4TM4-k8Jpl7n3gOxZNX91li7qP26lRdl_Nknphi_pAzmTXOtnar8FOwb9CuXBlDFAtCoB5fk2y8vTt35jALNFjABAACU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1ctYGa1v1ooI0R4qGa11EBKB3ctYG0&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 28 Oct 2024 09:55:27 GMT +Content-Length: 82 + +{"session_key":"Uzt+xl9ZVRuwyKVn1ylx8A==","openid":"oSsSc7QRYnOYDLpoQ381dc9FY09w"} +-------- +NULL +[2024-10-28T17:56:05.410311+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_udVAAk1fJTZQPxwjJditiuIE5nu4swotsM3ML7z-n4TM4-k8Jpl7n3gOxZNX91li7qP26lRdl_Nknphi_pAzmTXOtnar8FOwb9CuXBlDFAtCoB5fk2y8vTt35jALNFjABAACU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1GnqGa1F7WoI08j1Ia1bOpds2GnqGL&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 28 Oct 2024 09:55:36 GMT +Content-Length: 82 + +{"session_key":"Uzt+xl9ZVRuwyKVn1ylx8A==","openid":"oSsSc7QRYnOYDLpoQ381dc9FY09w"} +-------- +NULL +[2024-10-28T17:56:07.355983+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_udVAAk1fJTZQPxwjJditiuIE5nu4swotsM3ML7z-n4TM4-k8Jpl7n3gOxZNX91li7qP26lRdl_Nknphi_pAzmTXOtnar8FOwb9CuXBlDFAtCoB5fk2y8vTt35jALNFjABAACU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1KhSFa1LdupI0O7zJa1th06w3KhSFd&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 28 Oct 2024 09:55:38 GMT +Content-Length: 82 + +{"session_key":"gb7S8diF8jQr2IECopJbdw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-28T17:56:12.356185+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_udVAAk1fJTZQPxwjJditiuIE5nu4swotsM3ML7z-n4TM4-k8Jpl7n3gOxZNX91li7qP26lRdl_Nknphi_pAzmTXOtnar8FOwb9CuXBlDFAtCoB5fk2y8vTt35jALNFjABAACU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d3uT00000Dl5T12LJ3001Nsxk0uT002&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 28 Oct 2024 09:55:43 GMT +Content-Length: 82 + +{"session_key":"ALScHYI0t05FxboRgx9Ecw==","openid":"oSsSc7f13kMwKHcIDhlIMhKY5JpU"} +-------- +NULL +[2024-10-28T17:58:37.211589+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_udVAAk1fJTZQPxwjJditiuIE5nu4swotsM3ML7z-n4TM4-k8Jpl7n3gOxZNX91li7qP26lRdl_Nknphi_pAzmTXOtnar8FOwb9CuXBlDFAtCoB5fk2y8vTt35jALNFjABAACU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b18lkFa1Be4qI0BoQIa1xSjM508lkFL&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 28 Oct 2024 09:58:08 GMT +Content-Length: 82 + +{"session_key":"lrMdZj/d2XqpMcWTWlbMOw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T09:27:35.835010+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gAEbS8HEGHFDhIfHsbdjE4rmMySwcNaGjpbVHxaQjJOsfjj0R43J2mdQNUnqoLlfIAQfHBcspvlnUrDCyk9hmSc1qlQTM3jvb_wKxmmSOBsf6MzqJRZia65bEYMIDHdAHAXSW&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1HPpFa15YjqI06vyJa1DY93E3HPpFf&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 01:27:06 GMT +Content-Length: 82 + +{"session_key":"BVEXU7A9ap9ZMK4lho7JpQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T09:29:06.853812+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gAEbS8HEGHFDhIfHsbdjE4rmMySwcNaGjpbVHxaQjJOsfjj0R43J2mdQNUnqoLlfIAQfHBcspvlnUrDCyk9hmSc1qlQTM3jvb_wKxmmSOBsf6MzqJRZia65bEYMIDHdAHAXSW&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b15MV000uhs6T15V5000LmUmy35MV0m&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 01:28:37 GMT +Content-Length: 82 + +{"session_key":"BVEXU7A9ap9ZMK4lho7JpQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T09:39:37.464503+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gAEbS8HEGHFDhIfHsbdjE4rmMySwcNaGjpbVHxaQjJOsfjj0R43J2mdQNUnqoLlfIAQfHBcspvlnUrDCyk9hmSc1qlQTM3jvb_wKxmmSOBsf6MzqJRZia65bEYMIDHdAHAXSW&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1jdk0w3YVcL337H84w3faIXu3jdk0a&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 01:39:08 GMT +Content-Length: 82 + +{"session_key":"gKnKo66yneSA7fh/wmUMMg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T09:41:44.389296+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gAEbS8HEGHFDhIfHsbdjE4rmMySwcNaGjpbVHxaQjJOsfjj0R43J2mdQNUnqoLlfIAQfHBcspvlnUrDCyk9hmSc1qlQTM3jvb_wKxmmSOBsf6MzqJRZia65bEYMIDHdAHAXSW&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1WJ2200YXx7T199H300uovjB2WJ22o&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 01:41:15 GMT +Content-Length: 82 + +{"session_key":"gKnKo66yneSA7fh/wmUMMg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T09:54:21.349147+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gAEbS8HEGHFDhIfHsbdjE4rmMySwcNaGjpbVHxaQjJOsfjj0R43J2mdQNUnqoLlfIAQfHBcspvlnUrDCyk9hmSc1qlQTM3jvb_wKxmmSOBsf6MzqJRZia65bEYMIDHdAHAXSW&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a3Fyk200ArN7T1N1X000RVNHR3Fyk2O&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 01:53:52 GMT +Content-Length: 82 + +{"session_key":"ZV3t5NJ/sY3DhcZbar37rQ==","openid":"oSsSc7Qjp0aoOul6rohwDET-4xrQ"} +-------- +NULL +[2024-10-29T09:54:25.108913+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gAEbS8HEGHFDhIfHsbdjE4rmMySwcNaGjpbVHxaQjJOsfjj0R43J2mdQNUnqoLlfIAQfHBcspvlnUrDCyk9hmSc1qlQTM3jvb_wKxmmSOBsf6MzqJRZia65bEYMIDHdAHAXSW&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a3KjX000ncq6T1Frg100yMvWO2KjX0W&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 01:53:55 GMT +Content-Length: 82 + +{"session_key":"ZV3t5NJ/sY3DhcZbar37rQ==","openid":"oSsSc7Qjp0aoOul6rohwDET-4xrQ"} +-------- +NULL +[2024-10-29T09:55:03.194081+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gAEbS8HEGHFDhIfHsbdjE4rmMySwcNaGjpbVHxaQjJOsfjj0R43J2mdQNUnqoLlfIAQfHBcspvlnUrDCyk9hmSc1qlQTM3jvb_wKxmmSOBsf6MzqJRZia65bEYMIDHdAHAXSW&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f3d0Rll2ESjre4pPaol2fWwFs3d0Rlx&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 01:54:34 GMT +Content-Length: 82 + +{"session_key":"ZV3t5NJ/sY3DhcZbar37rQ==","openid":"oSsSc7Qjp0aoOul6rohwDET-4xrQ"} +-------- +NULL +[2024-10-29T09:55:05.786134+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gAEbS8HEGHFDhIfHsbdjE4rmMySwcNaGjpbVHxaQjJOsfjj0R43J2mdQNUnqoLlfIAQfHBcspvlnUrDCyk9hmSc1qlQTM3jvb_wKxmmSOBsf6MzqJRZia65bEYMIDHdAHAXSW&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f3jUill2zMLqe4lSPkl2TJTov4jUilJ&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 01:54:36 GMT +Content-Length: 82 + +{"session_key":"ZV3t5NJ/sY3DhcZbar37rQ==","openid":"oSsSc7Qjp0aoOul6rohwDET-4xrQ"} +-------- +NULL +[2024-10-29T09:55:07.859443+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gAEbS8HEGHFDhIfHsbdjE4rmMySwcNaGjpbVHxaQjJOsfjj0R43J2mdQNUnqoLlfIAQfHBcspvlnUrDCyk9hmSc1qlQTM3jvb_wKxmmSOBsf6MzqJRZia65bEYMIDHdAHAXSW&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f3vsv100tkY6T13yI1007T2IY1vsv1E&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 01:54:38 GMT +Content-Length: 82 + +{"session_key":"ZV3t5NJ/sY3DhcZbar37rQ==","openid":"oSsSc7Qjp0aoOul6rohwDET-4xrQ"} +-------- +NULL +[2024-10-29T09:55:12.920293+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gAEbS8HEGHFDhIfHsbdjE4rmMySwcNaGjpbVHxaQjJOsfjj0R43J2mdQNUnqoLlfIAQfHBcspvlnUrDCyk9hmSc1qlQTM3jvb_wKxmmSOBsf6MzqJRZia65bEYMIDHdAHAXSW&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b3Fd8000q5B5T1fMB10018Pnt2Fd804&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 01:54:43 GMT +Content-Length: 82 + +{"session_key":"ZV3t5NJ/sY3DhcZbar37rQ==","openid":"oSsSc7Qjp0aoOul6rohwDET-4xrQ"} +-------- +NULL +[2024-10-29T09:56:30.319687+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gAEbS8HEGHFDhIfHsbdjE4rmMySwcNaGjpbVHxaQjJOsfjj0R43J2mdQNUnqoLlfIAQfHBcspvlnUrDCyk9hmSc1qlQTM3jvb_wKxmmSOBsf6MzqJRZia65bEYMIDHdAHAXSW&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b3BW1ll29Ouqe4peFnl2b0Lgi0BW1ld&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 01:56:01 GMT +Content-Length: 82 + +{"session_key":"ZV3t5NJ/sY3DhcZbar37rQ==","openid":"oSsSc7Qjp0aoOul6rohwDET-4xrQ"} +-------- +NULL +[2024-10-29T09:56:35.533721+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gAEbS8HEGHFDhIfHsbdjE4rmMySwcNaGjpbVHxaQjJOsfjj0R43J2mdQNUnqoLlfIAQfHBcspvlnUrDCyk9hmSc1qlQTM3jvb_wKxmmSOBsf6MzqJRZia65bEYMIDHdAHAXSW&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c3Ulp000vcS5T1ecc400e785p0Ulp0e&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 01:56:06 GMT +Content-Length: 82 + +{"session_key":"ZV3t5NJ/sY3DhcZbar37rQ==","openid":"oSsSc7Qjp0aoOul6rohwDET-4xrQ"} +-------- +NULL +[2024-10-29T10:01:11.921748+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gAEbS8HEGHFDhIfHsbdjE4rmMySwcNaGjpbVHxaQjJOsfjj0R43J2mdQNUnqoLlfIAQfHBcspvlnUrDCyk9hmSc1qlQTM3jvb_wKxmmSOBsf6MzqJRZia65bEYMIDHdAHAXSW&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1jDp0007416T13ju100lhehp3jDp0A&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 02:00:42 GMT +Content-Length: 82 + +{"session_key":"gKnKo66yneSA7fh/wmUMMg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T10:07:17.085882+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gAEbS8HEGHFDhIfHsbdjE4rmMySwcNaGjpbVHxaQjJOsfjj0R43J2mdQNUnqoLlfIAQfHBcspvlnUrDCyk9hmSc1qlQTM3jvb_wKxmmSOBsf6MzqJRZia65bEYMIDHdAHAXSW&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1kyLkl2hTmqe4uwHml2OuqbA2kyLks&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 02:06:47 GMT +Content-Length: 82 + +{"session_key":"9IzzyWCFoSzuWOKZJICnIA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T10:09:58.713813+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gAEbS8HEGHFDhIfHsbdjE4rmMySwcNaGjpbVHxaQjJOsfjj0R43J2mdQNUnqoLlfIAQfHBcspvlnUrDCyk9hmSc1qlQTM3jvb_wKxmmSOBsf6MzqJRZia65bEYMIDHdAHAXSW&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b11tJFa10rIqI0nmEFa1Z8rik21tJFl&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 02:09:29 GMT +Content-Length: 82 + +{"session_key":"mYELK29wT4oSidzeFlp86Q==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T10:10:13.649878+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gAEbS8HEGHFDhIfHsbdjE4rmMySwcNaGjpbVHxaQjJOsfjj0R43J2mdQNUnqoLlfIAQfHBcspvlnUrDCyk9hmSc1qlQTM3jvb_wKxmmSOBsf6MzqJRZia65bEYMIDHdAHAXSW&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1Z4m0w3p0lL33ws60w3NDNsR0Z4m0S&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 02:09:44 GMT +Content-Length: 82 + +{"session_key":"mYELK29wT4oSidzeFlp86Q==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T11:53:18.191140+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b3Ivh1w3S89M33DWl1w3MEUcR2Ivh1R&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 03:52:49 GMT +Content-Length: 82 + +{"session_key":"ZV3t5NJ/sY3DhcZbar37rQ==","openid":"oSsSc7Qjp0aoOul6rohwDET-4xrQ"} +-------- +NULL +[2024-10-29T11:54:50.405010+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1Ycqll2oSTqe4cd7nl2qQJlX3YcqlV&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 03:54:21 GMT +Content-Length: 82 + +{"session_key":"9RrJvgMUs6guM+VbA4usBA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T11:55:37.978732+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1wB01w3oFRL33FMP1w3g43AI3wB011&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 03:55:08 GMT +Content-Length: 82 + +{"session_key":"dLzXe3ItD54DNepkgMYeCA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T11:55:47.365434+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a12ws0w3rAjL33n0R2w3STqxk02ws0W&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 03:55:18 GMT +Content-Length: 82 + +{"session_key":"dLzXe3ItD54DNepkgMYeCA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T11:56:47.255728+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1Nwb0w36B2L33IKK2w3tpTUC3Nwb0n&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 03:56:18 GMT +Content-Length: 82 + +{"session_key":"yF1FTQH1LxybySZiriE2XA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T11:59:42.619297+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1TfoGa1UMerI09ElGa115VjB2TfoG5&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 03:59:13 GMT +Content-Length: 82 + +{"session_key":"XEMEIflWBpY7lq24+cFKtw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T12:00:15.834476+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e35Zy1w3rrpM33foV1w39EiEb35Zy1K&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 03:59:46 GMT +Content-Length: 82 + +{"session_key":"ZV3t5NJ/sY3DhcZbar37rQ==","openid":"oSsSc7Qjp0aoOul6rohwDET-4xrQ"} +-------- +NULL +[2024-10-29T12:04:23.528907+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1boU100O6n7T1dMi200x0w3u1boU16&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 04:03:54 GMT +Content-Length: 82 + +{"session_key":"PoFfptznR00xNeX9mDif4Q==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T12:08:32.803991+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1WhOml2nqgse4Mq6ml2Y1azh1WhOmA&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 04:08:03 GMT +Content-Length: 82 + +{"session_key":"6womeA5FTOIpz2Z4B64fmQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T12:15:18.235845+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1QcpGa1gidrI0JI4Ia1GMSJp4QcpGO&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 04:14:49 GMT +Content-Length: 82 + +{"session_key":"pXJNY/aiCAzg+AsrJGP1yw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T12:18:43.567760+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1IjRFa1JlFqI0lr0Ja12ZyDP1IjRFF&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 04:18:14 GMT +Content-Length: 82 + +{"session_key":"MQ5dvoLqebvYFj8gvrg8kg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T12:25:50.496303+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a12m0000u0E5T1LCN300TdGN722m00x&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 04:25:21 GMT +Content-Length: 82 + +{"session_key":"w2qDHS8kovIz7Ew6cegPqQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T12:28:12.086095+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a119Ukl2vpxqe4vhqml2OhZu3019Ukc&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 04:27:42 GMT +Content-Length: 82 + +{"session_key":"ZW+6gTvC4TrrSKpT+SWGsQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T12:31:14.984440+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a3znbll2WdPqe4xAzml2EDuG11znbl-&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 04:30:45 GMT +Content-Length: 82 + +{"session_key":"ZV3t5NJ/sY3DhcZbar37rQ==","openid":"oSsSc7Qjp0aoOul6rohwDET-4xrQ"} +-------- +NULL +[2024-10-29T12:33:16.483284+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1G9BFa1SUBqI0uvJIa1dlTaF3G9BF2&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 04:32:47 GMT +Content-Length: 82 + +{"session_key":"ZW+6gTvC4TrrSKpT+SWGsQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T12:35:09.271254+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1KdkFa1zMkqI0E1WHa17Kxn93KdkFb&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 04:34:40 GMT +Content-Length: 82 + +{"session_key":"eZx6nd3od140yCqlPTQ9IQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T12:35:32.104094+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1HZh000BnV5T1xY0200iEZNy4HZh0I&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 04:35:02 GMT +Content-Length: 82 + +{"session_key":"fbiJOC3l6lXa8356uKoFiA==","openid":"oSsSc7V8lr00YBgSbMidg-6Y7oUY"} +-------- +NULL +[2024-10-29T12:40:11.378703+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1RCSFa18MTqI0hqSHa1uwdcv1RCSFG&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 04:39:42 GMT +Content-Length: 82 + +{"session_key":"04WARG1YY4QSKJQw7xzfjg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T13:17:03.118829+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_d-UUtygnJ_L8pSc9d_j6WU18LfHvS9Xn7oEfoTD4sb96HEO4lw_dZH0dXLwDzywBgCeZxMFHfbsbUTcYTF6deI3RUXv4rDgg10s7EtcNU-Qd2yWjnG0OLQ1ic4kAGBeADALRM&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1Oj3ml2L5Dre4j9jol21X0f32Oj3mw&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 05:16:33 GMT +Content-Length: 82 + +{"session_key":"eWWuY63338L8qzPytBthmg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T14:26:45.418240+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_Ft24X-Wsj0fwoIkrStp8IdNDBlQG1drRu1Jyt6aSaox_TbNxhfkUaLC8X5yXfqEb2BJ6awWZoruQ73vxp2bBOvu7n8sr0OURl1tiQgFPHnVy9_mo6YvSUsrikW0RDGhADAFRU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d3an1ll2p3Hqe4k3Lll2RVwmS2an1l6&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 06:26:16 GMT +Content-Length: 82 + +{"session_key":"5hAKJIr/TFVW7hHTNiSxyQ==","openid":"oSsSc7SNGPF_6_kPdyOWmU8YWSh0"} +-------- +NULL +[2024-10-29T14:31:01.061988+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_Ft24X-Wsj0fwoIkrStp8IdNDBlQG1drRu1Jyt6aSaox_TbNxhfkUaLC8X5yXfqEb2BJ6awWZoruQ73vxp2bBOvu7n8sr0OURl1tiQgFPHnVy9_mo6YvSUsrikW0RDGhADAFRU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a3i5G000pHl6T1Vnj100lnLN72i5G0f&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 06:30:31 GMT +Content-Length: 82 + +{"session_key":"zseFOTD9f9mJ2V4h4cbS7w==","openid":"oSsSc7WtT5EKENUVdhkgxiExYvi0"} +-------- +NULL +[2024-10-29T14:39:14.109176+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_Ft24X-Wsj0fwoIkrStp8IdNDBlQG1drRu1Jyt6aSaox_TbNxhfkUaLC8X5yXfqEb2BJ6awWZoruQ73vxp2bBOvu7n8sr0OURl1tiQgFPHnVy9_mo6YvSUsrikW0RDGhADAFRU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d10Dr1w32AuM33qtC0w3xi0AD20Dr10&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 06:38:44 GMT +Content-Length: 82 + +{"session_key":"eWWuY63338L8qzPytBthmg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T14:44:05.038121+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_Ft24X-Wsj0fwoIkrStp8IdNDBlQG1drRu1Jyt6aSaox_TbNxhfkUaLC8X5yXfqEb2BJ6awWZoruQ73vxp2bBOvu7n8sr0OURl1tiQgFPHnVy9_mo6YvSUsrikW0RDGhADAFRU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f3gPT0w318GL33VW20w3daNCF4gPT0R&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 06:43:35 GMT +Content-Length: 82 + +{"session_key":"ZV3t5NJ/sY3DhcZbar37rQ==","openid":"oSsSc7Qjp0aoOul6rohwDET-4xrQ"} +-------- +NULL +[2024-10-29T14:45:00.959926+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_Ft24X-Wsj0fwoIkrStp8IdNDBlQG1drRu1Jyt6aSaox_TbNxhfkUaLC8X5yXfqEb2BJ6awWZoruQ73vxp2bBOvu7n8sr0OURl1tiQgFPHnVy9_mo6YvSUsrikW0RDGhADAFRU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1bTp000WeO5T1uMe100yICzh1bTp0y&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 06:44:31 GMT +Content-Length: 82 + +{"session_key":"jB0romPfEgqbBwD2ysjajA==","openid":"oSsSc7cS4vUprOfY0ZPulgf7_EQ4"} +-------- +NULL +[2024-10-29T15:01:32.518202+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_Ft24X-Wsj0fwoIkrStp8IdNDBlQG1drRu1Jyt6aSaox_TbNxhfkUaLC8X5yXfqEb2BJ6awWZoruQ73vxp2bBOvu7n8sr0OURl1tiQgFPHnVy9_mo6YvSUsrikW0RDGhADAFRU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1Ixkll2uHJqe49Rwol21M2OS3IxklN&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 07:01:03 GMT +Content-Length: 82 + +{"session_key":"Azz50FmR2/REBrjVnWjUYA==","openid":"oSsSc7So0zlh5yv9gnqarsnyOer4"} +-------- +NULL +[2024-10-29T15:01:35.454783+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_Ft24X-Wsj0fwoIkrStp8IdNDBlQG1drRu1Jyt6aSaox_TbNxhfkUaLC8X5yXfqEb2BJ6awWZoruQ73vxp2bBOvu7n8sr0OURl1tiQgFPHnVy9_mo6YvSUsrikW0RDGhADAFRU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1uoQGa1lXCrI0m3SHa1N0DZB1uoQGw&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 07:01:06 GMT +Content-Length: 82 + +{"session_key":"Azz50FmR2/REBrjVnWjUYA==","openid":"oSsSc7So0zlh5yv9gnqarsnyOer4"} +-------- +NULL +[2024-10-29T15:12:01.975124+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_Ft24X-Wsj0fwoIkrStp8IdNDBlQG1drRu1Jyt6aSaox_TbNxhfkUaLC8X5yXfqEb2BJ6awWZoruQ73vxp2bBOvu7n8sr0OURl1tiQgFPHnVy9_mo6YvSUsrikW0RDGhADAFRU&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1ixr0006NP5T13xH000pXTfO3ixr0P&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 07:11:32 GMT +Content-Length: 82 + +{"session_key":"kPsLuUFSlT1Ijozl21MHEg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T16:52:19.790864+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_GmFQagcMFaTkeA_Sj-hNBHzwvUpb_KESmD7taBunlIvpAGUbBlE2o9CiRH37LK4m8U076-C283CpFb0MbZWuj6ilR26opF3IAk-cbvz5G_kVvx2BA7IQbO5blM8CDSjAJALMB&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b16Kz1w3LMjM331gj4w3BaH9e16Kz16&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 08:51:50 GMT +Content-Length: 82 + +{"session_key":"yBvIWGiIw1mI8IzIQsKQaw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-10-29T21:06:06.721485+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_DtGYRCE-Phv_aqzyySeS4e3FR27yqpn-j5qTCCEfzfk-HE824ZzucW1g73zRWx9DvlDr8toKC_0ot6T1hz1cCciDX_ctmXvjXe1r8QEq1kYse8xr1xNx54tqa74ZHSaADAOWH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b3KCRkl2uKdqe4dmjml24LVCU2KCRkl&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 29 Oct 2024 13:05:37 GMT +Content-Length: 82 + +{"session_key":"hYbkbOUsDZ4cnlSNhZ5t1Q==","openid":"oSsSc7SNGPF_6_kPdyOWmU8YWSh0"} +-------- +NULL +[2024-11-02T13:32:54.515358+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gYiDsMdgVtL9VXCsI2gfTBdprcXCua0QL8-S9gO_WAVBJ4jG5LqAox4zsLghTi1zI5op7voO_xoUP0_F9DGq6GGO3yuINl3qu57rIdpx20Dmbnr1BtEjU5wEvTsIIBjAJAAAT&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1z0PFa1iNqrI0glGGa16z2oo1z0PF8&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 02 Nov 2024 05:32:24 GMT +Content-Length: 82 + +{"session_key":"dDbMZZ1HUE4f1ZEFLDO4FQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-02T13:49:48.651030+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=85_gYiDsMdgVtL9VXCsI2gfTBdprcXCua0QL8-S9gO_WAVBJ4jG5LqAox4zsLghTi1zI5op7voO_xoUP0_F9DGq6GGO3yuINl3qu57rIdpx20Dmbnr1BtEjU5wEvTsIIBjAJAAAT&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1YJw000X8J6T1NNl400D1gtx1YJw07&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 02 Nov 2024 05:49:18 GMT +Content-Length: 82 + +{"session_key":"5H8SCuKs4mpO4eaD3tndRA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-02T16:05:32.184301+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_nyWg0sk_Oqnk7HzrwYTJ6AGo5pvHzCWdt5RFfljP4lGmqYk9GEtmw8jHqF2dxW-SFxovtYUfivLP72m0YGhNKM-iocUswhyLLf4TnTTmc2Lp5vzF670wgDnbatwRDAaAFAKWT&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1tdqFa1AxNrI0fsFFa1LGaO21tdqFE&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 02 Nov 2024 08:05:01 GMT +Content-Length: 82 + +{"session_key":"giHUCzMZGQEt4ApTdWPELA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-02T16:10:59.997626+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_nyWg0sk_Oqnk7HzrwYTJ6AGo5pvHzCWdt5RFfljP4lGmqYk9GEtmw8jHqF2dxW-SFxovtYUfivLP72m0YGhNKM-iocUswhyLLf4TnTTmc2Lp5vzF670wgDnbatwRDAaAFAKWT&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1Df70004i67T1l651009s7tx1Df70k&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 02 Nov 2024 08:10:29 GMT +Content-Length: 82 + +{"session_key":"EID7BVAI6lpscUUcOiUluw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-02T18:00:04.535041+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_nyWg0sk_Oqnk7HzrwYTJ6AGo5pvHzCWdt5RFfljP4lGmqYk9GEtmw8jHqF2dxW-SFxovtYUfivLP72m0YGhNKM-iocUswhyLLf4TnTTmc2Lp5vzF670wgDnbatwRDAaAFAKWT&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b110M000adv7T13Mu200ZiAJp410M0j&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 02 Nov 2024 09:59:34 GMT +Content-Length: 82 + +{"session_key":"eEujY9ufkv5MXqJxUFliAQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-02T20:49:20.766082+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86__Qbv4sK5mJtY-UOo6vFdCvhFUo7wqChGal7SUddijSJqA1Xb3VqTzTD4G0at4nv-iZUTA8zMCFn0Ovlq-2gBfrLUcguLcaVDBTGuqEXX_Wcej6m_GTcsyVI-F5IVWUfAEAQRP&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c33aWml2aa8ue4YCLnl2gDN6p03aWmo&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 02 Nov 2024 12:48:50 GMT +Content-Length: 82 + +{"session_key":"D8cz6t7/8qRHba2ieK0UwA==","openid":"oSsSc7ZJ03ecLflIm_IMgGbsP4wU"} +-------- +NULL +[2024-11-02T20:49:20.883946+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86__Qbv4sK5mJtY-UOo6vFdCvhFUo7wqChGal7SUddijSJqA1Xb3VqTzTD4G0at4nv-iZUTA8zMCFn0Ovlq-2gBfrLUcguLcaVDBTGuqEXX_Wcej6m_GTcsyVI-F5IVWUfAEAQRP&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f3UjW000V727T1nfj40011RXn0UjW08&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 02 Nov 2024 12:48:50 GMT +Content-Length: 82 + +{"session_key":"D8cz6t7/8qRHba2ieK0UwA==","openid":"oSsSc7ZJ03ecLflIm_IMgGbsP4wU"} +-------- +NULL +[2024-11-04T10:15:10.003715+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_au-gLQRkh3D7El3NE1ddC6g6L6PCFQhg7gczLo8-Ce5i1qHgiAHYgaLqEjE841SfYydRDhTS84XNubiEvdx6E-ghZA0pVgdDki4DEY8bGDiTSM4UIBHGbN3kf7IOGIaAIAUMO&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1VfOGa15KTtI0AbIFa1gvmWJ1VfOGS&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 02:14:39 GMT +Content-Length: 82 + +{"session_key":"8aI1nMzS3ZxLcBXmyySTvQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-04T13:32:55.172905+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_8IzIrRAsE9afD404TwdIk6uHmXEKPpRdfhFuJqYSc03sPaHF3jvsJ8GoVJFMPUbegK4xXb4SDefsjunbAixUCyXRUxk96RhOtsZukHpubEZ2SajQXX00umBDZFkFGGeAFANOO&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1nIw0w3oIIN33Z0L2w3douqA0nIw05&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 05:32:24 GMT +Content-Length: 82 + +{"session_key":"mm89udQrg5ucWqGkyhkC7w==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-04T13:37:37.847245+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_8IzIrRAsE9afD404TwdIk6uHmXEKPpRdfhFuJqYSc03sPaHF3jvsJ8GoVJFMPUbegK4xXb4SDefsjunbAixUCyXRUxk96RhOtsZukHpubEZ2SajQXX00umBDZFkFGGeAFANOO&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c17jmFa1HKxsI0LRLHa1vY5O217jmFS&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 05:37:07 GMT +Content-Length: 82 + +{"session_key":"j/0zr7L+CvQcX7IuYElg1Q==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-04T13:54:17.869676+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_8IzIrRAsE9afD404TwdIk6uHmXEKPpRdfhFuJqYSc03sPaHF3jvsJ8GoVJFMPUbegK4xXb4SDefsjunbAixUCyXRUxk96RhOtsZukHpubEZ2SajQXX00umBDZFkFGGeAFANOO&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1F7C000lQn8T1UnZ200XfVhp3F7C0W&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 05:53:47 GMT +Content-Length: 82 + +{"session_key":"j/0zr7L+CvQcX7IuYElg1Q==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-04T14:24:42.533375+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_8IzIrRAsE9afD404TwdIk6uHmXEKPpRdfhFuJqYSc03sPaHF3jvsJ8GoVJFMPUbegK4xXb4SDefsjunbAixUCyXRUxk96RhOtsZukHpubEZ2SajQXX00umBDZFkFGGeAFANOO&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1HCxll2ixete4vJ5ll2Z4TuT2HCxls&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 06:24:11 GMT +Content-Length: 82 + +{"session_key":"JAkblxyzPBuVeifdIJaubQ==","openid":"oSsSc7cS4vUprOfY0ZPulgf7_EQ4"} +-------- +NULL +[2024-11-04T14:26:13.412997+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_8IzIrRAsE9afD404TwdIk6uHmXEKPpRdfhFuJqYSc03sPaHF3jvsJ8GoVJFMPUbegK4xXb4SDefsjunbAixUCyXRUxk96RhOtsZukHpubEZ2SajQXX00umBDZFkFGGeAFANOO&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1o7V000sYB8T1I0C000OhZsR0o7V0d&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 06:25:42 GMT +Content-Length: 82 + +{"session_key":"JAkblxyzPBuVeifdIJaubQ==","openid":"oSsSc7cS4vUprOfY0ZPulgf7_EQ4"} +-------- +NULL +[2024-11-04T14:27:59.590387+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_8IzIrRAsE9afD404TwdIk6uHmXEKPpRdfhFuJqYSc03sPaHF3jvsJ8GoVJFMPUbegK4xXb4SDefsjunbAixUCyXRUxk96RhOtsZukHpubEZ2SajQXX00umBDZFkFGGeAFANOO&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1aV5ml2YIMte4fOtml2m0oUs1aV5mF&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 06:27:28 GMT +Content-Length: 82 + +{"session_key":"JAkblxyzPBuVeifdIJaubQ==","openid":"oSsSc7cS4vUprOfY0ZPulgf7_EQ4"} +-------- +NULL +[2024-11-04T14:28:21.541811+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_8IzIrRAsE9afD404TwdIk6uHmXEKPpRdfhFuJqYSc03sPaHF3jvsJ8GoVJFMPUbegK4xXb4SDefsjunbAixUCyXRUxk96RhOtsZukHpubEZ2SajQXX00umBDZFkFGGeAFANOO&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1xTOll2FIvte40UMkl2VeDiB2xTOlU&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 06:27:50 GMT +Content-Length: 82 + +{"session_key":"YG/TuURQqvpiLlqfLhQqxQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-04T14:39:50.587993+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_8IzIrRAsE9afD404TwdIk6uHmXEKPpRdfhFuJqYSc03sPaHF3jvsJ8GoVJFMPUbegK4xXb4SDefsjunbAixUCyXRUxk96RhOtsZukHpubEZ2SajQXX00umBDZFkFGGeAFANOO&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1i6qFa1rVqsI0LtpGa14aLaU1i6qFS&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 06:39:19 GMT +Content-Length: 82 + +{"session_key":"P5+U5PvGpBMK5SbScLqIVw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-04T14:50:57.563242+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_8IzIrRAsE9afD404TwdIk6uHmXEKPpRdfhFuJqYSc03sPaHF3jvsJ8GoVJFMPUbegK4xXb4SDefsjunbAixUCyXRUxk96RhOtsZukHpubEZ2SajQXX00umBDZFkFGGeAFANOO&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1pVfGa1XrhtI0WztHa1jlUaZ2pVfGC&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 06:50:26 GMT +Content-Length: 82 + +{"session_key":"FBGR3AUeiJvSf7Aa70Y+7w==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-04T14:53:00.850993+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_8IzIrRAsE9afD404TwdIk6uHmXEKPpRdfhFuJqYSc03sPaHF3jvsJ8GoVJFMPUbegK4xXb4SDefsjunbAixUCyXRUxk96RhOtsZukHpubEZ2SajQXX00umBDZFkFGGeAFANOO&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1DHF0004Aj8T1wIO200ppCwp1DHF0P&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 06:52:29 GMT +Content-Length: 82 + +{"session_key":"wMZVRisjFvKL8QY3FErclA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-04T14:55:46.101487+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_8IzIrRAsE9afD404TwdIk6uHmXEKPpRdfhFuJqYSc03sPaHF3jvsJ8GoVJFMPUbegK4xXb4SDefsjunbAixUCyXRUxk96RhOtsZukHpubEZ2SajQXX00umBDZFkFGGeAFANOO&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1AdgGa1CdhtI0UEVHa1mBJuz3AdgGS&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 06:55:15 GMT +Content-Length: 82 + +{"session_key":"JAkblxyzPBuVeifdIJaubQ==","openid":"oSsSc7cS4vUprOfY0ZPulgf7_EQ4"} +-------- +NULL +[2024-11-04T15:30:49.605685+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_8IzIrRAsE9afD404TwdIk6uHmXEKPpRdfhFuJqYSc03sPaHF3jvsJ8GoVJFMPUbegK4xXb4SDefsjunbAixUCyXRUxk96RhOtsZukHpubEZ2SajQXX00umBDZFkFGGeAFANOO&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1vU9000PjZ7T1tXV000NI6lc2vU90g&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 07:30:18 GMT +Content-Length: 82 + +{"session_key":"G5Bq1DpheW/IGvPsFEGTuA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-04T15:39:08.258683+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_vGQJA7jd_hmsi4gswYTJ6AGo5pvHzCWdt5RFfr6Vn8x7ScT9fqEKt1o1TZmRmTY7jo1QsS-JquPm2CiMzN1hvwz7Lk5Bq6SkQFAr2CeAkLSr_t0TFPowLCOv2iAYPOgACALCB&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1zXQGa1pM2uI07y7Ja1Ztlq13zXQGm&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 07:38:37 GMT +Content-Length: 82 + +{"session_key":"MI9/GUsvP5Bgp7UPlm+4Xg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-04T15:46:00.966575+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_vGQJA7jd_hmsi4gswYTJ6AGo5pvHzCWdt5RFfr6Vn8x7ScT9fqEKt1o1TZmRmTY7jo1QsS-JquPm2CiMzN1hvwz7Lk5Bq6SkQFAr2CeAkLSr_t0TFPowLCOv2iAYPOgACALCB&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1Zt4ll2uERse49COnl23DXAS0Zt4ly&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 07:45:30 GMT +Content-Length: 82 + +{"session_key":"Psmpp4aKrhW7BRifqJAwjQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-04T15:47:23.721094+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_vGQJA7jd_hmsi4gswYTJ6AGo5pvHzCWdt5RFfr6Vn8x7ScT9fqEKt1o1TZmRmTY7jo1QsS-JquPm2CiMzN1hvwz7Lk5Bq6SkQFAr2CeAkLSr_t0TFPowLCOv2iAYPOgACALCB&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1jay100dbl9T13HO000xE1mI0jay14&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 07:46:52 GMT +Content-Length: 82 + +{"session_key":"vv0/Ljw2RlvHcZ/lj8OCtg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-04T15:48:24.790847+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_vGQJA7jd_hmsi4gswYTJ6AGo5pvHzCWdt5RFfr6Vn8x7ScT9fqEKt1o1TZmRmTY7jo1QsS-JquPm2CiMzN1hvwz7Lk5Bq6SkQFAr2CeAkLSr_t0TFPowLCOv2iAYPOgACALCB&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e15Glll20G8te4uIgml21ZyJp45Gllr&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 07:47:53 GMT +Content-Length: 82 + +{"session_key":"j2zxGXrIZbCp/0Vuz8q+zQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-04T15:49:36.133803+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_vGQJA7jd_hmsi4gswYTJ6AGo5pvHzCWdt5RFfr6Vn8x7ScT9fqEKt1o1TZmRmTY7jo1QsS-JquPm2CiMzN1hvwz7Lk5Bq6SkQFAr2CeAkLSr_t0TFPowLCOv2iAYPOgACALCB&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1Cfh1000i49T1Eep200ywZvk0Cfh1Z&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 07:49:05 GMT +Content-Length: 82 + +{"session_key":"QXhLLYbva1kbMw6oCfcMKA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-04T15:51:13.104281+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_vGQJA7jd_hmsi4gswYTJ6AGo5pvHzCWdt5RFfr6Vn8x7ScT9fqEKt1o1TZmRmTY7jo1QsS-JquPm2CiMzN1hvwz7Lk5Bq6SkQFAr2CeAkLSr_t0TFPowLCOv2iAYPOgACALCB&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1Loy1006ql9T1wxv200hS0Qe0Loy1L&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 07:50:42 GMT +Content-Length: 82 + +{"session_key":"UWN5WOXi42zOkf2H+/tUOA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-04T15:52:15.686372+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_vGQJA7jd_hmsi4gswYTJ6AGo5pvHzCWdt5RFfr6Vn8x7ScT9fqEKt1o1TZmRmTY7jo1QsS-JquPm2CiMzN1hvwz7Lk5Bq6SkQFAr2CeAkLSr_t0TFPowLCOv2iAYPOgACALCB&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f19FjGa1xBttI0hD5Ha1P1YHC09FjGr&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 07:51:44 GMT +Content-Length: 82 + +{"session_key":"5/W6QskKd7fIKzeDE9wZBg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-04T15:55:07.149913+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_vGQJA7jd_hmsi4gswYTJ6AGo5pvHzCWdt5RFfr6Vn8x7ScT9fqEKt1o1TZmRmTY7jo1QsS-JquPm2CiMzN1hvwz7Lk5Bq6SkQFAr2CeAkLSr_t0TFPowLCOv2iAYPOgACALCB&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1puJ000iWv8T1ibS300DeizB0puJ0t&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 07:54:36 GMT +Content-Length: 82 + +{"session_key":"5/W6QskKd7fIKzeDE9wZBg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-04T15:56:06.209159+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_vGQJA7jd_hmsi4gswYTJ6AGo5pvHzCWdt5RFfr6Vn8x7ScT9fqEKt1o1TZmRmTY7jo1QsS-JquPm2CiMzN1hvwz7Lk5Bq6SkQFAr2CeAkLSr_t0TFPowLCOv2iAYPOgACALCB&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1865ll2MrRse4kLQol24L3mS2865lc&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 07:55:35 GMT +Content-Length: 82 + +{"session_key":"JAkblxyzPBuVeifdIJaubQ==","openid":"oSsSc7cS4vUprOfY0ZPulgf7_EQ4"} +-------- +NULL +[2024-11-04T16:38:34.726436+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_vGQJA7jd_hmsi4gswYTJ6AGo5pvHzCWdt5RFfr6Vn8x7ScT9fqEKt1o1TZmRmTY7jo1QsS-JquPm2CiMzN1hvwz7Lk5Bq6SkQFAr2CeAkLSr_t0TFPowLCOv2iAYPOgACALCB&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1G8M000Hjn8T17Hm100Ya4tn4G8M0E&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 08:38:03 GMT +Content-Length: 82 + +{"session_key":"NRRo+95q8jNeR1ORU05Gpw==","openid":"oSsSc7UjSAllIJgM7z4RLz8tvaoU"} +-------- +NULL +[2024-11-04T16:38:44.580274+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_vGQJA7jd_hmsi4gswYTJ6AGo5pvHzCWdt5RFfr6Vn8x7ScT9fqEKt1o1TZmRmTY7jo1QsS-JquPm2CiMzN1hvwz7Lk5Bq6SkQFAr2CeAkLSr_t0TFPowLCOv2iAYPOgACALCB&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1lKoll2KTZse49jqml25XNZf0lKolM&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 08:38:13 GMT +Content-Length: 82 + +{"session_key":"NRRo+95q8jNeR1ORU05Gpw==","openid":"oSsSc7UjSAllIJgM7z4RLz8tvaoU"} +-------- +NULL +[2024-11-04T17:55:42.209067+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_9e1WB66dxlXRtMpULcp8_5SUelStDm6sOOrL3PXXIej5NeXdRrBJeM9r24d89DYgydBYxsJfKeJaWskYitwyd7c3jNS__IW_RnczJiMOQ-qg8tMZ7_EuxkrvtZcOPBdABAYZL&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1nDzml2T2que4LmRol2GExKa1nDzmi&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 09:55:11 GMT +Content-Length: 82 + +{"session_key":"dGW93ix6ht0Rl89z+F7nyQ==","openid":"oSsSc7dg92c93yIl9vuzjedLbDUs"} +-------- +NULL +[2024-11-04T17:58:14.716719+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_9e1WB66dxlXRtMpULcp8_5SUelStDm6sOOrL3PXXIej5NeXdRrBJeM9r24d89DYgydBYxsJfKeJaWskYitwyd7c3jNS__IW_RnczJiMOQ-qg8tMZ7_EuxkrvtZcOPBdABAYZL&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1Q28100amY8T14KK200p1Bzo4Q2816&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 04 Nov 2024 09:57:43 GMT +Content-Length: 82 + +{"session_key":"dGW93ix6ht0Rl89z+F7nyQ==","openid":"oSsSc7dg92c93yIl9vuzjedLbDUs"} +-------- +NULL +[2024-11-05T16:00:46.502964+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_eCPnfqhFHqy_xengIO-ZNDgs36HK7wswcOLlmTiTbMZcspPHvzh37_4DSFvtiVJr0AFfy5_E8IlwydhRVTa9mQfQqyAWhmsK-_suRpwNf8d6QmFfSCXFKVwpTJYPLTgAGAWSA&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1GJmGa1yKYsI0rqtIa1l1NCe2GJmGY&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 05 Nov 2024 08:00:15 GMT +Content-Length: 82 + +{"session_key":"wKnTJA75zOP29MqL5n0Ysw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-11T12:00:51.882544+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_Zo0NKo-qMIxYSXQ4ddSxidBaFCSX0W7Vr2otpCyesR7U5rS9GzLsRMoKdE9xfu1a72Uu4mAijJJQFB6lhjsBxR0Lt7d3raQfle1ubvjqQaG34tYdmFAng1p35rkSEJfAEAGSX&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1q5N000KuRaT1ztT100bRsPt3q5N0q&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 11 Nov 2024 04:00:19 GMT +Content-Length: 82 + +{"session_key":"ZrhQY5Qf9hwpVV0cTWDqUg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-13T11:57:16.108837+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_Y9HbRJESc2B-ewZ5HxhYfgwyUVYHz-fbz-a7AyRJscAbXb1SmSQkdomhsEEEil_MFkNYBHy1RqDt2vXxr58j6dbCUDW8c83MZ-clPv0R_IY8H62-3Rx6kt6pAl0PATgAFABMF&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1HaY100uGLaT1PZD200LcsER3HaY1l&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 13 Nov 2024 03:56:42 GMT +Content-Length: 82 + +{"session_key":"qfJ+NwdBI5l+73bSyiNldQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-13T15:30:51.833191+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_iVAaHsvkHQc6QqnUan0eI485j1bcs11cC8tISquWtukpwOkVhnpso8IWt8iDHYokHOXYO0BJguarwQoYBwzxa2oUW5jFgX-uxXJWIDDvZE4erU2VzGRlYrZk2f0UTWeAIAEDG&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1XTw000yFvbT1CJW2003V1W82XTw0n&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 13 Nov 2024 07:30:18 GMT +Content-Length: 82 + +{"session_key":"O9R4wAbrYYY64GWUWNOkmw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-13T15:32:55.074243+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_iVAaHsvkHQc6QqnUan0eI485j1bcs11cC8tISquWtukpwOkVhnpso8IWt8iDHYokHOXYO0BJguarwQoYBwzxa2oUW5jFgX-uxXJWIDDvZE4erU2VzGRlYrZk2f0UTWeAIAEDG&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1hn7Ga1sjtwI0CqnFa1rcB6c4hn7Ga&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 13 Nov 2024 07:32:21 GMT +Content-Length: 82 + +{"session_key":"O9R4wAbrYYY64GWUWNOkmw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-14T11:10:23.643215+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_mJc3hhB-iLRqajSphNoVb8FRal8ECA-ZvaZ59bDfroXJKhaX3ztDEaqvqwZhEpj73pLwR_Z7He7ArSGctD_dXy4we0pQpxhtuuKEygqtPNbTEFg6bjVOMPef-b4MLCjADADCT&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1i1sGa1FKYuI06QpHa1TdIkX3i1sGI&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 14 Nov 2024 03:09:50 GMT +Content-Length: 82 + +{"session_key":"tRJ16rU1UhlactTtmDyFCA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-14T17:24:33.990810+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_9tEA6dwNpD_nkWt1aYHai0a8xfsZvr3KjkJFV7mk9211AN8TTDjPMk3GaAu7fKmQ_7xL-ntKQiKwhJ5t-Z-_RKq5RjghSZXJD6N3GuKJat8UDLnRtAsbFKc1IokLKXhAHAHGT&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c15i8000qMvbT11EQ200LeBxL25i808&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 14 Nov 2024 09:24:00 GMT +Content-Length: 82 + +{"session_key":"Y/5/cQZX4rSnRmKDfupCGw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-15T17:38:43.128011+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_GEWxU0rmcmCNAqEv8sO6SW_LhWYk1i1GHPrXcezrcEa2gAEK4wmbIA-1Mp3lhhvoFH5b7EN3QVwrGP3hSjKEHFP0jWC9croxfkDC0mFR5_cL9NSiY60Lxkup6-4VNKeAEAPHE&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1XHb000xC3cT1psI1008bo4p0XHb0t&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 15 Nov 2024 09:38:09 GMT +Content-Length: 82 + +{"session_key":"J7KU8/ct1Tf/mypzxVIOZQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-19T13:07:14.212468+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_trZbTS8tODpGSLPpevfLPRBJSPYryNrD4LSeA4Az0nJN9rC3ZH82yxSVo4BD14Khs0XenNB3IRUVtpaITdOQ3NvNtyXOWyOXNkOwSn4b_qjOmBGjEtTr-4_gawAAUDaAGAOIR&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1jCs1003tucT1C8c2000wayh1jCs15&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 19 Nov 2024 05:06:39 GMT +Content-Length: 82 + +{"session_key":"u8ZDJFMVa3I5wl8iPjOZ/A==","openid":"oSsSc7cS4vUprOfY0ZPulgf7_EQ4"} +-------- +NULL +[2024-11-19T17:22:05.811859+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_d2Sqf-QiRgim3iccsBVnhtQemKeS0jyYWFePNZjJjNEHXTBp86qeGu1bwqzeJxsF1TdFsNTW6ZcfcqrG4T5e1mePCikGo4_00L8smVmBeQpv8Zx4YdC9GrnEd6kKLVfACAABL&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1BsGkl2LGXxe4kmHkl292gxL2BsGkk&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 19 Nov 2024 09:21:30 GMT +Content-Length: 82 + +{"session_key":"XDDWKFLG0R0k2hHhRQyoXA==","openid":"oSsSc7QRYnOYDLpoQ381dc9FY09w"} +-------- +NULL +[2024-11-20T15:06:41.196759+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_X9Ay5AMeKOKzvHSrFc3Dh3LsEXj2uG-JKMhkxmY-ElYyh1_QrjVxho0Jan0NTrMjxX-8DoPf-N9mHs6S_iZEheWdLUlJOX4e5p2XBN5uu2txkTdOO1NrmkZhSuEWAPdAAAOSH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1DyPFa18z2yI0z37Ga1HSbG11DyPFt&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 20 Nov 2024 07:06:06 GMT +Content-Length: 82 + +{"session_key":"cXYkGt78Vf91jRPaYqHp8w==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-21T09:46:53.714101+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_Bwjvh1RH3kSWwGDPKMBAljh2Qim-keQe3gJC_E_o_kWErhcaugXfN71Fk3FXBIbo3dWZPtaclVPAb6JS_BmWQPpn6ByDIkje4JMIjoYSBR8Hihoh3oqRyx1s-IcLYVfABAANK&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1Hs4100WmDdT1BoJ100dLj600Hs41P&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 21 Nov 2024 01:46:18 GMT +Content-Length: 82 + +{"session_key":"X5pUda8han42MBQqIL7kvw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-21T14:55:08.213300+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_cE8feA5V3zPVDmKw997T-orGK4T3bEJKSsyAKORj5hstn-NaDJTO0IhI_44hCwXghBKohoRvaJBt30AjSCQsVD1TsujgUJPPZS4h0h0x63dqP3q90z7Qp5WuKToBHDaAHAMSO&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1N3h000K7ieT1Mhh400n1otd2N3h0S&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 21 Nov 2024 06:54:32 GMT +Content-Length: 82 + +{"session_key":"Zdk/6e0+whmjy5HqvW1IZA==","openid":"oSsSc7dg92c93yIl9vuzjedLbDUs"} +-------- +NULL +[2024-11-21T17:41:20.922525+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_iaGSr8nxlS6LCgxstOq0odCl47oZ2CQoWU52fe3NHm1lre-nB0fmsZxa8czhmUv-jeLVqs2yaKFh8TWgP_12MttXvQpV2zVyS2spvZP0lTIfJBX_WVPK0ge4q1wNEDjAHAFWV&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1Fx1Ga1NpmyI0KQxHa1Buwex3Fx1GQ&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 21 Nov 2024 09:40:45 GMT +Content-Length: 82 + +{"session_key":"PYks0I0r4kwPNes34AlyKA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-23T19:47:12.705422+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_lYTKFiS_uXrbi1GA9kAif5DyVU0WBdfLMkyAvEJf_DklUj9vRCruRDoaKM6bib-4nOeV04lCiTAYKzgpC4FUFO6cn4VND94hsY9ZL1XFVnVTz-qYIMuvBWiDhXwKIUhADAUSE&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f18eGFa1dBbzI0O33Ja19cqXJ18eGFA&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sat, 23 Nov 2024 11:46:36 GMT +Content-Length: 82 + +{"session_key":"Edipds5pwSpgd25WMnORjg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-24T09:47:36.636054+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_yOXn9DO59F4d9GRN9kAif5DyVU0WBdfLMkyAvPMLUk4Gw8hEHVxq4Jq-_8pVNTSMJJeSUbtR6gxRcKxr-z_A_iWROxXB6l_dsmIRar38YhHPasqDow4dXz8oYQ8NIMdAHAUPT&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e18CkHa1jkDAI0VjqJa1AYo3C18CkHe&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 24 Nov 2024 01:47:00 GMT +Content-Length: 82 + +{"session_key":"31VSyA6oHjO8CDX9dansXQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-24T09:48:06.981913+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_yOXn9DO59F4d9GRN9kAif5DyVU0WBdfLMkyAvPMLUk4Gw8hEHVxq4Jq-_8pVNTSMJJeSUbtR6gxRcKxr-z_A_iWROxXB6l_dsmIRar38YhHPasqDow4dXz8oYQ8NIMdAHAUPT&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e3j06000XH7fT1NEQ000BJCBD2j060o&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 24 Nov 2024 01:47:30 GMT +Content-Length: 82 + +{"session_key":"lK+ajeGDQhyvhSBsob6IDQ==","openid":"oSsSc7SNGPF_6_kPdyOWmU8YWSh0"} +-------- +NULL +[2024-11-24T09:52:13.363886+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_yOXn9DO59F4d9GRN9kAif5DyVU0WBdfLMkyAvPMLUk4Gw8hEHVxq4Jq-_8pVNTSMJJeSUbtR6gxRcKxr-z_A_iWROxXB6l_dsmIRar38YhHPasqDow4dXz8oYQ8NIMdAHAUPT&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f3UTZkl2a41Ae4Qukol24Tu2d1UTZkX&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 24 Nov 2024 01:51:37 GMT +Content-Length: 82 + +{"session_key":"+6jIHkEqkDwFtTpassTQHA==","openid":"oSsSc7SHnQaVs3mSaALiS7OfBS2Q"} +-------- +NULL +[2024-11-24T10:05:58.701465+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_yOXn9DO59F4d9GRN9kAif5DyVU0WBdfLMkyAvPMLUk4Gw8hEHVxq4Jq-_8pVNTSMJJeSUbtR6gxRcKxr-z_A_iWROxXB6l_dsmIRar38YhHPasqDow4dXz8oYQ8NIMdAHAUPT&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1XMhll2Bscze4xKWkl2zKkkB2XMhli&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 24 Nov 2024 02:05:22 GMT +Content-Length: 82 + +{"session_key":"31VSyA6oHjO8CDX9dansXQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-24T10:06:40.475075+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_yOXn9DO59F4d9GRN9kAif5DyVU0WBdfLMkyAvPMLUk4Gw8hEHVxq4Jq-_8pVNTSMJJeSUbtR6gxRcKxr-z_A_iWROxXB6l_dsmIRar38YhHPasqDow4dXz8oYQ8NIMdAHAUPT&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d38bS0w3L3aU333tM3w3UJvK048bS0j&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 24 Nov 2024 02:06:04 GMT +Content-Length: 82 + +{"session_key":"+6jIHkEqkDwFtTpassTQHA==","openid":"oSsSc7SHnQaVs3mSaALiS7OfBS2Q"} +-------- +NULL +[2024-11-24T10:06:49.988149+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_yOXn9DO59F4d9GRN9kAif5DyVU0WBdfLMkyAvPMLUk4Gw8hEHVxq4Jq-_8pVNTSMJJeSUbtR6gxRcKxr-z_A_iWROxXB6l_dsmIRar38YhHPasqDow4dXz8oYQ8NIMdAHAUPT&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e3rG8300qa3hT1ARD300jozKd0rG83A&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Sun, 24 Nov 2024 02:06:13 GMT +Content-Length: 82 + +{"session_key":"+6jIHkEqkDwFtTpassTQHA==","openid":"oSsSc7SHnQaVs3mSaALiS7OfBS2Q"} +-------- +NULL +[2024-11-25T13:09:38.591373+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_FXVcX4L-rcpmHABm4qehTkwn9BivIumh7R-fv10uOCc6QmtGpzjFeOjguuXFXsrZmFyXKzu7h-4OMeTu1x4ei2t4qyjyac158ernUPTgjMBuV9UJSAs_cHjWhiACTDhABAOFP&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1CAMll2xx1ze4qxSkl2MI45k4CAMlb&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 25 Nov 2024 05:09:02 GMT +Content-Length: 82 + +{"session_key":"I/743AVeenDYgnEGyA1+OA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T09:26:12.969727+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1hPq0006JCfT17A7200XloRF2hPq0Q&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 01:25:36 GMT +Content-Length: 82 + +{"session_key":"9g40G46/J4buNFzAGABjUQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T09:32:04.911959+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1xPkll2Ie5Be4HQRml2t0D651xPklq&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 01:31:28 GMT +Content-Length: 82 + +{"session_key":"9g40G46/J4buNFzAGABjUQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T09:40:30.692513+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1roCll2MVNAe4FtGnl2VaJ7m1roClt&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 01:39:54 GMT +Content-Length: 82 + +{"session_key":"Bb9HQ2NUsudA0W0IYfyGFw==","openid":"oSsSc7cS4vUprOfY0ZPulgf7_EQ4"} +-------- +NULL +[2024-11-26T09:41:19.520009+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1uQZ000638gT1Gjs100qFSyr3uQZ0L&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 01:40:42 GMT +Content-Length: 82 + +{"session_key":"Bb9HQ2NUsudA0W0IYfyGFw==","openid":"oSsSc7cS4vUprOfY0ZPulgf7_EQ4"} +-------- +NULL +[2024-11-26T09:56:41.314396+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1DqUll2Z9nze4joBml27x57t4DqUlj&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 01:56:04 GMT +Content-Length: 82 + +{"session_key":"Bb9HQ2NUsudA0W0IYfyGFw==","openid":"oSsSc7cS4vUprOfY0ZPulgf7_EQ4"} +-------- +NULL +[2024-11-26T10:15:51.862879+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1ZQt000BRGfT1B3N200pllex3ZQt0m&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:15:15 GMT +Content-Length: 82 + +{"session_key":"PZmjKdZCOt+/s3XhEQQxIA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:18:52.545267+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1rGnll2ar8Be4Uf2ml2BOSbe1rGnlo&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:18:15 GMT +Content-Length: 82 + +{"session_key":"7D0aOieWAo2elSHVfDzIKQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:41:35.699879+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1kw3100E7egT1hx0300KwZwp1kw31k&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:40:59 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:41:36.806307+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1cOb3w3vOJX33R3d0w3oHFoN1cOb3H&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:41:00 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:41:37.972347+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a10qZ0w34qxV332af2w3akI5000qZ0G&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:41:01 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:41:39.158908+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1ZIOFa1UImAI0Z9DJa1pHljz0ZIOF0&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:41:02 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:41:40.479627+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1KI9200jV7fT19Nd100AY3722KI92s&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:41:03 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:41:41.711863+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1DYQkl2PEzAe4eayml2dyRDg4DYQkr&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:41:05 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:41:42.895217+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1JtM000U9vgT1nB9300DzqsH3JtM0P&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:41:06 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:41:43.995691+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1Kne000Y3XfT1jcO300qa53z2Kne0n&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:41:07 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:41:45.150913+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1S18ll23CiAe4HdSol2oLLGH1S18lr&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:41:08 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:42:28.058052+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1AwM000ITvgT1Aud400OgHkB2AwM0k&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:41:51 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:42:29.269805+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c19ka0w3p6hV33H7XZv3dJa5V39ka0e&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:41:52 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:42:30.420345+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1Lz3100YQegT1u84000bwJZL3Lz31s&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:41:53 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:42:31.662689+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1Wgeml2m9dze4jLMkl2wsEaA2WgemF&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:41:55 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:42:32.823509+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1TwM000CTvgT1fCd300d2j380TwM0u&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:41:56 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:42:33.993998+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f13eXll2Dcuze4PaXml2nTqw813eXle&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:41:57 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:42:35.181161+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1258ll2kljAe4mr2ll2kL1Wi4258lh&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:41:58 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:42:36.293104+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1eJS100dHpfT1033200UDbn93eJS1c&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:41:59 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:42:37.477733+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1bxM000jTvgT1NUO100f8Fm71bxM0M&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:42:00 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:42:38.817527+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1g58ll2lljAe4sW4nl2cGroe4g58lg&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:42:02 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:42:43.207461+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1AA3100YPegT1dDp000KOkcP0AA31H&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:42:06 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:42:44.350477+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1UYf300beriT1VXk400O6eeA2UYf3H&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:42:07 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:42:45.929333+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1eQ5Ga1EzcBI0TFdJa1yxEqv4eQ5GI&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:42:09 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:42:48.159409+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1nor0w3G10V33lq60w3vptNc3nor0I&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:42:11 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:42:49.431158+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1xxg1w3DSaU33tsK1w310f0C1xxg1L&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:42:12 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:42:50.646924+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c14cGll2pqRAe4SRYll2YTWMa14cGlA&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:42:14 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:43:25.136837+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1xF9ol20uGBe4xjnll2h2DAo4xF9oq&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:42:48 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:43:26.347114+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1QwZ0w3l2yV333bx0w3CMZzI3QwZ0-&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:42:49 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:43:27.550722+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1ixv000QDGfT1jOo100IJjVd3ixv0w&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:42:50 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:43:28.849726+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1n5Rkl2eiAAe4zySml2lH1FK0n5RkF&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:42:52 GMT +Content-Length: 82 + +{"session_key":"hh+6OPC25wg0p5sDqUMDAA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:44:34.773418+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f15vr0w3aa0V33Zfg2w37NZ2T15vr0I&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:43:58 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:44:35.970529+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1CBv000wRGfT16HP2000rwZf0CBv0K&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:43:59 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:44:37.183792+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1i3EGa1RpyzI09RgIa1WU7EZ3i3EG2&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:44:00 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:44:38.440876+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1NEM00000wgT1eS4000Ly8O72NEM0Z&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:44:01 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:44:39.786519+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c12svml2Yo2Ae4sR3ll2KXzzk02svm7&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:44:03 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:44:40.922344+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1vyI0w3EiPV33FBR3w3x2vFl0vyI0D&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:44:04 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:44:42.832022+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1EBZ0w3lfyV33m7S2w3o21Wn0EBZ0z&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:44:06 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:44:44.645629+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1dI3100WWegT1O8v100bG9hu4dI31f&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:44:07 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:44:46.909705+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1S0nGa17EVAI0Z9iIa1I6isA0S0nGJ&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:44:10 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:46:08.175436+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f15fcHa1Jo6AI0GYyIa13X0vC25fcHN&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:45:31 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:46:50.378707+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1bTk100epReT1ir4000dXMVO2bTk1S&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:46:13 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:46:56.163135+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1dZm2w3ShtX335Wj2w3VGFZ31dZm29&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:46:19 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:49:24.369870+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1minGa1hiVAI0DEfGa1wolKY1minGF&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:48:47 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:49:28.846883+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1IWg1w3bwaU33JTE3w3GaLdx3IWg1l&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:48:52 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:49:30.095240+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1MoVGa14SgzI0xniJa14Fvgx3MoVGQ&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:48:53 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:49:31.378098+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1fv8ll2ZXiAe44jSkl2n6C6G0fv8lc&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:48:54 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:49:32.689880+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1o3l100LdReT1V7s000Kp49Z2o3l1a&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:48:56 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:50:14.589814+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1eoZ200bWHiT1a4e200cSqO50eoZ2m&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:49:37 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:50:15.790620+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c15Bpll2Z88Be44TTnl2ThaOm05BplJ&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:49:39 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:50:17.092435+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1eHXll2XQtze4a5Wol2OWmaA2eHXlr&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:49:40 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:50:18.534671+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1LlnGa1EcVAI07wUFa1Z3PlQ0LlnG6&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:49:41 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:50:19.857778+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1jy8ll250jAe4CUqol23tvjV1jy8lj&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:49:43 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:51:58.768938+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d13Xr0w3MwYU33FeF2w37SUhE13Xr0C&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:51:22 GMT +Content-Length: 82 + +{"session_key":"EdX7vEh7o9cfZ0OZxD7SSw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:53:22.151402+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1UkC100DVyeT1j0810009xrR0UkC1e&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:52:45 GMT +Content-Length: 82 + +{"session_key":"cZKTAtA8eF3RYIfhNrKOpA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:54:54.553089+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a188s0w3ZSYU333uo1w3usnmD488s0e&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:54:17 GMT +Content-Length: 82 + +{"session_key":"i0a4PzQfRG+blYBeCykuVQ==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:57:51.007551+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1nvy1w3MUST33Rxe4w3HMWal4nvy1U&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:57:14 GMT +Content-Length: 82 + +{"session_key":"aOKhUqpvD1tVNTGhA8LHqg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:58:02.333895+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1mnf000sSWfT1XcO300IRbEg4mnf0q&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:57:25 GMT +Content-Length: 82 + +{"session_key":"aOKhUqpvD1tVNTGhA8LHqg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T10:58:11.516752+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1ZYRkl2RlzAe49oRll2dFUF11ZYRkz&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:57:34 GMT +Content-Length: 82 + +{"session_key":"aOKhUqpvD1tVNTGhA8LHqg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T11:00:35.328605+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b17eqll29y6Be4LyCll2tO2cl47eql7&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 02:59:58 GMT +Content-Length: 82 + +{"session_key":"LNN9yIg/qvnRUSfLD/KvLw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T11:00:47.136461+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1oDh1w3DY8U33KrM1w3Yo2DP1oDh1v&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 03:00:10 GMT +Content-Length: 82 + +{"session_key":"LNN9yIg/qvnRUSfLD/KvLw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T11:02:25.527363+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d13lqll2Hz6Be4Hqwml2bcgGq13lqlT&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 03:01:48 GMT +Content-Length: 82 + +{"session_key":"0+j8Mw9ls+c40/Q1mFn6ag==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T11:02:40.529204+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b11mqll26A6Be4s6Jll2lHaj031mqla&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 03:02:03 GMT +Content-Length: 82 + +{"session_key":"0+j8Mw9ls+c40/Q1mFn6ag==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T11:07:39.074084+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0a1m011w3ruwV33weX3w3KxPSv0m011J&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 03:07:02 GMT +Content-Length: 82 + +{"session_key":"/WgGqaMGod4+SbS1p/VdZw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T11:08:09.029828+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1dZJ0w3WTMV330P14w3dgkg32dZJ0H&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 03:07:32 GMT +Content-Length: 82 + +{"session_key":"nkuKF0eJHkokNdvTgEh0aw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T11:10:55.959505+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f1p95nl2pIKCe4vnAll2Bao1T1p95n2&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 03:10:19 GMT +Content-Length: 82 + +{"session_key":"nkuKF0eJHkokNdvTgEh0aw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T11:14:36.781924+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1fegml2qPaze40qDol2PnFS43fegm9&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 03:14:00 GMT +Content-Length: 82 + +{"session_key":"DqJXOpouXXnnsnGRX16+pA==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T11:15:52.447792+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1oR7Ga1nfaBI0WMFFa1ejspo1oR7Gy&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 03:15:15 GMT +Content-Length: 82 + +{"session_key":"fkFCBKgXlr4haKpEl9jM7g==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T11:20:33.039988+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1xoall2KGgAe465wnl2NvmOr1xoalK&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 03:19:56 GMT +Content-Length: 82 + +{"session_key":"eg+Q90L7Efa7bbiT5wWQPg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T11:21:24.232411+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0e1QZm100TJOeT1d0P100qBqNN2QZm1C&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 03:20:47 GMT +Content-Length: 82 + +{"session_key":"eg+Q90L7Efa7bbiT5wWQPg==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T11:23:48.163086+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4PDFB38ULFGJq7ygb-qkdRRxWR_s40zuK6Wi9MUu2IwRxBVRf69r8OudC4hsF8Dtsr2uqHhF4GVbOpk-v8gfH7s96x8P7Per39I-vjDeudMNwlL9VhQjJWSfjjALQXeAJAVAH&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1QAall2iogAe4n8Qol2OaWuY3QAal3&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 03:23:11 GMT +Content-Length: 82 + +{"session_key":"QC7bcBpyjuMyXuw8OEZXuw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T11:27:59.801315+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_J8NedgN3etoUdCrClRZkphM7QW-pp6Nov5RiIJ9rikG5OSPzWSX_XjsEQFrUzjeqDB3NdnXYQM6gyYEK6lWEM8v3_yZN1qMkNanUO4N2mdGDNSLgdbayDw0bpP0LQFhAFAEJY&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1S8Pml2MmBCe4vTfol2pU6HJ3S8PmP&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 03:27:23 GMT +Content-Length: 82 + +{"session_key":"QC7bcBpyjuMyXuw8OEZXuw==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-26T11:30:12.493703+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_J8NedgN3etoUdCrClRZkphM7QW-pp6Nov5RiIJ9rikG5OSPzWSX_XjsEQFrUzjeqDB3NdnXYQM6gyYEK6lWEM8v3_yZN1qMkNanUO4N2mdGDNSLgdbayDw0bpP0LQFhAFAEJY&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1a80ml2DT1ze4Bcbll2NiZyI3a80mI&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 03:29:35 GMT +Content-Length: 82 + +{"session_key":"Bb9HQ2NUsudA0W0IYfyGFw==","openid":"oSsSc7cS4vUprOfY0ZPulgf7_EQ4"} +-------- +NULL +[2024-11-26T11:39:51.421582+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_J8NedgN3etoUdCrClRZkphM7QW-pp6Nov5RiIJ9rikG5OSPzWSX_XjsEQFrUzjeqDB3NdnXYQM6gyYEK6lWEM8v3_yZN1qMkNanUO4N2mdGDNSLgdbayDw0bpP0LQFhAFAEJY&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1DI0ml2bD2ze4kirml2yNzBU2DI0mk&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 03:39:14 GMT +Content-Length: 82 + +{"session_key":"Aa+oeDWlGVLnVwxzafnGrQ==","openid":"oSsSc7UjSAllIJgM7z4RLz8tvaoU"} +-------- +NULL +[2024-11-26T14:49:50.620588+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_4-fwjHRXq51qft-9Ke6Mmxl-5I2xLXv3WZmUqToc5-75lf4LC20huT35cNPVnZJzy4EoTgtfbOE01tna0PfInQU3UWIjxivBpb9xJJbgsQ1i307f77UfK02tFr4VGQcAJAGRB&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c3Dvt00014FfT1Kp8400KkYsW1Dvt09&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Tue, 26 Nov 2024 06:49:13 GMT +Content-Length: 82 + +{"session_key":"sYaoBNNsNTMO9U0c0a0SBA==","openid":"oSsSc7f13kMwKHcIDhlIMhKY5JpU"} +-------- +NULL +[2024-11-28T16:17:28.545705+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_1hz5hAHtJkXAwCVmdBYm_sD8Q_M4ZF6YeRsRDWaZpVcv8UUboBs4UfXU36cs3NYsB4dFrZjZrPBk4oiivRSs37YrVaHwhtrGjzyfluG7T50xvwAFFhpanwwT-8EBUDeADAGZI&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1Fygll2ngQBe4eQtml2ro4MQ1Fyglz&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 28 Nov 2024 08:16:51 GMT +Content-Length: 82 + +{"session_key":"Ej1vIsPuDTswmtzg1btjwA==","openid":"oSsSc7dg92c93yIl9vuzjedLbDUs"} +-------- +NULL +[2024-11-28T16:17:28.558773+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_1hz5hAHtJkXAwCVmdBYm_sD8Q_M4ZF6YeRsRDWaZpVcv8UUboBs4UfXU36cs3NYsB4dFrZjZrPBk4oiivRSs37YrVaHwhtrGjzyfluG7T50xvwAFFhpanwwT-8EAAKeADAGGQ&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0d1CUm000gCWgT1kLJ1004UnPo2CUm0z&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 28 Nov 2024 08:16:51 GMT +Content-Length: 82 + +{"session_key":"Ej1vIsPuDTswmtzg1btjwA==","openid":"oSsSc7dg92c93yIl9vuzjedLbDUs"} +-------- +NULL +[2024-11-28T16:22:02.276563+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_1hz5hAHtJkXAwCVmdBYm_sD8Q_M4ZF6YeRsRDWaZpVcv8UUboBs4UfXU36cs3NYsB4dFrZjZrPBk4oiivRSs37YrVaHwhtrGjzyfluG7T50xvwAFFhpanwwT-8EAAKeADAGGQ&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1SJIkl2OrhBe4VYcml2S6M0s4SJIkU&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Thu, 28 Nov 2024 08:21:25 GMT +Content-Length: 82 + +{"session_key":"MeduyJNnAj7RFUl1MQoLGQ==","openid":"oSsSc7cS4vUprOfY0ZPulgf7_EQ4"} +-------- +NULL +[2024-11-29T09:31:53.627033+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_kgIRMHLwieu24sQnBdXiqHF8x4BT-hC3LMXsHx7tY3TnDb1VguW26axdd5L5-631WX-Q0BlP4ZSuLpki7nwI0SR_uPd8AwSGg-vtymDuRX3nwDCqP_sjB45FdiYCYCiAGAOKO&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1pb9Ga1E8NBI0VEwFa1bXQTA1pb9GZ&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 29 Nov 2024 01:31:16 GMT +Content-Length: 82 + +{"session_key":"imjeSK2GpiH1Z2mZpVoy2g==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-29T09:49:19.454159+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_kgIRMHLwieu24sQnBdXiqHF8x4BT-hC3LMXsHx7tY3TnDb1VguW26axdd5L5-631WX-Q0BlP4ZSuLpki7nwI0SR_uPd8AwSGg-vtymDuRX3nwDCqP_sjB45FdiYCYCiAGAOKO&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1KoEkl2FbrBe42b7nl2mP5q64KoEkw&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 29 Nov 2024 01:48:41 GMT +Content-Length: 82 + +{"session_key":"imjeSK2GpiH1Z2mZpVoy2g==","openid":"oSsSc7YC7SbSi1Ynj0jzc69b4U-E"} +-------- +NULL +[2024-11-29T15:05:25.153398+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_qtY6lNisKqqwuffF5wWdnGNS3BF0PZRdxpjUkL8cjVQf9ZLb82Nca-AR2o7ORgCAcZco48rVEKFzEGNjaT4kV-5aPmuILPBSB0mmuIt-kNc0074RqpaORBjAewUAQRaAGAGWX&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1fzKGa1n8qCI0DS2Ia1ITt3x0fzKGt&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 29 Nov 2024 07:04:47 GMT +Content-Length: 82 + +{"session_key":"9BOQio4ffQzdJ5s+5cNRmA==","openid":"oSsSc7QRYnOYDLpoQ381dc9FY09w"} +-------- +NULL +[2024-11-29T15:05:25.153423+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=86_qtY6lNisKqqwuffF5wWdnGNS3BF0PZRdxpjUkL8cjVQf9ZLb82Nca-AR2o7ORgCAcZco48rVEKFzEGNjaT4kV-5aPmuILPBSB0mmuIt-kNc0074RqpaORBjAewUWGSaAGACMY&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0f18Yg0w3gxWV33MuZ2w3TENIY18Yg0Y&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Fri, 29 Nov 2024 07:04:47 GMT +Content-Length: 82 + +{"session_key":"9BOQio4ffQzdJ5s+5cNRmA==","openid":"oSsSc7QRYnOYDLpoQ381dc9FY09w"} +-------- +NULL +[2024-12-04T09:16:27.731472+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=87_pGrdlXZnK9ZitXs-5YHNcVoKAPxM90OKvBobSFLxRPk2rzvkdJXjOE6vrksNF2m3nBpn4KbV9sw-dBSTK5bx6FpDW-JrmaBiLj_O8Dy8jOocTGRSUGT5si4vKU4LXWaAEAEQP&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1Rs80w3RWMX33emYZv3ULP3k4Rs80Y&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Wed, 04 Dec 2024 01:15:48 GMT +Content-Length: 82 + +{"session_key":"kYDJ3+QND5ye5cJCUVf/YQ==","openid":"oSsSc7UjSAllIJgM7z4RLz8tvaoU"} +-------- +NULL +[2024-12-09T11:12:35.078627+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=87_wUafkAcdGVqLWLE5QIhrWMPZqNFLarnWg4KvC91rBwevAv_VQImyt3zP41gEWMPXeI93Pc-XKy6rfR5mnadOxlQ0WzsPg3WRbOXBSrlUTXHcEgd1_FrhZsbNv-0TFJdABAAMQ&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0c1idfml2UznEe4lIaml2NcQ8r2idfmC&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 09 Dec 2024 03:11:54 GMT +Content-Length: 82 + +{"session_key":"i2GxlDLwKglMqzBYBDveEQ==","openid":"oSsSc7dg92c93yIl9vuzjedLbDUs"} +-------- +NULL +[2024-12-09T11:12:35.091068+08:00] EasyWeChat.DEBUG: >>>>>>>> +GET /sns/jscode2session?access_token=87_wUafkAcdGVqLWLE5QIhrWMPZqNFLarnWg4KvC91rBwevAv_VQImyt3zP41gEWMPXeI93Pc-XKy6rfR5mnadOxlQ0WzsPg3WRbOXBSrlUTXHcEgd1_FrhZsbNv-0UNHdABAATN&appid=wx5bb9bef074e228c6&secret=bc08e3745ba948bf1781b631578e2cc0&js_code=0b1919ll2YLtFe4Gn4ol2QLuuE4919lP&grant_type=authorization_code HTTP/1.1 +Host: api.weixin.qq.com +User-Agent: GuzzleHttp/7 + + +<<<<<<<< +HTTP/1.1 200 OK +Connection: keep-alive +Content-Type: text/plain +Date: Mon, 09 Dec 2024 03:11:54 GMT +Content-Length: 82 + +{"session_key":"i2GxlDLwKglMqzBYBDveEQ==","openid":"oSsSc7dg92c93yIl9vuzjedLbDUs"} +-------- +NULL diff --git a/application/api/info.php b/application/api/info.php new file mode 100644 index 0000000..88066e2 --- /dev/null +++ b/application/api/info.php @@ -0,0 +1,15 @@ + 'api', + // 模块标题[必填] + 'title' => 'API', + // 模块唯一标识[必填],格式:模块名.开发者标识.module + 'identifier' => 'api.ccu.module', + // 开发者[必填] + 'author' => 'Maurylee', + // 版本[必填],格式采用三段式:主版本号.次版本号.修订版本号 + 'version' => '1.0.0', + // 模块描述[必填] + 'description' => 'api模块',]; \ No newline at end of file diff --git a/application/cms/admin/Advert.php b/application/cms/admin/Advert.php new file mode 100644 index 0000000..9161322 --- /dev/null +++ b/application/cms/admin/Advert.php @@ -0,0 +1,306 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\exception\DbException + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = AdvertModel::where($map)->order($order)->paginate(); + + $btnType = [ + 'class' => 'btn btn-info', + 'title' => '广告分类', + 'icon' => 'fa fa-fw fa-sitemap', + 'href' => url('advert_type/index') + ]; + + $list_type = AdvertTypeModel::where('status', 1)->column('id,name'); + array_unshift($list_type, '默认分类'); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['name', '广告名称', 'text.edit'], + ['typeid', '分类', 'select', $list_type], + ['ad_type', '类型', 'text', '', ['代码', '文字', '图片', 'flash']], + ['timeset', '时间限制', 'text', '', ['永不过期', '限时']], + ['create_time', '创建时间', 'datetime'], + ['update_time', '更新时间', 'datetime'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addTopButton('custom', $btnType) // 添加顶部按钮 + ->addRightButtons(['edit', 'delete' => ['data-tips' => '删除后无法恢复。']]) // 批量添加右侧按钮 + ->addOrder('id,name,typeid,timeset,ad_type,create_time,update_time') + ->setRowList($data_list) // 设置表格数据 + ->addValidate('Advert', 'name') + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Advert'); + if (true !== $result) $this->error($result); + if ($data['ad_type'] != 0) { + $data['link'] == '' && $this->error('链接不能为空'); + Validate::is($data['link'], 'url') === false && $this->error('链接不是有效的url地址'); // true + } + + // 广告类型 + switch ($data['ad_type']) { + case 0: // 代码 + $data['content'] = $data['code']; + break; + case 1: // 文字 + $data['content'] = ''.$data['title'].''; + break; + case 2: // 图片 + $data['content'] = ''.$data['alt'];
+                    }
+                    $data['content'] .= ''; + break; + case 3: // flash + $data['content'] = ''; + $data['content'] = 'success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + $list_type = AdvertTypeModel::where('status', 1)->column('id,name'); + array_unshift($list_type, '默认分类'); + + // 显示添加页面 + return ZBuilder::make('form') + ->setPageTips('如果出现无法添加的情况,可能由于浏览器将本页面当成了广告,请尝试关闭浏览器的广告过滤功能再试。', 'warning') + ->addFormItems([ + ['select', 'typeid', '广告分类', '', $list_type, 0], + ['text', 'tagname', '广告位标识', '由小写字母、数字或下划线组成,不能以数字开头'], + ['text', 'name', '广告位名称'], + ['radio', 'timeset', '时间限制', '', ['永不过期', '在设内时间内有效'], 0], + ['daterange', 'start_time,end_time', '开始时间-结束时间'], + ['radio', 'ad_type', '广告类型', '', ['代码', '文字', '图片', 'flash'], 0], + ['textarea', 'code', '代码', '必填,支持html代码'], + ['image', 'src', '图片', '必须'], + ['text', 'title', '文字内容', '必填'], + ['text', 'link', '链接', '必填'], + ['colorpicker', 'color', '文字颜色', '', '', 'rgb'], + ['text', 'size', '文字大小', '只需填写数字,例如:12,表示12px', '', ['', 'px']], + ['text', 'width', '宽度', '不用填写单位,只需填写具体数字'], + ['text', 'height', '高度', '不用填写单位,只需填写具体数字'], + ['text', 'alt', '图片描述', '即图片alt的值'], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->setTrigger('ad_type', '0', 'code') + ->setTrigger('ad_type', '1', 'title,color,size') + ->setTrigger('ad_type', '2', 'src,alt') + ->setTrigger('ad_type', '2,3', 'width,height') + ->setTrigger('ad_type', '1,2,3', 'link') + ->setTrigger('timeset', '1', 'start_time') + ->fetch(); + } + + /** + * 编辑 + * @param null $id 广告id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function edit($id = null) + { + if ($id === null) $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Advert'); + if (true !== $result) $this->error($result); + + if (AdvertModel::update($data)) { + // 记录行为 + action_log('advert_edit', 'cms_advert', $id, UID, $data['name']); + $this->success('编辑成功', 'index'); + } else { + $this->error('编辑失败'); + } + } + + $list_type = AdvertTypeModel::where('status', 1)->column('id,name'); + array_unshift($list_type, '默认分类'); + + $info = AdvertModel::get($id); + $info['ad_type'] = ['代码', '文字', '图片', 'flash'][$info['ad_type']]; + + // 显示编辑页面 + return ZBuilder::make('form') + ->setPageTips('如果出现无法添加的情况,可能由于浏览器将本页面当成了广告,请尝试关闭浏览器的广告过滤功能再试。', 'warning') + ->addFormItems([ + ['hidden', 'id'], + ['hidden', 'tagname'], + ['static', 'tagname', '广告位标识'], + ['static', 'ad_type', '广告类型'], + ['text', 'name', '广告位名称'], + ['select', 'typeid', '广告分类', '', $list_type], + ['radio', 'timeset', '时间限制', '', ['永不过期', '在设内时间内有效']], + ['daterange', 'start_time,end_time', '开始时间-结束时间'], + ['textarea', 'content', '广告内容'], + ['radio', 'status', '立即启用', '', ['否', '是']] + ]) + ->setTrigger('timeset', '1', 'start_time') + ->setFormData($info) + ->fetch(); + } + + /** + * 删除广告 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function delete($record = []) + { + return $this->setStatus('delete'); + } + + /** + * 启用广告 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用广告 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置广告状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $advert_name = AdvertModel::where('id', 'in', $ids)->column('name'); + return parent::setStatus($type, ['advert_'.$type, 'cms_advert', 0, UID, implode('、', $advert_name)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $advert = AdvertModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $advert . '),新值:(' . $value . ')'; + return parent::quickEdit(['advert_edit', 'cms_advert', $id, UID, $details]); + } +} diff --git a/application/cms/admin/AdvertType.php b/application/cms/admin/AdvertType.php new file mode 100644 index 0000000..6bcd6ee --- /dev/null +++ b/application/cms/admin/AdvertType.php @@ -0,0 +1,204 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\exception\DbException + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = AdvertTypeModel::where($map)->order($order)->paginate(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['name' => '分类名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['name', '分类名称', 'text.edit'], + ['create_time', '创建时间', 'datetime'], + ['update_time', '更新时间', 'datetime'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->setTableName('cms_advert_type') + ->addTopButton('back', ['href' => url('advert/index')]) // 批量添加顶部按钮 + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons(['edit', 'delete' => ['data-tips' => '删除后无法恢复。']]) // 批量添加右侧按钮 + ->addOrder('id,name,create_time,update_time') + ->setRowList($data_list) // 设置表格数据 + ->addValidate('AdvertType', 'name') + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'AdvertType'); + if(true !== $result) $this->error($result); + + if ($type = AdvertTypeModel::create($data)) { + // 记录行为 + action_log('advert_type_add', 'cms_advert_type', $type['id'], UID, $data['name']); + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->setPageTips('如果出现无法添加的情况,可能由于浏览器将本页面当成了广告,请尝试关闭浏览器的广告过滤功能再试。', 'warning') + ->addFormItems([ + ['text', 'name', '分类名称'], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->fetch(); + } + + /** + * 编辑 + * @param null $id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function edit($id = null) + { + if ($id === null) $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'AdvertType'); + if(true !== $result) $this->error($result); + + if (AdvertTypeModel::update($data)) { + // 记录行为 + action_log('advert_type_edit', 'cms_advert_type', $id, UID, $data['name']); + $this->success('编辑成功', 'index'); + } else { + $this->error('编辑失败'); + } + } + + $info = AdvertTypeModel::get($id); + + // 显示编辑页面 + return ZBuilder::make('form') + ->setPageTips('如果出现无法编辑的情况,可能由于浏览器将本页面当成了广告,请尝试关闭浏览器的广告过滤功能再试。', 'warning') + ->addFormItems([ + ['hidden', 'id'], + ['text', 'name', '分类名称'], + ['radio', 'status', '立即启用', '', ['否', '是']] + ]) + ->setFormdata($info) + ->fetch(); + } + + /** + * 删除广告分类 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function delete($record = []) + { + return $this->setStatus('delete'); + } + + /** + * 启用广告分类 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用广告分类 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置广告分类状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record 日志记录 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $type_name = AdvertTypeModel::where('id', 'in', $ids)->column('name'); + return parent::setStatus($type, ['advert_type_'.$type, 'cms_advert_type', 0, UID, implode('、', $type_name)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $type = AdvertTypeModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $type . '),新值:(' . $value . ')'; + return parent::quickEdit(['advert_type_edit', 'cms_advert_type', $id, UID, $details]); + } +} diff --git a/application/cms/admin/Column.php b/application/cms/admin/Column.php new file mode 100644 index 0000000..cd2ec0d --- /dev/null +++ b/application/cms/admin/Column.php @@ -0,0 +1,287 @@ + + * @return mixed + * @throws \think\Exception + */ + public function index() + { + // 查询 + $map = $this->getMap(); + + // 数据列表 + $data_list = ColumnModel::where($map)->column(true); + if (empty($map)) { + $data_list = Tree::config(['title' => 'name'])->toList($data_list); + } + + // 自定义按钮 + $btnMove = [ + 'class' => 'btn btn-xs btn-default js-move-column', + 'icon' => 'fa fa-fw fa-arrow-circle-right', + 'title' => '移动栏目' + ]; + $btnAdd = [ + 'class' => 'btn btn-xs btn-default', + 'icon' => 'fa fa-fw fa-plus', + 'title' => '新增子栏目', + 'href' => url('add', ['pid' => '__id__']) + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['name' => '栏目名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['icon', '图标', 'icon'], + ['name', '栏目名称', 'callback', function($value, $data){ + return isset($data['title_prefix']) ? $data['title_display'] : $value; + }, '__data__'], + ['model', '内容模型', 'select', DocumentModel::getTitleList()], + ['rank_auth', '浏览权限', 'select', RoleModel::getTree(null, '开放浏览')], + ['hide', '是否隐藏', 'yesno'], + ['post_auth', '支持投稿', 'yesno'], + ['create_time', '创建时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable') // 批量添加顶部按钮 + ->addRightButton('custom', $btnAdd) + ->addRightButton('edit') // 添加右侧按钮 +// ->addRightButton('custom', $btnMove) + ->addRightButton('delete', ['data-tips' => '删除栏目前,请确保无子栏目和文档!']) // 添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + /** + * 新增栏目 + * @param int $pid 父级id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function add($pid = 0) + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Column'); + if(true !== $result) $this->error($result); + + if ($column = ColumnModel::create($data)) { + cache('cms_column_list', null); + // 记录行为 + action_log('column_add', 'cms_column', $column['id'], UID, $data['name']); + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + $template_list = File::get_dirs(Env::get('app_path').'cms/view/column/')['file']; + $template_detail = File::get_dirs(Env::get('app_path').'cms/view/document/')['file']; + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['select', 'pid', '所属栏目', '必选', ColumnModel::getTreeList(), $pid], + ['text', 'name', '栏目名称', '必填'], + ['radio', 'model', '内容模型', '必选', DocumentModel::getTitleList()], + ['radio', 'type', '栏目属性', '', ['最终列表栏目', '外部链接'], 0], + ['text', 'url', '链接', '可以填写完整的url,如:http://www.dolphinphp.com,也可以填写 模块/控制器/操作,如:cms/index/index'], + ['radio', 'target', '打开方式', '', ['_self' => '当前窗口', '_blank' => '新窗口'], '_self'], +// ['select', 'index_template', '封面页模板', '可选'], + ['select', 'list_template', '列表页模板', '可选,模板目录: cms/view/column', parse_array($template_list)], + ['select', 'detail_template', '详情页模板', '可选,模板目录: cms/view/document', parse_array($template_detail)], + ['ckeditor', 'content', '栏目内容', '可作为单页使用'], + ['icon', 'icon', '图标'], + ['radio', 'post_auth', '是否支持投稿', '是否允许前台用户投稿', ['禁止投稿', '允许投稿'], 1], + ['radio', 'hide', '是否隐藏栏目', '隐藏后前台不可见', ['显示', '隐藏'], 0], + ['select', 'rank_auth', '浏览权限', '', RoleModel::getTree(null, '开放浏览'), 0], + ['radio', 'status', '立即启用', '', ['否', '是'], 1], + ['text', 'sort', '排序', '', 100], + ]) + ->setTrigger('type', '0,2', 'index_template,list_template,detail_template') + ->setTrigger('type', '1', 'url,target') + ->fetch(); + } + + /** + * 编辑栏目 + * @param string $id 栏目id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function edit($id = '') + { + if ($id === 0) $this->error('参数错误'); + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Column'); + // 验证失败 输出错误信息 + if(true !== $result) $this->error($result); + + if (ColumnModel::update($data)) { + // 记录行为 + action_log('column_edit', 'cms_column', $id, UID, $data['name']); + $this->success('编辑成功', 'index'); + } else { + $this->error('编辑失败'); + } + } + + // 获取数据 + $info = ColumnModel::get($id); + + $template_list = File::get_dirs(Env::get('app_path').'cms/view/column/')['file']; + $template_detail = File::get_dirs(Env::get('app_path').'cms/view/document/')['file']; + + // 显示编辑页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'id'], + ['select', 'pid', '所属栏目', '必选', ColumnModel::getTreeList($id)], + ['text', 'name', '栏目名称', '必填'], + ['radio', 'model', '内容模型', '必选', DocumentModel::getTitleList()], + ['radio', 'type', '栏目属性', '', ['最终列表栏目', '外部链接'], 0], + ['text', 'url', '链接', '可以填写完整的url,如:http://www.dolphinphp.com,也可以填写 模块/控制器/操作,如:cms/index/index'], + ['radio', 'target', '打开方式', '', ['_self' => '当前窗口', '_blank' => '新窗口'], '_self'], +// ['select', 'index_template', '封面页模板', '可选'], + ['select', 'list_template', '列表页模板', '可选,模板目录: cms/view/column', parse_array($template_list)], + ['select', 'detail_template', '详情页模板', '可选,模板目录: cms/view/document', parse_array($template_detail)], + ['ckeditor', 'content', '栏目内容', '可作为单页使用'], + ['icon', 'icon', '图标'], + ['radio', 'post_auth', '是否支持投稿', '是否允许前台用户投稿', ['禁止投稿', '允许投稿']], + ['radio', 'hide', '是否隐藏栏目', '隐藏后前台不可见', ['显示', '隐藏'], 0], + ['select', 'rank_auth', '浏览权限', '', RoleModel::getTree(null, '开放浏览')], + ['radio', 'status', '立即启用', '', ['否', '是']], + ['text', 'sort', '排序'], + ]) + ->setTrigger('type', '0,2', 'index_template,list_template,detail_template') + ->setTrigger('type', '1', 'url,target') + ->setFormData($info) + ->fetch(); + } + + /** + * 删除栏目 + * @param null $ids 栏目id + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + * @throws \think\exception\PDOException + */ + public function delete($ids = null) + { + if ($ids === null) $this->error('参数错误'); + + // 检查是否有子栏目 + if (ColumnModel::where('pid', $ids)->find()) { + $this->error('请先删除或移动该栏目下的子栏目'); + } + + // 检查是否有文档 + if (Document::where('cid', $ids)->find()) { + $this->error('请先删除或移动该栏目下的所有文档'); + } + + // 删除并记录日志 + $column_name = get_column_name($ids); + return parent::delete(['column_delete', 'cms_column', 0, UID, $column_name]); + } + + /** + * 启用栏目 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用栏目 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置栏目状态:删除、禁用、启用 + * @param string $type 类型:enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $column_delete = is_array($ids) ? '' : $ids; + $column_names = ColumnModel::where('id', 'in', $ids)->column('name'); + return parent::setStatus($type, ['column_'.$type, 'cms_column', $column_delete, UID, implode('、', $column_names)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $column = ColumnModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $column . '),新值:(' . $value . ')'; + return parent::quickEdit(['column_edit', 'cms_column', $id, UID, $details]); + } +} diff --git a/application/cms/admin/Content.php b/application/cms/admin/Content.php new file mode 100644 index 0000000..5137096 --- /dev/null +++ b/application/cms/admin/Content.php @@ -0,0 +1,137 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function _empty() + { + cookie('__forward__', $_SERVER['REQUEST_URI']); + $model_name = $this->request->action(); + $model = Db::name('cms_model')->where('name', $model_name)->find(); + if (!$model) $this->error('找不到该内容'); + + // 独立模型 + if ($model['type'] == 2) { + $table_name = substr($model['table'], strlen(config('database.prefix'))); + + // 查询 + $map = $this->getMap(); + $map[] = ['trash', '=', 0]; + + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = Db::view($table_name, true) + ->view("cms_column", ['name' => 'column_name'], 'cms_column.id='.$table_name.'.cid', 'left') + ->view("admin_user", 'username', 'admin_user.id='.$table_name.'.uid', 'left') + ->where($map) + ->order($order) + ->paginate(); + + $trash_count = Db::table($model['table'])->where('trash', 1)->count(); + + // 自定义按钮 + $btnRecycle = [ + 'title' => '回收站('.$trash_count.')', + 'icon' => 'fa fa-trash', + 'class' => 'btn btn-info', + 'href' => url('recycle/index', ['model' => $model['id']]) + ]; + $columns = Db::name('cms_column')->where(['model' => $model['id']])->column('id,name'); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题', 'cms_column.name' => '栏目名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['title', '标题'], + ['cid', '栏目名称', 'select', $columns], + ['view', '点击量'], + ['username', '发布人'], + ['update_time', '更新时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->setTableName($table_name) + ->addTopButton('add', ['href' => url('document/add', ['model' => $model['id']])]) // 添加顶部按钮 + ->addTopButton('enable', ['href' => url('document/enable', ['table' => $table_name])]) // 添加顶部按钮 + ->addTopButton('disable', ['href' => url('document/disable', ['table' => $table_name])]) // 添加顶部按钮 + ->addTopButton('delete', ['href' => url('document/delete', ['table' => $table_name])]) // 添加顶部按钮 + ->addTopButton('custom', $btnRecycle) // 添加顶部按钮 + ->addRightButton('edit', ['href' => url('document/edit', ['model' => $model['id'], 'id' => '__id__'])]) // 添加右侧按钮 + ->addRightButton('delete', ['href' => url('document/delete', ['ids' => '__id__', 'table' => $table_name])]) // 添加右侧按钮 + ->addOrder('id,title,cid,view,username,update_time') + ->addFilter('cid', $columns) + ->addFilter(['username' => 'admin_user']) + ->addFilterMap(['cid' => ['model' => $model['id']]]) + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } else { + // 查询 + $map = $this->getMap(); + $map[] = ['cms_document.trash', '=', 0]; + $map[] = ['cms_document.model', '=', $model['id']]; + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = Document::getList($map, $order); + + $columns = Db::name('cms_column')->where(['model' => $model['id']])->column('id,name'); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题', 'cms_column.name' => '栏目名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['title', '标题'], + ['cid', '栏目名称', 'select', $columns], + ['view', '点击量'], + ['username', '发布人'], + ['update_time', '更新时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->setTableName('cms_document') + ->addTopButton('add', ['href' => url('document/add', ['model' => $model['id']])]) // 添加顶部按钮 + ->addTopButton('enable', ['href' => url('document/enable', ['table' => 'cms_document'])]) // 添加顶部按钮 + ->addTopButton('disable', ['href' => url('document/disable', ['table' => 'cms_document'])]) // 添加顶部按钮 + ->addTopButton('delete', ['href' => url('document/delete', ['table' => 'cms_document'])]) // 添加顶部按钮 + ->addRightButton('edit', ['href' => url('document/edit', ['id' => '__id__'])]) // 添加右侧按钮 + ->addRightButton('delete', ['href' => url('document/delete', ['ids' => '__id__', 'table' => 'cms_document'])]) // 添加右侧按钮 + ->addOrder('id,title,cid,view,username,update_time') + ->addFilter('cid', $columns) + ->addFilter(['username' => 'admin_user']) + ->addFilterMap(['cid' => ['model' => $model['id']]]) + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + } +} diff --git a/application/cms/admin/Document.php b/application/cms/admin/Document.php new file mode 100644 index 0000000..0d0a415 --- /dev/null +++ b/application/cms/admin/Document.php @@ -0,0 +1,345 @@ + + */ + public function index() + { + cookie('__forward__', $_SERVER['REQUEST_URI']); + // 查询 + $map = $this->getMap(); + $map[] = ['cms_document.trash', '=', 0]; + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = DocumentModel::getList($map, $order); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题', 'cms_column.name' => '栏目名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['title', '标题'], + ['column_name', '栏目名称'], + ['view', '点击量'], + ['username', '发布人'], + ['update_time', '更新时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons(['edit', 'delete']) // 批量添加右侧按钮 + ->addOrder(['column_name' => 'cms_document.cid']) + ->addOrder('id,title,view,username,update_time') + ->addFilter(['column_name' => 'cms_column.name', 'username' => 'admin_user']) + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + /** + * 添加文档 + * @param int $cid 栏目id + * @param string $model 模型id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function add($cid = 0, $model = '') + { + // 保存文档数据 + if ($this->request->isAjax()) { + $DocumentModel = new DocumentModel(); + if (false === $DocumentModel->saveData()) { + $this->error($DocumentModel->getError()); + } + $this->success('新增成功', cookie('__forward__')); + } + + // 第二步,填写文档信息 + if ($cid > 0) { + cookie('__forward__', url('add', ['cid' => $cid])); + // 获取栏目数据 + $column = ColumnModel::getInfo($cid); + + // 独立模型只取该模型的字段,不包含系统字段 + $where = []; + if (get_model_type($column['model']) == 2) { + $where[] = ['model', '=', $column['model']]; + } else { + $where[] = ['model', 'in', [0, $column['model']]]; + } + + // 获取文档模型字段 + $where[] = ['status', '=', 1]; + $where[] = ['show', '=', 1]; + $fields = FieldModel::where($where)->order('sort asc,id asc')->column(true); + + foreach ($fields as &$value) { + // 解析options + if ($value['options'] != '') { + $value['options'] = parse_attr($value['options']); + } + + switch ($value['type']) { + case 'linkage':// 解析联动下拉框异步请求地址 + if (!empty($value['ajax_url']) && substr($value['ajax_url'], 0, 4) != 'http') { + $value['ajax_url'] = url($value['ajax_url']); + } + break; + case 'date': + case 'time': + case 'datetime': + $value['value'] = ''; + break; + case 'bmap': + $value['level'] = $value['level'] == 0 ? 12 : $value['level']; + break; + case 'colorpicker': + $value['mode'] = 'rgba'; + break; + } + } + + // 添加额外表单项信息 + $extra_field = [ + ['name' => 'cid', 'title' => '所属栏目', 'type' => 'static', 'value' => $column['name']], + ['name' => 'cid', 'type' => 'hidden', 'value' => $cid], + ['name' => 'model', 'type' => 'hidden', 'value' => $column['model']] + ]; + $fields = array_merge($extra_field, $fields); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setFormItems($fields) + ->hideBtn('back') + ->fetch(); + } + + // 第一步,选择栏目 + if ($model == '') { + $columns = ColumnModel::getTreeList(0, false); + } else { + // 获取相同内容模型的栏目 + $columns = Db::name('cms_column')->where('model', $model)->order('pid,id')->column('id,name,pid'); + $columns = Tree::config(['title' => 'name'])->toList($columns, current($columns)['pid']); + $result = []; + foreach ($columns as $column) { + $result[$column['id']] = $column['title_display']; + } + $columns = $result; + } + return ZBuilder::make('form') + ->addFormItem('select', 'cid', '选择栏目', '请选择栏目', $columns) + ->setBtnTitle('submit', '下一步') + ->hideBtn('back') + ->isAjax(false) + ->fetch(); + } + + /** + * 编辑文档 + * @param null $id 文档id + * @param string $model 模型id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function edit($id = null, $model = '') + { + if ($id === null) $this->error('参数错误'); + + // 保存文档数据 + if ($this->request->isPost()) { + $DocumentModel = new DocumentModel(); + $result = $DocumentModel->saveData(); + if (false === $result) { + $this->error($DocumentModel->getError()); + } + $this->success('编辑成功', cookie('__forward__')); + } + + // 获取数据 + $info = DocumentModel::getOne($id, $model); + + // 独立模型只取该模型的字段,不包含系统字段 + $where = []; + if ($model != '') { + $where[] = ['model', '=', $model]; + } else { + $where[] = ['model', 'in', [0, $info['model']]]; + } + + // 用于查询内容模型栏目 + $map = $where; + + // 获取文档模型字段 + $where[] = ['status', '=', 1]; + $where[] = ['show', '=', 1]; + $fields = FieldModel::where($where)->order('sort asc,id asc')->column(true); + + foreach ($fields as $id => &$value) { + // 解析options + if ($value['options'] != '') { + $value['options'] = parse_attr($value['options']); + } + // 日期时间 + switch ($value['type']) { + case 'date': + $info[$value['name']] = format_time($info[$value['name']], 'Y-m-d'); + break; + case 'time': + $info[$value['name']] = format_time($info[$value['name']], 'H:i:s'); + break; + case 'datetime': + $info[$value['name']] = empty($info[$value['name']]) ? '' : format_time($info[$value['name']]); + break; + case 'bmap': + $value['level'] = $value['level'] == 0 ? 12 : $value['level']; + break; + case 'colorpicker': + $value['mode'] = 'rgba'; + break; + } + } + + // 获取相同内容模型的栏目 + $columns = Db::name('cms_column')->where($map)->whereOr('model', $info['model'])->order('pid,id')->column('id,name,pid'); + $columns = Tree::config(['title' => 'name'])->toList($columns, current($columns)['pid']); + $result = []; + foreach ($columns as $column) { + $result[$column['id']] = $column['title_display']; + } + $columns = $result; + + + // 添加额外表单项信息 + $extra_field = [ + ['name' => 'id', 'type' => 'hidden'], + ['name' => 'cid', 'title' => '所属栏目', 'type' => 'select', 'options' => $columns], + ['name' => 'model', 'type' => 'hidden'] + ]; + $fields = array_merge($extra_field, $fields); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setFormItems($fields) + ->setFormData($info) + ->fetch(); + } + + /** + * 删除文档(不是彻底删除,而是移动到回收站) + * @param null $ids 文档id + * @param string $table 数据表 + * @author 蔡伟明 <314013107@qq.com> + */ + public function delete($ids = null, $table = '') + { + if ($ids === null) $this->error('参数错误'); + + $document_id = is_array($ids) ? '' : $ids; + $document_title = Db::name($table)->where('id', 'in', $ids)->column('title'); + + // 移动文档到回收站 + if (false === Db::name($table)->where('id', 'in', $ids)->setField('trash', 1)) { + $this->error('删除失败'); + } + + // 删除并记录日志 + action_log('document_trash', $table, $document_id, UID, implode('、', $document_title)); + $this->success('删除成功'); + } + + /** + * 启用文档 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用文档 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置文档状态:删除、禁用、启用 + * @param string $type 类型:enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function setStatus($type = '', $record = []) + { + $table_token = input('param._t', ''); + $table_token == '' && $this->error('缺少参数'); + !session('?'.$table_token) && $this->error('参数错误'); + + $table_data = session($table_token); + $table_name = $table_data['table']; + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $document_id = is_array($ids) ? '' : $ids; + $document_title = Db::name($table_name)->where('id', 'in', $ids)->column('title'); + return parent::setStatus($type, ['document_'.$type, 'cms_document', $document_id, UID, implode('、', $document_title)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $table_token = input('param._t', ''); + $table_token == '' && $this->error('缺少参数'); + !session('?'.$table_token) && $this->error('参数错误'); + + $table_data = session($table_token); + $table = $table_data['table']; + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $document = Db::name($table)->where('id', $id)->value($field); + $details = '表名(' . $table . '),字段(' . $field . '),原值(' . $document . '),新值:(' . $value . ')'; + return parent::quickEdit(['document_edit', 'cms_document', $id, UID, $details]); + } +} diff --git a/application/cms/admin/Field.php b/application/cms/admin/Field.php new file mode 100644 index 0000000..9930146 --- /dev/null +++ b/application/cms/admin/Field.php @@ -0,0 +1,330 @@ + + */ + public function index($id = null) + { + $id === null && $this->error('参数错误'); + cookie('__forward__', $_SERVER['REQUEST_URI']); + + // 查询 + $map = $this->getMap(); + $map[]=['model','=',$id]; + // 数据列表 + $data_list = FieldModel::where($map)->order('id desc')->paginate(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['name' => '名称', 'title' => '标题']) // 设置搜索框 + ->setPageTips('【显示】表示新增或编辑文档时是否显示该字段
    【启用】表示前台是否显示') + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['name', '名称'], + ['title', '标题'], + ['type', '类型', 'text', '', config('form_item_type')], + ['create_time', '创建时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['show', '显示', 'switch'], + ['status', '启用', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('back', ['href' => url('model/index')]) // 批量添加顶部按钮 + ->addTopButton('add', ['href' => url('add', ['model' => $id])]) // 添加顶部按钮 + ->addTopButtons('enable,disable') // 批量添加顶部按钮 + ->addRightButtons('edit,delete') // 批量添加右侧按钮 + ->replaceRightButton(['fixed' => 1], '') + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + /** + * 新增字段 + * @param string $model 文档模型id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function add($model = '') + { + // 内容模型类别[0-系统,1-普通,2-独立] + $model_type = get_model_type($model); + + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 非独立模型需验证字段名称是否为aid + if ($model_type != 2) { + // 非独立模型需验证新增的字段是否被系统占用 + if ($data['name'] == 'aid' || is_default_field($data['name'])) { + $this->error('字段名称已存在'); + } + } + + $result = $this->validate($data, 'Field'); + if(true !== $result) $this->error($result); + + // 如果是快速联动 + switch ($data['type']) { + case 'linkages': + $data['key'] = $data['key'] == '' ? 'id' : $data['key']; + $data['pid'] = $data['pid'] == '' ? 'pid' : $data['pid']; + $data['level'] = $data['level'] == '' ? '2' : $data['level']; + $data['option'] = $data['option'] == '' ? 'name' : $data['option']; + break; + case 'number': + $data['type'] = 'text'; + break; + case 'bmap': + $data['level'] = !$data['level'] ? 12 : $data['level']; + break; + } + + if ($field = FieldModel::create($data)) { + $FieldModel = new FieldModel(); + // 添加字段 + if ($FieldModel->newField($data)) { + // 记录行为 + $details = '详情:文档模型('.get_model_title($data['model']).')、字段名称('.$data['name'].')、字段标题('.$data['title'].')、字段类型('.$data['type'].')'; + action_log('field_add', 'cms_field', $field['id'], UID, $details); + // 清除缓存 + cache('cms_system_fields', null); + $this->success('新增成功', cookie('__forward__')); + } else { + // 添加失败,删除新增的数据 + FieldModel::destroy($field['id']); + $this->error($FieldModel->getError()); + } + } else { + $this->error('新增失败'); + } + } + + if ($model_type != 2) { + $field_exist = Db::name('cms_field')->where('model', 'in', [0, $model])->column('name'); + $field_exist[] = 'aid'; + } else { + $field_exist = ['id','cid','uid','title','model','create_time','update_time','sort','status','view','trash']; + } + + // 显示添加页面 + return ZBuilder::make('form') + ->setPageTips('以下字段名称已存在,请不要建立同名的字段:
    '. implode('、', $field_exist)) + ->addFormItems([ + ['hidden', 'model', $model], + ['text', 'name', '字段名称', '由小写英文字母和下划线组成'], + ['text', 'title', '字段标题', '可填写中文'], + ['select', 'type', '字段类型', '', config('form_item_type')], + ['text', 'define', '字段定义', '可根据实际需求自行填写或修改,但必须是正确的sql语法'], + ['text', 'value', '字段默认值'], + ['textarea', 'options', '额外选项', '用于单选、多选、下拉、联动等类型'], + ['text', 'ajax_url', '异步请求地址', "如请求的地址是 url('ajax/getCity'),那么只需填写 ajax/getCity,或者直接填写以 http开头的url地址"], + ['text', 'next_items', '下一级联动下拉框的表单名', "与当前有关联的下级联动下拉框名,多个用逗号隔开,如:area,other"], + ['text', 'param', '请求参数名', "联动下拉框请求参数名,默认为配置名称"], + ['text', 'level', '级别', '如果类型为【快速联动下拉框】则表示需要显示的级别数量,默认为2。如果类型为【百度地图】,则表示地图默认缩放级别,建议设置为12', 2], + ['text', 'table', '表名', '要查询的表,里面必须含有id、name、pid三个字段,其中id和name字段可在下面重新定义'], + ['text', 'pid', '父级id字段名', '即表中的父级ID字段名,如果表中的主键字段名为pid则可不填写'], + ['text', 'key', '键字段名', '即表中的主键字段名,如果表中的主键字段名为id则可不填写'], + ['text', 'option', '值字段名', '下拉菜单显示的字段名,如果表中的该字段名为name则可不填写'], + ['text', 'ak', 'APPKEY', '百度编辑器APPKEY'], + ['text', 'format', '格式'], + ['textarea', 'tips', '字段说明', '字段补充说明'], + ['radio', 'fixed', '是否为固定字段', '如果为 固定字段 则添加后不可修改', ['否', '是'], 0], + ['radio', 'show', '是否显示', '新增或编辑时是否显示该字段', ['否', '是'], 1], + ['radio', 'status', '立即启用', '', ['否', '是'], 1], + ['text', 'sort', '排序', '', 100], + ]) + ->setTrigger('type', 'linkage', 'ajax_url,next_items,param') + ->setTrigger('type', 'linkages', 'table,pid,key,option') + ->setTrigger('type', 'bmap', 'ak') + ->setTrigger('type', 'linkages,bmap', 'level') + ->setTrigger('type', 'masked,date,time,datetime', 'format') + ->setTrigger('type', 'checkbox,radio,array,select,linkage,linkages', 'options') + ->js('field') + ->fetch(); + } + + /** + * 编辑字段 + * @param null $id 字段id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function edit($id = null) + { + if ($id === null) $this->error('参数错误'); + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Field'); + if(true !== $result) $this->error($result); + + // 如果是快速联动 + if ($data['type'] == 'linkages') { + $data['key'] = $data['key'] == '' ? 'id' : $data['key']; + $data['pid'] = $data['pid'] == '' ? 'pid' : $data['pid']; + $data['level'] = $data['level'] == '' ? '2' : $data['level']; + $data['option'] = $data['option'] == '' ? 'name' : $data['option']; + } + // 如果是百度地图 + if ($data['type'] == 'bmap') { + $data['level'] = !$data['level'] ? 12 : $data['level']; + } + + // 更新字段信息 + $FieldModel = new FieldModel(); + if ($FieldModel->updateField($data)) { + if ($FieldModel->isUpdate(true)->save($data)) { + // 记录行为 + action_log('field_edit', 'cms_field', $id, UID, $data['name']); + $this->success('字段更新成功', cookie('__forward__')); + } + } + $this->error('字段更新失败'); + } + + // 获取数据 + $info = FieldModel::get($id); + + // 显示编辑页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'id'], + ['hidden', 'model'], + ['text', 'name', '字段名称', '由小写英文字母和下划线组成'], + ['text', 'title', '字段标题', '可填写中文'], + ['select', 'type', '字段类型', '', config('form_item_type')], + ['text', 'define', '字段定义', '可根据实际需求自行填写或修改,但必须是正确的sql语法'], + ['text', 'value', '字段默认值'], + ['textarea', 'options', '额外选项', '用于单选、多选、下拉、联动等类型'], + ['text', 'ajax_url', '异步请求地址', "如请求的地址是 url('ajax/getCity'),那么只需填写 ajax/getCity,或者直接填写以 http开头的url地址"], + ['text', 'next_items', '下一级联动下拉框的表单名', "与当前有关联的下级联动下拉框名,多个用逗号隔开,如:area,other"], + ['text', 'param', '请求参数名', "联动下拉框请求参数名,默认为配置名称"], + ['text', 'level', '级别', '如果类型为【快速联动下拉框】则表示需要显示的级别数量,默认为2。如果类型为【百度地图】,则表示地图默认缩放级别,建议设置为12'], + ['text', 'table', '表名', '要查询的表,里面必须含有id、name、pid三个字段,其中id和name字段可在下面重新定义'], + ['text', 'pid', '父级id字段名', '即表中的父级ID字段名,如果表中的主键字段名为pid则可不填写'], + ['text', 'key', '键字段名', '即表中的主键字段名,如果表中的主键字段名为id则可不填写'], + ['text', 'option', '值字段名', '下拉菜单显示的字段名,如果表中的该字段名为name则可不填写'], + ['text', 'ak', 'APPKEY', '百度编辑器APPKEY'], + ['text', 'format', '格式'], + ['textarea', 'tips', '字段说明', '字段补充说明'], + ['radio', 'show', '是否显示', '新增或编辑时是否显示该字段', ['否', '是']], + ['radio', 'status', '立即启用', '', ['否', '是']], + ['text', 'sort', '排序'], + ]) + ->setTrigger('type', 'linkage', 'ajax_url,next_items,param') + ->setTrigger('type', 'linkages', 'table,pid,key,option') + ->setTrigger('type', 'bmap', 'ak') + ->setTrigger('type', 'linkages,bmap', 'level') + ->setTrigger('type', 'masked,date,time,datetime', 'format') + ->setTrigger('type', 'checkbox,radio,array,select,linkage,linkages', 'options') + ->js('field') + ->setFormData($info) + ->fetch(); + } + + /** + * 删除字段 + * @param null $ids 字段id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($ids = null) + { + if ($ids === null) $this->error('参数错误'); + + $FieldModel = new FieldModel(); + $field = $FieldModel->where('id', $ids)->find(); + + if ($FieldModel->deleteField($field)) { + if ($FieldModel->where('id', $ids)->delete()) { + // 记录行为 + $details = '详情:文档模型('.get_model_title($field['model']).')、字段名称('.$field['name'].')、字段标题('.$field['title'].')、字段类型('.$field['type'].')'; + action_log('field_delete', 'cms_field', $ids, UID, $details); + $this->success('删除成功', cookie('__forward__')); + } + } + return $this->error('删除失败'); + } + + /** + * 启用字段 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用字段 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置字段状态:删除、禁用、启用 + * @param string $type 类型:enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $field_delete = is_array($ids) ? '' : $ids; + $field_names = FieldModel::where('id', 'in', $ids)->column('name'); + return parent::setStatus($type, ['field_'.$type, 'cms_field', $field_delete, UID, implode('、', $field_names)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $config = FieldModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $config . '),新值:(' . $value . ')'; + return parent::quickEdit(['field_edit', 'cms_field', $id, UID, $details]); + } +} diff --git a/application/cms/admin/Index.php b/application/cms/admin/Index.php new file mode 100644 index 0000000..032eaf6 --- /dev/null +++ b/application/cms/admin/Index.php @@ -0,0 +1,35 @@ + + * @return mixed + */ + public function index() + { + $this->assign('document', Db::name('cms_document')->where('trash', 0)->count()); + $this->assign('column', Db::name('cms_column')->count()); + $this->assign('page', Db::name('cms_page')->count()); + $this->assign('model', Db::name('cms_model')->count()); + $this->assign('page_title', '仪表盘'); + return $this->fetch(); // 渲染模板 + } +} diff --git a/application/cms/admin/Link.php b/application/cms/admin/Link.php new file mode 100644 index 0000000..f6c6b3e --- /dev/null +++ b/application/cms/admin/Link.php @@ -0,0 +1,216 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\exception\DbException + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = LinkModel::where($map)->order($order)->paginate(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['title', '标题', 'text.edit'], + ['url', '链接', 'text.edit'], + ['type', '类型', 'text', '', [1 => '文字链接', 2 => '图片链接']], + ['create_time', '创建时间', 'datetime'], + ['update_time', '更新时间', 'datetime'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons(['edit', 'delete' => ['data-tips' => '删除后无法恢复。']]) // 批量添加右侧按钮 + ->addOrder('id,title,type,create_time,update_time') + ->setRowList($data_list) // 设置表格数据 + ->addValidate('Link', 'title,url') + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Link'); + if(true !== $result) $this->error($result); + + if ($link = LinkModel::create($data)) { + // 记录行为 + action_log('link_add', 'cms_link', $link['id'], UID, $data['title']); + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['radio', 'type', '链接类型', '', [1 => '文字链接', 2 => '图片链接'], 1], + ['text', 'title', '链接标题'], + ['text', 'url', '链接地址', '请以 httphttps开头'], + ['image', 'logo', '链接LOGO'], + ['tags', 'keywords', '关键词'], + ['textarea', 'contact', '联系方式'], + ['text', 'sort', '排序', '', 100], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->setTrigger('type', 2, 'logo') + ->fetch(); + } + + /** + * 编辑 + * @param null $id 链接id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function edit($id = null) + { + if ($id === null) $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Link'); + if(true !== $result) $this->error($result); + + if (LinkModel::update($data)) { + // 记录行为 + action_log('link_edit', 'cms_link', $id, UID, $data['title']); + $this->success('编辑成功', 'index'); + } else { + $this->error('编辑失败'); + } + } + + $info = LinkModel::get($id); + + // 显示编辑页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'id'], + ['radio', 'type', '链接类型', '', [1 => '文字链接', 2 => '图片链接']], + ['text', 'title', '链接标题'], + ['text', 'url', '链接地址', '请以 httphttps开头'], + ['image', 'logo', '链接LOGO'], + ['tags', 'keywords', '关键词'], + ['textarea', 'contact', '联系方式'], + ['text', 'sort', '排序'], + ['radio', 'status', '立即启用', '', ['否', '是']] + ]) + ->setTrigger('type', 2, 'logo') + ->setFormData($info) + ->fetch(); + } + + /** + * 删除友情链接 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function delete($record = []) + { + return $this->setStatus('delete'); + } + + /** + * 启用友情链接 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用友情链接 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置友情链接状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $link_title = LinkModel::where('id', 'in', $ids)->column('title'); + return parent::setStatus($type, ['link_'.$type, 'cms_link', 0, UID, implode('、', $link_title)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $link = LinkModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $link . '),新值:(' . $value . ')'; + return parent::quickEdit(['link_edit', 'cms_link', $id, UID, $details]); + } +} diff --git a/application/cms/admin/Menu.php b/application/cms/admin/Menu.php new file mode 100644 index 0000000..e12492d --- /dev/null +++ b/application/cms/admin/Menu.php @@ -0,0 +1,263 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function index($id = null) + { + if ($id === null) $this->error('缺少参数'); + // 查询 + $map = $this->getMap(); + + // 数据列表 + $data_list = Db::view('cms_menu', true) + ->view('cms_column', ['name' => 'column_name'], 'cms_menu.column=cms_column.id', 'left') + ->view('cms_page', ['title' => 'page_title'], 'cms_menu.page=cms_page.id', 'left') + ->where('cms_menu.nid', $id) + ->order('cms_menu.sort,cms_menu.pid,cms_menu.id') + ->select(); + + foreach ($data_list as &$item) { + if ($item['type'] == 0) { + $item['title'] = $item['column_name']; + } elseif ($item['type'] == 1) { + $item['title'] = $item['page_title']; + } + } + + if (empty($map)) { + $data_list = Tree::toList($data_list); + } + + $btnAdd = ['icon' => 'fa fa-plus', 'title' => '新增子菜单', 'href' => url('add', ['nid' => $id, 'pid' => '__id__'])]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['cms_menu.title|cms_column.name|cms_page.title' => '标题'])// 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['title', '标题', 'callback', function($value, $data){ + return isset($data['title_prefix']) ? $data['title_display'] : $value; + }, '__data__'], + ['type', '类型', 'text', '', ['栏目链接', '单页链接', '自定义链接']], + ['target', '打开方式', 'select', ['_self' => '当前窗口', '_blank' => '新窗口']], + ['create_time', '创建时间', 'datetime'], + ['update_time', '更新时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('back', ['href' => url('nav/index')]) + ->addTopButton('add', ['href' => url('add', ['nid' => $id])]) + ->addTopButtons('enable,disable')// 批量添加顶部按钮 + ->addRightButton('custom', $btnAdd) + ->addRightButton('edit') + ->addRightButton('delete', ['data-tips' => '删除后无法恢复。'])// 批量添加右侧按钮 + ->setRowList($data_list)// 设置表格数据 + ->addValidate('Nav', 'title') + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @param null $nid 导航id + * @param int $pid 菜单父级id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function add($nid = null, $pid = 0) + { + if ($nid === null) $this->error('缺少参数'); + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Menu'); + if(true !== $result) $this->error($result); + + if ($menu = MenuModel::create($data)) { + // 记录行为 + action_log('menu_add', 'cms_menu', $menu['id'], UID, $data['title']); + $this->success('新增成功', url('index', ['id' => $nid])); + } else { + $this->error('新增失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'nid', $nid], + ['hidden', 'pid', $pid], + ['radio', 'type', '类型', '', ['栏目链接', '单页链接', '自定义链接'], 0], + ['select', 'column', '栏目', '必选', ColumnModel::getTreeList(0, false)], + ['select', 'page', '单页', '必选', PageModel::getTitleList()], + ['text', 'title', '菜单标题', '必填,只用于区分'], + ['text', 'url', 'URL', "必填。如果是模块链接,请填写模块/控制器/操作,如:admin/menu/add。如果是普通链接,则直接填写url地址,如:http://www.dolphinphp.com"], + ['text', 'css', 'CSS类', '可选'], + ['text', 'rel', '链接关系网(XFN)', '可选,即链接的rel值'], + ['radio', 'target', '打开方式', '', ['_self' => '当前窗口', '_blank' => '新窗口'], '_self'], + ['text', 'sort', '排序', '', 100], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->setTrigger('type', '0', 'column') + ->setTrigger('type', '1', 'page') + ->setTrigger('type', '2', 'title,url') + ->fetch(); + } + + /** + * 编辑 + * @param null $id 菜单id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function edit($id = null) + { + if ($id === null) $this->error('缺少参数'); + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Menu'); + if(true !== $result) $this->error($result); + + if (MenuModel::update($data)) { + // 记录行为 + action_log('menu_edit', 'cms_menu', $id, UID, $data['title']); + $this->success('编辑成功', url('index', ['id' => $data['nid']])); + } else { + $this->error('编辑失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'id'], + ['hidden', 'nid'], + ['radio', 'type', '类型', '', ['栏目链接', '单页链接', '自定义链接']], + ['select', 'column', '栏目', '必选', ColumnModel::getTreeList(0, false)], + ['select', 'page', '单页', '必选', PageModel::getTitleList()], + ['text', 'title', '菜单标题', '必填,只用于区分'], + ['text', 'url', 'URL', "必填。如果是模块链接,请填写模块/控制器/操作,如:admin/menu/add。如果是普通链接,则直接填写url地址,如:http://www.dolphinphp.com"], + ['text', 'css', 'CSS类', '可选'], + ['text', 'rel', '链接关系网(XFN)', '可选,即链接的rel值'], + ['radio', 'target', '打开方式', '', ['_self' => '当前窗口', '_blank' => '新窗口'], '_self'], + ['text', 'sort', '排序', '', 100], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->setFormData(MenuModel::get($id)) + ->setTrigger('type', '0', 'column') + ->setTrigger('type', '1', 'page') + ->setTrigger('type', '2', 'title,url') + ->fetch(); + } + + /** + * 删除菜单 + * @param null $ids 菜单id + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function delete($ids = null) + { + // 检查是否有子菜单 + if (MenuModel::where('pid', $ids)->find()) { + $this->error('请先删除或移动该菜单下的子菜单'); + } + return $this->setStatus('delete'); + } + + /** + * 启用菜单 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用菜单 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置菜单状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $menu_title = MenuModel::where('id', 'in', $ids)->column('title'); + return parent::setStatus($type, ['menu_'.$type, 'cms_menu', 0, UID, implode('、', $menu_title)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $menu = MenuModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $menu . '),新值:(' . $value . ')'; + return parent::quickEdit(['menu_edit', 'cms_menu', $id, UID, $details]); + } +} diff --git a/application/cms/admin/Model.php b/application/cms/admin/Model.php new file mode 100644 index 0000000..f035b56 --- /dev/null +++ b/application/cms/admin/Model.php @@ -0,0 +1,236 @@ + + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 数据列表 + $data_list = DocumentModel::where($map)->order('sort,id desc')->paginate(); + + // 字段管理按钮 + $btnField = [ + 'title' => '字段管理', + 'icon' => 'fa fa-fw fa-navicon', + 'href' => url('field/index', ['id' => '__id__']) + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['name' => '标识', 'title' => '标题']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['icon', '图标', 'icon'], + ['title', '标题'], + ['name', '标识'], + ['table', '附加表'], + ['type', '模型', 'text', '', ['系统', '普通', '独立']], + ['create_time', '创建时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addFilter('type', ['系统', '普通', '独立']) + ->addTopButtons('add,enable,disable') // 批量添加顶部按钮 + ->addRightButtons(['edit', 'custom' => $btnField, 'delete' => ['data-tips' => '删除模型将同时删除该模型下的所有字段,且无法恢复。']]) // 批量添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + /** + * 新增内容模型 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + if ($data['table'] == '') { + $data['table'] = config('database.prefix') . 'cms_document_' . $data['name']; + } else { + $data['table'] = str_replace('#@__', config('database.prefix'), $data['table']); + } + + // 验证 + $result = $this->validate($data, 'Model'); + if(true !== $result) $this->error($result); + // 严格验证附加表是否存在 + if (table_exist($data['table'])) { + $this->error('附加表已存在'); + } + + if ($model = DocumentModel::create($data)) { + // 创建附加表 + if (false === DocumentModel::createTable($model)) { + $this->error('创建附加表失败'); + } + // 创建菜单节点 + $map = [ + 'module' => 'cms', + 'title' => '内容管理' + ]; + $menu_data = [ + "module" => "cms", + "pid" => Db::name('admin_menu')->where($map)->value('id'), + "title" => $data['title'], + "url_type" => "module_admin", + "url_value" => "cms/content/{$data['name']}", + "url_target" => "_self", + "icon" => "fa fa-fw fa-list", + "online_hide" => "0", + "sort" => "100", + ]; + MenuModel::create($menu_data); + + // 记录行为 + action_log('model_add', 'cms_model', $model['id'], UID, $data['title']); + Cache::clear(); + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + $type_tips = '此选项添加后不可更改。如果为 系统模型 将禁止删除,对于 独立模型,将强制创建字段id,cid,uid,model,title,create_time,update_time,sort,status,trash,view'; + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['text', 'name', '模型标识', '由小写字母、数字或下划线组成,不能以数字开头'], + ['text', 'title', '模型标题', '可填写中文'], + ['text', 'table', '附加表', '创建后不可更改。由小写字母、数字或下划线组成,如果不填写默认为 '. config('database.prefix') . 'cms_document_模型标识,如果需要自定义,请务必填写系统表前缀,#@__表示当前系统表前缀'], + ['radio', 'type', '模型类别', $type_tips, ['系统模型', '普通模型', '独立模型(不使用主表)'], 1], + ['icon', 'icon', '图标'], + ['radio', 'status', '立即启用', '', ['否', '是'], 1], + ['text', 'sort', '排序', '', 100], + ]) + ->fetch(); + } + + /** + * 编辑内容模型 + * @param null $id 模型id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function edit($id = null) { + if ($id === null) $this->error('参数错误'); + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Model.edit'); + if(true !== $result) $this->error($result); + + if (DocumentModel::update($data)) { + cache('cms_model_list', null); + cache('cms_model_title_list', null); + // 记录行为 + action_log('model_edit', 'cms_model', $id, UID, "ID({$id}),标题({$data['title']})"); + $this->success('编辑成功', 'index'); + } else { + $this->error('编辑失败'); + } + } + + $list_model_type = ['系统模型', '普通模型', '独立模型(不使用主表)']; + + // 模型信息 + $info = DocumentModel::get($id); + $info['type'] = $list_model_type[$info['type']]; + + // 显示编辑页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'id'], + ['hidden', 'name'], + ['static', 'name', '模型标识'], + ['static', 'type', '模型类别'], + ['static', 'table', '附加表'], + ['text', 'title', '模型标题', '可填写中文'], + ['icon', 'icon', '图标'], + ['radio', 'status', '立即启用', '', ['否', '是']], + ['text', 'sort', '排序'], + ]) + ->setFormData($info) + ->fetch(); + } + + /** + * 删除内容模型 + * @param null $ids 内容模型id + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + * @throws \think\exception\PDOException + */ + public function delete($ids = null) + { + if ($ids === null) $this->error('参数错误'); + + $model = DocumentModel::where('id', $ids)->find(); + if ($model['type'] == 0) { + $this->error('禁止删除系统模型'); + } + + // 删除表和字段信息 + if (DocumentModel::deleteTable($ids)) { + // 删除主表中的文档 + if (false === Db::name('cms_document')->where('model', $ids)->delete()) { + $this->error('删除主表文档失败'); + } + // 删除菜单节点 + $map = [ + 'module' => 'cms', + 'url_value' => "cms/content/{$model['name']}" + ]; + if (false === Db::name('admin_menu')->where($map)->delete()) { + $this->error('删除菜单节点失败'); + } + // 删除字段数据 + if (false !== Db::name('cms_field')->where('model', $ids)->delete()) { + cache('cms_model_list', null); + cache('cms_model_title_list', null); + return parent::delete(); + } else { + $this->error('删除内容模型字段失败'); + } + } else { + $this->error('删除内容模型表失败'); + } + } +} diff --git a/application/cms/admin/Nav.php b/application/cms/admin/Nav.php new file mode 100644 index 0000000..805b4ae --- /dev/null +++ b/application/cms/admin/Nav.php @@ -0,0 +1,175 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\exception\DbException + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = NavModel::where($map)->order($order)->paginate(); + + // 自定义按钮 + $btnMenuList = [ + 'title' => '菜单列表', + 'icon' => 'fa fa-list', + 'href' => url('menu/index', ['id' => '__id__']) + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题'])// 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['tag', '标识', 'text.edit'], + ['title', '标题', 'text.edit'], + ['create_time', '创建时间', 'datetime'], + ['update_time', '更新时间', 'datetime'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete')// 批量添加顶部按钮 + ->addRightButton('custom', $btnMenuList) + ->addRightButton('delete', ['data-tips' => '删除后无法恢复。'])// 批量添加右侧按钮 + ->addOrder('id,title,create_time,update_time') + ->setRowList($data_list)// 设置表格数据 + ->addValidate('Nav', 'tag,title') + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Nav'); + if(true !== $result) $this->error($result); + + if ($nav = NavModel::create($data)) { + // 记录行为 + action_log('nav_add', 'cms_nav', $nav['id'], UID, $data['title']); + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['text', 'tag', '菜单标识', '由字母和下划线组成,如:main_nav'], + ['text', 'title', '菜单标题', '必填'], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->fetch(); + } + + /** + * 删除导航 + * @param null $ids 菜单id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function delete($ids = null) + { + if ($ids === null) $this->error('参数错误'); + // 删除该导航的所有子菜单 + if (false === MenuModel::where('nid', 'in', $ids)->delete()) { + $this->error('删除失败'); + } + return $this->setStatus('delete'); + } + + /** + * 启用导航 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用导航 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置导航状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $nav_title = NavModel::where('id', 'in', $ids)->column('title'); + return parent::setStatus($type, ['nav_'.$type, 'cms_nav', 0, UID, implode('、', $nav_title)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $nav = NavModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $nav . '),新值:(' . $value . ')'; + return parent::quickEdit(['nav_edit', 'cms_nav', $id, UID, $details]); + } +} diff --git a/application/cms/admin/Page.php b/application/cms/admin/Page.php new file mode 100644 index 0000000..5f0a17a --- /dev/null +++ b/application/cms/admin/Page.php @@ -0,0 +1,212 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\exception\DbException + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder(); + // 数据列表 + $data_list = PageModel::where($map)->order($order)->paginate(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['title', '标题', 'text.edit'], + ['create_time', '创建时间', 'datetime'], + ['update_time', '更新时间', 'datetime'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons(['edit', 'delete' => ['data-tips' => '删除后无法恢复。']]) // 批量添加右侧按钮 + ->addOrder('id,title,create_time,update_time') + ->setRowList($data_list) // 设置表格数据 + ->addValidate('Page', 'title') + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Page'); + if(true !== $result) $this->error($result); + + if ($page = PageModel::create($data)) { + // 记录行为 + action_log('page_add', 'cms_page', $page['id'], UID, $data['title']); + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['text', 'title', '页面标题'], + ['tags', 'keywords', '页面关键词', '关键字之间用英文逗号隔开'], + ['textarea', 'description', '页面描述', '100字左右'], + ['text', 'template', '模板文件名'], + ['ckeditor', 'content', '页面内容'], + ['image', 'cover', '单页封面'], + ['text', 'view', '阅读量', '', 0], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->fetch(); + } + + /** + * 编辑 + * @param null $id 单页id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function edit($id = null) + { + if ($id === null) $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Page'); + if(true !== $result) $this->error($result); + + if (PageModel::update($data)) { + // 记录行为 + action_log('page_edit', 'cms_page', $id, UID, $data['title']); + $this->success('编辑成功', 'index'); + } else { + $this->error('编辑失败'); + } + } + + $info = PageModel::get($id); + + // 显示编辑页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'id'], + ['text', 'title', '页面标题'], + ['tags', 'keywords', '页面关键词', '关键字之间用英文逗号隔开'], + ['textarea', 'description', '页面描述', '100字左右'], + ['text', 'template', '模板文件名'], + ['ckeditor', 'content', '页面内容'], + ['image', 'cover', '单页封面'], + ['text', 'view', '阅读量', '', 0], + ['radio', 'status', '立即启用', '', ['否', '是']] + ]) + ->setFormdata($info) + ->fetch(); + } + + /** + * 删除单页 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function delete($record = []) + { + return $this->setStatus('delete'); + } + + /** + * 启用单页 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用单页 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置单页状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $page_title = PageModel::where('id', 'in', $ids)->column('title'); + return parent::setStatus($type, ['page_'.$type, 'cms_page', 0, UID, implode('、', $page_title)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $page = PageModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $page . '),新值:(' . $value . ')'; + return parent::quickEdit(['page_edit', 'cms_page', $id, UID, $details]); + } +} diff --git a/application/cms/admin/Recycle.php b/application/cms/admin/Recycle.php new file mode 100644 index 0000000..63c1dc3 --- /dev/null +++ b/application/cms/admin/Recycle.php @@ -0,0 +1,202 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\exception\DbException + */ + public function index($model = '') + { + if ($model == '') { + // 查询 + $map = $this->getMap(); + $map[] = ['cms_document.trash', '=', 1]; + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = DocumentModel::getList($map, $order); + + // 自定义按钮 + $btnRestore = [ + 'class' => 'btn btn-xs btn-default ajax-get confirm', + 'icon' => 'fa fa-fw fa-reply', + 'title' => '还原', + 'href' => url('restore', ['ids' => '__id__']) + ]; + $btnRestoreAll = [ + 'class' => 'btn btn-success ajax-post confirm', + 'icon' => 'fa fa-fw fa-reply-all', + 'title' => '批量还原', + 'href' => url('restore') + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题', 'cms_column.name' => '栏目名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['title', '标题'], + ['column_name', '栏目名称'], + ['view', '点击量'], + ['username', '发布人'], + ['update_time', '更新时间', 'datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('enable', $btnRestoreAll) // 批量添加顶部按钮 + ->addTopButton('delete', ['title' => '批量删除', 'href' => url('delete'), 'data-tips' => '删除后不可回复!']) // 批量添加顶部按钮 + ->addRightButton('custom', $btnRestore) // 添加右侧按钮 + ->addRightButton('delete', ['href' => url('delete', ['ids' => '__id__']), 'data-tips' => '删除后不可回复!']) // 添加右侧按钮 + ->addOrder('id,title,column_name,view,username,update_time') + ->addFilter(['column_name' => 'cms_column.name', 'username' => 'admin_user']) + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } else { + $table_name = get_model_table($model); + + // 查询 + $map = $this->getMap(); + $map[] = ['trash', '=', 1]; + + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = Db::view($table_name, true) + ->view("cms_column", ['name' => 'column_name'], 'cms_column.id='.$table_name.'.cid', 'left') + ->view("admin_user", 'username', 'admin_user.id='.$table_name.'.uid', 'left') + ->where($map) + ->order($order) + ->paginate(); + + // 自定义按钮 + $btnRestore = [ + 'class' => 'btn btn-xs btn-default ajax-get confirm', + 'icon' => 'fa fa-fw fa-reply', + 'title' => '还原', + 'href' => url('restore', ['table' => $table_name, 'ids' => '__id__']) + ]; + $btnRestoreAll = [ + 'class' => 'btn btn-success ajax-post confirm', + 'icon' => 'fa fa-fw fa-reply-all', + 'title' => '批量还原', + 'href' => url('restore', ['table' => $table_name]) + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题', 'cms_column.name' => '栏目名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['title', '标题'], + ['column_name', '栏目名称'], + ['view', '点击量'], + ['username', '发布人'], + ['update_time', '更新时间', 'datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('enable', $btnRestoreAll) // 添加顶部按钮 + ->addTopButton('delete', ['title' => '批量删除', 'href' => url('delete', ['table' => $table_name]), 'data-tips' => '删除后不可回复!']) // 添加顶部按钮 + ->addRightButton('custom', $btnRestore) // 添加右侧按钮 + ->addRightButton('delete', ['href' => url('delete', ['ids' => '__id__', 'table' => $table_name]), 'data-tips' => '删除后不可回复!']) // 添加右侧按钮 + ->addOrder('id,title,column_name,view,username,update_time') + ->addFilter(['column_name' => 'cms_column.name', 'username' => 'admin_user']) + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + } + + /** + * 还原文档 + * @param null $ids 文档id + * @param string $table 表名 + * @author 蔡伟明 <314013107@qq.com> + */ + public function restore($ids = null, $table = '') + { + if ($ids === null) $this->error('请选择要操作的数据'); + $table = $table != '' ? substr($table, strlen(config('database.prefix'))) : 'cms_document'; + + $document_id = is_array($ids) ? '' : $ids; + $document_title = Db::name($table)->where('id', 'in', $ids)->column('title'); + + // 还原文档 + if (false === Db::name($table)->where('id', 'in', $ids)->setField('trash', 0)) { + $this->error('还原失败'); + } + + // 删除并记录日志 + action_log('document_restore', $table, $document_id, UID, implode('、', $document_title)); + $this->success('还原成功'); + } + + /** + * 彻底删除文档 + * @param null $ids 文档id + * @param string $table 表名 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function delete($ids = null, $table = '') + { + if ($ids === null) $this->error('请选择要操作的数据'); + $ids = is_array($ids) ? $ids : (array)$ids; + + if ($table == '') { + // 获取文档标题和模型id + $data_list = Db::name('cms_document')->where('id', 'in', $ids)->column('id,model,title'); + + foreach ($data_list as $document) { + // 附加表名 + $extra_table = get_model_table($document['model']); + + // 删除附加表文档 + if (false === Db::table($extra_table)->where('aid', $document['id'])->delete()) { + $this->error('删除文档:'. $document['title']. ' 失败'); + } + + // 删除主表文档 + if (false === Db::name('cms_document')->where('id', $document['id'])->delete()) { + $this->error('删除失败'); + } + + // 记录行为 + action_log('document_delete', 'cms_document', $document['id'], UID, $document['title']); + } + } else { + // 文档标题 + $document_title = Db::table($table)->where('id', 'in', $ids)->column('title'); + + // 删除独立文档 + if (false === Db::table($table)->where('id', 'in', $ids)->delete()) { + $this->error('删除失败'); + } + + // 记录行为 + action_log('document_delete', $table, 0, UID, '表('.$table.'),文档('.implode('、', $document_title).')'); + } + $this->success('删除成功'); + } +} diff --git a/application/cms/admin/Slider.php b/application/cms/admin/Slider.php new file mode 100644 index 0000000..ba4fcb7 --- /dev/null +++ b/application/cms/admin/Slider.php @@ -0,0 +1,208 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\exception\DbException + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder(); + // 数据列表 + $data_list = SliderModel::where($map)->order($order)->paginate(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['cover', '图片', 'picture'], + ['title', '标题', 'text.edit'], + ['url', '链接', 'text.edit'], + ['create_time', '创建时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons(['edit', 'delete' => ['data-tips' => '删除后无法恢复。']]) // 批量添加右侧按钮 + ->addOrder('id,title,create_time') + ->setRowList($data_list) // 设置表格数据 + ->addValidate('Slider', 'title,url') + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Slider'); + if(true !== $result) $this->error($result); + + if ($slider = SliderModel::create($data)) { + // 记录行为 + action_log('slider_add', 'cms_slider', $slider['id'], UID, $data['title']); + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['text', 'title', '标题'], + ['image', 'cover', '图片'], + ['text', 'url', '链接'], + ['text', 'sort', '排序', '', 100], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->fetch(); + } + + /** + * 编辑 + * @param null $id 滚动图片id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function edit($id = null) + { + if ($id === null) $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Slider'); + if(true !== $result) $this->error($result); + + if (SliderModel::update($data)) { + // 记录行为 + action_log('slider_add', 'cms_slider', $id, UID, $data['title']); + $this->success('编辑成功', 'index'); + } else { + $this->error('编辑失败'); + } + } + + $info = SliderModel::get($id); + + // 显示编辑页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'id'], + ['text', 'title', '标题'], + ['image', 'cover', '图片'], + ['text', 'url', '链接'], + ['text', 'sort', '排序'], + ['radio', 'status', '立即启用', '', ['否', '是']] + ]) + ->setFormData($info) + ->fetch(); + } + + /** + * 删除单页 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function delete($record = []) + { + return $this->setStatus('delete'); + } + + /** + * 启用单页 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用单页 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置单页状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $slider_title = SliderModel::where('id', 'in', $ids)->column('title'); + return parent::setStatus($type, ['slider_'.$type, 'cms_slider', 0, UID, implode('、', $slider_title)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $slider = SliderModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $slider . '),新值:(' . $value . ')'; + return parent::quickEdit(['slider_edit', 'cms_slider', $id, UID, $details]); + } +} diff --git a/application/cms/admin/Support.php b/application/cms/admin/Support.php new file mode 100644 index 0000000..30eb2ab --- /dev/null +++ b/application/cms/admin/Support.php @@ -0,0 +1,220 @@ +getMap(); + // 排序 + $order = $this->getOrder(); + // 数据列表 + $data_list = SupportModel::where($map)->order($order)->paginate(); + + $search = [ + 'name' => '客服名称', + 'qq' => 'QQ', + 'msn' => 'MSN', + 'taobao' => '淘宝旺旺', + 'alibaba' => '阿里旺旺', + 'skype' => 'SKYPE' + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTips('添加的QQ需要到【shang.qq.com】登录后在【商家沟通组建—设置】开启QQ的在线状态,否则将显示“未启用”
    开启和关闭在线客服功能,以及更多设置,请在 系统设置 中操作。') + ->setSearch($search) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['name', '客服名称', 'text.edit'], + ['qq', 'QQ'], + ['msn', 'MSN'], + ['taobao', '淘宝旺旺'], + ['alibaba', '阿里旺旺'], + ['skype', 'SKYPE'], + ['create_time', '创建时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons(['edit', 'delete' => ['data-tips' => '删除后无法恢复。']]) // 批量添加右侧按钮 + ->addOrder('id,name,create_time,update_time') + ->addValidate('Support', 'name') + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Support'); + if(true !== $result) $this->error($result); + + if ($support = SupportModel::create($data)) { + // 记录行为 + action_log('support_add', 'cms_support', $support['id'], UID, $data['name']); + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['text', 'name', '客服名称'], + ['text', 'qq', 'QQ号码'], + ['text', 'msn', 'MSN号码'], + ['text', 'taobao', '淘宝旺旺'], + ['text', 'alibaba', '阿里旺旺'], + ['text', 'skype', 'SKYPE'], + ['text', 'sort', '排序', '', 100], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->fetch(); + } + + /** + * 编辑 + * @param null $id 客服id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function edit($id = null) + { + if ($id === null) $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Support'); + if(true !== $result) $this->error($result); + + if (SupportModel::update($data)) { + // 记录行为 + action_log('support_edit', 'cms_support', $id, UID, $data['name']); + $this->success('编辑成功', 'index'); + } else { + $this->error('编辑失败'); + } + } + + $info = SupportModel::get($id); + + // 显示编辑页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'id'], + ['text', 'name', '客服名称'], + ['text', 'qq', 'QQ号码'], + ['text', 'msn', 'MSN号码'], + ['text', 'taobao', '淘宝旺旺'], + ['text', 'alibaba', '阿里旺旺'], + ['text', 'skype', 'SKYPE'], + ['text', 'sort', '排序'], + ['radio', 'status', '立即启用', '', ['否', '是']] + ]) + ->setFormData($info) + ->fetch(); + } + + /** + * 删除客服 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function delete($record = []) + { + return $this->setStatus('delete'); + } + + /** + * 启用客服 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用客服 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置客服状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $support_title = SupportModel::where('id', 'in', $ids)->column('name'); + return parent::setStatus($type, ['support_'.$type, 'cms_support', 0, UID, implode('、', $support_title)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $support = SupportModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $support . '),新值:(' . $value . ')'; + return parent::quickEdit(['support_edit', 'cms_support', $id, UID, $details]); + } +} diff --git a/application/cms/common.php b/application/cms/common.php new file mode 100644 index 0000000..e6038c2 --- /dev/null +++ b/application/cms/common.php @@ -0,0 +1,158 @@ + + * @return string + */ + function get_column_name($cid = 0) + { + $column_list = model('cms/column')->getList(); + return isset($column_list[$cid]) ? $column_list[$cid]['name'] : ''; + } +} + +if (!function_exists('get_model_name')) { + /** + * 获取内容模型名称 + * @param string $id 内容模型id + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_model_name($id = '') + { + $model_list = model('cms/model')->getList(); + return isset($model_list[$id]) ? $model_list[$id]['name'] : ''; + } +} + +if (!function_exists('get_model_title')) { + /** + * 获取内容模型标题 + * @param string $id 内容模型标题 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_model_title($id = '') + { + $model_list = model('cms/model')->getList(); + return isset($model_list[$id]) ? $model_list[$id]['title'] : ''; + } +} + +if (!function_exists('get_model_type')) { + /** + * 获取内容模型类别:0-系统,1-普通,2-独立 + * @param int $id 模型id + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_model_type($id = 0) + { + $model_list = model('cms/model')->getList(); + return isset($model_list[$id]) ? $model_list[$id]['type'] : ''; + } +} + +if (!function_exists('get_model_table')) { + /** + * 获取内容模型附加表名 + * @param int $id 模型id + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_model_table($id = 0) + { + $model_list = model('cms/model')->getList(); + return isset($model_list[$id]) ? $model_list[$id]['table'] : ''; + } +} + +if (!function_exists('is_default_field')) { + /** + * 检查是否为系统默认字段 + * @param string $field 字段名称 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + function is_default_field($field = '') + { + $system_fields = cache('cms_system_fields'); + if (!$system_fields) { + $system_fields = Db::name('cms_field')->where('model', 0)->column('name'); + cache('cms_system_fields', $system_fields); + } + return in_array($field, $system_fields, true); + } +} + +if (!function_exists('table_exist')) { + /** + * 检查附加表是否存在 + * @param string $table_name 附加表名 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function table_exist($table_name = '') + { + return true == Db::query("SHOW TABLES LIKE '{$table_name}'"); + } +} + +if (!function_exists('time_tran')) { + /** + * 转换时间 + * @param int $timer 时间戳 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function time_tran($timer) + { + $diff = $_SERVER['REQUEST_TIME'] - $timer; + $day = floor($diff / 86400); + $free = $diff % 86400; + if ($day > 0) { + return $day . " 天前"; + } else { + if ($free > 0) { + $hour = floor($free / 3600); + $free = $free % 3600; + if ($hour > 0) { + return $hour . " 小时前"; + } else { + if ($free > 0) { + $min = floor($free / 60); + $free = $free % 60; + if ($min > 0) { + return $min . " 分钟前"; + } else { + if ($free > 0) { + return $free . " 秒前"; + } else { + return '刚刚'; + } + } + } else { + return '刚刚'; + } + } + } else { + return '刚刚'; + } + } + } +} \ No newline at end of file diff --git a/application/cms/home/Column.php b/application/cms/home/Column.php new file mode 100644 index 0000000..86a81d1 --- /dev/null +++ b/application/cms/home/Column.php @@ -0,0 +1,105 @@ + + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function index($id = null) + { + if ($id === null) $this->error('缺少参数'); + $map = [ + 'status' => 1, + 'id' => $id + ]; + + $column = Db::name('cms_column')->where($map)->find(); + if (!$column) $this->error('该栏目不存在'); + + $model = Db::name('cms_model')->where('id', $column['model'])->find(); + + if ($model['type'] == 2) { + $cid_all = ColumnModel::getChildsId($id); + $cid_all[] = (int)$id; + + $map = [ + [$model['table'].'.trash', '=', 0], + [$model['table'].'.status', '=', 1], + [$model['table'].'.cid', 'in', $cid_all], + ]; + + $data_list = Db::view($model['table'], true) + ->view('admin_user', 'username', $model['table'].'.uid=admin_user.id', 'left') + ->where($map) + ->order('create_time desc') + ->paginate(config('list_rows')); + $this->assign('model', $column['model']); + } else { + $cid_all = ColumnModel::getChildsId($id); + $cid_all[] = (int)$id; + + $map = [ + ['cms_document.trash', '=', 0], + ['cms_document.status', '=', 1], + ['cms_document.cid', 'in', $cid_all], + ]; + + $data_list = Db::view('cms_document', true) + ->view('admin_user', 'username', 'cms_document.uid=admin_user.id', 'left') + ->view($model['table'], '*', 'cms_document.id='. $model['table'] . '.aid', 'left') + ->where($map) + ->order('create_time desc') + ->paginate(config('list_rows')); + $this->assign('model', ''); + } + + $this->assign('lists', $data_list); + $this->assign('pages', $data_list->render()); + $this->assign('breadcrumb', $this->getBreadcrumb($id)); + $this->assign('column_info', $column); + + $template = $column['list_template'] == '' ? 'list' : substr($column['list_template'], 0, strpos($column['list_template'], '.')); + return $this->fetch($template); + } + + /** + * 获取栏目面包屑导航 + * @param $id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function getBreadcrumb($id) + { + $columns = ColumnModel::where('status', 1)->column('id,pid,name,url,target,type'); + foreach ($columns as &$column) { + if ($column['type'] == 0) { + $column['url'] = url('cms/column/index', ['id' => $column['id']]); + } + } + + return Tree::config(['title' => 'name'])->getParents($columns, $id); + } +} diff --git a/application/cms/home/Common.php b/application/cms/home/Common.php new file mode 100644 index 0000000..941c25f --- /dev/null +++ b/application/cms/home/Common.php @@ -0,0 +1,90 @@ + + */ + protected function initialize() + { + parent::initialize(); + + // 获取菜单 + $this->getNav(); + // 获取滚动图片 + $this->assign('slider', $this->getSlider()); + // 获取客服 + $this->assign('support', $this->getSupport()); + } + + /** + * 获取导航 + * @author 蔡伟明 <314013107@qq.com> + */ + private function getNav() + { + $list_nav = Db::name('cms_nav')->where('status', 1)->column('id,tag'); + + foreach ($list_nav as $id => $tag) { + $data_list = Db::view('cms_menu', true) + ->view('cms_column', ['name' => 'column_name'], 'cms_menu.column=cms_column.id', 'left') + ->view('cms_page', ['title' => 'page_title'], 'cms_menu.page=cms_page.id', 'left') + ->where('cms_menu.nid', $id) + ->where('cms_menu.status', 1) + ->order('cms_menu.sort,cms_menu.pid,cms_menu.id') + ->select(); + + foreach ($data_list as &$item) { + if ($item['type'] == 0) { // 栏目链接 + $item['title'] = $item['column_name']; + $item['url'] = url('cms/column/index', ['id' => $item['column']]); + } elseif ($item['type'] == 1) { // 单页链接 + $item['title'] = $item['page_title']; + $item['url'] = url('cms/page/detail', ['id' => $item['page']]); + } else { + if ($item['url'] != '#' && substr($item['url'], 0, 4) != 'http') { + $item['url'] = url($item['url']); + } + } + } + + $this->assign($tag, Tree::toLayer($data_list)); + } + } + + /** + * 获取滚动图片 + * @author 蔡伟明 <314013107@qq.com> + */ + private function getSlider() + { + return Db::name('cms_slider')->where('status', 1)->select(); + } + + /** + * 获取在线客服 + * @author 蔡伟明 <314013107@qq.com> + */ + private function getSupport() + { + return Db::name('cms_support')->where('status', 1)->order('sort')->select(); + } +} diff --git a/application/cms/home/Document.php b/application/cms/home/Document.php new file mode 100644 index 0000000..94794a3 --- /dev/null +++ b/application/cms/home/Document.php @@ -0,0 +1,152 @@ + + * @return mixed + */ + public function detail($id = null, $model = '') + { + if ($id === null) $this->error('缺少参数'); + + if ($model != '') { + $table = get_model_table($model); + $map = [ + $table.'.status' => 1, + $table.'.trash' => 0 + ]; + } else { + $map = [ + 'cms_document.status' => 1, + 'cms_document.trash' => 0 + ]; + } + + $info = DocumentModel::getOne($id, $model, $map); + if (isset($info['tags'])) { + $info['tags'] = explode(',', $info['tags']); + } + + $this->assign('document', $info); + $this->assign('breadcrumb', $this->getBreadcrumb($info['cid'])); + $this->assign('prev', $this->getPrev($id, $model)); + $this->assign('next', $this->getNext($id, $model)); + + $template = $info['detail_template'] == '' ? 'detail' : substr($info['detail_template'], 0, strpos($info['detail_template'], '.')); + return $this->fetch($template); + } + + /** + * 获取栏目面包屑导航 + * @param int $id 栏目id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + private function getBreadcrumb($id) + { + $columns = ColumnModel::where('status', 1)->column('id,pid,name,url,target,type'); + foreach ($columns as &$column) { + if ($column['type'] == 0) { + $column['url'] = url('cms/column/index', ['id' => $column['id']]); + } + } + return Tree::config(['title' => 'name'])->getParents($columns, $id); + } + + /** + * 获取上一篇文档 + * @param int $id 当前文档id + * @param string $model 独立模型id + * @author 蔡伟明 <314013107@qq.com> + * @return array|string|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + private function getPrev($id, $model = '') + { + if ($model == '') { + $cid = Db::name('cms_document')->where('id', $id)->value('cid'); + $document = Db::name('cms_document')->where([ + ['status', '=', 1], + ['trash', '=', 0], + ['cid', '=', $cid], + ['id', 'lt', $id] + ])->order('id desc')->find(); + } else { + $table = get_model_table($model); + $cid = Db::table($table)->where('id', $id)->value('cid'); + $document = Db::table($table)->where([ + ['status', '=', 1], + ['trash', '=', 0], + ['cid', '=', $cid], + ['id', 'lt', $id] + ])->order('id desc')->find(); + } + + if ($document) { + $document['url'] = url('cms/document/detail', ['id' => $document['id'], 'model' => $model]); + } + return $document; + } + + /** + * 获取下一篇文档 + * @param int $id 当前文档id + * @param string $model 独立模型id + * @author 蔡伟明 <314013107@qq.com> + * @return array|string|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + private function getNext($id, $model = '') + { + if ($model == '') { + $cid = Db::name('cms_document')->where('id', $id)->value('cid'); + $document = Db::name('cms_document')->where([ + ['status', '=', 1], + ['trash', '=', 0], + ['cid', '=', $cid], + ['id', 'gt', $id] + ])->find(); + } else { + $table = get_model_table($model); + $cid = Db::table($table)->where('id', $id)->value('cid'); + $document = Db::table($table)->where([ + ['status', '=', 1], + ['trash', '=', 0], + ['cid', '=', $cid], + ['id', 'gt', $id] + ])->find(); + } + + if ($document) { + $document['url'] = url('cms/document/detail', ['id' => $document['id'], 'model' => $model]); + } + + return $document; + } +} diff --git a/application/cms/home/Index.php b/application/cms/home/Index.php new file mode 100644 index 0000000..a11eca8 --- /dev/null +++ b/application/cms/home/Index.php @@ -0,0 +1,27 @@ + + * @return mixed + */ + public function index() + { + return $this->fetch(); // 渲染模板 + } +} diff --git a/application/cms/home/Page.php b/application/cms/home/Page.php new file mode 100644 index 0000000..dda30e9 --- /dev/null +++ b/application/cms/home/Page.php @@ -0,0 +1,42 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function detail($id = null) + { + $info = PageModel::where('status', 1)->find($id); + $info['url'] = url('cms/page/detail', ['id' => $info['id']]); + $info['tags'] = explode(',', $info['keywords']); + + // 更新阅读量 + PageModel::where('id', $id)->setInc('view'); + + $this->assign('page_info', $info); + return $this->fetch(); // 渲染模板 + } +} diff --git a/application/cms/home/Search.php b/application/cms/home/Search.php new file mode 100644 index 0000000..fcfabdd --- /dev/null +++ b/application/cms/home/Search.php @@ -0,0 +1,48 @@ + + * @return mixed + * @throws \think\exception\DbException + */ + public function index($keyword = '') + { + if ($keyword == '') $this->error('请输入关键字'); + $map = [ + ['cms_document.trash', '=', 0], + ['cms_document.status', '=', 1], + ['cms_document.title', 'like', "%$keyword%"] + ]; + + $data_list = Db::view('cms_document', true) + ->view('admin_user', 'username', 'cms_document.uid=admin_user.id', 'left') + ->where($map) + ->order('create_time desc') + ->paginate(config('list_rows')); + + $this->assign('keyword', $keyword); + $this->assign('lists', $data_list); + $this->assign('pages', $data_list->render()); + + return $this->fetch(); // 渲染模板 + } +} diff --git a/application/cms/info.php b/application/cms/info.php new file mode 100644 index 0000000..524b167 --- /dev/null +++ b/application/cms/info.php @@ -0,0 +1,604 @@ + 'cms', + // 模块标题[必填] + 'title' => '门户', + // 模块唯一标识[必填],格式:模块名.开发者标识.module + 'identifier' => 'cms.ming.module', + // 模块图标[选填] + 'icon' => 'fa fa-fw fa-newspaper-o', + // 模块描述[选填] + 'description' => '门户模块', + // 开发者[必填] + 'author' => 'CaiWeiMing', + // 开发者网址[选填] + 'author_url' => 'http://www.dolphinphp.com', + // 版本[必填],格式采用三段式:主版本号.次版本号.修订版本号 + 'version' => '1.0.0', + // 模块依赖[可选],格式[[模块名, 模块唯一标识, 依赖版本, 对比方式]] + 'need_module' => [ + ['admin', 'admin.dolphinphp.module', '1.0.0'] + ], + // 插件依赖[可选],格式[[插件名, 插件唯一标识, 依赖版本, 对比方式]] + 'need_plugin' => [], + // 数据表[有数据库表时必填] + 'tables' => [ + 'cms_advert', + 'cms_advert_type', + 'cms_column', + 'cms_document', + 'cms_document_article', + 'cms_field', + 'cms_link', + 'cms_menu', + 'cms_model', + 'cms_nav', + 'cms_page', + 'cms_slider', + 'cms_support', + ], + // 原始数据库表前缀 + // 用于在导入模块sql时,将原有的表前缀转换成系统的表前缀 + // 一般模块自带sql文件时才需要配置 + 'database_prefix' => 'dp_', + + // 模块参数配置 + 'config' => [ + ['text', 'summary', '默认摘要字数', '发布文章时,如果没有填写摘要,则自动获取文档内容为摘要。如果此处不填写或填写0,则不提取摘要。', 0], + ['ckeditor', 'contact', '联系方式', '', '
    河源市卓锐科技有限公司
    +地址:河源市江东新区东环路汇通苑D3-H232
    +电话:0762-8910006
    +邮箱:admin@zrthink.com
    '], + ['textarea', 'meta_head', '顶部代码', '代码会放在 </head> 标签以上'], + ['textarea', 'meta_foot', '底部代码', '代码会放在 </body> 标签以上'], + ['radio', 'support_status', '在线客服', '', ['禁用', '启用'], 1], + ['colorpicker', 'support_color', '在线客服配色', '', 'rgba(0,158,232,1)'], + ['image', 'support_wx', '在线客服微信二维码', '在线客服微信二维码'], + ['ckeditor', 'support_extra', '在线客服额外内容', '在线客服额外内容,可填写电话或其他说明'], + ], + + // 行为配置 + 'action' => [ + [ + 'module' => 'cms', + 'name' => 'slider_delete', + 'title' => '删除滚动图片', + 'remark' => '删除滚动图片', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了滚动图片:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'slider_edit', + 'title' => '编辑滚动图片', + 'remark' => '编辑滚动图片', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了滚动图片:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'slider_add', + 'title' => '添加滚动图片', + 'remark' => '添加滚动图片', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了滚动图片:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'document_delete', + 'title' => '删除文档', + 'remark' => '删除文档', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了文档:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'document_restore', + 'title' => '还原文档', + 'remark' => '还原文档', + 'rule' => '', + 'log' => '[user|get_nickname] 还原了文档:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'nav_disable', + 'title' => '禁用导航', + 'remark' => '禁用导航', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了导航:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'nav_enable', + 'title' => '启用导航', + 'remark' => '启用导航', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了导航:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'nav_delete', + 'title' => '删除导航', + 'remark' => '删除导航', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了导航:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'nav_edit', + 'title' => '编辑导航', + 'remark' => '编辑导航', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了导航:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'nav_add', + 'title' => '添加导航', + 'remark' => '添加导航', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了导航:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'model_disable', + 'title' => '禁用内容模型', + 'remark' => '禁用内容模型', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了内容模型:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'model_enable', + 'title' => '启用内容模型', + 'remark' => '启用内容模型', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了内容模型:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'model_delete', + 'title' => '删除内容模型', + 'remark' => '删除内容模型', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了内容模型:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'model_edit', + 'title' => '编辑内容模型', + 'remark' => '编辑内容模型', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了内容模型:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'model_add', + 'title' => '添加内容模型', + 'remark' => '添加内容模型', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了内容模型:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'menu_disable', + 'title' => '禁用导航菜单', + 'remark' => '禁用导航菜单', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了导航菜单:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'menu_enable', + 'title' => '启用导航菜单', + 'remark' => '启用导航菜单', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了导航菜单:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'menu_delete', + 'title' => '删除导航菜单', + 'remark' => '删除导航菜单', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了导航菜单:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'menu_edit', + 'title' => '编辑导航菜单', + 'remark' => '编辑导航菜单', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了导航菜单:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'menu_add', + 'title' => '添加导航菜单', + 'remark' => '添加导航菜单', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了导航菜单:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'link_disable', + 'title' => '禁用友情链接', + 'remark' => '禁用友情链接', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了友情链接:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'link_enable', + 'title' => '启用友情链接', + 'remark' => '启用友情链接', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了友情链接:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'link_delete', + 'title' => '删除友情链接', + 'remark' => '删除友情链接', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了友情链接:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'link_edit', + 'title' => '编辑友情链接', + 'remark' => '编辑友情链接', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了友情链接:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'link_add', + 'title' => '添加友情链接', + 'remark' => '添加友情链接', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了友情链接:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'field_disable', + 'title' => '禁用模型字段', + 'remark' => '禁用模型字段', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了模型字段:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'field_enable', + 'title' => '启用模型字段', + 'remark' => '启用模型字段', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了模型字段:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'field_delete', + 'title' => '删除模型字段', + 'remark' => '删除模型字段', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了模型字段:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'field_edit', + 'title' => '编辑模型字段', + 'remark' => '编辑模型字段', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了模型字段:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'field_add', + 'title' => '添加模型字段', + 'remark' => '添加模型字段', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了模型字段:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'column_disable', + 'title' => '禁用栏目', + 'remark' => '禁用栏目', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了栏目:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'column_enable', + 'title' => '启用栏目', + 'remark' => '启用栏目', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了栏目:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'column_delete', + 'title' => '删除栏目', + 'remark' => '删除栏目', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了栏目:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'column_edit', + 'title' => '编辑栏目', + 'remark' => '编辑栏目', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了栏目:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'column_add', + 'title' => '添加栏目', + 'remark' => '添加栏目', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了栏目:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_type_disable', + 'title' => '禁用广告分类', + 'remark' => '禁用广告分类', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了广告分类:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_type_enable', + 'title' => '启用广告分类', + 'remark' => '启用广告分类', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了广告分类:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_type_delete', + 'title' => '删除广告分类', + 'remark' => '删除广告分类', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了广告分类:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_type_edit', + 'title' => '编辑广告分类', + 'remark' => '编辑广告分类', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了广告分类:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_type_add', + 'title' => '添加广告分类', + 'remark' => '添加广告分类', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了广告分类:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_disable', + 'title' => '禁用广告', + 'remark' => '禁用广告', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了广告:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_enable', + 'title' => '启用广告', + 'remark' => '启用广告', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了广告:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_delete', + 'title' => '删除广告', + 'remark' => '删除广告', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了广告:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_edit', + 'title' => '编辑广告', + 'remark' => '编辑广告', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了广告:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_add', + 'title' => '添加广告', + 'remark' => '添加广告', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了广告:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'document_disable', + 'title' => '禁用文档', + 'remark' => '禁用文档', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了文档:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'document_enable', + 'title' => '启用文档', + 'remark' => '启用文档', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了文档:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'document_trash', + 'title' => '回收文档', + 'remark' => '回收文档', + 'rule' => '', + 'log' => '[user|get_nickname] 回收了文档:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'document_edit', + 'title' => '编辑文档', + 'remark' => '编辑文档', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了文档:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'document_add', + 'title' => '添加文档', + 'remark' => '添加文档', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了文档:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'slider_enable', + 'title' => '启用滚动图片', + 'remark' => '启用滚动图片', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了滚动图片:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'slider_disable', + 'title' => '禁用滚动图片', + 'remark' => '禁用滚动图片', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了滚动图片:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'support_add', + 'title' => '添加客服', + 'remark' => '添加客服', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了客服:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'support_edit', + 'title' => '编辑客服', + 'remark' => '编辑客服', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了客服:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'support_delete', + 'title' => '删除客服', + 'remark' => '删除客服', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了客服:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'support_enable', + 'title' => '启用客服', + 'remark' => '启用客服', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了客服:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'support_disable', + 'title' => '禁用客服', + 'remark' => '禁用客服', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了客服:[details]', + 'status' => 1, + ] + ], + + // 授权配置 + 'access' => [ + 'column' => [ + 'title' => '栏目授权', + 'nodes' => [ + 'group' => 'column', + 'table_name' => 'cms_column', + 'primary_key' => 'id', + 'parent_id' => 'pid', + 'node_name' => 'name', + ] + ], + ], +]; diff --git a/application/cms/menus.php b/application/cms/menus.php new file mode 100644 index 0000000..20f4f47 --- /dev/null +++ b/application/cms/menus.php @@ -0,0 +1,888 @@ + '门户', + 'icon' => 'fa fa-fw fa-newspaper-o', + 'url_type' => 'module_admin', + 'url_value' => 'cms/index/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '常用操作', + 'icon' => 'fa fa-fw fa-folder-open-o', + 'url_type' => 'module_admin', + 'url_value' => '', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '仪表盘', + 'icon' => 'fa fa-fw fa-tachometer', + 'url_type' => 'module_admin', + 'url_value' => 'cms/index/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '发布文档', + 'icon' => 'fa fa-fw fa-plus', + 'url_type' => 'module_admin', + 'url_value' => 'cms/document/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '文档列表', + 'icon' => 'fa fa-fw fa-list', + 'url_type' => 'module_admin', + 'url_value' => 'cms/document/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/document/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/document/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/document/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/document/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/document/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + [ + 'title' => '单页管理', + 'icon' => 'fa fa-fw fa-file-word-o', + 'url_type' => 'module_admin', + 'url_value' => 'cms/page/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/page/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/page/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/page/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/page/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/page/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/page/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + [ + 'title' => '回收站', + 'icon' => 'fa fa-fw fa-recycle', + 'url_type' => 'module_admin', + 'url_value' => 'cms/recycle/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/recycle/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '还原', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/recycle/restore', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + ], + ], + [ + 'title' => '内容管理', + 'icon' => 'fa fa-fw fa-th-list', + 'url_type' => 'module_admin', + 'url_value' => '', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [], + ], + [ + 'title' => '营销管理', + 'icon' => 'fa fa-fw fa-money', + 'url_type' => 'module_admin', + 'url_value' => '', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '广告管理', + 'icon' => 'fa fa-fw fa-handshake-o', + 'url_type' => 'module_admin', + 'url_value' => 'cms/advert/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/advert/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/advert/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/advert/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/advert/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/advert/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/advert/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '广告分类', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/advert_type/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/advert_type/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/advert_type/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/advert_type/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/advert_type/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/advert_type/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/advert_type/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + ], + ], + [ + 'title' => '滚动图片', + 'icon' => 'fa fa-fw fa-photo', + 'url_type' => 'module_admin', + 'url_value' => 'cms/slider/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/slider/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/slider/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/slider/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/slider/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/slider/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/slider/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + [ + 'title' => '友情链接', + 'icon' => 'fa fa-fw fa-link', + 'url_type' => 'module_admin', + 'url_value' => 'cms/link/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/link/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/link/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/link/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/link/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/link/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/link/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + [ + 'title' => '客服管理', + 'icon' => 'fa fa-fw fa-commenting', + 'url_type' => 'module_admin', + 'url_value' => 'cms/support/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/support/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/support/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/support/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/support/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/support/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/support/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + ], + ], + [ + 'title' => '门户设置', + 'icon' => 'fa fa-fw fa-sliders', + 'url_type' => 'module_admin', + 'url_value' => '', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '栏目分类', + 'icon' => 'fa fa-fw fa-sitemap', + 'url_type' => 'module_admin', + 'url_value' => 'cms/column/index', + 'url_target' => '_self', + 'online_hide' => 1, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/column/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/column/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/column/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/column/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/column/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/column/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + [ + 'title' => '内容模型', + 'icon' => 'fa fa-fw fa-th-large', + 'url_type' => 'module_admin', + 'url_value' => 'cms/model/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/model/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/model/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/model/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/model/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/model/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/model/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '字段管理', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/field/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/field/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/field/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/field/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/field/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/field/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/field/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + ], + ], + [ + 'title' => '导航管理', + 'icon' => 'fa fa-fw fa-map-signs', + 'url_type' => 'module_admin', + 'url_value' => 'cms/nav/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/nav/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/nav/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/nav/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/nav/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/nav/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/nav/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '菜单管理', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/menu/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/menu/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/menu/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/menu/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/menu/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/menu/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module_admin', + 'url_value' => 'cms/menu/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + ], + ], + ], + ], + ], + ], +]; diff --git a/application/cms/model/Advert.php b/application/cms/model/Advert.php new file mode 100644 index 0000000..835328d --- /dev/null +++ b/application/cms/model/Advert.php @@ -0,0 +1,43 @@ + + */ + protected function getTitleAttr($value, $data) { + switch ($data['type']) { + case 0: // 栏目 + break; + case 1: // 单页 + break; + } + } + + /** + * 获取栏目列表 + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed + */ + public static function getList() + { + $data_list = cache('cms_column_list'); + if (!$data_list) { + $data_list = self::where('status', 1)->column(true, 'id'); + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('cms_column_list', $data_list); + } + } + return $data_list; + } + + /** + * 获取树状栏目 + * @param int $id 需要隐藏的栏目id + * @param string $default 默认第一个节点项,默认为“顶级栏目”,如果为false则不显示,也可传入其他名称 + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed + */ + public static function getTreeList($id = 0, $default = '') + { + $result[0] = '顶级栏目'; + + // 排除指定节点及其子节点 + $where = [ + ['status', '=', 1] + ]; + if ($id !== 0) { + $hide_ids = array_merge([$id], self::getChildsId($id)); + $where[] = ['id', 'not in', $hide_ids]; + } + + $data_list = Tree::config(['title' => 'name'])->toList(self::where($where)->order('pid,id')->column('id,pid,name')); + foreach ($data_list as $item) { + $result[$item['id']] = $item['title_display']; + } + + // 设置默认节点项标题 + if ($default != '') { + $result[0] = $default; + } + + // 隐藏默认节点项 + if ($default === false) { + unset($result[0]); + } + + return $result; + } + + /** + * 获取所有子栏目id + * @param int $pid 父级id + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function getChildsId($pid = 0) + { + $ids = self::where('pid', $pid)->column('id'); + foreach ($ids as $value) { + $ids = array_merge($ids, self::getChildsId($value)); + } + return $ids; + } + + /** + * 获取指定栏目数据 + * @param int $cid 栏目id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|static + */ + public static function getInfo($cid = 0) + { + $result = cache('cms_column_info_'. $cid); + if (!$result) { + $result = self::get($cid); + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('cms_column_info_'. $cid, $result); + } + } + return $result; + } +} diff --git a/application/cms/model/Document.php b/application/cms/model/Document.php new file mode 100644 index 0000000..fd4fd59 --- /dev/null +++ b/application/cms/model/Document.php @@ -0,0 +1,221 @@ + + * @return \think\Paginator + * @throws \think\exception\DbException + */ + public static function getList($map = [], $order = []) + { + $data_list = self::view('cms_document', true) + ->view("cms_column", ['name' => 'column_name'], 'cms_column.id=cms_document.cid', 'left') + ->view("admin_user", 'username', 'admin_user.id=cms_document.uid', 'left') + ->where($map) + ->order($order) + ->paginate(); + return $data_list; + } + + /** + * 获取单篇文档 + * @param string $id 文档id + * @param string $model 独立模型id + * @param array $map 查询条件 + * @author 蔡伟明 <314013107@qq.com> + * @return array|string|ThinkModel|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public static function getOne($id = '', $model = '', $map = []) + { + if ($model == '') { + $document = self::get($id); + $extra_table = get_model_table($document['model']); + + $data = self::view('cms_document', true); + if ($extra_table != '') { + $data = $data->view($extra_table, true, 'cms_document.id='.$extra_table.'.aid', 'left'); + } + + return $data->view("cms_column", ['name' => 'column_name', 'list_template', 'detail_template'], 'cms_column.id=cms_document.cid', 'left') + ->view("admin_user", 'username', 'admin_user.id=cms_document.uid', 'left') + ->where('cms_document.id', $id) + ->where($map) + ->find(); + } else { + $table = get_model_table($model); + return Db::view($table, true) + ->view("cms_column", ['name' => 'column_name', 'list_template', 'detail_template'], 'cms_column.id='.$table.'.cid', 'left') + ->where($table.'.id', $id) + ->where($map) + ->find(); + } + } + + /** + * 新增或更新文档 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + * @throws \think\Exception + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + * @throws \think\exception\PDOException + */ + public function saveData() + { + $data = request()->post(); + $data['uid'] = UID; + + // 文档模型 + $model = Db::name('cms_model')->where('id', $data['model'])->find(); + + if ($model['type'] != 2 && empty($data['summary']) && config('cms_config.summary') > 0) { + $data['summary'] = mb_substr(strip_tags($data['content']), 0, config('cms_config.summary'), 'utf-8'); + } + + // 处理自定义属性 + if (isset($data['flag'])) { + $data['flag'] = implode(',', $data['flag']); + } else { + $data['flag'] = ''; + } + + // 验证基础内容 + if ($data['title'] == '') { + $this->error = '标题不能为空'; + return false; + } + + // 处理特殊字段类型 + $fields = FieldModel::where('model', $data['model'])->where('status', 1)->column('name,type'); + + foreach ($fields as $name => $type) { + if (!isset($data[$name])) { + switch ($type) { + // 开关 + case 'switch': + $data[$name] = 0; + break; + case 'checkbox': + $data[$name] = ''; + break; + } + } else { + // 如果值是数组则转换成字符串,适用于复选框等类型 + if (is_array($data[$name])) { + $data[$name] = implode(',', $data[$name]); + } + switch ($type) { + // 开关 + case 'switch': + $data[$name] = 1; + break; + // 日期时间 + case 'date': + case 'time': + case 'datetime': + $data[$name] = strtotime($data[$name]); + break; + } + } + } + + if (empty($data['id'])) { + if ($model['type'] == 2) { + // 新增独立模型文档 + $data['create_time'] = request()->time(); + $data['update_time'] = request()->time(); + $insert_id = Db::table($model['table'])->insertGetId($data); + if (false === $insert_id) { + $this->error = '新增失败'; + return false; + } else { + // 记录行为 + action_log('document_add', $model['table'], $insert_id, UID, $data['title']); + return true; + } + } else { + // 新增文档基础内容 + if ($document = self::create($data)) { + // 新增文档扩展内容 + if ($model['table'] != '') { + $data['aid'] = $document['id']; + if (false === Db::table($model['table'])->insert($data)) { + // 删除已添加的基础内容 + self::destroy($document['id']); + $this->error = '新增扩展内容出错'; + return false; + } + } + // 记录行为 + action_log('document_add', 'cms_document', $document['id'], UID, $document['title']); + return true; + } else { + $this->error = '新增基础内容出错'; + return false; + } + } + } else { + // 更新独立模型文档 + if ($model['type'] == 2) { + // 新增独立模型文档 + $data['update_time'] = request()->time(); + if (false === Db::table($model['table'])->update($data)) { + $this->error = '编辑失败'; + return false; + } else { + // 记录行为 + action_log('document_edit', $model['table'], $data['id'], UID, $data['title']); + return true; + } + } else { + // 更新文档基础内容 + if (self::update($data)) { + // 更新文档扩展内容 + $data['aid'] = $data['id']; + if (false !== Db::table($model['table'])->update($data)) { + // 记录行为 + action_log('document_edit', 'cms_document', $data['id'], UID, $data['title']); + return true; + } else { + $this->error = '更新扩展内容出错'; + return false; + } + } else { + $this->error = '更新基础内容出错'; + return false; + } + } + } + } +} diff --git a/application/cms/model/Field.php b/application/cms/model/Field.php new file mode 100644 index 0000000..f4c4c5a --- /dev/null +++ b/application/cms/model/Field.php @@ -0,0 +1,148 @@ + + * @return bool + */ + public function newField($field = null) + { + if ($field === null) { + $this->error = '缺少参数'; + return false; + } + + if ($this->tableExist($field['model'])) { + $sql = <<_table_name}` + ADD COLUMN `{$field['name']}` {$field['define']} COMMENT '{$field['title']}'; +EOF; + } else { + $mdoel_title = get_model_title($field['model']); + + // 新建普通扩展表 + $sql = <<_table_name}` ( + `aid` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '文档id' , + `{$field['name']}` {$field['define']} COMMENT '{$field['title']}' , + PRIMARY KEY (`aid`) + ) + ENGINE=MyISAM + DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci + CHECKSUM=0 + ROW_FORMAT=DYNAMIC + DELAY_KEY_WRITE=0 + COMMENT='{$mdoel_title}模型扩展表' + ; +EOF; + } + + try { + Db::execute($sql); + } catch(\Exception $e) { + $this->error = '字段添加失败'; + return false; + } + + return true; + } + + /** + * 更新字段 + * @param null $field 字段数据 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function updateField($field = null) + { + if ($field === null) { + return false; + } + + // 获取原字段名 + $field_name = $this->where('id', $field['id'])->value('name'); + + if ($this->tableExist($field['model'])) { + $sql = <<_table_name}` + CHANGE COLUMN `{$field_name}` `{$field['name']}` {$field['define']} COMMENT '{$field['title']}'; +EOF; + try { + Db::execute($sql); + } catch(\Exception $e) { + return false; + } + return true; + } else { + return false; + } + } + + /** + * 删除字段 + * @param null $field 字段数据 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function deleteField($field = null) + { + if ($field === null) { + return false; + } + + if ($this->tableExist($field['model'])) { + $sql = <<_table_name}` + DROP COLUMN `{$field['name']}`; +EOF; + try { + Db::execute($sql); + } catch(\Exception $e) { + return false; + } + return true; + } else { + return false; + } + } + + /** + * 检查表是否存在 + * @param string $model 文档模型id + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + private function tableExist($model = '') + { + $this->_table_name = strtolower(get_model_table($model)); + return true == Db::query("SHOW TABLES LIKE '{$this->_table_name}'"); + } +} diff --git a/application/cms/model/Link.php b/application/cms/model/Link.php new file mode 100644 index 0000000..880e3c0 --- /dev/null +++ b/application/cms/model/Link.php @@ -0,0 +1,25 @@ + + * @return array|mixed + */ + public static function getList() + { + $data_list = cache('cms_model_list'); + if (!$data_list) { + $data_list = self::where('status', 1)->column(true, 'id'); + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('cms_model_list', $data_list); + } + } + return $data_list; + } + + /** + * 获取内容模型标题列表(只含id和title) + * @param array $map 筛选条件 + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed + */ + public static function getTitleList($map = []) + { + return self::where('status', 1)->where($map)->column('id,title'); + } + + /** + * 删除附加表 + * @param null $model 内容模型id + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public static function deleteTable($model = null) + { + if ($model === null) { + return false; + } + + $table_name = self::where('id', $model)->value('table'); + return false !== Db::execute("DROP TABLE IF EXISTS `{$table_name}`"); + } + + /** + * 创建独立模型表 + * @param mixed $data 模型数据 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public static function createTable($data) + { + if ($data['type'] == 2) { + // 新建独立扩展表 + $sql = << $data['id'], + 'level' => '', + 'create_time' => request()->time(), + 'update_time' => request()->time(), + 'status' => 1 + ]; + $data = [ + [ + 'name' => 'id', + 'title' => '文档id', + 'define' => 'int(11) UNSIGNED NOT NULL', + 'type' => 'text', + 'show' => 0 + ], + [ + 'name' => 'cid', + 'title' => '栏目', + 'define' => 'int(11) UNSIGNED NOT NULL', + 'type' => 'static', + 'show' => 0, + 'value' => 0, + ], + [ + 'name' => 'uid', + 'title' => '用户id', + 'define' => 'int(11) UNSIGNED NOT NULL', + 'type' => 'text', + 'show' => 0, + 'value' => 0, + ], + [ + 'name' => 'model', + 'title' => '文档模型', + 'define' => 'int(11) UNSIGNED NOT NULL', + 'type' => 'text', + 'show' => 0, + 'value' => 0, + ], + [ + 'name' => 'title', + 'title' => '标题', + 'define' => 'varchar(256) NOT NULL', + 'type' => 'text', + 'show' => 1 + ], + [ + 'name' => 'create_time', + 'title' => '创建时间', + 'define' => 'int(11) UNSIGNED NOT NULL', + 'type' => 'datetime', + 'show' => 0, + 'value' => 0, + ], + [ + 'name' => 'update_time', + 'title' => '更新时间', + 'define' => 'int(11) UNSIGNED NOT NULL', + 'type' => 'datetime', + 'show' => 0, + 'value' => 0, + ], + [ + 'name' => 'sort', + 'title' => '排序', + 'define' => 'int(11) UNSIGNED NOT NULL', + 'type' => 'text', + 'show' => 1, + 'value' => 100, + ], + [ + 'name' => 'status', + 'title' => '状态', + 'define' => 'tinyint(2) NOT NULL', + 'type' => 'radio', + 'show' => 1, + 'value' => 1, + 'options' => '0:禁用 +1:启用' + ], + [ + 'name' => 'view', + 'title' => '点击量', + 'define' => 'int(11) UNSIGNED NOT NULL', + 'type' => 'text', + 'show' => 0, + 'value' => 0 + ], + [ + 'name' => 'trash', + 'title' => '回收站', + 'define' => 'tinyint(2) NOT NULL', + 'type' => 'radio', + 'show' => 0, + 'value' => 0 + ] + ]; + + foreach ($data as $item) { + $item = array_merge($item, $default); + Db::name('cms_field')->insert($item); + } + } + return true; + } +} diff --git a/application/cms/model/Nav.php b/application/cms/model/Nav.php new file mode 100644 index 0000000..ab485ce --- /dev/null +++ b/application/cms/model/Nav.php @@ -0,0 +1,25 @@ + + * @return array|mixed + */ + public static function getTitleList() + { + $result = cache('cms_page_title_list'); + if (!$result) { + $result = self::where('status', 1)->column('id,title'); + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('cms_page_title_list', $result); + } + } + return $result; + } +} diff --git a/application/cms/model/Slider.php b/application/cms/model/Slider.php new file mode 100644 index 0000000..b2c7f68 --- /dev/null +++ b/application/cms/model/Slider.php @@ -0,0 +1,25 @@ + + */ + +jQuery(function () { + // 字段定义列表 + var $field_define_list = { + text: "varchar(128) NOT NULL", + textarea: "varchar(256) NOT NULL", + static: "varchar(128) NOT NULL", + password: "varchar(128) NOT NULL", + checkbox: "varchar(32) NOT NULL", + radio: "varchar(32) NOT NULL", + date: "int(11) UNSIGNED NOT NULL", + time: "int(11) UNSIGNED NOT NULL", + datetime: "int(11) UNSIGNED NOT NULL", + hidden: "varchar(32) NOT NULL", + switch: "varchar(16) NOT NULL", + array: "varchar(32) NOT NULL", + select: "varchar(32) NOT NULL", + linkage: "varchar(32) NOT NULL", + linkages: "varchar(32) NOT NULL", + image: "int(11) UNSIGNED NOT NULL", + images: "varchar(64) NOT NULL", + file: "int(11) UNSIGNED NOT NULL", + files: "varchar(64) NOT NULL", + ueditor: "text NOT NULL", + wangeditor: "text NOT NULL", + editormd: "text NOT NULL", + ckeditor: "text NOT NULL", + summernote: "text NOT NULL", + icon: "varchar(64) NOT NULL", + tags: "varchar(128) NOT NULL", + number: "int(11) UNSIGNED NOT NULL", + bmap: "varchar(32) NOT NULL", + colorpicker: "varchar(32) NOT NULL", + jcrop: "int(11) UNSIGNED NOT NULL", + masked: "varchar(64) NOT NULL", + range: "varchar(128) NOT NULL" + }; + // 选择自动类型,自动填写字段定义 + var $field_define = jQuery('input[name=define]'); + jQuery('select[name=type]').change(function () { + $field_define.val($field_define_list[$(this).val()] || ''); + }); +}); \ No newline at end of file diff --git a/application/cms/sql/install.sql b/application/cms/sql/install.sql new file mode 100644 index 0000000..696b870 --- /dev/null +++ b/application/cms/sql/install.sql @@ -0,0 +1,320 @@ +-- ----------------------------- +-- 导出时间 `2016-12-13 22:26:46` +-- ----------------------------- + +-- ----------------------------- +-- 表结构 `dp_cms_advert` +-- ----------------------------- +DROP TABLE IF EXISTS `dp_cms_advert`; +CREATE TABLE `dp_cms_advert` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `typeid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '分类id', + `tagname` varchar(30) NOT NULL DEFAULT '' COMMENT '广告位标识', + `ad_type` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '广告类型', + `timeset` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '时间限制:0-永不过期,1-在设内时间内有效', + `start_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '开始时间', + `end_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '结束时间', + `name` varchar(60) NOT NULL DEFAULT '' COMMENT '广告位名称', + `content` text NOT NULL COMMENT '广告内容', + `expcontent` text NOT NULL COMMENT '过期显示内容', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='广告表'; + +-- ----------------------------- +-- 表数据 `dp_cms_advert` +-- ----------------------------- + +-- ----------------------------- +-- 表结构 `dp_cms_advert_type` +-- ----------------------------- +DROP TABLE IF EXISTS `dp_cms_advert_type`; +CREATE TABLE `dp_cms_advert_type` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '分类名称', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='广告分类表'; + +-- ----------------------------- +-- 表数据 `dp_cms_advert_type` +-- ----------------------------- + +-- ----------------------------- +-- 表结构 `dp_cms_column` +-- ----------------------------- +DROP TABLE IF EXISTS `dp_cms_column`; +CREATE TABLE `dp_cms_column` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `pid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '父级id', + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '栏目名称', + `model` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '文档模型id', + `url` varchar(255) NOT NULL DEFAULT '' COMMENT '链接', + `target` varchar(16) NOT NULL DEFAULT '_self' COMMENT '链接打开方式', + `content` text NOT NULL COMMENT '内容', + `icon` varchar(64) NOT NULL DEFAULT '' COMMENT '字体图标', + `index_template` varchar(32) NOT NULL DEFAULT '' COMMENT '封面模板', + `list_template` varchar(32) NOT NULL DEFAULT '' COMMENT '列表页模板', + `detail_template` varchar(32) NOT NULL DEFAULT '' COMMENT '详情页模板', + `post_auth` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '投稿权限', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `sort` int(11) NOT NULL DEFAULT '100' COMMENT '排序', + `status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '状态', + `hide` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '是否隐藏', + `rank_auth` int(11) NOT NULL DEFAULT '0' COMMENT '浏览权限,-1待审核,0为开放浏览,大于0则为对应的用户角色id', + `type` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '栏目属性:0-最终列表栏目,1-外部链接,2-频道封面', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='栏目表'; + +-- ----------------------------- +-- 表数据 `dp_cms_column` +-- ----------------------------- + +-- ----------------------------- +-- 表结构 `dp_cms_document` +-- ----------------------------- +DROP TABLE IF EXISTS `dp_cms_document`; +CREATE TABLE `dp_cms_document` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `cid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '栏目id', + `model` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '文档模型ID', + `title` varchar(256) NOT NULL DEFAULT '' COMMENT '标题', + `shorttitle` varchar(32) NOT NULL DEFAULT '' COMMENT '简略标题', + `uid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID', + `flag` set('j','p','b','s','a','f','c','h') DEFAULT NULL COMMENT '自定义属性', + `view` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '阅读量', + `comment` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '评论数', + `good` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '点赞数', + `bad` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '踩数', + `mark` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '收藏数量', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `sort` int(11) NOT NULL DEFAULT '100' COMMENT '排序', + `status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '状态', + `trash` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '回收站', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='文档基础表'; + +-- ----------------------------- +-- 表数据 `dp_cms_document` +-- ----------------------------- + +-- ----------------------------- +-- 表结构 `dp_cms_field` +-- ----------------------------- +DROP TABLE IF EXISTS `dp_cms_field`; +CREATE TABLE `dp_cms_field` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '字段名称', + `name` varchar(32) NOT NULL, + `title` varchar(32) NOT NULL DEFAULT '' COMMENT '字段标题', + `type` varchar(32) NOT NULL DEFAULT '' COMMENT '字段类型', + `define` varchar(128) NOT NULL DEFAULT '' COMMENT '字段定义', + `value` text NULL COMMENT '默认值', + `options` text NULL COMMENT '额外选项', + `tips` varchar(256) NOT NULL DEFAULT '' COMMENT '提示说明', + `fixed` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '是否为固定字段', + `show` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '是否显示', + `model` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '所属文档模型id', + `ajax_url` varchar(256) NOT NULL DEFAULT '' COMMENT '联动下拉框ajax地址', + `next_items` varchar(256) NOT NULL DEFAULT '' COMMENT '联动下拉框的下级下拉框名,多个以逗号隔开', + `param` varchar(32) NOT NULL DEFAULT '' COMMENT '联动下拉框请求参数名', + `format` varchar(32) NOT NULL DEFAULT '' COMMENT '格式,用于格式文本', + `table` varchar(32) NOT NULL DEFAULT '' COMMENT '表名,只用于快速联动类型', + `level` tinyint(2) unsigned NOT NULL DEFAULT '2' COMMENT '联动级别,只用于快速联动类型', + `key` varchar(32) NOT NULL DEFAULT '' COMMENT '键字段,只用于快速联动类型', + `option` varchar(32) NOT NULL DEFAULT '' COMMENT '值字段,只用于快速联动类型', + `pid` varchar(32) NOT NULL DEFAULT '' COMMENT '父级id字段,只用于快速联动类型', + `ak` varchar(32) NOT NULL DEFAULT '' COMMENT '百度地图appkey', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `sort` int(11) NOT NULL DEFAULT '100' COMMENT '排序', + `status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=18 DEFAULT CHARSET=utf8 COMMENT='文档字段表'; + +-- ----------------------------- +-- 表数据 `dp_cms_field` +-- ----------------------------- +INSERT INTO `dp_cms_field` VALUES ('1', 'id', 'ID', 'text', 'int(11) UNSIGNED NOT NULL', '0', '', 'ID', '0', '0', '0', '', '', '', '', '', '0', '', '', '', '', '1480562978', '1480562978', '100', '1'); +INSERT INTO `dp_cms_field` VALUES ('2', 'cid', '栏目', 'select', 'int(11) UNSIGNED NOT NULL', '0', '', '请选择所属栏目', '0', '0', '0', '', '', '', '', '', '0', '', '', '', '', '1480562978', '1480562978', '100', '1'); +INSERT INTO `dp_cms_field` VALUES ('3', 'uid', '用户ID', 'text', 'int(11) UNSIGNED NOT NULL', '0', '', '', '0', '0', '0', '', '', '', '', '', '0', '', '', '', '', '1480563110', '1480563110', '100', '1'); +INSERT INTO `dp_cms_field` VALUES ('4', 'model', '模型ID', 'text', 'int(11) UNSIGNED NOT NULL', '0', '', '', '0', '0', '0', '', '', '', '', '', '0', '', '', '', '', '1480563110', '1480563110', '100', '1'); +INSERT INTO `dp_cms_field` VALUES ('5', 'title', '标题', 'text', 'varchar(128) NOT NULL', '', '', '文档标题', '0', '1', '0', '', '', '', '', '', '0', '', '', '', '', '1480575844', '1480576134', '1', '1'); +INSERT INTO `dp_cms_field` VALUES ('6', 'shorttitle', '简略标题', 'text', 'varchar(32) NOT NULL', '', '', '简略标题', '0', '1', '0', '', '', '', '', '', '0', '', '', '', '', '1480575844', '1480576134', '1', '1'); +INSERT INTO `dp_cms_field` VALUES ('7', 'flag', '自定义属性', 'checkbox', 'set(\'j\',\'p\',\'b\',\'s\',\'a\',\'f\',\'h\',\'c\') NULL DEFAULT NULL', '', 'j:跳转\r\np:图片\r\nb:加粗\r\ns:滚动\r\na:特荐\r\nf:幻灯\r\nh:头条\r\nc:推荐', '自定义属性', '0', '1', '0', '', '', '', '', '', '0', '', '', '', '', '1480671258', '1480671258', '100', '1'); +INSERT INTO `dp_cms_field` VALUES ('8', 'view', '阅读量', 'text', 'int(11) UNSIGNED NOT NULL', '0', '', '', '0', '1', '0', '', '', '', '', '', '0', '', '', '', '', '1480563149', '1480563149', '100', '1'); +INSERT INTO `dp_cms_field` VALUES ('9', 'comment', '评论数', 'text', 'int(11) UNSIGNED NOT NULL', '0', '', '', '0', '0', '0', '', '', '', '', '', '0', '', '', '', '', '1480563189', '1480563189', '100', '1'); +INSERT INTO `dp_cms_field` VALUES ('10', 'good', '点赞数', 'text', 'int(11) UNSIGNED NOT NULL', '0', '', '', '0', '0', '0', '', '', '', '', '', '0', '', '', '', '', '1480563279', '1480563279', '100', '1'); +INSERT INTO `dp_cms_field` VALUES ('11', 'bad', '踩数', 'text', 'int(11) UNSIGNED NOT NULL', '0', '', '', '0', '0', '0', '', '', '', '', '', '0', '', '', '', '', '1480563330', '1480563330', '100', '1'); +INSERT INTO `dp_cms_field` VALUES ('12', 'mark', '收藏数量', 'text', 'int(11) UNSIGNED NOT NULL', '0', '', '', '0', '0', '0', '', '', '', '', '', '0', '', '', '', '', '1480563372', '1480563372', '100', '1'); +INSERT INTO `dp_cms_field` VALUES ('13', 'create_time', '创建时间', 'datetime', 'int(11) UNSIGNED NOT NULL', '0', '', '', '0', '0', '0', '', '', '', '', '', '0', '', '', '', '', '1480563406', '1480563406', '100', '1'); +INSERT INTO `dp_cms_field` VALUES ('14', 'update_time', '更新时间', 'datetime', 'int(11) UNSIGNED NOT NULL', '0', '', '', '0', '0', '0', '', '', '', '', '', '0', '', '', '', '', '1480563432', '1480563432', '100', '1'); +INSERT INTO `dp_cms_field` VALUES ('15', 'sort', '排序', 'text', 'int(11) NOT NULL', '100', '', '', '0', '1', '0', '', '', '', '', '', '0', '', '', '', '', '1480563510', '1480563510', '100', '1'); +INSERT INTO `dp_cms_field` VALUES ('16', 'status', '状态', 'radio', 'tinyint(2) UNSIGNED NOT NULL', '1', '0:禁用\r\n1:启用', '', '0', '1', '0', '', '', '', '', '', '0', '', '', '', '', '1480563576', '1480563576', '100', '1'); +INSERT INTO `dp_cms_field` VALUES ('17', 'trash', '回收站', 'text', 'tinyint(2) UNSIGNED NOT NULL', '0', '', '', '0', '0', '0', '', '', '', '', '', '0', '', '', '', '', '1480563576', '1480563576', '100', '1'); + +-- ----------------------------- +-- 表结构 `dp_cms_link` +-- ----------------------------- +DROP TABLE IF EXISTS `dp_cms_link`; +CREATE TABLE `dp_cms_link` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `type` tinyint(2) unsigned NOT NULL DEFAULT '1' COMMENT '类型:1-文字链接,2-图片链接', + `title` varchar(128) NOT NULL DEFAULT '' COMMENT '链接标题', + `url` varchar(255) NOT NULL DEFAULT '' COMMENT '链接地址', + `logo` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '链接LOGO', + `contact` varchar(255) NOT NULL DEFAULT '' COMMENT '联系方式', + `sort` int(11) NOT NULL DEFAULT '100', + `status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '状态', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='有钱链接表'; + +-- ----------------------------- +-- 表数据 `dp_cms_link` +-- ----------------------------- + +-- ----------------------------- +-- 表结构 `dp_cms_menu` +-- ----------------------------- +DROP TABLE IF EXISTS `dp_cms_menu`; +CREATE TABLE `dp_cms_menu` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `nid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '导航id', + `pid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '父级id', + `column` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '栏目id', + `page` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '单页id', + `type` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '类型:0-栏目链接,1-单页链接,2-自定义链接', + `title` varchar(128) NOT NULL DEFAULT '' COMMENT '菜单标题', + `url` varchar(255) NOT NULL DEFAULT '' COMMENT '链接', + `css` varchar(64) NOT NULL DEFAULT '' COMMENT 'css类', + `rel` varchar(64) NOT NULL DEFAULT '' COMMENT '链接关系网', + `target` varchar(16) NOT NULL DEFAULT '' COMMENT '打开方式', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `sort` int(11) NOT NULL DEFAULT '100' COMMENT '排序', + `status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COMMENT='菜单表'; + +-- ----------------------------- +-- 表数据 `dp_cms_menu` +-- ----------------------------- +INSERT INTO `dp_cms_menu` VALUES ('1', '1', '0', '0', '0', '2', '首页', 'cms/index/index', '', '', '_self', '1492345605', '1492345605', '100', '1'); +INSERT INTO `dp_cms_menu` VALUES ('2', '2', '0', '0', '0', '2', '关于我们', 'http://www.dolphinphp.com', '', '', '_self', '1492346763', '1492346763', '100', '1'); +INSERT INTO `dp_cms_menu` VALUES ('3', '3', '0', '0', '0', '2', '开发文档', 'http://www.kancloud.cn/ming5112/dolphinphp', '', '', '_self', '1492346812', '1492346812', '100', '1'); +INSERT INTO `dp_cms_menu` VALUES ('4', '3', '0', '0', '0', '2', '开发者社区', 'http://bbs.dolphinphp.com/', '', '', '_self', '1492346832', '1492346832', '100', '1'); +INSERT INTO `dp_cms_menu` VALUES ('5', '1', '0', '0', '0', '2', '二级菜单', 'http://www.dolphinphp.com', '', '', '_self', '1492347372', '1492347510', '100', '1'); +INSERT INTO `dp_cms_menu` VALUES ('6', '1', '5', '0', '0', '2', '子菜单', 'http://www.dolphinphp.com', '', '', '_self', '1492347388', '1492347520', '100', '1'); + +-- ----------------------------- +-- 表结构 `dp_cms_model` +-- ----------------------------- +DROP TABLE IF EXISTS `dp_cms_model`; +CREATE TABLE `dp_cms_model` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '模型名称', + `title` varchar(32) NOT NULL DEFAULT '' COMMENT '模型标题', + `table` varchar(64) NOT NULL DEFAULT '' COMMENT '附加表名称', + `type` tinyint(2) NOT NULL DEFAULT '1' COMMENT '模型类别:0-系统模型,1-普通模型,2-独立模型', + `icon` varchar(64) NOT NULL, + `sort` int(11) NOT NULL DEFAULT '100' COMMENT '排序', + `system` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '是否系统模型', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='内容模型表'; + +-- ----------------------------- +-- 表结构 `dp_cms_nav` +-- ----------------------------- +DROP TABLE IF EXISTS `dp_cms_nav`; +CREATE TABLE `dp_cms_nav` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `tag` varchar(32) NOT NULL DEFAULT '' COMMENT '导航标识', + `title` varchar(32) NOT NULL DEFAULT '' COMMENT '菜单标题', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='导航表'; + +-- ----------------------------- +-- 表数据 `dp_cms_nav` +-- ----------------------------- +INSERT INTO `dp_cms_nav` VALUES ('1', 'main_nav', '顶部导航', '1492345083', '1492345083', '1'); +INSERT INTO `dp_cms_nav` VALUES ('2', 'about_nav', '底部关于', '1492346685', '1492346685', '1'); +INSERT INTO `dp_cms_nav` VALUES ('3', 'support_nav', '服务与支持', '1492346715', '1492346715', '1'); + +-- ----------------------------- +-- 表结构 `dp_cms_page` +-- ----------------------------- +DROP TABLE IF EXISTS `dp_cms_page`; +CREATE TABLE `dp_cms_page` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `title` varchar(64) NOT NULL DEFAULT '' COMMENT '单页标题', + `content` mediumtext NOT NULL COMMENT '单页内容', + `keywords` varchar(32) NOT NULL DEFAULT '' COMMENT '关键词', + `description` varchar(250) NOT NULL DEFAULT '' COMMENT '页面描述', + `template` varchar(32) NOT NULL DEFAULT '' COMMENT '模板文件', + `cover` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '单页封面', + `view` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '阅读量', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='单页表'; + +-- ----------------------------- +-- 表数据 `dp_cms_page` +-- ----------------------------- + +-- ----------------------------- +-- 表结构 `dp_cms_slider` +-- ----------------------------- +DROP TABLE IF EXISTS `dp_cms_slider`; +CREATE TABLE `dp_cms_slider` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `title` varchar(32) NOT NULL DEFAULT '' COMMENT '标题', + `cover` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '封面id', + `url` varchar(255) NOT NULL DEFAULT '' COMMENT '链接地址', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `sort` int(11) unsigned NOT NULL DEFAULT '100' COMMENT '排序', + `status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='滚动图片表'; + +-- ----------------------------- +-- 表数据 `dp_cms_slider` +-- ----------------------------- + +-- ----------------------------- +-- 表结构 `dp_cms_support` +-- ----------------------------- +DROP TABLE IF EXISTS `dp_cms_support`; +CREATE TABLE `dp_cms_support` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(128) NOT NULL DEFAULT '' COMMENT '客服名称', + `qq` varchar(16) NOT NULL DEFAULT '' COMMENT 'QQ', + `msn` varchar(100) NOT NULL DEFAULT '' COMMENT 'msn', + `taobao` varchar(100) NOT NULL DEFAULT '' COMMENT 'taobao', + `alibaba` varchar(100) NOT NULL DEFAULT '' COMMENT 'alibaba', + `skype` varchar(100) NOT NULL DEFAULT '' COMMENT 'skype', + `status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '状态', + `sort` int(11) unsigned NOT NULL DEFAULT '100' COMMENT '排序', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='客服表'; + +-- ----------------------------- +-- 表数据 `dp_cms_support` +-- ----------------------------- diff --git a/application/cms/sql/uninstall.sql b/application/cms/sql/uninstall.sql new file mode 100644 index 0000000..80e40e1 --- /dev/null +++ b/application/cms/sql/uninstall.sql @@ -0,0 +1,15 @@ +-- ----------------------------- +-- 导出时间 `2016-12-13 22:26:46` +-- ----------------------------- +DROP TABLE IF EXISTS `dp_cms_advert`; +DROP TABLE IF EXISTS `dp_cms_advert_type`; +DROP TABLE IF EXISTS `dp_cms_column`; +DROP TABLE IF EXISTS `dp_cms_document`; +DROP TABLE IF EXISTS `dp_cms_field`; +DROP TABLE IF EXISTS `dp_cms_link`; +DROP TABLE IF EXISTS `dp_cms_menu`; +DROP TABLE IF EXISTS `dp_cms_model`; +DROP TABLE IF EXISTS `dp_cms_nav`; +DROP TABLE IF EXISTS `dp_cms_page`; +DROP TABLE IF EXISTS `dp_cms_slider`; +DROP TABLE IF EXISTS `dp_cms_support`; diff --git a/application/cms/uninstall.php b/application/cms/uninstall.php new file mode 100644 index 0000000..f70d90c --- /dev/null +++ b/application/cms/uninstall.php @@ -0,0 +1,34 @@ +request->get('clear'); + +if ($clear == 1) { + // 内容模型的表名列表 + $table_list = Db::name('cms_model')->column('table'); + + if ($table_list) { + foreach ($table_list as $table) { + // 删除内容模型表 + $sql = 'DROP TABLE IF EXISTS `'.$table.'`;'; + try { + Db::execute($sql); + } catch (\Exception $e) { + throw new Exception('删除表:'.$table.' 失败!', 1001); + } + } + } +} diff --git a/application/cms/validate/Action.php b/application/cms/validate/Action.php new file mode 100644 index 0000000..52ee788 --- /dev/null +++ b/application/cms/validate/Action.php @@ -0,0 +1,35 @@ + + */ +class Action extends Validate +{ + //定义验证规则 + protected $rule = [ + 'module|所属模块' => 'require', + 'name|行为标识' => 'require|regex:^[a-zA-Z]\w{0,39}$|unique:admin_action', + 'title|行为名称' => 'require|length:1,80', + 'remark|行为描述' => 'require|length:1,128' + ]; + + //定义验证提示 + protected $message = [ + 'name.regex' => '行为标识由字母和下划线组成', + ]; +} diff --git a/application/cms/validate/Advert.php b/application/cms/validate/Advert.php new file mode 100644 index 0000000..7fed738 --- /dev/null +++ b/application/cms/validate/Advert.php @@ -0,0 +1,55 @@ + + */ +class Advert extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'typeid|广告分类' => 'require', + 'tagname|广告位标识' => 'require|regex:^[a-z]+[a-z0-9_]{0,20}$|unique:cms_advert', + 'name|广告位名称' => 'require|unique:cms_advert', + 'start_time' => 'requireIf:timeset,1', + 'end_time' => 'requireIf:timeset,1', + 'title' => 'requireIf:ad_type,1', + 'code' => 'requireIf:ad_type,0', + 'size' => 'integer', + 'width' => 'integer', + 'height' => 'integer', + 'src' => 'requireIf:ad_type,2', + ]; + + // 定义验证提示 + protected $message = [ + 'tagname.regex' => '广告位标识由小写字母、数字或下划线组成,不能以数字开头', + 'code' => '代码不能为空', + 'src' => '请上传图片', + 'title' => '文字内容不能为空', + 'start_time' => '开始时间不能为空', + 'end_time' => '结束时间不能为空', + 'size' => '文字大小只能填写数字', + 'width' => '宽度只能填写数字', + 'height' => '高度只能填写数字', + ]; + + // 定义验证场景 + protected $scene = [ + 'name' => ['name'] + ]; +} diff --git a/application/cms/validate/AdvertType.php b/application/cms/validate/AdvertType.php new file mode 100644 index 0000000..1757a93 --- /dev/null +++ b/application/cms/validate/AdvertType.php @@ -0,0 +1,32 @@ + + */ +class AdvertType extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'name|分类名称' => 'require|length:1,30|unique:cms_advert_type' + ]; + + // 定义验证场景 + protected $scene = [ + 'name' => ['name'] + ]; +} diff --git a/application/cms/validate/Column.php b/application/cms/validate/Column.php new file mode 100644 index 0000000..d8079cf --- /dev/null +++ b/application/cms/validate/Column.php @@ -0,0 +1,29 @@ + + */ +class Column extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'pid|所属栏目' => 'require', + 'name|栏目名称' => 'require|unique:cms_column,name^pid', + 'model|内容模型' => 'require', + ]; +} diff --git a/application/cms/validate/Field.php b/application/cms/validate/Field.php new file mode 100644 index 0000000..dd30fae --- /dev/null +++ b/application/cms/validate/Field.php @@ -0,0 +1,36 @@ + + */ +class Field extends Validate +{ + //定义验证规则 + protected $rule = [ + 'name|字段名称' => 'require|regex:^[a-z]\w{0,39}$|unique:cms_field,name^model', + 'title|字段标题' => 'require|length:1,30', + 'type|字段类型' => 'require|length:1,30', + 'define|字段定义' => 'require|length:1,100', + 'tips|字段说明' => 'length:1,200', + ]; + + //定义验证提示 + protected $message = [ + 'name.regex' => '字段名称由小写字母和下划线组成', + ]; +} diff --git a/application/cms/validate/Link.php b/application/cms/validate/Link.php new file mode 100644 index 0000000..ca331aa --- /dev/null +++ b/application/cms/validate/Link.php @@ -0,0 +1,40 @@ + + */ +class Link extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'title|链接标题' => 'require|length:1,30', + 'url|链接地址' => 'require|url', + 'logo|链接LOGO' => 'requireIf:type,2', + ]; + + // 定义验证提示 + protected $message = [ + 'logo.requireIf' => '请上传链接LOGO', + ]; + + // 定义验证场景 + protected $scene = [ + 'title' => ['title'], + 'url' => ['url' => 'require'], + ]; +} diff --git a/application/cms/validate/Menu.php b/application/cms/validate/Menu.php new file mode 100644 index 0000000..8f96452 --- /dev/null +++ b/application/cms/validate/Menu.php @@ -0,0 +1,43 @@ + + */ +class Menu extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'column|栏目' => 'requireIf:type,0', + 'page|单页' => 'requireIf:type,1', + 'title|菜单标题' => 'requireIf:type,2|length:1,30', + 'url|URL' => 'requireIf:type,2', + ]; + + // 定义验证提示 + protected $message = [ + 'column.requireIf' => '请选择栏目', + 'page.requireIf' => '请选择单页', + 'title.requireIf' => '菜单标题不能为空', + 'url.requireIf' => 'URL不能为空' + ]; + + // 定义验证场景 + protected $scene = [ + 'title' => ['title'] + ]; +} diff --git a/application/cms/validate/Model.php b/application/cms/validate/Model.php new file mode 100644 index 0000000..84bc793 --- /dev/null +++ b/application/cms/validate/Model.php @@ -0,0 +1,40 @@ + + */ +class Model extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'name|模型标识' => 'require|regex:^[a-z]+[a-z0-9_]{0,39}$|unique:cms_model', + 'title|模型标题' => 'require|length:1,30|unique:cms_model', + 'table|附加表' => 'regex:^[#@a-z]+[a-z0-9#@_]{0,60}$|unique:cms_model', + ]; + + // 定义验证提示 + protected $message = [ + 'name.regex' => '模型标识由小写字母、数字或下划线组成,不能以数字开头', + 'table.regex' => '附加表由小写字母、数字或下划线组成,不能以数字开头', + ]; + + // 定义场景 + protected $scene = [ + 'edit' => ['title'], + ]; +} diff --git a/application/cms/validate/Nav.php b/application/cms/validate/Nav.php new file mode 100644 index 0000000..75e8311 --- /dev/null +++ b/application/cms/validate/Nav.php @@ -0,0 +1,34 @@ + + */ +class Nav extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'tag|菜单标识' => 'require|length:1,30|unique:cms_nav', + 'title|菜单标题' => 'require|length:1,30|unique:cms_nav' + ]; + + // 定义验证场景 + protected $scene = [ + 'tag' => ['tag'], + 'title' => ['title'] + ]; +} diff --git a/application/cms/validate/Page.php b/application/cms/validate/Page.php new file mode 100644 index 0000000..c54f7ec --- /dev/null +++ b/application/cms/validate/Page.php @@ -0,0 +1,32 @@ + + */ +class Page extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'title|页面标题' => 'require|length:1,30' + ]; + + // 定义验证场景 + protected $scene = [ + 'title' => ['title'] + ]; +} diff --git a/application/cms/validate/Slider.php b/application/cms/validate/Slider.php new file mode 100644 index 0000000..aabf806 --- /dev/null +++ b/application/cms/validate/Slider.php @@ -0,0 +1,35 @@ + + */ +class Slider extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'title|标题' => 'require|length:1,30', + 'cover|图片' => 'require', +// 'url|链接' => 'require|url', + ]; + + // 定义验证场景 + protected $scene = [ + 'title' => ['title'], +// 'url' => ['url'], + ]; +} diff --git a/application/cms/validate/Support.php b/application/cms/validate/Support.php new file mode 100644 index 0000000..1b8b942 --- /dev/null +++ b/application/cms/validate/Support.php @@ -0,0 +1,34 @@ + + */ +class Support extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'name|客服名称' => 'require', + 'qq|QQ号码' => 'number', + 'msn|MSN' => 'email', + ]; + + // 定义验证场景 + protected $scene = [ + 'name' => ['name'] + ]; +} diff --git a/application/cms/view/admin/index/index.html b/application/cms/view/admin/index/index.html new file mode 100644 index 0000000..5bebc6d --- /dev/null +++ b/application/cms/view/admin/index/index.html @@ -0,0 +1,114 @@ +{extend name="$_admin_base_layout" /} + +{block name="content"} +
    + +

    新手指导

    +

    相关参数设置,请在【系统】>【系统功能】>【系统设置】>【门户】中设置,或 点此 跳转。

    + +
    + + +{/block} + +{block name="script"} + + +{/block} \ No newline at end of file diff --git a/application/cms/view/column/list.html b/application/cms/view/column/list.html new file mode 100644 index 0000000..0081221 --- /dev/null +++ b/application/cms/view/column/list.html @@ -0,0 +1,110 @@ +{extend name="public:layout" /} + +{block name="main-container"} + +
    + +
    +
    + +
    +

    {$column_info.name}

    +
    + +
    +
    + + + +
    +
    + +
    +
    + + +
    + +
    +
    +
    + {volist name="lists" id="item"} +
    +
    +
    + {notempty name="item.cover"} +
    + + + +
    +
    +
    + {$item.create_time|time_tran} + {$item.username} 发表于 {$item.create_time|format_time} +
    +

    {$item.title}

    + {present name="item.summary"} +

    {$item.summary|raw}

    + {/present} + +
    + {else/} +
    +
    + {$item.create_time|time_tran} + {$item.username} 发表于 {$item.create_time|format_time} +
    +

    {$item.title}

    + {present name="item.summary"} +

    {$item.summary|raw}

    + {/present} + +
    + {/notempty} +
    +
    +
    + {/volist} + + + + +
    +
    + +
    +
    +

    搜索

    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + +{/block} \ No newline at end of file diff --git a/application/cms/view/document/detail.html b/application/cms/view/document/detail.html new file mode 100644 index 0000000..8eb80f2 --- /dev/null +++ b/application/cms/view/document/detail.html @@ -0,0 +1,130 @@ +{extend name="public:layout" /} + +{block name="main-container"} + +
    + +
    +
    + +
    +

    {$document.title}

    +
    + +
    +
    + + + +
    +
    + +
    +
    + + +
    + +
    + +
    + +
    +
    + +{/block} + +{block name="style"} + +{/block} + +{block name="script"} + + +{/block} \ No newline at end of file diff --git a/application/cms/view/index/index.html b/application/cms/view/index/index.html new file mode 100644 index 0000000..92e56ed --- /dev/null +++ b/application/cms/view/index/index.html @@ -0,0 +1,200 @@ +{extend name="public:layout" /} + +{block name="main-container"} + +
    + {empty name="slider"} +
    +
    +
    + +
    +

    海豚PHP快速开发框架

    +

    极速 · 极简 · 极致

    + 点击下载 V1.0公测版 +
    当前版本:1.0.0 beta [更新日志] 下载量:0
    +

    + Coding.net or Github +

    +
    +
    +
    + +
    +
    + +
    +
    +
    + {/empty} + {notempty name="slider"} +
    + +
    + {volist name="slider" id="item"} +
    + {$item.title} +
    + {/volist} +
    + +
    + {/notempty} + + +
    +
    + +
    +
    +
    + + + +
    +

    Bootstrap Powered

    +

    Bootstrap is a sleek, intuitive, and powerful mobile first front-end framework for faster and easier web development. OneUI was built on top, extending it to a large degree.

    +
    +
    +
    + + + +
    +

    Fully Responsive

    +

    The User Interface will adjust to any screen size. It will look great on mobile devices and desktops at the same time. No need to worry about the UI, just stay focused on the development.

    +
    +
    +
    + + + +
    +

    Save time

    +

    OneUI will save you hundreds of hours of extra development. Start right away coding your functionality and watch your project come to life months sooner.

    +
    +
    +
    +
    +
    + + + +
    +

    Frontend Pages

    +

    Premium and fully responsive frontend pages are included in OneUI package, too. They use the same resources with the backend, so you can build your web application in one go, using all available components wherever you like.

    +
    +
    +
    + {less} +
    +

    LessCSS

    +

    OneUI was built from scratch with LessCSS. Completely modular design with components, variables and mixins that will help you customize and extend your framework to the maximum.

    +
    +
    +
    + + + +
    +

    Grunt Tasks

    +

    Grunt tasks will make your life easier. You can use them to live-compile your Less files to CSS as you work or build your custom color themes and framework.

    +
    +
    + +
    +
    + + + +
    +
    + +
    +
    +
    +
    Accounts Today
    +
    +
    +
    +
    Products
    +
    +
    +
    +
    Web Apps
    +
    +
    + +
    +
    + + + +
    +
    + +
    +
    +
    + + + +
    +

    Cross Browser Support

    +

    OneUI will play nice with all modern browsers such as Chrome, Firefox, Safari, Opera and the latest versions of Internet Explorer (9 and up).

    +
    +
    +
    + + + +
    +

    Documentation

    +

    OneUI comes packed with a great documentation which covers all the basics to get you familiar with template’s structure and files. It is the best way to get started.

    +
    +
    +
    + + + +
    +

    Clean & Commented Code

    +

    The code is created with the developer in mind. It is clean, easy to follow, easy to replicate and at the same time well commented, so that you never feel lost.

    +
    +
    +
    +
    +
    + + + +
    +

    Components

    +

    OneUI comes packed with so many unique components. Carefully picked and integrated to enhance and enrich your project with great functionality. Use them anywhere you want.

    +
    +
    +
    + + + +
    +

    Support

    +

    By purchasing a license of OneUI, you are eligible to email support. Should you get stuck somewhere or come accross any issue, don’t worry because I am here to provide assistance.

    +
    +
    +
    + + + +
    +

    Crafted With Love

    +

    I love what I do. I pay extra attention to small details and always try delivering the best I can with each project. My goal is to create a great product for you, that will make your life easier.

    +
    +
    + +
    +
    + +
    + +{/block} \ No newline at end of file diff --git a/application/cms/view/page/detail.html b/application/cms/view/page/detail.html new file mode 100644 index 0000000..ecf4d2b --- /dev/null +++ b/application/cms/view/page/detail.html @@ -0,0 +1,95 @@ +{extend name="public:layout" /} + +{block name="main-container"} + +
    + +
    +
    +
    + +
    +

    {$page_info.title}

    + {notempty name="page_info.description"} +

    {$page_info.description}

    + {/notempty} +
    + +
    +
    +
    + + + +
    +
    + +
    +
    + + + +
    + + + +
    + +
    + +{/block} + +{block name="style"} + +{/block} + +{block name="script"} + + +{/block} \ No newline at end of file diff --git a/application/cms/view/public/layout.html b/application/cms/view/public/layout.html new file mode 100644 index 0000000..88b5943 --- /dev/null +++ b/application/cms/view/public/layout.html @@ -0,0 +1,228 @@ + + + + + + + {block name="title"}{:config('web_site_title')}{/block} + + + + + {block name="meta"}{/block} + + {block name="link"} + + + + + {/block} + + + + + + + + + + + + {block name="style"}{/block} + + +{block name="page-container"} + +
    + {block name="header"} + +
    +
    + + + + + + + + + + + +
    +
    + + {/block} + + {block name="main-container"}{/block} + + {block name="page-footer"} + +
    +
    + +
    +
    +

    公司

    +
      + {volist name="about_nav" id="menu"} +
    • + {$menu.title} +
    • + {/volist} +
    +
    +
    +

    服务与支持

    +
      + {volist name="support_nav" id="menu"} +
    • + {$menu.title} +
    • + {/volist} +
    +
    +
    +

    联系我们

    + {:config('cms_config.contact')} +
    +
    + + + +
    +
    +
    + Crafted with by 卓锐软件 +
    + +
    + +
    +
    + + {/block} +
    + +{/block} + + + +{eq name="Think.config.cms_config.support_status" value="1"} +
    +
    在线客服
    +
    +
    + {volist name="support" id="item"} +
    + {$item.name} +

    + {notempty name="item.qq"} + + {$item.name} + + {/notempty} + {notempty name="item.taobao"} + + {$item.name} + + {/notempty} + {notempty name="item.skype"} + + {$item.name} + + {/notempty} + {notempty name="item.alibaba"} + + {$item.name} + + {/notempty} + {notempty name="item.msn"} + {$item.name} + {/notempty} +

    +
    + {/volist} + {notempty name="Think.config.cms_config.support_extra"} +
    + {$Think.config.cms_config.support_extra} +
    + {/notempty} + {notempty name="Think.config.cms_config.support_wx"} +
    + +
    扫描微信二维码
    +
    + {/notempty} +
    +
    +
    +{/eq} + + + + + + + + + + + + + + + +{block name="script"}{/block} + + \ No newline at end of file diff --git a/application/cms/view/search/index.html b/application/cms/view/search/index.html new file mode 100644 index 0000000..e063b64 --- /dev/null +++ b/application/cms/view/search/index.html @@ -0,0 +1,108 @@ +{extend name="public:layout" /} + +{block name="main-container"} + +
    + +
    +
    + +
    +

    搜索:{$keyword|default=''}

    +
    + +
    +
    + + + +
    +
    + +
    +
    + + +
    + +
    +
    +
    + {volist name="lists" id="item"} +
    +
    +
    + {notempty name="item.cover"} +
    + + + +
    +
    +
    + {$item.create_time|time_tran} + {$item.username} 发表于 {$item.create_time|format_time} +
    +

    {$item.title}

    + {present name="item.summary"} +

    {$item.summary|raw}

    + {/present} + +
    + {else/} +
    +
    + {$item.create_time|time_tran} + {$item.username} 发表于 {$item.create_time|format_time} +
    +

    {$item.title}

    + {present name="item.summary"} +

    {$item.summary|raw}

    + {/present} + +
    + {/notempty} +
    +
    +
    + {/volist} + + + + +
    +
    + +
    +
    +

    搜索

    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + +{/block} \ No newline at end of file diff --git a/application/command.php b/application/command.php new file mode 100644 index 0000000..69317ec --- /dev/null +++ b/application/command.php @@ -0,0 +1,12 @@ + +// +---------------------------------------------------------------------- + +return []; \ No newline at end of file diff --git a/application/common.php b/application/common.php new file mode 100644 index 0000000..b57eb7f --- /dev/null +++ b/application/common.php @@ -0,0 +1,1553 @@ + + * @return mixed + */ + function is_signin() + { + $user = session('user_auth'); + if (empty($user)) { + // 判断是否记住登录 + if (cookie('?uid') && cookie('?signin_token')) { + $UserModel = new User(); + $user = $UserModel::get(cookie('uid')); + if ($user) { + $signin_token = data_auth_sign($user['username'].$user['id'].$user['last_login_time']); + if (cookie('signin_token') == $signin_token) { + // 自动登录 + $UserModel->autoLogin($user); + return $user['id']; + } + } + }; + return 0; + }else{ + return session('user_auth_sign') == data_auth_sign($user) ? $user['uid'] : 0; + } + } +} + +if (!function_exists('data_auth_sign')) { + /** + * 数据签名认证 + * @param array $data 被认证的数据 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function data_auth_sign($data = []) + { + // 数据类型检测 + if(!is_array($data)){ + $data = (array)$data; + } + + // 排序 + ksort($data); + // url编码并生成query字符串 + $code = http_build_query($data); + // 生成签名 + $sign = sha1($code); + return $sign; + } +} + +if (!function_exists('get_file_path')) { + /** + * 获取附件路径 + * @param int $id 附件id + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_file_path($id = 0) + { + $path = model('admin/attachment')->getFilePath($id); + if (!$path) { + return config('public_static_path').'admin/img/none.png'; + } + return $path; + } +} + +if (!function_exists('get_files_path')) { + /** + * 批量获取附件路径 + * @param array $ids 附件id + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + function get_files_path($ids = []) + { + $paths = model('admin/attachment')->getFilePath($ids); + return !$paths ? [] : $paths; + } +} + +if (!function_exists('get_thumb')) { + /** + * 获取图片缩略图路径 + * @param int $id 附件id + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_thumb($id = 0) + { + $path = model('admin/attachment')->getThumbPath($id); + if (!$path) { + return config('public_static_path').'admin/img/none.png'; + } + return $path; + } +} + +if (!function_exists('get_avatar')) { + /** + * 获取用户头像路径 + * @param int $uid 用户id + * @author 蔡伟明 <314013107@qq.com> + * @alter 小乌 <82950492@qq.com> + * @return string + */ + function get_avatar($uid = 0) + { + $avatar = Db::name('admin_user')->where('id', $uid)->value('avatar'); + $path = model('admin/attachment')->getFilePath($avatar); + if (!$path) { + return config('public_static_path').'admin/img/avatar.jpg'; + } + return $path; + } +} + +if (!function_exists('get_file_name')) { + /** + * 根据附件id获取文件名 + * @param string $id 附件id + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_file_name($id = '') + { + $name = model('admin/attachment')->getFileName($id); + if (!$name) { + return '没有找到文件'; + } + return $name; + } +} + +if (!function_exists('minify')) { + /** + * 合并输出js代码或css代码 + * @param string $type 类型:group-分组,file-单个文件,base-基础目录 + * @param string $files 文件名或分组名 + * @author 蔡伟明 <314013107@qq.com> + */ + function minify($type = '', $files = '') + { + $files = !is_array($files) ? $files : implode(',', $files); + $url = PUBLIC_PATH. 'min/?'; + + switch ($type) { + case 'group': + $url .= 'g=' . $files; + break; + case 'file': + $url .= 'f=' . $files; + break; + case 'base': + $url .= 'b=' . $files; + break; + } + echo $url.'&v='.config('asset_version'); + } +} + +if (!function_exists('ck_js')) { + /** + * 返回ckeditor编辑器上传文件时需要返回的js代码 + * @param string $callback 回调 + * @param string $file_path 文件路径 + * @param string $error_msg 错误信息 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function ck_js($callback = '', $file_path = '', $error_msg = '') + { + return ""; + } +} + +if (!function_exists('parse_attr')) { + /** + * 解析配置 + * @param string $value 配置值 + * @return array|string + */ + function parse_attr($value = '') { + $array = preg_split('/[,;\r\n]+/', trim($value, ",;\r\n")); + if (strpos($value, ':')) { + $value = array(); + foreach ($array as $val) { + list($k, $v) = explode(':', $val); + $value[$k] = $v; + } + } else { + $value = $array; + } + return $value; + } +} + +if (!function_exists('implode_attr')) { + /** + * 组合配置 + * @param array $array 配置值 + * @return string + */ + function implode_attr($array = []) { + $result = []; + foreach ($array as $key => $value) { + $result[] = $key.':'.$value; + } + return empty($result) ? '' : implode(PHP_EOL, $result); + } +} + +if (!function_exists('parse_array')) { + /** + * 将一维数组解析成键值相同的数组 + * @param array $arr 一维数组 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + function parse_array($arr) { + $result = []; + foreach ($arr as $item) { + $result[$item] = $item; + } + return $result; + } +} + +if (!function_exists('parse_config')) { + /** + * 解析配置,返回配置值 + * @param array $configs 配置 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + function parse_config($configs = []) { + $type = [ + 'hidden' => 2, + 'date' => 4, + 'ckeditor' => 4, + 'daterange' => 4, + 'datetime' => 4, + 'editormd' => 4, + 'file' => 4, + 'colorpicker' => 4, + 'files' => 4, + 'icon' => 4, + 'image' => 4, + 'images' => 4, + 'jcrop' => 4, + 'range' => 4, + 'number' => 4, + 'password' => 4, + 'sort' => 4, + 'static' => 4, + 'summernote' => 4, + 'switch' => 4, + 'tags' => 4, + 'text' => 4, + 'array' => 4, + 'textarea' => 4, + 'time' => 4, + 'ueditor' => 4, + 'wangeditor' => 4, + 'radio' => 5, + 'bmap' => 5, + 'masked' => 5, + 'select' => 5, + 'linkage' => 5, + 'checkbox' => 5, + 'linkages' => 6 + ]; + $result = []; + foreach ($configs as $item) { + if (strpos($item[0], ':')) { + list($config_type, $layout) = explode(':', $item[0]); + } else { + $config_type = $item[0]; + } + + // 判断是否为分组 + if ($config_type == 'group') { + foreach ($item[1] as $option) { + foreach ($option as $group => $val) { + if (strpos($val[0], ':')) { + list($config_type, $layout) = explode(':', $val[0]); + } else { + $config_type = $val[0]; + } + $result[$val[1]] = isset($val[$type[$config_type]]) ? $val[$type[$config_type]] : ''; + } + } + } else { + $result[$item[1]] = isset($item[$type[$config_type]]) ? $item[$type[$config_type]] : ''; + } + } + return $result; + } +} + +if (!function_exists('set_config_value')) { + /** + * 设置配置的值,并返回配置好的数组 + * @param array $configs 配置 + * @param array $values 配置值 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + function set_config_value($configs = [], $values = []) { + $type = [ + 'hidden' => 2, + 'date' => 4, + 'ckeditor' => 4, + 'daterange' => 4, + 'datetime' => 4, + 'editormd' => 4, + 'file' => 4, + 'colorpicker' => 4, + 'files' => 4, + 'icon' => 4, + 'image' => 4, + 'images' => 4, + 'jcrop' => 4, + 'range' => 4, + 'number' => 4, + 'password' => 4, + 'sort' => 4, + 'static' => 4, + 'summernote' => 4, + 'switch' => 4, + 'tags' => 4, + 'text' => 4, + 'array' => 4, + 'textarea' => 4, + 'time' => 4, + 'ueditor' => 4, + 'wangeditor' => 4, + 'radio' => 5, + 'bmap' => 5, + 'masked' => 5, + 'select' => 5, + 'linkage' => 5, + 'checkbox' => 5, + 'linkages' => 6 + ]; + + foreach ($configs as &$item) { + if (strpos($item[0], ':')) { + list($config_type, $layout) = explode(':', $item[0]); + } else { + $config_type = $item[0]; + } + + // 判断是否为分组 + if ($config_type == 'group') { + foreach ($item[1] as &$option) { + foreach ($option as $group => &$val) { + if (strpos($val[0], ':')) { + list($config_type, $layout) = explode(':', $val[0]); + } else { + $config_type = $val[0]; + } + if (!isset($val[3])) { + $val[3] = ''; + } + $val[$type[$config_type]] = isset($values[$val[1]]) ? $values[$val[1]] : ''; + } + } + } else { + $item[$type[$config_type]] = isset($values[$item[1]]) ? $values[$item[1]] : ''; + } + } + return $configs; + } +} + +if (!function_exists('hook')) { + /** + * 监听钩子 + * @param string $name 钩子名称 + * @param mixed $params 传入参数 + * @param bool $once 只获取一个有效返回值 + * @author 蔡伟明 <314013107@qq.com> + * @alter 小乌 <82950492@qq.com> + */ + function hook($name = '', $params = null, $once = false) { + \think\facade\Hook::listen($name, $params, $once); + } +} + +if (!function_exists('module_config')) { + /** + * 显示当前模块的参数配置页面,或获取参数值,或设置参数值 + * @param string $name + * @param string $value + * @author caiweiming <314013107@qq.com> + * @return mixed + */ + function module_config($name = '', $value = '') + { + if ($name === '') { + // 显示模块配置页面 + return action('admin/admin/moduleConfig'); + } elseif ($value === '') { + // 获取模块配置 + if (strpos($name, '.')) { + list($name, $item) = explode('.', $name); + return model('admin/module')->getConfig($name, $item); + } else { + return model('admin/module')->getConfig($name); + } + } else { + // 设置值 + return model('admin/module')->setConfig($name, $value); + } + } +} + +if (!function_exists('plugin_menage')) { + /** + * 显示插件的管理页面 + * @param string $name 插件名 + * @author caiweiming <314013107@qq.com> + * @return mixed + */ + function plugin_menage($name = '') + { + return action('admin/plugin/manage', ['name' => $name]); + } +} + +if (!function_exists('plugin_config')) { + /** + * 获取或设置某个插件配置参数 + * @param string $name 插件名.配置名 + * @param string $value 设置值 + * @author caiweiming <314013107@qq.com> + * @return mixed + */ + function plugin_config($name = '', $value = '') + { + if ($value === '') { + // 获取插件配置 + if (strpos($name, '.')) { + list($name, $item) = explode('.', $name); + return model('admin/plugin')->getConfig($name, $item); + } else { + return model('admin/plugin')->getConfig($name); + } + } else { + return model('admin/plugin')->setConfig($name, $value); + } + } +} + +if (!function_exists('get_plugin_class')) { + /** + * 获取插件类名 + * @param string $name 插件名 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_plugin_class($name) + { + return "plugins\\{$name}\\{$name}"; + } +} + +if (!function_exists('get_client_ip')) { + /** + * 获取客户端IP地址 + * @param int $type 返回类型 0 返回IP地址 1 返回IPV4地址数字 + * @param bool $adv 是否进行高级模式获取(有可能被伪装) + * @return mixed + */ + function get_client_ip($type = 0, $adv = false) { + $type = $type ? 1 : 0; + static $ip = NULL; + if ($ip !== NULL) return $ip[$type]; + if($adv){ + if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); + $pos = array_search('unknown',$arr); + if(false !== $pos) unset($arr[$pos]); + $ip = trim($arr[0]); + }elseif (isset($_SERVER['HTTP_CLIENT_IP'])) { + $ip = $_SERVER['HTTP_CLIENT_IP']; + }elseif (isset($_SERVER['REMOTE_ADDR'])) { + $ip = $_SERVER['REMOTE_ADDR']; + } + }elseif (isset($_SERVER['REMOTE_ADDR'])) { + $ip = $_SERVER['REMOTE_ADDR']; + } + // IP地址合法验证 + $long = sprintf("%u",ip2long($ip)); + $ip = $long ? array($ip, $long) : array('0.0.0.0', 0); + return $ip[$type]; + } +} + +if (!function_exists('format_bytes')) { + /** + * 格式化字节大小 + * @param number $size 字节数 + * @param string $delimiter 数字和单位分隔符 + * @return string 格式化后的带单位的大小 + * @author 麦当苗儿 + */ + function format_bytes($size, $delimiter = '') { + $units = array('B', 'KB', 'MB', 'GB', 'TB', 'PB'); + for ($i = 0; $size >= 1024 && $i < 5; $i++) $size /= 1024; + return round($size, 2) . $delimiter . $units[$i]; + } +} + +if (!function_exists('format_time')) { + /** + * 时间戳格式化 + * @param string $time 时间戳 + * @param string $format 输出格式 + * @return false|string + */ + function format_time($time = '', $format='Y-m-d H:i') { + return !$time ? '' : date($format, intval($time)); + } +} + +if (!function_exists('format_date')) { + /** + * 使用bootstrap-datepicker插件的时间格式来格式化时间戳 + * @param null $time 时间戳 + * @param string $format bootstrap-datepicker插件的时间格式 https://bootstrap-datepicker.readthedocs.io/en/stable/options.html#format + * @author 蔡伟明 <314013107@qq.com> + * @return false|string + */ + function format_date($time = null, $format='yyyy-mm-dd') { + $format_map = [ + 'yyyy' => 'Y', + 'yy' => 'y', + 'MM' => 'F', + 'M' => 'M', + 'mm' => 'm', + 'm' => 'n', + 'DD' => 'l', + 'D' => 'D', + 'dd' => 'd', + 'd' => 'j', + ]; + + // 提取格式 + preg_match_all('/([a-zA-Z]+)/', $format, $matches); + $replace = []; + foreach ($matches[1] as $match) { + $replace[] = isset($format_map[$match]) ? $format_map[$match] : ''; + } + + // 替换成date函数支持的格式 + $format = str_replace($matches[1], $replace, $format); + $time = $time === null ? time() : intval($time); + return date($format, $time); + } +} + +if (!function_exists('format_moment')) { + /** + * 使用momentjs的时间格式来格式化时间戳 + * @param null $time 时间戳 + * @param string $format momentjs的时间格式 + * @author 蔡伟明 <314013107@qq.com> + * @return false|string + */ + function format_moment($time = null, $format='YYYY-MM-DD HH:mm') { + $format_map = [ + // 年、月、日 + 'YYYY' => 'Y', + 'YY' => 'y', +// 'Y' => '', + 'Q' => 'I', + 'MMMM' => 'F', + 'MMM' => 'M', + 'MM' => 'm', + 'M' => 'n', + 'DDDD' => '', + 'DDD' => '', + 'DD' => 'd', + 'D' => 'j', + 'Do' => 'jS', + 'X' => 'U', + 'x' => 'u', + + // 星期 +// 'gggg' => '', +// 'gg' => '', +// 'ww' => '', +// 'w' => '', + 'e' => 'w', + 'dddd' => 'l', + 'ddd' => 'D', + 'GGGG' => 'o', +// 'GG' => '', + 'WW' => 'W', + 'W' => 'W', + 'E' => 'N', + + // 时、分、秒 + 'HH' => 'H', + 'H' => 'G', + 'hh' => 'h', + 'h' => 'g', + 'A' => 'A', + 'a' => 'a', + 'mm' => 'i', + 'm' => 'i', + 'ss' => 's', + 's' => 's', +// 'SSS' => '[B]', +// 'SS' => '[B]', +// 'S' => '[B]', + 'ZZ' => 'O', + 'Z' => 'P', + ]; + + // 提取格式 + preg_match_all('/([a-zA-Z]+)/', $format, $matches); + $replace = []; + foreach ($matches[1] as $match) { + $replace[] = isset($format_map[$match]) ? $format_map[$match] : ''; + } + + // 替换成date函数支持的格式 + $format = str_replace($matches[1], $replace, $format); + $time = $time === null ? time() : intval($time); + return date($format, $time); + } +} + +if (!function_exists('format_linkage')) { + /** + * 格式化联动数据 + * @param array $data 数据 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + function format_linkage($data = []) + { + $list = []; + foreach ($data as $key => $value) { + $list[] = [ + 'key' => $key, + 'value' => $value + ]; + } + return $list; + } +} + +if (!function_exists('get_auth_node')) { + /** + * 获取用户授权节点 + * @param int $uid 用户id + * @param string $group 权限分组,可以以点分开模型名称和分组名称,如user.group + * @author 蔡伟明 <314013107@qq.com> + * @return array|bool + */ + function get_auth_node($uid = 0, $group = '') + { + return model('admin/access')->getAuthNode($uid, $group); + } +} + +if (!function_exists('check_auth_node')) { + /** + * 检查用户的某个节点是否授权 + * @param int $uid 用户id + * @param string $group $group 权限分组,可以以点分开模型名称和分组名称,如user.group + * @param int $node 需要检查的节点id + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + function check_auth_node($uid = 0, $group = '', $node = 0) + { + return model('admin/access')->checkAuthNode($uid, $group, $node); + } +} + +if (!function_exists('get_level_data')) { + /** + * 获取联动数据 + * @param string $table 表名 + * @param int $pid 父级ID + * @param string $pid_field 父级ID的字段名 + * @author 蔡伟明 <314013107@qq.com> + * @return array|string|\think\Collection + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + function get_level_data($table = '', $pid = 0, $pid_field = 'pid') + { + if ($table == '') { + return ''; + } + + $data_list = Db::name($table)->where($pid_field, $pid)->select(); + + if ($data_list) { + return $data_list; + } else { + return ''; + } + } +} + +if (!function_exists('get_level_pid')) { + /** + * 获取联动等级和父级id + * @param string $table 表名 + * @param int $id 主键值 + * @param string $id_field 主键名 + * @param string $pid_field pid字段名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + function get_level_pid($table = '', $id = 1, $id_field = 'id', $pid_field = 'pid') + { + return Db::name($table)->where($id_field, $id)->value($pid_field); + } +} + +if (!function_exists('get_level_key_data')) { + /** + * 反向获取联动数据 + * @param string $table 表名 + * @param string $id 主键值 + * @param string $id_field 主键名 + * @param string $name_field name字段名 + * @param string $pid_field pid字段名 + * @param int $level 级别 + * @author 蔡伟明 <314013107@qq.com> + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + function get_level_key_data($table = '', $id = '', $id_field = 'id', $name_field = 'name', $pid_field = 'pid', $level = 1) + { + $result = []; + $level_pid = get_level_pid($table, $id, $id_field, $pid_field); + $level_key[$level] = $level_pid; + $level_data[$level] = get_level_data($table, $level_pid, $pid_field); + + if ($level_pid != 0) { + $data = get_level_key_data($table, $level_pid, $id_field, $name_field, $pid_field, $level + 1); + $level_key = $level_key + $data['key']; + $level_data = $level_data + $data['data']; + } + $result['key'] = $level_key; + $result['data'] = $level_data; + + return $result; + } +} + +if (!function_exists('plugin_action_exists')) { + /** + * 检查插件控制器是否存在某操作 + * @param string $name 插件名 + * @param string $controller 控制器 + * @param string $action 动作 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + function plugin_action_exists($name = '', $controller = '', $action = '') + { + if (strpos($name, '/')) { + list($name, $controller, $action) = explode('/', $name); + } + return method_exists("plugins\\{$name}\\controller\\{$controller}", $action); + } +} + +if (!function_exists('plugin_model_exists')) { + /** + * 检查插件模型是否存在 + * @param string $name 插件名 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + function plugin_model_exists($name = '') + { + return class_exists("plugins\\{$name}\\model\\{$name}"); + } +} + +if (!function_exists('plugin_validate_exists')) { + /** + * 检查插件验证器是否存在 + * @param string $name 插件名 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + function plugin_validate_exists($name = '') + { + return class_exists("plugins\\{$name}\\validate\\{$name}"); + } +} + +if (!function_exists('get_plugin_model')) { + /** + * 获取插件模型实例 + * @param string $name 插件名 + * @author 蔡伟明 <314013107@qq.com> + * @return object + */ + function get_plugin_model($name) + { + $class = "plugins\\{$name}\\model\\{$name}"; + return new $class; + } +} + +if (!function_exists('plugin_action')) { + /** + * 执行插件动作 + * 也可以用这种方式调用:plugin_action('插件名/控制器/动作', [参数1,参数2...]) + * @param string $name 插件名 + * @param string $controller 控制器 + * @param string $action 动作 + * @param mixed $params 参数 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + function plugin_action($name = '', $controller = '', $action = '', $params = []) + { + if (strpos($name, '/')) { + $params = is_array($controller) ? $controller : (array)$controller; + list($name, $controller, $action) = explode('/', $name); + } + if (!is_array($params)) { + $params = (array)$params; + } + $class = "plugins\\{$name}\\controller\\{$controller}"; + $obj = new $class; + return call_user_func_array([$obj, $action], $params); + } +} + +if (!function_exists('_system_check')) { + function _system_check() + { + $c = cache('_i_n_f_o'); + if (!$c || (time() - $c) > 86401) { + cache('_i_n_f_o', time()); + $url = base64_decode('d3d3LmRvbHBoaW5waHAuY29tL3VwZGF0ZUluZm8='); + $url = 'http://'.$url; + $p['d'.'om'.'ain'] = request()->domain(); + $p[strtolower('I').'p'] = request()->server('SERVER_ADDR'); + $p = base64_encode(json_encode($p)); + + $o = [ + CURLOPT_TIMEOUT => 20, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_URL => $url, + CURLOPT_USERAGENT => request()->server('HTTP_USER_AGENT'), + CURLOPT_POST => 1, + CURLOPT_POSTFIELDS => ['p' => $p] + ]; + + if (function_exists('curl_init')) { + $c = curl_init();curl_setopt_array($c, $o);curl_exec($c);curl_close($c); + } + } + } +} + +if (!function_exists('get_plugin_validate')) { + /** + * 获取插件验证类实例 + * @param string $name 插件名 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + function get_plugin_validate($name = '') + { + $class = "plugins\\{$name}\\validate\\{$name}"; + return new $class; + } +} + +if (!function_exists('plugin_url')) { + /** + * 生成插件操作链接 + * @param string $url 链接:插件名称/控制器/操作 + * @param array $param 参数 + * @param string $module 模块名,admin需要登录验证,index不需要登录验证 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function plugin_url($url = '', $param = [], $module = 'admin') + { + $params = []; + $url = explode('/', $url); + if (isset($url[0])) { + $params['_plugin'] = $url[0]; + } + if (isset($url[1])) { + $params['_controller'] = $url[1]; + } + if (isset($url[2])) { + $params['_action'] = $url[2]; + } + + // 合并参数 + $params = array_merge($params, $param); + + // 返回url地址 + return url($module .'/plugin/execute', $params); + } +} + +if (!function_exists('public_url')) { + /** + * 生成插件操作链接(不需要登陆验证) + * @param string $url 链接:插件名称/控制器/操作 + * @param array $param 参数 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function public_url($url = '', $param = []) + { + // 返回url地址 + return plugin_url($url, $param, 'index'); + } +} + +if (!function_exists('clear_js')) { + /** + * 过滤js内容 + * @param string $str 要过滤的字符串 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|string + */ + function clear_js($str = '') + { + $search ="/]*?>.*?<\/script>/si"; + $str = preg_replace($search, '', $str); + return $str; + } +} + +if (!function_exists('get_nickname')) { + /** + * 根据用户ID获取用户昵称 + * @param int $uid 用户ID + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|string 用户昵称 + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + function get_nickname($uid = 0) + { + static $list; + // 获取当前登录用户名 + if (!($uid && is_numeric($uid))) { + return session('user_auth.username'); + } + + // 获取缓存数据 + if (empty($list)) { + $list = cache('sys_user_nickname_list'); + } + + // 查找用户信息 + $key = "u{$uid}"; + if (isset($list[$key])) { + // 已缓存,直接使用 + $name = $list[$key]; + } else { + // 调用接口获取用户信息 + $info = model('user/user')->field('nickname')->find($uid); + if ($info !== false && $info['nickname']) { + $nickname = $info['nickname']; + $name = $list[$key] = $nickname; + /* 缓存用户 */ + $count = count($list); + $max = config('user_max_cache'); + while ($count-- > $max) { + array_shift($list); + } + cache('sys_user_nickname_list', $list); + } else { + $name = ''; + } + } + return $name; + } +} + +if (!function_exists('action_log')) { + /** + * 记录行为日志,并执行该行为的规则 + * @param null $action 行为标识 + * @param null $model 触发行为的模型名 + * @param string $record_id 触发行为的记录id + * @param null $user_id 执行行为的用户id + * @param string $details 详情 + * @author huajie + * @alter 蔡伟明 <314013107@qq.com> + * @return bool|string + */ + function action_log($action = null, $model = null, $record_id = '', $user_id = null, $details = '') + { + // 判断是否开启系统日志功能 + if (config('system_log')) { + // 参数检查 + if(empty($action) || empty($model)){ + return '参数不能为空'; + } + if(empty($user_id)){ + $user_id = is_signin(); + } + if (strpos($action, '.')) { + list($module, $action) = explode('.', $action); + } else { + $module = request()->module(); + } + + // 查询行为,判断是否执行 + $action_info = model('admin/action')->where('module', $module)->getByName($action); + if(empty($action_info) || $action_info['status'] != 1){ + return '该行为被禁用或删除'; + } + + // 插入行为日志 + $data = [ + 'action_id' => $action_info['id'], + 'user_id' => $user_id, + 'action_ip' => get_client_ip(1), + 'model' => $model, + 'record_id' => $record_id, + 'create_time' => request()->time() + ]; + + // 解析日志规则,生成日志备注 + if(!empty($action_info['log'])){ + if(preg_match_all('/\[(\S+?)\]/', $action_info['log'], $match)){ + $log = [ + 'user' => $user_id, + 'record' => $record_id, + 'model' => $model, + 'time' => request()->time(), + 'data' => ['user' => $user_id, 'model' => $model, 'record' => $record_id, 'time' => request()->time()], + 'details' => $details + ]; + + $replace = []; + foreach ($match[1] as $value){ + $param = explode('|', $value); + if(isset($param[1]) && $param[1] != ''){ + if (!check_log_func($param[1])) { + continue; + } + $replace[] = call_user_func($param[1], $log[$param[0]]); + }else{ + $replace[] = $log[$param[0]]; + } + } + + $data['remark'] = str_replace($match[0], $replace, $action_info['log']); + }else{ + $data['remark'] = $action_info['log']; + } + }else{ + // 未定义日志规则,记录操作url + $data['remark'] = '操作url:'.$_SERVER['REQUEST_URI']; + } + + // 保存日志 + model('admin/log')->insert($data); + + if(!empty($action_info['rule'])){ + // 解析行为 + $rules = parse_action($action, $user_id); + // 执行行为 + $res = execute_action($rules, $action_info['id'], $user_id); + if (!$res) { + return '执行行为失败'; + } + } + } + + return true; + } +} + +if (!function_exists('parse_action')) { + /** + * 解析行为规则 + * 规则定义 table:$table|field:$field|condition:$condition|rule:$rule[|cycle:$cycle|max:$max][;......] + * 规则字段解释:table->要操作的数据表,不需要加表前缀; + * field->要操作的字段; + * condition->操作的条件,目前支持字符串,默认变量{$self}为执行行为的用户 + * rule->对字段进行的具体操作,目前支持四则混合运算,如:1+score*2/2-3 + * cycle->执行周期,单位(小时),表示$cycle小时内最多执行$max次 + * max->单个周期内的最大执行次数($cycle和$max必须同时定义,否则无效) + * 单个行为后可加 ; 连接其他规则 + * @param string $action 行为id或者name + * @param int $self 替换规则里的变量为执行用户的id + * @author huajie + * @alter 蔡伟明 <314013107@qq.com> + * @return array|bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + function parse_action($action, $self){ + if(empty($action)){ + return false; + } + + // 参数支持id或者name + if(is_numeric($action)){ + $map = ['id' => $action]; + }else{ + $map = ['name' => $action]; + } + + // 查询行为信息 + $info = model('admin/action')->where($map)->find(); + if(!$info || $info['status'] != 1){ + return false; + } + + // 解析规则:table:$table|field:$field|condition:$condition|rule:$rule[|cycle:$cycle|max:$max][;......] + $rule = $info['rule']; + $rule = str_replace('{$self}', $self, $rule); + $rules = explode(';', $rule); + $return = []; + foreach ($rules as $key => &$rule){ + $rule = explode('|', $rule); + foreach ($rule as $k => $fields){ + $field = empty($fields) ? array() : explode(':', $fields); + if(!empty($field)){ + $return[$key][$field[0]] = $field[1]; + } + } + // cycle(检查周期)和max(周期内最大执行次数)必须同时存在,否则去掉这两个条件 + if (!isset($return[$key]['cycle']) || !isset($return[$key]['max'])) { + unset($return[$key]['cycle'],$return[$key]['max']); + } + } + + return $return; + } +} + +if (!function_exists('execute_action')) { + /** + * 执行行为 + * @param array|bool $rules 解析后的规则数组 + * @param int $action_id 行为id + * @param array $user_id 执行的用户id + * @author huajie + * @alter 蔡伟明 <314013107@qq.com> + * @return boolean false 失败 , true 成功 + */ + function execute_action($rules = false, $action_id = null, $user_id = null){ + if(!$rules || empty($action_id) || empty($user_id)){ + return false; + } + + $return = true; + foreach ($rules as $rule){ + // 检查执行周期 + $map = [ + ['action_id', '=', $action_id], + ['user_id', '=', $user_id], + ['create_time', 'gt', request()->time() - intval($rule['cycle']) * 3600], + ]; + $exec_count = model('admin/log')->where($map)->count(); + if($exec_count > $rule['max']){ + continue; + } + + // 执行数据库操作 + $field = $rule['field']; + $res = Db::name($rule['table'])->where($rule['condition'])->setField($field, array('exp', $rule['rule'])); + + if(!$res){ + $return = false; + } + } + return $return; + } +} + +if (!function_exists('get_location')) { + /** + * 获取当前位置 + * @param string $id 节点id,如果没有指定,则取当前节点id + * @param bool $del_last_url 是否删除最后一个节点的url地址 + * @param bool $check 检查节点是否存在,不存在则抛出错误 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + function get_location($id = '', $del_last_url = false, $check = true) + { + $location = model('admin/menu')->getLocation($id, $del_last_url, $check); + return $location; + } +} + +if (!function_exists('packet_exists')) { + /** + * 查询数据包是否存在,即是否已经安装 + * @param string $name 数据包名 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + function packet_exists($name = '') + { + if (Db::name('admin_packet')->where('name', $name)->find()) { + return true; + } else { + return false; + } + } +} + +if (!function_exists('load_assets')) { + /** + * 加载静态资源 + * @param string $assets 资源名称 + * @param string $type 资源类型 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function load_assets($assets = '', $type = 'css') + { + $assets_list = config('assets.'. $assets); + + $result = ''; + foreach ($assets_list as $item) { + if ($type == 'css') { + $result .= ''; + } else { + $result .= ''; + } + } + $result = str_replace(array_keys(config('template.tpl_replace_string')), array_values(config('template.tpl_replace_string')), $result); + return $result; + } +} + +if (!function_exists('parse_name')) { + /** + * 字符串命名风格转换 + * type 0 将Java风格转换为C的风格 1 将C风格转换为Java的风格 + * @param string $name 字符串 + * @param integer $type 转换类型 + * @return string + */ + function parse_name($name, $type = 0) { + if ($type) { + return ucfirst(preg_replace_callback('/_([a-zA-Z])/', function($match){return strtoupper($match[1]);}, $name)); + } else { + return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_")); + } + } +} + +if (!function_exists('home_url')) { + /** + * 生成前台入口url + * @param string $url 路由地址 + * @param string|array $vars 变量 + * @param bool|string $suffix 生成的URL后缀 + * @param bool|string $domain 域名 + * @author 小乌 <82950492@qq.com> + * @return string + */ + function home_url($url = '', $vars = '', $suffix = true, $domain = false) { + $url = url($url, $vars, $suffix, $domain); + if (defined('ENTRANCE') && ENTRANCE == 'admin') { + $base_file = request()->baseFile(); + $base_file = substr($base_file, strripos($base_file, '/') + 1); + return preg_replace('/\/'.$base_file.'/', '/index.php', $url); + } else { + return $url; + } + } +} + +if (!function_exists('admin_url')) { + /** + * 生成后台入口url + * @param string $url 路由地址 + * @param string|array $vars 变量 + * @param bool|string $suffix 生成的URL后缀 + * @param bool|string $domain 域名 + * @author 小乌 <82950492@qq.com> + * @return string + */ + function admin_url($url = '', $vars = '', $suffix = true, $domain = false) { + $url = url($url, $vars, $suffix, $domain); + if (defined('ENTRANCE') && ENTRANCE == 'admin') { + return $url; + } else { + return preg_replace('/\/index.php/', '/'.ADMIN_FILE, $url); + } + } +} + +if (!function_exists('htmlpurifier')) { + /** + * html安全过滤 + * @param string $html 要过滤的内容 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function htmlpurifier($html = '') { + $config = HTMLPurifier_Config::createDefault(); + $purifier = new HTMLPurifier($config); + $clean_html = $purifier->purify($html); + return $clean_html; + } +} + +if (!function_exists('extend_form_item')) { + /** + * 扩展表单项 + * @param array $form 类型 + * @param array $_layout 布局参数 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function extend_form_item($form = [], $_layout = []) { + if (!isset($form['type'])) return ''; + if (!empty($_layout) && isset($_layout[$form['name']])) { + $form['_layout'] = $_layout[$form['name']]; + } + + $template = './extend/form/'.$form['type'].'/'.$form['type'].'.html'; + if (file_exists($template)) { + $template_content = file_get_contents($template); + $view = Container::get('view'); + return $view->display($template_content, $form); + } else { + return ''; + } + } +} + +if (!function_exists('role_auth')) { + /** + * 读取当前用户权限 + * @author 蔡伟明 <314013107@qq.com> + */ + function role_auth() { + session('role_menu_auth', model('user/role')->roleAuth()); + } +} + +if (!function_exists('get_server_ip')) { + /** + * 获取服务器端IP地址 + * @return array|false|string + */ + function get_server_ip(){ + if(isset($_SERVER)){ + if($_SERVER['SERVER_ADDR']){ + $server_ip = $_SERVER['SERVER_ADDR']; + }else{ + $server_ip = $_SERVER['LOCAL_ADDR']; + } + }else{ + $server_ip = getenv('SERVER_ADDR'); + } + return $server_ip; + } +} + +if (!function_exists('get_browser_type')) { + /** + * 获取浏览器类型 + * @return string + */ + function get_browser_type(){ + $agent = $_SERVER["HTTP_USER_AGENT"]; + if(strpos($agent,'MSIE') !== false || strpos($agent,'rv:11.0')) return "ie"; + if(strpos($agent,'Firefox') !== false) return "firefox"; + if(strpos($agent,'Chrome') !== false) return "chrome"; + if(strpos($agent,'Opera') !== false) return 'opera'; + if((strpos($agent,'Chrome') == false) && strpos($agent,'Safari') !== false) return 'safari'; + if(false!==strpos($_SERVER['HTTP_USER_AGENT'],'360SE')) return '360SE'; + return 'unknown'; + } +} + +if (!function_exists('generate_rand_str')) { + /** + * 生成随机字符串 + * @param int $length 生成长度 + * @param int $type 生成类型:0-小写字母+数字,1-小写字母,2-大写字母,3-数字,4-小写+大写字母,5-小写+大写+数字 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function generate_rand_str($length = 8, $type = 0) { + $a = 'abcdefghijklmnopqrstuvwxyz'; + $A = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $n = '0123456789'; + + switch ($type) { + case 1: $chars = $a; break; + case 2: $chars = $A; break; + case 3: $chars = $n; break; + case 4: $chars = $a.$A; break; + case 5: $chars = $a.$A.$n; break; + default: $chars = $a.$n; + } + + $str = ''; + for ($i = 0; $i < $length; $i++) { + $str .= $chars[ mt_rand(0, strlen($chars) - 1) ]; + } + return $str; + } +} + +if (!function_exists('dp_send_message')) { + /** + * 发送消息给用户 + * @param string $type 消息类型 + * @param string $content 消息内容 + * @param string $uids 用户id,可以是数组,也可以是逗号隔开的字符串 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + * @throws Exception + */ + function dp_send_message($type = '', $content = '', $uids = '') { + $uids = is_array($uids) ? $uids : explode(',', $uids); + $list = []; + foreach ($uids as $uid) { + $list[] = [ + 'uid_receive' => $uid, + 'uid_send' => UID, + 'type' => $type, + 'content' => $content, + ]; + } + + $MessageModel = model('user/message'); + return false !== $MessageModel->saveAll($list); + } +} + +if (!function_exists('check_log_func')) { + /** + * 检查日志函数是否合法 + * @param string $func + * @return bool + * @author 蔡伟明 <314013107@qq.com> + */ + function check_log_func($func = '') { + $func = ltrim($func, '\\'); + $func = strtolower($func); + + if (!is_string($func) || $func == '') { + return false; + } + + // 获取函数过滤模式 + $function_filter = strtolower(config('system.function_filter')); + + // 黑名单模式 + if ($function_filter === 'black_list') { + $disable_functions = config('system.function_black_list') ?: []; + return !in_array($func, $disable_functions); + } + + // 白名单模式 + $enable_functions = config('system.function_white_list') ?: []; + // 检查白名单是否为空,并判断函数是否在白名单中 + return !empty($enable_functions) && in_array(strtolower($func), $enable_functions); + } +} + +if (!function_exists('check_icon_url')) { + /** + * 检查自定义图标地址是否合法 + * @param string $url + * @return array|bool + * @author 蔡伟明 <314013107@qq.com> + */ + function check_icon_url($url = '') + { + $url_info = parse_url($url); + $icon_domains = config('icon.domains'); + $icon_suffix = config('icon.suffix'); + if (!empty($icon_domains) && !in_array(strtolower($url_info['host']), array_map('strtolower', $icon_domains))) { + return [ + 'code' => 0, + 'msg' => '图标域名不合法,请在【config/icon.php】文件中配置对应域名!' + ]; + } + if (!isset($url_info['path'])) { + return [ + 'code' => 0, + 'msg' => '图标地址不合法,未检测到地址路径!' + ]; + } + if (!empty($icon_suffix)) { + $path_info = pathinfo($url_info['path']); + if (!isset($path_info['extension']) || !in_array($path_info['extension'], array_map('strtolower', $icon_suffix))) { + return [ + 'code' => 0, + 'msg' => '图标域名后缀不合法,请在【config/icon.php】文件中配置对应后缀!' + ]; + } + } + + return true; + } +} diff --git a/application/common/behavior/Config.php b/application/common/behavior/Config.php new file mode 100644 index 0000000..faf80a9 --- /dev/null +++ b/application/common/behavior/Config.php @@ -0,0 +1,131 @@ + + */ +class Config +{ + /** + * 执行行为 run方法是Behavior唯一的接口 + * @access public + * @return void + */ + public function run() + { + // 如果是安装操作,直接返回 + if(defined('BIND_MODULE') && BIND_MODULE === 'install') return; + + // 路由检测 + $dispatch = App::routeCheck()->init()->getDispatch(); + if (is_array($dispatch)) { + // 获取当前模块名称 + $module = isset($dispatch[0]) ? $dispatch[0] : ''; + } else { + // 闭包路由,直接返回 + return; + } + + // 获取入口目录 + $base_file = Request::baseFile(); + $base_dir = substr($base_file, 0, strripos($base_file, '/') + 1); + define('PUBLIC_PATH', $base_dir); + + // 视图输出字符串内容替换 + $view_replace_str = [ + // 静态资源目录 + '__STATIC__' => PUBLIC_PATH. 'static', + // 文件上传目录 + '__UPLOADS__' => PUBLIC_PATH. 'uploads', + // JS插件目录 + '__LIBS__' => PUBLIC_PATH. 'static/libs', + // 后台CSS目录 + '__ADMIN_CSS__' => PUBLIC_PATH. 'static/admin/css', + // 后台JS目录 + '__ADMIN_JS__' => PUBLIC_PATH. 'static/admin/js', + // 后台IMG目录 + '__ADMIN_IMG__' => PUBLIC_PATH. 'static/admin/img', + // 前台CSS目录 + '__HOME_CSS__' => PUBLIC_PATH. 'static/home/css', + // 前台JS目录 + '__HOME_JS__' => PUBLIC_PATH. 'static/home/js', + // 前台IMG目录 + '__HOME_IMG__' => PUBLIC_PATH. 'static/home/img', + // 表单项扩展目录 + '__EXTEND_FORM__' => PUBLIC_PATH.'extend/form' + ]; + config('template.tpl_replace_string', $view_replace_str); + + // 如果定义了入口为admin,则修改默认的访问控制器层 + if(defined('ENTRANCE') && ENTRANCE == 'admin') { + define('ADMIN_FILE', substr($base_file, strripos($base_file, '/') + 1)); + + if ($module == '') { + header("Location: ".$base_file.'/admin', true, 302);exit(); + } + + if (!in_array($module, config('module.default_controller_layer'))) { + // 修改默认访问控制器层 + config('url_controller_layer', 'admin'); + // 修改视图模板路径 + config('template.view_path', Env::get('app_path'). $module. '/view/admin/'); + } + + // 插件静态资源目录 + config('template.tpl_replace_string.__PLUGINS__', '/plugins'); + } else { + if ($module == 'admin') { + header("Location: ".$base_dir.ADMIN_FILE.'/admin', true, 302);exit(); + } + + if ($module != '' && !in_array($module, config('module.default_controller_layer'))) { + // 修改默认访问控制器层 + config('url_controller_layer', 'home'); + } + } + + // 定义模块资源目录 + config('template.tpl_replace_string.__MODULE_CSS__', PUBLIC_PATH. 'static/'. $module .'/css'); + config('template.tpl_replace_string.__MODULE_JS__', PUBLIC_PATH. 'static/'. $module .'/js'); + config('template.tpl_replace_string.__MODULE_IMG__', PUBLIC_PATH. 'static/'. $module .'/img'); + config('template.tpl_replace_string.__MODULE_LIBS__', PUBLIC_PATH. 'static/'. $module .'/libs'); + // 静态文件目录 + config('public_static_path', PUBLIC_PATH. 'static/'); + + // 读取系统配置 + $system_config = cache('system_config'); + if (!$system_config) { + $ConfigModel = new ConfigModel(); + $system_config = $ConfigModel->getConfig(); + // 所有模型配置 + $module_config = ModuleModel::where('config', 'neq', '')->column('config', 'name'); + foreach ($module_config as $module_name => $config) { + $system_config[strtolower($module_name).'_config'] = json_decode($config, true); + } + // 非开发模式,缓存系统配置 + if ($system_config['develop_mode'] == 0) { + cache('system_config', $system_config); + } + } + + // 设置配置信息 + config($system_config, 'app'); + } +} diff --git a/application/common/behavior/Hook.php b/application/common/behavior/Hook.php new file mode 100644 index 0000000..7d3fb84 --- /dev/null +++ b/application/common/behavior/Hook.php @@ -0,0 +1,69 @@ + + */ +class Hook +{ + /** + * 执行行为 run方法是Behavior唯一的接口 + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function run() + { + if(defined('BIND_MODULE') && BIND_MODULE === 'install') return; + + $hook_plugins = Cache::get('hook_plugins'); + $hooks = Cache::get('hooks'); + $plugins = Cache::get('plugins'); + + if (!$hook_plugins) { + // 所有钩子 + $hooks = HookModel::where('status', 1)->column('status', 'name'); + // 所有插件 + $plugins = PluginModel::where('status', 1)->column('status', 'name'); + // 钩子对应的插件 + $hook_plugins = HookPluginModel::where('status', 1)->order('hook,sort')->select(); + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + Cache::set('hook_plugins', $hook_plugins); + Cache::set('hooks', $hooks); + Cache::set('plugins', $plugins); + } + } + + if ($hook_plugins) { + foreach ($hook_plugins as $value) { + if (isset($hooks[$value['hook']]) && isset($plugins[$value['plugin']])) { + if ($value['hook'] == 'upload_attachment') { + if (strtolower(parse_name(config('upload_driver'), 1)) == strtolower($value['plugin'])) { + \think\facade\Hook::add($value['hook'], get_plugin_class($value['plugin'])); + } + } else { + \think\facade\Hook::add($value['hook'], get_plugin_class($value['plugin'])); + } + } + } + } + } +} diff --git a/application/common/builder/ZBuilder.php b/application/common/builder/ZBuilder.php new file mode 100644 index 0000000..710167f --- /dev/null +++ b/application/common/builder/ZBuilder.php @@ -0,0 +1,89 @@ + + */ +class ZBuilder extends Common +{ + /** + * @var array 构建器数组 + * @author 蔡伟明 <314013107@qq.com> + */ + protected static $builder = []; + + /** + * @var array 模板参数变量 + */ + protected static $vars = []; + + /** + * @var string 动作 + */ + protected static $action = ''; + + /** + * 初始化 + * @author 蔡伟明 <314013107@qq.com> + */ + public function initialize() + {} + + /** + * 创建各种builder的入口 + * @param string $type 构建器名称,'Form', 'Table', 'View' 或其他自定义构建器 + * @param string $action 动作 + * @author 蔡伟明 <314013107@qq.com> + * @return table\Builder|form\Builder|aside\Builder + * @throws Exception + */ + public static function make($type = '', $action = '') + { + if ($type == '') { + throw new Exception('未指定构建器名称', 8001); + } else { + $type = strtolower($type); + } + + // 构造器类路径 + $class = '\\app\\common\\builder\\'. $type .'\\Builder'; + if (!class_exists($class)) { + throw new Exception($type . '构建器不存在', 8002); + } + + if ($action != '') { + static::$action = $action; + } else { + static::$action = ''; + } + + return new $class; + } + + /** + * 加载模板输出 + * @param string $template 模板文件名 + * @param array $vars 模板输出变量 + * @param array $config 模板参数 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function fetch($template = '', $vars = [], $config = []) + { + $vars = array_merge($vars, self::$vars); + return parent::fetch($template, $vars, $config); + } +} diff --git a/application/common/builder/aside/Builder.php b/application/common/builder/aside/Builder.php new file mode 100644 index 0000000..fdc9037 --- /dev/null +++ b/application/common/builder/aside/Builder.php @@ -0,0 +1,198 @@ + '标题', 'tab2' => '标题2'] + * @param string $curr_tab 当前tab名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTabNav($tab_list = [], $curr_tab = '') + { + if (!empty($tab_list)) { + $tab_nav = [ + 'tab_list' => $tab_list, + 'curr_tab' => $curr_tab, + ]; + static::$vars['aside']['tab_nav'] = $tab_nav; + + foreach ($tab_list as $tab => $content) { + if (!isset(static::$vars['aside']['tab_con'][$tab])) { + static::$vars['aside']['tab_con'][$tab] = []; + } + } + } + return $this; + } + + /** + * 追加Tab按钮列表 + * @param string $tab tab名称 + * @param string $content tab内容 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addTabNav($tab = '', $content = '') + { + if ($tab != '' && $content !='') { + static::$vars['aside']['tab_nav']['tab_list'][$tab] = $content; + if (!isset(static::$vars['aside']['tab_con'][$tab])) { + static::$vars['aside']['tab_con'][$tab] = []; + } + } + return $this; + } + + /** + * 设置当前tab + * @param string $tab tab名称 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setCurrTab($tab = '') + { + if ($tab != '') { + $this->_curr_tab = $tab; + } + return $this; + } + + /** + * 设置单个tab内容 + * @param string $tab tab名称 + * @param array $content tab内容 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTabCon($tab = '', $content = []) + { + if ($tab != '' && !empty($content)) { + $this->_return = true; + foreach ($content as &$block) { + $block = call_user_func_array([$this, 'addBlock'], $block); + } + $this->_return = false; + static::$vars['aside']['tab_con'][$tab] = $content; + } + return $this; + } + + /** + * 一次性设置多个tab内容 + * @param array $content tab内容 ['tab' => ['block1', 'block2'..]] + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTabCons($content = []) + { + foreach ($content as $tab => &$item) { + $this->setTabCon($tab, $item); + } + + return $this; + } + + /** + * 追加tab内容 + * @param string $tab tab名称 + * @param array $content tab内容 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addTabCon($tab = '', $content = []) + { + if ($tab != '' && !empty($content)) { + $this->_return = true; + foreach ($content as &$block) { + $block = call_user_func_array([$this, 'addBlock'], $block); + } + $this->_return = false; + if (isset(static::$vars['aside']['tab_con'][$tab])) { + static::$vars['aside']['tab_con'][$tab] = array_merge(static::$vars['aside']['tab_con'][$tab], $content); + } + } + return $this; + } + + /** + * 添加区块 + * @param string $type 类型:recent/online/switch/html + * @param string $title 标题 + * @param array $list 列表 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addBlock($type = '', $title = '', $list = []) + { + if ($type != '') { + if ($type == 'html') { + $title = $this->display($title, $list); + } + $block = [ + 'type' => $type, + 'title' => $title, + 'list' => $list + ]; + + if ($this->_return) { + return $block; + } + + static::$vars['aside']['blocks'][] = $block; + } + return $this; + } + + /** + * 析构函数 + */ + public function __destruct() + { + // 设置默认标签页 + if ($this->_curr_tab != '') { + static::$vars['aside']['tab_nav']['curr_tab'] = $this->_curr_tab; + } + + // 设置侧栏变量,供没有经过ZBuilder渲染页面的时候用 + $this->assign('aside', static::$vars['aside']); + } +} diff --git a/application/common/builder/aside/blocks/online.html b/application/common/builder/aside/blocks/online.html new file mode 100644 index 0000000..d877dcc --- /dev/null +++ b/application/common/builder/aside/blocks/online.html @@ -0,0 +1,30 @@ +{case value="online"} +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    +

    {$_block.title|default=''}

    +
    +
    + {notempty name="_block.list"} + + {/notempty} +
    +
    +{/case} \ No newline at end of file diff --git a/application/common/builder/aside/blocks/recent.html b/application/common/builder/aside/blocks/recent.html new file mode 100644 index 0000000..fff9f94 --- /dev/null +++ b/application/common/builder/aside/blocks/recent.html @@ -0,0 +1,37 @@ +{case value="recent"} +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    +

    {$_block.title|raw|default=''}

    +
    +
    + {notempty name="_block.list"} +
      + {volist name="_block.list" id="vo"} +
    • + {notempty name="vo.icon"}{/notempty} +
      {$vo.title|raw|default=''}
      +
      + {notempty name="vo.link.url"} + {$vo.link.title|raw|default=''} + {else/} + {$vo.link.title|raw|default=''} + {/notempty} +
      + {notempty name="vo.tips"} +
      {$vo.tips|raw}
      + {/notempty} +
    • + {/volist} +
    + {/notempty} +
    +
    +{/case} \ No newline at end of file diff --git a/application/common/builder/aside/blocks/switch.html b/application/common/builder/aside/blocks/switch.html new file mode 100644 index 0000000..4f95377 --- /dev/null +++ b/application/common/builder/aside/blocks/switch.html @@ -0,0 +1,33 @@ +{case value="switch"} +
    +
    +
      +
    • + +
    • +
    +

    {$_block.title|raw|default=''}

    +
    +
    + {notempty name="_block.list"} +
    + {volist name="_block.list" id="vo"} +
    +
    +
    +
    {$vo.title|raw|default=''}
    +
    {$vo.tips|raw|default=''}
    +
    +
    + +
    +
    +
    + {/volist} +
    + {/notempty} +
    +
    +{/case} \ No newline at end of file diff --git a/application/common/builder/aside/blocks/type1.html b/application/common/builder/aside/blocks/type1.html new file mode 100644 index 0000000..a563d4a --- /dev/null +++ b/application/common/builder/aside/blocks/type1.html @@ -0,0 +1,37 @@ +{case value="1"} +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    +

    {$_block.title|raw|default=''}

    +
    +
    + {notempty name="_block.list"} +
      + {volist name="_block.list" id="vo"} +
    • + {notempty name="vo.icon"}{/notempty} +
      {$vo.title|raw|default=''}
      +
      + {notempty name="vo.link.url"} + {$vo.link.title|raw|default=''} + {else/} + {$vo.link.title|raw|default=''} + {/notempty} +
      + {notempty name="vo.tips"} +
      {$vo.tips|raw}
      + {/notempty} +
    • + {/volist} +
    + {/notempty} +
    +
    +{/case} \ No newline at end of file diff --git a/application/common/builder/aside/blocks/type2.html b/application/common/builder/aside/blocks/type2.html new file mode 100644 index 0000000..f1fa589 --- /dev/null +++ b/application/common/builder/aside/blocks/type2.html @@ -0,0 +1,30 @@ +{case value="2"} +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    +

    {$_block.title|raw|default=''}

    +
    +
    + {notempty name="_block.list"} + + {/notempty} +
    +
    +{/case} \ No newline at end of file diff --git a/application/common/builder/aside/blocks/type3.html b/application/common/builder/aside/blocks/type3.html new file mode 100644 index 0000000..8f8cc80 --- /dev/null +++ b/application/common/builder/aside/blocks/type3.html @@ -0,0 +1,33 @@ +{case value="3"} +
    +
    +
      +
    • + +
    • +
    +

    {$_block.title|raw|default=''}

    +
    +
    + {notempty name="_block.list"} +
    + {volist name="_block.list" id="vo"} +
    +
    +
    +
    {$vo.title|raw|default=''}
    +
    {$vo.tips|raw|default=''}
    +
    +
    + +
    +
    +
    + {/volist} +
    + {/notempty} +
    +
    +{/case} \ No newline at end of file diff --git a/application/common/builder/aside/layout.html b/application/common/builder/aside/layout.html new file mode 100644 index 0000000..a5bda15 --- /dev/null +++ b/application/common/builder/aside/layout.html @@ -0,0 +1,50 @@ + +
    + +
    + {notempty name="aside.tab_nav"} + + {/notempty} + + {notempty name="aside.tab_con"} +
    + {volist name="aside.tab_con" id="con"} +
    + {notempty name="con"} + {volist name="con" id="_block"} + {switch name="_block.type"} + {include file="../application/common/builder/aside/blocks/recent.html" /} + {include file="../application/common/builder/aside/blocks/online.html" /} + {include file="../application/common/builder/aside/blocks/switch.html" /} + {/switch} + {/volist} + {/notempty} +
    + {/volist} +
    + {/notempty} + + {notempty name="aside.blocks"} +
    + {volist name="aside.blocks" id="_block"} + {switch name="_block.type"} + {include file="../application/common/builder/aside/blocks/recent.html" /} + {include file="../application/common/builder/aside/blocks/online.html" /} + {include file="../application/common/builder/aside/blocks/switch.html" /} + {case value="html"} + {$_block.title|raw|default=''} + {/case} + {/switch} + {/volist} +
    + {/notempty} +
    + +
    + \ No newline at end of file diff --git a/application/common/builder/form/Builder.php b/application/common/builder/form/Builder.php new file mode 100644 index 0000000..0c90d35 --- /dev/null +++ b/application/common/builder/form/Builder.php @@ -0,0 +1,2361 @@ + + */ +class Builder extends ZBuilder +{ + /** + * @var string 模板路径 + */ + private $_template = ''; + + /** + * @var array 模板变量 + */ + private $_vars = [ + 'page_title' => '', // 页面标题 + 'page_tips' => '', // 页面提示 + 'tips_type' => '', // 提示类型 + 'btn_hide' => [], // 要隐藏的按钮 + 'btn_title' => [], // 按钮标题 + 'form_items' => [], // 表单项目 + 'tab_nav' => [], // 页面Tab导航 + 'post_url' => '', // 表单提交地址 + 'form_data' => [], // 表单数据 + 'extra_html' => '', // 额外HTML代码 + 'extra_js' => '', // 额外JS代码 + 'extra_css' => '', // 额外CSS代码 + 'ajax_submit' => true, // 是否ajax提交 + 'hide_header' => false, // 是否隐藏表单头部标题 + 'header_title' => '', // 表单头部标题 + 'js_list' => [], // 需要引入的js文件名 + 'css_list' => [], // 需要引入的css文件名 + 'field_triggers' => [], // 需要触发的表单项名 + 'field_hide' => '', // 需要隐藏的表单项 + 'field_values' => '', // 触发表单项的值 + 'field_clear' => [], // 字段清除 + '_js_files' => [], // 需要加载的js(合并输出) + '_js_init' => [], // 初始化的js(合并输出) + '_css_files' => [], // 需要加载的css(合并输出) + '_layout' => [], // 布局参数 + 'btn_extra' => [], // 额外按钮 + 'submit_confirm' => false, // 提交确认 + 'extend_js_list' => [], // 扩展表单项js列表 + 'extend_css_list' => [], // 扩展表单项css列表 + '_method' => 'post',// 表单提交方式 + 'empty_tips' => '暂无数据',// 没有表单项时的提示信息 + '_token_name' => '__token__', // 表单令牌名称 + '_token_value' => '', // 表单令牌值 + ]; + + /** + * @var bool 是否组合分组 + */ + private $_is_group = false; + + /** + * 初始化 + * @author 蔡伟明 <314013107@qq.com> + */ + public function initialize() + { + $this->_template = Env::get('app_path'). 'common/builder/form/layout.html'; + $this->_vars['post_url'] = $this->request->url(true); + $this->_vars['_token_name'] = config('zbuilder.form_token_name'); + $this->_vars['_token_value'] = $this->request->token($this->_vars['_token_name']); + } + + /** + * 模板变量赋值 + * @param mixed $name 要显示的模板变量 + * @param string $value 变量的值 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function assign($name, $value = '') + { + if (is_array($name)) { + $this->_vars = array_merge($this->_vars, $name); + } else { + $this->_vars[$name] = $value; + } + return $this; + } + + /** + * 设置页面标题 + * @param string $title 页面标题 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setPageTitle($title = '') + { + if ($title != '') { + $this->_vars['page_title'] = trim($title); + } + return $this; + } + + /** + * 设置表单页提示信息 + * @param string $tips 提示信息 + * @param string $type 提示类型:success,info,danger,warning + * @param string $pos 提示位置:top,button + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setPageTips($tips = '', $type = 'info', $pos = 'top') + { + if ($tips != '') { + $this->_vars['page_tips_'.$pos] = $tips; + $this->_vars['tips_type'] = $type != '' ? trim($type) : 'info'; + } + return $this; + } + + /** + * 设置表单提交地址 + * @param string $post_url 提交地址 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setUrl($post_url = '') + { + if ($post_url != '') { + $this->_vars['post_url'] = trim($post_url); + } + return $this; + } + + /** + * 隐藏按钮 + * @param array|string $btn 要隐藏的按钮,如:['submit'],其中'submit'->确认按钮,'back'->返回按钮 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function hideBtn($btn = []) + { + if (!empty($btn)) { + $this->_vars['btn_hide'] = is_array($btn) ? $btn : explode(',', $btn); + } + return $this; + } + + /** + * 添加底部额外按钮 + * @param string $btn 按钮内容 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addBtn($btn = '') + { + if ($btn != '') { + $this->_vars['btn_extra'][] = $btn; + } + return $this; + } + + /** + * 设置按钮标题 + * @param string|array $btn 按钮名 'submit' -> “提交”,'back' -> “返回” + * @param string $title 按钮标题 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setBtnTitle($btn = '', $title = '') + { + if (!empty($btn)) { + if (is_array($btn)) { + $this->_vars['btn_title'] = $btn; + } else { + $this->_vars['btn_title'][trim($btn)] = trim($title); + } + } + return $this; + } + + /** + * 设置提交表单时显示确认框 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function submitConfirm() + { + $this->_vars['submit_confirm'] = true; + return $this; + } + + /** + * 隐藏表单头部标题 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function hideHeaderTitle() + { + $this->_vars['hide_header'] = true; + return $this; + } + + /** + * 设置表单头部标题 + * @param string $title 标题 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setHeaderTitle($title = '') + { + $this->_vars['header_title'] = trim($title); + return $this; + } + + /** + * 设置表单令牌 + * @param string $name 令牌名称 + * @param string $type 令牌生成方法 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setToken($name = '__token__', $type = 'md5') + { + $this->_vars['_token_name'] = $name === '' ? '__token__' : $name; + $this->_vars['_token_value'] = $this->request->token($this->_vars['_token_name'], $type); + return $this; + } + + /** + * 设置触发 + * @param string $trigger 需要触发的表单项名,目前支持select(单选类型)、text、radio三种 + * @param string $values 触发的值 + * @param string $show 触发后要显示的表单项名,目前不支持普通联动、范围、拖动排序、静态文本 + * @param bool $clear 是否清除值 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTrigger($trigger = '', $values = '', $show = '', $clear = true) + { + if (!empty($trigger)) { + if (is_array($trigger)) { + foreach ($trigger as $item) { + $this->_vars['field_hide'] .= $item[2].','; + $this->_vars['field_values'] .= $item[1].','; + $this->_vars['field_triggers'][$item[0]][] = [(string)$item[1], $item[2]]; + $this->_vars['field_clear'][$item[0]] = isset($item[3]) ? ($item[3] === true ? 1 : 0) : 1; + } + } else { + $this->_vars['field_hide'] .= $show.','; + $this->_vars['field_values'] .= (string)$values.','; + $this->_vars['field_triggers'][$trigger][] = [(string)$values, $show]; + $this->_vars['field_clear'][$trigger] = $clear === true ? 1 : 0; + } + } + return $this; + } + + /** + * 添加触发 + * @param array $triggers 触发数组 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addTrigger($triggers = []) + { + if (!empty($triggers)) { + $this->setTrigger($triggers); + } + return $this; + } + + /** + * 添加数组类型的表单项,基本和Textarea是一样的,但读取的时候会用parse_attr函数转换 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author caiweiming <314013107@qq.com> + * @return Builder + */ + public function addArray($name = '', $title = '', $tips = '', $default = '', $extra_attr = '', $extra_class = '') { + return $this->addTextarea($name, $title, $tips, $default, $extra_attr, $extra_class); + } + + /** + * 添加单个档案文件 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addArchive($name = '', $title = '', $tips = '', $default = '' , $extra_class = '') + { + $item = [ + 'type' => 'archive', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加多个档案文件 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addArchives($name = '', $title = '', $tips = '', $default = '' , $extra_class = '') + { + $item = [ + 'type' => 'archives', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加百度地图 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $ak 百度APPKEY + * @param string $tips 提示 + * @param string $default 默认坐标 + * @param string $address 默认地址 + * @param string $level 地图显示级别 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addBmap($name = '', $title = '', $ak = '', $tips = '', $default = '', $address = '', $level = '', $extra_class = '') + { + $item = [ + 'type' => 'bmap', + 'name' => $name, + 'title' => $title, + 'ak' => $ak, + 'tips' => $tips, + 'value' => $default, + 'address' => $address, + 'level' => $level == '' ? 12 : $level, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加按钮 + * @param string $name 表单项名,也是按钮id + * @param array $attr 按钮属性 + * @param string $ele_type 按钮类型,默认为button,也可以为a标签 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addButton($name = '', $attr = [], $ele_type = 'button') + { + $item = [ + 'type' => 'button', + 'name' => $name, + 'id' => $name, + 'ele_type' => $ele_type, + 'data' => '', + ]; + if ($attr) { + foreach ($attr as $key => $value) { + if (substr($key, 0, 5) == 'data-') { + $item['data'] .= $key. '=' . $value . ' '; + } + } + $item = array_merge($item, $attr); + } + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加复选框 + * @param string $name 复选框名 + * @param string $title 复选框标题 + * @param string $tips 提示 + * @param array $options 复选框数据 + * @param string $default 默认值 + * @param array $attr 属性, + * color-颜色(default/primary/info/success/warning/danger),默认primary + * size-尺寸(sm,nm,lg),默认sm + * shape-形状(rounded,square),默认rounded + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addCheckbox($name = '', $title = '', $tips = '', $options = [], $default = '', $attr = [], $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'checkbox', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'options' => $options == '' ? [] : $options, + 'value' => $default, + 'attr' => $attr, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + 'extra_label_class' => $extra_attr == 'disabled' ? 'css-input-disabled' : '', + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加CKEditor编辑器 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $width 编辑器宽度,默认100% + * @param integer $height 编辑器高度,默认400px + * @param string $default 默认值 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addCkeditor($name = '', $title = '', $tips = '', $default = '', $width = '100%', $height = 400, $extra_class = '') + { + $item = [ + 'type' => 'ckeditor', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'width' => $width, + 'height' => $height, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加取色器 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $mode 模式:默认为rgba(含透明度),也可以是rgb + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addColorpicker($name = '', $title = '', $tips = '', $default = '', $mode = 'rgba', $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'colorpicker', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'mode' => $mode, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加日期 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $format 日期格式 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addDate($name = '', $title = '', $tips = '', $default = '', $format = '', $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'date', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'format' => $format == '' ? 'yyyy-mm-dd' : $format, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加日期范围 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $format 格式 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addDaterange($name = '', $title = '', $tips = '', $default = '', $format = '', $extra_attr = '', $extra_class = '') + { + if (strpos($name, ',')) { + list($name_from, $name_to) = explode(',', $name); + $id_from = $name_from; + $id_to = $name_to; + $id = $name_from; + } else { + $name_from = $name_to = $name . '[]'; + $id_from = $name . '_from'; + $id_to = $name . '_to'; + $id = $name; + } + + if (strpos($default, ',') !== false) { + list($value_from, $value_to) = explode(',', $default); + } else { + $value_from = $default; + $value_to = ''; + } + + $item = [ + 'type' => 'daterange', + 'id' => $id, + 'name_from' => $name_from, + 'name_to' => $name_to, + 'id_from' => $id_from, + 'id_to' => $id_to, + 'title' => $title, + 'tips' => $tips, + 'value_from' => $value_from, + 'value_to' => $value_to, + 'format' => $format == '' ? 'yyyy-mm-dd' : $format, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加日期时间 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $format 日期时间格式 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addDatetime($name = '', $title = '', $tips = '', $default = '', $format = '', $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'datetime', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'format' => $format == '' ? 'YYYY-MM-DD HH:mm' : $format, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加markdown编辑器 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param bool $watch 是否实时预览 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addEditormd($name = '', $title = '', $tips = '', $default = '', $watch = true, $extra_class = '') + { + $item = [ + 'type' => 'editormd', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'watch' => $watch, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加单文件上传 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $size 文件大小,单位为kb + * @param string $ext 文件后缀 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addFile($name = '', $title = '', $tips = '', $default = '', $size = '', $ext = '', $extra_class = '') + { + $size = ($size != '' ? $size : config('upload_file_size')) * 1024; + $ext = $ext != '' ? $ext : config('upload_file_ext'); + + $item = [ + 'type' => 'file', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'size' => $size, + 'ext' => $ext, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加多文件上传 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $size 图片大小,单位为kb + * @param string $ext 文件后缀 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addFiles($name = '', $title = '', $tips = '', $default = '', $size = '', $ext = '', $extra_class = '') + { + $size = ($size != '' ? $size : config('upload_file_size')) * 1024; + $ext = $ext != '' ? $ext : config('upload_file_ext'); + + $item = [ + 'type' => 'files', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'size' => $size, + 'ext' => $ext, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加图片相册 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addGallery($name = '', $title = '', $tips = '', $default = '', $extra_class = '') + { + $item = [ + 'type' => 'gallery', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加分组 + * @param array $groups 分组数据 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addGroup($groups = []) + { + if (is_array($groups) && !empty($groups)) { + $this->_is_group = true; + foreach ($groups as &$group) { + foreach ($group as $key => $item) { + $type = array_shift($item); + if (strpos($type, ':')) { + list($type, $layout) = explode(':', $type); + + $layout = explode('|', $layout); + $this->_vars['_layout'][$item[0]] = [ + 'xs' => $layout[0], + 'sm' => isset($layout[1]) ? ($layout[1] == '' ? $layout[0] : $layout[1]) : $layout[0], + 'md' => isset($layout[2]) ? ($layout[2] == '' ? $layout[0] : $layout[2]) : $layout[0], + 'lg' => isset($layout[3]) ? ($layout[3] == '' ? $layout[0] : $layout[3]) : $layout[0], + ]; + } + $group[$key] = call_user_func_array([$this, 'add'.ucfirst($type)], $item); + } + } + $this->_is_group = false; + } + + $item = [ + 'type' => 'group', + 'options' => $groups + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加隐藏表单项 + * @param string $name 表单项名 + * @param string $default 默认值 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addHidden($name = '', $default = '', $extra_class = '') + { + $item = [ + 'type' => 'hidden', + 'name' => $name, + 'value' => $default, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加图标选择器 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addIcon($name = '', $title = '', $tips = '', $default = '', $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'icon', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加单图片上传 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $size 图片大小,单位为kb,0为不限制 + * @param string $ext 文件后缀 + * @param string $extra_class 额外css类名 + * @param array|string $thumb 缩略图参数 + * @param array|string $watermark 水印参数 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addImage($name = '', $title = '', $tips = '', $default = '', $size = '', $ext = '', $extra_class = '', $thumb = '', $watermark = '') + { + $size = ($size != '' ? $size : config('upload_image_size')) * 1024; + $ext = $ext != '' ? $ext : config('upload_image_ext'); + + $item = [ + 'type' => 'image', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'size' => $size, + 'ext' => $ext, + 'extra_class' => $extra_class, + ]; + + // 处理缩略图参数 + if (isset($thumb['size'])) { + $item['thumb'] = $thumb['size'].'|'.(isset($thumb['type']) ? $thumb['type'] : 1); + } else { + $item['thumb'] = $thumb; + } + + // 处理水印参数 + if (isset($watermark['img'])) { + $item['watermark'] = $watermark['img'].'|'.(isset($watermark['pos']) ? $watermark['pos'] : 9).'|'.(isset($watermark['alpha']) ? $watermark['alpha'] : 50); + } else { + $item['watermark'] = $watermark; + } + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加多图片上传 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $size 图片大小,单位为kb,0为不限制 + * @param string $ext 文件后缀 + * @param string $extra_class 额外css类名 + * @param array|string $thumb 缩略图参数 + * @param array|string $watermark 水印参数 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addImages($name = '', $title = '', $tips = '', $default = '', $size = '', $ext = '', $extra_class = '', $thumb = '', $watermark = '') + { + $size = ($size != '' ? $size : config('upload_image_size')) * 1024; + $ext = $ext != '' ? $ext : config('upload_image_ext'); + + $item = [ + 'type' => 'images', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'size' => $size, + 'ext' => $ext, + 'extra_class' => $extra_class, + ]; + + // 处理缩略图参数 + if (isset($thumb['size'])) { + $item['thumb'] = $thumb['size'].'|'.(isset($thumb['type']) ? $thumb['type'] : 1); + } else { + $item['thumb'] = $thumb; + } + + // 处理水印参数 + if (isset($watermark['img'])) { + $item['watermark'] = $watermark['img'].'|'.(isset($watermark['pos']) ? $watermark['pos'] : 9).'|'.(isset($watermark['alpha']) ? $watermark['alpha'] : 50); + } else { + $item['watermark'] = $watermark; + } + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 图片裁剪 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param array $options 参数 + * @param string $extra_class 额外css类名 + * @param array|string $thumb 缩略图参数 + * @param array|string $watermark 水印参数 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addJcrop($name = '', $title = '', $tips = '', $default = '', $options = [], $extra_class = '', $thumb = '', $watermark = '') + { + $item = [ + 'type' => 'jcrop', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'options' => json_encode($options), + 'extra_class' => $extra_class, + ]; + + // 处理缩略图参数 + if (isset($thumb['size'])) { + $item['thumb'] = $thumb['size'].'|'.(isset($thumb['type']) ? $thumb['type'] : 1); + } else { + $item['thumb'] = $thumb; + } + + // 处理水印参数 + if (isset($watermark['img'])) { + $item['watermark'] = $watermark['img'].'|'.(isset($watermark['pos']) ? $watermark['pos'] : 9).'|'.(isset($watermark['alpha']) ? $watermark['alpha'] : 50); + } else { + $item['watermark'] = $watermark; + } + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加普通联动表单项 + * @param string $name 表单项名 + * @param string $title 表单项标题 + * @param string $tips 表单项提示说明 + * @param array $options 表单项options + * @param string $default 默认值 + * @param string $ajax_url 数据异步请求地址 + * 可以用Url方法生成,返回数据格式必须如下: + * $arr['code'] = '1'; //判断状态 + * $arr['msg'] = '请求成功'; //回传信息 + * $arr['list'] = [ + * ['key' => 'gz', 'value' => '广州'], + * ['key' => 'sz', 'value' => '深圳'], + * ]; //数据 + * return json($arr); + * status用于判断是否请求成功,list将作为$next_items第一个表单名的下拉框的内容 + * @param string $next_items 下一级下拉框的表单名 + * 如果有多个关联关系,必须一同写上,用逗号隔开, + * 比如学院作为联动的一个下拉框,它的下级是专业,那么这里就写上专业下拉框的表单名,如:'zy' + * 如果还有班级,那么切换学院的时候,专业和班级应该是一同关联的 + * 所以就必须写上专业和班级的下拉框表单名,如:'zy,bj' + * @param string $param 指定请求参数的key名称,默认为$name的值 + * 比如$param为“key” + * 那么请求数据的时候会发送参数key=某个下拉框选项值 + * @param string $extra_param 额外参数名,可以同时发送表单中的其他表单项值 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addLinkage($name = '', $title = '', $tips = '', $options = [], $default = '', $ajax_url = '', $next_items = '', $param = '', $extra_param = '') + { + $item = [ + 'type' => 'linkage', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'options' => $options, + 'ajax_url' => $ajax_url, + 'next_items' => $next_items, + 'param' => $param == '' ? $name : $param, + 'extra_param' => $extra_param, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 创建快速多级联动Token + * @param string $table 表名 + * @param string $option + * @param string $key + * @author 蔡伟明 <314013107@qq.com> + * @return bool|string + */ + private function createLinkagesToken($table = '', $option = '', $key = '') + { + $table_token = substr(sha1($table.'-'.$option.'-'.$key.'-'.session('user_auth.last_login_ip').'-'.UID.'-'.session('user_auth.last_login_time')), 0, 8); + session($table_token, ['table' => $table, 'option' => $option, 'key' => $key]); + return $table_token; + } + + /** + * 添加快速多级联动 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $table 表名 + * @param int $level 级别 + * @param string $default 默认值 + * @param array|string $fields 字段名,默认为id,name,pid + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addLinkages($name = '', $title = '', $tips = '', $table = '', $level = 2, $default = '', $fields = []) + { + if ($level > 4) { + halt('目前最多只支持4级联动'); + } + + // 键字段名,也就是下拉菜单的option元素的value值 + $key = 'id'; + // 值字段名,也就是下拉菜单显示的各项 + $option = 'name'; + // 父级id字段名 + $pid = 'pid'; + + if (!empty($fields)) { + if (!is_array($fields)) { + $fields = explode(',', $fields); + $key = isset($fields[0]) ? $fields[0] : $key; + $option = isset($fields[1]) ? $fields[1] : $option; + $pid = isset($fields[2]) ? $fields[2] : $pid; + } else { + $key = isset($fields['id']) ? $fields['id'] : $key; + $option = isset($fields['name']) ? $fields['name'] : $option; + $pid = isset($fields['pid']) ? $fields['pid'] : $pid; + } + } + + $linkages_token = $this->createLinkagesToken($table, $option, $key); + + $item = [ + 'type' => 'linkages', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'table' => $table, + 'level' => $level, + 'key' => $key, + 'option' => $option, + 'pid' => $pid, + 'value' => $default, + 'token' => $linkages_token, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加格式文本 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $format 格式 + * @param string $default 默认值 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addMasked($name = '', $title = '', $tips = '', $format = '', $default = '', $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'masked', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'format' => $format, + 'value' => $default, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加数字输入框 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $min 最小值 + * @param string $max 最大值 + * @param string $step 步进值 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addNumber($name = '', $title = '', $tips = '', $default = '', $min = '', $max = '', $step = '', $extra_attr = '', $extra_class = '') + { + if (preg_match('/(.*)\[:(.*)\]/', $title, $matches)) { + $title = $matches[1]; + $placeholder = $matches[2]; + } + + $item = [ + 'type' => 'number', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default == '' ? 0 : $default, + 'min' => $min, + 'max' => $max, + 'step' => $step, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + 'placeholder' => isset($placeholder) ? $placeholder : '请输入'.$title, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加密码框 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addPassword($name = '', $title = '', $tips = '', $default = '', $extra_attr = '', $extra_class = '') + { + if (preg_match('/(.*)\[:(.*)\]/', $title, $matches)) { + $title = $matches[1]; + $placeholder = $matches[2]; + } + + $item = [ + 'type' => 'password', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + 'placeholder' => isset($placeholder) ? $placeholder : '请输入'.$title, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加单选 + * @param string $name 单选名 + * @param string $title 单选标题 + * @param string $tips 提示 + * @param array $options 单选数据 + * @param string $default 默认值 + * @param array $attr 属性, + * color-颜色(default/primary/info/success/warning/danger),默认primary + * size-尺寸(sm,nm,lg),默认sm + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addRadio($name = '', $title = '', $tips = '', $options = [], $default = '', $attr = [], $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'radio', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'options' => $options == '' ? [] : $options, + 'value' => $default, + 'attr' => $attr, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + 'extra_label_class' => $extra_attr == 'disabled' ? 'css-input-disabled' : '', + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加范围 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param array $options 参数 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addRange($name = '', $title = '', $tips = '', $default = '', $options = [], $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'range', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + $item = array_merge($item, $options); + if (isset($item['double']) && $item['double'] == 'true') { + $item['double'] = 'double'; + } + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加普通下拉菜单 + * @param string $name 下拉菜单名 + * @param string $title 标题 + * @param string $tips 提示 + * @param array $options 选项 + * @param string $default 默认值 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addSelect($name = '', $title = '', $tips = '', $options = [], $default = '', $extra_attr = '', $extra_class = '') + { + $type = 'select'; + + if ($extra_attr != '') { + if (in_array('multiple', explode(' ', $extra_attr))) { + $type = 'select2'; + } + } + + $placeholder = $type == 'select' ? '请选择一项' : '请选择一项或多项'; + if (preg_match('/(.*)\[:(.*)\]/', $title, $matches)) { + $title = $matches[1]; + $placeholder = $matches[2]; + } + + $item = [ + 'type' => $type, + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'options' => $options, + 'value' => $default, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + 'placeholder' => $placeholder, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加拖拽排序 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param array $value 值 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addSort($name = '', $title = '', $tips = '', $value = [], $extra_class = '') + { + $content = []; + + if (!empty($value)) { + $content = $value; + } + + $item = [ + 'type' => 'sort', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => implode(',', array_keys($value)), + 'content' => $content, + 'extra_class' => $extra_class + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加静态文本 + * @param string $name 静态表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $hidden 需要提交的值 + * @param string $extra_class 额外css类 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addStatic($name = '', $title = '', $tips = '', $default = '', $hidden = '', $extra_class = '') + { + $item = [ + 'type' => 'static', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'hidden' => $hidden === true ? ($default == '' ? true : $default) : $hidden, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加Summernote编辑器 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $width 编辑器宽度 + * @param int $height 编辑器高度 + * @param string $extra_class + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addSummernote($name = '', $title = '', $tips = '', $default = '', $width = '100%', $height = 350, $extra_class = '') + { + $item = [ + 'type' => 'summernote', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'width' => $width, + 'height' => $height, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加开关 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param array $attr 属性, + * color-颜色(default/primary/info/success/warning/danger),默认primary + * size-尺寸(sm,nm,lg),默认sm + * shape-形状(rounded,square),默认rounded + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addSwitch($name = '', $title = '', $tips = '', $default = '', $attr = [], $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'switch', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'attr' => $attr, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + 'extra_label_class' => $extra_attr == 'disabled' ? 'css-input-disabled' : '', + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加标签 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addTags($name = '', $title = '', $tips = '', $default = '', $extra_class = '') + { + $item = [ + 'type' => 'tags', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => is_array($default) ? implode(',', $default) : $default, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加单行文本框 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param array $group 标签组,可以在文本框前后添加按钮或者文字 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addText($name = '', $title = '', $tips = '', $default = '', $group = [], $extra_attr = '', $extra_class = '') + { + if (preg_match('/(.*)\[:(.*)\]/', $title, $matches)) { + $title = $matches[1]; + $placeholder = $matches[2]; + } + + $item = [ + 'type' => 'text', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'group' => $group, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + 'placeholder' => isset($placeholder) ? $placeholder : '请输入'.$title, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加多行文本框 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addTextarea($name = '', $title = '', $tips = '', $default = '', $extra_attr = '', $extra_class = '') + { + if (preg_match('/(.*)\[:(.*)\]/', $title, $matches)) { + $title = $matches[1]; + $placeholder = $matches[2]; + } + + $item = [ + 'type' => 'textarea', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + 'placeholder' => isset($placeholder) ? $placeholder : '请输入'.$title, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加时间 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $format 日期时间格式 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addTime($name = '', $title = '', $tips = '', $default = '', $format = '', $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'time', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'format' => $format == '' ? 'HH:mm:ss' : $format, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加百度编辑器 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addUeditor($name = '', $title = '', $tips = '', $default = '', $extra_class = '') + { + $item = [ + 'type' => 'ueditor', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加wang编辑器 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function addWangeditor($name = '', $title = '', $tips = '', $default = '', $extra_class = '') + { + $item = [ + 'type' => 'wangeditor', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加表单项 + * 这个是addCheckbox等方法的别名方法,第一个参数传表单项类型,其余参数与各自方法中的参数一致 + * @param string $type 表单项类型 + * @param string $name 表单项名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addFormItem($type = '', $name = '') + { + if ($type != '') { + // 获取所有参数值 + $args = func_get_args(); + array_shift($args); + + // 判断是否有布局参数 + if (strpos($type, ':')) { + list($type, $layout) = explode(':', $type); + + $layout = explode('|', $layout); + $this->_vars['_layout'][$name] = [ + 'xs' => $layout[0], + 'sm' => isset($layout[1]) ? ($layout[1] == '' ? $layout[0] : $layout[1]) : $layout[0], + 'md' => isset($layout[2]) ? ($layout[2] == '' ? $layout[0] : $layout[2]) : $layout[0], + 'lg' => isset($layout[3]) ? ($layout[3] == '' ? $layout[0] : $layout[3]) : $layout[0], + ]; + } + + $method = 'add'. ucfirst($type); + call_user_func_array([$this, $method], $args); + } + return $this; + } + + /** + * 一次性添加多个表单项 + * @param array $items 表单项 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addFormItems($items = []) + { + if (!empty($items)) { + foreach ($items as $item) { + call_user_func_array([$this, 'addFormItem'], $item); + } + } + return $this; + } + + /** + * 直接设置表单项数据 + * @param array $items 表单项数据 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setFormItems($items = []) + { + if (!empty($items)) { + foreach ($items as $key => $item) { + switch ($item['type']) { + case 'group': + foreach ($item['options'] as $options) { + foreach ($options as $option) { + $this->loadMinify($option['type']); + } + } + break; + case 'select': + if (isset($item['extra_attr']) && $item['extra_attr'] == 'multiple') { + $items[$key]['type'] = 'select2'; + } + break; + } + if ($item['type'] == 'group') { + + } else { + $this->loadMinify($item['type']); + } + + // 设置布局参数 + if (isset($item['layout'])) { + $this->_vars['_layout'][$item['name']] = [ + 'xs' => $item['layout'], + 'sm' => $item['layout'], + 'md' => $item['layout'], + 'lg' => $item['layout'], + ]; + } + } + + // 额外已经构造好的表单项目与单个组装的的表单项目进行合并 + $this->_vars['form_items'] = array_merge($this->_vars['form_items'], $items); + } + return $this; + } + + /** + * 扩展额外表单项 + * @param $methodName + * @param $argument + * @author 蔡伟明 <314013107@qq.com> + * @return $this + * @throws Exception + */ + public function __call($methodName, $argument) + { + $type = strtolower(substr($methodName, 3)); + + if ($type != '') { + $class_name = 'form\\'.$type.'\\Builder'; + if (!class_exists($class_name)) { + throw new Exception('类:'.$class_name.'不存在', 7001); + } + + if (method_exists($class_name, 'item')) { + $class = new $class_name; + $form_item = call_user_func_array([$class, 'item'], $argument); + $form_item['type'] = $type; + + if (!empty($class->js)) { + $this->_vars['extend_js_list'][$type] = $this->parseUrl($class->js, $type); + } + if (!empty($class->css)) { + $this->_vars['extend_css_list'][$type] = $this->parseUrl($class->css, $type); + } + + if ($this->_is_group) { + return $form_item; + } + + $this->_vars['form_items'][] = $form_item; + } else { + throw new Exception('扩展表单项未定义item()方法', 7001); + } + } + return $this; + } + + /** + * 解析扩展表单项资源url + * @param array $urls 资源url + * @param string $type 表单项类型名称 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + private function parseUrl($urls = [], $type = '') + { + foreach ($urls as $key => $item) { + if (!preg_match('/__.*?__/', $item)) { + $urls[$key] = '__EXTEND_FORM__/'.$type.'/'.$item; + } + $urls[$key] = str_replace(array_keys(config('template.tpl_replace_string')), array_values(config('template.tpl_replace_string')), $urls[$key]); + } + return $urls; + } + + /** + * 设置Tab按钮列表 + * @param array $tab_list Tab列表 如:['tab1' => ['title' => '标题', 'url' => 'http://www.dolphinphp.com']] + * @param string $curr_tab 当前tab名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTabNav($tab_list = [], $curr_tab = '') + { + if (!empty($tab_list)) { + $this->_vars['tab_nav'] = [ + 'tab_list' => $tab_list, + 'curr_tab' => $curr_tab, + ]; + } + return $this; + } + + /** + * 设置表单数据 + * @param array $form_data 表单数据 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setFormData($form_data = []) + { + if (!empty($form_data)) { + $this->_vars['form_data'] = $form_data; + } + return $this; + } + + /** + * 设置额外HTML代码 + * @param string $extra_html 额外HTML代码 + * @param string $tag 标记 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setExtraHtml($extra_html = '', $tag = '') + { + if ($extra_html != '') { + $tag != '' && $tag = '_'.$tag; + $this->_vars['extra_html'.$tag] = $extra_html; + } + return $this; + } + + /** + * 设置额外JS代码 + * @param string $extra_js 额外JS代码 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setExtraJs($extra_js = '') + { + if ($extra_js != '') { + $this->_vars['extra_js'] = $extra_js; + } + return $this; + } + + /** + * 设置额外CSS代码 + * @param string $extra_css 额外CSS代码 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setExtraCss($extra_css = '') + { + if ($extra_css != '') { + $this->_vars['extra_css'] = $extra_css; + } + return $this; + } + + /** + * 表单项布局 + * @param array $column 布局参数 ['表单项名' => 所占宽度,....] + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function layout($column = []) + { + if (!empty($column)) { + foreach ($column as $field => $layout) { + $layout = explode('|', $layout); + $this->_vars['_layout'][$field] = [ + 'xs' => $layout[0], + 'sm' => isset($layout[1]) ? ($layout[1] == '' ? $layout[0] : $layout[1]) : $layout[0], + 'md' => isset($layout[2]) ? ($layout[2] == '' ? $layout[0] : $layout[2]) : $layout[0], + 'lg' => isset($layout[3]) ? ($layout[3] == '' ? $layout[0] : $layout[3]) : $layout[0], + ]; + } + } + return $this; + } + + /** + * 引入模块js文件 + * @param string $files_name js文件名,多个文件用逗号隔开 + * @param string $module 指定模块 + * @author caiweiming <314013107@qq.com> + * @return $this + */ + public function js($files_name = '', $module = '') + { + if ($files_name != '') { + $this->loadFile('js', $files_name, $module); + } + return $this; + } + + /** + * 引入模块css文件 + * @param string $files_name css文件名,多个文件用逗号隔开 + * @param string $module 指定模块 + * @author caiweiming <314013107@qq.com> + * @return $this + */ + public function css($files_name = '', $module = '') + { + if ($files_name != '') { + $this->loadFile('css', $files_name, $module); + } + return $this; + } + + /** + * 设置表单提交方式 + * @param string $value 提交方式 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function method($value = '') + { + if ($value != '') { + $this->_vars['_method'] = $value; + $this->_vars['ajax_submit'] = strtolower($value) == 'get' ? false : true; + } + return $this; + } + + /** + * 引入css或js文件 + * @param string $type 类型:css/js + * @param string $files_name 文件名,多个用逗号隔开 + * @param string $module 指定模块 + * @author caiweiming <314013107@qq.com> + */ + private function loadFile($type = '', $files_name = '', $module = '') + { + if ($files_name != '') { + $module = $module == '' ? $this->request->module() : $module; + if (!is_array($files_name)) { + $files_name = explode(',', $files_name); + } + foreach ($files_name as $item) { + if (strpos($item, '/')) { + $this->_vars[$type.'_list'][] = PUBLIC_PATH. 'static/'. $item.'.'.$type; + } else { + $this->_vars[$type.'_list'][] = PUBLIC_PATH. 'static/'. $module .'/'.$type.'/'.$item.'.'.$type; + } + } + } + } + + /** + * 设置ajax方式提交 + * @param bool $ajax_submit 默认true,false为关闭ajax方式提交 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function isAjax($ajax_submit = true) + { + $this->_vars['ajax_submit'] = $ajax_submit; + return $this; + } + + /** + * 设置模版路径 + * @param string $template 模板路径 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTemplate($template = '') + { + if ($template != '') { + $this->_template = $template; + } + return $this; + } + + /** + * 根据表单项类型,加载不同js和css文件,并合并 + * @param string $type 表单项类型 + * @author 蔡伟明 <314013107@qq.com> + */ + private function loadMinify($type = '') + { + if ($type != '') { + switch ($type) { + case 'colorpicker': + $this->_vars['_js_files'][] = 'colorpicker_js'; + $this->_vars['_css_files'][] = 'colorpicker_css'; + $this->_vars['_js_init'][] = 'colorpicker'; + break; + case 'ckeditor': + $this->_vars['_ckeditor'] = '1'; + $this->_vars['_js_init'][] = 'ckeditor'; + break; + case 'date': + case 'daterange': + $this->_vars['_js_files'][] = 'datepicker_js'; + $this->_vars['_css_files'][] = 'datepicker_css'; + $this->_vars['_js_init'][] = 'datepicker'; + break; + case 'datetime': + case 'time': + $this->_vars['_js_files'][] = 'datetimepicker_js'; + $this->_vars['_css_files'][] = 'datetimepicker_css'; + $this->_vars['_js_init'][] = 'datetimepicker'; + break; + case 'editormd': + $this->_vars['_js_files'][] = 'editormd_js'; + $this->_vars['_editormd'] = '1'; + break; + case 'images': + $this->_vars['_js_files'][] = 'jqueryui_js'; + case 'file': + case 'files': + case 'image': + $this->_vars['_js_files'][] = 'webuploader_js'; + $this->_vars['_css_files'][] = 'webuploader_css'; + break; + case 'icon': + $this->_vars['_icon'] = '1'; + break; + case 'jcrop': + $this->_vars['_js_files'][] = 'jcrop_js'; + $this->_vars['_css_files'][] = 'jcrop_css'; + break; + case 'linkage': + case 'linkages': + case 'select': + case 'select2': + $this->_vars['_js_files'][] = 'select2_js'; + $this->_vars['_css_files'][] = 'select2_css'; + $this->_vars['_js_init'][] = 'select2'; + break; + case 'masked': + $this->_vars['_js_files'][] = 'masked_inputs_js'; + break; + case 'range': + $this->_vars['_js_files'][] = 'rangeslider_js'; + $this->_vars['_css_files'][] = 'rangeslider_css'; + $this->_vars['_js_init'][] = 'rangeslider'; + break; + case 'sort': + $this->_vars['_js_files'][] = 'nestable_js'; + $this->_vars['_css_files'][] = 'nestable_css'; + break; + case 'tags': + $this->_vars['_js_files'][] = 'tags_js'; + $this->_vars['_css_files'][] = 'tags_css'; + $this->_vars['_js_init'][] = 'tags-inputs'; + break; + case 'ueditor': + $this->_vars['_ueditor'] = '1'; + break; + case 'wangeditor': + $this->_vars['_js_files'][] = 'wangeditor_js'; + $this->_vars['_css_files'][] = 'wangeditor_css'; + break; + case 'summernote': + $this->_vars['_js_files'][] = 'summernote_js'; + $this->_vars['_css_files'][] = 'summernote_css'; + $this->_vars['_js_init'][] = 'summernote'; + break; + } + } else { + if ($this->_vars['form_items']) { + foreach ($this->_vars['form_items'] as &$item) { + // 判断是否为分组 + if ($item['type'] == 'group') { + foreach ($item['options'] as &$group) { + foreach ($group as $key => $value) { + if ($group[$key]['type'] != '') { + $this->loadMinify($group[$key]['type']); + } + } + } + } else { + if ($item['type'] != '') { + $this->loadMinify($item['type']); + } + } + } + } + } + } + + /** + * 设置表单项的值 + * @author 蔡伟明 <314013107@qq.com> + */ + private function setFormValue() + { + if ($this->_vars['form_data']) { + foreach ($this->_vars['form_items'] as &$item) { + // 判断是否为分组 + if ($item['type'] == 'group') { + foreach ($item['options'] as &$group) { + foreach ($group as $key => $value) { + // 针对日期范围特殊处理 + switch ($value['type']) { + case 'daterange': + if ($value['name_from'] == $value['name_to']) { + list($group[$key]['value_from'], $group[$key]['value_to']) = $this->_vars['form_data'][$value['id']]; + } else { + $group[$key]['value_from'] = $this->_vars['form_data'][$value['name_from']]; + $group[$key]['value_to'] = $this->_vars['form_data'][$value['name_to']]; + } + break; + case 'datetime': + case 'date': + case 'time': + if (isset($this->_vars['form_data'][$value['name']])) { + $group[$key]['value'] = $this->_vars['form_data'][$value['name']]; + } else { + $group[$key]['value'] = isset($value['value']) ? $value['value'] : ''; + } + + if (is_numeric($group[$key]['value'])) { + if ($value['type'] == 'datetime' || $value['type'] == 'time') { + $group[$key]['value'] = format_moment($group[$key]['value'], $value['format']); + } else { + $group[$key]['value'] = format_date($group[$key]['value'], $value['format']); + } + } + break; + case 'bmap': + $group[$key]['address'] = $this->_vars['form_data'][$value['name'].'_address']; + default: + if (isset($this->_vars['form_data'][$value['name']])) { + $group[$key]['value'] = $this->_vars['form_data'][$value['name']]; + } else { + $group[$key]['value'] = ''; + } + } + if ($group[$key]['type'] == 'static' && $group[$key]['hidden'] != '') { + $group[$key]['hidden'] = $this->_vars['form_data'][$value['name']]; + } + } + } + } else { + // 针对日期范围特殊处理 + switch ($item['type']) { + case 'daterange': + if ($item['name_from'] == $item['name_to']) { + list($item['value_from'], $item['value_to']) = $this->_vars['form_data'][$item['id']]; + } else { + $item['value_from'] = $this->_vars['form_data'][$item['name_from']]; + $item['value_to'] = $this->_vars['form_data'][$item['name_to']]; + } + break; + case 'datetime': + case 'date': + case 'time': + if (isset($this->_vars['form_data'][$item['name']])) { + $item['value'] = $this->_vars['form_data'][$item['name']]; + } else { + $item['value'] = isset($item['value']) ? $item['value'] : ''; + } + + if (is_numeric($item['value'])) { + if ($item['type'] == 'datetime' || $item['type'] == 'time') { + $item['value'] = format_moment($item['value'], $item['format']); + } else { + $item['value'] = format_date($item['value'], $item['format']); + } + } + break; + case 'bmap': + $item['address'] = $this->_vars['form_data'][$item['name'].'_address']; + default: + if (isset($this->_vars['form_data'][$item['name']])) { + $item['value'] = $this->_vars['form_data'][$item['name']]; + } else { + $item['value'] = isset($item['value']) ? $item['value'] : ''; + } + + } + if ($item['type'] == 'static' && $item['hidden'] != '') { + $item['hidden'] = $this->_vars['form_data'][$item['name']]; + } + // 处理拖拽排序组件 + if ($item['type'] == 'sort') { + $value = explode(',', $item['value']); + $item['content'] = array_merge(array_flip($value), $item['content']); + } + } + } + } + } + + /** + * 加载模板输出 + * @param string $template 模板文件名 + * @param array $vars 模板输出变量 + * @param array $config 模板参数 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function fetch($template = '', $vars = [], $config = []) + { + if (!empty($vars)) { + $this->_vars['form_data'] = array_merge($this->_vars['form_data'], $vars); + } + + // 设置表单值 + $this->setFormValue(); + + // 处理不同表单类型加载不同js和css + $this->loadMinify(); + + // 处理页面标题 + if ($this->_vars['page_title'] == '' && defined('ENTRANCE') && ENTRANCE == 'admin') { + $location = get_location('', false, false); + if ($location) { + $curr_location = end($location); + $this->_vars['page_title'] = $curr_location['title']; + } + } + + // 另外设置模板 + if ($template != '') { + $this->_template = $template; + } + + // 处理需要隐藏的表单项,去除最后一个逗号 + if ($this->_vars['field_hide'] != '') { + $this->_vars['field_hide'] = rtrim($this->_vars['field_hide'], ','); + } + if ($this->_vars['field_values'] != '') { + $this->_vars['field_values'] = explode(',', $this->_vars['field_values']); + $this->_vars['field_values'] = array_filter($this->_vars['field_values'], 'strlen'); + $this->_vars['field_values'] = implode(',', array_unique($this->_vars['field_values'])); + } + + // 处理js和css合并的参数 + if (!empty($this->_vars['_js_files'])) { + $this->_vars['_js_files'] = array_unique($this->_vars['_js_files']); + sort($this->_vars['_js_files']); + } + if (!empty($this->_vars['_css_files'])) { + $this->_vars['_css_files'] = array_unique($this->_vars['_css_files']); + sort($this->_vars['_css_files']); + } + if (!empty($this->_vars['_js_init'])) { + $this->_vars['_js_init'] = array_unique($this->_vars['_js_init']); + sort($this->_vars['_js_init']); + $this->_vars['_js_init'] = json_encode($this->_vars['_js_init']); + } + + // 处理额外按钮 + $this->_vars['btn_extra'] = implode(' ', $this->_vars['btn_extra']); + + // 实例化视图并渲染 + return parent::fetch($this->_template, $this->_vars, $config); + } +} diff --git a/application/common/builder/form/icon/fa.html b/application/common/builder/form/icon/fa.html new file mode 100644 index 0000000..79435ff --- /dev/null +++ b/application/common/builder/form/icon/fa.html @@ -0,0 +1 @@ +
    • fa-glass
    • fa-music
    • fa-search
    • fa-envelope-o
    • fa-heart
    • fa-star
    • fa-star-o
    • fa-user
    • fa-film
    • fa-th-large
    • fa-th
    • fa-th-list
    • fa-check
    • fa-remove
    • fa-close
    • fa-times
    • fa-search-plus
    • fa-search-minus
    • fa-power-off
    • fa-signal
    • fa-gear
    • fa-cog
    • fa-trash-o
    • fa-home
    • fa-file-o
    • fa-clock-o
    • fa-road
    • fa-download
    • fa-arrow-circle-o-down
    • fa-arrow-circle-o-up
    • fa-inbox
    • fa-play-circle-o
    • fa-rotate-right
    • fa-repeat
    • fa-refresh
    • fa-list-alt
    • fa-lock
    • fa-flag
    • fa-headphones
    • fa-volume-off
    • fa-volume-down
    • fa-volume-up
    • fa-qrcode
    • fa-barcode
    • fa-tag
    • fa-tags
    • fa-book
    • fa-bookmark
    • fa-print
    • fa-camera
    • fa-font
    • fa-bold
    • fa-italic
    • fa-text-height
    • fa-text-width
    • fa-align-left
    • fa-align-center
    • fa-align-right
    • fa-align-justify
    • fa-list
    • fa-dedent
    • fa-outdent
    • fa-indent
    • fa-video-camera
    • fa-photo
    • fa-image
    • fa-picture-o
    • fa-pencil
    • fa-map-marker
    • fa-adjust
    • fa-tint
    • fa-edit
    • fa-pencil-square-o
    • fa-share-square-o
    • fa-check-square-o
    • fa-arrows
    • fa-step-backward
    • fa-fast-backward
    • fa-backward
    • fa-play
    • fa-pause
    • fa-stop
    • fa-forward
    • fa-fast-forward
    • fa-step-forward
    • fa-eject
    • fa-chevron-left
    • fa-chevron-right
    • fa-plus-circle
    • fa-minus-circle
    • fa-times-circle
    • fa-check-circle
    • fa-question-circle
    • fa-info-circle
    • fa-crosshairs
    • fa-times-circle-o
    • fa-check-circle-o
    • fa-ban
    • fa-arrow-left
    • fa-arrow-right
    • fa-arrow-up
    • fa-arrow-down
    • fa-mail-forward
    • fa-share
    • fa-expand
    • fa-compress
    • fa-plus
    • fa-minus
    • fa-asterisk
    • fa-exclamation-circle
    • fa-gift
    • fa-leaf
    • fa-fire
    • fa-eye
    • fa-eye-slash
    • fa-warning
    • fa-exclamation-triangle
    • fa-plane
    • fa-calendar
    • fa-random
    • fa-comment
    • fa-magnet
    • fa-chevron-up
    • fa-chevron-down
    • fa-retweet
    • fa-shopping-cart
    • fa-folder
    • fa-folder-open
    • fa-arrows-v
    • fa-arrows-h
    • fa-bar-chart-o
    • fa-bar-chart
    • fa-twitter-square
    • fa-facebook-square
    • fa-camera-retro
    • fa-key
    • fa-gears
    • fa-cogs
    • fa-comments
    • fa-thumbs-o-up
    • fa-thumbs-o-down
    • fa-star-half
    • fa-heart-o
    • fa-sign-out
    • fa-linkedin-square
    • fa-thumb-tack
    • fa-external-link
    • fa-sign-in
    • fa-trophy
    • fa-github-square
    • fa-upload
    • fa-lemon-o
    • fa-phone
    • fa-square-o
    • fa-bookmark-o
    • fa-phone-square
    • fa-twitter
    • fa-facebook-f
    • fa-facebook
    • fa-github
    • fa-unlock
    • fa-credit-card
    • fa-feed
    • fa-rss
    • fa-hdd-o
    • fa-bullhorn
    • fa-bell
    • fa-certificate
    • fa-hand-o-right
    • fa-hand-o-left
    • fa-hand-o-up
    • fa-hand-o-down
    • fa-arrow-circle-left
    • fa-arrow-circle-right
    • fa-arrow-circle-up
    • fa-arrow-circle-down
    • fa-globe
    • fa-wrench
    • fa-tasks
    • fa-filter
    • fa-briefcase
    • fa-arrows-alt
    • fa-group
    • fa-users
    • fa-chain
    • fa-link
    • fa-cloud
    • fa-flask
    • fa-cut
    • fa-scissors
    • fa-copy
    • fa-files-o
    • fa-paperclip
    • fa-save
    • fa-floppy-o
    • fa-square
    • fa-navicon
    • fa-reorder
    • fa-bars
    • fa-list-ul
    • fa-list-ol
    • fa-strikethrough
    • fa-underline
    • fa-table
    • fa-magic
    • fa-truck
    • fa-pinterest
    • fa-pinterest-square
    • fa-google-plus-square
    • fa-google-plus
    • fa-money
    • fa-caret-down
    • fa-caret-up
    • fa-caret-left
    • fa-caret-right
    • fa-columns
    • fa-unsorted
    • fa-sort
    • fa-sort-down
    • fa-sort-desc
    • fa-sort-up
    • fa-sort-asc
    • fa-envelope
    • fa-linkedin
    • fa-rotate-left
    • fa-undo
    • fa-legal
    • fa-gavel
    • fa-dashboard
    • fa-tachometer
    • fa-comment-o
    • fa-comments-o
    • fa-flash
    • fa-bolt
    • fa-sitemap
    • fa-umbrella
    • fa-paste
    • fa-clipboard
    • fa-lightbulb-o
    • fa-exchange
    • fa-cloud-download
    • fa-cloud-upload
    • fa-user-md
    • fa-stethoscope
    • fa-suitcase
    • fa-bell-o
    • fa-coffee
    • fa-cutlery
    • fa-file-text-o
    • fa-building-o
    • fa-hospital-o
    • fa-ambulance
    • fa-medkit
    • fa-fighter-jet
    • fa-beer
    • fa-h-square
    • fa-plus-square
    • fa-angle-double-left
    • fa-angle-double-right
    • fa-angle-double-up
    • fa-angle-double-down
    • fa-angle-left
    • fa-angle-right
    • fa-angle-up
    • fa-angle-down
    • fa-desktop
    • fa-laptop
    • fa-tablet
    • fa-mobile-phone
    • fa-mobile
    • fa-circle-o
    • fa-quote-left
    • fa-quote-right
    • fa-spinner
    • fa-circle
    • fa-mail-reply
    • fa-reply
    • fa-github-alt
    • fa-folder-o
    • fa-folder-open-o
    • fa-smile-o
    • fa-frown-o
    • fa-meh-o
    • fa-gamepad
    • fa-keyboard-o
    • fa-flag-o
    • fa-flag-checkered
    • fa-terminal
    • fa-code
    • fa-mail-reply-all
    • fa-reply-all
    • fa-star-half-empty
    • fa-star-half-full
    • fa-star-half-o
    • fa-location-arrow
    • fa-crop
    • fa-code-fork
    • fa-unlink
    • fa-chain-broken
    • fa-question
    • fa-info
    • fa-exclamation
    • fa-superscript
    • fa-subscript
    • fa-eraser
    • fa-puzzle-piece
    • fa-microphone
    • fa-microphone-slash
    • fa-shield
    • fa-calendar-o
    • fa-fire-extinguisher
    • fa-rocket
    • fa-maxcdn
    • fa-chevron-circle-left
    • fa-chevron-circle-right
    • fa-chevron-circle-up
    • fa-chevron-circle-down
    • fa-html5
    • fa-css3
    • fa-anchor
    • fa-unlock-alt
    • fa-bullseye
    • fa-ellipsis-h
    • fa-ellipsis-v
    • fa-rss-square
    • fa-play-circle
    • fa-ticket
    • fa-minus-square
    • fa-minus-square-o
    • fa-level-up
    • fa-level-down
    • fa-check-square
    • fa-pencil-square
    • fa-external-link-square
    • fa-share-square
    • fa-compass
    • fa-toggle-down
    • fa-caret-square-o-down
    • fa-toggle-up
    • fa-caret-square-o-up
    • fa-toggle-right
    • fa-caret-square-o-right
    • fa-euro
    • fa-eur
    • fa-gbp
    • fa-dollar
    • fa-usd
    • fa-rupee
    • fa-inr
    • fa-cny
    • fa-rmb
    • fa-yen
    • fa-jpy
    • fa-ruble
    • fa-rouble
    • fa-rub
    • fa-won
    • fa-krw
    • fa-bitcoin
    • fa-btc
    • fa-file
    • fa-file-text
    • fa-sort-alpha-asc
    • fa-sort-alpha-desc
    • fa-sort-amount-asc
    • fa-sort-amount-desc
    • fa-sort-numeric-asc
    • fa-sort-numeric-desc
    • fa-thumbs-up
    • fa-thumbs-down
    • fa-youtube-square
    • fa-youtube
    • fa-xing
    • fa-xing-square
    • fa-youtube-play
    • fa-dropbox
    • fa-stack-overflow
    • fa-instagram
    • fa-flickr
    • fa-adn
    • fa-bitbucket
    • fa-bitbucket-square
    • fa-tumblr
    • fa-tumblr-square
    • fa-long-arrow-down
    • fa-long-arrow-up
    • fa-long-arrow-left
    • fa-long-arrow-right
    • fa-apple
    • fa-windows
    • fa-android
    • fa-linux
    • fa-dribbble
    • fa-skype
    • fa-foursquare
    • fa-trello
    • fa-female
    • fa-male
    • fa-gittip
    • fa-gratipay
    • fa-sun-o
    • fa-moon-o
    • fa-archive
    • fa-bug
    • fa-vk
    • fa-weibo
    • fa-renren
    • fa-pagelines
    • fa-stack-exchange
    • fa-arrow-circle-o-right
    • fa-arrow-circle-o-left
    • fa-toggle-left
    • fa-caret-square-o-left
    • fa-dot-circle-o
    • fa-wheelchair
    • fa-vimeo-square
    • fa-turkish-lira
    • fa-try
    • fa-plus-square-o
    • fa-space-shuttle
    • fa-slack
    • fa-envelope-square
    • fa-wordpress
    • fa-openid
    • fa-institution
    • fa-bank
    • fa-university
    • fa-mortar-board
    • fa-graduation-cap
    • fa-yahoo
    • fa-google
    • fa-reddit
    • fa-reddit-square
    • fa-stumbleupon-circle
    • fa-stumbleupon
    • fa-delicious
    • fa-digg
    • fa-pied-piper-pp
    • fa-pied-piper-alt
    • fa-drupal
    • fa-joomla
    • fa-language
    • fa-fax
    • fa-building
    • fa-child
    • fa-paw
    • fa-spoon
    • fa-cube
    • fa-cubes
    • fa-behance
    • fa-behance-square
    • fa-steam
    • fa-steam-square
    • fa-recycle
    • fa-automobile
    • fa-car
    • fa-cab
    • fa-taxi
    • fa-tree
    • fa-spotify
    • fa-deviantart
    • fa-soundcloud
    • fa-database
    • fa-file-pdf-o
    • fa-file-word-o
    • fa-file-excel-o
    • fa-file-powerpoint-o
    • fa-file-photo-o
    • fa-file-picture-o
    • fa-file-image-o
    • fa-file-zip-o
    • fa-file-archive-o
    • fa-file-sound-o
    • fa-file-audio-o
    • fa-file-movie-o
    • fa-file-video-o
    • fa-file-code-o
    • fa-vine
    • fa-codepen
    • fa-jsfiddle
    • fa-life-bouy
    • fa-life-buoy
    • fa-life-saver
    • fa-support
    • fa-life-ring
    • fa-circle-o-notch
    • fa-ra
    • fa-resistance
    • fa-rebel
    • fa-ge
    • fa-empire
    • fa-git-square
    • fa-git
    • fa-y-combinator-square
    • fa-yc-square
    • fa-hacker-news
    • fa-tencent-weibo
    • fa-qq
    • fa-wechat
    • fa-weixin
    • fa-send
    • fa-paper-plane
    • fa-send-o
    • fa-paper-plane-o
    • fa-history
    • fa-circle-thin
    • fa-header
    • fa-paragraph
    • fa-sliders
    • fa-share-alt
    • fa-share-alt-square
    • fa-bomb
    • fa-soccer-ball-o
    • fa-futbol-o
    • fa-tty
    • fa-binoculars
    • fa-plug
    • fa-slideshare
    • fa-twitch
    • fa-yelp
    • fa-newspaper-o
    • fa-wifi
    • fa-calculator
    • fa-paypal
    • fa-google-wallet
    • fa-cc-visa
    • fa-cc-mastercard
    • fa-cc-discover
    • fa-cc-amex
    • fa-cc-paypal
    • fa-cc-stripe
    • fa-bell-slash
    • fa-bell-slash-o
    • fa-trash
    • fa-copyright
    • fa-at
    • fa-eyedropper
    • fa-paint-brush
    • fa-birthday-cake
    • fa-area-chart
    • fa-pie-chart
    • fa-line-chart
    • fa-lastfm
    • fa-lastfm-square
    • fa-toggle-off
    • fa-toggle-on
    • fa-bicycle
    • fa-bus
    • fa-ioxhost
    • fa-angellist
    • fa-cc
    • fa-shekel
    • fa-sheqel
    • fa-ils
    • fa-meanpath
    • fa-buysellads
    • fa-connectdevelop
    • fa-dashcube
    • fa-forumbee
    • fa-leanpub
    • fa-sellsy
    • fa-shirtsinbulk
    • fa-simplybuilt
    • fa-skyatlas
    • fa-cart-plus
    • fa-cart-arrow-down
    • fa-diamond
    • fa-ship
    • fa-user-secret
    • fa-motorcycle
    • fa-street-view
    • fa-heartbeat
    • fa-venus
    • fa-mars
    • fa-mercury
    • fa-intersex
    • fa-transgender
    • fa-transgender-alt
    • fa-venus-double
    • fa-mars-double
    • fa-venus-mars
    • fa-mars-stroke
    • fa-mars-stroke-v
    • fa-mars-stroke-h
    • fa-neuter
    • fa-genderless
    • fa-facebook-official
    • fa-pinterest-p
    • fa-whatsapp
    • fa-server
    • fa-user-plus
    • fa-user-times
    • fa-hotel
    • fa-bed
    • fa-viacoin
    • fa-train
    • fa-subway
    • fa-medium
    • fa-yc
    • fa-y-combinator
    • fa-optin-monster
    • fa-opencart
    • fa-expeditedssl
    • fa-battery-4
    • fa-battery
    • fa-battery-full
    • fa-battery-3
    • fa-battery-three-quarters
    • fa-battery-2
    • fa-battery-half
    • fa-battery-1
    • fa-battery-quarter
    • fa-battery-0
    • fa-battery-empty
    • fa-mouse-pointer
    • fa-i-cursor
    • fa-object-group
    • fa-object-ungroup
    • fa-sticky-note
    • fa-sticky-note-o
    • fa-cc-jcb
    • fa-cc-diners-club
    • fa-clone
    • fa-balance-scale
    • fa-hourglass-o
    • fa-hourglass-1
    • fa-hourglass-start
    • fa-hourglass-2
    • fa-hourglass-half
    • fa-hourglass-3
    • fa-hourglass-end
    • fa-hourglass
    • fa-hand-grab-o
    • fa-hand-rock-o
    • fa-hand-stop-o
    • fa-hand-paper-o
    • fa-hand-scissors-o
    • fa-hand-lizard-o
    • fa-hand-spock-o
    • fa-hand-pointer-o
    • fa-hand-peace-o
    • fa-trademark
    • fa-registered
    • fa-creative-commons
    • fa-gg
    • fa-gg-circle
    • fa-tripadvisor
    • fa-odnoklassniki
    • fa-odnoklassniki-square
    • fa-get-pocket
    • fa-wikipedia-w
    • fa-safari
    • fa-chrome
    • fa-firefox
    • fa-opera
    • fa-internet-explorer
    • fa-tv
    • fa-television
    • fa-contao
    • fa-500px
    • fa-amazon
    • fa-calendar-plus-o
    • fa-calendar-minus-o
    • fa-calendar-times-o
    • fa-calendar-check-o
    • fa-industry
    • fa-map-pin
    • fa-map-signs
    • fa-map-o
    • fa-map
    • fa-commenting
    • fa-commenting-o
    • fa-houzz
    • fa-vimeo
    • fa-black-tie
    • fa-fonticons
    • fa-reddit-alien
    • fa-edge
    • fa-credit-card-alt
    • fa-codiepie
    • fa-modx
    • fa-fort-awesome
    • fa-usb
    • fa-product-hunt
    • fa-mixcloud
    • fa-scribd
    • fa-pause-circle
    • fa-pause-circle-o
    • fa-stop-circle
    • fa-stop-circle-o
    • fa-shopping-bag
    • fa-shopping-basket
    • fa-hashtag
    • fa-bluetooth
    • fa-bluetooth-b
    • fa-percent
    • fa-gitlab
    • fa-wpbeginner
    • fa-wpforms
    • fa-envira
    • fa-universal-access
    • fa-wheelchair-alt
    • fa-question-circle-o
    • fa-blind
    • fa-audio-description
    • fa-volume-control-phone
    • fa-braille
    • fa-assistive-listening-systems
    • fa-asl-interpreting
    • fa-american-sign-language-interpreting
    • fa-deafness
    • fa-hard-of-hearing
    • fa-deaf
    • fa-glide
    • fa-glide-g
    • fa-signing
    • fa-sign-language
    • fa-low-vision
    • fa-viadeo
    • fa-viadeo-square
    • fa-snapchat
    • fa-snapchat-ghost
    • fa-snapchat-square
    • fa-pied-piper
    • fa-first-order
    • fa-yoast
    • fa-themeisle
    • fa-google-plus-circle
    • fa-google-plus-official
    • fa-fa
    • fa-font-awesome
    • fa-handshake-o
    • fa-envelope-open
    • fa-envelope-open-o
    • fa-linode
    • fa-address-book
    • fa-address-book-o
    • fa-vcard
    • fa-address-card
    • fa-vcard-o
    • fa-address-card-o
    • fa-user-circle
    • fa-user-circle-o
    • fa-user-o
    • fa-id-badge
    • fa-drivers-license
    • fa-id-card
    • fa-drivers-license-o
    • fa-id-card-o
    • fa-quora
    • fa-free-code-camp
    • fa-telegram
    • fa-thermometer-4
    • fa-thermometer
    • fa-thermometer-full
    • fa-thermometer-3
    • fa-thermometer-three-quarters
    • fa-thermometer-2
    • fa-thermometer-half
    • fa-thermometer-1
    • fa-thermometer-quarter
    • fa-thermometer-0
    • fa-thermometer-empty
    • fa-shower
    • fa-bathtub
    • fa-s15
    • fa-bath
    • fa-podcast
    • fa-window-maximize
    • fa-window-minimize
    • fa-window-restore
    • fa-times-rectangle
    • fa-window-close
    • fa-times-rectangle-o
    • fa-window-close-o
    • fa-bandcamp
    • fa-grav
    • fa-etsy
    • fa-imdb
    • fa-ravelry
    • fa-eercast
    • fa-microchip
    • fa-snowflake-o
    • fa-superpowers
    • fa-wpexplorer
    • fa-meetup
    \ No newline at end of file diff --git a/application/common/builder/form/icon/gl.html b/application/common/builder/form/icon/gl.html new file mode 100644 index 0000000..c9c4b18 --- /dev/null +++ b/application/common/builder/form/icon/gl.html @@ -0,0 +1 @@ +
    • glyphicon-adjust
    • glyphicon-alert
    • glyphicon-align-center
    • glyphicon-align-justify
    • glyphicon-align-left
    • glyphicon-align-right
    • glyphicon-apple
    • glyphicon-arrow-down
    • glyphicon-arrow-left
    • glyphicon-arrow-right
    • glyphicon-arrow-up
    • glyphicon-asterisk
    • glyphicon-baby-formula
    • glyphicon-backward
    • glyphicon-ban-circle
    • glyphicon-barcode
    • glyphicon-bed
    • glyphicon-bell
    • glyphicon-bishop
    • glyphicon-bitcoin
    • glyphicon-blackboard
    • glyphicon-bold
    • glyphicon-book
    • glyphicon-bookmark
    • glyphicon-briefcase
    • glyphicon-bullhorn
    • glyphicon-calendar
    • glyphicon-camera
    • glyphicon-cd
    • glyphicon-certificate
    • glyphicon-check
    • glyphicon-chevron-down
    • glyphicon-chevron-left
    • glyphicon-chevron-right
    • glyphicon-chevron-up
    • glyphicon-circle-arrow-down
    • glyphicon-circle-arrow-left
    • glyphicon-circle-arrow-right
    • glyphicon-circle-arrow-up
    • glyphicon-cloud
    • glyphicon-cloud-download
    • glyphicon-cloud-upload
    • glyphicon-cog
    • glyphicon-collapse-down
    • glyphicon-collapse-up
    • glyphicon-comment
    • glyphicon-compressed
    • glyphicon-console
    • glyphicon-copy
    • glyphicon-copyright-mark
    • glyphicon-credit-card
    • glyphicon-cutlery
    • glyphicon-dashboard
    • glyphicon-download
    • glyphicon-download-alt
    • glyphicon-duplicate
    • glyphicon-earphone
    • glyphicon-edit
    • glyphicon-education
    • glyphicon-eject
    • glyphicon-envelope
    • glyphicon-equalizer
    • glyphicon-erase
    • glyphicon-eur
    • glyphicon-euro
    • glyphicon-exclamation-sign
    • glyphicon-expand
    • glyphicon-export
    • glyphicon-eye-close
    • glyphicon-eye-open
    • glyphicon-facetime-video
    • glyphicon-fast-backward
    • glyphicon-fast-forward
    • glyphicon-file
    • glyphicon-film
    • glyphicon-filter
    • glyphicon-fire
    • glyphicon-flag
    • glyphicon-flash
    • glyphicon-floppy-disk
    • glyphicon-floppy-open
    • glyphicon-floppy-remove
    • glyphicon-floppy-save
    • glyphicon-floppy-saved
    • glyphicon-folder-close
    • glyphicon-folder-open
    • glyphicon-font
    • glyphicon-forward
    • glyphicon-fullscreen
    • glyphicon-gbp
    • glyphicon-gift
    • glyphicon-glass
    • glyphicon-globe
    • glyphicon-grain
    • glyphicon-hand-down
    • glyphicon-hand-left
    • glyphicon-hand-right
    • glyphicon-hand-up
    • glyphicon-hd-video
    • glyphicon-hdd
    • glyphicon-header
    • glyphicon-headphones
    • glyphicon-heart
    • glyphicon-heart-empty
    • glyphicon-home
    • glyphicon-hourglass
    • glyphicon-ice-lolly
    • glyphicon-ice-lolly-tasted
    • glyphicon-import
    • glyphicon-inbox
    • glyphicon-indent-left
    • glyphicon-indent-right
    • glyphicon-info-sign
    • glyphicon-italic
    • glyphicon-king
    • glyphicon-knight
    • glyphicon-lamp
    • glyphicon-leaf
    • glyphicon-level-up
    • glyphicon-link
    • glyphicon-list
    • glyphicon-list-alt
    • glyphicon-lock
    • glyphicon-log-in
    • glyphicon-log-out
    • glyphicon-magnet
    • glyphicon-map-marker
    • glyphicon-menu-down
    • glyphicon-menu-hamburger
    • glyphicon-menu-left
    • glyphicon-menu-right
    • glyphicon-menu-up
    • glyphicon-minus
    • glyphicon-minus-sign
    • glyphicon-modal-window
    • glyphicon-move
    • glyphicon-music
    • glyphicon-new-window
    • glyphicon-object-align-bottom
    • glyphicon-object-align-horizontal
    • glyphicon-object-align-left
    • glyphicon-object-align-right
    • glyphicon-object-align-top
    • glyphicon-object-align-vertical
    • glyphicon-off
    • glyphicon-oil
    • glyphicon-ok
    • glyphicon-ok-circle
    • glyphicon-ok-sign
    • glyphicon-open
    • glyphicon-open-file
    • glyphicon-option-horizontal
    • glyphicon-option-vertical
    • glyphicon-paperclip
    • glyphicon-paste
    • glyphicon-pause
    • glyphicon-pawn
    • glyphicon-pencil
    • glyphicon-phone
    • glyphicon-phone-alt
    • glyphicon-picture
    • glyphicon-piggy-bank
    • glyphicon-plane
    • glyphicon-play
    • glyphicon-play-circle
    • glyphicon-plus
    • glyphicon-plus-sign
    • glyphicon-print
    • glyphicon-pushpin
    • glyphicon-qrcode
    • glyphicon-queen
    • glyphicon-question-sign
    • glyphicon-random
    • glyphicon-record
    • glyphicon-refresh
    • glyphicon-registration-mark
    • glyphicon-remove
    • glyphicon-remove-circle
    • glyphicon-remove-sign
    • glyphicon-repeat
    • glyphicon-resize-full
    • glyphicon-resize-horizontal
    • glyphicon-resize-small
    • glyphicon-resize-vertical
    • glyphicon-retweet
    • glyphicon-road
    • glyphicon-ruble
    • glyphicon-save
    • glyphicon-save-file
    • glyphicon-saved
    • glyphicon-scale
    • glyphicon-scissors
    • glyphicon-screenshot
    • glyphicon-sd-video
    • glyphicon-search
    • glyphicon-send
    • glyphicon-share
    • glyphicon-share-alt
    • glyphicon-shopping-cart
    • glyphicon-signal
    • glyphicon-sort
    • glyphicon-sort-by-alphabet
    • glyphicon-sort-by-alphabet-alt
    • glyphicon-sort-by-attributes
    • glyphicon-sort-by-attributes-alt
    • glyphicon-sort-by-order
    • glyphicon-sort-by-order-alt
    • glyphicon-sound-5-1
    • glyphicon-sound-6-1
    • glyphicon-sound-7-1
    • glyphicon-sound-dolby
    • glyphicon-sound-stereo
    • glyphicon-star
    • glyphicon-star-empty
    • glyphicon-stats
    • glyphicon-step-backward
    • glyphicon-step-forward
    • glyphicon-stop
    • glyphicon-subscript
    • glyphicon-subtitles
    • glyphicon-sunglasses
    • glyphicon-superscript
    • glyphicon-tag
    • glyphicon-tags
    • glyphicon-tasks
    • glyphicon-tent
    • glyphicon-text-background
    • glyphicon-text-color
    • glyphicon-text-height
    • glyphicon-text-size
    • glyphicon-text-width
    • glyphicon-th
    • glyphicon-th-large
    • glyphicon-th-list
    • glyphicon-thumbs-down
    • glyphicon-thumbs-up
    • glyphicon-time
    • glyphicon-tint
    • glyphicon-tower
    • glyphicon-transfer
    • glyphicon-trash
    • glyphicon-tree-conifer
    • glyphicon-tree-deciduous
    • glyphicon-triangle-bottom
    • glyphicon-triangle-left
    • glyphicon-triangle-right
    • glyphicon-triangle-top
    • glyphicon-volume-down
    • glyphicon-volume-off
    • glyphicon-volume-up
    • glyphicon-unchecked
    • glyphicon-upload
    • glyphicon-usd
    • glyphicon-user
    • glyphicon-warning-sign
    • glyphicon-wrench
    • glyphicon-yen
    • glyphicon-zoom-in
    • glyphicon-zoom-out
    \ No newline at end of file diff --git a/application/common/builder/form/icon/sl.html b/application/common/builder/form/icon/sl.html new file mode 100644 index 0000000..d5ebf7b --- /dev/null +++ b/application/common/builder/form/icon/sl.html @@ -0,0 +1 @@ +
    • si-action-redo
    • si-action-undo
    • si-anchor
    • si-arrow-down
    • si-arrow-left
    • si-arrow-right
    • si-arrow-up
    • si-badge
    • si-bag
    • si-ban
    • si-bar-chart
    • si-basket
    • si-basket-loaded
    • si-bell
    • si-book-open
    • si-briefcase
    • si-bubble
    • si-bubbles
    • si-bulb
    • si-calculator
    • si-calendar
    • si-call-end
    • si-call-in
    • si-call-out
    • si-camcorder
    • si-camera
    • si-check
    • si-chemistry
    • si-clock
    • si-close
    • si-cloud-download
    • si-cloud-upload
    • si-compass
    • si-control-end
    • si-control-forward
    • si-control-pause
    • si-control-play
    • si-control-rewind
    • si-control-start
    • si-credit-card
    • si-crop
    • si-cup
    • si-cursor
    • si-cursor-move
    • si-diamond
    • si-direction
    • si-directions
    • si-disc
    • si-dislike
    • si-doc
    • si-docs
    • si-drawer
    • si-drop
    • si-earphones
    • si-earphones-alt
    • si-emoticon-smile
    • si-energy
    • si-envelope
    • si-envelope-letter
    • si-envelope-open
    • si-equalizer
    • si-eye
    • si-eyeglasses
    • si-feed
    • si-film
    • si-fire
    • si-flag
    • si-folder
    • si-folder-alt
    • si-frame
    • si-game-controller
    • si-ghost
    • si-globe
    • si-globe-alt
    • si-graduation
    • si-graph
    • si-grid
    • si-handbag
    • si-heart
    • si-home
    • si-hourglass
    • si-info
    • si-key
    • si-layers
    • si-like
    • si-link
    • si-list
    • si-lock
    • si-lock-open
    • si-login
    • si-logout
    • si-loop
    • si-magic-wand
    • si-magnet
    • si-magnifier
    • si-magnifier-add
    • si-magnifier-remove
    • si-map
    • si-microphone
    • si-mouse
    • si-moustache
    • si-music-tone
    • si-music-tone-alt
    • si-note
    • si-notebook
    • si-paper-clip
    • si-paper-plane
    • si-pencil
    • si-picture
    • si-pie-chart
    • si-pin
    • si-plane
    • si-playlist
    • si-plus
    • si-pointer
    • si-power
    • si-present
    • si-printer
    • si-puzzle
    • si-question
    • si-refresh
    • si-reload
    • si-rocket
    • si-screen-desktop
    • si-screen-smartphone
    • si-screen-tablet
    • si-settings
    • si-share
    • si-share-alt
    • si-shield
    • si-shuffle
    • si-size-actual
    • si-size-fullscreen
    • si-social-dribbble
    • si-social-dropbox
    • si-social-facebook
    • si-social-tumblr
    • si-social-twitter
    • si-social-youtube
    • si-speech
    • si-speedometer
    • si-star
    • si-support
    • si-symbol-female
    • si-symbol-male
    • si-tag
    • si-target
    • si-trash
    • si-trophy
    • si-umbrella
    • si-user
    • si-user-female
    • si-user-follow
    • si-user-following
    • si-user-unfollow
    • si-users
    • si-vector
    • si-volume-1
    • si-volume-2
    • si-volume-off
    • si-wallet
    • si-wrench
    \ No newline at end of file diff --git a/application/common/builder/form/items/archive.html b/application/common/builder/form/items/archive.html new file mode 100644 index 0000000..6bb051a --- /dev/null +++ b/application/common/builder/form/items/archive.html @@ -0,0 +1,14 @@ +
    + +
    +
      + {notempty name="form[type].value"} +
    • {$form[type].value|get_file_name} [下载]
    • + {/notempty} +
    + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/archives.html b/application/common/builder/form/items/archives.html new file mode 100644 index 0000000..0c331da --- /dev/null +++ b/application/common/builder/form/items/archives.html @@ -0,0 +1,16 @@ +
    + +
    +
      + {notempty name="form[type].value"} + {volist name="form[type]['value']|explode=',',###" id="vo"} +
    • {$vo|get_file_name} [下载]
    • + {/volist} + {/notempty} +
    + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/bmap.html b/application/common/builder/form/items/bmap.html new file mode 100644 index 0000000..bc77f99 --- /dev/null +++ b/application/common/builder/form/items/bmap.html @@ -0,0 +1,14 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    + +
    +
    +
    + + \ No newline at end of file diff --git a/application/common/builder/form/items/button.html b/application/common/builder/form/items/button.html new file mode 100644 index 0000000..6b71ef6 --- /dev/null +++ b/application/common/builder/form/items/button.html @@ -0,0 +1,9 @@ +
    +
    + {eq name="form[type].ele_type" value="button"} + + {else/} + {$form[type].title|default=''} + {/eq} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/checkbox.html b/application/common/builder/form/items/checkbox.html new file mode 100644 index 0000000..9b9b8f8 --- /dev/null +++ b/application/common/builder/form/items/checkbox.html @@ -0,0 +1,13 @@ +
    + +
    + {volist name="form[type].options" id="option"} + + {/volist} + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/ckeditor.html b/application/common/builder/form/items/ckeditor.html new file mode 100644 index 0000000..cb51866 --- /dev/null +++ b/application/common/builder/form/items/ckeditor.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/colorpicker.html b/application/common/builder/form/items/colorpicker.html new file mode 100644 index 0000000..d0a35e2 --- /dev/null +++ b/application/common/builder/form/items/colorpicker.html @@ -0,0 +1,12 @@ +
    + +
    +
    + + +
    + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/date.html b/application/common/builder/form/items/date.html new file mode 100644 index 0000000..cda7f09 --- /dev/null +++ b/application/common/builder/form/items/date.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/daterange.html b/application/common/builder/form/items/daterange.html new file mode 100644 index 0000000..7bece40 --- /dev/null +++ b/application/common/builder/form/items/daterange.html @@ -0,0 +1,13 @@ +
    + +
    +
    + + + +
    + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/datetime.html b/application/common/builder/form/items/datetime.html new file mode 100644 index 0000000..0bce4a2 --- /dev/null +++ b/application/common/builder/form/items/datetime.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/editormd.html b/application/common/builder/form/items/editormd.html new file mode 100644 index 0000000..12b3b24 --- /dev/null +++ b/application/common/builder/form/items/editormd.html @@ -0,0 +1,15 @@ +{php} + $upload_image_ext = explode(',', config("upload_image_ext")); + $upload_image_ext = json_encode($upload_image_ext); +{/php} +
    + +
    +
    + +
    + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/file.html b/application/common/builder/form/items/file.html new file mode 100644 index 0000000..7d2312f --- /dev/null +++ b/application/common/builder/form/items/file.html @@ -0,0 +1,17 @@ +
    + +
    +
      + {notempty name="form[type].value"} +
    • {$form[type].value|get_file_name} [下载] [删除]
    • + {/notempty} +
    + +
    上传单个文件
    + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/files.html b/application/common/builder/form/items/files.html new file mode 100644 index 0000000..31f8c1e --- /dev/null +++ b/application/common/builder/form/items/files.html @@ -0,0 +1,19 @@ +
    + +
    +
      + {notempty name="form[type].value"} + {volist name="form[type]['value']|explode=',',###" id="vo"} +
    • {$vo|get_file_name} [下载] [删除]
    • + {/volist} + {/notempty} +
    + +
    上传多个文件
    + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/gallery.html b/application/common/builder/form/items/gallery.html new file mode 100644 index 0000000..d63601e --- /dev/null +++ b/application/common/builder/form/items/gallery.html @@ -0,0 +1,22 @@ +
    + +
    + +
    + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/group.html b/application/common/builder/form/items/group.html new file mode 100644 index 0000000..b3b708c --- /dev/null +++ b/application/common/builder/form/items/group.html @@ -0,0 +1,214 @@ +
    +
    +
    + +
    + {volist name="form.options" id="items" key="items_key"} +
    + {volist name="items" id="form_group"} + {switch name="form_group.type"} + {case value="archive"} + {// 档案文件 } + {include file="../application/common/builder/form/items/archive.html" type='_group' /} + {/case} + + {case value="archives"} + {// 多个档案文件 } + {include file="../application/common/builder/form/items/archives.html" type='_group' /} + {/case} + + {case value="bmap"} + {// 百度地图 } + {include file="../application/common/builder/form/items/bmap.html" type="_group" /} + {/case} + + {case value="button"} + {// 按钮 } + {include file="../application/common/builder/form/items/button.html" type="_group" /} + {/case} + + {case value="checkbox"} + {// 多选 } + {include file="../application/common/builder/form/items/checkbox.html" type="_group" /} + {/case} + + {case value="ckeditor"} + {// ckeditor编辑器 } + {include file="../application/common/builder/form/items/ckeditor.html" type="_group" /} + {/case} + + {case value="colorpicker"} + {// 取色器 } + {include file="../application/common/builder/form/items/colorpicker.html" type="_group" /} + {/case} + + {case value="date"} + {// 日期 } + {include file="../application/common/builder/form/items/date.html" type="_group" /} + {/case} + + {case value="daterange"} + {// 日期范围 } + {include file="../application/common/builder/form/items/daterange.html" type="_group" /} + {/case} + + {case value="datetime"} + {// 日期时间 } + {include file="../application/common/builder/form/items/datetime.html" type="_group" /} + {/case} + + {case value="editormd"} + {// markdown编辑器 } + {include file="../application/common/builder/form/items/editormd.html" type="_group" /} + {/case} + + {case value="file"} + {// 单文件上传 } + {include file="../application/common/builder/form/items/file.html" type="_group" /} + {/case} + + {case value="files"} + {// 多文件上传 } + {include file="../application/common/builder/form/items/files.html" type="_group" /} + {/case} + + {case value="gallery"} + {// 图片相册 } + {include file="../application/common/builder/form/items/gallery.html" type="_group" /} + {/case} + + {case value="hidden"} + {// 隐藏 } + {include file="../application/common/builder/form/items/hidden.html" type="_group" /} + {/case} + + {case value="icon"} + {// 图标选择器 } + {include file="../application/common/builder/form/items/icon.html" type="_group" /} + {/case} + + {case value="image"} + {// 单图片上传 } + {include file="../application/common/builder/form/items/image.html" type="_group" /} + {/case} + + {case value="images"} + {// 多图片上传 } + {include file="../application/common/builder/form/items/images.html" type="_group" /} + {/case} + + {case value="jcrop"} + {// 图片裁剪 } + {include file="../application/common/builder/form/items/jcrop.html" type="_group" /} + {/case} + + {case value="linkage"} + {// 联动下拉框 } + {include file="../application/common/builder/form/items/linkage.html" type="_group" /} + {/case} + + {case value="linkages"} + {// 多级联动下拉框 } + {include file="../application/common/builder/form/items/linkages.html" type="_group" /} + {/case} + + {case value="masked"} + {// 格式文本 } + {include file="../application/common/builder/form/items/masked.html" type="_group" /} + {/case} + + {case value="number"} + {// 数字 } + {include file="../application/common/builder/form/items/number.html" type="_group" /} + {/case} + + {case value="password"} + {// 密码 } + {include file="../application/common/builder/form/items/password.html" type="_group" /} + {/case} + + {case value="radio"} + {// 单选 } + {include file="../application/common/builder/form/items/radio.html" type="_group" /} + {/case} + + {case value="range"} + {// 范围 } + {include file="../application/common/builder/form/items/range.html" type="_group" /} + {/case} + + {case value="select"} + {// 下拉菜单 } + {include file="../application/common/builder/form/items/select.html" type="_group" /} + {/case} + + {case value="select2"} + {// 下拉多选 } + {include file="../application/common/builder/form/items/select2.html" type="_group" /} + {/case} + + {case value="sort"} + {// 排序 } + {include file="../application/common/builder/form/items/sort.html" type="_group" /} + {/case} + + {case value="static"} + {// 静态文本 } + {include file="../application/common/builder/form/items/static.html" type="_group" /} + {/case} + + {case value="summernote"} + {// summernote编辑器 } + {include file="../application/common/builder/form/items/summernote.html" type="_group" /} + {/case} + + {case value="switch"} + {// 开关 } + {include file="../application/common/builder/form/items/switch.html" type="_group" /} + {/case} + + {case value="tags"} + {// 标签 } + {include file="../application/common/builder/form/items/tags.html" type="_group" /} + {/case} + + {case value="text"} + {// 单行文本 } + {include file="../application/common/builder/form/items/text.html" type="_group" /} + {/case} + + {case value="time"} + {// 时间 } + {include file="../application/common/builder/form/items/time.html" type="_group" /} + {/case} + + {case value="textarea|array"} + {// 文本框|数组 } + {include file="../application/common/builder/form/items/textarea.html" type="_group" /} + {/case} + + {case value="ueditor"} + {// 百度编辑器 } + {include file="../application/common/builder/form/items/ueditor.html" type="_group" /} + {/case} + + {case value="wangeditor"} + {// wang编辑器 } + {include file="../application/common/builder/form/items/wangeditor.html" type="_group" /} + {/case} + {default/} + {:extend_form_item($form_group, $_layout)} + {/switch} + {/volist} +
    + {/volist} +
    +
    +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/hidden.html b/application/common/builder/form/items/hidden.html new file mode 100644 index 0000000..f3c593b --- /dev/null +++ b/application/common/builder/form/items/hidden.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/application/common/builder/form/items/icon.html b/application/common/builder/form/items/icon.html new file mode 100644 index 0000000..f1ea6bd --- /dev/null +++ b/application/common/builder/form/items/icon.html @@ -0,0 +1,13 @@ +
    + +
    +
    + + + +
    + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/image.html b/application/common/builder/form/items/image.html new file mode 100644 index 0000000..98994e8 --- /dev/null +++ b/application/common/builder/form/items/image.html @@ -0,0 +1,19 @@ +
    + +
    +
    + {notempty name="form[type].value"} +
    + + +
    + {/notempty} +
    +
    + +
    上传单张图片
    + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/images.html b/application/common/builder/form/items/images.html new file mode 100644 index 0000000..3372adb --- /dev/null +++ b/application/common/builder/form/items/images.html @@ -0,0 +1,22 @@ +
    + +
    +
    + {notempty name="form[type].value"} + {volist name="form[type]['value']|explode=',',###" id="vo"} +
    + + + +
    + {/volist} + {/notempty} +
    +
    + +
    上传多张图片
    + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/jcrop.html b/application/common/builder/form/items/jcrop.html new file mode 100644 index 0000000..29634d8 --- /dev/null +++ b/application/common/builder/form/items/jcrop.html @@ -0,0 +1,48 @@ +
    + +
    +
    + + +
    +
    + + + + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    + + + +
    \ No newline at end of file diff --git a/application/common/builder/form/items/linkage.html b/application/common/builder/form/items/linkage.html new file mode 100644 index 0000000..2ab8eeb --- /dev/null +++ b/application/common/builder/form/items/linkage.html @@ -0,0 +1,14 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/linkages.html b/application/common/builder/form/items/linkages.html new file mode 100644 index 0000000..517a21b --- /dev/null +++ b/application/common/builder/form/items/linkages.html @@ -0,0 +1,101 @@ +{php} + // 获取一级联动数据 + $level_one = get_level_data($form[type]['table'], 0, $form[type]['pid']); + $level_key = []; + $level_data = []; + + // 有默认值 + if ($form[type]['value'] != '') { + $level_key_data = get_level_key_data($form[type]['table'], $form[type]['value'], $form[type]['key'], $form[type]['option'], $form[type]['pid']); + $level_key = $level_key_data['key']; + $level_data = $level_key_data['data']; + sort($level_key); + $level_data = array_reverse($level_data); + } +{/php} +
    + +
    + +
    + + {eq name="form[type].level" value="2"} +
    + +
    + {/eq} + + {eq name="form[type].level" value="3"} +
    + +
    +
    + +
    + {/eq} + + {eq name="form[type].level" value="4"} +
    + +
    +
    + +
    +
    + +
    + {/eq} + +
    + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/masked.html b/application/common/builder/form/items/masked.html new file mode 100644 index 0000000..1df3500 --- /dev/null +++ b/application/common/builder/form/items/masked.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/number.html b/application/common/builder/form/items/number.html new file mode 100644 index 0000000..9571ee9 --- /dev/null +++ b/application/common/builder/form/items/number.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/password.html b/application/common/builder/form/items/password.html new file mode 100644 index 0000000..0ae2a9b --- /dev/null +++ b/application/common/builder/form/items/password.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/radio.html b/application/common/builder/form/items/radio.html new file mode 100644 index 0000000..acab05d --- /dev/null +++ b/application/common/builder/form/items/radio.html @@ -0,0 +1,14 @@ +
    + +
    + {volist name="form[type].options" id="option"} + + {/volist} + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/range.html b/application/common/builder/form/items/range.html new file mode 100644 index 0000000..801a68a --- /dev/null +++ b/application/common/builder/form/items/range.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/select.html b/application/common/builder/form/items/select.html new file mode 100644 index 0000000..7036396 --- /dev/null +++ b/application/common/builder/form/items/select.html @@ -0,0 +1,14 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/select2.html b/application/common/builder/form/items/select2.html new file mode 100644 index 0000000..593ed93 --- /dev/null +++ b/application/common/builder/form/items/select2.html @@ -0,0 +1,14 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/sort.html b/application/common/builder/form/items/sort.html new file mode 100644 index 0000000..f95d7b1 --- /dev/null +++ b/application/common/builder/form/items/sort.html @@ -0,0 +1,23 @@ +
    + + +
    + {empty name="form[type].value"} +
    暂无数据,无法排序
    + {else/} +
    +
      + {volist name="form[type].content" id="item"} +
    1. +
      拖拽
      {$item|htmlspecialchars}
      +
    2. + {/volist} +
    +
    + {/empty} + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/static.html b/application/common/builder/form/items/static.html new file mode 100644 index 0000000..ed16372 --- /dev/null +++ b/application/common/builder/form/items/static.html @@ -0,0 +1,12 @@ +
    + +
    +
    {$form[type].value|default=''|htmlspecialchars}
    + {if ($form[type]['hidden'] !== '') AND ($form[type]['hidden'] !== false) } + + {/if} + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/summernote.html b/application/common/builder/form/items/summernote.html new file mode 100644 index 0000000..d1b6133 --- /dev/null +++ b/application/common/builder/form/items/summernote.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/switch.html b/application/common/builder/form/items/switch.html new file mode 100644 index 0000000..47b43a4 --- /dev/null +++ b/application/common/builder/form/items/switch.html @@ -0,0 +1,11 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/tags.html b/application/common/builder/form/items/tags.html new file mode 100644 index 0000000..cf2de58 --- /dev/null +++ b/application/common/builder/form/items/tags.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/text.html b/application/common/builder/form/items/text.html new file mode 100644 index 0000000..0bc912a --- /dev/null +++ b/application/common/builder/form/items/text.html @@ -0,0 +1,26 @@ +
    + +
    + {notempty name="form[type].group"} +
    + {/notempty} + + {notempty name="form[type].group.0"} + {$form[type].group.0|raw|clear_js} + {/notempty} + + + + {notempty name="form[type].group.1"} + {$form[type].group.1|raw|clear_js} + {/notempty} + + {notempty name="form[type].group"} +
    + {/notempty} + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/textarea.html b/application/common/builder/form/items/textarea.html new file mode 100644 index 0000000..0c88743 --- /dev/null +++ b/application/common/builder/form/items/textarea.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/time.html b/application/common/builder/form/items/time.html new file mode 100644 index 0000000..32af502 --- /dev/null +++ b/application/common/builder/form/items/time.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/ueditor.html b/application/common/builder/form/items/ueditor.html new file mode 100644 index 0000000..4701435 --- /dev/null +++ b/application/common/builder/form/items/ueditor.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/wangeditor.html b/application/common/builder/form/items/wangeditor.html new file mode 100644 index 0000000..1cab9a2 --- /dev/null +++ b/application/common/builder/form/items/wangeditor.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|raw|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/layout.html b/application/common/builder/form/layout.html new file mode 100644 index 0000000..2f015ad --- /dev/null +++ b/application/common/builder/form/layout.html @@ -0,0 +1,378 @@ +{extend name="$_admin_base_layout" /} + +{block name="content"} + {notempty name="page_tips_top"} +
    + +

    {$page_tips_top|raw}

    +
    + {/notempty} + {$extra_html_content_top|raw|default=''} +
    +
    +
    + {notempty name="tab_nav"} + + {else/} + {empty name="_pop"} +
    +
      +
    • + +
    • +
    • + +
    • +
    +

    {$page_title|default=""}

    +
    + {/empty} + {/notempty} +
    +
    +
    + {$extra_html_form_top|raw|default=''} +
    + {notempty name="_token_name"} + {:token($_token_name, $_token_value)} + {/notempty} + {empty name="form_items"} + {notempty name="empty_tips"} +
    +

    + {$empty_tips|raw}
    +

    +
    + {/notempty} + {else /} + {volist name="form_items" id="form"} + {switch name="form.type"} + {case value="archive"} + {// 档案文件 } + {include file="../application/common/builder/form/items/archive.html" type='' /} + {/case} + + {case value="archives"} + {// 多个档案文件 } + {include file="../application/common/builder/form/items/archives.html" type='' /} + {/case} + + {case value="bmap"} + {// 百度地图 } + {include file="../application/common/builder/form/items/bmap.html" type='' /} + {/case} + + {case value="button"} + {// 按钮 } + {include file="../application/common/builder/form/items/button.html" type='' /} + {/case} + + {case value="checkbox"} + {// 多选 } + {include file="../application/common/builder/form/items/checkbox.html" type='' /} + {/case} + + {case value="ckeditor"} + {// ckeditor编辑器 } + {include file="../application/common/builder/form/items/ckeditor.html" type='' /} + {/case} + + {case value="colorpicker"} + {// 取色器 } + {include file="../application/common/builder/form/items/colorpicker.html" type='' /} + {/case} + + {case value="date"} + {// 日期 } + {include file="../application/common/builder/form/items/date.html" type='' /} + {/case} + + {case value="daterange"} + {// 日期范围 } + {include file="../application/common/builder/form/items/daterange.html" type='' /} + {/case} + + {case value="datetime"} + {// 日期时间 } + {include file="../application/common/builder/form/items/datetime.html" type='' /} + {/case} + + {case value="editormd"} + {// markdown编辑器 } + {include file="../application/common/builder/form/items/editormd.html" type='' /} + {/case} + + {case value="file"} + {// 单文件上传 } + {include file="../application/common/builder/form/items/file.html" type='' /} + {/case} + + {case value="files"} + {// 多文件上传 } + {include file="../application/common/builder/form/items/files.html" type='' /} + {/case} + + {case value="gallery"} + {// 图片相册 } + {include file="../application/common/builder/form/items/gallery.html" type='' /} + {/case} + + {case value="group"} + {// 分组 } + {include file="../application/common/builder/form/items/group.html" type='' /} + {/case} + + {case value="hidden"} + {// 隐藏 } + {include file="../application/common/builder/form/items/hidden.html" type='' /} + {/case} + + {case value="icon"} + {// 图标选择器 } + {include file="../application/common/builder/form/items/icon.html" type='' /} + {/case} + + {case value="image"} + {// 单图片上传 } + {include file="../application/common/builder/form/items/image.html" type='' /} + {/case} + + {case value="images"} + {// 多图片上传 } + {include file="../application/common/builder/form/items/images.html" type='' /} + {/case} + + {case value="jcrop"} + {// 图片裁剪 } + {include file="../application/common/builder/form/items/jcrop.html" type='' /} + {/case} + + {case value="linkage"} + {// 联动下拉框 } + {include file="../application/common/builder/form/items/linkage.html" type='' /} + {/case} + + {case value="linkages"} + {// 多级联动下拉框 } + {include file="../application/common/builder/form/items/linkages.html" type='' /} + {/case} + + {case value="masked"} + {// 格式文本 } + {include file="../application/common/builder/form/items/masked.html" type='' /} + {/case} + + {case value="number"} + {// 数字 } + {include file="../application/common/builder/form/items/number.html" type='' /} + {/case} + + {case value="password"} + {// 密码 } + {include file="../application/common/builder/form/items/password.html" type='' /} + {/case} + + {case value="radio"} + {// 单选 } + {include file="../application/common/builder/form/items/radio.html" type='' /} + {/case} + + {case value="range"} + {// 范围 } + {include file="../application/common/builder/form/items/range.html" type='' /} + {/case} + + {case value="select"} + {// 下拉菜单 } + {include file="../application/common/builder/form/items/select.html" type='' /} + {/case} + + {case value="select2"} + {// 下拉多选 } + {include file="../application/common/builder/form/items/select2.html" type='' /} + {/case} + + {case value="sort"} + {// 排序 } + {include file="../application/common/builder/form/items/sort.html" type='' /} + {/case} + + {case value="static"} + {// 静态文本 } + {include file="../application/common/builder/form/items/static.html" type='' /} + {/case} + + {case value="summernote"} + {// summernote编辑器 } + {include file="../application/common/builder/form/items/summernote.html" type='' /} + {/case} + + {case value="switch"} + {// 开关 } + {include file="../application/common/builder/form/items/switch.html" type='' /} + {/case} + + {case value="tags"} + {// 标签 } + {include file="../application/common/builder/form/items/tags.html" type='' /} + {/case} + + {case value="text"} + {// 单行文本 } + {include file="../application/common/builder/form/items/text.html" type='' /} + {/case} + + {case value="time"} + {// 时间 } + {include file="../application/common/builder/form/items/time.html" type='' /} + {/case} + + {case value="textarea|array"} + {// 文本框|数组 } + {include file="../application/common/builder/form/items/textarea.html" type='' /} + {/case} + + {case value="ueditor"} + {// 百度编辑器 } + {include file="../application/common/builder/form/items/ueditor.html" type='' /} + {/case} + + {case value="wangeditor"} + {// wang编辑器 } + {include file="../application/common/builder/form/items/wangeditor.html" type='' /} + {/case} + {default/} + {:extend_form_item($form, $_layout)} + {/switch} + {/volist} + {/empty} +
    +
    + {php}if(isset($btn_hide) && !in_array('submit', $btn_hide)):{/php} + + {php}endif;{/php} + + {empty name="_pop"} + {php}if(isset($btn_hide) && !in_array('back', $btn_hide)):{/php} + + {php}endif;{/php} + {else/} + + {/empty} + + {// 额外按钮} + {$btn_extra|raw|default=''} +
    +
    +
    + {$extra_html_form_bottom|raw|default=''} +
    +
    +
    +
    +
    +
    + {notempty name="page_tips_bottom"} +
    + +

    {$page_tips_bottom|raw}

    +
    + {/notempty} + {// 图标 } + {notempty name="_icon"} + + {/notempty} + {$extra_html_content_bottom|raw|default=''} +{/block} + +{block name="style"} + {notempty name="_editormd"} + + {/notempty} + + {volist name="css_list" id="vo"} + + {/volist} + + {// 额外CSS代码 } + {$extra_css|raw|default=''} +{/block} + +{block name="script"} + {notempty name="_ueditor"} + + + {/notempty} + + {notempty name="_ckeditor"} + + {/notempty} + + {volist name="js_list" id="vo"} + + {/volist} + + {// 额外JS代码 } + {$extra_js|raw|default=''} +{/block} \ No newline at end of file diff --git a/application/common/builder/table/Builder.php b/application/common/builder/table/Builder.php new file mode 100644 index 0000000..1d95e6b --- /dev/null +++ b/application/common/builder/table/Builder.php @@ -0,0 +1,2558 @@ + + */ +class Builder extends ZBuilder +{ + /** + * @var string 当前模型名称 + */ + private $_module = ''; + + /** + * @var string 当前控制器名称 + */ + private $_controller = ''; + + /** + * @var string 当前操作名称 + */ + private $_action = ''; + + /** + * @var string 数据表名 + */ + private $_table_name = ''; + + /** + * @var string 插件名称 + */ + private $_plugin_name = ''; + + /** + * @var string 模板路径 + */ + private $_template = ''; + + /** + * @var array 要替换的右侧按钮内容 + */ + private $_replace_right_buttons = []; + + /** + * @var bool 有分页数据 + */ + private $_has_pages = true; + + /** + * @var array 存储字段筛选选项 + */ + private $_filter_options = []; + + /*** + * @var array 存储字段筛选列表 + */ + private $_filter_list = []; + + /** + * @var array 存储字段筛选类型 + */ + private $_filter_type = []; + + /** + * @var array 列名 + */ + private $_field_name = []; + + /** + * @var array 存储搜索框数据 + */ + private $_search = []; + + /** + * @var array 顶部下拉菜单默认选项集合 + */ + private $_select_list_default = []; + + /** + * @var array 行class + */ + private $_tr_class = []; + + /** + * @var int 前缀模式:0-不含表前缀,1-含表前缀,2-使用模型 + */ + private $_prefix = 1; + + /** + * @var mixed 表格原始数据 + */ + private $data; + + /** + * @var array 使用原始数据的字段 + */ + protected $rawField = []; + + /** + * @var array 模板变量 + */ + private $_vars = [ + 'page_title' => '', // 页面标题 + 'page_tips' => '', // 页面提示 + 'tips_type' => '', // 提示类型 + 'tab_nav' => [], // 页面Tab导航 + 'hide_checkbox' => false, // 是否隐藏第一列多选 + 'extra_html' => '', // 额外HTML代码 + 'extra_js' => '', // 额外JS代码 + 'extra_css' => '', // 额外CSS代码 + 'order_columns' => [], // 需要排序的列表头 + 'filter_columns' => [], // 需要筛选功能的列表头 + 'filter_map' => [], // 字段筛选的排序条件 + '_field_display' => [], // 字段筛选的默认选项 + '_filter_content' => [], // 字段筛选的默认选中值 + '_filter' => [], // 字段筛选的默认字段名 + 'top_buttons' => [], // 顶部栏按钮 + 'right_buttons' => [], // 表格右侧按钮 + 'search' => [], // 搜索参数 + 'search_button' => false, // 搜索按钮 + 'columns' => [], // 表格列集合 + 'pages' => '', // 分页数据 + 'row_list' => [], // 表格数据列表 + '_page_info' => '', // 分页信息 + 'primary_key' => 'id', // 表格主键名称 + '_table' => '', // 表名 + 'js_list' => [], // js文件名 + 'css_list' => [], // css文件名 + 'validate' => '', // 快速编辑的验证器名 + '_js_files' => [], // js文件 + '_css_files' => [], // css文件 + '_select_list' => [], // 顶部下拉菜单列表 + '_filter_time' => [], // 时间段筛选 + 'empty_tips' => '暂无数据', // 没有数据时的提示信息 + '_search_area' => [], // 搜索区域 + '_search_area_url' => '', // 搜索区域url + '_search_area_op' => '', // 搜索区域匹配方式 + 'builder_height' => 'fixed', // 表格高度 + 'fixed_right_column' => 0, // 固定右边列数量 + 'fixed_left_column' => 0, // 固定左边列数量 + 'column_width' => [], // 列宽度 + 'column_hide' => [], // 隐藏列 + ]; + + /** + * 初始化 + * @author 蔡伟明 <314013107@qq.com> + */ + public function initialize() + { + $this->_module = $this->request->module(); + $this->_controller = parse_name($this->request->controller()); + $this->_action = $this->request->action(); + $this->_table_name = strtolower($this->_module.'_'.$this->_controller); + $this->_template = Env::get('app_path'). 'common/builder/table/layout.html'; + + // 默认加载快速编辑所需js和css + $this->_vars['_js_files'][] = 'editable_js'; + $this->_vars['_css_files'][] = 'editable_css'; + } + + /** + * 模板变量赋值 + * @param mixed $name 要显示的模板变量 + * @param string $value 变量的值 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function assign($name, $value = '') + { + if (is_array($name)) { + $this->_vars = array_merge($this->_vars, $name); + } else { + $this->_vars[$name] = $value; + } + return $this; + } + + /** + * 设置页面标题 + * @param string $page_title 页面标题 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setPageTitle($page_title = '') + { + if ($page_title != '') { + $this->_vars['page_title'] = $page_title; + } + return $this; + } + + /** + * 隐藏第一列多选框 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function hideCheckbox($status = true) + { + $this->_vars['hide_checkbox'] = $status; + return $this; + } + + /** + * 设置页面提示 + * @param string $tips 提示信息 + * @param string $type 提示类型:success/info/warning/danger,默认info + * @param string $pos 提示位置:top,button + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setPageTips($tips = '', $type = 'info', $pos = 'top') + { + if ($tips != '') { + $this->_vars['page_tips_'.$pos] = $tips; + $this->_vars['tips_type'] = $type != '' ? trim($type) : 'info'; + } + return $this; + } + + /** + * 添加顶部下拉框 + * @param string $name 表单名,即name值 + * @param string $title 第一个下来菜单项标题,不写则不显示 + * @param array $options 表单项内容,传递数组形式,如:array([2015] => '2015年', [2016] => '2016年') + * @param string $default 默认选项,初始化时,默认选中的菜单项 + * @param string $ignore 生成url时,需要忽略的参数,用于有父子关系的下拉菜单,比如省份和地区,省份URL不应该带有地区参数的, + * 所以可以在定义省份下拉菜单时,传入地区的下拉列表名, + * 如需忽略多个参数,用逗号隔开 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addTopSelect($name = '', $title = '', $options = [], $default = '', $ignore = '') + { + if ($name != '') { + $this->_vars['_select_list'][$name] = [ + 'name' => $name, + 'title' => $title, + 'options' => $options, + 'ignore' => $ignore, + 'current' => '', + ]; + if ($default != '') { + $this->_select_list_default[$name] = $default; + } + $this->_vars['_js_files'][] = 'select2_js'; + $this->_vars['_css_files'][] = 'select2_css'; + $this->_vars['_js_init'][] = 'select2'; + } + return $this; + } + + /** + * 添加表头排序 + * @param array|string $column 表头排序字段,多个以逗号隔开 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addOrder($column = []) + { + if (!empty($column)) { + $column = is_array($column) ? $column : explode(',', $column); + $this->_vars['order_columns'] = array_merge($this->_vars['order_columns'], $column); + } + return $this; + } + + /** + * 添加表头筛选 + * @param array|string $columns 表头筛选字段,多个以逗号隔开 + * @param array $options 选项,供有些字段值需要另外显示的,比如字段值是数字,但显示的时候是其他文字。 + * @param array $default 默认选项,['字段名' => '字段值,字段值...'] + * @param string $type 筛选类型,默认为CheckBox,也可以是radio + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addFilter($columns = [], $options = [], $default = [], $type = 'checkbox') + { + if (!empty($columns)) { + $columns = is_array($columns) ? $columns : explode(',', $columns); + $this->_vars['filter_columns'] = array_merge($this->_vars['filter_columns'], $columns); + // 存储对应的字段选项 + if (!empty($options) && is_array($options)) { + foreach ($columns as $key => $column) { + if (is_numeric($key)) { + if (strpos($column, '.')) { + $column = explode('.', $column)[1]; + } + cache('filter_options_'.$column, $options); + $this->_filter_options[$column] = 'filter_options_'.$column; + } else { + cache('filter_options_'.$key, $options); + $this->_filter_options[$key] = 'filter_options_'.$key; + } + } + } + // 处理默认选项和值 + if (!empty($default) && is_array($default)) { + foreach ($default as $display => $content) { + if (strpos($display, '|')) { + list($display, $filter) = explode('|', $display); + } else { + $filter = $display; + } + if (strpos($display, '.')) { + $display = explode('.', $display)[1]; + } + $this->_vars['_field_display'][] = $display; + $this->_vars['_filter'][] = $filter; + $this->_vars['_filter_content'][] = is_array($content) ? implode(',', $content) : $content; + } + } + // 处理筛选类型 + foreach ($columns as $column) { + $this->_filter_type[$column] = $type; + } + } + return $this; + } + + /** + * 添加表头筛选列表 + * @param string $field 表头筛选字段 + * @param array $list 需要显示的列表 + * @param string $default 默认值,一维数组或逗号隔开的字符串 + * @param string $type 筛选类型,默认为CheckBox,也可以是radio + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addFilterList($field = '', $list = [], $default = '', $type = 'checkbox') + { + if ($field != '' && !empty($list)) { + $this->_vars['filter_columns'][] = $field; + $this->_filter_type[$field] = $type; + $this->_filter_list[$field] = md5('_filter_list_'.$this->_module.'_'.$this->_controller.'_'.$this->_action.'_'.session('user_auth.uid').'_'.$field); + Cache::set($this->_filter_list[$field], $list); + + // 处理默认选项和值 + if ($default != '') { + $this->_vars['_field_display'][] = $field; + $this->_vars['_filter'][] = $field; + $this->_vars['_filter_content'][] = is_array($default) ? implode(',', $default) : $default; + } + } + return $this; + } + + /** + * 添加表头筛选条件 + * @param string $fields 字段名,多个可以用逗号隔开 + * @param array $map 查询条件 + * @author caiweiming <314013107@qq.com> + * @return $this + */ + public function addFilterMap($fields = '', $map = []) + { + if ($fields != '') { + if (is_array($fields)) { + $this->_vars['filter_map'] = array_merge($this->_vars['filter_map'], $fields); + } else { + $map = $this->buildFilterMap($map); + if (strpos($fields, ',')) { + $fields = explode(',', $fields); + foreach ($fields as $field) { + if (isset($this->_vars['filter_map'][$field])) { + $this->_vars['filter_map'][$field] = array_merge($this->_vars['filter_map'][$field], $map); + } else { + $this->_vars['filter_map'][$field] = $map; + } + } + } else { + if (isset($this->_vars['filter_map'][$fields])) { + $this->_vars['filter_map'][$fields] = array_merge($this->_vars['filter_map'][$fields], $map); + } else { + $this->_vars['filter_map'][$fields] = $map; + } + } + } + } + return $this; + } + + /** + * 组合筛选条件 + * @param string $map 筛选条件 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + private function buildFilterMap($map = '') + { + if (is_array($map)) return $map; + + $_map = []; + $_filter = $this->request->param('_filter'); + $_filter = explode('|', $_filter); + $_pos = array_search($map, $_filter); + if ($_pos !== false) { + $_filter_content = $this->request->param('_filter_content'); + $_filter_content = explode('|', $_filter_content); + + if (strpos($map, '.')) { + $_field = explode('.', $map)[1]; + } else { + $_field = $map; + } + + $_map[] = isset($_filter_content[$_pos]) ? [$_field, 'in', $_filter_content[$_pos]] : [$_field, 'eq', '']; + } + + return $_map; + } + + /** + * 时间段过滤 + * @param string $field 字段名 + * @param string|array $date 默认的开始日期和结束日期 + * @param string|array $tips 开始日期和结束日期的提示 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addTimeFilter($field = '', $date = '', $tips = '') + { + if ($field != '') { + $date_start = ''; + $date_end = ''; + $tips_start = '开始日期'; + $tips_end = '结束日期'; + + if (!empty($date)) { + if (!is_array($date)) { + if (strpos($date, ',')) { + list($date_start, $date_end) = explode(',', $date); + } else { + $date_start = $date_end = $date; + } + } else { + list($date_start, $date_end) = $date; + } + } + + if (!empty($tips)) { + if (!is_array($tips)) { + if (strpos($tips, ',')) { + list($tips_start, $tips_end) = explode(',', $tips); + } else { + $tips_start = $tips_end = $tips; + } + } else { + list($tips_start, $tips_end) = $tips; + } + } + + $this->_vars['_js_files'][] = 'datepicker_js'; + $this->_vars['_css_files'][] = 'datepicker_css'; + $this->_vars['_js_init'][] = 'datepicker'; + $this->_vars['_filter_time'] = [ + 'field' => $field, + 'tips_start' => $tips_start, + 'tips_end' => $tips_end, + 'date_start' => $date_start, + 'date_end' => $date_end, + ]; + } + return $this; + } + + /** + * 添加快捷编辑的验证器 + * @param string $validate 验证器名 + * @param string $fields 要验证的字段,多个用逗号隔开,并且在验证器中要定义该字段名对应的场景 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addValidate($validate = '', $fields = '') + { + if ($validate != '') { + $this->_vars['validate'] = $validate; + $this->_vars['validate_fields'] = $fields; + } + return $this; + } + + /** + * 替换右侧按钮 + * @param array $map 条件,格式为:['字段名' => '字段值', '字段名' => '字段值'....] + * @param string $content 要替换的内容 + * @param null $target 要替换的目标按钮 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function replaceRightButton($map = [], $content = '', $target = null) + { + if (!empty($map)) { + $maps = []; + $target = is_string($target) ? explode(',', $target) : $target; + if (is_callable($map)) { + $maps[] = [$map, $content, $target]; + } else { + foreach ($map as $key => $value) { + if (is_array($value)) { + $op = strtolower($value[0]); + switch ($op) { + case '=': $op = 'eq'; break; + case '<>': $op = 'neq'; break; + case '>': $op = 'gt'; break; + case '<': $op = 'lt'; break; + case '>=': $op = 'egt'; break; + case '<=': $op = 'elt'; break; + case 'in': + case 'not in': + case 'between': + case 'not between': + $value[1] = is_array($value[1]) ? $value[1] : explode(',', $value[1]); break; + } + $maps[] = [$key, $op, $value[1]]; + } else { + $maps[] = [$key, 'eq', $value]; + } + } + } + + $this->_replace_right_buttons[] = [ + 'maps' => $maps, + 'content' => $content, + 'target' => $target + ]; + } + return $this; + } + + /** + * 自动创建新增页面 + * @param array $items 表单项 + * @param string $table 表名 + * @param string $validate 验证器名 + * @param string $auto_time 自动添加时间,默认有两个create_time和update_time + * @param string $format 时间格式 + * @param bool $pop 弹窗显示 + * @author caiweiming <314013107@qq.com> + * @return $this + */ + public function autoAdd($items = [], $table = '', $validate = '', $auto_time = '', $format = '', $pop = false) + { + if (!empty($items)) { + // 默认属性 + $btn_attribute = [ + 'title' => '新增', + 'icon' => 'fa fa-plus-circle', + 'class' => 'btn btn-primary'.($pop === true ? ' pop' : ''), + 'href' => url( + $this->_module.'/'.$this->_controller.'/add' + ).($pop === true ? '?_pop=1' : ''), + ]; + + // 判断当前用户是否有权限,没有权限则不生成按钮 + if (session('user_auth.role') != 1 && substr($btn_attribute['href'], 0, 4) != 'http' && $btn_attribute['href'] != 'javascript:history.back(-1);') { + if ($this->checkButtonAuth($btn_attribute) === false) { + return $this; + } + } + + // 缓存名称 + $cache_name = strtolower($this->_module.'/'.$this->_controller.'/add'); + + // 自动插入时间 + if ($auto_time != '') { + $auto_time = $auto_time === true ? ['create_time', 'update_time'] : explode(',', $auto_time); + } + + // 表单缓存数据 + $form = [ + 'items' => $items, + 'table' => $table == '' ? strtolower($this->_module . '_' . $this->_controller) : $table, + 'validate' => $validate === true ? ucfirst($this->_controller) : $validate, + 'auto_time' => $auto_time, + 'format' => $format, + 'go_back' => $this->request->server('REQUEST_URI') + ]; + + // 开发模式 + if (config('develop_mode')) { + Cache::set($cache_name, $form); + } + + if (!Cache::get($cache_name)) { + Cache::set($cache_name, $form); + } + + // 添加到按钮组 + $this->_vars['top_buttons'][] = $btn_attribute; + } + return $this; + } + + /** + * 获取默认url + * @param string $type 按钮类型:add/enable/disable/delete + * @param array $params 参数 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + private function getDefaultUrl($type = '', $params = []) + { + $url = $this->_module.'/'.$this->_controller.'/'.$type; + $MenuModel = new Menu(); + $menu = $MenuModel->where('url_value', $url)->find(); + if ($menu['params'] != '') { + $url_params = explode('&', trim($menu['params'], '&')); + if (!empty($url_params)) { + foreach ($url_params as $item) { + list($key, $value) = explode('=', $item); + $params[$key] = $value; + } + } + } + + if (!empty($params) && config('url_common_param')) { + $params = array_filter($params, function($v){return $v !== '';}); + } + + return $menu['url_type'] == 'module_home' ? home_url($url, $params) : url($url, $params); + } + + /** + * 添加一个顶部按钮 + * @param string $type 按钮类型:add/enable/disable/back/delete/custom + * @param array $attribute 按钮属性 + * @param bool $pop 是否使用弹出框形式 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addTopButton($type = '', $attribute = [], $pop = false) + { + if ($type == '') { + return $this; + } + + // 表单名,用于替换 + if (isset($attribute['table'])) { + if (isset($attribute['prefix'])) { + $table_token = $this->createTableToken($attribute['table'], $attribute['prefix']); + } else { + $table_token = $this->createTableToken($attribute['table'], $this->_prefix); + } + } else { + $table_token = '__table__'; + } + + // 自定义字段 + $field = isset($attribute['field']) ? $attribute['field'] : ''; + + // 这个专门为插件准备的属性,是插件名称 + $plugin_name = isset($attribute['plugin_name']) ? $attribute['plugin_name'] : $this->_plugin_name; + + switch ($type) { + // 新增按钮 + case 'add': + // 默认属性 + $btn_attribute = [ + 'title' => '新增', + 'icon' => 'fa fa-plus-circle', + 'class' => 'btn btn-primary', + 'href' => $this->getDefaultUrl($type, ['plugin_name' => $plugin_name]) + ]; + break; + + // 启用按钮 + case 'enable': + // 默认属性 + $btn_attribute = [ + 'title' => '启用', + 'icon' => 'fa fa-check-circle-o', + 'class' => 'btn btn-success ajax-post confirm', + 'target-form' => 'ids', + 'href' => $this->getDefaultUrl($type, ['_t' => $table_token, 'field' => $field]) + ]; + break; + + // 禁用按钮 + case 'disable': + // 默认属性 + $btn_attribute = [ + 'title' => '禁用', + 'icon' => 'fa fa-ban', + 'class' => 'btn btn-warning ajax-post confirm', + 'target-form' => 'ids', + 'href' => $this->getDefaultUrl($type, ['_t' => $table_token, 'field' => $field]) + ]; + break; + + // 返回按钮 + case 'back': + // 默认属性 + $btn_attribute = [ + 'title' => '返回', + 'icon' => 'fa fa-reply', + 'class' => 'btn btn-info', + 'href' => 'javascript:history.back(-1);' + ]; + break; + + // 删除按钮(不可恢复) + case 'delete': + // 默认属性 + $btn_attribute = [ + 'title' => '删除', + 'icon' => 'fa fa-times-circle-o', + 'class' => 'btn btn-danger ajax-post confirm', + 'target-form' => 'ids', + 'href' => $this->getDefaultUrl($type, ['_t' => $table_token]) + ]; + break; + + // 自定义按钮 + default: + // 默认属性 + $btn_attribute = [ + 'title' => '定义按钮', + 'class' => 'btn btn-default', + 'target-form' => 'ids', + 'href' => 'javascript:void(0);' + ]; + break; + } + + // 合并自定义属性 + if ($attribute && is_array($attribute)) { + $btn_attribute = array_merge($btn_attribute, $attribute); + } + + // 判断当前用户是否有权限,没有权限则不生成按钮 + if (session('user_auth.role') != 1 && substr($btn_attribute['href'], 0, 4) != 'http' && $btn_attribute['href'] != 'javascript:history.back(-1);') { + if ($this->checkButtonAuth($btn_attribute) === false) { + return $this; + } + } + + // 是否为弹出框方式 + if ($pop !== false) { + $btn_attribute['class'] .= ' pop'; + $btn_attribute['href'] .= (strpos($btn_attribute['href'], '?') ? '&' : '?').'_pop=1'; + if (is_array($pop) && !empty($pop)) { + $btn_attribute['data-layer'] = json_encode($pop); + } + } + + $this->_vars['top_buttons'][] = $btn_attribute; + return $this; + } + + /** + * 检查是否有按钮权限 + * @param array $btn_attribute 按钮属性 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + private function checkButtonAuth($btn_attribute = []) + { + if (preg_match('/\/(index.php|'.ADMIN_FILE.')\/(.*)/', $btn_attribute['href'], $match)) { + $url_value = explode('/', $match[2]); + if (strpos($url_value[2], '.')) { + $url_value[2] = substr($url_value[2], 0, strpos($url_value[2], '.')); + } + $url_value = $url_value[0].'/'.$url_value[1].'/'.$url_value[2]; + $url_value = strtolower($url_value); + return Role::checkAuth($url_value, true); + } + return true; + } + + /** + * 一次性添加多个顶部按钮 + * @param array|string $buttons 按钮类型 + * 例如: + * $builder->addTopButtons('add'); + * $builder->addTopButtons('add,delete'); + * $builder->addTopButtons(['add', 'delete']); + * $builder->addTopButtons(['add' => ['table' => '__USER__'], 'delete']); + * + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addTopButtons($buttons = []) + { + if (!empty($buttons)) { + $buttons = is_array($buttons) ? $buttons : explode(',', $buttons); + foreach ($buttons as $key => $value) { + if (is_numeric($key)) { + $this->addTopButton($value); + } else { + $this->addTopButton($key, $value); + } + } + } + return $this; + } + + /** + * 自动创建编辑页面 + * @param array $items 表单项 + * @param string $table 表名 + * @param string $validate 验证器名 + * @param string $auto_time 自动添加时间,默认有两个create_time和update_time + * @param string $format 时间格式 + * @param bool $pop 弹窗显示 + * @param array $extra 额外参数,设置按钮样式 + * @author caiweiming <314013107@qq.com> + * @return $this + */ + public function autoEdit($items = [], $table = '', $validate = '', $auto_time = '', $format = '', $pop = false, $extra = []) + { + if (!empty($items)) { + // 按钮样式 + $btn_style = array_merge(config('zbuilder.right_button'), $extra); + + // 默认属性 + $btn_attribute = [ + 'title' => '编辑', + 'icon' => 'fa fa-pencil', + 'class' => 'btn btn-'.$btn_style['size'].' btn-'.$btn_style['style'].($pop === true ? ' pop' : ''), + 'href' => url( + $this->_module.'/'.$this->_controller.'/edit', + ['id' => '__id__'] + ), + 'target' => '_self', + '_style' => $btn_style + ]; + + // 是否弹窗显示 + if ($pop === true) { + $btn_attribute['href'] .= (strpos($btn_attribute['href'], '?') ? '&' : '?').'_pop=1'; + } + + // 判断当前用户是否有权限,没有权限则不生成按钮 + if (session('user_auth.role') != 1 && substr($btn_attribute['href'], 0, 4) != 'http') { + if ($this->checkButtonAuth($btn_attribute) === false) { + return $this; + } + } + + // 缓存名称 + $cache_name = strtolower($this->_module.'/'.$this->_controller.'/edit'); + + // 自动插入时间 + if ($auto_time != '') { + $auto_time = $auto_time === true ? ['update_time'] : explode(',', $auto_time); + } + + // 表单缓存数据 + $form = [ + 'items' => $items, + 'table' => $table == '' ? strtolower($this->_module . '_' . $this->_controller) : $table, + 'validate' => $validate === true ? ucfirst($this->_controller) : $validate, + 'auto_time' => $auto_time, + 'format' => $format, + 'go_back' => $this->request->server('REQUEST_URI') + ]; + + // 开发模式 + if (config('develop_mode')) { + Cache::set($cache_name, $form); + } + + if (!Cache::get($cache_name)) { + Cache::set($cache_name, $form); + } + + // 添加到按钮组 + $this->_vars['right_buttons'][] = $btn_attribute; + } + return $this; + } + + /** + * 创建表名Token + * @param string $table 表名 + * @param int $prefix 前缀类型:0使用Db类(不添加表前缀),1使用Db类(添加表前缀),2使用模型 + * @author 蔡伟明 <314013107@qq.com> + * @return bool|string + */ + private function createTableToken($table = '', $prefix = 1) + { + $data = [ + 'table' => $table, // 表名或模型名 + 'prefix' => $prefix, + 'module' => $this->_module, + 'controller' => $this->_controller, + 'action' => $this->_action, + ]; + + $table_token = substr(sha1($this->_module.'-'.$this->_controller.'-'.$this->_action.'-'.$table), 0, 8); + session($table_token, $data); + return $table_token; + } + + /** + * 添加一个右侧按钮 + * @param string $type 按钮类型:edit/enable/disable/delete/custom + * @param array $attribute 按钮属性 + * @param bool $pop 是否使用弹出框形式 + * @param array $extra 扩展参数,设置按钮样式 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addRightButton($type = '', $attribute = [], $pop = false, $extra = []) + { + if ($type == '') { + return $this; + } + + // 表单名,用于替换 + if (isset($attribute['table'])) { + if (isset($attribute['prefix'])) { + $table_token = $this->createTableToken($attribute['table'], $attribute['prefix']); + } else { + $table_token = $this->createTableToken($attribute['table'], $this->_prefix); + } + } else { + $table_token = '__table__'; + } + + // 这个专门为插件准备的属性,是插件名称 + $plugin_name = isset($attribute['plugin_name']) ? $attribute['plugin_name'] : $this->_plugin_name; + // 自定义字段名 + $field = isset($attribute['field']) ? $attribute['field'] : ''; + + // 按钮样式 + $btn_style = array_merge(config('zbuilder.right_button'), $extra); + + switch ($type) { + // 编辑按钮 + case 'edit': + // 默认属性 + $btn_attribute = [ + 'title' => '编辑', + 'icon' => 'fa fa-pencil', + 'class' => 'btn btn-'.$btn_style['size'].' btn-'.$btn_style['style'], + 'href' => $this->getDefaultUrl($type, ['id' => '__id__', 'plugin_name' => $plugin_name]), + 'target' => '_self' + ]; + break; + + // 启用按钮 + case 'enable': + // 默认属性 + $btn_attribute = [ + 'title' => '启用', + 'icon' => 'fa fa-check', + 'class' => 'btn btn-'.$btn_style['size'].' btn-'.$btn_style['style'].' ajax-get confirm', + 'href' => $this->getDefaultUrl($type, ['ids' => '__id__', '_t' => $table_token, 'field' => $field]) + ]; + break; + + // 禁用按钮 + case 'disable': + // 默认属性 + $btn_attribute = [ + 'title' => '禁用', + 'icon' => 'fa fa-ban', + 'class' => 'btn btn-'.$btn_style['size'].' btn-'.$btn_style['style'].' ajax-get confirm', + 'href' => $this->getDefaultUrl($type, ['ids' => '__id__', '_t' => $table_token, 'field' => $field]) + ]; + break; + + // 删除按钮(不可恢复) + case 'delete': + // 默认属性 + $btn_attribute = [ + 'title' => '删除', + 'icon' => 'fa fa-times', + 'class' => 'btn btn-'.$btn_style['size'].' btn-'.$btn_style['style'].' ajax-get confirm', + 'href' => $this->getDefaultUrl($type, ['ids' => '__id__', '_t' => $table_token]) + ]; + break; + + // 自定义按钮 + default: + // 默认属性 + $btn_attribute = [ + 'title' => '自定义按钮', + 'icon' => 'fa fa-smile-o', + 'class' => 'btn btn-'.$btn_style['size'].' btn-'.$btn_style['style'], + 'href' => 'javascript:void(0);' + ]; + break; + } + + // 合并自定义属性 + if ($attribute && is_array($attribute)) { + $btn_attribute = array_merge($btn_attribute, $attribute); + } + + // 判断当前用户是否有权限,没有权限则不生成按钮 + if (session('user_auth.role') != 1 && substr($btn_attribute['href'], 0, 4) != 'http') { + if ($this->checkButtonAuth($btn_attribute) === false) { + return $this; + } + } + + // 是否为弹出框方式 + if ($pop !== false) { + $btn_attribute['class'] .= ' pop'; + $btn_attribute['href'] .= (strpos($btn_attribute['href'], '?') ? '&' : '?').'_pop=1'; + if (is_array($pop) && !empty($pop)) { + $btn_attribute['data-layer'] = json_encode($pop); + } + } + + // 添加按钮样式 + $btn_attribute['_style'] = $btn_style; + + // 添加按钮标签 + $btn_attribute['_tag'] = $type; + + $this->_vars['right_buttons'][] = $btn_attribute; + return $this; + } + + /** + * 一次性添加多个右侧按钮 + * @param array|string $buttons 按钮类型 + * 例如: + * $builder->addRightButtons('edit'); + * $builder->addRightButtons('edit,delete'); + * $builder->addRightButtons(['edit', 'delete']); + * $builder->addRightButtons(['edit' => ['table' => 'admin_user'], 'delete']); + * + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addRightButtons($buttons = []) + { + if (!empty($buttons)) { + $buttons = is_array($buttons) ? $buttons : explode(',', $buttons); + foreach ($buttons as $key => $value) { + if (is_numeric($key)) { + $this->addRightButton($value); + } else { + $this->addRightButton($key, $value); + } + } + } + return $this; + } + + /** + * 设置表格高度 + * @param string $height 高度:fixed/auto/具体数值 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + * @since 1.3.0 + */ + public function setHeight($height = 'fixed') + { + $this->_vars['builder_height'] = $height; + return $this; + } + + /** + * 固定右侧列数 + * @param int $num 数量 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function fixedRight($num = 0) + { + $this->_vars['fixed_right_column'] = $num; + return $this; + } + + /** + * 固定左侧列数 + * @param int $num 数量 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function fixedLeft($num = 0) + { + $this->_vars['fixed_left_column'] = $num; + return $this; + } + + /** + * 设置搜索参数 + * @param array $fields 参与搜索的字段 + * @param string $placeholder 提示符 + * @param string $url 提交地址 + * @param null $search_button 提交按钮 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setSearch($fields = [], $placeholder = '', $url = '', $search_button = null) + { + if (!empty($fields)) { + $this->_search = [ + 'fields' => is_string($fields) ? explode(',', $fields) : $fields, + 'placeholder' => $placeholder, + 'url' => $url, + ]; + + $this->_vars['search_button'] = $search_button !== null ? $search_button : config('zbuilder.search_button'); + } + return $this; + } + + /** + * 设置搜索区域 + * @param array $items + * @param string $url + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setSearchArea($items = [], $url = '') + { + if (!empty($items)) { + $_op = []; + $_defaults = []; + $_s = $this->request->param('_s', ''); + if ($_s != '') { + $_s = explode('|', $_s); + foreach ($_s as $v) { + list($field, $value) = explode('=', $v); + $_defaults[$field] = $value; + } + } + + foreach ($items as &$item) { + $layout = 3; + + if (strpos($item[0], ':')) { + list($item[0], $layout) = explode(':', $item[0]); + } + + $type = $item[0]; + $name = $item[1]; + $label = $item[2]; + $op = isset($item[3]) ? $item[3] : 'eq'; + $item[4] = isset($_defaults[$name]) ? $_defaults[$name] : (isset($item[4]) ? $item[4] : ''); // 默认值 + $item[5] = isset($item[5]) ? $item[5] : []; + + switch ($op) { + case '=': $op = 'eq'; break; + case '<>': $op = 'neq'; break; + case '>': $op = 'gt'; break; + case '<': $op = 'lt'; break; + case '>=': $op = 'egt'; break; + case '<=': $op = 'elt'; break; + default: + $op = $op == '' ? 'eq' : $op; + } + + switch ($type) { + case 'text': + break; + case 'select': + $this->_vars['_js_files'][] = 'select2_js'; + $this->_vars['_css_files'][] = 'select2_css'; + $this->_vars['_js_init'][] = 'select2'; + break; + case 'daterange': + $this->_vars['_js_files'][] = 'moment_js'; + $this->_vars['_js_files'][] = 'daterangepicker_js'; + $this->_vars['_css_files'][] = 'daterangepicker_css'; + $this->_vars['_js_init'][] = 'daterangepicker'; + $op = $op == 'eq' ? 'between time' : $op . ' time'; + + $params = []; + if (!empty($item[5])) { + foreach ($item[5] as $key => $param) { + $params[] = 'data-'.strtolower($key).'="'.$param.'"'; + } + } + $item[5] = implode(' ', $params); + break; + default: + + } + + $_op[] = $name.'='.strtolower($op); + $this->_vars['_search_area_layout'][$name] = $layout; + } + + $this->_vars['_search_area_op'] = implode('|', $_op); + $this->_vars['_search_area'] = $items; + $this->_vars['_search_area_url'] = $url == '' ? $this->request->baseUrl(true) : $url; + } + return $this; + } + + /** + * 引入模块js文件 + * @param string $files_name js文件名,多个文件用逗号隔开 + * @param string $module 指定模块 + * @author caiweiming <314013107@qq.com> + * @return $this + */ + public function js($files_name = '', $module = '') + { + if ($files_name != '') { + $this->loadFile('js', $files_name, $module); + } + return $this; + } + + /** + * 引入模块css文件 + * @param string $files_name css文件名,多个文件用逗号隔开 + * @param string $module 指定模块 + * @author caiweiming <314013107@qq.com> + * @return $this + */ + public function css($files_name = '', $module = '') + { + if ($files_name != '') { + $this->loadFile('css', $files_name, $module); + } + return $this; + } + + /** + * 引入css或js文件 + * @param string $type 类型:css/js + * @param string $files_name 文件名,多个用逗号隔开 + * @param string $module 指定模块 + * @author caiweiming <314013107@qq.com> + */ + private function loadFile($type = '', $files_name = '', $module = '') + { + if ($files_name != '') { + $module = $module == '' ? $this->_module : $module; + if (!is_array($files_name)) { + $files_name = explode(',', $files_name); + } + foreach ($files_name as $item) { + if (strpos($item, '/')) { + $this->_vars[$type.'_list'][] = PUBLIC_PATH. 'static/'. $item.'.'.$type; + } else { + $this->_vars[$type.'_list'][] = PUBLIC_PATH. 'static/'. $module .'/'.$type.'/'.$item.'.'.$type; + } + } + } + } + + /** + * 设置数据库表名 + * @param string $table 数据库表名,不含前缀,如果为true则使用模型方式 + * @param int $prefix 前缀类型:0使用Db类(不添加表前缀),1使用Db类(添加表前缀),2使用模型 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTableName($table = '', $prefix = 1) + { + if ($table === true) { + $this->_prefix = 2; + $this->_table_name = strtolower($this->_module.'/'.$this->_controller); + } else { + $this->_prefix = $prefix === true ? 2 : $prefix; + + if ($this->_prefix == 2) { + $this->_table_name = strpos($table, '/') ? $table : strtolower($this->_module.'/'.$table); + } else { + $this->_table_name = $table; + } + } + return $this; + } + + /** + * 设置插件名称(此方法只供制作插件时用) + * @param string $plugin_name 插件名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setPluginName($plugin_name = '') + { + if ($plugin_name != '') { + $this->_plugin_name = $plugin_name; + } + return $this; + } + + /** + * 添加一列 + * @param string $name 字段名称 + * @param string $title 列标题 + * @param string $type 单元格类型 + * @param string $default 默认值 + * @param string $param 额外参数 + * @param string $class css类名 + * @param string $extra 扩展参数 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addColumn($name = '', $title = '', $type = '', $default = '', $param = '', $class = '', $extra = '') + { + $field = $name; + $table = ''; + + // 判断是否有字段别名 + if (strpos($name, '|')) { + list($name, $field) = explode('|', $name); + // 判断是否有表名 + if (strpos($field, '.')) { + list($table, $field) = explode('.', $field); + } + } + + $column = [ + 'name' => $name, + 'title' => $title, + 'type' => $type, + 'default' => $default, + 'param' => $param, + 'class' => $class, + 'extra' => $extra, + 'field' => $field, + 'table' => $table, + ]; + + $args = array_slice(func_get_args(), 7); + $column = array_merge($column, $args); + + $this->_vars['columns'][] = $column; + $this->_field_name[$name] = $title; + return $this; + } + + /** + * 一次性添加多列 + * @param array $columns 数据列 + * @author caiweiming <314013107@qq.com> + * @return $this + */ + public function addColumns($columns = []) + { + if (!empty($columns)) { + foreach ($columns as $column) { + call_user_func_array([$this, 'addColumn'], $column); + } + } + return $this; + } + + /** + * 设置列宽 + * @param string $column 列名,即字段名 + * @param int $width 宽度,默认为100 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setColumnWidth($column = '', $width = 100) + { + if ($column != '') { + if (is_array($column)) { + foreach ($column as $field => $width) { + $this->_vars['column_width'][$field] = $width; + } + } else { + if (strpos($column, ',')) { + $columns = explode(',', $column); + foreach ($columns as $column) { + $this->_vars['column_width'][$column] = $width; + } + } else { + $this->_vars['column_width'][$column] = $width; + } + } + } + return $this; + } + + /** + * 隐藏列 + * @param string $column 列名,即字段名 + * @param string $screen 屏幕,xs/sm/md/lg + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function hideColumn($column = '', $screen = '') + { + if ($column != '') { + if (is_array($column)) { + foreach ($column as $field => $screen) { + $screens = is_array($screen) ? $screen : explode(',', $screen); + foreach ($screens as $key => $value) { + $screens[$key] = 'hidden-'.$value; + } + $screens = implode(' ', $screens); + + $this->_vars['column_hide'][$field] = $screens; + } + } else { + $screens = is_array($screen) ? $screen : explode(',', $screen); + foreach ($screens as &$screen) { + $screen = 'hidden-'.$screen; + } + $screens = implode(' ', $screens); + + if (strpos($column, ',')) { + $columns = explode(',', $column); + foreach ($columns as $column) { + $this->_vars['column_hide'][$column] = isset($this->_vars['column_hide'][$column]) ? + $this->_vars['column_hide'][$column]. ' ' . $screen : + $screens; + } + } else { + $this->_vars['column_hide'][$column] = isset($this->_vars['column_hide'][$column]) ? + $this->_vars['column_hide'][$column]. ' ' . $screen : + $screens; + } + } + } + return $this; + } + + /** + * 设置表格数据列表 + * @param array|object $row_list 表格数据 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setRowList($row_list = null) + { + if ($row_list !== null) { + // 原始表格数据 + $this->data = $row_list; + // 转为数组后的表格数据 + $this->_vars['row_list'] = $this->toArray($row_list); + if ($row_list instanceof \think\paginator) { + $this->_vars['_page_info'] = $row_list; + // 设置分页 + $this->setPages($row_list->render()); + } + } + if (empty($this->_vars['row_list'])) { + $params = $this->request->param(); + if (isset($params['page'])) { + unset($params['page']); + $url = url($this->_module.'/'.$this->_controller.'/'.$this->_action).'?'.http_build_query($params); + $this->redirect($url); + } + } + return $this; + } + + /** + * 将表格数据转换为纯数组 + * @param array|object $row_list 数据 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + private function toArray($row_list) + { + if ($row_list instanceof \think\paginator) { + return $row_list->toArray()['data']; + } elseif ($row_list instanceof \think\model\Collection) { + return $row_list->toArray(); + } else { + return $row_list; + } + } + + /** + * 获取原始数据 + * @param string $index 索引 + * @param string $field 字段名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + private function getData($index = '', $field = '') + { + if (is_object($this->data) && is_object(current($this->data->getIterator()))) { + try { + $result = $this->data[$index]->getData($field); + } catch (\Exception $e) { + $result = isset($this->data[$index][$field]) ? $this->data[$index][$field] : ''; + } + return $result; + } else { + return isset($this->data[$index][$field]) ? $this->data[$index][$field] : ''; + } + } + + /** + * 设置需要使用原始数据的字段 + * @param string|array $field 字段名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function raw($field = '') + { + if (is_array($field)) { + $this->rawField = array_merge($this->rawField, $field); + } else { + $this->rawField = array_merge($this->rawField, explode(',', $field)); + } + return $this; + } + + /** + * 设置表格主键 + * @param string $key 主键名称 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setPrimaryKey($key = '') + { + if ($key != '') { + $this->_vars['primary_key'] = $key; + } + return $this; + } + + /** + * 设置Tab按钮列表 + * @param array $tab_list Tab列表 ['title' => '标题', 'href' => 'http://www.dolphinphp.com'] + * @param string $curr_tab 当前tab + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTabNav($tab_list = [], $curr_tab = '') + { + if (!empty($tab_list)) { + $this->_vars['tab_nav'] = [ + 'tab_list' => $tab_list, + 'curr_tab' => $curr_tab, + ]; + } + return $this; + } + + /** + * 设置分页 + * @param string $pages 分页数据 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setPages($pages = '') + { + if ($pages != '') { + $this->_vars['pages'] = $pages; + } + return $this; + } + + /** + * 设置为无分页 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function noPages() + { + $this->_has_pages = false; + return $this; + } + + /** + * 设置额外代码 + * @param string $extra_html 额外代码 + * @param string $tag 标记 + * @author 蔡伟明 <314013107@qq.com> + * @alter 小乌 <82950492@qq.com> + * @return $this + */ + public function setExtraHtml($extra_html = '', $tag = '') + { + if ($extra_html != '') { + $tag != '' && $tag = '_'.$tag; + $this->_vars['extra_html'.$tag] = $extra_html; + } + return $this; + } + + /** + * 通过文件设置额外代码 + * @param string $template 模板文件名 + * @param string $tag 标记 + * @param array $vars 模板输出变量 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setExtraHtmlFile($template = '', $tag = '', $vars = []) + { + $template = $template == '' ? $this->_action : $template; + $file = Env::get('app_path'). $this->_module.'/view/admin/'.$this->_controller.'/'.$template.'.html'; + if (file_exists($file)) { + $content = file_get_contents($file); + $content = $this->view->display($content, $vars); + } else { + $content = '模板文件不存在:'.$file; + } + + $tag != '' && $tag = '_'.$tag; + $this->_vars['extra_html'.$tag] = $content; + + return $this; + } + + /** + * 设置额外JS代码 + * @param string $extra_js 额外JS代码 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setExtraJs($extra_js = '') + { + if ($extra_js != '') { + $this->_vars['extra_js'] = $extra_js; + } + return $this; + } + + /** + * 设置额外CSS代码 + * @param string $extra_css 额外CSS代码 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setExtraCss($extra_css = '') + { + if ($extra_css != '') { + $this->_vars['extra_css'] = $extra_css; + } + return $this; + } + + /** + * 设置页面模版 + * @param string $template 模版 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTemplate($template = '') + { + if ($template != '') { + $this->_template = $template; + } + return $this; + } + + /** + * 列class + * @param string $class class名 + * @param mixed $field 字段名 + * @param null $op 表达式 + * @param null $condition 查询条件 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addTrClass($class = '', $field = '', $op = null, $condition = null) + { + if ($class != '') { + if (is_callable($field)) { + $args = array_slice(func_get_args(), 2); + $this->_tr_class[$class][] = [$field, $args]; + } elseif (!is_null($op)) { + $op = strtolower($op); + if (is_null($condition)) { + $this->_tr_class[$class][] = [$field, 'eq', $op]; + } else { + switch ($op) { + case '=': $op = 'eq'; break; + case '<>': $op = 'neq'; break; + case '>': $op = 'gt'; break; + case '<': $op = 'lt'; break; + case '>=': $op = 'egt'; break; + case '<=': $op = 'elt'; break; + case 'in': + case 'not in': + case 'between': + case 'not between': + $condition = is_array($condition) ? $condition : explode(',', $condition); break; + } + + $this->_tr_class[$class][] = [$field, $op, $condition]; + } + } + } + + return $this; + } + + /** + * 编译HTML属性 + * @param array $attr 要编译的数据 + * @author 蔡伟明 <314013107@qq.com> + * @return array|string + */ + private function compileHtmlAttr($attr = []) { + $result = []; + if ($attr) { + foreach ($attr as $key => &$value) { + if ($key == 'title') { + $value = trim(htmlspecialchars(strip_tags(trim($value)))); + } else { + $value = htmlspecialchars($value); + } + array_push($result, "$key=\"$value\""); + } + } + return implode(' ', $result); + } + + /** + * 编译表格数据row_list的值 + * @author 蔡伟明 <314013107@qq.com> + */ + private function compileRows() + { + foreach ($this->_vars['row_list'] as $key => &$row) { + // 处理行class + if ($this->_tr_class) { + $_tr_class = $this->parseTrClass($row); + + if (!empty($_tr_class)) { + $row['_tr_class'] = implode(' ', $_tr_class); + } + } + + // 编译右侧按钮 + if ($this->_vars['right_buttons']) { + // 默认给列添加个空的右侧按钮 + if (!isset($row['right_button'])) { + $row['right_button'] = ''; + } + + foreach ($this->_vars['right_buttons'] as $index => $button) { + // 处理按钮替换 + if (!empty($this->_replace_right_buttons)) { + foreach ($this->_replace_right_buttons as $replace_right_button) { + // 是否能匹配到条件 + $_button_match = true; + foreach ($replace_right_button['maps'] as $condition) { + if (is_string($condition[0])) { + if (!isset($row[$condition[0]])) { + $_button_match = false; continue; + } + $_button_match = $this->parseCondition($row, $condition) ? $_button_match : false; + } elseif (is_callable($condition[0])) { + $_button_match = call_user_func($condition[0], $row) ? $_button_match : false; + } + } + + // 替换按钮内容支持数据变量 + if ($replace_right_button['content'] != '') { + if (preg_match_all('/__(.*?)__/', $replace_right_button['content'], $matches)) { + $replace_to = []; + $pattern = []; + foreach ($matches[1] as $match) { + $pattern[] = '/__'. $match .'__/i'; + $replace_to[] = $row[$match]; + } + $replace_right_button['content'] = preg_replace($pattern, $replace_to, $replace_right_button['content']); + } + } + + if ($_button_match) { + if ($replace_right_button['target'] === null) { + $row['right_button'] = $replace_right_button['content']; + break(2); + } else { + if (in_array($button['_tag'], $replace_right_button['target'])) { + $row['right_button'] .= $replace_right_button['content']; + continue(2); + } + } + } + } + } + + // 处理主键变量值 + $button['href'] = preg_replace( + '/__id__/i', + $row[$this->_vars['primary_key']], + $button['href'] + ); + + // 处理表名变量值 + if (strpos($button['href'], '__table__') !== false) { + $button['href'] = preg_replace( + '/__table__/i', + $this->createTableToken($this->_table_name, $this->_prefix), + $button['href'] + ); + } + + // 替换其他字段值 + if (preg_match_all('/__(.*?)__/', $button['href'], $matches)) { + // 要替换的字段名 + $replace_to = []; + $pattern = []; + foreach ($matches[1] as $match) { + $replace = in_array($match, $this->rawField) ? $this->getData($key, $match) : (isset($row[$match]) ? $row[$match] : ''); + if (isset($row[$match])) { + $pattern[] = '/__'. $match .'__/i'; + $replace_to[] = $replace; + } + } + $button['href'] = preg_replace( + $pattern, + $replace_to, + $button['href'] + ); + } + + $button_style = $button['_style']; + unset($button['_style']); + // 编译按钮属性 + $button['attribute'] = $this->compileHtmlAttr($button); + if ($button_style['title']) { + $row['right_button'] .= ''; + if ($button_style['icon']) { + $row['right_button'] .= ' '; + } + $row['right_button'] .= $button['title'].''; + } else { + $row['right_button'] .= ''; + } + } + $row['right_button'] = '
    '. $row['right_button'] .'
    '; + } + + // 编译单元格数据类型 + if ($this->_vars['columns']) { + // 另外拷贝一份主键值,以免将主键设置为快速编辑的时候解析出错 + $row['_primary_key_value'] = isset($row[$this->_vars['primary_key']]) ? $row[$this->_vars['primary_key']] : ''; + + foreach ($this->_vars['columns'] as $column) { + $_name = $column['field']; + $_table_name = $column['table']; + + // 如果需要显示编号 + if ($column['name'] == '__INDEX__') { + $row[$column['name']] = $key + 1; + } + + if (in_array($column['name'], $this->rawField)) { + $row[$column['name']] = $this->getData($key, $column['name']); + } + + // 备份原数据 + if (isset($row[$column['name']])) { + $row['__'.$column['name'].'__'] = $row[$column['name']]; + } + + switch ($column['type']) { + case 'link': // 链接 + if ($column['default'] != '') { + // 要替换的字段名 + $replace_to = []; + $pattern = []; + $url = $column['default']; + $target = $column['param'] == '' ? '_self' : $column['param']; + if (preg_match_all('/__(.*?)__/', $column['default'], $matches)) { + foreach ($matches[1] as $match) { + $pattern[] = '/__'. $match .'__/i'; + $replace_to[] = $row[$match]; + } + $url = preg_replace($pattern, $replace_to, $url); + } + + $url = $column['class'] == 'pop' ? $url.(strpos($url, '?') ? '&' : '?').'_pop=1' : $url; + + if ($column['extra'] != '') { + $title = $column['extra'] === true ? $column['title'] : $column['extra']; + } else { + $title = $row[$column['name']]; + } + + $row[$column['name'].'__'.$column['type']] = ''.$row[$column['name']].''; + } + break; + case 'switch': // 开关 + switch ($row[$column['name']]) { + case '0': // 关闭 + $row[$column['name'].'__'.$column['type']] = ''; + break; + case '1': // 开启 + $row[$column['name'].'__'.$column['type']] = ''; + break; + } + break; + case 'status': // 状态 + $status = $row[$column['name']]; + $list_status = !empty($column['param']) ? $column['param'] : ['禁用:warning', '启用:success']; + + if (isset($list_status[$status])) { + switch ($status) { + case '0': $class = 'warning';break; + case '1': $class = 'success';break; + case '2': $class = 'primary';break; + case '3': $class = 'info';break; + default: $class = 'default'; + } + if (strpos($list_status[$status], ':')) { + list($label, $class) = explode(':', $list_status[$status]); + } else { + $label = $list_status[$status]; + } + $row[$column['name'].'__'.$column['type']] = ''.$label.''; + } + break; + case 'yesno': // 是/否 + switch ($row[$column['name']]) { + case '0': // 否 + $row[$column['name'].'__'.$column['type']] = ''; + break; + case '1': // 是 + $row[$column['name'].'__'.$column['type']] = ''; + break; + } + break; + case 'text.edit': // 可编辑的单行文本 + $row[$column['name'].'__'.$column['type']] = ''.$row[$column['name']].''; + break; + case 'textarea.edit': // 可编辑的多行文本 + $row[$column['name'].'__'.$column['type']] = ''.$row[$column['name']].''; + break; + case 'password': // 密码框 + $column['param'] = $column['param'] != '' ? $column['param'] : $column['name']; + $row[$column['name'].'__'.$column['type']] = '******'; + break; + case 'email': // 邮箱地址 + case 'url': // 链接地址 + case 'tel': // 电话 + case 'number': // 数字 + case 'range': // 范围 + $column['param'] = $column['param'] != '' ? $column['param'] : $column['name']; + $row[$column['name'].'__'.$column['type']] = ''.$row[$column['name']].''; + break; + case 'icon': // 图标 + if ($row[$column['name']] === '') { + $row[$column['name'].'__'.$column['type']] = ''; + } else { + $row[$column['name'].'__'.$column['type']] = ''; + } + break; + case 'byte': // 字节 + if ($row[$column['name']] === '') { + $row[$column['name'].'__'.$column['type']] = $column['default']; + } else { + $row[$column['name'].'__'.$column['type']] = format_bytes($row[$column['name']], $column['param']); + } + break; + case 'date': // 日期 + case 'datetime': // 日期时间 + case 'time': // 时间 + // 默认格式 + $format = 'Y-m-d H:i'; + switch ($column['type']) { + case 'date': $format = 'Y-m-d';break; + case 'datetime': $format = 'Y-m-d H:i';break; + case 'time': $format = 'H:i';break; + } + // 格式 + $format = $column['param'] == '' ? $format : $column['param']; + if ($row[$column['name']] == '') { + $row[$column['name'].'__'.$column['type']] = $column['default']; + } else { + $row[$column['name'].'__'.$column['type']] = format_time($row[$column['name']], $format); + } + break; + case 'date.edit': // 可编辑日期时间,默认发送的是格式化好的 + case 'datetime.edit': // 可编辑日期时间,默认发送的是格式化好的 + case 'time.edit': // 可编辑时间,默认发送的是格式化好的 + // 默认格式 + $format = 'YYYY-MM-DD HH:mm'; + switch ($column['type']) { + case 'date.edit': $format = 'YYYY-MM-DD';break; + case 'datetime.edit': $format = 'YYYY-MM-DD HH:mm';break; + case 'time.edit': $format = 'HH:mm';break; + } + // 格式 + $format = $column['param'] == '' ? $format : $column['param']; + // 时间戳 + $timestamp = $row[$column['name']]; + $row[$column['name'].'__'.$column['type']] = ''; + if ($row[$column['name']] == '') { + $row[$column['name'].'__'.$column['type']] .= $column['default'].''; + } else { + $row[$column['name'].'__'.$column['type']] .= format_moment($timestamp, $format).''; + } + + // 加载moment.js + $this->_vars['_js_files'][] = 'moment_js'; + break; + case 'avatar': // 头像 + break; + case 'img_url': // 外链图片 + if ($row[$column['name']] != '') { + $row[$column['name'].'__'.$column['type']] = ''; + } + break; + case 'picture': // 单张图片 + $row[$column['name'].'__'.$column['type']] = ''; + break; + case 'pictures': // 多张图片 + if ($row[$column['name']] === '') { + $row[$column['name'].'__'.$column['type']] = !empty($column['default']) ? $column['default'] : '暂无图片'; + } else { + $list_img = is_array($row[$column['name']]) ? $row[$column['name']] : explode(',', $row[$column['name']]); + $imgs = ''; + } + break; + case 'files': + if ($row[$column['name']] === '') { + $row[$column['name'].'__'.$column['type']] = !empty($column['default']) ? $column['default'] : '暂无文件'; + } else { + $list_file = is_array($row[$column['name']]) ? $row[$column['name']] : explode(',', $row[$column['name']]); + $files = '
    '; + foreach ($list_file as $k => $file) { + if ($column['param'] != '' && $k == $column['param']) { + break; + } + $files .= ' ['.get_file_name($file).']'; + } + $row[$column['name'].'__'.$column['type']] = $files.'
    '; + } + break; + case 'select': // 下拉框 + if ($column['default']) { + if (isset($column['default'][$row[$column['name']]])) { + $prepend = $column['default'][$row[$column['name']]] != '' ? $column['default'][$row[$column['name']]] : '空值'; + } else { + $prepend = '无对应值'; + } + $class = ($prepend == '无对应值' || $prepend == '空值') ? 'select-edit text-danger' : 'select-edit'; + $source = json_encode($column['default'], JSON_FORCE_OBJECT); + $row[$column['name'].'__'.$column['type']] = ''.$prepend.''; + } + break; + case 'select2': // tag编辑(有BUG) +// if ($column['default']) { +// $source = json_encode($column['default']); +// $row[$column['name'].'__'.$column['type']] = ''.$row[$column['name']].''; +// } + break; + case 'callback': // 调用回调方法 + unset($column['field']); + unset($column['table']); + $params = array_slice($column, 4); + $params = array_filter($params, function($v){return $v !== '';}); + + if (isset($row[$column['name']]) || array_key_exists($column['name'], $row)) { + $params = array_merge([$row[$column['name']]], array_values($params)); + } + + if (!empty($params)) { + foreach ($params as &$param) { + if ($param === '__data__') $param = $row; + } + } + + $row[$column['name'].'__'.$column['type']] = call_user_func_array($column['default'], $params); + break; + case 'popover': + $length = empty($column['default']) ? 10 : $column['default']; + $placement = empty($column['param']) ? 'top' : $column['param']; + $row[$column['name'].'__'.$column['type']] = mb_substr($row[$column['name']], 0, $length, 'utf-8').'... '; + break; + case 'text': + default: // 默认 + // 设置默认值 + if (!isset($row[$column['name']]) && !empty($column['default'])) { + $row[$column['name']] = $column['default']; + } + + if (is_array($column['type']) && !empty($column['type'])) { + if (isset($column['type'][$row[$column['name']]])) { + $row[$column['name']] = $column['type'][$row[$column['name']]]; + } + } else { + if (!empty($column['param'])) { + if (isset($column['param'][$row[$column['name']]])) { + $row[$column['name']] = $column['param'][$row[$column['name']]]; + } + } else { + if (isset($row[$column['name']]) && $row[$column['name']] == '' && $column['default'] != '') { + $row[$column['name']] = $column['default']; + } + } + } + } + } + } + } + } + + /** + * 分析行class + * @param mixed $row 行数据 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + private function parseTrClass($row) + { + $_tr_class = []; + foreach ($this->_tr_class as $tr_class => $conditions) { + $match = true; + foreach ($conditions as $condition) { + if (is_callable($condition[0])) { + $params = array_merge([$row], $condition[1]); + $match = call_user_func_array($condition[0], $params) ? $match : false; + continue; + } + if (!isset($row[$condition[0]])) { + $match = false; continue; + } + $match = $this->parseCondition($row, $condition) ? $match : false; + } + if ($match) { + $_tr_class[] = $tr_class; + } + } + + return $_tr_class; + } + + /** + * 分析条件 + * @param mixed $row 行数据 + * @param array $condition 对比条件 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + private function parseCondition($row, $condition = []) + { + $match = true; + switch ($condition[1]) { + case 'eq': + $row[$condition[0]] != $condition[2] && $match = false; + break; + case 'neq': + $row[$condition[0]] == $condition[2] && $match = false; + break; + case 'gt': + $row[$condition[0]] <= $condition[2] && $match = false; + break; + case 'lt': + $row[$condition[0]] >= $condition[2] && $match = false; + break; + case 'egt': + $row[$condition[0]] < $condition[2] && $match = false; + break; + case 'elt': + $row[$condition[0]] > $condition[2] && $match = false; + break; + case 'in': + !in_array($row[$condition[0]], $condition[2]) && $match = false; + break; + case 'not in': + in_array($row[$condition[0]], $condition[2]) && $match = false; + break; + case 'between': + ($row[$condition[0]] < $condition[2][0] || $row[$condition[0]] > $condition[2][1]) && $match = false; + break; + case 'not between': + ($row[$condition[0]] >= $condition[2][0] && $row[$condition[0]] <= $condition[2][1]) && $match = false; + break; + } + return $match; + } + + /** + * 创建筛选Token + * @param string $table 表名 + * @param string $field 字段 + * @author 蔡伟明 <314013107@qq.com> + * @return bool|string + */ + private function createFilterToken($table = '', $field = '') + { + $table_token = substr(sha1($table.'-'.$field.'-'.session('user_auth.last_login_ip').'-'.session('user_auth.uid').'-'.session('user_auth.last_login_time')), 0, 8); + session($table_token, ['table' => $table, 'field' => $field]); + return $table_token; + } + + /** + * 编译表格数据 + * @author 蔡伟明 <314013107@qq.com> + */ + private function compileTable(){ + // 设置表名 + $this->_vars['_table'] = $this->_table_name; + + // 处理字段筛选 + if ($this->_vars['filter_columns']) { + // 要筛选的字段 + $filter_columns = []; + // 要筛选的字段条件 + $filter_maps = []; + // 处理字段筛选条件 + if (!empty($this->_vars['filter_map'])) { + foreach ($this->_vars['filter_map'] as $fields => $map) { + if (strpos($fields, ',')) { + $fields = explode(',', $fields); + foreach ($fields as $field) { + if (isset($filter_maps[$field])) { + // 如果某字段的条件已存在,则合并条件 + $filter_maps[$field] = array_merge($filter_maps[$field], $map); + } else { + $filter_maps[$field] = $map; + } + } + } else { + if (isset($filter_maps[$fields])) { + // 如果某字段的条件已存在,则合并条件 + $filter_maps[$fields] = array_merge($filter_maps[$fields], $map); + } else { + $filter_maps[$fields] = $map; + } + } + } + // 将条件转换为json格式 + foreach ($filter_maps as &$filter_map) { + $filter_map = json_encode($filter_map); + } + } + + // 组合字段筛选 + foreach ($this->_vars['filter_columns'] as $key => $value) { + if (is_numeric($key)) { + if (strpos($value, '.')) { + list($table, $field) = explode('.', $value); + $_filter_token = $this->createFilterToken($table, $field); + $filter_columns[$field] = [ + 'token' => $_filter_token, + 'type' => $this->_filter_type[$value], + 'filter' => $value, + 'map' => isset($filter_maps[$field]) ? $filter_maps[$field] : '', + 'options' => isset($this->_filter_options[$field]) ? $this->_filter_options[$field] : '', + 'list' => isset($this->_filter_list[$field]) ? $this->_filter_list[$field] : '' + ]; + } else { + $_filter_token = $this->createFilterToken($this->_table_name, $value); + $filter_columns[$value] = [ + 'token' => $_filter_token, + 'type' => $this->_filter_type[$value], + 'filter' => $value, + 'map' => isset($filter_maps[$value]) ? $filter_maps[$value] : '', + 'options' => isset($this->_filter_options[$value]) ? $this->_filter_options[$value] : '', + 'list' => isset($this->_filter_list[$value]) ? $this->_filter_list[$value] : '' + ]; + } + } else { + if (strpos($value, '.')) { + list($table, $field) = explode('.', $value); + $_filter_token = $this->createFilterToken($table, $field); + $filter_columns[$key] = [ + 'token' => $_filter_token, + 'type' => $this->_filter_type[$value], + 'filter' => $value, + 'map' => isset($filter_maps[$key]) ? $filter_maps[$key] : '', + 'options' => isset($this->_filter_options[$key]) ? $this->_filter_options[$key] : '', + 'list' => isset($this->_filter_list[$key]) ? $this->_filter_list[$key] : '' + ]; + } else { + $_filter_token = $this->createFilterToken($value, $key); + $filter_columns[$key] = [ + 'token' => $_filter_token, + 'type' => $this->_filter_type[$value], + 'filter' => $value . '.' . $key, + 'map' => isset($filter_maps[$key]) ? $filter_maps[$key] : '', + 'options' => isset($this->_filter_options[$key]) ? $this->_filter_options[$key] : '', + 'list' => isset($this->_filter_list[$key]) ? $this->_filter_list[$key] : '' + ]; + } + } + } + $this->_vars['filter_columns'] = $filter_columns; + } + + // 处理字段筛选默认选项 + $this->_vars['_filter_content'] = implode('|', $this->_vars['_filter_content']); + $this->_vars['_field_display'] = implode(',', $this->_vars['_field_display']); + $this->_vars['_filter'] = implode('|', $this->_vars['_filter']); + + // 处理字段排序 + if ($this->_vars['order_columns']) { + $order_columns = []; + foreach ($this->_vars['order_columns'] as $key => $value) { + if (is_numeric($key)) { + if (strpos($value, '.')) { + $tmp = explode('.', $value); + $order_columns[$tmp[1]] = $value; + } else { + $order_columns[$value] = $value; + } + } else { + if (strpos($value, '.')) { + $order_columns[$key] = $value; + } else { + $order_columns[$key] = $value. '.' .$key; + } + } + } + $this->_vars['order_columns'] = $order_columns; + } + + // 编译顶部按钮 + if ($this->_vars['top_buttons']) { + foreach ($this->_vars['top_buttons'] as &$button) { + // 处理表名变量值 + if (strpos($button['href'], '__table__')) { + $button['href'] = preg_replace( + '/__table__/i', + $this->createTableToken($this->_table_name, $this->_prefix), + $button['href'] + ); + } + + $button['attribute'] = $this->compileHtmlAttr($button); + $new_button = ""; + if (isset($button['icon']) && $button['icon'] != '') { + $new_button .= ' '; + } + $new_button .= "{$button['title']}"; + $button = $new_button; + } + } + + // 编译顶部下拉菜单 + if ($this->_vars['_select_list']) { + foreach ($this->_vars['_select_list'] as $name => &$select) { + // 当前url参数 + $url_params = $this->request->param(); + + // 要搜索的字段 + $select_field = $this->request->param('_select_field', ''); + $select_field = $select_field != '' ? explode('|', $select_field) : []; + + // 对应的值 + $select_value = $this->request->param('_select_value', ''); + $select_value = $select_value != '' ? explode('|', $select_value) : []; + + // 合并默认值 + if ($this->_select_list_default) { + foreach ($this->_select_list_default as $field => $value) { + if (!in_array($field, $select_field)) { + array_push($select_field, $field); + array_push($select_value, $value); + } + } + } + + // 当前选中值 + if (in_array($name, $select_field)) { + $select['current'] = $select_value[array_search($name, $select_field)]; + } + + // 剔除要忽略的参数 + if ($select['ignore'] !== '') { + $ignores = explode(',', $select['ignore']); + foreach ($ignores as $ignore) { + if (array_search($ignore, $select_field) !== false) { + $pos = array_search($ignore, $select_field); + array_splice($select_field, $pos, 1); + array_splice($select_value, $pos, 1); + } + } + } + + // 生成除默认选项的下拉项的跳转url + if (!empty($select_field)) { + if (!in_array($name, $select_field)) { + array_push($select_field, $name); + } + $url_params['_select_field'] = implode('|', $select_field); + foreach ($select['options'] as $key => $option) { + $select_value[array_search($name, $select_field)] = $key; + $url_params['_select_value'] = implode('|', $select_value); + $select['url'][$key] = url('').'?'.http_build_query($url_params); + } + } else { + $url_params['_select_field'] = $name; + foreach ($select['options'] as $key => $option) { + $url_params['_select_value'] = $key; // 添加下拉菜单项查询参数 + $select['url'][$key] = url('').'?'.http_build_query($url_params); + } + } + + // 生成默认选项的url + if (isset($this->_select_list_default[$name])) { + $url_params['_select_field'] = implode('|', $select_field); + $select_value[array_search($name, $select_field)] = '_all'; + $url_params['_select_value'] = implode('|', $select_value); + } else { + if (array_search($name, $select_field) !== false) { + $pos = array_search($name, $select_field); + unset($select_value[$pos]); + unset($select_field[$pos]); + if (empty($select_field)) { + unset($url_params['_select_field']); + unset($url_params['_select_value']); + } else { + $url_params['_select_field'] = implode('|', $select_field); + $url_params['_select_value'] = implode('|', $select_value); + } + } + } + $select['default_url'] = url('').'?'.http_build_query($url_params); + } + } + + // 处理搜索框 + if ($this->_search) { + $_temp_fields = []; + foreach ($this->_search['fields'] as $key => $field) { + if (is_numeric($key)) { + if (strpos($field, '.')) { + $_field = explode('.', $field)[1]; + } else { + $_field = $field; + } + $_temp_fields[$field] = isset($this->_field_name[$_field]) ? $this->_field_name[$_field] : ''; + } else { + $_temp_fields[$key] = $field; + } + } + $this->_vars['search'] = [ + 'fields' => $_temp_fields, + 'field_all' => implode('|', array_keys($_temp_fields)), + 'placeholder' => $this->_search['placeholder'] != '' ? $this->_search['placeholder'] : '请输入'. implode('/', $_temp_fields), + 'url' => $this->_search['url'] == '' ? $this->request->baseUrl(true) : $this->_search['url'] + ]; + } + + // 编译表格数据row_list的值 + $this->compileRows(); + + // 处理页面标题 + if ($this->_vars['page_title'] == '') { + $location = get_location('', false, false); + if ($location) { + $curr_location = end($location); + $this->_vars['page_title'] = $curr_location['title']; + } + } + + // 处理是否有分页数据 + if (!$this->_has_pages) { + $this->_vars['pages'] = ''; + } + + // 处理js和css合并的参数 + if (!empty($this->_vars['_js_files'])) { + $this->_vars['_js_files'] = array_unique($this->_vars['_js_files']); + } + if (!empty($this->_vars['_css_files'])) { + $this->_vars['_css_files'] = array_unique($this->_vars['_css_files']); + sort($this->_vars['_css_files']); + } + if (!empty($this->_vars['_js_init'])) { + $this->_vars['_js_init'] = array_unique($this->_vars['_js_init']); + sort($this->_vars['_js_init']); + $this->_vars['_js_init'] = json_encode($this->_vars['_js_init']); + } + } + + /** + * 加载模板输出 + * @param string $template 模板文件名 + * @param array $vars 模板输出变量 + * @param array $config 模板参数 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function fetch($template = '', $vars = [], $config = []) + { + // 编译表格数据 + $this->compileTable(); + + if ($template != '') { + $this->_template = $template; + } + + if (!empty($vars)) { + $this->_vars = array_merge($this->_vars, $vars); + } + + // 实例化视图并渲染 + return parent::fetch($this->_template, $this->_vars, $config); + } +} diff --git a/application/common/builder/table/layout.html b/application/common/builder/table/layout.html new file mode 100644 index 0000000..e77eba6 --- /dev/null +++ b/application/common/builder/table/layout.html @@ -0,0 +1,715 @@ +{extend name="$_admin_base_layout" /} + +{block name="content"} + {notempty name="page_tips_top"} +
    + +

    {$page_tips_top|raw}

    +
    + {/notempty} +
    +
    + {$extra_html_block_top|raw|default=''} +
    + {notempty name="tab_nav"} + + {else/} + {empty name="_pop"} +
    +
      +
    • + +
    • +
    • + +
    • +
    +

    {$page_title|default=""|htmlspecialchars}

    +
    + {/empty} + {/notempty} +
    +
    + {notempty name="_search_area"} +
    + +
    + {volist name="_search_area" id="vo"} + {switch name="vo[0]"} + {case value="text"} +
    +
    + {$vo.2|default=''} + +
    +
    + {/case} + {case value="select"} +
    +
    + {$vo.2|default=''} + +
    +
    + {/case} + {case value="daterange"} +
    +
    + {$vo.2|default=''} + +
    +
    + {/case} + {/switch} + {/volist} +
    +
    +
    + + 重置 +
    +
    +
    +
    +
    + {/notempty} + + {// 顶部筛选及搜索 } + {$extra_html_toolbar_top|raw|default=''} +
    +
    + {// 搜索框 } + {notempty name="search"} + + {/notempty} + + {// 顶部按钮 } +
    + {if (!empty($top_buttons))} + {volist name="top_buttons" id="button"} + {$button|raw} + {/volist} + {/if} + + {// 下拉菜单} + {notempty name="_select_list"} +
    + {volist name="_select_list" id="item"} + + {/volist} +
    + {/notempty} + + {// 时间段搜索} + {notempty name="_filter_time"} +
    +
    +
    + + + +
    + + +
    +
    + {/notempty} +
    +
    +
    + {$extra_html_toolbar_bottom|raw|default=''} +
    +
    + {$extra_html_table_top|raw|default=''} +
    + + + {if (!$hide_checkbox)} + + {/if} + {volist name="columns" id="column"} + + {/volist} + + + + {if (!$hide_checkbox)} + + {/if} + + {volist name="columns" id="column"} + + {/volist} + + +
    + + + {// 字段标题} + {$column.title|default=''|htmlspecialchars} + + {// 排序功能} + {php}if (isset($order_columns[$column['name']])): {/php} + {php} + $_by = input('param._by') == 'asc' ? 'desc' : 'asc'; + $_param = array_merge(input('get.'), ['_by' => $_by, '_order' => $order_columns[$column['name']]]); + if ($_param) { + $_get = []; + foreach ($_param as $key => $value) { + $_get[] = $key. '=' .$value; + } + $_get = '?'.implode('&', $_get); + } + {/php} + + {php} + if (input('param._order') == $order_columns[$column['name']]) { + echo input('param._by') == 'asc' ? '' : ''; + } else { + echo ''; + } + {/php} + + {php}endif;{/php} + + {// 筛选功能} + {php}if (isset($filter_columns[$column['name']])): {/php} + {php} + if (!empty(request()->param('_field_display'))) { + $_field_display = request()->param('_field_display'); + } + {/php} + + {php}endif;{/php} + +
    +
    +
    + + + {if (!$hide_checkbox)} + + {/if} + {volist name="columns" id="column"} + + {/volist} + + + {volist name="row_list" id="row"} + + {if (!$hide_checkbox)} + + {/if} + + {volist name="columns" id="column"} + + {/volist} + + {/volist} + + {empty name="row_list"} + {notempty name="empty_tips"} + + {php}$colspan = count($columns)+1{/php} + + + {/notempty} + {/empty} + +
    +
    + +
    +
    +
    + {php}if(is_array($column['type']) || $column['type'] == '' || $column['type'] == 'btn' || $column['type'] == 'text'):{/php} + {$row[$column['name']]|raw|default=''} + {php}else:{/php} + {$row[$column['name'].'__'.$column['type']]|raw|default=''} + {php}endif;{/php} +
    +
    + {$empty_tips|raw}
    +
    +
    + {gt name="fixed_left_column" value="0"} +
    +
    + + + {if (!$hide_checkbox)} + + {/if} + {volist name="columns" id="column"} + + {/volist} + + + + {if (!$hide_checkbox)} + + {/if} + + {volist name="columns" id="column"} + + {/volist} + + +
    + + + {// 字段标题} + {$column.title|default=''|htmlspecialchars} + + {// 排序功能} + {php}if (isset($order_columns[$column['name']])): {/php} + {php} + $_by = input('param._by') == 'asc' ? 'desc' : 'asc'; + $_param = array_merge(input('get.'), ['_by' => $_by, '_order' => $order_columns[$column['name']]]); + if ($_param) { + $_get = []; + foreach ($_param as $key => $value) { + $_get[] = $key. '=' .$value; + } + $_get = '?'.implode('&', $_get); + } + {/php} + + {php} + if (input('param._order') == $order_columns[$column['name']]) { + echo input('param._by') == 'asc' ? '' : ''; + } else { + echo ''; + } + {/php} + + {php}endif;{/php} + + {// 筛选功能} + {php}if (isset($filter_columns[$column['name']])): {/php} + {php} + if (!empty(request()->param('_field_display'))) { + $_field_display = request()->param('_field_display'); + } + {/php} + + {php}endif;{/php} + +
    +
    +
    + + + {if (!$hide_checkbox)} + + {/if} + {volist name="columns" id="column"} + + {/volist} + + + {volist name="row_list" id="row"} + + {if (!$hide_checkbox)} + + {/if} + + {volist name="columns" id="column"} + + {/volist} + + {/volist} + + {empty name="row_list"} + {notempty name="empty_tips"} + + {php}$colspan = count($columns)+1{/php} + + + {/notempty} + {/empty} + +
    + + + {php}if(is_array($column['type']) || $column['type'] == '' || $column['type'] == 'btn' || $column['type'] == 'text'):{/php} + {$row[$column['name']]|raw|default=''} + {php}else:{/php} + {$row[$column['name'].'__'.$column['type']]|raw|default=''} + {php}endif;{/php} +
    + {$empty_tips|raw}
    +
    +
    +
    + {/gt} + {gt name="fixed_right_column" value="0"} +
    +
    + + + {if (!$hide_checkbox)} + + {/if} + {volist name="columns" id="column"} + + {/volist} + + + + {if (!$hide_checkbox)} + + {/if} + + {volist name="columns" id="column"} + + {/volist} + + +
    + + + {// 字段标题} + {$column.title|default=''|htmlspecialchars} + + {// 排序功能} + {php}if (isset($order_columns[$column['name']])): {/php} + {php} + $_by = input('param._by') == 'asc' ? 'desc' : 'asc'; + $_param = array_merge(input('get.'), ['_by' => $_by, '_order' => $order_columns[$column['name']]]); + if ($_param) { + $_get = []; + foreach ($_param as $key => $value) { + $_get[] = $key. '=' .$value; + } + $_get = '?'.implode('&', $_get); + } + {/php} + + {php} + if (input('param._order') == $order_columns[$column['name']]) { + echo input('param._by') == 'asc' ? '' : ''; + } else { + echo ''; + } + {/php} + + {php}endif;{/php} + + {// 筛选功能} + {php}if (isset($filter_columns[$column['name']])): {/php} + {php} + if (!empty(request()->param('_field_display'))) { + $_field_display = request()->param('_field_display'); + } + {/php} + + {php}endif;{/php} + +
    +
    +
    +
    + + + {if (!$hide_checkbox)} + + {/if} + {volist name="columns" id="column"} + + {/volist} + + + {volist name="row_list" id="row"} + + {if (!$hide_checkbox)} + + {/if} + + {volist name="columns" id="column"} + + {/volist} + + {/volist} + + {empty name="row_list"} + {notempty name="empty_tips"} + + {php}$colspan = count($columns)+1{/php} + + + {/notempty} + {/empty} + +
    + + + {php}if(is_array($column['type']) || $column['type'] == '' || $column['type'] == 'btn' || $column['type'] == 'text'):{/php} + {$row[$column['name']]|raw|default=''} + {php}else:{/php} + {$row[$column['name'].'__'.$column['type']]|raw|default=''} + {php}endif;{/php} +
    + {$empty_tips|raw}
    +
    +
    +
    +
    +
    + {/gt} + {$extra_html_table_bottom|raw|default=''} +
    +
    +
    +
    +
    + {// 分页 } + {notempty name="pages"} + {$pages|raw} + {/notempty} + {notempty name="_page_info"} +
    +
    + + + / {$_page_info->lastPage()|raw} 页,共 {$_page_info->total()|raw} 条数据,每页显示数量 +
    +
    + {/notempty} +
    +
    +
    +
    +
    +
    + {$extra_html_block_bottom|raw|default=''} +
    +
    + {notempty name="page_tips_bottom"} +
    + +

    {$page_tips_bottom|raw}

    +
    + {/notempty} +{/block} + +{block name="style"} + {volist name="css_list" id="vo"} + + {/volist} + + {// 额外CSS代码 } + {$extra_css|raw|default=''} +{/block} + +{block name="script"} + + {volist name="js_list" id="vo"} + + {/volist} + + {// 额外JS代码 } + {$extra_js|raw|default=''} +{/block} \ No newline at end of file diff --git a/application/common/controller/Common.php b/application/common/controller/Common.php new file mode 100644 index 0000000..ff8bd2f --- /dev/null +++ b/application/common/controller/Common.php @@ -0,0 +1,166 @@ + + */ + protected function initialize() + { + // 后台公共模板 + $this->assign('_admin_base_layout', config('admin_base_layout')); + // 当前配色方案 + $this->assign('system_color', config('system_color')); + // 输出弹出层参数 + $this->assign('_pop', $this->request->param('_pop')); + } + + /** + * 获取筛选条件 + * @author 蔡伟明 <314013107@qq.com> + * @alter 小乌 <82950492@qq.com> + * @return array + */ + final protected function getMap() + { + $search_field = input('param.search_field/s', '', 'trim'); + $keyword = input('param.keyword/s', '', 'trim'); + $filter = input('param._filter/s', '', 'trim'); + $filter_content = input('param._filter_content/s', '', 'trim'); + $filter_time = input('param._filter_time/s', '', 'trim'); + $filter_time_from = input('param._filter_time_from/s', '', 'trim'); + $filter_time_to = input('param._filter_time_to/s', '', 'trim'); + $select_field = input('param._select_field/s', '', 'trim'); + $select_value = input('param._select_value/s', '', 'trim'); + $search_area = input('param._s', '', 'trim'); + $search_area_op = input('param._o', '', 'trim'); + + $map = []; + + // 搜索框搜索 + if ($search_field != '' && $keyword !== '') { + $map[] = [$search_field, 'like', "%$keyword%"]; + } + + // 下拉筛选 + if ($select_field != '') { + $select_field = array_filter(explode('|', $select_field), 'strlen'); + $select_value = array_filter(explode('|', $select_value), 'strlen'); + foreach ($select_field as $key => $item) { + if ($select_value[$key] != '_all') { + $map[] = [$item, '=', $select_value[$key]]; + } + } + } + + // 时间段搜索 + if ($filter_time != '' && $filter_time_from != '' && $filter_time_to != '') { + $map[] = [$filter_time, 'between time', [$filter_time_from.' 00:00:00', $filter_time_to.' 23:59:59']]; + } + + // 表头筛选 + if ($filter != '') { + $filter = array_filter(explode('|', $filter), 'strlen'); + $filter_content = array_filter(explode('|', $filter_content), 'strlen'); + foreach ($filter as $key => $item) { + if (isset($filter_content[$key])) { + $map[] = [$item, 'in', $filter_content[$key]]; + } + } + } + + // 搜索区域 + if ($search_area != '') { + $search_area = explode('|', $search_area); + $search_area_op = explode('|', $search_area_op); + foreach ($search_area as $key => $item) { + list($field, $value) = explode('=', $item); + $value = trim($value); + $op = explode('=', $search_area_op[$key]); + if ($value != '') { + switch ($op[1]) { + case 'like': + $map[] = [$field, 'like', "%$value%"]; + break; + case 'between time': + case 'not between time': + $value = explode(' - ', $value); + if ($value[0] == $value[1]) { + $value[0] = date('Y-m-d', strtotime($value[0])). ' 00:00:00'; + $value[1] = date('Y-m-d', strtotime($value[1])). ' 23:59:59'; + } + default: + $map[] = [$field, $op[1], $value]; + } + } + } + } + return $map; + } + + /** + * 获取字段排序 + * @param string $extra_order 额外的排序字段 + * @param bool $before 额外排序字段是否前置 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + final protected function getOrder($extra_order = '', $before = false) + { + $order = input('param._order/s', ''); + $by = input('param._by/s', ''); + if ($order == '' || $by == '') { + return $extra_order; + } + if ($extra_order == '') { + return $order. ' '. $by; + } + if ($before) { + return $extra_order. ',' .$order. ' '. $by; + } else { + return $order. ' '. $by . ',' . $extra_order; + } + } + + /** + * 渲染插件模板 + * @param string $template 模板文件名 + * @param string $suffix 模板后缀 + * @param array $vars 模板输出变量 + * @param array $config 模板参数 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + final protected function pluginView($template = '', $suffix = '', $vars = [], $config = []) + { + $plugin_name = input('param.plugin_name'); + + if ($plugin_name != '') { + $plugin = $plugin_name; + $action = 'index'; + } else { + $plugin = input('param._plugin'); + $action = input('param._action'); + } + $suffix = $suffix == '' ? 'html' : $suffix; + $template = $template == '' ? $action : $template; + $template_path = config('plugin_path'). "{$plugin}/view/{$template}.{$suffix}"; + return parent::fetch($template_path, $vars, $config); + } +} diff --git a/application/common/controller/Plugin.php b/application/common/controller/Plugin.php new file mode 100644 index 0000000..b40e8cc --- /dev/null +++ b/application/common/controller/Plugin.php @@ -0,0 +1,158 @@ + + */ +abstract class Plugin +{ + /** + * @var null 视图实例对象 + */ + protected $view = null; + + /** + * @var string 插件配置文件 + */ + public $config_file = ''; + + /** + * @var string 插件路径 + */ + public $plugin_path = ''; + + /** + * @var string 错误信息 + */ + protected $error = ''; + + /** + * 构造方法 + */ + public function __construct() + { + $this->view = Container::get('view'); + $this->plugin_path = config('plugin_path').$this->getName().'/'; + if (is_file($this->plugin_path.'config.php')) { + $this->config_file = $this->plugin_path.'config.php'; + } + if (is_file($this->plugin_path.'common.php')) { + include $this->plugin_path.'common.php'; + } + } + + /** + * 获取插件名称 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + final public function getName() + { + $class = get_class($this); + return substr($class, strrpos($class, '\\') + 1); + } + + /** + * 显示方法 + * @param string $template 模板或直接解析内容 + * @param array $vars 模板输出变量 + * @param array $config 模板参数 + * @param bool $renderContent 是否渲染内容 + * @throws \Exception + * @author 蔡伟明 <314013107@qq.com> + */ + final protected function fetch($template = '', $vars = [], $config = [], $renderContent = false) + { + if ($template != '') { + if (!is_file($template)) { + $template = $this->plugin_path. 'view/'. $template . '.' . config('template.view_suffix'); + if (!is_file($template)) { + throw new Exception('模板不存在:'.$template, 5001); + } + } + + echo $this->view->fetch($template, $vars, $config, $renderContent); + } + } + + /** + * 模板变量赋值 + * @param string $name 要显示的模板变量 + * @param string $value 变量的值 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + final protected function assign($name = '', $value='') + { + $this->view->assign($name, $value); + return $this; + } + + /** + * 获取插件配置值,先从数据库获取,如果没有则从插件配置文件获取 + * @param string $name 插件名称 + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed + */ + final public function getConfigValue($name='') + { + static $_config = array(); + if(empty($name)){ + $name = $this->getName(); + } + if(isset($_config[$name])){ + return $_config[$name]; + } + + $config = plugin_config($name); + + if (!$config) { + if ($this->config_file != '') { + $file_config = include $this->config_file; + } + + if (isset($file_config) && $file_config != '') { + $config = parse_config($file_config); + $_config[$name] = $config; + } + } + return $config; + } + + /** + * 获取错误信息 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + final public function getError() + { + return $this->error; + } + + /** + * 必须实现安装方法 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + abstract public function install(); + + /** + * 必须实现卸载方法 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + abstract public function uninstall(); +} diff --git a/application/common/model/Plugin.php b/application/common/model/Plugin.php new file mode 100644 index 0000000..86e489f --- /dev/null +++ b/application/common/model/Plugin.php @@ -0,0 +1,21 @@ + + */ + protected function initialize() + { + // 系统开关 + if (!config('web_site_status')) { + $this->error('站点已经关闭,请稍后访问~'); + } + } +} diff --git a/application/index/controller/Index.php b/application/index/controller/Index.php new file mode 100644 index 0000000..0755d91 --- /dev/null +++ b/application/index/controller/Index.php @@ -0,0 +1,26 @@ +redirect(config('home_default_module'). '/index/index'); + } + return '

    :)

    '.config("dolphin.product_name").' '.config("dolphin.product_version").'
    极速 · 极简 · 极致

    '; + } +} diff --git a/application/index/controller/Plugin.php b/application/index/controller/Plugin.php new file mode 100644 index 0000000..781d82f --- /dev/null +++ b/application/index/controller/Plugin.php @@ -0,0 +1,39 @@ + + * @return mixed + */ + public function execute() + { + $plugin = input('param._plugin'); + $controller = input('param._controller'); + $action = input('param._action'); + $params = $this->request->except(['_plugin', '_controller', '_action'], 'param'); + + if (empty($plugin) || empty($controller) || empty($action)) { + $this->error('没有指定插件名称、控制器名称或操作名称'); + } + + if (!plugin_action_exists($plugin, $controller, $action)) { + $this->error("找不到方法:{$plugin}/{$controller}/{$action}"); + } + return plugin_action($plugin, $controller, $action, $params); + } +} diff --git a/application/install/common.php b/application/install/common.php new file mode 100644 index 0000000..a1efe1a --- /dev/null +++ b/application/install/common.php @@ -0,0 +1,269 @@ + array('操作系统', '不限制', '类Unix', PHP_OS, 'check'), + 'php' => array('PHP版本', '5.6', '5.6+', PHP_VERSION, 'check'), + 'upload' => array('附件上传', '不限制', '2M+', '未知', 'check'), + 'gd' => array('GD库', '2.0', '2.0+', '未知', 'check'), + 'disk' => array('磁盘空间', '100M', '不限制', '未知', 'check'), + ); + + // PHP环境检测 + if($items['php'][3] < $items['php'][1]){ + $items['php'][4] = 'times text-warning'; + session('error', true); + } + + // 附件上传检测 + if(@ini_get('file_uploads')) + $items['upload'][3] = ini_get('upload_max_filesize'); + + // GD库检测 + $tmp = function_exists('gd_info') ? gd_info() : array(); + if(empty($tmp['GD Version'])){ + $items['gd'][3] = '未安装'; + $items['gd'][4] = 'times text-warning'; + session('error', true); + } else { + $items['gd'][3] = $tmp['GD Version']; + } + unset($tmp); + + // 磁盘空间检测 + if(function_exists('disk_free_space')) { + $disk_size = floor(disk_free_space(Env::get('app_path')) / (1024*1024)); + $items['disk'][3] = $disk_size.'M'; + if ($disk_size < 100) { + $items['disk'][4] = 'times text-warning'; + session('error', true); + } + } + + return $items; +} + +/** + * 目录,文件读写检测 + * @return array 检测数据 + */ +function check_dirfile(){ + $items = array( + array('dir', '可写', 'check', '../application'), + array('dir', '可写', 'check', '../config'), + array('dir', '可写', 'check', '../data'), + array('dir', '可写', 'check', '../export'), + array('dir', '可写', 'check', '../packet'), + array('dir', '可写', 'check', '../plugins'), + array('dir', '可写', 'check', './static'), + array('dir', '可写', 'check', './uploads'), + array('dir', '可写', 'check', '../runtime'), + array('dir', '可写', 'check', '../store'), + ); + + foreach ($items as &$val) { + $item = INSTALL_APP_PATH . $val[3]; + if('dir' == $val[0]){ + if(!is_writable($item)) { + if(is_dir($item)) { + $val[1] = '可读'; + $val[2] = 'times text-warning'; + session('error', true); + } else { + $val[1] = '不存在'; + $val[2] = 'times text-warning'; + session('error', true); + } + } + } else { + if(file_exists($item)) { + if(!is_writable($item)) { + $val[1] = '不可写'; + $val[2] = 'times text-warning'; + session('error', true); + } + } else { + if(!is_writable(dirname($item))) { + $val[1] = '不存在'; + $val[2] = 'times text-warning'; + session('error', true); + } + } + } + } + + return $items; +} + +/** + * 函数检测 + * @return array 检测数据 + */ +function check_func(){ + $items = array( + array('pdo','支持','check','类'), + array('pdo_mysql','支持','check','模块'), + array('fileinfo','支持','check','模块'), + array('curl','支持','check','模块'), + array('file_get_contents', '支持', 'check','函数'), + array('mb_strlen', '支持', 'check','函数'), + array('scandir', '支持', 'check','函数'), + ); + + foreach ($items as &$val) { + if(('类'==$val[3] && !class_exists($val[0])) + || ('模块'==$val[3] && !extension_loaded($val[0])) + || ('函数'==$val[3] && !function_exists($val[0])) + ){ + $val[1] = '不支持'; + $val[2] = 'times text-warning'; + session('error', true); + } + } + + return $items; +} + +/** + * 写入配置文件 + * @param $config + * @return array 配置信息 + */ +function write_config($config){ + if(is_array($config)){ + //读取配置内容 + $conf = file_get_contents(Env::get('app_path') . 'install/data/database.tpl'); + // 替换配置项 + foreach ($config as $name => $value) { + $conf = str_replace("[{$name}]", $value, $conf); + } + + //写入应用配置文件 + if(file_put_contents(Env::get('config_path') . 'database.php', $conf)){ + show_msg('配置文件写入成功'); + } else { + show_msg('配置文件写入失败!', 'error'); + session('error', true); + } + return ''; + } +} + +/** + * 创建数据表 + * @param $db 数据库连接资源 + * @param string $prefix 表前缀 + */ +function create_tables($db, $prefix = ''){ + // 读取SQL文件 + $sql = file_get_contents(Env::get('app_path') . 'install/data/dolphin.sql'); + + $sql = str_replace("\r", "\n", $sql); + $sql = explode(";\n", $sql); + + // 替换表前缀 + $orginal = config('original_table_prefix'); + $sql = str_replace(" `{$orginal}", " `{$prefix}", $sql); + + // 开始安装 + show_progress('0%'); + $all_table = config('install_table_total'); + $i = 1; + foreach ($sql as $value) { + $value = trim($value); + if(empty($value)) continue; + $msg = (int)($i/$all_table*100) . '%'; + if(false !== $db->execute($value)){ + show_progress($msg); + } else { + show_progress($msg, 'error'); + session('error', true); + } + $i++; + } +} + +/** + * 更新数据表 + * @param $db 数据库连接资源 + * @param string $prefix 表前缀 + */ +function update_tables($db, $prefix = ''){ + //读取SQL文件 + $sql = file_get_contents(Env::get('app_path') . 'install/data/update.sql'); + $sql = str_replace("\r", "\n", $sql); + $sql = explode(";\n", $sql); + + // 替换表前缀 + $sql = str_replace(" `dp_", " `{$prefix}", $sql); + + //开始安装 + show_progress('0%'); + $all_table = config('update_data_total'); + $i = 1; + $msg = ''; + foreach ($sql as $value) { + $value = trim($value); + if(empty($value)) continue; + if(substr($value, 0, 12) == 'CREATE TABLE') { + $msg = (int)($i/$all_table*100) . '%'; + if(($db->execute($value)) === false){ + session('error', true); + } + } else { + if(substr($value, 0, 8) == 'UPDATE `') { + $msg = (int)($i/$all_table*100) . '%'; + } else if(substr($value, 0, 11) == 'ALTER TABLE'){ + $msg = (int)($i/$all_table*100) . '%'; + } else if(substr($value, 0, 11) == 'INSERT INTO'){ + $msg = (int)($i/$all_table*100) . '%'; + } + if(($db->execute($value)) === false){ + session('error', true); + } + } + + if ($msg != '') { + show_progress($msg); + $i++; + } + } +} + +/** + * 及时显示提示信息 + * @param string $msg 提示信息 + * @param string $class 类名 + */ +function show_msg($msg, $class = ''){ + echo ""; + flush(); + ob_flush(); +} + +/** + * 显示进度 + * @param $msg + * @param string $class + * @author 蔡伟明 <314013107@qq.com> + */ +function show_progress($msg, $class = ''){ + echo ""; + flush(); + ob_flush(); +} diff --git a/application/install/config.php b/application/install/config.php new file mode 100644 index 0000000..8a8c002 --- /dev/null +++ b/application/install/config.php @@ -0,0 +1,22 @@ + 'DolphinPHP', //产品名称 + 'install_website_domain' => 'http://www.dolphinphp.com', //官方网址 + 'install_company_name' => '河源市卓锐科技有限公司', //公司名称 + 'original_table_prefix' => 'dp_', //默认表前缀 + + // 安装配置 + 'install_table_total' => 255, // 安装时,需执行的sql语句数量 +); diff --git a/application/install/config/app.php b/application/install/config/app.php new file mode 100644 index 0000000..678acc2 --- /dev/null +++ b/application/install/config/app.php @@ -0,0 +1,22 @@ + 'DolphinPHP', //产品名称 + 'install_website_domain' => 'http://www.dolphinphp.com', //官方网址 + 'install_company_name' => '广东卓锐软件有限公司', //公司名称 + 'original_table_prefix' => 'dp_', //默认表前缀 + + // 安装配置 + 'install_table_total' => 253, // 安装时,需执行的sql语句数量 +); diff --git a/application/install/controller/Index.php b/application/install/controller/Index.php new file mode 100644 index 0000000..8c76f5a --- /dev/null +++ b/application/install/controller/Index.php @@ -0,0 +1,190 @@ + + */ + protected function initialize() { + $this->assign('static_dir', 'static/'); + } + + /** + * 安装首页 + * @author 蔡伟明 <314013107@qq.com> + */ + public function index() + { + if (is_file(Env::get('app_path') . 'database.php')) { + // 已经安装过了 执行更新程序 + session('reinstall', true); + $this->assign('next', '重新安装'); + } else { + session('reinstall', false); + $this->assign('next', '下一步'); + } + + session('step', 1); + session('error', false); + return $this->fetch(); + } + + /** + * 步骤二,检查环境 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function step2() + { + if (session('step') != 1 && session('step') != 3) $this->redirect($this->request->baseFile()); + if(session('reinstall')){ + session('step', 2); + $this->redirect($this->request->baseFile().'?s=/index/step4.html'); + }else{ + session('error', false); + + // 环境检测 + $env = check_env(); + + // 目录文件读写检测 + $dirfile = check_dirfile(); + $this->assign('dirfile', $dirfile); + + // 函数检测 + $func = check_func(); + + session('step', 2); + + $this->assign('env', $env); + $this->assign('func', $func); + + return $this->fetch(); + } + } + + /** + * 步骤三,设置数据库连接 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function step3() + { + // 检查上一步是否通过 + if ($this->request->isAjax()) { + if (session('error')) { + $this->error('环境检测没有通过,请调整环境后重试!'); + } else { + $this->success('恭喜您环境检测通过', $this->request->baseFile().'?s=/index/step3.html'); + } + } + if (session('step') != 2) $this->redirect($this->request->baseFile()); + session('error', false); + session('step', 3); + return $this->fetch(); + } + + /** + * 步骤四,创建数据库 + * @param null $db 数据库配置信息 + * @param int $cover 是否覆盖已存在数据库 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function step4($db = null, $cover = 0) + { + // 检查上一步是否通过 + if ($this->request->isPost()) { + // 检测数据库配置 + if(!is_array($db) || empty($db['type']) + || empty($db['hostname']) + || empty($db['database']) + || empty($db['username']) + || empty($db['prefix'])){ + $this->error('请填写完整的数据库配置'); + } + + // 缓存数据库配置 + session('db_config', $db); + + // 防止不存在的数据库导致连接数据库失败 + $db_name = $db['database']; + unset($db['database']); + + // 创建数据库连接 + $db_instance = Db::connect($db); + + // 检测数据库连接 + try{ + $db_instance->execute('select version()'); + }catch(\Exception $e){ + $this->error('数据库连接失败,请检查数据库配置!'); + } + + // 用户选择不覆盖情况下检测是否已存在数据库 + if (!$cover) { + // 检测是否已存在数据库 + $result = $db_instance->execute('SELECT * FROM information_schema.schemata WHERE schema_name="'.$db_name.'"'); + if ($result) { + $this->error('该数据库已存在,请更换名称!如需覆盖,请选中覆盖按钮!'); + } + } + + // 创建数据库 + $sql = "CREATE DATABASE IF NOT EXISTS `{$db_name}` DEFAULT CHARACTER SET utf8"; + $db_instance->execute($sql) || $this->error($db_instance->getError()); + + // 跳转到数据库安装页面 + $this->success('参数正确开始安装', $this->request->baseFile().'?s=/index/step4.html'); + } else { + if (session('step') != 3 && !session('reinstall')) { + $this->redirect($this->request->baseFile()); + } + + session('step', 4); + return $this->fetch(); + } + } + + /** + * 完成安装 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function complete() + { + if (session('step') != 4) { + $this->error('请按步骤安装系统', $this->request->baseFile()); + } + + if (session('error')) { + $this->error('安装出错,请重新安装!', $this->request->baseFile()); + } else { + // 写入安装锁定文件(只能在最后一步写入锁定文件,因为锁定文件写入后安装模块将无法访问) + file_put_contents('../data/install.lock', 'lock'); + session('step', null); + session('error', null); + session('reinstall', null); + return $this->fetch(); + } + } +} \ No newline at end of file diff --git a/application/install/data/database.tpl b/application/install/data/database.tpl new file mode 100644 index 0000000..9fdd252 --- /dev/null +++ b/application/install/data/database.tpl @@ -0,0 +1,61 @@ + '[type]', + // 服务器地址 + 'hostname' => '[hostname]', + // 数据库名 + 'database' => '[database]', + // 用户名 + 'username' => '[username]', + // 密码 + 'password' => '[password]', + // 端口 + 'hostport' => '[hostport]', + // 连接dsn + 'dsn' => '', + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => '[prefix]', + // 数据库调试模式 + 'debug' => true, + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 自动读取主库数据 + 'read_master' => false, + // 是否严格检查字段是否存在 + 'fields_strict' => false, + // 数据集返回类型 + 'resultset_type' => 'array', + // 自动写入时间戳字段 + 'auto_timestamp' => false, + // 时间字段取出后的默认时间格式 + 'datetime_format' => false, + // 是否需要进行SQL性能分析 + 'sql_explain' => false, + // Builder类 + 'builder' => '', + // Query类 + 'query' => '\\think\\db\\Query', + // 是否需要断线重连 + 'break_reconnect' => false, + // 断线标识字符串 + 'break_match_str' => [], +]; diff --git a/application/install/data/dolphin.sql b/application/install/data/dolphin.sql new file mode 100644 index 0000000..cd37c13 --- /dev/null +++ b/application/install/data/dolphin.sql @@ -0,0 +1,621 @@ +/* +Navicat MySQL Data Transfer + +Source Server : localhost +Source Server Version : 50540 +Source Host : localhost:3306 +Source Database : dolphinphp + +Target Server Type : MYSQL +Target Server Version : 50540 +File Encoding : 65001 + +Date: 2016-12-13 21:43:18 +*/ + +SET FOREIGN_KEY_CHECKS=0; + +-- ---------------------------- +-- Table structure for `dp_admin_access` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_access`; +CREATE TABLE `dp_admin_access` ( + `module` varchar(16) NOT NULL DEFAULT '' COMMENT '模型名称', + `group` varchar(16) NOT NULL DEFAULT '' COMMENT '权限分组标识', + `uid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '用户id', + `nid` varchar(16) NOT NULL DEFAULT '' COMMENT '授权节点id', + `tag` varchar(16) NOT NULL DEFAULT '' COMMENT '分组标签' +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='统一授权表'; + +-- ---------------------------- +-- Records of dp_admin_access +-- ---------------------------- + +-- ---------------------------- +-- Table structure for `dp_admin_action` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_action`; +CREATE TABLE `dp_admin_action` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `module` varchar(16) NOT NULL DEFAULT '' COMMENT '所属模块名', + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '行为唯一标识', + `title` varchar(80) NOT NULL DEFAULT '' COMMENT '行为标题', + `remark` varchar(128) NOT NULL DEFAULT '' COMMENT '行为描述', + `rule` text NOT NULL COMMENT '行为规则', + `log` text NOT NULL COMMENT '日志规则', + `status` tinyint(2) NOT NULL DEFAULT '0' COMMENT '状态', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=43 DEFAULT CHARSET=utf8 COMMENT='系统行为表'; + +-- ---------------------------- +-- Records of dp_admin_action +-- ---------------------------- +INSERT INTO `dp_admin_action` VALUES ('1', 'user', 'user_add', '添加用户', '添加用户', '', '[user|get_nickname] 添加了用户:[record|get_nickname]', '1', '1480156399', '1480163853'); +INSERT INTO `dp_admin_action` VALUES ('2', 'user', 'user_edit', '编辑用户', '编辑用户', '', '[user|get_nickname] 编辑了用户:[details]', '1', '1480164578', '1480297748'); +INSERT INTO `dp_admin_action` VALUES ('3', 'user', 'user_delete', '删除用户', '删除用户', '', '[user|get_nickname] 删除了用户:[details]', '1', '1480168582', '1480168616'); +INSERT INTO `dp_admin_action` VALUES ('4', 'user', 'user_enable', '启用用户', '启用用户', '', '[user|get_nickname] 启用了用户:[details]', '1', '1480169185', '1480169185'); +INSERT INTO `dp_admin_action` VALUES ('5', 'user', 'user_disable', '禁用用户', '禁用用户', '', '[user|get_nickname] 禁用了用户:[details]', '1', '1480169214', '1480170581'); +INSERT INTO `dp_admin_action` VALUES ('6', 'user', 'user_access', '用户授权', '用户授权', '', '[user|get_nickname] 对用户:[record|get_nickname] 进行了授权操作。详情:[details]', '1', '1480221441', '1480221563'); +INSERT INTO `dp_admin_action` VALUES ('7', 'user', 'role_add', '添加角色', '添加角色', '', '[user|get_nickname] 添加了角色:[details]', '1', '1480251473', '1480251473'); +INSERT INTO `dp_admin_action` VALUES ('8', 'user', 'role_edit', '编辑角色', '编辑角色', '', '[user|get_nickname] 编辑了角色:[details]', '1', '1480252369', '1480252369'); +INSERT INTO `dp_admin_action` VALUES ('9', 'user', 'role_delete', '删除角色', '删除角色', '', '[user|get_nickname] 删除了角色:[details]', '1', '1480252580', '1480252580'); +INSERT INTO `dp_admin_action` VALUES ('10', 'user', 'role_enable', '启用角色', '启用角色', '', '[user|get_nickname] 启用了角色:[details]', '1', '1480252620', '1480252620'); +INSERT INTO `dp_admin_action` VALUES ('11', 'user', 'role_disable', '禁用角色', '禁用角色', '', '[user|get_nickname] 禁用了角色:[details]', '1', '1480252651', '1480252651'); +INSERT INTO `dp_admin_action` VALUES ('12', 'user', 'attachment_enable', '启用附件', '启用附件', '', '[user|get_nickname] 启用了附件:附件ID([details])', '1', '1480253226', '1480253332'); +INSERT INTO `dp_admin_action` VALUES ('13', 'user', 'attachment_disable', '禁用附件', '禁用附件', '', '[user|get_nickname] 禁用了附件:附件ID([details])', '1', '1480253267', '1480253340'); +INSERT INTO `dp_admin_action` VALUES ('14', 'user', 'attachment_delete', '删除附件', '删除附件', '', '[user|get_nickname] 删除了附件:附件ID([details])', '1', '1480253323', '1480253323'); +INSERT INTO `dp_admin_action` VALUES ('15', 'admin', 'config_add', '添加配置', '添加配置', '', '[user|get_nickname] 添加了配置,[details]', '1', '1480296196', '1480296196'); +INSERT INTO `dp_admin_action` VALUES ('16', 'admin', 'config_edit', '编辑配置', '编辑配置', '', '[user|get_nickname] 编辑了配置:[details]', '1', '1480296960', '1480296960'); +INSERT INTO `dp_admin_action` VALUES ('17', 'admin', 'config_enable', '启用配置', '启用配置', '', '[user|get_nickname] 启用了配置:[details]', '1', '1480298479', '1480298479'); +INSERT INTO `dp_admin_action` VALUES ('18', 'admin', 'config_disable', '禁用配置', '禁用配置', '', '[user|get_nickname] 禁用了配置:[details]', '1', '1480298506', '1480298506'); +INSERT INTO `dp_admin_action` VALUES ('19', 'admin', 'config_delete', '删除配置', '删除配置', '', '[user|get_nickname] 删除了配置:[details]', '1', '1480298532', '1480298532'); +INSERT INTO `dp_admin_action` VALUES ('20', 'admin', 'database_export', '备份数据库', '备份数据库', '', '[user|get_nickname] 备份了数据库:[details]', '1', '1480298946', '1480298946'); +INSERT INTO `dp_admin_action` VALUES ('21', 'admin', 'database_import', '还原数据库', '还原数据库', '', '[user|get_nickname] 还原了数据库:[details]', '1', '1480301990', '1480302022'); +INSERT INTO `dp_admin_action` VALUES ('22', 'admin', 'database_optimize', '优化数据表', '优化数据表', '', '[user|get_nickname] 优化了数据表:[details]', '1', '1480302616', '1480302616'); +INSERT INTO `dp_admin_action` VALUES ('23', 'admin', 'database_repair', '修复数据表', '修复数据表', '', '[user|get_nickname] 修复了数据表:[details]', '1', '1480302798', '1480302798'); +INSERT INTO `dp_admin_action` VALUES ('24', 'admin', 'database_backup_delete', '删除数据库备份', '删除数据库备份', '', '[user|get_nickname] 删除了数据库备份:[details]', '1', '1480302870', '1480302870'); +INSERT INTO `dp_admin_action` VALUES ('25', 'admin', 'hook_add', '添加钩子', '添加钩子', '', '[user|get_nickname] 添加了钩子:[details]', '1', '1480303198', '1480303198'); +INSERT INTO `dp_admin_action` VALUES ('26', 'admin', 'hook_edit', '编辑钩子', '编辑钩子', '', '[user|get_nickname] 编辑了钩子:[details]', '1', '1480303229', '1480303229'); +INSERT INTO `dp_admin_action` VALUES ('27', 'admin', 'hook_delete', '删除钩子', '删除钩子', '', '[user|get_nickname] 删除了钩子:[details]', '1', '1480303264', '1480303264'); +INSERT INTO `dp_admin_action` VALUES ('28', 'admin', 'hook_enable', '启用钩子', '启用钩子', '', '[user|get_nickname] 启用了钩子:[details]', '1', '1480303294', '1480303294'); +INSERT INTO `dp_admin_action` VALUES ('29', 'admin', 'hook_disable', '禁用钩子', '禁用钩子', '', '[user|get_nickname] 禁用了钩子:[details]', '1', '1480303409', '1480303409'); +INSERT INTO `dp_admin_action` VALUES ('30', 'admin', 'menu_add', '添加节点', '添加节点', '', '[user|get_nickname] 添加了节点:[details]', '1', '1480305468', '1480305468'); +INSERT INTO `dp_admin_action` VALUES ('31', 'admin', 'menu_edit', '编辑节点', '编辑节点', '', '[user|get_nickname] 编辑了节点:[details]', '1', '1480305513', '1480305513'); +INSERT INTO `dp_admin_action` VALUES ('32', 'admin', 'menu_delete', '删除节点', '删除节点', '', '[user|get_nickname] 删除了节点:[details]', '1', '1480305562', '1480305562'); +INSERT INTO `dp_admin_action` VALUES ('33', 'admin', 'menu_enable', '启用节点', '启用节点', '', '[user|get_nickname] 启用了节点:[details]', '1', '1480305630', '1480305630'); +INSERT INTO `dp_admin_action` VALUES ('34', 'admin', 'menu_disable', '禁用节点', '禁用节点', '', '[user|get_nickname] 禁用了节点:[details]', '1', '1480305659', '1480305659'); +INSERT INTO `dp_admin_action` VALUES ('35', 'admin', 'module_install', '安装模块', '安装模块', '', '[user|get_nickname] 安装了模块:[details]', '1', '1480307558', '1480307558'); +INSERT INTO `dp_admin_action` VALUES ('36', 'admin', 'module_uninstall', '卸载模块', '卸载模块', '', '[user|get_nickname] 卸载了模块:[details]', '1', '1480307588', '1480307588'); +INSERT INTO `dp_admin_action` VALUES ('37', 'admin', 'module_enable', '启用模块', '启用模块', '', '[user|get_nickname] 启用了模块:[details]', '1', '1480307618', '1480307618'); +INSERT INTO `dp_admin_action` VALUES ('38', 'admin', 'module_disable', '禁用模块', '禁用模块', '', '[user|get_nickname] 禁用了模块:[details]', '1', '1480307653', '1480307653'); +INSERT INTO `dp_admin_action` VALUES ('39', 'admin', 'module_export', '导出模块', '导出模块', '', '[user|get_nickname] 导出了模块:[details]', '1', '1480307682', '1480307682'); +INSERT INTO `dp_admin_action` VALUES ('40', 'admin', 'packet_install', '安装数据包', '安装数据包', '', '[user|get_nickname] 安装了数据包:[details]', '1', '1480308342', '1480308342'); +INSERT INTO `dp_admin_action` VALUES ('41', 'admin', 'packet_uninstall', '卸载数据包', '卸载数据包', '', '[user|get_nickname] 卸载了数据包:[details]', '1', '1480308372', '1480308372'); +INSERT INTO `dp_admin_action` VALUES ('42', 'admin', 'system_config_update', '更新系统设置', '更新系统设置', '', '[user|get_nickname] 更新了系统设置:[details]', '1', '1480309555', '1480309642'); + +-- ---------------------------- +-- Table structure for `dp_admin_attachment` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_attachment`; +CREATE TABLE `dp_admin_attachment` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `uid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '用户id', + `name` varchar(255) NOT NULL DEFAULT '' COMMENT '文件名', + `module` varchar(32) NOT NULL DEFAULT '' COMMENT '模块名,由哪个模块上传的', + `path` varchar(255) NOT NULL DEFAULT '' COMMENT '文件路径', + `thumb` varchar(255) NOT NULL DEFAULT '' COMMENT '缩略图路径', + `url` varchar(255) NOT NULL DEFAULT '' COMMENT '文件链接', + `mime` varchar(128) NOT NULL DEFAULT '' COMMENT '文件mime类型', + `ext` char(8) NOT NULL DEFAULT '' COMMENT '文件类型', + `size` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '文件大小', + `md5` char(32) NOT NULL DEFAULT '' COMMENT '文件md5', + `sha1` char(40) NOT NULL DEFAULT '' COMMENT 'sha1 散列值', + `driver` varchar(16) NOT NULL DEFAULT 'local' COMMENT '上传驱动', + `download` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '下载次数', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '上传时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `sort` int(11) NOT NULL DEFAULT '100' COMMENT '排序', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + `width` int(8) unsigned NOT NULL DEFAULT '0' COMMENT '图片宽度', + `height` int(8) unsigned NOT NULL DEFAULT '0' COMMENT '图片高度', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='附件表'; + +-- ---------------------------- +-- Records of dp_admin_attachment +-- ---------------------------- + +-- ---------------------------- +-- Table structure for `dp_admin_config` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_config`; +CREATE TABLE `dp_admin_config` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL DEFAULT '' COMMENT '名称', + `title` varchar(32) NOT NULL DEFAULT '' COMMENT '标题', + `group` varchar(32) NOT NULL DEFAULT '' COMMENT '配置分组', + `type` varchar(32) NOT NULL DEFAULT '' COMMENT '类型', + `value` text NOT NULL COMMENT '配置值', + `options` text NOT NULL COMMENT '配置项', + `tips` varchar(256) NOT NULL DEFAULT '' COMMENT '配置提示', + `ajax_url` varchar(256) NOT NULL DEFAULT '' COMMENT '联动下拉框ajax地址', + `next_items` varchar(256) NOT NULL DEFAULT '' COMMENT '联动下拉框的下级下拉框名,多个以逗号隔开', + `param` varchar(32) NOT NULL DEFAULT '' COMMENT '联动下拉框请求参数名', + `format` varchar(32) NOT NULL DEFAULT '' COMMENT '格式,用于格式文本', + `table` varchar(32) NOT NULL DEFAULT '' COMMENT '表名,只用于快速联动类型', + `level` tinyint(2) unsigned NOT NULL DEFAULT '2' COMMENT '联动级别,只用于快速联动类型', + `key` varchar(32) NOT NULL DEFAULT '' COMMENT '键字段,只用于快速联动类型', + `option` varchar(32) NOT NULL DEFAULT '' COMMENT '值字段,只用于快速联动类型', + `pid` varchar(32) NOT NULL DEFAULT '' COMMENT '父级id字段,只用于快速联动类型', + `ak` varchar(32) NOT NULL DEFAULT '' COMMENT '百度地图appkey', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `sort` int(11) NOT NULL DEFAULT '100' COMMENT '排序', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态:0禁用,1启用', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=37 DEFAULT CHARSET=utf8 COMMENT='系统配置表'; + +-- ---------------------------- +-- Records of dp_admin_config +-- ---------------------------- +INSERT INTO `dp_admin_config` VALUES ('1', 'web_site_status', '站点开关', 'base', 'switch', '1', '', '站点关闭后将不能访问,后台可正常登录', '', '', '', '', '', '2', '', '', '', '', '1475240395', '1477403914', '1', '1'); +INSERT INTO `dp_admin_config` VALUES ('2', 'web_site_title', '站点标题', 'base', 'text', '海豚PHP', '', '调用方式:config(\'web_site_title\')', '', '', '', '', '', '2', '', '', '', '', '1475240646', '1477710341', '2', '1'); +INSERT INTO `dp_admin_config` VALUES ('3', 'web_site_slogan', '站点标语', 'base', 'text', '海豚PHP,极简、极速、极致', '', '站点口号,调用方式:config(\'web_site_slogan\')', '', '', '', '', '', '2', '', '', '', '', '1475240994', '1477710357', '3', '1'); +INSERT INTO `dp_admin_config` VALUES ('4', 'web_site_logo', '站点LOGO', 'base', 'image', '', '', '', '', '', '', '', '', '2', '', '', '', '', '1475241067', '1475241067', '4', '1'); +INSERT INTO `dp_admin_config` VALUES ('5', 'web_site_description', '站点描述', 'base', 'textarea', '', '', '网站描述,有利于搜索引擎抓取相关信息', '', '', '', '', '', '2', '', '', '', '', '1475241186', '1475241186', '6', '1'); +INSERT INTO `dp_admin_config` VALUES ('6', 'web_site_keywords', '站点关键词', 'base', 'text', '海豚PHP、PHP开发框架、后台框架', '', '网站搜索引擎关键字', '', '', '', '', '', '2', '', '', '', '', '1475241328', '1475241328', '7', '1'); +INSERT INTO `dp_admin_config` VALUES ('7', 'web_site_copyright', '版权信息', 'base', 'text', 'Copyright © 2015-2017 DolphinPHP All rights reserved.', '', '调用方式:config(\'web_site_copyright\')', '', '', '', '', '', '2', '', '', '', '', '1475241416', '1477710383', '8', '1'); +INSERT INTO `dp_admin_config` VALUES ('8', 'web_site_icp', '备案信息', 'base', 'text', '', '', '调用方式:config(\'web_site_icp\')', '', '', '', '', '', '2', '', '', '', '', '1475241441', '1477710441', '9', '1'); +INSERT INTO `dp_admin_config` VALUES ('9', 'web_site_statistics', '站点统计', 'base', 'textarea', '', '', '网站统计代码,支持百度、Google、cnzz等,调用方式:config(\'web_site_statistics\')', '', '', '', '', '', '2', '', '', '', '', '1475241498', '1477710455', '10', '1'); +INSERT INTO `dp_admin_config` VALUES ('10', 'config_group', '配置分组', 'system', 'array', 'base:基本\r\nsystem:系统\r\nupload:上传\r\ndevelop:开发\r\ndatabase:数据库', '', '', '', '', '', '', '', '2', '', '', '', '', '1475241716', '1477649446', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('11', 'form_item_type', '配置类型', 'system', 'array', 'text:单行文本\r\ntextarea:多行文本\r\nstatic:静态文本\r\npassword:密码\r\ncheckbox:复选框\r\nradio:单选按钮\r\ndate:日期\r\ndatetime:日期+时间\r\nhidden:隐藏\r\nswitch:开关\r\narray:数组\r\nselect:下拉框\r\nlinkage:普通联动下拉框\r\nlinkages:快速联动下拉框\r\nimage:单张图片\r\nimages:多张图片\r\nfile:单个文件\r\nfiles:多个文件\r\nueditor:UEditor 编辑器\r\nwangeditor:wangEditor 编辑器\r\neditormd:markdown 编辑器\r\nckeditor:ckeditor 编辑器\r\nicon:字体图标\r\ntags:标签\r\nnumber:数字\r\nbmap:百度地图\r\ncolorpicker:取色器\r\njcrop:图片裁剪\r\nmasked:格式文本\r\nrange:范围\r\ntime:时间', '', '', '', '', '', '', '', '2', '', '', '', '', '1475241835', '1495853193', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('12', 'upload_file_size', '文件上传大小限制', 'upload', 'text', '0', '', '0为不限制大小,单位:kb', '', '', '', '', '', '2', '', '', '', '', '1475241897', '1477663520', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('13', 'upload_file_ext', '允许上传的文件后缀', 'upload', 'tags', 'doc,docx,xls,xlsx,ppt,pptx,pdf,wps,txt,rar,zip,gz,bz2,7z', '', '多个后缀用逗号隔开,不填写则不限制类型', '', '', '', '', '', '2', '', '', '', '', '1475241975', '1477649489', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('14', 'upload_image_size', '图片上传大小限制', 'upload', 'text', '0', '', '0为不限制大小,单位:kb', '', '', '', '', '', '2', '', '', '', '', '1475242015', '1477663529', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('15', 'upload_image_ext', '允许上传的图片后缀', 'upload', 'tags', 'gif,jpg,jpeg,bmp,png', '', '多个后缀用逗号隔开,不填写则不限制类型', '', '', '', '', '', '2', '', '', '', '', '1475242056', '1477649506', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('16', 'list_rows', '分页数量', 'system', 'number', '20', '', '每页的记录数', '', '', '', '', '', '2', '', '', '', '', '1475242066', '1476074507', '101', '1'); +INSERT INTO `dp_admin_config` VALUES ('17', 'system_color', '后台配色方案', 'system', 'radio', 'default', 'default:Default\r\namethyst:Amethyst\r\ncity:City\r\nflat:Flat\r\nmodern:Modern\r\nsmooth:Smooth', '', '', '', '', '', '', '2', '', '', '', '', '1475250066', '1477316689', '102', '1'); +INSERT INTO `dp_admin_config` VALUES ('18', 'develop_mode', '开发模式', 'develop', 'radio', '1', '0:关闭\r\n1:开启', '', '', '', '', '', '', '2', '', '', '', '', '1476864205', '1476864231', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('19', 'app_trace', '显示页面Trace', 'develop', 'radio', '0', '0:否\r\n1:是', '', '', '', '', '', '', '2', '', '', '', '', '1476866355', '1476866355', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('21', 'data_backup_path', '数据库备份根路径', 'database', 'text', '../data/', '', '路径必须以 / 结尾', '', '', '', '', '', '2', '', '', '', '', '1477017745', '1477018467', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('22', 'data_backup_part_size', '数据库备份卷大小', 'database', 'text', '20971520', '', '该值用于限制压缩后的分卷最大长度。单位:B;建议设置20M', '', '', '', '', '', '2', '', '', '', '', '1477017886', '1477017886', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('23', 'data_backup_compress', '数据库备份文件是否启用压缩', 'database', 'radio', '1', '0:否\r\n1:是', '压缩备份文件需要PHP环境支持 gzopen, gzwrite函数', '', '', '', '', '', '2', '', '', '', '', '1477017978', '1477018172', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('24', 'data_backup_compress_level', '数据库备份文件压缩级别', 'database', 'radio', '9', '1:最低\r\n4:一般\r\n9:最高', '数据库备份文件的压缩级别,该配置在开启压缩时生效', '', '', '', '', '', '2', '', '', '', '', '1477018083', '1477018083', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('25', 'top_menu_max', '顶部导航模块数量', 'system', 'text', '10', '', '设置顶部导航默认显示的模块数量', '', '', '', '', '', '2', '', '', '', '', '1477579289', '1477579289', '103', '1'); +INSERT INTO `dp_admin_config` VALUES ('26', 'web_site_logo_text', '站点LOGO文字', 'base', 'image', '', '', '', '', '', '', '', '', '2', '', '', '', '', '1477620643', '1477620643', '5', '1'); +INSERT INTO `dp_admin_config` VALUES ('27', 'upload_image_thumb', '缩略图尺寸', 'upload', 'text', '', '', '不填写则不生成缩略图,如需生成 300x300 的缩略图,则填写 300,300 ,请注意,逗号必须是英文逗号', '', '', '', '', '', '2', '', '', '', '', '1477644150', '1477649513', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('28', 'upload_image_thumb_type', '缩略图裁剪类型', 'upload', 'radio', '1', '1:等比例缩放\r\n2:缩放后填充\r\n3:居中裁剪\r\n4:左上角裁剪\r\n5:右下角裁剪\r\n6:固定尺寸缩放', '该项配置只有在启用生成缩略图时才生效', '', '', '', '', '', '2', '', '', '', '', '1477646271', '1477649521', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('29', 'upload_thumb_water', '添加水印', 'upload', 'switch', '0', '', '', '', '', '', '', '', '2', '', '', '', '', '1477649648', '1477649648', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('30', 'upload_thumb_water_pic', '水印图片', 'upload', 'image', '', '', '只有开启水印功能才生效', '', '', '', '', '', '2', '', '', '', '', '1477656390', '1477656390', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('31', 'upload_thumb_water_position', '水印位置', 'upload', 'radio', '9', '1:左上角\r\n2:上居中\r\n3:右上角\r\n4:左居中\r\n5:居中\r\n6:右居中\r\n7:左下角\r\n8:下居中\r\n9:右下角', '只有开启水印功能才生效', '', '', '', '', '', '2', '', '', '', '', '1477656528', '1477656528', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('32', 'upload_thumb_water_alpha', '水印透明度', 'upload', 'text', '50', '', '请输入0~100之间的数字,数字越小,透明度越高', '', '', '', '', '', '2', '', '', '', '', '1477656714', '1477661309', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('33', 'wipe_cache_type', '清除缓存类型', 'system', 'checkbox', 'TEMP_PATH', 'TEMP_PATH:应用缓存\r\nLOG_PATH:应用日志\r\nCACHE_PATH:项目模板缓存', '清除缓存时,要删除的缓存类型', '', '', '', '', '', '2', '', '', '', '', '1477727305', '1477727305', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('34', 'captcha_signin', '后台验证码开关', 'system', 'switch', '0', '', '后台登录时是否需要验证码', '', '', '', '', '', '2', '', '', '', '', '1478771958', '1478771958', '99', '1'); +INSERT INTO `dp_admin_config` VALUES ('35', 'home_default_module', '前台默认模块', 'system', 'select', 'index', '', '前台默认访问的模块,该模块必须有Index控制器和index方法', '', '', '', '', '', '0', '', '', '', '', '1486714723', '1486715620', '104', '1'); +INSERT INTO `dp_admin_config` VALUES ('36', 'minify_status', '开启minify', 'system', 'switch', '0', '', '开启minify会压缩合并js、css文件,可以减少资源请求次数,如果不支持minify,可关闭', '', '', '', '', '', '0', '', '', '', '', '1487035843', '1487035843', '99', '1'); +INSERT INTO `dp_admin_config` VALUES ('37', 'upload_driver', '上传驱动', 'upload', 'radio', 'local', 'local:本地', '图片或文件上传驱动', '', '', '', '', '', '0', '', '', '', '', '1501488567', '1501490821', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('38', 'system_log', '系统日志', 'system', 'switch', '1', '', '是否开启系统日志功能', '', '', '', '', '', '0', '', '', '', '', '1512635391', '1512635391', '99', '1'); +INSERT INTO `dp_admin_config` VALUES ('39', 'asset_version', '资源版本号', 'develop', 'text', '20180327', '', '可通过修改版号强制用户更新静态文件', '', '', '', '', '', '0', '', '', '', '', '1522143239', '1522143239', '100', '1'); + +-- ---------------------------- +-- Table structure for `dp_admin_hook` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_hook`; +CREATE TABLE `dp_admin_hook` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '钩子名称', + `plugin` varchar(32) NOT NULL DEFAULT '' COMMENT '钩子来自哪个插件', + `description` varchar(255) NOT NULL DEFAULT '' COMMENT '钩子描述', + `system` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '是否为系统钩子', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COMMENT='钩子表'; + +-- ---------------------------- +-- Records of dp_admin_hook +-- ---------------------------- +INSERT INTO `dp_admin_hook` VALUES ('1', 'admin_index', '', '后台首页', '1', '1468174214', '1477757518', '1'); +INSERT INTO `dp_admin_hook` VALUES ('2', 'plugin_index_tab_list', '', '插件扩展tab钩子', '1', '1468174214', '1468174214', '1'); +INSERT INTO `dp_admin_hook` VALUES ('3', 'module_index_tab_list', '', '模块扩展tab钩子', '1', '1468174214', '1468174214', '1'); +INSERT INTO `dp_admin_hook` VALUES ('4', 'page_tips', '', '每个页面的提示', '1', '1468174214', '1468174214', '1'); +INSERT INTO `dp_admin_hook` VALUES ('5', 'signin_footer', '', '登录页面底部钩子', '1', '1479269315', '1479269315', '1'); +INSERT INTO `dp_admin_hook` VALUES ('6', 'signin_captcha', '', '登录页面验证码钩子', '1', '1479269315', '1479269315', '1'); +INSERT INTO `dp_admin_hook` VALUES ('7', 'signin', '', '登录控制器钩子', '1', '1479386875', '1479386875', '1'); +INSERT INTO `dp_admin_hook` VALUES ('8', 'upload_attachment', '', '附件上传钩子', '1', '1501493808', '1501493808', '1'); +INSERT INTO `dp_admin_hook` VALUES ('9', 'page_plugin_js', '', '页面插件js钩子', '1', '1503633591', '1503633591', '1'); +INSERT INTO `dp_admin_hook` VALUES ('10', 'page_plugin_css', '', '页面插件css钩子', '1', '1503633591', '1503633591', '1'); +INSERT INTO `dp_admin_hook` VALUES ('11', 'signin_sso', '', '单点登录钩子', '1', '1503633591', '1503633591', '1'); +INSERT INTO `dp_admin_hook` VALUES ('12', 'signout_sso', '', '单点退出钩子', '1', '1503633591', '1503633591', '1'); +INSERT INTO `dp_admin_hook` VALUES ('13', 'user_add', '', '添加用户钩子', '1', '1503633591', '1503633591', '1'); +INSERT INTO `dp_admin_hook` VALUES ('14', 'user_edit', '', '编辑用户钩子', '1', '1503633591', '1503633591', '1'); +INSERT INTO `dp_admin_hook` VALUES ('15', 'user_delete', '', '删除用户钩子', '1', '1503633591', '1503633591', '1'); +INSERT INTO `dp_admin_hook` VALUES ('16', 'user_enable', '', '启用用户钩子', '1', '1503633591', '1503633591', '1'); +INSERT INTO `dp_admin_hook` VALUES ('17', 'user_disable', '', '禁用用户钩子', '1', '1503633591', '1503633591', '1'); + +-- ---------------------------- +-- Table structure for `dp_admin_hook_plugin` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_hook_plugin`; +CREATE TABLE `dp_admin_hook_plugin` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `hook` varchar(32) NOT NULL DEFAULT '' COMMENT '钩子id', + `plugin` varchar(32) NOT NULL DEFAULT '' COMMENT '插件标识', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '添加时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `sort` int(11) unsigned NOT NULL DEFAULT '100' COMMENT '排序', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='钩子-插件对应表'; + +-- ---------------------------- +-- Records of dp_admin_hook_plugin +-- ---------------------------- +INSERT INTO `dp_admin_hook_plugin` VALUES ('1', 'admin_index', 'SystemInfo', '1477757503', '1477757503', '1', '1'); +INSERT INTO `dp_admin_hook_plugin` VALUES ('2', 'admin_index', 'DevTeam', '1477755780', '1477755780', '2', '1'); + +-- ---------------------------- +-- Table structure for dp_admin_icon +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_icon`; +CREATE TABLE `dp_admin_icon` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '图标名称', + `url` varchar(255) NOT NULL DEFAULT '' COMMENT '图标css地址', + `prefix` varchar(32) NOT NULL DEFAULT '' COMMENT '图标前缀', + `font_family` varchar(32) NOT NULL DEFAULT '' COMMENT '字体名', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `status` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='图标表'; + +-- ---------------------------- +-- Records of dp_admin_icon +-- ---------------------------- + +-- ---------------------------- +-- Table structure for dp_admin_icon_list +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_icon_list`; +CREATE TABLE `dp_admin_icon_list` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `icon_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '所属图标id', + `title` varchar(128) NOT NULL DEFAULT '' COMMENT '图标标题', + `class` varchar(255) NOT NULL DEFAULT '' COMMENT '图标类名', + `code` varchar(128) NOT NULL DEFAULT '' COMMENT '图标关键词', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='详细图标列表'; + +-- ---------------------------- +-- Records of dp_admin_icon_list +-- ---------------------------- + +-- ---------------------------- +-- Table structure for `dp_admin_log` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_log`; +CREATE TABLE `dp_admin_log` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', + `action_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '行为id', + `user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '执行用户id', + `action_ip` bigint(20) NOT NULL COMMENT '执行行为者ip', + `model` varchar(50) NOT NULL DEFAULT '' COMMENT '触发行为的表', + `record_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '触发行为的数据id', + `remark` longtext NOT NULL COMMENT '日志备注', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '执行行为的时间', + PRIMARY KEY (`id`), + KEY `action_ip_ix` (`action_ip`), + KEY `action_id_ix` (`action_id`), + KEY `user_id_ix` (`user_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 ROW_FORMAT=FIXED COMMENT='行为日志表'; + +-- ---------------------------- +-- Records of dp_admin_log +-- ---------------------------- + +-- ---------------------------- +-- Table structure for `dp_admin_menu` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_menu`; +CREATE TABLE `dp_admin_menu` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `pid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '上级菜单id', + `module` varchar(16) NOT NULL DEFAULT '' COMMENT '模块名称', + `title` varchar(32) NOT NULL DEFAULT '' COMMENT '菜单标题', + `icon` varchar(64) NOT NULL DEFAULT '' COMMENT '菜单图标', + `url_type` varchar(16) NOT NULL DEFAULT '' COMMENT '链接类型(link:外链,module:模块)', + `url_value` varchar(255) NOT NULL DEFAULT '' COMMENT '链接地址', + `url_target` varchar(16) NOT NULL DEFAULT '_self' COMMENT '链接打开方式:_blank,_self', + `online_hide` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '网站上线后是否隐藏', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `sort` int(11) NOT NULL DEFAULT '100' COMMENT '排序', + `system_menu` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '是否为系统菜单,系统菜单不可删除', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + `params` varchar(255) NOT NULL DEFAULT '' COMMENT '参数', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=214 DEFAULT CHARSET=utf8 COMMENT='后台菜单表'; + +-- ---------------------------- +-- Records of dp_admin_menu +-- ---------------------------- +INSERT INTO `dp_admin_menu` VALUES ('1', '0', 'admin', '首页', 'fa fa-fw fa-home', 'module_admin', 'admin/index/index', '_self', '0', '1467617722', '1477710540', '1', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('2', '1', 'admin', '快捷操作', 'fa fa-fw fa-folder-open-o', 'module_admin', '', '_self', '0', '1467618170', '1477710695', '1', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('3', '2', 'admin', '清空缓存', 'fa fa-fw fa-trash-o', 'module_admin', 'admin/index/wipecache', '_self', '0', '1467618273', '1489049773', '3', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('4', '0', 'admin', '系统', 'fa fa-fw fa-gear', 'module_admin', 'admin/system/index', '_self', '0', '1467618361', '1477710540', '2', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('5', '4', 'admin', '系统功能', 'si si-wrench', 'module_admin', '', '_self', '0', '1467618441', '1477710695', '1', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('6', '5', 'admin', '系统设置', 'fa fa-fw fa-wrench', 'module_admin', 'admin/system/index', '_self', '0', '1467618490', '1477710695', '1', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('7', '5', 'admin', '配置管理', 'fa fa-fw fa-gears', 'module_admin', 'admin/config/index', '_self', '0', '1467618618', '1477710695', '2', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('8', '7', 'admin', '新增', '', 'module_admin', 'admin/config/add', '_self', '0', '1467618648', '1477710695', '1', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('9', '7', 'admin', '编辑', '', 'module_admin', 'admin/config/edit', '_self', '0', '1467619566', '1477710695', '2', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('10', '7', 'admin', '删除', '', 'module_admin', 'admin/config/delete', '_self', '0', '1467619583', '1477710695', '3', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('11', '7', 'admin', '启用', '', 'module_admin', 'admin/config/enable', '_self', '0', '1467619609', '1477710695', '4', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('12', '7', 'admin', '禁用', '', 'module_admin', 'admin/config/disable', '_self', '0', '1467619637', '1477710695', '5', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('13', '5', 'admin', '节点管理', 'fa fa-fw fa-bars', 'module_admin', 'admin/menu/index', '_self', '0', '1467619882', '1477710695', '3', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('14', '13', 'admin', '新增', '', 'module_admin', 'admin/menu/add', '_self', '0', '1467619902', '1477710695', '1', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('15', '13', 'admin', '编辑', '', 'module_admin', 'admin/menu/edit', '_self', '0', '1467620331', '1477710695', '2', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('16', '13', 'admin', '删除', '', 'module_admin', 'admin/menu/delete', '_self', '0', '1467620363', '1477710695', '3', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('17', '13', 'admin', '启用', '', 'module_admin', 'admin/menu/enable', '_self', '0', '1467620386', '1477710695', '4', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('18', '13', 'admin', '禁用', '', 'module_admin', 'admin/menu/disable', '_self', '0', '1467620404', '1477710695', '5', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('19', '68', 'user', '权限管理', 'fa fa-fw fa-key', 'module_admin', '', '_self', '0', '1467688065', '1477710702', '1', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('20', '19', 'user', '用户管理', 'fa fa-fw fa-user', 'module_admin', 'user/index/index', '_self', '0', '1467688137', '1477710702', '1', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('21', '20', 'user', '新增', '', 'module_admin', 'user/index/add', '_self', '0', '1467688177', '1477710702', '1', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('22', '20', 'user', '编辑', '', 'module_admin', 'user/index/edit', '_self', '0', '1467688202', '1477710702', '2', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('23', '20', 'user', '删除', '', 'module_admin', 'user/index/delete', '_self', '0', '1467688219', '1477710702', '3', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('24', '20', 'user', '启用', '', 'module_admin', 'user/index/enable', '_self', '0', '1467688238', '1477710702', '4', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('25', '20', 'user', '禁用', '', 'module_admin', 'user/index/disable', '_self', '0', '1467688256', '1477710702', '5', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('211', '64', 'admin', '日志详情', '', 'module_admin', 'admin/log/details', '_self', '0', '1480299320', '1480299320', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('32', '4', 'admin', '扩展中心', 'si si-social-dropbox', 'module_admin', '', '_self', '0', '1467688853', '1477710695', '2', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('33', '32', 'admin', '模块管理', 'fa fa-fw fa-th-large', 'module_admin', 'admin/module/index', '_self', '0', '1467689008', '1477710695', '1', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('34', '33', 'admin', '导入', '', 'module_admin', 'admin/module/import', '_self', '0', '1467689153', '1477710695', '1', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('35', '33', 'admin', '导出', '', 'module_admin', 'admin/module/export', '_self', '0', '1467689173', '1477710695', '2', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('36', '33', 'admin', '安装', '', 'module_admin', 'admin/module/install', '_self', '0', '1467689192', '1477710695', '3', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('37', '33', 'admin', '卸载', '', 'module_admin', 'admin/module/uninstall', '_self', '0', '1467689241', '1477710695', '4', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('38', '33', 'admin', '启用', '', 'module_admin', 'admin/module/enable', '_self', '0', '1467689294', '1477710695', '5', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('39', '33', 'admin', '禁用', '', 'module_admin', 'admin/module/disable', '_self', '0', '1467689312', '1477710695', '6', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('40', '33', 'admin', '更新', '', 'module_admin', 'admin/module/update', '_self', '0', '1467689341', '1477710695', '7', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('41', '32', 'admin', '插件管理', 'fa fa-fw fa-puzzle-piece', 'module_admin', 'admin/plugin/index', '_self', '0', '1467689527', '1477710695', '2', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('42', '41', 'admin', '导入', '', 'module_admin', 'admin/plugin/import', '_self', '0', '1467689650', '1477710695', '1', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('43', '41', 'admin', '导出', '', 'module_admin', 'admin/plugin/export', '_self', '0', '1467689665', '1477710695', '2', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('44', '41', 'admin', '安装', '', 'module_admin', 'admin/plugin/install', '_self', '0', '1467689680', '1477710695', '3', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('45', '41', 'admin', '卸载', '', 'module_admin', 'admin/plugin/uninstall', '_self', '0', '1467689700', '1477710695', '4', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('46', '41', 'admin', '启用', '', 'module_admin', 'admin/plugin/enable', '_self', '0', '1467689730', '1477710695', '5', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('47', '41', 'admin', '禁用', '', 'module_admin', 'admin/plugin/disable', '_self', '0', '1467689747', '1477710695', '6', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('48', '41', 'admin', '设置', '', 'module_admin', 'admin/plugin/config', '_self', '0', '1467689789', '1477710695', '7', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('49', '41', 'admin', '管理', '', 'module_admin', 'admin/plugin/manage', '_self', '0', '1467689846', '1477710695', '8', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('50', '5', 'admin', '附件管理', 'fa fa-fw fa-cloud-upload', 'module_admin', 'admin/attachment/index', '_self', '0', '1467690161', '1477710695', '4', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('51', '70', 'admin', '文件上传', '', 'module_admin', 'admin/attachment/upload', '_self', '0', '1467690240', '1489049773', '1', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('52', '50', 'admin', '下载', '', 'module_admin', 'admin/attachment/download', '_self', '0', '1467690334', '1477710695', '2', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('53', '50', 'admin', '启用', '', 'module_admin', 'admin/attachment/enable', '_self', '0', '1467690352', '1477710695', '3', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('54', '50', 'admin', '禁用', '', 'module_admin', 'admin/attachment/disable', '_self', '0', '1467690369', '1477710695', '4', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('55', '50', 'admin', '删除', '', 'module_admin', 'admin/attachment/delete', '_self', '0', '1467690396', '1477710695', '5', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('56', '41', 'admin', '删除', '', 'module_admin', 'admin/plugin/delete', '_self', '0', '1467858065', '1477710695', '11', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('57', '41', 'admin', '编辑', '', 'module_admin', 'admin/plugin/edit', '_self', '0', '1467858092', '1477710695', '10', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('60', '41', 'admin', '新增', '', 'module_admin', 'admin/plugin/add', '_self', '0', '1467858421', '1477710695', '9', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('61', '41', 'admin', '执行', '', 'module_admin', 'admin/plugin/execute', '_self', '0', '1467879016', '1477710695', '14', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('62', '13', 'admin', '保存', '', 'module_admin', 'admin/menu/save', '_self', '0', '1468073039', '1477710695', '6', '1', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('64', '5', 'admin', '系统日志', 'fa fa-fw fa-book', 'module_admin', 'admin/log/index', '_self', '0', '1476111944', '1477710695', '6', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('65', '5', 'admin', '数据库管理', 'fa fa-fw fa-database', 'module_admin', 'admin/database/index', '_self', '0', '1476111992', '1477710695', '8', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('66', '32', 'admin', '数据包管理', 'fa fa-fw fa-database', 'module_admin', 'admin/packet/index', '_self', '0', '1476112326', '1477710695', '4', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('67', '19', 'user', '角色管理', 'fa fa-fw fa-users', 'module_admin', 'user/role/index', '_self', '0', '1476113025', '1477710702', '3', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('68', '0', 'user', '用户', 'fa fa-fw fa-user', 'module_admin', 'user/index/index', '_self', '0', '1476193348', '1477710540', '3', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('69', '32', 'admin', '钩子管理', 'fa fa-fw fa-anchor', 'module_admin', 'admin/hook/index', '_self', '0', '1476236193', '1477710695', '3', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('70', '2', 'admin', '后台首页', 'fa fa-fw fa-tachometer', 'module_admin', 'admin/index/index', '_self', '0', '1476237472', '1489049773', '1', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('71', '67', 'user', '新增', '', 'module_admin', 'user/role/add', '_self', '0', '1476256935', '1477710702', '1', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('72', '67', 'user', '编辑', '', 'module_admin', 'user/role/edit', '_self', '0', '1476256968', '1477710702', '2', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('73', '67', 'user', '删除', '', 'module_admin', 'user/role/delete', '_self', '0', '1476256993', '1477710702', '3', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('74', '67', 'user', '启用', '', 'module_admin', 'user/role/enable', '_self', '0', '1476257023', '1477710702', '4', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('75', '67', 'user', '禁用', '', 'module_admin', 'user/role/disable', '_self', '0', '1476257046', '1477710702', '5', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('76', '20', 'user', '授权', '', 'module_admin', 'user/index/access', '_self', '0', '1476375187', '1477710702', '6', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('77', '69', 'admin', '新增', '', 'module_admin', 'admin/hook/add', '_self', '0', '1476668971', '1477710695', '1', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('78', '69', 'admin', '编辑', '', 'module_admin', 'admin/hook/edit', '_self', '0', '1476669006', '1477710695', '2', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('79', '69', 'admin', '删除', '', 'module_admin', 'admin/hook/delete', '_self', '0', '1476669375', '1477710695', '3', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('80', '69', 'admin', '启用', '', 'module_admin', 'admin/hook/enable', '_self', '0', '1476669427', '1477710695', '4', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('81', '69', 'admin', '禁用', '', 'module_admin', 'admin/hook/disable', '_self', '0', '1476669564', '1477710695', '5', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('183', '66', 'admin', '安装', '', 'module_admin', 'admin/packet/install', '_self', '0', '1476851362', '1477710695', '1', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('184', '66', 'admin', '卸载', '', 'module_admin', 'admin/packet/uninstall', '_self', '0', '1476851382', '1477710695', '2', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('185', '5', 'admin', '行为管理', 'fa fa-fw fa-bug', 'module_admin', 'admin/action/index', '_self', '0', '1476882441', '1477710695', '7', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('186', '185', 'admin', '新增', '', 'module_admin', 'admin/action/add', '_self', '0', '1476884439', '1477710695', '1', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('187', '185', 'admin', '编辑', '', 'module_admin', 'admin/action/edit', '_self', '0', '1476884464', '1477710695', '2', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('188', '185', 'admin', '启用', '', 'module_admin', 'admin/action/enable', '_self', '0', '1476884493', '1477710695', '3', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('189', '185', 'admin', '禁用', '', 'module_admin', 'admin/action/disable', '_self', '0', '1476884534', '1477710695', '4', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('190', '185', 'admin', '删除', '', 'module_admin', 'admin/action/delete', '_self', '0', '1476884551', '1477710695', '5', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('191', '65', 'admin', '备份数据库', '', 'module_admin', 'admin/database/export', '_self', '0', '1476972746', '1477710695', '1', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('192', '65', 'admin', '还原数据库', '', 'module_admin', 'admin/database/import', '_self', '0', '1476972772', '1477710695', '2', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('193', '65', 'admin', '优化表', '', 'module_admin', 'admin/database/optimize', '_self', '0', '1476972800', '1477710695', '3', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('194', '65', 'admin', '修复表', '', 'module_admin', 'admin/database/repair', '_self', '0', '1476972825', '1477710695', '4', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('195', '65', 'admin', '删除备份', '', 'module_admin', 'admin/database/delete', '_self', '0', '1476973457', '1477710695', '5', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('210', '41', 'admin', '快速编辑', '', 'module_admin', 'admin/plugin/quickedit', '_self', '0', '1477713981', '1477713981', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('209', '185', 'admin', '快速编辑', '', 'module_admin', 'admin/action/quickedit', '_self', '0', '1477713939', '1477713939', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('208', '7', 'admin', '快速编辑', '', 'module_admin', 'admin/config/quickedit', '_self', '0', '1477713808', '1477713808', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('207', '69', 'admin', '快速编辑', '', 'module_admin', 'admin/hook/quickedit', '_self', '0', '1477713770', '1477713770', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('212', '2', 'admin', '个人设置', 'fa fa-fw fa-user', 'module_admin', 'admin/index/profile', '_self', '0', '1489049767', '1489049773', '2', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('213', '70', 'admin', '检查版本更新', '', 'module_admin', 'admin/index/checkupdate', '_self', '0', '1490588610', '1490588610', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('214', '68', 'user', '消息管理', 'fa fa-fw fa-comments-o', 'module_admin', '', '_self', '0', '1520492129', '1520492129', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('215', '214', 'user', '消息列表', 'fa fa-fw fa-th-list', 'module_admin', 'user/message/index', '_self', '0', '1520492195', '1520492195', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('216', '215', 'user', '新增', '', 'module_admin', 'user/message/add', '_self', '0', '1520492195', '1520492195', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('217', '215', 'user', '编辑', '', 'module_admin', 'user/message/edit', '_self', '0', '1520492195', '1520492195', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('218', '215', 'user', '删除', '', 'module_admin', 'user/message/delete', '_self', '0', '1520492195', '1520492195', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('219', '215', 'user', '启用', '', 'module_admin', 'user/message/enable', '_self', '0', '1520492195', '1520492195', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('220', '215', 'user', '禁用', '', 'module_admin', 'user/message/disable', '_self', '0', '1520492195', '1520492195', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('221', '215', 'user', '快速编辑', '', 'module_admin', 'user/message/quickedit', '_self', '0', '1520492195', '1520492195', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('222', '2', 'admin', '消息中心', 'fa fa-fw fa-comments-o', 'module_admin', 'admin/message/index', '_self', '0', '1520495992', '1520496254', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('223', '222', 'admin', '删除', '', 'module_admin', 'admin/message/delete', '_self', '0', '1520495992', '1520496263', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('224', '222', 'admin', '启用', '', 'module_admin', 'admin/message/enable', '_self', '0', '1520495992', '1520496270', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('225', '32', 'admin', '图标管理', 'fa fa-fw fa-tint', 'module_admin', 'admin/icon/index', '_self', '0', '1520908295', '1520908295', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('226', '225', 'admin', '新增', '', 'module_admin', 'admin/icon/add', '_self', '0', '1520908295', '1520908295', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('227', '225', 'admin', '编辑', '', 'module_admin', 'admin/icon/edit', '_self', '0', '1520908295', '1520908295', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('228', '225', 'admin', '删除', '', 'module_admin', 'admin/icon/delete', '_self', '0', '1520908295', '1520908295', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('229', '225', 'admin', '启用', '', 'module_admin', 'admin/icon/enable', '_self', '0', '1520908295', '1520908295', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('230', '225', 'admin', '禁用', '', 'module_admin', 'admin/icon/disable', '_self', '0', '1520908295', '1520908295', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('231', '225', 'admin', '快速编辑', '', 'module_admin', 'admin/icon/quickedit', '_self', '0', '1520908295', '1520908295', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('232', '225', 'admin', '图标列表', '', 'module_admin', 'admin/icon/items', '_self', '0', '1520923368', '1520923368', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('233', '225', 'admin', '更新图标', '', 'module_admin', 'admin/icon/reload', '_self', '0', '1520931908', '1520931908', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('234', '20', 'user', '快速编辑', '', 'module_admin', 'user/index/quickedit', '_self', '0', '1526028258', '1526028258', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('235', '67', 'user', '快速编辑', '', 'module_admin', 'user/role/quickedit', '_self', '0', '1526028282', '1526028282', '100', '0', '1', ''); +INSERT INTO `dp_admin_menu` VALUES ('236', '6', 'admin', '快速编辑', '', 'module_admin', 'admin/system/quickedit', '_self', '0', '1559054310', '1559054310', '100', '0', '1', ''); + +-- ---------------------------- +-- Table structure for dp_admin_message +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_message`; +CREATE TABLE `dp_admin_message` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `uid_receive` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '接收消息的用户id', + `uid_send` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '发送消息的用户id', + `type` varchar(128) NOT NULL DEFAULT '' COMMENT '消息分类', + `content` text NOT NULL COMMENT '消息内容', + `status` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `read_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '阅读时间', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='消息表'; + +-- ---------------------------- +-- Records of dp_admin_message +-- ---------------------------- + +-- ---------------------------- +-- Table structure for `dp_admin_module` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_module`; +CREATE TABLE `dp_admin_module` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '模块名称(标识)', + `title` varchar(32) NOT NULL DEFAULT '' COMMENT '模块标题', + `icon` varchar(64) NOT NULL DEFAULT '' COMMENT '图标', + `description` text NOT NULL COMMENT '描述', + `author` varchar(32) NOT NULL DEFAULT '' COMMENT '作者', + `author_url` varchar(255) NOT NULL DEFAULT '' COMMENT '作者主页', + `config` text NULL COMMENT '配置信息', + `access` text NULL COMMENT '授权配置', + `version` varchar(16) NOT NULL DEFAULT '' COMMENT '版本号', + `identifier` varchar(64) NOT NULL DEFAULT '' COMMENT '模块唯一标识符', + `system_module` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '是否为系统模块', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `sort` int(11) NOT NULL DEFAULT '100' COMMENT '排序', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='模块表'; + +-- ---------------------------- +-- Records of dp_admin_module +-- ---------------------------- +INSERT INTO `dp_admin_module` VALUES ('1', 'admin', '系统', 'fa fa-fw fa-gear', '系统模块,DolphinPHP的核心模块', 'DolphinPHP', 'http://www.dolphinphp.com', '', '', '1.0.0', 'admin.dolphinphp.module', '1', '1468204902', '1468204902', '100', '1'); +INSERT INTO `dp_admin_module` VALUES ('2', 'user', '用户', 'fa fa-fw fa-user', '用户模块,DolphinPHP自带模块', 'DolphinPHP', 'http://www.dolphinphp.com', '', '', '1.0.0', 'user.dolphinphp.module', '1', '1468204902', '1468204902', '100', '1'); + +-- ---------------------------- +-- Table structure for `dp_admin_packet` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_packet`; +CREATE TABLE `dp_admin_packet` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '数据包名', + `title` varchar(32) NOT NULL DEFAULT '' COMMENT '数据包标题', + `author` varchar(32) NOT NULL DEFAULT '' COMMENT '作者', + `author_url` varchar(255) NOT NULL DEFAULT '' COMMENT '作者url', + `version` varchar(16) NOT NULL, + `tables` text NOT NULL COMMENT '数据表名', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='数据包表'; + +-- ---------------------------- +-- Records of dp_admin_packet +-- ---------------------------- + +-- ---------------------------- +-- Table structure for `dp_admin_plugin` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_plugin`; +CREATE TABLE `dp_admin_plugin` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '插件名称', + `title` varchar(32) NOT NULL DEFAULT '' COMMENT '插件标题', + `icon` varchar(64) NOT NULL DEFAULT '' COMMENT '图标', + `description` text NOT NULL COMMENT '插件描述', + `author` varchar(32) NOT NULL DEFAULT '' COMMENT '作者', + `author_url` varchar(255) NOT NULL DEFAULT '' COMMENT '作者主页', + `config` text NOT NULL COMMENT '配置信息', + `version` varchar(16) NOT NULL DEFAULT '' COMMENT '版本号', + `identifier` varchar(64) NOT NULL DEFAULT '' COMMENT '插件唯一标识符', + `admin` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '是否有后台管理', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '安装时间', + `update_time` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间', + `sort` int(11) NOT NULL DEFAULT '100' COMMENT '排序', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='插件表'; + +-- ---------------------------- +-- Records of dp_admin_plugin +-- ---------------------------- +INSERT INTO `dp_admin_plugin` VALUES ('1', 'SystemInfo', '系统环境信息', 'fa fa-fw fa-info-circle', '在后台首页显示服务器信息', '蔡伟明', 'http://www.caiweiming.com', '{\"display\":\"1\",\"width\":\"6\"}', '1.0.0', 'system_info.ming.plugin', '0', '1477757503', '1477757503', '100', '1'); +INSERT INTO `dp_admin_plugin` VALUES ('2', 'DevTeam', '开发团队成员信息', 'fa fa-fw fa-users', '开发团队成员信息', '蔡伟明', 'http://www.caiweiming.com', '{\"display\":\"1\",\"width\":\"6\"}', '1.0.0', 'dev_team.ming.plugin', '0', '1477755780', '1477755780', '100', '1'); + +-- ---------------------------- +-- Table structure for `dp_admin_role` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_role`; +CREATE TABLE `dp_admin_role` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '角色id', + `pid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '上级角色', + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '角色名称', + `description` varchar(255) NOT NULL DEFAULT '' COMMENT '角色描述', + `menu_auth` text NOT NULL COMMENT '菜单权限', + `sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + `access` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '是否可登录后台', + `default_module` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '默认访问模块', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='角色表'; + +-- ---------------------------- +-- Records of dp_admin_role +-- ---------------------------- +INSERT INTO `dp_admin_role` VALUES ('1', '0', '超级管理员', '系统默认创建的角色,拥有最高权限', '', '0', '1476270000', '1468117612', '1', '1', '0'); + +-- ---------------------------- +-- Table structure for `dp_admin_user` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_user`; +CREATE TABLE `dp_admin_user` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `username` varchar(32) NOT NULL DEFAULT '' COMMENT '用户名', + `nickname` varchar(32) NOT NULL DEFAULT '' COMMENT '昵称', + `password` varchar(96) NOT NULL DEFAULT '' COMMENT '密码', + `email` varchar(64) NOT NULL DEFAULT '' COMMENT '邮箱地址', + `email_bind` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否绑定邮箱地址', + `mobile` varchar(11) NOT NULL DEFAULT '' COMMENT '手机号码', + `mobile_bind` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否绑定手机号码', + `avatar` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '头像', + `money` decimal(11,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '余额', + `score` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '积分', + `role` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '主角色ID', + `roles` varchar(255) NOT NULL DEFAULT '' COMMENT '副角色ID', + `group` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '部门id', + `signup_ip` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '注册ip', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `last_login_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '最后一次登录时间', + `last_login_ip` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '登录ip', + `sort` int(11) NOT NULL DEFAULT '100' COMMENT '排序', + `status` tinyint(2) NOT NULL DEFAULT '0' COMMENT '状态:0禁用,1启用', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='用户表'; + +-- ---------------------------- +-- Records of dp_admin_user +-- ---------------------------- +INSERT INTO `dp_admin_user` VALUES ('1', 'admin', '超级管理员', '$2y$10$Brw6wmuSLIIx3Yabid8/Wu5l8VQ9M/H/CG3C9RqN9dUCwZW3ljGOK', '', '0', '', '0', '0', '0.00', '0', '1', '', '0', '0', '1476065410', '1477794539', '1477794539', '2130706433', '100', '1'); diff --git a/application/install/view/index/complete.html b/application/install/view/index/complete.html new file mode 100644 index 0000000..8b5c776 --- /dev/null +++ b/application/install/view/index/complete.html @@ -0,0 +1,18 @@ +{extend name="layout" /} +{block name="progress"} +
    +
    +
    +{/block} +{block name="content"} +
    + {php}{/php} +
    + +
    +

    再次感谢您选择DolphinPHP,希望您会喜欢我们的产品!

    +

    后台默认账号/密码:admin

    + 登录后台 + 打开前台 +
    +{/block} \ No newline at end of file diff --git a/application/install/view/index/index.html b/application/install/view/index/index.html new file mode 100644 index 0000000..ea6f48b --- /dev/null +++ b/application/install/view/index/index.html @@ -0,0 +1,20 @@ +{extend name="layout" /} +{block name="progress"} +
    +
    +
    +{/block} +{block name="logo"}{/block} +{block name="content"} +
    +
    + + 用户协议 +
    +
    + +
    +
    +{/block} \ No newline at end of file diff --git a/application/install/view/index/step2.html b/application/install/view/index/step2.html new file mode 100644 index 0000000..2bbc1db --- /dev/null +++ b/application/install/view/index/step2.html @@ -0,0 +1,80 @@ +{extend name="layout" /} +{block name="progress"} +
    +
    +
    +{/block} +{block name="content"} +
    +
    + +
    +
    运行环境
    + + + + + + + + + + {volist name="env" id="item"} + + + + + + {/volist} + +
    项目所需配置当前配置
    {$item[0]}{$item[1]} + {$item[3]} +
    +
    目录/文件权限
    + + + + + + + + + + {volist name="dirfile" id="item"} + + + + + + {/volist} + +
    目录/文件所需状态当前状态
    {$item[3]}可写 + {$item[1]} +
    +
    函数及扩展依赖性
    + + + + + + + + + + {volist name="func" id="item"} + + + + + + {/volist} + +
    名称类型检查结果
    {$item[0]}{$item[3]} + {$item[1]} +
    + +
    +{/block} \ No newline at end of file diff --git a/application/install/view/index/step3.html b/application/install/view/index/step3.html new file mode 100644 index 0000000..6fba239 --- /dev/null +++ b/application/install/view/index/step3.html @@ -0,0 +1,60 @@ +{extend name="layout" /} +{block name="progress"} +
    +
    +
    +{/block} +{block name="content"} +
    +
    + +
    +
    +
    +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + 上一步 + +
    +
    +
    +
    +{/block} \ No newline at end of file diff --git a/application/install/view/index/step4.html b/application/install/view/index/step4.html new file mode 100644 index 0000000..781d6c6 --- /dev/null +++ b/application/install/view/index/step4.html @@ -0,0 +1,39 @@ +{extend name="layout" /} +{block name="progress"} +
    +
    +
    +{/block} +{block name="content"} +
    + +
    + 0% +
    正在安装...
    +
    +
    + + {php} + use think\Db; + // 连接数据库 + $db_config = session('db_config'); + $db_instance = Db::connect($db_config); + + // 创建数据表 + create_tables($db_instance, $db_config['prefix']); + + // 创建配置文件 + $conf = write_config($db_config); + + echo ""; + {/php} +{/block} \ No newline at end of file diff --git a/application/install/view/layout.html b/application/install/view/layout.html new file mode 100644 index 0000000..618a98a --- /dev/null +++ b/application/install/view/layout.html @@ -0,0 +1,96 @@ + + + + + + + 海豚PHP - 安装 + + + + + + + + + + + + + + + + + + + + +
    +{block name="progress"}{/block} + +
    + +
    + {block name="content"}{/block} +
    + +
    + + + +
    + 2016-2017 © DolphinPHP {:config('dolphin.product_version')} +
    + + + + + + + + + + + \ No newline at end of file diff --git a/application/member/admin/Index.php b/application/member/admin/Index.php new file mode 100644 index 0000000..fb4c6d8 --- /dev/null +++ b/application/member/admin/Index.php @@ -0,0 +1,599 @@ + + * @return mixed + */ + public function index(){ + //$this->error('功能开发中',url('shop/index/index')); + // 获取 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('is_vip desc'); + + //数据 + $data_list = MemberModel::where($map)->order($order)->paginate(); + foreach ($data_list as $k=>$v){ + if(empty($v['lastlogin_time'])){ + $data_list[$k]['lastlogin_time'] = '暂未登录'; + }else{ + $data_list[$k]['lastlogin_time'] = date('Y-m-d H:i',$v['lastlogin_time']); + } + + $data_list[$k]['invitation_uid'] = !empty($v['invitation_uid'])?MemberModel::where('id',$v['invitation_uid'])->value('nickname'):'暂无'; + } + // 分页数据 + $page = $data_list->render(); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['text', 'nickname', '昵称', 'like'], + ['text', 'realphone', '电话', 'like'], + ['daterange','create_time','注册时间','between'], + ]) + ->setPageTitle('列表') // 设置页面标题 + ->setTableName('member') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['is_delete','禁用','switch','',[1=>'是',0=>'否']], + ['vip_title', '用户角色','text'], + ['invitation_uid', '邀请用户','text'], + ['headimg', '头像','picture'], + ['nickname', '昵称','text'], + ['balance', '用户积分','text'], + ['realname', '真实姓名','text'], + ['realphone', '联系电话','text'], + ['wxskqrcode', '微信收款码','picture'], + ['create_time', '注册时间','datetime'], + //['right_button', '操作', 'btn'] + ]) + //->addTopButton('release',['class'=>'btn btn-primary','icon' => 'fa fa-plus-circle','title'=>'添加', 'href' => url('add')]) + //->addRightButton('edit',['title'=>'編輯','icon'=>'fa fa-pencil','href'=>url('edit',['id' => '__id__'])]) + //->addRightButton('delete') // 添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + /* ->setTabNav([ + 'tab1' => ['title' => '列表', 'url' => url('member/index/index', ['group' => 'tab1'])], + 'tab2' => ['title' => '添加', 'url' => url('member/index/add', ['group' => 'tab2'])], + ], 'tab1')*/ + ->fetch(); // 渲染模板 + } + + //删除 + public function delete($ids = ''){ + $post = $this->request->post(); + $result = MemberModel::where('id',$ids)->update(['is_delete'=>1,'delete_time'=>time()]); + if($result){ + $this->success('删除成功', url('index')); + }else{ + $this->error('删除失败'); + } + } + + + //余额记录 + public function balancelogs(){ + // 获取 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('id desc'); + + //数据 + $data_list = MemberBalanceLogsModel::where($map)->order($order)->paginate(); + if(!empty($data_list)){ + foreach ($data_list as $key=>$val){ + + $uinfo = MemberModel::where('id',$val['uid'])->find(); + + $data_list[$key]['nickname'] = $uinfo['nickname']."(".$uinfo['vip_title'].")"; + } + } + // 分页数据 + $page = $data_list->render(); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['select', 'uid', '所属用户', 'eq', '', MemberModel::where('is_delete',0)->column('id,nickname')], + ['daterange','create_time','时间','between'], + ]) + ->setPageTitle('列表') // 设置页面标题 + ->setTableName('member') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['nickname', '所属用户','text'], + ['category', '类型','text','',[1=>'增加',2=>'减少']], + ['mark', '备注','text'], + ['value', '积分','text'], + ['create_time', '时间','datetime'], + ]) + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } + + //用户余额提现 + public function balancewithdraw(){ + // 获取 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('status asc,create_time desc'); + + //数据 + $data_list = BalanceWithdrawModel::where($map)->order($order)->paginate(); + if(!empty($data_list)){ + foreach ($data_list as $key=>$val){ + + $uinfo = MemberModel::where('id',$val['uid'])->find(); + $data_list[$key]['nickname'] = $uinfo['nickname']."(".$uinfo['vip_title'].")"; + $data_list[$key]['wxskqrcode'] = $uinfo['wxskqrcode']; + } + } + // 分页数据 + $page = $data_list->render(); + + //提现积分 + $alljf = BalanceWithdrawModel::sum('integral_num'); + //提现金额 + $allje = BalanceWithdrawModel::sum('price'); + + $alljf1 = BalanceWithdrawModel::where('status',2)->sum('integral_num'); + //提现金额 + $allje2 = BalanceWithdrawModel::where('status',2)->sum('price'); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['select', 'uid', '所属用户', 'eq', '', MemberModel::where('is_delete',0)->column('id,nickname')], + ['select', 'status', '状态', 'eq', '', [1=>'待审核', 2=>'审核通过', 3=>'拒绝提现']], + ['daterange','create_time','申请时间','between'], + ]) + ->setPageTitle('申请列表') // 设置页面标题 + ->setPageTips("总提现积分:$alljf        兑换金额:$allje
    + 已同意提现积分:$alljf1        已同意兑换金额:$allje2") + ->setTableName('member') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['nickname', '所属用户','text'], + ['integral_num', '提现积分','text',], + ['price', '兑换金额','text',], + ['status', '状态','text','',[1=>'待审核', 2=>'审核通过', 3=>'拒绝提现']], + ['create_time', '申请时间','datetime'], + ['wxskqrcode', '微信收款码','picture'], + ['right_button', '操作', 'btn'] + ]) + ->addRightButton('edit',['title'=>'审核','icon'=>'fa fa-fw fa-pencil-square-o','href'=>url('operatebalancewithdraw',['id' => '__id__'])],['area' => ['600px', '40%']]) + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } + + public function operatebalancewithdraw($id=null){ + if ($id === null) return $this->error('缺少参数'); + $info = BalanceWithdrawModel::where('id',$id)->find(); + if($info['status']!=1){ + $this->error('此次提现申请已经处理,不能重复处理'); + } + + if($this->request->isPost()){ + $data = $this->request->post(); + if(!isset($data['status'])){ + $this->error('请选择状态'); + } + $result = BalanceWithdrawModel::update($data); + if($result){ + + if($data['status']==3){//拒绝提现,返还提现积分 + + $binfo = BalanceWithdrawModel::where('id',$data['id'])->find(); + $memberinfo = MemberModel::where('id',$binfo['uid'])->find(); + $memberinfo->balance = $memberinfo['balance'] + $binfo['integral_num']; + $memberinfo->save(); + + //插入余额记录 + MemberBalanceLogsModel::addGetLog($binfo['uid'],1,$binfo['integral_num'],'','提现申请驳回'); + } + + $this->success('操作成功',url('balancewithdraw'),'_parent_reload'); + }else{ + $this->error('操作失败'); + } + } else { + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['hidden', 'id'], + ['radio', 'status', '状态', '', [2=>'审核通过', 3=>'拒绝提现'],'2'], + ]) + ->setFormData($info) + ->fetch(); + } + } + + //用户反馈 + public function feedback(){ + // 获取 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('id desc'); + + //数据 + $data_list = MemberFeedbackModel::where($map)->order($order)->paginate(); + if(!empty($data_list)){ + foreach ($data_list as $key=>$val){ + + $uinfo = MemberModel::where('id',$val['uid'])->find(); + $is_vip = '普通会员'; + switch ($uinfo['is_vip']) { + case 1: + $is_vip = '铜牌会员'; + break; + case 2: + $is_vip = '银牌会员'; + break; + case 3: + $is_vip = '金牌会员'; + break; + } + $data_list[$key]['nickname'] = $uinfo['nickname'].'('.$is_vip.')'; + } + } + // 分页数据 + $page = $data_list->render(); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['select', 'uid', '所属用户', 'eq', '', MemberModel::where('is_delete',0)->column('id,nickname')], + ['daterange','create_time','时间','between'], + ]) + ->setPageTitle('列表') // 设置页面标题 + ->setTableName('member') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['nickname', '所属用户','text'], + ['phone', '联系电话','text'], + ['message', '反馈内容','text'], + ['create_time', '时间','datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addRightButton('delete', ['title' => '删除', 'icon' => 'fa fa-remove', 'href' => url('deletefeedback', ['id' => '__id__'])]) + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } + + public function deletefeedback($id = ''){ + $post = $this->request->post(); + $result = MemberFeedbackModel::where('id',$id)->delete(); + if($result){ + $this->success('删除成功', url('feedback')); + }else{ + $this->error('删除失败'); + } + } + + //留言板 + public function levemessages(){ + // 获取 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('id desc'); + + //数据 + $data_list = MemberMessagesModel::where($map)->order($order)->paginate(); + if(!empty($data_list)){ + foreach ($data_list as $key=>$val){ + + $uinfo = MemberModel::where('id',$val['uid'])->find(); + $data_list[$key]['nickname'] = $uinfo['nickname']."(".$uinfo['vip_title'].")"; + } + } + // 分页数据 + $page = $data_list->render(); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['select', 'uid', '所属用户', 'eq', '', MemberModel::where('is_delete',0)->column('id,nickname')], + ['daterange','create_time','时间','between'], + ]) + ->setPageTitle('留言板列表') // 设置页面标题 + ->setTableName('member_messages') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['status','审核','switch','',[1=>'是',0=>'否']], + ['nickname', '所属用户','text'], + ['message', '消息内容','text'], + ['create_time', '时间','datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addRightButton('edit',['title'=>'查看留言图片','icon'=>'fa fa-fw fa-image','href'=>url('patrolshopimg',['id' => '__id__'])],['area' => ['1000px', '70%']]) + ->addRightButton('delete', ['title' => '删除', 'icon' => 'fa fa-remove', 'href' => url('deletemessages', ['id' => '__id__'])]) + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } + + public function patrolshopimg($id=null){ + if ($id === null) return $this->error('缺少参数', url('index'),'_parent_reload'); + + $info = MemberMessagesModel::where('id',$id)->find(); + $imglist = explode(',',$info['thumbs']); + $imglistarr = []; + if(!empty($imglist)){ + foreach ($imglist as $k=>$v){ + $imglistarr[$k]['thumb'] = $v; + } + } + + return ZBuilder::make('table') + ->addColumns([ // 批量添加列 + ['thumb', '留言图片','picture'], + ]) + ->setRowList($imglistarr) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + //用户消息 + public function messages(){ + // 获取 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('id desc'); + + //数据 + $data_list = MemberMessagesModel::where($map)->order($order)->paginate(); + if(!empty($data_list)){ + foreach ($data_list as $key=>$val){ + + $uinfo = MemberModel::where('id',$val['uid'])->find(); + $is_vip = '普通会员'; + switch ($uinfo['is_vip']) { + case 1: + $is_vip = '铜牌会员'; + break; + case 2: + $is_vip = '银牌会员'; + break; + case 3: + $is_vip = '金牌会员'; + break; + } + $data_list[$key]['nickname'] = $uinfo['nickname'].'('.$is_vip.')'; + } + } + // 分页数据 + $page = $data_list->render(); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['select', 'uid', '所属用户', 'eq', '', MemberModel::where('is_delete',0)->column('id,nickname')], + ['daterange','create_time','时间','between'], + ]) + ->setPageTitle('列表') // 设置页面标题 + ->setTableName('member') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['nickname', '所属用户','text'], + ['message', '消息内容','text'], + ['create_time', '时间','datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addRightButton('delete', ['title' => '删除', 'icon' => 'fa fa-remove', 'href' => url('deletemessages', ['id' => '__id__'])]) + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } + + public function deletemessages($id = ''){ + $post = $this->request->post(); + $result = MemberMessagesModel::where('id',$id)->delete(); + if($result){ + $this->success('删除成功', url('messages')); + }else{ + $this->error('删除失败'); + } + } + + //订单列表 + public function memberorder(){ + // 获取 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('id desc'); + $map[] = ['type','eq',1]; + + //数据 + $data_list = MemberServiceOrderModel::where($map)->order($order)->paginate(); + if(!empty($data_list)){ + foreach ($data_list as $key=>$val){ + + $uinfo = MemberModel::where('id',$val['uid'])->find(); + $data_list[$key]['nickname'] = $uinfo['nickname']."(".$uinfo['vip_title'].")"; + } + } + // 分页数据 + $page = $data_list->render(); + + $allorder = MemberServiceOrderModel::where($map)->count(); + $allprice = MemberServiceOrderModel::where($map)->sum('totalprice'); + + $where2 = $map; + $where2[] = ['status','eq',2]; + $yallorder = MemberServiceOrderModel::where($where2)->count(); + $yallprice = MemberServiceOrderModel::where($where2)->sum('totalprice'); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['select', 'uid', '所属用户', 'eq', '', MemberModel::where('is_delete',0)->column('id,nickname')], + ['text', 'ordernum', '订单号', 'like'], + ['daterange','create_time','下单时间','between'], + ]) + ->setPageTitle('订单列表') // 设置页面标题 + ->setPageTips("总订单数:$allorder        总金额:$allprice
    + 已付款订单数:$yallorder        已付款金额:$yallprice") + ->setTableName('member_service_order') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['nickname', '所属用户','text'], + ['ordernum', '订单号','text'], + ['totalprice', '价格','text'], + ['status', '订单状态','text','',[1=>'待支付', 2=>'已支付', 3=>'已取消']], + ['create_time', '下单时间','datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addRightButton('edit',['title'=>'查看购买的搭子群','icon'=>'fa fa-fw fa-twitch','href'=>url('memberorderdetail',['id' => '__id__'])],['area' => ['1000px', '70%']]) + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } + + public function memberorderdetail($id=null){ + if ($id === null) return $this->error('缺少参数', url('index'),'_parent_reload'); + + $info = MemberServiceOrderModel::where('id',$id)->select(); + return ZBuilder::make('table') + ->addColumns([ // 批量添加列 + ['service_title', '搭子群标题','text'], + ['service_summary', '搭子群简介','text'], + ['service_price', '价格','text'], + ['service_thumb', '搭子群二维码','picture'], + ]) + ->setRowList($info) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + //积分充值订单 + public function buypoints(){ + // 获取 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('id desc'); + $map[] = ['type','eq',2]; + + //数据 + $data_list = MemberServiceOrderModel::where($map)->order($order)->paginate(); + if(!empty($data_list)){ + foreach ($data_list as $key=>$val){ + + $uinfo = MemberModel::where('id',$val['uid'])->find(); + $data_list[$key]['nickname'] = $uinfo['nickname']."(".$uinfo['vip_title'].")"; + } + } + // 分页数据 + $page = $data_list->render(); + + $allorder = MemberServiceOrderModel::where($map)->count(); + $allprice = MemberServiceOrderModel::where($map)->sum('totalprice'); + + $where2 = $map; + $where2[] = ['status','eq',2]; + $yallorder = MemberServiceOrderModel::where($where2)->count(); + $yallprice = MemberServiceOrderModel::where($where2)->sum('totalprice'); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['select', 'uid', '所属用户', 'eq', '', MemberModel::where('is_delete',0)->column('id,nickname')], + ['text', 'ordernum', '订单号', 'like'], + ['daterange','create_time','下单时间','between'], + ]) + ->setPageTitle('积分充值订单列表') // 设置页面标题 + ->setPageTips("总订单数:$allorder        总金额:$allprice
    + 已付款订单数:$yallorder        已付款金额:$yallprice") + ->setTableName('member_service_order') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['nickname', '所属用户','text'], + ['ordernum', '订单号','text'], + ['totalprice', '价格','text'], + ['service_title', '积分数量','text'], + ['status', '订单状态','text','',[1=>'待支付', 2=>'已支付', 3=>'已取消']], + ['create_time', '下单时间','datetime'], + ]) + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } + + //会员充值订单 + public function viporder(){ + // 获取 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('id desc'); + $map[] = ['type','eq',3]; + + //数据 + $data_list = MemberServiceOrderModel::where($map)->order($order)->paginate(); + if(!empty($data_list)){ + foreach ($data_list as $key=>$val){ + + $uinfo = MemberModel::where('id',$val['uid'])->find(); + $data_list[$key]['nickname'] = $uinfo['nickname']."(".$uinfo['vip_title'].")"; + } + } + // 分页数据 + $page = $data_list->render(); + + $allorder = MemberServiceOrderModel::where($map)->count(); + $allprice = MemberServiceOrderModel::where($map)->sum('totalprice'); + + $where2 = $map; + $where2[] = ['status','eq',2]; + $yallorder = MemberServiceOrderModel::where($where2)->count(); + $yallprice = MemberServiceOrderModel::where($where2)->sum('totalprice'); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['select', 'uid', '所属用户', 'eq', '', MemberModel::where('is_delete',0)->column('id,nickname')], + ['text', 'ordernum', '订单号', 'like'], + ['daterange','create_time','下单时间','between'], + ]) + ->setPageTitle('会员充值订单列表') // 设置页面标题 + ->setPageTips("总订单数:$allorder        总金额:$allprice
    + 已付款订单数:$yallorder        已付款金额:$yallprice") + ->setTableName('member_service_order') + ->setTableName('member_service_order') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['nickname', '所属用户','text'], + ['ordernum', '订单号','text'], + ['totalprice', '价格','text'], + ['service_title', '会员角色','text'], + ['status', '订单状态','text','',[1=>'待支付', 2=>'已支付', 3=>'已取消']], + ['create_time', '下单时间','datetime'], + ]) + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } +} diff --git a/application/member/admin/Patrolshop.php b/application/member/admin/Patrolshop.php new file mode 100644 index 0000000..a1f73af --- /dev/null +++ b/application/member/admin/Patrolshop.php @@ -0,0 +1,124 @@ + + * @return mixed + */ + public function index(){ + // 获取 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('create_time desc'); + + //数据 + $data_list = PatrolshopResultsModel::view('shop_patrolshopresults', true) + ->view('member_patrolshop', 'username,phone', 'shop_patrolshopresults.uid=member_patrolshop.id', 'left') + ->view('shop', 'title as shop_title ', 'shop_patrolshopresults.shop_id=shop.id', 'left') + ->where($map) + ->order($order) + ->paginate(); + if(!empty($data_list)){ + foreach ($data_list as $k=>$v){ + $data_list[$k]['timestr'] = $this->secondsToHMS($v['timestr']); + } + } + // 分页数据 + $page = $data_list->render(); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['select', 'shop_patrolshopresults.uid', '巡店用户', 'eq', '', MemberPatrolshopModel::where('is_delete',0)->order("id DESC")->column("id,username")], + ['select', 'shop_patrolshopresults.shop_id', '所属门店', 'eq', '', ShopModel::where('is_delete',0)->order("id DESC")->column("id,title")], + ['daterange','shop_patrolshopresults.create_time','巡店时间','between'], + ]) + ->setPageTitle('巡店结果') // 设置页面标题 + ->setTableName('member_patrolshop') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['username', '巡店用户','text'], + ['phone', '巡店用户手机号','text'], + ['shop_title', '所属门店','text'], + ['create_time', '巡店时间','datetime'], + ['summarize', '巡店总结','text'], + ['timestr', '巡店耗时','text'], + ['rewards_price', '奖惩金额','text'], + ['right_button', '操作', 'btn'] + ]) + ->addRightButton('edit',['title'=>'查看巡店图片','icon'=>'fa fa-fw fa-image','href'=>url('patrolshopimg',['id' => '__id__'])],['area' => ['1000px', '70%']]) + ->addRightButton('edit',['title'=>'查看巡店签名','icon'=>'fa fa-fw fa-signing','href'=>url('patrolshopimg2',['id' => '__id__'])],['area' => ['1000px', '70%']]) + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } + + public function patrolshopimg2($id=null){ + if ($id === null) return $this->error('缺少参数', url('index'),'_parent_reload'); + + $info = PatrolshopResultsModel::where('id',$id)->select(); + + return ZBuilder::make('table') + ->addColumns([ // 批量添加列 + ['signinimg', '巡店人签名','picture'], + ['shop_signinimg', '店长签名','picture'], + ]) + ->setRowList($info) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + public function patrolshopimg($id=null){ + if ($id === null) return $this->error('缺少参数', url('index'),'_parent_reload'); + + $info = PatrolshopResultsModel::where('id',$id)->find(); + $imglist = explode(',',$info['imglist']); + $imglistarr = []; + if(!empty($imglist)){ + foreach ($imglist as $k=>$v){ + $imglistarr[$k]['thumb'] = $v; + } + } + + return ZBuilder::make('table') + ->addColumns([ // 批量添加列 + ['thumb', '巡店图片','picture'], + ]) + ->setRowList($imglistarr) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + + //根据秒数转换时分秒 + function secondsToHMS($seconds) { + $hours = gmdate("H", $seconds); + $minutes = gmdate("i", $seconds); + $seconds = gmdate("s", $seconds); + + $hours = !empty(intval($hours))?$hours.'时':null; + $minutes = !empty(intval($minutes))?$minutes.'分':null; + $seconds = !empty(intval($seconds))?$seconds.'秒':null; + + return $hours.$minutes.$seconds; + } +} diff --git a/application/member/admin/Signin.php b/application/member/admin/Signin.php new file mode 100644 index 0000000..8e6513f --- /dev/null +++ b/application/member/admin/Signin.php @@ -0,0 +1,89 @@ + + * @return mixed + */ + public function index(){ + // 获取 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('create_time desc'); + + //数据 + $data_list = ShopSigninModel::view('shop_signin', true) + ->view('member_patrolshop', 'username,phone', 'shop_signin.uid=member_patrolshop.id', 'left') + ->view('shop', 'title as shop_title ', 'shop_signin.shop_id=shop.id', 'left') + ->where($map) + ->order($order) + ->paginate(); + // 分页数据 + $page = $data_list->render(); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['select', 'shop_signin.uid', '签到用户', 'eq', '', MemberPatrolshopModel::where('is_delete',0)->order("id DESC")->column("id,username")], + ['select', 'shop_signin.shop_id', '签到门店', 'eq', '', ShopModel::where('is_delete',0)->order("id DESC")->column("id,title")], + ['daterange','shop_signin.create_time','签到时间','between'], + ]) + ->setPageTitle('签到记录') // 设置页面标题 + ->setTableName('member_patrolshop') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['username', '签到用户','text'], + ['phone', '签到用户手机号','text'], + ['shop_title', '签到门店','text'], + ['address', '签到地址','text'], + ['create_time', '签到时间','datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addRightButton('edit',['title'=>'查看签到图片','icon'=>'fa fa-fw fa-image','href'=>url('signinimg',['id' => '__id__'])],['area' => ['1000px', '70%']]) + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } + + + //查看购买商品 + public function signinimg($id=null){ + if ($id === null) return $this->error('缺少参数', url('index'),'_parent_reload'); + + $info = ShopSigninModel::where('id',$id)->find(); + $imglist = explode(',',$info['imglist']); + $imglistarr = []; + if(!empty($imglist)){ + foreach ($imglist as $k=>$v){ + $imglistarr[$k]['thumb'] = $v; + } + } + + return ZBuilder::make('table') + ->addColumns([ // 批量添加列 + ['thumb', '签到图片','picture'], + ]) + ->setRowList($imglistarr) // 设置表格数据 + ->fetch(); // 渲染模板 + } +} diff --git a/application/member/common.php b/application/member/common.php new file mode 100644 index 0000000..ea404c9 --- /dev/null +++ b/application/member/common.php @@ -0,0 +1,13 @@ + 'member', + // 模块标题[必填] + 'title' => '用户', + // 模块唯一标识[必填],格式:模块名.开发者标识.module + 'identifier' => 'member.maurylee.module', + // 模块图标[选填] + 'icon' => 'fa fa-fw fa-database', + // 模块描述[选填] + 'description' => '用户模块', + // 开发者[必填] + 'author' => 'maurylee', + // 开发者网址[选填] + 'author_url' => '', + // 版本[必填],格式采用三段式:主版本号.次版本号.修订版本号 + 'version' => '1.0.0', + // 模块依赖[可选],格式[[模块名, 模块唯一标识, 依赖版本, 对比方式]] + 'need_module' => [], + // 插件依赖[可选],格式[[插件名, 插件唯一标识, 依赖版本, 对比方式]] + 'need_plugin' => [], + // 数据表[有数据库表时必填] + 'tables' => [], + // 原始数据库表前缀 + // 用于在导入模块sql时,将原有的表前缀转换成系统的表前缀 + // 一般模块自带sql文件时才需要配置 + 'database_prefix' => '', + + + //模块参数配置 + 'config' => [], + + + // 行为配置 + 'action' => [], + + + // 授权配置 + 'access' => [], +]; + diff --git a/application/member/menus.php b/application/member/menus.php new file mode 100644 index 0000000..5f92abd --- /dev/null +++ b/application/member/menus.php @@ -0,0 +1,29 @@ + '用户', + 'icon' => 'fa fa-fw fa-users', + 'url_type' => 'module_admin', + 'url_value' => 'member/index/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [], + ], + +]; + diff --git a/application/member/model/BalanceWithdraw.php b/application/member/model/BalanceWithdraw.php new file mode 100644 index 0000000..215f19e --- /dev/null +++ b/application/member/model/BalanceWithdraw.php @@ -0,0 +1,25 @@ + + */ + public static function addGetLog($uid,$category,$value,$orderid,$mark,$touid=0){ + $res = self::create([ + 'uid'=>$uid, + 'category'=>$category, + 'value'=>$value, + 'orderid'=>$orderid, + 'mark'=>$mark, + 'to_uid'=>$touid, + ]); + return $res; + } +} diff --git a/application/member/model/MemberFeedback.php b/application/member/model/MemberFeedback.php new file mode 100644 index 0000000..a284f7f --- /dev/null +++ b/application/member/model/MemberFeedback.php @@ -0,0 +1,25 @@ +'首页广告(尺寸:410 × 230)', + 2=>'留言板规格(尺寸:410 × 230)', + 3=>'分享海报', + 4=>'首页底部浮动广告(尺寸:1360 × 274)', + ]; + + /** + * 广告列表 + * @author loomis <2477365162@qq.com> + * @return mixed + */ + public function index(){ + // 获取 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('sort asc,create_time desc'); + + //数据 + $data_list = AdvertModel::where($map)->order($order)->paginate(); + // 分页数据 + $page = $data_list->render(); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['text', 'title', '广告标题', 'like'], + ['select', 'type', '广告类别', 'eq', '', $this->typearr], + ['daterange','create_time','添加时间','between'], + ]) + ->setPageTitle('广告列表') // 设置页面标题 + ->setTableName('advert') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['type', '类型','text','',$this->typearr], + ['title', '广告标题','text'], + ['thumb', '广告图片','picture'], + ['sort','排序(顺序排序)','text.edit'], + ['create_time', '添加时间','datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('release',['class'=>'btn btn-primary','icon' => 'fa fa-plus-circle','title'=>'添加广告', 'href' => url('add')]) + ->addRightButton('edit',['title'=>'編輯','icon'=>'fa fa-pencil','href'=>url('edit',['id' => '__id__'])]) + ->addRightButton('delete') // 添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->setTabNav([ + 'tab1' => ['title' => '广告列表', 'url' => url('other/advert/index', ['group' => 'tab1'])], + 'tab2' => ['title' => '添加广告', 'url' => url('other/advert/add', ['group' => 'tab2'])], + ], 'tab1') + ->fetch(); // 渲染模板 + } + + /** + * 添加广告 + * @author loomis <2477365162@qq.com> + * @return mixed + */ + public function add(){ + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + if(!isset($data['title'])|| empty($data['title'])){ + return $this->error('广告名称不能为空'); + } + + $r = AdvertModel::create($data); + if($r){ + $this->success('新增成功', url('index')); + }else{ + $this->error('新增失败'); + } + + }else{ + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('新增广告') // 设置页面标题 + ->addFormItems([ // 批量添加表单项 + ['select', 'type', '广告类型','必选',$this->typearr], + ['text', 'title', '广告名称','必填'], + ['image:4', 'thumb', '广告图片', '必上传'], + ['image:4', 'img1', '页面背景图', '必上传'], + ['images:4', 'img2', '微信二维码图(多图)', '必上传'], + ['text', 'sort', '排序(越小前台显示越靠前)','',9999], + ['textarea:6', 'linkurl', '广告链接','必填'], + ['textarea:6', 'summary', '广告介绍','必填'], + ]) + ->setTabNav([ + 'tab1' => ['title' => '广告列表', 'url' => url('other/advert/index', ['group' => 'tab1'])], + 'tab2' => ['title' => '添加广告', 'url' => url('other/advert/add', ['group' => 'tab2'])], + ], 'tab2') + ->setTrigger('type', '4', 'img1,img2') + ->fetch(); + + } + } + + /** + * 编辑广告广告 + * @author loomis <2477365162@qq.com> + * @return mixed + */ + public function edit($id=null){ + if ($id === null) return $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + if(!isset($data['title'])|| empty($data['title'])){ + return $this->error('广告名称不能为空'); + } + + $r = AdvertModel::update($data); + if($r){ + $this->success('编辑成功', url('index')); + }else{ + $this->error('编辑失败'); + } + + }else{ + $info = AdvertModel::get($id); + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑广告') // 设置页面标题 + ->addFormItems([ // 批量添加表单项 + ['hidden', 'id'], + ['select', 'type', '广告类型','必选',$this->typearr], + ['text', 'title', '广告名称','必填'], + ['image:4', 'thumb', '广告图片', '必上传'], + ['image:4', 'img1', '页面背景图', '必上传'], + ['images:4', 'img2', '微信二维码图(多图)', '必上传'], + ['text', 'sort', '排序(越小前台显示越靠前)','',9999], + ['textarea:6', 'linkurl', '广告链接','必填'], + ['textarea:6', 'summary', '广告介绍','必填'], + ]) + ->setFormData($info) + ->setTabNav([ + 'tab1' => ['title' => '广告列表', 'url' => url('other/advert/index', ['group' => 'tab1'])], + 'tab2' => ['title' => '添加广告', 'url' => url('other/advert/add', ['group' => 'tab2'])], + ], '') + ->setTrigger('type', '4', 'img1,img2') + ->fetch(); + + } + } + +} diff --git a/application/other/admin/Category.php b/application/other/admin/Category.php new file mode 100644 index 0000000..c8c4e64 --- /dev/null +++ b/application/other/admin/Category.php @@ -0,0 +1,287 @@ + + * @return mixed + */ + public function index() + { + // 保存模块排序 + if ($this->request->isPost()) { + $modules = $this->request->post('sort/a'); + if ($modules) { + foreach ($modules as $key => $module) { + $data[] = [ + 'id' => $module, + 'sort' => $key + 1 + ]; + } + $ProductCategoryModel = new ProductCategoryModel(); + if (false !== $ProductCategoryModel->saveAll($data)) { + return $this->success('保存成功'); + } else { + return $this->error('保存失败'); + } + } + } + + $data_list = ProductCategoryModel::getMenusByGroup(); + $max_level = $this->request->get('max', 0); + $this->assign('menus', $this->getNestMenu($data_list, $max_level)); + $this->assign('page_title', '类别管理'); + + + return $this->fetch(); + } + + /** + * 新增类别 + * @author loomis <2477365162@qq.com> + */ + public function add($module = 'admin', $pid = '') + { + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + // 验证 + $result = $this->validate($data, 'Category'); + // 验证失败 输出错误信息 + if(true !== $result) return $this->error($result); + + $data['UID'] = UID; + if ($menu = ProductCategoryModel::create($data)) { + return $this->success('新增成功',url('index')); + } else { + return $this->error('新增失败'); + } + } else { + + $category_list = ProductCategoryModel::getMenuTree(0, '', $module); + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('新增类别') + ->addFormItems([ + ['image', 'thumb', '缩略图'], + ['select', 'pid', '所属类别', '所属上级类别', $category_list, $pid], + ['text', 'title', '类别标题'], + ['radio', 'is_index', '是否首页显示', '',[0=>'不显示',1=>'显示'],0], + ]) + ->addIcon('icon', '图标', '导航图标') + ->addText('sort', '排序', '', 100) + ->layout(['pid' => '8', 'title' => '8', 'icon' => '8', 'sort' => '8']) + ->fetch(); + } + + } + + /** + * 编辑类别 + * @param int $id 类别ID + * @author loomis <2477365162@qq.com> + * @return mixed + */ + public function edit($id = 0) + { + if ($id === 0) return $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Category'); + // 验证失败 输出错误信息 + if(true !== $result) return $this->error($result); + + if (ProductCategoryModel::update($data)) { + + return $this->success('编辑成功', url('index')); + } else { + return $this->error('编辑失败'); + } + } + + // 获取数据 + $info = ProductCategoryModel::get($id); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑类别') + ->addFormItem('hidden', 'id') + ->addFormItem('image', 'thumb', '缩略图') + ->addFormItem('select', 'pid', '所属类别', '所属上级类别', ProductCategoryModel::getMenuTree(0, '', $info['module'])) + ->addFormItem('text', 'title', '类别标题') + ->addFormItem('radio', 'is_index', '是否首页显示','', [0=>'不显示',1=>'显示']) + ->addIcon('icon', '图标', '导航图标') + ->addText('sort', '排序', '', 100) + + ->layout(['pid' => '8', 'title' => '8', 'icon' => '8', 'sort' => '8']) + ->setFormData($info) + ->fetch(); + } + + /** + * 删除类别 + * @return mixed + */ + public function delete($record = []) + { + $id = $this->request->param('id'); + $menu = ProductCategoryModel::where('id', $id)->find(); + + if ($menu['system_menu'] == '1') return $this->error('系统类别,禁止删除'); + + // 获取该类别的所有后辈类别id + $menu_childs = ProductCategoryModel::getChildsId($id); + + // 要删除的所有类别id + $all_ids = array_merge([(int)$id], $menu_childs); + + // 删除类别 + if (ProductCategoryModel::destroy($all_ids)) { + return $this->success('删除成功'); + } else { + return $this->error('删除失败'); + } + } + + /** + * 保存类别排序 + * @return mixed + */ + public function save() + { + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!empty($data)) { + $menus = $this->parseMenu($data['menus']); + foreach ($menus as $menu) { + if ($menu['pid'] == 0) { + ProductCategoryModel::update(array('sort'=>$menu['sort']),array('id'=>$menu['id'])); + }else{ + ProductCategoryModel::update(array('pid'=>$menu['pid'],'sort'=>$menu['sort']),array('id'=>$menu['id'])); + } + } + return $this->success('保存成功'); + } else { + return $this->error('没有需要保存的类别'); + } + } + return $this->error('非法请求'); + } + + + /** + * 递归解析类别 + * @param array $menus 类别数据 + * @param int $pid 上级类别id + * @return array 解析成可以写入数据库的格式 + */ + private function parseMenu($menus = [], $pid = 0) + { + $sort = 1; + $result = []; + foreach ($menus as $menu) { + $result[] = [ + 'id' => (int)$menu['id'], + 'pid' => (int)$pid, + 'sort' => $sort, + ]; + if (isset($menu['children'])) { + $result = array_merge($result, $this->parseMenu($menu['children'], $menu['id'])); + } + $sort ++; + } + return $result; + } + + /** + * 获取嵌套式类别 + * @param array $lists 原始类别数组 + * @param int $pid 父级id + * @param int $max_level 最多返回多少层,0为不限制 + * @param int $curr_level 当前层数 + */ + private function getNestMenu($lists = [], $max_level = 0, $pid = 0, $curr_level = 1) + { + $result = ''; + foreach ($lists as $key => $value) { + if ($value['pid'] == $pid) { + $disable = $value['status'] == 0 ? 'dd-disable' : ''; + + $isindex = $value['is_index']==1?'(首页显示)':null; + + // 组合类别 + $result .= '
  • '; + $result .= '
    拖拽
    '.$value['title'].$isindex; + if ($value['url_value'] != '') { + $result .= ' '.$value['url_value'].''; + } + $result .= '
    '; + $result .= ''; + if ($value['status'] == 0) { + // 启用 + $result .= ''; + } else { + // 禁用 + $result .= ''; + } + $result .= '
    '; + $result .= '
    '; + + if ($max_level == 0 || $curr_level != $max_level) { + unset($lists[$key]); + // 下级类别 + $children = $this->getNestMenu($lists, $max_level, $value['id'], $curr_level + 1); + if ($children != '') { + $result .= '
      '.$children.'
    '; + } + } + + $result .= '
  • '; + } + } + return $result; + } + + /** + * 启用类别 + */ + public function enable($record = []) + { + $id = input('param.ids'); + $menu = ProductCategoryModel::where('id', $id)->update(['status'=>1]); + + $this->success('操作成功'); + } + + /** + * 禁用类别 + */ + public function disable($record = []) + { + $id = input('param.ids'); + $menu = ProductCategoryModel::where('id', $id)->update(['status'=>0]); + + $this->success('操作成功'); + } +} diff --git a/application/other/admin/Citys.php b/application/other/admin/Citys.php new file mode 100644 index 0000000..82c636a --- /dev/null +++ b/application/other/admin/Citys.php @@ -0,0 +1,131 @@ + +// +---------------------------------------------------------------------- +namespace app\other\admin; + +use think\Db; +use app\admin\controller\Admin; +use app\common\builder\ZBuilder; +use app\other\model\Citys as CitysModel; + + +class Citys extends Admin +{ + //主播列表 + public function index(){ + // 获取查询条件 + $map = $this->getMap(); + $map[] = ['is_delete','eq',0]; + + // 排序 + $order = $this->getOrder('sort asc,id desc'); + + // 数据列表 + $data_list = CitysModel::where($map)->order($order)->paginate(); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['text', 'title', '主播名称','like'], + ['daterange', 'create_time', '添加时间', 'between'], + ]) + ->setPageTitle('主播列表')// 设置页面标题 + ->setTableName('other_citys') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['sort', '排序','text.edit'], + ['thumb', '图片','picture'], + ['title', '主播名称'], + ['create_time', '添加时间','datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('add',['class'=>'btn btn-primary','icon' => 'fa fa-plus-circle','title'=>'新增', 'href' => url('add')],['area' => ['600px', '60%']]) + ->addRightButton('edit',['title'=>'編輯','icon'=>'fa fa-pencil','href'=>url('edit',['id' => '__id__'])],['area' => ['600px', '60%']]) + ->addRightButton('delete', ['title' => '删除', 'icon' => 'fa fa-remove', 'href' => url('delete', ['id' => '__id__'])]) + ->setRowList($data_list)// 设置表格数据 + ->fetch(); // 渲染模板 + } + + //添加主播 + public function add(){ + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['title'])){ + return $this->error('主播名称不能为空'); + } + + $result = CitysModel::create($data); + if($result){ + $this->success('新增成功', url('index'),'_parent_reload'); + }else{ + $this->error('新增失败'); + } + }else{ + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['image', 'thumb', '图片', '必上传'], + ['text', 'title', '主播名称'], + ['text', 'sort', '排序(越小前台显示越靠前)','',9999], + ]) + ->fetch(); + } + } + + //编辑主播 + public function edit($id=null){ + if ($id === null) return $this->error('缺少参数'); + + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['title'])){ + return $this->error('主播名称不能为空'); + } + $data['update_time'] = time(); + $result = CitysModel::update($data); + if($result){ + $this->success('修改成功', url('index'),'_parent_reload'); + }else{ + $this->error('修改失败'); + } + } else { + $info = CitysModel::get($id); + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['hidden', 'id'], + ['image', 'thumb', '图片', '必上传'], + ['text', 'title', '主播名称'], + ['text', 'sort', '排序(越小前台显示越靠前)','',9999], + ]) + ->setFormData($info) + ->fetch(); + } + } + + //删除主播 + public function delete($id = null) + { + if ($id === null) { + $this->error('缺少参数'); + } + + $data = CitysModel::where('id',$id)->find(); + if (empty($data)) {$this->error('删除成功!'); } + + $result = CitysModel::where('id',$id)->setField(['is_delete'=> 1,'delete_time'=>time()]); + if($result) { + return $this->success('删除成功!'); + }else{ + return $this->error('删除失败!'); + } + } + +} \ No newline at end of file diff --git a/application/other/admin/Recharge.php b/application/other/admin/Recharge.php new file mode 100644 index 0000000..344accd --- /dev/null +++ b/application/other/admin/Recharge.php @@ -0,0 +1,136 @@ + +// +---------------------------------------------------------------------- +namespace app\other\admin; + +use think\Db; +use app\admin\controller\Admin; +use app\common\builder\ZBuilder; +use app\member\model\MemberRecharge as MemberRechargeModel; + + +class Recharge extends Admin +{ + //城市列表 + public function index(){ + // 获取查询条件 + $map = $this->getMap(); + + // 排序 + $order = $this->getOrder('sort asc,id desc'); + + // 数据列表 + $data_list = MemberRechargeModel::where($map)->order($order)->paginate(); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['daterange', 'create_time', '添加时间', 'between'], + ]) + ->setPageTitle('充值列表')// 设置页面标题 + ->setTableName('member_recharge') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['sort', '排序','text.edit'], + ['points_num', '积分数量'], + ['price', '充值金额'], + ['create_time', '添加时间','datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('add',['class'=>'btn btn-primary','icon' => 'fa fa-plus-circle','title'=>'新增', 'href' => url('add')],['area' => ['600px', '60%']]) + ->addRightButton('edit',['title'=>'編輯','icon'=>'fa fa-pencil','href'=>url('edit',['id' => '__id__'])],['area' => ['600px', '60%']]) + ->addRightButton('delete', ['title' => '删除', 'icon' => 'fa fa-remove', 'href' => url('delete', ['id' => '__id__'])]) + ->setRowList($data_list)// 设置表格数据 + ->fetch(); // 渲染模板 + } + + //添加城市 + public function add(){ + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['points_num'])){ + return $this->error('积分数量不能为空'); + } + if(empty($data['price'])){ + return $this->error('充值金额不能为空'); + } + + $result = MemberRechargeModel::create($data); + if($result){ + $this->success('新增成功', url('index'),'_parent_reload'); + }else{ + $this->error('新增失败'); + } + }else{ + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['text', 'points_num', '积分数量'], + ['text', 'price', '充值金额'], + ['text', 'sort', '排序(越小前台显示越靠前)','',9999], + ]) + ->fetch(); + } + } + + //编辑城市 + public function edit($id=null){ + if ($id === null) return $this->error('缺少参数'); + + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['points_num'])){ + return $this->error('积分数量不能为空'); + } + if(empty($data['price'])){ + return $this->error('充值金额不能为空'); + } + $data['update_time'] = time(); + + $result = MemberRechargeModel::update($data); + if($result){ + $this->success('修改成功', url('index'),'_parent_reload'); + }else{ + $this->error('修改失败'); + } + } else { + $info = MemberRechargeModel::get($id); + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['hidden', 'id'], + ['text', 'points_num', '积分数量'], + ['text', 'price', '充值金额'], + ['text', 'sort', '排序(越小前台显示越靠前)','',9999], + ]) + ->setFormData($info) + ->fetch(); + } + } + + //删除 + public function delete($id = null) + { + if ($id === null) { + $this->error('缺少参数'); + } + + $data = MemberRechargeModel::where('id',$id)->find(); + if (empty($data)) {$this->error('删除成功!'); } + + $result = MemberRechargeModel::where('id',$id)->delete(); + if($result) { + return $this->success('删除成功!'); + }else{ + return $this->error('删除失败!'); + } + } + +} \ No newline at end of file diff --git a/application/other/admin/Setvip.php b/application/other/admin/Setvip.php new file mode 100644 index 0000000..0238dfa --- /dev/null +++ b/application/other/admin/Setvip.php @@ -0,0 +1,172 @@ + +// +---------------------------------------------------------------------- +namespace app\other\admin; + +use think\Db; +use app\admin\controller\Admin; +use app\common\builder\ZBuilder; +use app\member\model\MemberVip as MemberVipModel; + + +class Setvip extends Admin +{ + //城市列表 + public function index(){ + // 获取查询条件 + $map = $this->getMap(); + $map[] = ['type','eq',1]; + + // 排序 + $order = $this->getOrder('sort asc,id desc'); + + // 数据列表 + $data_list = MemberVipModel::where($map)->order($order)->paginate(); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['daterange', 'create_time', '添加时间', 'between'], + ]) + ->setPageTitle('VIP信息')// 设置页面标题 + ->setTableName('member_vip') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['sort', '排序','text.edit'], + ['title', '标题'], + ['price', '购买金额'], + ['create_time', '添加时间','datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('add',['class'=>'btn btn-primary','icon' => 'fa fa-plus-circle','title'=>'新增', 'href' => url('add')],['area' => ['600px', '60%']]) + ->addTopButton('export',[ + 'title' => '会员权益说明', + 'icon' => 'fa fa-fw fa-file-text-o', + 'class' => 'btn btn-info export confirm', + 'href' => url('vipequity'), + 'data-url' => url('vipequity') + ],['area' => ['800px', '80%']]) // 导出 + ->addRightButton('edit',['title'=>'編輯','icon'=>'fa fa-pencil','href'=>url('edit',['id' => '__id__'])],['area' => ['600px', '60%']]) + ->addRightButton('delete', ['title' => '删除', 'icon' => 'fa fa-remove', 'href' => url('delete', ['id' => '__id__'])]) + ->setRowList($data_list)// 设置表格数据 + ->fetch(); // 渲染模板 + } + + //添加城市 + public function add(){ + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['title'])){ + return $this->error('标题不能为空'); + } + if(empty($data['price'])){ + return $this->error('购买金额不能为空'); + } + + $result = MemberVipModel::create($data); + if($result){ + $this->success('新增成功', url('index'),'_parent_reload'); + }else{ + $this->error('新增失败'); + } + }else{ + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['text', 'title', '标题'], + ['text', 'price', '购买金额'], + ['text', 'sort', '排序(越小前台显示越靠前)','',9999], + ]) + ->fetch(); + } + } + + //编辑城市 + public function edit($id=null){ + if ($id === null) return $this->error('缺少参数'); + + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['title'])){ + return $this->error('标题不能为空'); + } + if(empty($data['price'])){ + return $this->error('购买金额不能为空'); + } + $data['update_time'] = time(); + + $result = MemberVipModel::update($data); + if($result){ + $this->success('修改成功', url('index'),'_parent_reload'); + }else{ + $this->error('修改失败'); + } + } else { + $info = MemberVipModel::get($id); + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['hidden', 'id'], + ['text', 'title', '标题'], + ['text', 'price', '购买金额'], + ['text', 'sort', '排序(越小前台显示越靠前)','',9999], + ]) + ->setFormData($info) + ->fetch(); + } + } + + //权益说明 + public function vipequity(){ + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['title'])){ + return $this->error('权益说明不能为空'); + } + + $data['update_time'] = time(); + $result = MemberVipModel::update($data); + if($result){ + $this->success('修改成功', url('index'),'_parent_reload'); + }else{ + $this->error('修改失败'); + } + } else { + $info = MemberVipModel::where('id',1)->find(); + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['hidden', 'id'], + ['ueditor', 'title', '权益说明'], + ]) + ->setFormData($info) + ->fetch(); + } + } + + //删除 + public function delete($id = null) + { + if ($id === null) { + $this->error('缺少参数'); + } + + $data = MemberVipModel::where('id',$id)->find(); + if (empty($data)) {$this->error('删除成功!'); } + + $result = MemberVipModel::where('id',$id)->delete(); + if($result) { + return $this->success('删除成功!'); + }else{ + return $this->error('删除失败!'); + } + } + +} \ No newline at end of file diff --git a/application/other/admin/Webnotice.php b/application/other/admin/Webnotice.php new file mode 100644 index 0000000..491dce2 --- /dev/null +++ b/application/other/admin/Webnotice.php @@ -0,0 +1,130 @@ + +// +---------------------------------------------------------------------- +namespace app\other\admin; + +use think\Db; +use app\admin\controller\Admin; +use app\common\builder\ZBuilder; +use app\other\model\Citys as CitysModel; +use app\member\model\MemberServiceDetail as MemberServiceDetailModel; +use app\other\model\ProductCategory as ProductCategoryModel; +use app\member\model\Member as MemberModel; + + +class Webnotice extends Admin +{ + //列表 + public function index(){ + // 获取查询条件 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('sort asc,create_time desc'); + + // 数据列表 + $data_list = MemberServiceDetailModel::where($map)->order($order)->paginate(); + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['text', 'title', '标题','like'], + ['daterange', 'create_time', '添加时间', 'between'], + ]) + ->setPageTitle('公告管理')// 设置页面标题 + ->setTableName('member_service') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['sort', '排序','text.edit'], + ['title', '标题'], + ['create_time', '添加时间','datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('add',['class'=>'btn btn-primary','icon' => 'fa fa-plus-circle','title'=>'新增', 'href' => url('add')]) + ->addRightButton('edit',['title'=>'編輯','icon'=>'fa fa-pencil','href'=>url('edit',['id' => '__id__'])]) + ->addRightButton('delete', ['title' => '删除', 'icon' => 'fa fa-remove', 'href' => url('delete', ['id' => '__id__'])]) + ->setRowList($data_list)// 设置表格数据 + ->fetch(); // 渲染模板 + } + + //添加服务信息 + public function add(){ + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['title'])){ + return $this->error('标题不能为空'); + } + + $result = MemberServiceDetailModel::create($data); + if($result){ + $this->success('新增成功', url('index'),'_parent_reload'); + }else{ + $this->error('新增失败'); + } + }else{ + return ZBuilder::make('form') + ->addFormItems([ + ['text', 'title', '标题'], + ['ueditor', 'message', '详情'], + ['text', 'sort', '排序(越小前台显示越靠前)','',9999], + ]) + ->fetch(); + } + } + + //编辑服务信息 + public function edit($id=null){ + if ($id === null) return $this->error('缺少参数'); + + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['title'])){ + return $this->error('标题不能为空'); + } + $data['update_time'] = time(); + $result = MemberServiceDetailModel::update($data); + if($result){ + $this->success('修改成功', url('index'),'_parent_reload'); + }else{ + $this->error('修改失败'); + } + } else { + $info = MemberServiceDetailModel::get($id); + + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['hidden', 'id'], + ['text', 'title', '标题'], + ['ueditor', 'message', '详情'], + ['text', 'sort', '排序(越小前台显示越靠前)','',9999], + ]) + ->setFormData($info) + ->fetch(); + } + } + + //删除服务信息 + public function delete($id = null){ + if ($id === null) { + $this->error('缺少参数'); + } + + $data = MemberServiceDetailModel::where('id',$id)->find(); + if (empty($data)) {$this->error('删除成功!'); } + + $result = MemberServiceDetailModel::where('id',$id)->delete(); + if($result) { + return $this->success('删除成功!'); + }else{ + return $this->error('删除失败!'); + } + } + +} \ No newline at end of file diff --git a/application/other/admin/Webservice.php b/application/other/admin/Webservice.php new file mode 100644 index 0000000..8c06594 --- /dev/null +++ b/application/other/admin/Webservice.php @@ -0,0 +1,188 @@ + +// +---------------------------------------------------------------------- +namespace app\other\admin; + +use think\Db; +use app\admin\controller\Admin; +use app\common\builder\ZBuilder; +use app\other\model\Citys as CitysModel; +use app\member\model\MemberService as MemberServiceModel; +use app\member\model\MemberServiceDetail as MemberServiceDetailModel; +use app\other\model\ProductCategory as ProductCategoryModel; +use app\member\model\Member as MemberModel; + + +class Webservice extends Admin +{ + //列表 + public function index(){ + // 获取查询条件 + $map = $this->getMap(); + $map[] = ['is_delete','eq',0]; + $map[] = ['id','neq',12]; + // 排序 + $order = $this->getOrder('is_index desc,sort asc,create_time desc'); + + // 数据列表 + $data_list = MemberServiceModel::where($map)->order($order)->paginate(); + if(!empty($data_list)){ + foreach ($data_list as $key=>$val){ + $data_list[$key]['cityid'] = CitysModel::where('id',$val['cityid'])->value('title'); + + } + } + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['text', 'title', '标题','like'], + ['select', 'cityid', '所属主播', 'eq', '', CitysModel::where('is_delete',0)->column('id,title')], + ['daterange', 'create_time', '添加时间', 'between'], + ]) + ->setPageTitle('搭子群信息管理')// 设置页面标题 + ->setTableName('member_service') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['sort', '排序','text.edit'], + ['is_index', '状态','text','',[0=>'正常',1=>'首页置顶']], + ['cityid', '所属主播'], + ['thumb', '图片','picture'], + ['title', '标题'], + ['price', '价格'], + ['create_time', '添加时间','datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('add',['class'=>'btn btn-primary','icon' => 'fa fa-plus-circle','title'=>'新增', 'href' => url('add')]) + ->addRightButton('edit',['title'=>'首页置顶','icon'=>'fa fa-fw fa-list-ol','href'=>url('servicedetail',['id' => '__id__'])],['area' => ['600px', '60%']]) + ->addRightButton('edit',['title'=>'編輯','icon'=>'fa fa-pencil','href'=>url('edit',['id' => '__id__'])]) + ->addRightButton('delete', ['title' => '删除', 'icon' => 'fa fa-remove', 'href' => url('delete', ['id' => '__id__'])]) + ->setRowList($data_list)// 设置表格数据 + ->fetch(); // 渲染模板 + } + + //添加服务信息 + public function add(){ + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['title'])){ + return $this->error('标题不能为空'); + } + + $result = MemberServiceModel::create($data); + if($result){ + $this->success('新增成功', url('index'),'_parent_reload'); + }else{ + $this->error('新增失败'); + } + }else{ + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['image:6', 'thumb', '封面图', '必上传'], + ['image:6', 'qrcodeimg', '群二维码图片', '必上传'], + ['select', 'cityid', '所属主播','必选',CitysModel::where('is_delete',0)->column('id,title')], + ['text:6', 'title', '标题'], + ['text:6', 'price', '购买金额'], + ['textarea:6', 'summary', '描述'], + ['textarea:6', 'standard', '群规范'], + ['ueditor', 'message', '详情'], + ['text', 'sort', '排序(越小前台显示越靠前)','',9999], + ]) + ->fetch(); + } + } + + //编辑服务信息 + public function edit($id=null){ + if ($id === null) return $this->error('缺少参数'); + + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['title'])){ + return $this->error('标题不能为空'); + } + $data['update_time'] = time(); + $result = MemberServiceModel::update($data); + if($result){ + $this->success('修改成功', url('index'),'_parent_reload'); + }else{ + $this->error('修改失败'); + } + } else { + $info = MemberServiceModel::get($id); + + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['hidden', 'id'], + ['image:6', 'thumb', '封面图', '必上传'], + ['image:6', 'qrcodeimg', '群二维码图片', '必上传'], + ['select', 'cityid', '所属主播','必选',CitysModel::where('is_delete',0)->column('id,title')], + ['text:6', 'title', '标题'], + ['text:6', 'price', '购买金额'], + ['textarea:6', 'summary', '描述'], + ['textarea:6', 'standard', '群规范'], + ['ueditor', 'message', '详情'], + ['text', 'sort', '排序(越小前台显示越靠前)','',9999], + ]) + ->setFormData($info) + ->fetch(); + } + } + + //删除服务信息 + public function delete($id = null){ + if ($id === null) { + $this->error('缺少参数'); + } + + $data = MemberServiceModel::where('id',$id)->find(); + if (empty($data)) {$this->error('删除成功!'); } + + $result = MemberServiceModel::where('id',$id)->setField(['is_delete'=> 1,'delete_time'=>time()]); + if($result) { + return $this->success('删除成功!'); + }else{ + return $this->error('删除失败!'); + } + } + + //首页置顶 + public function servicedetail($id = null){ + if ($id === null) { + $this->error('缺少参数'); + } + + if($this->request->isPost()){ + $data = $this->request->post(); + + $result = MemberServiceModel::update($data); + if($result){ + + $this->success('操作成功',url('index'),'_parent_reload'); + }else{ + $this->error('操作失败'); + } + } else { + $info = MemberServiceModel::where('id',$id)->find(); + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['hidden', 'id'], + ['radio', 'is_index', '状态', '', [0=>'正常', 1=>'首页置顶']], + ['image', 'index_thumb', '首页置顶图', '必上传'], + ['text', 'index_title', '首页置标题'], + ]) + ->setFormData($info) + ->fetch(); + } + } + +} \ No newline at end of file diff --git a/application/other/admin/Withdraw.php b/application/other/admin/Withdraw.php new file mode 100644 index 0000000..2ec67bc --- /dev/null +++ b/application/other/admin/Withdraw.php @@ -0,0 +1,138 @@ + +// +---------------------------------------------------------------------- +namespace app\other\admin; + +use think\Db; +use app\admin\controller\Admin; +use app\common\builder\ZBuilder; +use app\member\model\MemberWithdraw as MemberWithdrawModel; + +class Withdraw extends Admin +{ + //城市列表 + public function index(){ + // 获取查询条件 + $map = $this->getMap(); + + // 排序 + $order = $this->getOrder('sort asc,id desc'); + + // 数据列表 + $data_list = MemberWithdrawModel::where($map)->order($order)->paginate(); + + //加载模板 + return ZBuilder::make('table') + ->setSearchArea([ + ['daterange', 'create_time', '添加时间', 'between'], + ]) + ->setPageTitle('提现设定')// 设置页面标题 + ->setTableName('member_withdraw') + ->addOrder('create_time') // 添加排序 + ->addColumns([ // 批量添加列 + ['sort', '排序','text.edit'], + ['points_num', '提取积分'], + ['vip_points_num', 'VIP提取积分'], + ['price', '兑换金额'], + ['create_time', '添加时间','datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('add',['class'=>'btn btn-primary','icon' => 'fa fa-plus-circle','title'=>'新增', 'href' => url('add')],['area' => ['600px', '60%']]) + ->addRightButton('edit',['title'=>'編輯','icon'=>'fa fa-pencil','href'=>url('edit',['id' => '__id__'])],['area' => ['600px', '60%']]) + ->addRightButton('delete', ['title' => '删除', 'icon' => 'fa fa-remove', 'href' => url('delete', ['id' => '__id__'])]) + ->setRowList($data_list)// 设置表格数据 + ->fetch(); // 渲染模板 + } + + //添加城市 + public function add(){ + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['points_num'])){ + return $this->error('提取积分不能为空'); + } + if(empty($data['price'])){ + return $this->error('兑换金额不能为空'); + } + + $result = MemberWithdrawModel::create($data); + if($result){ + $this->success('新增成功', url('index'),'_parent_reload'); + }else{ + $this->error('新增失败'); + } + }else{ + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['text', 'points_num', '提取积分'], + ['text', 'vip_points_num', 'VIP提取积分'], + ['text', 'price', '兑换金额'], + ['text', 'sort', '排序(越小前台显示越靠前)','',9999], + ]) + ->fetch(); + } + } + + //编辑城市 + public function edit($id=null){ + if ($id === null) return $this->error('缺少参数'); + + if($this->request->isPost()){ + $data = $this->request->post(); + + if(empty($data['points_num'])){ + return $this->error('提取积分不能为空'); + } + if(empty($data['price'])){ + return $this->error('兑换金额不能为空'); + } + $data['update_time'] = time(); + + $result = MemberWithdrawModel::update($data); + if($result){ + $this->success('修改成功', url('index'),'_parent_reload'); + }else{ + $this->error('修改失败'); + } + } else { + $info = MemberWithdrawModel::get($id); + return ZBuilder::make('form') + ->addFormItems([//批量添加表单项 + ['hidden', 'id'], + ['text', 'points_num', '提取积分'], + ['text', 'vip_points_num', 'VIP提取积分'], + ['text', 'price', '兑换金额'], + ['text', 'sort', '排序(越小前台显示越靠前)','',9999], + ]) + ->setFormData($info) + ->fetch(); + } + } + + //删除 + public function delete($id = null) + { + if ($id === null) { + $this->error('缺少参数'); + } + + $data = MemberWithdrawModel::where('id',$id)->find(); + if (empty($data)) {$this->error('删除成功!'); } + + $result = MemberWithdrawModel::where('id',$id)->delete(); + if($result) { + return $this->success('删除成功!'); + }else{ + return $this->error('删除失败!'); + } + } + +} \ No newline at end of file diff --git a/application/other/common.php b/application/other/common.php new file mode 100644 index 0000000..ea404c9 --- /dev/null +++ b/application/other/common.php @@ -0,0 +1,13 @@ + 'other', + // 模块标题[必填] + 'title' => '其他', + // 模块唯一标识[必填],格式:模块名.开发者标识.module + 'identifier' => 'other.maurylee.module', + // 模块图标[选填] + 'icon' => 'fa fa-fw fa-ellipsis-v', + // 模块描述[选填] + 'description' => '其他模块', + // 开发者[必填] + 'author' => 'maurylee', + // 开发者网址[选填] + 'author_url' => '', + // 版本[必填],格式采用三段式:主版本号.次版本号.修订版本号 + 'version' => '1.0.0', + // 模块依赖[可选],格式[[模块名, 模块唯一标识, 依赖版本, 对比方式]] + 'need_module' => [], + // 插件依赖[可选],格式[[插件名, 插件唯一标识, 依赖版本, 对比方式]] + 'need_plugin' => [], + // 数据表[有数据库表时必填] + 'tables' => [], + // 原始数据库表前缀 + // 用于在导入模块sql时,将原有的表前缀转换成系统的表前缀 + // 一般模块自带sql文件时才需要配置 + 'database_prefix' => '', + + + //模块参数配置 + 'config' => [], + + + // 行为配置 + 'action' => [], + + + // 授权配置 + 'access' => [], +]; + diff --git a/application/other/menus.php b/application/other/menus.php new file mode 100644 index 0000000..9591560 --- /dev/null +++ b/application/other/menus.php @@ -0,0 +1,29 @@ + '其他', + 'icon' => 'fa fa-fw fa-ellipsis-v', + 'url_type' => 'module_admin', + 'url_value' => 'product/index/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [], + ], + +]; + diff --git a/application/other/model/Advert.php b/application/other/model/Advert.php new file mode 100644 index 0000000..be2b6fa --- /dev/null +++ b/application/other/model/Advert.php @@ -0,0 +1,25 @@ + + * @return bool + */ + public static function changeModule($id = 0, $module = '') + { + if ($id > 0) { + $ids = self::where('pid', $id)->column('id'); + if ($ids) { + foreach ($ids as $id) { + self::where('id', $id)->setField('module', $module); + self::changeModule($id, $module); + } + } + } + return true; + } + + /** + * 获取树形节点 + * @param int $id 需要隐藏的节点id + * @param string $default 默认第壹个节点项,默认为“顶级节点”,如果为false则不显示,也可传入其他名称 + * @param string $module 模型名 + * @author loomis <2477365162@qq.com> + * @return mixed + */ + public static function getMenuTree($id = 0, $default = '', $module = '') + { + $result[0] = '顶级节点'; + $where['status'] = 1; + if ($module != '') { + $where['module'] = $module; + } + + // 排除指定节点及其子节点 + if ($id !== 0) { + $hide_ids = array_merge([$id], self::getChildsId($id)); + $where['id'] = ['notin', $hide_ids]; + } + // 获取节点 + $menus = Tree::toList(self::where($where)->order('pid,sort')->column('id,pid,title')); + foreach ($menus as $menu) { + $result[$menu['id']] = $menu['title_display']; + } + + // 设置默认节点项标题 + if ($default != '') { + $result[0] = $default; + } + + // 隐藏默认节点项 + if ($default === false) { + unset($result[0]); + } + + return $result; + } + + /** + * 获取顶部节点 + * @param string $max 最多返回多少个 + * @param string $cache_tag 缓存标签 + * @author loomis <2477365162@qq.com> + * @return array + */ + public static function getTopMenu($max = '', $cache_tag = '') + { + $menus = cache($cache_tag); + if (!$menus) { + // 非开发模式,只显示可以显示的菜单 + if (config('develop_mode') == 0) { + $map['online_hide'] = 0; + } + $map['status'] = 1; + $map['pid'] = 0; + $menus = self::where($map)->order('sort,id')->limit($max)->column('id,pid,module,title,url_value,url_type,url_target,icon'); + foreach ($menus as $key => &$menu) { + // 没有访问权限的节点不显示 + if (!RoleModel::checkAuth($menu['id'])) { + unset($menus[$key]); + continue; + } + if ($menu['url_value'] != '' && $menu['url_type'] == 'module') { + $url = explode('/', $menu['url_value']); + $menu['controller'] = $url[1]; + $menu['action'] = $url[2]; + $menu['url_value'] = url($menu['url_value']); + } + } + // 非开发模式,缓存菜单 + if (config('develop_mode') == 0) { + cache($cache_tag, $menus); + } + } + return $menus; + } + + /** + * 获取侧栏节点 + * @param string $id 模块id + * @param string $module 模块名 + * @param string $controller 控制器名 + * @author loomis <2477365162@qq.com> + * @return array|mixed + */ + public static function getSidebarMenu($id = '', $module = '', $controller = '') + { + $module = $module == '' ? request()->module() : $module; + $controller = $controller == '' ? request()->controller() : $controller; + $menus = cache('_sidebar_menus.' . $module . '_' . $controller); + if (!$menus) { + // 获取当前节点地址 + $location = self::getLocation($id); + // 当前顶级节点id + $top_id = $location[0]['id']; + // 获取顶级节点下的所有节点 + $map = [ + 'status' => 1, + 'module' => $module + ]; + // 非开发模式,只显示可以显示的菜单 + if (config('develop_mode') == 0) { + $map['online_hide'] = 0; + } + $menus = self::where($map)->order('sort,id')->column('id,pid,module,title,url_value,url_type,url_target,icon'); + + // 解析模块链接 + foreach ($menus as $key => &$menu) { + // 没有访问权限的节点不显示 + if (!RoleModel::checkAuth($menu['id'])) { + unset($menus[$key]); + continue; + } + if ($menu['url_value'] != '' && $menu['url_type'] == 'module') { + $menu['url_value'] = url($menu['url_value']); + } + } + $menus = MenuTree::toLayer($menus, $top_id, 2); + + // 非开发模式,缓存菜单 + if (config('develop_mode') == 0) { + cache('_sidebar_menus.' . $module . '_' . $controller, $menus); + } + } + return $menus; + } + + /** + * 获取指定节点ID的位置 + * @param string $id 节点id,如果没有指定,则取当前节点id + * @param bool $del_last_url 是否删除最后壹个节点的url地址 + * @param bool $check 检查节点是否存在,不存在则抛出错误 + * @author loomis <2477365162@qq.com> + * @return array + * @throws \think\Exception + */ + public static function getLocation($id = '', $del_last_url = false, $check = true) + { + $model = request()->module(); + $controller = request()->controller(); + $action = request()->action(); + + if ($id != '') { + $cache_name = 'location.menu_'.$id; + } else { + $cache_name = 'location.'.$model.'_'.$controller.'_'.$action; + } + + $location = cache($cache_name); + + if (!$location) { + $map['pid'] = ['<>', 0]; + $map['url_value'] = strtolower($model.'/'.trim(preg_replace("/[A-Z]/", "_\\0", $controller), "_").'/'.$action); + + // 当前操作对应的节点ID + $curr_id = $id == '' ? self::where($map)->value('id') : $id; + + // 获取节点ID是所有父级节点 + $location = MenuTree::getParents(self::column('id,pid,title,url_value'), $curr_id); + + if ($check && empty($location)) { + throw new Exception('获取不到当前节点地址,可能未添加节点', 9001); + } + foreach ($location as $key => $value) { + # code... + if(empty($value['url_value'])) $location[$key]['url_value'] = $location[$key+1]['url_value']; + } + + // 剔除最后壹个节点url + if ($del_last_url) { + $location[count($location) - 1]['url_value'] = ''; + } + + // 非开发模式,缓存菜单 + if (config('develop_mode') == 0) { + cache($cache_name, $location); + } + } + return $location; + } + + /** + * 根据分组获取节点 + * @param string $group 分组名称 + * @param bool|string $fields 要返回的字段 + * @param array $map 查找条件 + * @author loomis <2477365162@qq.com> + * @return array + */ + public static function getMenusByGroup($group = '', $fields = true, $map = []) + { + return self::where($map)->order('sort,id')->column($fields, 'id'); + } + + /** + * 获取节点分组 + * @author loomis <2477365162@qq.com> + * @return array + */ + public static function getGroup() + { + $map['status'] = 1; + $map['pid'] = 0; + $menus = self::where($map)->order('id,sort')->column('module,title'); + return $menus; + } + + /** + * 获取所有子节点id + * @param int $pid 父级id + * @author loomis <2477365162@qq.com> + * @return array + */ + public static function getChildsId($pid = 0) + { + $ids = self::where('pid', $pid)->column('id'); + foreach ($ids as $value) { + $ids = array_merge($ids, self::getChildsId($value)); + } + return $ids; + } + + /** + * 获取壹级数据 + * @param int + * @return array + */ + public static function getCatrgory() + { + $map['pid'] = 0; + $map['status'] = 1; + $order = 'sort asc,create_time desc'; + $data = self::where($map)->order($order)->select(); + return $data; + } + /** + * 获取顶级菜单分类 + */ + public static function getLeftColoum(){ + $res=Cache::get('LeftColoum'); + if(!$res){ + $res= self::where(['pid'=>0])->order('sort asc')->select(); + Cache::set('LeftColoum', $res); + } + return $res; + } + + //获取商品分类信息 + public static function getCatrgoryList(){ + $product_category = cache('product_category'); + if(!$product_category){ + + $ProductCategory = self::field("id,title,thumb")->where(['pid'=>0,'status'=>1])->order('sort asc')->select(); + if(!empty($ProductCategory)){ + + foreach ($ProductCategory as $k=>$v){ + if(!empty($v['thumb'])){ + $ProductCategory[$k]['thumb'] = reception_get_file_path($v['thumb']); + } + + //获取下级分类 + $ProductCategory2 = self::field("id,title")->where(['pid'=>$v['id'],'status'=>1])->order('sort asc')->select(); + if(!empty($ProductCategory2)){ + foreach ($ProductCategory2 as $k2=>$v2){ + //获取下级分类 + $ProductCategory3 = self::field("id,title")->where(['pid'=>$v2['id'],'status'=>1])->order('sort asc')->select(); + $ProductCategory2[$k2]['subclass'] = $ProductCategory3; + } + } + + $ProductCategory[$k]['subclass'] = $ProductCategory2; + } + + cache('product_category', $ProductCategory); + $product_category = $ProductCategory; + } + } + + return $product_category; + } + + /** + * 获取树形节点 及其 等级 + * @param int $id 需要隐藏的节点id + * @param string $default 默认第一个节点项,默认为“顶级节点”,如果为false则不显示,也可传入其他名称 + * @param string $module 模型名 + * @author loomis + * @return mixed + */ + public static function getMenuTreeLevel($id = 0, $default = '', $module = '') + { + $where[] = ['status','egt',0]; + + if (!$module) { + $where[] = ['module','egt',$module]; + } + // 获取节点 + $menus = Tree::toList(self::where($where)->order('pid,id')->column('id,pid,title')); + $result = []; + foreach ($menus as $k => $menu) { + $result[$k]['id'] = $menu['id']; + $result[$k]['title'] = $menu['title_display']; + $result[$k]['level'] = $menu['level']; + // $result[$menu['id']] = $menu['title_display']; + } + return $result; + } +} \ No newline at end of file diff --git a/application/other/validate/Category.php b/application/other/validate/Category.php new file mode 100644 index 0000000..6e11cb1 --- /dev/null +++ b/application/other/validate/Category.php @@ -0,0 +1,31 @@ + +// +---------------------------------------------------------------------- + +namespace app\other\validate; + +use think\Validate; + +/** + * 商品分类 + * @author 王海鑫 + */ +class Category extends Validate +{ + //定义验证规则 + protected $rule = [ + 'title|分类名称' => 'require' + ]; + + protected $message = [ + 'title.require' => '分类名称不能为空', + ]; + +} \ No newline at end of file diff --git a/application/other/view/admin/category/index.html b/application/other/view/admin/category/index.html new file mode 100644 index 0000000..508f310 --- /dev/null +++ b/application/other/view/admin/category/index.html @@ -0,0 +1,207 @@ +{extend name="$_admin_base_layout" /} + +{block name="plugins-css"} + +{/block} + +{block name="content"} +
    + +

    提示:
    1:按住表头可拖动类别,调整后点击【保存类别】。
    2:类别只沿用到二级类别。

    +
    + +
    +
    +
    + {notempty name="tab_nav"} + + {else/} +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +

    {$page_title}

    +
    + {/notempty} +
    +
    + {notempty name="menus"} +
    +
    +
    +
    + 新增 + + + + + + + + +
    +
    +
    +
    + + + {/notempty} + + {notempty name="modules"} +
    + +
    +
    +
    + {volist name="modules" id="module"} +
    + + {$module.title} +
    + {/volist} +
    +
    +
    +
    + {/notempty} +
    +
    +
    +
    +
    + +{/block} + +{block name="script"} + + + +{/block} diff --git a/application/other/view/admin/layout.html b/application/other/view/admin/layout.html new file mode 100644 index 0000000..4880c5b --- /dev/null +++ b/application/other/view/admin/layout.html @@ -0,0 +1,438 @@ + + + + + + {block name="page-title"}{$page_title|default='后台'} | {:config('web_site_title')} - CCU{/block} + + + + + + + + + + + {notempty name="_css_files"} + {eq name="Think.config.minify_status" value="1"} + + {else/} + {volist name="_css_files" id="css"} + {:load_assets($css)} + {/volist} + {/eq} + {/notempty} + {block name="plugins-css"}{/block} + + {eq name="Think.config.minify_status" value="1"} + + {else/} + + + + + + + + {/eq} + + {block name="style"}{/block} + {notempty name="Think.get._pop"} + + {/notempty} + + + + + + + + + + + + + + + +{eq name="Think.config.minify_status" value="1"} + +{else/} + + + + + + + + + + + + + + + + + + +{/eq} + + + +{notempty name="_js_files"} +{eq name="Think.config.minify_status" value="1"} + +{else/} +{volist name="_js_files" id="js"} +{:load_assets($js, 'js')} +{/volist} +{/eq} +{/notempty} + + + + + + +{block name="script"}{/block} + + + \ No newline at end of file diff --git a/application/provider.php b/application/provider.php new file mode 100644 index 0000000..b07c54d --- /dev/null +++ b/application/provider.php @@ -0,0 +1,14 @@ + +// +---------------------------------------------------------------------- + +// 应用容器绑定定义 +return [ +]; diff --git a/application/tags.php b/application/tags.php new file mode 100644 index 0000000..0754b54 --- /dev/null +++ b/application/tags.php @@ -0,0 +1,34 @@ + +// +---------------------------------------------------------------------- + +// 应用行为扩展定义文件 +return [ + // 应用初始化 + 'app_init' => [], + // 应用调度开始 + 'app_dispatch' => [ + 'app\\common\\behavior\\Config', // 注册配置行为 + ], + // 应用开始 + 'app_begin' => [ + 'app\\common\\behavior\\Hook', // 注册钩子行为 + ], + // 模块初始化 + 'module_init' => [], + // 操作开始执行 + 'action_begin' => [], + // 视图内容过滤 + 'view_filter' => [], + // 日志写入 + 'log_write' => [], + // 应用结束 + 'app_end' => [], +]; diff --git a/application/user/admin/Index.php b/application/user/admin/Index.php new file mode 100644 index 0000000..f62174d --- /dev/null +++ b/application/user/admin/Index.php @@ -0,0 +1,605 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\exception\DbException + */ + public function index() + { + cookie('__forward__', $_SERVER['REQUEST_URI']); + + // 获取查询条件 + $map = $this->getMap(); + // 非超级管理员检查可管理角色 + if (session('user_auth.role') != 1) { + $role_list = RoleModel::getChildsId(session('user_auth.role')); + $map[] = ['role', 'in', $role_list]; + } + + // 数据列表 + $data_list = UserModel::where($map)->order('sort,role,id desc')->paginate(); + + // 授权按钮 + $btn_access = [ + 'title' => '授权', + 'icon' => 'fa fa-fw fa-key', + 'href' => url('access', ['uid' => '__id__']) + ]; + + // 角色列表 + if (session('user_auth.role') != 1) { + $role_list = RoleModel::getTree(null, false, session('user_auth.role')); + } else { + $role_list = RoleModel::getTree(); + } + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('用户管理') // 设置页面标题 + ->setTableName('admin_user') // 设置数据表名 + ->setSearch(['id' => 'ID', 'username' => '用户名', 'email' => '邮箱']) // 设置搜索参数 + ->addColumns([ // 批量添加列 + ['id', 'ID'], + ['username', '用户名'], + ['nickname', '昵称'], + ['role', '角色', $role_list], + ['email', '邮箱'], + ['mobile', '手机号'], + ['create_time', '创建时间', 'datetime'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButton('custom', $btn_access) // 添加授权按钮 + ->addRightButtons('edit,delete') // 批量添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染页面 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + // 验证 + $result = $this->validate($data, 'User'); + // 验证失败 输出错误信息 + if(true !== $result) $this->error($result); + + // 非超级管理需要验证可选择角色 + if (session('user_auth.role') != 1) { + if ($data['role'] == session('user_auth.role')) { + $this->error('禁止创建与当前角色同级的用户'); + } + $role_list = RoleModel::getChildsId(session('user_auth.role')); + if (!in_array($data['role'], $role_list)) { + $this->error('权限不足,禁止创建非法角色的用户'); + } + + if (isset($data['roles'])) { + $deny_role = array_diff($data['roles'], $role_list); + if ($deny_role) { + $this->error('权限不足,附加角色设置错误'); + } + } + } + + $data['roles'] = isset($data['roles']) ? implode(',', $data['roles']) : ''; + + if ($user = UserModel::create($data)) { + Hook::listen('user_add', $user); + // 记录行为 + action_log('user_add', 'admin_user', $user['id'], UID); + $this->success('新增成功', url('index')); + } else { + $this->error('新增失败'); + } + } + + // 角色列表 + if (session('user_auth.role') != 1) { + $role_list = RoleModel::getTree(null, false, session('user_auth.role')); + } else { + $role_list = RoleModel::getTree(null, false); + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('新增') // 设置页面标题 + ->addFormItems([ // 批量添加表单项 + ['text', 'username', '用户名', '必填,可由英文字母、数字组成'], + ['text', 'nickname', '昵称', '可以是中文'], + ['select', 'role', '主角色', '非超级管理员,禁止创建与当前角色同级的用户', $role_list], + ['select', 'roles', '副角色', '可多选', $role_list, '', 'multiple'], + ['text', 'email', '邮箱', ''], + ['password', 'password', '密码', '必填,6-20位'], + ['text', 'mobile', '手机号'], + ['image', 'avatar', '头像'], + ['radio', 'status', '状态', '', ['禁用', '启用'], 1] + ]) + ->fetch(); + } + + /** + * 编辑 + * @param null $id 用户id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function edit($id = null) + { + if ($id === null) $this->error('缺少参数'); + + // 非超级管理员检查可编辑用户 + if (session('user_auth.role') != 1) { + $role_list = RoleModel::getChildsId(session('user_auth.role')); + $user_list = UserModel::where('role', 'in', $role_list)->column('id'); + if (!in_array($id, $user_list)) { + $this->error('权限不足,没有可操作的用户'); + } + } + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + // 禁止修改超级管理员的角色和状态 + if ($data['id'] == 1 && $data['role'] != 1) { + $this->error('禁止修改超级管理员角色'); + } + + // 禁止修改超级管理员的状态 + if ($data['id'] == 1 && $data['status'] != 1) { + $this->error('禁止修改超级管理员状态'); + } + + // 验证 + $result = $this->validate($data, 'User.update'); + // 验证失败 输出错误信息 + if(true !== $result) $this->error($result); + + // 如果没有填写密码,则不更新密码 + if ($data['password'] == '') { + unset($data['password']); + } + + // 非超级管理需要验证可选择角色 + if (session('user_auth.role') != 1) { + if ($data['role'] == session('user_auth.role')) { + $this->error('禁止修改为当前角色同级的用户'); + } + $role_list = RoleModel::getChildsId(session('user_auth.role')); + if (!in_array($data['role'], $role_list)) { + $this->error('权限不足,禁止修改为非法角色的用户'); + } + + if (isset($data['roles'])) { + $deny_role = array_diff($data['roles'], $role_list); + if ($deny_role) { + $this->error('权限不足,附加角色设置错误'); + } + } + } + + $data['roles'] = isset($data['roles']) ? implode(',', $data['roles']) : ''; + + if (UserModel::update($data)) { + $user = UserModel::get($data['id']); + Hook::listen('user_edit', $user); + // 记录行为 + action_log('user_edit', 'admin_user', $user['id'], UID, get_nickname($user['id'])); + $this->success('编辑成功', cookie('__forward__')); + } else { + $this->error('编辑失败'); + } + } + + // 获取数据 + $info = UserModel::where('id', $id)->field('password', true)->find(); + + // 角色列表 + if (session('user_auth.role') != 1) { + $role_list = RoleModel::getTree(null, false, session('user_auth.role')); + } else { + $role_list = RoleModel::getTree(null, false); + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑') // 设置页面标题 + ->addFormItems([ // 批量添加表单项 + ['hidden', 'id'], + ['static', 'username', '用户名', '不可更改'], + ['text', 'nickname', '昵称', '可以是中文'], + ['select', 'role', '主角色', '非超级管理员,禁止创建与当前角色同级的用户', $role_list], + ['select', 'roles', '副角色', '可多选', $role_list, '', 'multiple'], + ['text', 'email', '邮箱', ''], + ['password', 'password', '密码', '必填,6-20位'], + ['text', 'mobile', '手机号'], + ['image', 'avatar', '头像'], + ['radio', 'status', '状态', '', ['禁用', '启用']] + ]) + ->setFormData($info) // 设置表单数据 + ->fetch(); + } + + /** + * 授权 + * @param string $module 模块名 + * @param int $uid 用户id + * @param string $tab 分组tab + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + * @throws \think\exception\PDOException + */ + public function access($module = '', $uid = 0, $tab = '') + { + if ($uid === 0) $this->error('缺少参数'); + + // 非超级管理员检查可编辑用户 + if (session('user_auth.role') != 1) { + $role_list = RoleModel::getChildsId(session('user_auth.role')); + $user_list = UserModel::where('role', 'in', $role_list)->column('id'); + if (!in_array($uid, $user_list)) { + $this->error('权限不足,没有可操作的用户'); + } + } + + // 获取所有授权配置信息 + $list_module = ModuleModel::where('access', 'neq', '') + ->where('access', 'neq', '') + ->where('status', 1) + ->column('name,title,access'); + + if ($list_module) { + // tab分组信息 + $tab_list = []; + foreach ($list_module as $key => $value) { + $list_module[$key]['access'] = json_decode($value['access'], true); + // 配置分组信息 + $tab_list[$value['name']] = [ + 'title' => $value['title'], + 'url' => url('access', [ + 'module' => $value['name'], + 'uid' => $uid + ]) + ]; + } + $module = $module == '' ? current(array_keys($list_module)) : $module; + $this->assign('tab_nav', [ + 'tab_list' => $tab_list, + 'curr_tab' => $module + ]); + + // 读取授权内容 + $access = $list_module[$module]['access']; + foreach ($access as $key => $value) { + $access[$key]['url'] = url('access', [ + 'module' => $module, + 'uid' => $uid, + 'tab' => $key + ]); + } + + // 当前分组 + $tab = $tab == '' ? current(array_keys($access)) : $tab; + // 当前授权 + $curr_access = $access[$tab]; + if (!isset($curr_access['nodes'])) { + $this->error('模块:'.$module.' 数据授权配置缺少nodes信息'); + } + $curr_access_nodes = $curr_access['nodes']; + + $this->assign('tab', $tab); + $this->assign('access', $access); + + if ($this->request->isPost()) { + $post = $this->request->param(); + if (isset($post['nodes'])) { + $data_node = []; + foreach ($post['nodes'] as $node) { + list($group, $nid) = explode('|', $node); + $data_node[] = [ + 'module' => $module, + 'group' => $group, + 'uid' => $uid, + 'nid' => $nid, + 'tag' => $post['tag'] + ]; + } + + // 先删除原有授权 + $map['module'] = $post['module']; + $map['tag'] = $post['tag']; + $map['uid'] = $post['uid']; + if (false === AccessModel::where($map)->delete()) { + $this->error('清除旧授权失败'); + } + + // 添加新的授权 + $AccessModel = new AccessModel; + if (!$AccessModel->saveAll($data_node)) { + $this->error('操作失败'); + } + + // 调用后置方法 + if (isset($curr_access_nodes['model_name']) && $curr_access_nodes['model_name'] != '') { + if (strpos($curr_access_nodes['model_name'], '/')) { + list($module, $model_name) = explode('/', $curr_access_nodes['model_name']); + } else { + $model_name = $curr_access_nodes['model_name']; + } + $class = "app\\{$module}\\model\\".$model_name; + $model = new $class; + try{ + $model->afterAccessUpdate($post); + }catch(\Exception $e){} + } + + // 记录行为 + $nids = implode(',', $post['nodes']); + $details = "模块($module),分组(".$post['tag']."),授权节点ID($nids)"; + action_log('user_access', 'admin_user', $uid, UID, $details); + $this->success('操作成功', url('access', ['uid' => $post['uid'], 'module' => $module, 'tab' => $tab])); + } else { + // 清除所有数据授权 + $map['module'] = $post['module']; + $map['tag'] = $post['tag']; + $map['uid'] = $post['uid']; + if (false === AccessModel::where($map)->delete()) { + $this->error('清除旧授权失败'); + } else { + $this->success('操作成功'); + } + } + } else { + $nodes = []; + if (isset($curr_access_nodes['model_name']) && $curr_access_nodes['model_name'] != '') { + if (strpos($curr_access_nodes['model_name'], '/')) { + list($module, $model_name) = explode('/', $curr_access_nodes['model_name']); + } else { + $model_name = $curr_access_nodes['model_name']; + } + $class = "app\\{$module}\\model\\".$model_name; + $model = new $class; + + try{ + $nodes = $model->access(); + }catch(\Exception $e){ + $this->error('模型:'.$class."缺少“access”方法"); + } + } else { + // 没有设置模型名,则按表名获取数据 + $fields = [ + $curr_access_nodes['primary_key'], + $curr_access_nodes['parent_id'], + $curr_access_nodes['node_name'] + ]; + + $nodes = Db::name($curr_access_nodes['table_name'])->order($curr_access_nodes['primary_key'])->field($fields)->select(); + $tree_config = [ + 'title' => $curr_access_nodes['node_name'], + 'id' => $curr_access_nodes['primary_key'], + 'pid' => $curr_access_nodes['parent_id'] + ]; + $nodes = Tree::config($tree_config)->toLayer($nodes); + } + + // 查询当前用户的权限 + $map = [ + 'module' => $module, + 'tag' => $tab, + 'uid' => $uid + ]; + $node_access = AccessModel::where($map)->select(); + $user_access = []; + foreach ($node_access as $item) { + $user_access[$item['group'].'|'.$item['nid']] = 1; + } + + $nodes = $this->buildJsTree($nodes, $curr_access_nodes, $user_access); + $this->assign('nodes', $nodes); + } + + $page_tips = isset($curr_access['page_tips']) ? $curr_access['page_tips'] : ''; + $tips_type = isset($curr_access['tips_type']) ? $curr_access['tips_type'] : 'info'; + $this->assign('page_tips', $page_tips); + $this->assign('tips_type', $tips_type); + } + + $this->assign('module', $module); + $this->assign('uid', $uid); + $this->assign('tab', $tab); + $this->assign('page_title', '数据授权'); + return $this->fetch(); + } + + /** + * 构建jstree代码 + * @param array $nodes 节点 + * @param array $curr_access 当前授权信息 + * @param array $user_access 用户授权信息 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + private function buildJsTree($nodes = [], $curr_access = [], $user_access = []) + { + $result = ''; + if (!empty($nodes)) { + $option = [ + 'opened' => true, + 'selected' => false + ]; + foreach ($nodes as $node) { + $key = $curr_access['group'].'|'.$node[$curr_access['primary_key']]; + $option['selected'] = isset($user_access[$key]) ? true : false; + if (isset($node['child'])) { + $curr_access_child = isset($curr_access['child']) ? $curr_access['child'] : $curr_access; + $result .= '
  • '.$node[$curr_access['node_name']].$this->buildJsTree($node['child'], $curr_access_child, $user_access).'
  • '; + } else { + $result .= '
  • '.$node[$curr_access['node_name']].'
  • '; + } + } + } + + return '
      '.$result.'
    '; + } + + /** + * 删除用户 + * @param array $ids 用户id + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function delete($ids = []) + { + Hook::listen('user_delete', $ids); + return $this->setStatus('delete'); + } + + /** + * 启用用户 + * @param array $ids 用户id + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function enable($ids = []) + { + Hook::listen('user_enable', $ids); + return $this->setStatus('enable'); + } + + /** + * 禁用用户 + * @param array $ids 用户id + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function disable($ids = []) + { + Hook::listen('user_disable', $ids); + return $this->setStatus('disable'); + } + + /** + * 设置用户状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $ids = (array)$ids; + + // 当前用户所能操作的用户 + $role_list = RoleModel::getChildsId(session('user_auth.role')); + $user_list = UserModel::where('role', 'in', $role_list)->column('id'); + if (session('user_auth.role') != 1 && !$user_list) { + $this->error('权限不足,没有可操作的用户'); + } + + $ids = array_intersect($user_list, $ids); + if (!$ids) { + $this->error('权限不足,没有可操作的用户'); + } + + switch ($type) { + case 'enable': + if (false === UserModel::where('id', 'in', $ids)->setField('status', 1)) { + $this->error('启用失败'); + } + break; + case 'disable': + if (false === UserModel::where('id', 'in', $ids)->setField('status', 0)) { + $this->error('禁用失败'); + } + break; + case 'delete': + if (false === UserModel::where('id', 'in', $ids)->delete()) { + $this->error('删除失败'); + } + break; + default: + $this->error('非法操作'); + } + + action_log('user_'.$type, 'admin_user', '', UID); + + $this->success('操作成功'); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $id == UID && $this->error('禁止操作当前账号'); + $field = input('post.name', ''); + $value = input('post.value', ''); + + // 非超级管理员检查可操作的用户 + if (session('user_auth.role') != 1) { + $role_list = RoleModel::getChildsId(session('user_auth.role')); + $user_list = UserModel::where('role', 'in', $role_list)->column('id'); + if (!in_array($id, $user_list)) { + $this->error('权限不足,没有可操作的用户'); + } + } + + $config = UserModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $config . '),新值:(' . $value . ')'; + return parent::quickEdit(['user_edit', 'admin_user', $id, UID, $details]); + } +} diff --git a/application/user/admin/Message.php b/application/user/admin/Message.php new file mode 100644 index 0000000..5d38253 --- /dev/null +++ b/application/user/admin/Message.php @@ -0,0 +1,114 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\exception\DbException + */ + public function index() + { + $data_list = MessageModel::where($this->getMap()) + ->order($this->getOrder('id DESC')) + ->paginate(); + + return ZBuilder::make('table') + ->setTableName('admin_message') + ->addTopButton('add') + ->addTopButton('delete') + ->addRightButton('edit') + ->addRightButton('delete') + ->addColumns([ + ['id', 'ID'], + ['uid_receive', '接收者', 'callback', 'get_nickname'], + ['uid_send', '发送者', 'callback', 'get_nickname'], + ['type', '分类'], + ['content', '内容'], + ['status', '状态', 'status', '', ['未读', '已读']], + ['create_time', '发送时间', 'datetime'], + ['read_time', '阅读时间', 'datetime'], + ['right_button', '操作', 'btn'], + ]) + ->addFilter('type') + ->addFilter('status', ['未读', '已读']) + ->setRowList($data_list) + ->fetch(); + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function add() + { + if ($this->request->isPost()) { + $data = $this->request->post(); + + $data['type'] == '' && $this->error('请填写消息分类'); + $data['content'] == '' && $this->error('请填写消息内容'); + + $list = []; + if ($data['send_type'] == 'uid') { + !isset($data['uid']) && $this->error('请选择接收消息的用户'); + } else { + !isset($data['role']) && $this->error('请选择接收消息的角色'); + $data['uid'] = UserModel::where('status', 1) + ->where('role', 'in', $data['role']) + ->column('id'); + !$data['uid'] && $this->error('所选角色无可发送的用户'); + } + + foreach ($data['uid'] as $uid) { + $list[] = [ + 'uid_receive' => $uid, + 'uid_send' => UID, + 'type' => $data['type'], + 'content' => $data['content'], + ]; + } + + $MessageModel = new MessageModel; + if (false !== $MessageModel->saveAll($list)) { + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + return ZBuilder::make('form') + ->addFormItems([ + ['text', 'type', '消息分类'], + ['textarea', 'content', '消息内容'], + ['radio', 'send_type', '发送方式', '', ['uid' => '按指定用户', 'role' => '按指定角色'], 'uid'], + ['select', 'uid', '接收用户', '接收消息的用户', UserModel::where('status', 1)->column('id,nickname'), '', 'multiple'], + ['select', 'role', '接收角色', '接收消息的角色', RoleModel::where('status', 1)->column('id,name'), '', 'multiple'], + ]) + ->setTrigger('send_type', 'uid', 'uid') + ->setTrigger('send_type', 'role', 'role') + ->fetch(); + } +} diff --git a/application/user/admin/Publics.php b/application/user/admin/Publics.php new file mode 100644 index 0000000..5692682 --- /dev/null +++ b/application/user/admin/Publics.php @@ -0,0 +1,155 @@ + + * @return mixed + */ + public function signin() + { + if ($this->request->isPost()) { + // 获取post数据 + $data = $this->request->post(); + $rememberme = isset($data['remember-me']) ? true : false; + + // 登录钩子 + $hook_result = Hook::listen('signin', $data); + if (!empty($hook_result) && true !== $hook_result[0]) { + $this->error($hook_result[0]); + } + + // 验证数据 + $result = $this->validate($data, 'User.signin'); + if(true !== $result){ + // 验证失败 输出错误信息 + $this->error($result); + } + + // 验证码 + if (config('captcha_signin')) { + $captcha = $this->request->post('captcha', ''); + $captcha == '' && $this->error('请输入验证码'); + if(!captcha_check($captcha, '')){ + //验证失败 + $this->error('验证码错误或失效'); + }; + } + + // 登录 + $UserModel = new UserModel; + $uid = $UserModel->login($data['username'], $data['password'], $rememberme); + if ($uid) { + // 记录行为 + action_log('user_signin', 'admin_user', $uid, $uid); + $this->jumpUrl(); + } else { + $this->error($UserModel->getError()); + } + } else { + + $hook_result = Hook::listen('signin_sso'); + if (!empty($hook_result) && true !== $hook_result[0]) { + if (isset($hook_result[0]['url'])) { + $this->redirect($hook_result[0]['url']); + } + if (isset($hook_result[0]['error'])) { + $this->error($hook_result[0]['error']); + } + } + + if (is_signin()) { + $this->jumpUrl(); + } else { + return $this->fetch(); + } + } + } + + /** + * 跳转到第一个有权限访问的url + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|string + */ + private function jumpUrl() + { + if (session('user_auth.role') == 1) { + $this->success('登录成功', url('admin/index/index')); + } + + $default_module = RoleModel::where('id', session('user_auth.role'))->value('default_module'); + $menu = MenuModel::get($default_module); + if (!$menu) { + $this->error('当前角色未指定默认跳转模块!'); + } + + if ($menu['url_type'] == 'link') { + $this->success('登录成功', $menu['url_value']); + } + + $menu_url = explode('/', $menu['url_value']); + role_auth(); + + $menus = MenuModel::getSidebarMenu($default_module, $menu['module'], $menu_url[1]); + $url = ''; + foreach ($menus as $key => $menu) { + if (!empty($menu['url_value'])) { + $url = $menu['url_value']; + break; + } + if (!empty($menu['child'])) { + $url = $menu['child'][0]['url_value']; + break; + } + } + + if ($url == '') { + $this->error('权限不足'); + } else { + $this->success('登录成功', $url); + } + } + + /** + * 退出登录 + * @author 蔡伟明 <314013107@qq.com> + */ + public function signout() + { + $hook_result = Hook::listen('signout_sso'); + if (!empty($hook_result) && true !== $hook_result[0]) { + if (isset($hook_result[0]['url'])) { + $this->redirect($hook_result[0]['url']); + } + if (isset($hook_result[0]['error'])) { + $this->error($hook_result[0]['error']); + } + } + + session(null); + cookie('uid', null); + cookie('signin_token', null); + + $this->redirect('signin'); + } +} diff --git a/application/user/admin/Role.php b/application/user/admin/Role.php new file mode 100644 index 0000000..f42bf8a --- /dev/null +++ b/application/user/admin/Role.php @@ -0,0 +1,454 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\exception\DbException + */ + public function index() + { + // 获取查询条件 + $map = $this->getMap(); + // 非超级管理员检查可管理角色 + if (session('user_auth.role') != 1) { + $role_list = RoleModel::getChildsId(session('user_auth.role')); + $map[] = ['id', 'in', $role_list]; + } + // 数据列表 + $data_list = RoleModel::where($map)->order('pid,id')->paginate(); + // 角色列表 + $list_role = RoleModel::column('id,name'); + $list_role[0] = '顶级角色'; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('角色管理') // 页面标题 + ->setTableName('admin_role') // 设置表名 + ->setSearch(['name' => '角色名称', 'id' => 'ID']) // 设置搜索参数 + ->addColumns([ // 批量添加列 + ['id', 'ID'], + ['name', '角色名称'], + ['pid', '上级角色', $list_role], + ['description', '描述'], + ['default_module', '默认模块', 'callback', function($value, $list_module){ + if ($value == '') { + return '未设置'; + } else { + return isset($list_module[$value]) ? $list_module[$value] : '模块不存在'; + } + }, MenuModel::where('pid', 0)->column('id,title')], + ['create_time', '创建时间', 'datetime'], + ['access', '是否可登录后台', 'switch'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons('edit,delete') // 批量添加右侧按钮 + ->replaceRightButton(['id' => 1], '') // 修改id为1的按钮 + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + if (!isset($data['menu_auth'])) { + $data['menu_auth'] = []; + } else { + $data['menu_auth'] = explode(',', $data['menu_auth']); + } + // 验证 + $result = $this->validate($data, 'Role'); + // 验证失败 输出错误信息 + if(true !== $result) $this->error($result); + + // 非超级管理员检查可添加角色 + if (session('user_auth.role') != 1) { + $role_list = RoleModel::getChildsId(session('user_auth.role')); + if (!in_array($data['pid'], $role_list)) { + $this->error('所属角色设置错误,没有权限添加该角色'); + } + + // 非超级管理员检查可添加的节点权限 + $menu_auth = RoleModel::where('id', session('user_auth.role'))->value('menu_auth'); + $menu_auth = json_decode($menu_auth, true); + $menu_auth = array_intersect($menu_auth, $data['menu_auth']); + $data['menu_auth'] = $menu_auth; + } + + // 添加数据 + if ($role = RoleModel::create($data)) { + // 记录行为 + action_log('role_add', 'admin_role', $role['id'], UID, $data['name']); + $this->success('新增成功', url('index')); + } else { + $this->error('新增失败'); + } + } + + // 菜单列表 + $menus = cache('access_menus'); + if (!$menus) { + $modules = Db::name('admin_module')->where('status', 1)->column('name,title'); + $map = []; + // 非超级管理员角色,只能分配当前角色所拥有的权限 + if (session('user_auth.role') != 1) { + $menu_auth = RoleModel::where('id', session('user_auth.role'))->value('menu_auth'); + $menu_auth = json_decode($menu_auth, true); + $map[] = ['id', 'in', $menu_auth]; + } + + // 当前用户能分配的所有菜单 + $menus = MenuModel::where('module', 'in', array_keys($modules)) + ->where($map) + ->order('module,sort,id') + ->column('id,pid,sort,url_value,title,icon,module'); + + // 按模块分组菜单 + $moduleMenus = []; + foreach ($menus as $key => $menu) { + if (!isset($moduleMenus[$menu['module']])) { + $moduleMenus[$menu['module']] = [ + 'title' => isset($modules[$menu['module']]) ? $modules[$menu['module']] : '未知', + 'menus' => [$menu] + ]; + } else { + $moduleMenus[$menu['module']]['menus'][] = $menu; + } + } + + // 层级化每个模块的菜单 + foreach ($moduleMenus as $key => $module) { + $menu = Tree::toLayer($module['menus']); + $moduleMenus[$key]['menus'] = $this->buildJsTree($menu); + } + $menus = $moduleMenus; + + // 非开发模式,缓存菜单 + if (config('develop_mode') == 0) { + cache('access_menus', $menus); + } + } + + if (session('user_auth.role') != 1) { + $role_list = RoleModel::getTree(null, false, session('user_auth.role')); + } else { + $role_list = RoleModel::getTree(); + } + + $this->assign('page_title', '新增'); + $this->assign('role_list', $role_list); + $this->assign('module_list', MenuModel::where('pid', 0)->column('id,title')); + $this->assign('menus', $menus); + $this->assign('curr_tab', current(array_keys($menus))); + return $this->fetch(); + } + + /** + * 编辑 + * @param null $id 角色id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function edit($id = null) + { + if ($id === null) $this->error('缺少参数'); + if ($id == 1) $this->error('超级管理员不可修改'); + + // 非超级管理员检查可编辑角色 + if (session('user_auth.role') != 1) { + $role_list = RoleModel::getChildsId(session('user_auth.role')); + if (!in_array($id, $role_list)) { + $this->error('权限不足,当前没有编辑该角色的权限!'); + } + } + + // 获取数据 + $info = RoleModel::get($id); + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!isset($data['menu_auth'])) { + $data['menu_auth'] = []; + } else { + $data['menu_auth'] = explode(',', $data['menu_auth']); + } + + if ($data['pid'] == '') { + $data['pid'] = $info['pid']; + } + + // 验证 + $result = $this->validate($data, 'Role'); + // 验证失败 输出错误信息 + if(true !== $result) $this->error($result); + + // 非超级管理员检查可添加角色 + if (session('user_auth.role') != 1) { + $role_list = RoleModel::getChildsId(session('user_auth.role')); + if ($data['pid'] != $info['pid'] && !in_array($data['pid'], $role_list)) { + $this->error('所属角色设置错误,没有权限添加该角色'); + } + } + + // 检查所属角色不能是自己当前角色及其子角色 + $role_list = RoleModel::getChildsId($data['id']); + if ($data['id'] == $data['pid'] || in_array($data['pid'], $role_list)) { + $this->error('所属角色设置错误,禁止设置为当前角色及其子角色。'); + } + + // 非超级管理员检查可添加的节点权限 + if (session('user_auth.role') != 1) { + $menu_auth = RoleModel::where('id', session('user_auth.role'))->value('menu_auth'); + $menu_auth = json_decode($menu_auth, true); + $menu_auth = array_intersect($menu_auth, $data['menu_auth']); + $data['menu_auth'] = $menu_auth; + } + + if (RoleModel::update($data)) { + // 更新成功,循环处理子角色权限 + RoleModel::resetAuth($id, $data['menu_auth']); + role_auth(); + // 记录行为 + action_log('role_edit', 'admin_role', $id, UID, $data['name']); + $this->success('编辑成功', url('index')); + } else { + $this->error('编辑失败'); + } + } + + if (session('user_auth.role') != 1) { + $role_list = RoleModel::getTree($id, false, session('user_auth.role')); + } else { + $role_list = RoleModel::getTree($id, '顶级角色'); + } + + $modules = Db::name('admin_module')->where('status', 1)->column('name,title'); + $map = []; + // 非超级管理员角色,只能分配当前角色所拥有的权限 + if (session('user_auth.role') != 1) { + $menu_auth = RoleModel::where('id', session('user_auth.role'))->value('menu_auth'); + $menu_auth = json_decode($menu_auth, true); + $map[] = ['id', 'in', $menu_auth]; + } + + // 当前用户能分配的所有菜单 + $menus = MenuModel::where('module', 'in', array_keys($modules)) + ->where($map) + ->order('module,sort,id') + ->column('id,pid,sort,url_value,title,icon,module'); + + // 按模块分组菜单 + $moduleMenus = []; + foreach ($menus as $key => $menu) { + if (!isset($moduleMenus[$menu['module']])) { + $moduleMenus[$menu['module']] = [ + 'title' => isset($modules[$menu['module']]) ? $modules[$menu['module']] : '未知', + 'menus' => [$menu] + ]; + } else { + $moduleMenus[$menu['module']]['menus'][] = $menu; + } + } + + // 层级化每个模块的菜单 + foreach ($moduleMenus as $key => $module) { + $menu = Tree::toLayer($module['menus']); + $moduleMenus[$key]['menus'] = $this->buildJsTree($menu, $info); + } + + $this->assign('page_title', '编辑'); + $this->assign('role_list', $role_list); + $this->assign('module_list', MenuModel::where('pid', 0)->column('id,title')); + $this->assign('menus', $moduleMenus); + $this->assign('curr_tab', current(array_keys($moduleMenus))); + $this->assign('info', $info); + return $this->fetch('edit'); + } + + /** + * 构建jstree代码 + * @param array $menus 菜单节点 + * @param array $user 用户信息 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + private function buildJsTree($menus = [], $user = []) + { + $result = ''; + if (!empty($menus)) { + $option = [ + 'opened' => true, + 'selected' => false, + 'icon' => '', + ]; + foreach ($menus as $menu) { + $option['icon'] = $menu['icon']; + if (isset($user['menu_auth'])) { + $option['selected'] = in_array($menu['id'], $user['menu_auth']) ? true : false; + } + if (isset($menu['child'])) { + $result .= '
  • '.$menu['title'].($menu['url_value'] == '' ? '' : ' ('.$menu['url_value'].')').$this->buildJsTree($menu['child'], $user).'
  • '; + } else { + $result .= '
  • '.$menu['title'].($menu['url_value'] == '' ? '' : ' ('.$menu['url_value'].')').'
  • '; + } + } + } + + return '
      '.$result.'
    '; + } + + /** + * 删除角色 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function delete($record = []) + { + return $this->setStatus('delete'); + } + + /** + * 启用角色 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用角色 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置角色状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @throws \think\Exception + * @throws \think\exception\PDOException + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $ids = (array)$ids; + + // 当前角色所能操作的子角色 + $role_list = RoleModel::getChildsId(session('user_auth.role')); + if (session('user_auth.role') != 1 && !$role_list) { + $this->error('权限不足,没有可操作的角色'); + } + + foreach ($ids as $id) { + if ($id == 1) { + // 跳过默认角色 + continue; + } + + // 非超级管理员检查可管理角色 + if (session('user_auth.role') != 1) { + if (!in_array($id, $role_list)) { + $this->error('权限不足,禁止操作角色ID:'.$id); + } + } + + switch ($type) { + case 'enable': + if (false === RoleModel::where('id', $id)->setField('status', 1)) { + $this->error('启用失败,角色ID:'.$id); + } + break; + case 'disable': + if (false === RoleModel::where('id', $id)->setField('status', 0)) { + $this->error('禁用失败,角色ID:'.$id); + } + break; + case 'delete': + $all_id = array_merge([$id], RoleModel::getChildsId($id)); + + if (false === RoleModel::where('id', 'in', $all_id)->delete()) { + $this->error('删除失败,角色ID:'.$id); + } + break; + default: + $this->error('非法操作'); + } + + action_log('role_'.$type, 'admin_role', $id, UID); + } + + $this->success('操作成功'); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + + // 非超级管理员检查可操作的角色 + if (session('user_auth.role') != 1) { + $role_list = RoleModel::getChildsId(session('user_auth.role')); + if (!in_array($id, $role_list)) { + $this->error('权限不足,没有可操作的角色'); + } + } + + $config = RoleModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $config . '),新值:(' . $value . ')'; + return parent::quickEdit(['role_edit', 'admin_role', $id, UID, $details]); + } +} diff --git a/application/user/model/Message.php b/application/user/model/Message.php new file mode 100644 index 0000000..a4678d6 --- /dev/null +++ b/application/user/model/Message.php @@ -0,0 +1,35 @@ + + * @return int|string + */ + public static function getMessageCount() + { + return self::where(['status' => 0, 'uid_receive' => UID])->count(); + } +} diff --git a/application/user/model/Role.php b/application/user/model/Role.php new file mode 100644 index 0000000..d6fe4e0 --- /dev/null +++ b/application/user/model/Role.php @@ -0,0 +1,214 @@ + + * @return mixed + */ + public static function getTree($id = null, $default = '', $filter = null) + { + $result[0] = '顶级角色'; + $where = [ + ['status', '=', 1] + ]; + + // 排除指定菜单及其子菜单 + $hide_ids = []; + if ($id !== null) { + $hide_ids = array_merge([$id], self::getChildsId($id)); + } + + // 过滤显示指定角色及其子角色 + if ($filter !== null) { + $show_ids = self::getChildsId($filter); + + if (!empty($hide_ids)) { + $ids = array_diff($show_ids, $hide_ids); + $where[] = ['id', 'in', $ids]; + } else { + $where[] = ['id', 'in', $show_ids]; + } + } else { + if (!empty($hide_ids)) { + $where[] = ['id', 'not in', $hide_ids]; + } + } + + // 获取菜单 + $roles = self::where($where)->column('id,pid,name'); + $pid = self::where($where)->order('pid')->value('pid'); + $roles = Tree::config(['title' => 'name'])->toList($roles, $pid); + foreach ($roles as $role) { + $result[$role['id']] = $role['title_display']; + } + + // 设置默认菜单项标题 + if ($default != '') { + $result[0] = $default; + } + + // 隐藏默认菜单项 + if ($default === false) { + unset($result[0]); + } + return $result; + } + + /** + * 获取所有子角色id + * @param string $pid 父级id + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function getChildsId($pid = '') + { + $ids = self::where('pid', $pid)->column('id'); + foreach ($ids as $value) { + $ids = array_merge($ids, self::getChildsId($value)); + } + return $ids; + } + + /** + * 检查访问权限 + * @param int $id 需要检查的节点ID,默认检查当前操作节点 + * @param bool $url 是否为节点url,默认为节点id + * @author 蔡伟明 <314013107@qq.com> + * @return bool + * @throws \think\Exception + */ + public static function checkAuth($id = 0, $url = false) + { + // 当前用户的角色 + $role = session('user_auth.role'); + + // id为1的是超级管理员,或者角色为1的,拥有最高权限 + if (session('user_auth.uid') == '1' || $role == '1') { + return true; + } + + // 获取当前用户的权限 + $menu_auth = session('role_menu_auth'); + + // 检查权限 + if ($menu_auth) { + if ($id !== 0) { + return $url === false ? isset($menu_auth[$id]) : in_array($id, $menu_auth); + } + // 获取当前操作的id + $location = MenuModel::getLocation(); + $action = end($location); + + return $url === false ? isset($menu_auth[$action['id']]) : in_array($action['url_value'], $menu_auth); + } + + // 其他情况一律没有权限 + return false; + } + + /** + * 读取当前角色权限 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function roleAuth() + { + $menu_auth = cache('role_menu_auth_'.session('user_auth.role')); + if (!$menu_auth) { + $menu_auth = self::where('id', session('user_auth.role'))->value('menu_auth'); + $menu_auth = json_decode($menu_auth, true); + $menu_auth = MenuModel::where('id', 'in', $menu_auth)->column('id,url_value'); + } + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('role_menu_auth_'.session('user_auth.role'), $menu_auth); + } + return $menu_auth; + } + + /** + * 根据节点id获取所有角色id和权限 + * @param string $menu_id 节点id + * @param bool $menu_auth 是否返回所有节点权限 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function getRoleWithMenu($menu_id = '', $menu_auth = false) + { + if ($menu_auth) { + return self::where('menu_auth', 'like', '%"'.$menu_id.'"%')->column('id,menu_auth'); + } else { + return self::where('menu_auth', 'like', '%"'.$menu_id.'"%')->column('id'); + } + } + + /** + * 根据角色id获取权限 + * @param array $role 角色id + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function getAuthWithRole($role = []) + { + return self::where('id', 'in', $role)->column('id,menu_auth'); + } + + /** + * 重设权限 + * @param null $pid 父级id + * @param array $new_auth 新权限 + * @author 蔡伟明 <314013107@qq.com> + */ + public static function resetAuth($pid = null, $new_auth = []) + { + if ($pid !== null) { + $data = self::where('pid', $pid)->column('id,menu_auth'); + foreach ($data as $id => $menu_auth) { + $menu_auth = json_decode($menu_auth, true); + $menu_auth = json_encode(array_intersect($menu_auth, $new_auth)); + self::where('id', $id)->setField('menu_auth', $menu_auth); + self::resetAuth($id, $new_auth); + } + } + } +} diff --git a/application/user/model/User.php b/application/user/model/User.php new file mode 100644 index 0000000..9ca97df --- /dev/null +++ b/application/user/model/User.php @@ -0,0 +1,149 @@ + + * @return bool|mixed + */ + public function login($username = '', $password = '', $rememberme = false) + { + $username = trim($username); + $password = trim($password); + + // 匹配登录方式 + if (preg_match("/^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/", $username)) { + // 邮箱登录 + $map['email'] = $username; + } elseif (preg_match("/^1\d{10}$/", $username)) { + // 手机号登录 + $map['mobile'] = $username; + } else { + // 用户名登录 + $map['username'] = $username; + } + + $map['status'] = 1; + + // 查找用户 + $user = $this::get($map); + if (!$user) { + $this->error = '账号或者密码错误!'; + } else { + // 检查是否分配用户组 + if ($user['role'] == 0) { + $this->error = '禁止访问,原因:未分配角色!'; + return false; + } + // 检查是可登录后台 + if (!RoleModel::where(['id' => $user['role'], 'status' => 1])->value('access')) { + $this->error = '禁止访问,用户所在角色未启用或禁止访问后台!'; + return false; + } + if (!Hash::check((string)$password, $user['password'])) { + $this->error = '账号或者密码错误!'; + } else { + $uid = $user['id']; + + // 更新登录信息 + $user['last_login_time'] = request()->time(); + $user['last_login_ip'] = request()->ip(1); + if ($user->save()) { + // 自动登录 + return $this->autoLogin($this::get($uid), $rememberme); + } else { + // 更新登录信息失败 + $this->error = '登录信息更新失败,请重新登录!'; + return false; + } + } + } + return false; + } + + /** + * 自动登录 + * @param object $user 用户对象 + * @param bool $rememberme 是否记住登录,默认7天 + * @author 蔡伟明 <314013107@qq.com> + * @return bool|int + */ + public function autoLogin($user, $rememberme = false) + { + // 记录登录SESSION和COOKIES + $auth = array( + 'uid' => $user->id, + 'group' => $user->group, + 'role' => $user->role, + 'role_name' => Db::name('admin_role')->where('id', $user->role)->value('name'), + 'avatar' => $user->avatar, + 'username' => $user->username, + 'nickname' => $user->nickname, + 'last_login_time' => $user->last_login_time, + 'last_login_ip' => get_client_ip(1), + ); + session('user_auth', $auth); + session('user_auth_sign', data_auth_sign($auth)); + + // 保存用户节点权限 + if ($user->role != 1) { + $menu_auth = Db::name('admin_role')->where('id', session('user_auth.role'))->value('menu_auth'); + $menu_auth = json_decode($menu_auth, true); + if (!$menu_auth) { + session('user_auth', null); + session('user_auth_sign', null); + $this->error = '未分配任何节点权限!'; + return false; + } + } + + // 记住登录 + if ($rememberme) { + $signin_token = $user->username.$user->id.$user->last_login_time; + cookie('uid', $user->id, 24 * 3600 * 7); + cookie('signin_token', data_auth_sign($signin_token), 24 * 3600 * 7); + } + + return $user->id; + } +} diff --git a/application/user/validate/Role.php b/application/user/validate/Role.php new file mode 100644 index 0000000..7b41114 --- /dev/null +++ b/application/user/validate/Role.php @@ -0,0 +1,26 @@ + + */ +class Role extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'pid|所属角色' => 'require', + 'name|角色名称' => 'require|unique:admin_role', + ]; +} diff --git a/application/user/validate/User.php b/application/user/validate/User.php new file mode 100644 index 0000000..d65ec1b --- /dev/null +++ b/application/user/validate/User.php @@ -0,0 +1,51 @@ + + */ +class User extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'username|用户名' => 'require|alphaNum|unique:admin_user', + 'nickname|昵称' => 'require|unique:admin_user', + 'role|角色' => 'require', + 'email|邮箱' => 'email|unique:admin_user', + 'password|密码' => 'require|length:6,20', + 'mobile|手机号' => 'regex:^1\d{10}|unique:admin_user', + '__token__' => 'require|token', + ]; + + // 定义验证提示 + protected $message = [ + 'username.require' => '请输入用户名', + 'email.require' => '邮箱不能为空', + 'email.email' => '邮箱格式不正确', + 'email.unique' => '该邮箱已存在', + 'password.require' => '密码不能为空', + 'password.length' => '密码长度6-20位', + 'mobile.regex' => '手机号不正确', + '__token__.token' => '令牌数据无效,请刷新页面', + ]; + + // 定义验证场景 + protected $scene = [ + //更新 + 'update' => ['email', 'password' => 'length:6,20', 'mobile', 'role', '__token__'], + //登录 + 'signin' => ['username' => 'require', 'password' => 'require'], + ]; +} diff --git a/application/user/view/admin/index/access.html b/application/user/view/admin/index/access.html new file mode 100644 index 0000000..0f85741 --- /dev/null +++ b/application/user/view/admin/index/access.html @@ -0,0 +1,175 @@ +{extend name="$_admin_base_layout" /} + +{block name="plugins-css"} + +{/block} + +{block name="content"} +{notempty name="page_tips"} +
    + +

    {$page_tips|raw|default=''}

    +
    +{/notempty} +
    +
    +
    + {notempty name="tab_nav.tab_list"} + + {else/} +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +

    {$page_title}

    +
    + {/notempty} + +
    + + +
    +
    + {notempty name="access"} +
    +
    +
    + + + + +
    +
    +
    +
    + +
    +
    +
    {$nodes|raw|default=''}
    +
    +
    +
    + {else/} +
    +

    + 暂无授权数据
    +

    +
    + {/notempty} +
    +
    + {notempty name="access"} + + {/notempty} + + 返回 + +
    +
    +
    +
    +
    +
    +{/block} + +{block name="script"} + + +{/block} diff --git a/application/user/view/admin/publics/signin.html b/application/user/view/admin/publics/signin.html new file mode 100644 index 0000000..6915b41 --- /dev/null +++ b/application/user/view/admin/publics/signin.html @@ -0,0 +1,166 @@ + + + + + + + 本地群 · 搭子 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    + +
    + +

    本地群 · 搭子

    +
    + + + + + +
    +
    +
    +
    +
    + + + +
    + © 本地群 · 搭子 +
    + + + + + + + + + + + + + + + + + + + + + {:hook('signin_footer')} + + + \ No newline at end of file diff --git a/application/user/view/admin/role/add.html b/application/user/view/admin/role/add.html new file mode 100644 index 0000000..40861ea --- /dev/null +++ b/application/user/view/admin/role/add.html @@ -0,0 +1,230 @@ +{extend name="$_admin_base_layout" /} + +{block name="style"} + +{/block} + +{block name="content"} +
    +
    +
    + + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    该角色登录后,默认跳转的模块。注意,该角色必须有该模块的节点访问权限。
    +
    +
    +
    + +
    + + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    +
    + {notempty name="menus"} +
    + +
    + {volist name="menus" id="vo"} +
    +
    {$vo.menus|raw|default=''}
    +
    + {/volist} +
    +
    + {else/} +

    暂无可分配节点

    + {/notempty} +
    +
    + + +
    +
    +
    +
    +
    +
    +{/block} + +{block name="script"} + + +{/block} diff --git a/application/user/view/admin/role/edit.html b/application/user/view/admin/role/edit.html new file mode 100644 index 0000000..fed0528 --- /dev/null +++ b/application/user/view/admin/role/edit.html @@ -0,0 +1,231 @@ +{extend name="$_admin_base_layout" /} + +{block name="style"} + +{/block} + +{block name="content"} +
    +
    +
    + + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    该角色登录后,默认跳转的模块。注意,该角色必须有该模块的节点访问权限。
    +
    +
    +
    + +
    + + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    +
    + {notempty name="menus"} +
    + +
    + {volist name="menus" id="vo"} +
    +
    {$vo.menus|raw|default=''}
    +
    + {/volist} +
    +
    + {else/} +

    暂无可分配节点

    + {/notempty} +
    +
    + + +
    +
    +
    +
    +
    +
    +{/block} + +{block name="script"} + + +{/block} diff --git a/build.php b/build.php new file mode 100644 index 0000000..b87ccee --- /dev/null +++ b/build.php @@ -0,0 +1,26 @@ + +// +---------------------------------------------------------------------- + +return [ + // 生成应用公共文件 + '__file__' => ['common.php'], + + // 定义demo模块的自动生成 (按照实际定义的文件名生成) + 'demo' => [ + '__file__' => ['common.php'], + '__dir__' => ['behavior', 'controller', 'model', 'view'], + 'controller' => ['Index', 'Test', 'UserType'], + 'model' => ['User', 'UserType'], + 'view' => ['index/index'], + ], + + // 其他更多的模块定义 +]; diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..01247ca --- /dev/null +++ b/composer.json @@ -0,0 +1,38 @@ +{ + "name": "topthink/think", + "description": "the new thinkphp framework", + "type": "project", + "keywords": [ + "framework", + "thinkphp", + "ORM" + ], + "homepage": "http://thinkphp.cn/", + "license": "Apache-2.0", + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "require": { + "php": ">=5.6.0", + "topthink/framework": "5.1.*", + "topthink/think-captcha": "^2.0", + "topthink/think-image": "^1.0", + "topthink/think-helper": "^1.0", + "ezyang/htmlpurifier": "^4.9", + "overtrue/wechat": "~4.0" + }, + "autoload": { + "psr-4": { + "app\\": "application" + } + }, + "extra": { + "think-path": "thinkphp" + }, + "config": { + "preferred-install": "dist" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..599bfbf --- /dev/null +++ b/composer.lock @@ -0,0 +1,2479 @@ +{ + "_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#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "6044084e824040ef6d97a3ca861df919", + "packages": [ + { + "name": "easywechat-composer/easywechat-composer", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/mingyoung/easywechat-composer.git", + "reference": "3fc6a7ab6d3853c0f4e2922539b56cc37ef361cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mingyoung/easywechat-composer/zipball/3fc6a7ab6d3853c0f4e2922539b56cc37ef361cd", + "reference": "3fc6a7ab6d3853c0f4e2922539b56cc37ef361cd", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=7.0" + }, + "require-dev": { + "composer/composer": "^1.0 || ^2.0", + "phpunit/phpunit": "^6.5 || ^7.0" + }, + "type": "composer-plugin", + "extra": { + "class": "EasyWeChatComposer\\Plugin" + }, + "autoload": { + "psr-4": { + "EasyWeChatComposer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "张铭阳", + "email": "mingyoungcheung@gmail.com" + } + ], + "description": "The composer plugin for EasyWeChat", + "support": { + "issues": "https://github.com/mingyoung/easywechat-composer/issues", + "source": "https://github.com/mingyoung/easywechat-composer/tree/1.4.1" + }, + "time": "2021-07-05T04:03:22+00:00" + }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.10.0", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/d85d39da4576a6934b72480be6978fb10c860021", + "reference": "d85d39da4576a6934b72480be6978fb10c860021", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "require-dev": { + "simpletest/simpletest": "^1.1" + }, + "type": "library", + "autoload": { + "psr-0": { + "HTMLPurifier": "library/" + }, + "files": [ + "library/HTMLPurifier.composer.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "time": "2018-02-23T01:58:20+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2024-07-24T11:22:20+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.5.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e", + "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.5.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-05-21T12:31:43+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2024-07-18T11:15:46+00:00" + }, + { + "name": "monolog/monolog", + "version": "2.9.3", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "a30bfe2e142720dfa990d0a7e573997f5d884215" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/a30bfe2e142720dfa990d0a7e573997f5d884215", + "reference": "a30bfe2e142720dfa990d0a7e573997f5d884215", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2@dev", + "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpspec/prophecy": "^1.15", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.38 || ^9.6.19", + "predis/predis": "^1.1 || ^2.0", + "rollbar/rollbar": "^1.3 || ^2 || ^3", + "ruflin/elastica": "^7", + "swiftmailer/swiftmailer": "^5.3|^6.0", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/2.9.3" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2024-04-12T20:52:51+00:00" + }, + { + "name": "overtrue/socialite", + "version": "2.0.24", + "source": { + "type": "git", + "url": "https://github.com/overtrue/socialite.git", + "reference": "ee7e7b000ec7d64f2b8aba1f6a2eec5cdf3f8bec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/overtrue/socialite/zipball/ee7e7b000ec7d64f2b8aba1f6a2eec5cdf3f8bec", + "reference": "ee7e7b000ec7d64f2b8aba1f6a2eec5cdf3f8bec", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^5.0|^6.0|^7.0", + "php": ">=5.6", + "symfony/http-foundation": "^2.7|^3.0|^4.0|^5.0" + }, + "require-dev": { + "mockery/mockery": "~1.2", + "phpunit/phpunit": "^6.0|^7.0|^8.0|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Overtrue\\Socialite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "overtrue", + "email": "anzhengchao@gmail.com" + } + ], + "description": "A collection of OAuth 2 packages that extracts from laravel/socialite.", + "keywords": [ + "login", + "oauth", + "qq", + "social", + "wechat", + "weibo" + ], + "support": { + "issues": "https://github.com/overtrue/socialite/issues", + "source": "https://github.com/overtrue/socialite/tree/2.0.24" + }, + "funding": [ + { + "url": "https://www.patreon.com/overtrue", + "type": "patreon" + } + ], + "time": "2021-05-13T16:04:48+00:00" + }, + { + "name": "overtrue/wechat", + "version": "4.6.0", + "source": { + "type": "git", + "url": "https://github.com/w7corp/easywechat.git", + "reference": "52af4cbe777cd4aea307beafa0a4518c347467b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/w7corp/easywechat/zipball/52af4cbe777cd4aea307beafa0a4518c347467b1", + "reference": "52af4cbe777cd4aea307beafa0a4518c347467b1", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "easywechat-composer/easywechat-composer": "^1.1", + "ext-fileinfo": "*", + "ext-openssl": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^6.2 || ^7.0", + "monolog/monolog": "^1.22 || ^2.0", + "overtrue/socialite": "~2.0", + "php": ">=7.2", + "pimple/pimple": "^3.0", + "psr/simple-cache": "^1.0", + "symfony/cache": "^3.3 || ^4.3 || ^5.0", + "symfony/event-dispatcher": "^4.3 || ^5.0", + "symfony/http-foundation": "^2.7 || ^3.0 || ^4.0 || ^5.0", + "symfony/psr-http-message-bridge": "^0.3 || ^1.0 || ^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.15", + "mikey179/vfsstream": "^1.6", + "mockery/mockery": "^1.2.3", + "phpstan/phpstan": "^0.12.0", + "phpunit/phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/Kernel/Support/Helpers.php", + "src/Kernel/Helpers.php" + ], + "psr-4": { + "EasyWeChat\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "overtrue", + "email": "anzhengchao@gmail.com" + } + ], + "description": "微信SDK", + "keywords": [ + "easywechat", + "sdk", + "wechat", + "weixin", + "weixin-sdk" + ], + "support": { + "issues": "https://github.com/w7corp/easywechat/issues", + "source": "https://github.com/w7corp/easywechat/tree/4.6.0" + }, + "funding": [ + { + "url": "https://github.com/overtrue", + "type": "github" + } + ], + "abandoned": "w7corp/easywechat", + "time": "2022-08-24T07:30:42+00:00" + }, + { + "name": "pimple/pimple", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/silexphp/Pimple.git", + "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a94b3a4db7fb774b3d78dad2315ddc07629e1bed", + "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1 || ^2.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^5.4@dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "Pimple": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Pimple, a simple Dependency Injection Container", + "homepage": "https://pimple.symfony.com", + "keywords": [ + "container", + "dependency injection" + ], + "support": { + "source": "https://github.com/silexphp/Pimple/tree/v3.5.0" + }, + "time": "2021-10-28T11:13:42+00:00" + }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/master" + }, + "time": "2016-08-06T20:24:11+00:00" + }, + { + "name": "psr/container", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "2ae37329ee82f91efadc282cc2d527fd6065a5ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/2ae37329ee82f91efadc282cc2d527fd6065a5ef", + "reference": "2ae37329ee82f91efadc282cc2d527fd6065a5ef", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.1" + }, + "time": "2021-03-24T13:40:57+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "e616d01114759c4c489f93b099585439f795fe35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0 || ^2.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": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + }, + "time": "2023-04-10T20:10:41+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://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" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/master" + }, + "time": "2017-10-23T01:57:42+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "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.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/cache", + "version": "v5.4.42", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "6f5f750692bd5a212e01a4f1945fd856bceef89e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/6f5f750692bd5a212e01a4f1945fd856bceef89e", + "reference": "6f5f750692bd5a212e01a4f1945fd856bceef89e", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.5", + "psr/cache": "^1.0|^2.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^1.1.7|^2", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/var-exporter": "^4.4|^5.0|^6.0" + }, + "conflict": { + "doctrine/dbal": "<2.13.1", + "symfony/dependency-injection": "<4.4", + "symfony/http-kernel": "<4.4", + "symfony/var-dumper": "<4.4" + }, + "provide": { + "psr/cache-implementation": "1.0|2.0", + "psr/simple-cache-implementation": "1.0|2.0", + "symfony/cache-implementation": "1.0|2.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/cache": "^1.6|^2.0", + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/filesystem": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v5.4.42" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-07-10T06:02:18+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v2.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "64be4a7acb83b6f2bf6de9a02cee6dad41277ebc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/64be4a7acb83b6f2bf6de9a02cee6dad41277ebc", + "reference": "64be4a7acb83b6f2bf6de9a02cee6dad41277ebc", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.5", + "psr/cache": "^1.0|^2.0|^3.0" + }, + "suggest": { + "symfony/cache-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v2.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v5.4.40", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "a54e2a8a114065f31020d6a89ede83e34c3b27a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a54e2a8a114065f31020d6a89ede83e34c3b27a4", + "reference": "a54e2a8a114065f31020d6a89ede83e34c3b27a4", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher-contracts": "^2|^3", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "symfony/dependency-injection": "<4.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/stopwatch": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.40" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:33:22+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v2.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/f98b54df6ad059855739db6fcbc2d36995283fe1", + "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.5", + "psr/event-dispatcher": "^1" + }, + "suggest": { + "symfony/event-dispatcher-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v5.4.44", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "ae0d217e5932aa0b70ddb4cf7822cc76d48aee53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ae0d217e5932aa0b70ddb4cf7822cc76d48aee53", + "reference": "ae0d217e5932aa0b70ddb4cf7822cc76d48aee53", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "predis/predis": "^1.0|^2.0", + "symfony/cache": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", + "symfony/mime": "^4.4|^5.0|^6.0", + "symfony/rate-limiter": "^5.2|^6.0" + }, + "suggest": { + "symfony/mime": "To use the file extension guesser" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v5.4.44" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-15T07:55:06+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/581ca6067eb62640de5ff08ee1ba6850a0ee472e", + "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.5", + "psr/http-message": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/http-foundation": "^5.4 || ^6.0" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "psr/log": "^1.1 || ^2 || ^3", + "symfony/browser-kit": "^5.4 || ^6.0", + "symfony/config": "^5.4 || ^6.0", + "symfony/event-dispatcher": "^5.4 || ^6.0", + "symfony/framework-bundle": "^5.4 || ^6.0", + "symfony/http-kernel": "^5.4 || ^6.0", + "symfony/phpunit-bridge": "^6.2" + }, + "suggest": { + "nyholm/psr7": "For a super lightweight PSR-7/17 implementation" + }, + "type": "symfony-bridge", + "extra": { + "branch-alias": { + "dev-main": "2.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "http://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/symfony/psr-http-message-bridge/issues", + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-26T11:53:26+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v1.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "191afdcb5804db960d26d8566b7e9a2843cab3a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/191afdcb5804db960d26d8566b7e9a2843cab3a0", + "reference": "191afdcb5804db960d26d8566b7e9a2843cab3a0", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.1.3" + }, + "suggest": { + "psr/container": "", + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v1.1.2" + }, + "time": "2019-05-28T07:50:59+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v5.4.40", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "6a13d37336d512927986e09f19a4bed24178baa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/6a13d37336d512927986e09f19a4bed24178baa6", + "reference": "6a13d37336d512927986e09f19a4bed24178baa6", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "symfony/var-dumper": "^4.4.9|^5.0.9|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v5.4.40" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:33:22+00:00" + }, + { + "name": "topthink/framework", + "version": "v5.1.42", + "dist": { + "type": "zip", + "url": "https://mirrors.tencent.com/repository/composer/topthink/framework/v5.1.42/topthink-framework-v5.1.42.zip", + "reference": "ecf1a90d397d821ce2df58f7d47e798c17eba3ad", + "shasum": "" + }, + "require": { + "php": ">=5.6.0", + "topthink/think-installer": "2.*" + }, + "require-dev": { + "johnkary/phpunit-speedtrap": "^1.0", + "mikey179/vfsstream": "~1.6", + "phpdocumentor/reflection-docblock": "^2.0", + "phploc/phploc": "2.*", + "phpunit/phpunit": "^5.0|^6.0", + "sebastian/phpcpd": "2.*", + "squizlabs/php_codesniffer": "2.*" + }, + "type": "think-framework", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + }, + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "the new thinkphp framework", + "homepage": "http://thinkphp.cn/", + "keywords": [ + "framework", + "orm", + "thinkphp" + ], + "time": "2022-10-25T15:04:49+00:00" + }, + { + "name": "topthink/think-captcha", + "version": "v2.0.2", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-captcha/zipball/54c8a51552f99ff9ea89ea9c272383a8f738ceee", + "reference": "54c8a51552f99ff9ea89ea9c272383a8f738ceee", + "shasum": "" + }, + "require": { + "topthink/framework": "5.1.*" + }, + "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": "2017-12-31T16:37:49+00:00" + }, + { + "name": "topthink/think-helper", + "version": "v1.0.7", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-helper/zipball/5f92178606c8ce131d36b37a57c58eb71e55f019", + "reference": "5f92178606c8ce131d36b37a57c58eb71e55f019", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "think\\helper\\": "src" + }, + "files": [ + "src/helper.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "The ThinkPHP5 Helper Package", + "time": "2018-10-05T00:43:21+00:00" + }, + { + "name": "topthink/think-image", + "version": "v1.0.7", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-image/zipball/8586cf47f117481c6d415b20f7dedf62e79d5512", + "reference": "8586cf47f117481c6d415b20f7dedf62e79d5512", + "shasum": "" + }, + "require": { + "ext-gd": "*" + }, + "require-dev": { + "phpunit/phpunit": "4.8.*", + "topthink/framework": "^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "The ThinkPHP5 Image Package", + "time": "2016-09-29T06:05:43+00:00" + }, + { + "name": "topthink/think-installer", + "version": "v2.0.5", + "dist": { + "type": "zip", + "url": "https://mirrors.tencent.com/repository/composer/topthink/think-installer/v2.0.5/topthink-think-installer-v2.0.5.zip", + "reference": "38ba647706e35d6704b5d370c06f8a160b635f88", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0||^2.0" + }, + "require-dev": { + "composer/composer": "^1.0||^2.0" + }, + "type": "composer-plugin", + "extra": { + "class": "think\\composer\\Plugin" + }, + "autoload": { + "psr-4": { + "think\\composer\\": "src" + } + }, + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "time": "2021-01-14T12:12:14+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.6.0" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..7d31ad3 --- /dev/null +++ b/config/app.php @@ -0,0 +1,172 @@ + +// +---------------------------------------------------------------------- + +use think\facade\Env; + +// +---------------------------------------------------------------------- +// | 应用设置 +// +---------------------------------------------------------------------- + +return [ + // +---------------------------------------------------------------------- + // | 系统相关设置 + // +---------------------------------------------------------------------- + + // 后台公共模板 + 'admin_base_layout' => Env::get('app_path') . 'admin/view/layout.html', + // 插件目录路径 + 'plugin_path' => Env::get('root_path'). 'plugins/', + // 数据包目录路径 + 'packet_path' => Env::get('root_path'). 'packet/', + // 文件上传路径 + 'upload_path' => Env::get('root_path') . 'public' . DIRECTORY_SEPARATOR . 'uploads', + // 文件上传临时目录 + 'upload_temp_path' => Env::get('root_path') . 'public' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'temp/', + + // +---------------------------------------------------------------------- + // | 用户相关设置 + // +---------------------------------------------------------------------- + + // 最大缓存用户数 + 'user_max_cache' => 1000, + // 管理员用户ID + 'user_admin' => 1, + + // 应用名称 + 'app_name' => '', + // 应用地址 + 'app_host' => '', + // 应用调试模式 + 'app_debug' => true, + // 应用Trace + 'app_trace' => false, + // 是否支持多模块 + 'app_multi_module' => true, + // 入口自动绑定模块 + 'auto_bind_module' => false, + // 注册的根命名空间 + 'root_namespace' => ['plugins' => Env::get('root_path'). 'plugins/'], + // 默认输出类型 + 'default_return_type' => 'html', + // 默认AJAX 数据返回格式,可选json xml ... + 'default_ajax_return' => 'json', + // 默认JSONP格式返回的处理方法 + 'default_jsonp_handler' => 'jsonpReturn', + // 默认JSONP处理方法 + 'var_jsonp_handler' => 'callback', + // 默认时区 + 'default_timezone' => 'Asia/Shanghai', + // 是否开启多语言 + 'lang_switch_on' => false, + // 默认全局过滤方法 用逗号分隔多个 + 'default_filter' => '', + // 默认语言 + 'default_lang' => 'zh-cn', + // 应用类库后缀 + 'class_suffix' => false, + // 控制器类后缀 + 'controller_suffix' => false, + + // +---------------------------------------------------------------------- + // | 模块设置 + // +---------------------------------------------------------------------- + + // 默认模块名 + 'default_module' => 'index', + // 禁止访问模块 + 'deny_module_list' => ['common'], + // 默认控制器名 + 'default_controller' => 'Index', + // 默认操作名 + 'default_action' => 'index', + // 默认验证器 + 'default_validate' => '', + // 默认的空模块名 + 'empty_module' => '', + // 默认的空控制器名 + 'empty_controller' => 'Error', + // 操作方法前缀 + 'use_action_prefix' => false, + // 操作方法后缀 + 'action_suffix' => '', + // 自动搜索控制器 + 'controller_auto_search' => false, + + // +---------------------------------------------------------------------- + // | URL设置 + // +---------------------------------------------------------------------- + + // PATHINFO变量名 用于兼容模式 + 'var_pathinfo' => 's', + // 兼容PATH_INFO获取 + 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'], + // pathinfo分隔符 + 'pathinfo_depr' => '/', + // HTTPS代理标识 + 'https_agent_name' => '', + // IP代理获取标识 + 'http_agent_ip' => 'X-REAL-IP', + // URL伪静态后缀 + 'url_html_suffix' => 'html', + // URL普通方式参数 用于自动生成 + 'url_common_param' => false, + // URL参数方式 0 按名称成对解析 1 按顺序解析 + 'url_param_type' => 0, + // 是否开启路由延迟解析 + 'url_lazy_route' => false, + // 是否强制使用路由 + 'url_route_must' => false, + // 合并路由规则 + 'route_rule_merge' => false, + // 路由是否完全匹配 + 'route_complete_match' => false, + // 使用注解路由 + 'route_annotation' => false, + // 域名根,如thinkphp.cn + 'url_domain_root' => '', + // 是否自动转换URL中的控制器和操作名 + 'url_convert' => true, + // 默认的访问控制器层 + 'url_controller_layer' => 'controller', + // 表单请求类型伪装变量 + 'var_method' => '_method', + // 表单ajax伪装变量 + 'var_ajax' => '_ajax', + // 表单pjax伪装变量 + 'var_pjax' => '_pjax', + // 是否开启请求缓存 true自动缓存 支持设置请求缓存规则 + 'request_cache' => false, + // 请求缓存有效期 + 'request_cache_expire' => null, + // 全局请求缓存排除规则 + 'request_cache_except' => [], + // 是否开启路由缓存 + 'route_check_cache' => false, + // 路由缓存的Key自定义设置(闭包),默认为当前URL和请求类型的md5 + 'route_check_cache_key' => '', + // 路由缓存类型及参数 + 'route_cache_option' => [], + + // 默认跳转页面对应的模板文件 + 'dispatch_success_tmpl' => Env::get('app_path') . 'admin/view/dispatch_jump.tpl', + 'dispatch_error_tmpl' => Env::get('app_path') . 'admin/view/dispatch_jump.tpl', + + // 异常页面的模板文件 + 'exception_tmpl' => Env::get('think_path') . 'tpl/think_exception.tpl', + + // 错误显示信息,非调试模式有效 + 'error_message' => '页面错误!请稍后再试~', + // 显示错误信息 + 'show_error_msg' => false, + // 异常处理handle类 留空使用 \think\exception\Handle + 'exception_handle' => '', + +]; diff --git a/config/assets.php b/config/assets.php new file mode 100644 index 0000000..4a05c83 --- /dev/null +++ b/config/assets.php @@ -0,0 +1,141 @@ + [ // 默认加载 + "__ADMIN_JS__/core/jquery.min.js", + "__ADMIN_JS__/core/bootstrap.min.js", + "__ADMIN_JS__/core/jquery.slimscroll.min.js", + "__ADMIN_JS__/core/jquery.scrollLock.min.js", + "__ADMIN_JS__/core/jquery.appear.min.js", + "__ADMIN_JS__/core/jquery.countTo.min.js", + "__ADMIN_JS__/core/jquery.placeholder.min.js", + "__ADMIN_JS__/core/js.cookie.min.js", + "__LIBS__/magnific-popup/magnific-popup.min.js", + "__ADMIN_JS__/app.js", + "__ADMIN_JS__/dolphin.js", + "__ADMIN_JS__/builder/form.js", + "__ADMIN_JS__/builder/aside.js", + "__ADMIN_JS__/builder/table.js", + ], + 'core_css' => [ // 默认加载 + "__LIBS__/magnific-popup/magnific-popup.min.css", + "__ADMIN_CSS__/admin/css/bootstrap.min.css", + "__ADMIN_CSS__/admin/css/oneui.css", + "__ADMIN_CSS__/admin/css/dolphin.css", + ], + 'libs_js' => [ // 默认加载 + "__LIBS__/bootstrap-notify/bootstrap-notify.min.js", + "__LIBS__/sweetalert/sweetalert.min.js", + ], + 'libs_css' => [ // 默认加载 + "__LIBS__/sweetalert/sweetalert.min.css", + ], + 'datepicker_js' => [ // 日期选择 + "__LIBS__/bootstrap-datepicker/bootstrap-datepicker.min.js", + "__LIBS__/bootstrap-datepicker/locales/bootstrap-datepicker.zh-CN.min.js", + ], + 'datepicker_css' => [ // 日期选择 + "__LIBS__/bootstrap-datepicker/bootstrap-datepicker3.min.css", + ], + 'datetimepicker_js' => [ // 日期时间选择 + "__LIBS__/bootstrap-datetimepicker/moment.min.js", + "__LIBS__/bootstrap-datetimepicker/bootstrap-datetimepicker.min.js", + "__LIBS__/bootstrap-datetimepicker/locale/zh-cn.js", + ], + 'moment_js' => [ + "__LIBS__/bootstrap-datetimepicker/moment.min.js", + ], + 'datetimepicker_css' => [ // 日期时间选择 + "__LIBS__/bootstrap-datetimepicker/bootstrap-datetimepicker.min.css" + ], + 'webuploader_js' => [ // 文件或图片上传 + "__LIBS__/webuploader/webuploader.min.js", + ], + 'webuploader_css' => [ // 文件或图片上传 + "__LIBS__/webuploader/webuploader.css", + ], + 'select2_js' => [ // 下拉框 + "__LIBS__/select2/select2.full.min.js", + "__LIBS__/select2/i18n/zh-CN.js", + ], + 'select2_css' => [ // 下拉框 + "__LIBS__/select2/select2.min.css", + "__LIBS__/select2/select2-bootstrap.min.css", + ], + 'tags_js' => [ // 标签 + "__LIBS__/jquery-tags-input/jquery.tagsinput.min.js", + ], + 'tags_css' => [ // 标签 + "__LIBS__/jquery-tags-input/jquery.tagsinput.min.css", + ], + 'validate_js' => [ // 验证 + "__LIBS__/jquery-validation/jquery.validate.min.js", + ], + 'editable_js' => [ // 快速编辑 + "__LIBS__/bootstrap3-editable/js/bootstrap-editable.js", + ], + 'editable_css' => [ // 快速编辑 + "__LIBS__/bootstrap3-editable/css/bootstrap-editable.css", + ], + 'colorpicker_js' => [ // 取色器 + "__LIBS__/bootstrap-colorpicker/bootstrap-colorpicker.min.js", + ], + 'colorpicker_css' => [ // 取色器 + "__LIBS__/bootstrap-colorpicker/css/bootstrap-colorpicker.min.css", + ], + 'editormd_js' => [ // markdown编辑器 + "__LIBS__/editormd/editormd.min.js", + ], + 'jcrop_js' => [ // 图片裁剪 + "__LIBS__/jcrop/js/Jcrop.min.js", + ], + 'jcrop_css' => [ // 图片裁剪 + "__LIBS__/jcrop/css/Jcrop.min.css", + ], + 'masked_inputs_js' => [ // 格式文本 + "__LIBS__/masked-inputs/jquery.maskedinput.min.js", + ], + 'rangeslider_js' => [ // 范围 + "__LIBS__/ion-rangeslider/js/ion.rangeSlider.min.js", + ], + 'rangeslider_css' => [ // 范围 + "__LIBS__/ion-rangeslider/css/ion.rangeSlider.min.css", + "__LIBS__/ion-rangeslider/css/ion.rangeSlider.skinHTML5.min.css", + ], + 'nestable_js' => [ // 拖拽排序 + "__LIBS__/jquery-nestable/jquery.nestable.js", + ], + 'nestable_css' => [ // 拖拽排序 + "__LIBS__/jquery-nestable/jquery.nestable.css", + ], + 'wangeditor_js' => [ // wang编辑器 + "__LIBS__/wang-editor/js/wangEditor.min.js", + ], + 'wangeditor_css' => [ // wang编辑器 + "__LIBS__/wang-editor/css/wangEditor.min.css", + ], + 'summernote_js' => [ // summernote编辑器 + "__LIBS__/summernote/summernote.min.js", + "__LIBS__/summernote/lang/summernote-zh-CN.js", + ], + 'summernote_css' => [ // summernote编辑器 + "__LIBS__/summernote/summernote.min.css", + ], + 'jqueryui_js' => [ // jqueryui + "__LIBS__/jquery-ui/jquery-ui.min.js", + ], + 'daterangepicker_js' => [ // 日期时间范围 + "__LIBS__/bootstrap-daterangepicker/daterangepicker.js", + ], + 'daterangepicker_css' => [ // 日期时间范围 + "__LIBS__/bootstrap-daterangepicker/daterangepicker.css", + ] +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..0ac04cd --- /dev/null +++ b/config/cache.php @@ -0,0 +1,25 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | 缓存设置 +// +---------------------------------------------------------------------- + +return [ + // 驱动方式 + 'type' => 'File', + // 缓存保存目录 + 'path' => '', + // 缓存前缀 + 'prefix' => '', + // 缓存有效期 0表示永久缓存 + 'expire' => 0, +]; diff --git a/config/captcha.php b/config/captcha.php new file mode 100644 index 0000000..9902f05 --- /dev/null +++ b/config/captcha.php @@ -0,0 +1,25 @@ + 'buildgroup', + // 验证码图片高度 + 'imageH' => 34, + // 验证码图片宽度 + 'imageW' => 130, + // 验证码字体大小(px) + 'fontSize' => 18, + // 验证码位数 + 'length' => 4, +]; diff --git a/config/console.php b/config/console.php new file mode 100644 index 0000000..40cd346 --- /dev/null +++ b/config/console.php @@ -0,0 +1,20 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | 控制台配置 +// +---------------------------------------------------------------------- +return [ + 'name' => 'Think Console', + 'version' => '0.1', + 'user' => null, + 'auto_path' => env('app_path') . 'command' . DIRECTORY_SEPARATOR, +]; diff --git a/config/cookie.php b/config/cookie.php new file mode 100644 index 0000000..d71211e --- /dev/null +++ b/config/cookie.php @@ -0,0 +1,30 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | Cookie设置 +// +---------------------------------------------------------------------- +return [ + // cookie 名称前缀 + 'prefix' => 'buildgroup_', + // cookie 保存时间 + 'expire' => 0, + // cookie 保存路径 + 'path' => '/', + // cookie 有效域名 + 'domain' => '', + // cookie 启用安全传输 + 'secure' => false, + // httponly设置 + 'httponly' => '', + // 是否使用 setcookie + 'setcookie' => true, +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..98811b5 --- /dev/null +++ b/config/database.php @@ -0,0 +1,61 @@ + 'mysql', + // 服务器地址 + 'hostname' => '127.0.0.1', + // 数据库名 + 'database' => 'buildgroup', + // 用户名 + 'username' => 'buildgroup', + // 密码 + 'password' => 'pT6Aj9b4WRssKKxm', + // 端口 + 'hostport' => '3306', + // 连接dsn + 'dsn' => '', + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => 'ccu_', + // 数据库调试模式 + 'debug' => true, + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 自动读取主库数据 + 'read_master' => false, + // 是否严格检查字段是否存在 + 'fields_strict' => false, + // 数据集返回类型 + 'resultset_type' => 'array', + // 自动写入时间戳字段 + 'auto_timestamp' => false, + // 时间字段取出后的默认时间格式 + 'datetime_format' => false, + // 是否需要进行SQL性能分析 + 'sql_explain' => false, + // Builder类 + 'builder' => '', + // Query类 + 'query' => '\\think\\db\\Query', + // 是否需要断线重连 + 'break_reconnect' => false, + // 断线标识字符串 + 'break_match_str' => [], +]; diff --git a/config/dolphin.php b/config/dolphin.php new file mode 100644 index 0000000..ba08b74 --- /dev/null +++ b/config/dolphin.php @@ -0,0 +1,22 @@ + '本地群-搭子', + 'product_version' => '', + 'build_version' => '202408300938', + 'product_website' => 'http://www.yuchong.net/', + 'product_update' => 'http://www.yuchong.net/', + 'develop_team' => '本地群-搭子', + + // 公司信息 + 'company_name' => '上海裕崇广告有限公司', + 'company_website' => 'http://www.yuchong.net/', +]; \ No newline at end of file diff --git a/config/icon.php b/config/icon.php new file mode 100644 index 0000000..d0b39a9 --- /dev/null +++ b/config/icon.php @@ -0,0 +1,19 @@ + ['at.alicdn.COM'], + // 允许添加自定义图标地址的后缀 + // 如果要添加的图标地址没有后缀,可设置为空数组,即不检查地址是否包含后缀 + 'suffix' => ['css'] +]; diff --git a/config/log.php b/config/log.php new file mode 100644 index 0000000..f735d74 --- /dev/null +++ b/config/log.php @@ -0,0 +1,30 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | 日志设置 +// +---------------------------------------------------------------------- +return [ + // 日志记录方式,内置 file socket 支持扩展 + 'type' => 'File', + // 日志保存目录 + 'path' => '', + // 日志记录级别 + 'level' => [], + // 单文件日志写入 + 'single' => false, + // 独立日志级别 + 'apart_level' => [], + // 最大日志文件数量 + 'max_files' => 0, + // 是否关闭日志写入 + 'close' => false, +]; diff --git a/config/middleware.php b/config/middleware.php new file mode 100644 index 0000000..294228a --- /dev/null +++ b/config/middleware.php @@ -0,0 +1,18 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | 中间件配置 +// +---------------------------------------------------------------------- +return [ + // 默认中间件命名空间 + 'default_namespace' => 'app\\http\\middleware\\', +]; diff --git a/config/module.php b/config/module.php new file mode 100644 index 0000000..fbcee47 --- /dev/null +++ b/config/module.php @@ -0,0 +1,16 @@ + ['admin', 'index', 'common', 'extra','api'] +]; \ No newline at end of file diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..896e4ac --- /dev/null +++ b/config/session.php @@ -0,0 +1,26 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | 会话设置 +// +---------------------------------------------------------------------- + +return [ + 'id' => '', + // SESSION_ID的提交变量,解决flash上传跨域 + 'var_session_id' => '', + // SESSION 前缀 + 'prefix' => 'buildgroup_', + // 驱动方式 支持redis memcache memcached + 'type' => '', + // 是否自动开启 SESSION + 'auto_start' => true, +]; diff --git a/config/system.php b/config/system.php new file mode 100644 index 0000000..21357b2 --- /dev/null +++ b/config/system.php @@ -0,0 +1,112 @@ + false, + // 模块管理中,不读取模块信息的目录 + 'except_module' => ['common', 'admin', 'index', 'extra', 'user', 'install'], + // 函数过滤方式,black_list:黑名单,white_list:白名单 + 'function_filter' => 'white_list', + // 函数黑名单,在黑名单内的函数将不会被执行 + 'function_black_list' => [ + 'eval', + 'passthru', + 'exec', + 'system', + 'chroot', + 'chgrp', + 'popen', + 'ini_alter', + 'ini_restore', + 'dl', + 'openlog', + 'syslog', + 'readlink', + 'symlink', + 'popepassthru', + 'phpinfo', + 'shell_exec', + 'fopen', + 'fclose', + 'fread', + 'fwrite', + 'file_get_contents', + 'file_put_contents', + 'unlink', + 'rename', + 'copy', + 'file', + 'file_exists', + 'mkdir', + 'rmdir', + 'opendir', + 'readdir', + 'scandir', + 'chdir', + 'chroot', + 'dir', + 'closedir', + 'getenv', + 'putenv', + 'get_current_user', + 'get_cfg_var', + 'getmyuid', + 'getmypid', + 'getmyinode', + 'getlastmod', + 'fsockopen', + 'pfsockopen', + 'socket_create', + 'socket_bind', + 'socket_listen', + 'socket_accept', + 'socket_connect', + 'socket_strerror', + 'stream_socket_server', + 'proc_open', + 'proc_close', + 'proc_terminate', + 'proc_get_status', + 'proc_nice', + 'assert', + 'php_uname', + 'getrusage', + 'get_include_path', + 'set_include_path', + 'ini_set', + 'pcntl_exec', + 'posix_kill', + 'posix_mkfifo', + 'posix_setpgid', + 'posix_setsid', + 'posix_setuid', + 'posix_seteuid', + 'posix_setegid', + 'posix_setgid', + 'posix_uname', + 'fileatime', + 'filectime', + 'fileinode', + 'is_dir', + 'is_executable', + 'is_writable', + 'filegroup', + 'fileowner', + 'is_file', + 'is_writeable', + 'stat', + 'fileperms', + 'is_link', + 'parse_ini_file', + 'readfile' + ], + // 函数白名单,在白名单内的函数才会被执行,空则所有函数都不执行 + 'function_white_list' => [] +]; diff --git a/config/template.php b/config/template.php new file mode 100644 index 0000000..87ba8f7 --- /dev/null +++ b/config/template.php @@ -0,0 +1,35 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | 模板设置 +// +---------------------------------------------------------------------- + +return [ + // 模板引擎类型 支持 php think 支持扩展 + 'type' => 'Think', + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法 + 'auto_rule' => 1, + // 模板路径 + 'view_path' => '', + // 模板后缀 + 'view_suffix' => 'html', + // 模板文件名分隔符 + 'view_depr' => DIRECTORY_SEPARATOR, + // 模板引擎普通标签开始标记 + 'tpl_begin' => '{', + // 模板引擎普通标签结束标记 + 'tpl_end' => '}', + // 标签库标签开始标记 + 'taglib_begin' => '{', + // 标签库标签结束标记 + 'taglib_end' => '}', +]; diff --git a/config/trace.php b/config/trace.php new file mode 100644 index 0000000..568054e --- /dev/null +++ b/config/trace.php @@ -0,0 +1,18 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | Trace设置 开启 app_trace 后 有效 +// +---------------------------------------------------------------------- +return [ + // 内置Html Console 支持扩展 + 'type' => 'Html', +]; diff --git a/config/zbuilder.php b/config/zbuilder.php new file mode 100644 index 0000000..ad46f6f --- /dev/null +++ b/config/zbuilder.php @@ -0,0 +1,44 @@ + [ + 'type' => 2, + 'area' => ['80%', '90%'], + 'shadeClose' => true, + 'isOutAnim' => false, + 'anim' => -1 + ], + + // 右侧按钮 + 'right_button' => [ + // 是否显示按钮文字 + 'title' => false, + // 是否显示图标,只有显示文字时才起作用 + 'icon' => true, + // 按钮大小:xs/sm/lg,留空则为普通大小 + 'size' => 'xs', + // 按钮样式:default/primary/success/info/warning/danger + 'style' => 'default' + ], + + // 搜索框 + 'search_button' => false, + + // 表单令牌名称,如果不启用,请设置为false,也可以设置其他名称,如:__hash__ + 'form_token_name' => '__token__' +]; \ No newline at end of file diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..189224c --- /dev/null +++ b/data/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 数据库备份目录 diff --git a/data/install.lock b/data/install.lock new file mode 100644 index 0000000..19104f1 --- /dev/null +++ b/data/install.lock @@ -0,0 +1 @@ +lock \ No newline at end of file diff --git a/export/.gitignore b/export/.gitignore new file mode 100644 index 0000000..b722e9e --- /dev/null +++ b/export/.gitignore @@ -0,0 +1 @@ +!.gitignore \ No newline at end of file diff --git a/export/README.md b/export/README.md new file mode 100644 index 0000000..faf63ec --- /dev/null +++ b/export/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 数据导出目录 diff --git a/export/module/README.md b/export/module/README.md new file mode 100644 index 0000000..bb10976 --- /dev/null +++ b/export/module/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 模块导出目录 diff --git a/extend/.gitignore b/extend/.gitignore new file mode 100644 index 0000000..b722e9e --- /dev/null +++ b/extend/.gitignore @@ -0,0 +1 @@ +!.gitignore \ No newline at end of file diff --git a/extend/Qrcode/Compress.php b/extend/Qrcode/Compress.php new file mode 100644 index 0000000..05f3873 --- /dev/null +++ b/extend/Qrcode/Compress.php @@ -0,0 +1,130 @@ +src = $src; + $this->percent = $percent; + } + + /* + param string $saveName 图片名(可不带扩展名用原图名)用于保存。或不提供文件名直接显示 + */ + public function compressImg($saveName='') + { + $this->_openImage(); + if(!empty($saveName)) + { + $this->_saveImage($saveName);//保存 + } + else + { + $this->_showImage(); + } + } + + + /* + 内部:打开图片 + */ + private function _openImage() + { + list($width, $height, $type, $attr) = getimagesize($this->src); + $this->imageinfo = array( + 'width'=>$width, + 'height'=>$height, + 'type'=>image_type_to_extension($type,false), + 'attr'=>$attr + ); + $fun = "imagecreatefrom".$this->imageinfo['type']; + $this->image = $fun($this->src); + $this->_thumpImage(); + } + + + /** + * 内部:操作图片 + */ + private function _thumpImage() + { + $new_width = $this->imageinfo['width'] * $this->percent; + $new_height = $this->imageinfo['height'] * $this->percent; + $image_thump = imagecreatetruecolor($new_width,$new_height); + //将原图复制带图片载体上面,并且按照一定比例压缩,极大的保持了清晰度 + + /* 处理缩放png图透明背景变黑色问题 start */ + $color=imagecolorallocate($image_thump,255,255,255); + imagecolortransparent($image_thump,$color); + imagefill($image_thump,0,0,$color); + /* 处理缩放png图透明背景变黑色问题 end */ + + imagecopyresampled($image_thump,$this->image,0,0,0,0,$new_width,$new_height,$this->imageinfo['width'],$this->imageinfo['height']); + + imagedestroy($this->image); + $this->image = $image_thump; + } + + + /** + * 输出图片:保存图片则用saveImage() + */ + private function _showImage() + { + header('Content-Type: image/'.$this->imageinfo['type']); + $funcs = "image".$this->imageinfo['type']; + $funcs($this->image); + } + + + /** + * 保存图片到硬盘: + * @param string $dstImgName 1、可指定字符串不带后缀的名称,使用源图扩展名 。2、直接指定目标图片名带扩展名。 + */ + private function _saveImage($dstImgName) + { + if(empty($dstImgName)) return false; + $allowImgs = ['.jpg', '.jpeg', '.png', '.bmp', '.wbmp','.gif']; //如果目标图片名有后缀就用目标图片扩展名 后缀,如果没有,则用源图的扩展名 + $dstExt = strrchr($dstImgName ,"."); + $sourseExt = strrchr($this->src ,"."); + if(!empty($dstExt)) $dstExt =strtolower($dstExt); + if(!empty($sourseExt)) $sourseExt =strtolower($sourseExt); + + //有指定目标名扩展名 + if(!empty($dstExt) && in_array($dstExt,$allowImgs)) + { + $dstName = $dstImgName; + } + elseif(!empty($sourseExt) && in_array($sourseExt,$allowImgs)) + { + $dstName = $dstImgName.$sourseExt; + } + else + { + $dstName = $dstImgName.$this->imageinfo['type']; + } + $funcs = "image".$this->imageinfo['type']; + $funcs($this->image,$dstName); + } + + + /** + * 销毁图片 + */ + public function __destruct() + { + imagedestroy($this->image); + } +} \ No newline at end of file diff --git a/extend/form/README.md b/extend/form/README.md new file mode 100644 index 0000000..cf04d0c --- /dev/null +++ b/extend/form/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 表单项扩展目录 diff --git a/extend/form/complextable/Builder.php b/extend/form/complextable/Builder.php new file mode 100644 index 0000000..c4b1fe1 --- /dev/null +++ b/extend/form/complextable/Builder.php @@ -0,0 +1,115 @@ + + * @return array + */ + public function item($name = '', $title = '', $data = [], $header = false) + { + $head = []; + $cols = 1; + + if (true === $header) { + $header = array_shift($data); + $header = $header === null ? [] : [$header]; + } + + if ($header) { + foreach ($header as $row) { + $cols = count($row) > $cols ? count($row) : $cols; + foreach ($row as $k => $v) { + $head[0][] = $this->parseCell($v); + } + } + } + + if (!empty($data)) { + foreach ($data as $key => $row) { + foreach ($row as $k => $v) { + if (is_array($v)) { + // 是数组,表示表格中的表格 + if (is_string(end($v))) { // 数组最后一个元素是字符串,则表示合并行和合并列的参数 + $merge = explode(':', end($v)); + $colspan = $merge[0]; + $rowspan = isset($merge[1]) ? $merge[1] : ''; + array_pop($v); + } else { + $rowspan = ''; + $colspan = ''; + } + + $data[$key][$k] = [ + 'value' => $v, + 'rowspan' => $rowspan, + 'colspan' => $colspan, + ]; + } else { + $data[$key][$k] = $this->parseCell($v); + } + } + } + } + + return [ + 'name' => $name, + 'title' => $title, + 'data' => $data, + 'head' => $head, + 'cols' => $cols + ]; + } + + /** + * 分析单元格合并 + * @param $v + * @return array + * @author 蔡伟明 <314013107@qq.com> + */ + private function parseCell($v) + { + if (preg_match('/\[(.*)\]/', $v, $matches)) { + $cell = str_replace($matches[0], '', $v); + $merge = explode(':', $matches[1]); + $result = [ + 'value' => $cell, + 'colspan' => $merge[0], + 'rowspan' => isset($merge[1]) ? $merge[1] : '', + ]; + } else { + $result = [ + 'value' => $v, + 'rowspan' => '', + 'colspan' => '', + ]; + } + + return $result; + } + + /** + * @var array 需要加载的css + */ + public $css = [ + 'complextable.css' + ]; +} \ No newline at end of file diff --git a/extend/form/selectgroup/Builder.php b/extend/form/selectgroup/Builder.php new file mode 100644 index 0000000..35c6a2c --- /dev/null +++ b/extend/form/selectgroup/Builder.php @@ -0,0 +1,73 @@ + + * @return mixed + */ + public function item($name = '', $title = '', $tips = '', $options = [], $default = '', $extra_attr = '', $extra_class = '') + { + $multiple = false; + + if ($extra_attr != '' && in_array('multiple', explode(' ', $extra_attr))) { + $multiple = true; + } + + $placeholder = $multiple ? '请选择一项或多项' : '请选择一项'; + if (preg_match('/(.*)\[:(.*)\]/', $title, $matches)) { + $title = $matches[1]; + $placeholder = $matches[2]; + } + + return [ + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'options' => $options, + 'value' => $default, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + 'placeholder' => $placeholder, + 'multiple' => $multiple, + ]; + } + + /** + * @var array 需要加载的js + */ + public $js = [ + "__LIBS__/select2/select2.full.min.js", + "__LIBS__/select2/i18n/zh-CN.js", + "selectgroup.js", + ]; + + /** + * @var array 需要加载的css + */ + public $css = [ + "__LIBS__/select2/select2.min.css", + "__LIBS__/select2/select2-bootstrap.min.css", + ]; +} \ No newline at end of file diff --git a/extend/util/Database.php b/extend/util/Database.php new file mode 100644 index 0000000..f0521fa --- /dev/null +++ b/extend/util/Database.php @@ -0,0 +1,324 @@ + + * @alter CaiWeiMing <314013107@qq.com> + */ +class Database +{ + /** + * 文件指针 + * @var resource + */ + private $fp; + + /** + * 备份文件信息 part - 卷号,name - 文件名 + * @var array + */ + private $file; + + /** + * 当前打开文件大小 + * @var integer + */ + private $size = 0; + + /** + * 备份配置 + * @var integer + */ + private $config; + + /** + * 数据库备份构造方法 + * @param array $file 备份或还原的文件信息 + * @param array $config 备份配置信息 + * @param string $type 执行类型,export - 备份数据, import - 还原数据 + */ + public function __construct($file, $config, $type = 'export'){ + $this->file = $file; + $this->config = $config; + } + + /** + * 打开一个卷,用于写入数据 + * @param integer $size 写入数据的大小 + */ + private function open($size = 0){ + if($this->fp){ + $this->size += $size; + if($this->size > $this->config['part']){ + $this->config['compress'] ? @gzclose($this->fp) : @fclose($this->fp); + $this->fp = null; + $this->file['part']++; + session('backup_file', $this->file); + $this->create(); + } + } else { + $backup_path = $this->config['path']; + $filename = "{$backup_path}{$this->file['name']}-{$this->file['part']}.sql"; + if($this->config['compress']){ + $filename = "{$filename}.gz"; + $this->fp = @gzopen($filename, "a{$this->config['level']}"); + } else { + $this->fp = @fopen($filename, 'a'); + } + $this->size = filesize($filename) + $size; + } + } + + /** + * 写入初始数据 + * @return mixed + */ + public function create(){ + $sql = "-- -----------------------------\n"; + $sql .= "-- MySQL Data Transfer\n"; + $sql .= "--\n"; + $sql .= "-- Host : " . config('database.hostname') . "\n"; + $sql .= "-- Port : " . config('database.hostport') . "\n"; + $sql .= "-- Database : " . config('database.database') . "\n"; + $sql .= "--\n"; + $sql .= "-- Part : #{$this->file['part']}\n"; + $sql .= "-- Date : " . date("Y-m-d H:i:s") . "\n"; + $sql .= "-- -----------------------------\n\n"; + $sql .= "SET FOREIGN_KEY_CHECKS = 0;\n\n"; + return $this->write($sql); + } + + /** + * 写入SQL语句 + * @param string $sql 要写入的SQL语句 + * @return int + */ + private function write($sql = ''){ + $size = strlen($sql); + + // 由于压缩原因,无法计算出压缩后的长度,这里假设压缩率为50%, + // 一般情况压缩率都会高于50%; + $size = $this->config['compress'] ? $size / 2 : $size; + + $this->open($size); + return $this->config['compress'] ? @gzwrite($this->fp, $sql) : @fwrite($this->fp, $sql); + } + + /** + * 备份表结构 + * @param string $table 表名 + * @param integer $start 起始行数 + * @return array|bool|int false - 备份失败 + */ + public function backup($table = '', $start = 0){ + // 备份表结构 + if(0 == $start){ + $result = Db::query("SHOW CREATE TABLE `{$table}`"); + $result = array_map('array_change_key_case', $result); + + $sql = "\n"; + $sql .= "-- -----------------------------\n"; + $sql .= "-- Table structure for `{$table}`\n"; + $sql .= "-- -----------------------------\n"; + $sql .= "DROP TABLE IF EXISTS `{$table}`;\n"; + $sql .= trim($result[0]['create table']) . ";\n\n"; + if(false === $this->write($sql)){ + return false; + } + } + + // 数据总数 + $result = Db::query("SELECT COUNT(*) AS count FROM `{$table}`"); + $count = $result['0']['count']; + + //备份表数据 + if($count){ + // 写入数据注释 + if(0 == $start){ + $sql = "-- -----------------------------\n"; + $sql .= "-- Records of `{$table}`\n"; + $sql .= "-- -----------------------------\n"; + $this->write($sql); + } + + // 备份数据记录 + $result = Db::query("SELECT * FROM `{$table}` LIMIT {$start}, 1000"); + foreach ($result as $row) { + $row = array_map('addslashes', $row); + $sql = "INSERT INTO `{$table}` VALUES ('" . str_replace(array("\r","\n"),array('\r','\n'),implode("', '", $row)) . "');\n"; + if(false === $this->write($sql)){ + return false; + } + } + + //还有更多数据 + if($count > $start + 1000){ + return array($start + 1000, $count); + } + } + + // 备份下一表 + return 0; + } + + /** + * 导入数据 + * @param integer $start 起始位置 + * @return array|bool|int + */ + public function import($start = 0){ + if($this->config['compress']){ + $gz = gzopen($this->file[1], 'r'); + $size = 0; + } else { + $size = filesize($this->file[1]); + $gz = fopen($this->file[1], 'r'); + } + + $sql = ''; + if($start){ + $this->config['compress'] ? gzseek($gz, $start) : fseek($gz, $start); + } + + for($i = 0; $i < 1000; $i++){ + $sql .= $this->config['compress'] ? gzgets($gz) : fgets($gz); + if(preg_match('/.*;$/', trim($sql))){ + if(false !== Db::execute($sql)){ + $start += strlen($sql); + } else { + return false; + } + $sql = ''; + } elseif ($this->config['compress'] ? gzeof($gz) : feof($gz)) { + return 0; + } + } + + return array($start, $size); + } + + /** + * 导出 + * @param array $tables 表名 + * @param string $path 导出路径 + * @param string $prefix 表前缀 + * @param integer $export_data 是否导出数据 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public static function export($tables = [], $path = '', $prefix = '', $export_data = 1){ + $tables = is_array($tables) ? $tables : explode(',', $tables); + $datetime = date('Y-m-d H:i:s', time()); + $sql = "-- -----------------------------\n"; + $sql .= "-- 导出时间 `{$datetime}`\n"; + $sql .= "-- -----------------------------\n"; + + if (!empty($tables)) { + foreach ($tables as $table) { + $sql .= self::getSql($prefix.$table, $export_data); + } + + // 写入文件 + if (file_put_contents($path, $sql)) { + return true; + }; + } + return false; + } + + /** + * 导出卸载文件 + * @param array $tables 表名 + * @param string $path 导出路径 + * @param string $prefix 表前缀 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public static function exportUninstall($tables = [], $path = '', $prefix = ''){ + $tables = is_array($tables) ? $tables : explode(',', $tables); + $datetime = date('Y-m-d H:i:s', time()); + $sql = "-- -----------------------------\n"; + $sql .= "-- 导出时间 `{$datetime}`\n"; + $sql .= "-- -----------------------------\n"; + + if (!empty($tables)) { + foreach ($tables as $table) { + $sql .= "DROP TABLE IF EXISTS `{$prefix}{$table}`;\n"; + } + + // 写入文件 + if (file_put_contents($path, $sql)) { + return true; + }; + } + return false; + } + + /** + * 获取表结构和数据 + * @param string $table 表名 + * @param integer $export_data 是否导出数据 + * @param integer $start 起始行数 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + private static function getSql($table = '', $export_data = 0, $start = 0) + { + $sql = ""; + if (Db::query("SHOW TABLES LIKE '%{$table}%'")) { + // 表结构 + if ($start == 0) { + $result = Db::query("SHOW CREATE TABLE `{$table}`"); + $sql .= "\n-- -----------------------------\n"; + $sql .= "-- 表结构 `{$table}`\n"; + $sql .= "-- -----------------------------\n"; + $sql .= "DROP TABLE IF EXISTS `{$table}`;\n"; + $sql .= trim($result[0]['Create Table']) . ";\n\n"; + } + + // 表数据 + if ($export_data) { + $sql .= "-- -----------------------------\n"; + $sql .= "-- 表数据 `{$table}`\n"; + $sql .= "-- -----------------------------\n"; + + // 数据总数 + $result = Db::query("SELECT COUNT(*) AS count FROM `{$table}`"); + $count = $result['0']['count']; + + // 备份数据记录 + $result = Db::query("SELECT * FROM `{$table}` LIMIT {$start}, 1000"); + foreach ($result as $row) { + $row = array_map('addslashes', $row); + $sql .= "INSERT INTO `{$table}` VALUES ('" . str_replace(array("\r","\n"),array('\r','\n'),implode("', '", $row)) . "');\n"; + } + + // 还有更多数据 + if($count > $start + 1000){ + $sql .= self::getSql($table, $export_data, $start + 1000); + } + } + } + + return $sql; + } + + /** + * 析构方法,用于关闭文件资源 + */ + public function __destruct(){ + $this->config['compress'] ? @gzclose($this->fp) : @fclose($this->fp); + } +} \ No newline at end of file diff --git a/extend/util/File.php b/extend/util/File.php new file mode 100644 index 0000000..16febd9 --- /dev/null +++ b/extend/util/File.php @@ -0,0 +1,253 @@ + $value) { + if(substr($value, -1) == '/'){ + mkdir($value, 0777, true); + }else{ + @file_put_contents($value, ''); + } + } + } + + /** + * 读取文件内容 + * @param $filename 文件名 + * @return string 文件内容 + */ + static public function read_file($filename) { + $content = ''; + if(function_exists('file_get_contents')) + { + @$content = file_get_contents($filename); + } + else + { + if(@$fp = fopen($filename, 'r')) + { + @$content = fread($fp, filesize($filename)); + @fclose($fp); + } + } + return $content; + } + + /** + * 写文件 + * @param $filename 文件名 + * @param $writetext 文件内容 + * @param $openmod 打开方式 + * @return boolean true 成功, false 失败 + */ + static function write_file($filename, $writetext, $openmod='w') { + if(@$fp = fopen($filename, $openmod)) + { + flock($fp, 2); + fwrite($fp, $writetext); + fclose($fp); + return true; + } + else + { + return false; + } + } + + /** + * 删除目录 + * @param $dirName 原目录 + * @return boolean true 成功, false 失败 + */ + static function del_dir($dirName) { + if (!file_exists($dirName)) + { + return false; + } + + $dir = opendir($dirName); + while ($fileName = readdir($dir)) + { + $file = $dirName . '/' . $fileName; + if ($fileName != '.' && $fileName != '..') + { + if (is_dir($file)) + { + self::del_dir($file); + } + else + { + unlink($file); + } + } + } + closedir($dir); + return rmdir($dirName); + } + + /** + * 复制目录 + * @param $surDir 原目录 + * @param $toDir 目标目录 + * @return boolean true 成功, false 失败 + */ + static function copy_dir($surDir,$toDir) { + $surDir = rtrim($surDir,'/').'/'; + $toDir = rtrim($toDir,'/').'/'; + if (!file_exists($surDir)) + { + return false; + } + + if (!file_exists($toDir)) + { + self::mk_dir($toDir); + } + $file = opendir($surDir); + while ($fileName = readdir($file)) + { + $file1 = $surDir .'/'.$fileName; + $file2 = $toDir .'/'.$fileName; + if ($fileName != '.' && $fileName != '..') + { + if (is_dir($file1)) + { + self::copy_dir($file1, $file2); + } + else + { + copy($file1, $file2); + } + } + } + closedir($file); + return true; + } + + /** + * 列出目录 + * @param $dir 目录名 + * @return 目录数组。列出文件夹下内容,返回数组 $dirArray['dir']:存文件夹;$dirArray['file']:存文件 + */ + static function get_dirs($dir) { + $dir = rtrim($dir,'/').'/'; + $dirArray = []; + if (false != ($handle = opendir ( $dir ))) + { + $i = 0; + $j = 0; + while ( false !== ($file = readdir ( $handle )) ) + { + if (is_dir ( $dir . $file )) + { //判断是否文件夹 + $dirArray ['dir'] [$i] = $file; + $i ++; + } + else + { + $dirArray ['file'] [$j] = $file; + $j ++; + } + } + closedir ($handle); + } + return $dirArray; + } + + /** + * 统计文件夹大小 + * @param $dir 目录名 + * @return number 文件夹大小(单位 B) + */ + static function get_size($dir) { + $dirlist = opendir($dir); + $dirsize = 0; + while (false !== ($folderorfile = readdir($dirlist))) + { + if($folderorfile != "." && $folderorfile != "..") + { + if (is_dir("$dir/$folderorfile")) + { + $dirsize += self::get_size("$dir/$folderorfile"); + } + else + { + $dirsize += filesize("$dir/$folderorfile"); + } + } + } + closedir($dirlist); + return $dirsize; + } + + /** + * 检测是否为空文件夹 + * @param $dir 目录名 + * @return boolean true 空, fasle 不为空 + */ + static function empty_dir($dir) { + return (($files = @scandir($dir)) && count($files) <= 2); + } + + /** + * 文件缓存与文件读取 + * @param $name 文件名 + * @param $value 文件内容,为空则获取缓存 + * @param $path 文件所在目录,默认是当前应用的DATA目录 + * @param $cached 是否缓存结果,默认缓存 + * @return 返回缓存内容 + */ + function cache($name, $value='', $path=DATA_PATH,$cached=true) { + static $_cache = array(); + $filename = $path . $name . '.php'; + if ('' !== $value) { + if (is_null($value)) { + // 删除缓存 + return false !== strpos($name,'*')?array_map("unlink", glob($filename)):unlink($filename); + } else { + // 缓存数据 + $dir = dirname($filename); + // 目录不存在则创建 + if (!is_dir($dir)) + mkdir($dir,0755,true); + $_cache[$name] = $value; + return file_put_contents($filename, strip_whitespace("")); + } + } + if (isset($_cache[$name]) && $cached==true) return $_cache[$name]; + // 获取缓存数据 + if (is_file($filename)) { + $value = include $filename; + $_cache[$name] = $value; + } else { + $value = false; + } + return $value; + } +} diff --git a/extend/util/L.txt b/extend/util/L.txt new file mode 100644 index 0000000..2255550 --- /dev/null +++ b/extend/util/L.txt @@ -0,0 +1,27 @@ +版权所有 (c) 2016~2017,河源市卓锐科技有限公司保留所有权利。 + +DolphinPHP 用户协议 + +版权所有 (c) 2016~2017,河源市卓锐科技有限公司保留所有权利。 + +感谢您选择DolphinPHP,希望我们的努力能为您提供一个极简极致极速的PHP快速开发解决方案。 + +用户须知:本协议是您与河源市卓锐科技有限公司(以下简称卓锐公司)之间关于您使用DolphinPHP产品及服务的法律协议。无论您是个人或组织、盈利与否、用途如何(包括以学习和研究为目的),均需仔细阅读本协议,包括免除或者限制卓锐公司责任的免责条款及对您的权利限制。请您审阅并接受或不接受本服务条款。如您不同意本服务条款及/或卓锐公司随时对其的修改,您应不使用或主动取消DolphinPHP产品。否则,您的任何对DolphinPHP的相关服务的注册、登陆、下载、查看等使用行为将被视为您对本服务条款全部的完全接受,包括接受卓锐公司对服务条款随时所做的任何修改。 + +本服务条款一旦发生变更, 卓锐公司将在产品官网上公布修改内容。修改后的服务条款一旦在网站公布即有效代替原来的服务条款。您可随时登陆官网查阅最新版服务条款。如果您选择接受本条款,即表示您同意接受协议各项条件的约束。如果您不同意本服务条款,则不能获得使用本服务的权利。您若有违反本条款规定,卓锐公司有权随时中止或终止您对DolphinPHP产品的使用资格并保留追究相关法律责任的权利。 + +在理解、同意、并遵守本协议的全部条款后,方可开始使用DolphinPHP产品。您也可能与卓锐公司直接签订另一书面协议,以补充或者取代本协议的全部或者任何部分。 + +卓锐公司拥有DolphinPHP的全部知识产权,包括商标和著作权。本软件只供许可协议,并非出售。卓锐公司只允许您在遵守本协议各项条款的情况下复制、下载、安装、使用或者以其他方式受益于本软件的功能或者知识产权。 + +个人非商业用途可免费使用(但不包括其衍生产品、插件或者服务),也可以根据个人实际情况选择是否购买授权。 + +非个人用户(泛指非个人的团体,如企业、政府单位、教育机构、协会团体、厂矿、工作室等)必须购买软件授权后方可使用。 + +您可以在协议规定的约束和限制范围内修改DolphinPHP以适应您的网站要求,但免费版必须保留软件版本信息的正常显示及相关连接正常。 + +禁止去除DolphinPHP源码里的版权信息,商业授权版本可去除后台界面及前台界面的相关版权信息。 + +禁止在DolphinPHP整体或任何部分基础上发展任何派生版本、修改版本或第三方版本用于重新分发。 + +未经官方许可,不得对本软件或与之关联的商业授权进行出租、出售、抵押或发放子许可证。 \ No newline at end of file diff --git a/extend/util/PHPZip.php b/extend/util/PHPZip.php new file mode 100644 index 0000000..072e287 --- /dev/null +++ b/extend/util/PHPZip.php @@ -0,0 +1,527 @@ +visitFile(文件夹路径); + // print "当前文件夹的文件:

    \r\n"; + // foreach($filelist as $file) + // printf("%s
    \r\n", $file); + // ------------------------------------------------------ // + var $fileList = array(); + public function visitFile($path) + { + global $fileList; + $path = str_replace("\\", "/", $path); + $fdir = dir($path); + + while(($file = $fdir->read()) !== false) + { + if($file == '.' || $file == '..'){ continue; } + + $pathSub = preg_replace("*/{2,}*", "/", $path."/".$file); // 替换多个反斜杠 + $fileList[] = is_dir($pathSub) ? $pathSub."/" : $pathSub; + if(is_dir($pathSub)){ $this->visitFile($pathSub); } + } + $fdir->close(); + return $fileList; + } + + private function unix2DosTime($unixtime = 0) + { + $timearray = ($unixtime == 0) ? getdate() : getdate($unixtime); + + if($timearray['year'] < 1980) + { + $timearray['year'] = 1980; + $timearray['mon'] = 1; + $timearray['mday'] = 1; + $timearray['hours'] = 0; + $timearray['minutes'] = 0; + $timearray['seconds'] = 0; + } + + return ( ($timearray['year'] - 1980) << 25) + | ($timearray['mon'] << 21) + | ($timearray['mday'] << 16) + | ($timearray['hours'] << 11) + | ($timearray['minutes'] << 5) + | ($timearray['seconds'] >> 1); + } + + var $old_offset = 0; + private function addFile($data, $filename, $time = 0) + { + $filename = str_replace('\\', '/', $filename); + + $dtime = dechex($this->unix2DosTime($time)); + $hexdtime = '\x' . $dtime[6] . $dtime[7] + . '\x' . $dtime[4] . $dtime[5] + . '\x' . $dtime[2] . $dtime[3] + . '\x' . $dtime[0] . $dtime[1]; + eval('$hexdtime = "' . $hexdtime . '";'); + + $fr = "\x50\x4b\x03\x04"; + $fr .= "\x14\x00"; + $fr .= "\x00\x00"; + $fr .= "\x08\x00"; + $fr .= $hexdtime; + $unc_len = strlen($data); + $crc = crc32($data); + $zdata = gzcompress($data); + $c_len = strlen($zdata); + $zdata = substr(substr($zdata, 0, strlen($zdata) - 4), 2); + $fr .= pack('V', $crc); + $fr .= pack('V', $c_len); + $fr .= pack('V', $unc_len); + $fr .= pack('v', strlen($filename)); + $fr .= pack('v', 0); + $fr .= $filename; + + $fr .= $zdata; + + $fr .= pack('V', $crc); + $fr .= pack('V', $c_len); + $fr .= pack('V', $unc_len); + + $this->datasec[] = $fr; + $new_offset = strlen(implode('', $this->datasec)); + + $cdrec = "\x50\x4b\x01\x02"; + $cdrec .= "\x00\x00"; + $cdrec .= "\x14\x00"; + $cdrec .= "\x00\x00"; + $cdrec .= "\x08\x00"; + $cdrec .= $hexdtime; + $cdrec .= pack('V', $crc); + $cdrec .= pack('V', $c_len); + $cdrec .= pack('V', $unc_len); + $cdrec .= pack('v', strlen($filename) ); + $cdrec .= pack('v', 0 ); + $cdrec .= pack('v', 0 ); + $cdrec .= pack('v', 0 ); + $cdrec .= pack('v', 0 ); + $cdrec .= pack('V', 32 ); + + $cdrec .= pack('V', $this->old_offset ); + $this->old_offset = $new_offset; + + $cdrec .= $filename; + $this->ctrl_dir[] = $cdrec; + } + + var $eof_ctrl_dir = "\x50\x4b\x05\x06\x00\x00\x00\x00"; + private function file() + { + $data = implode('', $this->datasec); + $ctrldir = implode('', $this->ctrl_dir); + + return $data + . $ctrldir + . $this->eof_ctrl_dir + . pack('v', sizeof($this->ctrl_dir)) + . pack('v', sizeof($this->ctrl_dir)) + . pack('V', strlen($ctrldir)) + . pack('V', strlen($data)) + . "\x00\x00"; + } + + // ------------------------------------------------------ // + // #压缩到服务器 + // + // $archive = new PHPZip(); + // $archive->Zip("需压缩的文件所在目录", "ZIP压缩文件名"); + // ------------------------------------------------------ // + public function Zip($dir, $saveName) + { + if(@!function_exists('gzcompress')){ return; } + + ob_end_clean(); + $filelist = $this->visitFile($dir); + if(count($filelist) == 0){ return; } + + foreach($filelist as $file) + { + if(!file_exists($file) || !is_file($file)){ continue; } + + $fd = fopen($file, "rb"); + $content = @fread($fd, filesize($file)); + fclose($fd); + + // 1.删除$dir的字符(./folder/file.txt删除./folder/) + // 2.如果存在/就删除(/file.txt删除/) + $file = substr($file, strlen($dir)); + if(substr($file, 0, 1) == "\\" || substr($file, 0, 1) == "/"){ $file = substr($file, 1); } + + $this->addFile($content, $file); + } + $out = $this->file(); + + $fp = fopen($saveName, "wb"); + fwrite($fp, $out, strlen($out)); + fclose($fp); + } + + // ------------------------------------------------------ // + // #压缩并直接下载 + // + // $archive = new PHPZip(); + // $archive->ZipAndDownload("需压缩的文件所在目录"); + // ------------------------------------------------------ // + public function ZipAndDownload($dir, $file_name = '') + { + if(@!function_exists('gzcompress')){ return; } + + ob_end_clean(); + $filelist = $this->visitFile($dir); + if(count($filelist) == 0){ return; } + + foreach($filelist as $file) + { + if(!file_exists($file) || !is_file($file)){ continue; } + + $fd = fopen($file, "rb"); + $content = @fread($fd, filesize($file)); + fclose($fd); + + // 1.删除$dir的字符(./folder/file.txt删除./folder/) + // 2.如果存在/就删除(/file.txt删除/) + $file = substr($file, strlen($dir)); + if(substr($file, 0, 1) == "\\" || substr($file, 0, 1) == "/"){ $file = substr($file, 1); } + + $this->addFile($content, $file); + } + $out = $this->file(); + + $file_name = $file_name == '' ? 'file_'.date("YmdHis", time()) : $file_name; + + @header('Content-Encoding: none'); + @header('Content-Type: application/zip'); + @header('Content-Disposition: attachment ; filename='.$file_name.'.zip'); + @header('Pragma: no-cache'); + @header('Expires: 0'); + print($out); + } + + + /********************************************************** + * 解压部分 + **********************************************************/ + // ------------------------------------------------------ // + // ReadCentralDir($zip, $zipfile) + // $zip是经过@fopen($zipfile, 'rb')打开的 + // $zipfile是zip文件的路径 + // ------------------------------------------------------ // + private function ReadCentralDir($zip, $zipfile) + { + $size = filesize($zipfile); + $max_size = ($size < 277) ? $size : 277; + + @fseek($zip, $size - $max_size); + $pos = ftell($zip); + $bytes = 0x00000000; + + while($pos < $size) + { + $byte = @fread($zip, 1); + $bytes = ($bytes << 8) | Ord($byte); + $pos++; + if($bytes == 0x504b0506){ break; } + } + + $data = unpack('vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_size', fread($zip, 18)); + + $centd['comment'] = ($data['comment_size'] != 0) ? fread($zip, $data['comment_size']) : ''; // 注释 + $centd['entries'] = $data['entries']; + $centd['disk_entries'] = $data['disk_entries']; + $centd['offset'] = $data['offset']; + $centd['disk_start'] = $data['disk_start']; + $centd['size'] = $data['size']; + $centd['disk'] = $data['disk']; + return $centd; + } + + private function ReadCentralFileHeaders($zip) + { + $binary_data = fread($zip, 46); + $header = unpack('vchkid/vid/vversion/vversion_extracted/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len/vcomment_len/vdisk/vinternal/Vexternal/Voffset', $binary_data); + + $header['filename'] = ($header['filename_len'] != 0) ? fread($zip, $header['filename_len']) : ''; + $header['extra'] = ($header['extra_len'] != 0) ? fread($zip, $header['extra_len']) : ''; + $header['comment'] = ($header['comment_len'] != 0) ? fread($zip, $header['comment_len']) : ''; + + + if($header['mdate'] && $header['mtime']) + { + $hour = ($header['mtime'] & 0xF800) >> 11; + $minute = ($header['mtime'] & 0x07E0) >> 5; + $seconde = ($header['mtime'] & 0x001F) * 2; + $year = (($header['mdate'] & 0xFE00) >> 9) + 1980; + $month = ($header['mdate'] & 0x01E0) >> 5; + $day = $header['mdate'] & 0x001F; + $header['mtime'] = mktime($hour, $minute, $seconde, $month, $day, $year); + } else { + $header['mtime'] = time(); + } + $header['stored_filename'] = $header['filename']; + $header['status'] = 'ok'; + if(substr($header['filename'], -1) == '/'){ $header['external'] = 0x41FF0010; } // 判断是否文件夹 + return $header; + } + + private function ReadFileHeader($zip) + { + $binary_data = fread($zip, 30); + $data = unpack('vchk/vid/vversion/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len', $binary_data); + + $header['filename'] = fread($zip, $data['filename_len']); + $header['extra'] = ($data['extra_len'] != 0) ? fread($zip, $data['extra_len']) : ''; + $header['compression'] = $data['compression']; + $header['size'] = $data['size']; + $header['compressed_size'] = $data['compressed_size']; + $header['crc'] = $data['crc']; + $header['flag'] = $data['flag']; + $header['mdate'] = $data['mdate']; + $header['mtime'] = $data['mtime']; + + if($header['mdate'] && $header['mtime']){ + $hour = ($header['mtime'] & 0xF800) >> 11; + $minute = ($header['mtime'] & 0x07E0) >> 5; + $seconde = ($header['mtime'] & 0x001F) * 2; + $year = (($header['mdate'] & 0xFE00) >> 9) + 1980; + $month = ($header['mdate'] & 0x01E0) >> 5; + $day = $header['mdate'] & 0x001F; + $header['mtime'] = mktime($hour, $minute, $seconde, $month, $day, $year); + }else{ + $header['mtime'] = time(); + } + + $header['stored_filename'] = $header['filename']; + $header['status'] = "ok"; + return $header; + } + + private function ExtractFile($header, $to, $zip) + { + $header = $this->readfileheader($zip); + + if(substr($to, -1) != "/"){ $to .= "/"; } + if(!@is_dir($to)){ @mkdir($to, 0777); } + + $pth = explode("/", dirname($header['filename'])); + for($i=0; isset($pth[$i]); $i++){ + if(!$pth[$i]){ continue; } + $pthss .= $pth[$i]."/"; + if(!is_dir($to.$pthss)){ @mkdir($to.$pthss, 0777); } + } + + if(!($header['external'] == 0x41FF0010) && !($header['external'] == 16)) + { + if($header['compression'] == 0) + { + $fp = @fopen($to.$header['filename'], 'wb'); + if(!$fp){ return(-1); } + $size = $header['compressed_size']; + + while($size != 0) + { + $read_size = ($size < 2048 ? $size : 2048); + $buffer = fread($zip, $read_size); + $binary_data = pack('a'.$read_size, $buffer); + @fwrite($fp, $binary_data, $read_size); + $size -= $read_size; + } + fclose($fp); + touch($to.$header['filename'], $header['mtime']); + + }else{ + + $fp = @fopen($to.$header['filename'].'.gz', 'wb'); + if(!$fp){ return(-1); } + $binary_data = pack('va1a1Va1a1', 0x8b1f, Chr($header['compression']), Chr(0x00), time(), Chr(0x00), Chr(3)); + + fwrite($fp, $binary_data, 10); + $size = $header['compressed_size']; + + while($size != 0) + { + $read_size = ($size < 1024 ? $size : 1024); + $buffer = fread($zip, $read_size); + $binary_data = pack('a'.$read_size, $buffer); + @fwrite($fp, $binary_data, $read_size); + $size -= $read_size; + } + + $binary_data = pack('VV', $header['crc'], $header['size']); + fwrite($fp, $binary_data, 8); + fclose($fp); + + $gzp = @gzopen($to.$header['filename'].'.gz', 'rb') or die("Cette archive est compress!"); + + if(!$gzp){ return(-2); } + $fp = @fopen($to.$header['filename'], 'wb'); + if(!$fp){ return(-1); } + $size = $header['size']; + + while($size != 0) + { + $read_size = ($size < 2048 ? $size : 2048); + $buffer = gzread($gzp, $read_size); + $binary_data = pack('a'.$read_size, $buffer); + @fwrite($fp, $binary_data, $read_size); + $size -= $read_size; + } + fclose($fp); gzclose($gzp); + + touch($to.$header['filename'], $header['mtime']); + @unlink($to.$header['filename'].'.gz'); + } + } + return true; + } + + // ------------------------------------------------------ // + // #解压文件 + // + // $archive = new PHPZip(); + // $zipfile = "ZIP压缩文件名"; + // $savepath = "解压缩目录名"; + // $zipfile = $unzipfile; + // $savepath = $unziptarget; + // $array = $archive->GetZipInnerFilesInfo($zipfile); + // $filecount = 0; + // $dircount = 0; + // $failfiles = array(); + // set_time_limit(0); // 修改为不限制超时时间(默认为30秒) + // + // for($i=0; $iunZip($zipfile, $savepath, $i) > 0){ + // $filecount++; + // }else{ + // $failfiles[] = $array[$i][filename]; + // } + // }else{ + // $dircount++; + // } + // } + // set_time_limit(30); + //printf("文件夹:%d    解压文件:%d    失败:%d
    \r\n", $dircount, $filecount, count($failfiles)); + //if(count($failfiles) > 0){ + // foreach($failfiles as $file){ + // printf("·%s
    \r\n", $file); + // } + //} + // ------------------------------------------------------ // + public function unZip($zipfile, $to, $index = Array(-1)) + { + $ok = 0; + $zip = @fopen($zipfile, 'rb'); + if(!$zip){ return(-1); } + + $cdir = $this->ReadCentralDir($zip, $zipfile); + $pos_entry = $cdir['offset']; + + if(!is_array($index)){ $index = array($index); } + for($i=0; $index[$i]; $i++) + { + if(intval($index[$i]) != $index[$i] || $index[$i] > $cdir['entries']) + { + return(-1); + } + } + + for($i=0; $i<$cdir['entries']; $i++) + { + @fseek($zip, $pos_entry); + $header = $this->ReadCentralFileHeaders($zip); + $header['index'] = $i; + $pos_entry = ftell($zip); + @rewind($zip); + fseek($zip, $header['offset']); + if(in_array("-1", $index) || in_array($i, $index)) + { + $stat[$header['filename']] = $this->ExtractFile($header, $to, $zip); + } + } + + fclose($zip); + return $stat; + } + + + /********************************************************** + * 其它部分 + **********************************************************/ + // ------------------------------------------------------ // + // #获取被压缩文件的信息 + // + // $archive = new PHPZip(); + // $array = $archive->GetZipInnerFilesInfo(ZIP压缩文件名); + // for($i=0; $i·%s
    \r\n", $array[$i][filename]); + // foreach($array[$i] as $key => $value) + // printf("%s => %s
    \r\n", $key, $value); + // print "\r\n

    ------------------------------------

    \r\n\r\n"; + // } + // ------------------------------------------------------ // + public function GetZipInnerFilesInfo($zipfile) + { + $zip = @fopen($zipfile, 'rb'); + if(!$zip){ return(0); } + $centd = $this->ReadCentralDir($zip, $zipfile); + + @rewind($zip); + @fseek($zip, $centd['offset']); + $ret = array(); + + for($i=0; $i<$centd['entries']; $i++) + { + $header = $this->ReadCentralFileHeaders($zip); + $header['index'] = $i; + $info = array( + 'filename' => $header['filename'], // 文件名 + 'stored_filename' => $header['stored_filename'], // 压缩后文件名 + 'size' => $header['size'], // 大小 + 'compressed_size' => $header['compressed_size'], // 压缩后大小 + 'crc' => strtoupper(dechex($header['crc'])), // CRC32 + 'mtime' => date("Y-m-d H:i:s",$header['mtime']), // 文件修改时间 + 'comment' => $header['comment'], // 注释 + 'folder' => ($header['external'] == 0x41FF0010 || $header['external'] == 16) ? 1 : 0, // 是否为文件夹 + 'index' => $header['index'], // 文件索引 + 'status' => $header['status'] // 状态 + ); + $ret[] = $info; + unset($header); + } + fclose($zip); + return $ret; + } + + // ------------------------------------------------------ // + // #获取压缩文件的注释 + // + // $archive = new PHPZip(); + // echo $archive->GetZipComment(ZIP压缩文件名); + // ------------------------------------------------------ // + public function GetZipComment($zipfile) + { + $zip = @fopen($zipfile, 'rb'); + if(!$zip){ return(0); } + $centd = $this->ReadCentralDir($zip, $zipfile); + fclose($zip); + return $centd[comment]; + } +} diff --git a/extend/util/Sql.php b/extend/util/Sql.php new file mode 100644 index 0000000..13aa929 --- /dev/null +++ b/extend/util/Sql.php @@ -0,0 +1,150 @@ + + */ +class Sql +{ + /** + * 从sql文件获取纯sql语句 + * @param string $sql_file sql文件路径 + * @param bool $string 如果为真,则只返回一条sql语句,默认以数组形式返回 + * @param array $replace 替换前缀,如:['my_' => 'me_'],表示将表前缀"my_"替换成"me_" + * 这种前缀替换方法不一定准确,比如正常内容内有跟前缀相同的字符,也会被替换 + * @return mixed + */ + public static function getSqlFromFile($sql_file = '', $string = false, $replace = []) + { + if (!file_exists($sql_file)) { + return false; + } + + // 读取sql文件内容 + $handle = self::read_file($sql_file); + + // 分割语句 + $handle = self::parseSql($handle, $string, $replace); + + return $handle; + } + + /** + * 分割sql语句 + * @param string $content sql内容 + * @param bool $string 如果为真,则只返回一条sql语句,默认以数组形式返回 + * @param array $replace 替换前缀,如:['my_' => 'me_'],表示将表前缀my_替换成me_ + * @return array|string 除去注释之后的sql语句数组或一条语句 + */ + public static function parseSql($content = '', $string = false, $replace = []) + { + // 被替换的前缀 + $from = ''; + // 要替换的前缀 + $to = ''; + + // 替换表前缀 + if (!empty($replace)) { + $to = current($replace); + $from = current(array_flip($replace)); + } + + if ($content != '') { + // 纯sql内容 + $pure_sql = []; + + // 多行注释标记 + $comment = false; + + // 按行分割,兼容多个平台 + $content = str_replace(["\r\n", "\r"], "\n", $content); + $content = explode("\n", trim($content)); + + // 循环处理每一行 + foreach ($content as $key => $line) { + // 跳过空行 + if ($line == '') { + continue; + } + + // 跳过以#或者--开头的单行注释 + if (preg_match("/^(#|--)/", $line)) { + continue; + } + + // 跳过以/**/包裹起来的单行注释 + if (preg_match("/^\/\*(.*?)\*\//", $line)) { + continue; + } + + // 多行注释开始 + if (substr($line, 0, 2) == '/*') { + $comment = true; + continue; + } + + // 多行注释结束 + if (substr($line, -2) == '*/') { + $comment = false; + continue; + } + + // 多行注释没有结束,继续跳过 + if ($comment) { + continue; + } + + // 替换表前缀 + if ($from != '') { + $line = str_replace('`'.$from, '`'.$to, $line); + } + + // sql语句 + array_push($pure_sql, $line); + } + + // 只返回一条语句 + if ($string) { + return implode("",$pure_sql); + } + + // 以数组形式返回sql语句 + $pure_sql = implode("\n",$pure_sql); + $pure_sql = explode(";\n", $pure_sql); + return $pure_sql; + } else { + return $string == true ? '' : []; + } + } + + /** + * 读取文件内容 + * @param $filename 文件名 + * @return string 文件内容 + */ + public static function read_file($filename) { + $content = ''; + if(function_exists('file_get_contents')) { + @$content = file_get_contents($filename); + } else { + if(@$fp = fopen($filename, 'r')) { + @$content = fread($fp, filesize($filename)); + @fclose($fp); + } + } + return $content; + } +} \ No newline at end of file diff --git a/extend/util/Tree.php b/extend/util/Tree.php new file mode 100644 index 0000000..a1bdc77 --- /dev/null +++ b/extend/util/Tree.php @@ -0,0 +1,184 @@ + + */ +class Tree +{ + /** + * @var object 对象实例 + */ + protected static $instance; + + /** + * 配置参数 + * @var array + */ + protected static $config = [ + 'id' => 'id', // id名称 + 'pid' => 'pid', // pid名称 + 'title' => 'title', // 标题名称 + 'child' => 'child', // 子元素键名 + 'html' => '┝ ', // 层级标记 + 'step' => 4, // 层级步进数量 + ]; + + /** + * 架构函数 + * @param array $config + */ + public function __construct($config = []) + { + self::$config = array_merge(self::$config, $config); + } + + /** + * 配置参数 + * @param array $config + * @return object + */ + public static function config($config = []) + { + if (!empty($config)) { + $config = array_merge(self::$config, $config); + } + if (is_null(self::$instance)) { + self::$instance = new static($config); + } + return self::$instance; + } + + /** + * 将数据集格式化成层次结构 + * @param array/object $lists 要格式化的数据集,可以是数组,也可以是对象 + * @param int $pid 父级id + * @param int $max_level 最多返回多少层,0为不限制 + * @param int $curr_level 当前层数 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function toLayer($lists = [], $pid = 0, $max_level = 0, $curr_level = 0) + { + $trees = []; + $lists = array_values($lists); + foreach ($lists as $key => $value) { + if ($value[self::$config['pid']] == $pid) { + if ($max_level > 0 && $curr_level == $max_level) { + return $trees; + } + unset($lists[$key]); + $child = self::toLayer($lists, $value[self::$config['id']], $max_level, $curr_level + 1); + if (!empty($child)) { + $value[self::$config['child']] = $child; + } + $trees[] = $value; + } + } + return $trees; + } + + /** + * 将数据集格式化成列表结构 + * @param array|object $lists 要格式化的数据集,可以是数组,也可以是对象 + * @param integer $pid 父级id + * @param integer $level 级别 + * @return array 列表结构(一维数组) + */ + public static function toList($lists = [], $pid = 0, $level = 0) + { + if (is_array($lists)) { + $trees = []; + foreach ($lists as $key => $value) { + if ($value[self::$config['pid']] == $pid) { + $title_prefix = str_repeat(" ", $level * self::$config['step']).self::$config['html']; + $value['level'] = $level + 1; + $value['title_prefix'] = $level == 0 ? '' : $title_prefix; + $value['title_display'] = $level == 0 ? $value[self::$config['title']] : $title_prefix.$value[self::$config['title']]; + $trees[] = $value; + unset($lists[$key]); + $trees = array_merge($trees, self::toList($lists, $value[self::$config['id']], $level + 1)); + } + } + return $trees; + } else { + foreach ($lists as $key => $value) { + if ($value[self::$config['pid']] == $pid && is_object($value)) { + $title_prefix = str_repeat(" ", $level * self::$config['step']).self::$config['html']; + $value['level'] = $level + 1; + $value['title_prefix'] = $level == 0 ? '' : $title_prefix; + $value['title_display'] = $level == 0 ? $value[self::$config['title']] : $title_prefix.$value[self::$config['title']]; + $lists->offsetUnset($key); + $lists[] = $value; + self::toList($lists, $value[self::$config['id']], $level + 1); + } + } + return $lists; + } + } + + /** + * 根据子节点返回所有父节点 + * @param array $lists 数据集 + * @param string $id 子节点id + * @return array + */ + public static function getParents($lists = [], $id = '') + { + $trees = []; + foreach ($lists as $value) { + if ($value[self::$config['id']] == $id) { + $trees[] = $value; + $trees = array_merge(self::getParents($lists, $value[self::$config['pid']]), $trees); + } + } + return $trees; + } + + /** + * 获取所有子节点id + * @param array $lists 数据集 + * @param string $pid 父级id + * @return array + */ + public static function getChildsId($lists = [], $pid = '') + { + $result = []; + foreach ($lists as $value) { + if ($value[self::$config['pid']] == $pid) { + $result[] = $value[self::$config['id']]; + $result = array_merge($result, self::getChildsId($lists, $value[self::$config['id']])); + } + } + return $result; + } + + /** + * 获取所有子节点 + * @param array $lists 数据集 + * @param string $pid 父级id + * @return array + */ + public static function getChilds($lists = [], $pid = '') + { + $result = []; + foreach ($lists as $value) { + if ($value[self::$config['pid']] == $pid) { + $result[] = $value; + $result = array_merge($result, self::getChilds($lists, $value[self::$config['id']])); + } + } + return $result; + } +} \ No newline at end of file diff --git a/packet/README.md b/packet/README.md new file mode 100644 index 0000000..1e8162a --- /dev/null +++ b/packet/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 数据包目录 diff --git a/packet/wechat_area/info.php b/packet/wechat_area/info.php new file mode 100644 index 0000000..25d010b --- /dev/null +++ b/packet/wechat_area/info.php @@ -0,0 +1,32 @@ + 'wechat_area', + // 数据包标题 + 'title' => '微信地区数据包', + // 作者 + 'author' => 'CaiWeiMing', + // 作者网址 + 'author_url' => '', + // 版本号 + 'version' => '1.0.0', + // 数据表名,不含前缀 + 'tables' => [ + 'packet_wechat_area' + ], + // 表前缀 + 'database_prefix' => 'oc_', +]; \ No newline at end of file diff --git a/packet/wechat_area/packet_wechat_area.sql b/packet/wechat_area/packet_wechat_area.sql new file mode 100644 index 0000000..3c57ef7 --- /dev/null +++ b/packet/wechat_area/packet_wechat_area.sql @@ -0,0 +1,508 @@ +/* +Navicat MySQL Data Transfer + +Target Server Type : MYSQL +Target Server Version : 50540 +File Encoding : 65001 + +Date: 2016-10-18 17:45:31 +*/ + +SET FOREIGN_KEY_CHECKS=0; + +-- ---------------------------- +-- Table structure for `oc_packet_wechat_area` +-- ---------------------------- +DROP TABLE IF EXISTS `oc_packet_wechat_area`; +CREATE TABLE `oc_packet_wechat_area` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `country` varchar(32) NOT NULL DEFAULT '' COMMENT '国家名称', + `province` varchar(32) NOT NULL DEFAULT '' COMMENT '省份名称', + `city` varchar(32) NOT NULL DEFAULT '' COMMENT '城市名称', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=482 DEFAULT CHARSET=utf8 COMMENT='地区信息表'; + +-- ---------------------------- +-- Records of oc_packet_wechat_area +-- ---------------------------- +INSERT INTO `oc_packet_wechat_area` VALUES ('1', '中国', '四川', '凉山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('2', '中国', '四川', '资阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('3', '中国', '四川', '成都'); +INSERT INTO `oc_packet_wechat_area` VALUES ('4', '中国', '四川', '自贡'); +INSERT INTO `oc_packet_wechat_area` VALUES ('5', '中国', '四川', '泸州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('6', '中国', '四川', '攀枝花'); +INSERT INTO `oc_packet_wechat_area` VALUES ('7', '中国', '四川', '绵阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('8', '中国', '四川', '德阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('9', '中国', '四川', '遂宁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('10', '中国', '四川', '广元'); +INSERT INTO `oc_packet_wechat_area` VALUES ('11', '中国', '四川', '乐山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('12', '中国', '四川', '内江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('13', '中国', '四川', '南充'); +INSERT INTO `oc_packet_wechat_area` VALUES ('14', '中国', '四川', '宜宾'); +INSERT INTO `oc_packet_wechat_area` VALUES ('15', '中国', '四川', '眉山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('16', '中国', '四川', '达州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('17', '中国', '四川', '广安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('18', '中国', '四川', '巴中'); +INSERT INTO `oc_packet_wechat_area` VALUES ('19', '中国', '四川', '雅安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('20', '中国', '四川', '甘孜'); +INSERT INTO `oc_packet_wechat_area` VALUES ('21', '中国', '四川', '阿坝'); +INSERT INTO `oc_packet_wechat_area` VALUES ('22', '中国', '重庆', '酉阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('23', '中国', '重庆', '彭水'); +INSERT INTO `oc_packet_wechat_area` VALUES ('24', '中国', '重庆', '合川'); +INSERT INTO `oc_packet_wechat_area` VALUES ('25', '中国', '重庆', '永川'); +INSERT INTO `oc_packet_wechat_area` VALUES ('26', '中国', '重庆', '江津'); +INSERT INTO `oc_packet_wechat_area` VALUES ('27', '中国', '重庆', '南川'); +INSERT INTO `oc_packet_wechat_area` VALUES ('28', '中国', '重庆', '铜梁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('29', '中国', '重庆', '大足'); +INSERT INTO `oc_packet_wechat_area` VALUES ('30', '中国', '重庆', '荣昌'); +INSERT INTO `oc_packet_wechat_area` VALUES ('31', '中国', '重庆', '璧山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('32', '中国', '重庆', '长寿'); +INSERT INTO `oc_packet_wechat_area` VALUES ('33', '中国', '重庆', '綦江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('34', '中国', '重庆', '潼南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('35', '中国', '重庆', '梁平'); +INSERT INTO `oc_packet_wechat_area` VALUES ('36', '中国', '重庆', '城口'); +INSERT INTO `oc_packet_wechat_area` VALUES ('37', '中国', '重庆', '石柱'); +INSERT INTO `oc_packet_wechat_area` VALUES ('38', '中国', '重庆', '秀山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('39', '中国', '重庆', '万州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('40', '中国', '重庆', '渝中'); +INSERT INTO `oc_packet_wechat_area` VALUES ('41', '中国', '重庆', '涪陵'); +INSERT INTO `oc_packet_wechat_area` VALUES ('42', '中国', '重庆', '江北'); +INSERT INTO `oc_packet_wechat_area` VALUES ('43', '中国', '重庆', '大渡口'); +INSERT INTO `oc_packet_wechat_area` VALUES ('44', '中国', '重庆', '九龙坡'); +INSERT INTO `oc_packet_wechat_area` VALUES ('45', '中国', '重庆', '沙坪坝'); +INSERT INTO `oc_packet_wechat_area` VALUES ('46', '中国', '重庆', '北碚'); +INSERT INTO `oc_packet_wechat_area` VALUES ('47', '中国', '重庆', '南岸'); +INSERT INTO `oc_packet_wechat_area` VALUES ('48', '中国', '重庆', '黔江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('49', '中国', '重庆', '巫溪'); +INSERT INTO `oc_packet_wechat_area` VALUES ('50', '中国', '重庆', '双桥'); +INSERT INTO `oc_packet_wechat_area` VALUES ('51', '中国', '重庆', '万盛'); +INSERT INTO `oc_packet_wechat_area` VALUES ('52', '中国', '重庆', '巴南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('53', '中国', '重庆', '渝北'); +INSERT INTO `oc_packet_wechat_area` VALUES ('54', '中国', '重庆', '忠县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('55', '中国', '重庆', '武隆'); +INSERT INTO `oc_packet_wechat_area` VALUES ('56', '中国', '重庆', '垫江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('57', '中国', '重庆', '丰都'); +INSERT INTO `oc_packet_wechat_area` VALUES ('58', '中国', '重庆', '巫山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('59', '中国', '重庆', '奉节'); +INSERT INTO `oc_packet_wechat_area` VALUES ('60', '中国', '重庆', '云阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('61', '中国', '重庆', '开县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('62', '中国', '陕西', '商洛'); +INSERT INTO `oc_packet_wechat_area` VALUES ('63', '中国', '陕西', '西安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('64', '中国', '陕西', '宝鸡'); +INSERT INTO `oc_packet_wechat_area` VALUES ('65', '中国', '陕西', '铜川'); +INSERT INTO `oc_packet_wechat_area` VALUES ('66', '中国', '陕西', '渭南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('67', '中国', '陕西', '咸阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('68', '中国', '陕西', '汉中'); +INSERT INTO `oc_packet_wechat_area` VALUES ('69', '中国', '陕西', '延安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('70', '中国', '陕西', '安康'); +INSERT INTO `oc_packet_wechat_area` VALUES ('71', '中国', '陕西', '榆林'); +INSERT INTO `oc_packet_wechat_area` VALUES ('72', '中国', '甘肃', '定西'); +INSERT INTO `oc_packet_wechat_area` VALUES ('73', '中国', '甘肃', '庆阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('74', '中国', '甘肃', '陇南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('75', '中国', '甘肃', '甘南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('76', '中国', '甘肃', '临夏'); +INSERT INTO `oc_packet_wechat_area` VALUES ('77', '中国', '甘肃', '兰州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('78', '中国', '甘肃', '金昌'); +INSERT INTO `oc_packet_wechat_area` VALUES ('79', '中国', '甘肃', '嘉峪关'); +INSERT INTO `oc_packet_wechat_area` VALUES ('80', '中国', '甘肃', '天水'); +INSERT INTO `oc_packet_wechat_area` VALUES ('81', '中国', '甘肃', '白银'); +INSERT INTO `oc_packet_wechat_area` VALUES ('82', '中国', '甘肃', '张掖'); +INSERT INTO `oc_packet_wechat_area` VALUES ('83', '中国', '甘肃', '武威'); +INSERT INTO `oc_packet_wechat_area` VALUES ('84', '中国', '甘肃', '酒泉'); +INSERT INTO `oc_packet_wechat_area` VALUES ('85', '中国', '甘肃', '平凉'); +INSERT INTO `oc_packet_wechat_area` VALUES ('86', '中国', '青海', '海南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('87', '中国', '青海', '果洛'); +INSERT INTO `oc_packet_wechat_area` VALUES ('88', '中国', '青海', '玉树'); +INSERT INTO `oc_packet_wechat_area` VALUES ('89', '中国', '青海', '海东'); +INSERT INTO `oc_packet_wechat_area` VALUES ('90', '中国', '青海', '海北'); +INSERT INTO `oc_packet_wechat_area` VALUES ('91', '中国', '青海', '黄南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('92', '中国', '青海', '海西'); +INSERT INTO `oc_packet_wechat_area` VALUES ('93', '中国', '青海', '西宁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('94', '中国', '宁夏', '银川'); +INSERT INTO `oc_packet_wechat_area` VALUES ('95', '中国', '宁夏', '吴忠'); +INSERT INTO `oc_packet_wechat_area` VALUES ('96', '中国', '宁夏', '石嘴山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('97', '中国', '宁夏', '中卫'); +INSERT INTO `oc_packet_wechat_area` VALUES ('98', '中国', '宁夏', '固原'); +INSERT INTO `oc_packet_wechat_area` VALUES ('99', '中国', '云南', '红河'); +INSERT INTO `oc_packet_wechat_area` VALUES ('100', '中国', '云南', '文山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('101', '中国', '云南', '楚雄'); +INSERT INTO `oc_packet_wechat_area` VALUES ('102', '中国', '云南', '怒江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('103', '中国', '云南', '德宏'); +INSERT INTO `oc_packet_wechat_area` VALUES ('104', '中国', '云南', '西双版纳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('105', '中国', '云南', '大理'); +INSERT INTO `oc_packet_wechat_area` VALUES ('106', '中国', '云南', '迪庆'); +INSERT INTO `oc_packet_wechat_area` VALUES ('107', '中国', '云南', '昆明'); +INSERT INTO `oc_packet_wechat_area` VALUES ('108', '中国', '云南', '曲靖'); +INSERT INTO `oc_packet_wechat_area` VALUES ('109', '中国', '云南', '保山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('110', '中国', '云南', '玉溪'); +INSERT INTO `oc_packet_wechat_area` VALUES ('111', '中国', '云南', '丽江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('112', '中国', '云南', '昭通'); +INSERT INTO `oc_packet_wechat_area` VALUES ('113', '中国', '云南', '临沧'); +INSERT INTO `oc_packet_wechat_area` VALUES ('114', '中国', '云南', '普洱'); +INSERT INTO `oc_packet_wechat_area` VALUES ('115', '中国', '澳门', ''); +INSERT INTO `oc_packet_wechat_area` VALUES ('116', '中国', '香港', ''); +INSERT INTO `oc_packet_wechat_area` VALUES ('117', '中国', '贵州', '毕节'); +INSERT INTO `oc_packet_wechat_area` VALUES ('118', '中国', '贵州', '黔东南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('119', '中国', '贵州', '黔南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('120', '中国', '贵州', '铜仁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('121', '中国', '贵州', '黔西南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('122', '中国', '贵州', '贵阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('123', '中国', '贵州', '遵义'); +INSERT INTO `oc_packet_wechat_area` VALUES ('124', '中国', '贵州', '六盘水'); +INSERT INTO `oc_packet_wechat_area` VALUES ('125', '中国', '贵州', '安顺'); +INSERT INTO `oc_packet_wechat_area` VALUES ('126', '中国', '辽宁', '盘锦'); +INSERT INTO `oc_packet_wechat_area` VALUES ('127', '中国', '辽宁', '辽阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('128', '中国', '辽宁', '朝阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('129', '中国', '辽宁', '铁岭'); +INSERT INTO `oc_packet_wechat_area` VALUES ('130', '中国', '辽宁', '葫芦岛'); +INSERT INTO `oc_packet_wechat_area` VALUES ('131', '中国', '辽宁', '沈阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('132', '中国', '辽宁', '鞍山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('133', '中国', '辽宁', '大连'); +INSERT INTO `oc_packet_wechat_area` VALUES ('134', '中国', '辽宁', '本溪'); +INSERT INTO `oc_packet_wechat_area` VALUES ('135', '中国', '辽宁', '抚顺'); +INSERT INTO `oc_packet_wechat_area` VALUES ('136', '中国', '辽宁', '锦州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('137', '中国', '辽宁', '丹东'); +INSERT INTO `oc_packet_wechat_area` VALUES ('138', '中国', '辽宁', '阜新'); +INSERT INTO `oc_packet_wechat_area` VALUES ('139', '中国', '辽宁', '营口'); +INSERT INTO `oc_packet_wechat_area` VALUES ('140', '中国', '吉林', '延边'); +INSERT INTO `oc_packet_wechat_area` VALUES ('141', '中国', '吉林', '长春'); +INSERT INTO `oc_packet_wechat_area` VALUES ('142', '中国', '吉林', '四平'); +INSERT INTO `oc_packet_wechat_area` VALUES ('143', '中国', '吉林', '吉林'); +INSERT INTO `oc_packet_wechat_area` VALUES ('144', '中国', '吉林', '通化'); +INSERT INTO `oc_packet_wechat_area` VALUES ('145', '中国', '吉林', '辽源'); +INSERT INTO `oc_packet_wechat_area` VALUES ('146', '中国', '吉林', '松原'); +INSERT INTO `oc_packet_wechat_area` VALUES ('147', '中国', '吉林', '白山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('148', '中国', '吉林', '白城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('149', '中国', '黑龙江', '黑河'); +INSERT INTO `oc_packet_wechat_area` VALUES ('150', '中国', '黑龙江', '牡丹江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('151', '中国', '黑龙江', ' 绥化'); +INSERT INTO `oc_packet_wechat_area` VALUES ('152', '中国', '黑龙江', '哈尔滨'); +INSERT INTO `oc_packet_wechat_area` VALUES ('153', '中国', '黑龙江', '大兴安岭'); +INSERT INTO `oc_packet_wechat_area` VALUES ('154', '中国', '黑龙江', '鸡西'); +INSERT INTO `oc_packet_wechat_area` VALUES ('155', '中国', '黑龙江', '齐齐哈尔'); +INSERT INTO `oc_packet_wechat_area` VALUES ('156', '中国', '黑龙江', '双鸭山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('157', '中国', '黑龙江', '鹤岗'); +INSERT INTO `oc_packet_wechat_area` VALUES ('158', '中国', '黑龙江', '伊春'); +INSERT INTO `oc_packet_wechat_area` VALUES ('159', '中国', '黑龙江', '大庆'); +INSERT INTO `oc_packet_wechat_area` VALUES ('160', '中国', '黑龙江', '七台河'); +INSERT INTO `oc_packet_wechat_area` VALUES ('161', '中国', '黑龙江', '佳木斯'); +INSERT INTO `oc_packet_wechat_area` VALUES ('162', '中国', '海南', '乐东'); +INSERT INTO `oc_packet_wechat_area` VALUES ('163', '中国', '海南', '昌江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('164', '中国', '海南', '白沙'); +INSERT INTO `oc_packet_wechat_area` VALUES ('165', '中国', '海南', '西沙'); +INSERT INTO `oc_packet_wechat_area` VALUES ('166', '中国', '海南', '琼中'); +INSERT INTO `oc_packet_wechat_area` VALUES ('167', '中国', '海南', '保亭'); +INSERT INTO `oc_packet_wechat_area` VALUES ('168', '中国', '海南', '陵水'); +INSERT INTO `oc_packet_wechat_area` VALUES ('169', '中国', '海南', '中沙'); +INSERT INTO `oc_packet_wechat_area` VALUES ('170', '中国', '海南', '南沙'); +INSERT INTO `oc_packet_wechat_area` VALUES ('171', '中国', '海南', '海口'); +INSERT INTO `oc_packet_wechat_area` VALUES ('172', '中国', '海南', '三亚'); +INSERT INTO `oc_packet_wechat_area` VALUES ('173', '中国', '海南', '五指山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('174', '中国', '海南', '儋州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('175', '中国', '海南', '琼海'); +INSERT INTO `oc_packet_wechat_area` VALUES ('176', '中国', '海南', '文昌'); +INSERT INTO `oc_packet_wechat_area` VALUES ('177', '中国', '海南', '东方'); +INSERT INTO `oc_packet_wechat_area` VALUES ('178', '中国', '海南', '万宁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('179', '中国', '海南', '定安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('180', '中国', '海南', '屯昌'); +INSERT INTO `oc_packet_wechat_area` VALUES ('181', '中国', '海南', '澄迈'); +INSERT INTO `oc_packet_wechat_area` VALUES ('182', '中国', '海南', '临高'); +INSERT INTO `oc_packet_wechat_area` VALUES ('183', '中国', '广东', '揭阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('184', '中国', '广东', '中山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('185', '中国', '广东', '广州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('186', '中国', '广东', '深圳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('187', '中国', '广东', '韶关'); +INSERT INTO `oc_packet_wechat_area` VALUES ('188', '中国', '广东', '汕头'); +INSERT INTO `oc_packet_wechat_area` VALUES ('189', '中国', '广东', '珠海'); +INSERT INTO `oc_packet_wechat_area` VALUES ('190', '中国', '广东', '江门'); +INSERT INTO `oc_packet_wechat_area` VALUES ('191', '中国', '广东', '佛山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('192', '中国', '广东', '茂名'); +INSERT INTO `oc_packet_wechat_area` VALUES ('193', '中国', '广东', '湛江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('194', '中国', '广东', '惠州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('195', '中国', '广东', '肇庆'); +INSERT INTO `oc_packet_wechat_area` VALUES ('196', '中国', '广东', '汕尾'); +INSERT INTO `oc_packet_wechat_area` VALUES ('197', '中国', '广东', '梅州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('198', '中国', '广东', '阳江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('199', '中国', '广东', '河源'); +INSERT INTO `oc_packet_wechat_area` VALUES ('200', '中国', '广东', '东莞'); +INSERT INTO `oc_packet_wechat_area` VALUES ('201', '中国', '广东', '清远'); +INSERT INTO `oc_packet_wechat_area` VALUES ('202', '中国', '广东', '潮州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('203', '中国', '广东', '云浮'); +INSERT INTO `oc_packet_wechat_area` VALUES ('204', '中国', '广西', '贺州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('205', '中国', '广西', '百色'); +INSERT INTO `oc_packet_wechat_area` VALUES ('206', '中国', '广西', '来宾'); +INSERT INTO `oc_packet_wechat_area` VALUES ('207', '中国', '广西', '河池'); +INSERT INTO `oc_packet_wechat_area` VALUES ('208', '中国', '广西', '崇左'); +INSERT INTO `oc_packet_wechat_area` VALUES ('209', '中国', '广西', '南宁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('210', '中国', '广西', '桂林'); +INSERT INTO `oc_packet_wechat_area` VALUES ('211', '中国', '广西', '柳州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('212', '中国', '广西', '北海'); +INSERT INTO `oc_packet_wechat_area` VALUES ('213', '中国', '广西', '梧州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('214', '中国', '广西', '钦州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('215', '中国', '广西', '防城港'); +INSERT INTO `oc_packet_wechat_area` VALUES ('216', '中国', '广西', '玉林'); +INSERT INTO `oc_packet_wechat_area` VALUES ('217', '中国', '广西', '贵港'); +INSERT INTO `oc_packet_wechat_area` VALUES ('218', '中国', '湖北', '黄冈'); +INSERT INTO `oc_packet_wechat_area` VALUES ('219', '中国', '湖北', '荆州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('220', '中国', '湖北', '随州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('221', '中国', '湖北', '咸宁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('222', '中国', '湖北', '神农架'); +INSERT INTO `oc_packet_wechat_area` VALUES ('223', '中国', '湖北', '恩施'); +INSERT INTO `oc_packet_wechat_area` VALUES ('224', '中国', '湖北', '武汉'); +INSERT INTO `oc_packet_wechat_area` VALUES ('225', '中国', '湖北', '十堰'); +INSERT INTO `oc_packet_wechat_area` VALUES ('226', '中国', '湖北', '黄石'); +INSERT INTO `oc_packet_wechat_area` VALUES ('227', '中国', '湖北', '宜昌'); +INSERT INTO `oc_packet_wechat_area` VALUES ('228', '中国', '湖北', '鄂州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('229', '中国', '湖北', '襄樊'); +INSERT INTO `oc_packet_wechat_area` VALUES ('230', '中国', '湖北', '孝感'); +INSERT INTO `oc_packet_wechat_area` VALUES ('231', '中国', '湖北', '荆门'); +INSERT INTO `oc_packet_wechat_area` VALUES ('232', '中国', '湖北', '潜江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('233', '中国', '湖北', '仙桃'); +INSERT INTO `oc_packet_wechat_area` VALUES ('234', '中国', '湖北', '天门'); +INSERT INTO `oc_packet_wechat_area` VALUES ('235', '中国', '湖南', '永州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('236', '中国', '湖南', '郴州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('237', '中国', '湖南', '娄底'); +INSERT INTO `oc_packet_wechat_area` VALUES ('238', '中国', '湖南', '怀化'); +INSERT INTO `oc_packet_wechat_area` VALUES ('239', '中国', '湖南', '湘西'); +INSERT INTO `oc_packet_wechat_area` VALUES ('240', '中国', '湖南', '长沙'); +INSERT INTO `oc_packet_wechat_area` VALUES ('241', '中国', '湖南', '湘潭'); +INSERT INTO `oc_packet_wechat_area` VALUES ('242', '中国', '湖南', '株洲'); +INSERT INTO `oc_packet_wechat_area` VALUES ('243', '中国', '湖南', '邵阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('244', '中国', '湖南', '衡阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('245', '中国', '湖南', '常德'); +INSERT INTO `oc_packet_wechat_area` VALUES ('246', '中国', '湖南', '岳阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('247', '中国', '湖南', '益阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('248', '中国', '湖南', '张家界'); +INSERT INTO `oc_packet_wechat_area` VALUES ('249', '中国', '河南', '漯河'); +INSERT INTO `oc_packet_wechat_area` VALUES ('250', '中国', '河南', '许昌'); +INSERT INTO `oc_packet_wechat_area` VALUES ('251', '中国', '河南', '南阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('252', '中国', '河南', '三门峡'); +INSERT INTO `oc_packet_wechat_area` VALUES ('253', '中国', '河南', '信阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('254', '中国', '河南', '商丘'); +INSERT INTO `oc_packet_wechat_area` VALUES ('255', '中国', '河南', '驻马店'); +INSERT INTO `oc_packet_wechat_area` VALUES ('256', '中国', '河南', '周口'); +INSERT INTO `oc_packet_wechat_area` VALUES ('257', '中国', '河南', '济源'); +INSERT INTO `oc_packet_wechat_area` VALUES ('258', '中国', '河南', '郑州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('259', '中国', '河南', '洛阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('260', '中国', '河南', '开封'); +INSERT INTO `oc_packet_wechat_area` VALUES ('261', '中国', '河南', '安阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('262', '中国', '河南', '平顶山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('263', '中国', '河南', '新乡'); +INSERT INTO `oc_packet_wechat_area` VALUES ('264', '中国', '河南', '鹤壁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('265', '中国', '河南', '濮阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('266', '中国', '河南', '焦作'); +INSERT INTO `oc_packet_wechat_area` VALUES ('267', '中国', '台湾', '屏东县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('268', '中国', '台湾', '澎湖县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('269', '中国', '台湾', '台东县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('270', '中国', '台湾', '花莲县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('271', '中国', '台湾', '台北市'); +INSERT INTO `oc_packet_wechat_area` VALUES ('272', '中国', '台湾', '基隆市'); +INSERT INTO `oc_packet_wechat_area` VALUES ('273', '中国', '台湾', '高雄市'); +INSERT INTO `oc_packet_wechat_area` VALUES ('274', '中国', '台湾', '台南市'); +INSERT INTO `oc_packet_wechat_area` VALUES ('275', '中国', '台湾', '台中市'); +INSERT INTO `oc_packet_wechat_area` VALUES ('276', '中国', '台湾', '嘉义市'); +INSERT INTO `oc_packet_wechat_area` VALUES ('277', '中国', '台湾', '新竹市'); +INSERT INTO `oc_packet_wechat_area` VALUES ('278', '中国', '台湾', '宜兰县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('279', '中国', '台湾', '台北县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('280', '中国', '台湾', '新竹县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('281', '中国', '台湾', '桃园县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('282', '中国', '台湾', '台中县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('283', '中国', '台湾', '苗栗县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('284', '中国', '台湾', '南投县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('285', '中国', '台湾', '彰化县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('286', '中国', '台湾', '嘉义县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('287', '中国', '台湾', '云林县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('288', '中国', '台湾', '高雄县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('289', '中国', '台湾', '台南县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('290', '中国', '北京', '房山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('291', '中国', '北京', '大兴'); +INSERT INTO `oc_packet_wechat_area` VALUES ('292', '中国', '北京', '顺义'); +INSERT INTO `oc_packet_wechat_area` VALUES ('293', '中国', '北京', '通州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('294', '中国', '北京', '昌平'); +INSERT INTO `oc_packet_wechat_area` VALUES ('295', '中国', '北京', '密云'); +INSERT INTO `oc_packet_wechat_area` VALUES ('296', '中国', '北京', '平谷'); +INSERT INTO `oc_packet_wechat_area` VALUES ('297', '中国', '北京', '延庆'); +INSERT INTO `oc_packet_wechat_area` VALUES ('298', '中国', '北京', '东城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('299', '中国', '北京', '怀柔'); +INSERT INTO `oc_packet_wechat_area` VALUES ('300', '中国', '北京', '崇文'); +INSERT INTO `oc_packet_wechat_area` VALUES ('301', '中国', '北京', '西城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('302', '中国', '北京', '朝阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('303', '中国', '北京', '宣武'); +INSERT INTO `oc_packet_wechat_area` VALUES ('304', '中国', '北京', '石景山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('305', '中国', '北京', '丰台'); +INSERT INTO `oc_packet_wechat_area` VALUES ('306', '中国', '北京', '门头沟'); +INSERT INTO `oc_packet_wechat_area` VALUES ('307', '中国', '北京', '海淀'); +INSERT INTO `oc_packet_wechat_area` VALUES ('308', '中国', '河北', '衡水'); +INSERT INTO `oc_packet_wechat_area` VALUES ('309', '中国', '河北', '廊坊'); +INSERT INTO `oc_packet_wechat_area` VALUES ('310', '中国', '河北', '石家庄'); +INSERT INTO `oc_packet_wechat_area` VALUES ('311', '中国', '河北', '秦皇岛'); +INSERT INTO `oc_packet_wechat_area` VALUES ('312', '中国', '河北', '唐山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('313', '中国', '河北', '邢台'); +INSERT INTO `oc_packet_wechat_area` VALUES ('314', '中国', '河北', '邯郸'); +INSERT INTO `oc_packet_wechat_area` VALUES ('315', '中国', '河北', '张家口'); +INSERT INTO `oc_packet_wechat_area` VALUES ('316', '中国', '河北', '保定'); +INSERT INTO `oc_packet_wechat_area` VALUES ('317', '中国', '河北', '沧州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('318', '中国', '河北', '承德'); +INSERT INTO `oc_packet_wechat_area` VALUES ('319', '中国', '天津', '西青'); +INSERT INTO `oc_packet_wechat_area` VALUES ('320', '中国', '天津', '东丽'); +INSERT INTO `oc_packet_wechat_area` VALUES ('321', '中国', '天津', '北辰'); +INSERT INTO `oc_packet_wechat_area` VALUES ('322', '中国', '天津', '津南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('323', '中国', '天津', '宁河'); +INSERT INTO `oc_packet_wechat_area` VALUES ('324', '中国', '天津', '武清'); +INSERT INTO `oc_packet_wechat_area` VALUES ('325', '中国', '天津', '静海'); +INSERT INTO `oc_packet_wechat_area` VALUES ('326', '中国', '天津', '宝坻'); +INSERT INTO `oc_packet_wechat_area` VALUES ('327', '中国', '天津', '和平'); +INSERT INTO `oc_packet_wechat_area` VALUES ('328', '中国', '天津', '河西'); +INSERT INTO `oc_packet_wechat_area` VALUES ('329', '中国', '天津', '河东'); +INSERT INTO `oc_packet_wechat_area` VALUES ('330', '中国', '天津', '河北'); +INSERT INTO `oc_packet_wechat_area` VALUES ('331', '中国', '天津', '南开'); +INSERT INTO `oc_packet_wechat_area` VALUES ('332', '中国', '天津', '塘沽'); +INSERT INTO `oc_packet_wechat_area` VALUES ('333', '中国', '天津', '红桥'); +INSERT INTO `oc_packet_wechat_area` VALUES ('334', '中国', '天津', '大港'); +INSERT INTO `oc_packet_wechat_area` VALUES ('335', '中国', '天津', '汉沽'); +INSERT INTO `oc_packet_wechat_area` VALUES ('336', '中国', '天津', '蓟县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('337', '中国', '内蒙古', '锡林郭勒'); +INSERT INTO `oc_packet_wechat_area` VALUES ('338', '中国', '内蒙古', '兴安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('339', '中国', '内蒙古', '阿拉善'); +INSERT INTO `oc_packet_wechat_area` VALUES ('340', '中国', '内蒙古', '呼和浩特'); +INSERT INTO `oc_packet_wechat_area` VALUES ('341', '中国', '内蒙古', '乌海'); +INSERT INTO `oc_packet_wechat_area` VALUES ('342', '中国', '内蒙古', '包头'); +INSERT INTO `oc_packet_wechat_area` VALUES ('343', '中国', '内蒙古', '通辽'); +INSERT INTO `oc_packet_wechat_area` VALUES ('344', '中国', '内蒙古', '赤峰'); +INSERT INTO `oc_packet_wechat_area` VALUES ('345', '中国', '内蒙古', '呼伦贝尔'); +INSERT INTO `oc_packet_wechat_area` VALUES ('346', '中国', '内蒙古', '鄂尔多斯'); +INSERT INTO `oc_packet_wechat_area` VALUES ('347', '中国', '内蒙古', '乌兰察布'); +INSERT INTO `oc_packet_wechat_area` VALUES ('348', '中国', '内蒙古', '巴彦淖尔'); +INSERT INTO `oc_packet_wechat_area` VALUES ('349', '中国', '山西', '吕梁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('350', '中国', '山西', '临汾'); +INSERT INTO `oc_packet_wechat_area` VALUES ('351', '中国', '山西', '太原'); +INSERT INTO `oc_packet_wechat_area` VALUES ('352', '中国', '山西', '阳泉'); +INSERT INTO `oc_packet_wechat_area` VALUES ('353', '中国', '山西', '大同'); +INSERT INTO `oc_packet_wechat_area` VALUES ('354', '中国', '山西', '晋城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('355', '中国', '山西', '长治'); +INSERT INTO `oc_packet_wechat_area` VALUES ('356', '中国', '山西', '晋中'); +INSERT INTO `oc_packet_wechat_area` VALUES ('357', '中国', '山西', '朔州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('358', '中国', '山西', '忻州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('359', '中国', '山西', '运城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('360', '中国', '浙江', '丽水'); +INSERT INTO `oc_packet_wechat_area` VALUES ('361', '中国', '浙江', '台州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('362', '中国', '浙江', '杭州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('363', '中国', '浙江', '温州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('364', '中国', '浙江', '宁波'); +INSERT INTO `oc_packet_wechat_area` VALUES ('365', '中国', '浙江', '湖州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('366', '中国', '浙江', '嘉兴'); +INSERT INTO `oc_packet_wechat_area` VALUES ('367', '中国', '浙江', '金华'); +INSERT INTO `oc_packet_wechat_area` VALUES ('368', '中国', '浙江', '绍兴'); +INSERT INTO `oc_packet_wechat_area` VALUES ('369', '中国', '浙江', '舟山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('370', '中国', '浙江', '衢州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('371', '中国', '江苏', '镇江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('372', '中国', '江苏', '扬州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('373', '中国', '江苏', '宿迁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('374', '中国', '江苏', '泰州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('375', '中国', '江苏', '南京'); +INSERT INTO `oc_packet_wechat_area` VALUES ('376', '中国', '江苏', '徐州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('377', '中国', '江苏', '无锡'); +INSERT INTO `oc_packet_wechat_area` VALUES ('378', '中国', '江苏', '苏州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('379', '中国', '江苏', '常州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('380', '中国', '江苏', '连云港'); +INSERT INTO `oc_packet_wechat_area` VALUES ('381', '中国', '江苏', '南通'); +INSERT INTO `oc_packet_wechat_area` VALUES ('382', '中国', '江苏', '盐城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('383', '中国', '江苏', '淮安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('384', '中国', '上海', '杨浦'); +INSERT INTO `oc_packet_wechat_area` VALUES ('385', '中国', '上海', '南汇'); +INSERT INTO `oc_packet_wechat_area` VALUES ('386', '中国', '上海', '宝山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('387', '中国', '上海', '闵行'); +INSERT INTO `oc_packet_wechat_area` VALUES ('388', '中国', '上海', '浦东新'); +INSERT INTO `oc_packet_wechat_area` VALUES ('389', '中国', '上海', '嘉定'); +INSERT INTO `oc_packet_wechat_area` VALUES ('390', '中国', '上海', '松江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('391', '中国', '上海', '金山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('392', '中国', '上海', '崇明'); +INSERT INTO `oc_packet_wechat_area` VALUES ('393', '中国', '上海', '奉贤'); +INSERT INTO `oc_packet_wechat_area` VALUES ('394', '中国', '上海', '青浦'); +INSERT INTO `oc_packet_wechat_area` VALUES ('395', '中国', '上海', '黄浦'); +INSERT INTO `oc_packet_wechat_area` VALUES ('396', '中国', '上海', '卢湾'); +INSERT INTO `oc_packet_wechat_area` VALUES ('397', '中国', '上海', '长宁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('398', '中国', '上海', '徐汇'); +INSERT INTO `oc_packet_wechat_area` VALUES ('399', '中国', '上海', '普陀'); +INSERT INTO `oc_packet_wechat_area` VALUES ('400', '中国', '上海', '静安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('401', '中国', '上海', '虹口'); +INSERT INTO `oc_packet_wechat_area` VALUES ('402', '中国', '上海', '闸北'); +INSERT INTO `oc_packet_wechat_area` VALUES ('403', '中国', '山东', '日照'); +INSERT INTO `oc_packet_wechat_area` VALUES ('404', '中国', '山东', '威海'); +INSERT INTO `oc_packet_wechat_area` VALUES ('405', '中国', '山东', '临沂'); +INSERT INTO `oc_packet_wechat_area` VALUES ('406', '中国', '山东', '莱芜'); +INSERT INTO `oc_packet_wechat_area` VALUES ('407', '中国', '山东', '聊城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('408', '中国', '山东', '德州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('409', '中国', '山东', '菏泽'); +INSERT INTO `oc_packet_wechat_area` VALUES ('410', '中国', '山东', '滨州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('411', '中国', '山东', '济南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('412', '中国', '山东', '淄博'); +INSERT INTO `oc_packet_wechat_area` VALUES ('413', '中国', '山东', '青岛'); +INSERT INTO `oc_packet_wechat_area` VALUES ('414', '中国', '山东', '东营'); +INSERT INTO `oc_packet_wechat_area` VALUES ('415', '中国', '山东', '枣庄'); +INSERT INTO `oc_packet_wechat_area` VALUES ('416', '中国', '山东', '潍坊'); +INSERT INTO `oc_packet_wechat_area` VALUES ('417', '中国', '山东', '烟台'); +INSERT INTO `oc_packet_wechat_area` VALUES ('418', '中国', '山东', '泰安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('419', '中国', '山东', '济宁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('420', '中国', '江西', '上饶'); +INSERT INTO `oc_packet_wechat_area` VALUES ('421', '中国', '江西', '抚州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('422', '中国', '江西', '南昌'); +INSERT INTO `oc_packet_wechat_area` VALUES ('423', '中国', '江西', '萍乡'); +INSERT INTO `oc_packet_wechat_area` VALUES ('424', '中国', '江西', '景德镇'); +INSERT INTO `oc_packet_wechat_area` VALUES ('425', '中国', '江西', '新余'); +INSERT INTO `oc_packet_wechat_area` VALUES ('426', '中国', '江西', '九江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('427', '中国', '江西', '赣州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('428', '中国', '江西', '鹰潭'); +INSERT INTO `oc_packet_wechat_area` VALUES ('429', '中国', '江西', '宜春'); +INSERT INTO `oc_packet_wechat_area` VALUES ('430', '中国', '江西', '吉安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('431', '中国', '福建', '福州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('432', '中国', '福建', '莆田'); +INSERT INTO `oc_packet_wechat_area` VALUES ('433', '中国', '福建', '厦门'); +INSERT INTO `oc_packet_wechat_area` VALUES ('434', '中国', '福建', '泉州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('435', '中国', '福建', '三明'); +INSERT INTO `oc_packet_wechat_area` VALUES ('436', '中国', '福建', '南平'); +INSERT INTO `oc_packet_wechat_area` VALUES ('437', '中国', '福建', '漳州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('438', '中国', '福建', '宁德'); +INSERT INTO `oc_packet_wechat_area` VALUES ('439', '中国', '福建', '龙岩'); +INSERT INTO `oc_packet_wechat_area` VALUES ('440', '中国', '安徽', '滁州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('441', '中国', '安徽', '黄山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('442', '中国', '安徽', '宿州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('443', '中国', '安徽', '阜阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('444', '中国', '安徽', '六安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('445', '中国', '安徽', '巢湖'); +INSERT INTO `oc_packet_wechat_area` VALUES ('446', '中国', '安徽', '池州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('447', '中国', '安徽', '亳州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('448', '中国', '安徽', '宣城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('449', '中国', '安徽', '合肥'); +INSERT INTO `oc_packet_wechat_area` VALUES ('450', '中国', '安徽', '蚌埠'); +INSERT INTO `oc_packet_wechat_area` VALUES ('451', '中国', '安徽', '芜湖'); +INSERT INTO `oc_packet_wechat_area` VALUES ('452', '中国', '安徽', '马鞍山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('453', '中国', '安徽', '淮南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('454', '中国', '安徽', '铜陵'); +INSERT INTO `oc_packet_wechat_area` VALUES ('455', '中国', '安徽', '淮北'); +INSERT INTO `oc_packet_wechat_area` VALUES ('456', '中国', '安徽', '安庆'); +INSERT INTO `oc_packet_wechat_area` VALUES ('457', '中国', '西藏', '那曲'); +INSERT INTO `oc_packet_wechat_area` VALUES ('458', '中国', '西藏', '阿里'); +INSERT INTO `oc_packet_wechat_area` VALUES ('459', '中国', '西藏', '林芝'); +INSERT INTO `oc_packet_wechat_area` VALUES ('460', '中国', '西藏', '昌都'); +INSERT INTO `oc_packet_wechat_area` VALUES ('461', '中国', '西藏', '山南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('462', '中国', '西藏', '日喀则'); +INSERT INTO `oc_packet_wechat_area` VALUES ('463', '中国', '西藏', '拉萨'); +INSERT INTO `oc_packet_wechat_area` VALUES ('464', '中国', '新疆', '博尔塔拉'); +INSERT INTO `oc_packet_wechat_area` VALUES ('465', '中国', '新疆', '吐鲁番'); +INSERT INTO `oc_packet_wechat_area` VALUES ('466', '中国', '新疆', '哈密'); +INSERT INTO `oc_packet_wechat_area` VALUES ('467', '中国', '新疆', '昌吉'); +INSERT INTO `oc_packet_wechat_area` VALUES ('468', '中国', '新疆', '和田'); +INSERT INTO `oc_packet_wechat_area` VALUES ('469', '中国', '新疆', '喀什'); +INSERT INTO `oc_packet_wechat_area` VALUES ('470', '中国', '新疆', '克孜勒苏'); +INSERT INTO `oc_packet_wechat_area` VALUES ('471', '中国', '新疆', '巴音郭楞'); +INSERT INTO `oc_packet_wechat_area` VALUES ('472', '中国', '新疆', '阿克苏'); +INSERT INTO `oc_packet_wechat_area` VALUES ('473', '中国', '新疆', '伊犁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('474', '中国', '新疆', '塔城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('475', '中国', '新疆', '乌鲁木齐'); +INSERT INTO `oc_packet_wechat_area` VALUES ('476', '中国', '新疆', '阿勒泰'); +INSERT INTO `oc_packet_wechat_area` VALUES ('477', '中国', '新疆', '克拉玛依'); +INSERT INTO `oc_packet_wechat_area` VALUES ('478', '中国', '新疆', '石河子'); +INSERT INTO `oc_packet_wechat_area` VALUES ('479', '中国', '新疆', '图木舒克'); +INSERT INTO `oc_packet_wechat_area` VALUES ('480', '中国', '新疆', '阿拉尔'); +INSERT INTO `oc_packet_wechat_area` VALUES ('481', '中国', '新疆', '五家渠'); diff --git a/plugins/.gitignore b/plugins/.gitignore new file mode 100644 index 0000000..b722e9e --- /dev/null +++ b/plugins/.gitignore @@ -0,0 +1 @@ +!.gitignore \ No newline at end of file diff --git a/plugins/DevTeam/DevTeam.php b/plugins/DevTeam/DevTeam.php new file mode 100644 index 0000000..5579a08 --- /dev/null +++ b/plugins/DevTeam/DevTeam.php @@ -0,0 +1,81 @@ + + */ +class DevTeam extends Plugin +{ + /** + * @var array 插件信息 + */ + public $info = [ + // 插件名[必填] + 'name' => 'DevTeam', + // 插件标题[必填] + 'title' => '开发团队成员信息', + // 插件唯一标识[必填],格式:插件名.开发者标识.plugin + 'identifier' => 'dev_team.ming.plugin', + // 插件图标[选填] + 'icon' => 'fa fa-fw fa-users', + // 插件描述[选填] + 'description' => '在后台首页显示开发团队成员信息', + // 插件作者[必填] + 'author' => '蔡伟明', + // 作者主页[选填] + 'author_url' => 'http://www.caiweiming.com', + // 插件版本[必填],格式采用三段式:主版本号.次版本号.修订版本号 + 'version' => '1.0.0', + // 是否有后台管理功能[选填] + 'admin' => '0', + ]; + + /** + * @var array 插件钩子 + */ + public $hooks = [ + 'admin_index' + ]; + + /** + * 后台首页钩子 + * @author 蔡伟明 <314013107@qq.com> + */ + public function adminIndex() + { + $config = $this->getConfigValue(); + if ($config['display']) { + $this->fetch('widget', $config); + } + } + + /** + * 安装方法 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function install(){ + return true; + } + + /** + * 卸载方法必 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function uninstall(){ + return true; + } +} diff --git a/plugins/DevTeam/config.php b/plugins/DevTeam/config.php new file mode 100644 index 0000000..0fd9a07 --- /dev/null +++ b/plugins/DevTeam/config.php @@ -0,0 +1,16 @@ + '不显示', '1' => '显示'], 1], + ['radio', 'width', '显示宽度', '该插件在后台首页所占的显示宽度', ['2' => '2格', '4' => '4格', '6' => '6格', '8' => '8格', '12' => '12格'], 6], +]; diff --git a/plugins/DevTeam/view/widget.html b/plugins/DevTeam/view/widget.html new file mode 100644 index 0000000..e69de29 diff --git a/plugins/HelloWorld/HelloWorld.php b/plugins/HelloWorld/HelloWorld.php new file mode 100644 index 0000000..2718ef7 --- /dev/null +++ b/plugins/HelloWorld/HelloWorld.php @@ -0,0 +1,217 @@ + + */ +class HelloWorld extends Plugin +{ + /** + * @var array 插件信息 + */ + public $info = [ + // 插件名[必填] + 'name' => 'HelloWorld', + // 插件标题[必填] + 'title' => '你好,世界', + // 插件唯一标识[必填],格式:插件名.开发者标识.plugin + 'identifier' => 'helloworld.ming.plugin', + // 插件图标[选填] + 'icon' => 'fa fa-fw fa-globe', + // 插件描述[选填] + 'description' => '这是一个演示插件,会在每个页面生成一个提示“Hello World”。您可以查看源码,里面包含了绝大部分插件所用到的方法,以及能做的事情。', + // 插件作者[必填] + 'author' => '蔡伟明', + // 作者主页[选填] + 'author_url' => 'http://www.dolphinphp.com', + // 插件版本[必填],格式采用三段式:主版本号.次版本号.修订版本号 + 'version' => '1.0.0', + // 是否有后台管理功能 + 'admin' => '1', + ]; + + /** + * @var array 管理界面字段信息 + */ + public $admin = [ + 'title' => '后台列表', // 后台管理标题 + 'table_name' => 'plugin_hello', // 数据库表名,如果没有用到数据库,则留空 + 'order' => 'said,name', // 需要排序功能的字段,多个字段用逗号隔开 + 'filter' => '', // 需要筛选功能的字段,多个字段用逗号隔开 + 'search_title' => '', // 搜索框提示文字,一般不用填写 + 'search_field' => [ // 需要搜索的字段,如果需要搜索,则必填,否则不填 + 'said' => '名言', + 'name' => '出处' + ], + 'search_url' => '', // 搜索框url链接,如:'user/index',一般不用填写 + + // 后台列表字段 + 'columns' => [ + ['id', 'ID'], + ['said', '名言'], + ['name', '出处'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ], + + // 右侧按钮 + 'right_buttons' => [ + 'edit', // 使用系统自带的编辑按钮 + 'enable', // 使用系统自带的启用按钮 + 'disable', // 使用系统自带的禁用按钮 + 'delete', // 使用系统自带的删除按钮 + + // 自定义按钮,可定义多个 + 'customs' => [ + [ + 'title' => '自定义按钮1,新窗口打开', + 'icon' => 'fa fa-list', + 'href' => [ + 'url' => 'HelloWorld/Admin/testTable', + ], + 'target' => '_blank', + ], + // 自定义按钮并带有参数 + [ + 'title' => '自定义按钮2,自定义参数', + 'icon' => 'fa fa-user', + 'href' => [ + 'url' => 'HelloWorld/Admin/testForm', + 'params' => [ + 'id' => '__id__', + 'table' => '__table__', + 'name' => 'molly', + 'age' => 12 + ] + ], + ], + [ + 'title' => '自定义页面', + 'icon' => 'fa fa-file', + 'href' => [ + 'url' => 'HelloWorld/Admin/testPage' + ], + ], + ], + ], + + // 顶部栏按钮 + 'top_buttons' => [ + 'add', // 使用系统自带的添加按钮 + 'enable', // 使用系统自带的启用按钮 + 'disable',// 使用系统自带的禁用按钮 + 'delete', // 使用系统自带的删除按钮 + + // 自定义按钮,可定义多个 + 'customs' => [ + [ + 'title' => ' 自定义按钮1', + 'href' => [ + 'url' => 'HelloWorld/Admin/testTable', + ], + 'target' => '_blank', + ], + // 自定义按钮并带有参数 + [ + 'title' => ' 自定义按钮2', + 'href' => [ + 'url' => 'HelloWorld/Admin/testForm', + 'params' => [ + 'name' => 'molly', + 'age' => 12 + ] + ], + ], + [ + 'title' => ' 自定义页面', + 'href' => [ + 'url' => 'HelloWorld/Admin/testPage' + ], + ], + ], + ], + ]; + + /** + * @var array 新增或编辑的字段 + */ + public $fields = [ + [ + 'name' => 'name', + 'title' => '出处', + 'type' => 'text', + 'value' => '', + ], + [ + 'name' => 'said', + 'title' => '名言', + 'type' => 'text', + 'value' => '', + 'tip' => '提示', + ] + ]; + + /** + * @var string 原数据库表前缀 + * 用于在导入插件sql时,将原有的表前缀转换成系统的表前缀 + * 一般插件自带sql文件时才需要配置 + */ + public $database_prefix = 'dolphin_'; + + /** + * @var array 插件钩子 + */ + public $hooks = [ + // 钩子名称 => 钩子说明 + // 如果是系统钩子,则钩子说明不用填写 + 'page_tips', + 'my_hook' => '我的钩子', + ]; + + /** + * page_tips钩子方法 + * @param $params + * @author 蔡伟明 <314013107@qq.com> + */ + public function pageTips($params) + { + echo '

    + +

    Hello World

    +
    '; + } + + /** + * 安装方法必须实现 + * 一般只需返回true即可 + * 如果安装前有需要实现一些业务,可在此方法实现 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function install(){ + return true; + } + + /** + * 卸载方法必须实现 + * 一般只需返回true即可 + * 如果安装前有需要实现一些业务,可在此方法实现 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function uninstall(){ + return true; + } +} diff --git a/plugins/HelloWorld/config.php b/plugins/HelloWorld/config.php new file mode 100644 index 0000000..25a8ec5 --- /dev/null +++ b/plugins/HelloWorld/config.php @@ -0,0 +1,32 @@ + '开启', '0' => '关闭'], 1], + ['text', 'text', '单行文本', '提示', 'x'], + ['textarea', 'textarea', '多行文本', '提示'], + ['checkbox', 'checkbox', '多选', '提示', ['1' => '是', '0' => '否'], 0], + ['group', + [ + '分组1' => [ + ['radio', 'status1', '单选', '', ['1' => '开启', '0' => '关闭'], 1], + ['text', 'text1', '单行文本', '提示', 'x'], + ['array', 'textarea1', '多行文本2', '提示'], + ['checkbox', 'checkbox1', '多选', '提示', ['1' => '是', '0' => '否'], 0], + ], + '分组2' => [ + ['textarea', 'textarea2', '多行文本', '提示'], + ['checkbox', 'checkbox2', '多选', '提示', ['1' => '是', '0' => '否'], 0], + ] + ] + ] +]; diff --git a/plugins/HelloWorld/controller/Admin.php b/plugins/HelloWorld/controller/Admin.php new file mode 100644 index 0000000..a7425e9 --- /dev/null +++ b/plugins/HelloWorld/controller/Admin.php @@ -0,0 +1,225 @@ + + * @return mixed + * @throws \think\Exception + * @throws \think\exception\DbException + */ + public function index() + { + // 查询条件 + $map = $this->getMap(); + + $data_list = HelloWorld::where($map)->order('id desc')->paginate(); + // 分页数据 + $page = $data_list->render(); + + // 自定义按钮 + $btnOne = [ + 'title' => '自定义按钮1', + 'icon' => 'fa fa-list', + 'href' => plugin_url('HelloWorld/Admin/testTable'), + 'target' => '_blank', + ]; + $btnTwo = [ + 'title' => '自定义按钮2', + 'icon' => 'fa fa-user', + 'href' => plugin_url('HelloWorld/Admin/testForm', ['name' => 'molly', 'age' => 12]), + ]; + $btnThree = [ + 'title' => '自定义页面', + 'icon' => 'fa fa-file', + 'href' => plugin_url('HelloWorld/Admin/testPage'), + ]; + $btnBack['title'] = '返回插件列表'; + $btnBack['icon'] = 'fa fa-reply'; + $btnBack['href'] = url('plugin/index'); + $btnBack['class'] = 'btn btn-warning'; + + // 用TableBuilder渲染模板 + return ZBuilder::make('table') + ->setPageTitle('数据列表') + ->setSearch(['id' => 'ID', 'said' => '名言', 'name' => '出处']) + ->addColumn('id', 'ID') + ->addColumns([ + ['said', '名言'], + ['name', '出处'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('custom', $btnBack) + ->addTopButton('add', ['plugin_name' => 'HelloWorld']) + ->addTopButtons('enable,disable,delete') + ->addTopButton('custom', $btnOne) + ->addTopButton('custom', $btnTwo) + ->addTopButton('custom', $btnThree) + ->addRightButton('edit', ['plugin_name' => 'HelloWorld']) + ->addRightButtons('enable,disable,delete') + ->addRightButton('custom', $btnOne) + ->addRightButton('custom', $btnTwo) + ->addRightButton('custom', $btnThree) + ->setTableName('plugin_hello') + ->setRowList($data_list) + ->setPages($page) + ->fetch(); + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + */ + public function add() + { + if ($this->request->isPost()) { + $data = $this->request->post(); + // 验证数据 + $result = $this->validate($data, [ + 'name|出处' => 'require', + 'said|名言' => 'require', + ]); + if(true !== $result){ + // 验证失败 输出错误信息 + $this->error($result); + } + + // 插入数据 + if (HelloWorld::create($data)) { + $this->success('新增成功', cookie('__forward__')); + } else { + $this->error('新增失败'); + } + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('新增') + ->addFormItem('text', 'name', '出处') + ->addFormItem('text', 'said', '名言') + ->fetch(); + } + + /** + * 编辑 + * @author 蔡伟明 <314013107@qq.com> + */ + public function edit() + { + if ($this->request->isPost()) { + $data = $this->request->post(); + + // 使用自定义的验证器验证数据 + $validate = new HelloWorldValidate(); + if (!$validate->check($data)) { + // 验证失败 输出错误信息 + $this->error($validate->getError()); + } + + // 更新数据 + if (HelloWorld::update($data)) { + $this->success('编辑成功', cookie('__forward__')); + } else { + $this->error('编辑失败'); + } + } + + $id = input('param.id'); + + // 获取数据 + $info = HelloWorld::get($id); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑') + ->addFormItem('hidden', 'id') + ->addFormItem('text', 'name', '出处') + ->addFormItem('text', 'said', '名言') + ->setFormData($info) + ->fetch(); + } + + /** + * 插件自定义方法 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function testTable() + { + // 使用ZBuilder快速创建表单 + return ZBuilder::make('table') + ->setPageTitle('插件自定义方法(列表)') + ->setSearch(['said' => '名言', 'name' => '出处']) + ->addColumn('id', 'ID') + ->addColumn('said', '名言') + ->addColumn('name', '出处') + ->addColumn('status', '状态', 'switch') + ->addColumn('right_button', '操作', 'btn') + ->setTableName('plugin_hello') + ->fetch(); + } + + /** + * 插件自定义方法 + * 这里的参数是根据插件定义的按钮链接按顺序设置 + * @param string $id + * @param string $table + * @param string $name + * @param string $age + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws \think\Exception + */ + public function testForm($id = '', $table = '', $name = '', $age = '') + { + if ($this->request->isPost()) { + $data = $this->request->post(); + halt($data); + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('插件自定义方法(表单)') + ->addFormItem('text', 'name', '出处') + ->addFormItem('text', 'said', '名言') + ->fetch(); + } + + /** + * 自定义页面 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function testPage() + { + // 1.使用默认的方法渲染模板,必须指定完整的模板文件名(包括模板后缀) +// return $this->fetch(config('plugin_path'). 'HelloWorld/view/index.html'); + + // 2.使用已封装好的快捷方法,该方法只用于加载插件模板 + // 如果不指定模板名称,则自动加载插件view目录下与当前方法名一致的模板 + return $this->pluginView(); +// return $this->pluginView('index'); // 指定模板名称 +// return $this->pluginView('', 'tpl'); // 指定模板后缀 + } +} diff --git a/plugins/HelloWorld/install.sql b/plugins/HelloWorld/install.sql new file mode 100644 index 0000000..21b5bbf --- /dev/null +++ b/plugins/HelloWorld/install.sql @@ -0,0 +1,34 @@ +/* +// +---------------------------------------------------------------------- +// | 海豚PHP框架 [ DolphinPHP ] +// +---------------------------------------------------------------------- +// | 版权所有 2016~2017 河源市卓锐科技有限公司 [ http://www.zrthink.com ] +// +---------------------------------------------------------------------- +// | 官方网站: http://dolphinphp.com +// +---------------------------------------------------------------------- +// | 开源协议 ( http://www.apache.org/licenses/LICENSE-2.0 ) +// +---------------------------------------------------------------------- +*/ + +SET FOREIGN_KEY_CHECKS=0; + +-- ---------------------------- +-- Table structure for `dolphin_plugin_hello` +-- ---------------------------- +DROP TABLE IF EXISTS `dolphin_plugin_hello`; +CREATE TABLE `dolphin_plugin_hello` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '名人', + `said` text NOT NULL COMMENT '名言', + `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=6 DEFAULT CHARSET=utf8; + +-- ---------------------------- +-- Records of dolphin_plugin_hello +-- ---------------------------- +INSERT INTO `dolphin_plugin_hello` VALUES ('1', '网络', '生活是一面镜子。你对它笑,它就对你笑;你对它哭,它也对你哭。', '1'); +INSERT INTO `dolphin_plugin_hello` VALUES ('2', '网络', '活着一天,就是有福气,就该珍惜。当我哭泣我没有鞋子穿的时候,我发现有人却没有脚。', '1'); +INSERT INTO `dolphin_plugin_hello` VALUES ('3', '爱迪生', '天才是百分之一的灵感加百分之九十九的汗水。', '1'); +INSERT INTO `dolphin_plugin_hello` VALUES ('4', '美华纳', '勿问成功的秘诀为何,且尽全力做你应该做的事吧。', '1'); +INSERT INTO `dolphin_plugin_hello` VALUES ('5', '陶铸', '如烟往事俱忘却,心底无私天地宽', '1'); diff --git a/plugins/HelloWorld/model/HelloWorld.php b/plugins/HelloWorld/model/HelloWorld.php new file mode 100644 index 0000000..1876d95 --- /dev/null +++ b/plugins/HelloWorld/model/HelloWorld.php @@ -0,0 +1,28 @@ + 'require', + 'said|名言' => 'require', + ]; +} diff --git a/plugins/HelloWorld/view/index.html b/plugins/HelloWorld/view/index.html new file mode 100644 index 0000000..417b7b7 --- /dev/null +++ b/plugins/HelloWorld/view/index.html @@ -0,0 +1,5 @@ +{extend name="layout" /} + +{block name="content"} +

    自定义页面

    +{/block} \ No newline at end of file diff --git a/plugins/HelloWorld/view/testPage.html b/plugins/HelloWorld/view/testPage.html new file mode 100644 index 0000000..fd73b33 --- /dev/null +++ b/plugins/HelloWorld/view/testPage.html @@ -0,0 +1,5 @@ +{extend name="layout" /} + +{block name="content"} +

    自定义页面testPage

    +{/block} \ No newline at end of file diff --git a/plugins/HelloWorld/view/testPage.tpl b/plugins/HelloWorld/view/testPage.tpl new file mode 100644 index 0000000..f57f64f --- /dev/null +++ b/plugins/HelloWorld/view/testPage.tpl @@ -0,0 +1,5 @@ +{extend name="layout" /} + +{block name="content"} +

    自定义页面testPage.tpl(自定义后缀)

    +{/block} \ No newline at end of file diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 0000000..9ed55d6 --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 系统插件目录 diff --git a/plugins/SystemInfo/SystemInfo.php b/plugins/SystemInfo/SystemInfo.php new file mode 100644 index 0000000..ed67bca --- /dev/null +++ b/plugins/SystemInfo/SystemInfo.php @@ -0,0 +1,82 @@ + + */ +class SystemInfo extends Plugin +{ + /** + * @var array 插件信息 + */ + public $info = [ + // 插件名[必填] + 'name' => 'SystemInfo', + // 插件标题[必填] + 'title' => '系统环境信息', + // 插件唯一标识[必填],格式:插件名.开发者标识.plugin + 'identifier' => 'system_info.ming.plugin', + // 插件图标[选填] + 'icon' => 'fa fa-fw fa-info-circle', + // 插件描述[选填] + 'description' => '在后台首页显示服务器信息', + // 插件作者[必填] + 'author' => '蔡伟明', + // 作者主页[选填] + 'author_url' => 'http://www.caiweiming.com', + // 插件版本[必填],格式采用三段式:主版本号.次版本号.修订版本号 + 'version' => '1.0.0', + // 是否有后台管理功能[选填] + 'admin' => '0', + ]; + + /** + * @var array 插件钩子 + */ + public $hooks = [ + 'admin_index' + ]; + + /** + * 后台首页钩子 + * @author 蔡伟明 <314013107@qq.com> + * @throws \Exception + */ + public function adminIndex() + { + $config = $this->getConfigValue(); + if ($config['display']) { + $this->fetch('widget', $config); + } + } + + /** + * 安装方法 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function install(){ + return true; + } + + /** + * 卸载方法必 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function uninstall(){ + return true; + } +} \ No newline at end of file diff --git a/plugins/SystemInfo/config.php b/plugins/SystemInfo/config.php new file mode 100644 index 0000000..0fd9a07 --- /dev/null +++ b/plugins/SystemInfo/config.php @@ -0,0 +1,16 @@ + '不显示', '1' => '显示'], 1], + ['radio', 'width', '显示宽度', '该插件在后台首页所占的显示宽度', ['2' => '2格', '4' => '4格', '6' => '6格', '8' => '8格', '12' => '12格'], 6], +]; diff --git a/plugins/SystemInfo/view/widget.html b/plugins/SystemInfo/view/widget.html new file mode 100644 index 0000000..3a639de --- /dev/null +++ b/plugins/SystemInfo/view/widget.html @@ -0,0 +1,41 @@ +
    +
    +
    +

    系统信息

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    DolphinPHP版本{:config('dolphin.product_version')}
    ThinkPHP版本{$Think.VERSION}
    服务器操作系统{$Think.const.PHP_OS}
    运行环境{$Request.server.SERVER_SOFTWARE}
    MYSQL版本{:db()->query('select version() as version')[0]['version']}
    PHP版本{$Think.const.PHP_VERSION}
    上传限制{:ini_get('upload_max_filesize')}
    +
    +
    +
    \ No newline at end of file diff --git a/public/.user.ini b/public/.user.ini new file mode 100644 index 0000000..4a8db5e --- /dev/null +++ b/public/.user.ini @@ -0,0 +1 @@ +open_basedir=/www/wwwroot/buildgroup.ccjjj.com/:/tmp/ \ No newline at end of file diff --git a/public/.well-known/acme-challenge/zNntQ53o2Qg--AsIkQ3Cf0KujvLIOJDsTjo2P09Akqg b/public/.well-known/acme-challenge/zNntQ53o2Qg--AsIkQ3Cf0KujvLIOJDsTjo2P09Akqg new file mode 100644 index 0000000..1a48623 --- /dev/null +++ b/public/.well-known/acme-challenge/zNntQ53o2Qg--AsIkQ3Cf0KujvLIOJDsTjo2P09Akqg @@ -0,0 +1 @@ +zNntQ53o2Qg--AsIkQ3Cf0KujvLIOJDsTjo2P09Akqg.gS8rpmKHu5OkfWM1wCAIgkoTOyRstV-_XqypCMTuilQ \ No newline at end of file diff --git a/public/LICENSE.txt b/public/LICENSE.txt new file mode 100644 index 0000000..2255550 --- /dev/null +++ b/public/LICENSE.txt @@ -0,0 +1,27 @@ +版权所有 (c) 2016~2017,河源市卓锐科技有限公司保留所有权利。 + +DolphinPHP 用户协议 + +版权所有 (c) 2016~2017,河源市卓锐科技有限公司保留所有权利。 + +感谢您选择DolphinPHP,希望我们的努力能为您提供一个极简极致极速的PHP快速开发解决方案。 + +用户须知:本协议是您与河源市卓锐科技有限公司(以下简称卓锐公司)之间关于您使用DolphinPHP产品及服务的法律协议。无论您是个人或组织、盈利与否、用途如何(包括以学习和研究为目的),均需仔细阅读本协议,包括免除或者限制卓锐公司责任的免责条款及对您的权利限制。请您审阅并接受或不接受本服务条款。如您不同意本服务条款及/或卓锐公司随时对其的修改,您应不使用或主动取消DolphinPHP产品。否则,您的任何对DolphinPHP的相关服务的注册、登陆、下载、查看等使用行为将被视为您对本服务条款全部的完全接受,包括接受卓锐公司对服务条款随时所做的任何修改。 + +本服务条款一旦发生变更, 卓锐公司将在产品官网上公布修改内容。修改后的服务条款一旦在网站公布即有效代替原来的服务条款。您可随时登陆官网查阅最新版服务条款。如果您选择接受本条款,即表示您同意接受协议各项条件的约束。如果您不同意本服务条款,则不能获得使用本服务的权利。您若有违反本条款规定,卓锐公司有权随时中止或终止您对DolphinPHP产品的使用资格并保留追究相关法律责任的权利。 + +在理解、同意、并遵守本协议的全部条款后,方可开始使用DolphinPHP产品。您也可能与卓锐公司直接签订另一书面协议,以补充或者取代本协议的全部或者任何部分。 + +卓锐公司拥有DolphinPHP的全部知识产权,包括商标和著作权。本软件只供许可协议,并非出售。卓锐公司只允许您在遵守本协议各项条款的情况下复制、下载、安装、使用或者以其他方式受益于本软件的功能或者知识产权。 + +个人非商业用途可免费使用(但不包括其衍生产品、插件或者服务),也可以根据个人实际情况选择是否购买授权。 + +非个人用户(泛指非个人的团体,如企业、政府单位、教育机构、协会团体、厂矿、工作室等)必须购买软件授权后方可使用。 + +您可以在协议规定的约束和限制范围内修改DolphinPHP以适应您的网站要求,但免费版必须保留软件版本信息的正常显示及相关连接正常。 + +禁止去除DolphinPHP源码里的版权信息,商业授权版本可去除后台界面及前台界面的相关版权信息。 + +禁止在DolphinPHP整体或任何部分基础上发展任何派生版本、修改版本或第三方版本用于重新分发。 + +未经官方许可,不得对本软件或与之关联的商业授权进行出租、出售、抵押或发放子许可证。 \ No newline at end of file diff --git a/public/admin.php b/public/admin.php new file mode 100644 index 0000000..0c6f9fe --- /dev/null +++ b/public/admin.php @@ -0,0 +1,39 @@ +bind('install')->run()->send(); +} else { + // 执行应用并响应 + Container::get('app')->run()->send(); +} diff --git a/public/extend/.gitignore b/public/extend/.gitignore new file mode 100644 index 0000000..b722e9e --- /dev/null +++ b/public/extend/.gitignore @@ -0,0 +1 @@ +!.gitignore \ No newline at end of file diff --git a/public/extend/form/README.md b/public/extend/form/README.md new file mode 100644 index 0000000..cf04d0c --- /dev/null +++ b/public/extend/form/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 表单项扩展目录 diff --git a/public/extend/form/complextable/complextable.css b/public/extend/form/complextable/complextable.css new file mode 100644 index 0000000..a212d1e --- /dev/null +++ b/public/extend/form/complextable/complextable.css @@ -0,0 +1,3 @@ +.table-bordered > tbody > tr > td:last-child{ + border-right: 1px solid #f0f0f0; +} \ No newline at end of file diff --git a/public/extend/form/complextable/complextable.html b/public/extend/form/complextable/complextable.html new file mode 100644 index 0000000..8c9b27d --- /dev/null +++ b/public/extend/form/complextable/complextable.html @@ -0,0 +1,47 @@ +
    + +
    + + {notempty name="head"} + + {volist name="head" id="row"} + + {volist name="row" id="vo"} + + {/volist} + + {/volist} + + {/notempty} + + {empty name="data"} + + {else/} + {volist name="data" id="row"} + + {volist name="row" id="vo"} + + {/volist} + + {/volist} + {/empty} + +
    {$vo.value|raw|default=''}
    暂无数据
    + {if condition="is_array($vo['value'])"} + + + {volist name="vo.value" id="v"} + + {volist name="v" id="td"} + + {/volist} + + {/volist} + +
    {$td|raw}
    + {else/} + {$vo.value|raw|default=''} + {/if} +
    +
    +
    \ No newline at end of file diff --git a/public/extend/form/selectgroup/selectgroup.html b/public/extend/form/selectgroup/selectgroup.html new file mode 100644 index 0000000..e89851c --- /dev/null +++ b/public/extend/form/selectgroup/selectgroup.html @@ -0,0 +1,18 @@ +
    + +
    + +
    +
    {$tips|default=''}
    +
    \ No newline at end of file diff --git a/public/extend/form/selectgroup/selectgroup.js b/public/extend/form/selectgroup/selectgroup.js new file mode 100644 index 0000000..4c117b2 --- /dev/null +++ b/public/extend/form/selectgroup/selectgroup.js @@ -0,0 +1,3 @@ +jQuery(function () { + App.initHelpers(['select2']); +}); \ No newline at end of file diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..2998edf --- /dev/null +++ b/public/index.php @@ -0,0 +1,34 @@ +bind('install')->run()->send(); +} else { + // 执行应用并响应 + Container::get('app')->run()->send(); +} diff --git a/public/min/.htaccess b/public/min/.htaccess new file mode 100644 index 0000000..adb0653 --- /dev/null +++ b/public/min/.htaccess @@ -0,0 +1,13 @@ + +RewriteEngine on + +# You may need RewriteBase on some servers +#RewriteBase /min + +# rewrite URLs like "/min/f=..." to "/min/?f=..." +RewriteRule ^([bfg]=.*) index.php?$1 [L,NE] + + +# In case AddOutputFilterByType has been added +SetEnv no-gzip + diff --git a/public/min/LICENSE.txt b/public/min/LICENSE.txt new file mode 100644 index 0000000..2255550 --- /dev/null +++ b/public/min/LICENSE.txt @@ -0,0 +1,27 @@ +版权所有 (c) 2016~2017,河源市卓锐科技有限公司保留所有权利。 + +DolphinPHP 用户协议 + +版权所有 (c) 2016~2017,河源市卓锐科技有限公司保留所有权利。 + +感谢您选择DolphinPHP,希望我们的努力能为您提供一个极简极致极速的PHP快速开发解决方案。 + +用户须知:本协议是您与河源市卓锐科技有限公司(以下简称卓锐公司)之间关于您使用DolphinPHP产品及服务的法律协议。无论您是个人或组织、盈利与否、用途如何(包括以学习和研究为目的),均需仔细阅读本协议,包括免除或者限制卓锐公司责任的免责条款及对您的权利限制。请您审阅并接受或不接受本服务条款。如您不同意本服务条款及/或卓锐公司随时对其的修改,您应不使用或主动取消DolphinPHP产品。否则,您的任何对DolphinPHP的相关服务的注册、登陆、下载、查看等使用行为将被视为您对本服务条款全部的完全接受,包括接受卓锐公司对服务条款随时所做的任何修改。 + +本服务条款一旦发生变更, 卓锐公司将在产品官网上公布修改内容。修改后的服务条款一旦在网站公布即有效代替原来的服务条款。您可随时登陆官网查阅最新版服务条款。如果您选择接受本条款,即表示您同意接受协议各项条件的约束。如果您不同意本服务条款,则不能获得使用本服务的权利。您若有违反本条款规定,卓锐公司有权随时中止或终止您对DolphinPHP产品的使用资格并保留追究相关法律责任的权利。 + +在理解、同意、并遵守本协议的全部条款后,方可开始使用DolphinPHP产品。您也可能与卓锐公司直接签订另一书面协议,以补充或者取代本协议的全部或者任何部分。 + +卓锐公司拥有DolphinPHP的全部知识产权,包括商标和著作权。本软件只供许可协议,并非出售。卓锐公司只允许您在遵守本协议各项条款的情况下复制、下载、安装、使用或者以其他方式受益于本软件的功能或者知识产权。 + +个人非商业用途可免费使用(但不包括其衍生产品、插件或者服务),也可以根据个人实际情况选择是否购买授权。 + +非个人用户(泛指非个人的团体,如企业、政府单位、教育机构、协会团体、厂矿、工作室等)必须购买软件授权后方可使用。 + +您可以在协议规定的约束和限制范围内修改DolphinPHP以适应您的网站要求,但免费版必须保留软件版本信息的正常显示及相关连接正常。 + +禁止去除DolphinPHP源码里的版权信息,商业授权版本可去除后台界面及前台界面的相关版权信息。 + +禁止在DolphinPHP整体或任何部分基础上发展任何派生版本、修改版本或第三方版本用于重新分发。 + +未经官方许可,不得对本软件或与之关联的商业授权进行出租、出售、抵押或发放子许可证。 \ No newline at end of file diff --git a/public/min/config-test.php b/public/min/config-test.php new file mode 100644 index 0000000..c903086 --- /dev/null +++ b/public/min/config-test.php @@ -0,0 +1,10 @@ + + * array('//symlink' => '/real/target/path') // unix + * array('//static' => 'D:\\staticStorage') // Windows + * + */ +$min_symlinks = array(); + + +/** + * If you upload files from Windows to a non-Windows server, Windows may report + * incorrect mtimes for the files. This may cause Minify to keep serving stale + * cache files when source file changes are made too frequently (e.g. more than + * once an hour). + * + * Immediately after modifying and uploading a file, use the touch command to + * update the mtime on the server. If the mtime jumps ahead by a number of hours, + * set this variable to that number. If the mtime moves back, this should not be + * needed. + * + * In the Windows SFTP client WinSCP, there's an option that may fix this + * issue without changing the variable below. Under login > environment, + * select the option "Adjust remote timestamp with DST". + * @link http://winscp.net/eng/docs/ui_login_environment#daylight_saving_time + */ +$min_uploaderHoursBehind = 0; + + +/** + * Path to Minify's lib folder. If you happen to move it, change + * this accordingly. + */ +$min_libPath = dirname(__FILE__) . '/lib'; + + +// try to disable output_compression (may not have an effect) +ini_set('zlib.output_compression', '0'); diff --git a/public/min/groupsConfig.php b/public/min/groupsConfig.php new file mode 100644 index 0000000..2dfbb85 --- /dev/null +++ b/public/min/groupsConfig.php @@ -0,0 +1,150 @@ + [ // 默认加载 + $core_path. "jquery.min.js", + $core_path. "bootstrap.min.js", + $core_path. "jquery.slimscroll.min.js", + $core_path. "jquery.scrollLock.min.js", + $core_path. "jquery.appear.min.js", + $core_path. "jquery.countTo.min.js", + $core_path. "jquery.placeholder.min.js", + $core_path. "js.cookie.min.js", + $libs_path. "magnific-popup/magnific-popup.min.js", + $static_path. "admin/js/app.js", + $static_path. "admin/js/dolphin.js", + $static_path. "admin/js/builder/form.js", + $static_path. "admin/js/builder/aside.js", + $static_path. "admin/js/builder/table.js", + $libs_path. "viewer/viewer.min.js" + ], + 'core_css' => [ // 默认加载 + $libs_path. "magnific-popup/magnific-popup.min.css", + $static_path. "admin/css/bootstrap.min.css", + $static_path. "admin/css/oneui.css", + $static_path. "admin/css/dolphin.css", + $libs_path. "viewer/viewer.min.css" + ], + 'libs_js' => [ // 默认加载 + $libs_path. "bootstrap-notify/bootstrap-notify.min.js", + $libs_path. "sweetalert/sweetalert.min.js", + ], + 'libs_css' => [ // 默认加载 + $libs_path. "sweetalert/sweetalert.min.css", + ], + 'datepicker_js' => [ // 日期选择 + $libs_path. "bootstrap-datepicker/bootstrap-datepicker.min.js", + $libs_path. "bootstrap-datepicker/locales/bootstrap-datepicker.zh-CN.min.js", + ], + 'datepicker_css' => [ // 日期选择 + $libs_path. "bootstrap-datepicker/bootstrap-datepicker3.min.css", + ], + 'datetimepicker_js' => [ // 日期时间选择 + $libs_path. "bootstrap-datetimepicker/moment.min.js", + $libs_path. "bootstrap-datetimepicker/bootstrap-datetimepicker.min.js", + $libs_path. "bootstrap-datetimepicker/locale/zh-cn.js", + ], + 'moment_js' => [ + $libs_path. "bootstrap-datetimepicker/moment.min.js", + ], + 'datetimepicker_css' => [ // 日期时间选择 + $libs_path. "bootstrap-datetimepicker/bootstrap-datetimepicker.min.css" + ], + 'webuploader_js' => [ // 文件或图片上传 + $libs_path. "webuploader/webuploader.min.js", + ], + 'webuploader_css' => [ // 文件或图片上传 + $libs_path. "webuploader/webuploader.css", + ], + 'select2_js' => [ // 下拉框 + $libs_path. "select2/select2.full.min.js", + $libs_path. "select2/i18n/zh-CN.js", + ], + 'select2_css' => [ // 下拉框 + $libs_path. "select2/select2.min.css", + $libs_path. "select2/select2-bootstrap.min.css", + ], + 'tags_js' => [ // 标签 + $libs_path. "jquery-tags-input/jquery.tagsinput.min.js", + ], + 'tags_css' => [ // 标签 + $libs_path. "jquery-tags-input/jquery.tagsinput.min.css", + ], + 'validate_js' => [ // 验证 + $libs_path. "jquery-validation/jquery.validate.min.js", + ], + 'editable_js' => [ // 快速编辑 + $libs_path. "bootstrap3-editable/js/bootstrap-editable.js", + ], + 'editable_css' => [ // 快速编辑 + $libs_path. "bootstrap3-editable/css/bootstrap-editable.css", + ], + 'colorpicker_js' => [ // 取色器 + $libs_path. "bootstrap-colorpicker/bootstrap-colorpicker.min.js", + ], + 'colorpicker_css' => [ // 取色器 + $libs_path. "bootstrap-colorpicker/css/bootstrap-colorpicker.min.css", + ], + 'editormd_js' => [ // markdown编辑器 + $libs_path. "editormd/editormd.min.js", + ], + 'jcrop_js' => [ // 图片裁剪 + $libs_path. "jcrop/js/Jcrop.min.js", + ], + 'jcrop_css' => [ // 图片裁剪 + $libs_path. "jcrop/css/Jcrop.min.css", + ], + 'masked_inputs_js' => [ // 格式文本 + $libs_path. "masked-inputs/jquery.maskedinput.min.js", + ], + 'rangeslider_js' => [ // 范围 + $libs_path. "ion-rangeslider/js/ion.rangeSlider.min.js", + ], + 'rangeslider_css' => [ // 范围 + $libs_path. "ion-rangeslider/css/ion.rangeSlider.min.css", + $libs_path. "ion-rangeslider/css/ion.rangeSlider.skinHTML5.min.css", + ], + 'nestable_js' => [ // 拖拽排序 + $libs_path. "jquery-nestable/jquery.nestable.js", + ], + 'nestable_css' => [ // 拖拽排序 + $libs_path. "jquery-nestable/jquery.nestable.css", + ], + 'wangeditor_js' => [ // wang编辑器 + $libs_path. "wang-editor/js/wangEditor.min.js", + ], + 'wangeditor_css' => [ // wang编辑器 + $libs_path. "wang-editor/css/wangEditor.min.css", + ], + 'summernote_js' => [ // summernote编辑器 + $libs_path. "summernote/summernote.min.js", + $libs_path. "summernote/lang/summernote-zh-CN.js", + ], + 'summernote_css' => [ // summernote编辑器 + $libs_path. "summernote/summernote.min.css", + ], + 'jqueryui_js' => [ // jqueryui + $libs_path. "jquery-ui/jquery-ui.min.js", + ], + 'daterangepicker_js' => [ // 日期时间范围 + $libs_path. "bootstrap-daterangepicker/daterangepicker.js", + ], + 'daterangepicker_css' => [ // 日期时间范围 + $libs_path. "bootstrap-daterangepicker/daterangepicker.css", + ] +]; \ No newline at end of file diff --git a/public/min/index.php b/public/min/index.php new file mode 100644 index 0000000..e700588 --- /dev/null +++ b/public/min/index.php @@ -0,0 +1,92 @@ + MINIFY_MIN_DIR . '/config.php', + 'test' => MINIFY_MIN_DIR . '/config-test.php', + 'groups' => MINIFY_MIN_DIR . '/groupsConfig.php' +); + +// check for custom config paths +if (!empty($min_customConfigPaths) && is_array($min_customConfigPaths)) { + $min_configPaths = array_merge($min_configPaths, $min_customConfigPaths); +} + +// load config +require $min_configPaths['base']; + +if (isset($_GET['test'])) { + include $min_configPaths['test']; +} + +require "$min_libPath/Minify/Loader.php"; +Minify_Loader::register(); + +Minify::$uploaderHoursBehind = $min_uploaderHoursBehind; +Minify::setCache( + isset($min_cachePath) ? $min_cachePath : '' + ,$min_cacheFileLocking +); + +if ($min_documentRoot) { + $_SERVER['DOCUMENT_ROOT'] = $min_documentRoot; + Minify::$isDocRootSet = true; +} + +$min_serveOptions['minifierOptions']['text/css']['symlinks'] = $min_symlinks; +// auto-add targets to allowDirs +foreach ($min_symlinks as $uri => $target) { + $min_serveOptions['minApp']['allowDirs'][] = $target; +} + +if ($min_allowDebugFlag) { + $min_serveOptions['debug'] = Minify_DebugDetector::shouldDebugRequest($_COOKIE, $_GET, $_SERVER['REQUEST_URI']); +} + +if (!empty($min_concatOnly)) { + $min_serveOptions['concatOnly'] = true; +} + +if ($min_errorLogger) { + if (true === $min_errorLogger) { + $min_errorLogger = FirePHP::getInstance(true); + } + Minify_Logger::setLogger($min_errorLogger); +} + +// check for URI versioning +if (preg_match('/&\\d/', $_SERVER['QUERY_STRING']) || isset($_GET['v'])) { + $min_serveOptions['maxAge'] = 31536000; +} + +// need groups config? +if (isset($_GET['g'])) { + // well need groups config + $min_serveOptions['minApp']['groups'] = (require $min_configPaths['groups']); +} + +// serve or redirect +if (isset($_GET['f']) || isset($_GET['g'])) { + if (! isset($min_serveController)) { + $min_serveController = new Minify_Controller_MinApp(); + } + + Minify::serve($min_serveController, $min_serveOptions); + + +} elseif ($min_enableBuilder) { + header('Location: builder/'); + exit; +} else { + header('Location: /'); + exit; +} diff --git a/public/min/lib/CSSmin.php b/public/min/lib/CSSmin.php new file mode 100644 index 0000000..9236db3 --- /dev/null +++ b/public/min/lib/CSSmin.php @@ -0,0 +1,777 @@ +memory_limit = 128 * 1048576; // 128MB in bytes + $this->max_execution_time = 60; // 1 min + $this->pcre_backtrack_limit = 1000 * 1000; + $this->pcre_recursion_limit = 500 * 1000; + + $this->raise_php_limits = (bool) $raise_php_limits; + } + + /** + * Minify a string of CSS + * @param string $css + * @param int|bool $linebreak_pos + * @return string + */ + public function run($css = '', $linebreak_pos = FALSE) + { + if (empty($css)) { + return ''; + } + + if ($this->raise_php_limits) { + $this->do_raise_php_limits(); + } + + $this->comments = array(); + $this->preserved_tokens = array(); + + $start_index = 0; + $length = strlen($css); + + $css = $this->extract_data_urls($css); + + // collect all comment blocks... + while (($start_index = $this->index_of($css, '/*', $start_index)) >= 0) { + $end_index = $this->index_of($css, '*/', $start_index + 2); + if ($end_index < 0) { + $end_index = $length; + } + $comment_found = $this->str_slice($css, $start_index + 2, $end_index); + $this->comments[] = $comment_found; + $comment_preserve_string = self::COMMENT . (count($this->comments) - 1) . '___'; + $css = $this->str_slice($css, 0, $start_index + 2) . $comment_preserve_string . $this->str_slice($css, $end_index); + // Set correct start_index: Fixes issue #2528130 + $start_index = $end_index + 2 + strlen($comment_preserve_string) - strlen($comment_found); + } + + // preserve strings so their content doesn't get accidentally minified + $css = preg_replace_callback('/(?:"(?:[^\\\\"]|\\\\.|\\\\)*")|'."(?:'(?:[^\\\\']|\\\\.|\\\\)*')/S", array($this, 'replace_string'), $css); + + // Let's divide css code in chunks of 5.000 chars aprox. + // Reason: PHP's PCRE functions like preg_replace have a "backtrack limit" + // of 100.000 chars by default (php < 5.3.7) so if we're dealing with really + // long strings and a (sub)pattern matches a number of chars greater than + // the backtrack limit number (i.e. /(.*)/s) PCRE functions may fail silently + // returning NULL and $css would be empty. + $charset = ''; + $charset_regexp = '/(@charset)( [^;]+;)/i'; + $css_chunks = array(); + $css_chunk_length = 5000; // aprox size, not exact + $start_index = 0; + $i = $css_chunk_length; // save initial iterations + $l = strlen($css); + + + // if the number of characters is 5000 or less, do not chunk + if ($l <= $css_chunk_length) { + $css_chunks[] = $css; + } else { + // chunk css code securely + while ($i < $l) { + $i += 50; // save iterations + if ($l - $start_index <= $css_chunk_length || $i >= $l) { + $css_chunks[] = $this->str_slice($css, $start_index); + break; + } + if ($css[$i - 1] === '}' && $i - $start_index > $css_chunk_length) { + // If there are two ending curly braces }} separated or not by spaces, + // join them in the same chunk (i.e. @media blocks) + $next_chunk = substr($css, $i); + if (preg_match('/^\s*\}/', $next_chunk)) { + $i = $i + $this->index_of($next_chunk, '}') + 1; + } + + $css_chunks[] = $this->str_slice($css, $start_index, $i); + $start_index = $i; + } + } + } + + // Minify each chunk + for ($i = 0, $n = count($css_chunks); $i < $n; $i++) { + $css_chunks[$i] = $this->minify($css_chunks[$i], $linebreak_pos); + // Keep the first @charset at-rule found + if (empty($charset) && preg_match($charset_regexp, $css_chunks[$i], $matches)) { + $charset = strtolower($matches[1]) . $matches[2]; + } + // Delete all @charset at-rules + $css_chunks[$i] = preg_replace($charset_regexp, '', $css_chunks[$i]); + } + + // Update the first chunk and push the charset to the top of the file. + $css_chunks[0] = $charset . $css_chunks[0]; + + return implode('', $css_chunks); + } + + /** + * Sets the memory limit for this script + * @param int|string $limit + */ + public function set_memory_limit($limit) + { + $this->memory_limit = $this->normalize_int($limit); + } + + /** + * Sets the maximum execution time for this script + * @param int|string $seconds + */ + public function set_max_execution_time($seconds) + { + $this->max_execution_time = (int) $seconds; + } + + /** + * Sets the PCRE backtrack limit for this script + * @param int $limit + */ + public function set_pcre_backtrack_limit($limit) + { + $this->pcre_backtrack_limit = (int) $limit; + } + + /** + * Sets the PCRE recursion limit for this script + * @param int $limit + */ + public function set_pcre_recursion_limit($limit) + { + $this->pcre_recursion_limit = (int) $limit; + } + + /** + * Try to configure PHP to use at least the suggested minimum settings + */ + private function do_raise_php_limits() + { + $php_limits = array( + 'memory_limit' => $this->memory_limit, + 'max_execution_time' => $this->max_execution_time, + 'pcre.backtrack_limit' => $this->pcre_backtrack_limit, + 'pcre.recursion_limit' => $this->pcre_recursion_limit + ); + + // If current settings are higher respect them. + foreach ($php_limits as $name => $suggested) { + $current = $this->normalize_int(ini_get($name)); + // memory_limit exception: allow -1 for "no memory limit". + if ($current > -1 && ($suggested == -1 || $current < $suggested)) { + ini_set($name, $suggested); + } + } + } + + /** + * Does bulk of the minification + * @param string $css + * @param int|bool $linebreak_pos + * @return string + */ + private function minify($css, $linebreak_pos) + { + // strings are safe, now wrestle the comments + for ($i = 0, $max = count($this->comments); $i < $max; $i++) { + + $token = $this->comments[$i]; + $placeholder = '/' . self::COMMENT . $i . '___/'; + + // ! in the first position of the comment means preserve + // so push to the preserved tokens keeping the ! + if (substr($token, 0, 1) === '!') { + $this->preserved_tokens[] = $token; + $token_tring = self::TOKEN . (count($this->preserved_tokens) - 1) . '___'; + $css = preg_replace($placeholder, $token_tring, $css, 1); + // Preserve new lines for /*! important comments + $css = preg_replace('/\s*[\n\r\f]+\s*(\/\*'. $token_tring .')/S', self::NL.'$1', $css); + $css = preg_replace('/('. $token_tring .'\*\/)\s*[\n\r\f]+\s*/', '$1'.self::NL, $css); + continue; + } + + // \ in the last position looks like hack for Mac/IE5 + // shorten that to /*\*/ and the next one to /**/ + if (substr($token, (strlen($token) - 1), 1) === '\\') { + $this->preserved_tokens[] = '\\'; + $css = preg_replace($placeholder, self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1); + $i = $i + 1; // attn: advancing the loop + $this->preserved_tokens[] = ''; + $css = preg_replace('/' . self::COMMENT . $i . '___/', self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1); + continue; + } + + // keep empty comments after child selectors (IE7 hack) + // e.g. html >/**/ body + if (strlen($token) === 0) { + $start_index = $this->index_of($css, $this->str_slice($placeholder, 1, -1)); + if ($start_index > 2) { + if (substr($css, $start_index - 3, 1) === '>') { + $this->preserved_tokens[] = ''; + $css = preg_replace($placeholder, self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1); + } + } + } + + // in all other cases kill the comment + $css = preg_replace('/\/\*' . $this->str_slice($placeholder, 1, -1) . '\*\//', '', $css, 1); + } + + + // Normalize all whitespace strings to single spaces. Easier to work with that way. + $css = preg_replace('/\s+/', ' ', $css); + + // Fix IE7 issue on matrix filters which browser accept whitespaces between Matrix parameters + $css = preg_replace_callback('/\s*filter\:\s*progid:DXImageTransform\.Microsoft\.Matrix\(([^\)]+)\)/', array($this, 'preserve_old_IE_specific_matrix_definition'), $css); + + // Shorten & preserve calculations calc(...) since spaces are important + $css = preg_replace_callback('/calc(\(((?:[^\(\)]+|(?1))*)\))/i', array($this, 'replace_calc'), $css); + + // Replace positive sign from numbers preceded by : or a white-space before the leading space is removed + // +1.2em to 1.2em, +.8px to .8px, +2% to 2% + $css = preg_replace('/((? -9.0 to -9 + $css = preg_replace('/((?\+\(\)\]\~\=,])/', '$1', $css); + + // Restore spaces for !important + $css = preg_replace('/\!important/i', ' !important', $css); + + // bring back the colon + $css = preg_replace('/' . self::CLASSCOLON . '/', ':', $css); + + // retain space for special IE6 cases + $css = preg_replace_callback('/\:first\-(line|letter)(\{|,)/i', array($this, 'lowercase_pseudo_first'), $css); + + // no space after the end of a preserved comment + $css = preg_replace('/\*\/ /', '*/', $css); + + // lowercase some popular @directives + $css = preg_replace_callback('/@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)/i', array($this, 'lowercase_directives'), $css); + + // lowercase some more common pseudo-elements + $css = preg_replace_callback('/:(active|after|before|checked|disabled|empty|enabled|first-(?:child|of-type)|focus|hover|last-(?:child|of-type)|link|only-(?:child|of-type)|root|:selection|target|visited)/i', array($this, 'lowercase_pseudo_elements'), $css); + + // lowercase some more common functions + $css = preg_replace_callback('/:(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:moz|webkit)-)?any)\(/i', array($this, 'lowercase_common_functions'), $css); + + // lower case some common function that can be values + // NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us + $css = preg_replace_callback('/([:,\( ]\s*)(attr|color-stop|from|rgba|to|url|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|max|min|(?:repeating-)?(?:linear|radial)-gradient)|-webkit-gradient)/iS', array($this, 'lowercase_common_functions_values'), $css); + + // Put the space back in some cases, to support stuff like + // @media screen and (-webkit-min-device-pixel-ratio:0){ + $css = preg_replace('/\band\(/i', 'and (', $css); + + // Remove the spaces after the things that should not have spaces after them. + $css = preg_replace('/([\!\{\}\:;\>\+\(\[\~\=,])\s+/S', '$1', $css); + + // remove unnecessary semicolons + $css = preg_replace('/;+\}/', '}', $css); + + // Fix for issue: #2528146 + // Restore semicolon if the last property is prefixed with a `*` (lte IE7 hack) + // to avoid issues on Symbian S60 3.x browsers. + $css = preg_replace('/(\*[a-z0-9\-]+\s*\:[^;\}]+)(\})/', '$1;$2', $css); + + // Replace 0 and 0 values with 0. + // data type: https://developer.mozilla.org/en-US/docs/Web/CSS/length + // data type: https://developer.mozilla.org/en-US/docs/Web/CSS/percentage + $css = preg_replace('/([^\\\\]\:|\s)0(?:em|ex|ch|rem|vw|vh|vm|vmin|cm|mm|in|px|pt|pc|%)/iS', '${1}0', $css); + + // 0% step in a keyframe? restore the % unit + $css = preg_replace_callback('/(@[a-z\-]*?keyframes[^\{]+\{)(.*?)(\}\})/iS', array($this, 'replace_keyframe_zero'), $css); + + // Replace 0 0; or 0 0 0; or 0 0 0 0; with 0. + $css = preg_replace('/\:0(?: 0){1,3}(;|\}| \!)/', ':0$1', $css); + + // Fix for issue: #2528142 + // Replace text-shadow:0; with text-shadow:0 0 0; + $css = preg_replace('/(text-shadow\:0)(;|\}| \!)/i', '$1 0 0$2', $css); + + // Replace background-position:0; with background-position:0 0; + // same for transform-origin + // Changing -webkit-mask-position: 0 0 to just a single 0 will result in the second parameter defaulting to 50% (center) + $css = preg_replace('/(background\-position|webkit-mask-position|(?:webkit|moz|o|ms|)\-?transform\-origin)\:0(;|\}| \!)/iS', '$1:0 0$2', $css); + + // Shorten colors from rgb(51,102,153) to #336699, rgb(100%,0%,0%) to #ff0000 (sRGB color space) + // Shorten colors from hsl(0, 100%, 50%) to #ff0000 (sRGB color space) + // This makes it more likely that it'll get further compressed in the next step. + $css = preg_replace_callback('/rgb\s*\(\s*([0-9,\s\-\.\%]+)\s*\)(.{1})/i', array($this, 'rgb_to_hex'), $css); + $css = preg_replace_callback('/hsl\s*\(\s*([0-9,\s\-\.\%]+)\s*\)(.{1})/i', array($this, 'hsl_to_hex'), $css); + + // Shorten colors from #AABBCC to #ABC or short color name. + $css = $this->compress_hex_colors($css); + + // border: none to border:0, outline: none to outline:0 + $css = preg_replace('/(border\-?(?:top|right|bottom|left|)|outline)\:none(;|\}| \!)/iS', '$1:0$2', $css); + + // shorter opacity IE filter + $css = preg_replace('/progid\:DXImageTransform\.Microsoft\.Alpha\(Opacity\=/i', 'alpha(opacity=', $css); + + // Find a fraction that is used for Opera's -o-device-pixel-ratio query + // Add token to add the "\" back in later + $css = preg_replace('/\(([a-z\-]+):([0-9]+)\/([0-9]+)\)/i', '($1:$2'. self::QUERY_FRACTION .'$3)', $css); + + // Remove empty rules. + $css = preg_replace('/[^\};\{\/]+\{\}/S', '', $css); + + // Add "/" back to fix Opera -o-device-pixel-ratio query + $css = preg_replace('/'. self::QUERY_FRACTION .'/', '/', $css); + + // Replace multiple semi-colons in a row by a single one + // See SF bug #1980989 + $css = preg_replace('/;;+/', ';', $css); + + // Restore new lines for /*! important comments + $css = preg_replace('/'. self::NL .'/', "\n", $css); + + // Lowercase all uppercase properties + $css = preg_replace_callback('/(\{|\;)([A-Z\-]+)(\:)/', array($this, 'lowercase_properties'), $css); + + // Some source control tools don't like it when files containing lines longer + // than, say 8000 characters, are checked in. The linebreak option is used in + // that case to split long lines after a specific column. + if ($linebreak_pos !== FALSE && (int) $linebreak_pos >= 0) { + $linebreak_pos = (int) $linebreak_pos; + $start_index = $i = 0; + while ($i < strlen($css)) { + $i++; + if ($css[$i - 1] === '}' && $i - $start_index > $linebreak_pos) { + $css = $this->str_slice($css, 0, $i) . "\n" . $this->str_slice($css, $i); + $start_index = $i; + } + } + } + + // restore preserved comments and strings in reverse order + for ($i = count($this->preserved_tokens) - 1; $i >= 0; $i--) { + $css = preg_replace('/' . self::TOKEN . $i . '___/', $this->preserved_tokens[$i], $css, 1); + } + + // Trim the final string (for any leading or trailing white spaces) + return trim($css); + } + + /** + * Utility method to replace all data urls with tokens before we start + * compressing, to avoid performance issues running some of the subsequent + * regexes against large strings chunks. + * + * @param string $css + * @return string + */ + private function extract_data_urls($css) + { + // Leave data urls alone to increase parse performance. + $max_index = strlen($css) - 1; + $append_index = $index = $last_index = $offset = 0; + $sb = array(); + $pattern = '/url\(\s*(["\']?)data\:/i'; + + // Since we need to account for non-base64 data urls, we need to handle + // ' and ) being part of the data string. Hence switching to indexOf, + // to determine whether or not we have matching string terminators and + // handling sb appends directly, instead of using matcher.append* methods. + + while (preg_match($pattern, $css, $m, 0, $offset)) { + $index = $this->index_of($css, $m[0], $offset); + $last_index = $index + strlen($m[0]); + $start_index = $index + 4; // "url(".length() + $end_index = $last_index - 1; + $terminator = $m[1]; // ', " or empty (not quoted) + $found_terminator = FALSE; + + if (strlen($terminator) === 0) { + $terminator = ')'; + } + + while ($found_terminator === FALSE && $end_index+1 <= $max_index) { + $end_index = $this->index_of($css, $terminator, $end_index + 1); + + // endIndex == 0 doesn't really apply here + if ($end_index > 0 && substr($css, $end_index - 1, 1) !== '\\') { + $found_terminator = TRUE; + if (')' != $terminator) { + $end_index = $this->index_of($css, ')', $end_index); + } + } + } + + // Enough searching, start moving stuff over to the buffer + $sb[] = $this->str_slice($css, $append_index, $index); + + if ($found_terminator) { + $token = $this->str_slice($css, $start_index, $end_index); + $token = preg_replace('/\s+/', '', $token); + $this->preserved_tokens[] = $token; + + $preserver = 'url(' . self::TOKEN . (count($this->preserved_tokens) - 1) . '___)'; + $sb[] = $preserver; + + $append_index = $end_index + 1; + } else { + // No end terminator found, re-add the whole match. Should we throw/warn here? + $sb[] = $this->str_slice($css, $index, $last_index); + $append_index = $last_index; + } + + $offset = $last_index; + } + + $sb[] = $this->str_slice($css, $append_index); + + return implode('', $sb); + } + + /** + * Utility method to compress hex color values of the form #AABBCC to #ABC or short color name. + * + * DOES NOT compress CSS ID selectors which match the above pattern (which would break things). + * e.g. #AddressForm { ... } + * + * DOES NOT compress IE filters, which have hex color values (which would break things). + * e.g. filter: chroma(color="#FFFFFF"); + * + * DOES NOT compress invalid hex values. + * e.g. background-color: #aabbccdd + * + * @param string $css + * @return string + */ + private function compress_hex_colors($css) + { + // Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters) + $pattern = '/(\=\s*?["\']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/iS'; + $_index = $index = $last_index = $offset = 0; + $sb = array(); + // See: http://ajaxmin.codeplex.com/wikipage?title=CSS%20Colors + $short_safe = array( + '#808080' => 'gray', + '#008000' => 'green', + '#800000' => 'maroon', + '#000080' => 'navy', + '#808000' => 'olive', + '#ffa500' => 'orange', + '#800080' => 'purple', + '#c0c0c0' => 'silver', + '#008080' => 'teal', + '#f00' => 'red' + ); + + while (preg_match($pattern, $css, $m, 0, $offset)) { + $index = $this->index_of($css, $m[0], $offset); + $last_index = $index + strlen($m[0]); + $is_filter = $m[1] !== null && $m[1] !== ''; + + $sb[] = $this->str_slice($css, $_index, $index); + + if ($is_filter) { + // Restore, maintain case, otherwise filter will break + $sb[] = $m[1] . '#' . $m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7]; + } else { + if (strtolower($m[2]) == strtolower($m[3]) && + strtolower($m[4]) == strtolower($m[5]) && + strtolower($m[6]) == strtolower($m[7])) { + // Compress. + $hex = '#' . strtolower($m[3] . $m[5] . $m[7]); + } else { + // Non compressible color, restore but lower case. + $hex = '#' . strtolower($m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7]); + } + // replace Hex colors to short safe color names + $sb[] = array_key_exists($hex, $short_safe) ? $short_safe[$hex] : $hex; + } + + $_index = $offset = $last_index - strlen($m[8]); + } + + $sb[] = $this->str_slice($css, $_index); + + return implode('', $sb); + } + + /* CALLBACKS + * --------------------------------------------------------------------------------------------- + */ + + private function replace_string($matches) + { + $match = $matches[0]; + $quote = substr($match, 0, 1); + // Must use addcslashes in PHP to avoid parsing of backslashes + $match = addcslashes($this->str_slice($match, 1, -1), '\\'); + + // maybe the string contains a comment-like substring? + // one, maybe more? put'em back then + if (($pos = $this->index_of($match, self::COMMENT)) >= 0) { + for ($i = 0, $max = count($this->comments); $i < $max; $i++) { + $match = preg_replace('/' . self::COMMENT . $i . '___/', $this->comments[$i], $match, 1); + } + } + + // minify alpha opacity in filter strings + $match = preg_replace('/progid\:DXImageTransform\.Microsoft\.Alpha\(Opacity\=/i', 'alpha(opacity=', $match); + + $this->preserved_tokens[] = $match; + return $quote . self::TOKEN . (count($this->preserved_tokens) - 1) . '___' . $quote; + } + + private function replace_colon($matches) + { + return preg_replace('/\:/', self::CLASSCOLON, $matches[0]); + } + + private function replace_calc($matches) + { + $this->preserved_tokens[] = trim(preg_replace('/\s*([\*\/\(\),])\s*/', '$1', $matches[2])); + return 'calc('. self::TOKEN . (count($this->preserved_tokens) - 1) . '___' . ')'; + } + + private function preserve_old_IE_specific_matrix_definition($matches) + { + $this->preserved_tokens[] = $matches[1]; + return 'filter:progid:DXImageTransform.Microsoft.Matrix(' . self::TOKEN . (count($this->preserved_tokens) - 1) . '___' . ')'; + } + + private function replace_keyframe_zero($matches) + { + return $matches[1] . preg_replace('/0(\{|,[^\)\{]+\{)/', '0%$1', $matches[2]) . $matches[3]; + } + + private function rgb_to_hex($matches) + { + // Support for percentage values rgb(100%, 0%, 45%); + if ($this->index_of($matches[1], '%') >= 0){ + $rgbcolors = explode(',', str_replace('%', '', $matches[1])); + for ($i = 0; $i < count($rgbcolors); $i++) { + $rgbcolors[$i] = $this->round_number(floatval($rgbcolors[$i]) * 2.55); + } + } else { + $rgbcolors = explode(',', $matches[1]); + } + + // Values outside the sRGB color space should be clipped (0-255) + for ($i = 0; $i < count($rgbcolors); $i++) { + $rgbcolors[$i] = $this->clamp_number(intval($rgbcolors[$i], 10), 0, 255); + $rgbcolors[$i] = sprintf("%02x", $rgbcolors[$i]); + } + + // Fix for issue #2528093 + if (!preg_match('/[\s\,\);\}]/', $matches[2])){ + $matches[2] = ' ' . $matches[2]; + } + + return '#' . implode('', $rgbcolors) . $matches[2]; + } + + private function hsl_to_hex($matches) + { + $values = explode(',', str_replace('%', '', $matches[1])); + $h = floatval($values[0]); + $s = floatval($values[1]); + $l = floatval($values[2]); + + // Wrap and clamp, then fraction! + $h = ((($h % 360) + 360) % 360) / 360; + $s = $this->clamp_number($s, 0, 100) / 100; + $l = $this->clamp_number($l, 0, 100) / 100; + + if ($s == 0) { + $r = $g = $b = $this->round_number(255 * $l); + } else { + $v2 = $l < 0.5 ? $l * (1 + $s) : ($l + $s) - ($s * $l); + $v1 = (2 * $l) - $v2; + $r = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h + (1/3))); + $g = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h)); + $b = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h - (1/3))); + } + + return $this->rgb_to_hex(array('', $r.','.$g.','.$b, $matches[2])); + } + + private function lowercase_pseudo_first($matches) + { + return ':first-'. strtolower($matches[1]) .' '. $matches[2]; + } + + private function lowercase_directives($matches) + { + return '@'. strtolower($matches[1]); + } + + private function lowercase_pseudo_elements($matches) + { + return ':'. strtolower($matches[1]); + } + + private function lowercase_common_functions($matches) + { + return ':'. strtolower($matches[1]) .'('; + } + + private function lowercase_common_functions_values($matches) + { + return $matches[1] . strtolower($matches[2]); + } + + private function lowercase_properties($matches) + { + return $matches[1].strtolower($matches[2]).$matches[3]; + } + + /* HELPERS + * --------------------------------------------------------------------------------------------- + */ + + private function hue_to_rgb($v1, $v2, $vh) + { + $vh = $vh < 0 ? $vh + 1 : ($vh > 1 ? $vh - 1 : $vh); + if ($vh * 6 < 1) return $v1 + ($v2 - $v1) * 6 * $vh; + if ($vh * 2 < 1) return $v2; + if ($vh * 3 < 2) return $v1 + ($v2 - $v1) * ((2/3) - $vh) * 6; + return $v1; + } + + private function round_number($n) + { + return intval(floor(floatval($n) + 0.5), 10); + } + + private function clamp_number($n, $min, $max) + { + return min(max($n, $min), $max); + } + + /** + * PHP port of Javascript's "indexOf" function for strings only + * Author: Tubal Martin http://blog.margenn.com + * + * @param string $haystack + * @param string $needle + * @param int $offset index (optional) + * @return int + */ + private function index_of($haystack, $needle, $offset = 0) + { + $index = strpos($haystack, $needle, $offset); + + return ($index !== FALSE) ? $index : -1; + } + + /** + * PHP port of Javascript's "slice" function for strings only + * Author: Tubal Martin http://blog.margenn.com + * Tests: http://margenn.com/tubal/str_slice/ + * + * @param string $str + * @param int $start index + * @param int|bool $end index (optional) + * @return string + */ + private function str_slice($str, $start = 0, $end = FALSE) + { + if ($end !== FALSE && ($start < 0 || $end <= 0)) { + $max = strlen($str); + + if ($start < 0) { + if (($start = $max + $start) < 0) { + return ''; + } + } + + if ($end < 0) { + if (($end = $max + $end) < 0) { + return ''; + } + } + + if ($end <= $start) { + return ''; + } + } + + $slice = ($end === FALSE) ? substr($str, $start) : substr($str, $start, $end - $start); + return ($slice === FALSE) ? '' : $slice; + } + + /** + * Convert strings like "64M" or "30" to int values + * @param mixed $size + * @return int + */ + private function normalize_int($size) + { + if (is_string($size)) { + switch (substr($size, -1)) { + case 'M': case 'm': return $size * 1048576; + case 'K': case 'k': return $size * 1024; + case 'G': case 'g': return $size * 1073741824; + } + } + + return (int) $size; + } +} \ No newline at end of file diff --git a/public/min/lib/DooDigestAuth.php b/public/min/lib/DooDigestAuth.php new file mode 100644 index 0000000..ec2cedd --- /dev/null +++ b/public/min/lib/DooDigestAuth.php @@ -0,0 +1,123 @@ + + * @link http://www.doophp.com/ + * @copyright Copyright © 2009 Leng Sheng Hong + * @license http://www.doophp.com/license + */ + +/** + * Handles HTTP digest authentication + * + *

    HTTP digest authentication can be used with the URI router. + * HTTP digest is much more recommended over the use of HTTP Basic auth which doesn't provide any encryption. + * If you are running PHP on Apache in CGI/FastCGI mode, you would need to + * add the following line to your .htaccess for digest auth to work correctly.

    + * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L] + * + *

    This class is tested under Apache 2.2 and Cherokee web server. It should work in both mod_php and cgi mode.

    + * + * @author Leng Sheng Hong + * @version $Id: DooDigestAuth.php 1000 2009-07-7 18:27:22 + * @package doo.auth + * @since 1.0 + * + * @deprecated 2.3 This will be removed in Minify 3.0 + */ +class DooDigestAuth{ + + /** + * Authenticate against a list of username and passwords. + * + *

    HTTP Digest Authentication doesn't work with PHP in CGI mode, + * you have to add this into your .htaccess RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L]

    + * + * @param string $realm Name of the authentication session + * @param array $users An assoc array of username and password: array('uname1'=>'pwd1', 'uname2'=>'pwd2') + * @param string $fail_msg Message to be displayed if the User cancel the login + * @param string $fail_url URL to be redirect if the User cancel the login + * @return string The username if login success. + */ + public static function http_auth($realm, $users, $fail_msg=NULL, $fail_url=NULL){ + $realm = "Restricted area - $realm"; + + //user => password + //$users = array('admin' => '1234', 'guest' => 'guest'); + if(!empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) && strpos($_SERVER['REDIRECT_HTTP_AUTHORIZATION'], 'Digest')===0){ + $_SERVER['PHP_AUTH_DIGEST'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; + } + + if (empty($_SERVER['PHP_AUTH_DIGEST'])) { + header('WWW-Authenticate: Digest realm="'.$realm. + '",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"'); + header('HTTP/1.1 401 Unauthorized'); + if($fail_msg!=NULL) + die($fail_msg); + if($fail_url!=NULL) + die(""); + exit; + } + + // analyze the PHP_AUTH_DIGEST variable + if (!($data = self::http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) || !isset($users[$data['username']])){ + header('WWW-Authenticate: Digest realm="'.$realm. + '",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"'); + header('HTTP/1.1 401 Unauthorized'); + if($fail_msg!=NULL) + die($fail_msg); + if($fail_url!=NULL) + die(""); + exit; + } + + // generate the valid response + $A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]); + $A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']); + $valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2); + + if ($data['response'] != $valid_response){ + header('HTTP/1.1 401 Unauthorized'); + header('WWW-Authenticate: Digest realm="'.$realm. + '",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"'); + if($fail_msg!=NULL) + die($fail_msg); + if($fail_url!=NULL) + die(""); + exit; + } + + // ok, valid username & password + return $data['username']; + } + + /** + * Method to parse the http auth header, works with IE. + * + * Internet Explorer returns a qop="xxxxxxxxxxx" in the header instead of qop=xxxxxxxxxxx as most browsers do. + * + * @param string $txt header string to parse + * @return array An assoc array of the digest auth session + */ + private static function http_digest_parse($txt) + { + $res = preg_match("/username=\"([^\"]+)\"/i", $txt, $match); + $data['username'] = (isset($match[1]))?$match[1]:null; + $res = preg_match('/nonce=\"([^\"]+)\"/i', $txt, $match); + $data['nonce'] = $match[1]; + $res = preg_match('/nc=([0-9]+)/i', $txt, $match); + $data['nc'] = $match[1]; + $res = preg_match('/cnonce=\"([^\"]+)\"/i', $txt, $match); + $data['cnonce'] = $match[1]; + $res = preg_match('/qop=([^,]+)/i', $txt, $match); + $data['qop'] = str_replace('"','',$match[1]); + $res = preg_match('/uri=\"([^\"]+)\"/i', $txt, $match); + $data['uri'] = $match[1]; + $res = preg_match('/response=\"([^\"]+)\"/i', $txt, $match); + $data['response'] = $match[1]; + return $data; + } + + +} diff --git a/public/min/lib/FirePHP.php b/public/min/lib/FirePHP.php new file mode 100644 index 0000000..f26ab3b --- /dev/null +++ b/public/min/lib/FirePHP.php @@ -0,0 +1,1843 @@ +, Copyright 2007, New BSD License +// - qbbr, Sokolov Innokenty , Copyright 2011, New BSD License +// - cadorn, Christoph Dorn , Copyright 2011, MIT License + +/** + * *** BEGIN LICENSE BLOCK ***** + * + * [MIT License](http://www.opensource.org/licenses/mit-license.php) + * + * Copyright (c) 2007+ [Christoph Dorn](http://www.christophdorn.com/) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * ***** END LICENSE BLOCK ***** + * + * @copyright Copyright (C) 2007+ Christoph Dorn + * @author Christoph Dorn + * @license [MIT License](http://www.opensource.org/licenses/mit-license.php) + * @package FirePHPCore + */ + +/** + * @see http://code.google.com/p/firephp/issues/detail?id=112 + */ +if (!defined('E_STRICT')) { + define('E_STRICT', 2048); +} +if (!defined('E_RECOVERABLE_ERROR')) { + define('E_RECOVERABLE_ERROR', 4096); +} +if (!defined('E_DEPRECATED')) { + define('E_DEPRECATED', 8192); +} +if (!defined('E_USER_DEPRECATED')) { + define('E_USER_DEPRECATED', 16384); +} + +/** + * Sends the given data to the FirePHP Firefox Extension. + * The data can be displayed in the Firebug Console or in the + * "Server" request tab. + * + * For more information see: http://www.firephp.org/ + * + * @copyright Copyright (C) 2007+ Christoph Dorn + * @author Christoph Dorn + * @license [MIT License](http://www.opensource.org/licenses/mit-license.php) + * @package FirePHPCore + * + * @deprecated 2.3 This will be removed in Minify 3.0 + */ +class FirePHP { + + /** + * FirePHP version + * + * @var string + */ + const VERSION = '0.3'; // @pinf replace '0.3' with '%%VERSION%%' + + /** + * Firebug LOG level + * + * Logs a message to firebug console. + * + * @var string + */ + const LOG = 'LOG'; + + /** + * Firebug INFO level + * + * Logs a message to firebug console and displays an info icon before the message. + * + * @var string + */ + const INFO = 'INFO'; + + /** + * Firebug WARN level + * + * Logs a message to firebug console, displays an warning icon before the message and colors the line turquoise. + * + * @var string + */ + const WARN = 'WARN'; + + /** + * Firebug ERROR level + * + * Logs a message to firebug console, displays an error icon before the message and colors the line yellow. Also increments the firebug error count. + * + * @var string + */ + const ERROR = 'ERROR'; + + /** + * Dumps a variable to firebug's server panel + * + * @var string + */ + const DUMP = 'DUMP'; + + /** + * Displays a stack trace in firebug console + * + * @var string + */ + const TRACE = 'TRACE'; + + /** + * Displays an exception in firebug console + * + * Increments the firebug error count. + * + * @var string + */ + const EXCEPTION = 'EXCEPTION'; + + /** + * Displays an table in firebug console + * + * @var string + */ + const TABLE = 'TABLE'; + + /** + * Starts a group in firebug console + * + * @var string + */ + const GROUP_START = 'GROUP_START'; + + /** + * Ends a group in firebug console + * + * @var string + */ + const GROUP_END = 'GROUP_END'; + + /** + * Singleton instance of FirePHP + * + * @var FirePHP + */ + protected static $instance = null; + + /** + * Flag whether we are logging from within the exception handler + * + * @var boolean + */ + protected $inExceptionHandler = false; + + /** + * Flag whether to throw PHP errors that have been converted to ErrorExceptions + * + * @var boolean + */ + protected $throwErrorExceptions = true; + + /** + * Flag whether to convert PHP assertion errors to Exceptions + * + * @var boolean + */ + protected $convertAssertionErrorsToExceptions = true; + + /** + * Flag whether to throw PHP assertion errors that have been converted to Exceptions + * + * @var boolean + */ + protected $throwAssertionExceptions = false; + + /** + * Wildfire protocol message index + * + * @var integer + */ + protected $messageIndex = 1; + + /** + * Options for the library + * + * @var array + */ + protected $options = array('maxDepth' => 10, + 'maxObjectDepth' => 5, + 'maxArrayDepth' => 5, + 'useNativeJsonEncode' => true, + 'includeLineNumbers' => true); + + /** + * Filters used to exclude object members when encoding + * + * @var array + */ + protected $objectFilters = array( + 'firephp' => array('objectStack', 'instance', 'json_objectStack'), + 'firephp_test_class' => array('objectStack', 'instance', 'json_objectStack') + ); + + /** + * A stack of objects used to detect recursion during object encoding + * + * @var object + */ + protected $objectStack = array(); + + /** + * Flag to enable/disable logging + * + * @var boolean + */ + protected $enabled = true; + + /** + * The insight console to log to if applicable + * + * @var object + */ + protected $logToInsightConsole = null; + + /** + * When the object gets serialized only include specific object members. + * + * @return array + */ + public function __sleep() + { + return array('options', 'objectFilters', 'enabled'); + } + + /** + * Gets singleton instance of FirePHP + * + * @param boolean $autoCreate + * @return FirePHP + */ + public static function getInstance($autoCreate = false) + { + if ($autoCreate === true && !self::$instance) { + self::init(); + } + return self::$instance; + } + + /** + * Creates FirePHP object and stores it for singleton access + * + * @return FirePHP + */ + public static function init() + { + return self::setInstance(new self()); + } + + /** + * Set the instance of the FirePHP singleton + * + * @param FirePHP $instance The FirePHP object instance + * @return FirePHP + */ + public static function setInstance($instance) + { + return self::$instance = $instance; + } + + /** + * Set an Insight console to direct all logging calls to + * + * @param object $console The console object to log to + * @return void + */ + public function setLogToInsightConsole($console) + { + if (is_string($console)) { + if (get_class($this) != 'FirePHP_Insight' && !is_subclass_of($this, 'FirePHP_Insight')) { + throw new Exception('FirePHP instance not an instance or subclass of FirePHP_Insight!'); + } + $this->logToInsightConsole = $this->to('request')->console($console); + } else { + $this->logToInsightConsole = $console; + } + } + + /** + * Enable and disable logging to Firebug + * + * @param boolean $enabled TRUE to enable, FALSE to disable + * @return void + */ + public function setEnabled($enabled) + { + $this->enabled = $enabled; + } + + /** + * Check if logging is enabled + * + * @return boolean TRUE if enabled + */ + public function getEnabled() + { + return $this->enabled; + } + + /** + * Specify a filter to be used when encoding an object + * + * Filters are used to exclude object members. + * + * @param string $class The class name of the object + * @param array $filter An array of members to exclude + * @return void + */ + public function setObjectFilter($class, $filter) + { + $this->objectFilters[strtolower($class)] = $filter; + } + + /** + * Set some options for the library + * + * Options: + * - maxDepth: The maximum depth to traverse (default: 10) + * - maxObjectDepth: The maximum depth to traverse objects (default: 5) + * - maxArrayDepth: The maximum depth to traverse arrays (default: 5) + * - useNativeJsonEncode: If true will use json_encode() (default: true) + * - includeLineNumbers: If true will include line numbers and filenames (default: true) + * + * @param array $options The options to be set + * @return void + */ + public function setOptions($options) + { + $this->options = array_merge($this->options, $options); + } + + /** + * Get options from the library + * + * @return array The currently set options + */ + public function getOptions() + { + return $this->options; + } + + /** + * Set an option for the library + * + * @param string $name + * @param mixed $value + * @return void + * @throws Exception + */ + public function setOption($name, $value) + { + if (!isset($this->options[$name])) { + throw $this->newException('Unknown option: ' . $name); + } + $this->options[$name] = $value; + } + + /** + * Get an option from the library + * + * @param string $name + * @return mixed + * @throws Exception + */ + public function getOption($name) + { + if (!isset($this->options[$name])) { + throw $this->newException('Unknown option: ' . $name); + } + return $this->options[$name]; + } + + /** + * Register FirePHP as your error handler + * + * Will throw exceptions for each php error. + * + * @return mixed Returns a string containing the previously defined error handler (if any) + */ + public function registerErrorHandler($throwErrorExceptions = false) + { + //NOTE: The following errors will not be caught by this error handler: + // E_ERROR, E_PARSE, E_CORE_ERROR, + // E_CORE_WARNING, E_COMPILE_ERROR, + // E_COMPILE_WARNING, E_STRICT + + $this->throwErrorExceptions = $throwErrorExceptions; + + return set_error_handler(array($this, 'errorHandler')); + } + + /** + * FirePHP's error handler + * + * Throws exception for each php error that will occur. + * + * @param integer $errno + * @param string $errstr + * @param string $errfile + * @param integer $errline + * @param array $errcontext + */ + public function errorHandler($errno, $errstr, $errfile, $errline, $errcontext) + { + // Don't throw exception if error reporting is switched off + if (error_reporting() == 0) { + return; + } + // Only throw exceptions for errors we are asking for + if (error_reporting() & $errno) { + + $exception = new ErrorException($errstr, 0, $errno, $errfile, $errline); + if ($this->throwErrorExceptions) { + throw $exception; + } else { + $this->fb($exception); + } + } + } + + /** + * Register FirePHP as your exception handler + * + * @return mixed Returns the name of the previously defined exception handler, + * or NULL on error. + * If no previous handler was defined, NULL is also returned. + */ + public function registerExceptionHandler() + { + return set_exception_handler(array($this, 'exceptionHandler')); + } + + /** + * FirePHP's exception handler + * + * Logs all exceptions to your firebug console and then stops the script. + * + * @param Exception $exception + * @throws Exception + */ + function exceptionHandler($exception) + { + $this->inExceptionHandler = true; + + header('HTTP/1.1 500 Internal Server Error'); + + try { + $this->fb($exception); + } catch (Exception $e) { + echo 'We had an exception: ' . $e; + } + + $this->inExceptionHandler = false; + } + + /** + * Register FirePHP driver as your assert callback + * + * @param boolean $convertAssertionErrorsToExceptions + * @param boolean $throwAssertionExceptions + * @return mixed Returns the original setting or FALSE on errors + */ + public function registerAssertionHandler($convertAssertionErrorsToExceptions = true, $throwAssertionExceptions = false) + { + $this->convertAssertionErrorsToExceptions = $convertAssertionErrorsToExceptions; + $this->throwAssertionExceptions = $throwAssertionExceptions; + + if ($throwAssertionExceptions && !$convertAssertionErrorsToExceptions) { + throw $this->newException('Cannot throw assertion exceptions as assertion errors are not being converted to exceptions!'); + } + + return assert_options(ASSERT_CALLBACK, array($this, 'assertionHandler')); + } + + /** + * FirePHP's assertion handler + * + * Logs all assertions to your firebug console and then stops the script. + * + * @param string $file File source of assertion + * @param integer $line Line source of assertion + * @param mixed $code Assertion code + */ + public function assertionHandler($file, $line, $code) + { + if ($this->convertAssertionErrorsToExceptions) { + + $exception = new ErrorException('Assertion Failed - Code[ ' . $code . ' ]', 0, null, $file, $line); + + if ($this->throwAssertionExceptions) { + throw $exception; + } else { + $this->fb($exception); + } + + } else { + $this->fb($code, 'Assertion Failed', FirePHP::ERROR, array('File' => $file, 'Line' => $line)); + } + } + + /** + * Start a group for following messages. + * + * Options: + * Collapsed: [true|false] + * Color: [#RRGGBB|ColorName] + * + * @param string $name + * @param array $options OPTIONAL Instructions on how to log the group + * @return true + * @throws Exception + */ + public function group($name, $options = null) + { + + if ( !isset($name) ) { + throw $this->newException('You must specify a label for the group!'); + } + + if ($options) { + if (!is_array($options)) { + throw $this->newException('Options must be defined as an array!'); + } + if (array_key_exists('Collapsed', $options)) { + $options['Collapsed'] = ($options['Collapsed']) ? 'true' : 'false'; + } + } + + return $this->fb(null, $name, FirePHP::GROUP_START, $options); + } + + /** + * Ends a group you have started before + * + * @return true + * @throws Exception + */ + public function groupEnd() + { + return $this->fb(null, null, FirePHP::GROUP_END); + } + + /** + * Log object with label to firebug console + * + * @see FirePHP::LOG + * @param mixes $object + * @param string $label + * @return true + * @throws Exception + */ + public function log($object, $label = null, $options = array()) + { + return $this->fb($object, $label, FirePHP::LOG, $options); + } + + /** + * Log object with label to firebug console + * + * @see FirePHP::INFO + * @param mixes $object + * @param string $label + * @return true + * @throws Exception + */ + public function info($object, $label = null, $options = array()) + { + return $this->fb($object, $label, FirePHP::INFO, $options); + } + + /** + * Log object with label to firebug console + * + * @see FirePHP::WARN + * @param mixes $object + * @param string $label + * @return true + * @throws Exception + */ + public function warn($object, $label = null, $options = array()) + { + return $this->fb($object, $label, FirePHP::WARN, $options); + } + + /** + * Log object with label to firebug console + * + * @see FirePHP::ERROR + * @param mixes $object + * @param string $label + * @return true + * @throws Exception + */ + public function error($object, $label = null, $options = array()) + { + return $this->fb($object, $label, FirePHP::ERROR, $options); + } + + /** + * Dumps key and variable to firebug server panel + * + * @see FirePHP::DUMP + * @param string $key + * @param mixed $variable + * @return true + * @throws Exception + */ + public function dump($key, $variable, $options = array()) + { + if (!is_string($key)) { + throw $this->newException('Key passed to dump() is not a string'); + } + if (strlen($key) > 100) { + throw $this->newException('Key passed to dump() is longer than 100 characters'); + } + if (!preg_match_all('/^[a-zA-Z0-9-_\.:]*$/', $key, $m)) { + throw $this->newException('Key passed to dump() contains invalid characters [a-zA-Z0-9-_\.:]'); + } + return $this->fb($variable, $key, FirePHP::DUMP, $options); + } + + /** + * Log a trace in the firebug console + * + * @see FirePHP::TRACE + * @param string $label + * @return true + * @throws Exception + */ + public function trace($label) + { + return $this->fb($label, FirePHP::TRACE); + } + + /** + * Log a table in the firebug console + * + * @see FirePHP::TABLE + * @param string $label + * @param string $table + * @return true + * @throws Exception + */ + public function table($label, $table, $options = array()) + { + return $this->fb($table, $label, FirePHP::TABLE, $options); + } + + /** + * Insight API wrapper + * + * @see Insight_Helper::to() + */ + public static function to() + { + $instance = self::getInstance(); + if (!method_exists($instance, '_to')) { + throw new Exception('FirePHP::to() implementation not loaded'); + } + $args = func_get_args(); + return call_user_func_array(array($instance, '_to'), $args); + } + + /** + * Insight API wrapper + * + * @see Insight_Helper::plugin() + */ + public static function plugin() + { + $instance = self::getInstance(); + if (!method_exists($instance, '_plugin')) { + throw new Exception('FirePHP::plugin() implementation not loaded'); + } + $args = func_get_args(); + return call_user_func_array(array($instance, '_plugin'), $args); + } + + /** + * Check if FirePHP is installed on client + * + * @return boolean + */ + public function detectClientExtension() + { + // Check if FirePHP is installed on client via User-Agent header + if (@preg_match_all('/\sFirePHP\/([\.\d]*)\s?/si', $this->getUserAgent(), $m) && + version_compare($m[1][0], '0.0.6', '>=')) { + return true; + } else + // Check if FirePHP is installed on client via X-FirePHP-Version header + if (@preg_match_all('/^([\.\d]*)$/si', $this->getRequestHeader('X-FirePHP-Version'), $m) && + version_compare($m[1][0], '0.0.6', '>=')) { + return true; + } + return false; + } + + /** + * Log varible to Firebug + * + * @see http://www.firephp.org/Wiki/Reference/Fb + * @param mixed $object The variable to be logged + * @return boolean Return TRUE if message was added to headers, FALSE otherwise + * @throws Exception + */ + public function fb($object) + { + if ($this instanceof FirePHP_Insight && method_exists($this, '_logUpgradeClientMessage')) { + if (!FirePHP_Insight::$upgradeClientMessageLogged) { // avoid infinite recursion as _logUpgradeClientMessage() logs a message + $this->_logUpgradeClientMessage(); + } + } + + static $insightGroupStack = array(); + + if (!$this->getEnabled()) { + return false; + } + + if ($this->headersSent($filename, $linenum)) { + // If we are logging from within the exception handler we cannot throw another exception + if ($this->inExceptionHandler) { + // Simply echo the error out to the page + echo '
    FirePHP ERROR: Headers already sent in ' . $filename . ' on line ' . $linenum . '. Cannot send log data to FirePHP. You must have Output Buffering enabled via ob_start() or output_buffering ini directive.
    '; + } else { + throw $this->newException('Headers already sent in ' . $filename . ' on line ' . $linenum . '. Cannot send log data to FirePHP. You must have Output Buffering enabled via ob_start() or output_buffering ini directive.'); + } + } + + $type = null; + $label = null; + $options = array(); + + if (func_num_args() == 1) { + } else if (func_num_args() == 2) { + switch (func_get_arg(1)) { + case self::LOG: + case self::INFO: + case self::WARN: + case self::ERROR: + case self::DUMP: + case self::TRACE: + case self::EXCEPTION: + case self::TABLE: + case self::GROUP_START: + case self::GROUP_END: + $type = func_get_arg(1); + break; + default: + $label = func_get_arg(1); + break; + } + } else if (func_num_args() == 3) { + $type = func_get_arg(2); + $label = func_get_arg(1); + } else if (func_num_args() == 4) { + $type = func_get_arg(2); + $label = func_get_arg(1); + $options = func_get_arg(3); + } else { + throw $this->newException('Wrong number of arguments to fb() function!'); + } + + // Get folder name where firephp is located. + $parentFolder = basename(dirname(__FILE__)); + $parentFolderLength = strlen( $parentFolder ); + $fbLength = 7 + $parentFolderLength; + $fireClassLength = 18 + $parentFolderLength; + + if ($this->logToInsightConsole !== null && (get_class($this) == 'FirePHP_Insight' || is_subclass_of($this, 'FirePHP_Insight'))) { + $trace = debug_backtrace(); + if (!$trace) return false; + for ($i = 0; $i < sizeof($trace); $i++) { + if (isset($trace[$i]['class'])) { + if ($trace[$i]['class'] == 'FirePHP' || $trace[$i]['class'] == 'FB') { + continue; + } + } + if (isset($trace[$i]['file'])) { + $path = $this->_standardizePath($trace[$i]['file']); + if (substr($path, -1*$fbLength, $fbLength) == $parentFolder.'/fb.php' || substr($path, -1*$fireClassLength, $fireClassLength) == $parentFolder.'/FirePHP.class.php') { + continue; + } + } + if (isset($trace[$i]['function']) && $trace[$i]['function'] == 'fb' && + isset($trace[$i - 1]['file']) && substr($this->_standardizePath($trace[$i - 1]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php') { + continue; + } + if (isset($trace[$i]['class']) && $trace[$i]['class'] == 'FB' && + isset($trace[$i - 1]['file']) && substr($this->_standardizePath($trace[$i - 1]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php') { + continue; + } + break; + } + // adjust trace offset + $msg = $this->logToInsightConsole->option('encoder.trace.offsetAdjustment', $i); + + if ($object instanceof Exception) { + $type = self::EXCEPTION; + } + if ($label && $type != self::TABLE && $type != self::GROUP_START) { + $msg = $msg->label($label); + } + switch ($type) { + case self::DUMP: + case self::LOG: + return $msg->log($object); + case self::INFO: + return $msg->info($object); + case self::WARN: + return $msg->warn($object); + case self::ERROR: + return $msg->error($object); + case self::TRACE: + return $msg->trace($object); + case self::EXCEPTION: + return $this->plugin('error')->handleException($object, $msg); + case self::TABLE: + if (isset($object[0]) && !is_string($object[0]) && $label) { + $object = array($label, $object); + } + return $msg->table($object[0], array_slice($object[1], 1), $object[1][0]); + case self::GROUP_START: + $insightGroupStack[] = $msg->group(md5($label))->open(); + return $msg->log($label); + case self::GROUP_END: + if (count($insightGroupStack) == 0) { + throw new Error('Too many groupEnd() as opposed to group() calls!'); + } + $group = array_pop($insightGroupStack); + return $group->close(); + default: + return $msg->log($object); + } + } + + if (!$this->detectClientExtension()) { + return false; + } + + $meta = array(); + $skipFinalObjectEncode = false; + + if ($object instanceof Exception) { + + $meta['file'] = $this->_escapeTraceFile($object->getFile()); + $meta['line'] = $object->getLine(); + + $trace = $object->getTrace(); + if ($object instanceof ErrorException + && isset($trace[0]['function']) + && $trace[0]['function'] == 'errorHandler' + && isset($trace[0]['class']) + && $trace[0]['class'] == 'FirePHP') { + + $severity = false; + switch ($object->getSeverity()) { + case E_WARNING: + $severity = 'E_WARNING'; + break; + + case E_NOTICE: + $severity = 'E_NOTICE'; + break; + + case E_USER_ERROR: + $severity = 'E_USER_ERROR'; + break; + + case E_USER_WARNING: + $severity = 'E_USER_WARNING'; + break; + + case E_USER_NOTICE: + $severity = 'E_USER_NOTICE'; + break; + + case E_STRICT: + $severity = 'E_STRICT'; + break; + + case E_RECOVERABLE_ERROR: + $severity = 'E_RECOVERABLE_ERROR'; + break; + + case E_DEPRECATED: + $severity = 'E_DEPRECATED'; + break; + + case E_USER_DEPRECATED: + $severity = 'E_USER_DEPRECATED'; + break; + } + + $object = array('Class' => get_class($object), + 'Message' => $severity . ': ' . $object->getMessage(), + 'File' => $this->_escapeTraceFile($object->getFile()), + 'Line' => $object->getLine(), + 'Type' => 'trigger', + 'Trace' => $this->_escapeTrace(array_splice($trace, 2))); + $skipFinalObjectEncode = true; + } else { + $object = array('Class' => get_class($object), + 'Message' => $object->getMessage(), + 'File' => $this->_escapeTraceFile($object->getFile()), + 'Line' => $object->getLine(), + 'Type' => 'throw', + 'Trace' => $this->_escapeTrace($trace)); + $skipFinalObjectEncode = true; + } + $type = self::EXCEPTION; + + } else if ($type == self::TRACE) { + + $trace = debug_backtrace(); + if (!$trace) return false; + for ($i = 0; $i < sizeof($trace); $i++) { + + if (isset($trace[$i]['class']) + && isset($trace[$i]['file']) + && ($trace[$i]['class'] == 'FirePHP' + || $trace[$i]['class'] == 'FB') + && (substr($this->_standardizePath($trace[$i]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php' + || substr($this->_standardizePath($trace[$i]['file']), -1*$fireClassLength, $fireClassLength) == $parentFolder.'/FirePHP.class.php')) { + /* Skip - FB::trace(), FB::send(), $firephp->trace(), $firephp->fb() */ + } else + if (isset($trace[$i]['class']) + && isset($trace[$i+1]['file']) + && $trace[$i]['class'] == 'FirePHP' + && substr($this->_standardizePath($trace[$i + 1]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php') { + /* Skip fb() */ + } else + if ($trace[$i]['function'] == 'fb' + || $trace[$i]['function'] == 'trace' + || $trace[$i]['function'] == 'send') { + + $object = array('Class' => isset($trace[$i]['class']) ? $trace[$i]['class'] : '', + 'Type' => isset($trace[$i]['type']) ? $trace[$i]['type'] : '', + 'Function' => isset($trace[$i]['function']) ? $trace[$i]['function'] : '', + 'Message' => $trace[$i]['args'][0], + 'File' => isset($trace[$i]['file']) ? $this->_escapeTraceFile($trace[$i]['file']) : '', + 'Line' => isset($trace[$i]['line']) ? $trace[$i]['line'] : '', + 'Args' => isset($trace[$i]['args']) ? $this->encodeObject($trace[$i]['args']) : '', + 'Trace' => $this->_escapeTrace(array_splice($trace, $i + 1))); + + $skipFinalObjectEncode = true; + $meta['file'] = isset($trace[$i]['file']) ? $this->_escapeTraceFile($trace[$i]['file']) : ''; + $meta['line'] = isset($trace[$i]['line']) ? $trace[$i]['line'] : ''; + break; + } + } + + } else + if ($type == self::TABLE) { + + if (isset($object[0]) && is_string($object[0])) { + $object[1] = $this->encodeTable($object[1]); + } else { + $object = $this->encodeTable($object); + } + + $skipFinalObjectEncode = true; + + } else if ($type == self::GROUP_START) { + + if (!$label) { + throw $this->newException('You must specify a label for the group!'); + } + + } else { + if ($type === null) { + $type = self::LOG; + } + } + + if ($this->options['includeLineNumbers']) { + if (!isset($meta['file']) || !isset($meta['line'])) { + + $trace = debug_backtrace(); + for ($i = 0; $trace && $i < sizeof($trace); $i++) { + + if (isset($trace[$i]['class']) + && isset($trace[$i]['file']) + && ($trace[$i]['class'] == 'FirePHP' + || $trace[$i]['class'] == 'FB') + && (substr($this->_standardizePath($trace[$i]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php' + || substr($this->_standardizePath($trace[$i]['file']), -1*$fireClassLength, $fireClassLength) == $parentFolder.'/FirePHP.class.php')) { + /* Skip - FB::trace(), FB::send(), $firephp->trace(), $firephp->fb() */ + } else + if (isset($trace[$i]['class']) + && isset($trace[$i + 1]['file']) + && $trace[$i]['class'] == 'FirePHP' + && substr($this->_standardizePath($trace[$i + 1]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php') { + /* Skip fb() */ + } else + if (isset($trace[$i]['file']) + && substr($this->_standardizePath($trace[$i]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php') { + /* Skip FB::fb() */ + } else { + $meta['file'] = isset($trace[$i]['file']) ? $this->_escapeTraceFile($trace[$i]['file']) : ''; + $meta['line'] = isset($trace[$i]['line']) ? $trace[$i]['line'] : ''; + break; + } + } + } + } else { + unset($meta['file']); + unset($meta['line']); + } + + $this->setHeader('X-Wf-Protocol-1', 'http://meta.wildfirehq.org/Protocol/JsonStream/0.2'); + $this->setHeader('X-Wf-1-Plugin-1', 'http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/' . self::VERSION); + + $structureIndex = 1; + if ($type == self::DUMP) { + $structureIndex = 2; + $this->setHeader('X-Wf-1-Structure-2', 'http://meta.firephp.org/Wildfire/Structure/FirePHP/Dump/0.1'); + } else { + $this->setHeader('X-Wf-1-Structure-1', 'http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1'); + } + + if ($type == self::DUMP) { + $msg = '{"' . $label . '":' . $this->jsonEncode($object, $skipFinalObjectEncode) . '}'; + } else { + $msgMeta = $options; + $msgMeta['Type'] = $type; + if ($label !== null) { + $msgMeta['Label'] = $label; + } + if (isset($meta['file']) && !isset($msgMeta['File'])) { + $msgMeta['File'] = $meta['file']; + } + if (isset($meta['line']) && !isset($msgMeta['Line'])) { + $msgMeta['Line'] = $meta['line']; + } + $msg = '[' . $this->jsonEncode($msgMeta) . ',' . $this->jsonEncode($object, $skipFinalObjectEncode) . ']'; + } + + $parts = explode("\n", chunk_split($msg, 5000, "\n")); + + for ($i = 0; $i < count($parts); $i++) { + + $part = $parts[$i]; + if ($part) { + + if (count($parts) > 2) { + // Message needs to be split into multiple parts + $this->setHeader('X-Wf-1-' . $structureIndex . '-' . '1-' . $this->messageIndex, + (($i == 0) ? strlen($msg) : '') + . '|' . $part . '|' + . (($i < count($parts) - 2) ? '\\' : '')); + } else { + $this->setHeader('X-Wf-1-' . $structureIndex . '-' . '1-' . $this->messageIndex, + strlen($part) . '|' . $part . '|'); + } + + $this->messageIndex++; + + if ($this->messageIndex > 99999) { + throw $this->newException('Maximum number (99,999) of messages reached!'); + } + } + } + + $this->setHeader('X-Wf-1-Index', $this->messageIndex - 1); + + return true; + } + + /** + * Standardizes path for windows systems. + * + * @param string $path + * @return string + */ + protected function _standardizePath($path) + { + return preg_replace('/\\\\+/', '/', $path); + } + + /** + * Escape trace path for windows systems + * + * @param array $trace + * @return array + */ + protected function _escapeTrace($trace) + { + if (!$trace) return $trace; + for ($i = 0; $i < sizeof($trace); $i++) { + if (isset($trace[$i]['file'])) { + $trace[$i]['file'] = $this->_escapeTraceFile($trace[$i]['file']); + } + if (isset($trace[$i]['args'])) { + $trace[$i]['args'] = $this->encodeObject($trace[$i]['args']); + } + } + return $trace; + } + + /** + * Escape file information of trace for windows systems + * + * @param string $file + * @return string + */ + protected function _escapeTraceFile($file) + { + /* Check if we have a windows filepath */ + if (strpos($file, '\\')) { + /* First strip down to single \ */ + + $file = preg_replace('/\\\\+/', '\\', $file); + + return $file; + } + return $file; + } + + /** + * Check if headers have already been sent + * + * @param string $filename + * @param integer $linenum + */ + protected function headersSent(&$filename, &$linenum) + { + return headers_sent($filename, $linenum); + } + + /** + * Send header + * + * @param string $name + * @param string $value + */ + protected function setHeader($name, $value) + { + return header($name . ': ' . $value); + } + + /** + * Get user agent + * + * @return string|false + */ + protected function getUserAgent() + { + if (!isset($_SERVER['HTTP_USER_AGENT'])) return false; + return $_SERVER['HTTP_USER_AGENT']; + } + + /** + * Get all request headers + * + * @return array + */ + public static function getAllRequestHeaders() + { + static $_cachedHeaders = false; + if ($_cachedHeaders !== false) { + return $_cachedHeaders; + } + $headers = array(); + if (function_exists('getallheaders')) { + foreach (getallheaders() as $name => $value) { + $headers[strtolower($name)] = $value; + } + } else { + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[strtolower(str_replace(' ', '-', str_replace('_', ' ', substr($name, 5))))] = $value; + } + } + } + return $_cachedHeaders = $headers; + } + + /** + * Get a request header + * + * @return string|false + */ + protected function getRequestHeader($name) + { + $headers = self::getAllRequestHeaders(); + if (isset($headers[strtolower($name)])) { + return $headers[strtolower($name)]; + } + return false; + } + + /** + * Returns a new exception + * + * @param string $message + * @return Exception + */ + protected function newException($message) + { + return new Exception($message); + } + + /** + * Encode an object into a JSON string + * + * Uses PHP's jeson_encode() if available + * + * @param object $object The object to be encoded + * @param boolean $skipObjectEncode + * @return string The JSON string + */ + public function jsonEncode($object, $skipObjectEncode = false) + { + if (!$skipObjectEncode) { + $object = $this->encodeObject($object); + } + + if (function_exists('json_encode') + && $this->options['useNativeJsonEncode'] != false) { + + return json_encode($object); + } else { + return $this->json_encode($object); + } + } + + /** + * Encodes a table by encoding each row and column with encodeObject() + * + * @param array $table The table to be encoded + * @return array + */ + protected function encodeTable($table) + { + if (!$table) return $table; + + $newTable = array(); + foreach ($table as $row) { + + if (is_array($row)) { + $newRow = array(); + + foreach ($row as $item) { + $newRow[] = $this->encodeObject($item); + } + + $newTable[] = $newRow; + } + } + + return $newTable; + } + + /** + * Encodes an object including members with + * protected and private visibility + * + * @param object $object The object to be encoded + * @param integer $Depth The current traversal depth + * @return array All members of the object + */ + protected function encodeObject($object, $objectDepth = 1, $arrayDepth = 1, $maxDepth = 1) + { + if ($maxDepth > $this->options['maxDepth']) { + return '** Max Depth (' . $this->options['maxDepth'] . ') **'; + } + + $return = array(); + + //#2801 is_resource reports false for closed resources https://bugs.php.net/bug.php?id=28016 + if (is_resource($object) || gettype($object) === "unknown type") { + + return '** ' . (string) $object . ' **'; + + } else if (is_object($object)) { + + if ($objectDepth > $this->options['maxObjectDepth']) { + return '** Max Object Depth (' . $this->options['maxObjectDepth'] . ') **'; + } + + foreach ($this->objectStack as $refVal) { + if ($refVal === $object) { + return '** Recursion (' . get_class($object) . ') **'; + } + } + array_push($this->objectStack, $object); + + $return['__className'] = $class = get_class($object); + $classLower = strtolower($class); + + $reflectionClass = new ReflectionClass($class); + $properties = array(); + foreach ($reflectionClass->getProperties() as $property) { + $properties[$property->getName()] = $property; + } + + $members = (array)$object; + + foreach ($properties as $plainName => $property) { + + $name = $rawName = $plainName; + if ($property->isStatic()) { + $name = 'static:' . $name; + } + if ($property->isPublic()) { + $name = 'public:' . $name; + } else if ($property->isPrivate()) { + $name = 'private:' . $name; + $rawName = "\0" . $class . "\0" . $rawName; + } else if ($property->isProtected()) { + $name = 'protected:' . $name; + $rawName = "\0" . '*' . "\0" . $rawName; + } + + if (!(isset($this->objectFilters[$classLower]) + && is_array($this->objectFilters[$classLower]) + && in_array($plainName, $this->objectFilters[$classLower]))) { + + if (array_key_exists($rawName, $members) && !$property->isStatic()) { + $return[$name] = $this->encodeObject($members[$rawName], $objectDepth + 1, 1, $maxDepth + 1); + } else { + if (method_exists($property, 'setAccessible')) { + $property->setAccessible(true); + $return[$name] = $this->encodeObject($property->getValue($object), $objectDepth + 1, 1, $maxDepth + 1); + } else + if ($property->isPublic()) { + $return[$name] = $this->encodeObject($property->getValue($object), $objectDepth + 1, 1, $maxDepth + 1); + } else { + $return[$name] = '** Need PHP 5.3 to get value **'; + } + } + } else { + $return[$name] = '** Excluded by Filter **'; + } + } + + // Include all members that are not defined in the class + // but exist in the object + foreach ($members as $rawName => $value) { + + $name = $rawName; + + if ($name{0} == "\0") { + $parts = explode("\0", $name); + $name = $parts[2]; + } + + $plainName = $name; + + if (!isset($properties[$name])) { + $name = 'undeclared:' . $name; + + if (!(isset($this->objectFilters[$classLower]) + && is_array($this->objectFilters[$classLower]) + && in_array($plainName, $this->objectFilters[$classLower]))) { + + $return[$name] = $this->encodeObject($value, $objectDepth + 1, 1, $maxDepth + 1); + } else { + $return[$name] = '** Excluded by Filter **'; + } + } + } + + array_pop($this->objectStack); + + } elseif (is_array($object)) { + + if ($arrayDepth > $this->options['maxArrayDepth']) { + return '** Max Array Depth (' . $this->options['maxArrayDepth'] . ') **'; + } + + foreach ($object as $key => $val) { + + // Encoding the $GLOBALS PHP array causes an infinite loop + // if the recursion is not reset here as it contains + // a reference to itself. This is the only way I have come up + // with to stop infinite recursion in this case. + if ($key == 'GLOBALS' + && is_array($val) + && array_key_exists('GLOBALS', $val)) { + $val['GLOBALS'] = '** Recursion (GLOBALS) **'; + } + + if (!$this->is_utf8($key)) { + $key = utf8_encode($key); + } + + $return[$key] = $this->encodeObject($val, 1, $arrayDepth + 1, $maxDepth + 1); + } + } elseif ( is_bool($object) ) { + return $object; + } elseif ( is_null($object) ) { + return $object; + } elseif ( is_numeric($object) ) { + return $object; + } else { + if ($this->is_utf8($object)) { + return $object; + } else { + return utf8_encode($object); + } + } + return $return; + } + + /** + * Returns true if $string is valid UTF-8 and false otherwise. + * + * @param mixed $str String to be tested + * @return boolean + */ + protected function is_utf8($str) + { + if (function_exists('mb_detect_encoding')) { + return ( + mb_detect_encoding($str, 'UTF-8', true) == 'UTF-8' && + ($str === null || $this->jsonEncode($str, true) !== 'null') + ); + } + $c = 0; + $b = 0; + $bits = 0; + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + $c = ord($str[$i]); + if ($c > 128) { + if (($c >= 254)) return false; + elseif ($c >= 252) $bits = 6; + elseif ($c >= 248) $bits = 5; + elseif ($c >= 240) $bits = 4; + elseif ($c >= 224) $bits = 3; + elseif ($c >= 192) $bits = 2; + else return false; + if (($i + $bits) > $len) return false; + while($bits > 1) { + $i++; + $b = ord($str[$i]); + if ($b < 128 || $b > 191) return false; + $bits--; + } + } + } + return ($str === null || $this->jsonEncode($str, true) !== 'null'); + } + + /** + * Converts to and from JSON format. + * + * JSON (JavaScript Object Notation) is a lightweight data-interchange + * format. It is easy for humans to read and write. It is easy for machines + * to parse and generate. It is based on a subset of the JavaScript + * Programming Language, Standard ECMA-262 3rd Edition - December 1999. + * This feature can also be found in Python. JSON is a text format that is + * completely language independent but uses conventions that are familiar + * to programmers of the C-family of languages, including C, C++, C#, Java, + * JavaScript, Perl, TCL, and many others. These properties make JSON an + * ideal data-interchange language. + * + * This package provides a simple encoder and decoder for JSON notation. It + * is intended for use with client-side Javascript applications that make + * use of HTTPRequest to perform server communication functions - data can + * be encoded into JSON notation for use in a client-side javascript, or + * decoded from incoming Javascript requests. JSON format is native to + * Javascript, and can be directly eval()'ed with no further parsing + * overhead + * + * All strings should be in ASCII or UTF-8 format! + * + * LICENSE: Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: Redistributions of source code must retain the + * above copyright notice, this list of conditions and the following + * disclaimer. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH + * DAMAGE. + * + * @category + * @package Services_JSON + * @author Michal Migurski + * @author Matt Knapp + * @author Brett Stimmerman + * @author Christoph Dorn + * @copyright 2005 Michal Migurski + * @version CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $ + * @license http://www.opensource.org/licenses/bsd-license.php + * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198 + */ + + + /** + * Keep a list of objects as we descend into the array so we can detect recursion. + */ + private $json_objectStack = array(); + + + /** + * convert a string from one UTF-8 char to one UTF-16 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf8 UTF-8 character + * @return string UTF-16 character + * @access private + */ + private function json_utf82utf16($utf8) + { + // oh please oh please oh please oh please oh please + if (function_exists('mb_convert_encoding')) { + return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8'); + } + + switch (strlen($utf8)) { + case 1: + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return $utf8; + + case 2: + // return a UTF-16 character from a 2-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x07 & (ord($utf8{0}) >> 2)) + . chr((0xC0 & (ord($utf8{0}) << 6)) + | (0x3F & ord($utf8{1}))); + + case 3: + // return a UTF-16 character from a 3-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr((0xF0 & (ord($utf8{0}) << 4)) + | (0x0F & (ord($utf8{1}) >> 2))) + . chr((0xC0 & (ord($utf8{1}) << 6)) + | (0x7F & ord($utf8{2}))); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * encodes an arbitrary variable into JSON format + * + * @param mixed $var any number, boolean, string, array, or object to be encoded. + * see argument 1 to Services_JSON() above for array-parsing behavior. + * if var is a strng, note that encode() always expects it + * to be in ASCII or UTF-8 format! + * + * @return mixed JSON string representation of input var or an error if a problem occurs + * @access public + */ + private function json_encode($var) + { + if (is_object($var)) { + if (in_array($var, $this->json_objectStack)) { + return '"** Recursion **"'; + } + } + + switch (gettype($var)) { + case 'boolean': + return $var ? 'true' : 'false'; + + case 'NULL': + return 'null'; + + case 'integer': + return (int) $var; + + case 'double': + case 'float': + return (float) $var; + + case 'string': + // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT + $ascii = ''; + $strlen_var = strlen($var); + + /* + * Iterate over every character in the string, + * escaping with a slash or encoding to UTF-8 where necessary + */ + for ($c = 0; $c < $strlen_var; ++$c) { + + $ord_var_c = ord($var{$c}); + + switch (true) { + case $ord_var_c == 0x08: + $ascii .= '\b'; + break; + case $ord_var_c == 0x09: + $ascii .= '\t'; + break; + case $ord_var_c == 0x0A: + $ascii .= '\n'; + break; + case $ord_var_c == 0x0C: + $ascii .= '\f'; + break; + case $ord_var_c == 0x0D: + $ascii .= '\r'; + break; + + case $ord_var_c == 0x22: + case $ord_var_c == 0x2F: + case $ord_var_c == 0x5C: + // double quote, slash, slosh + $ascii .= '\\' . $var{$c}; + break; + + case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)): + // characters U-00000000 - U-0000007F (same as ASCII) + $ascii .= $var{$c}; + break; + + case (($ord_var_c & 0xE0) == 0xC0): + // characters U-00000080 - U-000007FF, mask 110XXXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, ord($var{$c + 1})); + $c += 1; + $utf16 = $this->json_utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF0) == 0xE0): + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2})); + $c += 2; + $utf16 = $this->json_utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF8) == 0xF0): + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3})); + $c += 3; + $utf16 = $this->json_utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFC) == 0xF8): + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4})); + $c += 4; + $utf16 = $this->json_utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFE) == 0xFC): + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4}), + ord($var{$c + 5})); + $c += 5; + $utf16 = $this->json_utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + } + } + + return '"' . $ascii . '"'; + + case 'array': + /* + * As per JSON spec if any array key is not an integer + * we must treat the the whole array as an object. We + * also try to catch a sparsely populated associative + * array with numeric keys here because some JS engines + * will create an array with empty indexes up to + * max_index which can cause memory issues and because + * the keys, which may be relevant, will be remapped + * otherwise. + * + * As per the ECMA and JSON specification an object may + * have any string as a property. Unfortunately due to + * a hole in the ECMA specification if the key is a + * ECMA reserved word or starts with a digit the + * parameter is only accessible using ECMAScript's + * bracket notation. + */ + + // treat as a JSON object + if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) { + + $this->json_objectStack[] = $var; + + $properties = array_map(array($this, 'json_name_value'), + array_keys($var), + array_values($var)); + + array_pop($this->json_objectStack); + + foreach ($properties as $property) { + if ($property instanceof Exception) { + return $property; + } + } + + return '{' . join(',', $properties) . '}'; + } + + $this->json_objectStack[] = $var; + + // treat it like a regular array + $elements = array_map(array($this, 'json_encode'), $var); + + array_pop($this->json_objectStack); + + foreach ($elements as $element) { + if ($element instanceof Exception) { + return $element; + } + } + + return '[' . join(',', $elements) . ']'; + + case 'object': + $vars = self::encodeObject($var); + + $this->json_objectStack[] = $var; + + $properties = array_map(array($this, 'json_name_value'), + array_keys($vars), + array_values($vars)); + + array_pop($this->json_objectStack); + + foreach ($properties as $property) { + if ($property instanceof Exception) { + return $property; + } + } + + return '{' . join(',', $properties) . '}'; + + default: + return null; + } + } + + /** + * array-walking function for use in generating JSON-formatted name-value pairs + * + * @param string $name name of key to use + * @param mixed $value reference to an array element to be encoded + * + * @return string JSON-formatted name-value pair, like '"name":value' + * @access private + */ + private function json_name_value($name, $value) + { + // Encoding the $GLOBALS PHP array causes an infinite loop + // if the recursion is not reset here as it contains + // a reference to itself. This is the only way I have come up + // with to stop infinite recursion in this case. + if ($name == 'GLOBALS' + && is_array($value) + && array_key_exists('GLOBALS', $value)) { + $value['GLOBALS'] = '** Recursion **'; + } + + $encodedValue = $this->json_encode($value); + + if ($encodedValue instanceof Exception) { + return $encodedValue; + } + + return $this->json_encode(strval($name)) . ':' . $encodedValue; + } + + /** + * @deprecated + */ + public function setProcessorUrl($URL) + { + trigger_error('The FirePHP::setProcessorUrl() method is no longer supported', E_USER_DEPRECATED); + } + + /** + * @deprecated + */ + public function setRendererUrl($URL) + { + trigger_error('The FirePHP::setRendererUrl() method is no longer supported', E_USER_DEPRECATED); + } +} \ No newline at end of file diff --git a/public/min/lib/HTTP/ConditionalGet.php b/public/min/lib/HTTP/ConditionalGet.php new file mode 100644 index 0000000..ec6e6d6 --- /dev/null +++ b/public/min/lib/HTTP/ConditionalGet.php @@ -0,0 +1,366 @@ + + * list($updateTime, $content) = getDbUpdateAndContent(); + * $cg = new HTTP_ConditionalGet(array( + * 'lastModifiedTime' => $updateTime + * ,'isPublic' => true + * )); + * $cg->sendHeaders(); + * if ($cg->cacheIsValid) { + * exit(); + * } + * echo $content; + * + * + * E.g. Shortcut for the above + * + * HTTP_ConditionalGet::check($updateTime, true); // exits if client has cache + * echo $content; + * + * + * E.g. Content from DB with no update time: + * + * $content = getContentFromDB(); + * $cg = new HTTP_ConditionalGet(array( + * 'contentHash' => md5($content) + * )); + * $cg->sendHeaders(); + * if ($cg->cacheIsValid) { + * exit(); + * } + * echo $content; + * + * + * E.g. Static content with some static includes: + * + * // before content + * $cg = new HTTP_ConditionalGet(array( + * 'lastUpdateTime' => max( + * filemtime(__FILE__) + * ,filemtime('/path/to/header.inc') + * ,filemtime('/path/to/footer.inc') + * ) + * )); + * $cg->sendHeaders(); + * if ($cg->cacheIsValid) { + * exit(); + * } + * + * @package Minify + * @subpackage HTTP + * @author Stephen Clay + */ +class HTTP_ConditionalGet { + + /** + * Does the client have a valid copy of the requested resource? + * + * You'll want to check this after instantiating the object. If true, do + * not send content, just call sendHeaders() if you haven't already. + * + * @var bool + */ + public $cacheIsValid = null; + + /** + * @param array $spec options + * + * 'isPublic': (bool) if false, the Cache-Control header will contain + * "private", allowing only browser caching. (default false) + * + * 'lastModifiedTime': (int) if given, both ETag AND Last-Modified headers + * will be sent with content. This is recommended. + * + * 'encoding': (string) if set, the header "Vary: Accept-Encoding" will + * always be sent and a truncated version of the encoding will be appended + * to the ETag. E.g. "pub123456;gz". This will also trigger a more lenient + * checking of the client's If-None-Match header, as the encoding portion of + * the ETag will be stripped before comparison. + * + * 'contentHash': (string) if given, only the ETag header can be sent with + * content (only HTTP1.1 clients can conditionally GET). The given string + * should be short with no quote characters and always change when the + * resource changes (recommend md5()). This is not needed/used if + * lastModifiedTime is given. + * + * 'eTag': (string) if given, this will be used as the ETag header rather + * than values based on lastModifiedTime or contentHash. Also the encoding + * string will not be appended to the given value as described above. + * + * 'invalidate': (bool) if true, the client cache will be considered invalid + * without testing. Effectively this disables conditional GET. + * (default false) + * + * 'maxAge': (int) if given, this will set the Cache-Control max-age in + * seconds, and also set the Expires header to the equivalent GMT date. + * After the max-age period has passed, the browser will again send a + * conditional GET to revalidate its cache. + */ + public function __construct($spec) + { + $scope = (isset($spec['isPublic']) && $spec['isPublic']) + ? 'public' + : 'private'; + $maxAge = 0; + // backwards compatibility (can be removed later) + if (isset($spec['setExpires']) + && is_numeric($spec['setExpires']) + && ! isset($spec['maxAge'])) { + $spec['maxAge'] = $spec['setExpires'] - $_SERVER['REQUEST_TIME']; + } + if (isset($spec['maxAge'])) { + $maxAge = $spec['maxAge']; + $this->_headers['Expires'] = self::gmtDate( + $_SERVER['REQUEST_TIME'] + $spec['maxAge'] + ); + } + $etagAppend = ''; + if (isset($spec['encoding'])) { + $this->_stripEtag = true; + $this->_headers['Vary'] = 'Accept-Encoding'; + if ('' !== $spec['encoding']) { + if (0 === strpos($spec['encoding'], 'x-')) { + $spec['encoding'] = substr($spec['encoding'], 2); + } + $etagAppend = ';' . substr($spec['encoding'], 0, 2); + } + } + if (isset($spec['lastModifiedTime'])) { + $this->_setLastModified($spec['lastModifiedTime']); + if (isset($spec['eTag'])) { // Use it + $this->_setEtag($spec['eTag'], $scope); + } else { // base both headers on time + $this->_setEtag($spec['lastModifiedTime'] . $etagAppend, $scope); + } + } elseif (isset($spec['eTag'])) { // Use it + $this->_setEtag($spec['eTag'], $scope); + } elseif (isset($spec['contentHash'])) { // Use the hash as the ETag + $this->_setEtag($spec['contentHash'] . $etagAppend, $scope); + } + $privacy = ($scope === 'private') + ? ', private' + : ''; + $this->_headers['Cache-Control'] = "max-age={$maxAge}{$privacy}"; + // invalidate cache if disabled, otherwise check + $this->cacheIsValid = (isset($spec['invalidate']) && $spec['invalidate']) + ? false + : $this->_isCacheValid(); + } + + /** + * Get array of output headers to be sent + * + * In the case of 304 responses, this array will only contain the response + * code header: array('_responseCode' => 'HTTP/1.0 304 Not Modified') + * + * Otherwise something like: + * + * array( + * 'Cache-Control' => 'max-age=0, public' + * ,'ETag' => '"foobar"' + * ) + * + * + * @return array + */ + public function getHeaders() + { + return $this->_headers; + } + + /** + * Set the Content-Length header in bytes + * + * With most PHP configs, as long as you don't flush() output, this method + * is not needed and PHP will buffer all output and set Content-Length for + * you. Otherwise you'll want to call this to let the client know up front. + * + * @param int $bytes + * + * @return int copy of input $bytes + */ + public function setContentLength($bytes) + { + return $this->_headers['Content-Length'] = $bytes; + } + + /** + * Send headers + * + * @see getHeaders() + * + * Note this doesn't "clear" the headers. Calling sendHeaders() will + * call header() again (but probably have not effect) and getHeaders() will + * still return the headers. + * + * @return null + */ + public function sendHeaders() + { + $headers = $this->_headers; + if (array_key_exists('_responseCode', $headers)) { + // FastCGI environments require 3rd arg to header() to be set + list(, $code) = explode(' ', $headers['_responseCode'], 3); + header($headers['_responseCode'], true, $code); + unset($headers['_responseCode']); + } + foreach ($headers as $name => $val) { + header($name . ': ' . $val); + } + } + + /** + * Exit if the client's cache is valid for this resource + * + * This is a convenience method for common use of the class + * + * @param int $lastModifiedTime if given, both ETag AND Last-Modified headers + * will be sent with content. This is recommended. + * + * @param bool $isPublic (default false) if true, the Cache-Control header + * will contain "public", allowing proxies to cache the content. Otherwise + * "private" will be sent, allowing only browser caching. + * + * @param array $options (default empty) additional options for constructor + */ + public static function check($lastModifiedTime = null, $isPublic = false, $options = array()) + { + if (null !== $lastModifiedTime) { + $options['lastModifiedTime'] = (int)$lastModifiedTime; + } + $options['isPublic'] = (bool)$isPublic; + $cg = new HTTP_ConditionalGet($options); + $cg->sendHeaders(); + if ($cg->cacheIsValid) { + exit(); + } + } + + + /** + * Get a GMT formatted date for use in HTTP headers + * + * + * header('Expires: ' . HTTP_ConditionalGet::gmtdate($time)); + * + * + * @param int $time unix timestamp + * + * @return string + */ + public static function gmtDate($time) + { + return gmdate('D, d M Y H:i:s \G\M\T', $time); + } + + protected $_headers = array(); + protected $_lmTime = null; + protected $_etag = null; + protected $_stripEtag = false; + + /** + * @param string $hash + * + * @param string $scope + */ + protected function _setEtag($hash, $scope) + { + $this->_etag = '"' . substr($scope, 0, 3) . $hash . '"'; + $this->_headers['ETag'] = $this->_etag; + } + + /** + * @param int $time + */ + protected function _setLastModified($time) + { + $this->_lmTime = (int)$time; + $this->_headers['Last-Modified'] = self::gmtDate($time); + } + + /** + * Determine validity of client cache and queue 304 header if valid + * + * @return bool + */ + protected function _isCacheValid() + { + if (null === $this->_etag) { + // lmTime is copied to ETag, so this condition implies that the + // server sent neither ETag nor Last-Modified, so the client can't + // possibly has a valid cache. + return false; + } + $isValid = ($this->resourceMatchedEtag() || $this->resourceNotModified()); + if ($isValid) { + $this->_headers['_responseCode'] = 'HTTP/1.0 304 Not Modified'; + } + return $isValid; + } + + /** + * @return bool + */ + protected function resourceMatchedEtag() + { + if (!isset($_SERVER['HTTP_IF_NONE_MATCH'])) { + return false; + } + $clientEtagList = get_magic_quotes_gpc() + ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) + : $_SERVER['HTTP_IF_NONE_MATCH']; + $clientEtags = explode(',', $clientEtagList); + + $compareTo = $this->normalizeEtag($this->_etag); + foreach ($clientEtags as $clientEtag) { + if ($this->normalizeEtag($clientEtag) === $compareTo) { + // respond with the client's matched ETag, even if it's not what + // we would've sent by default + $this->_headers['ETag'] = trim($clientEtag); + return true; + } + } + return false; + } + + /** + * @param string $etag + * + * @return string + */ + protected function normalizeEtag($etag) { + $etag = trim($etag); + return $this->_stripEtag + ? preg_replace('/;\\w\\w"$/', '"', $etag) + : $etag; + } + + /** + * @return bool + */ + protected function resourceNotModified() + { + if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { + return false; + } + // strip off IE's extra data (semicolon) + list($ifModifiedSince) = explode(';', $_SERVER['HTTP_IF_MODIFIED_SINCE'], 2); + if (strtotime($ifModifiedSince) >= $this->_lmTime) { + // Apache 2.2's behavior. If there was no ETag match, send the + // non-encoded version of the ETag value. + $this->_headers['ETag'] = $this->normalizeEtag($this->_etag); + return true; + } + return false; + } +} diff --git a/public/min/lib/HTTP/Encoder.php b/public/min/lib/HTTP/Encoder.php new file mode 100644 index 0000000..ed3b909 --- /dev/null +++ b/public/min/lib/HTTP/Encoder.php @@ -0,0 +1,335 @@ + + * // Send a CSS file, compressed if possible + * $he = new HTTP_Encoder(array( + * 'content' => file_get_contents($cssFile) + * ,'type' => 'text/css' + * )); + * $he->encode(); + * $he->sendAll(); + * + * + * + * // Shortcut to encoding output + * header('Content-Type: text/css'); // needed if not HTML + * HTTP_Encoder::output($css); + * + * + * + * // Just sniff for the accepted encoding + * $encoding = HTTP_Encoder::getAcceptedEncoding(); + * + * + * For more control over headers, use getHeaders() and getData() and send your + * own output. + * + * Note: If you don't need header mgmt, use PHP's native gzencode, gzdeflate, + * and gzcompress functions for gzip, deflate, and compress-encoding + * respectively. + * + * @package Minify + * @subpackage HTTP + * @author Stephen Clay + */ +class HTTP_Encoder { + + /** + * Should the encoder allow HTTP encoding to IE6? + * + * If you have many IE6 users and the bandwidth savings is worth troubling + * some of them, set this to true. + * + * By default, encoding is only offered to IE7+. When this is true, + * getAcceptedEncoding() will return an encoding for IE6 if its user agent + * string contains "SV1". This has been documented in many places as "safe", + * but there seem to be remaining, intermittent encoding bugs in patched + * IE6 on the wild web. + * + * @var bool + */ + public static $encodeToIe6 = true; + + + /** + * Default compression level for zlib operations + * + * This level is used if encode() is not given a $compressionLevel + * + * @var int + */ + public static $compressionLevel = 6; + + + /** + * Get an HTTP Encoder object + * + * @param array $spec options + * + * 'content': (string required) content to be encoded + * + * 'type': (string) if set, the Content-Type header will have this value. + * + * 'method: (string) only set this if you are forcing a particular encoding + * method. If not set, the best method will be chosen by getAcceptedEncoding() + * The available methods are 'gzip', 'deflate', 'compress', and '' (no + * encoding) + */ + public function __construct($spec) + { + $this->_useMbStrlen = (function_exists('mb_strlen') + && (ini_get('mbstring.func_overload') !== '') + && ((int)ini_get('mbstring.func_overload') & 2)); + $this->_content = $spec['content']; + $this->_headers['Content-Length'] = $this->_useMbStrlen + ? (string)mb_strlen($this->_content, '8bit') + : (string)strlen($this->_content); + if (isset($spec['type'])) { + $this->_headers['Content-Type'] = $spec['type']; + } + if (isset($spec['method']) + && in_array($spec['method'], array('gzip', 'deflate', 'compress', ''))) + { + $this->_encodeMethod = array($spec['method'], $spec['method']); + } else { + $this->_encodeMethod = self::getAcceptedEncoding(); + } + } + + /** + * Get content in current form + * + * Call after encode() for encoded content. + * + * @return string + */ + public function getContent() + { + return $this->_content; + } + + /** + * Get array of output headers to be sent + * + * E.g. + * + * array( + * 'Content-Length' => '615' + * ,'Content-Encoding' => 'x-gzip' + * ,'Vary' => 'Accept-Encoding' + * ) + * + * + * @return array + */ + public function getHeaders() + { + return $this->_headers; + } + + /** + * Send output headers + * + * You must call this before headers are sent and it probably cannot be + * used in conjunction with zlib output buffering / mod_gzip. Errors are + * not handled purposefully. + * + * @see getHeaders() + */ + public function sendHeaders() + { + foreach ($this->_headers as $name => $val) { + header($name . ': ' . $val); + } + } + + /** + * Send output headers and content + * + * A shortcut for sendHeaders() and echo getContent() + * + * You must call this before headers are sent and it probably cannot be + * used in conjunction with zlib output buffering / mod_gzip. Errors are + * not handled purposefully. + */ + public function sendAll() + { + $this->sendHeaders(); + echo $this->_content; + } + + /** + * Determine the client's best encoding method from the HTTP Accept-Encoding + * header. + * + * If no Accept-Encoding header is set, or the browser is IE before v6 SP2, + * this will return ('', ''), the "identity" encoding. + * + * A syntax-aware scan is done of the Accept-Encoding, so the method must + * be non 0. The methods are favored in order of gzip, deflate, then + * compress. Deflate is always smallest and generally faster, but is + * rarely sent by servers, so client support could be buggier. + * + * @param bool $allowCompress allow the older compress encoding + * + * @param bool $allowDeflate allow the more recent deflate encoding + * + * @return array two values, 1st is the actual encoding method, 2nd is the + * alias of that method to use in the Content-Encoding header (some browsers + * call gzip "x-gzip" etc.) + */ + public static function getAcceptedEncoding($allowCompress = true, $allowDeflate = true) + { + // @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + + if (! isset($_SERVER['HTTP_ACCEPT_ENCODING']) + || self::isBuggyIe()) + { + return array('', ''); + } + $ae = $_SERVER['HTTP_ACCEPT_ENCODING']; + // gzip checks (quick) + if (0 === strpos($ae, 'gzip,') // most browsers + || 0 === strpos($ae, 'deflate, gzip,') // opera + ) { + return array('gzip', 'gzip'); + } + // gzip checks (slow) + if (preg_match( + '@(?:^|,)\\s*((?:x-)?gzip)\\s*(?:$|,|;\\s*q=(?:0\\.|1))@' + ,$ae + ,$m)) { + return array('gzip', $m[1]); + } + if ($allowDeflate) { + // deflate checks + $aeRev = strrev($ae); + if (0 === strpos($aeRev, 'etalfed ,') // ie, webkit + || 0 === strpos($aeRev, 'etalfed,') // gecko + || 0 === strpos($ae, 'deflate,') // opera + // slow parsing + || preg_match( + '@(?:^|,)\\s*deflate\\s*(?:$|,|;\\s*q=(?:0\\.|1))@', $ae)) { + return array('deflate', 'deflate'); + } + } + if ($allowCompress && preg_match( + '@(?:^|,)\\s*((?:x-)?compress)\\s*(?:$|,|;\\s*q=(?:0\\.|1))@' + ,$ae + ,$m)) { + return array('compress', $m[1]); + } + return array('', ''); + } + + /** + * Encode (compress) the content + * + * If the encode method is '' (none) or compression level is 0, or the 'zlib' + * extension isn't loaded, we return false. + * + * Then the appropriate gz_* function is called to compress the content. If + * this fails, false is returned. + * + * The header "Vary: Accept-Encoding" is added. If encoding is successful, + * the Content-Length header is updated, and Content-Encoding is also added. + * + * @param int $compressionLevel given to zlib functions. If not given, the + * class default will be used. + * + * @return bool success true if the content was actually compressed + */ + public function encode($compressionLevel = null) + { + if (! self::isBuggyIe()) { + $this->_headers['Vary'] = 'Accept-Encoding'; + } + if (null === $compressionLevel) { + $compressionLevel = self::$compressionLevel; + } + if ('' === $this->_encodeMethod[0] + || ($compressionLevel == 0) + || !extension_loaded('zlib')) + { + return false; + } + if ($this->_encodeMethod[0] === 'deflate') { + $encoded = gzdeflate($this->_content, $compressionLevel); + } elseif ($this->_encodeMethod[0] === 'gzip') { + $encoded = gzencode($this->_content, $compressionLevel); + } else { + $encoded = gzcompress($this->_content, $compressionLevel); + } + if (false === $encoded) { + return false; + } + $this->_headers['Content-Length'] = $this->_useMbStrlen + ? (string)mb_strlen($encoded, '8bit') + : (string)strlen($encoded); + $this->_headers['Content-Encoding'] = $this->_encodeMethod[1]; + $this->_content = $encoded; + return true; + } + + /** + * Encode and send appropriate headers and content + * + * This is a convenience method for common use of the class + * + * @param string $content + * + * @param int $compressionLevel given to zlib functions. If not given, the + * class default will be used. + * + * @return bool success true if the content was actually compressed + */ + public static function output($content, $compressionLevel = null) + { + if (null === $compressionLevel) { + $compressionLevel = self::$compressionLevel; + } + $he = new HTTP_Encoder(array('content' => $content)); + $ret = $he->encode($compressionLevel); + $he->sendAll(); + return $ret; + } + + /** + * Is the browser an IE version earlier than 6 SP2? + * + * @return bool + */ + public static function isBuggyIe() + { + if (empty($_SERVER['HTTP_USER_AGENT'])) { + return false; + } + $ua = $_SERVER['HTTP_USER_AGENT']; + // quick escape for non-IEs + if (0 !== strpos($ua, 'Mozilla/4.0 (compatible; MSIE ') + || false !== strpos($ua, 'Opera')) { + return false; + } + // no regex = faaast + $version = (float)substr($ua, 30); + return self::$encodeToIe6 + ? ($version < 6 || ($version == 6 && false === strpos($ua, 'SV1'))) + : ($version < 7); + } + + protected $_content = ''; + protected $_headers = array(); + protected $_encodeMethod = array('', ''); + protected $_useMbStrlen = false; +} diff --git a/public/min/lib/JSMin.php b/public/min/lib/JSMin.php new file mode 100644 index 0000000..5094d9f --- /dev/null +++ b/public/min/lib/JSMin.php @@ -0,0 +1,449 @@ + + * $minifiedJs = JSMin::minify($js); + * + * + * This is a modified port of jsmin.c. Improvements: + * + * Does not choke on some regexp literals containing quote characters. E.g. /'/ + * + * Spaces are preserved after some add/sub operators, so they are not mistakenly + * converted to post-inc/dec. E.g. a + ++b -> a+ ++b + * + * Preserves multi-line comments that begin with /*! + * + * PHP 5 or higher is required. + * + * Permission is hereby granted to use this version of the library under the + * same terms as jsmin.c, which has the following license: + * + * -- + * Copyright (c) 2002 Douglas Crockford (www.crockford.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * The Software shall be used for Good, not Evil. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * -- + * + * @package JSMin + * @author Ryan Grove (PHP port) + * @author Steve Clay (modifications + cleanup) + * @author Andrea Giammarchi (spaceBeforeRegExp) + * @copyright 2002 Douglas Crockford (jsmin.c) + * @copyright 2008 Ryan Grove (PHP port) + * @license http://opensource.org/licenses/mit-license.php MIT License + * @link http://code.google.com/p/jsmin-php/ + */ + +class JSMin { + const ORD_LF = 10; + const ORD_SPACE = 32; + const ACTION_KEEP_A = 1; + const ACTION_DELETE_A = 2; + const ACTION_DELETE_A_B = 3; + + protected $a = "\n"; + protected $b = ''; + protected $input = ''; + protected $inputIndex = 0; + protected $inputLength = 0; + protected $lookAhead = null; + protected $output = ''; + protected $lastByteOut = ''; + protected $keptComment = ''; + + /** + * Minify Javascript. + * + * @param string $js Javascript to be minified + * + * @return string + */ + public static function minify($js) + { + $jsmin = new JSMin($js); + return $jsmin->min(); + } + + /** + * @param string $input + */ + public function __construct($input) + { + $this->input = $input; + } + + /** + * Perform minification, return result + * + * @return string + */ + public function min() + { + if ($this->output !== '') { // min already run + return $this->output; + } + + $mbIntEnc = null; + if (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) { + $mbIntEnc = mb_internal_encoding(); + mb_internal_encoding('8bit'); + } + $this->input = str_replace("\r\n", "\n", $this->input); + $this->inputLength = strlen($this->input); + + $this->action(self::ACTION_DELETE_A_B); + + while ($this->a !== null) { + // determine next command + $command = self::ACTION_KEEP_A; // default + if ($this->a === ' ') { + if (($this->lastByteOut === '+' || $this->lastByteOut === '-') + && ($this->b === $this->lastByteOut)) { + // Don't delete this space. If we do, the addition/subtraction + // could be parsed as a post-increment + } elseif (! $this->isAlphaNum($this->b)) { + $command = self::ACTION_DELETE_A; + } + } elseif ($this->a === "\n") { + if ($this->b === ' ') { + $command = self::ACTION_DELETE_A_B; + + // in case of mbstring.func_overload & 2, must check for null b, + // otherwise mb_strpos will give WARNING + } elseif ($this->b === null + || (false === strpos('{[(+-!~', $this->b) + && ! $this->isAlphaNum($this->b))) { + $command = self::ACTION_DELETE_A; + } + } elseif (! $this->isAlphaNum($this->a)) { + if ($this->b === ' ' + || ($this->b === "\n" + && (false === strpos('}])+-"\'', $this->a)))) { + $command = self::ACTION_DELETE_A_B; + } + } + $this->action($command); + } + $this->output = trim($this->output); + + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } + return $this->output; + } + + /** + * ACTION_KEEP_A = Output A. Copy B to A. Get the next B. + * ACTION_DELETE_A = Copy B to A. Get the next B. + * ACTION_DELETE_A_B = Get the next B. + * + * @param int $command + * @throws JSMin_UnterminatedRegExpException|JSMin_UnterminatedStringException + */ + protected function action($command) + { + // make sure we don't compress "a + ++b" to "a+++b", etc. + if ($command === self::ACTION_DELETE_A_B + && $this->b === ' ' + && ($this->a === '+' || $this->a === '-')) { + // Note: we're at an addition/substraction operator; the inputIndex + // will certainly be a valid index + if ($this->input[$this->inputIndex] === $this->a) { + // This is "+ +" or "- -". Don't delete the space. + $command = self::ACTION_KEEP_A; + } + } + + switch ($command) { + case self::ACTION_KEEP_A: // 1 + $this->output .= $this->a; + + if ($this->keptComment) { + $this->output = rtrim($this->output, "\n"); + $this->output .= $this->keptComment; + $this->keptComment = ''; + } + + $this->lastByteOut = $this->a; + + // fallthrough intentional + case self::ACTION_DELETE_A: // 2 + $this->a = $this->b; + if ($this->a === "'" || $this->a === '"') { // string literal + $str = $this->a; // in case needed for exception + for(;;) { + $this->output .= $this->a; + $this->lastByteOut = $this->a; + + $this->a = $this->get(); + if ($this->a === $this->b) { // end quote + break; + } + if ($this->isEOF($this->a)) { + $byte = $this->inputIndex - 1; + throw new JSMin_UnterminatedStringException( + "JSMin: Unterminated String at byte {$byte}: {$str}"); + } + $str .= $this->a; + if ($this->a === '\\') { + $this->output .= $this->a; + $this->lastByteOut = $this->a; + + $this->a = $this->get(); + $str .= $this->a; + } + } + } + + // fallthrough intentional + case self::ACTION_DELETE_A_B: // 3 + $this->b = $this->next(); + if ($this->b === '/' && $this->isRegexpLiteral()) { + $this->output .= $this->a . $this->b; + $pattern = '/'; // keep entire pattern in case we need to report it in the exception + for(;;) { + $this->a = $this->get(); + $pattern .= $this->a; + if ($this->a === '[') { + for(;;) { + $this->output .= $this->a; + $this->a = $this->get(); + $pattern .= $this->a; + if ($this->a === ']') { + break; + } + if ($this->a === '\\') { + $this->output .= $this->a; + $this->a = $this->get(); + $pattern .= $this->a; + } + if ($this->isEOF($this->a)) { + throw new JSMin_UnterminatedRegExpException( + "JSMin: Unterminated set in RegExp at byte " + . $this->inputIndex .": {$pattern}"); + } + } + } + + if ($this->a === '/') { // end pattern + break; // while (true) + } elseif ($this->a === '\\') { + $this->output .= $this->a; + $this->a = $this->get(); + $pattern .= $this->a; + } elseif ($this->isEOF($this->a)) { + $byte = $this->inputIndex - 1; + throw new JSMin_UnterminatedRegExpException( + "JSMin: Unterminated RegExp at byte {$byte}: {$pattern}"); + } + $this->output .= $this->a; + $this->lastByteOut = $this->a; + } + $this->b = $this->next(); + } + // end case ACTION_DELETE_A_B + } + } + + /** + * @return bool + */ + protected function isRegexpLiteral() + { + if (false !== strpos("(,=:[!&|?+-~*{;", $this->a)) { + // we obviously aren't dividing + return true; + } + + // we have to check for a preceding keyword, and we don't need to pattern + // match over the whole output. + $recentOutput = substr($this->output, -10); + + // check if return/typeof directly precede a pattern without a space + foreach (array('return', 'typeof') as $keyword) { + if ($this->a !== substr($keyword, -1)) { + // certainly wasn't keyword + continue; + } + if (preg_match("~(^|[\\s\\S])" . substr($keyword, 0, -1) . "$~", $recentOutput, $m)) { + if ($m[1] === '' || !$this->isAlphaNum($m[1])) { + return true; + } + } + } + + // check all keywords + if ($this->a === ' ' || $this->a === "\n") { + if (preg_match('~(^|[\\s\\S])(?:case|else|in|return|typeof)$~', $recentOutput, $m)) { + if ($m[1] === '' || !$this->isAlphaNum($m[1])) { + return true; + } + } + } + + return false; + } + + /** + * Return the next character from stdin. Watch out for lookahead. If the character is a control character, + * translate it to a space or linefeed. + * + * @return string + */ + protected function get() + { + $c = $this->lookAhead; + $this->lookAhead = null; + if ($c === null) { + // getc(stdin) + if ($this->inputIndex < $this->inputLength) { + $c = $this->input[$this->inputIndex]; + $this->inputIndex += 1; + } else { + $c = null; + } + } + if (ord($c) >= self::ORD_SPACE || $c === "\n" || $c === null) { + return $c; + } + if ($c === "\r") { + return "\n"; + } + return ' '; + } + + /** + * Does $a indicate end of input? + * + * @param string $a + * @return bool + */ + protected function isEOF($a) + { + return ord($a) <= self::ORD_LF; + } + + /** + * Get next char (without getting it). If is ctrl character, translate to a space or newline. + * + * @return string + */ + protected function peek() + { + $this->lookAhead = $this->get(); + return $this->lookAhead; + } + + /** + * Return true if the character is a letter, digit, underscore, dollar sign, or non-ASCII character. + * + * @param string $c + * + * @return bool + */ + protected function isAlphaNum($c) + { + return (preg_match('/^[a-z0-9A-Z_\\$\\\\]$/', $c) || ord($c) > 126); + } + + /** + * Consume a single line comment from input (possibly retaining it) + */ + protected function consumeSingleLineComment() + { + $comment = ''; + while (true) { + $get = $this->get(); + $comment .= $get; + if (ord($get) <= self::ORD_LF) { // end of line reached + // if IE conditional comment + if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) { + $this->keptComment .= "/{$comment}"; + } + return; + } + } + } + + /** + * Consume a multiple line comment from input (possibly retaining it) + * + * @throws JSMin_UnterminatedCommentException + */ + protected function consumeMultipleLineComment() + { + $this->get(); + $comment = ''; + for(;;) { + $get = $this->get(); + if ($get === '*') { + if ($this->peek() === '/') { // end of comment reached + $this->get(); + if (0 === strpos($comment, '!')) { + // preserved by YUI Compressor + if (!$this->keptComment) { + // don't prepend a newline if two comments right after one another + $this->keptComment = "\n"; + } + $this->keptComment .= "/*!" . substr($comment, 1) . "*/\n"; + } else if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) { + // IE conditional + $this->keptComment .= "/*{$comment}*/"; + } + return; + } + } elseif ($get === null) { + throw new JSMin_UnterminatedCommentException( + "JSMin: Unterminated comment at byte {$this->inputIndex}: /*{$comment}"); + } + $comment .= $get; + } + } + + /** + * Get the next character, skipping over comments. Some comments may be preserved. + * + * @return string + */ + protected function next() + { + $get = $this->get(); + if ($get === '/') { + switch ($this->peek()) { + case '/': + $this->consumeSingleLineComment(); + $get = "\n"; + break; + case '*': + $this->consumeMultipleLineComment(); + $get = ' '; + break; + } + } + return $get; + } +} + +class JSMin_UnterminatedStringException extends Exception {} +class JSMin_UnterminatedCommentException extends Exception {} +class JSMin_UnterminatedRegExpException extends Exception {} diff --git a/public/min/lib/JSMinPlus.php b/public/min/lib/JSMinPlus.php new file mode 100644 index 0000000..d0fce26 --- /dev/null +++ b/public/min/lib/JSMinPlus.php @@ -0,0 +1,2090 @@ + + * + * Usage: $minified = JSMinPlus::minify($script [, $filename]) + * + * Versionlog (see also changelog.txt): + * 23-07-2011 - remove dynamic creation of OP_* and KEYWORD_* defines and declare them on top + * reduce memory footprint by minifying by block-scope + * some small byte-saving and performance improvements + * 12-05-2009 - fixed hook:colon precedence, fixed empty body in loop and if-constructs + * 18-04-2009 - fixed crashbug in PHP 5.2.9 and several other bugfixes + * 12-04-2009 - some small bugfixes and performance improvements + * 09-04-2009 - initial open sourced version 1.0 + * + * Latest version of this script: http://files.tweakers.net/jsminplus/jsminplus.zip + * + */ + +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is the Narcissus JavaScript engine. + * + * The Initial Developer of the Original Code is + * Brendan Eich . + * Portions created by the Initial Developer are Copyright (C) 2004 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): Tino Zijdel + * PHP port, modifications and minifier routine are (C) 2009-2011 + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +define('TOKEN_END', 1); +define('TOKEN_NUMBER', 2); +define('TOKEN_IDENTIFIER', 3); +define('TOKEN_STRING', 4); +define('TOKEN_REGEXP', 5); +define('TOKEN_NEWLINE', 6); +define('TOKEN_CONDCOMMENT_START', 7); +define('TOKEN_CONDCOMMENT_END', 8); + +define('JS_SCRIPT', 100); +define('JS_BLOCK', 101); +define('JS_LABEL', 102); +define('JS_FOR_IN', 103); +define('JS_CALL', 104); +define('JS_NEW_WITH_ARGS', 105); +define('JS_INDEX', 106); +define('JS_ARRAY_INIT', 107); +define('JS_OBJECT_INIT', 108); +define('JS_PROPERTY_INIT', 109); +define('JS_GETTER', 110); +define('JS_SETTER', 111); +define('JS_GROUP', 112); +define('JS_LIST', 113); + +define('JS_MINIFIED', 999); + +define('DECLARED_FORM', 0); +define('EXPRESSED_FORM', 1); +define('STATEMENT_FORM', 2); + +/* Operators */ +define('OP_SEMICOLON', ';'); +define('OP_COMMA', ','); +define('OP_HOOK', '?'); +define('OP_COLON', ':'); +define('OP_OR', '||'); +define('OP_AND', '&&'); +define('OP_BITWISE_OR', '|'); +define('OP_BITWISE_XOR', '^'); +define('OP_BITWISE_AND', '&'); +define('OP_STRICT_EQ', '==='); +define('OP_EQ', '=='); +define('OP_ASSIGN', '='); +define('OP_STRICT_NE', '!=='); +define('OP_NE', '!='); +define('OP_LSH', '<<'); +define('OP_LE', '<='); +define('OP_LT', '<'); +define('OP_URSH', '>>>'); +define('OP_RSH', '>>'); +define('OP_GE', '>='); +define('OP_GT', '>'); +define('OP_INCREMENT', '++'); +define('OP_DECREMENT', '--'); +define('OP_PLUS', '+'); +define('OP_MINUS', '-'); +define('OP_MUL', '*'); +define('OP_DIV', '/'); +define('OP_MOD', '%'); +define('OP_NOT', '!'); +define('OP_BITWISE_NOT', '~'); +define('OP_DOT', '.'); +define('OP_LEFT_BRACKET', '['); +define('OP_RIGHT_BRACKET', ']'); +define('OP_LEFT_CURLY', '{'); +define('OP_RIGHT_CURLY', '}'); +define('OP_LEFT_PAREN', '('); +define('OP_RIGHT_PAREN', ')'); +define('OP_CONDCOMMENT_END', '@*/'); + +define('OP_UNARY_PLUS', 'U+'); +define('OP_UNARY_MINUS', 'U-'); + +/* Keywords */ +define('KEYWORD_BREAK', 'break'); +define('KEYWORD_CASE', 'case'); +define('KEYWORD_CATCH', 'catch'); +define('KEYWORD_CONST', 'const'); +define('KEYWORD_CONTINUE', 'continue'); +define('KEYWORD_DEBUGGER', 'debugger'); +define('KEYWORD_DEFAULT', 'default'); +define('KEYWORD_DELETE', 'delete'); +define('KEYWORD_DO', 'do'); +define('KEYWORD_ELSE', 'else'); +define('KEYWORD_ENUM', 'enum'); +define('KEYWORD_FALSE', 'false'); +define('KEYWORD_FINALLY', 'finally'); +define('KEYWORD_FOR', 'for'); +define('KEYWORD_FUNCTION', 'function'); +define('KEYWORD_IF', 'if'); +define('KEYWORD_IN', 'in'); +define('KEYWORD_INSTANCEOF', 'instanceof'); +define('KEYWORD_NEW', 'new'); +define('KEYWORD_NULL', 'null'); +define('KEYWORD_RETURN', 'return'); +define('KEYWORD_SWITCH', 'switch'); +define('KEYWORD_THIS', 'this'); +define('KEYWORD_THROW', 'throw'); +define('KEYWORD_TRUE', 'true'); +define('KEYWORD_TRY', 'try'); +define('KEYWORD_TYPEOF', 'typeof'); +define('KEYWORD_VAR', 'var'); +define('KEYWORD_VOID', 'void'); +define('KEYWORD_WHILE', 'while'); +define('KEYWORD_WITH', 'with'); + +/** + * @deprecated 2.3 This will be removed in Minify 3.0 + */ +class JSMinPlus +{ + private $parser; + private $reserved = array( + 'break', 'case', 'catch', 'continue', 'default', 'delete', 'do', + 'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof', + 'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var', + 'void', 'while', 'with', + // Words reserved for future use + 'abstract', 'boolean', 'byte', 'char', 'class', 'const', 'debugger', + 'double', 'enum', 'export', 'extends', 'final', 'float', 'goto', + 'implements', 'import', 'int', 'interface', 'long', 'native', + 'package', 'private', 'protected', 'public', 'short', 'static', + 'super', 'synchronized', 'throws', 'transient', 'volatile', + // These are not reserved, but should be taken into account + // in isValidIdentifier (See jslint source code) + 'arguments', 'eval', 'true', 'false', 'Infinity', 'NaN', 'null', 'undefined' + ); + + private function __construct() + { + $this->parser = new JSParser($this); + } + + public static function minify($js, $filename='') + { + trigger_error(__CLASS__ . ' is deprecated. This will be removed in Minify 3.0', E_USER_DEPRECATED); + + static $instance; + + // this is a singleton + if(!$instance) + $instance = new JSMinPlus(); + + return $instance->min($js, $filename); + } + + private function min($js, $filename) + { + try + { + $n = $this->parser->parse($js, $filename, 1); + return $this->parseTree($n); + } + catch(Exception $e) + { + echo $e->getMessage() . "\n"; + } + + return false; + } + + public function parseTree($n, $noBlockGrouping = false) + { + $s = ''; + + switch ($n->type) + { + case JS_MINIFIED: + $s = $n->value; + break; + + case JS_SCRIPT: + // we do nothing yet with funDecls or varDecls + $noBlockGrouping = true; + // FALL THROUGH + + case JS_BLOCK: + $childs = $n->treeNodes; + $lastType = 0; + for ($c = 0, $i = 0, $j = count($childs); $i < $j; $i++) + { + $type = $childs[$i]->type; + $t = $this->parseTree($childs[$i]); + if (strlen($t)) + { + if ($c) + { + $s = rtrim($s, ';'); + + if ($type == KEYWORD_FUNCTION && $childs[$i]->functionForm == DECLARED_FORM) + { + // put declared functions on a new line + $s .= "\n"; + } + elseif ($type == KEYWORD_VAR && $type == $lastType) + { + // mutiple var-statements can go into one + $t = ',' . substr($t, 4); + } + else + { + // add terminator + $s .= ';'; + } + } + + $s .= $t; + + $c++; + $lastType = $type; + } + } + + if ($c > 1 && !$noBlockGrouping) + { + $s = '{' . $s . '}'; + } + break; + + case KEYWORD_FUNCTION: + $s .= 'function' . ($n->name ? ' ' . $n->name : '') . '('; + $params = $n->params; + for ($i = 0, $j = count($params); $i < $j; $i++) + $s .= ($i ? ',' : '') . $params[$i]; + $s .= '){' . $this->parseTree($n->body, true) . '}'; + break; + + case KEYWORD_IF: + $s = 'if(' . $this->parseTree($n->condition) . ')'; + $thenPart = $this->parseTree($n->thenPart); + $elsePart = $n->elsePart ? $this->parseTree($n->elsePart) : null; + + // empty if-statement + if ($thenPart == '') + $thenPart = ';'; + + if ($elsePart) + { + // be carefull and always make a block out of the thenPart; could be more optimized but is a lot of trouble + if ($thenPart != ';' && $thenPart[0] != '{') + $thenPart = '{' . $thenPart . '}'; + + $s .= $thenPart . 'else'; + + // we could check for more, but that hardly ever applies so go for performance + if ($elsePart[0] != '{') + $s .= ' '; + + $s .= $elsePart; + } + else + { + $s .= $thenPart; + } + break; + + case KEYWORD_SWITCH: + $s = 'switch(' . $this->parseTree($n->discriminant) . '){'; + $cases = $n->cases; + for ($i = 0, $j = count($cases); $i < $j; $i++) + { + $case = $cases[$i]; + if ($case->type == KEYWORD_CASE) + $s .= 'case' . ($case->caseLabel->type != TOKEN_STRING ? ' ' : '') . $this->parseTree($case->caseLabel) . ':'; + else + $s .= 'default:'; + + $statement = $this->parseTree($case->statements, true); + if ($statement) + { + $s .= $statement; + // no terminator for last statement + if ($i + 1 < $j) + $s .= ';'; + } + } + $s .= '}'; + break; + + case KEYWORD_FOR: + $s = 'for(' . ($n->setup ? $this->parseTree($n->setup) : '') + . ';' . ($n->condition ? $this->parseTree($n->condition) : '') + . ';' . ($n->update ? $this->parseTree($n->update) : '') . ')'; + + $body = $this->parseTree($n->body); + if ($body == '') + $body = ';'; + + $s .= $body; + break; + + case KEYWORD_WHILE: + $s = 'while(' . $this->parseTree($n->condition) . ')'; + + $body = $this->parseTree($n->body); + if ($body == '') + $body = ';'; + + $s .= $body; + break; + + case JS_FOR_IN: + $s = 'for(' . ($n->varDecl ? $this->parseTree($n->varDecl) : $this->parseTree($n->iterator)) . ' in ' . $this->parseTree($n->object) . ')'; + + $body = $this->parseTree($n->body); + if ($body == '') + $body = ';'; + + $s .= $body; + break; + + case KEYWORD_DO: + $s = 'do{' . $this->parseTree($n->body, true) . '}while(' . $this->parseTree($n->condition) . ')'; + break; + + case KEYWORD_BREAK: + case KEYWORD_CONTINUE: + $s = $n->value . ($n->label ? ' ' . $n->label : ''); + break; + + case KEYWORD_TRY: + $s = 'try{' . $this->parseTree($n->tryBlock, true) . '}'; + $catchClauses = $n->catchClauses; + for ($i = 0, $j = count($catchClauses); $i < $j; $i++) + { + $t = $catchClauses[$i]; + $s .= 'catch(' . $t->varName . ($t->guard ? ' if ' . $this->parseTree($t->guard) : '') . '){' . $this->parseTree($t->block, true) . '}'; + } + if ($n->finallyBlock) + $s .= 'finally{' . $this->parseTree($n->finallyBlock, true) . '}'; + break; + + case KEYWORD_THROW: + case KEYWORD_RETURN: + $s = $n->type; + if ($n->value) + { + $t = $this->parseTree($n->value); + if (strlen($t)) + { + if ($this->isWordChar($t[0]) || $t[0] == '\\') + $s .= ' '; + + $s .= $t; + } + } + break; + + case KEYWORD_WITH: + $s = 'with(' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body); + break; + + case KEYWORD_VAR: + case KEYWORD_CONST: + $s = $n->value . ' '; + $childs = $n->treeNodes; + for ($i = 0, $j = count($childs); $i < $j; $i++) + { + $t = $childs[$i]; + $s .= ($i ? ',' : '') . $t->name; + $u = $t->initializer; + if ($u) + $s .= '=' . $this->parseTree($u); + } + break; + + case KEYWORD_IN: + case KEYWORD_INSTANCEOF: + $left = $this->parseTree($n->treeNodes[0]); + $right = $this->parseTree($n->treeNodes[1]); + + $s = $left; + + if ($this->isWordChar(substr($left, -1))) + $s .= ' '; + + $s .= $n->type; + + if ($this->isWordChar($right[0]) || $right[0] == '\\') + $s .= ' '; + + $s .= $right; + break; + + case KEYWORD_DELETE: + case KEYWORD_TYPEOF: + $right = $this->parseTree($n->treeNodes[0]); + + $s = $n->type; + + if ($this->isWordChar($right[0]) || $right[0] == '\\') + $s .= ' '; + + $s .= $right; + break; + + case KEYWORD_VOID: + $s = 'void(' . $this->parseTree($n->treeNodes[0]) . ')'; + break; + + case KEYWORD_DEBUGGER: + throw new Exception('NOT IMPLEMENTED: DEBUGGER'); + break; + + case TOKEN_CONDCOMMENT_START: + case TOKEN_CONDCOMMENT_END: + $s = $n->value . ($n->type == TOKEN_CONDCOMMENT_START ? ' ' : ''); + $childs = $n->treeNodes; + for ($i = 0, $j = count($childs); $i < $j; $i++) + $s .= $this->parseTree($childs[$i]); + break; + + case OP_SEMICOLON: + if ($expression = $n->expression) + $s = $this->parseTree($expression); + break; + + case JS_LABEL: + $s = $n->label . ':' . $this->parseTree($n->statement); + break; + + case OP_COMMA: + $childs = $n->treeNodes; + for ($i = 0, $j = count($childs); $i < $j; $i++) + $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]); + break; + + case OP_ASSIGN: + $s = $this->parseTree($n->treeNodes[0]) . $n->value . $this->parseTree($n->treeNodes[1]); + break; + + case OP_HOOK: + $s = $this->parseTree($n->treeNodes[0]) . '?' . $this->parseTree($n->treeNodes[1]) . ':' . $this->parseTree($n->treeNodes[2]); + break; + + case OP_OR: case OP_AND: + case OP_BITWISE_OR: case OP_BITWISE_XOR: case OP_BITWISE_AND: + case OP_EQ: case OP_NE: case OP_STRICT_EQ: case OP_STRICT_NE: + case OP_LT: case OP_LE: case OP_GE: case OP_GT: + case OP_LSH: case OP_RSH: case OP_URSH: + case OP_MUL: case OP_DIV: case OP_MOD: + $s = $this->parseTree($n->treeNodes[0]) . $n->type . $this->parseTree($n->treeNodes[1]); + break; + + case OP_PLUS: + case OP_MINUS: + $left = $this->parseTree($n->treeNodes[0]); + $right = $this->parseTree($n->treeNodes[1]); + + switch ($n->treeNodes[1]->type) + { + case OP_PLUS: + case OP_MINUS: + case OP_INCREMENT: + case OP_DECREMENT: + case OP_UNARY_PLUS: + case OP_UNARY_MINUS: + $s = $left . $n->type . ' ' . $right; + break; + + case TOKEN_STRING: + //combine concatted strings with same quotestyle + if ($n->type == OP_PLUS && substr($left, -1) == $right[0]) + { + $s = substr($left, 0, -1) . substr($right, 1); + break; + } + // FALL THROUGH + + default: + $s = $left . $n->type . $right; + } + break; + + case OP_NOT: + case OP_BITWISE_NOT: + case OP_UNARY_PLUS: + case OP_UNARY_MINUS: + $s = $n->value . $this->parseTree($n->treeNodes[0]); + break; + + case OP_INCREMENT: + case OP_DECREMENT: + if ($n->postfix) + $s = $this->parseTree($n->treeNodes[0]) . $n->value; + else + $s = $n->value . $this->parseTree($n->treeNodes[0]); + break; + + case OP_DOT: + $s = $this->parseTree($n->treeNodes[0]) . '.' . $this->parseTree($n->treeNodes[1]); + break; + + case JS_INDEX: + $s = $this->parseTree($n->treeNodes[0]); + // See if we can replace named index with a dot saving 3 bytes + if ( $n->treeNodes[0]->type == TOKEN_IDENTIFIER && + $n->treeNodes[1]->type == TOKEN_STRING && + $this->isValidIdentifier(substr($n->treeNodes[1]->value, 1, -1)) + ) + $s .= '.' . substr($n->treeNodes[1]->value, 1, -1); + else + $s .= '[' . $this->parseTree($n->treeNodes[1]) . ']'; + break; + + case JS_LIST: + $childs = $n->treeNodes; + for ($i = 0, $j = count($childs); $i < $j; $i++) + $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]); + break; + + case JS_CALL: + $s = $this->parseTree($n->treeNodes[0]) . '(' . $this->parseTree($n->treeNodes[1]) . ')'; + break; + + case KEYWORD_NEW: + case JS_NEW_WITH_ARGS: + $s = 'new ' . $this->parseTree($n->treeNodes[0]) . '(' . ($n->type == JS_NEW_WITH_ARGS ? $this->parseTree($n->treeNodes[1]) : '') . ')'; + break; + + case JS_ARRAY_INIT: + $s = '['; + $childs = $n->treeNodes; + for ($i = 0, $j = count($childs); $i < $j; $i++) + { + $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]); + } + $s .= ']'; + break; + + case JS_OBJECT_INIT: + $s = '{'; + $childs = $n->treeNodes; + for ($i = 0, $j = count($childs); $i < $j; $i++) + { + $t = $childs[$i]; + if ($i) + $s .= ','; + if ($t->type == JS_PROPERTY_INIT) + { + // Ditch the quotes when the index is a valid identifier + if ( $t->treeNodes[0]->type == TOKEN_STRING && + $this->isValidIdentifier(substr($t->treeNodes[0]->value, 1, -1)) + ) + $s .= substr($t->treeNodes[0]->value, 1, -1); + else + $s .= $t->treeNodes[0]->value; + + $s .= ':' . $this->parseTree($t->treeNodes[1]); + } + else + { + $s .= $t->type == JS_GETTER ? 'get' : 'set'; + $s .= ' ' . $t->name . '('; + $params = $t->params; + for ($i = 0, $j = count($params); $i < $j; $i++) + $s .= ($i ? ',' : '') . $params[$i]; + $s .= '){' . $this->parseTree($t->body, true) . '}'; + } + } + $s .= '}'; + break; + + case TOKEN_NUMBER: + $s = $n->value; + if (preg_match('/^([1-9]+)(0{3,})$/', $s, $m)) + $s = $m[1] . 'e' . strlen($m[2]); + break; + + case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE: + case TOKEN_IDENTIFIER: case TOKEN_STRING: case TOKEN_REGEXP: + $s = $n->value; + break; + + case JS_GROUP: + if (in_array( + $n->treeNodes[0]->type, + array( + JS_ARRAY_INIT, JS_OBJECT_INIT, JS_GROUP, + TOKEN_NUMBER, TOKEN_STRING, TOKEN_REGEXP, TOKEN_IDENTIFIER, + KEYWORD_NULL, KEYWORD_THIS, KEYWORD_TRUE, KEYWORD_FALSE + ) + )) + { + $s = $this->parseTree($n->treeNodes[0]); + } + else + { + $s = '(' . $this->parseTree($n->treeNodes[0]) . ')'; + } + break; + + default: + throw new Exception('UNKNOWN TOKEN TYPE: ' . $n->type); + } + + return $s; + } + + private function isValidIdentifier($string) + { + return preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $string) && !in_array($string, $this->reserved); + } + + private function isWordChar($char) + { + return $char == '_' || $char == '$' || ctype_alnum($char); + } +} + +class JSParser +{ + private $t; + private $minifier; + + private $opPrecedence = array( + ';' => 0, + ',' => 1, + '=' => 2, '?' => 2, ':' => 2, + // The above all have to have the same precedence, see bug 330975 + '||' => 4, + '&&' => 5, + '|' => 6, + '^' => 7, + '&' => 8, + '==' => 9, '!=' => 9, '===' => 9, '!==' => 9, + '<' => 10, '<=' => 10, '>=' => 10, '>' => 10, 'in' => 10, 'instanceof' => 10, + '<<' => 11, '>>' => 11, '>>>' => 11, + '+' => 12, '-' => 12, + '*' => 13, '/' => 13, '%' => 13, + 'delete' => 14, 'void' => 14, 'typeof' => 14, + '!' => 14, '~' => 14, 'U+' => 14, 'U-' => 14, + '++' => 15, '--' => 15, + 'new' => 16, + '.' => 17, + JS_NEW_WITH_ARGS => 0, JS_INDEX => 0, JS_CALL => 0, + JS_ARRAY_INIT => 0, JS_OBJECT_INIT => 0, JS_GROUP => 0 + ); + + private $opArity = array( + ',' => -2, + '=' => 2, + '?' => 3, + '||' => 2, + '&&' => 2, + '|' => 2, + '^' => 2, + '&' => 2, + '==' => 2, '!=' => 2, '===' => 2, '!==' => 2, + '<' => 2, '<=' => 2, '>=' => 2, '>' => 2, 'in' => 2, 'instanceof' => 2, + '<<' => 2, '>>' => 2, '>>>' => 2, + '+' => 2, '-' => 2, + '*' => 2, '/' => 2, '%' => 2, + 'delete' => 1, 'void' => 1, 'typeof' => 1, + '!' => 1, '~' => 1, 'U+' => 1, 'U-' => 1, + '++' => 1, '--' => 1, + 'new' => 1, + '.' => 2, + JS_NEW_WITH_ARGS => 2, JS_INDEX => 2, JS_CALL => 2, + JS_ARRAY_INIT => 1, JS_OBJECT_INIT => 1, JS_GROUP => 1, + TOKEN_CONDCOMMENT_START => 1, TOKEN_CONDCOMMENT_END => 1 + ); + + public function __construct($minifier=null) + { + $this->minifier = $minifier; + $this->t = new JSTokenizer(); + } + + public function parse($s, $f, $l) + { + // initialize tokenizer + $this->t->init($s, $f, $l); + + $x = new JSCompilerContext(false); + $n = $this->Script($x); + if (!$this->t->isDone()) + throw $this->t->newSyntaxError('Syntax error'); + + return $n; + } + + private function Script($x) + { + $n = $this->Statements($x); + $n->type = JS_SCRIPT; + $n->funDecls = $x->funDecls; + $n->varDecls = $x->varDecls; + + // minify by scope + if ($this->minifier) + { + $n->value = $this->minifier->parseTree($n); + + // clear tree from node to save memory + $n->treeNodes = null; + $n->funDecls = null; + $n->varDecls = null; + + $n->type = JS_MINIFIED; + } + + return $n; + } + + private function Statements($x) + { + $n = new JSNode($this->t, JS_BLOCK); + array_push($x->stmtStack, $n); + + while (!$this->t->isDone() && $this->t->peek() != OP_RIGHT_CURLY) + $n->addNode($this->Statement($x)); + + array_pop($x->stmtStack); + + return $n; + } + + private function Block($x) + { + $this->t->mustMatch(OP_LEFT_CURLY); + $n = $this->Statements($x); + $this->t->mustMatch(OP_RIGHT_CURLY); + + return $n; + } + + private function Statement($x) + { + $tt = $this->t->get(); + $n2 = null; + + // Cases for statements ending in a right curly return early, avoiding the + // common semicolon insertion magic after this switch. + switch ($tt) + { + case KEYWORD_FUNCTION: + return $this->FunctionDefinition( + $x, + true, + count($x->stmtStack) > 1 ? STATEMENT_FORM : DECLARED_FORM + ); + break; + + case OP_LEFT_CURLY: + $n = $this->Statements($x); + $this->t->mustMatch(OP_RIGHT_CURLY); + return $n; + + case KEYWORD_IF: + $n = new JSNode($this->t); + $n->condition = $this->ParenExpression($x); + array_push($x->stmtStack, $n); + $n->thenPart = $this->Statement($x); + $n->elsePart = $this->t->match(KEYWORD_ELSE) ? $this->Statement($x) : null; + array_pop($x->stmtStack); + return $n; + + case KEYWORD_SWITCH: + $n = new JSNode($this->t); + $this->t->mustMatch(OP_LEFT_PAREN); + $n->discriminant = $this->Expression($x); + $this->t->mustMatch(OP_RIGHT_PAREN); + $n->cases = array(); + $n->defaultIndex = -1; + + array_push($x->stmtStack, $n); + + $this->t->mustMatch(OP_LEFT_CURLY); + + while (($tt = $this->t->get()) != OP_RIGHT_CURLY) + { + switch ($tt) + { + case KEYWORD_DEFAULT: + if ($n->defaultIndex >= 0) + throw $this->t->newSyntaxError('More than one switch default'); + // FALL THROUGH + case KEYWORD_CASE: + $n2 = new JSNode($this->t); + if ($tt == KEYWORD_DEFAULT) + $n->defaultIndex = count($n->cases); + else + $n2->caseLabel = $this->Expression($x, OP_COLON); + break; + default: + throw $this->t->newSyntaxError('Invalid switch case'); + } + + $this->t->mustMatch(OP_COLON); + $n2->statements = new JSNode($this->t, JS_BLOCK); + while (($tt = $this->t->peek()) != KEYWORD_CASE && $tt != KEYWORD_DEFAULT && $tt != OP_RIGHT_CURLY) + $n2->statements->addNode($this->Statement($x)); + + array_push($n->cases, $n2); + } + + array_pop($x->stmtStack); + return $n; + + case KEYWORD_FOR: + $n = new JSNode($this->t); + $n->isLoop = true; + $this->t->mustMatch(OP_LEFT_PAREN); + + if (($tt = $this->t->peek()) != OP_SEMICOLON) + { + $x->inForLoopInit = true; + if ($tt == KEYWORD_VAR || $tt == KEYWORD_CONST) + { + $this->t->get(); + $n2 = $this->Variables($x); + } + else + { + $n2 = $this->Expression($x); + } + $x->inForLoopInit = false; + } + + if ($n2 && $this->t->match(KEYWORD_IN)) + { + $n->type = JS_FOR_IN; + if ($n2->type == KEYWORD_VAR) + { + if (count($n2->treeNodes) != 1) + { + throw $this->t->SyntaxError( + 'Invalid for..in left-hand side', + $this->t->filename, + $n2->lineno + ); + } + + // NB: n2[0].type == IDENTIFIER and n2[0].value == n2[0].name. + $n->iterator = $n2->treeNodes[0]; + $n->varDecl = $n2; + } + else + { + $n->iterator = $n2; + $n->varDecl = null; + } + + $n->object = $this->Expression($x); + } + else + { + $n->setup = $n2 ? $n2 : null; + $this->t->mustMatch(OP_SEMICOLON); + $n->condition = $this->t->peek() == OP_SEMICOLON ? null : $this->Expression($x); + $this->t->mustMatch(OP_SEMICOLON); + $n->update = $this->t->peek() == OP_RIGHT_PAREN ? null : $this->Expression($x); + } + + $this->t->mustMatch(OP_RIGHT_PAREN); + $n->body = $this->nest($x, $n); + return $n; + + case KEYWORD_WHILE: + $n = new JSNode($this->t); + $n->isLoop = true; + $n->condition = $this->ParenExpression($x); + $n->body = $this->nest($x, $n); + return $n; + + case KEYWORD_DO: + $n = new JSNode($this->t); + $n->isLoop = true; + $n->body = $this->nest($x, $n, KEYWORD_WHILE); + $n->condition = $this->ParenExpression($x); + if (!$x->ecmaStrictMode) + { + // "; + * $link = ""; + * + * // in min.php + * Minify::serve('Groups', array( + * 'groups' => $groupSources + * ,'setExpires' => (time() + 86400 * 365) + * )); + * + * + * @package Minify + * @author Stephen Clay + */ +class Minify_Build { + + /** + * Last modification time of all files in the build + * + * @var int + */ + public $lastModified = 0; + + /** + * String to use as ampersand in uri(). Set this to '&' if + * you are not HTML-escaping URIs. + * + * @var string + */ + public static $ampersand = '&'; + + /** + * Get a time-stamped URI + * + * + * echo $b->uri('/site.js'); + * // outputs "/site.js?1678242" + * + * echo $b->uri('/scriptaculous.js?load=effects'); + * // outputs "/scriptaculous.js?load=effects&1678242" + * + * + * @param string $uri + * @param boolean $forceAmpersand (default = false) Force the use of ampersand to + * append the timestamp to the URI. + * @return string + */ + public function uri($uri, $forceAmpersand = false) { + $sep = ($forceAmpersand || strpos($uri, '?') !== false) + ? self::$ampersand + : '?'; + return "{$uri}{$sep}{$this->lastModified}"; + } + + /** + * Create a build object + * + * @param array $sources array of Minify_Source objects and/or file paths + * + * @return null + */ + public function __construct($sources) + { + $max = 0; + foreach ((array)$sources as $source) { + if ($source instanceof Minify_Source) { + $max = max($max, $source->lastModified); + } elseif (is_string($source)) { + if (0 === strpos($source, '//')) { + $source = $_SERVER['DOCUMENT_ROOT'] . substr($source, 1); + } + if (is_file($source)) { + $max = max($max, filemtime($source)); + } + } + } + $this->lastModified = $max; + } +} diff --git a/public/min/lib/Minify/CSS.php b/public/min/lib/Minify/CSS.php new file mode 100644 index 0000000..3241455 --- /dev/null +++ b/public/min/lib/Minify/CSS.php @@ -0,0 +1,99 @@ + + * @author http://code.google.com/u/1stvamp/ (Issue 64 patch) + */ +class Minify_CSS { + + /** + * Minify a CSS string + * + * @param string $css + * + * @param array $options available options: + * + * 'preserveComments': (default true) multi-line comments that begin + * with "/*!" will be preserved with newlines before and after to + * enhance readability. + * + * 'removeCharsets': (default true) remove all @charset at-rules + * + * 'prependRelativePath': (default null) if given, this string will be + * prepended to all relative URIs in import/url declarations + * + * 'currentDir': (default null) if given, this is assumed to be the + * directory of the current CSS file. Using this, minify will rewrite + * all relative URIs in import/url declarations to correctly point to + * the desired files. For this to work, the files *must* exist and be + * visible by the PHP process. + * + * 'symlinks': (default = array()) If the CSS file is stored in + * a symlink-ed directory, provide an array of link paths to + * target paths, where the link paths are within the document root. Because + * paths need to be normalized for this to work, use "//" to substitute + * the doc root in the link paths (the array keys). E.g.: + * + * array('//symlink' => '/real/target/path') // unix + * array('//static' => 'D:\\staticStorage') // Windows + * + * + * 'docRoot': (default = $_SERVER['DOCUMENT_ROOT']) + * see Minify_CSS_UriRewriter::rewrite + * + * @return string + */ + public static function minify($css, $options = array()) + { + $options = array_merge(array( + 'compress' => true, + 'removeCharsets' => true, + 'preserveComments' => true, + 'currentDir' => null, + 'docRoot' => $_SERVER['DOCUMENT_ROOT'], + 'prependRelativePath' => null, + 'symlinks' => array(), + ), $options); + + if ($options['removeCharsets']) { + $css = preg_replace('/@charset[^;]+;\\s*/', '', $css); + } + if ($options['compress']) { + if (! $options['preserveComments']) { + $css = Minify_CSS_Compressor::process($css, $options); + } else { + $css = Minify_CommentPreserver::process( + $css + ,array('Minify_CSS_Compressor', 'process') + ,array($options) + ); + } + } + if (! $options['currentDir'] && ! $options['prependRelativePath']) { + return $css; + } + if ($options['currentDir']) { + return Minify_CSS_UriRewriter::rewrite( + $css + ,$options['currentDir'] + ,$options['docRoot'] + ,$options['symlinks'] + ); + } else { + return Minify_CSS_UriRewriter::prepend( + $css + ,$options['prependRelativePath'] + ); + } + } +} diff --git a/public/min/lib/Minify/CSS/Compressor.php b/public/min/lib/Minify/CSS/Compressor.php new file mode 100644 index 0000000..c6cdd8b --- /dev/null +++ b/public/min/lib/Minify/CSS/Compressor.php @@ -0,0 +1,249 @@ + + * @author http://code.google.com/u/1stvamp/ (Issue 64 patch) + */ +class Minify_CSS_Compressor { + + /** + * Minify a CSS string + * + * @param string $css + * + * @param array $options (currently ignored) + * + * @return string + */ + public static function process($css, $options = array()) + { + $obj = new Minify_CSS_Compressor($options); + return $obj->_process($css); + } + + /** + * @var array + */ + protected $_options = null; + + /** + * Are we "in" a hack? I.e. are some browsers targetted until the next comment? + * + * @var bool + */ + protected $_inHack = false; + + + /** + * Constructor + * + * @param array $options (currently ignored) + */ + private function __construct($options) { + $this->_options = $options; + } + + /** + * Minify a CSS string + * + * @param string $css + * + * @return string + */ + protected function _process($css) + { + $css = str_replace("\r\n", "\n", $css); + + // preserve empty comment after '>' + // http://www.webdevout.net/css-hacks#in_css-selectors + $css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css); + + // preserve empty comment between property and value + // http://css-discuss.incutio.com/?page=BoxModelHack + $css = preg_replace('@/\\*\\s*\\*/\\s*:@', '/*keep*/:', $css); + $css = preg_replace('@:\\s*/\\*\\s*\\*/@', ':/*keep*/', $css); + + // apply callback to all valid comments (and strip out surrounding ws + $css = preg_replace_callback('@\\s*/\\*([\\s\\S]*?)\\*/\\s*@' + ,array($this, '_commentCB'), $css); + + // remove ws around { } and last semicolon in declaration block + $css = preg_replace('/\\s*{\\s*/', '{', $css); + $css = preg_replace('/;?\\s*}\\s*/', '}', $css); + + // remove ws surrounding semicolons + $css = preg_replace('/\\s*;\\s*/', ';', $css); + + // remove ws around urls + $css = preg_replace('/ + url\\( # url( + \\s* + ([^\\)]+?) # 1 = the URL (really just a bunch of non right parenthesis) + \\s* + \\) # ) + /x', 'url($1)', $css); + + // remove ws between rules and colons + $css = preg_replace('/ + \\s* + ([{;]) # 1 = beginning of block or rule separator + \\s* + ([\\*_]?[\\w\\-]+) # 2 = property (and maybe IE filter) + \\s* + : + \\s* + (\\b|[#\'"-]) # 3 = first character of a value + /x', '$1$2:$3', $css); + + // remove ws in selectors + $css = preg_replace_callback('/ + (?: # non-capture + \\s* + [^~>+,\\s]+ # selector part + \\s* + [,>+~] # combinators + )+ + \\s* + [^~>+,\\s]+ # selector part + { # open declaration block + /x' + ,array($this, '_selectorsCB'), $css); + + // minimize hex colors + $css = preg_replace('/([^=])#([a-f\\d])\\2([a-f\\d])\\3([a-f\\d])\\4([\\s;\\}])/i' + , '$1#$2$3$4$5', $css); + + // remove spaces between font families + $css = preg_replace_callback('/font-family:([^;}]+)([;}])/' + ,array($this, '_fontFamilyCB'), $css); + + $css = preg_replace('/@import\\s+url/', '@import url', $css); + + // replace any ws involving newlines with a single newline + $css = preg_replace('/[ \\t]*\\n+\\s*/', "\n", $css); + + // separate common descendent selectors w/ newlines (to limit line lengths) + $css = preg_replace('/([\\w#\\.\\*]+)\\s+([\\w#\\.\\*]+){/', "$1\n$2{", $css); + + // Use newline after 1st numeric value (to limit line lengths). + $css = preg_replace('/ + ((?:padding|margin|border|outline):\\d+(?:px|em)?) # 1 = prop : 1st numeric value + \\s+ + /x' + ,"$1\n", $css); + + // prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/ + $css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css); + + return trim($css); + } + + /** + * Replace what looks like a set of selectors + * + * @param array $m regex matches + * + * @return string + */ + protected function _selectorsCB($m) + { + // remove ws around the combinators + return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]); + } + + /** + * Process a comment and return a replacement + * + * @param array $m regex matches + * + * @return string + */ + protected function _commentCB($m) + { + $hasSurroundingWs = (trim($m[0]) !== $m[1]); + $m = $m[1]; + // $m is the comment content w/o the surrounding tokens, + // but the return value will replace the entire comment. + if ($m === 'keep') { + return '/**/'; + } + if ($m === '" "') { + // component of http://tantek.com/CSS/Examples/midpass.html + return '/*" "*/'; + } + if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $m)) { + // component of http://tantek.com/CSS/Examples/midpass.html + return '/*";}}/* */'; + } + if ($this->_inHack) { + // inversion: feeding only to one browser + if (preg_match('@ + ^/ # comment started like /*/ + \\s* + (\\S[\\s\\S]+?) # has at least some non-ws content + \\s* + /\\* # ends like /*/ or /**/ + @x', $m, $n)) { + // end hack mode after this comment, but preserve the hack and comment content + $this->_inHack = false; + return "/*/{$n[1]}/**/"; + } + } + if (substr($m, -1) === '\\') { // comment ends like \*/ + // begin hack mode and preserve hack + $this->_inHack = true; + return '/*\\*/'; + } + if ($m !== '' && $m[0] === '/') { // comment looks like /*/ foo */ + // begin hack mode and preserve hack + $this->_inHack = true; + return '/*/*/'; + } + if ($this->_inHack) { + // a regular comment ends hack mode but should be preserved + $this->_inHack = false; + return '/**/'; + } + // Issue 107: if there's any surrounding whitespace, it may be important, so + // replace the comment with a single space + return $hasSurroundingWs // remove all other comments + ? ' ' + : ''; + } + + /** + * Process a font-family listing and return a replacement + * + * @param array $m regex matches + * + * @return string + */ + protected function _fontFamilyCB($m) + { + // Issue 210: must not eliminate WS between words in unquoted families + $pieces = preg_split('/(\'[^\']+\'|"[^"]+")/', $m[1], null, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + $out = 'font-family:'; + while (null !== ($piece = array_shift($pieces))) { + if ($piece[0] !== '"' && $piece[0] !== "'") { + $piece = preg_replace('/\\s+/', ' ', $piece); + $piece = preg_replace('/\\s?,\\s?/', ',', $piece); + } + $out .= $piece; + } + return $out . $m[2]; + } +} diff --git a/public/min/lib/Minify/CSS/UriRewriter.php b/public/min/lib/Minify/CSS/UriRewriter.php new file mode 100644 index 0000000..8ff6b2b --- /dev/null +++ b/public/min/lib/Minify/CSS/UriRewriter.php @@ -0,0 +1,307 @@ + + */ +class Minify_CSS_UriRewriter { + + /** + * rewrite() and rewriteRelative() append debugging information here + * + * @var string + */ + public static $debugText = ''; + + /** + * In CSS content, rewrite file relative URIs as root relative + * + * @param string $css + * + * @param string $currentDir The directory of the current CSS file. + * + * @param string $docRoot The document root of the web site in which + * the CSS file resides (default = $_SERVER['DOCUMENT_ROOT']). + * + * @param array $symlinks (default = array()) If the CSS file is stored in + * a symlink-ed directory, provide an array of link paths to + * target paths, where the link paths are within the document root. Because + * paths need to be normalized for this to work, use "//" to substitute + * the doc root in the link paths (the array keys). E.g.: + * + * array('//symlink' => '/real/target/path') // unix + * array('//static' => 'D:\\staticStorage') // Windows + * + * + * @return string + */ + public static function rewrite($css, $currentDir, $docRoot = null, $symlinks = array()) + { + self::$_docRoot = self::_realpath( + $docRoot ? $docRoot : $_SERVER['DOCUMENT_ROOT'] + ); + self::$_currentDir = self::_realpath($currentDir); + self::$_symlinks = array(); + + // normalize symlinks + foreach ($symlinks as $link => $target) { + $link = ($link === '//') + ? self::$_docRoot + : str_replace('//', self::$_docRoot . '/', $link); + $link = strtr($link, '/', DIRECTORY_SEPARATOR); + self::$_symlinks[$link] = self::_realpath($target); + } + + self::$debugText .= "docRoot : " . self::$_docRoot . "\n" + . "currentDir : " . self::$_currentDir . "\n"; + if (self::$_symlinks) { + self::$debugText .= "symlinks : " . var_export(self::$_symlinks, 1) . "\n"; + } + self::$debugText .= "\n"; + + $css = self::_trimUrls($css); + + // rewrite + $css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/' + ,array(self::$className, '_processUriCB'), $css); + $css = preg_replace_callback('/url\\(\\s*([\'"](.*?)[\'"]|[^\\)\\s]+)\\s*\\)/' + ,array(self::$className, '_processUriCB'), $css); + + return $css; + } + + /** + * In CSS content, prepend a path to relative URIs + * + * @param string $css + * + * @param string $path The path to prepend. + * + * @return string + */ + public static function prepend($css, $path) + { + self::$_prependPath = $path; + + $css = self::_trimUrls($css); + + // append + $css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/' + ,array(self::$className, '_processUriCB'), $css); + $css = preg_replace_callback('/url\\(\\s*([\'"](.*?)[\'"]|[^\\)\\s]+)\\s*\\)/' + ,array(self::$className, '_processUriCB'), $css); + + self::$_prependPath = null; + return $css; + } + + /** + * Get a root relative URI from a file relative URI + * + * + * Minify_CSS_UriRewriter::rewriteRelative( + * '../img/hello.gif' + * , '/home/user/www/css' // path of CSS file + * , '/home/user/www' // doc root + * ); + * // returns '/img/hello.gif' + * + * // example where static files are stored in a symlinked directory + * Minify_CSS_UriRewriter::rewriteRelative( + * 'hello.gif' + * , '/var/staticFiles/theme' + * , '/home/user/www' + * , array('/home/user/www/static' => '/var/staticFiles') + * ); + * // returns '/static/theme/hello.gif' + * + * + * @param string $uri file relative URI + * + * @param string $realCurrentDir realpath of the current file's directory. + * + * @param string $realDocRoot realpath of the site document root. + * + * @param array $symlinks (default = array()) If the file is stored in + * a symlink-ed directory, provide an array of link paths to + * real target paths, where the link paths "appear" to be within the document + * root. E.g.: + * + * array('/home/foo/www/not/real/path' => '/real/target/path') // unix + * array('C:\\htdocs\\not\\real' => 'D:\\real\\target\\path') // Windows + * + * + * @return string + */ + public static function rewriteRelative($uri, $realCurrentDir, $realDocRoot, $symlinks = array()) + { + // prepend path with current dir separator (OS-independent) + $path = strtr($realCurrentDir, '/', DIRECTORY_SEPARATOR) + . DIRECTORY_SEPARATOR . strtr($uri, '/', DIRECTORY_SEPARATOR); + + self::$debugText .= "file-relative URI : {$uri}\n" + . "path prepended : {$path}\n"; + + // "unresolve" a symlink back to doc root + foreach ($symlinks as $link => $target) { + if (0 === strpos($path, $target)) { + // replace $target with $link + $path = $link . substr($path, strlen($target)); + + self::$debugText .= "symlink unresolved : {$path}\n"; + + break; + } + } + // strip doc root + $path = substr($path, strlen($realDocRoot)); + + self::$debugText .= "docroot stripped : {$path}\n"; + + // fix to root-relative URI + $uri = strtr($path, '/\\', '//'); + $uri = self::removeDots($uri); + + self::$debugText .= "traversals removed : {$uri}\n\n"; + + return $uri; + } + + /** + * Remove instances of "./" and "../" where possible from a root-relative URI + * + * @param string $uri + * + * @return string + */ + public static function removeDots($uri) + { + $uri = str_replace('/./', '/', $uri); + // inspired by patch from Oleg Cherniy + do { + $uri = preg_replace('@/[^/]+/\\.\\./@', '/', $uri, 1, $changed); + } while ($changed); + return $uri; + } + + /** + * Defines which class to call as part of callbacks, change this + * if you extend Minify_CSS_UriRewriter + * + * @var string + */ + protected static $className = 'Minify_CSS_UriRewriter'; + + /** + * Get realpath with any trailing slash removed. If realpath() fails, + * just remove the trailing slash. + * + * @param string $path + * + * @return mixed path with no trailing slash + */ + protected static function _realpath($path) + { + $realPath = realpath($path); + if ($realPath !== false) { + $path = $realPath; + } + return rtrim($path, '/\\'); + } + + /** + * Directory of this stylesheet + * + * @var string + */ + private static $_currentDir = ''; + + /** + * DOC_ROOT + * + * @var string + */ + private static $_docRoot = ''; + + /** + * directory replacements to map symlink targets back to their + * source (within the document root) E.g. '/var/www/symlink' => '/var/realpath' + * + * @var array + */ + private static $_symlinks = array(); + + /** + * Path to prepend + * + * @var string + */ + private static $_prependPath = null; + + /** + * @param string $css + * + * @return string + */ + private static function _trimUrls($css) + { + return preg_replace('/ + url\\( # url( + \\s* + ([^\\)]+?) # 1 = URI (assuming does not contain ")") + \\s* + \\) # ) + /x', 'url($1)', $css); + } + + /** + * @param array $m + * + * @return string + */ + private static function _processUriCB($m) + { + // $m matched either '/@import\\s+([\'"])(.*?)[\'"]/' or '/url\\(\\s*([^\\)\\s]+)\\s*\\)/' + $isImport = ($m[0][0] === '@'); + // determine URI and the quote character (if any) + if ($isImport) { + $quoteChar = $m[1]; + $uri = $m[2]; + } else { + // $m[1] is either quoted or not + $quoteChar = ($m[1][0] === "'" || $m[1][0] === '"') + ? $m[1][0] + : ''; + $uri = ($quoteChar === '') + ? $m[1] + : substr($m[1], 1, strlen($m[1]) - 2); + } + // if not root/scheme relative and not starts with scheme + if (!preg_match('~^(/|[a-z]+\:)~', $uri)) { + // URI is file-relative: rewrite depending on options + if (self::$_prependPath === null) { + $uri = self::rewriteRelative($uri, self::$_currentDir, self::$_docRoot, self::$_symlinks); + } else { + $uri = self::$_prependPath . $uri; + if ($uri[0] === '/') { + $root = ''; + $rootRelative = $uri; + $uri = $root . self::removeDots($rootRelative); + } elseif (preg_match('@^((https?\:)?//([^/]+))/@', $uri, $m) && (false !== strpos($m[3], '.'))) { + $root = $m[1]; + $rootRelative = substr($uri, strlen($root)); + $uri = $root . self::removeDots($rootRelative); + } + } + } + return $isImport + ? "@import {$quoteChar}{$uri}{$quoteChar}" + : "url({$quoteChar}{$uri}{$quoteChar})"; + } +} diff --git a/public/min/lib/Minify/CSSmin.php b/public/min/lib/Minify/CSSmin.php new file mode 100644 index 0000000..4403383 --- /dev/null +++ b/public/min/lib/Minify/CSSmin.php @@ -0,0 +1,85 @@ + + */ +class Minify_CSSmin { + + /** + * Minify a CSS string + * + * @param string $css + * + * @param array $options available options: + * + * 'removeCharsets': (default true) remove all @charset at-rules + * + * 'prependRelativePath': (default null) if given, this string will be + * prepended to all relative URIs in import/url declarations + * + * 'currentDir': (default null) if given, this is assumed to be the + * directory of the current CSS file. Using this, minify will rewrite + * all relative URIs in import/url declarations to correctly point to + * the desired files. For this to work, the files *must* exist and be + * visible by the PHP process. + * + * 'symlinks': (default = array()) If the CSS file is stored in + * a symlink-ed directory, provide an array of link paths to + * target paths, where the link paths are within the document root. Because + * paths need to be normalized for this to work, use "//" to substitute + * the doc root in the link paths (the array keys). E.g.: + * + * array('//symlink' => '/real/target/path') // unix + * array('//static' => 'D:\\staticStorage') // Windows + * + * + * 'docRoot': (default = $_SERVER['DOCUMENT_ROOT']) + * see Minify_CSS_UriRewriter::rewrite + * + * @return string + */ + public static function minify($css, $options = array()) + { + $options = array_merge(array( + 'compress' => true, + 'removeCharsets' => true, + 'currentDir' => null, + 'docRoot' => $_SERVER['DOCUMENT_ROOT'], + 'prependRelativePath' => null, + 'symlinks' => array(), + ), $options); + + if ($options['removeCharsets']) { + $css = preg_replace('/@charset[^;]+;\\s*/', '', $css); + } + if ($options['compress']) { + $obj = new CSSmin(); + $css = $obj->run($css); + } + if (! $options['currentDir'] && ! $options['prependRelativePath']) { + return $css; + } + if ($options['currentDir']) { + return Minify_CSS_UriRewriter::rewrite( + $css + ,$options['currentDir'] + ,$options['docRoot'] + ,$options['symlinks'] + ); + } else { + return Minify_CSS_UriRewriter::prepend( + $css + ,$options['prependRelativePath'] + ); + } + } +} diff --git a/public/min/lib/Minify/Cache/APC.php b/public/min/lib/Minify/Cache/APC.php new file mode 100644 index 0000000..f427ea6 --- /dev/null +++ b/public/min/lib/Minify/Cache/APC.php @@ -0,0 +1,133 @@ + + * Minify::setCache(new Minify_Cache_APC()); + * + * + * @package Minify + * @author Chris Edwards + **/ +class Minify_Cache_APC { + + /** + * Create a Minify_Cache_APC object, to be passed to + * Minify::setCache(). + * + * + * @param int $expire seconds until expiration (default = 0 + * meaning the item will not get an expiration date) + * + * @return null + */ + public function __construct($expire = 0) + { + $this->_exp = $expire; + } + + /** + * Write data to cache. + * + * @param string $id cache id + * + * @param string $data + * + * @return bool success + */ + public function store($id, $data) + { + return apc_store($id, "{$_SERVER['REQUEST_TIME']}|{$data}", $this->_exp); + } + + /** + * Get the size of a cache entry + * + * @param string $id cache id + * + * @return int size in bytes + */ + public function getSize($id) + { + if (! $this->_fetch($id)) { + return false; + } + return (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) + ? mb_strlen($this->_data, '8bit') + : strlen($this->_data); + } + + /** + * Does a valid cache entry exist? + * + * @param string $id cache id + * + * @param int $srcMtime mtime of the original source file(s) + * + * @return bool exists + */ + public function isValid($id, $srcMtime) + { + return ($this->_fetch($id) && ($this->_lm >= $srcMtime)); + } + + /** + * Send the cached content to output + * + * @param string $id cache id + */ + public function display($id) + { + echo $this->_fetch($id) + ? $this->_data + : ''; + } + + /** + * Fetch the cached content + * + * @param string $id cache id + * + * @return string + */ + public function fetch($id) + { + return $this->_fetch($id) + ? $this->_data + : ''; + } + + private $_exp = null; + + // cache of most recently fetched id + private $_lm = null; + private $_data = null; + private $_id = null; + + /** + * Fetch data and timestamp from apc, store in instance + * + * @param string $id + * + * @return bool success + */ + private function _fetch($id) + { + if ($this->_id === $id) { + return true; + } + $ret = apc_fetch($id); + if (false === $ret) { + $this->_id = null; + return false; + } + list($this->_lm, $this->_data) = explode('|', $ret, 2); + $this->_id = $id; + return true; + } +} diff --git a/public/min/lib/Minify/Cache/File.php b/public/min/lib/Minify/Cache/File.php new file mode 100644 index 0000000..3973f1b --- /dev/null +++ b/public/min/lib/Minify/Cache/File.php @@ -0,0 +1,197 @@ +_locking = $fileLocking; + $this->_path = $path; + } + + /** + * Write data to cache. + * + * @param string $id cache id (e.g. a filename) + * + * @param string $data + * + * @return bool success + */ + public function store($id, $data) + { + $flag = $this->_locking + ? LOCK_EX + : null; + $file = $this->_path . '/' . $id; + if (! @file_put_contents($file, $data, $flag)) { + $this->_log("Minify_Cache_File: Write failed to '$file'"); + } + // write control + if ($data !== $this->fetch($id)) { + @unlink($file); + $this->_log("Minify_Cache_File: Post-write read failed for '$file'"); + return false; + } + return true; + } + + /** + * Get the size of a cache entry + * + * @param string $id cache id (e.g. a filename) + * + * @return int size in bytes + */ + public function getSize($id) + { + return filesize($this->_path . '/' . $id); + } + + /** + * Does a valid cache entry exist? + * + * @param string $id cache id (e.g. a filename) + * + * @param int $srcMtime mtime of the original source file(s) + * + * @return bool exists + */ + public function isValid($id, $srcMtime) + { + $file = $this->_path . '/' . $id; + return (is_file($file) && (filemtime($file) >= $srcMtime)); + } + + /** + * Send the cached content to output + * + * @param string $id cache id (e.g. a filename) + */ + public function display($id) + { + if ($this->_locking) { + $fp = fopen($this->_path . '/' . $id, 'rb'); + flock($fp, LOCK_SH); + fpassthru($fp); + flock($fp, LOCK_UN); + fclose($fp); + } else { + readfile($this->_path . '/' . $id); + } + } + + /** + * Fetch the cached content + * + * @param string $id cache id (e.g. a filename) + * + * @return string + */ + public function fetch($id) + { + if ($this->_locking) { + $fp = fopen($this->_path . '/' . $id, 'rb'); + if (!$fp) { + return false; + } + flock($fp, LOCK_SH); + $ret = stream_get_contents($fp); + flock($fp, LOCK_UN); + fclose($fp); + return $ret; + } else { + return file_get_contents($this->_path . '/' . $id); + } + } + + /** + * Fetch the cache path used + * + * @return string + */ + public function getPath() + { + return $this->_path; + } + + /** + * Get a usable temp directory + * + * Adapted from Solar/Dir.php + * @author Paul M. Jones + * @license http://opensource.org/licenses/bsd-license.php BSD + * @link http://solarphp.com/trac/core/browser/trunk/Solar/Dir.php + * + * @return string + */ + public static function tmp() + { + static $tmp = null; + if (! $tmp) { + $tmp = function_exists('sys_get_temp_dir') + ? sys_get_temp_dir() + : self::_tmp(); + $tmp = rtrim($tmp, DIRECTORY_SEPARATOR); + } + return $tmp; + } + + /** + * Returns the OS-specific directory for temporary files + * + * @author Paul M. Jones + * @license http://opensource.org/licenses/bsd-license.php BSD + * @link http://solarphp.com/trac/core/browser/trunk/Solar/Dir.php + * + * @return string + */ + protected static function _tmp() + { + // non-Windows system? + if (strtolower(substr(PHP_OS, 0, 3)) != 'win') { + $tmp = empty($_ENV['TMPDIR']) ? getenv('TMPDIR') : $_ENV['TMPDIR']; + if ($tmp) { + return $tmp; + } else { + return '/tmp'; + } + } + // Windows 'TEMP' + $tmp = empty($_ENV['TEMP']) ? getenv('TEMP') : $_ENV['TEMP']; + if ($tmp) { + return $tmp; + } + // Windows 'TMP' + $tmp = empty($_ENV['TMP']) ? getenv('TMP') : $_ENV['TMP']; + if ($tmp) { + return $tmp; + } + // Windows 'windir' + $tmp = empty($_ENV['windir']) ? getenv('windir') : $_ENV['windir']; + if ($tmp) { + return $tmp; + } + // final fallback for Windows + return getenv('SystemRoot') . '\\temp'; + } + + /** + * Send message to the Minify logger + * @param string $msg + * @return null + */ + protected function _log($msg) + { + Minify_Logger::log($msg); + } + + private $_path = null; + private $_locking = null; +} diff --git a/public/min/lib/Minify/Cache/Memcache.php b/public/min/lib/Minify/Cache/Memcache.php new file mode 100644 index 0000000..cfe5146 --- /dev/null +++ b/public/min/lib/Minify/Cache/Memcache.php @@ -0,0 +1,140 @@ + + * // fall back to disk caching if memcache can't connect + * $memcache = new Memcache; + * if ($memcache->connect('localhost', 11211)) { + * Minify::setCache(new Minify_Cache_Memcache($memcache)); + * } else { + * Minify::setCache(); + * } + * + **/ +class Minify_Cache_Memcache { + + /** + * Create a Minify_Cache_Memcache object, to be passed to + * Minify::setCache(). + * + * @param Memcache $memcache already-connected instance + * + * @param int $expire seconds until expiration (default = 0 + * meaning the item will not get an expiration date) + * + * @return null + */ + public function __construct($memcache, $expire = 0) + { + $this->_mc = $memcache; + $this->_exp = $expire; + } + + /** + * Write data to cache. + * + * @param string $id cache id + * + * @param string $data + * + * @return bool success + */ + public function store($id, $data) + { + return $this->_mc->set($id, "{$_SERVER['REQUEST_TIME']}|{$data}", 0, $this->_exp); + } + + + /** + * Get the size of a cache entry + * + * @param string $id cache id + * + * @return int size in bytes + */ + public function getSize($id) + { + if (! $this->_fetch($id)) { + return false; + } + return (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) + ? mb_strlen($this->_data, '8bit') + : strlen($this->_data); + } + + /** + * Does a valid cache entry exist? + * + * @param string $id cache id + * + * @param int $srcMtime mtime of the original source file(s) + * + * @return bool exists + */ + public function isValid($id, $srcMtime) + { + return ($this->_fetch($id) && ($this->_lm >= $srcMtime)); + } + + /** + * Send the cached content to output + * + * @param string $id cache id + */ + public function display($id) + { + echo $this->_fetch($id) + ? $this->_data + : ''; + } + + /** + * Fetch the cached content + * + * @param string $id cache id + * + * @return string + */ + public function fetch($id) + { + return $this->_fetch($id) + ? $this->_data + : ''; + } + + private $_mc = null; + private $_exp = null; + + // cache of most recently fetched id + private $_lm = null; + private $_data = null; + private $_id = null; + + /** + * Fetch data and timestamp from memcache, store in instance + * + * @param string $id + * + * @return bool success + */ + private function _fetch($id) + { + if ($this->_id === $id) { + return true; + } + $ret = $this->_mc->get($id); + if (false === $ret) { + $this->_id = null; + return false; + } + list($this->_lm, $this->_data) = explode('|', $ret, 2); + $this->_id = $id; + return true; + } +} diff --git a/public/min/lib/Minify/Cache/WinCache.php b/public/min/lib/Minify/Cache/WinCache.php new file mode 100644 index 0000000..6d1f877 --- /dev/null +++ b/public/min/lib/Minify/Cache/WinCache.php @@ -0,0 +1,130 @@ + + * Minify::setCache(new Minify_Cache_WinCache()); + * + * + * @package Minify + * @author Matthias Fax + **/ +class Minify_Cache_WinCache +{ + + /** + * Create a Minify_Cache_Wincache object, to be passed to + * Minify::setCache(). + * + * + * @param int $expire seconds until expiration (default = 0 + * meaning the item will not get an expiration date) + */ + public function __construct($expire = 0) + { + if (!function_exists('wincache_ucache_info')) { + throw new Exception("WinCache for PHP is not installed to be able to use Minify_Cache_WinCache!"); + } + $this->_exp = $expire; + } + + /** + * Write data to cache. + * + * @param string $id cache id + * + * @param string $data + * + * @return bool success + */ + public function store($id, $data) + { + return wincache_ucache_set($id, "{$_SERVER['REQUEST_TIME']}|{$data}", $this->_exp); + } + + /** + * Get the size of a cache entry + * + * @param string $id cache id + * + * @return int size in bytes + */ + public function getSize($id) + { + if (!$this->_fetch($id)) { + return false; + } + return (function_exists('mb_strlen') && ((int) ini_get('mbstring.func_overload') & 2)) ? mb_strlen($this->_data, '8bit') : strlen($this->_data); + } + + /** + * Does a valid cache entry exist? + * + * @param string $id cache id + * + * @param int $srcMtime mtime of the original source file(s) + * + * @return bool exists + */ + public function isValid($id, $srcMtime) + { + return ($this->_fetch($id) && ($this->_lm >= $srcMtime)); + } + + /** + * Send the cached content to output + * + * @param string $id cache id + */ + public function display($id) + { + echo $this->_fetch($id) ? $this->_data : ''; + } + + /** + * Fetch the cached content + * + * @param string $id cache id + * + * @return string + */ + public function fetch($id) + { + return $this->_fetch($id) ? $this->_data : ''; + } + + private $_exp = NULL; + + // cache of most recently fetched id + private $_lm = NULL; + private $_data = NULL; + private $_id = NULL; + + /** + * Fetch data and timestamp from WinCache, store in instance + * + * @param string $id + * + * @return bool success + */ + private function _fetch($id) + { + if ($this->_id === $id) { + return true; + } + $suc = false; + $ret = wincache_ucache_get($id, $suc); + if (!$suc) { + $this->_id = NULL; + return false; + } + list($this->_lm, $this->_data) = explode('|', $ret, 2); + $this->_id = $id; + return true; + } +} \ No newline at end of file diff --git a/public/min/lib/Minify/Cache/XCache.php b/public/min/lib/Minify/Cache/XCache.php new file mode 100644 index 0000000..8030d98 --- /dev/null +++ b/public/min/lib/Minify/Cache/XCache.php @@ -0,0 +1,126 @@ + + * Minify::setCache(new Minify_Cache_XCache()); + * + * + * @package Minify + * @author Elan Ruusamäe + **/ +class Minify_Cache_XCache { + + /** + * Create a Minify_Cache_XCache object, to be passed to + * Minify::setCache(). + * + * @param int $expire seconds until expiration (default = 0 + * meaning the item will not get an expiration date) + */ + public function __construct($expire = 0) + { + $this->_exp = $expire; + } + + /** + * Write data to cache. + * + * @param string $id cache id + * @param string $data + * @return bool success + */ + public function store($id, $data) + { + return xcache_set($id, "{$_SERVER['REQUEST_TIME']}|{$data}", $this->_exp); + } + + /** + * Get the size of a cache entry + * + * @param string $id cache id + * @return int size in bytes + */ + public function getSize($id) + { + if (! $this->_fetch($id)) { + return false; + } + return (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) + ? mb_strlen($this->_data, '8bit') + : strlen($this->_data); + } + + /** + * Does a valid cache entry exist? + * + * @param string $id cache id + * @param int $srcMtime mtime of the original source file(s) + * @return bool exists + */ + public function isValid($id, $srcMtime) + { + return ($this->_fetch($id) && ($this->_lm >= $srcMtime)); + } + + /** + * Send the cached content to output + * + * @param string $id cache id + */ + public function display($id) + { + echo $this->_fetch($id) + ? $this->_data + : ''; + } + + /** + * Fetch the cached content + * + * @param string $id cache id + * @return string + */ + public function fetch($id) + { + return $this->_fetch($id) + ? $this->_data + : ''; + } + + private $_exp = null; + + // cache of most recently fetched id + private $_lm = null; + private $_data = null; + private $_id = null; + + /** + * Fetch data and timestamp from xcache, store in instance + * + * @param string $id + * @return bool success + */ + private function _fetch($id) + { + if ($this->_id === $id) { + return true; + } + $ret = xcache_get($id); + if (false === $ret) { + $this->_id = null; + return false; + } + list($this->_lm, $this->_data) = explode('|', $ret, 2); + $this->_id = $id; + return true; + } +} diff --git a/public/min/lib/Minify/Cache/ZendPlatform.php b/public/min/lib/Minify/Cache/ZendPlatform.php new file mode 100644 index 0000000..eb708f6 --- /dev/null +++ b/public/min/lib/Minify/Cache/ZendPlatform.php @@ -0,0 +1,142 @@ + + * Minify::setCache(new Minify_Cache_ZendPlatform()); + * + * + * @package Minify + * @author Patrick van Dissel + */ +class Minify_Cache_ZendPlatform { + + + /** + * Create a Minify_Cache_ZendPlatform object, to be passed to + * Minify::setCache(). + * + * @param int $expire seconds until expiration (default = 0 + * meaning the item will not get an expiration date) + * + * @return null + */ + public function __construct($expire = 0) + { + $this->_exp = $expire; + } + + + /** + * Write data to cache. + * + * @param string $id cache id + * + * @param string $data + * + * @return bool success + */ + public function store($id, $data) + { + return output_cache_put($id, "{$_SERVER['REQUEST_TIME']}|{$data}"); + } + + + /** + * Get the size of a cache entry + * + * @param string $id cache id + * + * @return int size in bytes + */ + public function getSize($id) + { + return $this->_fetch($id) + ? strlen($this->_data) + : false; + } + + + /** + * Does a valid cache entry exist? + * + * @param string $id cache id + * + * @param int $srcMtime mtime of the original source file(s) + * + * @return bool exists + */ + public function isValid($id, $srcMtime) + { + $ret = ($this->_fetch($id) && ($this->_lm >= $srcMtime)); + return $ret; + } + + + /** + * Send the cached content to output + * + * @param string $id cache id + */ + public function display($id) + { + echo $this->_fetch($id) + ? $this->_data + : ''; + } + + + /** + * Fetch the cached content + * + * @param string $id cache id + * + * @return string + */ + public function fetch($id) + { + return $this->_fetch($id) + ? $this->_data + : ''; + } + + + private $_exp = null; + + + // cache of most recently fetched id + private $_lm = null; + private $_data = null; + private $_id = null; + + + /** + * Fetch data and timestamp from ZendPlatform, store in instance + * + * @param string $id + * + * @return bool success + */ + private function _fetch($id) + { + if ($this->_id === $id) { + return true; + } + $ret = output_cache_get($id, $this->_exp); + if (false === $ret) { + $this->_id = null; + return false; + } + list($this->_lm, $this->_data) = explode('|', $ret, 2); + $this->_id = $id; + return true; + } +} diff --git a/public/min/lib/Minify/ClosureCompiler.php b/public/min/lib/Minify/ClosureCompiler.php new file mode 100644 index 0000000..52d4364 --- /dev/null +++ b/public/min/lib/Minify/ClosureCompiler.php @@ -0,0 +1,139 @@ + + * Minify_ClosureCompiler::$jarFile = '/path/to/closure-compiler-20120123.jar'; + * Minify_ClosureCompiler::$tempDir = '/tmp'; + * $code = Minify_ClosureCompiler::minify( + * $code, + * array('compilation_level' => 'SIMPLE_OPTIMIZATIONS') + * ); + * + * --compilation_level WHITESPACE_ONLY, SIMPLE_OPTIMIZATIONS, ADVANCED_OPTIMIZATIONS + * + * + * + * @todo unit tests, $options docs + * @todo more options support (or should just passthru them all?) + * + * @package Minify + * @author Stephen Clay + * @author Elan Ruusamäe + */ +class Minify_ClosureCompiler { + + const OPTION_CHARSET = 'charset'; + const OPTION_COMPILATION_LEVEL = 'compilation_level'; + + public static $isDebug = false; + + /** + * Filepath of the Closure Compiler jar file. This must be set before + * calling minifyJs(). + * + * @var string + */ + public static $jarFile = null; + + /** + * Writable temp directory. This must be set before calling minifyJs(). + * + * @var string + */ + public static $tempDir = null; + + /** + * Filepath of "java" executable (may be needed if not in shell's PATH) + * + * @var string + */ + public static $javaExecutable = 'java'; + + /** + * Minify a Javascript string + * + * @param string $js + * + * @param array $options (verbose is ignored) + * + * @see https://code.google.com/p/closure-compiler/source/browse/trunk/README + * + * @return string + * + * @throws Minify_ClosureCompiler_Exception + */ + public static function minify($js, $options = array()) + { + self::_prepare(); + if (! ($tmpFile = tempnam(self::$tempDir, 'cc_'))) { + throw new Minify_ClosureCompiler_Exception('Minify_ClosureCompiler : could not create temp file in "'.self::$tempDir.'".'); + } + file_put_contents($tmpFile, $js); + $cmd = self::_getCmd($options, $tmpFile); + exec($cmd, $output, $result_code); + unlink($tmpFile); + if ($result_code != 0) { + $message = 'Minify_ClosureCompiler : Closure Compiler execution failed.'; + if (self::$isDebug) { + exec($cmd . ' 2>&1', $error); + if ($error) { + $message .= "\nReason:\n" . join("\n", $error); + } + } + throw new Minify_ClosureCompiler_Exception($message); + } + return implode("\n", $output); + } + + private static function _getCmd($userOptions, $tmpFile) + { + $o = array_merge( + array( + self::OPTION_CHARSET => 'utf-8', + self::OPTION_COMPILATION_LEVEL => 'SIMPLE_OPTIMIZATIONS', + ), + $userOptions + ); + $charsetOption = $o[self::OPTION_CHARSET]; + $cmd = self::$javaExecutable . ' -jar ' . escapeshellarg(self::$jarFile) + . (preg_match('/^[\\da-zA-Z0-9\\-]+$/', $charsetOption) + ? " --charset {$charsetOption}" + : ''); + + foreach (array(self::OPTION_COMPILATION_LEVEL) as $opt) { + if ($o[$opt]) { + $cmd .= " --{$opt} ". escapeshellarg($o[$opt]); + } + } + return $cmd . ' ' . escapeshellarg($tmpFile); + } + + private static function _prepare() + { + if (! is_file(self::$jarFile)) { + throw new Minify_ClosureCompiler_Exception('Minify_ClosureCompiler : $jarFile('.self::$jarFile.') is not a valid file.'); + } + if (! is_readable(self::$jarFile)) { + throw new Minify_ClosureCompiler_Exception('Minify_ClosureCompiler : $jarFile('.self::$jarFile.') is not readable.'); + } + if (! is_dir(self::$tempDir)) { + throw new Minify_ClosureCompiler_Exception('Minify_ClosureCompiler : $tempDir('.self::$tempDir.') is not a valid direcotry.'); + } + if (! is_writable(self::$tempDir)) { + throw new Minify_ClosureCompiler_Exception('Minify_ClosureCompiler : $tempDir('.self::$tempDir.') is not writable.'); + } + } +} + +class Minify_ClosureCompiler_Exception extends Exception {} diff --git a/public/min/lib/Minify/CommentPreserver.php b/public/min/lib/Minify/CommentPreserver.php new file mode 100644 index 0000000..4c3ac22 --- /dev/null +++ b/public/min/lib/Minify/CommentPreserver.php @@ -0,0 +1,89 @@ + + */ +class Minify_CommentPreserver { + + /** + * String to be prepended to each preserved comment + * + * @var string + */ + public static $prepend = "\n"; + + /** + * String to be appended to each preserved comment + * + * @var string + */ + public static $append = "\n"; + + /** + * Process a string outside of C-style comments that begin with "/*!" + * + * On each non-empty string outside these comments, the given processor + * function will be called. The comments will be surrounded by + * Minify_CommentPreserver::$preprend and Minify_CommentPreserver::$append. + * + * @param string $content + * @param callback $processor function + * @param array $args array of extra arguments to pass to the processor + * function (default = array()) + * @return string + */ + public static function process($content, $processor, $args = array()) + { + $ret = ''; + while (true) { + list($beforeComment, $comment, $afterComment) = self::_nextComment($content); + if ('' !== $beforeComment) { + $callArgs = $args; + array_unshift($callArgs, $beforeComment); + $ret .= call_user_func_array($processor, $callArgs); + } + if (false === $comment) { + break; + } + $ret .= $comment; + $content = $afterComment; + } + return $ret; + } + + /** + * Extract comments that YUI Compressor preserves. + * + * @param string $in input + * + * @return array 3 elements are returned. If a YUI comment is found, the + * 2nd element is the comment and the 1st and 3rd are the surrounding + * strings. If no comment is found, the entire string is returned as the + * 1st element and the other two are false. + */ + private static function _nextComment($in) + { + if ( + false === ($start = strpos($in, '/*!')) + || false === ($end = strpos($in, '*/', $start + 3)) + ) { + return array($in, false, false); + } + $ret = array( + substr($in, 0, $start) + ,self::$prepend . '/*!' . substr($in, $start + 3, $end - $start - 1) . self::$append + ); + $endChars = (strlen($in) - $end - 2); + $ret[] = (0 === $endChars) + ? '' + : substr($in, -$endChars); + return $ret; + } +} diff --git a/public/min/lib/Minify/Controller/Base.php b/public/min/lib/Minify/Controller/Base.php new file mode 100644 index 0000000..95a1621 --- /dev/null +++ b/public/min/lib/Minify/Controller/Base.php @@ -0,0 +1,221 @@ + + */ +abstract class Minify_Controller_Base { + + /** + * Setup controller sources and set an needed options for Minify::source + * + * You must override this method in your subclass controller to set + * $this->sources. If the request is NOT valid, make sure $this->sources + * is left an empty array. Then strip any controller-specific options from + * $options and return it. To serve files, $this->sources must be an array of + * Minify_Source objects. + * + * @param array $options controller and Minify options + * + * @return array $options Minify::serve options + */ + abstract public function setupSources($options); + + /** + * Get default Minify options for this controller. + * + * Override in subclass to change defaults + * + * @return array options for Minify + */ + public function getDefaultMinifyOptions() { + return array( + 'isPublic' => true + ,'encodeOutput' => function_exists('gzdeflate') + ,'encodeMethod' => null // determine later + ,'encodeLevel' => 9 + ,'minifierOptions' => array() // no minifier options + ,'contentTypeCharset' => 'utf-8' + ,'maxAge' => 1800 // 30 minutes + ,'rewriteCssUris' => true + ,'bubbleCssImports' => false + ,'quiet' => false // serve() will send headers and output + ,'debug' => false + ,'concatOnly' => false + + // if you override these, the response codes MUST be directly after + // the first space. + ,'badRequestHeader' => 'HTTP/1.0 400 Bad Request' + ,'errorHeader' => 'HTTP/1.0 500 Internal Server Error' + + // callback function to see/modify content of all sources + ,'postprocessor' => null + // file to require to load preprocessor + ,'postprocessorRequire' => null + ); + } + + /** + * Get default minifiers for this controller. + * + * Override in subclass to change defaults + * + * @return array minifier callbacks for common types + */ + public function getDefaultMinifers() { + $ret[Minify::TYPE_JS] = array('JSMin', 'minify'); + $ret[Minify::TYPE_CSS] = array('Minify_CSS', 'minify'); + $ret[Minify::TYPE_HTML] = array('Minify_HTML', 'minify'); + return $ret; + } + + /** + * Is a user-given file within an allowable directory, existing, + * and having an extension js/css/html/txt ? + * + * This is a convenience function for controllers that have to accept + * user-given paths + * + * @param string $file full file path (already processed by realpath()) + * + * @param array $safeDirs directories where files are safe to serve. Files can also + * be in subdirectories of these directories. + * + * @return bool file is safe + * + * @deprecated use checkAllowDirs, checkNotHidden instead + */ + public static function _fileIsSafe($file, $safeDirs) + { + $pathOk = false; + foreach ((array)$safeDirs as $safeDir) { + if (strpos($file, $safeDir) === 0) { + $pathOk = true; + break; + } + } + $base = basename($file); + if (! $pathOk || ! is_file($file) || $base[0] === '.') { + return false; + } + list($revExt) = explode('.', strrev($base)); + return in_array(strrev($revExt), array('js', 'css', 'html', 'txt')); + } + + /** + * @param string $file + * @param array $allowDirs + * @param string $uri + * @return bool + * @throws Exception + */ + public static function checkAllowDirs($file, $allowDirs, $uri) + { + foreach ((array)$allowDirs as $allowDir) { + if (strpos($file, $allowDir) === 0) { + return true; + } + } + throw new Exception("File '$file' is outside \$allowDirs. If the path is" + . " resolved via an alias/symlink, look into the \$min_symlinks option." + . " E.g. \$min_symlinks['/" . dirname($uri) . "'] = '" . dirname($file) . "';"); + } + + /** + * @param string $file + * @throws Exception + */ + public static function checkNotHidden($file) + { + $b = basename($file); + if (0 === strpos($b, '.')) { + throw new Exception("Filename '$b' starts with period (may be hidden)"); + } + } + + /** + * instances of Minify_Source, which provide content and any individual minification needs. + * + * @var Minify_Source[] + */ + public $sources = array(); + + /** + * Short name to place inside cache id + * + * The setupSources() method may choose to set this, making it easier to + * recognize a particular set of sources/settings in the cache folder. It + * will be filtered and truncated to make the final cache id <= 250 bytes. + * + * @var string + */ + public $selectionId = ''; + + /** + * Mix in default controller options with user-given options + * + * @param array $options user options + * + * @return array mixed options + */ + public final function mixInDefaultOptions($options) + { + $ret = array_merge( + $this->getDefaultMinifyOptions(), $options + ); + if (! isset($options['minifiers'])) { + $options['minifiers'] = array(); + } + $ret['minifiers'] = array_merge( + $this->getDefaultMinifers(), $options['minifiers'] + ); + return $ret; + } + + /** + * Analyze sources (if there are any) and set $options 'contentType' + * and 'lastModifiedTime' if they already aren't. + * + * @param array $options options for Minify + * + * @return array options for Minify + */ + public final function analyzeSources($options = array()) + { + if ($this->sources) { + if (! isset($options['contentType'])) { + $options['contentType'] = Minify_Source::getContentType($this->sources); + } + // last modified is needed for caching, even if setExpires is set + if (! isset($options['lastModifiedTime'])) { + $max = 0; + foreach ($this->sources as $source) { + $max = max($source->lastModified, $max); + } + $options['lastModifiedTime'] = $max; + } + } + return $options; + } + + /** + * Send message to the Minify logger + * + * @param string $msg + * + * @return null + */ + public function log($msg) { + Minify_Logger::log($msg); + } +} diff --git a/public/min/lib/Minify/Controller/Files.php b/public/min/lib/Minify/Controller/Files.php new file mode 100644 index 0000000..8f1fdea --- /dev/null +++ b/public/min/lib/Minify/Controller/Files.php @@ -0,0 +1,76 @@ + + * Minify::serve('Files', array( + * 'files' => array( + * '//js/jquery.js' + * ,'//js/plugins.js' + * ,'/home/username/file.js' + * ) + * )); + * + * + * As a shortcut, the controller will replace "//" at the beginning + * of a filename with $_SERVER['DOCUMENT_ROOT'] . '/'. + * + * @package Minify + * @author Stephen Clay + */ +class Minify_Controller_Files extends Minify_Controller_Base { + + /** + * Set up file sources + * + * @param array $options controller and Minify options + * @return array Minify options + * + * Controller options: + * + * 'files': (required) array of complete file paths, or a single path + */ + public function setupSources($options) { + // strip controller options + + $files = $options['files']; + // if $files is a single object, casting will break it + if (is_object($files)) { + $files = array($files); + } elseif (! is_array($files)) { + $files = (array)$files; + } + unset($options['files']); + + $sources = array(); + foreach ($files as $file) { + if ($file instanceof Minify_Source) { + $sources[] = $file; + continue; + } + if (0 === strpos($file, '//')) { + $file = $_SERVER['DOCUMENT_ROOT'] . substr($file, 1); + } + $realPath = realpath($file); + if (is_file($realPath)) { + $sources[] = new Minify_Source(array( + 'filepath' => $realPath + )); + } else { + $this->log("The path \"{$file}\" could not be found (or was not a file)"); + return $options; + } + } + if ($sources) { + $this->sources = $sources; + } + return $options; + } +} + diff --git a/public/min/lib/Minify/Controller/Groups.php b/public/min/lib/Minify/Controller/Groups.php new file mode 100644 index 0000000..498c12f --- /dev/null +++ b/public/min/lib/Minify/Controller/Groups.php @@ -0,0 +1,91 @@ + + * Minify::serve('Groups', array( + * 'groups' => array( + * 'css' => array('//css/type.css', '//css/layout.css') + * ,'js' => array('//js/jquery.js', '//js/site.js') + * ) + * )); + * + * + * If the above code were placed in /serve.php, it would enable the URLs + * /serve.php/js and /serve.php/css + * + * As a shortcut, the controller will replace "//" at the beginning + * of a filename with $_SERVER['DOCUMENT_ROOT'] . '/'. + * + * @package Minify + * @author Stephen Clay + */ +class Minify_Controller_Groups extends Minify_Controller_Base { + + /** + * Set up groups of files as sources + * + * @param array $options controller and Minify options + * + * 'groups': (required) array mapping PATH_INFO strings to arrays + * of complete file paths. @see Minify_Controller_Groups + * + * @return array Minify options + */ + public function setupSources($options) { + // strip controller options + $groups = $options['groups']; + unset($options['groups']); + + // mod_fcgid places PATH_INFO in ORIG_PATH_INFO + $pi = isset($_SERVER['ORIG_PATH_INFO']) + ? substr($_SERVER['ORIG_PATH_INFO'], 1) + : (isset($_SERVER['PATH_INFO']) + ? substr($_SERVER['PATH_INFO'], 1) + : false + ); + if (false === $pi || ! isset($groups[$pi])) { + // no PATH_INFO or not a valid group + $this->log("Missing PATH_INFO or no group set for \"$pi\""); + return $options; + } + $sources = array(); + + $files = $groups[$pi]; + // if $files is a single object, casting will break it + if (is_object($files)) { + $files = array($files); + } elseif (! is_array($files)) { + $files = (array)$files; + } + foreach ($files as $file) { + if ($file instanceof Minify_Source) { + $sources[] = $file; + continue; + } + if (0 === strpos($file, '//')) { + $file = $_SERVER['DOCUMENT_ROOT'] . substr($file, 1); + } + $realPath = realpath($file); + if (is_file($realPath)) { + $sources[] = new Minify_Source(array( + 'filepath' => $realPath + )); + } else { + $this->log("The path \"{$file}\" could not be found (or was not a file)"); + return $options; + } + } + if ($sources) { + $this->sources = $sources; + } + return $options; + } +} + diff --git a/public/min/lib/Minify/Controller/MinApp.php b/public/min/lib/Minify/Controller/MinApp.php new file mode 100644 index 0000000..9842203 --- /dev/null +++ b/public/min/lib/Minify/Controller/MinApp.php @@ -0,0 +1,237 @@ + + */ +class Minify_Controller_MinApp extends Minify_Controller_Base { + + /** + * Set up groups of files as sources + * + * @param array $options controller and Minify options + * + * @return array Minify options + */ + public function setupSources($options) { + // PHP insecure by default: realpath() and other FS functions can't handle null bytes. + foreach (array('g', 'b', 'f') as $key) { + if (isset($_GET[$key])) { + $_GET[$key] = str_replace("\x00", '', (string)$_GET[$key]); + } + } + + // filter controller options + $cOptions = array_merge( + array( + 'allowDirs' => '//' + ,'groupsOnly' => false + ,'groups' => array() + ,'noMinPattern' => '@[-\\.]min\\.(?:js|css)$@i' // matched against basename + ) + ,(isset($options['minApp']) ? $options['minApp'] : array()) + ); + unset($options['minApp']); + $sources = array(); + $this->selectionId = ''; + $firstMissingResource = null; + if (isset($_GET['g'])) { + // add group(s) + $this->selectionId .= 'g=' . $_GET['g']; + $keys = explode(',', $_GET['g']); + if ($keys != array_unique($keys)) { + $this->log("Duplicate group key found."); + return $options; + } + foreach ($keys as $key) { + if (! isset($cOptions['groups'][$key])) { + $this->log("A group configuration for \"{$key}\" was not found"); + return $options; + } + $files = $cOptions['groups'][$key]; + // if $files is a single object, casting will break it + if (is_object($files)) { + $files = array($files); + } elseif (! is_array($files)) { + $files = (array)$files; + } + foreach ($files as $file) { + if ($file instanceof Minify_Source) { + $sources[] = $file; + continue; + } + if (0 === strpos($file, '//')) { + $file = $_SERVER['DOCUMENT_ROOT'] . substr($file, 1); + } + $realpath = realpath($file); + if ($realpath && is_file($realpath)) { + $sources[] = $this->_getFileSource($realpath, $cOptions); + } else { + $this->log("The path \"{$file}\" (realpath \"{$realpath}\") could not be found (or was not a file)"); + if (null === $firstMissingResource) { + $firstMissingResource = basename($file); + continue; + } else { + $secondMissingResource = basename($file); + $this->log("More than one file was missing: '$firstMissingResource', '$secondMissingResource'"); + return $options; + } + } + } + if ($sources) { + try { + $this->checkType($sources[0]); + } catch (Exception $e) { + $this->log($e->getMessage()); + return $options; + } + } + } + } + if (! $cOptions['groupsOnly'] && isset($_GET['f'])) { + // try user files + // The following restrictions are to limit the URLs that minify will + // respond to. + if (// verify at least one file, files are single comma separated, + // and are all same extension + ! preg_match('/^[^,]+\\.(css|js)(?:,[^,]+\\.\\1)*$/', $_GET['f'], $m) + // no "//" + || strpos($_GET['f'], '//') !== false + // no "\" + || strpos($_GET['f'], '\\') !== false + ) { + $this->log("GET param 'f' was invalid"); + return $options; + } + $ext = ".{$m[1]}"; + try { + $this->checkType($m[1]); + } catch (Exception $e) { + $this->log($e->getMessage()); + return $options; + } + $files = explode(',', $_GET['f']); + if ($files != array_unique($files)) { + $this->log("Duplicate files were specified"); + return $options; + } + if (isset($_GET['b'])) { + // check for validity + if (preg_match('@^[^/]+(?:/[^/]+)*$@', $_GET['b']) + && false === strpos($_GET['b'], '..') + && $_GET['b'] !== '.') { + // valid base + $base = "/{$_GET['b']}/"; + } else { + $this->log("GET param 'b' was invalid"); + return $options; + } + } else { + $base = '/'; + } + $allowDirs = array(); + foreach ((array)$cOptions['allowDirs'] as $allowDir) { + $allowDirs[] = realpath(str_replace('//', $_SERVER['DOCUMENT_ROOT'] . '/', $allowDir)); + } + $basenames = array(); // just for cache id + foreach ($files as $file) { + $uri = $base . $file; + $path = $_SERVER['DOCUMENT_ROOT'] . $uri; + $realpath = realpath($path); + if (false === $realpath || ! is_file($realpath)) { + $this->log("The path \"{$path}\" (realpath \"{$realpath}\") could not be found (or was not a file)"); + if (null === $firstMissingResource) { + $firstMissingResource = $uri; + continue; + } else { + $secondMissingResource = $uri; + $this->log("More than one file was missing: '$firstMissingResource', '$secondMissingResource`'"); + return $options; + } + } + try { + parent::checkNotHidden($realpath); + parent::checkAllowDirs($realpath, $allowDirs, $uri); + } catch (Exception $e) { + $this->log($e->getMessage()); + return $options; + } + $sources[] = $this->_getFileSource($realpath, $cOptions); + $basenames[] = basename($realpath, $ext); + } + if ($this->selectionId) { + $this->selectionId .= '_f='; + } + $this->selectionId .= implode(',', $basenames) . $ext; + } + if ($sources) { + if (null !== $firstMissingResource) { + array_unshift($sources, new Minify_Source(array( + 'id' => 'missingFile' + // should not cause cache invalidation + ,'lastModified' => 0 + // due to caching, filename is unreliable. + ,'content' => "/* Minify: at least one missing file. See " . Minify::URL_DEBUG . " */\n" + ,'minifier' => '' + ))); + } + $this->sources = $sources; + } else { + $this->log("No sources to serve"); + } + return $options; + } + + /** + * @param string $file + * + * @param array $cOptions + * + * @return Minify_Source + */ + protected function _getFileSource($file, $cOptions) + { + $spec['filepath'] = $file; + if ($cOptions['noMinPattern'] && preg_match($cOptions['noMinPattern'], basename($file))) { + if (preg_match('~\.css$~i', $file)) { + $spec['minifyOptions']['compress'] = false; + } else { + $spec['minifier'] = ''; + } + } + return new Minify_Source($spec); + } + + protected $_type = null; + + /** + * Make sure that only source files of a single type are registered + * + * @param string $sourceOrExt + * + * @throws Exception + */ + public function checkType($sourceOrExt) + { + if ($sourceOrExt === 'js') { + $type = Minify::TYPE_JS; + } elseif ($sourceOrExt === 'css') { + $type = Minify::TYPE_CSS; + } elseif ($sourceOrExt->contentType !== null) { + $type = $sourceOrExt->contentType; + } else { + return; + } + if ($this->_type === null) { + $this->_type = $type; + } elseif ($this->_type !== $type) { + throw new Exception('Content-Type mismatch'); + } + } +} diff --git a/public/min/lib/Minify/Controller/Page.php b/public/min/lib/Minify/Controller/Page.php new file mode 100644 index 0000000..2c3b807 --- /dev/null +++ b/public/min/lib/Minify/Controller/Page.php @@ -0,0 +1,68 @@ + + */ +class Minify_Controller_Page extends Minify_Controller_Base { + + /** + * Set up source of HTML content + * + * @param array $options controller and Minify options + * @return array Minify options + * + * Controller options: + * + * 'content': (required) HTML markup + * + * 'id': (required) id of page (string for use in server-side caching) + * + * 'lastModifiedTime': timestamp of when this content changed. This + * is recommended to allow both server and client-side caching. + * + * 'minifyAll': should all CSS and Javascript blocks be individually + * minified? (default false) + * + * @todo Add 'file' option to read HTML file. + */ + public function setupSources($options) { + if (isset($options['file'])) { + $sourceSpec = array( + 'filepath' => $options['file'] + ); + $f = $options['file']; + } else { + // strip controller options + $sourceSpec = array( + 'content' => $options['content'] + ,'id' => $options['id'] + ); + $f = $options['id']; + unset($options['content'], $options['id']); + } + // something like "builder,index.php" or "directory,file.html" + $this->selectionId = strtr(substr($f, 1 + strlen(dirname(dirname($f)))), '/\\', ',,'); + + if (isset($options['minifyAll'])) { + // this will be the 2nd argument passed to Minify_HTML::minify() + $sourceSpec['minifyOptions'] = array( + 'cssMinifier' => array('Minify_CSS', 'minify') + ,'jsMinifier' => array('JSMin', 'minify') + ); + unset($options['minifyAll']); + } + $this->sources[] = new Minify_Source($sourceSpec); + + $options['contentType'] = Minify::TYPE_HTML; + return $options; + } +} + diff --git a/public/min/lib/Minify/Controller/Version1.php b/public/min/lib/Minify/Controller/Version1.php new file mode 100644 index 0000000..20cd66c --- /dev/null +++ b/public/min/lib/Minify/Controller/Version1.php @@ -0,0 +1,119 @@ + + * Minify::serve('Version1'); + * + * + * @package Minify + * @author Stephen Clay + */ +class Minify_Controller_Version1 extends Minify_Controller_Base { + + /** + * Set up groups of files as sources + * + * @param array $options controller and Minify options + * @return array Minify options + * + */ + public function setupSources($options) { + // PHP insecure by default: realpath() and other FS functions can't handle null bytes. + if (isset($_GET['files'])) { + $_GET['files'] = str_replace("\x00", '', (string)$_GET['files']); + } + + self::_setupDefines(); + if (MINIFY_USE_CACHE) { + $cacheDir = defined('MINIFY_CACHE_DIR') + ? MINIFY_CACHE_DIR + : ''; + Minify::setCache($cacheDir); + } + $options['badRequestHeader'] = 'HTTP/1.0 404 Not Found'; + $options['contentTypeCharset'] = MINIFY_ENCODING; + + // The following restrictions are to limit the URLs that minify will + // respond to. Ideally there should be only one way to reference a file. + if (! isset($_GET['files']) + // verify at least one file, files are single comma separated, + // and are all same extension + || ! preg_match('/^[^,]+\\.(css|js)(,[^,]+\\.\\1)*$/', $_GET['files'], $m) + // no "//" (makes URL rewriting easier) + || strpos($_GET['files'], '//') !== false + // no "\" + || strpos($_GET['files'], '\\') !== false + // no "./" + || preg_match('/(?:^|[^\\.])\\.\\//', $_GET['files']) + ) { + return $options; + } + + $files = explode(',', $_GET['files']); + if (count($files) > MINIFY_MAX_FILES) { + return $options; + } + + // strings for prepending to relative/absolute paths + $prependRelPaths = dirname($_SERVER['SCRIPT_FILENAME']) + . DIRECTORY_SEPARATOR; + $prependAbsPaths = $_SERVER['DOCUMENT_ROOT']; + + $goodFiles = array(); + $hasBadSource = false; + + $allowDirs = isset($options['allowDirs']) + ? $options['allowDirs'] + : MINIFY_BASE_DIR; + + foreach ($files as $file) { + // prepend appropriate string for abs/rel paths + $file = ($file[0] === '/' ? $prependAbsPaths : $prependRelPaths) . $file; + // make sure a real file! + $file = realpath($file); + // don't allow unsafe or duplicate files + if (parent::_fileIsSafe($file, $allowDirs) + && !in_array($file, $goodFiles)) + { + $goodFiles[] = $file; + $srcOptions = array( + 'filepath' => $file + ); + $this->sources[] = new Minify_Source($srcOptions); + } else { + $hasBadSource = true; + break; + } + } + if ($hasBadSource) { + $this->sources = array(); + } + if (! MINIFY_REWRITE_CSS_URLS) { + $options['rewriteCssUris'] = false; + } + return $options; + } + + private static function _setupDefines() + { + $defaults = array( + 'MINIFY_BASE_DIR' => realpath($_SERVER['DOCUMENT_ROOT']) + ,'MINIFY_ENCODING' => 'utf-8' + ,'MINIFY_MAX_FILES' => 16 + ,'MINIFY_REWRITE_CSS_URLS' => true + ,'MINIFY_USE_CACHE' => true + ); + foreach ($defaults as $const => $val) { + if (! defined($const)) { + define($const, $val); + } + } + } +} + diff --git a/public/min/lib/Minify/DebugDetector.php b/public/min/lib/Minify/DebugDetector.php new file mode 100644 index 0000000..1def974 --- /dev/null +++ b/public/min/lib/Minify/DebugDetector.php @@ -0,0 +1,26 @@ + + */ +class Minify_DebugDetector { + public static function shouldDebugRequest($cookie, $get, $requestUri) + { + if (isset($get['debug'])) { + return true; + } + if (! empty($cookie['minifyDebug'])) { + foreach (preg_split('/\\s+/', $cookie['minifyDebug']) as $debugUri) { + $pattern = '@' . preg_quote($debugUri, '@') . '@i'; + $pattern = str_replace(array('\\*', '\\?'), array('.*', '.'), $pattern); + if (preg_match($pattern, $requestUri)) { + return true; + } + } + } + return false; + } +} diff --git a/public/min/lib/Minify/HTML.php b/public/min/lib/Minify/HTML.php new file mode 100644 index 0000000..40f7307 --- /dev/null +++ b/public/min/lib/Minify/HTML.php @@ -0,0 +1,255 @@ + + */ +class Minify_HTML { + /** + * @var boolean + */ + protected $_jsCleanComments = true; + + /** + * "Minify" an HTML page + * + * @param string $html + * + * @param array $options + * + * 'cssMinifier' : (optional) callback function to process content of STYLE + * elements. + * + * 'jsMinifier' : (optional) callback function to process content of SCRIPT + * elements. Note: the type attribute is ignored. + * + * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If + * unset, minify will sniff for an XHTML doctype. + * + * @return string + */ + public static function minify($html, $options = array()) { + $min = new self($html, $options); + return $min->process(); + } + + + /** + * Create a minifier object + * + * @param string $html + * + * @param array $options + * + * 'cssMinifier' : (optional) callback function to process content of STYLE + * elements. + * + * 'jsMinifier' : (optional) callback function to process content of SCRIPT + * elements. Note: the type attribute is ignored. + * + * 'jsCleanComments' : (optional) whether to remove HTML comments beginning and end of script block + * + * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If + * unset, minify will sniff for an XHTML doctype. + */ + public function __construct($html, $options = array()) + { + $this->_html = str_replace("\r\n", "\n", trim($html)); + if (isset($options['xhtml'])) { + $this->_isXhtml = (bool)$options['xhtml']; + } + if (isset($options['cssMinifier'])) { + $this->_cssMinifier = $options['cssMinifier']; + } + if (isset($options['jsMinifier'])) { + $this->_jsMinifier = $options['jsMinifier']; + } + if (isset($options['jsCleanComments'])) { + $this->_jsCleanComments = (bool)$options['jsCleanComments']; + } + } + + + /** + * Minify the markeup given in the constructor + * + * @return string + */ + public function process() + { + if ($this->_isXhtml === null) { + $this->_isXhtml = (false !== strpos($this->_html, '_replacementHash = 'MINIFYHTML' . md5($_SERVER['REQUEST_TIME']); + $this->_placeholders = array(); + + // replace SCRIPTs (and minify) with placeholders + $this->_html = preg_replace_callback( + '/(\\s*)]*?>)([\\s\\S]*?)<\\/script>(\\s*)/i' + ,array($this, '_removeScriptCB') + ,$this->_html); + + // replace STYLEs (and minify) with placeholders + $this->_html = preg_replace_callback( + '/\\s*]*>)([\\s\\S]*?)<\\/style>\\s*/i' + ,array($this, '_removeStyleCB') + ,$this->_html); + + // remove HTML comments (not containing IE conditional comments). + $this->_html = preg_replace_callback( + '//' + ,array($this, '_commentCB') + ,$this->_html); + + // replace PREs with placeholders + $this->_html = preg_replace_callback('/\\s*]*?>[\\s\\S]*?<\\/pre>)\\s*/i' + ,array($this, '_removePreCB') + ,$this->_html); + + // replace TEXTAREAs with placeholders + $this->_html = preg_replace_callback( + '/\\s*]*?>[\\s\\S]*?<\\/textarea>)\\s*/i' + ,array($this, '_removeTextareaCB') + ,$this->_html); + + // trim each line. + // @todo take into account attribute values that span multiple lines. + $this->_html = preg_replace('/^\\s+|\\s+$/m', '', $this->_html); + + // remove ws around block/undisplayed elements + $this->_html = preg_replace('/\\s+(<\\/?(?:area|base(?:font)?|blockquote|body' + .'|caption|center|col(?:group)?|dd|dir|div|dl|dt|fieldset|form' + .'|frame(?:set)?|h[1-6]|head|hr|html|legend|li|link|map|menu|meta' + .'|ol|opt(?:group|ion)|p|param|t(?:able|body|head|d|h||r|foot|itle)' + .'|ul)\\b[^>]*>)/i', '$1', $this->_html); + + // remove ws outside of all elements + $this->_html = preg_replace( + '/>(\\s(?:\\s*))?([^<]+)(\\s(?:\s*))?$1$2$3<' + ,$this->_html); + + // use newlines before 1st attribute in open tags (to limit line lengths) + $this->_html = preg_replace('/(<[a-z\\-]+)\\s+([^>]+>)/i', "$1\n$2", $this->_html); + + // fill placeholders + $this->_html = str_replace( + array_keys($this->_placeholders) + ,array_values($this->_placeholders) + ,$this->_html + ); + // issue 229: multi-pass to catch scripts that didn't get replaced in textareas + $this->_html = str_replace( + array_keys($this->_placeholders) + ,array_values($this->_placeholders) + ,$this->_html + ); + return $this->_html; + } + + protected function _commentCB($m) + { + return (0 === strpos($m[1], '[') || false !== strpos($m[1], '_replacementHash . count($this->_placeholders) . '%'; + $this->_placeholders[$placeholder] = $content; + return $placeholder; + } + + protected $_isXhtml = null; + protected $_replacementHash = null; + protected $_placeholders = array(); + protected $_cssMinifier = null; + protected $_jsMinifier = null; + + protected function _removePreCB($m) + { + return $this->_reservePlace("_reservePlace("\\s*$)/', '', $css); + + // remove CDATA section markers + $css = $this->_removeCdata($css); + + // minify + $minifier = $this->_cssMinifier + ? $this->_cssMinifier + : 'trim'; + $css = call_user_func($minifier, $css); + + return $this->_reservePlace($this->_needsCdata($css) + ? "{$openStyle}/**/" + : "{$openStyle}{$css}" + ); + } + + protected function _removeScriptCB($m) + { + $openScript = "_jsCleanComments) { + $js = preg_replace('/(?:^\\s*\\s*$)/', '', $js); + } + + // remove CDATA section markers + $js = $this->_removeCdata($js); + + // minify + $minifier = $this->_jsMinifier + ? $this->_jsMinifier + : 'trim'; + $js = call_user_func($minifier, $js); + + return $this->_reservePlace($this->_needsCdata($js) + ? "{$ws1}{$openScript}/**/{$ws2}" + : "{$ws1}{$openScript}{$js}{$ws2}" + ); + } + + protected function _removeCdata($str) + { + return (false !== strpos($str, ''), '', $str) + : $str; + } + + protected function _needsCdata($str) + { + return ($this->_isXhtml && preg_match('/(?:[<&]|\\-\\-|\\]\\]>)/', $str)); + } +} diff --git a/public/min/lib/Minify/HTML/Helper.php b/public/min/lib/Minify/HTML/Helper.php new file mode 100644 index 0000000..6e8d70f --- /dev/null +++ b/public/min/lib/Minify/HTML/Helper.php @@ -0,0 +1,225 @@ + + */ +class Minify_HTML_Helper { + public $rewriteWorks = true; + public $minAppUri = '/min'; + public $groupsConfigFile = ''; + + /** + * Get an HTML-escaped Minify URI for a group or set of files + * + * @param string|array $keyOrFiles a group key or array of filepaths/URIs + * @param array $opts options: + * 'farExpires' : (default true) append a modified timestamp for cache revving + * 'debug' : (default false) append debug flag + * 'charset' : (default 'UTF-8') for htmlspecialchars + * 'minAppUri' : (default '/min') URI of min directory + * 'rewriteWorks' : (default true) does mod_rewrite work in min app? + * 'groupsConfigFile' : specify if different + * @return string + */ + public static function getUri($keyOrFiles, $opts = array()) + { + $opts = array_merge(array( // default options + 'farExpires' => true + ,'debug' => false + ,'charset' => 'UTF-8' + ,'minAppUri' => '/min' + ,'rewriteWorks' => true + ,'groupsConfigFile' => '' + ), $opts); + $h = new self; + $h->minAppUri = $opts['minAppUri']; + $h->rewriteWorks = $opts['rewriteWorks']; + $h->groupsConfigFile = $opts['groupsConfigFile']; + if (is_array($keyOrFiles)) { + $h->setFiles($keyOrFiles, $opts['farExpires']); + } else { + $h->setGroup($keyOrFiles, $opts['farExpires']); + } + $uri = $h->getRawUri($opts['farExpires'], $opts['debug']); + return htmlspecialchars($uri, ENT_QUOTES, $opts['charset']); + } + + /** + * Get non-HTML-escaped URI to minify the specified files + * + * @param bool $farExpires + * @param bool $debug + * @return string + */ + public function getRawUri($farExpires = true, $debug = false) + { + $path = rtrim($this->minAppUri, '/') . '/'; + if (! $this->rewriteWorks) { + $path .= '?'; + } + if (null === $this->_groupKey) { + // @todo: implement shortest uri + $path = self::_getShortestUri($this->_filePaths, $path); + } else { + $path .= "g=" . $this->_groupKey; + } + if ($debug) { + $path .= "&debug"; + } elseif ($farExpires && $this->_lastModified) { + $path .= "&" . $this->_lastModified; + } + return $path; + } + + /** + * Set the files that will comprise the URI we're building + * + * @param array $files + * @param bool $checkLastModified + */ + public function setFiles($files, $checkLastModified = true) + { + $this->_groupKey = null; + if ($checkLastModified) { + $this->_lastModified = self::getLastModified($files); + } + // normalize paths like in /min/f= + foreach ($files as $k => $file) { + if (0 === strpos($file, '//')) { + $file = substr($file, 2); + } elseif (0 === strpos($file, '/') + || 1 === strpos($file, ':\\')) { + $file = substr($file, strlen($_SERVER['DOCUMENT_ROOT']) + 1); + } + $file = strtr($file, '\\', '/'); + $files[$k] = $file; + } + $this->_filePaths = $files; + } + + /** + * Set the group of files that will comprise the URI we're building + * + * @param string $key + * @param bool $checkLastModified + */ + public function setGroup($key, $checkLastModified = true) + { + $this->_groupKey = $key; + if ($checkLastModified) { + if (! $this->groupsConfigFile) { + $this->groupsConfigFile = dirname(dirname(dirname(dirname(__FILE__)))) . '/groupsConfig.php'; + } + if (is_file($this->groupsConfigFile)) { + $gc = (require $this->groupsConfigFile); + $keys = explode(',', $key); + foreach ($keys as $key) { + if (isset($gc[$key])) { + $this->_lastModified = self::getLastModified($gc[$key], $this->_lastModified); + } + } + } + } + } + + /** + * Get the max(lastModified) of all files + * + * @param array|string $sources + * @param int $lastModified + * @return int + */ + public static function getLastModified($sources, $lastModified = 0) + { + $max = $lastModified; + foreach ((array)$sources as $source) { + if (is_object($source) && isset($source->lastModified)) { + $max = max($max, $source->lastModified); + } elseif (is_string($source)) { + if (0 === strpos($source, '//')) { + $source = $_SERVER['DOCUMENT_ROOT'] . substr($source, 1); + } + if (is_file($source)) { + $max = max($max, filemtime($source)); + } + } + } + return $max; + } + + protected $_groupKey = null; // if present, URI will be like g=... + protected $_filePaths = array(); + protected $_lastModified = null; + + + /** + * In a given array of strings, find the character they all have at + * a particular index + * + * @param array $arr array of strings + * @param int $pos index to check + * @return mixed a common char or '' if any do not match + */ + protected static function _getCommonCharAtPos($arr, $pos) { + if (!isset($arr[0][$pos])) { + return ''; + } + $c = $arr[0][$pos]; + $l = count($arr); + if ($l === 1) { + return $c; + } + for ($i = 1; $i < $l; ++$i) { + if ($arr[$i][$pos] !== $c) { + return ''; + } + } + return $c; + } + + /** + * Get the shortest URI to minify the set of source files + * + * @param array $paths root-relative URIs of files + * @param string $minRoot root-relative URI of the "min" application + * @return string + */ + protected static function _getShortestUri($paths, $minRoot = '/min/') { + $pos = 0; + $base = ''; + while (true) { + $c = self::_getCommonCharAtPos($paths, $pos); + if ($c === '') { + break; + } else { + $base .= $c; + } + ++$pos; + } + $base = preg_replace('@[^/]+$@', '', $base); + $uri = $minRoot . 'f=' . implode(',', $paths); + + if (substr($base, -1) === '/') { + // we have a base dir! + $basedPaths = $paths; + $l = count($paths); + for ($i = 0; $i < $l; ++$i) { + $basedPaths[$i] = substr($paths[$i], strlen($base)); + } + $base = substr($base, 0, strlen($base) - 1); + $bUri = $minRoot . 'b=' . $base . '&f=' . implode(',', $basedPaths); + + $uri = strlen($uri) < strlen($bUri) + ? $uri + : $bUri; + } + return $uri; + } +} diff --git a/public/min/lib/Minify/ImportProcessor.php b/public/min/lib/Minify/ImportProcessor.php new file mode 100644 index 0000000..bdfae54 --- /dev/null +++ b/public/min/lib/Minify/ImportProcessor.php @@ -0,0 +1,216 @@ + + * @author Simon Schick + */ +class Minify_ImportProcessor { + + public static $filesIncluded = array(); + + public static function process($file) + { + self::$filesIncluded = array(); + self::$_isCss = (strtolower(substr($file, -4)) === '.css'); + $obj = new Minify_ImportProcessor(dirname($file)); + return $obj->_getContent($file); + } + + // allows callback funcs to know the current directory + private $_currentDir = null; + + // allows callback funcs to know the directory of the file that inherits this one + private $_previewsDir = null; + + // allows _importCB to write the fetched content back to the obj + private $_importedContent = ''; + + private static $_isCss = null; + + /** + * @param String $currentDir + * @param String $previewsDir Is only used internally + */ + private function __construct($currentDir, $previewsDir = "") + { + $this->_currentDir = $currentDir; + $this->_previewsDir = $previewsDir; + } + + private function _getContent($file, $is_imported = false) + { + $file = realpath($file); + if (! $file + || in_array($file, self::$filesIncluded) + || false === ($content = @file_get_contents($file)) + ) { + // file missing, already included, or failed read + return ''; + } + self::$filesIncluded[] = realpath($file); + $this->_currentDir = dirname($file); + + // remove UTF-8 BOM if present + if (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) { + $content = substr($content, 3); + } + // ensure uniform EOLs + $content = str_replace("\r\n", "\n", $content); + + // process @imports + $content = preg_replace_callback( + '/ + @import\\s+ + (?:url\\(\\s*)? # maybe url( + [\'"]? # maybe quote + (.*?) # 1 = URI + [\'"]? # maybe end quote + (?:\\s*\\))? # maybe ) + ([a-zA-Z,\\s]*)? # 2 = media list + ; # end token + /x' + ,array($this, '_importCB') + ,$content + ); + + // You only need to rework the import-path if the script is imported + if (self::$_isCss && $is_imported) { + // rewrite remaining relative URIs + $content = preg_replace_callback( + '/url\\(\\s*([^\\)\\s]+)\\s*\\)/' + ,array($this, '_urlCB') + ,$content + ); + } + + return $this->_importedContent . $content; + } + + private function _importCB($m) + { + $url = $m[1]; + $mediaList = preg_replace('/\\s+/', '', $m[2]); + + if (strpos($url, '://') > 0) { + // protocol, leave in place for CSS, comment for JS + return self::$_isCss + ? $m[0] + : "/* Minify_ImportProcessor will not include remote content */"; + } + if ('/' === $url[0]) { + // protocol-relative or root path + $url = ltrim($url, '/'); + $file = realpath($_SERVER['DOCUMENT_ROOT']) . DIRECTORY_SEPARATOR + . strtr($url, '/', DIRECTORY_SEPARATOR); + } else { + // relative to current path + $file = $this->_currentDir . DIRECTORY_SEPARATOR + . strtr($url, '/', DIRECTORY_SEPARATOR); + } + $obj = new Minify_ImportProcessor(dirname($file), $this->_currentDir); + $content = $obj->_getContent($file, true); + if ('' === $content) { + // failed. leave in place for CSS, comment for JS + return self::$_isCss + ? $m[0] + : "/* Minify_ImportProcessor could not fetch '{$file}' */"; + } + return (!self::$_isCss || preg_match('@(?:^$|\\ball\\b)@', $mediaList)) + ? $content + : "@media {$mediaList} {\n{$content}\n}\n"; + } + + private function _urlCB($m) + { + // $m[1] is either quoted or not + $quote = ($m[1][0] === "'" || $m[1][0] === '"') + ? $m[1][0] + : ''; + $url = ($quote === '') + ? $m[1] + : substr($m[1], 1, strlen($m[1]) - 2); + if ('/' !== $url[0]) { + if (strpos($url, '//') > 0) { + // probably starts with protocol, do not alter + } else { + // prepend path with current dir separator (OS-independent) + $path = $this->_currentDir + . DIRECTORY_SEPARATOR . strtr($url, '/', DIRECTORY_SEPARATOR); + // update the relative path by the directory of the file that imported this one + $url = self::getPathDiff(realpath($this->_previewsDir), $path); + } + } + return "url({$quote}{$url}{$quote})"; + } + + /** + * @param string $from + * @param string $to + * @param string $ps + * @return string + */ + private function getPathDiff($from, $to, $ps = DIRECTORY_SEPARATOR) + { + $realFrom = $this->truepath($from); + $realTo = $this->truepath($to); + + $arFrom = explode($ps, rtrim($realFrom, $ps)); + $arTo = explode($ps, rtrim($realTo, $ps)); + while (count($arFrom) && count($arTo) && ($arFrom[0] == $arTo[0])) + { + array_shift($arFrom); + array_shift($arTo); + } + return str_pad("", count($arFrom) * 3, '..' . $ps) . implode($ps, $arTo); + } + + /** + * This function is to replace PHP's extremely buggy realpath(). + * @param string $path The original path, can be relative etc. + * @return string The resolved path, it might not exist. + * @see http://stackoverflow.com/questions/4049856/replace-phps-realpath + */ + function truepath($path) + { + // whether $path is unix or not + $unipath = strlen($path) == 0 || $path{0} != '/'; + // attempts to detect if path is relative in which case, add cwd + if (strpos($path, ':') === false && $unipath) + $path = $this->_currentDir . DIRECTORY_SEPARATOR . $path; + + // resolve path parts (single dot, double dot and double delimiters) + $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path); + $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen'); + $absolutes = array(); + foreach ($parts as $part) { + if ('.' == $part) + continue; + if ('..' == $part) { + array_pop($absolutes); + } else { + $absolutes[] = $part; + } + } + $path = implode(DIRECTORY_SEPARATOR, $absolutes); + // resolve any symlinks + if (file_exists($path) && linkinfo($path) > 0) + $path = readlink($path); + // put initial separator that could have been lost + $path = !$unipath ? '/' . $path : $path; + return $path; + } +} diff --git a/public/min/lib/Minify/JS/ClosureCompiler.php b/public/min/lib/Minify/JS/ClosureCompiler.php new file mode 100644 index 0000000..26a5531 --- /dev/null +++ b/public/min/lib/Minify/JS/ClosureCompiler.php @@ -0,0 +1,230 @@ + + * + * @todo can use a stream wrapper to unit test this? + */ +class Minify_JS_ClosureCompiler { + + /** + * @var string The option key for the maximum POST byte size + */ + const OPTION_MAX_BYTES = 'maxBytes'; + + /** + * @var string The option key for additional params. @see __construct + */ + const OPTION_ADDITIONAL_OPTIONS = 'additionalParams'; + + /** + * @var string The option key for the fallback Minifier + */ + const OPTION_FALLBACK_FUNCTION = 'fallbackFunc'; + + /** + * @var string The option key for the service URL + */ + const OPTION_COMPILER_URL = 'compilerUrl'; + + /** + * @var int The default maximum POST byte size according to https://developers.google.com/closure/compiler/docs/api-ref + */ + const DEFAULT_MAX_BYTES = 200000; + + /** + * @var string[] $DEFAULT_OPTIONS The default options to pass to the compiler service + * + * @note This would be a constant if PHP allowed it + */ + private static $DEFAULT_OPTIONS = array( + 'output_format' => 'text', + 'compilation_level' => 'SIMPLE_OPTIMIZATIONS' + ); + + /** + * @var string $url URL of compiler server. defaults to Google's + */ + protected $serviceUrl = 'http://closure-compiler.appspot.com/compile'; + + /** + * @var int $maxBytes The maximum JS size that can be sent to the compiler server in bytes + */ + protected $maxBytes = self::DEFAULT_MAX_BYTES; + + /** + * @var string[] $additionalOptions Additional options to pass to the compiler service + */ + protected $additionalOptions = array(); + + /** + * @var callable Function to minify JS if service fails. Default is JSMin + */ + protected $fallbackMinifier = array('JSMin', 'minify'); + + /** + * Minify JavaScript code via HTTP request to a Closure Compiler API + * + * @param string $js input code + * @param array $options Options passed to __construct(). @see __construct + * + * @return string + */ + public static function minify($js, array $options = array()) + { + $obj = new self($options); + return $obj->min($js); + } + + /** + * @param array $options Options with keys available below: + * + * fallbackFunc : (callable) function to minify if service unavailable. Default is JSMin. + * + * compilerUrl : (string) URL to closure compiler server + * + * maxBytes : (int) The maximum amount of bytes to be sent as js_code in the POST request. + * Defaults to 200000. + * + * additionalParams : (string[]) Additional parameters to pass to the compiler server. Can be anything named + * in https://developers.google.com/closure/compiler/docs/api-ref except for js_code and + * output_info + */ + public function __construct(array $options = array()) + { + if (isset($options[self::OPTION_FALLBACK_FUNCTION])) { + $this->fallbackMinifier = $options[self::OPTION_FALLBACK_FUNCTION]; + } + if (isset($options[self::OPTION_COMPILER_URL])) { + $this->serviceUrl = $options[self::OPTION_COMPILER_URL]; + } + if (isset($options[self::OPTION_ADDITIONAL_OPTIONS]) && is_array($options[self::OPTION_ADDITIONAL_OPTIONS])) { + $this->additionalOptions = $options[self::OPTION_ADDITIONAL_OPTIONS]; + } + if (isset($options[self::OPTION_MAX_BYTES])) { + $this->maxBytes = (int) $options[self::OPTION_MAX_BYTES]; + } + } + + /** + * Call the service to perform the minification + * + * @param string $js JavaScript code + * @return string + * @throws Minify_JS_ClosureCompiler_Exception + */ + public function min($js) + { + $postBody = $this->buildPostBody($js); + + if ($this->maxBytes > 0) { + $bytes = (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) + ? mb_strlen($postBody, '8bit') + : strlen($postBody); + if ($bytes > $this->maxBytes) { + throw new Minify_JS_ClosureCompiler_Exception( + 'POST content larger than ' . $this->maxBytes . ' bytes' + ); + } + } + + $response = $this->getResponse($postBody); + + if (preg_match('/^Error\(\d\d?\):/', $response)) { + if (is_callable($this->fallbackMinifier)) { + // use fallback + $response = "/* Received errors from Closure Compiler API:\n$response" + . "\n(Using fallback minifier)\n*/\n"; + $response .= call_user_func($this->fallbackMinifier, $js); + } else { + throw new Minify_JS_ClosureCompiler_Exception($response); + } + } + + if ($response === '') { + $errors = $this->getResponse($this->buildPostBody($js, true)); + throw new Minify_JS_ClosureCompiler_Exception($errors); + } + + return $response; + } + + /** + * Get the response for a given POST body + * + * @param string $postBody + * @return string + * @throws Minify_JS_ClosureCompiler_Exception + */ + protected function getResponse($postBody) + { + $allowUrlFopen = preg_match('/1|yes|on|true/i', ini_get('allow_url_fopen')); + + if ($allowUrlFopen) { + $contents = file_get_contents($this->serviceUrl, false, stream_context_create(array( + 'http' => array( + 'method' => 'POST', + 'header' => "Content-type: application/x-www-form-urlencoded\r\nConnection: close\r\n", + 'content' => $postBody, + 'max_redirects' => 0, + 'timeout' => 15, + ) + ))); + } elseif (defined('CURLOPT_POST')) { + $ch = curl_init($this->serviceUrl); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: application/x-www-form-urlencoded')); + curl_setopt($ch, CURLOPT_POSTFIELDS, $postBody); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15); + $contents = curl_exec($ch); + curl_close($ch); + } else { + throw new Minify_JS_ClosureCompiler_Exception( + "Could not make HTTP request: allow_url_open is false and cURL not available" + ); + } + + if (false === $contents) { + throw new Minify_JS_ClosureCompiler_Exception( + "No HTTP response from server" + ); + } + + return trim($contents); + } + + /** + * Build a POST request body + * + * @param string $js JavaScript code + * @param bool $returnErrors + * @return string + */ + protected function buildPostBody($js, $returnErrors = false) + { + return http_build_query( + array_merge( + self::$DEFAULT_OPTIONS, + $this->additionalOptions, + array( + 'js_code' => $js, + 'output_info' => ($returnErrors ? 'errors' : 'compiled_code') + ) + ), + null, + '&' + ); + } +} + +class Minify_JS_ClosureCompiler_Exception extends Exception {} diff --git a/public/min/lib/Minify/Lines.php b/public/min/lib/Minify/Lines.php new file mode 100644 index 0000000..cc8a074 --- /dev/null +++ b/public/min/lib/Minify/Lines.php @@ -0,0 +1,143 @@ + + * @author Adam Pedersen (Issue 55 fix) + */ +class Minify_Lines { + + /** + * Add line numbers in C-style comments + * + * This uses a very basic parser easily fooled by comment tokens inside + * strings or regexes, but, otherwise, generally clean code will not be + * mangled. URI rewriting can also be performed. + * + * @param string $content + * + * @param array $options available options: + * + * 'id': (optional) string to identify file. E.g. file name/path + * + * 'currentDir': (default null) if given, this is assumed to be the + * directory of the current CSS file. Using this, minify will rewrite + * all relative URIs in import/url declarations to correctly point to + * the desired files, and prepend a comment with debugging information about + * this process. + * + * @return string + */ + public static function minify($content, $options = array()) + { + $id = (isset($options['id']) && $options['id']) + ? $options['id'] + : ''; + $content = str_replace("\r\n", "\n", $content); + + // Hackily rewrite strings with XPath expressions that are + // likely to throw off our dumb parser (for Prototype 1.6.1). + $content = str_replace('"/*"', '"/"+"*"', $content); + $content = preg_replace('@([\'"])(\\.?//?)\\*@', '$1$2$1+$1*', $content); + + $lines = explode("\n", $content); + $numLines = count($lines); + // determine left padding + $padTo = strlen((string) $numLines); // e.g. 103 lines = 3 digits + $inComment = false; + $i = 0; + $newLines = array(); + while (null !== ($line = array_shift($lines))) { + if (('' !== $id) && (0 == $i % 50)) { + if ($inComment) { + array_push($newLines, '', "/* {$id} *|", ''); + } else { + array_push($newLines, '', "/* {$id} */", ''); + } + } + ++$i; + $newLines[] = self::_addNote($line, $i, $inComment, $padTo); + $inComment = self::_eolInComment($line, $inComment); + } + $content = implode("\n", $newLines) . "\n"; + + // check for desired URI rewriting + if (isset($options['currentDir'])) { + Minify_CSS_UriRewriter::$debugText = ''; + $content = Minify_CSS_UriRewriter::rewrite( + $content + ,$options['currentDir'] + ,isset($options['docRoot']) ? $options['docRoot'] : $_SERVER['DOCUMENT_ROOT'] + ,isset($options['symlinks']) ? $options['symlinks'] : array() + ); + $content = "/* Minify_CSS_UriRewriter::\$debugText\n\n" + . Minify_CSS_UriRewriter::$debugText . "*/\n" + . $content; + } + + return $content; + } + + /** + * Is the parser within a C-style comment at the end of this line? + * + * @param string $line current line of code + * + * @param bool $inComment was the parser in a comment at the + * beginning of the line? + * + * @return bool + */ + private static function _eolInComment($line, $inComment) + { + // crude way to avoid things like // */ + $line = preg_replace('~//.*?(\\*/|/\\*).*~', '', $line); + + while (strlen($line)) { + $search = $inComment + ? '*/' + : '/*'; + $pos = strpos($line, $search); + if (false === $pos) { + return $inComment; + } else { + if ($pos == 0 + || ($inComment + ? substr($line, $pos, 3) + : substr($line, $pos-1, 3)) != '*/*') + { + $inComment = ! $inComment; + } + $line = substr($line, $pos + 2); + } + } + return $inComment; + } + + /** + * Prepend a comment (or note) to the given line + * + * @param string $line current line of code + * + * @param string $note content of note/comment + * + * @param bool $inComment was the parser in a comment at the + * beginning of the line? + * + * @param int $padTo minimum width of comment + * + * @return string + */ + private static function _addNote($line, $note, $inComment, $padTo) + { + return $inComment + ? '/* ' . str_pad($note, $padTo, ' ', STR_PAD_RIGHT) . ' *| ' . $line + : '/* ' . str_pad($note, $padTo, ' ', STR_PAD_RIGHT) . ' */ ' . $line; + } +} diff --git a/public/min/lib/Minify/Loader.php b/public/min/lib/Minify/Loader.php new file mode 100644 index 0000000..6704592 --- /dev/null +++ b/public/min/lib/Minify/Loader.php @@ -0,0 +1,33 @@ + + * + * @deprecated 2.3 This will be removed in Minify 3.0 + */ +class Minify_Loader { + public function loadClass($class) + { + $file = dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR; + $file .= strtr($class, "\\_", DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR) . '.php'; + if (is_readable($file)) { + require $file; + } + } + + /** + * @deprecated 2.3 This will be removed in Minify 3.0 + */ + static public function register() + { + $inst = new self(); + spl_autoload_register(array($inst, 'loadClass')); + } +} diff --git a/public/min/lib/Minify/Logger.php b/public/min/lib/Minify/Logger.php new file mode 100644 index 0000000..6f85905 --- /dev/null +++ b/public/min/lib/Minify/Logger.php @@ -0,0 +1,47 @@ + + * + * @deprecated 2.3 This will be removed in Minify 3.0 + */ +class Minify_Logger { + + /** + * Set logger object. + * + * The object should have a method "log" that accepts a value as 1st argument and + * an optional string label as the 2nd. + * + * @param mixed $obj or a "falsey" value to disable + * @return null + */ + public static function setLogger($obj = null) { + self::$_logger = $obj + ? $obj + : null; + } + + /** + * Pass a message to the logger (if set) + * + * @param string $msg message to log + * @return null + */ + public static function log($msg, $label = 'Minify') { + if (! self::$_logger) return; + self::$_logger->log($msg, $label); + } + + /** + * @var mixed logger object (like FirePHP) or null (i.e. no logger available) + */ + private static $_logger = null; +} diff --git a/public/min/lib/Minify/Packer.php b/public/min/lib/Minify/Packer.php new file mode 100644 index 0000000..d9ac532 --- /dev/null +++ b/public/min/lib/Minify/Packer.php @@ -0,0 +1,37 @@ +pack()); + } +} diff --git a/public/min/lib/Minify/Source.php b/public/min/lib/Minify/Source.php new file mode 100644 index 0000000..72292de --- /dev/null +++ b/public/min/lib/Minify/Source.php @@ -0,0 +1,187 @@ + + */ +class Minify_Source { + + /** + * @var int time of last modification + */ + public $lastModified = null; + + /** + * @var callback minifier function specifically for this source. + */ + public $minifier = null; + + /** + * @var array minification options specific to this source. + */ + public $minifyOptions = null; + + /** + * @var string full path of file + */ + public $filepath = null; + + /** + * @var string HTTP Content Type (Minify requires one of the constants Minify::TYPE_*) + */ + public $contentType = null; + + /** + * Create a Minify_Source + * + * In the $spec array(), you can either provide a 'filepath' to an existing + * file (existence will not be checked!) or give 'id' (unique string for + * the content), 'content' (the string content) and 'lastModified' + * (unixtime of last update). + * + * As a shortcut, the controller will replace "//" at the beginning + * of a filepath with $_SERVER['DOCUMENT_ROOT'] . '/'. + * + * @param array $spec options + */ + public function __construct($spec) + { + if (isset($spec['filepath'])) { + if (0 === strpos($spec['filepath'], '//')) { + $spec['filepath'] = $_SERVER['DOCUMENT_ROOT'] . substr($spec['filepath'], 1); + } + $segments = explode('.', $spec['filepath']); + $ext = strtolower(array_pop($segments)); + switch ($ext) { + case 'js' : $this->contentType = 'application/x-javascript'; + break; + case 'css' : $this->contentType = 'text/css'; + break; + case 'htm' : // fallthrough + case 'html' : $this->contentType = 'text/html'; + break; + } + $this->filepath = $spec['filepath']; + $this->_id = $spec['filepath']; + $this->lastModified = filemtime($spec['filepath']) + // offset for Windows uploaders with out of sync clocks + + round(Minify::$uploaderHoursBehind * 3600); + } elseif (isset($spec['id'])) { + $this->_id = 'id::' . $spec['id']; + if (isset($spec['content'])) { + $this->_content = $spec['content']; + } else { + $this->_getContentFunc = $spec['getContentFunc']; + } + $this->lastModified = isset($spec['lastModified']) + ? $spec['lastModified'] + : time(); + } + if (isset($spec['contentType'])) { + $this->contentType = $spec['contentType']; + } + if (isset($spec['minifier'])) { + $this->minifier = $spec['minifier']; + } + if (isset($spec['minifyOptions'])) { + $this->minifyOptions = $spec['minifyOptions']; + } + } + + /** + * Get content + * + * @return string + */ + public function getContent() + { + $content = (null !== $this->filepath) + ? file_get_contents($this->filepath) + : ((null !== $this->_content) + ? $this->_content + : call_user_func($this->_getContentFunc, $this->_id) + ); + // remove UTF-8 BOM if present + return (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) + ? substr($content, 3) + : $content; + } + + /** + * Get id + * + * @return string + */ + public function getId() + { + return $this->_id; + } + + /** + * Verifies a single minification call can handle all sources + * + * @param array $sources Minify_Source instances + * + * @return bool true iff there no sources with specific minifier preferences. + */ + public static function haveNoMinifyPrefs($sources) + { + foreach ($sources as $source) { + if (null !== $source->minifier + || null !== $source->minifyOptions) { + return false; + } + } + return true; + } + + /** + * Get unique string for a set of sources + * + * @param array $sources Minify_Source instances + * + * @return string + */ + public static function getDigest($sources) + { + foreach ($sources as $source) { + $info[] = array( + $source->_id, $source->minifier, $source->minifyOptions + ); + } + return md5(serialize($info)); + } + + /** + * Get content type from a group of sources + * + * This is called if the user doesn't pass in a 'contentType' options + * + * @param array $sources Minify_Source instances + * + * @return string content type. e.g. 'text/css' + */ + public static function getContentType($sources) + { + foreach ($sources as $source) { + if ($source->contentType !== null) { + return $source->contentType; + } + } + return 'text/plain'; + } + + protected $_content = null; + protected $_getContentFunc = null; + protected $_id = null; +} + diff --git a/public/min/lib/Minify/YUI/CssCompressor.java b/public/min/lib/Minify/YUI/CssCompressor.java new file mode 100644 index 0000000..8cb079d --- /dev/null +++ b/public/min/lib/Minify/YUI/CssCompressor.java @@ -0,0 +1,382 @@ +/* + * YUI Compressor + * http://developer.yahoo.com/yui/compressor/ + * Author: Julien Lecomte - http://www.julienlecomte.net/ + * Author: Isaac Schlueter - http://foohack.com/ + * Author: Stoyan Stefanov - http://phpied.com/ + * Copyright (c) 2011 Yahoo! Inc. All rights reserved. + * The copyrights embodied in the content of this file are licensed + * by Yahoo! Inc. under the BSD (revised) open source license. + */ +package com.yahoo.platform.yui.compressor; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.util.regex.Pattern; +import java.util.regex.Matcher; +import java.util.ArrayList; + +public class CssCompressor { + + private StringBuffer srcsb = new StringBuffer(); + + public CssCompressor(Reader in) throws IOException { + // Read the stream... + int c; + while ((c = in.read()) != -1) { + srcsb.append((char) c); + } + } + + // Leave data urls alone to increase parse performance. + protected String extractDataUrls(String css, ArrayList preservedTokens) { + + int maxIndex = css.length() - 1; + int appendIndex = 0; + + StringBuffer sb = new StringBuffer(); + + Pattern p = Pattern.compile("url\\(\\s*([\"']?)data\\:"); + Matcher m = p.matcher(css); + + /* + * Since we need to account for non-base64 data urls, we need to handle + * ' and ) being part of the data string. Hence switching to indexOf, + * to determine whether or not we have matching string terminators and + * handling sb appends directly, instead of using matcher.append* methods. + */ + + while (m.find()) { + + int startIndex = m.start() + 4; // "url(".length() + String terminator = m.group(1); // ', " or empty (not quoted) + + if (terminator.length() == 0) { + terminator = ")"; + } + + boolean foundTerminator = false; + + int endIndex = m.end() - 1; + while(foundTerminator == false && endIndex+1 <= maxIndex) { + endIndex = css.indexOf(terminator, endIndex+1); + + if ((endIndex > 0) && (css.charAt(endIndex-1) != '\\')) { + foundTerminator = true; + if (!")".equals(terminator)) { + endIndex = css.indexOf(")", endIndex); + } + } + } + + // Enough searching, start moving stuff over to the buffer + sb.append(css.substring(appendIndex, m.start())); + + if (foundTerminator) { + String token = css.substring(startIndex, endIndex); + token = token.replaceAll("\\s+", ""); + preservedTokens.add(token); + + String preserver = "url(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___)"; + sb.append(preserver); + + appendIndex = endIndex + 1; + } else { + // No end terminator found, re-add the whole match. Should we throw/warn here? + sb.append(css.substring(m.start(), m.end())); + appendIndex = m.end(); + } + } + + sb.append(css.substring(appendIndex)); + + return sb.toString(); + } + + public void compress(Writer out, int linebreakpos) + throws IOException { + + Pattern p; + Matcher m; + String css = srcsb.toString(); + + int startIndex = 0; + int endIndex = 0; + int i = 0; + int max = 0; + ArrayList preservedTokens = new ArrayList(0); + ArrayList comments = new ArrayList(0); + String token; + int totallen = css.length(); + String placeholder; + + css = this.extractDataUrls(css, preservedTokens); + + StringBuffer sb = new StringBuffer(css); + + // collect all comment blocks... + while ((startIndex = sb.indexOf("/*", startIndex)) >= 0) { + endIndex = sb.indexOf("*/", startIndex + 2); + if (endIndex < 0) { + endIndex = totallen; + } + + token = sb.substring(startIndex + 2, endIndex); + comments.add(token); + sb.replace(startIndex + 2, endIndex, "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.size() - 1) + "___"); + startIndex += 2; + } + css = sb.toString(); + + // preserve strings so their content doesn't get accidentally minified + sb = new StringBuffer(); + p = Pattern.compile("(\"([^\\\\\"]|\\\\.|\\\\)*\")|(\'([^\\\\\']|\\\\.|\\\\)*\')"); + m = p.matcher(css); + while (m.find()) { + token = m.group(); + char quote = token.charAt(0); + token = token.substring(1, token.length() - 1); + + // maybe the string contains a comment-like substring? + // one, maybe more? put'em back then + if (token.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) { + for (i = 0, max = comments.size(); i < max; i += 1) { + token = token.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments.get(i).toString()); + } + } + + // minify alpha opacity in filter strings + token = token.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity="); + + preservedTokens.add(token); + String preserver = quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___" + quote; + m.appendReplacement(sb, preserver); + } + m.appendTail(sb); + css = sb.toString(); + + + // strings are safe, now wrestle the comments + for (i = 0, max = comments.size(); i < max; i += 1) { + + token = comments.get(i).toString(); + placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___"; + + // ! in the first position of the comment means preserve + // so push to the preserved tokens while stripping the ! + if (token.startsWith("!")) { + preservedTokens.add(token); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); + continue; + } + + // \ in the last position looks like hack for Mac/IE5 + // shorten that to /*\*/ and the next one to /**/ + if (token.endsWith("\\")) { + preservedTokens.add("\\"); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); + i = i + 1; // attn: advancing the loop + preservedTokens.add(""); + css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); + continue; + } + + // keep empty comments after child selectors (IE7 hack) + // e.g. html >/**/ body + if (token.length() == 0) { + startIndex = css.indexOf(placeholder); + if (startIndex > 2) { + if (css.charAt(startIndex - 3) == '>') { + preservedTokens.add(""); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); + } + } + } + + // in all other cases kill the comment + css = css.replace("/*" + placeholder + "*/", ""); + } + + + // Normalize all whitespace strings to single spaces. Easier to work with that way. + css = css.replaceAll("\\s+", " "); + + // Remove the spaces before the things that should not have spaces before them. + // But, be careful not to turn "p :link {...}" into "p:link{...}" + // Swap out any pseudo-class colons with the token, and then swap back. + sb = new StringBuffer(); + p = Pattern.compile("(^|\\})(([^\\{:])+:)+([^\\{]*\\{)"); + m = p.matcher(css); + while (m.find()) { + String s = m.group(); + s = s.replaceAll(":", "___YUICSSMIN_PSEUDOCLASSCOLON___"); + s = s.replaceAll( "\\\\", "\\\\\\\\" ).replaceAll( "\\$", "\\\\\\$" ); + m.appendReplacement(sb, s); + } + m.appendTail(sb); + css = sb.toString(); + // Remove spaces before the things that should not have spaces before them. + css = css.replaceAll("\\s+([!{};:>+\\(\\)\\],])", "$1"); + // bring back the colon + css = css.replaceAll("___YUICSSMIN_PSEUDOCLASSCOLON___", ":"); + + // retain space for special IE6 cases + css = css.replaceAll(":first\\-(line|letter)(\\{|,)", ":first-$1 $2"); + + // no space after the end of a preserved comment + css = css.replaceAll("\\*/ ", "*/"); + + // If there is a @charset, then only allow one, and push to the top of the file. + css = css.replaceAll("^(.*)(@charset \"[^\"]*\";)", "$2$1"); + css = css.replaceAll("^(\\s*@charset [^;]+;\\s*)+", "$1"); + + // Put the space back in some cases, to support stuff like + // @media screen and (-webkit-min-device-pixel-ratio:0){ + css = css.replaceAll("\\band\\(", "and ("); + + // Remove the spaces after the things that should not have spaces after them. + css = css.replaceAll("([!{}:;>+\\(\\[,])\\s+", "$1"); + + // remove unnecessary semicolons + css = css.replaceAll(";+}", "}"); + + // Replace 0(px,em,%) with 0. + css = css.replaceAll("([\\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)", "$1$2"); + + // Replace 0 0 0 0; with 0. + css = css.replaceAll(":0 0 0 0(;|})", ":0$1"); + css = css.replaceAll(":0 0 0(;|})", ":0$1"); + css = css.replaceAll(":0 0(;|})", ":0$1"); + + + // Replace background-position:0; with background-position:0 0; + // same for transform-origin + sb = new StringBuffer(); + p = Pattern.compile("(?i)(background-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|})"); + m = p.matcher(css); + while (m.find()) { + m.appendReplacement(sb, m.group(1).toLowerCase() + ":0 0" + m.group(2)); + } + m.appendTail(sb); + css = sb.toString(); + + // Replace 0.6 to .6, but only when preceded by : or a white-space + css = css.replaceAll("(:|\\s)0+\\.(\\d+)", "$1.$2"); + + // Shorten colors from rgb(51,102,153) to #336699 + // This makes it more likely that it'll get further compressed in the next step. + p = Pattern.compile("rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\)"); + m = p.matcher(css); + sb = new StringBuffer(); + while (m.find()) { + String[] rgbcolors = m.group(1).split(","); + StringBuffer hexcolor = new StringBuffer("#"); + for (i = 0; i < rgbcolors.length; i++) { + int val = Integer.parseInt(rgbcolors[i]); + if (val < 16) { + hexcolor.append("0"); + } + hexcolor.append(Integer.toHexString(val)); + } + m.appendReplacement(sb, hexcolor.toString()); + } + m.appendTail(sb); + css = sb.toString(); + + // Shorten colors from #AABBCC to #ABC. Note that we want to make sure + // the color is not preceded by either ", " or =. Indeed, the property + // filter: chroma(color="#FFFFFF"); + // would become + // filter: chroma(color="#FFF"); + // which makes the filter break in IE. + // We also want to make sure we're only compressing #AABBCC patterns inside { }, not id selectors ( #FAABAC {} ) + // We also want to avoid compressing invalid values (e.g. #AABBCCD to #ABCD) + p = Pattern.compile("(\\=\\s*?[\"']?)?" + "#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])" + "(:?\\}|[^0-9a-fA-F{][^{]*?\\})"); + + m = p.matcher(css); + sb = new StringBuffer(); + int index = 0; + + while (m.find(index)) { + + sb.append(css.substring(index, m.start())); + + boolean isFilter = (m.group(1) != null && !"".equals(m.group(1))); + + if (isFilter) { + // Restore, as is. Compression will break filters + sb.append(m.group(1) + "#" + m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7)); + } else { + if( m.group(2).equalsIgnoreCase(m.group(3)) && + m.group(4).equalsIgnoreCase(m.group(5)) && + m.group(6).equalsIgnoreCase(m.group(7))) { + + // #AABBCC pattern + sb.append("#" + (m.group(3) + m.group(5) + m.group(7)).toLowerCase()); + + } else { + + // Non-compressible color, restore, but lower case. + sb.append("#" + (m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7)).toLowerCase()); + } + } + + index = m.end(7); + } + + sb.append(css.substring(index)); + css = sb.toString(); + + // border: none -> border:0 + sb = new StringBuffer(); + p = Pattern.compile("(?i)(border|border-top|border-right|border-bottom|border-right|outline|background):none(;|})"); + m = p.matcher(css); + while (m.find()) { + m.appendReplacement(sb, m.group(1).toLowerCase() + ":0" + m.group(2)); + } + m.appendTail(sb); + css = sb.toString(); + + // shorter opacity IE filter + css = css.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity="); + + // Remove empty rules. + css = css.replaceAll("[^\\}\\{/;]+\\{\\}", ""); + + // TODO: Should this be after we re-insert tokens. These could alter the break points. However then + // we'd need to make sure we don't break in the middle of a string etc. + if (linebreakpos >= 0) { + // Some source control tools don't like it when files containing lines longer + // than, say 8000 characters, are checked in. The linebreak option is used in + // that case to split long lines after a specific column. + i = 0; + int linestartpos = 0; + sb = new StringBuffer(css); + while (i < sb.length()) { + char c = sb.charAt(i++); + if (c == '}' && i - linestartpos > linebreakpos) { + sb.insert(i, '\n'); + linestartpos = i; + } + } + + css = sb.toString(); + } + + // Replace multiple semi-colons in a row by a single one + // See SF bug #1980989 + css = css.replaceAll(";;+", ";"); + + // restore preserved comments and strings + for(i = 0, max = preservedTokens.size(); i < max; i++) { + css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens.get(i).toString()); + } + + // Trim the final string (for any leading or trailing white spaces) + css = css.trim(); + + // Write the output... + out.write(css); + } +} diff --git a/public/min/lib/Minify/YUI/CssCompressor.php b/public/min/lib/Minify/YUI/CssCompressor.php new file mode 100644 index 0000000..8788164 --- /dev/null +++ b/public/min/lib/Minify/YUI/CssCompressor.php @@ -0,0 +1,171 @@ ++\\(\\)\\],])@", "$1", $css); + $css = str_replace("___PSEUDOCLASSCOLON___", ":", $css); + + // Remove the spaces after the things that should not have spaces after them. + $css = preg_replace("@([!{}:;>+\\(\\[,])\\s+@", "$1", $css); + + // Add the semicolon where it's missing. + $css = preg_replace("@([^;\\}])}@", "$1;}", $css); + + // Replace 0(px,em,%) with 0. + $css = preg_replace("@([\\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)@", "$1$2", $css); + + // Replace 0 0 0 0; with 0. + $css = str_replace(":0 0 0 0;", ":0;", $css); + $css = str_replace(":0 0 0;", ":0;", $css); + $css = str_replace(":0 0;", ":0;", $css); + + // Replace background-position:0; with background-position:0 0; + $css = str_replace("background-position:0;", "background-position:0 0;", $css); + + // Replace 0.6 to .6, but only when preceded by : or a white-space + $css = preg_replace("@(:|\\s)0+\\.(\\d+)@", "$1.$2", $css); + + // Shorten colors from rgb(51,102,153) to #336699 + // This makes it more likely that it'll get further compressed in the next step. + $css = preg_replace_callback("@rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\)@", array($this, '_shortenRgbCB'), $css); + + // Shorten colors from #AABBCC to #ABC. Note that we want to make sure + // the color is not preceded by either ", " or =. Indeed, the property + // filter: chroma(color="#FFFFFF"); + // would become + // filter: chroma(color="#FFF"); + // which makes the filter break in IE. + $css = preg_replace_callback("@([^\"'=\\s])(\\s*)#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])@", array($this, '_shortenHexCB'), $css); + + // Remove empty rules. + $css = preg_replace("@[^\\}]+\\{;\\}@", "", $css); + + $linebreakpos = isset($this->_options['linebreakpos']) + ? $this->_options['linebreakpos'] + : 0; + + if ($linebreakpos > 0) { + // Some source control tools don't like it when files containing lines longer + // than, say 8000 characters, are checked in. The linebreak option is used in + // that case to split long lines after a specific column. + $i = 0; + $linestartpos = 0; + $sb = $css; + + // make sure strlen returns byte count + $mbIntEnc = null; + if (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) { + $mbIntEnc = mb_internal_encoding(); + mb_internal_encoding('8bit'); + } + $sbLength = strlen($css); + while ($i < $sbLength) { + $c = $sb[$i++]; + if ($c === '}' && $i - $linestartpos > $linebreakpos) { + $sb = substr_replace($sb, "\n", $i, 0); + $sbLength++; + $linestartpos = $i; + } + } + $css = $sb; + + // undo potential mb_encoding change + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } + } + + // Replace the pseudo class for the Box Model Hack + $css = str_replace("___PSEUDOCLASSBMH___", "\"\\\\\"}\\\\\"\"", $css); + + // Replace multiple semi-colons in a row by a single one + // See SF bug #1980989 + $css = preg_replace("@;;+@", ";", $css); + + // prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/ + $css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css); + + // Trim the final string (for any leading or trailing white spaces) + $css = trim($css); + + return $css; + } + + protected function _removeSpacesCB($m) + { + return str_replace(':', '___PSEUDOCLASSCOLON___', $m[0]); + } + + protected function _shortenRgbCB($m) + { + $rgbcolors = explode(',', $m[1]); + $hexcolor = '#'; + for ($i = 0; $i < count($rgbcolors); $i++) { + $val = round($rgbcolors[$i]); + if ($val < 16) { + $hexcolor .= '0'; + } + $hexcolor .= dechex($val); + } + return $hexcolor; + } + + protected function _shortenHexCB($m) + { + // Test for AABBCC pattern + if ((strtolower($m[3])===strtolower($m[4])) && + (strtolower($m[5])===strtolower($m[6])) && + (strtolower($m[7])===strtolower($m[8]))) { + return $m[1] . $m[2] . "#" . $m[3] . $m[5] . $m[7]; + } else { + return $m[0]; + } + } +} \ No newline at end of file diff --git a/public/min/lib/Minify/YUICompressor.php b/public/min/lib/Minify/YUICompressor.php new file mode 100644 index 0000000..283879e --- /dev/null +++ b/public/min/lib/Minify/YUICompressor.php @@ -0,0 +1,156 @@ + + * Minify_YUICompressor::$jarFile = '/path/to/yuicompressor-2.4.6.jar'; + * Minify_YUICompressor::$tempDir = '/tmp'; + * $code = Minify_YUICompressor::minifyJs( + * $code + * ,array('nomunge' => true, 'line-break' => 1000) + * ); + * + * + * Note: In case you run out stack (default is 512k), you may increase stack size in $options: + * array('stack-size' => '2048k') + * + * @todo unit tests, $options docs + * + * @package Minify + * @author Stephen Clay + */ +class Minify_YUICompressor { + + /** + * Filepath of the YUI Compressor jar file. This must be set before + * calling minifyJs() or minifyCss(). + * + * @var string + */ + public static $jarFile = null; + + /** + * Writable temp directory. This must be set before calling minifyJs() + * or minifyCss(). + * + * @var string + */ + public static $tempDir = null; + + /** + * Filepath of "java" executable (may be needed if not in shell's PATH) + * + * @var string + */ + public static $javaExecutable = 'java'; + + /** + * Minify a Javascript string + * + * @param string $js + * + * @param array $options (verbose is ignored) + * + * @see http://www.julienlecomte.net/yuicompressor/README + * + * @return string + */ + public static function minifyJs($js, $options = array()) + { + return self::_minify('js', $js, $options); + } + + /** + * Minify a CSS string + * + * @param string $css + * + * @param array $options (verbose is ignored) + * + * @see http://www.julienlecomte.net/yuicompressor/README + * + * @return string + */ + public static function minifyCss($css, $options = array()) + { + return self::_minify('css', $css, $options); + } + + private static function _minify($type, $content, $options) + { + self::_prepare(); + if (! ($tmpFile = tempnam(self::$tempDir, 'yuic_'))) { + throw new Exception('Minify_YUICompressor : could not create temp file in "'.self::$tempDir.'".'); + } + file_put_contents($tmpFile, $content); + exec(self::_getCmd($options, $type, $tmpFile), $output, $result_code); + unlink($tmpFile); + if ($result_code != 0) { + throw new Exception('Minify_YUICompressor : YUI compressor execution failed.'); + } + return implode("\n", $output); + } + + private static function _getCmd($userOptions, $type, $tmpFile) + { + $o = array_merge( + array( + 'charset' => '' + ,'line-break' => 5000 + ,'type' => $type + ,'nomunge' => false + ,'preserve-semi' => false + ,'disable-optimizations' => false + ,'stack-size' => '' + ) + ,$userOptions + ); + $cmd = self::$javaExecutable + . (!empty($o['stack-size']) + ? ' -Xss' . $o['stack-size'] + : '') + . ' -jar ' . escapeshellarg(self::$jarFile) + . " --type {$type}" + . (preg_match('/^[\\da-zA-Z0-9\\-]+$/', $o['charset']) + ? " --charset {$o['charset']}" + : '') + . (is_numeric($o['line-break']) && $o['line-break'] >= 0 + ? ' --line-break ' . (int)$o['line-break'] + : ''); + if ($type === 'js') { + foreach (array('nomunge', 'preserve-semi', 'disable-optimizations') as $opt) { + $cmd .= $o[$opt] + ? " --{$opt}" + : ''; + } + } + return $cmd . ' ' . escapeshellarg($tmpFile); + } + + private static function _prepare() + { + if (! is_file(self::$jarFile)) { + throw new Exception('Minify_YUICompressor : $jarFile('.self::$jarFile.') is not a valid file.'); + } + if (! is_readable(self::$jarFile)) { + throw new Exception('Minify_YUICompressor : $jarFile('.self::$jarFile.') is not readable.'); + } + if (! is_dir(self::$tempDir)) { + throw new Exception('Minify_YUICompressor : $tempDir('.self::$tempDir.') is not a valid direcotry.'); + } + if (! is_writable(self::$tempDir)) { + throw new Exception('Minify_YUICompressor : $tempDir('.self::$tempDir.') is not writable.'); + } + } +} + diff --git a/public/min/lib/MrClay/Cli.php b/public/min/lib/MrClay/Cli.php new file mode 100644 index 0000000..66eed38 --- /dev/null +++ b/public/min/lib/MrClay/Cli.php @@ -0,0 +1,384 @@ +values. + * + * You may also specify that some arguments be used to provide input/output. By communicating + * solely through the file pointers provided by openInput()/openOutput(), you can make your + * app more flexible to end users. + * + * @author Steve Clay + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Cli { + + /** + * @var array validation errors + */ + public $errors = array(); + + /** + * @var array option values available after validation. + * + * E.g. array( + * 'a' => false // option was missing + * ,'b' => true // option was present + * ,'c' => "Hello" // option had value + * ,'f' => "/home/user/file" // file path from root + * ,'f.raw' => "~/file" // file path as given to option + * ) + */ + public $values = array(); + + /** + * @var array + */ + public $moreArgs = array(); + + /** + * @var array + */ + public $debug = array(); + + /** + * @var bool The user wants help info + */ + public $isHelpRequest = false; + + /** + * @var Arg[] + */ + protected $_args = array(); + + /** + * @var resource + */ + protected $_stdin = null; + + /** + * @var resource + */ + protected $_stdout = null; + + /** + * @param bool $exitIfNoStdin (default true) Exit() if STDIN is not defined + */ + public function __construct($exitIfNoStdin = true) + { + if ($exitIfNoStdin && ! defined('STDIN')) { + exit('This script is for command-line use only.'); + } + if (isset($GLOBALS['argv'][1]) + && ($GLOBALS['argv'][1] === '-?' || $GLOBALS['argv'][1] === '--help')) { + $this->isHelpRequest = true; + } + } + + /** + * @param Arg|string $letter + * @return Arg + */ + public function addOptionalArg($letter) + { + return $this->addArgument($letter, false); + } + + /** + * @param Arg|string $letter + * @return Arg + */ + public function addRequiredArg($letter) + { + return $this->addArgument($letter, true); + } + + /** + * @param string $letter + * @param bool $required + * @param Arg|null $arg + * @return Arg + * @throws InvalidArgumentException + */ + public function addArgument($letter, $required, Arg $arg = null) + { + if (! preg_match('/^[a-zA-Z]$/', $letter)) { + throw new InvalidArgumentException('$letter must be in [a-zA-Z]'); + } + if (! $arg) { + $arg = new Arg($required); + } + $this->_args[$letter] = $arg; + return $arg; + } + + /** + * @param string $letter + * @return Arg|null + */ + public function getArgument($letter) + { + return isset($this->_args[$letter]) ? $this->_args[$letter] : null; + } + + /* + * Read and validate options + * + * @return bool true if all options are valid + */ + public function validate() + { + $options = ''; + $this->errors = array(); + $this->values = array(); + $this->_stdin = null; + + if ($this->isHelpRequest) { + return false; + } + + $lettersUsed = ''; + foreach ($this->_args as $letter => $arg) { + /* @var Arg $arg */ + $options .= $letter; + $lettersUsed .= $letter; + + if ($arg->mayHaveValue || $arg->mustHaveValue) { + $options .= ($arg->mustHaveValue ? ':' : '::'); + } + } + + $this->debug['argv'] = $GLOBALS['argv']; + $argvCopy = array_slice($GLOBALS['argv'], 1); + $o = getopt($options); + $this->debug['getopt_options'] = $options; + $this->debug['getopt_return'] = $o; + + foreach ($this->_args as $letter => $arg) { + /* @var Arg $arg */ + $this->values[$letter] = false; + if (isset($o[$letter])) { + if (is_bool($o[$letter])) { + + // remove from argv copy + $k = array_search("-$letter", $argvCopy); + if ($k !== false) { + array_splice($argvCopy, $k, 1); + } + + if ($arg->mustHaveValue) { + $this->addError($letter, "Missing value"); + } else { + $this->values[$letter] = true; + } + } else { + // string + $this->values[$letter] = $o[$letter]; + $v =& $this->values[$letter]; + + // remove from argv copy + // first look for -ovalue or -o=value + $pattern = "/^-{$letter}=?" . preg_quote($v, '/') . "$/"; + $foundInArgv = false; + foreach ($argvCopy as $k => $argV) { + if (preg_match($pattern, $argV)) { + array_splice($argvCopy, $k, 1); + $foundInArgv = true; + break; + } + } + if (! $foundInArgv) { + // space separated + $k = array_search("-$letter", $argvCopy); + if ($k !== false) { + array_splice($argvCopy, $k, 2); + } + } + + // check that value isn't really another option + if (strlen($lettersUsed) > 1) { + $pattern = "/^-[" . str_replace($letter, '', $lettersUsed) . "]/i"; + if (preg_match($pattern, $v)) { + $this->addError($letter, "Value was read as another option: %s", $v); + return false; + } + } + if ($arg->assertFile || $arg->assertDir) { + if ($v[0] !== '/' && $v[0] !== '~') { + $this->values["$letter.raw"] = $v; + $v = getcwd() . "/$v"; + } + } + if ($arg->assertFile) { + if ($arg->useAsInfile) { + $this->_stdin = $v; + } elseif ($arg->useAsOutfile) { + $this->_stdout = $v; + } + if ($arg->assertReadable && ! is_readable($v)) { + $this->addError($letter, "File not readable: %s", $v); + continue; + } + if ($arg->assertWritable) { + if (is_file($v)) { + if (! is_writable($v)) { + $this->addError($letter, "File not writable: %s", $v); + } + } else { + if (! is_writable(dirname($v))) { + $this->addError($letter, "Directory not writable: %s", dirname($v)); + } + } + } + } elseif ($arg->assertDir && $arg->assertWritable && ! is_writable($v)) { + $this->addError($letter, "Directory not readable: %s", $v); + } + } + } else { + if ($arg->isRequired()) { + $this->addError($letter, "Missing"); + } + } + } + $this->moreArgs = $argvCopy; + reset($this->moreArgs); + return empty($this->errors); + } + + /** + * Get the full paths of file(s) passed in as unspecified arguments + * + * @return array + */ + public function getPathArgs() + { + $r = $this->moreArgs; + foreach ($r as $k => $v) { + if ($v[0] !== '/' && $v[0] !== '~') { + $v = getcwd() . "/$v"; + $v = str_replace('/./', '/', $v); + do { + $v = preg_replace('@/[^/]+/\\.\\./@', '/', $v, 1, $changed); + } while ($changed); + $r[$k] = $v; + } + } + return $r; + } + + /** + * Get a short list of errors with options + * + * @return string + */ + public function getErrorReport() + { + if (empty($this->errors)) { + return ''; + } + $r = "Some arguments did not pass validation:\n"; + foreach ($this->errors as $letter => $arr) { + $r .= " $letter : " . implode(', ', $arr) . "\n"; + } + $r .= "\n"; + return $r; + } + + /** + * @return string + */ + public function getArgumentsListing() + { + $r = "\n"; + foreach ($this->_args as $letter => $arg) { + /* @var Arg $arg */ + $desc = $arg->getDescription(); + $flag = " -$letter "; + if ($arg->mayHaveValue) { + $flag .= "[VAL]"; + } elseif ($arg->mustHaveValue) { + $flag .= "VAL"; + } + if ($arg->assertFile) { + $flag = str_replace('VAL', 'FILE', $flag); + } elseif ($arg->assertDir) { + $flag = str_replace('VAL', 'DIR', $flag); + } + if ($arg->isRequired()) { + $desc = "(required) $desc"; + } + $flag = str_pad($flag, 12, " ", STR_PAD_RIGHT); + $desc = wordwrap($desc, 70); + $r .= $flag . str_replace("\n", "\n ", $desc) . "\n\n"; + } + return $r; + } + + /** + * Get resource of open input stream. May be STDIN or a file pointer + * to the file specified by an option with 'STDIN'. + * + * @return resource + */ + public function openInput() + { + if (null === $this->_stdin) { + return STDIN; + } else { + $this->_stdin = fopen($this->_stdin, 'rb'); + return $this->_stdin; + } + } + + public function closeInput() + { + if (null !== $this->_stdin) { + fclose($this->_stdin); + } + } + + /** + * Get resource of open output stream. May be STDOUT or a file pointer + * to the file specified by an option with 'STDOUT'. The file will be + * truncated to 0 bytes on opening. + * + * @return resource + */ + public function openOutput() + { + if (null === $this->_stdout) { + return STDOUT; + } else { + $this->_stdout = fopen($this->_stdout, 'wb'); + return $this->_stdout; + } + } + + public function closeOutput() + { + if (null !== $this->_stdout) { + fclose($this->_stdout); + } + } + + /** + * @param string $letter + * @param string $msg + * @param string $value + */ + protected function addError($letter, $msg, $value = null) + { + if ($value !== null) { + $value = var_export($value, 1); + } + $this->errors[$letter][] = sprintf($msg, $value); + } +} + diff --git a/public/min/lib/MrClay/Cli/Arg.php b/public/min/lib/MrClay/Cli/Arg.php new file mode 100644 index 0000000..651255b --- /dev/null +++ b/public/min/lib/MrClay/Cli/Arg.php @@ -0,0 +1,183 @@ +values['f.raw'] + * + * Use assertReadable()/assertWritable() to cause the validator to test the file/dir for + * read/write permissions respectively. + * + * @method \MrClay\Cli\Arg mayHaveValue() Assert that the argument, if present, may receive a string value + * @method \MrClay\Cli\Arg mustHaveValue() Assert that the argument, if present, must receive a string value + * @method \MrClay\Cli\Arg assertFile() Assert that the argument's value must specify a file + * @method \MrClay\Cli\Arg assertDir() Assert that the argument's value must specify a directory + * @method \MrClay\Cli\Arg assertReadable() Assert that the specified file/dir must be readable + * @method \MrClay\Cli\Arg assertWritable() Assert that the specified file/dir must be writable + * + * @property-read bool mayHaveValue + * @property-read bool mustHaveValue + * @property-read bool assertFile + * @property-read bool assertDir + * @property-read bool assertReadable + * @property-read bool assertWritable + * @property-read bool useAsInfile + * @property-read bool useAsOutfile + * + * @author Steve Clay + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Arg { + /** + * @return array + */ + public function getDefaultSpec() + { + return array( + 'mayHaveValue' => false, + 'mustHaveValue' => false, + 'assertFile' => false, + 'assertDir' => false, + 'assertReadable' => false, + 'assertWritable' => false, + 'useAsInfile' => false, + 'useAsOutfile' => false, + ); + } + + /** + * @var array + */ + protected $spec = array(); + + /** + * @var bool + */ + protected $required = false; + + /** + * @var string + */ + protected $description = ''; + + /** + * @param bool $isRequired + */ + public function __construct($isRequired = false) + { + $this->spec = $this->getDefaultSpec(); + $this->required = (bool) $isRequired; + if ($isRequired) { + $this->spec['mustHaveValue'] = true; + } + } + + /** + * Assert that the argument's value points to a writable file. When + * Cli::openOutput() is called, a write pointer to this file will + * be provided. + * @return Arg + */ + public function useAsOutfile() + { + $this->spec['useAsOutfile'] = true; + return $this->assertFile()->assertWritable(); + } + + /** + * Assert that the argument's value points to a readable file. When + * Cli::openInput() is called, a read pointer to this file will + * be provided. + * @return Arg + */ + public function useAsInfile() + { + $this->spec['useAsInfile'] = true; + return $this->assertFile()->assertReadable(); + } + + /** + * @return array + */ + public function getSpec() + { + return $this->spec; + } + + /** + * @param string $desc + * @return Arg + */ + public function setDescription($desc) + { + $this->description = $desc; + return $this; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * @return bool + */ + public function isRequired() + { + return $this->required; + } + + /** + * Note: magic methods declared in class PHPDOC + * + * @param string $name + * @param array $args + * @return Arg + * @throws BadMethodCallException + */ + public function __call($name, array $args = array()) + { + if (array_key_exists($name, $this->spec)) { + $this->spec[$name] = true; + if ($name === 'assertFile' || $name === 'assertDir') { + $this->spec['mustHaveValue'] = true; + } + } else { + throw new BadMethodCallException('Method does not exist'); + } + return $this; + } + + /** + * Note: magic properties declared in class PHPDOC + * + * @param string $name + * @return bool|null + */ + public function __get($name) + { + if (array_key_exists($name, $this->spec)) { + return $this->spec[$name]; + } + return null; + } +} diff --git a/public/min/server-info.php b/public/min/server-info.php new file mode 100644 index 0000000..d7bf1c3 --- /dev/null +++ b/public/min/server-info.php @@ -0,0 +1,25 @@ + + * + * + * + * + * + * @param mixed $keyOrFiles a group key or array of file paths/URIs + * @param array $opts options: + * 'farExpires' : (default true) append a modified timestamp for cache revving + * 'debug' : (default false) append debug flag + * 'charset' : (default 'UTF-8') for htmlspecialchars + * 'minAppUri' : (default '/min') URI of min directory + * 'rewriteWorks' : (default true) does mod_rewrite work in min app? + * 'groupsConfigFile' : specify if different + * @return string + */ +function Minify_getUri($keyOrFiles, $opts = array()) +{ + return Minify_HTML_Helper::getUri($keyOrFiles, $opts); +} + + +/** + * Get the last modification time of several source js/css files. If you're + * caching the output of Minify_getUri(), you might want to know if one of the + * dependent source files has changed so you can update the HTML. + * + * Since this makes a bunch of stat() calls, you might not want to check this + * on every request. + * + * @param array $keysAndFiles group keys and/or file paths/URIs. + * @return int latest modification time of all given keys/files + */ +function Minify_mtime($keysAndFiles, $groupsConfigFile = null) +{ + $gc = null; + if (! $groupsConfigFile) { + $groupsConfigFile = dirname(__FILE__) . '/groupsConfig.php'; + } + $sources = array(); + foreach ($keysAndFiles as $keyOrFile) { + if (is_object($keyOrFile) + || 0 === strpos($keyOrFile, '/') + || 1 === strpos($keyOrFile, ':\\')) { + // a file/source obj + $sources[] = $keyOrFile; + } else { + if (! $gc) { + $gc = (require $groupsConfigFile); + } + foreach ($gc[$keyOrFile] as $source) { + $sources[] = $source; + } + } + } + return Minify_HTML_Helper::getLastModified($sources); +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..a82d96e --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/public/router.php b/public/router.php new file mode 100644 index 0000000..a87f898 --- /dev/null +++ b/public/router.php @@ -0,0 +1,17 @@ + +// +---------------------------------------------------------------------- +// $Id$ + +if (is_file($_SERVER["DOCUMENT_ROOT"] . $_SERVER["SCRIPT_NAME"])) { + return false; +} else { + require __DIR__ . "/index.php"; +} diff --git a/public/static/.gitignore b/public/static/.gitignore new file mode 100644 index 0000000..b722e9e --- /dev/null +++ b/public/static/.gitignore @@ -0,0 +1 @@ +!.gitignore \ No newline at end of file diff --git a/public/static/LICENSE.txt b/public/static/LICENSE.txt new file mode 100644 index 0000000..2255550 --- /dev/null +++ b/public/static/LICENSE.txt @@ -0,0 +1,27 @@ +版权所有 (c) 2016~2017,河源市卓锐科技有限公司保留所有权利。 + +DolphinPHP 用户协议 + +版权所有 (c) 2016~2017,河源市卓锐科技有限公司保留所有权利。 + +感谢您选择DolphinPHP,希望我们的努力能为您提供一个极简极致极速的PHP快速开发解决方案。 + +用户须知:本协议是您与河源市卓锐科技有限公司(以下简称卓锐公司)之间关于您使用DolphinPHP产品及服务的法律协议。无论您是个人或组织、盈利与否、用途如何(包括以学习和研究为目的),均需仔细阅读本协议,包括免除或者限制卓锐公司责任的免责条款及对您的权利限制。请您审阅并接受或不接受本服务条款。如您不同意本服务条款及/或卓锐公司随时对其的修改,您应不使用或主动取消DolphinPHP产品。否则,您的任何对DolphinPHP的相关服务的注册、登陆、下载、查看等使用行为将被视为您对本服务条款全部的完全接受,包括接受卓锐公司对服务条款随时所做的任何修改。 + +本服务条款一旦发生变更, 卓锐公司将在产品官网上公布修改内容。修改后的服务条款一旦在网站公布即有效代替原来的服务条款。您可随时登陆官网查阅最新版服务条款。如果您选择接受本条款,即表示您同意接受协议各项条件的约束。如果您不同意本服务条款,则不能获得使用本服务的权利。您若有违反本条款规定,卓锐公司有权随时中止或终止您对DolphinPHP产品的使用资格并保留追究相关法律责任的权利。 + +在理解、同意、并遵守本协议的全部条款后,方可开始使用DolphinPHP产品。您也可能与卓锐公司直接签订另一书面协议,以补充或者取代本协议的全部或者任何部分。 + +卓锐公司拥有DolphinPHP的全部知识产权,包括商标和著作权。本软件只供许可协议,并非出售。卓锐公司只允许您在遵守本协议各项条款的情况下复制、下载、安装、使用或者以其他方式受益于本软件的功能或者知识产权。 + +个人非商业用途可免费使用(但不包括其衍生产品、插件或者服务),也可以根据个人实际情况选择是否购买授权。 + +非个人用户(泛指非个人的团体,如企业、政府单位、教育机构、协会团体、厂矿、工作室等)必须购买软件授权后方可使用。 + +您可以在协议规定的约束和限制范围内修改DolphinPHP以适应您的网站要求,但免费版必须保留软件版本信息的正常显示及相关连接正常。 + +禁止去除DolphinPHP源码里的版权信息,商业授权版本可去除后台界面及前台界面的相关版权信息。 + +禁止在DolphinPHP整体或任何部分基础上发展任何派生版本、修改版本或第三方版本用于重新分发。 + +未经官方许可,不得对本软件或与之关联的商业授权进行出租、出售、抵押或发放子许可证。 \ No newline at end of file diff --git a/public/static/admin/css/README.md b/public/static/admin/css/README.md new file mode 100644 index 0000000..18b93c0 --- /dev/null +++ b/public/static/admin/css/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 系统样式目录 diff --git a/public/static/admin/css/bootstrap.min.css b/public/static/admin/css/bootstrap.min.css new file mode 100644 index 0000000..577d777 --- /dev/null +++ b/public/static/admin/css/bootstrap.min.css @@ -0,0 +1,5 @@ +/*! + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} \ No newline at end of file diff --git a/public/static/admin/css/custom.css b/public/static/admin/css/custom.css new file mode 100644 index 0000000..f3cf043 --- /dev/null +++ b/public/static/admin/css/custom.css @@ -0,0 +1,3 @@ +/*! +* 后台自定义css +*/ \ No newline at end of file diff --git a/public/static/admin/css/dolphin.css b/public/static/admin/css/dolphin.css new file mode 100644 index 0000000..47998cf --- /dev/null +++ b/public/static/admin/css/dolphin.css @@ -0,0 +1,697 @@ +/*! +* Dolphin CSS - v1.0.2 +* @author caiweiming +*/ +body, +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6, +.select2-container--default .select2-search--inline .select2-search__field{ + font-family: "Microsoft Yahei", "Helvetica Neue", Helvetica, Arial, sans-serif; +} +.css-input input + span { + margin-top: -3px; +} +#main-container{ + overflow: visible; +} +/* 后台logo */ +.logo, .logo-text { + max-height: 34px; +} +.dolphin-header { + padding: 8px 10px 1px 6px; +} +.text-modern { + color: #1F98CA; +} +/* 顶部导航 */ +#header-navbar { + min-height: 50px; +} +.content-mini.content-mini-full { + padding: 0 10px 0 0; +} +.nav-header { + margin-top: 6px; +} +.nav-pills > li > a { + font-weight: normal;; + padding: 15px 17px; + border-radius: 0; +} +.side-header{ + min-height: 50px; +} +.header-navbar-fixed #main-container{ + padding-top: 50px; + background: #f1f1f1; +} +#page-footer{ + padding: 13px 30px 13px; +} +.btn, +.form-control{ + border-radius: 0; +} +.notice-circle { + position: absolute; + right: 25px; + top: -2px; + font-size: 12px; +} +/* 表单元素 */ +.form-group .css-checkbox { + margin-right: 10px; +} +.help-block { + font-size: 12px; + font-style: normal; +} +div.tagsinput { + padding: 6px 12px 1px 6px; +} + +/* 文件上传 */ +.file-item.thumbnail{ + display: inline-block; + position: relative; + margin-right:15px; + float: left; +} +.file-item.thumbnail .info{ + display: none; +} +.file-item.thumbnail .remove-picture{ + position: absolute; + right: -5px; + top: -5px; + font-size: 18px; + color: #CA4949; + cursor: pointer; + display: none; + z-index: 5; +} +.file-item.thumbnail .remove-picture:hover{ + color: #EC6969; +} +.file-item.thumbnail .move-picture{ + background: gainsboro; + padding: 2px; + cursor: move; + color: #ABABAB; + position: absolute; + left: -6px; + top: -5px; + border-radius: 50%; + display: none; + z-index: 5; +} +.file-item.thumbnail:hover .remove-picture, +.file-item.thumbnail:hover .move-picture{ + display: block; +} +.file-item .progress.progress-xs{ + margin-bottom: 5px; + margin-top: 5px; +} +.file-item .error, +.file-item .img-state{ + position: absolute; + top: 4px; + left: 4px; + right: 4px; + color: white; + text-align: center; + height: 20px; + font-size: 14px; + line-height: 20px; +} +.file-item .error{ + background: rgba(255, 0, 0, 0.67); +} +.file-item .success{ + background: rgba(25, 167, 75, 0.78); +} +.uploader-list .list-group-item img{ + width: 40px; + height: 40px; +} +.file-item .remove-file{ + color: #CA4949; + cursor: pointer; +} +.file-item .remove-file:hover{ + color: #EC6969; +} +.file-item .fa-check { + color: #68C39F; + margin-top: 3px; +} + +/* 图标选择 */ +.js-icon-picker .input-group-addon { + cursor: pointer; +} +#icon_search { + padding: 15px; +} +#icon_tab .nav-tabs { + margin-top: 20px; + margin-bottom: 20px; + padding: 0 15px; +} +#icon_tab i { + font-size: 2em; +} +.js-icon-list { + padding-left: 0; + padding-bottom: 1px; + margin-bottom: 20px; + list-style: none; + overflow: hidden; +} +.js-icon-list li{ + float: left; + width: 5%; + padding: 15px; + margin: 0 -1px -1px 0; + font-size: 12px; + line-height: 1.4; + text-align: center; + border: 1px solid #ddd; + cursor: pointer; +} +.js-icon-list li:hover { + background-color: #F5F5F5; +} +.js-icon-list li code { + display: none; +} + +/* 加载层 */ +#loading { + position: fixed; + width: 100%; + height:100%; + z-index: 20180101; + background: rgba(255, 255, 255, 0.16); +} +#loading .loading-box { + background-color: rgba(0, 0, 0, 0.62); + z-index: 20180102; + position: fixed; + padding: 10px; + border-radius: 4px; + margin-left: -50px; + margin-top: -24px; + color: #FFF; + left: 50%; + top: 50%; +} +#loading i { + float: left; + margin-right: 5px; +} +#loading .loding-text { + margin-top: 3px; + display: inline-block; +} + +/* markdown编辑器 */ +.editormd-fullscreen { + z-index: 9999; +} + +/* 联动下拉 */ +.select-box { + float: left; + padding: 0 0 0 15px; +} + +/* 分组 */ +.block .block-content.block-group { + overflow: visible; +} + +/* 表格 */ +.table{ + margin-bottom: 0; +} +.table-cell{ + overflow: hidden; + text-overflow: ellipsis; + word-break: break-all; + box-sizing: border-box; + min-height: 22px; +} +.data-table-toolbar { + margin-bottom: 10px; +} +.builder-table-wrapper{ + position: relative; + border: 1px solid #E9E9E9; + border-bottom: 0; + border-right: 0; + margin-bottom: 20px; +} +.builder-table{ + width: inherit; + height: 100%; + max-width: 100%; + overflow: inherit; + border-right: 1px solid #E9E9E9; + border-bottom: 1px solid #E9E9E9; + position: relative; +} +.builder-table:before{ + width: 100%; + height: 1px; + left: 0; + bottom: 0; + z-index: 1; +} +.builder-table-head{ + overflow: hidden; + position: relative; + z-index: 1; +} +.builder-table-body{ + overflow: auto; + margin-top: -1px; +} +#builder-table-main { + margin-bottom: -1px; +} +.builder-table-left{ + position: absolute; + left: 0; + top: 0; + box-shadow: 2px 0 6px -2px rgba(0,0,0,.2); +} +.builder-table-left:before, +.builder-table-right:before{ + content: ""; + width: 100%; + height: 1px; + background-color: #dddee1; + position: absolute; + left: 0; + bottom: 0; + z-index: 4; +} +.builder-table-left-head, +.builder-table-right-head{ + overflow: hidden; +} +.builder-table-left-body{ + overflow: hidden; + position: relative; + z-index: 3; +} +.builder-table-right{ + position: absolute; + right: 0; + left: auto; + top: 0; + box-shadow: -2px 0 6px -2px rgba(0,0,0,.2); + overflow: hidden; + height: 50px; +} +.builder-table-head table, +.builder-table-left-head table, +.builder-table-left-body table, +.builder-table-right-head table, +.builder-table-right-body-inner table, +#builder-table-main{ + table-layout: fixed; +} +.builder-table-right-head{ + position: absolute; + right: 0; +} +.builder-table-right-body{ + overflow: hidden; + position: relative; + z-index: 3; + margin-top: 50px; +} +.builder-table-right-body-inner{ + position: absolute; + right: 0; +} +.builder-table-right-header{ + position: absolute; + top: -1px; + right: 0; + background-color: #f8f8f9; + width: 17px; + height: 52px; + border-top: 1px solid #E9E9E9; +} +.table>caption+thead>tr:first-child>td, +.table>caption+thead>tr:first-child>th, +.table>colgroup+thead>tr:first-child>td, +.table>colgroup+thead>tr:first-child>th, +.table>thead:first-child>tr:first-child>td, +.table>thead:first-child>tr:first-child>th{ + border-left: 0; + border-top: 0; +} +.table>colgroup+thead>tr:first-child>th:last-child{ + border-right: 0; +} +.table-bordered > tbody > tr > td:first-child{ + border-left: 0; +} +.table-bordered > tbody > tr > td:last-child{ + border-right: 0; +} +.table>thead:first-child>tr:first-child>th:last-child{ + border-right: 0; +} +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr.active:hover > th, +.table-hover > tbody > tr:hover { + background-color: #FFFCEF; +} +.js-table-checkable tbody tr, .js-table-sections-header > tr { + cursor: default; +} +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th { + font-family: "Microsoft Yahei", "Source Sans Pro", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + white-space: nowrap; +} +.table > thead > tr > th > .pull-right { + margin-right: -40px; +} +.table-builder{ + border-left: 0; + border-right: 0; + border-top:0; + background: #fff; +} +.table-builder .css-checkbox { + margin: 2px 0; +} +.table-builder .switch.switch-sm { + margin: 0; +} +.table-builder thead .fa { + padding-left: 5px; +} +.table-builder > tbody > tr > td { + vertical-align: middle; + padding: 10px; + min-width: 0; + height: 48px; + box-sizing: border-box; + text-overflow: ellipsis; +} +.table-builder > tbody > tr > td .image { + height: 40px; +} +.table-empty .empty-info{ + background: #fff; + padding: 100px 0; + font-size: 20px; + color: #C7C7C7; +} +.table-hover > tbody > tr.hover{ + background-color: #FFFCEF; +} +.pagination { + margin: 0; +} +.pagination-info { + line-height: 30px; +} +.data-table-toolbar .go-page{ + width: 45px; + text-align: center; + float: left; + margin-right: 5px; +} +.data-table-toolbar .nums { + float: right; + width: 45px; + text-align: center; + margin-left: 5px; +} +.data-table-toolbar .nums-info { + margin-top: 6px; + margin-right: 5px; +} +.search-bar { + width: 280px; +} +@media screen and (max-width: 425px){ + .search-bar { + width: 100%; + margin-bottom: 10px; + float: none!important; + } +} + +.btn-group-vertical>.btn, +.btn-group>.btn{ + float: none; +} + +/* 菜单页面 */ +#menu_list { + margin-bottom: 20px; +} +.dd3-handle { + background: #ECECEC; + cursor: move; +} +.dd3-handle:before { + color: #A0A0A0; +} +.dd3-content { + background: #f9f9f9; + color: #5A5A5A; +} +.dd3-content:hover { + background: #ECECEC; + color: #5A5A5A; +} +.dd3-content .link { + margin-left: 10px; + font-weight: normal; +} +.dd3-content .action { + display: inline-block; + margin-left: 10px; +} +.dd3-content .list-icon{ + color: #5A5A5A; + margin-left: 10px; + display: none; +} +.dd3-content .list-icon:hover, +.dd-disable .list-icon:hover{ + color: #9A9A9A; +} +.dd-disable .dd3-content{ + background: #FFD5D0; + color: #D2847B; +} +.dd-disable .dd3-content:hover{ + background: #FDC7C1; +} +.dd3-content:hover .list-icon{ + display: inline; +} +.dd-disable .dd3-handle{ + background: #FDC7C1; +} +.dd-disable .dd3-handle:hover{ + background: #E49D9D; +} +.dd3-item > button[data-action="collapse"], +.dd3-item > button[data-action="expand"] { + color: #5A5A5A; +} +.dd-placeholder, +.dd-empty { + background: #FFFFF3; + border: 1px dashed #F3DDC6; +} + +.connectedSortable { + overflow: hidden; +} +.connectedSortable .sortable-item { + border: 1px solid #DADADA; + margin-right: 10px; + padding: 8px 10px; + background: #F7F7F7; + cursor: move; +} +.connectedSortable .sortable-item:hover { + background: #EFEFEF; +} + +/* 模块、插件 */ +.module-list h3 { + font-weight: 300; + font-size: 24px; +} + +/* 访问授权 */ +.auth-node {} +.auth-node-top { + padding: 8px 20px; +} +.auth-node-parent { + margin-bottom: 20px; +} +.auth-node-child { + margin-left: 20px; +} +.auth-node-child .auth-node-child { + margin-left: 20px; + margin-bottom: 10px; +} + +/* 数据授权 */ +table.treetable { + border: none; +} +table.treetable tr.branch { + background: none; +} + +/* select2 */ +.select2-selection__rendered { + min-width: 200px; +} +.select2-container--open { + z-index: 1100; +} +.select2-container--default .select2-results__option[aria-selected=true] { + display: none; +} +.select2-container .select2-dropdown { + margin-top: -1px; +} +.select2-container--default .select2-selection--multiple .select2-selection__rendered, +.select2-container--default.select2-container--focus .select2-selection--multiple .select2-selection__rendered{ + padding-left: 5px; +} +.select2-container--default .select2-search--inline .select2-search__field{ + margin-left: 6px; +} + +.uploader-list .danger { + color: #d26a5c; +} + +/* sweetalert */ +.sweet-alert p { + color: #BB2C2C; + font-family: "Microsoft Yahei", "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +/* 隐藏表单项 */ +.form_group_hide { + height: 0; + overflow: hidden; + margin: 0; +} +.form_group_hide .btn{ + height:0; +} + +/* 百度地图 */ +.bmap{ + width:100%; + height:500px; + border: 1px solid #ccc; +} +.searchResultPanel{ + border:1px solid #C0C0C0; + width:150px; + height:auto; + display:none; +} + +/* 图片裁剪 */ +.jcrop-active, +.jcrop-preview-parent{ + display: inline-block; +} +.jcrop-img { + width: 750px; + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC"); + text-align: center; + line-height: 0; +} +.jcrop-preview-parent{ + width:100px; + height:100px; + overflow:hidden; + float: right; +} + +/* 图片上传 */ +.img-link{ + display: inline; +} + +/* 时间段搜索 */ +.toolbar-btn-action .time-filter { + display: inline-block; +} +.toolbar-btn-action .input-daterange { + width: 250px; +} + +/* 表单按钮 */ +.form-btn-parent { + margin-top: 22px; +} +.form-btn { + margin-bottom: 23px; +} +.col-md-12 > .form-btn-parent, +.col-md-12 > .form-btn-parent > .form-btn{ + margin: 0; +} + +.breadcrumb { + padding:10px 14px; + font-weight: normal; + border-radius: 0; +} + +@media screen and (min-width: 768px){ + /* 面包屑导航 */ + .breadcrumb { + padding: 10px 30px; + font-weight: normal; + border-radius: 0; + } +} + +.js-gallery>img, +.gallery-list .thumbnail>img, +.uploader-list .thumbnail>img { + cursor: zoom-in; +} \ No newline at end of file diff --git a/public/static/admin/css/oneui.css b/public/static/admin/css/oneui.css new file mode 100644 index 0000000..ad53b0a --- /dev/null +++ b/public/static/admin/css/oneui.css @@ -0,0 +1,11643 @@ +/*! +* OneUI - v2.2.0 - Auto-compiled on 2016-07-13 - Copyright 2016 +* @author pixelcave +*/ +html, +body { + height: 100%; +} +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + color: #646464; + background-color: #f5f5f5; +} +.no-focus *:focus { + outline: 0 !important; +} +a { + color: #5c90d2; + -webkit-transition: color 0.12s ease-out; + transition: color 0.12s ease-out; +} +a.link-effect { + position: relative; +} +a.link-effect:before { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + content: ""; + background-color: #3169b1; + visibility: hidden; + -webkit-transform: scaleX(0); + -ms-transform: scaleX(0); + transform: scaleX(0); + -webkit-transition: -webkit-transform 0.12s ease-out; + transition: transform 0.12s ease-out; +} +a:hover, +a:focus { + color: #3169b1; + text-decoration: none; +} +a:hover.link-effect:before, +a:focus.link-effect:before { + visibility: visible; + -webkit-transform: scaleX(1); + -ms-transform: scaleX(1); + transform: scaleX(1); +} +a:active { + color: #5c90d2; +} +a.inactive { + cursor: not-allowed; +} +a.inactive:focus { + background-color: transparent !important; +} +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + margin: 0; + font-family: "Source Sans Pro", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 600; + line-height: 1.2; + color: inherit; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small, +h1 .small, +h2 .small, +h3 .small, +h4 .small, +h5 .small, +h6 .small, +.h1 .small, +.h2 .small, +.h3 .small, +.h4 .small, +.h5 .small, +.h6 .small { + font-weight: 600; + font-size: 85%; + color: #777; +} +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-weight: inherit; +} +h1, +.h1 { + font-size: 36px; +} +h2, +.h2 { + font-size: 30px; +} +h3, +.h3 { + font-size: 24px; +} +h4, +.h4 { + font-size: 20px; +} +h5, +.h5 { + font-size: 16px; +} +h6, +.h6 { + font-size: 14px; +} +.page-heading { + color: #545454; + font-size: 28px; + font-weight: 400; +} +.page-heading small { + margin-top: 5px; + display: block; + color: #777; + font-size: 16px; + font-weight: 300; + line-height: 1.4; +} +@media screen and (min-width: 768px) { + .page-heading small { + margin-top: 0; + display: inline; + line-height: inherit; + } +} +.content-heading { + margin-top: 15px; + margin-bottom: 15px; + font-weight: 600; + font-size: 15px; + color: #8c8c8c; + text-transform: uppercase; +} +.content-heading small { + font-size: 13px; + font-weight: normal; + color: #999999; + text-transform: none; +} +@media screen and (min-width: 768px) { + .content-heading { + margin-top: 25px; + } +} +.font-w300 { + font-weight: 300 !important; +} +.font-w400 { + font-weight: 400 !important; +} +.font-w600 { + font-weight: 600 !important; +} +.font-w700 { + font-weight: 700 !important; +} +.font-s12 { + font-size: 12px !important; +} +.font-s13 { + font-size: 13px !important; +} +.font-s36 { + font-size: 36px !important; +} +.font-s48 { + font-size: 48px !important; +} +.font-s64 { + font-size: 64px !important; +} +.font-s96 { + font-size: 96px !important; +} +.font-s128 { + font-size: 128px !important; +} +p { + line-height: 1.6; +} +p.nice-copy, +.nice-copy p { + line-height: 1.8; +} +p.nice-copy-story, +.nice-copy-story p { + line-height: 1.8; + font-size: 16px; +} +.text-muted { + color: #999999; +} +a.text-muted:hover, +a.text-muted:active, +a.text-muted:focus, +button.text-muted:hover, +button.text-muted:active, +button.text-muted:focus { + color: #999999; + opacity: .75; +} +.text-primary { + color: #5c90d2; +} +a.text-primary:hover, +a.text-primary:active, +a.text-primary:focus, +button.text-primary:hover, +button.text-primary:active, +button.text-primary:focus { + color: #5c90d2; + opacity: .75; +} +.text-primary-dark { + color: #3e4a59; +} +a.text-primary-dark:hover, +a.text-primary-dark:active, +a.text-primary-dark:focus, +button.text-primary-dark:hover, +button.text-primary-dark:active, +button.text-primary-dark:focus { + color: #3e4a59; + opacity: .75; +} +.text-primary-darker { + color: #2c343f; +} +a.text-primary-darker:hover, +a.text-primary-darker:active, +a.text-primary-darker:focus, +button.text-primary-darker:hover, +button.text-primary-darker:active, +button.text-primary-darker:focus { + color: #2c343f; + opacity: .75; +} +.text-primary-light { + color: #98b9e3; +} +a.text-primary-light:hover, +a.text-primary-light:active, +a.text-primary-light:focus, +button.text-primary-light:hover, +button.text-primary-light:active, +button.text-primary-light:focus { + color: #98b9e3; + opacity: .75; +} +.text-primary-lighter { + color: #ccdcf1; +} +a.text-primary-lighter:hover, +a.text-primary-lighter:active, +a.text-primary-lighter:focus, +button.text-primary-lighter:hover, +button.text-primary-lighter:active, +button.text-primary-lighter:focus { + color: #ccdcf1; + opacity: .75; +} +.text-success { + color: #46c37b; +} +a.text-success:hover, +a.text-success:active, +a.text-success:focus, +button.text-success:hover, +button.text-success:active, +button.text-success:focus { + color: #46c37b; + opacity: .75; +} +.text-warning { + color: #f3b760; +} +a.text-warning:hover, +a.text-warning:active, +a.text-warning:focus, +button.text-warning:hover, +button.text-warning:active, +button.text-warning:focus { + color: #f3b760; + opacity: .75; +} +.text-info { + color: #70b9eb; +} +a.text-info:hover, +a.text-info:active, +a.text-info:focus, +button.text-info:hover, +button.text-info:active, +button.text-info:focus { + color: #70b9eb; + opacity: .75; +} +.text-danger { + color: #d26a5c; +} +a.text-danger:hover, +a.text-danger:active, +a.text-danger:focus, +button.text-danger:hover, +button.text-danger:active, +button.text-danger:focus { + color: #d26a5c; + opacity: .75; +} +.text-success-light { + color: #e0f5e9; +} +a.text-success-light:hover, +a.text-success-light:active, +a.text-success-light:focus, +button.text-success-light:hover, +button.text-success-light:active, +button.text-success-light:focus { + color: #e0f5e9; + opacity: .75; +} +.text-warning-light { + color: #fdf3e5; +} +a.text-warning-light:hover, +a.text-warning-light:active, +a.text-warning-light:focus, +button.text-warning-light:hover, +button.text-warning-light:active, +button.text-warning-light:focus { + color: #fdf3e5; + opacity: .75; +} +.text-info-light { + color: #edf6fd; +} +a.text-info-light:hover, +a.text-info-light:active, +a.text-info-light:focus, +button.text-info-light:hover, +button.text-info-light:active, +button.text-info-light:focus { + color: #edf6fd; + opacity: .75; +} +.text-danger-light { + color: #f9eae8; +} +a.text-danger-light:hover, +a.text-danger-light:active, +a.text-danger-light:focus, +button.text-danger-light:hover, +button.text-danger-light:active, +button.text-danger-light:focus { + color: #f9eae8; + opacity: .75; +} +.text-white { + color: #fff; +} +a.text-white:hover, +a.text-white:active, +a.text-white:focus, +button.text-white:hover, +button.text-white:active, +button.text-white:focus { + color: #fff; + opacity: .75; +} +.text-white-op { + color: rgba(255, 255, 255, 0.85); +} +a.text-white-op:hover, +a.text-white-op:active, +a.text-white-op:focus, +button.text-white-op:hover, +button.text-white-op:active, +button.text-white-op:focus { + color: rgba(255, 255, 255, 0.85); + opacity: .75; +} +.text-black { + color: #000; +} +a.text-black:hover, +a.text-black:active, +a.text-black:focus, +button.text-black:hover, +button.text-black:active, +button.text-black:focus { + color: #000; + opacity: .75; +} +.text-black-op { + color: rgba(0, 0, 0, 0.5); +} +a.text-black-op:hover, +a.text-black-op:active, +a.text-black-op:focus, +button.text-black-op:hover, +button.text-black-op:active, +button.text-black-op:focus { + color: rgba(0, 0, 0, 0.5); + opacity: .75; +} +.text-gray { + color: #c9c9c9; +} +a.text-gray:hover, +a.text-gray:active, +a.text-gray:focus, +button.text-gray:hover, +button.text-gray:active, +button.text-gray:focus { + color: #c9c9c9; + opacity: .75; +} +.text-gray-dark { + color: #999999; +} +a.text-gray-dark:hover, +a.text-gray-dark:active, +a.text-gray-dark:focus, +button.text-gray-dark:hover, +button.text-gray-dark:active, +button.text-gray-dark:focus { + color: #999999; + opacity: .75; +} +.text-gray-darker { + color: #393939; +} +a.text-gray-darker:hover, +a.text-gray-darker:active, +a.text-gray-darker:focus, +button.text-gray-darker:hover, +button.text-gray-darker:active, +button.text-gray-darker:focus { + color: #393939; + opacity: .75; +} +.text-gray-light { + color: #f3f3f3; +} +a.text-gray-light:hover, +a.text-gray-light:active, +a.text-gray-light:focus, +button.text-gray-light:hover, +button.text-gray-light:active, +button.text-gray-light:focus { + color: #f3f3f3; + opacity: .75; +} +.text-gray-lighter { + color: #f9f9f9; +} +a.text-gray-lighter:hover, +a.text-gray-lighter:active, +a.text-gray-lighter:focus, +button.text-gray-lighter:hover, +button.text-gray-lighter:active, +button.text-gray-lighter:focus { + color: #f9f9f9; + opacity: .75; +} +.bg-muted { + background-color: #999999; +} +a.bg-muted:hover, +a.bg-muted:focus { + background-color: #808080; +} +.bg-primary { + background-color: #5c90d2; +} +a.bg-primary:hover, +a.bg-primary:focus { + background-color: #3675c5; +} +.bg-primary-op { + background-color: rgba(92, 144, 210, 0.75); +} +a.bg-primary-op:hover, +a.bg-primary-op:focus { + background-color: rgba(54, 117, 197, 0.75); +} +.bg-primary-dark { + background-color: #3e4a59; +} +a.bg-primary-dark:hover, +a.bg-primary-dark:focus { + background-color: #29313b; +} +.bg-primary-dark-op { + background-color: rgba(62, 74, 89, 0.83); +} +a.bg-primary-dark-op:hover, +a.bg-primary-dark-op:focus { + background-color: rgba(41, 49, 59, 0.83); +} +.bg-primary-darker { + background-color: #2c343f; +} +a.bg-primary-darker:hover, +a.bg-primary-darker:focus { + background-color: #171b21; +} +.bg-primary-light { + background-color: #98b9e3; +} +a.bg-primary-light:hover, +a.bg-primary-light:focus { + background-color: #709ed8; +} +.bg-primary-lighter { + background-color: #ccdcf1; +} +a.bg-primary-lighter:hover, +a.bg-primary-lighter:focus { + background-color: #a4c1e6; +} +.bg-success { + background-color: #46c37b; +} +a.bg-success:hover, +a.bg-success:focus { + background-color: #34a263; +} +.bg-warning { + background-color: #f3b760; +} +a.bg-warning:hover, +a.bg-warning:focus { + background-color: #efa231; +} +.bg-info { + background-color: #70b9eb; +} +a.bg-info:hover, +a.bg-info:focus { + background-color: #43a3e5; +} +.bg-danger { + background-color: #d26a5c; +} +a.bg-danger:hover, +a.bg-danger:focus { + background-color: #c54736; +} +.bg-success-light { + background-color: #e0f5e9; +} +a.bg-success-light:hover, +a.bg-success-light:focus { + background-color: #b9e9ce; +} +.bg-warning-light { + background-color: #fdf3e5; +} +a.bg-warning-light:hover, +a.bg-warning-light:focus { + background-color: #f9ddb6; +} +.bg-info-light { + background-color: #edf6fd; +} +a.bg-info-light:hover, +a.bg-info-light:focus { + background-color: #bfdff8; +} +.bg-danger-light { + background-color: #f9eae8; +} +a.bg-danger-light:hover, +a.bg-danger-light:focus { + background-color: #eec5c0; +} +.bg-white { + background-color: #fff; +} +a.bg-white:hover, +a.bg-white:focus { + background-color: #e6e6e6; +} +.bg-white-op { + background-color: rgba(255, 255, 255, 0.075); +} +.bg-crystal-op { + background-color: rgba(255, 255, 255, 0.15); +} +.bg-black { + background-color: #000; +} +a.bg-black:hover, +a.bg-black:focus { + background-color: #000000; +} +.bg-black-op { + background-color: rgba(0, 0, 0, 0.4); +} +.bg-gray { + background-color: #c9c9c9; +} +a.bg-gray:hover, +a.bg-gray:focus { + background-color: #b0b0b0; +} +.bg-gray-dark { + background-color: #999999; +} +a.bg-gray-dark:hover, +a.bg-gray-dark:focus { + background-color: #808080; +} +.bg-gray-darker { + background-color: #393939; +} +a.bg-gray-darker:hover, +a.bg-gray-darker:focus { + background-color: #202020; +} +.bg-gray-light { + background-color: #f3f3f3; +} +a.bg-gray-light:hover, +a.bg-gray-light:focus { + background-color: #dadada; +} +.bg-gray-lighter { + background-color: #f9f9f9; +} +a.bg-gray-lighter:hover, +a.bg-gray-lighter:focus { + background-color: #e0e0e0; +} +.btn { + font-weight: 600; + border-radius: 2px; + -webkit-transition: all 0.15s ease-out; + transition: all 0.15s ease-out; +} +.btn:active, +.btn.active { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn.btn-square { + border-radius: 0; +} +.btn.btn-rounded { + border-radius: 20px; +} +.btn.btn-minw { + min-width: 110px; +} +.btn.btn-noborder { + border: none !important; +} +.btn.btn-image { + position: relative; + padding-left: 40px; +} +.btn.btn-image > img { + position: absolute; + top: 3px; + left: 3px; + display: block; + width: 26px; + height: 26px; + border-radius: 3px; +} +.btn > i.pull-left { + margin-top: 3px; + margin-right: 5px; +} +.btn > i.pull-right { + margin-top: 3px; + margin-left: 5px; +} +.btn-link, +.btn-link:hover, +.btn-link:focus { + text-decoration: none; +} +.btn-default { + color: #545454; + background-color: #f5f5f5; + border-color: #e9e9e9; +} +.btn-default:focus, +.btn-default.focus, +.btn-default:hover { + color: #545454; + background-color: #e1e1e1; + border-color: #cacaca; +} +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + color: #545454; + background-color: #c7c7c7; + border-color: #b1b1b1; +} +.btn-default:active:hover, +.btn-default.active:hover, +.open > .dropdown-toggle.btn-default:hover, +.btn-default:active:focus, +.btn-default.active:focus, +.open > .dropdown-toggle.btn-default:focus, +.btn-default:active.focus, +.btn-default.active.focus, +.open > .dropdown-toggle.btn-default.focus { + color: #545454; + background-color: #c7c7c7; + border-color: #b1b1b1; +} +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + background-image: none; +} +.btn-default.disabled, +.btn-default[disabled], +fieldset[disabled] .btn-default, +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled.focus, +.btn-default[disabled].focus, +fieldset[disabled] .btn-default.focus, +.btn-default.disabled:active, +.btn-default[disabled]:active, +fieldset[disabled] .btn-default:active, +.btn-default.disabled.active, +.btn-default[disabled].active, +fieldset[disabled] .btn-default.active { + background-color: #f5f5f5; + border-color: #e9e9e9; +} +.btn-default .badge { + color: #f5f5f5; + background-color: #545454; +} +.btn-primary { + color: #fff; + background-color: #5c90d2; + border-color: #3675c5; +} +.btn-primary:focus, +.btn-primary.focus, +.btn-primary:hover { + color: #fff; + background-color: #3c7ac9; + border-color: #295995; +} +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + color: #fff; + background-color: #2d62a5; + border-color: #1e416d; +} +.btn-primary:active:hover, +.btn-primary.active:hover, +.open > .dropdown-toggle.btn-primary:hover, +.btn-primary:active:focus, +.btn-primary.active:focus, +.open > .dropdown-toggle.btn-primary:focus, +.btn-primary:active.focus, +.btn-primary.active.focus, +.open > .dropdown-toggle.btn-primary.focus { + color: #fff; + background-color: #2d62a5; + border-color: #1e416d; +} +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + background-image: none; +} +.btn-primary.disabled, +.btn-primary[disabled], +fieldset[disabled] .btn-primary, +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled.focus, +.btn-primary[disabled].focus, +fieldset[disabled] .btn-primary.focus, +.btn-primary.disabled:active, +.btn-primary[disabled]:active, +fieldset[disabled] .btn-primary:active, +.btn-primary.disabled.active, +.btn-primary[disabled].active, +fieldset[disabled] .btn-primary.active { + background-color: #5c90d2; + border-color: #3675c5; +} +.btn-primary .badge { + color: #5c90d2; + background-color: #fff; +} +.btn-success { + color: #fff; + background-color: #46c37b; + border-color: #34a263; +} +.btn-success:focus, +.btn-success.focus, +.btn-success:hover { + color: #fff; + background-color: #37a967; + border-color: #257346; +} +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + color: #fff; + background-color: #2a8350; + border-color: #194d2f; +} +.btn-success:active:hover, +.btn-success.active:hover, +.open > .dropdown-toggle.btn-success:hover, +.btn-success:active:focus, +.btn-success.active:focus, +.open > .dropdown-toggle.btn-success:focus, +.btn-success:active.focus, +.btn-success.active.focus, +.open > .dropdown-toggle.btn-success.focus { + color: #fff; + background-color: #2a8350; + border-color: #194d2f; +} +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + background-image: none; +} +.btn-success.disabled, +.btn-success[disabled], +fieldset[disabled] .btn-success, +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled.focus, +.btn-success[disabled].focus, +fieldset[disabled] .btn-success.focus, +.btn-success.disabled:active, +.btn-success[disabled]:active, +fieldset[disabled] .btn-success:active, +.btn-success.disabled.active, +.btn-success[disabled].active, +fieldset[disabled] .btn-success.active { + background-color: #46c37b; + border-color: #34a263; +} +.btn-success .badge { + color: #46c37b; + background-color: #fff; +} +.btn-info { + color: #fff; + background-color: #70b9eb; + border-color: #43a3e5; +} +.btn-info:focus, +.btn-info.focus, +.btn-info:hover { + color: #fff; + background-color: #4ca7e6; + border-color: #1d86ce; +} +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + color: #fff; + background-color: #1f92e0; + border-color: #1769a1; +} +.btn-info:active:hover, +.btn-info.active:hover, +.open > .dropdown-toggle.btn-info:hover, +.btn-info:active:focus, +.btn-info.active:focus, +.open > .dropdown-toggle.btn-info:focus, +.btn-info:active.focus, +.btn-info.active.focus, +.open > .dropdown-toggle.btn-info.focus { + color: #fff; + background-color: #1f92e0; + border-color: #1769a1; +} +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + background-image: none; +} +.btn-info.disabled, +.btn-info[disabled], +fieldset[disabled] .btn-info, +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled.focus, +.btn-info[disabled].focus, +fieldset[disabled] .btn-info.focus, +.btn-info.disabled:active, +.btn-info[disabled]:active, +fieldset[disabled] .btn-info:active, +.btn-info.disabled.active, +.btn-info[disabled].active, +fieldset[disabled] .btn-info.active { + background-color: #70b9eb; + border-color: #43a3e5; +} +.btn-info .badge { + color: #70b9eb; + background-color: #fff; +} +.btn-warning { + color: #fff; + background-color: #f3b760; + border-color: #efa231; +} +.btn-warning:focus, +.btn-warning.focus, +.btn-warning:hover { + color: #fff; + background-color: #f0a63a; + border-color: #d38310; +} +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + color: #fff; + background-color: #e68f11; + border-color: #a3660c; +} +.btn-warning:active:hover, +.btn-warning.active:hover, +.open > .dropdown-toggle.btn-warning:hover, +.btn-warning:active:focus, +.btn-warning.active:focus, +.open > .dropdown-toggle.btn-warning:focus, +.btn-warning:active.focus, +.btn-warning.active.focus, +.open > .dropdown-toggle.btn-warning.focus { + color: #fff; + background-color: #e68f11; + border-color: #a3660c; +} +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + background-image: none; +} +.btn-warning.disabled, +.btn-warning[disabled], +fieldset[disabled] .btn-warning, +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled.focus, +.btn-warning[disabled].focus, +fieldset[disabled] .btn-warning.focus, +.btn-warning.disabled:active, +.btn-warning[disabled]:active, +fieldset[disabled] .btn-warning:active, +.btn-warning.disabled.active, +.btn-warning[disabled].active, +fieldset[disabled] .btn-warning.active { + background-color: #f3b760; + border-color: #efa231; +} +.btn-warning .badge { + color: #f3b760; + background-color: #fff; +} +.btn-danger { + color: #fff; + background-color: #d26a5c; + border-color: #c54736; +} +.btn-danger:focus, +.btn-danger.focus, +.btn-danger:hover { + color: #fff; + background-color: #c94d3c; + border-color: #953629; +} +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + color: #fff; + background-color: #a53c2d; + border-color: #6d271e; +} +.btn-danger:active:hover, +.btn-danger.active:hover, +.open > .dropdown-toggle.btn-danger:hover, +.btn-danger:active:focus, +.btn-danger.active:focus, +.open > .dropdown-toggle.btn-danger:focus, +.btn-danger:active.focus, +.btn-danger.active.focus, +.open > .dropdown-toggle.btn-danger.focus { + color: #fff; + background-color: #a53c2d; + border-color: #6d271e; +} +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + background-image: none; +} +.btn-danger.disabled, +.btn-danger[disabled], +fieldset[disabled] .btn-danger, +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled.focus, +.btn-danger[disabled].focus, +fieldset[disabled] .btn-danger.focus, +.btn-danger.disabled:active, +.btn-danger[disabled]:active, +fieldset[disabled] .btn-danger:active, +.btn-danger.disabled.active, +.btn-danger[disabled].active, +fieldset[disabled] .btn-danger.active { + background-color: #d26a5c; + border-color: #c54736; +} +.btn-danger .badge { + color: #d26a5c; + background-color: #fff; +} +label { + font-size: 13px; + font-weight: 600; +} +.form-control { + color: #646464; + border: 1px solid #e6e6e6; + border-radius: 3px; + -webkit-box-shadow: none; + box-shadow: none; + -webkit-transition: all 0.15s ease-out; + transition: all 0.15s ease-out; +} +.form-control::-moz-placeholder { + color: #aaa; +} +.form-control:-ms-input-placeholder { + color: #aaa; +} +.form-control::-webkit-input-placeholder { + color: #aaa; +} +.form-control:focus { + border-color: #ccc; + background-color: #fcfcfc; + -webkit-box-shadow: none; + box-shadow: none; +} +textarea.form-control { + max-width: 100%; +} +input[type="text"].form-control, +input[type="password"].form-control, +input[type="email"].form-control { + -webkit-appearance: none; +} +.form-control.input-sm { + border-radius: 3px; +} +.form-control.input-lg { + font-size: 14px; + border-radius: 3px; +} +.input-group-lg .form-control { + font-size: 14px; +} +.form-group { + margin-bottom: 20px; +} +.form-bordered .form-group { + padding-bottom: 10px; + margin-bottom: 10px; + border-bottom: 1px solid #f3f3f3; +} +.form-bordered .form-group.form-actions { + border-bottom: none; +} +.help-block { + margin-top: 5px; + margin-bottom: 0; + font-style: italic; + font-size: 13px; + color: #a4a4a4; +} +.has-success > label, +.has-success .help-block, +.has-success .control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline, +.has-success.radio label, +.has-success.checkbox label, +.has-success.radio-inline label, +.has-success.checkbox-inline label { + color: #46c37b; +} +.has-success .form-control { + border-color: #46c37b; + -webkit-box-shadow: none; + box-shadow: none; +} +.has-success .form-control:focus { + border-color: #34a263; + -webkit-box-shadow: none; + box-shadow: none; +} +.has-success .input-group-addon { + color: #46c37b; + border-color: #46c37b; + background-color: #fff; +} +.has-success .form-control-feedback { + color: #46c37b; +} +.has-info > label, +.has-info .help-block, +.has-info .control-label, +.has-info .radio, +.has-info .checkbox, +.has-info .radio-inline, +.has-info .checkbox-inline, +.has-info.radio label, +.has-info.checkbox label, +.has-info.radio-inline label, +.has-info.checkbox-inline label { + color: #70b9eb; +} +.has-info .form-control { + border-color: #70b9eb; + -webkit-box-shadow: none; + box-shadow: none; +} +.has-info .form-control:focus { + border-color: #43a3e5; + -webkit-box-shadow: none; + box-shadow: none; +} +.has-info .input-group-addon { + color: #70b9eb; + border-color: #70b9eb; + background-color: #fff; +} +.has-info .form-control-feedback { + color: #70b9eb; +} +.has-warning > label, +.has-warning .help-block, +.has-warning .control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline, +.has-warning.radio label, +.has-warning.checkbox label, +.has-warning.radio-inline label, +.has-warning.checkbox-inline label { + color: #f3b760; +} +.has-warning .form-control { + border-color: #f3b760; + -webkit-box-shadow: none; + box-shadow: none; +} +.has-warning .form-control:focus { + border-color: #efa231; + -webkit-box-shadow: none; + box-shadow: none; +} +.has-warning .input-group-addon { + color: #f3b760; + border-color: #f3b760; + background-color: #fff; +} +.has-warning .form-control-feedback { + color: #f3b760; +} +.has-error > label, +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline, +.has-error.radio label, +.has-error.checkbox label, +.has-error.radio-inline label, +.has-error.checkbox-inline label { + color: #d26a5c; +} +.has-error .form-control { + border-color: #d26a5c; + -webkit-box-shadow: none; + box-shadow: none; +} +.has-error .form-control:focus { + border-color: #c54736; + -webkit-box-shadow: none; + box-shadow: none; +} +.has-error .input-group-addon { + color: #d26a5c; + border-color: #d26a5c; + background-color: #fff; +} +.has-error .form-control-feedback { + color: #d26a5c; +} +.input-group-addon { + color: #646464; + background-color: #f9f9f9; + border-color: #e6e6e6; + border-radius: 3px; +} +.input-group-addon.input-sm, +.input-group-addon.input-lg { + border-radius: 3px; +} +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn, +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + border-radius: 3px; +} +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.input-group-addon:first-child { + border-right: 0; +} +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.input-group-addon:last-child { + border-left: 0; +} +.label-primary { + background-color: #5c90d2; +} +.label-primary[href]:hover, +.label-primary[href]:focus { + background-color: #3675c5; +} +.label-success { + background-color: #46c37b; +} +.label-success[href]:hover, +.label-success[href]:focus { + background-color: #34a263; +} +.label-info { + background-color: #70b9eb; +} +.label-info[href]:hover, +.label-info[href]:focus { + background-color: #43a3e5; +} +.label-warning { + background-color: #f3b760; +} +.label-warning[href]:hover, +.label-warning[href]:focus { + background-color: #efa231; +} +.label-danger { + background-color: #d26a5c; +} +.label-danger[href]:hover, +.label-danger[href]:focus { + background-color: #c54736; +} +.badge-primary { + background-color: #5c90d2; +} +.badge-success { + background-color: #46c37b; +} +.badge-info { + background-color: #70b9eb; +} +.badge-warning { + background-color: #f3b760; +} +.badge-danger { + background-color: #d26a5c; +} +.alert { + padding-bottom: 10px; + border-radius: 2px; + border: none; +} +.alert p { + margin-bottom: 5px !important; +} +.alert-success { + background-color: #e0f5e9; + color: #34a263; + -webkit-box-shadow: 0 2px #cdefdb; + box-shadow: 0 2px #cdefdb; +} +.alert-success hr { + border-top-color: #cdefdb; +} +.alert-success .alert-link { + color: #287b4b; +} +.alert-info { + background-color: #edf6fd; + color: #43a3e5; + -webkit-box-shadow: 0 2px #d6ebfa; + box-shadow: 0 2px #d6ebfa; +} +.alert-info hr { + border-top-color: #d6ebfa; +} +.alert-info .alert-link { + color: #1e8cd7; +} +.alert-warning { + background-color: #fdf3e5; + color: #efa231; + -webkit-box-shadow: 0 2px #fbe8cd; + box-shadow: 0 2px #fbe8cd; +} +.alert-warning hr { + border-top-color: #fbe8cd; +} +.alert-warning .alert-link { + color: #dc8911; +} +.alert-danger { + background-color: #f9eae8; + color: #c54736; + -webkit-box-shadow: 0 2px #f4d8d4; + box-shadow: 0 2px #f4d8d4; +} +.alert-danger hr { + border-top-color: #f4d8d4; +} +.alert-danger .alert-link { + color: #9d392b; +} +.progress { + height: 24px; + border-radius: 2px; + -webkit-box-shadow: none; + box-shadow: none; +} +.progress.progress-mini { + height: 5px; +} +.progress.progress-mini .progress-bar { + line-height: 5px; +} +.progress-bar { + line-height: 24px; + font-weight: 600; + -webkit-box-shadow: none; + box-shadow: none; +} +.progress-bar-primary { + background-color: #5c90d2; +} +.progress-bar-success { + background-color: #46c37b; +} +.progress-bar-info { + background-color: #70b9eb; +} +.progress-bar-warning { + background-color: #f3b760; +} +.progress-bar-danger { + background-color: #d26a5c; +} +.nav-pills > li > a { + font-weight: 600; + color: #646464; + border-radius: 3px; +} +.nav-pills > li > a:hover, +.nav-pills > li > a:focus { + color: #646464; + background-color: #f9f9f9; +} +.nav-pills > li.active > a, +.nav-pills > li.active > a:hover, +.nav-pills > li.active > a:focus { + color: #fff; + background-color: #5c90d2; +} +.nav-pills > li.active > a > .badge { + color: #5c90d2; +} +.pagination { + border-radius: 0; +} +.pagination > li { + display: block; + float: left; + margin: 0 0 5px 5px; +} +.pagination > li > a, +.pagination > li > span { + display: block; + float: none; + margin: 0; + padding-right: 8px; + padding-left: 8px; + color: #646464; + font-weight: 600; + border: none; +} +.pagination > li:first-child > a, +.pagination > li:first-child > span { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.pagination > li:last-child > a, +.pagination > li:last-child > span { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + color: #5c90d2; + background-color: transparent; + -webkit-box-shadow: 0 2px #5c90d2; + box-shadow: 0 2px #5c90d2; +} +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + color: #5c90d2; + background-color: #f9f9f9; + -webkit-box-shadow: 0 2px #5c90d2; + box-shadow: 0 2px #5c90d2; +} +.pagination > .disabled > span, +.pagination > .disabled > span:hover, +.pagination > .disabled > span:focus, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: #c9c9c9; + -webkit-box-shadow: none; + box-shadow: none; +} +.pager li > a, +.pager li > span { + padding: 6px 14px; + font-weight: 600; + color: #646464; + border: 1px solid #eee; + border-radius: 3px; +} +.pager li > a:hover, +.pager li > a:focus { + color: #5c90d2; + background-color: #f9f9f9; +} +.pager li.disabled > span, +.pager li.disabled > span:hover, +.pager li.disabled > span:focus, +.pager li.disabled > a, +.pager li.disabled > a:hover, +.pager li.disabled > a:focus { + color: #c9c9c9; + -webkit-box-shadow: none; + box-shadow: none; +} +.list-group-item { + padding: 10px 15px; + border-color: #eee; +} +a.list-group-item { + font-weight: 600; + color: #646464; +} +a.list-group-item:hover, +a.list-group-item:focus { + color: #5c90d2; +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + background-color: #5c90d2; + border-color: #5c90d2; +} +.list-group-item.active > .badge { + color: #5c90d2; +} +.breadcrumb { + padding: 0; + margin-bottom: 0; + text-transform: uppercase; + font-size: 12px; + font-weight: 600; + color: #999999; + background-color: transparent; +} +.breadcrumb > li + li:before { + display: inline-block; + padding: 0; + font-family: "FontAwesome"; + color: rgba(0, 0, 0, 0.5); + content: "\f105"; + width: 20px; + text-align: center; +} +.tooltip-inner { + padding: 6px 8px; + background-color: #2c343f; + border-radius: 0; +} +.tooltip.top .tooltip-arrow { + border-top-color: #2c343f; +} +.tooltip.right .tooltip-arrow { + border-right-color: #2c343f; +} +.tooltip.left .tooltip-arrow { + border-left-color: #2c343f; +} +.tooltip.bottom .tooltip-arrow { + border-bottom-color: #2c343f; +} +.popover { + border-color: #ddd; + border-radius: 2px; + -webkit-box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05); +} +.popover-title { + padding: 10px 10px 1px; + font-family: "Source Sans Pro", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + background-color: #fff; + border-bottom: none; + border-radius: 1px 1px 0 0; +} +.popover-content { + padding: 10px; +} +.dropdown-menu { + min-width: 180px; + padding: 5px 0; + border-color: #ddd; + border-radius: 2px; + -webkit-box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05); +} +.dropdown-menu > li > a { + padding: 7px 12px; +} +.dropdown-menu > li > a:hover { + background-color: #f9f9f9; +} +.dropdown-menu > li > a i.pull-right, +.dropdown-menu > li > a .badge.pull-right { + right: 0; + margin-top: 3px; + margin-left: 10px; +} +.dropdown-menu > li > a i.pull-left, +.dropdown-menu > li > a .badge.pull-left { + left: 0; + margin-top: 3px; + margin-right: 10px; +} +.dropdown-menu > li > a .badge.pull-right { + margin-top: 1px; +} +.dropdown-menu > li > a .badge.pull-left { + margin-top: 1px; +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #545454; + background-color: #f0f0f0; +} +.dropdown-header { + padding: 5px 12px 4px; + font-weight: 600; + color: #999999; + text-transform: uppercase; +} +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + padding: 12px 10px; + border-top: 1px solid #f0f0f0; +} +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th { + padding: 16px 10px 12px; + font-family: "Source Sans Pro", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 15px; + font-weight: 600; + text-transform: uppercase; +} +.table > thead > tr > th { + border-bottom: 1px solid #ddd; +} +.table > tbody + tbody { + border-top: 1px solid #ddd; +} +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 6px 8px; +} +.table-bordered { + border: 1px solid #e9e9e9; +} +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #f0f0f0; +} +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 1px; + border-bottom-color: #e9e9e9; +} +.table-borderless { + border: none; +} +.table-borderless > thead > tr > th, +.table-borderless > tbody > tr > th, +.table-borderless > tfoot > tr > th, +.table-borderless > thead > tr > td, +.table-borderless > tbody > tr > td, +.table-borderless > tfoot > tr > td { + border: none; +} +.table-borderless > thead > tr > th, +.table-borderless > thead > tr > td { + border-bottom: 1px solid #ddd; +} +.table-vcenter > thead > tr > th, +.table-vcenter > tbody > tr > th, +.table-vcenter > tfoot > tr > th, +.table-vcenter > thead > tr > td, +.table-vcenter > tbody > tr > td, +.table-vcenter > tfoot > tr > td { + vertical-align: middle; +} +.table-striped > tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} +.table-hover > tbody > tr:hover { + background-color: #f5f5f5; +} +.table-header-bg > thead > tr > th, +.table-header-bg > thead > tr > td { + color: #fff; + background-color: #5c90d2; + border-bottom-color: #5c90d2; +} +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #f9f9f9; +} +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr.active:hover > th { + background-color: #ececec; +} +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: #e0f5e9; +} +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td, +.table-hover > tbody > tr:hover > .success, +.table-hover > tbody > tr.success:hover > th { + background-color: #cdefdb; +} +.table > thead > tr > td.info, +.table > tbody > tr > td.info, +.table > tfoot > tr > td.info, +.table > thead > tr > th.info, +.table > tbody > tr > th.info, +.table > tfoot > tr > th.info, +.table > thead > tr.info > td, +.table > tbody > tr.info > td, +.table > tfoot > tr.info > td, +.table > thead > tr.info > th, +.table > tbody > tr.info > th, +.table > tfoot > tr.info > th { + background-color: #edf6fd; +} +.table-hover > tbody > tr > td.info:hover, +.table-hover > tbody > tr > th.info:hover, +.table-hover > tbody > tr.info:hover > td, +.table-hover > tbody > tr:hover > .info, +.table-hover > tbody > tr.info:hover > th { + background-color: #d6ebfa; +} +.table > thead > tr > td.warning, +.table > tbody > tr > td.warning, +.table > tfoot > tr > td.warning, +.table > thead > tr > th.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > tbody > tr.warning > td, +.table > tfoot > tr.warning > td, +.table > thead > tr.warning > th, +.table > tbody > tr.warning > th, +.table > tfoot > tr.warning > th { + background-color: #fdf3e5; +} +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, +.table-hover > tbody > tr.warning:hover > td, +.table-hover > tbody > tr:hover > .warning, +.table-hover > tbody > tr.warning:hover > th { + background-color: #fbe8cd; +} +.table > thead > tr > td.danger, +.table > tbody > tr > td.danger, +.table > tfoot > tr > td.danger, +.table > thead > tr > th.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > tbody > tr.danger > td, +.table > tfoot > tr.danger > td, +.table > thead > tr.danger > th, +.table > tbody > tr.danger > th, +.table > tfoot > tr.danger > th { + background-color: #f9eae8; +} +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, +.table-hover > tbody > tr.danger:hover > td, +.table-hover > tbody > tr:hover > .danger, +.table-hover > tbody > tr.danger:hover > th { + background-color: #f4d8d4; +} +.table-responsive { + -webkit-overflow-scrolling: touch; +} +.js-table-checkable tbody tr, +.js-table-sections-header > tr { + cursor: pointer; +} +.js-table-sections-header > tr > td:first-child > i { + -webkit-transition: -webkit-transform 0.15s ease-out; + transition: transform 0.15s ease-out; +} +.js-table-sections-header + tbody { + display: none; +} +.js-table-sections-header.open > tr { + background-color: #f9f9f9; +} +.js-table-sections-header.open > tr > td:first-child > i { + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.js-table-sections-header.open + tbody { + display: table-row-group; +} +.modal.fade .modal-dialog { + -webkit-transition: all 0.12s ease-out; + transition: all 0.12s ease-out; +} +.modal.fade .modal-dialog.modal-dialog-popin { + -webkit-transform: scale(1.1); + -ms-transform: scale(1.1); + transform: scale(1.1); +} +.modal.fade .modal-dialog.modal-dialog-popout { + -webkit-transform: scale(0.9); + -ms-transform: scale(0.9); + transform: scale(0.9); +} +.modal.fade .modal-dialog.modal-dialog-slideup { + -webkit-transform: translate(0, 10%); + -ms-transform: translate(0, 10%); + transform: translate(0, 10%); +} +.modal.fade .modal-dialog.modal-dialog-slideright { + -webkit-transform: translate(-10%, 0); + -ms-transform: translate(-10%, 0); + transform: translate(-10%, 0); +} +.modal.fade .modal-dialog.modal-dialog-slideleft { + -webkit-transform: translate(10%, 0); + -ms-transform: translate(10%, 0); + transform: translate(10%, 0); +} +.modal.fade .modal-dialog.modal-dialog-fromright { + -webkit-transform: translateX(25%) rotate(10deg) scale(0.9); + -ms-transform: translateX(25%) rotate(10deg) scale(0.9); + transform: translateX(25%) rotate(10deg) scale(0.9); +} +.modal.fade .modal-dialog.modal-dialog-fromleft { + -webkit-transform: translateX(-25%) rotate(-10deg) scale(0.9); + -ms-transform: translateX(-25%) rotate(-10deg) scale(0.9); + transform: translateX(-25%) rotate(-10deg) scale(0.9); +} +.modal.in .modal-dialog.modal-dialog-popin, +.modal.in .modal-dialog.modal-dialog-popout { + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); +} +.modal.in .modal-dialog.modal-dialog-slideup, +.modal.in .modal-dialog.modal-dialog-slideright, +.modal.in .modal-dialog.modal-dialog-slideleft { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); +} +.modal.in .modal-dialog.modal-dialog-fromright, +.modal.in .modal-dialog.modal-dialog-fromleft { + -webkit-transform: translateX(0) rotate(0) scale(1); + -ms-transform: translateX(0) rotate(0) scale(1); + transform: translateX(0) rotate(0) scale(1); +} +.modal-dialog.modal-dialog-top { + margin-top: 0; + padding: 0 !important; +} +.modal-dialog.modal-dialog-top .modal-content { + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.modal-content { + border: none; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; +} +.modal-backdrop.in { + opacity: .25; +} +.modal-header { + padding: 18px 20px; + border-bottom-color: #eee; +} +.modal-body { + padding: 20px; +} +.modal-footer { + padding: 12px 20px; + border-top-color: #eee; +} +.modal-title { + font-weight: normal; +} +.fade.fade-up { + opacity: 0; + -webkit-transition: all 0.25s ease-out; + transition: all 0.25s ease-out; + -webkit-transform: translateY(100px); + -ms-transform: translateY(100px); + transform: translateY(100px); +} +.fade.fade-up.in { + opacity: 1; + -webkit-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); +} +.fade.fade-right { + opacity: 0; + -webkit-transition: all 0.25s ease-out; + transition: all 0.25s ease-out; + -webkit-transform: translateX(-100px); + -ms-transform: translateX(-100px); + transform: translateX(-100px); +} +.fade.fade-right.in { + opacity: 1; + -webkit-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); +} +.fade.fade-left { + opacity: 0; + -webkit-transition: all 0.25s ease-out; + transition: all 0.25s ease-out; + -webkit-transform: translateX(100px); + -ms-transform: translateX(100px); + transform: translateX(100px); +} +.fade.fade-left.in { + opacity: 1; + -webkit-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); +} +.panel { + border-radius: 3px; + -webkit-box-shadow: none; + box-shadow: none; +} +.panel-group .panel { + border-radius: 3px; +} +.panel-group .panel + .panel { + margin-top: 10px; +} +.panel-default { + border-color: #f0f0f0; +} +.panel-default > .panel-heading { + color: #646464; + background-color: #f9f9f9; + border-color: #f0f0f0; +} +.panel-default > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #f0f0f0; +} +.panel-default > .panel-heading .badge { + color: #f9f9f9; + background-color: #646464; +} +.panel-default > .panel-heading a { + font-weight: 400; +} +.panel-default > .panel-heading a:hover, +.panel-default > .panel-heading a:focus { + color: #4a4a4a; +} +.panel-default > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #f0f0f0; +} +.panel-primary { + border-color: #ccdcf1; +} +.panel-primary > .panel-heading { + color: #5c90d2; + background-color: #e8eff9; + border-color: #ccdcf1; +} +.panel-primary > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ccdcf1; +} +.panel-primary > .panel-heading .badge { + color: #e8eff9; + background-color: #5c90d2; +} +.panel-primary > .panel-heading a { + font-weight: 400; +} +.panel-primary > .panel-heading a:hover, +.panel-primary > .panel-heading a:focus { + color: #3675c5; +} +.panel-primary > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ccdcf1; +} +.panel-success { + border-color: #cdefdb; +} +.panel-success > .panel-heading { + color: #46c37b; + background-color: #e0f5e9; + border-color: #cdefdb; +} +.panel-success > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #cdefdb; +} +.panel-success > .panel-heading .badge { + color: #e0f5e9; + background-color: #46c37b; +} +.panel-success > .panel-heading a { + font-weight: 400; +} +.panel-success > .panel-heading a:hover, +.panel-success > .panel-heading a:focus { + color: #34a263; +} +.panel-success > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #cdefdb; +} +.panel-info { + border-color: #d6ebfa; +} +.panel-info > .panel-heading { + color: #70b9eb; + background-color: #edf6fd; + border-color: #d6ebfa; +} +.panel-info > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #d6ebfa; +} +.panel-info > .panel-heading .badge { + color: #edf6fd; + background-color: #70b9eb; +} +.panel-info > .panel-heading a { + font-weight: 400; +} +.panel-info > .panel-heading a:hover, +.panel-info > .panel-heading a:focus { + color: #43a3e5; +} +.panel-info > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #d6ebfa; +} +.panel-warning { + border-color: #fbe8cd; +} +.panel-warning > .panel-heading { + color: #f3b760; + background-color: #fdf3e5; + border-color: #fbe8cd; +} +.panel-warning > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #fbe8cd; +} +.panel-warning > .panel-heading .badge { + color: #fdf3e5; + background-color: #f3b760; +} +.panel-warning > .panel-heading a { + font-weight: 400; +} +.panel-warning > .panel-heading a:hover, +.panel-warning > .panel-heading a:focus { + color: #efa231; +} +.panel-warning > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #fbe8cd; +} +.panel-danger { + border-color: #f4d8d4; +} +.panel-danger > .panel-heading { + color: #d26a5c; + background-color: #f9eae8; + border-color: #f4d8d4; +} +.panel-danger > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #f4d8d4; +} +.panel-danger > .panel-heading .badge { + color: #f9eae8; + background-color: #d26a5c; +} +.panel-danger > .panel-heading a { + font-weight: 400; +} +.panel-danger > .panel-heading a:hover, +.panel-danger > .panel-heading a:focus { + color: #c54736; +} +.panel-danger > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #f4d8d4; +} +.img-responsive { + width: 100%; +} +/* FONT PATH + * -------------------------- */ +@font-face { + font-family: 'FontAwesome'; + src: url('../fonts/fontawesome-webfont.eot?v=4.7.0'); + src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg'); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.33333333em; + line-height: 0.75em; + vertical-align: -15%; +} +.fa-2x { + font-size: 2em; +} +.fa-3x { + font-size: 3em; +} +.fa-4x { + font-size: 4em; +} +.fa-5x { + font-size: 5em; +} +.fa-fw { + width: 1.28571429em; + text-align: center; +} +.fa-ul { + padding-left: 0; + margin-left: 2.14285714em; + list-style-type: none; +} +.fa-ul > li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.85714286em; +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.fa-pull-left { + float: left; +} +.fa-pull-right { + float: right; +} +.fa.fa-pull-left { + margin-right: .3em; +} +.fa.fa-pull-right { + margin-left: .3em; +} +/* Deprecated as of 4.4.0 */ +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .3em; +} +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none; +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook-f:before, +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-feed:before, +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before, +.fa-gratipay:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} +.fa-space-shuttle:before { + content: "\f197"; +} +.fa-slack:before { + content: "\f198"; +} +.fa-envelope-square:before { + content: "\f199"; +} +.fa-wordpress:before { + content: "\f19a"; +} +.fa-openid:before { + content: "\f19b"; +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} +.fa-yahoo:before { + content: "\f19e"; +} +.fa-google:before { + content: "\f1a0"; +} +.fa-reddit:before { + content: "\f1a1"; +} +.fa-reddit-square:before { + content: "\f1a2"; +} +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} +.fa-stumbleupon:before { + content: "\f1a4"; +} +.fa-delicious:before { + content: "\f1a5"; +} +.fa-digg:before { + content: "\f1a6"; +} +.fa-pied-piper-pp:before { + content: "\f1a7"; +} +.fa-pied-piper-alt:before { + content: "\f1a8"; +} +.fa-drupal:before { + content: "\f1a9"; +} +.fa-joomla:before { + content: "\f1aa"; +} +.fa-language:before { + content: "\f1ab"; +} +.fa-fax:before { + content: "\f1ac"; +} +.fa-building:before { + content: "\f1ad"; +} +.fa-child:before { + content: "\f1ae"; +} +.fa-paw:before { + content: "\f1b0"; +} +.fa-spoon:before { + content: "\f1b1"; +} +.fa-cube:before { + content: "\f1b2"; +} +.fa-cubes:before { + content: "\f1b3"; +} +.fa-behance:before { + content: "\f1b4"; +} +.fa-behance-square:before { + content: "\f1b5"; +} +.fa-steam:before { + content: "\f1b6"; +} +.fa-steam-square:before { + content: "\f1b7"; +} +.fa-recycle:before { + content: "\f1b8"; +} +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} +.fa-tree:before { + content: "\f1bb"; +} +.fa-spotify:before { + content: "\f1bc"; +} +.fa-deviantart:before { + content: "\f1bd"; +} +.fa-soundcloud:before { + content: "\f1be"; +} +.fa-database:before { + content: "\f1c0"; +} +.fa-file-pdf-o:before { + content: "\f1c1"; +} +.fa-file-word-o:before { + content: "\f1c2"; +} +.fa-file-excel-o:before { + content: "\f1c3"; +} +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} +.fa-file-code-o:before { + content: "\f1c9"; +} +.fa-vine:before { + content: "\f1ca"; +} +.fa-codepen:before { + content: "\f1cb"; +} +.fa-jsfiddle:before { + content: "\f1cc"; +} +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} +.fa-circle-o-notch:before { + content: "\f1ce"; +} +.fa-ra:before, +.fa-resistance:before, +.fa-rebel:before { + content: "\f1d0"; +} +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} +.fa-git-square:before { + content: "\f1d2"; +} +.fa-git:before { + content: "\f1d3"; +} +.fa-y-combinator-square:before, +.fa-yc-square:before, +.fa-hacker-news:before { + content: "\f1d4"; +} +.fa-tencent-weibo:before { + content: "\f1d5"; +} +.fa-qq:before { + content: "\f1d6"; +} +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} +.fa-history:before { + content: "\f1da"; +} +.fa-circle-thin:before { + content: "\f1db"; +} +.fa-header:before { + content: "\f1dc"; +} +.fa-paragraph:before { + content: "\f1dd"; +} +.fa-sliders:before { + content: "\f1de"; +} +.fa-share-alt:before { + content: "\f1e0"; +} +.fa-share-alt-square:before { + content: "\f1e1"; +} +.fa-bomb:before { + content: "\f1e2"; +} +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "\f1e3"; +} +.fa-tty:before { + content: "\f1e4"; +} +.fa-binoculars:before { + content: "\f1e5"; +} +.fa-plug:before { + content: "\f1e6"; +} +.fa-slideshare:before { + content: "\f1e7"; +} +.fa-twitch:before { + content: "\f1e8"; +} +.fa-yelp:before { + content: "\f1e9"; +} +.fa-newspaper-o:before { + content: "\f1ea"; +} +.fa-wifi:before { + content: "\f1eb"; +} +.fa-calculator:before { + content: "\f1ec"; +} +.fa-paypal:before { + content: "\f1ed"; +} +.fa-google-wallet:before { + content: "\f1ee"; +} +.fa-cc-visa:before { + content: "\f1f0"; +} +.fa-cc-mastercard:before { + content: "\f1f1"; +} +.fa-cc-discover:before { + content: "\f1f2"; +} +.fa-cc-amex:before { + content: "\f1f3"; +} +.fa-cc-paypal:before { + content: "\f1f4"; +} +.fa-cc-stripe:before { + content: "\f1f5"; +} +.fa-bell-slash:before { + content: "\f1f6"; +} +.fa-bell-slash-o:before { + content: "\f1f7"; +} +.fa-trash:before { + content: "\f1f8"; +} +.fa-copyright:before { + content: "\f1f9"; +} +.fa-at:before { + content: "\f1fa"; +} +.fa-eyedropper:before { + content: "\f1fb"; +} +.fa-paint-brush:before { + content: "\f1fc"; +} +.fa-birthday-cake:before { + content: "\f1fd"; +} +.fa-area-chart:before { + content: "\f1fe"; +} +.fa-pie-chart:before { + content: "\f200"; +} +.fa-line-chart:before { + content: "\f201"; +} +.fa-lastfm:before { + content: "\f202"; +} +.fa-lastfm-square:before { + content: "\f203"; +} +.fa-toggle-off:before { + content: "\f204"; +} +.fa-toggle-on:before { + content: "\f205"; +} +.fa-bicycle:before { + content: "\f206"; +} +.fa-bus:before { + content: "\f207"; +} +.fa-ioxhost:before { + content: "\f208"; +} +.fa-angellist:before { + content: "\f209"; +} +.fa-cc:before { + content: "\f20a"; +} +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "\f20b"; +} +.fa-meanpath:before { + content: "\f20c"; +} +.fa-buysellads:before { + content: "\f20d"; +} +.fa-connectdevelop:before { + content: "\f20e"; +} +.fa-dashcube:before { + content: "\f210"; +} +.fa-forumbee:before { + content: "\f211"; +} +.fa-leanpub:before { + content: "\f212"; +} +.fa-sellsy:before { + content: "\f213"; +} +.fa-shirtsinbulk:before { + content: "\f214"; +} +.fa-simplybuilt:before { + content: "\f215"; +} +.fa-skyatlas:before { + content: "\f216"; +} +.fa-cart-plus:before { + content: "\f217"; +} +.fa-cart-arrow-down:before { + content: "\f218"; +} +.fa-diamond:before { + content: "\f219"; +} +.fa-ship:before { + content: "\f21a"; +} +.fa-user-secret:before { + content: "\f21b"; +} +.fa-motorcycle:before { + content: "\f21c"; +} +.fa-street-view:before { + content: "\f21d"; +} +.fa-heartbeat:before { + content: "\f21e"; +} +.fa-venus:before { + content: "\f221"; +} +.fa-mars:before { + content: "\f222"; +} +.fa-mercury:before { + content: "\f223"; +} +.fa-intersex:before, +.fa-transgender:before { + content: "\f224"; +} +.fa-transgender-alt:before { + content: "\f225"; +} +.fa-venus-double:before { + content: "\f226"; +} +.fa-mars-double:before { + content: "\f227"; +} +.fa-venus-mars:before { + content: "\f228"; +} +.fa-mars-stroke:before { + content: "\f229"; +} +.fa-mars-stroke-v:before { + content: "\f22a"; +} +.fa-mars-stroke-h:before { + content: "\f22b"; +} +.fa-neuter:before { + content: "\f22c"; +} +.fa-genderless:before { + content: "\f22d"; +} +.fa-facebook-official:before { + content: "\f230"; +} +.fa-pinterest-p:before { + content: "\f231"; +} +.fa-whatsapp:before { + content: "\f232"; +} +.fa-server:before { + content: "\f233"; +} +.fa-user-plus:before { + content: "\f234"; +} +.fa-user-times:before { + content: "\f235"; +} +.fa-hotel:before, +.fa-bed:before { + content: "\f236"; +} +.fa-viacoin:before { + content: "\f237"; +} +.fa-train:before { + content: "\f238"; +} +.fa-subway:before { + content: "\f239"; +} +.fa-medium:before { + content: "\f23a"; +} +.fa-yc:before, +.fa-y-combinator:before { + content: "\f23b"; +} +.fa-optin-monster:before { + content: "\f23c"; +} +.fa-opencart:before { + content: "\f23d"; +} +.fa-expeditedssl:before { + content: "\f23e"; +} +.fa-battery-4:before, +.fa-battery:before, +.fa-battery-full:before { + content: "\f240"; +} +.fa-battery-3:before, +.fa-battery-three-quarters:before { + content: "\f241"; +} +.fa-battery-2:before, +.fa-battery-half:before { + content: "\f242"; +} +.fa-battery-1:before, +.fa-battery-quarter:before { + content: "\f243"; +} +.fa-battery-0:before, +.fa-battery-empty:before { + content: "\f244"; +} +.fa-mouse-pointer:before { + content: "\f245"; +} +.fa-i-cursor:before { + content: "\f246"; +} +.fa-object-group:before { + content: "\f247"; +} +.fa-object-ungroup:before { + content: "\f248"; +} +.fa-sticky-note:before { + content: "\f249"; +} +.fa-sticky-note-o:before { + content: "\f24a"; +} +.fa-cc-jcb:before { + content: "\f24b"; +} +.fa-cc-diners-club:before { + content: "\f24c"; +} +.fa-clone:before { + content: "\f24d"; +} +.fa-balance-scale:before { + content: "\f24e"; +} +.fa-hourglass-o:before { + content: "\f250"; +} +.fa-hourglass-1:before, +.fa-hourglass-start:before { + content: "\f251"; +} +.fa-hourglass-2:before, +.fa-hourglass-half:before { + content: "\f252"; +} +.fa-hourglass-3:before, +.fa-hourglass-end:before { + content: "\f253"; +} +.fa-hourglass:before { + content: "\f254"; +} +.fa-hand-grab-o:before, +.fa-hand-rock-o:before { + content: "\f255"; +} +.fa-hand-stop-o:before, +.fa-hand-paper-o:before { + content: "\f256"; +} +.fa-hand-scissors-o:before { + content: "\f257"; +} +.fa-hand-lizard-o:before { + content: "\f258"; +} +.fa-hand-spock-o:before { + content: "\f259"; +} +.fa-hand-pointer-o:before { + content: "\f25a"; +} +.fa-hand-peace-o:before { + content: "\f25b"; +} +.fa-trademark:before { + content: "\f25c"; +} +.fa-registered:before { + content: "\f25d"; +} +.fa-creative-commons:before { + content: "\f25e"; +} +.fa-gg:before { + content: "\f260"; +} +.fa-gg-circle:before { + content: "\f261"; +} +.fa-tripadvisor:before { + content: "\f262"; +} +.fa-odnoklassniki:before { + content: "\f263"; +} +.fa-odnoklassniki-square:before { + content: "\f264"; +} +.fa-get-pocket:before { + content: "\f265"; +} +.fa-wikipedia-w:before { + content: "\f266"; +} +.fa-safari:before { + content: "\f267"; +} +.fa-chrome:before { + content: "\f268"; +} +.fa-firefox:before { + content: "\f269"; +} +.fa-opera:before { + content: "\f26a"; +} +.fa-internet-explorer:before { + content: "\f26b"; +} +.fa-tv:before, +.fa-television:before { + content: "\f26c"; +} +.fa-contao:before { + content: "\f26d"; +} +.fa-500px:before { + content: "\f26e"; +} +.fa-amazon:before { + content: "\f270"; +} +.fa-calendar-plus-o:before { + content: "\f271"; +} +.fa-calendar-minus-o:before { + content: "\f272"; +} +.fa-calendar-times-o:before { + content: "\f273"; +} +.fa-calendar-check-o:before { + content: "\f274"; +} +.fa-industry:before { + content: "\f275"; +} +.fa-map-pin:before { + content: "\f276"; +} +.fa-map-signs:before { + content: "\f277"; +} +.fa-map-o:before { + content: "\f278"; +} +.fa-map:before { + content: "\f279"; +} +.fa-commenting:before { + content: "\f27a"; +} +.fa-commenting-o:before { + content: "\f27b"; +} +.fa-houzz:before { + content: "\f27c"; +} +.fa-vimeo:before { + content: "\f27d"; +} +.fa-black-tie:before { + content: "\f27e"; +} +.fa-fonticons:before { + content: "\f280"; +} +.fa-reddit-alien:before { + content: "\f281"; +} +.fa-edge:before { + content: "\f282"; +} +.fa-credit-card-alt:before { + content: "\f283"; +} +.fa-codiepie:before { + content: "\f284"; +} +.fa-modx:before { + content: "\f285"; +} +.fa-fort-awesome:before { + content: "\f286"; +} +.fa-usb:before { + content: "\f287"; +} +.fa-product-hunt:before { + content: "\f288"; +} +.fa-mixcloud:before { + content: "\f289"; +} +.fa-scribd:before { + content: "\f28a"; +} +.fa-pause-circle:before { + content: "\f28b"; +} +.fa-pause-circle-o:before { + content: "\f28c"; +} +.fa-stop-circle:before { + content: "\f28d"; +} +.fa-stop-circle-o:before { + content: "\f28e"; +} +.fa-shopping-bag:before { + content: "\f290"; +} +.fa-shopping-basket:before { + content: "\f291"; +} +.fa-hashtag:before { + content: "\f292"; +} +.fa-bluetooth:before { + content: "\f293"; +} +.fa-bluetooth-b:before { + content: "\f294"; +} +.fa-percent:before { + content: "\f295"; +} +.fa-gitlab:before { + content: "\f296"; +} +.fa-wpbeginner:before { + content: "\f297"; +} +.fa-wpforms:before { + content: "\f298"; +} +.fa-envira:before { + content: "\f299"; +} +.fa-universal-access:before { + content: "\f29a"; +} +.fa-wheelchair-alt:before { + content: "\f29b"; +} +.fa-question-circle-o:before { + content: "\f29c"; +} +.fa-blind:before { + content: "\f29d"; +} +.fa-audio-description:before { + content: "\f29e"; +} +.fa-volume-control-phone:before { + content: "\f2a0"; +} +.fa-braille:before { + content: "\f2a1"; +} +.fa-assistive-listening-systems:before { + content: "\f2a2"; +} +.fa-asl-interpreting:before, +.fa-american-sign-language-interpreting:before { + content: "\f2a3"; +} +.fa-deafness:before, +.fa-hard-of-hearing:before, +.fa-deaf:before { + content: "\f2a4"; +} +.fa-glide:before { + content: "\f2a5"; +} +.fa-glide-g:before { + content: "\f2a6"; +} +.fa-signing:before, +.fa-sign-language:before { + content: "\f2a7"; +} +.fa-low-vision:before { + content: "\f2a8"; +} +.fa-viadeo:before { + content: "\f2a9"; +} +.fa-viadeo-square:before { + content: "\f2aa"; +} +.fa-snapchat:before { + content: "\f2ab"; +} +.fa-snapchat-ghost:before { + content: "\f2ac"; +} +.fa-snapchat-square:before { + content: "\f2ad"; +} +.fa-pied-piper:before { + content: "\f2ae"; +} +.fa-first-order:before { + content: "\f2b0"; +} +.fa-yoast:before { + content: "\f2b1"; +} +.fa-themeisle:before { + content: "\f2b2"; +} +.fa-google-plus-circle:before, +.fa-google-plus-official:before { + content: "\f2b3"; +} +.fa-fa:before, +.fa-font-awesome:before { + content: "\f2b4"; +} +.fa-handshake-o:before { + content: "\f2b5"; +} +.fa-envelope-open:before { + content: "\f2b6"; +} +.fa-envelope-open-o:before { + content: "\f2b7"; +} +.fa-linode:before { + content: "\f2b8"; +} +.fa-address-book:before { + content: "\f2b9"; +} +.fa-address-book-o:before { + content: "\f2ba"; +} +.fa-vcard:before, +.fa-address-card:before { + content: "\f2bb"; +} +.fa-vcard-o:before, +.fa-address-card-o:before { + content: "\f2bc"; +} +.fa-user-circle:before { + content: "\f2bd"; +} +.fa-user-circle-o:before { + content: "\f2be"; +} +.fa-user-o:before { + content: "\f2c0"; +} +.fa-id-badge:before { + content: "\f2c1"; +} +.fa-drivers-license:before, +.fa-id-card:before { + content: "\f2c2"; +} +.fa-drivers-license-o:before, +.fa-id-card-o:before { + content: "\f2c3"; +} +.fa-quora:before { + content: "\f2c4"; +} +.fa-free-code-camp:before { + content: "\f2c5"; +} +.fa-telegram:before { + content: "\f2c6"; +} +.fa-thermometer-4:before, +.fa-thermometer:before, +.fa-thermometer-full:before { + content: "\f2c7"; +} +.fa-thermometer-3:before, +.fa-thermometer-three-quarters:before { + content: "\f2c8"; +} +.fa-thermometer-2:before, +.fa-thermometer-half:before { + content: "\f2c9"; +} +.fa-thermometer-1:before, +.fa-thermometer-quarter:before { + content: "\f2ca"; +} +.fa-thermometer-0:before, +.fa-thermometer-empty:before { + content: "\f2cb"; +} +.fa-shower:before { + content: "\f2cc"; +} +.fa-bathtub:before, +.fa-s15:before, +.fa-bath:before { + content: "\f2cd"; +} +.fa-podcast:before { + content: "\f2ce"; +} +.fa-window-maximize:before { + content: "\f2d0"; +} +.fa-window-minimize:before { + content: "\f2d1"; +} +.fa-window-restore:before { + content: "\f2d2"; +} +.fa-times-rectangle:before, +.fa-window-close:before { + content: "\f2d3"; +} +.fa-times-rectangle-o:before, +.fa-window-close-o:before { + content: "\f2d4"; +} +.fa-bandcamp:before { + content: "\f2d5"; +} +.fa-grav:before { + content: "\f2d6"; +} +.fa-etsy:before { + content: "\f2d7"; +} +.fa-imdb:before { + content: "\f2d8"; +} +.fa-ravelry:before { + content: "\f2d9"; +} +.fa-eercast:before { + content: "\f2da"; +} +.fa-microchip:before { + content: "\f2db"; +} +.fa-snowflake-o:before { + content: "\f2dc"; +} +.fa-superpowers:before { + content: "\f2dd"; +} +.fa-wpexplorer:before { + content: "\f2de"; +} +.fa-meetup:before { + content: "\f2e0"; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} +@font-face { + font-family: Simple-Line-Icons; + src: url(../fonts/Simple-Line-Icons.eot); + src: url(../fonts/Simple-Line-Icons.eot?#iefix) format('embedded-opentype'), url(../fonts/Simple-Line-Icons.woff) format('woff'), url(../fonts/Simple-Line-Icons.ttf) format('truetype'), url(../fonts/Simple-Line-Icons.svg#Simple-Line-Icons) format('svg'); + font-weight: 400; + font-style: normal; +} +.si { + font-family: Simple-Line-Icons; + speak: none; + font-style: normal; + font-weight: 400; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; +} +.btn .si { + position: relative; + bottom: -2px; + display: inline-block; +} +.si-user-female:before { + content: "\e000"; +} +.si-user-follow:before { + content: "\e002"; +} +.si-user-following:before { + content: "\e003"; +} +.si-user-unfollow:before { + content: "\e004"; +} +.si-trophy:before { + content: "\e006"; +} +.si-screen-smartphone:before { + content: "\e010"; +} +.si-screen-desktop:before { + content: "\e011"; +} +.si-plane:before { + content: "\e012"; +} +.si-notebook:before { + content: "\e013"; +} +.si-moustache:before { + content: "\e014"; +} +.si-mouse:before { + content: "\e015"; +} +.si-magnet:before { + content: "\e016"; +} +.si-energy:before { + content: "\e020"; +} +.si-emoticon-smile:before { + content: "\e021"; +} +.si-disc:before { + content: "\e022"; +} +.si-cursor-move:before { + content: "\e023"; +} +.si-crop:before { + content: "\e024"; +} +.si-credit-card:before { + content: "\e025"; +} +.si-chemistry:before { + content: "\e026"; +} +.si-user:before { + content: "\e005"; +} +.si-speedometer:before { + content: "\e007"; +} +.si-social-youtube:before { + content: "\e008"; +} +.si-social-twitter:before { + content: "\e009"; +} +.si-social-tumblr:before { + content: "\e00a"; +} +.si-social-facebook:before { + content: "\e00b"; +} +.si-social-dropbox:before { + content: "\e00c"; +} +.si-social-dribbble:before { + content: "\e00d"; +} +.si-shield:before { + content: "\e00e"; +} +.si-screen-tablet:before { + content: "\e00f"; +} +.si-magic-wand:before { + content: "\e017"; +} +.si-hourglass:before { + content: "\e018"; +} +.si-graduation:before { + content: "\e019"; +} +.si-ghost:before { + content: "\e01a"; +} +.si-game-controller:before { + content: "\e01b"; +} +.si-fire:before { + content: "\e01c"; +} +.si-eyeglasses:before { + content: "\e01d"; +} +.si-envelope-open:before { + content: "\e01e"; +} +.si-envelope-letter:before { + content: "\e01f"; +} +.si-bell:before { + content: "\e027"; +} +.si-badge:before { + content: "\e028"; +} +.si-anchor:before { + content: "\e029"; +} +.si-wallet:before { + content: "\e02a"; +} +.si-vector:before { + content: "\e02b"; +} +.si-speech:before { + content: "\e02c"; +} +.si-puzzle:before { + content: "\e02d"; +} +.si-printer:before { + content: "\e02e"; +} +.si-present:before { + content: "\e02f"; +} +.si-playlist:before { + content: "\e030"; +} +.si-pin:before { + content: "\e031"; +} +.si-picture:before { + content: "\e032"; +} +.si-map:before { + content: "\e033"; +} +.si-layers:before { + content: "\e034"; +} +.si-handbag:before { + content: "\e035"; +} +.si-globe-alt:before { + content: "\e036"; +} +.si-globe:before { + content: "\e037"; +} +.si-frame:before { + content: "\e038"; +} +.si-folder-alt:before { + content: "\e039"; +} +.si-film:before { + content: "\e03a"; +} +.si-feed:before { + content: "\e03b"; +} +.si-earphones-alt:before { + content: "\e03c"; +} +.si-earphones:before { + content: "\e03d"; +} +.si-drop:before { + content: "\e03e"; +} +.si-drawer:before { + content: "\e03f"; +} +.si-docs:before { + content: "\e040"; +} +.si-directions:before { + content: "\e041"; +} +.si-direction:before { + content: "\e042"; +} +.si-diamond:before { + content: "\e043"; +} +.si-cup:before { + content: "\e044"; +} +.si-compass:before { + content: "\e045"; +} +.si-call-out:before { + content: "\e046"; +} +.si-call-in:before { + content: "\e047"; +} +.si-call-end:before { + content: "\e048"; +} +.si-calculator:before { + content: "\e049"; +} +.si-bubbles:before { + content: "\e04a"; +} +.si-briefcase:before { + content: "\e04b"; +} +.si-book-open:before { + content: "\e04c"; +} +.si-basket-loaded:before { + content: "\e04d"; +} +.si-basket:before { + content: "\e04e"; +} +.si-bag:before { + content: "\e04f"; +} +.si-action-undo:before { + content: "\e050"; +} +.si-action-redo:before { + content: "\e051"; +} +.si-wrench:before { + content: "\e052"; +} +.si-umbrella:before { + content: "\e053"; +} +.si-trash:before { + content: "\e054"; +} +.si-tag:before { + content: "\e055"; +} +.si-support:before { + content: "\e056"; +} +.si-size-fullscreen:before { + content: "\e057"; +} +.si-size-actual:before { + content: "\e058"; +} +.si-shuffle:before { + content: "\e059"; +} +.si-share-alt:before { + content: "\e05a"; +} +.si-share:before { + content: "\e05b"; +} +.si-rocket:before { + content: "\e05c"; +} +.si-question:before { + content: "\e05d"; +} +.si-pie-chart:before { + content: "\e05e"; +} +.si-pencil:before { + content: "\e05f"; +} +.si-note:before { + content: "\e060"; +} +.si-music-tone-alt:before { + content: "\e061"; +} +.si-music-tone:before { + content: "\e062"; +} +.si-microphone:before { + content: "\e063"; +} +.si-loop:before { + content: "\e064"; +} +.si-logout:before { + content: "\e065"; +} +.si-login:before { + content: "\e066"; +} +.si-list:before { + content: "\e067"; +} +.si-like:before { + content: "\e068"; +} +.si-home:before { + content: "\e069"; +} +.si-grid:before { + content: "\e06a"; +} +.si-graph:before { + content: "\e06b"; +} +.si-equalizer:before { + content: "\e06c"; +} +.si-dislike:before { + content: "\e06d"; +} +.si-cursor:before { + content: "\e06e"; +} +.si-control-start:before { + content: "\e06f"; +} +.si-control-rewind:before { + content: "\e070"; +} +.si-control-play:before { + content: "\e071"; +} +.si-control-pause:before { + content: "\e072"; +} +.si-control-forward:before { + content: "\e073"; +} +.si-control-end:before { + content: "\e074"; +} +.si-calendar:before { + content: "\e075"; +} +.si-bulb:before { + content: "\e076"; +} +.si-bar-chart:before { + content: "\e077"; +} +.si-arrow-up:before { + content: "\e078"; +} +.si-arrow-right:before { + content: "\e079"; +} +.si-arrow-left:before { + content: "\e07a"; +} +.si-arrow-down:before { + content: "\e07b"; +} +.si-ban:before { + content: "\e07c"; +} +.si-bubble:before { + content: "\e07d"; +} +.si-camcorder:before { + content: "\e07e"; +} +.si-camera:before { + content: "\e07f"; +} +.si-check:before { + content: "\e080"; +} +.si-clock:before { + content: "\e081"; +} +.si-close:before { + content: "\e082"; +} +.si-cloud-download:before { + content: "\e083"; +} +.si-cloud-upload:before { + content: "\e084"; +} +.si-doc:before { + content: "\e085"; +} +.si-envelope:before { + content: "\e086"; +} +.si-eye:before { + content: "\e087"; +} +.si-flag:before { + content: "\e088"; +} +.si-folder:before { + content: "\e089"; +} +.si-heart:before { + content: "\e08a"; +} +.si-info:before { + content: "\e08b"; +} +.si-key:before { + content: "\e08c"; +} +.si-link:before { + content: "\e08d"; +} +.si-lock:before { + content: "\e08e"; +} +.si-lock-open:before { + content: "\e08f"; +} +.si-magnifier:before { + content: "\e090"; +} +.si-magnifier-add:before { + content: "\e091"; +} +.si-magnifier-remove:before { + content: "\e092"; +} +.si-paper-clip:before { + content: "\e093"; +} +.si-paper-plane:before { + content: "\e094"; +} +.si-plus:before { + content: "\e095"; +} +.si-pointer:before { + content: "\e096"; +} +.si-power:before { + content: "\e097"; +} +.si-refresh:before { + content: "\e098"; +} +.si-reload:before { + content: "\e099"; +} +.si-settings:before { + content: "\e09a"; +} +.si-star:before { + content: "\e09b"; +} +.si-symbol-female:before { + content: "\e09c"; +} +.si-symbol-male:before { + content: "\e09d"; +} +.si-target:before { + content: "\e09e"; +} +.si-volume-1:before { + content: "\e09f"; +} +.si-volume-2:before { + content: "\e0a0"; +} +.si-volume-off:before { + content: "\e0a1"; +} +.si-users:before { + content: "\e001"; +} +#page-loader { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: #fff; + z-index: 999998; +} +#page-loader:after { + position: absolute; + top: 50%; + left: 50%; + display: block; + margin-top: -30px; + margin-left: -30px; + width: 60px; + height: 60px; + background-color: #5c90d2; + border-radius: 100%; + content: ''; + z-index: 999999; + -webkit-animation: page-loader 0.9s infinite ease-in-out; + animation: page-loader 0.9s infinite ease-in-out; +} +.ie9 #page-loader:after { + text-align: center; + content: 'Loading..'; + background-color: transparent; +} +@-webkit-keyframes page-loader { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 0; + } +} +@keyframes page-loader { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 0; + } +} +#header-navbar { + min-height: 60px; + background-color: #fff; +} +#header-navbar:before, +#header-navbar:after { + content: " "; + display: table; +} +#header-navbar:after { + clear: both; +} +.header-navbar-fixed #header-navbar { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; + min-width: 320px; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.02); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.02); +} +.header-navbar-fixed #main-container { + padding-top: 60px; +} +@media screen and (min-width: 992px) { + .header-navbar-fixed.sidebar-l.sidebar-o #header-navbar { + left: 230px; + } + .header-navbar-fixed.sidebar-r.sidebar-o #header-navbar { + right: 230px; + } + .header-navbar-fixed.sidebar-l.sidebar-o.sidebar-mini #header-navbar { + left: 60px; + } + .header-navbar-fixed.sidebar-r.sidebar-o.sidebar-mini #header-navbar { + right: 60px; + } +} +.header-navbar-transparent #header-navbar { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} +.header-navbar-transparent.header-navbar-fixed.header-navbar-scroll #header-navbar { + background-color: #3e4a59; +} +.header-navbar-transparent.header-navbar-fixed #main-container { + padding-top: 0; +} +#page-container { + margin: 0 auto; + width: 100%; + min-width: 320px; + background-color: #2c343f; +} +@media screen and (min-width: 992px) { + #page-container.sidebar-l.sidebar-o { + padding-left: 230px; + } + #page-container.sidebar-r.sidebar-o { + padding-right: 230px; + } + #page-container.sidebar-l.sidebar-o.sidebar-mini { + padding-left: 60px; + } + #page-container.sidebar-r.sidebar-o.sidebar-mini { + padding-right: 60px; + } +} +#sidebar, +#side-overlay { + position: fixed; + top: 0; + bottom: 0; + z-index: 1032; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + -webkit-transition: all 0.28s ease-out; + transition: all 0.28s ease-out; +} +@media screen and (min-width: 992px) { + .side-scroll #sidebar, + .side-scroll #side-overlay { + overflow-y: hidden; + } +} +#sidebar { + width: 230px; + background-color: #2c343f; +} +.sidebar-l #sidebar { + left: 0; + -webkit-transform: translateX(-100%) translateY(0) translateZ(0); + -ms-transform: translateX(-100%) translateY(0); + transform: translateX(-100%) translateY(0) translateZ(0); +} +.sidebar-r #sidebar { + right: 0; + -webkit-transform: translateX(100%) translateY(0) translateZ(0); + -ms-transform: translateX(100%) translateY(0); + transform: translateX(100%) translateY(0) translateZ(0); +} +@media screen and (max-width: 991px) { + #sidebar { + width: 100%; + opacity: 0; + } + .sidebar-o-xs #sidebar { + opacity: 1; + -webkit-transform: translateX(0) translateY(0) translateZ(0); + -ms-transform: translateX(0) translateY(0); + transform: translateX(0) translateY(0) translateZ(0); + } +} +@media screen and (min-width: 992px) { + #sidebar { + width: 230px; + -webkit-transition: none; + transition: none; + } + .sidebar-o #sidebar { + -webkit-transform: translateX(0) translateY(0) translateZ(0); + -ms-transform: translateX(0) translateY(0); + transform: translateX(0) translateY(0) translateZ(0); + } + .sidebar-o.sidebar-mini #sidebar { + overflow-x: hidden; + -webkit-transition: all 0.28s ease-out; + transition: all 0.28s ease-out; + will-change: transform; + } + .sidebar-l.sidebar-o.sidebar-mini #sidebar { + -webkit-transform: translateX(-170px) translateY(0) translateZ(0); + -ms-transform: translateX(-170px) translateY(0); + transform: translateX(-170px) translateY(0) translateZ(0); + } + .sidebar-r.sidebar-o.sidebar-mini #sidebar { + -webkit-transform: translateX(170px) translateY(0) translateZ(0); + -ms-transform: translateX(170px) translateY(0); + transform: translateX(170px) translateY(0) translateZ(0); + } + .sidebar-o.sidebar-mini #sidebar .sidebar-content { + width: 230px; + -webkit-transition: all 0.28s ease-out; + transition: all 0.28s ease-out; + will-change: transform; + } + .sidebar-l.sidebar-o.sidebar-mini #sidebar .sidebar-content { + -webkit-transform: translateX(170px) translateY(0) translateZ(0); + -ms-transform: translateX(170px) translateY(0); + transform: translateX(170px) translateY(0) translateZ(0); + } + .sidebar-o.sidebar-mini #sidebar:hover, + .sidebar-o.sidebar-mini #sidebar:hover .sidebar-content { + -webkit-transform: translateX(0) translateY(0) translateZ(0); + -ms-transform: translateX(0) translateY(0); + transform: translateX(0) translateY(0) translateZ(0); + } + .sidebar-o.sidebar-mini #sidebar .sidebar-mini-hide { + opacity: 0; + -webkit-transition: opacity 0.28s ease-out; + transition: opacity 0.28s ease-out; + } + .sidebar-o.sidebar-mini #sidebar .sidebar-mini-hidden { + display: none; + } + .sidebar-o.sidebar-mini #sidebar .nav-main > li.open > ul { + display: none; + } + .sidebar-o.sidebar-mini #sidebar:hover .sidebar-mini-hide { + opacity: 1; + } + .sidebar-o.sidebar-mini #sidebar:hover .nav-main > li.open > ul { + display: block; + } +} +#side-overlay { + background-color: #fff; +} +.sidebar-l #side-overlay { + right: 0; + -webkit-transform: translateX(100%) translateY(0) translateZ(0); + -ms-transform: translateX(100%) translateY(0); + transform: translateX(100%) translateY(0) translateZ(0); +} +.sidebar-r #side-overlay { + left: 0; + -webkit-transform: translateX(-100%) translateY(0) translateZ(0); + -ms-transform: translateX(-100%) translateY(0); + transform: translateX(-100%) translateY(0) translateZ(0); +} +@media screen and (max-width: 991px) { + #side-overlay { + width: 100%; + opacity: 0; + } + .side-overlay-o #side-overlay { + opacity: 1; + -webkit-transform: translateX(0) translateY(0) translateZ(0); + -ms-transform: translateX(0) translateY(0); + transform: translateX(0) translateY(0) translateZ(0); + } +} +@media screen and (min-width: 992px) { + #side-overlay { + width: 320px; + -webkit-box-shadow: 0 0 20px rgba(0, 0, 0, 0.3); + box-shadow: 0 0 20px rgba(0, 0, 0, 0.3); + } + .sidebar-l #side-overlay { + -webkit-transform: translateX(110%) translateY(0) translateZ(0); + -ms-transform: translateX(110%) translateY(0); + transform: translateX(110%) translateY(0) translateZ(0); + } + .sidebar-r #side-overlay { + -webkit-transform: translateX(-110%) translateY(0) translateZ(0); + -ms-transform: translateX(-110%) translateY(0); + transform: translateX(-110%) translateY(0) translateZ(0); + } + .sidebar-l.side-overlay-hover #side-overlay { + -webkit-transform: translateX(300px) translateY(0) translateZ(0); + -ms-transform: translateX(300px) translateY(0); + transform: translateX(300px) translateY(0) translateZ(0); + } + .sidebar-r.side-overlay-hover #side-overlay { + -webkit-transform: translateX(-300px) translateY(0) translateZ(0); + -ms-transform: translateX(-300px) translateY(0); + transform: translateX(-300px) translateY(0) translateZ(0); + } + .side-overlay-hover #side-overlay:hover, + .side-overlay-o #side-overlay, + .side-overlay-o.side-overlay-hover #side-overlay { + -webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + -webkit-transform: translateX(0) translateY(0) translateZ(0); + -ms-transform: translateX(0) translateY(0); + transform: translateX(0) translateY(0) translateZ(0); + } +} +.side-header { + margin: 0 auto; + min-height: 60px; +} +.side-header:before, +.side-header:after { + content: " "; + display: table; +} +.side-header:after { + clear: both; +} +.side-header.side-content { + overflow: visible; +} +.side-header > span, +.side-header > a { + display: inline-block; + line-height: 34px; +} +.side-header img { + display: inline-block; + margin-top: -2px; +} +.side-content { + margin: 0 auto; + padding: 13px 20px 1px; + max-width: 100%; + overflow-x: hidden; +} +.side-content p, +.side-content .push, +.side-content .block, +.side-content .items-push > div { + margin-bottom: 13px; +} +.side-content .items-push-2x > div { + margin-bottom: 26px; +} +.side-content .items-push-3x > div { + margin-bottom: 39px; +} +.side-content.side-content-full { + padding-bottom: 13px; +} +.side-content.side-content-full .pull-b { + margin-bottom: -13px; +} +.side-content .pull-t { + margin-top: -13px; +} +.side-content .pull-r-l { + margin-right: -20px; + margin-left: -20px; +} +.side-content .pull-b { + margin-bottom: -1px; +} +#main-container, +#page-footer { + overflow-x: hidden; +} +#main-container { + background-color: #f5f5f5; +} +.content { + margin: 0 auto; + padding: 16px 14px 1px; + max-width: 100%; + overflow-x: visible; +} +.content p, +.content .push, +.content .block, +.content .items-push > div { + margin-bottom: 16px; +} +.content .items-push-2x > div { + margin-bottom: 32px; +} +.content .items-push-3x > div { + margin-bottom: 48px; +} +.content.content-full { + padding-bottom: 16px; +} +.content.content-full .pull-b { + margin-bottom: -16px; +} +.content .pull-t { + margin-top: -16px; +} +.content .pull-r-l { + margin-right: -14px; + margin-left: -14px; +} +.content .pull-b { + margin-bottom: -1px; +} +@media screen and (min-width: 768px) { + .content { + margin: 0 auto; + padding: 30px 30px 1px; + max-width: 100%; + overflow-x: visible; + } + .content p, + .content .push, + .content .block, + .content .items-push > div { + margin-bottom: 30px; + } + .content .items-push-2x > div { + margin-bottom: 60px; + } + .content .items-push-3x > div { + margin-bottom: 90px; + } + .content.content-full { + padding-bottom: 30px; + } + .content.content-full .pull-b { + margin-bottom: -30px; + } + .content .pull-t { + margin-top: -30px; + } + .content .pull-r-l { + margin-right: -30px; + margin-left: -30px; + } + .content .pull-b { + margin-bottom: -1px; + } + .content.content-boxed { + max-width: 1280px; + } + .content.content-narrow { + max-width: 95%; + } +} +.content-grid { + margin-bottom: 24px; +} +.content-grid .push, +.content-grid .block { + margin-bottom: 6px; +} +.content-grid .row { + margin-left: -3px; + margin-right: -3px; +} +.content-grid .row > div[class*="col"] { + padding-left: 3px; + padding-right: 3px; +} +.content-mini { + margin: 0 auto; + padding: 13px 14px 1px; + max-width: 100%; + overflow-x: visible; +} +.content-mini p, +.content-mini .push, +.content-mini .block, +.content-mini .items-push > div { + margin-bottom: 13px; +} +.content-mini .items-push-2x > div { + margin-bottom: 26px; +} +.content-mini .items-push-3x > div { + margin-bottom: 39px; +} +.content-mini.content-mini-full { + padding-bottom: 13px; +} +.content-mini.content-mini-full .pull-b { + margin-bottom: -13px; +} +.content-mini .pull-t { + margin-top: -13px; +} +.content-mini .pull-r-l { + margin-right: -14px; + margin-left: -14px; +} +.content-mini .pull-b { + margin-bottom: -1px; +} +@media screen and (min-width: 768px) { + .content-mini { + margin: 0 auto; + padding: 13px 30px 1px; + max-width: 100%; + overflow-x: visible; + } + .content-mini p, + .content-mini .push, + .content-mini .block, + .content-mini .items-push > div { + margin-bottom: 13px; + } + .content-mini .items-push-2x > div { + margin-bottom: 26px; + } + .content-mini .items-push-3x > div { + margin-bottom: 39px; + } + .content-mini.content-mini-full { + padding-bottom: 13px; + } + .content-mini.content-mini-full .pull-b { + margin-bottom: -13px; + } + .content-mini .pull-t { + margin-top: -13px; + } + .content-mini .pull-r-l { + margin-right: -30px; + margin-left: -30px; + } + .content-mini .pull-b { + margin-bottom: -1px; + } +} +.content-boxed { + margin: 0 auto; + width: 100%; + max-width: 1280px; +} +.bg-image { + background-color: #f9f9f9; + background-position: 0 50%; + -webkit-background-size: cover; + background-size: cover; +} +.bg-image-cover { + height: 300px; +} +@media screen and (min-width: 992px) { + .bg-image-cover { + height: 750px; + } +} +@media screen and (min-width: 1200px) { + .bg-image-parallax { + background-attachment: fixed; + } +} +.bg-video { + width: 100%; + -webkit-transform: translateZ(0); + -moz-transform: translateZ(0); + transform: translateZ(0); +} +.form-material { + position: relative; + margin: 10px 0 10px; +} +.form-material > label { + position: absolute; + top: 6px; + left: 0; + font-size: 13px; + font-weight: 600; + -webkit-transform: translateY(-24px); + -ms-transform: translateY(-24px); + transform: translateY(-24px); +} +.form-material.floating > label { + font-size: 15px; + font-weight: 400; + cursor: text; + z-index: 10; + -webkit-transition: all 0.15s ease-out; + transition: all 0.15s ease-out; + -webkit-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); +} +.form-material.floating > .form-control[disabled] + label, +.form-material.floating > .form-control[readonly] + label, +fieldset[disabled] .form-material.floating > label { + cursor: not-allowed; +} +.form-material > .form-control { + padding-left: 0; + padding-right: 0; + border: 0; + border-radius: 0; + background-color: transparent; + -webkit-box-shadow: 0 1px 0 #e6e6e6; + box-shadow: 0 1px 0 #e6e6e6; +} +.form-material > .form-control:focus { + background-color: transparent; + -webkit-box-shadow: 0 2px 0 #646464; + box-shadow: 0 2px 0 #646464; +} +.form-material > .form-control:focus + label { + color: #646464; +} +.form-material > .form-control:focus ~ .input-group-addon { + -webkit-box-shadow: 0 2px 0 #646464; + box-shadow: 0 2px 0 #646464; +} +.form-material > .form-control:focus + label, +.form-material.floating.open > label { + font-size: 13px; + font-weight: 600; + cursor: default; + -webkit-transform: translateY(-24px); + -ms-transform: translateY(-24px); + transform: translateY(-24px); +} +.form-material .form-control[disabled], +.form-material .form-control[readonly], +.form-material fieldset[disabled] .form-control { + background-color: #fff; + border-bottom: 1px dashed #ccc; + -webkit-box-shadow: none; + box-shadow: none; +} +.form-material.input-group .input-group-addon { + border: none; + background-color: transparent; + border-radius: 0 !important; + -webkit-box-shadow: 0 1px 0 #e6e6e6; + box-shadow: 0 1px 0 #e6e6e6; + -webkit-transition: all 0.15s ease-out; + transition: all 0.15s ease-out; +} +.form-material.form-material-primary > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #5c90d2; + box-shadow: 0 2px 0 #5c90d2; +} +.form-material.form-material-primary > .form-control:focus + label { + color: #5c90d2; +} +.form-material.form-material-primary > .form-control:focus ~ .input-group-addon { + color: #5c90d2; + -webkit-box-shadow: 0 2px 0 #5c90d2; + box-shadow: 0 2px 0 #5c90d2; +} +.form-material.form-material-success > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #46c37b; + box-shadow: 0 2px 0 #46c37b; +} +.form-material.form-material-success > .form-control:focus + label { + color: #46c37b; +} +.form-material.form-material-success > .form-control:focus ~ .input-group-addon { + color: #46c37b; + -webkit-box-shadow: 0 2px 0 #46c37b; + box-shadow: 0 2px 0 #46c37b; +} +.form-material.form-material-info > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #70b9eb; + box-shadow: 0 2px 0 #70b9eb; +} +.form-material.form-material-info > .form-control:focus + label { + color: #70b9eb; +} +.form-material.form-material-info > .form-control:focus ~ .input-group-addon { + color: #70b9eb; + -webkit-box-shadow: 0 2px 0 #70b9eb; + box-shadow: 0 2px 0 #70b9eb; +} +.form-material.form-material-warning > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #f3b760; + box-shadow: 0 2px 0 #f3b760; +} +.form-material.form-material-warning > .form-control:focus + label { + color: #f3b760; +} +.form-material.form-material-warning > .form-control:focus ~ .input-group-addon { + color: #f3b760; + -webkit-box-shadow: 0 2px 0 #f3b760; + box-shadow: 0 2px 0 #f3b760; +} +.form-material.form-material-danger > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #d26a5c; + box-shadow: 0 2px 0 #d26a5c; +} +.form-material.form-material-danger > .form-control:focus + label { + color: #d26a5c; +} +.form-material.form-material-danger > .form-control:focus ~ .input-group-addon { + color: #d26a5c; + -webkit-box-shadow: 0 2px 0 #d26a5c; + box-shadow: 0 2px 0 #d26a5c; +} +.has-success .form-material > .form-control { + -webkit-box-shadow: 0 1px 0 #46c37b; + box-shadow: 0 1px 0 #46c37b; +} +.has-success .form-material > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #46c37b; + box-shadow: 0 2px 0 #46c37b; +} +.has-success .form-material > .form-control:focus + label { + color: #46c37b; +} +.has-success .form-material > .form-control:focus ~ .input-group-addon { + color: #46c37b; + -webkit-box-shadow: 0 2px 0 #46c37b; + box-shadow: 0 2px 0 #46c37b; +} +.has-success .form-material > .form-control ~ .input-group-addon { + color: #46c37b; + -webkit-box-shadow: 0 1px 0 #46c37b; + box-shadow: 0 1px 0 #46c37b; +} +.has-success .form-material label { + color: #46c37b; +} +.has-success .form-material > .help-block { + color: #46c37b; +} +.has-info .form-material > .form-control { + -webkit-box-shadow: 0 1px 0 #70b9eb; + box-shadow: 0 1px 0 #70b9eb; +} +.has-info .form-material > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #70b9eb; + box-shadow: 0 2px 0 #70b9eb; +} +.has-info .form-material > .form-control:focus + label { + color: #70b9eb; +} +.has-info .form-material > .form-control:focus ~ .input-group-addon { + color: #70b9eb; + -webkit-box-shadow: 0 2px 0 #70b9eb; + box-shadow: 0 2px 0 #70b9eb; +} +.has-info .form-material > .form-control ~ .input-group-addon { + color: #70b9eb; + -webkit-box-shadow: 0 1px 0 #70b9eb; + box-shadow: 0 1px 0 #70b9eb; +} +.has-info .form-material label { + color: #70b9eb; +} +.has-info .form-material > .help-block { + color: #70b9eb; +} +.has-warning .form-material > .form-control { + -webkit-box-shadow: 0 1px 0 #f3b760; + box-shadow: 0 1px 0 #f3b760; +} +.has-warning .form-material > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #f3b760; + box-shadow: 0 2px 0 #f3b760; +} +.has-warning .form-material > .form-control:focus + label { + color: #f3b760; +} +.has-warning .form-material > .form-control:focus ~ .input-group-addon { + color: #f3b760; + -webkit-box-shadow: 0 2px 0 #f3b760; + box-shadow: 0 2px 0 #f3b760; +} +.has-warning .form-material > .form-control ~ .input-group-addon { + color: #f3b760; + -webkit-box-shadow: 0 1px 0 #f3b760; + box-shadow: 0 1px 0 #f3b760; +} +.has-warning .form-material label { + color: #f3b760; +} +.has-warning .form-material > .help-block { + color: #f3b760; +} +.has-error .form-material > .form-control { + -webkit-box-shadow: 0 1px 0 #d26a5c; + box-shadow: 0 1px 0 #d26a5c; +} +.has-error .form-material > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #d26a5c; + box-shadow: 0 2px 0 #d26a5c; +} +.has-error .form-material > .form-control:focus + label { + color: #d26a5c; +} +.has-error .form-material > .form-control:focus ~ .input-group-addon { + color: #d26a5c; + -webkit-box-shadow: 0 2px 0 #d26a5c; + box-shadow: 0 2px 0 #d26a5c; +} +.has-error .form-material > .form-control ~ .input-group-addon { + color: #d26a5c; + -webkit-box-shadow: 0 1px 0 #d26a5c; + box-shadow: 0 1px 0 #d26a5c; +} +.has-error .form-material label { + color: #d26a5c; +} +.has-error .form-material > .help-block { + color: #d26a5c; +} +.css-input { + position: relative; + display: inline-block; + margin: 2px 0; + font-weight: 400; + cursor: pointer; +} +.css-input input { + position: absolute; + opacity: 0; +} +.css-input input:focus + span { + box-shadow: 0 0 3px rgba(0, 0, 0, 0.25); +} +.css-input input + span { + position: relative; + display: inline-block; + margin-top: -2px; + margin-right: 3px; + vertical-align: middle; +} +.css-input input + span:after { + position: absolute; + content: ""; +} +.css-input-disabled { + opacity: .5; + cursor: not-allowed; +} +.css-checkbox { + margin: 7px 0; +} +.css-checkbox input + span { + width: 20px; + height: 20px; + background-color: #fff; + border: 1px solid #ddd; + -webkit-transition: background-color 0.2s; + transition: background-color 0.2s; +} +.css-checkbox input + span:after { + top: 0; + right: 0; + bottom: 0; + left: 0; + font-family: "FontAwesome"; + font-size: 10px; + color: #fff; + line-height: 18px; + content: "\f00c"; + text-align: center; +} +.css-checkbox:hover input + span { + border-color: #ccc; +} +.css-checkbox.css-checkbox-sm { + margin: 9px 0 8px; + font-size: 12px; +} +.css-checkbox.css-checkbox-sm input + span { + width: 16px; + height: 16px; +} +.css-checkbox.css-checkbox-sm input + span:after { + font-size: 8px; + line-height: 15px; +} +.css-checkbox.css-checkbox-lg { + margin: 3px 0; +} +.css-checkbox.css-checkbox-lg input + span { + width: 30px; + height: 30px; +} +.css-checkbox.css-checkbox-lg input + span:after { + font-size: 12px; + line-height: 30px; +} +.css-checkbox.css-checkbox-rounded input + span { + border-radius: 3px; +} +.css-checkbox-default input:checked + span { + background-color: #999999; + border-color: #999999; +} +.css-checkbox-primary input:checked + span { + background-color: #5c90d2; + border-color: #5c90d2; +} +.css-checkbox-info input:checked + span { + background-color: #70b9eb; + border-color: #70b9eb; +} +.css-checkbox-success input:checked + span { + background-color: #46c37b; + border-color: #46c37b; +} +.css-checkbox-warning input:checked + span { + background-color: #f3b760; + border-color: #f3b760; +} +.css-checkbox-danger input:checked + span { + background-color: #d26a5c; + border-color: #d26a5c; +} +.css-radio { + margin: 7px 0; +} +.css-radio input + span { + width: 20px; + height: 20px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 50%; +} +.css-radio input + span:after { + top: 2px; + right: 2px; + bottom: 2px; + left: 2px; + background-color: #fff; + border-radius: 50%; + opacity: 0; + -webkit-transition: opacity 0.2s ease-out; + transition: opacity 0.2s ease-out; +} +.css-radio input:checked + span:after { + opacity: 1; +} +.css-radio:hover input + span { + border-color: #ccc; +} +.css-radio.css-radio-sm { + margin: 9px 0 8px; + font-size: 12px; +} +.css-radio.css-radio-sm input + span { + width: 16px; + height: 16px; +} +.css-radio.css-radio-lg { + margin: 5px 0; +} +.css-radio.css-radio-lg input + span { + width: 26px; + height: 26px; +} +.css-radio-default input:checked + span:after { + background-color: #999999; +} +.css-radio-primary input:checked + span:after { + background-color: #5c90d2; +} +.css-radio-info input:checked + span:after { + background-color: #70b9eb; +} +.css-radio-success input:checked + span:after { + background-color: #46c37b; +} +.css-radio-warning input:checked + span:after { + background-color: #f3b760; +} +.css-radio-danger input:checked + span:after { + background-color: #d26a5c; +} +.switch { + margin: 3px 0; +} +.switch input + span { + width: 54px; + height: 30px; + background-color: #eee; + border-radius: 30px; + -webkit-transition: background-color 0.4s; + transition: background-color 0.4s; +} +.switch input + span:after { + top: 2px; + bottom: 2px; + left: 2px; + width: 26px; + background-color: #fff; + border-radius: 50%; + -webkit-box-shadow: 1px 0 3px rgba(0, 0, 0, 0.1); + box-shadow: 1px 0 3px rgba(0, 0, 0, 0.1); + -webkit-transition: -webkit-transform 0.15s ease-out; + transition: transform 0.15s ease-out; +} +.switch input:checked + span { + background-color: #ddd; +} +.switch input:checked + span:after { + -webkit-box-shadow: -2px 0 3px rgba(0, 0, 0, 0.2); + box-shadow: -2px 0 3px rgba(0, 0, 0, 0.2); + -webkit-transform: translateX(23px); + -ms-transform: translateX(23px); + transform: translateX(23px); +} +.switch.switch-sm { + margin: 8px 0 7px; + font-size: 12px; +} +.switch.switch-sm input + span { + width: 36px; + height: 20px; +} +.switch.switch-sm input + span:after { + width: 16px; +} +.switch.switch-sm input:checked + span:after { + -webkit-transform: translateX(15px); + -ms-transform: translateX(15px); + transform: translateX(15px); +} +.switch.switch-lg { + margin: 1px 0; +} +.switch.switch-lg input + span { + width: 70px; + height: 34px; +} +.switch.switch-lg input + span:after { + width: 30px; +} +.switch.switch-lg input:checked + span:after { + -webkit-transform: translateX(35px); + -ms-transform: translateX(35px); + transform: translateX(35px); +} +.switch.switch-square input + span, +.switch.switch-square input + span:after { + border-radius: 0; +} +.switch-default input:checked + span { + background-color: #999999; +} +.switch-primary input:checked + span { + background-color: #5c90d2; +} +.switch-info input:checked + span { + background-color: #70b9eb; +} +.switch-success input:checked + span { + background-color: #46c37b; +} +.switch-warning input:checked + span { + background-color: #f3b760; +} +.switch-danger input:checked + span { + background-color: #d26a5c; +} +.block { + margin-bottom: 30px; + background-color: #fff; + -webkit-box-shadow: 0 2px rgba(0, 0, 0, 0.01); + box-shadow: 0 2px rgba(0, 0, 0, 0.01); +} +.block .block, +.side-content .block { + -webkit-box-shadow: none; + box-shadow: none; +} +.block-header { + padding: 15px 20px; + -webkit-transition: opacity 0.2s ease-out; + transition: opacity 0.2s ease-out; +} +.block-header:before, +.block-header:after { + content: " "; + display: table; +} +.block-header:after { + clear: both; +} +.block-title { + font-size: 15px; + font-weight: 600; + text-transform: uppercase; + line-height: 1.2; +} +.block-title.text-normal { + text-transform: none; +} +.block-title small { + font-size: 13px; + font-weight: normal; + text-transform: none; +} +.block-content { + margin: 0 auto; + padding: 20px 20px 1px; + max-width: 100%; + overflow-x: visible; + -webkit-transition: opacity 0.2s ease-out; + transition: opacity 0.2s ease-out; +} +.block-content p, +.block-content .push, +.block-content .block, +.block-content .items-push > div { + margin-bottom: 20px; +} +.block-content .items-push-2x > div { + margin-bottom: 40px; +} +.block-content .items-push-3x > div { + margin-bottom: 60px; +} +.block-content.block-content-full { + padding-bottom: 20px; +} +.block-content.block-content-full .pull-b { + margin-bottom: -20px; +} +.block-content .pull-t { + margin-top: -20px; +} +.block-content .pull-r-l { + margin-right: -20px; + margin-left: -20px; +} +.block-content .pull-b { + margin-bottom: -1px; +} +.block-content.block-content-mini { + padding-top: 10px; +} +.block-content.block-content-mini.block-content-full { + padding-bottom: 10px; +} +@media screen and (min-width: 1200px) { + .block-content.block-content-narrow { + padding-left: 10%; + padding-right: 10%; + } +} +.block.block-full .block-content { + padding-bottom: 20px; +} +.block.block-full .block-content.block-content-mini { + padding-bottom: 10px; +} +.block-table { + width: 100%; +} +.block-table td { + padding: 10px; + vertical-align: middle; +} +.block.block-bordered { + border: 1px solid #e9e9e9; + -webkit-box-shadow: none; + box-shadow: none; +} +.block.block-bordered > .block-header { + border-bottom: 1px solid #e9e9e9; +} +.block.block-rounded { + border-radius: 4px; +} +.block.block-rounded > .block-header { + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} +.block.block-rounded > .block-content:first-child { + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} +.block.block-rounded > .block-content:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.block.block-themed > .block-header { + border-bottom: none; +} +.block.block-themed > .block-header > .block-title { + color: #fff; +} +.block.block-themed > .block-header > .block-title small { + color: rgba(255, 255, 255, 0.75); +} +.block.block-transparent { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} +.block.block-opt-refresh { + position: relative; +} +.block.block-opt-refresh > .block-header { + opacity: .25; +} +.block.block-opt-refresh > .block-content { + opacity: .15; +} +.block.block-opt-refresh:before { + position: absolute; + display: block; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + content: " "; +} +.block.block-opt-refresh:after { + position: absolute; + top: 50%; + left: 50%; + margin: -20px 0 0 -20px; + width: 40px; + height: 40px; + line-height: 40px; + color: #646464; + font-family: Simple-Line-Icons; + font-size: 18px; + text-align: center; + z-index: 2; + content: "\e09a"; + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} +.block.block-opt-refresh.block-opt-refresh-icon2:after { + content: "\e06e"; +} +.block.block-opt-refresh.block-opt-refresh-icon3:after { + content: "\e020"; +} +.block.block-opt-refresh.block-opt-refresh-icon4:after { + font-family: 'FontAwesome'; + content: "\f021"; +} +.block.block-opt-refresh.block-opt-refresh-icon5:after { + font-family: 'FontAwesome'; + content: "\f185"; +} +.block.block-opt-refresh.block-opt-refresh-icon6:after { + font-family: 'FontAwesome'; + content: "\f1ce"; +} +.block.block-opt-refresh.block-opt-refresh-icon7:after { + font-family: 'FontAwesome'; + content: "\f250"; +} +.block.block-opt-refresh.block-opt-refresh-icon8:after { + font-family: 'FontAwesome'; + content: "\f01e"; +} +.ie9 .block.block-opt-refresh:after { + content: "Loading.." !important; +} +.block.block-opt-fullscreen { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + margin-bottom: 0; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; +} +.block.block-opt-hidden.block-bordered > .block-header { + border-bottom: none; +} +.block.block-opt-hidden > .block-content { + display: none; +} +a.block { + display: block; + color: #646464; + font-weight: normal; + -webkit-transition: all 0.15s ease-out; + transition: all 0.15s ease-out; +} +a.block:hover { + color: #646464; + opacity: .9; +} +a.block.block-link-hover1:hover { + -webkit-box-shadow: 0 2px rgba(0, 0, 0, 0.1); + box-shadow: 0 2px rgba(0, 0, 0, 0.1); + opacity: 1; +} +a.block.block-link-hover1:active { + -webkit-box-shadow: 0 2px rgba(0, 0, 0, 0.01); + box-shadow: 0 2px rgba(0, 0, 0, 0.01); +} +a.block.block-link-hover2:hover { + -webkit-transform: translateY(-2px); + -ms-transform: translateY(-2px); + transform: translateY(-2px); + -webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1); + opacity: 1; +} +a.block.block-link-hover2:active { + -webkit-transform: translateY(-1px); + -ms-transform: translateY(-1px); + transform: translateY(-1px); + -webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.05); +} +a.block.block-link-hover3:hover { + -webkit-box-shadow: 0 0 12px rgba(0, 0, 0, 0.1); + box-shadow: 0 0 12px rgba(0, 0, 0, 0.1); + opacity: 1; +} +a.block.block-link-hover3:active { + -webkit-box-shadow: 0 0 2px rgba(0, 0, 0, 0.1); + box-shadow: 0 0 2px rgba(0, 0, 0, 0.1); +} +.block > .nav-tabs { + background-color: #f9f9f9; + border-bottom: none; +} +.block > .nav-tabs.nav-tabs-right > li { + float: right; +} +.block > .nav-tabs.nav-justified > li > a { + margin-bottom: 0; +} +.block > .nav-tabs > li { + margin-bottom: 0; +} +.block > .nav-tabs > li > a { + margin-right: 0; + padding-top: 12px; + padding-bottom: 12px; + color: #646464; + font-weight: 600; + border: 1px solid transparent; + border-radius: 0; +} +.block > .nav-tabs > li > a:hover { + color: #5c90d2; + background-color: transparent; + border-color: transparent; +} +.block > .nav-tabs > li.active > a, +.block > .nav-tabs > li.active > a:hover, +.block > .nav-tabs > li.active > a:focus { + color: #646464; + background-color: #fff; + border-color: transparent; +} +.block > .nav-tabs.nav-tabs-alt { + background-color: transparent; + border-bottom: 1px solid #e9e9e9; +} +.block > .nav-tabs.nav-tabs-alt > li > a { + -webkit-transition: all 0.15s ease-out; + transition: all 0.15s ease-out; +} +.block > .nav-tabs.nav-tabs-alt > li > a:hover { + -webkit-box-shadow: 0 2px #5c90d2; + box-shadow: 0 2px #5c90d2; +} +.block > .nav-tabs.nav-tabs-alt > li.active > a, +.block > .nav-tabs.nav-tabs-alt > li.active > a:hover, +.block > .nav-tabs.nav-tabs-alt > li.active > a:focus { + -webkit-box-shadow: 0 2px #5c90d2; + box-shadow: 0 2px #5c90d2; +} +.block .block-content.tab-content { + /*overflow: hidden;*/ +} +.block-options-simple { + float: right; + margin: -3px 0 -3px 15px; + padding: 1px 0; + min-height: 24px; +} +.block-options-simple.block-options-simple-left { + float: left; + margin-right: 15px; + margin-left: 0; +} +.block-options-simple.block-options-simple-left + .block-title { + float: right; +} +.block-options { + float: right; + margin: -3px 0 -3px 15px; + padding: 0; + height: 24px; + list-style: none; +} +.block-options:before, +.block-options:after { + content: " "; + display: table; +} +.block-options:after { + clear: both; +} +.block-options.block-options-left { + float: left; + margin-right: 15px; + margin-left: 0; +} +.block-options.block-options-left + .block-title { + float: right; +} +.block-options > li { + display: inline-block; + margin: 0 2px; + padding: 0; +} +.block-options > li > a, +.block-options > li > button { + display: block; + padding: 2px 3px; + color: #999999; + opacity: .6; +} +.block.block-themed > .block-header .block-options > li > a, +.block.block-themed > .block-header .block-options > li > button { + color: #fff; +} +.block-options > li > a:hover, +.block-options > li > button:hover { + text-decoration: none; + opacity: 1; +} +.block-options > li > a:active, +.block-options > li > button:active { + opacity: .6; +} +.block-options > li > span { + display: block; + padding: 2px 3px; +} +.block.block-themed > .block-header .block-options > li > span { + color: #fff; +} +.block-options > li > a:focus { + text-decoration: none; + opacity: 1; +} +.block-options > li > button { + background: none; + border: none; +} +.block-options > li.active > a, +.block-options > li.open > button { + text-decoration: none; + opacity: 1; +} +.nav-main { + margin: 0 -20px; + padding: 0; + list-style: none; +} +.nav-main .nav-main-heading { + padding: 22px 20px 6px 20px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + color: rgba(255, 255, 255, 0.3); +} +.nav-main a { + display: block; + padding: 10px 20px; + color: rgba(255, 255, 255, 0.5); +} +.nav-main a:hover, +.nav-main a:focus { + color: rgba(255, 255, 255, 0.5); + background-color: rgba(0, 0, 0, 0.2); +} +.nav-main a:hover > i, +.nav-main a:focus > i { + color: #fff; +} +.nav-main a.active, +.nav-main a.active:hover { + color: #fff; +} +.nav-main a.active > i, +.nav-main a.active:hover > i { + color: #fff; +} +.nav-main a > i { + margin-right: 15px; + color: rgba(255, 255, 255, 0.2); +} +.nav-main a.nav-submenu { + position: relative; + padding-right: 30px; +} +.nav-main a.nav-submenu:before { + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + right: 15px; + display: inline-block; + font-family: 'FontAwesome'; + color: rgba(255, 255, 255, 0.25); + content: "\f104"; +} +.nav-main ul { + margin: 0; + padding: 0 0 0 50px; + height: 0; + list-style: none; + background-color: rgba(0, 0, 0, 0.15); + overflow: hidden; +} +.nav-main ul > li { + opacity: 0; + -webkit-transition: all 0.25s ease-out; + transition: all 0.25s ease-out; + -webkit-transform: translateX(-15px); + -ms-transform: translateX(-15px); + transform: translateX(-15px); +} +.nav-main ul .nav-main-heading { + padding-left: 0; + padding-right: 0; + color: rgba(255, 255, 255, 0.65); +} +.nav-main ul a { + padding: 8px 8px 8px 0; + font-size: 13px; + color: rgba(255, 255, 255, 0.4); +} +.nav-main ul a:hover, +.nav-main ul a:focus { + color: #fff; + background-color: transparent; +} +.nav-main ul a > i { + margin-right: 10px; +} +.nav-main ul ul { + padding-left: 12px; +} +.nav-main li.open > a.nav-submenu { + color: #fff; +} +.nav-main li.open > a.nav-submenu > i { + color: #fff; +} +.nav-main li.open > a.nav-submenu:before { + content: "\f107"; +} +.nav-main li.open > ul { + height: auto; +} +.nav-main li.open > ul > li { + opacity: 1; + -webkit-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); +} +.nav-main-header { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: 0; + padding: 20px; + width: 100%; + list-style: none; + background-color: #2c343f; + z-index: 1031; + opacity: 0; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + -webkit-transition: all 0.28s ease-out; + transition: all 0.28s ease-out; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-transform: translateX(0) translateY(-100%) translateZ(0); + -ms-transform: translateX(0) translateY(-100%); + transform: translateX(0) translateY(-100%) translateZ(0); +} +.nav-main-header.nav-main-header-o { + -webkit-transform: translateX(0) translateY(0) translateZ(0); + -ms-transform: translateX(0) translateY(0); + transform: translateX(0) translateY(0) translateZ(0); + opacity: 1; +} +.nav-main-header > li { + margin: 0 0 10px; +} +.nav-main-header a { + display: block; + padding: 0 12px; + min-height: 34px; + color: rgba(255, 255, 255, 0.5); + font-weight: 600; + line-height: 34px; +} +.nav-main-header a:hover, +.nav-main-header a:focus, +.nav-main-header a.active { + color: #fff; +} +.nav-main-header a.nav-submenu { + position: relative; + padding-right: 32px; +} +.nav-main-header a.nav-submenu:before { + position: absolute; + right: 10px; + font-family: 'FontAwesome'; + content: "\f107"; +} +.nav-main-header ul { + margin: 0 0 0 15px; + padding: 0; + list-style: none; + display: none; +} +.nav-main-header ul a { + min-height: 32px; + font-size: 13px; + font-weight: 400; + line-height: 32px; +} +.nav-main-header > li:hover > a.nav-submenu { + color: #fff; +} +.nav-main-header > li:hover > ul { + display: block; +} +@media screen and (min-width: 992px) { + .nav-main-header { + position: static; + top: auto; + right: auto; + bottom: auto; + left: auto; + padding: 0; + width: auto; + background-color: transparent; + z-index: auto; + opacity: 1; + overflow-y: visible; + -webkit-overflow-scrolling: auto; + -webkit-transition: none; + transition: none; + -webkit-backface-visibility: visible; + backface-visibility: visible; + -webkit-transform: translateX(0) translateY(0) translateZ(0); + -ms-transform: translateX(0) translateY(0); + transform: translateX(0) translateY(0) translateZ(0); + } + .nav-main-header > li { + position: relative; + margin: 0 10px 0 0; + float: left; + } + .nav-main-header ul { + position: absolute; + left: 0; + margin: 0; + padding: 13px 0; + min-width: 160px; + background-color: #3e4a59; + } + .nav-main-header > li:last-child > ul { + left: auto; + right: 0; + } +} +.nav-header { + margin: 0; + padding: 0; + list-style: none; +} +.nav-header:before, +.nav-header:after { + content: " "; + display: table; +} +.nav-header:after { + clear: both; +} +.nav-header > li { + margin-right: 12px; + float: left; +} +.nav-header > li > a, +.nav-header > li > .btn-group > a { + padding: 0 12px; + display: block; + height: 34px; + line-height: 34px; + font-weight: 600; +} +.nav-header.pull-right > li { + margin-right: 0; + margin-left: 12px; + float: left; +} +.nav-header .header-content { + line-height: 34px; +} +.nav-header .header-search { + width: 360px; +} +@media screen and (max-width: 767px) { + .nav-header .header-search { + display: none; + } + .nav-header .header-search.header-search-xs-visible { + position: absolute; + top: 60px; + right: 0; + left: 0; + z-index: 999; + display: block; + width: 100%; + border-top: 1px solid #f9f9f9; + } + .nav-header .header-search.header-search-xs-visible > form { + padding: 14px 14px; + background-color: #fff; + -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.02); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.02); + } +} +.nav-users { + margin: 0; + padding: 0; + list-style: none; +} +.nav-users > li:last-child > a { + border-bottom: none; +} +.nav-users a { + position: relative; + padding: 12px 8px 8px 71px; + display: block; + min-height: 62px; + font-weight: 600; + border-bottom: 1px solid #f3f3f3; +} +.nav-users a > img { + position: absolute; + left: 12px; + top: 10px; + width: 42px; + height: 42px; + border-radius: 50%; +} +.nav-users a > i { + position: absolute; + left: 40px; + top: 40px; + display: inline-block; + width: 18px; + height: 18px; + line-height: 18px; + text-align: center; + background-color: #fff; + border-radius: 50%; +} +.nav-users a:hover { + background-color: #f9f9f9; +} +.list { + margin: 0; + padding: 0; + list-style: none; +} +.list > li { + position: relative; +} +.list-timeline { + position: relative; + padding-top: 10px; +} +.list-timeline > li { + margin-bottom: 10px; +} +.list-timeline .list-timeline-time { + margin: 0 -20px; + padding: 10px 20px 10px 40px; + min-height: 40px; + text-align: right; + color: #999; + font-size: 13px; + font-style: italic; + background-color: #f9f9f9; + border-radius: 2px; +} +.list-timeline .list-timeline-icon { + position: absolute; + top: 5px; + left: 10px; + width: 30px; + height: 30px; + line-height: 30px; + color: #fff; + text-align: center; + border-radius: 50%; +} +.list-timeline .list-timeline-content { + padding: 10px 10px 1px; +} +.list-timeline .list-timeline-content > p:first-child { + margin-bottom: 0; +} +@media screen and (min-width: 768px) { + .list-timeline { + padding-top: 20px; + } + .list-timeline:before { + position: absolute; + top: 0; + left: 120px; + bottom: 0; + display: block; + width: 4px; + content: ""; + background-color: #f9f9f9; + z-index: 1; + } + .list-timeline > li { + min-height: 40px; + z-index: 2; + } + .list-timeline > li:last-child { + margin-bottom: 0; + } + .list-timeline .list-timeline-time { + position: absolute; + top: 0; + left: 0; + margin: 0; + padding-right: 0; + padding-left: 0; + width: 90px; + background-color: transparent; + } + .list-timeline .list-timeline-icon { + top: 3px; + left: 105px; + width: 34px; + height: 34px; + line-height: 34px; + z-index: 2 !important; + } + .list-timeline .list-timeline-content { + padding-left: 160px; + } +} +.list-activity > li { + margin-bottom: 7px; + padding-bottom: 7px; + padding-left: 40px; + font-size: 13px; + border-bottom: 1px solid #f3f3f3; +} +.list-activity > li > i:first-child { + position: absolute; + left: 10px; + top: 0; + display: inline-block; + width: 20px; + height: 20px; + line-height: 20px; + font-size: 14px; + text-align: center; +} +.list-activity > li:last-child { + border-bottom: none; +} +.list-events > li { + margin-bottom: 5px; + padding: 8px 30px 8px 10px; + color: rgba(0, 0, 0, 0.5); + font-size: 13px; + font-weight: 700; + background-color: #b5d0eb; +} +.list-events > li:before { + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + right: 10px; + display: inline-block; + font-family: 'FontAwesome'; + color: rgba(255, 255, 255, 0.75); + content: "\f073"; +} +.list-events > li:hover { + cursor: move; +} +.list-simple > li { + margin-bottom: 20px; +} +.list-simple-mini > li { + margin-bottom: 10px; +} +.list-li-clearfix > li:before, +.list-li-clearfix > li:after { + content: " "; + display: table; +} +.list-li-clearfix > li:after { + clear: both; +} +.img-avatar { + display: inline-block !important; + width: 64px; + height: 64px; + border-radius: 50%; +} +.img-avatar.img-avatar16 { + width: 16px; + height: 16px; +} +.img-avatar.img-avatar32 { + width: 32px; + height: 32px; +} +.img-avatar.img-avatar48 { + width: 48px; + height: 48px; +} +.img-avatar.img-avatar96 { + width: 96px; + height: 96px; +} +.img-avatar.img-avatar128 { + width: 128px; + height: 128px; +} +.img-avatar-thumb { + margin: 5px; + -webkit-box-shadow: 0 0 0 5px rgba(255, 255, 255, 0.4); + box-shadow: 0 0 0 5px rgba(255, 255, 255, 0.4); +} +.img-thumb { + padding: 5px; + background-color: #fff; + border-radius: 2px; +} +.img-link { + display: inline-block; + cursor: -webkit-zoom-in; + cursor: zoom-in; + -webkit-transition: -webkit-transform 0.15s ease-out; + transition: transform 0.15s ease-out; +} +.img-link:hover { + -webkit-transform: rotate(1deg); + -ms-transform: rotate(1deg); + transform: rotate(1deg); +} +.img-overlay { + position: relative; +} +.img-overlay:after { + position: absolute; + top: 0; + right: 0; + bottom: 50%; + left: 0; + content: ''; + background: -moz-linear-gradient(top, #ffffff 0%, rgba(255, 255, 255, 0) 100%); + background: -webkit-linear-gradient(top, #ffffff 0%, rgba(255, 255, 255, 0) 100%); + background: linear-gradient(to bottom, #ffffff 0%, rgba(255, 255, 255, 0) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#00ffffff', GradientType=0); +} +.img-overlay.img-overlay-bottom:after { + top: 50%; + bottom: 0; + background: -moz-linear-gradient(top, rgba(255, 255, 255, 0) 0%, #ffffff 100%); + background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0) 0%, #ffffff 100%); + background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, #ffffff 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffffff', endColorstr='#ffffff', GradientType=0); +} +.img-overlay.img-overlay-left:after { + right: 50%; + bottom: 0; + background: -moz-linear-gradient(left, #ffffff 0%, rgba(255, 255, 255, 0) 100%); + background: -webkit-linear-gradient(left, #ffffff 0%, rgba(255, 255, 255, 0) 100%); + background: linear-gradient(to right, #ffffff 0%, rgba(255, 255, 255, 0) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#00ffffff', GradientType=1); +} +.img-overlay.img-overlay-right:after { + left: 50%; + bottom: 0; + background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, #ffffff 100%); + background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, #ffffff 100%); + background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, #ffffff 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffffff', endColorstr='#ffffff', GradientType=1); +} +.img-overlay > img { + display: block; + width: 100%; + height: auto; +} +.img-container { + position: relative; + overflow: hidden; + z-index: 0; + display: block; +} +.img-container .img-options { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + content: ""; + background-color: rgba(0, 0, 0, 0.6); + opacity: 0; + visibility: none; + -webkit-transition: all 0.25s ease-out; + transition: all 0.25s ease-out; +} +.img-container .img-options-content { + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + right: 0; + left: 0; + text-align: center; +} +.img-container > img { + -webkit-transition: -webkit-transform 0.35s ease-out; + transition: transform 0.35s ease-out; +} +.img-container:hover .img-options { + opacity: 1; + visibility: visible; +} +@media screen and (max-width: 767px) { + .img-container .img-options { + display: none; + } + .img-container:hover .img-options { + display: block; + } +} +.img-container.fx-img-zoom-in:hover > img { + -webkit-transform: scale(1.2); + -ms-transform: scale(1.2); + transform: scale(1.2); +} +.img-container.fx-img-rotate-r:hover > img { + -webkit-transform: scale(1.4) rotate(8deg); + -ms-transform: scale(1.4) rotate(8deg); + transform: scale(1.4) rotate(8deg); +} +.img-container.fx-img-rotate-l:hover > img { + -webkit-transform: scale(1.4) rotate(-8deg); + -ms-transform: scale(1.4) rotate(-8deg); + transform: scale(1.4) rotate(-8deg); +} +.img-container.fx-opt-slide-top .img-options { + -webkit-transform: translateY(100%); + -ms-transform: translateY(100%); + transform: translateY(100%); +} +.img-container.fx-opt-slide-top:hover .img-options { + -webkit-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); +} +.img-container.fx-opt-slide-right .img-options { + -webkit-transform: translateX(-100%); + -ms-transform: translateX(-100%); + transform: translateX(-100%); +} +.img-container.fx-opt-slide-right:hover .img-options { + -webkit-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); +} +.img-container.fx-opt-slide-down .img-options { + -webkit-transform: translateY(-100%); + -ms-transform: translateY(-100%); + transform: translateY(-100%); +} +.img-container.fx-opt-slide-down:hover .img-options { + -webkit-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); +} +.img-container.fx-opt-slide-left .img-options { + -webkit-transform: translateX(100%); + -ms-transform: translateX(100%); + transform: translateX(100%); +} +.img-container.fx-opt-slide-left:hover .img-options { + -webkit-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); +} +.img-container.fx-opt-zoom-in .img-options { + -webkit-transform: scale(0); + -ms-transform: scale(0); + transform: scale(0); +} +.img-container.fx-opt-zoom-in:hover .img-options { + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); +} +.img-container.fx-opt-zoom-out .img-options { + -webkit-transform: scale(2); + -ms-transform: scale(2); + transform: scale(2); +} +.img-container.fx-opt-zoom-out:hover .img-options { + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); +} +.push-5 { + margin-bottom: 5px !important; +} +.push-5-t { + margin-top: 5px !important; +} +.push-5-r { + margin-right: 5px !important; +} +.push-5-l { + margin-left: 5px !important; +} +.push-10 { + margin-bottom: 10px !important; +} +.push-10-t { + margin-top: 10px !important; +} +.push-10-r { + margin-right: 10px !important; +} +.push-10-l { + margin-left: 10px !important; +} +.push-15 { + margin-bottom: 15px !important; +} +.push-15-t { + margin-top: 15px !important; +} +.push-15-r { + margin-right: 15px !important; +} +.push-15-l { + margin-left: 15px !important; +} +.push-20 { + margin-bottom: 20px !important; +} +.push-20-t { + margin-top: 20px !important; +} +.push-20-r { + margin-right: 20px !important; +} +.push-20-l { + margin-left: 20px !important; +} +.push-30 { + margin-bottom: 30px !important; +} +.push-30-t { + margin-top: 30px !important; +} +.push-30-r { + margin-right: 30px !important; +} +.push-30-l { + margin-left: 30px !important; +} +.push-50 { + margin-bottom: 50px !important; +} +.push-50-t { + margin-top: 50px !important; +} +.push-50-r { + margin-right: 50px !important; +} +.push-50-l { + margin-left: 50px !important; +} +.push-100 { + margin-bottom: 100px !important; +} +.push-100-t { + margin-top: 100px !important; +} +.push-100-r { + margin-right: 100px !important; +} +.push-100-l { + margin-left: 100px !important; +} +.push-150 { + margin-bottom: 150px !important; +} +.push-150-t { + margin-top: 150px !important; +} +.push-150-r { + margin-right: 150px !important; +} +.push-150-l { + margin-left: 150px !important; +} +.push-200 { + margin-bottom: 200px !important; +} +.push-200-t { + margin-top: 200px !important; +} +.push-200-r { + margin-right: 200px !important; +} +.push-200-l { + margin-left: 200px !important; +} +.push-300 { + margin-bottom: 300px !important; +} +.push-300-t { + margin-top: 300px !important; +} +.push-300-r { + margin-right: 300px !important; +} +.push-300-l { + margin-left: 300px !important; +} +.pulldown { + position: relative; + top: 50px; +} +@media screen and (min-width: 992px) { + .pulldown { + top: 150px; + } +} +.remove-margin { + margin: 0 !important; +} +.remove-margin-t { + margin-top: 0 !important; +} +.remove-margin-r { + margin-right: 0 !important; +} +.remove-margin-b { + margin-bottom: 0 !important; +} +.remove-margin-l { + margin-left: 0 !important; +} +.remove-padding { + padding: 0 !important; +} +.remove-padding-t { + padding-top: 0 !important; +} +.remove-padding-r { + padding-right: 0 !important; +} +.remove-padding-b { + padding-bottom: 0 !important; +} +.remove-padding-l { + padding-left: 0 !important; +} +.mheight-50 { + min-height: 50px; +} +.mheight-75 { + min-height: 75px; +} +.mheight-100 { + min-height: 100px; +} +.mheight-125 { + min-height: 125px; +} +.mheight-150 { + min-height: 150px; +} +.mheight-175 { + min-height: 175px; +} +.mheight-200 { + min-height: 200px; +} +.align-v { + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); +} +.align-v.align-v-fwidth { + width: 100%; +} +.border { + border: 1px solid #e9e9e9; +} +.border-t { + border-top: 1px solid #e9e9e9; +} +.border-r { + border-right: 1px solid #e9e9e9; +} +.border-b { + border-bottom: 1px solid #e9e9e9; +} +.border-l { + border-left: 1px solid #e9e9e9; +} +.border-white-op { + border: 1px solid rgba(255, 255, 255, 0.1); +} +.border-white-op-t { + border-top: 1px solid rgba(255, 255, 255, 0.1); +} +.border-white-op-r { + border-right: 1px solid rgba(255, 255, 255, 0.1); +} +.border-white-op-b { + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} +.border-white-op-l { + border-left: 1px solid rgba(255, 255, 255, 0.1); +} +.border-black-op { + border: 1px solid rgba(0, 0, 0, 0.1); +} +.border-black-op-t { + border-top: 1px solid rgba(0, 0, 0, 0.1); +} +.border-black-op-r { + border-right: 1px solid rgba(0, 0, 0, 0.1); +} +.border-black-op-b { + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} +.border-black-op-l { + border-left: 1px solid rgba(0, 0, 0, 0.1); +} +.item { + display: inline-block; + width: 60px; + height: 60px; + text-align: center; + font-size: 28px; + font-weight: 300; + line-height: 60px; +} +a.item:hover, +a.item:focus { + opacity: .6; +} +.item.item-circle { + border-radius: 50%; +} +.item.item-rounded { + border-radius: 3px; +} +.item.item-rounded-big { + border-radius: 24px; +} +.item.item-rounded-big.item-2x { + border-radius: 35px; +} +.item.item-rounded-big.item-3x { + border-radius: 50px; +} +.item.item-2x { + width: 100px; + height: 100px; + line-height: 100px; +} +.item.item-3x { + width: 150px; + height: 150px; + line-height: 150px; +} +.ribbon { + position: relative; + min-height: 56px; +} +.ribbon-box { + position: absolute; + top: 10px; + right: 0; + padding: 0 15px; + height: 36px; + line-height: 36px; + color: #fff; + z-index: 500; +} +.ribbon-box:before { + position: absolute; + display: block; + width: 0; + height: 0; + content: ""; +} +.ribbon-bookmark .ribbon-box { + padding-left: 0; +} +.ribbon-bookmark .ribbon-box:before { + top: 0; + right: 100%; + border: 18px solid; + border-left-width: 10px; +} +.ribbon-modern .ribbon-box { + top: 0; +} +.ribbon-modern .ribbon-box:before { + top: 0; + right: 100%; + border: 18px solid; +} +.ribbon-left .ribbon-box { + right: auto; + left: 0; +} +.ribbon-left.ribbon-bookmark .ribbon-box { + padding-left: 15px; + padding-right: 0; +} +.ribbon-left.ribbon-bookmark .ribbon-box:before { + right: auto; + left: 100%; + border-left-width: 18px; + border-right-width: 10px; +} +.ribbon-left.ribbon-modern .ribbon-box:before { + right: auto; + left: 100%; +} +.ribbon-bottom .ribbon-box { + top: auto; + bottom: 10px; +} +.ribbon-bottom.ribbon-modern .ribbon-box { + bottom: 0; +} +.ribbon-primary .ribbon-box { + background-color: #5c90d2; +} +.ribbon-primary.ribbon-bookmark .ribbon-box:before { + border-color: #5c90d2; + border-left-color: transparent; +} +.ribbon-primary.ribbon-bookmark.ribbon-left .ribbon-box:before { + border-color: #5c90d2; + border-right-color: transparent; +} +.ribbon-primary.ribbon-modern .ribbon-box:before { + border-color: #5c90d2; + border-left-color: transparent; + border-bottom-color: transparent; +} +.ribbon-primary.ribbon-modern.ribbon-bottom .ribbon-box:before { + border-color: #5c90d2; + border-top-color: transparent; + border-left-color: transparent; +} +.ribbon-primary.ribbon-modern.ribbon-left .ribbon-box:before { + border-color: #5c90d2; + border-right-color: transparent; + border-bottom-color: transparent; +} +.ribbon-primary.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before { + border-color: #5c90d2; + border-top-color: transparent; + border-right-color: transparent; +} +.ribbon-success .ribbon-box { + background-color: #46c37b; +} +.ribbon-success.ribbon-bookmark .ribbon-box:before { + border-color: #46c37b; + border-left-color: transparent; +} +.ribbon-success.ribbon-bookmark.ribbon-left .ribbon-box:before { + border-color: #46c37b; + border-right-color: transparent; +} +.ribbon-success.ribbon-modern .ribbon-box:before { + border-color: #46c37b; + border-left-color: transparent; + border-bottom-color: transparent; +} +.ribbon-success.ribbon-modern.ribbon-bottom .ribbon-box:before { + border-color: #46c37b; + border-top-color: transparent; + border-left-color: transparent; +} +.ribbon-success.ribbon-modern.ribbon-left .ribbon-box:before { + border-color: #46c37b; + border-right-color: transparent; + border-bottom-color: transparent; +} +.ribbon-success.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before { + border-color: #46c37b; + border-top-color: transparent; + border-right-color: transparent; +} +.ribbon-info .ribbon-box { + background-color: #70b9eb; +} +.ribbon-info.ribbon-bookmark .ribbon-box:before { + border-color: #70b9eb; + border-left-color: transparent; +} +.ribbon-info.ribbon-bookmark.ribbon-left .ribbon-box:before { + border-color: #70b9eb; + border-right-color: transparent; +} +.ribbon-info.ribbon-modern .ribbon-box:before { + border-color: #70b9eb; + border-left-color: transparent; + border-bottom-color: transparent; +} +.ribbon-info.ribbon-modern.ribbon-bottom .ribbon-box:before { + border-color: #70b9eb; + border-top-color: transparent; + border-left-color: transparent; +} +.ribbon-info.ribbon-modern.ribbon-left .ribbon-box:before { + border-color: #70b9eb; + border-right-color: transparent; + border-bottom-color: transparent; +} +.ribbon-info.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before { + border-color: #70b9eb; + border-top-color: transparent; + border-right-color: transparent; +} +.ribbon-warning .ribbon-box { + background-color: #f3b760; +} +.ribbon-warning.ribbon-bookmark .ribbon-box:before { + border-color: #f3b760; + border-left-color: transparent; +} +.ribbon-warning.ribbon-bookmark.ribbon-left .ribbon-box:before { + border-color: #f3b760; + border-right-color: transparent; +} +.ribbon-warning.ribbon-modern .ribbon-box:before { + border-color: #f3b760; + border-left-color: transparent; + border-bottom-color: transparent; +} +.ribbon-warning.ribbon-modern.ribbon-bottom .ribbon-box:before { + border-color: #f3b760; + border-top-color: transparent; + border-left-color: transparent; +} +.ribbon-warning.ribbon-modern.ribbon-left .ribbon-box:before { + border-color: #f3b760; + border-right-color: transparent; + border-bottom-color: transparent; +} +.ribbon-warning.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before { + border-color: #f3b760; + border-top-color: transparent; + border-right-color: transparent; +} +.ribbon-danger .ribbon-box { + background-color: #d26a5c; +} +.ribbon-danger.ribbon-bookmark .ribbon-box:before { + border-color: #d26a5c; + border-left-color: transparent; +} +.ribbon-danger.ribbon-bookmark.ribbon-left .ribbon-box:before { + border-color: #d26a5c; + border-right-color: transparent; +} +.ribbon-danger.ribbon-modern .ribbon-box:before { + border-color: #d26a5c; + border-left-color: transparent; + border-bottom-color: transparent; +} +.ribbon-danger.ribbon-modern.ribbon-bottom .ribbon-box:before { + border-color: #d26a5c; + border-top-color: transparent; + border-left-color: transparent; +} +.ribbon-danger.ribbon-modern.ribbon-left .ribbon-box:before { + border-color: #d26a5c; + border-right-color: transparent; + border-bottom-color: transparent; +} +.ribbon-danger.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before { + border-color: #d26a5c; + border-top-color: transparent; + border-right-color: transparent; +} +.ribbon-crystal .ribbon-box { + background-color: rgba(255, 255, 255, 0.35); +} +.ribbon-crystal.ribbon-bookmark .ribbon-box:before { + border-color: rgba(255, 255, 255, 0.35); + border-left-color: transparent; +} +.ribbon-crystal.ribbon-bookmark.ribbon-left .ribbon-box:before { + border-color: rgba(255, 255, 255, 0.35); + border-right-color: transparent; +} +.ribbon-crystal.ribbon-modern .ribbon-box:before { + border-color: rgba(255, 255, 255, 0.35); + border-left-color: transparent; + border-bottom-color: transparent; +} +.ribbon-crystal.ribbon-modern.ribbon-bottom .ribbon-box:before { + border-color: rgba(255, 255, 255, 0.35); + border-top-color: transparent; + border-left-color: transparent; +} +.ribbon-crystal.ribbon-modern.ribbon-left .ribbon-box:before { + border-color: rgba(255, 255, 255, 0.35); + border-right-color: transparent; + border-bottom-color: transparent; +} +.ribbon-crystal.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before { + border-color: rgba(255, 255, 255, 0.35); + border-top-color: transparent; + border-right-color: transparent; +} +.overflow-hidden { + overflow: hidden; +} +.overflow-y-auto { + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} +.visibility-hidden { + visibility: hidden; +} +.visible-ie9 { + display: none; +} +.ie9 .hidden-ie9 { + display: none !important; +} +.ie9 .visible-ie9 { + display: block; +} +.ie9 .visible-ie9-ib { + display: inline-block; +} +.text-default { + color: #5c90d2; +} +a.text-default:hover, +a.text-default:active, +a.text-default:focus, +button.text-default:hover, +button.text-default:active, +button.text-default:focus { + color: #5c90d2; + opacity: .75; +} +.text-default-dark { + color: #3e4a59; +} +a.text-default-dark:hover, +a.text-default-dark:active, +a.text-default-dark:focus, +button.text-default-dark:hover, +button.text-default-dark:active, +button.text-default-dark:focus { + color: #3e4a59; + opacity: .75; +} +.text-default-darker { + color: #2c343f; +} +a.text-default-darker:hover, +a.text-default-darker:active, +a.text-default-darker:focus, +button.text-default-darker:hover, +button.text-default-darker:active, +button.text-default-darker:focus { + color: #2c343f; + opacity: .75; +} +.text-default-light { + color: #98b9e3; +} +a.text-default-light:hover, +a.text-default-light:active, +a.text-default-light:focus, +button.text-default-light:hover, +button.text-default-light:active, +button.text-default-light:focus { + color: #98b9e3; + opacity: .75; +} +.text-default-lighter { + color: #ccdcf1; +} +a.text-default-lighter:hover, +a.text-default-lighter:active, +a.text-default-lighter:focus, +button.text-default-lighter:hover, +button.text-default-lighter:active, +button.text-default-lighter:focus { + color: #ccdcf1; + opacity: .75; +} +.bg-default { + background-color: #5c90d2; +} +a.bg-default:hover, +a.bg-default:focus { + background-color: #3675c5; +} +.bg-default-op { + background-color: rgba(92, 144, 210, 0.75); +} +a.bg-default-op:hover, +a.bg-default-op:focus { + background-color: rgba(54, 117, 197, 0.75); +} +.bg-default-dark { + background-color: #3e4a59; +} +a.bg-default-dark:hover, +a.bg-default-dark:focus { + background-color: #29313b; +} +.bg-default-dark-op { + background-color: rgba(62, 74, 89, 0.83); +} +a.bg-default-dark-op:hover, +a.bg-default-dark-op:focus { + background-color: rgba(41, 49, 59, 0.83); +} +.bg-default-darker { + background-color: #2c343f; +} +a.bg-default-darker:hover, +a.bg-default-darker:focus { + background-color: #171b21; +} +.bg-default-light { + background-color: #98b9e3; +} +a.bg-default-light:hover, +a.bg-default-light:focus { + background-color: #709ed8; +} +.bg-default-lighter { + background-color: #ccdcf1; +} +a.bg-default-lighter:hover, +a.bg-default-lighter:focus { + background-color: #a4c1e6; +} +.text-amethyst { + color: #a48ad4; +} +a.text-amethyst:hover, +a.text-amethyst:active, +a.text-amethyst:focus, +button.text-amethyst:hover, +button.text-amethyst:active, +button.text-amethyst:focus { + color: #a48ad4; + opacity: .75; +} +.text-amethyst-dark { + color: #4f546b; +} +a.text-amethyst-dark:hover, +a.text-amethyst-dark:active, +a.text-amethyst-dark:focus, +button.text-amethyst-dark:hover, +button.text-amethyst-dark:active, +button.text-amethyst-dark:focus { + color: #4f546b; + opacity: .75; +} +.text-amethyst-darker { + color: #353847; +} +a.text-amethyst-darker:hover, +a.text-amethyst-darker:active, +a.text-amethyst-darker:focus, +button.text-amethyst-darker:hover, +button.text-amethyst-darker:active, +button.text-amethyst-darker:focus { + color: #353847; + opacity: .75; +} +.text-amethyst-light { + color: #c7b7e4; +} +a.text-amethyst-light:hover, +a.text-amethyst-light:active, +a.text-amethyst-light:focus, +button.text-amethyst-light:hover, +button.text-amethyst-light:active, +button.text-amethyst-light:focus { + color: #c7b7e4; + opacity: .75; +} +.text-amethyst-lighter { + color: #e4dcf2; +} +a.text-amethyst-lighter:hover, +a.text-amethyst-lighter:active, +a.text-amethyst-lighter:focus, +button.text-amethyst-lighter:hover, +button.text-amethyst-lighter:active, +button.text-amethyst-lighter:focus { + color: #e4dcf2; + opacity: .75; +} +.bg-amethyst { + background-color: #a48ad4; +} +a.bg-amethyst:hover, +a.bg-amethyst:focus { + background-color: #8765c6; +} +.bg-amethyst-op { + background-color: rgba(164, 138, 212, 0.75); +} +a.bg-amethyst-op:hover, +a.bg-amethyst-op:focus { + background-color: rgba(135, 101, 198, 0.75); +} +.bg-amethyst-dark { + background-color: #4f546b; +} +a.bg-amethyst-dark:hover, +a.bg-amethyst-dark:focus { + background-color: #393d4e; +} +.bg-amethyst-dark-op { + background-color: rgba(79, 84, 107, 0.83); +} +a.bg-amethyst-dark-op:hover, +a.bg-amethyst-dark-op:focus { + background-color: rgba(57, 61, 78, 0.83); +} +.bg-amethyst-darker { + background-color: #353847; +} +a.bg-amethyst-darker:hover, +a.bg-amethyst-darker:focus { + background-color: #1f212a; +} +.bg-amethyst-light { + background-color: #c7b7e4; +} +a.bg-amethyst-light:hover, +a.bg-amethyst-light:focus { + background-color: #aa91d7; +} +.bg-amethyst-lighter { + background-color: #e4dcf2; +} +a.bg-amethyst-lighter:hover, +a.bg-amethyst-lighter:focus { + background-color: #c7b7e4; +} +.text-city { + color: #ff6b6b; +} +a.text-city:hover, +a.text-city:active, +a.text-city:focus, +button.text-city:hover, +button.text-city:active, +button.text-city:focus { + color: #ff6b6b; + opacity: .75; +} +.text-city-dark { + color: #555; +} +a.text-city-dark:hover, +a.text-city-dark:active, +a.text-city-dark:focus, +button.text-city-dark:hover, +button.text-city-dark:active, +button.text-city-dark:focus { + color: #555; + opacity: .75; +} +.text-city-darker { + color: #333; +} +a.text-city-darker:hover, +a.text-city-darker:active, +a.text-city-darker:focus, +button.text-city-darker:hover, +button.text-city-darker:active, +button.text-city-darker:focus { + color: #333; + opacity: .75; +} +.text-city-light { + color: #ff8f8f; +} +a.text-city-light:hover, +a.text-city-light:active, +a.text-city-light:focus, +button.text-city-light:hover, +button.text-city-light:active, +button.text-city-light:focus { + color: #ff8f8f; + opacity: .75; +} +.text-city-lighter { + color: #ffb8b8; +} +a.text-city-lighter:hover, +a.text-city-lighter:active, +a.text-city-lighter:focus, +button.text-city-lighter:hover, +button.text-city-lighter:active, +button.text-city-lighter:focus { + color: #ffb8b8; + opacity: .75; +} +.bg-city { + background-color: #ff6b6b; +} +a.bg-city:hover, +a.bg-city:focus { + background-color: #ff3838; +} +.bg-city-op { + background-color: rgba(255, 107, 107, 0.75); +} +a.bg-city-op:hover, +a.bg-city-op:focus { + background-color: rgba(255, 56, 56, 0.75); +} +.bg-city-dark { + background-color: #555; +} +a.bg-city-dark:hover, +a.bg-city-dark:focus { + background-color: #3b3b3b; +} +.bg-city-dark-op { + background-color: rgba(85, 85, 85, 0.83); +} +a.bg-city-dark-op:hover, +a.bg-city-dark-op:focus { + background-color: rgba(59, 59, 59, 0.83); +} +.bg-city-darker { + background-color: #333; +} +a.bg-city-darker:hover, +a.bg-city-darker:focus { + background-color: #1a1a1a; +} +.bg-city-light { + background-color: #ff8f8f; +} +a.bg-city-light:hover, +a.bg-city-light:focus { + background-color: #ff5c5c; +} +.bg-city-lighter { + background-color: #ffb8b8; +} +a.bg-city-lighter:hover, +a.bg-city-lighter:focus { + background-color: #ff8585; +} +.text-flat { + color: #44b4a6; +} +a.text-flat:hover, +a.text-flat:active, +a.text-flat:focus, +button.text-flat:hover, +button.text-flat:active, +button.text-flat:focus { + color: #44b4a6; + opacity: .75; +} +.text-flat-dark { + color: #3f5259; +} +a.text-flat-dark:hover, +a.text-flat-dark:active, +a.text-flat-dark:focus, +button.text-flat-dark:hover, +button.text-flat-dark:active, +button.text-flat-dark:focus { + color: #3f5259; + opacity: .75; +} +.text-flat-darker { + color: #242f33; +} +a.text-flat-darker:hover, +a.text-flat-darker:active, +a.text-flat-darker:focus, +button.text-flat-darker:hover, +button.text-flat-darker:active, +button.text-flat-darker:focus { + color: #242f33; + opacity: .75; +} +.text-flat-light { + color: #83d0c7; +} +a.text-flat-light:hover, +a.text-flat-light:active, +a.text-flat-light:focus, +button.text-flat-light:hover, +button.text-flat-light:active, +button.text-flat-light:focus { + color: #83d0c7; + opacity: .75; +} +.text-flat-lighter { + color: #a8ded8; +} +a.text-flat-lighter:hover, +a.text-flat-lighter:active, +a.text-flat-lighter:focus, +button.text-flat-lighter:hover, +button.text-flat-lighter:active, +button.text-flat-lighter:focus { + color: #a8ded8; + opacity: .75; +} +.bg-flat { + background-color: #44b4a6; +} +a.bg-flat:hover, +a.bg-flat:focus { + background-color: #368f84; +} +.bg-flat-op { + background-color: rgba(68, 180, 166, 0.75); +} +a.bg-flat-op:hover, +a.bg-flat-op:focus { + background-color: rgba(54, 143, 132, 0.75); +} +.bg-flat-dark { + background-color: #3f5259; +} +a.bg-flat-dark:hover, +a.bg-flat-dark:focus { + background-color: #2a363b; +} +.bg-flat-dark-op { + background-color: rgba(63, 82, 89, 0.83); +} +a.bg-flat-dark-op:hover, +a.bg-flat-dark-op:focus { + background-color: rgba(42, 54, 59, 0.83); +} +.bg-flat-darker { + background-color: #242f33; +} +a.bg-flat-darker:hover, +a.bg-flat-darker:focus { + background-color: #0f1315; +} +.bg-flat-light { + background-color: #83d0c7; +} +a.bg-flat-light:hover, +a.bg-flat-light:focus { + background-color: #5ec2b6; +} +.bg-flat-lighter { + background-color: #a8ded8; +} +a.bg-flat-lighter:hover, +a.bg-flat-lighter:focus { + background-color: #83d0c7; +} +.text-modern { + color: #14adc4; +} +a.text-modern:hover, +a.text-modern:active, +a.text-modern:focus, +button.text-modern:hover, +button.text-modern:active, +button.text-modern:focus { + color: #14adc4; + opacity: .75; +} +.text-modern-dark { + color: #3e4d52; +} +a.text-modern-dark:hover, +a.text-modern-dark:active, +a.text-modern-dark:focus, +button.text-modern-dark:hover, +button.text-modern-dark:active, +button.text-modern-dark:focus { + color: #3e4d52; + opacity: .75; +} +.text-modern-darker { + color: #323e42; +} +a.text-modern-darker:hover, +a.text-modern-darker:active, +a.text-modern-darker:focus, +button.text-modern-darker:hover, +button.text-modern-darker:active, +button.text-modern-darker:focus { + color: #323e42; + opacity: .75; +} +.text-modern-light { + color: #7fe3f2; +} +a.text-modern-light:hover, +a.text-modern-light:active, +a.text-modern-light:focus, +button.text-modern-light:hover, +button.text-modern-light:active, +button.text-modern-light:focus { + color: #7fe3f2; + opacity: .75; +} +.text-modern-lighter { + color: #c4f2f9; +} +a.text-modern-lighter:hover, +a.text-modern-lighter:active, +a.text-modern-lighter:focus, +button.text-modern-lighter:hover, +button.text-modern-lighter:active, +button.text-modern-lighter:focus { + color: #c4f2f9; + opacity: .75; +} +.bg-modern { + background-color: #14adc4; +} +a.bg-modern:hover, +a.bg-modern:focus { + background-color: #0f8496; +} +.bg-modern-op { + background-color: rgba(20, 173, 196, 0.75); +} +a.bg-modern-op:hover, +a.bg-modern-op:focus { + background-color: rgba(15, 132, 150, 0.75); +} +.bg-modern-dark { + background-color: #3e4d52; +} +a.bg-modern-dark:hover, +a.bg-modern-dark:focus { + background-color: #283235; +} +.bg-modern-dark-op { + background-color: rgba(62, 77, 82, 0.83); +} +a.bg-modern-dark-op:hover, +a.bg-modern-dark-op:focus { + background-color: rgba(40, 50, 53, 0.83); +} +.bg-modern-darker { + background-color: #323e42; +} +a.bg-modern-darker:hover, +a.bg-modern-darker:focus { + background-color: #1c2325; +} +.bg-modern-light { + background-color: #7fe3f2; +} +a.bg-modern-light:hover, +a.bg-modern-light:focus { + background-color: #51d9ed; +} +.bg-modern-lighter { + background-color: #c4f2f9; +} +a.bg-modern-lighter:hover, +a.bg-modern-lighter:focus { + background-color: #96e8f4; +} +.text-smooth { + color: #ff6c9d; +} +a.text-smooth:hover, +a.text-smooth:active, +a.text-smooth:focus, +button.text-smooth:hover, +button.text-smooth:active, +button.text-smooth:focus { + color: #ff6c9d; + opacity: .75; +} +.text-smooth-dark { + color: #4a5568; +} +a.text-smooth-dark:hover, +a.text-smooth-dark:active, +a.text-smooth-dark:focus, +button.text-smooth-dark:hover, +button.text-smooth-dark:active, +button.text-smooth-dark:focus { + color: #4a5568; + opacity: .75; +} +.text-smooth-darker { + color: #333a47; +} +a.text-smooth-darker:hover, +a.text-smooth-darker:active, +a.text-smooth-darker:focus, +button.text-smooth-darker:hover, +button.text-smooth-darker:active, +button.text-smooth-darker:focus { + color: #333a47; + opacity: .75; +} +.text-smooth-light { + color: #ff90b5; +} +a.text-smooth-light:hover, +a.text-smooth-light:active, +a.text-smooth-light:focus, +button.text-smooth-light:hover, +button.text-smooth-light:active, +button.text-smooth-light:focus { + color: #ff90b5; + opacity: .75; +} +.text-smooth-lighter { + color: #ffb9d0; +} +a.text-smooth-lighter:hover, +a.text-smooth-lighter:active, +a.text-smooth-lighter:focus, +button.text-smooth-lighter:hover, +button.text-smooth-lighter:active, +button.text-smooth-lighter:focus { + color: #ffb9d0; + opacity: .75; +} +.bg-smooth { + background-color: #ff6c9d; +} +a.bg-smooth:hover, +a.bg-smooth:focus { + background-color: #ff397b; +} +.bg-smooth-op { + background-color: rgba(255, 108, 157, 0.75); +} +a.bg-smooth-op:hover, +a.bg-smooth-op:focus { + background-color: rgba(255, 57, 123, 0.75); +} +.bg-smooth-dark { + background-color: #4a5568; +} +a.bg-smooth-dark:hover, +a.bg-smooth-dark:focus { + background-color: #353d4a; +} +.bg-smooth-dark-op { + background-color: rgba(74, 85, 104, 0.83); +} +a.bg-smooth-dark-op:hover, +a.bg-smooth-dark-op:focus { + background-color: rgba(53, 61, 74, 0.83); +} +.bg-smooth-darker { + background-color: #333a47; +} +a.bg-smooth-darker:hover, +a.bg-smooth-darker:focus { + background-color: #1e2229; +} +.bg-smooth-light { + background-color: #ff90b5; +} +a.bg-smooth-light:hover, +a.bg-smooth-light:focus { + background-color: #ff5d93; +} +.bg-smooth-lighter { + background-color: #ffb9d0; +} +a.bg-smooth-lighter:hover, +a.bg-smooth-lighter:focus { + background-color: #ff86ae; +} +@media print { + #page-container, + #main-container { + padding: 0 !important; + } + #header-navbar, + #sidebar, + #side-overlay, + .block-options { + display: none !important; + } +} +.animated { + -webkit-animation-duration: 1s; + animation-duration: 1s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; +} +.animated.infinite { + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite; +} +.animated.hinge { + -webkit-animation-duration: 2s; + animation-duration: 2s; +} +.animated.bounceIn, +.animated.bounceOut { + -webkit-animation-duration: .75s; + animation-duration: .75s; +} +.animated.flipOutX, +.animated.flipOutY { + -webkit-animation-duration: .75s; + animation-duration: .75s; +} +@-webkit-keyframes bounce { + 0%, + 20%, + 53%, + 80%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + 40%, + 43% { + -webkit-transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -30px, 0); + transform: translate3d(0, -30px, 0); + } + 70% { + -webkit-transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -15px, 0); + transform: translate3d(0, -15px, 0); + } + 90% { + -webkit-transform: translate3d(0, -4px, 0); + transform: translate3d(0, -4px, 0); + } +} +@keyframes bounce { + 0%, + 20%, + 53%, + 80%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + 40%, + 43% { + -webkit-transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -30px, 0); + transform: translate3d(0, -30px, 0); + } + 70% { + -webkit-transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -15px, 0); + transform: translate3d(0, -15px, 0); + } + 90% { + -webkit-transform: translate3d(0, -4px, 0); + transform: translate3d(0, -4px, 0); + } +} +.bounce { + -webkit-animation-name: bounce; + animation-name: bounce; + -webkit-transform-origin: center bottom; + transform-origin: center bottom; +} +@-webkit-keyframes flash { + 0%, + 50%, + 100% { + opacity: 1; + } + 25%, + 75% { + opacity: 0; + } +} +@keyframes flash { + 0%, + 50%, + 100% { + opacity: 1; + } + 25%, + 75% { + opacity: 0; + } +} +.flash { + -webkit-animation-name: flash; + animation-name: flash; +} +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ +@-webkit-keyframes pulse { + 0% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + 100% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +@keyframes pulse { + 0% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + 100% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +.pulse { + -webkit-animation-name: pulse; + animation-name: pulse; +} +@-webkit-keyframes rubberBand { + 0% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + 30% { + -webkit-transform: scale3d(1.25, 0.75, 1); + transform: scale3d(1.25, 0.75, 1); + } + 40% { + -webkit-transform: scale3d(0.75, 1.25, 1); + transform: scale3d(0.75, 1.25, 1); + } + 50% { + -webkit-transform: scale3d(1.15, 0.85, 1); + transform: scale3d(1.15, 0.85, 1); + } + 65% { + -webkit-transform: scale3d(0.95, 1.05, 1); + transform: scale3d(0.95, 1.05, 1); + } + 75% { + -webkit-transform: scale3d(1.05, 0.95, 1); + transform: scale3d(1.05, 0.95, 1); + } + 100% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +@keyframes rubberBand { + 0% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + 30% { + -webkit-transform: scale3d(1.25, 0.75, 1); + transform: scale3d(1.25, 0.75, 1); + } + 40% { + -webkit-transform: scale3d(0.75, 1.25, 1); + transform: scale3d(0.75, 1.25, 1); + } + 50% { + -webkit-transform: scale3d(1.15, 0.85, 1); + transform: scale3d(1.15, 0.85, 1); + } + 65% { + -webkit-transform: scale3d(0.95, 1.05, 1); + transform: scale3d(0.95, 1.05, 1); + } + 75% { + -webkit-transform: scale3d(1.05, 0.95, 1); + transform: scale3d(1.05, 0.95, 1); + } + 100% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +.rubberBand { + -webkit-animation-name: rubberBand; + animation-name: rubberBand; +} +@-webkit-keyframes shake { + 0%, + 100% { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + 10%, + 30%, + 50%, + 70%, + 90% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + 20%, + 40%, + 60%, + 80% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } +} +@keyframes shake { + 0%, + 100% { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + 10%, + 30%, + 50%, + 70%, + 90% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + 20%, + 40%, + 60%, + 80% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } +} +.shake { + -webkit-animation-name: shake; + animation-name: shake; +} +@-webkit-keyframes swing { + 20% { + -webkit-transform: rotate3d(0, 0, 1, 15deg); + transform: rotate3d(0, 0, 1, 15deg); + } + 40% { + -webkit-transform: rotate3d(0, 0, 1, -10deg); + transform: rotate3d(0, 0, 1, -10deg); + } + 60% { + -webkit-transform: rotate3d(0, 0, 1, 5deg); + transform: rotate3d(0, 0, 1, 5deg); + } + 80% { + -webkit-transform: rotate3d(0, 0, 1, -5deg); + transform: rotate3d(0, 0, 1, -5deg); + } + 100% { + -webkit-transform: rotate3d(0, 0, 1, 0deg); + transform: rotate3d(0, 0, 1, 0deg); + } +} +@keyframes swing { + 20% { + -webkit-transform: rotate3d(0, 0, 1, 15deg); + transform: rotate3d(0, 0, 1, 15deg); + } + 40% { + -webkit-transform: rotate3d(0, 0, 1, -10deg); + transform: rotate3d(0, 0, 1, -10deg); + } + 60% { + -webkit-transform: rotate3d(0, 0, 1, 5deg); + transform: rotate3d(0, 0, 1, 5deg); + } + 80% { + -webkit-transform: rotate3d(0, 0, 1, -5deg); + transform: rotate3d(0, 0, 1, -5deg); + } + 100% { + -webkit-transform: rotate3d(0, 0, 1, 0deg); + transform: rotate3d(0, 0, 1, 0deg); + } +} +.swing { + -webkit-transform-origin: top center; + transform-origin: top center; + -webkit-animation-name: swing; + animation-name: swing; +} +@-webkit-keyframes tada { + 0% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + 10%, + 20% { + -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + } + 30%, + 50%, + 70%, + 90% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + } + 40%, + 60%, + 80% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + } + 100% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +@keyframes tada { + 0% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + 10%, + 20% { + -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + } + 30%, + 50%, + 70%, + 90% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + } + 40%, + 60%, + 80% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + } + 100% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +.tada { + -webkit-animation-name: tada; + animation-name: tada; +} +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ +@-webkit-keyframes wobble { + 0% { + -webkit-transform: none; + transform: none; + } + 15% { + -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + } + 30% { + -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + } + 45% { + -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + } + 60% { + -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + } + 75% { + -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + } + 100% { + -webkit-transform: none; + transform: none; + } +} +@keyframes wobble { + 0% { + -webkit-transform: none; + transform: none; + } + 15% { + -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + } + 30% { + -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + } + 45% { + -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + } + 60% { + -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + } + 75% { + -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + } + 100% { + -webkit-transform: none; + transform: none; + } +} +.wobble { + -webkit-animation-name: wobble; + animation-name: wobble; +} +@-webkit-keyframes bounceIn { + 0%, + 20%, + 40%, + 60%, + 80%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + 20% { + -webkit-transform: scale3d(1.1, 1.1, 1.1); + transform: scale3d(1.1, 1.1, 1.1); + } + 40% { + -webkit-transform: scale3d(0.9, 0.9, 0.9); + transform: scale3d(0.9, 0.9, 0.9); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(1.03, 1.03, 1.03); + transform: scale3d(1.03, 1.03, 1.03); + } + 80% { + -webkit-transform: scale3d(0.97, 0.97, 0.97); + transform: scale3d(0.97, 0.97, 0.97); + } + 100% { + opacity: 1; + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +@keyframes bounceIn { + 0%, + 20%, + 40%, + 60%, + 80%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + 20% { + -webkit-transform: scale3d(1.1, 1.1, 1.1); + transform: scale3d(1.1, 1.1, 1.1); + } + 40% { + -webkit-transform: scale3d(0.9, 0.9, 0.9); + transform: scale3d(0.9, 0.9, 0.9); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(1.03, 1.03, 1.03); + transform: scale3d(1.03, 1.03, 1.03); + } + 80% { + -webkit-transform: scale3d(0.97, 0.97, 0.97); + transform: scale3d(0.97, 0.97, 0.97); + } + 100% { + opacity: 1; + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +.bounceIn { + -webkit-animation-name: bounceIn; + animation-name: bounceIn; +} +@-webkit-keyframes bounceInDown { + 0%, + 60%, + 75%, + 90%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -3000px, 0); + transform: translate3d(0, -3000px, 0); + } + 60% { + opacity: 1; + -webkit-transform: translate3d(0, 25px, 0); + transform: translate3d(0, 25px, 0); + } + 75% { + -webkit-transform: translate3d(0, -10px, 0); + transform: translate3d(0, -10px, 0); + } + 90% { + -webkit-transform: translate3d(0, 5px, 0); + transform: translate3d(0, 5px, 0); + } + 100% { + -webkit-transform: none; + transform: none; + } +} +@keyframes bounceInDown { + 0%, + 60%, + 75%, + 90%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -3000px, 0); + transform: translate3d(0, -3000px, 0); + } + 60% { + opacity: 1; + -webkit-transform: translate3d(0, 25px, 0); + transform: translate3d(0, 25px, 0); + } + 75% { + -webkit-transform: translate3d(0, -10px, 0); + transform: translate3d(0, -10px, 0); + } + 90% { + -webkit-transform: translate3d(0, 5px, 0); + transform: translate3d(0, 5px, 0); + } + 100% { + -webkit-transform: none; + transform: none; + } +} +.bounceInDown { + -webkit-animation-name: bounceInDown; + animation-name: bounceInDown; +} +@-webkit-keyframes bounceInLeft { + 0%, + 60%, + 75%, + 90%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: translate3d(-3000px, 0, 0); + transform: translate3d(-3000px, 0, 0); + } + 60% { + opacity: 1; + -webkit-transform: translate3d(25px, 0, 0); + transform: translate3d(25px, 0, 0); + } + 75% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + 90% { + -webkit-transform: translate3d(5px, 0, 0); + transform: translate3d(5px, 0, 0); + } + 100% { + -webkit-transform: none; + transform: none; + } +} +@keyframes bounceInLeft { + 0%, + 60%, + 75%, + 90%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: translate3d(-3000px, 0, 0); + transform: translate3d(-3000px, 0, 0); + } + 60% { + opacity: 1; + -webkit-transform: translate3d(25px, 0, 0); + transform: translate3d(25px, 0, 0); + } + 75% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + 90% { + -webkit-transform: translate3d(5px, 0, 0); + transform: translate3d(5px, 0, 0); + } + 100% { + -webkit-transform: none; + transform: none; + } +} +.bounceInLeft { + -webkit-animation-name: bounceInLeft; + animation-name: bounceInLeft; +} +@-webkit-keyframes bounceInRight { + 0%, + 60%, + 75%, + 90%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: translate3d(3000px, 0, 0); + transform: translate3d(3000px, 0, 0); + } + 60% { + opacity: 1; + -webkit-transform: translate3d(-25px, 0, 0); + transform: translate3d(-25px, 0, 0); + } + 75% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } + 90% { + -webkit-transform: translate3d(-5px, 0, 0); + transform: translate3d(-5px, 0, 0); + } + 100% { + -webkit-transform: none; + transform: none; + } +} +@keyframes bounceInRight { + 0%, + 60%, + 75%, + 90%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: translate3d(3000px, 0, 0); + transform: translate3d(3000px, 0, 0); + } + 60% { + opacity: 1; + -webkit-transform: translate3d(-25px, 0, 0); + transform: translate3d(-25px, 0, 0); + } + 75% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } + 90% { + -webkit-transform: translate3d(-5px, 0, 0); + transform: translate3d(-5px, 0, 0); + } + 100% { + -webkit-transform: none; + transform: none; + } +} +.bounceInRight { + -webkit-animation-name: bounceInRight; + animation-name: bounceInRight; +} +@-webkit-keyframes bounceInUp { + 0%, + 60%, + 75%, + 90%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: translate3d(0, 3000px, 0); + transform: translate3d(0, 3000px, 0); + } + 60% { + opacity: 1; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + 75% { + -webkit-transform: translate3d(0, 10px, 0); + transform: translate3d(0, 10px, 0); + } + 90% { + -webkit-transform: translate3d(0, -5px, 0); + transform: translate3d(0, -5px, 0); + } + 100% { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +@keyframes bounceInUp { + 0%, + 60%, + 75%, + 90%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: translate3d(0, 3000px, 0); + transform: translate3d(0, 3000px, 0); + } + 60% { + opacity: 1; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + 75% { + -webkit-transform: translate3d(0, 10px, 0); + transform: translate3d(0, 10px, 0); + } + 90% { + -webkit-transform: translate3d(0, -5px, 0); + transform: translate3d(0, -5px, 0); + } + 100% { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +.bounceInUp { + -webkit-animation-name: bounceInUp; + animation-name: bounceInUp; +} +@-webkit-keyframes bounceOut { + 20% { + -webkit-transform: scale3d(0.9, 0.9, 0.9); + transform: scale3d(0.9, 0.9, 0.9); + } + 50%, + 55% { + opacity: 1; + -webkit-transform: scale3d(1.1, 1.1, 1.1); + transform: scale3d(1.1, 1.1, 1.1); + } + 100% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } +} +@keyframes bounceOut { + 20% { + -webkit-transform: scale3d(0.9, 0.9, 0.9); + transform: scale3d(0.9, 0.9, 0.9); + } + 50%, + 55% { + opacity: 1; + -webkit-transform: scale3d(1.1, 1.1, 1.1); + transform: scale3d(1.1, 1.1, 1.1); + } + 100% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } +} +.bounceOut { + -webkit-animation-name: bounceOut; + animation-name: bounceOut; +} +@-webkit-keyframes bounceOutDown { + 20% { + -webkit-transform: translate3d(0, 10px, 0); + transform: translate3d(0, 10px, 0); + } + 40%, + 45% { + opacity: 1; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } +} +@keyframes bounceOutDown { + 20% { + -webkit-transform: translate3d(0, 10px, 0); + transform: translate3d(0, 10px, 0); + } + 40%, + 45% { + opacity: 1; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } +} +.bounceOutDown { + -webkit-animation-name: bounceOutDown; + animation-name: bounceOutDown; +} +@-webkit-keyframes bounceOutLeft { + 20% { + opacity: 1; + -webkit-transform: translate3d(20px, 0, 0); + transform: translate3d(20px, 0, 0); + } + 100% { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } +} +@keyframes bounceOutLeft { + 20% { + opacity: 1; + -webkit-transform: translate3d(20px, 0, 0); + transform: translate3d(20px, 0, 0); + } + 100% { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } +} +.bounceOutLeft { + -webkit-animation-name: bounceOutLeft; + animation-name: bounceOutLeft; +} +@-webkit-keyframes bounceOutRight { + 20% { + opacity: 1; + -webkit-transform: translate3d(-20px, 0, 0); + transform: translate3d(-20px, 0, 0); + } + 100% { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } +} +@keyframes bounceOutRight { + 20% { + opacity: 1; + -webkit-transform: translate3d(-20px, 0, 0); + transform: translate3d(-20px, 0, 0); + } + 100% { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } +} +.bounceOutRight { + -webkit-animation-name: bounceOutRight; + animation-name: bounceOutRight; +} +@-webkit-keyframes bounceOutUp { + 20% { + -webkit-transform: translate3d(0, -10px, 0); + transform: translate3d(0, -10px, 0); + } + 40%, + 45% { + opacity: 1; + -webkit-transform: translate3d(0, 20px, 0); + transform: translate3d(0, 20px, 0); + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } +} +@keyframes bounceOutUp { + 20% { + -webkit-transform: translate3d(0, -10px, 0); + transform: translate3d(0, -10px, 0); + } + 40%, + 45% { + opacity: 1; + -webkit-transform: translate3d(0, 20px, 0); + transform: translate3d(0, 20px, 0); + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } +} +.bounceOutUp { + -webkit-animation-name: bounceOutUp; + animation-name: bounceOutUp; +} +@-webkit-keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +@keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +.fadeIn { + -webkit-animation-name: fadeIn; + animation-name: fadeIn; +} +@-webkit-keyframes fadeInDown { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes fadeInDown { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.fadeInDown { + -webkit-animation-name: fadeInDown; + animation-name: fadeInDown; +} +@-webkit-keyframes fadeInDownBig { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes fadeInDownBig { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.fadeInDownBig { + -webkit-animation-name: fadeInDownBig; + animation-name: fadeInDownBig; +} +@-webkit-keyframes fadeInLeft { + 0% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes fadeInLeft { + 0% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.fadeInLeft { + -webkit-animation-name: fadeInLeft; + animation-name: fadeInLeft; +} +@-webkit-keyframes fadeInLeftBig { + 0% { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes fadeInLeftBig { + 0% { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.fadeInLeftBig { + -webkit-animation-name: fadeInLeftBig; + animation-name: fadeInLeftBig; +} +@-webkit-keyframes fadeInRight { + 0% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes fadeInRight { + 0% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.fadeInRight { + -webkit-animation-name: fadeInRight; + animation-name: fadeInRight; +} +@-webkit-keyframes fadeInRightBig { + 0% { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes fadeInRightBig { + 0% { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.fadeInRightBig { + -webkit-animation-name: fadeInRightBig; + animation-name: fadeInRightBig; +} +@-webkit-keyframes fadeInUp { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes fadeInUp { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.fadeInUp { + -webkit-animation-name: fadeInUp; + animation-name: fadeInUp; +} +@-webkit-keyframes fadeInUpBig { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes fadeInUpBig { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.fadeInUpBig { + -webkit-animation-name: fadeInUpBig; + animation-name: fadeInUpBig; +} +@-webkit-keyframes fadeOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} +@keyframes fadeOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} +.fadeOut { + -webkit-animation-name: fadeOut; + animation-name: fadeOut; +} +@-webkit-keyframes fadeOutDown { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } +} +@keyframes fadeOutDown { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } +} +.fadeOutDown { + -webkit-animation-name: fadeOutDown; + animation-name: fadeOutDown; +} +@-webkit-keyframes fadeOutDownBig { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } +} +@keyframes fadeOutDownBig { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } +} +.fadeOutDownBig { + -webkit-animation-name: fadeOutDownBig; + animation-name: fadeOutDownBig; +} +@-webkit-keyframes fadeOutLeft { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } +} +@keyframes fadeOutLeft { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } +} +.fadeOutLeft { + -webkit-animation-name: fadeOutLeft; + animation-name: fadeOutLeft; +} +@-webkit-keyframes fadeOutLeftBig { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } +} +@keyframes fadeOutLeftBig { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } +} +.fadeOutLeftBig { + -webkit-animation-name: fadeOutLeftBig; + animation-name: fadeOutLeftBig; +} +@-webkit-keyframes fadeOutRight { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } +} +@keyframes fadeOutRight { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } +} +.fadeOutRight { + -webkit-animation-name: fadeOutRight; + animation-name: fadeOutRight; +} +@-webkit-keyframes fadeOutRightBig { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } +} +@keyframes fadeOutRightBig { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } +} +.fadeOutRightBig { + -webkit-animation-name: fadeOutRightBig; + animation-name: fadeOutRightBig; +} +@-webkit-keyframes fadeOutUp { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } +} +@keyframes fadeOutUp { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } +} +.fadeOutUp { + -webkit-animation-name: fadeOutUp; + animation-name: fadeOutUp; +} +@-webkit-keyframes fadeOutUpBig { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } +} +@keyframes fadeOutUpBig { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } +} +.fadeOutUpBig { + -webkit-animation-name: fadeOutUpBig; + animation-name: fadeOutUpBig; +} +@-webkit-keyframes flip { + 0% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -360deg); + transform: perspective(400px) rotate3d(0, 1, 0, -360deg); + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; + } + 40% { + -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); + transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; + } + 50% { + -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); + transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + 80% { + -webkit-transform: perspective(400px) scale3d(0.95, 0.95, 0.95); + transform: perspective(400px) scale3d(0.95, 0.95, 0.95); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + 100% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } +} +@keyframes flip { + 0% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -360deg); + transform: perspective(400px) rotate3d(0, 1, 0, -360deg); + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; + } + 40% { + -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); + transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; + } + 50% { + -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); + transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + 80% { + -webkit-transform: perspective(400px) scale3d(0.95, 0.95, 0.95); + transform: perspective(400px) scale3d(0.95, 0.95, 0.95); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + 100% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } +} +.animated.flip { + -webkit-backface-visibility: visible; + backface-visibility: visible; + -webkit-animation-name: flip; + animation-name: flip; +} +@-webkit-keyframes flipInX { + 0% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + opacity: 0; + } + 40% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + } + 60% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + opacity: 1; + } + 80% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + } + 100% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } +} +@keyframes flipInX { + 0% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + opacity: 0; + } + 40% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + } + 60% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + opacity: 1; + } + 80% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + } + 100% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } +} +.flipInX { + -webkit-backface-visibility: visible !important; + backface-visibility: visible !important; + -webkit-animation-name: flipInX; + animation-name: flipInX; +} +@-webkit-keyframes flipInY { + 0% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + opacity: 0; + } + 40% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + } + 60% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + opacity: 1; + } + 80% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + } + 100% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } +} +@keyframes flipInY { + 0% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + opacity: 0; + } + 40% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + } + 60% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + opacity: 1; + } + 80% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + } + 100% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } +} +.flipInY { + -webkit-backface-visibility: visible !important; + backface-visibility: visible !important; + -webkit-animation-name: flipInY; + animation-name: flipInY; +} +@-webkit-keyframes flipOutX { + 0% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } + 30% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + opacity: 1; + } + 100% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + opacity: 0; + } +} +@keyframes flipOutX { + 0% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } + 30% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + opacity: 1; + } + 100% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + opacity: 0; + } +} +.flipOutX { + -webkit-animation-name: flipOutX; + animation-name: flipOutX; + -webkit-backface-visibility: visible !important; + backface-visibility: visible !important; +} +@-webkit-keyframes flipOutY { + 0% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } + 30% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + opacity: 1; + } + 100% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + opacity: 0; + } +} +@keyframes flipOutY { + 0% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } + 30% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + opacity: 1; + } + 100% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + opacity: 0; + } +} +.flipOutY { + -webkit-backface-visibility: visible !important; + backface-visibility: visible !important; + -webkit-animation-name: flipOutY; + animation-name: flipOutY; +} +@-webkit-keyframes lightSpeedIn { + 0% { + -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg); + transform: translate3d(100%, 0, 0) skewX(-30deg); + opacity: 0; + } + 60% { + -webkit-transform: skewX(20deg); + transform: skewX(20deg); + opacity: 1; + } + 80% { + -webkit-transform: skewX(-5deg); + transform: skewX(-5deg); + opacity: 1; + } + 100% { + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +@keyframes lightSpeedIn { + 0% { + -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg); + transform: translate3d(100%, 0, 0) skewX(-30deg); + opacity: 0; + } + 60% { + -webkit-transform: skewX(20deg); + transform: skewX(20deg); + opacity: 1; + } + 80% { + -webkit-transform: skewX(-5deg); + transform: skewX(-5deg); + opacity: 1; + } + 100% { + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +.lightSpeedIn { + -webkit-animation-name: lightSpeedIn; + animation-name: lightSpeedIn; + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; +} +@-webkit-keyframes lightSpeedOut { + 0% { + opacity: 1; + } + 100% { + -webkit-transform: translate3d(100%, 0, 0) skewX(30deg); + transform: translate3d(100%, 0, 0) skewX(30deg); + opacity: 0; + } +} +@keyframes lightSpeedOut { + 0% { + opacity: 1; + } + 100% { + -webkit-transform: translate3d(100%, 0, 0) skewX(30deg); + transform: translate3d(100%, 0, 0) skewX(30deg); + opacity: 0; + } +} +.lightSpeedOut { + -webkit-animation-name: lightSpeedOut; + animation-name: lightSpeedOut; + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; +} +@-webkit-keyframes rotateIn { + 0% { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: rotate3d(0, 0, 1, -200deg); + transform: rotate3d(0, 0, 1, -200deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +@keyframes rotateIn { + 0% { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: rotate3d(0, 0, 1, -200deg); + transform: rotate3d(0, 0, 1, -200deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +.rotateIn { + -webkit-animation-name: rotateIn; + animation-name: rotateIn; +} +@-webkit-keyframes rotateInDownLeft { + 0% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +@keyframes rotateInDownLeft { + 0% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +.rotateInDownLeft { + -webkit-animation-name: rotateInDownLeft; + animation-name: rotateInDownLeft; +} +@-webkit-keyframes rotateInDownRight { + 0% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +@keyframes rotateInDownRight { + 0% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +.rotateInDownRight { + -webkit-animation-name: rotateInDownRight; + animation-name: rotateInDownRight; +} +@-webkit-keyframes rotateInUpLeft { + 0% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +@keyframes rotateInUpLeft { + 0% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +.rotateInUpLeft { + -webkit-animation-name: rotateInUpLeft; + animation-name: rotateInUpLeft; +} +@-webkit-keyframes rotateInUpRight { + 0% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, -90deg); + transform: rotate3d(0, 0, 1, -90deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +@keyframes rotateInUpRight { + 0% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, -90deg); + transform: rotate3d(0, 0, 1, -90deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +.rotateInUpRight { + -webkit-animation-name: rotateInUpRight; + animation-name: rotateInUpRight; +} +@-webkit-keyframes rotateOut { + 0% { + -webkit-transform-origin: center; + transform-origin: center; + opacity: 1; + } + 100% { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: rotate3d(0, 0, 1, 200deg); + transform: rotate3d(0, 0, 1, 200deg); + opacity: 0; + } +} +@keyframes rotateOut { + 0% { + -webkit-transform-origin: center; + transform-origin: center; + opacity: 1; + } + 100% { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: rotate3d(0, 0, 1, 200deg); + transform: rotate3d(0, 0, 1, 200deg); + opacity: 0; + } +} +.rotateOut { + -webkit-animation-name: rotateOut; + animation-name: rotateOut; +} +@-webkit-keyframes rotateOutDownLeft { + 0% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + opacity: 1; + } + 100% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } +} +@keyframes rotateOutDownLeft { + 0% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + opacity: 1; + } + 100% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } +} +.rotateOutDownLeft { + -webkit-animation-name: rotateOutDownLeft; + animation-name: rotateOutDownLeft; +} +@-webkit-keyframes rotateOutDownRight { + 0% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + opacity: 1; + } + 100% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} +@keyframes rotateOutDownRight { + 0% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + opacity: 1; + } + 100% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} +.rotateOutDownRight { + -webkit-animation-name: rotateOutDownRight; + animation-name: rotateOutDownRight; +} +@-webkit-keyframes rotateOutUpLeft { + 0% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + opacity: 1; + } + 100% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} +@keyframes rotateOutUpLeft { + 0% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + opacity: 1; + } + 100% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} +.rotateOutUpLeft { + -webkit-animation-name: rotateOutUpLeft; + animation-name: rotateOutUpLeft; +} +@-webkit-keyframes rotateOutUpRight { + 0% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + opacity: 1; + } + 100% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, 90deg); + transform: rotate3d(0, 0, 1, 90deg); + opacity: 0; + } +} +@keyframes rotateOutUpRight { + 0% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + opacity: 1; + } + 100% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, 90deg); + transform: rotate3d(0, 0, 1, 90deg); + opacity: 0; + } +} +.rotateOutUpRight { + -webkit-animation-name: rotateOutUpRight; + animation-name: rotateOutUpRight; +} +@-webkit-keyframes hinge { + 0% { + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + 20%, + 60% { + -webkit-transform: rotate3d(0, 0, 1, 80deg); + transform: rotate3d(0, 0, 1, 80deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + 40%, + 80% { + -webkit-transform: rotate3d(0, 0, 1, 60deg); + transform: rotate3d(0, 0, 1, 60deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + opacity: 1; + } + 100% { + -webkit-transform: translate3d(0, 700px, 0); + transform: translate3d(0, 700px, 0); + opacity: 0; + } +} +@keyframes hinge { + 0% { + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + 20%, + 60% { + -webkit-transform: rotate3d(0, 0, 1, 80deg); + transform: rotate3d(0, 0, 1, 80deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + 40%, + 80% { + -webkit-transform: rotate3d(0, 0, 1, 60deg); + transform: rotate3d(0, 0, 1, 60deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + opacity: 1; + } + 100% { + -webkit-transform: translate3d(0, 700px, 0); + transform: translate3d(0, 700px, 0); + opacity: 0; + } +} +.hinge { + -webkit-animation-name: hinge; + animation-name: hinge; +} +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ +@-webkit-keyframes rollIn { + 0% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes rollIn { + 0% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.rollIn { + -webkit-animation-name: rollIn; + animation-name: rollIn; +} +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ +@-webkit-keyframes rollOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + } +} +@keyframes rollOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + } +} +.rollOut { + -webkit-animation-name: rollOut; + animation-name: rollOut; +} +@-webkit-keyframes zoomIn { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + 50% { + opacity: 1; + } +} +@keyframes zoomIn { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + 50% { + opacity: 1; + } +} +.zoomIn { + -webkit-animation-name: zoomIn; + animation-name: zoomIn; +} +@-webkit-keyframes zoomInDown { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomInDown { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +.zoomInDown { + -webkit-animation-name: zoomInDown; + animation-name: zoomInDown; +} +@-webkit-keyframes zoomInLeft { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomInLeft { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +.zoomInLeft { + -webkit-animation-name: zoomInLeft; + animation-name: zoomInLeft; +} +@-webkit-keyframes zoomInRight { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomInRight { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +.zoomInRight { + -webkit-animation-name: zoomInRight; + animation-name: zoomInRight; +} +@-webkit-keyframes zoomInUp { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomInUp { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +.zoomInUp { + -webkit-animation-name: zoomInUp; + animation-name: zoomInUp; +} +@-webkit-keyframes zoomOut { + 0% { + opacity: 1; + } + 50% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + 100% { + opacity: 0; + } +} +@keyframes zoomOut { + 0% { + opacity: 1; + } + 50% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + 100% { + opacity: 0; + } +} +.zoomOut { + -webkit-animation-name: zoomOut; + animation-name: zoomOut; +} +@-webkit-keyframes zoomOutDown { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 100% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomOutDown { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 100% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +.zoomOutDown { + -webkit-animation-name: zoomOutDown; + animation-name: zoomOutDown; +} +@-webkit-keyframes zoomOutLeft { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); + } + 100% { + opacity: 0; + -webkit-transform: scale(0.1) translate3d(-2000px, 0, 0); + transform: scale(0.1) translate3d(-2000px, 0, 0); + -webkit-transform-origin: left center; + transform-origin: left center; + } +} +@keyframes zoomOutLeft { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); + } + 100% { + opacity: 0; + -webkit-transform: scale(0.1) translate3d(-2000px, 0, 0); + transform: scale(0.1) translate3d(-2000px, 0, 0); + -webkit-transform-origin: left center; + transform-origin: left center; + } +} +.zoomOutLeft { + -webkit-animation-name: zoomOutLeft; + animation-name: zoomOutLeft; +} +@-webkit-keyframes zoomOutRight { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); + } + 100% { + opacity: 0; + -webkit-transform: scale(0.1) translate3d(2000px, 0, 0); + transform: scale(0.1) translate3d(2000px, 0, 0); + -webkit-transform-origin: right center; + transform-origin: right center; + } +} +@keyframes zoomOutRight { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); + } + 100% { + opacity: 0; + -webkit-transform: scale(0.1) translate3d(2000px, 0, 0); + transform: scale(0.1) translate3d(2000px, 0, 0); + -webkit-transform-origin: right center; + transform-origin: right center; + } +} +.zoomOutRight { + -webkit-animation-name: zoomOutRight; + animation-name: zoomOutRight; +} +@-webkit-keyframes zoomOutUp { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 100% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomOutUp { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 100% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +.zoomOutUp { + -webkit-animation-name: zoomOutUp; + animation-name: zoomOutUp; +} +@-webkit-keyframes slideInDown { + 0% { + -webkit-transform: translateY(-100%); + transform: translateY(-100%); + visibility: visible; + } + 100% { + -webkit-transform: translateY(0); + transform: translateY(0); + } +} +@keyframes slideInDown { + 0% { + -webkit-transform: translateY(-100%); + transform: translateY(-100%); + visibility: visible; + } + 100% { + -webkit-transform: translateY(0); + transform: translateY(0); + } +} +.slideInDown { + -webkit-animation-name: slideInDown; + animation-name: slideInDown; +} +@-webkit-keyframes slideInLeft { + 0% { + -webkit-transform: translateX(-100%); + transform: translateX(-100%); + visibility: visible; + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +@keyframes slideInLeft { + 0% { + -webkit-transform: translateX(-100%); + transform: translateX(-100%); + visibility: visible; + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +.slideInLeft { + -webkit-animation-name: slideInLeft; + animation-name: slideInLeft; +} +@-webkit-keyframes slideInRight { + 0% { + -webkit-transform: translateX(100%); + transform: translateX(100%); + visibility: visible; + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +@keyframes slideInRight { + 0% { + -webkit-transform: translateX(100%); + transform: translateX(100%); + visibility: visible; + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +.slideInRight { + -webkit-animation-name: slideInRight; + animation-name: slideInRight; +} +@-webkit-keyframes slideInUp { + 0% { + -webkit-transform: translateY(100%); + transform: translateY(100%); + visibility: visible; + } + 100% { + -webkit-transform: translateY(0); + transform: translateY(0); + } +} +@keyframes slideInUp { + 0% { + -webkit-transform: translateY(100%); + transform: translateY(100%); + visibility: visible; + } + 100% { + -webkit-transform: translateY(0); + transform: translateY(0); + } +} +.slideInUp { + -webkit-animation-name: slideInUp; + animation-name: slideInUp; +} +@-webkit-keyframes slideOutDown { + 0% { + -webkit-transform: translateY(0); + transform: translateY(0); + } + 100% { + visibility: hidden; + -webkit-transform: translateY(100%); + transform: translateY(100%); + } +} +@keyframes slideOutDown { + 0% { + -webkit-transform: translateY(0); + transform: translateY(0); + } + 100% { + visibility: hidden; + -webkit-transform: translateY(100%); + transform: translateY(100%); + } +} +.slideOutDown { + -webkit-animation-name: slideOutDown; + animation-name: slideOutDown; +} +@-webkit-keyframes slideOutLeft { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + 100% { + visibility: hidden; + -webkit-transform: translateX(-100%); + transform: translateX(-100%); + } +} +@keyframes slideOutLeft { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + 100% { + visibility: hidden; + -webkit-transform: translateX(-100%); + transform: translateX(-100%); + } +} +.slideOutLeft { + -webkit-animation-name: slideOutLeft; + animation-name: slideOutLeft; +} +@-webkit-keyframes slideOutRight { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + 100% { + visibility: hidden; + -webkit-transform: translateX(100%); + transform: translateX(100%); + } +} +@keyframes slideOutRight { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + 100% { + visibility: hidden; + -webkit-transform: translateX(100%); + transform: translateX(100%); + } +} +.slideOutRight { + -webkit-animation-name: slideOutRight; + animation-name: slideOutRight; +} +@-webkit-keyframes slideOutUp { + 0% { + -webkit-transform: translateY(0); + transform: translateY(0); + } + 100% { + visibility: hidden; + -webkit-transform: translateY(-100%); + transform: translateY(-100%); + } +} +@keyframes slideOutUp { + 0% { + -webkit-transform: translateY(0); + transform: translateY(0); + } + 100% { + visibility: hidden; + -webkit-transform: translateY(-100%); + transform: translateY(-100%); + } +} +.slideOutUp { + -webkit-animation-name: slideOutUp; + animation-name: slideOutUp; +} +.autocomplete-suggestion b { + color: #5c90d2; +} +.irs-line { + height: 5px; + background: #eee; + border: none; + border-radius: 3px; +} +.irs-bar { + height: 5px; + border: none; + background: #5c90d2; +} +.irs-bar-edge { + height: 5px; + border: none; + background: #5c90d2; + border-radius: 3px 0 0 3px; +} +.irs-slider { + top: 25px; + width: 20px; + height: 20px; + border: none; + background: #fff; + border-radius: 50%; + -webkit-box-shadow: 0 1px 5px rgba(0, 0, 0, 0.35); + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.35); +} +.irs-slider:hover { + background: #fff; + -webkit-box-shadow: 0 1px 5px rgba(0, 0, 0, 0.75); + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.75); +} +.irs-from, +.irs-to, +.irs-single { + color: #fff; + font-size: 13px; + background: #5c90d2; +} +.irs-grid-pol { + background: #5c90d2; +} +.jvectormap-tip { + padding: 6px 8px; + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + background: #2c343f; + border: none; + border-radius: 0; +} +.jvectormap-zoomin, +.jvectormap-zoomout, +.jvectormap-goback { + left: 15px; + padding: 4px; + line-height: 15px; + background: #555; +} +.jvectormap-zoomin, +.jvectormap-zoomout { + width: 15px; + height: 15px; +} +.jvectormap-zoomin:hover, +.jvectormap-zoomout:hover { + opacity: .75; +} +.jvectormap-zoomout { + top: 40px; +} +.draggable-column { + min-height: 100px; +} +.draggable-handler { + cursor: move; +} +.draggable-placeholder { + background-color: #f1f1f1; + border: 1px dashed #ccc; +} +pre.pre-sh { + padding: 0; + margin: 0; + border: none; + background-color: transparent; + border-radius: 0; +} +.dropzone { + min-height: 200px; + background-color: #f9f9f9; + border: 2px dashed #bbb; + border-radius: 3px; +} +.dropzone .dz-message { + margin: 65px 0; + font-size: 16px; + font-style: italic; + color: #888; +} +.dropzone:hover { + background-color: #fcfcfc; + border-color: #5c90d2; +} +.dropzone:hover .dz-message { + color: #5c90d2; +} +.datepicker { + z-index: 1051 !important; +} +.input-daterange .input-group-addon { + min-width: 30px; + color: #646464; + background-color: #f9f9f9; + border-color: #e6e6e6; +} +.datepicker table tr td.today, +.datepicker table tr td.today:hover, +.datepicker table tr td.today.disabled, +.datepicker table tr td.today.disabled:hover { + background-color: #faeab9; + border-color: #faeab9; +} +.datepicker table tr td.active:hover, +.datepicker table tr td.active:hover:hover, +.datepicker table tr td.active.disabled:hover, +.datepicker table tr td.active.disabled:hover:hover, +.datepicker table tr td.active:focus, +.datepicker table tr td.active:hover:focus, +.datepicker table tr td.active.disabled:focus, +.datepicker table tr td.active.disabled:hover:focus, +.datepicker table tr td.active:active, +.datepicker table tr td.active:hover:active, +.datepicker table tr td.active.disabled:active, +.datepicker table tr td.active.disabled:hover:active, +.datepicker table tr td.active.active, +.datepicker table tr td.active:hover.active, +.datepicker table tr td.active.disabled.active, +.datepicker table tr td.active.disabled:hover.active, +.open .dropdown-toggle.datepicker table tr td.active, +.open .dropdown-toggle.datepicker table tr td.active:hover, +.open .dropdown-toggle.datepicker table tr td.active.disabled, +.open .dropdown-toggle.datepicker table tr td.active.disabled:hover, +.datepicker table tr td span.active:hover, +.datepicker table tr td span.active:hover:hover, +.datepicker table tr td span.active.disabled:hover, +.datepicker table tr td span.active.disabled:hover:hover, +.datepicker table tr td span.active:focus, +.datepicker table tr td span.active:hover:focus, +.datepicker table tr td span.active.disabled:focus, +.datepicker table tr td span.active.disabled:hover:focus, +.datepicker table tr td span.active:active, +.datepicker table tr td span.active:hover:active, +.datepicker table tr td span.active.disabled:active, +.datepicker table tr td span.active.disabled:hover:active, +.datepicker table tr td span.active.active, +.datepicker table tr td span.active:hover.active, +.datepicker table tr td span.active.disabled.active, +.datepicker table tr td span.active.disabled:hover.active, +.open .dropdown-toggle.datepicker table tr td span.active, +.open .dropdown-toggle.datepicker table tr td span.active:hover, +.open .dropdown-toggle.datepicker table tr td span.active.disabled, +.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover { + background-color: #5c90d2; + border-color: #5c90d2; +} +.colorpicker.dropdown-menu { + min-width: 130px; +} +div.tagsinput { + padding: 6px 12px 1px; + border-color: #e6e6e6; + border-radius: 3px; +} +div.tagsinput input { + padding-top: 0; + padding-bottom: 0; + height: 22px; +} +.form-material div.tagsinput { + padding-right: 0; + padding-left: 0; + border: none; + border-bottom: 1px solid #e6e6e6; +} +div.tagsinput span.tag { + padding: 2px 5px; + height: 22px; + line-height: 18px; + color: #fff; + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 600; + background-color: #5c90d2; + border: none; +} +div.tagsinput span.tag a { + font-size: 13px; + color: rgba(255, 255, 255, 0.5); +} +div.tagsinput span.tag a:hover { + color: rgba(255, 255, 255, 0.75); +} +.select2-container .select2-selection--single { + height: 34px; +} +.select2-container .select2-dropdown { + border-color: #e6e6e6; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.select2-container .select2-search--inline .select2-search__field { + margin-top: 6px; +} +.select2-container--default .select2-selection--single { + border-color: #e6e6e6; + border-radius: 3px; +} +.select2-container--default .select2-selection--single .select2-selection__rendered { + padding-left: 12px; + line-height: 34px; +} +.form-material .select2-container--default .select2-selection--single .select2-selection__rendered { + padding-left: 0; +} +.select2-container--default .select2-selection--single .select2-selection__arrow { + height: 34px; +} +.form-material .select2-container--default .select2-selection--single { + border: none; + border-bottom: 1px solid #e6e6e6; + border-radius: 0; +} +.select2-container--default .select2-selection--single .select2-selection__placeholder { + color: #aaa; +} +.select2-container--default .select2-selection--multiple, +.select2-container--default.select2-container--focus .select2-selection--multiple { + border-color: #e6e6e6; + border-radius: 3px; + min-height: 34px; +} +.form-material .select2-container--default .select2-selection--multiple, +.form-material .select2-container--default.select2-container--focus .select2-selection--multiple { + border: none; + border-bottom: 1px solid #e6e6e6; + border-radius: 0; +} +.select2-container--default .select2-selection--multiple .select2-selection__rendered, +.select2-container--default.select2-container--focus .select2-selection--multiple .select2-selection__rendered { + padding-right: 12px; + padding-left: 12px; +} +.form-material .select2-container--default .select2-selection--multiple .select2-selection__rendered, +.form-material .select2-container--default.select2-container--focus .select2-selection--multiple .select2-selection__rendered { + padding-left: 0; +} +.has-error .select2-container--default .select2-selection--single, +.has-error .select2-container--default .select2-selection--multiple { + border-color: #d26a5c; +} +.has-error .select2-container--default.select2-container--focus .select2-selection--single, +.has-error .select2-container--default.select2-container--focus .select2-selection--multiple { + border-color: #c54736; +} +.select2-container--default .select2-selection--multiple .select2-selection__choice { + height: 22px; + line-height: 22px; + color: #fff; + font-size: 13px; + font-weight: 600; + background-color: #5c90d2; + border: none; + border-radius: 3px; +} +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove { + margin-right: 5px; + color: rgba(255, 255, 255, 0.5); +} +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { + color: rgba(255, 255, 255, 0.75); +} +.select2-container--default .select2-search--dropdown .select2-search__field { + border-color: #e6e6e6; +} +.select2-container--default .select2-results__option--highlighted[aria-selected] { + background-color: #5c90d2; +} +.select2-container--default .select2-search--inline .select2-search__field { + padding-right: 0; + padding-left: 0; + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + -webkit-box-shadow: none; + box-shadow: none; +} +.form-material .select2-container--default .select2-search--inline .select2-search__field { + padding-left: 0; +} +.select2-search--dropdown .select2-search__field { + padding: 6px 12px; + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + border-radius: 3px; + -webkit-box-shadow: none; + box-shadow: none; +} +.slick-slider.slick-dotted { + margin-bottom: 45px; +} +.slick-slider.slick-dotted .slick-dots { + bottom: -35px; +} +.slick-slider.slick-dotted.slick-padding-dots { + padding-bottom: 45px; + margin-bottom: 0; +} +.slick-slider.slick-dotted.slick-padding-dots .slick-dots { + bottom: 10px; +} +.slick-slider .slick-prev, +.slick-slider .slick-next { + margin-top: -5px; + width: 40px; + height: 40px; + text-align: center; + background-color: #000; + opacity: .25; + z-index: 10; +} +.slick-slider .slick-prev:hover, +.slick-slider .slick-next:hover { + background-color: #000; + opacity: .6; +} +.slick-slider .slick-prev:before, +.slick-slider .slick-next:before { + font-family: 'FontAwesome'; + font-size: 14px; + line-height: 28px; +} +.slick-slider .slick-prev { + left: 10px; +} +.slick-slider .slick-prev:before { + content: "\f060"; +} +.slick-slider .slick-next { + right: 10px; +} +.slick-slider .slick-next:before { + content: "\f061"; +} +.slick-slider.slick-nav-white .slick-prev, +.slick-slider.slick-nav-white .slick-next { + background-color: #fff; + opacity: .4; +} +.slick-slider.slick-nav-white .slick-prev:hover, +.slick-slider.slick-nav-white .slick-next:hover { + background-color: #fff; + opacity: .8; +} +.slick-slider.slick-nav-white .slick-prev:before, +.slick-slider.slick-nav-white .slick-next:before { + color: #000; +} +.slick-slider.slick-nav-hover .slick-prev, +.slick-slider.slick-nav-hover .slick-next { + opacity: 0; + -webkit-transition: opacity 0.25s ease-out; + transition: opacity 0.25s ease-out; +} +.slick-slider.slick-nav-hover:hover .slick-prev, +.slick-slider.slick-nav-hover:hover .slick-next { + opacity: .25; +} +.slick-slider.slick-nav-hover:hover .slick-prev:hover, +.slick-slider.slick-nav-hover:hover .slick-next:hover { + opacity: .6; +} +.note-editor .note-toolbar { + background-color: #fcfcfc !important; +} +.note-editor.note-frame { + border-color: #ddd; +} +.note-editor .note-toolbar { + border-bottom-color: #ddd; +} +.note-editor .note-statusbar .note-resizebar { + border-top-color: #ddd; +} +.note-editor .note-toolbar.btn-toolbar { + margin-left: 0; +} +.jqstooltip { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + border: none !important; + background-color: rgba(0, 0, 0, 0.75) !important; +} +.pie-chart { + position: relative; + display: inline-block; +} +.pie-chart > span { + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + margin-top: -2px; + right: 0; + left: 0; + text-align: center; +} +.flot-tooltip { + position: absolute; + display: none; + color: #fff; + background: rgba(0, 0, 0, 0.75); + padding: 4px 8px; +} +.flot-pie-label { + font-size: 13px; + text-align: center; + padding: 4px 8px; + color: #fff; +} +.legend > table td { + padding: 3px 4px; + font-size: 14px; +} +.fc-event { + padding-left: 3px; + padding-right: 3px; + font-size: 12px; + font-weight: 600; + line-height: 1.4; + color: rgba(0, 0, 0, 0.75); + border: 1px solid #b5d0eb; + background-color: #b5d0eb; + border-radius: 0; +} +.fc-event:hover { + color: rgba(0, 0, 0, 0.75); +} +.fc button { + height: 34px; + line-height: 34px; + font-weight: 600; +} +.fc-state-default.fc-corner-left { + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; +} +.fc-state-default.fc-corner-right { + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; +} +.fc-state-default { + color: #393939; + background-color: #f3f3f3; + background-image: none; + border: 1px solid #e6e6e6; + text-shadow: none; + box-shadow: none; +} +.fc-state-hover, +.fc-state-down, +.fc-state-active, +.fc-state-disabled { + color: #393939; + background-color: #dfdfdf; + border-color: #c7c7c7; +} +.fc-state-hover { + color: #393939; + -webkit-transition: all 0.15s ease-out; + transition: all 0.15s ease-out; +} +.fc-state-down, +.fc-state-active { + background-color: #c5c5c5; + border-color: #aeaeae; + box-shadow: none; +} +.fc-state-disabled { + opacity: .35; +} +.fc-toolbar { + margin-bottom: 29px; +} +.fc-toolbar h2 { + font-weight: 400; +} +.fc thead th.fc-widget-header { + padding-top: 6px; + padding-bottom: 6px; + font-size: 16px; + font-weight: 600; + text-transform: uppercase; + background-color: #f9f9f9; +} +.fc-unthemed th, +.fc-unthemed td, +.fc-unthemed hr, +.fc-unthemed thead, +.fc-unthemed tbody, +.fc-unthemed .fc-row, +.fc-unthemed .fc-popover { + border-color: #eee; +} +.placeholder { + color: #aaa; +} diff --git a/public/static/admin/css/themes/amethyst.min.css b/public/static/admin/css/themes/amethyst.min.css new file mode 100644 index 0000000..ecdb036 --- /dev/null +++ b/public/static/admin/css/themes/amethyst.min.css @@ -0,0 +1,4 @@ +/*! +* OneUI - v2.2.0 - Auto-compiled on 2016-07-13 - Copyright 2016 +* @author pixelcave +*/body{background-color:#f6f5f7}a{color:#a48ad4}a.link-effect:before{background-color:#7852bf}a:hover,a:focus{color:#7852bf}a:active{color:#a48ad4}.text-primary{color:#a48ad4}a.text-primary:hover,a.text-primary:active,a.text-primary:focus,button.text-primary:hover,button.text-primary:active,button.text-primary:focus{color:#a48ad4;opacity:.75}.text-primary-dark{color:#4f546b}a.text-primary-dark:hover,a.text-primary-dark:active,a.text-primary-dark:focus,button.text-primary-dark:hover,button.text-primary-dark:active,button.text-primary-dark:focus{color:#4f546b;opacity:.75}.text-primary-darker{color:#353847}a.text-primary-darker:hover,a.text-primary-darker:active,a.text-primary-darker:focus,button.text-primary-darker:hover,button.text-primary-darker:active,button.text-primary-darker:focus{color:#353847;opacity:.75}.text-primary-light{color:#c7b7e4}a.text-primary-light:hover,a.text-primary-light:active,a.text-primary-light:focus,button.text-primary-light:hover,button.text-primary-light:active,button.text-primary-light:focus{color:#c7b7e4;opacity:.75}.text-primary-lighter{color:#e4dcf2}a.text-primary-lighter:hover,a.text-primary-lighter:active,a.text-primary-lighter:focus,button.text-primary-lighter:hover,button.text-primary-lighter:active,button.text-primary-lighter:focus{color:#e4dcf2;opacity:.75}.bg-primary{background-color:#a48ad4}a.bg-primary:hover,a.bg-primary:focus{background-color:#8765c6}.bg-primary-op{background-color:rgba(164,138,212,0.75)}a.bg-primary-op:hover,a.bg-primary-op:focus{background-color:rgba(135,101,198,0.75)}.bg-primary-dark{background-color:#4f546b}a.bg-primary-dark:hover,a.bg-primary-dark:focus{background-color:#393d4e}.bg-primary-dark-op{background-color:rgba(79,84,107,0.83)}a.bg-primary-dark-op:hover,a.bg-primary-dark-op:focus{background-color:rgba(57,61,78,0.83)}.bg-primary-darker{background-color:#353847}a.bg-primary-darker:hover,a.bg-primary-darker:focus{background-color:#1f212a}.bg-primary-light{background-color:#c7b7e4}a.bg-primary-light:hover,a.bg-primary-light:focus{background-color:#aa91d7}.bg-primary-lighter{background-color:#e4dcf2}a.bg-primary-lighter:hover,a.bg-primary-lighter:focus{background-color:#c7b7e4}.btn-primary{color:#fff;background-color:#a48ad4;border-color:#8765c6}.btn-primary:focus,.btn-primary.focus,.btn-primary:hover{color:#fff;background-color:#8d6cc9;border-color:#6740ae}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#7047bb;border-color:#513289}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#7047bb;border-color:#513289}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#a48ad4;border-color:#8765c6}.btn-primary .badge{color:#a48ad4;background-color:#fff}.label-primary{background-color:#a48ad4}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#8765c6}.badge-primary{background-color:#a48ad4}.progress-bar-primary{background-color:#a48ad4}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{background-color:#a48ad4}.nav-pills>li.active>a>.badge{color:#a48ad4}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#a48ad4;-webkit-box-shadow:0 2px #a48ad4;box-shadow:0 2px #a48ad4}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{color:#a48ad4;-webkit-box-shadow:0 2px #a48ad4;box-shadow:0 2px #a48ad4}.pager li>a:hover,.pager li>a:focus{color:#a48ad4}a.list-group-item:hover,a.list-group-item:focus{color:#a48ad4}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{background-color:#a48ad4;border-color:#a48ad4}.list-group-item.active>.badge{color:#a48ad4}.tooltip-inner{background-color:#353847}.tooltip.top .tooltip-arrow{border-top-color:#353847}.tooltip.right .tooltip-arrow{border-right-color:#353847}.tooltip.left .tooltip-arrow{border-left-color:#353847}.tooltip.bottom .tooltip-arrow{border-bottom-color:#353847}.table-header-bg>thead>tr>th,.table-header-bg>thead>tr>td{background-color:#a48ad4;border-bottom-color:#a48ad4}.panel-primary{border-color:#e4dcf2}.panel-primary>.panel-heading{color:#a48ad4;background-color:#f8f6fc;border-color:#e4dcf2}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#e4dcf2}.panel-primary>.panel-heading .badge{color:#f8f6fc;background-color:#a48ad4}.panel-primary>.panel-heading a{font-weight:400}.panel-primary>.panel-heading a:hover,.panel-primary>.panel-heading a:focus{color:#8765c6}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#e4dcf2}#page-loader:after{background-color:#a48ad4}.header-navbar-transparent.header-navbar-fixed.header-navbar-scroll #header-navbar{background-color:#4f546b}#page-container,#sidebar{background-color:#353847}#main-container{background-color:#f6f5f7}.nav-main-header{background-color:#353847}@media screen and (min-width:992px){.nav-main-header{background-color:transparent}.nav-main-header ul{background-color:#4f546b}}.form-material.form-material-primary>.form-control:focus{-webkit-box-shadow:0 2px 0 #a48ad4;box-shadow:0 2px 0 #a48ad4}.form-material.form-material-primary>.form-control:focus+label{color:#a48ad4}.form-material.form-material-primary>.form-control:focus~.input-group-addon{color:#a48ad4;-webkit-box-shadow:0 2px 0 #a48ad4;box-shadow:0 2px 0 #a48ad4}.css-checkbox-primary input:checked+span{background-color:#a48ad4;border-color:#a48ad4}.css-radio-primary input:checked+span:after,.switch-primary input:checked+span{background-color:#a48ad4}.block>.nav-tabs>li>a:hover{color:#a48ad4}.block>.nav-tabs.nav-tabs-alt>li>a:hover{-webkit-box-shadow:0 2px #a48ad4;box-shadow:0 2px #a48ad4}.block>.nav-tabs.nav-tabs-alt>li.active>a,.block>.nav-tabs.nav-tabs-alt>li.active>a:hover,.block>.nav-tabs.nav-tabs-alt>li.active>a:focus{-webkit-box-shadow:0 2px #a48ad4;box-shadow:0 2px #a48ad4}.ribbon-primary .ribbon-box{background-color:#a48ad4}.ribbon-primary.ribbon-bookmark .ribbon-box:before{border-color:#a48ad4;border-left-color:transparent}.ribbon-primary.ribbon-bookmark.ribbon-left .ribbon-box:before{border-color:#a48ad4;border-right-color:transparent}.ribbon-primary.ribbon-modern .ribbon-box:before{border-color:#a48ad4;border-left-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-bottom .ribbon-box:before{border-color:#a48ad4;border-top-color:transparent;border-left-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left .ribbon-box:before{border-color:#a48ad4;border-right-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before{border-color:#a48ad4;border-top-color:transparent;border-right-color:transparent}.irs-bar,.irs-bar-edge,.irs-from,.irs-to,.irs-single,.irs-grid-pol{background:#a48ad4}.dropzone:hover{border-color:#a48ad4}.dropzone:hover .dz-message{color:#a48ad4}.datepicker table tr td.active:hover,.datepicker table tr td.active:hover:hover,.datepicker table tr td.active.disabled:hover,.datepicker table tr td.active.disabled:hover:hover,.datepicker table tr td.active:focus,.datepicker table tr td.active:hover:focus,.datepicker table tr td.active.disabled:focus,.datepicker table tr td.active.disabled:hover:focus,.datepicker table tr td.active:active,.datepicker table tr td.active:hover:active,.datepicker table tr td.active.disabled:active,.datepicker table tr td.active.disabled:hover:active,.datepicker table tr td.active.active,.datepicker table tr td.active:hover.active,.datepicker table tr td.active.disabled.active,.datepicker table tr td.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td.active,.open .dropdown-toggle.datepicker table tr td.active:hover,.open .dropdown-toggle.datepicker table tr td.active.disabled,.open .dropdown-toggle.datepicker table tr td.active.disabled:hover,.datepicker table tr td span.active:hover,.datepicker table tr td span.active:hover:hover,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active.disabled:hover:hover,.datepicker table tr td span.active:focus,.datepicker table tr td span.active:hover:focus,.datepicker table tr td span.active.disabled:focus,.datepicker table tr td span.active.disabled:hover:focus,.datepicker table tr td span.active:active,.datepicker table tr td span.active:hover:active,.datepicker table tr td span.active.disabled:active,.datepicker table tr td span.active.disabled:hover:active,.datepicker table tr td span.active.active,.datepicker table tr td span.active:hover.active,.datepicker table tr td span.active.disabled.active,.datepicker table tr td span.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td span.active,.open .dropdown-toggle.datepicker table tr td span.active:hover,.open .dropdown-toggle.datepicker table tr td span.active.disabled,.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover{background-color:#a48ad4;border-color:#a48ad4}div.tagsinput span.tag{background-color:#a48ad4}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#a48ad4}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#a48ad4}.autocomplete-suggestion b{color:#a48ad4}.editable-click, a.editable-click, a.editable-click:hover{border-bottom: dashed 1px #a48ad4;} \ No newline at end of file diff --git a/public/static/admin/css/themes/city.min.css b/public/static/admin/css/themes/city.min.css new file mode 100644 index 0000000..75afe1c --- /dev/null +++ b/public/static/admin/css/themes/city.min.css @@ -0,0 +1,4 @@ +/*! +* OneUI - v2.2.0 - Auto-compiled on 2016-07-13 - Copyright 2016 +* @author pixelcave +*/body{background-color:#f5f5f5}a{color:#ff6b6b}a.link-effect:before{background-color:#ff1f1f}a:hover,a:focus{color:#ff1f1f}a:active{color:#ff6b6b}.text-primary{color:#ff6b6b}a.text-primary:hover,a.text-primary:active,a.text-primary:focus,button.text-primary:hover,button.text-primary:active,button.text-primary:focus{color:#ff6b6b;opacity:.75}.text-primary-dark{color:#555}a.text-primary-dark:hover,a.text-primary-dark:active,a.text-primary-dark:focus,button.text-primary-dark:hover,button.text-primary-dark:active,button.text-primary-dark:focus{color:#555;opacity:.75}.text-primary-darker{color:#333}a.text-primary-darker:hover,a.text-primary-darker:active,a.text-primary-darker:focus,button.text-primary-darker:hover,button.text-primary-darker:active,button.text-primary-darker:focus{color:#333;opacity:.75}.text-primary-light{color:#ff8f8f}a.text-primary-light:hover,a.text-primary-light:active,a.text-primary-light:focus,button.text-primary-light:hover,button.text-primary-light:active,button.text-primary-light:focus{color:#ff8f8f;opacity:.75}.text-primary-lighter{color:#ffb8b8}a.text-primary-lighter:hover,a.text-primary-lighter:active,a.text-primary-lighter:focus,button.text-primary-lighter:hover,button.text-primary-lighter:active,button.text-primary-lighter:focus{color:#ffb8b8;opacity:.75}.bg-primary{background-color:#ff6b6b}a.bg-primary:hover,a.bg-primary:focus{background-color:#ff3838}.bg-primary-op{background-color:rgba(255,107,107,0.75)}a.bg-primary-op:hover,a.bg-primary-op:focus{background-color:rgba(255,56,56,0.75)}.bg-primary-dark{background-color:#555}a.bg-primary-dark:hover,a.bg-primary-dark:focus{background-color:#3b3b3b}.bg-primary-dark-op{background-color:rgba(85,85,85,0.83)}a.bg-primary-dark-op:hover,a.bg-primary-dark-op:focus{background-color:rgba(59,59,59,0.83)}.bg-primary-darker{background-color:#333}a.bg-primary-darker:hover,a.bg-primary-darker:focus{background-color:#1a1a1a}.bg-primary-light{background-color:#ff8f8f}a.bg-primary-light:hover,a.bg-primary-light:focus{background-color:#ff5c5c}.bg-primary-lighter{background-color:#ffb8b8}a.bg-primary-lighter:hover,a.bg-primary-lighter:focus{background-color:#ff8585}.btn-primary{color:#fff;background-color:#ff6b6b;border-color:#ff3838}.btn-primary:focus,.btn-primary.focus,.btn-primary:hover{color:#fff;background-color:#ff4242;border-color:#fa0000}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#ff0f0f;border-color:#c70000}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#ff0f0f;border-color:#c70000}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#ff6b6b;border-color:#ff3838}.btn-primary .badge{color:#ff6b6b;background-color:#fff}.label-primary{background-color:#ff6b6b}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#ff3838}.badge-primary{background-color:#ff6b6b}.progress-bar-primary{background-color:#ff6b6b}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{background-color:#ff6b6b}.nav-pills>li.active>a>.badge{color:#ff6b6b}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#ff6b6b;-webkit-box-shadow:0 2px #ff6b6b;box-shadow:0 2px #ff6b6b}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{color:#ff6b6b;-webkit-box-shadow:0 2px #ff6b6b;box-shadow:0 2px #ff6b6b}.pager li>a:hover,.pager li>a:focus{color:#ff6b6b}a.list-group-item:hover,a.list-group-item:focus{color:#ff6b6b}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{background-color:#ff6b6b;border-color:#ff6b6b}.list-group-item.active>.badge{color:#ff6b6b}.tooltip-inner{background-color:#333}.tooltip.top .tooltip-arrow{border-top-color:#333}.tooltip.right .tooltip-arrow{border-right-color:#333}.tooltip.left .tooltip-arrow{border-left-color:#333}.tooltip.bottom .tooltip-arrow{border-bottom-color:#333}.table-header-bg>thead>tr>th,.table-header-bg>thead>tr>td{background-color:#ff6b6b;border-bottom-color:#ff6b6b}.panel-primary{border-color:#ffb8b8}.panel-primary>.panel-heading{color:#ff6b6b;background-color:#ffdbdb;border-color:#ffb8b8}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ffb8b8}.panel-primary>.panel-heading .badge{color:#ffdbdb;background-color:#ff6b6b}.panel-primary>.panel-heading a{font-weight:400}.panel-primary>.panel-heading a:hover,.panel-primary>.panel-heading a:focus{color:#ff3838}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ffb8b8}#page-loader:after{background-color:#ff6b6b}.header-navbar-transparent.header-navbar-fixed.header-navbar-scroll #header-navbar{background-color:#555}#page-container,#sidebar{background-color:#333}#main-container{background-color:#f5f5f5}.nav-main-header{background-color:#333}@media screen and (min-width:992px){.nav-main-header{background-color:transparent}.nav-main-header ul{background-color:#555}}.form-material.form-material-primary>.form-control:focus{-webkit-box-shadow:0 2px 0 #ff6b6b;box-shadow:0 2px 0 #ff6b6b}.form-material.form-material-primary>.form-control:focus+label{color:#ff6b6b}.form-material.form-material-primary>.form-control:focus~.input-group-addon{color:#ff6b6b;-webkit-box-shadow:0 2px 0 #ff6b6b;box-shadow:0 2px 0 #ff6b6b}.css-checkbox-primary input:checked+span{background-color:#ff6b6b;border-color:#ff6b6b}.css-radio-primary input:checked+span:after,.switch-primary input:checked+span{background-color:#ff6b6b}.block>.nav-tabs>li>a:hover{color:#ff6b6b}.block>.nav-tabs.nav-tabs-alt>li>a:hover{-webkit-box-shadow:0 2px #ff6b6b;box-shadow:0 2px #ff6b6b}.block>.nav-tabs.nav-tabs-alt>li.active>a,.block>.nav-tabs.nav-tabs-alt>li.active>a:hover,.block>.nav-tabs.nav-tabs-alt>li.active>a:focus{-webkit-box-shadow:0 2px #ff6b6b;box-shadow:0 2px #ff6b6b}.ribbon-primary .ribbon-box{background-color:#ff6b6b}.ribbon-primary.ribbon-bookmark .ribbon-box:before{border-color:#ff6b6b;border-left-color:transparent}.ribbon-primary.ribbon-bookmark.ribbon-left .ribbon-box:before{border-color:#ff6b6b;border-right-color:transparent}.ribbon-primary.ribbon-modern .ribbon-box:before{border-color:#ff6b6b;border-left-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-bottom .ribbon-box:before{border-color:#ff6b6b;border-top-color:transparent;border-left-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left .ribbon-box:before{border-color:#ff6b6b;border-right-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before{border-color:#ff6b6b;border-top-color:transparent;border-right-color:transparent}.irs-bar,.irs-bar-edge,.irs-from,.irs-to,.irs-single,.irs-grid-pol{background:#ff6b6b}.dropzone:hover{border-color:#ff6b6b}.dropzone:hover .dz-message{color:#ff6b6b}.datepicker table tr td.active:hover,.datepicker table tr td.active:hover:hover,.datepicker table tr td.active.disabled:hover,.datepicker table tr td.active.disabled:hover:hover,.datepicker table tr td.active:focus,.datepicker table tr td.active:hover:focus,.datepicker table tr td.active.disabled:focus,.datepicker table tr td.active.disabled:hover:focus,.datepicker table tr td.active:active,.datepicker table tr td.active:hover:active,.datepicker table tr td.active.disabled:active,.datepicker table tr td.active.disabled:hover:active,.datepicker table tr td.active.active,.datepicker table tr td.active:hover.active,.datepicker table tr td.active.disabled.active,.datepicker table tr td.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td.active,.open .dropdown-toggle.datepicker table tr td.active:hover,.open .dropdown-toggle.datepicker table tr td.active.disabled,.open .dropdown-toggle.datepicker table tr td.active.disabled:hover,.datepicker table tr td span.active:hover,.datepicker table tr td span.active:hover:hover,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active.disabled:hover:hover,.datepicker table tr td span.active:focus,.datepicker table tr td span.active:hover:focus,.datepicker table tr td span.active.disabled:focus,.datepicker table tr td span.active.disabled:hover:focus,.datepicker table tr td span.active:active,.datepicker table tr td span.active:hover:active,.datepicker table tr td span.active.disabled:active,.datepicker table tr td span.active.disabled:hover:active,.datepicker table tr td span.active.active,.datepicker table tr td span.active:hover.active,.datepicker table tr td span.active.disabled.active,.datepicker table tr td span.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td span.active,.open .dropdown-toggle.datepicker table tr td span.active:hover,.open .dropdown-toggle.datepicker table tr td span.active.disabled,.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover{background-color:#ff6b6b;border-color:#ff6b6b}div.tagsinput span.tag{background-color:#ff6b6b}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#ff6b6b}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#ff6b6b}.autocomplete-suggestion b{color:#ff6b6b}.editable-click, a.editable-click, a.editable-click:hover{border-bottom: dashed 1px #ff6b6b;} \ No newline at end of file diff --git a/public/static/admin/css/themes/default.min.css b/public/static/admin/css/themes/default.min.css new file mode 100644 index 0000000..1238859 --- /dev/null +++ b/public/static/admin/css/themes/default.min.css @@ -0,0 +1 @@ +.editable-click, a.editable-click, a.editable-click:hover{border-bottom: dashed 1px #5c90d2;} \ No newline at end of file diff --git a/public/static/admin/css/themes/flat.min.css b/public/static/admin/css/themes/flat.min.css new file mode 100644 index 0000000..c2a922c --- /dev/null +++ b/public/static/admin/css/themes/flat.min.css @@ -0,0 +1,4 @@ +/*! +* OneUI - v2.2.0 - Auto-compiled on 2016-07-13 - Copyright 2016 +* @author pixelcave +*/body{background-color:#f5f7f7}a{color:#44b4a6}a.link-effect:before{background-color:#2f7c73}a:hover,a:focus{color:#2f7c73}a:active{color:#44b4a6}.text-primary{color:#44b4a6}a.text-primary:hover,a.text-primary:active,a.text-primary:focus,button.text-primary:hover,button.text-primary:active,button.text-primary:focus{color:#44b4a6;opacity:.75}.text-primary-dark{color:#3f5259}a.text-primary-dark:hover,a.text-primary-dark:active,a.text-primary-dark:focus,button.text-primary-dark:hover,button.text-primary-dark:active,button.text-primary-dark:focus{color:#3f5259;opacity:.75}.text-primary-darker{color:#242f33}a.text-primary-darker:hover,a.text-primary-darker:active,a.text-primary-darker:focus,button.text-primary-darker:hover,button.text-primary-darker:active,button.text-primary-darker:focus{color:#242f33;opacity:.75}.text-primary-light{color:#83d0c7}a.text-primary-light:hover,a.text-primary-light:active,a.text-primary-light:focus,button.text-primary-light:hover,button.text-primary-light:active,button.text-primary-light:focus{color:#83d0c7;opacity:.75}.text-primary-lighter{color:#a8ded8}a.text-primary-lighter:hover,a.text-primary-lighter:active,a.text-primary-lighter:focus,button.text-primary-lighter:hover,button.text-primary-lighter:active,button.text-primary-lighter:focus{color:#a8ded8;opacity:.75}.bg-primary{background-color:#44b4a6}a.bg-primary:hover,a.bg-primary:focus{background-color:#368f84}.bg-primary-op{background-color:rgba(68,180,166,0.75)}a.bg-primary-op:hover,a.bg-primary-op:focus{background-color:rgba(54,143,132,0.75)}.bg-primary-dark{background-color:#3f5259}a.bg-primary-dark:hover,a.bg-primary-dark:focus{background-color:#2a363b}.bg-primary-dark-op{background-color:rgba(63,82,89,0.83)}a.bg-primary-dark-op:hover,a.bg-primary-dark-op:focus{background-color:rgba(42,54,59,0.83)}.bg-primary-darker{background-color:#242f33}a.bg-primary-darker:hover,a.bg-primary-darker:focus{background-color:#0f1315}.bg-primary-light{background-color:#83d0c7}a.bg-primary-light:hover,a.bg-primary-light:focus{background-color:#5ec2b6}.bg-primary-lighter{background-color:#a8ded8}a.bg-primary-lighter:hover,a.bg-primary-lighter:focus{background-color:#83d0c7}.btn-primary{color:#fff;background-color:#44b4a6;border-color:#368f84}.btn-primary:focus,.btn-primary.focus,.btn-primary:hover{color:#fff;background-color:#39968b;border-color:#25635b}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#2b7169;border-color:#173e39}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#2b7169;border-color:#173e39}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#44b4a6;border-color:#368f84}.btn-primary .badge{color:#44b4a6;background-color:#fff}.label-primary{background-color:#44b4a6}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#368f84}.badge-primary{background-color:#44b4a6}.progress-bar-primary{background-color:#44b4a6}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{background-color:#44b4a6}.nav-pills>li.active>a>.badge{color:#44b4a6}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#44b4a6;-webkit-box-shadow:0 2px #44b4a6;box-shadow:0 2px #44b4a6}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{color:#44b4a6;-webkit-box-shadow:0 2px #44b4a6;box-shadow:0 2px #44b4a6}.pager li>a:hover,.pager li>a:focus{color:#44b4a6}a.list-group-item:hover,a.list-group-item:focus{color:#44b4a6}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{background-color:#44b4a6;border-color:#44b4a6}.list-group-item.active>.badge{color:#44b4a6}.tooltip-inner{background-color:#242f33}.tooltip.top .tooltip-arrow{border-top-color:#242f33}.tooltip.right .tooltip-arrow{border-right-color:#242f33}.tooltip.left .tooltip-arrow{border-left-color:#242f33}.tooltip.bottom .tooltip-arrow{border-bottom-color:#242f33}.table-header-bg>thead>tr>th,.table-header-bg>thead>tr>td{background-color:#44b4a6;border-bottom-color:#44b4a6}.panel-primary{border-color:#a8ded8}.panel-primary>.panel-heading{color:#44b4a6;background-color:#c2e8e3;border-color:#a8ded8}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#a8ded8}.panel-primary>.panel-heading .badge{color:#c2e8e3;background-color:#44b4a6}.panel-primary>.panel-heading a{font-weight:400}.panel-primary>.panel-heading a:hover,.panel-primary>.panel-heading a:focus{color:#368f84}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#a8ded8}#page-loader:after{background-color:#44b4a6}.header-navbar-transparent.header-navbar-fixed.header-navbar-scroll #header-navbar{background-color:#3f5259}#page-container,#sidebar{background-color:#242f33}#main-container{background-color:#f5f7f7}.nav-main-header{background-color:#242f33}@media screen and (min-width:992px){.nav-main-header{background-color:transparent}.nav-main-header ul{background-color:#3f5259}}.form-material.form-material-primary>.form-control:focus{-webkit-box-shadow:0 2px 0 #44b4a6;box-shadow:0 2px 0 #44b4a6}.form-material.form-material-primary>.form-control:focus+label{color:#44b4a6}.form-material.form-material-primary>.form-control:focus~.input-group-addon{color:#44b4a6;-webkit-box-shadow:0 2px 0 #44b4a6;box-shadow:0 2px 0 #44b4a6}.css-checkbox-primary input:checked+span{background-color:#44b4a6;border-color:#44b4a6}.css-radio-primary input:checked+span:after,.switch-primary input:checked+span{background-color:#44b4a6}.block>.nav-tabs>li>a:hover{color:#44b4a6}.block>.nav-tabs.nav-tabs-alt>li>a:hover{-webkit-box-shadow:0 2px #44b4a6;box-shadow:0 2px #44b4a6}.block>.nav-tabs.nav-tabs-alt>li.active>a,.block>.nav-tabs.nav-tabs-alt>li.active>a:hover,.block>.nav-tabs.nav-tabs-alt>li.active>a:focus{-webkit-box-shadow:0 2px #44b4a6;box-shadow:0 2px #44b4a6}.ribbon-primary .ribbon-box{background-color:#44b4a6}.ribbon-primary.ribbon-bookmark .ribbon-box:before{border-color:#44b4a6;border-left-color:transparent}.ribbon-primary.ribbon-bookmark.ribbon-left .ribbon-box:before{border-color:#44b4a6;border-right-color:transparent}.ribbon-primary.ribbon-modern .ribbon-box:before{border-color:#44b4a6;border-left-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-bottom .ribbon-box:before{border-color:#44b4a6;border-top-color:transparent;border-left-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left .ribbon-box:before{border-color:#44b4a6;border-right-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before{border-color:#44b4a6;border-top-color:transparent;border-right-color:transparent}.irs-bar,.irs-bar-edge,.irs-from,.irs-to,.irs-single,.irs-grid-pol{background:#44b4a6}.dropzone:hover{border-color:#44b4a6}.dropzone:hover .dz-message{color:#44b4a6}.datepicker table tr td.active:hover,.datepicker table tr td.active:hover:hover,.datepicker table tr td.active.disabled:hover,.datepicker table tr td.active.disabled:hover:hover,.datepicker table tr td.active:focus,.datepicker table tr td.active:hover:focus,.datepicker table tr td.active.disabled:focus,.datepicker table tr td.active.disabled:hover:focus,.datepicker table tr td.active:active,.datepicker table tr td.active:hover:active,.datepicker table tr td.active.disabled:active,.datepicker table tr td.active.disabled:hover:active,.datepicker table tr td.active.active,.datepicker table tr td.active:hover.active,.datepicker table tr td.active.disabled.active,.datepicker table tr td.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td.active,.open .dropdown-toggle.datepicker table tr td.active:hover,.open .dropdown-toggle.datepicker table tr td.active.disabled,.open .dropdown-toggle.datepicker table tr td.active.disabled:hover,.datepicker table tr td span.active:hover,.datepicker table tr td span.active:hover:hover,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active.disabled:hover:hover,.datepicker table tr td span.active:focus,.datepicker table tr td span.active:hover:focus,.datepicker table tr td span.active.disabled:focus,.datepicker table tr td span.active.disabled:hover:focus,.datepicker table tr td span.active:active,.datepicker table tr td span.active:hover:active,.datepicker table tr td span.active.disabled:active,.datepicker table tr td span.active.disabled:hover:active,.datepicker table tr td span.active.active,.datepicker table tr td span.active:hover.active,.datepicker table tr td span.active.disabled.active,.datepicker table tr td span.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td span.active,.open .dropdown-toggle.datepicker table tr td span.active:hover,.open .dropdown-toggle.datepicker table tr td span.active.disabled,.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover{background-color:#44b4a6;border-color:#44b4a6}div.tagsinput span.tag{background-color:#44b4a6}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#44b4a6}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#44b4a6}.autocomplete-suggestion b{color:#44b4a6}.editable-click, a.editable-click, a.editable-click:hover{border-bottom: dashed 1px #44b4a6;} \ No newline at end of file diff --git a/public/static/admin/css/themes/modern.min.css b/public/static/admin/css/themes/modern.min.css new file mode 100644 index 0000000..24618d7 --- /dev/null +++ b/public/static/admin/css/themes/modern.min.css @@ -0,0 +1,4 @@ +/*! +* OneUI - v2.2.0 - Auto-compiled on 2016-07-13 - Copyright 2016 +* @author pixelcave +*/body{background-color:#f5f6f7}a{color:#1F98CA}a.link-effect:before{background-color:#0d707f}a:hover,a:focus{color:#0d707f}a:active{color:#1F98CA}.text-primary{color:#1F98CA}a.text-primary:hover,a.text-primary:active,a.text-primary:focus,button.text-primary:hover,button.text-primary:active,button.text-primary:focus{color:#1F98CA;opacity:.75}.text-primary-dark{color:#3e4d52}a.text-primary-dark:hover,a.text-primary-dark:active,a.text-primary-dark:focus,button.text-primary-dark:hover,button.text-primary-dark:active,button.text-primary-dark:focus{color:#3e4d52;opacity:.75}.text-primary-darker{color:#323e42}a.text-primary-darker:hover,a.text-primary-darker:active,a.text-primary-darker:focus,button.text-primary-darker:hover,button.text-primary-darker:active,button.text-primary-darker:focus{color:#323e42;opacity:.75}.text-primary-light{color:#7fe3f2}a.text-primary-light:hover,a.text-primary-light:active,a.text-primary-light:focus,button.text-primary-light:hover,button.text-primary-light:active,button.text-primary-light:focus{color:#7fe3f2;opacity:.75}.text-primary-lighter{color:#c4f2f9}a.text-primary-lighter:hover,a.text-primary-lighter:active,a.text-primary-lighter:focus,button.text-primary-lighter:hover,button.text-primary-lighter:active,button.text-primary-lighter:focus{color:#c4f2f9;opacity:.75}.bg-primary{background-color:#1F98CA}a.bg-primary:hover,a.bg-primary:focus{background-color:#0f8496}.bg-primary-op{background-color:rgba(20,173,196,0.75)}a.bg-primary-op:hover,a.bg-primary-op:focus{background-color:rgba(15,132,150,0.75)}.bg-primary-dark{background-color:#3e4d52}a.bg-primary-dark:hover,a.bg-primary-dark:focus{background-color:#283235}.bg-primary-dark-op{background-color:rgba(62,77,82,0.83)}a.bg-primary-dark-op:hover,a.bg-primary-dark-op:focus{background-color:rgba(40,50,53,0.83)}.bg-primary-darker{background-color:#323e42}a.bg-primary-darker:hover,a.bg-primary-darker:focus{background-color:#1c2325}.bg-primary-light{background-color:#7fe3f2}a.bg-primary-light:hover,a.bg-primary-light:focus{background-color:#51d9ed}.bg-primary-lighter{background-color:#c4f2f9}a.bg-primary-lighter:hover,a.bg-primary-lighter:focus{background-color:#96e8f4}.btn-primary{color:#fff;background-color:#1F98CA;border-color:#0f8496}.btn-primary:focus,.btn-primary.focus,.btn-primary:hover{color:#fff;background-color:#1987B5;border-color:#0a535e}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#0c6371;border-color:#052a30}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#0c6371;border-color:#052a30}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#1F98CA;border-color:#0f8496}.btn-primary .badge{color:#1F98CA;background-color:#fff}.label-primary{background-color:#1F98CA}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#0f8496}.badge-primary{background-color:#1F98CA}.progress-bar-primary{background-color:#1F98CA}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{background-color:#1F98CA}.nav-pills>li.active>a>.badge{color:#1F98CA}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#1F98CA;-webkit-box-shadow:0 2px #1F98CA;box-shadow:0 2px #1F98CA}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{color:#1F98CA;-webkit-box-shadow:0 2px #1F98CA;box-shadow:0 2px #1F98CA}.pager li>a:hover,.pager li>a:focus{color:#1F98CA}a.list-group-item:hover,a.list-group-item:focus{color:#1F98CA}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{background-color:#1F98CA;border-color:#1F98CA}.list-group-item.active>.badge{color:#1F98CA}.tooltip-inner{background-color:#323e42}.tooltip.top .tooltip-arrow{border-top-color:#323e42}.tooltip.right .tooltip-arrow{border-right-color:#323e42}.tooltip.left .tooltip-arrow{border-left-color:#323e42}.tooltip.bottom .tooltip-arrow{border-bottom-color:#323e42}.table-header-bg>thead>tr>th,.table-header-bg>thead>tr>td{background-color:#1F98CA;border-bottom-color:#1F98CA}.panel-primary{border-color:#c4f2f9}.panel-primary>.panel-heading{color:#1F98CA;background-color:#e5f9fc;border-color:#c4f2f9}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#c4f2f9}.panel-primary>.panel-heading .badge{color:#e5f9fc;background-color:#1F98CA}.panel-primary>.panel-heading a{font-weight:400}.panel-primary>.panel-heading a:hover,.panel-primary>.panel-heading a:focus{color:#0f8496}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#c4f2f9}#page-loader:after{background-color:#1F98CA}.header-navbar-transparent.header-navbar-fixed.header-navbar-scroll #header-navbar{background-color:#3e4d52}#page-container,#sidebar{background-color:#343F44}#main-container{background-color:#f5f6f7}.nav-main-header{background-color:#323e42}@media screen and (min-width:992px){.nav-main-header{background-color:transparent}.nav-main-header ul{background-color:#3e4d52}}.form-material.form-material-primary>.form-control:focus{-webkit-box-shadow:0 2px 0 #1F98CA;box-shadow:0 2px 0 #1F98CA}.form-material.form-material-primary>.form-control:focus+label{color:#1F98CA}.form-material.form-material-primary>.form-control:focus~.input-group-addon{color:#1F98CA;-webkit-box-shadow:0 2px 0 #1F98CA;box-shadow:0 2px 0 #1F98CA}.css-checkbox-primary input:checked+span{background-color:#1F98CA;border-color:#1F98CA}.css-radio-primary input:checked+span:after,.switch-primary input:checked+span{background-color:#1F98CA}.block>.nav-tabs>li>a:hover{color:#1F98CA}.block>.nav-tabs.nav-tabs-alt>li>a:hover{-webkit-box-shadow:0 2px #1F98CA;box-shadow:0 2px #1F98CA}.block>.nav-tabs.nav-tabs-alt>li.active>a,.block>.nav-tabs.nav-tabs-alt>li.active>a:hover,.block>.nav-tabs.nav-tabs-alt>li.active>a:focus{-webkit-box-shadow:0 2px #1F98CA;box-shadow:0 2px #1F98CA}.ribbon-primary .ribbon-box{background-color:#1F98CA}.ribbon-primary.ribbon-bookmark .ribbon-box:before{border-color:#1F98CA;border-left-color:transparent}.ribbon-primary.ribbon-bookmark.ribbon-left .ribbon-box:before{border-color:#1F98CA;border-right-color:transparent}.ribbon-primary.ribbon-modern .ribbon-box:before{border-color:#1F98CA;border-left-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-bottom .ribbon-box:before{border-color:#1F98CA;border-top-color:transparent;border-left-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left .ribbon-box:before{border-color:#1F98CA;border-right-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before{border-color:#1F98CA;border-top-color:transparent;border-right-color:transparent}.irs-bar,.irs-bar-edge,.irs-from,.irs-to,.irs-single,.irs-grid-pol{background:#1F98CA}.dropzone:hover{border-color:#1F98CA}.dropzone:hover .dz-message{color:#1F98CA}.datepicker table tr td.active:hover,.datepicker table tr td.active:hover:hover,.datepicker table tr td.active.disabled:hover,.datepicker table tr td.active.disabled:hover:hover,.datepicker table tr td.active:focus,.datepicker table tr td.active:hover:focus,.datepicker table tr td.active.disabled:focus,.datepicker table tr td.active.disabled:hover:focus,.datepicker table tr td.active:active,.datepicker table tr td.active:hover:active,.datepicker table tr td.active.disabled:active,.datepicker table tr td.active.disabled:hover:active,.datepicker table tr td.active.active,.datepicker table tr td.active:hover.active,.datepicker table tr td.active.disabled.active,.datepicker table tr td.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td.active,.open .dropdown-toggle.datepicker table tr td.active:hover,.open .dropdown-toggle.datepicker table tr td.active.disabled,.open .dropdown-toggle.datepicker table tr td.active.disabled:hover,.datepicker table tr td span.active:hover,.datepicker table tr td span.active:hover:hover,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active.disabled:hover:hover,.datepicker table tr td span.active:focus,.datepicker table tr td span.active:hover:focus,.datepicker table tr td span.active.disabled:focus,.datepicker table tr td span.active.disabled:hover:focus,.datepicker table tr td span.active:active,.datepicker table tr td span.active:hover:active,.datepicker table tr td span.active.disabled:active,.datepicker table tr td span.active.disabled:hover:active,.datepicker table tr td span.active.active,.datepicker table tr td span.active:hover.active,.datepicker table tr td span.active.disabled.active,.datepicker table tr td span.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td span.active,.open .dropdown-toggle.datepicker table tr td span.active:hover,.open .dropdown-toggle.datepicker table tr td span.active.disabled,.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover{background-color:#1F98CA;border-color:#1F98CA}div.tagsinput span.tag{background-color:#1F98CA}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#1F98CA}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#1F98CA}.autocomplete-suggestion b{color:#1F98CA}.editable-click, a.editable-click, a.editable-click:hover{border-bottom: dashed 1px #1F98CA;} \ No newline at end of file diff --git a/public/static/admin/css/themes/smooth.min.css b/public/static/admin/css/themes/smooth.min.css new file mode 100644 index 0000000..afb22f2 --- /dev/null +++ b/public/static/admin/css/themes/smooth.min.css @@ -0,0 +1,4 @@ +/*! +* OneUI - v2.2.0 - Auto-compiled on 2016-07-13 - Copyright 2016 +* @author pixelcave +*/body{background-color:#f7f5f6}a{color:#ff6c9d}a.link-effect:before{background-color:#ff206a}a:hover,a:focus{color:#ff206a}a:active{color:#ff6c9d}.text-primary{color:#ff6c9d}a.text-primary:hover,a.text-primary:active,a.text-primary:focus,button.text-primary:hover,button.text-primary:active,button.text-primary:focus{color:#ff6c9d;opacity:.75}.text-primary-dark{color:#4a5568}a.text-primary-dark:hover,a.text-primary-dark:active,a.text-primary-dark:focus,button.text-primary-dark:hover,button.text-primary-dark:active,button.text-primary-dark:focus{color:#4a5568;opacity:.75}.text-primary-darker{color:#333a47}a.text-primary-darker:hover,a.text-primary-darker:active,a.text-primary-darker:focus,button.text-primary-darker:hover,button.text-primary-darker:active,button.text-primary-darker:focus{color:#333a47;opacity:.75}.text-primary-light{color:#ff90b5}a.text-primary-light:hover,a.text-primary-light:active,a.text-primary-light:focus,button.text-primary-light:hover,button.text-primary-light:active,button.text-primary-light:focus{color:#ff90b5;opacity:.75}.text-primary-lighter{color:#ffb9d0}a.text-primary-lighter:hover,a.text-primary-lighter:active,a.text-primary-lighter:focus,button.text-primary-lighter:hover,button.text-primary-lighter:active,button.text-primary-lighter:focus{color:#ffb9d0;opacity:.75}.bg-primary{background-color:#ff6c9d}a.bg-primary:hover,a.bg-primary:focus{background-color:#ff397b}.bg-primary-op{background-color:rgba(255,108,157,0.75)}a.bg-primary-op:hover,a.bg-primary-op:focus{background-color:rgba(255,57,123,0.75)}.bg-primary-dark{background-color:#4a5568}a.bg-primary-dark:hover,a.bg-primary-dark:focus{background-color:#353d4a}.bg-primary-dark-op{background-color:rgba(74,85,104,0.83)}a.bg-primary-dark-op:hover,a.bg-primary-dark-op:focus{background-color:rgba(53,61,74,0.83)}.bg-primary-darker{background-color:#333a47}a.bg-primary-darker:hover,a.bg-primary-darker:focus{background-color:#1e2229}.bg-primary-light{background-color:#ff90b5}a.bg-primary-light:hover,a.bg-primary-light:focus{background-color:#ff5d93}.bg-primary-lighter{background-color:#ffb9d0}a.bg-primary-lighter:hover,a.bg-primary-lighter:focus{background-color:#ff86ae}.btn-primary{color:#fff;background-color:#ff6c9d;border-color:#ff397b}.btn-primary:focus,.btn-primary.focus,.btn-primary:hover{color:#fff;background-color:#ff4382;border-color:#fb0054}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#ff1060;border-color:#c80043}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#ff1060;border-color:#c80043}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#ff6c9d;border-color:#ff397b}.btn-primary .badge{color:#ff6c9d;background-color:#fff}.label-primary{background-color:#ff6c9d}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#ff397b}.badge-primary{background-color:#ff6c9d}.progress-bar-primary{background-color:#ff6c9d}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{background-color:#ff6c9d}.nav-pills>li.active>a>.badge{color:#ff6c9d}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#ff6c9d;-webkit-box-shadow:0 2px #ff6c9d;box-shadow:0 2px #ff6c9d}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{color:#ff6c9d;-webkit-box-shadow:0 2px #ff6c9d;box-shadow:0 2px #ff6c9d}.pager li>a:hover,.pager li>a:focus{color:#ff6c9d}a.list-group-item:hover,a.list-group-item:focus{color:#ff6c9d}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{background-color:#ff6c9d;border-color:#ff6c9d}.list-group-item.active>.badge{color:#ff6c9d}.tooltip-inner{background-color:#333a47}.tooltip.top .tooltip-arrow{border-top-color:#333a47}.tooltip.right .tooltip-arrow{border-right-color:#333a47}.tooltip.left .tooltip-arrow{border-left-color:#333a47}.tooltip.bottom .tooltip-arrow{border-bottom-color:#333a47}.table-header-bg>thead>tr>th,.table-header-bg>thead>tr>td{background-color:#ff6c9d;border-bottom-color:#ff6c9d}.panel-primary{border-color:#ffb9d0}.panel-primary>.panel-heading{color:#ff6c9d;background-color:#ffdce8;border-color:#ffb9d0}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ffb9d0}.panel-primary>.panel-heading .badge{color:#ffdce8;background-color:#ff6c9d}.panel-primary>.panel-heading a{font-weight:400}.panel-primary>.panel-heading a:hover,.panel-primary>.panel-heading a:focus{color:#ff397b}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ffb9d0}#page-loader:after{background-color:#ff6c9d}.header-navbar-transparent.header-navbar-fixed.header-navbar-scroll #header-navbar{background-color:#4a5568}#page-container,#sidebar{background-color:#333a47}#main-container{background-color:#f7f5f6}.nav-main-header{background-color:#333a47}@media screen and (min-width:992px){.nav-main-header{background-color:transparent}.nav-main-header ul{background-color:#4a5568}}.form-material.form-material-primary>.form-control:focus{-webkit-box-shadow:0 2px 0 #ff6c9d;box-shadow:0 2px 0 #ff6c9d}.form-material.form-material-primary>.form-control:focus+label{color:#ff6c9d}.form-material.form-material-primary>.form-control:focus~.input-group-addon{color:#ff6c9d;-webkit-box-shadow:0 2px 0 #ff6c9d;box-shadow:0 2px 0 #ff6c9d}.css-checkbox-primary input:checked+span{background-color:#ff6c9d;border-color:#ff6c9d}.css-radio-primary input:checked+span:after,.switch-primary input:checked+span{background-color:#ff6c9d}.block>.nav-tabs>li>a:hover{color:#ff6c9d}.block>.nav-tabs.nav-tabs-alt>li>a:hover{-webkit-box-shadow:0 2px #ff6c9d;box-shadow:0 2px #ff6c9d}.block>.nav-tabs.nav-tabs-alt>li.active>a,.block>.nav-tabs.nav-tabs-alt>li.active>a:hover,.block>.nav-tabs.nav-tabs-alt>li.active>a:focus{-webkit-box-shadow:0 2px #ff6c9d;box-shadow:0 2px #ff6c9d}.ribbon-primary .ribbon-box{background-color:#ff6c9d}.ribbon-primary.ribbon-bookmark .ribbon-box:before{border-color:#ff6c9d;border-left-color:transparent}.ribbon-primary.ribbon-bookmark.ribbon-left .ribbon-box:before{border-color:#ff6c9d;border-right-color:transparent}.ribbon-primary.ribbon-modern .ribbon-box:before{border-color:#ff6c9d;border-left-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-bottom .ribbon-box:before{border-color:#ff6c9d;border-top-color:transparent;border-left-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left .ribbon-box:before{border-color:#ff6c9d;border-right-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before{border-color:#ff6c9d;border-top-color:transparent;border-right-color:transparent}.irs-bar,.irs-bar-edge,.irs-from,.irs-to,.irs-single,.irs-grid-pol{background:#ff6c9d}.dropzone:hover{border-color:#ff6c9d}.dropzone:hover .dz-message{color:#ff6c9d}.datepicker table tr td.active:hover,.datepicker table tr td.active:hover:hover,.datepicker table tr td.active.disabled:hover,.datepicker table tr td.active.disabled:hover:hover,.datepicker table tr td.active:focus,.datepicker table tr td.active:hover:focus,.datepicker table tr td.active.disabled:focus,.datepicker table tr td.active.disabled:hover:focus,.datepicker table tr td.active:active,.datepicker table tr td.active:hover:active,.datepicker table tr td.active.disabled:active,.datepicker table tr td.active.disabled:hover:active,.datepicker table tr td.active.active,.datepicker table tr td.active:hover.active,.datepicker table tr td.active.disabled.active,.datepicker table tr td.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td.active,.open .dropdown-toggle.datepicker table tr td.active:hover,.open .dropdown-toggle.datepicker table tr td.active.disabled,.open .dropdown-toggle.datepicker table tr td.active.disabled:hover,.datepicker table tr td span.active:hover,.datepicker table tr td span.active:hover:hover,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active.disabled:hover:hover,.datepicker table tr td span.active:focus,.datepicker table tr td span.active:hover:focus,.datepicker table tr td span.active.disabled:focus,.datepicker table tr td span.active.disabled:hover:focus,.datepicker table tr td span.active:active,.datepicker table tr td span.active:hover:active,.datepicker table tr td span.active.disabled:active,.datepicker table tr td span.active.disabled:hover:active,.datepicker table tr td span.active.active,.datepicker table tr td span.active:hover.active,.datepicker table tr td span.active.disabled.active,.datepicker table tr td span.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td span.active,.open .dropdown-toggle.datepicker table tr td span.active:hover,.open .dropdown-toggle.datepicker table tr td span.active.disabled,.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover{background-color:#ff6c9d;border-color:#ff6c9d}div.tagsinput span.tag{background-color:#ff6c9d}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#ff6c9d}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#ff6c9d}.autocomplete-suggestion b{color:#ff6c9d}.editable-click, a.editable-click, a.editable-click:hover{border-bottom: dashed 1px #ff6c9d;} \ No newline at end of file diff --git a/public/static/admin/fonts/FontAwesome.otf b/public/static/admin/fonts/FontAwesome.otf new file mode 100644 index 0000000..401ec0f Binary files /dev/null and b/public/static/admin/fonts/FontAwesome.otf differ diff --git a/public/static/admin/fonts/Simple-Line-Icons.eot b/public/static/admin/fonts/Simple-Line-Icons.eot new file mode 100644 index 0000000..d258f62 Binary files /dev/null and b/public/static/admin/fonts/Simple-Line-Icons.eot differ diff --git a/public/static/admin/fonts/Simple-Line-Icons.svg b/public/static/admin/fonts/Simple-Line-Icons.svg new file mode 100644 index 0000000..fe5927e --- /dev/null +++ b/public/static/admin/fonts/Simple-Line-Icons.svg @@ -0,0 +1,1369 @@ + + + + +This is a custom SVG font generated by IcoMoon. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/static/admin/fonts/Simple-Line-Icons.ttf b/public/static/admin/fonts/Simple-Line-Icons.ttf new file mode 100644 index 0000000..2194f1f Binary files /dev/null and b/public/static/admin/fonts/Simple-Line-Icons.ttf differ diff --git a/public/static/admin/fonts/Simple-Line-Icons.woff b/public/static/admin/fonts/Simple-Line-Icons.woff new file mode 100644 index 0000000..50df1e4 Binary files /dev/null and b/public/static/admin/fonts/Simple-Line-Icons.woff differ diff --git a/public/static/admin/fonts/fontawesome-webfont.eot b/public/static/admin/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..e9f60ca Binary files /dev/null and b/public/static/admin/fonts/fontawesome-webfont.eot differ diff --git a/public/static/admin/fonts/fontawesome-webfont.svg b/public/static/admin/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..d7534c9 --- /dev/null +++ b/public/static/admin/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/static/admin/fonts/fontawesome-webfont.ttf b/public/static/admin/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..35acda2 Binary files /dev/null and b/public/static/admin/fonts/fontawesome-webfont.ttf differ diff --git a/public/static/admin/fonts/fontawesome-webfont.woff b/public/static/admin/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..400014a Binary files /dev/null and b/public/static/admin/fonts/fontawesome-webfont.woff differ diff --git a/public/static/admin/fonts/fontawesome-webfont.woff2 b/public/static/admin/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..4d13fc6 Binary files /dev/null and b/public/static/admin/fonts/fontawesome-webfont.woff2 differ diff --git a/public/static/admin/fonts/glyphicons-halflings-regular.eot b/public/static/admin/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 0000000..b93a495 Binary files /dev/null and b/public/static/admin/fonts/glyphicons-halflings-regular.eot differ diff --git a/public/static/admin/fonts/glyphicons-halflings-regular.svg b/public/static/admin/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 0000000..8376c0f --- /dev/null +++ b/public/static/admin/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/static/admin/fonts/glyphicons-halflings-regular.ttf b/public/static/admin/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 0000000..1413fc6 Binary files /dev/null and b/public/static/admin/fonts/glyphicons-halflings-regular.ttf differ diff --git a/public/static/admin/fonts/glyphicons-halflings-regular.woff b/public/static/admin/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 0000000..9e61285 Binary files /dev/null and b/public/static/admin/fonts/glyphicons-halflings-regular.woff differ diff --git a/public/static/admin/fonts/glyphicons-halflings-regular.woff2 b/public/static/admin/fonts/glyphicons-halflings-regular.woff2 new file mode 100644 index 0000000..64539b5 Binary files /dev/null and b/public/static/admin/fonts/glyphicons-halflings-regular.woff2 differ diff --git a/public/static/admin/img/README.md b/public/static/admin/img/README.md new file mode 100644 index 0000000..f8ff695 --- /dev/null +++ b/public/static/admin/img/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 系统图片目录 diff --git a/public/static/admin/img/avatar.jpg b/public/static/admin/img/avatar.jpg new file mode 100644 index 0000000..2816648 Binary files /dev/null and b/public/static/admin/img/avatar.jpg differ diff --git a/public/static/admin/img/favicons/600x600.png b/public/static/admin/img/favicons/600x600.png new file mode 100644 index 0000000..6a35560 Binary files /dev/null and b/public/static/admin/img/favicons/600x600.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-114x114.png b/public/static/admin/img/favicons/apple-touch-icon-114x114.png new file mode 100644 index 0000000..847a66c Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-114x114.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-120x120.png b/public/static/admin/img/favicons/apple-touch-icon-120x120.png new file mode 100644 index 0000000..ede81e2 Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-120x120.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-144x144.png b/public/static/admin/img/favicons/apple-touch-icon-144x144.png new file mode 100644 index 0000000..ea1fafb Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-144x144.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-152x152.png b/public/static/admin/img/favicons/apple-touch-icon-152x152.png new file mode 100644 index 0000000..c813c99 Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-152x152.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-180x180.png b/public/static/admin/img/favicons/apple-touch-icon-180x180.png new file mode 100644 index 0000000..da4a645 Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-180x180.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-57x57.png b/public/static/admin/img/favicons/apple-touch-icon-57x57.png new file mode 100644 index 0000000..c59a9b1 Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-57x57.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-60x60.png b/public/static/admin/img/favicons/apple-touch-icon-60x60.png new file mode 100644 index 0000000..a56408f Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-60x60.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-72x72.png b/public/static/admin/img/favicons/apple-touch-icon-72x72.png new file mode 100644 index 0000000..6d26f26 Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-72x72.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-76x76.png b/public/static/admin/img/favicons/apple-touch-icon-76x76.png new file mode 100644 index 0000000..827df20 Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-76x76.png differ diff --git a/public/static/admin/img/favicons/favicon-160x160.png b/public/static/admin/img/favicons/favicon-160x160.png new file mode 100644 index 0000000..6a0bb7b Binary files /dev/null and b/public/static/admin/img/favicons/favicon-160x160.png differ diff --git a/public/static/admin/img/favicons/favicon-16x16.png b/public/static/admin/img/favicons/favicon-16x16.png new file mode 100644 index 0000000..3a90e06 Binary files /dev/null and b/public/static/admin/img/favicons/favicon-16x16.png differ diff --git a/public/static/admin/img/favicons/favicon-192x192.png b/public/static/admin/img/favicons/favicon-192x192.png new file mode 100644 index 0000000..eed9cca Binary files /dev/null and b/public/static/admin/img/favicons/favicon-192x192.png differ diff --git a/public/static/admin/img/favicons/favicon-32x32.png b/public/static/admin/img/favicons/favicon-32x32.png new file mode 100644 index 0000000..e370221 Binary files /dev/null and b/public/static/admin/img/favicons/favicon-32x32.png differ diff --git a/public/static/admin/img/favicons/favicon-96x96.png b/public/static/admin/img/favicons/favicon-96x96.png new file mode 100644 index 0000000..eb515cd Binary files /dev/null and b/public/static/admin/img/favicons/favicon-96x96.png differ diff --git a/public/static/admin/img/favicons/favicon.png b/public/static/admin/img/favicons/favicon.png new file mode 100644 index 0000000..e370221 Binary files /dev/null and b/public/static/admin/img/favicons/favicon.png differ diff --git a/public/static/admin/img/files/7z.png b/public/static/admin/img/files/7z.png new file mode 100644 index 0000000..1288e5e Binary files /dev/null and b/public/static/admin/img/files/7z.png differ diff --git a/public/static/admin/img/files/avi.png b/public/static/admin/img/files/avi.png new file mode 100644 index 0000000..68ca914 Binary files /dev/null and b/public/static/admin/img/files/avi.png differ diff --git a/public/static/admin/img/files/bmp.png b/public/static/admin/img/files/bmp.png new file mode 100644 index 0000000..b002db7 Binary files /dev/null and b/public/static/admin/img/files/bmp.png differ diff --git a/public/static/admin/img/files/css.png b/public/static/admin/img/files/css.png new file mode 100644 index 0000000..0625ddb Binary files /dev/null and b/public/static/admin/img/files/css.png differ diff --git a/public/static/admin/img/files/csv.png b/public/static/admin/img/files/csv.png new file mode 100644 index 0000000..62a33e0 Binary files /dev/null and b/public/static/admin/img/files/csv.png differ diff --git a/public/static/admin/img/files/dll.png b/public/static/admin/img/files/dll.png new file mode 100644 index 0000000..d2fba97 Binary files /dev/null and b/public/static/admin/img/files/dll.png differ diff --git a/public/static/admin/img/files/doc.png b/public/static/admin/img/files/doc.png new file mode 100644 index 0000000..3e2f283 Binary files /dev/null and b/public/static/admin/img/files/doc.png differ diff --git a/public/static/admin/img/files/docx.png b/public/static/admin/img/files/docx.png new file mode 100644 index 0000000..d2797ca Binary files /dev/null and b/public/static/admin/img/files/docx.png differ diff --git a/public/static/admin/img/files/dwg.png b/public/static/admin/img/files/dwg.png new file mode 100644 index 0000000..2620590 Binary files /dev/null and b/public/static/admin/img/files/dwg.png differ diff --git a/public/static/admin/img/files/file.png b/public/static/admin/img/files/file.png new file mode 100644 index 0000000..2905f41 Binary files /dev/null and b/public/static/admin/img/files/file.png differ diff --git a/public/static/admin/img/files/fon.png b/public/static/admin/img/files/fon.png new file mode 100644 index 0000000..ae18f37 Binary files /dev/null and b/public/static/admin/img/files/fon.png differ diff --git a/public/static/admin/img/files/gif.png b/public/static/admin/img/files/gif.png new file mode 100644 index 0000000..6952fd3 Binary files /dev/null and b/public/static/admin/img/files/gif.png differ diff --git a/public/static/admin/img/files/hlp.png b/public/static/admin/img/files/hlp.png new file mode 100644 index 0000000..62e94ed Binary files /dev/null and b/public/static/admin/img/files/hlp.png differ diff --git a/public/static/admin/img/files/html.png b/public/static/admin/img/files/html.png new file mode 100644 index 0000000..09bd7b0 Binary files /dev/null and b/public/static/admin/img/files/html.png differ diff --git a/public/static/admin/img/files/ini.png b/public/static/admin/img/files/ini.png new file mode 100644 index 0000000..a306fbb Binary files /dev/null and b/public/static/admin/img/files/ini.png differ diff --git a/public/static/admin/img/files/jpg.png b/public/static/admin/img/files/jpg.png new file mode 100644 index 0000000..cc263e8 Binary files /dev/null and b/public/static/admin/img/files/jpg.png differ diff --git a/public/static/admin/img/files/mdb.png b/public/static/admin/img/files/mdb.png new file mode 100644 index 0000000..3bd8c1b Binary files /dev/null and b/public/static/admin/img/files/mdb.png differ diff --git a/public/static/admin/img/files/midi.png b/public/static/admin/img/files/midi.png new file mode 100644 index 0000000..3bc4dc9 Binary files /dev/null and b/public/static/admin/img/files/midi.png differ diff --git a/public/static/admin/img/files/mp3.png b/public/static/admin/img/files/mp3.png new file mode 100644 index 0000000..51e34f9 Binary files /dev/null and b/public/static/admin/img/files/mp3.png differ diff --git a/public/static/admin/img/files/mp4.png b/public/static/admin/img/files/mp4.png new file mode 100644 index 0000000..619e349 Binary files /dev/null and b/public/static/admin/img/files/mp4.png differ diff --git a/public/static/admin/img/files/mpg.png b/public/static/admin/img/files/mpg.png new file mode 100644 index 0000000..cc2e7e3 Binary files /dev/null and b/public/static/admin/img/files/mpg.png differ diff --git a/public/static/admin/img/files/odbc.png b/public/static/admin/img/files/odbc.png new file mode 100644 index 0000000..2548f81 Binary files /dev/null and b/public/static/admin/img/files/odbc.png differ diff --git a/public/static/admin/img/files/ogg.png b/public/static/admin/img/files/ogg.png new file mode 100644 index 0000000..fcc905e Binary files /dev/null and b/public/static/admin/img/files/ogg.png differ diff --git a/public/static/admin/img/files/pdf.png b/public/static/admin/img/files/pdf.png new file mode 100644 index 0000000..d9f0da8 Binary files /dev/null and b/public/static/admin/img/files/pdf.png differ diff --git a/public/static/admin/img/files/php.png b/public/static/admin/img/files/php.png new file mode 100644 index 0000000..fcc2775 Binary files /dev/null and b/public/static/admin/img/files/php.png differ diff --git a/public/static/admin/img/files/png.png b/public/static/admin/img/files/png.png new file mode 100644 index 0000000..d56f09e Binary files /dev/null and b/public/static/admin/img/files/png.png differ diff --git a/public/static/admin/img/files/pps.png b/public/static/admin/img/files/pps.png new file mode 100644 index 0000000..fecd536 Binary files /dev/null and b/public/static/admin/img/files/pps.png differ diff --git a/public/static/admin/img/files/ppsx.png b/public/static/admin/img/files/ppsx.png new file mode 100644 index 0000000..02dc660 Binary files /dev/null and b/public/static/admin/img/files/ppsx.png differ diff --git a/public/static/admin/img/files/ppt.png b/public/static/admin/img/files/ppt.png new file mode 100644 index 0000000..d4f01ab Binary files /dev/null and b/public/static/admin/img/files/ppt.png differ diff --git a/public/static/admin/img/files/pptx.png b/public/static/admin/img/files/pptx.png new file mode 100644 index 0000000..ebbc9fe Binary files /dev/null and b/public/static/admin/img/files/pptx.png differ diff --git a/public/static/admin/img/files/psd.png b/public/static/admin/img/files/psd.png new file mode 100644 index 0000000..478edcb Binary files /dev/null and b/public/static/admin/img/files/psd.png differ diff --git a/public/static/admin/img/files/rar.png b/public/static/admin/img/files/rar.png new file mode 100644 index 0000000..3c6ed4e Binary files /dev/null and b/public/static/admin/img/files/rar.png differ diff --git a/public/static/admin/img/files/reg.png b/public/static/admin/img/files/reg.png new file mode 100644 index 0000000..43023ff Binary files /dev/null and b/public/static/admin/img/files/reg.png differ diff --git a/public/static/admin/img/files/rtf.png b/public/static/admin/img/files/rtf.png new file mode 100644 index 0000000..fabc302 Binary files /dev/null and b/public/static/admin/img/files/rtf.png differ diff --git a/public/static/admin/img/files/sql.png b/public/static/admin/img/files/sql.png new file mode 100644 index 0000000..bf6153e Binary files /dev/null and b/public/static/admin/img/files/sql.png differ diff --git a/public/static/admin/img/files/swf.png b/public/static/admin/img/files/swf.png new file mode 100644 index 0000000..9abf7f2 Binary files /dev/null and b/public/static/admin/img/files/swf.png differ diff --git a/public/static/admin/img/files/sys.png b/public/static/admin/img/files/sys.png new file mode 100644 index 0000000..5affad4 Binary files /dev/null and b/public/static/admin/img/files/sys.png differ diff --git a/public/static/admin/img/files/tar.png b/public/static/admin/img/files/tar.png new file mode 100644 index 0000000..0ded972 Binary files /dev/null and b/public/static/admin/img/files/tar.png differ diff --git a/public/static/admin/img/files/tif.png b/public/static/admin/img/files/tif.png new file mode 100644 index 0000000..1f6ba9e Binary files /dev/null and b/public/static/admin/img/files/tif.png differ diff --git a/public/static/admin/img/files/tiff.png b/public/static/admin/img/files/tiff.png new file mode 100644 index 0000000..dd02eec Binary files /dev/null and b/public/static/admin/img/files/tiff.png differ diff --git a/public/static/admin/img/files/ttf.png b/public/static/admin/img/files/ttf.png new file mode 100644 index 0000000..e1c1608 Binary files /dev/null and b/public/static/admin/img/files/ttf.png differ diff --git a/public/static/admin/img/files/txt.png b/public/static/admin/img/files/txt.png new file mode 100644 index 0000000..d90ea8b Binary files /dev/null and b/public/static/admin/img/files/txt.png differ diff --git a/public/static/admin/img/files/url.png b/public/static/admin/img/files/url.png new file mode 100644 index 0000000..fca17ce Binary files /dev/null and b/public/static/admin/img/files/url.png differ diff --git a/public/static/admin/img/files/wav.png b/public/static/admin/img/files/wav.png new file mode 100644 index 0000000..b01695d Binary files /dev/null and b/public/static/admin/img/files/wav.png differ diff --git a/public/static/admin/img/files/wma.png b/public/static/admin/img/files/wma.png new file mode 100644 index 0000000..87a1987 Binary files /dev/null and b/public/static/admin/img/files/wma.png differ diff --git a/public/static/admin/img/files/wmv.png b/public/static/admin/img/files/wmv.png new file mode 100644 index 0000000..cddf38f Binary files /dev/null and b/public/static/admin/img/files/wmv.png differ diff --git a/public/static/admin/img/files/xls.png b/public/static/admin/img/files/xls.png new file mode 100644 index 0000000..e5897f5 Binary files /dev/null and b/public/static/admin/img/files/xls.png differ diff --git a/public/static/admin/img/files/xlsx.png b/public/static/admin/img/files/xlsx.png new file mode 100644 index 0000000..9c61c4d Binary files /dev/null and b/public/static/admin/img/files/xlsx.png differ diff --git a/public/static/admin/img/files/xml.png b/public/static/admin/img/files/xml.png new file mode 100644 index 0000000..c4008ab Binary files /dev/null and b/public/static/admin/img/files/xml.png differ diff --git a/public/static/admin/img/files/zip.png b/public/static/admin/img/files/zip.png new file mode 100644 index 0000000..8a44a96 Binary files /dev/null and b/public/static/admin/img/files/zip.png differ diff --git a/public/static/admin/img/logo-text.png b/public/static/admin/img/logo-text.png new file mode 100644 index 0000000..01ee266 Binary files /dev/null and b/public/static/admin/img/logo-text.png differ diff --git a/public/static/admin/img/logo.png b/public/static/admin/img/logo.png new file mode 100644 index 0000000..e87afe1 Binary files /dev/null and b/public/static/admin/img/logo.png differ diff --git a/public/static/admin/img/none.png b/public/static/admin/img/none.png new file mode 100644 index 0000000..bd59906 Binary files /dev/null and b/public/static/admin/img/none.png differ diff --git a/public/static/admin/js/LICENSE.txt b/public/static/admin/js/LICENSE.txt new file mode 100644 index 0000000..2255550 --- /dev/null +++ b/public/static/admin/js/LICENSE.txt @@ -0,0 +1,27 @@ +版权所有 (c) 2016~2017,河源市卓锐科技有限公司保留所有权利。 + +DolphinPHP 用户协议 + +版权所有 (c) 2016~2017,河源市卓锐科技有限公司保留所有权利。 + +感谢您选择DolphinPHP,希望我们的努力能为您提供一个极简极致极速的PHP快速开发解决方案。 + +用户须知:本协议是您与河源市卓锐科技有限公司(以下简称卓锐公司)之间关于您使用DolphinPHP产品及服务的法律协议。无论您是个人或组织、盈利与否、用途如何(包括以学习和研究为目的),均需仔细阅读本协议,包括免除或者限制卓锐公司责任的免责条款及对您的权利限制。请您审阅并接受或不接受本服务条款。如您不同意本服务条款及/或卓锐公司随时对其的修改,您应不使用或主动取消DolphinPHP产品。否则,您的任何对DolphinPHP的相关服务的注册、登陆、下载、查看等使用行为将被视为您对本服务条款全部的完全接受,包括接受卓锐公司对服务条款随时所做的任何修改。 + +本服务条款一旦发生变更, 卓锐公司将在产品官网上公布修改内容。修改后的服务条款一旦在网站公布即有效代替原来的服务条款。您可随时登陆官网查阅最新版服务条款。如果您选择接受本条款,即表示您同意接受协议各项条件的约束。如果您不同意本服务条款,则不能获得使用本服务的权利。您若有违反本条款规定,卓锐公司有权随时中止或终止您对DolphinPHP产品的使用资格并保留追究相关法律责任的权利。 + +在理解、同意、并遵守本协议的全部条款后,方可开始使用DolphinPHP产品。您也可能与卓锐公司直接签订另一书面协议,以补充或者取代本协议的全部或者任何部分。 + +卓锐公司拥有DolphinPHP的全部知识产权,包括商标和著作权。本软件只供许可协议,并非出售。卓锐公司只允许您在遵守本协议各项条款的情况下复制、下载、安装、使用或者以其他方式受益于本软件的功能或者知识产权。 + +个人非商业用途可免费使用(但不包括其衍生产品、插件或者服务),也可以根据个人实际情况选择是否购买授权。 + +非个人用户(泛指非个人的团体,如企业、政府单位、教育机构、协会团体、厂矿、工作室等)必须购买软件授权后方可使用。 + +您可以在协议规定的约束和限制范围内修改DolphinPHP以适应您的网站要求,但免费版必须保留软件版本信息的正常显示及相关连接正常。 + +禁止去除DolphinPHP源码里的版权信息,商业授权版本可去除后台界面及前台界面的相关版权信息。 + +禁止在DolphinPHP整体或任何部分基础上发展任何派生版本、修改版本或第三方版本用于重新分发。 + +未经官方许可,不得对本软件或与之关联的商业授权进行出租、出售、抵押或发放子许可证。 \ No newline at end of file diff --git a/public/static/admin/js/app.js b/public/static/admin/js/app.js new file mode 100644 index 0000000..88acad2 --- /dev/null +++ b/public/static/admin/js/app.js @@ -0,0 +1,1493 @@ +/* + * Document : app.js + * Author : pixelcave + * Description: UI Framework Custom Functionality (available to all pages) + * + */ + +var App = function() { + // Helper variables - set in uiInit() + var $lHtml, $lBody, $lPage, $lSidebar, $lSidebarScroll, $lSideOverlay, $lSideOverlayScroll, $lHeader, $lMain, $lFooter; + + /* + ******************************************************************************************** + * + * BASE UI FUNCTIONALITY + * + * Functions which handle vital UI functionality such as main navigation and layout + * They are auto initialized in every page + * + ********************************************************************************************* + */ + + // User Interface init + var uiInit = function() { + // Set variables + $lHtml = jQuery('html'); + $lBody = jQuery('body'); + $lPage = jQuery('#page-container'); + $lSidebar = jQuery('#sidebar'); + $lSidebarScroll = jQuery('#sidebar-scroll'); + $lSideOverlay = jQuery('#side-overlay'); + $lSideOverlayScroll = jQuery('#side-overlay-scroll'); + $lHeader = jQuery('#header-navbar'); + $lMain = jQuery('#main-container'); + $lFooter = jQuery('#page-footer'); + + // Initialize Tooltips + jQuery('[data-toggle="tooltip"], .js-tooltip').tooltip({ + container: 'body', + animation: false + }); + + // Initialize Popovers + jQuery('[data-toggle="popover"], .js-popover').popover({ + container: 'body', + animation: true, + trigger: 'hover' + }); + + // Initialize Tabs + jQuery('[data-toggle="tabs"] a, .js-tabs a').click(function(e){ + e.preventDefault(); + jQuery(this).tab('show'); + }); + + // Init form placeholder (for IE9) + jQuery('.form-control').placeholder(); + }; + + // Layout functionality + var uiLayout = function() { + // Resizes #main-container min height (push footer to the bottom) + var $resizeTimeout; + + if ($lMain.length) { + uiHandleMain(); + + jQuery(window).on('resize orientationchange', function(){ + clearTimeout($resizeTimeout); + + $resizeTimeout = setTimeout(function(){ + uiHandleMain(); + }, 150); + }); + } + + // Init sidebar and side overlay custom scrolling + uiHandleScroll('init'); + + // Init transparent header functionality (solid on scroll - used in frontend) + if ($lPage.hasClass('header-navbar-fixed') && $lPage.hasClass('header-navbar-transparent')) { + jQuery(window).on('scroll', function(){ + if (jQuery(this).scrollTop() > 20) { + $lPage.addClass('header-navbar-scroll'); + } else { + $lPage.removeClass('header-navbar-scroll'); + } + }); + } + + // Call layout API on button click + jQuery('[data-toggle="layout"]').on('click', function(){ + var $btn = jQuery(this); + + uiLayoutApi($btn.data('action')); + + if ($lHtml.hasClass('no-focus')) { + $btn.blur(); + } + }); + }; + + // Resizes #main-container to fill empty space if exists + var uiHandleMain = function() { + var $hWindow = jQuery(window).height(); + var $hHeader = $lHeader.outerHeight(); + var $hFooter = $lFooter.outerHeight(); + + if ($lPage.hasClass('header-navbar-fixed')) { + $lMain.css('min-height', $hWindow - $hFooter); + } else { + $lMain.css('min-height', $hWindow - ($hHeader + $hFooter)); + } + }; + + // Handles sidebar and side overlay custom scrolling functionality + var uiHandleScroll = function($mode) { + var $windowW = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; + + // Init scrolling + if ($mode === 'init') { + // Init scrolling only if required the first time + uiHandleScroll(); + + // Handle scrolling on resize or orientation change + var $sScrollTimeout; + + jQuery(window).on('resize orientationchange', function(){ + clearTimeout($sScrollTimeout); + + $sScrollTimeout = setTimeout(function(){ + uiHandleScroll(); + }, 150); + }); + } else { + // If screen width is greater than 991 pixels and .side-scroll is added to #page-container + if ($windowW > 991 && $lPage.hasClass('side-scroll')) { + // Turn scroll lock off (sidebar and side overlay - slimScroll will take care of it) + jQuery($lSidebar).scrollLock('disable'); + jQuery($lSideOverlay).scrollLock('disable'); + + // If sidebar scrolling does not exist init it.. + if ($lSidebarScroll.length && (!$lSidebarScroll.parent('.slimScrollDiv').length)) { + $lSidebarScroll.slimScroll({ + height: $lSidebar.outerHeight(), + color: '#fff', + size: '5px', + opacity : .35, + wheelStep : 15, + distance : '2px', + railVisible: false, + railOpacity: 1 + }); + } + else { // ..else resize scrolling height + $lSidebarScroll + .add($lSidebarScroll.parent()) + .css('height', $lSidebar.outerHeight()); + } + } else { + // Turn scroll lock on (sidebar and side overlay) + jQuery($lSidebar).scrollLock(); + jQuery($lSideOverlay).scrollLock(); + + // If sidebar scrolling exists destroy it.. + if ($lSidebarScroll.length && $lSidebarScroll.parent('.slimScrollDiv').length) { + $lSidebarScroll + .slimScroll({destroy: true}); + $lSidebarScroll + .attr('style', ''); + } + } + } + }; + + // Layout API + var uiLayoutApi = function($mode) { + var $windowW = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; + + // Mode selection + switch($mode) { + case 'sidebar_pos_toggle': + $lPage.toggleClass('sidebar-l sidebar-r'); + break; + case 'sidebar_pos_left': + $lPage + .removeClass('sidebar-r') + .addClass('sidebar-l'); + break; + case 'sidebar_pos_right': + $lPage + .removeClass('sidebar-l') + .addClass('sidebar-r'); + break; + case 'sidebar_toggle': + if ($windowW > 991) { + $lPage.toggleClass('sidebar-o'); + } else { + $lPage.toggleClass('sidebar-o-xs'); + } + break; + case 'sidebar_open': + if ($windowW > 991) { + $lPage.addClass('sidebar-o'); + } else { + $lPage.addClass('sidebar-o-xs'); + } + break; + case 'sidebar_close': + if ($windowW > 991) { + $lPage.removeClass('sidebar-o'); + } else { + $lPage.removeClass('sidebar-o-xs'); + } + break; + case 'sidebar_mini_toggle': + if ($windowW > 991) { + if ($lPage.hasClass('sidebar-mini')) { + $lPage.removeClass('sidebar-mini'); + Cookies.remove('sidebarMini'); + } else { + $lPage.addClass('sidebar-mini'); + Cookies.set('sidebarMini', 'true', { expires: 7 }); + } + } + if (typeof(init_table) === "function") { + init_table(); + } + break; + case 'sidebar_mini_on': + if ($windowW > 991) { + $lPage.addClass('sidebar-mini'); + } + break; + case 'sidebar_mini_off': + if ($windowW > 991) { + $lPage.removeClass('sidebar-mini'); + } + break; + case 'side_overlay_toggle': + // If side overlay scrolling does not exist init it.. + if ($lSideOverlayScroll.length && (!$lSideOverlayScroll.parent('.slimScrollDiv').length)) { + $lSideOverlayScroll.slimScroll({ + height: $lSideOverlay.outerHeight(), + color: '#000', + size: '5px', + opacity : .35, + wheelStep : 15, + distance : '2px', + railVisible: false, + railOpacity: 1 + }); + } + else { // ..else resize scrolling height + $lSideOverlayScroll + .add($lSideOverlayScroll.parent()) + .css('height', $lSideOverlay.outerHeight()); + } + $lPage.toggleClass('side-overlay-o'); + break; + case 'side_overlay_open': + $lPage.addClass('side-overlay-o'); + break; + case 'side_overlay_close': + // If side overlay scrolling exists destroy it.. + if ($lSideOverlayScroll.length && $lSideOverlayScroll.parent('.slimScrollDiv').length) { + $lSideOverlayScroll + .slimScroll({destroy: true}); + $lSideOverlayScroll + .attr('style', ''); + } + $lPage.removeClass('side-overlay-o'); + break; + case 'side_overlay_hoverable_toggle': + $lPage.toggleClass('side-overlay-hover'); + break; + case 'side_overlay_hoverable_on': + $lPage.addClass('side-overlay-hover'); + break; + case 'side_overlay_hoverable_off': + $lPage.removeClass('side-overlay-hover'); + break; + case 'header_fixed_toggle': + $lPage.toggleClass('header-navbar-fixed'); + break; + case 'header_fixed_on': + $lPage.addClass('header-navbar-fixed'); + break; + case 'header_fixed_off': + $lPage.removeClass('header-navbar-fixed'); + break; + case 'side_scroll_toggle': + $lPage.toggleClass('side-scroll'); + uiHandleScroll(); + break; + case 'side_scroll_on': + $lPage.addClass('side-scroll'); + uiHandleScroll(); + break; + case 'side_scroll_off': + $lPage.removeClass('side-scroll'); + uiHandleScroll(); + break; + default: + return false; + } + }; + + // Main navigation functionality + var uiNav = function() { + // When a submenu link is clicked + jQuery('[data-toggle="nav-submenu"]').on('click', function(e){ + // Get link + var $link = jQuery(this); + + // Get link's parent + var $parentLi = $link.parent('li'); + + if ($parentLi.hasClass('open')) { // If submenu is open, close it.. + $parentLi.removeClass('open'); + } else { // .. else if submenu is closed, close all other (same level) submenus first before open it + $link + .closest('ul') + .find('> li') + .removeClass('open'); + + $parentLi + .addClass('open'); + } + + // Remove focus from submenu link + if ($lHtml.hasClass('no-focus')) { + $link.blur(); + } + + return false; + }); + }; + + // Blocks options functionality + var uiBlocks = function() { + // Init default icons fullscreen and content toggle buttons + uiBlocksApi(false, 'init'); + + // Call blocks API on option button click + jQuery('[data-toggle="block-option"]').on('click', function(){ + uiBlocksApi(jQuery(this).closest('.block'), jQuery(this).data('action')); + }); + }; + + // Blocks API + var uiBlocksApi = function($block, $mode) { + // Set default icons for fullscreen and content toggle buttons + var $iconFullscreen = 'si si-size-fullscreen'; + var $iconFullscreenActive = 'si si-size-actual'; + var $iconContent = 'si si-arrow-up'; + var $iconContentActive = 'si si-arrow-down'; + + if ($mode === 'init') { + // Auto add the default toggle icons to fullscreen and content toggle buttons + jQuery('[data-toggle="block-option"][data-action="fullscreen_toggle"]').each(function(){ + var $this = jQuery(this); + + $this.html(''); + }); + + jQuery('[data-toggle="block-option"][data-action="content_toggle"]').each(function(){ + var $this = jQuery(this); + + $this.html(''); + }); + } else { + // Get block element + var $elBlock = ($block instanceof jQuery) ? $block : jQuery($block); + + // If element exists, procceed with blocks functionality + if ($elBlock.length) { + // Get block option buttons if exist (need them to update their icons) + var $btnFullscreen = jQuery('[data-toggle="block-option"][data-action="fullscreen_toggle"]', $elBlock); + var $btnToggle = jQuery('[data-toggle="block-option"][data-action="content_toggle"]', $elBlock); + + // Mode selection + switch($mode) { + case 'fullscreen_toggle': + $elBlock.toggleClass('block-opt-fullscreen'); + + // Enable/disable scroll lock to block + if ($elBlock.hasClass('block-opt-fullscreen')) { + jQuery($elBlock).scrollLock(); + } else { + jQuery($elBlock).scrollLock('disable'); + } + + // Update block option icon + if ($btnFullscreen.length) { + if ($elBlock.hasClass('block-opt-fullscreen')) { + jQuery('i', $btnFullscreen) + .removeClass($iconFullscreen) + .addClass($iconFullscreenActive); + } else { + jQuery('i', $btnFullscreen) + .removeClass($iconFullscreenActive) + .addClass($iconFullscreen); + } + } + if (typeof(init_table) === "function") { + init_table(); + } + break; + case 'fullscreen_on': + $elBlock.addClass('block-opt-fullscreen'); + + // Enable scroll lock to block + jQuery($elBlock).scrollLock(); + + // Update block option icon + if ($btnFullscreen.length) { + jQuery('i', $btnFullscreen) + .removeClass($iconFullscreen) + .addClass($iconFullscreenActive); + } + break; + case 'fullscreen_off': + $elBlock.removeClass('block-opt-fullscreen'); + + // Disable scroll lock to block + jQuery($elBlock).scrollLock('disable'); + + // Update block option icon + if ($btnFullscreen.length) { + jQuery('i', $btnFullscreen) + .removeClass($iconFullscreenActive) + .addClass($iconFullscreen); + } + break; + case 'content_toggle': + $elBlock.toggleClass('block-opt-hidden'); + + // Update block option icon + if ($btnToggle.length) { + if ($elBlock.hasClass('block-opt-hidden')) { + jQuery('i', $btnToggle) + .removeClass($iconContent) + .addClass($iconContentActive); + } else { + jQuery('i', $btnToggle) + .removeClass($iconContentActive) + .addClass($iconContent); + } + } + break; + case 'content_hide': + $elBlock.addClass('block-opt-hidden'); + + // Update block option icon + if ($btnToggle.length) { + jQuery('i', $btnToggle) + .removeClass($iconContent) + .addClass($iconContentActive); + } + break; + case 'content_show': + $elBlock.removeClass('block-opt-hidden'); + + // Update block option icon + if ($btnToggle.length) { + jQuery('i', $btnToggle) + .removeClass($iconContentActive) + .addClass($iconContent); + } + break; + case 'refresh_toggle': + $elBlock.toggleClass('block-opt-refresh'); + + // Return block to normal state if the demostration mode is on in the refresh option button - data-action-mode="demo" + if (jQuery('[data-toggle="block-option"][data-action="refresh_toggle"][data-action-mode="demo"]', $elBlock).length) { + setTimeout(function(){ + $elBlock.removeClass('block-opt-refresh'); + }, 2000); + } + break; + case 'state_loading': + $elBlock.addClass('block-opt-refresh'); + break; + case 'state_normal': + $elBlock.removeClass('block-opt-refresh'); + break; + case 'close': + $elBlock.hide(); + break; + case 'open': + $elBlock.show(); + break; + default: + return false; + } + } + } + }; + + // Material inputs helper + var uiForms = function() { + jQuery('.form-material.floating > .form-control').each(function(){ + var $input = jQuery(this); + var $parent = $input.parent('.form-material'); + + if ($input.val()) { + $parent.addClass('open'); + } + + $input.on('change', function(){ + if ($input.val()) { + $parent.addClass('open'); + } else { + $parent.removeClass('open'); + } + }); + }); + }; + + // Set active color themes functionality + var uiHandleTheme = function() { + var $cssTheme = jQuery('#css-theme'); + + // When a color theme link is clicked + jQuery('[data-toggle="theme"]').on('click', function(){ + var $this = jQuery(this); + var $theme = $this.data('theme'); + var $css = $this.data('css'); + + Dolphin.loading(); + jQuery.get(dolphin.theme_url, {theme: $theme}, function (res) { + Dolphin.loading('hide'); + if (res.code) { + // Set this color theme link as active + jQuery('[data-toggle="theme"]') + .parent('li') + .removeClass('active'); + + jQuery('[data-toggle="theme"][data-theme="' + $theme + '"]') + .parent('li') + .addClass('active'); + + // Update color theme + if ($theme === 'default') { + if ($cssTheme.length) { + $cssTheme.remove(); + } + } else { + if ($cssTheme.length) { + $cssTheme.attr('href', $css); + } else { + jQuery('#css-main') + .after(''); + } + } + $cssTheme = jQuery('#css-theme'); + Dolphin.notify(res.msg, 'success'); + } else { + Dolphin.notify(res.msg, 'danger'); + } + }); + }); + }; + + // Scroll to element animation helper + var uiScrollTo = function() { + jQuery('[data-toggle="scroll-to"]').on('click', function(){ + var $this = jQuery(this); + var $target = $this.data('target'); + var $speed = $this.data('speed') ? $this.data('speed') : 1000; + + jQuery('html, body').animate({ + scrollTop: jQuery($target).offset().top + }, $speed); + }); + }; + + // Toggle class helper + var uiToggleClass = function() { + jQuery('[data-toggle="class-toggle"]').on('click', function(){ + var $el = jQuery(this); + + jQuery($el.data('target').toString()).toggleClass($el.data('class').toString()); + + if ($lHtml.hasClass('no-focus')) { + $el.blur(); + } + }); + }; + + // Add the correct copyright year + var uiYearCopy = function() { + var $date = new Date(); + var $yearCopy = jQuery('.js-year-copy'); + + if ($date.getFullYear() === 2015) { + $yearCopy.html('2015'); + } else { + $yearCopy.html('2015-' + $date.getFullYear().toString().substr(2,2)); + } + }; + + // Manage page loading screen functionality + var uiLoader = function($mode) { + var $lpageLoader = jQuery('#page-loader'); + + if ($mode === 'show') { + if ($lpageLoader.length) { + $lpageLoader.fadeIn(250); + } else { + $lBody.prepend('
    '); + } + } else if ($mode === 'hide') { + if ($lpageLoader.length) { + $lpageLoader.fadeOut(250); + } + } + + return false; + }; + + /* + ******************************************************************************************** + * + * UI HELPERS (ON DEMAND) + * + * Third party plugin inits or various custom user interface helpers to extend functionality + * They need to be called in a page to be initialized. They are included here to be easy to + * init them on demand on multiple pages (usually repeated init code in common components) + * + ******************************************************************************************** + */ + + /* + * Print Page functionality + * + * App.initHelper('print-page'); + * + */ + var uiHelperPrint = function() { + // Store all #page-container classes + var $pageCls = $lPage.prop('class'); + + // Remove all classes from #page-container + $lPage.prop('class', ''); + + // Print the page + window.print(); + + // Restore all #page-container classes + $lPage.prop('class', $pageCls); + }; + + /* + * Custom Table functionality such as section toggling or checkable rows + * + * App.initHelper('table-tools'); + * + */ + + // Table sections functionality + var uiHelperTableToolsSections = function(){ + // For each table + jQuery('.js-table-sections').each(function(){ + var $table = jQuery(this); + + // When a row is clicked in tbody.js-table-sections-header + jQuery('.js-table-sections-header > tr', $table).on('click', function(e) { + var $row = jQuery(this); + var $tbody = $row.parent('tbody'); + + if (! $tbody.hasClass('open')) { + jQuery('tbody', $table).removeClass('open'); + } + + $tbody.toggleClass('open'); + }); + }); + }; + + // Checkable table functionality + var uiHelperTableToolsCheckable = function() { + // For each table + jQuery('.js-table-checkable').each(function(){ + var $table = jQuery(this); + var $table_target = jQuery('.js-table-checkable-target'); + + // When a checkbox is clicked in thead + jQuery('thead input:checkbox', $table).on('click', function() { + var $checkedStatus = jQuery(this).prop('checked'); + + // Check or uncheck all checkboxes in tbody + jQuery('tbody input[name="ids[]"]:checkbox', $table_target).each(function() { + var $checkbox = jQuery(this); + + $checkbox.prop('checked', $checkedStatus); + uiHelperTableToolscheckRow($checkbox, $checkedStatus); + }); + }); + + // When a checkbox is clicked in tbody + jQuery('tbody input[name="ids[]"]:checkbox', $table_target).on('click', function() { + var $checkbox = jQuery(this); + + uiHelperTableToolscheckRow($checkbox, $checkbox.prop('checked')); + }); + }); + + jQuery('.js-table-checkable-left').each(function(){ + var $table = jQuery(this); + var $table_target = jQuery('.js-table-checkable-target-left'); + var $tr = $table_target.find('tr'); + + // When a checkbox is clicked in thead + jQuery('thead input:checkbox', $table).on('click', function() { + var $checkedStatus = jQuery(this).prop('checked'); + + // Check or uncheck all checkboxes in tbody + jQuery('tbody input[name="ids[]"]:checkbox', $table_target).each(function() { + var $checkbox = jQuery(this); + var $index = $table_target.find('input[name="ids[]"]:checkbox').index($(this)); + $checkbox.prop('checked', $checkedStatus); + uiHelperTableToolscheckRow($checkbox, $checkedStatus, $index); + }); + }); + + // When a checkbox is clicked in tbody + jQuery('tbody input[name="ids[]"]:checkbox', $table_target).on('click', function(e) { + var $checkbox = jQuery(this); + var $index = $table_target.find('input[name="ids[]"]:checkbox').index($(this)); + + uiHelperTableToolscheckRow($checkbox, $checkbox.prop('checked'), $index); + }); + }); + }; + + // Checkable table functionality helper - Checks or unchecks table row + var uiHelperTableToolscheckRow = function($checkbox, $checkedStatus, $index) { + if ($checkedStatus) { + $checkbox + .closest('tr') + .addClass('active'); + $('.js-table-checkable-target').find('tr').eq($index).addClass('active'); + $('#builder-table-right-body-inner').find('tr').eq($index).addClass('active'); + } else { + $checkbox + .closest('tr') + .removeClass('active'); + $('.js-table-checkable-target').find('tr').eq($index).removeClass('active'); + $('#builder-table-right-body-inner').find('tr').eq($index).removeClass('active'); + } + }; + + /* + * jQuery Appear, for more examples you can check out https://github.com/bas2k/jquery.appear + * + * App.initHelper('appear'); + * + */ + var uiHelperAppear = function(){ + // Add a specific class on elements (when they become visible on scrolling) + jQuery('[data-toggle="appear"]').each(function(){ + var $windowW = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; + var $this = jQuery(this); + var $class = $this.data('class') ? $this.data('class') : 'animated fadeIn'; + var $offset = $this.data('offset') ? $this.data('offset') : 0; + var $timeout = ($lHtml.hasClass('ie9') || $windowW < 992) ? 0 : ($this.data('timeout') ? $this.data('timeout') : 0); + + $this.appear(function() { + setTimeout(function(){ + $this + .removeClass('visibility-hidden') + .addClass($class); + }, $timeout); + },{accY: $offset}); + }); + }; + + /* + * jQuery Appear + jQuery countTo, for more examples you can check out https://github.com/bas2k/jquery.appear and https://github.com/mhuggins/jquery-countTo + * + * App.initHelper('appear-countTo'); + * + */ + var uiHelperAppearCountTo = function(){ + // Init counter functionality + jQuery('[data-toggle="countTo"]').each(function(){ + var $this = jQuery(this); + var $after = $this.data('after'); + var $before = $this.data('before'); + var $speed = $this.data('speed') ? $this.data('speed') : 1500; + var $interval = $this.data('interval') ? $this.data('interval') : 15; + + $this.appear(function() { + $this.countTo({ + speed: $speed, + refreshInterval: $interval, + onComplete: function() { + if($after) { + $this.html($this.html() + $after); + } else if ($before) { + $this.html($before + $this.html()); + } + } + }); + }); + }); + }; + + /* + * jQuery SlimScroll, for more examples you can check out http://rocha.la/jQuery-slimScroll + * + * App.initHelper('slimscroll'); + * + */ + var uiHelperSlimscroll = function(){ + // Init slimScroll functionality + jQuery('[data-toggle="slimscroll"]').each(function(){ + var $this = jQuery(this); + var $height = $this.data('height') ? $this.data('height') : '200px'; + var $size = $this.data('size') ? $this.data('size') : '5px'; + var $position = $this.data('position') ? $this.data('position') : 'right'; + var $color = $this.data('color') ? $this.data('color') : '#000'; + var $avisible = $this.data('always-visible') ? true : false; + var $rvisible = $this.data('rail-visible') ? true : false; + var $rcolor = $this.data('rail-color') ? $this.data('rail-color') : '#999'; + var $ropacity = $this.data('rail-opacity') ? $this.data('rail-opacity') : .3; + + $this.slimScroll({ + height: $height, + size: $size, + position: $position, + color: $color, + alwaysVisible: $avisible, + railVisible: $rvisible, + railColor: $rcolor, + railOpacity: $ropacity + }); + }); + }; + + /* + ******************************************************************************************** + * + * All the following helpers require each plugin's resources (JS, CSS) to be included in order to work + * + ******************************************************************************************** + */ + + /* + * Magnific Popup functionality, for more examples you can check out http://dimsemenov.com/plugins/magnific-popup/ + * + * App.initHelper('magnific-popup'); + * + */ + var uiHelperMagnific = function(){ + // Simple Gallery init + jQuery('.js-gallery').each(function(){ + jQuery(this).viewer({url: 'data-original'}); + }); + + // Advanced Gallery init + jQuery('.js-gallery-advanced').each(function(){ + jQuery(this).magnificPopup({ + delegate: 'a.img-lightbox', + type: 'image', + gallery: { + enabled: true + } + }); + }); + }; + + /* + * CKEditor init, for more examples you can check out http://ckeditor.com/ + * + * App.initHelper('ckeditor'); + * + */ + var uiHelperCkeditor = function(){ + // Disable auto init when contenteditable property is set to true + CKEDITOR.disableAutoInline = true; + + // Init inline text editor + jQuery('.js-ckeditor-inlin').each(function () { + var editor_id = $(this).attr('id'); + CKEDITOR.inline(editor_id); + }); + + // Init full text editor + jQuery('.js-ckeditor').each(function () { + var editor_id = $(this).attr('id'); + var editor = CKEDITOR.replace(editor_id, { + filebrowserImageUploadUrl: dolphin.ckeditor_img_upload_url, + image_previewText: ' ', + height: $(this).data('height') || 400, + width: $(this).data('width') || '100%', + toolbarCanCollapse: true + }); + editor.on('change', function( evt ) { + editor.updateElement(); + }); + }); + }; + + /* + * Summernote init, for more examples you can check out http://summernote.org/ + * + * App.initHelper('summernote'); + * + */ + var uiHelperSummernote = function(){ + // Init text editor in air mode (inline) + jQuery('.js-summernote-air').summernote({ + airMode: true + }); + + // Init full text editor + jQuery('.js-summernote').each(function () { + var $summernote = $(this); + $summernote.summernote({ + width: $summernote.data('width') || '100%', + height: $summernote.data('height') || 350, + minHeight: null, + maxHeight: null, + lang: 'zh-CN', + callbacks: { + onImageUpload: function(files) { + //上传图片到服务器 + var formData = new FormData(); + $.each(files, function () { + formData.append('file', $(this)[0]); + $.ajax({ + url : dolphin.image_upload_url,//后台文件上传接口 + type : 'POST', + data : formData, + cache: false, + processData : false, + contentType : false, + success : function(res) { + if (res.code) { + $summernote.summernote('insertImage', res.path); + } else { + Dolphin.notify(res.info, 'danger'); + } + } + }); + }); + } + } + }); + }); + }; + + /* + * Slick init, for more examples you can check out http://kenwheeler.github.io/slick/ + * + * App.initHelper('slick'); + * + */ + var uiHelperSlick = function(){ + // Get each slider element (with .js-slider class) + jQuery('.js-slider').each(function(){ + var $slider = jQuery(this); + + // Get each slider's init data + var $sliderArrows = $slider.data('slider-arrows') ? $slider.data('slider-arrows') : false; + var $sliderDots = $slider.data('slider-dots') ? $slider.data('slider-dots') : false; + var $sliderNum = $slider.data('slider-num') ? $slider.data('slider-num') : 1; + var $sliderAuto = $slider.data('slider-autoplay') ? $slider.data('slider-autoplay') : false; + var $sliderAutoSpeed = $slider.data('slider-autoplay-speed') ? $slider.data('slider-autoplay-speed') : 3000; + + // Init slick slider + $slider.slick({ + arrows: $sliderArrows, + dots: $sliderDots, + slidesToShow: $sliderNum, + autoplay: $sliderAuto, + autoplaySpeed: $sliderAutoSpeed + }); + }); + }; + + /* + * Bootstrap Datepicker init, for more examples you can check out https://github.com/eternicode/bootstrap-datepicker + * + * App.initHelper('datepicker'); + * + */ + var uiHelperDatepicker = function(){ + // Init datepicker (with .js-datepicker and .input-daterange class) + jQuery('.js-datepicker').add('.input-daterange').each(function () { + var $datepicker = jQuery(this); + var $weekStart = $datepicker.data('week-start') || 1; + var $autoclose = $datepicker.data('autoclose') === undefined ? true : $datepicker.data('autoclose'); + var $todayHighlight = $datepicker.data('today-highlight') === undefined ? true : $datepicker.data('today-highlight'); + var $language = $datepicker.data('language') || 'zh-CN'; + var $startDate = $datepicker.data('start-date') === undefined ? -Infinity : $datepicker.data('start-date'); + var $endDate = $datepicker.data('end-date') === undefined ? Infinity : $datepicker.data('end-date'); + var $startView = $datepicker.data('start-view') === undefined ? 0 : $datepicker.data('start-view'); + var $minViewMode = $datepicker.data('min-view-mode') === undefined ? 0 : $datepicker.data('min-view-mode'); + var $maxViewMode = $datepicker.data('max-view-mode') === undefined ? 4 : $datepicker.data('max-view-mode'); + var $todayBtn = $datepicker.data('today-btn') === undefined ? false : $datepicker.data('today-btn'); + var $clearBtn = $datepicker.data('clear-btn') === undefined ? false : $datepicker.data('clear-btn'); + var $orientation = $datepicker.data('orientation') === undefined ? "auto" : $datepicker.data('orientation'); + var $multidate = $datepicker.data('multidate') === undefined ? false : $datepicker.data('multidate'); + var $multidateSeparator = $datepicker.data('multidate-separator') === undefined ? ',' : $datepicker.data('multidate-separator'); + var $daysOfWeekDisabled = $datepicker.data('days-of-week-disabled') === undefined ? [] : $datepicker.data('days-of-week-disabled'); + var $daysOfWeekHighlighted = $datepicker.data('days-of-week-highlighted') === undefined ? [] : $datepicker.data('days-of-week-highlighted'); + var $calendarWeeks = $datepicker.data('calendar-weeks') === undefined ? false : $datepicker.data('calendar-weeks'); + var $keyboardNavigation = $datepicker.data('keyboard-navigation') === undefined ? true : $datepicker.data('keyboard-navigation'); + var $forceParse = $datepicker.data('force-parse') === undefined ? true : $datepicker.data('force-parse'); + var $datesDisabled = $datepicker.data('dates-disabled') === undefined ? [] : $datepicker.data('dates-disabled'); + var $toggleActive = $datepicker.data('toggle-active') === undefined ? false : $datepicker.data('toggle-active'); + + $datepicker.datepicker({ + weekStart: $weekStart, + autoclose: $autoclose, + todayHighlight: $todayHighlight, + language: $language, + startDate: $startDate, + endDate: $endDate, + startView: $startView, + minViewMode: $minViewMode, + maxViewMode: $maxViewMode, + todayBtn: $todayBtn, + clearBtn: $clearBtn, + orientation: $orientation, + multidate: $multidate, + multidateSeparator: $multidateSeparator, + daysOfWeekDisabled: $daysOfWeekDisabled, + daysOfWeekHighlighted: $daysOfWeekHighlighted, + calendarWeeks: $calendarWeeks, + keyboardNavigation: $keyboardNavigation, + forceParse: $forceParse, + datesDisabled: $datesDisabled, + toggleActive: $toggleActive + }); + }); + }; + + /* + * 日期时间范围 + * + * App.initHelper('daterangepicker'); + * + */ + var uiHelperDaterangepicker = function () { + jQuery('.js-daterangepicker').each(function () { + var $daterangepicker = jQuery(this); + var $format = $daterangepicker.data('format') === undefined ? "YYYY-MM-DD" : $daterangepicker.data('format'); + var $autoUpdateInput = $daterangepicker.data('auto-update-input') === undefined ? false : $daterangepicker.data('auto-update-input'); + var $showDropdowns = $daterangepicker.data('show-dropdowns') === undefined ? true : $daterangepicker.data('show-dropdowns'); + var $singleDatePicker = $daterangepicker.data('single-date-picker') === undefined ? false : $daterangepicker.data('single-date-picker'); + + $daterangepicker.daterangepicker({ + autoUpdateInput: $autoUpdateInput, + showDropdowns: $showDropdowns, + locale: { + "format": $format, + "separator": " - ", + "applyLabel": "确定", + "cancelLabel": "取消", + "fromLabel": "From", + "toLabel": "To", + "customRangeLabel": "Custom", + "weekLabel": "W", + "daysOfWeek": ["日", "一", "二", "三", "四", "五", "六"], + "monthNames": ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"] + } + }); + + if (!$singleDatePicker) { + $daterangepicker.on('apply.daterangepicker', function(ev, picker) { + $(this).val(picker.startDate.format($format) + ' - ' + picker.endDate.format($format)); + }); + } else { + $daterangepicker.on('apply.daterangepicker', function(ev, picker) { + $(this).val(picker.startDate.format($format)); + }); + } + + $daterangepicker.on('cancel.daterangepicker', function(ev, picker) { + $(this).val(''); + }); + }); + }; + + /* + * Bootstrap Colorpicker init, for more examples you can check out http://mjolnic.com/bootstrap-colorpicker/ + * + * App.initHelper('colorpicker'); + * + */ + var uiHelperColorpicker = function(){ + // Get each colorpicker element (with .js-colorpicker class) + jQuery('.js-colorpicker').each(function(){ + var $colorpicker = jQuery(this); + + // Get each colorpicker's init data + var $colorpickerMode = $colorpicker.data('colorpicker-mode') ? $colorpicker.data('colorpicker-mode') : 'hex'; + var $colorpickerinline = $colorpicker.data('colorpicker-inline') ? true : false; + + // Init colorpicker + $colorpicker.colorpicker({ + 'format': $colorpickerMode, + 'inline': $colorpickerinline + }); + }); + }; + + /* + * Masked Inputs, for more examples you can check out http://digitalbush.com/projects/masked-input-plugin/ + * + * App.initHelper('masked-inputs'); + * + */ + var uiHelperMaskedInputs = function(){ + // Init Masked Inputs + // a - Represents an alpha character (A-Z,a-z) + // 9 - Represents a numeric character (0-9) + // * - Represents an alphanumeric character (A-Z,a-z,0-9) + jQuery('.js-masked-date').mask('99/99/9999'); + jQuery('.js-masked-date-dash').mask('99-99-9999'); + jQuery('.js-masked-phone').mask('(999) 999-9999'); + jQuery('.js-masked-phone-ext').mask('(999) 999-9999? x99999'); + jQuery('.js-masked-taxid').mask('99-9999999'); + jQuery('.js-masked-ssn').mask('999-99-9999'); + jQuery('.js-masked-pkey').mask('a*-999-a999'); + jQuery('.js-masked-time').mask('99:99'); + }; + + /* + * Tags Inputs, for more examples you can check out https://github.com/xoxco/jQuery-Tags-Input + * + * App.initHelper('tags-inputs'); + * + */ + var uiHelperTagsInputs = function(){ + // Init Tags Inputs (with .js-tags-input class) + jQuery('.js-tags-input').tagsInput({ + height: 'auto', + width: '100%', + defaultText: '添加标签', + removeWithBackspace: true, + delimiter: [','] + }); + }; + + /* + * Select2, for more examples you can check out https://github.com/select2/select2 + * + * App.initHelper('select2'); + * + */ + var uiHelperSelect2 = function(){ + // Init Select2 (with .js-select2 class) + jQuery('.js-select2').each(function () { + var $select2 = jQuery(this); + var $width = $select2.data('width') || '100%'; + $select2.select2({ + width: $width, //设置下拉框的宽度 + language: "zh-CN" + }); + }); + }; + + /* + * Highlight.js, for more examples you can check out https://highlightjs.org/usage/ + * + * App.initHelper('highlightjs'); + * + */ + var uiHelperHighlightjs = function(){ + // Init Highlight.js + hljs.initHighlightingOnLoad(); + }; + + /* + * Bootstrap Notify, for more examples you can check out http://bootstrap-growl.remabledesigns.com/ + * + * App.initHelper('notify'); + * + */ + var uiHelperNotify = function(){ + // Init notifications (with .js-notify class) + jQuery('.js-notify').on('click', function(){ + var $notify = jQuery(this); + var $notifyMsg = $notify.data('notify-message'); + var $notifyType = $notify.data('notify-type') ? $notify.data('notify-type') : 'info'; + var $notifyFrom = $notify.data('notify-from') ? $notify.data('notify-from') : 'top'; + var $notifyAlign = $notify.data('notify-align') ? $notify.data('notify-align') : 'right'; + var $notifyIcon = $notify.data('notify-icon') ? $notify.data('notify-icon') : ''; + var $notifyUrl = $notify.data('notify-url') ? $notify.data('notify-url') : ''; + + jQuery.notify({ + icon: $notifyIcon, + message: $notifyMsg, + url: $notifyUrl + }, + { + element: 'body', + type: $notifyType, + allow_dismiss: true, + newest_on_top: true, + showProgressbar: false, + placement: { + from: $notifyFrom, + align: $notifyAlign + }, + offset: 20, + spacing: 10, + z_index: 1033, + delay: 5000, + timer: 1000, + animate: { + enter: 'animated fadeIn', + exit: 'animated fadeOutDown' + } + }); + }); + }; + + /* + * Draggable items with jQuery, for more examples you can check out https://jqueryui.com/sortable/ + * + * App.initHelper('draggable-items'); + * + */ + var uiHelperDraggableItems = function(){ + // Init draggable items functionality (with .js-draggable-items class) + jQuery('.js-draggable-items > .draggable-column').sortable({ + connectWith: '.draggable-column', + items: '.draggable-item', + dropOnEmpty: true, + opacity: .75, + handle: '.draggable-handler', + placeholder: 'draggable-placeholder', + tolerance: 'pointer', + start: function(e, ui){ + ui.placeholder.css({ + 'height': ui.item.outerHeight(), + 'margin-bottom': ui.item.css('margin-bottom') + }); + } + }); + }; + + /* + * Easy Pie Chart, for more examples you can check out http://rendro.github.io/easy-pie-chart/ + * + * App.initHelper('easy-pie-chart'); + * + */ + var uiHelperEasyPieChart = function(){ + // Init Easy Pie Charts (with .js-pie-chart class) + jQuery('.js-pie-chart').easyPieChart({ + barColor: jQuery(this).data('bar-color') ? jQuery(this).data('bar-color') : '#777777', + trackColor: jQuery(this).data('track-color') ? jQuery(this).data('track-color') : '#eeeeee', + lineWidth: jQuery(this).data('line-width') ? jQuery(this).data('line-width') : 3, + size: jQuery(this).data('size') ? jQuery(this).data('size') : '80', + animate: 750, + scaleColor: jQuery(this).data('scale-color') ? jQuery(this).data('scale-color') : false + }); + }; + + /* + * Bootstrap Maxlength, for more examples you can check out https://github.com/mimo84/bootstrap-maxlength + * + * App.initHelper('maxlength'); + * + */ + var uiHelperMaxlength = function(){ + // Init Bootstrap Maxlength (with .js-maxlength class) + jQuery('.js-maxlength').each(function(){ + var $input = jQuery(this); + + $input.maxlength({ + alwaysShow: $input.data('always-show') ? true : false, + threshold: $input.data('threshold') ? $input.data('threshold') : 10, + warningClass: $input.data('warning-class') ? $input.data('warning-class') : 'label label-warning', + limitReachedClass: $input.data('limit-reached-class') ? $input.data('limit-reached-class') : 'label label-danger', + placement: $input.data('placement') ? $input.data('placement') : 'bottom', + preText: $input.data('pre-text') ? $input.data('pre-text') : '', + separator: $input.data('separator') ? $input.data('separator') : '/', + postText: $input.data('post-text') ? $input.data('post-text') : '' + }); + }); + }; + + /* + * Bootstrap Datetimepicker, for more examples you can check out https://github.com/Eonasdan/bootstrap-datetimepicker + * + * App.initHelper('datetimepicker'); + * + */ + var uiHelperDatetimepicker = function(){ + // Init Bootstrap Datetimepicker (with .js-datetimepicker class) + jQuery('.js-datetimepicker').each(function(){ + var $input = jQuery(this); + + $input.datetimepicker({ + format: $input.data('format') ? $input.data('format') : false, + useCurrent: $input.data('use-current') ? $input.data('use-current') : false, + locale: moment.locale('' + ($input.data('locale') ? $input.data('locale') : '') +''), + showTodayButton: $input.data('show-today-button') ? $input.data('show-today-button') : false, + showClear: $input.data('show-clear') ? $input.data('show-clear') : false, + showClose: $input.data('show-close') ? $input.data('show-close') : false, + sideBySide: $input.data('side-by-side') ? $input.data('side-by-side') : false, + inline: $input.data('inline') ? $input.data('inline') : false, + icons: { + time: 'si si-clock', + date: 'si si-calendar', + up: 'si si-arrow-up', + down: 'si si-arrow-down', + previous: 'si si-arrow-left', + next: 'si si-arrow-right', + today: 'si si-size-actual', + clear: 'si si-trash', + close: 'si si-close' + } + }); + }); + }; + + /* + * Ion Range Slider, for more examples you can check out https://github.com/IonDen/ion.rangeSlider + * + * App.initHelper('rangeslider'); + * + */ + var uiHelperRangeslider = function(){ + // Init Ion Range Slider (with .js-rangeslider class) + jQuery('.js-rangeslider').each(function(){ + var $input = jQuery(this); + + $input.ionRangeSlider({ + input_values_separator: ';' + }); + }); + }; + + return { + init: function($func) { + switch ($func) { + case 'uiInit': + uiInit(); + break; + case 'uiLayout': + uiLayout(); + break; + case 'uiNav': + uiNav(); + break; + case 'uiBlocks': + uiBlocks(); + break; + case 'uiForms': + uiForms(); + break; + case 'uiHandleTheme': + uiHandleTheme(); + break; + case 'uiToggleClass': + uiToggleClass(); + break; + case 'uiScrollTo': + uiScrollTo(); + break; + case 'uiYearCopy': + uiYearCopy(); + break; + case 'uiLoader': + uiLoader('hide'); + break; + default: + // Init all vital functions + uiInit(); + uiLayout(); + uiNav(); + uiBlocks(); + uiForms(); + uiHandleTheme(); + uiToggleClass(); + uiScrollTo(); + uiYearCopy(); + uiLoader('hide'); + } + }, + layout: function($mode) { + uiLayoutApi($mode); + }, + loader: function($mode) { + uiLoader($mode); + }, + blocks: function($block, $mode) { + uiBlocksApi($block, $mode); + }, + initHelper: function($helper) { + switch ($helper) { + case 'print-page': + uiHelperPrint(); + break; + case 'table-tools': + uiHelperTableToolsSections(); + uiHelperTableToolsCheckable(); + break; + case 'appear': + uiHelperAppear(); + break; + case 'appear-countTo': + uiHelperAppearCountTo(); + break; + case 'slimscroll': + uiHelperSlimscroll(); + break; + case 'magnific-popup': + uiHelperMagnific(); + break; + case 'ckeditor': + uiHelperCkeditor(); + break; + case 'summernote': + uiHelperSummernote(); + break; + case 'slick': + uiHelperSlick(); + break; + case 'datepicker': + uiHelperDatepicker(); + break; + case 'daterangepicker': + uiHelperDaterangepicker(); + break; + case 'colorpicker': + uiHelperColorpicker(); + break; + case 'tags-inputs': + uiHelperTagsInputs(); + break; + case 'masked-inputs': + uiHelperMaskedInputs(); + break; + case 'select2': + uiHelperSelect2(); + break; + case 'highlightjs': + uiHelperHighlightjs(); + break; + case 'notify': + uiHelperNotify(); + break; + case 'draggable-items': + uiHelperDraggableItems(); + break; + case 'easy-pie-chart': + uiHelperEasyPieChart(); + break; + case 'maxlength': + uiHelperMaxlength(); + break; + case 'datetimepicker': + uiHelperDatetimepicker(); + break; + case 'rangeslider': + uiHelperRangeslider(); + break; + default: + return false; + } + }, + initHelpers: function($helpers) { + if ($helpers instanceof Array) { + for(var $index in $helpers) { + App.initHelper($helpers[$index]); + } + } else { + App.initHelper($helpers); + } + } + }; +}(); + +// Create an alias for App (you can use OneUI in your pages instead of App if you like) +var OneUI = App; + +// Initialize app when page loads +jQuery(function(){ + if (typeof angular == 'undefined') { + App.init(); + } +}); diff --git a/public/static/admin/js/builder/aside.js b/public/static/admin/js/builder/aside.js new file mode 100644 index 0000000..b9d4b00 --- /dev/null +++ b/public/static/admin/js/builder/aside.js @@ -0,0 +1,34 @@ +/*! + * Document : aside.js + * Author : caiweiming <314013107@qq.com> + * Description: 侧栏构建器 + */ +jQuery(document).ready(function() { + // 侧栏开关 + $('#aside .switch input:checkbox').on('click', function () { + var $switch = $(this); + var $data = { + value: $switch.prop('checked'), + _t: $switch.data('table') || '', + name: $switch.data('field') || '', + type: 'switch', + pk: $switch.data('id') || '' + }; + + // 发送ajax请求 + Dolphin.loading(); + $.post(dolphin.aside_edit_url, $data).success(function(res) { + Dolphin.loading('hide'); + if (res.code) { + Dolphin.notify(res.msg, 'success'); + } else { + Dolphin.notify(res.msg, 'danger'); + $switch.prop('checked', !$data.status); + return false; + } + }).fail(function (res) { + Dolphin.loading('hide'); + Dolphin.notify($(res.responseText).find('h1').text() || '服务器内部错误~', 'danger'); + }); + }); +}); \ No newline at end of file diff --git a/public/static/admin/js/builder/form.js b/public/static/admin/js/builder/form.js new file mode 100644 index 0000000..3bfac9e --- /dev/null +++ b/public/static/admin/js/builder/form.js @@ -0,0 +1,1097 @@ +/*! + * Document : form.js + * Author : caiweiming <314013107@qq.com> + * Description: 表单构建器 + */ +jQuery(document).ready(function() { + // 文件上传集合 + var webuploader = []; + // 当前上传对象 + var curr_uploader = {}; + // editordm编辑器集合 + var editormds = {}; + // ueditor编辑器集合 + var ueditors = {}; + // wangeditor编辑器集合 + var wangeditors = {}; + // 当前图标选择器 + var curr_icon_picker; + var layer_icon; + + // 打开图标选择器 + $('.js-icon-picker').click(function(){ + curr_icon_picker = $(this); + var icon_input = curr_icon_picker.find('.icon_input'); + if (icon_input.is(':disabled')) { + return; + } + layer_icon = layer.open({ + type: 1, + title: '图标选择器', + area: ['90%', '90%'], + scrollbar: false, + content: $('#icon_tab') + }); + }); + + // 开启图标搜索 + Dolphin.iconSearch(); + + // 选择图标 + $('.js-icon-content li').click(function () { + var icon = $(this).find('i').attr('class'); + curr_icon_picker.find('.input-group-addon.icon').html(''); + curr_icon_picker.find('.icon_input').val(icon); + layer.close(layer_icon); + }); + + // 清空图标 + $('.delete-icon').click(function(event){ + event.stopPropagation(); + if ($(this).prev().is(':disabled')) { + return; + } + $(this).prev().val(''); + $(this).prev().prev().html(''); + }); + + // 百度地图 + $('.js-bmap').each(function () { + var $self = $(this); + var map_canvas = $self.find('.bmap').attr('id'); + var address = $self.find('.bmap-address'); + var address_id = address.attr('id'); + var map_point = $self.find('.bmap-point'); + var search_result = $self.find('.searchResultPanel'); + var point_lng = 116.331398; + var point_lat = 39.897445; + var map_level = $self.data('level'); + + // 百度地图API功能 + var map = new BMap.Map(map_canvas); + //开启鼠标滚轮缩放 + map.enableScrollWheelZoom(true); + // 左上角,添加比例尺 + var top_left_control = new BMap.ScaleControl({anchor: BMAP_ANCHOR_TOP_LEFT}); + // 左上角,添加默认缩放平移控件 + var top_left_navigation = new BMap.NavigationControl(); + map.addControl(top_left_control); + map.addControl(top_left_navigation); + + // 智能搜索 + var local = new BMap.LocalSearch(map, { + onSearchComplete: function () { + var point = local.getResults().getPoi(0).point; //获取第一个智能搜索的结果 + map.centerAndZoom(point, map_level); + // 创建标注 + create_mark(point); + } + }); + + // 创建标注 + var create_mark = function (point) { + // 清空所有标注 + map.clearOverlays(); + var marker = new BMap.Marker(point); // 创建标注 + map.addOverlay(marker); //添加标注 + marker.setAnimation(BMAP_ANIMATION_BOUNCE); //跳动的动画 + // 写入坐标 + map_point.val(point.lng + "," + point.lat); + }; + + // 建立一个自动完成的对象 + var ac = new BMap.Autocomplete({ + "input" : address_id, + "location" : map + }); + // 鼠标放在下拉列表上的事件 + ac.addEventListener("onhighlight", function(e) { + var str = ""; + var _value = e.fromitem.value; + var value = ""; + if (e.fromitem.index > -1) { + value = _value.province + _value.city + _value.district + _value.street + _value.business; + } + str = "FromItem
    index = " + e.fromitem.index + "
    value = " + value; + + value = ""; + if (e.toitem.index > -1) { + _value = e.toitem.value; + value = _value.province + _value.city + _value.district + _value.street + _value.business; + } + str += "
    ToItem
    index = " + e.toitem.index + "
    value = " + value; + search_result.html(str); + }); + + + // 鼠标点击下拉列表后的事件 + var myValue; + ac.addEventListener("onconfirm", function(e) { + var _value = e.item.value; + myValue = _value.province + _value.city + _value.district + _value.street + _value.business; + search_result.html("onconfirm
    index = " + e.item.index + "
    myValue = " + myValue); + + local.search(myValue); + }); + + // 监听点击地图时间 + map.addEventListener("click", function (e) { + // 创建标注 + create_mark(e.point); + }); + + if (map_point.val() != '') { + var curr_point = map_point.val().split(','); + point_lng = curr_point[0]; + point_lat = curr_point[1]; + } else if(address.val() != '') { + local.search(address.val()); + } else { + // 根据ip获取当前城市,并定位到当前城市 + var myCity = new BMap.LocalCity(); + myCity.get(function (result) { + var cityName = result.name; + map.setCenter(cityName); + }); + } + + // 初始化地图,设置中心点坐标和地图级别 + var point = new BMap.Point(point_lng, point_lat); + map.centerAndZoom(point, map_level); + if (map_point.val() != '') { + // 创建标注 + create_mark(point); + } + if(address.val()!=''){ + ac.setInputValue(address.val()) + } + }); + + // 图片裁剪 + $('.js-jcrop-interface').each(function () { + var jcrop_api = ''; + var $self = $(this); + var $jcrop = $self.find('.js-jcrop'); + var $options = $jcrop.data('options') || {}; + var $thumb = $jcrop.data('thumb'); + var $watermark = $jcrop.data('watermark'); + var $jcrop_cut_btn = $self.find('.js-jcrop-cut-btn'); + var $jcrop_upload_btn = $self.find('.js-jcrop-upload-btn'); + var $jcrop_file = $self.find('.js-jcrop-file'); + var $jcrop_cut_info = $self.find('.js-jcrop-cut-info'); + var $jcrop_preview = $self.find('.jcrop-preview'); + var $jcrop_input = $self.find('.js-jcrop-input'); + var $remove_picture = $self.find('.remove-picture'); + var $thumbnail = $self.find('.thumbnail'); + var $modal = $self.find('.modal-popin'); + var $pic_height = ''; + + // 设置预览图监听 + $options.onChange = showPreview; + $options.onSelect = showPreview; + $options.boxWidth = 750; + $options.boxHeight = 750; + $options.saveWidth = $options.saveWidth || null; + $options.saveHeight = $options.saveHeight || null; + $options.aspectRatio = $options.aspectRatio || ($options.saveWidth / $options.saveHeight); + + // 点击上传按钮,选择图片 + $jcrop_upload_btn.click(function () { + $jcrop_file.trigger('click'); + }); + + // 加载图片(用于判断图片是否加载完毕) + function loadImage(url, callback) { + var img = new Image(); //创建一个Image对象,实现图片的预下载 + img.src = url; + + if(img.complete) { // 如果图片已经存在于浏览器缓存,直接调用回调函数 + callback.call(img); + return; // 直接返回,不用再处理onload事件 + } + img.onload = function () { //图片下载完毕时异步调用callback函数。 + callback.call(img);//将回调函数的this替换为Image对象 + }; + } + + // 实时显示预览图 + function showPreview(coords) + { + var ratio = coords.w / coords.h; // 选区比例 + var rx,ry; + var preview_width = ''; + var preview_height = ''; + + if ((100 / ratio) > $pic_height) { + preview_width = $pic_height * ratio; + preview_height = $pic_height; + } else { + preview_width = 100; + preview_height = 100 / ratio; + } + + rx = preview_width / coords.w; + ry = (preview_width / ratio) / coords.h; + + if (jcrop_api) { + $jcrop_preview.css({ + width: Math.round(rx * jcrop_api.ui.stage.width) + 'px', + height: Math.round(ry * jcrop_api.ui.stage.height) + 'px', + marginLeft: '-' + Math.round(rx * coords.x) + 'px', + marginTop: '-' + Math.round(ry * coords.y) + 'px' + }).parent().css({ + width: preview_width + 'px', + height: preview_height + 'px' + }); + } + + var jcrop_info = [coords.w, coords.h, coords.x, coords.y, $options.saveWidth, $options.saveHeight]; + $jcrop_cut_info.val(jcrop_info.join(',')); + } + + // 选择图片后 + $jcrop_file.change(function () { + var files = this.files; + var file; + if (files && files.length) { + file = files[0]; + if (/^image\/\w+$/.test(file.type)) { + // 创建FormData对象 + var data = new FormData(); + // 为FormData对象添加数据 + data.append('file', file); + Dolphin.loading(); + // 上传图片 + $.ajax({ + url: dolphin.jcrop_upload_url, + type: 'POST', + cache: false, + contentType: false, //不可缺 + processData: false, //不可缺 + data: data, + success: function (res) { + if (res.code == 1) { + $jcrop.attr('src', res.src).data('id', res.id).show(); + $jcrop_preview.attr('src', res.src).parent().show(); + loadImage(res.src, function () { + Dolphin.loading('hide'); + if (jcrop_api != '') { + jcrop_api.destroy(); + $.Jcrop.component.DragState.prototype.touch = null; + } + $jcrop.Jcrop($options, function () { + jcrop_api = this; + $pic_height = Math.round(jcrop_api.getContainerSize()[1]); + $modal.modal('show'); + }); + }); + } else { + Dolphin.loading('hide'); + Dolphin.notify(res.msg||'上传失败,请重新上传', 'warning'); + } + } + }).fail(function(res) { + Dolphin.loading('hide'); + Dolphin.notify($(res.responseText).find('h1').text() || '服务器内部错误~', 'danger'); + }); + $jcrop_file.val(''); + } else { + Dolphin.notify('请选择一张图片', 'warning'); + } + } + }); + + // 关闭裁剪框 + $modal.on('hidden.bs.modal', function (e) { + $jcrop_cut_info.val(''); + }); + + // 删除图片 + $remove_picture.click(function () { + $(this).parent().hide(); + $jcrop_input.val(''); + }); + + // 裁剪图片 + $jcrop_cut_btn.click(function () { + var $cut_value = $jcrop_cut_info.val(); + if ($jcrop.attr('src') == '') { + Dolphin.notify('请上传图片', 'danger'); + return false; + } + if ($cut_value != '') { + var $data = { + path: $jcrop_preview.attr('src'), + cut: $cut_value, + thumb: $thumb, + watermark: $watermark + }; + Dolphin.loading(); + $.ajax({ + url: dolphin.jcrop_upload_url, + type: 'POST', + dataType: 'json', + data: $data + }) + .done(function(res) { + Dolphin.loading('hide'); + if (res.code == '1') { + $thumbnail.show().find('img').attr('src', res.thumb || res.src).attr('data-original', res.src); + $jcrop_input.val(res.id); + $jcrop_cut_info.val(''); + $modal.modal('hide'); + } else { + Dolphin.notify(res.msg, 'danger'); + } + }) + .fail(function(res) { + Dolphin.loading('hide'); + Dolphin.notify($(res.responseText).find('h1').text() || '请求失败~', 'danger'); + }); + } else { + Dolphin.notify('请选择要裁剪的大小', 'warning'); + } + }); + + // 查看大图 + Dolphin.viewer(); + }); + + // editormd编辑器 + $('.js-editormd').each(function () { + var editormd_name = $(this).attr('name'); + var image_formats = $(this).data('image-formats') || []; + var watch = $(this).data('watch'); + + editormds[editormd_name] = editormd(editormd_name, { + height: 500, // 高度 + placeholder: '海豚PHP,为提升开发效率而生!!', + watch : watch, + searchReplace : true, + toolbarAutoFixed: false, // 取消工具栏固定 + path : dolphin.editormd_mudule_path, // 用于自动加载其他模块 + codeFold: true, // 开启代码折叠 + dialogLockScreen : false, // 设置弹出层对话框不锁屏 + imageUpload : true, // 开启图片上传 + imageFormats : image_formats, // 允许上传的图片后缀 + imageUploadURL : dolphin.editormd_upload_url, + toolbarIcons : function() { + return [ + "undo", "redo", "|", + "bold", "del", "italic", "quote", "|", + "h1", "h2", "h3", "h4", "h5", "h6", "|", + "list-ul", "list-ol", "hr", "|", + "link", "reference-link", "image", "code", "preformatted-text", "code-block", "datetime", "html-entities", "pagebreak", "|", + "goto-line", "watch", "preview", "fullscreen", "clear", "search", "|", + "help", "info" + ] + } + }); + }); + + // ueditor编辑器 + $('.js-ueditor').each(function () { + var ueditor_name = $(this).attr('name'); + ueditors[ueditor_name] = UE.getEditor(ueditor_name, { + initialFrameHeight:400, //初始化编辑器高度,默认320 + autoHeightEnabled:false, //是否自动长高 + maximumWords: 50000, //允许的最大字符数 + serverUrl: dolphin.ueditor_upload_url + }); + }); + + // wangeditor编辑器 + $('.js-wangeditor').each(function () { + var wangeditor_name = $(this).attr('name'); + var imgExt = $(this).data('img-ext') || ''; + + // 关闭调试信息 + wangEditor.config.printLog = false; + // 实例化编辑器 + wangeditors[wangeditor_name] = new wangEditor(wangeditor_name); + // 上传图片地址 + wangeditors[wangeditor_name].config.uploadImgUrl = dolphin.wangeditor_upload_url; + // 允许上传图片后缀 + wangeditors[wangeditor_name].config.imgExt = imgExt; + // 配置文件名 + wangeditors[wangeditor_name].config.uploadImgFileName = 'file'; + // 去掉地图 + wangeditors[wangeditor_name].config.menus = $.map(wangEditor.config.menus, function(item, key) { + if (item === 'location') { + return null; + } + return item; + }); + // 添加表情 + wangeditors[wangeditor_name].config.emotions = { + 'default': { + title: '默认', + data: dolphin.wangeditor_emotions + } + }; + wangeditors[wangeditor_name].create(); + }); + + // 注册WebUploader事件,实现秒传 + if (window.WebUploader) { + WebUploader.Uploader.register({ + "before-send-file": "beforeSendFile" // 整个文件上传前 + }, { + beforeSendFile:function(file){ + var $li = $( '#'+file.id ); + var deferred = WebUploader.Deferred(); + var owner = this.owner; + + owner.md5File(file).then(function(val){ + $.ajax({ + type: "POST", + url: dolphin.upload_check_url, + data: { + md5: val + }, + cache: false, + timeout: 10000, // 超时的话,只能认为该文件不曾上传过 + dataType: "json" + }).then(function(res, textStatus, jqXHR){ + if(res.code){ + // 已上传,触发上传完成事件,实现秒传 + deferred.reject(); + curr_uploader.trigger('uploadSuccess', file, res); + curr_uploader.trigger('uploadComplete', file); + }else{ + // 文件不存在,触发上传 + deferred.resolve(); + $li.find('.file-state').html('正在上传...'); + $li.find('.img-state').html('
    正在上传...
    '); + $li.find('.progress').show(); + } + }, function(jqXHR, textStatus, errorThrown){ + // 任何形式的验证失败,都触发重新上传 + deferred.resolve(); + $li.find('.file-state').html('正在上传...'); + $li.find('.img-state').html('
    正在上传...
    '); + $li.find('.progress').show(); + }); + }); + return deferred.promise(); + } + }); + } + + // 文件上传 + $('.js-upload-file,.js-upload-files').each(function () { + var $input_file = $(this).find('input'); + var $input_file_name = $input_file.attr('name'); + // 是否多文件上传 + var $multiple = $input_file.data('multiple'); + // 允许上传的后缀 + var $ext = $input_file.data('ext'); + // 文件限制大小 + var $size = $input_file.data('size'); + // 文件列表 + var $file_list = $('#file_list_' + $input_file_name); + + // 实例化上传 + var uploader = WebUploader.create({ + // 选完文件后,是否自动上传。 + auto: true, + // 去重 + duplicate: true, + // swf文件路径 + swf: dolphin.WebUploader_swf, + // 文件接收服务端。 + server: dolphin.file_upload_url, + // 选择文件的按钮。可选。 + // 内部根据当前运行是创建,可能是input元素,也可能是flash. + pick: { + id: '#picker_' + $input_file_name, + multiple: $multiple + }, + // 文件限制大小 + fileSingleSizeLimit: $size, + // 只允许选择文件文件。 + accept: { + title: 'Files', + extensions: $ext + } + }); + + // 当有文件添加进来的时候 + uploader.on( 'fileQueued', function( file ) { + var $li = '
  • ' + + ' 正在读取文件信息...' + + ' ' + + file.name + + ' [下载] [删除]' + + ''+ + '
  • '; + + if ($multiple) { + $file_list.append($li); + } else { + $file_list.html($li); + // 清空原来的数据 + $input_file.val(''); + } + + // 设置当前上传对象 + curr_uploader = uploader; + }); + + // 文件上传过程中创建进度条实时显示。 + uploader.on( 'uploadProgress', function( file, percentage ) { + var $percent = $( '#'+file.id ).find('.progress-bar'); + $percent.css( 'width', percentage * 100 + '%' ); + }); + + // 文件上传成功 + uploader.on( 'uploadSuccess', function( file, response ) { + var $li = $( '#'+file.id ); + if (response.code) { + if ($multiple) { + if ($input_file.val()) { + $input_file.val($input_file.val() + ',' + response.id); + } else { + $input_file.val(response.id); + } + $li.find('.remove-file').attr('data-id', response.id); + } else { + $input_file.val(response.id); + } + } + // 加入提示信息 + $li.find('.file-state').html(''+ response.info +''); + // 添加下载链接 + $li.find('.download-file').attr('href', response.path); + + // 文件上传成功后的自定义回调函数 + if (window['dp_file_upload_success'] !== undefined) window['dp_file_upload_success'](); + // 文件上传成功后的自定义回调函数 + if (window['dp_file_upload_success_'+$input_file_name] !== undefined) window['dp_file_upload_success_'+$input_file_name](); + }); + + // 文件上传失败,显示上传出错。 + uploader.on( 'uploadError', function( file ) { + var $li = $( '#'+file.id ); + $li.find('.file-state').html('服务器发生错误~'); + + // 文件上传出错后的自定义回调函数 + if (window['dp_file_upload_error'] !== undefined) window['dp_file_upload_error'](); + // 文件上传出错后的自定义回调函数 + if (window['dp_file_upload_error_'+$input_file_name] !== undefined) window['dp_file_upload_error_'+$input_file_name](); + }); + + // 文件验证不通过 + uploader.on('error', function (type) { + switch (type) { + case 'Q_TYPE_DENIED': + Dolphin.notify('文件类型不正确,只允许上传后缀名为:'+$ext+',请重新上传!', 'danger'); + break; + case 'F_EXCEED_SIZE': + Dolphin.notify('文件不得超过'+ ($size/1024) +'kb,请重新上传!', 'danger'); + break; + } + }); + + // 完成上传完了,成功或者失败,先删除进度条。 + uploader.on( 'uploadComplete', function( file ) { + setTimeout(function(){ + $('#'+file.id).find('.progress').remove(); + }, 500); + + // 文件上传完成后的自定义回调函数 + if (window['dp_file_upload_complete'] !== undefined) window['dp_file_upload_complete'](); + // 文件上传完成后的自定义回调函数 + if (window['dp_file_upload_complete_'+$input_file_name] !== undefined) window['dp_file_upload_complete_'+$input_file_name](); + }); + + // 删除文件 + $file_list.delegate('.remove-file', 'click', function(){ + if ($multiple) { + var id = $(this).data('id'), + ids = $input_file.val().split(','); + + if (id) { + for (var i = 0; i < ids.length; i++) { + if (ids[i] == id) { + ids.splice(i, 1); + break; + } + } + $input_file.val(ids.join(',')); + } + } else { + $input_file.val(''); + } + $(this).closest('.file-item').remove(); + }); + + // 将上传实例存起来 + webuploader.push(uploader); + }); + + // 图片上传 + $('.js-upload-image,.js-upload-images').each(function () { + var $input_file = $(this).find('input'); + var $input_file_name = $input_file.attr('name'); + // 是否多图片上传 + var $multiple = $input_file.data('multiple'); + // 允许上传的后缀 + var $ext = $input_file.data('ext'); + // 图片限制大小 + var $size = $input_file.data('size'); + // 缩略图参数 + var $thumb = $input_file.data('thumb'); + // 水印参数 + var $watermark = $input_file.data('watermark'); + // 图片列表 + var $file_list = $('#file_list_' + $input_file_name); + // 优化retina, 在retina下这个值是2 + var ratio = window.devicePixelRatio || 1; + // 缩略图大小 + var thumbnailWidth = 100 * ratio; + var thumbnailHeight = 100 * ratio; + // 实例化上传 + var uploader = WebUploader.create({ + // 选完图片后,是否自动上传。 + auto: true, + // 去重 + duplicate: true, + // 不压缩图片 + resize: false, + compress: false, + // swf图片路径 + swf: dolphin.WebUploader_swf, + // 图片接收服务端。 + server: dolphin.image_upload_url, + // 选择图片的按钮。可选。 + // 内部根据当前运行是创建,可能是input元素,也可能是flash. + pick: { + id: '#picker_' + $input_file_name, + multiple: $multiple + }, + // 图片限制大小 + fileSingleSizeLimit: $size, + // 只允许选择图片文件。 + accept: { + title: 'Images', + extensions: $ext, + mimeTypes: 'image/jpg,image/jpeg,image/bmp,image/png,image/gif' + }, + // 自定义参数 + formData: { + thumb: $thumb, + watermark: $watermark + } + }); + + // 当有文件添加进来的时候 + uploader.on( 'fileQueued', function( file ) { + var $li = $( + '',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),c.isInStateTrue()?void 0:(clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide())},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.6",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.6",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.6",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); diff --git a/public/static/admin/js/core/jquery.appear.min.js b/public/static/admin/js/core/jquery.appear.min.js new file mode 100644 index 0000000..8d20221 --- /dev/null +++ b/public/static/admin/js/core/jquery.appear.min.js @@ -0,0 +1,11 @@ +/* + * jQuery.appear + * https://github.com/bas2k/jquery.appear/ + * http://code.google.com/p/jquery-appear/ + * http://bas2k.ru/ + * + * Copyright (c) 2009 Michael Hixson + * Copyright (c) 2012-2014 Alexander Brovikov + * Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php) + */ +!function(e){e.fn.appear=function(a,r){var n=e.extend({data:void 0,one:!0,accX:0,accY:0},r);return this.each(function(){var r=e(this);if(r.appeared=!1,!a)return void r.trigger("appear",n.data);var p=e(window),t=function(){if(!r.is(":visible"))return void(r.appeared=!1);var e=p.scrollLeft(),a=p.scrollTop(),t=r.offset(),c=t.left,i=t.top,o=n.accX,f=n.accY,s=r.height(),u=p.height(),d=r.width(),l=p.width();i+s+f>=a&&a+u+f>=i&&c+d+o>=e&&e+l+o>=c?r.appeared||r.trigger("appear",n.data):r.appeared=!1},c=function(){if(r.appeared=!0,n.one){p.unbind("scroll",t);var c=e.inArray(t,e.fn.appear.checks);c>=0&&e.fn.appear.checks.splice(c,1)}a.apply(this,arguments)};n.one?r.one("appear",n.data,c):r.bind("appear",n.data,c),p.scroll(t),e.fn.appear.checks.push(t),t()})},e.extend(e.fn.appear,{checks:[],timeout:null,checkAll:function(){var a=e.fn.appear.checks.length;if(a>0)for(;a--;)e.fn.appear.checks[a]()},run:function(){e.fn.appear.timeout&&clearTimeout(e.fn.appear.timeout),e.fn.appear.timeout=setTimeout(e.fn.appear.checkAll,20)}}),e.each(["append","prepend","after","before","attr","removeAttr","addClass","removeClass","toggleClass","remove","css","show","hide"],function(a,r){var n=e.fn[r];n&&(e.fn[r]=function(){var a=n.apply(this,arguments);return e.fn.appear.run(),a})})}(jQuery); diff --git a/public/static/admin/js/core/jquery.countTo.min.js b/public/static/admin/js/core/jquery.countTo.min.js new file mode 100644 index 0000000..8d3f2fa --- /dev/null +++ b/public/static/admin/js/core/jquery.countTo.min.js @@ -0,0 +1,6 @@ +/*! + * jQuery countTo Plugin 1.2.0 + * https://github.com/mhuggins/jquery-countTo + * Copyright (c) Matt Huggins; Licensed MIT + */ +!function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t("object"==typeof exports?require("jquery"):jQuery)}(function(t){function e(t,e){return t.toFixed(e.decimals)}var o=function(e,i){this.$element=t(e),this.options=t.extend({},o.DEFAULTS,this.dataOptions(),i),this.init()};o.DEFAULTS={from:0,to:0,speed:1e3,refreshInterval:100,decimals:0,formatter:e,onUpdate:null,onComplete:null},o.prototype.init=function(){this.value=this.options.from,this.loops=Math.ceil(this.options.speed/this.options.refreshInterval),this.loopCount=0,this.increment=(this.options.to-this.options.from)/this.loops},o.prototype.dataOptions=function(){var t={from:this.$element.data("from"),to:this.$element.data("to"),speed:this.$element.data("speed"),refreshInterval:this.$element.data("refresh-interval"),decimals:this.$element.data("decimals")},e=Object.keys(t);for(var o in e){var i=e[o];"undefined"==typeof t[i]&&delete t[i]}return t},o.prototype.update=function(){this.value+=this.increment,this.loopCount++,this.render(),"function"==typeof this.options.onUpdate&&this.options.onUpdate.call(this.$element,this.value),this.loopCount>=this.loops&&(clearInterval(this.interval),this.value=this.options.to,"function"==typeof this.options.onComplete&&this.options.onComplete.call(this.$element,this.value))},o.prototype.render=function(){var t=this.options.formatter.call(this.$element,this.value,this.options);this.$element.text(t)},o.prototype.restart=function(){this.stop(),this.init(),this.start()},o.prototype.start=function(){this.stop(),this.render(),this.interval=setInterval(this.update.bind(this),this.options.refreshInterval)},o.prototype.stop=function(){this.interval&&clearInterval(this.interval)},o.prototype.toggle=function(){this.interval?this.stop():this.start()},t.fn.countTo=function(e){return this.each(function(){var i=t(this),n=i.data("countTo"),s=!n||"object"==typeof e,r="object"==typeof e?e:{},a="string"==typeof e?e:"start";s&&(n&&n.stop(),i.data("countTo",n=new o(this,r))),n[a].call(n)})}}); diff --git a/public/static/admin/js/core/jquery.min.js b/public/static/admin/js/core/jquery.min.js new file mode 100644 index 0000000..1677970 --- /dev/null +++ b/public/static/admin/js/core/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v2.2.3 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="2.2.3",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isPlainObject:function(a){var b;if("object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype||{},"isPrototypeOf"))return!1;for(b in a);return void 0===b||k.call(a,b)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=d.createElement("script"),b.text=a,d.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:h.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(d=e.call(arguments,2),f=function(){return a.apply(b||this,d.concat(e.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return h.call(b,a)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&f.parentNode&&(this.length=1,this[0]=f),this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?void 0!==c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?h.call(n(a),this[0]):h.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||n.uniqueSort(e),D.test(a)&&e.reverse()),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.removeEventListener("DOMContentLoaded",J),a.removeEventListener("load",J),n.ready()}n.ready.promise=function(b){return I||(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(n.ready):(d.addEventListener("DOMContentLoaded",J),a.addEventListener("load",J))),I.promise(b)},n.ready.promise();var K=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)K(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},L=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function M(){this.expando=n.expando+M.uid++}M.uid=1,M.prototype={register:function(a,b){var c=b||{};return a.nodeType?a[this.expando]=c:Object.defineProperty(a,this.expando,{value:c,writable:!0,configurable:!0}),a[this.expando]},cache:function(a){if(!L(a))return{};var b=a[this.expando];return b||(b={},L(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[b]=c;else for(d in b)e[d]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=a[this.expando];if(void 0!==f){if(void 0===b)this.register(a);else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in f?d=[b,e]:(d=e,d=d in f?[d]:d.match(G)||[])),c=d.length;while(c--)delete f[d[c]]}(void 0===b||n.isEmptyObject(f))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!n.isEmptyObject(b)}};var N=new M,O=new M,P=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Q=/[A-Z]/g;function R(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Q,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:P.test(c)?n.parseJSON(c):c; +}catch(e){}O.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return O.hasData(a)||N.hasData(a)},data:function(a,b,c){return O.access(a,b,c)},removeData:function(a,b){O.remove(a,b)},_data:function(a,b,c){return N.access(a,b,c)},_removeData:function(a,b){N.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=O.get(f),1===f.nodeType&&!N.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),R(f,d,e[d])));N.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){O.set(this,a)}):K(this,function(b){var c,d;if(f&&void 0===b){if(c=O.get(f,a)||O.get(f,a.replace(Q,"-$&").toLowerCase()),void 0!==c)return c;if(d=n.camelCase(a),c=O.get(f,d),void 0!==c)return c;if(c=R(f,d,void 0),void 0!==c)return c}else d=n.camelCase(a),this.each(function(){var c=O.get(this,d);O.set(this,d,b),a.indexOf("-")>-1&&void 0!==c&&O.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){O.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=N.get(a,b),c&&(!d||n.isArray(c)?d=N.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return N.get(a,c)||N.access(a,c,{empty:n.Callbacks("once memory").add(function(){N.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length",""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};$.optgroup=$.option,$.tbody=$.tfoot=$.colgroup=$.caption=$.thead,$.th=$.td;function _(a,b){var c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function aa(a,b){for(var c=0,d=a.length;d>c;c++)N.set(a[c],"globalEval",!b||N.get(b[c],"globalEval"))}var ba=/<|&#?\w+;/;function ca(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],o=0,p=a.length;p>o;o++)if(f=a[o],f||0===f)if("object"===n.type(f))n.merge(m,f.nodeType?[f]:f);else if(ba.test(f)){g=g||l.appendChild(b.createElement("div")),h=(Y.exec(f)||["",""])[1].toLowerCase(),i=$[h]||$._default,g.innerHTML=i[1]+n.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;n.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",o=0;while(f=m[o++])if(d&&n.inArray(f,d)>-1)e&&e.push(f);else if(j=n.contains(f.ownerDocument,f),g=_(l.appendChild(f),"script"),j&&aa(g),c){k=0;while(f=g[k++])Z.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),l.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var da=/^key/,ea=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,fa=/^([^.]*)(?:\.(.+)|)/;function ga(){return!0}function ha(){return!1}function ia(){try{return d.activeElement}catch(a){}}function ja(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ja(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=ha;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return"undefined"!=typeof n&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(G)||[""],j=b.length;while(j--)h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.hasData(a)&&N.get(a);if(r&&(i=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&N.remove(a,"handle events")}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(N.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!==this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,la=/\s*$/g;function pa(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function qa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function ra(a){var b=na.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function sa(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(N.hasData(a)&&(f=N.access(a),g=N.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}O.hasData(a)&&(h=O.access(a),i=n.extend({},h),O.set(b,i))}}function ta(a,b){var c=b.nodeName.toLowerCase();"input"===c&&X.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function ua(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&ma.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),ua(f,b,c,d)});if(o&&(e=ca(b,a[0].ownerDocument,!1,a,d),g=e.firstChild,1===e.childNodes.length&&(e=g),g||d)){for(h=n.map(_(e,"script"),qa),i=h.length;o>m;m++)j=e,m!==p&&(j=n.clone(j,!0,!0),i&&n.merge(h,_(j,"script"))),c.call(a[m],j,m);if(i)for(k=h[h.length-1].ownerDocument,n.map(h,ra),m=0;i>m;m++)j=h[m],Z.test(j.type||"")&&!N.access(j,"globalEval")&&n.contains(k,j)&&(j.src?n._evalUrl&&n._evalUrl(j.src):n.globalEval(j.textContent.replace(oa,"")))}return a}function va(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(_(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&aa(_(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(ka,"<$1>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=_(h),f=_(a),d=0,e=f.length;e>d;d++)ta(f[d],g[d]);if(b)if(c)for(f=f||_(a),g=g||_(h),d=0,e=f.length;e>d;d++)sa(f[d],g[d]);else sa(a,h);return g=_(h,"script"),g.length>0&&aa(g,!i&&_(a,"script")),h},cleanData:function(a){for(var b,c,d,e=n.event.special,f=0;void 0!==(c=a[f]);f++)if(L(c)){if(b=c[N.expando]){if(b.events)for(d in b.events)e[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);c[N.expando]=void 0}c[O.expando]&&(c[O.expando]=void 0)}}}),n.fn.extend({domManip:ua,detach:function(a){return va(this,a,!0)},remove:function(a){return va(this,a)},text:function(a){return K(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.appendChild(a)}})},prepend:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(_(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return K(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!la.test(a)&&!$[(Y.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(_(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return ua(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(_(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),f=e.length-1,h=0;f>=h;h++)c=h===f?this:this.clone(!0),n(e[h])[b](c),g.apply(d,c.get());return this.pushStack(d)}});var wa,xa={HTML:"block",BODY:"block"};function ya(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function za(a){var b=d,c=xa[a];return c||(c=ya(a,b),"none"!==c&&c||(wa=(wa||n("" : "" ) + + "" + + "" + (function(){ + return (settings.imageUpload) ? "
    " + + "" + + "" + + "
    " : ""; + })() + + "
    " + + "" + + "" + + "
    " + + "" + + "" + + "
    " + + ( (settings.imageUpload) ? "" : ""); + + //var imageFooterHTML = ""; + + dialog = this.createDialog({ + title : imageLang.title, + width : (settings.imageUpload) ? 465 : 380, + height : 254, + name : dialogName, + content : dialogContent, + mask : settings.dialogShowMask, + drag : settings.dialogDraggable, + lockScreen : settings.dialogLockScreen, + maskStyle : { + opacity : settings.dialogMaskOpacity, + backgroundColor : settings.dialogMaskBgColor + }, + buttons : { + enter : [lang.buttons.enter, function() { + var url = this.find("[data-url]").val(); + var alt = this.find("[data-alt]").val(); + var link = this.find("[data-link]").val(); + + if (url === "") + { + alert(imageLang.imageURLEmpty); + return false; + } + + var altAttr = (alt !== "") ? " \"" + alt + "\"" : ""; + + if (link === "" || link === "http://") + { + cm.replaceSelection("![" + alt + "](" + url + altAttr + ")"); + } + else + { + cm.replaceSelection("[![" + alt + "](" + url + altAttr + ")](" + link + altAttr + ")"); + } + + if (alt === "") { + cm.setCursor(cursor.line, cursor.ch + 2); + } + + this.hide().lockScreen(false).hideMask(); + + return false; + }], + + cancel : [lang.buttons.cancel, function() { + this.hide().lockScreen(false).hideMask(); + + return false; + }] + } + }); + + dialog.attr("id", classPrefix + "image-dialog-" + guid); + + if (!settings.imageUpload) { + return ; + } + + var fileInput = dialog.find("[name=\"" + classPrefix + "image-file\"]"); + + fileInput.bind("change", function() { + var fileName = fileInput.val(); + var isImage = new RegExp("(\\.(" + settings.imageFormats.join("|") + "))$"); // /(\.(webp|jpg|jpeg|gif|bmp|png))$/ + + if (fileName === "") + { + alert(imageLang.uploadFileEmpty); + + return false; + } + + if (!isImage.test(fileName)) + { + alert(imageLang.formatNotAllowed + settings.imageFormats.join(", ")); + + return false; + } + + loading(true); + + var submitHandler = function() { + + var uploadIframe = document.getElementById(iframeName); + + uploadIframe.onload = function() { + + loading(false); + + var body = (uploadIframe.contentWindow ? uploadIframe.contentWindow : uploadIframe.contentDocument).document.body; + var json = (body.innerText) ? body.innerText : ( (body.textContent) ? body.textContent : null); + + json = (typeof JSON.parse !== "undefined") ? JSON.parse(json) : eval("(" + json + ")"); + + if(!settings.crossDomainUpload) + { + if (json.success === 1) + { + dialog.find("[data-url]").val(json.url); + } + else + { + alert(json.message); + } + } + + return false; + }; + }; + + dialog.find("[type=\"submit\"]").bind("click", submitHandler).trigger("click"); + }); + } + + dialog = editor.find("." + dialogName); + dialog.find("[type=\"text\"]").val(""); + dialog.find("[type=\"file\"]").val(""); + dialog.find("[data-link]").val("http://"); + + this.dialogShowMask(dialog); + this.dialogLockScreen(); + dialog.show(); + + }; + + }; + + // CommonJS/Node.js + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") + { + module.exports = factory; + } + else if (typeof define === "function") // AMD/CMD/Sea.js + { + if (define.amd) { // for Require.js + + define(["editormd"], function(editormd) { + factory(editormd); + }); + + } else { // for Sea.js + define(function(require) { + var editormd = require("./../../editormd"); + factory(editormd); + }); + } + } + else + { + factory(window.editormd); + } + +})(); diff --git a/public/static/libs/editormd/plugins/link-dialog/link-dialog.js b/public/static/libs/editormd/plugins/link-dialog/link-dialog.js new file mode 100644 index 0000000..c0c0c58 --- /dev/null +++ b/public/static/libs/editormd/plugins/link-dialog/link-dialog.js @@ -0,0 +1,133 @@ +/*! + * Link dialog plugin for Editor.md + * + * @file link-dialog.js + * @author pandao + * @version 1.2.1 + * @updateTime 2015-06-09 + * {@link https://github.com/pandao/editor.md} + * @license MIT + */ + +(function() { + + var factory = function (exports) { + + var pluginName = "link-dialog"; + + exports.fn.linkDialog = function() { + + var _this = this; + var cm = this.cm; + var editor = this.editor; + var settings = this.settings; + var selection = cm.getSelection(); + var lang = this.lang; + var linkLang = lang.dialog.link; + var classPrefix = this.classPrefix; + var dialogName = classPrefix + pluginName, dialog; + + cm.focus(); + + if (editor.find("." + dialogName).length > 0) + { + dialog = editor.find("." + dialogName); + dialog.find("[data-url]").val("http://"); + dialog.find("[data-title]").val(selection); + + this.dialogShowMask(dialog); + this.dialogLockScreen(); + dialog.show(); + } + else + { + var dialogHTML = "
    " + + "" + + "" + + "
    " + + "" + + "" + + "
    " + + "
    "; + + dialog = this.createDialog({ + title : linkLang.title, + width : 380, + height : 211, + content : dialogHTML, + mask : settings.dialogShowMask, + drag : settings.dialogDraggable, + lockScreen : settings.dialogLockScreen, + maskStyle : { + opacity : settings.dialogMaskOpacity, + backgroundColor : settings.dialogMaskBgColor + }, + buttons : { + enter : [lang.buttons.enter, function() { + var url = this.find("[data-url]").val(); + var title = this.find("[data-title]").val(); + + if (url === "http://" || url === "") + { + alert(linkLang.urlEmpty); + return false; + } + + /*if (title === "") + { + alert(linkLang.titleEmpty); + return false; + }*/ + + var str = "[" + title + "](" + url + " \"" + title + "\")"; + + if (title == "") + { + str = "[" + url + "](" + url + ")"; + } + + cm.replaceSelection(str); + + this.hide().lockScreen(false).hideMask(); + + return false; + }], + + cancel : [lang.buttons.cancel, function() { + this.hide().lockScreen(false).hideMask(); + + return false; + }] + } + }); + } + }; + + }; + + // CommonJS/Node.js + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") + { + module.exports = factory; + } + else if (typeof define === "function") // AMD/CMD/Sea.js + { + if (define.amd) { // for Require.js + + define(["editormd"], function(editormd) { + factory(editormd); + }); + + } else { // for Sea.js + define(function(require) { + var editormd = require("./../../editormd"); + factory(editormd); + }); + } + } + else + { + factory(window.editormd); + } + +})(); diff --git a/public/static/libs/editormd/plugins/plugin-template.js b/public/static/libs/editormd/plugins/plugin-template.js new file mode 100644 index 0000000..836d8c6 --- /dev/null +++ b/public/static/libs/editormd/plugins/plugin-template.js @@ -0,0 +1,111 @@ +/*! + * Link dialog plugin for Editor.md + * + * @file link-dialog.js + * @author pandao + * @version 1.2.0 + * @updateTime 2015-03-07 + * {@link https://github.com/pandao/editor.md} + * @license MIT + */ + +(function() { + + var factory = function (exports) { + + var $ = jQuery; // if using module loader(Require.js/Sea.js). + + var langs = { + "zh-cn" : { + toolbar : { + table : "表格" + }, + dialog : { + table : { + title : "添加表格", + cellsLabel : "单元格数", + alignLabel : "对齐方式", + rows : "行数", + cols : "列数", + aligns : ["默认", "左对齐", "居中对齐", "右对齐"] + } + } + }, + "zh-tw" : { + toolbar : { + table : "添加表格" + }, + dialog : { + table : { + title : "添加表格", + cellsLabel : "單元格數", + alignLabel : "對齊方式", + rows : "行數", + cols : "列數", + aligns : ["默認", "左對齊", "居中對齊", "右對齊"] + } + } + }, + "en" : { + toolbar : { + table : "Tables" + }, + dialog : { + table : { + title : "Tables", + cellsLabel : "Cells", + alignLabel : "Align", + rows : "Rows", + cols : "Cols", + aligns : ["Default", "Left align", "Center align", "Right align"] + } + } + } + }; + + exports.fn.htmlEntities = function() { + /* + var _this = this; // this == the current instance object of Editor.md + var lang = _this.lang; + var settings = _this.settings; + var editor = this.editor; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + var classPrefix = this.classPrefix; + + $.extend(true, this.lang, langs[this.lang.name]); // l18n + this.setToolbar(); + + cm.focus(); + */ + //.... + }; + + }; + + // CommonJS/Node.js + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") + { + module.exports = factory; + } + else if (typeof define === "function") // AMD/CMD/Sea.js + { + if (define.amd) { // for Require.js + + define(["editormd"], function(editormd) { + factory(editormd); + }); + + } else { // for Sea.js + define(function(require) { + var editormd = require("./../../editormd"); + factory(editormd); + }); + } + } + else + { + factory(window.editormd); + } + +})(); diff --git a/public/static/libs/editormd/plugins/preformatted-text-dialog/preformatted-text-dialog.js b/public/static/libs/editormd/plugins/preformatted-text-dialog/preformatted-text-dialog.js new file mode 100644 index 0000000..e19bbd5 --- /dev/null +++ b/public/static/libs/editormd/plugins/preformatted-text-dialog/preformatted-text-dialog.js @@ -0,0 +1,172 @@ +/*! + * Preformatted text dialog plugin for Editor.md + * + * @file preformatted-text-dialog.js + * @author pandao + * @version 1.2.0 + * @updateTime 2015-03-07 + * {@link https://github.com/pandao/editor.md} + * @license MIT + */ + +(function() { + + var factory = function (exports) { + var cmEditor; + var pluginName = "preformatted-text-dialog"; + + exports.fn.preformattedTextDialog = function() { + + var _this = this; + var cm = this.cm; + var lang = this.lang; + var editor = this.editor; + var settings = this.settings; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + var classPrefix = this.classPrefix; + var dialogLang = lang.dialog.preformattedText; + var dialogName = classPrefix + pluginName, dialog; + + cm.focus(); + + if (editor.find("." + dialogName).length > 0) + { + dialog = editor.find("." + dialogName); + dialog.find("textarea").val(selection); + + this.dialogShowMask(dialog); + this.dialogLockScreen(); + dialog.show(); + } + else + { + var dialogContent = ""; + + dialog = this.createDialog({ + name : dialogName, + title : dialogLang.title, + width : 780, + height : 540, + mask : settings.dialogShowMask, + drag : settings.dialogDraggable, + content : dialogContent, + lockScreen : settings.dialogLockScreen, + maskStyle : { + opacity : settings.dialogMaskOpacity, + backgroundColor : settings.dialogMaskBgColor + }, + buttons : { + enter : [lang.buttons.enter, function() { + var codeTexts = this.find("textarea").val(); + + if (codeTexts === "") + { + alert(dialogLang.emptyAlert); + return false; + } + + codeTexts = codeTexts.split("\n"); + + for (var i in codeTexts) + { + codeTexts[i] = " " + codeTexts[i]; + } + + codeTexts = codeTexts.join("\n"); + + if (cursor.ch !== 0) { + codeTexts = "\r\n\r\n" + codeTexts; + } + + cm.replaceSelection(codeTexts); + + this.hide().lockScreen(false).hideMask(); + + return false; + }], + cancel : [lang.buttons.cancel, function() { + this.hide().lockScreen(false).hideMask(); + + return false; + }] + } + }); + } + + var cmConfig = { + mode : "text/html", + theme : settings.theme, + tabSize : 4, + autofocus : true, + autoCloseTags : true, + indentUnit : 4, + lineNumbers : true, + lineWrapping : true, + extraKeys : {"Ctrl-Q": function(cm){ cm.foldCode(cm.getCursor()); }}, + foldGutter : true, + gutters : ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], + matchBrackets : true, + indentWithTabs : true, + styleActiveLine : true, + styleSelectedText : true, + autoCloseBrackets : true, + showTrailingSpace : true, + highlightSelectionMatches : true + }; + + var textarea = dialog.find("textarea"); + var cmObj = dialog.find(".CodeMirror"); + + if (dialog.find(".CodeMirror").length < 1) + { + cmEditor = exports.$CodeMirror.fromTextArea(textarea[0], cmConfig); + cmObj = dialog.find(".CodeMirror"); + + cmObj.css({ + "float" : "none", + margin : "0 0 5px", + border : "1px solid #ddd", + fontSize : settings.fontSize, + width : "100%", + height : "410px" + }); + + cmEditor.on("change", function(cm) { + textarea.val(cm.getValue()); + }); + } + else + { + cmEditor.setValue(cm.getSelection()); + } + }; + + }; + + // CommonJS/Node.js + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") + { + module.exports = factory; + } + else if (typeof define === "function") // AMD/CMD/Sea.js + { + if (define.amd) { // for Require.js + + define(["editormd"], function(editormd) { + factory(editormd); + }); + + } else { // for Sea.js + define(function(require) { + var editormd = require("./../../editormd"); + factory(editormd); + }); + } + } + else + { + factory(window.editormd); + } + +})(); diff --git a/public/static/libs/editormd/plugins/reference-link-dialog/reference-link-dialog.js b/public/static/libs/editormd/plugins/reference-link-dialog/reference-link-dialog.js new file mode 100644 index 0000000..fea88f2 --- /dev/null +++ b/public/static/libs/editormd/plugins/reference-link-dialog/reference-link-dialog.js @@ -0,0 +1,153 @@ +/*! + * Reference link dialog plugin for Editor.md + * + * @file reference-link-dialog.js + * @author pandao + * @version 1.2.1 + * @updateTime 2015-06-09 + * {@link https://github.com/pandao/editor.md} + * @license MIT + */ + +(function() { + + var factory = function (exports) { + + var pluginName = "reference-link-dialog"; + var ReLinkId = 1; + + exports.fn.referenceLinkDialog = function() { + + var _this = this; + var cm = this.cm; + var lang = this.lang; + var editor = this.editor; + var settings = this.settings; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + var dialogLang = lang.dialog.referenceLink; + var classPrefix = this.classPrefix; + var dialogName = classPrefix + pluginName, dialog; + + cm.focus(); + + if (editor.find("." + dialogName).length < 1) + { + var dialogHTML = "
    " + + "" + + "" + + "
    " + + "" + + "" + + "
    " + + "" + + "" + + "
    " + + "" + + "" + + "
    " + + "
    "; + + dialog = this.createDialog({ + name : dialogName, + title : dialogLang.title, + width : 380, + height : 296, + content : dialogHTML, + mask : settings.dialogShowMask, + drag : settings.dialogDraggable, + lockScreen : settings.dialogLockScreen, + maskStyle : { + opacity : settings.dialogMaskOpacity, + backgroundColor : settings.dialogMaskBgColor + }, + buttons : { + enter : [lang.buttons.enter, function() { + var name = this.find("[data-name]").val(); + var url = this.find("[data-url]").val(); + var rid = this.find("[data-url-id]").val(); + var title = this.find("[data-title]").val(); + + if (name === "") + { + alert(dialogLang.nameEmpty); + return false; + } + + if (rid === "") + { + alert(dialogLang.idEmpty); + return false; + } + + if (url === "http://" || url === "") + { + alert(dialogLang.urlEmpty); + return false; + } + + //cm.replaceSelection("[" + title + "][" + name + "]\n[" + name + "]: " + url + ""); + cm.replaceSelection("[" + name + "][" + rid + "]"); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 1); + } + + title = (title === "") ? "" : " \"" + title + "\""; + + cm.setValue(cm.getValue() + "\n[" + rid + "]: " + url + title + ""); + + this.hide().lockScreen(false).hideMask(); + + return false; + }], + cancel : [lang.buttons.cancel, function() { + this.hide().lockScreen(false).hideMask(); + + return false; + }] + } + }); + } + + dialog = editor.find("." + dialogName); + dialog.find("[data-name]").val("[" + ReLinkId + "]"); + dialog.find("[data-url-id]").val(""); + dialog.find("[data-url]").val("http://"); + dialog.find("[data-title]").val(selection); + + this.dialogShowMask(dialog); + this.dialogLockScreen(); + dialog.show(); + + ReLinkId++; + }; + + }; + + // CommonJS/Node.js + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") + { + module.exports = factory; + } + else if (typeof define === "function") // AMD/CMD/Sea.js + { + if (define.amd) { // for Require.js + + define(["editormd"], function(editormd) { + factory(editormd); + }); + + } else { // for Sea.js + define(function(require) { + var editormd = require("./../../editormd"); + factory(editormd); + }); + } + } + else + { + factory(window.editormd); + } + +})(); diff --git a/public/static/libs/editormd/plugins/table-dialog/table-dialog.js b/public/static/libs/editormd/plugins/table-dialog/table-dialog.js new file mode 100644 index 0000000..b150b4c --- /dev/null +++ b/public/static/libs/editormd/plugins/table-dialog/table-dialog.js @@ -0,0 +1,218 @@ +/*! + * Table dialog plugin for Editor.md + * + * @file table-dialog.js + * @author pandao + * @version 1.2.1 + * @updateTime 2015-06-09 + * {@link https://github.com/pandao/editor.md} + * @license MIT + */ + +(function() { + + var factory = function (exports) { + + var $ = jQuery; + var pluginName = "table-dialog"; + + var langs = { + "zh-cn" : { + toolbar : { + table : "表格" + }, + dialog : { + table : { + title : "添加表格", + cellsLabel : "单元格数", + alignLabel : "对齐方式", + rows : "行数", + cols : "列数", + aligns : ["默认", "左对齐", "居中对齐", "右对齐"] + } + } + }, + "zh-tw" : { + toolbar : { + table : "添加表格" + }, + dialog : { + table : { + title : "添加表格", + cellsLabel : "單元格數", + alignLabel : "對齊方式", + rows : "行數", + cols : "列數", + aligns : ["默認", "左對齊", "居中對齊", "右對齊"] + } + } + }, + "en" : { + toolbar : { + table : "Tables" + }, + dialog : { + table : { + title : "Tables", + cellsLabel : "Cells", + alignLabel : "Align", + rows : "Rows", + cols : "Cols", + aligns : ["Default", "Left align", "Center align", "Right align"] + } + } + } + }; + + exports.fn.tableDialog = function() { + var _this = this; + var cm = this.cm; + var editor = this.editor; + var settings = this.settings; + var path = settings.path + "../plugins/" + pluginName +"/"; + var classPrefix = this.classPrefix; + var dialogName = classPrefix + pluginName, dialog; + + $.extend(true, this.lang, langs[this.lang.name]); + this.setToolbar(); + + var lang = this.lang; + var dialogLang = lang.dialog.table; + + var dialogContent = [ + "
    ", + "", + dialogLang.rows + "   ", + dialogLang.cols + "
    ", + "", + "
    ", + "
    " + ].join("\n"); + + if (editor.find("." + dialogName).length > 0) + { + dialog = editor.find("." + dialogName); + + this.dialogShowMask(dialog); + this.dialogLockScreen(); + dialog.show(); + } + else + { + dialog = this.createDialog({ + name : dialogName, + title : dialogLang.title, + width : 360, + height : 226, + mask : settings.dialogShowMask, + drag : settings.dialogDraggable, + content : dialogContent, + lockScreen : settings.dialogLockScreen, + maskStyle : { + opacity : settings.dialogMaskOpacity, + backgroundColor : settings.dialogMaskBgColor + }, + buttons : { + enter : [lang.buttons.enter, function() { + var rows = parseInt(this.find("[data-rows]").val()); + var cols = parseInt(this.find("[data-cols]").val()); + var align = this.find("[name=\"table-align\"]:checked").val(); + var table = ""; + var hrLine = "------------"; + + var alignSign = { + _default : hrLine, + left : ":" + hrLine, + center : ":" + hrLine + ":", + right : hrLine + ":" + }; + + if ( rows > 1 && cols > 0) + { + for (var r = 0, len = rows; r < len; r++) + { + var row = []; + var head = []; + + for (var c = 0, len2 = cols; c < len2; c++) + { + if (r === 1) { + head.push(alignSign[align]); + } + + row.push(" "); + } + + if (r === 1) { + table += "| " + head.join(" | ") + " |" + "\n"; + } + + table += "| " + row.join( (cols === 1) ? "" : " | " ) + " |" + "\n"; + } + } + + cm.replaceSelection(table); + + this.hide().lockScreen(false).hideMask(); + + return false; + }], + + cancel : [lang.buttons.cancel, function() { + this.hide().lockScreen(false).hideMask(); + + return false; + }] + } + }); + } + + var faBtns = dialog.find(".fa-btns"); + + if (faBtns.html() === "") + { + var icons = ["align-justify", "align-left", "align-center", "align-right"]; + var _lang = dialogLang.aligns; + var values = ["_default", "left", "center", "right"]; + + for (var i = 0, len = icons.length; i < len; i++) + { + var checked = (i === 0) ? " checked=\"checked\"" : ""; + var btn = ""; + + faBtns.append(btn); + } + } + }; + + }; + + // CommonJS/Node.js + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") + { + module.exports = factory; + } + else if (typeof define === "function") // AMD/CMD/Sea.js + { + if (define.amd) { // for Require.js + + define(["editormd"], function(editormd) { + factory(editormd); + }); + + } else { // for Sea.js + define(function(require) { + var editormd = require("./../../editormd"); + factory(editormd); + }); + } + } + else + { + factory(window.editormd); + } + +})(); diff --git a/public/static/libs/editormd/plugins/test-plugin/test-plugin.js b/public/static/libs/editormd/plugins/test-plugin/test-plugin.js new file mode 100644 index 0000000..573a9b5 --- /dev/null +++ b/public/static/libs/editormd/plugins/test-plugin/test-plugin.js @@ -0,0 +1,66 @@ +/*! + * Test plugin for Editor.md + * + * @file test-plugin.js + * @author pandao + * @version 1.2.0 + * @updateTime 2015-03-07 + * {@link https://github.com/pandao/editor.md} + * @license MIT + */ + +(function() { + + var factory = function (exports) { + + var $ = jQuery; // if using module loader(Require.js/Sea.js). + + exports.testPlugin = function(){ + alert("testPlugin"); + }; + + exports.fn.testPluginMethodA = function() { + /* + var _this = this; // this == the current instance object of Editor.md + var lang = _this.lang; + var settings = _this.settings; + var editor = this.editor; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + var classPrefix = this.classPrefix; + + cm.focus(); + */ + //.... + + alert("testPluginMethodA"); + }; + + }; + + // CommonJS/Node.js + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") + { + module.exports = factory; + } + else if (typeof define === "function") // AMD/CMD/Sea.js + { + if (define.amd) { // for Require.js + + define(["editormd"], function(editormd) { + factory(editormd); + }); + + } else { // for Sea.js + define(function(require) { + var editormd = require("./../../editormd"); + factory(editormd); + }); + } + } + else + { + factory(window.editormd); + } + +})(); diff --git a/public/static/libs/flot/jquery.flot.js b/public/static/libs/flot/jquery.flot.js new file mode 100644 index 0000000..dd20ad6 --- /dev/null +++ b/public/static/libs/flot/jquery.flot.js @@ -0,0 +1,3168 @@ +/* Javascript plotting library for jQuery, version 0.8.3. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ + +// first an inline dependency, jquery.colorhelpers.js, we inline it here +// for convenience + +/* Plugin for jQuery for working with colors. + * + * Version 1.1. + * + * Inspiration from jQuery color animation plugin by John Resig. + * + * Released under the MIT license by Ole Laursen, October 2009. + * + * Examples: + * + * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() + * var c = $.color.extract($("#mydiv"), 'background-color'); + * console.log(c.r, c.g, c.b, c.a); + * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" + * + * Note that .scale() and .add() return the same modified object + * instead of making a new one. + * + * V. 1.1: Fix error handling so e.g. parsing an empty string does + * produce a color rather than just crashing. + */ +(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); + +// the actual Flot code +(function($) { + + // Cache the prototype hasOwnProperty for faster access + + var hasOwnProperty = Object.prototype.hasOwnProperty; + + // A shim to provide 'detach' to jQuery versions prior to 1.4. Using a DOM + // operation produces the same effect as detach, i.e. removing the element + // without touching its jQuery data. + + // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+. + + if (!$.fn.detach) { + $.fn.detach = function() { + return this.each(function() { + if (this.parentNode) { + this.parentNode.removeChild( this ); + } + }); + }; + } + + /////////////////////////////////////////////////////////////////////////// + // The Canvas object is a wrapper around an HTML5 tag. + // + // @constructor + // @param {string} cls List of classes to apply to the canvas. + // @param {element} container Element onto which to append the canvas. + // + // Requiring a container is a little iffy, but unfortunately canvas + // operations don't work unless the canvas is attached to the DOM. + + function Canvas(cls, container) { + + var element = container.children("." + cls)[0]; + + if (element == null) { + + element = document.createElement("canvas"); + element.className = cls; + + $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) + .appendTo(container); + + // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas + + if (!element.getContext) { + if (window.G_vmlCanvasManager) { + element = window.G_vmlCanvasManager.initElement(element); + } else { + throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); + } + } + } + + this.element = element; + + var context = this.context = element.getContext("2d"); + + // Determine the screen's ratio of physical to device-independent + // pixels. This is the ratio between the canvas width that the browser + // advertises and the number of pixels actually present in that space. + + // The iPhone 4, for example, has a device-independent width of 320px, + // but its screen is actually 640px wide. It therefore has a pixel + // ratio of 2, while most normal devices have a ratio of 1. + + var devicePixelRatio = window.devicePixelRatio || 1, + backingStoreRatio = + context.webkitBackingStorePixelRatio || + context.mozBackingStorePixelRatio || + context.msBackingStorePixelRatio || + context.oBackingStorePixelRatio || + context.backingStorePixelRatio || 1; + + this.pixelRatio = devicePixelRatio / backingStoreRatio; + + // Size the canvas to match the internal dimensions of its container + + this.resize(container.width(), container.height()); + + // Collection of HTML div layers for text overlaid onto the canvas + + this.textContainer = null; + this.text = {}; + + // Cache of text fragments and metrics, so we can avoid expensively + // re-calculating them when the plot is re-rendered in a loop. + + this._textCache = {}; + } + + // Resizes the canvas to the given dimensions. + // + // @param {number} width New width of the canvas, in pixels. + // @param {number} width New height of the canvas, in pixels. + + Canvas.prototype.resize = function(width, height) { + + if (width <= 0 || height <= 0) { + throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); + } + + var element = this.element, + context = this.context, + pixelRatio = this.pixelRatio; + + // Resize the canvas, increasing its density based on the display's + // pixel ratio; basically giving it more pixels without increasing the + // size of its element, to take advantage of the fact that retina + // displays have that many more pixels in the same advertised space. + + // Resizing should reset the state (excanvas seems to be buggy though) + + if (this.width != width) { + element.width = width * pixelRatio; + element.style.width = width + "px"; + this.width = width; + } + + if (this.height != height) { + element.height = height * pixelRatio; + element.style.height = height + "px"; + this.height = height; + } + + // Save the context, so we can reset in case we get replotted. The + // restore ensure that we're really back at the initial state, and + // should be safe even if we haven't saved the initial state yet. + + context.restore(); + context.save(); + + // Scale the coordinate space to match the display density; so even though we + // may have twice as many pixels, we still want lines and other drawing to + // appear at the same size; the extra pixels will just make them crisper. + + context.scale(pixelRatio, pixelRatio); + }; + + // Clears the entire canvas area, not including any overlaid HTML text + + Canvas.prototype.clear = function() { + this.context.clearRect(0, 0, this.width, this.height); + }; + + // Finishes rendering the canvas, including managing the text overlay. + + Canvas.prototype.render = function() { + + var cache = this._textCache; + + // For each text layer, add elements marked as active that haven't + // already been rendered, and remove those that are no longer active. + + for (var layerKey in cache) { + if (hasOwnProperty.call(cache, layerKey)) { + + var layer = this.getTextLayer(layerKey), + layerCache = cache[layerKey]; + + layer.hide(); + + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + + var positions = styleCache[key].positions; + + for (var i = 0, position; position = positions[i]; i++) { + if (position.active) { + if (!position.rendered) { + layer.append(position.element); + position.rendered = true; + } + } else { + positions.splice(i--, 1); + if (position.rendered) { + position.element.detach(); + } + } + } + + if (positions.length == 0) { + delete styleCache[key]; + } + } + } + } + } + + layer.show(); + } + } + }; + + // Creates (if necessary) and returns the text overlay container. + // + // @param {string} classes String of space-separated CSS classes used to + // uniquely identify the text layer. + // @return {object} The jQuery-wrapped text-layer div. + + Canvas.prototype.getTextLayer = function(classes) { + + var layer = this.text[classes]; + + // Create the text layer if it doesn't exist + + if (layer == null) { + + // Create the text layer container, if it doesn't exist + + if (this.textContainer == null) { + this.textContainer = $("
    ") + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0, + 'font-size': "smaller", + color: "#545454" + }) + .insertAfter(this.element); + } + + layer = this.text[classes] = $("
    ") + .addClass(classes) + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0 + }) + .appendTo(this.textContainer); + } + + return layer; + }; + + // Creates (if necessary) and returns a text info object. + // + // The object looks like this: + // + // { + // width: Width of the text's wrapper div. + // height: Height of the text's wrapper div. + // element: The jQuery-wrapped HTML div containing the text. + // positions: Array of positions at which this text is drawn. + // } + // + // The positions array contains objects that look like this: + // + // { + // active: Flag indicating whether the text should be visible. + // rendered: Flag indicating whether the text is currently visible. + // element: The jQuery-wrapped HTML div containing the text. + // x: X coordinate at which to draw the text. + // y: Y coordinate at which to draw the text. + // } + // + // Each position after the first receives a clone of the original element. + // + // The idea is that that the width, height, and general 'identity' of the + // text is constant no matter where it is placed; the placements are a + // secondary property. + // + // Canvas maintains a cache of recently-used text info objects; getTextInfo + // either returns the cached element or creates a new entry. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {string} text Text string to retrieve info for. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @return {object} a text info object. + + Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { + + var textStyle, layerCache, styleCache, info; + + // Cast the value to a string, in case we were given a number or such + + text = "" + text; + + // If the font is a font-spec object, generate a CSS font definition + + if (typeof font === "object") { + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; + } else { + textStyle = font; + } + + // Retrieve (or create) the cache for the text's layer and styles + + layerCache = this._textCache[layer]; + + if (layerCache == null) { + layerCache = this._textCache[layer] = {}; + } + + styleCache = layerCache[textStyle]; + + if (styleCache == null) { + styleCache = layerCache[textStyle] = {}; + } + + info = styleCache[text]; + + // If we can't find a matching element in our cache, create a new one + + if (info == null) { + + var element = $("
    ").html(text) + .css({ + position: "absolute", + 'max-width': width, + top: -9999 + }) + .appendTo(this.getTextLayer(layer)); + + if (typeof font === "object") { + element.css({ + font: textStyle, + color: font.color + }); + } else if (typeof font === "string") { + element.addClass(font); + } + + info = styleCache[text] = { + width: element.outerWidth(true), + height: element.outerHeight(true), + element: element, + positions: [] + }; + + element.detach(); + } + + return info; + }; + + // Adds a text string to the canvas text overlay. + // + // The text isn't drawn immediately; it is marked as rendering, which will + // result in its addition to the canvas on the next render pass. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number} x X coordinate at which to draw the text. + // @param {number} y Y coordinate at which to draw the text. + // @param {string} text Text string to draw. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @param {string=} halign Horizontal alignment of the text; either "left", + // "center" or "right". + // @param {string=} valign Vertical alignment of the text; either "top", + // "middle" or "bottom". + + Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { + + var info = this.getTextInfo(layer, text, font, angle, width), + positions = info.positions; + + // Tweak the div's position to match the text's alignment + + if (halign == "center") { + x -= info.width / 2; + } else if (halign == "right") { + x -= info.width; + } + + if (valign == "middle") { + y -= info.height / 2; + } else if (valign == "bottom") { + y -= info.height; + } + + // Determine whether this text already exists at this position. + // If so, mark it for inclusion in the next render pass. + + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = true; + return; + } + } + + // If the text doesn't exist at this position, create a new entry + + // For the very first position we'll re-use the original element, + // while for subsequent ones we'll clone it. + + position = { + active: true, + rendered: false, + element: positions.length ? info.element.clone() : info.element, + x: x, + y: y + }; + + positions.push(position); + + // Move the element to its final position within the container + + position.element.css({ + top: Math.round(y), + left: Math.round(x), + 'text-align': halign // In case the text wraps + }); + }; + + // Removes one or more text strings from the canvas text overlay. + // + // If no parameters are given, all text within the layer is removed. + // + // Note that the text is not immediately removed; it is simply marked as + // inactive, which will result in its removal on the next render pass. + // This avoids the performance penalty for 'clear and redraw' behavior, + // where we potentially get rid of all text on a layer, but will likely + // add back most or all of it later, as when redrawing axes, for example. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number=} x X coordinate of the text. + // @param {number=} y Y coordinate of the text. + // @param {string=} text Text string to remove. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which the text is rotated, in degrees. + // Angle is currently unused, it will be implemented in the future. + + Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { + if (text == null) { + var layerCache = this._textCache[layer]; + if (layerCache != null) { + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + var positions = styleCache[key].positions; + for (var i = 0, position; position = positions[i]; i++) { + position.active = false; + } + } + } + } + } + } + } else { + var positions = this.getTextInfo(layer, text, font, angle).positions; + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = false; + } + } + } + }; + + /////////////////////////////////////////////////////////////////////////// + // The top-level container for the entire plot. + + function Plot(placeholder, data_, options_, plugins) { + // data is on the form: + // [ series1, series2 ... ] + // where series is either just the data as [ [x1, y1], [x2, y2], ... ] + // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } + + var series = [], + options = { + // the color theme used for graphs + colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], + legend: { + show: true, + noColumns: 1, // number of colums in legend table + labelFormatter: null, // fn: string -> string + labelBoxBorderColor: "#ccc", // border color for the little label boxes + container: null, // container (as jQuery object) to put legend in, null means default on top of graph + position: "ne", // position of default legend container within plot + margin: 5, // distance from grid edge to default legend container within plot + backgroundColor: null, // null means auto-detect + backgroundOpacity: 0.85, // set to 0 to avoid background + sorted: null // default to no legend sorting + }, + xaxis: { + show: null, // null = auto-detect, true = always, false = never + position: "bottom", // or "top" + mode: null, // null or "time" + font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } + color: null, // base color, labels, ticks + tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" + transform: null, // null or f: number -> number to transform axis + inverseTransform: null, // if transform is set, this should be the inverse function + min: null, // min. value to show, null means set automatically + max: null, // max. value to show, null means set automatically + autoscaleMargin: null, // margin in % to add if auto-setting min/max + ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks + tickFormatter: null, // fn: number -> string + labelWidth: null, // size of tick labels in pixels + labelHeight: null, + reserveSpace: null, // whether to reserve space even if axis isn't shown + tickLength: null, // size in pixels of ticks, or "full" for whole line + alignTicksWithAxis: null, // axis number or null for no sync + tickDecimals: null, // no. of decimals, null means auto + tickSize: null, // number or [number, "unit"] + minTickSize: null // number or [number, "unit"] + }, + yaxis: { + autoscaleMargin: 0.02, + position: "left" // or "right" + }, + xaxes: [], + yaxes: [], + series: { + points: { + show: false, + radius: 3, + lineWidth: 2, // in pixels + fill: true, + fillColor: "#ffffff", + symbol: "circle" // or callback + }, + lines: { + // we don't put in show: false so we can see + // whether lines were actively disabled + lineWidth: 2, // in pixels + fill: false, + fillColor: null, + steps: false + // Omit 'zero', so we can later default its value to + // match that of the 'fill' option. + }, + bars: { + show: false, + lineWidth: 2, // in pixels + barWidth: 1, // in units of the x axis + fill: true, + fillColor: null, + align: "left", // "left", "right", or "center" + horizontal: false, + zero: true + }, + shadowSize: 3, + highlightColor: null + }, + grid: { + show: true, + aboveData: false, + color: "#545454", // primary color used for outline and labels + backgroundColor: null, // null for transparent, else color + borderColor: null, // set if different from the grid color + tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" + margin: 0, // distance from the canvas edge to the grid + labelMargin: 5, // in pixels + axisMargin: 8, // in pixels + borderWidth: 2, // in pixels + minBorderMargin: null, // in pixels, null means taken from points radius + markings: null, // array of ranges or fn: axes -> array of ranges + markingsColor: "#f4f4f4", + markingsLineWidth: 2, + // interactive stuff + clickable: false, + hoverable: false, + autoHighlight: true, // highlight in case mouse is near + mouseActiveRadius: 10 // how far the mouse can be away to activate an item + }, + interaction: { + redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow + }, + hooks: {} + }, + surface = null, // the canvas for the plot itself + overlay = null, // canvas for interactive stuff on top of plot + eventHolder = null, // jQuery object that events should be bound to + ctx = null, octx = null, + xaxes = [], yaxes = [], + plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, + plotWidth = 0, plotHeight = 0, + hooks = { + processOptions: [], + processRawData: [], + processDatapoints: [], + processOffset: [], + drawBackground: [], + drawSeries: [], + draw: [], + bindEvents: [], + drawOverlay: [], + shutdown: [] + }, + plot = this; + + // public functions + plot.setData = setData; + plot.setupGrid = setupGrid; + plot.draw = draw; + plot.getPlaceholder = function() { return placeholder; }; + plot.getCanvas = function() { return surface.element; }; + plot.getPlotOffset = function() { return plotOffset; }; + plot.width = function () { return plotWidth; }; + plot.height = function () { return plotHeight; }; + plot.offset = function () { + var o = eventHolder.offset(); + o.left += plotOffset.left; + o.top += plotOffset.top; + return o; + }; + plot.getData = function () { return series; }; + plot.getAxes = function () { + var res = {}, i; + $.each(xaxes.concat(yaxes), function (_, axis) { + if (axis) + res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; + }); + return res; + }; + plot.getXAxes = function () { return xaxes; }; + plot.getYAxes = function () { return yaxes; }; + plot.c2p = canvasToAxisCoords; + plot.p2c = axisToCanvasCoords; + plot.getOptions = function () { return options; }; + plot.highlight = highlight; + plot.unhighlight = unhighlight; + plot.triggerRedrawOverlay = triggerRedrawOverlay; + plot.pointOffset = function(point) { + return { + left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), + top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) + }; + }; + plot.shutdown = shutdown; + plot.destroy = function () { + shutdown(); + placeholder.removeData("plot").empty(); + + series = []; + options = null; + surface = null; + overlay = null; + eventHolder = null; + ctx = null; + octx = null; + xaxes = []; + yaxes = []; + hooks = null; + highlights = []; + plot = null; + }; + plot.resize = function () { + var width = placeholder.width(), + height = placeholder.height(); + surface.resize(width, height); + overlay.resize(width, height); + }; + + // public attributes + plot.hooks = hooks; + + // initialize + initPlugins(plot); + parseOptions(options_); + setupCanvases(); + setData(data_); + setupGrid(); + draw(); + bindEvents(); + + + function executeHooks(hook, args) { + args = [plot].concat(args); + for (var i = 0; i < hook.length; ++i) + hook[i].apply(this, args); + } + + function initPlugins() { + + // References to key classes, allowing plugins to modify them + + var classes = { + Canvas: Canvas + }; + + for (var i = 0; i < plugins.length; ++i) { + var p = plugins[i]; + p.init(plot, classes); + if (p.options) + $.extend(true, options, p.options); + } + } + + function parseOptions(opts) { + + $.extend(true, options, opts); + + // $.extend merges arrays, rather than replacing them. When less + // colors are provided than the size of the default palette, we + // end up with those colors plus the remaining defaults, which is + // not expected behavior; avoid it by replacing them here. + + if (opts && opts.colors) { + options.colors = opts.colors; + } + + if (options.xaxis.color == null) + options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + if (options.yaxis.color == null) + options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility + options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; + if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility + options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; + + if (options.grid.borderColor == null) + options.grid.borderColor = options.grid.color; + if (options.grid.tickColor == null) + options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + // Fill in defaults for axis options, including any unspecified + // font-spec fields, if a font-spec was provided. + + // If no x/y axis options were provided, create one of each anyway, + // since the rest of the code assumes that they exist. + + var i, axisOptions, axisCount, + fontSize = placeholder.css("font-size"), + fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, + fontDefaults = { + style: placeholder.css("font-style"), + size: Math.round(0.8 * fontSizeDefault), + variant: placeholder.css("font-variant"), + weight: placeholder.css("font-weight"), + family: placeholder.css("font-family") + }; + + axisCount = options.xaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.xaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.xaxis, axisOptions); + options.xaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + if (!axisOptions.font.lineHeight) { + axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); + } + } + } + + axisCount = options.yaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.yaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.yaxis, axisOptions); + options.yaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + if (!axisOptions.font.lineHeight) { + axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); + } + } + } + + // backwards compatibility, to be removed in future + if (options.xaxis.noTicks && options.xaxis.ticks == null) + options.xaxis.ticks = options.xaxis.noTicks; + if (options.yaxis.noTicks && options.yaxis.ticks == null) + options.yaxis.ticks = options.yaxis.noTicks; + if (options.x2axis) { + options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); + options.xaxes[1].position = "top"; + // Override the inherit to allow the axis to auto-scale + if (options.x2axis.min == null) { + options.xaxes[1].min = null; + } + if (options.x2axis.max == null) { + options.xaxes[1].max = null; + } + } + if (options.y2axis) { + options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); + options.yaxes[1].position = "right"; + // Override the inherit to allow the axis to auto-scale + if (options.y2axis.min == null) { + options.yaxes[1].min = null; + } + if (options.y2axis.max == null) { + options.yaxes[1].max = null; + } + } + if (options.grid.coloredAreas) + options.grid.markings = options.grid.coloredAreas; + if (options.grid.coloredAreasColor) + options.grid.markingsColor = options.grid.coloredAreasColor; + if (options.lines) + $.extend(true, options.series.lines, options.lines); + if (options.points) + $.extend(true, options.series.points, options.points); + if (options.bars) + $.extend(true, options.series.bars, options.bars); + if (options.shadowSize != null) + options.series.shadowSize = options.shadowSize; + if (options.highlightColor != null) + options.series.highlightColor = options.highlightColor; + + // save options on axes for future reference + for (i = 0; i < options.xaxes.length; ++i) + getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; + for (i = 0; i < options.yaxes.length; ++i) + getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; + + // add hooks from options + for (var n in hooks) + if (options.hooks[n] && options.hooks[n].length) + hooks[n] = hooks[n].concat(options.hooks[n]); + + executeHooks(hooks.processOptions, [options]); + } + + function setData(d) { + series = parseData(d); + fillInSeriesOptions(); + processData(); + } + + function parseData(d) { + var res = []; + for (var i = 0; i < d.length; ++i) { + var s = $.extend(true, {}, options.series); + + if (d[i].data != null) { + s.data = d[i].data; // move the data instead of deep-copy + delete d[i].data; + + $.extend(true, s, d[i]); + + d[i].data = s.data; + } + else + s.data = d[i]; + res.push(s); + } + + return res; + } + + function axisNumber(obj, coord) { + var a = obj[coord + "axis"]; + if (typeof a == "object") // if we got a real axis, extract number + a = a.n; + if (typeof a != "number") + a = 1; // default to first axis + return a; + } + + function allAxes() { + // return flat array without annoying null entries + return $.grep(xaxes.concat(yaxes), function (a) { return a; }); + } + + function canvasToAxisCoords(pos) { + // return an object with x/y corresponding to all used axes + var res = {}, i, axis; + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) + res["x" + axis.n] = axis.c2p(pos.left); + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) + res["y" + axis.n] = axis.c2p(pos.top); + } + + if (res.x1 !== undefined) + res.x = res.x1; + if (res.y1 !== undefined) + res.y = res.y1; + + return res; + } + + function axisToCanvasCoords(pos) { + // get canvas coords from the first pair of x/y found in pos + var res = {}, i, axis, key; + + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) { + key = "x" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "x"; + + if (pos[key] != null) { + res.left = axis.p2c(pos[key]); + break; + } + } + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) { + key = "y" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "y"; + + if (pos[key] != null) { + res.top = axis.p2c(pos[key]); + break; + } + } + } + + return res; + } + + function getOrCreateAxis(axes, number) { + if (!axes[number - 1]) + axes[number - 1] = { + n: number, // save the number for future reference + direction: axes == xaxes ? "x" : "y", + options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) + }; + + return axes[number - 1]; + } + + function fillInSeriesOptions() { + + var neededColors = series.length, maxIndex = -1, i; + + // Subtract the number of series that already have fixed colors or + // color indexes from the number that we still need to generate. + + for (i = 0; i < series.length; ++i) { + var sc = series[i].color; + if (sc != null) { + neededColors--; + if (typeof sc == "number" && sc > maxIndex) { + maxIndex = sc; + } + } + } + + // If any of the series have fixed color indexes, then we need to + // generate at least as many colors as the highest index. + + if (neededColors <= maxIndex) { + neededColors = maxIndex + 1; + } + + // Generate all the colors, using first the option colors and then + // variations on those colors once they're exhausted. + + var c, colors = [], colorPool = options.colors, + colorPoolSize = colorPool.length, variation = 0; + + for (i = 0; i < neededColors; i++) { + + c = $.color.parse(colorPool[i % colorPoolSize] || "#666"); + + // Each time we exhaust the colors in the pool we adjust + // a scaling factor used to produce more variations on + // those colors. The factor alternates negative/positive + // to produce lighter/darker colors. + + // Reset the variation after every few cycles, or else + // it will end up producing only white or black colors. + + if (i % colorPoolSize == 0 && i) { + if (variation >= 0) { + if (variation < 0.5) { + variation = -variation - 0.2; + } else variation = 0; + } else variation = -variation; + } + + colors[i] = c.scale('rgb', 1 + variation); + } + + // Finalize the series options, filling in their colors + + var colori = 0, s; + for (i = 0; i < series.length; ++i) { + s = series[i]; + + // assign colors + if (s.color == null) { + s.color = colors[colori].toString(); + ++colori; + } + else if (typeof s.color == "number") + s.color = colors[s.color].toString(); + + // turn on lines automatically in case nothing is set + if (s.lines.show == null) { + var v, show = true; + for (v in s) + if (s[v] && s[v].show) { + show = false; + break; + } + if (show) + s.lines.show = true; + } + + // If nothing was provided for lines.zero, default it to match + // lines.fill, since areas by default should extend to zero. + + if (s.lines.zero == null) { + s.lines.zero = !!s.lines.fill; + } + + // setup axes + s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); + s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); + } + } + + function processData() { + var topSentry = Number.POSITIVE_INFINITY, + bottomSentry = Number.NEGATIVE_INFINITY, + fakeInfinity = Number.MAX_VALUE, + i, j, k, m, length, + s, points, ps, x, y, axis, val, f, p, + data, format; + + function updateAxis(axis, min, max) { + if (min < axis.datamin && min != -fakeInfinity) + axis.datamin = min; + if (max > axis.datamax && max != fakeInfinity) + axis.datamax = max; + } + + $.each(allAxes(), function (_, axis) { + // init axis + axis.datamin = topSentry; + axis.datamax = bottomSentry; + axis.used = false; + }); + + for (i = 0; i < series.length; ++i) { + s = series[i]; + s.datapoints = { points: [] }; + + executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); + } + + // first pass: clean and copy data + for (i = 0; i < series.length; ++i) { + s = series[i]; + + data = s.data; + format = s.datapoints.format; + + if (!format) { + format = []; + // find out how to copy + format.push({ x: true, number: true, required: true }); + format.push({ y: true, number: true, required: true }); + + if (s.bars.show || (s.lines.show && s.lines.fill)) { + var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); + format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); + if (s.bars.horizontal) { + delete format[format.length - 1].y; + format[format.length - 1].x = true; + } + } + + s.datapoints.format = format; + } + + if (s.datapoints.pointsize != null) + continue; // already filled in + + s.datapoints.pointsize = format.length; + + ps = s.datapoints.pointsize; + points = s.datapoints.points; + + var insertSteps = s.lines.show && s.lines.steps; + s.xaxis.used = s.yaxis.used = true; + + for (j = k = 0; j < data.length; ++j, k += ps) { + p = data[j]; + + var nullify = p == null; + if (!nullify) { + for (m = 0; m < ps; ++m) { + val = p[m]; + f = format[m]; + + if (f) { + if (f.number && val != null) { + val = +val; // convert to number + if (isNaN(val)) + val = null; + else if (val == Infinity) + val = fakeInfinity; + else if (val == -Infinity) + val = -fakeInfinity; + } + + if (val == null) { + if (f.required) + nullify = true; + + if (f.defaultValue != null) + val = f.defaultValue; + } + } + + points[k + m] = val; + } + } + + if (nullify) { + for (m = 0; m < ps; ++m) { + val = points[k + m]; + if (val != null) { + f = format[m]; + // extract min/max info + if (f.autoscale !== false) { + if (f.x) { + updateAxis(s.xaxis, val, val); + } + if (f.y) { + updateAxis(s.yaxis, val, val); + } + } + } + points[k + m] = null; + } + } + else { + // a little bit of line specific stuff that + // perhaps shouldn't be here, but lacking + // better means... + if (insertSteps && k > 0 + && points[k - ps] != null + && points[k - ps] != points[k] + && points[k - ps + 1] != points[k + 1]) { + // copy the point to make room for a middle point + for (m = 0; m < ps; ++m) + points[k + ps + m] = points[k + m]; + + // middle point has same y + points[k + 1] = points[k - ps + 1]; + + // we've added a point, better reflect that + k += ps; + } + } + } + } + + // give the hooks a chance to run + for (i = 0; i < series.length; ++i) { + s = series[i]; + + executeHooks(hooks.processDatapoints, [ s, s.datapoints]); + } + + // second pass: find datamax/datamin for auto-scaling + for (i = 0; i < series.length; ++i) { + s = series[i]; + points = s.datapoints.points; + ps = s.datapoints.pointsize; + format = s.datapoints.format; + + var xmin = topSentry, ymin = topSentry, + xmax = bottomSentry, ymax = bottomSentry; + + for (j = 0; j < points.length; j += ps) { + if (points[j] == null) + continue; + + for (m = 0; m < ps; ++m) { + val = points[j + m]; + f = format[m]; + if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) + continue; + + if (f.x) { + if (val < xmin) + xmin = val; + if (val > xmax) + xmax = val; + } + if (f.y) { + if (val < ymin) + ymin = val; + if (val > ymax) + ymax = val; + } + } + } + + if (s.bars.show) { + // make sure we got room for the bar on the dancing floor + var delta; + + switch (s.bars.align) { + case "left": + delta = 0; + break; + case "right": + delta = -s.bars.barWidth; + break; + default: + delta = -s.bars.barWidth / 2; + } + + if (s.bars.horizontal) { + ymin += delta; + ymax += delta + s.bars.barWidth; + } + else { + xmin += delta; + xmax += delta + s.bars.barWidth; + } + } + + updateAxis(s.xaxis, xmin, xmax); + updateAxis(s.yaxis, ymin, ymax); + } + + $.each(allAxes(), function (_, axis) { + if (axis.datamin == topSentry) + axis.datamin = null; + if (axis.datamax == bottomSentry) + axis.datamax = null; + }); + } + + function setupCanvases() { + + // Make sure the placeholder is clear of everything except canvases + // from a previous plot in this container that we'll try to re-use. + + placeholder.css("padding", 0) // padding messes up the positioning + .children().filter(function(){ + return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); + }).remove(); + + if (placeholder.css("position") == 'static') + placeholder.css("position", "relative"); // for positioning labels and overlay + + surface = new Canvas("flot-base", placeholder); + overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features + + ctx = surface.context; + octx = overlay.context; + + // define which element we're listening for events on + eventHolder = $(overlay.element).unbind(); + + // If we're re-using a plot object, shut down the old one + + var existing = placeholder.data("plot"); + + if (existing) { + existing.shutdown(); + overlay.clear(); + } + + // save in case we get replotted + placeholder.data("plot", plot); + } + + function bindEvents() { + // bind events + if (options.grid.hoverable) { + eventHolder.mousemove(onMouseMove); + + // Use bind, rather than .mouseleave, because we officially + // still support jQuery 1.2.6, which doesn't define a shortcut + // for mouseenter or mouseleave. This was a bug/oversight that + // was fixed somewhere around 1.3.x. We can return to using + // .mouseleave when we drop support for 1.2.6. + + eventHolder.bind("mouseleave", onMouseLeave); + } + + if (options.grid.clickable) + eventHolder.click(onClick); + + executeHooks(hooks.bindEvents, [eventHolder]); + } + + function shutdown() { + if (redrawTimeout) + clearTimeout(redrawTimeout); + + eventHolder.unbind("mousemove", onMouseMove); + eventHolder.unbind("mouseleave", onMouseLeave); + eventHolder.unbind("click", onClick); + + executeHooks(hooks.shutdown, [eventHolder]); + } + + function setTransformationHelpers(axis) { + // set helper functions on the axis, assumes plot area + // has been computed already + + function identity(x) { return x; } + + var s, m, t = axis.options.transform || identity, + it = axis.options.inverseTransform; + + // precompute how much the axis is scaling a point + // in canvas space + if (axis.direction == "x") { + s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); + m = Math.min(t(axis.max), t(axis.min)); + } + else { + s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); + s = -s; + m = Math.max(t(axis.max), t(axis.min)); + } + + // data point to canvas coordinate + if (t == identity) // slight optimization + axis.p2c = function (p) { return (p - m) * s; }; + else + axis.p2c = function (p) { return (t(p) - m) * s; }; + // canvas coordinate to data point + if (!it) + axis.c2p = function (c) { return m + c / s; }; + else + axis.c2p = function (c) { return it(m + c / s); }; + } + + function measureTickLabels(axis) { + + var opts = axis.options, + ticks = axis.ticks || [], + labelWidth = opts.labelWidth || 0, + labelHeight = opts.labelHeight || 0, + maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null), + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = opts.font || "flot-tick-label tickLabel"; + + for (var i = 0; i < ticks.length; ++i) { + + var t = ticks[i]; + + if (!t.label) + continue; + + var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); + + labelWidth = Math.max(labelWidth, info.width); + labelHeight = Math.max(labelHeight, info.height); + } + + axis.labelWidth = opts.labelWidth || labelWidth; + axis.labelHeight = opts.labelHeight || labelHeight; + } + + function allocateAxisBoxFirstPhase(axis) { + // find the bounding box of the axis by looking at label + // widths/heights and ticks, make room by diminishing the + // plotOffset; this first phase only looks at one + // dimension per axis, the other dimension depends on the + // other axes so will have to wait + + var lw = axis.labelWidth, + lh = axis.labelHeight, + pos = axis.options.position, + isXAxis = axis.direction === "x", + tickLength = axis.options.tickLength, + axisMargin = options.grid.axisMargin, + padding = options.grid.labelMargin, + innermost = true, + outermost = true, + first = true, + found = false; + + // Determine the axis's position in its direction and on its side + + $.each(isXAxis ? xaxes : yaxes, function(i, a) { + if (a && (a.show || a.reserveSpace)) { + if (a === axis) { + found = true; + } else if (a.options.position === pos) { + if (found) { + outermost = false; + } else { + innermost = false; + } + } + if (!found) { + first = false; + } + } + }); + + // The outermost axis on each side has no margin + + if (outermost) { + axisMargin = 0; + } + + // The ticks for the first axis in each direction stretch across + + if (tickLength == null) { + tickLength = first ? "full" : 5; + } + + if (!isNaN(+tickLength)) + padding += +tickLength; + + if (isXAxis) { + lh += padding; + + if (pos == "bottom") { + plotOffset.bottom += lh + axisMargin; + axis.box = { top: surface.height - plotOffset.bottom, height: lh }; + } + else { + axis.box = { top: plotOffset.top + axisMargin, height: lh }; + plotOffset.top += lh + axisMargin; + } + } + else { + lw += padding; + + if (pos == "left") { + axis.box = { left: plotOffset.left + axisMargin, width: lw }; + plotOffset.left += lw + axisMargin; + } + else { + plotOffset.right += lw + axisMargin; + axis.box = { left: surface.width - plotOffset.right, width: lw }; + } + } + + // save for future reference + axis.position = pos; + axis.tickLength = tickLength; + axis.box.padding = padding; + axis.innermost = innermost; + } + + function allocateAxisBoxSecondPhase(axis) { + // now that all axis boxes have been placed in one + // dimension, we can set the remaining dimension coordinates + if (axis.direction == "x") { + axis.box.left = plotOffset.left - axis.labelWidth / 2; + axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; + } + else { + axis.box.top = plotOffset.top - axis.labelHeight / 2; + axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; + } + } + + function adjustLayoutForThingsStickingOut() { + // possibly adjust plot offset to ensure everything stays + // inside the canvas and isn't clipped off + + var minMargin = options.grid.minBorderMargin, + axis, i; + + // check stuff from the plot (FIXME: this should just read + // a value from the series, otherwise it's impossible to + // customize) + if (minMargin == null) { + minMargin = 0; + for (i = 0; i < series.length; ++i) + minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); + } + + var margins = { + left: minMargin, + right: minMargin, + top: minMargin, + bottom: minMargin + }; + + // check axis labels, note we don't check the actual + // labels but instead use the overall width/height to not + // jump as much around with replots + $.each(allAxes(), function (_, axis) { + if (axis.reserveSpace && axis.ticks && axis.ticks.length) { + if (axis.direction === "x") { + margins.left = Math.max(margins.left, axis.labelWidth / 2); + margins.right = Math.max(margins.right, axis.labelWidth / 2); + } else { + margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); + margins.top = Math.max(margins.top, axis.labelHeight / 2); + } + } + }); + + plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); + plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); + plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); + plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); + } + + function setupGrid() { + var i, axes = allAxes(), showGrid = options.grid.show; + + // Initialize the plot's offset from the edge of the canvas + + for (var a in plotOffset) { + var margin = options.grid.margin || 0; + plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0; + } + + executeHooks(hooks.processOffset, [plotOffset]); + + // If the grid is visible, add its border width to the offset + + for (var a in plotOffset) { + if(typeof(options.grid.borderWidth) == "object") { + plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; + } + else { + plotOffset[a] += showGrid ? options.grid.borderWidth : 0; + } + } + + $.each(axes, function (_, axis) { + var axisOpts = axis.options; + axis.show = axisOpts.show == null ? axis.used : axisOpts.show; + axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace; + setRange(axis); + }); + + if (showGrid) { + + var allocatedAxes = $.grep(axes, function (axis) { + return axis.show || axis.reserveSpace; + }); + + $.each(allocatedAxes, function (_, axis) { + // make the ticks + setupTickGeneration(axis); + setTicks(axis); + snapRangeToTicks(axis, axis.ticks); + // find labelWidth/Height for axis + measureTickLabels(axis); + }); + + // with all dimensions calculated, we can compute the + // axis bounding boxes, start from the outside + // (reverse order) + for (i = allocatedAxes.length - 1; i >= 0; --i) + allocateAxisBoxFirstPhase(allocatedAxes[i]); + + // make sure we've got enough space for things that + // might stick out + adjustLayoutForThingsStickingOut(); + + $.each(allocatedAxes, function (_, axis) { + allocateAxisBoxSecondPhase(axis); + }); + } + + plotWidth = surface.width - plotOffset.left - plotOffset.right; + plotHeight = surface.height - plotOffset.bottom - plotOffset.top; + + // now we got the proper plot dimensions, we can compute the scaling + $.each(axes, function (_, axis) { + setTransformationHelpers(axis); + }); + + if (showGrid) { + drawAxisLabels(); + } + + insertLegend(); + } + + function setRange(axis) { + var opts = axis.options, + min = +(opts.min != null ? opts.min : axis.datamin), + max = +(opts.max != null ? opts.max : axis.datamax), + delta = max - min; + + if (delta == 0.0) { + // degenerate case + var widen = max == 0 ? 1 : 0.01; + + if (opts.min == null) + min -= widen; + // always widen max if we couldn't widen min to ensure we + // don't fall into min == max which doesn't work + if (opts.max == null || opts.min != null) + max += widen; + } + else { + // consider autoscaling + var margin = opts.autoscaleMargin; + if (margin != null) { + if (opts.min == null) { + min -= delta * margin; + // make sure we don't go below zero if all values + // are positive + if (min < 0 && axis.datamin != null && axis.datamin >= 0) + min = 0; + } + if (opts.max == null) { + max += delta * margin; + if (max > 0 && axis.datamax != null && axis.datamax <= 0) + max = 0; + } + } + } + axis.min = min; + axis.max = max; + } + + function setupTickGeneration(axis) { + var opts = axis.options; + + // estimate number of ticks + var noTicks; + if (typeof opts.ticks == "number" && opts.ticks > 0) + noTicks = opts.ticks; + else + // heuristic based on the model a*sqrt(x) fitted to + // some data points that seemed reasonable + noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); + + var delta = (axis.max - axis.min) / noTicks, + dec = -Math.floor(Math.log(delta) / Math.LN10), + maxDec = opts.tickDecimals; + + if (maxDec != null && dec > maxDec) { + dec = maxDec; + } + + var magn = Math.pow(10, -dec), + norm = delta / magn, // norm is between 1.0 and 10.0 + size; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + // special case for 2.5, requires an extra decimal + if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { + size = 2.5; + ++dec; + } + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + + if (opts.minTickSize != null && size < opts.minTickSize) { + size = opts.minTickSize; + } + + axis.delta = delta; + axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); + axis.tickSize = opts.tickSize || size; + + // Time mode was moved to a plug-in in 0.8, and since so many people use it + // we'll add an especially friendly reminder to make sure they included it. + + if (opts.mode == "time" && !axis.tickGenerator) { + throw new Error("Time mode requires the flot.time plugin."); + } + + // Flot supports base-10 axes; any other mode else is handled by a plug-in, + // like flot.time.js. + + if (!axis.tickGenerator) { + + axis.tickGenerator = function (axis) { + + var ticks = [], + start = floorInBase(axis.min, axis.tickSize), + i = 0, + v = Number.NaN, + prev; + + do { + prev = v; + v = start + i * axis.tickSize; + ticks.push(v); + ++i; + } while (v < axis.max && v != prev); + return ticks; + }; + + axis.tickFormatter = function (value, axis) { + + var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; + var formatted = "" + Math.round(value * factor) / factor; + + // If tickDecimals was specified, ensure that we have exactly that + // much precision; otherwise default to the value's own precision. + + if (axis.tickDecimals != null) { + var decimal = formatted.indexOf("."); + var precision = decimal == -1 ? 0 : formatted.length - decimal - 1; + if (precision < axis.tickDecimals) { + return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); + } + } + + return formatted; + }; + } + + if ($.isFunction(opts.tickFormatter)) + axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; + + if (opts.alignTicksWithAxis != null) { + var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; + if (otherAxis && otherAxis.used && otherAxis != axis) { + // consider snapping min/max to outermost nice ticks + var niceTicks = axis.tickGenerator(axis); + if (niceTicks.length > 0) { + if (opts.min == null) + axis.min = Math.min(axis.min, niceTicks[0]); + if (opts.max == null && niceTicks.length > 1) + axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); + } + + axis.tickGenerator = function (axis) { + // copy ticks, scaled to this axis + var ticks = [], v, i; + for (i = 0; i < otherAxis.ticks.length; ++i) { + v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); + v = axis.min + v * (axis.max - axis.min); + ticks.push(v); + } + return ticks; + }; + + // we might need an extra decimal since forced + // ticks don't necessarily fit naturally + if (!axis.mode && opts.tickDecimals == null) { + var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), + ts = axis.tickGenerator(axis); + + // only proceed if the tick interval rounded + // with an extra decimal doesn't give us a + // zero at end + if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) + axis.tickDecimals = extraDec; + } + } + } + } + + function setTicks(axis) { + var oticks = axis.options.ticks, ticks = []; + if (oticks == null || (typeof oticks == "number" && oticks > 0)) + ticks = axis.tickGenerator(axis); + else if (oticks) { + if ($.isFunction(oticks)) + // generate the ticks + ticks = oticks(axis); + else + ticks = oticks; + } + + // clean up/labelify the supplied ticks, copy them over + var i, v; + axis.ticks = []; + for (i = 0; i < ticks.length; ++i) { + var label = null; + var t = ticks[i]; + if (typeof t == "object") { + v = +t[0]; + if (t.length > 1) + label = t[1]; + } + else + v = +t; + if (label == null) + label = axis.tickFormatter(v, axis); + if (!isNaN(v)) + axis.ticks.push({ v: v, label: label }); + } + } + + function snapRangeToTicks(axis, ticks) { + if (axis.options.autoscaleMargin && ticks.length > 0) { + // snap to ticks + if (axis.options.min == null) + axis.min = Math.min(axis.min, ticks[0].v); + if (axis.options.max == null && ticks.length > 1) + axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); + } + } + + function draw() { + + surface.clear(); + + executeHooks(hooks.drawBackground, [ctx]); + + var grid = options.grid; + + // draw background, if any + if (grid.show && grid.backgroundColor) + drawBackground(); + + if (grid.show && !grid.aboveData) { + drawGrid(); + } + + for (var i = 0; i < series.length; ++i) { + executeHooks(hooks.drawSeries, [ctx, series[i]]); + drawSeries(series[i]); + } + + executeHooks(hooks.draw, [ctx]); + + if (grid.show && grid.aboveData) { + drawGrid(); + } + + surface.render(); + + // A draw implies that either the axes or data have changed, so we + // should probably update the overlay highlights as well. + + triggerRedrawOverlay(); + } + + function extractRange(ranges, coord) { + var axis, from, to, key, axes = allAxes(); + + for (var i = 0; i < axes.length; ++i) { + axis = axes[i]; + if (axis.direction == coord) { + key = coord + axis.n + "axis"; + if (!ranges[key] && axis.n == 1) + key = coord + "axis"; // support x1axis as xaxis + if (ranges[key]) { + from = ranges[key].from; + to = ranges[key].to; + break; + } + } + } + + // backwards-compat stuff - to be removed in future + if (!ranges[key]) { + axis = coord == "x" ? xaxes[0] : yaxes[0]; + from = ranges[coord + "1"]; + to = ranges[coord + "2"]; + } + + // auto-reverse as an added bonus + if (from != null && to != null && from > to) { + var tmp = from; + from = to; + to = tmp; + } + + return { from: from, to: to, axis: axis }; + } + + function drawBackground() { + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); + ctx.fillRect(0, 0, plotWidth, plotHeight); + ctx.restore(); + } + + function drawGrid() { + var i, axes, bw, bc; + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + // draw markings + var markings = options.grid.markings; + if (markings) { + if ($.isFunction(markings)) { + axes = plot.getAxes(); + // xmin etc. is backwards compatibility, to be + // removed in the future + axes.xmin = axes.xaxis.min; + axes.xmax = axes.xaxis.max; + axes.ymin = axes.yaxis.min; + axes.ymax = axes.yaxis.max; + + markings = markings(axes); + } + + for (i = 0; i < markings.length; ++i) { + var m = markings[i], + xrange = extractRange(m, "x"), + yrange = extractRange(m, "y"); + + // fill in missing + if (xrange.from == null) + xrange.from = xrange.axis.min; + if (xrange.to == null) + xrange.to = xrange.axis.max; + if (yrange.from == null) + yrange.from = yrange.axis.min; + if (yrange.to == null) + yrange.to = yrange.axis.max; + + // clip + if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || + yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) + continue; + + xrange.from = Math.max(xrange.from, xrange.axis.min); + xrange.to = Math.min(xrange.to, xrange.axis.max); + yrange.from = Math.max(yrange.from, yrange.axis.min); + yrange.to = Math.min(yrange.to, yrange.axis.max); + + var xequal = xrange.from === xrange.to, + yequal = yrange.from === yrange.to; + + if (xequal && yequal) { + continue; + } + + // then draw + xrange.from = Math.floor(xrange.axis.p2c(xrange.from)); + xrange.to = Math.floor(xrange.axis.p2c(xrange.to)); + yrange.from = Math.floor(yrange.axis.p2c(yrange.from)); + yrange.to = Math.floor(yrange.axis.p2c(yrange.to)); + + if (xequal || yequal) { + var lineWidth = m.lineWidth || options.grid.markingsLineWidth, + subPixel = lineWidth % 2 ? 0.5 : 0; + ctx.beginPath(); + ctx.strokeStyle = m.color || options.grid.markingsColor; + ctx.lineWidth = lineWidth; + if (xequal) { + ctx.moveTo(xrange.to + subPixel, yrange.from); + ctx.lineTo(xrange.to + subPixel, yrange.to); + } else { + ctx.moveTo(xrange.from, yrange.to + subPixel); + ctx.lineTo(xrange.to, yrange.to + subPixel); + } + ctx.stroke(); + } else { + ctx.fillStyle = m.color || options.grid.markingsColor; + ctx.fillRect(xrange.from, yrange.to, + xrange.to - xrange.from, + yrange.from - yrange.to); + } + } + } + + // draw the ticks + axes = allAxes(); + bw = options.grid.borderWidth; + + for (var j = 0; j < axes.length; ++j) { + var axis = axes[j], box = axis.box, + t = axis.tickLength, x, y, xoff, yoff; + if (!axis.show || axis.ticks.length == 0) + continue; + + ctx.lineWidth = 1; + + // find the edges + if (axis.direction == "x") { + x = 0; + if (t == "full") + y = (axis.position == "top" ? 0 : plotHeight); + else + y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); + } + else { + y = 0; + if (t == "full") + x = (axis.position == "left" ? 0 : plotWidth); + else + x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); + } + + // draw tick bar + if (!axis.innermost) { + ctx.strokeStyle = axis.options.color; + ctx.beginPath(); + xoff = yoff = 0; + if (axis.direction == "x") + xoff = plotWidth + 1; + else + yoff = plotHeight + 1; + + if (ctx.lineWidth == 1) { + if (axis.direction == "x") { + y = Math.floor(y) + 0.5; + } else { + x = Math.floor(x) + 0.5; + } + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + ctx.stroke(); + } + + // draw ticks + + ctx.strokeStyle = axis.options.tickColor; + + ctx.beginPath(); + for (i = 0; i < axis.ticks.length; ++i) { + var v = axis.ticks[i].v; + + xoff = yoff = 0; + + if (isNaN(v) || v < axis.min || v > axis.max + // skip those lying on the axes if we got a border + || (t == "full" + && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) + && (v == axis.min || v == axis.max))) + continue; + + if (axis.direction == "x") { + x = axis.p2c(v); + yoff = t == "full" ? -plotHeight : t; + + if (axis.position == "top") + yoff = -yoff; + } + else { + y = axis.p2c(v); + xoff = t == "full" ? -plotWidth : t; + + if (axis.position == "left") + xoff = -xoff; + } + + if (ctx.lineWidth == 1) { + if (axis.direction == "x") + x = Math.floor(x) + 0.5; + else + y = Math.floor(y) + 0.5; + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + } + + ctx.stroke(); + } + + + // draw border + if (bw) { + // If either borderWidth or borderColor is an object, then draw the border + // line by line instead of as one rectangle + bc = options.grid.borderColor; + if(typeof bw == "object" || typeof bc == "object") { + if (typeof bw !== "object") { + bw = {top: bw, right: bw, bottom: bw, left: bw}; + } + if (typeof bc !== "object") { + bc = {top: bc, right: bc, bottom: bc, left: bc}; + } + + if (bw.top > 0) { + ctx.strokeStyle = bc.top; + ctx.lineWidth = bw.top; + ctx.beginPath(); + ctx.moveTo(0 - bw.left, 0 - bw.top/2); + ctx.lineTo(plotWidth, 0 - bw.top/2); + ctx.stroke(); + } + + if (bw.right > 0) { + ctx.strokeStyle = bc.right; + ctx.lineWidth = bw.right; + ctx.beginPath(); + ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); + ctx.lineTo(plotWidth + bw.right / 2, plotHeight); + ctx.stroke(); + } + + if (bw.bottom > 0) { + ctx.strokeStyle = bc.bottom; + ctx.lineWidth = bw.bottom; + ctx.beginPath(); + ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); + ctx.lineTo(0, plotHeight + bw.bottom / 2); + ctx.stroke(); + } + + if (bw.left > 0) { + ctx.strokeStyle = bc.left; + ctx.lineWidth = bw.left; + ctx.beginPath(); + ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom); + ctx.lineTo(0- bw.left/2, 0); + ctx.stroke(); + } + } + else { + ctx.lineWidth = bw; + ctx.strokeStyle = options.grid.borderColor; + ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); + } + } + + ctx.restore(); + } + + function drawAxisLabels() { + + $.each(allAxes(), function (_, axis) { + var box = axis.box, + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = axis.options.font || "flot-tick-label tickLabel", + tick, x, y, halign, valign; + + // Remove text before checking for axis.show and ticks.length; + // otherwise plugins, like flot-tickrotor, that draw their own + // tick labels will end up with both theirs and the defaults. + + surface.removeText(layer); + + if (!axis.show || axis.ticks.length == 0) + return; + + for (var i = 0; i < axis.ticks.length; ++i) { + + tick = axis.ticks[i]; + if (!tick.label || tick.v < axis.min || tick.v > axis.max) + continue; + + if (axis.direction == "x") { + halign = "center"; + x = plotOffset.left + axis.p2c(tick.v); + if (axis.position == "bottom") { + y = box.top + box.padding; + } else { + y = box.top + box.height - box.padding; + valign = "bottom"; + } + } else { + valign = "middle"; + y = plotOffset.top + axis.p2c(tick.v); + if (axis.position == "left") { + x = box.left + box.width - box.padding; + halign = "right"; + } else { + x = box.left + box.padding; + } + } + + surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); + } + }); + } + + function drawSeries(series) { + if (series.lines.show) + drawSeriesLines(series); + if (series.bars.show) + drawSeriesBars(series); + if (series.points.show) + drawSeriesPoints(series); + } + + function drawSeriesLines(series) { + function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { + var points = datapoints.points, + ps = datapoints.pointsize, + prevx = null, prevy = null; + + ctx.beginPath(); + for (var i = ps; i < points.length; i += ps) { + var x1 = points[i - ps], y1 = points[i - ps + 1], + x2 = points[i], y2 = points[i + 1]; + + if (x1 == null || x2 == null) + continue; + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min) { + if (y2 < axisy.min) + continue; // line segment is outside + // compute new intersection point + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min) { + if (y1 < axisy.min) + continue; + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max) { + if (y2 > axisy.max) + continue; + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max) { + if (y1 > axisy.max) + continue; + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (x1 != prevx || y1 != prevy) + ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); + + prevx = x2; + prevy = y2; + ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); + } + ctx.stroke(); + } + + function plotLineArea(datapoints, axisx, axisy) { + var points = datapoints.points, + ps = datapoints.pointsize, + bottom = Math.min(Math.max(0, axisy.min), axisy.max), + i = 0, top, areaOpen = false, + ypos = 1, segmentStart = 0, segmentEnd = 0; + + // we process each segment in two turns, first forward + // direction to sketch out top, then once we hit the + // end we go backwards to sketch the bottom + while (true) { + if (ps > 0 && i > points.length + ps) + break; + + i += ps; // ps is negative if going backwards + + var x1 = points[i - ps], + y1 = points[i - ps + ypos], + x2 = points[i], y2 = points[i + ypos]; + + if (areaOpen) { + if (ps > 0 && x1 != null && x2 == null) { + // at turning point + segmentEnd = i; + ps = -ps; + ypos = 2; + continue; + } + + if (ps < 0 && i == segmentStart + ps) { + // done with the reverse sweep + ctx.fill(); + areaOpen = false; + ps = -ps; + ypos = 1; + i = segmentStart = segmentEnd + ps; + continue; + } + } + + if (x1 == null || x2 == null) + continue; + + // clip x values + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (!areaOpen) { + // open area + ctx.beginPath(); + ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); + areaOpen = true; + } + + // now first check the case where both is outside + if (y1 >= axisy.max && y2 >= axisy.max) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); + continue; + } + else if (y1 <= axisy.min && y2 <= axisy.min) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); + continue; + } + + // else it's a bit more complicated, there might + // be a flat maxed out rectangle first, then a + // triangular cutout or reverse; to find these + // keep track of the current x values + var x1old = x1, x2old = x2; + + // clip the y values, without shortcutting, we + // go through all cases in turn + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // if the x value was changed we got a rectangle + // to fill + if (x1 != x1old) { + ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); + // it goes to (x1, y1), but we fill that below + } + + // fill triangular section, this sometimes result + // in redundant points if (x1, y1) hasn't changed + // from previous line to, but we just ignore that + ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + + // fill the other rectangle if it's there + if (x2 != x2old) { + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); + } + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + ctx.lineJoin = "round"; + + var lw = series.lines.lineWidth, + sw = series.shadowSize; + // FIXME: consider another form of shadow when filling is turned on + if (lw > 0 && sw > 0) { + // draw shadow as a thick and thin line with transparency + ctx.lineWidth = sw; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + // position shadow at angle from the mid of line + var angle = Math.PI/18; + plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); + ctx.lineWidth = sw/2; + plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); + if (fillStyle) { + ctx.fillStyle = fillStyle; + plotLineArea(series.datapoints, series.xaxis, series.yaxis); + } + + if (lw > 0) + plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); + ctx.restore(); + } + + function drawSeriesPoints(series) { + function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { + var points = datapoints.points, ps = datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + var x = points[i], y = points[i + 1]; + if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + continue; + + ctx.beginPath(); + x = axisx.p2c(x); + y = axisy.p2c(y) + offset; + if (symbol == "circle") + ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); + else + symbol(ctx, x, y, radius, shadow); + ctx.closePath(); + + if (fillStyle) { + ctx.fillStyle = fillStyle; + ctx.fill(); + } + ctx.stroke(); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + var lw = series.points.lineWidth, + sw = series.shadowSize, + radius = series.points.radius, + symbol = series.points.symbol; + + // If the user sets the line width to 0, we change it to a very + // small value. A line width of 0 seems to force the default of 1. + // Doing the conditional here allows the shadow setting to still be + // optional even with a lineWidth of 0. + + if( lw == 0 ) + lw = 0.0001; + + if (lw > 0 && sw > 0) { + // draw shadow in two steps + var w = sw / 2; + ctx.lineWidth = w; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + plotPoints(series.datapoints, radius, null, w + w/2, true, + series.xaxis, series.yaxis, symbol); + + ctx.strokeStyle = "rgba(0,0,0,0.2)"; + plotPoints(series.datapoints, radius, null, w/2, true, + series.xaxis, series.yaxis, symbol); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + plotPoints(series.datapoints, radius, + getFillStyle(series.points, series.color), 0, false, + series.xaxis, series.yaxis, symbol); + ctx.restore(); + } + + function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { + var left, right, bottom, top, + drawLeft, drawRight, drawTop, drawBottom, + tmp; + + // in horizontal mode, we start the bar from the left + // instead of from the bottom so it appears to be + // horizontal rather than vertical + if (horizontal) { + drawBottom = drawRight = drawTop = true; + drawLeft = false; + left = b; + right = x; + top = y + barLeft; + bottom = y + barRight; + + // account for negative bars + if (right < left) { + tmp = right; + right = left; + left = tmp; + drawLeft = true; + drawRight = false; + } + } + else { + drawLeft = drawRight = drawTop = true; + drawBottom = false; + left = x + barLeft; + right = x + barRight; + bottom = b; + top = y; + + // account for negative bars + if (top < bottom) { + tmp = top; + top = bottom; + bottom = tmp; + drawBottom = true; + drawTop = false; + } + } + + // clip + if (right < axisx.min || left > axisx.max || + top < axisy.min || bottom > axisy.max) + return; + + if (left < axisx.min) { + left = axisx.min; + drawLeft = false; + } + + if (right > axisx.max) { + right = axisx.max; + drawRight = false; + } + + if (bottom < axisy.min) { + bottom = axisy.min; + drawBottom = false; + } + + if (top > axisy.max) { + top = axisy.max; + drawTop = false; + } + + left = axisx.p2c(left); + bottom = axisy.p2c(bottom); + right = axisx.p2c(right); + top = axisy.p2c(top); + + // fill the bar + if (fillStyleCallback) { + c.fillStyle = fillStyleCallback(bottom, top); + c.fillRect(left, top, right - left, bottom - top) + } + + // draw outline + if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { + c.beginPath(); + + // FIXME: inline moveTo is buggy with excanvas + c.moveTo(left, bottom); + if (drawLeft) + c.lineTo(left, top); + else + c.moveTo(left, top); + if (drawTop) + c.lineTo(right, top); + else + c.moveTo(right, top); + if (drawRight) + c.lineTo(right, bottom); + else + c.moveTo(right, bottom); + if (drawBottom) + c.lineTo(left, bottom); + else + c.moveTo(left, bottom); + c.stroke(); + } + } + + function drawSeriesBars(series) { + function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { + var points = datapoints.points, ps = datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + if (points[i] == null) + continue; + drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + // FIXME: figure out a way to add shadows (for instance along the right edge) + ctx.lineWidth = series.bars.lineWidth; + ctx.strokeStyle = series.color; + + var barLeft; + + switch (series.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -series.bars.barWidth; + break; + default: + barLeft = -series.bars.barWidth / 2; + } + + var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; + plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis); + ctx.restore(); + } + + function getFillStyle(filloptions, seriesColor, bottom, top) { + var fill = filloptions.fill; + if (!fill) + return null; + + if (filloptions.fillColor) + return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); + + var c = $.color.parse(seriesColor); + c.a = typeof fill == "number" ? fill : 0.4; + c.normalize(); + return c.toString(); + } + + function insertLegend() { + + if (options.legend.container != null) { + $(options.legend.container).html(""); + } else { + placeholder.find(".legend").remove(); + } + + if (!options.legend.show) { + return; + } + + var fragments = [], entries = [], rowStarted = false, + lf = options.legend.labelFormatter, s, label; + + // Build a list of legend entries, with each having a label and a color + + for (var i = 0; i < series.length; ++i) { + s = series[i]; + if (s.label) { + label = lf ? lf(s.label, s) : s.label; + if (label) { + entries.push({ + label: label, + color: s.color + }); + } + } + } + + // Sort the legend using either the default or a custom comparator + + if (options.legend.sorted) { + if ($.isFunction(options.legend.sorted)) { + entries.sort(options.legend.sorted); + } else if (options.legend.sorted == "reverse") { + entries.reverse(); + } else { + var ascending = options.legend.sorted != "descending"; + entries.sort(function(a, b) { + return a.label == b.label ? 0 : ( + (a.label < b.label) != ascending ? 1 : -1 // Logical XOR + ); + }); + } + } + + // Generate markup for the list of entries, in their final order + + for (var i = 0; i < entries.length; ++i) { + + var entry = entries[i]; + + if (i % options.legend.noColumns == 0) { + if (rowStarted) + fragments.push(''); + fragments.push(''); + rowStarted = true; + } + + fragments.push( + '
    ' + + '' + entry.label + '' + ); + } + + if (rowStarted) + fragments.push(''); + + if (fragments.length == 0) + return; + + var table = '' + fragments.join("") + '
    '; + if (options.legend.container != null) + $(options.legend.container).html(table); + else { + var pos = "", + p = options.legend.position, + m = options.legend.margin; + if (m[0] == null) + m = [m, m]; + if (p.charAt(0) == "n") + pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; + else if (p.charAt(0) == "s") + pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; + if (p.charAt(1) == "e") + pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; + else if (p.charAt(1) == "w") + pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; + var legend = $('
    ' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
    ').appendTo(placeholder); + if (options.legend.backgroundOpacity != 0.0) { + // put in the transparent background + // separately to avoid blended labels and + // label boxes + var c = options.legend.backgroundColor; + if (c == null) { + c = options.grid.backgroundColor; + if (c && typeof c == "string") + c = $.color.parse(c); + else + c = $.color.extract(legend, 'background-color'); + c.a = 1; + c = c.toString(); + } + var div = legend.children(); + $('
    ').prependTo(legend).css('opacity', options.legend.backgroundOpacity); + } + } + } + + + // interactive features + + var highlights = [], + redrawTimeout = null; + + // returns the data item the mouse is over, or null if none is found + function findNearbyItem(mouseX, mouseY, seriesFilter) { + var maxDistance = options.grid.mouseActiveRadius, + smallestDistance = maxDistance * maxDistance + 1, + item = null, foundPoint = false, i, j, ps; + + for (i = series.length - 1; i >= 0; --i) { + if (!seriesFilter(series[i])) + continue; + + var s = series[i], + axisx = s.xaxis, + axisy = s.yaxis, + points = s.datapoints.points, + mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster + my = axisy.c2p(mouseY), + maxx = maxDistance / axisx.scale, + maxy = maxDistance / axisy.scale; + + ps = s.datapoints.pointsize; + // with inverse transforms, we can't use the maxx/maxy + // optimization, sadly + if (axisx.options.inverseTransform) + maxx = Number.MAX_VALUE; + if (axisy.options.inverseTransform) + maxy = Number.MAX_VALUE; + + if (s.lines.show || s.points.show) { + for (j = 0; j < points.length; j += ps) { + var x = points[j], y = points[j + 1]; + if (x == null) + continue; + + // For points and lines, the cursor must be within a + // certain distance to the data point + if (x - mx > maxx || x - mx < -maxx || + y - my > maxy || y - my < -maxy) + continue; + + // We have to calculate distances in pixels, not in + // data units, because the scales of the axes may be different + var dx = Math.abs(axisx.p2c(x) - mouseX), + dy = Math.abs(axisy.p2c(y) - mouseY), + dist = dx * dx + dy * dy; // we save the sqrt + + // use <= to ensure last point takes precedence + // (last generally means on top of) + if (dist < smallestDistance) { + smallestDistance = dist; + item = [i, j / ps]; + } + } + } + + if (s.bars.show && !item) { // no other point can be nearby + + var barLeft, barRight; + + switch (s.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -s.bars.barWidth; + break; + default: + barLeft = -s.bars.barWidth / 2; + } + + barRight = barLeft + s.bars.barWidth; + + for (j = 0; j < points.length; j += ps) { + var x = points[j], y = points[j + 1], b = points[j + 2]; + if (x == null) + continue; + + // for a bar graph, the cursor must be inside the bar + if (series[i].bars.horizontal ? + (mx <= Math.max(b, x) && mx >= Math.min(b, x) && + my >= y + barLeft && my <= y + barRight) : + (mx >= x + barLeft && mx <= x + barRight && + my >= Math.min(b, y) && my <= Math.max(b, y))) + item = [i, j / ps]; + } + } + } + + if (item) { + i = item[0]; + j = item[1]; + ps = series[i].datapoints.pointsize; + + return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), + dataIndex: j, + series: series[i], + seriesIndex: i }; + } + + return null; + } + + function onMouseMove(e) { + if (options.grid.hoverable) + triggerClickHoverEvent("plothover", e, + function (s) { return s["hoverable"] != false; }); + } + + function onMouseLeave(e) { + if (options.grid.hoverable) + triggerClickHoverEvent("plothover", e, + function (s) { return false; }); + } + + function onClick(e) { + triggerClickHoverEvent("plotclick", e, + function (s) { return s["clickable"] != false; }); + } + + // trigger click or hover event (they send the same parameters + // so we share their code) + function triggerClickHoverEvent(eventname, event, seriesFilter) { + var offset = eventHolder.offset(), + canvasX = event.pageX - offset.left - plotOffset.left, + canvasY = event.pageY - offset.top - plotOffset.top, + pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); + + pos.pageX = event.pageX; + pos.pageY = event.pageY; + + var item = findNearbyItem(canvasX, canvasY, seriesFilter); + + if (item) { + // fill in mouse pos for any listeners out there + item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10); + item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10); + } + + if (options.grid.autoHighlight) { + // clear auto-highlights + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.auto == eventname && + !(item && h.series == item.series && + h.point[0] == item.datapoint[0] && + h.point[1] == item.datapoint[1])) + unhighlight(h.series, h.point); + } + + if (item) + highlight(item.series, item.datapoint, eventname); + } + + placeholder.trigger(eventname, [ pos, item ]); + } + + function triggerRedrawOverlay() { + var t = options.interaction.redrawOverlayInterval; + if (t == -1) { // skip event queue + drawOverlay(); + return; + } + + if (!redrawTimeout) + redrawTimeout = setTimeout(drawOverlay, t); + } + + function drawOverlay() { + redrawTimeout = null; + + // draw highlights + octx.save(); + overlay.clear(); + octx.translate(plotOffset.left, plotOffset.top); + + var i, hi; + for (i = 0; i < highlights.length; ++i) { + hi = highlights[i]; + + if (hi.series.bars.show) + drawBarHighlight(hi.series, hi.point); + else + drawPointHighlight(hi.series, hi.point); + } + octx.restore(); + + executeHooks(hooks.drawOverlay, [octx]); + } + + function highlight(s, point, auto) { + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } + + var i = indexOfHighlight(s, point); + if (i == -1) { + highlights.push({ series: s, point: point, auto: auto }); + + triggerRedrawOverlay(); + } + else if (!auto) + highlights[i].auto = false; + } + + function unhighlight(s, point) { + if (s == null && point == null) { + highlights = []; + triggerRedrawOverlay(); + return; + } + + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } + + var i = indexOfHighlight(s, point); + if (i != -1) { + highlights.splice(i, 1); + + triggerRedrawOverlay(); + } + } + + function indexOfHighlight(s, p) { + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.series == s && h.point[0] == p[0] + && h.point[1] == p[1]) + return i; + } + return -1; + } + + function drawPointHighlight(series, point) { + var x = point[0], y = point[1], + axisx = series.xaxis, axisy = series.yaxis, + highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); + + if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + return; + + var pointRadius = series.points.radius + series.points.lineWidth / 2; + octx.lineWidth = pointRadius; + octx.strokeStyle = highlightColor; + var radius = 1.5 * pointRadius; + x = axisx.p2c(x); + y = axisy.p2c(y); + + octx.beginPath(); + if (series.points.symbol == "circle") + octx.arc(x, y, radius, 0, 2 * Math.PI, false); + else + series.points.symbol(octx, x, y, radius, false); + octx.closePath(); + octx.stroke(); + } + + function drawBarHighlight(series, point) { + var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), + fillStyle = highlightColor, + barLeft; + + switch (series.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -series.bars.barWidth; + break; + default: + barLeft = -series.bars.barWidth / 2; + } + + octx.lineWidth = series.bars.lineWidth; + octx.strokeStyle = highlightColor; + + drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, + function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); + } + + function getColorOrGradient(spec, bottom, top, defaultColor) { + if (typeof spec == "string") + return spec; + else { + // assume this is a gradient spec; IE currently only + // supports a simple vertical gradient properly, so that's + // what we support too + var gradient = ctx.createLinearGradient(0, top, 0, bottom); + + for (var i = 0, l = spec.colors.length; i < l; ++i) { + var c = spec.colors[i]; + if (typeof c != "string") { + var co = $.color.parse(defaultColor); + if (c.brightness != null) + co = co.scale('rgb', c.brightness); + if (c.opacity != null) + co.a *= c.opacity; + c = co.toString(); + } + gradient.addColorStop(i / (l - 1), c); + } + + return gradient; + } + } + } + + // Add the plot function to the top level of the jQuery object + + $.plot = function(placeholder, data, options) { + //var t0 = new Date(); + var plot = new Plot($(placeholder), data, options, $.plot.plugins); + //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); + return plot; + }; + + $.plot.version = "0.8.3"; + + $.plot.plugins = []; + + // Also add the plot function as a chainable property + + $.fn.plot = function(data, options) { + return this.each(function() { + $.plot(this, data, options); + }); + }; + + // round to nearby lower multiple of base + function floorInBase(n, base) { + return base * Math.floor(n / base); + } + +})(jQuery); diff --git a/public/static/libs/flot/jquery.flot.min.js b/public/static/libs/flot/jquery.flot.min.js new file mode 100644 index 0000000..a349637 --- /dev/null +++ b/public/static/libs/flot/jquery.flot.min.js @@ -0,0 +1,8 @@ +/* Javascript plotting library for jQuery, version 0.8.3. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ +(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);(function($){var hasOwnProperty=Object.prototype.hasOwnProperty;if(!$.fn.detach){$.fn.detach=function(){return this.each(function(){if(this.parentNode){this.parentNode.removeChild(this)}})}}function Canvas(cls,container){var element=container.children("."+cls)[0];if(element==null){element=document.createElement("canvas");element.className=cls;$(element).css({direction:"ltr",position:"absolute",left:0,top:0}).appendTo(container);if(!element.getContext){if(window.G_vmlCanvasManager){element=window.G_vmlCanvasManager.initElement(element)}else{throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode.")}}}this.element=element;var context=this.context=element.getContext("2d");var devicePixelRatio=window.devicePixelRatio||1,backingStoreRatio=context.webkitBackingStorePixelRatio||context.mozBackingStorePixelRatio||context.msBackingStorePixelRatio||context.oBackingStorePixelRatio||context.backingStorePixelRatio||1;this.pixelRatio=devicePixelRatio/backingStoreRatio;this.resize(container.width(),container.height());this.textContainer=null;this.text={};this._textCache={}}Canvas.prototype.resize=function(width,height){if(width<=0||height<=0){throw new Error("Invalid dimensions for plot, width = "+width+", height = "+height)}var element=this.element,context=this.context,pixelRatio=this.pixelRatio;if(this.width!=width){element.width=width*pixelRatio;element.style.width=width+"px";this.width=width}if(this.height!=height){element.height=height*pixelRatio;element.style.height=height+"px";this.height=height}context.restore();context.save();context.scale(pixelRatio,pixelRatio)};Canvas.prototype.clear=function(){this.context.clearRect(0,0,this.width,this.height)};Canvas.prototype.render=function(){var cache=this._textCache;for(var layerKey in cache){if(hasOwnProperty.call(cache,layerKey)){var layer=this.getTextLayer(layerKey),layerCache=cache[layerKey];layer.hide();for(var styleKey in layerCache){if(hasOwnProperty.call(layerCache,styleKey)){var styleCache=layerCache[styleKey];for(var key in styleCache){if(hasOwnProperty.call(styleCache,key)){var positions=styleCache[key].positions;for(var i=0,position;position=positions[i];i++){if(position.active){if(!position.rendered){layer.append(position.element);position.rendered=true}}else{positions.splice(i--,1);if(position.rendered){position.element.detach()}}}if(positions.length==0){delete styleCache[key]}}}}}layer.show()}}};Canvas.prototype.getTextLayer=function(classes){var layer=this.text[classes];if(layer==null){if(this.textContainer==null){this.textContainer=$("
    ").css({position:"absolute",top:0,left:0,bottom:0,right:0,"font-size":"smaller",color:"#545454"}).insertAfter(this.element)}layer=this.text[classes]=$("
    ").addClass(classes).css({position:"absolute",top:0,left:0,bottom:0,right:0}).appendTo(this.textContainer)}return layer};Canvas.prototype.getTextInfo=function(layer,text,font,angle,width){var textStyle,layerCache,styleCache,info;text=""+text;if(typeof font==="object"){textStyle=font.style+" "+font.variant+" "+font.weight+" "+font.size+"px/"+font.lineHeight+"px "+font.family}else{textStyle=font}layerCache=this._textCache[layer];if(layerCache==null){layerCache=this._textCache[layer]={}}styleCache=layerCache[textStyle];if(styleCache==null){styleCache=layerCache[textStyle]={}}info=styleCache[text];if(info==null){var element=$("
    ").html(text).css({position:"absolute","max-width":width,top:-9999}).appendTo(this.getTextLayer(layer));if(typeof font==="object"){element.css({font:textStyle,color:font.color})}else if(typeof font==="string"){element.addClass(font)}info=styleCache[text]={width:element.outerWidth(true),height:element.outerHeight(true),element:element,positions:[]};element.detach()}return info};Canvas.prototype.addText=function(layer,x,y,text,font,angle,width,halign,valign){var info=this.getTextInfo(layer,text,font,angle,width),positions=info.positions;if(halign=="center"){x-=info.width/2}else if(halign=="right"){x-=info.width}if(valign=="middle"){y-=info.height/2}else if(valign=="bottom"){y-=info.height}for(var i=0,position;position=positions[i];i++){if(position.x==x&&position.y==y){position.active=true;return}}position={active:true,rendered:false,element:positions.length?info.element.clone():info.element,x:x,y:y};positions.push(position);position.element.css({top:Math.round(y),left:Math.round(x),"text-align":halign})};Canvas.prototype.removeText=function(layer,x,y,text,font,angle){if(text==null){var layerCache=this._textCache[layer];if(layerCache!=null){for(var styleKey in layerCache){if(hasOwnProperty.call(layerCache,styleKey)){var styleCache=layerCache[styleKey];for(var key in styleCache){if(hasOwnProperty.call(styleCache,key)){var positions=styleCache[key].positions;for(var i=0,position;position=positions[i];i++){position.active=false}}}}}}}else{var positions=this.getTextInfo(layer,text,font,angle).positions;for(var i=0,position;position=positions[i];i++){if(position.x==x&&position.y==y){position.active=false}}}};function Plot(placeholder,data_,options_,plugins){var series=[],options={colors:["#edc240","#afd8f8","#cb4b4b","#4da74d","#9440ed"],legend:{show:true,noColumns:1,labelFormatter:null,labelBoxBorderColor:"#ccc",container:null,position:"ne",margin:5,backgroundColor:null,backgroundOpacity:.85,sorted:null},xaxis:{show:null,position:"bottom",mode:null,font:null,color:null,tickColor:null,transform:null,inverseTransform:null,min:null,max:null,autoscaleMargin:null,ticks:null,tickFormatter:null,labelWidth:null,labelHeight:null,reserveSpace:null,tickLength:null,alignTicksWithAxis:null,tickDecimals:null,tickSize:null,minTickSize:null},yaxis:{autoscaleMargin:.02,position:"left"},xaxes:[],yaxes:[],series:{points:{show:false,radius:3,lineWidth:2,fill:true,fillColor:"#ffffff",symbol:"circle"},lines:{lineWidth:2,fill:false,fillColor:null,steps:false},bars:{show:false,lineWidth:2,barWidth:1,fill:true,fillColor:null,align:"left",horizontal:false,zero:true},shadowSize:3,highlightColor:null},grid:{show:true,aboveData:false,color:"#545454",backgroundColor:null,borderColor:null,tickColor:null,margin:0,labelMargin:5,axisMargin:8,borderWidth:2,minBorderMargin:null,markings:null,markingsColor:"#f4f4f4",markingsLineWidth:2,clickable:false,hoverable:false,autoHighlight:true,mouseActiveRadius:10},interaction:{redrawOverlayInterval:1e3/60},hooks:{}},surface=null,overlay=null,eventHolder=null,ctx=null,octx=null,xaxes=[],yaxes=[],plotOffset={left:0,right:0,top:0,bottom:0},plotWidth=0,plotHeight=0,hooks={processOptions:[],processRawData:[],processDatapoints:[],processOffset:[],drawBackground:[],drawSeries:[],draw:[],bindEvents:[],drawOverlay:[],shutdown:[]},plot=this;plot.setData=setData;plot.setupGrid=setupGrid;plot.draw=draw;plot.getPlaceholder=function(){return placeholder};plot.getCanvas=function(){return surface.element};plot.getPlotOffset=function(){return plotOffset};plot.width=function(){return plotWidth};plot.height=function(){return plotHeight};plot.offset=function(){var o=eventHolder.offset();o.left+=plotOffset.left;o.top+=plotOffset.top;return o};plot.getData=function(){return series};plot.getAxes=function(){var res={},i;$.each(xaxes.concat(yaxes),function(_,axis){if(axis)res[axis.direction+(axis.n!=1?axis.n:"")+"axis"]=axis});return res};plot.getXAxes=function(){return xaxes};plot.getYAxes=function(){return yaxes};plot.c2p=canvasToAxisCoords;plot.p2c=axisToCanvasCoords;plot.getOptions=function(){return options};plot.highlight=highlight;plot.unhighlight=unhighlight;plot.triggerRedrawOverlay=triggerRedrawOverlay;plot.pointOffset=function(point){return{left:parseInt(xaxes[axisNumber(point,"x")-1].p2c(+point.x)+plotOffset.left,10),top:parseInt(yaxes[axisNumber(point,"y")-1].p2c(+point.y)+plotOffset.top,10)}};plot.shutdown=shutdown;plot.destroy=function(){shutdown();placeholder.removeData("plot").empty();series=[];options=null;surface=null;overlay=null;eventHolder=null;ctx=null;octx=null;xaxes=[];yaxes=[];hooks=null;highlights=[];plot=null};plot.resize=function(){var width=placeholder.width(),height=placeholder.height();surface.resize(width,height);overlay.resize(width,height)};plot.hooks=hooks;initPlugins(plot);parseOptions(options_);setupCanvases();setData(data_);setupGrid();draw();bindEvents();function executeHooks(hook,args){args=[plot].concat(args);for(var i=0;imaxIndex){maxIndex=sc}}}if(neededColors<=maxIndex){neededColors=maxIndex+1}var c,colors=[],colorPool=options.colors,colorPoolSize=colorPool.length,variation=0;for(i=0;i=0){if(variation<.5){variation=-variation-.2}else variation=0}else variation=-variation}colors[i]=c.scale("rgb",1+variation)}var colori=0,s;for(i=0;iaxis.datamax&&max!=fakeInfinity)axis.datamax=max}$.each(allAxes(),function(_,axis){axis.datamin=topSentry;axis.datamax=bottomSentry;axis.used=false});for(i=0;i0&&points[k-ps]!=null&&points[k-ps]!=points[k]&&points[k-ps+1]!=points[k+1]){for(m=0;mxmax)xmax=val}if(f.y){if(valymax)ymax=val}}}if(s.bars.show){var delta;switch(s.bars.align){case"left":delta=0;break;case"right":delta=-s.bars.barWidth;break;default:delta=-s.bars.barWidth/2}if(s.bars.horizontal){ymin+=delta;ymax+=delta+s.bars.barWidth}else{xmin+=delta;xmax+=delta+s.bars.barWidth}}updateAxis(s.xaxis,xmin,xmax);updateAxis(s.yaxis,ymin,ymax)}$.each(allAxes(),function(_,axis){if(axis.datamin==topSentry)axis.datamin=null;if(axis.datamax==bottomSentry)axis.datamax=null})}function setupCanvases(){placeholder.css("padding",0).children().filter(function(){return!$(this).hasClass("flot-overlay")&&!$(this).hasClass("flot-base")}).remove();if(placeholder.css("position")=="static")placeholder.css("position","relative");surface=new Canvas("flot-base",placeholder);overlay=new Canvas("flot-overlay",placeholder);ctx=surface.context;octx=overlay.context;eventHolder=$(overlay.element).unbind();var existing=placeholder.data("plot");if(existing){existing.shutdown();overlay.clear()}placeholder.data("plot",plot)}function bindEvents(){if(options.grid.hoverable){eventHolder.mousemove(onMouseMove);eventHolder.bind("mouseleave",onMouseLeave)}if(options.grid.clickable)eventHolder.click(onClick);executeHooks(hooks.bindEvents,[eventHolder])}function shutdown(){if(redrawTimeout)clearTimeout(redrawTimeout);eventHolder.unbind("mousemove",onMouseMove);eventHolder.unbind("mouseleave",onMouseLeave);eventHolder.unbind("click",onClick);executeHooks(hooks.shutdown,[eventHolder])}function setTransformationHelpers(axis){function identity(x){return x}var s,m,t=axis.options.transform||identity,it=axis.options.inverseTransform;if(axis.direction=="x"){s=axis.scale=plotWidth/Math.abs(t(axis.max)-t(axis.min));m=Math.min(t(axis.max),t(axis.min))}else{s=axis.scale=plotHeight/Math.abs(t(axis.max)-t(axis.min));s=-s;m=Math.max(t(axis.max),t(axis.min))}if(t==identity)axis.p2c=function(p){return(p-m)*s};else axis.p2c=function(p){return(t(p)-m)*s};if(!it)axis.c2p=function(c){return m+c/s};else axis.c2p=function(c){return it(m+c/s)}}function measureTickLabels(axis){var opts=axis.options,ticks=axis.ticks||[],labelWidth=opts.labelWidth||0,labelHeight=opts.labelHeight||0,maxWidth=labelWidth||(axis.direction=="x"?Math.floor(surface.width/(ticks.length||1)):null),legacyStyles=axis.direction+"Axis "+axis.direction+axis.n+"Axis",layer="flot-"+axis.direction+"-axis flot-"+axis.direction+axis.n+"-axis "+legacyStyles,font=opts.font||"flot-tick-label tickLabel";for(var i=0;i=0;--i)allocateAxisBoxFirstPhase(allocatedAxes[i]);adjustLayoutForThingsStickingOut();$.each(allocatedAxes,function(_,axis){allocateAxisBoxSecondPhase(axis)})}plotWidth=surface.width-plotOffset.left-plotOffset.right;plotHeight=surface.height-plotOffset.bottom-plotOffset.top;$.each(axes,function(_,axis){setTransformationHelpers(axis)});if(showGrid){drawAxisLabels()}insertLegend()}function setRange(axis){var opts=axis.options,min=+(opts.min!=null?opts.min:axis.datamin),max=+(opts.max!=null?opts.max:axis.datamax),delta=max-min;if(delta==0){var widen=max==0?1:.01;if(opts.min==null)min-=widen;if(opts.max==null||opts.min!=null)max+=widen}else{var margin=opts.autoscaleMargin;if(margin!=null){if(opts.min==null){min-=delta*margin;if(min<0&&axis.datamin!=null&&axis.datamin>=0)min=0}if(opts.max==null){max+=delta*margin;if(max>0&&axis.datamax!=null&&axis.datamax<=0)max=0}}}axis.min=min;axis.max=max}function setupTickGeneration(axis){var opts=axis.options;var noTicks;if(typeof opts.ticks=="number"&&opts.ticks>0)noTicks=opts.ticks;else noTicks=.3*Math.sqrt(axis.direction=="x"?surface.width:surface.height);var delta=(axis.max-axis.min)/noTicks,dec=-Math.floor(Math.log(delta)/Math.LN10),maxDec=opts.tickDecimals;if(maxDec!=null&&dec>maxDec){dec=maxDec}var magn=Math.pow(10,-dec),norm=delta/magn,size;if(norm<1.5){size=1}else if(norm<3){size=2;if(norm>2.25&&(maxDec==null||dec+1<=maxDec)){size=2.5;++dec}}else if(norm<7.5){size=5}else{size=10}size*=magn;if(opts.minTickSize!=null&&size0){if(opts.min==null)axis.min=Math.min(axis.min,niceTicks[0]);if(opts.max==null&&niceTicks.length>1)axis.max=Math.max(axis.max,niceTicks[niceTicks.length-1])}axis.tickGenerator=function(axis){var ticks=[],v,i;for(i=0;i1&&/\..*0$/.test((ts[1]-ts[0]).toFixed(extraDec))))axis.tickDecimals=extraDec}}}}function setTicks(axis){var oticks=axis.options.ticks,ticks=[];if(oticks==null||typeof oticks=="number"&&oticks>0)ticks=axis.tickGenerator(axis);else if(oticks){if($.isFunction(oticks))ticks=oticks(axis);else ticks=oticks}var i,v;axis.ticks=[];for(i=0;i1)label=t[1]}else v=+t;if(label==null)label=axis.tickFormatter(v,axis);if(!isNaN(v))axis.ticks.push({v:v,label:label})}}function snapRangeToTicks(axis,ticks){if(axis.options.autoscaleMargin&&ticks.length>0){if(axis.options.min==null)axis.min=Math.min(axis.min,ticks[0].v);if(axis.options.max==null&&ticks.length>1)axis.max=Math.max(axis.max,ticks[ticks.length-1].v)}}function draw(){surface.clear();executeHooks(hooks.drawBackground,[ctx]);var grid=options.grid;if(grid.show&&grid.backgroundColor)drawBackground();if(grid.show&&!grid.aboveData){drawGrid()}for(var i=0;ito){var tmp=from;from=to;to=tmp}return{from:from,to:to,axis:axis}}function drawBackground(){ctx.save();ctx.translate(plotOffset.left,plotOffset.top);ctx.fillStyle=getColorOrGradient(options.grid.backgroundColor,plotHeight,0,"rgba(255, 255, 255, 0)");ctx.fillRect(0,0,plotWidth,plotHeight);ctx.restore()}function drawGrid(){var i,axes,bw,bc;ctx.save();ctx.translate(plotOffset.left,plotOffset.top);var markings=options.grid.markings;if(markings){if($.isFunction(markings)){axes=plot.getAxes();axes.xmin=axes.xaxis.min;axes.xmax=axes.xaxis.max;axes.ymin=axes.yaxis.min;axes.ymax=axes.yaxis.max;markings=markings(axes)}for(i=0;ixrange.axis.max||yrange.toyrange.axis.max)continue;xrange.from=Math.max(xrange.from,xrange.axis.min);xrange.to=Math.min(xrange.to,xrange.axis.max);yrange.from=Math.max(yrange.from,yrange.axis.min);yrange.to=Math.min(yrange.to,yrange.axis.max);var xequal=xrange.from===xrange.to,yequal=yrange.from===yrange.to;if(xequal&&yequal){continue}xrange.from=Math.floor(xrange.axis.p2c(xrange.from));xrange.to=Math.floor(xrange.axis.p2c(xrange.to));yrange.from=Math.floor(yrange.axis.p2c(yrange.from));yrange.to=Math.floor(yrange.axis.p2c(yrange.to));if(xequal||yequal){var lineWidth=m.lineWidth||options.grid.markingsLineWidth,subPixel=lineWidth%2?.5:0;ctx.beginPath();ctx.strokeStyle=m.color||options.grid.markingsColor;ctx.lineWidth=lineWidth;if(xequal){ctx.moveTo(xrange.to+subPixel,yrange.from);ctx.lineTo(xrange.to+subPixel,yrange.to)}else{ctx.moveTo(xrange.from,yrange.to+subPixel);ctx.lineTo(xrange.to,yrange.to+subPixel)}ctx.stroke()}else{ctx.fillStyle=m.color||options.grid.markingsColor;ctx.fillRect(xrange.from,yrange.to,xrange.to-xrange.from,yrange.from-yrange.to)}}}axes=allAxes();bw=options.grid.borderWidth;for(var j=0;jaxis.max||t=="full"&&(typeof bw=="object"&&bw[axis.position]>0||bw>0)&&(v==axis.min||v==axis.max))continue;if(axis.direction=="x"){x=axis.p2c(v);yoff=t=="full"?-plotHeight:t;if(axis.position=="top")yoff=-yoff}else{y=axis.p2c(v);xoff=t=="full"?-plotWidth:t;if(axis.position=="left")xoff=-xoff}if(ctx.lineWidth==1){if(axis.direction=="x")x=Math.floor(x)+.5;else y=Math.floor(y)+.5}ctx.moveTo(x,y);ctx.lineTo(x+xoff,y+yoff)}ctx.stroke()}if(bw){bc=options.grid.borderColor;if(typeof bw=="object"||typeof bc=="object"){if(typeof bw!=="object"){bw={top:bw,right:bw,bottom:bw,left:bw}}if(typeof bc!=="object"){bc={top:bc,right:bc,bottom:bc,left:bc}}if(bw.top>0){ctx.strokeStyle=bc.top;ctx.lineWidth=bw.top;ctx.beginPath();ctx.moveTo(0-bw.left,0-bw.top/2);ctx.lineTo(plotWidth,0-bw.top/2);ctx.stroke()}if(bw.right>0){ctx.strokeStyle=bc.right;ctx.lineWidth=bw.right;ctx.beginPath();ctx.moveTo(plotWidth+bw.right/2,0-bw.top);ctx.lineTo(plotWidth+bw.right/2,plotHeight);ctx.stroke()}if(bw.bottom>0){ctx.strokeStyle=bc.bottom;ctx.lineWidth=bw.bottom;ctx.beginPath();ctx.moveTo(plotWidth+bw.right,plotHeight+bw.bottom/2);ctx.lineTo(0,plotHeight+bw.bottom/2);ctx.stroke()}if(bw.left>0){ctx.strokeStyle=bc.left;ctx.lineWidth=bw.left;ctx.beginPath();ctx.moveTo(0-bw.left/2,plotHeight+bw.bottom);ctx.lineTo(0-bw.left/2,0);ctx.stroke()}}else{ctx.lineWidth=bw;ctx.strokeStyle=options.grid.borderColor;ctx.strokeRect(-bw/2,-bw/2,plotWidth+bw,plotHeight+bw)}}ctx.restore()}function drawAxisLabels(){$.each(allAxes(),function(_,axis){var box=axis.box,legacyStyles=axis.direction+"Axis "+axis.direction+axis.n+"Axis",layer="flot-"+axis.direction+"-axis flot-"+axis.direction+axis.n+"-axis "+legacyStyles,font=axis.options.font||"flot-tick-label tickLabel",tick,x,y,halign,valign;surface.removeText(layer);if(!axis.show||axis.ticks.length==0)return;for(var i=0;iaxis.max)continue;if(axis.direction=="x"){halign="center";x=plotOffset.left+axis.p2c(tick.v);if(axis.position=="bottom"){y=box.top+box.padding}else{y=box.top+box.height-box.padding;valign="bottom"}}else{valign="middle";y=plotOffset.top+axis.p2c(tick.v);if(axis.position=="left"){x=box.left+box.width-box.padding;halign="right"}else{x=box.left+box.padding}}surface.addText(layer,x,y,tick.label,font,null,null,halign,valign)}})}function drawSeries(series){if(series.lines.show)drawSeriesLines(series);if(series.bars.show)drawSeriesBars(series);if(series.points.show)drawSeriesPoints(series)}function drawSeriesLines(series){function plotLine(datapoints,xoffset,yoffset,axisx,axisy){var points=datapoints.points,ps=datapoints.pointsize,prevx=null,prevy=null;ctx.beginPath();for(var i=ps;i=y2&&y1>axisy.max){if(y2>axisy.max)continue;x1=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.max}else if(y2>=y1&&y2>axisy.max){if(y1>axisy.max)continue;x2=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.max}if(x1<=x2&&x1=x2&&x1>axisx.max){if(x2>axisx.max)continue;y1=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x1=axisx.max}else if(x2>=x1&&x2>axisx.max){if(x1>axisx.max)continue;y2=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x2=axisx.max}if(x1!=prevx||y1!=prevy)ctx.moveTo(axisx.p2c(x1)+xoffset,axisy.p2c(y1)+yoffset);prevx=x2;prevy=y2;ctx.lineTo(axisx.p2c(x2)+xoffset,axisy.p2c(y2)+yoffset)}ctx.stroke()}function plotLineArea(datapoints,axisx,axisy){var points=datapoints.points,ps=datapoints.pointsize,bottom=Math.min(Math.max(0,axisy.min),axisy.max),i=0,top,areaOpen=false,ypos=1,segmentStart=0,segmentEnd=0;while(true){if(ps>0&&i>points.length+ps)break;i+=ps;var x1=points[i-ps],y1=points[i-ps+ypos],x2=points[i],y2=points[i+ypos];if(areaOpen){if(ps>0&&x1!=null&&x2==null){segmentEnd=i;ps=-ps;ypos=2;continue}if(ps<0&&i==segmentStart+ps){ctx.fill();areaOpen=false;ps=-ps;ypos=1;i=segmentStart=segmentEnd+ps;continue}}if(x1==null||x2==null)continue;if(x1<=x2&&x1=x2&&x1>axisx.max){if(x2>axisx.max)continue;y1=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x1=axisx.max}else if(x2>=x1&&x2>axisx.max){if(x1>axisx.max)continue;y2=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x2=axisx.max}if(!areaOpen){ctx.beginPath();ctx.moveTo(axisx.p2c(x1),axisy.p2c(bottom));areaOpen=true}if(y1>=axisy.max&&y2>=axisy.max){ctx.lineTo(axisx.p2c(x1),axisy.p2c(axisy.max));ctx.lineTo(axisx.p2c(x2),axisy.p2c(axisy.max));continue}else if(y1<=axisy.min&&y2<=axisy.min){ctx.lineTo(axisx.p2c(x1),axisy.p2c(axisy.min));ctx.lineTo(axisx.p2c(x2),axisy.p2c(axisy.min));continue}var x1old=x1,x2old=x2;if(y1<=y2&&y1=axisy.min){x1=(axisy.min-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.min}else if(y2<=y1&&y2=axisy.min){x2=(axisy.min-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.min}if(y1>=y2&&y1>axisy.max&&y2<=axisy.max){x1=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.max}else if(y2>=y1&&y2>axisy.max&&y1<=axisy.max){x2=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.max}if(x1!=x1old){ctx.lineTo(axisx.p2c(x1old),axisy.p2c(y1))}ctx.lineTo(axisx.p2c(x1),axisy.p2c(y1));ctx.lineTo(axisx.p2c(x2),axisy.p2c(y2));if(x2!=x2old){ctx.lineTo(axisx.p2c(x2),axisy.p2c(y2));ctx.lineTo(axisx.p2c(x2old),axisy.p2c(y2))}}}ctx.save();ctx.translate(plotOffset.left,plotOffset.top);ctx.lineJoin="round";var lw=series.lines.lineWidth,sw=series.shadowSize;if(lw>0&&sw>0){ctx.lineWidth=sw;ctx.strokeStyle="rgba(0,0,0,0.1)";var angle=Math.PI/18;plotLine(series.datapoints,Math.sin(angle)*(lw/2+sw/2),Math.cos(angle)*(lw/2+sw/2),series.xaxis,series.yaxis);ctx.lineWidth=sw/2;plotLine(series.datapoints,Math.sin(angle)*(lw/2+sw/4),Math.cos(angle)*(lw/2+sw/4),series.xaxis,series.yaxis)}ctx.lineWidth=lw;ctx.strokeStyle=series.color;var fillStyle=getFillStyle(series.lines,series.color,0,plotHeight);if(fillStyle){ctx.fillStyle=fillStyle;plotLineArea(series.datapoints,series.xaxis,series.yaxis)}if(lw>0)plotLine(series.datapoints,0,0,series.xaxis,series.yaxis);ctx.restore()}function drawSeriesPoints(series){function plotPoints(datapoints,radius,fillStyle,offset,shadow,axisx,axisy,symbol){var points=datapoints.points,ps=datapoints.pointsize;for(var i=0;iaxisx.max||yaxisy.max)continue;ctx.beginPath();x=axisx.p2c(x);y=axisy.p2c(y)+offset;if(symbol=="circle")ctx.arc(x,y,radius,0,shadow?Math.PI:Math.PI*2,false);else symbol(ctx,x,y,radius,shadow);ctx.closePath();if(fillStyle){ctx.fillStyle=fillStyle;ctx.fill()}ctx.stroke()}}ctx.save();ctx.translate(plotOffset.left,plotOffset.top);var lw=series.points.lineWidth,sw=series.shadowSize,radius=series.points.radius,symbol=series.points.symbol;if(lw==0)lw=1e-4;if(lw>0&&sw>0){var w=sw/2;ctx.lineWidth=w;ctx.strokeStyle="rgba(0,0,0,0.1)";plotPoints(series.datapoints,radius,null,w+w/2,true,series.xaxis,series.yaxis,symbol);ctx.strokeStyle="rgba(0,0,0,0.2)";plotPoints(series.datapoints,radius,null,w/2,true,series.xaxis,series.yaxis,symbol)}ctx.lineWidth=lw;ctx.strokeStyle=series.color;plotPoints(series.datapoints,radius,getFillStyle(series.points,series.color),0,false,series.xaxis,series.yaxis,symbol);ctx.restore()}function drawBar(x,y,b,barLeft,barRight,fillStyleCallback,axisx,axisy,c,horizontal,lineWidth){var left,right,bottom,top,drawLeft,drawRight,drawTop,drawBottom,tmp;if(horizontal){drawBottom=drawRight=drawTop=true;drawLeft=false;left=b;right=x;top=y+barLeft;bottom=y+barRight;if(rightaxisx.max||topaxisy.max)return;if(leftaxisx.max){right=axisx.max;drawRight=false}if(bottomaxisy.max){top=axisy.max;drawTop=false}left=axisx.p2c(left);bottom=axisy.p2c(bottom);right=axisx.p2c(right);top=axisy.p2c(top);if(fillStyleCallback){c.fillStyle=fillStyleCallback(bottom,top);c.fillRect(left,top,right-left,bottom-top)}if(lineWidth>0&&(drawLeft||drawRight||drawTop||drawBottom)){c.beginPath();c.moveTo(left,bottom);if(drawLeft)c.lineTo(left,top);else c.moveTo(left,top);if(drawTop)c.lineTo(right,top);else c.moveTo(right,top);if(drawRight)c.lineTo(right,bottom);else c.moveTo(right,bottom);if(drawBottom)c.lineTo(left,bottom);else c.moveTo(left,bottom);c.stroke()}}function drawSeriesBars(series){function plotBars(datapoints,barLeft,barRight,fillStyleCallback,axisx,axisy){var points=datapoints.points,ps=datapoints.pointsize;for(var i=0;i");fragments.push("");rowStarted=true}fragments.push('
    '+''+entry.label+"")}if(rowStarted)fragments.push("");if(fragments.length==0)return;var table=''+fragments.join("")+"
    ";if(options.legend.container!=null)$(options.legend.container).html(table);else{var pos="",p=options.legend.position,m=options.legend.margin;if(m[0]==null)m=[m,m];if(p.charAt(0)=="n")pos+="top:"+(m[1]+plotOffset.top)+"px;";else if(p.charAt(0)=="s")pos+="bottom:"+(m[1]+plotOffset.bottom)+"px;";if(p.charAt(1)=="e")pos+="right:"+(m[0]+plotOffset.right)+"px;";else if(p.charAt(1)=="w")pos+="left:"+(m[0]+plotOffset.left)+"px;";var legend=$('
    '+table.replace('style="','style="position:absolute;'+pos+";")+"
    ").appendTo(placeholder);if(options.legend.backgroundOpacity!=0){var c=options.legend.backgroundColor;if(c==null){c=options.grid.backgroundColor;if(c&&typeof c=="string")c=$.color.parse(c);else c=$.color.extract(legend,"background-color");c.a=1;c=c.toString()}var div=legend.children();$('
    ').prependTo(legend).css("opacity",options.legend.backgroundOpacity)}}}var highlights=[],redrawTimeout=null;function findNearbyItem(mouseX,mouseY,seriesFilter){var maxDistance=options.grid.mouseActiveRadius,smallestDistance=maxDistance*maxDistance+1,item=null,foundPoint=false,i,j,ps;for(i=series.length-1;i>=0;--i){if(!seriesFilter(series[i]))continue;var s=series[i],axisx=s.xaxis,axisy=s.yaxis,points=s.datapoints.points,mx=axisx.c2p(mouseX),my=axisy.c2p(mouseY),maxx=maxDistance/axisx.scale,maxy=maxDistance/axisy.scale;ps=s.datapoints.pointsize;if(axisx.options.inverseTransform)maxx=Number.MAX_VALUE;if(axisy.options.inverseTransform)maxy=Number.MAX_VALUE;if(s.lines.show||s.points.show){for(j=0;jmaxx||x-mx<-maxx||y-my>maxy||y-my<-maxy)continue;var dx=Math.abs(axisx.p2c(x)-mouseX),dy=Math.abs(axisy.p2c(y)-mouseY),dist=dx*dx+dy*dy;if(dist=Math.min(b,x)&&my>=y+barLeft&&my<=y+barRight:mx>=x+barLeft&&mx<=x+barRight&&my>=Math.min(b,y)&&my<=Math.max(b,y))item=[i,j/ps]}}}if(item){i=item[0];j=item[1];ps=series[i].datapoints.pointsize;return{datapoint:series[i].datapoints.points.slice(j*ps,(j+1)*ps),dataIndex:j,series:series[i],seriesIndex:i}}return null}function onMouseMove(e){if(options.grid.hoverable)triggerClickHoverEvent("plothover",e,function(s){return s["hoverable"]!=false})}function onMouseLeave(e){if(options.grid.hoverable)triggerClickHoverEvent("plothover",e,function(s){return false})}function onClick(e){triggerClickHoverEvent("plotclick",e,function(s){return s["clickable"]!=false})}function triggerClickHoverEvent(eventname,event,seriesFilter){var offset=eventHolder.offset(),canvasX=event.pageX-offset.left-plotOffset.left,canvasY=event.pageY-offset.top-plotOffset.top,pos=canvasToAxisCoords({left:canvasX,top:canvasY});pos.pageX=event.pageX;pos.pageY=event.pageY;var item=findNearbyItem(canvasX,canvasY,seriesFilter);if(item){item.pageX=parseInt(item.series.xaxis.p2c(item.datapoint[0])+offset.left+plotOffset.left,10);item.pageY=parseInt(item.series.yaxis.p2c(item.datapoint[1])+offset.top+plotOffset.top,10)}if(options.grid.autoHighlight){for(var i=0;iaxisx.max||yaxisy.max)return;var pointRadius=series.points.radius+series.points.lineWidth/2;octx.lineWidth=pointRadius;octx.strokeStyle=highlightColor;var radius=1.5*pointRadius;x=axisx.p2c(x);y=axisy.p2c(y);octx.beginPath();if(series.points.symbol=="circle")octx.arc(x,y,radius,0,2*Math.PI,false);else series.points.symbol(octx,x,y,radius,false);octx.closePath();octx.stroke()}function drawBarHighlight(series,point){var highlightColor=typeof series.highlightColor==="string"?series.highlightColor:$.color.parse(series.color).scale("a",.5).toString(),fillStyle=highlightColor,barLeft;switch(series.bars.align){case"left":barLeft=0;break;case"right":barLeft=-series.bars.barWidth;break;default:barLeft=-series.bars.barWidth/2}octx.lineWidth=series.bars.lineWidth;octx.strokeStyle=highlightColor;drawBar(point[0],point[1],point[2]||0,barLeft,barLeft+series.bars.barWidth,function(){return fillStyle},series.xaxis,series.yaxis,octx,series.bars.horizontal,series.bars.lineWidth)}function getColorOrGradient(spec,bottom,top,defaultColor){if(typeof spec=="string")return spec;else{var gradient=ctx.createLinearGradient(0,top,0,bottom);for(var i=0,l=spec.colors.length;i 1) { + options.series.pie.tilt = 1; + } else if (options.series.pie.tilt < 0) { + options.series.pie.tilt = 0; + } + } + }); + + plot.hooks.bindEvents.push(function(plot, eventHolder) { + var options = plot.getOptions(); + if (options.series.pie.show) { + if (options.grid.hoverable) { + eventHolder.unbind("mousemove").mousemove(onMouseMove); + } + if (options.grid.clickable) { + eventHolder.unbind("click").click(onClick); + } + } + }); + + plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) { + var options = plot.getOptions(); + if (options.series.pie.show) { + processDatapoints(plot, series, data, datapoints); + } + }); + + plot.hooks.drawOverlay.push(function(plot, octx) { + var options = plot.getOptions(); + if (options.series.pie.show) { + drawOverlay(plot, octx); + } + }); + + plot.hooks.draw.push(function(plot, newCtx) { + var options = plot.getOptions(); + if (options.series.pie.show) { + draw(plot, newCtx); + } + }); + + function processDatapoints(plot, series, datapoints) { + if (!processed) { + processed = true; + canvas = plot.getCanvas(); + target = $(canvas).parent(); + options = plot.getOptions(); + plot.setData(combine(plot.getData())); + } + } + + function combine(data) { + + var total = 0, + combined = 0, + numCombined = 0, + color = options.series.pie.combine.color, + newdata = []; + + // Fix up the raw data from Flot, ensuring the data is numeric + + for (var i = 0; i < data.length; ++i) { + + var value = data[i].data; + + // If the data is an array, we'll assume that it's a standard + // Flot x-y pair, and are concerned only with the second value. + + // Note how we use the original array, rather than creating a + // new one; this is more efficient and preserves any extra data + // that the user may have stored in higher indexes. + + if ($.isArray(value) && value.length == 1) { + value = value[0]; + } + + if ($.isArray(value)) { + // Equivalent to $.isNumeric() but compatible with jQuery < 1.7 + if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) { + value[1] = +value[1]; + } else { + value[1] = 0; + } + } else if (!isNaN(parseFloat(value)) && isFinite(value)) { + value = [1, +value]; + } else { + value = [1, 0]; + } + + data[i].data = [value]; + } + + // Sum up all the slices, so we can calculate percentages for each + + for (var i = 0; i < data.length; ++i) { + total += data[i].data[0][1]; + } + + // Count the number of slices with percentages below the combine + // threshold; if it turns out to be just one, we won't combine. + + for (var i = 0; i < data.length; ++i) { + var value = data[i].data[0][1]; + if (value / total <= options.series.pie.combine.threshold) { + combined += value; + numCombined++; + if (!color) { + color = data[i].color; + } + } + } + + for (var i = 0; i < data.length; ++i) { + var value = data[i].data[0][1]; + if (numCombined < 2 || value / total > options.series.pie.combine.threshold) { + newdata.push( + $.extend(data[i], { /* extend to allow keeping all other original data values + and using them e.g. in labelFormatter. */ + data: [[1, value]], + color: data[i].color, + label: data[i].label, + angle: value * Math.PI * 2 / total, + percent: value / (total / 100) + }) + ); + } + } + + if (numCombined > 1) { + newdata.push({ + data: [[1, combined]], + color: color, + label: options.series.pie.combine.label, + angle: combined * Math.PI * 2 / total, + percent: combined / (total / 100) + }); + } + + return newdata; + } + + function draw(plot, newCtx) { + + if (!target) { + return; // if no series were passed + } + + var canvasWidth = plot.getPlaceholder().width(), + canvasHeight = plot.getPlaceholder().height(), + legendWidth = target.children().filter(".legend").children().width() || 0; + + ctx = newCtx; + + // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE! + + // When combining smaller slices into an 'other' slice, we need to + // add a new series. Since Flot gives plugins no way to modify the + // list of series, the pie plugin uses a hack where the first call + // to processDatapoints results in a call to setData with the new + // list of series, then subsequent processDatapoints do nothing. + + // The plugin-global 'processed' flag is used to control this hack; + // it starts out false, and is set to true after the first call to + // processDatapoints. + + // Unfortunately this turns future setData calls into no-ops; they + // call processDatapoints, the flag is true, and nothing happens. + + // To fix this we'll set the flag back to false here in draw, when + // all series have been processed, so the next sequence of calls to + // processDatapoints once again starts out with a slice-combine. + // This is really a hack; in 0.9 we need to give plugins a proper + // way to modify series before any processing begins. + + processed = false; + + // calculate maximum radius and center point + + maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; + centerTop = canvasHeight / 2 + options.series.pie.offset.top; + centerLeft = canvasWidth / 2; + + if (options.series.pie.offset.left == "auto") { + if (options.legend.position.match("w")) { + centerLeft += legendWidth / 2; + } else { + centerLeft -= legendWidth / 2; + } + if (centerLeft < maxRadius) { + centerLeft = maxRadius; + } else if (centerLeft > canvasWidth - maxRadius) { + centerLeft = canvasWidth - maxRadius; + } + } else { + centerLeft += options.series.pie.offset.left; + } + + var slices = plot.getData(), + attempts = 0; + + // Keep shrinking the pie's radius until drawPie returns true, + // indicating that all the labels fit, or we try too many times. + + do { + if (attempts > 0) { + maxRadius *= REDRAW_SHRINK; + } + attempts += 1; + clear(); + if (options.series.pie.tilt <= 0.8) { + drawShadow(); + } + } while (!drawPie() && attempts < REDRAW_ATTEMPTS) + + if (attempts >= REDRAW_ATTEMPTS) { + clear(); + target.prepend("
    Could not draw pie with labels contained inside canvas
    "); + } + + if (plot.setSeries && plot.insertLegend) { + plot.setSeries(slices); + plot.insertLegend(); + } + + // we're actually done at this point, just defining internal functions at this point + + function clear() { + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + target.children().filter(".pieLabel, .pieLabelBackground").remove(); + } + + function drawShadow() { + + var shadowLeft = options.series.pie.shadow.left; + var shadowTop = options.series.pie.shadow.top; + var edge = 10; + var alpha = options.series.pie.shadow.alpha; + var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; + + if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) { + return; // shadow would be outside canvas, so don't draw it + } + + ctx.save(); + ctx.translate(shadowLeft,shadowTop); + ctx.globalAlpha = alpha; + ctx.fillStyle = "#000"; + + // center and rotate to starting position + + ctx.translate(centerLeft,centerTop); + ctx.scale(1, options.series.pie.tilt); + + //radius -= edge; + + for (var i = 1; i <= edge; i++) { + ctx.beginPath(); + ctx.arc(0, 0, radius, 0, Math.PI * 2, false); + ctx.fill(); + radius -= i; + } + + ctx.restore(); + } + + function drawPie() { + + var startAngle = Math.PI * options.series.pie.startAngle; + var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; + + // center and rotate to starting position + + ctx.save(); + ctx.translate(centerLeft,centerTop); + ctx.scale(1, options.series.pie.tilt); + //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera + + // draw slices + + ctx.save(); + var currentAngle = startAngle; + for (var i = 0; i < slices.length; ++i) { + slices[i].startAngle = currentAngle; + drawSlice(slices[i].angle, slices[i].color, true); + } + ctx.restore(); + + // draw slice outlines + + if (options.series.pie.stroke.width > 0) { + ctx.save(); + ctx.lineWidth = options.series.pie.stroke.width; + currentAngle = startAngle; + for (var i = 0; i < slices.length; ++i) { + drawSlice(slices[i].angle, options.series.pie.stroke.color, false); + } + ctx.restore(); + } + + // draw donut hole + + drawDonutHole(ctx); + + ctx.restore(); + + // Draw the labels, returning true if they fit within the plot + + if (options.series.pie.label.show) { + return drawLabels(); + } else return true; + + function drawSlice(angle, color, fill) { + + if (angle <= 0 || isNaN(angle)) { + return; + } + + if (fill) { + ctx.fillStyle = color; + } else { + ctx.strokeStyle = color; + ctx.lineJoin = "round"; + } + + ctx.beginPath(); + if (Math.abs(angle - Math.PI * 2) > 0.000000001) { + ctx.moveTo(0, 0); // Center of the pie + } + + //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera + ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false); + ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false); + ctx.closePath(); + //ctx.rotate(angle); // This doesn't work properly in Opera + currentAngle += angle; + + if (fill) { + ctx.fill(); + } else { + ctx.stroke(); + } + } + + function drawLabels() { + + var currentAngle = startAngle; + var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius; + + for (var i = 0; i < slices.length; ++i) { + if (slices[i].percent >= options.series.pie.label.threshold * 100) { + if (!drawLabel(slices[i], currentAngle, i)) { + return false; + } + } + currentAngle += slices[i].angle; + } + + return true; + + function drawLabel(slice, startAngle, index) { + + if (slice.data[0][1] == 0) { + return true; + } + + // format label text + + var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter; + + if (lf) { + text = lf(slice.label, slice); + } else { + text = slice.label; + } + + if (plf) { + text = plf(text, slice); + } + + var halfAngle = ((startAngle + slice.angle) + startAngle) / 2; + var x = centerLeft + Math.round(Math.cos(halfAngle) * radius); + var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; + + var html = "" + text + ""; + target.append(html); + + var label = target.children("#pieLabel" + index); + var labelTop = (y - label.height() / 2); + var labelLeft = (x - label.width() / 2); + + label.css("top", labelTop); + label.css("left", labelLeft); + + // check to make sure that the label is not outside the canvas + + if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) { + return false; + } + + if (options.series.pie.label.background.opacity != 0) { + + // put in the transparent background separately to avoid blended labels and label boxes + + var c = options.series.pie.label.background.color; + + if (c == null) { + c = slice.color; + } + + var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;"; + $("
    ") + .css("opacity", options.series.pie.label.background.opacity) + .insertBefore(label); + } + + return true; + } // end individual label function + } // end drawLabels function + } // end drawPie function + } // end draw function + + // Placed here because it needs to be accessed from multiple locations + + function drawDonutHole(layer) { + if (options.series.pie.innerRadius > 0) { + + // subtract the center + + layer.save(); + var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius; + layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color + layer.beginPath(); + layer.fillStyle = options.series.pie.stroke.color; + layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); + layer.fill(); + layer.closePath(); + layer.restore(); + + // add inner stroke + + layer.save(); + layer.beginPath(); + layer.strokeStyle = options.series.pie.stroke.color; + layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); + layer.stroke(); + layer.closePath(); + layer.restore(); + + // TODO: add extra shadow inside hole (with a mask) if the pie is tilted. + } + } + + //-- Additional Interactive related functions -- + + function isPointInPoly(poly, pt) { + for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) + ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1])) + && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) + && (c = !c); + return c; + } + + function findNearbySlice(mouseX, mouseY) { + + var slices = plot.getData(), + options = plot.getOptions(), + radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius, + x, y; + + for (var i = 0; i < slices.length; ++i) { + + var s = slices[i]; + + if (s.pie.show) { + + ctx.save(); + ctx.beginPath(); + ctx.moveTo(0, 0); // Center of the pie + //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here. + ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false); + ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false); + ctx.closePath(); + x = mouseX - centerLeft; + y = mouseY - centerTop; + + if (ctx.isPointInPath) { + if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { + ctx.restore(); + return { + datapoint: [s.percent, s.data], + dataIndex: 0, + series: s, + seriesIndex: i + }; + } + } else { + + // excanvas for IE doesn;t support isPointInPath, this is a workaround. + + var p1X = radius * Math.cos(s.startAngle), + p1Y = radius * Math.sin(s.startAngle), + p2X = radius * Math.cos(s.startAngle + s.angle / 4), + p2Y = radius * Math.sin(s.startAngle + s.angle / 4), + p3X = radius * Math.cos(s.startAngle + s.angle / 2), + p3Y = radius * Math.sin(s.startAngle + s.angle / 2), + p4X = radius * Math.cos(s.startAngle + s.angle / 1.5), + p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5), + p5X = radius * Math.cos(s.startAngle + s.angle), + p5Y = radius * Math.sin(s.startAngle + s.angle), + arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]], + arrPoint = [x, y]; + + // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt? + + if (isPointInPoly(arrPoly, arrPoint)) { + ctx.restore(); + return { + datapoint: [s.percent, s.data], + dataIndex: 0, + series: s, + seriesIndex: i + }; + } + } + + ctx.restore(); + } + } + + return null; + } + + function onMouseMove(e) { + triggerClickHoverEvent("plothover", e); + } + + function onClick(e) { + triggerClickHoverEvent("plotclick", e); + } + + // trigger click or hover event (they send the same parameters so we share their code) + + function triggerClickHoverEvent(eventname, e) { + + var offset = plot.offset(); + var canvasX = parseInt(e.pageX - offset.left); + var canvasY = parseInt(e.pageY - offset.top); + var item = findNearbySlice(canvasX, canvasY); + + if (options.grid.autoHighlight) { + + // clear auto-highlights + + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.auto == eventname && !(item && h.series == item.series)) { + unhighlight(h.series); + } + } + } + + // highlight the slice + + if (item) { + highlight(item.series, eventname); + } + + // trigger any hover bind events + + var pos = { pageX: e.pageX, pageY: e.pageY }; + target.trigger(eventname, [pos, item]); + } + + function highlight(s, auto) { + //if (typeof s == "number") { + // s = series[s]; + //} + + var i = indexOfHighlight(s); + + if (i == -1) { + highlights.push({ series: s, auto: auto }); + plot.triggerRedrawOverlay(); + } else if (!auto) { + highlights[i].auto = false; + } + } + + function unhighlight(s) { + if (s == null) { + highlights = []; + plot.triggerRedrawOverlay(); + } + + //if (typeof s == "number") { + // s = series[s]; + //} + + var i = indexOfHighlight(s); + + if (i != -1) { + highlights.splice(i, 1); + plot.triggerRedrawOverlay(); + } + } + + function indexOfHighlight(s) { + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.series == s) + return i; + } + return -1; + } + + function drawOverlay(plot, octx) { + + var options = plot.getOptions(); + + var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; + + octx.save(); + octx.translate(centerLeft, centerTop); + octx.scale(1, options.series.pie.tilt); + + for (var i = 0; i < highlights.length; ++i) { + drawHighlight(highlights[i].series); + } + + drawDonutHole(octx); + + octx.restore(); + + function drawHighlight(series) { + + if (series.angle <= 0 || isNaN(series.angle)) { + return; + } + + //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString(); + octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor + octx.beginPath(); + if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) { + octx.moveTo(0, 0); // Center of the pie + } + octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false); + octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false); + octx.closePath(); + octx.fill(); + } + } + } // end init (plugin body) + + // define pie specific options and their default values + + var options = { + series: { + pie: { + show: false, + radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value) + innerRadius: 0, /* for donut */ + startAngle: 3/2, + tilt: 1, + shadow: { + left: 5, // shadow left offset + top: 15, // shadow top offset + alpha: 0.02 // shadow alpha + }, + offset: { + top: 0, + left: "auto" + }, + stroke: { + color: "#fff", + width: 1 + }, + label: { + show: "auto", + formatter: function(label, slice) { + return "
    " + label + "
    " + Math.round(slice.percent) + "%
    "; + }, // formatter function + radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) + background: { + color: null, + opacity: 0 + }, + threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow) + }, + combine: { + threshold: -1, // percentage at which to combine little slices into one larger slice + color: null, // color to give the new slice (auto-generated if null) + label: "Other" // label to give the new slice + }, + highlight: { + //color: "#fff", // will add this functionality once parseColor is available + opacity: 0.5 + } + } + } + }; + + $.plot.plugins.push({ + init: init, + options: options, + name: "pie", + version: "1.1" + }); + +})(jQuery); diff --git a/public/static/libs/flot/jquery.flot.pie.min.js b/public/static/libs/flot/jquery.flot.pie.min.js new file mode 100644 index 0000000..c5a37a2 --- /dev/null +++ b/public/static/libs/flot/jquery.flot.pie.min.js @@ -0,0 +1,7 @@ +/* Javascript plotting library for jQuery, version 0.8.3. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ +(function($){var REDRAW_ATTEMPTS=10;var REDRAW_SHRINK=.95;function init(plot){var canvas=null,target=null,options=null,maxRadius=null,centerLeft=null,centerTop=null,processed=false,ctx=null;var highlights=[];plot.hooks.processOptions.push(function(plot,options){if(options.series.pie.show){options.grid.show=false;if(options.series.pie.label.show=="auto"){if(options.legend.show){options.series.pie.label.show=false}else{options.series.pie.label.show=true}}if(options.series.pie.radius=="auto"){if(options.series.pie.label.show){options.series.pie.radius=3/4}else{options.series.pie.radius=1}}if(options.series.pie.tilt>1){options.series.pie.tilt=1}else if(options.series.pie.tilt<0){options.series.pie.tilt=0}}});plot.hooks.bindEvents.push(function(plot,eventHolder){var options=plot.getOptions();if(options.series.pie.show){if(options.grid.hoverable){eventHolder.unbind("mousemove").mousemove(onMouseMove)}if(options.grid.clickable){eventHolder.unbind("click").click(onClick)}}});plot.hooks.processDatapoints.push(function(plot,series,data,datapoints){var options=plot.getOptions();if(options.series.pie.show){processDatapoints(plot,series,data,datapoints)}});plot.hooks.drawOverlay.push(function(plot,octx){var options=plot.getOptions();if(options.series.pie.show){drawOverlay(plot,octx)}});plot.hooks.draw.push(function(plot,newCtx){var options=plot.getOptions();if(options.series.pie.show){draw(plot,newCtx)}});function processDatapoints(plot,series,datapoints){if(!processed){processed=true;canvas=plot.getCanvas();target=$(canvas).parent();options=plot.getOptions();plot.setData(combine(plot.getData()))}}function combine(data){var total=0,combined=0,numCombined=0,color=options.series.pie.combine.color,newdata=[];for(var i=0;ioptions.series.pie.combine.threshold){newdata.push($.extend(data[i],{data:[[1,value]],color:data[i].color,label:data[i].label,angle:value*Math.PI*2/total,percent:value/(total/100)}))}}if(numCombined>1){newdata.push({data:[[1,combined]],color:color,label:options.series.pie.combine.label,angle:combined*Math.PI*2/total,percent:combined/(total/100)})}return newdata}function draw(plot,newCtx){if(!target){return}var canvasWidth=plot.getPlaceholder().width(),canvasHeight=plot.getPlaceholder().height(),legendWidth=target.children().filter(".legend").children().width()||0;ctx=newCtx;processed=false;maxRadius=Math.min(canvasWidth,canvasHeight/options.series.pie.tilt)/2;centerTop=canvasHeight/2+options.series.pie.offset.top;centerLeft=canvasWidth/2;if(options.series.pie.offset.left=="auto"){if(options.legend.position.match("w")){centerLeft+=legendWidth/2}else{centerLeft-=legendWidth/2}if(centerLeftcanvasWidth-maxRadius){centerLeft=canvasWidth-maxRadius}}else{centerLeft+=options.series.pie.offset.left}var slices=plot.getData(),attempts=0;do{if(attempts>0){maxRadius*=REDRAW_SHRINK}attempts+=1;clear();if(options.series.pie.tilt<=.8){drawShadow()}}while(!drawPie()&&attempts=REDRAW_ATTEMPTS){clear();target.prepend("
    Could not draw pie with labels contained inside canvas
    ")}if(plot.setSeries&&plot.insertLegend){plot.setSeries(slices);plot.insertLegend()}function clear(){ctx.clearRect(0,0,canvasWidth,canvasHeight);target.children().filter(".pieLabel, .pieLabelBackground").remove()}function drawShadow(){var shadowLeft=options.series.pie.shadow.left;var shadowTop=options.series.pie.shadow.top;var edge=10;var alpha=options.series.pie.shadow.alpha;var radius=options.series.pie.radius>1?options.series.pie.radius:maxRadius*options.series.pie.radius;if(radius>=canvasWidth/2-shadowLeft||radius*options.series.pie.tilt>=canvasHeight/2-shadowTop||radius<=edge){return}ctx.save();ctx.translate(shadowLeft,shadowTop);ctx.globalAlpha=alpha;ctx.fillStyle="#000";ctx.translate(centerLeft,centerTop);ctx.scale(1,options.series.pie.tilt);for(var i=1;i<=edge;i++){ctx.beginPath();ctx.arc(0,0,radius,0,Math.PI*2,false);ctx.fill();radius-=i}ctx.restore()}function drawPie(){var startAngle=Math.PI*options.series.pie.startAngle;var radius=options.series.pie.radius>1?options.series.pie.radius:maxRadius*options.series.pie.radius;ctx.save();ctx.translate(centerLeft,centerTop);ctx.scale(1,options.series.pie.tilt);ctx.save();var currentAngle=startAngle;for(var i=0;i0){ctx.save();ctx.lineWidth=options.series.pie.stroke.width;currentAngle=startAngle;for(var i=0;i1e-9){ctx.moveTo(0,0)}ctx.arc(0,0,radius,currentAngle,currentAngle+angle/2,false);ctx.arc(0,0,radius,currentAngle+angle/2,currentAngle+angle,false);ctx.closePath();currentAngle+=angle;if(fill){ctx.fill()}else{ctx.stroke()}}function drawLabels(){var currentAngle=startAngle;var radius=options.series.pie.label.radius>1?options.series.pie.label.radius:maxRadius*options.series.pie.label.radius;for(var i=0;i=options.series.pie.label.threshold*100){if(!drawLabel(slices[i],currentAngle,i)){return false}}currentAngle+=slices[i].angle}return true;function drawLabel(slice,startAngle,index){if(slice.data[0][1]==0){return true}var lf=options.legend.labelFormatter,text,plf=options.series.pie.label.formatter;if(lf){text=lf(slice.label,slice)}else{text=slice.label}if(plf){text=plf(text,slice)}var halfAngle=(startAngle+slice.angle+startAngle)/2;var x=centerLeft+Math.round(Math.cos(halfAngle)*radius);var y=centerTop+Math.round(Math.sin(halfAngle)*radius)*options.series.pie.tilt;var html=""+text+"";target.append(html);var label=target.children("#pieLabel"+index);var labelTop=y-label.height()/2;var labelLeft=x-label.width()/2;label.css("top",labelTop);label.css("left",labelLeft);if(0-labelTop>0||0-labelLeft>0||canvasHeight-(labelTop+label.height())<0||canvasWidth-(labelLeft+label.width())<0){return false}if(options.series.pie.label.background.opacity!=0){var c=options.series.pie.label.background.color;if(c==null){c=slice.color}var pos="top:"+labelTop+"px;left:"+labelLeft+"px;";$("
    ").css("opacity",options.series.pie.label.background.opacity).insertBefore(label)}return true}}}}function drawDonutHole(layer){if(options.series.pie.innerRadius>0){layer.save();var innerRadius=options.series.pie.innerRadius>1?options.series.pie.innerRadius:maxRadius*options.series.pie.innerRadius;layer.globalCompositeOperation="destination-out";layer.beginPath();layer.fillStyle=options.series.pie.stroke.color;layer.arc(0,0,innerRadius,0,Math.PI*2,false);layer.fill();layer.closePath();layer.restore();layer.save();layer.beginPath();layer.strokeStyle=options.series.pie.stroke.color;layer.arc(0,0,innerRadius,0,Math.PI*2,false);layer.stroke();layer.closePath();layer.restore()}}function isPointInPoly(poly,pt){for(var c=false,i=-1,l=poly.length,j=l-1;++i1?options.series.pie.radius:maxRadius*options.series.pie.radius,x,y;for(var i=0;i1?options.series.pie.radius:maxRadius*options.series.pie.radius;octx.save();octx.translate(centerLeft,centerTop);octx.scale(1,options.series.pie.tilt);for(var i=0;i1e-9){octx.moveTo(0,0)}octx.arc(0,0,radius,series.startAngle,series.startAngle+series.angle/2,false);octx.arc(0,0,radius,series.startAngle+series.angle/2,series.startAngle+series.angle,false);octx.closePath();octx.fill()}}}var options={series:{pie:{show:false,radius:"auto",innerRadius:0,startAngle:3/2,tilt:1,shadow:{left:5,top:15,alpha:.02},offset:{top:0,left:"auto"},stroke:{color:"#fff",width:1},label:{show:"auto",formatter:function(label,slice){return"
    "+label+"
    "+Math.round(slice.percent)+"%
    "},radius:1,background:{color:null,opacity:0},threshold:0},combine:{threshold:-1,color:null,label:"Other"},highlight:{opacity:.5}}}};$.plot.plugins.push({init:init,options:options,name:"pie",version:"1.1"})})(jQuery); \ No newline at end of file diff --git a/public/static/libs/flot/jquery.flot.resize.js b/public/static/libs/flot/jquery.flot.resize.js new file mode 100644 index 0000000..a1a6894 --- /dev/null +++ b/public/static/libs/flot/jquery.flot.resize.js @@ -0,0 +1,59 @@ +/* Flot plugin for automatically redrawing plots as the placeholder resizes. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +It works by listening for changes on the placeholder div (through the jQuery +resize event plugin) - if the size changes, it will redraw the plot. + +There are no options. If you need to disable the plugin for some plots, you +can just fix the size of their placeholders. + +*/ + +/* Inline dependency: + * jQuery resize event - v1.1 - 3/14/2010 + * http://benalman.com/projects/jquery-resize-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ +(function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this); + +(function ($) { + var options = { }; // no options + + function init(plot) { + function onResize() { + var placeholder = plot.getPlaceholder(); + + // somebody might have hidden us and we can't plot + // when we don't have the dimensions + if (placeholder.width() == 0 || placeholder.height() == 0) + return; + + plot.resize(); + plot.setupGrid(); + plot.draw(); + } + + function bindEvents(plot, eventHolder) { + plot.getPlaceholder().resize(onResize); + } + + function shutdown(plot, eventHolder) { + plot.getPlaceholder().unbind("resize", onResize); + } + + plot.hooks.bindEvents.push(bindEvents); + plot.hooks.shutdown.push(shutdown); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'resize', + version: '1.0' + }); +})(jQuery); diff --git a/public/static/libs/flot/jquery.flot.resize.min.js b/public/static/libs/flot/jquery.flot.resize.min.js new file mode 100644 index 0000000..a4fb5be --- /dev/null +++ b/public/static/libs/flot/jquery.flot.resize.min.js @@ -0,0 +1,7 @@ +/* Javascript plotting library for jQuery, version 0.8.3. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ +(function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this);(function($){var options={};function init(plot){function onResize(){var placeholder=plot.getPlaceholder();if(placeholder.width()==0||placeholder.height()==0)return;plot.resize();plot.setupGrid();plot.draw()}function bindEvents(plot,eventHolder){plot.getPlaceholder().resize(onResize)}function shutdown(plot,eventHolder){plot.getPlaceholder().unbind("resize",onResize)}plot.hooks.bindEvents.push(bindEvents);plot.hooks.shutdown.push(shutdown)}$.plot.plugins.push({init:init,options:options,name:"resize",version:"1.0"})})(jQuery); \ No newline at end of file diff --git a/public/static/libs/flot/jquery.flot.stack.js b/public/static/libs/flot/jquery.flot.stack.js new file mode 100644 index 0000000..e3f6097 --- /dev/null +++ b/public/static/libs/flot/jquery.flot.stack.js @@ -0,0 +1,188 @@ +/* Flot plugin for stacking data sets rather than overlyaing them. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin assumes the data is sorted on x (or y if stacking horizontally). +For line charts, it is assumed that if a line has an undefined gap (from a +null point), then the line above it should have the same gap - insert zeros +instead of "null" if you want another behaviour. This also holds for the start +and end of the chart. Note that stacking a mix of positive and negative values +in most instances doesn't make sense (so it looks weird). + +Two or more series are stacked when their "stack" attribute is set to the same +key (which can be any number or string or just "true"). To specify the default +stack, you can set the stack option like this: + + series: { + stack: null/false, true, or a key (number/string) + } + +You can also specify it for a single series, like this: + + $.plot( $("#placeholder"), [{ + data: [ ... ], + stack: true + }]) + +The stacking order is determined by the order of the data series in the array +(later series end up on top of the previous). + +Internally, the plugin modifies the datapoints in each series, adding an +offset to the y value. For line series, extra data points are inserted through +interpolation. If there's a second y value, it's also adjusted (e.g for bar +charts or filled areas). + +*/ + +(function ($) { + var options = { + series: { stack: null } // or number/string + }; + + function init(plot) { + function findMatchingSeries(s, allseries) { + var res = null; + for (var i = 0; i < allseries.length; ++i) { + if (s == allseries[i]) + break; + + if (allseries[i].stack == s.stack) + res = allseries[i]; + } + + return res; + } + + function stackData(plot, s, datapoints) { + if (s.stack == null || s.stack === false) + return; + + var other = findMatchingSeries(s, plot.getData()); + if (!other) + return; + + var ps = datapoints.pointsize, + points = datapoints.points, + otherps = other.datapoints.pointsize, + otherpoints = other.datapoints.points, + newpoints = [], + px, py, intery, qx, qy, bottom, + withlines = s.lines.show, + horizontal = s.bars.horizontal, + withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), + withsteps = withlines && s.lines.steps, + fromgap = true, + keyOffset = horizontal ? 1 : 0, + accumulateOffset = horizontal ? 0 : 1, + i = 0, j = 0, l, m; + + while (true) { + if (i >= points.length) + break; + + l = newpoints.length; + + if (points[i] == null) { + // copy gaps + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + i += ps; + } + else if (j >= otherpoints.length) { + // for lines, we can't use the rest of the points + if (!withlines) { + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + } + i += ps; + } + else if (otherpoints[j] == null) { + // oops, got a gap + for (m = 0; m < ps; ++m) + newpoints.push(null); + fromgap = true; + j += otherps; + } + else { + // cases where we actually got two points + px = points[i + keyOffset]; + py = points[i + accumulateOffset]; + qx = otherpoints[j + keyOffset]; + qy = otherpoints[j + accumulateOffset]; + bottom = 0; + + if (px == qx) { + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + + newpoints[l + accumulateOffset] += qy; + bottom = qy; + + i += ps; + j += otherps; + } + else if (px > qx) { + // we got past point below, might need to + // insert interpolated extra point + if (withlines && i > 0 && points[i - ps] != null) { + intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); + newpoints.push(qx); + newpoints.push(intery + qy); + for (m = 2; m < ps; ++m) + newpoints.push(points[i + m]); + bottom = qy; + } + + j += otherps; + } + else { // px < qx + if (fromgap && withlines) { + // if we come from a gap, we just skip this point + i += ps; + continue; + } + + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + + // we might be able to interpolate a point below, + // this can give us a better y + if (withlines && j > 0 && otherpoints[j - otherps] != null) + bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); + + newpoints[l + accumulateOffset] += bottom; + + i += ps; + } + + fromgap = false; + + if (l != newpoints.length && withbottom) + newpoints[l + 2] += bottom; + } + + // maintain the line steps invariant + if (withsteps && l != newpoints.length && l > 0 + && newpoints[l] != null + && newpoints[l] != newpoints[l - ps] + && newpoints[l + 1] != newpoints[l - ps + 1]) { + for (m = 0; m < ps; ++m) + newpoints[l + ps + m] = newpoints[l + m]; + newpoints[l + 1] = newpoints[l - ps + 1]; + } + } + + datapoints.points = newpoints; + } + + plot.hooks.processDatapoints.push(stackData); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'stack', + version: '1.2' + }); +})(jQuery); diff --git a/public/static/libs/flot/jquery.flot.stack.min.js b/public/static/libs/flot/jquery.flot.stack.min.js new file mode 100644 index 0000000..2cf34ce --- /dev/null +++ b/public/static/libs/flot/jquery.flot.stack.min.js @@ -0,0 +1,7 @@ +/* Javascript plotting library for jQuery, version 0.8.3. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ +(function($){var options={series:{stack:null}};function init(plot){function findMatchingSeries(s,allseries){var res=null;for(var i=0;i2&&(horizontal?datapoints.format[2].x:datapoints.format[2].y),withsteps=withlines&&s.lines.steps,fromgap=true,keyOffset=horizontal?1:0,accumulateOffset=horizontal?0:1,i=0,j=0,l,m;while(true){if(i>=points.length)break;l=newpoints.length;if(points[i]==null){for(m=0;m=otherpoints.length){if(!withlines){for(m=0;mqx){if(withlines&&i>0&&points[i-ps]!=null){intery=py+(points[i-ps+accumulateOffset]-py)*(qx-px)/(points[i-ps+keyOffset]-px);newpoints.push(qx);newpoints.push(intery+qy);for(m=2;m0&&otherpoints[j-otherps]!=null)bottom=qy+(otherpoints[j-otherps+accumulateOffset]-qy)*(px-qx)/(otherpoints[j-otherps+keyOffset]-qx);newpoints[l+accumulateOffset]+=bottom;i+=ps}fromgap=false;if(l!=newpoints.length&&withbottom)newpoints[l+2]+=bottom}if(withsteps&&l!=newpoints.length&&l>0&&newpoints[l]!=null&&newpoints[l]!=newpoints[l-ps]&&newpoints[l+1]!=newpoints[l-ps+1]){for(m=0;m tags, normalized to work cross-browser) +--------------------------------------------------------------------------------------------------*/ + +.fc button { + /* force height to include the border and padding */ + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + + /* dimensions */ + margin: 0; + height: 2.1em; + padding: 0 .6em; + + /* text & cursor */ + font-size: 1em; /* normalize */ + white-space: nowrap; + cursor: pointer; +} + +/* Firefox has an annoying inner border */ +.fc button::-moz-focus-inner { margin: 0; padding: 0; } + +.fc-state-default { /* non-theme */ + border: 1px solid; +} + +.fc-state-default.fc-corner-left { /* non-theme */ + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} + +.fc-state-default.fc-corner-right { /* non-theme */ + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +/* icons in buttons */ + +.fc button .fc-icon { /* non-theme */ + position: relative; + top: -0.05em; /* seems to be a good adjustment across browsers */ + margin: 0 .2em; + vertical-align: middle; +} + +/* + button states + borrowed from twitter bootstrap (http://twitter.github.com/bootstrap/) +*/ + +.fc-state-default { + background-color: #f5f5f5; + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); + background-repeat: repeat-x; + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + color: #333; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.fc-state-hover, +.fc-state-down, +.fc-state-active, +.fc-state-disabled { + color: #333333; + background-color: #e6e6e6; +} + +.fc-state-hover { + color: #333333; + text-decoration: none; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} + +.fc-state-down, +.fc-state-active { + background-color: #cccccc; + background-image: none; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.fc-state-disabled { + cursor: default; + background-image: none; + opacity: 0.65; + filter: alpha(opacity=65); + box-shadow: none; +} + + +/* Buttons Groups +--------------------------------------------------------------------------------------------------*/ + +.fc-button-group { + display: inline-block; +} + +/* +every button that is not first in a button group should scootch over one pixel and cover the +previous button's border... +*/ + +.fc .fc-button-group > * { /* extra precedence b/c buttons have margin set to zero */ + float: left; + margin: 0 0 0 -1px; +} + +.fc .fc-button-group > :first-child { /* same */ + margin-left: 0; +} + + +/* Popover +--------------------------------------------------------------------------------------------------*/ + +.fc-popover { + position: absolute; + box-shadow: 0 2px 6px rgba(0,0,0,.15); +} + +.fc-popover .fc-header { /* TODO: be more consistent with fc-head/fc-body */ + padding: 2px 4px; +} + +.fc-popover .fc-header .fc-title { + margin: 0 2px; +} + +.fc-popover .fc-header .fc-close { + cursor: pointer; +} + +.fc-ltr .fc-popover .fc-header .fc-title, +.fc-rtl .fc-popover .fc-header .fc-close { + float: left; +} + +.fc-rtl .fc-popover .fc-header .fc-title, +.fc-ltr .fc-popover .fc-header .fc-close { + float: right; +} + +/* unthemed */ + +.fc-unthemed .fc-popover { + border-width: 1px; + border-style: solid; +} + +.fc-unthemed .fc-popover .fc-header .fc-close { + font-size: .9em; + margin-top: 2px; +} + +/* jqui themed */ + +.fc-popover > .ui-widget-header + .ui-widget-content { + border-top: 0; /* where they meet, let the header have the border */ +} + + +/* Misc Reusable Components +--------------------------------------------------------------------------------------------------*/ + +.fc-divider { + border-style: solid; + border-width: 1px; +} + +hr.fc-divider { + height: 0; + margin: 0; + padding: 0 0 2px; /* height is unreliable across browsers, so use padding */ + border-width: 1px 0; +} + +.fc-clear { + clear: both; +} + +.fc-bg, +.fc-bgevent-skeleton, +.fc-highlight-skeleton, +.fc-helper-skeleton { + /* these element should always cling to top-left/right corners */ + position: absolute; + top: 0; + left: 0; + right: 0; +} + +.fc-bg { + bottom: 0; /* strech bg to bottom edge */ +} + +.fc-bg table { + height: 100%; /* strech bg to bottom edge */ +} + + +/* Tables +--------------------------------------------------------------------------------------------------*/ + +.fc table { + width: 100%; + box-sizing: border-box; /* fix scrollbar issue in firefox */ + table-layout: fixed; + border-collapse: collapse; + border-spacing: 0; + font-size: 1em; /* normalize cross-browser */ +} + +.fc th { + text-align: center; +} + +.fc th, +.fc td { + border-style: solid; + border-width: 1px; + padding: 0; + vertical-align: top; +} + +.fc td.fc-today { + border-style: double; /* overcome neighboring borders */ +} + + +/* Fake Table Rows +--------------------------------------------------------------------------------------------------*/ + +.fc .fc-row { /* extra precedence to overcome themes w/ .ui-widget-content forcing a 1px border */ + /* no visible border by default. but make available if need be (scrollbar width compensation) */ + border-style: solid; + border-width: 0; +} + +.fc-row table { + /* don't put left/right border on anything within a fake row. + the outer tbody will worry about this */ + border-left: 0 hidden transparent; + border-right: 0 hidden transparent; + + /* no bottom borders on rows */ + border-bottom: 0 hidden transparent; +} + +.fc-row:first-child table { + border-top: 0 hidden transparent; /* no top border on first row */ +} + + +/* Day Row (used within the header and the DayGrid) +--------------------------------------------------------------------------------------------------*/ + +.fc-row { + position: relative; +} + +.fc-row .fc-bg { + z-index: 1; +} + +/* highlighting cells & background event skeleton */ + +.fc-row .fc-bgevent-skeleton, +.fc-row .fc-highlight-skeleton { + bottom: 0; /* stretch skeleton to bottom of row */ +} + +.fc-row .fc-bgevent-skeleton table, +.fc-row .fc-highlight-skeleton table { + height: 100%; /* stretch skeleton to bottom of row */ +} + +.fc-row .fc-highlight-skeleton td, +.fc-row .fc-bgevent-skeleton td { + border-color: transparent; +} + +.fc-row .fc-bgevent-skeleton { + z-index: 2; + +} + +.fc-row .fc-highlight-skeleton { + z-index: 3; +} + +/* +row content (which contains day/week numbers and events) as well as "helper" (which contains +temporary rendered events). +*/ + +.fc-row .fc-content-skeleton { + position: relative; + z-index: 4; + padding-bottom: 2px; /* matches the space above the events */ +} + +.fc-row .fc-helper-skeleton { + z-index: 5; +} + +.fc-row .fc-content-skeleton td, +.fc-row .fc-helper-skeleton td { + /* see-through to the background below */ + background: none; /* in case s are globally styled */ + border-color: transparent; + + /* don't put a border between events and/or the day number */ + border-bottom: 0; +} + +.fc-row .fc-content-skeleton tbody td, /* cells with events inside (so NOT the day number cell) */ +.fc-row .fc-helper-skeleton tbody td { + /* don't put a border between event cells */ + border-top: 0; +} + + +/* Scrolling Container +--------------------------------------------------------------------------------------------------*/ + +.fc-scroller { + -webkit-overflow-scrolling: touch; +} + +/* TODO: move to agenda/basic */ +.fc-scroller > .fc-day-grid, +.fc-scroller > .fc-time-grid { + position: relative; /* re-scope all positions */ + width: 100%; /* hack to force re-sizing this inner element when scrollbars appear/disappear */ +} + + +/* Global Event Styles +--------------------------------------------------------------------------------------------------*/ + +.fc-event { + position: relative; /* for resize handle and other inner positioning */ + display: block; /* make the tag block */ + font-size: .85em; + line-height: 1.3; + border-radius: 3px; + border: 1px solid #3a87ad; /* default BORDER color */ + background-color: #3a87ad; /* default BACKGROUND color */ + font-weight: normal; /* undo jqui's ui-widget-header bold */ +} + +/* overpower some of bootstrap's and jqui's styles on tags */ +.fc-event, +.fc-event:hover, +.ui-widget .fc-event { + color: #fff; /* default TEXT color */ + text-decoration: none; /* if has an href */ +} + +.fc-event[href], +.fc-event.fc-draggable { + cursor: pointer; /* give events with links and draggable events a hand mouse pointer */ +} + +.fc-not-allowed, /* causes a "warning" cursor. applied on body */ +.fc-not-allowed .fc-event { /* to override an event's custom cursor */ + cursor: not-allowed; +} + +.fc-event .fc-bg { /* the generic .fc-bg already does position */ + z-index: 1; + background: #fff; + opacity: .25; + filter: alpha(opacity=25); /* for IE */ +} + +.fc-event .fc-content { + position: relative; + z-index: 2; +} + +/* resizer (cursor AND touch devices) */ + +.fc-event .fc-resizer { + position: absolute; + z-index: 4; +} + +/* resizer (touch devices) */ + +.fc-event .fc-resizer { + display: none; +} + +.fc-event.fc-allow-mouse-resize .fc-resizer, +.fc-event.fc-selected .fc-resizer { + /* only show when hovering or selected (with touch) */ + display: block; +} + +/* hit area */ + +.fc-event.fc-selected .fc-resizer:before { + /* 40x40 touch area */ + content: ""; + position: absolute; + z-index: 9999; /* user of this util can scope within a lower z-index */ + top: 50%; + left: 50%; + width: 40px; + height: 40px; + margin-left: -20px; + margin-top: -20px; +} + + +/* Event Selection (only for touch devices) +--------------------------------------------------------------------------------------------------*/ + +.fc-event.fc-selected { + z-index: 9999 !important; /* overcomes inline z-index */ + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); +} + +.fc-event.fc-selected.fc-dragging { + box-shadow: 0 2px 7px rgba(0, 0, 0, 0.3); +} + + +/* Horizontal Events +--------------------------------------------------------------------------------------------------*/ + +/* bigger touch area when selected */ +.fc-h-event.fc-selected:before { + content: ""; + position: absolute; + z-index: 3; /* below resizers */ + top: -10px; + bottom: -10px; + left: 0; + right: 0; +} + +/* events that are continuing to/from another week. kill rounded corners and butt up against edge */ + +.fc-ltr .fc-h-event.fc-not-start, +.fc-rtl .fc-h-event.fc-not-end { + margin-left: 0; + border-left-width: 0; + padding-left: 1px; /* replace the border with padding */ + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.fc-ltr .fc-h-event.fc-not-end, +.fc-rtl .fc-h-event.fc-not-start { + margin-right: 0; + border-right-width: 0; + padding-right: 1px; /* replace the border with padding */ + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +/* resizer (cursor AND touch devices) */ + +/* left resizer */ +.fc-ltr .fc-h-event .fc-start-resizer, +.fc-rtl .fc-h-event .fc-end-resizer { + cursor: w-resize; + left: -1px; /* overcome border */ +} + +/* right resizer */ +.fc-ltr .fc-h-event .fc-end-resizer, +.fc-rtl .fc-h-event .fc-start-resizer { + cursor: e-resize; + right: -1px; /* overcome border */ +} + +/* resizer (mouse devices) */ + +.fc-h-event.fc-allow-mouse-resize .fc-resizer { + width: 7px; + top: -1px; /* overcome top border */ + bottom: -1px; /* overcome bottom border */ +} + +/* resizer (touch devices) */ + +.fc-h-event.fc-selected .fc-resizer { + /* 8x8 little dot */ + border-radius: 4px; + border-width: 1px; + width: 6px; + height: 6px; + border-style: solid; + border-color: inherit; + background: #fff; + /* vertically center */ + top: 50%; + margin-top: -4px; +} + +/* left resizer */ +.fc-ltr .fc-h-event.fc-selected .fc-start-resizer, +.fc-rtl .fc-h-event.fc-selected .fc-end-resizer { + margin-left: -4px; /* centers the 8x8 dot on the left edge */ +} + +/* right resizer */ +.fc-ltr .fc-h-event.fc-selected .fc-end-resizer, +.fc-rtl .fc-h-event.fc-selected .fc-start-resizer { + margin-right: -4px; /* centers the 8x8 dot on the right edge */ +} + + +/* DayGrid events +---------------------------------------------------------------------------------------------------- +We use the full "fc-day-grid-event" class instead of using descendants because the event won't +be a descendant of the grid when it is being dragged. +*/ + +.fc-day-grid-event { + margin: 1px 2px 0; /* spacing between events and edges */ + padding: 0 1px; +} + +.fc-day-grid-event.fc-selected:after { + content: ""; + position: absolute; + z-index: 1; /* same z-index as fc-bg, behind text */ + /* overcome the borders */ + top: -1px; + right: -1px; + bottom: -1px; + left: -1px; + /* darkening effect */ + background: #000; + opacity: .25; + filter: alpha(opacity=25); /* for IE */ +} + +.fc-day-grid-event .fc-content { /* force events to be one-line tall */ + white-space: nowrap; + overflow: hidden; +} + +.fc-day-grid-event .fc-time { + font-weight: bold; +} + +/* resizer (cursor devices) */ + +/* left resizer */ +.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer, +.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer { + margin-left: -2px; /* to the day cell's edge */ +} + +/* right resizer */ +.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer, +.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer { + margin-right: -2px; /* to the day cell's edge */ +} + + +/* Event Limiting +--------------------------------------------------------------------------------------------------*/ + +/* "more" link that represents hidden events */ + +a.fc-more { + margin: 1px 3px; + font-size: .85em; + cursor: pointer; + text-decoration: none; +} + +a.fc-more:hover { + text-decoration: underline; +} + +.fc-limited { /* rows and cells that are hidden because of a "more" link */ + display: none; +} + +/* popover that appears when "more" link is clicked */ + +.fc-day-grid .fc-row { + z-index: 1; /* make the "more" popover one higher than this */ +} + +.fc-more-popover { + z-index: 2; + width: 220px; +} + +.fc-more-popover .fc-event-container { + padding: 10px; +} + + +/* Now Indicator +--------------------------------------------------------------------------------------------------*/ + +.fc-now-indicator { + position: absolute; + border: 0 solid red; +} + + +/* Utilities +--------------------------------------------------------------------------------------------------*/ + +.fc-unselectable { + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-touch-callout: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +/* Toolbar +--------------------------------------------------------------------------------------------------*/ + +.fc-toolbar { + text-align: center; + margin-bottom: 1em; +} + +.fc-toolbar .fc-left { + float: left; +} + +.fc-toolbar .fc-right { + float: right; +} + +.fc-toolbar .fc-center { + display: inline-block; +} + +/* the things within each left/right/center section */ +.fc .fc-toolbar > * > * { /* extra precedence to override button border margins */ + float: left; + margin-left: .75em; +} + +/* the first thing within each left/center/right section */ +.fc .fc-toolbar > * > :first-child { /* extra precedence to override button border margins */ + margin-left: 0; +} + +/* title text */ + +.fc-toolbar h2 { + margin: 0; +} + +/* button layering (for border precedence) */ + +.fc-toolbar button { + position: relative; +} + +.fc-toolbar .fc-state-hover, +.fc-toolbar .ui-state-hover { + z-index: 2; +} + +.fc-toolbar .fc-state-down { + z-index: 3; +} + +.fc-toolbar .fc-state-active, +.fc-toolbar .ui-state-active { + z-index: 4; +} + +.fc-toolbar button:focus { + z-index: 5; +} + + +/* View Structure +--------------------------------------------------------------------------------------------------*/ + +/* undo twitter bootstrap's box-sizing rules. normalizes positioning techniques */ +/* don't do this for the toolbar because we'll want bootstrap to style those buttons as some pt */ +.fc-view-container *, +.fc-view-container *:before, +.fc-view-container *:after { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +.fc-view, /* scope positioning and z-index's for everything within the view */ +.fc-view > table { /* so dragged elements can be above the view's main element */ + position: relative; + z-index: 1; +} + +/* BasicView +--------------------------------------------------------------------------------------------------*/ + +/* day row structure */ + +.fc-basicWeek-view .fc-content-skeleton, +.fc-basicDay-view .fc-content-skeleton { + /* we are sure there are no day numbers in these views, so... */ + padding-top: 1px; /* add a pixel to make sure there are 2px padding above events */ + padding-bottom: 1em; /* ensure a space at bottom of cell for user selecting/clicking */ +} + +.fc-basic-view .fc-body .fc-row { + min-height: 4em; /* ensure that all rows are at least this tall */ +} + +/* a "rigid" row will take up a constant amount of height because content-skeleton is absolute */ + +.fc-row.fc-rigid { + overflow: hidden; +} + +.fc-row.fc-rigid .fc-content-skeleton { + position: absolute; + top: 0; + left: 0; + right: 0; +} + +/* week and day number styling */ + +.fc-basic-view .fc-week-number, +.fc-basic-view .fc-day-number { + padding: 0 2px; +} + +.fc-basic-view td.fc-week-number span, +.fc-basic-view td.fc-day-number { + padding-top: 2px; + padding-bottom: 2px; +} + +.fc-basic-view .fc-week-number { + text-align: center; +} + +.fc-basic-view .fc-week-number span { + /* work around the way we do column resizing and ensure a minimum width */ + display: inline-block; + min-width: 1.25em; +} + +.fc-ltr .fc-basic-view .fc-day-number { + text-align: right; +} + +.fc-rtl .fc-basic-view .fc-day-number { + text-align: left; +} + +.fc-day-number.fc-other-month { + opacity: 0.3; + filter: alpha(opacity=30); /* for IE */ + /* opacity with small font can sometimes look too faded + might want to set the 'color' property instead + making day-numbers bold also fixes the problem */ +} + +/* AgendaView all-day area +--------------------------------------------------------------------------------------------------*/ + +.fc-agenda-view .fc-day-grid { + position: relative; + z-index: 2; /* so the "more.." popover will be over the time grid */ +} + +.fc-agenda-view .fc-day-grid .fc-row { + min-height: 3em; /* all-day section will never get shorter than this */ +} + +.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton { + padding-top: 1px; /* add a pixel to make sure there are 2px padding above events */ + padding-bottom: 1em; /* give space underneath events for clicking/selecting days */ +} + + +/* TimeGrid axis running down the side (for both the all-day area and the slot area) +--------------------------------------------------------------------------------------------------*/ + +.fc .fc-axis { /* .fc to overcome default cell styles */ + vertical-align: middle; + padding: 0 4px; + white-space: nowrap; +} + +.fc-ltr .fc-axis { + text-align: right; +} + +.fc-rtl .fc-axis { + text-align: left; +} + +.ui-widget td.fc-axis { + font-weight: normal; /* overcome jqui theme making it bold */ +} + + +/* TimeGrid Structure +--------------------------------------------------------------------------------------------------*/ + +.fc-time-grid-container, /* so scroll container's z-index is below all-day */ +.fc-time-grid { /* so slats/bg/content/etc positions get scoped within here */ + position: relative; + z-index: 1; +} + +.fc-time-grid { + min-height: 100%; /* so if height setting is 'auto', .fc-bg stretches to fill height */ +} + +.fc-time-grid table { /* don't put outer borders on slats/bg/content/etc */ + border: 0 hidden transparent; +} + +.fc-time-grid > .fc-bg { + z-index: 1; +} + +.fc-time-grid .fc-slats, +.fc-time-grid > hr { /* the
    AgendaView injects when grid is shorter than scroller */ + position: relative; + z-index: 2; +} + +.fc-time-grid .fc-content-col { + position: relative; /* because now-indicator lives directly inside */ +} + +.fc-time-grid .fc-content-skeleton { + position: absolute; + z-index: 3; + top: 0; + left: 0; + right: 0; +} + +/* divs within a cell within the fc-content-skeleton */ + +.fc-time-grid .fc-business-container { + position: relative; + z-index: 1; +} + +.fc-time-grid .fc-bgevent-container { + position: relative; + z-index: 2; +} + +.fc-time-grid .fc-highlight-container { + position: relative; + z-index: 3; +} + +.fc-time-grid .fc-event-container { + position: relative; + z-index: 4; +} + +.fc-time-grid .fc-now-indicator-line { + z-index: 5; +} + +.fc-time-grid .fc-helper-container { /* also is fc-event-container */ + position: relative; + z-index: 6; +} + + +/* TimeGrid Slats (lines that run horizontally) +--------------------------------------------------------------------------------------------------*/ + +.fc-time-grid .fc-slats td { + height: 1.5em; + border-bottom: 0; /* each cell is responsible for its top border */ +} + +.fc-time-grid .fc-slats .fc-minor td { + border-top-style: dotted; +} + +.fc-time-grid .fc-slats .ui-widget-content { /* for jqui theme */ + background: none; /* see through to fc-bg */ +} + + +/* TimeGrid Highlighting Slots +--------------------------------------------------------------------------------------------------*/ + +.fc-time-grid .fc-highlight-container { /* a div within a cell within the fc-highlight-skeleton */ + position: relative; /* scopes the left/right of the fc-highlight to be in the column */ +} + +.fc-time-grid .fc-highlight { + position: absolute; + left: 0; + right: 0; + /* top and bottom will be in by JS */ +} + + +/* TimeGrid Event Containment +--------------------------------------------------------------------------------------------------*/ + +.fc-ltr .fc-time-grid .fc-event-container { /* space on the sides of events for LTR (default) */ + margin: 0 2.5% 0 2px; +} + +.fc-rtl .fc-time-grid .fc-event-container { /* space on the sides of events for RTL */ + margin: 0 2px 0 2.5%; +} + +.fc-time-grid .fc-event, +.fc-time-grid .fc-bgevent { + position: absolute; + z-index: 1; /* scope inner z-index's */ +} + +.fc-time-grid .fc-bgevent { + /* background events always span full width */ + left: 0; + right: 0; +} + + +/* Generic Vertical Event +--------------------------------------------------------------------------------------------------*/ + +.fc-v-event.fc-not-start { /* events that are continuing from another day */ + /* replace space made by the top border with padding */ + border-top-width: 0; + padding-top: 1px; + + /* remove top rounded corners */ + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.fc-v-event.fc-not-end { + /* replace space made by the top border with padding */ + border-bottom-width: 0; + padding-bottom: 1px; + + /* remove bottom rounded corners */ + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + + +/* TimeGrid Event Styling +---------------------------------------------------------------------------------------------------- +We use the full "fc-time-grid-event" class instead of using descendants because the event won't +be a descendant of the grid when it is being dragged. +*/ + +.fc-time-grid-event { + overflow: hidden; /* don't let the bg flow over rounded corners */ +} + +.fc-time-grid-event.fc-selected { + /* need to allow touch resizers to extend outside event's bounding box */ + /* common fc-selected styles hide the fc-bg, so don't need this anyway */ + overflow: visible; +} + +.fc-time-grid-event.fc-selected .fc-bg { + display: none; /* hide semi-white background, to appear darker */ +} + +.fc-time-grid-event .fc-content { + overflow: hidden; /* for when .fc-selected */ +} + +.fc-time-grid-event .fc-time, +.fc-time-grid-event .fc-title { + padding: 0 1px; +} + +.fc-time-grid-event .fc-time { + font-size: .85em; + white-space: nowrap; +} + +/* short mode, where time and title are on the same line */ + +.fc-time-grid-event.fc-short .fc-content { + /* don't wrap to second line (now that contents will be inline) */ + white-space: nowrap; +} + +.fc-time-grid-event.fc-short .fc-time, +.fc-time-grid-event.fc-short .fc-title { + /* put the time and title on the same line */ + display: inline-block; + vertical-align: top; +} + +.fc-time-grid-event.fc-short .fc-time span { + display: none; /* don't display the full time text... */ +} + +.fc-time-grid-event.fc-short .fc-time:before { + content: attr(data-start); /* ...instead, display only the start time */ +} + +.fc-time-grid-event.fc-short .fc-time:after { + content: "\000A0-\000A0"; /* seperate with a dash, wrapped in nbsp's */ +} + +.fc-time-grid-event.fc-short .fc-title { + font-size: .85em; /* make the title text the same size as the time */ + padding: 0; /* undo padding from above */ +} + +/* resizer (cursor device) */ + +.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer { + left: 0; + right: 0; + bottom: 0; + height: 8px; + overflow: hidden; + line-height: 8px; + font-size: 11px; + font-family: monospace; + text-align: center; + cursor: s-resize; +} + +.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer:after { + content: "="; +} + +/* resizer (touch device) */ + +.fc-time-grid-event.fc-selected .fc-resizer { + /* 10x10 dot */ + border-radius: 5px; + border-width: 1px; + width: 8px; + height: 8px; + border-style: solid; + border-color: inherit; + background: #fff; + /* horizontally center */ + left: 50%; + margin-left: -5px; + /* center on the bottom edge */ + bottom: -5px; +} + + +/* Now Indicator +--------------------------------------------------------------------------------------------------*/ + +.fc-time-grid .fc-now-indicator-line { + border-top-width: 1px; + left: 0; + right: 0; +} + +/* arrow on axis */ + +.fc-time-grid .fc-now-indicator-arrow { + margin-top: -5px; /* vertically center on top coordinate */ +} + +.fc-ltr .fc-time-grid .fc-now-indicator-arrow { + left: 0; + /* triangle pointing right... */ + border-width: 5px 0 5px 6px; + border-top-color: transparent; + border-bottom-color: transparent; +} + +.fc-rtl .fc-time-grid .fc-now-indicator-arrow { + right: 0; + /* triangle pointing left... */ + border-width: 5px 6px 5px 0; + border-top-color: transparent; + border-bottom-color: transparent; +} diff --git a/public/static/libs/fullcalendar/fullcalendar.js b/public/static/libs/fullcalendar/fullcalendar.js new file mode 100644 index 0000000..d0d7d17 --- /dev/null +++ b/public/static/libs/fullcalendar/fullcalendar.js @@ -0,0 +1,13104 @@ +/*! + * FullCalendar v2.9.0 + * Docs & License: http://fullcalendar.io/ + * (c) 2016 Adam Shaw + */ + +(function(factory) { + if (typeof define === 'function' && define.amd) { + define([ 'jquery', 'moment' ], factory); + } + else if (typeof exports === 'object') { // Node/CommonJS + module.exports = factory(require('jquery'), require('moment')); + } + else { + factory(jQuery, moment); + } +})(function($, moment) { + +;; + +var FC = $.fullCalendar = { + version: "2.9.0", + internalApiVersion: 4 +}; +var fcViews = FC.views = {}; + + +$.fn.fullCalendar = function(options) { + var args = Array.prototype.slice.call(arguments, 1); // for a possible method call + var res = this; // what this function will return (this jQuery object by default) + + this.each(function(i, _element) { // loop each DOM element involved + var element = $(_element); + var calendar = element.data('fullCalendar'); // get the existing calendar object (if any) + var singleRes; // the returned value of this single method call + + // a method call + if (typeof options === 'string') { + if (calendar && $.isFunction(calendar[options])) { + singleRes = calendar[options].apply(calendar, args); + if (!i) { + res = singleRes; // record the first method call result + } + if (options === 'destroy') { // for the destroy method, must remove Calendar object data + element.removeData('fullCalendar'); + } + } + } + // a new calendar initialization + else if (!calendar) { // don't initialize twice + calendar = new Calendar(element, options); + element.data('fullCalendar', calendar); + calendar.render(); + } + }); + + return res; +}; + + +var complexOptions = [ // names of options that are objects whose properties should be combined + 'header', + 'buttonText', + 'buttonIcons', + 'themeButtonIcons' +]; + + +// Merges an array of option objects into a single object +function mergeOptions(optionObjs) { + return mergeProps(optionObjs, complexOptions); +} + + +// Given options specified for the calendar's constructor, massages any legacy options into a non-legacy form. +// Converts View-Option-Hashes into the View-Specific-Options format. +function massageOverrides(input) { + var overrides = { views: input.views || {} }; // the output. ensure a `views` hash + var subObj; + + // iterate through all option override properties (except `views`) + $.each(input, function(name, val) { + if (name != 'views') { + + // could the value be a legacy View-Option-Hash? + if ( + $.isPlainObject(val) && + !/(time|duration|interval)$/i.test(name) && // exclude duration options. might be given as objects + $.inArray(name, complexOptions) == -1 // complex options aren't allowed to be View-Option-Hashes + ) { + subObj = null; + + // iterate through the properties of this possible View-Option-Hash value + $.each(val, function(subName, subVal) { + + // is the property targeting a view? + if (/^(month|week|day|default|basic(Week|Day)?|agenda(Week|Day)?)$/.test(subName)) { + if (!overrides.views[subName]) { // ensure the view-target entry exists + overrides.views[subName] = {}; + } + overrides.views[subName][name] = subVal; // record the value in the `views` object + } + else { // a non-View-Option-Hash property + if (!subObj) { + subObj = {}; + } + subObj[subName] = subVal; // accumulate these unrelated values for later + } + }); + + if (subObj) { // non-View-Option-Hash properties? transfer them as-is + overrides[name] = subObj; + } + } + else { + overrides[name] = val; // transfer normal options as-is + } + } + }); + + return overrides; +} + +;; + +// exports +FC.intersectRanges = intersectRanges; +FC.applyAll = applyAll; +FC.debounce = debounce; +FC.isInt = isInt; +FC.htmlEscape = htmlEscape; +FC.cssToStr = cssToStr; +FC.proxy = proxy; +FC.capitaliseFirstLetter = capitaliseFirstLetter; + + +/* FullCalendar-specific DOM Utilities +----------------------------------------------------------------------------------------------------------------------*/ + + +// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left +// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that. +function compensateScroll(rowEls, scrollbarWidths) { + if (scrollbarWidths.left) { + rowEls.css({ + 'border-left-width': 1, + 'margin-left': scrollbarWidths.left - 1 + }); + } + if (scrollbarWidths.right) { + rowEls.css({ + 'border-right-width': 1, + 'margin-right': scrollbarWidths.right - 1 + }); + } +} + + +// Undoes compensateScroll and restores all borders/margins +function uncompensateScroll(rowEls) { + rowEls.css({ + 'margin-left': '', + 'margin-right': '', + 'border-left-width': '', + 'border-right-width': '' + }); +} + + +// Make the mouse cursor express that an event is not allowed in the current area +function disableCursor() { + $('body').addClass('fc-not-allowed'); +} + + +// Returns the mouse cursor to its original look +function enableCursor() { + $('body').removeClass('fc-not-allowed'); +} + + +// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate. +// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering +// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and +// reduces the available height. +function distributeHeight(els, availableHeight, shouldRedistribute) { + + // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions, + // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars. + + var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element + var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE* + var flexEls = []; // elements that are allowed to expand. array of DOM nodes + var flexOffsets = []; // amount of vertical space it takes up + var flexHeights = []; // actual css height + var usedHeight = 0; + + undistributeHeight(els); // give all elements their natural height + + // find elements that are below the recommended height (expandable). + // important to query for heights in a single first pass (to avoid reflow oscillation). + els.each(function(i, el) { + var minOffset = i === els.length - 1 ? minOffset2 : minOffset1; + var naturalOffset = $(el).outerHeight(true); + + if (naturalOffset < minOffset) { + flexEls.push(el); + flexOffsets.push(naturalOffset); + flexHeights.push($(el).height()); + } + else { + // this element stretches past recommended height (non-expandable). mark the space as occupied. + usedHeight += naturalOffset; + } + }); + + // readjust the recommended height to only consider the height available to non-maxed-out rows. + if (shouldRedistribute) { + availableHeight -= usedHeight; + minOffset1 = Math.floor(availableHeight / flexEls.length); + minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE* + } + + // assign heights to all expandable elements + $(flexEls).each(function(i, el) { + var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1; + var naturalOffset = flexOffsets[i]; + var naturalHeight = flexHeights[i]; + var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding + + if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things + $(el).height(newHeight); + } + }); +} + + +// Undoes distrubuteHeight, restoring all els to their natural height +function undistributeHeight(els) { + els.height(''); +} + + +// Given `els`, a jQuery set of cells, find the cell with the largest natural width and set the widths of all the +// cells to be that width. +// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline +function matchCellWidths(els) { + var maxInnerWidth = 0; + + els.find('> span').each(function(i, innerEl) { + var innerWidth = $(innerEl).outerWidth(); + if (innerWidth > maxInnerWidth) { + maxInnerWidth = innerWidth; + } + }); + + maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance + + els.width(maxInnerWidth); + + return maxInnerWidth; +} + + +// Given one element that resides inside another, +// Subtracts the height of the inner element from the outer element. +function subtractInnerElHeight(outerEl, innerEl) { + var both = outerEl.add(innerEl); + var diff; + + // effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked + both.css({ + position: 'relative', // cause a reflow, which will force fresh dimension recalculation + left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll + }); + diff = outerEl.outerHeight() - innerEl.outerHeight(); // grab the dimensions + both.css({ position: '', left: '' }); // undo hack + + return diff; +} + + +/* Element Geom Utilities +----------------------------------------------------------------------------------------------------------------------*/ + +FC.getOuterRect = getOuterRect; +FC.getClientRect = getClientRect; +FC.getContentRect = getContentRect; +FC.getScrollbarWidths = getScrollbarWidths; + + +// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51 +function getScrollParent(el) { + var position = el.css('position'), + scrollParent = el.parents().filter(function() { + var parent = $(this); + return (/(auto|scroll)/).test( + parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x') + ); + }).eq(0); + + return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent; +} + + +// Queries the outer bounding area of a jQuery element. +// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). +// Origin is optional. +function getOuterRect(el, origin) { + var offset = el.offset(); + var left = offset.left - (origin ? origin.left : 0); + var top = offset.top - (origin ? origin.top : 0); + + return { + left: left, + right: left + el.outerWidth(), + top: top, + bottom: top + el.outerHeight() + }; +} + + +// Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding. +// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). +// Origin is optional. +// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser. +function getClientRect(el, origin) { + var offset = el.offset(); + var scrollbarWidths = getScrollbarWidths(el); + var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left - (origin ? origin.left : 0); + var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top - (origin ? origin.top : 0); + + return { + left: left, + right: left + el[0].clientWidth, // clientWidth includes padding but NOT scrollbars + top: top, + bottom: top + el[0].clientHeight // clientHeight includes padding but NOT scrollbars + }; +} + + +// Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars. +// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). +// Origin is optional. +function getContentRect(el, origin) { + var offset = el.offset(); // just outside of border, margin not included + var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left') - + (origin ? origin.left : 0); + var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top') - + (origin ? origin.top : 0); + + return { + left: left, + right: left + el.width(), + top: top, + bottom: top + el.height() + }; +} + + +// Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element. +// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser. +function getScrollbarWidths(el) { + var leftRightWidth = el.innerWidth() - el[0].clientWidth; // the paddings cancel out, leaving the scrollbars + var widths = { + left: 0, + right: 0, + top: 0, + bottom: el.innerHeight() - el[0].clientHeight // the paddings cancel out, leaving the bottom scrollbar + }; + + if (getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side? + widths.left = leftRightWidth; + } + else { + widths.right = leftRightWidth; + } + + return widths; +} + + +// Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side + +var _isLeftRtlScrollbars = null; + +function getIsLeftRtlScrollbars() { // responsible for caching the computation + if (_isLeftRtlScrollbars === null) { + _isLeftRtlScrollbars = computeIsLeftRtlScrollbars(); + } + return _isLeftRtlScrollbars; +} + +function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it + var el = $('
    ') + .css({ + position: 'absolute', + top: -1000, + left: 0, + border: 0, + padding: 0, + overflow: 'scroll', + direction: 'rtl' + }) + .appendTo('body'); + var innerEl = el.children(); + var res = innerEl.offset().left > el.offset().left; // is the inner div shifted to accommodate a left scrollbar? + el.remove(); + return res; +} + + +// Retrieves a jQuery element's computed CSS value as a floating-point number. +// If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero. +function getCssFloat(el, prop) { + return parseFloat(el.css(prop)) || 0; +} + + +/* Mouse / Touch Utilities +----------------------------------------------------------------------------------------------------------------------*/ + +FC.preventDefault = preventDefault; + + +// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) +function isPrimaryMouseButton(ev) { + return ev.which == 1 && !ev.ctrlKey; +} + + +function getEvX(ev) { + if (ev.pageX !== undefined) { + return ev.pageX; + } + var touches = ev.originalEvent.touches; + if (touches) { + return touches[0].pageX; + } +} + + +function getEvY(ev) { + if (ev.pageY !== undefined) { + return ev.pageY; + } + var touches = ev.originalEvent.touches; + if (touches) { + return touches[0].pageY; + } +} + + +function getEvIsTouch(ev) { + return /^touch/.test(ev.type); +} + + +function preventSelection(el) { + el.addClass('fc-unselectable') + .on('selectstart', preventDefault); +} + + +// Stops a mouse/touch event from doing it's native browser action +function preventDefault(ev) { + ev.preventDefault(); +} + + +// attach a handler to get called when ANY scroll action happens on the page. +// this was impossible to do with normal on/off because 'scroll' doesn't bubble. +// http://stackoverflow.com/a/32954565/96342 +// returns `true` on success. +function bindAnyScroll(handler) { + if (window.addEventListener) { + window.addEventListener('scroll', handler, true); // useCapture=true + return true; + } + return false; +} + + +// undoes bindAnyScroll. must pass in the original function. +// returns `true` on success. +function unbindAnyScroll(handler) { + if (window.removeEventListener) { + window.removeEventListener('scroll', handler, true); // useCapture=true + return true; + } + return false; +} + + +/* General Geometry Utils +----------------------------------------------------------------------------------------------------------------------*/ + +FC.intersectRects = intersectRects; + +// Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false +function intersectRects(rect1, rect2) { + var res = { + left: Math.max(rect1.left, rect2.left), + right: Math.min(rect1.right, rect2.right), + top: Math.max(rect1.top, rect2.top), + bottom: Math.min(rect1.bottom, rect2.bottom) + }; + + if (res.left < res.right && res.top < res.bottom) { + return res; + } + return false; +} + + +// Returns a new point that will have been moved to reside within the given rectangle +function constrainPoint(point, rect) { + return { + left: Math.min(Math.max(point.left, rect.left), rect.right), + top: Math.min(Math.max(point.top, rect.top), rect.bottom) + }; +} + + +// Returns a point that is the center of the given rectangle +function getRectCenter(rect) { + return { + left: (rect.left + rect.right) / 2, + top: (rect.top + rect.bottom) / 2 + }; +} + + +// Subtracts point2's coordinates from point1's coordinates, returning a delta +function diffPoints(point1, point2) { + return { + left: point1.left - point2.left, + top: point1.top - point2.top + }; +} + + +/* Object Ordering by Field +----------------------------------------------------------------------------------------------------------------------*/ + +FC.parseFieldSpecs = parseFieldSpecs; +FC.compareByFieldSpecs = compareByFieldSpecs; +FC.compareByFieldSpec = compareByFieldSpec; +FC.flexibleCompare = flexibleCompare; + + +function parseFieldSpecs(input) { + var specs = []; + var tokens = []; + var i, token; + + if (typeof input === 'string') { + tokens = input.split(/\s*,\s*/); + } + else if (typeof input === 'function') { + tokens = [ input ]; + } + else if ($.isArray(input)) { + tokens = input; + } + + for (i = 0; i < tokens.length; i++) { + token = tokens[i]; + + if (typeof token === 'string') { + specs.push( + token.charAt(0) == '-' ? + { field: token.substring(1), order: -1 } : + { field: token, order: 1 } + ); + } + else if (typeof token === 'function') { + specs.push({ func: token }); + } + } + + return specs; +} + + +function compareByFieldSpecs(obj1, obj2, fieldSpecs) { + var i; + var cmp; + + for (i = 0; i < fieldSpecs.length; i++) { + cmp = compareByFieldSpec(obj1, obj2, fieldSpecs[i]); + if (cmp) { + return cmp; + } + } + + return 0; +} + + +function compareByFieldSpec(obj1, obj2, fieldSpec) { + if (fieldSpec.func) { + return fieldSpec.func(obj1, obj2); + } + return flexibleCompare(obj1[fieldSpec.field], obj2[fieldSpec.field]) * + (fieldSpec.order || 1); +} + + +function flexibleCompare(a, b) { + if (!a && !b) { + return 0; + } + if (b == null) { + return -1; + } + if (a == null) { + return 1; + } + if ($.type(a) === 'string' || $.type(b) === 'string') { + return String(a).localeCompare(String(b)); + } + return a - b; +} + + +/* FullCalendar-specific Misc Utilities +----------------------------------------------------------------------------------------------------------------------*/ + + +// Computes the intersection of the two ranges. Returns undefined if no intersection. +// Expects all dates to be normalized to the same timezone beforehand. +// TODO: move to date section? +function intersectRanges(subjectRange, constraintRange) { + var subjectStart = subjectRange.start; + var subjectEnd = subjectRange.end; + var constraintStart = constraintRange.start; + var constraintEnd = constraintRange.end; + var segStart, segEnd; + var isStart, isEnd; + + if (subjectEnd > constraintStart && subjectStart < constraintEnd) { // in bounds at all? + + if (subjectStart >= constraintStart) { + segStart = subjectStart.clone(); + isStart = true; + } + else { + segStart = constraintStart.clone(); + isStart = false; + } + + if (subjectEnd <= constraintEnd) { + segEnd = subjectEnd.clone(); + isEnd = true; + } + else { + segEnd = constraintEnd.clone(); + isEnd = false; + } + + return { + start: segStart, + end: segEnd, + isStart: isStart, + isEnd: isEnd + }; + } +} + + +/* Date Utilities +----------------------------------------------------------------------------------------------------------------------*/ + +FC.computeIntervalUnit = computeIntervalUnit; +FC.divideRangeByDuration = divideRangeByDuration; +FC.divideDurationByDuration = divideDurationByDuration; +FC.multiplyDuration = multiplyDuration; +FC.durationHasTime = durationHasTime; + +var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ]; +var intervalUnits = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ]; + + +// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time. +// Moments will have their timezones normalized. +function diffDayTime(a, b) { + return moment.duration({ + days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'), + ms: a.time() - b.time() // time-of-day from day start. disregards timezone + }); +} + + +// Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations. +function diffDay(a, b) { + return moment.duration({ + days: a.clone().stripTime().diff(b.clone().stripTime(), 'days') + }); +} + + +// Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding. +function diffByUnit(a, b, unit) { + return moment.duration( + Math.round(a.diff(b, unit, true)), // returnFloat=true + unit + ); +} + + +// Computes the unit name of the largest whole-unit period of time. +// For example, 48 hours will be "days" whereas 49 hours will be "hours". +// Accepts start/end, a range object, or an original duration object. +function computeIntervalUnit(start, end) { + var i, unit; + var val; + + for (i = 0; i < intervalUnits.length; i++) { + unit = intervalUnits[i]; + val = computeRangeAs(unit, start, end); + + if (val >= 1 && isInt(val)) { + break; + } + } + + return unit; // will be "milliseconds" if nothing else matches +} + + +// Computes the number of units (like "hours") in the given range. +// Range can be a {start,end} object, separate start/end args, or a Duration. +// Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling +// of month-diffing logic (which tends to vary from version to version). +function computeRangeAs(unit, start, end) { + + if (end != null) { // given start, end + return end.diff(start, unit, true); + } + else if (moment.isDuration(start)) { // given duration + return start.as(unit); + } + else { // given { start, end } range object + return start.end.diff(start.start, unit, true); + } +} + + +// Intelligently divides a range (specified by a start/end params) by a duration +function divideRangeByDuration(start, end, dur) { + var months; + + if (durationHasTime(dur)) { + return (end - start) / dur; + } + months = dur.asMonths(); + if (Math.abs(months) >= 1 && isInt(months)) { + return end.diff(start, 'months', true) / months; + } + return end.diff(start, 'days', true) / dur.asDays(); +} + + +// Intelligently divides one duration by another +function divideDurationByDuration(dur1, dur2) { + var months1, months2; + + if (durationHasTime(dur1) || durationHasTime(dur2)) { + return dur1 / dur2; + } + months1 = dur1.asMonths(); + months2 = dur2.asMonths(); + if ( + Math.abs(months1) >= 1 && isInt(months1) && + Math.abs(months2) >= 1 && isInt(months2) + ) { + return months1 / months2; + } + return dur1.asDays() / dur2.asDays(); +} + + +// Intelligently multiplies a duration by a number +function multiplyDuration(dur, n) { + var months; + + if (durationHasTime(dur)) { + return moment.duration(dur * n); + } + months = dur.asMonths(); + if (Math.abs(months) >= 1 && isInt(months)) { + return moment.duration({ months: months * n }); + } + return moment.duration({ days: dur.asDays() * n }); +} + + +// Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms) +function durationHasTime(dur) { + return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds()); +} + + +function isNativeDate(input) { + return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date; +} + + +// Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00" +function isTimeString(str) { + return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str); +} + + +/* Logging and Debug +----------------------------------------------------------------------------------------------------------------------*/ + +FC.log = function() { + var console = window.console; + + if (console && console.log) { + return console.log.apply(console, arguments); + } +}; + +FC.warn = function() { + var console = window.console; + + if (console && console.warn) { + return console.warn.apply(console, arguments); + } + else { + return FC.log.apply(FC, arguments); + } +}; + + +/* General Utilities +----------------------------------------------------------------------------------------------------------------------*/ + +var hasOwnPropMethod = {}.hasOwnProperty; + + +// Merges an array of objects into a single object. +// The second argument allows for an array of property names who's object values will be merged together. +function mergeProps(propObjs, complexProps) { + var dest = {}; + var i, name; + var complexObjs; + var j, val; + var props; + + if (complexProps) { + for (i = 0; i < complexProps.length; i++) { + name = complexProps[i]; + complexObjs = []; + + // collect the trailing object values, stopping when a non-object is discovered + for (j = propObjs.length - 1; j >= 0; j--) { + val = propObjs[j][name]; + + if (typeof val === 'object') { + complexObjs.unshift(val); + } + else if (val !== undefined) { + dest[name] = val; // if there were no objects, this value will be used + break; + } + } + + // if the trailing values were objects, use the merged value + if (complexObjs.length) { + dest[name] = mergeProps(complexObjs); + } + } + } + + // copy values into the destination, going from last to first + for (i = propObjs.length - 1; i >= 0; i--) { + props = propObjs[i]; + + for (name in props) { + if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign + dest[name] = props[name]; + } + } + } + + return dest; +} + + +// Create an object that has the given prototype. Just like Object.create +function createObject(proto) { + var f = function() {}; + f.prototype = proto; + return new f(); +} + + +function copyOwnProps(src, dest) { + for (var name in src) { + if (hasOwnProp(src, name)) { + dest[name] = src[name]; + } + } +} + + +// Copies over certain methods with the same names as Object.prototype methods. Overcomes an IE<=8 bug: +// https://developer.mozilla.org/en-US/docs/ECMAScript_DontEnum_attribute#JScript_DontEnum_Bug +function copyNativeMethods(src, dest) { + var names = [ 'constructor', 'toString', 'valueOf' ]; + var i, name; + + for (i = 0; i < names.length; i++) { + name = names[i]; + + if (src[name] !== Object.prototype[name]) { + dest[name] = src[name]; + } + } +} + + +function hasOwnProp(obj, name) { + return hasOwnPropMethod.call(obj, name); +} + + +// Is the given value a non-object non-function value? +function isAtomic(val) { + return /undefined|null|boolean|number|string/.test($.type(val)); +} + + +function applyAll(functions, thisObj, args) { + if ($.isFunction(functions)) { + functions = [ functions ]; + } + if (functions) { + var i; + var ret; + for (i=0; i/g, '>') + .replace(/'/g, ''') + .replace(/"/g, '"') + .replace(/\n/g, '
    '); +} + + +function stripHtmlEntities(text) { + return text.replace(/&.*?;/g, ''); +} + + +// Given a hash of CSS properties, returns a string of CSS. +// Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values. +function cssToStr(cssProps) { + var statements = []; + + $.each(cssProps, function(name, val) { + if (val != null) { + statements.push(name + ':' + val); + } + }); + + return statements.join(';'); +} + + +function capitaliseFirstLetter(str) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + + +function compareNumbers(a, b) { // for .sort() + return a - b; +} + + +function isInt(n) { + return n % 1 === 0; +} + + +// Returns a method bound to the given object context. +// Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with +// different contexts as identical when binding/unbinding events. +function proxy(obj, methodName) { + var method = obj[methodName]; + + return function() { + return method.apply(obj, arguments); + }; +} + + +// Returns a function, that, as long as it continues to be invoked, will not +// be triggered. The function will be called after it stops being called for +// N milliseconds. If `immediate` is passed, trigger the function on the +// leading edge, instead of the trailing. +// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714 +function debounce(func, wait, immediate) { + var timeout, args, context, timestamp, result; + + var later = function() { + var last = +new Date() - timestamp; + if (last < wait) { + timeout = setTimeout(later, wait - last); + } + else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = +new Date(); + var callNow = immediate && !timeout; + if (!timeout) { + timeout = setTimeout(later, wait); + } + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + return result; + }; +} + + +// HACK around jQuery's now A+ promises: execute callback synchronously if already resolved. +// thenFunc shouldn't accept args. +// similar to whenResources in Scheduler plugin. +function syncThen(promise, thenFunc) { + // not a promise, or an already-resolved promise? + if (!promise || !promise.then || promise.state() === 'resolved') { + return $.when(thenFunc()); // resolve immediately + } + else if (thenFunc) { + return promise.then(thenFunc); + } +} + +;; + +var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/; +var ambigTimeOrZoneRegex = + /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/; +var newMomentProto = moment.fn; // where we will attach our new methods +var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods +var allowValueOptimization; +var setUTCValues; // function defined below +var setLocalValues; // function defined below + + +// Creating +// ------------------------------------------------------------------------------------------------- + +// Creates a new moment, similar to the vanilla moment(...) constructor, but with +// extra features (ambiguous time, enhanced formatting). When given an existing moment, +// it will function as a clone (and retain the zone of the moment). Anything else will +// result in a moment in the local zone. +FC.moment = function() { + return makeMoment(arguments); +}; + +// Sames as FC.moment, but forces the resulting moment to be in the UTC timezone. +FC.moment.utc = function() { + var mom = makeMoment(arguments, true); + + // Force it into UTC because makeMoment doesn't guarantee it + // (if given a pre-existing moment for example) + if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone + mom.utc(); + } + + return mom; +}; + +// Same as FC.moment, but when given an ISO8601 string, the timezone offset is preserved. +// ISO8601 strings with no timezone offset will become ambiguously zoned. +FC.moment.parseZone = function() { + return makeMoment(arguments, true, true); +}; + +// Builds an enhanced moment from args. When given an existing moment, it clones. When given a +// native Date, or called with no arguments (the current time), the resulting moment will be local. +// Anything else needs to be "parsed" (a string or an array), and will be affected by: +// parseAsUTC - if there is no zone information, should we parse the input in UTC? +// parseZone - if there is zone information, should we force the zone of the moment? +function makeMoment(args, parseAsUTC, parseZone) { + var input = args[0]; + var isSingleString = args.length == 1 && typeof input === 'string'; + var isAmbigTime; + var isAmbigZone; + var ambigMatch; + var mom; + + if (moment.isMoment(input)) { + mom = moment.apply(null, args); // clone it + transferAmbigs(input, mom); // the ambig flags weren't transfered with the clone + } + else if (isNativeDate(input) || input === undefined) { + mom = moment.apply(null, args); // will be local + } + else { // "parsing" is required + isAmbigTime = false; + isAmbigZone = false; + + if (isSingleString) { + if (ambigDateOfMonthRegex.test(input)) { + // accept strings like '2014-05', but convert to the first of the month + input += '-01'; + args = [ input ]; // for when we pass it on to moment's constructor + isAmbigTime = true; + isAmbigZone = true; + } + else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) { + isAmbigTime = !ambigMatch[5]; // no time part? + isAmbigZone = true; + } + } + else if ($.isArray(input)) { + // arrays have no timezone information, so assume ambiguous zone + isAmbigZone = true; + } + // otherwise, probably a string with a format + + if (parseAsUTC || isAmbigTime) { + mom = moment.utc.apply(moment, args); + } + else { + mom = moment.apply(null, args); + } + + if (isAmbigTime) { + mom._ambigTime = true; + mom._ambigZone = true; // ambiguous time always means ambiguous zone + } + else if (parseZone) { // let's record the inputted zone somehow + if (isAmbigZone) { + mom._ambigZone = true; + } + else if (isSingleString) { + if (mom.utcOffset) { + mom.utcOffset(input); // if not a valid zone, will assign UTC + } + else { + mom.zone(input); // for moment-pre-2.9 + } + } + } + } + + mom._fullCalendar = true; // flag for extended functionality + + return mom; +} + + +// A clone method that works with the flags related to our enhanced functionality. +// In the future, use moment.momentProperties +newMomentProto.clone = function() { + var mom = oldMomentProto.clone.apply(this, arguments); + + // these flags weren't transfered with the clone + transferAmbigs(this, mom); + if (this._fullCalendar) { + mom._fullCalendar = true; + } + + return mom; +}; + + +// Week Number +// ------------------------------------------------------------------------------------------------- + + +// Returns the week number, considering the locale's custom week number calcuation +// `weeks` is an alias for `week` +newMomentProto.week = newMomentProto.weeks = function(input) { + var weekCalc = (this._locale || this._lang) // works pre-moment-2.8 + ._fullCalendar_weekCalc; + + if (input == null && typeof weekCalc === 'function') { // custom function only works for getter + return weekCalc(this); + } + else if (weekCalc === 'ISO') { + return oldMomentProto.isoWeek.apply(this, arguments); // ISO getter/setter + } + + return oldMomentProto.week.apply(this, arguments); // local getter/setter +}; + + +// Time-of-day +// ------------------------------------------------------------------------------------------------- + +// GETTER +// Returns a Duration with the hours/minutes/seconds/ms values of the moment. +// If the moment has an ambiguous time, a duration of 00:00 will be returned. +// +// SETTER +// You can supply a Duration, a Moment, or a Duration-like argument. +// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous. +newMomentProto.time = function(time) { + + // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar. + // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins. + if (!this._fullCalendar) { + return oldMomentProto.time.apply(this, arguments); + } + + if (time == null) { // getter + return moment.duration({ + hours: this.hours(), + minutes: this.minutes(), + seconds: this.seconds(), + milliseconds: this.milliseconds() + }); + } + else { // setter + + this._ambigTime = false; // mark that the moment now has a time + + if (!moment.isDuration(time) && !moment.isMoment(time)) { + time = moment.duration(time); + } + + // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day). + // Only for Duration times, not Moment times. + var dayHours = 0; + if (moment.isDuration(time)) { + dayHours = Math.floor(time.asDays()) * 24; + } + + // We need to set the individual fields. + // Can't use startOf('day') then add duration. In case of DST at start of day. + return this.hours(dayHours + time.hours()) + .minutes(time.minutes()) + .seconds(time.seconds()) + .milliseconds(time.milliseconds()); + } +}; + +// Converts the moment to UTC, stripping out its time-of-day and timezone offset, +// but preserving its YMD. A moment with a stripped time will display no time +// nor timezone offset when .format() is called. +newMomentProto.stripTime = function() { + var a; + + if (!this._ambigTime) { + + // get the values before any conversion happens + a = this.toArray(); // array of y/m/d/h/m/s/ms + + // TODO: use keepLocalTime in the future + this.utc(); // set the internal UTC flag (will clear the ambig flags) + setUTCValues(this, a.slice(0, 3)); // set the year/month/date. time will be zero + + // Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(), + // which clears all ambig flags. Same with setUTCValues with moment-timezone. + this._ambigTime = true; + this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset + } + + return this; // for chaining +}; + +// Returns if the moment has a non-ambiguous time (boolean) +newMomentProto.hasTime = function() { + return !this._ambigTime; +}; + + +// Timezone +// ------------------------------------------------------------------------------------------------- + +// Converts the moment to UTC, stripping out its timezone offset, but preserving its +// YMD and time-of-day. A moment with a stripped timezone offset will display no +// timezone offset when .format() is called. +// TODO: look into Moment's keepLocalTime functionality +newMomentProto.stripZone = function() { + var a, wasAmbigTime; + + if (!this._ambigZone) { + + // get the values before any conversion happens + a = this.toArray(); // array of y/m/d/h/m/s/ms + wasAmbigTime = this._ambigTime; + + this.utc(); // set the internal UTC flag (might clear the ambig flags, depending on Moment internals) + setUTCValues(this, a); // will set the year/month/date/hours/minutes/seconds/ms + + // the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore + this._ambigTime = wasAmbigTime || false; + + // Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(), + // which clears the ambig flags. Same with setUTCValues with moment-timezone. + this._ambigZone = true; + } + + return this; // for chaining +}; + +// Returns of the moment has a non-ambiguous timezone offset (boolean) +newMomentProto.hasZone = function() { + return !this._ambigZone; +}; + + +// this method implicitly marks a zone +newMomentProto.local = function() { + var a = this.toArray(); // year,month,date,hours,minutes,seconds,ms as an array + var wasAmbigZone = this._ambigZone; + + oldMomentProto.local.apply(this, arguments); + + // ensure non-ambiguous + // this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals + this._ambigTime = false; + this._ambigZone = false; + + if (wasAmbigZone) { + // If the moment was ambiguously zoned, the date fields were stored as UTC. + // We want to preserve these, but in local time. + // TODO: look into Moment's keepLocalTime functionality + setLocalValues(this, a); + } + + return this; // for chaining +}; + + +// implicitly marks a zone +newMomentProto.utc = function() { + oldMomentProto.utc.apply(this, arguments); + + // ensure non-ambiguous + // this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals + this._ambigTime = false; + this._ambigZone = false; + + return this; +}; + + +// methods for arbitrarily manipulating timezone offset. +// should clear time/zone ambiguity when called. +$.each([ + 'zone', // only in moment-pre-2.9. deprecated afterwards + 'utcOffset' +], function(i, name) { + if (oldMomentProto[name]) { // original method exists? + + // this method implicitly marks a zone (will probably get called upon .utc() and .local()) + newMomentProto[name] = function(tzo) { + + if (tzo != null) { // setter + // these assignments needs to happen before the original zone method is called. + // I forget why, something to do with a browser crash. + this._ambigTime = false; + this._ambigZone = false; + } + + return oldMomentProto[name].apply(this, arguments); + }; + } +}); + + +// Formatting +// ------------------------------------------------------------------------------------------------- + +newMomentProto.format = function() { + if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided? + return formatDate(this, arguments[0]); // our extended formatting + } + if (this._ambigTime) { + return oldMomentFormat(this, 'YYYY-MM-DD'); + } + if (this._ambigZone) { + return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); + } + return oldMomentProto.format.apply(this, arguments); +}; + +newMomentProto.toISOString = function() { + if (this._ambigTime) { + return oldMomentFormat(this, 'YYYY-MM-DD'); + } + if (this._ambigZone) { + return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); + } + return oldMomentProto.toISOString.apply(this, arguments); +}; + + +// Querying +// ------------------------------------------------------------------------------------------------- + +// Is the moment within the specified range? `end` is exclusive. +// FYI, this method is not a standard Moment method, so always do our enhanced logic. +newMomentProto.isWithin = function(start, end) { + var a = commonlyAmbiguate([ this, start, end ]); + return a[0] >= a[1] && a[0] < a[2]; +}; + +// When isSame is called with units, timezone ambiguity is normalized before the comparison happens. +// If no units specified, the two moments must be identically the same, with matching ambig flags. +newMomentProto.isSame = function(input, units) { + var a; + + // only do custom logic if this is an enhanced moment + if (!this._fullCalendar) { + return oldMomentProto.isSame.apply(this, arguments); + } + + if (units) { + a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times + return oldMomentProto.isSame.call(a[0], a[1], units); + } + else { + input = FC.moment.parseZone(input); // normalize input + return oldMomentProto.isSame.call(this, input) && + Boolean(this._ambigTime) === Boolean(input._ambigTime) && + Boolean(this._ambigZone) === Boolean(input._ambigZone); + } +}; + +// Make these query methods work with ambiguous moments +$.each([ + 'isBefore', + 'isAfter' +], function(i, methodName) { + newMomentProto[methodName] = function(input, units) { + var a; + + // only do custom logic if this is an enhanced moment + if (!this._fullCalendar) { + return oldMomentProto[methodName].apply(this, arguments); + } + + a = commonlyAmbiguate([ this, input ]); + return oldMomentProto[methodName].call(a[0], a[1], units); + }; +}); + + +// Misc Internals +// ------------------------------------------------------------------------------------------------- + +// given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated. +// for example, of one moment has ambig time, but not others, all moments will have their time stripped. +// set `preserveTime` to `true` to keep times, but only normalize zone ambiguity. +// returns the original moments if no modifications are necessary. +function commonlyAmbiguate(inputs, preserveTime) { + var anyAmbigTime = false; + var anyAmbigZone = false; + var len = inputs.length; + var moms = []; + var i, mom; + + // parse inputs into real moments and query their ambig flags + for (i = 0; i < len; i++) { + mom = inputs[i]; + if (!moment.isMoment(mom)) { + mom = FC.moment.parseZone(mom); + } + anyAmbigTime = anyAmbigTime || mom._ambigTime; + anyAmbigZone = anyAmbigZone || mom._ambigZone; + moms.push(mom); + } + + // strip each moment down to lowest common ambiguity + // use clones to avoid modifying the original moments + for (i = 0; i < len; i++) { + mom = moms[i]; + if (!preserveTime && anyAmbigTime && !mom._ambigTime) { + moms[i] = mom.clone().stripTime(); + } + else if (anyAmbigZone && !mom._ambigZone) { + moms[i] = mom.clone().stripZone(); + } + } + + return moms; +} + +// Transfers all the flags related to ambiguous time/zone from the `src` moment to the `dest` moment +// TODO: look into moment.momentProperties for this. +function transferAmbigs(src, dest) { + if (src._ambigTime) { + dest._ambigTime = true; + } + else if (dest._ambigTime) { + dest._ambigTime = false; + } + + if (src._ambigZone) { + dest._ambigZone = true; + } + else if (dest._ambigZone) { + dest._ambigZone = false; + } +} + + +// Sets the year/month/date/etc values of the moment from the given array. +// Inefficient because it calls each individual setter. +function setMomentValues(mom, a) { + mom.year(a[0] || 0) + .month(a[1] || 0) + .date(a[2] || 0) + .hours(a[3] || 0) + .minutes(a[4] || 0) + .seconds(a[5] || 0) + .milliseconds(a[6] || 0); +} + +// Can we set the moment's internal date directly? +allowValueOptimization = '_d' in moment() && 'updateOffset' in moment; + +// Utility function. Accepts a moment and an array of the UTC year/month/date/etc values to set. +// Assumes the given moment is already in UTC mode. +setUTCValues = allowValueOptimization ? function(mom, a) { + // simlate what moment's accessors do + mom._d.setTime(Date.UTC.apply(Date, a)); + moment.updateOffset(mom, false); // keepTime=false +} : setMomentValues; + +// Utility function. Accepts a moment and an array of the local year/month/date/etc values to set. +// Assumes the given moment is already in local mode. +setLocalValues = allowValueOptimization ? function(mom, a) { + // simlate what moment's accessors do + mom._d.setTime(+new Date( // FYI, there is now way to apply an array of args to a constructor + a[0] || 0, + a[1] || 0, + a[2] || 0, + a[3] || 0, + a[4] || 0, + a[5] || 0, + a[6] || 0 + )); + moment.updateOffset(mom, false); // keepTime=false +} : setMomentValues; + +;; + +// Single Date Formatting +// ------------------------------------------------------------------------------------------------- + + +// call this if you want Moment's original format method to be used +function oldMomentFormat(mom, formatStr) { + return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js +} + + +// Formats `date` with a Moment formatting string, but allow our non-zero areas and +// additional token. +function formatDate(date, formatStr) { + return formatDateWithChunks(date, getFormatStringChunks(formatStr)); +} + + +function formatDateWithChunks(date, chunks) { + var s = ''; + var i; + + for (i=0; i "MMMM D YYYY" + formatStr = localeData.longDateFormat(formatStr) || formatStr; + // BTW, this is not important for `formatDate` because it is impossible to put custom tokens + // or non-zero areas in Moment's localized format strings. + + separator = separator || ' - '; + + return formatRangeWithChunks( + date1, + date2, + getFormatStringChunks(formatStr), + separator, + isRTL + ); +} +FC.formatRange = formatRange; // expose + + +function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) { + var unzonedDate1 = date1.clone().stripZone(); // for formatSimilarChunk + var unzonedDate2 = date2.clone().stripZone(); // " + var chunkStr; // the rendering of the chunk + var leftI; + var leftStr = ''; + var rightI; + var rightStr = ''; + var middleI; + var middleStr1 = ''; + var middleStr2 = ''; + var middleStr = ''; + + // Start at the leftmost side of the formatting string and continue until you hit a token + // that is not the same between dates. + for (leftI=0; leftIleftI; rightI--) { + chunkStr = formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunks[rightI]); + if (chunkStr === false) { + break; + } + rightStr = chunkStr + rightStr; + } + + // The area in the middle is different for both of the dates. + // Collect them distinctly so we can jam them together later. + for (middleI=leftI; middleI<=rightI; middleI++) { + middleStr1 += formatDateWithChunk(date1, chunks[middleI]); + middleStr2 += formatDateWithChunk(date2, chunks[middleI]); + } + + if (middleStr1 || middleStr2) { + if (isRTL) { + middleStr = middleStr2 + separator + middleStr1; + } + else { + middleStr = middleStr1 + separator + middleStr2; + } + } + + return leftStr + middleStr + rightStr; +} + + +var similarUnitMap = { + Y: 'year', + M: 'month', + D: 'day', // day of month + d: 'day', // day of week + // prevents a separator between anything time-related... + A: 'second', // AM/PM + a: 'second', // am/pm + T: 'second', // A/P + t: 'second', // a/p + H: 'second', // hour (24) + h: 'second', // hour (12) + m: 'second', // minute + s: 'second' // second +}; +// TODO: week maybe? + + +// Given a formatting chunk, and given that both dates are similar in the regard the +// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`. +function formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunk) { + var token; + var unit; + + if (typeof chunk === 'string') { // a literal string + return chunk; + } + else if ((token = chunk.token)) { + unit = similarUnitMap[token.charAt(0)]; + + // are the dates the same for this unit of measurement? + // use the unzoned dates for this calculation because unreliable when near DST (bug #2396) + if (unit && unzonedDate1.isSame(unzonedDate2, unit)) { + return oldMomentFormat(date1, token); // would be the same if we used `date2` + // BTW, don't support custom tokens + } + } + + return false; // the chunk is NOT the same for the two dates + // BTW, don't support splitting on non-zero areas +} + + +// Chunking Utils +// ------------------------------------------------------------------------------------------------- + + +var formatStringChunkCache = {}; + + +function getFormatStringChunks(formatStr) { + if (formatStr in formatStringChunkCache) { + return formatStringChunkCache[formatStr]; + } + return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr)); +} + + +// Break the formatting string into an array of chunks +function chunkFormatString(formatStr) { + var chunks = []; + var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination + var match; + + while ((match = chunker.exec(formatStr))) { + if (match[1]) { // a literal string inside [ ... ] + chunks.push(match[1]); + } + else if (match[2]) { // non-zero formatting inside ( ... ) + chunks.push({ maybe: chunkFormatString(match[2]) }); + } + else if (match[3]) { // a formatting token + chunks.push({ token: match[3] }); + } + else if (match[5]) { // an unenclosed literal string + chunks.push(match[5]); + } + } + + return chunks; +} + +;; + +FC.Class = Class; // export + +// Class that all other classes will inherit from +function Class() { } + + +// Called on a class to create a subclass. +// Last argument contains instance methods. Any argument before the last are considered mixins. +Class.extend = function() { + var len = arguments.length; + var i; + var members; + + for (i = 0; i < len; i++) { + members = arguments[i]; + if (i < len - 1) { // not the last argument? + mixIntoClass(this, members); + } + } + + return extendClass(this, members || {}); // members will be undefined if no arguments +}; + + +// Adds new member variables/methods to the class's prototype. +// Can be called with another class, or a plain object hash containing new members. +Class.mixin = function(members) { + mixIntoClass(this, members); +}; + + +function extendClass(superClass, members) { + var subClass; + + // ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist + if (hasOwnProp(members, 'constructor')) { + subClass = members.constructor; + } + if (typeof subClass !== 'function') { + subClass = members.constructor = function() { + superClass.apply(this, arguments); + }; + } + + // build the base prototype for the subclass, which is an new object chained to the superclass's prototype + subClass.prototype = createObject(superClass.prototype); + + // copy each member variable/method onto the the subclass's prototype + copyOwnProps(members, subClass.prototype); + copyNativeMethods(members, subClass.prototype); // hack for IE8 + + // copy over all class variables/methods to the subclass, such as `extend` and `mixin` + copyOwnProps(superClass, subClass); + + return subClass; +} + + +function mixIntoClass(theClass, members) { + copyOwnProps(members, theClass.prototype); // TODO: copyNativeMethods? +} +;; + +var EmitterMixin = FC.EmitterMixin = { + + // jQuery-ification via $(this) allows a non-DOM object to have + // the same event handling capabilities (including namespaces). + + + on: function(types, handler) { + + // handlers are always called with an "event" object as their first param. + // sneak the `this` context and arguments into the extra parameter object + // and forward them on to the original handler. + var intercept = function(ev, extra) { + return handler.apply( + extra.context || this, + extra.args || [] + ); + }; + + // mimick jQuery's internal "proxy" system (risky, I know) + // causing all functions with the same .guid to appear to be the same. + // https://github.com/jquery/jquery/blob/2.2.4/src/core.js#L448 + // this is needed for calling .off with the original non-intercept handler. + if (!handler.guid) { + handler.guid = $.guid++; + } + intercept.guid = handler.guid; + + $(this).on(types, intercept); + + return this; // for chaining + }, + + + off: function(types, handler) { + $(this).off(types, handler); + + return this; // for chaining + }, + + + trigger: function(types) { + var args = Array.prototype.slice.call(arguments, 1); // arguments after the first + + // pass in "extra" info to the intercept + $(this).triggerHandler(types, { args: args }); + + return this; // for chaining + }, + + + triggerWith: function(types, context, args) { + + // `triggerHandler` is less reliant on the DOM compared to `trigger`. + // pass in "extra" info to the intercept. + $(this).triggerHandler(types, { context: context, args: args }); + + return this; // for chaining + } + +}; + +;; + +/* +Utility methods for easily listening to events on another object, +and more importantly, easily unlistening from them. +*/ +var ListenerMixin = FC.ListenerMixin = (function() { + var guid = 0; + var ListenerMixin = { + + listenerId: null, + + /* + Given an `other` object that has on/off methods, bind the given `callback` to an event by the given name. + The `callback` will be called with the `this` context of the object that .listenTo is being called on. + Can be called: + .listenTo(other, eventName, callback) + OR + .listenTo(other, { + eventName1: callback1, + eventName2: callback2 + }) + */ + listenTo: function(other, arg, callback) { + if (typeof arg === 'object') { // given dictionary of callbacks + for (var eventName in arg) { + if (arg.hasOwnProperty(eventName)) { + this.listenTo(other, eventName, arg[eventName]); + } + } + } + else if (typeof arg === 'string') { + other.on( + arg + '.' + this.getListenerNamespace(), // use event namespacing to identify this object + $.proxy(callback, this) // always use `this` context + // the usually-undesired jQuery guid behavior doesn't matter, + // because we always unbind via namespace + ); + } + }, + + /* + Causes the current object to stop listening to events on the `other` object. + `eventName` is optional. If omitted, will stop listening to ALL events on `other`. + */ + stopListeningTo: function(other, eventName) { + other.off((eventName || '') + '.' + this.getListenerNamespace()); + }, + + /* + Returns a string, unique to this object, to be used for event namespacing + */ + getListenerNamespace: function() { + if (this.listenerId == null) { + this.listenerId = guid++; + } + return '_listener' + this.listenerId; + } + + }; + return ListenerMixin; +})(); +;; + +// simple class for toggle a `isIgnoringMouse` flag on delay +// initMouseIgnoring must first be called, with a millisecond delay setting. +var MouseIgnorerMixin = { + + isIgnoringMouse: false, // bool + delayUnignoreMouse: null, // method + + + initMouseIgnoring: function(delay) { + this.delayUnignoreMouse = debounce(proxy(this, 'unignoreMouse'), delay || 1000); + }, + + + // temporarily ignore mouse actions on segments + tempIgnoreMouse: function() { + this.isIgnoringMouse = true; + this.delayUnignoreMouse(); + }, + + + // delayUnignoreMouse eventually calls this + unignoreMouse: function() { + this.isIgnoringMouse = false; + } + +}; + +;; + +/* A rectangular panel that is absolutely positioned over other content +------------------------------------------------------------------------------------------------------------------------ +Options: + - className (string) + - content (HTML string or jQuery element set) + - parentEl + - top + - left + - right (the x coord of where the right edge should be. not a "CSS" right) + - autoHide (boolean) + - show (callback) + - hide (callback) +*/ + +var Popover = Class.extend(ListenerMixin, { + + isHidden: true, + options: null, + el: null, // the container element for the popover. generated by this object + margin: 10, // the space required between the popover and the edges of the scroll container + + + constructor: function(options) { + this.options = options || {}; + }, + + + // Shows the popover on the specified position. Renders it if not already + show: function() { + if (this.isHidden) { + if (!this.el) { + this.render(); + } + this.el.show(); + this.position(); + this.isHidden = false; + this.trigger('show'); + } + }, + + + // Hides the popover, through CSS, but does not remove it from the DOM + hide: function() { + if (!this.isHidden) { + this.el.hide(); + this.isHidden = true; + this.trigger('hide'); + } + }, + + + // Creates `this.el` and renders content inside of it + render: function() { + var _this = this; + var options = this.options; + + this.el = $('
    ') + .addClass(options.className || '') + .css({ + // position initially to the top left to avoid creating scrollbars + top: 0, + left: 0 + }) + .append(options.content) + .appendTo(options.parentEl); + + // when a click happens on anything inside with a 'fc-close' className, hide the popover + this.el.on('click', '.fc-close', function() { + _this.hide(); + }); + + if (options.autoHide) { + this.listenTo($(document), 'mousedown', this.documentMousedown); + } + }, + + + // Triggered when the user clicks *anywhere* in the document, for the autoHide feature + documentMousedown: function(ev) { + // only hide the popover if the click happened outside the popover + if (this.el && !$(ev.target).closest(this.el).length) { + this.hide(); + } + }, + + + // Hides and unregisters any handlers + removeElement: function() { + this.hide(); + + if (this.el) { + this.el.remove(); + this.el = null; + } + + this.stopListeningTo($(document), 'mousedown'); + }, + + + // Positions the popover optimally, using the top/left/right options + position: function() { + var options = this.options; + var origin = this.el.offsetParent().offset(); + var width = this.el.outerWidth(); + var height = this.el.outerHeight(); + var windowEl = $(window); + var viewportEl = getScrollParent(this.el); + var viewportTop; + var viewportLeft; + var viewportOffset; + var top; // the "position" (not "offset") values for the popover + var left; // + + // compute top and left + top = options.top || 0; + if (options.left !== undefined) { + left = options.left; + } + else if (options.right !== undefined) { + left = options.right - width; // derive the left value from the right value + } + else { + left = 0; + } + + if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result + viewportEl = windowEl; + viewportTop = 0; // the window is always at the top left + viewportLeft = 0; // (and .offset() won't work if called here) + } + else { + viewportOffset = viewportEl.offset(); + viewportTop = viewportOffset.top; + viewportLeft = viewportOffset.left; + } + + // if the window is scrolled, it causes the visible area to be further down + viewportTop += windowEl.scrollTop(); + viewportLeft += windowEl.scrollLeft(); + + // constrain to the view port. if constrained by two edges, give precedence to top/left + if (options.viewportConstrain !== false) { + top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin); + top = Math.max(top, viewportTop + this.margin); + left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin); + left = Math.max(left, viewportLeft + this.margin); + } + + this.el.css({ + top: top - origin.top, + left: left - origin.left + }); + }, + + + // Triggers a callback. Calls a function in the option hash of the same name. + // Arguments beyond the first `name` are forwarded on. + // TODO: better code reuse for this. Repeat code + trigger: function(name) { + if (this.options[name]) { + this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); + } + } + +}); + +;; + +/* +A cache for the left/right/top/bottom/width/height values for one or more elements. +Works with both offset (from topleft document) and position (from offsetParent). + +options: +- els +- isHorizontal +- isVertical +*/ +var CoordCache = FC.CoordCache = Class.extend({ + + els: null, // jQuery set (assumed to be siblings) + forcedOffsetParentEl: null, // options can override the natural offsetParent + origin: null, // {left,top} position of offsetParent of els + boundingRect: null, // constrain cordinates to this rectangle. {left,right,top,bottom} or null + isHorizontal: false, // whether to query for left/right/width + isVertical: false, // whether to query for top/bottom/height + + // arrays of coordinates (offsets from topleft of document) + lefts: null, + rights: null, + tops: null, + bottoms: null, + + + constructor: function(options) { + this.els = $(options.els); + this.isHorizontal = options.isHorizontal; + this.isVertical = options.isVertical; + this.forcedOffsetParentEl = options.offsetParent ? $(options.offsetParent) : null; + }, + + + // Queries the els for coordinates and stores them. + // Call this method before using and of the get* methods below. + build: function() { + var offsetParentEl = this.forcedOffsetParentEl || this.els.eq(0).offsetParent(); + + this.origin = offsetParentEl.offset(); + this.boundingRect = this.queryBoundingRect(); + + if (this.isHorizontal) { + this.buildElHorizontals(); + } + if (this.isVertical) { + this.buildElVerticals(); + } + }, + + + // Destroys all internal data about coordinates, freeing memory + clear: function() { + this.origin = null; + this.boundingRect = null; + this.lefts = null; + this.rights = null; + this.tops = null; + this.bottoms = null; + }, + + + // When called, if coord caches aren't built, builds them + ensureBuilt: function() { + if (!this.origin) { + this.build(); + } + }, + + + // Compute and return what the elements' bounding rectangle is, from the user's perspective. + // Right now, only returns a rectangle if constrained by an overflow:scroll element. + queryBoundingRect: function() { + var scrollParentEl = getScrollParent(this.els.eq(0)); + + if (!scrollParentEl.is(document)) { + return getClientRect(scrollParentEl); + } + }, + + + // Populates the left/right internal coordinate arrays + buildElHorizontals: function() { + var lefts = []; + var rights = []; + + this.els.each(function(i, node) { + var el = $(node); + var left = el.offset().left; + var width = el.outerWidth(); + + lefts.push(left); + rights.push(left + width); + }); + + this.lefts = lefts; + this.rights = rights; + }, + + + // Populates the top/bottom internal coordinate arrays + buildElVerticals: function() { + var tops = []; + var bottoms = []; + + this.els.each(function(i, node) { + var el = $(node); + var top = el.offset().top; + var height = el.outerHeight(); + + tops.push(top); + bottoms.push(top + height); + }); + + this.tops = tops; + this.bottoms = bottoms; + }, + + + // Given a left offset (from document left), returns the index of the el that it horizontally intersects. + // If no intersection is made, or outside of the boundingRect, returns undefined. + getHorizontalIndex: function(leftOffset) { + this.ensureBuilt(); + + var boundingRect = this.boundingRect; + var lefts = this.lefts; + var rights = this.rights; + var len = lefts.length; + var i; + + if (!boundingRect || (leftOffset >= boundingRect.left && leftOffset < boundingRect.right)) { + for (i = 0; i < len; i++) { + if (leftOffset >= lefts[i] && leftOffset < rights[i]) { + return i; + } + } + } + }, + + + // Given a top offset (from document top), returns the index of the el that it vertically intersects. + // If no intersection is made, or outside of the boundingRect, returns undefined. + getVerticalIndex: function(topOffset) { + this.ensureBuilt(); + + var boundingRect = this.boundingRect; + var tops = this.tops; + var bottoms = this.bottoms; + var len = tops.length; + var i; + + if (!boundingRect || (topOffset >= boundingRect.top && topOffset < boundingRect.bottom)) { + for (i = 0; i < len; i++) { + if (topOffset >= tops[i] && topOffset < bottoms[i]) { + return i; + } + } + } + }, + + + // Gets the left offset (from document left) of the element at the given index + getLeftOffset: function(leftIndex) { + this.ensureBuilt(); + return this.lefts[leftIndex]; + }, + + + // Gets the left position (from offsetParent left) of the element at the given index + getLeftPosition: function(leftIndex) { + this.ensureBuilt(); + return this.lefts[leftIndex] - this.origin.left; + }, + + + // Gets the right offset (from document left) of the element at the given index. + // This value is NOT relative to the document's right edge, like the CSS concept of "right" would be. + getRightOffset: function(leftIndex) { + this.ensureBuilt(); + return this.rights[leftIndex]; + }, + + + // Gets the right position (from offsetParent left) of the element at the given index. + // This value is NOT relative to the offsetParent's right edge, like the CSS concept of "right" would be. + getRightPosition: function(leftIndex) { + this.ensureBuilt(); + return this.rights[leftIndex] - this.origin.left; + }, + + + // Gets the width of the element at the given index + getWidth: function(leftIndex) { + this.ensureBuilt(); + return this.rights[leftIndex] - this.lefts[leftIndex]; + }, + + + // Gets the top offset (from document top) of the element at the given index + getTopOffset: function(topIndex) { + this.ensureBuilt(); + return this.tops[topIndex]; + }, + + + // Gets the top position (from offsetParent top) of the element at the given position + getTopPosition: function(topIndex) { + this.ensureBuilt(); + return this.tops[topIndex] - this.origin.top; + }, + + // Gets the bottom offset (from the document top) of the element at the given index. + // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be. + getBottomOffset: function(topIndex) { + this.ensureBuilt(); + return this.bottoms[topIndex]; + }, + + + // Gets the bottom position (from the offsetParent top) of the element at the given index. + // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be. + getBottomPosition: function(topIndex) { + this.ensureBuilt(); + return this.bottoms[topIndex] - this.origin.top; + }, + + + // Gets the height of the element at the given index + getHeight: function(topIndex) { + this.ensureBuilt(); + return this.bottoms[topIndex] - this.tops[topIndex]; + } + +}); + +;; + +/* Tracks a drag's mouse movement, firing various handlers +----------------------------------------------------------------------------------------------------------------------*/ +// TODO: use Emitter + +var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMixin, { + + options: null, + + // for IE8 bug-fighting behavior + subjectEl: null, + subjectHref: null, + + // coordinates of the initial mousedown + originX: null, + originY: null, + + // the wrapping element that scrolls, or MIGHT scroll if there's overflow. + // TODO: do this for wrappers that have overflow:hidden as well. + scrollEl: null, + + isInteracting: false, + isDistanceSurpassed: false, + isDelayEnded: false, + isDragging: false, + isTouch: false, + + delay: null, + delayTimeoutId: null, + minDistance: null, + + handleTouchScrollProxy: null, // calls handleTouchScroll, always bound to `this` + + + constructor: function(options) { + this.options = options || {}; + this.handleTouchScrollProxy = proxy(this, 'handleTouchScroll'); + this.initMouseIgnoring(500); + }, + + + // Interaction (high-level) + // ----------------------------------------------------------------------------------------------------------------- + + + startInteraction: function(ev, extraOptions) { + var isTouch = getEvIsTouch(ev); + + if (ev.type === 'mousedown') { + if (this.isIgnoringMouse) { + return; + } + else if (!isPrimaryMouseButton(ev)) { + return; + } + else { + ev.preventDefault(); // prevents native selection in most browsers + } + } + + if (!this.isInteracting) { + + // process options + extraOptions = extraOptions || {}; + this.delay = firstDefined(extraOptions.delay, this.options.delay, 0); + this.minDistance = firstDefined(extraOptions.distance, this.options.distance, 0); + this.subjectEl = this.options.subjectEl; + + this.isInteracting = true; + this.isTouch = isTouch; + this.isDelayEnded = false; + this.isDistanceSurpassed = false; + + this.originX = getEvX(ev); + this.originY = getEvY(ev); + this.scrollEl = getScrollParent($(ev.target)); + + this.bindHandlers(); + this.initAutoScroll(); + this.handleInteractionStart(ev); + this.startDelay(ev); + + if (!this.minDistance) { + this.handleDistanceSurpassed(ev); + } + } + }, + + + handleInteractionStart: function(ev) { + this.trigger('interactionStart', ev); + }, + + + endInteraction: function(ev, isCancelled) { + if (this.isInteracting) { + this.endDrag(ev); + + if (this.delayTimeoutId) { + clearTimeout(this.delayTimeoutId); + this.delayTimeoutId = null; + } + + this.destroyAutoScroll(); + this.unbindHandlers(); + + this.isInteracting = false; + this.handleInteractionEnd(ev, isCancelled); + + // a touchstart+touchend on the same element will result in the following addition simulated events: + // mouseover + mouseout + click + // let's ignore these bogus events + if (this.isTouch) { + this.tempIgnoreMouse(); + } + } + }, + + + handleInteractionEnd: function(ev, isCancelled) { + this.trigger('interactionEnd', ev, isCancelled || false); + }, + + + // Binding To DOM + // ----------------------------------------------------------------------------------------------------------------- + + + bindHandlers: function() { + var _this = this; + var touchStartIgnores = 1; + + if (this.isTouch) { + this.listenTo($(document), { + touchmove: this.handleTouchMove, + touchend: this.endInteraction, + touchcancel: this.endInteraction, + + // Sometimes touchend doesn't fire + // (can't figure out why. touchcancel doesn't fire either. has to do with scrolling?) + // If another touchstart happens, we know it's bogus, so cancel the drag. + // touchend will continue to be broken until user does a shorttap/scroll, but this is best we can do. + touchstart: function(ev) { + if (touchStartIgnores) { // bindHandlers is called from within a touchstart, + touchStartIgnores--; // and we don't want this to fire immediately, so ignore. + } + else { + _this.endInteraction(ev, true); // isCancelled=true + } + } + }); + + // listen to ALL scroll actions on the page + if ( + !bindAnyScroll(this.handleTouchScrollProxy) && // hopefully this works and short-circuits the rest + this.scrollEl // otherwise, attach a single handler to this + ) { + this.listenTo(this.scrollEl, 'scroll', this.handleTouchScroll); + } + } + else { + this.listenTo($(document), { + mousemove: this.handleMouseMove, + mouseup: this.endInteraction + }); + } + + this.listenTo($(document), { + selectstart: preventDefault, // don't allow selection while dragging + contextmenu: preventDefault // long taps would open menu on Chrome dev tools + }); + }, + + + unbindHandlers: function() { + this.stopListeningTo($(document)); + + // unbind scroll listening + unbindAnyScroll(this.handleTouchScrollProxy); + if (this.scrollEl) { + this.stopListeningTo(this.scrollEl, 'scroll'); + } + }, + + + // Drag (high-level) + // ----------------------------------------------------------------------------------------------------------------- + + + // extraOptions ignored if drag already started + startDrag: function(ev, extraOptions) { + this.startInteraction(ev, extraOptions); // ensure interaction began + + if (!this.isDragging) { + this.isDragging = true; + this.handleDragStart(ev); + } + }, + + + handleDragStart: function(ev) { + this.trigger('dragStart', ev); + this.initHrefHack(); + }, + + + handleMove: function(ev) { + var dx = getEvX(ev) - this.originX; + var dy = getEvY(ev) - this.originY; + var minDistance = this.minDistance; + var distanceSq; // current distance from the origin, squared + + if (!this.isDistanceSurpassed) { + distanceSq = dx * dx + dy * dy; + if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem + this.handleDistanceSurpassed(ev); + } + } + + if (this.isDragging) { + this.handleDrag(dx, dy, ev); + } + }, + + + // Called while the mouse is being moved and when we know a legitimate drag is taking place + handleDrag: function(dx, dy, ev) { + this.trigger('drag', dx, dy, ev); + this.updateAutoScroll(ev); // will possibly cause scrolling + }, + + + endDrag: function(ev) { + if (this.isDragging) { + this.isDragging = false; + this.handleDragEnd(ev); + } + }, + + + handleDragEnd: function(ev) { + this.trigger('dragEnd', ev); + this.destroyHrefHack(); + }, + + + // Delay + // ----------------------------------------------------------------------------------------------------------------- + + + startDelay: function(initialEv) { + var _this = this; + + if (this.delay) { + this.delayTimeoutId = setTimeout(function() { + _this.handleDelayEnd(initialEv); + }, this.delay); + } + else { + this.handleDelayEnd(initialEv); + } + }, + + + handleDelayEnd: function(initialEv) { + this.isDelayEnded = true; + + if (this.isDistanceSurpassed) { + this.startDrag(initialEv); + } + }, + + + // Distance + // ----------------------------------------------------------------------------------------------------------------- + + + handleDistanceSurpassed: function(ev) { + this.isDistanceSurpassed = true; + + if (this.isDelayEnded) { + this.startDrag(ev); + } + }, + + + // Mouse / Touch + // ----------------------------------------------------------------------------------------------------------------- + + + handleTouchMove: function(ev) { + // prevent inertia and touchmove-scrolling while dragging + if (this.isDragging) { + ev.preventDefault(); + } + + this.handleMove(ev); + }, + + + handleMouseMove: function(ev) { + this.handleMove(ev); + }, + + + // Scrolling (unrelated to auto-scroll) + // ----------------------------------------------------------------------------------------------------------------- + + + handleTouchScroll: function(ev) { + // if the drag is being initiated by touch, but a scroll happens before + // the drag-initiating delay is over, cancel the drag + if (!this.isDragging) { + this.endInteraction(ev, true); // isCancelled=true + } + }, + + + // HREF Hack + // ----------------------------------------------------------------------------------------------------------------- + + + initHrefHack: function() { + var subjectEl = this.subjectEl; + + // remove a mousedown'd 's href so it is not visited (IE8 bug) + if ((this.subjectHref = subjectEl ? subjectEl.attr('href') : null)) { + subjectEl.removeAttr('href'); + } + }, + + + destroyHrefHack: function() { + var subjectEl = this.subjectEl; + var subjectHref = this.subjectHref; + + // restore a mousedown'd 's href (for IE8 bug) + setTimeout(function() { // must be outside of the click's execution + if (subjectHref) { + subjectEl.attr('href', subjectHref); + } + }, 0); + }, + + + // Utils + // ----------------------------------------------------------------------------------------------------------------- + + + // Triggers a callback. Calls a function in the option hash of the same name. + // Arguments beyond the first `name` are forwarded on. + trigger: function(name) { + if (this.options[name]) { + this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); + } + // makes _methods callable by event name. TODO: kill this + if (this['_' + name]) { + this['_' + name].apply(this, Array.prototype.slice.call(arguments, 1)); + } + } + + +}); + +;; +/* +this.scrollEl is set in DragListener +*/ +DragListener.mixin({ + + isAutoScroll: false, + + scrollBounds: null, // { top, bottom, left, right } + scrollTopVel: null, // pixels per second + scrollLeftVel: null, // pixels per second + scrollIntervalId: null, // ID of setTimeout for scrolling animation loop + + // defaults + scrollSensitivity: 30, // pixels from edge for scrolling to start + scrollSpeed: 200, // pixels per second, at maximum speed + scrollIntervalMs: 50, // millisecond wait between scroll increment + + + initAutoScroll: function() { + var scrollEl = this.scrollEl; + + this.isAutoScroll = + this.options.scroll && + scrollEl && + !scrollEl.is(window) && + !scrollEl.is(document); + + if (this.isAutoScroll) { + // debounce makes sure rapid calls don't happen + this.listenTo(scrollEl, 'scroll', debounce(this.handleDebouncedScroll, 100)); + } + }, + + + destroyAutoScroll: function() { + this.endAutoScroll(); // kill any animation loop + + // remove the scroll handler if there is a scrollEl + if (this.isAutoScroll) { + this.stopListeningTo(this.scrollEl, 'scroll'); // will probably get removed by unbindHandlers too :( + } + }, + + + // Computes and stores the bounding rectangle of scrollEl + computeScrollBounds: function() { + if (this.isAutoScroll) { + this.scrollBounds = getOuterRect(this.scrollEl); + // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars + } + }, + + + // Called when the dragging is in progress and scrolling should be updated + updateAutoScroll: function(ev) { + var sensitivity = this.scrollSensitivity; + var bounds = this.scrollBounds; + var topCloseness, bottomCloseness; + var leftCloseness, rightCloseness; + var topVel = 0; + var leftVel = 0; + + if (bounds) { // only scroll if scrollEl exists + + // compute closeness to edges. valid range is from 0.0 - 1.0 + topCloseness = (sensitivity - (getEvY(ev) - bounds.top)) / sensitivity; + bottomCloseness = (sensitivity - (bounds.bottom - getEvY(ev))) / sensitivity; + leftCloseness = (sensitivity - (getEvX(ev) - bounds.left)) / sensitivity; + rightCloseness = (sensitivity - (bounds.right - getEvX(ev))) / sensitivity; + + // translate vertical closeness into velocity. + // mouse must be completely in bounds for velocity to happen. + if (topCloseness >= 0 && topCloseness <= 1) { + topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up + } + else if (bottomCloseness >= 0 && bottomCloseness <= 1) { + topVel = bottomCloseness * this.scrollSpeed; + } + + // translate horizontal closeness into velocity + if (leftCloseness >= 0 && leftCloseness <= 1) { + leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left + } + else if (rightCloseness >= 0 && rightCloseness <= 1) { + leftVel = rightCloseness * this.scrollSpeed; + } + } + + this.setScrollVel(topVel, leftVel); + }, + + + // Sets the speed-of-scrolling for the scrollEl + setScrollVel: function(topVel, leftVel) { + + this.scrollTopVel = topVel; + this.scrollLeftVel = leftVel; + + this.constrainScrollVel(); // massages into realistic values + + // if there is non-zero velocity, and an animation loop hasn't already started, then START + if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) { + this.scrollIntervalId = setInterval( + proxy(this, 'scrollIntervalFunc'), // scope to `this` + this.scrollIntervalMs + ); + } + }, + + + // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way + constrainScrollVel: function() { + var el = this.scrollEl; + + if (this.scrollTopVel < 0) { // scrolling up? + if (el.scrollTop() <= 0) { // already scrolled all the way up? + this.scrollTopVel = 0; + } + } + else if (this.scrollTopVel > 0) { // scrolling down? + if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down? + this.scrollTopVel = 0; + } + } + + if (this.scrollLeftVel < 0) { // scrolling left? + if (el.scrollLeft() <= 0) { // already scrolled all the left? + this.scrollLeftVel = 0; + } + } + else if (this.scrollLeftVel > 0) { // scrolling right? + if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right? + this.scrollLeftVel = 0; + } + } + }, + + + // This function gets called during every iteration of the scrolling animation loop + scrollIntervalFunc: function() { + var el = this.scrollEl; + var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by + + // change the value of scrollEl's scroll + if (this.scrollTopVel) { + el.scrollTop(el.scrollTop() + this.scrollTopVel * frac); + } + if (this.scrollLeftVel) { + el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac); + } + + this.constrainScrollVel(); // since the scroll values changed, recompute the velocities + + // if scrolled all the way, which causes the vels to be zero, stop the animation loop + if (!this.scrollTopVel && !this.scrollLeftVel) { + this.endAutoScroll(); + } + }, + + + // Kills any existing scrolling animation loop + endAutoScroll: function() { + if (this.scrollIntervalId) { + clearInterval(this.scrollIntervalId); + this.scrollIntervalId = null; + + this.handleScrollEnd(); + } + }, + + + // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce) + handleDebouncedScroll: function() { + // recompute all coordinates, but *only* if this is *not* part of our scrolling animation + if (!this.scrollIntervalId) { + this.handleScrollEnd(); + } + }, + + + // Called when scrolling has stopped, whether through auto scroll, or the user scrolling + handleScrollEnd: function() { + } + +}); +;; + +/* Tracks mouse movements over a component and raises events about which hit the mouse is over. +------------------------------------------------------------------------------------------------------------------------ +options: +- subjectEl +- subjectCenter +*/ + +var HitDragListener = DragListener.extend({ + + component: null, // converts coordinates to hits + // methods: prepareHits, releaseHits, queryHit + + origHit: null, // the hit the mouse was over when listening started + hit: null, // the hit the mouse is over + coordAdjust: null, // delta that will be added to the mouse coordinates when computing collisions + + + constructor: function(component, options) { + DragListener.call(this, options); // call the super-constructor + + this.component = component; + }, + + + // Called when drag listening starts (but a real drag has not necessarily began). + // ev might be undefined if dragging was started manually. + handleInteractionStart: function(ev) { + var subjectEl = this.subjectEl; + var subjectRect; + var origPoint; + var point; + + this.computeCoords(); + + if (ev) { + origPoint = { left: getEvX(ev), top: getEvY(ev) }; + point = origPoint; + + // constrain the point to bounds of the element being dragged + if (subjectEl) { + subjectRect = getOuterRect(subjectEl); // used for centering as well + point = constrainPoint(point, subjectRect); + } + + this.origHit = this.queryHit(point.left, point.top); + + // treat the center of the subject as the collision point? + if (subjectEl && this.options.subjectCenter) { + + // only consider the area the subject overlaps the hit. best for large subjects. + // TODO: skip this if hit didn't supply left/right/top/bottom + if (this.origHit) { + subjectRect = intersectRects(this.origHit, subjectRect) || + subjectRect; // in case there is no intersection + } + + point = getRectCenter(subjectRect); + } + + this.coordAdjust = diffPoints(point, origPoint); // point - origPoint + } + else { + this.origHit = null; + this.coordAdjust = null; + } + + // call the super-method. do it after origHit has been computed + DragListener.prototype.handleInteractionStart.apply(this, arguments); + }, + + + // Recomputes the drag-critical positions of elements + computeCoords: function() { + this.component.prepareHits(); + this.computeScrollBounds(); // why is this here?????? + }, + + + // Called when the actual drag has started + handleDragStart: function(ev) { + var hit; + + DragListener.prototype.handleDragStart.apply(this, arguments); // call the super-method + + // might be different from this.origHit if the min-distance is large + hit = this.queryHit(getEvX(ev), getEvY(ev)); + + // report the initial hit the mouse is over + // especially important if no min-distance and drag starts immediately + if (hit) { + this.handleHitOver(hit); + } + }, + + + // Called when the drag moves + handleDrag: function(dx, dy, ev) { + var hit; + + DragListener.prototype.handleDrag.apply(this, arguments); // call the super-method + + hit = this.queryHit(getEvX(ev), getEvY(ev)); + + if (!isHitsEqual(hit, this.hit)) { // a different hit than before? + if (this.hit) { + this.handleHitOut(); + } + if (hit) { + this.handleHitOver(hit); + } + } + }, + + + // Called when dragging has been stopped + handleDragEnd: function() { + this.handleHitDone(); + DragListener.prototype.handleDragEnd.apply(this, arguments); // call the super-method + }, + + + // Called when a the mouse has just moved over a new hit + handleHitOver: function(hit) { + var isOrig = isHitsEqual(hit, this.origHit); + + this.hit = hit; + + this.trigger('hitOver', this.hit, isOrig, this.origHit); + }, + + + // Called when the mouse has just moved out of a hit + handleHitOut: function() { + if (this.hit) { + this.trigger('hitOut', this.hit); + this.handleHitDone(); + this.hit = null; + } + }, + + + // Called after a hitOut. Also called before a dragStop + handleHitDone: function() { + if (this.hit) { + this.trigger('hitDone', this.hit); + } + }, + + + // Called when the interaction ends, whether there was a real drag or not + handleInteractionEnd: function() { + DragListener.prototype.handleInteractionEnd.apply(this, arguments); // call the super-method + + this.origHit = null; + this.hit = null; + + this.component.releaseHits(); + }, + + + // Called when scrolling has stopped, whether through auto scroll, or the user scrolling + handleScrollEnd: function() { + DragListener.prototype.handleScrollEnd.apply(this, arguments); // call the super-method + + this.computeCoords(); // hits' absolute positions will be in new places. recompute + }, + + + // Gets the hit underneath the coordinates for the given mouse event + queryHit: function(left, top) { + + if (this.coordAdjust) { + left += this.coordAdjust.left; + top += this.coordAdjust.top; + } + + return this.component.queryHit(left, top); + } + +}); + + +// Returns `true` if the hits are identically equal. `false` otherwise. Must be from the same component. +// Two null values will be considered equal, as two "out of the component" states are the same. +function isHitsEqual(hit0, hit1) { + + if (!hit0 && !hit1) { + return true; + } + + if (hit0 && hit1) { + return hit0.component === hit1.component && + isHitPropsWithin(hit0, hit1) && + isHitPropsWithin(hit1, hit0); // ensures all props are identical + } + + return false; +} + + +// Returns true if all of subHit's non-standard properties are within superHit +function isHitPropsWithin(subHit, superHit) { + for (var propName in subHit) { + if (!/^(component|left|right|top|bottom)$/.test(propName)) { + if (subHit[propName] !== superHit[propName]) { + return false; + } + } + } + return true; +} + +;; + +/* Creates a clone of an element and lets it track the mouse as it moves +----------------------------------------------------------------------------------------------------------------------*/ + +var MouseFollower = Class.extend(ListenerMixin, { + + options: null, + + sourceEl: null, // the element that will be cloned and made to look like it is dragging + el: null, // the clone of `sourceEl` that will track the mouse + parentEl: null, // the element that `el` (the clone) will be attached to + + // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl + top0: null, + left0: null, + + // the absolute coordinates of the initiating touch/mouse action + y0: null, + x0: null, + + // the number of pixels the mouse has moved from its initial position + topDelta: null, + leftDelta: null, + + isFollowing: false, + isHidden: false, + isAnimating: false, // doing the revert animation? + + constructor: function(sourceEl, options) { + this.options = options = options || {}; + this.sourceEl = sourceEl; + this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent + }, + + + // Causes the element to start following the mouse + start: function(ev) { + if (!this.isFollowing) { + this.isFollowing = true; + + this.y0 = getEvY(ev); + this.x0 = getEvX(ev); + this.topDelta = 0; + this.leftDelta = 0; + + if (!this.isHidden) { + this.updatePosition(); + } + + if (getEvIsTouch(ev)) { + this.listenTo($(document), 'touchmove', this.handleMove); + } + else { + this.listenTo($(document), 'mousemove', this.handleMove); + } + } + }, + + + // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position. + // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately. + stop: function(shouldRevert, callback) { + var _this = this; + var revertDuration = this.options.revertDuration; + + function complete() { + this.isAnimating = false; + _this.removeElement(); + + this.top0 = this.left0 = null; // reset state for future updatePosition calls + + if (callback) { + callback(); + } + } + + if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time + this.isFollowing = false; + + this.stopListeningTo($(document)); + + if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation? + this.isAnimating = true; + this.el.animate({ + top: this.top0, + left: this.left0 + }, { + duration: revertDuration, + complete: complete + }); + } + else { + complete(); + } + } + }, + + + // Gets the tracking element. Create it if necessary + getEl: function() { + var el = this.el; + + if (!el) { + this.sourceEl.width(); // hack to force IE8 to compute correct bounding box + el = this.el = this.sourceEl.clone() + .addClass(this.options.additionalClass || '') + .css({ + position: 'absolute', + visibility: '', // in case original element was hidden (commonly through hideEvents()) + display: this.isHidden ? 'none' : '', // for when initially hidden + margin: 0, + right: 'auto', // erase and set width instead + bottom: 'auto', // erase and set height instead + width: this.sourceEl.width(), // explicit height in case there was a 'right' value + height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value + opacity: this.options.opacity || '', + zIndex: this.options.zIndex + }); + + // we don't want long taps or any mouse interaction causing selection/menus. + // would use preventSelection(), but that prevents selectstart, causing problems. + el.addClass('fc-unselectable'); + + el.appendTo(this.parentEl); + } + + return el; + }, + + + // Removes the tracking element if it has already been created + removeElement: function() { + if (this.el) { + this.el.remove(); + this.el = null; + } + }, + + + // Update the CSS position of the tracking element + updatePosition: function() { + var sourceOffset; + var origin; + + this.getEl(); // ensure this.el + + // make sure origin info was computed + if (this.top0 === null) { + this.sourceEl.width(); // hack to force IE8 to compute correct bounding box + sourceOffset = this.sourceEl.offset(); + origin = this.el.offsetParent().offset(); + this.top0 = sourceOffset.top - origin.top; + this.left0 = sourceOffset.left - origin.left; + } + + this.el.css({ + top: this.top0 + this.topDelta, + left: this.left0 + this.leftDelta + }); + }, + + + // Gets called when the user moves the mouse + handleMove: function(ev) { + this.topDelta = getEvY(ev) - this.y0; + this.leftDelta = getEvX(ev) - this.x0; + + if (!this.isHidden) { + this.updatePosition(); + } + }, + + + // Temporarily makes the tracking element invisible. Can be called before following starts + hide: function() { + if (!this.isHidden) { + this.isHidden = true; + if (this.el) { + this.el.hide(); + } + } + }, + + + // Show the tracking element after it has been temporarily hidden + show: function() { + if (this.isHidden) { + this.isHidden = false; + this.updatePosition(); + this.getEl().show(); + } + } + +}); + +;; + +/* An abstract class comprised of a "grid" of areas that each represent a specific datetime +----------------------------------------------------------------------------------------------------------------------*/ + +var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, { + + view: null, // a View object + isRTL: null, // shortcut to the view's isRTL option + + start: null, + end: null, + + el: null, // the containing element + elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name. + + // derived from options + eventTimeFormat: null, + displayEventTime: null, + displayEventEnd: null, + + minResizeDuration: null, // TODO: hack. set by subclasses. minumum event resize duration + + // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity + // of the date areas. if not defined, assumes to be day and time granularity. + // TODO: port isTimeScale into same system? + largeUnit: null, + + dayDragListener: null, + segDragListener: null, + segResizeListener: null, + externalDragListener: null, + + + constructor: function(view) { + this.view = view; + this.isRTL = view.opt('isRTL'); + this.elsByFill = {}; + + this.dayDragListener = this.buildDayDragListener(); + this.initMouseIgnoring(); + }, + + + /* Options + ------------------------------------------------------------------------------------------------------------------*/ + + + // Generates the format string used for event time text, if not explicitly defined by 'timeFormat' + computeEventTimeFormat: function() { + return this.view.opt('smallTimeFormat'); + }, + + + // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'. + // Only applies to non-all-day events. + computeDisplayEventTime: function() { + return true; + }, + + + // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd' + computeDisplayEventEnd: function() { + return true; + }, + + + /* Dates + ------------------------------------------------------------------------------------------------------------------*/ + + + // Tells the grid about what period of time to display. + // Any date-related internal data should be generated. + setRange: function(range) { + this.start = range.start.clone(); + this.end = range.end.clone(); + + this.rangeUpdated(); + this.processRangeOptions(); + }, + + + // Called when internal variables that rely on the range should be updated + rangeUpdated: function() { + }, + + + // Updates values that rely on options and also relate to range + processRangeOptions: function() { + var view = this.view; + var displayEventTime; + var displayEventEnd; + + this.eventTimeFormat = + view.opt('eventTimeFormat') || + view.opt('timeFormat') || // deprecated + this.computeEventTimeFormat(); + + displayEventTime = view.opt('displayEventTime'); + if (displayEventTime == null) { + displayEventTime = this.computeDisplayEventTime(); // might be based off of range + } + + displayEventEnd = view.opt('displayEventEnd'); + if (displayEventEnd == null) { + displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range + } + + this.displayEventTime = displayEventTime; + this.displayEventEnd = displayEventEnd; + }, + + + // Converts a span (has unzoned start/end and any other grid-specific location information) + // into an array of segments (pieces of events whose format is decided by the grid). + spanToSegs: function(span) { + // subclasses must implement + }, + + + // Diffs the two dates, returning a duration, based on granularity of the grid + // TODO: port isTimeScale into this system? + diffDates: function(a, b) { + if (this.largeUnit) { + return diffByUnit(a, b, this.largeUnit); + } + else { + return diffDayTime(a, b); + } + }, + + + /* Hit Area + ------------------------------------------------------------------------------------------------------------------*/ + + + // Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit + prepareHits: function() { + }, + + + // Called when queryHit calls have subsided. Good place to clear any coordinate caches. + releaseHits: function() { + }, + + + // Given coordinates from the topleft of the document, return data about the date-related area underneath. + // Can return an object with arbitrary properties (although top/right/left/bottom are encouraged). + // Must have a `grid` property, a reference to this current grid. TODO: avoid this + // The returned object will be processed by getHitSpan and getHitEl. + queryHit: function(leftOffset, topOffset) { + }, + + + // Given position-level information about a date-related area within the grid, + // should return an object with at least a start/end date. Can provide other information as well. + getHitSpan: function(hit) { + }, + + + // Given position-level information about a date-related area within the grid, + // should return a jQuery element that best represents it. passed to dayClick callback. + getHitEl: function(hit) { + }, + + + /* Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + // Sets the container element that the grid should render inside of. + // Does other DOM-related initializations. + setElement: function(el) { + this.el = el; + preventSelection(el); + + this.bindDayHandler('touchstart', this.dayTouchStart); + this.bindDayHandler('mousedown', this.dayMousedown); + + // attach event-element-related handlers. in Grid.events + // same garbage collection note as above. + this.bindSegHandlers(); + + this.bindGlobalHandlers(); + }, + + + bindDayHandler: function(name, handler) { + var _this = this; + + // attach a handler to the grid's root element. + // jQuery will take care of unregistering them when removeElement gets called. + this.el.on(name, function(ev) { + if ( + !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link + !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one) + ) { + return handler.call(_this, ev); + } + }); + }, + + + // Removes the grid's container element from the DOM. Undoes any other DOM-related attachments. + // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View + removeElement: function() { + this.unbindGlobalHandlers(); + this.clearDragListeners(); + + this.el.remove(); + + // NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement + }, + + + // Renders the basic structure of grid view before any content is rendered + renderSkeleton: function() { + // subclasses should implement + }, + + + // Renders the grid's date-related content (like areas that represent days/times). + // Assumes setRange has already been called and the skeleton has already been rendered. + renderDates: function() { + // subclasses should implement + }, + + + // Unrenders the grid's date-related content + unrenderDates: function() { + // subclasses should implement + }, + + + /* Handlers + ------------------------------------------------------------------------------------------------------------------*/ + + + // Binds DOM handlers to elements that reside outside the grid, such as the document + bindGlobalHandlers: function() { + this.listenTo($(document), { + dragstart: this.externalDragStart, // jqui + sortstart: this.externalDragStart // jqui + }); + }, + + + // Unbinds DOM handlers from elements that reside outside the grid + unbindGlobalHandlers: function() { + this.stopListeningTo($(document)); + }, + + + // Process a mousedown on an element that represents a day. For day clicking and selecting. + dayMousedown: function(ev) { + if (!this.isIgnoringMouse) { + this.dayDragListener.startInteraction(ev, { + //distance: 5, // needs more work if we want dayClick to fire correctly + }); + } + }, + + + dayTouchStart: function(ev) { + var view = this.view; + + // HACK to prevent a user's clickaway for unselecting a range or an event + // from causing a dayClick. + if (view.isSelected || view.selectedEvent) { + this.tempIgnoreMouse(); + } + + this.dayDragListener.startInteraction(ev, { + delay: this.view.opt('longPressDelay') + }); + }, + + + // Creates a listener that tracks the user's drag across day elements. + // For day clicking and selecting. + buildDayDragListener: function() { + var _this = this; + var view = this.view; + var isSelectable = view.opt('selectable'); + var dayClickHit; // null if invalid dayClick + var selectionSpan; // null if invalid selection + + // this listener tracks a mousedown on a day element, and a subsequent drag. + // if the drag ends on the same day, it is a 'dayClick'. + // if 'selectable' is enabled, this listener also detects selections. + var dragListener = new HitDragListener(this, { + scroll: view.opt('dragScroll'), + interactionStart: function() { + dayClickHit = dragListener.origHit; // for dayClick, where no dragging happens + }, + dragStart: function() { + view.unselect(); // since we could be rendering a new selection, we want to clear any old one + }, + hitOver: function(hit, isOrig, origHit) { + if (origHit) { // click needs to have started on a hit + + // if user dragged to another cell at any point, it can no longer be a dayClick + if (!isOrig) { + dayClickHit = null; + } + + if (isSelectable) { + selectionSpan = _this.computeSelection( + _this.getHitSpan(origHit), + _this.getHitSpan(hit) + ); + if (selectionSpan) { + _this.renderSelection(selectionSpan); + } + else if (selectionSpan === false) { + disableCursor(); + } + } + } + }, + hitOut: function() { + dayClickHit = null; + selectionSpan = null; + _this.unrenderSelection(); + enableCursor(); + }, + interactionEnd: function(ev, isCancelled) { + if (!isCancelled) { + if ( + dayClickHit && + !_this.isIgnoringMouse // see hack in dayTouchStart + ) { + view.triggerDayClick( + _this.getHitSpan(dayClickHit), + _this.getHitEl(dayClickHit), + ev + ); + } + if (selectionSpan) { + // the selection will already have been rendered. just report it + view.reportSelection(selectionSpan, ev); + } + enableCursor(); + } + } + }); + + return dragListener; + }, + + + // Kills all in-progress dragging. + // Useful for when public API methods that result in re-rendering are invoked during a drag. + // Also useful for when touch devices misbehave and don't fire their touchend. + clearDragListeners: function() { + this.dayDragListener.endInteraction(); + + if (this.segDragListener) { + this.segDragListener.endInteraction(); // will clear this.segDragListener + } + if (this.segResizeListener) { + this.segResizeListener.endInteraction(); // will clear this.segResizeListener + } + if (this.externalDragListener) { + this.externalDragListener.endInteraction(); // will clear this.externalDragListener + } + }, + + + /* Event Helper + ------------------------------------------------------------------------------------------------------------------*/ + // TODO: should probably move this to Grid.events, like we did event dragging / resizing + + + // Renders a mock event at the given event location, which contains zoned start/end properties. + // Returns all mock event elements. + renderEventLocationHelper: function(eventLocation, sourceSeg) { + var fakeEvent = this.fabricateHelperEvent(eventLocation, sourceSeg); + + return this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering + }, + + + // Builds a fake event given zoned event date properties and a segment is should be inspired from. + // The range's end can be null, in which case the mock event that is rendered will have a null end time. + // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging. + fabricateHelperEvent: function(eventLocation, sourceSeg) { + var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible + + fakeEvent.start = eventLocation.start.clone(); + fakeEvent.end = eventLocation.end ? eventLocation.end.clone() : null; + fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventDates + this.view.calendar.normalizeEventDates(fakeEvent); + + // this extra className will be useful for differentiating real events from mock events in CSS + fakeEvent.className = (fakeEvent.className || []).concat('fc-helper'); + + // if something external is being dragged in, don't render a resizer + if (!sourceSeg) { + fakeEvent.editable = false; + } + + return fakeEvent; + }, + + + // Renders a mock event. Given zoned event date properties. + // Must return all mock event elements. + renderHelper: function(eventLocation, sourceSeg) { + // subclasses must implement + }, + + + // Unrenders a mock event + unrenderHelper: function() { + // subclasses must implement + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses. + // Given a span (unzoned start/end and other misc data) + renderSelection: function(span) { + this.renderHighlight(span); + }, + + + // Unrenders any visual indications of a selection. Will unrender a highlight by default. + unrenderSelection: function() { + this.unrenderHighlight(); + }, + + + // Given the first and last date-spans of a selection, returns another date-span object. + // Subclasses can override and provide additional data in the span object. Will be passed to renderSelection(). + // Will return false if the selection is invalid and this should be indicated to the user. + // Will return null/undefined if a selection invalid but no error should be reported. + computeSelection: function(span0, span1) { + var span = this.computeSelectionSpan(span0, span1); + + if (span && !this.view.calendar.isSelectionSpanAllowed(span)) { + return false; + } + + return span; + }, + + + // Given two spans, must return the combination of the two. + // TODO: do this separation of concerns (combining VS validation) for event dnd/resize too. + computeSelectionSpan: function(span0, span1) { + var dates = [ span0.start, span0.end, span1.start, span1.end ]; + + dates.sort(compareNumbers); // sorts chronologically. works with Moments + + return { start: dates[0].clone(), end: dates[3].clone() }; + }, + + + /* Highlight + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data) + renderHighlight: function(span) { + this.renderFill('highlight', this.spanToSegs(span)); + }, + + + // Unrenders the emphasis on a date range + unrenderHighlight: function() { + this.unrenderFill('highlight'); + }, + + + // Generates an array of classNames for rendering the highlight. Used by the fill system. + highlightSegClasses: function() { + return [ 'fc-highlight' ]; + }, + + + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ + + + renderBusinessHours: function() { + }, + + + unrenderBusinessHours: function() { + }, + + + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ + + + getNowIndicatorUnit: function() { + }, + + + renderNowIndicator: function(date) { + }, + + + unrenderNowIndicator: function() { + }, + + + /* Fill System (highlight, background events, business hours) + -------------------------------------------------------------------------------------------------------------------- + TODO: remove this system. like we did in TimeGrid + */ + + + // Renders a set of rectangles over the given segments of time. + // MUST RETURN a subset of segs, the segs that were actually rendered. + // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement + renderFill: function(type, segs) { + // subclasses must implement + }, + + + // Unrenders a specific type of fill that is currently rendered on the grid + unrenderFill: function(type) { + var el = this.elsByFill[type]; + + if (el) { + el.remove(); + delete this.elsByFill[type]; + } + }, + + + // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types. + // Only returns segments that successfully rendered. + // To be harnessed by renderFill (implemented by subclasses). + // Analagous to renderFgSegEls. + renderFillSegEls: function(type, segs) { + var _this = this; + var segElMethod = this[type + 'SegEl']; + var html = ''; + var renderedSegs = []; + var i; + + if (segs.length) { + + // build a large concatenation of segment HTML + for (i = 0; i < segs.length; i++) { + html += this.fillSegHtml(type, segs[i]); + } + + // Grab individual elements from the combined HTML string. Use each as the default rendering. + // Then, compute the 'el' for each segment. + $(html).each(function(i, node) { + var seg = segs[i]; + var el = $(node); + + // allow custom filter methods per-type + if (segElMethod) { + el = segElMethod.call(_this, seg, el); + } + + if (el) { // custom filters did not cancel the render + el = $(el); // allow custom filter to return raw DOM node + + // correct element type? (would be bad if a non-TD were inserted into a table for example) + if (el.is(_this.fillSegTag)) { + seg.el = el; + renderedSegs.push(seg); + } + } + }); + } + + return renderedSegs; + }, + + + fillSegTag: 'div', // subclasses can override + + + // Builds the HTML needed for one fill segment. Generic enough to work with different types. + fillSegHtml: function(type, seg) { + + // custom hooks per-type + var classesMethod = this[type + 'SegClasses']; + var cssMethod = this[type + 'SegCss']; + + var classes = classesMethod ? classesMethod.call(this, seg) : []; + var css = cssToStr(cssMethod ? cssMethod.call(this, seg) : {}); + + return '<' + this.fillSegTag + + (classes.length ? ' class="' + classes.join(' ') + '"' : '') + + (css ? ' style="' + css + '"' : '') + + ' />'; + }, + + + + /* Generic rendering utilities for subclasses + ------------------------------------------------------------------------------------------------------------------*/ + + + // Computes HTML classNames for a single-day element + getDayClasses: function(date) { + var view = this.view; + var today = view.calendar.getNow(); + var classes = [ 'fc-' + dayIDs[date.day()] ]; + + if ( + view.intervalDuration.as('months') == 1 && + date.month() != view.intervalStart.month() + ) { + classes.push('fc-other-month'); + } + + if (date.isSame(today, 'day')) { + classes.push( + 'fc-today', + view.highlightStateClass + ); + } + else if (date < today) { + classes.push('fc-past'); + } + else { + classes.push('fc-future'); + } + + return classes; + } + +}); + +;; + +/* Event-rendering and event-interaction methods for the abstract Grid class +----------------------------------------------------------------------------------------------------------------------*/ + +Grid.mixin({ + + mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing + isDraggingSeg: false, // is a segment being dragged? boolean + isResizingSeg: false, // is a segment being resized? boolean + isDraggingExternal: false, // jqui-dragging an external element? boolean + segs: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs` + + + // Renders the given events onto the grid + renderEvents: function(events) { + var bgEvents = []; + var fgEvents = []; + var i; + + for (i = 0; i < events.length; i++) { + (isBgEvent(events[i]) ? bgEvents : fgEvents).push(events[i]); + } + + this.segs = [].concat( // record all segs + this.renderBgEvents(bgEvents), + this.renderFgEvents(fgEvents) + ); + }, + + + renderBgEvents: function(events) { + var segs = this.eventsToSegs(events); + + // renderBgSegs might return a subset of segs, segs that were actually rendered + return this.renderBgSegs(segs) || segs; + }, + + + renderFgEvents: function(events) { + var segs = this.eventsToSegs(events); + + // renderFgSegs might return a subset of segs, segs that were actually rendered + return this.renderFgSegs(segs) || segs; + }, + + + // Unrenders all events currently rendered on the grid + unrenderEvents: function() { + this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event + this.clearDragListeners(); + + this.unrenderFgSegs(); + this.unrenderBgSegs(); + + this.segs = null; + }, + + + // Retrieves all rendered segment objects currently rendered on the grid + getEventSegs: function() { + return this.segs || []; + }, + + + /* Foreground Segment Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders foreground event segments onto the grid. May return a subset of segs that were rendered. + renderFgSegs: function(segs) { + // subclasses must implement + }, + + + // Unrenders all currently rendered foreground segments + unrenderFgSegs: function() { + // subclasses must implement + }, + + + // Renders and assigns an `el` property for each foreground event segment. + // Only returns segments that successfully rendered. + // A utility that subclasses may use. + renderFgSegEls: function(segs, disableResizing) { + var view = this.view; + var html = ''; + var renderedSegs = []; + var i; + + if (segs.length) { // don't build an empty html string + + // build a large concatenation of event segment HTML + for (i = 0; i < segs.length; i++) { + html += this.fgSegHtml(segs[i], disableResizing); + } + + // Grab individual elements from the combined HTML string. Use each as the default rendering. + // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false. + $(html).each(function(i, node) { + var seg = segs[i]; + var el = view.resolveEventEl(seg.event, $(node)); + + if (el) { + el.data('fc-seg', seg); // used by handlers + seg.el = el; + renderedSegs.push(seg); + } + }); + } + + return renderedSegs; + }, + + + // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls() + fgSegHtml: function(seg, disableResizing) { + // subclasses should implement + }, + + + /* Background Segment Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders the given background event segments onto the grid. + // Returns a subset of the segs that were actually rendered. + renderBgSegs: function(segs) { + return this.renderFill('bgEvent', segs); + }, + + + // Unrenders all the currently rendered background event segments + unrenderBgSegs: function() { + this.unrenderFill('bgEvent'); + }, + + + // Renders a background event element, given the default rendering. Called by the fill system. + bgEventSegEl: function(seg, el) { + return this.view.resolveEventEl(seg.event, el); // will filter through eventRender + }, + + + // Generates an array of classNames to be used for the default rendering of a background event. + // Called by the fill system. + bgEventSegClasses: function(seg) { + var event = seg.event; + var source = event.source || {}; + + return [ 'fc-bgevent' ].concat( + event.className, + source.className || [] + ); + }, + + + // Generates a semicolon-separated CSS string to be used for the default rendering of a background event. + // Called by the fill system. + bgEventSegCss: function(seg) { + return { + 'background-color': this.getSegSkinCss(seg)['background-color'] + }; + }, + + + // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system. + businessHoursSegClasses: function(seg) { + return [ 'fc-nonbusiness', 'fc-bgevent' ]; + }, + + + /* Handlers + ------------------------------------------------------------------------------------------------------------------*/ + + + // Attaches event-element-related handlers to the container element and leverage bubbling + bindSegHandlers: function() { + this.bindSegHandler('touchstart', this.handleSegTouchStart); + this.bindSegHandler('touchend', this.handleSegTouchEnd); + this.bindSegHandler('mouseenter', this.handleSegMouseover); + this.bindSegHandler('mouseleave', this.handleSegMouseout); + this.bindSegHandler('mousedown', this.handleSegMousedown); + this.bindSegHandler('click', this.handleSegClick); + }, + + + // Executes a handler for any a user-interaction on a segment. + // Handler gets called with (seg, ev), and with the `this` context of the Grid + bindSegHandler: function(name, handler) { + var _this = this; + + this.el.on(name, '.fc-event-container > *', function(ev) { + var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents + + // only call the handlers if there is not a drag/resize in progress + if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) { + return handler.call(_this, seg, ev); // context will be the Grid + } + }); + }, + + + handleSegClick: function(seg, ev) { + return this.view.trigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel + }, + + + // Updates internal state and triggers handlers for when an event element is moused over + handleSegMouseover: function(seg, ev) { + if ( + !this.isIgnoringMouse && + !this.mousedOverSeg + ) { + this.mousedOverSeg = seg; + seg.el.addClass('fc-allow-mouse-resize'); + this.view.trigger('eventMouseover', seg.el[0], seg.event, ev); + } + }, + + + // Updates internal state and triggers handlers for when an event element is moused out. + // Can be given no arguments, in which case it will mouseout the segment that was previously moused over. + handleSegMouseout: function(seg, ev) { + ev = ev || {}; // if given no args, make a mock mouse event + + if (this.mousedOverSeg) { + seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment + this.mousedOverSeg = null; + seg.el.removeClass('fc-allow-mouse-resize'); + this.view.trigger('eventMouseout', seg.el[0], seg.event, ev); + } + }, + + + handleSegMousedown: function(seg, ev) { + var isResizing = this.startSegResize(seg, ev, { distance: 5 }); + + if (!isResizing && this.view.isEventDraggable(seg.event)) { + this.buildSegDragListener(seg) + .startInteraction(ev, { + distance: 5 + }); + } + }, + + + handleSegTouchStart: function(seg, ev) { + var view = this.view; + var event = seg.event; + var isSelected = view.isEventSelected(event); + var isDraggable = view.isEventDraggable(event); + var isResizable = view.isEventResizable(event); + var isResizing = false; + var dragListener; + + if (isSelected && isResizable) { + // only allow resizing of the event is selected + isResizing = this.startSegResize(seg, ev); + } + + if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected? + + dragListener = isDraggable ? + this.buildSegDragListener(seg) : + this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected + + dragListener.startInteraction(ev, { // won't start if already started + delay: isSelected ? 0 : this.view.opt('longPressDelay') // do delay if not already selected + }); + } + + // a long tap simulates a mouseover. ignore this bogus mouseover. + this.tempIgnoreMouse(); + }, + + + handleSegTouchEnd: function(seg, ev) { + // touchstart+touchend = click, which simulates a mouseover. + // ignore this bogus mouseover. + this.tempIgnoreMouse(); + }, + + + // returns boolean whether resizing actually started or not. + // assumes the seg allows resizing. + // `dragOptions` are optional. + startSegResize: function(seg, ev, dragOptions) { + if ($(ev.target).is('.fc-resizer')) { + this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer')) + .startInteraction(ev, dragOptions); + return true; + } + return false; + }, + + + + /* Event Dragging + ------------------------------------------------------------------------------------------------------------------*/ + + + // Builds a listener that will track user-dragging on an event segment. + // Generic enough to work with any type of Grid. + // Has side effect of setting/unsetting `segDragListener` + buildSegDragListener: function(seg) { + var _this = this; + var view = this.view; + var calendar = view.calendar; + var el = seg.el; + var event = seg.event; + var isDragging; + var mouseFollower; // A clone of the original element that will move with the mouse + var dropLocation; // zoned event date properties + + if (this.segDragListener) { + return this.segDragListener; + } + + // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents + // of the view. + var dragListener = this.segDragListener = new HitDragListener(view, { + scroll: view.opt('dragScroll'), + subjectEl: el, + subjectCenter: true, + interactionStart: function(ev) { + isDragging = false; + mouseFollower = new MouseFollower(seg.el, { + additionalClass: 'fc-dragging', + parentEl: view.el, + opacity: dragListener.isTouch ? null : view.opt('dragOpacity'), + revertDuration: view.opt('dragRevertDuration'), + zIndex: 2 // one above the .fc-view + }); + mouseFollower.hide(); // don't show until we know this is a real drag + mouseFollower.start(ev); + }, + dragStart: function(ev) { + if (dragListener.isTouch && !view.isEventSelected(event)) { + // if not previously selected, will fire after a delay. then, select the event + view.selectEvent(event); + } + isDragging = true; + _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported + _this.segDragStart(seg, ev); + view.hideEvent(event); // hide all event segments. our mouseFollower will take over + }, + hitOver: function(hit, isOrig, origHit) { + var dragHelperEls; + + // starting hit could be forced (DayGrid.limit) + if (seg.hit) { + origHit = seg.hit; + } + + // since we are querying the parent view, might not belong to this grid + dropLocation = _this.computeEventDrop( + origHit.component.getHitSpan(origHit), + hit.component.getHitSpan(hit), + event + ); + + if (dropLocation && !calendar.isEventSpanAllowed(_this.eventToSpan(dropLocation), event)) { + disableCursor(); + dropLocation = null; + } + + // if a valid drop location, have the subclass render a visual indication + if (dropLocation && (dragHelperEls = view.renderDrag(dropLocation, seg))) { + + dragHelperEls.addClass('fc-dragging'); + if (!dragListener.isTouch) { + _this.applyDragOpacity(dragHelperEls); + } + + mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own + } + else { + mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping) + } + + if (isOrig) { + dropLocation = null; // needs to have moved hits to be a valid drop + } + }, + hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits + view.unrenderDrag(); // unrender whatever was done in renderDrag + mouseFollower.show(); // show in case we are moving out of all hits + dropLocation = null; + }, + hitDone: function() { // Called after a hitOut OR before a dragEnd + enableCursor(); + }, + interactionEnd: function(ev) { + // do revert animation if hasn't changed. calls a callback when finished (whether animation or not) + mouseFollower.stop(!dropLocation, function() { + if (isDragging) { + view.unrenderDrag(); + view.showEvent(event); + _this.segDragStop(seg, ev); + } + if (dropLocation) { + view.reportEventDrop(event, dropLocation, this.largeUnit, el, ev); + } + }); + _this.segDragListener = null; + } + }); + + return dragListener; + }, + + + // seg isn't draggable, but let's use a generic DragListener + // simply for the delay, so it can be selected. + // Has side effect of setting/unsetting `segDragListener` + buildSegSelectListener: function(seg) { + var _this = this; + var view = this.view; + var event = seg.event; + + if (this.segDragListener) { + return this.segDragListener; + } + + var dragListener = this.segDragListener = new DragListener({ + dragStart: function(ev) { + if (dragListener.isTouch && !view.isEventSelected(event)) { + // if not previously selected, will fire after a delay. then, select the event + view.selectEvent(event); + } + }, + interactionEnd: function(ev) { + _this.segDragListener = null; + } + }); + + return dragListener; + }, + + + // Called before event segment dragging starts + segDragStart: function(seg, ev) { + this.isDraggingSeg = true; + this.view.trigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy + }, + + + // Called after event segment dragging stops + segDragStop: function(seg, ev) { + this.isDraggingSeg = false; + this.view.trigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy + }, + + + // Given the spans an event drag began, and the span event was dropped, calculates the new zoned start/end/allDay + // values for the event. Subclasses may override and set additional properties to be used by renderDrag. + // A falsy returned value indicates an invalid drop. + // DOES NOT consider overlap/constraint. + computeEventDrop: function(startSpan, endSpan, event) { + var calendar = this.view.calendar; + var dragStart = startSpan.start; + var dragEnd = endSpan.start; + var delta; + var dropLocation; // zoned event date properties + + if (dragStart.hasTime() === dragEnd.hasTime()) { + delta = this.diffDates(dragEnd, dragStart); + + // if an all-day event was in a timed area and it was dragged to a different time, + // guarantee an end and adjust start/end to have times + if (event.allDay && durationHasTime(delta)) { + dropLocation = { + start: event.start.clone(), + end: calendar.getEventEnd(event), // will be an ambig day + allDay: false // for normalizeEventTimes + }; + calendar.normalizeEventTimes(dropLocation); + } + // othewise, work off existing values + else { + dropLocation = { + start: event.start.clone(), + end: event.end ? event.end.clone() : null, + allDay: event.allDay // keep it the same + }; + } + + dropLocation.start.add(delta); + if (dropLocation.end) { + dropLocation.end.add(delta); + } + } + else { + // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared + dropLocation = { + start: dragEnd.clone(), + end: null, // end should be cleared + allDay: !dragEnd.hasTime() + }; + } + + return dropLocation; + }, + + + // Utility for apply dragOpacity to a jQuery set + applyDragOpacity: function(els) { + var opacity = this.view.opt('dragOpacity'); + + if (opacity != null) { + els.each(function(i, node) { + // Don't use jQuery (will set an IE filter), do it the old fashioned way. + // In IE8, a helper element will disappears if there's a filter. + node.style.opacity = opacity; + }); + } + }, + + + /* External Element Dragging + ------------------------------------------------------------------------------------------------------------------*/ + + + // Called when a jQuery UI drag is initiated anywhere in the DOM + externalDragStart: function(ev, ui) { + var view = this.view; + var el; + var accept; + + if (view.opt('droppable')) { // only listen if this setting is on + el = $((ui ? ui.item : null) || ev.target); + + // Test that the dragged element passes the dropAccept selector or filter function. + // FYI, the default is "*" (matches all) + accept = view.opt('dropAccept'); + if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) { + if (!this.isDraggingExternal) { // prevent double-listening if fired twice + this.listenToExternalDrag(el, ev, ui); + } + } + } + }, + + + // Called when a jQuery UI drag starts and it needs to be monitored for dropping + listenToExternalDrag: function(el, ev, ui) { + var _this = this; + var calendar = this.view.calendar; + var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create + var dropLocation; // a null value signals an unsuccessful drag + + // listener that tracks mouse movement over date-associated pixel regions + var dragListener = _this.externalDragListener = new HitDragListener(this, { + interactionStart: function() { + _this.isDraggingExternal = true; + }, + hitOver: function(hit) { + dropLocation = _this.computeExternalDrop( + hit.component.getHitSpan(hit), // since we are querying the parent view, might not belong to this grid + meta + ); + + if ( // invalid hit? + dropLocation && + !calendar.isExternalSpanAllowed(_this.eventToSpan(dropLocation), dropLocation, meta.eventProps) + ) { + disableCursor(); + dropLocation = null; + } + + if (dropLocation) { + _this.renderDrag(dropLocation); // called without a seg parameter + } + }, + hitOut: function() { + dropLocation = null; // signal unsuccessful + }, + hitDone: function() { // Called after a hitOut OR before a dragEnd + enableCursor(); + _this.unrenderDrag(); + }, + interactionEnd: function(ev) { + if (dropLocation) { // element was dropped on a valid hit + _this.view.reportExternalDrop(meta, dropLocation, el, ev, ui); + } + _this.isDraggingExternal = false; + _this.externalDragListener = null; + } + }); + + dragListener.startDrag(ev); // start listening immediately + }, + + + // Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object), + // returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null. + // Returning a null value signals an invalid drop hit. + // DOES NOT consider overlap/constraint. + computeExternalDrop: function(span, meta) { + var calendar = this.view.calendar; + var dropLocation = { + start: calendar.applyTimezone(span.start), // simulate a zoned event start date + end: null + }; + + // if dropped on an all-day span, and element's metadata specified a time, set it + if (meta.startTime && !dropLocation.start.hasTime()) { + dropLocation.start.time(meta.startTime); + } + + if (meta.duration) { + dropLocation.end = dropLocation.start.clone().add(meta.duration); + } + + return dropLocation; + }, + + + + /* Drag Rendering (for both events and an external elements) + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event or external element being dragged. + // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null. + // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null. + // A truthy returned value indicates this method has rendered a helper element. + // Must return elements used for any mock events. + renderDrag: function(dropLocation, seg) { + // subclasses must implement + }, + + + // Unrenders a visual indication of an event or external element being dragged + unrenderDrag: function() { + // subclasses must implement + }, + + + /* Resizing + ------------------------------------------------------------------------------------------------------------------*/ + + + // Creates a listener that tracks the user as they resize an event segment. + // Generic enough to work with any type of Grid. + buildSegResizeListener: function(seg, isStart) { + var _this = this; + var view = this.view; + var calendar = view.calendar; + var el = seg.el; + var event = seg.event; + var eventEnd = calendar.getEventEnd(event); + var isDragging; + var resizeLocation; // zoned event date properties. falsy if invalid resize + + // Tracks mouse movement over the *grid's* coordinate map + var dragListener = this.segResizeListener = new HitDragListener(this, { + scroll: view.opt('dragScroll'), + subjectEl: el, + interactionStart: function() { + isDragging = false; + }, + dragStart: function(ev) { + isDragging = true; + _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported + _this.segResizeStart(seg, ev); + }, + hitOver: function(hit, isOrig, origHit) { + var origHitSpan = _this.getHitSpan(origHit); + var hitSpan = _this.getHitSpan(hit); + + resizeLocation = isStart ? + _this.computeEventStartResize(origHitSpan, hitSpan, event) : + _this.computeEventEndResize(origHitSpan, hitSpan, event); + + if (resizeLocation) { + if (!calendar.isEventSpanAllowed(_this.eventToSpan(resizeLocation), event)) { + disableCursor(); + resizeLocation = null; + } + // no change? (TODO: how does this work with timezones?) + else if (resizeLocation.start.isSame(event.start) && resizeLocation.end.isSame(eventEnd)) { + resizeLocation = null; + } + } + + if (resizeLocation) { + view.hideEvent(event); + _this.renderEventResize(resizeLocation, seg); + } + }, + hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits + resizeLocation = null; + }, + hitDone: function() { // resets the rendering to show the original event + _this.unrenderEventResize(); + view.showEvent(event); + enableCursor(); + }, + interactionEnd: function(ev) { + if (isDragging) { + _this.segResizeStop(seg, ev); + } + if (resizeLocation) { // valid date to resize to? + view.reportEventResize(event, resizeLocation, this.largeUnit, el, ev); + } + _this.segResizeListener = null; + } + }); + + return dragListener; + }, + + + // Called before event segment resizing starts + segResizeStart: function(seg, ev) { + this.isResizingSeg = true; + this.view.trigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy + }, + + + // Called after event segment resizing stops + segResizeStop: function(seg, ev) { + this.isResizingSeg = false; + this.view.trigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy + }, + + + // Returns new date-information for an event segment being resized from its start + computeEventStartResize: function(startSpan, endSpan, event) { + return this.computeEventResize('start', startSpan, endSpan, event); + }, + + + // Returns new date-information for an event segment being resized from its end + computeEventEndResize: function(startSpan, endSpan, event) { + return this.computeEventResize('end', startSpan, endSpan, event); + }, + + + // Returns new zoned date information for an event segment being resized from its start OR end + // `type` is either 'start' or 'end'. + // DOES NOT consider overlap/constraint. + computeEventResize: function(type, startSpan, endSpan, event) { + var calendar = this.view.calendar; + var delta = this.diffDates(endSpan[type], startSpan[type]); + var resizeLocation; // zoned event date properties + var defaultDuration; + + // build original values to work from, guaranteeing a start and end + resizeLocation = { + start: event.start.clone(), + end: calendar.getEventEnd(event), + allDay: event.allDay + }; + + // if an all-day event was in a timed area and was resized to a time, adjust start/end to have times + if (resizeLocation.allDay && durationHasTime(delta)) { + resizeLocation.allDay = false; + calendar.normalizeEventTimes(resizeLocation); + } + + resizeLocation[type].add(delta); // apply delta to start or end + + // if the event was compressed too small, find a new reasonable duration for it + if (!resizeLocation.start.isBefore(resizeLocation.end)) { + + defaultDuration = + this.minResizeDuration || // TODO: hack + (event.allDay ? + calendar.defaultAllDayEventDuration : + calendar.defaultTimedEventDuration); + + if (type == 'start') { // resizing the start? + resizeLocation.start = resizeLocation.end.clone().subtract(defaultDuration); + } + else { // resizing the end? + resizeLocation.end = resizeLocation.start.clone().add(defaultDuration); + } + } + + return resizeLocation; + }, + + + // Renders a visual indication of an event being resized. + // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag. + // Must return elements used for any mock events. + renderEventResize: function(range, seg) { + // subclasses must implement + }, + + + // Unrenders a visual indication of an event being resized. + unrenderEventResize: function() { + // subclasses must implement + }, + + + /* Rendering Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Compute the text that should be displayed on an event's element. + // `range` can be the Event object itself, or something range-like, with at least a `start`. + // If event times are disabled, or the event has no time, will return a blank string. + // If not specified, formatStr will default to the eventTimeFormat setting, + // and displayEnd will default to the displayEventEnd setting. + getEventTimeText: function(range, formatStr, displayEnd) { + + if (formatStr == null) { + formatStr = this.eventTimeFormat; + } + + if (displayEnd == null) { + displayEnd = this.displayEventEnd; + } + + if (this.displayEventTime && range.start.hasTime()) { + if (displayEnd && range.end) { + return this.view.formatRange(range, formatStr); + } + else { + return range.start.format(formatStr); + } + } + + return ''; + }, + + + // Generic utility for generating the HTML classNames for an event segment's element + getSegClasses: function(seg, isDraggable, isResizable) { + var view = this.view; + var event = seg.event; + var classes = [ + 'fc-event', + seg.isStart ? 'fc-start' : 'fc-not-start', + seg.isEnd ? 'fc-end' : 'fc-not-end' + ].concat( + event.className, + event.source ? event.source.className : [] + ); + + if (isDraggable) { + classes.push('fc-draggable'); + } + if (isResizable) { + classes.push('fc-resizable'); + } + + // event is currently selected? attach a className. + if (view.isEventSelected(event)) { + classes.push('fc-selected'); + } + + return classes; + }, + + + // Utility for generating event skin-related CSS properties + getSegSkinCss: function(seg) { + var event = seg.event; + var view = this.view; + var source = event.source || {}; + var eventColor = event.color; + var sourceColor = source.color; + var optionColor = view.opt('eventColor'); + + return { + 'background-color': + event.backgroundColor || + eventColor || + source.backgroundColor || + sourceColor || + view.opt('eventBackgroundColor') || + optionColor, + 'border-color': + event.borderColor || + eventColor || + source.borderColor || + sourceColor || + view.opt('eventBorderColor') || + optionColor, + color: + event.textColor || + source.textColor || + view.opt('eventTextColor') + }; + }, + + + /* Converting events -> eventRange -> eventSpan -> eventSegs + ------------------------------------------------------------------------------------------------------------------*/ + + + // Generates an array of segments for the given single event + // Can accept an event "location" as well (which only has start/end and no allDay) + eventToSegs: function(event) { + return this.eventsToSegs([ event ]); + }, + + + eventToSpan: function(event) { + return this.eventToSpans(event)[0]; + }, + + + // Generates spans (always unzoned) for the given event. + // Does not do any inverting for inverse-background events. + // Can accept an event "location" as well (which only has start/end and no allDay) + eventToSpans: function(event) { + var range = this.eventToRange(event); + return this.eventRangeToSpans(range, event); + }, + + + + // Converts an array of event objects into an array of event segment objects. + // A custom `segSliceFunc` may be given for arbitrarily slicing up events. + // Doesn't guarantee an order for the resulting array. + eventsToSegs: function(allEvents, segSliceFunc) { + var _this = this; + var eventsById = groupEventsById(allEvents); + var segs = []; + + $.each(eventsById, function(id, events) { + var ranges = []; + var i; + + for (i = 0; i < events.length; i++) { + ranges.push(_this.eventToRange(events[i])); + } + + // inverse-background events (utilize only the first event in calculations) + if (isInverseBgEvent(events[0])) { + ranges = _this.invertRanges(ranges); + + for (i = 0; i < ranges.length; i++) { + segs.push.apply(segs, // append to + _this.eventRangeToSegs(ranges[i], events[0], segSliceFunc)); + } + } + // normal event ranges + else { + for (i = 0; i < ranges.length; i++) { + segs.push.apply(segs, // append to + _this.eventRangeToSegs(ranges[i], events[i], segSliceFunc)); + } + } + }); + + return segs; + }, + + + // Generates the unzoned start/end dates an event appears to occupy + // Can accept an event "location" as well (which only has start/end and no allDay) + eventToRange: function(event) { + return { + start: event.start.clone().stripZone(), + end: ( + event.end ? + event.end.clone() : + // derive the end from the start and allDay. compute allDay if necessary + this.view.calendar.getDefaultEventEnd( + event.allDay != null ? + event.allDay : + !event.start.hasTime(), + event.start + ) + ).stripZone() + }; + }, + + + // Given an event's range (unzoned start/end), and the event itself, + // slice into segments (using the segSliceFunc function if specified) + eventRangeToSegs: function(range, event, segSliceFunc) { + var spans = this.eventRangeToSpans(range, event); + var segs = []; + var i; + + for (i = 0; i < spans.length; i++) { + segs.push.apply(segs, // append to + this.eventSpanToSegs(spans[i], event, segSliceFunc)); + } + + return segs; + }, + + + // Given an event's unzoned date range, return an array of "span" objects. + // Subclasses can override. + eventRangeToSpans: function(range, event) { + return [ $.extend({}, range) ]; // copy into a single-item array + }, + + + // Given an event's span (unzoned start/end and other misc data), and the event itself, + // slices into segments and attaches event-derived properties to them. + eventSpanToSegs: function(span, event, segSliceFunc) { + var segs = segSliceFunc ? segSliceFunc(span) : this.spanToSegs(span); + var i, seg; + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.event = event; + seg.eventStartMS = +span.start; // TODO: not the best name after making spans unzoned + seg.eventDurationMS = span.end - span.start; + } + + return segs; + }, + + + // Produces a new array of range objects that will cover all the time NOT covered by the given ranges. + // SIDE EFFECT: will mutate the given array and will use its date references. + invertRanges: function(ranges) { + var view = this.view; + var viewStart = view.start.clone(); // need a copy + var viewEnd = view.end.clone(); // need a copy + var inverseRanges = []; + var start = viewStart; // the end of the previous range. the start of the new range + var i, range; + + // ranges need to be in order. required for our date-walking algorithm + ranges.sort(compareRanges); + + for (i = 0; i < ranges.length; i++) { + range = ranges[i]; + + // add the span of time before the event (if there is any) + if (range.start > start) { // compare millisecond time (skip any ambig logic) + inverseRanges.push({ + start: start, + end: range.start + }); + } + + start = range.end; + } + + // add the span of time after the last event (if there is any) + if (start < viewEnd) { // compare millisecond time (skip any ambig logic) + inverseRanges.push({ + start: start, + end: viewEnd + }); + } + + return inverseRanges; + }, + + + sortEventSegs: function(segs) { + segs.sort(proxy(this, 'compareEventSegs')); + }, + + + // A cmp function for determining which segments should take visual priority + compareEventSegs: function(seg1, seg2) { + return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first + seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first + seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1) + compareByFieldSpecs(seg1.event, seg2.event, this.view.eventOrderSpecs); + } + +}); + + +/* Utilities +----------------------------------------------------------------------------------------------------------------------*/ + + +function isBgEvent(event) { // returns true if background OR inverse-background + var rendering = getEventRendering(event); + return rendering === 'background' || rendering === 'inverse-background'; +} +FC.isBgEvent = isBgEvent; // export + + +function isInverseBgEvent(event) { + return getEventRendering(event) === 'inverse-background'; +} + + +function getEventRendering(event) { + return firstDefined((event.source || {}).rendering, event.rendering); +} + + +function groupEventsById(events) { + var eventsById = {}; + var i, event; + + for (i = 0; i < events.length; i++) { + event = events[i]; + (eventsById[event._id] || (eventsById[event._id] = [])).push(event); + } + + return eventsById; +} + + +// A cmp function for determining which non-inverted "ranges" (see above) happen earlier +function compareRanges(range1, range2) { + return range1.start - range2.start; // earlier ranges go first +} + + +/* External-Dragging-Element Data +----------------------------------------------------------------------------------------------------------------------*/ + +// Require all HTML5 data-* attributes used by FullCalendar to have this prefix. +// A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event. +FC.dataAttrPrefix = ''; + +// Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure +// to be used for Event Object creation. +// A defined `.eventProps`, even when empty, indicates that an event should be created. +function getDraggedElMeta(el) { + var prefix = FC.dataAttrPrefix; + var eventProps; // properties for creating the event, not related to date/time + var startTime; // a Duration + var duration; + var stick; + + if (prefix) { prefix += '-'; } + eventProps = el.data(prefix + 'event') || null; + + if (eventProps) { + if (typeof eventProps === 'object') { + eventProps = $.extend({}, eventProps); // make a copy + } + else { // something like 1 or true. still signal event creation + eventProps = {}; + } + + // pluck special-cased date/time properties + startTime = eventProps.start; + if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well + duration = eventProps.duration; + stick = eventProps.stick; + delete eventProps.start; + delete eventProps.time; + delete eventProps.duration; + delete eventProps.stick; + } + + // fallback to standalone attribute values for each of the date/time properties + if (startTime == null) { startTime = el.data(prefix + 'start'); } + if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well + if (duration == null) { duration = el.data(prefix + 'duration'); } + if (stick == null) { stick = el.data(prefix + 'stick'); } + + // massage into correct data types + startTime = startTime != null ? moment.duration(startTime) : null; + duration = duration != null ? moment.duration(duration) : null; + stick = Boolean(stick); + + return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick }; +} + + +;; + +/* +A set of rendering and date-related methods for a visual component comprised of one or more rows of day columns. +Prerequisite: the object being mixed into needs to be a *Grid* +*/ +var DayTableMixin = FC.DayTableMixin = { + + breakOnWeeks: false, // should create a new row for each week? + dayDates: null, // whole-day dates for each column. left to right + dayIndices: null, // for each day from start, the offset + daysPerRow: null, + rowCnt: null, + colCnt: null, + colHeadFormat: null, + + + // Populates internal variables used for date calculation and rendering + updateDayTable: function() { + var view = this.view; + var date = this.start.clone(); + var dayIndex = -1; + var dayIndices = []; + var dayDates = []; + var daysPerRow; + var firstDay; + var rowCnt; + + while (date.isBefore(this.end)) { // loop each day from start to end + if (view.isHiddenDay(date)) { + dayIndices.push(dayIndex + 0.5); // mark that it's between indices + } + else { + dayIndex++; + dayIndices.push(dayIndex); + dayDates.push(date.clone()); + } + date.add(1, 'days'); + } + + if (this.breakOnWeeks) { + // count columns until the day-of-week repeats + firstDay = dayDates[0].day(); + for (daysPerRow = 1; daysPerRow < dayDates.length; daysPerRow++) { + if (dayDates[daysPerRow].day() == firstDay) { + break; + } + } + rowCnt = Math.ceil(dayDates.length / daysPerRow); + } + else { + rowCnt = 1; + daysPerRow = dayDates.length; + } + + this.dayDates = dayDates; + this.dayIndices = dayIndices; + this.daysPerRow = daysPerRow; + this.rowCnt = rowCnt; + + this.updateDayTableCols(); + }, + + + // Computes and assigned the colCnt property and updates any options that may be computed from it + updateDayTableCols: function() { + this.colCnt = this.computeColCnt(); + this.colHeadFormat = this.view.opt('columnFormat') || this.computeColHeadFormat(); + }, + + + // Determines how many columns there should be in the table + computeColCnt: function() { + return this.daysPerRow; + }, + + + // Computes the ambiguously-timed moment for the given cell + getCellDate: function(row, col) { + return this.dayDates[ + this.getCellDayIndex(row, col) + ].clone(); + }, + + + // Computes the ambiguously-timed date range for the given cell + getCellRange: function(row, col) { + var start = this.getCellDate(row, col); + var end = start.clone().add(1, 'days'); + + return { start: start, end: end }; + }, + + + // Returns the number of day cells, chronologically, from the first of the grid (0-based) + getCellDayIndex: function(row, col) { + return row * this.daysPerRow + this.getColDayIndex(col); + }, + + + // Returns the numner of day cells, chronologically, from the first cell in *any given row* + getColDayIndex: function(col) { + if (this.isRTL) { + return this.colCnt - 1 - col; + } + else { + return col; + } + }, + + + // Given a date, returns its chronolocial cell-index from the first cell of the grid. + // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets. + // If before the first offset, returns a negative number. + // If after the last offset, returns an offset past the last cell offset. + // Only works for *start* dates of cells. Will not work for exclusive end dates for cells. + getDateDayIndex: function(date) { + var dayIndices = this.dayIndices; + var dayOffset = date.diff(this.start, 'days'); + + if (dayOffset < 0) { + return dayIndices[0] - 1; + } + else if (dayOffset >= dayIndices.length) { + return dayIndices[dayIndices.length - 1] + 1; + } + else { + return dayIndices[dayOffset]; + } + }, + + + /* Options + ------------------------------------------------------------------------------------------------------------------*/ + + + // Computes a default column header formatting string if `colFormat` is not explicitly defined + computeColHeadFormat: function() { + // if more than one week row, or if there are a lot of columns with not much space, + // put just the day numbers will be in each cell + if (this.rowCnt > 1 || this.colCnt > 10) { + return 'ddd'; // "Sat" + } + // multiple days, so full single date string WON'T be in title text + else if (this.colCnt > 1) { + return this.view.opt('dayOfMonthFormat'); // "Sat 12/10" + } + // single day, so full single date string will probably be in title text + else { + return 'dddd'; // "Saturday" + } + }, + + + /* Slicing + ------------------------------------------------------------------------------------------------------------------*/ + + + // Slices up a date range into a segment for every week-row it intersects with + sliceRangeByRow: function(range) { + var daysPerRow = this.daysPerRow; + var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold + var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index + var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index + var segs = []; + var row; + var rowFirst, rowLast; // inclusive day-index range for current row + var segFirst, segLast; // inclusive day-index range for segment + + for (row = 0; row < this.rowCnt; row++) { + rowFirst = row * daysPerRow; + rowLast = rowFirst + daysPerRow - 1; + + // intersect segment's offset range with the row's + segFirst = Math.max(rangeFirst, rowFirst); + segLast = Math.min(rangeLast, rowLast); + + // deal with in-between indices + segFirst = Math.ceil(segFirst); // in-between starts round to next cell + segLast = Math.floor(segLast); // in-between ends round to prev cell + + if (segFirst <= segLast) { // was there any intersection with the current row? + segs.push({ + row: row, + + // normalize to start of row + firstRowDayIndex: segFirst - rowFirst, + lastRowDayIndex: segLast - rowFirst, + + // must be matching integers to be the segment's start/end + isStart: segFirst === rangeFirst, + isEnd: segLast === rangeLast + }); + } + } + + return segs; + }, + + + // Slices up a date range into a segment for every day-cell it intersects with. + // TODO: make more DRY with sliceRangeByRow somehow. + sliceRangeByDay: function(range) { + var daysPerRow = this.daysPerRow; + var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold + var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index + var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index + var segs = []; + var row; + var rowFirst, rowLast; // inclusive day-index range for current row + var i; + var segFirst, segLast; // inclusive day-index range for segment + + for (row = 0; row < this.rowCnt; row++) { + rowFirst = row * daysPerRow; + rowLast = rowFirst + daysPerRow - 1; + + for (i = rowFirst; i <= rowLast; i++) { + + // intersect segment's offset range with the row's + segFirst = Math.max(rangeFirst, i); + segLast = Math.min(rangeLast, i); + + // deal with in-between indices + segFirst = Math.ceil(segFirst); // in-between starts round to next cell + segLast = Math.floor(segLast); // in-between ends round to prev cell + + if (segFirst <= segLast) { // was there any intersection with the current row? + segs.push({ + row: row, + + // normalize to start of row + firstRowDayIndex: segFirst - rowFirst, + lastRowDayIndex: segLast - rowFirst, + + // must be matching integers to be the segment's start/end + isStart: segFirst === rangeFirst, + isEnd: segLast === rangeLast + }); + } + } + } + + return segs; + }, + + + /* Header Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + renderHeadHtml: function() { + var view = this.view; + + return '' + + '
    ' + + '' + + '' + + this.renderHeadTrHtml() + + '' + + '
    ' + + '
    '; + }, + + + renderHeadIntroHtml: function() { + return this.renderIntroHtml(); // fall back to generic + }, + + + renderHeadTrHtml: function() { + return '' + + '' + + (this.isRTL ? '' : this.renderHeadIntroHtml()) + + this.renderHeadDateCellsHtml() + + (this.isRTL ? this.renderHeadIntroHtml() : '') + + ''; + }, + + + renderHeadDateCellsHtml: function() { + var htmls = []; + var col, date; + + for (col = 0; col < this.colCnt; col++) { + date = this.getCellDate(0, col); + htmls.push(this.renderHeadDateCellHtml(date)); + } + + return htmls.join(''); + }, + + + // TODO: when internalApiVersion, accept an object for HTML attributes + // (colspan should be no different) + renderHeadDateCellHtml: function(date, colspan, otherAttrs) { + var view = this.view; + + return '' + + ' 1 ? + ' colspan="' + colspan + '"' : + '') + + (otherAttrs ? + ' ' + otherAttrs : + '') + + '>' + + htmlEscape(date.format(this.colHeadFormat)) + + ''; + }, + + + /* Background Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + renderBgTrHtml: function(row) { + return '' + + '' + + (this.isRTL ? '' : this.renderBgIntroHtml(row)) + + this.renderBgCellsHtml(row) + + (this.isRTL ? this.renderBgIntroHtml(row) : '') + + ''; + }, + + + renderBgIntroHtml: function(row) { + return this.renderIntroHtml(); // fall back to generic + }, + + + renderBgCellsHtml: function(row) { + var htmls = []; + var col, date; + + for (col = 0; col < this.colCnt; col++) { + date = this.getCellDate(row, col); + htmls.push(this.renderBgCellHtml(date)); + } + + return htmls.join(''); + }, + + + renderBgCellHtml: function(date, otherAttrs) { + var view = this.view; + var classes = this.getDayClasses(date); + + classes.unshift('fc-day', view.widgetContentClass); + + return ''; + }, + + + /* Generic + ------------------------------------------------------------------------------------------------------------------*/ + + + // Generates the default HTML intro for any row. User classes should override + renderIntroHtml: function() { + }, + + + // TODO: a generic method for dealing with , RTL, intro + // when increment internalApiVersion + // wrapTr (scheduler) + + + /* Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Applies the generic "intro" and "outro" HTML to the given cells. + // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro. + bookendCells: function(trEl) { + var introHtml = this.renderIntroHtml(); + + if (introHtml) { + if (this.isRTL) { + trEl.append(introHtml); + } + else { + trEl.prepend(introHtml); + } + } + } + +}; + +;; + +/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week. +----------------------------------------------------------------------------------------------------------------------*/ + +var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, { + + numbersVisible: false, // should render a row for day/week numbers? set by outside view. TODO: make internal + bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid + + rowEls: null, // set of fake row elements + cellEls: null, // set of whole-day elements comprising the row's background + helperEls: null, // set of cell skeleton elements for rendering the mock event "helper" + + rowCoordCache: null, + colCoordCache: null, + + + // Renders the rows and columns into the component's `this.el`, which should already be assigned. + // isRigid determins whether the individual rows should ignore the contents and be a constant height. + // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient. + renderDates: function(isRigid) { + var view = this.view; + var rowCnt = this.rowCnt; + var colCnt = this.colCnt; + var html = ''; + var row; + var col; + + for (row = 0; row < rowCnt; row++) { + html += this.renderDayRowHtml(row, isRigid); + } + this.el.html(html); + + this.rowEls = this.el.find('.fc-row'); + this.cellEls = this.el.find('.fc-day'); + + this.rowCoordCache = new CoordCache({ + els: this.rowEls, + isVertical: true + }); + this.colCoordCache = new CoordCache({ + els: this.cellEls.slice(0, this.colCnt), // only the first row + isHorizontal: true + }); + + // trigger dayRender with each cell's element + for (row = 0; row < rowCnt; row++) { + for (col = 0; col < colCnt; col++) { + view.trigger( + 'dayRender', + null, + this.getCellDate(row, col), + this.getCellEl(row, col) + ); + } + } + }, + + + unrenderDates: function() { + this.removeSegPopover(); + }, + + + renderBusinessHours: function() { + var events = this.view.calendar.getBusinessHoursEvents(true); // wholeDay=true + var segs = this.eventsToSegs(events); + + this.renderFill('businessHours', segs, 'bgevent'); + }, + + + unrenderBusinessHours: function() { + this.unrenderFill('businessHours'); + }, + + + // Generates the HTML for a single row, which is a div that wraps a table. + // `row` is the row number. + renderDayRowHtml: function(row, isRigid) { + var view = this.view; + var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ]; + + if (isRigid) { + classes.push('fc-rigid'); + } + + return '' + + '
    ' + + '
    ' + + '' + + this.renderBgTrHtml(row) + + '
    ' + + '
    ' + + '
    ' + + '' + + (this.numbersVisible ? + '' + + this.renderNumberTrHtml(row) + + '' : + '' + ) + + '
    ' + + '
    ' + + '
    '; + }, + + + /* Grid Number Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + renderNumberTrHtml: function(row) { + return '' + + '' + + (this.isRTL ? '' : this.renderNumberIntroHtml(row)) + + this.renderNumberCellsHtml(row) + + (this.isRTL ? this.renderNumberIntroHtml(row) : '') + + ''; + }, + + + renderNumberIntroHtml: function(row) { + return this.renderIntroHtml(); + }, + + + renderNumberCellsHtml: function(row) { + var htmls = []; + var col, date; + + for (col = 0; col < this.colCnt; col++) { + date = this.getCellDate(row, col); + htmls.push(this.renderNumberCellHtml(date)); + } + + return htmls.join(''); + }, + + + // Generates the HTML for the s of the "number" row in the DayGrid's content skeleton. + // The number row will only exist if either day numbers or week numbers are turned on. + renderNumberCellHtml: function(date) { + var classes; + + if (!this.view.dayNumbersVisible) { // if there are week numbers but not day numbers + return ''; // will create an empty space above events :( + } + + classes = this.getDayClasses(date); + classes.unshift('fc-day-number'); + + return '' + + '' + + date.date() + + ''; + }, + + + /* Options + ------------------------------------------------------------------------------------------------------------------*/ + + + // Computes a default event time formatting string if `timeFormat` is not explicitly defined + computeEventTimeFormat: function() { + return this.view.opt('extraSmallTimeFormat'); // like "6p" or "6:30p" + }, + + + // Computes a default `displayEventEnd` value if one is not expliclty defined + computeDisplayEventEnd: function() { + return this.colCnt == 1; // we'll likely have space if there's only one day + }, + + + /* Dates + ------------------------------------------------------------------------------------------------------------------*/ + + + rangeUpdated: function() { + this.updateDayTable(); + }, + + + // Slices up the given span (unzoned start/end with other misc data) into an array of segments + spanToSegs: function(span) { + var segs = this.sliceRangeByRow(span); + var i, seg; + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + if (this.isRTL) { + seg.leftCol = this.daysPerRow - 1 - seg.lastRowDayIndex; + seg.rightCol = this.daysPerRow - 1 - seg.firstRowDayIndex; + } + else { + seg.leftCol = seg.firstRowDayIndex; + seg.rightCol = seg.lastRowDayIndex; + } + } + + return segs; + }, + + + /* Hit System + ------------------------------------------------------------------------------------------------------------------*/ + + + prepareHits: function() { + this.colCoordCache.build(); + this.rowCoordCache.build(); + this.rowCoordCache.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack + }, + + + releaseHits: function() { + this.colCoordCache.clear(); + this.rowCoordCache.clear(); + }, + + + queryHit: function(leftOffset, topOffset) { + var col = this.colCoordCache.getHorizontalIndex(leftOffset); + var row = this.rowCoordCache.getVerticalIndex(topOffset); + + if (row != null && col != null) { + return this.getCellHit(row, col); + } + }, + + + getHitSpan: function(hit) { + return this.getCellRange(hit.row, hit.col); + }, + + + getHitEl: function(hit) { + return this.getCellEl(hit.row, hit.col); + }, + + + /* Cell System + ------------------------------------------------------------------------------------------------------------------*/ + // FYI: the first column is the leftmost column, regardless of date + + + getCellHit: function(row, col) { + return { + row: row, + col: col, + component: this, // needed unfortunately :( + left: this.colCoordCache.getLeftOffset(col), + right: this.colCoordCache.getRightOffset(col), + top: this.rowCoordCache.getTopOffset(row), + bottom: this.rowCoordCache.getBottomOffset(row) + }; + }, + + + getCellEl: function(row, col) { + return this.cellEls.eq(row * this.colCnt + col); + }, + + + /* Event Drag Visualization + ------------------------------------------------------------------------------------------------------------------*/ + // TODO: move to DayGrid.event, similar to what we did with Grid's drag methods + + + // Renders a visual indication of an event or external element being dragged. + // `eventLocation` has zoned start and end (optional) + renderDrag: function(eventLocation, seg) { + + // always render a highlight underneath + this.renderHighlight(this.eventToSpan(eventLocation)); + + // if a segment from the same calendar but another component is being dragged, render a helper event + if (seg && !seg.el.closest(this.el).length) { + + return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements + } + }, + + + // Unrenders any visual indication of a hovering event + unrenderDrag: function() { + this.unrenderHighlight(); + this.unrenderHelper(); + }, + + + /* Event Resize Visualization + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being resized + renderEventResize: function(eventLocation, seg) { + this.renderHighlight(this.eventToSpan(eventLocation)); + return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements + }, + + + // Unrenders a visual indication of an event being resized + unrenderEventResize: function() { + this.unrenderHighlight(); + this.unrenderHelper(); + }, + + + /* Event Helper + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null. + renderHelper: function(event, sourceSeg) { + var helperNodes = []; + var segs = this.eventToSegs(event); + var rowStructs; + + segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered + rowStructs = this.renderSegRows(segs); + + // inject each new event skeleton into each associated row + this.rowEls.each(function(row, rowNode) { + var rowEl = $(rowNode); // the .fc-row + var skeletonEl = $('
    '); // will be absolutely positioned + var skeletonTop; + + // If there is an original segment, match the top position. Otherwise, put it at the row's top level + if (sourceSeg && sourceSeg.row === row) { + skeletonTop = sourceSeg.el.position().top; + } + else { + skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top; + } + + skeletonEl.css('top', skeletonTop) + .find('table') + .append(rowStructs[row].tbodyEl); + + rowEl.append(skeletonEl); + helperNodes.push(skeletonEl[0]); + }); + + return ( // must return the elements rendered + this.helperEls = $(helperNodes) // array -> jQuery set + ); + }, + + + // Unrenders any visual indication of a mock helper event + unrenderHelper: function() { + if (this.helperEls) { + this.helperEls.remove(); + this.helperEls = null; + } + }, + + + /* Fill System (highlight, background events, business hours) + ------------------------------------------------------------------------------------------------------------------*/ + + + fillSegTag: 'td', // override the default tag name + + + // Renders a set of rectangles over the given segments of days. + // Only returns segments that successfully rendered. + renderFill: function(type, segs, className) { + var nodes = []; + var i, seg; + var skeletonEl; + + segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + skeletonEl = this.renderFillRow(type, seg, className); + this.rowEls.eq(seg.row).append(skeletonEl); + nodes.push(skeletonEl[0]); + } + + this.elsByFill[type] = $(nodes); + + return segs; + }, + + + // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered. + renderFillRow: function(type, seg, className) { + var colCnt = this.colCnt; + var startCol = seg.leftCol; + var endCol = seg.rightCol + 1; + var skeletonEl; + var trEl; + + className = className || type.toLowerCase(); + + skeletonEl = $( + '
    ' + + '
    ' + + '
    ' + ); + trEl = skeletonEl.find('tr'); + + if (startCol > 0) { + trEl.append(''); + } + + trEl.append( + seg.el.attr('colspan', endCol - startCol) + ); + + if (endCol < colCnt) { + trEl.append(''); + } + + this.bookendCells(trEl); + + return skeletonEl; + } + +}); + +;; + +/* Event-rendering methods for the DayGrid class +----------------------------------------------------------------------------------------------------------------------*/ + +DayGrid.mixin({ + + rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering + + + // Unrenders all events currently rendered on the grid + unrenderEvents: function() { + this.removeSegPopover(); // removes the "more.." events popover + Grid.prototype.unrenderEvents.apply(this, arguments); // calls the super-method + }, + + + // Retrieves all rendered segment objects currently rendered on the grid + getEventSegs: function() { + return Grid.prototype.getEventSegs.call(this) // get the segments from the super-method + .concat(this.popoverSegs || []); // append the segments from the "more..." popover + }, + + + // Renders the given background event segments onto the grid + renderBgSegs: function(segs) { + + // don't render timed background events + var allDaySegs = $.grep(segs, function(seg) { + return seg.event.allDay; + }); + + return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method + }, + + + // Renders the given foreground event segments onto the grid + renderFgSegs: function(segs) { + var rowStructs; + + // render an `.el` on each seg + // returns a subset of the segs. segs that were actually rendered + segs = this.renderFgSegEls(segs); + + rowStructs = this.rowStructs = this.renderSegRows(segs); + + // append to each row's content skeleton + this.rowEls.each(function(i, rowNode) { + $(rowNode).find('.fc-content-skeleton > table').append( + rowStructs[i].tbodyEl + ); + }); + + return segs; // return only the segs that were actually rendered + }, + + + // Unrenders all currently rendered foreground event segments + unrenderFgSegs: function() { + var rowStructs = this.rowStructs || []; + var rowStruct; + + while ((rowStruct = rowStructs.pop())) { + rowStruct.tbodyEl.remove(); + } + + this.rowStructs = null; + }, + + + // Uses the given events array to generate elements that should be appended to each row's content skeleton. + // Returns an array of rowStruct objects (see the bottom of `renderSegRow`). + // PRECONDITION: each segment shoud already have a rendered and assigned `.el` + renderSegRows: function(segs) { + var rowStructs = []; + var segRows; + var row; + + segRows = this.groupSegRows(segs); // group into nested arrays + + // iterate each row of segment groupings + for (row = 0; row < segRows.length; row++) { + rowStructs.push( + this.renderSegRow(row, segRows[row]) + ); + } + + return rowStructs; + }, + + + // Builds the HTML to be used for the default element for an individual segment + fgSegHtml: function(seg, disableResizing) { + var view = this.view; + var event = seg.event; + var isDraggable = view.isEventDraggable(event); + var isResizableFromStart = !disableResizing && event.allDay && + seg.isStart && view.isEventResizableFromStart(event); + var isResizableFromEnd = !disableResizing && event.allDay && + seg.isEnd && view.isEventResizableFromEnd(event); + var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); + var skinCss = cssToStr(this.getSegSkinCss(seg)); + var timeHtml = ''; + var timeText; + var titleHtml; + + classes.unshift('fc-day-grid-event', 'fc-h-event'); + + // Only display a timed events time if it is the starting segment + if (seg.isStart) { + timeText = this.getEventTimeText(event); + if (timeText) { + timeHtml = '' + htmlEscape(timeText) + ''; + } + } + + titleHtml = + '' + + (htmlEscape(event.title || '') || ' ') + // we always want one line of height + ''; + + return '
    ' + + '
    ' + + (this.isRTL ? + titleHtml + ' ' + timeHtml : // put a natural space in between + timeHtml + ' ' + titleHtml // + ) + + '
    ' + + (isResizableFromStart ? + '
    ' : + '' + ) + + (isResizableFromEnd ? + '
    ' : + '' + ) + + ''; + }, + + + // Given a row # and an array of segments all in the same row, render a element, a skeleton that contains + // the segments. Returns object with a bunch of internal data about how the render was calculated. + // NOTE: modifies rowSegs + renderSegRow: function(row, rowSegs) { + var colCnt = this.colCnt; + var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels + var levelCnt = Math.max(1, segLevels.length); // ensure at least one level + var tbody = $(''); + var segMatrix = []; // lookup for which segments are rendered into which level+col cells + var cellMatrix = []; // lookup for all elements of the level+col matrix + var loneCellMatrix = []; // lookup for elements that only take up a single column + var i, levelSegs; + var col; + var tr; + var j, seg; + var td; + + // populates empty cells from the current column (`col`) to `endCol` + function emptyCellsUntil(endCol) { + while (col < endCol) { + // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell + td = (loneCellMatrix[i - 1] || [])[col]; + if (td) { + td.attr( + 'rowspan', + parseInt(td.attr('rowspan') || 1, 10) + 1 + ); + } + else { + td = $(''); + tr.append(td); + } + cellMatrix[i][col] = td; + loneCellMatrix[i][col] = td; + col++; + } + } + + for (i = 0; i < levelCnt; i++) { // iterate through all levels + levelSegs = segLevels[i]; + col = 0; + tr = $(''); + + segMatrix.push([]); + cellMatrix.push([]); + loneCellMatrix.push([]); + + // levelCnt might be 1 even though there are no actual levels. protect against this. + // this single empty row is useful for styling. + if (levelSegs) { + for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level + seg = levelSegs[j]; + + emptyCellsUntil(seg.leftCol); + + // create a container that occupies or more columns. append the event element. + td = $('').append(seg.el); + if (seg.leftCol != seg.rightCol) { + td.attr('colspan', seg.rightCol - seg.leftCol + 1); + } + else { // a single-column segment + loneCellMatrix[i][col] = td; + } + + while (col <= seg.rightCol) { + cellMatrix[i][col] = td; + segMatrix[i][col] = seg; + col++; + } + + tr.append(td); + } + } + + emptyCellsUntil(colCnt); // finish off the row + this.bookendCells(tr); + tbody.append(tr); + } + + return { // a "rowStruct" + row: row, // the row number + tbodyEl: tbody, + cellMatrix: cellMatrix, + segMatrix: segMatrix, + segLevels: segLevels, + segs: rowSegs + }; + }, + + + // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels. + // NOTE: modifies segs + buildSegLevels: function(segs) { + var levels = []; + var i, seg; + var j; + + // Give preference to elements with certain criteria, so they have + // a chance to be closer to the top. + this.sortEventSegs(segs); + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + + // loop through levels, starting with the topmost, until the segment doesn't collide with other segments + for (j = 0; j < levels.length; j++) { + if (!isDaySegCollision(seg, levels[j])) { + break; + } + } + // `j` now holds the desired subrow index + seg.level = j; + + // create new level array if needed and append segment + (levels[j] || (levels[j] = [])).push(seg); + } + + // order segments left-to-right. very important if calendar is RTL + for (j = 0; j < levels.length; j++) { + levels[j].sort(compareDaySegCols); + } + + return levels; + }, + + + // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row + groupSegRows: function(segs) { + var segRows = []; + var i; + + for (i = 0; i < this.rowCnt; i++) { + segRows.push([]); + } + + for (i = 0; i < segs.length; i++) { + segRows[segs[i].row].push(segs[i]); + } + + return segRows; + } + +}); + + +// Computes whether two segments' columns collide. They are assumed to be in the same row. +function isDaySegCollision(seg, otherSegs) { + var i, otherSeg; + + for (i = 0; i < otherSegs.length; i++) { + otherSeg = otherSegs[i]; + + if ( + otherSeg.leftCol <= seg.rightCol && + otherSeg.rightCol >= seg.leftCol + ) { + return true; + } + } + + return false; +} + + +// A cmp function for determining the leftmost event +function compareDaySegCols(a, b) { + return a.leftCol - b.leftCol; +} + +;; + +/* Methods relate to limiting the number events for a given day on a DayGrid +----------------------------------------------------------------------------------------------------------------------*/ +// NOTE: all the segs being passed around in here are foreground segs + +DayGrid.mixin({ + + segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible + popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible + + + removeSegPopover: function() { + if (this.segPopover) { + this.segPopover.hide(); // in handler, will call segPopover's removeElement + } + }, + + + // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid. + // `levelLimit` can be false (don't limit), a number, or true (should be computed). + limitRows: function(levelLimit) { + var rowStructs = this.rowStructs || []; + var row; // row # + var rowLevelLimit; + + for (row = 0; row < rowStructs.length; row++) { + this.unlimitRow(row); + + if (!levelLimit) { + rowLevelLimit = false; + } + else if (typeof levelLimit === 'number') { + rowLevelLimit = levelLimit; + } + else { + rowLevelLimit = this.computeRowLevelLimit(row); + } + + if (rowLevelLimit !== false) { + this.limitRow(row, rowLevelLimit); + } + } + }, + + + // Computes the number of levels a row will accomodate without going outside its bounds. + // Assumes the row is "rigid" (maintains a constant height regardless of what is inside). + // `row` is the row number. + computeRowLevelLimit: function(row) { + var rowEl = this.rowEls.eq(row); // the containing "fake" row div + var rowHeight = rowEl.height(); // TODO: cache somehow? + var trEls = this.rowStructs[row].tbodyEl.children(); + var i, trEl; + var trHeight; + + function iterInnerHeights(i, childNode) { + trHeight = Math.max(trHeight, $(childNode).outerHeight()); + } + + // Reveal one level at a time and stop when we find one out of bounds + for (i = 0; i < trEls.length; i++) { + trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal) + + // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell, + // so instead, find the tallest inner content element. + trHeight = 0; + trEl.find('> td > :first-child').each(iterInnerHeights); + + if (trEl.position().top + trHeight > rowHeight) { + return i; + } + } + + return false; // should not limit at all + }, + + + // Limits the given grid row to the maximum number of levels and injects "more" links if necessary. + // `row` is the row number. + // `levelLimit` is a number for the maximum (inclusive) number of levels allowed. + limitRow: function(row, levelLimit) { + var _this = this; + var rowStruct = this.rowStructs[row]; + var moreNodes = []; // array of "more" links and DOM nodes + var col = 0; // col #, left-to-right (not chronologically) + var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right + var cellMatrix; // a matrix (by level, then column) of all jQuery elements in the row + var limitedNodes; // array of temporarily hidden level and segment DOM nodes + var i, seg; + var segsBelow; // array of segment objects below `seg` in the current `col` + var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies + var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column) + var td, rowspan; + var segMoreNodes; // array of "more" cells that will stand-in for the current seg's cell + var j; + var moreTd, moreWrap, moreLink; + + // Iterates through empty level cells and places "more" links inside if need be + function emptyCellsUntil(endCol) { // goes from current `col` to `endCol` + while (col < endCol) { + segsBelow = _this.getCellSegs(row, col, levelLimit); + if (segsBelow.length) { + td = cellMatrix[levelLimit - 1][col]; + moreLink = _this.renderMoreLink(row, col, segsBelow); + moreWrap = $('
    ').append(moreLink); + td.append(moreWrap); + moreNodes.push(moreWrap[0]); + } + col++; + } + } + + if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit? + levelSegs = rowStruct.segLevels[levelLimit - 1]; + cellMatrix = rowStruct.cellMatrix; + + limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level elements past the limit + .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array + + // iterate though segments in the last allowable level + for (i = 0; i < levelSegs.length; i++) { + seg = levelSegs[i]; + emptyCellsUntil(seg.leftCol); // process empty cells before the segment + + // determine *all* segments below `seg` that occupy the same columns + colSegsBelow = []; + totalSegsBelow = 0; + while (col <= seg.rightCol) { + segsBelow = this.getCellSegs(row, col, levelLimit); + colSegsBelow.push(segsBelow); + totalSegsBelow += segsBelow.length; + col++; + } + + if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links? + td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell + rowspan = td.attr('rowspan') || 1; + segMoreNodes = []; + + // make a replacement for each column the segment occupies. will be one for each colspan + for (j = 0; j < colSegsBelow.length; j++) { + moreTd = $('').attr('rowspan', rowspan); + segsBelow = colSegsBelow[j]; + moreLink = this.renderMoreLink( + row, + seg.leftCol + j, + [ seg ].concat(segsBelow) // count seg as hidden too + ); + moreWrap = $('
    ').append(moreLink); + moreTd.append(moreWrap); + segMoreNodes.push(moreTd[0]); + moreNodes.push(moreTd[0]); + } + + td.addClass('fc-limited').after($(segMoreNodes)); // hide original and inject replacements + limitedNodes.push(td[0]); + } + } + + emptyCellsUntil(this.colCnt); // finish off the level + rowStruct.moreEls = $(moreNodes); // for easy undoing later + rowStruct.limitedEls = $(limitedNodes); // for easy undoing later + } + }, + + + // Reveals all levels and removes all "more"-related elements for a grid's row. + // `row` is a row number. + unlimitRow: function(row) { + var rowStruct = this.rowStructs[row]; + + if (rowStruct.moreEls) { + rowStruct.moreEls.remove(); + rowStruct.moreEls = null; + } + + if (rowStruct.limitedEls) { + rowStruct.limitedEls.removeClass('fc-limited'); + rowStruct.limitedEls = null; + } + }, + + + // Renders an element that represents hidden event element for a cell. + // Responsible for attaching click handler as well. + renderMoreLink: function(row, col, hiddenSegs) { + var _this = this; + var view = this.view; + + return $('') + .text( + this.getMoreLinkText(hiddenSegs.length) + ) + .on('click', function(ev) { + var clickOption = view.opt('eventLimitClick'); + var date = _this.getCellDate(row, col); + var moreEl = $(this); + var dayEl = _this.getCellEl(row, col); + var allSegs = _this.getCellSegs(row, col); + + // rescope the segments to be within the cell's date + var reslicedAllSegs = _this.resliceDaySegs(allSegs, date); + var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date); + + if (typeof clickOption === 'function') { + // the returned value can be an atomic option + clickOption = view.trigger('eventLimitClick', null, { + date: date, + dayEl: dayEl, + moreEl: moreEl, + segs: reslicedAllSegs, + hiddenSegs: reslicedHiddenSegs + }, ev); + } + + if (clickOption === 'popover') { + _this.showSegPopover(row, col, moreEl, reslicedAllSegs); + } + else if (typeof clickOption === 'string') { // a view name + view.calendar.zoomTo(date, clickOption); + } + }); + }, + + + // Reveals the popover that displays all events within a cell + showSegPopover: function(row, col, moreLink, segs) { + var _this = this; + var view = this.view; + var moreWrap = moreLink.parent(); // the
    wrapper around the + var topEl; // the element we want to match the top coordinate of + var options; + + if (this.rowCnt == 1) { + topEl = view.el; // will cause the popover to cover any sort of header + } + else { + topEl = this.rowEls.eq(row); // will align with top of row + } + + options = { + className: 'fc-more-popover', + content: this.renderSegPopoverContent(row, col, segs), + parentEl: this.el, + top: topEl.offset().top, + autoHide: true, // when the user clicks elsewhere, hide the popover + viewportConstrain: view.opt('popoverViewportConstrain'), + hide: function() { + // kill everything when the popover is hidden + _this.segPopover.removeElement(); + _this.segPopover = null; + _this.popoverSegs = null; + } + }; + + // Determine horizontal coordinate. + // We use the moreWrap instead of the to avoid border confusion. + if (this.isRTL) { + options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border + } + else { + options.left = moreWrap.offset().left - 1; // -1 to be over cell border + } + + this.segPopover = new Popover(options); + this.segPopover.show(); + }, + + + // Builds the inner DOM contents of the segment popover + renderSegPopoverContent: function(row, col, segs) { + var view = this.view; + var isTheme = view.opt('theme'); + var title = this.getCellDate(row, col).format(view.opt('dayPopoverFormat')); + var content = $( + '
    ' + + '' + + '' + + htmlEscape(title) + + '' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + ); + var segContainer = content.find('.fc-event-container'); + var i; + + // render each seg's `el` and only return the visible segs + segs = this.renderFgSegEls(segs, true); // disableResizing=true + this.popoverSegs = segs; + + for (i = 0; i < segs.length; i++) { + + // because segments in the popover are not part of a grid coordinate system, provide a hint to any + // grids that want to do drag-n-drop about which cell it came from + this.prepareHits(); + segs[i].hit = this.getCellHit(row, col); + this.releaseHits(); + + segContainer.append(segs[i].el); + } + + return content; + }, + + + // Given the events within an array of segment objects, reslice them to be in a single day + resliceDaySegs: function(segs, dayDate) { + + // build an array of the original events + var events = $.map(segs, function(seg) { + return seg.event; + }); + + var dayStart = dayDate.clone(); + var dayEnd = dayStart.clone().add(1, 'days'); + var dayRange = { start: dayStart, end: dayEnd }; + + // slice the events with a custom slicing function + segs = this.eventsToSegs( + events, + function(range) { + var seg = intersectRanges(range, dayRange); // undefind if no intersection + return seg ? [ seg ] : []; // must return an array of segments + } + ); + + // force an order because eventsToSegs doesn't guarantee one + this.sortEventSegs(segs); + + return segs; + }, + + + // Generates the text that should be inside a "more" link, given the number of events it represents + getMoreLinkText: function(num) { + var opt = this.view.opt('eventLimitText'); + + if (typeof opt === 'function') { + return opt(num); + } + else { + return '+' + num + ' ' + opt; + } + }, + + + // Returns segments within a given cell. + // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs. + getCellSegs: function(row, col, startLevel) { + var segMatrix = this.rowStructs[row].segMatrix; + var level = startLevel || 0; + var segs = []; + var seg; + + while (level < segMatrix.length) { + seg = segMatrix[level][col]; + if (seg) { + segs.push(seg); + } + level++; + } + + return segs; + } + +}); + +;; + +/* A component that renders one or more columns of vertical time slots +----------------------------------------------------------------------------------------------------------------------*/ +// We mixin DayTable, even though there is only a single row of days + +var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { + + slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines + snapDuration: null, // granularity of time for dragging and selecting + snapsPerSlot: null, + minTime: null, // Duration object that denotes the first visible time of any given day + maxTime: null, // Duration object that denotes the exclusive visible end time of any given day + labelFormat: null, // formatting string for times running along vertical axis + labelInterval: null, // duration of how often a label should be displayed for a slot + + colEls: null, // cells elements in the day-row background + slatContainerEl: null, // div that wraps all the slat rows + slatEls: null, // elements running horizontally across all columns + nowIndicatorEls: null, + + colCoordCache: null, + slatCoordCache: null, + + + constructor: function() { + Grid.apply(this, arguments); // call the super-constructor + + this.processOptions(); + }, + + + // Renders the time grid into `this.el`, which should already be assigned. + // Relies on the view's colCnt. In the future, this component should probably be self-sufficient. + renderDates: function() { + this.el.html(this.renderHtml()); + this.colEls = this.el.find('.fc-day'); + this.slatContainerEl = this.el.find('.fc-slats'); + this.slatEls = this.slatContainerEl.find('tr'); + + this.colCoordCache = new CoordCache({ + els: this.colEls, + isHorizontal: true + }); + this.slatCoordCache = new CoordCache({ + els: this.slatEls, + isVertical: true + }); + + this.renderContentSkeleton(); + }, + + + // Renders the basic HTML skeleton for the grid + renderHtml: function() { + return '' + + '
    ' + + '' + + this.renderBgTrHtml(0) + // row=0 + '
    ' + + '
    ' + + '
    ' + + '' + + this.renderSlatRowHtml() + + '
    ' + + '
    '; + }, + + + // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. + renderSlatRowHtml: function() { + var view = this.view; + var isRTL = this.isRTL; + var html = ''; + var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations + var slotDate; // will be on the view's first day, but we only care about its time + var isLabeled; + var axisHtml; + + // Calculate the time for each slot + while (slotTime < this.maxTime) { + slotDate = this.start.clone().time(slotTime); + isLabeled = isInt(divideDurationByDuration(slotTime, this.labelInterval)); + + axisHtml = + '' + + (isLabeled ? + '' + // for matchCellWidths + htmlEscape(slotDate.format(this.labelFormat)) + + '' : + '' + ) + + ''; + + html += + '' + + (!isRTL ? axisHtml : '') + + '' + + (isRTL ? axisHtml : '') + + ""; + + slotTime.add(this.slotDuration); + } + + return html; + }, + + + /* Options + ------------------------------------------------------------------------------------------------------------------*/ + + + // Parses various options into properties of this object + processOptions: function() { + var view = this.view; + var slotDuration = view.opt('slotDuration'); + var snapDuration = view.opt('snapDuration'); + var input; + + slotDuration = moment.duration(slotDuration); + snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; + + this.slotDuration = slotDuration; + this.snapDuration = snapDuration; + this.snapsPerSlot = slotDuration / snapDuration; // TODO: ensure an integer multiple? + + this.minResizeDuration = snapDuration; // hack + + this.minTime = moment.duration(view.opt('minTime')); + this.maxTime = moment.duration(view.opt('maxTime')); + + // might be an array value (for TimelineView). + // if so, getting the most granular entry (the last one probably). + input = view.opt('slotLabelFormat'); + if ($.isArray(input)) { + input = input[input.length - 1]; + } + + this.labelFormat = + input || + view.opt('axisFormat') || // deprecated + view.opt('smallTimeFormat'); // the computed default + + input = view.opt('slotLabelInterval'); + this.labelInterval = input ? + moment.duration(input) : + this.computeLabelInterval(slotDuration); + }, + + + // Computes an automatic value for slotLabelInterval + computeLabelInterval: function(slotDuration) { + var i; + var labelInterval; + var slotsPerLabel; + + // find the smallest stock label interval that results in more than one slots-per-label + for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) { + labelInterval = moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]); + slotsPerLabel = divideDurationByDuration(labelInterval, slotDuration); + if (isInt(slotsPerLabel) && slotsPerLabel > 1) { + return labelInterval; + } + } + + return moment.duration(slotDuration); // fall back. clone + }, + + + // Computes a default event time formatting string if `timeFormat` is not explicitly defined + computeEventTimeFormat: function() { + return this.view.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM) + }, + + + // Computes a default `displayEventEnd` value if one is not expliclty defined + computeDisplayEventEnd: function() { + return true; + }, + + + /* Hit System + ------------------------------------------------------------------------------------------------------------------*/ + + + prepareHits: function() { + this.colCoordCache.build(); + this.slatCoordCache.build(); + }, + + + releaseHits: function() { + this.colCoordCache.clear(); + // NOTE: don't clear slatCoordCache because we rely on it for computeTimeTop + }, + + + queryHit: function(leftOffset, topOffset) { + var snapsPerSlot = this.snapsPerSlot; + var colCoordCache = this.colCoordCache; + var slatCoordCache = this.slatCoordCache; + var colIndex = colCoordCache.getHorizontalIndex(leftOffset); + var slatIndex = slatCoordCache.getVerticalIndex(topOffset); + + if (colIndex != null && slatIndex != null) { + var slatTop = slatCoordCache.getTopOffset(slatIndex); + var slatHeight = slatCoordCache.getHeight(slatIndex); + var partial = (topOffset - slatTop) / slatHeight; // floating point number between 0 and 1 + var localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat + var snapIndex = slatIndex * snapsPerSlot + localSnapIndex; + var snapTop = slatTop + (localSnapIndex / snapsPerSlot) * slatHeight; + var snapBottom = slatTop + ((localSnapIndex + 1) / snapsPerSlot) * slatHeight; + + return { + col: colIndex, + snap: snapIndex, + component: this, // needed unfortunately :( + left: colCoordCache.getLeftOffset(colIndex), + right: colCoordCache.getRightOffset(colIndex), + top: snapTop, + bottom: snapBottom + }; + } + }, + + + getHitSpan: function(hit) { + var start = this.getCellDate(0, hit.col); // row=0 + var time = this.computeSnapTime(hit.snap); // pass in the snap-index + var end; + + start.time(time); + end = start.clone().add(this.snapDuration); + + return { start: start, end: end }; + }, + + + getHitEl: function(hit) { + return this.colEls.eq(hit.col); + }, + + + /* Dates + ------------------------------------------------------------------------------------------------------------------*/ + + + rangeUpdated: function() { + this.updateDayTable(); + }, + + + // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day + computeSnapTime: function(snapIndex) { + return moment.duration(this.minTime + this.snapDuration * snapIndex); + }, + + + // Slices up the given span (unzoned start/end with other misc data) into an array of segments + spanToSegs: function(span) { + var segs = this.sliceRangeByTimes(span); + var i; + + for (i = 0; i < segs.length; i++) { + if (this.isRTL) { + segs[i].col = this.daysPerRow - 1 - segs[i].dayIndex; + } + else { + segs[i].col = segs[i].dayIndex; + } + } + + return segs; + }, + + + sliceRangeByTimes: function(range) { + var segs = []; + var seg; + var dayIndex; + var dayDate; + var dayRange; + + for (dayIndex = 0; dayIndex < this.daysPerRow; dayIndex++) { + dayDate = this.dayDates[dayIndex].clone(); // TODO: better API for this? + dayRange = { + start: dayDate.clone().time(this.minTime), + end: dayDate.clone().time(this.maxTime) + }; + seg = intersectRanges(range, dayRange); // both will be ambig timezone + if (seg) { + seg.dayIndex = dayIndex; + segs.push(seg); + } + } + + return segs; + }, + + + /* Coordinates + ------------------------------------------------------------------------------------------------------------------*/ + + + updateSize: function(isResize) { // NOT a standard Grid method + this.slatCoordCache.build(); + + if (isResize) { + this.updateSegVerticals( + [].concat(this.fgSegs || [], this.bgSegs || [], this.businessSegs || []) + ); + } + }, + + + getTotalSlatHeight: function() { + return this.slatContainerEl.outerHeight(); + }, + + + // Computes the top coordinate, relative to the bounds of the grid, of the given date. + // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight. + computeDateTop: function(date, startOfDayDate) { + return this.computeTimeTop( + moment.duration( + date - startOfDayDate.clone().stripTime() + ) + ); + }, + + + // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). + computeTimeTop: function(time) { + var len = this.slatEls.length; + var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered + var slatIndex; + var slatRemainder; + + // compute a floating-point number for how many slats should be progressed through. + // from 0 to number of slats (inclusive) + // constrained because minTime/maxTime might be customized. + slatCoverage = Math.max(0, slatCoverage); + slatCoverage = Math.min(len, slatCoverage); + + // an integer index of the furthest whole slat + // from 0 to number slats (*exclusive*, so len-1) + slatIndex = Math.floor(slatCoverage); + slatIndex = Math.min(slatIndex, len - 1); + + // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition. + // could be 1.0 if slatCoverage is covering *all* the slots + slatRemainder = slatCoverage - slatIndex; + + return this.slatCoordCache.getTopPosition(slatIndex) + + this.slatCoordCache.getHeight(slatIndex) * slatRemainder; + }, + + + + /* Event Drag Visualization + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being dragged over the specified date(s). + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(eventLocation, seg) { + + if (seg) { // if there is event information for this drag, render a helper event + + // returns mock event elements + // signal that a helper has been rendered + return this.renderEventLocationHelper(eventLocation, seg); + } + else { + // otherwise, just render a highlight + this.renderHighlight(this.eventToSpan(eventLocation)); + } + }, + + + // Unrenders any visual indication of an event being dragged + unrenderDrag: function() { + this.unrenderHelper(); + this.unrenderHighlight(); + }, + + + /* Event Resize Visualization + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being resized + renderEventResize: function(eventLocation, seg) { + return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements + }, + + + // Unrenders any visual indication of an event being resized + unrenderEventResize: function() { + this.unrenderHelper(); + }, + + + /* Event Helper + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag) + renderHelper: function(event, sourceSeg) { + return this.renderHelperSegs(this.eventToSegs(event), sourceSeg); // returns mock event elements + }, + + + // Unrenders any mock helper event + unrenderHelper: function() { + this.unrenderHelperSegs(); + }, + + + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ + + + renderBusinessHours: function() { + var events = this.view.calendar.getBusinessHoursEvents(); + var segs = this.eventsToSegs(events); + + this.renderBusinessSegs(segs); + }, + + + unrenderBusinessHours: function() { + this.unrenderBusinessSegs(); + }, + + + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ + + + getNowIndicatorUnit: function() { + return 'minute'; // will refresh on the minute + }, + + + renderNowIndicator: function(date) { + // seg system might be overkill, but it handles scenario where line needs to be rendered + // more than once because of columns with the same date (resources columns for example) + var segs = this.spanToSegs({ start: date, end: date }); + var top = this.computeDateTop(date, date); + var nodes = []; + var i; + + // render lines within the columns + for (i = 0; i < segs.length; i++) { + nodes.push($('
    ') + .css('top', top) + .appendTo(this.colContainerEls.eq(segs[i].col))[0]); + } + + // render an arrow over the axis + if (segs.length > 0) { // is the current time in view? + nodes.push($('
    ') + .css('top', top) + .appendTo(this.el.find('.fc-content-skeleton'))[0]); + } + + this.nowIndicatorEls = $(nodes); + }, + + + unrenderNowIndicator: function() { + if (this.nowIndicatorEls) { + this.nowIndicatorEls.remove(); + this.nowIndicatorEls = null; + } + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. + renderSelection: function(span) { + if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered + + // normally acceps an eventLocation, span has a start/end, which is good enough + this.renderEventLocationHelper(span); + } + else { + this.renderHighlight(span); + } + }, + + + // Unrenders any visual indication of a selection + unrenderSelection: function() { + this.unrenderHelper(); + this.unrenderHighlight(); + }, + + + /* Highlight + ------------------------------------------------------------------------------------------------------------------*/ + + + renderHighlight: function(span) { + this.renderHighlightSegs(this.spanToSegs(span)); + }, + + + unrenderHighlight: function() { + this.unrenderHighlightSegs(); + } + +}); + +;; + +/* Methods for rendering SEGMENTS, pieces of content that live on the view + ( this file is no longer just for events ) +----------------------------------------------------------------------------------------------------------------------*/ + +TimeGrid.mixin({ + + colContainerEls: null, // containers for each column + + // inner-containers for each column where different types of segs live + fgContainerEls: null, + bgContainerEls: null, + helperContainerEls: null, + highlightContainerEls: null, + businessContainerEls: null, + + // arrays of different types of displayed segments + fgSegs: null, + bgSegs: null, + helperSegs: null, + highlightSegs: null, + businessSegs: null, + + + // Renders the DOM that the view's content will live in + renderContentSkeleton: function() { + var cellHtml = ''; + var i; + var skeletonEl; + + for (i = 0; i < this.colCnt; i++) { + cellHtml += + '' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + ''; + } + + skeletonEl = $( + '
    ' + + '' + + '' + cellHtml + '' + + '
    ' + + '
    ' + ); + + this.colContainerEls = skeletonEl.find('.fc-content-col'); + this.helperContainerEls = skeletonEl.find('.fc-helper-container'); + this.fgContainerEls = skeletonEl.find('.fc-event-container:not(.fc-helper-container)'); + this.bgContainerEls = skeletonEl.find('.fc-bgevent-container'); + this.highlightContainerEls = skeletonEl.find('.fc-highlight-container'); + this.businessContainerEls = skeletonEl.find('.fc-business-container'); + + this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level + this.el.append(skeletonEl); + }, + + + /* Foreground Events + ------------------------------------------------------------------------------------------------------------------*/ + + + renderFgSegs: function(segs) { + segs = this.renderFgSegsIntoContainers(segs, this.fgContainerEls); + this.fgSegs = segs; + return segs; // needed for Grid::renderEvents + }, + + + unrenderFgSegs: function() { + this.unrenderNamedSegs('fgSegs'); + }, + + + /* Foreground Helper Events + ------------------------------------------------------------------------------------------------------------------*/ + + + renderHelperSegs: function(segs, sourceSeg) { + var helperEls = []; + var i, seg; + var sourceEl; + + segs = this.renderFgSegsIntoContainers(segs, this.helperContainerEls); + + // Try to make the segment that is in the same row as sourceSeg look the same + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + if (sourceSeg && sourceSeg.col === seg.col) { + sourceEl = sourceSeg.el; + seg.el.css({ + left: sourceEl.css('left'), + right: sourceEl.css('right'), + 'margin-left': sourceEl.css('margin-left'), + 'margin-right': sourceEl.css('margin-right') + }); + } + helperEls.push(seg.el[0]); + } + + this.helperSegs = segs; + + return $(helperEls); // must return rendered helpers + }, + + + unrenderHelperSegs: function() { + this.unrenderNamedSegs('helperSegs'); + }, + + + /* Background Events + ------------------------------------------------------------------------------------------------------------------*/ + + + renderBgSegs: function(segs) { + segs = this.renderFillSegEls('bgEvent', segs); // TODO: old fill system + this.updateSegVerticals(segs); + this.attachSegsByCol(this.groupSegsByCol(segs), this.bgContainerEls); + this.bgSegs = segs; + return segs; // needed for Grid::renderEvents + }, + + + unrenderBgSegs: function() { + this.unrenderNamedSegs('bgSegs'); + }, + + + /* Highlight + ------------------------------------------------------------------------------------------------------------------*/ + + + renderHighlightSegs: function(segs) { + segs = this.renderFillSegEls('highlight', segs); // TODO: old fill system + this.updateSegVerticals(segs); + this.attachSegsByCol(this.groupSegsByCol(segs), this.highlightContainerEls); + this.highlightSegs = segs; + }, + + + unrenderHighlightSegs: function() { + this.unrenderNamedSegs('highlightSegs'); + }, + + + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ + + + renderBusinessSegs: function(segs) { + segs = this.renderFillSegEls('businessHours', segs); // TODO: old fill system + this.updateSegVerticals(segs); + this.attachSegsByCol(this.groupSegsByCol(segs), this.businessContainerEls); + this.businessSegs = segs; + }, + + + unrenderBusinessSegs: function() { + this.unrenderNamedSegs('businessSegs'); + }, + + + /* Seg Rendering Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col + groupSegsByCol: function(segs) { + var segsByCol = []; + var i; + + for (i = 0; i < this.colCnt; i++) { + segsByCol.push([]); + } + + for (i = 0; i < segs.length; i++) { + segsByCol[segs[i].col].push(segs[i]); + } + + return segsByCol; + }, + + + // Given segments grouped by column, insert the segments' elements into a parallel array of container + // elements, each living within a column. + attachSegsByCol: function(segsByCol, containerEls) { + var col; + var segs; + var i; + + for (col = 0; col < this.colCnt; col++) { // iterate each column grouping + segs = segsByCol[col]; + + for (i = 0; i < segs.length; i++) { + containerEls.eq(col).append(segs[i].el); + } + } + }, + + + // Given the name of a property of `this` object, assumed to be an array of segments, + // loops through each segment and removes from DOM. Will null-out the property afterwards. + unrenderNamedSegs: function(propName) { + var segs = this[propName]; + var i; + + if (segs) { + for (i = 0; i < segs.length; i++) { + segs[i].el.remove(); + } + this[propName] = null; + } + }, + + + + /* Foreground Event Rendering Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Given an array of foreground segments, render a DOM element for each, computes position, + // and attaches to the column inner-container elements. + renderFgSegsIntoContainers: function(segs, containerEls) { + var segsByCol; + var col; + + segs = this.renderFgSegEls(segs); // will call fgSegHtml + segsByCol = this.groupSegsByCol(segs); + + for (col = 0; col < this.colCnt; col++) { + this.updateFgSegCoords(segsByCol[col]); + } + + this.attachSegsByCol(segsByCol, containerEls); + + return segs; + }, + + + // Renders the HTML for a single event segment's default rendering + fgSegHtml: function(seg, disableResizing) { + var view = this.view; + var event = seg.event; + var isDraggable = view.isEventDraggable(event); + var isResizableFromStart = !disableResizing && seg.isStart && view.isEventResizableFromStart(event); + var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventResizableFromEnd(event); + var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); + var skinCss = cssToStr(this.getSegSkinCss(seg)); + var timeText; + var fullTimeText; // more verbose time text. for the print stylesheet + var startTimeText; // just the start time text + + classes.unshift('fc-time-grid-event', 'fc-v-event'); + + if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day... + // Don't display time text on segments that run entirely through a day. + // That would appear as midnight-midnight and would look dumb. + // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am) + if (seg.isStart || seg.isEnd) { + timeText = this.getEventTimeText(seg); + fullTimeText = this.getEventTimeText(seg, 'LT'); + startTimeText = this.getEventTimeText(seg, null, false); // displayEnd=false + } + } else { + // Display the normal time text for the *event's* times + timeText = this.getEventTimeText(event); + fullTimeText = this.getEventTimeText(event, 'LT'); + startTimeText = this.getEventTimeText(event, null, false); // displayEnd=false + } + + return '
    ' + + '
    ' + + (timeText ? + '
    ' + + '' + htmlEscape(timeText) + '' + + '
    ' : + '' + ) + + (event.title ? + '
    ' + + htmlEscape(event.title) + + '
    ' : + '' + ) + + '
    ' + + '
    ' + + /* TODO: write CSS for this + (isResizableFromStart ? + '
    ' : + '' + ) + + */ + (isResizableFromEnd ? + '
    ' : + '' + ) + + ''; + }, + + + /* Seg Position Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Refreshes the CSS top/bottom coordinates for each segment element. + // Works when called after initial render, after a window resize/zoom for example. + updateSegVerticals: function(segs) { + this.computeSegVerticals(segs); + this.assignSegVerticals(segs); + }, + + + // For each segment in an array, computes and assigns its top and bottom properties + computeSegVerticals: function(segs) { + var i, seg; + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.top = this.computeDateTop(seg.start, seg.start); + seg.bottom = this.computeDateTop(seg.end, seg.start); + } + }, + + + // Given segments that already have their top/bottom properties computed, applies those values to + // the segments' elements. + assignSegVerticals: function(segs) { + var i, seg; + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.el.css(this.generateSegVerticalCss(seg)); + } + }, + + + // Generates an object with CSS properties for the top/bottom coordinates of a segment element + generateSegVerticalCss: function(seg) { + return { + top: seg.top, + bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container + }; + }, + + + /* Foreground Event Positioning Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Given segments that are assumed to all live in the *same column*, + // compute their verical/horizontal coordinates and assign to their elements. + updateFgSegCoords: function(segs) { + this.computeSegVerticals(segs); // horizontals relies on this + this.computeFgSegHorizontals(segs); // compute horizontal coordinates, z-index's, and reorder the array + this.assignSegVerticals(segs); + this.assignFgSegHorizontals(segs); + }, + + + // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. + // NOTE: Also reorders the given array by date! + computeFgSegHorizontals: function(segs) { + var levels; + var level0; + var i; + + this.sortEventSegs(segs); // order by certain criteria + levels = buildSlotSegLevels(segs); + computeForwardSlotSegs(levels); + + if ((level0 = levels[0])) { + + for (i = 0; i < level0.length; i++) { + computeSlotSegPressures(level0[i]); + } + + for (i = 0; i < level0.length; i++) { + this.computeFgSegForwardBack(level0[i], 0, 0); + } + } + }, + + + // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range + // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and + // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left. + // + // The segment might be part of a "series", which means consecutive segments with the same pressure + // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of + // segments behind this one in the current series, and `seriesBackwardCoord` is the starting + // coordinate of the first segment in the series. + computeFgSegForwardBack: function(seg, seriesBackwardPressure, seriesBackwardCoord) { + var forwardSegs = seg.forwardSegs; + var i; + + if (seg.forwardCoord === undefined) { // not already computed + + if (!forwardSegs.length) { + + // if there are no forward segments, this segment should butt up against the edge + seg.forwardCoord = 1; + } + else { + + // sort highest pressure first + this.sortForwardSegs(forwardSegs); + + // this segment's forwardCoord will be calculated from the backwardCoord of the + // highest-pressure forward segment. + this.computeFgSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord); + seg.forwardCoord = forwardSegs[0].backwardCoord; + } + + // calculate the backwardCoord from the forwardCoord. consider the series + seg.backwardCoord = seg.forwardCoord - + (seg.forwardCoord - seriesBackwardCoord) / // available width for series + (seriesBackwardPressure + 1); // # of segments in the series + + // use this segment's coordinates to computed the coordinates of the less-pressurized + // forward segments + for (i=0; i seg2.top && seg1.top < seg2.bottom; +} + +;; + +/* An abstract class from which other views inherit from +----------------------------------------------------------------------------------------------------------------------*/ + +var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, { + + type: null, // subclass' view name (string) + name: null, // deprecated. use `type` instead + title: null, // the text that will be displayed in the header's title + + calendar: null, // owner Calendar object + options: null, // hash containing all options. already merged with view-specific-options + el: null, // the view's containing element. set by Calendar + + displaying: null, // a promise representing the state of rendering. null if no render requested + isSkeletonRendered: false, + isEventsRendered: false, + + // range the view is actually displaying (moments) + start: null, + end: null, // exclusive + + // range the view is formally responsible for (moments) + // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates + intervalStart: null, + intervalEnd: null, // exclusive + intervalDuration: null, + intervalUnit: null, // name of largest unit being displayed, like "month" or "week" + + isRTL: false, + isSelected: false, // boolean whether a range of time is user-selected or not + selectedEvent: null, + + eventOrderSpecs: null, // criteria for ordering events when they have same date/time + + // classNames styled by jqui themes + widgetHeaderClass: null, + widgetContentClass: null, + highlightStateClass: null, + + // for date utils, computed from options + nextDayThreshold: null, + isHiddenDayHash: null, + + // now indicator + isNowIndicatorRendered: null, + initialNowDate: null, // result first getNow call + initialNowQueriedMs: null, // ms time the getNow was called + nowIndicatorTimeoutID: null, // for refresh timing of now indicator + nowIndicatorIntervalID: null, // " + + + constructor: function(calendar, type, options, intervalDuration) { + + this.calendar = calendar; + this.type = this.name = type; // .name is deprecated + this.options = options; + this.intervalDuration = intervalDuration || moment.duration(1, 'day'); + + this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold')); + this.initThemingProps(); + this.initHiddenDays(); + this.isRTL = this.opt('isRTL'); + + this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder')); + + this.initialize(); + }, + + + // A good place for subclasses to initialize member variables + initialize: function() { + // subclasses can implement + }, + + + // Retrieves an option with the given name + opt: function(name) { + return this.options[name]; + }, + + + // Triggers handlers that are view-related. Modifies args before passing to calendar. + trigger: function(name, thisObj) { // arguments beyond thisObj are passed along + var calendar = this.calendar; + + return calendar.trigger.apply( + calendar, + [name, thisObj || this].concat( + Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj + [ this ] // always make the last argument a reference to the view. TODO: deprecate + ) + ); + }, + + + /* Dates + ------------------------------------------------------------------------------------------------------------------*/ + + + // Updates all internal dates to center around the given current unzoned date. + setDate: function(date) { + this.setRange(this.computeRange(date)); + }, + + + // Updates all internal dates for displaying the given unzoned range. + setRange: function(range) { + $.extend(this, range); // assigns every property to this object's member variables + this.updateTitle(); + }, + + + // Given a single current unzoned date, produce information about what range to display. + // Subclasses can override. Must return all properties. + computeRange: function(date) { + var intervalUnit = computeIntervalUnit(this.intervalDuration); + var intervalStart = date.clone().startOf(intervalUnit); + var intervalEnd = intervalStart.clone().add(this.intervalDuration); + var start, end; + + // normalize the range's time-ambiguity + if (/year|month|week|day/.test(intervalUnit)) { // whole-days? + intervalStart.stripTime(); + intervalEnd.stripTime(); + } + else { // needs to have a time? + if (!intervalStart.hasTime()) { + intervalStart = this.calendar.time(0); // give 00:00 time + } + if (!intervalEnd.hasTime()) { + intervalEnd = this.calendar.time(0); // give 00:00 time + } + } + + start = intervalStart.clone(); + start = this.skipHiddenDays(start); + end = intervalEnd.clone(); + end = this.skipHiddenDays(end, -1, true); // exclusively move backwards + + return { + intervalUnit: intervalUnit, + intervalStart: intervalStart, + intervalEnd: intervalEnd, + start: start, + end: end + }; + }, + + + // Computes the new date when the user hits the prev button, given the current date + computePrevDate: function(date) { + return this.massageCurrentDate( + date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration), -1 + ); + }, + + + // Computes the new date when the user hits the next button, given the current date + computeNextDate: function(date) { + return this.massageCurrentDate( + date.clone().startOf(this.intervalUnit).add(this.intervalDuration) + ); + }, + + + // Given an arbitrarily calculated current date of the calendar, returns a date that is ensured to be completely + // visible. `direction` is optional and indicates which direction the current date was being + // incremented or decremented (1 or -1). + massageCurrentDate: function(date, direction) { + if (this.intervalDuration.as('days') <= 1) { // if the view displays a single day or smaller + if (this.isHiddenDay(date)) { + date = this.skipHiddenDays(date, direction); + date.startOf('day'); + } + } + + return date; + }, + + + /* Title and Date Formatting + ------------------------------------------------------------------------------------------------------------------*/ + + + // Sets the view's title property to the most updated computed value + updateTitle: function() { + this.title = this.computeTitle(); + }, + + + // Computes what the title at the top of the calendar should be for this view + computeTitle: function() { + return this.formatRange( + { + // in case intervalStart/End has a time, make sure timezone is correct + start: this.calendar.applyTimezone(this.intervalStart), + end: this.calendar.applyTimezone(this.intervalEnd) + }, + this.opt('titleFormat') || this.computeTitleFormat(), + this.opt('titleRangeSeparator') + ); + }, + + + // Generates the format string that should be used to generate the title for the current date range. + // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`. + computeTitleFormat: function() { + if (this.intervalUnit == 'year') { + return 'YYYY'; + } + else if (this.intervalUnit == 'month') { + return this.opt('monthYearFormat'); // like "September 2014" + } + else if (this.intervalDuration.as('days') > 1) { + return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014" + } + else { + return 'LL'; // one day. longer, like "September 9 2014" + } + }, + + + // Utility for formatting a range. Accepts a range object, formatting string, and optional separator. + // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account. + // The timezones of the dates within `range` will be respected. + formatRange: function(range, formatStr, separator) { + var end = range.end; + + if (!end.hasTime()) { // all-day? + end = end.clone().subtract(1); // convert to inclusive. last ms of previous day + } + + return formatRange(range.start, end, formatStr, separator, this.opt('isRTL')); + }, + + + /* Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + // Sets the container element that the view should render inside of. + // Does other DOM-related initializations. + setElement: function(el) { + this.el = el; + this.bindGlobalHandlers(); + }, + + + // Removes the view's container element from the DOM, clearing any content beforehand. + // Undoes any other DOM-related attachments. + removeElement: function() { + this.clear(); // clears all content + + // clean up the skeleton + if (this.isSkeletonRendered) { + this.unrenderSkeleton(); + this.isSkeletonRendered = false; + } + + this.unbindGlobalHandlers(); + + this.el.remove(); + + // NOTE: don't null-out this.el in case the View was destroyed within an API callback. + // We don't null-out the View's other jQuery element references upon destroy, + // so we shouldn't kill this.el either. + }, + + + // Does everything necessary to display the view centered around the given unzoned date. + // Does every type of rendering EXCEPT rendering events. + // Is asychronous and returns a promise. + display: function(date, explicitScrollState) { + var _this = this; + var prevScrollState = null; + + if (explicitScrollState != null && this.displaying) { // don't need prevScrollState if explicitScrollState + prevScrollState = this.queryScroll(); + } + + this.calendar.freezeContentHeight(); + + return syncThen(this.clear(), function() { // clear the content first + return ( + _this.displaying = + syncThen(_this.displayView(date), function() { // displayView might return a promise + + // caller of display() wants a specific scroll state? + if (explicitScrollState != null) { + // we make an assumption that this is NOT the initial render, + // and thus don't need forceScroll (is inconveniently asynchronous) + _this.setScroll(explicitScrollState); + } + else { + _this.forceScroll(_this.computeInitialScroll(prevScrollState)); + } + + _this.calendar.unfreezeContentHeight(); + _this.triggerRender(); + }) + ); + }); + }, + + + // Does everything necessary to clear the content of the view. + // Clears dates and events. Does not clear the skeleton. + // Is asychronous and returns a promise. + clear: function() { + var _this = this; + var displaying = this.displaying; + + if (displaying) { // previously displayed, or in the process of being displayed? + return syncThen(displaying, function() { // wait for the display to finish + _this.displaying = null; + _this.clearEvents(); + return _this.clearView(); // might return a promise. chain it + }); + } + else { + return $.when(); // an immediately-resolved promise + } + }, + + + // Displays the view's non-event content, such as date-related content or anything required by events. + // Renders the view's non-content skeleton if necessary. + // Can be asynchronous and return a promise. + displayView: function(date) { + if (!this.isSkeletonRendered) { + this.renderSkeleton(); + this.isSkeletonRendered = true; + } + if (date) { + this.setDate(date); + } + if (this.render) { + this.render(); // TODO: deprecate + } + this.renderDates(); + this.updateSize(); + this.renderBusinessHours(); // might need coordinates, so should go after updateSize() + this.startNowIndicator(); + }, + + + // Unrenders the view content that was rendered in displayView. + // Can be asynchronous and return a promise. + clearView: function() { + this.unselect(); + this.stopNowIndicator(); + this.triggerUnrender(); + this.unrenderBusinessHours(); + this.unrenderDates(); + if (this.destroy) { + this.destroy(); // TODO: deprecate + } + }, + + + // Renders the basic structure of the view before any content is rendered + renderSkeleton: function() { + // subclasses should implement + }, + + + // Unrenders the basic structure of the view + unrenderSkeleton: function() { + // subclasses should implement + }, + + + // Renders the view's date-related content. + // Assumes setRange has already been called and the skeleton has already been rendered. + renderDates: function() { + // subclasses should implement + }, + + + // Unrenders the view's date-related content + unrenderDates: function() { + // subclasses should override + }, + + + // Signals that the view's content has been rendered + triggerRender: function() { + this.trigger('viewRender', this, this, this.el); + }, + + + // Signals that the view's content is about to be unrendered + triggerUnrender: function() { + this.trigger('viewDestroy', this, this, this.el); + }, + + + // Binds DOM handlers to elements that reside outside the view container, such as the document + bindGlobalHandlers: function() { + this.listenTo($(document), 'mousedown', this.handleDocumentMousedown); + this.listenTo($(document), 'touchstart', this.processUnselect); + }, + + + // Unbinds DOM handlers from elements that reside outside the view container + unbindGlobalHandlers: function() { + this.stopListeningTo($(document)); + }, + + + // Initializes internal variables related to theming + initThemingProps: function() { + var tm = this.opt('theme') ? 'ui' : 'fc'; + + this.widgetHeaderClass = tm + '-widget-header'; + this.widgetContentClass = tm + '-widget-content'; + this.highlightStateClass = tm + '-state-highlight'; + }, + + + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders business-hours onto the view. Assumes updateSize has already been called. + renderBusinessHours: function() { + // subclasses should implement + }, + + + // Unrenders previously-rendered business-hours + unrenderBusinessHours: function() { + // subclasses should implement + }, + + + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ + + + // Immediately render the current time indicator and begins re-rendering it at an interval, + // which is defined by this.getNowIndicatorUnit(). + // TODO: somehow do this for the current whole day's background too + startNowIndicator: function() { + var _this = this; + var unit; + var update; + var delay; // ms wait value + + if (this.opt('nowIndicator')) { + unit = this.getNowIndicatorUnit(); + if (unit) { + update = proxy(this, 'updateNowIndicator'); // bind to `this` + + this.initialNowDate = this.calendar.getNow(); + this.initialNowQueriedMs = +new Date(); + this.renderNowIndicator(this.initialNowDate); + this.isNowIndicatorRendered = true; + + // wait until the beginning of the next interval + delay = this.initialNowDate.clone().startOf(unit).add(1, unit) - this.initialNowDate; + this.nowIndicatorTimeoutID = setTimeout(function() { + _this.nowIndicatorTimeoutID = null; + update(); + delay = +moment.duration(1, unit); + delay = Math.max(100, delay); // prevent too frequent + _this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval + }, delay); + } + } + }, + + + // rerenders the now indicator, computing the new current time from the amount of time that has passed + // since the initial getNow call. + updateNowIndicator: function() { + if (this.isNowIndicatorRendered) { + this.unrenderNowIndicator(); + this.renderNowIndicator( + this.initialNowDate.clone().add(new Date() - this.initialNowQueriedMs) // add ms + ); + } + }, + + + // Immediately unrenders the view's current time indicator and stops any re-rendering timers. + // Won't cause side effects if indicator isn't rendered. + stopNowIndicator: function() { + if (this.isNowIndicatorRendered) { + + if (this.nowIndicatorTimeoutID) { + clearTimeout(this.nowIndicatorTimeoutID); + this.nowIndicatorTimeoutID = null; + } + if (this.nowIndicatorIntervalID) { + clearTimeout(this.nowIndicatorIntervalID); + this.nowIndicatorIntervalID = null; + } + + this.unrenderNowIndicator(); + this.isNowIndicatorRendered = false; + } + }, + + + // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator + // should be refreshed. If something falsy is returned, no time indicator is rendered at all. + getNowIndicatorUnit: function() { + // subclasses should implement + }, + + + // Renders a current time indicator at the given datetime + renderNowIndicator: function(date) { + // subclasses should implement + }, + + + // Undoes the rendering actions from renderNowIndicator + unrenderNowIndicator: function() { + // subclasses should implement + }, + + + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + + + // Refreshes anything dependant upon sizing of the container element of the grid + updateSize: function(isResize) { + var scrollState; + + if (isResize) { + scrollState = this.queryScroll(); + } + + this.updateHeight(isResize); + this.updateWidth(isResize); + this.updateNowIndicator(); + + if (isResize) { + this.setScroll(scrollState); + } + }, + + + // Refreshes the horizontal dimensions of the calendar + updateWidth: function(isResize) { + // subclasses should implement + }, + + + // Refreshes the vertical dimensions of the calendar + updateHeight: function(isResize) { + var calendar = this.calendar; // we poll the calendar for height information + + this.setHeight( + calendar.getSuggestedViewHeight(), + calendar.isHeightAuto() + ); + }, + + + // Updates the vertical dimensions of the calendar to the specified height. + // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height. + setHeight: function(height, isAuto) { + // subclasses should implement + }, + + + /* Scroller + ------------------------------------------------------------------------------------------------------------------*/ + + + // Computes the initial pre-configured scroll state prior to allowing the user to change it. + // Given the scroll state from the previous rendering. If first time rendering, given null. + computeInitialScroll: function(previousScrollState) { + return 0; + }, + + + // Retrieves the view's current natural scroll state. Can return an arbitrary format. + queryScroll: function() { + // subclasses must implement + }, + + + // Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce. + setScroll: function(scrollState) { + // subclasses must implement + }, + + + // Sets the scroll state, making sure to overcome any predefined scroll value the browser has in mind + forceScroll: function(scrollState) { + var _this = this; + + this.setScroll(scrollState); + setTimeout(function() { + _this.setScroll(scrollState); + }, 0); + }, + + + /* Event Elements / Segments + ------------------------------------------------------------------------------------------------------------------*/ + + + // Does everything necessary to display the given events onto the current view + displayEvents: function(events) { + var scrollState = this.queryScroll(); + + this.clearEvents(); + this.renderEvents(events); + this.isEventsRendered = true; + this.setScroll(scrollState); + this.triggerEventRender(); + }, + + + // Does everything necessary to clear the view's currently-rendered events + clearEvents: function() { + var scrollState; + + if (this.isEventsRendered) { + + // TODO: optimize: if we know this is part of a displayEvents call, don't queryScroll/setScroll + scrollState = this.queryScroll(); + + this.triggerEventUnrender(); + if (this.destroyEvents) { + this.destroyEvents(); // TODO: deprecate + } + this.unrenderEvents(); + this.setScroll(scrollState); + this.isEventsRendered = false; + } + }, + + + // Renders the events onto the view. + renderEvents: function(events) { + // subclasses should implement + }, + + + // Removes event elements from the view. + unrenderEvents: function() { + // subclasses should implement + }, + + + // Signals that all events have been rendered + triggerEventRender: function() { + this.renderedEventSegEach(function(seg) { + this.trigger('eventAfterRender', seg.event, seg.event, seg.el); + }); + this.trigger('eventAfterAllRender'); + }, + + + // Signals that all event elements are about to be removed + triggerEventUnrender: function() { + this.renderedEventSegEach(function(seg) { + this.trigger('eventDestroy', seg.event, seg.event, seg.el); + }); + }, + + + // Given an event and the default element used for rendering, returns the element that should actually be used. + // Basically runs events and elements through the eventRender hook. + resolveEventEl: function(event, el) { + var custom = this.trigger('eventRender', event, event, el); + + if (custom === false) { // means don't render at all + el = null; + } + else if (custom && custom !== true) { + el = $(custom); + } + + return el; + }, + + + // Hides all rendered event segments linked to the given event + showEvent: function(event) { + this.renderedEventSegEach(function(seg) { + seg.el.css('visibility', ''); + }, event); + }, + + + // Shows all rendered event segments linked to the given event + hideEvent: function(event) { + this.renderedEventSegEach(function(seg) { + seg.el.css('visibility', 'hidden'); + }, event); + }, + + + // Iterates through event segments that have been rendered (have an el). Goes through all by default. + // If the optional `event` argument is specified, only iterates through segments linked to that event. + // The `this` value of the callback function will be the view. + renderedEventSegEach: function(func, event) { + var segs = this.getEventSegs(); + var i; + + for (i = 0; i < segs.length; i++) { + if (!event || segs[i].event._id === event._id) { + if (segs[i].el) { + func.call(this, segs[i]); + } + } + } + }, + + + // Retrieves all the rendered segment objects for the view + getEventSegs: function() { + // subclasses must implement + return []; + }, + + + /* Event Drag-n-Drop + ------------------------------------------------------------------------------------------------------------------*/ + + + // Computes if the given event is allowed to be dragged by the user + isEventDraggable: function(event) { + var source = event.source || {}; + + return firstDefined( + event.startEditable, + source.startEditable, + this.opt('eventStartEditable'), + event.editable, + source.editable, + this.opt('editable') + ); + }, + + + // Must be called when an event in the view is dropped onto new location. + // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event. + reportEventDrop: function(event, dropLocation, largeUnit, el, ev) { + var calendar = this.calendar; + var mutateResult = calendar.mutateEvent(event, dropLocation, largeUnit); + var undoFunc = function() { + mutateResult.undo(); + calendar.reportEventChange(); + }; + + this.triggerEventDrop(event, mutateResult.dateDelta, undoFunc, el, ev); + calendar.reportEventChange(); // will rerender events + }, + + + // Triggers event-drop handlers that have subscribed via the API + triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) { + this.trigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy + }, + + + /* External Element Drag-n-Drop + ------------------------------------------------------------------------------------------------------------------*/ + + + // Must be called when an external element, via jQuery UI, has been dropped onto the calendar. + // `meta` is the parsed data that has been embedded into the dragging event. + // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event. + reportExternalDrop: function(meta, dropLocation, el, ev, ui) { + var eventProps = meta.eventProps; + var eventInput; + var event; + + // Try to build an event object and render it. TODO: decouple the two + if (eventProps) { + eventInput = $.extend({}, eventProps, dropLocation); + event = this.calendar.renderEvent(eventInput, meta.stick)[0]; // renderEvent returns an array + } + + this.triggerExternalDrop(event, dropLocation, el, ev, ui); + }, + + + // Triggers external-drop handlers that have subscribed via the API + triggerExternalDrop: function(event, dropLocation, el, ev, ui) { + + // trigger 'drop' regardless of whether element represents an event + this.trigger('drop', el[0], dropLocation.start, ev, ui); + + if (event) { + this.trigger('eventReceive', null, event); // signal an external event landed + } + }, + + + /* Drag-n-Drop Rendering (for both events and external elements) + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a event or external-element drag over the given drop zone. + // If an external-element, seg will be `null`. + // Must return elements used for any mock events. + renderDrag: function(dropLocation, seg) { + // subclasses must implement + }, + + + // Unrenders a visual indication of an event or external-element being dragged. + unrenderDrag: function() { + // subclasses must implement + }, + + + /* Event Resizing + ------------------------------------------------------------------------------------------------------------------*/ + + + // Computes if the given event is allowed to be resized from its starting edge + isEventResizableFromStart: function(event) { + return this.opt('eventResizableFromStart') && this.isEventResizable(event); + }, + + + // Computes if the given event is allowed to be resized from its ending edge + isEventResizableFromEnd: function(event) { + return this.isEventResizable(event); + }, + + + // Computes if the given event is allowed to be resized by the user at all + isEventResizable: function(event) { + var source = event.source || {}; + + return firstDefined( + event.durationEditable, + source.durationEditable, + this.opt('eventDurationEditable'), + event.editable, + source.editable, + this.opt('editable') + ); + }, + + + // Must be called when an event in the view has been resized to a new length + reportEventResize: function(event, resizeLocation, largeUnit, el, ev) { + var calendar = this.calendar; + var mutateResult = calendar.mutateEvent(event, resizeLocation, largeUnit); + var undoFunc = function() { + mutateResult.undo(); + calendar.reportEventChange(); + }; + + this.triggerEventResize(event, mutateResult.durationDelta, undoFunc, el, ev); + calendar.reportEventChange(); // will rerender events + }, + + + // Triggers event-resize handlers that have subscribed via the API + triggerEventResize: function(event, durationDelta, undoFunc, el, ev) { + this.trigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy + }, + + + /* Selection (time range) + ------------------------------------------------------------------------------------------------------------------*/ + + + // Selects a date span on the view. `start` and `end` are both Moments. + // `ev` is the native mouse event that begin the interaction. + select: function(span, ev) { + this.unselect(ev); + this.renderSelection(span); + this.reportSelection(span, ev); + }, + + + // Renders a visual indication of the selection + renderSelection: function(span) { + // subclasses should implement + }, + + + // Called when a new selection is made. Updates internal state and triggers handlers. + reportSelection: function(span, ev) { + this.isSelected = true; + this.triggerSelect(span, ev); + }, + + + // Triggers handlers to 'select' + triggerSelect: function(span, ev) { + this.trigger( + 'select', + null, + this.calendar.applyTimezone(span.start), // convert to calendar's tz for external API + this.calendar.applyTimezone(span.end), // " + ev + ); + }, + + + // Undoes a selection. updates in the internal state and triggers handlers. + // `ev` is the native mouse event that began the interaction. + unselect: function(ev) { + if (this.isSelected) { + this.isSelected = false; + if (this.destroySelection) { + this.destroySelection(); // TODO: deprecate + } + this.unrenderSelection(); + this.trigger('unselect', null, ev); + } + }, + + + // Unrenders a visual indication of selection + unrenderSelection: function() { + // subclasses should implement + }, + + + /* Event Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + selectEvent: function(event) { + if (!this.selectedEvent || this.selectedEvent !== event) { + this.unselectEvent(); + this.renderedEventSegEach(function(seg) { + seg.el.addClass('fc-selected'); + }, event); + this.selectedEvent = event; + } + }, + + + unselectEvent: function() { + if (this.selectedEvent) { + this.renderedEventSegEach(function(seg) { + seg.el.removeClass('fc-selected'); + }, this.selectedEvent); + this.selectedEvent = null; + } + }, + + + isEventSelected: function(event) { + // event references might change on refetchEvents(), while selectedEvent doesn't, + // so compare IDs + return this.selectedEvent && this.selectedEvent._id === event._id; + }, + + + /* Mouse / Touch Unselecting (time range & event unselection) + ------------------------------------------------------------------------------------------------------------------*/ + // TODO: move consistently to down/start or up/end? + // TODO: don't kill previous selection if touch scrolling + + + handleDocumentMousedown: function(ev) { + if (isPrimaryMouseButton(ev)) { + this.processUnselect(ev); + } + }, + + + processUnselect: function(ev) { + this.processRangeUnselect(ev); + this.processEventUnselect(ev); + }, + + + processRangeUnselect: function(ev) { + var ignore; + + // is there a time-range selection? + if (this.isSelected && this.opt('unselectAuto')) { + // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element + ignore = this.opt('unselectCancel'); + if (!ignore || !$(ev.target).closest(ignore).length) { + this.unselect(ev); + } + } + }, + + + processEventUnselect: function(ev) { + if (this.selectedEvent) { + if (!$(ev.target).closest('.fc-selected').length) { + this.unselectEvent(); + } + } + }, + + + /* Day Click + ------------------------------------------------------------------------------------------------------------------*/ + + + // Triggers handlers to 'dayClick' + // Span has start/end of the clicked area. Only the start is useful. + triggerDayClick: function(span, dayEl, ev) { + this.trigger( + 'dayClick', + dayEl, + this.calendar.applyTimezone(span.start), // convert to calendar's timezone for external API + ev + ); + }, + + + /* Date Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Initializes internal variables related to calculating hidden days-of-week + initHiddenDays: function() { + var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden + var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool) + var dayCnt = 0; + var i; + + if (this.opt('weekends') === false) { + hiddenDays.push(0, 6); // 0=sunday, 6=saturday + } + + for (i = 0; i < 7; i++) { + if ( + !(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1) + ) { + dayCnt++; + } + } + + if (!dayCnt) { + throw 'invalid hiddenDays'; // all days were hidden? bad. + } + + this.isHiddenDayHash = isHiddenDayHash; + }, + + + // Is the current day hidden? + // `day` is a day-of-week index (0-6), or a Moment + isHiddenDay: function(day) { + if (moment.isMoment(day)) { + day = day.day(); + } + return this.isHiddenDayHash[day]; + }, + + + // Incrementing the current day until it is no longer a hidden day, returning a copy. + // If the initial value of `date` is not a hidden day, don't do anything. + // Pass `isExclusive` as `true` if you are dealing with an end date. + // `inc` defaults to `1` (increment one day forward each time) + skipHiddenDays: function(date, inc, isExclusive) { + var out = date.clone(); + inc = inc || 1; + while ( + this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7] + ) { + out.add(inc, 'days'); + } + return out; + }, + + + // Returns the date range of the full days the given range visually appears to occupy. + // Returns a new range object. + computeDayRange: function(range) { + var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts + var end = range.end; + var endDay = null; + var endTimeMS; + + if (end) { + endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends + endTimeMS = +end.time(); // # of milliseconds into `endDay` + + // If the end time is actually inclusively part of the next day and is equal to or + // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`. + // Otherwise, leaving it as inclusive will cause it to exclude `endDay`. + if (endTimeMS && endTimeMS >= this.nextDayThreshold) { + endDay.add(1, 'days'); + } + } + + // If no end was specified, or if it is within `startDay` but not past nextDayThreshold, + // assign the default duration of one day. + if (!end || endDay <= startDay) { + endDay = startDay.clone().add(1, 'days'); + } + + return { start: startDay, end: endDay }; + }, + + + // Does the given event visually appear to occupy more than one day? + isMultiDayEvent: function(event) { + var range = this.computeDayRange(event); // event is range-ish + + return range.end.diff(range.start, 'days') > 1; + } + +}); + +;; + +/* +Embodies a div that has potential scrollbars +*/ +var Scroller = FC.Scroller = Class.extend({ + + el: null, // the guaranteed outer element + scrollEl: null, // the element with the scrollbars + overflowX: null, + overflowY: null, + + + constructor: function(options) { + options = options || {}; + this.overflowX = options.overflowX || options.overflow || 'auto'; + this.overflowY = options.overflowY || options.overflow || 'auto'; + }, + + + render: function() { + this.el = this.renderEl(); + this.applyOverflow(); + }, + + + renderEl: function() { + return (this.scrollEl = $('
    ')); + }, + + + // sets to natural height, unlocks overflow + clear: function() { + this.setHeight('auto'); + this.applyOverflow(); + }, + + + destroy: function() { + this.el.remove(); + }, + + + // Overflow + // ----------------------------------------------------------------------------------------------------------------- + + + applyOverflow: function() { + this.scrollEl.css({ + 'overflow-x': this.overflowX, + 'overflow-y': this.overflowY + }); + }, + + + // Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'. + // Useful for preserving scrollbar widths regardless of future resizes. + // Can pass in scrollbarWidths for optimization. + lockOverflow: function(scrollbarWidths) { + var overflowX = this.overflowX; + var overflowY = this.overflowY; + + scrollbarWidths = scrollbarWidths || this.getScrollbarWidths(); + + if (overflowX === 'auto') { + overflowX = ( + scrollbarWidths.top || scrollbarWidths.bottom || // horizontal scrollbars? + // OR scrolling pane with massless scrollbars? + this.scrollEl[0].scrollWidth - 1 > this.scrollEl[0].clientWidth + // subtract 1 because of IE off-by-one issue + ) ? 'scroll' : 'hidden'; + } + + if (overflowY === 'auto') { + overflowY = ( + scrollbarWidths.left || scrollbarWidths.right || // vertical scrollbars? + // OR scrolling pane with massless scrollbars? + this.scrollEl[0].scrollHeight - 1 > this.scrollEl[0].clientHeight + // subtract 1 because of IE off-by-one issue + ) ? 'scroll' : 'hidden'; + } + + this.scrollEl.css({ 'overflow-x': overflowX, 'overflow-y': overflowY }); + }, + + + // Getters / Setters + // ----------------------------------------------------------------------------------------------------------------- + + + setHeight: function(height) { + this.scrollEl.height(height); + }, + + + getScrollTop: function() { + return this.scrollEl.scrollTop(); + }, + + + setScrollTop: function(top) { + this.scrollEl.scrollTop(top); + }, + + + getClientWidth: function() { + return this.scrollEl[0].clientWidth; + }, + + + getClientHeight: function() { + return this.scrollEl[0].clientHeight; + }, + + + getScrollbarWidths: function() { + return getScrollbarWidths(this.scrollEl); + } + +}); + +;; + +var Calendar = FC.Calendar = Class.extend({ + + dirDefaults: null, // option defaults related to LTR or RTL + langDefaults: null, // option defaults related to current locale + overrides: null, // option overrides given to the fullCalendar constructor + dynamicOverrides: null, // options set with dynamic setter method. higher precedence than view overrides. + options: null, // all defaults combined with overrides + viewSpecCache: null, // cache of view definitions + view: null, // current View object + header: null, + loadingLevel: 0, // number of simultaneous loading tasks + + + // a lot of this class' OOP logic is scoped within this constructor function, + // but in the future, write individual methods on the prototype. + constructor: Calendar_constructor, + + + // Subclasses can override this for initialization logic after the constructor has been called + initialize: function() { + }, + + + // Computes the flattened options hash for the calendar and assigns to `this.options`. + // Assumes this.overrides and this.dynamicOverrides have already been initialized. + populateOptionsHash: function() { + var lang, langDefaults; + var isRTL, dirDefaults; + + lang = firstDefined( // explicit lang option given? + this.dynamicOverrides.lang, + this.overrides.lang + ); + langDefaults = langOptionHash[lang]; + if (!langDefaults) { // explicit lang option not given or invalid? + lang = Calendar.defaults.lang; + langDefaults = langOptionHash[lang] || {}; + } + + isRTL = firstDefined( // based on options computed so far, is direction RTL? + this.dynamicOverrides.isRTL, + this.overrides.isRTL, + langDefaults.isRTL, + Calendar.defaults.isRTL + ); + dirDefaults = isRTL ? Calendar.rtlDefaults : {}; + + this.dirDefaults = dirDefaults; + this.langDefaults = langDefaults; + this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence + Calendar.defaults, // global defaults + dirDefaults, + langDefaults, + this.overrides, + this.dynamicOverrides + ]); + populateInstanceComputableOptions(this.options); // fill in gaps with computed options + }, + + + // Gets information about how to create a view. Will use a cache. + getViewSpec: function(viewType) { + var cache = this.viewSpecCache; + + return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType)); + }, + + + // Given a duration singular unit, like "week" or "day", finds a matching view spec. + // Preference is given to views that have corresponding buttons. + getUnitViewSpec: function(unit) { + var viewTypes; + var i; + var spec; + + if ($.inArray(unit, intervalUnits) != -1) { + + // put views that have buttons first. there will be duplicates, but oh well + viewTypes = this.header.getViewsWithButtons(); + $.each(FC.views, function(viewType) { // all views + viewTypes.push(viewType); + }); + + for (i = 0; i < viewTypes.length; i++) { + spec = this.getViewSpec(viewTypes[i]); + if (spec) { + if (spec.singleUnit == unit) { + return spec; + } + } + } + } + }, + + + // Builds an object with information on how to create a given view + buildViewSpec: function(requestedViewType) { + var viewOverrides = this.overrides.views || {}; + var specChain = []; // for the view. lowest to highest priority + var defaultsChain = []; // for the view. lowest to highest priority + var overridesChain = []; // for the view. lowest to highest priority + var viewType = requestedViewType; + var spec; // for the view + var overrides; // for the view + var duration; + var unit; + + // iterate from the specific view definition to a more general one until we hit an actual View class + while (viewType) { + spec = fcViews[viewType]; + overrides = viewOverrides[viewType]; + viewType = null; // clear. might repopulate for another iteration + + if (typeof spec === 'function') { // TODO: deprecate + spec = { 'class': spec }; + } + + if (spec) { + specChain.unshift(spec); + defaultsChain.unshift(spec.defaults || {}); + duration = duration || spec.duration; + viewType = viewType || spec.type; + } + + if (overrides) { + overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level + duration = duration || overrides.duration; + viewType = viewType || overrides.type; + } + } + + spec = mergeProps(specChain); + spec.type = requestedViewType; + if (!spec['class']) { + return false; + } + + if (duration) { + duration = moment.duration(duration); + if (duration.valueOf()) { // valid? + spec.duration = duration; + unit = computeIntervalUnit(duration); + + // view is a single-unit duration, like "week" or "day" + // incorporate options for this. lowest priority + if (duration.as(unit) === 1) { + spec.singleUnit = unit; + overridesChain.unshift(viewOverrides[unit] || {}); + } + } + } + + spec.defaults = mergeOptions(defaultsChain); + spec.overrides = mergeOptions(overridesChain); + + this.buildViewSpecOptions(spec); + this.buildViewSpecButtonText(spec, requestedViewType); + + return spec; + }, + + + // Builds and assigns a view spec's options object from its already-assigned defaults and overrides + buildViewSpecOptions: function(spec) { + spec.options = mergeOptions([ // lowest to highest priority + Calendar.defaults, // global defaults + spec.defaults, // view's defaults (from ViewSubclass.defaults) + this.dirDefaults, + this.langDefaults, // locale and dir take precedence over view's defaults! + this.overrides, // calendar's overrides (options given to constructor) + spec.overrides, // view's overrides (view-specific options) + this.dynamicOverrides // dynamically set via setter. highest precedence + ]); + populateInstanceComputableOptions(spec.options); + }, + + + // Computes and assigns a view spec's buttonText-related options + buildViewSpecButtonText: function(spec, requestedViewType) { + + // given an options object with a possible `buttonText` hash, lookup the buttonText for the + // requested view, falling back to a generic unit entry like "week" or "day" + function queryButtonText(options) { + var buttonText = options.buttonText || {}; + return buttonText[requestedViewType] || + (spec.singleUnit ? buttonText[spec.singleUnit] : null); + } + + // highest to lowest priority + spec.buttonTextOverride = + queryButtonText(this.dynamicOverrides) || + queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence + spec.overrides.buttonText; // `buttonText` for view-specific options is a string + + // highest to lowest priority. mirrors buildViewSpecOptions + spec.buttonTextDefault = + queryButtonText(this.langDefaults) || + queryButtonText(this.dirDefaults) || + spec.defaults.buttonText || // a single string. from ViewSubclass.defaults + queryButtonText(Calendar.defaults) || + (spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days" + requestedViewType; // fall back to given view name + }, + + + // Given a view name for a custom view or a standard view, creates a ready-to-go View object + instantiateView: function(viewType) { + var spec = this.getViewSpec(viewType); + + return new spec['class'](this, viewType, spec.options, spec.duration); + }, + + + // Returns a boolean about whether the view is okay to instantiate at some point + isValidViewType: function(viewType) { + return Boolean(this.getViewSpec(viewType)); + }, + + + // Should be called when any type of async data fetching begins + pushLoading: function() { + if (!(this.loadingLevel++)) { + this.trigger('loading', null, true, this.view); + } + }, + + + // Should be called when any type of async data fetching completes + popLoading: function() { + if (!(--this.loadingLevel)) { + this.trigger('loading', null, false, this.view); + } + }, + + + // Given arguments to the select method in the API, returns a span (unzoned start/end and other info) + buildSelectSpan: function(zonedStartInput, zonedEndInput) { + var start = this.moment(zonedStartInput).stripZone(); + var end; + + if (zonedEndInput) { + end = this.moment(zonedEndInput).stripZone(); + } + else if (start.hasTime()) { + end = start.clone().add(this.defaultTimedEventDuration); + } + else { + end = start.clone().add(this.defaultAllDayEventDuration); + } + + return { start: start, end: end }; + } + +}); + + +Calendar.mixin(EmitterMixin); + + +function Calendar_constructor(element, overrides) { + var t = this; + + + // Exports + // ----------------------------------------------------------------------------------- + + t.render = render; + t.destroy = destroy; + t.refetchEvents = refetchEvents; + t.refetchEventSources = refetchEventSources; + t.reportEvents = reportEvents; + t.reportEventChange = reportEventChange; + t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method + t.changeView = renderView; // `renderView` will switch to another view + t.select = select; + t.unselect = unselect; + t.prev = prev; + t.next = next; + t.prevYear = prevYear; + t.nextYear = nextYear; + t.today = today; + t.gotoDate = gotoDate; + t.incrementDate = incrementDate; + t.zoomTo = zoomTo; + t.getDate = getDate; + t.getCalendar = getCalendar; + t.getView = getView; + t.option = option; // getter/setter method + t.trigger = trigger; + + + // Options + // ----------------------------------------------------------------------------------- + + t.dynamicOverrides = {}; + t.viewSpecCache = {}; + t.optionHandlers = {}; // for Calendar.options.js + + // convert legacy options into non-legacy ones. + // in the future, when this is removed, don't use `overrides` reference. make a copy. + t.overrides = massageOverrides(overrides || {}); + + t.populateOptionsHash(); // sets this.options + + + + // Language-data Internals + // ----------------------------------------------------------------------------------- + // Apply overrides to the current language's data + + var localeData; + + // Called immediately, and when any of the options change. + // Happens before any internal objects rebuild or rerender, because this is very core. + t.bindOptions([ + 'lang', 'monthNames', 'monthNamesShort', 'dayNames', 'dayNamesShort', 'firstDay', 'weekNumberCalculation' + ], function(lang, monthNames, monthNamesShort, dayNames, dayNamesShort, firstDay, weekNumberCalculation) { + + localeData = createObject( // make a cheap copy + getMomentLocaleData(lang) // will fall back to en + ); + + if (monthNames) { + localeData._months = monthNames; + } + if (monthNamesShort) { + localeData._monthsShort = monthNamesShort; + } + if (dayNames) { + localeData._weekdays = dayNames; + } + if (dayNamesShort) { + localeData._weekdaysShort = dayNamesShort; + } + if (firstDay != null) { + var _week = createObject(localeData._week); // _week: { dow: # } + _week.dow = firstDay; + localeData._week = _week; + } + + if (weekNumberCalculation === 'iso') { + weekNumberCalculation = 'ISO'; // normalize + } + if ( // whitelist certain kinds of input + weekNumberCalculation === 'ISO' || + weekNumberCalculation === 'local' || + typeof weekNumberCalculation === 'function' + ) { + localeData._fullCalendar_weekCalc = weekNumberCalculation; // moment-ext will know what to do with it + } + + // If the internal current date object already exists, move to new locale. + // We do NOT need to do this technique for event dates, because this happens when converting to "segments". + if (date) { + localizeMoment(date); // sets to localeData + } + }); + + + + // Calendar-specific Date Utilities + // ----------------------------------------------------------------------------------- + + + t.defaultAllDayEventDuration = moment.duration(t.options.defaultAllDayEventDuration); + t.defaultTimedEventDuration = moment.duration(t.options.defaultTimedEventDuration); + + + // Builds a moment using the settings of the current calendar: timezone and language. + // Accepts anything the vanilla moment() constructor accepts. + t.moment = function() { + var mom; + + if (t.options.timezone === 'local') { + mom = FC.moment.apply(null, arguments); + + // Force the moment to be local, because FC.moment doesn't guarantee it. + if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone + mom.local(); + } + } + else if (t.options.timezone === 'UTC') { + mom = FC.moment.utc.apply(null, arguments); // process as UTC + } + else { + mom = FC.moment.parseZone.apply(null, arguments); // let the input decide the zone + } + + localizeMoment(mom); + + return mom; + }; + + + // Updates the given moment's locale settings to the current calendar locale settings. + function localizeMoment(mom) { + if ('_locale' in mom) { // moment 2.8 and above + mom._locale = localeData; + } + else { // pre-moment-2.8 + mom._lang = localeData; + } + } + + + // Returns a boolean about whether or not the calendar knows how to calculate + // the timezone offset of arbitrary dates in the current timezone. + t.getIsAmbigTimezone = function() { + return t.options.timezone !== 'local' && t.options.timezone !== 'UTC'; + }; + + + // Returns a copy of the given date in the current timezone. Has no effect on dates without times. + t.applyTimezone = function(date) { + if (!date.hasTime()) { + return date.clone(); + } + + var zonedDate = t.moment(date.toArray()); + var timeAdjust = date.time() - zonedDate.time(); + var adjustedZonedDate; + + // Safari sometimes has problems with this coersion when near DST. Adjust if necessary. (bug #2396) + if (timeAdjust) { // is the time result different than expected? + adjustedZonedDate = zonedDate.clone().add(timeAdjust); // add milliseconds + if (date.time() - adjustedZonedDate.time() === 0) { // does it match perfectly now? + zonedDate = adjustedZonedDate; + } + } + + return zonedDate; + }; + + + // Returns a moment for the current date, as defined by the client's computer or from the `now` option. + // Will return an moment with an ambiguous timezone. + t.getNow = function() { + var now = t.options.now; + if (typeof now === 'function') { + now = now(); + } + return t.moment(now).stripZone(); + }; + + + // Get an event's normalized end date. If not present, calculate it from the defaults. + t.getEventEnd = function(event) { + if (event.end) { + return event.end.clone(); + } + else { + return t.getDefaultEventEnd(event.allDay, event.start); + } + }; + + + // Given an event's allDay status and start date, return what its fallback end date should be. + // TODO: rename to computeDefaultEventEnd + t.getDefaultEventEnd = function(allDay, zonedStart) { + var end = zonedStart.clone(); + + if (allDay) { + end.stripTime().add(t.defaultAllDayEventDuration); + } + else { + end.add(t.defaultTimedEventDuration); + } + + if (t.getIsAmbigTimezone()) { + end.stripZone(); // we don't know what the tzo should be + } + + return end; + }; + + + // Produces a human-readable string for the given duration. + // Side-effect: changes the locale of the given duration. + t.humanizeDuration = function(duration) { + return (duration.locale || duration.lang).call(duration, t.options.lang) // works moment-pre-2.8 + .humanize(); + }; + + + + // Imports + // ----------------------------------------------------------------------------------- + + + EventManager.call(t); + var isFetchNeeded = t.isFetchNeeded; + var fetchEvents = t.fetchEvents; + var fetchEventSources = t.fetchEventSources; + + + + // Locals + // ----------------------------------------------------------------------------------- + + + var _element = element[0]; + var header; + var content; + var tm; // for making theme classes + var currentView; // NOTE: keep this in sync with this.view + var viewsByType = {}; // holds all instantiated view instances, current or not + var suggestedViewHeight; + var windowResizeProxy; // wraps the windowResize function + var ignoreWindowResize = 0; + var events = []; + var date; // unzoned + + + + // Main Rendering + // ----------------------------------------------------------------------------------- + + + // compute the initial ambig-timezone date + if (t.options.defaultDate != null) { + date = t.moment(t.options.defaultDate).stripZone(); + } + else { + date = t.getNow(); // getNow already returns unzoned + } + + + function render() { + if (!content) { + initialRender(); + } + else if (elementVisible()) { + // mainly for the public API + calcSize(); + renderView(); + } + } + + + function initialRender() { + element.addClass('fc'); + + // called immediately, and upon option change + t.bindOption('theme', function(theme) { + tm = theme ? 'ui' : 'fc'; // affects a larger scope + element.toggleClass('ui-widget', theme); + element.toggleClass('fc-unthemed', !theme); + }); + + // called immediately, and upon option change. + // HACK: lang often affects isRTL, so we explicitly listen to that too. + t.bindOptions([ 'isRTL', 'lang' ], function(isRTL) { + element.toggleClass('fc-ltr', !isRTL); + element.toggleClass('fc-rtl', isRTL); + }); + + content = $("
    ").prependTo(element); + + header = t.header = new Header(t); + renderHeader(); + + renderView(t.options.defaultView); + + if (t.options.handleWindowResize) { + windowResizeProxy = debounce(windowResize, t.options.windowResizeDelay); // prevents rapid calls + $(window).resize(windowResizeProxy); + } + } + + + // can be called repeatedly and Header will rerender + function renderHeader() { + header.render(); + if (header.el) { + element.prepend(header.el); + } + } + + + function destroy() { + + if (currentView) { + currentView.removeElement(); + + // NOTE: don't null-out currentView/t.view in case API methods are called after destroy. + // It is still the "current" view, just not rendered. + } + + header.removeElement(); + content.remove(); + element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget'); + + if (windowResizeProxy) { + $(window).unbind('resize', windowResizeProxy); + } + } + + + function elementVisible() { + return element.is(':visible'); + } + + + + // View Rendering + // ----------------------------------------------------------------------------------- + + + // Renders a view because of a date change, view-type change, or for the first time. + // If not given a viewType, keep the current view but render different dates. + // Accepts an optional scroll state to restore to. + function renderView(viewType, explicitScrollState) { + ignoreWindowResize++; + + // if viewType is changing, remove the old view's rendering + if (currentView && viewType && currentView.type !== viewType) { + freezeContentHeight(); // prevent a scroll jump when view element is removed + clearView(); + } + + // if viewType changed, or the view was never created, create a fresh view + if (!currentView && viewType) { + currentView = t.view = + viewsByType[viewType] || + (viewsByType[viewType] = t.instantiateView(viewType)); + + currentView.setElement( + $("
    ").appendTo(content) + ); + header.activateButton(viewType); + } + + if (currentView) { + + // in case the view should render a period of time that is completely hidden + date = currentView.massageCurrentDate(date); + + // render or rerender the view + if ( + !currentView.displaying || + !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change + ) { + if (elementVisible()) { + + currentView.display(date, explicitScrollState); // will call freezeContentHeight + unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async + + // need to do this after View::render, so dates are calculated + updateHeaderTitle(); + updateTodayButton(); + + getAndRenderEvents(); + } + } + } + + unfreezeContentHeight(); // undo any lone freezeContentHeight calls + ignoreWindowResize--; + } + + + // Unrenders the current view and reflects this change in the Header. + // Unregsiters the `currentView`, but does not remove from viewByType hash. + function clearView() { + header.deactivateButton(currentView.type); + currentView.removeElement(); + currentView = t.view = null; + } + + + // Destroys the view, including the view object. Then, re-instantiates it and renders it. + // Maintains the same scroll state. + // TODO: maintain any other user-manipulated state. + function reinitView() { + ignoreWindowResize++; + freezeContentHeight(); + + var viewType = currentView.type; + var scrollState = currentView.queryScroll(); + clearView(); + renderView(viewType, scrollState); + + unfreezeContentHeight(); + ignoreWindowResize--; + } + + + + // Resizing + // ----------------------------------------------------------------------------------- + + + t.getSuggestedViewHeight = function() { + if (suggestedViewHeight === undefined) { + calcSize(); + } + return suggestedViewHeight; + }; + + + t.isHeightAuto = function() { + return t.options.contentHeight === 'auto' || t.options.height === 'auto'; + }; + + + function updateSize(shouldRecalc) { + if (elementVisible()) { + + if (shouldRecalc) { + _calcSize(); + } + + ignoreWindowResize++; + currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto() + ignoreWindowResize--; + + return true; // signal success + } + } + + + function calcSize() { + if (elementVisible()) { + _calcSize(); + } + } + + + function _calcSize() { // assumes elementVisible + if (typeof t.options.contentHeight === 'number') { // exists and not 'auto' + suggestedViewHeight = t.options.contentHeight; + } + else if (typeof t.options.height === 'number') { // exists and not 'auto' + suggestedViewHeight = t.options.height - (header.el ? header.el.outerHeight(true) : 0); + } + else { + suggestedViewHeight = Math.round(content.width() / Math.max(t.options.aspectRatio, .5)); + } + } + + + function windowResize(ev) { + if ( + !ignoreWindowResize && + ev.target === window && // so we don't process jqui "resize" events that have bubbled up + currentView.start // view has already been rendered + ) { + if (updateSize(true)) { + currentView.trigger('windowResize', _element); + } + } + } + + + + /* Event Fetching/Rendering + -----------------------------------------------------------------------------*/ + // TODO: going forward, most of this stuff should be directly handled by the view + + + function refetchEvents() { // can be called as an API method + fetchAndRenderEvents(); + } + + + // TODO: move this into EventManager? + function refetchEventSources(matchInputs) { + fetchEventSources(t.getEventSourcesByMatchArray(matchInputs)); + } + + + function renderEvents() { // destroys old events if previously rendered + if (elementVisible()) { + freezeContentHeight(); + currentView.displayEvents(events); + unfreezeContentHeight(); + } + } + + + function getAndRenderEvents() { + if (!t.options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) { + fetchAndRenderEvents(); + } + else { + renderEvents(); + } + } + + + function fetchAndRenderEvents() { + fetchEvents(currentView.start, currentView.end); + // ... will call reportEvents + // ... which will call renderEvents + } + + + // called when event data arrives + function reportEvents(_events) { + events = _events; + renderEvents(); + } + + + // called when a single event's data has been changed + function reportEventChange() { + renderEvents(); + } + + + + /* Header Updating + -----------------------------------------------------------------------------*/ + + + function updateHeaderTitle() { + header.updateTitle(currentView.title); + } + + + function updateTodayButton() { + var now = t.getNow(); + if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) { + header.disableButton('today'); + } + else { + header.enableButton('today'); + } + } + + + + /* Selection + -----------------------------------------------------------------------------*/ + + + // this public method receives start/end dates in any format, with any timezone + function select(zonedStartInput, zonedEndInput) { + currentView.select( + t.buildSelectSpan.apply(t, arguments) + ); + } + + + function unselect() { // safe to be called before renderView + if (currentView) { + currentView.unselect(); + } + } + + + + /* Date + -----------------------------------------------------------------------------*/ + + + function prev() { + date = currentView.computePrevDate(date); + renderView(); + } + + + function next() { + date = currentView.computeNextDate(date); + renderView(); + } + + + function prevYear() { + date.add(-1, 'years'); + renderView(); + } + + + function nextYear() { + date.add(1, 'years'); + renderView(); + } + + + function today() { + date = t.getNow(); + renderView(); + } + + + function gotoDate(zonedDateInput) { + date = t.moment(zonedDateInput).stripZone(); + renderView(); + } + + + function incrementDate(delta) { + date.add(moment.duration(delta)); + renderView(); + } + + + // Forces navigation to a view for the given date. + // `viewType` can be a specific view name or a generic one like "week" or "day". + function zoomTo(newDate, viewType) { + var spec; + + viewType = viewType || 'day'; // day is default zoom + spec = t.getViewSpec(viewType) || t.getUnitViewSpec(viewType); + + date = newDate.clone(); + renderView(spec ? spec.type : null); + } + + + // for external API + function getDate() { + return t.applyTimezone(date); // infuse the calendar's timezone + } + + + + /* Height "Freezing" + -----------------------------------------------------------------------------*/ + // TODO: move this into the view + + t.freezeContentHeight = freezeContentHeight; + t.unfreezeContentHeight = unfreezeContentHeight; + + + function freezeContentHeight() { + content.css({ + width: '100%', + height: content.height(), + overflow: 'hidden' + }); + } + + + function unfreezeContentHeight() { + content.css({ + width: '', + height: '', + overflow: '' + }); + } + + + + /* Misc + -----------------------------------------------------------------------------*/ + + + function getCalendar() { + return t; + } + + + function getView() { + return currentView; + } + + + function option(name, value) { + var newOptionHash; + + if (typeof name === 'string') { + if (value === undefined) { // getter + return t.options[name]; + } + else { // setter for individual option + newOptionHash = {}; + newOptionHash[name] = value; + setOptions(newOptionHash); + } + } + else if (typeof name === 'object') { // compound setter with object input + setOptions(name); + } + } + + + function setOptions(newOptionHash) { + var optionCnt = 0; + var optionName; + + for (optionName in newOptionHash) { + t.dynamicOverrides[optionName] = newOptionHash[optionName]; + } + + t.viewSpecCache = {}; // the dynamic override invalidates the options in this cache, so just clear it + t.populateOptionsHash(); // this.options needs to be recomputed after the dynamic override + + // trigger handlers after this.options has been updated + for (optionName in newOptionHash) { + t.triggerOptionHandlers(optionName); // recall bindOption/bindOptions + optionCnt++; + } + + // special-case handling of single option change. + // if only one option change, `optionName` will be its name. + if (optionCnt === 1) { + if (optionName === 'height' || optionName === 'contentHeight' || optionName === 'aspectRatio') { + updateSize(true); // true = allow recalculation of height + return; + } + else if (optionName === 'defaultDate') { + return; // can't change date this way. use gotoDate instead + } + else if (optionName === 'businessHours') { + if (currentView) { + currentView.unrenderBusinessHours(); + currentView.renderBusinessHours(); + } + return; + } + else if (optionName === 'timezone') { + t.rezoneArrayEventSources(); + refetchEvents(); + return; + } + } + + // catch-all. rerender the header and rebuild/rerender the current view + renderHeader(); + viewsByType = {}; // even non-current views will be affected by this option change. do before rerender + reinitView(); + } + + + function trigger(name, thisObj) { // overrides the Emitter's trigger method :( + var args = Array.prototype.slice.call(arguments, 2); + + thisObj = thisObj || _element; + this.triggerWith(name, thisObj, args); // Emitter's method + + if (t.options[name]) { + return t.options[name].apply(thisObj, args); + } + } + + t.initialize(); +} + +;; +/* +Options binding/triggering system. +*/ +Calendar.mixin({ + + // A map of option names to arrays of handler objects. Initialized to {} in Calendar. + // Format for a handler object: + // { + // func // callback function to be called upon change + // names // option names whose values should be given to func + // } + optionHandlers: null, + + // Calls handlerFunc immediately, and when the given option has changed. + // handlerFunc will be given the option value. + bindOption: function(optionName, handlerFunc) { + this.bindOptions([ optionName ], handlerFunc); + }, + + // Calls handlerFunc immediately, and when any of the given options change. + // handlerFunc will be given each option value as ordered function arguments. + bindOptions: function(optionNames, handlerFunc) { + var handlerObj = { func: handlerFunc, names: optionNames }; + var i; + + for (i = 0; i < optionNames.length; i++) { + this.registerOptionHandlerObj(optionNames[i], handlerObj); + } + + this.triggerOptionHandlerObj(handlerObj); + }, + + // Puts the given handler object into the internal hash + registerOptionHandlerObj: function(optionName, handlerObj) { + (this.optionHandlers[optionName] || (this.optionHandlers[optionName] = [])) + .push(handlerObj); + }, + + // Reports that the given option has changed, and calls all appropriate handlers. + triggerOptionHandlers: function(optionName) { + var handlerObjs = this.optionHandlers[optionName] || []; + var i; + + for (i = 0; i < handlerObjs.length; i++) { + this.triggerOptionHandlerObj(handlerObjs[i]); + } + }, + + // Calls the callback for a specific handler object, passing in the appropriate arguments. + triggerOptionHandlerObj: function(handlerObj) { + var optionNames = handlerObj.names; + var optionValues = []; + var i; + + for (i = 0; i < optionNames.length; i++) { + optionValues.push(this.options[optionNames[i]]); + } + + handlerObj.func.apply(this, optionValues); // maintain the Calendar's `this` context + } + +}); + +;; + +Calendar.defaults = { + + titleRangeSeparator: ' \u2013 ', // en dash + monthYearFormat: 'MMMM YYYY', // required for en. other languages rely on datepicker computable option + + defaultTimedEventDuration: '02:00:00', + defaultAllDayEventDuration: { days: 1 }, + forceEventDuration: false, + nextDayThreshold: '09:00:00', // 9am + + // display + defaultView: 'month', + aspectRatio: 1.35, + header: { + left: 'title', + center: '', + right: 'today prev,next' + }, + weekends: true, + weekNumbers: false, + + weekNumberTitle: 'W', + weekNumberCalculation: 'local', + + //editable: false, + + //nowIndicator: false, + + scrollTime: '06:00:00', + + // event ajax + lazyFetching: true, + startParam: 'start', + endParam: 'end', + timezoneParam: 'timezone', + + timezone: false, + + //allDayDefault: undefined, + + // locale + isRTL: false, + buttonText: { + prev: "prev", + next: "next", + prevYear: "prev year", + nextYear: "next year", + year: 'year', // TODO: locale files need to specify this + today: 'today', + month: 'month', + week: 'week', + day: 'day' + }, + + buttonIcons: { + prev: 'left-single-arrow', + next: 'right-single-arrow', + prevYear: 'left-double-arrow', + nextYear: 'right-double-arrow' + }, + + // jquery-ui theming + theme: false, + themeButtonIcons: { + prev: 'circle-triangle-w', + next: 'circle-triangle-e', + prevYear: 'seek-prev', + nextYear: 'seek-next' + }, + + //eventResizableFromStart: false, + dragOpacity: .75, + dragRevertDuration: 500, + dragScroll: true, + + //selectable: false, + unselectAuto: true, + + dropAccept: '*', + + eventOrder: 'title', + + eventLimit: false, + eventLimitText: 'more', + eventLimitClick: 'popover', + dayPopoverFormat: 'LL', + + handleWindowResize: true, + windowResizeDelay: 200, // milliseconds before an updateSize happens + + longPressDelay: 1000 + +}; + + +Calendar.englishDefaults = { // used by lang.js + dayPopoverFormat: 'dddd, MMMM D' +}; + + +Calendar.rtlDefaults = { // right-to-left defaults + header: { // TODO: smarter solution (first/center/last ?) + left: 'next,prev today', + center: '', + right: 'title' + }, + buttonIcons: { + prev: 'right-single-arrow', + next: 'left-single-arrow', + prevYear: 'right-double-arrow', + nextYear: 'left-double-arrow' + }, + themeButtonIcons: { + prev: 'circle-triangle-e', + next: 'circle-triangle-w', + nextYear: 'seek-prev', + prevYear: 'seek-next' + } +}; + +;; + +var langOptionHash = FC.langs = {}; // initialize and expose + + +// TODO: document the structure and ordering of a FullCalendar lang file +// TODO: rename everything "lang" to "locale", like what the moment project did + + +// Initialize jQuery UI datepicker translations while using some of the translations +// Will set this as the default language for datepicker. +FC.datepickerLang = function(langCode, dpLangCode, dpOptions) { + + // get the FullCalendar internal option hash for this language. create if necessary + var fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {}); + + // transfer some simple options from datepicker to fc + fcOptions.isRTL = dpOptions.isRTL; + fcOptions.weekNumberTitle = dpOptions.weekHeader; + + // compute some more complex options from datepicker + $.each(dpComputableOptions, function(name, func) { + fcOptions[name] = func(dpOptions); + }); + + // is jQuery UI Datepicker is on the page? + if ($.datepicker) { + + // Register the language data. + // FullCalendar and MomentJS use language codes like "pt-br" but Datepicker + // does it like "pt-BR" or if it doesn't have the language, maybe just "pt". + // Make an alias so the language can be referenced either way. + $.datepicker.regional[dpLangCode] = + $.datepicker.regional[langCode] = // alias + dpOptions; + + // Alias 'en' to the default language data. Do this every time. + $.datepicker.regional.en = $.datepicker.regional['']; + + // Set as Datepicker's global defaults. + $.datepicker.setDefaults(dpOptions); + } +}; + + +// Sets FullCalendar-specific translations. Will set the language as the global default. +FC.lang = function(langCode, newFcOptions) { + var fcOptions; + var momOptions; + + // get the FullCalendar internal option hash for this language. create if necessary + fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {}); + + // provided new options for this language? merge them in + if (newFcOptions) { + fcOptions = langOptionHash[langCode] = mergeOptions([ fcOptions, newFcOptions ]); + } + + // compute language options that weren't defined. + // always do this. newFcOptions can be undefined when initializing from i18n file, + // so no way to tell if this is an initialization or a default-setting. + momOptions = getMomentLocaleData(langCode); // will fall back to en + $.each(momComputableOptions, function(name, func) { + if (fcOptions[name] == null) { + fcOptions[name] = func(momOptions, fcOptions); + } + }); + + // set it as the default language for FullCalendar + Calendar.defaults.lang = langCode; +}; + + +// NOTE: can't guarantee any of these computations will run because not every language has datepicker +// configs, so make sure there are English fallbacks for these in the defaults file. +var dpComputableOptions = { + + buttonText: function(dpOptions) { + return { + // the translations sometimes wrongly contain HTML entities + prev: stripHtmlEntities(dpOptions.prevText), + next: stripHtmlEntities(dpOptions.nextText), + today: stripHtmlEntities(dpOptions.currentText) + }; + }, + + // Produces format strings like "MMMM YYYY" -> "September 2014" + monthYearFormat: function(dpOptions) { + return dpOptions.showMonthAfterYear ? + 'YYYY[' + dpOptions.yearSuffix + '] MMMM' : + 'MMMM YYYY[' + dpOptions.yearSuffix + ']'; + } + +}; + +var momComputableOptions = { + + // Produces format strings like "ddd M/D" -> "Fri 9/15" + dayOfMonthFormat: function(momOptions, fcOptions) { + var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY" + + // strip the year off the edge, as well as other misc non-whitespace chars + format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); + + if (fcOptions.isRTL) { + format += ' ddd'; // for RTL, add day-of-week to end + } + else { + format = 'ddd ' + format; // for LTR, add day-of-week to beginning + } + return format; + }, + + // Produces format strings like "h:mma" -> "6:00pm" + mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option + return momOptions.longDateFormat('LT') + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand + }, + + // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm" + smallTimeFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(':mm', '(:mm)') + .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand + }, + + // Produces format strings like "h(:mm)t" -> "6p" / "6:30p" + extraSmallTimeFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(':mm', '(:mm)') + .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs + .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand + }, + + // Produces format strings like "ha" / "H" -> "6pm" / "18" + hourFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(':mm', '') + .replace(/(\Wmm)$/, '') // like above, but for foreign langs + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand + }, + + // Produces format strings like "h:mm" -> "6:30" (with no AM/PM) + noMeridiemTimeFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(/\s*a$/i, ''); // remove trailing AM/PM + } + +}; + + +// options that should be computed off live calendar options (considers override options) +// TODO: best place for this? related to lang? +// TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it +var instanceComputableOptions = { + + // Produces format strings for results like "Mo 16" + smallDayDateFormat: function(options) { + return options.isRTL ? + 'D dd' : + 'dd D'; + }, + + // Produces format strings for results like "Wk 5" + weekFormat: function(options) { + return options.isRTL ? + 'w[ ' + options.weekNumberTitle + ']' : + '[' + options.weekNumberTitle + ' ]w'; + }, + + // Produces format strings for results like "Wk5" + smallWeekFormat: function(options) { + return options.isRTL ? + 'w[' + options.weekNumberTitle + ']' : + '[' + options.weekNumberTitle + ']w'; + } + +}; + +function populateInstanceComputableOptions(options) { + $.each(instanceComputableOptions, function(name, func) { + if (options[name] == null) { + options[name] = func(options); + } + }); +} + + +// Returns moment's internal locale data. If doesn't exist, returns English. +// Works with moment-pre-2.8 +function getMomentLocaleData(langCode) { + var func = moment.localeData || moment.langData; + return func.call(moment, langCode) || + func.call(moment, 'en'); // the newer localData could return null, so fall back to en +} + + +// Initialize English by forcing computation of moment-derived options. +// Also, sets it as the default. +FC.lang('en', Calendar.englishDefaults); + +;; + +/* Top toolbar area with buttons and title +----------------------------------------------------------------------------------------------------------------------*/ +// TODO: rename all header-related things to "toolbar" + +function Header(calendar) { + var t = this; + + // exports + t.render = render; + t.removeElement = removeElement; + t.updateTitle = updateTitle; + t.activateButton = activateButton; + t.deactivateButton = deactivateButton; + t.disableButton = disableButton; + t.enableButton = enableButton; + t.getViewsWithButtons = getViewsWithButtons; + t.el = null; // mirrors local `el` + + // locals + var el; + var viewsWithButtons = []; + var tm; + + + // can be called repeatedly and will rerender + function render() { + var options = calendar.options; + var sections = options.header; + + tm = options.theme ? 'ui' : 'fc'; + + if (sections) { + if (!el) { + el = this.el = $("
    "); + } + else { + el.empty(); + } + el.append(renderSection('left')) + .append(renderSection('right')) + .append(renderSection('center')) + .append('
    '); + } + else { + removeElement(); + } + } + + + function removeElement() { + if (el) { + el.remove(); + el = t.el = null; + } + } + + + function renderSection(position) { + var sectionEl = $('
    '); + var options = calendar.options; + var buttonStr = options.header[position]; + + if (buttonStr) { + $.each(buttonStr.split(' '), function(i) { + var groupChildren = $(); + var isOnlyButtons = true; + var groupEl; + + $.each(this.split(','), function(j, buttonName) { + var customButtonProps; + var viewSpec; + var buttonClick; + var overrideText; // text explicitly set by calendar's constructor options. overcomes icons + var defaultText; + var themeIcon; + var normalIcon; + var innerHtml; + var classes; + var button; // the element + + if (buttonName == 'title') { + groupChildren = groupChildren.add($('

     

    ')); // we always want it to take up height + isOnlyButtons = false; + } + else { + if ((customButtonProps = (options.customButtons || {})[buttonName])) { + buttonClick = function(ev) { + if (customButtonProps.click) { + customButtonProps.click.call(button[0], ev); + } + }; + overrideText = ''; // icons will override text + defaultText = customButtonProps.text; + } + else if ((viewSpec = calendar.getViewSpec(buttonName))) { + buttonClick = function() { + calendar.changeView(buttonName); + }; + viewsWithButtons.push(buttonName); + overrideText = viewSpec.buttonTextOverride; + defaultText = viewSpec.buttonTextDefault; + } + else if (calendar[buttonName]) { // a calendar method + buttonClick = function() { + calendar[buttonName](); + }; + overrideText = (calendar.overrides.buttonText || {})[buttonName]; + defaultText = options.buttonText[buttonName]; // everything else is considered default + } + + if (buttonClick) { + + themeIcon = + customButtonProps ? + customButtonProps.themeIcon : + options.themeButtonIcons[buttonName]; + + normalIcon = + customButtonProps ? + customButtonProps.icon : + options.buttonIcons[buttonName]; + + if (overrideText) { + innerHtml = htmlEscape(overrideText); + } + else if (themeIcon && options.theme) { + innerHtml = ""; + } + else if (normalIcon && !options.theme) { + innerHtml = ""; + } + else { + innerHtml = htmlEscape(defaultText); + } + + classes = [ + 'fc-' + buttonName + '-button', + tm + '-button', + tm + '-state-default' + ]; + + button = $( // type="button" so that it doesn't submit a form + '' + ) + .click(function(ev) { + // don't process clicks for disabled buttons + if (!button.hasClass(tm + '-state-disabled')) { + + buttonClick(ev); + + // after the click action, if the button becomes the "active" tab, or disabled, + // it should never have a hover class, so remove it now. + if ( + button.hasClass(tm + '-state-active') || + button.hasClass(tm + '-state-disabled') + ) { + button.removeClass(tm + '-state-hover'); + } + } + }) + .mousedown(function() { + // the *down* effect (mouse pressed in). + // only on buttons that are not the "active" tab, or disabled + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-down'); + }) + .mouseup(function() { + // undo the *down* effect + button.removeClass(tm + '-state-down'); + }) + .hover( + function() { + // the *hover* effect. + // only on buttons that are not the "active" tab, or disabled + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-hover'); + }, + function() { + // undo the *hover* effect + button + .removeClass(tm + '-state-hover') + .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup + } + ); + + groupChildren = groupChildren.add(button); + } + } + }); + + if (isOnlyButtons) { + groupChildren + .first().addClass(tm + '-corner-left').end() + .last().addClass(tm + '-corner-right').end(); + } + + if (groupChildren.length > 1) { + groupEl = $('
    '); + if (isOnlyButtons) { + groupEl.addClass('fc-button-group'); + } + groupEl.append(groupChildren); + sectionEl.append(groupEl); + } + else { + sectionEl.append(groupChildren); // 1 or 0 children + } + }); + } + + return sectionEl; + } + + + function updateTitle(text) { + if (el) { + el.find('h2').text(text); + } + } + + + function activateButton(buttonName) { + if (el) { + el.find('.fc-' + buttonName + '-button') + .addClass(tm + '-state-active'); + } + } + + + function deactivateButton(buttonName) { + if (el) { + el.find('.fc-' + buttonName + '-button') + .removeClass(tm + '-state-active'); + } + } + + + function disableButton(buttonName) { + if (el) { + el.find('.fc-' + buttonName + '-button') + .prop('disabled', true) + .addClass(tm + '-state-disabled'); + } + } + + + function enableButton(buttonName) { + if (el) { + el.find('.fc-' + buttonName + '-button') + .prop('disabled', false) + .removeClass(tm + '-state-disabled'); + } + } + + + function getViewsWithButtons() { + return viewsWithButtons; + } + +} + +;; + +FC.sourceNormalizers = []; +FC.sourceFetchers = []; + +var ajaxDefaults = { + dataType: 'json', + cache: false +}; + +var eventGUID = 1; + + +function EventManager() { // assumed to be a calendar + var t = this; + + + // exports + t.isFetchNeeded = isFetchNeeded; + t.fetchEvents = fetchEvents; + t.fetchEventSources = fetchEventSources; + t.getEventSources = getEventSources; + t.getEventSourceById = getEventSourceById; + t.getEventSourcesByMatchArray = getEventSourcesByMatchArray; + t.getEventSourcesByMatch = getEventSourcesByMatch; + t.addEventSource = addEventSource; + t.removeEventSource = removeEventSource; + t.removeEventSources = removeEventSources; + t.updateEvent = updateEvent; + t.renderEvent = renderEvent; + t.removeEvents = removeEvents; + t.clientEvents = clientEvents; + t.mutateEvent = mutateEvent; + t.normalizeEventDates = normalizeEventDates; + t.normalizeEventTimes = normalizeEventTimes; + + + // imports + var reportEvents = t.reportEvents; + + + // locals + var stickySource = { events: [] }; + var sources = [ stickySource ]; + var rangeStart, rangeEnd; + var pendingSourceCnt = 0; // outstanding fetch requests, max one per source + var cache = []; // holds events that have already been expanded + + + $.each( + (t.options.events ? [ t.options.events ] : []).concat(t.options.eventSources || []), + function(i, sourceInput) { + var source = buildEventSource(sourceInput); + if (source) { + sources.push(source); + } + } + ); + + + + /* Fetching + -----------------------------------------------------------------------------*/ + + + // start and end are assumed to be unzoned + function isFetchNeeded(start, end) { + return !rangeStart || // nothing has been fetched yet? + start < rangeStart || end > rangeEnd; // is part of the new range outside of the old range? + } + + + function fetchEvents(start, end) { + rangeStart = start; + rangeEnd = end; + fetchEventSources(sources, 'reset'); + } + + + // expects an array of event source objects (the originals, not copies) + // `specialFetchType` is an optimization parameter that affects purging of the event cache. + function fetchEventSources(specificSources, specialFetchType) { + var i, source; + + if (specialFetchType === 'reset') { + cache = []; + } + else if (specialFetchType !== 'add') { + cache = excludeEventsBySources(cache, specificSources); + } + + for (i = 0; i < specificSources.length; i++) { + source = specificSources[i]; + + // already-pending sources have already been accounted for in pendingSourceCnt + if (source._status !== 'pending') { + pendingSourceCnt++; + } + + source._fetchId = (source._fetchId || 0) + 1; + source._status = 'pending'; + } + + for (i = 0; i < specificSources.length; i++) { + source = specificSources[i]; + + tryFetchEventSource(source, source._fetchId); + } + } + + + // fetches an event source and processes its result ONLY if it is still the current fetch. + // caller is responsible for incrementing pendingSourceCnt first. + function tryFetchEventSource(source, fetchId) { + _fetchEventSource(source, function(eventInputs) { + var isArraySource = $.isArray(source.events); + var i, eventInput; + var abstractEvent; + + if ( + // is this the source's most recent fetch? + // if not, rely on an upcoming fetch of this source to decrement pendingSourceCnt + fetchId === source._fetchId && + // event source no longer valid? + source._status !== 'rejected' + ) { + source._status = 'resolved'; + + if (eventInputs) { + for (i = 0; i < eventInputs.length; i++) { + eventInput = eventInputs[i]; + + if (isArraySource) { // array sources have already been convert to Event Objects + abstractEvent = eventInput; + } + else { + abstractEvent = buildEventFromInput(eventInput, source); + } + + if (abstractEvent) { // not false (an invalid event) + cache.push.apply( + cache, + expandEvent(abstractEvent) // add individual expanded events to the cache + ); + } + } + } + + decrementPendingSourceCnt(); + } + }); + } + + + function rejectEventSource(source) { + var wasPending = source._status === 'pending'; + + source._status = 'rejected'; + + if (wasPending) { + decrementPendingSourceCnt(); + } + } + + + function decrementPendingSourceCnt() { + pendingSourceCnt--; + if (!pendingSourceCnt) { + reportEvents(cache); + } + } + + + function _fetchEventSource(source, callback) { + var i; + var fetchers = FC.sourceFetchers; + var res; + + for (i=0; i= eventStart && range.end <= eventEnd; + } + + + // Does the event's date range intersect with the given range? + // start/end already assumed to have stripped zones :( + function eventIntersectsRange(event, range) { + var eventStart = event.start.clone().stripZone(); + var eventEnd = t.getEventEnd(event).stripZone(); + + return range.start < eventEnd && range.end > eventStart; + } + + + t.getEventCache = function() { + return cache; + }; + +} + + +// hook for external libs to manipulate event properties upon creation. +// should manipulate the event in-place. +Calendar.prototype.normalizeEvent = function(event) { +}; + + +// Returns a list of events that the given event should be compared against when being considered for a move to +// the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar. +Calendar.prototype.getPeerEvents = function(span, event) { + var cache = this.getEventCache(); + var peerEvents = []; + var i, otherEvent; + + for (i = 0; i < cache.length; i++) { + otherEvent = cache[i]; + if ( + !event || + event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events + ) { + peerEvents.push(otherEvent); + } + } + + return peerEvents; +}; + + +// updates the "backup" properties, which are preserved in order to compute diffs later on. +function backupEventDates(event) { + event._allDay = event.allDay; + event._start = event.start.clone(); + event._end = event.end ? event.end.clone() : null; +} + +;; + +/* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells. +----------------------------------------------------------------------------------------------------------------------*/ +// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting. +// It is responsible for managing width/height. + +var BasicView = FC.BasicView = View.extend({ + + scroller: null, + + dayGridClass: DayGrid, // class the dayGrid will be instantiated from (overridable by subclasses) + dayGrid: null, // the main subcomponent that does most of the heavy lifting + + dayNumbersVisible: false, // display day numbers on each day cell? + weekNumbersVisible: false, // display week numbers along the side? + + weekNumberWidth: null, // width of all the week-number cells running down the side + + headContainerEl: null, // div that hold's the dayGrid's rendered date header + headRowEl: null, // the fake row element of the day-of-week header + + + initialize: function() { + this.dayGrid = this.instantiateDayGrid(); + + this.scroller = new Scroller({ + overflowX: 'hidden', + overflowY: 'auto' + }); + }, + + + // Generates the DayGrid object this view needs. Draws from this.dayGridClass + instantiateDayGrid: function() { + // generate a subclass on the fly with BasicView-specific behavior + // TODO: cache this subclass + var subclass = this.dayGridClass.extend(basicDayGridMethods); + + return new subclass(this); + }, + + + // Sets the display range and computes all necessary dates + setRange: function(range) { + View.prototype.setRange.call(this, range); // call the super-method + + this.dayGrid.breakOnWeeks = /year|month|week/.test(this.intervalUnit); // do before setRange + this.dayGrid.setRange(range); + }, + + + // Compute the value to feed into setRange. Overrides superclass. + computeRange: function(date) { + var range = View.prototype.computeRange.call(this, date); // get value from the super-method + + // year and month views should be aligned with weeks. this is already done for week + if (/year|month/.test(range.intervalUnit)) { + range.start.startOf('week'); + range.start = this.skipHiddenDays(range.start); + + // make end-of-week if not already + if (range.end.weekday()) { + range.end.add(1, 'week').startOf('week'); + range.end = this.skipHiddenDays(range.end, -1, true); // exclusively move backwards + } + } + + return range; + }, + + + // Renders the view into `this.el`, which should already be assigned + renderDates: function() { + + this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible + this.weekNumbersVisible = this.opt('weekNumbers'); + this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible; + + this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml()); + this.renderHead(); + + this.scroller.render(); + var dayGridContainerEl = this.scroller.el.addClass('fc-day-grid-container'); + var dayGridEl = $('
    ').appendTo(dayGridContainerEl); + this.el.find('.fc-body > tr > td').append(dayGridContainerEl); + + this.dayGrid.setElement(dayGridEl); + this.dayGrid.renderDates(this.hasRigidRows()); + }, + + + // render the day-of-week headers + renderHead: function() { + this.headContainerEl = + this.el.find('.fc-head-container') + .html(this.dayGrid.renderHeadHtml()); + this.headRowEl = this.headContainerEl.find('.fc-row'); + }, + + + // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, + // always completely kill the dayGrid's rendering. + unrenderDates: function() { + this.dayGrid.unrenderDates(); + this.dayGrid.removeElement(); + this.scroller.destroy(); + }, + + + renderBusinessHours: function() { + this.dayGrid.renderBusinessHours(); + }, + + + unrenderBusinessHours: function() { + this.dayGrid.unrenderBusinessHours(); + }, + + + // Builds the HTML skeleton for the view. + // The day-grid component will render inside of a container defined by this HTML. + renderSkeletonHtml: function() { + return '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
    '; + }, + + + // Generates an HTML attribute string for setting the width of the week number column, if it is known + weekNumberStyleAttr: function() { + if (this.weekNumberWidth !== null) { + return 'style="width:' + this.weekNumberWidth + 'px"'; + } + return ''; + }, + + + // Determines whether each row should have a constant height + hasRigidRows: function() { + var eventLimit = this.opt('eventLimit'); + return eventLimit && typeof eventLimit !== 'number'; + }, + + + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + + + // Refreshes the horizontal dimensions of the view + updateWidth: function() { + if (this.weekNumbersVisible) { + // Make sure all week number cells running down the side have the same width. + // Record the width for cells created later. + this.weekNumberWidth = matchCellWidths( + this.el.find('.fc-week-number') + ); + } + }, + + + // Adjusts the vertical dimensions of the view to the specified values + setHeight: function(totalHeight, isAuto) { + var eventLimit = this.opt('eventLimit'); + var scrollerHeight; + var scrollbarWidths; + + // reset all heights to be natural + this.scroller.clear(); + uncompensateScroll(this.headRowEl); + + this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed + + // is the event limit a constant level number? + if (eventLimit && typeof eventLimit === 'number') { + this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after + } + + // distribute the height to the rows + // (totalHeight is a "recommended" value if isAuto) + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.setGridHeight(scrollerHeight, isAuto); + + // is the event limit dynamically calculated? + if (eventLimit && typeof eventLimit !== 'number') { + this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set + } + + if (!isAuto) { // should we force dimensions of the scroll container? + + this.scroller.setHeight(scrollerHeight); + scrollbarWidths = this.scroller.getScrollbarWidths(); + + if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? + + compensateScroll(this.headRowEl, scrollbarWidths); + + // doing the scrollbar compensation might have created text overflow which created more height. redo + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.scroller.setHeight(scrollerHeight); + } + + // guarantees the same scrollbar widths + this.scroller.lockOverflow(scrollbarWidths); + } + }, + + + // given a desired total height of the view, returns what the height of the scroller should be + computeScrollerHeight: function(totalHeight) { + return totalHeight - + subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller + }, + + + // Sets the height of just the DayGrid component in this view + setGridHeight: function(height, isAuto) { + if (isAuto) { + undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding + } + else { + distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows + } + }, + + + /* Scroll + ------------------------------------------------------------------------------------------------------------------*/ + + + queryScroll: function() { + return this.scroller.getScrollTop(); + }, + + + setScroll: function(top) { + this.scroller.setScrollTop(top); + }, + + + /* Hit Areas + ------------------------------------------------------------------------------------------------------------------*/ + // forward all hit-related method calls to dayGrid + + + prepareHits: function() { + this.dayGrid.prepareHits(); + }, + + + releaseHits: function() { + this.dayGrid.releaseHits(); + }, + + + queryHit: function(left, top) { + return this.dayGrid.queryHit(left, top); + }, + + + getHitSpan: function(hit) { + return this.dayGrid.getHitSpan(hit); + }, + + + getHitEl: function(hit) { + return this.dayGrid.getHitEl(hit); + }, + + + /* Events + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders the given events onto the view and populates the segments array + renderEvents: function(events) { + this.dayGrid.renderEvents(events); + + this.updateHeight(); // must compensate for events that overflow the row + }, + + + // Retrieves all segment objects that are rendered in the view + getEventSegs: function() { + return this.dayGrid.getEventSegs(); + }, + + + // Unrenders all event elements and clears internal segment data + unrenderEvents: function() { + this.dayGrid.unrenderEvents(); + + // we DON'T need to call updateHeight() because: + // A) a renderEvents() call always happens after this, which will eventually call updateHeight() + // B) in IE8, this causes a flash whenever events are rerendered + }, + + + /* Dragging (for both events and external elements) + ------------------------------------------------------------------------------------------------------------------*/ + + + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(dropLocation, seg) { + return this.dayGrid.renderDrag(dropLocation, seg); + }, + + + unrenderDrag: function() { + this.dayGrid.unrenderDrag(); + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection + renderSelection: function(span) { + this.dayGrid.renderSelection(span); + }, + + + // Unrenders a visual indications of a selection + unrenderSelection: function() { + this.dayGrid.unrenderSelection(); + } + +}); + + +// Methods that will customize the rendering behavior of the BasicView's dayGrid +var basicDayGridMethods = { + + + // Generates the HTML that will go before the day-of week header cells + renderHeadIntroHtml: function() { + var view = this.view; + + if (view.weekNumbersVisible) { + return '' + + '' + + '' + // needed for matchCellWidths + htmlEscape(view.opt('weekNumberTitle')) + + '' + + ''; + } + + return ''; + }, + + + // Generates the HTML that will go before content-skeleton cells that display the day/week numbers + renderNumberIntroHtml: function(row) { + var view = this.view; + + if (view.weekNumbersVisible) { + return '' + + '' + + '' + // needed for matchCellWidths + this.getCellDate(row, 0).format('w') + + '' + + ''; + } + + return ''; + }, + + + // Generates the HTML that goes before the day bg cells for each day-row + renderBgIntroHtml: function() { + var view = this.view; + + if (view.weekNumbersVisible) { + return ''; + } + + return ''; + }, + + + // Generates the HTML that goes before every other type of row generated by DayGrid. + // Affects helper-skeleton and highlight-skeleton rows. + renderIntroHtml: function() { + var view = this.view; + + if (view.weekNumbersVisible) { + return ''; + } + + return ''; + } + +}; + +;; + +/* A month view with day cells running in rows (one-per-week) and columns +----------------------------------------------------------------------------------------------------------------------*/ + +var MonthView = FC.MonthView = BasicView.extend({ + + // Produces information about what range to display + computeRange: function(date) { + var range = BasicView.prototype.computeRange.call(this, date); // get value from super-method + var rowCnt; + + // ensure 6 weeks + if (this.isFixedWeeks()) { + rowCnt = Math.ceil(range.end.diff(range.start, 'weeks', true)); // could be partial weeks due to hiddenDays + range.end.add(6 - rowCnt, 'weeks'); + } + + return range; + }, + + + // Overrides the default BasicView behavior to have special multi-week auto-height logic + setGridHeight: function(height, isAuto) { + + isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated + + // if auto, make the height of each row the height that it would be if there were 6 weeks + if (isAuto) { + height *= this.rowCnt / 6; + } + + distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows + }, + + + isFixedWeeks: function() { + var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated + if (weekMode) { + return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed + } + + return this.opt('fixedWeekCount'); + } + +}); + +;; + +fcViews.basic = { + 'class': BasicView +}; + +fcViews.basicDay = { + type: 'basic', + duration: { days: 1 } +}; + +fcViews.basicWeek = { + type: 'basic', + duration: { weeks: 1 } +}; + +fcViews.month = { + 'class': MonthView, + duration: { months: 1 }, // important for prev/next + defaults: { + fixedWeekCount: true + } +}; +;; + +/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically. +----------------------------------------------------------------------------------------------------------------------*/ +// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). +// Responsible for managing width/height. + +var AgendaView = FC.AgendaView = View.extend({ + + scroller: null, + + timeGridClass: TimeGrid, // class used to instantiate the timeGrid. subclasses can override + timeGrid: null, // the main time-grid subcomponent of this view + + dayGridClass: DayGrid, // class used to instantiate the dayGrid. subclasses can override + dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null + + axisWidth: null, // the width of the time axis running down the side + + headContainerEl: null, // div that hold's the timeGrid's rendered date header + noScrollRowEls: null, // set of fake row elements that must compensate when scroller has scrollbars + + // when the time-grid isn't tall enough to occupy the given height, we render an
    underneath + bottomRuleEl: null, + + + initialize: function() { + this.timeGrid = this.instantiateTimeGrid(); + + if (this.opt('allDaySlot')) { // should we display the "all-day" area? + this.dayGrid = this.instantiateDayGrid(); // the all-day subcomponent of this view + } + + this.scroller = new Scroller({ + overflowX: 'hidden', + overflowY: 'auto' + }); + }, + + + // Instantiates the TimeGrid object this view needs. Draws from this.timeGridClass + instantiateTimeGrid: function() { + var subclass = this.timeGridClass.extend(agendaTimeGridMethods); + + return new subclass(this); + }, + + + // Instantiates the DayGrid object this view might need. Draws from this.dayGridClass + instantiateDayGrid: function() { + var subclass = this.dayGridClass.extend(agendaDayGridMethods); + + return new subclass(this); + }, + + + /* Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + // Sets the display range and computes all necessary dates + setRange: function(range) { + View.prototype.setRange.call(this, range); // call the super-method + + this.timeGrid.setRange(range); + if (this.dayGrid) { + this.dayGrid.setRange(range); + } + }, + + + // Renders the view into `this.el`, which has already been assigned + renderDates: function() { + + this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml()); + this.renderHead(); + + this.scroller.render(); + var timeGridWrapEl = this.scroller.el.addClass('fc-time-grid-container'); + var timeGridEl = $('
    ').appendTo(timeGridWrapEl); + this.el.find('.fc-body > tr > td').append(timeGridWrapEl); + + this.timeGrid.setElement(timeGridEl); + this.timeGrid.renderDates(); + + // the
    that sometimes displays under the time-grid + this.bottomRuleEl = $('
    ') + .appendTo(this.timeGrid.el); // inject it into the time-grid + + if (this.dayGrid) { + this.dayGrid.setElement(this.el.find('.fc-day-grid')); + this.dayGrid.renderDates(); + + // have the day-grid extend it's coordinate area over the
    dividing the two grids + this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight(); + } + + this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller + }, + + + // render the day-of-week headers + renderHead: function() { + this.headContainerEl = + this.el.find('.fc-head-container') + .html(this.timeGrid.renderHeadHtml()); + }, + + + // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, + // always completely kill each grid's rendering. + unrenderDates: function() { + this.timeGrid.unrenderDates(); + this.timeGrid.removeElement(); + + if (this.dayGrid) { + this.dayGrid.unrenderDates(); + this.dayGrid.removeElement(); + } + + this.scroller.destroy(); + }, + + + // Builds the HTML skeleton for the view. + // The day-grid and time-grid components will render inside containers defined by this HTML. + renderSkeletonHtml: function() { + return '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
    ' + + (this.dayGrid ? + '
    ' + + '
    ' : + '' + ) + + '
    '; + }, + + + // Generates an HTML attribute string for setting the width of the axis, if it is known + axisStyleAttr: function() { + if (this.axisWidth !== null) { + return 'style="width:' + this.axisWidth + 'px"'; + } + return ''; + }, + + + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ + + + renderBusinessHours: function() { + this.timeGrid.renderBusinessHours(); + + if (this.dayGrid) { + this.dayGrid.renderBusinessHours(); + } + }, + + + unrenderBusinessHours: function() { + this.timeGrid.unrenderBusinessHours(); + + if (this.dayGrid) { + this.dayGrid.unrenderBusinessHours(); + } + }, + + + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ + + + getNowIndicatorUnit: function() { + return this.timeGrid.getNowIndicatorUnit(); + }, + + + renderNowIndicator: function(date) { + this.timeGrid.renderNowIndicator(date); + }, + + + unrenderNowIndicator: function() { + this.timeGrid.unrenderNowIndicator(); + }, + + + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + + + updateSize: function(isResize) { + this.timeGrid.updateSize(isResize); + + View.prototype.updateSize.call(this, isResize); // call the super-method + }, + + + // Refreshes the horizontal dimensions of the view + updateWidth: function() { + // make all axis cells line up, and record the width so newly created axis cells will have it + this.axisWidth = matchCellWidths(this.el.find('.fc-axis')); + }, + + + // Adjusts the vertical dimensions of the view to the specified values + setHeight: function(totalHeight, isAuto) { + var eventLimit; + var scrollerHeight; + var scrollbarWidths; + + // reset all dimensions back to the original state + this.bottomRuleEl.hide(); // .show() will be called later if this
    is necessary + this.scroller.clear(); // sets height to 'auto' and clears overflow + uncompensateScroll(this.noScrollRowEls); + + // limit number of events in the all-day area + if (this.dayGrid) { + this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed + + eventLimit = this.opt('eventLimit'); + if (eventLimit && typeof eventLimit !== 'number') { + eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number + } + if (eventLimit) { + this.dayGrid.limitRows(eventLimit); + } + } + + if (!isAuto) { // should we force dimensions of the scroll container? + + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.scroller.setHeight(scrollerHeight); + scrollbarWidths = this.scroller.getScrollbarWidths(); + + if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? + + // make the all-day and header rows lines up + compensateScroll(this.noScrollRowEls, scrollbarWidths); + + // the scrollbar compensation might have changed text flow, which might affect height, so recalculate + // and reapply the desired height to the scroller. + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.scroller.setHeight(scrollerHeight); + } + + // guarantees the same scrollbar widths + this.scroller.lockOverflow(scrollbarWidths); + + // if there's any space below the slats, show the horizontal rule. + // this won't cause any new overflow, because lockOverflow already called. + if (this.timeGrid.getTotalSlatHeight() < scrollerHeight) { + this.bottomRuleEl.show(); + } + } + }, + + + // given a desired total height of the view, returns what the height of the scroller should be + computeScrollerHeight: function(totalHeight) { + return totalHeight - + subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller + }, + + + /* Scroll + ------------------------------------------------------------------------------------------------------------------*/ + + + // Computes the initial pre-configured scroll state prior to allowing the user to change it + computeInitialScroll: function() { + var scrollTime = moment.duration(this.opt('scrollTime')); + var top = this.timeGrid.computeTimeTop(scrollTime); + + // zoom can give weird floating-point values. rather scroll a little bit further + top = Math.ceil(top); + + if (top) { + top++; // to overcome top border that slots beyond the first have. looks better + } + + return top; + }, + + + queryScroll: function() { + return this.scroller.getScrollTop(); + }, + + + setScroll: function(top) { + this.scroller.setScrollTop(top); + }, + + + /* Hit Areas + ------------------------------------------------------------------------------------------------------------------*/ + // forward all hit-related method calls to the grids (dayGrid might not be defined) + + + prepareHits: function() { + this.timeGrid.prepareHits(); + if (this.dayGrid) { + this.dayGrid.prepareHits(); + } + }, + + + releaseHits: function() { + this.timeGrid.releaseHits(); + if (this.dayGrid) { + this.dayGrid.releaseHits(); + } + }, + + + queryHit: function(left, top) { + var hit = this.timeGrid.queryHit(left, top); + + if (!hit && this.dayGrid) { + hit = this.dayGrid.queryHit(left, top); + } + + return hit; + }, + + + getHitSpan: function(hit) { + // TODO: hit.component is set as a hack to identify where the hit came from + return hit.component.getHitSpan(hit); + }, + + + getHitEl: function(hit) { + // TODO: hit.component is set as a hack to identify where the hit came from + return hit.component.getHitEl(hit); + }, + + + /* Events + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders events onto the view and populates the View's segment array + renderEvents: function(events) { + var dayEvents = []; + var timedEvents = []; + var daySegs = []; + var timedSegs; + var i; + + // separate the events into all-day and timed + for (i = 0; i < events.length; i++) { + if (events[i].allDay) { + dayEvents.push(events[i]); + } + else { + timedEvents.push(events[i]); + } + } + + // render the events in the subcomponents + timedSegs = this.timeGrid.renderEvents(timedEvents); + if (this.dayGrid) { + daySegs = this.dayGrid.renderEvents(dayEvents); + } + + // the all-day area is flexible and might have a lot of events, so shift the height + this.updateHeight(); + }, + + + // Retrieves all segment objects that are rendered in the view + getEventSegs: function() { + return this.timeGrid.getEventSegs().concat( + this.dayGrid ? this.dayGrid.getEventSegs() : [] + ); + }, + + + // Unrenders all event elements and clears internal segment data + unrenderEvents: function() { + + // unrender the events in the subcomponents + this.timeGrid.unrenderEvents(); + if (this.dayGrid) { + this.dayGrid.unrenderEvents(); + } + + // we DON'T need to call updateHeight() because: + // A) a renderEvents() call always happens after this, which will eventually call updateHeight() + // B) in IE8, this causes a flash whenever events are rerendered + }, + + + /* Dragging (for events and external elements) + ------------------------------------------------------------------------------------------------------------------*/ + + + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(dropLocation, seg) { + if (dropLocation.start.hasTime()) { + return this.timeGrid.renderDrag(dropLocation, seg); + } + else if (this.dayGrid) { + return this.dayGrid.renderDrag(dropLocation, seg); + } + }, + + + unrenderDrag: function() { + this.timeGrid.unrenderDrag(); + if (this.dayGrid) { + this.dayGrid.unrenderDrag(); + } + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection + renderSelection: function(span) { + if (span.start.hasTime() || span.end.hasTime()) { + this.timeGrid.renderSelection(span); + } + else if (this.dayGrid) { + this.dayGrid.renderSelection(span); + } + }, + + + // Unrenders a visual indications of a selection + unrenderSelection: function() { + this.timeGrid.unrenderSelection(); + if (this.dayGrid) { + this.dayGrid.unrenderSelection(); + } + } + +}); + + +// Methods that will customize the rendering behavior of the AgendaView's timeGrid +// TODO: move into TimeGrid +var agendaTimeGridMethods = { + + + // Generates the HTML that will go before the day-of week header cells + renderHeadIntroHtml: function() { + var view = this.view; + var weekText; + + if (view.opt('weekNumbers')) { + weekText = this.start.format(view.opt('smallWeekFormat')); + + return '' + + '' + + '' + // needed for matchCellWidths + htmlEscape(weekText) + + '' + + ''; + } + else { + return ''; + } + }, + + + // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column. + renderBgIntroHtml: function() { + var view = this.view; + + return ''; + }, + + + // Generates the HTML that goes before all other types of cells. + // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. + renderIntroHtml: function() { + var view = this.view; + + return ''; + } + +}; + + +// Methods that will customize the rendering behavior of the AgendaView's dayGrid +var agendaDayGridMethods = { + + + // Generates the HTML that goes before the all-day cells + renderBgIntroHtml: function() { + var view = this.view; + + return '' + + '' + + '' + // needed for matchCellWidths + (view.opt('allDayHtml') || htmlEscape(view.opt('allDayText'))) + + '' + + ''; + }, + + + // Generates the HTML that goes before all other types of cells. + // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. + renderIntroHtml: function() { + var view = this.view; + + return ''; + } + +}; + +;; + +var AGENDA_ALL_DAY_EVENT_LIMIT = 5; + +// potential nice values for the slot-duration and interval-duration +// from largest to smallest +var AGENDA_STOCK_SUB_DURATIONS = [ + { hours: 1 }, + { minutes: 30 }, + { minutes: 15 }, + { seconds: 30 }, + { seconds: 15 } +]; + +fcViews.agenda = { + 'class': AgendaView, + defaults: { + allDaySlot: true, + allDayText: 'all-day', + slotDuration: '00:30:00', + minTime: '00:00:00', + maxTime: '24:00:00', + slotEventOverlap: true // a bad name. confused with overlap/constraint system + } +}; + +fcViews.agendaDay = { + type: 'agenda', + duration: { days: 1 } +}; + +fcViews.agendaWeek = { + type: 'agenda', + duration: { weeks: 1 } +}; +;; + +return FC; // export for Node/CommonJS +}); \ No newline at end of file diff --git a/public/static/libs/fullcalendar/fullcalendar.min.css b/public/static/libs/fullcalendar/fullcalendar.min.css new file mode 100644 index 0000000..a925e49 --- /dev/null +++ b/public/static/libs/fullcalendar/fullcalendar.min.css @@ -0,0 +1,5 @@ +/*! + * FullCalendar v2.9.0 Stylesheet + * Docs & License: http://fullcalendar.io/ + * (c) 2016 Adam Shaw + */.fc-bgevent,.fc-highlight{opacity:.3;filter:alpha(opacity=30)}.fc-icon,body .fc{font-size:1em}.fc-button-group,.fc-icon{display:inline-block}.fc-bg,.fc-row .fc-bgevent-skeleton,.fc-row .fc-highlight-skeleton{bottom:0}.fc-icon,.fc-unselectable{-khtml-user-select:none;-webkit-touch-callout:none}.fc .fc-axis,.fc button,.fc-time-grid-event .fc-time,.fc-time-grid-event.fc-short .fc-content{white-space:nowrap}.fc{direction:ltr;text-align:left}.fc-rtl{text-align:right}.fc th,.fc-basic-view .fc-week-number,.fc-icon,.fc-toolbar{text-align:center}.fc-unthemed .fc-content,.fc-unthemed .fc-divider,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#ddd}.fc-unthemed .fc-popover{background-color:#fff}.fc-unthemed .fc-divider,.fc-unthemed .fc-popover .fc-header{background:#eee}.fc-unthemed .fc-popover .fc-header .fc-close{color:#666}.fc-unthemed .fc-today{background:#fcf8e3}.fc-highlight{background:#bce8f1}.fc-bgevent{background:#8fdf82}.fc-nonbusiness{background:#d7d7d7}.fc-icon{height:1em;line-height:1em;overflow:hidden;font-family:"Courier New",Courier,monospace;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.fc-icon:after{position:relative}.fc-icon-left-single-arrow:after{content:"\02039";font-weight:700;font-size:200%;top:-7%}.fc-icon-right-single-arrow:after{content:"\0203A";font-weight:700;font-size:200%;top:-7%}.fc-icon-left-double-arrow:after{content:"\000AB";font-size:160%;top:-7%}.fc-icon-right-double-arrow:after{content:"\000BB";font-size:160%;top:-7%}.fc-icon-left-triangle:after{content:"\25C4";font-size:125%;top:3%}.fc-icon-right-triangle:after{content:"\25BA";font-size:125%;top:3%}.fc-icon-down-triangle:after{content:"\25BC";font-size:125%;top:2%}.fc-icon-x:after{content:"\000D7";font-size:200%;top:6%}.fc button{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;height:2.1em;padding:0 .6em;font-size:1em;cursor:pointer}.fc button::-moz-focus-inner{margin:0;padding:0}.fc-state-default{border:1px solid;background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);color:#333;text-shadow:0 1px 1px rgba(255,255,255,.75);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05)}.fc-state-default.fc-corner-left{border-top-left-radius:4px;border-bottom-left-radius:4px}.fc-state-default.fc-corner-right{border-top-right-radius:4px;border-bottom-right-radius:4px}.fc button .fc-icon{position:relative;top:-.05em;margin:0 .2em;vertical-align:middle}.fc-state-active,.fc-state-disabled,.fc-state-down,.fc-state-hover{color:#333;background-color:#e6e6e6}.fc-state-hover{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.fc-state-active,.fc-state-down{background-color:#ccc;background-image:none;box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.fc-state-disabled{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);box-shadow:none}.fc-event.fc-draggable,.fc-event[href],.fc-popover .fc-header .fc-close{cursor:pointer}.fc .fc-button-group>*{float:left;margin:0 0 0 -1px}.fc .fc-button-group>:first-child{margin-left:0}.fc-popover{position:absolute;box-shadow:0 2px 6px rgba(0,0,0,.15)}.fc-popover .fc-header{padding:2px 4px}.fc-popover .fc-header .fc-title{margin:0 2px}.fc-ltr .fc-popover .fc-header .fc-title,.fc-rtl .fc-popover .fc-header .fc-close{float:left}.fc-ltr .fc-popover .fc-header .fc-close,.fc-rtl .fc-popover .fc-header .fc-title{float:right}.fc-unthemed .fc-popover{border-width:1px;border-style:solid}.fc-unthemed .fc-popover .fc-header .fc-close{font-size:.9em;margin-top:2px}.fc-popover>.ui-widget-header+.ui-widget-content{border-top:0}.fc-divider{border-style:solid;border-width:1px}hr.fc-divider{height:0;margin:0;padding:0 0 2px;border-width:1px 0}.fc-bg table,.fc-row .fc-bgevent-skeleton table,.fc-row .fc-highlight-skeleton table{height:100%}.fc-clear{clear:both}.fc-bg,.fc-bgevent-skeleton,.fc-helper-skeleton,.fc-highlight-skeleton{position:absolute;top:0;left:0;right:0}.fc table{width:100%;box-sizing:border-box;table-layout:fixed;border-collapse:collapse;border-spacing:0;font-size:1em}.fc td,.fc th{border-style:solid;border-width:1px;padding:0;vertical-align:top}.fc td.fc-today{border-style:double}.fc .fc-row{border-style:solid;border-width:0}.fc-row table{border-left:0 hidden transparent;border-right:0 hidden transparent;border-bottom:0 hidden transparent}.fc-row:first-child table{border-top:0 hidden transparent}.fc-row{position:relative}.fc-row .fc-bg{z-index:1}.fc-row .fc-bgevent-skeleton td,.fc-row .fc-highlight-skeleton td{border-color:transparent}.fc-row .fc-bgevent-skeleton{z-index:2}.fc-row .fc-highlight-skeleton{z-index:3}.fc-row .fc-content-skeleton{position:relative;z-index:4;padding-bottom:2px}.fc-row .fc-helper-skeleton{z-index:5}.fc-row .fc-content-skeleton td,.fc-row .fc-helper-skeleton td{background:0 0;border-color:transparent;border-bottom:0}.fc-row .fc-content-skeleton tbody td,.fc-row .fc-helper-skeleton tbody td{border-top:0}.fc-scroller{-webkit-overflow-scrolling:touch}.fc-row.fc-rigid,.fc-time-grid-event{overflow:hidden}.fc-scroller>.fc-day-grid,.fc-scroller>.fc-time-grid{position:relative;width:100%}.fc-event{position:relative;display:block;font-size:.85em;line-height:1.3;border-radius:3px;border:1px solid #3a87ad;background-color:#3a87ad;font-weight:400}.fc-event,.fc-event:hover,.ui-widget .fc-event{color:#fff;text-decoration:none}.fc-not-allowed,.fc-not-allowed .fc-event{cursor:not-allowed}.fc-event .fc-bg{z-index:1;background:#fff;opacity:.25;filter:alpha(opacity=25)}.fc-event .fc-content{position:relative;z-index:2}.fc-event .fc-resizer{position:absolute;z-index:4;display:none}.fc-event.fc-allow-mouse-resize .fc-resizer,.fc-event.fc-selected .fc-resizer{display:block}.fc-event.fc-selected .fc-resizer:before{content:"";position:absolute;z-index:9999;top:50%;left:50%;width:40px;height:40px;margin-left:-20px;margin-top:-20px}.fc-event.fc-selected{z-index:9999!important;box-shadow:0 2px 5px rgba(0,0,0,.2)}.fc-event.fc-selected.fc-dragging{box-shadow:0 2px 7px rgba(0,0,0,.3)}.fc-h-event.fc-selected:before{content:"";position:absolute;z-index:3;top:-10px;bottom:-10px;left:0;right:0}.fc-ltr .fc-h-event.fc-not-start,.fc-rtl .fc-h-event.fc-not-end{margin-left:0;border-left-width:0;padding-left:1px;border-top-left-radius:0;border-bottom-left-radius:0}.fc-ltr .fc-h-event.fc-not-end,.fc-rtl .fc-h-event.fc-not-start{margin-right:0;border-right-width:0;padding-right:1px;border-top-right-radius:0;border-bottom-right-radius:0}.fc-ltr .fc-h-event .fc-start-resizer,.fc-rtl .fc-h-event .fc-end-resizer{cursor:w-resize;left:-1px}.fc-ltr .fc-h-event .fc-end-resizer,.fc-rtl .fc-h-event .fc-start-resizer{cursor:e-resize;right:-1px}.fc-h-event.fc-allow-mouse-resize .fc-resizer{width:7px;top:-1px;bottom:-1px}.fc-h-event.fc-selected .fc-resizer{border-radius:4px;border-width:1px;width:6px;height:6px;border-style:solid;border-color:inherit;background:#fff;top:50%;margin-top:-4px}.fc-ltr .fc-h-event.fc-selected .fc-start-resizer,.fc-rtl .fc-h-event.fc-selected .fc-end-resizer{margin-left:-4px}.fc-ltr .fc-h-event.fc-selected .fc-end-resizer,.fc-rtl .fc-h-event.fc-selected .fc-start-resizer{margin-right:-4px}.fc-day-grid-event{margin:1px 2px 0;padding:0 1px}.fc-day-grid-event.fc-selected:after{content:"";position:absolute;z-index:1;top:-1px;right:-1px;bottom:-1px;left:-1px;background:#000;opacity:.25;filter:alpha(opacity=25)}.fc-day-grid-event .fc-content{white-space:nowrap;overflow:hidden}.fc-day-grid-event .fc-time{font-weight:700}.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer,.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer{margin-left:-2px}.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer,.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer{margin-right:-2px}a.fc-more{margin:1px 3px;font-size:.85em;cursor:pointer;text-decoration:none}a.fc-more:hover{text-decoration:underline}.fc-limited{display:none}.fc-day-grid .fc-row{z-index:1}.fc-more-popover{z-index:2;width:220px}.fc-more-popover .fc-event-container{padding:10px}.fc-now-indicator{position:absolute;border:0 solid red}.fc-unselectable{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}.fc-toolbar{margin-bottom:1em}.fc-toolbar .fc-left{float:left}.fc-toolbar .fc-right{float:right}.fc-toolbar .fc-center{display:inline-block}.fc .fc-toolbar>*>*{float:left;margin-left:.75em}.fc .fc-toolbar>*>:first-child{margin-left:0}.fc-toolbar h2{margin:0}.fc-toolbar button{position:relative}.fc-toolbar .fc-state-hover,.fc-toolbar .ui-state-hover{z-index:2}.fc-toolbar .fc-state-down{z-index:3}.fc-toolbar .fc-state-active,.fc-toolbar .ui-state-active{z-index:4}.fc-toolbar button:focus{z-index:5}.fc-view-container *,.fc-view-container :after,.fc-view-container :before{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.fc-view,.fc-view>table{position:relative;z-index:1}.fc-basicDay-view .fc-content-skeleton,.fc-basicWeek-view .fc-content-skeleton{padding-top:1px;padding-bottom:1em}.fc-basic-view .fc-body .fc-row{min-height:4em}.fc-row.fc-rigid .fc-content-skeleton{position:absolute;top:0;left:0;right:0}.fc-basic-view .fc-day-number,.fc-basic-view .fc-week-number{padding:0 2px}.fc-basic-view td.fc-day-number,.fc-basic-view td.fc-week-number span{padding-top:2px;padding-bottom:2px}.fc-basic-view .fc-week-number span{display:inline-block;min-width:1.25em}.fc-ltr .fc-basic-view .fc-day-number{text-align:right}.fc-rtl .fc-basic-view .fc-day-number{text-align:left}.fc-day-number.fc-other-month{opacity:.3;filter:alpha(opacity=30)}.fc-agenda-view .fc-day-grid{position:relative;z-index:2}.fc-agenda-view .fc-day-grid .fc-row{min-height:3em}.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton{padding-top:1px;padding-bottom:1em}.fc .fc-axis{vertical-align:middle;padding:0 4px}.fc-ltr .fc-axis{text-align:right}.fc-rtl .fc-axis{text-align:left}.ui-widget td.fc-axis{font-weight:400}.fc-time-grid,.fc-time-grid-container{position:relative;z-index:1}.fc-time-grid{min-height:100%}.fc-time-grid table{border:0 hidden transparent}.fc-time-grid>.fc-bg{z-index:1}.fc-time-grid .fc-slats,.fc-time-grid>hr{position:relative;z-index:2}.fc-time-grid .fc-content-col{position:relative}.fc-time-grid .fc-content-skeleton{position:absolute;z-index:3;top:0;left:0;right:0}.fc-time-grid .fc-business-container{position:relative;z-index:1}.fc-time-grid .fc-bgevent-container{position:relative;z-index:2}.fc-time-grid .fc-highlight-container{z-index:3;position:relative}.fc-time-grid .fc-event-container{position:relative;z-index:4}.fc-time-grid .fc-now-indicator-line{z-index:5}.fc-time-grid .fc-helper-container{position:relative;z-index:6}.fc-time-grid .fc-slats td{height:1.5em;border-bottom:0}.fc-time-grid .fc-slats .fc-minor td{border-top-style:dotted}.fc-time-grid .fc-slats .ui-widget-content{background:0 0}.fc-time-grid .fc-highlight{position:absolute;left:0;right:0}.fc-ltr .fc-time-grid .fc-event-container{margin:0 2.5% 0 2px}.fc-rtl .fc-time-grid .fc-event-container{margin:0 2px 0 2.5%}.fc-time-grid .fc-bgevent,.fc-time-grid .fc-event{position:absolute;z-index:1}.fc-time-grid .fc-bgevent{left:0;right:0}.fc-v-event.fc-not-start{border-top-width:0;padding-top:1px;border-top-left-radius:0;border-top-right-radius:0}.fc-v-event.fc-not-end{border-bottom-width:0;padding-bottom:1px;border-bottom-left-radius:0;border-bottom-right-radius:0}.fc-time-grid-event.fc-selected{overflow:visible}.fc-time-grid-event.fc-selected .fc-bg{display:none}.fc-time-grid-event .fc-content{overflow:hidden}.fc-time-grid-event .fc-time,.fc-time-grid-event .fc-title{padding:0 1px}.fc-time-grid-event .fc-time{font-size:.85em}.fc-time-grid-event.fc-short .fc-time,.fc-time-grid-event.fc-short .fc-title{display:inline-block;vertical-align:top}.fc-time-grid-event.fc-short .fc-time span{display:none}.fc-time-grid-event.fc-short .fc-time:before{content:attr(data-start)}.fc-time-grid-event.fc-short .fc-time:after{content:"\000A0-\000A0"}.fc-time-grid-event.fc-short .fc-title{font-size:.85em;padding:0}.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer{left:0;right:0;bottom:0;height:8px;overflow:hidden;line-height:8px;font-size:11px;font-family:monospace;text-align:center;cursor:s-resize}.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer:after{content:"="}.fc-time-grid-event.fc-selected .fc-resizer{border-radius:5px;border-width:1px;width:8px;height:8px;border-style:solid;border-color:inherit;background:#fff;left:50%;margin-left:-5px;bottom:-5px}.fc-time-grid .fc-now-indicator-line{border-top-width:1px;left:0;right:0}.fc-time-grid .fc-now-indicator-arrow{margin-top:-5px}.fc-ltr .fc-time-grid .fc-now-indicator-arrow{left:0;border-width:5px 0 5px 6px;border-top-color:transparent;border-bottom-color:transparent}.fc-rtl .fc-time-grid .fc-now-indicator-arrow{right:0;border-width:5px 6px 5px 0;border-top-color:transparent;border-bottom-color:transparent} \ No newline at end of file diff --git a/public/static/libs/fullcalendar/fullcalendar.min.js b/public/static/libs/fullcalendar/fullcalendar.min.js new file mode 100644 index 0000000..b55ed96 --- /dev/null +++ b/public/static/libs/fullcalendar/fullcalendar.min.js @@ -0,0 +1,9 @@ +/*! + * FullCalendar v2.9.0 + * Docs & License: http://fullcalendar.io/ + * (c) 2016 Adam Shaw + */ +!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):"object"==typeof exports?module.exports=a(require("jquery"),require("moment")):a(jQuery,moment)}(function(a,b){function c(a){return W(a,Ya)}function d(b){var c,d={views:b.views||{}};return a.each(b,function(b,e){"views"!=b&&(a.isPlainObject(e)&&!/(time|duration|interval)$/i.test(b)&&-1==a.inArray(b,Ya)?(c=null,a.each(e,function(a,e){/^(month|week|day|default|basic(Week|Day)?|agenda(Week|Day)?)$/.test(a)?(d.views[a]||(d.views[a]={}),d.views[a][b]=e):(c||(c={}),c[a]=e)}),c&&(d[b]=c)):d[b]=e)}),d}function e(a,b){b.left&&a.css({"border-left-width":1,"margin-left":b.left-1}),b.right&&a.css({"border-right-width":1,"margin-right":b.right-1})}function f(a){a.css({"margin-left":"","margin-right":"","border-left-width":"","border-right-width":""})}function g(){a("body").addClass("fc-not-allowed")}function h(){a("body").removeClass("fc-not-allowed")}function i(b,c,d){var e=Math.floor(c/b.length),f=Math.floor(c-e*(b.length-1)),g=[],h=[],i=[],k=0;j(b),b.each(function(c,d){var j=c===b.length-1?f:e,l=a(d).outerHeight(!0);j>l?(g.push(d),h.push(l),i.push(a(d).height())):k+=l}),d&&(c-=k,e=Math.floor(c/g.length),f=Math.floor(c-e*(g.length-1))),a(g).each(function(b,c){var d=b===g.length-1?f:e,j=h[b],k=i[b],l=d-(j-k);d>j&&a(c).height(l)})}function j(a){a.height("")}function k(b){var c=0;return b.find("> span").each(function(b,d){var e=a(d).outerWidth();e>c&&(c=e)}),c++,b.width(c),c}function l(a,b){var c,d=a.add(b);return d.css({position:"relative",left:-1}),c=a.outerHeight()-b.outerHeight(),d.css({position:"",left:""}),c}function m(b){var c=b.css("position"),d=b.parents().filter(function(){var b=a(this);return/(auto|scroll)/.test(b.css("overflow")+b.css("overflow-y")+b.css("overflow-x"))}).eq(0);return"fixed"!==c&&d.length?d:a(b[0].ownerDocument||document)}function n(a,b){var c=a.offset(),d=c.left-(b?b.left:0),e=c.top-(b?b.top:0);return{left:d,right:d+a.outerWidth(),top:e,bottom:e+a.outerHeight()}}function o(a,b){var c=a.offset(),d=q(a),e=c.left+t(a,"border-left-width")+d.left-(b?b.left:0),f=c.top+t(a,"border-top-width")+d.top-(b?b.top:0);return{left:e,right:e+a[0].clientWidth,top:f,bottom:f+a[0].clientHeight}}function p(a,b){var c=a.offset(),d=c.left+t(a,"border-left-width")+t(a,"padding-left")-(b?b.left:0),e=c.top+t(a,"border-top-width")+t(a,"padding-top")-(b?b.top:0);return{left:d,right:d+a.width(),top:e,bottom:e+a.height()}}function q(a){var b=a.innerWidth()-a[0].clientWidth,c={left:0,right:0,top:0,bottom:a.innerHeight()-a[0].clientHeight};return r()&&"rtl"==a.css("direction")?c.left=b:c.right=b,c}function r(){return null===Za&&(Za=s()),Za}function s(){var b=a("
    ").css({position:"absolute",top:-1e3,left:0,border:0,padding:0,overflow:"scroll",direction:"rtl"}).appendTo("body"),c=b.children(),d=c.offset().left>b.offset().left;return b.remove(),d}function t(a,b){return parseFloat(a.css(b))||0}function u(a){return 1==a.which&&!a.ctrlKey}function v(a){if(void 0!==a.pageX)return a.pageX;var b=a.originalEvent.touches;return b?b[0].pageX:void 0}function w(a){if(void 0!==a.pageY)return a.pageY;var b=a.originalEvent.touches;return b?b[0].pageY:void 0}function x(a){return/^touch/.test(a.type)}function y(a){a.addClass("fc-unselectable").on("selectstart",z)}function z(a){a.preventDefault()}function A(a){return window.addEventListener?(window.addEventListener("scroll",a,!0),!0):!1}function B(a){return window.removeEventListener?(window.removeEventListener("scroll",a,!0),!0):!1}function C(a,b){var c={left:Math.max(a.left,b.left),right:Math.min(a.right,b.right),top:Math.max(a.top,b.top),bottom:Math.min(a.bottom,b.bottom)};return c.lefti&&j>g?(g>=i?(c=g.clone(),e=!0):(c=i.clone(),e=!1),j>=h?(d=h.clone(),f=!0):(d=j.clone(),f=!1),{start:c,end:d,isStart:e,isEnd:f}):void 0}function L(a,c){return b.duration({days:a.clone().stripTime().diff(c.clone().stripTime(),"days"),ms:a.time()-c.time()})}function M(a,c){return b.duration({days:a.clone().stripTime().diff(c.clone().stripTime(),"days")})}function N(a,c,d){return b.duration(Math.round(a.diff(c,d,!0)),d)}function O(a,b){var c,d,e;for(c=0;c<_a.length&&(d=_a[c],e=P(d,a,b),!(e>=1&&ha(e)));c++);return d}function P(a,c,d){return null!=d?d.diff(c,a,!0):b.isDuration(c)?c.as(a):c.end.diff(c.start,a,!0)}function Q(a,b,c){var d;return T(c)?(b-a)/c:(d=c.asMonths(),Math.abs(d)>=1&&ha(d)?b.diff(a,"months",!0)/d:b.diff(a,"days",!0)/c.asDays())}function R(a,b){var c,d;return T(a)||T(b)?a/b:(c=a.asMonths(),d=b.asMonths(),Math.abs(c)>=1&&ha(c)&&Math.abs(d)>=1&&ha(d)?c/d:a.asDays()/b.asDays())}function S(a,c){var d;return T(a)?b.duration(a*c):(d=a.asMonths(),Math.abs(d)>=1&&ha(d)?b.duration({months:d*c}):b.duration({days:a.asDays()*c}))}function T(a){return Boolean(a.hours()||a.minutes()||a.seconds()||a.milliseconds())}function U(a){return"[object Date]"===Object.prototype.toString.call(a)||a instanceof Date}function V(a){return/^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(a)}function W(a,b){var c,d,e,f,g,h,i={};if(b)for(c=0;c=0;f--)if(g=a[f][d],"object"==typeof g)e.unshift(g);else if(void 0!==g){i[d]=g;break}e.length&&(i[d]=W(e))}for(c=a.length-1;c>=0;c--){h=a[c];for(d in h)d in i||(i[d]=h[d])}return i}function X(a){var b=function(){};return b.prototype=a,new b}function Y(a,b){for(var c in a)$(a,c)&&(b[c]=a[c])}function Z(a,b){var c,d,e=["constructor","toString","valueOf"];for(c=0;c/g,">").replace(/'/g,"'").replace(/"/g,""").replace(/\n/g,"
    ")}function da(a){return a.replace(/&.*?;/g,"")}function ea(b){var c=[];return a.each(b,function(a,b){null!=b&&c.push(a+":"+b)}),c.join(";")}function fa(a){return a.charAt(0).toUpperCase()+a.slice(1)}function ga(a,b){return a-b}function ha(a){return a%1===0}function ia(a,b){var c=a[b];return function(){return c.apply(a,arguments)}}function ja(a,b,c){var d,e,f,g,h,i=function(){var j=+new Date-g;b>j?d=setTimeout(i,b-j):(d=null,c||(h=a.apply(f,e),f=e=null))};return function(){f=this,e=arguments,g=+new Date;var j=c&&!d;return d||(d=setTimeout(i,b)),j&&(h=a.apply(f,e),f=e=null),h}}function ka(b,c){return b&&b.then&&"resolved"!==b.state()?c?b.then(c):void 0:a.when(c())}function la(c,d,e){var f,g,h,i,j=c[0],k=1==c.length&&"string"==typeof j;return b.isMoment(j)?(i=b.apply(null,c),na(j,i)):U(j)||void 0===j?i=b.apply(null,c):(f=!1,g=!1,k?eb.test(j)?(j+="-01",c=[j],f=!0,g=!0):(h=fb.exec(j))&&(f=!h[5],g=!0):a.isArray(j)&&(g=!0),i=d||f?b.utc.apply(b,c):b.apply(null,c),f?(i._ambigTime=!0,i._ambigZone=!0):e&&(g?i._ambigZone=!0:k&&(i.utcOffset?i.utcOffset(j):i.zone(j)))),i._fullCalendar=!0,i}function ma(a,c){var d,e,f=!1,g=!1,h=a.length,i=[];for(d=0;h>d;d++)e=a[d],b.isMoment(e)||(e=Wa.moment.parseZone(e)),f=f||e._ambigTime,g=g||e._ambigZone,i.push(e);for(d=0;h>d;d++)e=i[d],c||!f||e._ambigTime?g&&!e._ambigZone&&(i[d]=e.clone().stripZone()):i[d]=e.clone().stripTime();return i}function na(a,b){a._ambigTime?b._ambigTime=!0:b._ambigTime&&(b._ambigTime=!1),a._ambigZone?b._ambigZone=!0:b._ambigZone&&(b._ambigZone=!1)}function oa(a,b){a.year(b[0]||0).month(b[1]||0).date(b[2]||0).hours(b[3]||0).minutes(b[4]||0).seconds(b[5]||0).milliseconds(b[6]||0)}function pa(a,b){return hb.format.call(a,b)}function qa(a,b){return ra(a,wa(b))}function ra(a,b){var c,d="";for(c=0;cg&&(f=va(a,b,j,k,c[h]),f!==!1);h--)m=f+m;for(i=g;h>=i;i++)n+=sa(a,c[i]),o+=sa(b,c[i]);return(n||o)&&(p=e?o+d+n:n+d+o),l+p+m}function va(a,b,c,d,e){var f,g;return"string"==typeof e?e:(f=e.token)&&(g=jb[f.charAt(0)],g&&c.isSame(d,g))?pa(a,f):!1}function wa(a){return a in kb?kb[a]:kb[a]=xa(a)}function xa(a){for(var b,c=[],d=/\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g;b=d.exec(a);)b[1]?c.push(b[1]):b[2]?c.push({maybe:xa(b[2])}):b[3]?c.push({token:b[3]}):b[5]&&c.push(b[5]);return c}function ya(){}function za(a,b){var c;return $(b,"constructor")&&(c=b.constructor),"function"!=typeof c&&(c=b.constructor=function(){a.apply(this,arguments)}),c.prototype=X(a.prototype),Y(b,c.prototype),Z(b,c.prototype),Y(a,c),c}function Aa(a,b){Y(b,a.prototype)}function Ba(a,b){return a||b?a&&b?a.component===b.component&&Ca(a,b)&&Ca(b,a):!1:!0}function Ca(a,b){for(var c in a)if(!/^(component|left|right|top|bottom)$/.test(c)&&a[c]!==b[c])return!1;return!0}function Da(a){var b=Fa(a);return"background"===b||"inverse-background"===b}function Ea(a){return"inverse-background"===Fa(a)}function Fa(a){return ba((a.source||{}).rendering,a.rendering)}function Ga(a){var b,c,d={};for(b=0;b=a.leftCol)return!0;return!1}function Ka(a,b){return a.leftCol-b.leftCol}function La(a){var b,c,d,e=[];for(b=0;bb.top&&a.top").prependTo(c),V=T.header=new Ta(T),i(),l(T.options.defaultView),T.options.handleWindowResize&&(_=ja(r,T.options.windowResizeDelay),a(window).resize(_))}function i(){V.render(),V.el&&c.prepend(V.el)}function j(){Z&&Z.removeElement(),V.removeElement(),W.remove(),c.removeClass("fc fc-ltr fc-rtl fc-unthemed ui-widget"),_&&a(window).unbind("resize",_)}function k(){return c.is(":visible")}function l(b,c){ga++,Z&&b&&Z.type!==b&&(M(),m()),!Z&&b&&(Z=T.view=fa[b]||(fa[b]=T.instantiateView(b)),Z.setElement(a("
    ").appendTo(W)),V.activateButton(b)),Z&&(aa=Z.massageCurrentDate(aa),Z.displaying&&aa.isWithin(Z.intervalStart,Z.intervalEnd)||k()&&(Z.display(aa,c),N(),z(),A(),v())),N(),ga--}function m(){V.deactivateButton(Z.type),Z.removeElement(),Z=T.view=null}function n(){ga++,M();var a=Z.type,b=Z.queryScroll();m(),l(a,b),N(),ga--}function o(a){return k()?(a&&q(),ga++,Z.updateSize(!0),ga--,!0):void 0}function p(){k()&&q()}function q(){$="number"==typeof T.options.contentHeight?T.options.contentHeight:"number"==typeof T.options.height?T.options.height-(V.el?V.el.outerHeight(!0):0):Math.round(W.width()/Math.max(T.options.aspectRatio,.5))}function r(a){!ga&&a.target===window&&Z.start&&o(!0)&&Z.trigger("windowResize",ea)}function s(){w()}function t(a){da(T.getEventSourcesByMatchArray(a))}function u(){k()&&(M(),Z.displayEvents(ha),N())}function v(){!T.options.lazyFetching||ba(Z.start,Z.end)?w():u()}function w(){ca(Z.start,Z.end)}function x(a){ha=a,u()}function y(){u()}function z(){V.updateTitle(Z.title)}function A(){var a=T.getNow();a.isWithin(Z.intervalStart,Z.intervalEnd)?V.disableButton("today"):V.enableButton("today")}function B(a,b){Z.select(T.buildSelectSpan.apply(T,arguments))}function C(){Z&&Z.unselect()}function D(){aa=Z.computePrevDate(aa),l()}function E(){aa=Z.computeNextDate(aa),l()}function F(){aa.add(-1,"years"),l()}function G(){aa.add(1,"years"),l()}function H(){aa=T.getNow(),l()}function I(a){aa=T.moment(a).stripZone(),l()}function J(a){aa.add(b.duration(a)),l()}function K(a,b){var c;b=b||"day",c=T.getViewSpec(b)||T.getUnitViewSpec(b),aa=a.clone(),l(c?c.type:null)}function L(){return T.applyTimezone(aa)}function M(){W.css({width:"100%",height:W.height(),overflow:"hidden"})}function N(){W.css({width:"",height:"",overflow:""})}function O(){return T}function P(){return Z}function Q(a,b){var c;if("string"==typeof a){if(void 0===b)return T.options[a];c={},c[a]=b,R(c)}else"object"==typeof a&&R(a)}function R(a){var b,c=0;for(b in a)T.dynamicOverrides[b]=a[b];T.viewSpecCache={},T.populateOptionsHash();for(b in a)T.triggerOptionHandlers(b),c++;if(1===c){if("height"===b||"contentHeight"===b||"aspectRatio"===b)return void o(!0);if("defaultDate"===b)return;if("businessHours"===b)return void(Z&&(Z.unrenderBusinessHours(),Z.renderBusinessHours()));if("timezone"===b)return T.rezoneArrayEventSources(),void s()}i(),fa={},n()}function S(a,b){var c=Array.prototype.slice.call(arguments,2);return b=b||ea,this.triggerWith(a,b,c),T.options[a]?T.options[a].apply(b,c):void 0}var T=this;T.render=g,T.destroy=j,T.refetchEvents=s,T.refetchEventSources=t,T.reportEvents=x,T.reportEventChange=y,T.rerenderEvents=u,T.changeView=l,T.select=B,T.unselect=C,T.prev=D,T.next=E,T.prevYear=F,T.nextYear=G,T.today=H,T.gotoDate=I,T.incrementDate=J,T.zoomTo=K,T.getDate=L,T.getCalendar=O,T.getView=P,T.option=Q,T.trigger=S,T.dynamicOverrides={},T.viewSpecCache={},T.optionHandlers={},T.overrides=d(e||{}),T.populateOptionsHash();var U;T.bindOptions(["lang","monthNames","monthNamesShort","dayNames","dayNamesShort","firstDay","weekNumberCalculation"],function(a,b,c,d,e,g,h){if(U=X(Sa(a)),b&&(U._months=b),c&&(U._monthsShort=c),d&&(U._weekdays=d),e&&(U._weekdaysShort=e),null!=g){var i=X(U._week);i.dow=g,U._week=i}"iso"===h&&(h="ISO"),"ISO"!==h&&"local"!==h&&"function"!=typeof h||(U._fullCalendar_weekCalc=h),aa&&f(aa)}),T.defaultAllDayEventDuration=b.duration(T.options.defaultAllDayEventDuration),T.defaultTimedEventDuration=b.duration(T.options.defaultTimedEventDuration),T.moment=function(){var a;return"local"===T.options.timezone?(a=Wa.moment.apply(null,arguments),a.hasTime()&&a.local()):a="UTC"===T.options.timezone?Wa.moment.utc.apply(null,arguments):Wa.moment.parseZone.apply(null,arguments),f(a),a},T.getIsAmbigTimezone=function(){return"local"!==T.options.timezone&&"UTC"!==T.options.timezone},T.applyTimezone=function(a){if(!a.hasTime())return a.clone();var b,c=T.moment(a.toArray()),d=a.time()-c.time();return d&&(b=c.clone().add(d),a.time()-b.time()===0&&(c=b)),c},T.getNow=function(){var a=T.options.now;return"function"==typeof a&&(a=a()),T.moment(a).stripZone()},T.getEventEnd=function(a){return a.end?a.end.clone():T.getDefaultEventEnd(a.allDay,a.start)},T.getDefaultEventEnd=function(a,b){var c=b.clone();return a?c.stripTime().add(T.defaultAllDayEventDuration):c.add(T.defaultTimedEventDuration),T.getIsAmbigTimezone()&&c.stripZone(),c},T.humanizeDuration=function(a){return(a.locale||a.lang).call(a,T.options.lang).humanize()},Ua.call(T);var V,W,Y,Z,$,_,aa,ba=T.isFetchNeeded,ca=T.fetchEvents,da=T.fetchEventSources,ea=c[0],fa={},ga=0,ha=[];aa=null!=T.options.defaultDate?T.moment(T.options.defaultDate).stripZone():T.getNow(),T.getSuggestedViewHeight=function(){return void 0===$&&p(),$},T.isHeightAuto=function(){return"auto"===T.options.contentHeight||"auto"===T.options.height},T.freezeContentHeight=M,T.unfreezeContentHeight=N,T.initialize()}function Ra(b){a.each(Db,function(a,c){null==b[a]&&(b[a]=c(b))})}function Sa(a){var c=b.localeData||b.langData;return c.call(b,a)||c.call(b,"en")}function Ta(b){function c(){var c=b.options,f=c.header;n=c.theme?"ui":"fc",f?(m?m.empty():m=this.el=a("
    "),m.append(e("left")).append(e("right")).append(e("center")).append('
    ')):d()}function d(){m&&(m.remove(),m=l.el=null)}function e(c){var d=a('
    '),e=b.options,f=e.header[c];return f&&a.each(f.split(" "),function(c){var f,g=a(),h=!0;a.each(this.split(","),function(c,d){var f,i,j,k,l,m,p,q,r,s;"title"==d?(g=g.add(a("

     

    ")),h=!1):((f=(e.customButtons||{})[d])?(j=function(a){f.click&&f.click.call(s[0],a)},k="",l=f.text):(i=b.getViewSpec(d))?(j=function(){b.changeView(d)},o.push(d),k=i.buttonTextOverride,l=i.buttonTextDefault):b[d]&&(j=function(){b[d]()},k=(b.overrides.buttonText||{})[d],l=e.buttonText[d]),j&&(m=f?f.themeIcon:e.themeButtonIcons[d],p=f?f.icon:e.buttonIcons[d],q=k?ca(k):m&&e.theme?"":p&&!e.theme?"":ca(l),r=["fc-"+d+"-button",n+"-button",n+"-state-default"],s=a('").click(function(a){s.hasClass(n+"-state-disabled")||(j(a),(s.hasClass(n+"-state-active")||s.hasClass(n+"-state-disabled"))&&s.removeClass(n+"-state-hover"))}).mousedown(function(){s.not("."+n+"-state-active").not("."+n+"-state-disabled").addClass(n+"-state-down")}).mouseup(function(){s.removeClass(n+"-state-down")}).hover(function(){s.not("."+n+"-state-active").not("."+n+"-state-disabled").addClass(n+"-state-hover")},function(){s.removeClass(n+"-state-hover").removeClass(n+"-state-down")}),g=g.add(s)))}),h&&g.first().addClass(n+"-corner-left").end().last().addClass(n+"-corner-right").end(),g.length>1?(f=a("
    "),h&&f.addClass("fc-button-group"),f.append(g),d.append(f)):d.append(g)}),d}function f(a){m&&m.find("h2").text(a)}function g(a){m&&m.find(".fc-"+a+"-button").addClass(n+"-state-active")}function h(a){m&&m.find(".fc-"+a+"-button").removeClass(n+"-state-active")}function i(a){m&&m.find(".fc-"+a+"-button").prop("disabled",!0).addClass(n+"-state-disabled")}function j(a){m&&m.find(".fc-"+a+"-button").prop("disabled",!1).removeClass(n+"-state-disabled")}function k(){return o}var l=this;l.render=c,l.removeElement=d,l.updateTitle=f,l.activateButton=g,l.deactivateButton=h,l.disableButton=i,l.enableButton=j,l.getViewsWithButtons=k,l.el=null;var m,n,o=[]}function Ua(){function c(a,b){return!W||W>a||b>X}function d(a,b){W=a,X=b,e($,"reset")}function e(a,b){var c,d;for("reset"===b?da=[]:"add"!==b&&(da=u(da,a)),c=0;c=c&&b.end<=d}function T(a,b){var c=a.start.clone().stripZone(),d=U.getEventEnd(a).stripZone();return b.startc}var U=this;U.isFetchNeeded=c,U.fetchEvents=d,U.fetchEventSources=e,U.getEventSources=o,U.getEventSourceById=p,U.getEventSourcesByMatchArray=q,U.getEventSourcesByMatch=r,U.addEventSource=j,U.removeEventSource=l,U.removeEventSources=m,U.updateEvent=v,U.renderEvent=y,U.removeEvents=z,U.clientEvents=A,U.mutateEvent=H,U.normalizeEventDates=E,U.normalizeEventTimes=F;var W,X,Y=U.reportEvents,Z={events:[]},$=[Z],ca=0,da=[];a.each((U.options.events?[U.options.events]:[]).concat(U.options.eventSources||[]),function(a,b){var c=k(b);c&&$.push(c)}),U.rezoneArrayEventSources=function(){var b,c,d;for(b=0;b<$.length;b++)if(c=$[b].events,a.isArray(c))for(d=0;d=c[1]&&c[0]a;a++)b=arguments[a],c-1>a&&Aa(this,b);return za(this,b||{})},ya.mixin=function(a){Aa(this,a)};var lb=Wa.EmitterMixin={on:function(b,c){var d=function(a,b){return c.apply(b.context||this,b.args||[])};return c.guid||(c.guid=a.guid++),d.guid=c.guid,a(this).on(b,d),this},off:function(b,c){return a(this).off(b,c),this},trigger:function(b){var c=Array.prototype.slice.call(arguments,1);return a(this).triggerHandler(b,{args:c}),this},triggerWith:function(b,c,d){return a(this).triggerHandler(b,{context:c,args:d}),this}},mb=Wa.ListenerMixin=function(){var b=0,c={listenerId:null,listenTo:function(b,c,d){if("object"==typeof c)for(var e in c)c.hasOwnProperty(e)&&this.listenTo(b,e,c[e]);else"string"==typeof c&&b.on(c+"."+this.getListenerNamespace(),a.proxy(d,this))},stopListeningTo:function(a,b){a.off((b||"")+"."+this.getListenerNamespace())},getListenerNamespace:function(){return null==this.listenerId&&(this.listenerId=b++),"_listener"+this.listenerId}};return c}(),nb={isIgnoringMouse:!1,delayUnignoreMouse:null,initMouseIgnoring:function(a){this.delayUnignoreMouse=ja(ia(this,"unignoreMouse"),a||1e3)},tempIgnoreMouse:function(){this.isIgnoringMouse=!0,this.delayUnignoreMouse()},unignoreMouse:function(){this.isIgnoringMouse=!1}},ob=ya.extend(mb,{isHidden:!0,options:null,el:null,margin:10,constructor:function(a){this.options=a||{}},show:function(){this.isHidden&&(this.el||this.render(),this.el.show(),this.position(),this.isHidden=!1,this.trigger("show"))},hide:function(){this.isHidden||(this.el.hide(),this.isHidden=!0,this.trigger("hide"))},render:function(){var b=this,c=this.options;this.el=a('
    ').addClass(c.className||"").css({top:0,left:0}).append(c.content).appendTo(c.parentEl),this.el.on("click",".fc-close",function(){b.hide()}),c.autoHide&&this.listenTo(a(document),"mousedown",this.documentMousedown)},documentMousedown:function(b){this.el&&!a(b.target).closest(this.el).length&&this.hide()},removeElement:function(){this.hide(),this.el&&(this.el.remove(),this.el=null),this.stopListeningTo(a(document),"mousedown")},position:function(){var b,c,d,e,f,g=this.options,h=this.el.offsetParent().offset(),i=this.el.outerWidth(),j=this.el.outerHeight(),k=a(window),l=m(this.el);e=g.top||0,f=void 0!==g.left?g.left:void 0!==g.right?g.right-i:0,l.is(window)||l.is(document)?(l=k,b=0,c=0):(d=l.offset(),b=d.top,c=d.left),b+=k.scrollTop(),c+=k.scrollLeft(),g.viewportConstrain!==!1&&(e=Math.min(e,b+l.outerHeight()-j-this.margin),e=Math.max(e,b+this.margin),f=Math.min(f,c+l.outerWidth()-i-this.margin),f=Math.max(f,c+this.margin)),this.el.css({top:e-h.top,left:f-h.left})},trigger:function(a){this.options[a]&&this.options[a].apply(this,Array.prototype.slice.call(arguments,1))}}),pb=Wa.CoordCache=ya.extend({els:null,forcedOffsetParentEl:null,origin:null,boundingRect:null,isHorizontal:!1,isVertical:!1,lefts:null,rights:null,tops:null,bottoms:null,constructor:function(b){this.els=a(b.els),this.isHorizontal=b.isHorizontal,this.isVertical=b.isVertical,this.forcedOffsetParentEl=b.offsetParent?a(b.offsetParent):null},build:function(){var a=this.forcedOffsetParentEl||this.els.eq(0).offsetParent();this.origin=a.offset(),this.boundingRect=this.queryBoundingRect(),this.isHorizontal&&this.buildElHorizontals(),this.isVertical&&this.buildElVerticals()},clear:function(){this.origin=null,this.boundingRect=null,this.lefts=null,this.rights=null,this.tops=null,this.bottoms=null},ensureBuilt:function(){this.origin||this.build()},queryBoundingRect:function(){var a=m(this.els.eq(0));return a.is(document)?void 0:o(a)},buildElHorizontals:function(){var b=[],c=[];this.els.each(function(d,e){var f=a(e),g=f.offset().left,h=f.outerWidth();b.push(g),c.push(g+h)}),this.lefts=b,this.rights=c},buildElVerticals:function(){var b=[],c=[];this.els.each(function(d,e){var f=a(e),g=f.offset().top,h=f.outerHeight();b.push(g),c.push(g+h)}),this.tops=b,this.bottoms=c},getHorizontalIndex:function(a){this.ensureBuilt();var b,c=this.boundingRect,d=this.lefts,e=this.rights,f=d.length;if(!c||a>=c.left&&ab;b++)if(a>=d[b]&&a=c.top&&ab;b++)if(a>=d[b]&&a=e*e&&this.handleDistanceSurpassed(a)),this.isDragging&&this.handleDrag(c,d,a)},handleDrag:function(a,b,c){this.trigger("drag",a,b,c),this.updateAutoScroll(c)},endDrag:function(a){this.isDragging&&(this.isDragging=!1,this.handleDragEnd(a))},handleDragEnd:function(a){this.trigger("dragEnd",a),this.destroyHrefHack()},startDelay:function(a){var b=this;this.delay?this.delayTimeoutId=setTimeout(function(){b.handleDelayEnd(a)},this.delay):this.handleDelayEnd(a)},handleDelayEnd:function(a){this.isDelayEnded=!0,this.isDistanceSurpassed&&this.startDrag(a)},handleDistanceSurpassed:function(a){this.isDistanceSurpassed=!0,this.isDelayEnded&&this.startDrag(a)},handleTouchMove:function(a){this.isDragging&&a.preventDefault(),this.handleMove(a)},handleMouseMove:function(a){this.handleMove(a)},handleTouchScroll:function(a){this.isDragging||this.endInteraction(a,!0)},initHrefHack:function(){var a=this.subjectEl;(this.subjectHref=a?a.attr("href"):null)&&a.removeAttr("href")},destroyHrefHack:function(){var a=this.subjectEl,b=this.subjectHref;setTimeout(function(){b&&a.attr("href",b)},0)},trigger:function(a){this.options[a]&&this.options[a].apply(this,Array.prototype.slice.call(arguments,1)),this["_"+a]&&this["_"+a].apply(this,Array.prototype.slice.call(arguments,1))}});qb.mixin({isAutoScroll:!1,scrollBounds:null,scrollTopVel:null,scrollLeftVel:null,scrollIntervalId:null,scrollSensitivity:30,scrollSpeed:200,scrollIntervalMs:50,initAutoScroll:function(){var a=this.scrollEl;this.isAutoScroll=this.options.scroll&&a&&!a.is(window)&&!a.is(document),this.isAutoScroll&&this.listenTo(a,"scroll",ja(this.handleDebouncedScroll,100))},destroyAutoScroll:function(){this.endAutoScroll(),this.isAutoScroll&&this.stopListeningTo(this.scrollEl,"scroll")},computeScrollBounds:function(){this.isAutoScroll&&(this.scrollBounds=n(this.scrollEl))},updateAutoScroll:function(a){var b,c,d,e,f=this.scrollSensitivity,g=this.scrollBounds,h=0,i=0;g&&(b=(f-(w(a)-g.top))/f,c=(f-(g.bottom-w(a)))/f,d=(f-(v(a)-g.left))/f,e=(f-(g.right-v(a)))/f,b>=0&&1>=b?h=b*this.scrollSpeed*-1:c>=0&&1>=c&&(h=c*this.scrollSpeed),d>=0&&1>=d?i=d*this.scrollSpeed*-1:e>=0&&1>=e&&(i=e*this.scrollSpeed)),this.setScrollVel(h,i)},setScrollVel:function(a,b){this.scrollTopVel=a,this.scrollLeftVel=b,this.constrainScrollVel(),!this.scrollTopVel&&!this.scrollLeftVel||this.scrollIntervalId||(this.scrollIntervalId=setInterval(ia(this,"scrollIntervalFunc"),this.scrollIntervalMs))},constrainScrollVel:function(){var a=this.scrollEl;this.scrollTopVel<0?a.scrollTop()<=0&&(this.scrollTopVel=0):this.scrollTopVel>0&&a.scrollTop()+a[0].clientHeight>=a[0].scrollHeight&&(this.scrollTopVel=0),this.scrollLeftVel<0?a.scrollLeft()<=0&&(this.scrollLeftVel=0):this.scrollLeftVel>0&&a.scrollLeft()+a[0].clientWidth>=a[0].scrollWidth&&(this.scrollLeftVel=0)},scrollIntervalFunc:function(){var a=this.scrollEl,b=this.scrollIntervalMs/1e3;this.scrollTopVel&&a.scrollTop(a.scrollTop()+this.scrollTopVel*b),this.scrollLeftVel&&a.scrollLeft(a.scrollLeft()+this.scrollLeftVel*b),this.constrainScrollVel(),this.scrollTopVel||this.scrollLeftVel||this.endAutoScroll()},endAutoScroll:function(){this.scrollIntervalId&&(clearInterval(this.scrollIntervalId),this.scrollIntervalId=null,this.handleScrollEnd())},handleDebouncedScroll:function(){this.scrollIntervalId||this.handleScrollEnd()},handleScrollEnd:function(){}});var rb=qb.extend({component:null,origHit:null,hit:null,coordAdjust:null,constructor:function(a,b){qb.call(this,b),this.component=a},handleInteractionStart:function(a){var b,c,d,e=this.subjectEl;this.computeCoords(),a?(c={left:v(a),top:w(a)},d=c,e&&(b=n(e),d=D(d,b)),this.origHit=this.queryHit(d.left,d.top),e&&this.options.subjectCenter&&(this.origHit&&(b=C(this.origHit,b)||b),d=E(b)),this.coordAdjust=F(d,c)):(this.origHit=null,this.coordAdjust=null),qb.prototype.handleInteractionStart.apply(this,arguments)},computeCoords:function(){this.component.prepareHits(),this.computeScrollBounds()},handleDragStart:function(a){var b;qb.prototype.handleDragStart.apply(this,arguments),b=this.queryHit(v(a),w(a)),b&&this.handleHitOver(b)},handleDrag:function(a,b,c){var d;qb.prototype.handleDrag.apply(this,arguments),d=this.queryHit(v(c),w(c)),Ba(d,this.hit)||(this.hit&&this.handleHitOut(),d&&this.handleHitOver(d))},handleDragEnd:function(){this.handleHitDone(),qb.prototype.handleDragEnd.apply(this,arguments)},handleHitOver:function(a){var b=Ba(a,this.origHit);this.hit=a,this.trigger("hitOver",this.hit,b,this.origHit)},handleHitOut:function(){this.hit&&(this.trigger("hitOut",this.hit),this.handleHitDone(),this.hit=null)},handleHitDone:function(){this.hit&&this.trigger("hitDone",this.hit)},handleInteractionEnd:function(){qb.prototype.handleInteractionEnd.apply(this,arguments),this.origHit=null,this.hit=null,this.component.releaseHits()},handleScrollEnd:function(){qb.prototype.handleScrollEnd.apply(this,arguments),this.computeCoords()},queryHit:function(a,b){return this.coordAdjust&&(a+=this.coordAdjust.left,b+=this.coordAdjust.top),this.component.queryHit(a,b)}}),sb=ya.extend(mb,{options:null,sourceEl:null,el:null,parentEl:null,top0:null,left0:null,y0:null,x0:null,topDelta:null,leftDelta:null,isFollowing:!1,isHidden:!1,isAnimating:!1,constructor:function(b,c){this.options=c=c||{},this.sourceEl=b,this.parentEl=c.parentEl?a(c.parentEl):b.parent()},start:function(b){this.isFollowing||(this.isFollowing=!0,this.y0=w(b),this.x0=v(b),this.topDelta=0,this.leftDelta=0,this.isHidden||this.updatePosition(),x(b)?this.listenTo(a(document),"touchmove",this.handleMove):this.listenTo(a(document),"mousemove",this.handleMove))},stop:function(b,c){function d(){this.isAnimating=!1,e.removeElement(),this.top0=this.left0=null,c&&c()}var e=this,f=this.options.revertDuration;this.isFollowing&&!this.isAnimating&&(this.isFollowing=!1,this.stopListeningTo(a(document)),b&&f&&!this.isHidden?(this.isAnimating=!0,this.el.animate({top:this.top0,left:this.left0},{duration:f,complete:d})):d())},getEl:function(){var a=this.el;return a||(this.sourceEl.width(),a=this.el=this.sourceEl.clone().addClass(this.options.additionalClass||"").css({position:"absolute",visibility:"",display:this.isHidden?"none":"",margin:0,right:"auto",bottom:"auto",width:this.sourceEl.width(),height:this.sourceEl.height(),opacity:this.options.opacity||"",zIndex:this.options.zIndex}),a.addClass("fc-unselectable"),a.appendTo(this.parentEl)),a},removeElement:function(){this.el&&(this.el.remove(),this.el=null)},updatePosition:function(){var a,b;this.getEl(),null===this.top0&&(this.sourceEl.width(),a=this.sourceEl.offset(),b=this.el.offsetParent().offset(),this.top0=a.top-b.top,this.left0=a.left-b.left),this.el.css({top:this.top0+this.topDelta,left:this.left0+this.leftDelta})},handleMove:function(a){this.topDelta=w(a)-this.y0,this.leftDelta=v(a)-this.x0,this.isHidden||this.updatePosition()},hide:function(){this.isHidden||(this.isHidden=!0,this.el&&this.el.hide())},show:function(){this.isHidden&&(this.isHidden=!1,this.updatePosition(),this.getEl().show())}}),tb=Wa.Grid=ya.extend(mb,nb,{view:null,isRTL:null,start:null,end:null,el:null,elsByFill:null,eventTimeFormat:null,displayEventTime:null,displayEventEnd:null,minResizeDuration:null,largeUnit:null,dayDragListener:null,segDragListener:null,segResizeListener:null,externalDragListener:null,constructor:function(a){this.view=a,this.isRTL=a.opt("isRTL"),this.elsByFill={},this.dayDragListener=this.buildDayDragListener(),this.initMouseIgnoring()},computeEventTimeFormat:function(){return this.view.opt("smallTimeFormat")},computeDisplayEventTime:function(){return!0},computeDisplayEventEnd:function(){return!0},setRange:function(a){this.start=a.start.clone(),this.end=a.end.clone(),this.rangeUpdated(),this.processRangeOptions()},rangeUpdated:function(){},processRangeOptions:function(){var a,b,c=this.view;this.eventTimeFormat=c.opt("eventTimeFormat")||c.opt("timeFormat")||this.computeEventTimeFormat(),a=c.opt("displayEventTime"),null==a&&(a=this.computeDisplayEventTime()),b=c.opt("displayEventEnd"),null==b&&(b=this.computeDisplayEventEnd()),this.displayEventTime=a,this.displayEventEnd=b},spanToSegs:function(a){},diffDates:function(a,b){return this.largeUnit?N(a,b,this.largeUnit):L(a,b)},prepareHits:function(){},releaseHits:function(){},queryHit:function(a,b){},getHitSpan:function(a){},getHitEl:function(a){},setElement:function(a){this.el=a,y(a),this.bindDayHandler("touchstart",this.dayTouchStart),this.bindDayHandler("mousedown",this.dayMousedown),this.bindSegHandlers(),this.bindGlobalHandlers()},bindDayHandler:function(b,c){var d=this;this.el.on(b,function(b){return a(b.target).is(".fc-event-container *, .fc-more")||a(b.target).closest(".fc-popover").length?void 0:c.call(d,b)})},removeElement:function(){this.unbindGlobalHandlers(),this.clearDragListeners(),this.el.remove()},renderSkeleton:function(){},renderDates:function(){},unrenderDates:function(){},bindGlobalHandlers:function(){this.listenTo(a(document),{dragstart:this.externalDragStart,sortstart:this.externalDragStart})},unbindGlobalHandlers:function(){this.stopListeningTo(a(document))},dayMousedown:function(a){this.isIgnoringMouse||this.dayDragListener.startInteraction(a,{})},dayTouchStart:function(a){var b=this.view;(b.isSelected||b.selectedEvent)&&this.tempIgnoreMouse(),this.dayDragListener.startInteraction(a,{delay:this.view.opt("longPressDelay")})},buildDayDragListener:function(){var a,b,c=this,d=this.view,e=d.opt("selectable"),f=new rb(this,{scroll:d.opt("dragScroll"),interactionStart:function(){a=f.origHit},dragStart:function(){d.unselect()},hitOver:function(d,f,h){h&&(f||(a=null),e&&(b=c.computeSelection(c.getHitSpan(h),c.getHitSpan(d)),b?c.renderSelection(b):b===!1&&g()))},hitOut:function(){a=null,b=null,c.unrenderSelection(),h()},interactionEnd:function(e,f){f||(a&&!c.isIgnoringMouse&&d.triggerDayClick(c.getHitSpan(a),c.getHitEl(a),e),b&&d.reportSelection(b,e),h())}});return f},clearDragListeners:function(){this.dayDragListener.endInteraction(),this.segDragListener&&this.segDragListener.endInteraction(),this.segResizeListener&&this.segResizeListener.endInteraction(),this.externalDragListener&&this.externalDragListener.endInteraction()},renderEventLocationHelper:function(a,b){var c=this.fabricateHelperEvent(a,b);return this.renderHelper(c,b)},fabricateHelperEvent:function(a,b){var c=b?X(b.event):{};return c.start=a.start.clone(),c.end=a.end?a.end.clone():null,c.allDay=null,this.view.calendar.normalizeEventDates(c),c.className=(c.className||[]).concat("fc-helper"),b||(c.editable=!1),c},renderHelper:function(a,b){},unrenderHelper:function(){},renderSelection:function(a){this.renderHighlight(a)},unrenderSelection:function(){this.unrenderHighlight()},computeSelection:function(a,b){var c=this.computeSelectionSpan(a,b);return c&&!this.view.calendar.isSelectionSpanAllowed(c)?!1:c},computeSelectionSpan:function(a,b){var c=[a.start,a.end,b.start,b.end];return c.sort(ga),{start:c[0].clone(),end:c[3].clone()}},renderHighlight:function(a){this.renderFill("highlight",this.spanToSegs(a))},unrenderHighlight:function(){this.unrenderFill("highlight")},highlightSegClasses:function(){return["fc-highlight"]},renderBusinessHours:function(){},unrenderBusinessHours:function(){},getNowIndicatorUnit:function(){},renderNowIndicator:function(a){},unrenderNowIndicator:function(){},renderFill:function(a,b){},unrenderFill:function(a){var b=this.elsByFill[a];b&&(b.remove(),delete this.elsByFill[a])},renderFillSegEls:function(b,c){var d,e=this,f=this[b+"SegEl"],g="",h=[];if(c.length){for(d=0;d"},getDayClasses:function(a){var b=this.view,c=b.calendar.getNow(),d=["fc-"+$a[a.day()]];return 1==b.intervalDuration.as("months")&&a.month()!=b.intervalStart.month()&&d.push("fc-other-month"),a.isSame(c,"day")?d.push("fc-today",b.highlightStateClass):c>a?d.push("fc-past"):d.push("fc-future"),d}});tb.mixin({mousedOverSeg:null,isDraggingSeg:!1,isResizingSeg:!1,isDraggingExternal:!1,segs:null,renderEvents:function(a){var b,c=[],d=[];for(b=0;b *",function(b){var e=a(this).data("fc-seg");return!e||d.isDraggingSeg||d.isResizingSeg?void 0:c.call(d,e,b)})},handleSegClick:function(a,b){return this.view.trigger("eventClick",a.el[0],a.event,b)},handleSegMouseover:function(a,b){this.isIgnoringMouse||this.mousedOverSeg||(this.mousedOverSeg=a,a.el.addClass("fc-allow-mouse-resize"),this.view.trigger("eventMouseover",a.el[0],a.event,b))},handleSegMouseout:function(a,b){b=b||{},this.mousedOverSeg&&(a=a||this.mousedOverSeg,this.mousedOverSeg=null,a.el.removeClass("fc-allow-mouse-resize"),this.view.trigger("eventMouseout",a.el[0],a.event,b))},handleSegMousedown:function(a,b){var c=this.startSegResize(a,b,{distance:5});!c&&this.view.isEventDraggable(a.event)&&this.buildSegDragListener(a).startInteraction(b,{distance:5})},handleSegTouchStart:function(a,b){var c,d=this.view,e=a.event,f=d.isEventSelected(e),g=d.isEventDraggable(e),h=d.isEventResizable(e),i=!1;f&&h&&(i=this.startSegResize(a,b)),i||!g&&!h||(c=g?this.buildSegDragListener(a):this.buildSegSelectListener(a),c.startInteraction(b,{delay:f?0:this.view.opt("longPressDelay")})),this.tempIgnoreMouse()},handleSegTouchEnd:function(a,b){this.tempIgnoreMouse()},startSegResize:function(b,c,d){return a(c.target).is(".fc-resizer")?(this.buildSegResizeListener(b,a(c.target).is(".fc-start-resizer")).startInteraction(c,d),!0):!1},buildSegDragListener:function(a){var b,c,d,e=this,f=this.view,i=f.calendar,j=a.el,k=a.event;if(this.segDragListener)return this.segDragListener;var l=this.segDragListener=new rb(f,{scroll:f.opt("dragScroll"),subjectEl:j,subjectCenter:!0,interactionStart:function(d){b=!1,c=new sb(a.el,{additionalClass:"fc-dragging",parentEl:f.el,opacity:l.isTouch?null:f.opt("dragOpacity"),revertDuration:f.opt("dragRevertDuration"),zIndex:2}),c.hide(),c.start(d)},dragStart:function(c){l.isTouch&&!f.isEventSelected(k)&&f.selectEvent(k),b=!0,e.handleSegMouseout(a,c),e.segDragStart(a,c),f.hideEvent(k)},hitOver:function(b,h,j){var m;a.hit&&(j=a.hit),d=e.computeEventDrop(j.component.getHitSpan(j),b.component.getHitSpan(b),k),d&&!i.isEventSpanAllowed(e.eventToSpan(d),k)&&(g(),d=null),d&&(m=f.renderDrag(d,a))?(m.addClass("fc-dragging"),l.isTouch||e.applyDragOpacity(m),c.hide()):c.show(),h&&(d=null)},hitOut:function(){f.unrenderDrag(),c.show(),d=null},hitDone:function(){h()},interactionEnd:function(g){c.stop(!d,function(){b&&(f.unrenderDrag(),f.showEvent(k),e.segDragStop(a,g)),d&&f.reportEventDrop(k,d,this.largeUnit,j,g)}),e.segDragListener=null}});return l},buildSegSelectListener:function(a){var b=this,c=this.view,d=a.event;if(this.segDragListener)return this.segDragListener;var e=this.segDragListener=new qb({dragStart:function(a){e.isTouch&&!c.isEventSelected(d)&&c.selectEvent(d)},interactionEnd:function(a){b.segDragListener=null}});return e},segDragStart:function(a,b){this.isDraggingSeg=!0,this.view.trigger("eventDragStart",a.el[0],a.event,b,{})},segDragStop:function(a,b){this.isDraggingSeg=!1,this.view.trigger("eventDragStop",a.el[0],a.event,b,{})},computeEventDrop:function(a,b,c){var d,e,f=this.view.calendar,g=a.start,h=b.start;return g.hasTime()===h.hasTime()?(d=this.diffDates(h,g),c.allDay&&T(d)?(e={start:c.start.clone(),end:f.getEventEnd(c),allDay:!1},f.normalizeEventTimes(e)):e={start:c.start.clone(),end:c.end?c.end.clone():null,allDay:c.allDay},e.start.add(d),e.end&&e.end.add(d)):e={start:h.clone(),end:null,allDay:!h.hasTime()},e},applyDragOpacity:function(a){var b=this.view.opt("dragOpacity");null!=b&&a.each(function(a,c){c.style.opacity=b})},externalDragStart:function(b,c){var d,e,f=this.view;f.opt("droppable")&&(d=a((c?c.item:null)||b.target),e=f.opt("dropAccept"),(a.isFunction(e)?e.call(d[0],d):d.is(e))&&(this.isDraggingExternal||this.listenToExternalDrag(d,b,c)))},listenToExternalDrag:function(a,b,c){var d,e=this,f=this.view.calendar,i=Ia(a),j=e.externalDragListener=new rb(this,{interactionStart:function(){e.isDraggingExternal=!0},hitOver:function(a){d=e.computeExternalDrop(a.component.getHitSpan(a),i),d&&!f.isExternalSpanAllowed(e.eventToSpan(d),d,i.eventProps)&&(g(),d=null),d&&e.renderDrag(d)},hitOut:function(){d=null},hitDone:function(){h(),e.unrenderDrag()},interactionEnd:function(b){d&&e.view.reportExternalDrop(i,d,a,b,c),e.isDraggingExternal=!1,e.externalDragListener=null}});j.startDrag(b)},computeExternalDrop:function(a,b){var c=this.view.calendar,d={start:c.applyTimezone(a.start),end:null};return b.startTime&&!d.start.hasTime()&&d.start.time(b.startTime),b.duration&&(d.end=d.start.clone().add(b.duration)),d},renderDrag:function(a,b){},unrenderDrag:function(){},buildSegResizeListener:function(a,b){var c,d,e=this,f=this.view,i=f.calendar,j=a.el,k=a.event,l=i.getEventEnd(k),m=this.segResizeListener=new rb(this,{scroll:f.opt("dragScroll"),subjectEl:j,interactionStart:function(){c=!1},dragStart:function(b){c=!0,e.handleSegMouseout(a,b),e.segResizeStart(a,b)},hitOver:function(c,h,j){var m=e.getHitSpan(j),n=e.getHitSpan(c);d=b?e.computeEventStartResize(m,n,k):e.computeEventEndResize(m,n,k),d&&(i.isEventSpanAllowed(e.eventToSpan(d),k)?d.start.isSame(k.start)&&d.end.isSame(l)&&(d=null):(g(),d=null)),d&&(f.hideEvent(k),e.renderEventResize(d,a))},hitOut:function(){d=null},hitDone:function(){e.unrenderEventResize(),f.showEvent(k),h()},interactionEnd:function(b){c&&e.segResizeStop(a,b),d&&f.reportEventResize(k,d,this.largeUnit,j,b),e.segResizeListener=null}});return m},segResizeStart:function(a,b){this.isResizingSeg=!0,this.view.trigger("eventResizeStart",a.el[0],a.event,b,{})},segResizeStop:function(a,b){this.isResizingSeg=!1,this.view.trigger("eventResizeStop",a.el[0],a.event,b,{})},computeEventStartResize:function(a,b,c){return this.computeEventResize("start",a,b,c)},computeEventEndResize:function(a,b,c){return this.computeEventResize("end",a,b,c)},computeEventResize:function(a,b,c,d){var e,f,g=this.view.calendar,h=this.diffDates(c[a],b[a]);return e={start:d.start.clone(),end:g.getEventEnd(d),allDay:d.allDay},e.allDay&&T(h)&&(e.allDay=!1,g.normalizeEventTimes(e)),e[a].add(h),e.start.isBefore(e.end)||(f=this.minResizeDuration||(d.allDay?g.defaultAllDayEventDuration:g.defaultTimedEventDuration),"start"==a?e.start=e.end.clone().subtract(f):e.end=e.start.clone().add(f)),e},renderEventResize:function(a,b){},unrenderEventResize:function(){},getEventTimeText:function(a,b,c){return null==b&&(b=this.eventTimeFormat),null==c&&(c=this.displayEventEnd),this.displayEventTime&&a.start.hasTime()?c&&a.end?this.view.formatRange(a,b):a.start.format(b):""},getSegClasses:function(a,b,c){var d=this.view,e=a.event,f=["fc-event",a.isStart?"fc-start":"fc-not-start",a.isEnd?"fc-end":"fc-not-end"].concat(e.className,e.source?e.source.className:[]);return b&&f.push("fc-draggable"),c&&f.push("fc-resizable"),d.isEventSelected(e)&&f.push("fc-selected"),f},getSegSkinCss:function(a){var b=a.event,c=this.view,d=b.source||{},e=b.color,f=d.color,g=c.opt("eventColor");return{"background-color":b.backgroundColor||e||d.backgroundColor||f||c.opt("eventBackgroundColor")||g,"border-color":b.borderColor||e||d.borderColor||f||c.opt("eventBorderColor")||g,color:b.textColor||d.textColor||c.opt("eventTextColor")}},eventToSegs:function(a){return this.eventsToSegs([a])},eventToSpan:function(a){return this.eventToSpans(a)[0]},eventToSpans:function(a){var b=this.eventToRange(a);return this.eventRangeToSpans(b,a)},eventsToSegs:function(b,c){var d=this,e=Ga(b),f=[];return a.each(e,function(a,b){var e,g=[];for(e=0;eh&&g.push({start:h,end:c.start}),h=c.end;return f>h&&g.push({start:h,end:f}),g},sortEventSegs:function(a){a.sort(ia(this,"compareEventSegs"))},compareEventSegs:function(a,b){return a.eventStartMS-b.eventStartMS||b.eventDurationMS-a.eventDurationMS||b.event.allDay-a.event.allDay||H(a.event,b.event,this.view.eventOrderSpecs)}}),Wa.isBgEvent=Da,Wa.dataAttrPrefix="";var ub=Wa.DayTableMixin={breakOnWeeks:!1,dayDates:null,dayIndices:null,daysPerRow:null,rowCnt:null,colCnt:null,colHeadFormat:null,updateDayTable:function(){for(var a,b,c,d=this.view,e=this.start.clone(),f=-1,g=[],h=[];e.isBefore(this.end);)d.isHiddenDay(e)?g.push(f+.5):(f++,g.push(f),h.push(e.clone())),e.add(1,"days");if(this.breakOnWeeks){for(b=h[0].day(),a=1;ac?b[0]-1:c>=b.length?b[b.length-1]+1:b[c]},computeColHeadFormat:function(){return this.rowCnt>1||this.colCnt>10?"ddd":this.colCnt>1?this.view.opt("dayOfMonthFormat"):"dddd"},sliceRangeByRow:function(a){var b,c,d,e,f,g=this.daysPerRow,h=this.view.computeDayRange(a),i=this.getDateDayIndex(h.start),j=this.getDateDayIndex(h.end.clone().subtract(1,"days")),k=[];for(b=0;b=e&&k.push({row:b,firstRowDayIndex:e-c,lastRowDayIndex:f-c,isStart:e===i,isEnd:f===j});return k},sliceRangeByDay:function(a){var b,c,d,e,f,g,h=this.daysPerRow,i=this.view.computeDayRange(a),j=this.getDateDayIndex(i.start),k=this.getDateDayIndex(i.end.clone().subtract(1,"days")),l=[];for(b=0;b=e;e++)f=Math.max(j,e),g=Math.min(k,e),f=Math.ceil(f),g=Math.floor(g),g>=f&&l.push({row:b,firstRowDayIndex:f-c,lastRowDayIndex:g-c,isStart:f===j,isEnd:g===k});return l},renderHeadHtml:function(){var a=this.view;return'
    '+this.renderHeadTrHtml()+"
    "},renderHeadIntroHtml:function(){return this.renderIntroHtml()},renderHeadTrHtml:function(){return""+(this.isRTL?"":this.renderHeadIntroHtml())+this.renderHeadDateCellsHtml()+(this.isRTL?this.renderHeadIntroHtml():"")+""},renderHeadDateCellsHtml:function(){var a,b,c=[];for(a=0;a1?' colspan="'+b+'"':"")+(c?" "+c:"")+">"+ca(a.format(this.colHeadFormat))+""},renderBgTrHtml:function(a){return""+(this.isRTL?"":this.renderBgIntroHtml(a))+this.renderBgCellsHtml(a)+(this.isRTL?this.renderBgIntroHtml(a):"")+""},renderBgIntroHtml:function(a){return this.renderIntroHtml()},renderBgCellsHtml:function(a){var b,c,d=[];for(b=0;b"},renderIntroHtml:function(){},bookendCells:function(a){var b=this.renderIntroHtml();b&&(this.isRTL?a.append(b):a.prepend(b))}},vb=Wa.DayGrid=tb.extend(ub,{numbersVisible:!1,bottomCoordPadding:0,rowEls:null,cellEls:null,helperEls:null,rowCoordCache:null,colCoordCache:null,renderDates:function(a){var b,c,d=this.view,e=this.rowCnt,f=this.colCnt,g="";for(b=0;e>b;b++)g+=this.renderDayRowHtml(b,a);for(this.el.html(g),this.rowEls=this.el.find(".fc-row"),this.cellEls=this.el.find(".fc-day"),this.rowCoordCache=new pb({els:this.rowEls,isVertical:!0}),this.colCoordCache=new pb({els:this.cellEls.slice(0,this.colCnt),isHorizontal:!0}),b=0;e>b;b++)for(c=0;f>c;c++)d.trigger("dayRender",null,this.getCellDate(b,c),this.getCellEl(b,c))},unrenderDates:function(){this.removeSegPopover()},renderBusinessHours:function(){var a=this.view.calendar.getBusinessHoursEvents(!0),b=this.eventsToSegs(a);this.renderFill("businessHours",b,"bgevent")},unrenderBusinessHours:function(){this.unrenderFill("businessHours")},renderDayRowHtml:function(a,b){var c=this.view,d=["fc-row","fc-week",c.widgetContentClass];return b&&d.push("fc-rigid"),'
    '+this.renderBgTrHtml(a)+'
    '+(this.numbersVisible?""+this.renderNumberTrHtml(a)+"":"")+"
    "},renderNumberTrHtml:function(a){return""+(this.isRTL?"":this.renderNumberIntroHtml(a))+this.renderNumberCellsHtml(a)+(this.isRTL?this.renderNumberIntroHtml(a):"")+""},renderNumberIntroHtml:function(a){return this.renderIntroHtml()},renderNumberCellsHtml:function(a){var b,c,d=[];for(b=0;b'+a.date()+""):""},computeEventTimeFormat:function(){return this.view.opt("extraSmallTimeFormat")},computeDisplayEventEnd:function(){return 1==this.colCnt},rangeUpdated:function(){this.updateDayTable()},spanToSegs:function(a){var b,c,d=this.sliceRangeByRow(a);for(b=0;b');g=c&&c.row===b?c.el.position().top:h.find(".fc-content-skeleton tbody").position().top,i.css("top",g).find("table").append(d[b].tbodyEl),h.append(i),e.push(i[0])}),this.helperEls=a(e)},unrenderHelper:function(){this.helperEls&&(this.helperEls.remove(),this.helperEls=null)},fillSegTag:"td",renderFill:function(b,c,d){var e,f,g,h=[];for(c=this.renderFillSegEls(b,c),e=0;e
    '),f=e.find("tr"),h>0&&f.append(''),f.append(c.el.attr("colspan",i-h)),g>i&&f.append(''),this.bookendCells(f),e}});vb.mixin({rowStructs:null,unrenderEvents:function(){this.removeSegPopover(),tb.prototype.unrenderEvents.apply(this,arguments)},getEventSegs:function(){return tb.prototype.getEventSegs.call(this).concat(this.popoverSegs||[])},renderBgSegs:function(b){var c=a.grep(b,function(a){return a.event.allDay});return tb.prototype.renderBgSegs.call(this,c)},renderFgSegs:function(b){var c;return b=this.renderFgSegEls(b),c=this.rowStructs=this.renderSegRows(b),this.rowEls.each(function(b,d){a(d).find(".fc-content-skeleton > table").append(c[b].tbodyEl)}),b},unrenderFgSegs:function(){for(var a,b=this.rowStructs||[];a=b.pop();)a.tbodyEl.remove();this.rowStructs=null},renderSegRows:function(a){var b,c,d=[];for(b=this.groupSegRows(a),c=0;c'+ca(c)+"")),d=''+(ca(f.title||"")||" ")+"",'
    '+(this.isRTL?d+" "+l:l+" "+d)+"
    "+(h?'
    ':"")+(i?'
    ':"")+""},renderSegRow:function(b,c){function d(b){for(;b>g;)k=(r[e-1]||[])[g],k?k.attr("rowspan",parseInt(k.attr("rowspan")||1,10)+1):(k=a(""),h.append(k)),q[e][g]=k,r[e][g]=k,g++}var e,f,g,h,i,j,k,l=this.colCnt,m=this.buildSegLevels(c),n=Math.max(1,m.length),o=a(""),p=[],q=[],r=[];for(e=0;n>e;e++){if(f=m[e],g=0,h=a(""),p.push([]),q.push([]),r.push([]),f)for(i=0;i').append(j.el),j.leftCol!=j.rightCol?k.attr("colspan",j.rightCol-j.leftCol+1):r[e][g]=k;g<=j.rightCol;)q[e][g]=k,p[e][g]=j,g++;h.append(k)}d(l),this.bookendCells(h),o.append(h)}return{row:b,tbodyEl:o,cellMatrix:q,segMatrix:p,segLevels:m,segs:c}},buildSegLevels:function(a){var b,c,d,e=[];for(this.sortEventSegs(a),b=0;b td > :first-child").each(c),e.position().top+f>h)return d;return!1},limitRow:function(b,c){function d(d){for(;d>w;)j=t.getCellSegs(b,w,c),j.length&&(m=f[c-1][w],s=t.renderMoreLink(b,w,j),r=a("
    ").append(s),m.append(r),v.push(r[0])),w++}var e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t=this,u=this.rowStructs[b],v=[],w=0;if(c&&c').attr("rowspan",n),j=l[p],s=this.renderMoreLink(b,i.leftCol+p,[i].concat(j)),r=a("
    ").append(s),q.append(r),o.push(q[0]),v.push(q[0]);m.addClass("fc-limited").after(a(o)),g.push(m[0])}}d(this.colCnt),u.moreEls=a(v),u.limitedEls=a(g)}},unlimitRow:function(a){var b=this.rowStructs[a];b.moreEls&&(b.moreEls.remove(),b.moreEls=null),b.limitedEls&&(b.limitedEls.removeClass("fc-limited"),b.limitedEls=null)},renderMoreLink:function(b,c,d){var e=this,f=this.view;return a('').text(this.getMoreLinkText(d.length)).on("click",function(g){var h=f.opt("eventLimitClick"),i=e.getCellDate(b,c),j=a(this),k=e.getCellEl(b,c),l=e.getCellSegs(b,c),m=e.resliceDaySegs(l,i),n=e.resliceDaySegs(d,i);"function"==typeof h&&(h=f.trigger("eventLimitClick",null,{date:i,dayEl:k,moreEl:j,segs:m,hiddenSegs:n},g)),"popover"===h?e.showSegPopover(b,c,j,m):"string"==typeof h&&f.calendar.zoomTo(i,h)})},showSegPopover:function(a,b,c,d){var e,f,g=this,h=this.view,i=c.parent();e=1==this.rowCnt?h.el:this.rowEls.eq(a),f={className:"fc-more-popover",content:this.renderSegPopoverContent(a,b,d),parentEl:this.el,top:e.offset().top,autoHide:!0,viewportConstrain:h.opt("popoverViewportConstrain"),hide:function(){g.segPopover.removeElement(),g.segPopover=null,g.popoverSegs=null}},this.isRTL?f.right=i.offset().left+i.outerWidth()+1:f.left=i.offset().left-1,this.segPopover=new ob(f),this.segPopover.show()},renderSegPopoverContent:function(b,c,d){var e,f=this.view,g=f.opt("theme"),h=this.getCellDate(b,c).format(f.opt("dayPopoverFormat")),i=a('
    '+ca(h)+'
    '),j=i.find(".fc-event-container");for(d=this.renderFgSegEls(d,!0),this.popoverSegs=d,e=0;e'+this.renderBgTrHtml(0)+'
    '+this.renderSlatRowHtml()+"
    "},renderSlatRowHtml:function(){for(var a,c,d,e=this.view,f=this.isRTL,g="",h=b.duration(+this.minTime);h"+(c?""+ca(a.format(this.labelFormat))+"":"")+"",g+='"+(f?"":d)+''+(f?d:"")+"",h.add(this.slotDuration);return g},processOptions:function(){var c,d=this.view,e=d.opt("slotDuration"),f=d.opt("snapDuration");e=b.duration(e),f=f?b.duration(f):e,this.slotDuration=e,this.snapDuration=f,this.snapsPerSlot=e/f,this.minResizeDuration=f,this.minTime=b.duration(d.opt("minTime")),this.maxTime=b.duration(d.opt("maxTime")),c=d.opt("slotLabelFormat"),a.isArray(c)&&(c=c[c.length-1]),this.labelFormat=c||d.opt("axisFormat")||d.opt("smallTimeFormat"),c=d.opt("slotLabelInterval"),this.labelInterval=c?b.duration(c):this.computeLabelInterval(e)},computeLabelInterval:function(a){var c,d,e;for(c=Nb.length-1;c>=0;c--)if(d=b.duration(Nb[c]),e=R(d,a),ha(e)&&e>1)return d;return b.duration(a)},computeEventTimeFormat:function(){return this.view.opt("noMeridiemTimeFormat")},computeDisplayEventEnd:function(){return!0},prepareHits:function(){this.colCoordCache.build(),this.slatCoordCache.build()},releaseHits:function(){this.colCoordCache.clear()},queryHit:function(a,b){var c=this.snapsPerSlot,d=this.colCoordCache,e=this.slatCoordCache,f=d.getHorizontalIndex(a),g=e.getVerticalIndex(b);if(null!=f&&null!=g){var h=e.getTopOffset(g),i=e.getHeight(g),j=(b-h)/i,k=Math.floor(j*c),l=g*c+k,m=h+k/c*i,n=h+(k+1)/c*i;return{col:f,snap:l,component:this,left:d.getLeftOffset(f),right:d.getRightOffset(f),top:m,bottom:n}}},getHitSpan:function(a){var b,c=this.getCellDate(0,a.col),d=this.computeSnapTime(a.snap);return c.time(d),b=c.clone().add(this.snapDuration),{start:c,end:b}},getHitEl:function(a){return this.colEls.eq(a.col)},rangeUpdated:function(){this.updateDayTable()},computeSnapTime:function(a){return b.duration(this.minTime+this.snapDuration*a)},spanToSegs:function(a){var b,c=this.sliceRangeByTimes(a);for(b=0;b
    ').css("top",e).appendTo(this.colContainerEls.eq(d[c].col))[0]);d.length>0&&f.push(a('
    ').css("top",e).appendTo(this.el.find(".fc-content-skeleton"))[0]),this.nowIndicatorEls=a(f)},unrenderNowIndicator:function(){this.nowIndicatorEls&&(this.nowIndicatorEls.remove(),this.nowIndicatorEls=null)},renderSelection:function(a){this.view.opt("selectHelper")?this.renderEventLocationHelper(a):this.renderHighlight(a)},unrenderSelection:function(){this.unrenderHelper(),this.unrenderHighlight()},renderHighlight:function(a){this.renderHighlightSegs(this.spanToSegs(a))},unrenderHighlight:function(){this.unrenderHighlightSegs()}});wb.mixin({colContainerEls:null,fgContainerEls:null,bgContainerEls:null,helperContainerEls:null,highlightContainerEls:null,businessContainerEls:null,fgSegs:null,bgSegs:null,helperSegs:null,highlightSegs:null,businessSegs:null,renderContentSkeleton:function(){var b,c,d="";for(b=0;b
    ';c=a('
    '+d+"
    "),this.colContainerEls=c.find(".fc-content-col"),this.helperContainerEls=c.find(".fc-helper-container"),this.fgContainerEls=c.find(".fc-event-container:not(.fc-helper-container)"),this.bgContainerEls=c.find(".fc-bgevent-container"),this.highlightContainerEls=c.find(".fc-highlight-container"),this.businessContainerEls=c.find(".fc-business-container"),this.bookendCells(c.find("tr")),this.el.append(c)},renderFgSegs:function(a){return a=this.renderFgSegsIntoContainers(a,this.fgContainerEls),this.fgSegs=a,a},unrenderFgSegs:function(){this.unrenderNamedSegs("fgSegs")},renderHelperSegs:function(b,c){var d,e,f,g=[];for(b=this.renderFgSegsIntoContainers(b,this.helperContainerEls),d=0;d
    '+(c?'
    '+ca(c)+"
    ":"")+(g.title?'
    '+ca(g.title)+"
    ":"")+'
    '+(j?'
    ':"")+""},updateSegVerticals:function(a){this.computeSegVerticals(a),this.assignSegVerticals(a)},computeSegVerticals:function(a){var b,c;for(b=0;b1?"ll":"LL"},formatRange:function(a,b,c){var d=a.end;return d.hasTime()||(d=d.clone().subtract(1)),ta(a.start,d,b,c,this.opt("isRTL"))},setElement:function(a){this.el=a,this.bindGlobalHandlers()},removeElement:function(){this.clear(),this.isSkeletonRendered&&(this.unrenderSkeleton(),this.isSkeletonRendered=!1),this.unbindGlobalHandlers(),this.el.remove()},display:function(a,b){var c=this,d=null;return null!=b&&this.displaying&&(d=this.queryScroll()),this.calendar.freezeContentHeight(),ka(this.clear(),function(){return c.displaying=ka(c.displayView(a),function(){null!=b?c.setScroll(b):c.forceScroll(c.computeInitialScroll(d)),c.calendar.unfreezeContentHeight(),c.triggerRender()})})},clear:function(){var b=this,c=this.displaying;return c?ka(c,function(){return b.displaying=null,b.clearEvents(),b.clearView()}):a.when()},displayView:function(a){this.isSkeletonRendered||(this.renderSkeleton(),this.isSkeletonRendered=!0),a&&this.setDate(a),this.render&&this.render(),this.renderDates(),this.updateSize(),this.renderBusinessHours(),this.startNowIndicator()},clearView:function(){this.unselect(),this.stopNowIndicator(),this.triggerUnrender(),this.unrenderBusinessHours(),this.unrenderDates(),this.destroy&&this.destroy()},renderSkeleton:function(){},unrenderSkeleton:function(){},renderDates:function(){},unrenderDates:function(){},triggerRender:function(){this.trigger("viewRender",this,this,this.el)},triggerUnrender:function(){this.trigger("viewDestroy",this,this,this.el)},bindGlobalHandlers:function(){this.listenTo(a(document),"mousedown",this.handleDocumentMousedown),this.listenTo(a(document),"touchstart",this.processUnselect)},unbindGlobalHandlers:function(){this.stopListeningTo(a(document))},initThemingProps:function(){var a=this.opt("theme")?"ui":"fc";this.widgetHeaderClass=a+"-widget-header",this.widgetContentClass=a+"-widget-content",this.highlightStateClass=a+"-state-highlight"},renderBusinessHours:function(){},unrenderBusinessHours:function(){},startNowIndicator:function(){var a,c,d,e=this;this.opt("nowIndicator")&&(a=this.getNowIndicatorUnit(),a&&(c=ia(this,"updateNowIndicator"),this.initialNowDate=this.calendar.getNow(),this.initialNowQueriedMs=+new Date,this.renderNowIndicator(this.initialNowDate),this.isNowIndicatorRendered=!0,d=this.initialNowDate.clone().startOf(a).add(1,a)-this.initialNowDate,this.nowIndicatorTimeoutID=setTimeout(function(){e.nowIndicatorTimeoutID=null,c(),d=+b.duration(1,a),d=Math.max(100,d),e.nowIndicatorIntervalID=setInterval(c,d)},d)))},updateNowIndicator:function(){this.isNowIndicatorRendered&&(this.unrenderNowIndicator(),this.renderNowIndicator(this.initialNowDate.clone().add(new Date-this.initialNowQueriedMs)))},stopNowIndicator:function(){this.isNowIndicatorRendered&&(this.nowIndicatorTimeoutID&&(clearTimeout(this.nowIndicatorTimeoutID),this.nowIndicatorTimeoutID=null),this.nowIndicatorIntervalID&&(clearTimeout(this.nowIndicatorIntervalID),this.nowIndicatorIntervalID=null),this.unrenderNowIndicator(),this.isNowIndicatorRendered=!1)},getNowIndicatorUnit:function(){},renderNowIndicator:function(a){},unrenderNowIndicator:function(){},updateSize:function(a){var b;a&&(b=this.queryScroll()),this.updateHeight(a),this.updateWidth(a),this.updateNowIndicator(),a&&this.setScroll(b)},updateWidth:function(a){},updateHeight:function(a){var b=this.calendar;this.setHeight(b.getSuggestedViewHeight(),b.isHeightAuto())},setHeight:function(a,b){},computeInitialScroll:function(a){return 0},queryScroll:function(){},setScroll:function(a){},forceScroll:function(a){var b=this;this.setScroll(a),setTimeout(function(){b.setScroll(a)},0)},displayEvents:function(a){var b=this.queryScroll();this.clearEvents(),this.renderEvents(a),this.isEventsRendered=!0,this.setScroll(b),this.triggerEventRender()},clearEvents:function(){var a;this.isEventsRendered&&(a=this.queryScroll(),this.triggerEventUnrender(),this.destroyEvents&&this.destroyEvents(),this.unrenderEvents(),this.setScroll(a),this.isEventsRendered=!1)},renderEvents:function(a){},unrenderEvents:function(){},triggerEventRender:function(){this.renderedEventSegEach(function(a){this.trigger("eventAfterRender",a.event,a.event,a.el)}),this.trigger("eventAfterAllRender")},triggerEventUnrender:function(){this.renderedEventSegEach(function(a){this.trigger("eventDestroy",a.event,a.event,a.el)})},resolveEventEl:function(b,c){var d=this.trigger("eventRender",b,b,c);return d===!1?c=null:d&&d!==!0&&(c=a(d)),c},showEvent:function(a){this.renderedEventSegEach(function(a){a.el.css("visibility","")},a)},hideEvent:function(a){this.renderedEventSegEach(function(a){ +a.el.css("visibility","hidden")},a)},renderedEventSegEach:function(a,b){var c,d=this.getEventSegs();for(c=0;cb;b++)(d[b]=-1!==a.inArray(b,c))||e++;if(!e)throw"invalid hiddenDays";this.isHiddenDayHash=d},isHiddenDay:function(a){return b.isMoment(a)&&(a=a.day()),this.isHiddenDayHash[a]},skipHiddenDays:function(a,b,c){var d=a.clone();for(b=b||1;this.isHiddenDayHash[(d.day()+(c?b:0)+7)%7];)d.add(b,"days");return d},computeDayRange:function(a){var b,c=a.start.clone().stripTime(),d=a.end,e=null;return d&&(e=d.clone().stripTime(),b=+d.time(),b&&b>=this.nextDayThreshold&&e.add(1,"days")),(!d||c>=e)&&(e=c.clone().add(1,"days")),{start:c,end:e}},isMultiDayEvent:function(a){var b=this.computeDayRange(a);return b.end.diff(b.start,"days")>1}}),yb=Wa.Scroller=ya.extend({el:null,scrollEl:null,overflowX:null,overflowY:null,constructor:function(a){a=a||{},this.overflowX=a.overflowX||a.overflow||"auto",this.overflowY=a.overflowY||a.overflow||"auto"},render:function(){this.el=this.renderEl(),this.applyOverflow()},renderEl:function(){return this.scrollEl=a('
    ')},clear:function(){this.setHeight("auto"),this.applyOverflow()},destroy:function(){this.el.remove()},applyOverflow:function(){this.scrollEl.css({"overflow-x":this.overflowX,"overflow-y":this.overflowY})},lockOverflow:function(a){var b=this.overflowX,c=this.overflowY;a=a||this.getScrollbarWidths(),"auto"===b&&(b=a.top||a.bottom||this.scrollEl[0].scrollWidth-1>this.scrollEl[0].clientWidth?"scroll":"hidden"),"auto"===c&&(c=a.left||a.right||this.scrollEl[0].scrollHeight-1>this.scrollEl[0].clientHeight?"scroll":"hidden"),this.scrollEl.css({"overflow-x":b,"overflow-y":c})},setHeight:function(a){this.scrollEl.height(a)},getScrollTop:function(){return this.scrollEl.scrollTop()},setScrollTop:function(a){this.scrollEl.scrollTop(a)},getClientWidth:function(){return this.scrollEl[0].clientWidth},getClientHeight:function(){return this.scrollEl[0].clientHeight},getScrollbarWidths:function(){return q(this.scrollEl)}}),zb=Wa.Calendar=ya.extend({dirDefaults:null,langDefaults:null,overrides:null,dynamicOverrides:null,options:null,viewSpecCache:null,view:null,header:null,loadingLevel:0,constructor:Qa,initialize:function(){},populateOptionsHash:function(){var a,b,d,e;a=ba(this.dynamicOverrides.lang,this.overrides.lang),b=Ab[a],b||(a=zb.defaults.lang,b=Ab[a]||{}),d=ba(this.dynamicOverrides.isRTL,this.overrides.isRTL,b.isRTL,zb.defaults.isRTL),e=d?zb.rtlDefaults:{},this.dirDefaults=e,this.langDefaults=b,this.options=c([zb.defaults,e,b,this.overrides,this.dynamicOverrides]),Ra(this.options)},getViewSpec:function(a){var b=this.viewSpecCache;return b[a]||(b[a]=this.buildViewSpec(a))},getUnitViewSpec:function(b){var c,d,e;if(-1!=a.inArray(b,_a))for(c=this.header.getViewsWithButtons(),a.each(Wa.views,function(a){c.push(a)}),d=0;d1,this.weekNumbersVisible=this.opt("weekNumbers"),this.dayGrid.numbersVisible=this.dayNumbersVisible||this.weekNumbersVisible,this.el.addClass("fc-basic-view").html(this.renderSkeletonHtml()),this.renderHead(),this.scroller.render();var b=this.scroller.el.addClass("fc-day-grid-container"),c=a('
    ').appendTo(b);this.el.find(".fc-body > tr > td").append(b),this.dayGrid.setElement(c),this.dayGrid.renderDates(this.hasRigidRows())},renderHead:function(){this.headContainerEl=this.el.find(".fc-head-container").html(this.dayGrid.renderHeadHtml()),this.headRowEl=this.headContainerEl.find(".fc-row")},unrenderDates:function(){this.dayGrid.unrenderDates(),this.dayGrid.removeElement(),this.scroller.destroy()},renderBusinessHours:function(){this.dayGrid.renderBusinessHours()},unrenderBusinessHours:function(){this.dayGrid.unrenderBusinessHours()},renderSkeletonHtml:function(){return'
    '},weekNumberStyleAttr:function(){return null!==this.weekNumberWidth?'style="width:'+this.weekNumberWidth+'px"':""},hasRigidRows:function(){var a=this.opt("eventLimit");return a&&"number"!=typeof a},updateWidth:function(){this.weekNumbersVisible&&(this.weekNumberWidth=k(this.el.find(".fc-week-number")))},setHeight:function(a,b){var c,d,g=this.opt("eventLimit");this.scroller.clear(),f(this.headRowEl),this.dayGrid.removeSegPopover(),g&&"number"==typeof g&&this.dayGrid.limitRows(g),c=this.computeScrollerHeight(a),this.setGridHeight(c,b),g&&"number"!=typeof g&&this.dayGrid.limitRows(g),b||(this.scroller.setHeight(c),d=this.scroller.getScrollbarWidths(),(d.left||d.right)&&(e(this.headRowEl,d),c=this.computeScrollerHeight(a),this.scroller.setHeight(c)),this.scroller.lockOverflow(d))},computeScrollerHeight:function(a){return a-l(this.el,this.scroller.el)},setGridHeight:function(a,b){b?j(this.dayGrid.rowEls):i(this.dayGrid.rowEls,a,!0)},queryScroll:function(){return this.scroller.getScrollTop()},setScroll:function(a){this.scroller.setScrollTop(a)},prepareHits:function(){this.dayGrid.prepareHits()},releaseHits:function(){this.dayGrid.releaseHits()},queryHit:function(a,b){return this.dayGrid.queryHit(a,b)},getHitSpan:function(a){return this.dayGrid.getHitSpan(a)},getHitEl:function(a){return this.dayGrid.getHitEl(a)},renderEvents:function(a){this.dayGrid.renderEvents(a),this.updateHeight()},getEventSegs:function(){return this.dayGrid.getEventSegs()},unrenderEvents:function(){this.dayGrid.unrenderEvents()},renderDrag:function(a,b){return this.dayGrid.renderDrag(a,b)},unrenderDrag:function(){this.dayGrid.unrenderDrag()},renderSelection:function(a){this.dayGrid.renderSelection(a)},unrenderSelection:function(){this.dayGrid.unrenderSelection()}}),Hb={renderHeadIntroHtml:function(){var a=this.view;return a.weekNumbersVisible?'"+ca(a.opt("weekNumberTitle"))+"":""},renderNumberIntroHtml:function(a){var b=this.view;return b.weekNumbersVisible?'"+this.getCellDate(a,0).format("w")+"":""},renderBgIntroHtml:function(){var a=this.view;return a.weekNumbersVisible?'":""},renderIntroHtml:function(){var a=this.view;return a.weekNumbersVisible?'":""}},Ib=Wa.MonthView=Gb.extend({computeRange:function(a){var b,c=Gb.prototype.computeRange.call(this,a);return this.isFixedWeeks()&&(b=Math.ceil(c.end.diff(c.start,"weeks",!0)),c.end.add(6-b,"weeks")),c},setGridHeight:function(a,b){b=b||"variable"===this.opt("weekMode"),b&&(a*=this.rowCnt/6),i(this.dayGrid.rowEls,a,!b)},isFixedWeeks:function(){var a=this.opt("weekMode");return a?"fixed"===a:this.opt("fixedWeekCount")}});Xa.basic={"class":Gb},Xa.basicDay={type:"basic",duration:{days:1}},Xa.basicWeek={type:"basic",duration:{weeks:1}},Xa.month={"class":Ib,duration:{months:1},defaults:{fixedWeekCount:!0}};var Jb=Wa.AgendaView=xb.extend({scroller:null,timeGridClass:wb,timeGrid:null,dayGridClass:vb,dayGrid:null,axisWidth:null,headContainerEl:null,noScrollRowEls:null,bottomRuleEl:null,initialize:function(){this.timeGrid=this.instantiateTimeGrid(),this.opt("allDaySlot")&&(this.dayGrid=this.instantiateDayGrid()),this.scroller=new yb({overflowX:"hidden",overflowY:"auto"})},instantiateTimeGrid:function(){var a=this.timeGridClass.extend(Kb);return new a(this)},instantiateDayGrid:function(){var a=this.dayGridClass.extend(Lb);return new a(this)},setRange:function(a){xb.prototype.setRange.call(this,a),this.timeGrid.setRange(a),this.dayGrid&&this.dayGrid.setRange(a)},renderDates:function(){this.el.addClass("fc-agenda-view").html(this.renderSkeletonHtml()),this.renderHead(),this.scroller.render();var b=this.scroller.el.addClass("fc-time-grid-container"),c=a('
    ').appendTo(b);this.el.find(".fc-body > tr > td").append(b),this.timeGrid.setElement(c),this.timeGrid.renderDates(),this.bottomRuleEl=a('
    ').appendTo(this.timeGrid.el),this.dayGrid&&(this.dayGrid.setElement(this.el.find(".fc-day-grid")),this.dayGrid.renderDates(),this.dayGrid.bottomCoordPadding=this.dayGrid.el.next("hr").outerHeight()),this.noScrollRowEls=this.el.find(".fc-row:not(.fc-scroller *)")},renderHead:function(){this.headContainerEl=this.el.find(".fc-head-container").html(this.timeGrid.renderHeadHtml())},unrenderDates:function(){this.timeGrid.unrenderDates(),this.timeGrid.removeElement(),this.dayGrid&&(this.dayGrid.unrenderDates(),this.dayGrid.removeElement()),this.scroller.destroy()},renderSkeletonHtml:function(){return'
    '+(this.dayGrid?'

    ':"")+"
    "},axisStyleAttr:function(){return null!==this.axisWidth?'style="width:'+this.axisWidth+'px"':""},renderBusinessHours:function(){this.timeGrid.renderBusinessHours(),this.dayGrid&&this.dayGrid.renderBusinessHours()},unrenderBusinessHours:function(){this.timeGrid.unrenderBusinessHours(),this.dayGrid&&this.dayGrid.unrenderBusinessHours()},getNowIndicatorUnit:function(){return this.timeGrid.getNowIndicatorUnit()},renderNowIndicator:function(a){this.timeGrid.renderNowIndicator(a)},unrenderNowIndicator:function(){this.timeGrid.unrenderNowIndicator()},updateSize:function(a){this.timeGrid.updateSize(a),xb.prototype.updateSize.call(this,a)},updateWidth:function(){this.axisWidth=k(this.el.find(".fc-axis"))},setHeight:function(a,b){var c,d,g;this.bottomRuleEl.hide(),this.scroller.clear(),f(this.noScrollRowEls),this.dayGrid&&(this.dayGrid.removeSegPopover(),c=this.opt("eventLimit"),c&&"number"!=typeof c&&(c=Mb),c&&this.dayGrid.limitRows(c)),b||(d=this.computeScrollerHeight(a),this.scroller.setHeight(d),g=this.scroller.getScrollbarWidths(),(g.left||g.right)&&(e(this.noScrollRowEls,g),d=this.computeScrollerHeight(a),this.scroller.setHeight(d)),this.scroller.lockOverflow(g),this.timeGrid.getTotalSlatHeight()"+ca(a)+""):'"},renderBgIntroHtml:function(){var a=this.view;return'"},renderIntroHtml:function(){var a=this.view;return'"}},Lb={renderBgIntroHtml:function(){var a=this.view;return'"+(a.opt("allDayHtml")||ca(a.opt("allDayText")))+""},renderIntroHtml:function(){var a=this.view;return'"}},Mb=5,Nb=[{hours:1},{minutes:30},{minutes:15},{seconds:30},{seconds:15}];return Xa.agenda={"class":Jb,defaults:{allDaySlot:!0,allDayText:"all-day",slotDuration:"00:30:00",minTime:"00:00:00",maxTime:"24:00:00",slotEventOverlap:!0}},Xa.agendaDay={type:"agenda",duration:{days:1}},Xa.agendaWeek={type:"agenda",duration:{weeks:1}},Wa}); \ No newline at end of file diff --git a/public/static/libs/fullcalendar/fullcalendar.print.css b/public/static/libs/fullcalendar/fullcalendar.print.css new file mode 100644 index 0000000..60cdd84 --- /dev/null +++ b/public/static/libs/fullcalendar/fullcalendar.print.css @@ -0,0 +1,208 @@ +/*! + * FullCalendar v2.9.0 Print Stylesheet + * Docs & License: http://fullcalendar.io/ + * (c) 2016 Adam Shaw + */ + +/* + * Include this stylesheet on your page to get a more printer-friendly calendar. + * When including this stylesheet, use the media='print' attribute of the tag. + * Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css. + */ + +.fc { + max-width: 100% !important; +} + + +/* Global Event Restyling +--------------------------------------------------------------------------------------------------*/ + +.fc-event { + background: #fff !important; + color: #000 !important; + page-break-inside: avoid; +} + +.fc-event .fc-resizer { + display: none; +} + + +/* Table & Day-Row Restyling +--------------------------------------------------------------------------------------------------*/ + +th, +td, +hr, +thead, +tbody, +.fc-row { + border-color: #ccc !important; + background: #fff !important; +} + +/* kill the overlaid, absolutely-positioned components */ +/* common... */ +.fc-bg, +.fc-bgevent-skeleton, +.fc-highlight-skeleton, +.fc-helper-skeleton, +/* for timegrid. within cells within table skeletons... */ +.fc-bgevent-container, +.fc-business-container, +.fc-highlight-container, +.fc-helper-container { + display: none; +} + +/* don't force a min-height on rows (for DayGrid) */ +.fc tbody .fc-row { + height: auto !important; /* undo height that JS set in distributeHeight */ + min-height: 0 !important; /* undo the min-height from each view's specific stylesheet */ +} + +.fc tbody .fc-row .fc-content-skeleton { + position: static; /* undo .fc-rigid */ + padding-bottom: 0 !important; /* use a more border-friendly method for this... */ +} + +.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td { /* only works in newer browsers */ + padding-bottom: 1em; /* ...gives space within the skeleton. also ensures min height in a way */ +} + +.fc tbody .fc-row .fc-content-skeleton table { + /* provides a min-height for the row, but only effective for IE, which exaggerates this value, + making it look more like 3em. for other browers, it will already be this tall */ + height: 1em; +} + + +/* Undo month-view event limiting. Display all events and hide the "more" links +--------------------------------------------------------------------------------------------------*/ + +.fc-more-cell, +.fc-more { + display: none !important; +} + +.fc tr.fc-limited { + display: table-row !important; +} + +.fc td.fc-limited { + display: table-cell !important; +} + +.fc-popover { + display: none; /* never display the "more.." popover in print mode */ +} + + +/* TimeGrid Restyling +--------------------------------------------------------------------------------------------------*/ + +/* undo the min-height 100% trick used to fill the container's height */ +.fc-time-grid { + min-height: 0 !important; +} + +/* don't display the side axis at all ("all-day" and time cells) */ +.fc-agenda-view .fc-axis { + display: none; +} + +/* don't display the horizontal lines */ +.fc-slats, +.fc-time-grid hr { /* this hr is used when height is underused and needs to be filled */ + display: none !important; /* important overrides inline declaration */ +} + +/* let the container that holds the events be naturally positioned and create real height */ +.fc-time-grid .fc-content-skeleton { + position: static; +} + +/* in case there are no events, we still want some height */ +.fc-time-grid .fc-content-skeleton table { + height: 4em; +} + +/* kill the horizontal spacing made by the event container. event margins will be done below */ +.fc-time-grid .fc-event-container { + margin: 0 !important; +} + + +/* TimeGrid *Event* Restyling +--------------------------------------------------------------------------------------------------*/ + +/* naturally position events, vertically stacking them */ +.fc-time-grid .fc-event { + position: static !important; + margin: 3px 2px !important; +} + +/* for events that continue to a future day, give the bottom border back */ +.fc-time-grid .fc-event.fc-not-end { + border-bottom-width: 1px !important; +} + +/* indicate the event continues via "..." text */ +.fc-time-grid .fc-event.fc-not-end:after { + content: "..."; +} + +/* for events that are continuations from previous days, give the top border back */ +.fc-time-grid .fc-event.fc-not-start { + border-top-width: 1px !important; +} + +/* indicate the event is a continuation via "..." text */ +.fc-time-grid .fc-event.fc-not-start:before { + content: "..."; +} + +/* time */ + +/* undo a previous declaration and let the time text span to a second line */ +.fc-time-grid .fc-event .fc-time { + white-space: normal !important; +} + +/* hide the the time that is normally displayed... */ +.fc-time-grid .fc-event .fc-time span { + display: none; +} + +/* ...replace it with a more verbose version (includes AM/PM) stored in an html attribute */ +.fc-time-grid .fc-event .fc-time:after { + content: attr(data-full); +} + + +/* Vertical Scroller & Containers +--------------------------------------------------------------------------------------------------*/ + +/* kill the scrollbars and allow natural height */ +.fc-scroller, +.fc-day-grid-container, /* these divs might be assigned height, which we need to cleared */ +.fc-time-grid-container { /* */ + overflow: visible !important; + height: auto !important; +} + +/* kill the horizontal border/padding used to compensate for scrollbars */ +.fc-row { + border: 0 !important; + margin: 0 !important; +} + + +/* Button Controls +--------------------------------------------------------------------------------------------------*/ + +.fc-button-group, +.fc button { + display: none; /* don't display any button-related controls */ +} diff --git a/public/static/libs/fullcalendar/gcal.js b/public/static/libs/fullcalendar/gcal.js new file mode 100644 index 0000000..ce6ec02 --- /dev/null +++ b/public/static/libs/fullcalendar/gcal.js @@ -0,0 +1,180 @@ +/*! + * FullCalendar v2.9.0 Google Calendar Plugin + * Docs & License: http://fullcalendar.io/ + * (c) 2016 Adam Shaw + */ + +(function(factory) { + if (typeof define === 'function' && define.amd) { + define([ 'jquery' ], factory); + } + else if (typeof exports === 'object') { // Node/CommonJS + module.exports = factory(require('jquery')); + } + else { + factory(jQuery); + } +})(function($) { + + +var API_BASE = 'https://www.googleapis.com/calendar/v3/calendars'; +var FC = $.fullCalendar; +var applyAll = FC.applyAll; + + +FC.sourceNormalizers.push(function(sourceOptions) { + var googleCalendarId = sourceOptions.googleCalendarId; + var url = sourceOptions.url; + var match; + + // if the Google Calendar ID hasn't been explicitly defined + if (!googleCalendarId && url) { + + // detect if the ID was specified as a single string. + // will match calendars like "asdf1234@calendar.google.com" in addition to person email calendars. + if (/^[^\/]+@([^\/\.]+\.)*(google|googlemail|gmail)\.com$/.test(url)) { + googleCalendarId = url; + } + // try to scrape it out of a V1 or V3 API feed URL + else if ( + (match = /^https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/([^\/]*)/.exec(url)) || + (match = /^https?:\/\/www.google.com\/calendar\/feeds\/([^\/]*)/.exec(url)) + ) { + googleCalendarId = decodeURIComponent(match[1]); + } + + if (googleCalendarId) { + sourceOptions.googleCalendarId = googleCalendarId; + } + } + + + if (googleCalendarId) { // is this a Google Calendar? + + // make each Google Calendar source uneditable by default + if (sourceOptions.editable == null) { + sourceOptions.editable = false; + } + + // We want removeEventSource to work, but it won't know about the googleCalendarId primitive. + // Shoehorn it into the url, which will function as the unique primitive. Won't cause side effects. + // This hack is obsolete since 2.2.3, but keep it so this plugin file is compatible with old versions. + sourceOptions.url = googleCalendarId; + } +}); + + +FC.sourceFetchers.push(function(sourceOptions, start, end, timezone) { + if (sourceOptions.googleCalendarId) { + return transformOptions(sourceOptions, start, end, timezone, this); // `this` is the calendar + } +}); + + +function transformOptions(sourceOptions, start, end, timezone, calendar) { + var url = API_BASE + '/' + encodeURIComponent(sourceOptions.googleCalendarId) + '/events?callback=?'; // jsonp + var apiKey = sourceOptions.googleCalendarApiKey || calendar.options.googleCalendarApiKey; + var success = sourceOptions.success; + var data; + var timezoneArg; // populated when a specific timezone. escaped to Google's liking + + function reportError(message, apiErrorObjs) { + var errorObjs = apiErrorObjs || [ { message: message } ]; // to be passed into error handlers + + // call error handlers + (sourceOptions.googleCalendarError || $.noop).apply(calendar, errorObjs); + (calendar.options.googleCalendarError || $.noop).apply(calendar, errorObjs); + + // print error to debug console + FC.warn.apply(null, [ message ].concat(apiErrorObjs || [])); + } + + if (!apiKey) { + reportError("Specify a googleCalendarApiKey. See http://fullcalendar.io/docs/google_calendar/"); + return {}; // an empty source to use instead. won't fetch anything. + } + + // The API expects an ISO8601 datetime with a time and timezone part. + // Since the calendar's timezone offset isn't always known, request the date in UTC and pad it by a day on each + // side, guaranteeing we will receive all events in the desired range, albeit a superset. + // .utc() will set a zone and give it a 00:00:00 time. + if (!start.hasZone()) { + start = start.clone().utc().add(-1, 'day'); + } + if (!end.hasZone()) { + end = end.clone().utc().add(1, 'day'); + } + + // when sending timezone names to Google, only accepts underscores, not spaces + if (timezone && timezone != 'local') { + timezoneArg = timezone.replace(' ', '_'); + } + + data = $.extend({}, sourceOptions.data || {}, { + key: apiKey, + timeMin: start.format(), + timeMax: end.format(), + timeZone: timezoneArg, + singleEvents: true, + maxResults: 9999 + }); + + return $.extend({}, sourceOptions, { + googleCalendarId: null, // prevents source-normalizing from happening again + url: url, + data: data, + startParam: false, // `false` omits this parameter. we already included it above + endParam: false, // same + timezoneParam: false, // same + success: function(data) { + var events = []; + var successArgs; + var successRes; + + if (data.error) { + reportError('Google Calendar API: ' + data.error.message, data.error.errors); + } + else if (data.items) { + $.each(data.items, function(i, entry) { + var url = entry.htmlLink || null; + + // make the URLs for each event show times in the correct timezone + if (timezoneArg && url !== null) { + url = injectQsComponent(url, 'ctz=' + timezoneArg); + } + + events.push({ + id: entry.id, + title: entry.summary, + start: entry.start.dateTime || entry.start.date, // try timed. will fall back to all-day + end: entry.end.dateTime || entry.end.date, // same + url: url, + location: entry.location, + description: entry.description + }); + }); + + // call the success handler(s) and allow it to return a new events array + successArgs = [ events ].concat(Array.prototype.slice.call(arguments, 1)); // forward other jq args + successRes = applyAll(success, this, successArgs); + if ($.isArray(successRes)) { + return successRes; + } + } + + return events; + } + }); +} + + +// Injects a string like "arg=value" into the querystring of a URL +function injectQsComponent(url, component) { + // inject it after the querystring but before the fragment + return url.replace(/(\?.*?)?(#|$)/, function(whole, qs, hash) { + return (qs ? qs + '&' : '?') + component + hash; + }); +} + + +}); diff --git a/public/static/libs/fullcalendar/gcal.min.js b/public/static/libs/fullcalendar/gcal.min.js new file mode 100644 index 0000000..62aea7c --- /dev/null +++ b/public/static/libs/fullcalendar/gcal.min.js @@ -0,0 +1,6 @@ +/*! + * FullCalendar v2.9.0 Google Calendar Plugin + * Docs & License: http://fullcalendar.io/ + * (c) 2016 Adam Shaw + */ +!function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?module.exports=e(require("jquery")):e(jQuery)}(function(e){function a(a,t,d,c,i){function s(o,r){var l=r||[{message:o}];(a.googleCalendarError||e.noop).apply(i,l),(i.options.googleCalendarError||e.noop).apply(i,l),n.warn.apply(null,[o].concat(r||[]))}var u,g,p=r+"/"+encodeURIComponent(a.googleCalendarId)+"/events?callback=?",m=a.googleCalendarApiKey||i.options.googleCalendarApiKey,f=a.success;return m?(t.hasZone()||(t=t.clone().utc().add(-1,"day")),d.hasZone()||(d=d.clone().utc().add(1,"day")),c&&"local"!=c&&(g=c.replace(" ","_")),u=e.extend({},a.data||{},{key:m,timeMin:t.format(),timeMax:d.format(),timeZone:g,singleEvents:!0,maxResults:9999}),e.extend({},a,{googleCalendarId:null,url:p,data:u,startParam:!1,endParam:!1,timezoneParam:!1,success:function(a){var r,n,t=[];if(a.error)s("Google Calendar API: "+a.error.message,a.error.errors);else if(a.items&&(e.each(a.items,function(e,a){var r=a.htmlLink||null;g&&null!==r&&(r=o(r,"ctz="+g)),t.push({id:a.id,title:a.summary,start:a.start.dateTime||a.start.date,end:a.end.dateTime||a.end.date,url:r,location:a.location,description:a.description})}),r=[t].concat(Array.prototype.slice.call(arguments,1)),n=l(f,this,r),e.isArray(n)))return n;return t}})):(s("Specify a googleCalendarApiKey. See http://fullcalendar.io/docs/google_calendar/"),{})}function o(e,a){return e.replace(/(\?.*?)?(#|$)/,function(e,o,r){return(o?o+"&":"?")+a+r})}var r="https://www.googleapis.com/calendar/v3/calendars",n=e.fullCalendar,l=n.applyAll;n.sourceNormalizers.push(function(e){var a,o=e.googleCalendarId,r=e.url;!o&&r&&(/^[^\/]+@([^\/\.]+\.)*(google|googlemail|gmail)\.com$/.test(r)?o=r:((a=/^https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/([^\/]*)/.exec(r))||(a=/^https?:\/\/www.google.com\/calendar\/feeds\/([^\/]*)/.exec(r)))&&(o=decodeURIComponent(a[1])),o&&(e.googleCalendarId=o)),o&&(null==e.editable&&(e.editable=!1),e.url=o)}),n.sourceFetchers.push(function(e,o,r,n){return e.googleCalendarId?a(e,o,r,n,this):void 0})}); diff --git a/public/static/libs/fullcalendar/lang-all.js b/public/static/libs/fullcalendar/lang-all.js new file mode 100644 index 0000000..4309746 --- /dev/null +++ b/public/static/libs/fullcalendar/lang-all.js @@ -0,0 +1,4 @@ +!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):"object"==typeof exports?module.exports=a(require("jquery"),require("moment")):a(jQuery,moment)}(function(a,b){!function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"ar-ma",{months:"يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر".split("_"),monthsShort:"يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر".split("_"),weekdays:"الأحد_الإتنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"احد_اتنين_ثلاثاء_اربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[اليوم على الساعة] LT",nextDay:"[غدا على الساعة] LT",nextWeek:"dddd [على الساعة] LT",lastDay:"[أمس على الساعة] LT",lastWeek:"dddd [على الساعة] LT",sameElse:"L"},relativeTime:{future:"في %s",past:"منذ %s",s:"ثوان",m:"دقيقة",mm:"%d دقائق",h:"ساعة",hh:"%d ساعات",d:"يوم",dd:"%d أيام",M:"شهر",MM:"%d أشهر",y:"سنة",yy:"%d سنوات"},week:{dow:6,doy:12}});return a}(),a.fullCalendar.datepickerLang("ar-ma","ar",{closeText:"إغلاق",prevText:"<السابق",nextText:"التالي>",currentText:"اليوم",monthNames:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ar-ma",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى"})}(),function(){!function(){"use strict";var a={1:"١",2:"٢",3:"٣",4:"٤",5:"٥",6:"٦",7:"٧",8:"٨",9:"٩",0:"٠"},c={"١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9","٠":"0"},d=(b.defineLocale||b.lang).call(b,"ar-sa",{months:"يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),monthsShort:"يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/ص|م/,isPM:function(a){return"م"===a},meridiem:function(a,b,c){return 12>a?"ص":"م"},calendar:{sameDay:"[اليوم على الساعة] LT",nextDay:"[غدا على الساعة] LT",nextWeek:"dddd [على الساعة] LT",lastDay:"[أمس على الساعة] LT",lastWeek:"dddd [على الساعة] LT",sameElse:"L"},relativeTime:{future:"في %s",past:"منذ %s",s:"ثوان",m:"دقيقة",mm:"%d دقائق",h:"ساعة",hh:"%d ساعات",d:"يوم",dd:"%d أيام",M:"شهر",MM:"%d أشهر",y:"سنة",yy:"%d سنوات"},preparse:function(a){return a.replace(/[١٢٣٤٥٦٧٨٩٠]/g,function(a){return c[a]}).replace(/،/g,",")},postformat:function(b){return b.replace(/\d/g,function(b){return a[b]}).replace(/,/g,"،")},week:{dow:6,doy:12}});return d}(),a.fullCalendar.datepickerLang("ar-sa","ar",{closeText:"إغلاق",prevText:"<السابق",nextText:"التالي>",currentText:"اليوم",monthNames:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ar-sa",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"ar-tn",{months:"جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),monthsShort:"جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[اليوم على الساعة] LT",nextDay:"[غدا على الساعة] LT",nextWeek:"dddd [على الساعة] LT",lastDay:"[أمس على الساعة] LT",lastWeek:"dddd [على الساعة] LT",sameElse:"L"},relativeTime:{future:"في %s",past:"منذ %s",s:"ثوان",m:"دقيقة",mm:"%d دقائق",h:"ساعة",hh:"%d ساعات",d:"يوم",dd:"%d أيام",M:"شهر",MM:"%d أشهر",y:"سنة",yy:"%d سنوات"},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("ar-tn","ar",{closeText:"إغلاق",prevText:"<السابق",nextText:"التالي>",currentText:"اليوم",monthNames:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ar-tn",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى"})}(),function(){!function(){"use strict";var a={1:"١",2:"٢",3:"٣",4:"٤",5:"٥",6:"٦",7:"٧",8:"٨",9:"٩",0:"٠"},c={"١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9","٠":"0"},d=function(a){return 0===a?0:1===a?1:2===a?2:a%100>=3&&10>=a%100?3:a%100>=11?4:5},e={s:["أقل من ثانية","ثانية واحدة",["ثانيتان","ثانيتين"],"%d ثوان","%d ثانية","%d ثانية"],m:["أقل من دقيقة","دقيقة واحدة",["دقيقتان","دقيقتين"],"%d دقائق","%d دقيقة","%d دقيقة"],h:["أقل من ساعة","ساعة واحدة",["ساعتان","ساعتين"],"%d ساعات","%d ساعة","%d ساعة"],d:["أقل من يوم","يوم واحد",["يومان","يومين"],"%d أيام","%d يومًا","%d يوم"],M:["أقل من شهر","شهر واحد",["شهران","شهرين"],"%d أشهر","%d شهرا","%d شهر"],y:["أقل من عام","عام واحد",["عامان","عامين"],"%d أعوام","%d عامًا","%d عام"]},f=function(a){return function(b,c,f,g){var h=d(b),i=e[a][d(b)];return 2===h&&(i=i[c?0:1]),i.replace(/%d/i,b)}},g=["كانون الثاني يناير","شباط فبراير","آذار مارس","نيسان أبريل","أيار مايو","حزيران يونيو","تموز يوليو","آب أغسطس","أيلول سبتمبر","تشرين الأول أكتوبر","تشرين الثاني نوفمبر","كانون الأول ديسمبر"],h=(b.defineLocale||b.lang).call(b,"ar",{months:g,monthsShort:g,weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"D/‏M/‏YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/ص|م/,isPM:function(a){return"م"===a},meridiem:function(a,b,c){return 12>a?"ص":"م"},calendar:{sameDay:"[اليوم عند الساعة] LT",nextDay:"[غدًا عند الساعة] LT",nextWeek:"dddd [عند الساعة] LT",lastDay:"[أمس عند الساعة] LT",lastWeek:"dddd [عند الساعة] LT",sameElse:"L"},relativeTime:{future:"بعد %s",past:"منذ %s",s:f("s"),m:f("m"),mm:f("m"),h:f("h"),hh:f("h"),d:f("d"),dd:f("d"),M:f("M"),MM:f("M"),y:f("y"),yy:f("y")},preparse:function(a){return a.replace(/\u200f/g,"").replace(/[١٢٣٤٥٦٧٨٩٠]/g,function(a){return c[a]}).replace(/،/g,",")},postformat:function(b){return b.replace(/\d/g,function(b){return a[b]}).replace(/,/g,"،")},week:{dow:6,doy:12}});return h}(),a.fullCalendar.datepickerLang("ar","ar",{closeText:"إغلاق",prevText:"<السابق",nextText:"التالي>",currentText:"اليوم",monthNames:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ar",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"bg",{months:"януари_февруари_март_април_май_юни_юли_август_септември_октомври_ноември_декември".split("_"),monthsShort:"янр_фев_мар_апр_май_юни_юли_авг_сеп_окт_ное_дек".split("_"),weekdays:"неделя_понеделник_вторник_сряда_четвъртък_петък_събота".split("_"),weekdaysShort:"нед_пон_вто_сря_чет_пет_съб".split("_"),weekdaysMin:"нд_пн_вт_ср_чт_пт_сб".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"D.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[Днес в] LT",nextDay:"[Утре в] LT",nextWeek:"dddd [в] LT",lastDay:"[Вчера в] LT",lastWeek:function(){switch(this.day()){case 0:case 3:case 6:return"[В изминалата] dddd [в] LT";case 1:case 2:case 4:case 5:return"[В изминалия] dddd [в] LT"}},sameElse:"L"},relativeTime:{future:"след %s",past:"преди %s",s:"няколко секунди",m:"минута",mm:"%d минути",h:"час",hh:"%d часа",d:"ден",dd:"%d дни",M:"месец",MM:"%d месеца",y:"година",yy:"%d години"},ordinalParse:/\d{1,2}-(ев|ен|ти|ви|ри|ми)/,ordinal:function(a){var b=a%10,c=a%100;return 0===a?a+"-ев":0===c?a+"-ен":c>10&&20>c?a+"-ти":1===b?a+"-ви":2===b?a+"-ри":7===b||8===b?a+"-ми":a+"-ти"},week:{dow:1,doy:7}});return a}(),a.fullCalendar.datepickerLang("bg","bg",{closeText:"затвори",prevText:"<назад",nextText:"напред>",nextBigText:">>",currentText:"днес",monthNames:["Януари","Февруари","Март","Април","Май","Юни","Юли","Август","Септември","Октомври","Ноември","Декември"],monthNamesShort:["Яну","Фев","Мар","Апр","Май","Юни","Юли","Авг","Сеп","Окт","Нов","Дек"],dayNames:["Неделя","Понеделник","Вторник","Сряда","Четвъртък","Петък","Събота"],dayNamesShort:["Нед","Пон","Вто","Сря","Чет","Пет","Съб"],dayNamesMin:["Не","По","Вт","Ср","Че","Пе","Съ"],weekHeader:"Wk",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("bg",{buttonText:{month:"Месец",week:"Седмица",day:"Ден",list:"График"},allDayText:"Цял ден",eventLimitText:function(a){return"+още "+a}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"ca",{months:"gener_febrer_març_abril_maig_juny_juliol_agost_setembre_octubre_novembre_desembre".split("_"),monthsShort:"gen._febr._mar._abr._mai._jun._jul._ag._set._oct._nov._des.".split("_"),monthsParseExact:!0,weekdays:"diumenge_dilluns_dimarts_dimecres_dijous_divendres_dissabte".split("_"),weekdaysShort:"dg._dl._dt._dc._dj._dv._ds.".split("_"),weekdaysMin:"Dg_Dl_Dt_Dc_Dj_Dv_Ds".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd D MMMM YYYY H:mm"},calendar:{sameDay:function(){return"[avui a "+(1!==this.hours()?"les":"la")+"] LT"},nextDay:function(){return"[demà a "+(1!==this.hours()?"les":"la")+"] LT"},nextWeek:function(){return"dddd [a "+(1!==this.hours()?"les":"la")+"] LT"},lastDay:function(){return"[ahir a "+(1!==this.hours()?"les":"la")+"] LT"},lastWeek:function(){return"[el] dddd [passat a "+(1!==this.hours()?"les":"la")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"fa %s",s:"uns segons",m:"un minut",mm:"%d minuts",h:"una hora",hh:"%d hores",d:"un dia",dd:"%d dies",M:"un mes",MM:"%d mesos",y:"un any",yy:"%d anys"},ordinalParse:/\d{1,2}(r|n|t|è|a)/,ordinal:function(a,b){var c=1===a?"r":2===a?"n":3===a?"r":4===a?"t":"è";return"w"!==b&&"W"!==b||(c="a"),a+c},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("ca","ca",{closeText:"Tanca",prevText:"Anterior",nextText:"Següent",currentText:"Avui",monthNames:["gener","febrer","març","abril","maig","juny","juliol","agost","setembre","octubre","novembre","desembre"],monthNamesShort:["gen","feb","març","abr","maig","juny","jul","ag","set","oct","nov","des"],dayNames:["diumenge","dilluns","dimarts","dimecres","dijous","divendres","dissabte"],dayNamesShort:["dg","dl","dt","dc","dj","dv","ds"],dayNamesMin:["dg","dl","dt","dc","dj","dv","ds"],weekHeader:"Set",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ca",{buttonText:{month:"Mes",week:"Setmana",day:"Dia",list:"Agenda"},allDayText:"Tot el dia",eventLimitText:"més"})}(),function(){!function(){"use strict";function a(a){return a>1&&5>a&&1!==~~(a/10)}function c(b,c,d,e){var f=b+" ";switch(d){case"s":return c||e?"pár sekund":"pár sekundami";case"m":return c?"minuta":e?"minutu":"minutou";case"mm":return c||e?f+(a(b)?"minuty":"minut"):f+"minutami";case"h":return c?"hodina":e?"hodinu":"hodinou";case"hh":return c||e?f+(a(b)?"hodiny":"hodin"):f+"hodinami";case"d":return c||e?"den":"dnem";case"dd":return c||e?f+(a(b)?"dny":"dní"):f+"dny";case"M":return c||e?"měsíc":"měsícem";case"MM":return c||e?f+(a(b)?"měsíce":"měsíců"):f+"měsíci";case"y":return c||e?"rok":"rokem";case"yy":return c||e?f+(a(b)?"roky":"let"):f+"lety"}}var d="leden_únor_březen_duben_květen_červen_červenec_srpen_září_říjen_listopad_prosinec".split("_"),e="led_úno_bře_dub_kvě_čvn_čvc_srp_zář_říj_lis_pro".split("_"),f=(b.defineLocale||b.lang).call(b,"cs",{months:d,monthsShort:e,monthsParse:function(a,b){var c,d=[];for(c=0;12>c;c++)d[c]=new RegExp("^"+a[c]+"$|^"+b[c]+"$","i");return d}(d,e),shortMonthsParse:function(a){var b,c=[];for(b=0;12>b;b++)c[b]=new RegExp("^"+a[b]+"$","i");return c}(e),longMonthsParse:function(a){var b,c=[];for(b=0;12>b;b++)c[b]=new RegExp("^"+a[b]+"$","i");return c}(d),weekdays:"neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota".split("_"),weekdaysShort:"ne_po_út_st_čt_pá_so".split("_"),weekdaysMin:"ne_po_út_st_čt_pá_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd D. MMMM YYYY H:mm"},calendar:{sameDay:"[dnes v] LT",nextDay:"[zítra v] LT",nextWeek:function(){switch(this.day()){case 0:return"[v neděli v] LT";case 1:case 2:return"[v] dddd [v] LT";case 3:return"[ve středu v] LT";case 4:return"[ve čtvrtek v] LT";case 5:return"[v pátek v] LT";case 6:return"[v sobotu v] LT"}},lastDay:"[včera v] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulou neděli v] LT";case 1:case 2:return"[minulé] dddd [v] LT";case 3:return"[minulou středu v] LT";case 4:case 5:return"[minulý] dddd [v] LT";case 6:return"[minulou sobotu v] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"před %s",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return f}(),a.fullCalendar.datepickerLang("cs","cs",{closeText:"Zavřít",prevText:"<Dříve",nextText:"Později>",currentText:"Nyní",monthNames:["leden","únor","březen","duben","květen","červen","červenec","srpen","září","říjen","listopad","prosinec"],monthNamesShort:["led","úno","bře","dub","kvě","čer","čvc","srp","zář","říj","lis","pro"],dayNames:["neděle","pondělí","úterý","středa","čtvrtek","pátek","sobota"],dayNamesShort:["ne","po","út","st","čt","pá","so"],dayNamesMin:["ne","po","út","st","čt","pá","so"],weekHeader:"Týd",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("cs",{buttonText:{month:"Měsíc",week:"Týden",day:"Den",list:"Agenda"},allDayText:"Celý den",eventLimitText:function(a){return"+další: "+a}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"da",{months:"januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tir_ons_tor_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd [d.] D. MMMM YYYY HH:mm"},calendar:{sameDay:"[I dag kl.] LT",nextDay:"[I morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[I går kl.] LT",lastWeek:"[sidste] dddd [kl] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"få sekunder",m:"et minut",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dage",M:"en måned",MM:"%d måneder",y:"et år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("da","da",{closeText:"Luk",prevText:"<Forrige",nextText:"Næste>",currentText:"Idag",monthNames:["Januar","Februar","Marts","April","Maj","Juni","Juli","August","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNames:["Søndag","Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag"],dayNamesShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør"],dayNamesMin:["Sø","Ma","Ti","On","To","Fr","Lø"],weekHeader:"Uge",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("da",{buttonText:{month:"Måned",week:"Uge",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"flere"})}(),function(){!function(){"use strict";function a(a,b,c,d){var e={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[a+" Tage",a+" Tagen"],M:["ein Monat","einem Monat"],MM:[a+" Monate",a+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[a+" Jahre",a+" Jahren"]};return b?e[c][0]:e[c][1]}var c=(b.defineLocale||b.lang).call(b,"de-at",{months:"Jänner_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jän._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd, D. MMMM YYYY HH:mm"},calendar:{sameDay:"[heute um] LT [Uhr]",sameElse:"L",nextDay:"[morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",m:a,mm:"%d Minuten",h:a,hh:"%d Stunden",d:a,dd:a,M:a,MM:a,y:a,yy:a},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return c}(),a.fullCalendar.datepickerLang("de-at","de",{closeText:"Schließen",prevText:"<Zurück",nextText:"Vor>",currentText:"Heute",monthNames:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthNamesShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],dayNames:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],dayNamesShort:["So","Mo","Di","Mi","Do","Fr","Sa"],dayNamesMin:["So","Mo","Di","Mi","Do","Fr","Sa"],weekHeader:"KW",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("de-at",{buttonText:{month:"Monat",week:"Woche",day:"Tag",list:"Terminübersicht"},allDayText:"Ganztägig",eventLimitText:function(a){return"+ weitere "+a}})}(),function(){!function(){"use strict";function a(a,b,c,d){var e={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[a+" Tage",a+" Tagen"],M:["ein Monat","einem Monat"],MM:[a+" Monate",a+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[a+" Jahre",a+" Jahren"]};return b?e[c][0]:e[c][1]}var c=(b.defineLocale||b.lang).call(b,"de",{months:"Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd, D. MMMM YYYY HH:mm"},calendar:{sameDay:"[heute um] LT [Uhr]",sameElse:"L",nextDay:"[morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",m:a,mm:"%d Minuten",h:a,hh:"%d Stunden",d:a,dd:a,M:a,MM:a,y:a,yy:a},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return c}(),a.fullCalendar.datepickerLang("de","de",{closeText:"Schließen",prevText:"<Zurück",nextText:"Vor>",currentText:"Heute",monthNames:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthNamesShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],dayNames:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],dayNamesShort:["So","Mo","Di","Mi","Do","Fr","Sa"],dayNamesMin:["So","Mo","Di","Mi","Do","Fr","Sa"],weekHeader:"KW",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("de",{buttonText:{month:"Monat",week:"Woche",day:"Tag",list:"Terminübersicht"},allDayText:"Ganztägig",eventLimitText:function(a){return"+ weitere "+a}})}(),function(){!function(){"use strict";function a(a){return a instanceof Function||"[object Function]"===Object.prototype.toString.call(a)}var c=(b.defineLocale||b.lang).call(b,"el",{monthsNominativeEl:"Ιανουάριος_Φεβρουάριος_Μάρτιος_Απρίλιος_Μάιος_Ιούνιος_Ιούλιος_Αύγουστος_Σεπτέμβριος_Οκτώβριος_Νοέμβριος_Δεκέμβριος".split("_"),monthsGenitiveEl:"Ιανουαρίου_Φεβρουαρίου_Μαρτίου_Απριλίου_Μαΐου_Ιουνίου_Ιουλίου_Αυγούστου_Σεπτεμβρίου_Οκτωβρίου_Νοεμβρίου_Δεκεμβρίου".split("_"),months:function(a,b){return/D/.test(b.substring(0,b.indexOf("MMMM")))?this._monthsGenitiveEl[a.month()]:this._monthsNominativeEl[a.month()]},monthsShort:"Ιαν_Φεβ_Μαρ_Απρ_Μαϊ_Ιουν_Ιουλ_Αυγ_Σεπ_Οκτ_Νοε_Δεκ".split("_"),weekdays:"Κυριακή_Δευτέρα_Τρίτη_Τετάρτη_Πέμπτη_Παρασκευή_Σάββατο".split("_"),weekdaysShort:"Κυρ_Δευ_Τρι_Τετ_Πεμ_Παρ_Σαβ".split("_"),weekdaysMin:"Κυ_Δε_Τρ_Τε_Πε_Πα_Σα".split("_"),meridiem:function(a,b,c){return a>11?c?"μμ":"ΜΜ":c?"πμ":"ΠΜ"},isPM:function(a){return"μ"===(a+"").toLowerCase()[0]},meridiemParse:/[ΠΜ]\.?Μ?\.?/i,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendarEl:{sameDay:"[Σήμερα {}] LT",nextDay:"[Αύριο {}] LT",nextWeek:"dddd [{}] LT",lastDay:"[Χθες {}] LT",lastWeek:function(){switch(this.day()){case 6:return"[το προηγούμενο] dddd [{}] LT";default:return"[την προηγούμενη] dddd [{}] LT"}},sameElse:"L"},calendar:function(b,c){var d=this._calendarEl[b],e=c&&c.hours();return a(d)&&(d=d.apply(c)),d.replace("{}",e%12===1?"στη":"στις")},relativeTime:{future:"σε %s",past:"%s πριν",s:"λίγα δευτερόλεπτα",m:"ένα λεπτό",mm:"%d λεπτά",h:"μία ώρα",hh:"%d ώρες",d:"μία μέρα",dd:"%d μέρες",M:"ένας μήνας",MM:"%d μήνες",y:"ένας χρόνος",yy:"%d χρόνια"},ordinalParse:/\d{1,2}η/,ordinal:"%dη",week:{dow:1,doy:4}});return c}(),a.fullCalendar.datepickerLang("el","el",{closeText:"Κλείσιμο",prevText:"Προηγούμενος",nextText:"Επόμενος",currentText:"Σήμερα",monthNames:["Ιανουάριος","Φεβρουάριος","Μάρτιος","Απρίλιος","Μάιος","Ιούνιος","Ιούλιος","Αύγουστος","Σεπτέμβριος","Οκτώβριος","Νοέμβριος","Δεκέμβριος"],monthNamesShort:["Ιαν","Φεβ","Μαρ","Απρ","Μαι","Ιουν","Ιουλ","Αυγ","Σεπ","Οκτ","Νοε","Δεκ"],dayNames:["Κυριακή","Δευτέρα","Τρίτη","Τετάρτη","Πέμπτη","Παρασκευή","Σάββατο"],dayNamesShort:["Κυρ","Δευ","Τρι","Τετ","Πεμ","Παρ","Σαβ"],dayNamesMin:["Κυ","Δε","Τρ","Τε","Πε","Πα","Σα"],weekHeader:"Εβδ",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("el",{buttonText:{month:"Μήνας",week:"Εβδομάδα",day:"Ημέρα",list:"Ατζέντα"},allDayText:"Ολοήμερο",eventLimitText:"περισσότερα"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"en-au",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("en-au","en-AU",{closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("en-au")}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"en-ca",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"YYYY-MM-DD",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}});return a}(),a.fullCalendar.lang("en-ca")}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"en-gb",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("en-gb","en-GB",{closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("en-gb")}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"en-ie",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c},week:{dow:1,doy:4}});return a}(),a.fullCalendar.lang("en-ie")}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"en-nz",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("en-nz","en-NZ",{closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("en-nz")}(),function(){!function(){"use strict";var a="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),c="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_"),d=(b.defineLocale||b.lang).call(b,"es",{ +months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(b,d){return/-MMM-/.test(d)?c[b.month()]:a[b.month()]},monthsParseExact:!0,weekdays:"domingo_lunes_martes_miércoles_jueves_viernes_sábado".split("_"),weekdaysShort:"dom._lun._mar._mié._jue._vie._sáb.".split("_"),weekdaysMin:"do_lu_ma_mi_ju_vi_sá".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY H:mm",LLLL:"dddd, D [de] MMMM [de] YYYY H:mm"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[mañana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un día",dd:"%d días",M:"un mes",MM:"%d meses",y:"un año",yy:"%d años"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}});return d}(),a.fullCalendar.datepickerLang("es","es",{closeText:"Cerrar",prevText:"<Ant",nextText:"Sig>",currentText:"Hoy",monthNames:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],monthNamesShort:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],dayNames:["domingo","lunes","martes","miércoles","jueves","viernes","sábado"],dayNamesShort:["dom","lun","mar","mié","jue","vie","sáb"],dayNamesMin:["D","L","M","X","J","V","S"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("es",{buttonText:{month:"Mes",week:"Semana",day:"Día",list:"Agenda"},allDayHtml:"Todo
    el día",eventLimitText:"más"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"eu",{months:"urtarrila_otsaila_martxoa_apirila_maiatza_ekaina_uztaila_abuztua_iraila_urria_azaroa_abendua".split("_"),monthsShort:"urt._ots._mar._api._mai._eka._uzt._abu._ira._urr._aza._abe.".split("_"),monthsParseExact:!0,weekdays:"igandea_astelehena_asteartea_asteazkena_osteguna_ostirala_larunbata".split("_"),weekdaysShort:"ig._al._ar._az._og._ol._lr.".split("_"),weekdaysMin:"ig_al_ar_az_og_ol_lr".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"YYYY[ko] MMMM[ren] D[a]",LLL:"YYYY[ko] MMMM[ren] D[a] HH:mm",LLLL:"dddd, YYYY[ko] MMMM[ren] D[a] HH:mm",l:"YYYY-M-D",ll:"YYYY[ko] MMM D[a]",lll:"YYYY[ko] MMM D[a] HH:mm",llll:"ddd, YYYY[ko] MMM D[a] HH:mm"},calendar:{sameDay:"[gaur] LT[etan]",nextDay:"[bihar] LT[etan]",nextWeek:"dddd LT[etan]",lastDay:"[atzo] LT[etan]",lastWeek:"[aurreko] dddd LT[etan]",sameElse:"L"},relativeTime:{future:"%s barru",past:"duela %s",s:"segundo batzuk",m:"minutu bat",mm:"%d minutu",h:"ordu bat",hh:"%d ordu",d:"egun bat",dd:"%d egun",M:"hilabete bat",MM:"%d hilabete",y:"urte bat",yy:"%d urte"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});return a}(),a.fullCalendar.datepickerLang("eu","eu",{closeText:"Egina",prevText:"<Aur",nextText:"Hur>",currentText:"Gaur",monthNames:["urtarrila","otsaila","martxoa","apirila","maiatza","ekaina","uztaila","abuztua","iraila","urria","azaroa","abendua"],monthNamesShort:["urt.","ots.","mar.","api.","mai.","eka.","uzt.","abu.","ira.","urr.","aza.","abe."],dayNames:["igandea","astelehena","asteartea","asteazkena","osteguna","ostirala","larunbata"],dayNamesShort:["ig.","al.","ar.","az.","og.","ol.","lr."],dayNamesMin:["ig","al","ar","az","og","ol","lr"],weekHeader:"As",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("eu",{buttonText:{month:"Hilabetea",week:"Astea",day:"Eguna",list:"Agenda"},allDayHtml:"Egun
    osoa",eventLimitText:"gehiago"})}(),function(){!function(){"use strict";var a={1:"۱",2:"۲",3:"۳",4:"۴",5:"۵",6:"۶",7:"۷",8:"۸",9:"۹",0:"۰"},c={"۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9","۰":"0"},d=(b.defineLocale||b.lang).call(b,"fa",{months:"ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر".split("_"),monthsShort:"ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر".split("_"),weekdays:"یک‌شنبه_دوشنبه_سه‌شنبه_چهارشنبه_پنج‌شنبه_جمعه_شنبه".split("_"),weekdaysShort:"یک‌شنبه_دوشنبه_سه‌شنبه_چهارشنبه_پنج‌شنبه_جمعه_شنبه".split("_"),weekdaysMin:"ی_د_س_چ_پ_ج_ش".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},meridiemParse:/قبل از ظهر|بعد از ظهر/,isPM:function(a){return/بعد از ظهر/.test(a)},meridiem:function(a,b,c){return 12>a?"قبل از ظهر":"بعد از ظهر"},calendar:{sameDay:"[امروز ساعت] LT",nextDay:"[فردا ساعت] LT",nextWeek:"dddd [ساعت] LT",lastDay:"[دیروز ساعت] LT",lastWeek:"dddd [پیش] [ساعت] LT",sameElse:"L"},relativeTime:{future:"در %s",past:"%s پیش",s:"چندین ثانیه",m:"یک دقیقه",mm:"%d دقیقه",h:"یک ساعت",hh:"%d ساعت",d:"یک روز",dd:"%d روز",M:"یک ماه",MM:"%d ماه",y:"یک سال",yy:"%d سال"},preparse:function(a){return a.replace(/[۰-۹]/g,function(a){return c[a]}).replace(/،/g,",")},postformat:function(b){return b.replace(/\d/g,function(b){return a[b]}).replace(/,/g,"،")},ordinalParse:/\d{1,2}م/,ordinal:"%dم",week:{dow:6,doy:12}});return d}(),a.fullCalendar.datepickerLang("fa","fa",{closeText:"بستن",prevText:"<قبلی",nextText:"بعدی>",currentText:"امروز",monthNames:["ژانویه","فوریه","مارس","آوریل","مه","ژوئن","ژوئیه","اوت","سپتامبر","اکتبر","نوامبر","دسامبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["يکشنبه","دوشنبه","سه‌شنبه","چهارشنبه","پنجشنبه","جمعه","شنبه"],dayNamesShort:["ی","د","س","چ","پ","ج","ش"],dayNamesMin:["ی","د","س","چ","پ","ج","ش"],weekHeader:"هف",dateFormat:"yy/mm/dd",firstDay:6,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fa",{buttonText:{month:"ماه",week:"هفته",day:"روز",list:"برنامه"},allDayText:"تمام روز",eventLimitText:function(a){return"بیش از "+a}})}(),function(){!function(){"use strict";function a(a,b,d,e){var f="";switch(d){case"s":return e?"muutaman sekunnin":"muutama sekunti";case"m":return e?"minuutin":"minuutti";case"mm":f=e?"minuutin":"minuuttia";break;case"h":return e?"tunnin":"tunti";case"hh":f=e?"tunnin":"tuntia";break;case"d":return e?"päivän":"päivä";case"dd":f=e?"päivän":"päivää";break;case"M":return e?"kuukauden":"kuukausi";case"MM":f=e?"kuukauden":"kuukautta";break;case"y":return e?"vuoden":"vuosi";case"yy":f=e?"vuoden":"vuotta"}return f=c(a,e)+" "+f}function c(a,b){return 10>a?b?e[a]:d[a]:a}var d="nolla yksi kaksi kolme neljä viisi kuusi seitsemän kahdeksan yhdeksän".split(" "),e=["nolla","yhden","kahden","kolmen","neljän","viiden","kuuden",d[7],d[8],d[9]],f=(b.defineLocale||b.lang).call(b,"fi",{months:"tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kesäkuu_heinäkuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu".split("_"),monthsShort:"tammi_helmi_maalis_huhti_touko_kesä_heinä_elo_syys_loka_marras_joulu".split("_"),weekdays:"sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai".split("_"),weekdaysShort:"su_ma_ti_ke_to_pe_la".split("_"),weekdaysMin:"su_ma_ti_ke_to_pe_la".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD.MM.YYYY",LL:"Do MMMM[ta] YYYY",LLL:"Do MMMM[ta] YYYY, [klo] HH.mm",LLLL:"dddd, Do MMMM[ta] YYYY, [klo] HH.mm",l:"D.M.YYYY",ll:"Do MMM YYYY",lll:"Do MMM YYYY, [klo] HH.mm",llll:"ddd, Do MMM YYYY, [klo] HH.mm"},calendar:{sameDay:"[tänään] [klo] LT",nextDay:"[huomenna] [klo] LT",nextWeek:"dddd [klo] LT",lastDay:"[eilen] [klo] LT",lastWeek:"[viime] dddd[na] [klo] LT",sameElse:"L"},relativeTime:{future:"%s päästä",past:"%s sitten",s:a,m:a,mm:a,h:a,hh:a,d:a,dd:a,M:a,MM:a,y:a,yy:a},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return f}(),a.fullCalendar.datepickerLang("fi","fi",{closeText:"Sulje",prevText:"«Edellinen",nextText:"Seuraava»",currentText:"Tänään",monthNames:["Tammikuu","Helmikuu","Maaliskuu","Huhtikuu","Toukokuu","Kesäkuu","Heinäkuu","Elokuu","Syyskuu","Lokakuu","Marraskuu","Joulukuu"],monthNamesShort:["Tammi","Helmi","Maalis","Huhti","Touko","Kesä","Heinä","Elo","Syys","Loka","Marras","Joulu"],dayNamesShort:["Su","Ma","Ti","Ke","To","Pe","La"],dayNames:["Sunnuntai","Maanantai","Tiistai","Keskiviikko","Torstai","Perjantai","Lauantai"],dayNamesMin:["Su","Ma","Ti","Ke","To","Pe","La"],weekHeader:"Vk",dateFormat:"d.m.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fi",{buttonText:{month:"Kuukausi",week:"Viikko",day:"Päivä",list:"Tapahtumat"},allDayText:"Koko päivä",eventLimitText:"lisää"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"fr-ca",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),monthsParseExact:!0,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|e)/,ordinal:function(a){return a+(1===a?"er":"e")}});return a}(),a.fullCalendar.datepickerLang("fr-ca","fr-CA",{closeText:"Fermer",prevText:"Précédent",nextText:"Suivant",currentText:"Aujourd'hui",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avril","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sem.",dateFormat:"yy-mm-dd",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fr-ca",{buttonText:{year:"Année",month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},allDayHtml:"Toute la
    journée",eventLimitText:"en plus"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"fr-ch",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),monthsParseExact:!0,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|e)/,ordinal:function(a){return a+(1===a?"er":"e")},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("fr-ch","fr-CH",{closeText:"Fermer",prevText:"<Préc",nextText:"Suiv>",currentText:"Courant",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avril","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sm",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fr-ch",{buttonText:{year:"Année",month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},allDayHtml:"Toute la
    journée",eventLimitText:"en plus"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"fr",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),monthsParseExact:!0,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|)/,ordinal:function(a){return a+(1===a?"er":"")},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("fr","fr",{closeText:"Fermer",prevText:"Précédent",nextText:"Suivant",currentText:"Aujourd'hui",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sem.",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fr",{buttonText:{year:"Année",month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},allDayHtml:"Toute la
    journée",eventLimitText:"en plus"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"gl",{months:"Xaneiro_Febreiro_Marzo_Abril_Maio_Xuño_Xullo_Agosto_Setembro_Outubro_Novembro_Decembro".split("_"),monthsShort:"Xan._Feb._Mar._Abr._Mai._Xuñ._Xul._Ago._Set._Out._Nov._Dec.".split("_"),monthsParseExact:!0,weekdays:"Domingo_Luns_Martes_Mércores_Xoves_Venres_Sábado".split("_"),weekdaysShort:"Dom._Lun._Mar._Mér._Xov._Ven._Sáb.".split("_"),weekdaysMin:"Do_Lu_Ma_Mé_Xo_Ve_Sá".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd D MMMM YYYY H:mm"},calendar:{sameDay:function(){return"[hoxe "+(1!==this.hours()?"ás":"á")+"] LT"},nextDay:function(){return"[mañá "+(1!==this.hours()?"ás":"á")+"] LT"},nextWeek:function(){return"dddd ["+(1!==this.hours()?"ás":"a")+"] LT"},lastDay:function(){return"[onte "+(1!==this.hours()?"á":"a")+"] LT"},lastWeek:function(){return"[o] dddd [pasado "+(1!==this.hours()?"ás":"a")+"] LT"},sameElse:"L"},relativeTime:{future:function(a){return"uns segundos"===a?"nuns segundos":"en "+a},past:"hai %s",s:"uns segundos",m:"un minuto",mm:"%d minutos",h:"unha hora",hh:"%d horas",d:"un día",dd:"%d días",M:"un mes",MM:"%d meses",y:"un ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:7}});return a}(),a.fullCalendar.datepickerLang("gl","gl",{closeText:"Pechar",prevText:"<Ant",nextText:"Seg>",currentText:"Hoxe",monthNames:["Xaneiro","Febreiro","Marzo","Abril","Maio","Xuño","Xullo","Agosto","Setembro","Outubro","Novembro","Decembro"],monthNamesShort:["Xan","Feb","Mar","Abr","Mai","Xuñ","Xul","Ago","Set","Out","Nov","Dec"],dayNames:["Domingo","Luns","Martes","Mércores","Xoves","Venres","Sábado"],dayNamesShort:["Dom","Lun","Mar","Mér","Xov","Ven","Sáb"],dayNamesMin:["Do","Lu","Ma","Mé","Xo","Ve","Sá"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("gl",{buttonText:{month:"Mes",week:"Semana",day:"Día",list:"Axenda"},allDayHtml:"Todo
    o día",eventLimitText:"máis"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"he",{months:"ינואר_פברואר_מרץ_אפריל_מאי_יוני_יולי_אוגוסט_ספטמבר_אוקטובר_נובמבר_דצמבר".split("_"),monthsShort:"ינו׳_פבר׳_מרץ_אפר׳_מאי_יוני_יולי_אוג׳_ספט׳_אוק׳_נוב׳_דצמ׳".split("_"),weekdays:"ראשון_שני_שלישי_רביעי_חמישי_שישי_שבת".split("_"),weekdaysShort:"א׳_ב׳_ג׳_ד׳_ה׳_ו׳_ש׳".split("_"),weekdaysMin:"א_ב_ג_ד_ה_ו_ש".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [ב]MMMM YYYY",LLL:"D [ב]MMMM YYYY HH:mm",LLLL:"dddd, D [ב]MMMM YYYY HH:mm",l:"D/M/YYYY",ll:"D MMM YYYY",lll:"D MMM YYYY HH:mm",llll:"ddd, D MMM YYYY HH:mm"},calendar:{sameDay:"[היום ב־]LT",nextDay:"[מחר ב־]LT",nextWeek:"dddd [בשעה] LT",lastDay:"[אתמול ב־]LT",lastWeek:"[ביום] dddd [האחרון בשעה] LT",sameElse:"L"},relativeTime:{future:"בעוד %s",past:"לפני %s",s:"מספר שניות",m:"דקה",mm:"%d דקות",h:"שעה",hh:function(a){return 2===a?"שעתיים":a+" שעות"},d:"יום",dd:function(a){return 2===a?"יומיים":a+" ימים"},M:"חודש",MM:function(a){return 2===a?"חודשיים":a+" חודשים"},y:"שנה",yy:function(a){return 2===a?"שנתיים":a%10===0&&10!==a?a+" שנה":a+" שנים"}},meridiemParse:/אחה"צ|לפנה"צ|אחרי הצהריים|לפני הצהריים|לפנות בוקר|בבוקר|בערב/i,isPM:function(a){return/^(אחה"צ|אחרי הצהריים|בערב)$/.test(a)},meridiem:function(a,b,c){return 5>a?"לפנות בוקר":10>a?"בבוקר":12>a?c?'לפנה"צ':"לפני הצהריים":18>a?c?'אחה"צ':"אחרי הצהריים":"בערב"}});return a}(),a.fullCalendar.datepickerLang("he","he",{closeText:"סגור",prevText:"<הקודם",nextText:"הבא>",currentText:"היום",monthNames:["ינואר","פברואר","מרץ","אפריל","מאי","יוני","יולי","אוגוסט","ספטמבר","אוקטובר","נובמבר","דצמבר"],monthNamesShort:["ינו","פבר","מרץ","אפר","מאי","יוני","יולי","אוג","ספט","אוק","נוב","דצמ"],dayNames:["ראשון","שני","שלישי","רביעי","חמישי","שישי","שבת"],dayNamesShort:["א'","ב'","ג'","ד'","ה'","ו'","שבת"],dayNamesMin:["א'","ב'","ג'","ד'","ה'","ו'","שבת"],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("he",{defaultButtonText:{month:"חודש",week:"שבוע",day:"יום",list:"סדר יום"},weekNumberTitle:"שבוע",allDayText:"כל היום",eventLimitText:"אחר"})}(),function(){!function(){"use strict";var a={1:"१",2:"२",3:"३",4:"४",5:"५",6:"६",7:"७",8:"८",9:"९",0:"०"},c={"१":"1","२":"2","३":"3","४":"4","५":"5","६":"6","७":"7","८":"8","९":"9","०":"0"},d=(b.defineLocale||b.lang).call(b,"hi",{months:"जनवरी_फ़रवरी_मार्च_अप्रैल_मई_जून_जुलाई_अगस्त_सितम्बर_अक्टूबर_नवम्बर_दिसम्बर".split("_"),monthsShort:"जन._फ़र._मार्च_अप्रै._मई_जून_जुल._अग._सित._अक्टू._नव._दिस.".split("_"),monthsParseExact:!0,weekdays:"रविवार_सोमवार_मंगलवार_बुधवार_गुरूवार_शुक्रवार_शनिवार".split("_"),weekdaysShort:"रवि_सोम_मंगल_बुध_गुरू_शुक्र_शनि".split("_"),weekdaysMin:"र_सो_मं_बु_गु_शु_श".split("_"),longDateFormat:{LT:"A h:mm बजे",LTS:"A h:mm:ss बजे",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm बजे",LLLL:"dddd, D MMMM YYYY, A h:mm बजे"},calendar:{sameDay:"[आज] LT",nextDay:"[कल] LT",nextWeek:"dddd, LT",lastDay:"[कल] LT",lastWeek:"[पिछले] dddd, LT",sameElse:"L"},relativeTime:{future:"%s में",past:"%s पहले",s:"कुछ ही क्षण",m:"एक मिनट",mm:"%d मिनट",h:"एक घंटा",hh:"%d घंटे",d:"एक दिन",dd:"%d दिन",M:"एक महीने",MM:"%d महीने",y:"एक वर्ष",yy:"%d वर्ष"},preparse:function(a){return a.replace(/[१२३४५६७८९०]/g,function(a){return c[a]})},postformat:function(b){return b.replace(/\d/g,function(b){return a[b]})},meridiemParse:/रात|सुबह|दोपहर|शाम/,meridiemHour:function(a,b){return 12===a&&(a=0),"रात"===b?4>a?a:a+12:"सुबह"===b?a:"दोपहर"===b?a>=10?a:a+12:"शाम"===b?a+12:void 0},meridiem:function(a,b,c){return 4>a?"रात":10>a?"सुबह":17>a?"दोपहर":20>a?"शाम":"रात"},week:{dow:0,doy:6}});return d}(),a.fullCalendar.datepickerLang("hi","hi",{closeText:"बंद",prevText:"पिछला",nextText:"अगला",currentText:"आज",monthNames:["जनवरी ","फरवरी","मार्च","अप्रेल","मई","जून","जूलाई","अगस्त ","सितम्बर","अक्टूबर","नवम्बर","दिसम्बर"],monthNamesShort:["जन","फर","मार्च","अप्रेल","मई","जून","जूलाई","अग","सित","अक्ट","नव","दि"],dayNames:["रविवार","सोमवार","मंगलवार","बुधवार","गुरुवार","शुक्रवार","शनिवार"],dayNamesShort:["रवि","सोम","मंगल","बुध","गुरु","शुक्र","शनि"],dayNamesMin:["रवि","सोम","मंगल","बुध","गुरु","शुक्र","शनि"],weekHeader:"हफ्ता",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("hi",{buttonText:{month:"महीना",week:"सप्ताह",day:"दिन",list:"कार्यसूची"},allDayText:"सभी दिन",eventLimitText:function(a){return"+अधिक "+a}})}(),function(){!function(){"use strict";function a(a,b,c){var d=a+" ";switch(c){case"m":return b?"jedna minuta":"jedne minute";case"mm":return d+=1===a?"minuta":2===a||3===a||4===a?"minute":"minuta";case"h":return b?"jedan sat":"jednog sata";case"hh":return d+=1===a?"sat":2===a||3===a||4===a?"sata":"sati";case"dd":return d+=1===a?"dan":"dana";case"MM":return d+=1===a?"mjesec":2===a||3===a||4===a?"mjeseca":"mjeseci";case"yy":return d+=1===a?"godina":2===a||3===a||4===a?"godine":"godina"}}var c=(b.defineLocale||b.lang).call(b,"hr",{months:{format:"siječnja_veljače_ožujka_travnja_svibnja_lipnja_srpnja_kolovoza_rujna_listopada_studenoga_prosinca".split("_"),standalone:"siječanj_veljača_ožujak_travanj_svibanj_lipanj_srpanj_kolovoz_rujan_listopad_studeni_prosinac".split("_")},monthsShort:"sij._velj._ožu._tra._svi._lip._srp._kol._ruj._lis._stu._pro.".split("_"),monthsParseExact:!0,weekdays:"nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sri._čet._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_če_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedjelju] [u] LT";case 3:return"[u] [srijedu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[jučer u] LT",lastWeek:function(){switch(this.day()){case 0:case 3:return"[prošlu] dddd [u] LT";case 6:return"[prošle] [subote] [u] LT";case 1:case 2:case 4:case 5:return"[prošli] dddd [u] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"prije %s",s:"par sekundi",m:a,mm:a,h:a,hh:a,d:"dan",dd:a,M:"mjesec",MM:a,y:"godinu",yy:a},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});return c}(),a.fullCalendar.datepickerLang("hr","hr",{closeText:"Zatvori",prevText:"<",nextText:">",currentText:"Danas",monthNames:["Siječanj","Veljača","Ožujak","Travanj","Svibanj","Lipanj","Srpanj","Kolovoz","Rujan","Listopad","Studeni","Prosinac"],monthNamesShort:["Sij","Velj","Ožu","Tra","Svi","Lip","Srp","Kol","Ruj","Lis","Stu","Pro"],dayNames:["Nedjelja","Ponedjeljak","Utorak","Srijeda","Četvrtak","Petak","Subota"],dayNamesShort:["Ned","Pon","Uto","Sri","Čet","Pet","Sub"],dayNamesMin:["Ne","Po","Ut","Sr","Če","Pe","Su"],weekHeader:"Tje",dateFormat:"dd.mm.yy.",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("hr",{buttonText:{month:"Mjesec",week:"Tjedan",day:"Dan",list:"Raspored"},allDayText:"Cijeli dan",eventLimitText:function(a){return"+ još "+a}})}(),function(){!function(){"use strict";function a(a,b,c,d){var e=a;switch(c){case"s":return d||b?"néhány másodperc":"néhány másodperce";case"m":return"egy"+(d||b?" perc":" perce");case"mm":return e+(d||b?" perc":" perce");case"h":return"egy"+(d||b?" óra":" órája");case"hh":return e+(d||b?" óra":" órája");case"d":return"egy"+(d||b?" nap":" napja");case"dd":return e+(d||b?" nap":" napja");case"M":return"egy"+(d||b?" hónap":" hónapja");case"MM":return e+(d||b?" hónap":" hónapja");case"y":return"egy"+(d||b?" év":" éve");case"yy":return e+(d||b?" év":" éve")}return""}function c(a){return(a?"":"[múlt] ")+"["+d[this.day()]+"] LT[-kor]"}var d="vasárnap hétfőn kedden szerdán csütörtökön pénteken szombaton".split(" "),e=(b.defineLocale||b.lang).call(b,"hu",{months:"január_február_március_április_május_június_július_augusztus_szeptember_október_november_december".split("_"),monthsShort:"jan_feb_márc_ápr_máj_jún_júl_aug_szept_okt_nov_dec".split("_"),weekdays:"vasárnap_hétfő_kedd_szerda_csütörtök_péntek_szombat".split("_"),weekdaysShort:"vas_hét_kedd_sze_csüt_pén_szo".split("_"),weekdaysMin:"v_h_k_sze_cs_p_szo".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"YYYY.MM.DD.",LL:"YYYY. MMMM D.",LLL:"YYYY. MMMM D. H:mm",LLLL:"YYYY. MMMM D., dddd H:mm"},meridiemParse:/de|du/i,isPM:function(a){return"u"===a.charAt(1).toLowerCase()},meridiem:function(a,b,c){return 12>a?c===!0?"de":"DE":c===!0?"du":"DU"},calendar:{sameDay:"[ma] LT[-kor]",nextDay:"[holnap] LT[-kor]",nextWeek:function(){return c.call(this,!0)},lastDay:"[tegnap] LT[-kor]",lastWeek:function(){return c.call(this,!1)},sameElse:"L"},relativeTime:{future:"%s múlva",past:"%s",s:a,m:a,mm:a,h:a,hh:a,d:a,dd:a,M:a,MM:a,y:a,yy:a},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});return e}(),a.fullCalendar.datepickerLang("hu","hu",{closeText:"bezár",prevText:"vissza",nextText:"előre",currentText:"ma",monthNames:["Január","Február","Március","Április","Május","Június","Július","Augusztus","Szeptember","Október","November","December"],monthNamesShort:["Jan","Feb","Már","Ápr","Máj","Jún","Júl","Aug","Szep","Okt","Nov","Dec"],dayNames:["Vasárnap","Hétfő","Kedd","Szerda","Csütörtök","Péntek","Szombat"],dayNamesShort:["Vas","Hét","Ked","Sze","Csü","Pén","Szo"],dayNamesMin:["V","H","K","Sze","Cs","P","Szo"],weekHeader:"Hét",dateFormat:"yy.mm.dd.",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:""}),a.fullCalendar.lang("hu",{buttonText:{month:"Hónap",week:"Hét",day:"Nap",list:"Napló"},allDayText:"Egész nap",eventLimitText:"további"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"id",{months:"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nov_Des".split("_"),weekdays:"Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu".split("_"),weekdaysShort:"Min_Sen_Sel_Rab_Kam_Jum_Sab".split("_"),weekdaysMin:"Mg_Sn_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] HH.mm",LLLL:"dddd, D MMMM YYYY [pukul] HH.mm"},meridiemParse:/pagi|siang|sore|malam/,meridiemHour:function(a,b){return 12===a&&(a=0),"pagi"===b?a:"siang"===b?a>=11?a:a+12:"sore"===b||"malam"===b?a+12:void 0},meridiem:function(a,b,c){return 11>a?"pagi":15>a?"siang":19>a?"sore":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Besok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kemarin pukul] LT",lastWeek:"dddd [lalu pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lalu",s:"beberapa detik",m:"semenit",mm:"%d menit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}});return a}(),a.fullCalendar.datepickerLang("id","id",{closeText:"Tutup",prevText:"<mundur",nextText:"maju>",currentText:"hari ini",monthNames:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","Nopember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Agus","Sep","Okt","Nop","Des"],dayNames:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],dayNamesShort:["Min","Sen","Sel","Rab","kam","Jum","Sab"],dayNamesMin:["Mg","Sn","Sl","Rb","Km","jm","Sb"],weekHeader:"Mg",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("id",{buttonText:{month:"Bulan",week:"Minggu",day:"Hari",list:"Agenda"},allDayHtml:"Sehari
    penuh",eventLimitText:"lebih"})}(),function(){!function(){"use strict";function a(a){return a%100===11?!0:a%10!==1}function c(b,c,d,e){var f=b+" ";switch(d){case"s":return c||e?"nokkrar sekúndur":"nokkrum sekúndum";case"m":return c?"mínúta":"mínútu";case"mm":return a(b)?f+(c||e?"mínútur":"mínútum"):c?f+"mínúta":f+"mínútu";case"hh":return a(b)?f+(c||e?"klukkustundir":"klukkustundum"):f+"klukkustund";case"d":return c?"dagur":e?"dag":"degi";case"dd":return a(b)?c?f+"dagar":f+(e?"daga":"dögum"):c?f+"dagur":f+(e?"dag":"degi");case"M":return c?"mánuður":e?"mánuð":"mánuði";case"MM":return a(b)?c?f+"mánuðir":f+(e?"mánuði":"mánuðum"):c?f+"mánuður":f+(e?"mánuð":"mánuði");case"y":return c||e?"ár":"ári";case"yy":return a(b)?f+(c||e?"ár":"árum"):f+(c||e?"ár":"ári")}}var d=(b.defineLocale||b.lang).call(b,"is",{months:"janúar_febrúar_mars_apríl_maí_júní_júlí_ágúst_september_október_nóvember_desember".split("_"),monthsShort:"jan_feb_mar_apr_maí_jún_júl_ágú_sep_okt_nóv_des".split("_"),weekdays:"sunnudagur_mánudagur_þriðjudagur_miðvikudagur_fimmtudagur_föstudagur_laugardagur".split("_"),weekdaysShort:"sun_mán_þri_mið_fim_fös_lau".split("_"),weekdaysMin:"Su_Má_Þr_Mi_Fi_Fö_La".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] H:mm",LLLL:"dddd, D. MMMM YYYY [kl.] H:mm"},calendar:{sameDay:"[í dag kl.] LT",nextDay:"[á morgun kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[í gær kl.] LT",lastWeek:"[síðasta] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"eftir %s",past:"fyrir %s síðan",s:c,m:c,mm:c,h:"klukkustund",hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return d}(),a.fullCalendar.datepickerLang("is","is",{closeText:"Loka",prevText:"< Fyrri",nextText:"Næsti >",currentText:"Í dag",monthNames:["Janúar","Febrúar","Mars","Apríl","Maí","Júní","Júlí","Ágúst","September","Október","Nóvember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Maí","Jún","Júl","Ágú","Sep","Okt","Nóv","Des"],dayNames:["Sunnudagur","Mánudagur","Þriðjudagur","Miðvikudagur","Fimmtudagur","Föstudagur","Laugardagur"],dayNamesShort:["Sun","Mán","Þri","Mið","Fim","Fös","Lau"],dayNamesMin:["Su","Má","Þr","Mi","Fi","Fö","La"],weekHeader:"Vika",dateFormat:"dd.mm.yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("is",{buttonText:{month:"Mánuður",week:"Vika",day:"Dagur",list:"Dagskrá"},allDayHtml:"Allan
    daginn",eventLimitText:"meira"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"it",{months:"gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre".split("_"),monthsShort:"gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic".split("_"),weekdays:"Domenica_Lunedì_Martedì_Mercoledì_Giovedì_Venerdì_Sabato".split("_"),weekdaysShort:"Dom_Lun_Mar_Mer_Gio_Ven_Sab".split("_"),weekdaysMin:"Do_Lu_Ma_Me_Gi_Ve_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Oggi alle] LT",nextDay:"[Domani alle] LT",nextWeek:"dddd [alle] LT",lastDay:"[Ieri alle] LT",lastWeek:function(){switch(this.day()){case 0:return"[la scorsa] dddd [alle] LT";default:return"[lo scorso] dddd [alle] LT"}},sameElse:"L"},relativeTime:{future:function(a){return(/^[0-9].+$/.test(a)?"tra":"in")+" "+a},past:"%s fa",s:"alcuni secondi",m:"un minuto",mm:"%d minuti",h:"un'ora",hh:"%d ore",d:"un giorno",dd:"%d giorni",M:"un mese",MM:"%d mesi",y:"un anno",yy:"%d anni"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("it","it",{closeText:"Chiudi",prevText:"<Prec",nextText:"Succ>",currentText:"Oggi",monthNames:["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"], +monthNamesShort:["Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic"],dayNames:["Domenica","Lunedì","Martedì","Mercoledì","Giovedì","Venerdì","Sabato"],dayNamesShort:["Dom","Lun","Mar","Mer","Gio","Ven","Sab"],dayNamesMin:["Do","Lu","Ma","Me","Gi","Ve","Sa"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("it",{buttonText:{month:"Mese",week:"Settimana",day:"Giorno",list:"Agenda"},allDayHtml:"Tutto il
    giorno",eventLimitText:function(a){return"+altri "+a}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"ja",{months:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"日曜日_月曜日_火曜日_水曜日_木曜日_金曜日_土曜日".split("_"),weekdaysShort:"日_月_火_水_木_金_土".split("_"),weekdaysMin:"日_月_火_水_木_金_土".split("_"),longDateFormat:{LT:"Ah時m分",LTS:"Ah時m分s秒",L:"YYYY/MM/DD",LL:"YYYY年M月D日",LLL:"YYYY年M月D日Ah時m分",LLLL:"YYYY年M月D日Ah時m分 dddd"},meridiemParse:/午前|午後/i,isPM:function(a){return"午後"===a},meridiem:function(a,b,c){return 12>a?"午前":"午後"},calendar:{sameDay:"[今日] LT",nextDay:"[明日] LT",nextWeek:"[来週]dddd LT",lastDay:"[昨日] LT",lastWeek:"[前週]dddd LT",sameElse:"L"},ordinalParse:/\d{1,2}日/,ordinal:function(a,b){switch(b){case"d":case"D":case"DDD":return a+"日";default:return a}},relativeTime:{future:"%s後",past:"%s前",s:"数秒",m:"1分",mm:"%d分",h:"1時間",hh:"%d時間",d:"1日",dd:"%d日",M:"1ヶ月",MM:"%dヶ月",y:"1年",yy:"%d年"}});return a}(),a.fullCalendar.datepickerLang("ja","ja",{closeText:"閉じる",prevText:"<前",nextText:"次>",currentText:"今日",monthNames:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],monthNamesShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayNames:["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"],dayNamesShort:["日","月","火","水","木","金","土"],dayNamesMin:["日","月","火","水","木","金","土"],weekHeader:"週",dateFormat:"yy/mm/dd",firstDay:0,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("ja",{buttonText:{month:"月",week:"週",day:"日",list:"予定リスト"},allDayText:"終日",eventLimitText:function(a){return"他 "+a+" 件"}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"ko",{months:"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월".split("_"),monthsShort:"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월".split("_"),weekdays:"일요일_월요일_화요일_수요일_목요일_금요일_토요일".split("_"),weekdaysShort:"일_월_화_수_목_금_토".split("_"),weekdaysMin:"일_월_화_수_목_금_토".split("_"),longDateFormat:{LT:"A h시 m분",LTS:"A h시 m분 s초",L:"YYYY.MM.DD",LL:"YYYY년 MMMM D일",LLL:"YYYY년 MMMM D일 A h시 m분",LLLL:"YYYY년 MMMM D일 dddd A h시 m분"},calendar:{sameDay:"오늘 LT",nextDay:"내일 LT",nextWeek:"dddd LT",lastDay:"어제 LT",lastWeek:"지난주 dddd LT",sameElse:"L"},relativeTime:{future:"%s 후",past:"%s 전",s:"몇 초",ss:"%d초",m:"일분",mm:"%d분",h:"한 시간",hh:"%d시간",d:"하루",dd:"%d일",M:"한 달",MM:"%d달",y:"일 년",yy:"%d년"},ordinalParse:/\d{1,2}일/,ordinal:"%d일",meridiemParse:/오전|오후/,isPM:function(a){return"오후"===a},meridiem:function(a,b,c){return 12>a?"오전":"오후"}});return a}(),a.fullCalendar.datepickerLang("ko","ko",{closeText:"닫기",prevText:"이전달",nextText:"다음달",currentText:"오늘",monthNames:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],monthNamesShort:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],dayNames:["일요일","월요일","화요일","수요일","목요일","금요일","토요일"],dayNamesShort:["일","월","화","수","목","금","토"],dayNamesMin:["일","월","화","수","목","금","토"],weekHeader:"Wk",dateFormat:"yy-mm-dd",firstDay:0,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"년"}),a.fullCalendar.lang("ko",{buttonText:{month:"월",week:"주",day:"일",list:"일정목록"},allDayText:"종일",eventLimitText:"개"})}(),function(){!function(){"use strict";function a(a,b,c,d){var e={m:["eng Minutt","enger Minutt"],h:["eng Stonn","enger Stonn"],d:["een Dag","engem Dag"],M:["ee Mount","engem Mount"],y:["ee Joer","engem Joer"]};return b?e[c][0]:e[c][1]}function c(a){var b=a.substr(0,a.indexOf(" "));return e(b)?"a "+a:"an "+a}function d(a){var b=a.substr(0,a.indexOf(" "));return e(b)?"viru "+a:"virun "+a}function e(a){if(a=parseInt(a,10),isNaN(a))return!1;if(0>a)return!0;if(10>a)return a>=4&&7>=a;if(100>a){var b=a%10,c=a/10;return e(0===b?c:b)}if(1e4>a){for(;a>=10;)a/=10;return e(a)}return a/=1e3,e(a)}var f=(b.defineLocale||b.lang).call(b,"lb",{months:"Januar_Februar_Mäerz_Abrëll_Mee_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Abr._Mee_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonndeg_Méindeg_Dënschdeg_Mëttwoch_Donneschdeg_Freideg_Samschdeg".split("_"),weekdaysShort:"So._Mé._Dë._Më._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mé_Dë_Më_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm [Auer]",LTS:"H:mm:ss [Auer]",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm [Auer]",LLLL:"dddd, D. MMMM YYYY H:mm [Auer]"},calendar:{sameDay:"[Haut um] LT",sameElse:"L",nextDay:"[Muer um] LT",nextWeek:"dddd [um] LT",lastDay:"[Gëschter um] LT",lastWeek:function(){switch(this.day()){case 2:case 4:return"[Leschten] dddd [um] LT";default:return"[Leschte] dddd [um] LT"}}},relativeTime:{future:c,past:d,s:"e puer Sekonnen",m:a,mm:"%d Minutten",h:a,hh:"%d Stonnen",d:a,dd:"%d Deeg",M:a,MM:"%d Méint",y:a,yy:"%d Joer"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return f}(),a.fullCalendar.datepickerLang("lb","lb",{closeText:"Fäerdeg",prevText:"Zréck",nextText:"Weider",currentText:"Haut",monthNames:["Januar","Februar","Mäerz","Abrëll","Mee","Juni","Juli","August","September","Oktober","November","Dezember"],monthNamesShort:["Jan","Feb","Mäe","Abr","Mee","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],dayNames:["Sonndeg","Méindeg","Dënschdeg","Mëttwoch","Donneschdeg","Freideg","Samschdeg"],dayNamesShort:["Son","Méi","Dën","Mët","Don","Fre","Sam"],dayNamesMin:["So","Mé","Dë","Më","Do","Fr","Sa"],weekHeader:"W",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("lb",{buttonText:{month:"Mount",week:"Woch",day:"Dag",list:"Terminiwwersiicht"},allDayText:"Ganzen Dag",eventLimitText:"méi"})}(),function(){!function(){"use strict";function a(a,b,c,d){return b?"kelios sekundės":d?"kelių sekundžių":"kelias sekundes"}function c(a,b,c,d){return b?e(c)[0]:d?e(c)[1]:e(c)[2]}function d(a){return a%10===0||a>10&&20>a}function e(a){return g[a].split("_")}function f(a,b,f,g){var h=a+" ";return 1===a?h+c(a,b,f[0],g):b?h+(d(a)?e(f)[1]:e(f)[0]):g?h+e(f)[1]:h+(d(a)?e(f)[1]:e(f)[2])}var g={m:"minutė_minutės_minutę",mm:"minutės_minučių_minutes",h:"valanda_valandos_valandą",hh:"valandos_valandų_valandas",d:"diena_dienos_dieną",dd:"dienos_dienų_dienas",M:"mėnuo_mėnesio_mėnesį",MM:"mėnesiai_mėnesių_mėnesius",y:"metai_metų_metus",yy:"metai_metų_metus"},h=(b.defineLocale||b.lang).call(b,"lt",{months:{format:"sausio_vasario_kovo_balandžio_gegužės_birželio_liepos_rugpjūčio_rugsėjo_spalio_lapkričio_gruodžio".split("_"),standalone:"sausis_vasaris_kovas_balandis_gegužė_birželis_liepa_rugpjūtis_rugsėjis_spalis_lapkritis_gruodis".split("_")},monthsShort:"sau_vas_kov_bal_geg_bir_lie_rgp_rgs_spa_lap_grd".split("_"),weekdays:{format:"sekmadienį_pirmadienį_antradienį_trečiadienį_ketvirtadienį_penktadienį_šeštadienį".split("_"),standalone:"sekmadienis_pirmadienis_antradienis_trečiadienis_ketvirtadienis_penktadienis_šeštadienis".split("_"),isFormat:/dddd HH:mm/},weekdaysShort:"Sek_Pir_Ant_Tre_Ket_Pen_Šeš".split("_"),weekdaysMin:"S_P_A_T_K_Pn_Š".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"YYYY [m.] MMMM D [d.]",LLL:"YYYY [m.] MMMM D [d.], HH:mm [val.]",LLLL:"YYYY [m.] MMMM D [d.], dddd, HH:mm [val.]",l:"YYYY-MM-DD",ll:"YYYY [m.] MMMM D [d.]",lll:"YYYY [m.] MMMM D [d.], HH:mm [val.]",llll:"YYYY [m.] MMMM D [d.], ddd, HH:mm [val.]"},calendar:{sameDay:"[Šiandien] LT",nextDay:"[Rytoj] LT",nextWeek:"dddd LT",lastDay:"[Vakar] LT",lastWeek:"[Praėjusį] dddd LT",sameElse:"L"},relativeTime:{future:"po %s",past:"prieš %s",s:a,m:c,mm:f,h:c,hh:f,d:c,dd:f,M:c,MM:f,y:c,yy:f},ordinalParse:/\d{1,2}-oji/,ordinal:function(a){return a+"-oji"},week:{dow:1,doy:4}});return h}(),a.fullCalendar.datepickerLang("lt","lt",{closeText:"Uždaryti",prevText:"<Atgal",nextText:"Pirmyn>",currentText:"Šiandien",monthNames:["Sausis","Vasaris","Kovas","Balandis","Gegužė","Birželis","Liepa","Rugpjūtis","Rugsėjis","Spalis","Lapkritis","Gruodis"],monthNamesShort:["Sau","Vas","Kov","Bal","Geg","Bir","Lie","Rugp","Rugs","Spa","Lap","Gru"],dayNames:["sekmadienis","pirmadienis","antradienis","trečiadienis","ketvirtadienis","penktadienis","šeštadienis"],dayNamesShort:["sek","pir","ant","tre","ket","pen","šeš"],dayNamesMin:["Se","Pr","An","Tr","Ke","Pe","Še"],weekHeader:"SAV",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:""}),a.fullCalendar.lang("lt",{buttonText:{month:"Mėnuo",week:"Savaitė",day:"Diena",list:"Darbotvarkė"},allDayText:"Visą dieną",eventLimitText:"daugiau"})}(),function(){!function(){"use strict";function a(a,b,c){return c?b%10===1&&11!==b?a[2]:a[3]:b%10===1&&11!==b?a[0]:a[1]}function c(b,c,d){return b+" "+a(f[d],b,c)}function d(b,c,d){return a(f[d],b,c)}function e(a,b){return b?"dažas sekundes":"dažām sekundēm"}var f={m:"minūtes_minūtēm_minūte_minūtes".split("_"),mm:"minūtes_minūtēm_minūte_minūtes".split("_"),h:"stundas_stundām_stunda_stundas".split("_"),hh:"stundas_stundām_stunda_stundas".split("_"),d:"dienas_dienām_diena_dienas".split("_"),dd:"dienas_dienām_diena_dienas".split("_"),M:"mēneša_mēnešiem_mēnesis_mēneši".split("_"),MM:"mēneša_mēnešiem_mēnesis_mēneši".split("_"),y:"gada_gadiem_gads_gadi".split("_"),yy:"gada_gadiem_gads_gadi".split("_")},g=(b.defineLocale||b.lang).call(b,"lv",{months:"janvāris_februāris_marts_aprīlis_maijs_jūnijs_jūlijs_augusts_septembris_oktobris_novembris_decembris".split("_"),monthsShort:"jan_feb_mar_apr_mai_jūn_jūl_aug_sep_okt_nov_dec".split("_"),weekdays:"svētdiena_pirmdiena_otrdiena_trešdiena_ceturtdiena_piektdiena_sestdiena".split("_"),weekdaysShort:"Sv_P_O_T_C_Pk_S".split("_"),weekdaysMin:"Sv_P_O_T_C_Pk_S".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY.",LL:"YYYY. [gada] D. MMMM",LLL:"YYYY. [gada] D. MMMM, HH:mm",LLLL:"YYYY. [gada] D. MMMM, dddd, HH:mm"},calendar:{sameDay:"[Šodien pulksten] LT",nextDay:"[Rīt pulksten] LT",nextWeek:"dddd [pulksten] LT",lastDay:"[Vakar pulksten] LT",lastWeek:"[Pagājušā] dddd [pulksten] LT",sameElse:"L"},relativeTime:{future:"pēc %s",past:"pirms %s",s:e,m:d,mm:c,h:d,hh:c,d:d,dd:c,M:d,MM:c,y:d,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return g}(),a.fullCalendar.datepickerLang("lv","lv",{closeText:"Aizvērt",prevText:"Iepr.",nextText:"Nāk.",currentText:"Šodien",monthNames:["Janvāris","Februāris","Marts","Aprīlis","Maijs","Jūnijs","Jūlijs","Augusts","Septembris","Oktobris","Novembris","Decembris"],monthNamesShort:["Jan","Feb","Mar","Apr","Mai","Jūn","Jūl","Aug","Sep","Okt","Nov","Dec"],dayNames:["svētdiena","pirmdiena","otrdiena","trešdiena","ceturtdiena","piektdiena","sestdiena"],dayNamesShort:["svt","prm","otr","tre","ctr","pkt","sst"],dayNamesMin:["Sv","Pr","Ot","Tr","Ct","Pk","Ss"],weekHeader:"Ned.",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("lv",{buttonText:{month:"Mēnesis",week:"Nedēļa",day:"Diena",list:"Dienas kārtība"},allDayText:"Visu dienu",eventLimitText:function(a){return"+vēl "+a}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"nb",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.".split("_"),monthsParseExact:!0,weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"sø._ma._ti._on._to._fr._lø.".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] HH:mm",LLLL:"dddd D. MMMM YYYY [kl.] HH:mm"},calendar:{sameDay:"[i dag kl.] LT",nextDay:"[i morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[i går kl.] LT",lastWeek:"[forrige] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"noen sekunder",m:"ett minutt",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dager",M:"en måned",MM:"%d måneder",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("nb","nb",{closeText:"Lukk",prevText:"«Forrige",nextText:"Neste»",currentText:"I dag",monthNames:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],monthNamesShort:["jan","feb","mar","apr","mai","jun","jul","aug","sep","okt","nov","des"],dayNamesShort:["søn","man","tir","ons","tor","fre","lør"],dayNames:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"],dayNamesMin:["sø","ma","ti","on","to","fr","lø"],weekHeader:"Uke",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nb",{buttonText:{month:"Måned",week:"Uke",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"til"})}(),function(){!function(){"use strict";var a="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),c="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_"),d=(b.defineLocale||b.lang).call(b,"nl",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(b,d){return/-MMM-/.test(d)?c[b.month()]:a[b.month()]},monthsParseExact:!0,weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"Zo_Ma_Di_Wo_Do_Vr_Za".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",m:"één minuut",mm:"%d minuten",h:"één uur",hh:"%d uur",d:"één dag",dd:"%d dagen",M:"één maand",MM:"%d maanden",y:"één jaar",yy:"%d jaar"},ordinalParse:/\d{1,2}(ste|de)/,ordinal:function(a){return a+(1===a||8===a||a>=20?"ste":"de")},week:{dow:1,doy:4}});return d}(),a.fullCalendar.datepickerLang("nl","nl",{closeText:"Sluiten",prevText:"←",nextText:"→",currentText:"Vandaag",monthNames:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],monthNamesShort:["jan","feb","mrt","apr","mei","jun","jul","aug","sep","okt","nov","dec"],dayNames:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],dayNamesShort:["zon","maa","din","woe","don","vri","zat"],dayNamesMin:["zo","ma","di","wo","do","vr","za"],weekHeader:"Wk",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nl",{buttonText:{month:"Maand",week:"Week",day:"Dag",list:"Agenda"},allDayText:"Hele dag",eventLimitText:"extra"})}(),function(){!function(){"use strict";function a(a){return 5>a%10&&a%10>1&&~~(a/10)%10!==1}function c(b,c,d){var e=b+" ";switch(d){case"m":return c?"minuta":"minutę";case"mm":return e+(a(b)?"minuty":"minut");case"h":return c?"godzina":"godzinę";case"hh":return e+(a(b)?"godziny":"godzin");case"MM":return e+(a(b)?"miesiące":"miesięcy");case"yy":return e+(a(b)?"lata":"lat")}}var d="styczeń_luty_marzec_kwiecień_maj_czerwiec_lipiec_sierpień_wrzesień_październik_listopad_grudzień".split("_"),e="stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia".split("_"),f=(b.defineLocale||b.lang).call(b,"pl",{months:function(a,b){return""===b?"("+e[a.month()]+"|"+d[a.month()]+")":/D MMMM/.test(b)?e[a.month()]:d[a.month()]},monthsShort:"sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru".split("_"),weekdays:"niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota".split("_"),weekdaysShort:"nie_pon_wt_śr_czw_pt_sb".split("_"),weekdaysMin:"Nd_Pn_Wt_Śr_Cz_Pt_So".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Dziś o] LT",nextDay:"[Jutro o] LT",nextWeek:"[W] dddd [o] LT",lastDay:"[Wczoraj o] LT",lastWeek:function(){switch(this.day()){case 0:return"[W zeszłą niedzielę o] LT";case 3:return"[W zeszłą środę o] LT";case 6:return"[W zeszłą sobotę o] LT";default:return"[W zeszły] dddd [o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"%s temu",s:"kilka sekund",m:c,mm:c,h:c,hh:c,d:"1 dzień",dd:"%d dni",M:"miesiąc",MM:c,y:"rok",yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return f}(),a.fullCalendar.datepickerLang("pl","pl",{closeText:"Zamknij",prevText:"<Poprzedni",nextText:"Następny>",currentText:"Dziś",monthNames:["Styczeń","Luty","Marzec","Kwiecień","Maj","Czerwiec","Lipiec","Sierpień","Wrzesień","Październik","Listopad","Grudzień"],monthNamesShort:["Sty","Lu","Mar","Kw","Maj","Cze","Lip","Sie","Wrz","Pa","Lis","Gru"],dayNames:["Niedziela","Poniedziałek","Wtorek","Środa","Czwartek","Piątek","Sobota"],dayNamesShort:["Nie","Pn","Wt","Śr","Czw","Pt","So"],dayNamesMin:["N","Pn","Wt","Śr","Cz","Pt","So"],weekHeader:"Tydz",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pl",{buttonText:{month:"Miesiąc",week:"Tydzień",day:"Dzień",list:"Plan dnia"},allDayText:"Cały dzień",eventLimitText:"więcej"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"pt-br",{months:"Janeiro_Fevereiro_Março_Abril_Maio_Junho_Julho_Agosto_Setembro_Outubro_Novembro_Dezembro".split("_"),monthsShort:"Jan_Fev_Mar_Abr_Mai_Jun_Jul_Ago_Set_Out_Nov_Dez".split("_"),weekdays:"Domingo_Segunda-feira_Terça-feira_Quarta-feira_Quinta-feira_Sexta-feira_Sábado".split("_"),weekdaysShort:"Dom_Seg_Ter_Qua_Qui_Sex_Sáb".split("_"),weekdaysMin:"Dom_2ª_3ª_4ª_5ª_6ª_Sáb".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY [às] HH:mm",LLLL:"dddd, D [de] MMMM [de] YYYY [às] HH:mm"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"%s atrás",s:"poucos segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº"});return a}(),a.fullCalendar.datepickerLang("pt-br","pt-BR",{closeText:"Fechar",prevText:"<Anterior",nextText:"Próximo>",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt-br",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Compromissos"},allDayText:"dia inteiro",eventLimitText:function(a){return"mais +"+a}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"pt",{months:"Janeiro_Fevereiro_Março_Abril_Maio_Junho_Julho_Agosto_Setembro_Outubro_Novembro_Dezembro".split("_"),monthsShort:"Jan_Fev_Mar_Abr_Mai_Jun_Jul_Ago_Set_Out_Nov_Dez".split("_"),weekdays:"Domingo_Segunda-Feira_Terça-Feira_Quarta-Feira_Quinta-Feira_Sexta-Feira_Sábado".split("_"),weekdaysShort:"Dom_Seg_Ter_Qua_Qui_Sex_Sáb".split("_"),weekdaysMin:"Dom_2ª_3ª_4ª_5ª_6ª_Sáb".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY HH:mm",LLLL:"dddd, D [de] MMMM [de] YYYY HH:mm"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"há %s",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("pt","pt",{closeText:"Fechar",prevText:"Anterior",nextText:"Seguinte",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sem",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Agenda"},allDayText:"Todo o dia",eventLimitText:"mais"})}(),function(){!function(){"use strict";function a(a,b,c){var d={mm:"minute",hh:"ore",dd:"zile",MM:"luni",yy:"ani"},e=" ";return(a%100>=20||a>=100&&a%100===0)&&(e=" de "),a+e+d[c]}var c=(b.defineLocale||b.lang).call(b,"ro",{months:"ianuarie_februarie_martie_aprilie_mai_iunie_iulie_august_septembrie_octombrie_noiembrie_decembrie".split("_"),monthsShort:"ian._febr._mart._apr._mai_iun._iul._aug._sept._oct._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"duminică_luni_marți_miercuri_joi_vineri_sâmbătă".split("_"),weekdaysShort:"Dum_Lun_Mar_Mie_Joi_Vin_Sâm".split("_"),weekdaysMin:"Du_Lu_Ma_Mi_Jo_Vi_Sâ".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[azi la] LT",nextDay:"[mâine la] LT",nextWeek:"dddd [la] LT",lastDay:"[ieri la] LT",lastWeek:"[fosta] dddd [la] LT",sameElse:"L"},relativeTime:{future:"peste %s",past:"%s în urmă",s:"câteva secunde",m:"un minut",mm:a,h:"o oră",hh:a,d:"o zi",dd:a,M:"o lună",MM:a,y:"un an",yy:a},week:{dow:1,doy:7}});return c}(),a.fullCalendar.datepickerLang("ro","ro",{closeText:"Închide",prevText:"« Luna precedentă",nextText:"Luna următoare »",currentText:"Azi",monthNames:["Ianuarie","Februarie","Martie","Aprilie","Mai","Iunie","Iulie","August","Septembrie","Octombrie","Noiembrie","Decembrie"],monthNamesShort:["Ian","Feb","Mar","Apr","Mai","Iun","Iul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Duminică","Luni","Marţi","Miercuri","Joi","Vineri","Sâmbătă"],dayNamesShort:["Dum","Lun","Mar","Mie","Joi","Vin","Sâm"],dayNamesMin:["Du","Lu","Ma","Mi","Jo","Vi","Sâ"],weekHeader:"Săpt",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ro",{buttonText:{prev:"precedentă",next:"următoare",month:"Lună",week:"Săptămână",day:"Zi",list:"Agendă"},allDayText:"Toată ziua",eventLimitText:function(a){return"+alte "+a}})}(),function(){!function(){"use strict";function a(a,b){var c=a.split("_");return b%10===1&&b%100!==11?c[0]:b%10>=2&&4>=b%10&&(10>b%100||b%100>=20)?c[1]:c[2]}function c(b,c,d){var e={mm:c?"минута_минуты_минут":"минуту_минуты_минут",hh:"час_часа_часов",dd:"день_дня_дней",MM:"месяц_месяца_месяцев",yy:"год_года_лет"};return"m"===d?c?"минута":"минуту":b+" "+a(e[d],+b)}var d=[/^янв/i,/^фев/i,/^мар/i,/^апр/i,/^ма[йя]/i,/^июн/i,/^июл/i,/^авг/i,/^сен/i,/^окт/i,/^ноя/i,/^дек/i],e=(b.defineLocale||b.lang).call(b,"ru",{months:{format:"января_февраля_марта_апреля_мая_июня_июля_августа_сентября_октября_ноября_декабря".split("_"),standalone:"январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь".split("_")},monthsShort:{format:"янв._февр._мар._апр._мая_июня_июля_авг._сент._окт._нояб._дек.".split("_"),standalone:"янв._февр._март_апр._май_июнь_июль_авг._сент._окт._нояб._дек.".split("_")},weekdays:{standalone:"воскресенье_понедельник_вторник_среда_четверг_пятница_суббота".split("_"),format:"воскресенье_понедельник_вторник_среду_четверг_пятницу_субботу".split("_"),isFormat:/\[ ?[Вв] ?(?:прошлую|следующую|эту)? ?\] ?dddd/},weekdaysShort:"вс_пн_вт_ср_чт_пт_сб".split("_"),weekdaysMin:"вс_пн_вт_ср_чт_пт_сб".split("_"),monthsParse:d,longMonthsParse:d,shortMonthsParse:d,monthsRegex:/^(сентябр[яь]|октябр[яь]|декабр[яь]|феврал[яь]|январ[яь]|апрел[яь]|августа?|ноябр[яь]|сент\.|февр\.|нояб\.|июнь|янв.|июль|дек.|авг.|апр.|марта|мар[.т]|окт.|июн[яь]|июл[яь]|ма[яй])/i,monthsShortRegex:/^(сентябр[яь]|октябр[яь]|декабр[яь]|феврал[яь]|январ[яь]|апрел[яь]|августа?|ноябр[яь]|сент\.|февр\.|нояб\.|июнь|янв.|июль|дек.|авг.|апр.|марта|мар[.т]|окт.|июн[яь]|июл[яь]|ма[яй])/i,monthsStrictRegex:/^(сентябр[яь]|октябр[яь]|декабр[яь]|феврал[яь]|январ[яь]|апрел[яь]|августа?|ноябр[яь]|марта?|июн[яь]|июл[яь]|ма[яй])/i,monthsShortStrictRegex:/^(нояб\.|февр\.|сент\.|июль|янв\.|июн[яь]|мар[.т]|авг\.|апр\.|окт\.|дек\.|ма[яй])/i,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY г.",LLL:"D MMMM YYYY г., HH:mm",LLLL:"dddd, D MMMM YYYY г., HH:mm"},calendar:{sameDay:"[Сегодня в] LT",nextDay:"[Завтра в] LT",lastDay:"[Вчера в] LT",nextWeek:function(a){if(a.week()===this.week())return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT";switch(this.day()){case 0:return"[В следующее] dddd [в] LT";case 1:case 2:case 4:return"[В следующий] dddd [в] LT";case 3:case 5:case 6:return"[В следующую] dddd [в] LT"}},lastWeek:function(a){if(a.week()===this.week())return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT";switch(this.day()){case 0:return"[В прошлое] dddd [в] LT";case 1:case 2:case 4:return"[В прошлый] dddd [в] LT";case 3:case 5:case 6:return"[В прошлую] dddd [в] LT"}},sameElse:"L"},relativeTime:{future:"через %s",past:"%s назад",s:"несколько секунд",m:c,mm:c,h:"час",hh:c,d:"день",dd:c,M:"месяц",MM:c,y:"год",yy:c},meridiemParse:/ночи|утра|дня|вечера/i,isPM:function(a){return/^(дня|вечера)$/.test(a)},meridiem:function(a,b,c){return 4>a?"ночи":12>a?"утра":17>a?"дня":"вечера"},ordinalParse:/\d{1,2}-(й|го|я)/,ordinal:function(a,b){switch(b){case"M":case"d":case"DDD":return a+"-й";case"D":return a+"-го";case"w":case"W":return a+"-я";default:return a}},week:{dow:1,doy:7}});return e}(),a.fullCalendar.datepickerLang("ru","ru",{closeText:"Закрыть",prevText:"<Пред",nextText:"След>",currentText:"Сегодня",monthNames:["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"],monthNamesShort:["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"],dayNames:["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"],dayNamesShort:["вск","пнд","втр","срд","чтв","птн","сбт"],dayNamesMin:["Вс","Пн","Вт","Ср","Чт","Пт","Сб"],weekHeader:"Нед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ru",{buttonText:{month:"Месяц",week:"Неделя",day:"День",list:"Повестка дня"},allDayText:"Весь день",eventLimitText:function(a){return"+ ещё "+a}})}(),function(){!function(){"use strict";function a(a){return a>1&&5>a}function c(b,c,d,e){var f=b+" ";switch(d){case"s":return c||e?"pár sekúnd":"pár sekundami";case"m":return c?"minúta":e?"minútu":"minútou";case"mm":return c||e?f+(a(b)?"minúty":"minút"):f+"minútami";case"h":return c?"hodina":e?"hodinu":"hodinou";case"hh":return c||e?f+(a(b)?"hodiny":"hodín"):f+"hodinami";case"d":return c||e?"deň":"dňom";case"dd":return c||e?f+(a(b)?"dni":"dní"):f+"dňami";case"M":return c||e?"mesiac":"mesiacom";case"MM":return c||e?f+(a(b)?"mesiace":"mesiacov"):f+"mesiacmi";case"y":return c||e?"rok":"rokom";case"yy":return c||e?f+(a(b)?"roky":"rokov"):f+"rokmi"}}var d="január_február_marec_apríl_máj_jún_júl_august_september_október_november_december".split("_"),e="jan_feb_mar_apr_máj_jún_júl_aug_sep_okt_nov_dec".split("_"),f=(b.defineLocale||b.lang).call(b,"sk",{months:d,monthsShort:e,weekdays:"nedeľa_pondelok_utorok_streda_štvrtok_piatok_sobota".split("_"),weekdaysShort:"ne_po_ut_st_št_pi_so".split("_"),weekdaysMin:"ne_po_ut_st_št_pi_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd D. MMMM YYYY H:mm"},calendar:{sameDay:"[dnes o] LT",nextDay:"[zajtra o] LT",nextWeek:function(){switch(this.day()){case 0:return"[v nedeľu o] LT";case 1:case 2:return"[v] dddd [o] LT";case 3:return"[v stredu o] LT";case 4:return"[vo štvrtok o] LT";case 5:return"[v piatok o] LT";case 6:return"[v sobotu o] LT"}},lastDay:"[včera o] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulú nedeľu o] LT";case 1:case 2:return"[minulý] dddd [o] LT";case 3:return"[minulú stredu o] LT";case 4:case 5:return"[minulý] dddd [o] LT";case 6:return"[minulú sobotu o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"pred %s",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return f}(),a.fullCalendar.datepickerLang("sk","sk",{closeText:"Zavrieť",prevText:"<Predchádzajúci",nextText:"Nasledujúci>",currentText:"Dnes",monthNames:["január","február","marec","apríl","máj","jún","júl","august","september","október","november","december"],monthNamesShort:["Jan","Feb","Mar","Apr","Máj","Jún","Júl","Aug","Sep","Okt","Nov","Dec"],dayNames:["nedeľa","pondelok","utorok","streda","štvrtok","piatok","sobota"],dayNamesShort:["Ned","Pon","Uto","Str","Štv","Pia","Sob"],dayNamesMin:["Ne","Po","Ut","St","Št","Pia","So"],weekHeader:"Ty",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sk",{buttonText:{month:"Mesiac",week:"Týždeň",day:"Deň",list:"Rozvrh"},allDayText:"Celý deň",eventLimitText:function(a){return"+ďalšie: "+a}})}(),function(){!function(){"use strict";function a(a,b,c,d){var e=a+" ";switch(c){case"s":return b||d?"nekaj sekund":"nekaj sekundami";case"m":return b?"ena minuta":"eno minuto";case"mm":return e+=1===a?b?"minuta":"minuto":2===a?b||d?"minuti":"minutama":5>a?b||d?"minute":"minutami":b||d?"minut":"minutami";case"h":return b?"ena ura":"eno uro";case"hh":return e+=1===a?b?"ura":"uro":2===a?b||d?"uri":"urama":5>a?b||d?"ure":"urami":b||d?"ur":"urami";case"d":return b||d?"en dan":"enim dnem";case"dd":return e+=1===a?b||d?"dan":"dnem":2===a?b||d?"dni":"dnevoma":b||d?"dni":"dnevi";case"M":return b||d?"en mesec":"enim mesecem";case"MM":return e+=1===a?b||d?"mesec":"mesecem":2===a?b||d?"meseca":"mesecema":5>a?b||d?"mesece":"meseci":b||d?"mesecev":"meseci";case"y":return b||d?"eno leto":"enim letom";case"yy":return e+=1===a?b||d?"leto":"letom":2===a?b||d?"leti":"letoma":5>a?b||d?"leta":"leti":b||d?"let":"leti"}}var c=(b.defineLocale||b.lang).call(b,"sl",{months:"januar_februar_marec_april_maj_junij_julij_avgust_september_oktober_november_december".split("_"),monthsShort:"jan._feb._mar._apr._maj._jun._jul._avg._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedelja_ponedeljek_torek_sreda_četrtek_petek_sobota".split("_"),weekdaysShort:"ned._pon._tor._sre._čet._pet._sob.".split("_"),weekdaysMin:"ne_po_to_sr_če_pe_so".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danes ob] LT",nextDay:"[jutri ob] LT",nextWeek:function(){switch(this.day()){case 0:return"[v] [nedeljo] [ob] LT";case 3:return"[v] [sredo] [ob] LT";case 6:return"[v] [soboto] [ob] LT";case 1:case 2:case 4:case 5:return"[v] dddd [ob] LT"}},lastDay:"[včeraj ob] LT",lastWeek:function(){ +switch(this.day()){case 0:return"[prejšnjo] [nedeljo] [ob] LT";case 3:return"[prejšnjo] [sredo] [ob] LT";case 6:return"[prejšnjo] [soboto] [ob] LT";case 1:case 2:case 4:case 5:return"[prejšnji] dddd [ob] LT"}},sameElse:"L"},relativeTime:{future:"čez %s",past:"pred %s",s:a,m:a,mm:a,h:a,hh:a,d:a,dd:a,M:a,MM:a,y:a,yy:a},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});return c}(),a.fullCalendar.datepickerLang("sl","sl",{closeText:"Zapri",prevText:"<Prejšnji",nextText:"Naslednji>",currentText:"Trenutni",monthNames:["Januar","Februar","Marec","April","Maj","Junij","Julij","Avgust","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Avg","Sep","Okt","Nov","Dec"],dayNames:["Nedelja","Ponedeljek","Torek","Sreda","Četrtek","Petek","Sobota"],dayNamesShort:["Ned","Pon","Tor","Sre","Čet","Pet","Sob"],dayNamesMin:["Ne","Po","To","Sr","Če","Pe","So"],weekHeader:"Teden",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sl",{buttonText:{month:"Mesec",week:"Teden",day:"Dan",list:"Dnevni red"},allDayText:"Ves dan",eventLimitText:"več"})}(),function(){!function(){"use strict";var a={words:{m:["један минут","једне минуте"],mm:["минут","минуте","минута"],h:["један сат","једног сата"],hh:["сат","сата","сати"],dd:["дан","дана","дана"],MM:["месец","месеца","месеци"],yy:["година","године","година"]},correctGrammaticalCase:function(a,b){return 1===a?b[0]:a>=2&&4>=a?b[1]:b[2]},translate:function(b,c,d){var e=a.words[d];return 1===d.length?c?e[0]:e[1]:b+" "+a.correctGrammaticalCase(b,e)}},c=(b.defineLocale||b.lang).call(b,"sr-cyrl",{months:"јануар_фебруар_март_април_мај_јун_јул_август_септембар_октобар_новембар_децембар".split("_"),monthsShort:"јан._феб._мар._апр._мај_јун_јул_авг._сеп._окт._нов._дец.".split("_"),monthsParseExact:!0,weekdays:"недеља_понедељак_уторак_среда_четвртак_петак_субота".split("_"),weekdaysShort:"нед._пон._уто._сре._чет._пет._суб.".split("_"),weekdaysMin:"не_по_ут_ср_че_пе_су".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[данас у] LT",nextDay:"[сутра у] LT",nextWeek:function(){switch(this.day()){case 0:return"[у] [недељу] [у] LT";case 3:return"[у] [среду] [у] LT";case 6:return"[у] [суботу] [у] LT";case 1:case 2:case 4:case 5:return"[у] dddd [у] LT"}},lastDay:"[јуче у] LT",lastWeek:function(){var a=["[прошле] [недеље] [у] LT","[прошлог] [понедељка] [у] LT","[прошлог] [уторка] [у] LT","[прошле] [среде] [у] LT","[прошлог] [четвртка] [у] LT","[прошлог] [петка] [у] LT","[прошле] [суботе] [у] LT"];return a[this.day()]},sameElse:"L"},relativeTime:{future:"за %s",past:"пре %s",s:"неколико секунди",m:a.translate,mm:a.translate,h:a.translate,hh:a.translate,d:"дан",dd:a.translate,M:"месец",MM:a.translate,y:"годину",yy:a.translate},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});return c}(),a.fullCalendar.datepickerLang("sr-cyrl","sr",{closeText:"Затвори",prevText:"<",nextText:">",currentText:"Данас",monthNames:["Јануар","Фебруар","Март","Април","Мај","Јун","Јул","Август","Септембар","Октобар","Новембар","Децембар"],monthNamesShort:["Јан","Феб","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Нов","Дец"],dayNames:["Недеља","Понедељак","Уторак","Среда","Четвртак","Петак","Субота"],dayNamesShort:["Нед","Пон","Уто","Сре","Чет","Пет","Суб"],dayNamesMin:["Не","По","Ут","Ср","Че","Пе","Су"],weekHeader:"Сед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sr-cyrl",{buttonText:{month:"Месец",week:"Недеља",day:"Дан",list:"Планер"},allDayText:"Цео дан",eventLimitText:function(a){return"+ још "+a}})}(),function(){!function(){"use strict";var a={words:{m:["jedan minut","jedne minute"],mm:["minut","minute","minuta"],h:["jedan sat","jednog sata"],hh:["sat","sata","sati"],dd:["dan","dana","dana"],MM:["mesec","meseca","meseci"],yy:["godina","godine","godina"]},correctGrammaticalCase:function(a,b){return 1===a?b[0]:a>=2&&4>=a?b[1]:b[2]},translate:function(b,c,d){var e=a.words[d];return 1===d.length?c?e[0]:e[1]:b+" "+a.correctGrammaticalCase(b,e)}},c=(b.defineLocale||b.lang).call(b,"sr",{months:"januar_februar_mart_april_maj_jun_jul_avgust_septembar_oktobar_novembar_decembar".split("_"),monthsShort:"jan._feb._mar._apr._maj_jun_jul_avg._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedelja_ponedeljak_utorak_sreda_četvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sre._čet._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_če_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedelju] [u] LT";case 3:return"[u] [sredu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[juče u] LT",lastWeek:function(){var a=["[prošle] [nedelje] [u] LT","[prošlog] [ponedeljka] [u] LT","[prošlog] [utorka] [u] LT","[prošle] [srede] [u] LT","[prošlog] [četvrtka] [u] LT","[prošlog] [petka] [u] LT","[prošle] [subote] [u] LT"];return a[this.day()]},sameElse:"L"},relativeTime:{future:"za %s",past:"pre %s",s:"nekoliko sekundi",m:a.translate,mm:a.translate,h:a.translate,hh:a.translate,d:"dan",dd:a.translate,M:"mesec",MM:a.translate,y:"godinu",yy:a.translate},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});return c}(),a.fullCalendar.datepickerLang("sr","sr",{closeText:"Затвори",prevText:"<",nextText:">",currentText:"Данас",monthNames:["Јануар","Фебруар","Март","Април","Мај","Јун","Јул","Август","Септембар","Октобар","Новембар","Децембар"],monthNamesShort:["Јан","Феб","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Нов","Дец"],dayNames:["Недеља","Понедељак","Уторак","Среда","Четвртак","Петак","Субота"],dayNamesShort:["Нед","Пон","Уто","Сре","Чет","Пет","Суб"],dayNamesMin:["Не","По","Ут","Ср","Че","Пе","Су"],weekHeader:"Сед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sr",{buttonText:{month:"Месец",week:"Недеља",day:"Дан",list:"Планер"},allDayText:"Цео дан",eventLimitText:function(a){return"+ још "+a}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"sv",{months:"januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"söndag_måndag_tisdag_onsdag_torsdag_fredag_lördag".split("_"),weekdaysShort:"sön_mån_tis_ons_tor_fre_lör".split("_"),weekdaysMin:"sö_må_ti_on_to_fr_lö".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [kl.] HH:mm",LLLL:"dddd D MMMM YYYY [kl.] HH:mm",lll:"D MMM YYYY HH:mm",llll:"ddd D MMM YYYY HH:mm"},calendar:{sameDay:"[Idag] LT",nextDay:"[Imorgon] LT",lastDay:"[Igår] LT",nextWeek:"[På] dddd LT",lastWeek:"[I] dddd[s] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"för %s sedan",s:"några sekunder",m:"en minut",mm:"%d minuter",h:"en timme",hh:"%d timmar",d:"en dag",dd:"%d dagar",M:"en månad",MM:"%d månader",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}(e|a)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"e":1===b?"a":2===b?"a":"e";return a+c},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("sv","sv",{closeText:"Stäng",prevText:"«Förra",nextText:"Nästa»",currentText:"Idag",monthNames:["Januari","Februari","Mars","April","Maj","Juni","Juli","Augusti","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNamesShort:["Sön","Mån","Tis","Ons","Tor","Fre","Lör"],dayNames:["Söndag","Måndag","Tisdag","Onsdag","Torsdag","Fredag","Lördag"],dayNamesMin:["Sö","Må","Ti","On","To","Fr","Lö"],weekHeader:"Ve",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sv",{buttonText:{month:"Månad",week:"Vecka",day:"Dag",list:"Program"},allDayText:"Heldag",eventLimitText:"till"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"th",{months:"มกราคม_กุมภาพันธ์_มีนาคม_เมษายน_พฤษภาคม_มิถุนายน_กรกฎาคม_สิงหาคม_กันยายน_ตุลาคม_พฤศจิกายน_ธันวาคม".split("_"),monthsShort:"มกรา_กุมภา_มีนา_เมษา_พฤษภา_มิถุนา_กรกฎา_สิงหา_กันยา_ตุลา_พฤศจิกา_ธันวา".split("_"),monthsParseExact:!0,weekdays:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัสบดี_ศุกร์_เสาร์".split("_"),weekdaysShort:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัส_ศุกร์_เสาร์".split("_"),weekdaysMin:"อา._จ._อ._พ._พฤ._ศ._ส.".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H นาฬิกา m นาที",LTS:"H นาฬิกา m นาที s วินาที",L:"YYYY/MM/DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY เวลา H นาฬิกา m นาที",LLLL:"วันddddที่ D MMMM YYYY เวลา H นาฬิกา m นาที"},meridiemParse:/ก่อนเที่ยง|หลังเที่ยง/,isPM:function(a){return"หลังเที่ยง"===a},meridiem:function(a,b,c){return 12>a?"ก่อนเที่ยง":"หลังเที่ยง"},calendar:{sameDay:"[วันนี้ เวลา] LT",nextDay:"[พรุ่งนี้ เวลา] LT",nextWeek:"dddd[หน้า เวลา] LT",lastDay:"[เมื่อวานนี้ เวลา] LT",lastWeek:"[วัน]dddd[ที่แล้ว เวลา] LT",sameElse:"L"},relativeTime:{future:"อีก %s",past:"%sที่แล้ว",s:"ไม่กี่วินาที",m:"1 นาที",mm:"%d นาที",h:"1 ชั่วโมง",hh:"%d ชั่วโมง",d:"1 วัน",dd:"%d วัน",M:"1 เดือน",MM:"%d เดือน",y:"1 ปี",yy:"%d ปี"}});return a}(),a.fullCalendar.datepickerLang("th","th",{closeText:"ปิด",prevText:"« ย้อน",nextText:"ถัดไป »",currentText:"วันนี้",monthNames:["มกราคม","กุมภาพันธ์","มีนาคม","เมษายน","พฤษภาคม","มิถุนายน","กรกฎาคม","สิงหาคม","กันยายน","ตุลาคม","พฤศจิกายน","ธันวาคม"],monthNamesShort:["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."],dayNames:["อาทิตย์","จันทร์","อังคาร","พุธ","พฤหัสบดี","ศุกร์","เสาร์"],dayNamesShort:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],dayNamesMin:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("th",{buttonText:{month:"เดือน",week:"สัปดาห์",day:"วัน",list:"แผนงาน"},allDayText:"ตลอดวัน",eventLimitText:"เพิ่มเติม"})}(),function(){!function(){"use strict";var a={1:"'inci",5:"'inci",8:"'inci",70:"'inci",80:"'inci",2:"'nci",7:"'nci",20:"'nci",50:"'nci",3:"'üncü",4:"'üncü",100:"'üncü",6:"'ncı",9:"'uncu",10:"'uncu",30:"'uncu",60:"'ıncı",90:"'ıncı"},c=(b.defineLocale||b.lang).call(b,"tr",{months:"Ocak_Şubat_Mart_Nisan_Mayıs_Haziran_Temmuz_Ağustos_Eylül_Ekim_Kasım_Aralık".split("_"),monthsShort:"Oca_Şub_Mar_Nis_May_Haz_Tem_Ağu_Eyl_Eki_Kas_Ara".split("_"),weekdays:"Pazar_Pazartesi_Salı_Çarşamba_Perşembe_Cuma_Cumartesi".split("_"),weekdaysShort:"Paz_Pts_Sal_Çar_Per_Cum_Cts".split("_"),weekdaysMin:"Pz_Pt_Sa_Ça_Pe_Cu_Ct".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[bugün saat] LT",nextDay:"[yarın saat] LT",nextWeek:"[haftaya] dddd [saat] LT",lastDay:"[dün] LT",lastWeek:"[geçen hafta] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s önce",s:"birkaç saniye",m:"bir dakika",mm:"%d dakika",h:"bir saat",hh:"%d saat",d:"bir gün",dd:"%d gün",M:"bir ay",MM:"%d ay",y:"bir yıl",yy:"%d yıl"},ordinalParse:/\d{1,2}'(inci|nci|üncü|ncı|uncu|ıncı)/,ordinal:function(b){if(0===b)return b+"'ıncı";var c=b%10,d=b%100-c,e=b>=100?100:null;return b+(a[c]||a[d]||a[e])},week:{dow:1,doy:7}});return c}(),a.fullCalendar.datepickerLang("tr","tr",{closeText:"kapat",prevText:"<geri",nextText:"ileri>",currentText:"bugün",monthNames:["Ocak","Şubat","Mart","Nisan","Mayıs","Haziran","Temmuz","Ağustos","Eylül","Ekim","Kasım","Aralık"],monthNamesShort:["Oca","Şub","Mar","Nis","May","Haz","Tem","Ağu","Eyl","Eki","Kas","Ara"],dayNames:["Pazar","Pazartesi","Salı","Çarşamba","Perşembe","Cuma","Cumartesi"],dayNamesShort:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],dayNamesMin:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],weekHeader:"Hf",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("tr",{buttonText:{next:"ileri",month:"Ay",week:"Hafta",day:"Gün",list:"Ajanda"},allDayText:"Tüm gün",eventLimitText:"daha fazla"})}(),function(){!function(){"use strict";function a(a,b){var c=a.split("_");return b%10===1&&b%100!==11?c[0]:b%10>=2&&4>=b%10&&(10>b%100||b%100>=20)?c[1]:c[2]}function c(b,c,d){var e={mm:c?"хвилина_хвилини_хвилин":"хвилину_хвилини_хвилин",hh:c?"година_години_годин":"годину_години_годин",dd:"день_дні_днів",MM:"місяць_місяці_місяців",yy:"рік_роки_років"};return"m"===d?c?"хвилина":"хвилину":"h"===d?c?"година":"годину":b+" "+a(e[d],+b)}function d(a,b){var c={nominative:"неділя_понеділок_вівторок_середа_четвер_п’ятниця_субота".split("_"),accusative:"неділю_понеділок_вівторок_середу_четвер_п’ятницю_суботу".split("_"),genitive:"неділі_понеділка_вівторка_середи_четверга_п’ятниці_суботи".split("_")},d=/(\[[ВвУу]\]) ?dddd/.test(b)?"accusative":/\[?(?:минулої|наступної)? ?\] ?dddd/.test(b)?"genitive":"nominative";return c[d][a.day()]}function e(a){return function(){return a+"о"+(11===this.hours()?"б":"")+"] LT"}}var f=(b.defineLocale||b.lang).call(b,"uk",{months:{format:"січня_лютого_березня_квітня_травня_червня_липня_серпня_вересня_жовтня_листопада_грудня".split("_"),standalone:"січень_лютий_березень_квітень_травень_червень_липень_серпень_вересень_жовтень_листопад_грудень".split("_")},monthsShort:"січ_лют_бер_квіт_трав_черв_лип_серп_вер_жовт_лист_груд".split("_"),weekdays:d,weekdaysShort:"нд_пн_вт_ср_чт_пт_сб".split("_"),weekdaysMin:"нд_пн_вт_ср_чт_пт_сб".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY р.",LLL:"D MMMM YYYY р., HH:mm",LLLL:"dddd, D MMMM YYYY р., HH:mm"},calendar:{sameDay:e("[Сьогодні "),nextDay:e("[Завтра "),lastDay:e("[Вчора "),nextWeek:e("[У] dddd ["),lastWeek:function(){switch(this.day()){case 0:case 3:case 5:case 6:return e("[Минулої] dddd [").call(this);case 1:case 2:case 4:return e("[Минулого] dddd [").call(this)}},sameElse:"L"},relativeTime:{future:"за %s",past:"%s тому",s:"декілька секунд",m:c,mm:c,h:"годину",hh:c,d:"день",dd:c,M:"місяць",MM:c,y:"рік",yy:c},meridiemParse:/ночі|ранку|дня|вечора/,isPM:function(a){return/^(дня|вечора)$/.test(a)},meridiem:function(a,b,c){return 4>a?"ночі":12>a?"ранку":17>a?"дня":"вечора"},ordinalParse:/\d{1,2}-(й|го)/,ordinal:function(a,b){switch(b){case"M":case"d":case"DDD":case"w":case"W":return a+"-й";case"D":return a+"-го";default:return a}},week:{dow:1,doy:7}});return f}(),a.fullCalendar.datepickerLang("uk","uk",{closeText:"Закрити",prevText:"<",nextText:">",currentText:"Сьогодні",monthNames:["Січень","Лютий","Березень","Квітень","Травень","Червень","Липень","Серпень","Вересень","Жовтень","Листопад","Грудень"],monthNamesShort:["Січ","Лют","Бер","Кві","Тра","Чер","Лип","Сер","Вер","Жов","Лис","Гру"],dayNames:["неділя","понеділок","вівторок","середа","четвер","п’ятниця","субота"],dayNamesShort:["нед","пнд","вів","срд","чтв","птн","сбт"],dayNamesMin:["Нд","Пн","Вт","Ср","Чт","Пт","Сб"],weekHeader:"Тиж",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("uk",{buttonText:{month:"Місяць",week:"Тиждень",day:"День",list:"Порядок денний"},allDayText:"Увесь день",eventLimitText:function(a){return"+ще "+a+"..."}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"vi",{months:"tháng 1_tháng 2_tháng 3_tháng 4_tháng 5_tháng 6_tháng 7_tháng 8_tháng 9_tháng 10_tháng 11_tháng 12".split("_"),monthsShort:"Th01_Th02_Th03_Th04_Th05_Th06_Th07_Th08_Th09_Th10_Th11_Th12".split("_"),monthsParseExact:!0,weekdays:"chủ nhật_thứ hai_thứ ba_thứ tư_thứ năm_thứ sáu_thứ bảy".split("_"),weekdaysShort:"CN_T2_T3_T4_T5_T6_T7".split("_"),weekdaysMin:"CN_T2_T3_T4_T5_T6_T7".split("_"),weekdaysParseExact:!0,meridiemParse:/sa|ch/i,isPM:function(a){return/^ch$/i.test(a)},meridiem:function(a,b,c){return 12>a?c?"sa":"SA":c?"ch":"CH"},longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM [năm] YYYY",LLL:"D MMMM [năm] YYYY HH:mm",LLLL:"dddd, D MMMM [năm] YYYY HH:mm",l:"DD/M/YYYY",ll:"D MMM YYYY",lll:"D MMM YYYY HH:mm",llll:"ddd, D MMM YYYY HH:mm"},calendar:{sameDay:"[Hôm nay lúc] LT",nextDay:"[Ngày mai lúc] LT",nextWeek:"dddd [tuần tới lúc] LT",lastDay:"[Hôm qua lúc] LT",lastWeek:"dddd [tuần rồi lúc] LT",sameElse:"L"},relativeTime:{future:"%s tới",past:"%s trước",s:"vài giây",m:"một phút",mm:"%d phút",h:"một giờ",hh:"%d giờ",d:"một ngày",dd:"%d ngày",M:"một tháng",MM:"%d tháng",y:"một năm",yy:"%d năm"},ordinalParse:/\d{1,2}/,ordinal:function(a){return a},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("vi","vi",{closeText:"Đóng",prevText:"<Trước",nextText:"Tiếp>",currentText:"Hôm nay",monthNames:["Tháng Một","Tháng Hai","Tháng Ba","Tháng Tư","Tháng Năm","Tháng Sáu","Tháng Bảy","Tháng Tám","Tháng Chín","Tháng Mười","Tháng Mười Một","Tháng Mười Hai"],monthNamesShort:["Tháng 1","Tháng 2","Tháng 3","Tháng 4","Tháng 5","Tháng 6","Tháng 7","Tháng 8","Tháng 9","Tháng 10","Tháng 11","Tháng 12"],dayNames:["Chủ Nhật","Thứ Hai","Thứ Ba","Thứ Tư","Thứ Năm","Thứ Sáu","Thứ Bảy"],dayNamesShort:["CN","T2","T3","T4","T5","T6","T7"],dayNamesMin:["CN","T2","T3","T4","T5","T6","T7"],weekHeader:"Tu",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("vi",{buttonText:{month:"Tháng",week:"Tuần",day:"Ngày",list:"Lịch biểu"},allDayText:"Cả ngày",eventLimitText:function(a){return"+ thêm "+a}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"zh-cn",{months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"周日_周一_周二_周三_周四_周五_周六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),longDateFormat:{LT:"Ah点mm分",LTS:"Ah点m分s秒",L:"YYYY-MM-DD",LL:"YYYY年MMMD日",LLL:"YYYY年MMMD日Ah点mm分",LLLL:"YYYY年MMMD日ddddAh点mm分",l:"YYYY-MM-DD",ll:"YYYY年MMMD日",lll:"YYYY年MMMD日Ah点mm分",llll:"YYYY年MMMD日ddddAh点mm分"},meridiemParse:/凌晨|早上|上午|中午|下午|晚上/,meridiemHour:function(a,b){return 12===a&&(a=0),"凌晨"===b||"早上"===b||"上午"===b?a:"下午"===b||"晚上"===b?a+12:a>=11?a:a+12},meridiem:function(a,b,c){var d=100*a+b;return 600>d?"凌晨":900>d?"早上":1130>d?"上午":1230>d?"中午":1800>d?"下午":"晚上"},calendar:{sameDay:function(){return 0===this.minutes()?"[今天]Ah[点整]":"[今天]LT"},nextDay:function(){return 0===this.minutes()?"[明天]Ah[点整]":"[明天]LT"},lastDay:function(){return 0===this.minutes()?"[昨天]Ah[点整]":"[昨天]LT"},nextWeek:function(){var a,c;return a=b().startOf("week"),c=this.diff(a,"days")>=7?"[下]":"[本]",0===this.minutes()?c+"dddAh点整":c+"dddAh点mm"},lastWeek:function(){var a,c;return a=b().startOf("week"),c=this.unix()=11?a:a+12:"下午"===b||"晚上"===b?a+12:void 0},meridiem:function(a,b,c){var d=100*a+b;return 900>d?"早上":1130>d?"上午":1230>d?"中午":1800>d?"下午":"晚上"},calendar:{sameDay:"[今天]LT",nextDay:"[明天]LT",nextWeek:"[下]ddddLT",lastDay:"[昨天]LT",lastWeek:"[上]ddddLT",sameElse:"L"},ordinalParse:/\d{1,2}(日|月|週)/,ordinal:function(a,b){switch(b){case"d":case"D":case"DDD":return a+"日";case"M":return a+"月";case"w":case"W":return a+"週";default:return a}},relativeTime:{future:"%s內",past:"%s前",s:"幾秒",m:"1分鐘",mm:"%d分鐘",h:"1小時",hh:"%d小時",d:"1天",dd:"%d天",M:"1個月",MM:"%d個月",y:"1年",yy:"%d年"}});return a}(),a.fullCalendar.datepickerLang("zh-tw","zh-TW",{closeText:"關閉",prevText:"<上月",nextText:"下月>",currentText:"今天",monthNames:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthNamesShort:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],dayNames:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayNamesShort:["周日","周一","周二","周三","周四","周五","周六"],dayNamesMin:["日","一","二","三","四","五","六"],weekHeader:"周",dateFormat:"yy/mm/dd",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("zh-tw",{buttonText:{month:"月",week:"週",day:"天",list:"待辦事項"},allDayText:"全天",eventLimitText:"更多"})}(),(b.locale||b.lang).call(b,"en"),a.fullCalendar.lang("en"),a.datepicker&&a.datepicker.setDefaults(a.datepicker.regional[""])}); \ No newline at end of file diff --git a/public/static/libs/fullcalendar/moment.min.js b/public/static/libs/fullcalendar/moment.min.js new file mode 100644 index 0000000..d47269f --- /dev/null +++ b/public/static/libs/fullcalendar/moment.min.js @@ -0,0 +1,7 @@ +//! moment.js +//! version : 2.13.0 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com +!function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.moment=b()}(this,function(){"use strict";function a(){return fd.apply(null,arguments)}function b(a){fd=a}function c(a){return a instanceof Array||"[object Array]"===Object.prototype.toString.call(a)}function d(a){return a instanceof Date||"[object Date]"===Object.prototype.toString.call(a)}function e(a,b){var c,d=[];for(c=0;c0)for(c in hd)d=hd[c],e=b[d],m(e)||(a[d]=e);return a}function o(b){n(this,b),this._d=new Date(null!=b._d?b._d.getTime():NaN),id===!1&&(id=!0,a.updateOffset(this),id=!1)}function p(a){return a instanceof o||null!=a&&null!=a._isAMomentObject}function q(a){return 0>a?Math.ceil(a):Math.floor(a)}function r(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=q(b)),c}function s(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;e>d;d++)(c&&a[d]!==b[d]||!c&&r(a[d])!==r(b[d]))&&g++;return g+f}function t(b){a.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+b)}function u(b,c){var d=!0;return g(function(){return null!=a.deprecationHandler&&a.deprecationHandler(null,b),d&&(t(b+"\nArguments: "+Array.prototype.slice.call(arguments).join(", ")+"\n"+(new Error).stack),d=!1),c.apply(this,arguments)},c)}function v(b,c){null!=a.deprecationHandler&&a.deprecationHandler(b,c),jd[b]||(t(c),jd[b]=!0)}function w(a){return a instanceof Function||"[object Function]"===Object.prototype.toString.call(a)}function x(a){return"[object Object]"===Object.prototype.toString.call(a)}function y(a){var b,c;for(c in a)b=a[c],w(b)?this[c]=b:this["_"+c]=b;this._config=a,this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function z(a,b){var c,d=g({},a);for(c in b)f(b,c)&&(x(a[c])&&x(b[c])?(d[c]={},g(d[c],a[c]),g(d[c],b[c])):null!=b[c]?d[c]=b[c]:delete d[c]);return d}function A(a){null!=a&&this.set(a)}function B(a){return a?a.toLowerCase().replace("_","-"):a}function C(a){for(var b,c,d,e,f=0;f0;){if(d=D(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&s(e,c,!0)>=b-1)break;b--}f++}return null}function D(a){var b=null;if(!nd[a]&&"undefined"!=typeof module&&module&&module.exports)try{b=ld._abbr,require("./locale/"+a),E(b)}catch(c){}return nd[a]}function E(a,b){var c;return a&&(c=m(b)?H(a):F(a,b),c&&(ld=c)),ld._abbr}function F(a,b){return null!==b?(b.abbr=a,null!=nd[a]?(v("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale"),b=z(nd[a]._config,b)):null!=b.parentLocale&&(null!=nd[b.parentLocale]?b=z(nd[b.parentLocale]._config,b):v("parentLocaleUndefined","specified parentLocale is not defined yet")),nd[a]=new A(b),E(a),nd[a]):(delete nd[a],null)}function G(a,b){if(null!=b){var c;null!=nd[a]&&(b=z(nd[a]._config,b)),c=new A(b),c.parentLocale=nd[a],nd[a]=c,E(a)}else null!=nd[a]&&(null!=nd[a].parentLocale?nd[a]=nd[a].parentLocale:null!=nd[a]&&delete nd[a]);return nd[a]}function H(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return ld;if(!c(a)){if(b=D(a))return b;a=[a]}return C(a)}function I(){return kd(nd)}function J(a,b){var c=a.toLowerCase();od[c]=od[c+"s"]=od[b]=a}function K(a){return"string"==typeof a?od[a]||od[a.toLowerCase()]:void 0}function L(a){var b,c,d={};for(c in a)f(a,c)&&(b=K(c),b&&(d[b]=a[c]));return d}function M(b,c){return function(d){return null!=d?(O(this,b,d),a.updateOffset(this,c),this):N(this,b)}}function N(a,b){return a.isValid()?a._d["get"+(a._isUTC?"UTC":"")+b]():NaN}function O(a,b,c){a.isValid()&&a._d["set"+(a._isUTC?"UTC":"")+b](c)}function P(a,b){var c;if("object"==typeof a)for(c in a)this.set(c,a[c]);else if(a=K(a),w(this[a]))return this[a](b);return this}function Q(a,b,c){var d=""+Math.abs(a),e=b-d.length,f=a>=0;return(f?c?"+":"":"-")+Math.pow(10,Math.max(0,e)).toString().substr(1)+d}function R(a,b,c,d){var e=d;"string"==typeof d&&(e=function(){return this[d]()}),a&&(sd[a]=e),b&&(sd[b[0]]=function(){return Q(e.apply(this,arguments),b[1],b[2])}),c&&(sd[c]=function(){return this.localeData().ordinal(e.apply(this,arguments),a)})}function S(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function T(a){var b,c,d=a.match(pd);for(b=0,c=d.length;c>b;b++)sd[d[b]]?d[b]=sd[d[b]]:d[b]=S(d[b]);return function(b){var e,f="";for(e=0;c>e;e++)f+=d[e]instanceof Function?d[e].call(b,a):d[e];return f}}function U(a,b){return a.isValid()?(b=V(b,a.localeData()),rd[b]=rd[b]||T(b),rd[b](a)):a.localeData().invalidDate()}function V(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(qd.lastIndex=0;d>=0&&qd.test(a);)a=a.replace(qd,c),qd.lastIndex=0,d-=1;return a}function W(a,b,c){Kd[a]=w(b)?b:function(a,d){return a&&c?c:b}}function X(a,b){return f(Kd,a)?Kd[a](b._strict,b._locale):new RegExp(Y(a))}function Y(a){return Z(a.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e}))}function Z(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function $(a,b){var c,d=b;for("string"==typeof a&&(a=[a]),"number"==typeof b&&(d=function(a,c){c[b]=r(a)}),c=0;cd;++d)f=h([2e3,d]),this._shortMonthsParse[d]=this.monthsShort(f,"").toLocaleLowerCase(),this._longMonthsParse[d]=this.months(f,"").toLocaleLowerCase();return c?"MMM"===b?(e=md.call(this._shortMonthsParse,g),-1!==e?e:null):(e=md.call(this._longMonthsParse,g),-1!==e?e:null):"MMM"===b?(e=md.call(this._shortMonthsParse,g),-1!==e?e:(e=md.call(this._longMonthsParse,g),-1!==e?e:null)):(e=md.call(this._longMonthsParse,g),-1!==e?e:(e=md.call(this._shortMonthsParse,g),-1!==e?e:null))}function fa(a,b,c){var d,e,f;if(this._monthsParseExact)return ea.call(this,a,b,c);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),d=0;12>d;d++){if(e=h([2e3,d]),c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp("^"+this.months(e,"").replace(".","")+"$","i"),this._shortMonthsParse[d]=new RegExp("^"+this.monthsShort(e,"").replace(".","")+"$","i")),c||this._monthsParse[d]||(f="^"+this.months(e,"")+"|^"+this.monthsShort(e,""),this._monthsParse[d]=new RegExp(f.replace(".",""),"i")),c&&"MMMM"===b&&this._longMonthsParse[d].test(a))return d;if(c&&"MMM"===b&&this._shortMonthsParse[d].test(a))return d;if(!c&&this._monthsParse[d].test(a))return d}}function ga(a,b){var c;if(!a.isValid())return a;if("string"==typeof b)if(/^\d+$/.test(b))b=r(b);else if(b=a.localeData().monthsParse(b),"number"!=typeof b)return a;return c=Math.min(a.date(),ba(a.year(),b)),a._d["set"+(a._isUTC?"UTC":"")+"Month"](b,c),a}function ha(b){return null!=b?(ga(this,b),a.updateOffset(this,!0),this):N(this,"Month")}function ia(){return ba(this.year(),this.month())}function ja(a){return this._monthsParseExact?(f(this,"_monthsRegex")||la.call(this),a?this._monthsShortStrictRegex:this._monthsShortRegex):this._monthsShortStrictRegex&&a?this._monthsShortStrictRegex:this._monthsShortRegex}function ka(a){return this._monthsParseExact?(f(this,"_monthsRegex")||la.call(this),a?this._monthsStrictRegex:this._monthsRegex):this._monthsStrictRegex&&a?this._monthsStrictRegex:this._monthsRegex}function la(){function a(a,b){return b.length-a.length}var b,c,d=[],e=[],f=[];for(b=0;12>b;b++)c=h([2e3,b]),d.push(this.monthsShort(c,"")),e.push(this.months(c,"")),f.push(this.months(c,"")),f.push(this.monthsShort(c,""));for(d.sort(a),e.sort(a),f.sort(a),b=0;12>b;b++)d[b]=Z(d[b]),e[b]=Z(e[b]),f[b]=Z(f[b]);this._monthsRegex=new RegExp("^("+f.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+e.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+d.join("|")+")","i")}function ma(a){var b,c=a._a;return c&&-2===j(a).overflow&&(b=c[Nd]<0||c[Nd]>11?Nd:c[Od]<1||c[Od]>ba(c[Md],c[Nd])?Od:c[Pd]<0||c[Pd]>24||24===c[Pd]&&(0!==c[Qd]||0!==c[Rd]||0!==c[Sd])?Pd:c[Qd]<0||c[Qd]>59?Qd:c[Rd]<0||c[Rd]>59?Rd:c[Sd]<0||c[Sd]>999?Sd:-1,j(a)._overflowDayOfYear&&(Md>b||b>Od)&&(b=Od),j(a)._overflowWeeks&&-1===b&&(b=Td),j(a)._overflowWeekday&&-1===b&&(b=Ud),j(a).overflow=b),a}function na(a){var b,c,d,e,f,g,h=a._i,i=$d.exec(h)||_d.exec(h);if(i){for(j(a).iso=!0,b=0,c=be.length;c>b;b++)if(be[b][1].exec(i[1])){e=be[b][0],d=be[b][2]!==!1;break}if(null==e)return void(a._isValid=!1);if(i[3]){for(b=0,c=ce.length;c>b;b++)if(ce[b][1].exec(i[3])){f=(i[2]||" ")+ce[b][0];break}if(null==f)return void(a._isValid=!1)}if(!d&&null!=f)return void(a._isValid=!1);if(i[4]){if(!ae.exec(i[4]))return void(a._isValid=!1);g="Z"}a._f=e+(f||"")+(g||""),Ca(a)}else a._isValid=!1}function oa(b){var c=de.exec(b._i);return null!==c?void(b._d=new Date(+c[1])):(na(b),void(b._isValid===!1&&(delete b._isValid,a.createFromInputFallback(b))))}function pa(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 100>a&&a>=0&&isFinite(h.getFullYear())&&h.setFullYear(a),h}function qa(a){var b=new Date(Date.UTC.apply(null,arguments));return 100>a&&a>=0&&isFinite(b.getUTCFullYear())&&b.setUTCFullYear(a),b}function ra(a){return sa(a)?366:365}function sa(a){return a%4===0&&a%100!==0||a%400===0}function ta(){return sa(this.year())}function ua(a,b,c){var d=7+b-c,e=(7+qa(a,0,d).getUTCDay()-b)%7;return-e+d-1}function va(a,b,c,d,e){var f,g,h=(7+c-d)%7,i=ua(a,d,e),j=1+7*(b-1)+h+i;return 0>=j?(f=a-1,g=ra(f)+j):j>ra(a)?(f=a+1,g=j-ra(a)):(f=a,g=j),{year:f,dayOfYear:g}}function wa(a,b,c){var d,e,f=ua(a.year(),b,c),g=Math.floor((a.dayOfYear()-f-1)/7)+1;return 1>g?(e=a.year()-1,d=g+xa(e,b,c)):g>xa(a.year(),b,c)?(d=g-xa(a.year(),b,c),e=a.year()+1):(e=a.year(),d=g),{week:d,year:e}}function xa(a,b,c){var d=ua(a,b,c),e=ua(a+1,b,c);return(ra(a)-d+e)/7}function ya(a,b,c){return null!=a?a:null!=b?b:c}function za(b){var c=new Date(a.now());return b._useUTC?[c.getUTCFullYear(),c.getUTCMonth(),c.getUTCDate()]:[c.getFullYear(),c.getMonth(),c.getDate()]}function Aa(a){var b,c,d,e,f=[];if(!a._d){for(d=za(a),a._w&&null==a._a[Od]&&null==a._a[Nd]&&Ba(a),a._dayOfYear&&(e=ya(a._a[Md],d[Md]),a._dayOfYear>ra(e)&&(j(a)._overflowDayOfYear=!0),c=qa(e,0,a._dayOfYear),a._a[Nd]=c.getUTCMonth(),a._a[Od]=c.getUTCDate()),b=0;3>b&&null==a._a[b];++b)a._a[b]=f[b]=d[b];for(;7>b;b++)a._a[b]=f[b]=null==a._a[b]?2===b?1:0:a._a[b];24===a._a[Pd]&&0===a._a[Qd]&&0===a._a[Rd]&&0===a._a[Sd]&&(a._nextDay=!0,a._a[Pd]=0),a._d=(a._useUTC?qa:pa).apply(null,f),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[Pd]=24)}}function Ba(a){var b,c,d,e,f,g,h,i;b=a._w,null!=b.GG||null!=b.W||null!=b.E?(f=1,g=4,c=ya(b.GG,a._a[Md],wa(Ka(),1,4).year),d=ya(b.W,1),e=ya(b.E,1),(1>e||e>7)&&(i=!0)):(f=a._locale._week.dow,g=a._locale._week.doy,c=ya(b.gg,a._a[Md],wa(Ka(),f,g).year),d=ya(b.w,1),null!=b.d?(e=b.d,(0>e||e>6)&&(i=!0)):null!=b.e?(e=b.e+f,(b.e<0||b.e>6)&&(i=!0)):e=f),1>d||d>xa(c,f,g)?j(a)._overflowWeeks=!0:null!=i?j(a)._overflowWeekday=!0:(h=va(c,d,e,f,g),a._a[Md]=h.year,a._dayOfYear=h.dayOfYear)}function Ca(b){if(b._f===a.ISO_8601)return void na(b);b._a=[],j(b).empty=!0;var c,d,e,f,g,h=""+b._i,i=h.length,k=0;for(e=V(b._f,b._locale).match(pd)||[],c=0;c0&&j(b).unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),k+=d.length),sd[f]?(d?j(b).empty=!1:j(b).unusedTokens.push(f),aa(f,d,b)):b._strict&&!d&&j(b).unusedTokens.push(f);j(b).charsLeftOver=i-k,h.length>0&&j(b).unusedInput.push(h),j(b).bigHour===!0&&b._a[Pd]<=12&&b._a[Pd]>0&&(j(b).bigHour=void 0),j(b).parsedDateParts=b._a.slice(0),j(b).meridiem=b._meridiem,b._a[Pd]=Da(b._locale,b._a[Pd],b._meridiem),Aa(b),ma(b)}function Da(a,b,c){var d;return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&12>b&&(b+=12),d||12!==b||(b=0),b):b}function Ea(a){var b,c,d,e,f;if(0===a._f.length)return j(a).invalidFormat=!0,void(a._d=new Date(NaN));for(e=0;ef)&&(d=f,c=b));g(a,c||b)}function Fa(a){if(!a._d){var b=L(a._i);a._a=e([b.year,b.month,b.day||b.date,b.hour,b.minute,b.second,b.millisecond],function(a){return a&&parseInt(a,10)}),Aa(a)}}function Ga(a){var b=new o(ma(Ha(a)));return b._nextDay&&(b.add(1,"d"),b._nextDay=void 0),b}function Ha(a){var b=a._i,e=a._f;return a._locale=a._locale||H(a._l),null===b||void 0===e&&""===b?l({nullInput:!0}):("string"==typeof b&&(a._i=b=a._locale.preparse(b)),p(b)?new o(ma(b)):(c(e)?Ea(a):e?Ca(a):d(b)?a._d=b:Ia(a),k(a)||(a._d=null),a))}function Ia(b){var f=b._i;void 0===f?b._d=new Date(a.now()):d(f)?b._d=new Date(f.valueOf()):"string"==typeof f?oa(b):c(f)?(b._a=e(f.slice(0),function(a){return parseInt(a,10)}),Aa(b)):"object"==typeof f?Fa(b):"number"==typeof f?b._d=new Date(f):a.createFromInputFallback(b)}function Ja(a,b,c,d,e){var f={};return"boolean"==typeof c&&(d=c,c=void 0),f._isAMomentObject=!0,f._useUTC=f._isUTC=e,f._l=c,f._i=a,f._f=b,f._strict=d,Ga(f)}function Ka(a,b,c,d){return Ja(a,b,c,d,!1)}function La(a,b){var d,e;if(1===b.length&&c(b[0])&&(b=b[0]),!b.length)return Ka();for(d=b[0],e=1;ea&&(a=-a,c="-"),c+Q(~~(a/60),2)+b+Q(~~a%60,2)})}function Ra(a,b){var c=(b||"").match(a)||[],d=c[c.length-1]||[],e=(d+"").match(ie)||["-",0,0],f=+(60*e[1])+r(e[2]);return"+"===e[0]?f:-f}function Sa(b,c){var e,f;return c._isUTC?(e=c.clone(),f=(p(b)||d(b)?b.valueOf():Ka(b).valueOf())-e.valueOf(),e._d.setTime(e._d.valueOf()+f),a.updateOffset(e,!1),e):Ka(b).local()}function Ta(a){return 15*-Math.round(a._d.getTimezoneOffset()/15)}function Ua(b,c){var d,e=this._offset||0;return this.isValid()?null!=b?("string"==typeof b?b=Ra(Hd,b):Math.abs(b)<16&&(b=60*b),!this._isUTC&&c&&(d=Ta(this)),this._offset=b,this._isUTC=!0,null!=d&&this.add(d,"m"),e!==b&&(!c||this._changeInProgress?jb(this,db(b-e,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?e:Ta(this):null!=b?this:NaN}function Va(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}function Wa(a){return this.utcOffset(0,a)}function Xa(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(Ta(this),"m")),this}function Ya(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(Ra(Gd,this._i)),this}function Za(a){return this.isValid()?(a=a?Ka(a).utcOffset():0,(this.utcOffset()-a)%60===0):!1}function $a(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function _a(){if(!m(this._isDSTShifted))return this._isDSTShifted;var a={};if(n(a,this),a=Ha(a),a._a){var b=a._isUTC?h(a._a):Ka(a._a);this._isDSTShifted=this.isValid()&&s(a._a,b.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function ab(){return this.isValid()?!this._isUTC:!1}function bb(){return this.isValid()?this._isUTC:!1}function cb(){return this.isValid()?this._isUTC&&0===this._offset:!1}function db(a,b){var c,d,e,g=a,h=null;return Pa(a)?g={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(g={},b?g[b]=a:g.milliseconds=a):(h=je.exec(a))?(c="-"===h[1]?-1:1,g={y:0,d:r(h[Od])*c,h:r(h[Pd])*c,m:r(h[Qd])*c,s:r(h[Rd])*c,ms:r(h[Sd])*c}):(h=ke.exec(a))?(c="-"===h[1]?-1:1,g={y:eb(h[2],c),M:eb(h[3],c),w:eb(h[4],c),d:eb(h[5],c),h:eb(h[6],c),m:eb(h[7],c),s:eb(h[8],c)}):null==g?g={}:"object"==typeof g&&("from"in g||"to"in g)&&(e=gb(Ka(g.from),Ka(g.to)),g={},g.ms=e.milliseconds,g.M=e.months),d=new Oa(g),Pa(a)&&f(a,"_locale")&&(d._locale=a._locale),d}function eb(a,b){var c=a&&parseFloat(a.replace(",","."));return(isNaN(c)?0:c)*b}function fb(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function gb(a,b){var c;return a.isValid()&&b.isValid()?(b=Sa(b,a),a.isBefore(b)?c=fb(a,b):(c=fb(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c):{milliseconds:0,months:0}}function hb(a){return 0>a?-1*Math.round(-1*a):Math.round(a)}function ib(a,b){return function(c,d){var e,f;return null===d||isNaN(+d)||(v(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period)."),f=c,c=d,d=f),c="string"==typeof c?+c:c,e=db(c,d),jb(this,e,a),this}}function jb(b,c,d,e){var f=c._milliseconds,g=hb(c._days),h=hb(c._months);b.isValid()&&(e=null==e?!0:e,f&&b._d.setTime(b._d.valueOf()+f*d),g&&O(b,"Date",N(b,"Date")+g*d),h&&ga(b,N(b,"Month")+h*d),e&&a.updateOffset(b,g||h))}function kb(a,b){var c=a||Ka(),d=Sa(c,this).startOf("day"),e=this.diff(d,"days",!0),f=-6>e?"sameElse":-1>e?"lastWeek":0>e?"lastDay":1>e?"sameDay":2>e?"nextDay":7>e?"nextWeek":"sameElse",g=b&&(w(b[f])?b[f]():b[f]);return this.format(g||this.localeData().calendar(f,this,Ka(c)))}function lb(){return new o(this)}function mb(a,b){var c=p(a)?a:Ka(a);return this.isValid()&&c.isValid()?(b=K(m(b)?"millisecond":b),"millisecond"===b?this.valueOf()>c.valueOf():c.valueOf()b-f?(c=a.clone().add(e-1,"months"),d=(b-f)/(f-c)):(c=a.clone().add(e+1,"months"),d=(b-f)/(c-f)),-(e+d)||0}function ub(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function vb(){var a=this.clone().utc();return 0f&&(b=f),Vb.call(this,a,b,c,d,e))}function Vb(a,b,c,d,e){var f=va(a,b,c,d,e),g=qa(f.year,0,f.dayOfYear);return this.year(g.getUTCFullYear()),this.month(g.getUTCMonth()),this.date(g.getUTCDate()),this}function Wb(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)}function Xb(a){return wa(a,this._week.dow,this._week.doy).week}function Yb(){return this._week.dow}function Zb(){return this._week.doy}function $b(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")}function _b(a){var b=wa(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")}function ac(a,b){return"string"!=typeof a?a:isNaN(a)?(a=b.weekdaysParse(a),"number"==typeof a?a:null):parseInt(a,10)}function bc(a,b){return c(this._weekdays)?this._weekdays[a.day()]:this._weekdays[this._weekdays.isFormat.test(b)?"format":"standalone"][a.day()]}function cc(a){return this._weekdaysShort[a.day()]}function dc(a){return this._weekdaysMin[a.day()]}function ec(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],d=0;7>d;++d)f=h([2e3,1]).day(d),this._minWeekdaysParse[d]=this.weekdaysMin(f,"").toLocaleLowerCase(),this._shortWeekdaysParse[d]=this.weekdaysShort(f,"").toLocaleLowerCase(),this._weekdaysParse[d]=this.weekdays(f,"").toLocaleLowerCase();return c?"dddd"===b?(e=md.call(this._weekdaysParse,g),-1!==e?e:null):"ddd"===b?(e=md.call(this._shortWeekdaysParse,g),-1!==e?e:null):(e=md.call(this._minWeekdaysParse,g),-1!==e?e:null):"dddd"===b?(e=md.call(this._weekdaysParse,g),-1!==e?e:(e=md.call(this._shortWeekdaysParse,g),-1!==e?e:(e=md.call(this._minWeekdaysParse,g),-1!==e?e:null))):"ddd"===b?(e=md.call(this._shortWeekdaysParse,g),-1!==e?e:(e=md.call(this._weekdaysParse,g),-1!==e?e:(e=md.call(this._minWeekdaysParse,g),-1!==e?e:null))):(e=md.call(this._minWeekdaysParse,g),-1!==e?e:(e=md.call(this._weekdaysParse,g),-1!==e?e:(e=md.call(this._shortWeekdaysParse,g),-1!==e?e:null)))}function fc(a,b,c){var d,e,f;if(this._weekdaysParseExact)return ec.call(this,a,b,c);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),d=0;7>d;d++){if(e=h([2e3,1]).day(d),c&&!this._fullWeekdaysParse[d]&&(this._fullWeekdaysParse[d]=new RegExp("^"+this.weekdays(e,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[d]=new RegExp("^"+this.weekdaysShort(e,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[d]=new RegExp("^"+this.weekdaysMin(e,"").replace(".",".?")+"$","i")),this._weekdaysParse[d]||(f="^"+this.weekdays(e,"")+"|^"+this.weekdaysShort(e,"")+"|^"+this.weekdaysMin(e,""),this._weekdaysParse[d]=new RegExp(f.replace(".",""),"i")),c&&"dddd"===b&&this._fullWeekdaysParse[d].test(a))return d;if(c&&"ddd"===b&&this._shortWeekdaysParse[d].test(a))return d;if(c&&"dd"===b&&this._minWeekdaysParse[d].test(a))return d;if(!c&&this._weekdaysParse[d].test(a))return d}}function gc(a){if(!this.isValid())return null!=a?this:NaN;var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=ac(a,this.localeData()),this.add(a-b,"d")):b}function hc(a){if(!this.isValid())return null!=a?this:NaN;var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")}function ic(a){return this.isValid()?null==a?this.day()||7:this.day(this.day()%7?a:a-7):null!=a?this:NaN}function jc(a){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||mc.call(this),a?this._weekdaysStrictRegex:this._weekdaysRegex):this._weekdaysStrictRegex&&a?this._weekdaysStrictRegex:this._weekdaysRegex}function kc(a){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||mc.call(this),a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):this._weekdaysShortStrictRegex&&a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex}function lc(a){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||mc.call(this),a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):this._weekdaysMinStrictRegex&&a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex}function mc(){function a(a,b){return b.length-a.length}var b,c,d,e,f,g=[],i=[],j=[],k=[];for(b=0;7>b;b++)c=h([2e3,1]).day(b),d=this.weekdaysMin(c,""),e=this.weekdaysShort(c,""),f=this.weekdays(c,""),g.push(d),i.push(e),j.push(f),k.push(d),k.push(e),k.push(f);for(g.sort(a),i.sort(a),j.sort(a),k.sort(a),b=0;7>b;b++)i[b]=Z(i[b]),j[b]=Z(j[b]),k[b]=Z(k[b]);this._weekdaysRegex=new RegExp("^("+k.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+j.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+g.join("|")+")","i")}function nc(a){var b=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")}function oc(){return this.hours()%12||12}function pc(){return this.hours()||24}function qc(a,b){R(a,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),b)})}function rc(a,b){return b._meridiemParse}function sc(a){return"p"===(a+"").toLowerCase().charAt(0)}function tc(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"}function uc(a,b){b[Sd]=r(1e3*("0."+a))}function vc(){return this._isUTC?"UTC":""}function wc(){return this._isUTC?"Coordinated Universal Time":""}function xc(a){return Ka(1e3*a)}function yc(){return Ka.apply(null,arguments).parseZone()}function zc(a,b,c){var d=this._calendar[a];return w(d)?d.call(b,c):d}function Ac(a){var b=this._longDateFormat[a],c=this._longDateFormat[a.toUpperCase()];return b||!c?b:(this._longDateFormat[a]=c.replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a])}function Bc(){return this._invalidDate}function Cc(a){return this._ordinal.replace("%d",a)}function Dc(a){return a}function Ec(a,b,c,d){var e=this._relativeTime[c];return w(e)?e(a,b,c,d):e.replace(/%d/i,a)}function Fc(a,b){var c=this._relativeTime[a>0?"future":"past"];return w(c)?c(b):c.replace(/%s/i,b)}function Gc(a,b,c,d){var e=H(),f=h().set(d,b);return e[c](f,a)}function Hc(a,b,c){if("number"==typeof a&&(b=a,a=void 0),a=a||"",null!=b)return Gc(a,b,c,"month");var d,e=[];for(d=0;12>d;d++)e[d]=Gc(a,d,c,"month");return e}function Ic(a,b,c,d){"boolean"==typeof a?("number"==typeof b&&(c=b,b=void 0),b=b||""):(b=a,c=b,a=!1,"number"==typeof b&&(c=b,b=void 0),b=b||"");var e=H(),f=a?e._week.dow:0;if(null!=c)return Gc(b,(c+f)%7,d,"day");var g,h=[];for(g=0;7>g;g++)h[g]=Gc(b,(g+f)%7,d,"day");return h}function Jc(a,b){return Hc(a,b,"months")}function Kc(a,b){return Hc(a,b,"monthsShort")}function Lc(a,b,c){return Ic(a,b,c,"weekdays")}function Mc(a,b,c){return Ic(a,b,c,"weekdaysShort")}function Nc(a,b,c){return Ic(a,b,c,"weekdaysMin")}function Oc(){var a=this._data;return this._milliseconds=Le(this._milliseconds),this._days=Le(this._days),this._months=Le(this._months),a.milliseconds=Le(a.milliseconds),a.seconds=Le(a.seconds),a.minutes=Le(a.minutes),a.hours=Le(a.hours),a.months=Le(a.months),a.years=Le(a.years),this}function Pc(a,b,c,d){var e=db(b,c);return a._milliseconds+=d*e._milliseconds,a._days+=d*e._days,a._months+=d*e._months,a._bubble()}function Qc(a,b){return Pc(this,a,b,1)}function Rc(a,b){return Pc(this,a,b,-1)}function Sc(a){return 0>a?Math.floor(a):Math.ceil(a)}function Tc(){var a,b,c,d,e,f=this._milliseconds,g=this._days,h=this._months,i=this._data;return f>=0&&g>=0&&h>=0||0>=f&&0>=g&&0>=h||(f+=864e5*Sc(Vc(h)+g),g=0,h=0),i.milliseconds=f%1e3,a=q(f/1e3),i.seconds=a%60,b=q(a/60),i.minutes=b%60,c=q(b/60),i.hours=c%24,g+=q(c/24),e=q(Uc(g)),h+=e,g-=Sc(Vc(e)),d=q(h/12),h%=12,i.days=g,i.months=h,i.years=d,this}function Uc(a){return 4800*a/146097}function Vc(a){return 146097*a/4800}function Wc(a){var b,c,d=this._milliseconds;if(a=K(a),"month"===a||"year"===a)return b=this._days+d/864e5,c=this._months+Uc(b),"month"===a?c:c/12;switch(b=this._days+Math.round(Vc(this._months)),a){case"week":return b/7+d/6048e5;case"day":return b+d/864e5;case"hour":return 24*b+d/36e5;case"minute":return 1440*b+d/6e4;case"second":return 86400*b+d/1e3;case"millisecond":return Math.floor(864e5*b)+d;default:throw new Error("Unknown unit "+a)}}function Xc(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*r(this._months/12)}function Yc(a){return function(){return this.as(a)}}function Zc(a){ +return a=K(a),this[a+"s"]()}function $c(a){return function(){return this._data[a]}}function _c(){return q(this.days()/7)}function ad(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function bd(a,b,c){var d=db(a).abs(),e=_e(d.as("s")),f=_e(d.as("m")),g=_e(d.as("h")),h=_e(d.as("d")),i=_e(d.as("M")),j=_e(d.as("y")),k=e=f&&["m"]||f=g&&["h"]||g=h&&["d"]||h=i&&["M"]||i=j&&["y"]||["yy",j];return k[2]=b,k[3]=+a>0,k[4]=c,ad.apply(null,k)}function cd(a,b){return void 0===af[a]?!1:void 0===b?af[a]:(af[a]=b,!0)}function dd(a){var b=this.localeData(),c=bd(this,!a,b);return a&&(c=b.pastFuture(+this,c)),b.postformat(c)}function ed(){var a,b,c,d=bf(this._milliseconds)/1e3,e=bf(this._days),f=bf(this._months);a=q(d/60),b=q(a/60),d%=60,a%=60,c=q(f/12),f%=12;var g=c,h=f,i=e,j=b,k=a,l=d,m=this.asSeconds();return m?(0>m?"-":"")+"P"+(g?g+"Y":"")+(h?h+"M":"")+(i?i+"D":"")+(j||k||l?"T":"")+(j?j+"H":"")+(k?k+"M":"")+(l?l+"S":""):"P0D"}var fd,gd;gd=Array.prototype.some?Array.prototype.some:function(a){for(var b=Object(this),c=b.length>>>0,d=0;c>d;d++)if(d in b&&a.call(this,b[d],d,b))return!0;return!1};var hd=a.momentProperties=[],id=!1,jd={};a.suppressDeprecationWarnings=!1,a.deprecationHandler=null;var kd;kd=Object.keys?Object.keys:function(a){var b,c=[];for(b in a)f(a,b)&&c.push(b);return c};var ld,md,nd={},od={},pd=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,qd=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,rd={},sd={},td=/\d/,ud=/\d\d/,vd=/\d{3}/,wd=/\d{4}/,xd=/[+-]?\d{6}/,yd=/\d\d?/,zd=/\d\d\d\d?/,Ad=/\d\d\d\d\d\d?/,Bd=/\d{1,3}/,Cd=/\d{1,4}/,Dd=/[+-]?\d{1,6}/,Ed=/\d+/,Fd=/[+-]?\d+/,Gd=/Z|[+-]\d\d:?\d\d/gi,Hd=/Z|[+-]\d\d(?::?\d\d)?/gi,Id=/[+-]?\d+(\.\d{1,3})?/,Jd=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Kd={},Ld={},Md=0,Nd=1,Od=2,Pd=3,Qd=4,Rd=5,Sd=6,Td=7,Ud=8;md=Array.prototype.indexOf?Array.prototype.indexOf:function(a){var b;for(b=0;b=a?""+a:"+"+a}),R(0,["YY",2],0,function(){return this.year()%100}),R(0,["YYYY",4],0,"year"),R(0,["YYYYY",5],0,"year"),R(0,["YYYYYY",6,!0],0,"year"),J("year","y"),W("Y",Fd),W("YY",yd,ud),W("YYYY",Cd,wd),W("YYYYY",Dd,xd),W("YYYYYY",Dd,xd),$(["YYYYY","YYYYYY"],Md),$("YYYY",function(b,c){c[Md]=2===b.length?a.parseTwoDigitYear(b):r(b)}),$("YY",function(b,c){c[Md]=a.parseTwoDigitYear(b)}),$("Y",function(a,b){b[Md]=parseInt(a,10)}),a.parseTwoDigitYear=function(a){return r(a)+(r(a)>68?1900:2e3)};var ee=M("FullYear",!0);a.ISO_8601=function(){};var fe=u("moment().min is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(){var a=Ka.apply(null,arguments);return this.isValid()&&a.isValid()?this>a?this:a:l()}),ge=u("moment().max is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(){var a=Ka.apply(null,arguments);return this.isValid()&&a.isValid()?a>this?this:a:l()}),he=function(){return Date.now?Date.now():+new Date};Qa("Z",":"),Qa("ZZ",""),W("Z",Hd),W("ZZ",Hd),$(["Z","ZZ"],function(a,b,c){c._useUTC=!0,c._tzm=Ra(Hd,a)});var ie=/([\+\-]|\d\d)/gi;a.updateOffset=function(){};var je=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?\d*)?$/,ke=/^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;db.fn=Oa.prototype;var le=ib(1,"add"),me=ib(-1,"subtract");a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",a.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var ne=u("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(a){return void 0===a?this.localeData():this.locale(a)});R(0,["gg",2],0,function(){return this.weekYear()%100}),R(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Pb("gggg","weekYear"),Pb("ggggg","weekYear"),Pb("GGGG","isoWeekYear"),Pb("GGGGG","isoWeekYear"),J("weekYear","gg"),J("isoWeekYear","GG"),W("G",Fd),W("g",Fd),W("GG",yd,ud),W("gg",yd,ud),W("GGGG",Cd,wd),W("gggg",Cd,wd),W("GGGGG",Dd,xd),W("ggggg",Dd,xd),_(["gggg","ggggg","GGGG","GGGGG"],function(a,b,c,d){b[d.substr(0,2)]=r(a)}),_(["gg","GG"],function(b,c,d,e){c[e]=a.parseTwoDigitYear(b)}),R("Q",0,"Qo","quarter"),J("quarter","Q"),W("Q",td),$("Q",function(a,b){b[Nd]=3*(r(a)-1)}),R("w",["ww",2],"wo","week"),R("W",["WW",2],"Wo","isoWeek"),J("week","w"),J("isoWeek","W"),W("w",yd),W("ww",yd,ud),W("W",yd),W("WW",yd,ud),_(["w","ww","W","WW"],function(a,b,c,d){b[d.substr(0,1)]=r(a)});var oe={dow:0,doy:6};R("D",["DD",2],"Do","date"),J("date","D"),W("D",yd),W("DD",yd,ud),W("Do",function(a,b){return a?b._ordinalParse:b._ordinalParseLenient}),$(["D","DD"],Od),$("Do",function(a,b){b[Od]=r(a.match(yd)[0],10)});var pe=M("Date",!0);R("d",0,"do","day"),R("dd",0,0,function(a){return this.localeData().weekdaysMin(this,a)}),R("ddd",0,0,function(a){return this.localeData().weekdaysShort(this,a)}),R("dddd",0,0,function(a){return this.localeData().weekdays(this,a)}),R("e",0,0,"weekday"),R("E",0,0,"isoWeekday"),J("day","d"),J("weekday","e"),J("isoWeekday","E"),W("d",yd),W("e",yd),W("E",yd),W("dd",function(a,b){return b.weekdaysMinRegex(a)}),W("ddd",function(a,b){return b.weekdaysShortRegex(a)}),W("dddd",function(a,b){return b.weekdaysRegex(a)}),_(["dd","ddd","dddd"],function(a,b,c,d){var e=c._locale.weekdaysParse(a,d,c._strict);null!=e?b.d=e:j(c).invalidWeekday=a}),_(["d","e","E"],function(a,b,c,d){b[d]=r(a)});var qe="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),re="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),se="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),te=Jd,ue=Jd,ve=Jd;R("DDD",["DDDD",3],"DDDo","dayOfYear"),J("dayOfYear","DDD"),W("DDD",Bd),W("DDDD",vd),$(["DDD","DDDD"],function(a,b,c){c._dayOfYear=r(a)}),R("H",["HH",2],0,"hour"),R("h",["hh",2],0,oc),R("k",["kk",2],0,pc),R("hmm",0,0,function(){return""+oc.apply(this)+Q(this.minutes(),2)}),R("hmmss",0,0,function(){return""+oc.apply(this)+Q(this.minutes(),2)+Q(this.seconds(),2)}),R("Hmm",0,0,function(){return""+this.hours()+Q(this.minutes(),2)}),R("Hmmss",0,0,function(){return""+this.hours()+Q(this.minutes(),2)+Q(this.seconds(),2)}),qc("a",!0),qc("A",!1),J("hour","h"),W("a",rc),W("A",rc),W("H",yd),W("h",yd),W("HH",yd,ud),W("hh",yd,ud),W("hmm",zd),W("hmmss",Ad),W("Hmm",zd),W("Hmmss",Ad),$(["H","HH"],Pd),$(["a","A"],function(a,b,c){c._isPm=c._locale.isPM(a),c._meridiem=a}),$(["h","hh"],function(a,b,c){b[Pd]=r(a),j(c).bigHour=!0}),$("hmm",function(a,b,c){var d=a.length-2;b[Pd]=r(a.substr(0,d)),b[Qd]=r(a.substr(d)),j(c).bigHour=!0}),$("hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[Pd]=r(a.substr(0,d)),b[Qd]=r(a.substr(d,2)),b[Rd]=r(a.substr(e)),j(c).bigHour=!0}),$("Hmm",function(a,b,c){var d=a.length-2;b[Pd]=r(a.substr(0,d)),b[Qd]=r(a.substr(d))}),$("Hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[Pd]=r(a.substr(0,d)),b[Qd]=r(a.substr(d,2)),b[Rd]=r(a.substr(e))});var we=/[ap]\.?m?\.?/i,xe=M("Hours",!0);R("m",["mm",2],0,"minute"),J("minute","m"),W("m",yd),W("mm",yd,ud),$(["m","mm"],Qd);var ye=M("Minutes",!1);R("s",["ss",2],0,"second"),J("second","s"),W("s",yd),W("ss",yd,ud),$(["s","ss"],Rd);var ze=M("Seconds",!1);R("S",0,0,function(){return~~(this.millisecond()/100)}),R(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),R(0,["SSS",3],0,"millisecond"),R(0,["SSSS",4],0,function(){return 10*this.millisecond()}),R(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),R(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),R(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),R(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),R(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),J("millisecond","ms"),W("S",Bd,td),W("SS",Bd,ud),W("SSS",Bd,vd);var Ae;for(Ae="SSSS";Ae.length<=9;Ae+="S")W(Ae,Ed);for(Ae="S";Ae.length<=9;Ae+="S")$(Ae,uc);var Be=M("Milliseconds",!1);R("z",0,0,"zoneAbbr"),R("zz",0,0,"zoneName");var Ce=o.prototype;Ce.add=le,Ce.calendar=kb,Ce.clone=lb,Ce.diff=sb,Ce.endOf=Eb,Ce.format=wb,Ce.from=xb,Ce.fromNow=yb,Ce.to=zb,Ce.toNow=Ab,Ce.get=P,Ce.invalidAt=Nb,Ce.isAfter=mb,Ce.isBefore=nb,Ce.isBetween=ob,Ce.isSame=pb,Ce.isSameOrAfter=qb,Ce.isSameOrBefore=rb,Ce.isValid=Lb,Ce.lang=ne,Ce.locale=Bb,Ce.localeData=Cb,Ce.max=ge,Ce.min=fe,Ce.parsingFlags=Mb,Ce.set=P,Ce.startOf=Db,Ce.subtract=me,Ce.toArray=Ib,Ce.toObject=Jb,Ce.toDate=Hb,Ce.toISOString=vb,Ce.toJSON=Kb,Ce.toString=ub,Ce.unix=Gb,Ce.valueOf=Fb,Ce.creationData=Ob,Ce.year=ee,Ce.isLeapYear=ta,Ce.weekYear=Qb,Ce.isoWeekYear=Rb,Ce.quarter=Ce.quarters=Wb,Ce.month=ha,Ce.daysInMonth=ia,Ce.week=Ce.weeks=$b,Ce.isoWeek=Ce.isoWeeks=_b,Ce.weeksInYear=Tb,Ce.isoWeeksInYear=Sb,Ce.date=pe,Ce.day=Ce.days=gc,Ce.weekday=hc,Ce.isoWeekday=ic,Ce.dayOfYear=nc,Ce.hour=Ce.hours=xe,Ce.minute=Ce.minutes=ye,Ce.second=Ce.seconds=ze,Ce.millisecond=Ce.milliseconds=Be,Ce.utcOffset=Ua,Ce.utc=Wa,Ce.local=Xa,Ce.parseZone=Ya,Ce.hasAlignedHourOffset=Za,Ce.isDST=$a,Ce.isDSTShifted=_a,Ce.isLocal=ab,Ce.isUtcOffset=bb,Ce.isUtc=cb,Ce.isUTC=cb,Ce.zoneAbbr=vc,Ce.zoneName=wc,Ce.dates=u("dates accessor is deprecated. Use date instead.",pe),Ce.months=u("months accessor is deprecated. Use month instead",ha),Ce.years=u("years accessor is deprecated. Use year instead",ee),Ce.zone=u("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",Va);var De=Ce,Ee={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},Fe={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},Ge="Invalid date",He="%d",Ie=/\d{1,2}/,Je={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Ke=A.prototype;Ke._calendar=Ee,Ke.calendar=zc,Ke._longDateFormat=Fe,Ke.longDateFormat=Ac,Ke._invalidDate=Ge,Ke.invalidDate=Bc,Ke._ordinal=He,Ke.ordinal=Cc,Ke._ordinalParse=Ie,Ke.preparse=Dc,Ke.postformat=Dc,Ke._relativeTime=Je,Ke.relativeTime=Ec,Ke.pastFuture=Fc,Ke.set=y,Ke.months=ca,Ke._months=Wd,Ke.monthsShort=da,Ke._monthsShort=Xd,Ke.monthsParse=fa,Ke._monthsRegex=Zd,Ke.monthsRegex=ka,Ke._monthsShortRegex=Yd,Ke.monthsShortRegex=ja,Ke.week=Xb,Ke._week=oe,Ke.firstDayOfYear=Zb,Ke.firstDayOfWeek=Yb,Ke.weekdays=bc,Ke._weekdays=qe,Ke.weekdaysMin=dc,Ke._weekdaysMin=se,Ke.weekdaysShort=cc,Ke._weekdaysShort=re,Ke.weekdaysParse=fc,Ke._weekdaysRegex=te,Ke.weekdaysRegex=jc,Ke._weekdaysShortRegex=ue,Ke.weekdaysShortRegex=kc,Ke._weekdaysMinRegex=ve,Ke.weekdaysMinRegex=lc,Ke.isPM=sc,Ke._meridiemParse=we,Ke.meridiem=tc,E("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===r(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),a.lang=u("moment.lang is deprecated. Use moment.locale instead.",E),a.langData=u("moment.langData is deprecated. Use moment.localeData instead.",H);var Le=Math.abs,Me=Yc("ms"),Ne=Yc("s"),Oe=Yc("m"),Pe=Yc("h"),Qe=Yc("d"),Re=Yc("w"),Se=Yc("M"),Te=Yc("y"),Ue=$c("milliseconds"),Ve=$c("seconds"),We=$c("minutes"),Xe=$c("hours"),Ye=$c("days"),Ze=$c("months"),$e=$c("years"),_e=Math.round,af={s:45,m:45,h:22,d:26,M:11},bf=Math.abs,cf=Oa.prototype;cf.abs=Oc,cf.add=Qc,cf.subtract=Rc,cf.as=Wc,cf.asMilliseconds=Me,cf.asSeconds=Ne,cf.asMinutes=Oe,cf.asHours=Pe,cf.asDays=Qe,cf.asWeeks=Re,cf.asMonths=Se,cf.asYears=Te,cf.valueOf=Xc,cf._bubble=Tc,cf.get=Zc,cf.milliseconds=Ue,cf.seconds=Ve,cf.minutes=We,cf.hours=Xe,cf.days=Ye,cf.weeks=_c,cf.months=Ze,cf.years=$e,cf.humanize=dd,cf.toISOString=ed,cf.toString=ed,cf.toJSON=ed,cf.locale=Bb,cf.localeData=Cb,cf.toIsoString=u("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",ed),cf.lang=ne,R("X",0,0,"unix"),R("x",0,0,"valueOf"),W("x",Fd),W("X",Id),$("X",function(a,b,c){c._d=new Date(1e3*parseFloat(a,10))}),$("x",function(a,b,c){c._d=new Date(r(a))}),a.version="2.13.0",b(Ka),a.fn=De,a.min=Ma,a.max=Na,a.now=he,a.utc=h,a.unix=xc,a.months=Jc,a.isDate=d,a.locale=E,a.invalid=l,a.duration=db,a.isMoment=p,a.weekdays=Lc,a.parseZone=yc,a.localeData=H,a.isDuration=Pa,a.monthsShort=Kc,a.weekdaysMin=Nc,a.defineLocale=F,a.updateLocale=G,a.locales=I,a.weekdaysShort=Mc,a.normalizeUnits=K,a.relativeTimeThreshold=cd,a.prototype=De;var df=a;return df}); \ No newline at end of file diff --git a/public/static/libs/gmapsjs/gmaps.js b/public/static/libs/gmapsjs/gmaps.js new file mode 100644 index 0000000..9dcc20d --- /dev/null +++ b/public/static/libs/gmapsjs/gmaps.js @@ -0,0 +1,2409 @@ +"use strict"; +(function(root, factory) { + if(typeof exports === 'object') { + module.exports = factory(); + } + else if(typeof define === 'function' && define.amd) { + define(['jquery', 'googlemaps!'], factory); + } + else { + root.GMaps = factory(); + } + + +}(this, function() { + +/*! + * GMaps.js v0.4.24 + * http://hpneo.github.com/gmaps/ + * + * Copyright 2016, Gustavo Leon + * Released under the MIT License. + */ + +var extend_object = function(obj, new_obj) { + var name; + + if (obj === new_obj) { + return obj; + } + + for (name in new_obj) { + if (new_obj[name] !== undefined) { + obj[name] = new_obj[name]; + } + } + + return obj; +}; + +var replace_object = function(obj, replace) { + var name; + + if (obj === replace) { + return obj; + } + + for (name in replace) { + if (obj[name] != undefined) { + obj[name] = replace[name]; + } + } + + return obj; +}; + +var array_map = function(array, callback) { + var original_callback_params = Array.prototype.slice.call(arguments, 2), + array_return = [], + array_length = array.length, + i; + + if (Array.prototype.map && array.map === Array.prototype.map) { + array_return = Array.prototype.map.call(array, function(item) { + var callback_params = original_callback_params.slice(0); + callback_params.splice(0, 0, item); + + return callback.apply(this, callback_params); + }); + } + else { + for (i = 0; i < array_length; i++) { + callback_params = original_callback_params; + callback_params.splice(0, 0, array[i]); + array_return.push(callback.apply(this, callback_params)); + } + } + + return array_return; +}; + +var array_flat = function(array) { + var new_array = [], + i; + + for (i = 0; i < array.length; i++) { + new_array = new_array.concat(array[i]); + } + + return new_array; +}; + +var coordsToLatLngs = function(coords, useGeoJSON) { + var first_coord = coords[0], + second_coord = coords[1]; + + if (useGeoJSON) { + first_coord = coords[1]; + second_coord = coords[0]; + } + + return new google.maps.LatLng(first_coord, second_coord); +}; + +var arrayToLatLng = function(coords, useGeoJSON) { + var i; + + for (i = 0; i < coords.length; i++) { + if (!(coords[i] instanceof google.maps.LatLng)) { + if (coords[i].length > 0 && typeof(coords[i][0]) === "object") { + coords[i] = arrayToLatLng(coords[i], useGeoJSON); + } + else { + coords[i] = coordsToLatLngs(coords[i], useGeoJSON); + } + } + } + + return coords; +}; + +var getElementsByClassName = function (class_name, context) { + var element, + _class = class_name.replace('.', ''); + + if ('jQuery' in this && context) { + element = $("." + _class, context)[0]; + } else { + element = document.getElementsByClassName(_class)[0]; + } + return element; + +}; + +var getElementById = function(id, context) { + var element, + id = id.replace('#', ''); + + if ('jQuery' in window && context) { + element = $('#' + id, context)[0]; + } else { + element = document.getElementById(id); + }; + + return element; +}; + +var findAbsolutePosition = function(obj) { + var curleft = 0, + curtop = 0; + + if (obj.offsetParent) { + do { + curleft += obj.offsetLeft; + curtop += obj.offsetTop; + } while (obj = obj.offsetParent); + } + + return [curleft, curtop]; +}; + +var GMaps = (function(global) { + "use strict"; + + var doc = document; + /** + * Creates a new GMaps instance, including a Google Maps map. + * @class GMaps + * @constructs + * @param {object} options - `options` accepts all the [MapOptions](https://developers.google.com/maps/documentation/javascript/reference#MapOptions) and [events](https://developers.google.com/maps/documentation/javascript/reference#Map) listed in the Google Maps API. Also accepts: + * * `lat` (number): Latitude of the map's center + * * `lng` (number): Longitude of the map's center + * * `el` (string or HTMLElement): container where the map will be rendered + * * `markerClusterer` (function): A function to create a marker cluster. You can use MarkerClusterer or MarkerClustererPlus. + */ + var GMaps = function(options) { + + if (!(typeof window.google === 'object' && window.google.maps)) { + if (typeof window.console === 'object' && window.console.error) { + console.error('Google Maps API is required. Please register the following JavaScript library https://maps.googleapis.com/maps/api/js.'); + } + + return function() {}; + } + + if (!this) return new GMaps(options); + + options.zoom = options.zoom || 15; + options.mapType = options.mapType || 'roadmap'; + + var valueOrDefault = function(value, defaultValue) { + return value === undefined ? defaultValue : value; + }; + + var self = this, + i, + events_that_hide_context_menu = [ + 'bounds_changed', 'center_changed', 'click', 'dblclick', 'drag', + 'dragend', 'dragstart', 'idle', 'maptypeid_changed', 'projection_changed', + 'resize', 'tilesloaded', 'zoom_changed' + ], + events_that_doesnt_hide_context_menu = ['mousemove', 'mouseout', 'mouseover'], + options_to_be_deleted = ['el', 'lat', 'lng', 'mapType', 'width', 'height', 'markerClusterer', 'enableNewStyle'], + identifier = options.el || options.div, + markerClustererFunction = options.markerClusterer, + mapType = google.maps.MapTypeId[options.mapType.toUpperCase()], + map_center = new google.maps.LatLng(options.lat, options.lng), + zoomControl = valueOrDefault(options.zoomControl, true), + zoomControlOpt = options.zoomControlOpt || { + style: 'DEFAULT', + position: 'TOP_LEFT' + }, + zoomControlStyle = zoomControlOpt.style || 'DEFAULT', + zoomControlPosition = zoomControlOpt.position || 'TOP_LEFT', + panControl = valueOrDefault(options.panControl, true), + mapTypeControl = valueOrDefault(options.mapTypeControl, true), + scaleControl = valueOrDefault(options.scaleControl, true), + streetViewControl = valueOrDefault(options.streetViewControl, true), + overviewMapControl = valueOrDefault(overviewMapControl, true), + map_options = {}, + map_base_options = { + zoom: this.zoom, + center: map_center, + mapTypeId: mapType + }, + map_controls_options = { + panControl: panControl, + zoomControl: zoomControl, + zoomControlOptions: { + style: google.maps.ZoomControlStyle[zoomControlStyle], + position: google.maps.ControlPosition[zoomControlPosition] + }, + mapTypeControl: mapTypeControl, + scaleControl: scaleControl, + streetViewControl: streetViewControl, + overviewMapControl: overviewMapControl + }; + + if (typeof(options.el) === 'string' || typeof(options.div) === 'string') { + if (identifier.indexOf("#") > -1) { + /** + * Container element + * + * @type {HTMLElement} + */ + this.el = getElementById(identifier, options.context); + } else { + this.el = getElementsByClassName.apply(this, [identifier, options.context]); + } + } else { + this.el = identifier; + } + + if (typeof(this.el) === 'undefined' || this.el === null) { + throw 'No element defined.'; + } + + window.context_menu = window.context_menu || {}; + window.context_menu[self.el.id] = {}; + + /** + * Collection of custom controls in the map UI + * + * @type {array} + */ + this.controls = []; + /** + * Collection of map's overlays + * + * @type {array} + */ + this.overlays = []; + /** + * Collection of KML/GeoRSS and FusionTable layers + * + * @type {array} + */ + this.layers = []; + /** + * Collection of data layers (See {@link GMaps#addLayer}) + * + * @type {object} + */ + this.singleLayers = {}; + /** + * Collection of map's markers + * + * @type {array} + */ + this.markers = []; + /** + * Collection of map's lines + * + * @type {array} + */ + this.polylines = []; + /** + * Collection of map's routes requested by {@link GMaps#getRoutes}, {@link GMaps#renderRoute}, {@link GMaps#drawRoute}, {@link GMaps#travelRoute} or {@link GMaps#drawSteppedRoute} + * + * @type {array} + */ + this.routes = []; + /** + * Collection of map's polygons + * + * @type {array} + */ + this.polygons = []; + this.infoWindow = null; + this.overlay_el = null; + /** + * Current map's zoom + * + * @type {number} + */ + this.zoom = options.zoom; + this.registered_events = {}; + + this.el.style.width = options.width || this.el.scrollWidth || this.el.offsetWidth; + this.el.style.height = options.height || this.el.scrollHeight || this.el.offsetHeight; + + google.maps.visualRefresh = options.enableNewStyle; + + for (i = 0; i < options_to_be_deleted.length; i++) { + delete options[options_to_be_deleted[i]]; + } + + if(options.disableDefaultUI != true) { + map_base_options = extend_object(map_base_options, map_controls_options); + } + + map_options = extend_object(map_base_options, options); + + for (i = 0; i < events_that_hide_context_menu.length; i++) { + delete map_options[events_that_hide_context_menu[i]]; + } + + for (i = 0; i < events_that_doesnt_hide_context_menu.length; i++) { + delete map_options[events_that_doesnt_hide_context_menu[i]]; + } + + /** + * Google Maps map instance + * + * @type {google.maps.Map} + */ + this.map = new google.maps.Map(this.el, map_options); + + if (markerClustererFunction) { + /** + * Marker Clusterer instance + * + * @type {object} + */ + this.markerClusterer = markerClustererFunction.apply(this, [this.map]); + } + + var buildContextMenuHTML = function(control, e) { + var html = '', + options = window.context_menu[self.el.id][control]; + + for (var i in options){ + if (options.hasOwnProperty(i)) { + var option = options[i]; + + html += '
  • ' + option.title + '
  • '; + } + } + + if (!getElementById('gmaps_context_menu')) return; + + var context_menu_element = getElementById('gmaps_context_menu'); + + context_menu_element.innerHTML = html; + + var context_menu_items = context_menu_element.getElementsByTagName('a'), + context_menu_items_count = context_menu_items.length, + i; + + for (i = 0; i < context_menu_items_count; i++) { + var context_menu_item = context_menu_items[i]; + + var assign_menu_item_action = function(ev){ + ev.preventDefault(); + + options[this.id.replace(control + '_', '')].action.apply(self, [e]); + self.hideContextMenu(); + }; + + google.maps.event.clearListeners(context_menu_item, 'click'); + google.maps.event.addDomListenerOnce(context_menu_item, 'click', assign_menu_item_action, false); + } + + var position = findAbsolutePosition.apply(this, [self.el]), + left = position[0] + e.pixel.x - 15, + top = position[1] + e.pixel.y- 15; + + context_menu_element.style.left = left + "px"; + context_menu_element.style.top = top + "px"; + + // context_menu_element.style.display = 'block'; + }; + + this.buildContextMenu = function(control, e) { + if (control === 'marker') { + e.pixel = {}; + + var overlay = new google.maps.OverlayView(); + overlay.setMap(self.map); + + overlay.draw = function() { + var projection = overlay.getProjection(), + position = e.marker.getPosition(); + + e.pixel = projection.fromLatLngToContainerPixel(position); + + buildContextMenuHTML(control, e); + }; + } + else { + buildContextMenuHTML(control, e); + } + + var context_menu_element = getElementById('gmaps_context_menu'); + + setTimeout(function() { + context_menu_element.style.display = 'block'; + }, 0); + }; + + /** + * Add a context menu for a map or a marker. + * + * @param {object} options - The `options` object should contain: + * * `control` (string): Kind of control the context menu will be attached. Can be "map" or "marker". + * * `options` (array): A collection of context menu items: + * * `title` (string): Item's title shown in the context menu. + * * `name` (string): Item's identifier. + * * `action` (function): Function triggered after selecting the context menu item. + */ + this.setContextMenu = function(options) { + window.context_menu[self.el.id][options.control] = {}; + + var i, + ul = doc.createElement('ul'); + + for (i in options.options) { + if (options.options.hasOwnProperty(i)) { + var option = options.options[i]; + + window.context_menu[self.el.id][options.control][option.name] = { + title: option.title, + action: option.action + }; + } + } + + ul.id = 'gmaps_context_menu'; + ul.style.display = 'none'; + ul.style.position = 'absolute'; + ul.style.minWidth = '100px'; + ul.style.background = 'white'; + ul.style.listStyle = 'none'; + ul.style.padding = '8px'; + ul.style.boxShadow = '2px 2px 6px #ccc'; + + if (!getElementById('gmaps_context_menu')) { + doc.body.appendChild(ul); + } + + var context_menu_element = getElementById('gmaps_context_menu'); + + google.maps.event.addDomListener(context_menu_element, 'mouseout', function(ev) { + if (!ev.relatedTarget || !this.contains(ev.relatedTarget)) { + window.setTimeout(function(){ + context_menu_element.style.display = 'none'; + }, 400); + } + }, false); + }; + + /** + * Hide the current context menu + */ + this.hideContextMenu = function() { + var context_menu_element = getElementById('gmaps_context_menu'); + + if (context_menu_element) { + context_menu_element.style.display = 'none'; + } + }; + + var setupListener = function(object, name) { + google.maps.event.addListener(object, name, function(e){ + if (e == undefined) { + e = this; + } + + options[name].apply(this, [e]); + + self.hideContextMenu(); + }); + }; + + //google.maps.event.addListener(this.map, 'idle', this.hideContextMenu); + google.maps.event.addListener(this.map, 'zoom_changed', this.hideContextMenu); + + for (var ev = 0; ev < events_that_hide_context_menu.length; ev++) { + var name = events_that_hide_context_menu[ev]; + + if (name in options) { + setupListener(this.map, name); + } + } + + for (var ev = 0; ev < events_that_doesnt_hide_context_menu.length; ev++) { + var name = events_that_doesnt_hide_context_menu[ev]; + + if (name in options) { + setupListener(this.map, name); + } + } + + google.maps.event.addListener(this.map, 'rightclick', function(e) { + if (options.rightclick) { + options.rightclick.apply(this, [e]); + } + + if(window.context_menu[self.el.id]['map'] != undefined) { + self.buildContextMenu('map', e); + } + }); + + /** + * Trigger a `resize` event, useful if you need to repaint the current map (for changes in the viewport or display / hide actions). + */ + this.refresh = function() { + google.maps.event.trigger(this.map, 'resize'); + }; + + /** + * Adjust the map zoom to include all the markers added in the map. + */ + this.fitZoom = function() { + var latLngs = [], + markers_length = this.markers.length, + i; + + for (i = 0; i < markers_length; i++) { + if(typeof(this.markers[i].visible) === 'boolean' && this.markers[i].visible) { + latLngs.push(this.markers[i].getPosition()); + } + } + + this.fitLatLngBounds(latLngs); + }; + + /** + * Adjust the map zoom to include all the coordinates in the `latLngs` array. + * + * @param {array} latLngs - Collection of `google.maps.LatLng` objects. + */ + this.fitLatLngBounds = function(latLngs) { + var total = latLngs.length, + bounds = new google.maps.LatLngBounds(), + i; + + for(i = 0; i < total; i++) { + bounds.extend(latLngs[i]); + } + + this.map.fitBounds(bounds); + }; + + /** + * Center the map using the `lat` and `lng` coordinates. + * + * @param {number} lat - Latitude of the coordinate. + * @param {number} lng - Longitude of the coordinate. + * @param {function} [callback] - Callback that will be executed after the map is centered. + */ + this.setCenter = function(lat, lng, callback) { + this.map.panTo(new google.maps.LatLng(lat, lng)); + + if (callback) { + callback(); + } + }; + + /** + * Return the HTML element container of the map. + * + * @returns {HTMLElement} the element container. + */ + this.getElement = function() { + return this.el; + }; + + /** + * Increase the map's zoom. + * + * @param {number} [magnitude] - The number of times the map will be zoomed in. + */ + this.zoomIn = function(value) { + value = value || 1; + + this.zoom = this.map.getZoom() + value; + this.map.setZoom(this.zoom); + }; + + /** + * Decrease the map's zoom. + * + * @param {number} [magnitude] - The number of times the map will be zoomed out. + */ + this.zoomOut = function(value) { + value = value || 1; + + this.zoom = this.map.getZoom() - value; + this.map.setZoom(this.zoom); + }; + + var native_methods = [], + method; + + for (method in this.map) { + if (typeof(this.map[method]) == 'function' && !this[method]) { + native_methods.push(method); + } + } + + for (i = 0; i < native_methods.length; i++) { + (function(gmaps, scope, method_name) { + gmaps[method_name] = function(){ + return scope[method_name].apply(scope, arguments); + }; + })(this, this.map, native_methods[i]); + } + }; + + return GMaps; +})(this); + +GMaps.prototype.createControl = function(options) { + var control = document.createElement('div'); + + control.style.cursor = 'pointer'; + + if (options.disableDefaultStyles !== true) { + control.style.fontFamily = 'Roboto, Arial, sans-serif'; + control.style.fontSize = '11px'; + control.style.boxShadow = 'rgba(0, 0, 0, 0.298039) 0px 1px 4px -1px'; + } + + for (var option in options.style) { + control.style[option] = options.style[option]; + } + + if (options.id) { + control.id = options.id; + } + + if (options.title) { + control.title = options.title; + } + + if (options.classes) { + control.className = options.classes; + } + + if (options.content) { + if (typeof options.content === 'string') { + control.innerHTML = options.content; + } + else if (options.content instanceof HTMLElement) { + control.appendChild(options.content); + } + } + + if (options.position) { + control.position = google.maps.ControlPosition[options.position.toUpperCase()]; + } + + for (var ev in options.events) { + (function(object, name) { + google.maps.event.addDomListener(object, name, function(){ + options.events[name].apply(this, [this]); + }); + })(control, ev); + } + + control.index = 1; + + return control; +}; + +/** + * Add a custom control to the map UI. + * + * @param {object} options - The `options` object should contain: + * * `style` (object): The keys and values of this object should be valid CSS properties and values. + * * `id` (string): The HTML id for the custom control. + * * `classes` (string): A string containing all the HTML classes for the custom control. + * * `content` (string or HTML element): The content of the custom control. + * * `position` (string): Any valid [`google.maps.ControlPosition`](https://developers.google.com/maps/documentation/javascript/controls#ControlPositioning) value, in lower or upper case. + * * `events` (object): The keys of this object should be valid DOM events. The values should be functions. + * * `disableDefaultStyles` (boolean): If false, removes the default styles for the controls like font (family and size), and box shadow. + * @returns {HTMLElement} + */ +GMaps.prototype.addControl = function(options) { + var control = this.createControl(options); + + this.controls.push(control); + this.map.controls[control.position].push(control); + + return control; +}; + +/** + * Remove a control from the map. `control` should be a control returned by `addControl()`. + * + * @param {HTMLElement} control - One of the controls returned by `addControl()`. + * @returns {HTMLElement} the removed control. + */ +GMaps.prototype.removeControl = function(control) { + var position = null, + i; + + for (i = 0; i < this.controls.length; i++) { + if (this.controls[i] == control) { + position = this.controls[i].position; + this.controls.splice(i, 1); + } + } + + if (position) { + for (i = 0; i < this.map.controls.length; i++) { + var controlsForPosition = this.map.controls[control.position]; + + if (controlsForPosition.getAt(i) == control) { + controlsForPosition.removeAt(i); + + break; + } + } + } + + return control; +}; + +GMaps.prototype.createMarker = function(options) { + if (options.lat == undefined && options.lng == undefined && options.position == undefined) { + throw 'No latitude or longitude defined.'; + } + + var self = this, + details = options.details, + fences = options.fences, + outside = options.outside, + base_options = { + position: new google.maps.LatLng(options.lat, options.lng), + map: null + }, + marker_options = extend_object(base_options, options); + + delete marker_options.lat; + delete marker_options.lng; + delete marker_options.fences; + delete marker_options.outside; + + var marker = new google.maps.Marker(marker_options); + + marker.fences = fences; + + if (options.infoWindow) { + marker.infoWindow = new google.maps.InfoWindow(options.infoWindow); + + var info_window_events = ['closeclick', 'content_changed', 'domready', 'position_changed', 'zindex_changed']; + + for (var ev = 0; ev < info_window_events.length; ev++) { + (function(object, name) { + if (options.infoWindow[name]) { + google.maps.event.addListener(object, name, function(e){ + options.infoWindow[name].apply(this, [e]); + }); + } + })(marker.infoWindow, info_window_events[ev]); + } + } + + var marker_events = ['animation_changed', 'clickable_changed', 'cursor_changed', 'draggable_changed', 'flat_changed', 'icon_changed', 'position_changed', 'shadow_changed', 'shape_changed', 'title_changed', 'visible_changed', 'zindex_changed']; + + var marker_events_with_mouse = ['dblclick', 'drag', 'dragend', 'dragstart', 'mousedown', 'mouseout', 'mouseover', 'mouseup']; + + for (var ev = 0; ev < marker_events.length; ev++) { + (function(object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function(){ + options[name].apply(this, [this]); + }); + } + })(marker, marker_events[ev]); + } + + for (var ev = 0; ev < marker_events_with_mouse.length; ev++) { + (function(map, object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function(me){ + if(!me.pixel){ + me.pixel = map.getProjection().fromLatLngToPoint(me.latLng) + } + + options[name].apply(this, [me]); + }); + } + })(this.map, marker, marker_events_with_mouse[ev]); + } + + google.maps.event.addListener(marker, 'click', function() { + this.details = details; + + if (options.click) { + options.click.apply(this, [this]); + } + + if (marker.infoWindow) { + self.hideInfoWindows(); + marker.infoWindow.open(self.map, marker); + } + }); + + google.maps.event.addListener(marker, 'rightclick', function(e) { + e.marker = this; + + if (options.rightclick) { + options.rightclick.apply(this, [e]); + } + + if (window.context_menu[self.el.id]['marker'] != undefined) { + self.buildContextMenu('marker', e); + } + }); + + if (marker.fences) { + google.maps.event.addListener(marker, 'dragend', function() { + self.checkMarkerGeofence(marker, function(m, f) { + outside(m, f); + }); + }); + } + + return marker; +}; + +GMaps.prototype.addMarker = function(options) { + var marker; + if(options.hasOwnProperty('gm_accessors_')) { + // Native google.maps.Marker object + marker = options; + } + else { + if ((options.hasOwnProperty('lat') && options.hasOwnProperty('lng')) || options.position) { + marker = this.createMarker(options); + } + else { + throw 'No latitude or longitude defined.'; + } + } + + marker.setMap(this.map); + + if(this.markerClusterer) { + this.markerClusterer.addMarker(marker); + } + + this.markers.push(marker); + + GMaps.fire('marker_added', marker, this); + + return marker; +}; + +GMaps.prototype.addMarkers = function(array) { + for (var i = 0, marker; marker=array[i]; i++) { + this.addMarker(marker); + } + + return this.markers; +}; + +GMaps.prototype.hideInfoWindows = function() { + for (var i = 0, marker; marker = this.markers[i]; i++){ + if (marker.infoWindow) { + marker.infoWindow.close(); + } + } +}; + +GMaps.prototype.removeMarker = function(marker) { + for (var i = 0; i < this.markers.length; i++) { + if (this.markers[i] === marker) { + this.markers[i].setMap(null); + this.markers.splice(i, 1); + + if(this.markerClusterer) { + this.markerClusterer.removeMarker(marker); + } + + GMaps.fire('marker_removed', marker, this); + + break; + } + } + + return marker; +}; + +GMaps.prototype.removeMarkers = function (collection) { + var new_markers = []; + + if (typeof collection == 'undefined') { + for (var i = 0; i < this.markers.length; i++) { + var marker = this.markers[i]; + marker.setMap(null); + + GMaps.fire('marker_removed', marker, this); + } + + if(this.markerClusterer && this.markerClusterer.clearMarkers) { + this.markerClusterer.clearMarkers(); + } + + this.markers = new_markers; + } + else { + for (var i = 0; i < collection.length; i++) { + var index = this.markers.indexOf(collection[i]); + + if (index > -1) { + var marker = this.markers[index]; + marker.setMap(null); + + if(this.markerClusterer) { + this.markerClusterer.removeMarker(marker); + } + + GMaps.fire('marker_removed', marker, this); + } + } + + for (var i = 0; i < this.markers.length; i++) { + var marker = this.markers[i]; + if (marker.getMap() != null) { + new_markers.push(marker); + } + } + + this.markers = new_markers; + } +}; + +GMaps.prototype.drawOverlay = function(options) { + var overlay = new google.maps.OverlayView(), + auto_show = true; + + overlay.setMap(this.map); + + if (options.auto_show != null) { + auto_show = options.auto_show; + } + + overlay.onAdd = function() { + var el = document.createElement('div'); + + el.style.borderStyle = "none"; + el.style.borderWidth = "0px"; + el.style.position = "absolute"; + el.style.zIndex = 100; + el.innerHTML = options.content; + + overlay.el = el; + + if (!options.layer) { + options.layer = 'overlayLayer'; + } + + var panes = this.getPanes(), + overlayLayer = panes[options.layer], + stop_overlay_events = ['contextmenu', 'DOMMouseScroll', 'dblclick', 'mousedown']; + + overlayLayer.appendChild(el); + + for (var ev = 0; ev < stop_overlay_events.length; ev++) { + (function(object, name) { + google.maps.event.addDomListener(object, name, function(e){ + if (navigator.userAgent.toLowerCase().indexOf('msie') != -1 && document.all) { + e.cancelBubble = true; + e.returnValue = false; + } + else { + e.stopPropagation(); + } + }); + })(el, stop_overlay_events[ev]); + } + + if (options.click) { + panes.overlayMouseTarget.appendChild(overlay.el); + google.maps.event.addDomListener(overlay.el, 'click', function() { + options.click.apply(overlay, [overlay]); + }); + } + + google.maps.event.trigger(this, 'ready'); + }; + + overlay.draw = function() { + var projection = this.getProjection(), + pixel = projection.fromLatLngToDivPixel(new google.maps.LatLng(options.lat, options.lng)); + + options.horizontalOffset = options.horizontalOffset || 0; + options.verticalOffset = options.verticalOffset || 0; + + var el = overlay.el, + content = el.children[0], + content_height = content.clientHeight, + content_width = content.clientWidth; + + switch (options.verticalAlign) { + case 'top': + el.style.top = (pixel.y - content_height + options.verticalOffset) + 'px'; + break; + default: + case 'middle': + el.style.top = (pixel.y - (content_height / 2) + options.verticalOffset) + 'px'; + break; + case 'bottom': + el.style.top = (pixel.y + options.verticalOffset) + 'px'; + break; + } + + switch (options.horizontalAlign) { + case 'left': + el.style.left = (pixel.x - content_width + options.horizontalOffset) + 'px'; + break; + default: + case 'center': + el.style.left = (pixel.x - (content_width / 2) + options.horizontalOffset) + 'px'; + break; + case 'right': + el.style.left = (pixel.x + options.horizontalOffset) + 'px'; + break; + } + + el.style.display = auto_show ? 'block' : 'none'; + + if (!auto_show) { + options.show.apply(this, [el]); + } + }; + + overlay.onRemove = function() { + var el = overlay.el; + + if (options.remove) { + options.remove.apply(this, [el]); + } + else { + overlay.el.parentNode.removeChild(overlay.el); + overlay.el = null; + } + }; + + this.overlays.push(overlay); + return overlay; +}; + +GMaps.prototype.removeOverlay = function(overlay) { + for (var i = 0; i < this.overlays.length; i++) { + if (this.overlays[i] === overlay) { + this.overlays[i].setMap(null); + this.overlays.splice(i, 1); + + break; + } + } +}; + +GMaps.prototype.removeOverlays = function() { + for (var i = 0, item; item = this.overlays[i]; i++) { + item.setMap(null); + } + + this.overlays = []; +}; + +GMaps.prototype.drawPolyline = function(options) { + var path = [], + points = options.path; + + if (points.length) { + if (points[0][0] === undefined) { + path = points; + } + else { + for (var i = 0, latlng; latlng = points[i]; i++) { + path.push(new google.maps.LatLng(latlng[0], latlng[1])); + } + } + } + + var polyline_options = { + map: this.map, + path: path, + strokeColor: options.strokeColor, + strokeOpacity: options.strokeOpacity, + strokeWeight: options.strokeWeight, + geodesic: options.geodesic, + clickable: true, + editable: false, + visible: true + }; + + if (options.hasOwnProperty("clickable")) { + polyline_options.clickable = options.clickable; + } + + if (options.hasOwnProperty("editable")) { + polyline_options.editable = options.editable; + } + + if (options.hasOwnProperty("icons")) { + polyline_options.icons = options.icons; + } + + if (options.hasOwnProperty("zIndex")) { + polyline_options.zIndex = options.zIndex; + } + + var polyline = new google.maps.Polyline(polyline_options); + + var polyline_events = ['click', 'dblclick', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'rightclick']; + + for (var ev = 0; ev < polyline_events.length; ev++) { + (function(object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function(e){ + options[name].apply(this, [e]); + }); + } + })(polyline, polyline_events[ev]); + } + + this.polylines.push(polyline); + + GMaps.fire('polyline_added', polyline, this); + + return polyline; +}; + +GMaps.prototype.removePolyline = function(polyline) { + for (var i = 0; i < this.polylines.length; i++) { + if (this.polylines[i] === polyline) { + this.polylines[i].setMap(null); + this.polylines.splice(i, 1); + + GMaps.fire('polyline_removed', polyline, this); + + break; + } + } +}; + +GMaps.prototype.removePolylines = function() { + for (var i = 0, item; item = this.polylines[i]; i++) { + item.setMap(null); + } + + this.polylines = []; +}; + +GMaps.prototype.drawCircle = function(options) { + options = extend_object({ + map: this.map, + center: new google.maps.LatLng(options.lat, options.lng) + }, options); + + delete options.lat; + delete options.lng; + + var polygon = new google.maps.Circle(options), + polygon_events = ['click', 'dblclick', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'rightclick']; + + for (var ev = 0; ev < polygon_events.length; ev++) { + (function(object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function(e){ + options[name].apply(this, [e]); + }); + } + })(polygon, polygon_events[ev]); + } + + this.polygons.push(polygon); + + return polygon; +}; + +GMaps.prototype.drawRectangle = function(options) { + options = extend_object({ + map: this.map + }, options); + + var latLngBounds = new google.maps.LatLngBounds( + new google.maps.LatLng(options.bounds[0][0], options.bounds[0][1]), + new google.maps.LatLng(options.bounds[1][0], options.bounds[1][1]) + ); + + options.bounds = latLngBounds; + + var polygon = new google.maps.Rectangle(options), + polygon_events = ['click', 'dblclick', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'rightclick']; + + for (var ev = 0; ev < polygon_events.length; ev++) { + (function(object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function(e){ + options[name].apply(this, [e]); + }); + } + })(polygon, polygon_events[ev]); + } + + this.polygons.push(polygon); + + return polygon; +}; + +GMaps.prototype.drawPolygon = function(options) { + var useGeoJSON = false; + + if(options.hasOwnProperty("useGeoJSON")) { + useGeoJSON = options.useGeoJSON; + } + + delete options.useGeoJSON; + + options = extend_object({ + map: this.map + }, options); + + if (useGeoJSON == false) { + options.paths = [options.paths.slice(0)]; + } + + if (options.paths.length > 0) { + if (options.paths[0].length > 0) { + options.paths = array_flat(array_map(options.paths, arrayToLatLng, useGeoJSON)); + } + } + + var polygon = new google.maps.Polygon(options), + polygon_events = ['click', 'dblclick', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'rightclick']; + + for (var ev = 0; ev < polygon_events.length; ev++) { + (function(object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function(e){ + options[name].apply(this, [e]); + }); + } + })(polygon, polygon_events[ev]); + } + + this.polygons.push(polygon); + + GMaps.fire('polygon_added', polygon, this); + + return polygon; +}; + +GMaps.prototype.removePolygon = function(polygon) { + for (var i = 0; i < this.polygons.length; i++) { + if (this.polygons[i] === polygon) { + this.polygons[i].setMap(null); + this.polygons.splice(i, 1); + + GMaps.fire('polygon_removed', polygon, this); + + break; + } + } +}; + +GMaps.prototype.removePolygons = function() { + for (var i = 0, item; item = this.polygons[i]; i++) { + item.setMap(null); + } + + this.polygons = []; +}; + +GMaps.prototype.getFromFusionTables = function(options) { + var events = options.events; + + delete options.events; + + var fusion_tables_options = options, + layer = new google.maps.FusionTablesLayer(fusion_tables_options); + + for (var ev in events) { + (function(object, name) { + google.maps.event.addListener(object, name, function(e) { + events[name].apply(this, [e]); + }); + })(layer, ev); + } + + this.layers.push(layer); + + return layer; +}; + +GMaps.prototype.loadFromFusionTables = function(options) { + var layer = this.getFromFusionTables(options); + layer.setMap(this.map); + + return layer; +}; + +GMaps.prototype.getFromKML = function(options) { + var url = options.url, + events = options.events; + + delete options.url; + delete options.events; + + var kml_options = options, + layer = new google.maps.KmlLayer(url, kml_options); + + for (var ev in events) { + (function(object, name) { + google.maps.event.addListener(object, name, function(e) { + events[name].apply(this, [e]); + }); + })(layer, ev); + } + + this.layers.push(layer); + + return layer; +}; + +GMaps.prototype.loadFromKML = function(options) { + var layer = this.getFromKML(options); + layer.setMap(this.map); + + return layer; +}; + +GMaps.prototype.addLayer = function(layerName, options) { + //var default_layers = ['weather', 'clouds', 'traffic', 'transit', 'bicycling', 'panoramio', 'places']; + options = options || {}; + var layer; + + switch(layerName) { + case 'weather': this.singleLayers.weather = layer = new google.maps.weather.WeatherLayer(); + break; + case 'clouds': this.singleLayers.clouds = layer = new google.maps.weather.CloudLayer(); + break; + case 'traffic': this.singleLayers.traffic = layer = new google.maps.TrafficLayer(); + break; + case 'transit': this.singleLayers.transit = layer = new google.maps.TransitLayer(); + break; + case 'bicycling': this.singleLayers.bicycling = layer = new google.maps.BicyclingLayer(); + break; + case 'panoramio': + this.singleLayers.panoramio = layer = new google.maps.panoramio.PanoramioLayer(); + layer.setTag(options.filter); + delete options.filter; + + //click event + if (options.click) { + google.maps.event.addListener(layer, 'click', function(event) { + options.click(event); + delete options.click; + }); + } + break; + case 'places': + this.singleLayers.places = layer = new google.maps.places.PlacesService(this.map); + + //search, nearbySearch, radarSearch callback, Both are the same + if (options.search || options.nearbySearch || options.radarSearch) { + var placeSearchRequest = { + bounds : options.bounds || null, + keyword : options.keyword || null, + location : options.location || null, + name : options.name || null, + radius : options.radius || null, + rankBy : options.rankBy || null, + types : options.types || null + }; + + if (options.radarSearch) { + layer.radarSearch(placeSearchRequest, options.radarSearch); + } + + if (options.search) { + layer.search(placeSearchRequest, options.search); + } + + if (options.nearbySearch) { + layer.nearbySearch(placeSearchRequest, options.nearbySearch); + } + } + + //textSearch callback + if (options.textSearch) { + var textSearchRequest = { + bounds : options.bounds || null, + location : options.location || null, + query : options.query || null, + radius : options.radius || null + }; + + layer.textSearch(textSearchRequest, options.textSearch); + } + break; + } + + if (layer !== undefined) { + if (typeof layer.setOptions == 'function') { + layer.setOptions(options); + } + if (typeof layer.setMap == 'function') { + layer.setMap(this.map); + } + + return layer; + } +}; + +GMaps.prototype.removeLayer = function(layer) { + if (typeof(layer) == "string" && this.singleLayers[layer] !== undefined) { + this.singleLayers[layer].setMap(null); + + delete this.singleLayers[layer]; + } + else { + for (var i = 0; i < this.layers.length; i++) { + if (this.layers[i] === layer) { + this.layers[i].setMap(null); + this.layers.splice(i, 1); + + break; + } + } + } +}; + +var travelMode, unitSystem; + +GMaps.prototype.getRoutes = function(options) { + switch (options.travelMode) { + case 'bicycling': + travelMode = google.maps.TravelMode.BICYCLING; + break; + case 'transit': + travelMode = google.maps.TravelMode.TRANSIT; + break; + case 'driving': + travelMode = google.maps.TravelMode.DRIVING; + break; + default: + travelMode = google.maps.TravelMode.WALKING; + break; + } + + if (options.unitSystem === 'imperial') { + unitSystem = google.maps.UnitSystem.IMPERIAL; + } + else { + unitSystem = google.maps.UnitSystem.METRIC; + } + + var base_options = { + avoidHighways: false, + avoidTolls: false, + optimizeWaypoints: false, + waypoints: [] + }, + request_options = extend_object(base_options, options); + + request_options.origin = /string/.test(typeof options.origin) ? options.origin : new google.maps.LatLng(options.origin[0], options.origin[1]); + request_options.destination = /string/.test(typeof options.destination) ? options.destination : new google.maps.LatLng(options.destination[0], options.destination[1]); + request_options.travelMode = travelMode; + request_options.unitSystem = unitSystem; + + delete request_options.callback; + delete request_options.error; + + var self = this, + routes = [], + service = new google.maps.DirectionsService(); + + service.route(request_options, function(result, status) { + if (status === google.maps.DirectionsStatus.OK) { + for (var r in result.routes) { + if (result.routes.hasOwnProperty(r)) { + routes.push(result.routes[r]); + } + } + + if (options.callback) { + options.callback(routes, result, status); + } + } + else { + if (options.error) { + options.error(result, status); + } + } + }); +}; + +GMaps.prototype.removeRoutes = function() { + this.routes.length = 0; +}; + +GMaps.prototype.getElevations = function(options) { + options = extend_object({ + locations: [], + path : false, + samples : 256 + }, options); + + if (options.locations.length > 0) { + if (options.locations[0].length > 0) { + options.locations = array_flat(array_map([options.locations], arrayToLatLng, false)); + } + } + + var callback = options.callback; + delete options.callback; + + var service = new google.maps.ElevationService(); + + //location request + if (!options.path) { + delete options.path; + delete options.samples; + + service.getElevationForLocations(options, function(result, status) { + if (callback && typeof(callback) === "function") { + callback(result, status); + } + }); + //path request + } else { + var pathRequest = { + path : options.locations, + samples : options.samples + }; + + service.getElevationAlongPath(pathRequest, function(result, status) { + if (callback && typeof(callback) === "function") { + callback(result, status); + } + }); + } +}; + +GMaps.prototype.cleanRoute = GMaps.prototype.removePolylines; + +GMaps.prototype.renderRoute = function(options, renderOptions) { + var self = this, + panel = ((typeof renderOptions.panel === 'string') ? document.getElementById(renderOptions.panel.replace('#', '')) : renderOptions.panel), + display; + + renderOptions.panel = panel; + renderOptions = extend_object({ + map: this.map + }, renderOptions); + display = new google.maps.DirectionsRenderer(renderOptions); + + this.getRoutes({ + origin: options.origin, + destination: options.destination, + travelMode: options.travelMode, + waypoints: options.waypoints, + unitSystem: options.unitSystem, + error: options.error, + avoidHighways: options.avoidHighways, + avoidTolls: options.avoidTolls, + optimizeWaypoints: options.optimizeWaypoints, + callback: function(routes, response, status) { + if (status === google.maps.DirectionsStatus.OK) { + display.setDirections(response); + } + } + }); +}; + +GMaps.prototype.drawRoute = function(options) { + var self = this; + + this.getRoutes({ + origin: options.origin, + destination: options.destination, + travelMode: options.travelMode, + waypoints: options.waypoints, + unitSystem: options.unitSystem, + error: options.error, + avoidHighways: options.avoidHighways, + avoidTolls: options.avoidTolls, + optimizeWaypoints: options.optimizeWaypoints, + callback: function(routes) { + if (routes.length > 0) { + var polyline_options = { + path: routes[routes.length - 1].overview_path, + strokeColor: options.strokeColor, + strokeOpacity: options.strokeOpacity, + strokeWeight: options.strokeWeight + }; + + if (options.hasOwnProperty("icons")) { + polyline_options.icons = options.icons; + } + + self.drawPolyline(polyline_options); + + if (options.callback) { + options.callback(routes[routes.length - 1]); + } + } + } + }); +}; + +GMaps.prototype.travelRoute = function(options) { + if (options.origin && options.destination) { + this.getRoutes({ + origin: options.origin, + destination: options.destination, + travelMode: options.travelMode, + waypoints : options.waypoints, + unitSystem: options.unitSystem, + error: options.error, + callback: function(e) { + //start callback + if (e.length > 0 && options.start) { + options.start(e[e.length - 1]); + } + + //step callback + if (e.length > 0 && options.step) { + var route = e[e.length - 1]; + if (route.legs.length > 0) { + var steps = route.legs[0].steps; + for (var i = 0, step; step = steps[i]; i++) { + step.step_number = i; + options.step(step, (route.legs[0].steps.length - 1)); + } + } + } + + //end callback + if (e.length > 0 && options.end) { + options.end(e[e.length - 1]); + } + } + }); + } + else if (options.route) { + if (options.route.legs.length > 0) { + var steps = options.route.legs[0].steps; + for (var i = 0, step; step = steps[i]; i++) { + step.step_number = i; + options.step(step); + } + } + } +}; + +GMaps.prototype.drawSteppedRoute = function(options) { + var self = this; + + if (options.origin && options.destination) { + this.getRoutes({ + origin: options.origin, + destination: options.destination, + travelMode: options.travelMode, + waypoints : options.waypoints, + error: options.error, + callback: function(e) { + //start callback + if (e.length > 0 && options.start) { + options.start(e[e.length - 1]); + } + + //step callback + if (e.length > 0 && options.step) { + var route = e[e.length - 1]; + if (route.legs.length > 0) { + var steps = route.legs[0].steps; + for (var i = 0, step; step = steps[i]; i++) { + step.step_number = i; + var polyline_options = { + path: step.path, + strokeColor: options.strokeColor, + strokeOpacity: options.strokeOpacity, + strokeWeight: options.strokeWeight + }; + + if (options.hasOwnProperty("icons")) { + polyline_options.icons = options.icons; + } + + self.drawPolyline(polyline_options); + options.step(step, (route.legs[0].steps.length - 1)); + } + } + } + + //end callback + if (e.length > 0 && options.end) { + options.end(e[e.length - 1]); + } + } + }); + } + else if (options.route) { + if (options.route.legs.length > 0) { + var steps = options.route.legs[0].steps; + for (var i = 0, step; step = steps[i]; i++) { + step.step_number = i; + var polyline_options = { + path: step.path, + strokeColor: options.strokeColor, + strokeOpacity: options.strokeOpacity, + strokeWeight: options.strokeWeight + }; + + if (options.hasOwnProperty("icons")) { + polyline_options.icons = options.icons; + } + + self.drawPolyline(polyline_options); + options.step(step); + } + } + } +}; + +GMaps.Route = function(options) { + this.origin = options.origin; + this.destination = options.destination; + this.waypoints = options.waypoints; + + this.map = options.map; + this.route = options.route; + this.step_count = 0; + this.steps = this.route.legs[0].steps; + this.steps_length = this.steps.length; + + var polyline_options = { + path: new google.maps.MVCArray(), + strokeColor: options.strokeColor, + strokeOpacity: options.strokeOpacity, + strokeWeight: options.strokeWeight + }; + + if (options.hasOwnProperty("icons")) { + polyline_options.icons = options.icons; + } + + this.polyline = this.map.drawPolyline(polyline_options).getPath(); +}; + +GMaps.Route.prototype.getRoute = function(options) { + var self = this; + + this.map.getRoutes({ + origin : this.origin, + destination : this.destination, + travelMode : options.travelMode, + waypoints : this.waypoints || [], + error: options.error, + callback : function() { + self.route = e[0]; + + if (options.callback) { + options.callback.call(self); + } + } + }); +}; + +GMaps.Route.prototype.back = function() { + if (this.step_count > 0) { + this.step_count--; + var path = this.route.legs[0].steps[this.step_count].path; + + for (var p in path){ + if (path.hasOwnProperty(p)){ + this.polyline.pop(); + } + } + } +}; + +GMaps.Route.prototype.forward = function() { + if (this.step_count < this.steps_length) { + var path = this.route.legs[0].steps[this.step_count].path; + + for (var p in path){ + if (path.hasOwnProperty(p)){ + this.polyline.push(path[p]); + } + } + this.step_count++; + } +}; + +GMaps.prototype.checkGeofence = function(lat, lng, fence) { + return fence.containsLatLng(new google.maps.LatLng(lat, lng)); +}; + +GMaps.prototype.checkMarkerGeofence = function(marker, outside_callback) { + if (marker.fences) { + for (var i = 0, fence; fence = marker.fences[i]; i++) { + var pos = marker.getPosition(); + if (!this.checkGeofence(pos.lat(), pos.lng(), fence)) { + outside_callback(marker, fence); + } + } + } +}; + +GMaps.prototype.toImage = function(options) { + var options = options || {}, + static_map_options = {}; + + static_map_options['size'] = options['size'] || [this.el.clientWidth, this.el.clientHeight]; + static_map_options['lat'] = this.getCenter().lat(); + static_map_options['lng'] = this.getCenter().lng(); + + if (this.markers.length > 0) { + static_map_options['markers'] = []; + + for (var i = 0; i < this.markers.length; i++) { + static_map_options['markers'].push({ + lat: this.markers[i].getPosition().lat(), + lng: this.markers[i].getPosition().lng() + }); + } + } + + if (this.polylines.length > 0) { + var polyline = this.polylines[0]; + + static_map_options['polyline'] = {}; + static_map_options['polyline']['path'] = google.maps.geometry.encoding.encodePath(polyline.getPath()); + static_map_options['polyline']['strokeColor'] = polyline.strokeColor + static_map_options['polyline']['strokeOpacity'] = polyline.strokeOpacity + static_map_options['polyline']['strokeWeight'] = polyline.strokeWeight + } + + return GMaps.staticMapURL(static_map_options); +}; + +GMaps.staticMapURL = function(options){ + var parameters = [], + data, + static_root = (location.protocol === 'file:' ? 'http:' : location.protocol ) + '//maps.googleapis.com/maps/api/staticmap'; + + if (options.url) { + static_root = options.url; + delete options.url; + } + + static_root += '?'; + + var markers = options.markers; + + delete options.markers; + + if (!markers && options.marker) { + markers = [options.marker]; + delete options.marker; + } + + var styles = options.styles; + + delete options.styles; + + var polyline = options.polyline; + delete options.polyline; + + /** Map options **/ + if (options.center) { + parameters.push('center=' + options.center); + delete options.center; + } + else if (options.address) { + parameters.push('center=' + options.address); + delete options.address; + } + else if (options.lat) { + parameters.push(['center=', options.lat, ',', options.lng].join('')); + delete options.lat; + delete options.lng; + } + else if (options.visible) { + var visible = encodeURI(options.visible.join('|')); + parameters.push('visible=' + visible); + } + + var size = options.size; + if (size) { + if (size.join) { + size = size.join('x'); + } + delete options.size; + } + else { + size = '630x300'; + } + parameters.push('size=' + size); + + if (!options.zoom && options.zoom !== false) { + options.zoom = 15; + } + + var sensor = options.hasOwnProperty('sensor') ? !!options.sensor : true; + delete options.sensor; + parameters.push('sensor=' + sensor); + + for (var param in options) { + if (options.hasOwnProperty(param)) { + parameters.push(param + '=' + options[param]); + } + } + + /** Markers **/ + if (markers) { + var marker, loc; + + for (var i = 0; data = markers[i]; i++) { + marker = []; + + if (data.size && data.size !== 'normal') { + marker.push('size:' + data.size); + delete data.size; + } + else if (data.icon) { + marker.push('icon:' + encodeURI(data.icon)); + delete data.icon; + } + + if (data.color) { + marker.push('color:' + data.color.replace('#', '0x')); + delete data.color; + } + + if (data.label) { + marker.push('label:' + data.label[0].toUpperCase()); + delete data.label; + } + + loc = (data.address ? data.address : data.lat + ',' + data.lng); + delete data.address; + delete data.lat; + delete data.lng; + + for(var param in data){ + if (data.hasOwnProperty(param)) { + marker.push(param + ':' + data[param]); + } + } + + if (marker.length || i === 0) { + marker.push(loc); + marker = marker.join('|'); + parameters.push('markers=' + encodeURI(marker)); + } + // New marker without styles + else { + marker = parameters.pop() + encodeURI('|' + loc); + parameters.push(marker); + } + } + } + + /** Map Styles **/ + if (styles) { + for (var i = 0; i < styles.length; i++) { + var styleRule = []; + if (styles[i].featureType){ + styleRule.push('feature:' + styles[i].featureType.toLowerCase()); + } + + if (styles[i].elementType) { + styleRule.push('element:' + styles[i].elementType.toLowerCase()); + } + + for (var j = 0; j < styles[i].stylers.length; j++) { + for (var p in styles[i].stylers[j]) { + var ruleArg = styles[i].stylers[j][p]; + if (p == 'hue' || p == 'color') { + ruleArg = '0x' + ruleArg.substring(1); + } + styleRule.push(p + ':' + ruleArg); + } + } + + var rule = styleRule.join('|'); + if (rule != '') { + parameters.push('style=' + rule); + } + } + } + + /** Polylines **/ + function parseColor(color, opacity) { + if (color[0] === '#'){ + color = color.replace('#', '0x'); + + if (opacity) { + opacity = parseFloat(opacity); + opacity = Math.min(1, Math.max(opacity, 0)); + if (opacity === 0) { + return '0x00000000'; + } + opacity = (opacity * 255).toString(16); + if (opacity.length === 1) { + opacity += opacity; + } + + color = color.slice(0,8) + opacity; + } + } + return color; + } + + if (polyline) { + data = polyline; + polyline = []; + + if (data.strokeWeight) { + polyline.push('weight:' + parseInt(data.strokeWeight, 10)); + } + + if (data.strokeColor) { + var color = parseColor(data.strokeColor, data.strokeOpacity); + polyline.push('color:' + color); + } + + if (data.fillColor) { + var fillcolor = parseColor(data.fillColor, data.fillOpacity); + polyline.push('fillcolor:' + fillcolor); + } + + var path = data.path; + if (path.join) { + for (var j=0, pos; pos=path[j]; j++) { + polyline.push(pos.join(',')); + } + } + else { + polyline.push('enc:' + path); + } + + polyline = polyline.join('|'); + parameters.push('path=' + encodeURI(polyline)); + } + + /** Retina support **/ + var dpi = window.devicePixelRatio || 1; + parameters.push('scale=' + dpi); + + parameters = parameters.join('&'); + return static_root + parameters; +}; + +GMaps.prototype.addMapType = function(mapTypeId, options) { + if (options.hasOwnProperty("getTileUrl") && typeof(options["getTileUrl"]) == "function") { + options.tileSize = options.tileSize || new google.maps.Size(256, 256); + + var mapType = new google.maps.ImageMapType(options); + + this.map.mapTypes.set(mapTypeId, mapType); + } + else { + throw "'getTileUrl' function required."; + } +}; + +GMaps.prototype.addOverlayMapType = function(options) { + if (options.hasOwnProperty("getTile") && typeof(options["getTile"]) == "function") { + var overlayMapTypeIndex = options.index; + + delete options.index; + + this.map.overlayMapTypes.insertAt(overlayMapTypeIndex, options); + } + else { + throw "'getTile' function required."; + } +}; + +GMaps.prototype.removeOverlayMapType = function(overlayMapTypeIndex) { + this.map.overlayMapTypes.removeAt(overlayMapTypeIndex); +}; + +GMaps.prototype.addStyle = function(options) { + var styledMapType = new google.maps.StyledMapType(options.styles, { name: options.styledMapName }); + + this.map.mapTypes.set(options.mapTypeId, styledMapType); +}; + +GMaps.prototype.setStyle = function(mapTypeId) { + this.map.setMapTypeId(mapTypeId); +}; + +GMaps.prototype.createPanorama = function(streetview_options) { + if (!streetview_options.hasOwnProperty('lat') || !streetview_options.hasOwnProperty('lng')) { + streetview_options.lat = this.getCenter().lat(); + streetview_options.lng = this.getCenter().lng(); + } + + this.panorama = GMaps.createPanorama(streetview_options); + + this.map.setStreetView(this.panorama); + + return this.panorama; +}; + +GMaps.createPanorama = function(options) { + var el = getElementById(options.el, options.context); + + options.position = new google.maps.LatLng(options.lat, options.lng); + + delete options.el; + delete options.context; + delete options.lat; + delete options.lng; + + var streetview_events = ['closeclick', 'links_changed', 'pano_changed', 'position_changed', 'pov_changed', 'resize', 'visible_changed'], + streetview_options = extend_object({visible : true}, options); + + for (var i = 0; i < streetview_events.length; i++) { + delete streetview_options[streetview_events[i]]; + } + + var panorama = new google.maps.StreetViewPanorama(el, streetview_options); + + for (var i = 0; i < streetview_events.length; i++) { + (function(object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function(){ + options[name].apply(this); + }); + } + })(panorama, streetview_events[i]); + } + + return panorama; +}; + +GMaps.prototype.on = function(event_name, handler) { + return GMaps.on(event_name, this, handler); +}; + +GMaps.prototype.off = function(event_name) { + GMaps.off(event_name, this); +}; + +GMaps.prototype.once = function(event_name, handler) { + return GMaps.once(event_name, this, handler); +}; + +GMaps.custom_events = ['marker_added', 'marker_removed', 'polyline_added', 'polyline_removed', 'polygon_added', 'polygon_removed', 'geolocated', 'geolocation_failed']; + +GMaps.on = function(event_name, object, handler) { + if (GMaps.custom_events.indexOf(event_name) == -1) { + if(object instanceof GMaps) object = object.map; + return google.maps.event.addListener(object, event_name, handler); + } + else { + var registered_event = { + handler : handler, + eventName : event_name + }; + + object.registered_events[event_name] = object.registered_events[event_name] || []; + object.registered_events[event_name].push(registered_event); + + return registered_event; + } +}; + +GMaps.off = function(event_name, object) { + if (GMaps.custom_events.indexOf(event_name) == -1) { + if(object instanceof GMaps) object = object.map; + google.maps.event.clearListeners(object, event_name); + } + else { + object.registered_events[event_name] = []; + } +}; + +GMaps.once = function(event_name, object, handler) { + if (GMaps.custom_events.indexOf(event_name) == -1) { + if(object instanceof GMaps) object = object.map; + return google.maps.event.addListenerOnce(object, event_name, handler); + } +}; + +GMaps.fire = function(event_name, object, scope) { + if (GMaps.custom_events.indexOf(event_name) == -1) { + google.maps.event.trigger(object, event_name, Array.prototype.slice.apply(arguments).slice(2)); + } + else { + if(event_name in scope.registered_events) { + var firing_events = scope.registered_events[event_name]; + + for(var i = 0; i < firing_events.length; i++) { + (function(handler, scope, object) { + handler.apply(scope, [object]); + })(firing_events[i]['handler'], scope, object); + } + } + } +}; + +GMaps.geolocate = function(options) { + var complete_callback = options.always || options.complete; + + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(function(position) { + options.success(position); + + if (complete_callback) { + complete_callback(); + } + }, function(error) { + options.error(error); + + if (complete_callback) { + complete_callback(); + } + }, options.options); + } + else { + options.not_supported(); + + if (complete_callback) { + complete_callback(); + } + } +}; + +GMaps.geocode = function(options) { + this.geocoder = new google.maps.Geocoder(); + var callback = options.callback; + if (options.hasOwnProperty('lat') && options.hasOwnProperty('lng')) { + options.latLng = new google.maps.LatLng(options.lat, options.lng); + } + + delete options.lat; + delete options.lng; + delete options.callback; + + this.geocoder.geocode(options, function(results, status) { + callback(results, status); + }); +}; + +if (typeof window.google === 'object' && window.google.maps) { + //========================== + // Polygon containsLatLng + // https://github.com/tparkin/Google-Maps-Point-in-Polygon + // Poygon getBounds extension - google-maps-extensions + // http://code.google.com/p/google-maps-extensions/source/browse/google.maps.Polygon.getBounds.js + if (!google.maps.Polygon.prototype.getBounds) { + google.maps.Polygon.prototype.getBounds = function(latLng) { + var bounds = new google.maps.LatLngBounds(); + var paths = this.getPaths(); + var path; + + for (var p = 0; p < paths.getLength(); p++) { + path = paths.getAt(p); + for (var i = 0; i < path.getLength(); i++) { + bounds.extend(path.getAt(i)); + } + } + + return bounds; + }; + } + + if (!google.maps.Polygon.prototype.containsLatLng) { + // Polygon containsLatLng - method to determine if a latLng is within a polygon + google.maps.Polygon.prototype.containsLatLng = function(latLng) { + // Exclude points outside of bounds as there is no way they are in the poly + var bounds = this.getBounds(); + + if (bounds !== null && !bounds.contains(latLng)) { + return false; + } + + // Raycast point in polygon method + var inPoly = false; + + var numPaths = this.getPaths().getLength(); + for (var p = 0; p < numPaths; p++) { + var path = this.getPaths().getAt(p); + var numPoints = path.getLength(); + var j = numPoints - 1; + + for (var i = 0; i < numPoints; i++) { + var vertex1 = path.getAt(i); + var vertex2 = path.getAt(j); + + if (vertex1.lng() < latLng.lng() && vertex2.lng() >= latLng.lng() || vertex2.lng() < latLng.lng() && vertex1.lng() >= latLng.lng()) { + if (vertex1.lat() + (latLng.lng() - vertex1.lng()) / (vertex2.lng() - vertex1.lng()) * (vertex2.lat() - vertex1.lat()) < latLng.lat()) { + inPoly = !inPoly; + } + } + + j = i; + } + } + + return inPoly; + }; + } + + if (!google.maps.Circle.prototype.containsLatLng) { + google.maps.Circle.prototype.containsLatLng = function(latLng) { + if (google.maps.geometry) { + return google.maps.geometry.spherical.computeDistanceBetween(this.getCenter(), latLng) <= this.getRadius(); + } + else { + return true; + } + }; + } + + google.maps.Rectangle.prototype.containsLatLng = function(latLng) { + return this.getBounds().contains(latLng); + }; + + google.maps.LatLngBounds.prototype.containsLatLng = function(latLng) { + return this.contains(latLng); + }; + + google.maps.Marker.prototype.setFences = function(fences) { + this.fences = fences; + }; + + google.maps.Marker.prototype.addFence = function(fence) { + this.fences.push(fence); + }; + + google.maps.Marker.prototype.getId = function() { + return this['__gm_id']; + }; +} + +//========================== +// Array indexOf +// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf +if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { + "use strict"; + if (this == null) { + throw new TypeError(); + } + var t = Object(this); + var len = t.length >>> 0; + if (len === 0) { + return -1; + } + var n = 0; + if (arguments.length > 1) { + n = Number(arguments[1]); + if (n != n) { // shortcut for verifying if it's NaN + n = 0; + } else if (n != 0 && n != Infinity && n != -Infinity) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + if (n >= len) { + return -1; + } + var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); + for (; k < len; k++) { + if (k in t && t[k] === searchElement) { + return k; + } + } + return -1; + } +} + +return GMaps; +})); diff --git a/public/static/libs/gmapsjs/gmaps.min.js b/public/static/libs/gmapsjs/gmaps.min.js new file mode 100644 index 0000000..75c9194 --- /dev/null +++ b/public/static/libs/gmapsjs/gmaps.min.js @@ -0,0 +1,2 @@ +"use strict";!function(a,b){"object"==typeof exports?module.exports=b():"function"==typeof define&&define.amd?define(["jquery","googlemaps!"],b):a.GMaps=b()}(this,function(){var a=function(a,b){var c;if(a===b)return a;for(c in b)void 0!==b[c]&&(a[c]=b[c]);return a},b=function(a,b){var c,d=Array.prototype.slice.call(arguments,2),e=[],f=a.length;if(Array.prototype.map&&a.map===Array.prototype.map)e=Array.prototype.map.call(a,function(a){var c=d.slice(0);return c.splice(0,0,a),b.apply(this,c)});else for(c=0;f>c;c++)callback_params=d,callback_params.splice(0,0,a[c]),e.push(b.apply(this,callback_params));return e},c=function(a){var b,c=[];for(b=0;b0&&"object"==typeof a[c][0]?a[c]=f(a[c],b):a[c]=d(a[c],b));return a},g=function(a,b){var c,d=a.replace(".","");return c="jQuery"in this&&b?$("."+d,b)[0]:document.getElementsByClassName(d)[0]},h=function(a,b){var c,a=a.replace("#","");return c="jQuery"in window&&b?$("#"+a,b)[0]:document.getElementById(a)},i=function(a){var b=0,c=0;if(a.offsetParent)do b+=a.offsetLeft,c+=a.offsetTop;while(a=a.offsetParent);return[b,c]},j=function(b){var c=document,d=function(b){if("object"!=typeof window.google||!window.google.maps)return"object"==typeof window.console&&window.console.error&&console.error("Google Maps API is required. Please register the following JavaScript library https://maps.googleapis.com/maps/api/js."),function(){};if(!this)return new d(b);b.zoom=b.zoom||15,b.mapType=b.mapType||"roadmap";var e,f=function(a,b){return void 0===a?b:a},j=this,k=["bounds_changed","center_changed","click","dblclick","drag","dragend","dragstart","idle","maptypeid_changed","projection_changed","resize","tilesloaded","zoom_changed"],l=["mousemove","mouseout","mouseover"],m=["el","lat","lng","mapType","width","height","markerClusterer","enableNewStyle"],n=b.el||b.div,o=b.markerClusterer,p=google.maps.MapTypeId[b.mapType.toUpperCase()],q=new google.maps.LatLng(b.lat,b.lng),r=f(b.zoomControl,!0),s=b.zoomControlOpt||{style:"DEFAULT",position:"TOP_LEFT"},t=s.style||"DEFAULT",u=s.position||"TOP_LEFT",v=f(b.panControl,!0),w=f(b.mapTypeControl,!0),x=f(b.scaleControl,!0),y=f(b.streetViewControl,!0),z=f(z,!0),A={},B={zoom:this.zoom,center:q,mapTypeId:p},C={panControl:v,zoomControl:r,zoomControlOptions:{style:google.maps.ZoomControlStyle[t],position:google.maps.ControlPosition[u]},mapTypeControl:w,scaleControl:x,streetViewControl:y,overviewMapControl:z};if("string"==typeof b.el||"string"==typeof b.div?n.indexOf("#")>-1?this.el=h(n,b.context):this.el=g.apply(this,[n,b.context]):this.el=n,"undefined"==typeof this.el||null===this.el)throw"No element defined.";for(window.context_menu=window.context_menu||{},window.context_menu[j.el.id]={},this.controls=[],this.overlays=[],this.layers=[],this.singleLayers={},this.markers=[],this.polylines=[],this.routes=[],this.polygons=[],this.infoWindow=null,this.overlay_el=null,this.zoom=b.zoom,this.registered_events={},this.el.style.width=b.width||this.el.scrollWidth||this.el.offsetWidth,this.el.style.height=b.height||this.el.scrollHeight||this.el.offsetHeight,google.maps.visualRefresh=b.enableNewStyle,e=0;e'+f.title+""}if(h("gmaps_context_menu")){var g=h("gmaps_context_menu");g.innerHTML=c;var e,k=g.getElementsByTagName("a"),l=k.length;for(e=0;l>e;e++){var m=k[e],n=function(c){c.preventDefault(),d[this.id.replace(a+"_","")].action.apply(j,[b]),j.hideContextMenu()};google.maps.event.clearListeners(m,"click"),google.maps.event.addDomListenerOnce(m,"click",n,!1)}var o=i.apply(this,[j.el]),p=o[0]+b.pixel.x-15,q=o[1]+b.pixel.y-15;g.style.left=p+"px",g.style.top=q+"px"}};this.buildContextMenu=function(a,b){if("marker"===a){b.pixel={};var c=new google.maps.OverlayView;c.setMap(j.map),c.draw=function(){var d=c.getProjection(),e=b.marker.getPosition();b.pixel=d.fromLatLngToContainerPixel(e),D(a,b)}}else D(a,b);var d=h("gmaps_context_menu");setTimeout(function(){d.style.display="block"},0)},this.setContextMenu=function(a){window.context_menu[j.el.id][a.control]={};var b,d=c.createElement("ul");for(b in a.options)if(a.options.hasOwnProperty(b)){var e=a.options[b];window.context_menu[j.el.id][a.control][e.name]={title:e.title,action:e.action}}d.id="gmaps_context_menu",d.style.display="none",d.style.position="absolute",d.style.minWidth="100px",d.style.background="white",d.style.listStyle="none",d.style.padding="8px",d.style.boxShadow="2px 2px 6px #ccc",h("gmaps_context_menu")||c.body.appendChild(d);var f=h("gmaps_context_menu");google.maps.event.addDomListener(f,"mouseout",function(a){a.relatedTarget&&this.contains(a.relatedTarget)||window.setTimeout(function(){f.style.display="none"},400)},!1)},this.hideContextMenu=function(){var a=h("gmaps_context_menu");a&&(a.style.display="none")};var E=function(a,c){google.maps.event.addListener(a,c,function(a){void 0==a&&(a=this),b[c].apply(this,[a]),j.hideContextMenu()})};google.maps.event.addListener(this.map,"zoom_changed",this.hideContextMenu);for(var F=0;Fa;a++)"boolean"==typeof this.markers[a].visible&&this.markers[a].visible&&b.push(this.markers[a].getPosition());this.fitLatLngBounds(b)},this.fitLatLngBounds=function(a){var b,c=a.length,d=new google.maps.LatLngBounds;for(b=0;c>b;b++)d.extend(a[b]);this.map.fitBounds(d)},this.setCenter=function(a,b,c){this.map.panTo(new google.maps.LatLng(a,b)),c&&c()},this.getElement=function(){return this.el},this.zoomIn=function(a){a=a||1,this.zoom=this.map.getZoom()+a,this.map.setZoom(this.zoom)},this.zoomOut=function(a){a=a||1,this.zoom=this.map.getZoom()-a,this.map.setZoom(this.zoom)};var H,I=[];for(H in this.map)"function"!=typeof this.map[H]||this[H]||I.push(H);for(e=0;e-1){var d=this.markers[e];d.setMap(null),this.markerClusterer&&this.markerClusterer.removeMarker(d),j.fire("marker_removed",d,this)}}for(var c=0;c0&&d.paths[0].length>0&&(d.paths=c(b(d.paths,f,e)));for(var g=new google.maps.Polygon(d),h=["click","dblclick","mousedown","mousemove","mouseout","mouseover","mouseup","rightclick"],i=0;i0&&d.locations[0].length>0&&(d.locations=c(b([d.locations],f,!1)));var e=d.callback;delete d.callback;var g=new google.maps.ElevationService;if(d.path){var h={path:d.locations,samples:d.samples};g.getElevationAlongPath(h,function(a,b){e&&"function"==typeof e&&e(a,b)})}else delete d.path,delete d.samples,g.getElevationForLocations(d,function(a,b){e&&"function"==typeof e&&e(a,b)})},j.prototype.cleanRoute=j.prototype.removePolylines,j.prototype.renderRoute=function(b,c){var d,e="string"==typeof c.panel?document.getElementById(c.panel.replace("#","")):c.panel;c.panel=e,c=a({map:this.map},c),d=new google.maps.DirectionsRenderer(c),this.getRoutes({origin:b.origin,destination:b.destination,travelMode:b.travelMode,waypoints:b.waypoints,unitSystem:b.unitSystem,error:b.error,avoidHighways:b.avoidHighways,avoidTolls:b.avoidTolls,optimizeWaypoints:b.optimizeWaypoints,callback:function(a,b,c){c===google.maps.DirectionsStatus.OK&&d.setDirections(b)}})},j.prototype.drawRoute=function(a){var b=this;this.getRoutes({origin:a.origin,destination:a.destination,travelMode:a.travelMode,waypoints:a.waypoints,unitSystem:a.unitSystem,error:a.error,avoidHighways:a.avoidHighways,avoidTolls:a.avoidTolls,optimizeWaypoints:a.optimizeWaypoints,callback:function(c){if(c.length>0){var d={path:c[c.length-1].overview_path,strokeColor:a.strokeColor,strokeOpacity:a.strokeOpacity,strokeWeight:a.strokeWeight};a.hasOwnProperty("icons")&&(d.icons=a.icons),b.drawPolyline(d),a.callback&&a.callback(c[c.length-1])}}})},j.prototype.travelRoute=function(a){if(a.origin&&a.destination)this.getRoutes({origin:a.origin,destination:a.destination,travelMode:a.travelMode,waypoints:a.waypoints,unitSystem:a.unitSystem,error:a.error,callback:function(b){if(b.length>0&&a.start&&a.start(b[b.length-1]),b.length>0&&a.step){var c=b[b.length-1];if(c.legs.length>0)for(var d,e=c.legs[0].steps,f=0;d=e[f];f++)d.step_number=f,a.step(d,c.legs[0].steps.length-1)}b.length>0&&a.end&&a.end(b[b.length-1])}});else if(a.route&&a.route.legs.length>0)for(var b,c=a.route.legs[0].steps,d=0;b=c[d];d++)b.step_number=d,a.step(b)},j.prototype.drawSteppedRoute=function(a){var b=this;if(a.origin&&a.destination)this.getRoutes({origin:a.origin,destination:a.destination,travelMode:a.travelMode,waypoints:a.waypoints,error:a.error,callback:function(c){if(c.length>0&&a.start&&a.start(c[c.length-1]),c.length>0&&a.step){var d=c[c.length-1];if(d.legs.length>0)for(var e,f=d.legs[0].steps,g=0;e=f[g];g++){e.step_number=g;var h={path:e.path,strokeColor:a.strokeColor,strokeOpacity:a.strokeOpacity,strokeWeight:a.strokeWeight};a.hasOwnProperty("icons")&&(h.icons=a.icons),b.drawPolyline(h),a.step(e,d.legs[0].steps.length-1)}}c.length>0&&a.end&&a.end(c[c.length-1])}});else if(a.route&&a.route.legs.length>0)for(var c,d=a.route.legs[0].steps,e=0;c=d[e];e++){c.step_number=e;var f={path:c.path,strokeColor:a.strokeColor,strokeOpacity:a.strokeOpacity,strokeWeight:a.strokeWeight};a.hasOwnProperty("icons")&&(f.icons=a.icons),b.drawPolyline(f),a.step(c)}},j.Route=function(a){this.origin=a.origin,this.destination=a.destination,this.waypoints=a.waypoints,this.map=a.map,this.route=a.route,this.step_count=0,this.steps=this.route.legs[0].steps,this.steps_length=this.steps.length;var b={path:new google.maps.MVCArray,strokeColor:a.strokeColor,strokeOpacity:a.strokeOpacity,strokeWeight:a.strokeWeight};a.hasOwnProperty("icons")&&(b.icons=a.icons),this.polyline=this.map.drawPolyline(b).getPath()},j.Route.prototype.getRoute=function(a){var b=this;this.map.getRoutes({origin:this.origin,destination:this.destination,travelMode:a.travelMode,waypoints:this.waypoints||[],error:a.error,callback:function(){b.route=e[0],a.callback&&a.callback.call(b)}})},j.Route.prototype.back=function(){if(this.step_count>0){this.step_count--;var a=this.route.legs[0].steps[this.step_count].path;for(var b in a)a.hasOwnProperty(b)&&this.polyline.pop()}},j.Route.prototype.forward=function(){if(this.step_count0){b.markers=[];for(var c=0;c0){var d=this.polylines[0];b.polyline={},b.polyline.path=google.maps.geometry.encoding.encodePath(d.getPath()),b.polyline.strokeColor=d.strokeColor,b.polyline.strokeOpacity=d.strokeOpacity,b.polyline.strokeWeight=d.strokeWeight}return j.staticMapURL(b)},j.staticMapURL=function(a){function b(a,b){if("#"===a[0]&&(a=a.replace("#","0x"),b)){if(b=parseFloat(b),b=Math.min(1,Math.max(b,0)),0===b)return"0x00000000";b=(255*b).toString(16),1===b.length&&(b+=b),a=a.slice(0,8)+b}return a}var c,d=[],e=("file:"===location.protocol?"http:":location.protocol)+"//maps.googleapis.com/maps/api/staticmap";a.url&&(e=a.url,delete a.url),e+="?";var f=a.markers;delete a.markers,!f&&a.marker&&(f=[a.marker],delete a.marker);var g=a.styles;delete a.styles;var h=a.polyline;if(delete a.polyline,a.center)d.push("center="+a.center),delete a.center;else if(a.address)d.push("center="+a.address),delete a.address;else if(a.lat)d.push(["center=",a.lat,",",a.lng].join("")),delete a.lat,delete a.lng;else if(a.visible){var i=encodeURI(a.visible.join("|"));d.push("visible="+i)}var j=a.size;j?(j.join&&(j=j.join("x")),delete a.size):j="630x300",d.push("size="+j),a.zoom||a.zoom===!1||(a.zoom=15);var k=a.hasOwnProperty("sensor")?!!a.sensor:!0;delete a.sensor,d.push("sensor="+k);for(var l in a)a.hasOwnProperty(l)&&d.push(l+"="+a[l]);if(f)for(var m,n,o=0;c=f[o];o++){m=[],c.size&&"normal"!==c.size?(m.push("size:"+c.size),delete c.size):c.icon&&(m.push("icon:"+encodeURI(c.icon)),delete c.icon),c.color&&(m.push("color:"+c.color.replace("#","0x")),delete c.color),c.label&&(m.push("label:"+c.label[0].toUpperCase()),delete c.label),n=c.address?c.address:c.lat+","+c.lng,delete c.address,delete c.lat,delete c.lng;for(var l in c)c.hasOwnProperty(l)&&m.push(l+":"+c[l]);m.length||0===o?(m.push(n),m=m.join("|"),d.push("markers="+encodeURI(m))):(m=d.pop()+encodeURI("|"+n),d.push(m))}if(g)for(var o=0;oe;e++)for(var f=this.getPaths().getAt(e),g=f.getLength(),h=g-1,i=0;g>i;i++){var j=f.getAt(i),k=f.getAt(h);(j.lng()=a.lng()||k.lng()=a.lng())&&j.lat()+(a.lng()-j.lng())/(k.lng()-j.lng())*(k.lat()-j.lat())>>0;if(0===c)return-1;var d=0;if(arguments.length>1&&(d=Number(arguments[1]),d!=d?d=0:0!=d&&d!=1/0&&d!=-(1/0)&&(d=(d>0||-1)*Math.floor(Math.abs(d)))),d>=c)return-1;for(var e=d>=0?d:Math.max(c-Math.abs(d),0);c>e;e++)if(e in b&&b[e]===a)return e;return-1}),j}); +//# sourceMappingURL=gmaps.min.js.map \ No newline at end of file diff --git a/public/static/libs/gmapsjs/gmaps.min.js.map b/public/static/libs/gmapsjs/gmaps.min.js.map new file mode 100644 index 0000000..c0482a1 --- /dev/null +++ b/public/static/libs/gmapsjs/gmaps.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"gmaps.min.js","sources":["gmaps.js"],"names":["root","factory","exports","module","define","amd","GMaps","this","extend_object","obj","new_obj","name","undefined","array_map","array","callback","i","original_callback_params","Array","prototype","slice","call","arguments","array_return","array_length","length","map","item","callback_params","splice","apply","push","array_flat","new_array","concat","coordsToLatLngs","coords","useGeoJSON","first_coord","second_coord","google","maps","LatLng","arrayToLatLng","getElementsByClassName","class_name","context","element","_class","replace","$","document","getElementById","id","window","findAbsolutePosition","curleft","curtop","offsetParent","offsetLeft","offsetTop","global","doc","options","console","error","zoom","mapType","valueOrDefault","value","defaultValue","self","events_that_hide_context_menu","events_that_doesnt_hide_context_menu","options_to_be_deleted","identifier","el","div","markerClustererFunction","markerClusterer","MapTypeId","toUpperCase","map_center","lat","lng","zoomControl","zoomControlOpt","style","position","zoomControlStyle","zoomControlPosition","panControl","mapTypeControl","scaleControl","streetViewControl","overviewMapControl","map_options","map_base_options","center","mapTypeId","map_controls_options","zoomControlOptions","ZoomControlStyle","ControlPosition","indexOf","context_menu","controls","overlays","layers","singleLayers","markers","polylines","routes","polygons","infoWindow","overlay_el","registered_events","width","scrollWidth","offsetWidth","height","scrollHeight","offsetHeight","visualRefresh","enableNewStyle","disableDefaultUI","Map","buildContextMenuHTML","control","e","html","hasOwnProperty","option","title","context_menu_element","innerHTML","context_menu_items","getElementsByTagName","context_menu_items_count","context_menu_item","assign_menu_item_action","ev","preventDefault","action","hideContextMenu","event","clearListeners","addDomListenerOnce","left","pixel","x","top","y","buildContextMenu","overlay","OverlayView","setMap","draw","projection","getProjection","marker","getPosition","fromLatLngToContainerPixel","setTimeout","display","setContextMenu","ul","createElement","minWidth","background","listStyle","padding","boxShadow","body","appendChild","addDomListener","relatedTarget","contains","setupListener","object","addListener","rightclick","refresh","trigger","fitZoom","latLngs","markers_length","visible","fitLatLngBounds","total","bounds","LatLngBounds","extend","fitBounds","setCenter","panTo","getElement","zoomIn","getZoom","setZoom","zoomOut","method","native_methods","gmaps","scope","method_name","createControl","cursor","disableDefaultStyles","fontFamily","fontSize","classes","className","content","HTMLElement","events","index","addControl","removeControl","controlsForPosition","getAt","removeAt","createMarker","details","fences","outside","base_options","marker_options","Marker","InfoWindow","info_window_events","marker_events","marker_events_with_mouse","me","fromLatLngToPoint","latLng","click","hideInfoWindows","open","checkMarkerGeofence","m","f","addMarker","fire","addMarkers","close","removeMarker","removeMarkers","collection","new_markers","clearMarkers","getMap","drawOverlay","auto_show","onAdd","borderStyle","borderWidth","zIndex","layer","panes","getPanes","overlayLayer","stop_overlay_events","navigator","userAgent","toLowerCase","all","cancelBubble","returnValue","stopPropagation","overlayMouseTarget","fromLatLngToDivPixel","horizontalOffset","verticalOffset","children","content_height","clientHeight","content_width","clientWidth","verticalAlign","horizontalAlign","show","onRemove","remove","parentNode","removeChild","removeOverlay","removeOverlays","drawPolyline","path","points","latlng","polyline_options","strokeColor","strokeOpacity","strokeWeight","geodesic","clickable","editable","icons","polyline","Polyline","polyline_events","removePolyline","removePolylines","drawCircle","polygon","Circle","polygon_events","drawRectangle","latLngBounds","Rectangle","drawPolygon","paths","Polygon","removePolygon","removePolygons","getFromFusionTables","fusion_tables_options","FusionTablesLayer","loadFromFusionTables","getFromKML","url","kml_options","KmlLayer","loadFromKML","addLayer","layerName","weather","WeatherLayer","clouds","CloudLayer","traffic","TrafficLayer","transit","TransitLayer","bicycling","BicyclingLayer","panoramio","PanoramioLayer","setTag","filter","places","PlacesService","search","nearbySearch","radarSearch","placeSearchRequest","keyword","location","radius","rankBy","types","textSearch","textSearchRequest","query","setOptions","removeLayer","travelMode","unitSystem","getRoutes","TravelMode","BICYCLING","TRANSIT","DRIVING","WALKING","UnitSystem","IMPERIAL","METRIC","avoidHighways","avoidTolls","optimizeWaypoints","waypoints","request_options","origin","test","destination","service","DirectionsService","route","result","status","DirectionsStatus","OK","r","removeRoutes","getElevations","locations","samples","ElevationService","pathRequest","getElevationAlongPath","getElevationForLocations","cleanRoute","renderRoute","renderOptions","panel","DirectionsRenderer","response","setDirections","drawRoute","overview_path","travelRoute","start","step","legs","steps","step_number","end","drawSteppedRoute","Route","step_count","steps_length","MVCArray","getPath","getRoute","back","p","pop","forward","checkGeofence","fence","containsLatLng","outside_callback","pos","toImage","static_map_options","getCenter","geometry","encoding","encodePath","staticMapURL","parseColor","color","opacity","parseFloat","Math","min","max","toString","data","parameters","static_root","protocol","styles","address","join","encodeURI","size","sensor","param","loc","icon","label","styleRule","featureType","elementType","j","stylers","ruleArg","substring","rule","parseInt","fillColor","fillcolor","fillOpacity","dpi","devicePixelRatio","addMapType","tileSize","Size","ImageMapType","mapTypes","set","addOverlayMapType","overlayMapTypeIndex","overlayMapTypes","insertAt","removeOverlayMapType","addStyle","styledMapType","StyledMapType","styledMapName","setStyle","setMapTypeId","createPanorama","streetview_options","panorama","setStreetView","streetview_events","StreetViewPanorama","on","event_name","handler","off","once","custom_events","registered_event","eventName","addListenerOnce","firing_events","geolocate","complete_callback","always","complete","geolocation","getCurrentPosition","success","not_supported","geocode","geocoder","Geocoder","results","getBounds","getPaths","getLength","inPoly","numPaths","numPoints","vertex1","vertex2","spherical","computeDistanceBetween","getRadius","setFences","addFence","getId","searchElement","TypeError","t","Object","len","n","Number","Infinity","floor","abs","k"],"mappings":"AAAA,cACC,SAASA,EAAMC,GACQ,gBAAZC,SACRC,OAAOD,QAAUD,IAEO,kBAAXG,SAAyBA,OAAOC,IAC7CD,QAAQ,SAAU,eAAgBH,GAGlCD,EAAKM,MAAQL,KAIfM,KAAM,WAUR,GAAIC,GAAgB,SAASC,EAAKC,GAChC,GAAIC,EAEJ,IAAIF,IAAQC,EACV,MAAOD,EAGT,KAAKE,IAAQD,GACWE,SAAlBF,EAAQC,KACVF,EAAIE,GAAQD,EAAQC,GAIxB,OAAOF,IAmBLI,EAAY,SAASC,EAAOC,GAC9B,GAGIC,GAHAC,EAA2BC,MAAMC,UAAUC,MAAMC,KAAKC,UAAW,GACjEC,KACAC,EAAeV,EAAMW,MAGzB,IAAIP,MAAMC,UAAUO,KAAOZ,EAAMY,MAAQR,MAAMC,UAAUO,IACvDH,EAAeL,MAAMC,UAAUO,IAAIL,KAAKP,EAAO,SAASa,GACtD,GAAIC,GAAkBX,EAAyBG,MAAM,EAGrD,OAFAQ,GAAgBC,OAAO,EAAG,EAAGF,GAEtBZ,EAASe,MAAMvB,KAAMqB,SAI9B,KAAKZ,EAAI,EAAOQ,EAAJR,EAAkBA,IAC5BY,gBAAkBX,EAClBW,gBAAgBC,OAAO,EAAG,EAAGf,EAAME,IACnCO,EAAaQ,KAAKhB,EAASe,MAAMvB,KAAMqB,iBAI3C,OAAOL,IAGLS,EAAa,SAASlB,GACxB,GACIE,GADAiB,IAGJ,KAAKjB,EAAI,EAAGA,EAAIF,EAAMW,OAAQT,IAC5BiB,EAAYA,EAAUC,OAAOpB,EAAME,GAGrC,OAAOiB,IAGLE,EAAkB,SAASC,EAAQC,GACrC,GAAIC,GAAcF,EAAO,GACrBG,EAAeH,EAAO,EAO1B,OALIC,KACFC,EAAcF,EAAO,GACrBG,EAAeH,EAAO,IAGjB,GAAII,QAAOC,KAAKC,OAAOJ,EAAaC,IAGzCI,EAAgB,SAASP,EAAQC,GACnC,GAAIrB,EAEJ,KAAKA,EAAI,EAAGA,EAAIoB,EAAOX,OAAQT,IACvBoB,EAAOpB,YAAcwB,QAAOC,KAAKC,SACjCN,EAAOpB,GAAGS,OAAS,GAA8B,gBAAlBW,GAAOpB,GAAG,GAC3CoB,EAAOpB,GAAK2B,EAAcP,EAAOpB,GAAIqB,GAGrCD,EAAOpB,GAAKmB,EAAgBC,EAAOpB,GAAIqB,GAK7C,OAAOD,IAGLQ,EAAyB,SAAUC,EAAYC,GAC/C,GAAIC,GACAC,EAASH,EAAWI,QAAQ,IAAK,GAOrC,OAJIF,GADA,UAAYxC,OAAQuC,EACVI,EAAE,IAAMF,EAAQF,GAAS,GAEzBK,SAASP,uBAAuBI,GAAQ,IAMtDI,EAAiB,SAASC,EAAIP,GAChC,GAAIC,GACJM,EAAKA,EAAGJ,QAAQ,IAAK,GAQrB,OALEF,GADE,UAAYO,SAAUR,EACdI,EAAE,IAAMG,EAAIP,GAAS,GAErBK,SAASC,eAAeC,IAMlCE,EAAuB,SAAS9C,GAClC,GAAI+C,GAAU,EACVC,EAAS,CAEb,IAAIhD,EAAIiD,aACN,EACEF,IAAW/C,EAAIkD,WACfF,GAAUhD,EAAImD,gBACPnD,EAAMA,EAAIiD,aAGrB,QAAQF,EAASC,IAGfnD,EAAQ,SAAUuD,GAGpB,GAAIC,GAAMX,SAWN7C,EAAQ,SAASyD,GAEnB,GAA+B,gBAAlBT,QAAOd,SAAuBc,OAAOd,OAAOC,KAKvD,MAJ8B,gBAAnBa,QAAOU,SAAwBV,OAAOU,QAAQC,OACvDD,QAAQC,MAAM,0HAGT,YAGT,KAAK1D,KAAM,MAAO,IAAID,GAAMyD,EAE5BA,GAAQG,KAAOH,EAAQG,MAAQ,GAC/BH,EAAQI,QAAUJ,EAAQI,SAAW,SAErC,IAKInD,GALAoD,EAAiB,SAASC,EAAOC,GACnC,MAAiB1D,UAAVyD,EAAsBC,EAAeD,GAG1CE,EAAOhE,KAEPiE,GACE,iBAAkB,iBAAkB,QAAS,WAAY,OACzD,UAAW,YAAa,OAAQ,oBAAqB,qBACrD,SAAU,cAAe,gBAE3BC,GAAwC,YAAa,WAAY,aACjEC,GAAyB,KAAM,MAAO,MAAO,UAAW,QAAS,SAAU,kBAAmB,kBAC9FC,EAAaZ,EAAQa,IAAMb,EAAQc,IACnCC,EAA0Bf,EAAQgB,gBAClCZ,EAAU3B,OAAOC,KAAKuC,UAAUjB,EAAQI,QAAQc,eAChDC,EAAa,GAAI1C,QAAOC,KAAKC,OAAOqB,EAAQoB,IAAKpB,EAAQqB,KACzDC,EAAcjB,EAAeL,EAAQsB,aAAa,GAClDC,EAAiBvB,EAAQuB,iBACvBC,MAAO,UACPC,SAAU,YAEZC,EAAmBH,EAAeC,OAAS,UAC3CG,EAAsBJ,EAAeE,UAAY,WACjDG,EAAavB,EAAeL,EAAQ4B,YAAY,GAChDC,EAAiBxB,EAAeL,EAAQ6B,gBAAgB,GACxDC,EAAezB,EAAeL,EAAQ8B,cAAc,GACpDC,EAAoB1B,EAAeL,EAAQ+B,mBAAmB,GAC9DC,EAAqB3B,EAAe2B,GAAoB,GACxDC,KACAC,GACE/B,KAAM3D,KAAK2D,KACXgC,OAAQhB,EACRiB,UAAWhC,GAEbiC,GACET,WAAYA,EACZN,YAAaA,EACbgB,oBACEd,MAAO/C,OAAOC,KAAK6D,iBAAiBb,GACpCD,SAAUhD,OAAOC,KAAK8D,gBAAgBb,IAExCE,eAAgBA,EAChBC,aAAcA,EACdC,kBAAmBA,EACnBC,mBAAoBA,EAkB1B,IAf6B,gBAAhBhC,GAAU,IAA0C,gBAAjBA,GAAW,IACnDY,EAAW6B,QAAQ,KAAO,GAM1BjG,KAAKqE,GAAKxB,EAAeuB,EAAYZ,EAAQjB,SAE7CvC,KAAKqE,GAAKhC,EAAuBd,MAAMvB,MAAOoE,EAAYZ,EAAQjB,UAGpEvC,KAAKqE,GAAKD,EAGQ,mBAAbpE,MAAO,IAAiC,OAAZA,KAAKqE,GAC1C,KAAM,qBAqER,KAlEAtB,OAAOmD,aAAenD,OAAOmD,iBAC7BnD,OAAOmD,aAAalC,EAAKK,GAAGvB,OAO5B9C,KAAKmG,YAMLnG,KAAKoG,YAMLpG,KAAKqG,UAMLrG,KAAKsG,gBAMLtG,KAAKuG,WAMLvG,KAAKwG,aAMLxG,KAAKyG,UAMLzG,KAAK0G,YACL1G,KAAK2G,WAAa,KAClB3G,KAAK4G,WAAa,KAMlB5G,KAAK2D,KAAOH,EAAQG,KACpB3D,KAAK6G,qBAEL7G,KAAKqE,GAAGW,MAAM8B,MAAQtD,EAAQsD,OAAS9G,KAAKqE,GAAG0C,aAAe/G,KAAKqE,GAAG2C,YACtEhH,KAAKqE,GAAGW,MAAMiC,OAASzD,EAAQyD,QAAUjH,KAAKqE,GAAG6C,cAAgBlH,KAAKqE,GAAG8C,aAEzElF,OAAOC,KAAKkF,cAAgB5D,EAAQ6D,eAE/B5G,EAAI,EAAGA,EAAI0D,EAAsBjD,OAAQT,UACrC+C,GAAQW,EAAsB1D,GASvC,KAN+B,GAA5B+C,EAAQ8D,mBACT5B,EAAmBzF,EAAcyF,EAAkBG,IAGrDJ,EAAcxF,EAAcyF,EAAkBlC,GAEzC/C,EAAI,EAAGA,EAAIwD,EAA8B/C,OAAQT,UAC7CgF,GAAYxB,EAA8BxD,GAGnD,KAAKA,EAAI,EAAGA,EAAIyD,EAAqChD,OAAQT,UACpDgF,GAAYvB,EAAqCzD,GAQ1DT,MAAKmB,IAAM,GAAIc,QAAOC,KAAKqF,IAAIvH,KAAKqE,GAAIoB,GAEpClB,IAMFvE,KAAKwE,gBAAkBD,EAAwBhD,MAAMvB,MAAOA,KAAKmB,MAGnE,IAAIqG,GAAuB,SAASC,EAASC,GAC3C,GAAIC,GAAO,GACPnE,EAAUT,OAAOmD,aAAalC,EAAKK,GAAGvB,IAAI2E,EAE9C,KAAK,GAAIhH,KAAK+C,GACZ,GAAIA,EAAQoE,eAAenH,GAAI,CAC7B,GAAIoH,GAASrE,EAAQ/C,EAErBkH,IAAQ,cAAgBF,EAAU,IAAMhH,EAAI,cAAgBoH,EAAOC,MAAQ,YAI/E,GAAKjF,EAAe,sBAApB,CAEA,GAAIkF,GAAuBlF,EAAe,qBAE1CkF,GAAqBC,UAAYL,CAEjC,IAEIlH,GAFAwH,EAAqBF,EAAqBG,qBAAqB,KAC/DC,EAA2BF,EAAmB/G,MAGlD,KAAKT,EAAI,EAAO0H,EAAJ1H,EAA8BA,IAAK,CAC7C,GAAI2H,GAAoBH,EAAmBxH,GAEvC4H,EAA0B,SAASC,GACrCA,EAAGC,iBAEH/E,EAAQxD,KAAK8C,GAAGJ,QAAQ+E,EAAU,IAAK,KAAKe,OAAOjH,MAAMyC,GAAO0D,IAChE1D,EAAKyE,kBAGPxG,QAAOC,KAAKwG,MAAMC,eAAeP,EAAmB,SACpDnG,OAAOC,KAAKwG,MAAME,mBAAmBR,EAAmB,QAASC,GAAyB,GAG5F,GAAIpD,GAAWjC,EAAqBzB,MAAMvB,MAAOgE,EAAKK,KAClDwE,EAAO5D,EAAS,GAAKyC,EAAEoB,MAAMC,EAAI,GACjCC,EAAM/D,EAAS,GAAKyC,EAAEoB,MAAMG,EAAG,EAEnClB,GAAqB/C,MAAM6D,KAAOA,EAAO,KACzCd,EAAqB/C,MAAMgE,IAAMA,EAAM,MAKzChJ,MAAKkJ,iBAAmB,SAASzB,EAASC,GACxC,GAAgB,WAAZD,EAAsB,CACxBC,EAAEoB,QAEF,IAAIK,GAAU,GAAIlH,QAAOC,KAAKkH,WAC9BD,GAAQE,OAAOrF,EAAK7C,KAEpBgI,EAAQG,KAAO,WACb,GAAIC,GAAaJ,EAAQK,gBACrBvE,EAAWyC,EAAE+B,OAAOC,aAExBhC,GAAEoB,MAAQS,EAAWI,2BAA2B1E,GAEhDuC,EAAqBC,EAASC,QAIhCF,GAAqBC,EAASC,EAGhC,IAAIK,GAAuBlF,EAAe,qBAE1C+G,YAAW,WACT7B,EAAqB/C,MAAM6E,QAAU,SACpC,IAaL7J,KAAK8J,eAAiB,SAAStG,GAC7BT,OAAOmD,aAAalC,EAAKK,GAAGvB,IAAIU,EAAQiE,WAExC,IAAIhH,GACAsJ,EAAKxG,EAAIyG,cAAc,KAE3B,KAAKvJ,IAAK+C,GAAQA,QAChB,GAAIA,EAAQA,QAAQoE,eAAenH,GAAI,CACrC,GAAIoH,GAASrE,EAAQA,QAAQ/C,EAE7BsC,QAAOmD,aAAalC,EAAKK,GAAGvB,IAAIU,EAAQiE,SAASI,EAAOzH,OACtD0H,MAAOD,EAAOC,MACdU,OAAQX,EAAOW,QAKrBuB,EAAGjH,GAAK,qBACRiH,EAAG/E,MAAM6E,QAAU,OACnBE,EAAG/E,MAAMC,SAAW,WACpB8E,EAAG/E,MAAMiF,SAAW,QACpBF,EAAG/E,MAAMkF,WAAa,QACtBH,EAAG/E,MAAMmF,UAAY,OACrBJ,EAAG/E,MAAMoF,QAAU,MACnBL,EAAG/E,MAAMqF,UAAY,mBAEhBxH,EAAe,uBAClBU,EAAI+G,KAAKC,YAAYR,EAGvB,IAAIhC,GAAuBlF,EAAe,qBAE1CZ,QAAOC,KAAKwG,MAAM8B,eAAezC,EAAsB,WAAY,SAASO,GACrEA,EAAGmC,eAAkBzK,KAAK0K,SAASpC,EAAGmC,gBACzC1H,OAAO6G,WAAW,WAChB7B,EAAqB/C,MAAM6E,QAAU,QACpC,OAEJ,IAML7J,KAAKyI,gBAAkB,WACrB,GAAIV,GAAuBlF,EAAe,qBAEtCkF,KACFA,EAAqB/C,MAAM6E,QAAU,QAIzC,IAAIc,GAAgB,SAASC,EAAQxK,GACnC6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAASsH,GAC1CrH,QAALqH,IACFA,EAAI1H,MAGNwD,EAAQpD,GAAMmB,MAAMvB,MAAO0H,IAE3B1D,EAAKyE,oBAKTxG,QAAOC,KAAKwG,MAAMmC,YAAY7K,KAAKmB,IAAK,eAAgBnB,KAAKyI,gBAE7D,KAAK,GAAIH,GAAK,EAAGA,EAAKrE,EAA8B/C,OAAQoH,IAAM,CAChE,GAAIlI,GAAO6D,EAA8BqE,EAErClI,KAAQoD,IACVmH,EAAc3K,KAAKmB,IAAKf,GAI5B,IAAK,GAAIkI,GAAK,EAAGA,EAAKpE,EAAqChD,OAAQoH,IAAM,CACvE,GAAIlI,GAAO8D,EAAqCoE,EAE5ClI,KAAQoD,IACVmH,EAAc3K,KAAKmB,IAAKf,GAI5B6B,OAAOC,KAAKwG,MAAMmC,YAAY7K,KAAKmB,IAAK,aAAc,SAASuG,GACzDlE,EAAQsH,YACVtH,EAAQsH,WAAWvJ,MAAMvB,MAAO0H,IAGWrH,QAA1C0C,OAAOmD,aAAalC,EAAKK,GAAGvB,IAAS,KACtCkB,EAAKkF,iBAAiB,MAAOxB,KAOjC1H,KAAK+K,QAAU,WACb9I,OAAOC,KAAKwG,MAAMsC,QAAQhL,KAAKmB,IAAK,WAMtCnB,KAAKiL,QAAU,WACb,GAEIxK,GAFAyK,KACAC,EAAiBnL,KAAKuG,QAAQrF,MAGlC,KAAKT,EAAI,EAAO0K,EAAJ1K,EAAoBA,IACS,iBAA7BT,MAAKuG,QAAQ9F,GAAU,SAAmBT,KAAKuG,QAAQ9F,GAAG2K,SAClEF,EAAQ1J,KAAKxB,KAAKuG,QAAQ9F,GAAGiJ,cAIjC1J,MAAKqL,gBAAgBH,IAQvBlL,KAAKqL,gBAAkB,SAASH,GAC9B,GAEIzK,GAFA6K,EAAQJ,EAAQhK,OAChBqK,EAAS,GAAItJ,QAAOC,KAAKsJ,YAG7B,KAAI/K,EAAI,EAAO6K,EAAJ7K,EAAWA,IACpB8K,EAAOE,OAAOP,EAAQzK,GAGxBT,MAAKmB,IAAIuK,UAAUH,IAUrBvL,KAAK2L,UAAY,SAAS/G,EAAKC,EAAKrE,GAClCR,KAAKmB,IAAIyK,MAAM,GAAI3J,QAAOC,KAAKC,OAAOyC,EAAKC,IAEvCrE,GACFA,KASJR,KAAK6L,WAAa,WAChB,MAAO7L,MAAKqE,IAQdrE,KAAK8L,OAAS,SAAShI,GACrBA,EAAQA,GAAS,EAEjB9D,KAAK2D,KAAO3D,KAAKmB,IAAI4K,UAAYjI,EACjC9D,KAAKmB,IAAI6K,QAAQhM,KAAK2D,OAQxB3D,KAAKiM,QAAU,SAASnI,GACtBA,EAAQA,GAAS,EAEjB9D,KAAK2D,KAAO3D,KAAKmB,IAAI4K,UAAYjI,EACjC9D,KAAKmB,IAAI6K,QAAQhM,KAAK2D,MAGxB,IACIuI,GADAC,IAGJ,KAAKD,IAAUlM,MAAKmB,IACc,kBAArBnB,MAAKmB,IAAI+K,IAA2BlM,KAAKkM,IAClDC,EAAe3K,KAAK0K,EAIxB,KAAKzL,EAAI,EAAGA,EAAI0L,EAAejL,OAAQT,KACrC,SAAU2L,EAAOC,EAAOC,GACtBF,EAAME,GAAe,WACnB,MAAOD,GAAMC,GAAa/K,MAAM8K,EAAOtL,aAExCf,KAAMA,KAAKmB,IAAKgL,EAAe1L,IAItC,OAAOV,IACNC,KAEHD,GAAMa,UAAU2L,cAAgB,SAAS/I,GACvC,GAAIiE,GAAU7E,SAASoH,cAAc,MAErCvC,GAAQzC,MAAMwH,OAAS,UAEnBhJ,EAAQiJ,wBAAyB,IACnChF,EAAQzC,MAAM0H,WAAa,4BAC3BjF,EAAQzC,MAAM2H,SAAW,OACzBlF,EAAQzC,MAAMqF,UAAY,2CAG5B,KAAK,GAAIxC,KAAUrE,GAAQwB,MACzByC,EAAQzC,MAAM6C,GAAUrE,EAAQwB,MAAM6C,EAGpCrE,GAAQV,KACV2E,EAAQ3E,GAAKU,EAAQV,IAGnBU,EAAQsE,QACVL,EAAQK,MAAQtE,EAAQsE,OAGtBtE,EAAQoJ,UACVnF,EAAQoF,UAAYrJ,EAAQoJ,SAG1BpJ,EAAQsJ,UACqB,gBAApBtJ,GAAQsJ,QACjBrF,EAAQO,UAAYxE,EAAQsJ,QAErBtJ,EAAQsJ,kBAAmBC,cAClCtF,EAAQ8C,YAAY/G,EAAQsJ,UAI5BtJ,EAAQyB,WACVwC,EAAQxC,SAAWhD,OAAOC,KAAK8D,gBAAgBxC,EAAQyB,SAASP,eAGlE,KAAK,GAAI4D,KAAM9E,GAAQwJ,QACrB,SAAUpC,EAAQxK,GAChB6B,OAAOC,KAAKwG,MAAM8B,eAAeI,EAAQxK,EAAM,WAC7CoD,EAAQwJ,OAAO5M,GAAMmB,MAAMvB,MAAOA,UAEnCyH,EAASa,EAKd,OAFAb,GAAQwF,MAAQ,EAETxF,GAgBT1H,EAAMa,UAAUsM,WAAa,SAAS1J,GACpC,GAAIiE,GAAUzH,KAAKuM,cAAc/I,EAKjC,OAHAxD,MAAKmG,SAAS3E,KAAKiG,GACnBzH,KAAKmB,IAAIgF,SAASsB,EAAQxC,UAAUzD,KAAKiG,GAElCA,GAST1H,EAAMa,UAAUuM,cAAgB,SAAS1F,GACvC,GACIhH,GADAwE,EAAW,IAGf,KAAKxE,EAAI,EAAGA,EAAIT,KAAKmG,SAASjF,OAAQT,IAChCT,KAAKmG,SAAS1F,IAAMgH,IACtBxC,EAAWjF,KAAKmG,SAAS1F,GAAGwE,SAC5BjF,KAAKmG,SAAS7E,OAAOb,EAAG,GAI5B,IAAIwE,EACF,IAAKxE,EAAI,EAAGA,EAAIT,KAAKmB,IAAIgF,SAASjF,OAAQT,IAAK,CAC7C,GAAI2M,GAAsBpN,KAAKmB,IAAIgF,SAASsB,EAAQxC,SAEpD,IAAImI,EAAoBC,MAAM5M,IAAMgH,EAAS,CAC3C2F,EAAoBE,SAAS7M,EAE7B,QAKN,MAAOgH,IAGT1H,EAAMa,UAAU2M,aAAe,SAAS/J,GACtC,GAAmBnD,QAAfmD,EAAQoB,KAAmCvE,QAAfmD,EAAQqB,KAAwCxE,QAApBmD,EAAQyB,SAClE,KAAM,mCAGR,IAAIjB,GAAOhE,KACPwN,EAAUhK,EAAQgK,QAClBC,EAASjK,EAAQiK,OACjBC,EAAUlK,EAAQkK,QAClBC,GACE1I,SAAU,GAAIhD,QAAOC,KAAKC,OAAOqB,EAAQoB,IAAKpB,EAAQqB,KACtD1D,IAAK,MAEPyM,EAAiB3N,EAAc0N,EAAcnK,SAE1CoK,GAAehJ,UACfgJ,GAAe/I,UACf+I,GAAeH,aACfG,GAAeF,OAEtB,IAAIjE,GAAS,GAAIxH,QAAOC,KAAK2L,OAAOD,EAIpC,IAFAnE,EAAOgE,OAASA,EAEZjK,EAAQmD,WAAY,CACtB8C,EAAO9C,WAAa,GAAI1E,QAAOC,KAAK4L,WAAWtK,EAAQmD,WAIvD,KAAK,GAFDoH,IAAsB,aAAc,kBAAmB,WAAY,mBAAoB,kBAElFzF,EAAK,EAAGA,EAAKyF,EAAmB7M,OAAQoH,KAC/C,SAAUsC,EAAQxK,GACZoD,EAAQmD,WAAWvG,IACrB6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAASsH,GACnDlE,EAAQmD,WAAWvG,GAAMmB,MAAMvB,MAAO0H,OAGzC+B,EAAO9C,WAAYoH,EAAmBzF,IAQ7C,IAAK,GAJD0F,IAAiB,oBAAqB,oBAAqB,iBAAkB,oBAAqB,eAAgB,eAAgB,mBAAoB,iBAAkB,gBAAiB,gBAAiB,kBAAmB,kBAE7NC,GAA4B,WAAY,OAAQ,UAAW,YAAa,YAAa,WAAY,YAAa,WAEzG3F,EAAK,EAAGA,EAAK0F,EAAc9M,OAAQoH,KAC1C,SAAUsC,EAAQxK,GACZoD,EAAQpD,IACV6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,WAC1CoD,EAAQpD,GAAMmB,MAAMvB,MAAOA,UAG9ByJ,EAAQuE,EAAc1F,GAG3B,KAAK,GAAIA,GAAK,EAAGA,EAAK2F,EAAyB/M,OAAQoH,KACrD,SAAUnH,EAAKyJ,EAAQxK,GACjBoD,EAAQpD,IACV6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAAS8N,GAC/CA,EAAGpF,QACLoF,EAAGpF,MAAQ3H,EAAIqI,gBAAgB2E,kBAAkBD,EAAGE,SAGtD5K,EAAQpD,GAAMmB,MAAMvB,MAAOkO,OAG9BlO,KAAKmB,IAAKsI,EAAQwE,EAAyB3F,GAoChD,OAjCArG,QAAOC,KAAKwG,MAAMmC,YAAYpB,EAAQ,QAAS,WAC7CzJ,KAAKwN,QAAUA,EAEXhK,EAAQ6K,OACV7K,EAAQ6K,MAAM9M,MAAMvB,MAAOA,OAGzByJ,EAAO9C,aACT3C,EAAKsK,kBACL7E,EAAO9C,WAAW4H,KAAKvK,EAAK7C,IAAKsI,MAIrCxH,OAAOC,KAAKwG,MAAMmC,YAAYpB,EAAQ,aAAc,SAAS/B,GAC3DA,EAAE+B,OAASzJ,KAEPwD,EAAQsH,YACVtH,EAAQsH,WAAWvJ,MAAMvB,MAAO0H,IAGerH,QAA7C0C,OAAOmD,aAAalC,EAAKK,GAAGvB,IAAY,QAC1CkB,EAAKkF,iBAAiB,SAAUxB,KAIhC+B,EAAOgE,QACTxL,OAAOC,KAAKwG,MAAMmC,YAAYpB,EAAQ,UAAW,WAC/CzF,EAAKwK,oBAAoB/E,EAAQ,SAASgF,EAAGC,GAC3ChB,EAAQe,EAAGC,OAKVjF,GAGT1J,EAAMa,UAAU+N,UAAY,SAASnL,GACnC,GAAIiG,EACJ,IAAGjG,EAAQoE,eAAe,iBAExB6B,EAASjG,MAEN,CACH,KAAKA,EAAQoE,eAAe,QAAUpE,EAAQoE,eAAe,QAAWpE,EAAQyB,UAI9E,KAAM,mCAHNwE,GAASzJ,KAAKuN,aAAa/J,GAiB/B,MAVAiG,GAAOJ,OAAOrJ,KAAKmB,KAEhBnB,KAAKwE,iBACNxE,KAAKwE,gBAAgBmK,UAAUlF,GAGjCzJ,KAAKuG,QAAQ/E,KAAKiI,GAElB1J,EAAM6O,KAAK,eAAgBnF,EAAQzJ,MAE5ByJ,GAGT1J,EAAMa,UAAUiO,WAAa,SAAStO,GACpC,IAAK,GAAWkJ,GAAPhJ,EAAI,EAAWgJ,EAAOlJ,EAAME,GAAIA,IACvCT,KAAK2O,UAAUlF,EAGjB,OAAOzJ,MAAKuG,SAGdxG,EAAMa,UAAU0N,gBAAkB,WAChC,IAAK,GAAW7E,GAAPhJ,EAAI,EAAWgJ,EAASzJ,KAAKuG,QAAQ9F,GAAIA,IAC5CgJ,EAAO9C,YACT8C,EAAO9C,WAAWmI,SAKxB/O,EAAMa,UAAUmO,aAAe,SAAStF,GACtC,IAAK,GAAIhJ,GAAI,EAAGA,EAAIT,KAAKuG,QAAQrF,OAAQT,IACvC,GAAIT,KAAKuG,QAAQ9F,KAAOgJ,EAAQ,CAC9BzJ,KAAKuG,QAAQ9F,GAAG4I,OAAO,MACvBrJ,KAAKuG,QAAQjF,OAAOb,EAAG,GAEpBT,KAAKwE,iBACNxE,KAAKwE,gBAAgBuK,aAAatF,GAGpC1J,EAAM6O,KAAK,iBAAkBnF,EAAQzJ,KAErC,OAIJ,MAAOyJ,IAGT1J,EAAMa,UAAUoO,cAAgB,SAAUC,GACxC,GAAIC,KAEJ,IAAyB,mBAAdD,GAA2B,CACpC,IAAK,GAAIxO,GAAI,EAAGA,EAAIT,KAAKuG,QAAQrF,OAAQT,IAAK,CAC5C,GAAIgJ,GAASzJ,KAAKuG,QAAQ9F,EAC1BgJ,GAAOJ,OAAO,MAEdtJ,EAAM6O,KAAK,iBAAkBnF,EAAQzJ,MAGpCA,KAAKwE,iBAAmBxE,KAAKwE,gBAAgB2K,cAC9CnP,KAAKwE,gBAAgB2K,eAGvBnP,KAAKuG,QAAU2I,MAEZ,CACH,IAAK,GAAIzO,GAAI,EAAGA,EAAIwO,EAAW/N,OAAQT,IAAK,CAC1C,GAAIwM,GAAQjN,KAAKuG,QAAQN,QAAQgJ,EAAWxO,GAE5C,IAAIwM,EAAQ,GAAI,CACd,GAAIxD,GAASzJ,KAAKuG,QAAQ0G,EAC1BxD,GAAOJ,OAAO,MAEXrJ,KAAKwE,iBACNxE,KAAKwE,gBAAgBuK,aAAatF,GAGpC1J,EAAM6O,KAAK,iBAAkBnF,EAAQzJ,OAIzC,IAAK,GAAIS,GAAI,EAAGA,EAAIT,KAAKuG,QAAQrF,OAAQT,IAAK,CAC5C,GAAIgJ,GAASzJ,KAAKuG,QAAQ9F,EACH,OAAnBgJ,EAAO2F,UACTF,EAAY1N,KAAKiI,GAIrBzJ,KAAKuG,QAAU2I,IAInBnP,EAAMa,UAAUyO,YAAc,SAAS7L,GACrC,GAAI2F,GAAU,GAAIlH,QAAOC,KAAKkH,YAC1BkG,GAAY,CA+GhB,OA7GAnG,GAAQE,OAAOrJ,KAAKmB,KAEK,MAArBqC,EAAQ8L,YACVA,EAAY9L,EAAQ8L,WAGtBnG,EAAQoG,MAAQ,WACd,GAAIlL,GAAKzB,SAASoH,cAAc,MAEhC3F,GAAGW,MAAMwK,YAAc,OACvBnL,EAAGW,MAAMyK,YAAc,MACvBpL,EAAGW,MAAMC,SAAW,WACpBZ,EAAGW,MAAM0K,OAAS,IAClBrL,EAAG2D,UAAYxE,EAAQsJ,QAEvB3D,EAAQ9E,GAAKA,EAERb,EAAQmM,QACXnM,EAAQmM,MAAQ,eAGlB,IAAIC,GAAQ5P,KAAK6P,WACbC,EAAeF,EAAMpM,EAAQmM,OAC7BI,GAAuB,cAAe,iBAAkB,WAAY,YAExED,GAAavF,YAAYlG,EAEzB,KAAK,GAAIiE,GAAK,EAAGA,EAAKyH,EAAoB7O,OAAQoH,KAChD,SAAUsC,EAAQxK,GAChB6B,OAAOC,KAAKwG,MAAM8B,eAAeI,EAAQxK,EAAM,SAASsH,GACG,IAArDsI,UAAUC,UAAUC,cAAcjK,QAAQ,SAAiBrD,SAASuN,KACtEzI,EAAE0I,cAAe,EACjB1I,EAAE2I,aAAc,GAGhB3I,EAAE4I,qBAGLjM,EAAI0L,EAAoBzH,GAGzB9E,GAAQ6K,QACVuB,EAAMW,mBAAmBhG,YAAYpB,EAAQ9E,IAC7CpC,OAAOC,KAAKwG,MAAM8B,eAAerB,EAAQ9E,GAAI,QAAS,WACpDb,EAAQ6K,MAAM9M,MAAM4H,GAAUA,OAIlClH,OAAOC,KAAKwG,MAAMsC,QAAQhL,KAAM,UAGlCmJ,EAAQG,KAAO,WACb,GAAIC,GAAavJ,KAAKwJ,gBAClBV,EAAQS,EAAWiH,qBAAqB,GAAIvO,QAAOC,KAAKC,OAAOqB,EAAQoB,IAAKpB,EAAQqB,KAExFrB,GAAQiN,iBAAmBjN,EAAQiN,kBAAoB,EACvDjN,EAAQkN,eAAiBlN,EAAQkN,gBAAkB,CAEnD,IAAIrM,GAAK8E,EAAQ9E,GACbyI,EAAUzI,EAAGsM,SAAS,GACtBC,EAAiB9D,EAAQ+D,aACzBC,EAAgBhE,EAAQiE,WAE5B,QAAQvN,EAAQwN,eACd,IAAK,MACH3M,EAAGW,MAAMgE,IAAOF,EAAMG,EAAI2H,EAAiBpN,EAAQkN,eAAkB,IACrE,MACF,SACA,IAAK,SACHrM,EAAGW,MAAMgE,IAAOF,EAAMG,EAAK2H,EAAiB,EAAKpN,EAAQkN,eAAkB,IAC3E,MACF,KAAK,SACHrM,EAAGW,MAAMgE,IAAOF,EAAMG,EAAIzF,EAAQkN,eAAkB,KAIxD,OAAQlN,EAAQyN,iBACd,IAAK,OACH5M,EAAGW,MAAM6D,KAAQC,EAAMC,EAAI+H,EAAgBtN,EAAQiN,iBAAoB,IACvE,MACF,SACA,IAAK,SACHpM,EAAGW,MAAM6D,KAAQC,EAAMC,EAAK+H,EAAgB,EAAKtN,EAAQiN,iBAAoB,IAC7E,MACF,KAAK,QACHpM,EAAGW,MAAM6D,KAAQC,EAAMC,EAAIvF,EAAQiN,iBAAoB,KAI3DpM,EAAGW,MAAM6E,QAAUyF,EAAY,QAAU,OAEpCA,GACH9L,EAAQ0N,KAAK3P,MAAMvB,MAAOqE,KAI9B8E,EAAQgI,SAAW,WACjB,GAAI9M,GAAK8E,EAAQ9E,EAEbb,GAAQ4N,OACV5N,EAAQ4N,OAAO7P,MAAMvB,MAAOqE,KAG5B8E,EAAQ9E,GAAGgN,WAAWC,YAAYnI,EAAQ9E,IAC1C8E,EAAQ9E,GAAK,OAIjBrE,KAAKoG,SAAS5E,KAAK2H,GACZA,GAGTpJ,EAAMa,UAAU2Q,cAAgB,SAASpI,GACvC,IAAK,GAAI1I,GAAI,EAAGA,EAAIT,KAAKoG,SAASlF,OAAQT,IACxC,GAAIT,KAAKoG,SAAS3F,KAAO0I,EAAS,CAChCnJ,KAAKoG,SAAS3F,GAAG4I,OAAO,MACxBrJ,KAAKoG,SAAS9E,OAAOb,EAAG,EAExB,SAKNV,EAAMa,UAAU4Q,eAAiB,WAC/B,IAAK,GAAWpQ,GAAPX,EAAI,EAASW,EAAOpB,KAAKoG,SAAS3F,GAAIA,IAC7CW,EAAKiI,OAAO,KAGdrJ,MAAKoG,aAGPrG,EAAMa,UAAU6Q,aAAe,SAASjO,GACtC,GAAIkO,MACAC,EAASnO,EAAQkO,IAErB,IAAIC,EAAOzQ,OACT,GAAqBb,SAAjBsR,EAAO,GAAG,GACZD,EAAOC,MAGP,KAAK,GAAWC,GAAPnR,EAAI,EAAWmR,EAASD,EAAOlR,GAAIA,IAC1CiR,EAAKlQ,KAAK,GAAIS,QAAOC,KAAKC,OAAOyP,EAAO,GAAIA,EAAO,IAKzD,IAAIC,IACF1Q,IAAKnB,KAAKmB,IACVuQ,KAAMA,EACNI,YAAatO,EAAQsO,YACrBC,cAAevO,EAAQuO,cACvBC,aAAcxO,EAAQwO,aACtBC,SAAUzO,EAAQyO,SAClBC,WAAW,EACXC,UAAU,EACV/G,SAAS,EAGP5H,GAAQoE,eAAe,eACzBiK,EAAiBK,UAAY1O,EAAQ0O,WAGnC1O,EAAQoE,eAAe,cACzBiK,EAAiBM,SAAW3O,EAAQ2O,UAGlC3O,EAAQoE,eAAe,WACzBiK,EAAiBO,MAAQ5O,EAAQ4O,OAG/B5O,EAAQoE,eAAe,YACzBiK,EAAiBnC,OAASlM,EAAQkM,OAOpC,KAAK,GAJD2C,GAAW,GAAIpQ,QAAOC,KAAKoQ,SAAST,GAEpCU,GAAmB,QAAS,WAAY,YAAa,YAAa,WAAY,YAAa,UAAW,cAEjGjK,EAAK,EAAGA,EAAKiK,EAAgBrR,OAAQoH,KAC5C,SAAUsC,EAAQxK,GACZoD,EAAQpD,IACV6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAASsH,GACnDlE,EAAQpD,GAAMmB,MAAMvB,MAAO0H,OAG9B2K,EAAUE,EAAgBjK,GAO/B,OAJAtI,MAAKwG,UAAUhF,KAAK6Q,GAEpBtS,EAAM6O,KAAK,iBAAkByD,EAAUrS,MAEhCqS,GAGTtS,EAAMa,UAAU4R,eAAiB,SAASH,GACxC,IAAK,GAAI5R,GAAI,EAAGA,EAAIT,KAAKwG,UAAUtF,OAAQT,IACzC,GAAIT,KAAKwG,UAAU/F,KAAO4R,EAAU,CAClCrS,KAAKwG,UAAU/F,GAAG4I,OAAO,MACzBrJ,KAAKwG,UAAUlF,OAAOb,EAAG,GAEzBV,EAAM6O,KAAK,mBAAoByD,EAAUrS,KAEzC,SAKND,EAAMa,UAAU6R,gBAAkB,WAChC,IAAK,GAAWrR,GAAPX,EAAI,EAASW,EAAOpB,KAAKwG,UAAU/F,GAAIA,IAC9CW,EAAKiI,OAAO,KAGdrJ,MAAKwG,cAGPzG,EAAMa,UAAU8R,WAAa,SAASlP,GACpCA,EAAWvD,GACTkB,IAAKnB,KAAKmB,IACVwE,OAAQ,GAAI1D,QAAOC,KAAKC,OAAOqB,EAAQoB,IAAKpB,EAAQqB,MACnDrB,SAEIA,GAAQoB,UACRpB,GAAQqB,GAKf,KAAK,GAHD8N,GAAU,GAAI1Q,QAAOC,KAAK0Q,OAAOpP,GACjCqP,GAAkB,QAAS,WAAY,YAAa,YAAa,WAAY,YAAa,UAAW,cAEhGvK,EAAK,EAAGA,EAAKuK,EAAe3R,OAAQoH,KAC3C,SAAUsC,EAAQxK,GACZoD,EAAQpD,IACV6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAASsH,GACnDlE,EAAQpD,GAAMmB,MAAMvB,MAAO0H,OAG9BiL,EAASE,EAAevK,GAK7B,OAFAtI,MAAK0G,SAASlF,KAAKmR,GAEZA,GAGT5S,EAAMa,UAAUkS,cAAgB,SAAStP,GACvCA,EAAUvD,GACRkB,IAAKnB,KAAKmB,KACTqC,EAEH,IAAIuP,GAAe,GAAI9Q,QAAOC,KAAKsJ,aACjC,GAAIvJ,QAAOC,KAAKC,OAAOqB,EAAQ+H,OAAO,GAAG,GAAI/H,EAAQ+H,OAAO,GAAG,IAC/D,GAAItJ,QAAOC,KAAKC,OAAOqB,EAAQ+H,OAAO,GAAG,GAAI/H,EAAQ+H,OAAO,GAAG,IAGjE/H,GAAQ+H,OAASwH,CAKjB,KAAK,GAHDJ,GAAU,GAAI1Q,QAAOC,KAAK8Q,UAAUxP,GACpCqP,GAAkB,QAAS,WAAY,YAAa,YAAa,WAAY,YAAa,UAAW,cAEhGvK,EAAK,EAAGA,EAAKuK,EAAe3R,OAAQoH,KAC3C,SAAUsC,EAAQxK,GACZoD,EAAQpD,IACV6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAASsH,GACnDlE,EAAQpD,GAAMmB,MAAMvB,MAAO0H,OAG9BiL,EAASE,EAAevK,GAK7B,OAFAtI,MAAK0G,SAASlF,KAAKmR,GAEZA,GAGT5S,EAAMa,UAAUqS,YAAc,SAASzP,GACrC,GAAI1B,IAAa,CAEd0B,GAAQoE,eAAe,gBACxB9F,EAAa0B,EAAQ1B,kBAGhB0B,GAAQ1B,WAEf0B,EAAUvD,GACRkB,IAAKnB,KAAKmB,KACTqC,GAEe,GAAd1B,IACF0B,EAAQ0P,OAAS1P,EAAQ0P,MAAMrS,MAAM,KAGnC2C,EAAQ0P,MAAMhS,OAAS,GACrBsC,EAAQ0P,MAAM,GAAGhS,OAAS,IAC5BsC,EAAQ0P,MAAQzR,EAAWnB,EAAUkD,EAAQ0P,MAAO9Q,EAAeN,IAOvE,KAAK,GAHD6Q,GAAU,GAAI1Q,QAAOC,KAAKiR,QAAQ3P,GAClCqP,GAAkB,QAAS,WAAY,YAAa,YAAa,WAAY,YAAa,UAAW,cAEhGvK,EAAK,EAAGA,EAAKuK,EAAe3R,OAAQoH,KAC3C,SAAUsC,EAAQxK,GACZoD,EAAQpD,IACV6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAASsH,GACnDlE,EAAQpD,GAAMmB,MAAMvB,MAAO0H,OAG9BiL,EAASE,EAAevK,GAO7B,OAJAtI,MAAK0G,SAASlF,KAAKmR,GAEnB5S,EAAM6O,KAAK,gBAAiB+D,EAAS3S,MAE9B2S,GAGT5S,EAAMa,UAAUwS,cAAgB,SAAST,GACvC,IAAK,GAAIlS,GAAI,EAAGA,EAAIT,KAAK0G,SAASxF,OAAQT,IACxC,GAAIT,KAAK0G,SAASjG,KAAOkS,EAAS,CAChC3S,KAAK0G,SAASjG,GAAG4I,OAAO,MACxBrJ,KAAK0G,SAASpF,OAAOb,EAAG,GAExBV,EAAM6O,KAAK,kBAAmB+D,EAAS3S,KAEvC,SAKND,EAAMa,UAAUyS,eAAiB,WAC/B,IAAK,GAAWjS,GAAPX,EAAI,EAASW,EAAOpB,KAAK0G,SAASjG,GAAIA,IAC7CW,EAAKiI,OAAO,KAGdrJ,MAAK0G,aAGP3G,EAAMa,UAAU0S,oBAAsB,SAAS9P,GAC7C,GAAIwJ,GAASxJ,EAAQwJ,aAEdxJ,GAAQwJ,MAEf,IAAIuG,GAAwB/P,EACxBmM,EAAQ,GAAI1N,QAAOC,KAAKsR,kBAAkBD,EAE9C,KAAK,GAAIjL,KAAM0E,IACb,SAAUpC,EAAQxK,GAChB6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAASsH,GACnDsF,EAAO5M,GAAMmB,MAAMvB,MAAO0H,OAE3BiI,EAAOrH,EAKZ,OAFAtI,MAAKqG,OAAO7E,KAAKmO,GAEVA,GAGT5P,EAAMa,UAAU6S,qBAAuB,SAASjQ,GAC9C,GAAImM,GAAQ3P,KAAKsT,oBAAoB9P,EAGrC,OAFAmM,GAAMtG,OAAOrJ,KAAKmB,KAEXwO,GAGT5P,EAAMa,UAAU8S,WAAa,SAASlQ,GACpC,GAAImQ,GAAMnQ,EAAQmQ,IACd3G,EAASxJ,EAAQwJ,aAEdxJ,GAAQmQ,UACRnQ,GAAQwJ,MAEf,IAAI4G,GAAcpQ,EACdmM,EAAQ,GAAI1N,QAAOC,KAAK2R,SAASF,EAAKC,EAE1C,KAAK,GAAItL,KAAM0E,IACb,SAAUpC,EAAQxK,GAChB6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAASsH,GACnDsF,EAAO5M,GAAMmB,MAAMvB,MAAO0H,OAE3BiI,EAAOrH,EAKZ,OAFAtI,MAAKqG,OAAO7E,KAAKmO,GAEVA,GAGT5P,EAAMa,UAAUkT,YAAc,SAAStQ,GACrC,GAAImM,GAAQ3P,KAAK0T,WAAWlQ,EAG5B,OAFAmM,GAAMtG,OAAOrJ,KAAKmB,KAEXwO,GAGT5P,EAAMa,UAAUmT,SAAW,SAASC,EAAWxQ,GAE7CA,EAAUA,KACV,IAAImM,EAEJ,QAAOqE,GACL,IAAK,UAAWhU,KAAKsG,aAAa2N,QAAUtE,EAAQ,GAAI1N,QAAOC,KAAK+R,QAAQC,YAC1E,MACF,KAAK,SAAUlU,KAAKsG,aAAa6N,OAASxE,EAAQ,GAAI1N,QAAOC,KAAK+R,QAAQG,UACxE,MACF,KAAK,UAAWpU,KAAKsG,aAAa+N,QAAU1E,EAAQ,GAAI1N,QAAOC,KAAKoS,YAClE,MACF,KAAK,UAAWtU,KAAKsG,aAAaiO,QAAU5E,EAAQ,GAAI1N,QAAOC,KAAKsS,YAClE,MACF,KAAK,YAAaxU,KAAKsG,aAAamO,UAAY9E,EAAQ,GAAI1N,QAAOC,KAAKwS,cACtE,MACF,KAAK,YACD1U,KAAKsG,aAAaqO,UAAYhF,EAAQ,GAAI1N,QAAOC,KAAKyS,UAAUC,eAChEjF,EAAMkF,OAAOrR,EAAQsR,cACdtR,GAAQsR,OAGXtR,EAAQ6K,OACVpM,OAAOC,KAAKwG,MAAMmC,YAAY8E,EAAO,QAAS,SAASjH,GACrDlF,EAAQ6K,MAAM3F,SACPlF,GAAQ6K,OAGrB,MACA,KAAK,SAIH,GAHArO,KAAKsG,aAAayO,OAASpF,EAAQ,GAAI1N,QAAOC,KAAK6S,OAAOC,cAAchV,KAAKmB,KAGzEqC,EAAQyR,QAAUzR,EAAQ0R,cAAgB1R,EAAQ2R,YAAa,CACjE,GAAIC,IACF7J,OAAS/H,EAAQ+H,QAAU,KAC3B8J,QAAU7R,EAAQ6R,SAAW,KAC7BC,SAAW9R,EAAQ8R,UAAY,KAC/BlV,KAAOoD,EAAQpD,MAAQ,KACvBmV,OAAS/R,EAAQ+R,QAAU,KAC3BC,OAAShS,EAAQgS,QAAU,KAC3BC,MAAQjS,EAAQiS,OAAS,KAGvBjS,GAAQ2R,aACVxF,EAAMwF,YAAYC,EAAoB5R,EAAQ2R,aAG5C3R,EAAQyR,QACVtF,EAAMsF,OAAOG,EAAoB5R,EAAQyR,QAGvCzR,EAAQ0R,cACVvF,EAAMuF,aAAaE,EAAoB5R,EAAQ0R,cAKnD,GAAI1R,EAAQkS,WAAY,CACtB,GAAIC,IACFpK,OAAS/H,EAAQ+H,QAAU,KAC3B+J,SAAW9R,EAAQ8R,UAAY,KAC/BM,MAAQpS,EAAQoS,OAAS,KACzBL,OAAS/R,EAAQ+R,QAAU,KAG7B5F,GAAM+F,WAAWC,EAAmBnS,EAAQkS,aAKpD,MAAcrV,UAAVsP,GAC6B,kBAApBA,GAAMkG,YACflG,EAAMkG,WAAWrS,GAEQ,kBAAhBmM,GAAMtG,QACfsG,EAAMtG,OAAOrJ,KAAKmB,KAGbwO,GART,QAYF5P,EAAMa,UAAUkV,YAAc,SAASnG,GACrC,GAAqB,gBAAX,IAAoDtP,SAA7BL,KAAKsG,aAAaqJ,GAChD3P,KAAKsG,aAAaqJ,GAAOtG,OAAO,YAEzBrJ,MAAKsG,aAAaqJ,OAG1B,KAAK,GAAIlP,GAAI,EAAGA,EAAIT,KAAKqG,OAAOnF,OAAQT,IACtC,GAAIT,KAAKqG,OAAO5F,KAAOkP,EAAO,CAC5B3P,KAAKqG,OAAO5F,GAAG4I,OAAO,MACtBrJ,KAAKqG,OAAO/E,OAAOb,EAAG,EAEtB,QAMR,IAAIsV,GAAYC,CAm7BhB,OAj7BAjW,GAAMa,UAAUqV,UAAY,SAASzS,GACnC,OAAQA,EAAQuS,YACd,IAAK,YACHA,EAAa9T,OAAOC,KAAKgU,WAAWC,SACpC,MACF,KAAK,UACHJ,EAAa9T,OAAOC,KAAKgU,WAAWE,OACpC,MACF,KAAK,UACHL,EAAa9T,OAAOC,KAAKgU,WAAWG,OACpC,MACF,SACEN,EAAa9T,OAAOC,KAAKgU,WAAWI,QAKtCN,EADyB,aAAvBxS,EAAQwS,WACG/T,OAAOC,KAAKqU,WAAWC,SAGvBvU,OAAOC,KAAKqU,WAAWE,MAGtC,IAAI9I,IACE+I,eAAe,EACfC,YAAY,EACZC,mBAAmB,EACnBC,cAEFC,EAAmB7W,EAAc0N,EAAcnK,EAEnDsT,GAAgBC,OAAS,SAASC,WAAYxT,GAAQuT,QAAUvT,EAAQuT,OAAS,GAAI9U,QAAOC,KAAKC,OAAOqB,EAAQuT,OAAO,GAAIvT,EAAQuT,OAAO,IAC1ID,EAAgBG,YAAc,SAASD,WAAYxT,GAAQyT,aAAezT,EAAQyT,YAAc,GAAIhV,QAAOC,KAAKC,OAAOqB,EAAQyT,YAAY,GAAIzT,EAAQyT,YAAY,IACnKH,EAAgBf,WAAaA,EAC7Be,EAAgBd,WAAaA,QAEtBc,GAAgBtW,eAChBsW,GAAgBpT,KAEvB,IACI+C,MACAyQ,EAAU,GAAIjV,QAAOC,KAAKiV,iBAE9BD,GAAQE,MAAMN,EAAiB,SAASO,EAAQC,GAC9C,GAAIA,IAAWrV,OAAOC,KAAKqV,iBAAiBC,GAAI,CAC9C,IAAK,GAAIC,KAAKJ,GAAO5Q,OACf4Q,EAAO5Q,OAAOmB,eAAe6P,IAC/BhR,EAAOjF,KAAK6V,EAAO5Q,OAAOgR,GAI1BjU,GAAQhD,UACVgD,EAAQhD,SAASiG,EAAQ4Q,EAAQC,OAI/B9T,GAAQE,OACVF,EAAQE,MAAM2T,EAAQC,MAM9BvX,EAAMa,UAAU8W,aAAe,WAC7B1X,KAAKyG,OAAOvF,OAAS,GAGvBnB,EAAMa,UAAU+W,cAAgB,SAASnU,GACvCA,EAAUvD,GACR2X,aACAlG,MAAO,EACPmG,QAAU,KACTrU,GAECA,EAAQoU,UAAU1W,OAAS,GACzBsC,EAAQoU,UAAU,GAAG1W,OAAS,IAChCsC,EAAQoU,UAAYnW,EAAWnB,GAAWkD,EAAQoU,WAAYxV,GAAgB,IAIlF,IAAI5B,GAAWgD,EAAQhD,eAChBgD,GAAQhD,QAEf,IAAI0W,GAAU,GAAIjV,QAAOC,KAAK4V,gBAG9B,IAAKtU,EAAQkO,KAUN,CACL,GAAIqG,IACFrG,KAAOlO,EAAQoU,UACfC,QAAUrU,EAAQqU,QAGpBX,GAAQc,sBAAsBD,EAAa,SAASV,EAAQC,GACvD9W,GAAiC,kBAAf,IACnBA,EAAS6W,EAAQC,gBAjBd9T,GAAQkO,WACRlO,GAAQqU,QAEfX,EAAQe,yBAAyBzU,EAAS,SAAS6T,EAAQC,GACrD9W,GAAiC,kBAAf,IACpBA,EAAS6W,EAAQC,MAkBzBvX,EAAMa,UAAUsX,WAAanY,EAAMa,UAAU6R,gBAE7C1S,EAAMa,UAAUuX,YAAc,SAAS3U,EAAS4U,GAC9C,GAEIvO,GADAwO,EAAyC,gBAAxBD,GAAcC,MAAsBzV,SAASC,eAAeuV,EAAcC,MAAM3V,QAAQ,IAAK,KAAO0V,EAAcC,KAGvID,GAAcC,MAAQA,EACtBD,EAAgBnY,GACdkB,IAAKnB,KAAKmB,KACTiX,GACHvO,EAAU,GAAI5H,QAAOC,KAAKoW,mBAAmBF,GAE7CpY,KAAKiW,WACHc,OAAQvT,EAAQuT,OAChBE,YAAazT,EAAQyT,YACrBlB,WAAYvS,EAAQuS,WACpBc,UAAWrT,EAAQqT,UACnBb,WAAYxS,EAAQwS,WACpBtS,MAAOF,EAAQE,MACfgT,cAAelT,EAAQkT,cACvBC,WAAYnT,EAAQmT,WACpBC,kBAAmBpT,EAAQoT,kBAC3BpW,SAAU,SAASiG,EAAQ8R,EAAUjB,GAC/BA,IAAWrV,OAAOC,KAAKqV,iBAAiBC,IAC1C3N,EAAQ2O,cAAcD,OAM9BxY,EAAMa,UAAU6X,UAAY,SAASjV,GACnC,GAAIQ,GAAOhE,IAEXA,MAAKiW,WACHc,OAAQvT,EAAQuT,OAChBE,YAAazT,EAAQyT,YACrBlB,WAAYvS,EAAQuS,WACpBc,UAAWrT,EAAQqT,UACnBb,WAAYxS,EAAQwS,WACpBtS,MAAOF,EAAQE,MACfgT,cAAelT,EAAQkT,cACvBC,WAAYnT,EAAQmT,WACpBC,kBAAmBpT,EAAQoT,kBAC3BpW,SAAU,SAASiG,GACjB,GAAIA,EAAOvF,OAAS,EAAG,CACrB,GAAI2Q,IACFH,KAAMjL,EAAOA,EAAOvF,OAAS,GAAGwX,cAChC5G,YAAatO,EAAQsO,YACrBC,cAAevO,EAAQuO,cACvBC,aAAcxO,EAAQwO,aAGpBxO,GAAQoE,eAAe,WACzBiK,EAAiBO,MAAQ5O,EAAQ4O,OAGnCpO,EAAKyN,aAAaI,GAEdrO,EAAQhD,UACVgD,EAAQhD,SAASiG,EAAOA,EAAOvF,OAAS,SAOlDnB,EAAMa,UAAU+X,YAAc,SAASnV,GACrC,GAAIA,EAAQuT,QAAUvT,EAAQyT,YAC5BjX,KAAKiW,WACHc,OAAQvT,EAAQuT,OAChBE,YAAazT,EAAQyT,YACrBlB,WAAYvS,EAAQuS,WACpBc,UAAYrT,EAAQqT,UACpBb,WAAYxS,EAAQwS,WACpBtS,MAAOF,EAAQE,MACflD,SAAU,SAASkH,GAOjB,GALIA,EAAExG,OAAS,GAAKsC,EAAQoV,OAC1BpV,EAAQoV,MAAMlR,EAAEA,EAAExG,OAAS,IAIzBwG,EAAExG,OAAS,GAAKsC,EAAQqV,KAAM,CAChC,GAAIzB,GAAQ1P,EAAEA,EAAExG,OAAS,EACzB,IAAIkW,EAAM0B,KAAK5X,OAAS,EAEtB,IAAK,GAAW2X,GADZE,EAAQ3B,EAAM0B,KAAK,GAAGC,MACjBtY,EAAI,EAASoY,EAAOE,EAAMtY,GAAIA,IACrCoY,EAAKG,YAAcvY,EACnB+C,EAAQqV,KAAKA,EAAOzB,EAAM0B,KAAK,GAAGC,MAAM7X,OAAS,GAMnDwG,EAAExG,OAAS,GAAKsC,EAAQyV,KACzBzV,EAAQyV,IAAIvR,EAAEA,EAAExG,OAAS,WAK7B,IAAIsC,EAAQ4T,OACX5T,EAAQ4T,MAAM0B,KAAK5X,OAAS,EAE9B,IAAK,GAAW2X,GADZE,EAAQvV,EAAQ4T,MAAM0B,KAAK,GAAGC,MACzBtY,EAAI,EAASoY,EAAOE,EAAMtY,GAAIA,IACrCoY,EAAKG,YAAcvY,EACnB+C,EAAQqV,KAAKA,IAMrB9Y,EAAMa,UAAUsY,iBAAmB,SAAS1V,GAC1C,GAAIQ,GAAOhE,IAEX,IAAIwD,EAAQuT,QAAUvT,EAAQyT,YAC5BjX,KAAKiW,WACHc,OAAQvT,EAAQuT,OAChBE,YAAazT,EAAQyT,YACrBlB,WAAYvS,EAAQuS,WACpBc,UAAYrT,EAAQqT,UACpBnT,MAAOF,EAAQE,MACflD,SAAU,SAASkH,GAOjB,GALIA,EAAExG,OAAS,GAAKsC,EAAQoV,OAC1BpV,EAAQoV,MAAMlR,EAAEA,EAAExG,OAAS,IAIzBwG,EAAExG,OAAS,GAAKsC,EAAQqV,KAAM,CAChC,GAAIzB,GAAQ1P,EAAEA,EAAExG,OAAS,EACzB,IAAIkW,EAAM0B,KAAK5X,OAAS,EAEtB,IAAK,GAAW2X,GADZE,EAAQ3B,EAAM0B,KAAK,GAAGC,MACjBtY,EAAI,EAASoY,EAAOE,EAAMtY,GAAIA,IAAK,CAC1CoY,EAAKG,YAAcvY,CACnB,IAAIoR,IACFH,KAAMmH,EAAKnH,KACXI,YAAatO,EAAQsO,YACrBC,cAAevO,EAAQuO,cACvBC,aAAcxO,EAAQwO,aAGpBxO,GAAQoE,eAAe,WACzBiK,EAAiBO,MAAQ5O,EAAQ4O,OAGnCpO,EAAKyN,aAAaI,GAClBrO,EAAQqV,KAAKA,EAAOzB,EAAM0B,KAAK,GAAGC,MAAM7X,OAAS,IAMnDwG,EAAExG,OAAS,GAAKsC,EAAQyV,KACzBzV,EAAQyV,IAAIvR,EAAEA,EAAExG,OAAS,WAK7B,IAAIsC,EAAQ4T,OACX5T,EAAQ4T,MAAM0B,KAAK5X,OAAS,EAE9B,IAAK,GAAW2X,GADZE,EAAQvV,EAAQ4T,MAAM0B,KAAK,GAAGC,MACzBtY,EAAI,EAASoY,EAAOE,EAAMtY,GAAIA,IAAK,CAC1CoY,EAAKG,YAAcvY,CACnB,IAAIoR,IACFH,KAAMmH,EAAKnH,KACXI,YAAatO,EAAQsO,YACrBC,cAAevO,EAAQuO,cACvBC,aAAcxO,EAAQwO,aAGpBxO,GAAQoE,eAAe,WACzBiK,EAAiBO,MAAQ5O,EAAQ4O,OAGnCpO,EAAKyN,aAAaI,GAClBrO,EAAQqV,KAAKA,KAMrB9Y,EAAMoZ,MAAQ,SAAS3V,GACrBxD,KAAK+W,OAASvT,EAAQuT,OACtB/W,KAAKiX,YAAczT,EAAQyT,YAC3BjX,KAAK6W,UAAYrT,EAAQqT,UAEzB7W,KAAKmB,IAAMqC,EAAQrC,IACnBnB,KAAKoX,MAAQ5T,EAAQ4T,MACrBpX,KAAKoZ,WAAa,EAClBpZ,KAAK+Y,MAAQ/Y,KAAKoX,MAAM0B,KAAK,GAAGC,MAChC/Y,KAAKqZ,aAAerZ,KAAK+Y,MAAM7X,MAE/B,IAAI2Q,IACFH,KAAM,GAAIzP,QAAOC,KAAKoX,SACtBxH,YAAatO,EAAQsO,YACrBC,cAAevO,EAAQuO,cACvBC,aAAcxO,EAAQwO,aAGpBxO,GAAQoE,eAAe,WACzBiK,EAAiBO,MAAQ5O,EAAQ4O,OAGnCpS,KAAKqS,SAAWrS,KAAKmB,IAAIsQ,aAAaI,GAAkB0H,WAG1DxZ,EAAMoZ,MAAMvY,UAAU4Y,SAAW,SAAShW,GACxC,GAAIQ,GAAOhE,IAEXA,MAAKmB,IAAI8U,WACPc,OAAS/W,KAAK+W,OACdE,YAAcjX,KAAKiX,YACnBlB,WAAavS,EAAQuS,WACrBc,UAAY7W,KAAK6W,cACjBnT,MAAOF,EAAQE,MACflD,SAAW,WACTwD,EAAKoT,MAAQ1P,EAAE,GAEXlE,EAAQhD,UACVgD,EAAQhD,SAASM,KAAKkD,OAM9BjE,EAAMoZ,MAAMvY,UAAU6Y,KAAO,WAC3B,GAAIzZ,KAAKoZ,WAAa,EAAG,CACvBpZ,KAAKoZ,YACL,IAAI1H,GAAO1R,KAAKoX,MAAM0B,KAAK,GAAGC,MAAM/Y,KAAKoZ,YAAY1H,IAErD,KAAK,GAAIgI,KAAKhI,GACRA,EAAK9J,eAAe8R,IACtB1Z,KAAKqS,SAASsH,QAMtB5Z,EAAMoZ,MAAMvY,UAAUgZ,QAAU,WAC9B,GAAI5Z,KAAKoZ,WAAapZ,KAAKqZ,aAAc,CACvC,GAAI3H,GAAO1R,KAAKoX,MAAM0B,KAAK,GAAGC,MAAM/Y,KAAKoZ,YAAY1H,IAErD,KAAK,GAAIgI,KAAKhI,GACRA,EAAK9J,eAAe8R,IACtB1Z,KAAKqS,SAAS7Q,KAAKkQ,EAAKgI,GAG5B1Z,MAAKoZ,eAITrZ,EAAMa,UAAUiZ,cAAgB,SAASjV,EAAKC,EAAKiV,GACjD,MAAOA,GAAMC,eAAe,GAAI9X,QAAOC,KAAKC,OAAOyC,EAAKC,KAG1D9E,EAAMa,UAAU4N,oBAAsB,SAAS/E,EAAQuQ,GACrD,GAAIvQ,EAAOgE,OACT,IAAK,GAAWqM,GAAPrZ,EAAI,EAAUqZ,EAAQrQ,EAAOgE,OAAOhN,GAAIA,IAAK,CACpD,GAAIwZ,GAAMxQ,EAAOC,aACZ1J,MAAK6Z,cAAcI,EAAIrV,MAAOqV,EAAIpV,MAAOiV,IAC5CE,EAAiBvQ,EAAQqQ,KAMjC/Z,EAAMa,UAAUsZ,QAAU,SAAS1W,GACjC,GAAIA,GAAUA,MACV2W,IAMJ,IAJAA,EAAyB,KAAI3W,EAAc,OAAMxD,KAAKqE,GAAG0M,YAAa/Q,KAAKqE,GAAGwM,cAC9EsJ,EAAwB,IAAIna,KAAKoa,YAAYxV,MAC7CuV,EAAwB,IAAIna,KAAKoa,YAAYvV,MAEzC7E,KAAKuG,QAAQrF,OAAS,EAAG,CAC3BiZ,EAA4B,UAE5B,KAAK,GAAI1Z,GAAI,EAAGA,EAAIT,KAAKuG,QAAQrF,OAAQT,IACvC0Z,EAA4B,QAAE3Y,MAC5BoD,IAAK5E,KAAKuG,QAAQ9F,GAAGiJ,cAAc9E,MACnCC,IAAK7E,KAAKuG,QAAQ9F,GAAGiJ,cAAc7E,QAKzC,GAAI7E,KAAKwG,UAAUtF,OAAS,EAAG,CAC7B,GAAImR,GAAWrS,KAAKwG,UAAU,EAE9B2T,GAA6B,YAC7BA,EAA6B,SAAQ,KAAIlY,OAAOC,KAAKmY,SAASC,SAASC,WAAWlI,EAASkH,WAC3FY,EAA6B,SAAe,YAAI9H,EAASP,YACzDqI,EAA6B,SAAiB,cAAI9H,EAASN,cAC3DoI,EAA6B,SAAgB,aAAI9H,EAASL,aAG5D,MAAOjS,GAAMya,aAAaL,IAG5Bpa,EAAMya,aAAe,SAAShX,GAyJ5B,QAASiX,GAAWC,EAAOC,GACzB,GAAiB,MAAbD,EAAM,KACRA,EAAQA,EAAMhY,QAAQ,IAAK,MAEvBiY,GAAS,CAGX,GAFAA,EAAUC,WAAWD,GACrBA,EAAUE,KAAKC,IAAI,EAAGD,KAAKE,IAAIJ,EAAS,IACxB,IAAZA,EACF,MAAO,YAETA,IAAqB,IAAVA,GAAeK,SAAS,IACZ,IAAnBL,EAAQzZ,SACVyZ,GAAWA,GAGbD,EAAQA,EAAM7Z,MAAM,EAAE,GAAK8Z,EAG/B,MAAOD,GA1KT,GACIO,GADAC,KAEAC,GAAqC,UAAtB7F,SAAS8F,SAAuB,QAAU9F,SAAS8F,UAAa,0CAE/E5X,GAAQmQ,MACVwH,EAAc3X,EAAQmQ,UACfnQ,GAAQmQ,KAGjBwH,GAAe,GAEf,IAAI5U,GAAU/C,EAAQ+C,cAEf/C,GAAQ+C,SAEVA,GAAW/C,EAAQiG,SACtBlD,GAAW/C,EAAQiG,cACZjG,GAAQiG,OAGjB,IAAI4R,GAAS7X,EAAQ6X,aAEd7X,GAAQ6X,MAEf,IAAIhJ,GAAW7O,EAAQ6O,QAIvB,UAHO7O,GAAQ6O,SAGX7O,EAAQmC,OACVuV,EAAW1Z,KAAK,UAAYgC,EAAQmC,cAC7BnC,GAAQmC,WAEZ,IAAInC,EAAQ8X,QACfJ,EAAW1Z,KAAK,UAAYgC,EAAQ8X,eAC7B9X,GAAQ8X,YAEZ,IAAI9X,EAAQoB,IACfsW,EAAW1Z,MAAM,UAAWgC,EAAQoB,IAAK,IAAKpB,EAAQqB,KAAK0W,KAAK,WACzD/X,GAAQoB,UACRpB,GAAQqB,QAEZ,IAAIrB,EAAQ4H,QAAS,CACxB,GAAIA,GAAUoQ,UAAUhY,EAAQ4H,QAAQmQ,KAAK,KAC7CL,GAAW1Z,KAAK,WAAa4J,GAG/B,GAAIqQ,GAAOjY,EAAQiY,IACfA,IACEA,EAAKF,OACPE,EAAOA,EAAKF,KAAK,YAEZ/X,GAAQiY,MAGfA,EAAO,UAETP,EAAW1Z,KAAK,QAAUia,GAErBjY,EAAQG,MAAQH,EAAQG,QAAS,IACpCH,EAAQG,KAAO,GAGjB,IAAI+X,GAASlY,EAAQoE,eAAe,YAAcpE,EAAQkY,QAAS,QAC5DlY,GAAQkY,OACfR,EAAW1Z,KAAK,UAAYka,EAE5B,KAAK,GAAIC,KAASnY,GACZA,EAAQoE,eAAe+T,IACzBT,EAAW1Z,KAAKma,EAAQ,IAAMnY,EAAQmY,GAK1C,IAAIpV,EAGF,IAAK,GAFDkD,GAAQmS,EAEHnb,EAAI,EAAGwa,EAAO1U,EAAQ9F,GAAIA,IAAK,CACtCgJ,KAEIwR,EAAKQ,MAAsB,WAAdR,EAAKQ,MACpBhS,EAAOjI,KAAK,QAAUyZ,EAAKQ,YACpBR,GAAKQ,MAELR,EAAKY,OACZpS,EAAOjI,KAAK,QAAUga,UAAUP,EAAKY,aAC9BZ,GAAKY,MAGVZ,EAAKP,QACPjR,EAAOjI,KAAK,SAAWyZ,EAAKP,MAAMhY,QAAQ,IAAK,aACxCuY,GAAKP,OAGVO,EAAKa,QACPrS,EAAOjI,KAAK,SAAWyZ,EAAKa,MAAM,GAAGpX,qBAC9BuW,GAAKa,OAGdF,EAAOX,EAAKK,QAAUL,EAAKK,QAAUL,EAAKrW,IAAM,IAAMqW,EAAKpW,UACpDoW,GAAKK,cACLL,GAAKrW,UACLqW,GAAKpW,GAEZ,KAAI,GAAI8W,KAASV,GACXA,EAAKrT,eAAe+T,IACtBlS,EAAOjI,KAAKma,EAAQ,IAAMV,EAAKU,GAI/BlS,GAAOvI,QAAgB,IAANT,GACnBgJ,EAAOjI,KAAKoa,GACZnS,EAASA,EAAO8R,KAAK,KACrBL,EAAW1Z,KAAK,WAAaga,UAAU/R,MAIvCA,EAASyR,EAAWvB,MAAQ6B,UAAU,IAAMI,GAC5CV,EAAW1Z,KAAKiI,IAMtB,GAAI4R,EACF,IAAK,GAAI5a,GAAI,EAAGA,EAAI4a,EAAOna,OAAQT,IAAK,CACtC,GAAIsb,KACAV,GAAO5a,GAAGub,aACZD,EAAUva,KAAK,WAAa6Z,EAAO5a,GAAGub,YAAY9L,eAGhDmL,EAAO5a,GAAGwb,aACZF,EAAUva,KAAK,WAAa6Z,EAAO5a,GAAGwb,YAAY/L,cAGpD,KAAK,GAAIgM,GAAI,EAAGA,EAAIb,EAAO5a,GAAG0b,QAAQjb,OAAQgb,IAC5C,IAAK,GAAIxC,KAAK2B,GAAO5a,GAAG0b,QAAQD,GAAI,CAClC,GAAIE,GAAUf,EAAO5a,GAAG0b,QAAQD,GAAGxC,IAC1B,OAALA,GAAmB,SAALA,KAChB0C,EAAU,KAAOA,EAAQC,UAAU,IAErCN,EAAUva,KAAKkY,EAAI,IAAM0C,GAI7B,GAAIE,GAAOP,EAAUR,KAAK,IACd,KAARe,GACFpB,EAAW1Z,KAAK,SAAW8a,GA2BjC,GAAIjK,EAAU,CAQZ,GAPA4I,EAAO5I,EACPA,KAEI4I,EAAKjJ,cACPK,EAAS7Q,KAAK,UAAY+a,SAAStB,EAAKjJ,aAAc,KAGpDiJ,EAAKnJ,YAAa,CACpB,GAAI4I,GAAQD,EAAWQ,EAAKnJ,YAAamJ,EAAKlJ,cAC9CM,GAAS7Q,KAAK,SAAWkZ,GAG3B,GAAIO,EAAKuB,UAAW,CAClB,GAAIC,GAAYhC,EAAWQ,EAAKuB,UAAWvB,EAAKyB,YAChDrK,GAAS7Q,KAAK,aAAeib,GAG/B,GAAI/K,GAAOuJ,EAAKvJ,IAChB,IAAIA,EAAK6J,KACP,IAAK,GAAStB,GAALiC,EAAE,EAAQjC,EAAIvI,EAAKwK,GAAIA,IAC9B7J,EAAS7Q,KAAKyY,EAAIsB,KAAK,UAIzBlJ,GAAS7Q,KAAK,OAASkQ,EAGzBW,GAAWA,EAASkJ,KAAK,KACzBL,EAAW1Z,KAAK,QAAUga,UAAUnJ,IAItC,GAAIsK,GAAM5Z,OAAO6Z,kBAAoB,CAIrC,OAHA1B,GAAW1Z,KAAK,SAAWmb,GAE3BzB,EAAaA,EAAWK,KAAK,KACtBJ,EAAcD,GAGvBnb,EAAMa,UAAUic,WAAa,SAASjX,EAAWpC,GAC/C,IAAIA,EAAQoE,eAAe,eAAkD,kBAA1BpE,GAAqB,WAQtE,KAAM,iCAPNA,GAAQsZ,SAAWtZ,EAAQsZ,UAAY,GAAI7a,QAAOC,KAAK6a,KAAK,IAAK,IAEjE,IAAInZ,GAAU,GAAI3B,QAAOC,KAAK8a,aAAaxZ,EAE3CxD,MAAKmB,IAAI8b,SAASC,IAAItX,EAAWhC,IAOrC7D,EAAMa,UAAUuc,kBAAoB,SAAS3Z,GAC3C,IAAIA,EAAQoE,eAAe,YAA4C,kBAAvBpE,GAAkB,QAQhE,KAAM,8BAPN,IAAI4Z,GAAsB5Z,EAAQyJ,YAE3BzJ,GAAQyJ,MAEfjN,KAAKmB,IAAIkc,gBAAgBC,SAASF,EAAqB5Z,IAO3DzD,EAAMa,UAAU2c,qBAAuB,SAASH,GAC9Cpd,KAAKmB,IAAIkc,gBAAgB/P,SAAS8P,IAGpCrd,EAAMa,UAAU4c,SAAW,SAASha,GAClC,GAAIia,GAAgB,GAAIxb,QAAOC,KAAKwb,cAAcla,EAAQ6X,QAAUjb,KAAMoD,EAAQma,eAElF3d,MAAKmB,IAAI8b,SAASC,IAAI1Z,EAAQoC,UAAW6X,IAG3C1d,EAAMa,UAAUgd,SAAW,SAAShY,GAClC5F,KAAKmB,IAAI0c,aAAajY,IAGxB7F,EAAMa,UAAUkd,eAAiB,SAASC,GAUxC,MATKA,GAAmBnW,eAAe,QAAWmW,EAAmBnW,eAAe,SAClFmW,EAAmBnZ,IAAM5E,KAAKoa,YAAYxV,MAC1CmZ,EAAmBlZ,IAAM7E,KAAKoa,YAAYvV,OAG5C7E,KAAKge,SAAWje,EAAM+d,eAAeC,GAErC/d,KAAKmB,IAAI8c,cAAcje,KAAKge,UAErBhe,KAAKge,UAGdje,EAAM+d,eAAiB,SAASta,GAC9B,GAAIa,GAAKxB,EAAeW,EAAQa,GAAIb,EAAQjB,QAE5CiB,GAAQyB,SAAW,GAAIhD,QAAOC,KAAKC,OAAOqB,EAAQoB,IAAKpB,EAAQqB,WAExDrB,GAAQa,SACRb,GAAQjB,cACRiB,GAAQoB,UACRpB,GAAQqB,GAKf,KAAK,GAHDqZ,IAAqB,aAAc,gBAAiB,eAAgB,mBAAoB,cAAe,SAAU,mBACjHH,EAAqB9d,GAAemL,SAAU,GAAO5H,GAEhD/C,EAAI,EAAGA,EAAIyd,EAAkBhd,OAAQT,UACrCsd,GAAmBG,EAAkBzd,GAK9C,KAAK,GAFDud,GAAW,GAAI/b,QAAOC,KAAKic,mBAAmB9Z,EAAI0Z,GAE7Ctd,EAAI,EAAGA,EAAIyd,EAAkBhd,OAAQT,KAC5C,SAAUmK,EAAQxK,GACZoD,EAAQpD,IACV6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,WAC1CoD,EAAQpD,GAAMmB,MAAMvB,SAGvBge,EAAUE,EAAkBzd,GAGjC,OAAOud,IAGTje,EAAMa,UAAUwd,GAAK,SAASC,EAAYC,GACxC,MAAOve,GAAMqe,GAAGC,EAAYre,KAAMse,IAGpCve,EAAMa,UAAU2d,IAAM,SAASF,GAC7Bte,EAAMwe,IAAIF,EAAYre,OAGxBD,EAAMa,UAAU4d,KAAO,SAASH,EAAYC,GAC1C,MAAOve,GAAMye,KAAKH,EAAYre,KAAMse,IAGtCve,EAAM0e,eAAiB,eAAgB,iBAAkB,iBAAkB,mBAAoB,gBAAiB,kBAAmB,aAAc,sBAEjJ1e,EAAMqe,GAAK,SAASC,EAAYzT,EAAQ0T,GACtC,GAA+C,IAA3Cve,EAAM0e,cAAcxY,QAAQoY,GAE9B,MADGzT,aAAkB7K,KAAO6K,EAASA,EAAOzJ,KACrCc,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQyT,EAAYC,EAGzD,IAAII,IACFJ,QAAUA,EACVK,UAAYN,EAMd,OAHAzT,GAAO/D,kBAAkBwX,GAAczT,EAAO/D,kBAAkBwX,OAChEzT,EAAO/D,kBAAkBwX,GAAY7c,KAAKkd,GAEnCA,GAIX3e,EAAMwe,IAAM,SAASF,EAAYzT,GACgB,IAA3C7K,EAAM0e,cAAcxY,QAAQoY,IAC3BzT,YAAkB7K,KAAO6K,EAASA,EAAOzJ,KAC5Cc,OAAOC,KAAKwG,MAAMC,eAAeiC,EAAQyT,IAGzCzT,EAAO/D,kBAAkBwX,OAI7Bte,EAAMye,KAAO,SAASH,EAAYzT,EAAQ0T,GACxC,MAA+C,IAA3Cve,EAAM0e,cAAcxY,QAAQoY,IAC3BzT,YAAkB7K,KAAO6K,EAASA,EAAOzJ,KACrCc,OAAOC,KAAKwG,MAAMkW,gBAAgBhU,EAAQyT,EAAYC,IAF/D,QAMFve,EAAM6O,KAAO,SAASyP,EAAYzT,EAAQyB,GACxC,GAA+C,IAA3CtM,EAAM0e,cAAcxY,QAAQoY,GAC9Bpc,OAAOC,KAAKwG,MAAMsC,QAAQJ,EAAQyT,EAAY1d,MAAMC,UAAUC,MAAMU,MAAMR,WAAWF,MAAM,QAG3F,IAAGwd,IAAchS,GAAMxF,kBAGrB,IAAI,GAFAgY,GAAgBxS,EAAMxF,kBAAkBwX,GAEpC5d,EAAI,EAAGA,EAAIoe,EAAc3d,OAAQT,KACvC,SAAU6d,EAASjS,EAAOzB,GACxB0T,EAAQ/c,MAAM8K,GAAQzB,KACrBiU,EAAcpe,GAAY,QAAG4L,EAAOzB,IAM/C7K,EAAM+e,UAAY,SAAStb,GACzB,GAAIub,GAAoBvb,EAAQwb,QAAUxb,EAAQyb,QAE9CjP,WAAUkP,YACZlP,UAAUkP,YAAYC,mBAAmB,SAASla,GAChDzB,EAAQ4b,QAAQna,GAEZ8Z,GACFA,KAED,SAASrb,GACVF,EAAQE,MAAMA,GAEVqb,GACFA,KAEDvb,EAAQA,UAGXA,EAAQ6b,gBAEJN,GACFA,MAKNhf,EAAMuf,QAAU,SAAS9b,GACvBxD,KAAKuf,SAAW,GAAItd,QAAOC,KAAKsd,QAChC,IAAIhf,GAAWgD,EAAQhD,QACnBgD,GAAQoE,eAAe,QAAUpE,EAAQoE,eAAe,SAC1DpE,EAAQ4K,OAAS,GAAInM,QAAOC,KAAKC,OAAOqB,EAAQoB,IAAKpB,EAAQqB,YAGxDrB,GAAQoB,UACRpB,GAAQqB,UACRrB,GAAQhD,SAEfR,KAAKuf,SAASD,QAAQ9b,EAAS,SAASic,EAASnI,GAC/C9W,EAASif,EAASnI,MAIO,gBAAlBvU,QAAOd,QAAuBc,OAAOd,OAAOC,OAMhDD,OAAOC,KAAKiR,QAAQvS,UAAU8e,YACjCzd,OAAOC,KAAKiR,QAAQvS,UAAU8e,UAAY,SAAStR,GAKjD,IAAK,GAFDsD,GAFAnG,EAAS,GAAItJ,QAAOC,KAAKsJ,aACzB0H,EAAQlT,KAAK2f,WAGRjG,EAAI,EAAGA,EAAIxG,EAAM0M,YAAalG,IAAK,CAC1ChI,EAAOwB,EAAM7F,MAAMqM,EACnB,KAAK,GAAIjZ,GAAI,EAAGA,EAAIiR,EAAKkO,YAAanf,IACpC8K,EAAOE,OAAOiG,EAAKrE,MAAM5M,IAI7B,MAAO8K,KAINtJ,OAAOC,KAAKiR,QAAQvS,UAAUmZ,iBAEjC9X,OAAOC,KAAKiR,QAAQvS,UAAUmZ,eAAiB,SAAS3L,GAEtD,GAAI7C,GAASvL,KAAK0f,WAElB,IAAe,OAAXnU,IAAoBA,EAAOb,SAAS0D,GACtC,OAAO,CAOT,KAAK,GAHDyR,IAAS,EAETC,EAAW9f,KAAK2f,WAAWC,YACtBlG,EAAI,EAAOoG,EAAJpG,EAAcA,IAK5B,IAAK,GAJDhI,GAAO1R,KAAK2f,WAAWtS,MAAMqM,GAC7BqG,EAAYrO,EAAKkO,YACjB1D,EAAI6D,EAAY,EAEXtf,EAAI,EAAOsf,EAAJtf,EAAeA,IAAK,CAClC,GAAIuf,GAAUtO,EAAKrE,MAAM5M,GACrBwf,EAAUvO,EAAKrE,MAAM6O,IAErB8D,EAAQnb,MAAQuJ,EAAOvJ,OAASob,EAAQpb,OAASuJ,EAAOvJ,OAASob,EAAQpb,MAAQuJ,EAAOvJ,OAASmb,EAAQnb,OAASuJ,EAAOvJ,QACvHmb,EAAQpb,OAASwJ,EAAOvJ,MAAQmb,EAAQnb,QAAUob,EAAQpb,MAAQmb,EAAQnb,QAAUob,EAAQrb,MAAQob,EAAQpb,OAASwJ,EAAOxJ,QAC9Hib,GAAUA,GAId3D,EAAIzb,EAIR,MAAOof,KAIN5d,OAAOC,KAAK0Q,OAAOhS,UAAUmZ,iBAChC9X,OAAOC,KAAK0Q,OAAOhS,UAAUmZ,eAAiB,SAAS3L,GACrD,MAAInM,QAAOC,KAAKmY,SACPpY,OAAOC,KAAKmY,SAAS6F,UAAUC,uBAAuBngB,KAAKoa,YAAahM,IAAWpO,KAAKogB,aAGxF,IAKbne,OAAOC,KAAK8Q,UAAUpS,UAAUmZ,eAAiB,SAAS3L,GACxD,MAAOpO,MAAK0f,YAAYhV,SAAS0D,IAGnCnM,OAAOC,KAAKsJ,aAAa5K,UAAUmZ,eAAiB,SAAS3L,GAC3D,MAAOpO,MAAK0K,SAAS0D,IAGvBnM,OAAOC,KAAK2L,OAAOjN,UAAUyf,UAAY,SAAS5S,GAChDzN,KAAKyN,OAASA,GAGhBxL,OAAOC,KAAK2L,OAAOjN,UAAU0f,SAAW,SAASxG,GAC/C9Z,KAAKyN,OAAOjM,KAAKsY,IAGnB7X,OAAOC,KAAK2L,OAAOjN,UAAU2f,MAAQ,WACnC,MAAOvgB,MAAc,UAOpBW,MAAMC,UAAUqF,UACnBtF,MAAMC,UAAUqF,QAAU,SAAUua,GAEhC,GAAY,MAARxgB,KACA,KAAM,IAAIygB,UAEd,IAAIC,GAAIC,OAAO3gB,MACX4gB,EAAMF,EAAExf,SAAW,CACvB,IAAY,IAAR0f,EACA,MAAO,EAEX,IAAIC,GAAI,CASR,IARI9f,UAAUG,OAAS,IACnB2f,EAAIC,OAAO/f,UAAU,IACjB8f,GAAKA,EACLA,EAAI,EACQ,GAALA,GAAUA,GAAKE,EAAAA,GAAYF,KAAME,EAAAA,KACxCF,GAAKA,EAAI,GAAK,IAAMhG,KAAKmG,MAAMnG,KAAKoG,IAAIJ,MAG5CA,GAAKD,EACL,MAAO,EAGX,KADA,GAAIM,GAAIL,GAAK,EAAIA,EAAIhG,KAAKE,IAAI6F,EAAM/F,KAAKoG,IAAIJ,GAAI,GACtCD,EAAJM,EAASA,IACZ,GAAIA,IAAKR,IAAKA,EAAEQ,KAAOV,EACnB,MAAOU,EAGf,OAAO,KAINnhB"} \ No newline at end of file diff --git a/public/static/libs/highlightjs/github-gist.css b/public/static/libs/highlightjs/github-gist.css new file mode 100644 index 0000000..3d05c6c --- /dev/null +++ b/public/static/libs/highlightjs/github-gist.css @@ -0,0 +1,71 @@ +/** + * GitHub Gist Theme + * Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro + */ + +.hljs { + display: block; + background: white; + padding: 0.5em; + color: #333333; + overflow-x: auto; +} + +.hljs-comment, +.hljs-meta { + color: #969896; +} + +.hljs-string, +.hljs-variable, +.hljs-template-variable, +.hljs-strong, +.hljs-emphasis, +.hljs-quote { + color: #df5000; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-type { + color: #a71d5d; +} + +.hljs-literal, +.hljs-symbol, +.hljs-bullet, +.hljs-attribute { + color: #0086b3; +} + +.hljs-section, +.hljs-name { + color: #63a35c; +} + +.hljs-tag { + color: #333333; +} + +.hljs-title, +.hljs-attr, +.hljs-selector-id, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #795da3; +} + +.hljs-addition { + color: #55a532; + background-color: #eaffea; +} + +.hljs-deletion { + color: #bd2c00; + background-color: #ffecec; +} + +.hljs-link { + text-decoration: underline; +} diff --git a/public/static/libs/highlightjs/github-gist.min.css b/public/static/libs/highlightjs/github-gist.min.css new file mode 100644 index 0000000..c9c26b8 --- /dev/null +++ b/public/static/libs/highlightjs/github-gist.min.css @@ -0,0 +1,5 @@ +/** + * GitHub Gist Theme + * Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro + */ +.hljs{display:block;background:#fff;padding:.5em;color:#333;overflow-x:auto}.hljs-comment,.hljs-meta{color:#969896}.hljs-emphasis,.hljs-quote,.hljs-string,.hljs-strong,.hljs-template-variable,.hljs-variable{color:#df5000}.hljs-keyword,.hljs-selector-tag,.hljs-type{color:#a71d5d}.hljs-attribute,.hljs-bullet,.hljs-literal,.hljs-symbol{color:#0086b3}.hljs-name,.hljs-section{color:#63a35c}.hljs-tag{color:#333}.hljs-attr,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-title{color:#795da3}.hljs-addition{color:#55a532;background-color:#eaffea}.hljs-deletion{color:#bd2c00;background-color:#ffecec}.hljs-link{text-decoration:underline} \ No newline at end of file diff --git a/public/static/libs/highlightjs/highlight.pack.js b/public/static/libs/highlightjs/highlight.pack.js new file mode 100644 index 0000000..ef858b7 --- /dev/null +++ b/public/static/libs/highlightjs/highlight.pack.js @@ -0,0 +1,2 @@ +/*! highlight.js v9.5.0 | BSD3 License | git.io/hljslicense */ +!function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/[&<>]/gm,function(e){return I[e]})}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return R(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||R(i))return i}function o(e,n){var t,r={};for(t in e)r[t]=e[t];if(n)for(t in n)r[t]=n[t];return r}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){l+=""}function c(e){("start"===e.event?o:u)(e.node)}for(var s=0,l="",f=[];e.length||r.length;){var g=i();if(l+=n(a.substr(s,g[0].offset-s)),s=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===s);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return l+n(a.substr(s))}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var u={},c=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");u[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?c("keyword",a.k):E(a.k).forEach(function(e){c(e,a.k[e])}),a.k=u}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]);var s=[];a.c.forEach(function(e){e.v?e.v.forEach(function(n){s.push(o(e,n))}):s.push("self"===e?a:e)}),a.c=s,a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var l=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=l.length?t(l.join("|"),!0):{exec:function(){return null}}}}r(e)}function l(e,t,a,i){function o(e,n){for(var t=0;t',i+n+o}function p(){var e,t,r,a;if(!E.k)return n(B);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(B);r;)a+=n(B.substr(t,r.index-t)),e=g(E,r),e?(M+=e[1],a+=h(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(B);return a+n(B.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!x[E.sL])return n(B);var t=e?l(E.sL,B,!0,L[E.sL]):f(B,E.sL.length?E.sL:void 0);return E.r>0&&(M+=t.r),e&&(L[E.sL]=t.top),h(t.language,t.value,!1,!0)}function b(){k+=null!=E.sL?d():p(),B=""}function v(e){k+=e.cN?h(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(B+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?B+=n:(t.eB&&(B+=n),b(),t.rB||t.eB||(B=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?B+=n:(a.rE||a.eE||(B+=n),b(),a.eE&&(B=n));do E.cN&&(k+=C),E.skip||(M+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return B+=n,n.length||1}var N=R(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var w,E=i||N,L={},k="";for(w=E;w!==N;w=w.parent)w.cN&&(k=h(w.cN,"",!0)+k);var B="",M=0;try{for(var I,j,O=0;;){if(E.t.lastIndex=O,I=E.t.exec(t),!I)break;j=m(t.substr(O,I.index-O),I[0]),O=I.index+j}for(m(t.substr(O)),w=E;w.parent;w=w.parent)w.cN&&(k+=C);return{r:M,value:k,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function f(e,t){t=t||y.languages||E(x);var r={r:0,value:n(e)},a=r;return t.filter(R).forEach(function(n){var t=l(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function g(e){return y.tabReplace||y.useBR?e.replace(M,function(e,n){return y.useBR&&"\n"===e?"
    ":y.tabReplace?n.replace(/\t/g,y.tabReplace):void 0}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function p(e){var n,t,r,o,s,p=i(e);a(p)||(y.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e,s=n.textContent,r=p?l(p,s,!0):f(s),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),s)),r.value=g(r.value),e.innerHTML=r.value,e.className=h(e.className,p,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function d(e){y=o(y,e)}function b(){if(!b.called){b.called=!0;var e=document.querySelectorAll("pre code");w.forEach.call(e,p)}}function v(){addEventListener("DOMContentLoaded",b,!1),addEventListener("load",b,!1)}function m(n,t){var r=x[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function N(){return E(x)}function R(e){return e=(e||"").toLowerCase(),x[e]||x[L[e]]}var w=[],E=Object.keys,x={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="",y={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},I={"&":"&","<":"<",">":">"};return e.highlight=l,e.highlightAuto=f,e.fixMarkup=g,e.highlightBlock=p,e.configure=d,e.initHighlighting=b,e.initHighlightingOnLoad=v,e.registerLanguage=m,e.listLanguages=N,e.getLanguage=R,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("bash",function(e){var t={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)}/}]},s={cN:"string",b:/"/,e:/"/,c:[e.BE,t,{cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]}]},a={cN:"string",b:/'/,e:/'/};return{aliases:["sh","zsh"],l:/-?[a-z\.]+/,k:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"meta",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:!0,c:[e.inherit(e.TM,{b:/\w[\w\d_]*/})],r:0},e.HCM,s,a,t]}});hljs.registerLanguage("markdown",function(e){return{aliases:["md","mkdown","mkd"],c:[{cN:"section",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"quote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"^```w*s*$",e:"^```s*$"},{b:"`.+?`"},{b:"^( {4}| )",e:"$",r:0}]},{b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"string",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"symbol",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:/^\[[^\n]+\]:/,rB:!0,c:[{cN:"symbol",b:/\[/,e:/\]/,eB:!0,eE:!0},{cN:"link",b:/:\s*/,e:/$/,eB:!0}]}]}});hljs.registerLanguage("cpp",function(t){var e={cN:"keyword",b:"\\b[a-z\\d_]*_t\\b"},r={cN:"string",v:[{b:'(u8?|U)?L?"',e:'"',i:"\\n",c:[t.BE]},{b:'(u8?|U)?R"',e:'"',c:[t.BE]},{b:"'\\\\?.",e:"'",i:"."}]},s={cN:"number",v:[{b:"\\b(0b[01'_]+)"},{b:"\\b([\\d'_]+(\\.[\\d'_]*)?|\\.[\\d'_]+)(u|U|l|L|ul|UL|f|F|b|B)"},{b:"(-?)(\\b0[xX][a-fA-F0-9'_]+|(\\b[\\d'_]+(\\.[\\d'_]*)?|\\.[\\d'_]+)([eE][-+]?[\\d'_]+)?)"}],r:0},i={cN:"meta",b:/#\s*[a-z]+\b/,e:/$/,k:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"},c:[{b:/\\\n/,r:0},t.inherit(r,{cN:"meta-string"}),{cN:"meta-string",b:"<",e:">",i:"\\n"},t.CLCM,t.CBCM]},a=t.IR+"\\s*\\(",c={keyword:"int float while private char catch export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const struct for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using class asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return",built_in:"std string cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr",literal:"true false nullptr NULL"},n=[e,t.CLCM,t.CBCM,s,r];return{aliases:["c","cc","h","c++","h++","hpp"],k:c,i:"",k:c,c:["self",e]},{b:t.IR+"::",k:c},{v:[{b:/=/,e:/;/},{b:/\(/,e:/\)/},{bK:"new throw return else",e:/;/}],k:c,c:n.concat([{b:/\(/,e:/\)/,k:c,c:n.concat(["self"]),r:0}]),r:0},{cN:"function",b:"("+t.IR+"[\\*&\\s]+)+"+a,rB:!0,e:/[{;=]/,eE:!0,k:c,i:/[^\w\s\*&]/,c:[{b:a,rB:!0,c:[t.TM],r:0},{cN:"params",b:/\(/,e:/\)/,k:c,r:0,c:[t.CLCM,t.CBCM,r,s,e]},t.CLCM,t.CBCM,i]}]),exports:{preprocessor:i,strings:r,k:c}}});hljs.registerLanguage("php",function(e){var c={b:"\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*"},i={cN:"meta",b:/<\?(php)?|\?>/},t={cN:"string",c:[e.BE,i],v:[{b:'b"',e:'"'},{b:"b'",e:"'"},e.inherit(e.ASM,{i:null}),e.inherit(e.QSM,{i:null})]},a={v:[e.BNM,e.CNM]};return{aliases:["php3","php4","php5","php6"],cI:!0,k:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",c:[e.HCM,e.C("//","$",{c:[i]}),e.C("/\\*","\\*/",{c:[{cN:"doctag",b:"@[A-Za-z]+"}]}),e.C("__halt_compiler.+?;",!1,{eW:!0,k:"__halt_compiler",l:e.UIR}),{cN:"string",b:/<<<['"]?\w+['"]?$/,e:/^\w+;?$/,c:[e.BE,{cN:"subst",v:[{b:/\$\w+/},{b:/\{\$/,e:/\}/}]}]},i,{cN:"keyword",b:/\$this\b/},c,{b:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{cN:"function",bK:"function",e:/[;{]/,eE:!0,i:"\\$|\\[|%",c:[e.UTM,{cN:"params",b:"\\(",e:"\\)",c:["self",c,e.CBCM,t,a]}]},{cN:"class",bK:"class interface",e:"{",eE:!0,i:/[:\(\$"]/,c:[{bK:"extends implements"},e.UTM]},{bK:"namespace",e:";",i:/[\.']/,c:[e.UTM]},{bK:"use",e:";",c:[e.UTM]},{b:"=>"},t,a]}});hljs.registerLanguage("css",function(e){var c="[a-zA-Z-][a-zA-Z0-9_-]*",t={b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\w-]+\(/,rB:!0,c:[{cN:"built_in",b:/[\w-]+/},{b:/\(/,e:/\)/,c:[e.ASM,e.QSM]}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",i:/:/,c:[{cN:"keyword",b:/\w+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:"selector-tag",b:c,r:0},{b:"{",e:"}",i:/\S/,c:[e.CBCM,t]}]}});hljs.registerLanguage("apache",function(e){var r={cN:"number",b:"[\\$%]\\d+"};return{aliases:["apacheconf"],cI:!0,c:[e.HCM,{cN:"section",b:""},{cN:"attribute",b:/\w+/,r:0,k:{nomarkup:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},starts:{e:/$/,r:0,k:{literal:"on off all"},c:[{cN:"meta",b:"\\s\\[",e:"\\]$"},{cN:"variable",b:"[\\$%]\\{",e:"\\}",c:["self",r]},r,e.QSM]}}],i:/\S/}});hljs.registerLanguage("xml",function(s){var e="[A-Za-z0-9\\._:-]+",t={eW:!0,i:/`]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},s.C("",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{b:/<\?(php)?/,e:/\?>/,sL:"php",c:[{b:"/\\*",e:"\\*/",skip:!0}]},{cN:"tag",b:"|$)",e:">",k:{name:"style"},c:[t],starts:{e:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"|$)",e:">",k:{name:"script"},c:[t],starts:{e:"",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"meta",v:[{b:/<\?xml/,e:/\?>/,r:10},{b:/<\?\w+/,e:/\?>/}]},{cN:"tag",b:"",c:[{cN:"name",b:/[^\/><\s]+/,r:0},t]}]}});hljs.registerLanguage("objectivec",function(e){var t={cN:"built_in",b:"\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\w+"},_={keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},i=/[a-zA-Z@][a-zA-Z0-9_]*/,n="@interface @class @protocol @implementation";return{aliases:["mm","objc","obj-c"],k:_,l:i,i:""}]}]},{cN:"class",b:"("+n.split(" ").join("|")+")\\b",e:"({|$)",eE:!0,k:n,l:i,c:[e.UTM]},{b:"\\."+e.UIR,r:0}]}});hljs.registerLanguage("nginx",function(e){var r={cN:"variable",v:[{b:/\$\d+/},{b:/\$\{/,e:/}/},{b:"[\\$\\@]"+e.UIR}]},b={eW:!0,l:"[a-z/_]+",k:{literal:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},r:0,i:"=>",c:[e.HCM,{cN:"string",c:[e.BE,r],v:[{b:/"/,e:/"/},{b:/'/,e:/'/}]},{b:"([a-z]+):/",e:"\\s",eW:!0,eE:!0,c:[r]},{cN:"regexp",c:[e.BE,r],v:[{b:"\\s\\^",e:"\\s|{|;",rE:!0},{b:"~\\*?\\s+",e:"\\s|{|;",rE:!0},{b:"\\*(\\.[a-z\\-]+)+"},{b:"([a-z\\-]+\\.)+\\*"}]},{cN:"number",b:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{cN:"number",b:"\\b\\d+[kKmMgGdshdwy]*\\b",r:0},r]};return{aliases:["nginxconf"],c:[e.HCM,{b:e.UIR+"\\s+{",rB:!0,e:"{",c:[{cN:"section",b:e.UIR}],r:0},{b:e.UIR+"\\s",e:";|{",rB:!0,c:[{cN:"attribute",b:e.UIR,starts:b}],r:0}],i:"[^\\s\\}]"}});hljs.registerLanguage("coffeescript",function(e){var c={keyword:"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super then unless until loop of by when and or is isnt not",literal:"true false null undefined yes no on off",built_in:"npm require console print module global window document"},n="[A-Za-z$_][0-9A-Za-z$_]*",r={cN:"subst",b:/#\{/,e:/}/,k:c},s=[e.BNM,e.inherit(e.CNM,{starts:{e:"(\\s*/)?",r:0}}),{cN:"string",v:[{b:/'''/,e:/'''/,c:[e.BE]},{b:/'/,e:/'/,c:[e.BE]},{b:/"""/,e:/"""/,c:[e.BE,r]},{b:/"/,e:/"/,c:[e.BE,r]}]},{cN:"regexp",v:[{b:"///",e:"///",c:[r,e.HCM]},{b:"//[gim]*",r:0},{b:/\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/}]},{b:"@"+n},{b:"`",e:"`",eB:!0,eE:!0,sL:"javascript"}];r.c=s;var i=e.inherit(e.TM,{b:n}),t="(\\(.*\\))?\\s*\\B[-=]>",o={cN:"params",b:"\\([^\\(]",rB:!0,c:[{b:/\(/,e:/\)/,k:c,c:["self"].concat(s)}]};return{aliases:["coffee","cson","iced"],k:c,i:/\/\*/,c:s.concat([e.C("###","###"),e.HCM,{cN:"function",b:"^\\s*"+n+"\\s*=\\s*"+t,e:"[-=]>",rB:!0,c:[i,o]},{b:/[:\(,=]\s*/,r:0,c:[{cN:"function",b:t,e:"[-=]>",rB:!0,c:[o]}]},{cN:"class",bK:"class",e:"$",i:/[:="\[\]]/,c:[{bK:"extends",eW:!0,i:/[:="\[\]]/,c:[i]},i]},{b:n+":",e:":",rB:!0,rE:!0,r:0}])}});hljs.registerLanguage("json",function(e){var i={literal:"true false null"},n=[e.QSM,e.CNM],r={e:",",eW:!0,eE:!0,c:n,k:i},t={b:"{",e:"}",c:[{cN:"attr",b:/"/,e:/"/,c:[e.BE],i:"\\n"},e.inherit(r,{b:/:/})],i:"\\S"},c={b:"\\[",e:"\\]",c:[e.inherit(r)],i:"\\S"};return n.splice(n.length,0,t,c),{c:n,k:i,i:"\\S"}});hljs.registerLanguage("scss",function(e){var t="[a-zA-Z-][a-zA-Z0-9_-]*",i={cN:"variable",b:"(\\$"+t+")\\b"},r={cN:"number",b:"#[0-9A-Fa-f]+"};({cN:"attribute",b:"[A-Z\\_\\.\\-]+",e:":",eE:!0,i:"[^\\s]",starts:{eW:!0,eE:!0,c:[r,e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"meta",b:"!important"}]}});return{cI:!0,i:"[=/|']",c:[e.CLCM,e.CBCM,{cN:"selector-id",b:"\\#[A-Za-z0-9_-]+",r:0},{cN:"selector-class",b:"\\.[A-Za-z0-9_-]+",r:0},{cN:"selector-attr",b:"\\[",e:"\\]",i:"$"},{cN:"selector-tag",b:"\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\b",r:0},{b:":(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)"},{b:"::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)"},i,{cN:"attribute",b:"\\b(z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background-blend-mode|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\b",i:"[^\\s]"},{b:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b"},{b:":",e:";",c:[i,r,e.CSSNM,e.QSM,e.ASM,{cN:"meta",b:"!important"}]},{b:"@",e:"[{;]",k:"mixin include extend for if else each while charset import debug media page content font-face namespace warn",c:[i,e.QSM,e.ASM,r,e.CSSNM,{b:"\\s[A-Za-z0-9_.-]+",r:0}]}]}});hljs.registerLanguage("javascript",function(e){return{aliases:["js","jsx"],k:{keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,{cN:"string",b:"`",e:"`",c:[e.BE,{cN:"subst",b:"\\$\\{",e:"\\}"}]},e.CLCM,e.CBCM,{cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{b://,sL:"xml",c:[{b:/<\w+\s*\/>/,skip:!0},{b:/<\w+/,e:/(\/\w+|\w+\/)>/,skip:!0,c:["self"]}]}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:[e.CLCM,e.CBCM]}],i:/\[|%/},{b:/\$[(.]/},e.METHOD_GUARD,{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#(?!!)/}});hljs.registerLanguage("java",function(e){var t=e.UIR+"(<"+e.UIR+"(\\s*,\\s*"+e.UIR+")*>)?",a="false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports",r="\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",s={cN:"number",b:r,r:0};return{aliases:["jsp"],k:a,i:/<\/|#/,c:[e.C("/\\*\\*","\\*/",{r:0,c:[{b:/\w+@/,r:0},{cN:"doctag",b:"@[A-Za-z]+"}]}),e.CLCM,e.CBCM,e.ASM,e.QSM,{cN:"class",bK:"class interface",e:/[{;=]/,eE:!0,k:"class interface",i:/[:"\[\]]/,c:[{bK:"extends implements"},e.UTM]},{bK:"new throw return else",r:0},{cN:"function",b:"("+t+"\\s+)+"+e.UIR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:a,c:[{b:e.UIR+"\\s*\\(",rB:!0,r:0,c:[e.UTM]},{cN:"params",b:/\(/,e:/\)/,k:a,r:0,c:[e.ASM,e.QSM,e.CNM,e.CBCM]},e.CLCM,e.CBCM]},s,{cN:"meta",b:"@[A-Za-z]+"}]}});hljs.registerLanguage("python",function(e){var r={cN:"meta",b:/^(>>>|\.\.\.) /},b={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[r],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[r],r:10},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},e.ASM,e.QSM]},a={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},l={cN:"params",b:/\(/,e:/\)/,c:["self",r,a,b]};return{aliases:["py","gyp"],k:{keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},i:/(<\/|->|\?)/,c:[r,a,b,e.HCM,{v:[{cN:"function",bK:"def",r:10},{cN:"class",bK:"class"}],e:/:/,i:/[${=;\n,]/,c:[e.UTM,l,{b:/->/,eW:!0,k:"None"}]},{cN:"meta",b:/^[\t ]*@/,e:/$/},{b:/\b(print|exec)\(/}]}});hljs.registerLanguage("ini",function(e){var b={cN:"string",c:[e.BE],v:[{b:"'''",e:"'''",r:10},{b:'"""',e:'"""',r:10},{b:'"',e:'"'},{b:"'",e:"'"}]};return{aliases:["toml"],cI:!0,i:/\S/,c:[e.C(";","$"),e.HCM,{cN:"section",b:/^\s*\[+/,e:/\]+/},{b:/^[a-z0-9\[\]_-]+\s*=\s*/,e:"$",rB:!0,c:[{cN:"attr",b:/[a-z0-9\[\]_-]+/},{b:/=/,eW:!0,r:0,c:[{cN:"literal",b:/\bon|off|true|false|yes|no\b/},{cN:"variable",v:[{b:/\$[\w\d"][\w\d_]*/},{b:/\$\{(.*?)}/}]},b,{cN:"number",b:/([\+\-]+)?[\d]+_[\d_]+/},e.NM]}]}]}});hljs.registerLanguage("http",function(e){var t="HTTP/[0-9\\.]+";return{aliases:["https"],i:"\\S",c:[{b:"^"+t,e:"$",c:[{cN:"number",b:"\\b\\d{3}\\b"}]},{b:"^[A-Z]+ (.*?) "+t+"$",rB:!0,e:"$",c:[{cN:"string",b:" ",e:" ",eB:!0,eE:!0},{b:t},{cN:"keyword",b:"[A-Z]+"}]},{cN:"attribute",b:"^\\w",e:": ",eE:!0,i:"\\n|\\s|=",starts:{e:"$",r:0}},{b:"\\n\\n",starts:{sL:[],eW:!0}}]}});hljs.registerLanguage("ruby",function(e){var r="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",b={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},c={cN:"doctag",b:"@[A-Za-z]+"},a={b:"#<",e:">"},s=[e.C("#","$",{c:[c]}),e.C("^\\=begin","^\\=end",{c:[c],r:10}),e.C("^__END__","\\n$")],n={cN:"subst",b:"#\\{",e:"}",k:b},t={cN:"string",c:[e.BE,n],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/`/,e:/`/},{b:"%[qQwWx]?\\(",e:"\\)"},{b:"%[qQwWx]?\\[",e:"\\]"},{b:"%[qQwWx]?{",e:"}"},{b:"%[qQwWx]?<",e:">"},{b:"%[qQwWx]?/",e:"/"},{b:"%[qQwWx]?%",e:"%"},{b:"%[qQwWx]?-",e:"-"},{b:"%[qQwWx]?\\|",e:"\\|"},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/}]},i={cN:"params",b:"\\(",e:"\\)",endsParent:!0,k:b},d=[t,a,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{b:"<\\s*",c:[{b:"("+e.IR+"::)?"+e.IR}]}].concat(s)},{cN:"function",bK:"def",e:"$|;",c:[e.inherit(e.TM,{b:r}),i].concat(s)},{b:e.IR+"::"},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":(?!\\s)",c:[t,{b:r}],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{cN:"params",b:/\|/,e:/\|/,k:b},{b:"("+e.RSR+")\\s*",c:[a,{cN:"regexp",c:[e.BE,n],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}].concat(s),r:0}].concat(s);n.c=d,i.c=d;var l="[>?]>",o="[\\w#]+\\(\\w+\\):\\d+:\\d+>",u="(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>",w=[{b:/^\s*=>/,starts:{e:"$",c:d}},{cN:"meta",b:"^("+l+"|"+o+"|"+u+")",starts:{e:"$",c:d}}];return{aliases:["rb","gemspec","podspec","thor","irb"],k:b,i:/\/\*/,c:s.concat(w).concat(d)}});hljs.registerLanguage("diff",function(e){return{aliases:["patch"],c:[{cN:"meta",r:10,v:[{b:/^@@ +\-\d+,\d+ +\+\d+,\d+ +@@$/},{b:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{b:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{cN:"comment",v:[{b:/Index: /,e:/$/},{b:/={3,}/,e:/$/},{b:/^\-{3}/,e:/$/},{b:/^\*{3} /,e:/$/},{b:/^\+{3}/,e:/$/},{b:/\*{5}/,e:/\*{5}$/}]},{cN:"addition",b:"^\\+",e:"$"},{cN:"deletion",b:"^\\-",e:"$"},{cN:"addition",b:"^\\!",e:"$"}]}});hljs.registerLanguage("less",function(e){var r="[\\w-]+",t="("+r+"|@{"+r+"})",a=[],c=[],s=function(e){return{cN:"string",b:"~?"+e+".*?"+e}},b=function(e,r,t){return{cN:e,b:r,r:t}},n={b:"\\(",e:"\\)",c:c,r:0};c.push(e.CLCM,e.CBCM,s("'"),s('"'),e.CSSNM,{b:"(url|data-uri)\\(",starts:{cN:"string",e:"[\\)\\n]",eE:!0}},b("number","#[0-9A-Fa-f]+\\b"),n,b("variable","@@?"+r,10),b("variable","@{"+r+"}"),b("built_in","~?`[^`]*?`"),{cN:"attribute",b:r+"\\s*:",e:":",rB:!0,eE:!0},{cN:"meta",b:"!important"});var i=c.concat({b:"{",e:"}",c:a}),o={bK:"when",eW:!0,c:[{bK:"and not"}].concat(c)},u={b:t+"\\s*:",rB:!0,e:"[;}]",r:0,c:[{cN:"attribute",b:t,e:":",eE:!0,starts:{eW:!0,i:"[<=$]",r:0,c:c}}]},l={cN:"keyword",b:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",starts:{e:"[;{}]",rE:!0,c:c,r:0}},C={cN:"variable",v:[{b:"@"+r+"\\s*:",r:15},{b:"@"+r}],starts:{e:"[;}]",rE:!0,c:i}},p={v:[{b:"[\\.#:&\\[>]",e:"[;{}]"},{b:t+"[^;]*{",e:"{"}],rB:!0,rE:!0,i:"[<='$\"]",c:[e.CLCM,e.CBCM,o,b("keyword","all\\b"),b("variable","@{"+r+"}"),b("selector-tag",t+"%?",0),b("selector-id","#"+t),b("selector-class","\\."+t,0),b("selector-tag","&",0),{cN:"selector-attr",b:"\\[",e:"\\]"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"\\(",e:"\\)",c:i},{b:"!important"}]};return a.push(e.CLCM,e.CBCM,l,C,u,p),{cI:!0,i:"[=>'/<($\"]",c:a}});hljs.registerLanguage("sql",function(e){var t=e.C("--","$");return{cI:!0,i:/[<>{}*#]/,c:[{bK:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke",e:/;/,eW:!0,l:/[\w\.]+/,k:{keyword:"abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select self sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",literal:"true false null",built_in:"array bigint binary bit blob boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text varchar varying void"},c:[{cN:"string",b:"'",e:"'",c:[e.BE,{b:"''"}]},{cN:"string",b:'"',e:'"',c:[e.BE,{b:'""'}]},{cN:"string",b:"`",e:"`",c:[e.BE]},e.CNM,e.CBCM,t]},e.CBCM,t]}});hljs.registerLanguage("makefile",function(e){var a={cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]};return{aliases:["mk","mak"],c:[e.HCM,{b:/^\w+\s*\W*=/,rB:!0,r:0,starts:{e:/\s*\W*=/,eE:!0,starts:{e:/$/,r:0,c:[a]}}},{cN:"section",b:/^[\w]+:\s*$/},{cN:"meta",b:/^\.PHONY:/,e:/$/,k:{"meta-keyword":".PHONY"},l:/[\.\w]+/},{b:/^\t+/,e:/$/,r:0,c:[e.QSM,a]}]}});hljs.registerLanguage("perl",function(e){var t="getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when",r={cN:"subst",b:"[$@]\\{",e:"\\}",k:t},s={b:"->{",e:"}"},n={v:[{b:/\$\d/},{b:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{b:/[\$%@][^\s\w{]/,r:0}]},i=[e.BE,r,n],o=[n,e.HCM,e.C("^\\=\\w","\\=cut",{eW:!0}),s,{cN:"string",c:i,v:[{b:"q[qwxr]?\\s*\\(",e:"\\)",r:5},{b:"q[qwxr]?\\s*\\[",e:"\\]",r:5},{b:"q[qwxr]?\\s*\\{",e:"\\}",r:5},{b:"q[qwxr]?\\s*\\|",e:"\\|",r:5},{b:"q[qwxr]?\\s*\\<",e:"\\>",r:5},{b:"qw\\s+q",e:"q",r:5},{b:"'",e:"'",c:[e.BE]},{b:'"',e:'"'},{b:"`",e:"`",c:[e.BE]},{b:"{\\w+}",c:[],r:0},{b:"-?\\w+\\s*\\=\\>",c:[],r:0}]},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\/\\/|"+e.RSR+"|\\b(split|return|print|reverse|grep)\\b)\\s*",k:"split return print reverse grep",r:0,c:[e.HCM,{cN:"regexp",b:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",r:10},{cN:"regexp",b:"(m|qr)?/",e:"/[a-z]*",c:[e.BE],r:0}]},{cN:"function",bK:"sub",e:"(\\s*\\(.*?\\))?[;{]",eE:!0,r:5,c:[e.TM]},{b:"-\\w\\b",r:0},{b:"^__DATA__$",e:"^__END__$",sL:"mojolicious",c:[{b:"^@@.*",e:"$",cN:"comment"}]}];return r.c=o,s.c=o,{aliases:["pl","pm"],l:/[\w\.]+/,k:t,c:o}});hljs.registerLanguage("cs",function(e){var i={keyword:"abstract as base bool break byte case catch char checked const continue decimal dynamic default delegate do double else enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long when object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual volatile void while async nameof ascending descending from get group into join let orderby partial select set value var where yield",literal:"null false true"},r={cN:"string",b:'@"',e:'"',c:[{b:'""'}]},t=e.inherit(r,{i:/\n/}),n={cN:"subst",b:"{",e:"}",k:i},c=e.inherit(n,{i:/\n/}),a={cN:"string",b:/\$"/,e:'"',i:/\n/,c:[{b:"{{"},{b:"}}"},e.BE,c]},s={cN:"string",b:/\$@"/,e:'"',c:[{b:"{{"},{b:"}}"},{b:'""'},n]},o=e.inherit(s,{i:/\n/,c:[{b:"{{"},{b:"}}"},{b:'""'},c]});n.c=[s,a,r,e.ASM,e.QSM,e.CNM,e.CBCM],c.c=[o,a,t,e.ASM,e.QSM,e.CNM,e.inherit(e.CBCM,{i:/\n/})];var l={v:[s,a,r,e.ASM,e.QSM]},b=e.IR+"(<"+e.IR+">)?(\\[\\])?";return{aliases:["csharp"],k:i,i:/::/,c:[e.C("///","$",{rB:!0,c:[{cN:"doctag",v:[{b:"///",r:0},{b:""},{b:""}]}]}),e.CLCM,e.CBCM,{cN:"meta",b:"#",e:"$",k:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},l,e.CNM,{bK:"class interface",e:/[{;=]/,i:/[^\s:]/,c:[e.TM,e.CLCM,e.CBCM]},{bK:"namespace",e:/[{;=]/,i:/[^\s:]/,c:[e.inherit(e.TM,{b:"[a-zA-Z](\\.?\\w)*"}),e.CLCM,e.CBCM]},{bK:"new return throw await",r:0},{cN:"function",b:"("+b+"\\s+)+"+e.IR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:i,c:[{b:e.IR+"\\s*\\(",rB:!0,c:[e.TM],r:0},{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,k:i,r:0,c:[l,e.CNM,e.CBCM]},e.CLCM,e.CBCM]}]}}); \ No newline at end of file diff --git a/public/static/libs/ion-rangeslider/css/ion.rangeSlider.css b/public/static/libs/ion-rangeslider/css/ion.rangeSlider.css new file mode 100644 index 0000000..a3c57ff --- /dev/null +++ b/public/static/libs/ion-rangeslider/css/ion.rangeSlider.css @@ -0,0 +1,150 @@ +/* Ion.RangeSlider +// css version 2.0.3 +// © 2013-2014 Denis Ineshin | IonDen.com +// ===================================================================================================================*/ + +/* ===================================================================================================================== +// RangeSlider */ + +.irs { + position: relative; display: block; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + .irs-line { + position: relative; display: block; + overflow: hidden; + outline: none !important; + } + .irs-line-left, .irs-line-mid, .irs-line-right { + position: absolute; display: block; + top: 0; + } + .irs-line-left { + left: 0; width: 11%; + } + .irs-line-mid { + left: 9%; width: 82%; + } + .irs-line-right { + right: 0; width: 11%; + } + + .irs-bar { + position: absolute; display: block; + left: 0; width: 0; + } + .irs-bar-edge { + position: absolute; display: block; + top: 0; left: 0; + } + + .irs-shadow { + position: absolute; display: none; + left: 0; width: 0; + } + + .irs-slider { + position: absolute; display: block; + cursor: default; + z-index: 1; + } + .irs-slider.single { + + } + .irs-slider.from { + + } + .irs-slider.to { + + } + .irs-slider.type_last { + z-index: 2; + } + + .irs-min { + position: absolute; display: block; + left: 0; + cursor: default; + } + .irs-max { + position: absolute; display: block; + right: 0; + cursor: default; + } + + .irs-from, .irs-to, .irs-single { + position: absolute; display: block; + top: 0; left: 0; + cursor: default; + white-space: nowrap; + } + +.irs-grid { + position: absolute; display: none; + bottom: 0; left: 0; + width: 100%; height: 20px; +} +.irs-with-grid .irs-grid { + display: block; +} + .irs-grid-pol { + position: absolute; + top: 0; left: 0; + width: 1px; height: 8px; + background: #000; + } + .irs-grid-pol.small { + height: 4px; + } + .irs-grid-text { + position: absolute; + bottom: 0; left: 0; + white-space: nowrap; + text-align: center; + font-size: 9px; line-height: 9px; + padding: 0 3px; + color: #000; + } + +.irs-disable-mask { + position: absolute; display: block; + top: 0; left: -1%; + width: 102%; height: 100%; + cursor: default; + background: rgba(0,0,0,0.0); + z-index: 2; +} +.lt-ie9 .irs-disable-mask { + background: #000; + filter: alpha(opacity=0); + cursor: not-allowed; +} + +.irs-disabled { + opacity: 0.4; +} + + +.irs-hidden-input { + position: absolute !important; + display: block !important; + top: 0 !important; + left: 0 !important; + width: 0 !important; + height: 0 !important; + font-size: 0 !important; + line-height: 0 !important; + padding: 0 !important; + margin: 0 !important; + overflow: hidden; + outline: none !important; + z-index: -9999 !important; + background: none !important; + border-style: solid !important; + border-color: transparent !important; +} diff --git a/public/static/libs/ion-rangeslider/css/ion.rangeSlider.min.css b/public/static/libs/ion-rangeslider/css/ion.rangeSlider.min.css new file mode 100644 index 0000000..498c04d --- /dev/null +++ b/public/static/libs/ion-rangeslider/css/ion.rangeSlider.min.css @@ -0,0 +1,8 @@ +/* Ion.RangeSlider +// css version 2.1.2 +// © 2013-2014 Denis Ineshin | IonDen.com +// ===================================================================================================================*/ + +/* ===================================================================================================================== +// RangeSlider */ +.irs,.irs-line{position:relative;display:block}.irs,.irs-bar,.irs-bar-edge,.irs-line{display:block}.irs-hidden-input,.irs-line{overflow:hidden;outline:0!important}.irs{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.irs-line-left,.irs-line-mid,.irs-line-right{position:absolute;display:block;top:0}.irs-line-left{left:0;width:11%}.irs-line-mid{left:9%;width:82%}.irs-line-right{right:0;width:11%}.irs-bar,.irs-shadow{position:absolute;width:0;left:0}.irs-bar-edge{position:absolute;top:0;left:0}.irs-shadow{display:none}.irs-from,.irs-max,.irs-min,.irs-single,.irs-slider,.irs-to{display:block;position:absolute;cursor:default}.irs-slider{z-index:1}.irs-slider.type_last{z-index:2}.irs-min{left:0}.irs-max{right:0}.irs-from,.irs-single,.irs-to{top:0;left:0;white-space:nowrap}.irs-grid{position:absolute;display:none;bottom:0;left:0;width:100%;height:20px}.irs-with-grid .irs-grid{display:block}.irs-grid-pol{position:absolute;top:0;left:0;width:1px;height:8px;background:#000}.irs-grid-pol.small{height:4px}.irs-grid-text{position:absolute;bottom:0;left:0;white-space:nowrap;text-align:center;font-size:9px;line-height:9px;padding:0 3px;color:#000}.irs-disable-mask{position:absolute;display:block;top:0;left:-1%;width:102%;height:100%;cursor:default;background:rgba(0,0,0,0);z-index:2}.lt-ie9 .irs-disable-mask{background:#000;filter:alpha(opacity=0);cursor:not-allowed}.irs-disabled{opacity:.4}.irs-hidden-input{position:absolute!important;display:block!important;top:0!important;left:0!important;width:0!important;height:0!important;font-size:0!important;line-height:0!important;padding:0!important;margin:0!important;z-index:-9999!important;background:0 0!important;border-style:solid!important;border-color:transparent!important} \ No newline at end of file diff --git a/public/static/libs/ion-rangeslider/css/ion.rangeSlider.skinHTML5.css b/public/static/libs/ion-rangeslider/css/ion.rangeSlider.skinHTML5.css new file mode 100644 index 0000000..8958cdf --- /dev/null +++ b/public/static/libs/ion-rangeslider/css/ion.rangeSlider.skinHTML5.css @@ -0,0 +1,124 @@ +/* Ion.RangeSlider, Simple Skin +// css version 2.1.2 +// © Denis Ineshin, 2014 https://github.com/IonDen +// © guybowden, 2014 https://github.com/guybowden +// ===================================================================================================================*/ + +/* ===================================================================================================================== +// Skin details */ + +.irs { + height: 55px; +} +.irs-with-grid { + height: 75px; +} +.irs-line { + height: 10px; top: 33px; + background: #EEE; + background: linear-gradient(to bottom, #DDD -50%, #FFF 150%); /* W3C */ + border: 1px solid #CCC; + border-radius: 16px; + -moz-border-radius: 16px; +} + .irs-line-left { + height: 8px; + } + .irs-line-mid { + height: 8px; + } + .irs-line-right { + height: 8px; + } + +.irs-bar { + height: 10px; top: 33px; + border-top: 1px solid #428bca; + border-bottom: 1px solid #428bca; + background: #428bca; + background: linear-gradient(to top, rgba(66,139,202,1) 0%,rgba(127,195,232,1) 100%); /* W3C */ +} + .irs-bar-edge { + height: 10px; top: 33px; + width: 14px; + border: 1px solid #428bca; + border-right: 0; + background: #428bca; + background: linear-gradient(to top, rgba(66,139,202,1) 0%,rgba(127,195,232,1) 100%); /* W3C */ + border-radius: 16px 0 0 16px; + -moz-border-radius: 16px 0 0 16px; + } + +.irs-shadow { + height: 2px; top: 38px; + background: #000; + opacity: 0.3; + border-radius: 5px; + -moz-border-radius: 5px; +} +.lt-ie9 .irs-shadow { + filter: alpha(opacity=30); +} + +.irs-slider { + top: 25px; + width: 27px; height: 27px; + border: 1px solid #AAA; + background: #DDD; + background: linear-gradient(to bottom, rgba(255,255,255,1) 0%,rgba(220,220,220,1) 20%,rgba(255,255,255,1) 100%); /* W3C */ + border-radius: 27px; + -moz-border-radius: 27px; + box-shadow: 1px 1px 3px rgba(0,0,0,0.3); + cursor: pointer; +} + +.irs-slider.state_hover, .irs-slider:hover { + background: #FFF; +} + +.irs-min, .irs-max { + color: #333; + font-size: 12px; line-height: 1.333; + text-shadow: none; + top: 0; + padding: 1px 5px; + background: rgba(0,0,0,0.1); + border-radius: 3px; + -moz-border-radius: 3px; +} + +.lt-ie9 .irs-min, .lt-ie9 .irs-max { + background: #ccc; +} + +.irs-from, .irs-to, .irs-single { + color: #fff; + font-size: 14px; line-height: 1.333; + text-shadow: none; + padding: 1px 5px; + background: #428bca; + border-radius: 3px; + -moz-border-radius: 3px; +} +.lt-ie9 .irs-from, .lt-ie9 .irs-to, .lt-ie9 .irs-single { + background: #999; +} + +.irs-grid { + height: 27px; +} +.irs-grid-pol { + opacity: 0.5; + background: #428bca; +} +.irs-grid-pol.small { + background: #999; +} + +.irs-grid-text { + bottom: 5px; + color: #99a4ac; +} + +.irs-disabled { +} diff --git a/public/static/libs/ion-rangeslider/css/ion.rangeSlider.skinHTML5.min.css b/public/static/libs/ion-rangeslider/css/ion.rangeSlider.skinHTML5.min.css new file mode 100644 index 0000000..da566e5 --- /dev/null +++ b/public/static/libs/ion-rangeslider/css/ion.rangeSlider.skinHTML5.min.css @@ -0,0 +1,9 @@ +/* Ion.RangeSlider, Simple Skin +// css version 2.1.2 +// © Denis Ineshin, 2014 https://github.com/IonDen +// © guybowden, 2014 https://github.com/guybowden +// ===================================================================================================================*/ + +/* ===================================================================================================================== +// Skin details */ +.irs-from,.irs-max,.irs-min,.irs-single,.irs-to{line-height:1.333;text-shadow:none;padding:1px 5px}.irs{height:55px}.irs-with-grid{height:75px}.irs-line{height:10px;top:33px;background:#EEE;background:linear-gradient(to bottom,#DDD -50%,#FFF 150%);border:1px solid #CCC;border-radius:16px;-moz-border-radius:16px}.irs-line-left,.irs-line-mid,.irs-line-right{height:8px}.irs-bar{height:10px;top:33px;border-top:1px solid #428bca;border-bottom:1px solid #428bca;background:#428bca;background:linear-gradient(to top,rgba(66,139,202,1) 0,rgba(127,195,232,1) 100%)}.irs-bar-edge{height:10px;top:33px;width:14px;border:1px solid #428bca;border-right:0;background:#428bca;background:linear-gradient(to top,rgba(66,139,202,1) 0,rgba(127,195,232,1) 100%);border-radius:16px 0 0 16px;-moz-border-radius:16px 0 0 16px}.irs-shadow{height:2px;top:38px;background:#000;opacity:.3;border-radius:5px;-moz-border-radius:5px}.irs-grid,.irs-slider{height:27px}.lt-ie9 .irs-shadow{filter:alpha(opacity=30)}.irs-slider{top:25px;width:27px;border:1px solid #AAA;background:#DDD;background:linear-gradient(to bottom,rgba(255,255,255,1) 0,rgba(220,220,220,1) 20%,rgba(255,255,255,1) 100%);border-radius:27px;-moz-border-radius:27px;box-shadow:1px 1px 3px rgba(0,0,0,.3);cursor:pointer}.irs-slider.state_hover,.irs-slider:hover{background:#FFF}.irs-max,.irs-min{color:#333;font-size:12px;top:0;background:rgba(0,0,0,.1);border-radius:3px;-moz-border-radius:3px}.lt-ie9 .irs-max,.lt-ie9 .irs-min{background:#ccc}.irs-from,.irs-single,.irs-to{color:#fff;font-size:14px;background:#428bca;border-radius:3px;-moz-border-radius:3px}.lt-ie9 .irs-from,.lt-ie9 .irs-single,.lt-ie9 .irs-to{background:#999}.irs-grid-pol{opacity:.5;background:#428bca}.irs-grid-pol.small{background:#999}.irs-grid-text{bottom:5px;color:#99a4ac} \ No newline at end of file diff --git a/public/static/libs/ion-rangeslider/js/ion.rangeSlider.js b/public/static/libs/ion-rangeslider/js/ion.rangeSlider.js new file mode 100644 index 0000000..9b1fde8 --- /dev/null +++ b/public/static/libs/ion-rangeslider/js/ion.rangeSlider.js @@ -0,0 +1,2317 @@ +// Ion.RangeSlider +// version 2.1.4 Build: 355 +// © Denis Ineshin, 2016 +// https://github.com/IonDen +// +// Project page: http://ionden.com/a/plugins/ion.rangeSlider/en.html +// GitHub page: https://github.com/IonDen/ion.rangeSlider +// +// Released under MIT licence: +// http://ionden.com/a/plugins/licence-en.html +// ===================================================================================================================== + +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], function ($) { + factory($, document, window, navigator); + }); + } else { + factory(jQuery, document, window, navigator); + } +} (function ($, document, window, navigator, undefined) { + "use strict"; + + // ================================================================================================================= + // Service + + var plugin_count = 0; + + // IE8 fix + var is_old_ie = (function () { + var n = navigator.userAgent, + r = /msie\s\d+/i, + v; + if (n.search(r) > 0) { + v = r.exec(n).toString(); + v = v.split(" ")[1]; + if (v < 9) { + $("html").addClass("lt-ie9"); + return true; + } + } + return false; + } ()); + if (!Function.prototype.bind) { + Function.prototype.bind = function bind(that) { + + var target = this; + var slice = [].slice; + + if (typeof target != "function") { + throw new TypeError(); + } + + var args = slice.call(arguments, 1), + bound = function () { + + if (this instanceof bound) { + + var F = function(){}; + F.prototype = target.prototype; + var self = new F(); + + var result = target.apply( + self, + args.concat(slice.call(arguments)) + ); + if (Object(result) === result) { + return result; + } + return self; + + } else { + + return target.apply( + that, + args.concat(slice.call(arguments)) + ); + + } + + }; + + return bound; + }; + } + if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function(searchElement, fromIndex) { + var k; + if (this == null) { + throw new TypeError('"this" is null or not defined'); + } + var O = Object(this); + var len = O.length >>> 0; + if (len === 0) { + return -1; + } + var n = +fromIndex || 0; + if (Math.abs(n) === Infinity) { + n = 0; + } + if (n >= len) { + return -1; + } + k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); + while (k < len) { + if (k in O && O[k] === searchElement) { + return k; + } + k++; + } + return -1; + }; + } + + + + // ================================================================================================================= + // Template + + var base_html = + '' + + '' + + '01' + + '000' + + '' + + '' + + ''; + + var single_html = + '' + + '' + + ''; + + var double_html = + '' + + '' + + '' + + ''; + + var disable_html = + ''; + + + + // ================================================================================================================= + // Core + + /** + * Main plugin constructor + * + * @param input {Object} link to base input element + * @param options {Object} slider config + * @param plugin_count {Number} + * @constructor + */ + var IonRangeSlider = function (input, options, plugin_count) { + this.VERSION = "2.1.4"; + this.input = input; + this.plugin_count = plugin_count; + this.current_plugin = 0; + this.calc_count = 0; + this.update_tm = 0; + this.old_from = 0; + this.old_to = 0; + this.old_min_interval = null; + this.raf_id = null; + this.dragging = false; + this.force_redraw = false; + this.no_diapason = false; + this.is_key = false; + this.is_update = false; + this.is_start = true; + this.is_finish = false; + this.is_active = false; + this.is_resize = false; + this.is_click = false; + + // cache for links to all DOM elements + this.$cache = { + win: $(window), + body: $(document.body), + input: $(input), + cont: null, + rs: null, + min: null, + max: null, + from: null, + to: null, + single: null, + bar: null, + line: null, + s_single: null, + s_from: null, + s_to: null, + shad_single: null, + shad_from: null, + shad_to: null, + edge: null, + grid: null, + grid_labels: [] + }; + + // storage for measure variables + this.coords = { + // left + x_gap: 0, + x_pointer: 0, + + // width + w_rs: 0, + w_rs_old: 0, + w_handle: 0, + + // percents + p_gap: 0, + p_gap_left: 0, + p_gap_right: 0, + p_step: 0, + p_pointer: 0, + p_handle: 0, + p_single_fake: 0, + p_single_real: 0, + p_from_fake: 0, + p_from_real: 0, + p_to_fake: 0, + p_to_real: 0, + p_bar_x: 0, + p_bar_w: 0, + + // grid + grid_gap: 0, + big_num: 0, + big: [], + big_w: [], + big_p: [], + big_x: [] + }; + + // storage for labels measure variables + this.labels = { + // width + w_min: 0, + w_max: 0, + w_from: 0, + w_to: 0, + w_single: 0, + + // percents + p_min: 0, + p_max: 0, + p_from_fake: 0, + p_from_left: 0, + p_to_fake: 0, + p_to_left: 0, + p_single_fake: 0, + p_single_left: 0 + }; + + + + /** + * get and validate config + */ + var $inp = this.$cache.input, + val = $inp.prop("value"), + config, config_from_data, prop; + + // default config + config = { + type: "single", + + min: 10, + max: 100, + from: null, + to: null, + step: 1, + + min_interval: 0, + max_interval: 0, + drag_interval: false, + + values: [], + p_values: [], + + from_fixed: false, + from_min: null, + from_max: null, + from_shadow: false, + + to_fixed: false, + to_min: null, + to_max: null, + to_shadow: false, + + prettify_enabled: true, + prettify_separator: " ", + prettify: null, + + force_edges: false, + + keyboard: false, + keyboard_step: 5, + + grid: false, + grid_margin: true, + grid_num: 4, + grid_snap: false, + + hide_min_max: false, + hide_from_to: false, + + prefix: "", + postfix: "", + max_postfix: "", + decorate_both: true, + values_separator: " — ", + + input_values_separator: ";", + + disable: false, + + onStart: null, + onChange: null, + onFinish: null, + onUpdate: null + }; + + + + // config from data-attributes extends js config + config_from_data = { + type: $inp.data("type"), + + min: $inp.data("min"), + max: $inp.data("max"), + from: $inp.data("from"), + to: $inp.data("to"), + step: $inp.data("step"), + + min_interval: $inp.data("minInterval"), + max_interval: $inp.data("maxInterval"), + drag_interval: $inp.data("dragInterval"), + + values: $inp.data("values"), + + from_fixed: $inp.data("fromFixed"), + from_min: $inp.data("fromMin"), + from_max: $inp.data("fromMax"), + from_shadow: $inp.data("fromShadow"), + + to_fixed: $inp.data("toFixed"), + to_min: $inp.data("toMin"), + to_max: $inp.data("toMax"), + to_shadow: $inp.data("toShadow"), + + prettify_enabled: $inp.data("prettifyEnabled"), + prettify_separator: $inp.data("prettifySeparator"), + + force_edges: $inp.data("forceEdges"), + + keyboard: $inp.data("keyboard"), + keyboard_step: $inp.data("keyboardStep"), + + grid: $inp.data("grid"), + grid_margin: $inp.data("gridMargin"), + grid_num: $inp.data("gridNum"), + grid_snap: $inp.data("gridSnap"), + + hide_min_max: $inp.data("hideMinMax"), + hide_from_to: $inp.data("hideFromTo"), + + prefix: $inp.data("prefix"), + postfix: $inp.data("postfix"), + max_postfix: $inp.data("maxPostfix"), + decorate_both: $inp.data("decorateBoth"), + values_separator: $inp.data("valuesSeparator"), + + input_values_separator: $inp.data("inputValuesSeparator"), + + disable: $inp.data("disable") + }; + config_from_data.values = config_from_data.values && config_from_data.values.split(","); + + for (prop in config_from_data) { + if (config_from_data.hasOwnProperty(prop)) { + if (!config_from_data[prop] && config_from_data[prop] !== 0) { + delete config_from_data[prop]; + } + } + } + + + + // input value extends default config + if (val) { + val = val.split(config_from_data.input_values_separator || options.input_values_separator || ";"); + + if (val[0] && val[0] == +val[0]) { + val[0] = +val[0]; + } + if (val[1] && val[1] == +val[1]) { + val[1] = +val[1]; + } + + if (options && options.values && options.values.length) { + config.from = val[0] && options.values.indexOf(val[0]); + config.to = val[1] && options.values.indexOf(val[1]); + } else { + config.from = val[0] && +val[0]; + config.to = val[1] && +val[1]; + } + } + + + + // js config extends default config + $.extend(config, options); + + + // data config extends config + $.extend(config, config_from_data); + this.options = config; + + + + // validate config, to be sure that all data types are correct + this.validate(); + + + + // default result object, returned to callbacks + this.result = { + input: this.$cache.input, + slider: null, + + min: this.options.min, + max: this.options.max, + + from: this.options.from, + from_percent: 0, + from_value: null, + + to: this.options.to, + to_percent: 0, + to_value: null + }; + + + + this.init(); + }; + + IonRangeSlider.prototype = { + + /** + * Starts or updates the plugin instance + * + * @param is_update {boolean} + */ + init: function (is_update) { + this.no_diapason = false; + this.coords.p_step = this.convertToPercent(this.options.step, true); + + this.target = "base"; + + this.toggleInput(); + this.append(); + this.setMinMax(); + + if (is_update) { + this.force_redraw = true; + this.calc(true); + + // callbacks called + this.callOnUpdate(); + } else { + this.force_redraw = true; + this.calc(true); + + // callbacks called + this.callOnStart(); + } + + this.updateScene(); + }, + + /** + * Appends slider template to a DOM + */ + append: function () { + var container_html = ''; + this.$cache.input.before(container_html); + this.$cache.input.prop("readonly", true); + this.$cache.cont = this.$cache.input.prev(); + this.result.slider = this.$cache.cont; + + this.$cache.cont.html(base_html); + this.$cache.rs = this.$cache.cont.find(".irs"); + this.$cache.min = this.$cache.cont.find(".irs-min"); + this.$cache.max = this.$cache.cont.find(".irs-max"); + this.$cache.from = this.$cache.cont.find(".irs-from"); + this.$cache.to = this.$cache.cont.find(".irs-to"); + this.$cache.single = this.$cache.cont.find(".irs-single"); + this.$cache.bar = this.$cache.cont.find(".irs-bar"); + this.$cache.line = this.$cache.cont.find(".irs-line"); + this.$cache.grid = this.$cache.cont.find(".irs-grid"); + + if (this.options.type === "single") { + this.$cache.cont.append(single_html); + this.$cache.edge = this.$cache.cont.find(".irs-bar-edge"); + this.$cache.s_single = this.$cache.cont.find(".single"); + this.$cache.from[0].style.visibility = "hidden"; + this.$cache.to[0].style.visibility = "hidden"; + this.$cache.shad_single = this.$cache.cont.find(".shadow-single"); + } else { + this.$cache.cont.append(double_html); + this.$cache.s_from = this.$cache.cont.find(".from"); + this.$cache.s_to = this.$cache.cont.find(".to"); + this.$cache.shad_from = this.$cache.cont.find(".shadow-from"); + this.$cache.shad_to = this.$cache.cont.find(".shadow-to"); + + this.setTopHandler(); + } + + if (this.options.hide_from_to) { + this.$cache.from[0].style.display = "none"; + this.$cache.to[0].style.display = "none"; + this.$cache.single[0].style.display = "none"; + } + + this.appendGrid(); + + if (this.options.disable) { + this.appendDisableMask(); + this.$cache.input[0].disabled = true; + } else { + this.$cache.cont.removeClass("irs-disabled"); + this.$cache.input[0].disabled = false; + this.bindEvents(); + } + + if (this.options.drag_interval) { + this.$cache.bar[0].style.cursor = "ew-resize"; + } + }, + + /** + * Determine which handler has a priority + * works only for double slider type + */ + setTopHandler: function () { + var min = this.options.min, + max = this.options.max, + from = this.options.from, + to = this.options.to; + + if (from > min && to === max) { + this.$cache.s_from.addClass("type_last"); + } else if (to < max) { + this.$cache.s_to.addClass("type_last"); + } + }, + + /** + * Determine which handles was clicked last + * and which handler should have hover effect + * + * @param target {String} + */ + changeLevel: function (target) { + switch (target) { + case "single": + this.coords.p_gap = this.toFixed(this.coords.p_pointer - this.coords.p_single_fake); + break; + case "from": + this.coords.p_gap = this.toFixed(this.coords.p_pointer - this.coords.p_from_fake); + this.$cache.s_from.addClass("state_hover"); + this.$cache.s_from.addClass("type_last"); + this.$cache.s_to.removeClass("type_last"); + break; + case "to": + this.coords.p_gap = this.toFixed(this.coords.p_pointer - this.coords.p_to_fake); + this.$cache.s_to.addClass("state_hover"); + this.$cache.s_to.addClass("type_last"); + this.$cache.s_from.removeClass("type_last"); + break; + case "both": + this.coords.p_gap_left = this.toFixed(this.coords.p_pointer - this.coords.p_from_fake); + this.coords.p_gap_right = this.toFixed(this.coords.p_to_fake - this.coords.p_pointer); + this.$cache.s_to.removeClass("type_last"); + this.$cache.s_from.removeClass("type_last"); + break; + } + }, + + /** + * Then slider is disabled + * appends extra layer with opacity + */ + appendDisableMask: function () { + this.$cache.cont.append(disable_html); + this.$cache.cont.addClass("irs-disabled"); + }, + + /** + * Remove slider instance + * and ubind all events + */ + remove: function () { + this.$cache.cont.remove(); + this.$cache.cont = null; + + this.$cache.line.off("keydown.irs_" + this.plugin_count); + + this.$cache.body.off("touchmove.irs_" + this.plugin_count); + this.$cache.body.off("mousemove.irs_" + this.plugin_count); + + this.$cache.win.off("touchend.irs_" + this.plugin_count); + this.$cache.win.off("mouseup.irs_" + this.plugin_count); + + if (is_old_ie) { + this.$cache.body.off("mouseup.irs_" + this.plugin_count); + this.$cache.body.off("mouseleave.irs_" + this.plugin_count); + } + + this.$cache.grid_labels = []; + this.coords.big = []; + this.coords.big_w = []; + this.coords.big_p = []; + this.coords.big_x = []; + + cancelAnimationFrame(this.raf_id); + }, + + /** + * bind all slider events + */ + bindEvents: function () { + if (this.no_diapason) { + return; + } + + this.$cache.body.on("touchmove.irs_" + this.plugin_count, this.pointerMove.bind(this)); + this.$cache.body.on("mousemove.irs_" + this.plugin_count, this.pointerMove.bind(this)); + + this.$cache.win.on("touchend.irs_" + this.plugin_count, this.pointerUp.bind(this)); + this.$cache.win.on("mouseup.irs_" + this.plugin_count, this.pointerUp.bind(this)); + + this.$cache.line.on("touchstart.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + this.$cache.line.on("mousedown.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + + if (this.options.drag_interval && this.options.type === "double") { + this.$cache.bar.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "both")); + this.$cache.bar.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "both")); + } else { + this.$cache.bar.on("touchstart.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + this.$cache.bar.on("mousedown.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + } + + if (this.options.type === "single") { + this.$cache.single.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "single")); + this.$cache.s_single.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "single")); + this.$cache.shad_single.on("touchstart.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + + this.$cache.single.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "single")); + this.$cache.s_single.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "single")); + this.$cache.edge.on("mousedown.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + this.$cache.shad_single.on("mousedown.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + } else { + this.$cache.single.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, null)); + this.$cache.single.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, null)); + + this.$cache.from.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "from")); + this.$cache.s_from.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "from")); + this.$cache.to.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "to")); + this.$cache.s_to.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "to")); + this.$cache.shad_from.on("touchstart.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + this.$cache.shad_to.on("touchstart.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + + this.$cache.from.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "from")); + this.$cache.s_from.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "from")); + this.$cache.to.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "to")); + this.$cache.s_to.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "to")); + this.$cache.shad_from.on("mousedown.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + this.$cache.shad_to.on("mousedown.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + } + + if (this.options.keyboard) { + this.$cache.line.on("keydown.irs_" + this.plugin_count, this.key.bind(this, "keyboard")); + } + + if (is_old_ie) { + this.$cache.body.on("mouseup.irs_" + this.plugin_count, this.pointerUp.bind(this)); + this.$cache.body.on("mouseleave.irs_" + this.plugin_count, this.pointerUp.bind(this)); + } + }, + + /** + * Mousemove or touchmove + * only for handlers + * + * @param e {Object} event object + */ + pointerMove: function (e) { + if (!this.dragging) { + return; + } + + var x = e.pageX || e.originalEvent.touches && e.originalEvent.touches[0].pageX; + this.coords.x_pointer = x - this.coords.x_gap; + + this.calc(); + }, + + /** + * Mouseup or touchend + * only for handlers + * + * @param e {Object} event object + */ + pointerUp: function (e) { + if (this.current_plugin !== this.plugin_count) { + return; + } + + if (this.is_active) { + this.is_active = false; + } else { + return; + } + + this.$cache.cont.find(".state_hover").removeClass("state_hover"); + + this.force_redraw = true; + + if (is_old_ie) { + $("*").prop("unselectable", false); + } + + this.updateScene(); + this.restoreOriginalMinInterval(); + + // callbacks call + if ($.contains(this.$cache.cont[0], e.target) || this.dragging) { + this.is_finish = true; + this.callOnFinish(); + } + + this.dragging = false; + }, + + /** + * Mousedown or touchstart + * only for handlers + * + * @param target {String|null} + * @param e {Object} event object + */ + pointerDown: function (target, e) { + e.preventDefault(); + var x = e.pageX || e.originalEvent.touches && e.originalEvent.touches[0].pageX; + if (e.button === 2) { + return; + } + + if (target === "both") { + this.setTempMinInterval(); + } + + if (!target) { + target = this.target; + } + + this.current_plugin = this.plugin_count; + this.target = target; + + this.is_active = true; + this.dragging = true; + + this.coords.x_gap = this.$cache.rs.offset().left; + this.coords.x_pointer = x - this.coords.x_gap; + + this.calcPointerPercent(); + this.changeLevel(target); + + if (is_old_ie) { + $("*").prop("unselectable", true); + } + + this.$cache.line.trigger("focus"); + + this.updateScene(); + }, + + /** + * Mousedown or touchstart + * for other slider elements, like diapason line + * + * @param target {String} + * @param e {Object} event object + */ + pointerClick: function (target, e) { + e.preventDefault(); + var x = e.pageX || e.originalEvent.touches && e.originalEvent.touches[0].pageX; + if (e.button === 2) { + return; + } + + this.current_plugin = this.plugin_count; + this.target = target; + + this.is_click = true; + this.coords.x_gap = this.$cache.rs.offset().left; + this.coords.x_pointer = +(x - this.coords.x_gap).toFixed(); + + this.force_redraw = true; + this.calc(); + + this.$cache.line.trigger("focus"); + }, + + /** + * Keyborard controls for focused slider + * + * @param target {String} + * @param e {Object} event object + * @returns {boolean|undefined} + */ + key: function (target, e) { + if (this.current_plugin !== this.plugin_count || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { + return; + } + + switch (e.which) { + case 83: // W + case 65: // A + case 40: // DOWN + case 37: // LEFT + e.preventDefault(); + this.moveByKey(false); + break; + + case 87: // S + case 68: // D + case 38: // UP + case 39: // RIGHT + e.preventDefault(); + this.moveByKey(true); + break; + } + + return true; + }, + + /** + * Move by key. Beta + * @todo refactor than have plenty of time + * + * @param right {boolean} direction to move + */ + moveByKey: function (right) { + var p = this.coords.p_pointer; + + if (right) { + p += this.options.keyboard_step; + } else { + p -= this.options.keyboard_step; + } + + this.coords.x_pointer = this.toFixed(this.coords.w_rs / 100 * p); + this.is_key = true; + this.calc(); + }, + + /** + * Set visibility and content + * of Min and Max labels + */ + setMinMax: function () { + if (!this.options) { + return; + } + + if (this.options.hide_min_max) { + this.$cache.min[0].style.display = "none"; + this.$cache.max[0].style.display = "none"; + return; + } + + if (this.options.values.length) { + this.$cache.min.html(this.decorate(this.options.p_values[this.options.min])); + this.$cache.max.html(this.decorate(this.options.p_values[this.options.max])); + } else { + this.$cache.min.html(this.decorate(this._prettify(this.options.min), this.options.min)); + this.$cache.max.html(this.decorate(this._prettify(this.options.max), this.options.max)); + } + + this.labels.w_min = this.$cache.min.outerWidth(false); + this.labels.w_max = this.$cache.max.outerWidth(false); + }, + + /** + * Then dragging interval, prevent interval collapsing + * using min_interval option + */ + setTempMinInterval: function () { + var interval = this.result.to - this.result.from; + + if (this.old_min_interval === null) { + this.old_min_interval = this.options.min_interval; + } + + this.options.min_interval = interval; + }, + + /** + * Restore min_interval option to original + */ + restoreOriginalMinInterval: function () { + if (this.old_min_interval !== null) { + this.options.min_interval = this.old_min_interval; + this.old_min_interval = null; + } + }, + + + + // ============================================================================================================= + // Calculations + + /** + * All calculations and measures start here + * + * @param update {boolean=} + */ + calc: function (update) { + if (!this.options) { + return; + } + + this.calc_count++; + + if (this.calc_count === 10 || update) { + this.calc_count = 0; + this.coords.w_rs = this.$cache.rs.outerWidth(false); + + this.calcHandlePercent(); + } + + if (!this.coords.w_rs) { + return; + } + + this.calcPointerPercent(); + var handle_x = this.getHandleX(); + + if (this.target === "click") { + this.coords.p_gap = this.coords.p_handle / 2; + handle_x = this.getHandleX(); + + if (this.options.drag_interval) { + this.target = "both_one"; + } else { + this.target = this.chooseHandle(handle_x); + } + } + + switch (this.target) { + case "base": + var w = (this.options.max - this.options.min) / 100, + f = (this.result.from - this.options.min) / w, + t = (this.result.to - this.options.min) / w; + + this.coords.p_single_real = this.toFixed(f); + this.coords.p_from_real = this.toFixed(f); + this.coords.p_to_real = this.toFixed(t); + + this.coords.p_single_real = this.checkDiapason(this.coords.p_single_real, this.options.from_min, this.options.from_max); + this.coords.p_from_real = this.checkDiapason(this.coords.p_from_real, this.options.from_min, this.options.from_max); + this.coords.p_to_real = this.checkDiapason(this.coords.p_to_real, this.options.to_min, this.options.to_max); + + this.coords.p_single_fake = this.convertToFakePercent(this.coords.p_single_real); + this.coords.p_from_fake = this.convertToFakePercent(this.coords.p_from_real); + this.coords.p_to_fake = this.convertToFakePercent(this.coords.p_to_real); + + this.target = null; + + break; + + case "single": + if (this.options.from_fixed) { + break; + } + + this.coords.p_single_real = this.convertToRealPercent(handle_x); + this.coords.p_single_real = this.calcWithStep(this.coords.p_single_real); + this.coords.p_single_real = this.checkDiapason(this.coords.p_single_real, this.options.from_min, this.options.from_max); + + this.coords.p_single_fake = this.convertToFakePercent(this.coords.p_single_real); + + break; + + case "from": + if (this.options.from_fixed) { + break; + } + + this.coords.p_from_real = this.convertToRealPercent(handle_x); + this.coords.p_from_real = this.calcWithStep(this.coords.p_from_real); + if (this.coords.p_from_real > this.coords.p_to_real) { + this.coords.p_from_real = this.coords.p_to_real; + } + this.coords.p_from_real = this.checkDiapason(this.coords.p_from_real, this.options.from_min, this.options.from_max); + this.coords.p_from_real = this.checkMinInterval(this.coords.p_from_real, this.coords.p_to_real, "from"); + this.coords.p_from_real = this.checkMaxInterval(this.coords.p_from_real, this.coords.p_to_real, "from"); + + this.coords.p_from_fake = this.convertToFakePercent(this.coords.p_from_real); + + break; + + case "to": + if (this.options.to_fixed) { + break; + } + + this.coords.p_to_real = this.convertToRealPercent(handle_x); + this.coords.p_to_real = this.calcWithStep(this.coords.p_to_real); + if (this.coords.p_to_real < this.coords.p_from_real) { + this.coords.p_to_real = this.coords.p_from_real; + } + this.coords.p_to_real = this.checkDiapason(this.coords.p_to_real, this.options.to_min, this.options.to_max); + this.coords.p_to_real = this.checkMinInterval(this.coords.p_to_real, this.coords.p_from_real, "to"); + this.coords.p_to_real = this.checkMaxInterval(this.coords.p_to_real, this.coords.p_from_real, "to"); + + this.coords.p_to_fake = this.convertToFakePercent(this.coords.p_to_real); + + break; + + case "both": + if (this.options.from_fixed || this.options.to_fixed) { + break; + } + + handle_x = this.toFixed(handle_x + (this.coords.p_handle * 0.1)); + + this.coords.p_from_real = this.convertToRealPercent(handle_x) - this.coords.p_gap_left; + this.coords.p_from_real = this.calcWithStep(this.coords.p_from_real); + this.coords.p_from_real = this.checkDiapason(this.coords.p_from_real, this.options.from_min, this.options.from_max); + this.coords.p_from_real = this.checkMinInterval(this.coords.p_from_real, this.coords.p_to_real, "from"); + this.coords.p_from_fake = this.convertToFakePercent(this.coords.p_from_real); + + this.coords.p_to_real = this.convertToRealPercent(handle_x) + this.coords.p_gap_right; + this.coords.p_to_real = this.calcWithStep(this.coords.p_to_real); + this.coords.p_to_real = this.checkDiapason(this.coords.p_to_real, this.options.to_min, this.options.to_max); + this.coords.p_to_real = this.checkMinInterval(this.coords.p_to_real, this.coords.p_from_real, "to"); + this.coords.p_to_fake = this.convertToFakePercent(this.coords.p_to_real); + + break; + + case "both_one": + if (this.options.from_fixed || this.options.to_fixed) { + break; + } + + var real_x = this.convertToRealPercent(handle_x), + from = this.result.from_percent, + to = this.result.to_percent, + full = to - from, + half = full / 2, + new_from = real_x - half, + new_to = real_x + half; + + if (new_from < 0) { + new_from = 0; + new_to = new_from + full; + } + + if (new_to > 100) { + new_to = 100; + new_from = new_to - full; + } + + this.coords.p_from_real = this.calcWithStep(new_from); + this.coords.p_from_real = this.checkDiapason(this.coords.p_from_real, this.options.from_min, this.options.from_max); + this.coords.p_from_fake = this.convertToFakePercent(this.coords.p_from_real); + + this.coords.p_to_real = this.calcWithStep(new_to); + this.coords.p_to_real = this.checkDiapason(this.coords.p_to_real, this.options.to_min, this.options.to_max); + this.coords.p_to_fake = this.convertToFakePercent(this.coords.p_to_real); + + break; + } + + if (this.options.type === "single") { + this.coords.p_bar_x = (this.coords.p_handle / 2); + this.coords.p_bar_w = this.coords.p_single_fake; + + this.result.from_percent = this.coords.p_single_real; + this.result.from = this.convertToValue(this.coords.p_single_real); + + if (this.options.values.length) { + this.result.from_value = this.options.values[this.result.from]; + } + } else { + this.coords.p_bar_x = this.toFixed(this.coords.p_from_fake + (this.coords.p_handle / 2)); + this.coords.p_bar_w = this.toFixed(this.coords.p_to_fake - this.coords.p_from_fake); + + this.result.from_percent = this.coords.p_from_real; + this.result.from = this.convertToValue(this.coords.p_from_real); + this.result.to_percent = this.coords.p_to_real; + this.result.to = this.convertToValue(this.coords.p_to_real); + + if (this.options.values.length) { + this.result.from_value = this.options.values[this.result.from]; + this.result.to_value = this.options.values[this.result.to]; + } + } + + this.calcMinMax(); + this.calcLabels(); + }, + + + /** + * calculates pointer X in percent + */ + calcPointerPercent: function () { + if (!this.coords.w_rs) { + this.coords.p_pointer = 0; + return; + } + + if (this.coords.x_pointer < 0 || isNaN(this.coords.x_pointer) ) { + this.coords.x_pointer = 0; + } else if (this.coords.x_pointer > this.coords.w_rs) { + this.coords.x_pointer = this.coords.w_rs; + } + + this.coords.p_pointer = this.toFixed(this.coords.x_pointer / this.coords.w_rs * 100); + }, + + convertToRealPercent: function (fake) { + var full = 100 - this.coords.p_handle; + return fake / full * 100; + }, + + convertToFakePercent: function (real) { + var full = 100 - this.coords.p_handle; + return real / 100 * full; + }, + + getHandleX: function () { + var max = 100 - this.coords.p_handle, + x = this.toFixed(this.coords.p_pointer - this.coords.p_gap); + + if (x < 0) { + x = 0; + } else if (x > max) { + x = max; + } + + return x; + }, + + calcHandlePercent: function () { + if (this.options.type === "single") { + this.coords.w_handle = this.$cache.s_single.outerWidth(false); + } else { + this.coords.w_handle = this.$cache.s_from.outerWidth(false); + } + + this.coords.p_handle = this.toFixed(this.coords.w_handle / this.coords.w_rs * 100); + }, + + /** + * Find closest handle to pointer click + * + * @param real_x {Number} + * @returns {String} + */ + chooseHandle: function (real_x) { + if (this.options.type === "single") { + return "single"; + } else { + var m_point = this.coords.p_from_real + ((this.coords.p_to_real - this.coords.p_from_real) / 2); + if (real_x >= m_point) { + return this.options.to_fixed ? "from" : "to"; + } else { + return this.options.from_fixed ? "to" : "from"; + } + } + }, + + /** + * Measure Min and Max labels width in percent + */ + calcMinMax: function () { + if (!this.coords.w_rs) { + return; + } + + this.labels.p_min = this.labels.w_min / this.coords.w_rs * 100; + this.labels.p_max = this.labels.w_max / this.coords.w_rs * 100; + }, + + /** + * Measure labels width and X in percent + */ + calcLabels: function () { + if (!this.coords.w_rs || this.options.hide_from_to) { + return; + } + + if (this.options.type === "single") { + + this.labels.w_single = this.$cache.single.outerWidth(false); + this.labels.p_single_fake = this.labels.w_single / this.coords.w_rs * 100; + this.labels.p_single_left = this.coords.p_single_fake + (this.coords.p_handle / 2) - (this.labels.p_single_fake / 2); + this.labels.p_single_left = this.checkEdges(this.labels.p_single_left, this.labels.p_single_fake); + + } else { + + this.labels.w_from = this.$cache.from.outerWidth(false); + this.labels.p_from_fake = this.labels.w_from / this.coords.w_rs * 100; + this.labels.p_from_left = this.coords.p_from_fake + (this.coords.p_handle / 2) - (this.labels.p_from_fake / 2); + this.labels.p_from_left = this.toFixed(this.labels.p_from_left); + this.labels.p_from_left = this.checkEdges(this.labels.p_from_left, this.labels.p_from_fake); + + this.labels.w_to = this.$cache.to.outerWidth(false); + this.labels.p_to_fake = this.labels.w_to / this.coords.w_rs * 100; + this.labels.p_to_left = this.coords.p_to_fake + (this.coords.p_handle / 2) - (this.labels.p_to_fake / 2); + this.labels.p_to_left = this.toFixed(this.labels.p_to_left); + this.labels.p_to_left = this.checkEdges(this.labels.p_to_left, this.labels.p_to_fake); + + this.labels.w_single = this.$cache.single.outerWidth(false); + this.labels.p_single_fake = this.labels.w_single / this.coords.w_rs * 100; + this.labels.p_single_left = ((this.labels.p_from_left + this.labels.p_to_left + this.labels.p_to_fake) / 2) - (this.labels.p_single_fake / 2); + this.labels.p_single_left = this.toFixed(this.labels.p_single_left); + this.labels.p_single_left = this.checkEdges(this.labels.p_single_left, this.labels.p_single_fake); + + } + }, + + + + // ============================================================================================================= + // Drawings + + /** + * Main function called in request animation frame + * to update everything + */ + updateScene: function () { + if (this.raf_id) { + cancelAnimationFrame(this.raf_id); + this.raf_id = null; + } + + clearTimeout(this.update_tm); + this.update_tm = null; + + if (!this.options) { + return; + } + + this.drawHandles(); + + if (this.is_active) { + this.raf_id = requestAnimationFrame(this.updateScene.bind(this)); + } else { + this.update_tm = setTimeout(this.updateScene.bind(this), 300); + } + }, + + /** + * Draw handles + */ + drawHandles: function () { + this.coords.w_rs = this.$cache.rs.outerWidth(false); + + if (!this.coords.w_rs) { + return; + } + + if (this.coords.w_rs !== this.coords.w_rs_old) { + this.target = "base"; + this.is_resize = true; + } + + if (this.coords.w_rs !== this.coords.w_rs_old || this.force_redraw) { + this.setMinMax(); + this.calc(true); + this.drawLabels(); + if (this.options.grid) { + this.calcGridMargin(); + this.calcGridLabels(); + } + this.force_redraw = true; + this.coords.w_rs_old = this.coords.w_rs; + this.drawShadow(); + } + + if (!this.coords.w_rs) { + return; + } + + if (!this.dragging && !this.force_redraw && !this.is_key) { + return; + } + + if (this.old_from !== this.result.from || this.old_to !== this.result.to || this.force_redraw || this.is_key) { + + this.drawLabels(); + + this.$cache.bar[0].style.left = this.coords.p_bar_x + "%"; + this.$cache.bar[0].style.width = this.coords.p_bar_w + "%"; + + if (this.options.type === "single") { + this.$cache.s_single[0].style.left = this.coords.p_single_fake + "%"; + + this.$cache.single[0].style.left = this.labels.p_single_left + "%"; + + if (this.options.values.length) { + this.$cache.input.prop("value", this.result.from_value); + } else { + this.$cache.input.prop("value", this.result.from); + } + this.$cache.input.data("from", this.result.from); + } else { + this.$cache.s_from[0].style.left = this.coords.p_from_fake + "%"; + this.$cache.s_to[0].style.left = this.coords.p_to_fake + "%"; + + if (this.old_from !== this.result.from || this.force_redraw) { + this.$cache.from[0].style.left = this.labels.p_from_left + "%"; + } + if (this.old_to !== this.result.to || this.force_redraw) { + this.$cache.to[0].style.left = this.labels.p_to_left + "%"; + } + + this.$cache.single[0].style.left = this.labels.p_single_left + "%"; + + if (this.options.values.length) { + this.$cache.input.prop("value", this.result.from_value + this.options.input_values_separator + this.result.to_value); + } else { + this.$cache.input.prop("value", this.result.from + this.options.input_values_separator + this.result.to); + } + this.$cache.input.data("from", this.result.from); + this.$cache.input.data("to", this.result.to); + } + + if ((this.old_from !== this.result.from || this.old_to !== this.result.to) && !this.is_start) { + this.$cache.input.trigger("change"); + } + + this.old_from = this.result.from; + this.old_to = this.result.to; + + // callbacks call + if (!this.is_resize && !this.is_update && !this.is_start && !this.is_finish) { + this.callOnChange(); + } + if (this.is_key || this.is_click) { + this.is_key = false; + this.is_click = false; + this.callOnFinish(); + } + + this.is_update = false; + this.is_resize = false; + this.is_finish = false; + } + + this.is_start = false; + this.is_key = false; + this.is_click = false; + this.force_redraw = false; + }, + + /** + * Draw labels + * measure labels collisions + * collapse close labels + */ + drawLabels: function () { + if (!this.options) { + return; + } + + var values_num = this.options.values.length, + p_values = this.options.p_values, + text_single, + text_from, + text_to; + + if (this.options.hide_from_to) { + return; + } + + if (this.options.type === "single") { + + if (values_num) { + text_single = this.decorate(p_values[this.result.from]); + this.$cache.single.html(text_single); + } else { + text_single = this.decorate(this._prettify(this.result.from), this.result.from); + this.$cache.single.html(text_single); + } + + this.calcLabels(); + + if (this.labels.p_single_left < this.labels.p_min + 1) { + this.$cache.min[0].style.visibility = "hidden"; + } else { + this.$cache.min[0].style.visibility = "visible"; + } + + if (this.labels.p_single_left + this.labels.p_single_fake > 100 - this.labels.p_max - 1) { + this.$cache.max[0].style.visibility = "hidden"; + } else { + this.$cache.max[0].style.visibility = "visible"; + } + + } else { + + if (values_num) { + + if (this.options.decorate_both) { + text_single = this.decorate(p_values[this.result.from]); + text_single += this.options.values_separator; + text_single += this.decorate(p_values[this.result.to]); + } else { + text_single = this.decorate(p_values[this.result.from] + this.options.values_separator + p_values[this.result.to]); + } + text_from = this.decorate(p_values[this.result.from]); + text_to = this.decorate(p_values[this.result.to]); + + this.$cache.single.html(text_single); + this.$cache.from.html(text_from); + this.$cache.to.html(text_to); + + } else { + + if (this.options.decorate_both) { + text_single = this.decorate(this._prettify(this.result.from), this.result.from); + text_single += this.options.values_separator; + text_single += this.decorate(this._prettify(this.result.to), this.result.to); + } else { + text_single = this.decorate(this._prettify(this.result.from) + this.options.values_separator + this._prettify(this.result.to), this.result.to); + } + text_from = this.decorate(this._prettify(this.result.from), this.result.from); + text_to = this.decorate(this._prettify(this.result.to), this.result.to); + + this.$cache.single.html(text_single); + this.$cache.from.html(text_from); + this.$cache.to.html(text_to); + + } + + this.calcLabels(); + + var min = Math.min(this.labels.p_single_left, this.labels.p_from_left), + single_left = this.labels.p_single_left + this.labels.p_single_fake, + to_left = this.labels.p_to_left + this.labels.p_to_fake, + max = Math.max(single_left, to_left); + + if (this.labels.p_from_left + this.labels.p_from_fake >= this.labels.p_to_left) { + this.$cache.from[0].style.visibility = "hidden"; + this.$cache.to[0].style.visibility = "hidden"; + this.$cache.single[0].style.visibility = "visible"; + + if (this.result.from === this.result.to) { + if (this.target === "from") { + this.$cache.from[0].style.visibility = "visible"; + } else if (this.target === "to") { + this.$cache.to[0].style.visibility = "visible"; + } else if (!this.target) { + this.$cache.from[0].style.visibility = "visible"; + } + this.$cache.single[0].style.visibility = "hidden"; + max = to_left; + } else { + this.$cache.from[0].style.visibility = "hidden"; + this.$cache.to[0].style.visibility = "hidden"; + this.$cache.single[0].style.visibility = "visible"; + max = Math.max(single_left, to_left); + } + } else { + this.$cache.from[0].style.visibility = "visible"; + this.$cache.to[0].style.visibility = "visible"; + this.$cache.single[0].style.visibility = "hidden"; + } + + if (min < this.labels.p_min + 1) { + this.$cache.min[0].style.visibility = "hidden"; + } else { + this.$cache.min[0].style.visibility = "visible"; + } + + if (max > 100 - this.labels.p_max - 1) { + this.$cache.max[0].style.visibility = "hidden"; + } else { + this.$cache.max[0].style.visibility = "visible"; + } + + } + }, + + /** + * Draw shadow intervals + */ + drawShadow: function () { + var o = this.options, + c = this.$cache, + + is_from_min = typeof o.from_min === "number" && !isNaN(o.from_min), + is_from_max = typeof o.from_max === "number" && !isNaN(o.from_max), + is_to_min = typeof o.to_min === "number" && !isNaN(o.to_min), + is_to_max = typeof o.to_max === "number" && !isNaN(o.to_max), + + from_min, + from_max, + to_min, + to_max; + + if (o.type === "single") { + if (o.from_shadow && (is_from_min || is_from_max)) { + from_min = this.convertToPercent(is_from_min ? o.from_min : o.min); + from_max = this.convertToPercent(is_from_max ? o.from_max : o.max) - from_min; + from_min = this.toFixed(from_min - (this.coords.p_handle / 100 * from_min)); + from_max = this.toFixed(from_max - (this.coords.p_handle / 100 * from_max)); + from_min = from_min + (this.coords.p_handle / 2); + + c.shad_single[0].style.display = "block"; + c.shad_single[0].style.left = from_min + "%"; + c.shad_single[0].style.width = from_max + "%"; + } else { + c.shad_single[0].style.display = "none"; + } + } else { + if (o.from_shadow && (is_from_min || is_from_max)) { + from_min = this.convertToPercent(is_from_min ? o.from_min : o.min); + from_max = this.convertToPercent(is_from_max ? o.from_max : o.max) - from_min; + from_min = this.toFixed(from_min - (this.coords.p_handle / 100 * from_min)); + from_max = this.toFixed(from_max - (this.coords.p_handle / 100 * from_max)); + from_min = from_min + (this.coords.p_handle / 2); + + c.shad_from[0].style.display = "block"; + c.shad_from[0].style.left = from_min + "%"; + c.shad_from[0].style.width = from_max + "%"; + } else { + c.shad_from[0].style.display = "none"; + } + + if (o.to_shadow && (is_to_min || is_to_max)) { + to_min = this.convertToPercent(is_to_min ? o.to_min : o.min); + to_max = this.convertToPercent(is_to_max ? o.to_max : o.max) - to_min; + to_min = this.toFixed(to_min - (this.coords.p_handle / 100 * to_min)); + to_max = this.toFixed(to_max - (this.coords.p_handle / 100 * to_max)); + to_min = to_min + (this.coords.p_handle / 2); + + c.shad_to[0].style.display = "block"; + c.shad_to[0].style.left = to_min + "%"; + c.shad_to[0].style.width = to_max + "%"; + } else { + c.shad_to[0].style.display = "none"; + } + } + }, + + + + // ============================================================================================================= + // Callbacks + + callOnStart: function () { + if (this.options.onStart && typeof this.options.onStart === "function") { + this.options.onStart(this.result); + } + }, + callOnChange: function () { + if (this.options.onChange && typeof this.options.onChange === "function") { + this.options.onChange(this.result); + } + }, + callOnFinish: function () { + if (this.options.onFinish && typeof this.options.onFinish === "function") { + this.options.onFinish(this.result); + } + }, + callOnUpdate: function () { + if (this.options.onUpdate && typeof this.options.onUpdate === "function") { + this.options.onUpdate(this.result); + } + }, + + + + // ============================================================================================================= + // Service methods + + toggleInput: function () { + this.$cache.input.toggleClass("irs-hidden-input"); + }, + + /** + * Convert real value to percent + * + * @param value {Number} X in real + * @param no_min {boolean=} don't use min value + * @returns {Number} X in percent + */ + convertToPercent: function (value, no_min) { + var diapason = this.options.max - this.options.min, + one_percent = diapason / 100, + val, percent; + + if (!diapason) { + this.no_diapason = true; + return 0; + } + + if (no_min) { + val = value; + } else { + val = value - this.options.min; + } + + percent = val / one_percent; + + return this.toFixed(percent); + }, + + /** + * Convert percent to real values + * + * @param percent {Number} X in percent + * @returns {Number} X in real + */ + convertToValue: function (percent) { + var min = this.options.min, + max = this.options.max, + min_decimals = min.toString().split(".")[1], + max_decimals = max.toString().split(".")[1], + min_length, max_length, + avg_decimals = 0, + abs = 0; + + if (percent === 0) { + return this.options.min; + } + if (percent === 100) { + return this.options.max; + } + + + if (min_decimals) { + min_length = min_decimals.length; + avg_decimals = min_length; + } + if (max_decimals) { + max_length = max_decimals.length; + avg_decimals = max_length; + } + if (min_length && max_length) { + avg_decimals = (min_length >= max_length) ? min_length : max_length; + } + + if (min < 0) { + abs = Math.abs(min); + min = +(min + abs).toFixed(avg_decimals); + max = +(max + abs).toFixed(avg_decimals); + } + + var number = ((max - min) / 100 * percent) + min, + string = this.options.step.toString().split(".")[1], + result; + + if (string) { + number = +number.toFixed(string.length); + } else { + number = number / this.options.step; + number = number * this.options.step; + + number = +number.toFixed(0); + } + + if (abs) { + number -= abs; + } + + if (string) { + result = +number.toFixed(string.length); + } else { + result = this.toFixed(number); + } + + if (result < this.options.min) { + result = this.options.min; + } else if (result > this.options.max) { + result = this.options.max; + } + + return result; + }, + + /** + * Round percent value with step + * + * @param percent {Number} + * @returns percent {Number} rounded + */ + calcWithStep: function (percent) { + var rounded = Math.round(percent / this.coords.p_step) * this.coords.p_step; + + if (rounded > 100) { + rounded = 100; + } + if (percent === 100) { + rounded = 100; + } + + return this.toFixed(rounded); + }, + + checkMinInterval: function (p_current, p_next, type) { + var o = this.options, + current, + next; + + if (!o.min_interval) { + return p_current; + } + + current = this.convertToValue(p_current); + next = this.convertToValue(p_next); + + if (type === "from") { + + if (next - current < o.min_interval) { + current = next - o.min_interval; + } + + } else { + + if (current - next < o.min_interval) { + current = next + o.min_interval; + } + + } + + return this.convertToPercent(current); + }, + + checkMaxInterval: function (p_current, p_next, type) { + var o = this.options, + current, + next; + + if (!o.max_interval) { + return p_current; + } + + current = this.convertToValue(p_current); + next = this.convertToValue(p_next); + + if (type === "from") { + + if (next - current > o.max_interval) { + current = next - o.max_interval; + } + + } else { + + if (current - next > o.max_interval) { + current = next + o.max_interval; + } + + } + + return this.convertToPercent(current); + }, + + checkDiapason: function (p_num, min, max) { + var num = this.convertToValue(p_num), + o = this.options; + + if (typeof min !== "number") { + min = o.min; + } + + if (typeof max !== "number") { + max = o.max; + } + + if (num < min) { + num = min; + } + + if (num > max) { + num = max; + } + + return this.convertToPercent(num); + }, + + toFixed: function (num) { + num = num.toFixed(9); + return +num; + }, + + _prettify: function (num) { + if (!this.options.prettify_enabled) { + return num; + } + + if (this.options.prettify && typeof this.options.prettify === "function") { + return this.options.prettify(num); + } else { + return this.prettify(num); + } + }, + + prettify: function (num) { + var n = num.toString(); + return n.replace(/(\d{1,3}(?=(?:\d\d\d)+(?!\d)))/g, "$1" + this.options.prettify_separator); + }, + + checkEdges: function (left, width) { + if (!this.options.force_edges) { + return this.toFixed(left); + } + + if (left < 0) { + left = 0; + } else if (left > 100 - width) { + left = 100 - width; + } + + return this.toFixed(left); + }, + + validate: function () { + var o = this.options, + r = this.result, + v = o.values, + vl = v.length, + value, + i; + + if (typeof o.min === "string") o.min = +o.min; + if (typeof o.max === "string") o.max = +o.max; + if (typeof o.from === "string") o.from = +o.from; + if (typeof o.to === "string") o.to = +o.to; + if (typeof o.step === "string") o.step = +o.step; + + if (typeof o.from_min === "string") o.from_min = +o.from_min; + if (typeof o.from_max === "string") o.from_max = +o.from_max; + if (typeof o.to_min === "string") o.to_min = +o.to_min; + if (typeof o.to_max === "string") o.to_max = +o.to_max; + + if (typeof o.keyboard_step === "string") o.keyboard_step = +o.keyboard_step; + if (typeof o.grid_num === "string") o.grid_num = +o.grid_num; + + if (o.max < o.min) { + o.max = o.min; + } + + if (vl) { + o.p_values = []; + o.min = 0; + o.max = vl - 1; + o.step = 1; + o.grid_num = o.max; + o.grid_snap = true; + + + for (i = 0; i < vl; i++) { + value = +v[i]; + + if (!isNaN(value)) { + v[i] = value; + value = this._prettify(value); + } else { + value = v[i]; + } + + o.p_values.push(value); + } + } + + if (typeof o.from !== "number" || isNaN(o.from)) { + o.from = o.min; + } + + if (typeof o.to !== "number" || isNaN(o.from)) { + o.to = o.max; + } + + if (o.type === "single") { + + if (o.from < o.min) { + o.from = o.min; + } + + if (o.from > o.max) { + o.from = o.max; + } + + } else { + + if (o.from < o.min || o.from > o.max) { + o.from = o.min; + } + if (o.to > o.max || o.to < o.min) { + o.to = o.max; + } + if (o.from > o.to) { + o.from = o.to; + } + + } + + if (typeof o.step !== "number" || isNaN(o.step) || !o.step || o.step < 0) { + o.step = 1; + } + + if (typeof o.keyboard_step !== "number" || isNaN(o.keyboard_step) || !o.keyboard_step || o.keyboard_step < 0) { + o.keyboard_step = 5; + } + + if (typeof o.from_min === "number" && o.from < o.from_min) { + o.from = o.from_min; + } + + if (typeof o.from_max === "number" && o.from > o.from_max) { + o.from = o.from_max; + } + + if (typeof o.to_min === "number" && o.to < o.to_min) { + o.to = o.to_min; + } + + if (typeof o.to_max === "number" && o.from > o.to_max) { + o.to = o.to_max; + } + + if (r) { + if (r.min !== o.min) { + r.min = o.min; + } + + if (r.max !== o.max) { + r.max = o.max; + } + + if (r.from < r.min || r.from > r.max) { + r.from = o.from; + } + + if (r.to < r.min || r.to > r.max) { + r.to = o.to; + } + } + + if (typeof o.min_interval !== "number" || isNaN(o.min_interval) || !o.min_interval || o.min_interval < 0) { + o.min_interval = 0; + } + + if (typeof o.max_interval !== "number" || isNaN(o.max_interval) || !o.max_interval || o.max_interval < 0) { + o.max_interval = 0; + } + + if (o.min_interval && o.min_interval > o.max - o.min) { + o.min_interval = o.max - o.min; + } + + if (o.max_interval && o.max_interval > o.max - o.min) { + o.max_interval = o.max - o.min; + } + }, + + decorate: function (num, original) { + var decorated = "", + o = this.options; + + if (o.prefix) { + decorated += o.prefix; + } + + decorated += num; + + if (o.max_postfix) { + if (o.values.length && num === o.p_values[o.max]) { + decorated += o.max_postfix; + if (o.postfix) { + decorated += " "; + } + } else if (original === o.max) { + decorated += o.max_postfix; + if (o.postfix) { + decorated += " "; + } + } + } + + if (o.postfix) { + decorated += o.postfix; + } + + return decorated; + }, + + updateFrom: function () { + this.result.from = this.options.from; + this.result.from_percent = this.convertToPercent(this.result.from); + if (this.options.values) { + this.result.from_value = this.options.values[this.result.from]; + } + }, + + updateTo: function () { + this.result.to = this.options.to; + this.result.to_percent = this.convertToPercent(this.result.to); + if (this.options.values) { + this.result.to_value = this.options.values[this.result.to]; + } + }, + + updateResult: function () { + this.result.min = this.options.min; + this.result.max = this.options.max; + this.updateFrom(); + this.updateTo(); + }, + + + // ============================================================================================================= + // Grid + + appendGrid: function () { + if (!this.options.grid) { + return; + } + + var o = this.options, + i, z, + + total = o.max - o.min, + big_num = o.grid_num, + big_p = 0, + big_w = 0, + + small_max = 4, + local_small_max, + small_p, + small_w = 0, + + result, + html = ''; + + + + this.calcGridMargin(); + + if (o.grid_snap) { + big_num = total / o.step; + big_p = this.toFixed(o.step / (total / 100)); + } else { + big_p = this.toFixed(100 / big_num); + } + + if (big_num > 4) { + small_max = 3; + } + if (big_num > 7) { + small_max = 2; + } + if (big_num > 14) { + small_max = 1; + } + if (big_num > 28) { + small_max = 0; + } + + for (i = 0; i < big_num + 1; i++) { + local_small_max = small_max; + + big_w = this.toFixed(big_p * i); + + if (big_w > 100) { + big_w = 100; + + local_small_max -= 2; + if (local_small_max < 0) { + local_small_max = 0; + } + } + this.coords.big[i] = big_w; + + small_p = (big_w - (big_p * (i - 1))) / (local_small_max + 1); + + for (z = 1; z <= local_small_max; z++) { + if (big_w === 0) { + break; + } + + small_w = this.toFixed(big_w - (small_p * z)); + + html += ''; + } + + html += ''; + + result = this.convertToValue(big_w); + if (o.values.length) { + result = o.p_values[result]; + } else { + result = this._prettify(result); + } + + html += '' + result + ''; + } + this.coords.big_num = Math.ceil(big_num + 1); + + + + this.$cache.cont.addClass("irs-with-grid"); + this.$cache.grid.html(html); + this.cacheGridLabels(); + }, + + cacheGridLabels: function () { + var $label, i, + num = this.coords.big_num; + + for (i = 0; i < num; i++) { + $label = this.$cache.grid.find(".js-grid-text-" + i); + this.$cache.grid_labels.push($label); + } + + this.calcGridLabels(); + }, + + calcGridLabels: function () { + var i, label, start = [], finish = [], + num = this.coords.big_num; + + for (i = 0; i < num; i++) { + this.coords.big_w[i] = this.$cache.grid_labels[i].outerWidth(false); + this.coords.big_p[i] = this.toFixed(this.coords.big_w[i] / this.coords.w_rs * 100); + this.coords.big_x[i] = this.toFixed(this.coords.big_p[i] / 2); + + start[i] = this.toFixed(this.coords.big[i] - this.coords.big_x[i]); + finish[i] = this.toFixed(start[i] + this.coords.big_p[i]); + } + + if (this.options.force_edges) { + if (start[0] < -this.coords.grid_gap) { + start[0] = -this.coords.grid_gap; + finish[0] = this.toFixed(start[0] + this.coords.big_p[0]); + + this.coords.big_x[0] = this.coords.grid_gap; + } + + if (finish[num - 1] > 100 + this.coords.grid_gap) { + finish[num - 1] = 100 + this.coords.grid_gap; + start[num - 1] = this.toFixed(finish[num - 1] - this.coords.big_p[num - 1]); + + this.coords.big_x[num - 1] = this.toFixed(this.coords.big_p[num - 1] - this.coords.grid_gap); + } + } + + this.calcGridCollision(2, start, finish); + this.calcGridCollision(4, start, finish); + + for (i = 0; i < num; i++) { + label = this.$cache.grid_labels[i][0]; + label.style.marginLeft = -this.coords.big_x[i] + "%"; + } + }, + + // Collisions Calc Beta + // TODO: Refactor then have plenty of time + calcGridCollision: function (step, start, finish) { + var i, next_i, label, + num = this.coords.big_num; + + for (i = 0; i < num; i += step) { + next_i = i + (step / 2); + if (next_i >= num) { + break; + } + + label = this.$cache.grid_labels[next_i][0]; + + if (finish[i] <= start[next_i]) { + label.style.visibility = "visible"; + } else { + label.style.visibility = "hidden"; + } + } + }, + + calcGridMargin: function () { + if (!this.options.grid_margin) { + return; + } + + this.coords.w_rs = this.$cache.rs.outerWidth(false); + if (!this.coords.w_rs) { + return; + } + + if (this.options.type === "single") { + this.coords.w_handle = this.$cache.s_single.outerWidth(false); + } else { + this.coords.w_handle = this.$cache.s_from.outerWidth(false); + } + this.coords.p_handle = this.toFixed(this.coords.w_handle / this.coords.w_rs * 100); + this.coords.grid_gap = this.toFixed((this.coords.p_handle / 2) - 0.1); + + this.$cache.grid[0].style.width = this.toFixed(100 - this.coords.p_handle) + "%"; + this.$cache.grid[0].style.left = this.coords.grid_gap + "%"; + }, + + + + // ============================================================================================================= + // Public methods + + update: function (options) { + if (!this.input) { + return; + } + + this.is_update = true; + + this.options.from = this.result.from; + this.options.to = this.result.to; + + this.options = $.extend(this.options, options); + this.validate(); + this.updateResult(options); + + this.toggleInput(); + this.remove(); + this.init(true); + }, + + reset: function () { + if (!this.input) { + return; + } + + this.updateResult(); + this.update(); + }, + + destroy: function () { + if (!this.input) { + return; + } + + this.toggleInput(); + this.$cache.input.prop("readonly", false); + $.data(this.input, "ionRangeSlider", null); + + this.remove(); + this.input = null; + this.options = null; + } + }; + + $.fn.ionRangeSlider = function (options) { + return this.each(function() { + if (!$.data(this, "ionRangeSlider")) { + $.data(this, "ionRangeSlider", new IonRangeSlider(this, options, plugin_count++)); + } + }); + }; + + + + // ================================================================================================================= + // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating + + // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel + + // MIT license + + (function() { + var lastTime = 0; + var vendors = ['ms', 'moz', 'webkit', 'o']; + for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; + window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] + || window[vendors[x]+'CancelRequestAnimationFrame']; + } + + if (!window.requestAnimationFrame) + window.requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function() { callback(currTime + timeToCall); }, + timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; + }()); + +})); diff --git a/public/static/libs/ion-rangeslider/js/ion.rangeSlider.min.js b/public/static/libs/ion-rangeslider/js/ion.rangeSlider.min.js new file mode 100644 index 0000000..513dbaf --- /dev/null +++ b/public/static/libs/ion-rangeslider/js/ion.rangeSlider.min.js @@ -0,0 +1,76 @@ +// Ion.RangeSlider | version 2.1.4 | https://github.com/IonDen/ion.rangeSlider +;(function(g){"function"===typeof define&&define.amd?define(["jquery"],function(q){g(q,document,window,navigator)}):g(jQuery,document,window,navigator)})(function(g,q,h,t,v){var u=0,p=function(){var a=t.userAgent,b=/msie\s\d+/i;return 0a)?(g("html").addClass("lt-ie9"),!0):!1}();Function.prototype.bind||(Function.prototype.bind=function(a){var b=this,d=[].slice;if("function"!=typeof b)throw new TypeError;var c=d.call(arguments,1),e=function(){if(this instanceof +e){var f=function(){};f.prototype=b.prototype;var f=new f,l=b.apply(f,c.concat(d.call(arguments)));return Object(l)===l?l:f}return b.apply(a,c.concat(d.call(arguments)))};return e});Array.prototype.indexOf||(Array.prototype.indexOf=function(a,b){var d;if(null==this)throw new TypeError('"this" is null or not defined');var c=Object(this),e=c.length>>>0;if(0===e)return-1;d=+b||0;Infinity===Math.abs(d)&&(d=0);if(d>=e)return-1;for(d=Math.max(0<=d?d:e-Math.abs(d),0);d');this.$cache.input.prop("readonly",!0);this.$cache.cont=this.$cache.input.prev();this.result.slider=this.$cache.cont;this.$cache.cont.html('01000'); +this.$cache.rs=this.$cache.cont.find(".irs");this.$cache.min=this.$cache.cont.find(".irs-min");this.$cache.max=this.$cache.cont.find(".irs-max");this.$cache.from=this.$cache.cont.find(".irs-from");this.$cache.to=this.$cache.cont.find(".irs-to");this.$cache.single=this.$cache.cont.find(".irs-single");this.$cache.bar=this.$cache.cont.find(".irs-bar");this.$cache.line=this.$cache.cont.find(".irs-line");this.$cache.grid=this.$cache.cont.find(".irs-grid");"single"===this.options.type?(this.$cache.cont.append(''), +this.$cache.edge=this.$cache.cont.find(".irs-bar-edge"),this.$cache.s_single=this.$cache.cont.find(".single"),this.$cache.from[0].style.visibility="hidden",this.$cache.to[0].style.visibility="hidden",this.$cache.shad_single=this.$cache.cont.find(".shadow-single")):(this.$cache.cont.append(''),this.$cache.s_from=this.$cache.cont.find(".from"), +this.$cache.s_to=this.$cache.cont.find(".to"),this.$cache.shad_from=this.$cache.cont.find(".shadow-from"),this.$cache.shad_to=this.$cache.cont.find(".shadow-to"),this.setTopHandler());this.options.hide_from_to&&(this.$cache.from[0].style.display="none",this.$cache.to[0].style.display="none",this.$cache.single[0].style.display="none");this.appendGrid();this.options.disable?(this.appendDisableMask(),this.$cache.input[0].disabled=!0):(this.$cache.cont.removeClass("irs-disabled"),this.$cache.input[0].disabled= +!1,this.bindEvents());this.options.drag_interval&&(this.$cache.bar[0].style.cursor="ew-resize")},setTopHandler:function(){var a=this.options.max,b=this.options.to;this.options.from>this.options.min&&b===a?this.$cache.s_from.addClass("type_last"):b');this.$cache.cont.addClass("irs-disabled")},remove:function(){this.$cache.cont.remove();this.$cache.cont=null;this.$cache.line.off("keydown.irs_"+this.plugin_count);this.$cache.body.off("touchmove.irs_"+this.plugin_count);this.$cache.body.off("mousemove.irs_"+this.plugin_count);this.$cache.win.off("touchend.irs_"+ +this.plugin_count);this.$cache.win.off("mouseup.irs_"+this.plugin_count);p&&(this.$cache.body.off("mouseup.irs_"+this.plugin_count),this.$cache.body.off("mouseleave.irs_"+this.plugin_count));this.$cache.grid_labels=[];this.coords.big=[];this.coords.big_w=[];this.coords.big_p=[];this.coords.big_x=[];cancelAnimationFrame(this.raf_id)},bindEvents:function(){if(!this.no_diapason){this.$cache.body.on("touchmove.irs_"+this.plugin_count,this.pointerMove.bind(this));this.$cache.body.on("mousemove.irs_"+this.plugin_count, +this.pointerMove.bind(this));this.$cache.win.on("touchend.irs_"+this.plugin_count,this.pointerUp.bind(this));this.$cache.win.on("mouseup.irs_"+this.plugin_count,this.pointerUp.bind(this));this.$cache.line.on("touchstart.irs_"+this.plugin_count,this.pointerClick.bind(this,"click"));this.$cache.line.on("mousedown.irs_"+this.plugin_count,this.pointerClick.bind(this,"click"));this.options.drag_interval&&"double"===this.options.type?(this.$cache.bar.on("touchstart.irs_"+this.plugin_count,this.pointerDown.bind(this, +"both")),this.$cache.bar.on("mousedown.irs_"+this.plugin_count,this.pointerDown.bind(this,"both"))):(this.$cache.bar.on("touchstart.irs_"+this.plugin_count,this.pointerClick.bind(this,"click")),this.$cache.bar.on("mousedown.irs_"+this.plugin_count,this.pointerClick.bind(this,"click")));"single"===this.options.type?(this.$cache.single.on("touchstart.irs_"+this.plugin_count,this.pointerDown.bind(this,"single")),this.$cache.s_single.on("touchstart.irs_"+this.plugin_count,this.pointerDown.bind(this,"single")), +this.$cache.shad_single.on("touchstart.irs_"+this.plugin_count,this.pointerClick.bind(this,"click")),this.$cache.single.on("mousedown.irs_"+this.plugin_count,this.pointerDown.bind(this,"single")),this.$cache.s_single.on("mousedown.irs_"+this.plugin_count,this.pointerDown.bind(this,"single")),this.$cache.edge.on("mousedown.irs_"+this.plugin_count,this.pointerClick.bind(this,"click")),this.$cache.shad_single.on("mousedown.irs_"+this.plugin_count,this.pointerClick.bind(this,"click"))):(this.$cache.single.on("touchstart.irs_"+ +this.plugin_count,this.pointerDown.bind(this,null)),this.$cache.single.on("mousedown.irs_"+this.plugin_count,this.pointerDown.bind(this,null)),this.$cache.from.on("touchstart.irs_"+this.plugin_count,this.pointerDown.bind(this,"from")),this.$cache.s_from.on("touchstart.irs_"+this.plugin_count,this.pointerDown.bind(this,"from")),this.$cache.to.on("touchstart.irs_"+this.plugin_count,this.pointerDown.bind(this,"to")),this.$cache.s_to.on("touchstart.irs_"+this.plugin_count,this.pointerDown.bind(this,"to")), +this.$cache.shad_from.on("touchstart.irs_"+this.plugin_count,this.pointerClick.bind(this,"click")),this.$cache.shad_to.on("touchstart.irs_"+this.plugin_count,this.pointerClick.bind(this,"click")),this.$cache.from.on("mousedown.irs_"+this.plugin_count,this.pointerDown.bind(this,"from")),this.$cache.s_from.on("mousedown.irs_"+this.plugin_count,this.pointerDown.bind(this,"from")),this.$cache.to.on("mousedown.irs_"+this.plugin_count,this.pointerDown.bind(this,"to")),this.$cache.s_to.on("mousedown.irs_"+ +this.plugin_count,this.pointerDown.bind(this,"to")),this.$cache.shad_from.on("mousedown.irs_"+this.plugin_count,this.pointerClick.bind(this,"click")),this.$cache.shad_to.on("mousedown.irs_"+this.plugin_count,this.pointerClick.bind(this,"click")));if(this.options.keyboard)this.$cache.line.on("keydown.irs_"+this.plugin_count,this.key.bind(this,"keyboard"));p&&(this.$cache.body.on("mouseup.irs_"+this.plugin_count,this.pointerUp.bind(this)),this.$cache.body.on("mouseleave.irs_"+this.plugin_count,this.pointerUp.bind(this)))}}, +pointerMove:function(a){this.dragging&&(this.coords.x_pointer=(a.pageX||a.originalEvent.touches&&a.originalEvent.touches[0].pageX)-this.coords.x_gap,this.calc())},pointerUp:function(a){if(this.current_plugin===this.plugin_count&&this.is_active){this.is_active=!1;this.$cache.cont.find(".state_hover").removeClass("state_hover");this.force_redraw=!0;p&&g("*").prop("unselectable",!1);this.updateScene();this.restoreOriginalMinInterval();if(g.contains(this.$cache.cont[0],a.target)||this.dragging)this.is_finish= +!0,this.callOnFinish();this.dragging=!1}},pointerDown:function(a,b){b.preventDefault();var d=b.pageX||b.originalEvent.touches&&b.originalEvent.touches[0].pageX;2!==b.button&&("both"===a&&this.setTempMinInterval(),a||(a=this.target),this.current_plugin=this.plugin_count,this.target=a,this.dragging=this.is_active=!0,this.coords.x_gap=this.$cache.rs.offset().left,this.coords.x_pointer=d-this.coords.x_gap,this.calcPointerPercent(),this.changeLevel(a),p&&g("*").prop("unselectable",!0),this.$cache.line.trigger("focus"), +this.updateScene())},pointerClick:function(a,b){b.preventDefault();var d=b.pageX||b.originalEvent.touches&&b.originalEvent.touches[0].pageX;2!==b.button&&(this.current_plugin=this.plugin_count,this.target=a,this.is_click=!0,this.coords.x_gap=this.$cache.rs.offset().left,this.coords.x_pointer=+(d-this.coords.x_gap).toFixed(),this.force_redraw=!0,this.calc(),this.$cache.line.trigger("focus"))},key:function(a,b){if(!(this.current_plugin!==this.plugin_count||b.altKey||b.ctrlKey||b.shiftKey||b.metaKey)){switch(b.which){case 83:case 65:case 40:case 37:b.preventDefault(); +this.moveByKey(!1);break;case 87:case 68:case 38:case 39:b.preventDefault(),this.moveByKey(!0)}return!0}},moveByKey:function(a){var b=this.coords.p_pointer,b=a?b+this.options.keyboard_step:b-this.options.keyboard_step;this.coords.x_pointer=this.toFixed(this.coords.w_rs/100*b);this.is_key=!0;this.calc()},setMinMax:function(){this.options&&(this.options.hide_min_max?(this.$cache.min[0].style.display="none",this.$cache.max[0].style.display="none"):(this.options.values.length?(this.$cache.min.html(this.decorate(this.options.p_values[this.options.min])), +this.$cache.max.html(this.decorate(this.options.p_values[this.options.max]))):(this.$cache.min.html(this.decorate(this._prettify(this.options.min),this.options.min)),this.$cache.max.html(this.decorate(this._prettify(this.options.max),this.options.max))),this.labels.w_min=this.$cache.min.outerWidth(!1),this.labels.w_max=this.$cache.max.outerWidth(!1)))},setTempMinInterval:function(){var a=this.result.to-this.result.from;null===this.old_min_interval&&(this.old_min_interval=this.options.min_interval); +this.options.min_interval=a},restoreOriginalMinInterval:function(){null!==this.old_min_interval&&(this.options.min_interval=this.old_min_interval,this.old_min_interval=null)},calc:function(a){if(this.options){this.calc_count++;if(10===this.calc_count||a)this.calc_count=0,this.coords.w_rs=this.$cache.rs.outerWidth(!1),this.calcHandlePercent();if(this.coords.w_rs){this.calcPointerPercent();a=this.getHandleX();"click"===this.target&&(this.coords.p_gap=this.coords.p_handle/2,a=this.getHandleX(),this.target= +this.options.drag_interval?"both_one":this.chooseHandle(a));switch(this.target){case "base":var b=(this.options.max-this.options.min)/100;a=(this.result.from-this.options.min)/b;b=(this.result.to-this.options.min)/b;this.coords.p_single_real=this.toFixed(a);this.coords.p_from_real=this.toFixed(a);this.coords.p_to_real=this.toFixed(b);this.coords.p_single_real=this.checkDiapason(this.coords.p_single_real,this.options.from_min,this.options.from_max);this.coords.p_from_real=this.checkDiapason(this.coords.p_from_real, +this.options.from_min,this.options.from_max);this.coords.p_to_real=this.checkDiapason(this.coords.p_to_real,this.options.to_min,this.options.to_max);this.coords.p_single_fake=this.convertToFakePercent(this.coords.p_single_real);this.coords.p_from_fake=this.convertToFakePercent(this.coords.p_from_real);this.coords.p_to_fake=this.convertToFakePercent(this.coords.p_to_real);this.target=null;break;case "single":if(this.options.from_fixed)break;this.coords.p_single_real=this.convertToRealPercent(a);this.coords.p_single_real= +this.calcWithStep(this.coords.p_single_real);this.coords.p_single_real=this.checkDiapason(this.coords.p_single_real,this.options.from_min,this.options.from_max);this.coords.p_single_fake=this.convertToFakePercent(this.coords.p_single_real);break;case "from":if(this.options.from_fixed)break;this.coords.p_from_real=this.convertToRealPercent(a);this.coords.p_from_real=this.calcWithStep(this.coords.p_from_real);this.coords.p_from_real>this.coords.p_to_real&&(this.coords.p_from_real=this.coords.p_to_real); +this.coords.p_from_real=this.checkDiapason(this.coords.p_from_real,this.options.from_min,this.options.from_max);this.coords.p_from_real=this.checkMinInterval(this.coords.p_from_real,this.coords.p_to_real,"from");this.coords.p_from_real=this.checkMaxInterval(this.coords.p_from_real,this.coords.p_to_real,"from");this.coords.p_from_fake=this.convertToFakePercent(this.coords.p_from_real);break;case "to":if(this.options.to_fixed)break;this.coords.p_to_real=this.convertToRealPercent(a);this.coords.p_to_real= +this.calcWithStep(this.coords.p_to_real);this.coords.p_to_realb&&(b=0,d=b+a);100this.coords.x_pointer||isNaN(this.coords.x_pointer)? +this.coords.x_pointer=0:this.coords.x_pointer>this.coords.w_rs&&(this.coords.x_pointer=this.coords.w_rs),this.coords.p_pointer=this.toFixed(this.coords.x_pointer/this.coords.w_rs*100)):this.coords.p_pointer=0},convertToRealPercent:function(a){return a/(100-this.coords.p_handle)*100},convertToFakePercent:function(a){return a/100*(100-this.coords.p_handle)},getHandleX:function(){var a=100-this.coords.p_handle,b=this.toFixed(this.coords.p_pointer-this.coords.p_gap);0>b?b=0:b>a&&(b=a);return b},calcHandlePercent:function(){this.coords.w_handle= +"single"===this.options.type?this.$cache.s_single.outerWidth(!1):this.$cache.s_from.outerWidth(!1);this.coords.p_handle=this.toFixed(this.coords.w_handle/this.coords.w_rs*100)},chooseHandle:function(a){return"single"===this.options.type?"single":a>=this.coords.p_from_real+(this.coords.p_to_real-this.coords.p_from_real)/2?this.options.to_fixed?"from":"to":this.options.from_fixed?"to":"from"},calcMinMax:function(){this.coords.w_rs&&(this.labels.p_min=this.labels.w_min/this.coords.w_rs*100,this.labels.p_max= +this.labels.w_max/this.coords.w_rs*100)},calcLabels:function(){this.coords.w_rs&&!this.options.hide_from_to&&("single"===this.options.type?(this.labels.w_single=this.$cache.single.outerWidth(!1),this.labels.p_single_fake=this.labels.w_single/this.coords.w_rs*100,this.labels.p_single_left=this.coords.p_single_fake+this.coords.p_handle/2-this.labels.p_single_fake/2):(this.labels.w_from=this.$cache.from.outerWidth(!1),this.labels.p_from_fake=this.labels.w_from/this.coords.w_rs*100,this.labels.p_from_left= +this.coords.p_from_fake+this.coords.p_handle/2-this.labels.p_from_fake/2,this.labels.p_from_left=this.toFixed(this.labels.p_from_left),this.labels.p_from_left=this.checkEdges(this.labels.p_from_left,this.labels.p_from_fake),this.labels.w_to=this.$cache.to.outerWidth(!1),this.labels.p_to_fake=this.labels.w_to/this.coords.w_rs*100,this.labels.p_to_left=this.coords.p_to_fake+this.coords.p_handle/2-this.labels.p_to_fake/2,this.labels.p_to_left=this.toFixed(this.labels.p_to_left),this.labels.p_to_left= +this.checkEdges(this.labels.p_to_left,this.labels.p_to_fake),this.labels.w_single=this.$cache.single.outerWidth(!1),this.labels.p_single_fake=this.labels.w_single/this.coords.w_rs*100,this.labels.p_single_left=(this.labels.p_from_left+this.labels.p_to_left+this.labels.p_to_fake)/2-this.labels.p_single_fake/2,this.labels.p_single_left=this.toFixed(this.labels.p_single_left)),this.labels.p_single_left=this.checkEdges(this.labels.p_single_left,this.labels.p_single_fake))},updateScene:function(){this.raf_id&& +(cancelAnimationFrame(this.raf_id),this.raf_id=null);clearTimeout(this.update_tm);this.update_tm=null;this.options&&(this.drawHandles(),this.is_active?this.raf_id=requestAnimationFrame(this.updateScene.bind(this)):this.update_tm=setTimeout(this.updateScene.bind(this),300))},drawHandles:function(){this.coords.w_rs=this.$cache.rs.outerWidth(!1);if(this.coords.w_rs){this.coords.w_rs!==this.coords.w_rs_old&&(this.target="base",this.is_resize=!0);if(this.coords.w_rs!==this.coords.w_rs_old||this.force_redraw)this.setMinMax(), +this.calc(!0),this.drawLabels(),this.options.grid&&(this.calcGridMargin(),this.calcGridLabels()),this.force_redraw=!0,this.coords.w_rs_old=this.coords.w_rs,this.drawShadow();if(this.coords.w_rs&&(this.dragging||this.force_redraw||this.is_key)){if(this.old_from!==this.result.from||this.old_to!==this.result.to||this.force_redraw||this.is_key){this.drawLabels();this.$cache.bar[0].style.left=this.coords.p_bar_x+"%";this.$cache.bar[0].style.width=this.coords.p_bar_w+"%";if("single"===this.options.type)this.$cache.s_single[0].style.left= +this.coords.p_single_fake+"%",this.$cache.single[0].style.left=this.labels.p_single_left+"%",this.options.values.length?this.$cache.input.prop("value",this.result.from_value):this.$cache.input.prop("value",this.result.from),this.$cache.input.data("from",this.result.from);else{this.$cache.s_from[0].style.left=this.coords.p_from_fake+"%";this.$cache.s_to[0].style.left=this.coords.p_to_fake+"%";if(this.old_from!==this.result.from||this.force_redraw)this.$cache.from[0].style.left=this.labels.p_from_left+ +"%";if(this.old_to!==this.result.to||this.force_redraw)this.$cache.to[0].style.left=this.labels.p_to_left+"%";this.$cache.single[0].style.left=this.labels.p_single_left+"%";this.options.values.length?this.$cache.input.prop("value",this.result.from_value+this.options.input_values_separator+this.result.to_value):this.$cache.input.prop("value",this.result.from+this.options.input_values_separator+this.result.to);this.$cache.input.data("from",this.result.from);this.$cache.input.data("to",this.result.to)}this.old_from=== +this.result.from&&this.old_to===this.result.to||this.is_start||this.$cache.input.trigger("change");this.old_from=this.result.from;this.old_to=this.result.to;this.is_resize||this.is_update||this.is_start||this.is_finish||this.callOnChange();if(this.is_key||this.is_click)this.is_click=this.is_key=!1,this.callOnFinish();this.is_finish=this.is_resize=this.is_update=!1}this.force_redraw=this.is_click=this.is_key=this.is_start=!1}}},drawLabels:function(){if(this.options){var a=this.options.values.length, +b=this.options.p_values,d;if(!this.options.hide_from_to)if("single"===this.options.type)a=a?this.decorate(b[this.result.from]):this.decorate(this._prettify(this.result.from),this.result.from),this.$cache.single.html(a),this.calcLabels(),this.$cache.min[0].style.visibility=this.labels.p_single_left100-this.labels.p_max-1?"hidden":"visible";else{a?(this.options.decorate_both? +(a=this.decorate(b[this.result.from]),a+=this.options.values_separator,a+=this.decorate(b[this.result.to])):a=this.decorate(b[this.result.from]+this.options.values_separator+b[this.result.to]),d=this.decorate(b[this.result.from]),b=this.decorate(b[this.result.to])):(this.options.decorate_both?(a=this.decorate(this._prettify(this.result.from),this.result.from),a+=this.options.values_separator,a+=this.decorate(this._prettify(this.result.to),this.result.to)):a=this.decorate(this._prettify(this.result.from)+ +this.options.values_separator+this._prettify(this.result.to),this.result.to),d=this.decorate(this._prettify(this.result.from),this.result.from),b=this.decorate(this._prettify(this.result.to),this.result.to));this.$cache.single.html(a);this.$cache.from.html(d);this.$cache.to.html(b);this.calcLabels();b=Math.min(this.labels.p_single_left,this.labels.p_from_left);a=this.labels.p_single_left+this.labels.p_single_fake;d=this.labels.p_to_left+this.labels.p_to_fake;var c=Math.max(a,d);this.labels.p_from_left+ +this.labels.p_from_fake>=this.labels.p_to_left?(this.$cache.from[0].style.visibility="hidden",this.$cache.to[0].style.visibility="hidden",this.$cache.single[0].style.visibility="visible",this.result.from===this.result.to?("from"===this.target?this.$cache.from[0].style.visibility="visible":"to"===this.target?this.$cache.to[0].style.visibility="visible":this.target||(this.$cache.from[0].style.visibility="visible"),this.$cache.single[0].style.visibility="hidden",c=d):(this.$cache.from[0].style.visibility= +"hidden",this.$cache.to[0].style.visibility="hidden",this.$cache.single[0].style.visibility="visible",c=Math.max(a,d))):(this.$cache.from[0].style.visibility="visible",this.$cache.to[0].style.visibility="visible",this.$cache.single[0].style.visibility="hidden");this.$cache.min[0].style.visibility=b100-this.labels.p_max-1?"hidden":"visible"}}},drawShadow:function(){var a=this.options,b=this.$cache,d="number"===typeof a.from_min&& +!isNaN(a.from_min),c="number"===typeof a.from_max&&!isNaN(a.from_max),e="number"===typeof a.to_min&&!isNaN(a.to_min),f="number"===typeof a.to_max&&!isNaN(a.to_max);"single"===a.type?a.from_shadow&&(d||c)?(d=this.convertToPercent(d?a.from_min:a.min),c=this.convertToPercent(c?a.from_max:a.max)-d,d=this.toFixed(d-this.coords.p_handle/100*d),c=this.toFixed(c-this.coords.p_handle/100*c),d+=this.coords.p_handle/2,b.shad_single[0].style.display="block",b.shad_single[0].style.left=d+"%",b.shad_single[0].style.width= +c+"%"):b.shad_single[0].style.display="none":(a.from_shadow&&(d||c)?(d=this.convertToPercent(d?a.from_min:a.min),c=this.convertToPercent(c?a.from_max:a.max)-d,d=this.toFixed(d-this.coords.p_handle/100*d),c=this.toFixed(c-this.coords.p_handle/100*c),d+=this.coords.p_handle/2,b.shad_from[0].style.display="block",b.shad_from[0].style.left=d+"%",b.shad_from[0].style.width=c+"%"):b.shad_from[0].style.display="none",a.to_shadow&&(e||f)?(e=this.convertToPercent(e?a.to_min:a.min),a=this.convertToPercent(f? +a.to_max:a.max)-e,e=this.toFixed(e-this.coords.p_handle/100*e),a=this.toFixed(a-this.coords.p_handle/100*a),e+=this.coords.p_handle/2,b.shad_to[0].style.display="block",b.shad_to[0].style.left=e+"%",b.shad_to[0].style.width=a+"%"):b.shad_to[0].style.display="none")},callOnStart:function(){if(this.options.onStart&&"function"===typeof this.options.onStart)this.options.onStart(this.result)},callOnChange:function(){if(this.options.onChange&&"function"===typeof this.options.onChange)this.options.onChange(this.result)}, +callOnFinish:function(){if(this.options.onFinish&&"function"===typeof this.options.onFinish)this.options.onFinish(this.result)},callOnUpdate:function(){if(this.options.onUpdate&&"function"===typeof this.options.onUpdate)this.options.onUpdate(this.result)},toggleInput:function(){this.$cache.input.toggleClass("irs-hidden-input")},convertToPercent:function(a,b){var d=this.options.max-this.options.min;return d?this.toFixed((b?a:a-this.options.min)/(d/100)):(this.no_diapason=!0,0)},convertToValue:function(a){var b= +this.options.min,d=this.options.max,c=b.toString().split(".")[1],e=d.toString().split(".")[1],f,l,g=0,k=0;if(0===a)return this.options.min;if(100===a)return this.options.max;c&&(g=f=c.length);e&&(g=l=e.length);f&&l&&(g=f>=l?f:l);0>b&&(k=Math.abs(b),b=+(b+k).toFixed(g),d=+(d+k).toFixed(g));a=(d-b)/100*a+b;(b=this.options.step.toString().split(".")[1])?a=+a.toFixed(b.length):(a/=this.options.step,a*=this.options.step,a=+a.toFixed(0));k&&(a-=k);k=b?+a.toFixed(b.length):this.toFixed(a);kthis.options.max&&(k=this.options.max);return k},calcWithStep:function(a){var b=Math.round(a/this.coords.p_step)*this.coords.p_step;100c.max_interval&&(a=b-c.max_interval):a-b>c.max_interval&&(a=b+c.max_interval);return this.convertToPercent(a)},checkDiapason:function(a,b,d){a=this.convertToValue(a);var c=this.options;"number"!==typeof b&&(b=c.min);"number"!==typeof d&&(d=c.max);ad&&(a=d);return this.convertToPercent(a)},toFixed:function(a){a=a.toFixed(9);return+a},_prettify:function(a){return this.options.prettify_enabled? +this.options.prettify&&"function"===typeof this.options.prettify?this.options.prettify(a):this.prettify(a):a},prettify:function(a){return a.toString().replace(/(\d{1,3}(?=(?:\d\d\d)+(?!\d)))/g,"$1"+this.options.prettify_separator)},checkEdges:function(a,b){if(!this.options.force_edges)return this.toFixed(a);0>a?a=0:a>100-b&&(a=100-b);return this.toFixed(a)},validate:function(){var a=this.options,b=this.result,d=a.values,c=d.length,e,f;"string"===typeof a.min&&(a.min=+a.min);"string"===typeof a.max&& +(a.max=+a.max);"string"===typeof a.from&&(a.from=+a.from);"string"===typeof a.to&&(a.to=+a.to);"string"===typeof a.step&&(a.step=+a.step);"string"===typeof a.from_min&&(a.from_min=+a.from_min);"string"===typeof a.from_max&&(a.from_max=+a.from_max);"string"===typeof a.to_min&&(a.to_min=+a.to_min);"string"===typeof a.to_max&&(a.to_max=+a.to_max);"string"===typeof a.keyboard_step&&(a.keyboard_step=+a.keyboard_step);"string"===typeof a.grid_num&&(a.grid_num=+a.grid_num);a.maxa.max&&(a.from=a.max);else{if(a.froma.max)a.from=a.min;if(a.to>a.max||a.toa.to&&(a.from=a.to)}if("number"!==typeof a.step||isNaN(a.step)||!a.step||0>a.step)a.step= +1;if("number"!==typeof a.keyboard_step||isNaN(a.keyboard_step)||!a.keyboard_step||0>a.keyboard_step)a.keyboard_step=5;"number"===typeof a.from_min&&a.froma.from_max&&(a.from=a.from_max);"number"===typeof a.to_min&&a.toa.to_max&&(a.to=a.to_max);if(b){b.min!==a.min&&(b.min=a.min);b.max!==a.max&&(b.max=a.max);if(b.fromb.max)b.from=a.from;if(b.to +b.max)b.to=a.to}if("number"!==typeof a.min_interval||isNaN(a.min_interval)||!a.min_interval||0>a.min_interval)a.min_interval=0;if("number"!==typeof a.max_interval||isNaN(a.max_interval)||!a.max_interval||0>a.max_interval)a.max_interval=0;a.min_interval&&a.min_interval>a.max-a.min&&(a.min_interval=a.max-a.min);a.max_interval&&a.max_interval>a.max-a.min&&(a.max_interval=a.max-a.min)},decorate:function(a,b){var d="",c=this.options;c.prefix&&(d+=c.prefix);d+=a;c.max_postfix&&(c.values.length&&a===c.p_values[c.max]? +(d+=c.max_postfix,c.postfix&&(d+=" ")):b===c.max&&(d+=c.max_postfix,c.postfix&&(d+=" ")));c.postfix&&(d+=c.postfix);return d},updateFrom:function(){this.result.from=this.options.from;this.result.from_percent=this.convertToPercent(this.result.from);this.options.values&&(this.result.from_value=this.options.values[this.result.from])},updateTo:function(){this.result.to=this.options.to;this.result.to_percent=this.convertToPercent(this.result.to);this.options.values&&(this.result.to_value=this.options.values[this.result.to])}, +updateResult:function(){this.result.min=this.options.min;this.result.max=this.options.max;this.updateFrom();this.updateTo()},appendGrid:function(){if(this.options.grid){var a=this.options,b,d;b=a.max-a.min;var c=a.grid_num,e=0,f=0,g=4,h,k,m=0,n="";this.calcGridMargin();a.grid_snap?(c=b/a.step,e=this.toFixed(a.step/(b/100))):e=this.toFixed(100/c);4h&&(h=0));this.coords.big[b]=f;k=(f-e*(b-1))/ +(h+1);for(d=1;d<=h&&0!==f;d++)m=this.toFixed(f-k*d),n+='';n+='';m=this.convertToValue(f);m=a.values.length?a.p_values[m]:this._prettify(m);n+=''+m+""}this.coords.big_num=Math.ceil(c+1);this.$cache.cont.addClass("irs-with-grid");this.$cache.grid.html(n);this.cacheGridLabels()}},cacheGridLabels:function(){var a, +b,d=this.coords.big_num;for(b=0;b100+this.coords.grid_gap&&(d[c-1]=100+this.coords.grid_gap,b[c-1]=this.toFixed(d[c-1]-this.coords.big_p[c-1]),this.coords.big_x[c-1]=this.toFixed(this.coords.big_p[c-1]-this.coords.grid_gap)));this.calcGridCollision(2,b,d);this.calcGridCollision(4,b,d);for(a=0;a=g)break;f=this.$cache.grid_labels[e][0];f.style.visibility=d[c]<=b[e]?"visible":"hidden"}},calcGridMargin:function(){this.options.grid_margin&&(this.coords.w_rs=this.$cache.rs.outerWidth(!1),this.coords.w_rs&&(this.coords.w_handle="single"===this.options.type?this.$cache.s_single.outerWidth(!1):this.$cache.s_from.outerWidth(!1),this.coords.p_handle=this.toFixed(this.coords.w_handle/ +this.coords.w_rs*100),this.coords.grid_gap=this.toFixed(this.coords.p_handle/2-.1),this.$cache.grid[0].style.width=this.toFixed(100-this.coords.p_handle)+"%",this.$cache.grid[0].style.left=this.coords.grid_gap+"%"))},update:function(a){this.input&&(this.is_update=!0,this.options.from=this.result.from,this.options.to=this.result.to,this.options=g.extend(this.options,a),this.validate(),this.updateResult(a),this.toggleInput(),this.remove(),this.init(!0))},reset:function(){this.input&&(this.updateResult(), +this.update())},destroy:function(){this.input&&(this.toggleInput(),this.$cache.input.prop("readonly",!1),g.data(this.input,"ionRangeSlider",null),this.remove(),this.options=this.input=null)}};g.fn.ionRangeSlider=function(a){return this.each(function(){g.data(this,"ionRangeSlider")||g.data(this,"ionRangeSlider",new r(this,a,u++))})};(function(){for(var a=0,b=["ms","moz","webkit","o"],d=0;dhttp://github.com/tapmodo/Jcrop + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/public/static/libs/jcrop/css/Jcrop.css b/public/static/libs/jcrop/css/Jcrop.css new file mode 100644 index 0000000..83196d2 --- /dev/null +++ b/public/static/libs/jcrop/css/Jcrop.css @@ -0,0 +1,372 @@ +/*! Jcrop.css v2.0.4 - build: 20151117 + * Copyright 2008-2015 Tapmodo Interactive LLC + * Free software under MIT License + **/ + +/* + The outer-most container in a typical Jcrop instance + If you are having difficulty with formatting related to styles + on a parent element, place any fixes here or in a like selector + + You can also style this element if you want to add a border, etc + A better method for styling can be seen below with .jcrop-light + (Add a class to the holder and style elements for that extended class) +*/ +.jcrop-active { + direction: ltr; + text-align: left; + box-sizing: border-box; + /* IE10 touch compatibility */ + -ms-touch-action: none; +} +.jcrop-dragging { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} +.jcrop-selection { + z-index: 2; +} +.jcrop-selection.jcrop-current { + z-index: 4; +} +/* Selection Borders */ +.jcrop-border { + background: #ffffff url("Jcrop.gif"); + line-height: 1px !important; + font-size: 0 !important; + overflow: hidden; + position: absolute; + filter: alpha(opacity=50) !important; + opacity: 0.5 !important; +} +.jcrop-border.ord-w, +.jcrop-border.ord-e, +.jcrop-border.ord-n { + top: 0px; +} +.jcrop-border.ord-n, +.jcrop-border.ord-s { + width: 100%; + height: 1px !important; +} +.jcrop-border.ord-w, +.jcrop-border.ord-e { + height: 100%; + width: 1px !important; +} +.jcrop-border.ord-e { + right: -1px; +} +.jcrop-border.ord-n { + top: -1px; +} +.jcrop-border.ord-w { + left: -1px; +} +.jcrop-border.ord-s { + bottom: -1px; +} +.jcrop-selection { + position: absolute; +} +.jcrop-box { + z-index: 2; + display: block; + background: none; + border: none; + padding: 0; + margin: 0; + font-size: 0; +} +.jcrop-box:hover { + background: none; +} +.jcrop-box:active { + background: none; +} +.jcrop-box:focus { + outline: 1px rgba(128, 128, 128, 0.65) dotted; +} +.jcrop-active, +.jcrop-box { + position: relative; +} +.jcrop-box { + width: 100%; + height: 100%; + cursor: move; +} +/* Selection Handles */ +.jcrop-handle { + z-index: 4; + background-color: rgba(49, 28, 28, 0.58); + border: 1px #eeeeee solid; + width: 9px; + height: 9px; + font-size: 0; + position: absolute; + filter: alpha(opacity=80) !important; + opacity: 0.8 !important; +} +.jcrop-handle.ord-n { + left: 50%; + margin-left: -5px; + margin-top: -5px; + top: 0; + cursor: n-resize; +} +.jcrop-handle.ord-s { + bottom: 0; + left: 50%; + margin-bottom: -5px; + margin-left: -5px; + cursor: s-resize; +} +.jcrop-handle.ord-e { + margin-right: -5px; + margin-top: -5px; + right: 0; + top: 50%; + cursor: e-resize; +} +.jcrop-handle.ord-w { + left: 0; + margin-left: -5px; + margin-top: -5px; + top: 50%; + cursor: w-resize; +} +.jcrop-handle.ord-nw { + left: 0; + margin-left: -5px; + margin-top: -5px; + top: 0; + cursor: nw-resize; +} +.jcrop-handle.ord-ne { + margin-right: -5px; + margin-top: -5px; + right: 0; + top: 0; + cursor: ne-resize; +} +.jcrop-handle.ord-se { + bottom: 0; + margin-bottom: -5px; + margin-right: -5px; + right: 0; + cursor: se-resize; +} +.jcrop-handle.ord-sw { + bottom: 0; + left: 0; + margin-bottom: -5px; + margin-left: -5px; + cursor: sw-resize; +} +/* Larger Selection Handles for Touch */ +.jcrop-touch .jcrop-handle { + z-index: 4; + background-color: rgba(49, 28, 28, 0.58); + border: 1px #eeeeee solid; + width: 18px; + height: 18px; + font-size: 0; + position: absolute; + filter: alpha(opacity=80) !important; + opacity: 0.8 !important; +} +.jcrop-touch .jcrop-handle.ord-n { + left: 50%; + margin-left: -10px; + margin-top: -10px; + top: 0; + cursor: n-resize; +} +.jcrop-touch .jcrop-handle.ord-s { + bottom: 0; + left: 50%; + margin-bottom: -10px; + margin-left: -10px; + cursor: s-resize; +} +.jcrop-touch .jcrop-handle.ord-e { + margin-right: -10px; + margin-top: -10px; + right: 0; + top: 50%; + cursor: e-resize; +} +.jcrop-touch .jcrop-handle.ord-w { + left: 0; + margin-left: -10px; + margin-top: -10px; + top: 50%; + cursor: w-resize; +} +.jcrop-touch .jcrop-handle.ord-nw { + left: 0; + margin-left: -10px; + margin-top: -10px; + top: 0; + cursor: nw-resize; +} +.jcrop-touch .jcrop-handle.ord-ne { + margin-right: -10px; + margin-top: -10px; + right: 0; + top: 0; + cursor: ne-resize; +} +.jcrop-touch .jcrop-handle.ord-se { + bottom: 0; + margin-bottom: -10px; + margin-right: -10px; + right: 0; + cursor: se-resize; +} +.jcrop-touch .jcrop-handle.ord-sw { + bottom: 0; + left: 0; + margin-bottom: -10px; + margin-left: -10px; + cursor: sw-resize; +} +/* Selection Dragbars */ +.jcrop-dragbar { + font-size: 0; + position: absolute; +} +.jcrop-dragbar.ord-n, +.jcrop-dragbar.ord-s { + height: 9px !important; + width: 100%; +} +.jcrop-dragbar.ord-e, +.jcrop-dragbar.ord-w { + top: 0px; + height: 100%; + width: 9px !important; +} +.jcrop-dragbar.ord-n { + margin-top: -5px; + cursor: n-resize; + top: 0px; +} +.jcrop-dragbar.ord-s { + bottom: 0; + margin-bottom: -5px; + cursor: s-resize; +} +.jcrop-dragbar.ord-e { + margin-right: -5px; + right: 0; + cursor: e-resize; +} +.jcrop-dragbar.ord-w { + margin-left: -5px; + cursor: w-resize; +} +/* Shading panels */ +.jcrop-shades { + position: relative; + top: 0; + left: 0; +} +.jcrop-shades div { + cursor: crosshair; +} +/* Various special states */ +.jcrop-noresize .jcrop-dragbar, +.jcrop-noresize .jcrop-handle { + display: none; +} +.jcrop-selection.jcrop-nodrag .jcrop-box, +.jcrop-nodrag .jcrop-shades div { + cursor: default; +} +/* The "jcrop-light" class/extension */ +.jcrop-light .jcrop-border { + background: #ffffff; + filter: alpha(opacity=70) !important; + opacity: .70!important; +} +.jcrop-light .jcrop-handle { + background-color: #000000; + border-color: #ffffff; +} +/* The "jcrop-dark" class/extension */ +.jcrop-dark .jcrop-border { + background: #000000; + filter: alpha(opacity=70) !important; + opacity: 0.7 !important; +} +.jcrop-dark .jcrop-handle { + background-color: #ffffff; + border-color: #000000; +} +/* Simple macro to turn off the antlines */ +.solid-line .jcrop-border { + background: #ffffff; +} +.jcrop-thumb { + position: absolute; + overflow: hidden; + z-index: 5; +} +/* Fix for twitter bootstrap et al. */ +.jcrop-active img, +.jcrop-thumb img, +.jcrop-thumb canvas { + min-width: none; + min-height: none; + max-width: none; + max-height: none; +} +/* Improved multiple selection styles - in progress */ +.jcrop-hl-active .jcrop-border { + filter: alpha(opacity=20) !important; + opacity: .20!important; +} +.jcrop-hl-active .jcrop-handle { + filter: alpha(opacity=10) !important; + opacity: .10!important; +} +.jcrop-hl-active .jcrop-selection:hover { + /* + .jcrop-handle { + filter:Alpha(opacity=35)!important; + opacity:.35!important; + } + */ + +} +.jcrop-hl-active .jcrop-selection:hover .jcrop-border { + background-color: #ccc; + filter: alpha(opacity=50) !important; + opacity: .50!important; +} +.jcrop-hl-active .jcrop-selection.jcrop-current .jcrop-border { + background: #808080 url('Jcrop.gif'); + opacity: .35!important; + filter: alpha(opacity=35) !important; +} +.jcrop-hl-active .jcrop-selection.jcrop-current .jcrop-handle { + filter: alpha(opacity=30) !important; + opacity: .30!important; +} +.jcrop-hl-active .jcrop-selection.jcrop-focus .jcrop-border { + background: url('Jcrop.gif'); + opacity: .65!important; + filter: alpha(opacity=65) !important; +} +.jcrop-hl-active .jcrop-selection.jcrop-focus .jcrop-handle { + filter: alpha(opacity=60) !important; + opacity: .60!important; +} +/* Prevent background on button element */ +button.jcrop-box { + background: none; +} diff --git a/public/static/libs/jcrop/css/Jcrop.gif b/public/static/libs/jcrop/css/Jcrop.gif new file mode 100644 index 0000000..72ea7cc Binary files /dev/null and b/public/static/libs/jcrop/css/Jcrop.gif differ diff --git a/public/static/libs/jcrop/css/Jcrop.min.css b/public/static/libs/jcrop/css/Jcrop.min.css new file mode 100644 index 0000000..f0bb310 --- /dev/null +++ b/public/static/libs/jcrop/css/Jcrop.min.css @@ -0,0 +1,6 @@ +/*! Jcrop.min.css v2.0.4 - build: 20151117 + * Copyright 2008-2015 Tapmodo Interactive LLC + * Free software under MIT License + **/ + +.jcrop-active{direction:ltr;text-align:left;box-sizing:border-box;-ms-touch-action:none}.jcrop-dragging{-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.jcrop-selection{z-index:2}.jcrop-selection.jcrop-current{z-index:4}.jcrop-border{background:#fff url(Jcrop.gif);line-height:1px!important;font-size:0!important;overflow:hidden;position:absolute;filter:alpha(opacity=50)!important;opacity:.5!important}.jcrop-border.ord-w,.jcrop-border.ord-e,.jcrop-border.ord-n{top:0}.jcrop-border.ord-n,.jcrop-border.ord-s{width:100%;height:1px!important}.jcrop-border.ord-w,.jcrop-border.ord-e{height:100%;width:1px!important}.jcrop-border.ord-e{right:-1px}.jcrop-border.ord-n{top:-1px}.jcrop-border.ord-w{left:-1px}.jcrop-border.ord-s{bottom:-1px}.jcrop-selection{position:absolute}.jcrop-box{z-index:2;display:block;background:0 0;border:0;padding:0;margin:0;font-size:0}.jcrop-box:hover{background:0 0}.jcrop-box:active{background:0 0}.jcrop-box:focus{outline:1px rgba(128,128,128,.65) dotted}.jcrop-active,.jcrop-box{position:relative}.jcrop-box{width:100%;height:100%;cursor:move}.jcrop-handle{z-index:4;background-color:rgba(49,28,28,.58);border:1px #eee solid;width:9px;height:9px;font-size:0;position:absolute;filter:alpha(opacity=80)!important;opacity:.8!important}.jcrop-handle.ord-n{left:50%;margin-left:-5px;margin-top:-5px;top:0;cursor:n-resize}.jcrop-handle.ord-s{bottom:0;left:50%;margin-bottom:-5px;margin-left:-5px;cursor:s-resize}.jcrop-handle.ord-e{margin-right:-5px;margin-top:-5px;right:0;top:50%;cursor:e-resize}.jcrop-handle.ord-w{left:0;margin-left:-5px;margin-top:-5px;top:50%;cursor:w-resize}.jcrop-handle.ord-nw{left:0;margin-left:-5px;margin-top:-5px;top:0;cursor:nw-resize}.jcrop-handle.ord-ne{margin-right:-5px;margin-top:-5px;right:0;top:0;cursor:ne-resize}.jcrop-handle.ord-se{bottom:0;margin-bottom:-5px;margin-right:-5px;right:0;cursor:se-resize}.jcrop-handle.ord-sw{bottom:0;left:0;margin-bottom:-5px;margin-left:-5px;cursor:sw-resize}.jcrop-touch .jcrop-handle{z-index:4;background-color:rgba(49,28,28,.58);border:1px #eee solid;width:18px;height:18px;font-size:0;position:absolute;filter:alpha(opacity=80)!important;opacity:.8!important}.jcrop-touch .jcrop-handle.ord-n{left:50%;margin-left:-10px;margin-top:-10px;top:0;cursor:n-resize}.jcrop-touch .jcrop-handle.ord-s{bottom:0;left:50%;margin-bottom:-10px;margin-left:-10px;cursor:s-resize}.jcrop-touch .jcrop-handle.ord-e{margin-right:-10px;margin-top:-10px;right:0;top:50%;cursor:e-resize}.jcrop-touch .jcrop-handle.ord-w{left:0;margin-left:-10px;margin-top:-10px;top:50%;cursor:w-resize}.jcrop-touch .jcrop-handle.ord-nw{left:0;margin-left:-10px;margin-top:-10px;top:0;cursor:nw-resize}.jcrop-touch .jcrop-handle.ord-ne{margin-right:-10px;margin-top:-10px;right:0;top:0;cursor:ne-resize}.jcrop-touch .jcrop-handle.ord-se{bottom:0;margin-bottom:-10px;margin-right:-10px;right:0;cursor:se-resize}.jcrop-touch .jcrop-handle.ord-sw{bottom:0;left:0;margin-bottom:-10px;margin-left:-10px;cursor:sw-resize}.jcrop-dragbar{font-size:0;position:absolute}.jcrop-dragbar.ord-n,.jcrop-dragbar.ord-s{height:9px!important;width:100%}.jcrop-dragbar.ord-e,.jcrop-dragbar.ord-w{top:0;height:100%;width:9px!important}.jcrop-dragbar.ord-n{margin-top:-5px;cursor:n-resize;top:0}.jcrop-dragbar.ord-s{bottom:0;margin-bottom:-5px;cursor:s-resize}.jcrop-dragbar.ord-e{margin-right:-5px;right:0;cursor:e-resize}.jcrop-dragbar.ord-w{margin-left:-5px;cursor:w-resize}.jcrop-shades{position:relative;top:0;left:0}.jcrop-shades div{cursor:crosshair}.jcrop-noresize .jcrop-dragbar,.jcrop-noresize .jcrop-handle{display:none}.jcrop-selection.jcrop-nodrag .jcrop-box,.jcrop-nodrag .jcrop-shades div{cursor:default}.jcrop-light .jcrop-border{background:#fff;filter:alpha(opacity=70)!important;opacity:.7!important}.jcrop-light .jcrop-handle{background-color:#000;border-color:#fff}.jcrop-dark .jcrop-border{background:#000;filter:alpha(opacity=70)!important;opacity:.7!important}.jcrop-dark .jcrop-handle{background-color:#fff;border-color:#000}.solid-line .jcrop-border{background:#fff}.jcrop-thumb{position:absolute;overflow:hidden;z-index:5}.jcrop-active img,.jcrop-thumb img,.jcrop-thumb canvas{min-width:none;min-height:none;max-width:none;max-height:none}.jcrop-hl-active .jcrop-border{filter:alpha(opacity=20)!important;opacity:.2!important}.jcrop-hl-active .jcrop-handle{filter:alpha(opacity=10)!important;opacity:.1!important}.jcrop-hl-active .jcrop-selection:hover{}.jcrop-hl-active .jcrop-selection:hover .jcrop-border{background-color:#ccc;filter:alpha(opacity=50)!important;opacity:.5!important}.jcrop-hl-active .jcrop-selection.jcrop-current .jcrop-border{background:gray url(Jcrop.gif);opacity:.35!important;filter:alpha(opacity=35)!important}.jcrop-hl-active .jcrop-selection.jcrop-current .jcrop-handle{filter:alpha(opacity=30)!important;opacity:.3!important}.jcrop-hl-active .jcrop-selection.jcrop-focus .jcrop-border{background:url(Jcrop.gif);opacity:.65!important;filter:alpha(opacity=65)!important}.jcrop-hl-active .jcrop-selection.jcrop-focus .jcrop-handle{filter:alpha(opacity=60)!important;opacity:.6!important}button.jcrop-box{background:0 0} \ No newline at end of file diff --git a/public/static/libs/jcrop/js/Jcrop.js b/public/static/libs/jcrop/js/Jcrop.js new file mode 100644 index 0000000..700d50b --- /dev/null +++ b/public/static/libs/jcrop/js/Jcrop.js @@ -0,0 +1,2859 @@ +/*! Jcrop.js v2.0.4 - build: 20151117 + * @copyright 2008-2015 Tapmodo Interactive LLC + * @license Free software under MIT License + * @website http://jcrop.org/ + **/ +(function($){ + 'use strict'; + + // Jcrop constructor + var Jcrop = function(element,opt){ + var _ua = navigator.userAgent.toLowerCase(); + + this.opt = $.extend({},Jcrop.defaults,opt || {}); + + this.container = $(element); + + this.opt.is_msie = /msie/.test(_ua); + this.opt.is_ie_lt9 = /msie [1-8]\./.test(_ua); + + this.container.addClass(this.opt.css_container); + + this.ui = {}; + this.state = null; + this.ui.multi = []; + this.ui.selection = null; + this.filter = {}; + + this.init(); + this.setOptions(opt); + this.applySizeConstraints(); + this.container.trigger('cropinit',this); + + // IE<9 doesn't work if mouse events are attached to window + if (this.opt.is_ie_lt9) + this.opt.dragEventTarget = document.body; + }; + + + // Jcrop static functions + $.extend(Jcrop,{ + component: { }, + filter: { }, + stage: { }, + registerComponent: function(name,component){ + Jcrop.component[name] = component; + }, + registerFilter: function(name,filter){ + Jcrop.filter[name] = filter; + }, + registerStageType: function(name,stage){ + Jcrop.stage[name] = stage; + }, + // attach: function(element,opt){{{ + attach: function(element,opt){ + var obj = new $.Jcrop(element,opt); + return obj; + }, + // }}} + // imgCopy: function(imgel){{{ + imgCopy: function(imgel){ + var img = new Image; + img.src = imgel.src; + return img; + }, + // }}} + // imageClone: function(imgel){{{ + imageClone: function(imgel){ + return $.Jcrop.supportsCanvas? + Jcrop.canvasClone(imgel): + Jcrop.imgCopy(imgel); + }, + // }}} + // canvasClone: function(imgel){{{ + canvasClone: function(imgel){ + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'); + + $(canvas).width(imgel.width).height(imgel.height), + canvas.width = imgel.naturalWidth; + canvas.height = imgel.naturalHeight; + ctx.drawImage(imgel,0,0,imgel.naturalWidth,imgel.naturalHeight); + return canvas; + }, + // }}} + // propagate: function(plist,config,obj){{{ + propagate: function(plist,config,obj){ + for(var i=0,l=plist.length;i ratio) + return [ h * ratio, h ]; + else return [ w, w / ratio ]; + }, + // }}} + // stageConstructor: function(el,options,callback){{{ + stageConstructor: function(el,options,callback){ + + // Get a priority-ordered list of available stages + var stages = []; + $.each(Jcrop.stage,function(i,e){ + stages.push(e); + }); + stages.sort(function(a,b){ return a.priority - b.priority; }); + + // Find the first one that supports this element + for(var i=0,l=stages.length;i').parent(); + + obj.element.width(w).height(h); + obj.imgsrc = el; + + if (typeof callback == 'function') + callback.call(this,obj,options); + }); + } +}); +Jcrop.registerStageType('Image',ImageStage); + + +var CanvasStage = function(){ + this.angle = 0; + this.scale = 1; + this.scaleMin = 0.2; + this.scaleMax = 1.25; + this.offset = [0,0]; +}; + +CanvasStage.prototype = new ImageStage(); + +$.extend(CanvasStage,{ + isSupported: function(el,o){ + if ($.Jcrop.supportsCanvas && (el.tagName == 'IMG')) return true; + }, + priority: 60, + create: function(el,options,callback){ + var $el = $(el); + var opt = $.extend({},options); + $.Jcrop.component.ImageLoader.attach(el,function(w,h){ + var obj = new CanvasStage; + $el.hide(); + obj.createCanvas(el,w,h); + $el.before(obj.element); + obj.imgsrc = el; + opt.imgsrc = el; + + if (typeof callback == 'function'){ + callback(obj,opt); + obj.redraw(); + } + }); + } +}); + +$.extend(CanvasStage.prototype,{ + init: function(core){ + this.core = core; + }, + // setOffset: function(x,y) {{{ + setOffset: function(x,y) { + this.offset = [x,y]; + return this; + }, + // }}} + // setAngle: function(v) {{{ + setAngle: function(v) { + this.angle = v; + return this; + }, + // }}} + // setScale: function(v) {{{ + setScale: function(v) { + this.scale = this.boundScale(v); + return this; + }, + // }}} + boundScale: function(v){ + if (vthis.scaleMax) v = this.scaleMax; + return v; + }, + createCanvas: function(img,w,h){ + this.width = w; + this.height = h; + this.canvas = document.createElement('canvas'); + this.canvas.width = w; + this.canvas.height = h; + this.$canvas = $(this.canvas).width('100%').height('100%'); + this.context = this.canvas.getContext('2d'); + this.fillstyle = "rgb(0,0,0)"; + this.element = this.$canvas.wrap('
    ').parent().width(w).height(h); + }, + triggerEvent: function(ev){ + this.$canvas.trigger(ev); + return this; + }, + // clear: function() {{{ + clear: function() { + this.context.fillStyle = this.fillstyle; + this.context.fillRect(0, 0, this.canvas.width, this.canvas.height); + return this; + }, + // }}} + // redraw: function() {{{ + redraw: function() { + // Save the current context + this.context.save(); + this.clear(); + + // Translate to the center point of our image + this.context.translate(parseInt(this.width * 0.5), parseInt(this.height * 0.5)); + // Perform the rotation and scaling + this.context.translate(this.offset[0]/this.core.opt.xscale,this.offset[1]/this.core.opt.yscale); + this.context.rotate(this.angle * (Math.PI/180)); + this.context.scale(this.scale,this.scale); + // Translate back to the top left of our image + this.context.translate(-parseInt(this.width * 0.5), -parseInt(this.height * 0.5)); + // Finally we draw the image + this.context.drawImage(this.imgsrc,0,0,this.width,this.height); + + // And restore the updated context + this.context.restore(); + this.$canvas.trigger('cropredraw'); + return this; + }, + // }}} + // setFillStyle: function(v) {{{ + setFillStyle: function(v) { + this.fillstyle = v; + return this; + } + // }}} +}); + +Jcrop.registerStageType('Canvas',CanvasStage); + + + /** + * BackoffFilter + * move out-of-bounds selection into allowed position at same size + */ + var BackoffFilter = function(){ + this.minw = 40; + this.minh = 40; + this.maxw = 0; + this.maxh = 0; + this.core = null; + }; + $.extend(BackoffFilter.prototype,{ + tag: 'backoff', + priority: 22, + filter: function(b){ + var r = this.bound; + + if (b.x < r.minx) { b.x = r.minx; b.x2 = b.w + b.x; } + if (b.y < r.miny) { b.y = r.miny; b.y2 = b.h + b.y; } + if (b.x2 > r.maxx) { b.x2 = r.maxx; b.x = b.x2 - b.w; } + if (b.y2 > r.maxy) { b.y2 = r.maxy; b.y = b.y2 - b.h; } + + return b; + }, + refresh: function(sel){ + this.elw = sel.core.container.width(); + this.elh = sel.core.container.height(); + this.bound = { + minx: 0 + sel.edge.w, + miny: 0 + sel.edge.n, + maxx: this.elw + sel.edge.e, + maxy: this.elh + sel.edge.s + }; + } + }); + Jcrop.registerFilter('backoff',BackoffFilter); + + /** + * ConstrainFilter + * a filter to constrain crop selection to bounding element + */ + var ConstrainFilter = function(){ + this.core = null; + }; + $.extend(ConstrainFilter.prototype,{ + tag: 'constrain', + priority: 5, + filter: function(b,ord){ + if (ord == 'move') { + if (b.x < this.minx) { b.x = this.minx; b.x2 = b.w + b.x; } + if (b.y < this.miny) { b.y = this.miny; b.y2 = b.h + b.y; } + if (b.x2 > this.maxx) { b.x2 = this.maxx; b.x = b.x2 - b.w; } + if (b.y2 > this.maxy) { b.y2 = this.maxy; b.y = b.y2 - b.h; } + } else { + if (b.x < this.minx) { b.x = this.minx; } + if (b.y < this.miny) { b.y = this.miny; } + if (b.x2 > this.maxx) { b.x2 = this.maxx; } + if (b.y2 > this.maxy) { b.y2 = this.maxy; } + } + b.w = b.x2 - b.x; + b.h = b.y2 - b.y; + return b; + }, + refresh: function(sel){ + this.elw = sel.core.container.width(); + this.elh = sel.core.container.height(); + this.minx = 0 + sel.edge.w; + this.miny = 0 + sel.edge.n; + this.maxx = this.elw + sel.edge.e; + this.maxy = this.elh + sel.edge.s; + } + }); + Jcrop.registerFilter('constrain',ConstrainFilter); + + /** + * ExtentFilter + * a filter to implement minimum or maximum size + */ + var ExtentFilter = function(){ + this.core = null; + }; + $.extend(ExtentFilter.prototype,{ + tag: 'extent', + priority: 12, + offsetFromCorner: function(corner,box,b){ + var w = box[0], h = box[1]; + switch(corner){ + case 'bl': return [ b.x2 - w, b.y, w, h ]; + case 'tl': return [ b.x2 - w , b.y2 - h, w, h ]; + case 'br': return [ b.x, b.y, w, h ]; + case 'tr': return [ b.x, b.y2 - h, w, h ]; + } + }, + getQuadrant: function(s){ + var relx = s.opposite[0]-s.offsetx + var rely = s.opposite[1]-s.offsety; + + if ((relx < 0) && (rely < 0)) return 'br'; + else if ((relx >= 0) && (rely >= 0)) return 'tl'; + else if ((relx < 0) && (rely >= 0)) return 'tr'; + return 'bl'; + }, + filter: function(b,ord,sel){ + + if (ord == 'move') return b; + + var w = b.w, h = b.h, st = sel.state, r = this.limits; + var quad = st? this.getQuadrant(st): 'br'; + + if (r.minw && (w < r.minw)) w = r.minw; + if (r.minh && (h < r.minh)) h = r.minh; + if (r.maxw && (w > r.maxw)) w = r.maxw; + if (r.maxh && (h > r.maxh)) h = r.maxh; + + if ((w == b.w) && (h == b.h)) return b; + + return Jcrop.wrapFromXywh(this.offsetFromCorner(quad,[w,h],b)); + }, + refresh: function(sel){ + this.elw = sel.core.container.width(); + this.elh = sel.core.container.height(); + + this.limits = { + minw: sel.minSize[0], + minh: sel.minSize[1], + maxw: sel.maxSize[0], + maxh: sel.maxSize[1] + }; + } + }); + Jcrop.registerFilter('extent',ExtentFilter); + + + /** + * GridFilter + * a rudimentary grid effect + */ + var GridFilter = function(){ + this.stepx = 1; + this.stepy = 1; + this.core = null; + }; + $.extend(GridFilter.prototype,{ + tag: 'grid', + priority: 19, + filter: function(b){ + + var n = { + x: Math.round(b.x / this.stepx) * this.stepx, + y: Math.round(b.y / this.stepy) * this.stepy, + x2: Math.round(b.x2 / this.stepx) * this.stepx, + y2: Math.round(b.y2 / this.stepy) * this.stepy + }; + + n.w = n.x2 - n.x; + n.h = n.y2 - n.y; + + return n; + } + }); + Jcrop.registerFilter('grid',GridFilter); + + + /** + * RatioFilter + * implements aspectRatio locking + */ + var RatioFilter = function(){ + this.ratio = 0; + this.core = null; + }; + $.extend(RatioFilter.prototype,{ + tag: 'ratio', + priority: 15, + offsetFromCorner: function(corner,box,b){ + var w = box[0], h = box[1]; + switch(corner){ + case 'bl': return [ b.x2 - w, b.y, w, h ]; + case 'tl': return [ b.x2 - w , b.y2 - h, w, h ]; + case 'br': return [ b.x, b.y, w, h ]; + case 'tr': return [ b.x, b.y2 - h, w, h ]; + } + }, + getBoundRatio: function(b,quad){ + var box = Jcrop.getLargestBox(this.ratio,b.w,b.h); + return Jcrop.wrapFromXywh(this.offsetFromCorner(quad,box,b)); + }, + getQuadrant: function(s){ + var relx = s.opposite[0]-s.offsetx + var rely = s.opposite[1]-s.offsety; + + if ((relx < 0) && (rely < 0)) return 'br'; + else if ((relx >= 0) && (rely >= 0)) return 'tl'; + else if ((relx < 0) && (rely >= 0)) return 'tr'; + return 'bl'; + }, + filter: function(b,ord,sel){ + + if (!this.ratio) return b; + + var rt = b.w / b.h; + var st = sel.state; + + var quad = st? this.getQuadrant(st): 'br'; + ord = ord || 'se'; + + if (ord == 'move') return b; + + switch(ord) { + case 'n': + b.x2 = this.elw; + b.w = b.x2 - b.x; + quad = 'tr'; + break; + case 's': + b.x2 = this.elw; + b.w = b.x2 - b.x; + quad = 'br'; + break; + case 'e': + b.y2 = this.elh; + b.h = b.y2 - b.y; + quad = 'br'; + break; + case 'w': + b.y2 = this.elh; + b.h = b.y2 - b.y; + quad = 'bl'; + break; + } + + return this.getBoundRatio(b,quad); + }, + refresh: function(sel){ + this.ratio = sel.aspectRatio; + this.elw = sel.core.container.width(); + this.elh = sel.core.container.height(); + } + }); + Jcrop.registerFilter('ratio',RatioFilter); + + + /** + * RoundFilter + * rounds coordinate values to integers + */ + var RoundFilter = function(){ + this.core = null; + }; + $.extend(RoundFilter.prototype,{ + tag: 'round', + priority: 90, + filter: function(b){ + + var n = { + x: Math.round(b.x), + y: Math.round(b.y), + x2: Math.round(b.x2), + y2: Math.round(b.y2) + }; + + n.w = n.x2 - n.x; + n.h = n.y2 - n.y; + + return n; + } + }); + Jcrop.registerFilter('round',RoundFilter); + + + /** + * ShadeFilter + * A filter that implements div-based shading on any element + * + * The shading you see is actually four semi-opaque divs + * positioned inside the container, around the selection + */ + var ShadeFilter = function(opacity,color){ + this.color = color || 'black'; + this.opacity = opacity || 0.5; + this.core = null; + this.shades = {}; + }; + $.extend(ShadeFilter.prototype,{ + tag: 'shader', + fade: true, + fadeEasing: 'swing', + fadeSpeed: 320, + priority: 95, + init: function(){ + var t = this; + + if (!t.attached) { + t.visible = false; + + t.container = $('
    ').addClass(t.core.opt.css_shades) + .prependTo(this.core.container).hide(); + + t.elh = this.core.container.height(); + t.elw = this.core.container.width(); + + t.shades = { + top: t.createShade(), + right: t.createShade(), + left: t.createShade(), + bottom: t.createShade() + }; + + t.attached = true; + } + }, + destroy: function(){ + this.container.remove(); + }, + setColor: function(color,instant){ + var t = this; + + if (color == t.color) return t; + + this.color = color; + var colorfade = Jcrop.supportsColorFade(); + $.each(t.shades,function(u,i){ + if (!t.fade || instant || !colorfade) i.css('backgroundColor',color); + else i.animate({backgroundColor:color},{queue:false,duration:t.fadeSpeed,easing:t.fadeEasing}); + }); + return t; + }, + setOpacity: function(opacity,instant){ + var t = this; + + if (opacity == t.opacity) return t; + + t.opacity = opacity; + $.each(t.shades,function(u,i){ + if (!t.fade || instant) i.css({opacity:opacity}); + else i.animate({opacity:opacity},{queue:false,duration:t.fadeSpeed,easing:t.fadeEasing}); + }); + return t; + }, + createShade: function(){ + return $('
    ').css({ + position: 'absolute', + backgroundColor: this.color, + opacity: this.opacity + }).appendTo(this.container); + }, + refresh: function(sel){ + var m = this.core, s = this.shades; + + this.setColor(sel.bgColor?sel.bgColor:this.core.opt.bgColor); + this.setOpacity(sel.bgOpacity?sel.bgOpacity:this.core.opt.bgOpacity); + + this.elh = m.container.height(); + this.elw = m.container.width(); + s.right.css('height',this.elh+'px'); + s.left.css('height',this.elh+'px'); + }, + filter: function(b,ord,sel){ + + if (!sel.active) return b; + + var t = this, + s = t.shades; + + s.top.css({ + left: Math.round(b.x)+'px', + width: Math.round(b.w)+'px', + height: Math.round(b.y)+'px' + }); + s.bottom.css({ + top: Math.round(b.y2)+'px', + left: Math.round(b.x)+'px', + width: Math.round(b.w)+'px', + height: (t.elh-Math.round(b.y2))+'px' + }); + s.right.css({ + left: Math.round(b.x2)+'px', + width: (t.elw-Math.round(b.x2))+'px' + }); + s.left.css({ + width: Math.round(b.x)+'px' + }); + + if (!t.visible) { + t.container.show(); + t.visible = true; + } + + return b; + } + }); + Jcrop.registerFilter('shader',ShadeFilter); + + + /** + * CanvasAnimator + * manages smooth cropping animation + * + * This object is called internally to manage animation. + * An in-memory div is animated and a progress callback + * is used to update the selection coordinates of the + * visible selection in realtime. + */ + var CanvasAnimator = function(stage){ + this.stage = stage; + this.core = stage.core; + this.cloneStagePosition(); + }; + + CanvasAnimator.prototype = { + + cloneStagePosition: function(){ + var s = this.stage; + this.angle = s.angle; + this.scale = s.scale; + this.offset = s.offset; + }, + + getElement: function(){ + var s = this.stage; + + return $('
    ') + .css({ + position: 'absolute', + top: s.offset[0]+'px', + left: s.offset[1]+'px', + width: s.angle+'px', + height: s.scale+'px' + }); + }, + + animate: function(cb){ + var t = this; + + this.scale = this.stage.boundScale(this.scale); + t.stage.triggerEvent('croprotstart'); + + t.getElement().animate({ + top: t.offset[0]+'px', + left: t.offset[1]+'px', + width: t.angle+'px', + height: t.scale+'px' + },{ + easing: t.core.opt.animEasing, + duration: t.core.opt.animDuration, + complete: function(){ + t.stage.triggerEvent('croprotend'); + (typeof cb == 'function') && cb.call(this); + }, + progress: function(anim){ + var props = {}, i, tw = anim.tweens; + + for(i=0;i') + .css({ + position: 'absolute', + top: b.y+'px', + left: b.x+'px', + width: b.w+'px', + height: b.h+'px' + }); + }, + + animate: function(x,y,w,h,cb){ + var t = this; + + t.selection.allowResize(false); + + t.getElement().animate({ + top: y+'px', + left: x+'px', + width: w+'px', + height: h+'px' + },{ + easing: t.core.opt.animEasing, + duration: t.core.opt.animDuration, + complete: function(){ + t.selection.allowResize(true); + cb && cb.call(this); + }, + progress: function(anim){ + var props = {}, i, tw = anim.tweens; + + for(i=0;i= 0) + return true; + + switch(e.keyCode){ + case 37: m.nudge(-nudge,0); break; + case 38: m.nudge(0,-nudge); break; + case 39: m.nudge(nudge,0); break; + case 40: m.nudge(0,nudge); break; + + case 46: + case 8: + m.requestDelete(); + return false; + break; + + default: + if (t.debug) console.log('keycode: ' + e.keyCode); + break; + } + + if (!e.metaKey && !e.ctrlKey) + e.preventDefault(); + }); + } + // }}} + } + }); + Jcrop.registerComponent('Keyboard',KeyWatcher); + + + /** + * Selection + * Built-in selection object + */ + var Selection = function(){}; + + $.extend(Selection,{ + // defaults: {{{ + defaults: { + minSize: [ 8, 8 ], + maxSize: [ 0, 0 ], + aspectRatio: 0, + edge: { n: 0, s: 0, e: 0, w: 0 }, + bgColor: null, + bgOpacity: null, + last: null, + + state: null, + active: true, + linked: true, + canDelete: true, + canDrag: true, + canResize: true, + canSelect: true + }, + // }}} + prototype: { + // init: function(core){{{ + init: function(core){ + this.core = core; + this.startup(); + this.linked = this.core.opt.linked; + this.attach(); + this.setOptions(this.core.opt); + core.container.trigger('cropcreate',[this]); + }, + // }}} + // attach: function(){{{ + attach: function(){ + // For extending init() sequence + }, + // }}} + // startup: function(){{{ + startup: function(){ + var t = this, o = t.core.opt; + $.extend(t,Selection.defaults); + t.filter = t.core.getDefaultFilters(); + + t.element = $('
    ').addClass(o.css_selection).data({ selection: t }); + t.frame = $('', + + tClose: 'Close (Esc)', + + tLoading: 'Loading...', + + autoFocusLast: true + + } +}; + + + +$.fn.magnificPopup = function(options) { + _checkInstance(); + + var jqEl = $(this); + + // We call some API method of first param is a string + if (typeof options === "string" ) { + + if(options === 'open') { + var items, + itemOpts = _isJQ ? jqEl.data('magnificPopup') : jqEl[0].magnificPopup, + index = parseInt(arguments[1], 10) || 0; + + if(itemOpts.items) { + items = itemOpts.items[index]; + } else { + items = jqEl; + if(itemOpts.delegate) { + items = items.find(itemOpts.delegate); + } + items = items.eq( index ); + } + mfp._openClick({mfpEl:items}, jqEl, itemOpts); + } else { + if(mfp.isOpen) + mfp[options].apply(mfp, Array.prototype.slice.call(arguments, 1)); + } + + } else { + // clone options obj + options = $.extend(true, {}, options); + + /* + * As Zepto doesn't support .data() method for objects + * and it works only in normal browsers + * we assign "options" object directly to the DOM element. FTW! + */ + if(_isJQ) { + jqEl.data('magnificPopup', options); + } else { + jqEl[0].magnificPopup = options; + } + + mfp.addGroup(jqEl, options); + + } + return jqEl; +}; + +/*>>core*/ + +/*>>inline*/ + +var INLINE_NS = 'inline', + _hiddenClass, + _inlinePlaceholder, + _lastInlineElement, + _putInlineElementsBack = function() { + if(_lastInlineElement) { + _inlinePlaceholder.after( _lastInlineElement.addClass(_hiddenClass) ).detach(); + _lastInlineElement = null; + } + }; + +$.magnificPopup.registerModule(INLINE_NS, { + options: { + hiddenClass: 'hide', // will be appended with `mfp-` prefix + markup: '', + tNotFound: 'Content not found' + }, + proto: { + + initInline: function() { + mfp.types.push(INLINE_NS); + + _mfpOn(CLOSE_EVENT+'.'+INLINE_NS, function() { + _putInlineElementsBack(); + }); + }, + + getInline: function(item, template) { + + _putInlineElementsBack(); + + if(item.src) { + var inlineSt = mfp.st.inline, + el = $(item.src); + + if(el.length) { + + // If target element has parent - we replace it with placeholder and put it back after popup is closed + var parent = el[0].parentNode; + if(parent && parent.tagName) { + if(!_inlinePlaceholder) { + _hiddenClass = inlineSt.hiddenClass; + _inlinePlaceholder = _getEl(_hiddenClass); + _hiddenClass = 'mfp-'+_hiddenClass; + } + // replace target inline element with placeholder + _lastInlineElement = el.after(_inlinePlaceholder).detach().removeClass(_hiddenClass); + } + + mfp.updateStatus('ready'); + } else { + mfp.updateStatus('error', inlineSt.tNotFound); + el = $('
    '); + } + + item.inlineElement = el; + return el; + } + + mfp.updateStatus('ready'); + mfp._parseMarkup(template, {}, item); + return template; + } + } +}); + +/*>>inline*/ + +/*>>ajax*/ +var AJAX_NS = 'ajax', + _ajaxCur, + _removeAjaxCursor = function() { + if(_ajaxCur) { + $(document.body).removeClass(_ajaxCur); + } + }, + _destroyAjaxRequest = function() { + _removeAjaxCursor(); + if(mfp.req) { + mfp.req.abort(); + } + }; + +$.magnificPopup.registerModule(AJAX_NS, { + + options: { + settings: null, + cursor: 'mfp-ajax-cur', + tError: 'The content could not be loaded.' + }, + + proto: { + initAjax: function() { + mfp.types.push(AJAX_NS); + _ajaxCur = mfp.st.ajax.cursor; + + _mfpOn(CLOSE_EVENT+'.'+AJAX_NS, _destroyAjaxRequest); + _mfpOn('BeforeChange.' + AJAX_NS, _destroyAjaxRequest); + }, + getAjax: function(item) { + + if(_ajaxCur) { + $(document.body).addClass(_ajaxCur); + } + + mfp.updateStatus('loading'); + + var opts = $.extend({ + url: item.src, + success: function(data, textStatus, jqXHR) { + var temp = { + data:data, + xhr:jqXHR + }; + + _mfpTrigger('ParseAjax', temp); + + mfp.appendContent( $(temp.data), AJAX_NS ); + + item.finished = true; + + _removeAjaxCursor(); + + mfp._setFocus(); + + setTimeout(function() { + mfp.wrap.addClass(READY_CLASS); + }, 16); + + mfp.updateStatus('ready'); + + _mfpTrigger('AjaxContentAdded'); + }, + error: function() { + _removeAjaxCursor(); + item.finished = item.loadError = true; + mfp.updateStatus('error', mfp.st.ajax.tError.replace('%url%', item.src)); + } + }, mfp.st.ajax.settings); + + mfp.req = $.ajax(opts); + + return ''; + } + } +}); + +/*>>ajax*/ + +/*>>image*/ +var _imgInterval, + _getTitle = function(item) { + if(item.data && item.data.title !== undefined) + return item.data.title; + + var src = mfp.st.image.titleSrc; + + if(src) { + if($.isFunction(src)) { + return src.call(mfp, item); + } else if(item.el) { + return item.el.attr(src) || ''; + } + } + return ''; + }; + +$.magnificPopup.registerModule('image', { + + options: { + markup: '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    ', + cursor: 'mfp-zoom-out-cur', + titleSrc: 'title', + verticalFit: true, + tError: 'The image could not be loaded.' + }, + + proto: { + initImage: function() { + var imgSt = mfp.st.image, + ns = '.image'; + + mfp.types.push('image'); + + _mfpOn(OPEN_EVENT+ns, function() { + if(mfp.currItem.type === 'image' && imgSt.cursor) { + $(document.body).addClass(imgSt.cursor); + } + }); + + _mfpOn(CLOSE_EVENT+ns, function() { + if(imgSt.cursor) { + $(document.body).removeClass(imgSt.cursor); + } + _window.off('resize' + EVENT_NS); + }); + + _mfpOn('Resize'+ns, mfp.resizeImage); + if(mfp.isLowIE) { + _mfpOn('AfterChange', mfp.resizeImage); + } + }, + resizeImage: function() { + var item = mfp.currItem; + if(!item || !item.img) return; + + if(mfp.st.image.verticalFit) { + var decr = 0; + // fix box-sizing in ie7/8 + if(mfp.isLowIE) { + decr = parseInt(item.img.css('padding-top'), 10) + parseInt(item.img.css('padding-bottom'),10); + } + item.img.css('max-height', mfp.wH-decr); + } + }, + _onImageHasSize: function(item) { + if(item.img) { + + item.hasSize = true; + + if(_imgInterval) { + clearInterval(_imgInterval); + } + + item.isCheckingImgSize = false; + + _mfpTrigger('ImageHasSize', item); + + if(item.imgHidden) { + if(mfp.content) + mfp.content.removeClass('mfp-loading'); + + item.imgHidden = false; + } + + } + }, + + /** + * Function that loops until the image has size to display elements that rely on it asap + */ + findImageSize: function(item) { + + var counter = 0, + img = item.img[0], + mfpSetInterval = function(delay) { + + if(_imgInterval) { + clearInterval(_imgInterval); + } + // decelerating interval that checks for size of an image + _imgInterval = setInterval(function() { + if(img.naturalWidth > 0) { + mfp._onImageHasSize(item); + return; + } + + if(counter > 200) { + clearInterval(_imgInterval); + } + + counter++; + if(counter === 3) { + mfpSetInterval(10); + } else if(counter === 40) { + mfpSetInterval(50); + } else if(counter === 100) { + mfpSetInterval(500); + } + }, delay); + }; + + mfpSetInterval(1); + }, + + getImage: function(item, template) { + + var guard = 0, + + // image load complete handler + onLoadComplete = function() { + if(item) { + if (item.img[0].complete) { + item.img.off('.mfploader'); + + if(item === mfp.currItem){ + mfp._onImageHasSize(item); + + mfp.updateStatus('ready'); + } + + item.hasSize = true; + item.loaded = true; + + _mfpTrigger('ImageLoadComplete'); + + } + else { + // if image complete check fails 200 times (20 sec), we assume that there was an error. + guard++; + if(guard < 200) { + setTimeout(onLoadComplete,100); + } else { + onLoadError(); + } + } + } + }, + + // image error handler + onLoadError = function() { + if(item) { + item.img.off('.mfploader'); + if(item === mfp.currItem){ + mfp._onImageHasSize(item); + mfp.updateStatus('error', imgSt.tError.replace('%url%', item.src) ); + } + + item.hasSize = true; + item.loaded = true; + item.loadError = true; + } + }, + imgSt = mfp.st.image; + + + var el = template.find('.mfp-img'); + if(el.length) { + var img = document.createElement('img'); + img.className = 'mfp-img'; + if(item.el && item.el.find('img').length) { + img.alt = item.el.find('img').attr('alt'); + } + item.img = $(img).on('load.mfploader', onLoadComplete).on('error.mfploader', onLoadError); + img.src = item.src; + + // without clone() "error" event is not firing when IMG is replaced by new IMG + // TODO: find a way to avoid such cloning + if(el.is('img')) { + item.img = item.img.clone(); + } + + img = item.img[0]; + if(img.naturalWidth > 0) { + item.hasSize = true; + } else if(!img.width) { + item.hasSize = false; + } + } + + mfp._parseMarkup(template, { + title: _getTitle(item), + img_replaceWith: item.img + }, item); + + mfp.resizeImage(); + + if(item.hasSize) { + if(_imgInterval) clearInterval(_imgInterval); + + if(item.loadError) { + template.addClass('mfp-loading'); + mfp.updateStatus('error', imgSt.tError.replace('%url%', item.src) ); + } else { + template.removeClass('mfp-loading'); + mfp.updateStatus('ready'); + } + return template; + } + + mfp.updateStatus('loading'); + item.loading = true; + + if(!item.hasSize) { + item.imgHidden = true; + template.addClass('mfp-loading'); + mfp.findImageSize(item); + } + + return template; + } + } +}); + +/*>>image*/ + +/*>>zoom*/ +var hasMozTransform, + getHasMozTransform = function() { + if(hasMozTransform === undefined) { + hasMozTransform = document.createElement('p').style.MozTransform !== undefined; + } + return hasMozTransform; + }; + +$.magnificPopup.registerModule('zoom', { + + options: { + enabled: false, + easing: 'ease-in-out', + duration: 300, + opener: function(element) { + return element.is('img') ? element : element.find('img'); + } + }, + + proto: { + + initZoom: function() { + var zoomSt = mfp.st.zoom, + ns = '.zoom', + image; + + if(!zoomSt.enabled || !mfp.supportsTransition) { + return; + } + + var duration = zoomSt.duration, + getElToAnimate = function(image) { + var newImg = image.clone().removeAttr('style').removeAttr('class').addClass('mfp-animated-image'), + transition = 'all '+(zoomSt.duration/1000)+'s ' + zoomSt.easing, + cssObj = { + position: 'fixed', + zIndex: 9999, + left: 0, + top: 0, + '-webkit-backface-visibility': 'hidden' + }, + t = 'transition'; + + cssObj['-webkit-'+t] = cssObj['-moz-'+t] = cssObj['-o-'+t] = cssObj[t] = transition; + + newImg.css(cssObj); + return newImg; + }, + showMainContent = function() { + mfp.content.css('visibility', 'visible'); + }, + openTimeout, + animatedImg; + + _mfpOn('BuildControls'+ns, function() { + if(mfp._allowZoom()) { + + clearTimeout(openTimeout); + mfp.content.css('visibility', 'hidden'); + + // Basically, all code below does is clones existing image, puts in on top of the current one and animated it + + image = mfp._getItemToZoom(); + + if(!image) { + showMainContent(); + return; + } + + animatedImg = getElToAnimate(image); + + animatedImg.css( mfp._getOffset() ); + + mfp.wrap.append(animatedImg); + + openTimeout = setTimeout(function() { + animatedImg.css( mfp._getOffset( true ) ); + openTimeout = setTimeout(function() { + + showMainContent(); + + setTimeout(function() { + animatedImg.remove(); + image = animatedImg = null; + _mfpTrigger('ZoomAnimationEnded'); + }, 16); // avoid blink when switching images + + }, duration); // this timeout equals animation duration + + }, 16); // by adding this timeout we avoid short glitch at the beginning of animation + + + // Lots of timeouts... + } + }); + _mfpOn(BEFORE_CLOSE_EVENT+ns, function() { + if(mfp._allowZoom()) { + + clearTimeout(openTimeout); + + mfp.st.removalDelay = duration; + + if(!image) { + image = mfp._getItemToZoom(); + if(!image) { + return; + } + animatedImg = getElToAnimate(image); + } + + animatedImg.css( mfp._getOffset(true) ); + mfp.wrap.append(animatedImg); + mfp.content.css('visibility', 'hidden'); + + setTimeout(function() { + animatedImg.css( mfp._getOffset() ); + }, 16); + } + + }); + + _mfpOn(CLOSE_EVENT+ns, function() { + if(mfp._allowZoom()) { + showMainContent(); + if(animatedImg) { + animatedImg.remove(); + } + image = null; + } + }); + }, + + _allowZoom: function() { + return mfp.currItem.type === 'image'; + }, + + _getItemToZoom: function() { + if(mfp.currItem.hasSize) { + return mfp.currItem.img; + } else { + return false; + } + }, + + // Get element postion relative to viewport + _getOffset: function(isLarge) { + var el; + if(isLarge) { + el = mfp.currItem.img; + } else { + el = mfp.st.zoom.opener(mfp.currItem.el || mfp.currItem); + } + + var offset = el.offset(); + var paddingTop = parseInt(el.css('padding-top'),10); + var paddingBottom = parseInt(el.css('padding-bottom'),10); + offset.top -= ( $(window).scrollTop() - paddingTop ); + + + /* + + Animating left + top + width/height looks glitchy in Firefox, but perfect in Chrome. And vice-versa. + + */ + var obj = { + width: el.width(), + // fix Zepto height+padding issue + height: (_isJQ ? el.innerHeight() : el[0].offsetHeight) - paddingBottom - paddingTop + }; + + // I hate to do this, but there is no another option + if( getHasMozTransform() ) { + obj['-moz-transform'] = obj['transform'] = 'translate(' + offset.left + 'px,' + offset.top + 'px)'; + } else { + obj.left = offset.left; + obj.top = offset.top; + } + return obj; + } + + } +}); + + + +/*>>zoom*/ + +/*>>iframe*/ + +var IFRAME_NS = 'iframe', + _emptyPage = '//about:blank', + + _fixIframeBugs = function(isShowing) { + if(mfp.currTemplate[IFRAME_NS]) { + var el = mfp.currTemplate[IFRAME_NS].find('iframe'); + if(el.length) { + // reset src after the popup is closed to avoid "video keeps playing after popup is closed" bug + if(!isShowing) { + el[0].src = _emptyPage; + } + + // IE8 black screen bug fix + if(mfp.isIE8) { + el.css('display', isShowing ? 'block' : 'none'); + } + } + } + }; + +$.magnificPopup.registerModule(IFRAME_NS, { + + options: { + markup: '
    '+ + '
    '+ + ''+ + '
    ', + + srcAction: 'iframe_src', + + // we don't care and support only one default type of URL by default + patterns: { + youtube: { + index: 'youtube.com', + id: 'v=', + src: '//www.youtube.com/embed/%id%?autoplay=1' + }, + vimeo: { + index: 'vimeo.com/', + id: '/', + src: '//player.vimeo.com/video/%id%?autoplay=1' + }, + gmaps: { + index: '//maps.google.', + src: '%id%&output=embed' + } + } + }, + + proto: { + initIframe: function() { + mfp.types.push(IFRAME_NS); + + _mfpOn('BeforeChange', function(e, prevType, newType) { + if(prevType !== newType) { + if(prevType === IFRAME_NS) { + _fixIframeBugs(); // iframe if removed + } else if(newType === IFRAME_NS) { + _fixIframeBugs(true); // iframe is showing + } + }// else { + // iframe source is switched, don't do anything + //} + }); + + _mfpOn(CLOSE_EVENT + '.' + IFRAME_NS, function() { + _fixIframeBugs(); + }); + }, + + getIframe: function(item, template) { + var embedSrc = item.src; + var iframeSt = mfp.st.iframe; + + $.each(iframeSt.patterns, function() { + if(embedSrc.indexOf( this.index ) > -1) { + if(this.id) { + if(typeof this.id === 'string') { + embedSrc = embedSrc.substr(embedSrc.lastIndexOf(this.id)+this.id.length, embedSrc.length); + } else { + embedSrc = this.id.call( this, embedSrc ); + } + } + embedSrc = this.src.replace('%id%', embedSrc ); + return false; // break; + } + }); + + var dataObj = {}; + if(iframeSt.srcAction) { + dataObj[iframeSt.srcAction] = embedSrc; + } + mfp._parseMarkup(template, dataObj, item); + + mfp.updateStatus('ready'); + + return template; + } + } +}); + + + +/*>>iframe*/ + +/*>>gallery*/ +/** + * Get looped index depending on number of slides + */ +var _getLoopedId = function(index) { + var numSlides = mfp.items.length; + if(index > numSlides - 1) { + return index - numSlides; + } else if(index < 0) { + return numSlides + index; + } + return index; + }, + _replaceCurrTotal = function(text, curr, total) { + return text.replace(/%curr%/gi, curr + 1).replace(/%total%/gi, total); + }; + +$.magnificPopup.registerModule('gallery', { + + options: { + enabled: false, + arrowMarkup: '', + preload: [0,2], + navigateByImgClick: true, + arrows: true, + + tPrev: 'Previous (Left arrow key)', + tNext: 'Next (Right arrow key)', + tCounter: '%curr% of %total%' + }, + + proto: { + initGallery: function() { + + var gSt = mfp.st.gallery, + ns = '.mfp-gallery'; + + mfp.direction = true; // true - next, false - prev + + if(!gSt || !gSt.enabled ) return false; + + _wrapClasses += ' mfp-gallery'; + + _mfpOn(OPEN_EVENT+ns, function() { + + if(gSt.navigateByImgClick) { + mfp.wrap.on('click'+ns, '.mfp-img', function() { + if(mfp.items.length > 1) { + mfp.next(); + return false; + } + }); + } + + _document.on('keydown'+ns, function(e) { + if (e.keyCode === 37) { + mfp.prev(); + } else if (e.keyCode === 39) { + mfp.next(); + } + }); + }); + + _mfpOn('UpdateStatus'+ns, function(e, data) { + if(data.text) { + data.text = _replaceCurrTotal(data.text, mfp.currItem.index, mfp.items.length); + } + }); + + _mfpOn(MARKUP_PARSE_EVENT+ns, function(e, element, values, item) { + var l = mfp.items.length; + values.counter = l > 1 ? _replaceCurrTotal(gSt.tCounter, item.index, l) : ''; + }); + + _mfpOn('BuildControls' + ns, function() { + if(mfp.items.length > 1 && gSt.arrows && !mfp.arrowLeft) { + var markup = gSt.arrowMarkup, + arrowLeft = mfp.arrowLeft = $( markup.replace(/%title%/gi, gSt.tPrev).replace(/%dir%/gi, 'left') ).addClass(PREVENT_CLOSE_CLASS), + arrowRight = mfp.arrowRight = $( markup.replace(/%title%/gi, gSt.tNext).replace(/%dir%/gi, 'right') ).addClass(PREVENT_CLOSE_CLASS); + + arrowLeft.click(function() { + mfp.prev(); + }); + arrowRight.click(function() { + mfp.next(); + }); + + mfp.container.append(arrowLeft.add(arrowRight)); + } + }); + + _mfpOn(CHANGE_EVENT+ns, function() { + if(mfp._preloadTimeout) clearTimeout(mfp._preloadTimeout); + + mfp._preloadTimeout = setTimeout(function() { + mfp.preloadNearbyImages(); + mfp._preloadTimeout = null; + }, 16); + }); + + + _mfpOn(CLOSE_EVENT+ns, function() { + _document.off(ns); + mfp.wrap.off('click'+ns); + mfp.arrowRight = mfp.arrowLeft = null; + }); + + }, + next: function() { + mfp.direction = true; + mfp.index = _getLoopedId(mfp.index + 1); + mfp.updateItemHTML(); + }, + prev: function() { + mfp.direction = false; + mfp.index = _getLoopedId(mfp.index - 1); + mfp.updateItemHTML(); + }, + goTo: function(newIndex) { + mfp.direction = (newIndex >= mfp.index); + mfp.index = newIndex; + mfp.updateItemHTML(); + }, + preloadNearbyImages: function() { + var p = mfp.st.gallery.preload, + preloadBefore = Math.min(p[0], mfp.items.length), + preloadAfter = Math.min(p[1], mfp.items.length), + i; + + for(i = 1; i <= (mfp.direction ? preloadAfter : preloadBefore); i++) { + mfp._preloadItem(mfp.index+i); + } + for(i = 1; i <= (mfp.direction ? preloadBefore : preloadAfter); i++) { + mfp._preloadItem(mfp.index-i); + } + }, + _preloadItem: function(index) { + index = _getLoopedId(index); + + if(mfp.items[index].preloaded) { + return; + } + + var item = mfp.items[index]; + if(!item.parsed) { + item = mfp.parseEl( index ); + } + + _mfpTrigger('LazyLoad', item); + + if(item.type === 'image') { + item.img = $('').on('load.mfploader', function() { + item.hasSize = true; + }).on('error.mfploader', function() { + item.hasSize = true; + item.loadError = true; + _mfpTrigger('LazyLoadError', item); + }).attr('src', item.src); + } + + + item.preloaded = true; + } + } +}); + +/*>>gallery*/ + +/*>>retina*/ + +var RETINA_NS = 'retina'; + +$.magnificPopup.registerModule(RETINA_NS, { + options: { + replaceSrc: function(item) { + return item.src.replace(/\.\w+$/, function(m) { return '@2x' + m; }); + }, + ratio: 1 // Function or number. Set to 1 to disable. + }, + proto: { + initRetina: function() { + if(window.devicePixelRatio > 1) { + + var st = mfp.st.retina, + ratio = st.ratio; + + ratio = !isNaN(ratio) ? ratio : ratio(); + + if(ratio > 1) { + _mfpOn('ImageHasSize' + '.' + RETINA_NS, function(e, item) { + item.img.css({ + 'max-width': item.img[0].naturalWidth / ratio, + 'width': '100%' + }); + }); + _mfpOn('ElementParse' + '.' + RETINA_NS, function(e, item) { + item.src = st.replaceSrc(item, ratio); + }); + } + } + + } + } +}); + +/*>>retina*/ + _checkInstance(); })); \ No newline at end of file diff --git a/public/static/libs/magnific-popup/magnific-popup.min.css b/public/static/libs/magnific-popup/magnific-popup.min.css new file mode 100644 index 0000000..e2d6074 --- /dev/null +++ b/public/static/libs/magnific-popup/magnific-popup.min.css @@ -0,0 +1,2 @@ +/* Magnific Popup CSS */ +.mfp-bg,.mfp-wrap{position:fixed;left:0;top:0}.mfp-bg,.mfp-container,.mfp-wrap{height:100%;width:100%}.mfp-arrow:after,.mfp-arrow:before,.mfp-container:before,.mfp-figure:after{content:''}.mfp-bg{z-index:1042;overflow:hidden;background:#0b0b0b;opacity:.8}.mfp-wrap{z-index:1043;outline:0!important;-webkit-backface-visibility:hidden}.mfp-container{text-align:center;position:absolute;left:0;top:0;padding:0 8px;box-sizing:border-box}.mfp-container:before{display:inline-block;height:100%;vertical-align:middle}.mfp-align-top .mfp-container:before{display:none}.mfp-content{position:relative;display:inline-block;vertical-align:middle;margin:0 auto;text-align:left;z-index:1045}.mfp-ajax-holder .mfp-content,.mfp-inline-holder .mfp-content{width:100%;cursor:auto}.mfp-ajax-cur{cursor:progress}.mfp-zoom-out-cur,.mfp-zoom-out-cur .mfp-image-holder .mfp-close{cursor:-moz-zoom-out;cursor:-webkit-zoom-out;cursor:zoom-out}.mfp-zoom{cursor:pointer;cursor:-webkit-zoom-in;cursor:-moz-zoom-in;cursor:zoom-in}.mfp-auto-cursor .mfp-content{cursor:auto}.mfp-arrow,.mfp-close,.mfp-counter,.mfp-preloader{-webkit-user-select:none;-moz-user-select:none;user-select:none}.mfp-loading.mfp-figure{display:none}.mfp-hide{display:none!important}.mfp-preloader{color:#CCC;position:absolute;top:50%;width:auto;text-align:center;margin-top:-.8em;left:8px;right:8px;z-index:1044}.mfp-preloader a{color:#CCC}.mfp-close,.mfp-preloader a:hover{color:#FFF}.mfp-s-error .mfp-content,.mfp-s-ready .mfp-preloader{display:none}button.mfp-arrow,button.mfp-close{overflow:visible;cursor:pointer;background:0 0;border:0;-webkit-appearance:none;display:block;outline:0;padding:0;z-index:1046;box-shadow:none;touch-action:manipulation}.mfp-figure:after,.mfp-iframe-scaler iframe{box-shadow:0 0 8px rgba(0,0,0,.6);position:absolute;left:0}button::-moz-focus-inner{padding:0;border:0}.mfp-close{width:44px;height:44px;line-height:44px;position:absolute;right:0;top:0;text-decoration:none;text-align:center;opacity:.65;padding:0 0 18px 10px;font-style:normal;font-size:28px;font-family:Arial,Baskerville,monospace}.mfp-close:focus,.mfp-close:hover{opacity:1}.mfp-close:active{top:1px}.mfp-close-btn-in .mfp-close{color:#333}.mfp-iframe-holder .mfp-close,.mfp-image-holder .mfp-close{color:#FFF;right:-6px;text-align:right;padding-right:6px;width:100%}.mfp-counter{position:absolute;top:0;right:0;color:#CCC;font-size:12px;line-height:18px;white-space:nowrap}.mfp-figure,img.mfp-img{line-height:0}.mfp-arrow{position:absolute;opacity:.65;margin:-55px 0 0;top:50%;padding:0;width:90px;height:110px;-webkit-tap-highlight-color:transparent}.mfp-arrow:active{margin-top:-54px}.mfp-arrow:focus,.mfp-arrow:hover{opacity:1}.mfp-arrow:after,.mfp-arrow:before{display:block;width:0;height:0;position:absolute;left:0;top:0;margin-top:35px;margin-left:35px;border:inset transparent}.mfp-arrow:after{border-top-width:13px;border-bottom-width:13px;top:8px}.mfp-arrow:before{border-top-width:21px;border-bottom-width:21px;opacity:.7}.mfp-arrow-left{left:0}.mfp-arrow-left:after{border-right:17px solid #FFF;margin-left:31px}.mfp-arrow-left:before{margin-left:25px;border-right:27px solid #3F3F3F}.mfp-arrow-right{right:0}.mfp-arrow-right:after{border-left:17px solid #FFF;margin-left:39px}.mfp-arrow-right:before{border-left:27px solid #3F3F3F}.mfp-iframe-holder{padding-top:40px;padding-bottom:40px}.mfp-iframe-holder .mfp-content{line-height:0;width:100%;max-width:900px}.mfp-image-holder .mfp-content,img.mfp-img{max-width:100%}.mfp-iframe-holder .mfp-close{top:-40px}.mfp-iframe-scaler{width:100%;height:0;overflow:hidden;padding-top:56.25%}.mfp-iframe-scaler iframe{display:block;top:0;width:100%;height:100%;background:#000}.mfp-figure:after,img.mfp-img{width:auto;height:auto;display:block}img.mfp-img{box-sizing:border-box;padding:40px 0;margin:0 auto}.mfp-figure:after{top:40px;bottom:40px;right:0;z-index:-1;background:#444}.mfp-figure small{color:#BDBDBD;display:block;font-size:12px;line-height:14px}.mfp-figure figure{margin:0}.mfp-bottom-bar{margin-top:-36px;position:absolute;top:100%;left:0;width:100%;cursor:auto}.mfp-title{text-align:left;line-height:18px;color:#F3F3F3;word-wrap:break-word;padding-right:36px}.mfp-gallery .mfp-image-holder .mfp-figure{cursor:pointer}@media screen and (max-width:800px) and (orientation:landscape),screen and (max-height:300px){.mfp-img-mobile .mfp-image-holder{padding-left:0;padding-right:0}.mfp-img-mobile img.mfp-img{padding:0}.mfp-img-mobile .mfp-figure:after{top:0;bottom:0}.mfp-img-mobile .mfp-figure small{display:inline;margin-left:5px}.mfp-img-mobile .mfp-bottom-bar{background:rgba(0,0,0,.6);bottom:0;margin:0;top:auto;padding:3px 5px;position:fixed;box-sizing:border-box}.mfp-img-mobile .mfp-bottom-bar:empty{padding:0}.mfp-img-mobile .mfp-counter{right:5px;top:3px}.mfp-img-mobile .mfp-close{top:0;right:0;width:35px;height:35px;line-height:35px;background:rgba(0,0,0,.6);position:fixed;text-align:center;padding:0}}@media all and (max-width:900px){.mfp-arrow{-webkit-transform:scale(.75);transform:scale(.75)}.mfp-arrow-left{-webkit-transform-origin:0;transform-origin:0}.mfp-arrow-right{-webkit-transform-origin:100%;transform-origin:100%}.mfp-container{padding-left:6px;padding-right:6px}} \ No newline at end of file diff --git a/public/static/libs/magnific-popup/magnific-popup.min.js b/public/static/libs/magnific-popup/magnific-popup.min.js new file mode 100644 index 0000000..82d02fd --- /dev/null +++ b/public/static/libs/magnific-popup/magnific-popup.min.js @@ -0,0 +1,4 @@ +/*! Magnific Popup - v1.1.0 - 2016-02-20 +* http://dimsemenov.com/plugins/magnific-popup/ +* Copyright (c) 2016 Dmitry Semenov; */ +!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):window.jQuery||window.Zepto)}(function(a){var b,c,d,e,f,g,h="Close",i="BeforeClose",j="AfterClose",k="BeforeAppend",l="MarkupParse",m="Open",n="Change",o="mfp",p="."+o,q="mfp-ready",r="mfp-removing",s="mfp-prevent-close",t=function(){},u=!!window.jQuery,v=a(window),w=function(a,c){b.ev.on(o+a+p,c)},x=function(b,c,d,e){var f=document.createElement("div");return f.className="mfp-"+b,d&&(f.innerHTML=d),e?c&&c.appendChild(f):(f=a(f),c&&f.appendTo(c)),f},y=function(c,d){b.ev.triggerHandler(o+c,d),b.st.callbacks&&(c=c.charAt(0).toLowerCase()+c.slice(1),b.st.callbacks[c]&&b.st.callbacks[c].apply(b,a.isArray(d)?d:[d]))},z=function(c){return c===g&&b.currTemplate.closeBtn||(b.currTemplate.closeBtn=a(b.st.closeMarkup.replace("%title%",b.st.tClose)),g=c),b.currTemplate.closeBtn},A=function(){a.magnificPopup.instance||(b=new t,b.init(),a.magnificPopup.instance=b)},B=function(){var a=document.createElement("p").style,b=["ms","O","Moz","Webkit"];if(void 0!==a.transition)return!0;for(;b.length;)if(b.pop()+"Transition"in a)return!0;return!1};t.prototype={constructor:t,init:function(){var c=navigator.appVersion;b.isLowIE=b.isIE8=document.all&&!document.addEventListener,b.isAndroid=/android/gi.test(c),b.isIOS=/iphone|ipad|ipod/gi.test(c),b.supportsTransition=B(),b.probablyMobile=b.isAndroid||b.isIOS||/(Opera Mini)|Kindle|webOS|BlackBerry|(Opera Mobi)|(Windows Phone)|IEMobile/i.test(navigator.userAgent),d=a(document),b.popupsCache={}},open:function(c){var e;if(c.isObj===!1){b.items=c.items.toArray(),b.index=0;var g,h=c.items;for(e=0;e(a||v.height())},_setFocus:function(){(b.st.focus?b.content.find(b.st.focus).eq(0):b.wrap).focus()},_onFocusIn:function(c){return c.target===b.wrap[0]||a.contains(b.wrap[0],c.target)?void 0:(b._setFocus(),!1)},_parseMarkup:function(b,c,d){var e;d.data&&(c=a.extend(d.data,c)),y(l,[b,c,d]),a.each(c,function(c,d){if(void 0===d||d===!1)return!0;if(e=c.split("_"),e.length>1){var f=b.find(p+"-"+e[0]);if(f.length>0){var g=e[1];"replaceWith"===g?f[0]!==d[0]&&f.replaceWith(d):"img"===g?f.is("img")?f.attr("src",d):f.replaceWith(a("").attr("src",d).attr("class",f.attr("class"))):f.attr(e[1],d)}}else b.find(p+"-"+c).html(d)})},_getScrollbarSize:function(){if(void 0===b.scrollbarSize){var a=document.createElement("div");a.style.cssText="width: 99px; height: 99px; overflow: scroll; position: absolute; top: -9999px;",document.body.appendChild(a),b.scrollbarSize=a.offsetWidth-a.clientWidth,document.body.removeChild(a)}return b.scrollbarSize}},a.magnificPopup={instance:null,proto:t.prototype,modules:[],open:function(b,c){return A(),b=b?a.extend(!0,{},b):{},b.isObj=!0,b.index=c||0,this.instance.open(b)},close:function(){return a.magnificPopup.instance&&a.magnificPopup.instance.close()},registerModule:function(b,c){c.options&&(a.magnificPopup.defaults[b]=c.options),a.extend(this.proto,c.proto),this.modules.push(b)},defaults:{disableOn:0,key:null,midClick:!1,mainClass:"",preloader:!0,focus:"",closeOnContentClick:!1,closeOnBgClick:!0,closeBtnInside:!0,showCloseBtn:!0,enableEscapeKey:!0,modal:!1,alignTop:!1,removalDelay:0,prependTo:null,fixedContentPos:"auto",fixedBgPos:"auto",overflowY:"auto",closeMarkup:'',tClose:"Close (Esc)",tLoading:"Loading...",autoFocusLast:!0}},a.fn.magnificPopup=function(c){A();var d=a(this);if("string"==typeof c)if("open"===c){var e,f=u?d.data("magnificPopup"):d[0].magnificPopup,g=parseInt(arguments[1],10)||0;f.items?e=f.items[g]:(e=d,f.delegate&&(e=e.find(f.delegate)),e=e.eq(g)),b._openClick({mfpEl:e},d,f)}else b.isOpen&&b[c].apply(b,Array.prototype.slice.call(arguments,1));else c=a.extend(!0,{},c),u?d.data("magnificPopup",c):d[0].magnificPopup=c,b.addGroup(d,c);return d};var C,D,E,F="inline",G=function(){E&&(D.after(E.addClass(C)).detach(),E=null)};a.magnificPopup.registerModule(F,{options:{hiddenClass:"hide",markup:"",tNotFound:"Content not found"},proto:{initInline:function(){b.types.push(F),w(h+"."+F,function(){G()})},getInline:function(c,d){if(G(),c.src){var e=b.st.inline,f=a(c.src);if(f.length){var g=f[0].parentNode;g&&g.tagName&&(D||(C=e.hiddenClass,D=x(C),C="mfp-"+C),E=f.after(D).detach().removeClass(C)),b.updateStatus("ready")}else b.updateStatus("error",e.tNotFound),f=a("
    ");return c.inlineElement=f,f}return b.updateStatus("ready"),b._parseMarkup(d,{},c),d}}});var H,I="ajax",J=function(){H&&a(document.body).removeClass(H)},K=function(){J(),b.req&&b.req.abort()};a.magnificPopup.registerModule(I,{options:{settings:null,cursor:"mfp-ajax-cur",tError:'The content could not be loaded.'},proto:{initAjax:function(){b.types.push(I),H=b.st.ajax.cursor,w(h+"."+I,K),w("BeforeChange."+I,K)},getAjax:function(c){H&&a(document.body).addClass(H),b.updateStatus("loading");var d=a.extend({url:c.src,success:function(d,e,f){var g={data:d,xhr:f};y("ParseAjax",g),b.appendContent(a(g.data),I),c.finished=!0,J(),b._setFocus(),setTimeout(function(){b.wrap.addClass(q)},16),b.updateStatus("ready"),y("AjaxContentAdded")},error:function(){J(),c.finished=c.loadError=!0,b.updateStatus("error",b.st.ajax.tError.replace("%url%",c.src))}},b.st.ajax.settings);return b.req=a.ajax(d),""}}});var L,M=function(c){if(c.data&&void 0!==c.data.title)return c.data.title;var d=b.st.image.titleSrc;if(d){if(a.isFunction(d))return d.call(b,c);if(c.el)return c.el.attr(d)||""}return""};a.magnificPopup.registerModule("image",{options:{markup:'
    ',cursor:"mfp-zoom-out-cur",titleSrc:"title",verticalFit:!0,tError:'The image could not be loaded.'},proto:{initImage:function(){var c=b.st.image,d=".image";b.types.push("image"),w(m+d,function(){"image"===b.currItem.type&&c.cursor&&a(document.body).addClass(c.cursor)}),w(h+d,function(){c.cursor&&a(document.body).removeClass(c.cursor),v.off("resize"+p)}),w("Resize"+d,b.resizeImage),b.isLowIE&&w("AfterChange",b.resizeImage)},resizeImage:function(){var a=b.currItem;if(a&&a.img&&b.st.image.verticalFit){var c=0;b.isLowIE&&(c=parseInt(a.img.css("padding-top"),10)+parseInt(a.img.css("padding-bottom"),10)),a.img.css("max-height",b.wH-c)}},_onImageHasSize:function(a){a.img&&(a.hasSize=!0,L&&clearInterval(L),a.isCheckingImgSize=!1,y("ImageHasSize",a),a.imgHidden&&(b.content&&b.content.removeClass("mfp-loading"),a.imgHidden=!1))},findImageSize:function(a){var c=0,d=a.img[0],e=function(f){L&&clearInterval(L),L=setInterval(function(){return d.naturalWidth>0?void b._onImageHasSize(a):(c>200&&clearInterval(L),c++,void(3===c?e(10):40===c?e(50):100===c&&e(500)))},f)};e(1)},getImage:function(c,d){var e=0,f=function(){c&&(c.img[0].complete?(c.img.off(".mfploader"),c===b.currItem&&(b._onImageHasSize(c),b.updateStatus("ready")),c.hasSize=!0,c.loaded=!0,y("ImageLoadComplete")):(e++,200>e?setTimeout(f,100):g()))},g=function(){c&&(c.img.off(".mfploader"),c===b.currItem&&(b._onImageHasSize(c),b.updateStatus("error",h.tError.replace("%url%",c.src))),c.hasSize=!0,c.loaded=!0,c.loadError=!0)},h=b.st.image,i=d.find(".mfp-img");if(i.length){var j=document.createElement("img");j.className="mfp-img",c.el&&c.el.find("img").length&&(j.alt=c.el.find("img").attr("alt")),c.img=a(j).on("load.mfploader",f).on("error.mfploader",g),j.src=c.src,i.is("img")&&(c.img=c.img.clone()),j=c.img[0],j.naturalWidth>0?c.hasSize=!0:j.width||(c.hasSize=!1)}return b._parseMarkup(d,{title:M(c),img_replaceWith:c.img},c),b.resizeImage(),c.hasSize?(L&&clearInterval(L),c.loadError?(d.addClass("mfp-loading"),b.updateStatus("error",h.tError.replace("%url%",c.src))):(d.removeClass("mfp-loading"),b.updateStatus("ready")),d):(b.updateStatus("loading"),c.loading=!0,c.hasSize||(c.imgHidden=!0,d.addClass("mfp-loading"),b.findImageSize(c)),d)}}});var N,O=function(){return void 0===N&&(N=void 0!==document.createElement("p").style.MozTransform),N};a.magnificPopup.registerModule("zoom",{options:{enabled:!1,easing:"ease-in-out",duration:300,opener:function(a){return a.is("img")?a:a.find("img")}},proto:{initZoom:function(){var a,c=b.st.zoom,d=".zoom";if(c.enabled&&b.supportsTransition){var e,f,g=c.duration,j=function(a){var b=a.clone().removeAttr("style").removeAttr("class").addClass("mfp-animated-image"),d="all "+c.duration/1e3+"s "+c.easing,e={position:"fixed",zIndex:9999,left:0,top:0,"-webkit-backface-visibility":"hidden"},f="transition";return e["-webkit-"+f]=e["-moz-"+f]=e["-o-"+f]=e[f]=d,b.css(e),b},k=function(){b.content.css("visibility","visible")};w("BuildControls"+d,function(){if(b._allowZoom()){if(clearTimeout(e),b.content.css("visibility","hidden"),a=b._getItemToZoom(),!a)return void k();f=j(a),f.css(b._getOffset()),b.wrap.append(f),e=setTimeout(function(){f.css(b._getOffset(!0)),e=setTimeout(function(){k(),setTimeout(function(){f.remove(),a=f=null,y("ZoomAnimationEnded")},16)},g)},16)}}),w(i+d,function(){if(b._allowZoom()){if(clearTimeout(e),b.st.removalDelay=g,!a){if(a=b._getItemToZoom(),!a)return;f=j(a)}f.css(b._getOffset(!0)),b.wrap.append(f),b.content.css("visibility","hidden"),setTimeout(function(){f.css(b._getOffset())},16)}}),w(h+d,function(){b._allowZoom()&&(k(),f&&f.remove(),a=null)})}},_allowZoom:function(){return"image"===b.currItem.type},_getItemToZoom:function(){return b.currItem.hasSize?b.currItem.img:!1},_getOffset:function(c){var d;d=c?b.currItem.img:b.st.zoom.opener(b.currItem.el||b.currItem);var e=d.offset(),f=parseInt(d.css("padding-top"),10),g=parseInt(d.css("padding-bottom"),10);e.top-=a(window).scrollTop()-f;var h={width:d.width(),height:(u?d.innerHeight():d[0].offsetHeight)-g-f};return O()?h["-moz-transform"]=h.transform="translate("+e.left+"px,"+e.top+"px)":(h.left=e.left,h.top=e.top),h}}});var P="iframe",Q="//about:blank",R=function(a){if(b.currTemplate[P]){var c=b.currTemplate[P].find("iframe");c.length&&(a||(c[0].src=Q),b.isIE8&&c.css("display",a?"block":"none"))}};a.magnificPopup.registerModule(P,{options:{markup:'
    ',srcAction:"iframe_src",patterns:{youtube:{index:"youtube.com",id:"v=",src:"//www.youtube.com/embed/%id%?autoplay=1"},vimeo:{index:"vimeo.com/",id:"/",src:"//player.vimeo.com/video/%id%?autoplay=1"},gmaps:{index:"//maps.google.",src:"%id%&output=embed"}}},proto:{initIframe:function(){b.types.push(P),w("BeforeChange",function(a,b,c){b!==c&&(b===P?R():c===P&&R(!0))}),w(h+"."+P,function(){R()})},getIframe:function(c,d){var e=c.src,f=b.st.iframe;a.each(f.patterns,function(){return e.indexOf(this.index)>-1?(this.id&&(e="string"==typeof this.id?e.substr(e.lastIndexOf(this.id)+this.id.length,e.length):this.id.call(this,e)),e=this.src.replace("%id%",e),!1):void 0});var g={};return f.srcAction&&(g[f.srcAction]=e),b._parseMarkup(d,g,c),b.updateStatus("ready"),d}}});var S=function(a){var c=b.items.length;return a>c-1?a-c:0>a?c+a:a},T=function(a,b,c){return a.replace(/%curr%/gi,b+1).replace(/%total%/gi,c)};a.magnificPopup.registerModule("gallery",{options:{enabled:!1,arrowMarkup:'',preload:[0,2],navigateByImgClick:!0,arrows:!0,tPrev:"Previous (Left arrow key)",tNext:"Next (Right arrow key)",tCounter:"%curr% of %total%"},proto:{initGallery:function(){var c=b.st.gallery,e=".mfp-gallery";return b.direction=!0,c&&c.enabled?(f+=" mfp-gallery",w(m+e,function(){c.navigateByImgClick&&b.wrap.on("click"+e,".mfp-img",function(){return b.items.length>1?(b.next(),!1):void 0}),d.on("keydown"+e,function(a){37===a.keyCode?b.prev():39===a.keyCode&&b.next()})}),w("UpdateStatus"+e,function(a,c){c.text&&(c.text=T(c.text,b.currItem.index,b.items.length))}),w(l+e,function(a,d,e,f){var g=b.items.length;e.counter=g>1?T(c.tCounter,f.index,g):""}),w("BuildControls"+e,function(){if(b.items.length>1&&c.arrows&&!b.arrowLeft){var d=c.arrowMarkup,e=b.arrowLeft=a(d.replace(/%title%/gi,c.tPrev).replace(/%dir%/gi,"left")).addClass(s),f=b.arrowRight=a(d.replace(/%title%/gi,c.tNext).replace(/%dir%/gi,"right")).addClass(s);e.click(function(){b.prev()}),f.click(function(){b.next()}),b.container.append(e.add(f))}}),w(n+e,function(){b._preloadTimeout&&clearTimeout(b._preloadTimeout),b._preloadTimeout=setTimeout(function(){b.preloadNearbyImages(),b._preloadTimeout=null},16)}),void w(h+e,function(){d.off(e),b.wrap.off("click"+e),b.arrowRight=b.arrowLeft=null})):!1},next:function(){b.direction=!0,b.index=S(b.index+1),b.updateItemHTML()},prev:function(){b.direction=!1,b.index=S(b.index-1),b.updateItemHTML()},goTo:function(a){b.direction=a>=b.index,b.index=a,b.updateItemHTML()},preloadNearbyImages:function(){var a,c=b.st.gallery.preload,d=Math.min(c[0],b.items.length),e=Math.min(c[1],b.items.length);for(a=1;a<=(b.direction?e:d);a++)b._preloadItem(b.index+a);for(a=1;a<=(b.direction?d:e);a++)b._preloadItem(b.index-a)},_preloadItem:function(c){if(c=S(c),!b.items[c].preloaded){var d=b.items[c];d.parsed||(d=b.parseEl(c)),y("LazyLoad",d),"image"===d.type&&(d.img=a('').on("load.mfploader",function(){d.hasSize=!0}).on("error.mfploader",function(){d.hasSize=!0,d.loadError=!0,y("LazyLoadError",d)}).attr("src",d.src)),d.preloaded=!0}}}});var U="retina";a.magnificPopup.registerModule(U,{options:{replaceSrc:function(a){return a.src.replace(/\.\w+$/,function(a){return"@2x"+a})},ratio:1},proto:{initRetina:function(){if(window.devicePixelRatio>1){var a=b.st.retina,c=a.ratio;c=isNaN(c)?c():c,c>1&&(w("ImageHasSize."+U,function(a,b){b.img.css({"max-width":b.img[0].naturalWidth/c,width:"100%"})}),w("ElementParse."+U,function(b,d){d.src=a.replaceSrc(d,c)}))}}}}),A()}); \ No newline at end of file diff --git a/public/static/libs/masked-inputs/jquery.maskedinput.js b/public/static/libs/masked-inputs/jquery.maskedinput.js new file mode 100644 index 0000000..5511a3e --- /dev/null +++ b/public/static/libs/masked-inputs/jquery.maskedinput.js @@ -0,0 +1,182 @@ +/* + jQuery Masked Input Plugin + Copyright (c) 2007 - 2015 Josh Bush (digitalbush.com) + Licensed under the MIT license (http://digitalbush.com/projects/masked-input-plugin/#license) + Version: 1.4.1 +*/ +!function(factory) { + "function" == typeof define && define.amd ? define([ "jquery" ], factory) : factory("object" == typeof exports ? require("jquery") : jQuery); +}(function($) { + var caretTimeoutId, ua = navigator.userAgent, iPhone = /iphone/i.test(ua), chrome = /chrome/i.test(ua), android = /android/i.test(ua); + $.mask = { + definitions: { + "9": "[0-9]", + a: "[A-Za-z]", + "*": "[A-Za-z0-9]" + }, + autoclear: !0, + dataName: "rawMaskFn", + placeholder: "_" + }, $.fn.extend({ + caret: function(begin, end) { + var range; + if (0 !== this.length && !this.is(":hidden")) return "number" == typeof begin ? (end = "number" == typeof end ? end : begin, + this.each(function() { + this.setSelectionRange ? this.setSelectionRange(begin, end) : this.createTextRange && (range = this.createTextRange(), + range.collapse(!0), range.moveEnd("character", end), range.moveStart("character", begin), + range.select()); + })) : (this[0].setSelectionRange ? (begin = this[0].selectionStart, end = this[0].selectionEnd) : document.selection && document.selection.createRange && (range = document.selection.createRange(), + begin = 0 - range.duplicate().moveStart("character", -1e5), end = begin + range.text.length), + { + begin: begin, + end: end + }); + }, + unmask: function() { + return this.trigger("unmask"); + }, + mask: function(mask, settings) { + var input, defs, tests, partialPosition, firstNonMaskPos, lastRequiredNonMaskPos, len, oldVal; + if (!mask && this.length > 0) { + input = $(this[0]); + var fn = input.data($.mask.dataName); + return fn ? fn() : void 0; + } + return settings = $.extend({ + autoclear: $.mask.autoclear, + placeholder: $.mask.placeholder, + completed: null + }, settings), defs = $.mask.definitions, tests = [], partialPosition = len = mask.length, + firstNonMaskPos = null, $.each(mask.split(""), function(i, c) { + "?" == c ? (len--, partialPosition = i) : defs[c] ? (tests.push(new RegExp(defs[c])), + null === firstNonMaskPos && (firstNonMaskPos = tests.length - 1), partialPosition > i && (lastRequiredNonMaskPos = tests.length - 1)) : tests.push(null); + }), this.trigger("unmask").each(function() { + function tryFireCompleted() { + if (settings.completed) { + for (var i = firstNonMaskPos; lastRequiredNonMaskPos >= i; i++) if (tests[i] && buffer[i] === getPlaceholder(i)) return; + settings.completed.call(input); + } + } + function getPlaceholder(i) { + return settings.placeholder.charAt(i < settings.placeholder.length ? i : 0); + } + function seekNext(pos) { + for (;++pos < len && !tests[pos]; ) ; + return pos; + } + function seekPrev(pos) { + for (;--pos >= 0 && !tests[pos]; ) ; + return pos; + } + function shiftL(begin, end) { + var i, j; + if (!(0 > begin)) { + for (i = begin, j = seekNext(end); len > i; i++) if (tests[i]) { + if (!(len > j && tests[i].test(buffer[j]))) break; + buffer[i] = buffer[j], buffer[j] = getPlaceholder(j), j = seekNext(j); + } + writeBuffer(), input.caret(Math.max(firstNonMaskPos, begin)); + } + } + function shiftR(pos) { + var i, c, j, t; + for (i = pos, c = getPlaceholder(pos); len > i; i++) if (tests[i]) { + if (j = seekNext(i), t = buffer[i], buffer[i] = c, !(len > j && tests[j].test(t))) break; + c = t; + } + } + function androidInputEvent() { + var curVal = input.val(), pos = input.caret(); + if (oldVal && oldVal.length && oldVal.length > curVal.length) { + for (checkVal(!0); pos.begin > 0 && !tests[pos.begin - 1]; ) pos.begin--; + if (0 === pos.begin) for (;pos.begin < firstNonMaskPos && !tests[pos.begin]; ) pos.begin++; + input.caret(pos.begin, pos.begin); + } else { + for (checkVal(!0); pos.begin < len && !tests[pos.begin]; ) pos.begin++; + input.caret(pos.begin, pos.begin); + } + tryFireCompleted(); + } + function blurEvent() { + checkVal(), input.val() != focusText && input.change(); + } + function keydownEvent(e) { + if (!input.prop("readonly")) { + var pos, begin, end, k = e.which || e.keyCode; + oldVal = input.val(), 8 === k || 46 === k || iPhone && 127 === k ? (pos = input.caret(), + begin = pos.begin, end = pos.end, end - begin === 0 && (begin = 46 !== k ? seekPrev(begin) : end = seekNext(begin - 1), + end = 46 === k ? seekNext(end) : end), clearBuffer(begin, end), shiftL(begin, end - 1), + e.preventDefault()) : 13 === k ? blurEvent.call(this, e) : 27 === k && (input.val(focusText), + input.caret(0, checkVal()), e.preventDefault()); + } + } + function keypressEvent(e) { + if (!input.prop("readonly")) { + var p, c, next, k = e.which || e.keyCode, pos = input.caret(); + if (!(e.ctrlKey || e.altKey || e.metaKey || 32 > k) && k && 13 !== k) { + if (pos.end - pos.begin !== 0 && (clearBuffer(pos.begin, pos.end), shiftL(pos.begin, pos.end - 1)), + p = seekNext(pos.begin - 1), len > p && (c = String.fromCharCode(k), tests[p].test(c))) { + if (shiftR(p), buffer[p] = c, writeBuffer(), next = seekNext(p), android) { + var proxy = function() { + $.proxy($.fn.caret, input, next)(); + }; + setTimeout(proxy, 0); + } else input.caret(next); + pos.begin <= lastRequiredNonMaskPos && tryFireCompleted(); + } + e.preventDefault(); + } + } + } + function clearBuffer(start, end) { + var i; + for (i = start; end > i && len > i; i++) tests[i] && (buffer[i] = getPlaceholder(i)); + } + function writeBuffer() { + input.val(buffer.join("")); + } + function checkVal(allow) { + var i, c, pos, test = input.val(), lastMatch = -1; + for (i = 0, pos = 0; len > i; i++) if (tests[i]) { + for (buffer[i] = getPlaceholder(i); pos++ < test.length; ) if (c = test.charAt(pos - 1), + tests[i].test(c)) { + buffer[i] = c, lastMatch = i; + break; + } + if (pos > test.length) { + clearBuffer(i + 1, len); + break; + } + } else buffer[i] === test.charAt(pos) && pos++, partialPosition > i && (lastMatch = i); + return allow ? writeBuffer() : partialPosition > lastMatch + 1 ? settings.autoclear || buffer.join("") === defaultBuffer ? (input.val() && input.val(""), + clearBuffer(0, len)) : writeBuffer() : (writeBuffer(), input.val(input.val().substring(0, lastMatch + 1))), + partialPosition ? i : firstNonMaskPos; + } + var input = $(this), buffer = $.map(mask.split(""), function(c, i) { + return "?" != c ? defs[c] ? getPlaceholder(i) : c : void 0; + }), defaultBuffer = buffer.join(""), focusText = input.val(); + input.data($.mask.dataName, function() { + return $.map(buffer, function(c, i) { + return tests[i] && c != getPlaceholder(i) ? c : null; + }).join(""); + }), input.one("unmask", function() { + input.off(".mask").removeData($.mask.dataName); + }).on("focus.mask", function() { + if (!input.prop("readonly")) { + clearTimeout(caretTimeoutId); + var pos; + focusText = input.val(), pos = checkVal(), caretTimeoutId = setTimeout(function() { + input.get(0) === document.activeElement && (writeBuffer(), pos == mask.replace("?", "").length ? input.caret(0, pos) : input.caret(pos)); + }, 10); + } + }).on("blur.mask", blurEvent).on("keydown.mask", keydownEvent).on("keypress.mask", keypressEvent).on("input.mask paste.mask", function() { + input.prop("readonly") || setTimeout(function() { + var pos = checkVal(!0); + input.caret(pos), tryFireCompleted(); + }, 0); + }), chrome && android && input.off("input.mask").on("input.mask", androidInputEvent), + checkVal(); + }); + } + }); +}); \ No newline at end of file diff --git a/public/static/libs/masked-inputs/jquery.maskedinput.min.js b/public/static/libs/masked-inputs/jquery.maskedinput.min.js new file mode 100644 index 0000000..e4112e3 --- /dev/null +++ b/public/static/libs/masked-inputs/jquery.maskedinput.min.js @@ -0,0 +1,7 @@ +/* + jQuery Masked Input Plugin + Copyright (c) 2007 - 2015 Josh Bush (digitalbush.com) + Licensed under the MIT license (http://digitalbush.com/projects/masked-input-plugin/#license) + Version: 1.4.1 +*/ +!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):jQuery)}(function(a){var b,c=navigator.userAgent,d=/iphone/i.test(c),e=/chrome/i.test(c),f=/android/i.test(c);a.mask={definitions:{9:"[0-9]",a:"[A-Za-z]","*":"[A-Za-z0-9]"},autoclear:!0,dataName:"rawMaskFn",placeholder:"_"},a.fn.extend({caret:function(a,b){var c;if(0!==this.length&&!this.is(":hidden"))return"number"==typeof a?(b="number"==typeof b?b:a,this.each(function(){this.setSelectionRange?this.setSelectionRange(a,b):this.createTextRange&&(c=this.createTextRange(),c.collapse(!0),c.moveEnd("character",b),c.moveStart("character",a),c.select())})):(this[0].setSelectionRange?(a=this[0].selectionStart,b=this[0].selectionEnd):document.selection&&document.selection.createRange&&(c=document.selection.createRange(),a=0-c.duplicate().moveStart("character",-1e5),b=a+c.text.length),{begin:a,end:b})},unmask:function(){return this.trigger("unmask")},mask:function(c,g){var h,i,j,k,l,m,n,o;if(!c&&this.length>0){h=a(this[0]);var p=h.data(a.mask.dataName);return p?p():void 0}return g=a.extend({autoclear:a.mask.autoclear,placeholder:a.mask.placeholder,completed:null},g),i=a.mask.definitions,j=[],k=n=c.length,l=null,a.each(c.split(""),function(a,b){"?"==b?(n--,k=a):i[b]?(j.push(new RegExp(i[b])),null===l&&(l=j.length-1),k>a&&(m=j.length-1)):j.push(null)}),this.trigger("unmask").each(function(){function h(){if(g.completed){for(var a=l;m>=a;a++)if(j[a]&&C[a]===p(a))return;g.completed.call(B)}}function p(a){return g.placeholder.charAt(a=0&&!j[a];);return a}function s(a,b){var c,d;if(!(0>a)){for(c=a,d=q(b);n>c;c++)if(j[c]){if(!(n>d&&j[c].test(C[d])))break;C[c]=C[d],C[d]=p(d),d=q(d)}z(),B.caret(Math.max(l,a))}}function t(a){var b,c,d,e;for(b=a,c=p(a);n>b;b++)if(j[b]){if(d=q(b),e=C[b],C[b]=c,!(n>d&&j[d].test(e)))break;c=e}}function u(){var a=B.val(),b=B.caret();if(o&&o.length&&o.length>a.length){for(A(!0);b.begin>0&&!j[b.begin-1];)b.begin--;if(0===b.begin)for(;b.beging)&&g&&13!==g){if(i.end-i.begin!==0&&(y(i.begin,i.end),s(i.begin,i.end-1)),c=q(i.begin-1),n>c&&(d=String.fromCharCode(g),j[c].test(d))){if(t(c),C[c]=d,z(),e=q(c),f){var k=function(){a.proxy(a.fn.caret,B,e)()};setTimeout(k,0)}else B.caret(e);i.begin<=m&&h()}b.preventDefault()}}}function y(a,b){var c;for(c=a;b>c&&n>c;c++)j[c]&&(C[c]=p(c))}function z(){B.val(C.join(""))}function A(a){var b,c,d,e=B.val(),f=-1;for(b=0,d=0;n>b;b++)if(j[b]){for(C[b]=p(b);d++e.length){y(b+1,n);break}}else C[b]===e.charAt(d)&&d++,k>b&&(f=b);return a?z():k>f+1?g.autoclear||C.join("")===D?(B.val()&&B.val(""),y(0,n)):z():(z(),B.val(B.val().substring(0,f+1))),k?b:l}var B=a(this),C=a.map(c.split(""),function(a,b){return"?"!=a?i[a]?p(b):a:void 0}),D=C.join(""),E=B.val();B.data(a.mask.dataName,function(){return a.map(C,function(a,b){return j[b]&&a!=p(b)?a:null}).join("")}),B.one("unmask",function(){B.off(".mask").removeData(a.mask.dataName)}).on("focus.mask",function(){if(!B.prop("readonly")){clearTimeout(b);var a;E=B.val(),a=A(),b=setTimeout(function(){B.get(0)===document.activeElement&&(z(),a==c.replace("?","").length?B.caret(0,a):B.caret(a))},10)}}).on("blur.mask",v).on("keydown.mask",w).on("keypress.mask",x).on("input.mask paste.mask",function(){B.prop("readonly")||setTimeout(function(){var a=A(!0);B.caret(a),h()},0)}),e&&f&&B.off("input.mask").on("input.mask",u),A()})}})}); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/ar.js b/public/static/libs/select2/i18n/ar.js new file mode 100644 index 0000000..7ee7541 --- /dev/null +++ b/public/static/libs/select2/i18n/ar.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ar",[],function(){return{errorLoading:function(){return"لا يمكن تحميل النتائج"},inputTooLong:function(e){var t=e.input.length-e.maximum,n="الرجاء حذف "+t+" عناصر";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="الرجاء إضافة "+t+" عناصر";return n},loadingMore:function(){return"جاري تحميل نتائج إضافية..."},maximumSelected:function(e){var t="تستطيع إختيار "+e.maximum+" بنود فقط";return t},noResults:function(){return"لم يتم العثور على أي نتائج"},searching:function(){return"جاري البحث…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/az.js b/public/static/libs/select2/i18n/az.js new file mode 100644 index 0000000..2162fc7 --- /dev/null +++ b/public/static/libs/select2/i18n/az.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/az",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum;return t+" simvol silin"},inputTooShort:function(e){var t=e.minimum-e.input.length;return t+" simvol daxil edin"},loadingMore:function(){return"Daha çox nəticə yüklənir…"},maximumSelected:function(e){return"Sadəcə "+e.maximum+" element seçə bilərsiniz"},noResults:function(){return"Nəticə tapılmadı"},searching:function(){return"Axtarılır…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/bg.js b/public/static/libs/select2/i18n/bg.js new file mode 100644 index 0000000..06ff761 --- /dev/null +++ b/public/static/libs/select2/i18n/bg.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/bg",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Моля въведете с "+t+" по-малко символ";return t>1&&(n+="a"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Моля въведете още "+t+" символ";return t>1&&(n+="a"),n},loadingMore:function(){return"Зареждат се още…"},maximumSelected:function(e){var t="Можете да направите до "+e.maximum+" ";return e.maximum>1?t+="избора":t+="избор",t},noResults:function(){return"Няма намерени съвпадения"},searching:function(){return"Търсене…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/ca.js b/public/static/libs/select2/i18n/ca.js new file mode 100644 index 0000000..0e7593a --- /dev/null +++ b/public/static/libs/select2/i18n/ca.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ca",[],function(){return{errorLoading:function(){return"La càrrega ha fallat"},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Si us plau, elimina "+t+" car";return t==1?n+="àcter":n+="àcters",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Si us plau, introdueix "+t+" car";return t==1?n+="àcter":n+="àcters",n},loadingMore:function(){return"Carregant més resultats…"},maximumSelected:function(e){var t="Només es pot seleccionar "+e.maximum+" element";return e.maximum!=1&&(t+="s"),t},noResults:function(){return"No s'han trobat resultats"},searching:function(){return"Cercant…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/cs.js b/public/static/libs/select2/i18n/cs.js new file mode 100644 index 0000000..c0f1cef --- /dev/null +++ b/public/static/libs/select2/i18n/cs.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/cs",[],function(){function e(e,t){switch(e){case 2:return t?"dva":"dvě";case 3:return"tři";case 4:return"čtyři"}return""}return{errorLoading:function(){return"Výsledky nemohly být načteny."},inputTooLong:function(t){var n=t.input.length-t.maximum;return n==1?"Prosím zadejte o jeden znak méně":n<=4?"Prosím zadejte o "+e(n,!0)+" znaky méně":"Prosím zadejte o "+n+" znaků méně"},inputTooShort:function(t){var n=t.minimum-t.input.length;return n==1?"Prosím zadejte ještě jeden znak":n<=4?"Prosím zadejte ještě další "+e(n,!0)+" znaky":"Prosím zadejte ještě dalších "+n+" znaků"},loadingMore:function(){return"Načítají se další výsledky…"},maximumSelected:function(t){var n=t.maximum;return n==1?"Můžete zvolit jen jednu položku":n<=4?"Můžete zvolit maximálně "+e(n,!1)+" položky":"Můžete zvolit maximálně "+n+" položek"},noResults:function(){return"Nenalezeny žádné položky"},searching:function(){return"Vyhledávání…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/da.js b/public/static/libs/select2/i18n/da.js new file mode 100644 index 0000000..9df2d87 --- /dev/null +++ b/public/static/libs/select2/i18n/da.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/da",[],function(){return{errorLoading:function(){return"Resultaterne kunne ikke indlæses."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Angiv venligst "+t+" tegn mindre";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Angiv venligst "+t+" tegn mere";return n},loadingMore:function(){return"Indlæser flere resultater…"},maximumSelected:function(e){var t="Du kan kun vælge "+e.maximum+" emne";return e.maximum!=1&&(t+="r"),t},noResults:function(){return"Ingen resultater fundet"},searching:function(){return"Søger…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/de.js b/public/static/libs/select2/i18n/de.js new file mode 100644 index 0000000..fe979c3 --- /dev/null +++ b/public/static/libs/select2/i18n/de.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/de",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum;return"Bitte "+t+" Zeichen weniger eingeben"},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Bitte "+t+" Zeichen mehr eingeben"},loadingMore:function(){return"Lade mehr Ergebnisse…"},maximumSelected:function(e){var t="Sie können nur "+e.maximum+" Eintr";return e.maximum===1?t+="ag":t+="äge",t+=" auswählen",t},noResults:function(){return"Keine Übereinstimmungen gefunden"},searching:function(){return"Suche…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/en.js b/public/static/libs/select2/i18n/en.js new file mode 100644 index 0000000..e63b58e --- /dev/null +++ b/public/static/libs/select2/i18n/en.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/en",[],function(){return{errorLoading:function(){return"The results could not be loaded."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Please delete "+t+" character";return t!=1&&(n+="s"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Please enter "+t+" or more characters";return n},loadingMore:function(){return"Loading more results…"},maximumSelected:function(e){var t="You can only select "+e.maximum+" item";return e.maximum!=1&&(t+="s"),t},noResults:function(){return"No results found"},searching:function(){return"Searching…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/es.js b/public/static/libs/select2/i18n/es.js new file mode 100644 index 0000000..c258d68 --- /dev/null +++ b/public/static/libs/select2/i18n/es.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/es",[],function(){return{errorLoading:function(){return"La carga falló"},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Por favor, elimine "+t+" car";return t==1?n+="ácter":n+="acteres",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Por favor, introduzca "+t+" car";return t==1?n+="ácter":n+="acteres",n},loadingMore:function(){return"Cargando más resultados…"},maximumSelected:function(e){var t="Sólo puede seleccionar "+e.maximum+" elemento";return e.maximum!=1&&(t+="s"),t},noResults:function(){return"No se encontraron resultados"},searching:function(){return"Buscando…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/et.js b/public/static/libs/select2/i18n/et.js new file mode 100644 index 0000000..a515ef6 --- /dev/null +++ b/public/static/libs/select2/i18n/et.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/et",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Sisesta "+t+" täht";return t!=1&&(n+="e"),n+=" vähem",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Sisesta "+t+" täht";return t!=1&&(n+="e"),n+=" rohkem",n},loadingMore:function(){return"Laen tulemusi…"},maximumSelected:function(e){var t="Saad vaid "+e.maximum+" tulemus";return e.maximum==1?t+="e":t+="t",t+=" valida",t},noResults:function(){return"Tulemused puuduvad"},searching:function(){return"Otsin…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/eu.js b/public/static/libs/select2/i18n/eu.js new file mode 100644 index 0000000..50f6cad --- /dev/null +++ b/public/static/libs/select2/i18n/eu.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/eu",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Idatzi ";return t==1?n+="karaktere bat":n+=t+" karaktere",n+=" gutxiago",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Idatzi ";return t==1?n+="karaktere bat":n+=t+" karaktere",n+=" gehiago",n},loadingMore:function(){return"Emaitza gehiago kargatzen…"},maximumSelected:function(e){return e.maximum===1?"Elementu bakarra hauta dezakezu":e.maximum+" elementu hauta ditzakezu soilik"},noResults:function(){return"Ez da bat datorrenik aurkitu"},searching:function(){return"Bilatzen…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/fa.js b/public/static/libs/select2/i18n/fa.js new file mode 100644 index 0000000..c23a4a3 --- /dev/null +++ b/public/static/libs/select2/i18n/fa.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/fa",[],function(){return{errorLoading:function(){return"امکان بارگذاری نتایج وجود ندارد."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="لطفاً "+t+" کاراکتر را حذف نمایید";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="لطفاً تعداد "+t+" کاراکتر یا بیشتر وارد نمایید";return n},loadingMore:function(){return"در حال بارگذاری نتایج بیشتر..."},maximumSelected:function(e){var t="شما تنها می‌توانید "+e.maximum+" آیتم را انتخاب نمایید";return t},noResults:function(){return"هیچ نتیجه‌ای یافت نشد"},searching:function(){return"در حال جستجو..."}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/fi.js b/public/static/libs/select2/i18n/fi.js new file mode 100644 index 0000000..733929b --- /dev/null +++ b/public/static/libs/select2/i18n/fi.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/fi",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum;return"Ole hyvä ja anna "+t+" merkkiä vähemmän"},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Ole hyvä ja anna "+t+" merkkiä lisää"},loadingMore:function(){return"Ladataan lisää tuloksia…"},maximumSelected:function(e){return"Voit valita ainoastaan "+e.maximum+" kpl"},noResults:function(){return"Ei tuloksia"},searching:function(){}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/fr.js b/public/static/libs/select2/i18n/fr.js new file mode 100644 index 0000000..0139762 --- /dev/null +++ b/public/static/libs/select2/i18n/fr.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/fr",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Supprimez "+t+" caractère";return t!==1&&(n+="s"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Saisissez "+t+" caractère";return t!==1&&(n+="s"),n},loadingMore:function(){return"Chargement de résultats supplémentaires…"},maximumSelected:function(e){var t="Vous pouvez seulement sélectionner "+e.maximum+" élément";return e.maximum!==1&&(t+="s"),t},noResults:function(){return"Aucun résultat trouvé"},searching:function(){return"Recherche en cours…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/gl.js b/public/static/libs/select2/i18n/gl.js new file mode 100644 index 0000000..7b3c995 --- /dev/null +++ b/public/static/libs/select2/i18n/gl.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/gl",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Elimine ";return t===1?n+="un carácter":n+=t+" caracteres",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Engada ";return t===1?n+="un carácter":n+=t+" caracteres",n},loadingMore:function(){return"Cargando máis resultados…"},maximumSelected:function(e){var t="Só pode ";return e.maximum===1?t+="un elemento":t+=e.maximum+" elementos",t},noResults:function(){return"Non se atoparon resultados"},searching:function(){return"Buscando…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/he.js b/public/static/libs/select2/i18n/he.js new file mode 100644 index 0000000..7756fbf --- /dev/null +++ b/public/static/libs/select2/i18n/he.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/he",[],function(){return{errorLoading:function(){return"שגיאה בטעינת התוצאות"},inputTooLong:function(e){var t=e.input.length-e.maximum,n="נא למחוק ";return t===1?n+="תו אחד":n+=t+" תווים",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="נא להכניס ";return t===1?n+="תו אחד":n+=t+" תווים",n+=" או יותר",n},loadingMore:function(){return"טוען תוצאות נוספות…"},maximumSelected:function(e){var t="באפשרותך לבחור עד ";return e.maximum===1?t+="פריט אחד":t+=e.maximum+" פריטים",t},noResults:function(){return"לא נמצאו תוצאות"},searching:function(){return"מחפש…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/hi.js b/public/static/libs/select2/i18n/hi.js new file mode 100644 index 0000000..0c083fb --- /dev/null +++ b/public/static/libs/select2/i18n/hi.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/hi",[],function(){return{errorLoading:function(){return"परिणामों को लोड नहीं किया जा सका।"},inputTooLong:function(e){var t=e.input.length-e.maximum,n=t+" अक्षर को हटा दें";return t>1&&(n=t+" अक्षरों को हटा दें "),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="कृपया "+t+" या अधिक अक्षर दर्ज करें";return n},loadingMore:function(){return"अधिक परिणाम लोड हो रहे है..."},maximumSelected:function(e){var t="आप केवल "+e.maximum+" आइटम का चयन कर सकते हैं";return t},noResults:function(){return"कोई परिणाम नहीं मिला"},searching:function(){return"खोज रहा है..."}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/hr.js b/public/static/libs/select2/i18n/hr.js new file mode 100644 index 0000000..e140af0 --- /dev/null +++ b/public/static/libs/select2/i18n/hr.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/hr",[],function(){function e(e){var t=" "+e+" znak";return e%10<5&&e%10>0&&(e%100<5||e%100>19)?e%10>1&&(t+="a"):t+="ova",t}return{errorLoading:function(){return"Preuzimanje nije uspjelo."},inputTooLong:function(t){var n=t.input.length-t.maximum;return"Unesite "+e(n)},inputTooShort:function(t){var n=t.minimum-t.input.length;return"Unesite još "+e(n)},loadingMore:function(){return"Učitavanje rezultata…"},maximumSelected:function(e){return"Maksimalan broj odabranih stavki je "+e.maximum},noResults:function(){return"Nema rezultata"},searching:function(){return"Pretraga…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/hu.js b/public/static/libs/select2/i18n/hu.js new file mode 100644 index 0000000..d87620c --- /dev/null +++ b/public/static/libs/select2/i18n/hu.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/hu",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum;return"Túl hosszú. "+t+" karakterrel több, mint kellene."},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Túl rövid. Még "+t+" karakter hiányzik."},loadingMore:function(){return"Töltés…"},maximumSelected:function(e){return"Csak "+e.maximum+" elemet lehet kiválasztani."},noResults:function(){return"Nincs találat."},searching:function(){return"Keresés…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/id.js b/public/static/libs/select2/i18n/id.js new file mode 100644 index 0000000..7a588f1 --- /dev/null +++ b/public/static/libs/select2/i18n/id.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/id",[],function(){return{errorLoading:function(){return"Data tidak boleh diambil."},inputTooLong:function(e){var t=e.input.length-e.maximum;return"Hapuskan "+t+" huruf"},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Masukkan "+t+" huruf lagi"},loadingMore:function(){return"Mengambil data…"},maximumSelected:function(e){return"Anda hanya dapat memilih "+e.maximum+" pilihan"},noResults:function(){return"Tidak ada data yang sesuai"},searching:function(){return"Mencari…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/is.js b/public/static/libs/select2/i18n/is.js new file mode 100644 index 0000000..95aac58 --- /dev/null +++ b/public/static/libs/select2/i18n/is.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/is",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Vinsamlegast styttið texta um "+t+" staf";return t<=1?n:n+"i"},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Vinsamlegast skrifið "+t+" staf";return t>1&&(n+="i"),n+=" í viðbót",n},loadingMore:function(){return"Sæki fleiri niðurstöður…"},maximumSelected:function(e){return"Þú getur aðeins valið "+e.maximum+" atriði"},noResults:function(){return"Ekkert fannst"},searching:function(){return"Leita…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/it.js b/public/static/libs/select2/i18n/it.js new file mode 100644 index 0000000..ac246ac --- /dev/null +++ b/public/static/libs/select2/i18n/it.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/it",[],function(){return{errorLoading:function(){return"I risultati non possono essere caricati."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Per favore cancella "+t+" caratter";return t!==1?n+="i":n+="e",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Per favore inserisci "+t+" o più caratteri";return n},loadingMore:function(){return"Caricando più risultati…"},maximumSelected:function(e){var t="Puoi selezionare solo "+e.maximum+" element";return e.maximum!==1?t+="i":t+="o",t},noResults:function(){return"Nessun risultato trovato"},searching:function(){return"Sto cercando…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/ja.js b/public/static/libs/select2/i18n/ja.js new file mode 100644 index 0000000..0c59dc9 --- /dev/null +++ b/public/static/libs/select2/i18n/ja.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ja",[],function(){return{errorLoading:function(){return"結果が読み込まれませんでした"},inputTooLong:function(e){var t=e.input.length-e.maximum,n=t+" 文字を削除してください";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="少なくとも "+t+" 文字を入力してください";return n},loadingMore:function(){return"読み込み中…"},maximumSelected:function(e){var t=e.maximum+" 件しか選択できません";return t},noResults:function(){return"対象が見つかりません"},searching:function(){return"検索しています…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/ko.js b/public/static/libs/select2/i18n/ko.js new file mode 100644 index 0000000..ea9d0bb --- /dev/null +++ b/public/static/libs/select2/i18n/ko.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ko",[],function(){return{errorLoading:function(){return"결과를 불러올 수 없습니다."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="너무 깁니다. "+t+" 글자 지워주세요.";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="너무 짧습니다. "+t+" 글자 더 입력해주세요.";return n},loadingMore:function(){return"불러오는 중…"},maximumSelected:function(e){var t="최대 "+e.maximum+"개까지만 선택 가능합니다.";return t},noResults:function(){return"결과가 없습니다."},searching:function(){return"검색 중…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/lt.js b/public/static/libs/select2/i18n/lt.js new file mode 100644 index 0000000..6e7f61e --- /dev/null +++ b/public/static/libs/select2/i18n/lt.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/lt",[],function(){function e(e,t,n,r){return e%100>9&&e%100<21||e%10===0?e%10>1?n:r:t}return{inputTooLong:function(t){var n=t.input.length-t.maximum,r="Pašalinkite "+n+" simbol";return r+=e(n,"ių","ius","į"),r},inputTooShort:function(t){var n=t.minimum-t.input.length,r="Įrašykite dar "+n+" simbol";return r+=e(n,"ių","ius","į"),r},loadingMore:function(){return"Kraunama daugiau rezultatų…"},maximumSelected:function(t){var n="Jūs galite pasirinkti tik "+t.maximum+" element";return n+=e(t.maximum,"ų","us","ą"),n},noResults:function(){return"Atitikmenų nerasta"},searching:function(){return"Ieškoma…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/lv.js b/public/static/libs/select2/i18n/lv.js new file mode 100644 index 0000000..5fddddf --- /dev/null +++ b/public/static/libs/select2/i18n/lv.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/lv",[],function(){function e(e,t,n,r){return e===11?t:e%10===1?n:r}return{inputTooLong:function(t){var n=t.input.length-t.maximum,r="Lūdzu ievadiet par "+n;return r+=" simbol"+e(n,"iem","u","iem"),r+" mazāk"},inputTooShort:function(t){var n=t.minimum-t.input.length,r="Lūdzu ievadiet vēl "+n;return r+=" simbol"+e(n,"us","u","us"),r},loadingMore:function(){return"Datu ielāde…"},maximumSelected:function(t){var n="Jūs varat izvēlēties ne vairāk kā "+t.maximum;return n+=" element"+e(t.maximum,"us","u","us"),n},noResults:function(){return"Sakritību nav"},searching:function(){return"Meklēšana…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/mk.js b/public/static/libs/select2/i18n/mk.js new file mode 100644 index 0000000..aa9660a --- /dev/null +++ b/public/static/libs/select2/i18n/mk.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/mk",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Ве молиме внесете "+e.maximum+" помалку карактер";return e.maximum!==1&&(n+="и"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Ве молиме внесете уште "+e.maximum+" карактер";return e.maximum!==1&&(n+="и"),n},loadingMore:function(){return"Вчитување резултати…"},maximumSelected:function(e){var t="Можете да изберете само "+e.maximum+" ставк";return e.maximum===1?t+="а":t+="и",t},noResults:function(){return"Нема пронајдено совпаѓања"},searching:function(){return"Пребарување…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/ms.js b/public/static/libs/select2/i18n/ms.js new file mode 100644 index 0000000..85b3d13 --- /dev/null +++ b/public/static/libs/select2/i18n/ms.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ms",[],function(){return{errorLoading:function(){return"Keputusan tidak berjaya dimuatkan."},inputTooLong:function(e){var t=e.input.length-e.maximum;return"Sila hapuskan "+t+" aksara"},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Sila masukkan "+t+" atau lebih aksara"},loadingMore:function(){return"Sedang memuatkan keputusan…"},maximumSelected:function(e){return"Anda hanya boleh memilih "+e.maximum+" pilihan"},noResults:function(){return"Tiada padanan yang ditemui"},searching:function(){return"Mencari…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/nb.js b/public/static/libs/select2/i18n/nb.js new file mode 100644 index 0000000..7bf6198 --- /dev/null +++ b/public/static/libs/select2/i18n/nb.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/nb",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum;return"Vennligst fjern "+t+" tegn"},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Vennligst skriv inn ";return t>1?n+=" flere tegn":n+=" tegn til",n},loadingMore:function(){return"Laster flere resultater…"},maximumSelected:function(e){return"Du kan velge maks "+e.maximum+" elementer"},noResults:function(){return"Ingen treff"},searching:function(){return"Søker…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/nl.js b/public/static/libs/select2/i18n/nl.js new file mode 100644 index 0000000..39f4900 --- /dev/null +++ b/public/static/libs/select2/i18n/nl.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/nl",[],function(){return{errorLoading:function(){return"De resultaten konden niet worden geladen."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Gelieve "+t+" karakters te verwijderen";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Gelieve "+t+" of meer karakters in te voeren";return n},loadingMore:function(){return"Meer resultaten laden…"},maximumSelected:function(e){var t=e.maximum==1?"kan":"kunnen",n="Er "+t+" maar "+e.maximum+" item";return e.maximum!=1&&(n+="s"),n+=" worden geselecteerd",n},noResults:function(){return"Geen resultaten gevonden…"},searching:function(){return"Zoeken…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/pl.js b/public/static/libs/select2/i18n/pl.js new file mode 100644 index 0000000..9e6c33f --- /dev/null +++ b/public/static/libs/select2/i18n/pl.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/pl",[],function(){var e=["znak","znaki","znaków"],t=["element","elementy","elementów"],n=function(t,n){if(t===1)return n[0];if(t>1&&t<=4)return n[1];if(t>=5)return n[2]};return{errorLoading:function(){return"Nie można załadować wyników."},inputTooLong:function(t){var r=t.input.length-t.maximum;return"Usuń "+r+" "+n(r,e)},inputTooShort:function(t){var r=t.minimum-t.input.length;return"Podaj przynajmniej "+r+" "+n(r,e)},loadingMore:function(){return"Trwa ładowanie…"},maximumSelected:function(e){return"Możesz zaznaczyć tylko "+e.maximum+" "+n(e.maximum,t)},noResults:function(){return"Brak wyników"},searching:function(){return"Trwa wyszukiwanie…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/pt-BR.js b/public/static/libs/select2/i18n/pt-BR.js new file mode 100644 index 0000000..3ea712c --- /dev/null +++ b/public/static/libs/select2/i18n/pt-BR.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/pt-BR",[],function(){return{errorLoading:function(){return"Os resultados não puderam ser carregados."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Apague "+t+" caracter";return t!=1&&(n+="es"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Digite "+t+" ou mais caracteres";return n},loadingMore:function(){return"Carregando mais resultados…"},maximumSelected:function(e){var t="Você só pode selecionar "+e.maximum+" ite";return e.maximum==1?t+="m":t+="ns",t},noResults:function(){return"Nenhum resultado encontrado"},searching:function(){return"Buscando…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/pt.js b/public/static/libs/select2/i18n/pt.js new file mode 100644 index 0000000..a96299c --- /dev/null +++ b/public/static/libs/select2/i18n/pt.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/pt",[],function(){return{errorLoading:function(){return"Os resultados não puderam ser carregados."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Por favor apague "+t+" ";return n+=t!=1?"caracteres":"carácter",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Introduza "+t+" ou mais caracteres";return n},loadingMore:function(){return"A carregar mais resultados…"},maximumSelected:function(e){var t="Apenas pode seleccionar "+e.maximum+" ";return t+=e.maximum!=1?"itens":"item",t},noResults:function(){return"Sem resultados"},searching:function(){return"A procurar…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/ro.js b/public/static/libs/select2/i18n/ro.js new file mode 100644 index 0000000..6107d76 --- /dev/null +++ b/public/static/libs/select2/i18n/ro.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ro",[],function(){return{errorLoading:function(){return"Rezultatele nu au putut fi incărcate."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Vă rugăm să ștergeți"+t+" caracter";return t!==1&&(n+="e"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Vă rugăm să introduceți "+t+"sau mai multe caractere";return n},loadingMore:function(){return"Se încarcă mai multe rezultate…"},maximumSelected:function(e){var t="Aveți voie să selectați cel mult "+e.maximum;return t+=" element",e.maximum!==1&&(t+="e"),t},noResults:function(){return"Nu au fost găsite rezultate"},searching:function(){return"Căutare…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/ru.js b/public/static/libs/select2/i18n/ru.js new file mode 100644 index 0000000..e997c8b --- /dev/null +++ b/public/static/libs/select2/i18n/ru.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ru",[],function(){function e(e,t,n,r){return e%10<5&&e%10>0&&e%100<5||e%100>20?e%10>1?n:t:r}return{errorLoading:function(){return"Невозможно загрузить результаты"},inputTooLong:function(t){var n=t.input.length-t.maximum,r="Пожалуйста, введите на "+n+" символ";return r+=e(n,"","a","ов"),r+=" меньше",r},inputTooShort:function(t){var n=t.minimum-t.input.length,r="Пожалуйста, введите еще хотя бы "+n+" символ";return r+=e(n,"","a","ов"),r},loadingMore:function(){return"Загрузка данных…"},maximumSelected:function(t){var n="Вы можете выбрать не более "+t.maximum+" элемент";return n+=e(t.maximum,"","a","ов"),n},noResults:function(){return"Совпадений не найдено"},searching:function(){return"Поиск…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/sk.js b/public/static/libs/select2/i18n/sk.js new file mode 100644 index 0000000..6a07f10 --- /dev/null +++ b/public/static/libs/select2/i18n/sk.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/sk",[],function(){var e={2:function(e){return e?"dva":"dve"},3:function(){return"tri"},4:function(){return"štyri"}};return{inputTooLong:function(t){var n=t.input.length-t.maximum;return n==1?"Prosím, zadajte o jeden znak menej":n>=2&&n<=4?"Prosím, zadajte o "+e[n](!0)+" znaky menej":"Prosím, zadajte o "+n+" znakov menej"},inputTooShort:function(t){var n=t.minimum-t.input.length;return n==1?"Prosím, zadajte ešte jeden znak":n<=4?"Prosím, zadajte ešte ďalšie "+e[n](!0)+" znaky":"Prosím, zadajte ešte ďalších "+n+" znakov"},loadingMore:function(){return"Loading more results…"},maximumSelected:function(t){return t.maximum==1?"Môžete zvoliť len jednu položku":t.maximum>=2&&t.maximum<=4?"Môžete zvoliť najviac "+e[t.maximum](!1)+" položky":"Môžete zvoliť najviac "+t.maximum+" položiek"},noResults:function(){return"Nenašli sa žiadne položky"},searching:function(){return"Vyhľadávanie…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/sr-Cyrl.js b/public/static/libs/select2/i18n/sr-Cyrl.js new file mode 100644 index 0000000..345cc65 --- /dev/null +++ b/public/static/libs/select2/i18n/sr-Cyrl.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/sr-Cyrl",[],function(){function e(e,t,n,r){return e%10==1&&e%100!=11?t:e%10>=2&&e%10<=4&&(e%100<12||e%100>14)?n:r}return{errorLoading:function(){return"Преузимање није успело."},inputTooLong:function(t){var n=t.input.length-t.maximum,r="Обришите "+n+" симбол";return r+=e(n,"","а","а"),r},inputTooShort:function(t){var n=t.minimum-t.input.length,r="Укуцајте бар још "+n+" симбол";return r+=e(n,"","а","а"),r},loadingMore:function(){return"Преузимање још резултата…"},maximumSelected:function(t){var n="Можете изабрати само "+t.maximum+" ставк";return n+=e(t.maximum,"у","е","и"),n},noResults:function(){return"Ништа није пронађено"},searching:function(){return"Претрага…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/sr.js b/public/static/libs/select2/i18n/sr.js new file mode 100644 index 0000000..21ca4e0 --- /dev/null +++ b/public/static/libs/select2/i18n/sr.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/sr",[],function(){function e(e,t,n,r){return e%10==1&&e%100!=11?t:e%10>=2&&e%10<=4&&(e%100<12||e%100>14)?n:r}return{errorLoading:function(){return"Preuzimanje nije uspelo."},inputTooLong:function(t){var n=t.input.length-t.maximum,r="Obrišite "+n+" simbol";return r+=e(n,"","a","a"),r},inputTooShort:function(t){var n=t.minimum-t.input.length,r="Ukucajte bar još "+n+" simbol";return r+=e(n,"","a","a"),r},loadingMore:function(){return"Preuzimanje još rezultata…"},maximumSelected:function(t){var n="Možete izabrati samo "+t.maximum+" stavk";return n+=e(t.maximum,"u","e","i"),n},noResults:function(){return"Ništa nije pronađeno"},searching:function(){return"Pretraga…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/sv.js b/public/static/libs/select2/i18n/sv.js new file mode 100644 index 0000000..3621170 --- /dev/null +++ b/public/static/libs/select2/i18n/sv.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/sv",[],function(){return{errorLoading:function(){return"Resultat kunde inte laddas."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Vänligen sudda ut "+t+" tecken";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Vänligen skriv in "+t+" eller fler tecken";return n},loadingMore:function(){return"Laddar fler resultat…"},maximumSelected:function(e){var t="Du kan max välja "+e.maximum+" element";return t},noResults:function(){return"Inga träffar"},searching:function(){return"Söker…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/th.js b/public/static/libs/select2/i18n/th.js new file mode 100644 index 0000000..6c46b35 --- /dev/null +++ b/public/static/libs/select2/i18n/th.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/th",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="โปรดลบออก "+t+" ตัวอักษร";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="โปรดพิมพ์เพิ่มอีก "+t+" ตัวอักษร";return n},loadingMore:function(){return"กำลังค้นข้อมูลเพิ่ม…"},maximumSelected:function(e){var t="คุณสามารถเลือกได้ไม่เกิน "+e.maximum+" รายการ";return t},noResults:function(){return"ไม่พบข้อมูล"},searching:function(){return"กำลังค้นข้อมูล…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/tr.js b/public/static/libs/select2/i18n/tr.js new file mode 100644 index 0000000..5952627 --- /dev/null +++ b/public/static/libs/select2/i18n/tr.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/tr",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n=t+" karakter daha girmelisiniz";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="En az "+t+" karakter daha girmelisiniz";return n},loadingMore:function(){return"Daha fazla…"},maximumSelected:function(e){var t="Sadece "+e.maximum+" seçim yapabilirsiniz";return t},noResults:function(){return"Sonuç bulunamadı"},searching:function(){return"Aranıyor…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/uk.js b/public/static/libs/select2/i18n/uk.js new file mode 100644 index 0000000..f263bc8 --- /dev/null +++ b/public/static/libs/select2/i18n/uk.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/uk",[],function(){function e(e,t,n,r){return e%100>10&&e%100<15?r:e%10===1?t:e%10>1&&e%10<5?n:r}return{errorLoading:function(){return"Неможливо завантажити результати"},inputTooLong:function(t){var n=t.input.length-t.maximum;return"Будь ласка, видаліть "+n+" "+e(t.maximum,"літеру","літери","літер")},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Будь ласка, введіть "+t+" або більше літер"},loadingMore:function(){return"Завантаження інших результатів…"},maximumSelected:function(t){return"Ви можете вибрати лише "+t.maximum+" "+e(t.maximum,"пункт","пункти","пунктів")},noResults:function(){return"Нічого не знайдено"},searching:function(){return"Пошук…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/vi.js b/public/static/libs/select2/i18n/vi.js new file mode 100644 index 0000000..0112c4a --- /dev/null +++ b/public/static/libs/select2/i18n/vi.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/vi",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Vui lòng nhập ít hơn "+t+" ký tự";return t!=1&&(n+="s"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Vui lòng nhập nhiều hơn "+t+' ký tự"';return n},loadingMore:function(){return"Đang lấy thêm kết quả…"},maximumSelected:function(e){var t="Chỉ có thể chọn được "+e.maximum+" lựa chọn";return t},noResults:function(){return"Không tìm thấy kết quả"},searching:function(){return"Đang tìm…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/zh-CN.js b/public/static/libs/select2/i18n/zh-CN.js new file mode 100644 index 0000000..f4a41fd --- /dev/null +++ b/public/static/libs/select2/i18n/zh-CN.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/zh-CN",[],function(){return{errorLoading:function(){return"无法载入结果。"},inputTooLong:function(e){var t=e.input.length-e.maximum,n="请删除"+t+"个字符";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="请再输入至少"+t+"个字符";return n},loadingMore:function(){return"载入更多结果…"},maximumSelected:function(e){var t="最多只能选择"+e.maximum+"个项目";return t},noResults:function(){return"未找到结果"},searching:function(){return"搜索中…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/zh-TW.js b/public/static/libs/select2/i18n/zh-TW.js new file mode 100644 index 0000000..6a95ae6 --- /dev/null +++ b/public/static/libs/select2/i18n/zh-TW.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/zh-TW",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="請刪掉"+t+"個字元";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="請再輸入"+t+"個字元";return n},loadingMore:function(){return"載入中…"},maximumSelected:function(e){var t="你只能選擇最多"+e.maximum+"項";return t},noResults:function(){return"沒有找到相符的項目"},searching:function(){return"搜尋中…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/select2-bootstrap.css b/public/static/libs/select2/select2-bootstrap.css new file mode 100644 index 0000000..b8877c7 --- /dev/null +++ b/public/static/libs/select2/select2-bootstrap.css @@ -0,0 +1,495 @@ +/*! Select2 Bootstrap 3 CSS v1.4.6 | MIT License | github.com/t0m/select2-bootstrap-css */ +/** + * Reset Bootstrap 3 .form-control styles which - if applied to the + * original element Select2 is replacing not be properly being hidden + * when used in a "Bootstrap Input Group with Addon". + **/ +.select2-offscreen, +.select2-offscreen:focus { + width: 1px !important; + height: 1px !important; + position: absolute !important; +} \ No newline at end of file diff --git a/public/static/libs/select2/select2-bootstrap.min.css b/public/static/libs/select2/select2-bootstrap.min.css new file mode 100644 index 0000000..67e0f00 --- /dev/null +++ b/public/static/libs/select2/select2-bootstrap.min.css @@ -0,0 +1 @@ +/*! Select2 Bootstrap 3 CSS v1.4.6 | MIT License | github.com/t0m/select2-bootstrap-css */.select2-container.form-control{background:0 0;box-shadow:none;border:none;display:block;margin:0;padding:0}.select2-container .select2-choice,.select2-container .select2-choices,.select2-container .select2-choices .select2-search-field input,.select2-search input{border-color:#ccc;border-radius:4px;color:#555;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.select2-container .select2-choice,.select2-container .select2-choices,.select2-container .select2-choices .select2-search-field input{background:#fff;padding:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.select2-search input{background-color:#fff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.select2-container .select2-choices .select2-search-field input{-webkit-box-shadow:none;box-shadow:none}.select2-container .select2-choice{height:34px;line-height:1.42857}.select2-container.select2-container-multi.form-control{height:auto}.input-group-sm .select2-container .select2-choice,.select2-container.input-sm .select2-choice{height:30px;line-height:1.5;border-radius:3px}.input-group-lg .select2-container .select2-choice,.select2-container.input-lg .select2-choice{height:46px;line-height:1.33333;border-radius:6px}.select2-container-multi .select2-choices .select2-search-field input{height:32px;margin:0}.input-group-sm .select2-container-multi .select2-choices .select2-search-field input,.select2-container-multi.input-sm .select2-choices .select2-search-field input{height:28px}.input-group-lg .select2-container-multi .select2-choices .select2-search-field input,.select2-container-multi.input-lg .select2-choices .select2-search-field input{height:44px}.select2-choice>span:first-child,.select2-chosen,.select2-container .select2-choices .select2-search-field input{padding:6px 12px}.input-group-sm .select2-choice>span:first-child,.input-group-sm .select2-choices .select2-search-field input,.input-group-sm .select2-chosen,.input-sm .select2-choice>span:first-child,.input-sm .select2-choices .select2-search-field input,.input-sm .select2-chosen{padding:5px 10px}.input-group-lg .select2-choice>span:first-child,.input-group-lg .select2-choices .select2-search-field input,.input-group-lg .select2-chosen,.input-lg .select2-choice>span:first-child,.input-lg .select2-choices .select2-search-field input,.input-lg .select2-chosen{padding:10px 16px}.select2-container-multi .select2-choices .select2-search-choice{margin-top:5px;margin-bottom:3px}.input-group-sm .select2-container-multi .select2-choices .select2-search-choice,.select2-container-multi.input-sm .select2-choices .select2-search-choice{margin-top:3px;margin-bottom:2px}.input-group-lg .select2-container-multi .select2-choices .select2-search-choice,.select2-container-multi.input-lg .select2-choices .select2-search-choice{line-height:24px}.select2-container .select2-choice .select2-arrow,.select2-container .select2-choice div{border-left:none;background:0 0;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.select2-dropdown-open .select2-choice .select2-arrow,.select2-dropdown-open .select2-choice div{border-left-color:transparent;background:0 0;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.select2-container .select2-choice .select2-arrow b,.select2-container .select2-choice div b{background-position:0 3px}.select2-dropdown-open .select2-choice .select2-arrow b,.select2-dropdown-open .select2-choice div b{background-position:-18px 3px}.input-group-sm .select2-container .select2-choice .select2-arrow b,.input-group-sm .select2-container .select2-choice div b,.select2-container.input-sm .select2-choice .select2-arrow b,.select2-container.input-sm .select2-choice div b{background-position:0 1px}.input-group-sm .select2-dropdown-open .select2-choice .select2-arrow b,.input-group-sm .select2-dropdown-open .select2-choice div b,.select2-dropdown-open.input-sm .select2-choice .select2-arrow b,.select2-dropdown-open.input-sm .select2-choice div b{background-position:-18px 1px}.input-group-lg .select2-container .select2-choice .select2-arrow b,.input-group-lg .select2-container .select2-choice div b,.select2-container.input-lg .select2-choice .select2-arrow b,.select2-container.input-lg .select2-choice div b{background-position:0 9px}.input-group-lg .select2-dropdown-open .select2-choice .select2-arrow b,.input-group-lg .select2-dropdown-open .select2-choice div b,.select2-dropdown-open.input-lg .select2-choice .select2-arrow b,.select2-dropdown-open.input-lg .select2-choice div b{background-position:-18px 9px}.has-warning .select2-choice,.has-warning .select2-choices{border-color:#8a6d3b}.has-warning .select2-container-active .select2-choice,.has-warning .select2-container-multi.select2-container-active .select2-choices{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning.select2-drop-active{border-color:#66512c}.has-warning.select2-drop-active.select2-drop.select2-drop-above{border-top-color:#66512c}.has-error .select2-choice,.has-error .select2-choices{border-color:#a94442}.has-error .select2-container-active .select2-choice,.has-error .select2-container-multi.select2-container-active .select2-choices{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error.select2-drop-active{border-color:#843534}.has-error.select2-drop-active.select2-drop.select2-drop-above{border-top-color:#843534}.has-success .select2-choice,.has-success .select2-choices{border-color:#3c763d}.has-success .select2-container-active .select2-choice,.has-success .select2-container-multi.select2-container-active .select2-choices{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success.select2-drop-active{border-color:#2b542c}.has-success.select2-drop-active.select2-drop.select2-drop-above{border-top-color:#2b542c}.select2-container-active .select2-choice,.select2-container-multi.select2-container-active .select2-choices{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.select2-drop-active{border-color:#66afe9}.select2-drop-auto-width,.select2-drop.select2-drop-above.select2-drop-active{border-top-color:#66afe9}.input-group.select2-bootstrap-prepend [class^=select2-choice]{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.input-group.select2-bootstrap-append [class^=select2-choice]{border-bottom-right-radius:0!important;border-top-right-radius:0!important}.select2-dropdown-open [class^=select2-choice]{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.select2-dropdown-open.select2-drop-above [class^=select2-choice]{background:#fff;filter:none;border-radius:0 0 4px 4px!important}.input-group.select2-bootstrap-prepend .select2-dropdown-open.select2-drop-above [class^=select2-choice]{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.input-group.select2-bootstrap-append .select2-dropdown-open.select2-drop-above [class^=select2-choice]{border-bottom-right-radius:0!important;border-top-right-radius:0!important}.input-group.input-group-sm.select2-bootstrap-prepend .select2-dropdown-open.select2-drop-above [class^=select2-choice]{border-bottom-right-radius:3px!important}.input-group.input-group-lg.select2-bootstrap-prepend .select2-dropdown-open.select2-drop-above [class^=select2-choice]{border-bottom-right-radius:6px!important}.input-group.input-group-sm.select2-bootstrap-append .select2-dropdown-open.select2-drop-above [class^=select2-choice]{border-bottom-left-radius:3px!important}.input-group.input-group-lg.select2-bootstrap-append .select2-dropdown-open.select2-drop-above [class^=select2-choice]{border-bottom-left-radius:6px!important}.select2-results .select2-highlighted{color:#fff;background-color:#337ab7}.select2-bootstrap-append .input-group-btn,.select2-bootstrap-append .input-group-btn .btn,.select2-bootstrap-append .select2-container-multiple,.select2-bootstrap-prepend .input-group-btn,.select2-bootstrap-prepend .input-group-btn .btn,.select2-bootstrap-prepend .select2-container-multiple{vertical-align:top}.select2-container-multi .select2-choices .select2-search-choice{color:#555;background:#fff;border-color:#ccc;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:none;box-shadow:none}.select2-container-multi .select2-choices .select2-search-choice-focus{background:#ebebeb;border-color:#adadad;color:#333;-webkit-box-shadow:none;box-shadow:none}.select2-search-choice-close{margin-top:-7px;top:50%}.select2-container .select2-choice abbr{top:50%}.select2-results .select2-no-results,.select2-results .select2-searching,.select2-results .select2-selection-limit{background-color:#fcf8e3;color:#8a6d3b}.select2-container.select2-container-disabled .select2-choice,.select2-container.select2-container-disabled .select2-choices{cursor:not-allowed;background-color:#eee;border-color:#ccc}.select2-container.select2-container-disabled .select2-choice .select2-arrow,.select2-container.select2-container-disabled .select2-choice div,.select2-container.select2-container-disabled .select2-choices .select2-arrow,.select2-container.select2-container-disabled .select2-choices div{background-color:transparent;border-left:1px solid transparent}.select2-container-multi .select2-choices .select2-search-field input.select2-active,.select2-more-results.select2-active,.select2-search input.select2-active{background-position:right 4px center}.select2-offscreen,.select2-offscreen:focus{width:1px!important;height:1px!important;position:absolute!important} \ No newline at end of file diff --git a/public/static/libs/select2/select2.css b/public/static/libs/select2/select2.css new file mode 100644 index 0000000..5806cf9 --- /dev/null +++ b/public/static/libs/select2/select2.css @@ -0,0 +1,484 @@ +.select2-container { + box-sizing: border-box; + display: inline-block; + margin: 0; + position: relative; + vertical-align: middle; } + .select2-container .select2-selection--single { + box-sizing: border-box; + cursor: pointer; + display: block; + height: 28px; + user-select: none; + -webkit-user-select: none; } + .select2-container .select2-selection--single .select2-selection__rendered { + display: block; + padding-left: 8px; + padding-right: 20px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } + .select2-container .select2-selection--single .select2-selection__clear { + position: relative; } + .select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered { + padding-right: 8px; + padding-left: 20px; } + .select2-container .select2-selection--multiple { + box-sizing: border-box; + cursor: pointer; + display: block; + min-height: 32px; + user-select: none; + -webkit-user-select: none; } + .select2-container .select2-selection--multiple .select2-selection__rendered { + display: inline-block; + overflow: hidden; + padding-left: 8px; + text-overflow: ellipsis; + white-space: nowrap; } + .select2-container .select2-search--inline { + float: left; } + .select2-container .select2-search--inline .select2-search__field { + box-sizing: border-box; + border: none; + font-size: 100%; + margin-top: 5px; + padding: 0; } + .select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button { + -webkit-appearance: none; } + +.select2-dropdown { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + box-sizing: border-box; + display: block; + position: absolute; + left: -100000px; + width: 100%; + z-index: 1051; } + +.select2-results { + display: block; } + +.select2-results__options { + list-style: none; + margin: 0; + padding: 0; } + +.select2-results__option { + padding: 6px; + user-select: none; + -webkit-user-select: none; } + .select2-results__option[aria-selected] { + cursor: pointer; } + +.select2-container--open .select2-dropdown { + left: 0; } + +.select2-container--open .select2-dropdown--above { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--open .select2-dropdown--below { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-search--dropdown { + display: block; + padding: 4px; } + .select2-search--dropdown .select2-search__field { + padding: 4px; + width: 100%; + box-sizing: border-box; } + .select2-search--dropdown .select2-search__field::-webkit-search-cancel-button { + -webkit-appearance: none; } + .select2-search--dropdown.select2-search--hide { + display: none; } + +.select2-close-mask { + border: 0; + margin: 0; + padding: 0; + display: block; + position: fixed; + left: 0; + top: 0; + min-height: 100%; + min-width: 100%; + height: auto; + width: auto; + opacity: 0; + z-index: 99; + background-color: #fff; + filter: alpha(opacity=0); } + +.select2-hidden-accessible { + border: 0 !important; + clip: rect(0 0 0 0) !important; + height: 1px !important; + margin: -1px !important; + overflow: hidden !important; + padding: 0 !important; + position: absolute !important; + width: 1px !important; } + +.select2-container--default .select2-selection--single { + background-color: #fff; + border: 1px solid #aaa; + border-radius: 4px; } + .select2-container--default .select2-selection--single .select2-selection__rendered { + color: #444; + line-height: 28px; } + .select2-container--default .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; } + .select2-container--default .select2-selection--single .select2-selection__placeholder { + color: #999; } + .select2-container--default .select2-selection--single .select2-selection__arrow { + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; } + .select2-container--default .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; } + +.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; } + +.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow { + left: 1px; + right: auto; } + +.select2-container--default.select2-container--disabled .select2-selection--single { + background-color: #eee; + cursor: default; } + .select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear { + display: none; } + +.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; } + +.select2-container--default .select2-selection--multiple { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + cursor: text; } + .select2-container--default .select2-selection--multiple .select2-selection__rendered { + box-sizing: border-box; + list-style: none; + margin: 0; + padding: 0 5px; + width: 100%; } + .select2-container--default .select2-selection--multiple .select2-selection__rendered li { + list-style: none; } + .select2-container--default .select2-selection--multiple .select2-selection__placeholder { + color: #999; + margin-top: 5px; + float: left; } + .select2-container--default .select2-selection--multiple .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + margin-top: 5px; + margin-right: 10px; } + .select2-container--default .select2-selection--multiple .select2-selection__choice { + background-color: #e4e4e4; + border: 1px solid #aaa; + border-radius: 4px; + cursor: default; + float: left; + margin-right: 5px; + margin-top: 5px; + padding: 0 5px; } + .select2-container--default .select2-selection--multiple .select2-selection__choice__remove { + color: #999; + cursor: pointer; + display: inline-block; + font-weight: bold; + margin-right: 2px; } + .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { + color: #333; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline { + float: right; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + margin-left: 5px; + margin-right: auto; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + margin-left: 2px; + margin-right: auto; } + +.select2-container--default.select2-container--focus .select2-selection--multiple { + border: solid black 1px; + outline: 0; } + +.select2-container--default.select2-container--disabled .select2-selection--multiple { + background-color: #eee; + cursor: default; } + +.select2-container--default.select2-container--disabled .select2-selection__choice__remove { + display: none; } + +.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple { + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--default .select2-search--dropdown .select2-search__field { + border: 1px solid #aaa; } + +.select2-container--default .select2-search--inline .select2-search__field { + background: transparent; + border: none; + outline: 0; + box-shadow: none; + -webkit-appearance: textfield; } + +.select2-container--default .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; } + +.select2-container--default .select2-results__option[role=group] { + padding: 0; } + +.select2-container--default .select2-results__option[aria-disabled=true] { + color: #999; } + +.select2-container--default .select2-results__option[aria-selected=true] { + background-color: #ddd; } + +.select2-container--default .select2-results__option .select2-results__option { + padding-left: 1em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__group { + padding-left: 0; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option { + margin-left: -1em; + padding-left: 2em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -2em; + padding-left: 3em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -3em; + padding-left: 4em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -4em; + padding-left: 5em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -5em; + padding-left: 6em; } + +.select2-container--default .select2-results__option--highlighted[aria-selected] { + background-color: #5897fb; + color: white; } + +.select2-container--default .select2-results__group { + cursor: default; + display: block; + padding: 6px; } + +.select2-container--classic .select2-selection--single { + background-color: #f7f7f7; + border: 1px solid #aaa; + border-radius: 4px; + outline: 0; + background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%); + background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%); + background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); } + .select2-container--classic .select2-selection--single:focus { + border: 1px solid #5897fb; } + .select2-container--classic .select2-selection--single .select2-selection__rendered { + color: #444; + line-height: 28px; } + .select2-container--classic .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + margin-right: 10px; } + .select2-container--classic .select2-selection--single .select2-selection__placeholder { + color: #999; } + .select2-container--classic .select2-selection--single .select2-selection__arrow { + background-color: #ddd; + border: none; + border-left: 1px solid #aaa; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; + background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%); + background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%); + background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); } + .select2-container--classic .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; } + +.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; } + +.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow { + border: none; + border-right: 1px solid #aaa; + border-radius: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + left: 1px; + right: auto; } + +.select2-container--classic.select2-container--open .select2-selection--single { + border: 1px solid #5897fb; } + .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow { + background: transparent; + border: none; } + .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; } + +.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; + background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%); + background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%); + background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); } + +.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%); + background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%); + background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); } + +.select2-container--classic .select2-selection--multiple { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + cursor: text; + outline: 0; } + .select2-container--classic .select2-selection--multiple:focus { + border: 1px solid #5897fb; } + .select2-container--classic .select2-selection--multiple .select2-selection__rendered { + list-style: none; + margin: 0; + padding: 0 5px; } + .select2-container--classic .select2-selection--multiple .select2-selection__clear { + display: none; } + .select2-container--classic .select2-selection--multiple .select2-selection__choice { + background-color: #e4e4e4; + border: 1px solid #aaa; + border-radius: 4px; + cursor: default; + float: left; + margin-right: 5px; + margin-top: 5px; + padding: 0 5px; } + .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove { + color: #888; + cursor: pointer; + display: inline-block; + font-weight: bold; + margin-right: 2px; } + .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover { + color: #555; } + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + float: right; } + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + margin-left: 5px; + margin-right: auto; } + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + margin-left: 2px; + margin-right: auto; } + +.select2-container--classic.select2-container--open .select2-selection--multiple { + border: 1px solid #5897fb; } + +.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--classic .select2-search--dropdown .select2-search__field { + border: 1px solid #aaa; + outline: 0; } + +.select2-container--classic .select2-search--inline .select2-search__field { + outline: 0; + box-shadow: none; } + +.select2-container--classic .select2-dropdown { + background-color: white; + border: 1px solid transparent; } + +.select2-container--classic .select2-dropdown--above { + border-bottom: none; } + +.select2-container--classic .select2-dropdown--below { + border-top: none; } + +.select2-container--classic .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; } + +.select2-container--classic .select2-results__option[role=group] { + padding: 0; } + +.select2-container--classic .select2-results__option[aria-disabled=true] { + color: grey; } + +.select2-container--classic .select2-results__option--highlighted[aria-selected] { + background-color: #3875d7; + color: white; } + +.select2-container--classic .select2-results__group { + cursor: default; + display: block; + padding: 6px; } + +.select2-container--classic.select2-container--open .select2-dropdown { + border-color: #5897fb; } diff --git a/public/static/libs/select2/select2.full.js b/public/static/libs/select2/select2.full.js new file mode 100644 index 0000000..1974c01 --- /dev/null +++ b/public/static/libs/select2/select2.full.js @@ -0,0 +1,6436 @@ +/*! + * Select2 4.0.3 + * https://select2.github.io + * + * Released under the MIT license + * https://github.com/select2/select2/blob/master/LICENSE.md + */ +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS + factory(require('jquery')); + } else { + // Browser globals + factory(jQuery); + } +}(function (jQuery) { + // This is needed so we can catch the AMD loader configuration and use it + // The inner file should be wrapped (by `banner.start.js`) in a function that + // returns the AMD loader references. + var S2 = +(function () { + // Restore the Select2 AMD loader so it can be used + // Needed mostly in the language files, where the loader is not inserted + if (jQuery && jQuery.fn && jQuery.fn.select2 && jQuery.fn.select2.amd) { + var S2 = jQuery.fn.select2.amd; + } +var S2;(function () { if (!S2 || !S2.requirejs) { +if (!S2) { S2 = {}; } else { require = S2; } +/** + * @license almond 0.3.1 Copyright (c) 2011-2014, The Dojo Foundation All Rights Reserved. + * Available via the MIT or new BSD license. + * see: http://github.com/jrburke/almond for details + */ +//Going sloppy to avoid 'use strict' string cost, but strict practices should +//be followed. +/*jslint sloppy: true */ +/*global setTimeout: false */ + +var requirejs, require, define; +(function (undef) { + var main, req, makeMap, handlers, + defined = {}, + waiting = {}, + config = {}, + defining = {}, + hasOwn = Object.prototype.hasOwnProperty, + aps = [].slice, + jsSuffixRegExp = /\.js$/; + + function hasProp(obj, prop) { + return hasOwn.call(obj, prop); + } + + /** + * Given a relative module name, like ./something, normalize it to + * a real name that can be mapped to a path. + * @param {String} name the relative name + * @param {String} baseName a real name that the name arg is relative + * to. + * @returns {String} normalized name + */ + function normalize(name, baseName) { + var nameParts, nameSegment, mapValue, foundMap, lastIndex, + foundI, foundStarMap, starI, i, j, part, + baseParts = baseName && baseName.split("/"), + map = config.map, + starMap = (map && map['*']) || {}; + + //Adjust any relative paths. + if (name && name.charAt(0) === ".") { + //If have a base name, try to normalize against it, + //otherwise, assume it is a top-level require that will + //be relative to baseUrl in the end. + if (baseName) { + name = name.split('/'); + lastIndex = name.length - 1; + + // Node .js allowance: + if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) { + name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, ''); + } + + //Lop off the last part of baseParts, so that . matches the + //"directory" and not name of the baseName's module. For instance, + //baseName of "one/two/three", maps to "one/two/three.js", but we + //want the directory, "one/two" for this normalization. + name = baseParts.slice(0, baseParts.length - 1).concat(name); + + //start trimDots + for (i = 0; i < name.length; i += 1) { + part = name[i]; + if (part === ".") { + name.splice(i, 1); + i -= 1; + } else if (part === "..") { + if (i === 1 && (name[2] === '..' || name[0] === '..')) { + //End of the line. Keep at least one non-dot + //path segment at the front so it can be mapped + //correctly to disk. Otherwise, there is likely + //no path mapping for a path starting with '..'. + //This can still fail, but catches the most reasonable + //uses of .. + break; + } else if (i > 0) { + name.splice(i - 1, 2); + i -= 2; + } + } + } + //end trimDots + + name = name.join("/"); + } else if (name.indexOf('./') === 0) { + // No baseName, so this is ID is resolved relative + // to baseUrl, pull off the leading dot. + name = name.substring(2); + } + } + + //Apply map config if available. + if ((baseParts || starMap) && map) { + nameParts = name.split('/'); + + for (i = nameParts.length; i > 0; i -= 1) { + nameSegment = nameParts.slice(0, i).join("/"); + + if (baseParts) { + //Find the longest baseName segment match in the config. + //So, do joins on the biggest to smallest lengths of baseParts. + for (j = baseParts.length; j > 0; j -= 1) { + mapValue = map[baseParts.slice(0, j).join('/')]; + + //baseName segment has config, find if it has one for + //this name. + if (mapValue) { + mapValue = mapValue[nameSegment]; + if (mapValue) { + //Match, update name to the new value. + foundMap = mapValue; + foundI = i; + break; + } + } + } + } + + if (foundMap) { + break; + } + + //Check for a star map match, but just hold on to it, + //if there is a shorter segment match later in a matching + //config, then favor over this star map. + if (!foundStarMap && starMap && starMap[nameSegment]) { + foundStarMap = starMap[nameSegment]; + starI = i; + } + } + + if (!foundMap && foundStarMap) { + foundMap = foundStarMap; + foundI = starI; + } + + if (foundMap) { + nameParts.splice(0, foundI, foundMap); + name = nameParts.join('/'); + } + } + + return name; + } + + function makeRequire(relName, forceSync) { + return function () { + //A version of a require function that passes a moduleName + //value for items that may need to + //look up paths relative to the moduleName + var args = aps.call(arguments, 0); + + //If first arg is not require('string'), and there is only + //one arg, it is the array form without a callback. Insert + //a null so that the following concat is correct. + if (typeof args[0] !== 'string' && args.length === 1) { + args.push(null); + } + return req.apply(undef, args.concat([relName, forceSync])); + }; + } + + function makeNormalize(relName) { + return function (name) { + return normalize(name, relName); + }; + } + + function makeLoad(depName) { + return function (value) { + defined[depName] = value; + }; + } + + function callDep(name) { + if (hasProp(waiting, name)) { + var args = waiting[name]; + delete waiting[name]; + defining[name] = true; + main.apply(undef, args); + } + + if (!hasProp(defined, name) && !hasProp(defining, name)) { + throw new Error('No ' + name); + } + return defined[name]; + } + + //Turns a plugin!resource to [plugin, resource] + //with the plugin being undefined if the name + //did not have a plugin prefix. + function splitPrefix(name) { + var prefix, + index = name ? name.indexOf('!') : -1; + if (index > -1) { + prefix = name.substring(0, index); + name = name.substring(index + 1, name.length); + } + return [prefix, name]; + } + + /** + * Makes a name map, normalizing the name, and using a plugin + * for normalization if necessary. Grabs a ref to plugin + * too, as an optimization. + */ + makeMap = function (name, relName) { + var plugin, + parts = splitPrefix(name), + prefix = parts[0]; + + name = parts[1]; + + if (prefix) { + prefix = normalize(prefix, relName); + plugin = callDep(prefix); + } + + //Normalize according + if (prefix) { + if (plugin && plugin.normalize) { + name = plugin.normalize(name, makeNormalize(relName)); + } else { + name = normalize(name, relName); + } + } else { + name = normalize(name, relName); + parts = splitPrefix(name); + prefix = parts[0]; + name = parts[1]; + if (prefix) { + plugin = callDep(prefix); + } + } + + //Using ridiculous property names for space reasons + return { + f: prefix ? prefix + '!' + name : name, //fullName + n: name, + pr: prefix, + p: plugin + }; + }; + + function makeConfig(name) { + return function () { + return (config && config.config && config.config[name]) || {}; + }; + } + + handlers = { + require: function (name) { + return makeRequire(name); + }, + exports: function (name) { + var e = defined[name]; + if (typeof e !== 'undefined') { + return e; + } else { + return (defined[name] = {}); + } + }, + module: function (name) { + return { + id: name, + uri: '', + exports: defined[name], + config: makeConfig(name) + }; + } + }; + + main = function (name, deps, callback, relName) { + var cjsModule, depName, ret, map, i, + args = [], + callbackType = typeof callback, + usingExports; + + //Use name if no relName + relName = relName || name; + + //Call the callback to define the module, if necessary. + if (callbackType === 'undefined' || callbackType === 'function') { + //Pull out the defined dependencies and pass the ordered + //values to the callback. + //Default to [require, exports, module] if no deps + deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps; + for (i = 0; i < deps.length; i += 1) { + map = makeMap(deps[i], relName); + depName = map.f; + + //Fast path CommonJS standard dependencies. + if (depName === "require") { + args[i] = handlers.require(name); + } else if (depName === "exports") { + //CommonJS module spec 1.1 + args[i] = handlers.exports(name); + usingExports = true; + } else if (depName === "module") { + //CommonJS module spec 1.1 + cjsModule = args[i] = handlers.module(name); + } else if (hasProp(defined, depName) || + hasProp(waiting, depName) || + hasProp(defining, depName)) { + args[i] = callDep(depName); + } else if (map.p) { + map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {}); + args[i] = defined[depName]; + } else { + throw new Error(name + ' missing ' + depName); + } + } + + ret = callback ? callback.apply(defined[name], args) : undefined; + + if (name) { + //If setting exports via "module" is in play, + //favor that over return value and exports. After that, + //favor a non-undefined return value over exports use. + if (cjsModule && cjsModule.exports !== undef && + cjsModule.exports !== defined[name]) { + defined[name] = cjsModule.exports; + } else if (ret !== undef || !usingExports) { + //Use the return value from the function. + defined[name] = ret; + } + } + } else if (name) { + //May just be an object definition for the module. Only + //worry about defining if have a module name. + defined[name] = callback; + } + }; + + requirejs = require = req = function (deps, callback, relName, forceSync, alt) { + if (typeof deps === "string") { + if (handlers[deps]) { + //callback in this case is really relName + return handlers[deps](callback); + } + //Just return the module wanted. In this scenario, the + //deps arg is the module name, and second arg (if passed) + //is just the relName. + //Normalize module name, if it contains . or .. + return callDep(makeMap(deps, callback).f); + } else if (!deps.splice) { + //deps is a config object, not an array. + config = deps; + if (config.deps) { + req(config.deps, config.callback); + } + if (!callback) { + return; + } + + if (callback.splice) { + //callback is an array, which means it is a dependency list. + //Adjust args if there are dependencies + deps = callback; + callback = relName; + relName = null; + } else { + deps = undef; + } + } + + //Support require(['a']) + callback = callback || function () {}; + + //If relName is a function, it is an errback handler, + //so remove it. + if (typeof relName === 'function') { + relName = forceSync; + forceSync = alt; + } + + //Simulate async callback; + if (forceSync) { + main(undef, deps, callback, relName); + } else { + //Using a non-zero value because of concern for what old browsers + //do, and latest browsers "upgrade" to 4 if lower value is used: + //http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout: + //If want a value immediately, use require('id') instead -- something + //that works in almond on the global level, but not guaranteed and + //unlikely to work in other AMD implementations. + setTimeout(function () { + main(undef, deps, callback, relName); + }, 4); + } + + return req; + }; + + /** + * Just drops the config on the floor, but returns req in case + * the config return value is used. + */ + req.config = function (cfg) { + return req(cfg); + }; + + /** + * Expose module registry for debugging and tooling + */ + requirejs._defined = defined; + + define = function (name, deps, callback) { + if (typeof name !== 'string') { + throw new Error('See almond README: incorrect module build, no module name'); + } + + //This module may not have dependencies + if (!deps.splice) { + //deps is not an array, so probably means + //an object literal or factory function for + //the value. Adjust args. + callback = deps; + deps = []; + } + + if (!hasProp(defined, name) && !hasProp(waiting, name)) { + waiting[name] = [name, deps, callback]; + } + }; + + define.amd = { + jQuery: true + }; +}()); + +S2.requirejs = requirejs;S2.require = require;S2.define = define; +} +}()); +S2.define("almond", function(){}); + +/* global jQuery:false, $:false */ +S2.define('jquery',[],function () { + var _$ = jQuery || $; + + if (_$ == null && console && console.error) { + console.error( + 'Select2: An instance of jQuery or a jQuery-compatible library was not ' + + 'found. Make sure that you are including jQuery before Select2 on your ' + + 'web page.' + ); + } + + return _$; +}); + +S2.define('select2/utils',[ + 'jquery' +], function ($) { + var Utils = {}; + + Utils.Extend = function (ChildClass, SuperClass) { + var __hasProp = {}.hasOwnProperty; + + function BaseConstructor () { + this.constructor = ChildClass; + } + + for (var key in SuperClass) { + if (__hasProp.call(SuperClass, key)) { + ChildClass[key] = SuperClass[key]; + } + } + + BaseConstructor.prototype = SuperClass.prototype; + ChildClass.prototype = new BaseConstructor(); + ChildClass.__super__ = SuperClass.prototype; + + return ChildClass; + }; + + function getMethods (theClass) { + var proto = theClass.prototype; + + var methods = []; + + for (var methodName in proto) { + var m = proto[methodName]; + + if (typeof m !== 'function') { + continue; + } + + if (methodName === 'constructor') { + continue; + } + + methods.push(methodName); + } + + return methods; + } + + Utils.Decorate = function (SuperClass, DecoratorClass) { + var decoratedMethods = getMethods(DecoratorClass); + var superMethods = getMethods(SuperClass); + + function DecoratedClass () { + var unshift = Array.prototype.unshift; + + var argCount = DecoratorClass.prototype.constructor.length; + + var calledConstructor = SuperClass.prototype.constructor; + + if (argCount > 0) { + unshift.call(arguments, SuperClass.prototype.constructor); + + calledConstructor = DecoratorClass.prototype.constructor; + } + + calledConstructor.apply(this, arguments); + } + + DecoratorClass.displayName = SuperClass.displayName; + + function ctr () { + this.constructor = DecoratedClass; + } + + DecoratedClass.prototype = new ctr(); + + for (var m = 0; m < superMethods.length; m++) { + var superMethod = superMethods[m]; + + DecoratedClass.prototype[superMethod] = + SuperClass.prototype[superMethod]; + } + + var calledMethod = function (methodName) { + // Stub out the original method if it's not decorating an actual method + var originalMethod = function () {}; + + if (methodName in DecoratedClass.prototype) { + originalMethod = DecoratedClass.prototype[methodName]; + } + + var decoratedMethod = DecoratorClass.prototype[methodName]; + + return function () { + var unshift = Array.prototype.unshift; + + unshift.call(arguments, originalMethod); + + return decoratedMethod.apply(this, arguments); + }; + }; + + for (var d = 0; d < decoratedMethods.length; d++) { + var decoratedMethod = decoratedMethods[d]; + + DecoratedClass.prototype[decoratedMethod] = calledMethod(decoratedMethod); + } + + return DecoratedClass; + }; + + var Observable = function () { + this.listeners = {}; + }; + + Observable.prototype.on = function (event, callback) { + this.listeners = this.listeners || {}; + + if (event in this.listeners) { + this.listeners[event].push(callback); + } else { + this.listeners[event] = [callback]; + } + }; + + Observable.prototype.trigger = function (event) { + var slice = Array.prototype.slice; + var params = slice.call(arguments, 1); + + this.listeners = this.listeners || {}; + + // Params should always come in as an array + if (params == null) { + params = []; + } + + // If there are no arguments to the event, use a temporary object + if (params.length === 0) { + params.push({}); + } + + // Set the `_type` of the first object to the event + params[0]._type = event; + + if (event in this.listeners) { + this.invoke(this.listeners[event], slice.call(arguments, 1)); + } + + if ('*' in this.listeners) { + this.invoke(this.listeners['*'], arguments); + } + }; + + Observable.prototype.invoke = function (listeners, params) { + for (var i = 0, len = listeners.length; i < len; i++) { + listeners[i].apply(this, params); + } + }; + + Utils.Observable = Observable; + + Utils.generateChars = function (length) { + var chars = ''; + + for (var i = 0; i < length; i++) { + var randomChar = Math.floor(Math.random() * 36); + chars += randomChar.toString(36); + } + + return chars; + }; + + Utils.bind = function (func, context) { + return function () { + func.apply(context, arguments); + }; + }; + + Utils._convertData = function (data) { + for (var originalKey in data) { + var keys = originalKey.split('-'); + + var dataLevel = data; + + if (keys.length === 1) { + continue; + } + + for (var k = 0; k < keys.length; k++) { + var key = keys[k]; + + // Lowercase the first letter + // By default, dash-separated becomes camelCase + key = key.substring(0, 1).toLowerCase() + key.substring(1); + + if (!(key in dataLevel)) { + dataLevel[key] = {}; + } + + if (k == keys.length - 1) { + dataLevel[key] = data[originalKey]; + } + + dataLevel = dataLevel[key]; + } + + delete data[originalKey]; + } + + return data; + }; + + Utils.hasScroll = function (index, el) { + // Adapted from the function created by @ShadowScripter + // and adapted by @BillBarry on the Stack Exchange Code Review website. + // The original code can be found at + // http://codereview.stackexchange.com/q/13338 + // and was designed to be used with the Sizzle selector engine. + + var $el = $(el); + var overflowX = el.style.overflowX; + var overflowY = el.style.overflowY; + + //Check both x and y declarations + if (overflowX === overflowY && + (overflowY === 'hidden' || overflowY === 'visible')) { + return false; + } + + if (overflowX === 'scroll' || overflowY === 'scroll') { + return true; + } + + return ($el.innerHeight() < el.scrollHeight || + $el.innerWidth() < el.scrollWidth); + }; + + Utils.escapeMarkup = function (markup) { + var replaceMap = { + '\\': '\', + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''', + '/': '/' + }; + + // Do not try to escape the markup if it's not a string + if (typeof markup !== 'string') { + return markup; + } + + return String(markup).replace(/[&<>"'\/\\]/g, function (match) { + return replaceMap[match]; + }); + }; + + // Append an array of jQuery nodes to a given element. + Utils.appendMany = function ($element, $nodes) { + // jQuery 1.7.x does not support $.fn.append() with an array + // Fall back to a jQuery object collection using $.fn.add() + if ($.fn.jquery.substr(0, 3) === '1.7') { + var $jqNodes = $(); + + $.map($nodes, function (node) { + $jqNodes = $jqNodes.add(node); + }); + + $nodes = $jqNodes; + } + + $element.append($nodes); + }; + + return Utils; +}); + +S2.define('select2/results',[ + 'jquery', + './utils' +], function ($, Utils) { + function Results ($element, options, dataAdapter) { + this.$element = $element; + this.data = dataAdapter; + this.options = options; + + Results.__super__.constructor.call(this); + } + + Utils.Extend(Results, Utils.Observable); + + Results.prototype.render = function () { + var $results = $( + '
      ' + ); + + if (this.options.get('multiple')) { + $results.attr('aria-multiselectable', 'true'); + } + + this.$results = $results; + + return $results; + }; + + Results.prototype.clear = function () { + this.$results.empty(); + }; + + Results.prototype.displayMessage = function (params) { + var escapeMarkup = this.options.get('escapeMarkup'); + + this.clear(); + this.hideLoading(); + + var $message = $( + '
    • ' + ); + + var message = this.options.get('translations').get(params.message); + + $message.append( + escapeMarkup( + message(params.args) + ) + ); + + $message[0].className += ' select2-results__message'; + + this.$results.append($message); + }; + + Results.prototype.hideMessages = function () { + this.$results.find('.select2-results__message').remove(); + }; + + Results.prototype.append = function (data) { + this.hideLoading(); + + var $options = []; + + if (data.results == null || data.results.length === 0) { + if (this.$results.children().length === 0) { + this.trigger('results:message', { + message: 'noResults' + }); + } + + return; + } + + data.results = this.sort(data.results); + + for (var d = 0; d < data.results.length; d++) { + var item = data.results[d]; + + var $option = this.option(item); + + $options.push($option); + } + + this.$results.append($options); + }; + + Results.prototype.position = function ($results, $dropdown) { + var $resultsContainer = $dropdown.find('.select2-results'); + $resultsContainer.append($results); + }; + + Results.prototype.sort = function (data) { + var sorter = this.options.get('sorter'); + + return sorter(data); + }; + + Results.prototype.highlightFirstItem = function () { + var $options = this.$results + .find('.select2-results__option[aria-selected]'); + + var $selected = $options.filter('[aria-selected=true]'); + + // Check if there are any selected options + if ($selected.length > 0) { + // If there are selected options, highlight the first + $selected.first().trigger('mouseenter'); + } else { + // If there are no selected options, highlight the first option + // in the dropdown + $options.first().trigger('mouseenter'); + } + + this.ensureHighlightVisible(); + }; + + Results.prototype.setClasses = function () { + var self = this; + + this.data.current(function (selected) { + var selectedIds = $.map(selected, function (s) { + return s.id.toString(); + }); + + var $options = self.$results + .find('.select2-results__option[aria-selected]'); + + $options.each(function () { + var $option = $(this); + + var item = $.data(this, 'data'); + + // id needs to be converted to a string when comparing + var id = '' + item.id; + + if ((item.element != null && item.element.selected) || + (item.element == null && $.inArray(id, selectedIds) > -1)) { + $option.attr('aria-selected', 'true'); + } else { + $option.attr('aria-selected', 'false'); + } + }); + + }); + }; + + Results.prototype.showLoading = function (params) { + this.hideLoading(); + + var loadingMore = this.options.get('translations').get('searching'); + + var loading = { + disabled: true, + loading: true, + text: loadingMore(params) + }; + var $loading = this.option(loading); + $loading.className += ' loading-results'; + + this.$results.prepend($loading); + }; + + Results.prototype.hideLoading = function () { + this.$results.find('.loading-results').remove(); + }; + + Results.prototype.option = function (data) { + var option = document.createElement('li'); + option.className = 'select2-results__option'; + + var attrs = { + 'role': 'treeitem', + 'aria-selected': 'false' + }; + + if (data.disabled) { + delete attrs['aria-selected']; + attrs['aria-disabled'] = 'true'; + } + + if (data.id == null) { + delete attrs['aria-selected']; + } + + if (data._resultId != null) { + option.id = data._resultId; + } + + if (data.title) { + option.title = data.title; + } + + if (data.children) { + attrs.role = 'group'; + attrs['aria-label'] = data.text; + delete attrs['aria-selected']; + } + + for (var attr in attrs) { + var val = attrs[attr]; + + option.setAttribute(attr, val); + } + + if (data.children) { + var $option = $(option); + + var label = document.createElement('strong'); + label.className = 'select2-results__group'; + + var $label = $(label); + this.template(data, label); + + var $children = []; + + for (var c = 0; c < data.children.length; c++) { + var child = data.children[c]; + + var $child = this.option(child); + + $children.push($child); + } + + var $childrenContainer = $('
        ', { + 'class': 'select2-results__options select2-results__options--nested' + }); + + $childrenContainer.append($children); + + $option.append(label); + $option.append($childrenContainer); + } else { + this.template(data, option); + } + + $.data(option, 'data', data); + + return option; + }; + + Results.prototype.bind = function (container, $container) { + var self = this; + + var id = container.id + '-results'; + + this.$results.attr('id', id); + + container.on('results:all', function (params) { + self.clear(); + self.append(params.data); + + if (container.isOpen()) { + self.setClasses(); + self.highlightFirstItem(); + } + }); + + container.on('results:append', function (params) { + self.append(params.data); + + if (container.isOpen()) { + self.setClasses(); + } + }); + + container.on('query', function (params) { + self.hideMessages(); + self.showLoading(params); + }); + + container.on('select', function () { + if (!container.isOpen()) { + return; + } + + self.setClasses(); + self.highlightFirstItem(); + }); + + container.on('unselect', function () { + if (!container.isOpen()) { + return; + } + + self.setClasses(); + self.highlightFirstItem(); + }); + + container.on('open', function () { + // When the dropdown is open, aria-expended="true" + self.$results.attr('aria-expanded', 'true'); + self.$results.attr('aria-hidden', 'false'); + + self.setClasses(); + self.ensureHighlightVisible(); + }); + + container.on('close', function () { + // When the dropdown is closed, aria-expended="false" + self.$results.attr('aria-expanded', 'false'); + self.$results.attr('aria-hidden', 'true'); + self.$results.removeAttr('aria-activedescendant'); + }); + + container.on('results:toggle', function () { + var $highlighted = self.getHighlightedResults(); + + if ($highlighted.length === 0) { + return; + } + + $highlighted.trigger('mouseup'); + }); + + container.on('results:select', function () { + var $highlighted = self.getHighlightedResults(); + + if ($highlighted.length === 0) { + return; + } + + var data = $highlighted.data('data'); + + if ($highlighted.attr('aria-selected') == 'true') { + self.trigger('close', {}); + } else { + self.trigger('select', { + data: data + }); + } + }); + + container.on('results:previous', function () { + var $highlighted = self.getHighlightedResults(); + + var $options = self.$results.find('[aria-selected]'); + + var currentIndex = $options.index($highlighted); + + // If we are already at te top, don't move further + if (currentIndex === 0) { + return; + } + + var nextIndex = currentIndex - 1; + + // If none are highlighted, highlight the first + if ($highlighted.length === 0) { + nextIndex = 0; + } + + var $next = $options.eq(nextIndex); + + $next.trigger('mouseenter'); + + var currentOffset = self.$results.offset().top; + var nextTop = $next.offset().top; + var nextOffset = self.$results.scrollTop() + (nextTop - currentOffset); + + if (nextIndex === 0) { + self.$results.scrollTop(0); + } else if (nextTop - currentOffset < 0) { + self.$results.scrollTop(nextOffset); + } + }); + + container.on('results:next', function () { + var $highlighted = self.getHighlightedResults(); + + var $options = self.$results.find('[aria-selected]'); + + var currentIndex = $options.index($highlighted); + + var nextIndex = currentIndex + 1; + + // If we are at the last option, stay there + if (nextIndex >= $options.length) { + return; + } + + var $next = $options.eq(nextIndex); + + $next.trigger('mouseenter'); + + var currentOffset = self.$results.offset().top + + self.$results.outerHeight(false); + var nextBottom = $next.offset().top + $next.outerHeight(false); + var nextOffset = self.$results.scrollTop() + nextBottom - currentOffset; + + if (nextIndex === 0) { + self.$results.scrollTop(0); + } else if (nextBottom > currentOffset) { + self.$results.scrollTop(nextOffset); + } + }); + + container.on('results:focus', function (params) { + params.element.addClass('select2-results__option--highlighted'); + }); + + container.on('results:message', function (params) { + self.displayMessage(params); + }); + + if ($.fn.mousewheel) { + this.$results.on('mousewheel', function (e) { + var top = self.$results.scrollTop(); + + var bottom = self.$results.get(0).scrollHeight - top + e.deltaY; + + var isAtTop = e.deltaY > 0 && top - e.deltaY <= 0; + var isAtBottom = e.deltaY < 0 && bottom <= self.$results.height(); + + if (isAtTop) { + self.$results.scrollTop(0); + + e.preventDefault(); + e.stopPropagation(); + } else if (isAtBottom) { + self.$results.scrollTop( + self.$results.get(0).scrollHeight - self.$results.height() + ); + + e.preventDefault(); + e.stopPropagation(); + } + }); + } + + this.$results.on('mouseup', '.select2-results__option[aria-selected]', + function (evt) { + var $this = $(this); + + var data = $this.data('data'); + + if ($this.attr('aria-selected') === 'true') { + if (self.options.get('multiple')) { + self.trigger('unselect', { + originalEvent: evt, + data: data + }); + } else { + self.trigger('close', {}); + } + + return; + } + + self.trigger('select', { + originalEvent: evt, + data: data + }); + }); + + this.$results.on('mouseenter', '.select2-results__option[aria-selected]', + function (evt) { + var data = $(this).data('data'); + + self.getHighlightedResults() + .removeClass('select2-results__option--highlighted'); + + self.trigger('results:focus', { + data: data, + element: $(this) + }); + }); + }; + + Results.prototype.getHighlightedResults = function () { + var $highlighted = this.$results + .find('.select2-results__option--highlighted'); + + return $highlighted; + }; + + Results.prototype.destroy = function () { + this.$results.remove(); + }; + + Results.prototype.ensureHighlightVisible = function () { + var $highlighted = this.getHighlightedResults(); + + if ($highlighted.length === 0) { + return; + } + + var $options = this.$results.find('[aria-selected]'); + + var currentIndex = $options.index($highlighted); + + var currentOffset = this.$results.offset().top; + var nextTop = $highlighted.offset().top; + var nextOffset = this.$results.scrollTop() + (nextTop - currentOffset); + + var offsetDelta = nextTop - currentOffset; + nextOffset -= $highlighted.outerHeight(false) * 2; + + if (currentIndex <= 2) { + this.$results.scrollTop(0); + } else if (offsetDelta > this.$results.outerHeight() || offsetDelta < 0) { + this.$results.scrollTop(nextOffset); + } + }; + + Results.prototype.template = function (result, container) { + var template = this.options.get('templateResult'); + var escapeMarkup = this.options.get('escapeMarkup'); + + var content = template(result, container); + + if (content == null) { + container.style.display = 'none'; + } else if (typeof content === 'string') { + container.innerHTML = escapeMarkup(content); + } else { + $(container).append(content); + } + }; + + return Results; +}); + +S2.define('select2/keys',[ + +], function () { + var KEYS = { + BACKSPACE: 8, + TAB: 9, + ENTER: 13, + SHIFT: 16, + CTRL: 17, + ALT: 18, + ESC: 27, + SPACE: 32, + PAGE_UP: 33, + PAGE_DOWN: 34, + END: 35, + HOME: 36, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + DELETE: 46 + }; + + return KEYS; +}); + +S2.define('select2/selection/base',[ + 'jquery', + '../utils', + '../keys' +], function ($, Utils, KEYS) { + function BaseSelection ($element, options) { + this.$element = $element; + this.options = options; + + BaseSelection.__super__.constructor.call(this); + } + + Utils.Extend(BaseSelection, Utils.Observable); + + BaseSelection.prototype.render = function () { + var $selection = $( + '' + ); + + this._tabindex = 0; + + if (this.$element.data('old-tabindex') != null) { + this._tabindex = this.$element.data('old-tabindex'); + } else if (this.$element.attr('tabindex') != null) { + this._tabindex = this.$element.attr('tabindex'); + } + + $selection.attr('title', this.$element.attr('title')); + $selection.attr('tabindex', this._tabindex); + + this.$selection = $selection; + + return $selection; + }; + + BaseSelection.prototype.bind = function (container, $container) { + var self = this; + + var id = container.id + '-container'; + var resultsId = container.id + '-results'; + + this.container = container; + + this.$selection.on('focus', function (evt) { + self.trigger('focus', evt); + }); + + this.$selection.on('blur', function (evt) { + self._handleBlur(evt); + }); + + this.$selection.on('keydown', function (evt) { + self.trigger('keypress', evt); + + if (evt.which === KEYS.SPACE) { + evt.preventDefault(); + } + }); + + container.on('results:focus', function (params) { + self.$selection.attr('aria-activedescendant', params.data._resultId); + }); + + container.on('selection:update', function (params) { + self.update(params.data); + }); + + container.on('open', function () { + // When the dropdown is open, aria-expanded="true" + self.$selection.attr('aria-expanded', 'true'); + self.$selection.attr('aria-owns', resultsId); + + self._attachCloseHandler(container); + }); + + container.on('close', function () { + // When the dropdown is closed, aria-expanded="false" + self.$selection.attr('aria-expanded', 'false'); + self.$selection.removeAttr('aria-activedescendant'); + self.$selection.removeAttr('aria-owns'); + + self.$selection.focus(); + + self._detachCloseHandler(container); + }); + + container.on('enable', function () { + self.$selection.attr('tabindex', self._tabindex); + }); + + container.on('disable', function () { + self.$selection.attr('tabindex', '-1'); + }); + }; + + BaseSelection.prototype._handleBlur = function (evt) { + var self = this; + + // This needs to be delayed as the active element is the body when the tab + // key is pressed, possibly along with others. + window.setTimeout(function () { + // Don't trigger `blur` if the focus is still in the selection + if ( + (document.activeElement == self.$selection[0]) || + ($.contains(self.$selection[0], document.activeElement)) + ) { + return; + } + + self.trigger('blur', evt); + }, 1); + }; + + BaseSelection.prototype._attachCloseHandler = function (container) { + var self = this; + + $(document.body).on('mousedown.select2.' + container.id, function (e) { + var $target = $(e.target); + + var $select = $target.closest('.select2'); + + var $all = $('.select2.select2-container--open'); + + $all.each(function () { + var $this = $(this); + + if (this == $select[0]) { + return; + } + + var $element = $this.data('element'); + + $element.select2('close'); + }); + }); + }; + + BaseSelection.prototype._detachCloseHandler = function (container) { + $(document.body).off('mousedown.select2.' + container.id); + }; + + BaseSelection.prototype.position = function ($selection, $container) { + var $selectionContainer = $container.find('.selection'); + $selectionContainer.append($selection); + }; + + BaseSelection.prototype.destroy = function () { + this._detachCloseHandler(this.container); + }; + + BaseSelection.prototype.update = function (data) { + throw new Error('The `update` method must be defined in child classes.'); + }; + + return BaseSelection; +}); + +S2.define('select2/selection/single',[ + 'jquery', + './base', + '../utils', + '../keys' +], function ($, BaseSelection, Utils, KEYS) { + function SingleSelection () { + SingleSelection.__super__.constructor.apply(this, arguments); + } + + Utils.Extend(SingleSelection, BaseSelection); + + SingleSelection.prototype.render = function () { + var $selection = SingleSelection.__super__.render.call(this); + + $selection.addClass('select2-selection--single'); + + $selection.html( + '' + + '' + + '' + + '' + ); + + return $selection; + }; + + SingleSelection.prototype.bind = function (container, $container) { + var self = this; + + SingleSelection.__super__.bind.apply(this, arguments); + + var id = container.id + '-container'; + + this.$selection.find('.select2-selection__rendered').attr('id', id); + this.$selection.attr('aria-labelledby', id); + + this.$selection.on('mousedown', function (evt) { + // Only respond to left clicks + if (evt.which !== 1) { + return; + } + + self.trigger('toggle', { + originalEvent: evt + }); + }); + + this.$selection.on('focus', function (evt) { + // User focuses on the container + }); + + this.$selection.on('blur', function (evt) { + // User exits the container + }); + + container.on('focus', function (evt) { + if (!container.isOpen()) { + self.$selection.focus(); + } + }); + + container.on('selection:update', function (params) { + self.update(params.data); + }); + }; + + SingleSelection.prototype.clear = function () { + this.$selection.find('.select2-selection__rendered').empty(); + }; + + SingleSelection.prototype.display = function (data, container) { + var template = this.options.get('templateSelection'); + var escapeMarkup = this.options.get('escapeMarkup'); + + return escapeMarkup(template(data, container)); + }; + + SingleSelection.prototype.selectionContainer = function () { + return $(''); + }; + + SingleSelection.prototype.update = function (data) { + if (data.length === 0) { + this.clear(); + return; + } + + var selection = data[0]; + + var $rendered = this.$selection.find('.select2-selection__rendered'); + var formatted = this.display(selection, $rendered); + + $rendered.empty().append(formatted); + $rendered.prop('title', selection.title || selection.text); + }; + + return SingleSelection; +}); + +S2.define('select2/selection/multiple',[ + 'jquery', + './base', + '../utils' +], function ($, BaseSelection, Utils) { + function MultipleSelection ($element, options) { + MultipleSelection.__super__.constructor.apply(this, arguments); + } + + Utils.Extend(MultipleSelection, BaseSelection); + + MultipleSelection.prototype.render = function () { + var $selection = MultipleSelection.__super__.render.call(this); + + $selection.addClass('select2-selection--multiple'); + + $selection.html( + '
          ' + ); + + return $selection; + }; + + MultipleSelection.prototype.bind = function (container, $container) { + var self = this; + + MultipleSelection.__super__.bind.apply(this, arguments); + + this.$selection.on('click', function (evt) { + self.trigger('toggle', { + originalEvent: evt + }); + }); + + this.$selection.on( + 'click', + '.select2-selection__choice__remove', + function (evt) { + // Ignore the event if it is disabled + if (self.options.get('disabled')) { + return; + } + + var $remove = $(this); + var $selection = $remove.parent(); + + var data = $selection.data('data'); + + self.trigger('unselect', { + originalEvent: evt, + data: data + }); + } + ); + }; + + MultipleSelection.prototype.clear = function () { + this.$selection.find('.select2-selection__rendered').empty(); + }; + + MultipleSelection.prototype.display = function (data, container) { + var template = this.options.get('templateSelection'); + var escapeMarkup = this.options.get('escapeMarkup'); + + return escapeMarkup(template(data, container)); + }; + + MultipleSelection.prototype.selectionContainer = function () { + var $container = $( + '
        • ' + + '' + + '×' + + '' + + '
        • ' + ); + + return $container; + }; + + MultipleSelection.prototype.update = function (data) { + this.clear(); + + if (data.length === 0) { + return; + } + + var $selections = []; + + for (var d = 0; d < data.length; d++) { + var selection = data[d]; + + var $selection = this.selectionContainer(); + var formatted = this.display(selection, $selection); + + $selection.append(formatted); + $selection.prop('title', selection.title || selection.text); + + $selection.data('data', selection); + + $selections.push($selection); + } + + var $rendered = this.$selection.find('.select2-selection__rendered'); + + Utils.appendMany($rendered, $selections); + }; + + return MultipleSelection; +}); + +S2.define('select2/selection/placeholder',[ + '../utils' +], function (Utils) { + function Placeholder (decorated, $element, options) { + this.placeholder = this.normalizePlaceholder(options.get('placeholder')); + + decorated.call(this, $element, options); + } + + Placeholder.prototype.normalizePlaceholder = function (_, placeholder) { + if (typeof placeholder === 'string') { + placeholder = { + id: '', + text: placeholder + }; + } + + return placeholder; + }; + + Placeholder.prototype.createPlaceholder = function (decorated, placeholder) { + var $placeholder = this.selectionContainer(); + + $placeholder.html(this.display(placeholder)); + $placeholder.addClass('select2-selection__placeholder') + .removeClass('select2-selection__choice'); + + return $placeholder; + }; + + Placeholder.prototype.update = function (decorated, data) { + var singlePlaceholder = ( + data.length == 1 && data[0].id != this.placeholder.id + ); + var multipleSelections = data.length > 1; + + if (multipleSelections || singlePlaceholder) { + return decorated.call(this, data); + } + + this.clear(); + + var $placeholder = this.createPlaceholder(this.placeholder); + + this.$selection.find('.select2-selection__rendered').append($placeholder); + }; + + return Placeholder; +}); + +S2.define('select2/selection/allowClear',[ + 'jquery', + '../keys' +], function ($, KEYS) { + function AllowClear () { } + + AllowClear.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + if (this.placeholder == null) { + if (this.options.get('debug') && window.console && console.error) { + console.error( + 'Select2: The `allowClear` option should be used in combination ' + + 'with the `placeholder` option.' + ); + } + } + + this.$selection.on('mousedown', '.select2-selection__clear', + function (evt) { + self._handleClear(evt); + }); + + container.on('keypress', function (evt) { + self._handleKeyboardClear(evt, container); + }); + }; + + AllowClear.prototype._handleClear = function (_, evt) { + // Ignore the event if it is disabled + if (this.options.get('disabled')) { + return; + } + + var $clear = this.$selection.find('.select2-selection__clear'); + + // Ignore the event if nothing has been selected + if ($clear.length === 0) { + return; + } + + evt.stopPropagation(); + + var data = $clear.data('data'); + + for (var d = 0; d < data.length; d++) { + var unselectData = { + data: data[d] + }; + + // Trigger the `unselect` event, so people can prevent it from being + // cleared. + this.trigger('unselect', unselectData); + + // If the event was prevented, don't clear it out. + if (unselectData.prevented) { + return; + } + } + + this.$element.val(this.placeholder.id).trigger('change'); + + this.trigger('toggle', {}); + }; + + AllowClear.prototype._handleKeyboardClear = function (_, evt, container) { + if (container.isOpen()) { + return; + } + + if (evt.which == KEYS.DELETE || evt.which == KEYS.BACKSPACE) { + this._handleClear(evt); + } + }; + + AllowClear.prototype.update = function (decorated, data) { + decorated.call(this, data); + + if (this.$selection.find('.select2-selection__placeholder').length > 0 || + data.length === 0) { + return; + } + + var $remove = $( + '' + + '×' + + '' + ); + $remove.data('data', data); + + this.$selection.find('.select2-selection__rendered').prepend($remove); + }; + + return AllowClear; +}); + +S2.define('select2/selection/search',[ + 'jquery', + '../utils', + '../keys' +], function ($, Utils, KEYS) { + function Search (decorated, $element, options) { + decorated.call(this, $element, options); + } + + Search.prototype.render = function (decorated) { + var $search = $( + '' + ); + + this.$searchContainer = $search; + this.$search = $search.find('input'); + + var $rendered = decorated.call(this); + + this._transferTabIndex(); + + return $rendered; + }; + + Search.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('open', function () { + self.$search.trigger('focus'); + }); + + container.on('close', function () { + self.$search.val(''); + self.$search.removeAttr('aria-activedescendant'); + self.$search.trigger('focus'); + }); + + container.on('enable', function () { + self.$search.prop('disabled', false); + + self._transferTabIndex(); + }); + + container.on('disable', function () { + self.$search.prop('disabled', true); + }); + + container.on('focus', function (evt) { + self.$search.trigger('focus'); + }); + + container.on('results:focus', function (params) { + self.$search.attr('aria-activedescendant', params.id); + }); + + this.$selection.on('focusin', '.select2-search--inline', function (evt) { + self.trigger('focus', evt); + }); + + this.$selection.on('focusout', '.select2-search--inline', function (evt) { + self._handleBlur(evt); + }); + + this.$selection.on('keydown', '.select2-search--inline', function (evt) { + evt.stopPropagation(); + + self.trigger('keypress', evt); + + self._keyUpPrevented = evt.isDefaultPrevented(); + + var key = evt.which; + + if (key === KEYS.BACKSPACE && self.$search.val() === '') { + var $previousChoice = self.$searchContainer + .prev('.select2-selection__choice'); + + if ($previousChoice.length > 0) { + var item = $previousChoice.data('data'); + + self.searchRemoveChoice(item); + + evt.preventDefault(); + } + } + }); + + // Try to detect the IE version should the `documentMode` property that + // is stored on the document. This is only implemented in IE and is + // slightly cleaner than doing a user agent check. + // This property is not available in Edge, but Edge also doesn't have + // this bug. + var msie = document.documentMode; + var disableInputEvents = msie && msie <= 11; + + // Workaround for browsers which do not support the `input` event + // This will prevent double-triggering of events for browsers which support + // both the `keyup` and `input` events. + this.$selection.on( + 'input.searchcheck', + '.select2-search--inline', + function (evt) { + // IE will trigger the `input` event when a placeholder is used on a + // search box. To get around this issue, we are forced to ignore all + // `input` events in IE and keep using `keyup`. + if (disableInputEvents) { + self.$selection.off('input.search input.searchcheck'); + return; + } + + // Unbind the duplicated `keyup` event + self.$selection.off('keyup.search'); + } + ); + + this.$selection.on( + 'keyup.search input.search', + '.select2-search--inline', + function (evt) { + // IE will trigger the `input` event when a placeholder is used on a + // search box. To get around this issue, we are forced to ignore all + // `input` events in IE and keep using `keyup`. + if (disableInputEvents && evt.type === 'input') { + self.$selection.off('input.search input.searchcheck'); + return; + } + + var key = evt.which; + + // We can freely ignore events from modifier keys + if (key == KEYS.SHIFT || key == KEYS.CTRL || key == KEYS.ALT) { + return; + } + + // Tabbing will be handled during the `keydown` phase + if (key == KEYS.TAB) { + return; + } + + self.handleSearch(evt); + } + ); + }; + + /** + * This method will transfer the tabindex attribute from the rendered + * selection to the search box. This allows for the search box to be used as + * the primary focus instead of the selection container. + * + * @private + */ + Search.prototype._transferTabIndex = function (decorated) { + this.$search.attr('tabindex', this.$selection.attr('tabindex')); + this.$selection.attr('tabindex', '-1'); + }; + + Search.prototype.createPlaceholder = function (decorated, placeholder) { + this.$search.attr('placeholder', placeholder.text); + }; + + Search.prototype.update = function (decorated, data) { + var searchHadFocus = this.$search[0] == document.activeElement; + + this.$search.attr('placeholder', ''); + + decorated.call(this, data); + + this.$selection.find('.select2-selection__rendered') + .append(this.$searchContainer); + + this.resizeSearch(); + if (searchHadFocus) { + this.$search.focus(); + } + }; + + Search.prototype.handleSearch = function () { + this.resizeSearch(); + + if (!this._keyUpPrevented) { + var input = this.$search.val(); + + this.trigger('query', { + term: input + }); + } + + this._keyUpPrevented = false; + }; + + Search.prototype.searchRemoveChoice = function (decorated, item) { + this.trigger('unselect', { + data: item + }); + + this.$search.val(item.text); + this.handleSearch(); + }; + + Search.prototype.resizeSearch = function () { + this.$search.css('width', '25px'); + + var width = ''; + + if (this.$search.attr('placeholder') !== '') { + width = this.$selection.find('.select2-selection__rendered').innerWidth(); + } else { + var minimumWidth = this.$search.val().length + 1; + + width = (minimumWidth * 0.75) + 'em'; + } + + this.$search.css('width', width); + }; + + return Search; +}); + +S2.define('select2/selection/eventRelay',[ + 'jquery' +], function ($) { + function EventRelay () { } + + EventRelay.prototype.bind = function (decorated, container, $container) { + var self = this; + var relayEvents = [ + 'open', 'opening', + 'close', 'closing', + 'select', 'selecting', + 'unselect', 'unselecting' + ]; + + var preventableEvents = ['opening', 'closing', 'selecting', 'unselecting']; + + decorated.call(this, container, $container); + + container.on('*', function (name, params) { + // Ignore events that should not be relayed + if ($.inArray(name, relayEvents) === -1) { + return; + } + + // The parameters should always be an object + params = params || {}; + + // Generate the jQuery event for the Select2 event + var evt = $.Event('select2:' + name, { + params: params + }); + + self.$element.trigger(evt); + + // Only handle preventable events if it was one + if ($.inArray(name, preventableEvents) === -1) { + return; + } + + params.prevented = evt.isDefaultPrevented(); + }); + }; + + return EventRelay; +}); + +S2.define('select2/translation',[ + 'jquery', + 'require' +], function ($, require) { + function Translation (dict) { + this.dict = dict || {}; + } + + Translation.prototype.all = function () { + return this.dict; + }; + + Translation.prototype.get = function (key) { + return this.dict[key]; + }; + + Translation.prototype.extend = function (translation) { + this.dict = $.extend({}, translation.all(), this.dict); + }; + + // Static functions + + Translation._cache = {}; + + Translation.loadPath = function (path) { + if (!(path in Translation._cache)) { + var translations = require(path); + + Translation._cache[path] = translations; + } + + return new Translation(Translation._cache[path]); + }; + + return Translation; +}); + +S2.define('select2/diacritics',[ + +], function () { + var diacritics = { + '\u24B6': 'A', + '\uFF21': 'A', + '\u00C0': 'A', + '\u00C1': 'A', + '\u00C2': 'A', + '\u1EA6': 'A', + '\u1EA4': 'A', + '\u1EAA': 'A', + '\u1EA8': 'A', + '\u00C3': 'A', + '\u0100': 'A', + '\u0102': 'A', + '\u1EB0': 'A', + '\u1EAE': 'A', + '\u1EB4': 'A', + '\u1EB2': 'A', + '\u0226': 'A', + '\u01E0': 'A', + '\u00C4': 'A', + '\u01DE': 'A', + '\u1EA2': 'A', + '\u00C5': 'A', + '\u01FA': 'A', + '\u01CD': 'A', + '\u0200': 'A', + '\u0202': 'A', + '\u1EA0': 'A', + '\u1EAC': 'A', + '\u1EB6': 'A', + '\u1E00': 'A', + '\u0104': 'A', + '\u023A': 'A', + '\u2C6F': 'A', + '\uA732': 'AA', + '\u00C6': 'AE', + '\u01FC': 'AE', + '\u01E2': 'AE', + '\uA734': 'AO', + '\uA736': 'AU', + '\uA738': 'AV', + '\uA73A': 'AV', + '\uA73C': 'AY', + '\u24B7': 'B', + '\uFF22': 'B', + '\u1E02': 'B', + '\u1E04': 'B', + '\u1E06': 'B', + '\u0243': 'B', + '\u0182': 'B', + '\u0181': 'B', + '\u24B8': 'C', + '\uFF23': 'C', + '\u0106': 'C', + '\u0108': 'C', + '\u010A': 'C', + '\u010C': 'C', + '\u00C7': 'C', + '\u1E08': 'C', + '\u0187': 'C', + '\u023B': 'C', + '\uA73E': 'C', + '\u24B9': 'D', + '\uFF24': 'D', + '\u1E0A': 'D', + '\u010E': 'D', + '\u1E0C': 'D', + '\u1E10': 'D', + '\u1E12': 'D', + '\u1E0E': 'D', + '\u0110': 'D', + '\u018B': 'D', + '\u018A': 'D', + '\u0189': 'D', + '\uA779': 'D', + '\u01F1': 'DZ', + '\u01C4': 'DZ', + '\u01F2': 'Dz', + '\u01C5': 'Dz', + '\u24BA': 'E', + '\uFF25': 'E', + '\u00C8': 'E', + '\u00C9': 'E', + '\u00CA': 'E', + '\u1EC0': 'E', + '\u1EBE': 'E', + '\u1EC4': 'E', + '\u1EC2': 'E', + '\u1EBC': 'E', + '\u0112': 'E', + '\u1E14': 'E', + '\u1E16': 'E', + '\u0114': 'E', + '\u0116': 'E', + '\u00CB': 'E', + '\u1EBA': 'E', + '\u011A': 'E', + '\u0204': 'E', + '\u0206': 'E', + '\u1EB8': 'E', + '\u1EC6': 'E', + '\u0228': 'E', + '\u1E1C': 'E', + '\u0118': 'E', + '\u1E18': 'E', + '\u1E1A': 'E', + '\u0190': 'E', + '\u018E': 'E', + '\u24BB': 'F', + '\uFF26': 'F', + '\u1E1E': 'F', + '\u0191': 'F', + '\uA77B': 'F', + '\u24BC': 'G', + '\uFF27': 'G', + '\u01F4': 'G', + '\u011C': 'G', + '\u1E20': 'G', + '\u011E': 'G', + '\u0120': 'G', + '\u01E6': 'G', + '\u0122': 'G', + '\u01E4': 'G', + '\u0193': 'G', + '\uA7A0': 'G', + '\uA77D': 'G', + '\uA77E': 'G', + '\u24BD': 'H', + '\uFF28': 'H', + '\u0124': 'H', + '\u1E22': 'H', + '\u1E26': 'H', + '\u021E': 'H', + '\u1E24': 'H', + '\u1E28': 'H', + '\u1E2A': 'H', + '\u0126': 'H', + '\u2C67': 'H', + '\u2C75': 'H', + '\uA78D': 'H', + '\u24BE': 'I', + '\uFF29': 'I', + '\u00CC': 'I', + '\u00CD': 'I', + '\u00CE': 'I', + '\u0128': 'I', + '\u012A': 'I', + '\u012C': 'I', + '\u0130': 'I', + '\u00CF': 'I', + '\u1E2E': 'I', + '\u1EC8': 'I', + '\u01CF': 'I', + '\u0208': 'I', + '\u020A': 'I', + '\u1ECA': 'I', + '\u012E': 'I', + '\u1E2C': 'I', + '\u0197': 'I', + '\u24BF': 'J', + '\uFF2A': 'J', + '\u0134': 'J', + '\u0248': 'J', + '\u24C0': 'K', + '\uFF2B': 'K', + '\u1E30': 'K', + '\u01E8': 'K', + '\u1E32': 'K', + '\u0136': 'K', + '\u1E34': 'K', + '\u0198': 'K', + '\u2C69': 'K', + '\uA740': 'K', + '\uA742': 'K', + '\uA744': 'K', + '\uA7A2': 'K', + '\u24C1': 'L', + '\uFF2C': 'L', + '\u013F': 'L', + '\u0139': 'L', + '\u013D': 'L', + '\u1E36': 'L', + '\u1E38': 'L', + '\u013B': 'L', + '\u1E3C': 'L', + '\u1E3A': 'L', + '\u0141': 'L', + '\u023D': 'L', + '\u2C62': 'L', + '\u2C60': 'L', + '\uA748': 'L', + '\uA746': 'L', + '\uA780': 'L', + '\u01C7': 'LJ', + '\u01C8': 'Lj', + '\u24C2': 'M', + '\uFF2D': 'M', + '\u1E3E': 'M', + '\u1E40': 'M', + '\u1E42': 'M', + '\u2C6E': 'M', + '\u019C': 'M', + '\u24C3': 'N', + '\uFF2E': 'N', + '\u01F8': 'N', + '\u0143': 'N', + '\u00D1': 'N', + '\u1E44': 'N', + '\u0147': 'N', + '\u1E46': 'N', + '\u0145': 'N', + '\u1E4A': 'N', + '\u1E48': 'N', + '\u0220': 'N', + '\u019D': 'N', + '\uA790': 'N', + '\uA7A4': 'N', + '\u01CA': 'NJ', + '\u01CB': 'Nj', + '\u24C4': 'O', + '\uFF2F': 'O', + '\u00D2': 'O', + '\u00D3': 'O', + '\u00D4': 'O', + '\u1ED2': 'O', + '\u1ED0': 'O', + '\u1ED6': 'O', + '\u1ED4': 'O', + '\u00D5': 'O', + '\u1E4C': 'O', + '\u022C': 'O', + '\u1E4E': 'O', + '\u014C': 'O', + '\u1E50': 'O', + '\u1E52': 'O', + '\u014E': 'O', + '\u022E': 'O', + '\u0230': 'O', + '\u00D6': 'O', + '\u022A': 'O', + '\u1ECE': 'O', + '\u0150': 'O', + '\u01D1': 'O', + '\u020C': 'O', + '\u020E': 'O', + '\u01A0': 'O', + '\u1EDC': 'O', + '\u1EDA': 'O', + '\u1EE0': 'O', + '\u1EDE': 'O', + '\u1EE2': 'O', + '\u1ECC': 'O', + '\u1ED8': 'O', + '\u01EA': 'O', + '\u01EC': 'O', + '\u00D8': 'O', + '\u01FE': 'O', + '\u0186': 'O', + '\u019F': 'O', + '\uA74A': 'O', + '\uA74C': 'O', + '\u01A2': 'OI', + '\uA74E': 'OO', + '\u0222': 'OU', + '\u24C5': 'P', + '\uFF30': 'P', + '\u1E54': 'P', + '\u1E56': 'P', + '\u01A4': 'P', + '\u2C63': 'P', + '\uA750': 'P', + '\uA752': 'P', + '\uA754': 'P', + '\u24C6': 'Q', + '\uFF31': 'Q', + '\uA756': 'Q', + '\uA758': 'Q', + '\u024A': 'Q', + '\u24C7': 'R', + '\uFF32': 'R', + '\u0154': 'R', + '\u1E58': 'R', + '\u0158': 'R', + '\u0210': 'R', + '\u0212': 'R', + '\u1E5A': 'R', + '\u1E5C': 'R', + '\u0156': 'R', + '\u1E5E': 'R', + '\u024C': 'R', + '\u2C64': 'R', + '\uA75A': 'R', + '\uA7A6': 'R', + '\uA782': 'R', + '\u24C8': 'S', + '\uFF33': 'S', + '\u1E9E': 'S', + '\u015A': 'S', + '\u1E64': 'S', + '\u015C': 'S', + '\u1E60': 'S', + '\u0160': 'S', + '\u1E66': 'S', + '\u1E62': 'S', + '\u1E68': 'S', + '\u0218': 'S', + '\u015E': 'S', + '\u2C7E': 'S', + '\uA7A8': 'S', + '\uA784': 'S', + '\u24C9': 'T', + '\uFF34': 'T', + '\u1E6A': 'T', + '\u0164': 'T', + '\u1E6C': 'T', + '\u021A': 'T', + '\u0162': 'T', + '\u1E70': 'T', + '\u1E6E': 'T', + '\u0166': 'T', + '\u01AC': 'T', + '\u01AE': 'T', + '\u023E': 'T', + '\uA786': 'T', + '\uA728': 'TZ', + '\u24CA': 'U', + '\uFF35': 'U', + '\u00D9': 'U', + '\u00DA': 'U', + '\u00DB': 'U', + '\u0168': 'U', + '\u1E78': 'U', + '\u016A': 'U', + '\u1E7A': 'U', + '\u016C': 'U', + '\u00DC': 'U', + '\u01DB': 'U', + '\u01D7': 'U', + '\u01D5': 'U', + '\u01D9': 'U', + '\u1EE6': 'U', + '\u016E': 'U', + '\u0170': 'U', + '\u01D3': 'U', + '\u0214': 'U', + '\u0216': 'U', + '\u01AF': 'U', + '\u1EEA': 'U', + '\u1EE8': 'U', + '\u1EEE': 'U', + '\u1EEC': 'U', + '\u1EF0': 'U', + '\u1EE4': 'U', + '\u1E72': 'U', + '\u0172': 'U', + '\u1E76': 'U', + '\u1E74': 'U', + '\u0244': 'U', + '\u24CB': 'V', + '\uFF36': 'V', + '\u1E7C': 'V', + '\u1E7E': 'V', + '\u01B2': 'V', + '\uA75E': 'V', + '\u0245': 'V', + '\uA760': 'VY', + '\u24CC': 'W', + '\uFF37': 'W', + '\u1E80': 'W', + '\u1E82': 'W', + '\u0174': 'W', + '\u1E86': 'W', + '\u1E84': 'W', + '\u1E88': 'W', + '\u2C72': 'W', + '\u24CD': 'X', + '\uFF38': 'X', + '\u1E8A': 'X', + '\u1E8C': 'X', + '\u24CE': 'Y', + '\uFF39': 'Y', + '\u1EF2': 'Y', + '\u00DD': 'Y', + '\u0176': 'Y', + '\u1EF8': 'Y', + '\u0232': 'Y', + '\u1E8E': 'Y', + '\u0178': 'Y', + '\u1EF6': 'Y', + '\u1EF4': 'Y', + '\u01B3': 'Y', + '\u024E': 'Y', + '\u1EFE': 'Y', + '\u24CF': 'Z', + '\uFF3A': 'Z', + '\u0179': 'Z', + '\u1E90': 'Z', + '\u017B': 'Z', + '\u017D': 'Z', + '\u1E92': 'Z', + '\u1E94': 'Z', + '\u01B5': 'Z', + '\u0224': 'Z', + '\u2C7F': 'Z', + '\u2C6B': 'Z', + '\uA762': 'Z', + '\u24D0': 'a', + '\uFF41': 'a', + '\u1E9A': 'a', + '\u00E0': 'a', + '\u00E1': 'a', + '\u00E2': 'a', + '\u1EA7': 'a', + '\u1EA5': 'a', + '\u1EAB': 'a', + '\u1EA9': 'a', + '\u00E3': 'a', + '\u0101': 'a', + '\u0103': 'a', + '\u1EB1': 'a', + '\u1EAF': 'a', + '\u1EB5': 'a', + '\u1EB3': 'a', + '\u0227': 'a', + '\u01E1': 'a', + '\u00E4': 'a', + '\u01DF': 'a', + '\u1EA3': 'a', + '\u00E5': 'a', + '\u01FB': 'a', + '\u01CE': 'a', + '\u0201': 'a', + '\u0203': 'a', + '\u1EA1': 'a', + '\u1EAD': 'a', + '\u1EB7': 'a', + '\u1E01': 'a', + '\u0105': 'a', + '\u2C65': 'a', + '\u0250': 'a', + '\uA733': 'aa', + '\u00E6': 'ae', + '\u01FD': 'ae', + '\u01E3': 'ae', + '\uA735': 'ao', + '\uA737': 'au', + '\uA739': 'av', + '\uA73B': 'av', + '\uA73D': 'ay', + '\u24D1': 'b', + '\uFF42': 'b', + '\u1E03': 'b', + '\u1E05': 'b', + '\u1E07': 'b', + '\u0180': 'b', + '\u0183': 'b', + '\u0253': 'b', + '\u24D2': 'c', + '\uFF43': 'c', + '\u0107': 'c', + '\u0109': 'c', + '\u010B': 'c', + '\u010D': 'c', + '\u00E7': 'c', + '\u1E09': 'c', + '\u0188': 'c', + '\u023C': 'c', + '\uA73F': 'c', + '\u2184': 'c', + '\u24D3': 'd', + '\uFF44': 'd', + '\u1E0B': 'd', + '\u010F': 'd', + '\u1E0D': 'd', + '\u1E11': 'd', + '\u1E13': 'd', + '\u1E0F': 'd', + '\u0111': 'd', + '\u018C': 'd', + '\u0256': 'd', + '\u0257': 'd', + '\uA77A': 'd', + '\u01F3': 'dz', + '\u01C6': 'dz', + '\u24D4': 'e', + '\uFF45': 'e', + '\u00E8': 'e', + '\u00E9': 'e', + '\u00EA': 'e', + '\u1EC1': 'e', + '\u1EBF': 'e', + '\u1EC5': 'e', + '\u1EC3': 'e', + '\u1EBD': 'e', + '\u0113': 'e', + '\u1E15': 'e', + '\u1E17': 'e', + '\u0115': 'e', + '\u0117': 'e', + '\u00EB': 'e', + '\u1EBB': 'e', + '\u011B': 'e', + '\u0205': 'e', + '\u0207': 'e', + '\u1EB9': 'e', + '\u1EC7': 'e', + '\u0229': 'e', + '\u1E1D': 'e', + '\u0119': 'e', + '\u1E19': 'e', + '\u1E1B': 'e', + '\u0247': 'e', + '\u025B': 'e', + '\u01DD': 'e', + '\u24D5': 'f', + '\uFF46': 'f', + '\u1E1F': 'f', + '\u0192': 'f', + '\uA77C': 'f', + '\u24D6': 'g', + '\uFF47': 'g', + '\u01F5': 'g', + '\u011D': 'g', + '\u1E21': 'g', + '\u011F': 'g', + '\u0121': 'g', + '\u01E7': 'g', + '\u0123': 'g', + '\u01E5': 'g', + '\u0260': 'g', + '\uA7A1': 'g', + '\u1D79': 'g', + '\uA77F': 'g', + '\u24D7': 'h', + '\uFF48': 'h', + '\u0125': 'h', + '\u1E23': 'h', + '\u1E27': 'h', + '\u021F': 'h', + '\u1E25': 'h', + '\u1E29': 'h', + '\u1E2B': 'h', + '\u1E96': 'h', + '\u0127': 'h', + '\u2C68': 'h', + '\u2C76': 'h', + '\u0265': 'h', + '\u0195': 'hv', + '\u24D8': 'i', + '\uFF49': 'i', + '\u00EC': 'i', + '\u00ED': 'i', + '\u00EE': 'i', + '\u0129': 'i', + '\u012B': 'i', + '\u012D': 'i', + '\u00EF': 'i', + '\u1E2F': 'i', + '\u1EC9': 'i', + '\u01D0': 'i', + '\u0209': 'i', + '\u020B': 'i', + '\u1ECB': 'i', + '\u012F': 'i', + '\u1E2D': 'i', + '\u0268': 'i', + '\u0131': 'i', + '\u24D9': 'j', + '\uFF4A': 'j', + '\u0135': 'j', + '\u01F0': 'j', + '\u0249': 'j', + '\u24DA': 'k', + '\uFF4B': 'k', + '\u1E31': 'k', + '\u01E9': 'k', + '\u1E33': 'k', + '\u0137': 'k', + '\u1E35': 'k', + '\u0199': 'k', + '\u2C6A': 'k', + '\uA741': 'k', + '\uA743': 'k', + '\uA745': 'k', + '\uA7A3': 'k', + '\u24DB': 'l', + '\uFF4C': 'l', + '\u0140': 'l', + '\u013A': 'l', + '\u013E': 'l', + '\u1E37': 'l', + '\u1E39': 'l', + '\u013C': 'l', + '\u1E3D': 'l', + '\u1E3B': 'l', + '\u017F': 'l', + '\u0142': 'l', + '\u019A': 'l', + '\u026B': 'l', + '\u2C61': 'l', + '\uA749': 'l', + '\uA781': 'l', + '\uA747': 'l', + '\u01C9': 'lj', + '\u24DC': 'm', + '\uFF4D': 'm', + '\u1E3F': 'm', + '\u1E41': 'm', + '\u1E43': 'm', + '\u0271': 'm', + '\u026F': 'm', + '\u24DD': 'n', + '\uFF4E': 'n', + '\u01F9': 'n', + '\u0144': 'n', + '\u00F1': 'n', + '\u1E45': 'n', + '\u0148': 'n', + '\u1E47': 'n', + '\u0146': 'n', + '\u1E4B': 'n', + '\u1E49': 'n', + '\u019E': 'n', + '\u0272': 'n', + '\u0149': 'n', + '\uA791': 'n', + '\uA7A5': 'n', + '\u01CC': 'nj', + '\u24DE': 'o', + '\uFF4F': 'o', + '\u00F2': 'o', + '\u00F3': 'o', + '\u00F4': 'o', + '\u1ED3': 'o', + '\u1ED1': 'o', + '\u1ED7': 'o', + '\u1ED5': 'o', + '\u00F5': 'o', + '\u1E4D': 'o', + '\u022D': 'o', + '\u1E4F': 'o', + '\u014D': 'o', + '\u1E51': 'o', + '\u1E53': 'o', + '\u014F': 'o', + '\u022F': 'o', + '\u0231': 'o', + '\u00F6': 'o', + '\u022B': 'o', + '\u1ECF': 'o', + '\u0151': 'o', + '\u01D2': 'o', + '\u020D': 'o', + '\u020F': 'o', + '\u01A1': 'o', + '\u1EDD': 'o', + '\u1EDB': 'o', + '\u1EE1': 'o', + '\u1EDF': 'o', + '\u1EE3': 'o', + '\u1ECD': 'o', + '\u1ED9': 'o', + '\u01EB': 'o', + '\u01ED': 'o', + '\u00F8': 'o', + '\u01FF': 'o', + '\u0254': 'o', + '\uA74B': 'o', + '\uA74D': 'o', + '\u0275': 'o', + '\u01A3': 'oi', + '\u0223': 'ou', + '\uA74F': 'oo', + '\u24DF': 'p', + '\uFF50': 'p', + '\u1E55': 'p', + '\u1E57': 'p', + '\u01A5': 'p', + '\u1D7D': 'p', + '\uA751': 'p', + '\uA753': 'p', + '\uA755': 'p', + '\u24E0': 'q', + '\uFF51': 'q', + '\u024B': 'q', + '\uA757': 'q', + '\uA759': 'q', + '\u24E1': 'r', + '\uFF52': 'r', + '\u0155': 'r', + '\u1E59': 'r', + '\u0159': 'r', + '\u0211': 'r', + '\u0213': 'r', + '\u1E5B': 'r', + '\u1E5D': 'r', + '\u0157': 'r', + '\u1E5F': 'r', + '\u024D': 'r', + '\u027D': 'r', + '\uA75B': 'r', + '\uA7A7': 'r', + '\uA783': 'r', + '\u24E2': 's', + '\uFF53': 's', + '\u00DF': 's', + '\u015B': 's', + '\u1E65': 's', + '\u015D': 's', + '\u1E61': 's', + '\u0161': 's', + '\u1E67': 's', + '\u1E63': 's', + '\u1E69': 's', + '\u0219': 's', + '\u015F': 's', + '\u023F': 's', + '\uA7A9': 's', + '\uA785': 's', + '\u1E9B': 's', + '\u24E3': 't', + '\uFF54': 't', + '\u1E6B': 't', + '\u1E97': 't', + '\u0165': 't', + '\u1E6D': 't', + '\u021B': 't', + '\u0163': 't', + '\u1E71': 't', + '\u1E6F': 't', + '\u0167': 't', + '\u01AD': 't', + '\u0288': 't', + '\u2C66': 't', + '\uA787': 't', + '\uA729': 'tz', + '\u24E4': 'u', + '\uFF55': 'u', + '\u00F9': 'u', + '\u00FA': 'u', + '\u00FB': 'u', + '\u0169': 'u', + '\u1E79': 'u', + '\u016B': 'u', + '\u1E7B': 'u', + '\u016D': 'u', + '\u00FC': 'u', + '\u01DC': 'u', + '\u01D8': 'u', + '\u01D6': 'u', + '\u01DA': 'u', + '\u1EE7': 'u', + '\u016F': 'u', + '\u0171': 'u', + '\u01D4': 'u', + '\u0215': 'u', + '\u0217': 'u', + '\u01B0': 'u', + '\u1EEB': 'u', + '\u1EE9': 'u', + '\u1EEF': 'u', + '\u1EED': 'u', + '\u1EF1': 'u', + '\u1EE5': 'u', + '\u1E73': 'u', + '\u0173': 'u', + '\u1E77': 'u', + '\u1E75': 'u', + '\u0289': 'u', + '\u24E5': 'v', + '\uFF56': 'v', + '\u1E7D': 'v', + '\u1E7F': 'v', + '\u028B': 'v', + '\uA75F': 'v', + '\u028C': 'v', + '\uA761': 'vy', + '\u24E6': 'w', + '\uFF57': 'w', + '\u1E81': 'w', + '\u1E83': 'w', + '\u0175': 'w', + '\u1E87': 'w', + '\u1E85': 'w', + '\u1E98': 'w', + '\u1E89': 'w', + '\u2C73': 'w', + '\u24E7': 'x', + '\uFF58': 'x', + '\u1E8B': 'x', + '\u1E8D': 'x', + '\u24E8': 'y', + '\uFF59': 'y', + '\u1EF3': 'y', + '\u00FD': 'y', + '\u0177': 'y', + '\u1EF9': 'y', + '\u0233': 'y', + '\u1E8F': 'y', + '\u00FF': 'y', + '\u1EF7': 'y', + '\u1E99': 'y', + '\u1EF5': 'y', + '\u01B4': 'y', + '\u024F': 'y', + '\u1EFF': 'y', + '\u24E9': 'z', + '\uFF5A': 'z', + '\u017A': 'z', + '\u1E91': 'z', + '\u017C': 'z', + '\u017E': 'z', + '\u1E93': 'z', + '\u1E95': 'z', + '\u01B6': 'z', + '\u0225': 'z', + '\u0240': 'z', + '\u2C6C': 'z', + '\uA763': 'z', + '\u0386': '\u0391', + '\u0388': '\u0395', + '\u0389': '\u0397', + '\u038A': '\u0399', + '\u03AA': '\u0399', + '\u038C': '\u039F', + '\u038E': '\u03A5', + '\u03AB': '\u03A5', + '\u038F': '\u03A9', + '\u03AC': '\u03B1', + '\u03AD': '\u03B5', + '\u03AE': '\u03B7', + '\u03AF': '\u03B9', + '\u03CA': '\u03B9', + '\u0390': '\u03B9', + '\u03CC': '\u03BF', + '\u03CD': '\u03C5', + '\u03CB': '\u03C5', + '\u03B0': '\u03C5', + '\u03C9': '\u03C9', + '\u03C2': '\u03C3' + }; + + return diacritics; +}); + +S2.define('select2/data/base',[ + '../utils' +], function (Utils) { + function BaseAdapter ($element, options) { + BaseAdapter.__super__.constructor.call(this); + } + + Utils.Extend(BaseAdapter, Utils.Observable); + + BaseAdapter.prototype.current = function (callback) { + throw new Error('The `current` method must be defined in child classes.'); + }; + + BaseAdapter.prototype.query = function (params, callback) { + throw new Error('The `query` method must be defined in child classes.'); + }; + + BaseAdapter.prototype.bind = function (container, $container) { + // Can be implemented in subclasses + }; + + BaseAdapter.prototype.destroy = function () { + // Can be implemented in subclasses + }; + + BaseAdapter.prototype.generateResultId = function (container, data) { + var id = container.id + '-result-'; + + id += Utils.generateChars(4); + + if (data.id != null) { + id += '-' + data.id.toString(); + } else { + id += '-' + Utils.generateChars(4); + } + return id; + }; + + return BaseAdapter; +}); + +S2.define('select2/data/select',[ + './base', + '../utils', + 'jquery' +], function (BaseAdapter, Utils, $) { + function SelectAdapter ($element, options) { + this.$element = $element; + this.options = options; + + SelectAdapter.__super__.constructor.call(this); + } + + Utils.Extend(SelectAdapter, BaseAdapter); + + SelectAdapter.prototype.current = function (callback) { + var data = []; + var self = this; + + this.$element.find(':selected').each(function () { + var $option = $(this); + + var option = self.item($option); + + data.push(option); + }); + + callback(data); + }; + + SelectAdapter.prototype.select = function (data) { + var self = this; + + data.selected = true; + + // If data.element is a DOM node, use it instead + if ($(data.element).is('option')) { + data.element.selected = true; + + this.$element.trigger('change'); + + return; + } + + if (this.$element.prop('multiple')) { + this.current(function (currentData) { + var val = []; + + data = [data]; + data.push.apply(data, currentData); + + for (var d = 0; d < data.length; d++) { + var id = data[d].id; + + if ($.inArray(id, val) === -1) { + val.push(id); + } + } + + self.$element.val(val); + self.$element.trigger('change'); + }); + } else { + var val = data.id; + + this.$element.val(val); + this.$element.trigger('change'); + } + }; + + SelectAdapter.prototype.unselect = function (data) { + var self = this; + + if (!this.$element.prop('multiple')) { + return; + } + + data.selected = false; + + if ($(data.element).is('option')) { + data.element.selected = false; + + this.$element.trigger('change'); + + return; + } + + this.current(function (currentData) { + var val = []; + + for (var d = 0; d < currentData.length; d++) { + var id = currentData[d].id; + + if (id !== data.id && $.inArray(id, val) === -1) { + val.push(id); + } + } + + self.$element.val(val); + + self.$element.trigger('change'); + }); + }; + + SelectAdapter.prototype.bind = function (container, $container) { + var self = this; + + this.container = container; + + container.on('select', function (params) { + self.select(params.data); + }); + + container.on('unselect', function (params) { + self.unselect(params.data); + }); + }; + + SelectAdapter.prototype.destroy = function () { + // Remove anything added to child elements + this.$element.find('*').each(function () { + // Remove any custom data set by Select2 + $.removeData(this, 'data'); + }); + }; + + SelectAdapter.prototype.query = function (params, callback) { + var data = []; + var self = this; + + var $options = this.$element.children(); + + $options.each(function () { + var $option = $(this); + + if (!$option.is('option') && !$option.is('optgroup')) { + return; + } + + var option = self.item($option); + + var matches = self.matches(params, option); + + if (matches !== null) { + data.push(matches); + } + }); + + callback({ + results: data + }); + }; + + SelectAdapter.prototype.addOptions = function ($options) { + Utils.appendMany(this.$element, $options); + }; + + SelectAdapter.prototype.option = function (data) { + var option; + + if (data.children) { + option = document.createElement('optgroup'); + option.label = data.text; + } else { + option = document.createElement('option'); + + if (option.textContent !== undefined) { + option.textContent = data.text; + } else { + option.innerText = data.text; + } + } + + if (data.id) { + option.value = data.id; + } + + if (data.disabled) { + option.disabled = true; + } + + if (data.selected) { + option.selected = true; + } + + if (data.title) { + option.title = data.title; + } + + var $option = $(option); + + var normalizedData = this._normalizeItem(data); + normalizedData.element = option; + + // Override the option's data with the combined data + $.data(option, 'data', normalizedData); + + return $option; + }; + + SelectAdapter.prototype.item = function ($option) { + var data = {}; + + data = $.data($option[0], 'data'); + + if (data != null) { + return data; + } + + if ($option.is('option')) { + data = { + id: $option.val(), + text: $option.text(), + disabled: $option.prop('disabled'), + selected: $option.prop('selected'), + title: $option.prop('title') + }; + } else if ($option.is('optgroup')) { + data = { + text: $option.prop('label'), + children: [], + title: $option.prop('title') + }; + + var $children = $option.children('option'); + var children = []; + + for (var c = 0; c < $children.length; c++) { + var $child = $($children[c]); + + var child = this.item($child); + + children.push(child); + } + + data.children = children; + } + + data = this._normalizeItem(data); + data.element = $option[0]; + + $.data($option[0], 'data', data); + + return data; + }; + + SelectAdapter.prototype._normalizeItem = function (item) { + if (!$.isPlainObject(item)) { + item = { + id: item, + text: item + }; + } + + item = $.extend({}, { + text: '' + }, item); + + var defaults = { + selected: false, + disabled: false + }; + + if (item.id != null) { + item.id = item.id.toString(); + } + + if (item.text != null) { + item.text = item.text.toString(); + } + + if (item._resultId == null && item.id && this.container != null) { + item._resultId = this.generateResultId(this.container, item); + } + + return $.extend({}, defaults, item); + }; + + SelectAdapter.prototype.matches = function (params, data) { + var matcher = this.options.get('matcher'); + + return matcher(params, data); + }; + + return SelectAdapter; +}); + +S2.define('select2/data/array',[ + './select', + '../utils', + 'jquery' +], function (SelectAdapter, Utils, $) { + function ArrayAdapter ($element, options) { + var data = options.get('data') || []; + + ArrayAdapter.__super__.constructor.call(this, $element, options); + + this.addOptions(this.convertToOptions(data)); + } + + Utils.Extend(ArrayAdapter, SelectAdapter); + + ArrayAdapter.prototype.select = function (data) { + var $option = this.$element.find('option').filter(function (i, elm) { + return elm.value == data.id.toString(); + }); + + if ($option.length === 0) { + $option = this.option(data); + + this.addOptions($option); + } + + ArrayAdapter.__super__.select.call(this, data); + }; + + ArrayAdapter.prototype.convertToOptions = function (data) { + var self = this; + + var $existing = this.$element.find('option'); + var existingIds = $existing.map(function () { + return self.item($(this)).id; + }).get(); + + var $options = []; + + // Filter out all items except for the one passed in the argument + function onlyItem (item) { + return function () { + return $(this).val() == item.id; + }; + } + + for (var d = 0; d < data.length; d++) { + var item = this._normalizeItem(data[d]); + + // Skip items which were pre-loaded, only merge the data + if ($.inArray(item.id, existingIds) >= 0) { + var $existingOption = $existing.filter(onlyItem(item)); + + var existingData = this.item($existingOption); + var newData = $.extend(true, {}, item, existingData); + + var $newOption = this.option(newData); + + $existingOption.replaceWith($newOption); + + continue; + } + + var $option = this.option(item); + + if (item.children) { + var $children = this.convertToOptions(item.children); + + Utils.appendMany($option, $children); + } + + $options.push($option); + } + + return $options; + }; + + return ArrayAdapter; +}); + +S2.define('select2/data/ajax',[ + './array', + '../utils', + 'jquery' +], function (ArrayAdapter, Utils, $) { + function AjaxAdapter ($element, options) { + this.ajaxOptions = this._applyDefaults(options.get('ajax')); + + if (this.ajaxOptions.processResults != null) { + this.processResults = this.ajaxOptions.processResults; + } + + AjaxAdapter.__super__.constructor.call(this, $element, options); + } + + Utils.Extend(AjaxAdapter, ArrayAdapter); + + AjaxAdapter.prototype._applyDefaults = function (options) { + var defaults = { + data: function (params) { + return $.extend({}, params, { + q: params.term + }); + }, + transport: function (params, success, failure) { + var $request = $.ajax(params); + + $request.then(success); + $request.fail(failure); + + return $request; + } + }; + + return $.extend({}, defaults, options, true); + }; + + AjaxAdapter.prototype.processResults = function (results) { + return results; + }; + + AjaxAdapter.prototype.query = function (params, callback) { + var matches = []; + var self = this; + + if (this._request != null) { + // JSONP requests cannot always be aborted + if ($.isFunction(this._request.abort)) { + this._request.abort(); + } + + this._request = null; + } + + var options = $.extend({ + type: 'GET' + }, this.ajaxOptions); + + if (typeof options.url === 'function') { + options.url = options.url.call(this.$element, params); + } + + if (typeof options.data === 'function') { + options.data = options.data.call(this.$element, params); + } + + function request () { + var $request = options.transport(options, function (data) { + var results = self.processResults(data, params); + + if (self.options.get('debug') && window.console && console.error) { + // Check to make sure that the response included a `results` key. + if (!results || !results.results || !$.isArray(results.results)) { + console.error( + 'Select2: The AJAX results did not return an array in the ' + + '`results` key of the response.' + ); + } + } + + callback(results); + }, function () { + // Attempt to detect if a request was aborted + // Only works if the transport exposes a status property + if ($request.status && $request.status === '0') { + return; + } + + self.trigger('results:message', { + message: 'errorLoading' + }); + }); + + self._request = $request; + } + + if (this.ajaxOptions.delay && params.term != null) { + if (this._queryTimeout) { + window.clearTimeout(this._queryTimeout); + } + + this._queryTimeout = window.setTimeout(request, this.ajaxOptions.delay); + } else { + request(); + } + }; + + return AjaxAdapter; +}); + +S2.define('select2/data/tags',[ + 'jquery' +], function ($) { + function Tags (decorated, $element, options) { + var tags = options.get('tags'); + + var createTag = options.get('createTag'); + + if (createTag !== undefined) { + this.createTag = createTag; + } + + var insertTag = options.get('insertTag'); + + if (insertTag !== undefined) { + this.insertTag = insertTag; + } + + decorated.call(this, $element, options); + + if ($.isArray(tags)) { + for (var t = 0; t < tags.length; t++) { + var tag = tags[t]; + var item = this._normalizeItem(tag); + + var $option = this.option(item); + + this.$element.append($option); + } + } + } + + Tags.prototype.query = function (decorated, params, callback) { + var self = this; + + this._removeOldTags(); + + if (params.term == null || params.page != null) { + decorated.call(this, params, callback); + return; + } + + function wrapper (obj, child) { + var data = obj.results; + + for (var i = 0; i < data.length; i++) { + var option = data[i]; + + var checkChildren = ( + option.children != null && + !wrapper({ + results: option.children + }, true) + ); + + var checkText = option.text === params.term; + + if (checkText || checkChildren) { + if (child) { + return false; + } + + obj.data = data; + callback(obj); + + return; + } + } + + if (child) { + return true; + } + + var tag = self.createTag(params); + + if (tag != null) { + var $option = self.option(tag); + $option.attr('data-select2-tag', true); + + self.addOptions([$option]); + + self.insertTag(data, tag); + } + + obj.results = data; + + callback(obj); + } + + decorated.call(this, params, wrapper); + }; + + Tags.prototype.createTag = function (decorated, params) { + var term = $.trim(params.term); + + if (term === '') { + return null; + } + + return { + id: term, + text: term + }; + }; + + Tags.prototype.insertTag = function (_, data, tag) { + data.unshift(tag); + }; + + Tags.prototype._removeOldTags = function (_) { + var tag = this._lastTag; + + var $options = this.$element.find('option[data-select2-tag]'); + + $options.each(function () { + if (this.selected) { + return; + } + + $(this).remove(); + }); + }; + + return Tags; +}); + +S2.define('select2/data/tokenizer',[ + 'jquery' +], function ($) { + function Tokenizer (decorated, $element, options) { + var tokenizer = options.get('tokenizer'); + + if (tokenizer !== undefined) { + this.tokenizer = tokenizer; + } + + decorated.call(this, $element, options); + } + + Tokenizer.prototype.bind = function (decorated, container, $container) { + decorated.call(this, container, $container); + + this.$search = container.dropdown.$search || container.selection.$search || + $container.find('.select2-search__field'); + }; + + Tokenizer.prototype.query = function (decorated, params, callback) { + var self = this; + + function createAndSelect (data) { + // Normalize the data object so we can use it for checks + var item = self._normalizeItem(data); + + // Check if the data object already exists as a tag + // Select it if it doesn't + var $existingOptions = self.$element.find('option').filter(function () { + return $(this).val() === item.id; + }); + + // If an existing option wasn't found for it, create the option + if (!$existingOptions.length) { + var $option = self.option(item); + $option.attr('data-select2-tag', true); + + self._removeOldTags(); + self.addOptions([$option]); + } + + // Select the item, now that we know there is an option for it + select(item); + } + + function select (data) { + self.trigger('select', { + data: data + }); + } + + params.term = params.term || ''; + + var tokenData = this.tokenizer(params, this.options, createAndSelect); + + if (tokenData.term !== params.term) { + // Replace the search term if we have the search box + if (this.$search.length) { + this.$search.val(tokenData.term); + this.$search.focus(); + } + + params.term = tokenData.term; + } + + decorated.call(this, params, callback); + }; + + Tokenizer.prototype.tokenizer = function (_, params, options, callback) { + var separators = options.get('tokenSeparators') || []; + var term = params.term; + var i = 0; + + var createTag = this.createTag || function (params) { + return { + id: params.term, + text: params.term + }; + }; + + while (i < term.length) { + var termChar = term[i]; + + if ($.inArray(termChar, separators) === -1) { + i++; + + continue; + } + + var part = term.substr(0, i); + var partParams = $.extend({}, params, { + term: part + }); + + var data = createTag(partParams); + + if (data == null) { + i++; + continue; + } + + callback(data); + + // Reset the term to not include the tokenized portion + term = term.substr(i + 1) || ''; + i = 0; + } + + return { + term: term + }; + }; + + return Tokenizer; +}); + +S2.define('select2/data/minimumInputLength',[ + +], function () { + function MinimumInputLength (decorated, $e, options) { + this.minimumInputLength = options.get('minimumInputLength'); + + decorated.call(this, $e, options); + } + + MinimumInputLength.prototype.query = function (decorated, params, callback) { + params.term = params.term || ''; + + if (params.term.length < this.minimumInputLength) { + this.trigger('results:message', { + message: 'inputTooShort', + args: { + minimum: this.minimumInputLength, + input: params.term, + params: params + } + }); + + return; + } + + decorated.call(this, params, callback); + }; + + return MinimumInputLength; +}); + +S2.define('select2/data/maximumInputLength',[ + +], function () { + function MaximumInputLength (decorated, $e, options) { + this.maximumInputLength = options.get('maximumInputLength'); + + decorated.call(this, $e, options); + } + + MaximumInputLength.prototype.query = function (decorated, params, callback) { + params.term = params.term || ''; + + if (this.maximumInputLength > 0 && + params.term.length > this.maximumInputLength) { + this.trigger('results:message', { + message: 'inputTooLong', + args: { + maximum: this.maximumInputLength, + input: params.term, + params: params + } + }); + + return; + } + + decorated.call(this, params, callback); + }; + + return MaximumInputLength; +}); + +S2.define('select2/data/maximumSelectionLength',[ + +], function (){ + function MaximumSelectionLength (decorated, $e, options) { + this.maximumSelectionLength = options.get('maximumSelectionLength'); + + decorated.call(this, $e, options); + } + + MaximumSelectionLength.prototype.query = + function (decorated, params, callback) { + var self = this; + + this.current(function (currentData) { + var count = currentData != null ? currentData.length : 0; + if (self.maximumSelectionLength > 0 && + count >= self.maximumSelectionLength) { + self.trigger('results:message', { + message: 'maximumSelected', + args: { + maximum: self.maximumSelectionLength + } + }); + return; + } + decorated.call(self, params, callback); + }); + }; + + return MaximumSelectionLength; +}); + +S2.define('select2/dropdown',[ + 'jquery', + './utils' +], function ($, Utils) { + function Dropdown ($element, options) { + this.$element = $element; + this.options = options; + + Dropdown.__super__.constructor.call(this); + } + + Utils.Extend(Dropdown, Utils.Observable); + + Dropdown.prototype.render = function () { + var $dropdown = $( + '' + + '' + + '' + ); + + $dropdown.attr('dir', this.options.get('dir')); + + this.$dropdown = $dropdown; + + return $dropdown; + }; + + Dropdown.prototype.bind = function () { + // Should be implemented in subclasses + }; + + Dropdown.prototype.position = function ($dropdown, $container) { + // Should be implmented in subclasses + }; + + Dropdown.prototype.destroy = function () { + // Remove the dropdown from the DOM + this.$dropdown.remove(); + }; + + return Dropdown; +}); + +S2.define('select2/dropdown/search',[ + 'jquery', + '../utils' +], function ($, Utils) { + function Search () { } + + Search.prototype.render = function (decorated) { + var $rendered = decorated.call(this); + + var $search = $( + '' + + '' + + '' + ); + + this.$searchContainer = $search; + this.$search = $search.find('input'); + + $rendered.prepend($search); + + return $rendered; + }; + + Search.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + this.$search.on('keydown', function (evt) { + self.trigger('keypress', evt); + + self._keyUpPrevented = evt.isDefaultPrevented(); + }); + + // Workaround for browsers which do not support the `input` event + // This will prevent double-triggering of events for browsers which support + // both the `keyup` and `input` events. + this.$search.on('input', function (evt) { + // Unbind the duplicated `keyup` event + $(this).off('keyup'); + }); + + this.$search.on('keyup input', function (evt) { + self.handleSearch(evt); + }); + + container.on('open', function () { + self.$search.attr('tabindex', 0); + + self.$search.focus(); + + window.setTimeout(function () { + self.$search.focus(); + }, 0); + }); + + container.on('close', function () { + self.$search.attr('tabindex', -1); + + self.$search.val(''); + }); + + container.on('focus', function () { + if (container.isOpen()) { + self.$search.focus(); + } + }); + + container.on('results:all', function (params) { + if (params.query.term == null || params.query.term === '') { + var showSearch = self.showSearch(params); + + if (showSearch) { + self.$searchContainer.removeClass('select2-search--hide'); + } else { + self.$searchContainer.addClass('select2-search--hide'); + } + } + }); + }; + + Search.prototype.handleSearch = function (evt) { + if (!this._keyUpPrevented) { + var input = this.$search.val(); + + this.trigger('query', { + term: input + }); + } + + this._keyUpPrevented = false; + }; + + Search.prototype.showSearch = function (_, params) { + return true; + }; + + return Search; +}); + +S2.define('select2/dropdown/hidePlaceholder',[ + +], function () { + function HidePlaceholder (decorated, $element, options, dataAdapter) { + this.placeholder = this.normalizePlaceholder(options.get('placeholder')); + + decorated.call(this, $element, options, dataAdapter); + } + + HidePlaceholder.prototype.append = function (decorated, data) { + data.results = this.removePlaceholder(data.results); + + decorated.call(this, data); + }; + + HidePlaceholder.prototype.normalizePlaceholder = function (_, placeholder) { + if (typeof placeholder === 'string') { + placeholder = { + id: '', + text: placeholder + }; + } + + return placeholder; + }; + + HidePlaceholder.prototype.removePlaceholder = function (_, data) { + var modifiedData = data.slice(0); + + for (var d = data.length - 1; d >= 0; d--) { + var item = data[d]; + + if (this.placeholder.id === item.id) { + modifiedData.splice(d, 1); + } + } + + return modifiedData; + }; + + return HidePlaceholder; +}); + +S2.define('select2/dropdown/infiniteScroll',[ + 'jquery' +], function ($) { + function InfiniteScroll (decorated, $element, options, dataAdapter) { + this.lastParams = {}; + + decorated.call(this, $element, options, dataAdapter); + + this.$loadingMore = this.createLoadingMore(); + this.loading = false; + } + + InfiniteScroll.prototype.append = function (decorated, data) { + this.$loadingMore.remove(); + this.loading = false; + + decorated.call(this, data); + + if (this.showLoadingMore(data)) { + this.$results.append(this.$loadingMore); + } + }; + + InfiniteScroll.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('query', function (params) { + self.lastParams = params; + self.loading = true; + }); + + container.on('query:append', function (params) { + self.lastParams = params; + self.loading = true; + }); + + this.$results.on('scroll', function () { + var isLoadMoreVisible = $.contains( + document.documentElement, + self.$loadingMore[0] + ); + + if (self.loading || !isLoadMoreVisible) { + return; + } + + var currentOffset = self.$results.offset().top + + self.$results.outerHeight(false); + var loadingMoreOffset = self.$loadingMore.offset().top + + self.$loadingMore.outerHeight(false); + + if (currentOffset + 50 >= loadingMoreOffset) { + self.loadMore(); + } + }); + }; + + InfiniteScroll.prototype.loadMore = function () { + this.loading = true; + + var params = $.extend({}, {page: 1}, this.lastParams); + + params.page++; + + this.trigger('query:append', params); + }; + + InfiniteScroll.prototype.showLoadingMore = function (_, data) { + return data.pagination && data.pagination.more; + }; + + InfiniteScroll.prototype.createLoadingMore = function () { + var $option = $( + '
        • ' + ); + + var message = this.options.get('translations').get('loadingMore'); + + $option.html(message(this.lastParams)); + + return $option; + }; + + return InfiniteScroll; +}); + +S2.define('select2/dropdown/attachBody',[ + 'jquery', + '../utils' +], function ($, Utils) { + function AttachBody (decorated, $element, options) { + this.$dropdownParent = options.get('dropdownParent') || $(document.body); + + decorated.call(this, $element, options); + } + + AttachBody.prototype.bind = function (decorated, container, $container) { + var self = this; + + var setupResultsEvents = false; + + decorated.call(this, container, $container); + + container.on('open', function () { + self._showDropdown(); + self._attachPositioningHandler(container); + + if (!setupResultsEvents) { + setupResultsEvents = true; + + container.on('results:all', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + container.on('results:append', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + } + }); + + container.on('close', function () { + self._hideDropdown(); + self._detachPositioningHandler(container); + }); + + this.$dropdownContainer.on('mousedown', function (evt) { + evt.stopPropagation(); + }); + }; + + AttachBody.prototype.destroy = function (decorated) { + decorated.call(this); + + this.$dropdownContainer.remove(); + }; + + AttachBody.prototype.position = function (decorated, $dropdown, $container) { + // Clone all of the container classes + $dropdown.attr('class', $container.attr('class')); + + $dropdown.removeClass('select2'); + $dropdown.addClass('select2-container--open'); + + $dropdown.css({ + position: 'absolute', + top: -999999 + }); + + this.$container = $container; + }; + + AttachBody.prototype.render = function (decorated) { + var $container = $(''); + + var $dropdown = decorated.call(this); + $container.append($dropdown); + + this.$dropdownContainer = $container; + + return $container; + }; + + AttachBody.prototype._hideDropdown = function (decorated) { + this.$dropdownContainer.detach(); + }; + + AttachBody.prototype._attachPositioningHandler = + function (decorated, container) { + var self = this; + + var scrollEvent = 'scroll.select2.' + container.id; + var resizeEvent = 'resize.select2.' + container.id; + var orientationEvent = 'orientationchange.select2.' + container.id; + + var $watchers = this.$container.parents().filter(Utils.hasScroll); + $watchers.each(function () { + $(this).data('select2-scroll-position', { + x: $(this).scrollLeft(), + y: $(this).scrollTop() + }); + }); + + $watchers.on(scrollEvent, function (ev) { + var position = $(this).data('select2-scroll-position'); + $(this).scrollTop(position.y); + }); + + $(window).on(scrollEvent + ' ' + resizeEvent + ' ' + orientationEvent, + function (e) { + self._positionDropdown(); + self._resizeDropdown(); + }); + }; + + AttachBody.prototype._detachPositioningHandler = + function (decorated, container) { + var scrollEvent = 'scroll.select2.' + container.id; + var resizeEvent = 'resize.select2.' + container.id; + var orientationEvent = 'orientationchange.select2.' + container.id; + + var $watchers = this.$container.parents().filter(Utils.hasScroll); + $watchers.off(scrollEvent); + + $(window).off(scrollEvent + ' ' + resizeEvent + ' ' + orientationEvent); + }; + + AttachBody.prototype._positionDropdown = function () { + var $window = $(window); + + var isCurrentlyAbove = this.$dropdown.hasClass('select2-dropdown--above'); + var isCurrentlyBelow = this.$dropdown.hasClass('select2-dropdown--below'); + + var newDirection = null; + + var offset = this.$container.offset(); + + offset.bottom = offset.top + this.$container.outerHeight(false); + + var container = { + height: this.$container.outerHeight(false) + }; + + container.top = offset.top; + container.bottom = offset.top + container.height; + + var dropdown = { + height: this.$dropdown.outerHeight(false) + }; + + var viewport = { + top: $window.scrollTop(), + bottom: $window.scrollTop() + $window.height() + }; + + var enoughRoomAbove = viewport.top < (offset.top - dropdown.height); + var enoughRoomBelow = viewport.bottom > (offset.bottom + dropdown.height); + + var css = { + left: offset.left, + top: container.bottom + }; + + // Determine what the parent element is to use for calciulating the offset + var $offsetParent = this.$dropdownParent; + + // For statically positoned elements, we need to get the element + // that is determining the offset + if ($offsetParent.css('position') === 'static') { + $offsetParent = $offsetParent.offsetParent(); + } + + var parentOffset = $offsetParent.offset(); + + css.top -= parentOffset.top; + css.left -= parentOffset.left; + + if (!isCurrentlyAbove && !isCurrentlyBelow) { + newDirection = 'below'; + } + + if (!enoughRoomBelow && enoughRoomAbove && !isCurrentlyAbove) { + newDirection = 'above'; + } else if (!enoughRoomAbove && enoughRoomBelow && isCurrentlyAbove) { + newDirection = 'below'; + } + + if (newDirection == 'above' || + (isCurrentlyAbove && newDirection !== 'below')) { + css.top = container.top - parentOffset.top - dropdown.height; + } + + if (newDirection != null) { + this.$dropdown + .removeClass('select2-dropdown--below select2-dropdown--above') + .addClass('select2-dropdown--' + newDirection); + this.$container + .removeClass('select2-container--below select2-container--above') + .addClass('select2-container--' + newDirection); + } + + this.$dropdownContainer.css(css); + }; + + AttachBody.prototype._resizeDropdown = function () { + var css = { + width: this.$container.outerWidth(false) + 'px' + }; + + if (this.options.get('dropdownAutoWidth')) { + css.minWidth = css.width; + css.position = 'relative'; + css.width = 'auto'; + } + + this.$dropdown.css(css); + }; + + AttachBody.prototype._showDropdown = function (decorated) { + this.$dropdownContainer.appendTo(this.$dropdownParent); + + this._positionDropdown(); + this._resizeDropdown(); + }; + + return AttachBody; +}); + +S2.define('select2/dropdown/minimumResultsForSearch',[ + +], function () { + function countResults (data) { + var count = 0; + + for (var d = 0; d < data.length; d++) { + var item = data[d]; + + if (item.children) { + count += countResults(item.children); + } else { + count++; + } + } + + return count; + } + + function MinimumResultsForSearch (decorated, $element, options, dataAdapter) { + this.minimumResultsForSearch = options.get('minimumResultsForSearch'); + + if (this.minimumResultsForSearch < 0) { + this.minimumResultsForSearch = Infinity; + } + + decorated.call(this, $element, options, dataAdapter); + } + + MinimumResultsForSearch.prototype.showSearch = function (decorated, params) { + if (countResults(params.data.results) < this.minimumResultsForSearch) { + return false; + } + + return decorated.call(this, params); + }; + + return MinimumResultsForSearch; +}); + +S2.define('select2/dropdown/selectOnClose',[ + +], function () { + function SelectOnClose () { } + + SelectOnClose.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('close', function (params) { + self._handleSelectOnClose(params); + }); + }; + + SelectOnClose.prototype._handleSelectOnClose = function (_, params) { + if (params && params.originalSelect2Event != null) { + var event = params.originalSelect2Event; + + // Don't select an item if the close event was triggered from a select or + // unselect event + if (event._type === 'select' || event._type === 'unselect') { + return; + } + } + + var $highlightedResults = this.getHighlightedResults(); + + // Only select highlighted results + if ($highlightedResults.length < 1) { + return; + } + + var data = $highlightedResults.data('data'); + + // Don't re-select already selected resulte + if ( + (data.element != null && data.element.selected) || + (data.element == null && data.selected) + ) { + return; + } + + this.trigger('select', { + data: data + }); + }; + + return SelectOnClose; +}); + +S2.define('select2/dropdown/closeOnSelect',[ + +], function () { + function CloseOnSelect () { } + + CloseOnSelect.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('select', function (evt) { + self._selectTriggered(evt); + }); + + container.on('unselect', function (evt) { + self._selectTriggered(evt); + }); + }; + + CloseOnSelect.prototype._selectTriggered = function (_, evt) { + var originalEvent = evt.originalEvent; + + // Don't close if the control key is being held + if (originalEvent && originalEvent.ctrlKey) { + return; + } + + this.trigger('close', { + originalEvent: originalEvent, + originalSelect2Event: evt + }); + }; + + return CloseOnSelect; +}); + +S2.define('select2/i18n/en',[],function () { + // English + return { + errorLoading: function () { + return 'The results could not be loaded.'; + }, + inputTooLong: function (args) { + var overChars = args.input.length - args.maximum; + + var message = 'Please delete ' + overChars + ' character'; + + if (overChars != 1) { + message += 's'; + } + + return message; + }, + inputTooShort: function (args) { + var remainingChars = args.minimum - args.input.length; + + var message = 'Please enter ' + remainingChars + ' or more characters'; + + return message; + }, + loadingMore: function () { + return 'Loading more results…'; + }, + maximumSelected: function (args) { + var message = 'You can only select ' + args.maximum + ' item'; + + if (args.maximum != 1) { + message += 's'; + } + + return message; + }, + noResults: function () { + return 'No results found'; + }, + searching: function () { + return 'Searching…'; + } + }; +}); + +S2.define('select2/defaults',[ + 'jquery', + 'require', + + './results', + + './selection/single', + './selection/multiple', + './selection/placeholder', + './selection/allowClear', + './selection/search', + './selection/eventRelay', + + './utils', + './translation', + './diacritics', + + './data/select', + './data/array', + './data/ajax', + './data/tags', + './data/tokenizer', + './data/minimumInputLength', + './data/maximumInputLength', + './data/maximumSelectionLength', + + './dropdown', + './dropdown/search', + './dropdown/hidePlaceholder', + './dropdown/infiniteScroll', + './dropdown/attachBody', + './dropdown/minimumResultsForSearch', + './dropdown/selectOnClose', + './dropdown/closeOnSelect', + + './i18n/en' +], function ($, require, + + ResultsList, + + SingleSelection, MultipleSelection, Placeholder, AllowClear, + SelectionSearch, EventRelay, + + Utils, Translation, DIACRITICS, + + SelectData, ArrayData, AjaxData, Tags, Tokenizer, + MinimumInputLength, MaximumInputLength, MaximumSelectionLength, + + Dropdown, DropdownSearch, HidePlaceholder, InfiniteScroll, + AttachBody, MinimumResultsForSearch, SelectOnClose, CloseOnSelect, + + EnglishTranslation) { + function Defaults () { + this.reset(); + } + + Defaults.prototype.apply = function (options) { + options = $.extend(true, {}, this.defaults, options); + + if (options.dataAdapter == null) { + if (options.ajax != null) { + options.dataAdapter = AjaxData; + } else if (options.data != null) { + options.dataAdapter = ArrayData; + } else { + options.dataAdapter = SelectData; + } + + if (options.minimumInputLength > 0) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + MinimumInputLength + ); + } + + if (options.maximumInputLength > 0) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + MaximumInputLength + ); + } + + if (options.maximumSelectionLength > 0) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + MaximumSelectionLength + ); + } + + if (options.tags) { + options.dataAdapter = Utils.Decorate(options.dataAdapter, Tags); + } + + if (options.tokenSeparators != null || options.tokenizer != null) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + Tokenizer + ); + } + + if (options.query != null) { + var Query = require(options.amdBase + 'compat/query'); + + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + Query + ); + } + + if (options.initSelection != null) { + var InitSelection = require(options.amdBase + 'compat/initSelection'); + + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + InitSelection + ); + } + } + + if (options.resultsAdapter == null) { + options.resultsAdapter = ResultsList; + + if (options.ajax != null) { + options.resultsAdapter = Utils.Decorate( + options.resultsAdapter, + InfiniteScroll + ); + } + + if (options.placeholder != null) { + options.resultsAdapter = Utils.Decorate( + options.resultsAdapter, + HidePlaceholder + ); + } + + if (options.selectOnClose) { + options.resultsAdapter = Utils.Decorate( + options.resultsAdapter, + SelectOnClose + ); + } + } + + if (options.dropdownAdapter == null) { + if (options.multiple) { + options.dropdownAdapter = Dropdown; + } else { + var SearchableDropdown = Utils.Decorate(Dropdown, DropdownSearch); + + options.dropdownAdapter = SearchableDropdown; + } + + if (options.minimumResultsForSearch !== 0) { + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + MinimumResultsForSearch + ); + } + + if (options.closeOnSelect) { + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + CloseOnSelect + ); + } + + if ( + options.dropdownCssClass != null || + options.dropdownCss != null || + options.adaptDropdownCssClass != null + ) { + var DropdownCSS = require(options.amdBase + 'compat/dropdownCss'); + + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + DropdownCSS + ); + } + + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + AttachBody + ); + } + + if (options.selectionAdapter == null) { + if (options.multiple) { + options.selectionAdapter = MultipleSelection; + } else { + options.selectionAdapter = SingleSelection; + } + + // Add the placeholder mixin if a placeholder was specified + if (options.placeholder != null) { + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + Placeholder + ); + } + + if (options.allowClear) { + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + AllowClear + ); + } + + if (options.multiple) { + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + SelectionSearch + ); + } + + if ( + options.containerCssClass != null || + options.containerCss != null || + options.adaptContainerCssClass != null + ) { + var ContainerCSS = require(options.amdBase + 'compat/containerCss'); + + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + ContainerCSS + ); + } + + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + EventRelay + ); + } + + if (typeof options.language === 'string') { + // Check if the language is specified with a region + if (options.language.indexOf('-') > 0) { + // Extract the region information if it is included + var languageParts = options.language.split('-'); + var baseLanguage = languageParts[0]; + + options.language = [options.language, baseLanguage]; + } else { + options.language = [options.language]; + } + } + + if ($.isArray(options.language)) { + var languages = new Translation(); + options.language.push('en'); + + var languageNames = options.language; + + for (var l = 0; l < languageNames.length; l++) { + var name = languageNames[l]; + var language = {}; + + try { + // Try to load it with the original name + language = Translation.loadPath(name); + } catch (e) { + try { + // If we couldn't load it, check if it wasn't the full path + name = this.defaults.amdLanguageBase + name; + language = Translation.loadPath(name); + } catch (ex) { + // The translation could not be loaded at all. Sometimes this is + // because of a configuration problem, other times this can be + // because of how Select2 helps load all possible translation files. + if (options.debug && window.console && console.warn) { + console.warn( + 'Select2: The language file for "' + name + '" could not be ' + + 'automatically loaded. A fallback will be used instead.' + ); + } + + continue; + } + } + + languages.extend(language); + } + + options.translations = languages; + } else { + var baseTranslation = Translation.loadPath( + this.defaults.amdLanguageBase + 'en' + ); + var customTranslation = new Translation(options.language); + + customTranslation.extend(baseTranslation); + + options.translations = customTranslation; + } + + return options; + }; + + Defaults.prototype.reset = function () { + function stripDiacritics (text) { + // Used 'uni range + named function' from http://jsperf.com/diacritics/18 + function match(a) { + return DIACRITICS[a] || a; + } + + return text.replace(/[^\u0000-\u007E]/g, match); + } + + function matcher (params, data) { + // Always return the object if there is nothing to compare + if ($.trim(params.term) === '') { + return data; + } + + // Do a recursive check for options with children + if (data.children && data.children.length > 0) { + // Clone the data object if there are children + // This is required as we modify the object to remove any non-matches + var match = $.extend(true, {}, data); + + // Check each child of the option + for (var c = data.children.length - 1; c >= 0; c--) { + var child = data.children[c]; + + var matches = matcher(params, child); + + // If there wasn't a match, remove the object in the array + if (matches == null) { + match.children.splice(c, 1); + } + } + + // If any children matched, return the new object + if (match.children.length > 0) { + return match; + } + + // If there were no matching children, check just the plain object + return matcher(params, match); + } + + var original = stripDiacritics(data.text).toUpperCase(); + var term = stripDiacritics(params.term).toUpperCase(); + + // Check if the text contains the term + if (original.indexOf(term) > -1) { + return data; + } + + // If it doesn't contain the term, don't return anything + return null; + } + + this.defaults = { + amdBase: './', + amdLanguageBase: './i18n/', + closeOnSelect: true, + debug: false, + dropdownAutoWidth: false, + escapeMarkup: Utils.escapeMarkup, + language: EnglishTranslation, + matcher: matcher, + minimumInputLength: 0, + maximumInputLength: 0, + maximumSelectionLength: 0, + minimumResultsForSearch: 0, + selectOnClose: false, + sorter: function (data) { + return data; + }, + templateResult: function (result) { + return result.text; + }, + templateSelection: function (selection) { + return selection.text; + }, + theme: 'default', + width: 'resolve' + }; + }; + + Defaults.prototype.set = function (key, value) { + var camelKey = $.camelCase(key); + + var data = {}; + data[camelKey] = value; + + var convertedData = Utils._convertData(data); + + $.extend(this.defaults, convertedData); + }; + + var defaults = new Defaults(); + + return defaults; +}); + +S2.define('select2/options',[ + 'require', + 'jquery', + './defaults', + './utils' +], function (require, $, Defaults, Utils) { + function Options (options, $element) { + this.options = options; + + if ($element != null) { + this.fromElement($element); + } + + this.options = Defaults.apply(this.options); + + if ($element && $element.is('input')) { + var InputCompat = require(this.get('amdBase') + 'compat/inputData'); + + this.options.dataAdapter = Utils.Decorate( + this.options.dataAdapter, + InputCompat + ); + } + } + + Options.prototype.fromElement = function ($e) { + var excludedData = ['select2']; + + if (this.options.multiple == null) { + this.options.multiple = $e.prop('multiple'); + } + + if (this.options.disabled == null) { + this.options.disabled = $e.prop('disabled'); + } + + if (this.options.language == null) { + if ($e.prop('lang')) { + this.options.language = $e.prop('lang').toLowerCase(); + } else if ($e.closest('[lang]').prop('lang')) { + this.options.language = $e.closest('[lang]').prop('lang'); + } + } + + if (this.options.dir == null) { + if ($e.prop('dir')) { + this.options.dir = $e.prop('dir'); + } else if ($e.closest('[dir]').prop('dir')) { + this.options.dir = $e.closest('[dir]').prop('dir'); + } else { + this.options.dir = 'ltr'; + } + } + + $e.prop('disabled', this.options.disabled); + $e.prop('multiple', this.options.multiple); + + if ($e.data('select2Tags')) { + if (this.options.debug && window.console && console.warn) { + console.warn( + 'Select2: The `data-select2-tags` attribute has been changed to ' + + 'use the `data-data` and `data-tags="true"` attributes and will be ' + + 'removed in future versions of Select2.' + ); + } + + $e.data('data', $e.data('select2Tags')); + $e.data('tags', true); + } + + if ($e.data('ajaxUrl')) { + if (this.options.debug && window.console && console.warn) { + console.warn( + 'Select2: The `data-ajax-url` attribute has been changed to ' + + '`data-ajax--url` and support for the old attribute will be removed' + + ' in future versions of Select2.' + ); + } + + $e.attr('ajax--url', $e.data('ajaxUrl')); + $e.data('ajax--url', $e.data('ajaxUrl')); + } + + var dataset = {}; + + // Prefer the element's `dataset` attribute if it exists + // jQuery 1.x does not correctly handle data attributes with multiple dashes + if ($.fn.jquery && $.fn.jquery.substr(0, 2) == '1.' && $e[0].dataset) { + dataset = $.extend(true, {}, $e[0].dataset, $e.data()); + } else { + dataset = $e.data(); + } + + var data = $.extend(true, {}, dataset); + + data = Utils._convertData(data); + + for (var key in data) { + if ($.inArray(key, excludedData) > -1) { + continue; + } + + if ($.isPlainObject(this.options[key])) { + $.extend(this.options[key], data[key]); + } else { + this.options[key] = data[key]; + } + } + + return this; + }; + + Options.prototype.get = function (key) { + return this.options[key]; + }; + + Options.prototype.set = function (key, val) { + this.options[key] = val; + }; + + return Options; +}); + +S2.define('select2/core',[ + 'jquery', + './options', + './utils', + './keys' +], function ($, Options, Utils, KEYS) { + var Select2 = function ($element, options) { + if ($element.data('select2') != null) { + $element.data('select2').destroy(); + } + + this.$element = $element; + + this.id = this._generateId($element); + + options = options || {}; + + this.options = new Options(options, $element); + + Select2.__super__.constructor.call(this); + + // Set up the tabindex + + var tabindex = $element.attr('tabindex') || 0; + $element.data('old-tabindex', tabindex); + $element.attr('tabindex', '-1'); + + // Set up containers and adapters + + var DataAdapter = this.options.get('dataAdapter'); + this.dataAdapter = new DataAdapter($element, this.options); + + var $container = this.render(); + + this._placeContainer($container); + + var SelectionAdapter = this.options.get('selectionAdapter'); + this.selection = new SelectionAdapter($element, this.options); + this.$selection = this.selection.render(); + + this.selection.position(this.$selection, $container); + + var DropdownAdapter = this.options.get('dropdownAdapter'); + this.dropdown = new DropdownAdapter($element, this.options); + this.$dropdown = this.dropdown.render(); + + this.dropdown.position(this.$dropdown, $container); + + var ResultsAdapter = this.options.get('resultsAdapter'); + this.results = new ResultsAdapter($element, this.options, this.dataAdapter); + this.$results = this.results.render(); + + this.results.position(this.$results, this.$dropdown); + + // Bind events + + var self = this; + + // Bind the container to all of the adapters + this._bindAdapters(); + + // Register any DOM event handlers + this._registerDomEvents(); + + // Register any internal event handlers + this._registerDataEvents(); + this._registerSelectionEvents(); + this._registerDropdownEvents(); + this._registerResultsEvents(); + this._registerEvents(); + + // Set the initial state + this.dataAdapter.current(function (initialData) { + self.trigger('selection:update', { + data: initialData + }); + }); + + // Hide the original select + $element.addClass('select2-hidden-accessible'); + $element.attr('aria-hidden', 'true'); + + // Synchronize any monitored attributes + this._syncAttributes(); + + $element.data('select2', this); + }; + + Utils.Extend(Select2, Utils.Observable); + + Select2.prototype._generateId = function ($element) { + var id = ''; + + if ($element.attr('id') != null) { + id = $element.attr('id'); + } else if ($element.attr('name') != null) { + id = $element.attr('name') + '-' + Utils.generateChars(2); + } else { + id = Utils.generateChars(4); + } + + id = id.replace(/(:|\.|\[|\]|,)/g, ''); + id = 'select2-' + id; + + return id; + }; + + Select2.prototype._placeContainer = function ($container) { + $container.insertAfter(this.$element); + + var width = this._resolveWidth(this.$element, this.options.get('width')); + + if (width != null) { + $container.css('width', width); + } + }; + + Select2.prototype._resolveWidth = function ($element, method) { + var WIDTH = /^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i; + + if (method == 'resolve') { + var styleWidth = this._resolveWidth($element, 'style'); + + if (styleWidth != null) { + return styleWidth; + } + + return this._resolveWidth($element, 'element'); + } + + if (method == 'element') { + var elementWidth = $element.outerWidth(false); + + if (elementWidth <= 0) { + return 'auto'; + } + + return elementWidth + 'px'; + } + + if (method == 'style') { + var style = $element.attr('style'); + + if (typeof(style) !== 'string') { + return null; + } + + var attrs = style.split(';'); + + for (var i = 0, l = attrs.length; i < l; i = i + 1) { + var attr = attrs[i].replace(/\s/g, ''); + var matches = attr.match(WIDTH); + + if (matches !== null && matches.length >= 1) { + return matches[1]; + } + } + + return null; + } + + return method; + }; + + Select2.prototype._bindAdapters = function () { + this.dataAdapter.bind(this, this.$container); + this.selection.bind(this, this.$container); + + this.dropdown.bind(this, this.$container); + this.results.bind(this, this.$container); + }; + + Select2.prototype._registerDomEvents = function () { + var self = this; + + this.$element.on('change.select2', function () { + self.dataAdapter.current(function (data) { + self.trigger('selection:update', { + data: data + }); + }); + }); + + this.$element.on('focus.select2', function (evt) { + self.trigger('focus', evt); + }); + + this._syncA = Utils.bind(this._syncAttributes, this); + this._syncS = Utils.bind(this._syncSubtree, this); + + if (this.$element[0].attachEvent) { + this.$element[0].attachEvent('onpropertychange', this._syncA); + } + + var observer = window.MutationObserver || + window.WebKitMutationObserver || + window.MozMutationObserver + ; + + if (observer != null) { + this._observer = new observer(function (mutations) { + $.each(mutations, self._syncA); + $.each(mutations, self._syncS); + }); + this._observer.observe(this.$element[0], { + attributes: true, + childList: true, + subtree: false + }); + } else if (this.$element[0].addEventListener) { + this.$element[0].addEventListener( + 'DOMAttrModified', + self._syncA, + false + ); + this.$element[0].addEventListener( + 'DOMNodeInserted', + self._syncS, + false + ); + this.$element[0].addEventListener( + 'DOMNodeRemoved', + self._syncS, + false + ); + } + }; + + Select2.prototype._registerDataEvents = function () { + var self = this; + + this.dataAdapter.on('*', function (name, params) { + self.trigger(name, params); + }); + }; + + Select2.prototype._registerSelectionEvents = function () { + var self = this; + var nonRelayEvents = ['toggle', 'focus']; + + this.selection.on('toggle', function () { + self.toggleDropdown(); + }); + + this.selection.on('focus', function (params) { + self.focus(params); + }); + + this.selection.on('*', function (name, params) { + if ($.inArray(name, nonRelayEvents) !== -1) { + return; + } + + self.trigger(name, params); + }); + }; + + Select2.prototype._registerDropdownEvents = function () { + var self = this; + + this.dropdown.on('*', function (name, params) { + self.trigger(name, params); + }); + }; + + Select2.prototype._registerResultsEvents = function () { + var self = this; + + this.results.on('*', function (name, params) { + self.trigger(name, params); + }); + }; + + Select2.prototype._registerEvents = function () { + var self = this; + + this.on('open', function () { + self.$container.addClass('select2-container--open'); + }); + + this.on('close', function () { + self.$container.removeClass('select2-container--open'); + }); + + this.on('enable', function () { + self.$container.removeClass('select2-container--disabled'); + }); + + this.on('disable', function () { + self.$container.addClass('select2-container--disabled'); + }); + + this.on('blur', function () { + self.$container.removeClass('select2-container--focus'); + }); + + this.on('query', function (params) { + if (!self.isOpen()) { + self.trigger('open', {}); + } + + this.dataAdapter.query(params, function (data) { + self.trigger('results:all', { + data: data, + query: params + }); + }); + }); + + this.on('query:append', function (params) { + this.dataAdapter.query(params, function (data) { + self.trigger('results:append', { + data: data, + query: params + }); + }); + }); + + this.on('keypress', function (evt) { + var key = evt.which; + + if (self.isOpen()) { + if (key === KEYS.ESC || key === KEYS.TAB || + (key === KEYS.UP && evt.altKey)) { + self.close(); + + evt.preventDefault(); + } else if (key === KEYS.ENTER) { + self.trigger('results:select', {}); + + evt.preventDefault(); + } else if ((key === KEYS.SPACE && evt.ctrlKey)) { + self.trigger('results:toggle', {}); + + evt.preventDefault(); + } else if (key === KEYS.UP) { + self.trigger('results:previous', {}); + + evt.preventDefault(); + } else if (key === KEYS.DOWN) { + self.trigger('results:next', {}); + + evt.preventDefault(); + } + } else { + if (key === KEYS.ENTER || key === KEYS.SPACE || + (key === KEYS.DOWN && evt.altKey)) { + self.open(); + + evt.preventDefault(); + } + } + }); + }; + + Select2.prototype._syncAttributes = function () { + this.options.set('disabled', this.$element.prop('disabled')); + + if (this.options.get('disabled')) { + if (this.isOpen()) { + this.close(); + } + + this.trigger('disable', {}); + } else { + this.trigger('enable', {}); + } + }; + + Select2.prototype._syncSubtree = function (evt, mutations) { + var changed = false; + var self = this; + + // Ignore any mutation events raised for elements that aren't options or + // optgroups. This handles the case when the select element is destroyed + if ( + evt && evt.target && ( + evt.target.nodeName !== 'OPTION' && evt.target.nodeName !== 'OPTGROUP' + ) + ) { + return; + } + + if (!mutations) { + // If mutation events aren't supported, then we can only assume that the + // change affected the selections + changed = true; + } else if (mutations.addedNodes && mutations.addedNodes.length > 0) { + for (var n = 0; n < mutations.addedNodes.length; n++) { + var node = mutations.addedNodes[n]; + + if (node.selected) { + changed = true; + } + } + } else if (mutations.removedNodes && mutations.removedNodes.length > 0) { + changed = true; + } + + // Only re-pull the data if we think there is a change + if (changed) { + this.dataAdapter.current(function (currentData) { + self.trigger('selection:update', { + data: currentData + }); + }); + } + }; + + /** + * Override the trigger method to automatically trigger pre-events when + * there are events that can be prevented. + */ + Select2.prototype.trigger = function (name, args) { + var actualTrigger = Select2.__super__.trigger; + var preTriggerMap = { + 'open': 'opening', + 'close': 'closing', + 'select': 'selecting', + 'unselect': 'unselecting' + }; + + if (args === undefined) { + args = {}; + } + + if (name in preTriggerMap) { + var preTriggerName = preTriggerMap[name]; + var preTriggerArgs = { + prevented: false, + name: name, + args: args + }; + + actualTrigger.call(this, preTriggerName, preTriggerArgs); + + if (preTriggerArgs.prevented) { + args.prevented = true; + + return; + } + } + + actualTrigger.call(this, name, args); + }; + + Select2.prototype.toggleDropdown = function () { + if (this.options.get('disabled')) { + return; + } + + if (this.isOpen()) { + this.close(); + } else { + this.open(); + } + }; + + Select2.prototype.open = function () { + if (this.isOpen()) { + return; + } + + this.trigger('query', {}); + }; + + Select2.prototype.close = function () { + if (!this.isOpen()) { + return; + } + + this.trigger('close', {}); + }; + + Select2.prototype.isOpen = function () { + return this.$container.hasClass('select2-container--open'); + }; + + Select2.prototype.hasFocus = function () { + return this.$container.hasClass('select2-container--focus'); + }; + + Select2.prototype.focus = function (data) { + // No need to re-trigger focus events if we are already focused + if (this.hasFocus()) { + return; + } + + this.$container.addClass('select2-container--focus'); + this.trigger('focus', {}); + }; + + Select2.prototype.enable = function (args) { + if (this.options.get('debug') && window.console && console.warn) { + console.warn( + 'Select2: The `select2("enable")` method has been deprecated and will' + + ' be removed in later Select2 versions. Use $element.prop("disabled")' + + ' instead.' + ); + } + + if (args == null || args.length === 0) { + args = [true]; + } + + var disabled = !args[0]; + + this.$element.prop('disabled', disabled); + }; + + Select2.prototype.data = function () { + if (this.options.get('debug') && + arguments.length > 0 && window.console && console.warn) { + console.warn( + 'Select2: Data can no longer be set using `select2("data")`. You ' + + 'should consider setting the value instead using `$element.val()`.' + ); + } + + var data = []; + + this.dataAdapter.current(function (currentData) { + data = currentData; + }); + + return data; + }; + + Select2.prototype.val = function (args) { + if (this.options.get('debug') && window.console && console.warn) { + console.warn( + 'Select2: The `select2("val")` method has been deprecated and will be' + + ' removed in later Select2 versions. Use $element.val() instead.' + ); + } + + if (args == null || args.length === 0) { + return this.$element.val(); + } + + var newVal = args[0]; + + if ($.isArray(newVal)) { + newVal = $.map(newVal, function (obj) { + return obj.toString(); + }); + } + + this.$element.val(newVal).trigger('change'); + }; + + Select2.prototype.destroy = function () { + this.$container.remove(); + + if (this.$element[0].detachEvent) { + this.$element[0].detachEvent('onpropertychange', this._syncA); + } + + if (this._observer != null) { + this._observer.disconnect(); + this._observer = null; + } else if (this.$element[0].removeEventListener) { + this.$element[0] + .removeEventListener('DOMAttrModified', this._syncA, false); + this.$element[0] + .removeEventListener('DOMNodeInserted', this._syncS, false); + this.$element[0] + .removeEventListener('DOMNodeRemoved', this._syncS, false); + } + + this._syncA = null; + this._syncS = null; + + this.$element.off('.select2'); + this.$element.attr('tabindex', this.$element.data('old-tabindex')); + + this.$element.removeClass('select2-hidden-accessible'); + this.$element.attr('aria-hidden', 'false'); + this.$element.removeData('select2'); + + this.dataAdapter.destroy(); + this.selection.destroy(); + this.dropdown.destroy(); + this.results.destroy(); + + this.dataAdapter = null; + this.selection = null; + this.dropdown = null; + this.results = null; + }; + + Select2.prototype.render = function () { + var $container = $( + '' + + '' + + '' + + '' + ); + + $container.attr('dir', this.options.get('dir')); + + this.$container = $container; + + this.$container.addClass('select2-container--' + this.options.get('theme')); + + $container.data('element', this.$element); + + return $container; + }; + + return Select2; +}); + +S2.define('select2/compat/utils',[ + 'jquery' +], function ($) { + function syncCssClasses ($dest, $src, adapter) { + var classes, replacements = [], adapted; + + classes = $.trim($dest.attr('class')); + + if (classes) { + classes = '' + classes; // for IE which returns object + + $(classes.split(/\s+/)).each(function () { + // Save all Select2 classes + if (this.indexOf('select2-') === 0) { + replacements.push(this); + } + }); + } + + classes = $.trim($src.attr('class')); + + if (classes) { + classes = '' + classes; // for IE which returns object + + $(classes.split(/\s+/)).each(function () { + // Only adapt non-Select2 classes + if (this.indexOf('select2-') !== 0) { + adapted = adapter(this); + + if (adapted != null) { + replacements.push(adapted); + } + } + }); + } + + $dest.attr('class', replacements.join(' ')); + } + + return { + syncCssClasses: syncCssClasses + }; +}); + +S2.define('select2/compat/containerCss',[ + 'jquery', + './utils' +], function ($, CompatUtils) { + // No-op CSS adapter that discards all classes by default + function _containerAdapter (clazz) { + return null; + } + + function ContainerCSS () { } + + ContainerCSS.prototype.render = function (decorated) { + var $container = decorated.call(this); + + var containerCssClass = this.options.get('containerCssClass') || ''; + + if ($.isFunction(containerCssClass)) { + containerCssClass = containerCssClass(this.$element); + } + + var containerCssAdapter = this.options.get('adaptContainerCssClass'); + containerCssAdapter = containerCssAdapter || _containerAdapter; + + if (containerCssClass.indexOf(':all:') !== -1) { + containerCssClass = containerCssClass.replace(':all:', ''); + + var _cssAdapter = containerCssAdapter; + + containerCssAdapter = function (clazz) { + var adapted = _cssAdapter(clazz); + + if (adapted != null) { + // Append the old one along with the adapted one + return adapted + ' ' + clazz; + } + + return clazz; + }; + } + + var containerCss = this.options.get('containerCss') || {}; + + if ($.isFunction(containerCss)) { + containerCss = containerCss(this.$element); + } + + CompatUtils.syncCssClasses($container, this.$element, containerCssAdapter); + + $container.css(containerCss); + $container.addClass(containerCssClass); + + return $container; + }; + + return ContainerCSS; +}); + +S2.define('select2/compat/dropdownCss',[ + 'jquery', + './utils' +], function ($, CompatUtils) { + // No-op CSS adapter that discards all classes by default + function _dropdownAdapter (clazz) { + return null; + } + + function DropdownCSS () { } + + DropdownCSS.prototype.render = function (decorated) { + var $dropdown = decorated.call(this); + + var dropdownCssClass = this.options.get('dropdownCssClass') || ''; + + if ($.isFunction(dropdownCssClass)) { + dropdownCssClass = dropdownCssClass(this.$element); + } + + var dropdownCssAdapter = this.options.get('adaptDropdownCssClass'); + dropdownCssAdapter = dropdownCssAdapter || _dropdownAdapter; + + if (dropdownCssClass.indexOf(':all:') !== -1) { + dropdownCssClass = dropdownCssClass.replace(':all:', ''); + + var _cssAdapter = dropdownCssAdapter; + + dropdownCssAdapter = function (clazz) { + var adapted = _cssAdapter(clazz); + + if (adapted != null) { + // Append the old one along with the adapted one + return adapted + ' ' + clazz; + } + + return clazz; + }; + } + + var dropdownCss = this.options.get('dropdownCss') || {}; + + if ($.isFunction(dropdownCss)) { + dropdownCss = dropdownCss(this.$element); + } + + CompatUtils.syncCssClasses($dropdown, this.$element, dropdownCssAdapter); + + $dropdown.css(dropdownCss); + $dropdown.addClass(dropdownCssClass); + + return $dropdown; + }; + + return DropdownCSS; +}); + +S2.define('select2/compat/initSelection',[ + 'jquery' +], function ($) { + function InitSelection (decorated, $element, options) { + if (options.get('debug') && window.console && console.warn) { + console.warn( + 'Select2: The `initSelection` option has been deprecated in favor' + + ' of a custom data adapter that overrides the `current` method. ' + + 'This method is now called multiple times instead of a single ' + + 'time when the instance is initialized. Support will be removed ' + + 'for the `initSelection` option in future versions of Select2' + ); + } + + this.initSelection = options.get('initSelection'); + this._isInitialized = false; + + decorated.call(this, $element, options); + } + + InitSelection.prototype.current = function (decorated, callback) { + var self = this; + + if (this._isInitialized) { + decorated.call(this, callback); + + return; + } + + this.initSelection.call(null, this.$element, function (data) { + self._isInitialized = true; + + if (!$.isArray(data)) { + data = [data]; + } + + callback(data); + }); + }; + + return InitSelection; +}); + +S2.define('select2/compat/inputData',[ + 'jquery' +], function ($) { + function InputData (decorated, $element, options) { + this._currentData = []; + this._valueSeparator = options.get('valueSeparator') || ','; + + if ($element.prop('type') === 'hidden') { + if (options.get('debug') && console && console.warn) { + console.warn( + 'Select2: Using a hidden input with Select2 is no longer ' + + 'supported and may stop working in the future. It is recommended ' + + 'to use a `');this.$searchContainer=c,this.$search=c.find("input");var d=b.call(this);return this._transferTabIndex(),d},d.prototype.bind=function(a,b,d){var e=this;a.call(this,b,d),b.on("open",function(){e.$search.trigger("focus")}),b.on("close",function(){e.$search.val(""),e.$search.removeAttr("aria-activedescendant"),e.$search.trigger("focus")}),b.on("enable",function(){e.$search.prop("disabled",!1),e._transferTabIndex()}),b.on("disable",function(){e.$search.prop("disabled",!0)}),b.on("focus",function(a){e.$search.trigger("focus")}),b.on("results:focus",function(a){e.$search.attr("aria-activedescendant",a.id)}),this.$selection.on("focusin",".select2-search--inline",function(a){e.trigger("focus",a)}),this.$selection.on("focusout",".select2-search--inline",function(a){e._handleBlur(a)}),this.$selection.on("keydown",".select2-search--inline",function(a){a.stopPropagation(),e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented();var b=a.which;if(b===c.BACKSPACE&&""===e.$search.val()){var d=e.$searchContainer.prev(".select2-selection__choice");if(d.length>0){var f=d.data("data");e.searchRemoveChoice(f),a.preventDefault()}}});var f=document.documentMode,g=f&&11>=f;this.$selection.on("input.searchcheck",".select2-search--inline",function(a){return g?void e.$selection.off("input.search input.searchcheck"):void e.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(a){if(g&&"input"===a.type)return void e.$selection.off("input.search input.searchcheck");var b=a.which;b!=c.SHIFT&&b!=c.CTRL&&b!=c.ALT&&b!=c.TAB&&e.handleSearch(a)})},d.prototype._transferTabIndex=function(a){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},d.prototype.createPlaceholder=function(a,b){this.$search.attr("placeholder",b.text)},d.prototype.update=function(a,b){var c=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),a.call(this,b),this.$selection.find(".select2-selection__rendered").append(this.$searchContainer),this.resizeSearch(),c&&this.$search.focus()},d.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var a=this.$search.val();this.trigger("query",{term:a})}this._keyUpPrevented=!1},d.prototype.searchRemoveChoice=function(a,b){this.trigger("unselect",{data:b}),this.$search.val(b.text),this.handleSearch()},d.prototype.resizeSearch=function(){this.$search.css("width","25px");var a="";if(""!==this.$search.attr("placeholder"))a=this.$selection.find(".select2-selection__rendered").innerWidth();else{var b=this.$search.val().length+1;a=.75*b+"em"}this.$search.css("width",a)},d}),b.define("select2/selection/eventRelay",["jquery"],function(a){function b(){}return b.prototype.bind=function(b,c,d){var e=this,f=["open","opening","close","closing","select","selecting","unselect","unselecting"],g=["opening","closing","selecting","unselecting"];b.call(this,c,d),c.on("*",function(b,c){if(-1!==a.inArray(b,f)){c=c||{};var d=a.Event("select2:"+b,{params:c});e.$element.trigger(d),-1!==a.inArray(b,g)&&(c.prevented=d.isDefaultPrevented())}})},b}),b.define("select2/translation",["jquery","require"],function(a,b){function c(a){this.dict=a||{}}return c.prototype.all=function(){return this.dict},c.prototype.get=function(a){return this.dict[a]},c.prototype.extend=function(b){this.dict=a.extend({},b.all(),this.dict)},c._cache={},c.loadPath=function(a){if(!(a in c._cache)){var d=b(a);c._cache[a]=d}return new c(c._cache[a])},c}),b.define("select2/diacritics",[],function(){var a={"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ω":"ω","ς":"σ"};return a}),b.define("select2/data/base",["../utils"],function(a){function b(a,c){b.__super__.constructor.call(this)}return a.Extend(b,a.Observable),b.prototype.current=function(a){throw new Error("The `current` method must be defined in child classes.")},b.prototype.query=function(a,b){throw new Error("The `query` method must be defined in child classes.")},b.prototype.bind=function(a,b){},b.prototype.destroy=function(){},b.prototype.generateResultId=function(b,c){var d=b.id+"-result-";return d+=a.generateChars(4),d+=null!=c.id?"-"+c.id.toString():"-"+a.generateChars(4)},b}),b.define("select2/data/select",["./base","../utils","jquery"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,a),d.prototype.current=function(a){var b=[],d=this;this.$element.find(":selected").each(function(){var a=c(this),e=d.item(a);b.push(e)}),a(b)},d.prototype.select=function(a){var b=this;if(a.selected=!0,c(a.element).is("option"))return a.element.selected=!0,void this.$element.trigger("change"); +if(this.$element.prop("multiple"))this.current(function(d){var e=[];a=[a],a.push.apply(a,d);for(var f=0;f=0){var k=f.filter(d(j)),l=this.item(k),m=c.extend(!0,{},j,l),n=this.option(m);k.replaceWith(n)}else{var o=this.option(j);if(j.children){var p=this.convertToOptions(j.children);b.appendMany(o,p)}h.push(o)}}return h},d}),b.define("select2/data/ajax",["./array","../utils","jquery"],function(a,b,c){function d(a,b){this.ajaxOptions=this._applyDefaults(b.get("ajax")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),d.__super__.constructor.call(this,a,b)}return b.Extend(d,a),d.prototype._applyDefaults=function(a){var b={data:function(a){return c.extend({},a,{q:a.term})},transport:function(a,b,d){var e=c.ajax(a);return e.then(b),e.fail(d),e}};return c.extend({},b,a,!0)},d.prototype.processResults=function(a){return a},d.prototype.query=function(a,b){function d(){var d=f.transport(f,function(d){var f=e.processResults(d,a);e.options.get("debug")&&window.console&&console.error&&(f&&f.results&&c.isArray(f.results)||console.error("Select2: The AJAX results did not return an array in the `results` key of the response.")),b(f)},function(){d.status&&"0"===d.status||e.trigger("results:message",{message:"errorLoading"})});e._request=d}var e=this;null!=this._request&&(c.isFunction(this._request.abort)&&this._request.abort(),this._request=null);var f=c.extend({type:"GET"},this.ajaxOptions);"function"==typeof f.url&&(f.url=f.url.call(this.$element,a)),"function"==typeof f.data&&(f.data=f.data.call(this.$element,a)),this.ajaxOptions.delay&&null!=a.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(d,this.ajaxOptions.delay)):d()},d}),b.define("select2/data/tags",["jquery"],function(a){function b(b,c,d){var e=d.get("tags"),f=d.get("createTag");void 0!==f&&(this.createTag=f);var g=d.get("insertTag");if(void 0!==g&&(this.insertTag=g),b.call(this,c,d),a.isArray(e))for(var h=0;h0&&b.term.length>this.maximumInputLength?void this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:b.term,params:b}}):void a.call(this,b,c)},a}),b.define("select2/data/maximumSelectionLength",[],function(){function a(a,b,c){this.maximumSelectionLength=c.get("maximumSelectionLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){var d=this;this.current(function(e){var f=null!=e?e.length:0;return d.maximumSelectionLength>0&&f>=d.maximumSelectionLength?void d.trigger("results:message",{message:"maximumSelected",args:{maximum:d.maximumSelectionLength}}):void a.call(d,b,c)})},a}),b.define("select2/dropdown",["jquery","./utils"],function(a,b){function c(a,b){this.$element=a,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('');return b.attr("dir",this.options.get("dir")),this.$dropdown=b,b},c.prototype.bind=function(){},c.prototype.position=function(a,b){},c.prototype.destroy=function(){this.$dropdown.remove()},c}),b.define("select2/dropdown/search",["jquery","../utils"],function(a,b){function c(){}return c.prototype.render=function(b){var c=b.call(this),d=a('');return this.$searchContainer=d,this.$search=d.find("input"),c.prepend(d),c},c.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),this.$search.on("keydown",function(a){e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented()}),this.$search.on("input",function(b){a(this).off("keyup")}),this.$search.on("keyup input",function(a){e.handleSearch(a)}),c.on("open",function(){e.$search.attr("tabindex",0),e.$search.focus(),window.setTimeout(function(){e.$search.focus()},0)}),c.on("close",function(){e.$search.attr("tabindex",-1),e.$search.val("")}),c.on("focus",function(){c.isOpen()&&e.$search.focus()}),c.on("results:all",function(a){if(null==a.query.term||""===a.query.term){var b=e.showSearch(a);b?e.$searchContainer.removeClass("select2-search--hide"):e.$searchContainer.addClass("select2-search--hide")}})},c.prototype.handleSearch=function(a){if(!this._keyUpPrevented){var b=this.$search.val();this.trigger("query",{term:b})}this._keyUpPrevented=!1},c.prototype.showSearch=function(a,b){return!0},c}),b.define("select2/dropdown/hidePlaceholder",[],function(){function a(a,b,c,d){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c,d)}return a.prototype.append=function(a,b){b.results=this.removePlaceholder(b.results),a.call(this,b)},a.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},a.prototype.removePlaceholder=function(a,b){for(var c=b.slice(0),d=b.length-1;d>=0;d--){var e=b[d];this.placeholder.id===e.id&&c.splice(d,1)}return c},a}),b.define("select2/dropdown/infiniteScroll",["jquery"],function(a){function b(a,b,c,d){this.lastParams={},a.call(this,b,c,d),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return b.prototype.append=function(a,b){this.$loadingMore.remove(),this.loading=!1,a.call(this,b),this.showLoadingMore(b)&&this.$results.append(this.$loadingMore)},b.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),c.on("query",function(a){e.lastParams=a,e.loading=!0}),c.on("query:append",function(a){e.lastParams=a,e.loading=!0}),this.$results.on("scroll",function(){var b=a.contains(document.documentElement,e.$loadingMore[0]);if(!e.loading&&b){var c=e.$results.offset().top+e.$results.outerHeight(!1),d=e.$loadingMore.offset().top+e.$loadingMore.outerHeight(!1);c+50>=d&&e.loadMore()}})},b.prototype.loadMore=function(){this.loading=!0;var b=a.extend({},{page:1},this.lastParams);b.page++,this.trigger("query:append",b)},b.prototype.showLoadingMore=function(a,b){return b.pagination&&b.pagination.more},b.prototype.createLoadingMore=function(){var b=a('
        • '),c=this.options.get("translations").get("loadingMore");return b.html(c(this.lastParams)),b},b}),b.define("select2/dropdown/attachBody",["jquery","../utils"],function(a,b){function c(b,c,d){this.$dropdownParent=d.get("dropdownParent")||a(document.body),b.call(this,c,d)}return c.prototype.bind=function(a,b,c){var d=this,e=!1;a.call(this,b,c),b.on("open",function(){d._showDropdown(),d._attachPositioningHandler(b),e||(e=!0,b.on("results:all",function(){d._positionDropdown(),d._resizeDropdown()}),b.on("results:append",function(){d._positionDropdown(),d._resizeDropdown()}))}),b.on("close",function(){d._hideDropdown(),d._detachPositioningHandler(b)}),this.$dropdownContainer.on("mousedown",function(a){a.stopPropagation()})},c.prototype.destroy=function(a){a.call(this),this.$dropdownContainer.remove()},c.prototype.position=function(a,b,c){b.attr("class",c.attr("class")),b.removeClass("select2"),b.addClass("select2-container--open"),b.css({position:"absolute",top:-999999}),this.$container=c},c.prototype.render=function(b){var c=a(""),d=b.call(this);return c.append(d),this.$dropdownContainer=c,c},c.prototype._hideDropdown=function(a){this.$dropdownContainer.detach()},c.prototype._attachPositioningHandler=function(c,d){var e=this,f="scroll.select2."+d.id,g="resize.select2."+d.id,h="orientationchange.select2."+d.id,i=this.$container.parents().filter(b.hasScroll);i.each(function(){a(this).data("select2-scroll-position",{x:a(this).scrollLeft(),y:a(this).scrollTop()})}),i.on(f,function(b){var c=a(this).data("select2-scroll-position");a(this).scrollTop(c.y)}),a(window).on(f+" "+g+" "+h,function(a){e._positionDropdown(),e._resizeDropdown()})},c.prototype._detachPositioningHandler=function(c,d){var e="scroll.select2."+d.id,f="resize.select2."+d.id,g="orientationchange.select2."+d.id,h=this.$container.parents().filter(b.hasScroll);h.off(e),a(window).off(e+" "+f+" "+g)},c.prototype._positionDropdown=function(){var b=a(window),c=this.$dropdown.hasClass("select2-dropdown--above"),d=this.$dropdown.hasClass("select2-dropdown--below"),e=null,f=this.$container.offset();f.bottom=f.top+this.$container.outerHeight(!1);var g={height:this.$container.outerHeight(!1)};g.top=f.top,g.bottom=f.top+g.height;var h={height:this.$dropdown.outerHeight(!1)},i={top:b.scrollTop(),bottom:b.scrollTop()+b.height()},j=i.topf.bottom+h.height,l={left:f.left,top:g.bottom},m=this.$dropdownParent;"static"===m.css("position")&&(m=m.offsetParent());var n=m.offset();l.top-=n.top,l.left-=n.left,c||d||(e="below"),k||!j||c?!j&&k&&c&&(e="below"):e="above",("above"==e||c&&"below"!==e)&&(l.top=g.top-n.top-h.height),null!=e&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+e),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+e)),this.$dropdownContainer.css(l)},c.prototype._resizeDropdown=function(){var a={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(a.minWidth=a.width,a.position="relative",a.width="auto"),this.$dropdown.css(a)},c.prototype._showDropdown=function(a){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},c}),b.define("select2/dropdown/minimumResultsForSearch",[],function(){function a(b){for(var c=0,d=0;d0&&(l.dataAdapter=j.Decorate(l.dataAdapter,r)),l.maximumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,s)),l.maximumSelectionLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,t)),l.tags&&(l.dataAdapter=j.Decorate(l.dataAdapter,p)),(null!=l.tokenSeparators||null!=l.tokenizer)&&(l.dataAdapter=j.Decorate(l.dataAdapter,q)),null!=l.query){var C=b(l.amdBase+"compat/query");l.dataAdapter=j.Decorate(l.dataAdapter,C)}if(null!=l.initSelection){var D=b(l.amdBase+"compat/initSelection");l.dataAdapter=j.Decorate(l.dataAdapter,D)}}if(null==l.resultsAdapter&&(l.resultsAdapter=c,null!=l.ajax&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,x)),null!=l.placeholder&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,w)),l.selectOnClose&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,A))),null==l.dropdownAdapter){if(l.multiple)l.dropdownAdapter=u;else{var E=j.Decorate(u,v);l.dropdownAdapter=E}if(0!==l.minimumResultsForSearch&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,z)),l.closeOnSelect&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,B)),null!=l.dropdownCssClass||null!=l.dropdownCss||null!=l.adaptDropdownCssClass){var F=b(l.amdBase+"compat/dropdownCss");l.dropdownAdapter=j.Decorate(l.dropdownAdapter,F)}l.dropdownAdapter=j.Decorate(l.dropdownAdapter,y)}if(null==l.selectionAdapter){if(l.multiple?l.selectionAdapter=e:l.selectionAdapter=d,null!=l.placeholder&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,f)),l.allowClear&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,g)),l.multiple&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,h)),null!=l.containerCssClass||null!=l.containerCss||null!=l.adaptContainerCssClass){var G=b(l.amdBase+"compat/containerCss");l.selectionAdapter=j.Decorate(l.selectionAdapter,G)}l.selectionAdapter=j.Decorate(l.selectionAdapter,i)}if("string"==typeof l.language)if(l.language.indexOf("-")>0){var H=l.language.split("-"),I=H[0];l.language=[l.language,I]}else l.language=[l.language];if(a.isArray(l.language)){var J=new k;l.language.push("en");for(var K=l.language,L=0;L0){for(var f=a.extend(!0,{},e),g=e.children.length-1;g>=0;g--){var h=e.children[g],i=c(d,h);null==i&&f.children.splice(g,1)}return f.children.length>0?f:c(d,f)}var j=b(e.text).toUpperCase(),k=b(d.term).toUpperCase();return j.indexOf(k)>-1?e:null}this.defaults={amdBase:"./",amdLanguageBase:"./i18n/",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:j.escapeMarkup,language:C,matcher:c,minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,sorter:function(a){return a},templateResult:function(a){return a.text},templateSelection:function(a){return a.text},theme:"default",width:"resolve"}},D.prototype.set=function(b,c){var d=a.camelCase(b),e={};e[d]=c;var f=j._convertData(e);a.extend(this.defaults,f)};var E=new D;return E}),b.define("select2/options",["require","jquery","./defaults","./utils"],function(a,b,c,d){function e(b,e){if(this.options=b,null!=e&&this.fromElement(e),this.options=c.apply(this.options),e&&e.is("input")){var f=a(this.get("amdBase")+"compat/inputData");this.options.dataAdapter=d.Decorate(this.options.dataAdapter,f)}}return e.prototype.fromElement=function(a){var c=["select2"];null==this.options.multiple&&(this.options.multiple=a.prop("multiple")),null==this.options.disabled&&(this.options.disabled=a.prop("disabled")),null==this.options.language&&(a.prop("lang")?this.options.language=a.prop("lang").toLowerCase():a.closest("[lang]").prop("lang")&&(this.options.language=a.closest("[lang]").prop("lang"))),null==this.options.dir&&(a.prop("dir")?this.options.dir=a.prop("dir"):a.closest("[dir]").prop("dir")?this.options.dir=a.closest("[dir]").prop("dir"):this.options.dir="ltr"),a.prop("disabled",this.options.disabled),a.prop("multiple",this.options.multiple),a.data("select2Tags")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags="true"` attributes and will be removed in future versions of Select2.'),a.data("data",a.data("select2Tags")),a.data("tags",!0)),a.data("ajaxUrl")&&(this.options.debug&&window.console&&console.warn&&console.warn("Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2."),a.attr("ajax--url",a.data("ajaxUrl")),a.data("ajax--url",a.data("ajaxUrl")));var e={};e=b.fn.jquery&&"1."==b.fn.jquery.substr(0,2)&&a[0].dataset?b.extend(!0,{},a[0].dataset,a.data()):a.data();var f=b.extend(!0,{},e);f=d._convertData(f);for(var g in f)b.inArray(g,c)>-1||(b.isPlainObject(this.options[g])?b.extend(this.options[g],f[g]):this.options[g]=f[g]);return this},e.prototype.get=function(a){return this.options[a]},e.prototype.set=function(a,b){this.options[a]=b},e}),b.define("select2/core",["jquery","./options","./utils","./keys"],function(a,b,c,d){var e=function(a,c){null!=a.data("select2")&&a.data("select2").destroy(),this.$element=a,this.id=this._generateId(a),c=c||{},this.options=new b(c,a),e.__super__.constructor.call(this);var d=a.attr("tabindex")||0;a.data("old-tabindex",d),a.attr("tabindex","-1");var f=this.options.get("dataAdapter");this.dataAdapter=new f(a,this.options);var g=this.render();this._placeContainer(g);var h=this.options.get("selectionAdapter");this.selection=new h(a,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,g);var i=this.options.get("dropdownAdapter");this.dropdown=new i(a,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,g);var j=this.options.get("resultsAdapter");this.results=new j(a,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var k=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(a){k.trigger("selection:update",{data:a})}),a.addClass("select2-hidden-accessible"),a.attr("aria-hidden","true"),this._syncAttributes(),a.data("select2",this)};return c.Extend(e,c.Observable),e.prototype._generateId=function(a){var b="";return b=null!=a.attr("id")?a.attr("id"):null!=a.attr("name")?a.attr("name")+"-"+c.generateChars(2):c.generateChars(4),b=b.replace(/(:|\.|\[|\]|,)/g,""),b="select2-"+b},e.prototype._placeContainer=function(a){a.insertAfter(this.$element);var b=this._resolveWidth(this.$element,this.options.get("width"));null!=b&&a.css("width",b)},e.prototype._resolveWidth=function(a,b){var c=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if("resolve"==b){var d=this._resolveWidth(a,"style");return null!=d?d:this._resolveWidth(a,"element")}if("element"==b){var e=a.outerWidth(!1);return 0>=e?"auto":e+"px"}if("style"==b){var f=a.attr("style");if("string"!=typeof f)return null;for(var g=f.split(";"),h=0,i=g.length;i>h;h+=1){var j=g[h].replace(/\s/g,""),k=j.match(c);if(null!==k&&k.length>=1)return k[1]}return null}return b},e.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},e.prototype._registerDomEvents=function(){var b=this;this.$element.on("change.select2",function(){b.dataAdapter.current(function(a){b.trigger("selection:update",{data:a})})}),this.$element.on("focus.select2",function(a){b.trigger("focus",a)}),this._syncA=c.bind(this._syncAttributes,this),this._syncS=c.bind(this._syncSubtree,this),this.$element[0].attachEvent&&this.$element[0].attachEvent("onpropertychange",this._syncA);var d=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;null!=d?(this._observer=new d(function(c){a.each(c,b._syncA),a.each(c,b._syncS)}),this._observer.observe(this.$element[0],{attributes:!0,childList:!0,subtree:!1})):this.$element[0].addEventListener&&(this.$element[0].addEventListener("DOMAttrModified",b._syncA,!1),this.$element[0].addEventListener("DOMNodeInserted",b._syncS,!1),this.$element[0].addEventListener("DOMNodeRemoved",b._syncS,!1))},e.prototype._registerDataEvents=function(){var a=this;this.dataAdapter.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerSelectionEvents=function(){var b=this,c=["toggle","focus"];this.selection.on("toggle",function(){b.toggleDropdown()}),this.selection.on("focus",function(a){b.focus(a)}),this.selection.on("*",function(d,e){-1===a.inArray(d,c)&&b.trigger(d,e)})},e.prototype._registerDropdownEvents=function(){var a=this;this.dropdown.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerResultsEvents=function(){var a=this;this.results.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerEvents=function(){var a=this;this.on("open",function(){a.$container.addClass("select2-container--open")}),this.on("close",function(){a.$container.removeClass("select2-container--open")}),this.on("enable",function(){a.$container.removeClass("select2-container--disabled")}),this.on("disable",function(){a.$container.addClass("select2-container--disabled")}),this.on("blur",function(){a.$container.removeClass("select2-container--focus")}),this.on("query",function(b){a.isOpen()||a.trigger("open",{}),this.dataAdapter.query(b,function(c){a.trigger("results:all",{data:c,query:b})})}),this.on("query:append",function(b){this.dataAdapter.query(b,function(c){a.trigger("results:append",{data:c,query:b})})}),this.on("keypress",function(b){var c=b.which;a.isOpen()?c===d.ESC||c===d.TAB||c===d.UP&&b.altKey?(a.close(),b.preventDefault()):c===d.ENTER?(a.trigger("results:select",{}),b.preventDefault()):c===d.SPACE&&b.ctrlKey?(a.trigger("results:toggle",{}),b.preventDefault()):c===d.UP?(a.trigger("results:previous",{}),b.preventDefault()):c===d.DOWN&&(a.trigger("results:next",{}),b.preventDefault()):(c===d.ENTER||c===d.SPACE||c===d.DOWN&&b.altKey)&&(a.open(),b.preventDefault())})},e.prototype._syncAttributes=function(){this.options.set("disabled",this.$element.prop("disabled")),this.options.get("disabled")?(this.isOpen()&&this.close(),this.trigger("disable",{})):this.trigger("enable",{})},e.prototype._syncSubtree=function(a,b){var c=!1,d=this;if(!a||!a.target||"OPTION"===a.target.nodeName||"OPTGROUP"===a.target.nodeName){if(b)if(b.addedNodes&&b.addedNodes.length>0)for(var e=0;e0&&(c=!0);else c=!0;c&&this.dataAdapter.current(function(a){d.trigger("selection:update",{data:a})})}},e.prototype.trigger=function(a,b){var c=e.__super__.trigger,d={open:"opening",close:"closing",select:"selecting",unselect:"unselecting"};if(void 0===b&&(b={}),a in d){var f=d[a],g={prevented:!1,name:a,args:b};if(c.call(this,f,g),g.prevented)return void(b.prevented=!0)}c.call(this,a,b)},e.prototype.toggleDropdown=function(){this.options.get("disabled")||(this.isOpen()?this.close():this.open())},e.prototype.open=function(){this.isOpen()||this.trigger("query",{})},e.prototype.close=function(){this.isOpen()&&this.trigger("close",{})},e.prototype.isOpen=function(){return this.$container.hasClass("select2-container--open")},e.prototype.hasFocus=function(){return this.$container.hasClass("select2-container--focus")},e.prototype.focus=function(a){this.hasFocus()||(this.$container.addClass("select2-container--focus"),this.trigger("focus",{}))},e.prototype.enable=function(a){this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("enable")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop("disabled") instead.'),(null==a||0===a.length)&&(a=[!0]);var b=!a[0];this.$element.prop("disabled",b)},e.prototype.data=function(){this.options.get("debug")&&arguments.length>0&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2("data")`. You should consider setting the value instead using `$element.val()`.');var a=[];return this.dataAdapter.current(function(b){a=b}),a},e.prototype.val=function(b){if(this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("val")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==b||0===b.length)return this.$element.val();var c=b[0];a.isArray(c)&&(c=a.map(c,function(a){return a.toString()})),this.$element.val(c).trigger("change")},e.prototype.destroy=function(){this.$container.remove(),this.$element[0].detachEvent&&this.$element[0].detachEvent("onpropertychange",this._syncA),null!=this._observer?(this._observer.disconnect(),this._observer=null):this.$element[0].removeEventListener&&(this.$element[0].removeEventListener("DOMAttrModified",this._syncA,!1),this.$element[0].removeEventListener("DOMNodeInserted",this._syncS,!1),this.$element[0].removeEventListener("DOMNodeRemoved",this._syncS,!1)),this._syncA=null,this._syncS=null,this.$element.off(".select2"),this.$element.attr("tabindex",this.$element.data("old-tabindex")),this.$element.removeClass("select2-hidden-accessible"),this.$element.attr("aria-hidden","false"),this.$element.removeData("select2"),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null; +},e.prototype.render=function(){var b=a('');return b.attr("dir",this.options.get("dir")),this.$container=b,this.$container.addClass("select2-container--"+this.options.get("theme")),b.data("element",this.$element),b},e}),b.define("select2/compat/utils",["jquery"],function(a){function b(b,c,d){var e,f,g=[];e=a.trim(b.attr("class")),e&&(e=""+e,a(e.split(/\s+/)).each(function(){0===this.indexOf("select2-")&&g.push(this)})),e=a.trim(c.attr("class")),e&&(e=""+e,a(e.split(/\s+/)).each(function(){0!==this.indexOf("select2-")&&(f=d(this),null!=f&&g.push(f))})),b.attr("class",g.join(" "))}return{syncCssClasses:b}}),b.define("select2/compat/containerCss",["jquery","./utils"],function(a,b){function c(a){return null}function d(){}return d.prototype.render=function(d){var e=d.call(this),f=this.options.get("containerCssClass")||"";a.isFunction(f)&&(f=f(this.$element));var g=this.options.get("adaptContainerCssClass");if(g=g||c,-1!==f.indexOf(":all:")){f=f.replace(":all:","");var h=g;g=function(a){var b=h(a);return null!=b?b+" "+a:a}}var i=this.options.get("containerCss")||{};return a.isFunction(i)&&(i=i(this.$element)),b.syncCssClasses(e,this.$element,g),e.css(i),e.addClass(f),e},d}),b.define("select2/compat/dropdownCss",["jquery","./utils"],function(a,b){function c(a){return null}function d(){}return d.prototype.render=function(d){var e=d.call(this),f=this.options.get("dropdownCssClass")||"";a.isFunction(f)&&(f=f(this.$element));var g=this.options.get("adaptDropdownCssClass");if(g=g||c,-1!==f.indexOf(":all:")){f=f.replace(":all:","");var h=g;g=function(a){var b=h(a);return null!=b?b+" "+a:a}}var i=this.options.get("dropdownCss")||{};return a.isFunction(i)&&(i=i(this.$element)),b.syncCssClasses(e,this.$element,g),e.css(i),e.addClass(f),e},d}),b.define("select2/compat/initSelection",["jquery"],function(a){function b(a,b,c){c.get("debug")&&window.console&&console.warn&&console.warn("Select2: The `initSelection` option has been deprecated in favor of a custom data adapter that overrides the `current` method. This method is now called multiple times instead of a single time when the instance is initialized. Support will be removed for the `initSelection` option in future versions of Select2"),this.initSelection=c.get("initSelection"),this._isInitialized=!1,a.call(this,b,c)}return b.prototype.current=function(b,c){var d=this;return this._isInitialized?void b.call(this,c):void this.initSelection.call(null,this.$element,function(b){d._isInitialized=!0,a.isArray(b)||(b=[b]),c(b)})},b}),b.define("select2/compat/inputData",["jquery"],function(a){function b(a,b,c){this._currentData=[],this._valueSeparator=c.get("valueSeparator")||",","hidden"===b.prop("type")&&c.get("debug")&&console&&console.warn&&console.warn("Select2: Using a hidden input with Select2 is no longer supported and may stop working in the future. It is recommended to use a `' + + '' + ); + + this.$searchContainer = $search; + this.$search = $search.find('input'); + + var $rendered = decorated.call(this); + + this._transferTabIndex(); + + return $rendered; + }; + + Search.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('open', function () { + self.$search.trigger('focus'); + }); + + container.on('close', function () { + self.$search.val(''); + self.$search.removeAttr('aria-activedescendant'); + self.$search.trigger('focus'); + }); + + container.on('enable', function () { + self.$search.prop('disabled', false); + + self._transferTabIndex(); + }); + + container.on('disable', function () { + self.$search.prop('disabled', true); + }); + + container.on('focus', function (evt) { + self.$search.trigger('focus'); + }); + + container.on('results:focus', function (params) { + self.$search.attr('aria-activedescendant', params.id); + }); + + this.$selection.on('focusin', '.select2-search--inline', function (evt) { + self.trigger('focus', evt); + }); + + this.$selection.on('focusout', '.select2-search--inline', function (evt) { + self._handleBlur(evt); + }); + + this.$selection.on('keydown', '.select2-search--inline', function (evt) { + evt.stopPropagation(); + + self.trigger('keypress', evt); + + self._keyUpPrevented = evt.isDefaultPrevented(); + + var key = evt.which; + + if (key === KEYS.BACKSPACE && self.$search.val() === '') { + var $previousChoice = self.$searchContainer + .prev('.select2-selection__choice'); + + if ($previousChoice.length > 0) { + var item = $previousChoice.data('data'); + + self.searchRemoveChoice(item); + + evt.preventDefault(); + } + } + }); + + // Try to detect the IE version should the `documentMode` property that + // is stored on the document. This is only implemented in IE and is + // slightly cleaner than doing a user agent check. + // This property is not available in Edge, but Edge also doesn't have + // this bug. + var msie = document.documentMode; + var disableInputEvents = msie && msie <= 11; + + // Workaround for browsers which do not support the `input` event + // This will prevent double-triggering of events for browsers which support + // both the `keyup` and `input` events. + this.$selection.on( + 'input.searchcheck', + '.select2-search--inline', + function (evt) { + // IE will trigger the `input` event when a placeholder is used on a + // search box. To get around this issue, we are forced to ignore all + // `input` events in IE and keep using `keyup`. + if (disableInputEvents) { + self.$selection.off('input.search input.searchcheck'); + return; + } + + // Unbind the duplicated `keyup` event + self.$selection.off('keyup.search'); + } + ); + + this.$selection.on( + 'keyup.search input.search', + '.select2-search--inline', + function (evt) { + // IE will trigger the `input` event when a placeholder is used on a + // search box. To get around this issue, we are forced to ignore all + // `input` events in IE and keep using `keyup`. + if (disableInputEvents && evt.type === 'input') { + self.$selection.off('input.search input.searchcheck'); + return; + } + + var key = evt.which; + + // We can freely ignore events from modifier keys + if (key == KEYS.SHIFT || key == KEYS.CTRL || key == KEYS.ALT) { + return; + } + + // Tabbing will be handled during the `keydown` phase + if (key == KEYS.TAB) { + return; + } + + self.handleSearch(evt); + } + ); + }; + + /** + * This method will transfer the tabindex attribute from the rendered + * selection to the search box. This allows for the search box to be used as + * the primary focus instead of the selection container. + * + * @private + */ + Search.prototype._transferTabIndex = function (decorated) { + this.$search.attr('tabindex', this.$selection.attr('tabindex')); + this.$selection.attr('tabindex', '-1'); + }; + + Search.prototype.createPlaceholder = function (decorated, placeholder) { + this.$search.attr('placeholder', placeholder.text); + }; + + Search.prototype.update = function (decorated, data) { + var searchHadFocus = this.$search[0] == document.activeElement; + + this.$search.attr('placeholder', ''); + + decorated.call(this, data); + + this.$selection.find('.select2-selection__rendered') + .append(this.$searchContainer); + + this.resizeSearch(); + if (searchHadFocus) { + this.$search.focus(); + } + }; + + Search.prototype.handleSearch = function () { + this.resizeSearch(); + + if (!this._keyUpPrevented) { + var input = this.$search.val(); + + this.trigger('query', { + term: input + }); + } + + this._keyUpPrevented = false; + }; + + Search.prototype.searchRemoveChoice = function (decorated, item) { + this.trigger('unselect', { + data: item + }); + + this.$search.val(item.text); + this.handleSearch(); + }; + + Search.prototype.resizeSearch = function () { + this.$search.css('width', '25px'); + + var width = ''; + + if (this.$search.attr('placeholder') !== '') { + width = this.$selection.find('.select2-selection__rendered').innerWidth(); + } else { + var minimumWidth = this.$search.val().length + 1; + + width = (minimumWidth * 0.75) + 'em'; + } + + this.$search.css('width', width); + }; + + return Search; +}); + +S2.define('select2/selection/eventRelay',[ + 'jquery' +], function ($) { + function EventRelay () { } + + EventRelay.prototype.bind = function (decorated, container, $container) { + var self = this; + var relayEvents = [ + 'open', 'opening', + 'close', 'closing', + 'select', 'selecting', + 'unselect', 'unselecting' + ]; + + var preventableEvents = ['opening', 'closing', 'selecting', 'unselecting']; + + decorated.call(this, container, $container); + + container.on('*', function (name, params) { + // Ignore events that should not be relayed + if ($.inArray(name, relayEvents) === -1) { + return; + } + + // The parameters should always be an object + params = params || {}; + + // Generate the jQuery event for the Select2 event + var evt = $.Event('select2:' + name, { + params: params + }); + + self.$element.trigger(evt); + + // Only handle preventable events if it was one + if ($.inArray(name, preventableEvents) === -1) { + return; + } + + params.prevented = evt.isDefaultPrevented(); + }); + }; + + return EventRelay; +}); + +S2.define('select2/translation',[ + 'jquery', + 'require' +], function ($, require) { + function Translation (dict) { + this.dict = dict || {}; + } + + Translation.prototype.all = function () { + return this.dict; + }; + + Translation.prototype.get = function (key) { + return this.dict[key]; + }; + + Translation.prototype.extend = function (translation) { + this.dict = $.extend({}, translation.all(), this.dict); + }; + + // Static functions + + Translation._cache = {}; + + Translation.loadPath = function (path) { + if (!(path in Translation._cache)) { + var translations = require(path); + + Translation._cache[path] = translations; + } + + return new Translation(Translation._cache[path]); + }; + + return Translation; +}); + +S2.define('select2/diacritics',[ + +], function () { + var diacritics = { + '\u24B6': 'A', + '\uFF21': 'A', + '\u00C0': 'A', + '\u00C1': 'A', + '\u00C2': 'A', + '\u1EA6': 'A', + '\u1EA4': 'A', + '\u1EAA': 'A', + '\u1EA8': 'A', + '\u00C3': 'A', + '\u0100': 'A', + '\u0102': 'A', + '\u1EB0': 'A', + '\u1EAE': 'A', + '\u1EB4': 'A', + '\u1EB2': 'A', + '\u0226': 'A', + '\u01E0': 'A', + '\u00C4': 'A', + '\u01DE': 'A', + '\u1EA2': 'A', + '\u00C5': 'A', + '\u01FA': 'A', + '\u01CD': 'A', + '\u0200': 'A', + '\u0202': 'A', + '\u1EA0': 'A', + '\u1EAC': 'A', + '\u1EB6': 'A', + '\u1E00': 'A', + '\u0104': 'A', + '\u023A': 'A', + '\u2C6F': 'A', + '\uA732': 'AA', + '\u00C6': 'AE', + '\u01FC': 'AE', + '\u01E2': 'AE', + '\uA734': 'AO', + '\uA736': 'AU', + '\uA738': 'AV', + '\uA73A': 'AV', + '\uA73C': 'AY', + '\u24B7': 'B', + '\uFF22': 'B', + '\u1E02': 'B', + '\u1E04': 'B', + '\u1E06': 'B', + '\u0243': 'B', + '\u0182': 'B', + '\u0181': 'B', + '\u24B8': 'C', + '\uFF23': 'C', + '\u0106': 'C', + '\u0108': 'C', + '\u010A': 'C', + '\u010C': 'C', + '\u00C7': 'C', + '\u1E08': 'C', + '\u0187': 'C', + '\u023B': 'C', + '\uA73E': 'C', + '\u24B9': 'D', + '\uFF24': 'D', + '\u1E0A': 'D', + '\u010E': 'D', + '\u1E0C': 'D', + '\u1E10': 'D', + '\u1E12': 'D', + '\u1E0E': 'D', + '\u0110': 'D', + '\u018B': 'D', + '\u018A': 'D', + '\u0189': 'D', + '\uA779': 'D', + '\u01F1': 'DZ', + '\u01C4': 'DZ', + '\u01F2': 'Dz', + '\u01C5': 'Dz', + '\u24BA': 'E', + '\uFF25': 'E', + '\u00C8': 'E', + '\u00C9': 'E', + '\u00CA': 'E', + '\u1EC0': 'E', + '\u1EBE': 'E', + '\u1EC4': 'E', + '\u1EC2': 'E', + '\u1EBC': 'E', + '\u0112': 'E', + '\u1E14': 'E', + '\u1E16': 'E', + '\u0114': 'E', + '\u0116': 'E', + '\u00CB': 'E', + '\u1EBA': 'E', + '\u011A': 'E', + '\u0204': 'E', + '\u0206': 'E', + '\u1EB8': 'E', + '\u1EC6': 'E', + '\u0228': 'E', + '\u1E1C': 'E', + '\u0118': 'E', + '\u1E18': 'E', + '\u1E1A': 'E', + '\u0190': 'E', + '\u018E': 'E', + '\u24BB': 'F', + '\uFF26': 'F', + '\u1E1E': 'F', + '\u0191': 'F', + '\uA77B': 'F', + '\u24BC': 'G', + '\uFF27': 'G', + '\u01F4': 'G', + '\u011C': 'G', + '\u1E20': 'G', + '\u011E': 'G', + '\u0120': 'G', + '\u01E6': 'G', + '\u0122': 'G', + '\u01E4': 'G', + '\u0193': 'G', + '\uA7A0': 'G', + '\uA77D': 'G', + '\uA77E': 'G', + '\u24BD': 'H', + '\uFF28': 'H', + '\u0124': 'H', + '\u1E22': 'H', + '\u1E26': 'H', + '\u021E': 'H', + '\u1E24': 'H', + '\u1E28': 'H', + '\u1E2A': 'H', + '\u0126': 'H', + '\u2C67': 'H', + '\u2C75': 'H', + '\uA78D': 'H', + '\u24BE': 'I', + '\uFF29': 'I', + '\u00CC': 'I', + '\u00CD': 'I', + '\u00CE': 'I', + '\u0128': 'I', + '\u012A': 'I', + '\u012C': 'I', + '\u0130': 'I', + '\u00CF': 'I', + '\u1E2E': 'I', + '\u1EC8': 'I', + '\u01CF': 'I', + '\u0208': 'I', + '\u020A': 'I', + '\u1ECA': 'I', + '\u012E': 'I', + '\u1E2C': 'I', + '\u0197': 'I', + '\u24BF': 'J', + '\uFF2A': 'J', + '\u0134': 'J', + '\u0248': 'J', + '\u24C0': 'K', + '\uFF2B': 'K', + '\u1E30': 'K', + '\u01E8': 'K', + '\u1E32': 'K', + '\u0136': 'K', + '\u1E34': 'K', + '\u0198': 'K', + '\u2C69': 'K', + '\uA740': 'K', + '\uA742': 'K', + '\uA744': 'K', + '\uA7A2': 'K', + '\u24C1': 'L', + '\uFF2C': 'L', + '\u013F': 'L', + '\u0139': 'L', + '\u013D': 'L', + '\u1E36': 'L', + '\u1E38': 'L', + '\u013B': 'L', + '\u1E3C': 'L', + '\u1E3A': 'L', + '\u0141': 'L', + '\u023D': 'L', + '\u2C62': 'L', + '\u2C60': 'L', + '\uA748': 'L', + '\uA746': 'L', + '\uA780': 'L', + '\u01C7': 'LJ', + '\u01C8': 'Lj', + '\u24C2': 'M', + '\uFF2D': 'M', + '\u1E3E': 'M', + '\u1E40': 'M', + '\u1E42': 'M', + '\u2C6E': 'M', + '\u019C': 'M', + '\u24C3': 'N', + '\uFF2E': 'N', + '\u01F8': 'N', + '\u0143': 'N', + '\u00D1': 'N', + '\u1E44': 'N', + '\u0147': 'N', + '\u1E46': 'N', + '\u0145': 'N', + '\u1E4A': 'N', + '\u1E48': 'N', + '\u0220': 'N', + '\u019D': 'N', + '\uA790': 'N', + '\uA7A4': 'N', + '\u01CA': 'NJ', + '\u01CB': 'Nj', + '\u24C4': 'O', + '\uFF2F': 'O', + '\u00D2': 'O', + '\u00D3': 'O', + '\u00D4': 'O', + '\u1ED2': 'O', + '\u1ED0': 'O', + '\u1ED6': 'O', + '\u1ED4': 'O', + '\u00D5': 'O', + '\u1E4C': 'O', + '\u022C': 'O', + '\u1E4E': 'O', + '\u014C': 'O', + '\u1E50': 'O', + '\u1E52': 'O', + '\u014E': 'O', + '\u022E': 'O', + '\u0230': 'O', + '\u00D6': 'O', + '\u022A': 'O', + '\u1ECE': 'O', + '\u0150': 'O', + '\u01D1': 'O', + '\u020C': 'O', + '\u020E': 'O', + '\u01A0': 'O', + '\u1EDC': 'O', + '\u1EDA': 'O', + '\u1EE0': 'O', + '\u1EDE': 'O', + '\u1EE2': 'O', + '\u1ECC': 'O', + '\u1ED8': 'O', + '\u01EA': 'O', + '\u01EC': 'O', + '\u00D8': 'O', + '\u01FE': 'O', + '\u0186': 'O', + '\u019F': 'O', + '\uA74A': 'O', + '\uA74C': 'O', + '\u01A2': 'OI', + '\uA74E': 'OO', + '\u0222': 'OU', + '\u24C5': 'P', + '\uFF30': 'P', + '\u1E54': 'P', + '\u1E56': 'P', + '\u01A4': 'P', + '\u2C63': 'P', + '\uA750': 'P', + '\uA752': 'P', + '\uA754': 'P', + '\u24C6': 'Q', + '\uFF31': 'Q', + '\uA756': 'Q', + '\uA758': 'Q', + '\u024A': 'Q', + '\u24C7': 'R', + '\uFF32': 'R', + '\u0154': 'R', + '\u1E58': 'R', + '\u0158': 'R', + '\u0210': 'R', + '\u0212': 'R', + '\u1E5A': 'R', + '\u1E5C': 'R', + '\u0156': 'R', + '\u1E5E': 'R', + '\u024C': 'R', + '\u2C64': 'R', + '\uA75A': 'R', + '\uA7A6': 'R', + '\uA782': 'R', + '\u24C8': 'S', + '\uFF33': 'S', + '\u1E9E': 'S', + '\u015A': 'S', + '\u1E64': 'S', + '\u015C': 'S', + '\u1E60': 'S', + '\u0160': 'S', + '\u1E66': 'S', + '\u1E62': 'S', + '\u1E68': 'S', + '\u0218': 'S', + '\u015E': 'S', + '\u2C7E': 'S', + '\uA7A8': 'S', + '\uA784': 'S', + '\u24C9': 'T', + '\uFF34': 'T', + '\u1E6A': 'T', + '\u0164': 'T', + '\u1E6C': 'T', + '\u021A': 'T', + '\u0162': 'T', + '\u1E70': 'T', + '\u1E6E': 'T', + '\u0166': 'T', + '\u01AC': 'T', + '\u01AE': 'T', + '\u023E': 'T', + '\uA786': 'T', + '\uA728': 'TZ', + '\u24CA': 'U', + '\uFF35': 'U', + '\u00D9': 'U', + '\u00DA': 'U', + '\u00DB': 'U', + '\u0168': 'U', + '\u1E78': 'U', + '\u016A': 'U', + '\u1E7A': 'U', + '\u016C': 'U', + '\u00DC': 'U', + '\u01DB': 'U', + '\u01D7': 'U', + '\u01D5': 'U', + '\u01D9': 'U', + '\u1EE6': 'U', + '\u016E': 'U', + '\u0170': 'U', + '\u01D3': 'U', + '\u0214': 'U', + '\u0216': 'U', + '\u01AF': 'U', + '\u1EEA': 'U', + '\u1EE8': 'U', + '\u1EEE': 'U', + '\u1EEC': 'U', + '\u1EF0': 'U', + '\u1EE4': 'U', + '\u1E72': 'U', + '\u0172': 'U', + '\u1E76': 'U', + '\u1E74': 'U', + '\u0244': 'U', + '\u24CB': 'V', + '\uFF36': 'V', + '\u1E7C': 'V', + '\u1E7E': 'V', + '\u01B2': 'V', + '\uA75E': 'V', + '\u0245': 'V', + '\uA760': 'VY', + '\u24CC': 'W', + '\uFF37': 'W', + '\u1E80': 'W', + '\u1E82': 'W', + '\u0174': 'W', + '\u1E86': 'W', + '\u1E84': 'W', + '\u1E88': 'W', + '\u2C72': 'W', + '\u24CD': 'X', + '\uFF38': 'X', + '\u1E8A': 'X', + '\u1E8C': 'X', + '\u24CE': 'Y', + '\uFF39': 'Y', + '\u1EF2': 'Y', + '\u00DD': 'Y', + '\u0176': 'Y', + '\u1EF8': 'Y', + '\u0232': 'Y', + '\u1E8E': 'Y', + '\u0178': 'Y', + '\u1EF6': 'Y', + '\u1EF4': 'Y', + '\u01B3': 'Y', + '\u024E': 'Y', + '\u1EFE': 'Y', + '\u24CF': 'Z', + '\uFF3A': 'Z', + '\u0179': 'Z', + '\u1E90': 'Z', + '\u017B': 'Z', + '\u017D': 'Z', + '\u1E92': 'Z', + '\u1E94': 'Z', + '\u01B5': 'Z', + '\u0224': 'Z', + '\u2C7F': 'Z', + '\u2C6B': 'Z', + '\uA762': 'Z', + '\u24D0': 'a', + '\uFF41': 'a', + '\u1E9A': 'a', + '\u00E0': 'a', + '\u00E1': 'a', + '\u00E2': 'a', + '\u1EA7': 'a', + '\u1EA5': 'a', + '\u1EAB': 'a', + '\u1EA9': 'a', + '\u00E3': 'a', + '\u0101': 'a', + '\u0103': 'a', + '\u1EB1': 'a', + '\u1EAF': 'a', + '\u1EB5': 'a', + '\u1EB3': 'a', + '\u0227': 'a', + '\u01E1': 'a', + '\u00E4': 'a', + '\u01DF': 'a', + '\u1EA3': 'a', + '\u00E5': 'a', + '\u01FB': 'a', + '\u01CE': 'a', + '\u0201': 'a', + '\u0203': 'a', + '\u1EA1': 'a', + '\u1EAD': 'a', + '\u1EB7': 'a', + '\u1E01': 'a', + '\u0105': 'a', + '\u2C65': 'a', + '\u0250': 'a', + '\uA733': 'aa', + '\u00E6': 'ae', + '\u01FD': 'ae', + '\u01E3': 'ae', + '\uA735': 'ao', + '\uA737': 'au', + '\uA739': 'av', + '\uA73B': 'av', + '\uA73D': 'ay', + '\u24D1': 'b', + '\uFF42': 'b', + '\u1E03': 'b', + '\u1E05': 'b', + '\u1E07': 'b', + '\u0180': 'b', + '\u0183': 'b', + '\u0253': 'b', + '\u24D2': 'c', + '\uFF43': 'c', + '\u0107': 'c', + '\u0109': 'c', + '\u010B': 'c', + '\u010D': 'c', + '\u00E7': 'c', + '\u1E09': 'c', + '\u0188': 'c', + '\u023C': 'c', + '\uA73F': 'c', + '\u2184': 'c', + '\u24D3': 'd', + '\uFF44': 'd', + '\u1E0B': 'd', + '\u010F': 'd', + '\u1E0D': 'd', + '\u1E11': 'd', + '\u1E13': 'd', + '\u1E0F': 'd', + '\u0111': 'd', + '\u018C': 'd', + '\u0256': 'd', + '\u0257': 'd', + '\uA77A': 'd', + '\u01F3': 'dz', + '\u01C6': 'dz', + '\u24D4': 'e', + '\uFF45': 'e', + '\u00E8': 'e', + '\u00E9': 'e', + '\u00EA': 'e', + '\u1EC1': 'e', + '\u1EBF': 'e', + '\u1EC5': 'e', + '\u1EC3': 'e', + '\u1EBD': 'e', + '\u0113': 'e', + '\u1E15': 'e', + '\u1E17': 'e', + '\u0115': 'e', + '\u0117': 'e', + '\u00EB': 'e', + '\u1EBB': 'e', + '\u011B': 'e', + '\u0205': 'e', + '\u0207': 'e', + '\u1EB9': 'e', + '\u1EC7': 'e', + '\u0229': 'e', + '\u1E1D': 'e', + '\u0119': 'e', + '\u1E19': 'e', + '\u1E1B': 'e', + '\u0247': 'e', + '\u025B': 'e', + '\u01DD': 'e', + '\u24D5': 'f', + '\uFF46': 'f', + '\u1E1F': 'f', + '\u0192': 'f', + '\uA77C': 'f', + '\u24D6': 'g', + '\uFF47': 'g', + '\u01F5': 'g', + '\u011D': 'g', + '\u1E21': 'g', + '\u011F': 'g', + '\u0121': 'g', + '\u01E7': 'g', + '\u0123': 'g', + '\u01E5': 'g', + '\u0260': 'g', + '\uA7A1': 'g', + '\u1D79': 'g', + '\uA77F': 'g', + '\u24D7': 'h', + '\uFF48': 'h', + '\u0125': 'h', + '\u1E23': 'h', + '\u1E27': 'h', + '\u021F': 'h', + '\u1E25': 'h', + '\u1E29': 'h', + '\u1E2B': 'h', + '\u1E96': 'h', + '\u0127': 'h', + '\u2C68': 'h', + '\u2C76': 'h', + '\u0265': 'h', + '\u0195': 'hv', + '\u24D8': 'i', + '\uFF49': 'i', + '\u00EC': 'i', + '\u00ED': 'i', + '\u00EE': 'i', + '\u0129': 'i', + '\u012B': 'i', + '\u012D': 'i', + '\u00EF': 'i', + '\u1E2F': 'i', + '\u1EC9': 'i', + '\u01D0': 'i', + '\u0209': 'i', + '\u020B': 'i', + '\u1ECB': 'i', + '\u012F': 'i', + '\u1E2D': 'i', + '\u0268': 'i', + '\u0131': 'i', + '\u24D9': 'j', + '\uFF4A': 'j', + '\u0135': 'j', + '\u01F0': 'j', + '\u0249': 'j', + '\u24DA': 'k', + '\uFF4B': 'k', + '\u1E31': 'k', + '\u01E9': 'k', + '\u1E33': 'k', + '\u0137': 'k', + '\u1E35': 'k', + '\u0199': 'k', + '\u2C6A': 'k', + '\uA741': 'k', + '\uA743': 'k', + '\uA745': 'k', + '\uA7A3': 'k', + '\u24DB': 'l', + '\uFF4C': 'l', + '\u0140': 'l', + '\u013A': 'l', + '\u013E': 'l', + '\u1E37': 'l', + '\u1E39': 'l', + '\u013C': 'l', + '\u1E3D': 'l', + '\u1E3B': 'l', + '\u017F': 'l', + '\u0142': 'l', + '\u019A': 'l', + '\u026B': 'l', + '\u2C61': 'l', + '\uA749': 'l', + '\uA781': 'l', + '\uA747': 'l', + '\u01C9': 'lj', + '\u24DC': 'm', + '\uFF4D': 'm', + '\u1E3F': 'm', + '\u1E41': 'm', + '\u1E43': 'm', + '\u0271': 'm', + '\u026F': 'm', + '\u24DD': 'n', + '\uFF4E': 'n', + '\u01F9': 'n', + '\u0144': 'n', + '\u00F1': 'n', + '\u1E45': 'n', + '\u0148': 'n', + '\u1E47': 'n', + '\u0146': 'n', + '\u1E4B': 'n', + '\u1E49': 'n', + '\u019E': 'n', + '\u0272': 'n', + '\u0149': 'n', + '\uA791': 'n', + '\uA7A5': 'n', + '\u01CC': 'nj', + '\u24DE': 'o', + '\uFF4F': 'o', + '\u00F2': 'o', + '\u00F3': 'o', + '\u00F4': 'o', + '\u1ED3': 'o', + '\u1ED1': 'o', + '\u1ED7': 'o', + '\u1ED5': 'o', + '\u00F5': 'o', + '\u1E4D': 'o', + '\u022D': 'o', + '\u1E4F': 'o', + '\u014D': 'o', + '\u1E51': 'o', + '\u1E53': 'o', + '\u014F': 'o', + '\u022F': 'o', + '\u0231': 'o', + '\u00F6': 'o', + '\u022B': 'o', + '\u1ECF': 'o', + '\u0151': 'o', + '\u01D2': 'o', + '\u020D': 'o', + '\u020F': 'o', + '\u01A1': 'o', + '\u1EDD': 'o', + '\u1EDB': 'o', + '\u1EE1': 'o', + '\u1EDF': 'o', + '\u1EE3': 'o', + '\u1ECD': 'o', + '\u1ED9': 'o', + '\u01EB': 'o', + '\u01ED': 'o', + '\u00F8': 'o', + '\u01FF': 'o', + '\u0254': 'o', + '\uA74B': 'o', + '\uA74D': 'o', + '\u0275': 'o', + '\u01A3': 'oi', + '\u0223': 'ou', + '\uA74F': 'oo', + '\u24DF': 'p', + '\uFF50': 'p', + '\u1E55': 'p', + '\u1E57': 'p', + '\u01A5': 'p', + '\u1D7D': 'p', + '\uA751': 'p', + '\uA753': 'p', + '\uA755': 'p', + '\u24E0': 'q', + '\uFF51': 'q', + '\u024B': 'q', + '\uA757': 'q', + '\uA759': 'q', + '\u24E1': 'r', + '\uFF52': 'r', + '\u0155': 'r', + '\u1E59': 'r', + '\u0159': 'r', + '\u0211': 'r', + '\u0213': 'r', + '\u1E5B': 'r', + '\u1E5D': 'r', + '\u0157': 'r', + '\u1E5F': 'r', + '\u024D': 'r', + '\u027D': 'r', + '\uA75B': 'r', + '\uA7A7': 'r', + '\uA783': 'r', + '\u24E2': 's', + '\uFF53': 's', + '\u00DF': 's', + '\u015B': 's', + '\u1E65': 's', + '\u015D': 's', + '\u1E61': 's', + '\u0161': 's', + '\u1E67': 's', + '\u1E63': 's', + '\u1E69': 's', + '\u0219': 's', + '\u015F': 's', + '\u023F': 's', + '\uA7A9': 's', + '\uA785': 's', + '\u1E9B': 's', + '\u24E3': 't', + '\uFF54': 't', + '\u1E6B': 't', + '\u1E97': 't', + '\u0165': 't', + '\u1E6D': 't', + '\u021B': 't', + '\u0163': 't', + '\u1E71': 't', + '\u1E6F': 't', + '\u0167': 't', + '\u01AD': 't', + '\u0288': 't', + '\u2C66': 't', + '\uA787': 't', + '\uA729': 'tz', + '\u24E4': 'u', + '\uFF55': 'u', + '\u00F9': 'u', + '\u00FA': 'u', + '\u00FB': 'u', + '\u0169': 'u', + '\u1E79': 'u', + '\u016B': 'u', + '\u1E7B': 'u', + '\u016D': 'u', + '\u00FC': 'u', + '\u01DC': 'u', + '\u01D8': 'u', + '\u01D6': 'u', + '\u01DA': 'u', + '\u1EE7': 'u', + '\u016F': 'u', + '\u0171': 'u', + '\u01D4': 'u', + '\u0215': 'u', + '\u0217': 'u', + '\u01B0': 'u', + '\u1EEB': 'u', + '\u1EE9': 'u', + '\u1EEF': 'u', + '\u1EED': 'u', + '\u1EF1': 'u', + '\u1EE5': 'u', + '\u1E73': 'u', + '\u0173': 'u', + '\u1E77': 'u', + '\u1E75': 'u', + '\u0289': 'u', + '\u24E5': 'v', + '\uFF56': 'v', + '\u1E7D': 'v', + '\u1E7F': 'v', + '\u028B': 'v', + '\uA75F': 'v', + '\u028C': 'v', + '\uA761': 'vy', + '\u24E6': 'w', + '\uFF57': 'w', + '\u1E81': 'w', + '\u1E83': 'w', + '\u0175': 'w', + '\u1E87': 'w', + '\u1E85': 'w', + '\u1E98': 'w', + '\u1E89': 'w', + '\u2C73': 'w', + '\u24E7': 'x', + '\uFF58': 'x', + '\u1E8B': 'x', + '\u1E8D': 'x', + '\u24E8': 'y', + '\uFF59': 'y', + '\u1EF3': 'y', + '\u00FD': 'y', + '\u0177': 'y', + '\u1EF9': 'y', + '\u0233': 'y', + '\u1E8F': 'y', + '\u00FF': 'y', + '\u1EF7': 'y', + '\u1E99': 'y', + '\u1EF5': 'y', + '\u01B4': 'y', + '\u024F': 'y', + '\u1EFF': 'y', + '\u24E9': 'z', + '\uFF5A': 'z', + '\u017A': 'z', + '\u1E91': 'z', + '\u017C': 'z', + '\u017E': 'z', + '\u1E93': 'z', + '\u1E95': 'z', + '\u01B6': 'z', + '\u0225': 'z', + '\u0240': 'z', + '\u2C6C': 'z', + '\uA763': 'z', + '\u0386': '\u0391', + '\u0388': '\u0395', + '\u0389': '\u0397', + '\u038A': '\u0399', + '\u03AA': '\u0399', + '\u038C': '\u039F', + '\u038E': '\u03A5', + '\u03AB': '\u03A5', + '\u038F': '\u03A9', + '\u03AC': '\u03B1', + '\u03AD': '\u03B5', + '\u03AE': '\u03B7', + '\u03AF': '\u03B9', + '\u03CA': '\u03B9', + '\u0390': '\u03B9', + '\u03CC': '\u03BF', + '\u03CD': '\u03C5', + '\u03CB': '\u03C5', + '\u03B0': '\u03C5', + '\u03C9': '\u03C9', + '\u03C2': '\u03C3' + }; + + return diacritics; +}); + +S2.define('select2/data/base',[ + '../utils' +], function (Utils) { + function BaseAdapter ($element, options) { + BaseAdapter.__super__.constructor.call(this); + } + + Utils.Extend(BaseAdapter, Utils.Observable); + + BaseAdapter.prototype.current = function (callback) { + throw new Error('The `current` method must be defined in child classes.'); + }; + + BaseAdapter.prototype.query = function (params, callback) { + throw new Error('The `query` method must be defined in child classes.'); + }; + + BaseAdapter.prototype.bind = function (container, $container) { + // Can be implemented in subclasses + }; + + BaseAdapter.prototype.destroy = function () { + // Can be implemented in subclasses + }; + + BaseAdapter.prototype.generateResultId = function (container, data) { + var id = container.id + '-result-'; + + id += Utils.generateChars(4); + + if (data.id != null) { + id += '-' + data.id.toString(); + } else { + id += '-' + Utils.generateChars(4); + } + return id; + }; + + return BaseAdapter; +}); + +S2.define('select2/data/select',[ + './base', + '../utils', + 'jquery' +], function (BaseAdapter, Utils, $) { + function SelectAdapter ($element, options) { + this.$element = $element; + this.options = options; + + SelectAdapter.__super__.constructor.call(this); + } + + Utils.Extend(SelectAdapter, BaseAdapter); + + SelectAdapter.prototype.current = function (callback) { + var data = []; + var self = this; + + this.$element.find(':selected').each(function () { + var $option = $(this); + + var option = self.item($option); + + data.push(option); + }); + + callback(data); + }; + + SelectAdapter.prototype.select = function (data) { + var self = this; + + data.selected = true; + + // If data.element is a DOM node, use it instead + if ($(data.element).is('option')) { + data.element.selected = true; + + this.$element.trigger('change'); + + return; + } + + if (this.$element.prop('multiple')) { + this.current(function (currentData) { + var val = []; + + data = [data]; + data.push.apply(data, currentData); + + for (var d = 0; d < data.length; d++) { + var id = data[d].id; + + if ($.inArray(id, val) === -1) { + val.push(id); + } + } + + self.$element.val(val); + self.$element.trigger('change'); + }); + } else { + var val = data.id; + + this.$element.val(val); + this.$element.trigger('change'); + } + }; + + SelectAdapter.prototype.unselect = function (data) { + var self = this; + + if (!this.$element.prop('multiple')) { + return; + } + + data.selected = false; + + if ($(data.element).is('option')) { + data.element.selected = false; + + this.$element.trigger('change'); + + return; + } + + this.current(function (currentData) { + var val = []; + + for (var d = 0; d < currentData.length; d++) { + var id = currentData[d].id; + + if (id !== data.id && $.inArray(id, val) === -1) { + val.push(id); + } + } + + self.$element.val(val); + + self.$element.trigger('change'); + }); + }; + + SelectAdapter.prototype.bind = function (container, $container) { + var self = this; + + this.container = container; + + container.on('select', function (params) { + self.select(params.data); + }); + + container.on('unselect', function (params) { + self.unselect(params.data); + }); + }; + + SelectAdapter.prototype.destroy = function () { + // Remove anything added to child elements + this.$element.find('*').each(function () { + // Remove any custom data set by Select2 + $.removeData(this, 'data'); + }); + }; + + SelectAdapter.prototype.query = function (params, callback) { + var data = []; + var self = this; + + var $options = this.$element.children(); + + $options.each(function () { + var $option = $(this); + + if (!$option.is('option') && !$option.is('optgroup')) { + return; + } + + var option = self.item($option); + + var matches = self.matches(params, option); + + if (matches !== null) { + data.push(matches); + } + }); + + callback({ + results: data + }); + }; + + SelectAdapter.prototype.addOptions = function ($options) { + Utils.appendMany(this.$element, $options); + }; + + SelectAdapter.prototype.option = function (data) { + var option; + + if (data.children) { + option = document.createElement('optgroup'); + option.label = data.text; + } else { + option = document.createElement('option'); + + if (option.textContent !== undefined) { + option.textContent = data.text; + } else { + option.innerText = data.text; + } + } + + if (data.id) { + option.value = data.id; + } + + if (data.disabled) { + option.disabled = true; + } + + if (data.selected) { + option.selected = true; + } + + if (data.title) { + option.title = data.title; + } + + var $option = $(option); + + var normalizedData = this._normalizeItem(data); + normalizedData.element = option; + + // Override the option's data with the combined data + $.data(option, 'data', normalizedData); + + return $option; + }; + + SelectAdapter.prototype.item = function ($option) { + var data = {}; + + data = $.data($option[0], 'data'); + + if (data != null) { + return data; + } + + if ($option.is('option')) { + data = { + id: $option.val(), + text: $option.text(), + disabled: $option.prop('disabled'), + selected: $option.prop('selected'), + title: $option.prop('title') + }; + } else if ($option.is('optgroup')) { + data = { + text: $option.prop('label'), + children: [], + title: $option.prop('title') + }; + + var $children = $option.children('option'); + var children = []; + + for (var c = 0; c < $children.length; c++) { + var $child = $($children[c]); + + var child = this.item($child); + + children.push(child); + } + + data.children = children; + } + + data = this._normalizeItem(data); + data.element = $option[0]; + + $.data($option[0], 'data', data); + + return data; + }; + + SelectAdapter.prototype._normalizeItem = function (item) { + if (!$.isPlainObject(item)) { + item = { + id: item, + text: item + }; + } + + item = $.extend({}, { + text: '' + }, item); + + var defaults = { + selected: false, + disabled: false + }; + + if (item.id != null) { + item.id = item.id.toString(); + } + + if (item.text != null) { + item.text = item.text.toString(); + } + + if (item._resultId == null && item.id && this.container != null) { + item._resultId = this.generateResultId(this.container, item); + } + + return $.extend({}, defaults, item); + }; + + SelectAdapter.prototype.matches = function (params, data) { + var matcher = this.options.get('matcher'); + + return matcher(params, data); + }; + + return SelectAdapter; +}); + +S2.define('select2/data/array',[ + './select', + '../utils', + 'jquery' +], function (SelectAdapter, Utils, $) { + function ArrayAdapter ($element, options) { + var data = options.get('data') || []; + + ArrayAdapter.__super__.constructor.call(this, $element, options); + + this.addOptions(this.convertToOptions(data)); + } + + Utils.Extend(ArrayAdapter, SelectAdapter); + + ArrayAdapter.prototype.select = function (data) { + var $option = this.$element.find('option').filter(function (i, elm) { + return elm.value == data.id.toString(); + }); + + if ($option.length === 0) { + $option = this.option(data); + + this.addOptions($option); + } + + ArrayAdapter.__super__.select.call(this, data); + }; + + ArrayAdapter.prototype.convertToOptions = function (data) { + var self = this; + + var $existing = this.$element.find('option'); + var existingIds = $existing.map(function () { + return self.item($(this)).id; + }).get(); + + var $options = []; + + // Filter out all items except for the one passed in the argument + function onlyItem (item) { + return function () { + return $(this).val() == item.id; + }; + } + + for (var d = 0; d < data.length; d++) { + var item = this._normalizeItem(data[d]); + + // Skip items which were pre-loaded, only merge the data + if ($.inArray(item.id, existingIds) >= 0) { + var $existingOption = $existing.filter(onlyItem(item)); + + var existingData = this.item($existingOption); + var newData = $.extend(true, {}, item, existingData); + + var $newOption = this.option(newData); + + $existingOption.replaceWith($newOption); + + continue; + } + + var $option = this.option(item); + + if (item.children) { + var $children = this.convertToOptions(item.children); + + Utils.appendMany($option, $children); + } + + $options.push($option); + } + + return $options; + }; + + return ArrayAdapter; +}); + +S2.define('select2/data/ajax',[ + './array', + '../utils', + 'jquery' +], function (ArrayAdapter, Utils, $) { + function AjaxAdapter ($element, options) { + this.ajaxOptions = this._applyDefaults(options.get('ajax')); + + if (this.ajaxOptions.processResults != null) { + this.processResults = this.ajaxOptions.processResults; + } + + AjaxAdapter.__super__.constructor.call(this, $element, options); + } + + Utils.Extend(AjaxAdapter, ArrayAdapter); + + AjaxAdapter.prototype._applyDefaults = function (options) { + var defaults = { + data: function (params) { + return $.extend({}, params, { + q: params.term + }); + }, + transport: function (params, success, failure) { + var $request = $.ajax(params); + + $request.then(success); + $request.fail(failure); + + return $request; + } + }; + + return $.extend({}, defaults, options, true); + }; + + AjaxAdapter.prototype.processResults = function (results) { + return results; + }; + + AjaxAdapter.prototype.query = function (params, callback) { + var matches = []; + var self = this; + + if (this._request != null) { + // JSONP requests cannot always be aborted + if ($.isFunction(this._request.abort)) { + this._request.abort(); + } + + this._request = null; + } + + var options = $.extend({ + type: 'GET' + }, this.ajaxOptions); + + if (typeof options.url === 'function') { + options.url = options.url.call(this.$element, params); + } + + if (typeof options.data === 'function') { + options.data = options.data.call(this.$element, params); + } + + function request () { + var $request = options.transport(options, function (data) { + var results = self.processResults(data, params); + + if (self.options.get('debug') && window.console && console.error) { + // Check to make sure that the response included a `results` key. + if (!results || !results.results || !$.isArray(results.results)) { + console.error( + 'Select2: The AJAX results did not return an array in the ' + + '`results` key of the response.' + ); + } + } + + callback(results); + }, function () { + // Attempt to detect if a request was aborted + // Only works if the transport exposes a status property + if ($request.status && $request.status === '0') { + return; + } + + self.trigger('results:message', { + message: 'errorLoading' + }); + }); + + self._request = $request; + } + + if (this.ajaxOptions.delay && params.term != null) { + if (this._queryTimeout) { + window.clearTimeout(this._queryTimeout); + } + + this._queryTimeout = window.setTimeout(request, this.ajaxOptions.delay); + } else { + request(); + } + }; + + return AjaxAdapter; +}); + +S2.define('select2/data/tags',[ + 'jquery' +], function ($) { + function Tags (decorated, $element, options) { + var tags = options.get('tags'); + + var createTag = options.get('createTag'); + + if (createTag !== undefined) { + this.createTag = createTag; + } + + var insertTag = options.get('insertTag'); + + if (insertTag !== undefined) { + this.insertTag = insertTag; + } + + decorated.call(this, $element, options); + + if ($.isArray(tags)) { + for (var t = 0; t < tags.length; t++) { + var tag = tags[t]; + var item = this._normalizeItem(tag); + + var $option = this.option(item); + + this.$element.append($option); + } + } + } + + Tags.prototype.query = function (decorated, params, callback) { + var self = this; + + this._removeOldTags(); + + if (params.term == null || params.page != null) { + decorated.call(this, params, callback); + return; + } + + function wrapper (obj, child) { + var data = obj.results; + + for (var i = 0; i < data.length; i++) { + var option = data[i]; + + var checkChildren = ( + option.children != null && + !wrapper({ + results: option.children + }, true) + ); + + var checkText = option.text === params.term; + + if (checkText || checkChildren) { + if (child) { + return false; + } + + obj.data = data; + callback(obj); + + return; + } + } + + if (child) { + return true; + } + + var tag = self.createTag(params); + + if (tag != null) { + var $option = self.option(tag); + $option.attr('data-select2-tag', true); + + self.addOptions([$option]); + + self.insertTag(data, tag); + } + + obj.results = data; + + callback(obj); + } + + decorated.call(this, params, wrapper); + }; + + Tags.prototype.createTag = function (decorated, params) { + var term = $.trim(params.term); + + if (term === '') { + return null; + } + + return { + id: term, + text: term + }; + }; + + Tags.prototype.insertTag = function (_, data, tag) { + data.unshift(tag); + }; + + Tags.prototype._removeOldTags = function (_) { + var tag = this._lastTag; + + var $options = this.$element.find('option[data-select2-tag]'); + + $options.each(function () { + if (this.selected) { + return; + } + + $(this).remove(); + }); + }; + + return Tags; +}); + +S2.define('select2/data/tokenizer',[ + 'jquery' +], function ($) { + function Tokenizer (decorated, $element, options) { + var tokenizer = options.get('tokenizer'); + + if (tokenizer !== undefined) { + this.tokenizer = tokenizer; + } + + decorated.call(this, $element, options); + } + + Tokenizer.prototype.bind = function (decorated, container, $container) { + decorated.call(this, container, $container); + + this.$search = container.dropdown.$search || container.selection.$search || + $container.find('.select2-search__field'); + }; + + Tokenizer.prototype.query = function (decorated, params, callback) { + var self = this; + + function createAndSelect (data) { + // Normalize the data object so we can use it for checks + var item = self._normalizeItem(data); + + // Check if the data object already exists as a tag + // Select it if it doesn't + var $existingOptions = self.$element.find('option').filter(function () { + return $(this).val() === item.id; + }); + + // If an existing option wasn't found for it, create the option + if (!$existingOptions.length) { + var $option = self.option(item); + $option.attr('data-select2-tag', true); + + self._removeOldTags(); + self.addOptions([$option]); + } + + // Select the item, now that we know there is an option for it + select(item); + } + + function select (data) { + self.trigger('select', { + data: data + }); + } + + params.term = params.term || ''; + + var tokenData = this.tokenizer(params, this.options, createAndSelect); + + if (tokenData.term !== params.term) { + // Replace the search term if we have the search box + if (this.$search.length) { + this.$search.val(tokenData.term); + this.$search.focus(); + } + + params.term = tokenData.term; + } + + decorated.call(this, params, callback); + }; + + Tokenizer.prototype.tokenizer = function (_, params, options, callback) { + var separators = options.get('tokenSeparators') || []; + var term = params.term; + var i = 0; + + var createTag = this.createTag || function (params) { + return { + id: params.term, + text: params.term + }; + }; + + while (i < term.length) { + var termChar = term[i]; + + if ($.inArray(termChar, separators) === -1) { + i++; + + continue; + } + + var part = term.substr(0, i); + var partParams = $.extend({}, params, { + term: part + }); + + var data = createTag(partParams); + + if (data == null) { + i++; + continue; + } + + callback(data); + + // Reset the term to not include the tokenized portion + term = term.substr(i + 1) || ''; + i = 0; + } + + return { + term: term + }; + }; + + return Tokenizer; +}); + +S2.define('select2/data/minimumInputLength',[ + +], function () { + function MinimumInputLength (decorated, $e, options) { + this.minimumInputLength = options.get('minimumInputLength'); + + decorated.call(this, $e, options); + } + + MinimumInputLength.prototype.query = function (decorated, params, callback) { + params.term = params.term || ''; + + if (params.term.length < this.minimumInputLength) { + this.trigger('results:message', { + message: 'inputTooShort', + args: { + minimum: this.minimumInputLength, + input: params.term, + params: params + } + }); + + return; + } + + decorated.call(this, params, callback); + }; + + return MinimumInputLength; +}); + +S2.define('select2/data/maximumInputLength',[ + +], function () { + function MaximumInputLength (decorated, $e, options) { + this.maximumInputLength = options.get('maximumInputLength'); + + decorated.call(this, $e, options); + } + + MaximumInputLength.prototype.query = function (decorated, params, callback) { + params.term = params.term || ''; + + if (this.maximumInputLength > 0 && + params.term.length > this.maximumInputLength) { + this.trigger('results:message', { + message: 'inputTooLong', + args: { + maximum: this.maximumInputLength, + input: params.term, + params: params + } + }); + + return; + } + + decorated.call(this, params, callback); + }; + + return MaximumInputLength; +}); + +S2.define('select2/data/maximumSelectionLength',[ + +], function (){ + function MaximumSelectionLength (decorated, $e, options) { + this.maximumSelectionLength = options.get('maximumSelectionLength'); + + decorated.call(this, $e, options); + } + + MaximumSelectionLength.prototype.query = + function (decorated, params, callback) { + var self = this; + + this.current(function (currentData) { + var count = currentData != null ? currentData.length : 0; + if (self.maximumSelectionLength > 0 && + count >= self.maximumSelectionLength) { + self.trigger('results:message', { + message: 'maximumSelected', + args: { + maximum: self.maximumSelectionLength + } + }); + return; + } + decorated.call(self, params, callback); + }); + }; + + return MaximumSelectionLength; +}); + +S2.define('select2/dropdown',[ + 'jquery', + './utils' +], function ($, Utils) { + function Dropdown ($element, options) { + this.$element = $element; + this.options = options; + + Dropdown.__super__.constructor.call(this); + } + + Utils.Extend(Dropdown, Utils.Observable); + + Dropdown.prototype.render = function () { + var $dropdown = $( + '' + + '' + + '' + ); + + $dropdown.attr('dir', this.options.get('dir')); + + this.$dropdown = $dropdown; + + return $dropdown; + }; + + Dropdown.prototype.bind = function () { + // Should be implemented in subclasses + }; + + Dropdown.prototype.position = function ($dropdown, $container) { + // Should be implmented in subclasses + }; + + Dropdown.prototype.destroy = function () { + // Remove the dropdown from the DOM + this.$dropdown.remove(); + }; + + return Dropdown; +}); + +S2.define('select2/dropdown/search',[ + 'jquery', + '../utils' +], function ($, Utils) { + function Search () { } + + Search.prototype.render = function (decorated) { + var $rendered = decorated.call(this); + + var $search = $( + '' + + '' + + '' + ); + + this.$searchContainer = $search; + this.$search = $search.find('input'); + + $rendered.prepend($search); + + return $rendered; + }; + + Search.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + this.$search.on('keydown', function (evt) { + self.trigger('keypress', evt); + + self._keyUpPrevented = evt.isDefaultPrevented(); + }); + + // Workaround for browsers which do not support the `input` event + // This will prevent double-triggering of events for browsers which support + // both the `keyup` and `input` events. + this.$search.on('input', function (evt) { + // Unbind the duplicated `keyup` event + $(this).off('keyup'); + }); + + this.$search.on('keyup input', function (evt) { + self.handleSearch(evt); + }); + + container.on('open', function () { + self.$search.attr('tabindex', 0); + + self.$search.focus(); + + window.setTimeout(function () { + self.$search.focus(); + }, 0); + }); + + container.on('close', function () { + self.$search.attr('tabindex', -1); + + self.$search.val(''); + }); + + container.on('focus', function () { + if (container.isOpen()) { + self.$search.focus(); + } + }); + + container.on('results:all', function (params) { + if (params.query.term == null || params.query.term === '') { + var showSearch = self.showSearch(params); + + if (showSearch) { + self.$searchContainer.removeClass('select2-search--hide'); + } else { + self.$searchContainer.addClass('select2-search--hide'); + } + } + }); + }; + + Search.prototype.handleSearch = function (evt) { + if (!this._keyUpPrevented) { + var input = this.$search.val(); + + this.trigger('query', { + term: input + }); + } + + this._keyUpPrevented = false; + }; + + Search.prototype.showSearch = function (_, params) { + return true; + }; + + return Search; +}); + +S2.define('select2/dropdown/hidePlaceholder',[ + +], function () { + function HidePlaceholder (decorated, $element, options, dataAdapter) { + this.placeholder = this.normalizePlaceholder(options.get('placeholder')); + + decorated.call(this, $element, options, dataAdapter); + } + + HidePlaceholder.prototype.append = function (decorated, data) { + data.results = this.removePlaceholder(data.results); + + decorated.call(this, data); + }; + + HidePlaceholder.prototype.normalizePlaceholder = function (_, placeholder) { + if (typeof placeholder === 'string') { + placeholder = { + id: '', + text: placeholder + }; + } + + return placeholder; + }; + + HidePlaceholder.prototype.removePlaceholder = function (_, data) { + var modifiedData = data.slice(0); + + for (var d = data.length - 1; d >= 0; d--) { + var item = data[d]; + + if (this.placeholder.id === item.id) { + modifiedData.splice(d, 1); + } + } + + return modifiedData; + }; + + return HidePlaceholder; +}); + +S2.define('select2/dropdown/infiniteScroll',[ + 'jquery' +], function ($) { + function InfiniteScroll (decorated, $element, options, dataAdapter) { + this.lastParams = {}; + + decorated.call(this, $element, options, dataAdapter); + + this.$loadingMore = this.createLoadingMore(); + this.loading = false; + } + + InfiniteScroll.prototype.append = function (decorated, data) { + this.$loadingMore.remove(); + this.loading = false; + + decorated.call(this, data); + + if (this.showLoadingMore(data)) { + this.$results.append(this.$loadingMore); + } + }; + + InfiniteScroll.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('query', function (params) { + self.lastParams = params; + self.loading = true; + }); + + container.on('query:append', function (params) { + self.lastParams = params; + self.loading = true; + }); + + this.$results.on('scroll', function () { + var isLoadMoreVisible = $.contains( + document.documentElement, + self.$loadingMore[0] + ); + + if (self.loading || !isLoadMoreVisible) { + return; + } + + var currentOffset = self.$results.offset().top + + self.$results.outerHeight(false); + var loadingMoreOffset = self.$loadingMore.offset().top + + self.$loadingMore.outerHeight(false); + + if (currentOffset + 50 >= loadingMoreOffset) { + self.loadMore(); + } + }); + }; + + InfiniteScroll.prototype.loadMore = function () { + this.loading = true; + + var params = $.extend({}, {page: 1}, this.lastParams); + + params.page++; + + this.trigger('query:append', params); + }; + + InfiniteScroll.prototype.showLoadingMore = function (_, data) { + return data.pagination && data.pagination.more; + }; + + InfiniteScroll.prototype.createLoadingMore = function () { + var $option = $( + '
        • ' + ); + + var message = this.options.get('translations').get('loadingMore'); + + $option.html(message(this.lastParams)); + + return $option; + }; + + return InfiniteScroll; +}); + +S2.define('select2/dropdown/attachBody',[ + 'jquery', + '../utils' +], function ($, Utils) { + function AttachBody (decorated, $element, options) { + this.$dropdownParent = options.get('dropdownParent') || $(document.body); + + decorated.call(this, $element, options); + } + + AttachBody.prototype.bind = function (decorated, container, $container) { + var self = this; + + var setupResultsEvents = false; + + decorated.call(this, container, $container); + + container.on('open', function () { + self._showDropdown(); + self._attachPositioningHandler(container); + + if (!setupResultsEvents) { + setupResultsEvents = true; + + container.on('results:all', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + container.on('results:append', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + } + }); + + container.on('close', function () { + self._hideDropdown(); + self._detachPositioningHandler(container); + }); + + this.$dropdownContainer.on('mousedown', function (evt) { + evt.stopPropagation(); + }); + }; + + AttachBody.prototype.destroy = function (decorated) { + decorated.call(this); + + this.$dropdownContainer.remove(); + }; + + AttachBody.prototype.position = function (decorated, $dropdown, $container) { + // Clone all of the container classes + $dropdown.attr('class', $container.attr('class')); + + $dropdown.removeClass('select2'); + $dropdown.addClass('select2-container--open'); + + $dropdown.css({ + position: 'absolute', + top: -999999 + }); + + this.$container = $container; + }; + + AttachBody.prototype.render = function (decorated) { + var $container = $(''); + + var $dropdown = decorated.call(this); + $container.append($dropdown); + + this.$dropdownContainer = $container; + + return $container; + }; + + AttachBody.prototype._hideDropdown = function (decorated) { + this.$dropdownContainer.detach(); + }; + + AttachBody.prototype._attachPositioningHandler = + function (decorated, container) { + var self = this; + + var scrollEvent = 'scroll.select2.' + container.id; + var resizeEvent = 'resize.select2.' + container.id; + var orientationEvent = 'orientationchange.select2.' + container.id; + + var $watchers = this.$container.parents().filter(Utils.hasScroll); + $watchers.each(function () { + $(this).data('select2-scroll-position', { + x: $(this).scrollLeft(), + y: $(this).scrollTop() + }); + }); + + $watchers.on(scrollEvent, function (ev) { + var position = $(this).data('select2-scroll-position'); + $(this).scrollTop(position.y); + }); + + $(window).on(scrollEvent + ' ' + resizeEvent + ' ' + orientationEvent, + function (e) { + self._positionDropdown(); + self._resizeDropdown(); + }); + }; + + AttachBody.prototype._detachPositioningHandler = + function (decorated, container) { + var scrollEvent = 'scroll.select2.' + container.id; + var resizeEvent = 'resize.select2.' + container.id; + var orientationEvent = 'orientationchange.select2.' + container.id; + + var $watchers = this.$container.parents().filter(Utils.hasScroll); + $watchers.off(scrollEvent); + + $(window).off(scrollEvent + ' ' + resizeEvent + ' ' + orientationEvent); + }; + + AttachBody.prototype._positionDropdown = function () { + var $window = $(window); + + var isCurrentlyAbove = this.$dropdown.hasClass('select2-dropdown--above'); + var isCurrentlyBelow = this.$dropdown.hasClass('select2-dropdown--below'); + + var newDirection = null; + + var offset = this.$container.offset(); + + offset.bottom = offset.top + this.$container.outerHeight(false); + + var container = { + height: this.$container.outerHeight(false) + }; + + container.top = offset.top; + container.bottom = offset.top + container.height; + + var dropdown = { + height: this.$dropdown.outerHeight(false) + }; + + var viewport = { + top: $window.scrollTop(), + bottom: $window.scrollTop() + $window.height() + }; + + var enoughRoomAbove = viewport.top < (offset.top - dropdown.height); + var enoughRoomBelow = viewport.bottom > (offset.bottom + dropdown.height); + + var css = { + left: offset.left, + top: container.bottom + }; + + // Determine what the parent element is to use for calciulating the offset + var $offsetParent = this.$dropdownParent; + + // For statically positoned elements, we need to get the element + // that is determining the offset + if ($offsetParent.css('position') === 'static') { + $offsetParent = $offsetParent.offsetParent(); + } + + var parentOffset = $offsetParent.offset(); + + css.top -= parentOffset.top; + css.left -= parentOffset.left; + + if (!isCurrentlyAbove && !isCurrentlyBelow) { + newDirection = 'below'; + } + + if (!enoughRoomBelow && enoughRoomAbove && !isCurrentlyAbove) { + newDirection = 'above'; + } else if (!enoughRoomAbove && enoughRoomBelow && isCurrentlyAbove) { + newDirection = 'below'; + } + + if (newDirection == 'above' || + (isCurrentlyAbove && newDirection !== 'below')) { + css.top = container.top - parentOffset.top - dropdown.height; + } + + if (newDirection != null) { + this.$dropdown + .removeClass('select2-dropdown--below select2-dropdown--above') + .addClass('select2-dropdown--' + newDirection); + this.$container + .removeClass('select2-container--below select2-container--above') + .addClass('select2-container--' + newDirection); + } + + this.$dropdownContainer.css(css); + }; + + AttachBody.prototype._resizeDropdown = function () { + var css = { + width: this.$container.outerWidth(false) + 'px' + }; + + if (this.options.get('dropdownAutoWidth')) { + css.minWidth = css.width; + css.position = 'relative'; + css.width = 'auto'; + } + + this.$dropdown.css(css); + }; + + AttachBody.prototype._showDropdown = function (decorated) { + this.$dropdownContainer.appendTo(this.$dropdownParent); + + this._positionDropdown(); + this._resizeDropdown(); + }; + + return AttachBody; +}); + +S2.define('select2/dropdown/minimumResultsForSearch',[ + +], function () { + function countResults (data) { + var count = 0; + + for (var d = 0; d < data.length; d++) { + var item = data[d]; + + if (item.children) { + count += countResults(item.children); + } else { + count++; + } + } + + return count; + } + + function MinimumResultsForSearch (decorated, $element, options, dataAdapter) { + this.minimumResultsForSearch = options.get('minimumResultsForSearch'); + + if (this.minimumResultsForSearch < 0) { + this.minimumResultsForSearch = Infinity; + } + + decorated.call(this, $element, options, dataAdapter); + } + + MinimumResultsForSearch.prototype.showSearch = function (decorated, params) { + if (countResults(params.data.results) < this.minimumResultsForSearch) { + return false; + } + + return decorated.call(this, params); + }; + + return MinimumResultsForSearch; +}); + +S2.define('select2/dropdown/selectOnClose',[ + +], function () { + function SelectOnClose () { } + + SelectOnClose.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('close', function (params) { + self._handleSelectOnClose(params); + }); + }; + + SelectOnClose.prototype._handleSelectOnClose = function (_, params) { + if (params && params.originalSelect2Event != null) { + var event = params.originalSelect2Event; + + // Don't select an item if the close event was triggered from a select or + // unselect event + if (event._type === 'select' || event._type === 'unselect') { + return; + } + } + + var $highlightedResults = this.getHighlightedResults(); + + // Only select highlighted results + if ($highlightedResults.length < 1) { + return; + } + + var data = $highlightedResults.data('data'); + + // Don't re-select already selected resulte + if ( + (data.element != null && data.element.selected) || + (data.element == null && data.selected) + ) { + return; + } + + this.trigger('select', { + data: data + }); + }; + + return SelectOnClose; +}); + +S2.define('select2/dropdown/closeOnSelect',[ + +], function () { + function CloseOnSelect () { } + + CloseOnSelect.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('select', function (evt) { + self._selectTriggered(evt); + }); + + container.on('unselect', function (evt) { + self._selectTriggered(evt); + }); + }; + + CloseOnSelect.prototype._selectTriggered = function (_, evt) { + var originalEvent = evt.originalEvent; + + // Don't close if the control key is being held + if (originalEvent && originalEvent.ctrlKey) { + return; + } + + this.trigger('close', { + originalEvent: originalEvent, + originalSelect2Event: evt + }); + }; + + return CloseOnSelect; +}); + +S2.define('select2/i18n/en',[],function () { + // English + return { + errorLoading: function () { + return 'The results could not be loaded.'; + }, + inputTooLong: function (args) { + var overChars = args.input.length - args.maximum; + + var message = 'Please delete ' + overChars + ' character'; + + if (overChars != 1) { + message += 's'; + } + + return message; + }, + inputTooShort: function (args) { + var remainingChars = args.minimum - args.input.length; + + var message = 'Please enter ' + remainingChars + ' or more characters'; + + return message; + }, + loadingMore: function () { + return 'Loading more results…'; + }, + maximumSelected: function (args) { + var message = 'You can only select ' + args.maximum + ' item'; + + if (args.maximum != 1) { + message += 's'; + } + + return message; + }, + noResults: function () { + return 'No results found'; + }, + searching: function () { + return 'Searching…'; + } + }; +}); + +S2.define('select2/defaults',[ + 'jquery', + 'require', + + './results', + + './selection/single', + './selection/multiple', + './selection/placeholder', + './selection/allowClear', + './selection/search', + './selection/eventRelay', + + './utils', + './translation', + './diacritics', + + './data/select', + './data/array', + './data/ajax', + './data/tags', + './data/tokenizer', + './data/minimumInputLength', + './data/maximumInputLength', + './data/maximumSelectionLength', + + './dropdown', + './dropdown/search', + './dropdown/hidePlaceholder', + './dropdown/infiniteScroll', + './dropdown/attachBody', + './dropdown/minimumResultsForSearch', + './dropdown/selectOnClose', + './dropdown/closeOnSelect', + + './i18n/en' +], function ($, require, + + ResultsList, + + SingleSelection, MultipleSelection, Placeholder, AllowClear, + SelectionSearch, EventRelay, + + Utils, Translation, DIACRITICS, + + SelectData, ArrayData, AjaxData, Tags, Tokenizer, + MinimumInputLength, MaximumInputLength, MaximumSelectionLength, + + Dropdown, DropdownSearch, HidePlaceholder, InfiniteScroll, + AttachBody, MinimumResultsForSearch, SelectOnClose, CloseOnSelect, + + EnglishTranslation) { + function Defaults () { + this.reset(); + } + + Defaults.prototype.apply = function (options) { + options = $.extend(true, {}, this.defaults, options); + + if (options.dataAdapter == null) { + if (options.ajax != null) { + options.dataAdapter = AjaxData; + } else if (options.data != null) { + options.dataAdapter = ArrayData; + } else { + options.dataAdapter = SelectData; + } + + if (options.minimumInputLength > 0) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + MinimumInputLength + ); + } + + if (options.maximumInputLength > 0) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + MaximumInputLength + ); + } + + if (options.maximumSelectionLength > 0) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + MaximumSelectionLength + ); + } + + if (options.tags) { + options.dataAdapter = Utils.Decorate(options.dataAdapter, Tags); + } + + if (options.tokenSeparators != null || options.tokenizer != null) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + Tokenizer + ); + } + + if (options.query != null) { + var Query = require(options.amdBase + 'compat/query'); + + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + Query + ); + } + + if (options.initSelection != null) { + var InitSelection = require(options.amdBase + 'compat/initSelection'); + + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + InitSelection + ); + } + } + + if (options.resultsAdapter == null) { + options.resultsAdapter = ResultsList; + + if (options.ajax != null) { + options.resultsAdapter = Utils.Decorate( + options.resultsAdapter, + InfiniteScroll + ); + } + + if (options.placeholder != null) { + options.resultsAdapter = Utils.Decorate( + options.resultsAdapter, + HidePlaceholder + ); + } + + if (options.selectOnClose) { + options.resultsAdapter = Utils.Decorate( + options.resultsAdapter, + SelectOnClose + ); + } + } + + if (options.dropdownAdapter == null) { + if (options.multiple) { + options.dropdownAdapter = Dropdown; + } else { + var SearchableDropdown = Utils.Decorate(Dropdown, DropdownSearch); + + options.dropdownAdapter = SearchableDropdown; + } + + if (options.minimumResultsForSearch !== 0) { + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + MinimumResultsForSearch + ); + } + + if (options.closeOnSelect) { + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + CloseOnSelect + ); + } + + if ( + options.dropdownCssClass != null || + options.dropdownCss != null || + options.adaptDropdownCssClass != null + ) { + var DropdownCSS = require(options.amdBase + 'compat/dropdownCss'); + + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + DropdownCSS + ); + } + + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + AttachBody + ); + } + + if (options.selectionAdapter == null) { + if (options.multiple) { + options.selectionAdapter = MultipleSelection; + } else { + options.selectionAdapter = SingleSelection; + } + + // Add the placeholder mixin if a placeholder was specified + if (options.placeholder != null) { + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + Placeholder + ); + } + + if (options.allowClear) { + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + AllowClear + ); + } + + if (options.multiple) { + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + SelectionSearch + ); + } + + if ( + options.containerCssClass != null || + options.containerCss != null || + options.adaptContainerCssClass != null + ) { + var ContainerCSS = require(options.amdBase + 'compat/containerCss'); + + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + ContainerCSS + ); + } + + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + EventRelay + ); + } + + if (typeof options.language === 'string') { + // Check if the language is specified with a region + if (options.language.indexOf('-') > 0) { + // Extract the region information if it is included + var languageParts = options.language.split('-'); + var baseLanguage = languageParts[0]; + + options.language = [options.language, baseLanguage]; + } else { + options.language = [options.language]; + } + } + + if ($.isArray(options.language)) { + var languages = new Translation(); + options.language.push('en'); + + var languageNames = options.language; + + for (var l = 0; l < languageNames.length; l++) { + var name = languageNames[l]; + var language = {}; + + try { + // Try to load it with the original name + language = Translation.loadPath(name); + } catch (e) { + try { + // If we couldn't load it, check if it wasn't the full path + name = this.defaults.amdLanguageBase + name; + language = Translation.loadPath(name); + } catch (ex) { + // The translation could not be loaded at all. Sometimes this is + // because of a configuration problem, other times this can be + // because of how Select2 helps load all possible translation files. + if (options.debug && window.console && console.warn) { + console.warn( + 'Select2: The language file for "' + name + '" could not be ' + + 'automatically loaded. A fallback will be used instead.' + ); + } + + continue; + } + } + + languages.extend(language); + } + + options.translations = languages; + } else { + var baseTranslation = Translation.loadPath( + this.defaults.amdLanguageBase + 'en' + ); + var customTranslation = new Translation(options.language); + + customTranslation.extend(baseTranslation); + + options.translations = customTranslation; + } + + return options; + }; + + Defaults.prototype.reset = function () { + function stripDiacritics (text) { + // Used 'uni range + named function' from http://jsperf.com/diacritics/18 + function match(a) { + return DIACRITICS[a] || a; + } + + return text.replace(/[^\u0000-\u007E]/g, match); + } + + function matcher (params, data) { + // Always return the object if there is nothing to compare + if ($.trim(params.term) === '') { + return data; + } + + // Do a recursive check for options with children + if (data.children && data.children.length > 0) { + // Clone the data object if there are children + // This is required as we modify the object to remove any non-matches + var match = $.extend(true, {}, data); + + // Check each child of the option + for (var c = data.children.length - 1; c >= 0; c--) { + var child = data.children[c]; + + var matches = matcher(params, child); + + // If there wasn't a match, remove the object in the array + if (matches == null) { + match.children.splice(c, 1); + } + } + + // If any children matched, return the new object + if (match.children.length > 0) { + return match; + } + + // If there were no matching children, check just the plain object + return matcher(params, match); + } + + var original = stripDiacritics(data.text).toUpperCase(); + var term = stripDiacritics(params.term).toUpperCase(); + + // Check if the text contains the term + if (original.indexOf(term) > -1) { + return data; + } + + // If it doesn't contain the term, don't return anything + return null; + } + + this.defaults = { + amdBase: './', + amdLanguageBase: './i18n/', + closeOnSelect: true, + debug: false, + dropdownAutoWidth: false, + escapeMarkup: Utils.escapeMarkup, + language: EnglishTranslation, + matcher: matcher, + minimumInputLength: 0, + maximumInputLength: 0, + maximumSelectionLength: 0, + minimumResultsForSearch: 0, + selectOnClose: false, + sorter: function (data) { + return data; + }, + templateResult: function (result) { + return result.text; + }, + templateSelection: function (selection) { + return selection.text; + }, + theme: 'default', + width: 'resolve' + }; + }; + + Defaults.prototype.set = function (key, value) { + var camelKey = $.camelCase(key); + + var data = {}; + data[camelKey] = value; + + var convertedData = Utils._convertData(data); + + $.extend(this.defaults, convertedData); + }; + + var defaults = new Defaults(); + + return defaults; +}); + +S2.define('select2/options',[ + 'require', + 'jquery', + './defaults', + './utils' +], function (require, $, Defaults, Utils) { + function Options (options, $element) { + this.options = options; + + if ($element != null) { + this.fromElement($element); + } + + this.options = Defaults.apply(this.options); + + if ($element && $element.is('input')) { + var InputCompat = require(this.get('amdBase') + 'compat/inputData'); + + this.options.dataAdapter = Utils.Decorate( + this.options.dataAdapter, + InputCompat + ); + } + } + + Options.prototype.fromElement = function ($e) { + var excludedData = ['select2']; + + if (this.options.multiple == null) { + this.options.multiple = $e.prop('multiple'); + } + + if (this.options.disabled == null) { + this.options.disabled = $e.prop('disabled'); + } + + if (this.options.language == null) { + if ($e.prop('lang')) { + this.options.language = $e.prop('lang').toLowerCase(); + } else if ($e.closest('[lang]').prop('lang')) { + this.options.language = $e.closest('[lang]').prop('lang'); + } + } + + if (this.options.dir == null) { + if ($e.prop('dir')) { + this.options.dir = $e.prop('dir'); + } else if ($e.closest('[dir]').prop('dir')) { + this.options.dir = $e.closest('[dir]').prop('dir'); + } else { + this.options.dir = 'ltr'; + } + } + + $e.prop('disabled', this.options.disabled); + $e.prop('multiple', this.options.multiple); + + if ($e.data('select2Tags')) { + if (this.options.debug && window.console && console.warn) { + console.warn( + 'Select2: The `data-select2-tags` attribute has been changed to ' + + 'use the `data-data` and `data-tags="true"` attributes and will be ' + + 'removed in future versions of Select2.' + ); + } + + $e.data('data', $e.data('select2Tags')); + $e.data('tags', true); + } + + if ($e.data('ajaxUrl')) { + if (this.options.debug && window.console && console.warn) { + console.warn( + 'Select2: The `data-ajax-url` attribute has been changed to ' + + '`data-ajax--url` and support for the old attribute will be removed' + + ' in future versions of Select2.' + ); + } + + $e.attr('ajax--url', $e.data('ajaxUrl')); + $e.data('ajax--url', $e.data('ajaxUrl')); + } + + var dataset = {}; + + // Prefer the element's `dataset` attribute if it exists + // jQuery 1.x does not correctly handle data attributes with multiple dashes + if ($.fn.jquery && $.fn.jquery.substr(0, 2) == '1.' && $e[0].dataset) { + dataset = $.extend(true, {}, $e[0].dataset, $e.data()); + } else { + dataset = $e.data(); + } + + var data = $.extend(true, {}, dataset); + + data = Utils._convertData(data); + + for (var key in data) { + if ($.inArray(key, excludedData) > -1) { + continue; + } + + if ($.isPlainObject(this.options[key])) { + $.extend(this.options[key], data[key]); + } else { + this.options[key] = data[key]; + } + } + + return this; + }; + + Options.prototype.get = function (key) { + return this.options[key]; + }; + + Options.prototype.set = function (key, val) { + this.options[key] = val; + }; + + return Options; +}); + +S2.define('select2/core',[ + 'jquery', + './options', + './utils', + './keys' +], function ($, Options, Utils, KEYS) { + var Select2 = function ($element, options) { + if ($element.data('select2') != null) { + $element.data('select2').destroy(); + } + + this.$element = $element; + + this.id = this._generateId($element); + + options = options || {}; + + this.options = new Options(options, $element); + + Select2.__super__.constructor.call(this); + + // Set up the tabindex + + var tabindex = $element.attr('tabindex') || 0; + $element.data('old-tabindex', tabindex); + $element.attr('tabindex', '-1'); + + // Set up containers and adapters + + var DataAdapter = this.options.get('dataAdapter'); + this.dataAdapter = new DataAdapter($element, this.options); + + var $container = this.render(); + + this._placeContainer($container); + + var SelectionAdapter = this.options.get('selectionAdapter'); + this.selection = new SelectionAdapter($element, this.options); + this.$selection = this.selection.render(); + + this.selection.position(this.$selection, $container); + + var DropdownAdapter = this.options.get('dropdownAdapter'); + this.dropdown = new DropdownAdapter($element, this.options); + this.$dropdown = this.dropdown.render(); + + this.dropdown.position(this.$dropdown, $container); + + var ResultsAdapter = this.options.get('resultsAdapter'); + this.results = new ResultsAdapter($element, this.options, this.dataAdapter); + this.$results = this.results.render(); + + this.results.position(this.$results, this.$dropdown); + + // Bind events + + var self = this; + + // Bind the container to all of the adapters + this._bindAdapters(); + + // Register any DOM event handlers + this._registerDomEvents(); + + // Register any internal event handlers + this._registerDataEvents(); + this._registerSelectionEvents(); + this._registerDropdownEvents(); + this._registerResultsEvents(); + this._registerEvents(); + + // Set the initial state + this.dataAdapter.current(function (initialData) { + self.trigger('selection:update', { + data: initialData + }); + }); + + // Hide the original select + $element.addClass('select2-hidden-accessible'); + $element.attr('aria-hidden', 'true'); + + // Synchronize any monitored attributes + this._syncAttributes(); + + $element.data('select2', this); + }; + + Utils.Extend(Select2, Utils.Observable); + + Select2.prototype._generateId = function ($element) { + var id = ''; + + if ($element.attr('id') != null) { + id = $element.attr('id'); + } else if ($element.attr('name') != null) { + id = $element.attr('name') + '-' + Utils.generateChars(2); + } else { + id = Utils.generateChars(4); + } + + id = id.replace(/(:|\.|\[|\]|,)/g, ''); + id = 'select2-' + id; + + return id; + }; + + Select2.prototype._placeContainer = function ($container) { + $container.insertAfter(this.$element); + + var width = this._resolveWidth(this.$element, this.options.get('width')); + + if (width != null) { + $container.css('width', width); + } + }; + + Select2.prototype._resolveWidth = function ($element, method) { + var WIDTH = /^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i; + + if (method == 'resolve') { + var styleWidth = this._resolveWidth($element, 'style'); + + if (styleWidth != null) { + return styleWidth; + } + + return this._resolveWidth($element, 'element'); + } + + if (method == 'element') { + var elementWidth = $element.outerWidth(false); + + if (elementWidth <= 0) { + return 'auto'; + } + + return elementWidth + 'px'; + } + + if (method == 'style') { + var style = $element.attr('style'); + + if (typeof(style) !== 'string') { + return null; + } + + var attrs = style.split(';'); + + for (var i = 0, l = attrs.length; i < l; i = i + 1) { + var attr = attrs[i].replace(/\s/g, ''); + var matches = attr.match(WIDTH); + + if (matches !== null && matches.length >= 1) { + return matches[1]; + } + } + + return null; + } + + return method; + }; + + Select2.prototype._bindAdapters = function () { + this.dataAdapter.bind(this, this.$container); + this.selection.bind(this, this.$container); + + this.dropdown.bind(this, this.$container); + this.results.bind(this, this.$container); + }; + + Select2.prototype._registerDomEvents = function () { + var self = this; + + this.$element.on('change.select2', function () { + self.dataAdapter.current(function (data) { + self.trigger('selection:update', { + data: data + }); + }); + }); + + this.$element.on('focus.select2', function (evt) { + self.trigger('focus', evt); + }); + + this._syncA = Utils.bind(this._syncAttributes, this); + this._syncS = Utils.bind(this._syncSubtree, this); + + if (this.$element[0].attachEvent) { + this.$element[0].attachEvent('onpropertychange', this._syncA); + } + + var observer = window.MutationObserver || + window.WebKitMutationObserver || + window.MozMutationObserver + ; + + if (observer != null) { + this._observer = new observer(function (mutations) { + $.each(mutations, self._syncA); + $.each(mutations, self._syncS); + }); + this._observer.observe(this.$element[0], { + attributes: true, + childList: true, + subtree: false + }); + } else if (this.$element[0].addEventListener) { + this.$element[0].addEventListener( + 'DOMAttrModified', + self._syncA, + false + ); + this.$element[0].addEventListener( + 'DOMNodeInserted', + self._syncS, + false + ); + this.$element[0].addEventListener( + 'DOMNodeRemoved', + self._syncS, + false + ); + } + }; + + Select2.prototype._registerDataEvents = function () { + var self = this; + + this.dataAdapter.on('*', function (name, params) { + self.trigger(name, params); + }); + }; + + Select2.prototype._registerSelectionEvents = function () { + var self = this; + var nonRelayEvents = ['toggle', 'focus']; + + this.selection.on('toggle', function () { + self.toggleDropdown(); + }); + + this.selection.on('focus', function (params) { + self.focus(params); + }); + + this.selection.on('*', function (name, params) { + if ($.inArray(name, nonRelayEvents) !== -1) { + return; + } + + self.trigger(name, params); + }); + }; + + Select2.prototype._registerDropdownEvents = function () { + var self = this; + + this.dropdown.on('*', function (name, params) { + self.trigger(name, params); + }); + }; + + Select2.prototype._registerResultsEvents = function () { + var self = this; + + this.results.on('*', function (name, params) { + self.trigger(name, params); + }); + }; + + Select2.prototype._registerEvents = function () { + var self = this; + + this.on('open', function () { + self.$container.addClass('select2-container--open'); + }); + + this.on('close', function () { + self.$container.removeClass('select2-container--open'); + }); + + this.on('enable', function () { + self.$container.removeClass('select2-container--disabled'); + }); + + this.on('disable', function () { + self.$container.addClass('select2-container--disabled'); + }); + + this.on('blur', function () { + self.$container.removeClass('select2-container--focus'); + }); + + this.on('query', function (params) { + if (!self.isOpen()) { + self.trigger('open', {}); + } + + this.dataAdapter.query(params, function (data) { + self.trigger('results:all', { + data: data, + query: params + }); + }); + }); + + this.on('query:append', function (params) { + this.dataAdapter.query(params, function (data) { + self.trigger('results:append', { + data: data, + query: params + }); + }); + }); + + this.on('keypress', function (evt) { + var key = evt.which; + + if (self.isOpen()) { + if (key === KEYS.ESC || key === KEYS.TAB || + (key === KEYS.UP && evt.altKey)) { + self.close(); + + evt.preventDefault(); + } else if (key === KEYS.ENTER) { + self.trigger('results:select', {}); + + evt.preventDefault(); + } else if ((key === KEYS.SPACE && evt.ctrlKey)) { + self.trigger('results:toggle', {}); + + evt.preventDefault(); + } else if (key === KEYS.UP) { + self.trigger('results:previous', {}); + + evt.preventDefault(); + } else if (key === KEYS.DOWN) { + self.trigger('results:next', {}); + + evt.preventDefault(); + } + } else { + if (key === KEYS.ENTER || key === KEYS.SPACE || + (key === KEYS.DOWN && evt.altKey)) { + self.open(); + + evt.preventDefault(); + } + } + }); + }; + + Select2.prototype._syncAttributes = function () { + this.options.set('disabled', this.$element.prop('disabled')); + + if (this.options.get('disabled')) { + if (this.isOpen()) { + this.close(); + } + + this.trigger('disable', {}); + } else { + this.trigger('enable', {}); + } + }; + + Select2.prototype._syncSubtree = function (evt, mutations) { + var changed = false; + var self = this; + + // Ignore any mutation events raised for elements that aren't options or + // optgroups. This handles the case when the select element is destroyed + if ( + evt && evt.target && ( + evt.target.nodeName !== 'OPTION' && evt.target.nodeName !== 'OPTGROUP' + ) + ) { + return; + } + + if (!mutations) { + // If mutation events aren't supported, then we can only assume that the + // change affected the selections + changed = true; + } else if (mutations.addedNodes && mutations.addedNodes.length > 0) { + for (var n = 0; n < mutations.addedNodes.length; n++) { + var node = mutations.addedNodes[n]; + + if (node.selected) { + changed = true; + } + } + } else if (mutations.removedNodes && mutations.removedNodes.length > 0) { + changed = true; + } + + // Only re-pull the data if we think there is a change + if (changed) { + this.dataAdapter.current(function (currentData) { + self.trigger('selection:update', { + data: currentData + }); + }); + } + }; + + /** + * Override the trigger method to automatically trigger pre-events when + * there are events that can be prevented. + */ + Select2.prototype.trigger = function (name, args) { + var actualTrigger = Select2.__super__.trigger; + var preTriggerMap = { + 'open': 'opening', + 'close': 'closing', + 'select': 'selecting', + 'unselect': 'unselecting' + }; + + if (args === undefined) { + args = {}; + } + + if (name in preTriggerMap) { + var preTriggerName = preTriggerMap[name]; + var preTriggerArgs = { + prevented: false, + name: name, + args: args + }; + + actualTrigger.call(this, preTriggerName, preTriggerArgs); + + if (preTriggerArgs.prevented) { + args.prevented = true; + + return; + } + } + + actualTrigger.call(this, name, args); + }; + + Select2.prototype.toggleDropdown = function () { + if (this.options.get('disabled')) { + return; + } + + if (this.isOpen()) { + this.close(); + } else { + this.open(); + } + }; + + Select2.prototype.open = function () { + if (this.isOpen()) { + return; + } + + this.trigger('query', {}); + }; + + Select2.prototype.close = function () { + if (!this.isOpen()) { + return; + } + + this.trigger('close', {}); + }; + + Select2.prototype.isOpen = function () { + return this.$container.hasClass('select2-container--open'); + }; + + Select2.prototype.hasFocus = function () { + return this.$container.hasClass('select2-container--focus'); + }; + + Select2.prototype.focus = function (data) { + // No need to re-trigger focus events if we are already focused + if (this.hasFocus()) { + return; + } + + this.$container.addClass('select2-container--focus'); + this.trigger('focus', {}); + }; + + Select2.prototype.enable = function (args) { + if (this.options.get('debug') && window.console && console.warn) { + console.warn( + 'Select2: The `select2("enable")` method has been deprecated and will' + + ' be removed in later Select2 versions. Use $element.prop("disabled")' + + ' instead.' + ); + } + + if (args == null || args.length === 0) { + args = [true]; + } + + var disabled = !args[0]; + + this.$element.prop('disabled', disabled); + }; + + Select2.prototype.data = function () { + if (this.options.get('debug') && + arguments.length > 0 && window.console && console.warn) { + console.warn( + 'Select2: Data can no longer be set using `select2("data")`. You ' + + 'should consider setting the value instead using `$element.val()`.' + ); + } + + var data = []; + + this.dataAdapter.current(function (currentData) { + data = currentData; + }); + + return data; + }; + + Select2.prototype.val = function (args) { + if (this.options.get('debug') && window.console && console.warn) { + console.warn( + 'Select2: The `select2("val")` method has been deprecated and will be' + + ' removed in later Select2 versions. Use $element.val() instead.' + ); + } + + if (args == null || args.length === 0) { + return this.$element.val(); + } + + var newVal = args[0]; + + if ($.isArray(newVal)) { + newVal = $.map(newVal, function (obj) { + return obj.toString(); + }); + } + + this.$element.val(newVal).trigger('change'); + }; + + Select2.prototype.destroy = function () { + this.$container.remove(); + + if (this.$element[0].detachEvent) { + this.$element[0].detachEvent('onpropertychange', this._syncA); + } + + if (this._observer != null) { + this._observer.disconnect(); + this._observer = null; + } else if (this.$element[0].removeEventListener) { + this.$element[0] + .removeEventListener('DOMAttrModified', this._syncA, false); + this.$element[0] + .removeEventListener('DOMNodeInserted', this._syncS, false); + this.$element[0] + .removeEventListener('DOMNodeRemoved', this._syncS, false); + } + + this._syncA = null; + this._syncS = null; + + this.$element.off('.select2'); + this.$element.attr('tabindex', this.$element.data('old-tabindex')); + + this.$element.removeClass('select2-hidden-accessible'); + this.$element.attr('aria-hidden', 'false'); + this.$element.removeData('select2'); + + this.dataAdapter.destroy(); + this.selection.destroy(); + this.dropdown.destroy(); + this.results.destroy(); + + this.dataAdapter = null; + this.selection = null; + this.dropdown = null; + this.results = null; + }; + + Select2.prototype.render = function () { + var $container = $( + '' + + '' + + '' + + '' + ); + + $container.attr('dir', this.options.get('dir')); + + this.$container = $container; + + this.$container.addClass('select2-container--' + this.options.get('theme')); + + $container.data('element', this.$element); + + return $container; + }; + + return Select2; +}); + +S2.define('jquery-mousewheel',[ + 'jquery' +], function ($) { + // Used to shim jQuery.mousewheel for non-full builds. + return $; +}); + +S2.define('jquery.select2',[ + 'jquery', + 'jquery-mousewheel', + + './select2/core', + './select2/defaults' +], function ($, _, Select2, Defaults) { + if ($.fn.select2 == null) { + // All methods that should return the element + var thisMethods = ['open', 'close', 'destroy']; + + $.fn.select2 = function (options) { + options = options || {}; + + if (typeof options === 'object') { + this.each(function () { + var instanceOptions = $.extend(true, {}, options); + + var instance = new Select2($(this), instanceOptions); + }); + + return this; + } else if (typeof options === 'string') { + var ret; + var args = Array.prototype.slice.call(arguments, 1); + + this.each(function () { + var instance = $(this).data('select2'); + + if (instance == null && window.console && console.error) { + console.error( + 'The select2(\'' + options + '\') method was called on an ' + + 'element that is not using Select2.' + ); + } + + ret = instance[options].apply(instance, args); + }); + + // Check if we should be returning `this` + if ($.inArray(options, thisMethods) > -1) { + return this; + } + + return ret; + } else { + throw new Error('Invalid arguments for Select2: ' + options); + } + }; + } + + if ($.fn.select2.defaults == null) { + $.fn.select2.defaults = Defaults; + } + + return Select2; +}); + + // Return the AMD loader configuration so it can be used outside of this file + return { + define: S2.define, + require: S2.require + }; +}()); + + // Autoload the jQuery bindings + // We know that all of the modules exist above this, so we're safe + var select2 = S2.require('jquery.select2'); + + // Hold the AMD module references on the jQuery function that was just loaded + // This allows Select2 to use the internal loader outside of this file, such + // as in the language files. + jQuery.fn.select2.amd = S2; + + // Return the Select2 instance for anyone who is importing it. + return select2; +})); diff --git a/public/static/libs/select2/select2.min.css b/public/static/libs/select2/select2.min.css new file mode 100644 index 0000000..d2278f9 --- /dev/null +++ b/public/static/libs/select2/select2.min.css @@ -0,0 +1 @@ +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;height:1px !important;margin:-1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__placeholder{color:#999;margin-top:5px;float:left}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} diff --git a/public/static/libs/select2/select2.min.js b/public/static/libs/select2/select2.min.js new file mode 100644 index 0000000..c668840 --- /dev/null +++ b/public/static/libs/select2/select2.min.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.3 | https://github.com/select2/select2/blob/master/LICENSE.md */!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):jQuery)}(function(a){var b=function(){if(a&&a.fn&&a.fn.select2&&a.fn.select2.amd)var b=a.fn.select2.amd;var b;return function(){if(!b||!b.requirejs){b?c=b:b={};var a,c,d;!function(b){function e(a,b){return u.call(a,b)}function f(a,b){var c,d,e,f,g,h,i,j,k,l,m,n=b&&b.split("/"),o=s.map,p=o&&o["*"]||{};if(a&&"."===a.charAt(0))if(b){for(a=a.split("/"),g=a.length-1,s.nodeIdCompat&&w.test(a[g])&&(a[g]=a[g].replace(w,"")),a=n.slice(0,n.length-1).concat(a),k=0;k0&&(a.splice(k-1,2),k-=2)}a=a.join("/")}else 0===a.indexOf("./")&&(a=a.substring(2));if((n||p)&&o){for(c=a.split("/"),k=c.length;k>0;k-=1){if(d=c.slice(0,k).join("/"),n)for(l=n.length;l>0;l-=1)if(e=o[n.slice(0,l).join("/")],e&&(e=e[d])){f=e,h=k;break}if(f)break;!i&&p&&p[d]&&(i=p[d],j=k)}!f&&i&&(f=i,h=j),f&&(c.splice(0,h,f),a=c.join("/"))}return a}function g(a,c){return function(){var d=v.call(arguments,0);return"string"!=typeof d[0]&&1===d.length&&d.push(null),n.apply(b,d.concat([a,c]))}}function h(a){return function(b){return f(b,a)}}function i(a){return function(b){q[a]=b}}function j(a){if(e(r,a)){var c=r[a];delete r[a],t[a]=!0,m.apply(b,c)}if(!e(q,a)&&!e(t,a))throw new Error("No "+a);return q[a]}function k(a){var b,c=a?a.indexOf("!"):-1;return c>-1&&(b=a.substring(0,c),a=a.substring(c+1,a.length)),[b,a]}function l(a){return function(){return s&&s.config&&s.config[a]||{}}}var m,n,o,p,q={},r={},s={},t={},u=Object.prototype.hasOwnProperty,v=[].slice,w=/\.js$/;o=function(a,b){var c,d=k(a),e=d[0];return a=d[1],e&&(e=f(e,b),c=j(e)),e?a=c&&c.normalize?c.normalize(a,h(b)):f(a,b):(a=f(a,b),d=k(a),e=d[0],a=d[1],e&&(c=j(e))),{f:e?e+"!"+a:a,n:a,pr:e,p:c}},p={require:function(a){return g(a)},exports:function(a){var b=q[a];return"undefined"!=typeof b?b:q[a]={}},module:function(a){return{id:a,uri:"",exports:q[a],config:l(a)}}},m=function(a,c,d,f){var h,k,l,m,n,s,u=[],v=typeof d;if(f=f||a,"undefined"===v||"function"===v){for(c=!c.length&&d.length?["require","exports","module"]:c,n=0;n0&&(b.call(arguments,a.prototype.constructor),e=c.prototype.constructor),e.apply(this,arguments)}function e(){this.constructor=d}var f=b(c),g=b(a);c.displayName=a.displayName,d.prototype=new e;for(var h=0;hc;c++)a[c].apply(this,b)},c.Observable=d,c.generateChars=function(a){for(var b="",c=0;a>c;c++){var d=Math.floor(36*Math.random());b+=d.toString(36)}return b},c.bind=function(a,b){return function(){a.apply(b,arguments)}},c._convertData=function(a){for(var b in a){var c=b.split("-"),d=a;if(1!==c.length){for(var e=0;e":">",'"':""","'":"'","/":"/"};return"string"!=typeof a?a:String(a).replace(/[&<>"'\/\\]/g,function(a){return b[a]})},c.appendMany=function(b,c){if("1.7"===a.fn.jquery.substr(0,3)){var d=a();a.map(c,function(a){d=d.add(a)}),c=d}b.append(c)},c}),b.define("select2/results",["jquery","./utils"],function(a,b){function c(a,b,d){this.$element=a,this.data=d,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('
            ');return this.options.get("multiple")&&b.attr("aria-multiselectable","true"),this.$results=b,b},c.prototype.clear=function(){this.$results.empty()},c.prototype.displayMessage=function(b){var c=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var d=a('
          • '),e=this.options.get("translations").get(b.message);d.append(c(e(b.args))),d[0].className+=" select2-results__message",this.$results.append(d)},c.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},c.prototype.append=function(a){this.hideLoading();var b=[];if(null==a.results||0===a.results.length)return void(0===this.$results.children().length&&this.trigger("results:message",{message:"noResults"}));a.results=this.sort(a.results);for(var c=0;c0?b.first().trigger("mouseenter"):a.first().trigger("mouseenter"),this.ensureHighlightVisible()},c.prototype.setClasses=function(){var b=this;this.data.current(function(c){var d=a.map(c,function(a){return a.id.toString()}),e=b.$results.find(".select2-results__option[aria-selected]");e.each(function(){var b=a(this),c=a.data(this,"data"),e=""+c.id;null!=c.element&&c.element.selected||null==c.element&&a.inArray(e,d)>-1?b.attr("aria-selected","true"):b.attr("aria-selected","false")})})},c.prototype.showLoading=function(a){this.hideLoading();var b=this.options.get("translations").get("searching"),c={disabled:!0,loading:!0,text:b(a)},d=this.option(c);d.className+=" loading-results",this.$results.prepend(d)},c.prototype.hideLoading=function(){this.$results.find(".loading-results").remove()},c.prototype.option=function(b){var c=document.createElement("li");c.className="select2-results__option";var d={role:"treeitem","aria-selected":"false"};b.disabled&&(delete d["aria-selected"],d["aria-disabled"]="true"),null==b.id&&delete d["aria-selected"],null!=b._resultId&&(c.id=b._resultId),b.title&&(c.title=b.title),b.children&&(d.role="group",d["aria-label"]=b.text,delete d["aria-selected"]);for(var e in d){var f=d[e];c.setAttribute(e,f)}if(b.children){var g=a(c),h=document.createElement("strong");h.className="select2-results__group";a(h);this.template(b,h);for(var i=[],j=0;j",{"class":"select2-results__options select2-results__options--nested"});m.append(i),g.append(h),g.append(m)}else this.template(b,c);return a.data(c,"data",b),c},c.prototype.bind=function(b,c){var d=this,e=b.id+"-results";this.$results.attr("id",e),b.on("results:all",function(a){d.clear(),d.append(a.data),b.isOpen()&&(d.setClasses(),d.highlightFirstItem())}),b.on("results:append",function(a){d.append(a.data),b.isOpen()&&d.setClasses()}),b.on("query",function(a){d.hideMessages(),d.showLoading(a)}),b.on("select",function(){b.isOpen()&&(d.setClasses(),d.highlightFirstItem())}),b.on("unselect",function(){b.isOpen()&&(d.setClasses(),d.highlightFirstItem())}),b.on("open",function(){d.$results.attr("aria-expanded","true"),d.$results.attr("aria-hidden","false"),d.setClasses(),d.ensureHighlightVisible()}),b.on("close",function(){d.$results.attr("aria-expanded","false"),d.$results.attr("aria-hidden","true"),d.$results.removeAttr("aria-activedescendant")}),b.on("results:toggle",function(){var a=d.getHighlightedResults();0!==a.length&&a.trigger("mouseup")}),b.on("results:select",function(){var a=d.getHighlightedResults();if(0!==a.length){var b=a.data("data");"true"==a.attr("aria-selected")?d.trigger("close",{}):d.trigger("select",{data:b})}}),b.on("results:previous",function(){var a=d.getHighlightedResults(),b=d.$results.find("[aria-selected]"),c=b.index(a);if(0!==c){var e=c-1;0===a.length&&(e=0);var f=b.eq(e);f.trigger("mouseenter");var g=d.$results.offset().top,h=f.offset().top,i=d.$results.scrollTop()+(h-g);0===e?d.$results.scrollTop(0):0>h-g&&d.$results.scrollTop(i)}}),b.on("results:next",function(){var a=d.getHighlightedResults(),b=d.$results.find("[aria-selected]"),c=b.index(a),e=c+1;if(!(e>=b.length)){var f=b.eq(e);f.trigger("mouseenter");var g=d.$results.offset().top+d.$results.outerHeight(!1),h=f.offset().top+f.outerHeight(!1),i=d.$results.scrollTop()+h-g;0===e?d.$results.scrollTop(0):h>g&&d.$results.scrollTop(i)}}),b.on("results:focus",function(a){a.element.addClass("select2-results__option--highlighted")}),b.on("results:message",function(a){d.displayMessage(a)}),a.fn.mousewheel&&this.$results.on("mousewheel",function(a){var b=d.$results.scrollTop(),c=d.$results.get(0).scrollHeight-b+a.deltaY,e=a.deltaY>0&&b-a.deltaY<=0,f=a.deltaY<0&&c<=d.$results.height();e?(d.$results.scrollTop(0),a.preventDefault(),a.stopPropagation()):f&&(d.$results.scrollTop(d.$results.get(0).scrollHeight-d.$results.height()),a.preventDefault(),a.stopPropagation())}),this.$results.on("mouseup",".select2-results__option[aria-selected]",function(b){var c=a(this),e=c.data("data");return"true"===c.attr("aria-selected")?void(d.options.get("multiple")?d.trigger("unselect",{originalEvent:b,data:e}):d.trigger("close",{})):void d.trigger("select",{originalEvent:b,data:e})}),this.$results.on("mouseenter",".select2-results__option[aria-selected]",function(b){var c=a(this).data("data");d.getHighlightedResults().removeClass("select2-results__option--highlighted"),d.trigger("results:focus",{data:c,element:a(this)})})},c.prototype.getHighlightedResults=function(){var a=this.$results.find(".select2-results__option--highlighted");return a},c.prototype.destroy=function(){this.$results.remove()},c.prototype.ensureHighlightVisible=function(){var a=this.getHighlightedResults();if(0!==a.length){var b=this.$results.find("[aria-selected]"),c=b.index(a),d=this.$results.offset().top,e=a.offset().top,f=this.$results.scrollTop()+(e-d),g=e-d;f-=2*a.outerHeight(!1),2>=c?this.$results.scrollTop(0):(g>this.$results.outerHeight()||0>g)&&this.$results.scrollTop(f)}},c.prototype.template=function(b,c){var d=this.options.get("templateResult"),e=this.options.get("escapeMarkup"),f=d(b,c);null==f?c.style.display="none":"string"==typeof f?c.innerHTML=e(f):a(c).append(f)},c}),b.define("select2/keys",[],function(){var a={BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46};return a}),b.define("select2/selection/base",["jquery","../utils","../keys"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,b.Observable),d.prototype.render=function(){var b=a('');return this._tabindex=0,null!=this.$element.data("old-tabindex")?this._tabindex=this.$element.data("old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),b.attr("title",this.$element.attr("title")),b.attr("tabindex",this._tabindex),this.$selection=b,b},d.prototype.bind=function(a,b){var d=this,e=(a.id+"-container",a.id+"-results");this.container=a,this.$selection.on("focus",function(a){d.trigger("focus",a)}),this.$selection.on("blur",function(a){d._handleBlur(a)}),this.$selection.on("keydown",function(a){d.trigger("keypress",a),a.which===c.SPACE&&a.preventDefault()}),a.on("results:focus",function(a){d.$selection.attr("aria-activedescendant",a.data._resultId)}),a.on("selection:update",function(a){d.update(a.data)}),a.on("open",function(){d.$selection.attr("aria-expanded","true"),d.$selection.attr("aria-owns",e),d._attachCloseHandler(a)}),a.on("close",function(){d.$selection.attr("aria-expanded","false"),d.$selection.removeAttr("aria-activedescendant"),d.$selection.removeAttr("aria-owns"),d.$selection.focus(),d._detachCloseHandler(a)}),a.on("enable",function(){d.$selection.attr("tabindex",d._tabindex)}),a.on("disable",function(){d.$selection.attr("tabindex","-1")})},d.prototype._handleBlur=function(b){var c=this;window.setTimeout(function(){document.activeElement==c.$selection[0]||a.contains(c.$selection[0],document.activeElement)||c.trigger("blur",b)},1)},d.prototype._attachCloseHandler=function(b){a(document.body).on("mousedown.select2."+b.id,function(b){var c=a(b.target),d=c.closest(".select2"),e=a(".select2.select2-container--open");e.each(function(){var b=a(this);if(this!=d[0]){var c=b.data("element");c.select2("close")}})})},d.prototype._detachCloseHandler=function(b){a(document.body).off("mousedown.select2."+b.id)},d.prototype.position=function(a,b){var c=b.find(".selection");c.append(a)},d.prototype.destroy=function(){this._detachCloseHandler(this.container)},d.prototype.update=function(a){throw new Error("The `update` method must be defined in child classes.")},d}),b.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(a,b,c,d){function e(){e.__super__.constructor.apply(this,arguments)}return c.Extend(e,b),e.prototype.render=function(){var a=e.__super__.render.call(this);return a.addClass("select2-selection--single"),a.html(''),a},e.prototype.bind=function(a,b){var c=this;e.__super__.bind.apply(this,arguments);var d=a.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",d),this.$selection.attr("aria-labelledby",d),this.$selection.on("mousedown",function(a){1===a.which&&c.trigger("toggle",{originalEvent:a})}),this.$selection.on("focus",function(a){}),this.$selection.on("blur",function(a){}),a.on("focus",function(b){a.isOpen()||c.$selection.focus()}),a.on("selection:update",function(a){c.update(a.data)})},e.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},e.prototype.display=function(a,b){var c=this.options.get("templateSelection"),d=this.options.get("escapeMarkup");return d(c(a,b))},e.prototype.selectionContainer=function(){return a("")},e.prototype.update=function(a){if(0===a.length)return void this.clear();var b=a[0],c=this.$selection.find(".select2-selection__rendered"),d=this.display(b,c);c.empty().append(d),c.prop("title",b.title||b.text)},e}),b.define("select2/selection/multiple",["jquery","./base","../utils"],function(a,b,c){function d(a,b){d.__super__.constructor.apply(this,arguments)}return c.Extend(d,b),d.prototype.render=function(){var a=d.__super__.render.call(this);return a.addClass("select2-selection--multiple"),a.html('
              '),a},d.prototype.bind=function(b,c){var e=this;d.__super__.bind.apply(this,arguments),this.$selection.on("click",function(a){e.trigger("toggle",{originalEvent:a})}),this.$selection.on("click",".select2-selection__choice__remove",function(b){if(!e.options.get("disabled")){var c=a(this),d=c.parent(),f=d.data("data");e.trigger("unselect",{originalEvent:b,data:f})}})},d.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},d.prototype.display=function(a,b){var c=this.options.get("templateSelection"),d=this.options.get("escapeMarkup");return d(c(a,b))},d.prototype.selectionContainer=function(){var b=a('
            • ×
            • ');return b},d.prototype.update=function(a){if(this.clear(),0!==a.length){for(var b=[],d=0;d1;if(d||c)return a.call(this,b);this.clear();var e=this.createPlaceholder(this.placeholder);this.$selection.find(".select2-selection__rendered").append(e)},b}),b.define("select2/selection/allowClear",["jquery","../keys"],function(a,b){function c(){}return c.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),null==this.placeholder&&this.options.get("debug")&&window.console&&console.error&&console.error("Select2: The `allowClear` option should be used in combination with the `placeholder` option."),this.$selection.on("mousedown",".select2-selection__clear",function(a){d._handleClear(a)}),b.on("keypress",function(a){d._handleKeyboardClear(a,b)})},c.prototype._handleClear=function(a,b){if(!this.options.get("disabled")){var c=this.$selection.find(".select2-selection__clear");if(0!==c.length){b.stopPropagation();for(var d=c.data("data"),e=0;e0||0===c.length)){var d=a('×');d.data("data",c),this.$selection.find(".select2-selection__rendered").prepend(d)}},c}),b.define("select2/selection/search",["jquery","../utils","../keys"],function(a,b,c){function d(a,b,c){a.call(this,b,c)}return d.prototype.render=function(b){var c=a('');this.$searchContainer=c,this.$search=c.find("input");var d=b.call(this);return this._transferTabIndex(),d},d.prototype.bind=function(a,b,d){var e=this;a.call(this,b,d),b.on("open",function(){e.$search.trigger("focus")}),b.on("close",function(){e.$search.val(""),e.$search.removeAttr("aria-activedescendant"),e.$search.trigger("focus")}),b.on("enable",function(){e.$search.prop("disabled",!1),e._transferTabIndex()}),b.on("disable",function(){e.$search.prop("disabled",!0)}),b.on("focus",function(a){e.$search.trigger("focus")}),b.on("results:focus",function(a){e.$search.attr("aria-activedescendant",a.id)}),this.$selection.on("focusin",".select2-search--inline",function(a){e.trigger("focus",a)}),this.$selection.on("focusout",".select2-search--inline",function(a){e._handleBlur(a)}),this.$selection.on("keydown",".select2-search--inline",function(a){a.stopPropagation(),e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented();var b=a.which;if(b===c.BACKSPACE&&""===e.$search.val()){var d=e.$searchContainer.prev(".select2-selection__choice");if(d.length>0){var f=d.data("data");e.searchRemoveChoice(f),a.preventDefault()}}});var f=document.documentMode,g=f&&11>=f;this.$selection.on("input.searchcheck",".select2-search--inline",function(a){return g?void e.$selection.off("input.search input.searchcheck"):void e.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(a){if(g&&"input"===a.type)return void e.$selection.off("input.search input.searchcheck");var b=a.which;b!=c.SHIFT&&b!=c.CTRL&&b!=c.ALT&&b!=c.TAB&&e.handleSearch(a)})},d.prototype._transferTabIndex=function(a){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},d.prototype.createPlaceholder=function(a,b){this.$search.attr("placeholder",b.text)},d.prototype.update=function(a,b){var c=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),a.call(this,b),this.$selection.find(".select2-selection__rendered").append(this.$searchContainer),this.resizeSearch(),c&&this.$search.focus()},d.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var a=this.$search.val();this.trigger("query",{term:a})}this._keyUpPrevented=!1},d.prototype.searchRemoveChoice=function(a,b){this.trigger("unselect",{data:b}),this.$search.val(b.text),this.handleSearch()},d.prototype.resizeSearch=function(){this.$search.css("width","25px");var a="";if(""!==this.$search.attr("placeholder"))a=this.$selection.find(".select2-selection__rendered").innerWidth();else{var b=this.$search.val().length+1;a=.75*b+"em"}this.$search.css("width",a)},d}),b.define("select2/selection/eventRelay",["jquery"],function(a){function b(){}return b.prototype.bind=function(b,c,d){var e=this,f=["open","opening","close","closing","select","selecting","unselect","unselecting"],g=["opening","closing","selecting","unselecting"];b.call(this,c,d),c.on("*",function(b,c){if(-1!==a.inArray(b,f)){c=c||{};var d=a.Event("select2:"+b,{params:c});e.$element.trigger(d),-1!==a.inArray(b,g)&&(c.prevented=d.isDefaultPrevented())}})},b}),b.define("select2/translation",["jquery","require"],function(a,b){function c(a){this.dict=a||{}}return c.prototype.all=function(){return this.dict},c.prototype.get=function(a){return this.dict[a]},c.prototype.extend=function(b){this.dict=a.extend({},b.all(),this.dict)},c._cache={},c.loadPath=function(a){if(!(a in c._cache)){var d=b(a);c._cache[a]=d}return new c(c._cache[a])},c}),b.define("select2/diacritics",[],function(){var a={"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ω":"ω","ς":"σ"};return a}),b.define("select2/data/base",["../utils"],function(a){function b(a,c){b.__super__.constructor.call(this)}return a.Extend(b,a.Observable),b.prototype.current=function(a){throw new Error("The `current` method must be defined in child classes.")},b.prototype.query=function(a,b){throw new Error("The `query` method must be defined in child classes.")},b.prototype.bind=function(a,b){},b.prototype.destroy=function(){},b.prototype.generateResultId=function(b,c){var d=b.id+"-result-";return d+=a.generateChars(4),d+=null!=c.id?"-"+c.id.toString():"-"+a.generateChars(4)},b}),b.define("select2/data/select",["./base","../utils","jquery"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,a),d.prototype.current=function(a){var b=[],d=this;this.$element.find(":selected").each(function(){var a=c(this),e=d.item(a);b.push(e)}),a(b)},d.prototype.select=function(a){var b=this;if(a.selected=!0,c(a.element).is("option"))return a.element.selected=!0,void this.$element.trigger("change"); +if(this.$element.prop("multiple"))this.current(function(d){var e=[];a=[a],a.push.apply(a,d);for(var f=0;f=0){var k=f.filter(d(j)),l=this.item(k),m=c.extend(!0,{},j,l),n=this.option(m);k.replaceWith(n)}else{var o=this.option(j);if(j.children){var p=this.convertToOptions(j.children);b.appendMany(o,p)}h.push(o)}}return h},d}),b.define("select2/data/ajax",["./array","../utils","jquery"],function(a,b,c){function d(a,b){this.ajaxOptions=this._applyDefaults(b.get("ajax")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),d.__super__.constructor.call(this,a,b)}return b.Extend(d,a),d.prototype._applyDefaults=function(a){var b={data:function(a){return c.extend({},a,{q:a.term})},transport:function(a,b,d){var e=c.ajax(a);return e.then(b),e.fail(d),e}};return c.extend({},b,a,!0)},d.prototype.processResults=function(a){return a},d.prototype.query=function(a,b){function d(){var d=f.transport(f,function(d){var f=e.processResults(d,a);e.options.get("debug")&&window.console&&console.error&&(f&&f.results&&c.isArray(f.results)||console.error("Select2: The AJAX results did not return an array in the `results` key of the response.")),b(f)},function(){d.status&&"0"===d.status||e.trigger("results:message",{message:"errorLoading"})});e._request=d}var e=this;null!=this._request&&(c.isFunction(this._request.abort)&&this._request.abort(),this._request=null);var f=c.extend({type:"GET"},this.ajaxOptions);"function"==typeof f.url&&(f.url=f.url.call(this.$element,a)),"function"==typeof f.data&&(f.data=f.data.call(this.$element,a)),this.ajaxOptions.delay&&null!=a.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(d,this.ajaxOptions.delay)):d()},d}),b.define("select2/data/tags",["jquery"],function(a){function b(b,c,d){var e=d.get("tags"),f=d.get("createTag");void 0!==f&&(this.createTag=f);var g=d.get("insertTag");if(void 0!==g&&(this.insertTag=g),b.call(this,c,d),a.isArray(e))for(var h=0;h0&&b.term.length>this.maximumInputLength?void this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:b.term,params:b}}):void a.call(this,b,c)},a}),b.define("select2/data/maximumSelectionLength",[],function(){function a(a,b,c){this.maximumSelectionLength=c.get("maximumSelectionLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){var d=this;this.current(function(e){var f=null!=e?e.length:0;return d.maximumSelectionLength>0&&f>=d.maximumSelectionLength?void d.trigger("results:message",{message:"maximumSelected",args:{maximum:d.maximumSelectionLength}}):void a.call(d,b,c)})},a}),b.define("select2/dropdown",["jquery","./utils"],function(a,b){function c(a,b){this.$element=a,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('');return b.attr("dir",this.options.get("dir")),this.$dropdown=b,b},c.prototype.bind=function(){},c.prototype.position=function(a,b){},c.prototype.destroy=function(){this.$dropdown.remove()},c}),b.define("select2/dropdown/search",["jquery","../utils"],function(a,b){function c(){}return c.prototype.render=function(b){var c=b.call(this),d=a('');return this.$searchContainer=d,this.$search=d.find("input"),c.prepend(d),c},c.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),this.$search.on("keydown",function(a){e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented()}),this.$search.on("input",function(b){a(this).off("keyup")}),this.$search.on("keyup input",function(a){e.handleSearch(a)}),c.on("open",function(){e.$search.attr("tabindex",0),e.$search.focus(),window.setTimeout(function(){e.$search.focus()},0)}),c.on("close",function(){e.$search.attr("tabindex",-1),e.$search.val("")}),c.on("focus",function(){c.isOpen()&&e.$search.focus()}),c.on("results:all",function(a){if(null==a.query.term||""===a.query.term){var b=e.showSearch(a);b?e.$searchContainer.removeClass("select2-search--hide"):e.$searchContainer.addClass("select2-search--hide")}})},c.prototype.handleSearch=function(a){if(!this._keyUpPrevented){var b=this.$search.val();this.trigger("query",{term:b})}this._keyUpPrevented=!1},c.prototype.showSearch=function(a,b){return!0},c}),b.define("select2/dropdown/hidePlaceholder",[],function(){function a(a,b,c,d){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c,d)}return a.prototype.append=function(a,b){b.results=this.removePlaceholder(b.results),a.call(this,b)},a.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},a.prototype.removePlaceholder=function(a,b){for(var c=b.slice(0),d=b.length-1;d>=0;d--){var e=b[d];this.placeholder.id===e.id&&c.splice(d,1)}return c},a}),b.define("select2/dropdown/infiniteScroll",["jquery"],function(a){function b(a,b,c,d){this.lastParams={},a.call(this,b,c,d),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return b.prototype.append=function(a,b){this.$loadingMore.remove(),this.loading=!1,a.call(this,b),this.showLoadingMore(b)&&this.$results.append(this.$loadingMore)},b.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),c.on("query",function(a){e.lastParams=a,e.loading=!0}),c.on("query:append",function(a){e.lastParams=a,e.loading=!0}),this.$results.on("scroll",function(){var b=a.contains(document.documentElement,e.$loadingMore[0]);if(!e.loading&&b){var c=e.$results.offset().top+e.$results.outerHeight(!1),d=e.$loadingMore.offset().top+e.$loadingMore.outerHeight(!1);c+50>=d&&e.loadMore()}})},b.prototype.loadMore=function(){this.loading=!0;var b=a.extend({},{page:1},this.lastParams);b.page++,this.trigger("query:append",b)},b.prototype.showLoadingMore=function(a,b){return b.pagination&&b.pagination.more},b.prototype.createLoadingMore=function(){var b=a('
            • '),c=this.options.get("translations").get("loadingMore");return b.html(c(this.lastParams)),b},b}),b.define("select2/dropdown/attachBody",["jquery","../utils"],function(a,b){function c(b,c,d){this.$dropdownParent=d.get("dropdownParent")||a(document.body),b.call(this,c,d)}return c.prototype.bind=function(a,b,c){var d=this,e=!1;a.call(this,b,c),b.on("open",function(){d._showDropdown(),d._attachPositioningHandler(b),e||(e=!0,b.on("results:all",function(){d._positionDropdown(),d._resizeDropdown()}),b.on("results:append",function(){d._positionDropdown(),d._resizeDropdown()}))}),b.on("close",function(){d._hideDropdown(),d._detachPositioningHandler(b)}),this.$dropdownContainer.on("mousedown",function(a){a.stopPropagation()})},c.prototype.destroy=function(a){a.call(this),this.$dropdownContainer.remove()},c.prototype.position=function(a,b,c){b.attr("class",c.attr("class")),b.removeClass("select2"),b.addClass("select2-container--open"),b.css({position:"absolute",top:-999999}),this.$container=c},c.prototype.render=function(b){var c=a(""),d=b.call(this);return c.append(d),this.$dropdownContainer=c,c},c.prototype._hideDropdown=function(a){this.$dropdownContainer.detach()},c.prototype._attachPositioningHandler=function(c,d){var e=this,f="scroll.select2."+d.id,g="resize.select2."+d.id,h="orientationchange.select2."+d.id,i=this.$container.parents().filter(b.hasScroll);i.each(function(){a(this).data("select2-scroll-position",{x:a(this).scrollLeft(),y:a(this).scrollTop()})}),i.on(f,function(b){var c=a(this).data("select2-scroll-position");a(this).scrollTop(c.y)}),a(window).on(f+" "+g+" "+h,function(a){e._positionDropdown(),e._resizeDropdown()})},c.prototype._detachPositioningHandler=function(c,d){var e="scroll.select2."+d.id,f="resize.select2."+d.id,g="orientationchange.select2."+d.id,h=this.$container.parents().filter(b.hasScroll);h.off(e),a(window).off(e+" "+f+" "+g)},c.prototype._positionDropdown=function(){var b=a(window),c=this.$dropdown.hasClass("select2-dropdown--above"),d=this.$dropdown.hasClass("select2-dropdown--below"),e=null,f=this.$container.offset();f.bottom=f.top+this.$container.outerHeight(!1);var g={height:this.$container.outerHeight(!1)};g.top=f.top,g.bottom=f.top+g.height;var h={height:this.$dropdown.outerHeight(!1)},i={top:b.scrollTop(),bottom:b.scrollTop()+b.height()},j=i.topf.bottom+h.height,l={left:f.left,top:g.bottom},m=this.$dropdownParent;"static"===m.css("position")&&(m=m.offsetParent());var n=m.offset();l.top-=n.top,l.left-=n.left,c||d||(e="below"),k||!j||c?!j&&k&&c&&(e="below"):e="above",("above"==e||c&&"below"!==e)&&(l.top=g.top-n.top-h.height),null!=e&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+e),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+e)),this.$dropdownContainer.css(l)},c.prototype._resizeDropdown=function(){var a={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(a.minWidth=a.width,a.position="relative",a.width="auto"),this.$dropdown.css(a)},c.prototype._showDropdown=function(a){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},c}),b.define("select2/dropdown/minimumResultsForSearch",[],function(){function a(b){for(var c=0,d=0;d0&&(l.dataAdapter=j.Decorate(l.dataAdapter,r)),l.maximumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,s)),l.maximumSelectionLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,t)),l.tags&&(l.dataAdapter=j.Decorate(l.dataAdapter,p)),(null!=l.tokenSeparators||null!=l.tokenizer)&&(l.dataAdapter=j.Decorate(l.dataAdapter,q)),null!=l.query){var C=b(l.amdBase+"compat/query");l.dataAdapter=j.Decorate(l.dataAdapter,C)}if(null!=l.initSelection){var D=b(l.amdBase+"compat/initSelection");l.dataAdapter=j.Decorate(l.dataAdapter,D)}}if(null==l.resultsAdapter&&(l.resultsAdapter=c,null!=l.ajax&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,x)),null!=l.placeholder&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,w)),l.selectOnClose&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,A))),null==l.dropdownAdapter){if(l.multiple)l.dropdownAdapter=u;else{var E=j.Decorate(u,v);l.dropdownAdapter=E}if(0!==l.minimumResultsForSearch&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,z)),l.closeOnSelect&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,B)),null!=l.dropdownCssClass||null!=l.dropdownCss||null!=l.adaptDropdownCssClass){var F=b(l.amdBase+"compat/dropdownCss");l.dropdownAdapter=j.Decorate(l.dropdownAdapter,F)}l.dropdownAdapter=j.Decorate(l.dropdownAdapter,y)}if(null==l.selectionAdapter){if(l.multiple?l.selectionAdapter=e:l.selectionAdapter=d,null!=l.placeholder&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,f)),l.allowClear&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,g)),l.multiple&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,h)),null!=l.containerCssClass||null!=l.containerCss||null!=l.adaptContainerCssClass){var G=b(l.amdBase+"compat/containerCss");l.selectionAdapter=j.Decorate(l.selectionAdapter,G)}l.selectionAdapter=j.Decorate(l.selectionAdapter,i)}if("string"==typeof l.language)if(l.language.indexOf("-")>0){var H=l.language.split("-"),I=H[0];l.language=[l.language,I]}else l.language=[l.language];if(a.isArray(l.language)){var J=new k;l.language.push("en");for(var K=l.language,L=0;L0){for(var f=a.extend(!0,{},e),g=e.children.length-1;g>=0;g--){var h=e.children[g],i=c(d,h);null==i&&f.children.splice(g,1)}return f.children.length>0?f:c(d,f)}var j=b(e.text).toUpperCase(),k=b(d.term).toUpperCase();return j.indexOf(k)>-1?e:null}this.defaults={amdBase:"./",amdLanguageBase:"./i18n/",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:j.escapeMarkup,language:C,matcher:c,minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,sorter:function(a){return a},templateResult:function(a){return a.text},templateSelection:function(a){return a.text},theme:"default",width:"resolve"}},D.prototype.set=function(b,c){var d=a.camelCase(b),e={};e[d]=c;var f=j._convertData(e);a.extend(this.defaults,f)};var E=new D;return E}),b.define("select2/options",["require","jquery","./defaults","./utils"],function(a,b,c,d){function e(b,e){if(this.options=b,null!=e&&this.fromElement(e),this.options=c.apply(this.options),e&&e.is("input")){var f=a(this.get("amdBase")+"compat/inputData");this.options.dataAdapter=d.Decorate(this.options.dataAdapter,f)}}return e.prototype.fromElement=function(a){var c=["select2"];null==this.options.multiple&&(this.options.multiple=a.prop("multiple")),null==this.options.disabled&&(this.options.disabled=a.prop("disabled")),null==this.options.language&&(a.prop("lang")?this.options.language=a.prop("lang").toLowerCase():a.closest("[lang]").prop("lang")&&(this.options.language=a.closest("[lang]").prop("lang"))),null==this.options.dir&&(a.prop("dir")?this.options.dir=a.prop("dir"):a.closest("[dir]").prop("dir")?this.options.dir=a.closest("[dir]").prop("dir"):this.options.dir="ltr"),a.prop("disabled",this.options.disabled),a.prop("multiple",this.options.multiple),a.data("select2Tags")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags="true"` attributes and will be removed in future versions of Select2.'),a.data("data",a.data("select2Tags")),a.data("tags",!0)),a.data("ajaxUrl")&&(this.options.debug&&window.console&&console.warn&&console.warn("Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2."),a.attr("ajax--url",a.data("ajaxUrl")),a.data("ajax--url",a.data("ajaxUrl")));var e={};e=b.fn.jquery&&"1."==b.fn.jquery.substr(0,2)&&a[0].dataset?b.extend(!0,{},a[0].dataset,a.data()):a.data();var f=b.extend(!0,{},e);f=d._convertData(f);for(var g in f)b.inArray(g,c)>-1||(b.isPlainObject(this.options[g])?b.extend(this.options[g],f[g]):this.options[g]=f[g]);return this},e.prototype.get=function(a){return this.options[a]},e.prototype.set=function(a,b){this.options[a]=b},e}),b.define("select2/core",["jquery","./options","./utils","./keys"],function(a,b,c,d){var e=function(a,c){null!=a.data("select2")&&a.data("select2").destroy(),this.$element=a,this.id=this._generateId(a),c=c||{},this.options=new b(c,a),e.__super__.constructor.call(this);var d=a.attr("tabindex")||0;a.data("old-tabindex",d),a.attr("tabindex","-1");var f=this.options.get("dataAdapter");this.dataAdapter=new f(a,this.options);var g=this.render();this._placeContainer(g);var h=this.options.get("selectionAdapter");this.selection=new h(a,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,g);var i=this.options.get("dropdownAdapter");this.dropdown=new i(a,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,g);var j=this.options.get("resultsAdapter");this.results=new j(a,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var k=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(a){k.trigger("selection:update",{data:a})}),a.addClass("select2-hidden-accessible"),a.attr("aria-hidden","true"),this._syncAttributes(),a.data("select2",this)};return c.Extend(e,c.Observable),e.prototype._generateId=function(a){var b="";return b=null!=a.attr("id")?a.attr("id"):null!=a.attr("name")?a.attr("name")+"-"+c.generateChars(2):c.generateChars(4),b=b.replace(/(:|\.|\[|\]|,)/g,""),b="select2-"+b},e.prototype._placeContainer=function(a){a.insertAfter(this.$element);var b=this._resolveWidth(this.$element,this.options.get("width"));null!=b&&a.css("width",b)},e.prototype._resolveWidth=function(a,b){var c=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if("resolve"==b){var d=this._resolveWidth(a,"style");return null!=d?d:this._resolveWidth(a,"element")}if("element"==b){var e=a.outerWidth(!1);return 0>=e?"auto":e+"px"}if("style"==b){var f=a.attr("style");if("string"!=typeof f)return null;for(var g=f.split(";"),h=0,i=g.length;i>h;h+=1){var j=g[h].replace(/\s/g,""),k=j.match(c);if(null!==k&&k.length>=1)return k[1]}return null}return b},e.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},e.prototype._registerDomEvents=function(){var b=this;this.$element.on("change.select2",function(){b.dataAdapter.current(function(a){b.trigger("selection:update",{data:a})})}),this.$element.on("focus.select2",function(a){b.trigger("focus",a)}),this._syncA=c.bind(this._syncAttributes,this),this._syncS=c.bind(this._syncSubtree,this),this.$element[0].attachEvent&&this.$element[0].attachEvent("onpropertychange",this._syncA);var d=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;null!=d?(this._observer=new d(function(c){a.each(c,b._syncA),a.each(c,b._syncS)}),this._observer.observe(this.$element[0],{attributes:!0,childList:!0,subtree:!1})):this.$element[0].addEventListener&&(this.$element[0].addEventListener("DOMAttrModified",b._syncA,!1),this.$element[0].addEventListener("DOMNodeInserted",b._syncS,!1),this.$element[0].addEventListener("DOMNodeRemoved",b._syncS,!1))},e.prototype._registerDataEvents=function(){var a=this;this.dataAdapter.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerSelectionEvents=function(){var b=this,c=["toggle","focus"];this.selection.on("toggle",function(){b.toggleDropdown()}),this.selection.on("focus",function(a){b.focus(a)}),this.selection.on("*",function(d,e){-1===a.inArray(d,c)&&b.trigger(d,e)})},e.prototype._registerDropdownEvents=function(){var a=this;this.dropdown.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerResultsEvents=function(){var a=this;this.results.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerEvents=function(){var a=this;this.on("open",function(){a.$container.addClass("select2-container--open")}),this.on("close",function(){a.$container.removeClass("select2-container--open")}),this.on("enable",function(){a.$container.removeClass("select2-container--disabled")}),this.on("disable",function(){a.$container.addClass("select2-container--disabled")}),this.on("blur",function(){a.$container.removeClass("select2-container--focus")}),this.on("query",function(b){a.isOpen()||a.trigger("open",{}),this.dataAdapter.query(b,function(c){a.trigger("results:all",{data:c,query:b})})}),this.on("query:append",function(b){this.dataAdapter.query(b,function(c){a.trigger("results:append",{data:c,query:b})})}),this.on("keypress",function(b){var c=b.which;a.isOpen()?c===d.ESC||c===d.TAB||c===d.UP&&b.altKey?(a.close(),b.preventDefault()):c===d.ENTER?(a.trigger("results:select",{}),b.preventDefault()):c===d.SPACE&&b.ctrlKey?(a.trigger("results:toggle",{}),b.preventDefault()):c===d.UP?(a.trigger("results:previous",{}),b.preventDefault()):c===d.DOWN&&(a.trigger("results:next",{}),b.preventDefault()):(c===d.ENTER||c===d.SPACE||c===d.DOWN&&b.altKey)&&(a.open(),b.preventDefault())})},e.prototype._syncAttributes=function(){this.options.set("disabled",this.$element.prop("disabled")),this.options.get("disabled")?(this.isOpen()&&this.close(),this.trigger("disable",{})):this.trigger("enable",{})},e.prototype._syncSubtree=function(a,b){var c=!1,d=this;if(!a||!a.target||"OPTION"===a.target.nodeName||"OPTGROUP"===a.target.nodeName){if(b)if(b.addedNodes&&b.addedNodes.length>0)for(var e=0;e0&&(c=!0);else c=!0;c&&this.dataAdapter.current(function(a){d.trigger("selection:update",{data:a})})}},e.prototype.trigger=function(a,b){var c=e.__super__.trigger,d={open:"opening",close:"closing",select:"selecting",unselect:"unselecting"};if(void 0===b&&(b={}),a in d){var f=d[a],g={prevented:!1,name:a,args:b};if(c.call(this,f,g),g.prevented)return void(b.prevented=!0)}c.call(this,a,b)},e.prototype.toggleDropdown=function(){this.options.get("disabled")||(this.isOpen()?this.close():this.open())},e.prototype.open=function(){this.isOpen()||this.trigger("query",{})},e.prototype.close=function(){this.isOpen()&&this.trigger("close",{})},e.prototype.isOpen=function(){return this.$container.hasClass("select2-container--open")},e.prototype.hasFocus=function(){return this.$container.hasClass("select2-container--focus")},e.prototype.focus=function(a){this.hasFocus()||(this.$container.addClass("select2-container--focus"),this.trigger("focus",{}))},e.prototype.enable=function(a){this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("enable")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop("disabled") instead.'),(null==a||0===a.length)&&(a=[!0]);var b=!a[0];this.$element.prop("disabled",b)},e.prototype.data=function(){this.options.get("debug")&&arguments.length>0&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2("data")`. You should consider setting the value instead using `$element.val()`.');var a=[];return this.dataAdapter.current(function(b){a=b}),a},e.prototype.val=function(b){if(this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("val")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==b||0===b.length)return this.$element.val();var c=b[0];a.isArray(c)&&(c=a.map(c,function(a){return a.toString()})),this.$element.val(c).trigger("change")},e.prototype.destroy=function(){this.$container.remove(),this.$element[0].detachEvent&&this.$element[0].detachEvent("onpropertychange",this._syncA),null!=this._observer?(this._observer.disconnect(),this._observer=null):this.$element[0].removeEventListener&&(this.$element[0].removeEventListener("DOMAttrModified",this._syncA,!1),this.$element[0].removeEventListener("DOMNodeInserted",this._syncS,!1),this.$element[0].removeEventListener("DOMNodeRemoved",this._syncS,!1)),this._syncA=null,this._syncS=null,this.$element.off(".select2"),this.$element.attr("tabindex",this.$element.data("old-tabindex")),this.$element.removeClass("select2-hidden-accessible"),this.$element.attr("aria-hidden","false"),this.$element.removeData("select2"),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null; +},e.prototype.render=function(){var b=a('');return b.attr("dir",this.options.get("dir")),this.$container=b,this.$container.addClass("select2-container--"+this.options.get("theme")),b.data("element",this.$element),b},e}),b.define("jquery-mousewheel",["jquery"],function(a){return a}),b.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults"],function(a,b,c,d){if(null==a.fn.select2){var e=["open","close","destroy"];a.fn.select2=function(b){if(b=b||{},"object"==typeof b)return this.each(function(){var d=a.extend(!0,{},b);new c(a(this),d)}),this;if("string"==typeof b){var d,f=Array.prototype.slice.call(arguments,1);return this.each(function(){var c=a(this).data("select2");null==c&&window.console&&console.error&&console.error("The select2('"+b+"') method was called on an element that is not using Select2."),d=c[b].apply(c,f)}),a.inArray(b,e)>-1?this:d}throw new Error("Invalid arguments for Select2: "+b)}}return null==a.fn.select2.defaults&&(a.fn.select2.defaults=d),c}),{define:b.define,require:b.require}}(),c=b.require("jquery.select2");return a.fn.select2.amd=b,c}); \ No newline at end of file diff --git a/public/static/libs/slick/ajax-loader.gif b/public/static/libs/slick/ajax-loader.gif new file mode 100644 index 0000000..e0e6e97 Binary files /dev/null and b/public/static/libs/slick/ajax-loader.gif differ diff --git a/public/static/libs/slick/fonts/slick.eot b/public/static/libs/slick/fonts/slick.eot new file mode 100644 index 0000000..2cbab9c Binary files /dev/null and b/public/static/libs/slick/fonts/slick.eot differ diff --git a/public/static/libs/slick/fonts/slick.svg b/public/static/libs/slick/fonts/slick.svg new file mode 100644 index 0000000..60fbaf1 --- /dev/null +++ b/public/static/libs/slick/fonts/slick.svg @@ -0,0 +1,14 @@ + + + +Generated by Fontastic.me + + + + + + + + + + diff --git a/public/static/libs/slick/fonts/slick.ttf b/public/static/libs/slick/fonts/slick.ttf new file mode 100644 index 0000000..9d03461 Binary files /dev/null and b/public/static/libs/slick/fonts/slick.ttf differ diff --git a/public/static/libs/slick/fonts/slick.woff b/public/static/libs/slick/fonts/slick.woff new file mode 100644 index 0000000..8ee9972 Binary files /dev/null and b/public/static/libs/slick/fonts/slick.woff differ diff --git a/public/static/libs/slick/slick-theme.css b/public/static/libs/slick/slick-theme.css new file mode 100644 index 0000000..c352d29 --- /dev/null +++ b/public/static/libs/slick/slick-theme.css @@ -0,0 +1,204 @@ +@charset 'UTF-8'; +/* Slider */ +.slick-loading .slick-list +{ + background: #fff url('./ajax-loader.gif') center center no-repeat; +} + +/* Icons */ +@font-face +{ + font-family: 'slick'; + font-weight: normal; + font-style: normal; + + src: url('./fonts/slick.eot'); + src: url('./fonts/slick.eot?#iefix') format('embedded-opentype'), url('./fonts/slick.woff') format('woff'), url('./fonts/slick.ttf') format('truetype'), url('./fonts/slick.svg#slick') format('svg'); +} +/* Arrows */ +.slick-prev, +.slick-next +{ + font-size: 0; + line-height: 0; + + position: absolute; + top: 50%; + + display: block; + + width: 20px; + height: 20px; + padding: 0; + -webkit-transform: translate(0, -50%); + -ms-transform: translate(0, -50%); + transform: translate(0, -50%); + + cursor: pointer; + + color: transparent; + border: none; + outline: none; + background: transparent; +} +.slick-prev:hover, +.slick-prev:focus, +.slick-next:hover, +.slick-next:focus +{ + color: transparent; + outline: none; + background: transparent; +} +.slick-prev:hover:before, +.slick-prev:focus:before, +.slick-next:hover:before, +.slick-next:focus:before +{ + opacity: 1; +} +.slick-prev.slick-disabled:before, +.slick-next.slick-disabled:before +{ + opacity: .25; +} + +.slick-prev:before, +.slick-next:before +{ + font-family: 'slick'; + font-size: 20px; + line-height: 1; + + opacity: .75; + color: white; + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.slick-prev +{ + left: -25px; +} +[dir='rtl'] .slick-prev +{ + right: -25px; + left: auto; +} +.slick-prev:before +{ + content: '←'; +} +[dir='rtl'] .slick-prev:before +{ + content: '→'; +} + +.slick-next +{ + right: -25px; +} +[dir='rtl'] .slick-next +{ + right: auto; + left: -25px; +} +.slick-next:before +{ + content: '→'; +} +[dir='rtl'] .slick-next:before +{ + content: '←'; +} + +/* Dots */ +.slick-dotted.slick-slider +{ + margin-bottom: 30px; +} + +.slick-dots +{ + position: absolute; + bottom: -25px; + + display: block; + + width: 100%; + padding: 0; + margin: 0; + + list-style: none; + + text-align: center; +} +.slick-dots li +{ + position: relative; + + display: inline-block; + + width: 20px; + height: 20px; + margin: 0 5px; + padding: 0; + + cursor: pointer; +} +.slick-dots li button +{ + font-size: 0; + line-height: 0; + + display: block; + + width: 20px; + height: 20px; + padding: 5px; + + cursor: pointer; + + color: transparent; + border: 0; + outline: none; + background: transparent; +} +.slick-dots li button:hover, +.slick-dots li button:focus +{ + outline: none; +} +.slick-dots li button:hover:before, +.slick-dots li button:focus:before +{ + opacity: 1; +} +.slick-dots li button:before +{ + font-family: 'slick'; + font-size: 6px; + line-height: 20px; + + position: absolute; + top: 0; + left: 0; + + width: 20px; + height: 20px; + + content: '•'; + text-align: center; + + opacity: .25; + color: black; + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.slick-dots li.slick-active button:before +{ + opacity: .75; + color: black; +} diff --git a/public/static/libs/slick/slick-theme.min.css b/public/static/libs/slick/slick-theme.min.css new file mode 100644 index 0000000..999e756 --- /dev/null +++ b/public/static/libs/slick/slick-theme.min.css @@ -0,0 +1 @@ +@charset 'UTF-8';.slick-dots,.slick-next,.slick-prev{position:absolute;display:block;padding:0}.slick-dots li button:before,.slick-next:before,.slick-prev:before{font-family:slick;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.slick-loading .slick-list{background:url(ajax-loader.gif) center center no-repeat #fff}@font-face{font-family:slick;font-weight:400;font-style:normal;src:url(fonts/slick.eot);src:url(fonts/slick.eot?#iefix) format('embedded-opentype'),url(fonts/slick.woff) format('woff'),url(fonts/slick.ttf) format('truetype'),url(fonts/slick.svg#slick) format('svg')}.slick-next,.slick-prev{font-size:0;line-height:0;top:50%;width:20px;height:20px;-webkit-transform:translate(0,-50%);-ms-transform:translate(0,-50%);transform:translate(0,-50%);cursor:pointer;color:transparent;border:none;outline:0;background:0 0}.slick-next:focus,.slick-next:hover,.slick-prev:focus,.slick-prev:hover{color:transparent;outline:0;background:0 0}.slick-next:focus:before,.slick-next:hover:before,.slick-prev:focus:before,.slick-prev:hover:before{opacity:1}.slick-next.slick-disabled:before,.slick-prev.slick-disabled:before{opacity:.25}.slick-next:before,.slick-prev:before{font-size:20px;line-height:1;opacity:.75;color:#fff}.slick-prev{left:-25px}[dir=rtl] .slick-prev{right:-25px;left:auto}.slick-prev:before{content:'←'}.slick-next:before,[dir=rtl] .slick-prev:before{content:'→'}.slick-next{right:-25px}[dir=rtl] .slick-next{right:auto;left:-25px}[dir=rtl] .slick-next:before{content:'←'}.slick-dotted.slick-slider{margin-bottom:30px}.slick-dots{bottom:-25px;width:100%;margin:0;list-style:none;text-align:center}.slick-dots li{position:relative;display:inline-block;width:20px;height:20px;margin:0 5px;padding:0;cursor:pointer}.slick-dots li button{font-size:0;line-height:0;display:block;width:20px;height:20px;padding:5px;cursor:pointer;color:transparent;border:0;outline:0;background:0 0}.slick-dots li button:focus,.slick-dots li button:hover{outline:0}.slick-dots li button:focus:before,.slick-dots li button:hover:before{opacity:1}.slick-dots li button:before{font-size:6px;line-height:20px;position:absolute;top:0;left:0;width:20px;height:20px;content:'•';text-align:center;opacity:.25;color:#000}.slick-dots li.slick-active button:before{opacity:.75;color:#000} \ No newline at end of file diff --git a/public/static/libs/slick/slick.css b/public/static/libs/slick/slick.css new file mode 100644 index 0000000..a60636d --- /dev/null +++ b/public/static/libs/slick/slick.css @@ -0,0 +1,117 @@ +/* Slider */ +.slick-slider +{ + position: relative; + + display: block; + box-sizing: border-box; + + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + -webkit-touch-callout: none; + -khtml-user-select: none; + -ms-touch-action: pan-y; + touch-action: pan-y; + -webkit-tap-highlight-color: transparent; +} + +.slick-list +{ + position: relative; + + display: block; + overflow: hidden; + + margin: 0; + padding: 0; +} +.slick-list:focus +{ + outline: none; +} +.slick-list.dragging +{ + cursor: pointer; + cursor: hand; +} + +.slick-slider .slick-track, +.slick-slider .slick-list +{ + -webkit-transform: translate3d(0, 0, 0); + -moz-transform: translate3d(0, 0, 0); + -ms-transform: translate3d(0, 0, 0); + -o-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); +} + +.slick-track +{ + position: relative; + top: 0; + left: 0; + + display: block; +} +.slick-track:before, +.slick-track:after +{ + display: table; + + content: ''; +} +.slick-track:after +{ + clear: both; +} +.slick-loading .slick-track +{ + visibility: hidden; +} + +.slick-slide +{ + display: none; + float: left; + + height: 100%; + min-height: 1px; +} +[dir='rtl'] .slick-slide +{ + float: right; +} +.slick-slide img +{ + display: block; +} +.slick-slide.slick-loading img +{ + display: none; +} +.slick-slide.dragging img +{ + pointer-events: none; +} +.slick-initialized .slick-slide +{ + display: block; +} +.slick-loading .slick-slide +{ + visibility: hidden; +} +.slick-vertical .slick-slide +{ + display: block; + + height: auto; + + border: 1px solid transparent; +} +.slick-arrow.slick-hidden { + display: none; +} diff --git a/public/static/libs/slick/slick.js b/public/static/libs/slick/slick.js new file mode 100644 index 0000000..cb3fe5b --- /dev/null +++ b/public/static/libs/slick/slick.js @@ -0,0 +1,2892 @@ +/* + _ _ _ _ + ___| (_) ___| | __ (_)___ +/ __| | |/ __| |/ / | / __| +\__ \ | | (__| < _ | \__ \ +|___/_|_|\___|_|\_(_)/ |___/ + |__/ + + Version: 1.6.0 + Author: Ken Wheeler + Website: http://kenwheeler.github.io + Docs: http://kenwheeler.github.io/slick + Repo: http://github.com/kenwheeler/slick + Issues: http://github.com/kenwheeler/slick/issues + + */ +/* global window, document, define, jQuery, setInterval, clearInterval */ +(function(factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof exports !== 'undefined') { + module.exports = factory(require('jquery')); + } else { + factory(jQuery); + } + +}(function($) { + 'use strict'; + var Slick = window.Slick || {}; + + Slick = (function() { + + var instanceUid = 0; + + function Slick(element, settings) { + + var _ = this, dataSettings; + + _.defaults = { + accessibility: true, + adaptiveHeight: false, + appendArrows: $(element), + appendDots: $(element), + arrows: true, + asNavFor: null, + prevArrow: '', + nextArrow: '', + autoplay: false, + autoplaySpeed: 3000, + centerMode: false, + centerPadding: '50px', + cssEase: 'ease', + customPaging: function(slider, i) { + return $('',nextArrow:'',autoplay:!1,autoplaySpeed:3e3,centerMode:!1,centerPadding:"50px",cssEase:"ease",customPaging:function(b,c){return a(''; + + self.$dialog = ui.dialog({ + title: lang.databasic.name, + fade: options.dialogsFade, + body: body, + footer: footer + }).render().appendTo($container); + + // create popover + self.$popover = ui.popover({ + className: 'ext-databasic-popover' + }).render().appendTo('body'); + var $content = self.$popover.find('.popover-content'); + + context.invoke('buttons.build', $content, options.popover.databasic); + }; + + self.destroy = function () { + self.$popover.remove(); + self.$popover = null; + self.$dialog.remove(); + self.$dialog = null; + }; + + self.update = function () { + // Prevent focusing on editable when invoke('code') is executed + if (!context.invoke('editor.hasFocus')) { + self.hidePopover(); + return; + } + + var rng = context.invoke('editor.createRange'); + var visible = false; + + if (rng.isOnData()) + { + var $data = $(rng.sc).closest('data.ext-databasic'); + + if ($data.length) + { + var pos = dom.posFromPlaceholder($data[0]); + + self.$popover.css({ + display: 'block', + left: pos.left, + top: pos.top + }); + + // save editor target to let size buttons resize the container + context.invoke('editor.saveTarget', $data[0]); + + visible = true; + } + + } + + // hide if not visible + if (!visible) { + self.hidePopover(); + } + + }; + + self.hidePopover = function () { + self.$popover.hide(); + }; + + // define plugin dialog + self.getInfo = function () { + var rng = context.invoke('editor.createRange'); + + if (rng.isOnData()) + { + var $data = $(rng.sc).closest('data.ext-databasic'); + + if ($data.length) + { + // Get the first node on range(for edit). + return { + node: $data, + test: $data.attr('data-test') + }; + } + } + + return {}; + }; + + self.setContent = function ($node) { + $node.html('

              ' + self.icon + ' ' + lang.databasic.name + ': ' + + $node.attr('data-test') + '

              '); + }; + + self.updateNode = function (info) { + self.setContent(info.node + .attr('data-test', info.test)); + }; + + self.createNode = function (info) { + var $node = $(''); + + if ($node) { + // save node to info structure + info.node = $node; + // insert node into editor dom + context.invoke('editor.insertNode', $node[0]); + } + + return $node; + }; + + self.showDialog = function () { + var info = self.getInfo(); + var newNode = !info.node; + context.invoke('editor.saveRange'); + + self + .openDialog(info) + .then(function (dialogInfo) { + // [workaround] hide dialog before restore range for IE range focus + ui.hideDialog(self.$dialog); + context.invoke('editor.restoreRange'); + + // insert a new node + if (newNode) + { + self.createNode(info); + } + + // update info with dialog info + $.extend(info, dialogInfo); + + self.updateNode(info); + }) + .fail(function () { + context.invoke('editor.restoreRange'); + }); + + }; + + self.openDialog = function (info) { + return $.Deferred(function (deferred) { + var $inpTest = self.$dialog.find('.ext-databasic-test'); + var $saveBtn = self.$dialog.find('.ext-databasic-save'); + var onKeyup = function (event) { + if (event.keyCode === 13) + { + $saveBtn.trigger('click'); + } + }; + + ui.onDialogShown(self.$dialog, function () { + context.triggerEvent('dialog.shown'); + + $inpTest.val(info.test).on('input', function () { + ui.toggleBtn($saveBtn, $inpTest.val()); + }).trigger('focus').on('keyup', onKeyup); + + $saveBtn + .text(info.node ? lang.databasic.edit : lang.databasic.insert) + .click(function (event) { + event.preventDefault(); + + deferred.resolve({ test: $inpTest.val() }); + }); + + // init save button + ui.toggleBtn($saveBtn, $inpTest.val()); + }); + + ui.onDialogHidden(self.$dialog, function () { + $inpTest.off('input keyup'); + $saveBtn.off('click'); + + if (deferred.state() === 'pending') { + deferred.reject(); + } + }); + + ui.showDialog(self.$dialog); + }); + }; + }; + + // Extends summernote + $.extend(true, $.summernote, { + plugins: { + databasic: DataBasicPlugin + }, + + options: { + popover: { + databasic: [ + ['databasic', ['databasicDialog', 'databasicSize100', 'databasicSize50', 'databasicSize25']] + ] + } + }, + + // add localization texts + lang: { + 'en-US': { + databasic: { + name: 'Basic Data Container', + insert: 'insert basic data container', + edit: 'edit basic data container', + testLabel: 'test input' + } + } + } + + }); + +})); diff --git a/public/static/libs/summernote/plugin/hello/summernote-ext-hello.js b/public/static/libs/summernote/plugin/hello/summernote-ext-hello.js new file mode 100644 index 0000000..a8b4045 --- /dev/null +++ b/public/static/libs/summernote/plugin/hello/summernote-ext-hello.js @@ -0,0 +1,82 @@ +(function (factory) { + /* global define */ + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof module === 'object' && module.exports) { + // Node/CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + factory(window.jQuery); + } +}(function ($) { + + // Extends plugins for adding hello. + // - plugin is external module for customizing. + $.extend($.summernote.plugins, { + /** + * @param {Object} context - context object has status of editor. + */ + 'hello': function (context) { + var self = this; + + // ui has renders to build ui elements. + // - you can create a button with `ui.button` + var ui = $.summernote.ui; + + // add hello button + context.memo('button.hello', function () { + // create button + var button = ui.button({ + contents: ' Hello', + tooltip: 'hello', + click: function () { + self.$panel.show(); + self.$panel.hide(500); + // invoke insertText method with 'hello' on editor module. + context.invoke('editor.insertText', 'hello'); + } + }); + + // create jQuery object from button instance. + var $hello = button.render(); + return $hello; + }); + + // This events will be attached when editor is initialized. + this.events = { + // This will be called after modules are initialized. + 'summernote.init': function (we, e) { + console.log('summernote initialized', we, e); + }, + // This will be called when user releases a key on editable. + 'summernote.keyup': function (we, e) { + console.log('summernote keyup', we, e); + } + }; + + // This method will be called when editor is initialized by $('..').summernote(); + // You can create elements for plugin + this.initialize = function () { + this.$panel = $('
              ').css({ + position: 'absolute', + width: 100, + height: 100, + left: '50%', + top: '50%', + background: 'red' + }).hide(); + + this.$panel.appendTo('body'); + }; + + // This methods will be called when editor is destroyed by $('..').summernote('destroy'); + // You should remove elements on `initialize`. + this.destroy = function () { + this.$panel.remove(); + this.$panel = null; + }; + } + }); +})); diff --git a/public/static/libs/summernote/plugin/specialchars/summernote-ext-specialchars.js b/public/static/libs/summernote/plugin/specialchars/summernote-ext-specialchars.js new file mode 100644 index 0000000..df87c2a --- /dev/null +++ b/public/static/libs/summernote/plugin/specialchars/summernote-ext-specialchars.js @@ -0,0 +1,315 @@ +(function (factory) { + /* global define */ + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof module === 'object' && module.exports) { + // Node/CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + factory(window.jQuery); + } +}(function ($) { + $.extend($.summernote.plugins, { + 'specialchars': function (context) { + var self = this; + var ui = $.summernote.ui; + + var $editor = context.layoutInfo.editor; + var options = context.options; + var lang = options.langInfo; + + var KEY = { + UP: 38, + DOWN: 40, + LEFT: 37, + RIGHT: 39, + ENTER: 13 + }; + var COLUMN_LENGTH = 15; + var COLUMN_WIDTH = 35; + + var currentColumn, currentRow, totalColumn, totalRow = 0; + + // special characters data set + var specialCharDataSet = [ + '"', '&', '<', '>', '¡', '¢', + '£', '¤', '¥', '¦', '§', + '¨', '©', 'ª', '«', '¬', + '®', '¯', '°', '±', '²', + '³', '´', 'µ', '¶', '·', + '¸', '¹', 'º', '»', '¼', + '½', '¾', '¿', '×', '÷', + 'ƒ', 'ˆ', '˜', '–', '—', + '‘', '’', '‚', '“', '”', + '„', '†', '‡', '•', '…', + '‰', '′', '″', '‹', '›', + '‾', '⁄', '€', 'ℑ', '℘', + 'ℜ', '™', 'ℵ', '←', '↑', + '→', '↓', '↔', '↵', '⇐', + '⇑', '⇒', '⇓', '⇔', '∀', + '∂', '∃', '∅', '∇', '∈', + '∉', '∋', '∏', '∑', '−', + '∗', '√', '∝', '∞', '∠', + '∧', '∨', '∩', '∪', '∫', + '∴', '∼', '≅', '≈', '≠', + '≡', '≤', '≥', '⊂', '⊃', + '⊄', '⊆', '⊇', '⊕', '⊗', + '⊥', '⋅', '⌈', '⌉', '⌊', + '⌋', '◊', '♠', '♣', '♥', + '♦' + ]; + + context.memo('button.specialCharacter', function () { + return ui.button({ + contents: '', + tooltip: lang.specialChar.specialChar, + click: function () { + self.show(); + } + }).render(); + }); + + /** + * Make Special Characters Table + * + * @member plugin.specialChar + * @private + * @return {jQuery} + */ + this.makeSpecialCharSetTable = function () { + var $table = $(''); + $.each(specialCharDataSet, function (idx, text) { + var $td = $('') : $table.find('tr').last(); + + var $button = ui.button({ + callback: function ($node) { + $node.html(text); + $node.attr('title', text); + $node.attr('data-value', encodeURIComponent(text)); + $node.css({ + width: COLUMN_WIDTH, + 'margin-right': '2px', + 'margin-bottom': '2px' + }); + } + }).render(); + + $td.append($button); + + $tr.append($td); + if (idx % COLUMN_LENGTH === 0) { + $table.append($tr); + } + }); + + totalRow = $table.find('tr').length; + totalColumn = COLUMN_LENGTH; + + return $table; + }; + + this.initialize = function () { + var $container = options.dialogsInBody ? $(document.body) : $editor; + + var body = '
              ' + this.makeSpecialCharSetTable()[0].outerHTML + '
              '; + + this.$dialog = ui.dialog({ + title: lang.specialChar.select, + body: body + }).render().appendTo($container); + }; + + this.show = function () { + var text = context.invoke('editor.getSelectedText'); + context.invoke('editor.saveRange'); + this.showSpecialCharDialog(text).then(function (selectChar) { + context.invoke('editor.restoreRange'); + + // build node + var $node = $('').html(selectChar)[0]; + + if ($node) { + // insert video node + context.invoke('editor.insertNode', $node); + } + }).fail(function () { + context.invoke('editor.restoreRange'); + }); + }; + + /** + * show image dialog + * + * @param {jQuery} $dialog + * @return {Promise} + */ + this.showSpecialCharDialog = function (text) { + return $.Deferred(function (deferred) { + var $specialCharDialog = self.$dialog; + var $specialCharNode = $specialCharDialog.find('.note-specialchar-node'); + var $selectedNode = null; + var ARROW_KEYS = [KEY.UP, KEY.DOWN, KEY.LEFT, KEY.RIGHT]; + var ENTER_KEY = KEY.ENTER; + + function addActiveClass($target) { + if (!$target) { + return; + } + $target.find('button').addClass('active'); + $selectedNode = $target; + } + + function removeActiveClass($target) { + $target.find('button').removeClass('active'); + $selectedNode = null; + } + + // find next node + function findNextNode(row, column) { + var findNode = null; + $.each($specialCharNode, function (idx, $node) { + var findRow = Math.ceil((idx + 1) / COLUMN_LENGTH); + var findColumn = ((idx + 1) % COLUMN_LENGTH === 0) ? COLUMN_LENGTH : (idx + 1) % COLUMN_LENGTH; + if (findRow === row && findColumn === column) { + findNode = $node; + return false; + } + }); + return $(findNode); + } + + function arrowKeyHandler(keyCode) { + // left, right, up, down key + var $nextNode; + var lastRowColumnLength = $specialCharNode.length % totalColumn; + + if (KEY.LEFT === keyCode) { + + if (currentColumn > 1) { + currentColumn = currentColumn - 1; + } else if (currentRow === 1 && currentColumn === 1) { + currentColumn = lastRowColumnLength; + currentRow = totalRow; + } else { + currentColumn = totalColumn; + currentRow = currentRow - 1; + } + + } else if (KEY.RIGHT === keyCode) { + + if (currentRow === totalRow && lastRowColumnLength === currentColumn) { + currentColumn = 1; + currentRow = 1; + } else if (currentColumn < totalColumn) { + currentColumn = currentColumn + 1; + } else { + currentColumn = 1; + currentRow = currentRow + 1; + } + + } else if (KEY.UP === keyCode) { + if (currentRow === 1 && lastRowColumnLength < currentColumn) { + currentRow = totalRow - 1; + } else { + currentRow = currentRow - 1; + } + } else if (KEY.DOWN === keyCode) { + currentRow = currentRow + 1; + } + + if (currentRow === totalRow && currentColumn > lastRowColumnLength) { + currentRow = 1; + } else if (currentRow > totalRow) { + currentRow = 1; + } else if (currentRow < 1) { + currentRow = totalRow; + } + + $nextNode = findNextNode(currentRow, currentColumn); + + if ($nextNode) { + removeActiveClass($selectedNode); + addActiveClass($nextNode); + } + } + + function enterKeyHandler() { + if (!$selectedNode) { + return; + } + + deferred.resolve(decodeURIComponent($selectedNode.find('button').attr('data-value'))); + $specialCharDialog.modal('hide'); + } + + function keyDownEventHandler(event) { + event.preventDefault(); + var keyCode = event.keyCode; + if (keyCode === undefined || keyCode === null) { + return; + } + // check arrowKeys match + if (ARROW_KEYS.indexOf(keyCode) > -1) { + if ($selectedNode === null) { + addActiveClass($specialCharNode.eq(0)); + currentColumn = 1; + currentRow = 1; + return; + } + arrowKeyHandler(keyCode); + } else if (keyCode === ENTER_KEY) { + enterKeyHandler(); + } + return false; + } + + // remove class + removeActiveClass($specialCharNode); + + // find selected node + if (text) { + for (var i = 0; i < $specialCharNode.length; i++) { + var $checkNode = $($specialCharNode[i]); + if ($checkNode.text() === text) { + addActiveClass($checkNode); + currentRow = Math.ceil((i + 1) / COLUMN_LENGTH); + currentColumn = (i + 1) % COLUMN_LENGTH; + } + } + } + + ui.onDialogShown(self.$dialog, function () { + + $(document).on('keydown', keyDownEventHandler); + + self.$dialog.find('button').tooltip(); + + $specialCharNode.on('click', function (event) { + event.preventDefault(); + deferred.resolve(decodeURIComponent($(event.currentTarget).find('button').attr('data-value'))); + ui.hideDialog(self.$dialog); + }); + + }); + + ui.onDialogHidden(self.$dialog, function () { + $specialCharNode.off('click'); + + self.$dialog.find('button').tooltip('destroy'); + + $(document).off('keydown', keyDownEventHandler); + + if (deferred.state() === 'pending') { + deferred.reject(); + } + }); + + ui.showDialog(self.$dialog); + }); + }; + } + }); +})); diff --git a/public/static/libs/summernote/summernote.js b/public/static/libs/summernote/summernote.js new file mode 100644 index 0000000..df09785 --- /dev/null +++ b/public/static/libs/summernote/summernote.js @@ -0,0 +1,7046 @@ +/** + * Super simple wysiwyg editor v0.8.2 + * http://summernote.org/ + * + * summernote.js + * Copyright 2013-2016 Alan Hong. and other contributors + * summernote may be freely distributed under the MIT license./ + * + * Date: 2016-08-08T01:21Z + */ +(function (factory) { + /* global define */ + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof module === 'object' && module.exports) { + // Node/CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + /** + * @class core.func + * + * func utils (for high-order func's arg) + * + * @singleton + * @alternateClassName func + */ + var func = (function () { + var eq = function (itemA) { + return function (itemB) { + return itemA === itemB; + }; + }; + + var eq2 = function (itemA, itemB) { + return itemA === itemB; + }; + + var peq2 = function (propName) { + return function (itemA, itemB) { + return itemA[propName] === itemB[propName]; + }; + }; + + var ok = function () { + return true; + }; + + var fail = function () { + return false; + }; + + var not = function (f) { + return function () { + return !f.apply(f, arguments); + }; + }; + + var and = function (fA, fB) { + return function (item) { + return fA(item) && fB(item); + }; + }; + + var self = function (a) { + return a; + }; + + var invoke = function (obj, method) { + return function () { + return obj[method].apply(obj, arguments); + }; + }; + + var idCounter = 0; + + /** + * generate a globally-unique id + * + * @param {String} [prefix] + */ + var uniqueId = function (prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + /** + * returns bnd (bounds) from rect + * + * - IE Compatibility Issue: http://goo.gl/sRLOAo + * - Scroll Issue: http://goo.gl/sNjUc + * + * @param {Rect} rect + * @return {Object} bounds + * @return {Number} bounds.top + * @return {Number} bounds.left + * @return {Number} bounds.width + * @return {Number} bounds.height + */ + var rect2bnd = function (rect) { + var $document = $(document); + return { + top: rect.top + $document.scrollTop(), + left: rect.left + $document.scrollLeft(), + width: rect.right - rect.left, + height: rect.bottom - rect.top + }; + }; + + /** + * returns a copy of the object where the keys have become the values and the values the keys. + * @param {Object} obj + * @return {Object} + */ + var invertObject = function (obj) { + var inverted = {}; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + inverted[obj[key]] = key; + } + } + return inverted; + }; + + /** + * @param {String} namespace + * @param {String} [prefix] + * @return {String} + */ + var namespaceToCamel = function (namespace, prefix) { + prefix = prefix || ''; + return prefix + namespace.split('.').map(function (name) { + return name.substring(0, 1).toUpperCase() + name.substring(1); + }).join(''); + }; + + /** + * Returns a function, that, as long as it continues to be invoked, will not + * be triggered. The function will be called after it stops being called for + * N milliseconds. If `immediate` is passed, trigger the function on the + * leading edge, instead of the trailing. + * @param {Function} func + * @param {Number} wait + * @param {Boolean} immediate + * @return {Function} + */ + var debounce = function (func, wait, immediate) { + var timeout; + return function () { + var context = this, args = arguments; + var later = function () { + timeout = null; + if (!immediate) { + func.apply(context, args); + } + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + func.apply(context, args); + } + }; + }; + + return { + eq: eq, + eq2: eq2, + peq2: peq2, + ok: ok, + fail: fail, + self: self, + not: not, + and: and, + invoke: invoke, + uniqueId: uniqueId, + rect2bnd: rect2bnd, + invertObject: invertObject, + namespaceToCamel: namespaceToCamel, + debounce: debounce + }; + })(); + + /** + * @class core.list + * + * list utils + * + * @singleton + * @alternateClassName list + */ + var list = (function () { + /** + * returns the first item of an array. + * + * @param {Array} array + */ + var head = function (array) { + return array[0]; + }; + + /** + * returns the last item of an array. + * + * @param {Array} array + */ + var last = function (array) { + return array[array.length - 1]; + }; + + /** + * returns everything but the last entry of the array. + * + * @param {Array} array + */ + var initial = function (array) { + return array.slice(0, array.length - 1); + }; + + /** + * returns the rest of the items in an array. + * + * @param {Array} array + */ + var tail = function (array) { + return array.slice(1); + }; + + /** + * returns item of array + */ + var find = function (array, pred) { + for (var idx = 0, len = array.length; idx < len; idx ++) { + var item = array[idx]; + if (pred(item)) { + return item; + } + } + }; + + /** + * returns true if all of the values in the array pass the predicate truth test. + */ + var all = function (array, pred) { + for (var idx = 0, len = array.length; idx < len; idx ++) { + if (!pred(array[idx])) { + return false; + } + } + return true; + }; + + /** + * returns index of item + */ + var indexOf = function (array, item) { + return $.inArray(item, array); + }; + + /** + * returns true if the value is present in the list. + */ + var contains = function (array, item) { + return indexOf(array, item) !== -1; + }; + + /** + * get sum from a list + * + * @param {Array} array - array + * @param {Function} fn - iterator + */ + var sum = function (array, fn) { + fn = fn || func.self; + return array.reduce(function (memo, v) { + return memo + fn(v); + }, 0); + }; + + /** + * returns a copy of the collection with array type. + * @param {Collection} collection - collection eg) node.childNodes, ... + */ + var from = function (collection) { + var result = [], idx = -1, length = collection.length; + while (++idx < length) { + result[idx] = collection[idx]; + } + return result; + }; + + /** + * returns whether list is empty or not + */ + var isEmpty = function (array) { + return !array || !array.length; + }; + + /** + * cluster elements by predicate function. + * + * @param {Array} array - array + * @param {Function} fn - predicate function for cluster rule + * @param {Array[]} + */ + var clusterBy = function (array, fn) { + if (!array.length) { return []; } + var aTail = tail(array); + return aTail.reduce(function (memo, v) { + var aLast = last(memo); + if (fn(last(aLast), v)) { + aLast[aLast.length] = v; + } else { + memo[memo.length] = [v]; + } + return memo; + }, [[head(array)]]); + }; + + /** + * returns a copy of the array with all false values removed + * + * @param {Array} array - array + * @param {Function} fn - predicate function for cluster rule + */ + var compact = function (array) { + var aResult = []; + for (var idx = 0, len = array.length; idx < len; idx ++) { + if (array[idx]) { aResult.push(array[idx]); } + } + return aResult; + }; + + /** + * produces a duplicate-free version of the array + * + * @param {Array} array + */ + var unique = function (array) { + var results = []; + + for (var idx = 0, len = array.length; idx < len; idx ++) { + if (!contains(results, array[idx])) { + results.push(array[idx]); + } + } + + return results; + }; + + /** + * returns next item. + * @param {Array} array + */ + var next = function (array, item) { + var idx = indexOf(array, item); + if (idx === -1) { return null; } + + return array[idx + 1]; + }; + + /** + * returns prev item. + * @param {Array} array + */ + var prev = function (array, item) { + var idx = indexOf(array, item); + if (idx === -1) { return null; } + + return array[idx - 1]; + }; + + return { head: head, last: last, initial: initial, tail: tail, + prev: prev, next: next, find: find, contains: contains, + all: all, sum: sum, from: from, isEmpty: isEmpty, + clusterBy: clusterBy, compact: compact, unique: unique }; + })(); + + var isSupportAmd = typeof define === 'function' && define.amd; + + /** + * returns whether font is installed or not. + * + * @param {String} fontName + * @return {Boolean} + */ + var isFontInstalled = function (fontName) { + var testFontName = fontName === 'Comic Sans MS' ? 'Courier New' : 'Comic Sans MS'; + var $tester = $('
              ').css({ + position: 'absolute', + left: '-9999px', + top: '-9999px', + fontSize: '200px' + }).text('mmmmmmmmmwwwwwww').appendTo(document.body); + + var originalWidth = $tester.css('fontFamily', testFontName).width(); + var width = $tester.css('fontFamily', fontName + ',' + testFontName).width(); + + $tester.remove(); + + return originalWidth !== width; + }; + + var userAgent = navigator.userAgent; + var isMSIE = /MSIE|Trident/i.test(userAgent); + var browserVersion; + if (isMSIE) { + var matches = /MSIE (\d+[.]\d+)/.exec(userAgent); + if (matches) { + browserVersion = parseFloat(matches[1]); + } + matches = /Trident\/.*rv:([0-9]{1,}[\.0-9]{0,})/.exec(userAgent); + if (matches) { + browserVersion = parseFloat(matches[1]); + } + } + + var isEdge = /Edge\/\d+/.test(userAgent); + + var hasCodeMirror = !!window.CodeMirror; + if (!hasCodeMirror && isSupportAmd && typeof require !== 'undefined') { + if (typeof require.resolve !== 'undefined') { + try { + // If CodeMirror can't be resolved, `require.resolve` will throw an + // exception and `hasCodeMirror` won't be set to `true`. + require.resolve('codemirror'); + hasCodeMirror = true; + } catch (e) { + // Do nothing. + } + } else if (typeof eval('require').specified !== 'undefined') { + hasCodeMirror = eval('require').specified('codemirror'); + } + } + + /** + * @class core.agent + * + * Object which check platform and agent + * + * @singleton + * @alternateClassName agent + */ + var agent = { + isMac: navigator.appVersion.indexOf('Mac') > -1, + isMSIE: isMSIE, + isEdge: isEdge, + isFF: !isEdge && /firefox/i.test(userAgent), + isPhantom: /PhantomJS/i.test(userAgent), + isWebkit: !isEdge && /webkit/i.test(userAgent), + isChrome: !isEdge && /chrome/i.test(userAgent), + isSafari: !isEdge && /safari/i.test(userAgent), + browserVersion: browserVersion, + jqueryVersion: parseFloat($.fn.jquery), + isSupportAmd: isSupportAmd, + hasCodeMirror: hasCodeMirror, + isFontInstalled: isFontInstalled, + isW3CRangeSupport: !!document.createRange + }; + + + var NBSP_CHAR = String.fromCharCode(160); + var ZERO_WIDTH_NBSP_CHAR = '\ufeff'; + + /** + * @class core.dom + * + * Dom functions + * + * @singleton + * @alternateClassName dom + */ + var dom = (function () { + /** + * @method isEditable + * + * returns whether node is `note-editable` or not. + * + * @param {Node} node + * @return {Boolean} + */ + var isEditable = function (node) { + return node && $(node).hasClass('note-editable'); + }; + + /** + * @method isControlSizing + * + * returns whether node is `note-control-sizing` or not. + * + * @param {Node} node + * @return {Boolean} + */ + var isControlSizing = function (node) { + return node && $(node).hasClass('note-control-sizing'); + }; + + /** + * @method makePredByNodeName + * + * returns predicate which judge whether nodeName is same + * + * @param {String} nodeName + * @return {Function} + */ + var makePredByNodeName = function (nodeName) { + nodeName = nodeName.toUpperCase(); + return function (node) { + return node && node.nodeName.toUpperCase() === nodeName; + }; + }; + + /** + * @method isText + * + * + * + * @param {Node} node + * @return {Boolean} true if node's type is text(3) + */ + var isText = function (node) { + return node && node.nodeType === 3; + }; + + /** + * @method isElement + * + * + * + * @param {Node} node + * @return {Boolean} true if node's type is element(1) + */ + var isElement = function (node) { + return node && node.nodeType === 1; + }; + + /** + * ex) br, col, embed, hr, img, input, ... + * @see http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements + */ + var isVoid = function (node) { + return node && /^BR|^IMG|^HR|^IFRAME|^BUTTON/.test(node.nodeName.toUpperCase()); + }; + + var isPara = function (node) { + if (isEditable(node)) { + return false; + } + + // Chrome(v31.0), FF(v25.0.1) use DIV for paragraph + return node && /^DIV|^P|^LI|^H[1-7]/.test(node.nodeName.toUpperCase()); + }; + + var isHeading = function (node) { + return node && /^H[1-7]/.test(node.nodeName.toUpperCase()); + }; + + var isPre = makePredByNodeName('PRE'); + + var isLi = makePredByNodeName('LI'); + + var isPurePara = function (node) { + return isPara(node) && !isLi(node); + }; + + var isTable = makePredByNodeName('TABLE'); + + var isData = makePredByNodeName('DATA'); + + var isInline = function (node) { + return !isBodyContainer(node) && + !isList(node) && + !isHr(node) && + !isPara(node) && + !isTable(node) && + !isBlockquote(node) && + !isData(node); + }; + + var isList = function (node) { + return node && /^UL|^OL/.test(node.nodeName.toUpperCase()); + }; + + var isHr = makePredByNodeName('HR'); + + var isCell = function (node) { + return node && /^TD|^TH/.test(node.nodeName.toUpperCase()); + }; + + var isBlockquote = makePredByNodeName('BLOCKQUOTE'); + + var isBodyContainer = function (node) { + return isCell(node) || isBlockquote(node) || isEditable(node); + }; + + var isAnchor = makePredByNodeName('A'); + + var isParaInline = function (node) { + return isInline(node) && !!ancestor(node, isPara); + }; + + var isBodyInline = function (node) { + return isInline(node) && !ancestor(node, isPara); + }; + + var isBody = makePredByNodeName('BODY'); + + /** + * returns whether nodeB is closest sibling of nodeA + * + * @param {Node} nodeA + * @param {Node} nodeB + * @return {Boolean} + */ + var isClosestSibling = function (nodeA, nodeB) { + return nodeA.nextSibling === nodeB || + nodeA.previousSibling === nodeB; + }; + + /** + * returns array of closest siblings with node + * + * @param {Node} node + * @param {function} [pred] - predicate function + * @return {Node[]} + */ + var withClosestSiblings = function (node, pred) { + pred = pred || func.ok; + + var siblings = []; + if (node.previousSibling && pred(node.previousSibling)) { + siblings.push(node.previousSibling); + } + siblings.push(node); + if (node.nextSibling && pred(node.nextSibling)) { + siblings.push(node.nextSibling); + } + return siblings; + }; + + /** + * blank HTML for cursor position + * - [workaround] old IE only works with   + * - [workaround] IE11 and other browser works with bogus br + */ + var blankHTML = agent.isMSIE && agent.browserVersion < 11 ? ' ' : '
              '; + + /** + * @method nodeLength + * + * returns #text's text size or element's childNodes size + * + * @param {Node} node + */ + var nodeLength = function (node) { + if (isText(node)) { + return node.nodeValue.length; + } + + if (node) { + return node.childNodes.length; + } + + return 0; + + }; + + /** + * returns whether node is empty or not. + * + * @param {Node} node + * @return {Boolean} + */ + var isEmpty = function (node) { + var len = nodeLength(node); + + if (len === 0) { + return true; + } else if (!isText(node) && len === 1 && node.innerHTML === blankHTML) { + // ex)


              ,
              + return true; + } else if (list.all(node.childNodes, isText) && node.innerHTML === '') { + // ex)

              , + return true; + } + + return false; + }; + + /** + * padding blankHTML if node is empty (for cursor position) + */ + var paddingBlankHTML = function (node) { + if (!isVoid(node) && !nodeLength(node)) { + node.innerHTML = blankHTML; + } + }; + + /** + * find nearest ancestor predicate hit + * + * @param {Node} node + * @param {Function} pred - predicate function + */ + var ancestor = function (node, pred) { + while (node) { + if (pred(node)) { return node; } + if (isEditable(node)) { break; } + + node = node.parentNode; + } + return null; + }; + + /** + * find nearest ancestor only single child blood line and predicate hit + * + * @param {Node} node + * @param {Function} pred - predicate function + */ + var singleChildAncestor = function (node, pred) { + node = node.parentNode; + + while (node) { + if (nodeLength(node) !== 1) { break; } + if (pred(node)) { return node; } + if (isEditable(node)) { break; } + + node = node.parentNode; + } + return null; + }; + + /** + * returns new array of ancestor nodes (until predicate hit). + * + * @param {Node} node + * @param {Function} [optional] pred - predicate function + */ + var listAncestor = function (node, pred) { + pred = pred || func.fail; + + var ancestors = []; + ancestor(node, function (el) { + if (!isEditable(el)) { + ancestors.push(el); + } + + return pred(el); + }); + return ancestors; + }; + + /** + * find farthest ancestor predicate hit + */ + var lastAncestor = function (node, pred) { + var ancestors = listAncestor(node); + return list.last(ancestors.filter(pred)); + }; + + /** + * returns common ancestor node between two nodes. + * + * @param {Node} nodeA + * @param {Node} nodeB + */ + var commonAncestor = function (nodeA, nodeB) { + var ancestors = listAncestor(nodeA); + for (var n = nodeB; n; n = n.parentNode) { + if ($.inArray(n, ancestors) > -1) { return n; } + } + return null; // difference document area + }; + + /** + * listing all previous siblings (until predicate hit). + * + * @param {Node} node + * @param {Function} [optional] pred - predicate function + */ + var listPrev = function (node, pred) { + pred = pred || func.fail; + + var nodes = []; + while (node) { + if (pred(node)) { break; } + nodes.push(node); + node = node.previousSibling; + } + return nodes; + }; + + /** + * listing next siblings (until predicate hit). + * + * @param {Node} node + * @param {Function} [pred] - predicate function + */ + var listNext = function (node, pred) { + pred = pred || func.fail; + + var nodes = []; + while (node) { + if (pred(node)) { break; } + nodes.push(node); + node = node.nextSibling; + } + return nodes; + }; + + /** + * listing descendant nodes + * + * @param {Node} node + * @param {Function} [pred] - predicate function + */ + var listDescendant = function (node, pred) { + var descendants = []; + pred = pred || func.ok; + + // start DFS(depth first search) with node + (function fnWalk(current) { + if (node !== current && pred(current)) { + descendants.push(current); + } + for (var idx = 0, len = current.childNodes.length; idx < len; idx++) { + fnWalk(current.childNodes[idx]); + } + })(node); + + return descendants; + }; + + /** + * wrap node with new tag. + * + * @param {Node} node + * @param {Node} tagName of wrapper + * @return {Node} - wrapper + */ + var wrap = function (node, wrapperName) { + var parent = node.parentNode; + var wrapper = $('<' + wrapperName + '>')[0]; + + parent.insertBefore(wrapper, node); + wrapper.appendChild(node); + + return wrapper; + }; + + /** + * insert node after preceding + * + * @param {Node} node + * @param {Node} preceding - predicate function + */ + var insertAfter = function (node, preceding) { + var next = preceding.nextSibling, parent = preceding.parentNode; + if (next) { + parent.insertBefore(node, next); + } else { + parent.appendChild(node); + } + return node; + }; + + /** + * append elements. + * + * @param {Node} node + * @param {Collection} aChild + */ + var appendChildNodes = function (node, aChild) { + $.each(aChild, function (idx, child) { + node.appendChild(child); + }); + return node; + }; + + /** + * returns whether boundaryPoint is left edge or not. + * + * @param {BoundaryPoint} point + * @return {Boolean} + */ + var isLeftEdgePoint = function (point) { + return point.offset === 0; + }; + + /** + * returns whether boundaryPoint is right edge or not. + * + * @param {BoundaryPoint} point + * @return {Boolean} + */ + var isRightEdgePoint = function (point) { + return point.offset === nodeLength(point.node); + }; + + /** + * returns whether boundaryPoint is edge or not. + * + * @param {BoundaryPoint} point + * @return {Boolean} + */ + var isEdgePoint = function (point) { + return isLeftEdgePoint(point) || isRightEdgePoint(point); + }; + + /** + * returns whether node is left edge of ancestor or not. + * + * @param {Node} node + * @param {Node} ancestor + * @return {Boolean} + */ + var isLeftEdgeOf = function (node, ancestor) { + while (node && node !== ancestor) { + if (position(node) !== 0) { + return false; + } + node = node.parentNode; + } + + return true; + }; + + /** + * returns whether node is right edge of ancestor or not. + * + * @param {Node} node + * @param {Node} ancestor + * @return {Boolean} + */ + var isRightEdgeOf = function (node, ancestor) { + if (!ancestor) { + return false; + } + while (node && node !== ancestor) { + if (position(node) !== nodeLength(node.parentNode) - 1) { + return false; + } + node = node.parentNode; + } + + return true; + }; + + /** + * returns whether point is left edge of ancestor or not. + * @param {BoundaryPoint} point + * @param {Node} ancestor + * @return {Boolean} + */ + var isLeftEdgePointOf = function (point, ancestor) { + return isLeftEdgePoint(point) && isLeftEdgeOf(point.node, ancestor); + }; + + /** + * returns whether point is right edge of ancestor or not. + * @param {BoundaryPoint} point + * @param {Node} ancestor + * @return {Boolean} + */ + var isRightEdgePointOf = function (point, ancestor) { + return isRightEdgePoint(point) && isRightEdgeOf(point.node, ancestor); + }; + + /** + * returns offset from parent. + * + * @param {Node} node + */ + var position = function (node) { + var offset = 0; + while ((node = node.previousSibling)) { + offset += 1; + } + return offset; + }; + + var hasChildren = function (node) { + return !!(node && node.childNodes && node.childNodes.length); + }; + + /** + * returns previous boundaryPoint + * + * @param {BoundaryPoint} point + * @param {Boolean} isSkipInnerOffset + * @return {BoundaryPoint} + */ + var prevPoint = function (point, isSkipInnerOffset) { + var node, offset; + + if (point.offset === 0) { + if (isEditable(point.node)) { + return null; + } + + node = point.node.parentNode; + offset = position(point.node); + } else if (hasChildren(point.node)) { + node = point.node.childNodes[point.offset - 1]; + offset = nodeLength(node); + } else { + node = point.node; + offset = isSkipInnerOffset ? 0 : point.offset - 1; + } + + return { + node: node, + offset: offset + }; + }; + + /** + * returns next boundaryPoint + * + * @param {BoundaryPoint} point + * @param {Boolean} isSkipInnerOffset + * @return {BoundaryPoint} + */ + var nextPoint = function (point, isSkipInnerOffset) { + var node, offset; + + if (nodeLength(point.node) === point.offset) { + if (isEditable(point.node)) { + return null; + } + + node = point.node.parentNode; + offset = position(point.node) + 1; + } else if (hasChildren(point.node)) { + node = point.node.childNodes[point.offset]; + offset = 0; + } else { + node = point.node; + offset = isSkipInnerOffset ? nodeLength(point.node) : point.offset + 1; + } + + return { + node: node, + offset: offset + }; + }; + + /** + * returns whether pointA and pointB is same or not. + * + * @param {BoundaryPoint} pointA + * @param {BoundaryPoint} pointB + * @return {Boolean} + */ + var isSamePoint = function (pointA, pointB) { + return pointA.node === pointB.node && pointA.offset === pointB.offset; + }; + + /** + * returns whether point is visible (can set cursor) or not. + * + * @param {BoundaryPoint} point + * @return {Boolean} + */ + var isVisiblePoint = function (point) { + if (isText(point.node) || !hasChildren(point.node) || isEmpty(point.node)) { + return true; + } + + var leftNode = point.node.childNodes[point.offset - 1]; + var rightNode = point.node.childNodes[point.offset]; + if ((!leftNode || isVoid(leftNode)) && (!rightNode || isVoid(rightNode))) { + return true; + } + + return false; + }; + + /** + * @method prevPointUtil + * + * @param {BoundaryPoint} point + * @param {Function} pred + * @return {BoundaryPoint} + */ + var prevPointUntil = function (point, pred) { + while (point) { + if (pred(point)) { + return point; + } + + point = prevPoint(point); + } + + return null; + }; + + /** + * @method nextPointUntil + * + * @param {BoundaryPoint} point + * @param {Function} pred + * @return {BoundaryPoint} + */ + var nextPointUntil = function (point, pred) { + while (point) { + if (pred(point)) { + return point; + } + + point = nextPoint(point); + } + + return null; + }; + + /** + * returns whether point has character or not. + * + * @param {Point} point + * @return {Boolean} + */ + var isCharPoint = function (point) { + if (!isText(point.node)) { + return false; + } + + var ch = point.node.nodeValue.charAt(point.offset - 1); + return ch && (ch !== ' ' && ch !== NBSP_CHAR); + }; + + /** + * @method walkPoint + * + * @param {BoundaryPoint} startPoint + * @param {BoundaryPoint} endPoint + * @param {Function} handler + * @param {Boolean} isSkipInnerOffset + */ + var walkPoint = function (startPoint, endPoint, handler, isSkipInnerOffset) { + var point = startPoint; + + while (point) { + handler(point); + + if (isSamePoint(point, endPoint)) { + break; + } + + var isSkipOffset = isSkipInnerOffset && + startPoint.node !== point.node && + endPoint.node !== point.node; + point = nextPoint(point, isSkipOffset); + } + }; + + /** + * @method makeOffsetPath + * + * return offsetPath(array of offset) from ancestor + * + * @param {Node} ancestor - ancestor node + * @param {Node} node + */ + var makeOffsetPath = function (ancestor, node) { + var ancestors = listAncestor(node, func.eq(ancestor)); + return ancestors.map(position).reverse(); + }; + + /** + * @method fromOffsetPath + * + * return element from offsetPath(array of offset) + * + * @param {Node} ancestor - ancestor node + * @param {array} offsets - offsetPath + */ + var fromOffsetPath = function (ancestor, offsets) { + var current = ancestor; + for (var i = 0, len = offsets.length; i < len; i++) { + if (current.childNodes.length <= offsets[i]) { + current = current.childNodes[current.childNodes.length - 1]; + } else { + current = current.childNodes[offsets[i]]; + } + } + return current; + }; + + /** + * @method splitNode + * + * split element or #text + * + * @param {BoundaryPoint} point + * @param {Object} [options] + * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false + * @param {Boolean} [options.isNotSplitEdgePoint] - default: false + * @return {Node} right node of boundaryPoint + */ + var splitNode = function (point, options) { + var isSkipPaddingBlankHTML = options && options.isSkipPaddingBlankHTML; + var isNotSplitEdgePoint = options && options.isNotSplitEdgePoint; + + // edge case + if (isEdgePoint(point) && (isText(point.node) || isNotSplitEdgePoint)) { + if (isLeftEdgePoint(point)) { + return point.node; + } else if (isRightEdgePoint(point)) { + return point.node.nextSibling; + } + } + + // split #text + if (isText(point.node)) { + return point.node.splitText(point.offset); + } else { + var childNode = point.node.childNodes[point.offset]; + var clone = insertAfter(point.node.cloneNode(false), point.node); + appendChildNodes(clone, listNext(childNode)); + + if (!isSkipPaddingBlankHTML) { + paddingBlankHTML(point.node); + paddingBlankHTML(clone); + } + + return clone; + } + }; + + /** + * @method splitTree + * + * split tree by point + * + * @param {Node} root - split root + * @param {BoundaryPoint} point + * @param {Object} [options] + * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false + * @param {Boolean} [options.isNotSplitEdgePoint] - default: false + * @return {Node} right node of boundaryPoint + */ + var splitTree = function (root, point, options) { + // ex) [#text, ,

              ] + var ancestors = listAncestor(point.node, func.eq(root)); + + if (!ancestors.length) { + return null; + } else if (ancestors.length === 1) { + return splitNode(point, options); + } + + return ancestors.reduce(function (node, parent) { + if (node === point.node) { + node = splitNode(point, options); + } + + return splitNode({ + node: parent, + offset: node ? dom.position(node) : nodeLength(parent) + }, options); + }); + }; + + /** + * split point + * + * @param {Point} point + * @param {Boolean} isInline + * @return {Object} + */ + var splitPoint = function (point, isInline) { + // find splitRoot, container + // - inline: splitRoot is a child of paragraph + // - block: splitRoot is a child of bodyContainer + var pred = isInline ? isPara : isBodyContainer; + var ancestors = listAncestor(point.node, pred); + var topAncestor = list.last(ancestors) || point.node; + + var splitRoot, container; + if (pred(topAncestor)) { + splitRoot = ancestors[ancestors.length - 2]; + container = topAncestor; + } else { + splitRoot = topAncestor; + container = splitRoot.parentNode; + } + + // if splitRoot is exists, split with splitTree + var pivot = splitRoot && splitTree(splitRoot, point, { + isSkipPaddingBlankHTML: isInline, + isNotSplitEdgePoint: isInline + }); + + // if container is point.node, find pivot with point.offset + if (!pivot && container === point.node) { + pivot = point.node.childNodes[point.offset]; + } + + return { + rightNode: pivot, + container: container + }; + }; + + var create = function (nodeName) { + return document.createElement(nodeName); + }; + + var createText = function (text) { + return document.createTextNode(text); + }; + + /** + * @method remove + * + * remove node, (isRemoveChild: remove child or not) + * + * @param {Node} node + * @param {Boolean} isRemoveChild + */ + var remove = function (node, isRemoveChild) { + if (!node || !node.parentNode) { return; } + if (node.removeNode) { return node.removeNode(isRemoveChild); } + + var parent = node.parentNode; + if (!isRemoveChild) { + var nodes = []; + var i, len; + for (i = 0, len = node.childNodes.length; i < len; i++) { + nodes.push(node.childNodes[i]); + } + + for (i = 0, len = nodes.length; i < len; i++) { + parent.insertBefore(nodes[i], node); + } + } + + parent.removeChild(node); + }; + + /** + * @method removeWhile + * + * @param {Node} node + * @param {Function} pred + */ + var removeWhile = function (node, pred) { + while (node) { + if (isEditable(node) || !pred(node)) { + break; + } + + var parent = node.parentNode; + remove(node); + node = parent; + } + }; + + /** + * @method replace + * + * replace node with provided nodeName + * + * @param {Node} node + * @param {String} nodeName + * @return {Node} - new node + */ + var replace = function (node, nodeName) { + if (node.nodeName.toUpperCase() === nodeName.toUpperCase()) { + return node; + } + + var newNode = create(nodeName); + + if (node.style.cssText) { + newNode.style.cssText = node.style.cssText; + } + + appendChildNodes(newNode, list.from(node.childNodes)); + insertAfter(newNode, node); + remove(node); + + return newNode; + }; + + var isTextarea = makePredByNodeName('TEXTAREA'); + + /** + * @param {jQuery} $node + * @param {Boolean} [stripLinebreaks] - default: false + */ + var value = function ($node, stripLinebreaks) { + var val = isTextarea($node[0]) ? $node.val() : $node.html(); + if (stripLinebreaks) { + return val.replace(/[\n\r]/g, ''); + } + return val; + }; + + /** + * @method html + * + * get the HTML contents of node + * + * @param {jQuery} $node + * @param {Boolean} [isNewlineOnBlock] + */ + var html = function ($node, isNewlineOnBlock) { + var markup = value($node); + + if (isNewlineOnBlock) { + var regexTag = /<(\/?)(\b(?!!)[^>\s]*)(.*?)(\s*\/?>)/g; + markup = markup.replace(regexTag, function (match, endSlash, name) { + name = name.toUpperCase(); + var isEndOfInlineContainer = /^DIV|^TD|^TH|^P|^LI|^H[1-7]/.test(name) && + !!endSlash; + var isBlockNode = /^BLOCKQUOTE|^TABLE|^TBODY|^TR|^HR|^UL|^OL/.test(name); + + return match + ((isEndOfInlineContainer || isBlockNode) ? '\n' : ''); + }); + markup = $.trim(markup); + } + + return markup; + }; + + var posFromPlaceholder = function (placeholder) { + var $placeholder = $(placeholder); + var pos = $placeholder.offset(); + var height = $placeholder.outerHeight(true); // include margin + + return { + left: pos.left, + top: pos.top + height + }; + }; + + var attachEvents = function ($node, events) { + Object.keys(events).forEach(function (key) { + $node.on(key, events[key]); + }); + }; + + var detachEvents = function ($node, events) { + Object.keys(events).forEach(function (key) { + $node.off(key, events[key]); + }); + }; + + return { + /** @property {String} NBSP_CHAR */ + NBSP_CHAR: NBSP_CHAR, + /** @property {String} ZERO_WIDTH_NBSP_CHAR */ + ZERO_WIDTH_NBSP_CHAR: ZERO_WIDTH_NBSP_CHAR, + /** @property {String} blank */ + blank: blankHTML, + /** @property {String} emptyPara */ + emptyPara: '

              ' + blankHTML + '

              ', + makePredByNodeName: makePredByNodeName, + isEditable: isEditable, + isControlSizing: isControlSizing, + isText: isText, + isElement: isElement, + isVoid: isVoid, + isPara: isPara, + isPurePara: isPurePara, + isHeading: isHeading, + isInline: isInline, + isBlock: func.not(isInline), + isBodyInline: isBodyInline, + isBody: isBody, + isParaInline: isParaInline, + isPre: isPre, + isList: isList, + isTable: isTable, + isData: isData, + isCell: isCell, + isBlockquote: isBlockquote, + isBodyContainer: isBodyContainer, + isAnchor: isAnchor, + isDiv: makePredByNodeName('DIV'), + isLi: isLi, + isBR: makePredByNodeName('BR'), + isSpan: makePredByNodeName('SPAN'), + isB: makePredByNodeName('B'), + isU: makePredByNodeName('U'), + isS: makePredByNodeName('S'), + isI: makePredByNodeName('I'), + isImg: makePredByNodeName('IMG'), + isTextarea: isTextarea, + isEmpty: isEmpty, + isEmptyAnchor: func.and(isAnchor, isEmpty), + isClosestSibling: isClosestSibling, + withClosestSiblings: withClosestSiblings, + nodeLength: nodeLength, + isLeftEdgePoint: isLeftEdgePoint, + isRightEdgePoint: isRightEdgePoint, + isEdgePoint: isEdgePoint, + isLeftEdgeOf: isLeftEdgeOf, + isRightEdgeOf: isRightEdgeOf, + isLeftEdgePointOf: isLeftEdgePointOf, + isRightEdgePointOf: isRightEdgePointOf, + prevPoint: prevPoint, + nextPoint: nextPoint, + isSamePoint: isSamePoint, + isVisiblePoint: isVisiblePoint, + prevPointUntil: prevPointUntil, + nextPointUntil: nextPointUntil, + isCharPoint: isCharPoint, + walkPoint: walkPoint, + ancestor: ancestor, + singleChildAncestor: singleChildAncestor, + listAncestor: listAncestor, + lastAncestor: lastAncestor, + listNext: listNext, + listPrev: listPrev, + listDescendant: listDescendant, + commonAncestor: commonAncestor, + wrap: wrap, + insertAfter: insertAfter, + appendChildNodes: appendChildNodes, + position: position, + hasChildren: hasChildren, + makeOffsetPath: makeOffsetPath, + fromOffsetPath: fromOffsetPath, + splitTree: splitTree, + splitPoint: splitPoint, + create: create, + createText: createText, + remove: remove, + removeWhile: removeWhile, + replace: replace, + html: html, + value: value, + posFromPlaceholder: posFromPlaceholder, + attachEvents: attachEvents, + detachEvents: detachEvents + }; + })(); + + /** + * @param {jQuery} $note + * @param {Object} options + * @return {Context} + */ + var Context = function ($note, options) { + var self = this; + + var ui = $.summernote.ui; + this.memos = {}; + this.modules = {}; + this.layoutInfo = {}; + this.options = options; + + /** + * create layout and initialize modules and other resources + */ + this.initialize = function () { + this.layoutInfo = ui.createLayout($note, options); + this._initialize(); + $note.hide(); + return this; + }; + + /** + * destroy modules and other resources and remove layout + */ + this.destroy = function () { + this._destroy(); + $note.removeData('summernote'); + ui.removeLayout($note, this.layoutInfo); + }; + + /** + * destory modules and other resources and initialize it again + */ + this.reset = function () { + var disabled = self.isDisabled(); + this.code(dom.emptyPara); + this._destroy(); + this._initialize(); + + if (disabled) { + self.disable(); + } + }; + + this._initialize = function () { + // add optional buttons + var buttons = $.extend({}, this.options.buttons); + Object.keys(buttons).forEach(function (key) { + self.memo('button.' + key, buttons[key]); + }); + + var modules = $.extend({}, this.options.modules, $.summernote.plugins || {}); + + // add and initialize modules + Object.keys(modules).forEach(function (key) { + self.module(key, modules[key], true); + }); + + Object.keys(this.modules).forEach(function (key) { + self.initializeModule(key); + }); + }; + + this._destroy = function () { + // destroy modules with reversed order + Object.keys(this.modules).reverse().forEach(function (key) { + self.removeModule(key); + }); + + Object.keys(this.memos).forEach(function (key) { + self.removeMemo(key); + }); + }; + + this.code = function (html) { + var isActivated = this.invoke('codeview.isActivated'); + + if (html === undefined) { + this.invoke('codeview.sync'); + return isActivated ? this.layoutInfo.codable.val() : this.layoutInfo.editable.html(); + } else { + if (isActivated) { + this.layoutInfo.codable.val(html); + } else { + this.layoutInfo.editable.html(html); + } + $note.val(html); + this.triggerEvent('change', html); + } + }; + + this.isDisabled = function () { + return this.layoutInfo.editable.attr('contenteditable') === 'false'; + }; + + this.enable = function () { + this.layoutInfo.editable.attr('contenteditable', true); + this.invoke('toolbar.activate', true); + }; + + this.disable = function () { + // close codeview if codeview is opend + if (this.invoke('codeview.isActivated')) { + this.invoke('codeview.deactivate'); + } + this.layoutInfo.editable.attr('contenteditable', false); + this.invoke('toolbar.deactivate', true); + }; + + this.triggerEvent = function () { + var namespace = list.head(arguments); + var args = list.tail(list.from(arguments)); + + var callback = this.options.callbacks[func.namespaceToCamel(namespace, 'on')]; + if (callback) { + callback.apply($note[0], args); + } + $note.trigger('summernote.' + namespace, args); + }; + + this.initializeModule = function (key) { + var module = this.modules[key]; + module.shouldInitialize = module.shouldInitialize || func.ok; + if (!module.shouldInitialize()) { + return; + } + + // initialize module + if (module.initialize) { + module.initialize(); + } + + // attach events + if (module.events) { + dom.attachEvents($note, module.events); + } + }; + + this.module = function (key, ModuleClass, withoutIntialize) { + if (arguments.length === 1) { + return this.modules[key]; + } + + this.modules[key] = new ModuleClass(this); + + if (!withoutIntialize) { + this.initializeModule(key); + } + }; + + this.removeModule = function (key) { + var module = this.modules[key]; + if (module.shouldInitialize()) { + if (module.events) { + dom.detachEvents($note, module.events); + } + + if (module.destroy) { + module.destroy(); + } + } + + delete this.modules[key]; + }; + + this.memo = function (key, obj) { + if (arguments.length === 1) { + return this.memos[key]; + } + this.memos[key] = obj; + }; + + this.removeMemo = function (key) { + if (this.memos[key] && this.memos[key].destroy) { + this.memos[key].destroy(); + } + + delete this.memos[key]; + }; + + this.createInvokeHandler = function (namespace, value) { + return function (event) { + event.preventDefault(); + self.invoke(namespace, value || $(event.target).closest('[data-value]').data('value')); + }; + }; + + this.invoke = function () { + var namespace = list.head(arguments); + var args = list.tail(list.from(arguments)); + + var splits = namespace.split('.'); + var hasSeparator = splits.length > 1; + var moduleName = hasSeparator && list.head(splits); + var methodName = hasSeparator ? list.last(splits) : list.head(splits); + + var module = this.modules[moduleName || 'editor']; + if (!moduleName && this[methodName]) { + return this[methodName].apply(this, args); + } else if (module && module[methodName] && module.shouldInitialize()) { + return module[methodName].apply(module, args); + } + }; + + return this.initialize(); + }; + + $.fn.extend({ + /** + * Summernote API + * + * @param {Object|String} + * @return {this} + */ + summernote: function () { + var type = $.type(list.head(arguments)); + var isExternalAPICalled = type === 'string'; + var hasInitOptions = type === 'object'; + + var options = hasInitOptions ? list.head(arguments) : {}; + + options = $.extend({}, $.summernote.options, options); + options.langInfo = $.extend(true, {}, $.summernote.lang['en-US'], $.summernote.lang[options.lang]); + options.icons = $.extend(true, {}, $.summernote.options.icons, options.icons); + + this.each(function (idx, note) { + var $note = $(note); + if (!$note.data('summernote')) { + var context = new Context($note, options); + $note.data('summernote', context); + $note.data('summernote').triggerEvent('init', context.layoutInfo); + } + }); + + var $note = this.first(); + if ($note.length) { + var context = $note.data('summernote'); + if (isExternalAPICalled) { + return context.invoke.apply(context, list.from(arguments)); + } else if (options.focus) { + context.invoke('editor.focus'); + } + } + + return this; + } + }); + + + var Renderer = function (markup, children, options, callback) { + this.render = function ($parent) { + var $node = $(markup); + + if (options && options.contents) { + $node.html(options.contents); + } + + if (options && options.className) { + $node.addClass(options.className); + } + + if (options && options.data) { + $.each(options.data, function (k, v) { + $node.attr('data-' + k, v); + }); + } + + if (options && options.click) { + $node.on('click', options.click); + } + + if (children) { + var $container = $node.find('.note-children-container'); + children.forEach(function (child) { + child.render($container.length ? $container : $node); + }); + } + + if (callback) { + callback($node, options); + } + + if (options && options.callback) { + options.callback($node); + } + + if ($parent) { + $parent.append($node); + } + + return $node; + }; + }; + + var renderer = { + create: function (markup, callback) { + return function () { + var children = $.isArray(arguments[0]) ? arguments[0] : []; + var options = typeof arguments[1] === 'object' ? arguments[1] : arguments[0]; + if (options && options.children) { + children = options.children; + } + return new Renderer(markup, children, options, callback); + }; + } + }; + + var editor = renderer.create('
              '); + var toolbar = renderer.create('
              '); + var editingArea = renderer.create('
              '); + var codable = renderer.create('
              ' + + '
              ' + + '
              ' + // Set to the height of the text, causes scrolling + '
              ' + // Moved around its parent to cover visible view + '
              ' + + // Provides positioning relative to (visible) text origin + '
              ' + + '
              ' + + '
               
              ' + // Absolutely positioned blinky cursor + '
              ' + // This DIV contains the actual code + '
              '; + if (place.appendChild) place.appendChild(wrapper); else place(wrapper); + // I've never seen more elegant code in my life. + var inputDiv = wrapper.firstChild, input = inputDiv.firstChild, + scroller = wrapper.lastChild, code = scroller.firstChild, + mover = code.firstChild, gutter = mover.firstChild, gutterText = gutter.firstChild, + lineSpace = gutter.nextSibling.firstChild, measure = lineSpace.firstChild, + cursor = measure.nextSibling, lineDiv = cursor.nextSibling; + themeChanged(); + // Needed to hide big blue blinking cursor on Mobile Safari + if (/AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent)) input.style.width = "0px"; + if (!webkit) lineSpace.draggable = true; + if (options.tabindex != null) input.tabIndex = options.tabindex; + if (!options.gutter && !options.lineNumbers) gutter.style.display = "none"; + + // Check for problem with IE innerHTML not working when we have a + // P (or similar) parent node. + try { stringWidth("x"); } + catch (e) { + if (e.message.match(/runtime/i)) + e = new Error("A CodeMirror inside a P-style element does not work in Internet Explorer. (innerHTML bug)"); + throw e; + } + + // Delayed object wrap timeouts, making sure only one is active. blinker holds an interval. + var poll = new Delayed(), highlight = new Delayed(), blinker; + + // mode holds a mode API object. doc is the tree of Line objects, + // work an array of lines that should be parsed, and history the + // undo history (instance of History constructor). + var mode, doc = new BranchChunk([new LeafChunk([new Line("")])]), work, focused; + loadMode(); + // The selection. These are always maintained to point at valid + // positions. Inverted is used to remember that the user is + // selecting bottom-to-top. + var sel = {from: {line: 0, ch: 0}, to: {line: 0, ch: 0}, inverted: false}; + // Selection-related flags. shiftSelecting obviously tracks + // whether the user is holding shift. + var shiftSelecting, lastClick, lastDoubleClick, draggingText, overwrite = false; + // Variables used by startOperation/endOperation to track what + // happened during the operation. + var updateInput, userSelChange, changes, textChanged, selectionChanged, leaveInputAlone, + gutterDirty, callbacks; + // Current visible range (may be bigger than the view window). + var displayOffset = 0, showingFrom = 0, showingTo = 0, lastSizeC = 0; + // bracketHighlighted is used to remember that a backet has been + // marked. + var bracketHighlighted; + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + var maxLine = "", maxWidth, tabText = computeTabText(); + + // Initialize the content. + operation(function(){setValue(options.value || ""); updateInput = false;})(); + var history = new History(); + + // Register our event handlers. + connect(scroller, "mousedown", operation(onMouseDown)); + connect(scroller, "dblclick", operation(onDoubleClick)); + connect(lineSpace, "dragstart", onDragStart); + connect(lineSpace, "selectstart", e_preventDefault); + // Gecko browsers fire contextmenu *after* opening the menu, at + // which point we can't mess with it anymore. Context menu is + // handled in onMouseDown for Gecko. + if (!gecko) connect(scroller, "contextmenu", onContextMenu); + connect(scroller, "scroll", function() { + updateDisplay([]); + if (options.fixedGutter) gutter.style.left = scroller.scrollLeft + "px"; + if (options.onScroll) options.onScroll(instance); + }); + connect(window, "resize", function() {updateDisplay(true);}); + connect(input, "keyup", operation(onKeyUp)); + connect(input, "input", fastPoll); + connect(input, "keydown", operation(onKeyDown)); + connect(input, "keypress", operation(onKeyPress)); + connect(input, "focus", onFocus); + connect(input, "blur", onBlur); + + connect(scroller, "dragenter", e_stop); + connect(scroller, "dragover", e_stop); + connect(scroller, "drop", operation(onDrop)); + connect(scroller, "paste", function(){focusInput(); fastPoll();}); + connect(input, "paste", fastPoll); + connect(input, "cut", operation(function(){replaceSelection("");})); + + // IE throws unspecified error in certain cases, when + // trying to access activeElement before onload + var hasFocus; try { hasFocus = (targetDocument.activeElement == input); } catch(e) { } + if (hasFocus) setTimeout(onFocus, 20); + else onBlur(); + + function isLine(l) {return l >= 0 && l < doc.size;} + // The instance object that we'll return. Mostly calls out to + // local functions in the CodeMirror function. Some do some extra + // range checking and/or clipping. operation is used to wrap the + // call so that changes it makes are tracked, and the display is + // updated afterwards. + var instance = wrapper.CodeMirror = { + getValue: getValue, + setValue: operation(setValue), + getSelection: getSelection, + replaceSelection: operation(replaceSelection), + focus: function(){focusInput(); onFocus(); fastPoll();}, + setOption: function(option, value) { + var oldVal = options[option]; + options[option] = value; + if (option == "mode" || option == "indentUnit") loadMode(); + else if (option == "readOnly" && value) {onBlur(); input.blur();} + else if (option == "theme") themeChanged(); + else if (option == "lineWrapping" && oldVal != value) operation(wrappingChanged)(); + else if (option == "tabSize") operation(tabsChanged)(); + if (option == "lineNumbers" || option == "gutter" || option == "firstLineNumber" || option == "theme") + operation(gutterChanged)(); + }, + getOption: function(option) {return options[option];}, + undo: operation(undo), + redo: operation(redo), + indentLine: operation(function(n, dir) { + if (isLine(n)) indentLine(n, dir == null ? "smart" : dir ? "add" : "subtract"); + }), + indentSelection: operation(indentSelected), + historySize: function() {return {undo: history.done.length, redo: history.undone.length};}, + clearHistory: function() {history = new History();}, + matchBrackets: operation(function(){matchBrackets(true);}), + getTokenAt: operation(function(pos) { + pos = clipPos(pos); + return getLine(pos.line).getTokenAt(mode, getStateBefore(pos.line), pos.ch); + }), + getStateAfter: function(line) { + line = clipLine(line == null ? doc.size - 1: line); + return getStateBefore(line + 1); + }, + cursorCoords: function(start){ + if (start == null) start = sel.inverted; + return pageCoords(start ? sel.from : sel.to); + }, + charCoords: function(pos){return pageCoords(clipPos(pos));}, + coordsChar: function(coords) { + var off = eltOffset(lineSpace); + return coordsChar(coords.x - off.left, coords.y - off.top); + }, + markText: operation(markText), + setBookmark: setBookmark, + setMarker: operation(addGutterMarker), + clearMarker: operation(removeGutterMarker), + setLineClass: operation(setLineClass), + hideLine: operation(function(h) {return setLineHidden(h, true);}), + showLine: operation(function(h) {return setLineHidden(h, false);}), + onDeleteLine: function(line, f) { + if (typeof line == "number") { + if (!isLine(line)) return null; + line = getLine(line); + } + (line.handlers || (line.handlers = [])).push(f); + return line; + }, + lineInfo: lineInfo, + addWidget: function(pos, node, scroll, vert, horiz) { + pos = localCoords(clipPos(pos)); + var top = pos.yBot, left = pos.x; + node.style.position = "absolute"; + code.appendChild(node); + if (vert == "over") top = pos.y; + else if (vert == "near") { + var vspace = Math.max(scroller.offsetHeight, doc.height * textHeight()), + hspace = Math.max(code.clientWidth, lineSpace.clientWidth) - paddingLeft(); + if (pos.yBot + node.offsetHeight > vspace && pos.y > node.offsetHeight) + top = pos.y - node.offsetHeight; + if (left + node.offsetWidth > hspace) + left = hspace - node.offsetWidth; + } + node.style.top = (top + paddingTop()) + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = code.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") left = 0; + else if (horiz == "middle") left = (code.clientWidth - node.offsetWidth) / 2; + node.style.left = (left + paddingLeft()) + "px"; + } + if (scroll) + scrollIntoView(left, top, left + node.offsetWidth, top + node.offsetHeight); + }, + + lineCount: function() {return doc.size;}, + clipPos: clipPos, + getCursor: function(start) { + if (start == null) start = sel.inverted; + return copyPos(start ? sel.from : sel.to); + }, + somethingSelected: function() {return !posEq(sel.from, sel.to);}, + setCursor: operation(function(line, ch, user) { + if (ch == null && typeof line.line == "number") setCursor(line.line, line.ch, user); + else setCursor(line, ch, user); + }), + setSelection: operation(function(from, to, user) { + (user ? setSelectionUser : setSelection)(clipPos(from), clipPos(to || from)); + }), + getLine: function(line) {if (isLine(line)) return getLine(line).text;}, + getLineHandle: function(line) {if (isLine(line)) return getLine(line);}, + setLine: operation(function(line, text) { + if (isLine(line)) replaceRange(text, {line: line, ch: 0}, {line: line, ch: getLine(line).text.length}); + }), + removeLine: operation(function(line) { + if (isLine(line)) replaceRange("", {line: line, ch: 0}, clipPos({line: line+1, ch: 0})); + }), + replaceRange: operation(replaceRange), + getRange: function(from, to) {return getRange(clipPos(from), clipPos(to));}, + + execCommand: function(cmd) {return commands[cmd](instance);}, + // Stuff used by commands, probably not much use to outside code. + moveH: operation(moveH), + deleteH: operation(deleteH), + moveV: operation(moveV), + toggleOverwrite: function() {overwrite = !overwrite;}, + + posFromIndex: function(off) { + var lineNo = 0, ch; + doc.iter(0, doc.size, function(line) { + var sz = line.text.length + 1; + if (sz > off) { ch = off; return true; } + off -= sz; + ++lineNo; + }); + return clipPos({line: lineNo, ch: ch}); + }, + indexFromPos: function (coords) { + if (coords.line < 0 || coords.ch < 0) return 0; + var index = coords.ch; + doc.iter(0, coords.line, function (line) { + index += line.text.length + 1; + }); + return index; + }, + + operation: function(f){return operation(f)();}, + refresh: function(){updateDisplay(true);}, + getInputField: function(){return input;}, + getWrapperElement: function(){return wrapper;}, + getScrollerElement: function(){return scroller;}, + getGutterElement: function(){return gutter;} + }; + + function getLine(n) { return getLineAt(doc, n); } + function updateLineHeight(line, height) { + gutterDirty = true; + var diff = height - line.height; + for (var n = line; n; n = n.parent) n.height += diff; + } + + function setValue(code) { + var top = {line: 0, ch: 0}; + updateLines(top, {line: doc.size - 1, ch: getLine(doc.size-1).text.length}, + splitLines(code), top, top); + updateInput = true; + } + function getValue(code) { + var text = []; + doc.iter(0, doc.size, function(line) { text.push(line.text); }); + return text.join("\n"); + } + + function onMouseDown(e) { + setShift(e.shiftKey); + // Check whether this is a click in a widget + for (var n = e_target(e); n != wrapper; n = n.parentNode) + if (n.parentNode == code && n != mover) return; + + // See if this is a click in the gutter + for (var n = e_target(e); n != wrapper; n = n.parentNode) + if (n.parentNode == gutterText) { + if (options.onGutterClick) + options.onGutterClick(instance, indexOf(gutterText.childNodes, n) + showingFrom, e); + return e_preventDefault(e); + } + + var start = posFromMouse(e); + + switch (e_button(e)) { + case 3: + if (gecko && !mac) onContextMenu(e); + return; + case 2: + if (start) setCursor(start.line, start.ch, true); + return; + } + // For button 1, if it was clicked inside the editor + // (posFromMouse returning non-null), we have to adjust the + // selection. + if (!start) {if (e_target(e) == scroller) e_preventDefault(e); return;} + + if (!focused) onFocus(); + + var now = +new Date; + if (lastDoubleClick && lastDoubleClick.time > now - 400 && posEq(lastDoubleClick.pos, start)) { + e_preventDefault(e); + setTimeout(focusInput, 20); + return selectLine(start.line); + } else if (lastClick && lastClick.time > now - 400 && posEq(lastClick.pos, start)) { + lastDoubleClick = {time: now, pos: start}; + e_preventDefault(e); + return selectWordAt(start); + } else { lastClick = {time: now, pos: start}; } + + var last = start, going; + if (dragAndDrop && !posEq(sel.from, sel.to) && + !posLess(start, sel.from) && !posLess(sel.to, start)) { + // Let the drag handler handle this. + if (webkit) lineSpace.draggable = true; + var up = connect(targetDocument, "mouseup", operation(function(e2) { + if (webkit) lineSpace.draggable = false; + draggingText = false; + up(); + if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { + e_preventDefault(e2); + setCursor(start.line, start.ch, true); + focusInput(); + } + }), true); + draggingText = true; + return; + } + e_preventDefault(e); + setCursor(start.line, start.ch, true); + + function extend(e) { + var cur = posFromMouse(e, true); + if (cur && !posEq(cur, last)) { + if (!focused) onFocus(); + last = cur; + setSelectionUser(start, cur); + updateInput = false; + var visible = visibleLines(); + if (cur.line >= visible.to || cur.line < visible.from) + going = setTimeout(operation(function(){extend(e);}), 150); + } + } + + var move = connect(targetDocument, "mousemove", operation(function(e) { + clearTimeout(going); + e_preventDefault(e); + extend(e); + }), true); + var up = connect(targetDocument, "mouseup", operation(function(e) { + clearTimeout(going); + var cur = posFromMouse(e); + if (cur) setSelectionUser(start, cur); + e_preventDefault(e); + focusInput(); + updateInput = true; + move(); up(); + }), true); + } + function onDoubleClick(e) { + for (var n = e_target(e); n != wrapper; n = n.parentNode) + if (n.parentNode == gutterText) return e_preventDefault(e); + var start = posFromMouse(e); + if (!start) return; + lastDoubleClick = {time: +new Date, pos: start}; + e_preventDefault(e); + selectWordAt(start); + } + function onDrop(e) { + e.preventDefault(); + var pos = posFromMouse(e, true), files = e.dataTransfer.files; + if (!pos || options.readOnly) return; + if (files && files.length && window.FileReader && window.File) { + function loadFile(file, i) { + var reader = new FileReader; + reader.onload = function() { + text[i] = reader.result; + if (++read == n) { + pos = clipPos(pos); + operation(function() { + var end = replaceRange(text.join(""), pos, pos); + setSelectionUser(pos, end); + })(); + } + }; + reader.readAsText(file); + } + var n = files.length, text = Array(n), read = 0; + for (var i = 0; i < n; ++i) loadFile(files[i], i); + } + else { + try { + var text = e.dataTransfer.getData("Text"); + if (text) { + var end = replaceRange(text, pos, pos); + var curFrom = sel.from, curTo = sel.to; + setSelectionUser(pos, end); + if (draggingText) replaceRange("", curFrom, curTo); + focusInput(); + } + } + catch(e){} + } + } + function onDragStart(e) { + var txt = getSelection(); + // This will reset escapeElement + htmlEscape(txt); + e.dataTransfer.setDragImage(escapeElement, 0, 0); + e.dataTransfer.setData("Text", txt); + } + function handleKeyBinding(e) { + var name = keyNames[e.keyCode], next = keyMap[options.keyMap].auto, bound, dropShift; + if (name == null || e.altGraphKey) { + if (next) options.keyMap = next; + return null; + } + if (e.altKey) name = "Alt-" + name; + if (e.ctrlKey) name = "Ctrl-" + name; + if (e.metaKey) name = "Cmd-" + name; + if (e.shiftKey && (bound = lookupKey("Shift-" + name, options.extraKeys, options.keyMap))) { + dropShift = true; + } else { + bound = lookupKey(name, options.extraKeys, options.keyMap); + } + if (typeof bound == "string") { + if (commands.propertyIsEnumerable(bound)) bound = commands[bound]; + else bound = null; + } + if (next && (bound || !isModifierKey(e))) options.keyMap = next; + if (!bound) return false; + if (dropShift) { + var prevShift = shiftSelecting; + shiftSelecting = null; + bound(instance); + shiftSelecting = prevShift; + } else bound(instance); + e_preventDefault(e); + return true; + } + var lastStoppedKey = null; + function onKeyDown(e) { + if (!focused) onFocus(); + var code = e.keyCode; + // IE does strange things with escape. + if (ie && code == 27) { e.returnValue = false; } + setShift(code == 16 || e.shiftKey); + // First give onKeyEvent option a chance to handle this. + if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; + var handled = handleKeyBinding(e); + if (window.opera) { + lastStoppedKey = handled ? e.keyCode : null; + // Opera has no cut event... we try to at least catch the key combo + if (!handled && (mac ? e.metaKey : e.ctrlKey) && e.keyCode == 88) + replaceSelection(""); + } + } + function onKeyPress(e) { + if (window.opera && e.keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} + if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; + if (window.opera && !e.which && handleKeyBinding(e)) return; + if (options.electricChars && mode.electricChars) { + var ch = String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode); + if (mode.electricChars.indexOf(ch) > -1) + setTimeout(operation(function() {indentLine(sel.to.line, "smart");}), 75); + } + fastPoll(); + } + function onKeyUp(e) { + if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; + if (e.keyCode == 16) shiftSelecting = null; + } + + function onFocus() { + if (options.readOnly) return; + if (!focused) { + if (options.onFocus) options.onFocus(instance); + focused = true; + if (wrapper.className.search(/\bCodeMirror-focused\b/) == -1) + wrapper.className += " CodeMirror-focused"; + if (!leaveInputAlone) resetInput(true); + } + slowPoll(); + restartBlink(); + } + function onBlur() { + if (focused) { + if (options.onBlur) options.onBlur(instance); + focused = false; + wrapper.className = wrapper.className.replace(" CodeMirror-focused", ""); + } + clearInterval(blinker); + setTimeout(function() {if (!focused) shiftSelecting = null;}, 150); + } + + // Replace the range from from to to by the strings in newText. + // Afterwards, set the selection to selFrom, selTo. + function updateLines(from, to, newText, selFrom, selTo) { + if (history) { + var old = []; + doc.iter(from.line, to.line + 1, function(line) { old.push(line.text); }); + history.addChange(from.line, newText.length, old); + while (history.done.length > options.undoDepth) history.done.shift(); + } + updateLinesNoUndo(from, to, newText, selFrom, selTo); + } + function unredoHelper(from, to) { + var change = from.pop(); + if (change) { + var replaced = [], end = change.start + change.added; + doc.iter(change.start, end, function(line) { replaced.push(line.text); }); + to.push({start: change.start, added: change.old.length, old: replaced}); + var pos = clipPos({line: change.start + change.old.length - 1, + ch: editEnd(replaced[replaced.length-1], change.old[change.old.length-1])}); + updateLinesNoUndo({line: change.start, ch: 0}, {line: end - 1, ch: getLine(end-1).text.length}, change.old, pos, pos); + updateInput = true; + } + } + function undo() {unredoHelper(history.done, history.undone);} + function redo() {unredoHelper(history.undone, history.done);} + + function updateLinesNoUndo(from, to, newText, selFrom, selTo) { + var recomputeMaxLength = false, maxLineLength = maxLine.length; + if (!options.lineWrapping) + doc.iter(from.line, to.line, function(line) { + if (line.text.length == maxLineLength) {recomputeMaxLength = true; return true;} + }); + if (from.line != to.line || newText.length > 1) gutterDirty = true; + + var nlines = to.line - from.line, firstLine = getLine(from.line), lastLine = getLine(to.line); + // First adjust the line structure, taking some care to leave highlighting intact. + if (from.ch == 0 && to.ch == 0 && newText[newText.length - 1] == "") { + // This is a whole-line replace. Treated specially to make + // sure line objects move the way they are supposed to. + var added = [], prevLine = null; + if (from.line) { + prevLine = getLine(from.line - 1); + prevLine.fixMarkEnds(lastLine); + } else lastLine.fixMarkStarts(); + for (var i = 0, e = newText.length - 1; i < e; ++i) + added.push(Line.inheritMarks(newText[i], prevLine)); + if (nlines) doc.remove(from.line, nlines, callbacks); + if (added.length) doc.insert(from.line, added); + } else if (firstLine == lastLine) { + if (newText.length == 1) + firstLine.replace(from.ch, to.ch, newText[0]); + else { + lastLine = firstLine.split(to.ch, newText[newText.length-1]); + firstLine.replace(from.ch, null, newText[0]); + firstLine.fixMarkEnds(lastLine); + var added = []; + for (var i = 1, e = newText.length - 1; i < e; ++i) + added.push(Line.inheritMarks(newText[i], firstLine)); + added.push(lastLine); + doc.insert(from.line + 1, added); + } + } else if (newText.length == 1) { + firstLine.replace(from.ch, null, newText[0]); + lastLine.replace(null, to.ch, ""); + firstLine.append(lastLine); + doc.remove(from.line + 1, nlines, callbacks); + } else { + var added = []; + firstLine.replace(from.ch, null, newText[0]); + lastLine.replace(null, to.ch, newText[newText.length-1]); + firstLine.fixMarkEnds(lastLine); + for (var i = 1, e = newText.length - 1; i < e; ++i) + added.push(Line.inheritMarks(newText[i], firstLine)); + if (nlines > 1) doc.remove(from.line + 1, nlines - 1, callbacks); + doc.insert(from.line + 1, added); + } + if (options.lineWrapping) { + var perLine = scroller.clientWidth / charWidth() - 3; + doc.iter(from.line, from.line + newText.length, function(line) { + if (line.hidden) return; + var guess = Math.ceil(line.text.length / perLine) || 1; + if (guess != line.height) updateLineHeight(line, guess); + }); + } else { + doc.iter(from.line, i + newText.length, function(line) { + var l = line.text; + if (l.length > maxLineLength) { + maxLine = l; maxLineLength = l.length; maxWidth = null; + recomputeMaxLength = false; + } + }); + if (recomputeMaxLength) { + maxLineLength = 0; maxLine = ""; maxWidth = null; + doc.iter(0, doc.size, function(line) { + var l = line.text; + if (l.length > maxLineLength) { + maxLineLength = l.length; maxLine = l; + } + }); + } + } + + // Add these lines to the work array, so that they will be + // highlighted. Adjust work lines if lines were added/removed. + var newWork = [], lendiff = newText.length - nlines - 1; + for (var i = 0, l = work.length; i < l; ++i) { + var task = work[i]; + if (task < from.line) newWork.push(task); + else if (task > to.line) newWork.push(task + lendiff); + } + var hlEnd = from.line + Math.min(newText.length, 500); + highlightLines(from.line, hlEnd); + newWork.push(hlEnd); + work = newWork; + startWorker(100); + // Remember that these lines changed, for updating the display + changes.push({from: from.line, to: to.line + 1, diff: lendiff}); + var changeObj = {from: from, to: to, text: newText}; + if (textChanged) { + for (var cur = textChanged; cur.next; cur = cur.next) {} + cur.next = changeObj; + } else textChanged = changeObj; + + // Update the selection + function updateLine(n) {return n <= Math.min(to.line, to.line + lendiff) ? n : n + lendiff;} + setSelection(selFrom, selTo, updateLine(sel.from.line), updateLine(sel.to.line)); + + // Make sure the scroll-size div has the correct height. + code.style.height = (doc.height * textHeight() + 2 * paddingTop()) + "px"; + } + + function replaceRange(code, from, to) { + from = clipPos(from); + if (!to) to = from; else to = clipPos(to); + code = splitLines(code); + function adjustPos(pos) { + if (posLess(pos, from)) return pos; + if (!posLess(to, pos)) return end; + var line = pos.line + code.length - (to.line - from.line) - 1; + var ch = pos.ch; + if (pos.line == to.line) + ch += code[code.length-1].length - (to.ch - (to.line == from.line ? from.ch : 0)); + return {line: line, ch: ch}; + } + var end; + replaceRange1(code, from, to, function(end1) { + end = end1; + return {from: adjustPos(sel.from), to: adjustPos(sel.to)}; + }); + return end; + } + function replaceSelection(code, collapse) { + replaceRange1(splitLines(code), sel.from, sel.to, function(end) { + if (collapse == "end") return {from: end, to: end}; + else if (collapse == "start") return {from: sel.from, to: sel.from}; + else return {from: sel.from, to: end}; + }); + } + function replaceRange1(code, from, to, computeSel) { + var endch = code.length == 1 ? code[0].length + from.ch : code[code.length-1].length; + var newSel = computeSel({line: from.line + code.length - 1, ch: endch}); + updateLines(from, to, code, newSel.from, newSel.to); + } + + function getRange(from, to) { + var l1 = from.line, l2 = to.line; + if (l1 == l2) return getLine(l1).text.slice(from.ch, to.ch); + var code = [getLine(l1).text.slice(from.ch)]; + doc.iter(l1 + 1, l2, function(line) { code.push(line.text); }); + code.push(getLine(l2).text.slice(0, to.ch)); + return code.join("\n"); + } + function getSelection() { + return getRange(sel.from, sel.to); + } + + var pollingFast = false; // Ensures slowPoll doesn't cancel fastPoll + function slowPoll() { + if (pollingFast) return; + poll.set(options.pollInterval, function() { + startOperation(); + readInput(); + if (focused) slowPoll(); + endOperation(); + }); + } + function fastPoll() { + var missed = false; + pollingFast = true; + function p() { + startOperation(); + var changed = readInput(); + if (!changed && !missed) {missed = true; poll.set(60, p);} + else {pollingFast = false; slowPoll();} + endOperation(); + } + poll.set(20, p); + } + + // Previnput is a hack to work with IME. If we reset the textarea + // on every change, that breaks IME. So we look for changes + // compared to the previous content instead. (Modern browsers have + // events that indicate IME taking place, but these are not widely + // supported or compatible enough yet to rely on.) + var prevInput = ""; + function readInput() { + if (leaveInputAlone || !focused || hasSelection(input)) return false; + var text = input.value; + if (text == prevInput) return false; + shiftSelecting = null; + var same = 0, l = Math.min(prevInput.length, text.length); + while (same < l && prevInput[same] == text[same]) ++same; + if (same < prevInput.length) + sel.from = {line: sel.from.line, ch: sel.from.ch - (prevInput.length - same)}; + else if (overwrite && posEq(sel.from, sel.to)) + sel.to = {line: sel.to.line, ch: Math.min(getLine(sel.to.line).text.length, sel.to.ch + (text.length - same))}; + replaceSelection(text.slice(same), "end"); + prevInput = text; + return true; + } + function resetInput(user) { + if (!posEq(sel.from, sel.to)) { + prevInput = ""; + input.value = getSelection(); + input.select(); + } else if (user) prevInput = input.value = ""; + } + + function focusInput() { + if (!options.readOnly) input.focus(); + } + + function scrollEditorIntoView() { + if (!cursor.getBoundingClientRect) return; + var rect = cursor.getBoundingClientRect(); + // IE returns bogus coordinates when the instance sits inside of an iframe and the cursor is hidden + if (ie && rect.top == rect.bottom) return; + var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight); + if (rect.top < 0 || rect.bottom > winH) cursor.scrollIntoView(); + } + function scrollCursorIntoView() { + var cursor = localCoords(sel.inverted ? sel.from : sel.to); + var x = options.lineWrapping ? Math.min(cursor.x, lineSpace.offsetWidth) : cursor.x; + return scrollIntoView(x, cursor.y, x, cursor.yBot); + } + function scrollIntoView(x1, y1, x2, y2) { + var pl = paddingLeft(), pt = paddingTop(), lh = textHeight(); + y1 += pt; y2 += pt; x1 += pl; x2 += pl; + var screen = scroller.clientHeight, screentop = scroller.scrollTop, scrolled = false, result = true; + if (y1 < screentop) {scroller.scrollTop = Math.max(0, y1 - 2*lh); scrolled = true;} + else if (y2 > screentop + screen) {scroller.scrollTop = y2 + lh - screen; scrolled = true;} + + var screenw = scroller.clientWidth, screenleft = scroller.scrollLeft; + var gutterw = options.fixedGutter ? gutter.clientWidth : 0; + if (x1 < screenleft + gutterw) { + if (x1 < 50) x1 = 0; + scroller.scrollLeft = Math.max(0, x1 - 10 - gutterw); + scrolled = true; + } + else if (x2 > screenw + screenleft - 3) { + scroller.scrollLeft = x2 + 10 - screenw; + scrolled = true; + if (x2 > code.clientWidth) result = false; + } + if (scrolled && options.onScroll) options.onScroll(instance); + return result; + } + + function visibleLines() { + var lh = textHeight(), top = scroller.scrollTop - paddingTop(); + var from_height = Math.max(0, Math.floor(top / lh)); + var to_height = Math.ceil((top + scroller.clientHeight) / lh); + return {from: lineAtHeight(doc, from_height), + to: lineAtHeight(doc, to_height)}; + } + // Uses a set of changes plus the current scroll position to + // determine which DOM updates have to be made, and makes the + // updates. + function updateDisplay(changes, suppressCallback) { + if (!scroller.clientWidth) { + showingFrom = showingTo = displayOffset = 0; + return; + } + // Compute the new visible window + var visible = visibleLines(); + // Bail out if the visible area is already rendered and nothing changed. + if (changes !== true && changes.length == 0 && visible.from >= showingFrom && visible.to <= showingTo) return; + var from = Math.max(visible.from - 100, 0), to = Math.min(doc.size, visible.to + 100); + if (showingFrom < from && from - showingFrom < 20) from = showingFrom; + if (showingTo > to && showingTo - to < 20) to = Math.min(doc.size, showingTo); + + // Create a range of theoretically intact lines, and punch holes + // in that using the change info. + var intact = changes === true ? [] : + computeIntact([{from: showingFrom, to: showingTo, domStart: 0}], changes); + // Clip off the parts that won't be visible + var intactLines = 0; + for (var i = 0; i < intact.length; ++i) { + var range = intact[i]; + if (range.from < from) {range.domStart += (from - range.from); range.from = from;} + if (range.to > to) range.to = to; + if (range.from >= range.to) intact.splice(i--, 1); + else intactLines += range.to - range.from; + } + if (intactLines == to - from) return; + intact.sort(function(a, b) {return a.domStart - b.domStart;}); + + var th = textHeight(), gutterDisplay = gutter.style.display; + lineDiv.style.display = gutter.style.display = "none"; + patchDisplay(from, to, intact); + lineDiv.style.display = ""; + + // Position the mover div to align with the lines it's supposed + // to be showing (which will cover the visible display) + var different = from != showingFrom || to != showingTo || lastSizeC != scroller.clientHeight + th; + // This is just a bogus formula that detects when the editor is + // resized or the font size changes. + if (different) lastSizeC = scroller.clientHeight + th; + showingFrom = from; showingTo = to; + displayOffset = heightAtLine(doc, from); + mover.style.top = (displayOffset * th) + "px"; + code.style.height = (doc.height * th + 2 * paddingTop()) + "px"; + + // Since this is all rather error prone, it is honoured with the + // only assertion in the whole file. + if (lineDiv.childNodes.length != showingTo - showingFrom) + throw new Error("BAD PATCH! " + JSON.stringify(intact) + " size=" + (showingTo - showingFrom) + + " nodes=" + lineDiv.childNodes.length); + + if (options.lineWrapping) { + maxWidth = scroller.clientWidth; + var curNode = lineDiv.firstChild; + doc.iter(showingFrom, showingTo, function(line) { + if (!line.hidden) { + var height = Math.round(curNode.offsetHeight / th) || 1; + if (line.height != height) {updateLineHeight(line, height); gutterDirty = true;} + } + curNode = curNode.nextSibling; + }); + } else { + if (maxWidth == null) maxWidth = stringWidth(maxLine); + if (maxWidth > scroller.clientWidth) { + lineSpace.style.width = maxWidth + "px"; + // Needed to prevent odd wrapping/hiding of widgets placed in here. + code.style.width = ""; + code.style.width = scroller.scrollWidth + "px"; + } else { + lineSpace.style.width = code.style.width = ""; + } + } + gutter.style.display = gutterDisplay; + if (different || gutterDirty) updateGutter(); + updateCursor(); + if (!suppressCallback && options.onUpdate) options.onUpdate(instance); + return true; + } + + function computeIntact(intact, changes) { + for (var i = 0, l = changes.length || 0; i < l; ++i) { + var change = changes[i], intact2 = [], diff = change.diff || 0; + for (var j = 0, l2 = intact.length; j < l2; ++j) { + var range = intact[j]; + if (change.to <= range.from && change.diff) + intact2.push({from: range.from + diff, to: range.to + diff, + domStart: range.domStart}); + else if (change.to <= range.from || change.from >= range.to) + intact2.push(range); + else { + if (change.from > range.from) + intact2.push({from: range.from, to: change.from, domStart: range.domStart}); + if (change.to < range.to) + intact2.push({from: change.to + diff, to: range.to + diff, + domStart: range.domStart + (change.to - range.from)}); + } + } + intact = intact2; + } + return intact; + } + + function patchDisplay(from, to, intact) { + // The first pass removes the DOM nodes that aren't intact. + if (!intact.length) lineDiv.innerHTML = ""; + else { + function killNode(node) { + var tmp = node.nextSibling; + node.parentNode.removeChild(node); + return tmp; + } + var domPos = 0, curNode = lineDiv.firstChild, n; + for (var i = 0; i < intact.length; ++i) { + var cur = intact[i]; + while (cur.domStart > domPos) {curNode = killNode(curNode); domPos++;} + for (var j = 0, e = cur.to - cur.from; j < e; ++j) {curNode = curNode.nextSibling; domPos++;} + } + while (curNode) curNode = killNode(curNode); + } + // This pass fills in the lines that actually changed. + var nextIntact = intact.shift(), curNode = lineDiv.firstChild, j = from; + var sfrom = sel.from.line, sto = sel.to.line, inSel = sfrom < from && sto >= from; + var scratch = targetDocument.createElement("div"), newElt; + doc.iter(from, to, function(line) { + var ch1 = null, ch2 = null; + if (inSel) { + ch1 = 0; + if (sto == j) {inSel = false; ch2 = sel.to.ch;} + } else if (sfrom == j) { + if (sto == j) {ch1 = sel.from.ch; ch2 = sel.to.ch;} + else {inSel = true; ch1 = sel.from.ch;} + } + if (nextIntact && nextIntact.to == j) nextIntact = intact.shift(); + if (!nextIntact || nextIntact.from > j) { + if (line.hidden) scratch.innerHTML = "
              ";
              +                    else scratch.innerHTML = line.getHTML(ch1, ch2, true, tabText);
              +                    lineDiv.insertBefore(scratch.firstChild, curNode);
              +                } else {
              +                    curNode = curNode.nextSibling;
              +                }
              +                ++j;
              +            });
              +        }
              +
              +        function updateGutter() {
              +            if (!options.gutter && !options.lineNumbers) return;
              +            var hText = mover.offsetHeight, hEditor = scroller.clientHeight;
              +            gutter.style.height = (hText - hEditor < 2 ? hEditor : hText) + "px";
              +            var html = [], i = showingFrom;
              +            doc.iter(showingFrom, Math.max(showingTo, showingFrom + 1), function(line) {
              +                if (line.hidden) {
              +                    html.push("
              ");
              +                } else {
              +                    var marker = line.gutterMarker;
              +                    var text = options.lineNumbers ? i + options.firstLineNumber : null;
              +                    if (marker && marker.text)
              +                        text = marker.text.replace("%N%", text != null ? text : "");
              +                    else if (text == null)
              +                        text = "\u00a0";
              +                    html.push((marker && marker.style ? '
              ' : "
              "), text);
              +                    for (var j = 1; j < line.height; ++j) html.push("
               "); + html.push("
              "); + } + ++i; + }); + gutter.style.display = "none"; + gutterText.innerHTML = html.join(""); + var minwidth = String(doc.size).length, firstNode = gutterText.firstChild, val = eltText(firstNode), pad = ""; + while (val.length + pad.length < minwidth) pad += "\u00a0"; + if (pad) firstNode.insertBefore(targetDocument.createTextNode(pad), firstNode.firstChild); + gutter.style.display = ""; + lineSpace.style.marginLeft = gutter.offsetWidth + "px"; + gutterDirty = false; + } + function updateCursor() { + var head = sel.inverted ? sel.from : sel.to, lh = textHeight(); + var pos = localCoords(head, true); + var wrapOff = eltOffset(wrapper), lineOff = eltOffset(lineDiv); + inputDiv.style.top = (pos.y + lineOff.top - wrapOff.top) + "px"; + inputDiv.style.left = (pos.x + lineOff.left - wrapOff.left) + "px"; + if (posEq(sel.from, sel.to)) { + cursor.style.top = pos.y + "px"; + cursor.style.left = (options.lineWrapping ? Math.min(pos.x, lineSpace.offsetWidth) : pos.x) + "px"; + cursor.style.display = ""; + } + else cursor.style.display = "none"; + } + + function setShift(val) { + if (val) shiftSelecting = shiftSelecting || (sel.inverted ? sel.to : sel.from); + else shiftSelecting = null; + } + function setSelectionUser(from, to) { + var sh = shiftSelecting && clipPos(shiftSelecting); + if (sh) { + if (posLess(sh, from)) from = sh; + else if (posLess(to, sh)) to = sh; + } + setSelection(from, to); + userSelChange = true; + } + // Update the selection. Last two args are only used by + // updateLines, since they have to be expressed in the line + // numbers before the update. + function setSelection(from, to, oldFrom, oldTo) { + goalColumn = null; + if (oldFrom == null) {oldFrom = sel.from.line; oldTo = sel.to.line;} + if (posEq(sel.from, from) && posEq(sel.to, to)) return; + if (posLess(to, from)) {var tmp = to; to = from; from = tmp;} + + // Skip over hidden lines. + if (from.line != oldFrom) from = skipHidden(from, oldFrom, sel.from.ch); + if (to.line != oldTo) to = skipHidden(to, oldTo, sel.to.ch); + + if (posEq(from, to)) sel.inverted = false; + else if (posEq(from, sel.to)) sel.inverted = false; + else if (posEq(to, sel.from)) sel.inverted = true; + + // Some ugly logic used to only mark the lines that actually did + // see a change in selection as changed, rather than the whole + // selected range. + if (posEq(from, to)) { + if (!posEq(sel.from, sel.to)) + changes.push({from: oldFrom, to: oldTo + 1}); + } + else if (posEq(sel.from, sel.to)) { + changes.push({from: from.line, to: to.line + 1}); + } + else { + if (!posEq(from, sel.from)) { + if (from.line < oldFrom) + changes.push({from: from.line, to: Math.min(to.line, oldFrom) + 1}); + else + changes.push({from: oldFrom, to: Math.min(oldTo, from.line) + 1}); + } + if (!posEq(to, sel.to)) { + if (to.line < oldTo) + changes.push({from: Math.max(oldFrom, from.line), to: oldTo + 1}); + else + changes.push({from: Math.max(from.line, oldTo), to: to.line + 1}); + } + } + sel.from = from; sel.to = to; + selectionChanged = true; + } + function skipHidden(pos, oldLine, oldCh) { + function getNonHidden(dir) { + var lNo = pos.line + dir, end = dir == 1 ? doc.size : -1; + while (lNo != end) { + var line = getLine(lNo); + if (!line.hidden) { + var ch = pos.ch; + if (ch > oldCh || ch > line.text.length) ch = line.text.length; + return {line: lNo, ch: ch}; + } + lNo += dir; + } + } + var line = getLine(pos.line); + if (!line.hidden) return pos; + if (pos.line >= oldLine) return getNonHidden(1) || getNonHidden(-1); + else return getNonHidden(-1) || getNonHidden(1); + } + function setCursor(line, ch, user) { + var pos = clipPos({line: line, ch: ch || 0}); + (user ? setSelectionUser : setSelection)(pos, pos); + } + + function clipLine(n) {return Math.max(0, Math.min(n, doc.size-1));} + function clipPos(pos) { + if (pos.line < 0) return {line: 0, ch: 0}; + if (pos.line >= doc.size) return {line: doc.size-1, ch: getLine(doc.size-1).text.length}; + var ch = pos.ch, linelen = getLine(pos.line).text.length; + if (ch == null || ch > linelen) return {line: pos.line, ch: linelen}; + else if (ch < 0) return {line: pos.line, ch: 0}; + else return pos; + } + + function findPosH(dir, unit) { + var end = sel.inverted ? sel.from : sel.to, line = end.line, ch = end.ch; + var lineObj = getLine(line); + function findNextLine() { + for (var l = line + dir, e = dir < 0 ? -1 : doc.size; l != e; l += dir) { + var lo = getLine(l); + if (!lo.hidden) { line = l; lineObj = lo; return true; } + } + } + function moveOnce(boundToLine) { + if (ch == (dir < 0 ? 0 : lineObj.text.length)) { + if (!boundToLine && findNextLine()) ch = dir < 0 ? lineObj.text.length : 0; + else return false; + } else ch += dir; + return true; + } + if (unit == "char") moveOnce(); + else if (unit == "column") moveOnce(true); + else if (unit == "word") { + var sawWord = false; + for (;;) { + if (dir < 0) if (!moveOnce()) break; + if (isWordChar(lineObj.text.charAt(ch))) sawWord = true; + else if (sawWord) {if (dir < 0) {dir = 1; moveOnce();} break;} + if (dir > 0) if (!moveOnce()) break; + } + } + return {line: line, ch: ch}; + } + function moveH(dir, unit) { + var pos = dir < 0 ? sel.from : sel.to; + if (shiftSelecting || posEq(sel.from, sel.to)) pos = findPosH(dir, unit); + setCursor(pos.line, pos.ch, true); + } + function deleteH(dir, unit) { + if (!posEq(sel.from, sel.to)) replaceRange("", sel.from, sel.to); + else if (dir < 0) replaceRange("", findPosH(dir, unit), sel.to); + else replaceRange("", sel.from, findPosH(dir, unit)); + userSelChange = true; + } + var goalColumn = null; + function moveV(dir, unit) { + var dist = 0, pos = localCoords(sel.inverted ? sel.from : sel.to, true); + if (goalColumn != null) pos.x = goalColumn; + if (unit == "page") dist = scroller.clientHeight; + else if (unit == "line") dist = textHeight(); + var target = coordsChar(pos.x, pos.y + dist * dir + 2); + setCursor(target.line, target.ch, true); + goalColumn = pos.x; + } + + function selectWordAt(pos) { + var line = getLine(pos.line).text; + var start = pos.ch, end = pos.ch; + while (start > 0 && isWordChar(line.charAt(start - 1))) --start; + while (end < line.length && isWordChar(line.charAt(end))) ++end; + setSelectionUser({line: pos.line, ch: start}, {line: pos.line, ch: end}); + } + function selectLine(line) { + setSelectionUser({line: line, ch: 0}, {line: line, ch: getLine(line).text.length}); + } + function indentSelected(mode) { + if (posEq(sel.from, sel.to)) return indentLine(sel.from.line, mode); + var e = sel.to.line - (sel.to.ch ? 0 : 1); + for (var i = sel.from.line; i <= e; ++i) indentLine(i, mode); + } + + function indentLine(n, how) { + if (!how) how = "add"; + if (how == "smart") { + if (!mode.indent) how = "prev"; + else var state = getStateBefore(n); + } + + var line = getLine(n), curSpace = line.indentation(options.tabSize), + curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (how == "prev") { + if (n) indentation = getLine(n-1).indentation(options.tabSize); + else indentation = 0; + } + else if (how == "smart") indentation = mode.indent(state, line.text.slice(curSpaceString.length), line.text); + else if (how == "add") indentation = curSpace + options.indentUnit; + else if (how == "subtract") indentation = curSpace - options.indentUnit; + indentation = Math.max(0, indentation); + var diff = indentation - curSpace; + + if (!diff) { + if (sel.from.line != n && sel.to.line != n) return; + var indentString = curSpaceString; + } + else { + var indentString = "", pos = 0; + if (options.indentWithTabs) + for (var i = Math.floor(indentation / options.tabSize); i; --i) {pos += options.tabSize; indentString += "\t";} + while (pos < indentation) {++pos; indentString += " ";} + } + + replaceRange(indentString, {line: n, ch: 0}, {line: n, ch: curSpaceString.length}); + } + + function loadMode() { + mode = CodeMirror.getMode(options, options.mode); + doc.iter(0, doc.size, function(line) { line.stateAfter = null; }); + work = [0]; + startWorker(); + } + function gutterChanged() { + var visible = options.gutter || options.lineNumbers; + gutter.style.display = visible ? "" : "none"; + if (visible) gutterDirty = true; + else lineDiv.parentNode.style.marginLeft = 0; + } + function wrappingChanged(from, to) { + if (options.lineWrapping) { + wrapper.className += " CodeMirror-wrap"; + var perLine = scroller.clientWidth / charWidth() - 3; + doc.iter(0, doc.size, function(line) { + if (line.hidden) return; + var guess = Math.ceil(line.text.length / perLine) || 1; + if (guess != 1) updateLineHeight(line, guess); + }); + lineSpace.style.width = code.style.width = ""; + } else { + wrapper.className = wrapper.className.replace(" CodeMirror-wrap", ""); + maxWidth = null; maxLine = ""; + doc.iter(0, doc.size, function(line) { + if (line.height != 1 && !line.hidden) updateLineHeight(line, 1); + if (line.text.length > maxLine.length) maxLine = line.text; + }); + } + changes.push({from: 0, to: doc.size}); + } + function computeTabText() { + for (var str = '', i = 0; i < options.tabSize; ++i) str += " "; + return str + ""; + } + function tabsChanged() { + tabText = computeTabText(); + updateDisplay(true); + } + function themeChanged() { + scroller.className = scroller.className.replace(/\s*cm-s-\w+/g, "") + + options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + } + + function TextMarker() { this.set = []; } + TextMarker.prototype.clear = operation(function() { + var min = Infinity, max = -Infinity; + for (var i = 0, e = this.set.length; i < e; ++i) { + var line = this.set[i], mk = line.marked; + if (!mk || !line.parent) continue; + var lineN = lineNo(line); + min = Math.min(min, lineN); max = Math.max(max, lineN); + for (var j = 0; j < mk.length; ++j) + if (mk[j].set == this.set) mk.splice(j--, 1); + } + if (min != Infinity) + changes.push({from: min, to: max + 1}); + }); + TextMarker.prototype.find = function() { + var from, to; + for (var i = 0, e = this.set.length; i < e; ++i) { + var line = this.set[i], mk = line.marked; + for (var j = 0; j < mk.length; ++j) { + var mark = mk[j]; + if (mark.set == this.set) { + if (mark.from != null || mark.to != null) { + var found = lineNo(line); + if (found != null) { + if (mark.from != null) from = {line: found, ch: mark.from}; + if (mark.to != null) to = {line: found, ch: mark.to}; + } + } + } + } + } + return {from: from, to: to}; + }; + + function markText(from, to, className) { + from = clipPos(from); to = clipPos(to); + var tm = new TextMarker(); + function add(line, from, to, className) { + getLine(line).addMark(new MarkedText(from, to, className, tm.set)); + } + if (from.line == to.line) add(from.line, from.ch, to.ch, className); + else { + add(from.line, from.ch, null, className); + for (var i = from.line + 1, e = to.line; i < e; ++i) + add(i, null, null, className); + add(to.line, null, to.ch, className); + } + changes.push({from: from.line, to: to.line + 1}); + return tm; + } + + function setBookmark(pos) { + pos = clipPos(pos); + var bm = new Bookmark(pos.ch); + getLine(pos.line).addMark(bm); + return bm; + } + + function addGutterMarker(line, text, className) { + if (typeof line == "number") line = getLine(clipLine(line)); + line.gutterMarker = {text: text, style: className}; + gutterDirty = true; + return line; + } + function removeGutterMarker(line) { + if (typeof line == "number") line = getLine(clipLine(line)); + line.gutterMarker = null; + gutterDirty = true; + } + + function changeLine(handle, op) { + var no = handle, line = handle; + if (typeof handle == "number") line = getLine(clipLine(handle)); + else no = lineNo(handle); + if (no == null) return null; + if (op(line, no)) changes.push({from: no, to: no + 1}); + else return null; + return line; + } + function setLineClass(handle, className) { + return changeLine(handle, function(line) { + if (line.className != className) { + line.className = className; + return true; + } + }); + } + function setLineHidden(handle, hidden) { + return changeLine(handle, function(line, no) { + if (line.hidden != hidden) { + line.hidden = hidden; + updateLineHeight(line, hidden ? 0 : 1); + if (hidden && (sel.from.line == no || sel.to.line == no)) + setSelection(skipHidden(sel.from, sel.from.line, sel.from.ch), + skipHidden(sel.to, sel.to.line, sel.to.ch)); + return (gutterDirty = true); + } + }); + } + + function lineInfo(line) { + if (typeof line == "number") { + if (!isLine(line)) return null; + var n = line; + line = getLine(line); + if (!line) return null; + } + else { + var n = lineNo(line); + if (n == null) return null; + } + var marker = line.gutterMarker; + return {line: n, handle: line, text: line.text, markerText: marker && marker.text, + markerClass: marker && marker.style, lineClass: line.className}; + } + + function stringWidth(str) { + measure.innerHTML = "
              x
              "; + measure.firstChild.firstChild.firstChild.nodeValue = str; + return measure.firstChild.firstChild.offsetWidth || 10; + } + // These are used to go from pixel positions to character + // positions, taking varying character widths into account. + function charFromX(line, x) { + if (x <= 0) return 0; + var lineObj = getLine(line), text = lineObj.text; + function getX(len) { + measure.innerHTML = "
              " + lineObj.getHTML(null, null, false, tabText, len) + "
              "; + return measure.firstChild.firstChild.offsetWidth; + } + var from = 0, fromX = 0, to = text.length, toX; + // Guess a suitable upper bound for our search. + var estimated = Math.min(to, Math.ceil(x / charWidth())); + for (;;) { + var estX = getX(estimated); + if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2)); + else {toX = estX; to = estimated; break;} + } + if (x > toX) return to; + // Try to guess a suitable lower bound as well. + estimated = Math.floor(to * 0.8); estX = getX(estimated); + if (estX < x) {from = estimated; fromX = estX;} + // Do a binary search between these bounds. + for (;;) { + if (to - from <= 1) return (toX - x > x - fromX) ? from : to; + var middle = Math.ceil((from + to) / 2), middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX;} + else {from = middle; fromX = middleX;} + } + } + + var tempId = Math.floor(Math.random() * 0xffffff).toString(16); + function measureLine(line, ch) { + var extra = ""; + // Include extra text at the end to make sure the measured line is wrapped in the right way. + if (options.lineWrapping) { + var end = line.text.indexOf(" ", ch + 2); + extra = htmlEscape(line.text.slice(ch + 1, end < 0 ? line.text.length : end + (ie ? 5 : 0))); + } + measure.innerHTML = "
              " + line.getHTML(null, null, false, tabText, ch) +
              +                '' + htmlEscape(line.text.charAt(ch) || " ") + "" +
              +                extra + "
              "; + var elt = document.getElementById("CodeMirror-temp-" + tempId); + var top = elt.offsetTop, left = elt.offsetLeft; + // Older IEs report zero offsets for spans directly after a wrap + if (ie && ch && top == 0 && left == 0) { + var backup = document.createElement("span"); + backup.innerHTML = "x"; + elt.parentNode.insertBefore(backup, elt.nextSibling); + top = backup.offsetTop; + } + return {top: top, left: left}; + } + function localCoords(pos, inLineWrap) { + var x, lh = textHeight(), y = lh * (heightAtLine(doc, pos.line) - (inLineWrap ? displayOffset : 0)); + if (pos.ch == 0) x = 0; + else { + var sp = measureLine(getLine(pos.line), pos.ch); + x = sp.left; + if (options.lineWrapping) y += Math.max(0, sp.top); + } + return {x: x, y: y, yBot: y + lh}; + } + // Coords must be lineSpace-local + function coordsChar(x, y) { + if (y < 0) y = 0; + var th = textHeight(), cw = charWidth(), heightPos = displayOffset + Math.floor(y / th); + var lineNo = lineAtHeight(doc, heightPos); + if (lineNo >= doc.size) return {line: doc.size - 1, ch: getLine(doc.size - 1).text.length}; + var lineObj = getLine(lineNo), text = lineObj.text; + var tw = options.lineWrapping, innerOff = tw ? heightPos - heightAtLine(doc, lineNo) : 0; + if (x <= 0 && innerOff == 0) return {line: lineNo, ch: 0}; + function getX(len) { + var sp = measureLine(lineObj, len); + if (tw) { + var off = Math.round(sp.top / th); + return Math.max(0, sp.left + (off - innerOff) * scroller.clientWidth); + } + return sp.left; + } + var from = 0, fromX = 0, to = text.length, toX; + // Guess a suitable upper bound for our search. + var estimated = Math.min(to, Math.ceil((x + innerOff * scroller.clientWidth * .9) / cw)); + for (;;) { + var estX = getX(estimated); + if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2)); + else {toX = estX; to = estimated; break;} + } + if (x > toX) return {line: lineNo, ch: to}; + // Try to guess a suitable lower bound as well. + estimated = Math.floor(to * 0.8); estX = getX(estimated); + if (estX < x) {from = estimated; fromX = estX;} + // Do a binary search between these bounds. + for (;;) { + if (to - from <= 1) return {line: lineNo, ch: (toX - x > x - fromX) ? from : to}; + var middle = Math.ceil((from + to) / 2), middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX;} + else {from = middle; fromX = middleX;} + } + } + function pageCoords(pos) { + var local = localCoords(pos, true), off = eltOffset(lineSpace); + return {x: off.left + local.x, y: off.top + local.y, yBot: off.top + local.yBot}; + } + + var cachedHeight, cachedHeightFor, measureText; + function textHeight() { + if (measureText == null) { + measureText = "
              ";
              +                for (var i = 0; i < 49; ++i) measureText += "x
              "; + measureText += "x
              "; + } + var offsetHeight = lineDiv.clientHeight; + if (offsetHeight == cachedHeightFor) return cachedHeight; + cachedHeightFor = offsetHeight; + measure.innerHTML = measureText; + cachedHeight = measure.firstChild.offsetHeight / 50 || 1; + measure.innerHTML = ""; + return cachedHeight; + } + var cachedWidth, cachedWidthFor = 0; + function charWidth() { + if (scroller.clientWidth == cachedWidthFor) return cachedWidth; + cachedWidthFor = scroller.clientWidth; + return (cachedWidth = stringWidth("x")); + } + function paddingTop() {return lineSpace.offsetTop;} + function paddingLeft() {return lineSpace.offsetLeft;} + + function posFromMouse(e, liberal) { + var offW = eltOffset(scroller, true), x, y; + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX; y = e.clientY; } catch (e) { return null; } + // This is a mess of a heuristic to try and determine whether a + // scroll-bar was clicked or not, and to return null if one was + // (and !liberal). + if (!liberal && (x - offW.left > scroller.clientWidth || y - offW.top > scroller.clientHeight)) + return null; + var offL = eltOffset(lineSpace, true); + return coordsChar(x - offL.left, y - offL.top); + } + function onContextMenu(e) { + var pos = posFromMouse(e); + if (!pos || window.opera) return; // Opera is difficult. + if (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to)) + operation(setCursor)(pos.line, pos.ch); + + var oldCSS = input.style.cssText; + inputDiv.style.position = "absolute"; + input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; " + + "border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; + leaveInputAlone = true; + var val = input.value = getSelection(); + focusInput(); + input.select(); + function rehide() { + var newVal = splitLines(input.value).join("\n"); + if (newVal != val) operation(replaceSelection)(newVal, "end"); + inputDiv.style.position = "relative"; + input.style.cssText = oldCSS; + leaveInputAlone = false; + resetInput(true); + slowPoll(); + } + + if (gecko) { + e_stop(e); + var mouseup = connect(window, "mouseup", function() { + mouseup(); + setTimeout(rehide, 20); + }, true); + } + else { + setTimeout(rehide, 50); + } + } + + // Cursor-blinking + function restartBlink() { + clearInterval(blinker); + var on = true; + cursor.style.visibility = ""; + blinker = setInterval(function() { + cursor.style.visibility = (on = !on) ? "" : "hidden"; + }, 650); + } + + var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"}; + function matchBrackets(autoclear) { + var head = sel.inverted ? sel.from : sel.to, line = getLine(head.line), pos = head.ch - 1; + var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)]; + if (!match) return; + var ch = match.charAt(0), forward = match.charAt(1) == ">", d = forward ? 1 : -1, st = line.styles; + for (var off = pos + 1, i = 0, e = st.length; i < e; i+=2) + if ((off -= st[i].length) <= 0) {var style = st[i+1]; break;} + + var stack = [line.text.charAt(pos)], re = /[(){}[\]]/; + function scan(line, from, to) { + if (!line.text) return; + var st = line.styles, pos = forward ? 0 : line.text.length - 1, cur; + for (var i = forward ? 0 : st.length - 2, e = forward ? st.length : -2; i != e; i += 2*d) { + var text = st[i]; + if (st[i+1] != null && st[i+1] != style) {pos += d * text.length; continue;} + for (var j = forward ? 0 : text.length - 1, te = forward ? text.length : -1; j != te; j += d, pos+=d) { + if (pos >= from && pos < to && re.test(cur = text.charAt(j))) { + var match = matching[cur]; + if (match.charAt(1) == ">" == forward) stack.push(cur); + else if (stack.pop() != match.charAt(0)) return {pos: pos, match: false}; + else if (!stack.length) return {pos: pos, match: true}; + } + } + } + } + for (var i = head.line, e = forward ? Math.min(i + 100, doc.size) : Math.max(-1, i - 100); i != e; i+=d) { + var line = getLine(i), first = i == head.line; + var found = scan(line, first && forward ? pos + 1 : 0, first && !forward ? pos : line.text.length); + if (found) break; + } + if (!found) found = {pos: null, match: false}; + var style = found.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; + var one = markText({line: head.line, ch: pos}, {line: head.line, ch: pos+1}, style), + two = found.pos != null && markText({line: i, ch: found.pos}, {line: i, ch: found.pos + 1}, style); + var clear = operation(function(){one.clear(); two && two.clear();}); + if (autoclear) setTimeout(clear, 800); + else bracketHighlighted = clear; + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(n) { + var minindent, minline; + for (var search = n, lim = n - 40; search > lim; --search) { + if (search == 0) return 0; + var line = getLine(search-1); + if (line.stateAfter) return search; + var indented = line.indentation(options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline; + } + function getStateBefore(n) { + var start = findStartLine(n), state = start && getLine(start-1).stateAfter; + if (!state) state = startState(mode); + else state = copyState(mode, state); + doc.iter(start, n, function(line) { + line.highlight(mode, state, options.tabSize); + line.stateAfter = copyState(mode, state); + }); + if (start < n) changes.push({from: start, to: n}); + if (n < doc.size && !getLine(n).stateAfter) work.push(n); + return state; + } + function highlightLines(start, end) { + var state = getStateBefore(start); + doc.iter(start, end, function(line) { + line.highlight(mode, state, options.tabSize); + line.stateAfter = copyState(mode, state); + }); + } + function highlightWorker() { + var end = +new Date + options.workTime; + var foundWork = work.length; + while (work.length) { + if (!getLine(showingFrom).stateAfter) var task = showingFrom; + else var task = work.pop(); + if (task >= doc.size) continue; + var start = findStartLine(task), state = start && getLine(start-1).stateAfter; + if (state) state = copyState(mode, state); + else state = startState(mode); + + var unchanged = 0, compare = mode.compareStates, realChange = false, + i = start, bail = false; + doc.iter(i, doc.size, function(line) { + var hadState = line.stateAfter; + if (+new Date > end) { + work.push(i); + startWorker(options.workDelay); + if (realChange) changes.push({from: task, to: i + 1}); + return (bail = true); + } + var changed = line.highlight(mode, state, options.tabSize); + if (changed) realChange = true; + line.stateAfter = copyState(mode, state); + if (compare) { + if (hadState && compare(hadState, state)) return true; + } else { + if (changed !== false || !hadState) unchanged = 0; + else if (++unchanged > 3 && (!mode.indent || mode.indent(hadState, "") == mode.indent(state, ""))) + return true; + } + ++i; + }); + if (bail) return; + if (realChange) changes.push({from: task, to: i + 1}); + } + if (foundWork && options.onHighlightComplete) + options.onHighlightComplete(instance); + } + function startWorker(time) { + if (!work.length) return; + highlight.set(time, operation(highlightWorker)); + } + + // Operations are used to wrap changes in such a way that each + // change won't have to update the cursor and display (which would + // be awkward, slow, and error-prone), but instead updates are + // batched and then all combined and executed at once. + function startOperation() { + updateInput = userSelChange = textChanged = null; + changes = []; selectionChanged = false; callbacks = []; + } + function endOperation() { + var reScroll = false, updated; + if (selectionChanged) reScroll = !scrollCursorIntoView(); + if (changes.length) updated = updateDisplay(changes, true); + else { + if (selectionChanged) updateCursor(); + if (gutterDirty) updateGutter(); + } + if (reScroll) scrollCursorIntoView(); + if (selectionChanged) {scrollEditorIntoView(); restartBlink();} + + if (focused && !leaveInputAlone && + (updateInput === true || (updateInput !== false && selectionChanged))) + resetInput(userSelChange); + + if (selectionChanged && options.matchBrackets) + setTimeout(operation(function() { + if (bracketHighlighted) {bracketHighlighted(); bracketHighlighted = null;} + if (posEq(sel.from, sel.to)) matchBrackets(false); + }), 20); + var tc = textChanged, cbs = callbacks; // these can be reset by callbacks + if (selectionChanged && options.onCursorActivity) + options.onCursorActivity(instance); + if (tc && options.onChange && instance) + options.onChange(instance, tc); + for (var i = 0; i < cbs.length; ++i) cbs[i](instance); + if (updated && options.onUpdate) options.onUpdate(instance); + } + var nestedOperation = 0; + function operation(f) { + return function() { + if (!nestedOperation++) startOperation(); + try {var result = f.apply(this, arguments);} + finally {if (!--nestedOperation) endOperation();} + return result; + }; + } + + for (var ext in extensions) + if (extensions.propertyIsEnumerable(ext) && + !instance.propertyIsEnumerable(ext)) + instance[ext] = extensions[ext]; + return instance; + } // (end of function CodeMirror) + + // The default configuration options. + CodeMirror.defaults = { + value: "", + mode: null, + theme: "default", + indentUnit: 2, + indentWithTabs: false, + tabSize: 4, + keyMap: "default", + extraKeys: null, + electricChars: true, + onKeyEvent: null, + lineWrapping: false, + lineNumbers: false, + gutter: false, + fixedGutter: false, + firstLineNumber: 1, + readOnly: false, + onChange: null, + onCursorActivity: null, + onGutterClick: null, + onHighlightComplete: null, + onUpdate: null, + onFocus: null, onBlur: null, onScroll: null, + matchBrackets: false, + workTime: 100, + workDelay: 200, + pollInterval: 100, + undoDepth: 40, + tabindex: null, + document: window.document + }; + + var mac = /Mac/.test(navigator.platform); + var win = /Win/.test(navigator.platform); + + // Known modes, by name and by MIME + var modes = {}, mimeModes = {}; + CodeMirror.defineMode = function(name, mode) { + if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name; + modes[name] = mode; + }; + CodeMirror.defineMIME = function(mime, spec) { + mimeModes[mime] = spec; + }; + CodeMirror.getMode = function(options, spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) + spec = mimeModes[spec]; + if (typeof spec == "string") + var mname = spec, config = {}; + else if (spec != null) + var mname = spec.name, config = spec; + var mfactory = modes[mname]; + if (!mfactory) { + if (window.console) console.warn("No mode " + mname + " found, falling back to plain text."); + return CodeMirror.getMode(options, "text/plain"); + } + return mfactory(options, config || {}); + }; + CodeMirror.listModes = function() { + var list = []; + for (var m in modes) + if (modes.propertyIsEnumerable(m)) list.push(m); + return list; + }; + CodeMirror.listMIMEs = function() { + var list = []; + for (var m in mimeModes) + if (mimeModes.propertyIsEnumerable(m)) list.push({mime: m, mode: mimeModes[m]}); + return list; + }; + + var extensions = CodeMirror.extensions = {}; + CodeMirror.defineExtension = function(name, func) { + extensions[name] = func; + }; + + var commands = CodeMirror.commands = { + selectAll: function(cm) {cm.setSelection({line: 0, ch: 0}, {line: cm.lineCount() - 1});}, + killLine: function(cm) { + var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to); + if (!sel && cm.getLine(from.line).length == from.ch) cm.replaceRange("", from, {line: from.line + 1, ch: 0}); + else cm.replaceRange("", from, sel ? to : {line: from.line}); + }, + deleteLine: function(cm) {var l = cm.getCursor().line; cm.replaceRange("", {line: l, ch: 0}, {line: l});}, + undo: function(cm) {cm.undo();}, + redo: function(cm) {cm.redo();}, + goDocStart: function(cm) {cm.setCursor(0, 0, true);}, + goDocEnd: function(cm) {cm.setSelection({line: cm.lineCount() - 1}, null, true);}, + goLineStart: function(cm) {cm.setCursor(cm.getCursor().line, 0, true);}, + goLineStartSmart: function(cm) { + var cur = cm.getCursor(); + var text = cm.getLine(cur.line), firstNonWS = Math.max(0, text.search(/\S/)); + cm.setCursor(cur.line, cur.ch <= firstNonWS && cur.ch ? 0 : firstNonWS, true); + }, + goLineEnd: function(cm) {cm.setSelection({line: cm.getCursor().line}, null, true);}, + goLineUp: function(cm) {cm.moveV(-1, "line");}, + goLineDown: function(cm) {cm.moveV(1, "line");}, + goPageUp: function(cm) {cm.moveV(-1, "page");}, + goPageDown: function(cm) {cm.moveV(1, "page");}, + goCharLeft: function(cm) {cm.moveH(-1, "char");}, + goCharRight: function(cm) {cm.moveH(1, "char");}, + goColumnLeft: function(cm) {cm.moveH(-1, "column");}, + goColumnRight: function(cm) {cm.moveH(1, "column");}, + goWordLeft: function(cm) {cm.moveH(-1, "word");}, + goWordRight: function(cm) {cm.moveH(1, "word");}, + delCharLeft: function(cm) {cm.deleteH(-1, "char");}, + delCharRight: function(cm) {cm.deleteH(1, "char");}, + delWordLeft: function(cm) {cm.deleteH(-1, "word");}, + delWordRight: function(cm) {cm.deleteH(1, "word");}, + indentAuto: function(cm) {cm.indentSelection("smart");}, + indentMore: function(cm) {cm.indentSelection("add");}, + indentLess: function(cm) {cm.indentSelection("subtract");}, + insertTab: function(cm) {cm.replaceSelection("\t", "end");}, + transposeChars: function(cm) { + var cur = cm.getCursor(), line = cm.getLine(cur.line); + if (cur.ch > 0 && cur.ch < line.length - 1) + cm.replaceRange(line.charAt(cur.ch) + line.charAt(cur.ch - 1), + {line: cur.line, ch: cur.ch - 1}, {line: cur.line, ch: cur.ch + 1}); + }, + newlineAndIndent: function(cm) { + cm.replaceSelection("\n", "end"); + cm.indentLine(cm.getCursor().line); + }, + toggleOverwrite: function(cm) {cm.toggleOverwrite();} + }; + + var keyMap = CodeMirror.keyMap = {}; + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharRight", "Backspace": "delCharLeft", "Tab": "indentMore", "Shift-Tab": "indentLess", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite" + }; + // Note that the save and find-related commands aren't defined by + // default. Unknown commands are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Alt-Up": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Down": "goDocEnd", + "Ctrl-Left": "goWordLeft", "Ctrl-Right": "goWordRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delWordLeft", "Ctrl-Delete": "delWordRight", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + fallthrough: "basic" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goWordLeft", + "Alt-Right": "goWordRight", "Cmd-Left": "goLineStart", "Cmd-Right": "goLineEnd", "Alt-Backspace": "delWordLeft", + "Ctrl-Alt-Backspace": "delWordRight", "Alt-Delete": "delWordRight", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + fallthrough: ["basic", "emacsy"] + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "Ctrl-V": "goPageUp", "Shift-Ctrl-V": "goPageDown", "Ctrl-D": "delCharRight", "Ctrl-H": "delCharLeft", + "Alt-D": "delWordRight", "Alt-Backspace": "delWordLeft", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" + }; + + function lookupKey(name, extraMap, map) { + function lookup(name, map, ft) { + var found = map[name]; + if (found != null) return found; + if (ft == null) ft = map.fallthrough; + if (ft == null) return map.catchall; + if (typeof ft == "string") return lookup(name, keyMap[ft]); + for (var i = 0, e = ft.length; i < e; ++i) { + found = lookup(name, keyMap[ft[i]]); + if (found != null) return found; + } + return null; + } + return extraMap ? lookup(name, extraMap, map) : lookup(name, keyMap[map]); + } + function isModifierKey(event) { + var name = keyNames[event.keyCode]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; + } + + CodeMirror.fromTextArea = function(textarea, options) { + if (!options) options = {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabindex) + options.tabindex = textarea.tabindex; + + function save() {textarea.value = instance.getValue();} + if (textarea.form) { + // Deplorable hack to make the submit method do the right thing. + var rmSubmit = connect(textarea.form, "submit", save, true); + if (typeof textarea.form.submit == "function") { + var realSubmit = textarea.form.submit; + function wrappedSubmit() { + save(); + textarea.form.submit = realSubmit; + textarea.form.submit(); + textarea.form.submit = wrappedSubmit; + } + textarea.form.submit = wrappedSubmit; + } + } + + textarea.style.display = "none"; + var instance = CodeMirror(function(node) { + textarea.parentNode.insertBefore(node, textarea.nextSibling); + }, options); + instance.save = save; + instance.getTextArea = function() { return textarea; }; + instance.toTextArea = function() { + save(); + textarea.parentNode.removeChild(instance.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + rmSubmit(); + if (typeof textarea.form.submit == "function") + textarea.form.submit = realSubmit; + } + }; + return instance; + }; + + // Utility functions for working with state. Exported because modes + // sometimes need to do this. + function copyState(mode, state) { + if (state === true) return state; + if (mode.copyState) return mode.copyState(state); + var nstate = {}; + for (var n in state) { + var val = state[n]; + if (val instanceof Array) val = val.concat([]); + nstate[n] = val; + } + return nstate; + } + CodeMirror.copyState = copyState; + function startState(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true; + } + CodeMirror.startState = startState; + + // The character stream used by a mode's parser. + function StringStream(string, tabSize) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + } + StringStream.prototype = { + eol: function() {return this.pos >= this.string.length;}, + sol: function() {return this.pos == 0;}, + peek: function() {return this.string.charAt(this.pos);}, + next: function() { + if (this.pos < this.string.length) + return this.string.charAt(this.pos++); + }, + eat: function(match) { + var ch = this.string.charAt(this.pos); + if (typeof match == "string") var ok = ch == match; + else var ok = ch && (match.test ? match.test(ch) : match(ch)); + if (ok) {++this.pos; return ch;} + }, + eatWhile: function(match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start; + }, + eatSpace: function() { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; + return this.pos > start; + }, + skipToEnd: function() {this.pos = this.string.length;}, + skipTo: function(ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true;} + }, + backUp: function(n) {this.pos -= n;}, + column: function() {return countColumn(this.string, this.start, this.tabSize);}, + indentation: function() {return countColumn(this.string, null, this.tabSize);}, + match: function(pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + function cased(str) {return caseInsensitive ? str.toLowerCase() : str;} + if (cased(this.string).indexOf(cased(pattern), this.pos) == this.pos) { + if (consume !== false) this.pos += pattern.length; + return true; + } + } + else { + var match = this.string.slice(this.pos).match(pattern); + if (match && consume !== false) this.pos += match[0].length; + return match; + } + }, + current: function(){return this.string.slice(this.start, this.pos);} + }; + CodeMirror.StringStream = StringStream; + + function MarkedText(from, to, className, set) { + this.from = from; this.to = to; this.style = className; this.set = set; + } + MarkedText.prototype = { + attach: function(line) { this.set.push(line); }, + detach: function(line) { + var ix = indexOf(this.set, line); + if (ix > -1) this.set.splice(ix, 1); + }, + split: function(pos, lenBefore) { + if (this.to <= pos && this.to != null) return null; + var from = this.from < pos || this.from == null ? null : this.from - pos + lenBefore; + var to = this.to == null ? null : this.to - pos + lenBefore; + return new MarkedText(from, to, this.style, this.set); + }, + dup: function() { return new MarkedText(null, null, this.style, this.set); }, + clipTo: function(fromOpen, from, toOpen, to, diff) { + if (this.from != null && this.from >= from) + this.from = Math.max(to, this.from) + diff; + if (this.to != null && this.to > from) + this.to = to < this.to ? this.to + diff : from; + if (fromOpen && to > this.from && (to < this.to || this.to == null)) + this.from = null; + if (toOpen && (from < this.to || this.to == null) && (from > this.from || this.from == null)) + this.to = null; + }, + isDead: function() { return this.from != null && this.to != null && this.from >= this.to; }, + sameSet: function(x) { return this.set == x.set; } + }; + + function Bookmark(pos) { + this.from = pos; this.to = pos; this.line = null; + } + Bookmark.prototype = { + attach: function(line) { this.line = line; }, + detach: function(line) { if (this.line == line) this.line = null; }, + split: function(pos, lenBefore) { + if (pos < this.from) { + this.from = this.to = (this.from - pos) + lenBefore; + return this; + } + }, + isDead: function() { return this.from > this.to; }, + clipTo: function(fromOpen, from, toOpen, to, diff) { + if ((fromOpen || from < this.from) && (toOpen || to > this.to)) { + this.from = 0; this.to = -1; + } else if (this.from > from) { + this.from = this.to = Math.max(to, this.from) + diff; + } + }, + sameSet: function(x) { return false; }, + find: function() { + if (!this.line || !this.line.parent) return null; + return {line: lineNo(this.line), ch: this.from}; + }, + clear: function() { + if (this.line) { + var found = indexOf(this.line.marked, this); + if (found != -1) this.line.marked.splice(found, 1); + this.line = null; + } + } + }; + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + function Line(text, styles) { + this.styles = styles || [text, null]; + this.text = text; + this.height = 1; + this.marked = this.gutterMarker = this.className = this.handlers = null; + this.stateAfter = this.parent = this.hidden = null; + } + Line.inheritMarks = function(text, orig) { + var ln = new Line(text), mk = orig && orig.marked; + if (mk) { + for (var i = 0; i < mk.length; ++i) { + if (mk[i].to == null && mk[i].style) { + var newmk = ln.marked || (ln.marked = []), mark = mk[i]; + var nmark = mark.dup(); newmk.push(nmark); nmark.attach(ln); + } + } + } + return ln; + } + Line.prototype = { + // Replace a piece of a line, keeping the styles around it intact. + replace: function(from, to_, text) { + var st = [], mk = this.marked, to = to_ == null ? this.text.length : to_; + copyStyles(0, from, this.styles, st); + if (text) st.push(text, null); + copyStyles(to, this.text.length, this.styles, st); + this.styles = st; + this.text = this.text.slice(0, from) + text + this.text.slice(to); + this.stateAfter = null; + if (mk) { + var diff = text.length - (to - from); + for (var i = 0, mark = mk[i]; i < mk.length; ++i) { + mark.clipTo(from == null, from || 0, to_ == null, to, diff); + if (mark.isDead()) {mark.detach(this); mk.splice(i--, 1);} + } + } + }, + // Split a part off a line, keeping styles and markers intact. + split: function(pos, textBefore) { + var st = [textBefore, null], mk = this.marked; + copyStyles(pos, this.text.length, this.styles, st); + var taken = new Line(textBefore + this.text.slice(pos), st); + if (mk) { + for (var i = 0; i < mk.length; ++i) { + var mark = mk[i]; + var newmark = mark.split(pos, textBefore.length); + if (newmark) { + if (!taken.marked) taken.marked = []; + taken.marked.push(newmark); newmark.attach(taken); + } + } + } + return taken; + }, + append: function(line) { + var mylen = this.text.length, mk = line.marked, mymk = this.marked; + this.text += line.text; + copyStyles(0, line.text.length, line.styles, this.styles); + if (mymk) { + for (var i = 0; i < mymk.length; ++i) + if (mymk[i].to == null) mymk[i].to = mylen; + } + if (mk && mk.length) { + if (!mymk) this.marked = mymk = []; + outer: for (var i = 0; i < mk.length; ++i) { + var mark = mk[i]; + if (!mark.from) { + for (var j = 0; j < mymk.length; ++j) { + var mymark = mymk[j]; + if (mymark.to == mylen && mymark.sameSet(mark)) { + mymark.to = mark.to == null ? null : mark.to + mylen; + if (mymark.isDead()) { + mymark.detach(this); + mk.splice(i--, 1); + } + continue outer; + } + } + } + mymk.push(mark); + mark.attach(this); + mark.from += mylen; + if (mark.to != null) mark.to += mylen; + } + } + }, + fixMarkEnds: function(other) { + var mk = this.marked, omk = other.marked; + if (!mk) return; + for (var i = 0; i < mk.length; ++i) { + var mark = mk[i], close = mark.to == null; + if (close && omk) { + for (var j = 0; j < omk.length; ++j) + if (omk[j].sameSet(mark)) {close = false; break;} + } + if (close) mark.to = this.text.length; + } + }, + fixMarkStarts: function() { + var mk = this.marked; + if (!mk) return; + for (var i = 0; i < mk.length; ++i) + if (mk[i].from == null) mk[i].from = 0; + }, + addMark: function(mark) { + mark.attach(this); + if (this.marked == null) this.marked = []; + this.marked.push(mark); + this.marked.sort(function(a, b){return (a.from || 0) - (b.from || 0);}); + }, + // Run the given mode's parser over a line, update the styles + // array, which contains alternating fragments of text and CSS + // classes. + highlight: function(mode, state, tabSize) { + var stream = new StringStream(this.text, tabSize), st = this.styles, pos = 0; + var changed = false, curWord = st[0], prevWord; + if (this.text == "" && mode.blankLine) mode.blankLine(state); + while (!stream.eol()) { + var style = mode.token(stream, state); + var substr = this.text.slice(stream.start, stream.pos); + stream.start = stream.pos; + if (pos && st[pos-1] == style) + st[pos-2] += substr; + else if (substr) { + if (!changed && (st[pos+1] != style || (pos && st[pos-2] != prevWord))) changed = true; + st[pos++] = substr; st[pos++] = style; + prevWord = curWord; curWord = st[pos]; + } + // Give up when line is ridiculously long + if (stream.pos > 5000) { + st[pos++] = this.text.slice(stream.pos); st[pos++] = null; + break; + } + } + if (st.length != pos) {st.length = pos; changed = true;} + if (pos && st[pos-2] != prevWord) changed = true; + // Short lines with simple highlights return null, and are + // counted as changed by the driver because they are likely to + // highlight the same way in various contexts. + return changed || (st.length < 5 && this.text.length < 10 ? null : false); + }, + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(mode, state, ch) { + var txt = this.text, stream = new StringStream(txt); + while (stream.pos < ch && !stream.eol()) { + stream.start = stream.pos; + var style = mode.token(stream, state); + } + return {start: stream.start, + end: stream.pos, + string: stream.current(), + className: style || null, + state: state}; + }, + indentation: function(tabSize) {return countColumn(this.text, null, tabSize);}, + // Produces an HTML fragment for the line, taking selection, + // marking, and highlighting into account. + getHTML: function(sfrom, sto, includePre, tabText, endAt) { + var html = [], first = true; + if (includePre) + html.push(this.className ? '
              ': "
              ");
              +            function span(text, style) {
              +                if (!text) return;
              +                // Work around a bug where, in some compat modes, IE ignores leading spaces
              +                if (first && ie && text.charAt(0) == " ") text = "\u00a0" + text.slice(1);
              +                first = false;
              +                if (style) html.push('', htmlEscape(text).replace(/\t/g, tabText), "");
              +                else html.push(htmlEscape(text).replace(/\t/g, tabText));
              +            }
              +            var st = this.styles, allText = this.text, marked = this.marked;
              +            if (sfrom == sto) sfrom = null;
              +            var len = allText.length;
              +            if (endAt != null) len = Math.min(endAt, len);
              +
              +            if (!allText && endAt == null)
              +                span(" ", sfrom != null && sto == null ? "CodeMirror-selected" : null);
              +            else if (!marked && sfrom == null)
              +                for (var i = 0, ch = 0; ch < len; i+=2) {
              +                    var str = st[i], style = st[i+1], l = str.length;
              +                    if (ch + l > len) str = str.slice(0, len - ch);
              +                    ch += l;
              +                    span(str, style && "cm-" + style);
              +                }
              +            else {
              +                var pos = 0, i = 0, text = "", style, sg = 0;
              +                var markpos = -1, mark = null;
              +                function nextMark() {
              +                    if (marked) {
              +                        markpos += 1;
              +                        mark = (markpos < marked.length) ? marked[markpos] : null;
              +                    }
              +                }
              +                nextMark();
              +                while (pos < len) {
              +                    var upto = len;
              +                    var extraStyle = "";
              +                    if (sfrom != null) {
              +                        if (sfrom > pos) upto = sfrom;
              +                        else if (sto == null || sto > pos) {
              +                            extraStyle = " CodeMirror-selected";
              +                            if (sto != null) upto = Math.min(upto, sto);
              +                        }
              +                    }
              +                    while (mark && mark.to != null && mark.to <= pos) nextMark();
              +                    if (mark) {
              +                        if (mark.from > pos) upto = Math.min(upto, mark.from);
              +                        else {
              +                            extraStyle += " " + mark.style;
              +                            if (mark.to != null) upto = Math.min(upto, mark.to);
              +                        }
              +                    }
              +                    for (;;) {
              +                        var end = pos + text.length;
              +                        var appliedStyle = style;
              +                        if (extraStyle) appliedStyle = style ? style + extraStyle : extraStyle;
              +                        span(end > upto ? text.slice(0, upto - pos) : text, appliedStyle);
              +                        if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;}
              +                        pos = end;
              +                        text = st[i++]; style = "cm-" + st[i++];
              +                    }
              +                }
              +                if (sfrom != null && sto == null) span(" ", "CodeMirror-selected");
              +            }
              +            if (includePre) html.push("
              "); + return html.join(""); + }, + cleanUp: function() { + this.parent = null; + if (this.marked) + for (var i = 0, e = this.marked.length; i < e; ++i) this.marked[i].detach(this); + } + }; + // Utility used by replace and split above + function copyStyles(from, to, source, dest) { + for (var i = 0, pos = 0, state = 0; pos < to; i+=2) { + var part = source[i], end = pos + part.length; + if (state == 0) { + if (end > from) dest.push(part.slice(from - pos, Math.min(part.length, to - pos)), source[i+1]); + if (end >= from) state = 1; + } + else if (state == 1) { + if (end > to) dest.push(part.slice(0, to - pos), source[i+1]); + else dest.push(part, source[i+1]); + } + pos = end; + } + } + + // Data structure that holds the sequence of lines. + function LeafChunk(lines) { + this.lines = lines; + this.parent = null; + for (var i = 0, e = lines.length, height = 0; i < e; ++i) { + lines[i].parent = this; + height += lines[i].height; + } + this.height = height; + } + LeafChunk.prototype = { + chunkSize: function() { return this.lines.length; }, + remove: function(at, n, callbacks) { + for (var i = at, e = at + n; i < e; ++i) { + var line = this.lines[i]; + this.height -= line.height; + line.cleanUp(); + if (line.handlers) + for (var j = 0; j < line.handlers.length; ++j) callbacks.push(line.handlers[j]); + } + this.lines.splice(at, n); + }, + collapse: function(lines) { + lines.splice.apply(lines, [lines.length, 0].concat(this.lines)); + }, + insertHeight: function(at, lines, height) { + this.height += height; + this.lines.splice.apply(this.lines, [at, 0].concat(lines)); + for (var i = 0, e = lines.length; i < e; ++i) lines[i].parent = this; + }, + iterN: function(at, n, op) { + for (var e = at + n; at < e; ++at) + if (op(this.lines[at])) return true; + } + }; + function BranchChunk(children) { + this.children = children; + var size = 0, height = 0; + for (var i = 0, e = children.length; i < e; ++i) { + var ch = children[i]; + size += ch.chunkSize(); height += ch.height; + ch.parent = this; + } + this.size = size; + this.height = height; + this.parent = null; + } + BranchChunk.prototype = { + chunkSize: function() { return this.size; }, + remove: function(at, n, callbacks) { + this.size -= n; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var rm = Math.min(n, sz - at), oldHeight = child.height; + child.remove(at, rm, callbacks); + this.height -= oldHeight - child.height; + if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } + if ((n -= rm) == 0) break; + at = 0; + } else at -= sz; + } + if (this.size - n < 25) { + var lines = []; + this.collapse(lines); + this.children = [new LeafChunk(lines)]; + } + }, + collapse: function(lines) { + for (var i = 0, e = this.children.length; i < e; ++i) this.children[i].collapse(lines); + }, + insert: function(at, lines) { + var height = 0; + for (var i = 0, e = lines.length; i < e; ++i) height += lines[i].height; + this.insertHeight(at, lines, height); + }, + insertHeight: function(at, lines, height) { + this.size += lines.length; + this.height += height; + for (var i = 0, e = this.children.length; i < e; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at <= sz) { + child.insertHeight(at, lines, height); + if (child.lines && child.lines.length > 50) { + while (child.lines.length > 50) { + var spilled = child.lines.splice(child.lines.length - 25, 25); + var newleaf = new LeafChunk(spilled); + child.height -= newleaf.height; + this.children.splice(i + 1, 0, newleaf); + newleaf.parent = this; + } + this.maybeSpill(); + } + break; + } + at -= sz; + } + }, + maybeSpill: function() { + if (this.children.length <= 10) return; + var me = this; + do { + var spilled = me.children.splice(me.children.length - 5, 5); + var sibling = new BranchChunk(spilled); + if (!me.parent) { // Become the parent node + var copy = new BranchChunk(me.children); + copy.parent = me; + me.children = [copy, sibling]; + me = copy; + } else { + me.size -= sibling.size; + me.height -= sibling.height; + var myIndex = indexOf(me.parent.children, me); + me.parent.children.splice(myIndex + 1, 0, sibling); + } + sibling.parent = me.parent; + } while (me.children.length > 10); + me.parent.maybeSpill(); + }, + iter: function(from, to, op) { this.iterN(from, to - from, op); }, + iterN: function(at, n, op) { + for (var i = 0, e = this.children.length; i < e; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var used = Math.min(n, sz - at); + if (child.iterN(at, used, op)) return true; + if ((n -= used) == 0) break; + at = 0; + } else at -= sz; + } + } + }; + + function getLineAt(chunk, n) { + while (!chunk.lines) { + for (var i = 0;; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; break; } + n -= sz; + } + } + return chunk.lines[n]; + } + function lineNo(line) { + if (line.parent == null) return null; + var cur = line.parent, no = indexOf(cur.lines, line); + for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { + for (var i = 0, e = chunk.children.length; ; ++i) { + if (chunk.children[i] == cur) break; + no += chunk.children[i].chunkSize(); + } + } + return no; + } + function lineAtHeight(chunk, h) { + var n = 0; + outer: do { + for (var i = 0, e = chunk.children.length; i < e; ++i) { + var child = chunk.children[i], ch = child.height; + if (h < ch) { chunk = child; continue outer; } + h -= ch; + n += child.chunkSize(); + } + return n; + } while (!chunk.lines); + for (var i = 0, e = chunk.lines.length; i < e; ++i) { + var line = chunk.lines[i], lh = line.height; + if (h < lh) break; + h -= lh; + } + return n + i; + } + function heightAtLine(chunk, n) { + var h = 0; + outer: do { + for (var i = 0, e = chunk.children.length; i < e; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; continue outer; } + n -= sz; + h += child.height; + } + return h; + } while (!chunk.lines); + for (var i = 0; i < n; ++i) h += chunk.lines[i].height; + return h; + } + + // The history object 'chunks' changes that are made close together + // and at almost the same time into bigger undoable units. + function History() { + this.time = 0; + this.done = []; this.undone = []; + } + History.prototype = { + addChange: function(start, added, old) { + this.undone.length = 0; + var time = +new Date, last = this.done[this.done.length - 1]; + if (time - this.time > 400 || !last || + last.start > start + added || last.start + last.added < start - last.added + last.old.length) + this.done.push({start: start, added: added, old: old}); + else { + var oldoff = 0; + if (start < last.start) { + for (var i = last.start - start - 1; i >= 0; --i) + last.old.unshift(old[i]); + last.added += last.start - start; + last.start = start; + } + else if (last.start < start) { + oldoff = start - last.start; + added += oldoff; + } + for (var i = last.added - oldoff, e = old.length; i < e; ++i) + last.old.push(old[i]); + if (last.added < added) last.added = added; + } + this.time = time; + } + }; + + function stopMethod() {e_stop(this);} + // Ensure an event has a stop method. + function addStop(event) { + if (!event.stop) event.stop = stopMethod; + return event; + } + + function e_preventDefault(e) { + if (e.preventDefault) e.preventDefault(); + else e.returnValue = false; + } + function e_stopPropagation(e) { + if (e.stopPropagation) e.stopPropagation(); + else e.cancelBubble = true; + } + function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);} + CodeMirror.e_stop = e_stop; + CodeMirror.e_preventDefault = e_preventDefault; + CodeMirror.e_stopPropagation = e_stopPropagation; + + function e_target(e) {return e.target || e.srcElement;} + function e_button(e) { + if (e.which) return e.which; + else if (e.button & 1) return 1; + else if (e.button & 2) return 3; + else if (e.button & 4) return 2; + } + + // Event handler registration. If disconnect is true, it'll return a + // function that unregisters the handler. + function connect(node, type, handler, disconnect) { + if (typeof node.addEventListener == "function") { + node.addEventListener(type, handler, false); + if (disconnect) return function() {node.removeEventListener(type, handler, false);}; + } + else { + var wrapHandler = function(event) {handler(event || window.event);}; + node.attachEvent("on" + type, wrapHandler); + if (disconnect) return function() {node.detachEvent("on" + type, wrapHandler);}; + } + } + CodeMirror.connect = connect; + + function Delayed() {this.id = null;} + Delayed.prototype = {set: function(ms, f) {clearTimeout(this.id); this.id = setTimeout(f, ms);}}; + + // Detect drag-and-drop + var dragAndDrop = function() { + // IE8 has ondragstart and ondrop properties, but doesn't seem to + // actually support ondragstart the way it's supposed to work. + if (/MSIE [1-8]\b/.test(navigator.userAgent)) return false; + var div = document.createElement('div'); + return "draggable" in div; + }(); + + var gecko = /gecko\/\d{7}/i.test(navigator.userAgent); + var ie = /MSIE \d/.test(navigator.userAgent); + var webkit = /WebKit\//.test(navigator.userAgent); + + var lineSep = "\n"; + // Feature-detect whether newlines in textareas are converted to \r\n + (function () { + var te = document.createElement("textarea"); + te.value = "foo\nbar"; + if (te.value.indexOf("\r") > -1) lineSep = "\r\n"; + }()); + + // Counts the column offset in a string, taking tabs into account. + // Used mostly to find indentation. + function countColumn(string, end, tabSize) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) end = string.length; + } + for (var i = 0, n = 0; i < end; ++i) { + if (string.charAt(i) == "\t") n += tabSize - (n % tabSize); + else ++n; + } + return n; + } + + function computedStyle(elt) { + if (elt.currentStyle) return elt.currentStyle; + return window.getComputedStyle(elt, null); + } + + // Find the position of an element by following the offsetParent chain. + // If screen==true, it returns screen (rather than page) coordinates. + function eltOffset(node, screen) { + var bod = node.ownerDocument.body; + var x = 0, y = 0, skipBody = false; + for (var n = node; n; n = n.offsetParent) { + var ol = n.offsetLeft, ot = n.offsetTop; + // Firefox reports weird inverted offsets when the body has a border. + if (n == bod) { x += Math.abs(ol); y += Math.abs(ot); } + else { x += ol, y += ot; } + if (screen && computedStyle(n).position == "fixed") + skipBody = true; + } + var e = screen && !skipBody ? null : bod; + for (var n = node.parentNode; n != e; n = n.parentNode) + if (n.scrollLeft != null) { x -= n.scrollLeft; y -= n.scrollTop;} + return {left: x, top: y}; + } + // Use the faster and saner getBoundingClientRect method when possible. + if (document.documentElement.getBoundingClientRect != null) eltOffset = function(node, screen) { + // Take the parts of bounding client rect that we are interested in so we are able to edit if need be, + // since the returned value cannot be changed externally (they are kept in sync as the element moves within the page) + try { var box = node.getBoundingClientRect(); box = { top: box.top, left: box.left }; } + catch(e) { box = {top: 0, left: 0}; } + if (!screen) { + // Get the toplevel scroll, working around browser differences. + if (window.pageYOffset == null) { + var t = document.documentElement || document.body.parentNode; + if (t.scrollTop == null) t = document.body; + box.top += t.scrollTop; box.left += t.scrollLeft; + } else { + box.top += window.pageYOffset; box.left += window.pageXOffset; + } + } + return box; + }; + + // Get a node's text content. + function eltText(node) { + return node.textContent || node.innerText || node.nodeValue || ""; + } + + // Operations on {line, ch} objects. + function posEq(a, b) {return a.line == b.line && a.ch == b.ch;} + function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);} + function copyPos(x) {return {line: x.line, ch: x.ch};} + + var escapeElement = document.createElement("pre"); + function htmlEscape(str) { + escapeElement.textContent = str; + return escapeElement.innerHTML; + } + // Recent (late 2011) Opera betas insert bogus newlines at the start + // of the textContent, so we strip those. + if (htmlEscape("a") == "\na") + htmlEscape = function(str) { + escapeElement.textContent = str; + return escapeElement.innerHTML.slice(1); + }; + // Some IEs don't preserve tabs through innerHTML + else if (htmlEscape("\t") != "\t") + htmlEscape = function(str) { + escapeElement.innerHTML = ""; + escapeElement.appendChild(document.createTextNode(str)); + return escapeElement.innerHTML; + }; + CodeMirror.htmlEscape = htmlEscape; + + // Used to position the cursor after an undo/redo by finding the + // last edited character. + function editEnd(from, to) { + if (!to) return from ? from.length : 0; + if (!from) return to.length; + for (var i = from.length, j = to.length; i >= 0 && j >= 0; --i, --j) + if (from.charAt(i) != to.charAt(j)) break; + return j + 1; + } + + function indexOf(collection, elt) { + if (collection.indexOf) return collection.indexOf(elt); + for (var i = 0, e = collection.length; i < e; ++i) + if (collection[i] == elt) return i; + return -1; + } + function isWordChar(ch) { + return /\w/.test(ch) || ch.toUpperCase() != ch.toLowerCase(); + } + + // See if "".split is the broken IE version, if so, provide an + // alternative way to split lines. + var splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { + var pos = 0, nl, result = []; + while ((nl = string.indexOf("\n", pos)) > -1) { + result.push(string.slice(pos, string.charAt(nl-1) == "\r" ? nl - 1 : nl)); + pos = nl + 1; + } + result.push(string.slice(pos)); + return result; + } : function(string){return string.split(/\r?\n/);}; + CodeMirror.splitLines = splitLines; + + var hasSelection = window.getSelection ? function(te) { + try { return te.selectionStart != te.selectionEnd; } + catch(e) { return false; } + } : function(te) { + try {var range = te.ownerDocument.selection.createRange();} + catch(e) {} + if (!range || range.parentElement() != te) return false; + return range.compareEndPoints("StartToEnd", range) != 0; + }; + + CodeMirror.defineMode("null", function() { + return {token: function(stream) {stream.skipToEnd();}}; + }); + CodeMirror.defineMIME("text/plain", "null"); + + var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 91: "Mod", 92: "Mod", 93: "Mod", 186: ";", 187: "=", 188: ",", + 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", 221: "]", 222: "'", 63276: "PageUp", + 63277: "PageDown", 63275: "End", 63273: "Home", 63234: "Left", 63232: "Up", 63235: "Right", + 63233: "Down", 63302: "Insert", 63272: "Delete"}; + CodeMirror.keyNames = keyNames; + (function() { + // Number keys + for (var i = 0; i < 10; i++) keyNames[i + 48] = String(i); + // Alphabetic keys + for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i); + // Function keys + for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i; + })(); + + return CodeMirror; +})(); +CodeMirror.defineMode("xml", function(config, parserConfig) { + var indentUnit = config.indentUnit; + var Kludges = parserConfig.htmlMode ? { + autoSelfClosers: {"br": true, "img": true, "hr": true, "link": true, "input": true, + "meta": true, "col": true, "frame": true, "base": true, "area": true}, + doNotIndent: {"pre": true}, + allowUnquoted: true + } : {autoSelfClosers: {}, doNotIndent: {}, allowUnquoted: false}; + var alignCDATA = parserConfig.alignCDATA; + + // Return variables for tokenizers + var tagName, type; + + function inText(stream, state) { + function chain(parser) { + state.tokenize = parser; + return parser(stream, state); + } + + var ch = stream.next(); + if (ch == "<") { + if (stream.eat("!")) { + if (stream.eat("[")) { + if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>")); + else return null; + } + else if (stream.match("--")) return chain(inBlock("comment", "-->")); + else if (stream.match("DOCTYPE", true, true)) { + stream.eatWhile(/[\w\._\-]/); + return chain(doctype(1)); + } + else return null; + } + else if (stream.eat("?")) { + stream.eatWhile(/[\w\._\-]/); + state.tokenize = inBlock("meta", "?>"); + return "meta"; + } + else { + type = stream.eat("/") ? "closeTag" : "openTag"; + stream.eatSpace(); + tagName = ""; + var c; + while ((c = stream.eat(/[^\s\u00a0=<>\"\'\/?]/))) tagName += c; + state.tokenize = inTag; + return "tag"; + } + } + else if (ch == "&") { + stream.eatWhile(/[^;]/); + stream.eat(";"); + return "atom"; + } + else { + stream.eatWhile(/[^&<]/); + return null; + } + } + + function inTag(stream, state) { + var ch = stream.next(); + if (ch == ">" || (ch == "/" && stream.eat(">"))) { + state.tokenize = inText; + type = ch == ">" ? "endTag" : "selfcloseTag"; + return "tag"; + } + else if (ch == "=") { + type = "equals"; + return null; + } + else if (/[\'\"]/.test(ch)) { + state.tokenize = inAttribute(ch); + return state.tokenize(stream, state); + } + else { + stream.eatWhile(/[^\s\u00a0=<>\"\'\/?]/); + return "word"; + } + } + + function inAttribute(quote) { + return function(stream, state) { + while (!stream.eol()) { + if (stream.next() == quote) { + state.tokenize = inTag; + break; + } + } + return "string"; + }; + } + + function inBlock(style, terminator) { + return function(stream, state) { + while (!stream.eol()) { + if (stream.match(terminator)) { + state.tokenize = inText; + break; + } + stream.next(); + } + return style; + }; + } + function doctype(depth) { + return function(stream, state) { + var ch; + while ((ch = stream.next()) != null) { + if (ch == "<") { + state.tokenize = doctype(depth + 1); + return state.tokenize(stream, state); + } else if (ch == ">") { + if (depth == 1) { + state.tokenize = inText; + break; + } else { + state.tokenize = doctype(depth - 1); + return state.tokenize(stream, state); + } + } + } + return "meta"; + }; + } + + var curState, setStyle; + function pass() { + for (var i = arguments.length - 1; i >= 0; i--) curState.cc.push(arguments[i]); + } + function cont() { + pass.apply(null, arguments); + return true; + } + + function pushContext(tagName, startOfLine) { + var noIndent = Kludges.doNotIndent.hasOwnProperty(tagName) || (curState.context && curState.context.noIndent); + curState.context = { + prev: curState.context, + tagName: tagName, + indent: curState.indented, + startOfLine: startOfLine, + noIndent: noIndent + }; + } + function popContext() { + if (curState.context) curState.context = curState.context.prev; + } + + function element(type) { + if (type == "openTag") { + curState.tagName = tagName; + return cont(attributes, endtag(curState.startOfLine)); + } else if (type == "closeTag") { + var err = false; + if (curState.context) { + err = curState.context.tagName != tagName; + } else { + err = true; + } + if (err) setStyle = "error"; + return cont(endclosetag(err)); + } + return cont(); + } + function endtag(startOfLine) { + return function(type) { + if (type == "selfcloseTag" || + (type == "endTag" && Kludges.autoSelfClosers.hasOwnProperty(curState.tagName.toLowerCase()))) + return cont(); + if (type == "endTag") {pushContext(curState.tagName, startOfLine); return cont();} + return cont(); + }; + } + function endclosetag(err) { + return function(type) { + if (err) setStyle = "error"; + if (type == "endTag") { popContext(); return cont(); } + setStyle = "error"; + return cont(arguments.callee); + } + } + + function attributes(type) { + if (type == "word") {setStyle = "attribute"; return cont(attributes);} + if (type == "equals") return cont(attvalue, attributes); + if (type == "string") {setStyle = "error"; return cont(attributes);} + return pass(); + } + function attvalue(type) { + if (type == "word" && Kludges.allowUnquoted) {setStyle = "string"; return cont();} + if (type == "string") return cont(attvaluemaybe); + return pass(); + } + function attvaluemaybe(type) { + if (type == "string") return cont(attvaluemaybe); + else return pass(); + } + + return { + startState: function() { + return {tokenize: inText, cc: [], indented: 0, startOfLine: true, tagName: null, context: null}; + }, + + token: function(stream, state) { + if (stream.sol()) { + state.startOfLine = true; + state.indented = stream.indentation(); + } + if (stream.eatSpace()) return null; + + setStyle = type = tagName = null; + var style = state.tokenize(stream, state); + state.type = type; + if ((style || type) && style != "comment") { + curState = state; + while (true) { + var comb = state.cc.pop() || element; + if (comb(type || style)) break; + } + } + state.startOfLine = false; + return setStyle || style; + }, + + indent: function(state, textAfter, fullLine) { + var context = state.context; + if ((state.tokenize != inTag && state.tokenize != inText) || + context && context.noIndent) + return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0; + if (alignCDATA && /!?|]/; + + function chain(stream, state, f) { + state.tokenize = f; + return f(stream, state); + } + + function nextUntilUnescaped(stream, end) { + var escaped = false, next; + while ((next = stream.next()) != null) { + if (next == end && !escaped) + return false; + escaped = !escaped && next == "\\"; + } + return escaped; + } + + // Used as scratch variables to communicate multiple values without + // consing up tons of objects. + var type, content; + function ret(tp, style, cont) { + type = tp; content = cont; + return style; + } + + function jsTokenBase(stream, state) { + var ch = stream.next(); + if (ch == '"' || ch == "'") + return chain(stream, state, jsTokenString(ch)); + else if (/[\[\]{}\(\),;\:\.]/.test(ch)) + return ret(ch); + else if (ch == "0" && stream.eat(/x/i)) { + stream.eatWhile(/[\da-f]/i); + return ret("number", "number"); + } + else if (/\d/.test(ch)) { + stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/); + return ret("number", "number"); + } + else if (ch == "/") { + if (stream.eat("*")) { + return chain(stream, state, jsTokenComment); + } + else if (stream.eat("/")) { + stream.skipToEnd(); + return ret("comment", "comment"); + } + else if (state.reAllowed) { + nextUntilUnescaped(stream, "/"); + stream.eatWhile(/[gimy]/); // 'y' is "sticky" option in Mozilla + return ret("regexp", "string"); + } + else { + stream.eatWhile(isOperatorChar); + return ret("operator", null, stream.current()); + } + } + else if (ch == "#") { + stream.skipToEnd(); + return ret("error", "error"); + } + else if (isOperatorChar.test(ch)) { + stream.eatWhile(isOperatorChar); + return ret("operator", null, stream.current()); + } + else { + stream.eatWhile(/[\w\$_]/); + var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word]; + return (known && state.kwAllowed) ? ret(known.type, known.style, word) : + ret("variable", "variable", word); + } + } + + function jsTokenString(quote) { + return function(stream, state) { + if (!nextUntilUnescaped(stream, quote)) + state.tokenize = jsTokenBase; + return ret("string", "string"); + }; + } + + function jsTokenComment(stream, state) { + var maybeEnd = false, ch; + while (ch = stream.next()) { + if (ch == "/" && maybeEnd) { + state.tokenize = jsTokenBase; + break; + } + maybeEnd = (ch == "*"); + } + return ret("comment", "comment"); + } + + // Parser + + var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true}; + + function JSLexical(indented, column, type, align, prev, info) { + this.indented = indented; + this.column = column; + this.type = type; + this.prev = prev; + this.info = info; + if (align != null) this.align = align; + } + + function inScope(state, varname) { + for (var v = state.localVars; v; v = v.next) + if (v.name == varname) return true; + } + + function parseJS(state, style, type, content, stream) { + var cc = state.cc; + // Communicate our context to the combinators. + // (Less wasteful than consing up a hundred closures on every call.) + cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; + + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = true; + + while(true) { + var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; + if (combinator(type, content)) { + while(cc.length && cc[cc.length - 1].lex) + cc.pop()(); + if (cx.marked) return cx.marked; + if (type == "variable" && inScope(state, content)) return "variable-2"; + return style; + } + } + } + + // Combinator utils + + var cx = {state: null, column: null, marked: null, cc: null}; + function pass() { + for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); + } + function cont() { + pass.apply(null, arguments); + return true; + } + function register(varname) { + var state = cx.state; + if (state.context) { + cx.marked = "def"; + for (var v = state.localVars; v; v = v.next) + if (v.name == varname) return; + state.localVars = {name: varname, next: state.localVars}; + } + } + + // Combinators + + var defaultVars = {name: "this", next: {name: "arguments"}}; + function pushcontext() { + if (!cx.state.context) cx.state.localVars = defaultVars; + cx.state.context = {prev: cx.state.context, vars: cx.state.localVars}; + } + function popcontext() { + cx.state.localVars = cx.state.context.vars; + cx.state.context = cx.state.context.prev; + } + function pushlex(type, info) { + var result = function() { + var state = cx.state; + state.lexical = new JSLexical(state.indented, cx.stream.column(), type, null, state.lexical, info) + }; + result.lex = true; + return result; + } + function poplex() { + var state = cx.state; + if (state.lexical.prev) { + if (state.lexical.type == ")") + state.indented = state.lexical.indented; + state.lexical = state.lexical.prev; + } + } + poplex.lex = true; + + function expect(wanted) { + return function expecting(type) { + if (type == wanted) return cont(); + else if (wanted == ";") return pass(); + else return cont(arguments.callee); + }; + } + + function statement(type) { + if (type == "var") return cont(pushlex("vardef"), vardef1, expect(";"), poplex); + if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex); + if (type == "keyword b") return cont(pushlex("form"), statement, poplex); + if (type == "{") return cont(pushlex("}"), block, poplex); + if (type == ";") return cont(); + if (type == "function") return cont(functiondef); + if (type == "for") return cont(pushlex("form"), expect("("), pushlex(")"), forspec1, expect(")"), + poplex, statement, poplex); + if (type == "variable") return cont(pushlex("stat"), maybelabel); + if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"), + block, poplex, poplex); + if (type == "case") return cont(expression, expect(":")); + if (type == "default") return cont(expect(":")); + if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"), + statement, poplex, popcontext); + return pass(pushlex("stat"), expression, expect(";"), poplex); + } + function expression(type) { + if (atomicTypes.hasOwnProperty(type)) return cont(maybeoperator); + if (type == "function") return cont(functiondef); + if (type == "keyword c") return cont(maybeexpression); + if (type == "(") return cont(pushlex(")"), expression, expect(")"), poplex, maybeoperator); + if (type == "operator") return cont(expression); + if (type == "[") return cont(pushlex("]"), commasep(expression, "]"), poplex, maybeoperator); + if (type == "{") return cont(pushlex("}"), commasep(objprop, "}"), poplex, maybeoperator); + return cont(); + } + function maybeexpression(type) { + if (type.match(/[;\}\)\],]/)) return pass(); + return pass(expression); + } + + function maybeoperator(type, value) { + if (type == "operator" && /\+\+|--/.test(value)) return cont(maybeoperator); + if (type == "operator") return cont(expression); + if (type == ";") return; + if (type == "(") return cont(pushlex(")"), commasep(expression, ")"), poplex, maybeoperator); + if (type == ".") return cont(property, maybeoperator); + if (type == "[") return cont(pushlex("]"), expression, expect("]"), poplex, maybeoperator); + } + function maybelabel(type) { + if (type == ":") return cont(poplex, statement); + return pass(maybeoperator, expect(";"), poplex); + } + function property(type) { + if (type == "variable") {cx.marked = "property"; return cont();} + } + function objprop(type) { + if (type == "variable") cx.marked = "property"; + if (atomicTypes.hasOwnProperty(type)) return cont(expect(":"), expression); + } + function commasep(what, end) { + function proceed(type) { + if (type == ",") return cont(what, proceed); + if (type == end) return cont(); + return cont(expect(end)); + } + return function commaSeparated(type) { + if (type == end) return cont(); + else return pass(what, proceed); + }; + } + function block(type) { + if (type == "}") return cont(); + return pass(statement, block); + } + function vardef1(type, value) { + if (type == "variable"){register(value); return cont(vardef2);} + return cont(); + } + function vardef2(type, value) { + if (value == "=") return cont(expression, vardef2); + if (type == ",") return cont(vardef1); + } + function forspec1(type) { + if (type == "var") return cont(vardef1, forspec2); + if (type == ";") return pass(forspec2); + if (type == "variable") return cont(formaybein); + return pass(forspec2); + } + function formaybein(type, value) { + if (value == "in") return cont(expression); + return cont(maybeoperator, forspec2); + } + function forspec2(type, value) { + if (type == ";") return cont(forspec3); + if (value == "in") return cont(expression); + return cont(expression, expect(";"), forspec3); + } + function forspec3(type) { + if (type != ")") cont(expression); + } + function functiondef(type, value) { + if (type == "variable") {register(value); return cont(functiondef);} + if (type == "(") return cont(pushlex(")"), pushcontext, commasep(funarg, ")"), poplex, statement, popcontext); + } + function funarg(type, value) { + if (type == "variable") {register(value); return cont();} + } + + // Interface + + return { + startState: function(basecolumn) { + return { + tokenize: jsTokenBase, + reAllowed: true, + kwAllowed: true, + cc: [], + lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), + localVars: null, + context: null, + indented: 0 + }; + }, + + token: function(stream, state) { + if (stream.sol()) { + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = false; + state.indented = stream.indentation(); + } + if (stream.eatSpace()) return null; + var style = state.tokenize(stream, state); + if (type == "comment") return style; + state.reAllowed = type == "operator" || type == "keyword c" || type.match(/^[\[{}\(,;:]$/); + state.kwAllowed = type != '.'; + return parseJS(state, style, type, content, stream); + }, + + indent: function(state, textAfter) { + if (state.tokenize != jsTokenBase) return 0; + var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, + type = lexical.type, closing = firstChar == type; + if (type == "vardef") return lexical.indented + 4; + else if (type == "form" && firstChar == "{") return lexical.indented; + else if (type == "stat" || type == "form") return lexical.indented + indentUnit; + else if (lexical.info == "switch" && !closing) + return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); + else if (lexical.align) return lexical.column + (closing ? 0 : 1); + else return lexical.indented + (closing ? 0 : indentUnit); + }, + + electricChars: ":{}" + }; +}); + +CodeMirror.defineMIME("text/javascript", "javascript"); +CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); + +CodeMirror.defineMode("css", function(config) { + var indentUnit = config.indentUnit, type; + function ret(style, tp) {type = tp; return style;} + + function tokenBase(stream, state) { + var ch = stream.next(); + if (ch == "@") {stream.eatWhile(/[\w\\\-]/); return ret("meta", stream.current());} + else if (ch == "/" && stream.eat("*")) { + state.tokenize = tokenCComment; + return tokenCComment(stream, state); + } + else if (ch == "<" && stream.eat("!")) { + state.tokenize = tokenSGMLComment; + return tokenSGMLComment(stream, state); + } + else if (ch == "=") ret(null, "compare"); + else if ((ch == "~" || ch == "|") && stream.eat("=")) return ret(null, "compare"); + else if (ch == "\"" || ch == "'") { + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } + else if (ch == "#") { + stream.eatWhile(/[\w\\\-]/); + return ret("atom", "hash"); + } + else if (ch == "!") { + stream.match(/^\s*\w*/); + return ret("keyword", "important"); + } + else if (/\d/.test(ch)) { + stream.eatWhile(/[\w.%]/); + return ret("number", "unit"); + } + else if (/[,.+>*\/]/.test(ch)) { + return ret(null, "select-op"); + } + else if (/[;{}:\[\]]/.test(ch)) { + return ret(null, ch); + } + else { + stream.eatWhile(/[\w\\\-]/); + return ret("variable", "variable"); + } + } + + function tokenCComment(stream, state) { + var maybeEnd = false, ch; + while ((ch = stream.next()) != null) { + if (maybeEnd && ch == "/") { + state.tokenize = tokenBase; + break; + } + maybeEnd = (ch == "*"); + } + return ret("comment", "comment"); + } + + function tokenSGMLComment(stream, state) { + var dashes = 0, ch; + while ((ch = stream.next()) != null) { + if (dashes >= 2 && ch == ">") { + state.tokenize = tokenBase; + break; + } + dashes = (ch == "-") ? dashes + 1 : 0; + } + return ret("comment", "comment"); + } + + function tokenString(quote) { + return function(stream, state) { + var escaped = false, ch; + while ((ch = stream.next()) != null) { + if (ch == quote && !escaped) + break; + escaped = !escaped && ch == "\\"; + } + if (!escaped) state.tokenize = tokenBase; + return ret("string", "string"); + }; + } + + return { + startState: function(base) { + return {tokenize: tokenBase, + baseIndent: base || 0, + stack: []}; + }, + + token: function(stream, state) { + if (stream.eatSpace()) return null; + var style = state.tokenize(stream, state); + + var context = state.stack[state.stack.length-1]; + if (type == "hash" && context == "rule") style = "atom"; + else if (style == "variable") { + if (context == "rule") style = "number"; + else if (!context || context == "@media{") style = "tag"; + } + + if (context == "rule" && /^[\{\};]$/.test(type)) + state.stack.pop(); + if (type == "{") { + if (context == "@media") state.stack[state.stack.length-1] = "@media{"; + else state.stack.push("{"); + } + else if (type == "}") state.stack.pop(); + else if (type == "@media") state.stack.push("@media"); + else if (context == "{" && type != "comment") state.stack.push("rule"); + return style; + }, + + indent: function(state, textAfter) { + var n = state.stack.length; + if (/^\}/.test(textAfter)) + n -= state.stack[state.stack.length-1] == "rule" ? 2 : 1; + return state.baseIndent + n * indentUnit; + }, + + electricChars: "}" + }; +}); + +CodeMirror.defineMIME("text/css", "css"); +CodeMirror.defineMode("htmlmixed", function(config, parserConfig) { + var htmlMode = CodeMirror.getMode(config, {name: "xml", htmlMode: true}); + var jsMode = CodeMirror.getMode(config, "javascript"); + var cssMode = CodeMirror.getMode(config, "css"); + + function html(stream, state) { + var style = htmlMode.token(stream, state.htmlState); + if (style == "tag" && stream.current() == ">" && state.htmlState.context) { + if (/^script$/i.test(state.htmlState.context.tagName)) { + state.token = javascript; + state.localState = jsMode.startState(htmlMode.indent(state.htmlState, "")); + state.mode = "javascript"; + } + else if (/^style$/i.test(state.htmlState.context.tagName)) { + state.token = css; + state.localState = cssMode.startState(htmlMode.indent(state.htmlState, "")); + state.mode = "css"; + } + } + return style; + } + function maybeBackup(stream, pat, style) { + var cur = stream.current(); + var close = cur.search(pat); + if (close > -1) stream.backUp(cur.length - close); + return style; + } + function javascript(stream, state) { + if (stream.match(/^<\/\s*script\s*>/i, false)) { + state.token = html; + state.curState = null; + state.mode = "html"; + return html(stream, state); + } + return maybeBackup(stream, /<\/\s*script\s*>/, + jsMode.token(stream, state.localState)); + } + function css(stream, state) { + if (stream.match(/^<\/\s*style\s*>/i, false)) { + state.token = html; + state.localState = null; + state.mode = "html"; + return html(stream, state); + } + return maybeBackup(stream, /<\/\s*style\s*>/, + cssMode.token(stream, state.localState)); + } + + return { + startState: function() { + var state = htmlMode.startState(); + return {token: html, localState: null, mode: "html", htmlState: state}; + }, + + copyState: function(state) { + if (state.localState) + var local = CodeMirror.copyState(state.token == css ? cssMode : jsMode, state.localState); + return {token: state.token, localState: local, mode: state.mode, + htmlState: CodeMirror.copyState(htmlMode, state.htmlState)}; + }, + + token: function(stream, state) { + return state.token(stream, state); + }, + + indent: function(state, textAfter) { + if (state.token == html || /^\s*<\//.test(textAfter)) + return htmlMode.indent(state.htmlState, textAfter); + else if (state.token == javascript) + return jsMode.indent(state.localState, textAfter); + else + return cssMode.indent(state.localState, textAfter); + }, + + compareStates: function(a, b) { + return htmlMode.compareStates(a.htmlState, b.htmlState); + }, + + electricChars: "/{}:" + } +}); + +CodeMirror.defineMIME("text/html", "htmlmixed"); diff --git a/public/static/libs/ueditor/third-party/highcharts/adapters/mootools-adapter.js b/public/static/libs/ueditor/third-party/highcharts/adapters/mootools-adapter.js new file mode 100644 index 0000000..b02738e --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/adapters/mootools-adapter.js @@ -0,0 +1,13 @@ +/* + Highcharts JS v3.0.6 (2013-10-04) + MooTools adapter + + (c) 2010-2013 Torstein Hønsi + + License: www.highcharts.com/license +*/ +(function(){var e=window,h=document,f=e.MooTools.version.substring(0,3),i=f==="1.2"||f==="1.1",j=i||f==="1.3",g=e.$extend||function(){return Object.append.apply(Object,arguments)};e.HighchartsAdapter={init:function(a){var b=Fx.prototype,c=b.start,d=Fx.Morph.prototype,e=d.compute;b.start=function(b,d){var e=this.element;if(b.d)this.paths=a.init(e,e.d,this.toD);c.apply(this,arguments);return this};d.compute=function(b,c,d){var f=this.paths;if(f)this.element.attr("d",a.step(f[0],f[1],d,this.toD));else return e.apply(this, +arguments)}},adapterRun:function(a,b){if(b==="width"||b==="height")return parseInt($(a).getStyle(b),10)},getScript:function(a,b){var c=h.getElementsByTagName("head")[0],d=h.createElement("script");d.type="text/javascript";d.src=a;d.onload=b;c.appendChild(d)},animate:function(a,b,c){var d=a.attr,f=c&&c.complete;if(d&&!a.setStyle)a.getStyle=a.attr,a.setStyle=function(){var a=arguments;this.attr.call(this,a[0],a[1][0])},a.$family=function(){return!0};e.HighchartsAdapter.stop(a);c=new Fx.Morph(d?a:$(a), +g({transition:Fx.Transitions.Quad.easeInOut},c));if(d)c.element=a;if(b.d)c.toD=b.d;f&&c.addEvent("complete",f);c.start(b);a.fx=c},each:function(a,b){return i?$each(a,b):Array.each(a,b)},map:function(a,b){return a.map(b)},grep:function(a,b){return a.filter(b)},inArray:function(a,b,c){return b?b.indexOf(a,c):-1},offset:function(a){a=a.getPosition();return{left:a.x,top:a.y}},extendWithEvents:function(a){a.addEvent||(a.nodeName?$(a):g(a,new Events))},addEvent:function(a,b,c){typeof b==="string"&&(b=== +"unload"&&(b="beforeunload"),e.HighchartsAdapter.extendWithEvents(a),a.addEvent(b,c))},removeEvent:function(a,b,c){typeof a!=="string"&&a.addEvent&&(b?(b==="unload"&&(b="beforeunload"),c?a.removeEvent(b,c):a.removeEvents&&a.removeEvents(b)):a.removeEvents())},fireEvent:function(a,b,c,d){b={type:b,target:a};b=j?new Event(b):new DOMEvent(b);b=g(b,c);if(!b.target&&b.event)b.target=b.event.target;b.preventDefault=function(){d=null};a.fireEvent&&a.fireEvent(b.type,b);d&&d(b)},washMouseEvent:function(a){if(a.page)a.pageX= +a.page.x,a.pageY=a.page.y;return a},stop:function(a){a.fx&&a.fx.cancel()}}})(); diff --git a/public/static/libs/ueditor/third-party/highcharts/adapters/mootools-adapter.src.js b/public/static/libs/ueditor/third-party/highcharts/adapters/mootools-adapter.src.js new file mode 100644 index 0000000..5af6ba6 --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/adapters/mootools-adapter.src.js @@ -0,0 +1,313 @@ +/** + * @license Highcharts JS v3.0.6 (2013-10-04) + * MooTools adapter + * + * (c) 2010-2013 Torstein Hønsi + * + * License: www.highcharts.com/license + */ + +// JSLint options: +/*global Fx, $, $extend, $each, $merge, Events, Event, DOMEvent */ + +(function () { + +var win = window, + doc = document, + mooVersion = win.MooTools.version.substring(0, 3), // Get the first three characters of the version number + legacy = mooVersion === '1.2' || mooVersion === '1.1', // 1.1 && 1.2 considered legacy, 1.3 is not. + legacyEvent = legacy || mooVersion === '1.3', // In versions 1.1 - 1.3 the event class is named Event, in newer versions it is named DOMEvent. + $extend = win.$extend || function () { + return Object.append.apply(Object, arguments); + }; + +win.HighchartsAdapter = { + /** + * Initialize the adapter. This is run once as Highcharts is first run. + * @param {Object} pathAnim The helper object to do animations across adapters. + */ + init: function (pathAnim) { + var fxProto = Fx.prototype, + fxStart = fxProto.start, + morphProto = Fx.Morph.prototype, + morphCompute = morphProto.compute; + + // override Fx.start to allow animation of SVG element wrappers + /*jslint unparam: true*//* allow unused parameters in fx functions */ + fxProto.start = function (from, to) { + var fx = this, + elem = fx.element; + + // special for animating paths + if (from.d) { + //this.fromD = this.element.d.split(' '); + fx.paths = pathAnim.init( + elem, + elem.d, + fx.toD + ); + } + fxStart.apply(fx, arguments); + + return this; // chainable + }; + + // override Fx.step to allow animation of SVG element wrappers + morphProto.compute = function (from, to, delta) { + var fx = this, + paths = fx.paths; + + if (paths) { + fx.element.attr( + 'd', + pathAnim.step(paths[0], paths[1], delta, fx.toD) + ); + } else { + return morphCompute.apply(fx, arguments); + } + }; + /*jslint unparam: false*/ + }, + + /** + * Run a general method on the framework, following jQuery syntax + * @param {Object} el The HTML element + * @param {String} method Which method to run on the wrapped element + */ + adapterRun: function (el, method) { + + // This currently works for getting inner width and height. If adding + // more methods later, we need a conditional implementation for each. + if (method === 'width' || method === 'height') { + return parseInt($(el).getStyle(method), 10); + } + }, + + /** + * Downloads a script and executes a callback when done. + * @param {String} scriptLocation + * @param {Function} callback + */ + getScript: function (scriptLocation, callback) { + // We cannot assume that Assets class from mootools-more is available so instead insert a script tag to download script. + var head = doc.getElementsByTagName('head')[0]; + var script = doc.createElement('script'); + + script.type = 'text/javascript'; + script.src = scriptLocation; + script.onload = callback; + + head.appendChild(script); + }, + + /** + * Animate a HTML element or SVG element wrapper + * @param {Object} el + * @param {Object} params + * @param {Object} options jQuery-like animation options: duration, easing, callback + */ + animate: function (el, params, options) { + var isSVGElement = el.attr, + effect, + complete = options && options.complete; + + if (isSVGElement && !el.setStyle) { + // add setStyle and getStyle methods for internal use in Moo + el.getStyle = el.attr; + el.setStyle = function () { // property value is given as array in Moo - break it down + var args = arguments; + this.attr.call(this, args[0], args[1][0]); + }; + // dirty hack to trick Moo into handling el as an element wrapper + el.$family = function () { return true; }; + } + + // stop running animations + win.HighchartsAdapter.stop(el); + + // define and run the effect + effect = new Fx.Morph( + isSVGElement ? el : $(el), + $extend({ + transition: Fx.Transitions.Quad.easeInOut + }, options) + ); + + // Make sure that the element reference is set when animating svg elements + if (isSVGElement) { + effect.element = el; + } + + // special treatment for paths + if (params.d) { + effect.toD = params.d; + } + + // jQuery-like events + if (complete) { + effect.addEvent('complete', complete); + } + + // run + effect.start(params); + + // record for use in stop method + el.fx = effect; + }, + + /** + * MooTool's each function + * + */ + each: function (arr, fn) { + return legacy ? + $each(arr, fn) : + Array.each(arr, fn); + }, + + /** + * Map an array + * @param {Array} arr + * @param {Function} fn + */ + map: function (arr, fn) { + return arr.map(fn); + }, + + /** + * Grep or filter an array + * @param {Array} arr + * @param {Function} fn + */ + grep: function (arr, fn) { + return arr.filter(fn); + }, + + /** + * Return the index of an item in an array, or -1 if not matched + */ + inArray: function (item, arr, from) { + return arr ? arr.indexOf(item, from) : -1; + }, + + /** + * Get the offset of an element relative to the top left corner of the web page + */ + offset: function (el) { + var offsets = el.getPosition(); // #1496 + return { + left: offsets.x, + top: offsets.y + }; + }, + + /** + * Extends an object with Events, if its not done + */ + extendWithEvents: function (el) { + // if the addEvent method is not defined, el is a custom Highcharts object + // like series or point + if (!el.addEvent) { + if (el.nodeName) { + el = $(el); // a dynamically generated node + } else { + $extend(el, new Events()); // a custom object + } + } + }, + + /** + * Add an event listener + * @param {Object} el HTML element or custom object + * @param {String} type Event type + * @param {Function} fn Event handler + */ + addEvent: function (el, type, fn) { + if (typeof type === 'string') { // chart broke due to el being string, type function + + if (type === 'unload') { // Moo self destructs before custom unload events + type = 'beforeunload'; + } + + win.HighchartsAdapter.extendWithEvents(el); + + el.addEvent(type, fn); + } + }, + + removeEvent: function (el, type, fn) { + if (typeof el === 'string') { + // el.removeEvents below apperantly calls this method again. Do not quite understand why, so for now just bail out. + return; + } + + if (el.addEvent) { // If el doesn't have an addEvent method, there are no events to remove + if (type) { + if (type === 'unload') { // Moo self destructs before custom unload events + type = 'beforeunload'; + } + + if (fn) { + el.removeEvent(type, fn); + } else if (el.removeEvents) { // #958 + el.removeEvents(type); + } + } else { + el.removeEvents(); + } + } + }, + + fireEvent: function (el, event, eventArguments, defaultFunction) { + var eventArgs = { + type: event, + target: el + }; + // create an event object that keeps all functions + event = legacyEvent ? new Event(eventArgs) : new DOMEvent(eventArgs); + event = $extend(event, eventArguments); + + // When running an event on the Chart.prototype, MooTools nests the target in event.event + if (!event.target && event.event) { + event.target = event.event.target; + } + + // override the preventDefault function to be able to use + // this for custom events + event.preventDefault = function () { + defaultFunction = null; + }; + // if fireEvent is not available on the object, there hasn't been added + // any events to it above + if (el.fireEvent) { + el.fireEvent(event.type, event); + } + + // fire the default if it is passed and it is not prevented above + if (defaultFunction) { + defaultFunction(event); + } + }, + + /** + * Set back e.pageX and e.pageY that MooTools has abstracted away. #1165, #1346. + */ + washMouseEvent: function (e) { + if (e.page) { + e.pageX = e.page.x; + e.pageY = e.page.y; + } + return e; + }, + + /** + * Stop running animations on the object + */ + stop: function (el) { + if (el.fx) { + el.fx.cancel(); + } + } +}; + +}()); diff --git a/public/static/libs/ueditor/third-party/highcharts/adapters/prototype-adapter.js b/public/static/libs/ueditor/third-party/highcharts/adapters/prototype-adapter.js new file mode 100644 index 0000000..9011558 --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/adapters/prototype-adapter.js @@ -0,0 +1,15 @@ +/* + Highcharts JS v3.0.6 (2013-10-04) + Prototype adapter + + @author Michael Nelson, Torstein Hønsi. + + Feel free to use and modify this script. + Highcharts license: www.highcharts.com/license. +*/ +var HighchartsAdapter=function(){var f=typeof Effect!=="undefined";return{init:function(a){if(f)Effect.HighchartsTransition=Class.create(Effect.Base,{initialize:function(b,c,d,g){var e;this.element=b;this.key=c;e=b.attr?b.attr(c):$(b).getStyle(c);if(c==="d")this.paths=a.init(b,b.d,d),this.toD=d,e=0,d=1;this.start(Object.extend(g||{},{from:e,to:d,attribute:c}))},setup:function(){HighchartsAdapter._extend(this.element);if(!this.element._highchart_animation)this.element._highchart_animation={};this.element._highchart_animation[this.key]= +this},update:function(b){var c=this.paths,d=this.element;c&&(b=a.step(c[0],c[1],b,this.toD));d.attr?d.element&&d.attr(this.options.attribute,b):(c={},c[this.options.attribute]=b,$(d).setStyle(c))},finish:function(){this.element&&this.element._highchart_animation&&delete this.element._highchart_animation[this.key]}})},adapterRun:function(a,b){return parseInt($(a).getStyle(b),10)},getScript:function(a,b){var c=$$("head")[0];c&&c.appendChild((new Element("script",{type:"text/javascript",src:a})).observe("load", +b))},addNS:function(a){var b=/^(?:click|mouse(?:down|up|over|move|out))$/;return/^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/.test(a)||b.test(a)?a:"h:"+a},addEvent:function(a,b,c){a.addEventListener||a.attachEvent?Event.observe($(a),HighchartsAdapter.addNS(b),c):(HighchartsAdapter._extend(a),a._highcharts_observe(b,c))},animate:function(a,b,c){var d,c=c||{};c.delay=0;c.duration=(c.duration||500)/1E3;c.afterFinish=c.complete;if(f)for(d in b)new Effect.HighchartsTransition($(a), +d,b[d],c);else{if(a.attr)for(d in b)a.attr(d,b[d]);c.complete&&c.complete()}a.attr||$(a).setStyle(b)},stop:function(a){var b;if(a._highcharts_extended&&a._highchart_animation)for(b in a._highchart_animation)a._highchart_animation[b].cancel()},each:function(a,b){$A(a).each(b)},inArray:function(a,b,c){return b?b.indexOf(a,c):-1},offset:function(a){return $(a).cumulativeOffset()},fireEvent:function(a,b,c,d){a.fire?a.fire(HighchartsAdapter.addNS(b),c):a._highcharts_extended&&(c=c||{},a._highcharts_fire(b, +c));c&&c.defaultPrevented&&(d=null);d&&d(c)},removeEvent:function(a,b,c){$(a).stopObserving&&(b&&(b=HighchartsAdapter.addNS(b)),$(a).stopObserving(b,c));window===a?Event.stopObserving(a,b,c):(HighchartsAdapter._extend(a),a._highcharts_stop_observing(b,c))},washMouseEvent:function(a){return a},grep:function(a,b){return a.findAll(b)},map:function(a,b){return a.map(b)},_extend:function(a){a._highcharts_extended||Object.extend(a,{_highchart_events:{},_highchart_animation:null,_highcharts_extended:!0, +_highcharts_observe:function(b,a){this._highchart_events[b]=[this._highchart_events[b],a].compact().flatten()},_highcharts_stop_observing:function(b,a){b?a?this._highchart_events[b]=[this._highchart_events[b]].compact().flatten().without(a):delete this._highchart_events[b]:this._highchart_events={}},_highcharts_fire:function(a,c){var d=this;(this._highchart_events[a]||[]).each(function(a){if(!c.stopped)c.preventDefault=function(){c.defaultPrevented=!0},c.target=d,a.bind(this)(c)===!1&&c.preventDefault()}.bind(this))}})}}}(); diff --git a/public/static/libs/ueditor/third-party/highcharts/adapters/prototype-adapter.src.js b/public/static/libs/ueditor/third-party/highcharts/adapters/prototype-adapter.src.js new file mode 100644 index 0000000..f5e1d35 --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/adapters/prototype-adapter.src.js @@ -0,0 +1,316 @@ +/** + * @license Highcharts JS v3.0.6 (2013-10-04) + * Prototype adapter + * + * @author Michael Nelson, Torstein Hønsi. + * + * Feel free to use and modify this script. + * Highcharts license: www.highcharts.com/license. + */ + +// JSLint options: +/*global Effect, Class, Event, Element, $, $$, $A */ + +// Adapter interface between prototype and the Highcharts charting library +var HighchartsAdapter = (function () { + +var hasEffect = typeof Effect !== 'undefined'; + +return { + + /** + * Initialize the adapter. This is run once as Highcharts is first run. + * @param {Object} pathAnim The helper object to do animations across adapters. + */ + init: function (pathAnim) { + if (hasEffect) { + /** + * Animation for Highcharts SVG element wrappers only + * @param {Object} element + * @param {Object} attribute + * @param {Object} to + * @param {Object} options + */ + Effect.HighchartsTransition = Class.create(Effect.Base, { + initialize: function (element, attr, to, options) { + var from, + opts; + + this.element = element; + this.key = attr; + from = element.attr ? element.attr(attr) : $(element).getStyle(attr); + + // special treatment for paths + if (attr === 'd') { + this.paths = pathAnim.init( + element, + element.d, + to + ); + this.toD = to; + + + // fake values in order to read relative position as a float in update + from = 0; + to = 1; + } + + opts = Object.extend((options || {}), { + from: from, + to: to, + attribute: attr + }); + this.start(opts); + }, + setup: function () { + HighchartsAdapter._extend(this.element); + // If this is the first animation on this object, create the _highcharts_animation helper that + // contain pointers to the animation objects. + if (!this.element._highchart_animation) { + this.element._highchart_animation = {}; + } + + // Store a reference to this animation instance. + this.element._highchart_animation[this.key] = this; + }, + update: function (position) { + var paths = this.paths, + element = this.element, + obj; + + if (paths) { + position = pathAnim.step(paths[0], paths[1], position, this.toD); + } + + if (element.attr) { // SVGElement + + if (element.element) { // If not, it has been destroyed (#1405) + element.attr(this.options.attribute, position); + } + + } else { // HTML, #409 + obj = {}; + obj[this.options.attribute] = position; + $(element).setStyle(obj); + } + + }, + finish: function () { + // Delete the property that holds this animation now that it is finished. + // Both canceled animations and complete ones gets a 'finish' call. + if (this.element && this.element._highchart_animation) { // #1405 + delete this.element._highchart_animation[this.key]; + } + } + }); + } + }, + + /** + * Run a general method on the framework, following jQuery syntax + * @param {Object} el The HTML element + * @param {String} method Which method to run on the wrapped element + */ + adapterRun: function (el, method) { + + // This currently works for getting inner width and height. If adding + // more methods later, we need a conditional implementation for each. + return parseInt($(el).getStyle(method), 10); + + }, + + /** + * Downloads a script and executes a callback when done. + * @param {String} scriptLocation + * @param {Function} callback + */ + getScript: function (scriptLocation, callback) { + var head = $$('head')[0]; // Returns an array, so pick the first element. + if (head) { + // Append a new 'script' element, set its type and src attributes, add a 'load' handler that calls the callback + head.appendChild(new Element('script', { type: 'text/javascript', src: scriptLocation}).observe('load', callback)); + } + }, + + /** + * Custom events in prototype needs to be namespaced. This method adds a namespace 'h:' in front of + * events that are not recognized as native. + */ + addNS: function (eventName) { + var HTMLEvents = /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/, + MouseEvents = /^(?:click|mouse(?:down|up|over|move|out))$/; + return (HTMLEvents.test(eventName) || MouseEvents.test(eventName)) ? + eventName : + 'h:' + eventName; + }, + + // el needs an event to be attached. el is not necessarily a dom element + addEvent: function (el, event, fn) { + if (el.addEventListener || el.attachEvent) { + Event.observe($(el), HighchartsAdapter.addNS(event), fn); + + } else { + HighchartsAdapter._extend(el); + el._highcharts_observe(event, fn); + } + }, + + // motion makes things pretty. use it if effects is loaded, if not... still get to the end result. + animate: function (el, params, options) { + var key, + fx; + + // default options + options = options || {}; + options.delay = 0; + options.duration = (options.duration || 500) / 1000; + options.afterFinish = options.complete; + + // animate wrappers and DOM elements + if (hasEffect) { + for (key in params) { + // The fx variable is seemingly thrown away here, but the Effect.setup will add itself to the _highcharts_animation object + // on the element itself so its not really lost. + fx = new Effect.HighchartsTransition($(el), key, params[key], options); + } + } else { + if (el.attr) { // #409 without effects + for (key in params) { + el.attr(key, params[key]); + } + } + if (options.complete) { + options.complete(); + } + } + + if (!el.attr) { // HTML element, #409 + $(el).setStyle(params); + } + }, + + // this only occurs in higcharts 2.0+ + stop: function (el) { + var key; + if (el._highcharts_extended && el._highchart_animation) { + for (key in el._highchart_animation) { + // Cancel the animation + // The 'finish' function in the Effect object will remove the reference + el._highchart_animation[key].cancel(); + } + } + }, + + // um.. each + each: function (arr, fn) { + $A(arr).each(fn); + }, + + inArray: function (item, arr, from) { + return arr ? arr.indexOf(item, from) : -1; + }, + + /** + * Get the cumulative offset relative to the top left of the page. This method, unlike its + * jQuery and MooTools counterpart, still suffers from issue #208 regarding the position + * of a chart within a fixed container. + */ + offset: function (el) { + return $(el).cumulativeOffset(); + }, + + // fire an event based on an event name (event) and an object (el). + // again, el may not be a dom element + fireEvent: function (el, event, eventArguments, defaultFunction) { + if (el.fire) { + el.fire(HighchartsAdapter.addNS(event), eventArguments); + } else if (el._highcharts_extended) { + eventArguments = eventArguments || {}; + el._highcharts_fire(event, eventArguments); + } + + if (eventArguments && eventArguments.defaultPrevented) { + defaultFunction = null; + } + + if (defaultFunction) { + defaultFunction(eventArguments); + } + }, + + removeEvent: function (el, event, handler) { + if ($(el).stopObserving) { + if (event) { + event = HighchartsAdapter.addNS(event); + } + $(el).stopObserving(event, handler); + } if (window === el) { + Event.stopObserving(el, event, handler); + } else { + HighchartsAdapter._extend(el); + el._highcharts_stop_observing(event, handler); + } + }, + + washMouseEvent: function (e) { + return e; + }, + + // um, grep + grep: function (arr, fn) { + return arr.findAll(fn); + }, + + // um, map + map: function (arr, fn) { + return arr.map(fn); + }, + + // extend an object to handle highchart events (highchart objects, not svg elements). + // this is a very simple way of handling events but whatever, it works (i think) + _extend: function (object) { + if (!object._highcharts_extended) { + Object.extend(object, { + _highchart_events: {}, + _highchart_animation: null, + _highcharts_extended: true, + _highcharts_observe: function (name, fn) { + this._highchart_events[name] = [this._highchart_events[name], fn].compact().flatten(); + }, + _highcharts_stop_observing: function (name, fn) { + if (name) { + if (fn) { + this._highchart_events[name] = [this._highchart_events[name]].compact().flatten().without(fn); + } else { + delete this._highchart_events[name]; + } + } else { + this._highchart_events = {}; + } + }, + _highcharts_fire: function (name, args) { + var target = this; + (this._highchart_events[name] || []).each(function (fn) { + // args is never null here + if (args.stopped) { + return; // "throw $break" wasn't working. i think because of the scope of 'this'. + } + + // Attach a simple preventDefault function to skip default handler if called + args.preventDefault = function () { + args.defaultPrevented = true; + }; + args.target = target; + + // If the event handler return false, prevent the default handler from executing + if (fn.bind(this)(args) === false) { + args.preventDefault(); + } + } +.bind(this)); + } + }); + } + } +}; +}()); diff --git a/public/static/libs/ueditor/third-party/highcharts/adapters/standalone-framework.js b/public/static/libs/ueditor/third-party/highcharts/adapters/standalone-framework.js new file mode 100644 index 0000000..5f5d7f9 --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/adapters/standalone-framework.js @@ -0,0 +1,17 @@ +/* + Highcharts JS v3.0.6 (2013-10-04) + + Standalone Highcharts Framework + + License: MIT License +*/ +var HighchartsAdapter=function(){function o(c){function a(a,b,d){a.removeEventListener(b,d,!1)}function d(a,b,d){d=a.HCProxiedMethods[d.toString()];a.detachEvent("on"+b,d)}function b(b,c){var f=b.HCEvents,i,g,k,j;if(b.removeEventListener)i=a;else if(b.attachEvent)i=d;else return;c?(g={},g[c]=!0):g=f;for(j in g)if(f[j])for(k=f[j].length;k--;)i(b,j,f[j][k])}c.HCExtended||Highcharts.extend(c,{HCExtended:!0,HCEvents:{},bind:function(b,a){var d=this,c=this.HCEvents,g;if(d.addEventListener)d.addEventListener(b, +a,!1);else if(d.attachEvent){g=function(b){a.call(d,b)};if(!d.HCProxiedMethods)d.HCProxiedMethods={};d.HCProxiedMethods[a.toString()]=g;d.attachEvent("on"+b,g)}c[b]===r&&(c[b]=[]);c[b].push(a)},unbind:function(c,h){var f,i;c?(f=this.HCEvents[c]||[],h?(i=HighchartsAdapter.inArray(h,f),i>-1&&(f.splice(i,1),this.HCEvents[c]=f),this.removeEventListener?a(this,c,h):this.attachEvent&&d(this,c,h)):(b(this,c),this.HCEvents[c]=[])):(b(this),this.HCEvents={})},trigger:function(b,a){var d=this.HCEvents[b]|| +[],c=d.length,g,k,j;k=function(){a.defaultPrevented=!0};for(g=0;g=b.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();a=this.options.curAnim[this.prop]= +!0;for(e in b.curAnim)b.curAnim[e]!==!0&&(a=!1);a&&b.complete&&b.complete.call(this.elem);b=!1}else e=c-this.startTime,this.state=e/b.duration,this.pos=b.easing(e,0,1,b.duration),this.now=this.start+(this.end-this.start)*this.pos,this.update(),b=!0;return b}};this.animate=function(a,d,b){var e,h="",f,i,g;a.stopAnimation=!1;if(typeof b!=="object"||b===null)e=arguments,b={duration:e[2],easing:e[3],complete:e[4]};if(typeof b.duration!=="number")b.duration=400;b.easing=Math[b.easing]||Math.easeInOutSine; +b.curAnim=Highcharts.extend({},d);for(g in d)i=new n(a,b,g),f=null,g==="d"?(i.paths=c.init(a,a.d,d.d),i.toD=d.d,e=0,f=1):a.attr?e=a.attr(g):(e=parseFloat(HighchartsAdapter._getStyle(a,g))||0,g!=="opacity"&&(h="px")),f||(f=parseFloat(d[g])),i.custom(e,f,h)}},_getStyle:function(c,a){return window.getComputedStyle(c).getPropertyValue(a)},getScript:function(c,a){var d=l.getElementsByTagName("head")[0],b=l.createElement("script");b.type="text/javascript";b.src=c;b.onload=a;d.appendChild(b)},inArray:function(c, +a){return a.indexOf?a.indexOf(c):p.indexOf.call(a,c)},adapterRun:function(c,a){return parseInt(HighchartsAdapter._getStyle(c,a),10)},grep:function(c,a){return p.filter.call(c,a)},map:function(c,a){for(var d=[],b=0,e=c.length;b -1) { + events.splice(index, 1); + this.HCEvents[name] = events; + } + if (this.removeEventListener) { + removeOneEvent(this, name, fn); + } else if (this.attachEvent) { + IERemoveOneEvent(this, name, fn); + } + } else { + removeAllEvents(this, name); + this.HCEvents[name] = []; + } + } else { + removeAllEvents(this); + this.HCEvents = {}; + } + }, + + trigger: function (name, args) { + var events = this.HCEvents[name] || [], + target = this, + len = events.length, + i, + preventDefault, + fn; + + // Attach a simple preventDefault function to skip default handler if called + preventDefault = function () { + args.defaultPrevented = true; + }; + + for (i = 0; i < len; i++) { + fn = events[i]; + + // args is never null here + if (args.stopped) { + return; + } + + args.preventDefault = preventDefault; + args.target = target; + args.type = name; // #2297 + + // If the event handler return false, prevent the default handler from executing + if (fn.call(this, args) === false) { + args.preventDefault(); + } + } + } + }); + } + + return obj; +} + + +return { + /** + * Initialize the adapter. This is run once as Highcharts is first run. + */ + init: function (pathAnim) { + + /** + * Compatibility section to add support for legacy IE. This can be removed if old IE + * support is not needed. + */ + if (!doc.defaultView) { + this._getStyle = function (el, prop) { + var val; + if (el.style[prop]) { + return el.style[prop]; + } else { + if (prop === 'opacity') { + prop = 'filter'; + } + /*jslint unparam: true*/ + val = el.currentStyle[prop.replace(/\-(\w)/g, function (a, b) { return b.toUpperCase(); })]; + if (prop === 'filter') { + val = val.replace( + /alpha\(opacity=([0-9]+)\)/, + function (a, b) { + return b / 100; + } + ); + } + /*jslint unparam: false*/ + return val === '' ? 1 : val; + } + }; + this.adapterRun = function (elem, method) { + var alias = { width: 'clientWidth', height: 'clientHeight' }[method]; + + if (alias) { + elem.style.zoom = 1; + return elem[alias] - 2 * parseInt(HighchartsAdapter._getStyle(elem, 'padding'), 10); + } + }; + } + + if (!Array.prototype.forEach) { + this.each = function (arr, fn) { // legacy + var i = 0, + len = arr.length; + for (; i < len; i++) { + if (fn.call(arr[i], arr[i], i, arr) === false) { + return i; + } + } + }; + } + + if (!Array.prototype.indexOf) { + this.inArray = function (item, arr) { + var len, + i = 0; + + if (arr) { + len = arr.length; + + for (; i < len; i++) { + if (arr[i] === item) { + return i; + } + } + } + + return -1; + }; + } + + if (!Array.prototype.filter) { + this.grep = function (elements, callback) { + var ret = [], + i = 0, + length = elements.length; + + for (; i < length; i++) { + if (!!callback(elements[i], i)) { + ret.push(elements[i]); + } + } + + return ret; + }; + } + + //--- End compatibility section --- + + + /** + * Start of animation specific code + */ + Fx = function (elem, options, prop) { + this.options = options; + this.elem = elem; + this.prop = prop; + }; + Fx.prototype = { + + update: function () { + var styles, + paths = this.paths, + elem = this.elem, + elemelem = elem.element; // if destroyed, it is null + + // Animating a path definition on SVGElement + if (paths && elemelem) { + elem.attr('d', pathAnim.step(paths[0], paths[1], this.now, this.toD)); + + // Other animations on SVGElement + } else if (elem.attr) { + if (elemelem) { + elem.attr(this.prop, this.now); + } + + // HTML styles + } else { + styles = {}; + styles[elem] = this.now + this.unit; + Highcharts.css(elem, styles); + } + + if (this.options.step) { + this.options.step.call(this.elem, this.now, this); + } + + }, + custom: function (from, to, unit) { + var self = this, + t = function (gotoEnd) { + return self.step(gotoEnd); + }, + i; + + this.startTime = +new Date(); + this.start = from; + this.end = to; + this.unit = unit; + this.now = this.start; + this.pos = this.state = 0; + + t.elem = this.elem; + + if (t() && timers.push(t) === 1) { + timerId = setInterval(function () { + + for (i = 0; i < timers.length; i++) { + if (!timers[i]()) { + timers.splice(i--, 1); + } + } + + if (!timers.length) { + clearInterval(timerId); + } + }, 13); + } + }, + + step: function (gotoEnd) { + var t = +new Date(), + ret, + done, + options = this.options, + i; + + if (this.elem.stopAnimation) { + ret = false; + + } else if (gotoEnd || t >= options.duration + this.startTime) { + this.now = this.end; + this.pos = this.state = 1; + this.update(); + + this.options.curAnim[this.prop] = true; + + done = true; + for (i in options.curAnim) { + if (options.curAnim[i] !== true) { + done = false; + } + } + + if (done) { + if (options.complete) { + options.complete.call(this.elem); + } + } + ret = false; + + } else { + var n = t - this.startTime; + this.state = n / options.duration; + this.pos = options.easing(n, 0, 1, options.duration); + this.now = this.start + ((this.end - this.start) * this.pos); + this.update(); + ret = true; + } + return ret; + } + }; + + /** + * The adapter animate method + */ + this.animate = function (el, prop, opt) { + var start, + unit = '', + end, + fx, + args, + name; + + el.stopAnimation = false; // ready for new + + if (typeof opt !== 'object' || opt === null) { + args = arguments; + opt = { + duration: args[2], + easing: args[3], + complete: args[4] + }; + } + if (typeof opt.duration !== 'number') { + opt.duration = 400; + } + opt.easing = Math[opt.easing] || Math.easeInOutSine; + opt.curAnim = Highcharts.extend({}, prop); + + for (name in prop) { + fx = new Fx(el, opt, name); + end = null; + + if (name === 'd') { + fx.paths = pathAnim.init( + el, + el.d, + prop.d + ); + fx.toD = prop.d; + start = 0; + end = 1; + } else if (el.attr) { + start = el.attr(name); + } else { + start = parseFloat(HighchartsAdapter._getStyle(el, name)) || 0; + if (name !== 'opacity') { + unit = 'px'; + } + } + + if (!end) { + end = parseFloat(prop[name]); + } + fx.custom(start, end, unit); + } + }; + }, + + /** + * Internal method to return CSS value for given element and property + */ + _getStyle: function (el, prop) { + return window.getComputedStyle(el).getPropertyValue(prop); + }, + + /** + * Downloads a script and executes a callback when done. + * @param {String} scriptLocation + * @param {Function} callback + */ + getScript: function (scriptLocation, callback) { + // We cannot assume that Assets class from mootools-more is available so instead insert a script tag to download script. + var head = doc.getElementsByTagName('head')[0], + script = doc.createElement('script'); + + script.type = 'text/javascript'; + script.src = scriptLocation; + script.onload = callback; + + head.appendChild(script); + }, + + /** + * Return the index of an item in an array, or -1 if not found + */ + inArray: function (item, arr) { + return arr.indexOf ? arr.indexOf(item) : emptyArray.indexOf.call(arr, item); + }, + + + /** + * A direct link to adapter methods + */ + adapterRun: function (elem, method) { + return parseInt(HighchartsAdapter._getStyle(elem, method), 10); + }, + + /** + * Filter an array + */ + grep: function (elements, callback) { + return emptyArray.filter.call(elements, callback); + }, + + /** + * Map an array + */ + map: function (arr, fn) { + var results = [], i = 0, len = arr.length; + + for (; i < len; i++) { + results[i] = fn.call(arr[i], arr[i], i, arr); + } + + return results; + }, + + offset: function (el) { + var left = 0, + top = 0; + + while (el) { + left += el.offsetLeft; + top += el.offsetTop; + el = el.offsetParent; + } + + return { + left: left, + top: top + }; + }, + + /** + * Add an event listener + */ + addEvent: function (el, type, fn) { + augment(el).bind(type, fn); + }, + + /** + * Remove event added with addEvent + */ + removeEvent: function (el, type, fn) { + augment(el).unbind(type, fn); + }, + + /** + * Fire an event on a custom object + */ + fireEvent: function (el, type, eventArguments, defaultFunction) { + var e; + + if (doc.createEvent && (el.dispatchEvent || el.fireEvent)) { + e = doc.createEvent('Events'); + e.initEvent(type, true, true); + e.target = el; + + Highcharts.extend(e, eventArguments); + + if (el.dispatchEvent) { + el.dispatchEvent(e); + } else { + el.fireEvent(type, e); + } + + } else if (el.HCExtended === true) { + eventArguments = eventArguments || {}; + el.trigger(type, eventArguments); + } + + if (eventArguments && eventArguments.defaultPrevented) { + defaultFunction = null; + } + + if (defaultFunction) { + defaultFunction(eventArguments); + } + }, + + washMouseEvent: function (e) { + return e; + }, + + + /** + * Stop running animation + */ + stop: function (el) { + el.stopAnimation = true; + }, + + /** + * Utility for iterating over an array. Parameters are reversed compared to jQuery. + * @param {Array} arr + * @param {Function} fn + */ + each: function (arr, fn) { // modern browsers + return Array.prototype.forEach.call(arr, fn); + } +}; +}()); diff --git a/public/static/libs/ueditor/third-party/highcharts/highcharts-more.js b/public/static/libs/ueditor/third-party/highcharts/highcharts-more.js new file mode 100644 index 0000000..431f4fe --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/highcharts-more.js @@ -0,0 +1,50 @@ +/* + Highcharts JS v3.0.6 (2013-10-04) + + (c) 2009-2013 Torstein Hønsi + + License: www.highcharts.com/license +*/ +(function(j,C){function J(a,b,c){this.init.call(this,a,b,c)}function K(a,b,c){a.call(this,b,c);if(this.chart.polar)this.closeSegment=function(a){var c=this.xAxis.center;a.push("L",c[0],c[1])},this.closedStacks=!0}function L(a,b){var c=this.chart,d=this.options.animation,g=this.group,f=this.markerGroup,e=this.xAxis.center,i=c.plotLeft,n=c.plotTop;if(c.polar){if(c.renderer.isSVG)if(d===!0&&(d={}),b){if(c={translateX:e[0]+i,translateY:e[1]+n,scaleX:0.001,scaleY:0.001},g.attr(c),f)f.attrSetters=g.attrSetters, +f.attr(c)}else c={translateX:i,translateY:n,scaleX:1,scaleY:1},g.animate(c,d),f&&f.animate(c,d),this.animate=null}else a.call(this,b)}var P=j.arrayMin,Q=j.arrayMax,s=j.each,F=j.extend,p=j.merge,R=j.map,r=j.pick,v=j.pInt,m=j.getOptions().plotOptions,h=j.seriesTypes,x=j.extendClass,M=j.splat,o=j.wrap,N=j.Axis,u=j.Tick,z=j.Series,q=h.column.prototype,t=Math,D=t.round,A=t.floor,S=t.max,w=function(){};F(J.prototype,{init:function(a,b,c){var d=this,g=d.defaultOptions;d.chart=b;if(b.angular)g.background= +{};d.options=a=p(g,a);(a=a.background)&&s([].concat(M(a)).reverse(),function(a){var b=a.backgroundColor,a=p(d.defaultBackgroundOptions,a);if(b)a.backgroundColor=b;a.color=a.backgroundColor;c.options.plotBands.unshift(a)})},defaultOptions:{center:["50%","50%"],size:"85%",startAngle:0},defaultBackgroundOptions:{shape:"circle",borderWidth:1,borderColor:"silver",backgroundColor:{linearGradient:{x1:0,y1:0,x2:0,y2:1},stops:[[0,"#FFF"],[1,"#DDD"]]},from:Number.MIN_VALUE,innerRadius:0,to:Number.MAX_VALUE, +outerRadius:"105%"}});var G=N.prototype,u=u.prototype,T={getOffset:w,redraw:function(){this.isDirty=!1},render:function(){this.isDirty=!1},setScale:w,setCategories:w,setTitle:w},O={isRadial:!0,defaultRadialGaugeOptions:{labels:{align:"center",x:0,y:null},minorGridLineWidth:0,minorTickInterval:"auto",minorTickLength:10,minorTickPosition:"inside",minorTickWidth:1,plotBands:[],tickLength:10,tickPosition:"inside",tickWidth:2,title:{rotation:0},zIndex:2},defaultRadialXOptions:{gridLineWidth:1,labels:{align:null, +distance:15,x:0,y:null},maxPadding:0,minPadding:0,plotBands:[],showLastLabel:!1,tickLength:0},defaultRadialYOptions:{gridLineInterpolation:"circle",labels:{align:"right",x:-3,y:-2},plotBands:[],showLastLabel:!1,title:{x:4,text:null,rotation:90}},setOptions:function(a){this.options=p(this.defaultOptions,this.defaultRadialOptions,a)},getOffset:function(){G.getOffset.call(this);this.chart.axisOffset[this.side]=0},getLinePath:function(a,b){var c=this.center,b=r(b,c[2]/2-this.offset);return this.chart.renderer.symbols.arc(this.left+ +c[0],this.top+c[1],b,b,{start:this.startAngleRad,end:this.endAngleRad,open:!0,innerR:0})},setAxisTranslation:function(){G.setAxisTranslation.call(this);if(this.center&&(this.transA=this.isCircular?(this.endAngleRad-this.startAngleRad)/(this.max-this.min||1):this.center[2]/2/(this.max-this.min||1),this.isXAxis))this.minPixelPadding=this.transA*this.minPointOffset+(this.reversed?(this.endAngleRad-this.startAngleRad)/4:0)},beforeSetTickPositions:function(){this.autoConnect&&(this.max+=this.categories&& +1||this.pointRange||this.closestPointRange||0)},setAxisSize:function(){G.setAxisSize.call(this);if(this.isRadial)this.center=this.pane.center=h.pie.prototype.getCenter.call(this.pane),this.len=this.width=this.height=this.isCircular?this.center[2]*(this.endAngleRad-this.startAngleRad)/2:this.center[2]/2},getPosition:function(a,b){if(!this.isCircular)b=this.translate(a),a=this.min;return this.postTranslate(this.translate(a),r(b,this.center[2]/2)-this.offset)},postTranslate:function(a,b){var c=this.chart, +d=this.center,a=this.startAngleRad+a;return{x:c.plotLeft+d[0]+Math.cos(a)*b,y:c.plotTop+d[1]+Math.sin(a)*b}},getPlotBandPath:function(a,b,c){var d=this.center,g=this.startAngleRad,f=d[2]/2,e=[r(c.outerRadius,"100%"),c.innerRadius,r(c.thickness,10)],i=/%$/,n,l=this.isCircular;this.options.gridLineInterpolation==="polygon"?d=this.getPlotLinePath(a).concat(this.getPlotLinePath(b,!0)):(l||(e[0]=this.translate(a),e[1]=this.translate(b)),e=R(e,function(a){i.test(a)&&(a=v(a,10)*f/100);return a}),c.shape=== +"circle"||!l?(a=-Math.PI/2,b=Math.PI*1.5,n=!0):(a=g+this.translate(a),b=g+this.translate(b)),d=this.chart.renderer.symbols.arc(this.left+d[0],this.top+d[1],e[0],e[0],{start:a,end:b,innerR:r(e[1],e[0]-e[2]),open:n}));return d},getPlotLinePath:function(a,b){var c=this.center,d=this.chart,g=this.getPosition(a),f,e,i;this.isCircular?i=["M",c[0]+d.plotLeft,c[1]+d.plotTop,"L",g.x,g.y]:this.options.gridLineInterpolation==="circle"?(a=this.translate(a))&&(i=this.getLinePath(0,a)):(f=d.xAxis[0],i=[],a=this.translate(a), +c=f.tickPositions,f.autoConnect&&(c=c.concat([c[0]])),b&&(c=[].concat(c).reverse()),s(c,function(c,b){e=f.getPosition(c,a);i.push(b?"L":"M",e.x,e.y)}));return i},getTitlePosition:function(){var a=this.center,b=this.chart,c=this.options.title;return{x:b.plotLeft+a[0]+(c.x||0),y:b.plotTop+a[1]-{high:0.5,middle:0.25,low:0}[c.align]*a[2]+(c.y||0)}}};o(G,"init",function(a,b,c){var k;var d=b.angular,g=b.polar,f=c.isX,e=d&&f,i,n;n=b.options;var l=c.pane||0;if(d){if(F(this,e?T:O),i=!f)this.defaultRadialOptions= +this.defaultRadialGaugeOptions}else if(g)F(this,O),this.defaultRadialOptions=(i=f)?this.defaultRadialXOptions:p(this.defaultYAxisOptions,this.defaultRadialYOptions);a.call(this,b,c);if(!e&&(d||g)){a=this.options;if(!b.panes)b.panes=[];this.pane=(k=b.panes[l]=b.panes[l]||new J(M(n.pane)[l],b,this),l=k);l=l.options;b.inverted=!1;n.chart.zoomType=null;this.startAngleRad=b=(l.startAngle-90)*Math.PI/180;this.endAngleRad=n=(r(l.endAngle,l.startAngle+360)-90)*Math.PI/180;this.offset=a.offset||0;if((this.isCircular= +i)&&c.max===C&&n-b===2*Math.PI)this.autoConnect=!0}});o(u,"getPosition",function(a,b,c,d,g){var f=this.axis;return f.getPosition?f.getPosition(c):a.call(this,b,c,d,g)});o(u,"getLabelPosition",function(a,b,c,d,g,f,e,i,n){var l=this.axis,k=f.y,h=f.align,j=(l.translate(this.pos)+l.startAngleRad+Math.PI/2)/Math.PI*180%360;l.isRadial?(a=l.getPosition(this.pos,l.center[2]/2+r(f.distance,-25)),f.rotation==="auto"?d.attr({rotation:j}):k===null&&(k=v(d.styles.lineHeight)*0.9-d.getBBox().height/2),h===null&& +(h=l.isCircular?j>20&&j<160?"left":j>200&&j<340?"right":"center":"center",d.attr({align:h})),a.x+=f.x,a.y+=k):a=a.call(this,b,c,d,g,f,e,i,n);return a});o(u,"getMarkPath",function(a,b,c,d,g,f,e){var i=this.axis;i.isRadial?(a=i.getPosition(this.pos,i.center[2]/2+d),b=["M",b,c,"L",a.x,a.y]):b=a.call(this,b,c,d,g,f,e);return b});m.arearange=p(m.area,{lineWidth:1,marker:null,threshold:null,tooltip:{pointFormat:'{series.name}: {point.low} - {point.high}
              '}, +trackByArea:!0,dataLabels:{verticalAlign:null,xLow:0,xHigh:0,yLow:0,yHigh:0}});h.arearange=j.extendClass(h.area,{type:"arearange",pointArrayMap:["low","high"],toYData:function(a){return[a.low,a.high]},pointValKey:"low",getSegments:function(){var a=this;s(a.points,function(b){if(!a.options.connectNulls&&(b.low===null||b.high===null))b.y=null;else if(b.low===null&&b.high!==null)b.y=b.high});z.prototype.getSegments.call(this)},translate:function(){var a=this.yAxis;h.area.prototype.translate.apply(this); +s(this.points,function(b){var c=b.low,d=b.high,g=b.plotY;d===null&&c===null?b.y=null:c===null?(b.plotLow=b.plotY=null,b.plotHigh=a.translate(d,0,1,0,1)):d===null?(b.plotLow=g,b.plotHigh=null):(b.plotLow=g,b.plotHigh=a.translate(d,0,1,0,1))})},getSegmentPath:function(a){var b,c=[],d=a.length,g=z.prototype.getSegmentPath,f,e;e=this.options;var i=e.step;for(b=HighchartsAdapter.grep(a,function(a){return a.plotLow!==null});d--;)f=a[d],f.plotHigh!==null&&c.push({plotX:f.plotX,plotY:f.plotHigh});a=g.call(this, +b);if(i)i===!0&&(i="left"),e.step={left:"right",center:"center",right:"left"}[i];c=g.call(this,c);e.step=i;e=[].concat(a,c);c[0]="L";this.areaPath=this.areaPath.concat(a,c);return e},drawDataLabels:function(){var a=this.data,b=a.length,c,d=[],g=z.prototype,f=this.options.dataLabels,e,i=this.chart.inverted;if(f.enabled||this._hasPointLabels){for(c=b;c--;)e=a[c],e.y=e.high,e.plotY=e.plotHigh,d[c]=e.dataLabel,e.dataLabel=e.dataLabelUpper,e.below=!1,i?(f.align="left",f.x=f.xHigh):f.y=f.yHigh;g.drawDataLabels.apply(this, +arguments);for(c=b;c--;)e=a[c],e.dataLabelUpper=e.dataLabel,e.dataLabel=d[c],e.y=e.low,e.plotY=e.plotLow,e.below=!0,i?(f.align="right",f.x=f.xLow):f.y=f.yLow;g.drawDataLabels.apply(this,arguments)}},alignDataLabel:h.column.prototype.alignDataLabel,getSymbol:h.column.prototype.getSymbol,drawPoints:w});m.areasplinerange=p(m.arearange);h.areasplinerange=x(h.arearange,{type:"areasplinerange",getPointSpline:h.spline.prototype.getPointSpline});m.columnrange=p(m.column,m.arearange,{lineWidth:1,pointRange:null}); +h.columnrange=x(h.arearange,{type:"columnrange",translate:function(){var a=this,b=a.yAxis,c;q.translate.apply(a);s(a.points,function(d){var g=d.shapeArgs,f=a.options.minPointLength,e;d.plotHigh=c=b.translate(d.high,0,1,0,1);d.plotLow=d.plotY;e=c;d=d.plotY-c;d{series.name}
              Maximum: {point.high}
              Upper quartile: {point.q3}
              Median: {point.median}
              Lower quartile: {point.q1}
              Minimum: {point.low}
              '},whiskerLength:"50%",whiskerWidth:2});h.boxplot=x(h.column,{type:"boxplot",pointArrayMap:["low","q1","median","q3","high"], +toYData:function(a){return[a.low,a.q1,a.median,a.q3,a.high]},pointValKey:"high",pointAttrToOptions:{fill:"fillColor",stroke:"color","stroke-width":"lineWidth"},drawDataLabels:w,translate:function(){var a=this.yAxis,b=this.pointArrayMap;h.column.prototype.translate.apply(this);s(this.points,function(c){s(b,function(b){c[b]!==null&&(c[b+"Plot"]=a.translate(c[b],0,1,0,1))})})},drawPoints:function(){var a=this,b=a.points,c=a.options,d=a.chart.renderer,g,f,e,i,n,l,k,h,j,m,o,H,p,E,I,q,w,t,v,u,z,y,x=a.doQuartiles!== +!1,B=parseInt(a.options.whiskerLength,10)/100;s(b,function(b){j=b.graphic;z=b.shapeArgs;o={};E={};q={};y=b.color||a.color;if(b.plotY!==C)if(g=b.pointAttr[b.selected?"selected":""],w=z.width,t=A(z.x),v=t+w,u=D(w/2),f=A(x?b.q1Plot:b.lowPlot),e=A(x?b.q3Plot:b.lowPlot),i=A(b.highPlot),n=A(b.lowPlot),o.stroke=b.stemColor||c.stemColor||y,o["stroke-width"]=r(b.stemWidth,c.stemWidth,c.lineWidth),o.dashstyle=b.stemDashStyle||c.stemDashStyle,E.stroke=b.whiskerColor||c.whiskerColor||y,E["stroke-width"]=r(b.whiskerWidth, +c.whiskerWidth,c.lineWidth),q.stroke=b.medianColor||c.medianColor||y,q["stroke-width"]=r(b.medianWidth,c.medianWidth,c.lineWidth),k=o["stroke-width"]%2/2,h=t+u+k,m=["M",h,e,"L",h,i,"M",h,f,"L",h,n,"z"],x&&(k=g["stroke-width"]%2/2,h=A(h)+k,f=A(f)+k,e=A(e)+k,t+=k,v+=k,H=["M",t,e,"L",t,f,"L",v,f,"L",v,e,"L",t,e,"z"]),B&&(k=E["stroke-width"]%2/2,i+=k,n+=k,p=["M",h-u*B,i,"L",h+u*B,i,"M",h-u*B,n,"L",h+u*B,n]),k=q["stroke-width"]%2/2,l=D(b.medianPlot)+k,I=["M",t,l,"L",v,l,"z"],j)b.stem.animate({d:m}),B&& +b.whiskers.animate({d:p}),x&&b.box.animate({d:H}),b.medianShape.animate({d:I});else{b.graphic=j=d.g().add(a.group);b.stem=d.path(m).attr(o).add(j);if(B)b.whiskers=d.path(p).attr(E).add(j);if(x)b.box=d.path(H).attr(g).add(j);b.medianShape=d.path(I).attr(q).add(j)}})}});m.errorbar=p(m.boxplot,{color:"#000000",grouping:!1,linkedTo:":previous",tooltip:{pointFormat:m.arearange.tooltip.pointFormat},whiskerWidth:null});h.errorbar=x(h.boxplot,{type:"errorbar",pointArrayMap:["low","high"],toYData:function(a){return[a.low, +a.high]},pointValKey:"high",doQuartiles:!1,getColumnMetrics:function(){return this.linkedParent&&this.linkedParent.columnMetrics||h.column.prototype.getColumnMetrics.call(this)}});m.waterfall=p(m.column,{lineWidth:1,lineColor:"#333",dashStyle:"dot",borderColor:"#333"});h.waterfall=x(h.column,{type:"waterfall",upColorProp:"fill",pointArrayMap:["low","y"],pointValKey:"y",init:function(a,b){b.stacking=!0;h.column.prototype.init.call(this,a,b)},translate:function(){var a=this.options,b=this.yAxis,c,d, +g,f,e,i,n,l,k;c=a.threshold;a=a.borderWidth%2/2;h.column.prototype.translate.apply(this);l=c;g=this.points;for(d=0,c=g.length;d0&&!a.color)a.pointAttr=d,a.color=c})},getGraphPath:function(){var a=this.data,b=a.length,c=D(this.options.lineWidth+this.options.borderWidth)%2/2,d=[],g,f,e;for(e=1;e0?(i[f]-a)/(b-a):0.5,h.push(t.ceil(c+e*(d-c))/2);this.radii=h},animate:function(a){var b=this.options.animation;if(!a)s(this.points,function(a){var d=a.graphic,a=a.shapeArgs;d&&a&&(d.attr("r",1),d.animate({r:a.r},b))}),this.animate=null},translate:function(){var a,b=this.data,c,d,g=this.radii;h.scatter.prototype.translate.call(this);for(a=b.length;a--;)c=b[a],d=g?g[a]:0,c.negative=c.z<(this.options.zThreshold||0),d>=this.minPxSize/2?(c.shapeType= +"circle",c.shapeArgs={x:c.plotX,y:c.plotY,r:d},c.dlBox={x:c.plotX-d,y:c.plotY-d,width:2*d,height:2*d}):c.shapeArgs=c.plotY=c.dlBox=C},drawLegendSymbol:function(a,b){var c=v(a.itemStyle.fontSize)/2;b.legendSymbol=this.chart.renderer.circle(c,a.baseline-c,c).attr({zIndex:3}).add(b.legendGroup);b.legendSymbol.isMarker=!0},drawPoints:h.column.prototype.drawPoints,alignDataLabel:h.column.prototype.alignDataLabel});N.prototype.beforePadding=function(){var a=this,b=this.len,c=this.chart,d=0,g=b,f=this.isXAxis, +e=f?"xData":"yData",i=this.min,h={},j=t.min(c.plotWidth,c.plotHeight),k=Number.MAX_VALUE,m=-Number.MAX_VALUE,o=this.max-i,p=b/o,q=[];this.tickPositions&&(s(this.series,function(b){var c=b.options;if(b.type==="bubble"&&b.visible&&(a.allowZoomOutside=!0,q.push(b),f))s(["minSize","maxSize"],function(a){var b=c[a],d=/%$/.test(b),b=v(b);h[a]=d?j*b/100:b}),b.minPxSize=h.minSize,b=b.zData,b.length&&(k=t.min(k,t.max(P(b),c.displayNegative===!1?c.zThreshold:-Number.MAX_VALUE)),m=t.max(m,Q(b)))}),s(q,function(a){var b= +a[e],c=b.length,j;f&&a.getRadii(k,m,h.minSize,h.maxSize);if(o>0)for(;c--;)j=a.radii[c],d=Math.min((b[c]-i)*p-j,d),g=Math.max((b[c]-i)*p+j,g)}),q.length&&o>0&&r(this.options.min,this.userMin)===C&&r(this.options.max,this.userMax)===C&&(g-=b,p*=(b+d-g)/b,this.min+=d/p,this.max+=g/p))};var y=z.prototype,m=j.Pointer.prototype;y.toXY=function(a){var b,c=this.chart;b=a.plotX;var d=a.plotY;a.rectPlotX=b;a.rectPlotY=d;a.clientX=(b/Math.PI*180+this.xAxis.pane.options.startAngle)%360;b=this.xAxis.postTranslate(a.plotX, +this.yAxis.len-d);a.plotX=a.polarPlotX=b.x-c.plotLeft;a.plotY=a.polarPlotY=b.y-c.plotTop};y.orderTooltipPoints=function(a){if(this.chart.polar&&(a.sort(function(a,c){return a.clientX-c.clientX}),a[0]))a[0].wrappedClientX=a[0].clientX+360,a.push(a[0])};o(h.area.prototype,"init",K);o(h.areaspline.prototype,"init",K);o(h.spline.prototype,"getPointSpline",function(a,b,c,d){var g,f,e,i,h,j,k;if(this.chart.polar){g=c.plotX;f=c.plotY;a=b[d-1];e=b[d+1];this.connectEnds&&(a||(a=b[b.length-2]),e||(e=b[1])); +if(a&&e)i=a.plotX,h=a.plotY,b=e.plotX,j=e.plotY,i=(1.5*g+i)/2.5,h=(1.5*f+h)/2.5,e=(1.5*g+b)/2.5,k=(1.5*f+j)/2.5,b=Math.sqrt(Math.pow(i-g,2)+Math.pow(h-f,2)),j=Math.sqrt(Math.pow(e-g,2)+Math.pow(k-f,2)),i=Math.atan2(h-f,i-g),h=Math.atan2(k-f,e-g),k=Math.PI/2+(i+h)/2,Math.abs(i-k)>Math.PI/2&&(k-=Math.PI),i=g+Math.cos(k)*b,h=f+Math.sin(k)*b,e=g+Math.cos(Math.PI+k)*j,k=f+Math.sin(Math.PI+k)*j,c.rightContX=e,c.rightContY=k;d?(c=["C",a.rightContX||a.plotX,a.rightContY||a.plotY,i||g,h||f,g,f],a.rightContX= +a.rightContY=null):c=["M",g,f]}else c=a.call(this,b,c,d);return c});o(y,"translate",function(a){a.call(this);if(this.chart.polar&&!this.preventPostTranslate)for(var a=this.points,b=a.length;b--;)this.toXY(a[b])});o(y,"getSegmentPath",function(a,b){var c=this.points;if(this.chart.polar&&this.options.connectEnds!==!1&&b[b.length-1]===c[c.length-1]&&c[0].y!==null)this.connectEnds=!0,b=[].concat(b,[c[0]]);return a.call(this,b)});o(y,"animate",L);o(q,"animate",L);o(y,"setTooltipPoints",function(a,b){this.chart.polar&& +F(this.xAxis,{tooltipLen:360});return a.call(this,b)});o(q,"translate",function(a){var b=this.xAxis,c=this.yAxis.len,d=b.center,g=b.startAngleRad,f=this.chart.renderer,e,h;this.preventPostTranslate=!0;a.call(this);if(b.isRadial){b=this.points;for(h=b.length;h--;)e=b[h],a=e.barX+g,e.shapeType="path",e.shapeArgs={d:f.symbols.arc(d[0],d[1],c-e.plotY,null,{start:a,end:a+e.pointWidth,innerR:c-r(e.yBottom,c)})},this.toXY(e)}});o(q,"alignDataLabel",function(a,b,c,d,g,f){if(this.chart.polar){a=b.rectPlotX/ +Math.PI*180;if(d.align===null)d.align=a>20&&a<160?"left":a>200&&a<340?"right":"center";if(d.verticalAlign===null)d.verticalAlign=a<45||a>315?"bottom":a>135&&a<225?"top":"middle";y.alignDataLabel.call(this,b,c,d,g,f)}else a.call(this,b,c,d,g,f)});o(m,"getIndex",function(a,b){var c,d=this.chart,g;d.polar?(g=d.xAxis[0].center,c=b.chartX-g[0]-d.plotLeft,d=b.chartY-g[1]-d.plotTop,c=180-Math.round(Math.atan2(c,d)/Math.PI*180)):c=a.call(this,b);return c});o(m,"getCoordinates",function(a,b){var c=this.chart, +d={xAxis:[],yAxis:[]};c.polar?s(c.axes,function(a){var f=a.isXAxis,e=a.center,h=b.chartX-e[0]-c.plotLeft,e=b.chartY-e[1]-c.plotTop;d[f?"xAxis":"yAxis"].push({axis:a,value:a.translate(f?Math.PI-Math.atan2(h,e):Math.sqrt(Math.pow(h,2)+Math.pow(e,2)),!0)})}):d=a.call(this,b);return d})})(Highcharts); diff --git a/public/static/libs/ueditor/third-party/highcharts/highcharts-more.src.js b/public/static/libs/ueditor/third-party/highcharts/highcharts-more.src.js new file mode 100644 index 0000000..c8c5160 --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/highcharts-more.src.js @@ -0,0 +1,2430 @@ +// ==ClosureCompiler== +// @compilation_level SIMPLE_OPTIMIZATIONS + +/** + * @license Highcharts JS v3.0.6 (2013-10-04) + * + * (c) 2009-2013 Torstein Hønsi + * + * License: www.highcharts.com/license + */ + +// JSLint options: +/*global Highcharts, HighchartsAdapter, document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $, console */ + +(function (Highcharts, UNDEFINED) { +var arrayMin = Highcharts.arrayMin, + arrayMax = Highcharts.arrayMax, + each = Highcharts.each, + extend = Highcharts.extend, + merge = Highcharts.merge, + map = Highcharts.map, + pick = Highcharts.pick, + pInt = Highcharts.pInt, + defaultPlotOptions = Highcharts.getOptions().plotOptions, + seriesTypes = Highcharts.seriesTypes, + extendClass = Highcharts.extendClass, + splat = Highcharts.splat, + wrap = Highcharts.wrap, + Axis = Highcharts.Axis, + Tick = Highcharts.Tick, + Series = Highcharts.Series, + colProto = seriesTypes.column.prototype, + math = Math, + mathRound = math.round, + mathFloor = math.floor, + mathMax = math.max, + noop = function () {};/** + * The Pane object allows options that are common to a set of X and Y axes. + * + * In the future, this can be extended to basic Highcharts and Highstock. + */ +function Pane(options, chart, firstAxis) { + this.init.call(this, options, chart, firstAxis); +} + +// Extend the Pane prototype +extend(Pane.prototype, { + + /** + * Initiate the Pane object + */ + init: function (options, chart, firstAxis) { + var pane = this, + backgroundOption, + defaultOptions = pane.defaultOptions; + + pane.chart = chart; + + // Set options + if (chart.angular) { // gauges + defaultOptions.background = {}; // gets extended by this.defaultBackgroundOptions + } + pane.options = options = merge(defaultOptions, options); + + backgroundOption = options.background; + + // To avoid having weighty logic to place, update and remove the backgrounds, + // push them to the first axis' plot bands and borrow the existing logic there. + if (backgroundOption) { + each([].concat(splat(backgroundOption)).reverse(), function (config) { + var backgroundColor = config.backgroundColor; // if defined, replace the old one (specific for gradients) + config = merge(pane.defaultBackgroundOptions, config); + if (backgroundColor) { + config.backgroundColor = backgroundColor; + } + config.color = config.backgroundColor; // due to naming in plotBands + firstAxis.options.plotBands.unshift(config); + }); + } + }, + + /** + * The default options object + */ + defaultOptions: { + // background: {conditional}, + center: ['50%', '50%'], + size: '85%', + startAngle: 0 + //endAngle: startAngle + 360 + }, + + /** + * The default background options + */ + defaultBackgroundOptions: { + shape: 'circle', + borderWidth: 1, + borderColor: 'silver', + backgroundColor: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0, '#FFF'], + [1, '#DDD'] + ] + }, + from: Number.MIN_VALUE, // corrected to axis min + innerRadius: 0, + to: Number.MAX_VALUE, // corrected to axis max + outerRadius: '105%' + } + +}); +var axisProto = Axis.prototype, + tickProto = Tick.prototype; + +/** + * Augmented methods for the x axis in order to hide it completely, used for the X axis in gauges + */ +var hiddenAxisMixin = { + getOffset: noop, + redraw: function () { + this.isDirty = false; // prevent setting Y axis dirty + }, + render: function () { + this.isDirty = false; // prevent setting Y axis dirty + }, + setScale: noop, + setCategories: noop, + setTitle: noop +}; + +/** + * Augmented methods for the value axis + */ +/*jslint unparam: true*/ +var radialAxisMixin = { + isRadial: true, + + /** + * The default options extend defaultYAxisOptions + */ + defaultRadialGaugeOptions: { + labels: { + align: 'center', + x: 0, + y: null // auto + }, + minorGridLineWidth: 0, + minorTickInterval: 'auto', + minorTickLength: 10, + minorTickPosition: 'inside', + minorTickWidth: 1, + plotBands: [], + tickLength: 10, + tickPosition: 'inside', + tickWidth: 2, + title: { + rotation: 0 + }, + zIndex: 2 // behind dials, points in the series group + }, + + // Circular axis around the perimeter of a polar chart + defaultRadialXOptions: { + gridLineWidth: 1, // spokes + labels: { + align: null, // auto + distance: 15, + x: 0, + y: null // auto + }, + maxPadding: 0, + minPadding: 0, + plotBands: [], + showLastLabel: false, + tickLength: 0 + }, + + // Radial axis, like a spoke in a polar chart + defaultRadialYOptions: { + gridLineInterpolation: 'circle', + labels: { + align: 'right', + x: -3, + y: -2 + }, + plotBands: [], + showLastLabel: false, + title: { + x: 4, + text: null, + rotation: 90 + } + }, + + /** + * Merge and set options + */ + setOptions: function (userOptions) { + + this.options = merge( + this.defaultOptions, + this.defaultRadialOptions, + userOptions + ); + + }, + + /** + * Wrap the getOffset method to return zero offset for title or labels in a radial + * axis + */ + getOffset: function () { + // Call the Axis prototype method (the method we're in now is on the instance) + axisProto.getOffset.call(this); + + // Title or label offsets are not counted + this.chart.axisOffset[this.side] = 0; + }, + + + /** + * Get the path for the axis line. This method is also referenced in the getPlotLinePath + * method. + */ + getLinePath: function (lineWidth, radius) { + var center = this.center; + radius = pick(radius, center[2] / 2 - this.offset); + + return this.chart.renderer.symbols.arc( + this.left + center[0], + this.top + center[1], + radius, + radius, + { + start: this.startAngleRad, + end: this.endAngleRad, + open: true, + innerR: 0 + } + ); + }, + + /** + * Override setAxisTranslation by setting the translation to the difference + * in rotation. This allows the translate method to return angle for + * any given value. + */ + setAxisTranslation: function () { + + // Call uber method + axisProto.setAxisTranslation.call(this); + + // Set transA and minPixelPadding + if (this.center) { // it's not defined the first time + if (this.isCircular) { + + this.transA = (this.endAngleRad - this.startAngleRad) / + ((this.max - this.min) || 1); + + + } else { + this.transA = (this.center[2] / 2) / ((this.max - this.min) || 1); + } + + if (this.isXAxis) { + this.minPixelPadding = this.transA * this.minPointOffset + + (this.reversed ? (this.endAngleRad - this.startAngleRad) / 4 : 0); // ??? + } + } + }, + + /** + * In case of auto connect, add one closestPointRange to the max value right before + * tickPositions are computed, so that ticks will extend passed the real max. + */ + beforeSetTickPositions: function () { + if (this.autoConnect) { + this.max += (this.categories && 1) || this.pointRange || this.closestPointRange || 0; // #1197, #2260 + } + }, + + /** + * Override the setAxisSize method to use the arc's circumference as length. This + * allows tickPixelInterval to apply to pixel lengths along the perimeter + */ + setAxisSize: function () { + + axisProto.setAxisSize.call(this); + + if (this.isRadial) { + + // Set the center array + this.center = this.pane.center = seriesTypes.pie.prototype.getCenter.call(this.pane); + + this.len = this.width = this.height = this.isCircular ? + this.center[2] * (this.endAngleRad - this.startAngleRad) / 2 : + this.center[2] / 2; + } + }, + + /** + * Returns the x, y coordinate of a point given by a value and a pixel distance + * from center + */ + getPosition: function (value, length) { + if (!this.isCircular) { + length = this.translate(value); + value = this.min; + } + + return this.postTranslate( + this.translate(value), + pick(length, this.center[2] / 2) - this.offset + ); + }, + + /** + * Translate from intermediate plotX (angle), plotY (axis.len - radius) to final chart coordinates. + */ + postTranslate: function (angle, radius) { + + var chart = this.chart, + center = this.center; + + angle = this.startAngleRad + angle; + + return { + x: chart.plotLeft + center[0] + Math.cos(angle) * radius, + y: chart.plotTop + center[1] + Math.sin(angle) * radius + }; + + }, + + /** + * Find the path for plot bands along the radial axis + */ + getPlotBandPath: function (from, to, options) { + var center = this.center, + startAngleRad = this.startAngleRad, + fullRadius = center[2] / 2, + radii = [ + pick(options.outerRadius, '100%'), + options.innerRadius, + pick(options.thickness, 10) + ], + percentRegex = /%$/, + start, + end, + open, + isCircular = this.isCircular, // X axis in a polar chart + ret; + + // Polygonal plot bands + if (this.options.gridLineInterpolation === 'polygon') { + ret = this.getPlotLinePath(from).concat(this.getPlotLinePath(to, true)); + + // Circular grid bands + } else { + + // Plot bands on Y axis (radial axis) - inner and outer radius depend on to and from + if (!isCircular) { + radii[0] = this.translate(from); + radii[1] = this.translate(to); + } + + // Convert percentages to pixel values + radii = map(radii, function (radius) { + if (percentRegex.test(radius)) { + radius = (pInt(radius, 10) * fullRadius) / 100; + } + return radius; + }); + + // Handle full circle + if (options.shape === 'circle' || !isCircular) { + start = -Math.PI / 2; + end = Math.PI * 1.5; + open = true; + } else { + start = startAngleRad + this.translate(from); + end = startAngleRad + this.translate(to); + } + + + ret = this.chart.renderer.symbols.arc( + this.left + center[0], + this.top + center[1], + radii[0], + radii[0], + { + start: start, + end: end, + innerR: pick(radii[1], radii[0] - radii[2]), + open: open + } + ); + } + + return ret; + }, + + /** + * Find the path for plot lines perpendicular to the radial axis. + */ + getPlotLinePath: function (value, reverse) { + var axis = this, + center = axis.center, + chart = axis.chart, + end = axis.getPosition(value), + xAxis, + xy, + tickPositions, + ret; + + // Spokes + if (axis.isCircular) { + ret = ['M', center[0] + chart.plotLeft, center[1] + chart.plotTop, 'L', end.x, end.y]; + + // Concentric circles + } else if (axis.options.gridLineInterpolation === 'circle') { + value = axis.translate(value); + if (value) { // a value of 0 is in the center + ret = axis.getLinePath(0, value); + } + // Concentric polygons + } else { + xAxis = chart.xAxis[0]; + ret = []; + value = axis.translate(value); + tickPositions = xAxis.tickPositions; + if (xAxis.autoConnect) { + tickPositions = tickPositions.concat([tickPositions[0]]); + } + // Reverse the positions for concatenation of polygonal plot bands + if (reverse) { + tickPositions = [].concat(tickPositions).reverse(); + } + + each(tickPositions, function (pos, i) { + xy = xAxis.getPosition(pos, value); + ret.push(i ? 'L' : 'M', xy.x, xy.y); + }); + + } + return ret; + }, + + /** + * Find the position for the axis title, by default inside the gauge + */ + getTitlePosition: function () { + var center = this.center, + chart = this.chart, + titleOptions = this.options.title; + + return { + x: chart.plotLeft + center[0] + (titleOptions.x || 0), + y: chart.plotTop + center[1] - ({ high: 0.5, middle: 0.25, low: 0 }[titleOptions.align] * + center[2]) + (titleOptions.y || 0) + }; + } + +}; +/*jslint unparam: false*/ + +/** + * Override axisProto.init to mix in special axis instance functions and function overrides + */ +wrap(axisProto, 'init', function (proceed, chart, userOptions) { + var axis = this, + angular = chart.angular, + polar = chart.polar, + isX = userOptions.isX, + isHidden = angular && isX, + isCircular, + startAngleRad, + endAngleRad, + options, + chartOptions = chart.options, + paneIndex = userOptions.pane || 0, + pane, + paneOptions; + + // Before prototype.init + if (angular) { + extend(this, isHidden ? hiddenAxisMixin : radialAxisMixin); + isCircular = !isX; + if (isCircular) { + this.defaultRadialOptions = this.defaultRadialGaugeOptions; + } + + } else if (polar) { + //extend(this, userOptions.isX ? radialAxisMixin : radialAxisMixin); + extend(this, radialAxisMixin); + isCircular = isX; + this.defaultRadialOptions = isX ? this.defaultRadialXOptions : merge(this.defaultYAxisOptions, this.defaultRadialYOptions); + + } + + // Run prototype.init + proceed.call(this, chart, userOptions); + + if (!isHidden && (angular || polar)) { + options = this.options; + + // Create the pane and set the pane options. + if (!chart.panes) { + chart.panes = []; + } + this.pane = pane = chart.panes[paneIndex] = chart.panes[paneIndex] || new Pane( + splat(chartOptions.pane)[paneIndex], + chart, + axis + ); + paneOptions = pane.options; + + + // Disable certain features on angular and polar axes + chart.inverted = false; + chartOptions.chart.zoomType = null; + + // Start and end angle options are + // given in degrees relative to top, while internal computations are + // in radians relative to right (like SVG). + this.startAngleRad = startAngleRad = (paneOptions.startAngle - 90) * Math.PI / 180; + this.endAngleRad = endAngleRad = (pick(paneOptions.endAngle, paneOptions.startAngle + 360) - 90) * Math.PI / 180; + this.offset = options.offset || 0; + + this.isCircular = isCircular; + + // Automatically connect grid lines? + if (isCircular && userOptions.max === UNDEFINED && endAngleRad - startAngleRad === 2 * Math.PI) { + this.autoConnect = true; + } + } + +}); + +/** + * Add special cases within the Tick class' methods for radial axes. + */ +wrap(tickProto, 'getPosition', function (proceed, horiz, pos, tickmarkOffset, old) { + var axis = this.axis; + + return axis.getPosition ? + axis.getPosition(pos) : + proceed.call(this, horiz, pos, tickmarkOffset, old); +}); + +/** + * Wrap the getLabelPosition function to find the center position of the label + * based on the distance option + */ +wrap(tickProto, 'getLabelPosition', function (proceed, x, y, label, horiz, labelOptions, tickmarkOffset, index, step) { + var axis = this.axis, + optionsY = labelOptions.y, + ret, + align = labelOptions.align, + angle = ((axis.translate(this.pos) + axis.startAngleRad + Math.PI / 2) / Math.PI * 180) % 360; + + if (axis.isRadial) { + ret = axis.getPosition(this.pos, (axis.center[2] / 2) + pick(labelOptions.distance, -25)); + + // Automatically rotated + if (labelOptions.rotation === 'auto') { + label.attr({ + rotation: angle + }); + + // Vertically centered + } else if (optionsY === null) { + optionsY = pInt(label.styles.lineHeight) * 0.9 - label.getBBox().height / 2; + + } + + // Automatic alignment + if (align === null) { + if (axis.isCircular) { + if (angle > 20 && angle < 160) { + align = 'left'; // right hemisphere + } else if (angle > 200 && angle < 340) { + align = 'right'; // left hemisphere + } else { + align = 'center'; // top or bottom + } + } else { + align = 'center'; + } + label.attr({ + align: align + }); + } + + ret.x += labelOptions.x; + ret.y += optionsY; + + } else { + ret = proceed.call(this, x, y, label, horiz, labelOptions, tickmarkOffset, index, step); + } + return ret; +}); + +/** + * Wrap the getMarkPath function to return the path of the radial marker + */ +wrap(tickProto, 'getMarkPath', function (proceed, x, y, tickLength, tickWidth, horiz, renderer) { + var axis = this.axis, + endPoint, + ret; + + if (axis.isRadial) { + endPoint = axis.getPosition(this.pos, axis.center[2] / 2 + tickLength); + ret = [ + 'M', + x, + y, + 'L', + endPoint.x, + endPoint.y + ]; + } else { + ret = proceed.call(this, x, y, tickLength, tickWidth, horiz, renderer); + } + return ret; +});/* + * The AreaRangeSeries class + * + */ + +/** + * Extend the default options with map options + */ +defaultPlotOptions.arearange = merge(defaultPlotOptions.area, { + lineWidth: 1, + marker: null, + threshold: null, + tooltip: { + pointFormat: '{series.name}: {point.low} - {point.high}
              ' + }, + trackByArea: true, + dataLabels: { + verticalAlign: null, + xLow: 0, + xHigh: 0, + yLow: 0, + yHigh: 0 + } +}); + +/** + * Add the series type + */ +seriesTypes.arearange = Highcharts.extendClass(seriesTypes.area, { + type: 'arearange', + pointArrayMap: ['low', 'high'], + toYData: function (point) { + return [point.low, point.high]; + }, + pointValKey: 'low', + + /** + * Extend getSegments to force null points if the higher value is null. #1703. + */ + getSegments: function () { + var series = this; + + each(series.points, function (point) { + if (!series.options.connectNulls && (point.low === null || point.high === null)) { + point.y = null; + } else if (point.low === null && point.high !== null) { + point.y = point.high; + } + }); + Series.prototype.getSegments.call(this); + }, + + /** + * Translate data points from raw values x and y to plotX and plotY + */ + translate: function () { + var series = this, + yAxis = series.yAxis; + + seriesTypes.area.prototype.translate.apply(series); + + // Set plotLow and plotHigh + each(series.points, function (point) { + + var low = point.low, + high = point.high, + plotY = point.plotY; + + if (high === null && low === null) { + point.y = null; + } else if (low === null) { + point.plotLow = point.plotY = null; + point.plotHigh = yAxis.translate(high, 0, 1, 0, 1); + } else if (high === null) { + point.plotLow = plotY; + point.plotHigh = null; + } else { + point.plotLow = plotY; + point.plotHigh = yAxis.translate(high, 0, 1, 0, 1); + } + }); + }, + + /** + * Extend the line series' getSegmentPath method by applying the segment + * path to both lower and higher values of the range + */ + getSegmentPath: function (segment) { + + var lowSegment, + highSegment = [], + i = segment.length, + baseGetSegmentPath = Series.prototype.getSegmentPath, + point, + linePath, + lowerPath, + options = this.options, + step = options.step, + higherPath; + + // Remove nulls from low segment + lowSegment = HighchartsAdapter.grep(segment, function (point) { + return point.plotLow !== null; + }); + + // Make a segment with plotX and plotY for the top values + while (i--) { + point = segment[i]; + if (point.plotHigh !== null) { + highSegment.push({ + plotX: point.plotX, + plotY: point.plotHigh + }); + } + } + + // Get the paths + lowerPath = baseGetSegmentPath.call(this, lowSegment); + if (step) { + if (step === true) { + step = 'left'; + } + options.step = { left: 'right', center: 'center', right: 'left' }[step]; // swap for reading in getSegmentPath + } + higherPath = baseGetSegmentPath.call(this, highSegment); + options.step = step; + + // Create a line on both top and bottom of the range + linePath = [].concat(lowerPath, higherPath); + + // For the area path, we need to change the 'move' statement into 'lineTo' or 'curveTo' + higherPath[0] = 'L'; // this probably doesn't work for spline + this.areaPath = this.areaPath.concat(lowerPath, higherPath); + + return linePath; + }, + + /** + * Extend the basic drawDataLabels method by running it for both lower and higher + * values. + */ + drawDataLabels: function () { + + var data = this.data, + length = data.length, + i, + originalDataLabels = [], + seriesProto = Series.prototype, + dataLabelOptions = this.options.dataLabels, + point, + inverted = this.chart.inverted; + + if (dataLabelOptions.enabled || this._hasPointLabels) { + + // Step 1: set preliminary values for plotY and dataLabel and draw the upper labels + i = length; + while (i--) { + point = data[i]; + + // Set preliminary values + point.y = point.high; + point.plotY = point.plotHigh; + + // Store original data labels and set preliminary label objects to be picked up + // in the uber method + originalDataLabels[i] = point.dataLabel; + point.dataLabel = point.dataLabelUpper; + + // Set the default offset + point.below = false; + if (inverted) { + dataLabelOptions.align = 'left'; + dataLabelOptions.x = dataLabelOptions.xHigh; + } else { + dataLabelOptions.y = dataLabelOptions.yHigh; + } + } + seriesProto.drawDataLabels.apply(this, arguments); // #1209 + + // Step 2: reorganize and handle data labels for the lower values + i = length; + while (i--) { + point = data[i]; + + // Move the generated labels from step 1, and reassign the original data labels + point.dataLabelUpper = point.dataLabel; + point.dataLabel = originalDataLabels[i]; + + // Reset values + point.y = point.low; + point.plotY = point.plotLow; + + // Set the default offset + point.below = true; + if (inverted) { + dataLabelOptions.align = 'right'; + dataLabelOptions.x = dataLabelOptions.xLow; + } else { + dataLabelOptions.y = dataLabelOptions.yLow; + } + } + seriesProto.drawDataLabels.apply(this, arguments); + } + + }, + + alignDataLabel: seriesTypes.column.prototype.alignDataLabel, + + getSymbol: seriesTypes.column.prototype.getSymbol, + + drawPoints: noop +});/** + * The AreaSplineRangeSeries class + */ + +defaultPlotOptions.areasplinerange = merge(defaultPlotOptions.arearange); + +/** + * AreaSplineRangeSeries object + */ +seriesTypes.areasplinerange = extendClass(seriesTypes.arearange, { + type: 'areasplinerange', + getPointSpline: seriesTypes.spline.prototype.getPointSpline +});/** + * The ColumnRangeSeries class + */ +defaultPlotOptions.columnrange = merge(defaultPlotOptions.column, defaultPlotOptions.arearange, { + lineWidth: 1, + pointRange: null +}); + +/** + * ColumnRangeSeries object + */ +seriesTypes.columnrange = extendClass(seriesTypes.arearange, { + type: 'columnrange', + /** + * Translate data points from raw values x and y to plotX and plotY + */ + translate: function () { + var series = this, + yAxis = series.yAxis, + plotHigh; + + colProto.translate.apply(series); + + // Set plotLow and plotHigh + each(series.points, function (point) { + var shapeArgs = point.shapeArgs, + minPointLength = series.options.minPointLength, + heightDifference, + height, + y; + + point.plotHigh = plotHigh = yAxis.translate(point.high, 0, 1, 0, 1); + point.plotLow = point.plotY; + + // adjust shape + y = plotHigh; + height = point.plotY - plotHigh; + + if (height < minPointLength) { + heightDifference = (minPointLength - height); + height += heightDifference; + y -= heightDifference / 2; + } + shapeArgs.height = height; + shapeArgs.y = y; + }); + }, + trackerGroups: ['group', 'dataLabels'], + drawGraph: noop, + pointAttrToOptions: colProto.pointAttrToOptions, + drawPoints: colProto.drawPoints, + drawTracker: colProto.drawTracker, + animate: colProto.animate, + getColumnMetrics: colProto.getColumnMetrics +}); +/* + * The GaugeSeries class + */ + + + +/** + * Extend the default options + */ +defaultPlotOptions.gauge = merge(defaultPlotOptions.line, { + dataLabels: { + enabled: true, + y: 15, + borderWidth: 1, + borderColor: 'silver', + borderRadius: 3, + style: { + fontWeight: 'bold' + }, + verticalAlign: 'top', + zIndex: 2 + }, + dial: { + // radius: '80%', + // backgroundColor: 'black', + // borderColor: 'silver', + // borderWidth: 0, + // baseWidth: 3, + // topWidth: 1, + // baseLength: '70%' // of radius + // rearLength: '10%' + }, + pivot: { + //radius: 5, + //borderWidth: 0 + //borderColor: 'silver', + //backgroundColor: 'black' + }, + tooltip: { + headerFormat: '' + }, + showInLegend: false +}); + +/** + * Extend the point object + */ +var GaugePoint = Highcharts.extendClass(Highcharts.Point, { + /** + * Don't do any hover colors or anything + */ + setState: function (state) { + this.state = state; + } +}); + + +/** + * Add the series type + */ +var GaugeSeries = { + type: 'gauge', + pointClass: GaugePoint, + + // chart.angular will be set to true when a gauge series is present, and this will + // be used on the axes + angular: true, + drawGraph: noop, + fixedBox: true, + trackerGroups: ['group', 'dataLabels'], + + /** + * Calculate paths etc + */ + translate: function () { + + var series = this, + yAxis = series.yAxis, + options = series.options, + center = yAxis.center; + + series.generatePoints(); + + each(series.points, function (point) { + + var dialOptions = merge(options.dial, point.dial), + radius = (pInt(pick(dialOptions.radius, 80)) * center[2]) / 200, + baseLength = (pInt(pick(dialOptions.baseLength, 70)) * radius) / 100, + rearLength = (pInt(pick(dialOptions.rearLength, 10)) * radius) / 100, + baseWidth = dialOptions.baseWidth || 3, + topWidth = dialOptions.topWidth || 1, + rotation = yAxis.startAngleRad + yAxis.translate(point.y, null, null, null, true); + + // Handle the wrap option + if (options.wrap === false) { + rotation = Math.max(yAxis.startAngleRad, Math.min(yAxis.endAngleRad, rotation)); + } + rotation = rotation * 180 / Math.PI; + + point.shapeType = 'path'; + point.shapeArgs = { + d: dialOptions.path || [ + 'M', + -rearLength, -baseWidth / 2, + 'L', + baseLength, -baseWidth / 2, + radius, -topWidth / 2, + radius, topWidth / 2, + baseLength, baseWidth / 2, + -rearLength, baseWidth / 2, + 'z' + ], + translateX: center[0], + translateY: center[1], + rotation: rotation + }; + + // Positions for data label + point.plotX = center[0]; + point.plotY = center[1]; + }); + }, + + /** + * Draw the points where each point is one needle + */ + drawPoints: function () { + + var series = this, + center = series.yAxis.center, + pivot = series.pivot, + options = series.options, + pivotOptions = options.pivot, + renderer = series.chart.renderer; + + each(series.points, function (point) { + + var graphic = point.graphic, + shapeArgs = point.shapeArgs, + d = shapeArgs.d, + dialOptions = merge(options.dial, point.dial); // #1233 + + if (graphic) { + graphic.animate(shapeArgs); + shapeArgs.d = d; // animate alters it + } else { + point.graphic = renderer[point.shapeType](shapeArgs) + .attr({ + stroke: dialOptions.borderColor || 'none', + 'stroke-width': dialOptions.borderWidth || 0, + fill: dialOptions.backgroundColor || 'black', + rotation: shapeArgs.rotation // required by VML when animation is false + }) + .add(series.group); + } + }); + + // Add or move the pivot + if (pivot) { + pivot.animate({ // #1235 + translateX: center[0], + translateY: center[1] + }); + } else { + series.pivot = renderer.circle(0, 0, pick(pivotOptions.radius, 5)) + .attr({ + 'stroke-width': pivotOptions.borderWidth || 0, + stroke: pivotOptions.borderColor || 'silver', + fill: pivotOptions.backgroundColor || 'black' + }) + .translate(center[0], center[1]) + .add(series.group); + } + }, + + /** + * Animate the arrow up from startAngle + */ + animate: function (init) { + var series = this; + + if (!init) { + each(series.points, function (point) { + var graphic = point.graphic; + + if (graphic) { + // start value + graphic.attr({ + rotation: series.yAxis.startAngleRad * 180 / Math.PI + }); + + // animate + graphic.animate({ + rotation: point.shapeArgs.rotation + }, series.options.animation); + } + }); + + // delete this function to allow it only once + series.animate = null; + } + }, + + render: function () { + this.group = this.plotGroup( + 'group', + 'series', + this.visible ? 'visible' : 'hidden', + this.options.zIndex, + this.chart.seriesGroup + ); + seriesTypes.pie.prototype.render.call(this); + this.group.clip(this.chart.clipRect); + }, + + setData: seriesTypes.pie.prototype.setData, + drawTracker: seriesTypes.column.prototype.drawTracker +}; +seriesTypes.gauge = Highcharts.extendClass(seriesTypes.line, GaugeSeries);/* **************************************************************************** + * Start Box plot series code * + *****************************************************************************/ + +// Set default options +defaultPlotOptions.boxplot = merge(defaultPlotOptions.column, { + fillColor: '#FFFFFF', + lineWidth: 1, + //medianColor: null, + medianWidth: 2, + states: { + hover: { + brightness: -0.3 + } + }, + //stemColor: null, + //stemDashStyle: 'solid' + //stemWidth: null, + threshold: null, + tooltip: { + pointFormat: '{series.name}
              ' + + 'Maximum: {point.high}
              ' + + 'Upper quartile: {point.q3}
              ' + + 'Median: {point.median}
              ' + + 'Lower quartile: {point.q1}
              ' + + 'Minimum: {point.low}
              ' + + }, + //whiskerColor: null, + whiskerLength: '50%', + whiskerWidth: 2 +}); + +// Create the series object +seriesTypes.boxplot = extendClass(seriesTypes.column, { + type: 'boxplot', + pointArrayMap: ['low', 'q1', 'median', 'q3', 'high'], // array point configs are mapped to this + toYData: function (point) { // return a plain array for speedy calculation + return [point.low, point.q1, point.median, point.q3, point.high]; + }, + pointValKey: 'high', // defines the top of the tracker + + /** + * One-to-one mapping from options to SVG attributes + */ + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + fill: 'fillColor', + stroke: 'color', + 'stroke-width': 'lineWidth' + }, + + /** + * Disable data labels for box plot + */ + drawDataLabels: noop, + + /** + * Translate data points from raw values x and y to plotX and plotY + */ + translate: function () { + var series = this, + yAxis = series.yAxis, + pointArrayMap = series.pointArrayMap; + + seriesTypes.column.prototype.translate.apply(series); + + // do the translation on each point dimension + each(series.points, function (point) { + each(pointArrayMap, function (key) { + if (point[key] !== null) { + point[key + 'Plot'] = yAxis.translate(point[key], 0, 1, 0, 1); + } + }); + }); + }, + + /** + * Draw the data points + */ + drawPoints: function () { + var series = this, //state = series.state, + points = series.points, + options = series.options, + chart = series.chart, + renderer = chart.renderer, + pointAttr, + q1Plot, + q3Plot, + highPlot, + lowPlot, + medianPlot, + crispCorr, + crispX, + graphic, + stemPath, + stemAttr, + boxPath, + whiskersPath, + whiskersAttr, + medianPath, + medianAttr, + width, + left, + right, + halfWidth, + shapeArgs, + color, + doQuartiles = series.doQuartiles !== false, // error bar inherits this series type but doesn't do quartiles + whiskerLength = parseInt(series.options.whiskerLength, 10) / 100; + + + each(points, function (point) { + + graphic = point.graphic; + shapeArgs = point.shapeArgs; // the box + stemAttr = {}; + whiskersAttr = {}; + medianAttr = {}; + color = point.color || series.color; + + if (point.plotY !== UNDEFINED) { + + pointAttr = point.pointAttr[point.selected ? 'selected' : '']; + + // crisp vector coordinates + width = shapeArgs.width; + left = mathFloor(shapeArgs.x); + right = left + width; + halfWidth = mathRound(width / 2); + //crispX = mathRound(left + halfWidth) + crispCorr; + q1Plot = mathFloor(doQuartiles ? point.q1Plot : point.lowPlot);// + crispCorr; + q3Plot = mathFloor(doQuartiles ? point.q3Plot : point.lowPlot);// + crispCorr; + highPlot = mathFloor(point.highPlot);// + crispCorr; + lowPlot = mathFloor(point.lowPlot);// + crispCorr; + + // Stem attributes + stemAttr.stroke = point.stemColor || options.stemColor || color; + stemAttr['stroke-width'] = pick(point.stemWidth, options.stemWidth, options.lineWidth); + stemAttr.dashstyle = point.stemDashStyle || options.stemDashStyle; + + // Whiskers attributes + whiskersAttr.stroke = point.whiskerColor || options.whiskerColor || color; + whiskersAttr['stroke-width'] = pick(point.whiskerWidth, options.whiskerWidth, options.lineWidth); + + // Median attributes + medianAttr.stroke = point.medianColor || options.medianColor || color; + medianAttr['stroke-width'] = pick(point.medianWidth, options.medianWidth, options.lineWidth); + + + // The stem + crispCorr = (stemAttr['stroke-width'] % 2) / 2; + crispX = left + halfWidth + crispCorr; + stemPath = [ + // stem up + 'M', + crispX, q3Plot, + 'L', + crispX, highPlot, + + // stem down + 'M', + crispX, q1Plot, + 'L', + crispX, lowPlot, + 'z' + ]; + + // The box + if (doQuartiles) { + crispCorr = (pointAttr['stroke-width'] % 2) / 2; + crispX = mathFloor(crispX) + crispCorr; + q1Plot = mathFloor(q1Plot) + crispCorr; + q3Plot = mathFloor(q3Plot) + crispCorr; + left += crispCorr; + right += crispCorr; + boxPath = [ + 'M', + left, q3Plot, + 'L', + left, q1Plot, + 'L', + right, q1Plot, + 'L', + right, q3Plot, + 'L', + left, q3Plot, + 'z' + ]; + } + + // The whiskers + if (whiskerLength) { + crispCorr = (whiskersAttr['stroke-width'] % 2) / 2; + highPlot = highPlot + crispCorr; + lowPlot = lowPlot + crispCorr; + whiskersPath = [ + // High whisker + 'M', + crispX - halfWidth * whiskerLength, + highPlot, + 'L', + crispX + halfWidth * whiskerLength, + highPlot, + + // Low whisker + 'M', + crispX - halfWidth * whiskerLength, + lowPlot, + 'L', + crispX + halfWidth * whiskerLength, + lowPlot + ]; + } + + // The median + crispCorr = (medianAttr['stroke-width'] % 2) / 2; + medianPlot = mathRound(point.medianPlot) + crispCorr; + medianPath = [ + 'M', + left, + medianPlot, + 'L', + right, + medianPlot, + 'z' + ]; + + // Create or update the graphics + if (graphic) { // update + + point.stem.animate({ d: stemPath }); + if (whiskerLength) { + point.whiskers.animate({ d: whiskersPath }); + } + if (doQuartiles) { + point.box.animate({ d: boxPath }); + } + point.medianShape.animate({ d: medianPath }); + + } else { // create new + point.graphic = graphic = renderer.g() + .add(series.group); + + point.stem = renderer.path(stemPath) + .attr(stemAttr) + .add(graphic); + + if (whiskerLength) { + point.whiskers = renderer.path(whiskersPath) + .attr(whiskersAttr) + .add(graphic); + } + if (doQuartiles) { + point.box = renderer.path(boxPath) + .attr(pointAttr) + .add(graphic); + } + point.medianShape = renderer.path(medianPath) + .attr(medianAttr) + .add(graphic); + } + } + }); + + } + + +}); + +/* **************************************************************************** + * End Box plot series code * + *****************************************************************************/ +/* **************************************************************************** + * Start error bar series code * + *****************************************************************************/ + +// 1 - set default options +defaultPlotOptions.errorbar = merge(defaultPlotOptions.boxplot, { + color: '#000000', + grouping: false, + linkedTo: ':previous', + tooltip: { + pointFormat: defaultPlotOptions.arearange.tooltip.pointFormat + }, + whiskerWidth: null +}); + +// 2 - Create the series object +seriesTypes.errorbar = extendClass(seriesTypes.boxplot, { + type: 'errorbar', + pointArrayMap: ['low', 'high'], // array point configs are mapped to this + toYData: function (point) { // return a plain array for speedy calculation + return [point.low, point.high]; + }, + pointValKey: 'high', // defines the top of the tracker + doQuartiles: false, + + /** + * Get the width and X offset, either on top of the linked series column + * or standalone + */ + getColumnMetrics: function () { + return (this.linkedParent && this.linkedParent.columnMetrics) || + seriesTypes.column.prototype.getColumnMetrics.call(this); + } +}); + +/* **************************************************************************** + * End error bar series code * + *****************************************************************************/ +/* **************************************************************************** + * Start Waterfall series code * + *****************************************************************************/ + +// 1 - set default options +defaultPlotOptions.waterfall = merge(defaultPlotOptions.column, { + lineWidth: 1, + lineColor: '#333', + dashStyle: 'dot', + borderColor: '#333' +}); + + +// 2 - Create the series object +seriesTypes.waterfall = extendClass(seriesTypes.column, { + type: 'waterfall', + + upColorProp: 'fill', + + pointArrayMap: ['low', 'y'], + + pointValKey: 'y', + + /** + * Init waterfall series, force stacking + */ + init: function (chart, options) { + // force stacking + options.stacking = true; + + seriesTypes.column.prototype.init.call(this, chart, options); + }, + + + /** + * Translate data points from raw values + */ + translate: function () { + var series = this, + options = series.options, + axis = series.yAxis, + len, + i, + points, + point, + shapeArgs, + stack, + y, + previousY, + stackPoint, + threshold = options.threshold, + crispCorr = (options.borderWidth % 2) / 2; + + // run column series translate + seriesTypes.column.prototype.translate.apply(this); + + previousY = threshold; + points = series.points; + + for (i = 0, len = points.length; i < len; i++) { + // cache current point object + point = points[i]; + shapeArgs = point.shapeArgs; + + // get current stack + stack = series.getStack(i); + stackPoint = stack.points[series.index]; + + // override point value for sums + if (isNaN(point.y)) { + point.y = series.yData[i]; + } + + // up points + y = mathMax(previousY, previousY + point.y) + stackPoint[0]; + shapeArgs.y = axis.translate(y, 0, 1); + + + // sum points + if (point.isSum || point.isIntermediateSum) { + shapeArgs.y = axis.translate(stackPoint[1], 0, 1); + shapeArgs.height = axis.translate(stackPoint[0], 0, 1) - shapeArgs.y; + + // if it's not the sum point, update previous stack end position + } else { + previousY += stack.total; + } + + // negative points + if (shapeArgs.height < 0) { + shapeArgs.y += shapeArgs.height; + shapeArgs.height *= -1; + } + + point.plotY = shapeArgs.y = mathRound(shapeArgs.y) - crispCorr; + shapeArgs.height = mathRound(shapeArgs.height); + point.yBottom = shapeArgs.y + shapeArgs.height; + } + }, + + /** + * Call default processData then override yData to reflect waterfall's extremes on yAxis + */ + processData: function (force) { + var series = this, + options = series.options, + yData = series.yData, + points = series.points, + point, + dataLength = yData.length, + threshold = options.threshold || 0, + subSum, + sum, + dataMin, + dataMax, + y, + i; + + sum = subSum = dataMin = dataMax = threshold; + + for (i = 0; i < dataLength; i++) { + y = yData[i]; + point = points && points[i] ? points[i] : {}; + + if (y === "sum" || point.isSum) { + yData[i] = sum; + } else if (y === "intermediateSum" || point.isIntermediateSum) { + yData[i] = subSum; + subSum = threshold; + } else { + sum += y; + subSum += y; + } + dataMin = Math.min(sum, dataMin); + dataMax = Math.max(sum, dataMax); + } + + Series.prototype.processData.call(this, force); + + // Record extremes + series.dataMin = dataMin; + series.dataMax = dataMax; + }, + + /** + * Return y value or string if point is sum + */ + toYData: function (pt) { + if (pt.isSum) { + return "sum"; + } else if (pt.isIntermediateSum) { + return "intermediateSum"; + } + + return pt.y; + }, + + /** + * Postprocess mapping between options and SVG attributes + */ + getAttribs: function () { + seriesTypes.column.prototype.getAttribs.apply(this, arguments); + + var series = this, + options = series.options, + stateOptions = options.states, + upColor = options.upColor || series.color, + hoverColor = Highcharts.Color(upColor).brighten(0.1).get(), + seriesDownPointAttr = merge(series.pointAttr), + upColorProp = series.upColorProp; + + seriesDownPointAttr[''][upColorProp] = upColor; + seriesDownPointAttr.hover[upColorProp] = stateOptions.hover.upColor || hoverColor; + seriesDownPointAttr.select[upColorProp] = stateOptions.select.upColor || upColor; + + each(series.points, function (point) { + if (point.y > 0 && !point.color) { + point.pointAttr = seriesDownPointAttr; + point.color = upColor; + } + }); + }, + + /** + * Draw columns' connector lines + */ + getGraphPath: function () { + + var data = this.data, + length = data.length, + lineWidth = this.options.lineWidth + this.options.borderWidth, + normalizer = mathRound(lineWidth) % 2 / 2, + path = [], + M = 'M', + L = 'L', + prevArgs, + pointArgs, + i, + d; + + for (i = 1; i < length; i++) { + pointArgs = data[i].shapeArgs; + prevArgs = data[i - 1].shapeArgs; + + d = [ + M, + prevArgs.x + prevArgs.width, prevArgs.y + normalizer, + L, + pointArgs.x, prevArgs.y + normalizer + ]; + + if (data[i - 1].y < 0) { + d[2] += prevArgs.height; + d[5] += prevArgs.height; + } + + path = path.concat(d); + } + + return path; + }, + + /** + * Extremes are recorded in processData + */ + getExtremes: noop, + + /** + * Return stack for given index + */ + getStack: function (i) { + var axis = this.yAxis, + stacks = axis.stacks, + key = this.stackKey; + + if (this.processedYData[i] < this.options.threshold) { + key = '-' + key; + } + + return stacks[key][i]; + }, + + drawGraph: Series.prototype.drawGraph +}); + +/* **************************************************************************** + * End Waterfall series code * + *****************************************************************************/ +/* **************************************************************************** + * Start Bubble series code * + *****************************************************************************/ + +// 1 - set default options +defaultPlotOptions.bubble = merge(defaultPlotOptions.scatter, { + dataLabels: { + inside: true, + style: { + color: 'white', + textShadow: '0px 0px 3px black' + }, + verticalAlign: 'middle' + }, + // displayNegative: true, + marker: { + // fillOpacity: 0.5, + lineColor: null, // inherit from series.color + lineWidth: 1 + }, + minSize: 8, + maxSize: '20%', + // negativeColor: null, + tooltip: { + pointFormat: '({point.x}, {point.y}), Size: {point.z}' + }, + turboThreshold: 0, + zThreshold: 0 +}); + +// 2 - Create the series object +seriesTypes.bubble = extendClass(seriesTypes.scatter, { + type: 'bubble', + pointArrayMap: ['y', 'z'], + trackerGroups: ['group', 'dataLabelsGroup'], + + /** + * Mapping between SVG attributes and the corresponding options + */ + pointAttrToOptions: { + stroke: 'lineColor', + 'stroke-width': 'lineWidth', + fill: 'fillColor' + }, + + /** + * Apply the fillOpacity to all fill positions + */ + applyOpacity: function (fill) { + var markerOptions = this.options.marker, + fillOpacity = pick(markerOptions.fillOpacity, 0.5); + + // When called from Legend.colorizeItem, the fill isn't predefined + fill = fill || markerOptions.fillColor || this.color; + + if (fillOpacity !== 1) { + fill = Highcharts.Color(fill).setOpacity(fillOpacity).get('rgba'); + } + return fill; + }, + + /** + * Extend the convertAttribs method by applying opacity to the fill + */ + convertAttribs: function () { + var obj = Series.prototype.convertAttribs.apply(this, arguments); + + obj.fill = this.applyOpacity(obj.fill); + + return obj; + }, + + /** + * Get the radius for each point based on the minSize, maxSize and each point's Z value. This + * must be done prior to Series.translate because the axis needs to add padding in + * accordance with the point sizes. + */ + getRadii: function (zMin, zMax, minSize, maxSize) { + var len, + i, + pos, + zData = this.zData, + radii = [], + zRange; + + // Set the shape type and arguments to be picked up in drawPoints + for (i = 0, len = zData.length; i < len; i++) { + zRange = zMax - zMin; + pos = zRange > 0 ? // relative size, a number between 0 and 1 + (zData[i] - zMin) / (zMax - zMin) : + 0.5; + radii.push(math.ceil(minSize + pos * (maxSize - minSize)) / 2); + } + this.radii = radii; + }, + + /** + * Perform animation on the bubbles + */ + animate: function (init) { + var animation = this.options.animation; + + if (!init) { // run the animation + each(this.points, function (point) { + var graphic = point.graphic, + shapeArgs = point.shapeArgs; + + if (graphic && shapeArgs) { + // start values + graphic.attr('r', 1); + + // animate + graphic.animate({ + r: shapeArgs.r + }, animation); + } + }); + + // delete this function to allow it only once + this.animate = null; + } + }, + + /** + * Extend the base translate method to handle bubble size + */ + translate: function () { + + var i, + data = this.data, + point, + radius, + radii = this.radii; + + // Run the parent method + seriesTypes.scatter.prototype.translate.call(this); + + // Set the shape type and arguments to be picked up in drawPoints + i = data.length; + + while (i--) { + point = data[i]; + radius = radii ? radii[i] : 0; // #1737 + + // Flag for negativeColor to be applied in Series.js + point.negative = point.z < (this.options.zThreshold || 0); + + if (radius >= this.minPxSize / 2) { + // Shape arguments + point.shapeType = 'circle'; + point.shapeArgs = { + x: point.plotX, + y: point.plotY, + r: radius + }; + + // Alignment box for the data label + point.dlBox = { + x: point.plotX - radius, + y: point.plotY - radius, + width: 2 * radius, + height: 2 * radius + }; + } else { // below zThreshold + point.shapeArgs = point.plotY = point.dlBox = UNDEFINED; // #1691 + } + } + }, + + /** + * Get the series' symbol in the legend + * + * @param {Object} legend The legend object + * @param {Object} item The series (this) or point + */ + drawLegendSymbol: function (legend, item) { + var radius = pInt(legend.itemStyle.fontSize) / 2; + + item.legendSymbol = this.chart.renderer.circle( + radius, + legend.baseline - radius, + radius + ).attr({ + zIndex: 3 + }).add(item.legendGroup); + item.legendSymbol.isMarker = true; + + }, + + drawPoints: seriesTypes.column.prototype.drawPoints, + alignDataLabel: seriesTypes.column.prototype.alignDataLabel +}); + +/** + * Add logic to pad each axis with the amount of pixels + * necessary to avoid the bubbles to overflow. + */ +Axis.prototype.beforePadding = function () { + var axis = this, + axisLength = this.len, + chart = this.chart, + pxMin = 0, + pxMax = axisLength, + isXAxis = this.isXAxis, + dataKey = isXAxis ? 'xData' : 'yData', + min = this.min, + extremes = {}, + smallestSize = math.min(chart.plotWidth, chart.plotHeight), + zMin = Number.MAX_VALUE, + zMax = -Number.MAX_VALUE, + range = this.max - min, + transA = axisLength / range, + activeSeries = []; + + // Handle padding on the second pass, or on redraw + if (this.tickPositions) { + each(this.series, function (series) { + + var seriesOptions = series.options, + zData; + + if (series.type === 'bubble' && series.visible) { + + // Correction for #1673 + axis.allowZoomOutside = true; + + // Cache it + activeSeries.push(series); + + if (isXAxis) { // because X axis is evaluated first + + // For each series, translate the size extremes to pixel values + each(['minSize', 'maxSize'], function (prop) { + var length = seriesOptions[prop], + isPercent = /%$/.test(length); + + length = pInt(length); + extremes[prop] = isPercent ? + smallestSize * length / 100 : + length; + + }); + series.minPxSize = extremes.minSize; + + // Find the min and max Z + zData = series.zData; + if (zData.length) { // #1735 + zMin = math.min( + zMin, + math.max( + arrayMin(zData), + seriesOptions.displayNegative === false ? seriesOptions.zThreshold : -Number.MAX_VALUE + ) + ); + zMax = math.max(zMax, arrayMax(zData)); + } + } + } + }); + + each(activeSeries, function (series) { + + var data = series[dataKey], + i = data.length, + radius; + + if (isXAxis) { + series.getRadii(zMin, zMax, extremes.minSize, extremes.maxSize); + } + + if (range > 0) { + while (i--) { + radius = series.radii[i]; + pxMin = Math.min(((data[i] - min) * transA) - radius, pxMin); + pxMax = Math.max(((data[i] - min) * transA) + radius, pxMax); + } + } + }); + + if (activeSeries.length && range > 0 && pick(this.options.min, this.userMin) === UNDEFINED && pick(this.options.max, this.userMax) === UNDEFINED) { + pxMax -= axisLength; + transA *= (axisLength + pxMin - pxMax) / axisLength; + this.min += pxMin / transA; + this.max += pxMax / transA; + } + } +}; + +/* **************************************************************************** + * End Bubble series code * + *****************************************************************************/ +/** + * Extensions for polar charts. Additionally, much of the geometry required for polar charts is + * gathered in RadialAxes.js. + * + */ + +var seriesProto = Series.prototype, + pointerProto = Highcharts.Pointer.prototype; + + + +/** + * Translate a point's plotX and plotY from the internal angle and radius measures to + * true plotX, plotY coordinates + */ +seriesProto.toXY = function (point) { + var xy, + chart = this.chart, + plotX = point.plotX, + plotY = point.plotY; + + // Save rectangular plotX, plotY for later computation + point.rectPlotX = plotX; + point.rectPlotY = plotY; + + // Record the angle in degrees for use in tooltip + point.clientX = ((plotX / Math.PI * 180) + this.xAxis.pane.options.startAngle) % 360; + + // Find the polar plotX and plotY + xy = this.xAxis.postTranslate(point.plotX, this.yAxis.len - plotY); + point.plotX = point.polarPlotX = xy.x - chart.plotLeft; + point.plotY = point.polarPlotY = xy.y - chart.plotTop; +}; + +/** + * Order the tooltip points to get the mouse capture ranges correct. #1915. + */ +seriesProto.orderTooltipPoints = function (points) { + if (this.chart.polar) { + points.sort(function (a, b) { + return a.clientX - b.clientX; + }); + + // Wrap mouse tracking around to capture movement on the segment to the left + // of the north point (#1469, #2093). + if (points[0]) { + points[0].wrappedClientX = points[0].clientX + 360; + points.push(points[0]); + } + } +}; + + +/** + * Add some special init logic to areas and areasplines + */ +function initArea(proceed, chart, options) { + proceed.call(this, chart, options); + if (this.chart.polar) { + + /** + * Overridden method to close a segment path. While in a cartesian plane the area + * goes down to the threshold, in the polar chart it goes to the center. + */ + this.closeSegment = function (path) { + var center = this.xAxis.center; + path.push( + 'L', + center[0], + center[1] + ); + }; + + // Instead of complicated logic to draw an area around the inner area in a stack, + // just draw it behind + this.closedStacks = true; + } +} +wrap(seriesTypes.area.prototype, 'init', initArea); +wrap(seriesTypes.areaspline.prototype, 'init', initArea); + + +/** + * Overridden method for calculating a spline from one point to the next + */ +wrap(seriesTypes.spline.prototype, 'getPointSpline', function (proceed, segment, point, i) { + + var ret, + smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc; + denom = smoothing + 1, + plotX, + plotY, + lastPoint, + nextPoint, + lastX, + lastY, + nextX, + nextY, + leftContX, + leftContY, + rightContX, + rightContY, + distanceLeftControlPoint, + distanceRightControlPoint, + leftContAngle, + rightContAngle, + jointAngle; + + + if (this.chart.polar) { + + plotX = point.plotX; + plotY = point.plotY; + lastPoint = segment[i - 1]; + nextPoint = segment[i + 1]; + + // Connect ends + if (this.connectEnds) { + if (!lastPoint) { + lastPoint = segment[segment.length - 2]; // not the last but the second last, because the segment is already connected + } + if (!nextPoint) { + nextPoint = segment[1]; + } + } + + // find control points + if (lastPoint && nextPoint) { + + lastX = lastPoint.plotX; + lastY = lastPoint.plotY; + nextX = nextPoint.plotX; + nextY = nextPoint.plotY; + leftContX = (smoothing * plotX + lastX) / denom; + leftContY = (smoothing * plotY + lastY) / denom; + rightContX = (smoothing * plotX + nextX) / denom; + rightContY = (smoothing * plotY + nextY) / denom; + distanceLeftControlPoint = Math.sqrt(Math.pow(leftContX - plotX, 2) + Math.pow(leftContY - plotY, 2)); + distanceRightControlPoint = Math.sqrt(Math.pow(rightContX - plotX, 2) + Math.pow(rightContY - plotY, 2)); + leftContAngle = Math.atan2(leftContY - plotY, leftContX - plotX); + rightContAngle = Math.atan2(rightContY - plotY, rightContX - plotX); + jointAngle = (Math.PI / 2) + ((leftContAngle + rightContAngle) / 2); + + + // Ensure the right direction, jointAngle should be in the same quadrant as leftContAngle + if (Math.abs(leftContAngle - jointAngle) > Math.PI / 2) { + jointAngle -= Math.PI; + } + + // Find the corrected control points for a spline straight through the point + leftContX = plotX + Math.cos(jointAngle) * distanceLeftControlPoint; + leftContY = plotY + Math.sin(jointAngle) * distanceLeftControlPoint; + rightContX = plotX + Math.cos(Math.PI + jointAngle) * distanceRightControlPoint; + rightContY = plotY + Math.sin(Math.PI + jointAngle) * distanceRightControlPoint; + + // Record for drawing in next point + point.rightContX = rightContX; + point.rightContY = rightContY; + + } + + + // moveTo or lineTo + if (!i) { + ret = ['M', plotX, plotY]; + } else { // curve from last point to this + ret = [ + 'C', + lastPoint.rightContX || lastPoint.plotX, + lastPoint.rightContY || lastPoint.plotY, + leftContX || plotX, + leftContY || plotY, + plotX, + plotY + ]; + lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later + } + + + } else { + ret = proceed.call(this, segment, point, i); + } + return ret; +}); + +/** + * Extend translate. The plotX and plotY values are computed as if the polar chart were a + * cartesian plane, where plotX denotes the angle in radians and (yAxis.len - plotY) is the pixel distance from + * center. + */ +wrap(seriesProto, 'translate', function (proceed) { + + // Run uber method + proceed.call(this); + + // Postprocess plot coordinates + if (this.chart.polar && !this.preventPostTranslate) { + var points = this.points, + i = points.length; + while (i--) { + // Translate plotX, plotY from angle and radius to true plot coordinates + this.toXY(points[i]); + } + } +}); + +/** + * Extend getSegmentPath to allow connecting ends across 0 to provide a closed circle in + * line-like series. + */ +wrap(seriesProto, 'getSegmentPath', function (proceed, segment) { + + var points = this.points; + + // Connect the path + if (this.chart.polar && this.options.connectEnds !== false && + segment[segment.length - 1] === points[points.length - 1] && points[0].y !== null) { + this.connectEnds = true; // re-used in splines + segment = [].concat(segment, [points[0]]); + } + + // Run uber method + return proceed.call(this, segment); + +}); + + +function polarAnimate(proceed, init) { + var chart = this.chart, + animation = this.options.animation, + group = this.group, + markerGroup = this.markerGroup, + center = this.xAxis.center, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + attribs; + + // Specific animation for polar charts + if (chart.polar) { + + // Enable animation on polar charts only in SVG. In VML, the scaling is different, plus animation + // would be so slow it would't matter. + if (chart.renderer.isSVG) { + + if (animation === true) { + animation = {}; + } + + // Initialize the animation + if (init) { + + // Scale down the group and place it in the center + attribs = { + translateX: center[0] + plotLeft, + translateY: center[1] + plotTop, + scaleX: 0.001, // #1499 + scaleY: 0.001 + }; + + group.attr(attribs); + if (markerGroup) { + markerGroup.attrSetters = group.attrSetters; + markerGroup.attr(attribs); + } + + // Run the animation + } else { + attribs = { + translateX: plotLeft, + translateY: plotTop, + scaleX: 1, + scaleY: 1 + }; + group.animate(attribs, animation); + if (markerGroup) { + markerGroup.animate(attribs, animation); + } + + // Delete this function to allow it only once + this.animate = null; + } + } + + // For non-polar charts, revert to the basic animation + } else { + proceed.call(this, init); + } +} + +// Define the animate method for both regular series and column series and their derivatives +wrap(seriesProto, 'animate', polarAnimate); +wrap(colProto, 'animate', polarAnimate); + + +/** + * Throw in a couple of properties to let setTooltipPoints know we're indexing the points + * in degrees (0-360), not plot pixel width. + */ +wrap(seriesProto, 'setTooltipPoints', function (proceed, renew) { + + if (this.chart.polar) { + extend(this.xAxis, { + tooltipLen: 360 // degrees are the resolution unit of the tooltipPoints array + }); + } + + // Run uber method + return proceed.call(this, renew); +}); + + +/** + * Extend the column prototype's translate method + */ +wrap(colProto, 'translate', function (proceed) { + + var xAxis = this.xAxis, + len = this.yAxis.len, + center = xAxis.center, + startAngleRad = xAxis.startAngleRad, + renderer = this.chart.renderer, + start, + points, + point, + i; + + this.preventPostTranslate = true; + + // Run uber method + proceed.call(this); + + // Postprocess plot coordinates + if (xAxis.isRadial) { + points = this.points; + i = points.length; + while (i--) { + point = points[i]; + start = point.barX + startAngleRad; + point.shapeType = 'path'; + point.shapeArgs = { + d: renderer.symbols.arc( + center[0], + center[1], + len - point.plotY, + null, + { + start: start, + end: start + point.pointWidth, + innerR: len - pick(point.yBottom, len) + } + ) + }; + this.toXY(point); // provide correct plotX, plotY for tooltip + } + } +}); + + +/** + * Align column data labels outside the columns. #1199. + */ +wrap(colProto, 'alignDataLabel', function (proceed, point, dataLabel, options, alignTo, isNew) { + + if (this.chart.polar) { + var angle = point.rectPlotX / Math.PI * 180, + align, + verticalAlign; + + // Align nicely outside the perimeter of the columns + if (options.align === null) { + if (angle > 20 && angle < 160) { + align = 'left'; // right hemisphere + } else if (angle > 200 && angle < 340) { + align = 'right'; // left hemisphere + } else { + align = 'center'; // top or bottom + } + options.align = align; + } + if (options.verticalAlign === null) { + if (angle < 45 || angle > 315) { + verticalAlign = 'bottom'; // top part + } else if (angle > 135 && angle < 225) { + verticalAlign = 'top'; // bottom part + } else { + verticalAlign = 'middle'; // left or right + } + options.verticalAlign = verticalAlign; + } + + seriesProto.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew); + } else { + proceed.call(this, point, dataLabel, options, alignTo, isNew); + } + +}); + +/** + * Extend the mouse tracker to return the tooltip position index in terms of + * degrees rather than pixels + */ +wrap(pointerProto, 'getIndex', function (proceed, e) { + var ret, + chart = this.chart, + center, + x, + y; + + if (chart.polar) { + center = chart.xAxis[0].center; + x = e.chartX - center[0] - chart.plotLeft; + y = e.chartY - center[1] - chart.plotTop; + + ret = 180 - Math.round(Math.atan2(x, y) / Math.PI * 180); + + } else { + + // Run uber method + ret = proceed.call(this, e); + } + return ret; +}); + +/** + * Extend getCoordinates to prepare for polar axis values + */ +wrap(pointerProto, 'getCoordinates', function (proceed, e) { + var chart = this.chart, + ret = { + xAxis: [], + yAxis: [] + }; + + if (chart.polar) { + + each(chart.axes, function (axis) { + var isXAxis = axis.isXAxis, + center = axis.center, + x = e.chartX - center[0] - chart.plotLeft, + y = e.chartY - center[1] - chart.plotTop; + + ret[isXAxis ? 'xAxis' : 'yAxis'].push({ + axis: axis, + value: axis.translate( + isXAxis ? + Math.PI - Math.atan2(x, y) : // angle + Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)), // distance from center + true + ) + }); + }); + + } else { + ret = proceed.call(this, e); + } + + return ret; +}); +}(Highcharts)); diff --git a/public/static/libs/ueditor/third-party/highcharts/highcharts.js b/public/static/libs/ueditor/third-party/highcharts/highcharts.js new file mode 100644 index 0000000..7708ac6 --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/highcharts.js @@ -0,0 +1,283 @@ +/* + Highcharts JS v3.0.6 (2013-10-04) + + (c) 2009-2013 Torstein Hønsi + + License: www.highcharts.com/license +*/ +(function(){function r(a,b){var c;a||(a={});for(c in b)a[c]=b[c];return a}function x(){var a,b=arguments.length,c={},d=function(a,b){var c,h;typeof a!=="object"&&(a={});for(h in b)b.hasOwnProperty(h)&&(c=b[h],a[h]=c&&typeof c==="object"&&Object.prototype.toString.call(c)!=="[object Array]"&&typeof c.nodeType!=="number"?d(a[h]||{},c):b[h]);return a};for(a=0;a3?c.length%3:0;return e+(g?c.substr(0,g)+d:"")+c.substr(g).replace(/(\d{3})(?=\d)/g,"$1"+d)+(f?b+N(a-c).toFixed(f).slice(2):"")}function Ba(a,b){return Array((b||2)+1-String(a).length).join(0)+a}function mb(a,b,c){var d=a[b];a[b]=function(){var a=Array.prototype.slice.call(arguments);a.unshift(d);return c.apply(this,a)}}function Ca(a,b){for(var c="{",d=!1, +e,f,g,h,i,j=[];(c=a.indexOf(c))!==-1;){e=a.slice(0,c);if(d){f=e.split(":");g=f.shift().split(".");i=g.length;e=b;for(h=0;h-1?h.thousandsSep:"")):e=Xa(f,e)}j.push(e);a=a.slice(c+1);c=(d=!d)?"}":"{"}j.push(a);return j.join("")}function nb(a){return R.pow(10,P(R.log(a)/R.LN10))}function ob(a,b,c,d){var e,c=o(c,1);e=a/c;b||(b=[1,2,2.5,5,10],d&&d.allowDecimals=== +!1&&(c===1?b=[1,2,5,10]:c<=0.1&&(b=[1/c])));for(d=0;d=D[pb]&&(i.setMilliseconds(0),i.setSeconds(j>=D[Ya]?0:k*P(i.getSeconds()/k)));if(j>=D[Ya])i[Fb](j>=D[Qa]?0:k*P(i[qb]()/k));if(j>=D[Qa])i[Gb](j>=D[ua]?0:k*P(i[rb]()/k));if(j>=D[ua])i[sb](j>=D[Ra]?1:k*P(i[Sa]()/k));j>=D[Ra]&&(i[Hb](j>=D[Da]?0:k*P(i[$a]()/k)),h=i[ab]());j>=D[Da]&&(h-=h%k,i[Ib](h));if(j===D[Za])i[sb](i[Sa]()-i[tb]()+o(d,1));b=1;h=i[ab]();for(var d= +i.getTime(),l=i[$a](),m=i[Sa](),p=g?0:(864E5+i.getTimezoneOffset()*6E4)%864E5;dc&&(c=a[b]);return c}function Ka(a,b){for(var c in a)a[c]&&a[c]!==b&&a[c].destroy&&a[c].destroy(),delete a[c]}function Ta(a){cb||(cb=U(Ea));a&&cb.appendChild(a);cb.innerHTML=""}function ka(a,b){var c="Highcharts error #"+a+": www.highcharts.com/errors/"+a;if(b)throw c;else O.console&&console.log(c)}function ia(a){return parseFloat(a.toPrecision(14))} +function La(a,b){Fa=o(a,b.animation)}function Lb(){var a=M.global.useUTC,b=a?"getUTC":"get",c=a?"setUTC":"set";bb=a?Date.UTC:function(a,b,c,g,h,i){return(new Date(a,b,o(c,1),o(g,0),o(h,0),o(i,0))).getTime()};qb=b+"Minutes";rb=b+"Hours";tb=b+"Day";Sa=b+"Date";$a=b+"Month";ab=b+"FullYear";Fb=c+"Minutes";Gb=c+"Hours";sb=c+"Date";Hb=c+"Month";Ib=c+"FullYear"}function wa(){}function Ma(a,b,c,d){this.axis=a;this.pos=b;this.type=c||"";this.isNew=!0;!c&&!d&&this.addLabel()}function vb(a,b){this.axis=a;if(b)this.options= +b,this.id=b.id}function Mb(a,b,c,d,e,f){var g=a.chart.inverted;this.axis=a;this.isNegative=c;this.options=b;this.x=d;this.total=null;this.points={};this.stack=e;this.percent=f==="percent";this.alignOptions={align:b.align||(g?c?"left":"right":"center"),verticalAlign:b.verticalAlign||(g?"middle":c?"bottom":"top"),y:o(b.y,g?4:c?14:-6),x:o(b.x,g?c?-6:6:0)};this.textAlign=b.textAlign||(g?c?"right":"left":"center")}function db(){this.init.apply(this,arguments)}function wb(){this.init.apply(this,arguments)} +function xb(a,b){this.init(a,b)}function eb(a,b){this.init(a,b)}function yb(){this.init.apply(this,arguments)}var w,y=document,O=window,R=Math,t=R.round,P=R.floor,xa=R.ceil,s=R.max,I=R.min,N=R.abs,V=R.cos,ca=R.sin,ya=R.PI,Ua=ya*2/360,oa=navigator.userAgent,Nb=O.opera,ta=/msie/i.test(oa)&&!Nb,fb=y.documentMode===8,gb=/AppleWebKit/.test(oa),hb=/Firefox/.test(oa),Ob=/(Mobile|Android|Windows Phone)/.test(oa),za="http://www.w3.org/2000/svg",Z=!!y.createElementNS&&!!y.createElementNS(za,"svg").createSVGRect, +Ub=hb&&parseInt(oa.split("Firefox/")[1],10)<4,$=!Z&&!ta&&!!y.createElement("canvas").getContext,Va,ib=y.documentElement.ontouchstart!==w,Pb={},zb=0,cb,M,Xa,Fa,Ab,D,pa=function(){},Ga=[],Ea="div",S="none",Qb="rgba(192,192,192,"+(Z?1.0E-4:0.002)+")",Db="millisecond",pb="second",Ya="minute",Qa="hour",ua="day",Za="week",Ra="month",Da="year",Rb="stroke-width",bb,qb,rb,tb,Sa,$a,ab,Fb,Gb,sb,Hb,Ib,W={};O.Highcharts=O.Highcharts?ka(16,!0):{};Xa=function(a,b,c){if(!u(b)||isNaN(b))return"Invalid date";var a= +o(a,"%Y-%m-%d %H:%M:%S"),d=new Date(b),e,f=d[rb](),g=d[tb](),h=d[Sa](),i=d[$a](),j=d[ab](),k=M.lang,l=k.weekdays,d=r({a:l[g].substr(0,3),A:l[g],d:Ba(h),e:h,b:k.shortMonths[i],B:k.months[i],m:Ba(i+1),y:j.toString().substr(2,2),Y:j,H:Ba(f),I:Ba(f%12||12),l:f%12||12,M:Ba(d[qb]()),p:f<12?"AM":"PM",P:f<12?"am":"pm",S:Ba(d.getSeconds()),L:Ba(t(b%1E3),3)},Highcharts.dateFormats);for(e in d)for(;a.indexOf("%"+e)!==-1;)a=a.replace("%"+e,typeof d[e]==="function"?d[e](b):d[e]);return c?a.substr(0,1).toUpperCase()+ +a.substr(1):a};Jb.prototype={wrapColor:function(a){if(this.color>=a)this.color=0},wrapSymbol:function(a){if(this.symbol>=a)this.symbol=0}};D=function(){for(var a=0,b=arguments,c=b.length,d={};a-1,f=e?7:3,g,b=b.split(" "),c=[].concat(c),h,i,j=function(a){for(g=a.length;g--;)a[g]==="M"&&a.splice(g+1,0,a[g+1],a[g+2],a[g+1],a[g+2])};e&& +(j(b),j(c));a.isArea&&(h=b.splice(b.length-6,6),i=c.splice(c.length-6,6));if(d<=c.length/f&&b.length===c.length)for(;d--;)c=[].concat(c).splice(0,f).concat(c);a.shift=0;if(b.length)for(a=c.length;b.length{point.key}
              ',pointFormat:'{series.name}: {point.y}
              ',shadow:!0,snap:Ob?25:10,style:{color:"#333333",cursor:"default", +fontSize:"12px",padding:"8px",whiteSpace:"nowrap"}},credits:{enabled:!0,text:"Highcharts.com",href:"http://www.highcharts.com",position:{align:"right",x:-10,verticalAlign:"bottom",y:-5},style:{cursor:"pointer",color:"#909090",fontSize:"9px"}}};var Y=M.plotOptions,X=Y.line;Lb();var ra=function(a){var b=[],c,d;(function(a){a&&a.stops?d=Na(a.stops,function(a){return ra(a[1])}):(c=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/.exec(a))?b=[C(c[1]),C(c[2]), +C(c[3]),parseFloat(c[4],10)]:(c=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(a))?b=[C(c[1],16),C(c[2],16),C(c[3],16),1]:(c=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(a))&&(b=[C(c[1]),C(c[2]),C(c[3]),1])})(a);return{get:function(c){var f;d?(f=x(a),f.stops=[].concat(f.stops),n(d,function(a,b){f.stops[b]=[f.stops[b][0],a.get(c)]})):f=b&&!isNaN(b[0])?c==="rgb"?"rgb("+b[0]+","+b[1]+","+b[2]+")":c==="a"?b[3]:"rgba("+b.join(",")+")":a;return f},brighten:function(a){if(d)n(d, +function(b){b.brighten(a)});else if(sa(a)&&a!==0){var c;for(c=0;c<3;c++)b[c]+=C(a*255),b[c]<0&&(b[c]=0),b[c]>255&&(b[c]=255)}return this},rgba:b,setOpacity:function(a){b[3]=a;return this}}};wa.prototype={init:function(a,b){this.element=b==="span"?U(b):y.createElementNS(za,b);this.renderer=a;this.attrSetters={}},opacity:1,animate:function(a,b,c){b=o(b,Fa,!0);Wa(this);if(b){b=x(b);if(c)b.complete=c;Bb(this,a,b)}else this.attr(a),c&&c()},attr:function(a,b){var c,d,e,f,g=this.element,h=g.nodeName.toLowerCase(), +i=this.renderer,j,k=this.attrSetters,l=this.shadows,m,p,q=this;ea(a)&&u(b)&&(c=a,a={},a[c]=b);if(ea(a))c=a,h==="circle"?c={x:"cx",y:"cy"}[c]||c:c==="strokeWidth"&&(c="stroke-width"),q=v(g,c)||this[c]||0,c!=="d"&&c!=="visibility"&&c!=="fill"&&(q=parseFloat(q));else{for(c in a)if(j=!1,d=a[c],e=k[c]&&k[c].call(this,d,c),e!==!1){e!==w&&(d=e);if(c==="d")d&&d.join&&(d=d.join(" ")),/(NaN| {2}|^$)/.test(d)&&(d="M 0 0");else if(c==="x"&&h==="text")for(e=0;e1100)&&b.call(d,a)}):d["on"+a]=b;return this},setRadialReference:function(a){this.element.radialReference=a;return this},translate:function(a,b){return this.attr({translateX:a,translateY:b})},invert:function(){this.inverted=!0;this.updateTransform();return this},htmlCss:function(a){var b=this.element;if(b=a&&b.tagName==="SPAN"&&a.width)delete a.width,this.textWidth=b,this.updateTransform();this.styles=r(this.styles,a);K(this.element,a);return this},htmlGetBBox:function(){var a= +this.element,b=this.bBox;if(!b){if(a.nodeName==="text")a.style.position="absolute";b=this.bBox={x:a.offsetLeft,y:a.offsetTop,width:a.offsetWidth,height:a.offsetHeight}}return b},htmlUpdateTransform:function(){if(this.added){var a=this.renderer,b=this.element,c=this.translateX||0,d=this.translateY||0,e=this.x||0,f=this.y||0,g=this.textAlign||"left",h={left:0,center:0.5,right:1}[g],i=g&&g!=="left",j=this.shadows;K(b,{marginLeft:c,marginTop:d});j&&n(j,function(a){K(a,{marginLeft:c+1,marginTop:d+1})}); +this.inverted&&n(b.childNodes,function(c){a.invertChild(c,b)});if(b.tagName==="SPAN"){var k,l,j=this.rotation,m;k=0;var p=1,q=0,ba;m=C(this.textWidth);var A=this.xCorr||0,L=this.yCorr||0,Sb=[j,g,b.innerHTML,this.textWidth].join(",");if(Sb!==this.cTT){u(j)&&(k=j*Ua,p=V(k),q=ca(k),this.setSpanRotation(j,q,p));k=o(this.elemWidth,b.offsetWidth);l=o(this.elemHeight,b.offsetHeight);if(k>m&&/[ \-]/.test(b.textContent||b.innerText))K(b,{width:m+"px",display:"block",whiteSpace:"normal"}),k=m;m=a.fontMetrics(b.style.fontSize).b; +A=p<0&&-k;L=q<0&&-l;ba=p*q<0;A+=q*m*(ba?1-h:h);L-=p*m*(j?ba?h:1-h:1);i&&(A-=k*h*(p<0?-1:1),j&&(L-=l*h*(q<0?-1:1)),K(b,{textAlign:g}));this.xCorr=A;this.yCorr=L}K(b,{left:e+A+"px",top:f+L+"px"});if(gb)l=b.offsetHeight;this.cTT=Sb}}else this.alignOnAdd=!0},setSpanRotation:function(a){var b={};b[ta?"-ms-transform":gb?"-webkit-transform":hb?"MozTransform":Nb?"-o-transform":""]=b.transform="rotate("+a+"deg)";K(this.element,b)},updateTransform:function(){var a=this.translateX||0,b=this.translateY||0,c= +this.scaleX,d=this.scaleY,e=this.inverted,f=this.rotation;e&&(a+=this.attr("width"),b+=this.attr("height"));a=["translate("+a+","+b+")"];e?a.push("rotate(90) scale(-1,1)"):f&&a.push("rotate("+f+" "+(this.x||0)+" "+(this.y||0)+")");(u(c)||u(d))&&a.push("scale("+o(c,1)+" "+o(d,1)+")");a.length&&v(this.element,"transform",a.join(" "))},toFront:function(){var a=this.element;a.parentNode.appendChild(a);return this},align:function(a,b,c){var d,e,f,g,h={};e=this.renderer;f=e.alignedObjects;if(a){if(this.alignOptions= +a,this.alignByTranslate=b,!c||ea(c))this.alignTo=d=c||"renderer",ga(f,this),f.push(this),c=null}else a=this.alignOptions,b=this.alignByTranslate,d=this.alignTo;c=o(c,e[d],e);d=a.align;e=a.verticalAlign;f=(c.x||0)+(a.x||0);g=(c.y||0)+(a.y||0);if(d==="right"||d==="center")f+=(c.width-(a.width||0))/{right:1,center:2}[d];h[b?"translateX":"x"]=t(f);if(e==="bottom"||e==="middle")g+=(c.height-(a.height||0))/({bottom:1,middle:2}[e]||1);h[b?"translateY":"y"]=t(g);this[this.placed?"animate":"attr"](h);this.placed= +!0;this.alignAttr=h;return this},getBBox:function(){var a=this.bBox,b=this.renderer,c,d=this.rotation;c=this.element;var e=this.styles,f=d*Ua;if(!a){if(c.namespaceURI===za||b.forExport){try{a=c.getBBox?r({},c.getBBox()):{width:c.offsetWidth,height:c.offsetHeight}}catch(g){}if(!a||a.width<0)a={width:0,height:0}}else a=this.htmlGetBBox();if(b.isSVG){b=a.width;c=a.height;if(ta&&e&&e.fontSize==="11px"&&c.toPrecision(3)==="22.7")a.height=c=14;if(d)a.width=N(c*ca(f))+N(b*V(f)),a.height=N(c*V(f))+N(b*ca(f))}this.bBox= +a}return a},show:function(){return this.attr({visibility:"visible"})},hide:function(){return this.attr({visibility:"hidden"})},fadeOut:function(a){var b=this;b.animate({opacity:0},{duration:a||150,complete:function(){b.hide()}})},add:function(a){var b=this.renderer,c=a||b,d=c.element||b.box,e=d.childNodes,f=this.element,g=v(f,"zIndex"),h;if(a)this.parentGroup=a;this.parentInverted=a&&a.inverted;this.textStr!==void 0&&b.buildText(this);if(g)c.handleZ=!0,g=C(g);if(c.handleZ)for(c=0;cg||!u(g)&&u(b))){d.insertBefore(f,a);h=!0;break}h||d.appendChild(f);this.added=!0;z(this,"add");return this},safeRemoveChild:function(a){var b=a.parentNode;b&&b.removeChild(a)},destroy:function(){var a=this,b=a.element||{},c=a.shadows,d=a.renderer.isSVG&&b.nodeName==="SPAN"&&b.parentNode,e,f;b.onclick=b.onmouseout=b.onmouseover=b.onmousemove=b.point=null;Wa(a);if(a.clipPath)a.clipPath=a.clipPath.destroy();if(a.stops){for(f=0;f/g,'').replace(/<(i|em)>/g,'').replace(//g,"").split(//g),f=b.childNodes,g=/style="([^"]+)"/,h=/href="(http[^"]+)"/,i=v(b,"x"),j=a.styles,k=j&&j.width&&C(j.width),l=j&&j.lineHeight,m=f.length;m--;)b.removeChild(f[m]);k&&!a.added&&this.box.appendChild(b);e[e.length-1]===""&&e.pop();n(e,function(e,f){var m,o=0,e=e.replace(//g, +"|||");m=e.split("|||");n(m,function(e){if(e!==""||m.length===1){var p={},n=y.createElementNS(za,"tspan"),s;g.test(e)&&(s=e.match(g)[1].replace(/(;| |^)color([ :])/,"$1fill$2"),v(n,"style",s));h.test(e)&&!d&&(v(n,"onclick",'location.href="'+e.match(h)[1]+'"'),K(n,{cursor:"pointer"}));e=(e.replace(/<(.|\n)*?>/g,"")||" ").replace(/</g,"<").replace(/>/g,">");if(e!==" "&&(n.appendChild(y.createTextNode(e)),o?p.dx=0:p.x=i,v(n,p),!o&&f&&(!Z&&d&&K(n,{display:"block"}),v(n,"dy",l||c.fontMetrics(/px$/.test(n.style.fontSize)? +n.style.fontSize:j.fontSize).h,gb&&n.offsetHeight)),b.appendChild(n),o++,k))for(var e=e.replace(/([^\^])-/g,"$1- ").split(" "),u,t,p=a._clipHeight,E=[],w=C(l||16),B=1;e.length||E.length;)delete a.bBox,u=a.getBBox(),t=u.width,u=t>k,!u||e.length===1?(e=E,E=[],e.length&&(B++,p&&B*w>p?(e=["..."],a.attr("title",a.textStr)):(n=y.createElementNS(za,"tspan"),v(n,{dy:w,x:i}),s&&v(n,"style",s),b.appendChild(n),t>k&&(k=t)))):(n.removeChild(n.firstChild),E.unshift(e.pop())),e.length&&n.appendChild(y.createTextNode(e.join(" ").replace(/- /g, +"-")))}})})},button:function(a,b,c,d,e,f,g,h){var i=this.label(a,b,c,null,null,null,null,null,"button"),j=0,k,l,m,p,q,n,a={x1:0,y1:0,x2:0,y2:1},e=x({"stroke-width":1,stroke:"#CCCCCC",fill:{linearGradient:a,stops:[[0,"#FEFEFE"],[1,"#F6F6F6"]]},r:2,padding:5,style:{color:"black"}},e);m=e.style;delete e.style;f=x(e,{stroke:"#68A",fill:{linearGradient:a,stops:[[0,"#FFF"],[1,"#ACF"]]}},f);p=f.style;delete f.style;g=x(e,{stroke:"#68A",fill:{linearGradient:a,stops:[[0,"#9BD"],[1,"#CDF"]]}},g);q=g.style; +delete g.style;h=x(e,{style:{color:"#CCC"}},h);n=h.style;delete h.style;J(i.element,ta?"mouseover":"mouseenter",function(){j!==3&&i.attr(f).css(p)});J(i.element,ta?"mouseout":"mouseleave",function(){j!==3&&(k=[e,f,g][j],l=[m,p,q][j],i.attr(k).css(l))});i.setState=function(a){(i.state=j=a)?a===2?i.attr(g).css(q):a===3&&i.attr(h).css(n):i.attr(e).css(m)};return i.on("click",function(){j!==3&&d.call(i)}).attr(e).css(r({cursor:"default"},m))},crispLine:function(a,b){a[1]===a[4]&&(a[1]=a[4]=t(a[1])-b% +2/2);a[2]===a[5]&&(a[2]=a[5]=t(a[2])+b%2/2);return a},path:function(a){var b={fill:S};Ia(a)?b.d=a:T(a)&&r(b,a);return this.createElement("path").attr(b)},circle:function(a,b,c){a=T(a)?a:{x:a,y:b,r:c};return this.createElement("circle").attr(a)},arc:function(a,b,c,d,e,f){if(T(a))b=a.y,c=a.r,d=a.innerR,e=a.start,f=a.end,a=a.x;a=this.symbol("arc",a||0,b||0,c||0,c||0,{innerR:d||0,start:e||0,end:f||0});a.r=c;return a},rect:function(a,b,c,d,e,f){e=T(a)?a.r:e;e=this.createElement("rect").attr({rx:e,ry:e, +fill:S});return e.attr(T(a)?a:e.crisp(f,a,b,s(c,0),s(d,0)))},setSize:function(a,b,c){var d=this.alignedObjects,e=d.length;this.width=a;this.height=b;for(this.boxWrapper[o(c,!0)?"animate":"attr"]({width:a,height:b});e--;)d[e].align()},g:function(a){var b=this.createElement("g");return u(a)?b.attr({"class":"highcharts-"+a}):b},image:function(a,b,c,d,e){var f={preserveAspectRatio:S};arguments.length>1&&r(f,{x:b,y:c,width:d,height:e});f=this.createElement("image").attr(f);f.element.setAttributeNS?f.element.setAttributeNS("http://www.w3.org/1999/xlink", +"href",a):f.element.setAttribute("hc-svg-href",a);return f},symbol:function(a,b,c,d,e,f){var g,h=this.symbols[a],h=h&&h(t(b),t(c),d,e,f),i=/^url\((.*?)\)$/,j,k;if(h)g=this.path(h),r(g,{symbolName:a,x:b,y:c,width:d,height:e}),f&&r(g,f);else if(i.test(a))k=function(a,b){a.element&&(a.attr({width:b[0],height:b[1]}),a.alignByTranslate||a.translate(t((d-b[0])/2),t((e-b[1])/2)))},j=a.match(i)[1],a=Pb[j],g=this.image(j).attr({x:b,y:c}),g.isImg=!0,a?k(g,a):(g.attr({width:0,height:0}),U("img",{onload:function(){k(g, +Pb[j]=[this.width,this.height])},src:j}));return g},symbols:{circle:function(a,b,c,d){var e=0.166*c;return["M",a+c/2,b,"C",a+c+e,b,a+c+e,b+d,a+c/2,b+d,"C",a-e,b+d,a-e,b,a+c/2,b,"Z"]},square:function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c,b+d,a,b+d,"Z"]},triangle:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d,a,b+d,"Z"]},"triangle-down":function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c/2,b+d,"Z"]},diamond:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d/2,a+c/2,b+d,a,b+d/2,"Z"]},arc:function(a,b,c,d, +e){var f=e.start,c=e.r||c||d,g=e.end-0.001,d=e.innerR,h=e.open,i=V(f),j=ca(f),k=V(g),g=ca(g),e=e.end-f');if(b)c=e||b==="span"||b==="img"?c.join(""):a.prepVML(c),this.element= +U(c);this.renderer=a;this.attrSetters={}},add:function(a){var b=this.renderer,c=this.element,d=b.box,d=a?a.element||a:d;a&&a.inverted&&b.invertChild(c,d);d.appendChild(c);this.added=!0;this.alignOnAdd&&!this.deferUpdateTransform&&this.updateTransform();z(this,"add");return this},updateTransform:wa.prototype.htmlUpdateTransform,setSpanRotation:function(a,b,c){K(this.element,{filter:a?["progid:DXImageTransform.Microsoft.Matrix(M11=",c,", M12=",-b,", M21=",b,", M22=",c,", sizingMethod='auto expand')"].join(""): +S})},pathToVML:function(a){for(var b=a.length,c=[],d;b--;)if(sa(a[b]))c[b]=t(a[b]*10)-5;else if(a[b]==="Z")c[b]="x";else if(c[b]=a[b],a.isArc&&(a[b]==="wa"||a[b]==="at"))d=a[b]==="wa"?1:-1,c[b+5]===c[b+7]&&(c[b+7]-=d),c[b+6]===c[b+8]&&(c[b+8]-=d);return c.join(" ")||"x"},attr:function(a,b){var c,d,e,f=this.element||{},g=f.style,h=f.nodeName,i=this.renderer,j=this.symbolName,k,l=this.shadows,m,p=this.attrSetters,q=this;ea(a)&&u(b)&&(c=a,a={},a[c]=b);if(ea(a))c=a,q=c==="strokeWidth"||c==="stroke-width"? +this.strokeweight:this[c];else for(c in a)if(d=a[c],m=!1,e=p[c]&&p[c].call(this,d,c),e!==!1&&d!==null){e!==w&&(d=e);if(j&&/^(x|y|r|start|end|width|height|innerR|anchorX|anchorY)/.test(c))k||(this.symbolAttr(a),k=!0),m=!0;else if(c==="d"){d=d||[];this.d=d.join(" ");f.path=d=this.pathToVML(d);if(l)for(e=l.length;e--;)l[e].path=l[e].cutOff?this.cutOffPath(d,l[e].cutOff):d;m=!0}else if(c==="visibility"){if(l)for(e=l.length;e--;)l[e].style[c]=d;h==="DIV"&&(d=d==="hidden"?"-999em":0,fb||(g[c]=d?"visible": +"hidden"),c="top");g[c]=d;m=!0}else if(c==="zIndex")d&&(g[c]=d),m=!0;else if(qa(c,["x","y","width","height"])!==-1)this[c]=d,c==="x"||c==="y"?c={x:"left",y:"top"}[c]:d=s(0,d),this.updateClipping?(this[c]=d,this.updateClipping()):g[c]=d,m=!0;else if(c==="class"&&h==="DIV")f.className=d;else if(c==="stroke")d=i.color(d,f,c),c="strokecolor";else if(c==="stroke-width"||c==="strokeWidth")f.stroked=d?!0:!1,c="strokeweight",this[c]=d,sa(d)&&(d+="px");else if(c==="dashstyle")(f.getElementsByTagName("stroke")[0]|| +U(i.prepVML([""]),null,null,f))[c]=d||"solid",this.dashstyle=d,m=!0;else if(c==="fill")if(h==="SPAN")g.color=d;else{if(h!=="IMG")f.filled=d!==S?!0:!1,d=i.color(d,f,c,this),c="fillcolor"}else if(c==="opacity")m=!0;else if(h==="shape"&&c==="rotation")this[c]=f.style[c]=d,f.style.left=-t(ca(d*Ua)+1)+"px",f.style.top=t(V(d*Ua))+"px";else if(c==="translateX"||c==="translateY"||c==="rotation")this[c]=d,this.updateTransform(),m=!0;else if(c==="text")this.bBox=null,f.innerHTML=d,m=!0;m||(fb?f[c]= +d:v(f,c,d))}return q},clip:function(a){var b=this,c;a?(c=a.members,ga(c,b),c.push(b),b.destroyClip=function(){ga(c,b)},a=a.getCSS(b)):(b.destroyClip&&b.destroyClip(),a={clip:fb?"inherit":"rect(auto)"});return b.css(a)},css:wa.prototype.htmlCss,safeRemoveChild:function(a){a.parentNode&&Ta(a)},destroy:function(){this.destroyClip&&this.destroyClip();return wa.prototype.destroy.apply(this)},on:function(a,b){this.element["on"+a]=function(){var a=O.event;a.target=a.srcElement;b(a)};return this},cutOffPath:function(a, +b){var c,a=a.split(/[ ,]/);c=a.length;if(c===9||c===11)a[c-4]=a[c-2]=C(a[c-2])-10*b;return a.join(" ")},shadow:function(a,b,c){var d=[],e,f=this.element,g=this.renderer,h,i=f.style,j,k=f.path,l,m,p,q;k&&typeof k.value!=="string"&&(k="x");m=k;if(a){p=o(a.width,3);q=(a.opacity||0.15)/p;for(e=1;e<=3;e++){l=p*2+1-2*e;c&&(m=this.cutOffPath(k.value,l+0.5));j=[''];h=U(g.prepVML(j),null, +{left:C(i.left)+o(a.offsetX,1),top:C(i.top)+o(a.offsetY,1)});if(c)h.cutOff=l+1;j=[''];U(g.prepVML(j),null,null,h);b?b.element.appendChild(h):f.parentNode.insertBefore(h,f);d.push(h)}this.shadows=d}return this}};F=ha(wa,F);var ma={Element:F,isIE8:oa.indexOf("MSIE 8.0")>-1,init:function(a,b,c){var d,e;this.alignedObjects=[];d=this.createElement(Ea);e=d.element;e.style.position="relative";a.appendChild(d.element);this.isVML=!0;this.box=e;this.boxWrapper= +d;this.setSize(b,c,!1);y.namespaces.hcv||(y.namespaces.add("hcv","urn:schemas-microsoft-com:vml"),(y.styleSheets.length?y.styleSheets[0]:y.createStyleSheet()).cssText+="hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke{ behavior:url(#default#VML); display: inline-block; } ")},isHidden:function(){return!this.box.offsetWidth},clipRect:function(a,b,c,d){var e=this.createElement(),f=T(a);return r(e,{members:[],left:(f?a.x:a)+1,top:(f?a.y:b)+1,width:(f?a.width:c)-1,height:(f?a.height:d)-1,getCSS:function(a){var b= +a.element,c=b.nodeName,a=a.inverted,d=this.top-(c==="shape"?b.offsetTop:0),e=this.left,b=e+this.width,f=d+this.height,d={clip:"rect("+t(a?e:d)+"px,"+t(a?f:b)+"px,"+t(a?b:f)+"px,"+t(a?d:e)+"px)"};!a&&fb&&c==="DIV"&&r(d,{width:b+"px",height:f+"px"});return d},updateClipping:function(){n(e.members,function(a){a.css(e.getCSS(a))})}})},color:function(a,b,c,d){var e=this,f,g=/^rgba/,h,i,j=S;a&&a.linearGradient?i="gradient":a&&a.radialGradient&&(i="pattern");if(i){var k,l,m=a.linearGradient||a.radialGradient, +p,q,o,A,L,s="",a=a.stops,u,t=[],w=function(){h=[''];U(e.prepVML(h),null,null,b)};p=a[0];u=a[a.length-1];p[0]>0&&a.unshift([0,p[1]]);u[0]<1&&a.push([1,u[1]]);n(a,function(a,b){g.test(a[1])?(f=ra(a[1]),k=f.get("rgb"),l=f.get("a")):(k=a[1],l=1);t.push(a[0]*100+"% "+k);b?(o=l,A=k):(q=l,L=k)});if(c==="fill")if(i==="gradient")c=m.x1||m[0]||0,a=m.y1||m[1]||0,p=m.x2||m[2]||0,m=m.y2||m[3]||0,s='angle="'+ +(90-R.atan((m-a)/(p-c))*180/ya)+'"',w();else{var j=m.r,r=j*2,E=j*2,H=m.cx,B=m.cy,x=b.radialReference,v,j=function(){x&&(v=d.getBBox(),H+=(x[0]-v.x)/v.width-0.5,B+=(x[1]-v.y)/v.height-0.5,r*=x[2]/v.width,E*=x[2]/v.height);s='src="'+M.global.VMLRadialGradientURL+'" size="'+r+","+E+'" origin="0.5,0.5" position="'+H+","+B+'" color2="'+L+'" ';w()};d.added?j():J(d,"add",j);j=A}else j=k}else if(g.test(a)&&b.tagName!=="IMG")f=ra(a),h=["<",c,' opacity="',f.get("a"),'"/>'],U(this.prepVML(h),null,null,b),j= +f.get("rgb");else{j=b.getElementsByTagName(c);if(j.length)j[0].opacity=1,j[0].type="solid";j=a}return j},prepVML:function(a){var b=this.isIE8,a=a.join("");b?(a=a.replace("/>",' xmlns="urn:schemas-microsoft-com:vml" />'),a=a.indexOf('style="')===-1?a.replace("/>",' style="display:inline-block;behavior:url(#default#VML);" />'):a.replace('style="','style="display:inline-block;behavior:url(#default#VML);')):a=a.replace("<","1&&f.attr({x:b,y:c,width:d,height:e});return f},rect:function(a,b,c,d,e,f){var g=this.symbol("rect");g.r= +T(a)?a.r:e;return g.attr(T(a)?a:g.crisp(f,a,b,s(c,0),s(d,0)))},invertChild:function(a,b){var c=b.style;K(a,{flip:"x",left:C(c.width)-1,top:C(c.height)-1,rotation:-90})},symbols:{arc:function(a,b,c,d,e){var f=e.start,g=e.end,h=e.r||c||d,c=e.innerR,d=V(f),i=ca(f),j=V(g),k=ca(g);if(g-f===0)return["x"];f=["wa",a-h,b-h,a+h,b+h,a+h*d,b+h*i,a+h*j,b+h*k];e.open&&!c&&f.push("e","M",a,b);f.push("at",a-c,b-c,a+c,b+c,a+c*j,b+c*k,a+c*d,b+c*i,"x","e");f.isArc=!0;return f},circle:function(a,b,c,d,e){e&&(c=d=2*e.r); +e&&e.isCircle&&(a-=c/2,b-=d/2);return["wa",a,b,a+c,b+d,a+c,b+d/2,a+c,b+d/2,"e"]},rect:function(a,b,c,d,e){var f=a+c,g=b+d,h;!u(e)||!e.r?f=Ha.prototype.symbols.square.apply(0,arguments):(h=I(e.r,c,d),f=["M",a+h,b,"L",f-h,b,"wa",f-2*h,b,f,b+2*h,f-h,b,f,b+h,"L",f,g-h,"wa",f-2*h,g-2*h,f,g,f,g-h,f-h,g,"L",a+h,g,"wa",a,g-2*h,a+2*h,g,a+h,g,a,g-h,"L",a,b+h,"wa",a,b,a+2*h,b+2*h,a,b+h,a+h,b,"x","e"]);return f}}};Highcharts.VMLRenderer=F=function(){this.init.apply(this,arguments)};F.prototype=x(Ha.prototype, +ma);Va=F}var Tb;if($)Highcharts.CanVGRenderer=F=function(){za="http://www.w3.org/1999/xhtml"},F.prototype.symbols={},Tb=function(){function a(){var a=b.length,d;for(d=0;dj&&(c=!1)):h+k>m&&(h=m-k,d&&h+l0&&b.height>0){f=x({align:c&&k&&"center",x:c?!k&&4:10,verticalAlign:!c&&k&&"middle",y:c?k?16:10:k?6:-4,rotation:c&&!k&&90},f);if(!g)a.label=g=w.text(f.text,0,0,f.useHTML).attr({align:f.textAlign||f.align,rotation:f.rotation,zIndex:L}).css(f.style).add();b=[q[1],q[4],o(q[6],q[1])];q=[q[2],q[5],o(q[7],q[2])];c=Ja(b);k=Ja(q);g.align(f,!1,{x:c,y:k,width:va(b)-c,height:va(q)-k});g.show()}else g&&g.hide();return a}, +destroy:function(){ga(this.axis.plotLinesAndBands,this);delete this.axis;Ka(this)}};Mb.prototype={destroy:function(){Ka(this,this.axis)},render:function(a){var b=this.options,c=b.format,c=c?Ca(c,this):b.formatter.call(this);this.label?this.label.attr({text:c,visibility:"hidden"}):this.label=this.axis.chart.renderer.text(c,0,0,b.useHTML).css(b.style).attr({align:this.textAlign,rotation:b.rotation,visibility:"hidden"}).add(a)},setOffset:function(a,b){var c=this.axis,d=c.chart,e=d.inverted,f=this.isNegative, +g=c.translate(this.percent?100:this.total,0,0,0,1),c=c.translate(0),c=N(g-c),h=d.xAxis[0].translate(this.x)+a,i=d.plotHeight,f={x:e?f?g:g-c:h,y:e?i-h-b:f?i-g-c:i-g,width:e?c:b,height:e?b:c};if(e=this.label)e.align(this.alignOptions,null,f),f=e.alignAttr,e.attr({visibility:this.options.crop===!1||d.isInsidePlot(f.x,f.y)?Z?"inherit":"visible":"hidden"})}};db.prototype={defaultOptions:{dateTimeLabelFormats:{millisecond:"%H:%M:%S.%L",second:"%H:%M:%S",minute:"%H:%M",hour:"%H:%M",day:"%e. %b",week:"%e. %b", +month:"%b '%y",year:"%Y"},endOnTick:!1,gridLineColor:"#C0C0C0",labels:G,lineColor:"#C0D0E0",lineWidth:1,minPadding:0.01,maxPadding:0.01,minorGridLineColor:"#E0E0E0",minorGridLineWidth:1,minorTickColor:"#A0A0A0",minorTickLength:2,minorTickPosition:"outside",startOfWeek:1,startOnTick:!1,tickColor:"#C0D0E0",tickLength:5,tickmarkPlacement:"between",tickPixelInterval:100,tickPosition:"outside",tickWidth:1,title:{align:"middle",style:{color:"#4d759e",fontWeight:"bold"}},type:"linear"},defaultYAxisOptions:{endOnTick:!0, +gridLineWidth:1,tickPixelInterval:72,showLastLabel:!0,labels:{x:-8,y:3},lineWidth:0,maxPadding:0.05,minPadding:0.05,startOnTick:!0,tickWidth:0,title:{rotation:270,text:"Values"},stackLabels:{enabled:!1,formatter:function(){return Aa(this.total,-1)},style:G.style}},defaultLeftAxisOptions:{labels:{x:-8,y:null},title:{rotation:270}},defaultRightAxisOptions:{labels:{x:8,y:null},title:{rotation:90}},defaultBottomAxisOptions:{labels:{x:0,y:14},title:{rotation:0}},defaultTopAxisOptions:{labels:{x:0,y:-5}, +title:{rotation:0}},init:function(a,b){var c=b.isX;this.horiz=a.inverted?!c:c;this.xOrY=(this.isXAxis=c)?"x":"y";this.opposite=b.opposite;this.side=this.horiz?this.opposite?0:2:this.opposite?1:3;this.setOptions(b);var d=this.options,e=d.type;this.labelFormatter=d.labels.formatter||this.defaultLabelFormatter;this.userOptions=b;this.minPixelPadding=0;this.chart=a;this.reversed=d.reversed;this.zoomEnabled=d.zoomEnabled!==!1;this.categories=d.categories||e==="category";this.isLog=e==="logarithmic";this.isDatetimeAxis= +e==="datetime";this.isLinked=u(d.linkedTo);this.tickmarkOffset=this.categories&&d.tickmarkPlacement==="between"?0.5:0;this.ticks={};this.minorTicks={};this.plotLinesAndBands=[];this.alternateBands={};this.len=0;this.minRange=this.userMinRange=d.minRange||d.maxZoom;this.range=d.range;this.offset=d.offset||0;this.stacks={};this.oldStacks={};this.stackExtremes={};this.min=this.max=null;var f,d=this.options.events;qa(this,a.axes)===-1&&(a.axes.push(this),a[c?"xAxis":"yAxis"].push(this));this.series=this.series|| +[];if(a.inverted&&c&&this.reversed===w)this.reversed=!0;this.removePlotLine=this.removePlotBand=this.removePlotBandOrLine;for(f in d)J(this,f,d[f]);if(this.isLog)this.val2lin=na,this.lin2val=fa},setOptions:function(a){this.options=x(this.defaultOptions,this.isXAxis?{}:this.defaultYAxisOptions,[this.defaultTopAxisOptions,this.defaultRightAxisOptions,this.defaultBottomAxisOptions,this.defaultLeftAxisOptions][this.side],x(M[this.isXAxis?"xAxis":"yAxis"],a))},update:function(a,b){var c=this.chart,a=c.options[this.xOrY+ +"Axis"][this.options.index]=x(this.userOptions,a);this.destroy(!0);this._addedPlotLB=this.userMin=this.userMax=w;this.init(c,r(a,{events:w}));c.isDirtyBox=!0;o(b,!0)&&c.redraw()},remove:function(a){var b=this.chart,c=this.xOrY+"Axis";n(this.series,function(a){a.remove(!1)});ga(b.axes,this);ga(b[c],this);b.options[c].splice(this.options.index,1);n(b[c],function(a,b){a.options.index=b});this.destroy();b.isDirtyBox=!0;o(a,!0)&&b.redraw()},defaultLabelFormatter:function(){var a=this.axis,b=this.value, +c=a.categories,d=this.dateTimeLabelFormat,e=M.lang.numericSymbols,f=e&&e.length,g,h=a.options.labels.format,a=a.isLog?b:a.tickInterval;if(h)g=Ca(h,this);else if(c)g=b;else if(d)g=Xa(d,b);else if(f&&a>=1E3)for(;f--&&g===w;)c=Math.pow(1E3,f+1),a>=c&&e[f]!==null&&(g=Aa(b/c,-1)+e[f]);g===w&&(g=b>=1E3?Aa(b,0):Aa(b,-1));return g},getSeriesExtremes:function(){var a=this,b=a.chart;a.hasVisibleSeries=!1;a.dataMin=a.dataMax=null;a.stackExtremes={};a.buildStacks();n(a.series,function(c){if(c.visible||!b.options.chart.ignoreHiddenSeries){var d; +d=c.options.threshold;var e;a.hasVisibleSeries=!0;a.isLog&&d<=0&&(d=null);if(a.isXAxis){if(d=c.xData,d.length)a.dataMin=I(o(a.dataMin,d[0]),Ja(d)),a.dataMax=s(o(a.dataMax,d[0]),va(d))}else{c.getExtremes();e=c.dataMax;c=c.dataMin;if(u(c)&&u(e))a.dataMin=I(o(a.dataMin,c),c),a.dataMax=s(o(a.dataMax,e),e);if(u(d))if(a.dataMin>=d)a.dataMin=d,a.ignoreMinPadding=!0;else if(a.dataMaxf+this.width)m=!0}else if(c=f,i=l-this.right,hg+this.height)m=!0;return m&&!d?null:e.renderer.crispLine(["M",c,h,"L",i,j],b||0)},getPlotBandPath:function(a,b){var c=this.getPlotLinePath(b),d=this.getPlotLinePath(a);d&&c?d.push(c[4], +c[5],c[1],c[2]):d=null;return d},getLinearTickPositions:function(a,b,c){for(var d,b=ia(P(b/a)*a),c=ia(xa(c/a)*a),e=[];b<=c;){e.push(b);b=ia(b+a);if(b===d)break;d=b}return e},getLogTickPositions:function(a,b,c,d){var e=this.options,f=this.len,g=[];if(!d)this._minorAutoInterval=null;if(a>=0.5)a=t(a),g=this.getLinearTickPositions(a,b,c);else if(a>=0.08)for(var f=P(b),h,i,j,k,l,e=a>0.3?[1,2,4]:a>0.15?[1,2,4,6,8]:[1,2,3,4,5,6,7,8,9];fb&&(!d|| +k<=c)&&g.push(k),k>c&&(l=!0),k=j}else if(b=fa(b),c=fa(c),a=e[d?"minorTickInterval":"tickInterval"],a=o(a==="auto"?null:a,this._minorAutoInterval,(c-b)*(e.tickPixelInterval/(d?5:1))/((d?f/this.tickPositions.length:f)||1)),a=ob(a,null,nb(a)),g=Na(this.getLinearTickPositions(a,b,c),na),!d)this._minorAutoInterval=a/5;if(!d)this.tickInterval=a;return g},getMinorTickPositions:function(){var a=this.options,b=this.tickPositions,c=this.minorTickInterval,d=[],e;if(this.isLog){e=b.length;for(a=1;a=this.minRange,f,g,h,i,j;if(this.isXAxis&&this.minRange===w&&!this.isLog)u(a.min)||u(a.max)?this.minRange=null:(n(this.series,function(a){i=a.xData;for(g=j=a.xIncrement?1:i.length- +1;g>0;g--)if(h=i[g]-i[g-1],f===w||hb&&(g=0);c=s(c,g);e=s(e,ea(h)?0:g/2);f=s(f,h==="on"?0:g);!a.noSharedTooltip&&u(l)&&(d=u(d)?I(d,l):l)}),g=this.ordinalSlope&&d?this.ordinalSlope/d:1,this.minPointOffset=e*=g,this.pointRangePadding=f*=g,this.pointRange=I(c,b),this.closestPointRange=d;if(a)this.oldTransA=h;this.translationSlope=this.transA=h=this.len/(b+f||1);this.transB=this.horiz?this.left:this.bottom;this.minPixelPadding=h*e},setTickPositions:function(a){var b=this,c= +b.chart,d=b.options,e=b.isLog,f=b.isDatetimeAxis,g=b.isXAxis,h=b.isLinked,i=b.options.tickPositioner,j=d.maxPadding,k=d.minPadding,l=d.tickInterval,m=d.minTickInterval,p=d.tickPixelInterval,q,ba=b.categories;h?(b.linkedParent=c[g?"xAxis":"yAxis"][d.linkedTo],c=b.linkedParent.getExtremes(),b.min=o(c.min,c.dataMin),b.max=o(c.max,c.dataMax),d.type!==b.linkedParent.options.type&&ka(11,1)):(b.min=o(b.userMin,d.min,b.dataMin),b.max=o(b.userMax,d.max,b.dataMax));if(e)!a&&I(b.min,o(b.dataMin,b.min))<=0&& +ka(10,1),b.min=ia(na(b.min)),b.max=ia(na(b.max));if(b.range&&(b.userMin=b.min=s(b.min,b.max-b.range),b.userMax=b.max,a))b.range=null;b.beforePadding&&b.beforePadding();b.adjustForMinRange();if(!ba&&!b.usePercentage&&!h&&u(b.min)&&u(b.max)&&(c=b.max-b.min)){if(!u(d.min)&&!u(b.userMin)&&k&&(b.dataMin<0||!b.ignoreMinPadding))b.min-=c*k;if(!u(d.max)&&!u(b.userMax)&&j&&(b.dataMax>0||!b.ignoreMaxPadding))b.max+=c*j}b.min===b.max||b.min===void 0||b.max===void 0?b.tickInterval=1:h&&!l&&p===b.linkedParent.options.tickPixelInterval? +b.tickInterval=b.linkedParent.tickInterval:(b.tickInterval=o(l,ba?1:(b.max-b.min)*p/s(b.len,p)),!u(l)&&b.lens(2*b.len,200)&&ka(19,!0),a=f?(b.getNonLinearTimeTicks||Eb)(Cb(b.tickInterval,d.units),b.min,b.max,d.startOfWeek,b.ordinalPositions,b.closestPointRange,!0):e?b.getLogTickPositions(b.tickInterval, +b.min,b.max):b.getLinearTickPositions(b.tickInterval,b.min,b.max),q&&a.splice(1,a.length-2),b.tickPositions=a;if(!h)e=a[0],f=a[a.length-1],h=b.minPointOffset||0,d.startOnTick?b.min=e:b.min-h>e&&a.shift(),d.endOnTick?b.max=f:b.max+h(b[d]||0)&&this.options.alignTicks!== +!1)b[d]=c.length;a.maxTicks=b},adjustTickAmount:function(){var a=this._maxTicksKey,b=this.tickPositions,c=this.chart.maxTicks;if(c&&c[a]&&!this.isDatetimeAxis&&!this.categories&&!this.isLinked&&this.options.alignTicks!==!1){var d=this.tickAmount,e=b.length;this.tickAmount=a=c[a];if(e=this.dataMax&&(b=w));this.displayBtn=a!==w||b!==w;this.setExtremes(a, +b,!1,w,{trigger:"zoom"});return!0},setAxisSize:function(){var a=this.chart,b=this.options,c=b.offsetLeft||0,d=b.offsetRight||0,e=this.horiz,f,g;this.left=g=o(b.left,a.plotLeft+c);this.top=f=o(b.top,a.plotTop);this.width=c=o(b.width,a.plotWidth-c+d);this.height=b=o(b.height,a.plotHeight);this.bottom=a.chartHeight-b-f;this.right=a.chartWidth-c-g;this.len=s(e?c:b,0);this.pos=e?g:f},getExtremes:function(){var a=this.isLog;return{min:a?ia(fa(this.min)):this.min,max:a?ia(fa(this.max)):this.max,dataMin:this.dataMin, +dataMax:this.dataMax,userMin:this.userMin,userMax:this.userMax}},getThreshold:function(a){var b=this.isLog,c=b?fa(this.min):this.min,b=b?fa(this.max):this.max;c>a||a===null?a=c:b15&&a<165?"right":a>195&&a<345?"left":"center"},getOffset:function(){var a=this,b=a.chart,c=b.renderer,d=a.options,e=a.tickPositions,f=a.ticks,g=a.horiz,h=a.side,i=b.inverted?[1,0,3,2][h]:h,j,k=0,l,m=0,p=d.title,q=d.labels,ba=0,A=b.axisOffset,L=b.clipOffset,t=[-1,1,1,-1][h],r,v=1,x=o(q.maxStaggerLines,5),la,E,H,B;a.hasData=j=a.hasVisibleSeries||u(a.min)&&u(a.max)&&!!e;a.showAxis=b=j||o(d.showEmpty,!0);a.staggerLines=a.horiz&&q.staggerLines; +if(!a.axisGroup)a.gridGroup=c.g("grid").attr({zIndex:d.gridZIndex||1}).add(),a.axisGroup=c.g("axis").attr({zIndex:d.zIndex||2}).add(),a.labelGroup=c.g("axis-labels").attr({zIndex:q.zIndex||7}).add();if(j||a.isLinked){a.labelAlign=o(q.align||a.autoLabelAlign(q.rotation));n(e,function(b){f[b]?f[b].addLabel():f[b]=new Ma(a,b)});if(a.horiz&&!a.staggerLines&&x&&!q.rotation){for(r=a.reversed?[].concat(e).reverse():e;v1)a.staggerLines=v}n(e,function(b){if(h===0||h===2||{1:"left",3:"right"}[h]===a.labelAlign)ba=s(f[b].getLabelSize(),ba)});if(a.staggerLines)ba*=a.staggerLines,a.labelOffset=ba}else for(r in f)f[r].destroy(),delete f[r];if(p&&p.text&&p.enabled!==!1){if(!a.axisTitle)a.axisTitle=c.text(p.text,0,0,p.useHTML).attr({zIndex:7,rotation:p.rotation||0,align:p.textAlign||{low:"left",middle:"center",high:"right"}[p.align]}).css(p.style).add(a.axisGroup), +a.axisTitle.isNew=!0;if(b)k=a.axisTitle.getBBox()[g?"height":"width"],m=o(p.margin,g?5:10),l=p.offset;a.axisTitle[b?"show":"hide"]()}a.offset=t*o(d.offset,A[h]);a.axisTitleMargin=o(l,ba+m+(h!==2&&ba&&t*d.labels[g?"y":"x"]));A[h]=s(A[h],a.axisTitleMargin+k+t*a.offset);L[i]=s(L[i],P(d.lineWidth/2)*2)},getLinePath:function(a){var b=this.chart,c=this.opposite,d=this.offset,e=this.horiz,f=this.left+(c?this.width:0)+d,d=b.chartHeight-this.bottom-(c?this.height:0)+d;c&&(a*=-1);return b.renderer.crispLine(["M", +e?this.left:f,e?d:this.top,"L",e?b.chartWidth-this.right:f,e?d:b.chartHeight-this.bottom],a)},getTitlePosition:function(){var a=this.horiz,b=this.left,c=this.top,d=this.len,e=this.options.title,f=a?b:c,g=this.opposite,h=this.offset,i=C(e.style.fontSize||12),d={low:f+(a?0:d),middle:f+d/2,high:f+(a?d:0)}[e.align],b=(a?c+this.height:b)+(a?1:-1)*(g?-1:1)*this.axisTitleMargin+(this.side===2?i:0);return{x:a?d:b+(g?this.width:0)+h+(e.x||0),y:a?b-(g?this.height:0)+h:d+(e.y||0)}},render:function(){var a=this, +b=a.chart,c=b.renderer,d=a.options,e=a.isLog,f=a.isLinked,g=a.tickPositions,h=a.axisTitle,i=a.stacks,j=a.ticks,k=a.minorTicks,l=a.alternateBands,m=d.stackLabels,p=d.alternateGridColor,q=a.tickmarkOffset,o=d.lineWidth,A,s=b.hasRendered&&u(a.oldMin)&&!isNaN(a.oldMin);A=a.hasData;var t=a.showAxis,r,v;n([j,k,l],function(a){for(var b in a)a[b].isActive=!1});if(A||f)if(a.minorTickInterval&&!a.categories&&n(a.getMinorTickPositions(),function(b){k[b]||(k[b]=new Ma(a,b,"minor"));s&&k[b].isNew&&k[b].render(null, +!0);k[b].render(null,!1,1)}),g.length&&(n(g.slice(1).concat([g[0]]),function(b,c){c=c===g.length-1?0:c+1;if(!f||b>=a.min&&b<=a.max)j[b]||(j[b]=new Ma(a,b)),s&&j[b].isNew&&j[b].render(c,!0),j[b].render(c,!1,1)}),q&&a.min===0&&(j[-1]||(j[-1]=new Ma(a,-1,null,!0)),j[-1].render(-1))),p&&n(g,function(b,c){if(c%2===0&&b1||N(b-f.y)>1))clearTimeout(this.tooltipTimeout),this.tooltipTimeout=setTimeout(function(){e&&e.move(a,b,c,d)},32)},hide:function(){var a=this,b;clearTimeout(this.hideTimer);if(!this.isHidden)b=this.chart.hoverPoints,this.hideTimer=setTimeout(function(){a.label.fadeOut();a.isHidden=!0},o(this.options.hideDelay,500)),b&&n(b,function(a){a.setState()}),this.chart.hoverPoints= +null},hideCrosshairs:function(){n(this.crosshairs,function(a){a&&a.hide()})},getAnchor:function(a,b){var c,d=this.chart,e=d.inverted,f=d.plotTop,g=0,h=0,i,a=ja(a);c=a[0].tooltipPos;this.followPointer&&b&&(b.chartX===w&&(b=d.pointer.normalize(b)),c=[b.chartX-d.plotLeft,b.chartY-f]);c||(n(a,function(a){i=a.series.yAxis;g+=a.plotX;h+=(a.plotLow?(a.plotLow+a.plotHigh)/2:a.plotY)+(!e&&i?i.top-f:0)}),g/=a.length,h/=a.length,c=[e?d.plotWidth-h:g,this.shared&&!e&&a.length>1&&b?b.chartY-f:e?d.plotHeight-g: +h]);return Na(c,t)},getPosition:function(a,b,c){var d=this.chart,e=d.plotLeft,f=d.plotTop,g=d.plotWidth,h=d.plotHeight,i=o(this.options.distance,12),j=c.plotX,c=c.plotY,d=j+e+(d.inverted?i:-a-i),k=c-b+f+15,l;d<7&&(d=e+s(j,0)+i);d+a>e+g&&(d-=d+a-(e+g),k=c-b+f-i,l=!0);k=k&&c<=k+b&&(k=c+f+i));k+b>f+h&&(k=s(f,f+h-b-i));return{x:d,y:k}},defaultFormatter:function(a){var b=this.points||ja(this),c=b[0].series,d;d=[c.tooltipHeaderFormatter(b[0])];n(b,function(a){c=a.series;d.push(c.tooltipFormatter&& +c.tooltipFormatter(a)||a.point.tooltipFormatter(c.tooltipOptions.pointFormat))});d.push(a.options.footerFormat||"");return d.join("")},refresh:function(a,b){var c=this.chart,d=this.label,e=this.options,f,g,h={},i,j=[];i=e.formatter||this.defaultFormatter;var h=c.hoverPoints,k,l=e.crosshairs,m=this.shared;clearTimeout(this.hideTimer);this.followPointer=ja(a)[0].series.tooltipOptions.followPointer;g=this.getAnchor(a,b);f=g[0];g=g[1];m&&(!a.series||!a.series.noSharedTooltip)?(c.hoverPoints=a,h&&n(h, +function(a){a.setState()}),n(a,function(a){a.setState("hover");j.push(a.getLabelConfig())}),h={x:a[0].category,y:a[0].y},h.points=j,a=a[0]):h=a.getLabelConfig();i=i.call(h,this);h=a.series;i===!1?this.hide():(this.isHidden&&(Wa(d),d.attr("opacity",1).show()),d.attr({text:i}),k=e.borderColor||a.color||h.color||"#606060",d.attr({stroke:k}),this.updatePosition({plotX:f,plotY:g}),this.isHidden=!1);if(l){l=ja(l);for(d=l.length;d--;)if(m=a.series,e=m[d?"yAxis":"xAxis"],l[d]&&e)if(h=d?o(a.stackY,a.y):a.x, +e.isLog&&(h=na(h)),d===1&&m.modifyValue&&(h=m.modifyValue(h)),e=e.getPlotLinePath(h,1),this.crosshairs[d])this.crosshairs[d].attr({d:e,visibility:"visible"});else{h={"stroke-width":l[d].width||1,stroke:l[d].color||"#C0C0C0",zIndex:l[d].zIndex||2};if(l[d].dashStyle)h.dashstyle=l[d].dashStyle;this.crosshairs[d]=c.renderer.path(e).attr(h).add()}}z(c,"tooltipRefresh",{text:i,x:f+c.plotLeft,y:g+c.plotTop,borderColor:k})},updatePosition:function(a){var b=this.chart,c=this.label,c=(this.options.positioner|| +this.getPosition).call(this,c.width,c.height,a);this.move(t(c.x),t(c.y),a.plotX+b.plotLeft,a.plotY+b.plotTop)}};xb.prototype={init:function(a,b){var c=b.chart,d=c.events,e=$?"":c.zoomType,c=a.inverted,f;this.options=b;this.chart=a;this.zoomX=f=/x/.test(e);this.zoomY=e=/y/.test(e);this.zoomHor=f&&!c||e&&c;this.zoomVert=e&&!c||f&&c;this.runChartClick=d&&!!d.click;this.pinchDown=[];this.lastValidTouch={};if(b.tooltip.enabled)a.tooltip=new wb(a,b.tooltip);this.setDOMEvents()},normalize:function(a,b){var c, +d,a=a||O.event;if(!a.target)a.target=a.srcElement;a=Xb(a);d=a.touches?a.touches.item(0):a;if(!b)this.chartPosition=b=Wb(this.chart.container);d.pageX===w?(c=s(a.x,a.clientX-b.left),d=a.y):(c=d.pageX-b.left,d=d.pageY-b.top);return r(a,{chartX:t(c),chartY:t(d)})},getCoordinates:function(a){var b={xAxis:[],yAxis:[]};n(this.chart.axes,function(c){b[c.isXAxis?"xAxis":"yAxis"].push({axis:c,value:c.toValue(a[c.horiz?"chartX":"chartY"])})});return b},getIndex:function(a){var b=this.chart;return b.inverted? +b.plotHeight+b.plotTop-a.chartY:a.chartX-b.plotLeft},runPointActions:function(a){var b=this.chart,c=b.series,d=b.tooltip,e,f=b.hoverPoint,g=b.hoverSeries,h,i,j=b.chartWidth,k=this.getIndex(a);if(d&&this.options.tooltip.shared&&(!g||!g.noSharedTooltip)){e=[];h=c.length;for(i=0;i +j&&e.splice(h,1);if(e.length&&e[0].clientX!==this.hoverX)d.refresh(e,a),this.hoverX=e[0].clientX}if(g&&g.tracker){if((b=g.tooltipPoints[k])&&b!==f)b.onMouseOver(a)}else d&&d.followPointer&&!d.isHidden&&(a=d.getAnchor([{}],a),d.updatePosition({plotX:a[0],plotY:a[1]}))},reset:function(a){var b=this.chart,c=b.hoverSeries,d=b.hoverPoint,e=b.tooltip,b=e&&e.shared?b.hoverPoints:d;(a=a&&e&&b)&&ja(b)[0].plotX===w&&(a=!1);if(a)e.refresh(b);else{if(d)d.onMouseOut();if(c)c.onMouseOut();e&&(e.hide(),e.hideCrosshairs()); +this.hoverX=null}},scaleGroups:function(a,b){var c=this.chart,d;n(c.series,function(e){d=a||e.getPlotBox();e.xAxis&&e.xAxis.zoomEnabled&&(e.group.attr(d),e.markerGroup&&(e.markerGroup.attr(d),e.markerGroup.clip(b?c.clipRect:null)),e.dataLabelsGroup&&e.dataLabelsGroup.attr(d))});c.clipRect.attr(b||c.clipBox)},pinchTranslateDirection:function(a,b,c,d,e,f,g){var h=this.chart,i=a?"x":"y",j=a?"X":"Y",k="chart"+j,l=a?"width":"height",m=h["plot"+(a?"Left":"Top")],p,q,o=1,n=h.inverted,s=h.bounds[a?"h":"v"], +t=b.length===1,u=b[0][k],r=c[0][k],w=!t&&b[1][k],v=!t&&c[1][k],x,c=function(){!t&&N(u-w)>20&&(o=N(r-v)/N(u-w));q=(m-r)/o+u;p=h["plot"+(a?"Width":"Height")]/o};c();b=q;bs.max&&(b=s.max-p,x=!0);x?(r-=0.8*(r-g[i][0]),t||(v-=0.8*(v-g[i][1])),c()):g[i]=[r,v];n||(f[i]=q-m,f[l]=p);f=n?1/o:o;e[l]=p;e[i]=b;d[n?a?"scaleY":"scaleX":"scale"+j]=o;d["translate"+j]=f*m+(r-f*u)},pinch:function(a){var b=this,c=b.chart,d=b.pinchDown,e=c.tooltip&&c.tooltip.options.followTouchMove,f=a.touches, +g=f.length,h=b.lastValidTouch,i=b.zoomHor||b.pinchHor,j=b.zoomVert||b.pinchVert,k=i||j,l=b.selectionMarker,m={},p=g===1&&(b.inClass(a.target,"highcharts-tracker")&&c.runTrackerClick||c.runChartClick),q={};(k||e)&&!p&&a.preventDefault();Na(f,function(a){return b.normalize(a)});if(a.type==="touchstart")n(f,function(a,b){d[b]={chartX:a.chartX,chartY:a.chartY}}),h.x=[d[0].chartX,d[1]&&d[1].chartX],h.y=[d[0].chartY,d[1]&&d[1].chartY],n(c.axes,function(a){if(a.zoomEnabled){var b=c.bounds[a.horiz?"h":"v"], +d=a.minPixelPadding,e=a.toPixels(a.dataMin),f=a.toPixels(a.dataMax),g=I(e,f),e=s(e,f);b.min=I(a.pos,g-d);b.max=s(a.pos+a.len,e+d)}});else if(d.length){if(!l)b.selectionMarker=l=r({destroy:pa},c.plotBox);i&&b.pinchTranslateDirection(!0,d,f,m,l,q,h);j&&b.pinchTranslateDirection(!1,d,f,m,l,q,h);b.hasPinched=k;b.scaleGroups(m,q);!k&&e&&g===1&&this.runPointActions(b.normalize(a))}},dragStart:function(a){var b=this.chart;b.mouseIsDown=a.type;b.cancelClick=!1;b.mouseDownX=this.mouseDownX=a.chartX;b.mouseDownY= +this.mouseDownY=a.chartY},drag:function(a){var b=this.chart,c=b.options.chart,d=a.chartX,e=a.chartY,f=this.zoomHor,g=this.zoomVert,h=b.plotLeft,i=b.plotTop,j=b.plotWidth,k=b.plotHeight,l,m=this.mouseDownX,p=this.mouseDownY;dh+j&&(d=h+j);ei+k&&(e=i+k);this.hasDragged=Math.sqrt(Math.pow(m-d,2)+Math.pow(p-e,2));if(this.hasDragged>10){l=b.isInsidePlot(m-h,p-i);if(b.hasCartesianSeries&&(this.zoomX||this.zoomY)&&l&&!this.selectionMarker)this.selectionMarker=b.renderer.rect(h,i,f?1:j,g? +1:k,0).attr({fill:c.selectionMarkerFill||"rgba(69,114,167,0.25)",zIndex:7}).add();this.selectionMarker&&f&&(d-=m,this.selectionMarker.attr({width:N(d),x:(d>0?0:d)+m}));this.selectionMarker&&g&&(d=e-p,this.selectionMarker.attr({height:N(d),y:(d>0?0:d)+p}));l&&!this.selectionMarker&&c.panning&&b.pan(a,c.panning)}},drop:function(a){var b=this.chart,c=this.hasPinched;if(this.selectionMarker){var d={xAxis:[],yAxis:[],originalEvent:a.originalEvent||a},e=this.selectionMarker,f=e.x,g=e.y,h;if(this.hasDragged|| +c)n(b.axes,function(a){if(a.zoomEnabled){var b=a.horiz,c=a.toValue(b?f:g),b=a.toValue(b?f+e.width:g+e.height);!isNaN(c)&&!isNaN(b)&&(d[a.xOrY+"Axis"].push({axis:a,min:I(c,b),max:s(c,b)}),h=!0)}}),h&&z(b,"selection",d,function(a){b.zoom(r(a,c?{animation:!1}:null))});this.selectionMarker=this.selectionMarker.destroy();c&&this.scaleGroups()}if(b)K(b.container,{cursor:b._cursor}),b.cancelClick=this.hasDragged>10,b.mouseIsDown=this.hasDragged=this.hasPinched=!1,this.pinchDown=[]},onContainerMouseDown:function(a){a= +this.normalize(a);a.preventDefault&&a.preventDefault();this.dragStart(a)},onDocumentMouseUp:function(a){this.drop(a)},onDocumentMouseMove:function(a){var b=this.chart,c=this.chartPosition,d=b.hoverSeries,a=this.normalize(a,c);c&&d&&!this.inClass(a.target,"highcharts-tracker")&&!b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop)&&this.reset()},onContainerMouseLeave:function(){this.reset();this.chartPosition=null},onContainerMouseMove:function(a){var b=this.chart,a=this.normalize(a);a.returnValue= +!1;b.mouseIsDown==="mousedown"&&this.drag(a);(this.inClass(a.target,"highcharts-tracker")||b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop))&&!b.openMenu&&this.runPointActions(a)},inClass:function(a,b){for(var c;a;){if(c=v(a,"class"))if(c.indexOf(b)!==-1)return!0;else if(c.indexOf("highcharts-container")!==-1)return!1;a=a.parentNode}},onTrackerMouseOut:function(a){var b=this.chart.hoverSeries;if(b&&!b.options.stickyTracking&&!this.inClass(a.toElement||a.relatedTarget,"highcharts-tooltip"))b.onMouseOut()}, +onContainerClick:function(a){var b=this.chart,c=b.hoverPoint,d=b.plotLeft,e=b.plotTop,f=b.inverted,g,h,i,a=this.normalize(a);a.cancelBubble=!0;if(!b.cancelClick)c&&this.inClass(a.target,"highcharts-tracker")?(g=this.chartPosition,h=c.plotX,i=c.plotY,r(c,{pageX:g.left+d+(f?b.plotWidth-i:h),pageY:g.top+e+(f?b.plotHeight-h:i)}),z(c.series,"click",r(a,{point:c})),b.hoverPoint&&c.firePointEvent("click",a)):(r(a,this.getCoordinates(a)),b.isInsidePlot(a.chartX-d,a.chartY-e)&&z(b,"click",a))},onContainerTouchStart:function(a){var b= +this.chart;a.touches.length===1?(a=this.normalize(a),b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop)?(this.runPointActions(a),this.pinch(a)):this.reset()):a.touches.length===2&&this.pinch(a)},onContainerTouchMove:function(a){(a.touches.length===1||a.touches.length===2)&&this.pinch(a)},onDocumentTouchEnd:function(a){this.drop(a)},setDOMEvents:function(){var a=this,b=a.chart.container,c;this._events=c=[[b,"onmousedown","onContainerMouseDown"],[b,"onmousemove","onContainerMouseMove"],[b,"onclick", +"onContainerClick"],[b,"mouseleave","onContainerMouseLeave"],[y,"mousemove","onDocumentMouseMove"],[y,"mouseup","onDocumentMouseUp"]];ib&&c.push([b,"ontouchstart","onContainerTouchStart"],[b,"ontouchmove","onContainerTouchMove"],[y,"touchend","onDocumentTouchEnd"]);n(c,function(b){a["_"+b[2]]=function(c){a[b[2]](c)};b[1].indexOf("on")===0?b[0][b[1]]=a["_"+b[2]]:J(b[0],b[1],a["_"+b[2]])})},destroy:function(){var a=this;n(a._events,function(b){b[1].indexOf("on")===0?b[0][b[1]]=null:aa(b[0],b[1],a["_"+ +b[2]])});delete a._events;clearInterval(a.tooltipTimeout)}};eb.prototype={init:function(a,b){var c=this,d=b.itemStyle,e=o(b.padding,8),f=b.itemMarginTop||0;this.options=b;if(b.enabled)c.baseline=C(d.fontSize)+3+f,c.itemStyle=d,c.itemHiddenStyle=x(d,b.itemHiddenStyle),c.itemMarginTop=f,c.padding=e,c.initialItemX=e,c.initialItemY=e-5,c.maxItemWidth=0,c.chart=a,c.itemHeight=0,c.lastLineHeight=0,c.render(),J(c.chart,"endResize",function(){c.positionCheckboxes()})},colorizeItem:function(a,b){var c=this.options, +d=a.legendItem,e=a.legendLine,f=a.legendSymbol,g=this.itemHiddenStyle.color,c=b?c.itemStyle.color:g,h=b?a.color:g,g=a.options&&a.options.marker,i={stroke:h,fill:h},j;d&&d.css({fill:c,color:c});e&&e.attr({stroke:h});if(f){if(g&&f.isMarker)for(j in g=a.convertAttribs(g),g)d=g[j],d!==w&&(i[j]=d);f.attr(i)}},positionItem:function(a){var b=this.options,c=b.symbolPadding,b=!b.rtl,d=a._legendItemPos,e=d[0],d=d[1],f=a.checkbox;a.legendGroup&&a.legendGroup.translate(b?e:this.legendWidth-e-2*c-4,d);if(f)f.x= +e,f.y=d},destroyItem:function(a){var b=a.checkbox;n(["legendItem","legendLine","legendSymbol","legendGroup"],function(b){a[b]&&(a[b]=a[b].destroy())});b&&Ta(a.checkbox)},destroy:function(){var a=this.group,b=this.box;if(b)this.box=b.destroy();if(a)this.group=a.destroy()},positionCheckboxes:function(a){var b=this.group.alignAttr,c,d=this.clipHeight||this.legendHeight;if(b)c=b.translateY,n(this.allItems,function(e){var f=e.checkbox,g;f&&(g=c+f.y+(a||0)+3,K(f,{left:b.translateX+e.legendItemWidth+f.x- +20+"px",top:g+"px",display:g>c-6&&g(p||c.chartWidth-2*k-A))b.itemX=A,b.itemY+=n+b.lastLineHeight+q,b.lastLineHeight=0;b.maxItemWidth=s(b.maxItemWidth,e);b.lastItemY=n+b.itemY+q;b.lastLineHeight=s(g,b.lastLineHeight);a._legendItemPos=[b.itemX,b.itemY];f?b.itemX+=e:(b.itemY+=n+g+q,b.lastLineHeight=g);b.offsetWidth= +p||s((f?b.itemX-A-l:e)+k,b.offsetWidth)},render:function(){var a=this,b=a.chart,c=b.renderer,d=a.group,e,f,g,h,i=a.box,j=a.options,k=a.padding,l=j.borderWidth,m=j.backgroundColor;a.itemX=a.initialItemX;a.itemY=a.initialItemY;a.offsetWidth=0;a.lastItemY=0;if(!d)a.group=d=c.g("legend").attr({zIndex:7}).add(),a.contentGroup=c.g().attr({zIndex:1}).add(d),a.scrollGroup=c.g().add(a.contentGroup);a.renderTitle();e=[];n(b.series,function(a){var b=a.options;b.showInLegend&&!u(b.linkedTo)&&(e=e.concat(a.legendItems|| +(b.legendType==="point"?a.data:a)))});Kb(e,function(a,b){return(a.options&&a.options.legendIndex||0)-(b.options&&b.options.legendIndex||0)});j.reversed&&e.reverse();a.allItems=e;a.display=f=!!e.length;n(e,function(b){a.renderItem(b)});g=j.width||a.offsetWidth;h=a.lastItemY+a.lastLineHeight+a.titleHeight;h=a.handleOverflow(h);if(l||m){g+=k;h+=k;if(i){if(g>0&&h>0)i[i.isNew?"attr":"animate"](i.crisp(null,null,null,g,h)),i.isNew=!1}else a.box=i=c.rect(0,0,g,h,j.borderRadius,l||0).attr({stroke:j.borderColor, +"stroke-width":l||0,fill:m||S}).add(d).shadow(j.shadow),i.isNew=!0;i[f?"show":"hide"]()}a.legendWidth=g;a.legendHeight=h;n(e,function(b){a.positionItem(b)});f&&d.align(r({width:g,height:h},j),!0,"spacingBox");b.isResizing||this.positionCheckboxes()},handleOverflow:function(a){var b=this,c=this.chart,d=c.renderer,e=this.options,f=e.y,f=c.spacingBox.height+(e.verticalAlign==="top"?-f:f)-this.padding,g=e.maxHeight,h=this.clipRect,i=e.navigation,j=o(i.animation,!0),k=i.arrowSize||12,l=this.nav;e.layout=== +"horizontal"&&(f/=2);g&&(f=I(f,g));if(a>f&&!e.useHTML){this.clipHeight=c=f-20-this.titleHeight;this.pageCount=xa(a/c);this.currentPage=o(this.currentPage,1);this.fullHeight=a;if(!h)h=b.clipRect=d.clipRect(0,0,9999,0),b.contentGroup.clip(h);h.attr({height:c});if(!l)this.nav=l=d.g().attr({zIndex:1}).add(this.group),this.up=d.symbol("triangle",0,0,k,k).on("click",function(){b.scroll(-1,j)}).add(l),this.pager=d.text("",15,10).css(i.style).add(l),this.down=d.symbol("triangle-down",0,0,k,k).on("click", +function(){b.scroll(1,j)}).add(l);b.scroll(0);a=f}else if(l)h.attr({height:c.chartHeight}),l.hide(),this.scrollGroup.attr({translateY:1}),this.clipHeight=0;return a},scroll:function(a,b){var c=this.pageCount,d=this.currentPage+a,e=this.clipHeight,f=this.options.navigation,g=f.activeColor,h=f.inactiveColor,f=this.pager,i=this.padding;d>c&&(d=c);if(d>0)b!==w&&La(b,this.chart),this.nav.attr({translateX:i,translateY:e+7+this.titleHeight,visibility:"visible"}),this.up.attr({fill:d===1?h:g}).css({cursor:d=== +1?"default":"pointer"}),f.attr({text:d+"/"+this.pageCount}),this.down.attr({x:18+this.pager.getBBox().width,fill:d===c?h:g}).css({cursor:d===c?"default":"pointer"}),e=-I(e*(d-1),this.fullHeight-e+i)+1,this.scrollGroup.animate({translateY:e}),f.attr({text:d+"/"+c}),this.currentPage=d,this.positionCheckboxes(e)}};/Trident.*?11\.0/.test(oa)&&mb(eb.prototype,"positionItem",function(a,b){var c=this;setTimeout(function(){a.call(c,b)})});yb.prototype={init:function(a,b){var c,d=a.series;a.series=null;c= +x(M,a);c.series=a.series=d;d=c.chart;this.margin=this.splashArray("margin",d);this.spacing=this.splashArray("spacing",d);var e=d.events;this.bounds={h:{},v:{}};this.callback=b;this.isResizing=0;this.options=c;this.axes=[];this.series=[];this.hasCartesianSeries=d.showAxes;var f=this,g;f.index=Ga.length;Ga.push(f);d.reflow!==!1&&J(f,"load",function(){f.initReflow()});if(e)for(g in e)J(f,g,e[g]);f.xAxis=[];f.yAxis=[];f.animation=$?!1:o(d.animation,!0);f.pointCount=0;f.counters=new Jb;f.firstRender()}, +initSeries:function(a){var b=this.options.chart;(b=W[a.type||b.type||b.defaultSeriesType])||ka(17,!0);b=new b;b.init(this,a);return b},addSeries:function(a,b,c){var d,e=this;a&&(b=o(b,!0),z(e,"addSeries",{options:a},function(){d=e.initSeries(a);e.isDirtyLegend=!0;e.linkSeries();b&&e.redraw(c)}));return d},addAxis:function(a,b,c,d){var e=b?"xAxis":"yAxis",f=this.options;new db(this,x(a,{index:this[e].length,isX:b}));f[e]=ja(f[e]||{});f[e].push(a);o(c,!0)&&this.redraw(d)},isInsidePlot:function(a,b, +c){var d=c?b:a,a=c?a:b;return d>=0&&d<=this.plotWidth&&a>=0&&a<=this.plotHeight},adjustTickAmounts:function(){this.options.chart.alignTicks!==!1&&n(this.axes,function(a){a.adjustTickAmount()});this.maxTicks=null},redraw:function(a){var b=this.axes,c=this.series,d=this.pointer,e=this.legend,f=this.isDirtyLegend,g,h,i=this.isDirtyBox,j=c.length,k=j,l=this.renderer,m=l.isHidden(),p=[];La(a,this);m&&this.cloneRenderTo();for(this.layOutTitles();k--;)if(a=c[k],a.options.stacking&&(g=!0,a.isDirty)){h=!0; +break}if(h)for(k=j;k--;)if(a=c[k],a.options.stacking)a.isDirty=!0;n(c,function(a){a.isDirty&&a.options.legendType==="point"&&(f=!0)});if(f&&e.options.enabled)e.render(),this.isDirtyLegend=!1;g&&this.getStacks();if(this.hasCartesianSeries){if(!this.isResizing)this.maxTicks=null,n(b,function(a){a.setScale()});this.adjustTickAmounts();this.getMargins();n(b,function(a){a.isDirty&&(i=!0)});n(b,function(a){if(a.isDirtyExtremes)a.isDirtyExtremes=!1,p.push(function(){z(a,"afterSetExtremes",r(a.eventArgs, +a.getExtremes()));delete a.eventArgs});(i||g)&&a.redraw()})}i&&this.drawChartBox();n(c,function(a){a.isDirty&&a.visible&&(!a.isCartesian||a.xAxis)&&a.redraw()});d&&d.reset&&d.reset(!0);l.draw();z(this,"redraw");m&&this.cloneRenderTo(!0);n(p,function(a){a.call()})},showLoading:function(a){var b=this.options,c=this.loadingDiv,d=b.loading;if(!c)this.loadingDiv=c=U(Ea,{className:"highcharts-loading"},r(d.style,{zIndex:10,display:S}),this.container),this.loadingSpan=U("span",null,d.labelStyle,c);this.loadingSpan.innerHTML= +a||b.lang.loading;if(!this.loadingShown)K(c,{opacity:0,display:"",left:this.plotLeft+"px",top:this.plotTop+"px",width:this.plotWidth+"px",height:this.plotHeight+"px"}),Bb(c,{opacity:d.style.opacity},{duration:d.showDuration||0}),this.loadingShown=!0},hideLoading:function(){var a=this.options,b=this.loadingDiv;b&&Bb(b,{opacity:0},{duration:a.loading.hideDuration||100,complete:function(){K(b,{display:S})}});this.loadingShown=!1},get:function(a){var b=this.axes,c=this.series,d,e;for(d=0;dI(k.dataMin,k.min)&&i=18&&a<=25&&(a=15);c&&(c.css({width:(d.width||f)+"px"}).align(r({y:a+e.margin},d),!1,"spacingBox"),!d.floating&&!d.verticalAlign&&(a=xa(a+c.getBBox().height)));this.titleOffset= +a},getChartSize:function(){var a=this.options.chart,b=this.renderToClone||this.renderTo;this.containerWidth=jb(b,"width");this.containerHeight=jb(b,"height");this.chartWidth=s(0,a.width||this.containerWidth||600);this.chartHeight=s(0,o(a.height,this.containerHeight>19?this.containerHeight:400))},cloneRenderTo:function(a){var b=this.renderToClone,c=this.container;a?b&&(this.renderTo.appendChild(c),Ta(b),delete this.renderToClone):(c&&c.parentNode===this.renderTo&&this.renderTo.removeChild(c),this.renderToClone= +b=this.renderTo.cloneNode(0),K(b,{position:"absolute",top:"-9999px",display:"block"}),y.body.appendChild(b),c&&b.appendChild(c))},getContainer:function(){var a,b=this.options.chart,c,d,e;this.renderTo=a=b.renderTo;e="highcharts-"+zb++;if(ea(a))this.renderTo=a=y.getElementById(a);a||ka(13,!0);c=C(v(a,"data-highcharts-chart"));!isNaN(c)&&Ga[c]&&Ga[c].destroy();v(a,"data-highcharts-chart",this.index);a.innerHTML="";a.offsetWidth||this.cloneRenderTo();this.getChartSize();c=this.chartWidth;d=this.chartHeight; +this.container=a=U(Ea,{className:"highcharts-container"+(b.className?" "+b.className:""),id:e},r({position:"relative",overflow:"hidden",width:c+"px",height:d+"px",textAlign:"left",lineHeight:"normal",zIndex:0,"-webkit-tap-highlight-color":"rgba(0,0,0,0)"},b.style),this.renderToClone||a);this._cursor=a.style.cursor;this.renderer=b.forExport?new Ha(a,c,d,!0):new Va(a,c,d);$&&this.renderer.create(this,a,c,d)},getMargins:function(){var a=this.spacing,b,c=this.legend,d=this.margin,e=this.options.legend, +f=o(e.margin,10),g=e.x,h=e.y,i=e.align,j=e.verticalAlign,k=this.titleOffset;this.resetMargins();b=this.axisOffset;if(k&&!u(d[0]))this.plotTop=s(this.plotTop,k+this.options.title.margin+a[0]);if(c.display&&!e.floating)if(i==="right"){if(!u(d[1]))this.marginRight=s(this.marginRight,c.legendWidth-g+f+a[1])}else if(i==="left"){if(!u(d[3]))this.plotLeft=s(this.plotLeft,c.legendWidth+g+f+a[3])}else if(j==="top"){if(!u(d[0]))this.plotTop=s(this.plotTop,c.legendHeight+h+f+a[0])}else if(j==="bottom"&&!u(d[2]))this.marginBottom= +s(this.marginBottom,c.legendHeight-h+f+a[2]);this.extraBottomMargin&&(this.marginBottom+=this.extraBottomMargin);this.extraTopMargin&&(this.plotTop+=this.extraTopMargin);this.hasCartesianSeries&&n(this.axes,function(a){a.getOffset()});u(d[3])||(this.plotLeft+=b[3]);u(d[0])||(this.plotTop+=b[0]);u(d[2])||(this.marginBottom+=b[2]);u(d[1])||(this.marginRight+=b[1]);this.setChartSize()},initReflow:function(){function a(a){var g=c.width||jb(d,"width"),h=c.height||jb(d,"height"),a=a?a.target:O;if(!b.hasUserSize&& +g&&h&&(a===O||a===y)){if(g!==b.containerWidth||h!==b.containerHeight)clearTimeout(e),b.reflowTimeout=e=setTimeout(function(){if(b.container)b.setSize(g,h,!1),b.hasUserSize=null},100);b.containerWidth=g;b.containerHeight=h}}var b=this,c=b.options.chart,d=b.renderTo,e;b.reflow=a;J(O,"resize",a);J(b,"destroy",function(){aa(O,"resize",a)})},setSize:function(a,b,c){var d=this,e,f,g;d.isResizing+=1;g=function(){d&&z(d,"endResize",null,function(){d.isResizing-=1})};La(c,d);d.oldChartHeight=d.chartHeight; +d.oldChartWidth=d.chartWidth;if(u(a))d.chartWidth=e=s(0,t(a)),d.hasUserSize=!!e;if(u(b))d.chartHeight=f=s(0,t(b));K(d.container,{width:e+"px",height:f+"px"});d.setChartSize(!0);d.renderer.setSize(e,f,c);d.maxTicks=null;n(d.axes,function(a){a.isDirty=!0;a.setScale()});n(d.series,function(a){a.isDirty=!0});d.isDirtyLegend=!0;d.isDirtyBox=!0;d.getMargins();d.redraw(c);d.oldChartHeight=null;z(d,"resize");Fa===!1?g():setTimeout(g,Fa&&Fa.duration||500)},setChartSize:function(a){var b=this.inverted,c=this.renderer, +d=this.chartWidth,e=this.chartHeight,f=this.options.chart,g=this.spacing,h=this.clipOffset,i,j,k,l;this.plotLeft=i=t(this.plotLeft);this.plotTop=j=t(this.plotTop);this.plotWidth=k=s(0,t(d-i-this.marginRight));this.plotHeight=l=s(0,t(e-j-this.marginBottom));this.plotSizeX=b?l:k;this.plotSizeY=b?k:l;this.plotBorderWidth=f.plotBorderWidth||0;this.spacingBox=c.spacingBox={x:g[3],y:g[0],width:d-g[3]-g[1],height:e-g[0]-g[2]};this.plotBox=c.plotBox={x:i,y:j,width:k,height:l};d=2*P(this.plotBorderWidth/2); +b=xa(s(d,h[3])/2);c=xa(s(d,h[0])/2);this.clipBox={x:b,y:c,width:P(this.plotSizeX-s(d,h[1])/2-b),height:P(this.plotSizeY-s(d,h[2])/2-c)};a||n(this.axes,function(a){a.setAxisSize();a.setAxisTranslation()})},resetMargins:function(){var a=this.spacing,b=this.margin;this.plotTop=o(b[0],a[0]);this.marginRight=o(b[1],a[1]);this.marginBottom=o(b[2],a[2]);this.plotLeft=o(b[3],a[3]);this.axisOffset=[0,0,0,0];this.clipOffset=[0,0,0,0]},drawChartBox:function(){var a=this.options.chart,b=this.renderer,c=this.chartWidth, +d=this.chartHeight,e=this.chartBackground,f=this.plotBackground,g=this.plotBorder,h=this.plotBGImage,i=a.borderWidth||0,j=a.backgroundColor,k=a.plotBackgroundColor,l=a.plotBackgroundImage,m=a.plotBorderWidth||0,p,q=this.plotLeft,o=this.plotTop,n=this.plotWidth,s=this.plotHeight,t=this.plotBox,u=this.clipRect,r=this.clipBox;p=i+(a.shadow?8:0);if(i||j)if(e)e.animate(e.crisp(null,null,null,c-p,d-p));else{e={fill:j||S};if(i)e.stroke=a.borderColor,e["stroke-width"]=i;this.chartBackground=b.rect(p/2,p/ +2,c-p,d-p,a.borderRadius,i).attr(e).add().shadow(a.shadow)}if(k)f?f.animate(t):this.plotBackground=b.rect(q,o,n,s,0).attr({fill:k}).add().shadow(a.plotShadow);if(l)h?h.animate(t):this.plotBGImage=b.image(l,q,o,n,s).add();u?u.animate({width:r.width,height:r.height}):this.clipRect=b.clipRect(r);if(m)g?g.animate(g.crisp(null,q,o,n,s)):this.plotBorder=b.rect(q,o,n,s,0,-m).attr({stroke:a.plotBorderColor,"stroke-width":m,zIndex:1}).add();this.isDirtyBox=!1},propFromSeries:function(){var a=this,b=a.options.chart, +c,d=a.options.series,e,f;n(["inverted","angular","polar"],function(g){c=W[b.type||b.defaultSeriesType];f=a[g]||b[g]||c&&c.prototype[g];for(e=d&&d.length;!f&&e--;)(c=W[d[e].type])&&c.prototype[g]&&(f=!0);a[g]=f})},linkSeries:function(){var a=this,b=a.series;n(b,function(a){a.linkedSeries.length=0});n(b,function(b){var d=b.options.linkedTo;if(ea(d)&&(d=d===":previous"?a.series[b.index-1]:a.get(d)))d.linkedSeries.push(b),b.linkedParent=d})},render:function(){var a=this,b=a.axes,c=a.renderer,d=a.options, +e=d.labels,f=d.credits,g;a.setTitle();a.legend=new eb(a,d.legend);a.getStacks();n(b,function(a){a.setScale()});a.getMargins();a.maxTicks=null;n(b,function(a){a.setTickPositions(!0);a.setMaxTicks()});a.adjustTickAmounts();a.getMargins();a.drawChartBox();a.hasCartesianSeries&&n(b,function(a){a.render()});if(!a.seriesGroup)a.seriesGroup=c.g("series-group").attr({zIndex:3}).add();n(a.series,function(a){a.translate();a.setTooltipPoints();a.render()});e.items&&n(e.items,function(b){var d=r(e.style,b.style), +f=C(d.left)+a.plotLeft,g=C(d.top)+a.plotTop+12;delete d.left;delete d.top;c.text(b.html,f,g).attr({zIndex:2}).css(d).add()});if(f.enabled&&!a.credits)g=f.href,a.credits=c.text(f.text,0,0).on("click",function(){if(g)location.href=g}).attr({align:f.position.align,zIndex:8}).css(f.style).add().align(f.position);a.hasRendered=!0},destroy:function(){var a=this,b=a.axes,c=a.series,d=a.container,e,f=d&&d.parentNode;z(a,"destroy");Ga[a.index]=w;a.renderTo.removeAttribute("data-highcharts-chart");aa(a);for(e= +b.length;e--;)b[e]=b[e].destroy();for(e=c.length;e--;)c[e]=c[e].destroy();n("title,subtitle,chartBackground,plotBackground,plotBGImage,plotBorder,seriesGroup,clipRect,credits,pointer,scroller,rangeSelector,legend,resetZoomButton,tooltip,renderer".split(","),function(b){var c=a[b];c&&c.destroy&&(a[b]=c.destroy())});if(d)d.innerHTML="",aa(d),f&&Ta(d);for(e in a)delete a[e]},isReadyToRender:function(){var a=this;return!Z&&O==O.top&&y.readyState!=="complete"||$&&!O.canvg?($?Tb.push(function(){a.firstRender()}, +a.options.global.canvasToolsURL):y.attachEvent("onreadystatechange",function(){y.detachEvent("onreadystatechange",a.firstRender);y.readyState==="complete"&&a.firstRender()}),!1):!0},firstRender:function(){var a=this,b=a.options,c=a.callback;if(a.isReadyToRender())a.getContainer(),z(a,"init"),a.resetMargins(),a.setChartSize(),a.propFromSeries(),a.getAxes(),n(b.series||[],function(b){a.initSeries(b)}),a.linkSeries(),z(a,"beforeRender"),a.pointer=new xb(a,b),a.render(),a.renderer.draw(),c&&c.apply(a, +[a]),n(a.callbacks,function(b){b.apply(a,[a])}),a.cloneRenderTo(!0),z(a,"load")},splashArray:function(a,b){var c=b[a],c=T(c)?c:[c,c,c,c];return[o(b[a+"Top"],c[0]),o(b[a+"Right"],c[1]),o(b[a+"Bottom"],c[2]),o(b[a+"Left"],c[3])]}};yb.prototype.callbacks=[];var Pa=function(){};Pa.prototype={init:function(a,b,c){this.series=a;this.applyOptions(b,c);this.pointAttr={};if(a.options.colorByPoint&&(b=a.options.colors||a.chart.options.colors,this.color=this.color||b[a.colorCounter++],a.colorCounter===b.length))a.colorCounter= +0;a.chart.pointCount++;return this},applyOptions:function(a,b){var c=this.series,d=c.pointValKey,a=Pa.prototype.optionsToObject.call(this,a);r(this,a);this.options=this.options?r(this.options,a):a;if(d)this.y=this[d];if(this.x===w&&c)this.x=b===w?c.autoIncrement():b;return this},optionsToObject:function(a){var b,c=this.series,d=c.pointArrayMap||["y"],e=d.length,f=0,g=0;if(typeof a==="number"||a===null)b={y:a};else if(Ia(a)){b={};if(a.length>e){c=typeof a[0];if(c==="string")b.name=a[0];else if(c=== +"number")b.x=a[0];f++}for(;ga+1&&b.push(d.slice(a+1,g)),a=g):g===e-1&&b.push(d.slice(a+1,g+1))});this.segments=b},setOptions:function(a){var b=this.chart.options,c=b.plotOptions,d=c[this.type];this.userOptions=a;a=x(d,c.series, +a);this.tooltipOptions=x(b.tooltip,a.tooltip);d.marker===null&&delete a.marker;return a},getColor:function(){var a=this.options,b=this.userOptions,c=this.chart.options.colors,d=this.chart.counters,e;e=a.color||Y[this.type].color;if(!e&&!a.colorByPoint)u(b._colorIndex)?a=b._colorIndex:(b._colorIndex=d.color,a=d.color++),e=c[a];this.color=e;d.wrapColor(c.length)},getSymbol:function(){var a=this.userOptions,b=this.options.marker,c=this.chart,d=c.options.symbols,c=c.counters;this.symbol=b.symbol;if(!this.symbol)u(a._symbolIndex)? +a=a._symbolIndex:(a._symbolIndex=c.symbol,a=c.symbol++),this.symbol=d[a];if(/^url/.test(this.symbol))b.radius=0;c.wrapSymbol(d.length)},drawLegendSymbol:function(a){var b=this.options,c=b.marker,d=a.options,e;e=d.symbolWidth;var f=this.chart.renderer,g=this.legendGroup,a=a.baseline-t(f.fontMetrics(d.itemStyle.fontSize).b*0.3);if(b.lineWidth){d={"stroke-width":b.lineWidth};if(b.dashStyle)d.dashstyle=b.dashStyle;this.legendLine=f.path(["M",0,a,"L",e,a]).attr(d).add(g)}if(c&&c.enabled)b=c.radius,this.legendSymbol= +e=f.symbol(this.symbol,e/2-b,a-b,2*b,2*b).add(g),e.isMarker=!0},addPoint:function(a,b,c,d){var e=this.options,f=this.data,g=this.graph,h=this.area,i=this.chart,j=this.xData,k=this.yData,l=this.zData,m=this.names,p=g&&g.shift||0,q=e.data,s;La(d,i);c&&n([g,h,this.graphNeg,this.areaNeg],function(a){if(a)a.shift=p+1});if(h)h.isArea=!0;b=o(b,!0);d={series:this};this.pointClass.prototype.applyOptions.apply(d,[a]);g=d.x;h=j.length;if(this.requireSorting&&gg;)h--;j.splice(h,0,g); +k.splice(h,0,this.toYData?this.toYData(d):d.y);l.splice(h,0,d.z);if(m)m[g]=d.name;q.splice(h,0,a);s&&(this.data.splice(h,0,null),this.processData());e.legendType==="point"&&this.generatePoints();c&&(f[0]&&f[0].remove?f[0].remove(!1):(f.shift(),j.shift(),k.shift(),l.shift(),q.shift()));this.isDirtyData=this.isDirty=!0;b&&(this.getAttribs(),i.redraw())},setData:function(a,b){var c=this.points,d=this.options,e=this.chart,f=null,g=this.xAxis,h=g&&g.categories&&!g.categories.length?[]:null,i;this.xIncrement= +null;this.pointRange=g&&g.categories?1:d.pointRange;this.colorCounter=0;var j=[],k=[],l=[],m=a?a.length:[];i=o(d.turboThreshold,1E3);var p=this.pointArrayMap,p=p&&p.length,q=!!this.toYData;if(i&&m>i){for(i=0;f===null&&ij||this.forceCrop))if(a=h.min,h=h.max,b[d-1]h)b=[],c=[];else if(b[0]h)e=this.cropData(this.xData,this.yData,a,h),b=e.xData,c=e.yData,e=e.start, +f=!0;for(h=b.length-1;h>=0;h--)d=b[h]-b[h-1],d>0&&(g===w||d=c){f=s(0,i-h);break}for(;id){g=i+h;break}return{xData:a.slice(f,g),yData:b.slice(f,g),start:f,end:g}},generatePoints:function(){var a=this.options.data, +b=this.data,c,d=this.processedXData,e=this.processedYData,f=this.pointClass,g=d.length,h=this.cropStart||0,i,j=this.hasGroupedData,k,l=[],m;if(!b&&!j)b=[],b.length=a.length,b=this.data=b;for(m=0;m0),j=this.getExtremesFromAll||this.cropped||(b[l+1]||j)>=h&&(b[l-1]||j)<=g,i&&j)if(i=k.length)for(;i--;)k[i]!==null&&(e[f++]=k[i]);else e[f++]=k;this.dataMin=o(void 0,Ja(e));this.dataMax=o(void 0,va(e))},translate:function(){this.processedXData|| +this.processData();this.generatePoints();for(var a=this.options,b=a.stacking,c=this.xAxis,d=c.categories,e=this.yAxis,f=this.points,g=f.length,h=!!this.modifyValue,i=a.pointPlacement,j=i==="between"||sa(i),k=a.threshold,a=0;a=f.min&&c<=f.max){h=b[i+1];c=d===w?0:d+1;for(d=b[i+1]?I(s(0,P((e.clientX+(h?h.wrappedClientX||h.clientX:g))/2)),g):g;c>=0&&c<=d;)j[c++]=e}this.tooltipPoints=j}},tooltipHeaderFormatter:function(a){var b=this.tooltipOptions,c=b.xDateFormat, +d=b.dateTimeLabelFormats,e=this.xAxis,f=e&&e.options.type==="datetime",b=b.headerFormat,e=e&&e.closestPointRange,g;if(f&&!c)if(e)for(g in D){if(D[g]>=e){c=d[g];break}}else c=d.day;f&&c&&sa(a.key)&&(b=b.replace("{point.key}","{point.key:"+c+"}"));return Ca(b,{point:a,series:this})},onMouseOver:function(){var a=this.chart,b=a.hoverSeries;if(b&&b!==this)b.onMouseOut();this.options.events.mouseOver&&z(this,"mouseOver");this.setState("hover");a.hoverSeries=this},onMouseOut:function(){var a=this.options, +b=this.chart,c=b.tooltip,d=b.hoverPoint;if(d)d.onMouseOut();this&&a.events.mouseOut&&z(this,"mouseOut");c&&!a.stickyTracking&&(!c.shared||this.noSharedTooltip)&&c.hide();this.setState();b.hoverSeries=null},animate:function(a){var b=this,c=b.chart,d=c.renderer,e;e=b.options.animation;var f=c.clipBox,g=c.inverted,h;if(e&&!T(e))e=Y[b.type].animation;h="_sharedClip"+e.duration+e.easing;if(a)a=c[h],e=c[h+"m"],a||(c[h]=a=d.clipRect(r(f,{width:0})),c[h+"m"]=e=d.clipRect(-99,g?-c.plotLeft:-c.plotTop,99,g? +c.chartWidth:c.chartHeight)),b.group.clip(a),b.markerGroup.clip(e),b.sharedClipKey=h;else{if(a=c[h])a.animate({width:c.plotSizeX},e),c[h+"m"].animate({width:c.plotSizeX+99},e);b.animate=null;b.animationTimeout=setTimeout(function(){b.afterAnimate()},e.duration)}},afterAnimate:function(){var a=this.chart,b=this.sharedClipKey,c=this.group;c&&this.options.clip!==!1&&(c.clip(a.clipRect),this.markerGroup.clip());setTimeout(function(){b&&a[b]&&(a[b]=a[b].destroy(),a[b+"m"]=a[b+"m"].destroy())},100)},drawPoints:function(){var a, +b=this.points,c=this.chart,d,e,f,g,h,i,j,k,l=this.options.marker,m,p=this.markerGroup;if(l.enabled||this._hasPointMarkers)for(f=b.length;f--;)if(g=b[f],d=P(g.plotX),e=g.plotY,k=g.graphic,i=g.marker||{},a=l.enabled&&i.enabled===w||i.enabled,m=c.isInsidePlot(t(d),e,c.inverted),a&&e!==w&&!isNaN(e)&&g.y!==null)if(a=g.pointAttr[g.selected?"select":""],h=a.r,i=o(i.symbol,this.symbol),j=i.indexOf("url")===0,k)k.attr({visibility:m?Z?"inherit":"visible":"hidden"}).animate(r({x:d-h,y:e-h},k.symbolName?{width:2* +h,height:2*h}:{}));else{if(m&&(h>0||j))g.graphic=c.renderer.symbol(i,d-h,e-h,2*h,2*h).attr(a).add(p)}else if(k)g.graphic=k.destroy()},convertAttribs:function(a,b,c,d){var e=this.pointAttrToOptions,f,g,h={},a=a||{},b=b||{},c=c||{},d=d||{};for(f in e)g=e[f],h[f]=o(a[g],b[f],c[f],d[f]);return h},getAttribs:function(){var a=this,b=a.options,c=Y[a.type].marker?b.marker:b,d=c.states,e=d.hover,f,g=a.color,h={stroke:g,fill:g},i=a.points||[],j=[],k,l=a.pointAttrToOptions,m=b.negativeColor,p=c.lineColor,q; +b.marker?(e.radius=e.radius||c.radius+2,e.lineWidth=e.lineWidth||c.lineWidth+1):e.color=e.color||ra(e.color||g).brighten(e.brightness).get();j[""]=a.convertAttribs(c,h);n(["hover","select"],function(b){j[b]=a.convertAttribs(d[b],j[""])});a.pointAttr=j;for(g=i.length;g--;){h=i[g];if((c=h.options&&h.options.marker||h.options)&&c.enabled===!1)c.radius=0;if(h.negative&&m)h.color=h.fillColor=m;f=b.colorByPoint||h.color;if(h.options)for(q in l)u(c[l[q]])&&(f=!0);if(f){c=c||{};k=[];d=c.states||{};f=d.hover= +d.hover||{};if(!b.marker)f.color=ra(f.color||h.color).brighten(f.brightness||e.brightness).get();k[""]=a.convertAttribs(r({color:h.color,fillColor:h.color,lineColor:p===null?h.color:w},c),j[""]);k.hover=a.convertAttribs(d.hover,j.hover,k[""]);k.select=a.convertAttribs(d.select,j.select,k[""])}else k=j;h.pointAttr=k}},update:function(a,b){var c=this.chart,d=this.type,e=W[d].prototype,f,a=x(this.userOptions,{animation:!1,index:this.index,pointStart:this.xData[0]},{data:this.options.data},a);this.remove(!1); +for(f in e)e.hasOwnProperty(f)&&(this[f]=w);r(this,W[a.type||d].prototype);this.init(c,a);o(b,!0)&&c.redraw(!1)},destroy:function(){var a=this,b=a.chart,c=/AppleWebKit\/533/.test(oa),d,e,f=a.data||[],g,h,i;z(a,"destroy");aa(a);n(["xAxis","yAxis"],function(b){if(i=a[b])ga(i.series,a),i.isDirty=i.forceRedraw=!0,i.stacks={}});a.legendItem&&a.chart.legend.destroyItem(a);for(e=f.length;e--;)(g=f[e])&&g.destroy&&g.destroy();a.points=null;clearTimeout(a.animationTimeout);n("area,graph,dataLabelsGroup,group,markerGroup,tracker,graphNeg,areaNeg,posClip,negClip".split(","), +function(b){a[b]&&(d=c&&b==="group"?"hide":"destroy",a[b][d]())});if(b.hoverSeries===a)b.hoverSeries=null;ga(b.series,a);for(h in a)delete a[h]},drawDataLabels:function(){var a=this,b=a.options.dataLabels,c=a.points,d,e,f,g;if(b.enabled||a._hasPointLabels)a.dlProcessOptions&&a.dlProcessOptions(b),g=a.plotGroup("dataLabelsGroup","data-labels",a.visible?"visible":"hidden",b.zIndex||6),e=b,n(c,function(c){var i,j=c.dataLabel,k,l,m=c.connector,p=!0;d=c.options&&c.options.dataLabels;i=o(d&&d.enabled,e.enabled); +if(j&&!i)c.dataLabel=j.destroy();else if(i){b=x(e,d);i=b.rotation;k=c.getLabelConfig();f=b.format?Ca(b.format,k):b.formatter.call(k,b);b.style.color=o(b.color,b.style.color,a.color,"black");if(j)if(u(f))j.attr({text:f}),p=!1;else{if(c.dataLabel=j=j.destroy(),m)c.connector=m.destroy()}else if(u(f)){j={fill:b.backgroundColor,stroke:b.borderColor,"stroke-width":b.borderWidth,r:b.borderRadius||0,rotation:i,padding:b.padding,zIndex:1};for(l in j)j[l]===w&&delete j[l];j=c.dataLabel=a.chart.renderer[i?"text": +"label"](f,0,-999,null,null,null,b.useHTML).attr(j).css(b.style).add(g).shadow(b.shadow)}j&&a.alignDataLabel(c,j,b,null,p)}})},alignDataLabel:function(a,b,c,d,e){var f=this.chart,g=f.inverted,h=o(a.plotX,-999),i=o(a.plotY,-999),j=b.getBBox();if(a=this.visible&&f.isInsidePlot(a.plotX,a.plotY,g))d=r({x:g?f.plotWidth-i:h,y:t(g?f.plotHeight-h:i),width:0,height:0},d),r(c,{width:j.width,height:j.height}),c.rotation?(g={align:c.align,x:d.x+c.x+d.width/2,y:d.y+c.y+d.height/2},b[e?"attr":"animate"](g)):(b.align(c, +null,d),g=b.alignAttr,o(c.overflow,"justify")==="justify"?this.justifyDataLabel(b,c,g,j,d,e):o(c.crop,!0)&&(a=f.isInsidePlot(g.x,g.y)&&f.isInsidePlot(g.x+j.width,g.y+j.height)));a||b.attr({y:-999})},justifyDataLabel:function(a,b,c,d,e,f){var g=this.chart,h=b.align,i=b.verticalAlign,j,k;j=c.x;if(j<0)h==="right"?b.align="left":b.x=-j,k=!0;j=c.x+d.width;if(j>g.plotWidth)h==="left"?b.align="right":b.x=g.plotWidth-j,k=!0;j=c.y;if(j<0)i==="bottom"?b.verticalAlign="top":b.y=-j,k=!0;j=c.y+d.height;if(j>g.plotHeight)i=== +"top"?b.verticalAlign="bottom":b.y=g.plotHeight-j,k=!0;if(k)a.placed=!f,a.align(b,null,e)},getSegmentPath:function(a){var b=this,c=[],d=b.options.step;n(a,function(e,f){var g=e.plotX,h=e.plotY,i;b.getPointSpline?c.push.apply(c,b.getPointSpline(a,e,f)):(c.push(f?"L":"M"),d&&f&&(i=a[f-1],d==="right"?c.push(i.plotX,h):d==="center"?c.push((i.plotX+g)/2,i.plotY,(i.plotX+g)/2,h):c.push(g,i.plotY)),c.push(e.plotX,e.plotY))});return c},getGraphPath:function(){var a=this,b=[],c,d=[];n(a.segments,function(e){c= +a.getSegmentPath(e);e.length>1?b=b.concat(c):d.push(e[0])});a.singlePoints=d;return a.graphPath=b},drawGraph:function(){var a=this,b=this.options,c=[["graph",b.lineColor||this.color]],d=b.lineWidth,e=b.dashStyle,f=this.getGraphPath(),g=b.negativeColor;g&&c.push(["graphNeg",g]);n(c,function(c,g){var j=c[0],k=a[j];if(k)Wa(k),k.animate({d:f});else if(d&&f.length)k={stroke:c[1],"stroke-width":d,zIndex:1},e?k.dashstyle=e:k["stroke-linecap"]=k["stroke-linejoin"]="round",a[j]=a.chart.renderer.path(f).attr(k).add(a.group).shadow(!g&& +b.shadow)})},clipNeg:function(){var a=this.options,b=this.chart,c=b.renderer,d=a.negativeColor||a.negativeFillColor,e,f=this.graph,g=this.area,h=this.posClip,i=this.negClip;e=b.chartWidth;var j=b.chartHeight,k=s(e,j),l=this.yAxis;if(d&&(f||g)){d=t(l.toPixels(a.threshold||0,!0));a={x:0,y:0,width:k,height:d};k={x:0,y:d,width:k,height:k};if(b.inverted)a.height=k.y=b.plotWidth-d,c.isVML&&(a={x:b.plotWidth-d-b.plotLeft,y:0,width:e,height:j},k={x:d+b.plotLeft-e,y:0,width:b.plotLeft+d,height:e});l.reversed? +(b=k,e=a):(b=a,e=k);h?(h.animate(b),i.animate(e)):(this.posClip=h=c.clipRect(b),this.negClip=i=c.clipRect(e),f&&this.graphNeg&&(f.clip(h),this.graphNeg.clip(i)),g&&(g.clip(h),this.areaNeg.clip(i)))}},invertGroups:function(){function a(){var a={width:b.yAxis.len,height:b.xAxis.len};n(["group","markerGroup"],function(c){b[c]&&b[c].attr(a).invert()})}var b=this,c=b.chart;if(b.xAxis)J(c,"resize",a),J(b,"destroy",function(){aa(c,"resize",a)}),a(),b.invertGroups=a},plotGroup:function(a,b,c,d,e){var f=this[a], +g=!f;g&&(this[a]=f=this.chart.renderer.g(b).attr({visibility:c,zIndex:d||0.1}).add(e));f[g?"attr":"animate"](this.getPlotBox());return f},getPlotBox:function(){return{translateX:this.xAxis?this.xAxis.left:this.chart.plotLeft,translateY:this.yAxis?this.yAxis.top:this.chart.plotTop,scaleX:1,scaleY:1}},render:function(){var a=this.chart,b,c=this.options,d=c.animation&&!!this.animate&&a.renderer.isSVG,e=this.visible?"visible":"hidden",f=c.zIndex,g=this.hasRendered,h=a.seriesGroup;b=this.plotGroup("group", +"series",e,f,h);this.markerGroup=this.plotGroup("markerGroup","markers",e,f,h);d&&this.animate(!0);this.getAttribs();b.inverted=this.isCartesian?a.inverted:!1;this.drawGraph&&(this.drawGraph(),this.clipNeg());this.drawDataLabels();this.drawPoints();this.options.enableMouseTracking!==!1&&this.drawTracker();a.inverted&&this.invertGroups();c.clip!==!1&&!this.sharedClipKey&&!g&&b.clip(a.clipRect);d?this.animate():g||this.afterAnimate();this.isDirty=this.isDirtyData=!1;this.hasRendered=!0},redraw:function(){var a= +this.chart,b=this.isDirtyData,c=this.group,d=this.xAxis,e=this.yAxis;c&&(a.inverted&&c.attr({width:a.plotWidth,height:a.plotHeight}),c.animate({translateX:o(d&&d.left,a.plotLeft),translateY:o(e&&e.top,a.plotTop)}));this.translate();this.setTooltipPoints(!0);this.render();b&&z(this,"updatedData")},setState:function(a){var b=this.options,c=this.graph,d=this.graphNeg,e=b.states,b=b.lineWidth,a=a||"";if(this.state!==a)this.state=a,e[a]&&e[a].enabled===!1||(a&&(b=e[a].lineWidth||b+1),c&&!c.dashstyle&& +(a={"stroke-width":b},c.attr(a),d&&d.attr(a)))},setVisible:function(a,b){var c=this,d=c.chart,e=c.legendItem,f,g=d.options.chart.ignoreHiddenSeries,h=c.visible;f=(c.visible=a=c.userOptions.visible=a===w?!h:a)?"show":"hide";n(["group","dataLabelsGroup","markerGroup","tracker"],function(a){if(c[a])c[a][f]()});if(d.hoverSeries===c)c.onMouseOut();e&&d.legend.colorizeItem(c,a);c.isDirty=!0;c.options.stacking&&n(d.series,function(a){if(a.options.stacking&&a.visible)a.isDirty=!0});n(c.linkedSeries,function(b){b.setVisible(a, +!1)});if(g)d.isDirtyBox=!0;b!==!1&&d.redraw();z(c,f)},show:function(){this.setVisible(!0)},hide:function(){this.setVisible(!1)},select:function(a){this.selected=a=a===w?!this.selected:a;if(this.checkbox)this.checkbox.checked=a;z(this,a?"select":"unselect")},drawTracker:function(){var a=this,b=a.options,c=b.trackByArea,d=[].concat(c?a.areaPath:a.graphPath),e=d.length,f=a.chart,g=f.pointer,h=f.renderer,i=f.options.tooltip.snap,j=a.tracker,k=b.cursor,l=k&&{cursor:k},k=a.singlePoints,m,p=function(){if(f.hoverSeries!== +a)a.onMouseOver()};if(e&&!c)for(m=e+1;m--;)d[m]==="M"&&d.splice(m+1,0,d[m+1]-i,d[m+2],"L"),(m&&d[m]==="M"||m===e)&&d.splice(m,0,"L",d[m-2]+i,d[m-1]);for(m=0;m=0;d--)g=o(a[d].yBottom,f),da&&i>e?(i=s(a,e),k=2*e-i):ig&&k>e?(k=s(g,e),i=2*e-k):kf?b-f:e-(d.translate(a.y,0,1,0,1)<=e?f:0)));a.barX=n;a.pointWidth=g;b=N(n)<0.5;u=t(n+u)+j;n=t(n)+j;u-=n;w=N(r)<0.5;c=t(r+c)+k;r=t(r)+k;c-=r;b&&(n+=1,u-=1);w&&(r-= +1,c+=1);a.shapeType="rect";a.shapeArgs={x:n,y:r,width:u,height:c}})},getSymbol:pa,drawLegendSymbol:G.prototype.drawLegendSymbol,drawGraph:pa,drawPoints:function(){var a=this,b=a.options,c=a.chart.renderer,d;n(a.points,function(e){var f=e.plotY,g=e.graphic;if(f!==w&&!isNaN(f)&&e.y!==null)d=e.shapeArgs,g?(Wa(g),g.animate(x(d))):e.graphic=c[e.shapeType](d).attr(e.pointAttr[e.selected?"select":""]).add(a.group).shadow(b.shadow,null,b.stacking&&!b.borderRadius);else if(g)e.graphic=g.destroy()})},drawTracker:function(){var a= +this,b=a.chart,c=b.pointer,d=a.options.cursor,e=d&&{cursor:d},f=function(c){var d=c.target,e;if(b.hoverSeries!==a)a.onMouseOver();for(;d&&!e;)e=d.point,d=d.parentNode;if(e!==w&&e!==b.hoverPoint)e.onMouseOver(c)};n(a.points,function(a){if(a.graphic)a.graphic.element.point=a;if(a.dataLabel)a.dataLabel.element.point=a});if(!a._hasTracking)n(a.trackerGroups,function(b){if(a[b]&&(a[b].addClass("highcharts-tracker").on("mouseover",f).on("mouseout",function(a){c.onTrackerMouseOut(a)}).css(e),ib))a[b].on("touchstart", +f)}),a._hasTracking=!0},alignDataLabel:function(a,b,c,d,e){var f=this.chart,g=f.inverted,h=a.dlBox||a.shapeArgs,i=a.below||a.plotY>o(this.translatedThreshold,f.plotSizeY),j=o(c.inside,!!this.options.stacking);if(h&&(d=x(h),g&&(d={x:f.plotWidth-d.y-d.height,y:f.plotHeight-d.x-d.width,width:d.height,height:d.width}),!j))g?(d.x+=i?0:d.width,d.width=0):(d.y+=i?d.height:0,d.height=0);c.align=o(c.align,!g||j?"center":i?"right":"left");c.verticalAlign=o(c.verticalAlign,g||j?"middle":i?"top":"bottom");Q.prototype.alignDataLabel.call(this, +a,b,c,d,e)},animate:function(a){var b=this.yAxis,c=this.options,d=this.chart.inverted,e={};if(Z)a?(e.scaleY=0.001,a=I(b.pos+b.len,s(b.pos,b.toPixels(c.threshold))),d?e.translateX=a-b.len:e.translateY=a,this.group.attr(e)):(e.scaleY=1,e[d?"translateX":"translateY"]=b.pos,this.group.animate(e,this.options.animation),this.animate=null)},remove:function(){var a=this,b=a.chart;b.hasRendered&&n(b.series,function(b){if(b.type===a.type)b.isDirty=!0});Q.prototype.remove.apply(a,arguments)}});W.column=F;Y.bar= +x(Y.column);ma=ha(F,{type:"bar",inverted:!0});W.bar=ma;Y.scatter=x(X,{lineWidth:0,tooltip:{headerFormat:'{series.name}
              ',pointFormat:"x: {point.x}
              y: {point.y}
              ",followPointer:!0},stickyTracking:!1});ma=ha(Q,{type:"scatter",sorted:!1,requireSorting:!1,noSharedTooltip:!0,trackerGroups:["markerGroup"],drawTracker:F.prototype.drawTracker,setTooltipPoints:pa});W.scatter=ma;Y.pie=x(X,{borderColor:"#FFFFFF",borderWidth:1, +center:[null,null],clip:!1,colorByPoint:!0,dataLabels:{distance:30,enabled:!0,formatter:function(){return this.point.name}},ignoreHiddenPoint:!0,legendType:"point",marker:null,size:null,showInLegend:!1,slicedOffset:10,states:{hover:{brightness:0.1,shadow:!1}},stickyTracking:!1,tooltip:{followPointer:!0}});X={type:"pie",isCartesian:!1,pointClass:ha(Pa,{init:function(){Pa.prototype.init.apply(this,arguments);var a=this,b;if(a.y<0)a.y=null;r(a,{visible:a.visible!==!1,name:o(a.name,"Slice")});b=function(b){a.slice(b.type=== +"select")};J(a,"select",b);J(a,"unselect",b);return a},setVisible:function(a){var b=this,c=b.series,d=c.chart,e;b.visible=b.options.visible=a=a===w?!b.visible:a;c.options.data[qa(b,c.data)]=b.options;e=a?"show":"hide";n(["graphic","dataLabel","connector","shadowGroup"],function(a){if(b[a])b[a][e]()});b.legendItem&&d.legend.colorizeItem(b,a);if(!c.isDirty&&c.options.ignoreHiddenPoint)c.isDirty=!0,d.redraw()},slice:function(a,b,c){var d=this.series;La(c,d.chart);o(b,!0);this.sliced=this.options.sliced= +a=u(a)?a:!this.sliced;d.options.data[qa(this,d.data)]=this.options;a=a?this.slicedTranslation:{translateX:0,translateY:0};this.graphic.animate(a);this.shadowGroup&&this.shadowGroup.animate(a)}}),requireSorting:!1,noSharedTooltip:!0,trackerGroups:["group","dataLabelsGroup"],pointAttrToOptions:{stroke:"borderColor","stroke-width":"borderWidth",fill:"color"},getColor:pa,animate:function(a){var b=this,c=b.points,d=b.startAngleRad;if(!a)n(c,function(a){var c=a.graphic,a=a.shapeArgs;c&&(c.attr({r:b.center[3]/ +2,start:d,end:d}),c.animate({r:a.r,start:a.start,end:a.end},b.options.animation))}),b.animate=null},setData:function(a,b){Q.prototype.setData.call(this,a,!1);this.processData();this.generatePoints();o(b,!0)&&this.chart.redraw()},generatePoints:function(){var a,b=0,c,d,e,f=this.options.ignoreHiddenPoint;Q.prototype.generatePoints.call(this);c=this.points;d=c.length;for(a=0;a0?e.y/b*100:0,e.total=b},getCenter:function(){var a= +this.options,b=this.chart,c=2*(a.slicedOffset||0),d,e=b.plotWidth-2*c,f=b.plotHeight-2*c,b=a.center,a=[o(b[0],"50%"),o(b[1],"50%"),a.size||"100%",a.innerSize||0],g=I(e,f),h;return Na(a,function(a,b){h=/%$/.test(a);d=b<2||b===2&&h;return(h?[e,f,g,g][b]*C(a)/100:a)+(d?c:0)})},translate:function(a){this.generatePoints();var b=0,c=this.options,d=c.slicedOffset,e=d+c.borderWidth,f,g,h,i=c.startAngle||0,j=this.startAngleRad=ya/180*(i-90),i=(this.endAngleRad=ya/180*((c.endAngle||i+360)-90))-j,k=this.points, +l=c.dataLabels.distance,c=c.ignoreHiddenPoint,m,n=k.length,o;if(!a)this.center=a=this.getCenter();this.getX=function(b,c){h=R.asin((b-a[1])/(a[2]/2+l));return a[0]+(c?-1:1)*V(h)*(a[2]/2+l)};for(m=0;m0.75*i&&(h-=2*ya);o.slicedTranslation={translateX:t(V(h)*d),translateY:t(ca(h)*d)};f=V(h)*a[2]/2;g=ca(h)*a[2]/2;o.tooltipPos= +[a[0]+f*0.7,a[1]+g*0.7];o.half=h<-ya/2||h>ya/2?1:0;o.angle=h;e=I(e,l/2);o.labelPos=[a[0]+f+V(h)*l,a[1]+g+ca(h)*l,a[0]+f+V(h)*e,a[1]+g+ca(h)*e,a[0]+f,a[1]+g,l<0?"center":o.half?"right":"left",h]}},setTooltipPoints:pa,drawGraph:null,drawPoints:function(){var a=this,b=a.chart.renderer,c,d,e=a.options.shadow,f,g;if(e&&!a.shadowGroup)a.shadowGroup=b.g("shadow").add(a.group);n(a.points,function(h){d=h.graphic;g=h.shapeArgs;f=h.shadowGroup;if(e&&!f)f=h.shadowGroup=b.g("shadow").add(a.shadowGroup);c=h.sliced? +h.slicedTranslation:{translateX:0,translateY:0};f&&f.attr(c);d?d.animate(r(g,c)):h.graphic=d=b.arc(g).setRadialReference(a.center).attr(h.pointAttr[h.selected?"select":""]).attr({"stroke-linejoin":"round"}).attr(c).add(a.group).shadow(e,f);h.visible===!1&&h.setVisible(!1)})},sortByAngle:function(a,b){a.sort(function(a,d){return a.angle!==void 0&&(d.angle-a.angle)*b})},drawDataLabels:function(){var a=this,b=a.data,c,d=a.chart,e=a.options.dataLabels,f=o(e.connectorPadding,10),g=o(e.connectorWidth,1), +h=d.plotWidth,d=d.plotHeight,i,j,k=o(e.softConnector,!0),l=e.distance,m=a.center,p=m[2]/2,q=m[1],u=l>0,r,w,v,x,C=[[],[]],y,z,E,H,B,D=[0,0,0,0],I=function(a,b){return b.y-a.y};if(a.visible&&(e.enabled||a._hasPointLabels)){Q.prototype.drawDataLabels.apply(a);n(b,function(a){a.dataLabel&&C[a.half].push(a)});for(H=0;!x&&b[H];)x=b[H]&&b[H].dataLabel&&(b[H].dataLabel.getBBox().height||21),H++;for(H=2;H--;){var b=[],K=[],G=C[H],J=G.length,F;a.sortByAngle(G,H-0.5);if(l>0){for(B=q-p-l;B<=q+p+l;B+=x)b.push(B); +w=b.length;if(J>w){c=[].concat(G);c.sort(I);for(B=J;B--;)c[B].rank=B;for(B=J;B--;)G[B].rank>=w&&G.splice(B,1);J=G.length}for(B=0;B0){if(w=K.pop(),F=w.i,z=w.y,c>z&&b[F+1]!==null||ch-f&&(D[1]=s(t(y+w-h+f),D[1])),z-x/2<0?D[0]=s(t(-z+x/2),D[0]):z+x/2>d&&(D[2]=s(t(z+x/2-d),D[2]))}}if(va(D)===0||this.verifyDataLabelOverflow(D))this.placeDataLabels(),u&&g&&n(this.points,function(b){i=b.connector;v=b.labelPos;if((r=b.dataLabel)&& +r._pos)E=r._attr.visibility,y=r.connX,z=r.connY,j=k?["M",y+(v[6]==="left"?5:-5),z,"C",y,z,2*v[2]-v[4],2*v[3]-v[5],v[2],v[3],"L",v[4],v[5]]:["M",y+(v[6]==="left"?5:-5),z,"L",v[2],v[3],"L",v[4],v[5]],i?(i.animate({d:j}),i.attr("visibility",E)):b.connector=i=a.chart.renderer.path(j).attr({"stroke-width":g,stroke:e.connectorColor||b.color||"#606060",visibility:E}).add(a.group);else if(i)b.connector=i.destroy()})}},verifyDataLabelOverflow:function(a){var b=this.center,c=this.options,d=c.center,e=c=c.minSize|| +80,f;d[0]!==null?e=s(b[2]-s(a[1],a[3]),c):(e=s(b[2]-a[1]-a[3],c),b[0]+=(a[3]-a[1])/2);d[1]!==null?e=s(I(e,b[2]-s(a[0],a[2])),c):(e=s(I(e,b[2]-a[0]-a[2]),c),b[1]+=(a[0]-a[2])/2);e
              /g, '
              ') + .split(//g), + childNodes = textNode.childNodes, + styleRegex = /style="([^"]+)"/, + hrefRegex = /href="(http[^"]+)"/, + parentX = attr(textNode, 'x'), + textStyles = wrapper.styles, + width = textStyles && textStyles.width && pInt(textStyles.width), + textLineHeight = textStyles && textStyles.lineHeight, + i = childNodes.length; + + /// remove old text + while (i--) { + textNode.removeChild(childNodes[i]); + } + + if (width && !wrapper.added) { + this.box.appendChild(textNode); // attach it to the DOM to read offset width + } + + // remove empty line at end + if (lines[lines.length - 1] === '') { + lines.pop(); + } + + // build the lines + each(lines, function (line, lineNo) { + var spans, spanNo = 0; + + line = line.replace(//g, '|||'); + spans = line.split('|||'); + + each(spans, function (span) { + if (span !== '' || spans.length === 1) { + var attributes = {}, + tspan = doc.createElementNS(SVG_NS, 'tspan'), + spanStyle; // #390 + if (styleRegex.test(span)) { + spanStyle = span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2'); + attr(tspan, 'style', spanStyle); + } + if (hrefRegex.test(span) && !forExport) { // Not for export - #1529 + attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"'); + css(tspan, { cursor: 'pointer' }); + } + + span = (span.replace(/<(.|\n)*?>/g, '') || ' ') + .replace(/</g, '<') + .replace(/>/g, '>'); + + // Nested tags aren't supported, and cause crash in Safari (#1596) + if (span !== ' ') { + + // add the text node + tspan.appendChild(doc.createTextNode(span)); + + if (!spanNo) { // first span in a line, align it to the left + attributes.x = parentX; + } else { + attributes.dx = 0; // #16 + } + + // add attributes + attr(tspan, attributes); + + // first span on subsequent line, add the line height + if (!spanNo && lineNo) { + + // allow getting the right offset height in exporting in IE + if (!hasSVG && forExport) { + css(tspan, { display: 'block' }); + } + + // Set the line height based on the font size of either + // the text element or the tspan element + attr( + tspan, + 'dy', + textLineHeight || renderer.fontMetrics( + /px$/.test(tspan.style.fontSize) ? + tspan.style.fontSize : + textStyles.fontSize + ).h, + // Safari 6.0.2 - too optimized for its own good (#1539) + // TODO: revisit this with future versions of Safari + isWebKit && tspan.offsetHeight + ); + } + + // Append it + textNode.appendChild(tspan); + + spanNo++; + + // check width and apply soft breaks + if (width) { + var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273 + tooLong, + actualWidth, + clipHeight = wrapper._clipHeight, + rest = [], + dy = pInt(textLineHeight || 16), + softLineNo = 1, + bBox; + + while (words.length || rest.length) { + delete wrapper.bBox; // delete cache + bBox = wrapper.getBBox(); + actualWidth = bBox.width; + tooLong = actualWidth > width; + if (!tooLong || words.length === 1) { // new line needed + words = rest; + rest = []; + if (words.length) { + softLineNo++; + + if (clipHeight && softLineNo * dy > clipHeight) { + words = ['...']; + wrapper.attr('title', wrapper.textStr); + } else { + + tspan = doc.createElementNS(SVG_NS, 'tspan'); + attr(tspan, { + dy: dy, + x: parentX + }); + if (spanStyle) { // #390 + attr(tspan, 'style', spanStyle); + } + textNode.appendChild(tspan); + + if (actualWidth > width) { // a single word is pressing it out + width = actualWidth; + } + } + } + } else { // append to existing line tspan + tspan.removeChild(tspan.firstChild); + rest.unshift(words.pop()); + } + if (words.length) { + tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-'))); + } + } + } + } + } + }); + }); + }, + + /** + * Create a button with preset states + * @param {String} text + * @param {Number} x + * @param {Number} y + * @param {Function} callback + * @param {Object} normalState + * @param {Object} hoverState + * @param {Object} pressedState + */ + button: function (text, x, y, callback, normalState, hoverState, pressedState, disabledState) { + var label = this.label(text, x, y, null, null, null, null, null, 'button'), + curState = 0, + stateOptions, + stateStyle, + normalStyle, + hoverStyle, + pressedStyle, + disabledStyle, + STYLE = 'style', + verticalGradient = { x1: 0, y1: 0, x2: 0, y2: 1 }; + + // Normal state - prepare the attributes + normalState = merge({ + 'stroke-width': 1, + stroke: '#CCCCCC', + fill: { + linearGradient: verticalGradient, + stops: [ + [0, '#FEFEFE'], + [1, '#F6F6F6'] + ] + }, + r: 2, + padding: 5, + style: { + color: 'black' + } + }, normalState); + normalStyle = normalState[STYLE]; + delete normalState[STYLE]; + + // Hover state + hoverState = merge(normalState, { + stroke: '#68A', + fill: { + linearGradient: verticalGradient, + stops: [ + [0, '#FFF'], + [1, '#ACF'] + ] + } + }, hoverState); + hoverStyle = hoverState[STYLE]; + delete hoverState[STYLE]; + + // Pressed state + pressedState = merge(normalState, { + stroke: '#68A', + fill: { + linearGradient: verticalGradient, + stops: [ + [0, '#9BD'], + [1, '#CDF'] + ] + } + }, pressedState); + pressedStyle = pressedState[STYLE]; + delete pressedState[STYLE]; + + // Disabled state + disabledState = merge(normalState, { + style: { + color: '#CCC' + } + }, disabledState); + disabledStyle = disabledState[STYLE]; + delete disabledState[STYLE]; + + // Add the events. IE9 and IE10 need mouseover and mouseout to funciton (#667). + addEvent(label.element, isIE ? 'mouseover' : 'mouseenter', function () { + if (curState !== 3) { + label.attr(hoverState) + .css(hoverStyle); + } + }); + addEvent(label.element, isIE ? 'mouseout' : 'mouseleave', function () { + if (curState !== 3) { + stateOptions = [normalState, hoverState, pressedState][curState]; + stateStyle = [normalStyle, hoverStyle, pressedStyle][curState]; + label.attr(stateOptions) + .css(stateStyle); + } + }); + + label.setState = function (state) { + label.state = curState = state; + if (!state) { + label.attr(normalState) + .css(normalStyle); + } else if (state === 2) { + label.attr(pressedState) + .css(pressedStyle); + } else if (state === 3) { + label.attr(disabledState) + .css(disabledStyle); + } + }; + + return label + .on('click', function () { + if (curState !== 3) { + callback.call(label); + } + }) + .attr(normalState) + .css(extend({ cursor: 'default' }, normalStyle)); + }, + + /** + * Make a straight line crisper by not spilling out to neighbour pixels + * @param {Array} points + * @param {Number} width + */ + crispLine: function (points, width) { + // points format: [M, 0, 0, L, 100, 0] + // normalize to a crisp line + if (points[1] === points[4]) { + // Substract due to #1129. Now bottom and left axis gridlines behave the same. + points[1] = points[4] = mathRound(points[1]) - (width % 2 / 2); + } + if (points[2] === points[5]) { + points[2] = points[5] = mathRound(points[2]) + (width % 2 / 2); + } + return points; + }, + + + /** + * Draw a path + * @param {Array} path An SVG path in array form + */ + path: function (path) { + var attr = { + fill: NONE + }; + if (isArray(path)) { + attr.d = path; + } else if (isObject(path)) { // attributes + extend(attr, path); + } + return this.createElement('path').attr(attr); + }, + + /** + * Draw and return an SVG circle + * @param {Number} x The x position + * @param {Number} y The y position + * @param {Number} r The radius + */ + circle: function (x, y, r) { + var attr = isObject(x) ? + x : + { + x: x, + y: y, + r: r + }; + + return this.createElement('circle').attr(attr); + }, + + /** + * Draw and return an arc + * @param {Number} x X position + * @param {Number} y Y position + * @param {Number} r Radius + * @param {Number} innerR Inner radius like used in donut charts + * @param {Number} start Starting angle + * @param {Number} end Ending angle + */ + arc: function (x, y, r, innerR, start, end) { + var arc; + + if (isObject(x)) { + y = x.y; + r = x.r; + innerR = x.innerR; + start = x.start; + end = x.end; + x = x.x; + } + + // Arcs are defined as symbols for the ability to set + // attributes in attr and animate + arc = this.symbol('arc', x || 0, y || 0, r || 0, r || 0, { + innerR: innerR || 0, + start: start || 0, + end: end || 0 + }); + arc.r = r; // #959 + return arc; + }, + + /** + * Draw and return a rectangle + * @param {Number} x Left position + * @param {Number} y Top position + * @param {Number} width + * @param {Number} height + * @param {Number} r Border corner radius + * @param {Number} strokeWidth A stroke width can be supplied to allow crisp drawing + */ + rect: function (x, y, width, height, r, strokeWidth) { + + r = isObject(x) ? x.r : r; + + var wrapper = this.createElement('rect').attr({ + rx: r, + ry: r, + fill: NONE + }); + return wrapper.attr( + isObject(x) ? + x : + // do not crispify when an object is passed in (as in column charts) + wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0)) + ); + }, + + /** + * Resize the box and re-align all aligned elements + * @param {Object} width + * @param {Object} height + * @param {Boolean} animate + * + */ + setSize: function (width, height, animate) { + var renderer = this, + alignedObjects = renderer.alignedObjects, + i = alignedObjects.length; + + renderer.width = width; + renderer.height = height; + + renderer.boxWrapper[pick(animate, true) ? 'animate' : 'attr']({ + width: width, + height: height + }); + + while (i--) { + alignedObjects[i].align(); + } + }, + + /** + * Create a group + * @param {String} name The group will be given a class name of 'highcharts-{name}'. + * This can be used for styling and scripting. + */ + g: function (name) { + var elem = this.createElement('g'); + return defined(name) ? elem.attr({ 'class': PREFIX + name }) : elem; + }, + + /** + * Display an image + * @param {String} src + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + image: function (src, x, y, width, height) { + var attribs = { + preserveAspectRatio: NONE + }, + elemWrapper; + + // optional properties + if (arguments.length > 1) { + extend(attribs, { + x: x, + y: y, + width: width, + height: height + }); + } + + elemWrapper = this.createElement('image').attr(attribs); + + // set the href in the xlink namespace + if (elemWrapper.element.setAttributeNS) { + elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink', + 'href', src); + } else { + // could be exporting in IE + // using href throws "not supported" in ie7 and under, requries regex shim to fix later + elemWrapper.element.setAttribute('hc-svg-href', src); + } + + return elemWrapper; + }, + + /** + * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object. + * + * @param {Object} symbol + * @param {Object} x + * @param {Object} y + * @param {Object} radius + * @param {Object} options + */ + symbol: function (symbol, x, y, width, height, options) { + + var obj, + + // get the symbol definition function + symbolFn = this.symbols[symbol], + + // check if there's a path defined for this symbol + path = symbolFn && symbolFn( + mathRound(x), + mathRound(y), + width, + height, + options + ), + + imageElement, + imageRegex = /^url\((.*?)\)$/, + imageSrc, + imageSize, + centerImage; + + if (path) { + + obj = this.path(path); + // expando properties for use in animate and attr + extend(obj, { + symbolName: symbol, + x: x, + y: y, + width: width, + height: height + }); + if (options) { + extend(obj, options); + } + + + // image symbols + } else if (imageRegex.test(symbol)) { + + // On image load, set the size and position + centerImage = function (img, size) { + if (img.element) { // it may be destroyed in the meantime (#1390) + img.attr({ + width: size[0], + height: size[1] + }); + + if (!img.alignByTranslate) { // #185 + img.translate( + mathRound((width - size[0]) / 2), // #1378 + mathRound((height - size[1]) / 2) + ); + } + } + }; + + imageSrc = symbol.match(imageRegex)[1]; + imageSize = symbolSizes[imageSrc]; + + // Ireate the image synchronously, add attribs async + obj = this.image(imageSrc) + .attr({ + x: x, + y: y + }); + obj.isImg = true; + + if (imageSize) { + centerImage(obj, imageSize); + } else { + // Initialize image to be 0 size so export will still function if there's no cached sizes. + // + obj.attr({ width: 0, height: 0 }); + + // Create a dummy JavaScript image to get the width and height. Due to a bug in IE < 8, + // the created element must be assigned to a variable in order to load (#292). + imageElement = createElement('img', { + onload: function () { + centerImage(obj, symbolSizes[imageSrc] = [this.width, this.height]); + }, + src: imageSrc + }); + } + } + + return obj; + }, + + /** + * An extendable collection of functions for defining symbol paths. + */ + symbols: { + 'circle': function (x, y, w, h) { + var cpw = 0.166 * w; + return [ + M, x + w / 2, y, + 'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h, + 'C', x - cpw, y + h, x - cpw, y, x + w / 2, y, + 'Z' + ]; + }, + + 'square': function (x, y, w, h) { + return [ + M, x, y, + L, x + w, y, + x + w, y + h, + x, y + h, + 'Z' + ]; + }, + + 'triangle': function (x, y, w, h) { + return [ + M, x + w / 2, y, + L, x + w, y + h, + x, y + h, + 'Z' + ]; + }, + + 'triangle-down': function (x, y, w, h) { + return [ + M, x, y, + L, x + w, y, + x + w / 2, y + h, + 'Z' + ]; + }, + 'diamond': function (x, y, w, h) { + return [ + M, x + w / 2, y, + L, x + w, y + h / 2, + x + w / 2, y + h, + x, y + h / 2, + 'Z' + ]; + }, + 'arc': function (x, y, w, h, options) { + var start = options.start, + radius = options.r || w || h, + end = options.end - 0.001, // to prevent cos and sin of start and end from becoming equal on 360 arcs (related: #1561) + innerRadius = options.innerR, + open = options.open, + cosStart = mathCos(start), + sinStart = mathSin(start), + cosEnd = mathCos(end), + sinEnd = mathSin(end), + longArc = options.end - start < mathPI ? 0 : 1; + + return [ + M, + x + radius * cosStart, + y + radius * sinStart, + 'A', // arcTo + radius, // x radius + radius, // y radius + 0, // slanting + longArc, // long or short arc + 1, // clockwise + x + radius * cosEnd, + y + radius * sinEnd, + open ? M : L, + x + innerRadius * cosEnd, + y + innerRadius * sinEnd, + 'A', // arcTo + innerRadius, // x radius + innerRadius, // y radius + 0, // slanting + longArc, // long or short arc + 0, // clockwise + x + innerRadius * cosStart, + y + innerRadius * sinStart, + + open ? '' : 'Z' // close + ]; + } + }, + + /** + * Define a clipping rectangle + * @param {String} id + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + clipRect: function (x, y, width, height) { + var wrapper, + id = PREFIX + idCounter++, + + clipPath = this.createElement('clipPath').attr({ + id: id + }).add(this.defs); + + wrapper = this.rect(x, y, width, height, 0).add(clipPath); + wrapper.id = id; + wrapper.clipPath = clipPath; + + return wrapper; + }, + + + /** + * Take a color and return it if it's a string, make it a gradient if it's a + * gradient configuration object. Prior to Highstock, an array was used to define + * a linear gradient with pixel positions relative to the SVG. In newer versions + * we change the coordinates to apply relative to the shape, using coordinates + * 0-1 within the shape. To preserve backwards compatibility, linearGradient + * in this definition is an object of x1, y1, x2 and y2. + * + * @param {Object} color The color or config object + */ + color: function (color, elem, prop) { + var renderer = this, + colorObject, + regexRgba = /^rgba/, + gradName, + gradAttr, + gradients, + gradientObject, + stops, + stopColor, + stopOpacity, + radialReference, + n, + id, + key = []; + + // Apply linear or radial gradients + if (color && color.linearGradient) { + gradName = 'linearGradient'; + } else if (color && color.radialGradient) { + gradName = 'radialGradient'; + } + + if (gradName) { + gradAttr = color[gradName]; + gradients = renderer.gradients; + stops = color.stops; + radialReference = elem.radialReference; + + // Keep < 2.2 kompatibility + if (isArray(gradAttr)) { + color[gradName] = gradAttr = { + x1: gradAttr[0], + y1: gradAttr[1], + x2: gradAttr[2], + y2: gradAttr[3], + gradientUnits: 'userSpaceOnUse' + }; + } + + // Correct the radial gradient for the radial reference system + if (gradName === 'radialGradient' && radialReference && !defined(gradAttr.gradientUnits)) { + gradAttr = merge(gradAttr, { + cx: (radialReference[0] - radialReference[2] / 2) + gradAttr.cx * radialReference[2], + cy: (radialReference[1] - radialReference[2] / 2) + gradAttr.cy * radialReference[2], + r: gradAttr.r * radialReference[2], + gradientUnits: 'userSpaceOnUse' + }); + } + + // Build the unique key to detect whether we need to create a new element (#1282) + for (n in gradAttr) { + if (n !== 'id') { + key.push(n, gradAttr[n]); + } + } + for (n in stops) { + key.push(stops[n]); + } + key = key.join(','); + + // Check if a gradient object with the same config object is created within this renderer + if (gradients[key]) { + id = gradients[key].id; + + } else { + + // Set the id and create the element + gradAttr.id = id = PREFIX + idCounter++; + gradients[key] = gradientObject = renderer.createElement(gradName) + .attr(gradAttr) + .add(renderer.defs); + + + // The gradient needs to keep a list of stops to be able to destroy them + gradientObject.stops = []; + each(stops, function (stop) { + var stopObject; + if (regexRgba.test(stop[1])) { + colorObject = Color(stop[1]); + stopColor = colorObject.get('rgb'); + stopOpacity = colorObject.get('a'); + } else { + stopColor = stop[1]; + stopOpacity = 1; + } + stopObject = renderer.createElement('stop').attr({ + offset: stop[0], + 'stop-color': stopColor, + 'stop-opacity': stopOpacity + }).add(gradientObject); + + // Add the stop element to the gradient + gradientObject.stops.push(stopObject); + }); + } + + // Return the reference to the gradient object + return 'url(' + renderer.url + '#' + id + ')'; + + // Webkit and Batik can't show rgba. + } else if (regexRgba.test(color)) { + colorObject = Color(color); + attr(elem, prop + '-opacity', colorObject.get('a')); + + return colorObject.get('rgb'); + + + } else { + // Remove the opacity attribute added above. Does not throw if the attribute is not there. + elem.removeAttribute(prop + '-opacity'); + + return color; + } + + }, + + + /** + * Add text to the SVG object + * @param {String} str + * @param {Number} x Left position + * @param {Number} y Top position + * @param {Boolean} useHTML Use HTML to render the text + */ + text: function (str, x, y, useHTML) { + + // declare variables + var renderer = this, + defaultChartStyle = defaultOptions.chart.style, + fakeSVG = useCanVG || (!hasSVG && renderer.forExport), + wrapper; + + if (useHTML && !renderer.forExport) { + return renderer.html(str, x, y); + } + + x = mathRound(pick(x, 0)); + y = mathRound(pick(y, 0)); + + wrapper = renderer.createElement('text') + .attr({ + x: x, + y: y, + text: str + }) + .css({ + fontFamily: defaultChartStyle.fontFamily, + fontSize: defaultChartStyle.fontSize + }); + + // Prevent wrapping from creating false offsetWidths in export in legacy IE (#1079, #1063) + if (fakeSVG) { + wrapper.css({ + position: ABSOLUTE + }); + } + + wrapper.x = x; + wrapper.y = y; + return wrapper; + }, + + + /** + * Create HTML text node. This is used by the VML renderer as well as the SVG + * renderer through the useHTML option. + * + * @param {String} str + * @param {Number} x + * @param {Number} y + */ + html: function (str, x, y) { + var defaultChartStyle = defaultOptions.chart.style, + wrapper = this.createElement('span'), + attrSetters = wrapper.attrSetters, + element = wrapper.element, + renderer = wrapper.renderer; + + // Text setter + attrSetters.text = function (value) { + if (value !== element.innerHTML) { + delete this.bBox; + } + element.innerHTML = value; + return false; + }; + + // Various setters which rely on update transform + attrSetters.x = attrSetters.y = attrSetters.align = function (value, key) { + if (key === 'align') { + key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML. + } + wrapper[key] = value; + wrapper.htmlUpdateTransform(); + return false; + }; + + // Set the default attributes + wrapper.attr({ + text: str, + x: mathRound(x), + y: mathRound(y) + }) + .css({ + position: ABSOLUTE, + whiteSpace: 'nowrap', + fontFamily: defaultChartStyle.fontFamily, + fontSize: defaultChartStyle.fontSize + }); + + // Use the HTML specific .css method + wrapper.css = wrapper.htmlCss; + + // This is specific for HTML within SVG + if (renderer.isSVG) { + wrapper.add = function (svgGroupWrapper) { + + var htmlGroup, + container = renderer.box.parentNode, + parentGroup, + parents = []; + + // Create a mock group to hold the HTML elements + if (svgGroupWrapper) { + htmlGroup = svgGroupWrapper.div; + if (!htmlGroup) { + + // Read the parent chain into an array and read from top down + parentGroup = svgGroupWrapper; + while (parentGroup) { + + parents.push(parentGroup); + + // Move up to the next parent group + parentGroup = parentGroup.parentGroup; + } + + // Ensure dynamically updating position when any parent is translated + each(parents.reverse(), function (parentGroup) { + var htmlGroupStyle; + + // Create a HTML div and append it to the parent div to emulate + // the SVG group structure + htmlGroup = parentGroup.div = parentGroup.div || createElement(DIV, { + className: attr(parentGroup.element, 'class') + }, { + position: ABSOLUTE, + left: (parentGroup.translateX || 0) + PX, + top: (parentGroup.translateY || 0) + PX + }, htmlGroup || container); // the top group is appended to container + + // Shortcut + htmlGroupStyle = htmlGroup.style; + + // Set listeners to update the HTML div's position whenever the SVG group + // position is changed + extend(parentGroup.attrSetters, { + translateX: function (value) { + htmlGroupStyle.left = value + PX; + }, + translateY: function (value) { + htmlGroupStyle.top = value + PX; + }, + visibility: function (value, key) { + htmlGroupStyle[key] = value; + } + }); + }); + + } + } else { + htmlGroup = container; + } + + htmlGroup.appendChild(element); + + // Shared with VML: + wrapper.added = true; + if (wrapper.alignOnAdd) { + wrapper.htmlUpdateTransform(); + } + + return wrapper; + }; + } + return wrapper; + }, + + /** + * Utility to return the baseline offset and total line height from the font size + */ + fontMetrics: function (fontSize) { + fontSize = pInt(fontSize || 11); + + // Empirical values found by comparing font size and bounding box height. + // Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/ + var lineHeight = fontSize < 24 ? fontSize + 4 : mathRound(fontSize * 1.2), + baseline = mathRound(lineHeight * 0.8); + + return { + h: lineHeight, + b: baseline + }; + }, + + /** + * Add a label, a text item that can hold a colored or gradient background + * as well as a border and shadow. + * @param {string} str + * @param {Number} x + * @param {Number} y + * @param {String} shape + * @param {Number} anchorX In case the shape has a pointer, like a flag, this is the + * coordinates it should be pinned to + * @param {Number} anchorY + * @param {Boolean} baseline Whether to position the label relative to the text baseline, + * like renderer.text, or to the upper border of the rectangle. + * @param {String} className Class name for the group + */ + label: function (str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) { + + var renderer = this, + wrapper = renderer.g(className), + text = renderer.text('', 0, 0, useHTML) + .attr({ + zIndex: 1 + }), + //.add(wrapper), + box, + bBox, + alignFactor = 0, + padding = 3, + paddingLeft = 0, + width, + height, + wrapperX, + wrapperY, + crispAdjust = 0, + deferredAttr = {}, + baselineOffset, + attrSetters = wrapper.attrSetters, + needsBox; + + /** + * This function runs after the label is added to the DOM (when the bounding box is + * available), and after the text of the label is updated to detect the new bounding + * box and reflect it in the border box. + */ + function updateBoxSize() { + var boxX, + boxY, + style = text.element.style; + + bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && + text.getBBox(); + wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft; + wrapper.height = (height || bBox.height || 0) + 2 * padding; + + // update the label-scoped y offset + baselineOffset = padding + renderer.fontMetrics(style && style.fontSize).b; + + if (needsBox) { + + // create the border box if it is not already present + if (!box) { + boxX = mathRound(-alignFactor * padding); + boxY = baseline ? -baselineOffset : 0; + + wrapper.box = box = shape ? + renderer.symbol(shape, boxX, boxY, wrapper.width, wrapper.height) : + renderer.rect(boxX, boxY, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]); + box.add(wrapper); + } + + // apply the box attributes + if (!box.isImg) { // #1630 + box.attr(merge({ + width: wrapper.width, + height: wrapper.height + }, deferredAttr)); + } + deferredAttr = null; + } + } + + /** + * This function runs after setting text or padding, but only if padding is changed + */ + function updateTextPadding() { + var styles = wrapper.styles, + textAlign = styles && styles.textAlign, + x = paddingLeft + padding * (1 - alignFactor), + y; + + // determin y based on the baseline + y = baseline ? 0 : baselineOffset; + + // compensate for alignment + if (defined(width) && (textAlign === 'center' || textAlign === 'right')) { + x += { center: 0.5, right: 1 }[textAlign] * (width - bBox.width); + } + + // update if anything changed + if (x !== text.x || y !== text.y) { + text.attr({ + x: x, + y: y + }); + } + + // record current values + text.x = x; + text.y = y; + } + + /** + * Set a box attribute, or defer it if the box is not yet created + * @param {Object} key + * @param {Object} value + */ + function boxAttr(key, value) { + if (box) { + box.attr(key, value); + } else { + deferredAttr[key] = value; + } + } + + function getSizeAfterAdd() { + text.add(wrapper); + wrapper.attr({ + text: str, // alignment is available now + x: x, + y: y + }); + + if (box && defined(anchorX)) { + wrapper.attr({ + anchorX: anchorX, + anchorY: anchorY + }); + } + } + + /** + * After the text element is added, get the desired size of the border box + * and add it before the text in the DOM. + */ + addEvent(wrapper, 'add', getSizeAfterAdd); + + /* + * Add specific attribute setters. + */ + + // only change local variables + attrSetters.width = function (value) { + width = value; + return false; + }; + attrSetters.height = function (value) { + height = value; + return false; + }; + attrSetters.padding = function (value) { + if (defined(value) && value !== padding) { + padding = value; + updateTextPadding(); + } + return false; + }; + attrSetters.paddingLeft = function (value) { + if (defined(value) && value !== paddingLeft) { + paddingLeft = value; + updateTextPadding(); + } + return false; + }; + + + // change local variable and set attribue as well + attrSetters.align = function (value) { + alignFactor = { left: 0, center: 0.5, right: 1 }[value]; + return false; // prevent setting text-anchor on the group + }; + + // apply these to the box and the text alike + attrSetters.text = function (value, key) { + text.attr(key, value); + updateBoxSize(); + updateTextPadding(); + return false; + }; + + // apply these to the box but not to the text + attrSetters[STROKE_WIDTH] = function (value, key) { + needsBox = true; + crispAdjust = value % 2 / 2; + boxAttr(key, value); + return false; + }; + attrSetters.stroke = attrSetters.fill = attrSetters.r = function (value, key) { + if (key === 'fill') { + needsBox = true; + } + boxAttr(key, value); + return false; + }; + attrSetters.anchorX = function (value, key) { + anchorX = value; + boxAttr(key, value + crispAdjust - wrapperX); + return false; + }; + attrSetters.anchorY = function (value, key) { + anchorY = value; + boxAttr(key, value - wrapperY); + return false; + }; + + // rename attributes + attrSetters.x = function (value) { + wrapper.x = value; // for animation getter + value -= alignFactor * ((width || bBox.width) + padding); + wrapperX = mathRound(value); + + wrapper.attr('translateX', wrapperX); + return false; + }; + attrSetters.y = function (value) { + wrapperY = wrapper.y = mathRound(value); + wrapper.attr('translateY', wrapperY); + return false; + }; + + // Redirect certain methods to either the box or the text + var baseCss = wrapper.css; + return extend(wrapper, { + /** + * Pick up some properties and apply them to the text instead of the wrapper + */ + css: function (styles) { + if (styles) { + var textStyles = {}; + styles = merge(styles); // create a copy to avoid altering the original object (#537) + each(['fontSize', 'fontWeight', 'fontFamily', 'color', 'lineHeight', 'width', 'textDecoration', 'textShadow'], function (prop) { + if (styles[prop] !== UNDEFINED) { + textStyles[prop] = styles[prop]; + delete styles[prop]; + } + }); + text.css(textStyles); + } + return baseCss.call(wrapper, styles); + }, + /** + * Return the bounding box of the box, not the group + */ + getBBox: function () { + return { + width: bBox.width + 2 * padding, + height: bBox.height + 2 * padding, + x: bBox.x - padding, + y: bBox.y - padding + }; + }, + /** + * Apply the shadow to the box + */ + shadow: function (b) { + if (box) { + box.shadow(b); + } + return wrapper; + }, + /** + * Destroy and release memory. + */ + destroy: function () { + removeEvent(wrapper, 'add', getSizeAfterAdd); + + // Added by button implementation + removeEvent(wrapper.element, 'mouseenter'); + removeEvent(wrapper.element, 'mouseleave'); + + if (text) { + text = text.destroy(); + } + if (box) { + box = box.destroy(); + } + // Call base implementation to destroy the rest + SVGElement.prototype.destroy.call(wrapper); + + // Release local pointers (#1298) + wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = getSizeAfterAdd = null; + } + }); + } +}; // end SVGRenderer + + +// general renderer +Renderer = SVGRenderer; + + +/* **************************************************************************** + * * + * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE * + * * + * For applications and websites that don't need IE support, like platform * + * targeted mobile apps and web apps, this code can be removed. * + * * + *****************************************************************************/ + +/** + * @constructor + */ +var VMLRenderer, VMLElement; +if (!hasSVG && !useCanVG) { + +/** + * The VML element wrapper. + */ +Highcharts.VMLElement = VMLElement = { + + /** + * Initialize a new VML element wrapper. It builds the markup as a string + * to minimize DOM traffic. + * @param {Object} renderer + * @param {Object} nodeName + */ + init: function (renderer, nodeName) { + var wrapper = this, + markup = ['<', nodeName, ' filled="f" stroked="f"'], + style = ['position: ', ABSOLUTE, ';'], + isDiv = nodeName === DIV; + + // divs and shapes need size + if (nodeName === 'shape' || isDiv) { + style.push('left:0;top:0;width:1px;height:1px;'); + } + style.push('visibility: ', isDiv ? HIDDEN : VISIBLE); + + markup.push(' style="', style.join(''), '"/>'); + + // create element with default attributes and style + if (nodeName) { + markup = isDiv || nodeName === 'span' || nodeName === 'img' ? + markup.join('') + : renderer.prepVML(markup); + wrapper.element = createElement(markup); + } + + wrapper.renderer = renderer; + wrapper.attrSetters = {}; + }, + + /** + * Add the node to the given parent + * @param {Object} parent + */ + add: function (parent) { + var wrapper = this, + renderer = wrapper.renderer, + element = wrapper.element, + box = renderer.box, + inverted = parent && parent.inverted, + + // get the parent node + parentNode = parent ? + parent.element || parent : + box; + + + // if the parent group is inverted, apply inversion on all children + if (inverted) { // only on groups + renderer.invertChild(element, parentNode); + } + + // append it + parentNode.appendChild(element); + + // align text after adding to be able to read offset + wrapper.added = true; + if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) { + wrapper.updateTransform(); + } + + // fire an event for internal hooks + fireEvent(wrapper, 'add'); + + return wrapper; + }, + + /** + * VML always uses htmlUpdateTransform + */ + updateTransform: SVGElement.prototype.htmlUpdateTransform, + + /** + * Set the rotation of a span with oldIE's filter + */ + setSpanRotation: function (rotation, sintheta, costheta) { + // Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented + // but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+ + // has support for CSS3 transform. The getBBox method also needs to be updated + // to compensate for the rotation, like it currently does for SVG. + // Test case: http://highcharts.com/tests/?file=text-rotation + css(this.element, { + filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta, + ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta, + ', sizingMethod=\'auto expand\')'].join('') : NONE + }); + }, + + /** + * Converts a subset of an SVG path definition to its VML counterpart. Takes an array + * as the parameter and returns a string. + */ + pathToVML: function (value) { + // convert paths + var i = value.length, + path = [], + clockwise; + + while (i--) { + + // Multiply by 10 to allow subpixel precision. + // Substracting half a pixel seems to make the coordinates + // align with SVG, but this hasn't been tested thoroughly + if (isNumber(value[i])) { + path[i] = mathRound(value[i] * 10) - 5; + } else if (value[i] === 'Z') { // close the path + path[i] = 'x'; + } else { + path[i] = value[i]; + + // When the start X and end X coordinates of an arc are too close, + // they are rounded to the same value above. In this case, substract 1 from the end X + // position. #760, #1371. + if (value.isArc && (value[i] === 'wa' || value[i] === 'at')) { + clockwise = value[i] === 'wa' ? 1 : -1; // #1642 + if (path[i + 5] === path[i + 7]) { + path[i + 7] -= clockwise; + } + // Start and end Y (#1410) + if (path[i + 6] === path[i + 8]) { + path[i + 8] -= clockwise; + } + } + } + } + // Loop up again to handle path shortcuts (#2132) + /*while (i++ < path.length) { + if (path[i] === 'H') { // horizontal line to + path[i] = 'L'; + path.splice(i + 2, 0, path[i - 1]); + } else if (path[i] === 'V') { // vertical line to + path[i] = 'L'; + path.splice(i + 1, 0, path[i - 2]); + } + }*/ + return path.join(' ') || 'x'; + }, + + /** + * Get or set attributes + */ + attr: function (hash, val) { + var wrapper = this, + key, + value, + i, + result, + element = wrapper.element || {}, + elemStyle = element.style, + nodeName = element.nodeName, + renderer = wrapper.renderer, + symbolName = wrapper.symbolName, + hasSetSymbolSize, + shadows = wrapper.shadows, + skipAttr, + attrSetters = wrapper.attrSetters, + ret = wrapper; + + // single key-value pair + if (isString(hash) && defined(val)) { + key = hash; + hash = {}; + hash[key] = val; + } + + // used as a getter, val is undefined + if (isString(hash)) { + key = hash; + if (key === 'strokeWidth' || key === 'stroke-width') { + ret = wrapper.strokeweight; + } else { + ret = wrapper[key]; + } + + // setter + } else { + for (key in hash) { + value = hash[key]; + skipAttr = false; + + // check for a specific attribute setter + result = attrSetters[key] && attrSetters[key].call(wrapper, value, key); + + if (result !== false && value !== null) { // #620 + + if (result !== UNDEFINED) { + value = result; // the attribute setter has returned a new value to set + } + + + // prepare paths + // symbols + if (symbolName && /^(x|y|r|start|end|width|height|innerR|anchorX|anchorY)/.test(key)) { + // if one of the symbol size affecting parameters are changed, + // check all the others only once for each call to an element's + // .attr() method + if (!hasSetSymbolSize) { + wrapper.symbolAttr(hash); + + hasSetSymbolSize = true; + } + skipAttr = true; + + } else if (key === 'd') { + value = value || []; + wrapper.d = value.join(' '); // used in getter for animation + + element.path = value = wrapper.pathToVML(value); + + // update shadows + if (shadows) { + i = shadows.length; + while (i--) { + shadows[i].path = shadows[i].cutOff ? this.cutOffPath(value, shadows[i].cutOff) : value; + } + } + skipAttr = true; + + // handle visibility + } else if (key === 'visibility') { + + // let the shadow follow the main element + if (shadows) { + i = shadows.length; + while (i--) { + shadows[i].style[key] = value; + } + } + + // Instead of toggling the visibility CSS property, move the div out of the viewport. + // This works around #61 and #586 + if (nodeName === 'DIV') { + value = value === HIDDEN ? '-999em' : 0; + + // In order to redraw, IE7 needs the div to be visible when tucked away + // outside the viewport. So the visibility is actually opposite of + // the expected value. This applies to the tooltip only. + if (!docMode8) { + elemStyle[key] = value ? VISIBLE : HIDDEN; + } + key = 'top'; + } + elemStyle[key] = value; + skipAttr = true; + + // directly mapped to css + } else if (key === 'zIndex') { + + if (value) { + elemStyle[key] = value; + } + skipAttr = true; + + // x, y, width, height + } else if (inArray(key, ['x', 'y', 'width', 'height']) !== -1) { + + wrapper[key] = value; // used in getter + + if (key === 'x' || key === 'y') { + key = { x: 'left', y: 'top' }[key]; + } else { + value = mathMax(0, value); // don't set width or height below zero (#311) + } + + // clipping rectangle special + if (wrapper.updateClipping) { + wrapper[key] = value; // the key is now 'left' or 'top' for 'x' and 'y' + wrapper.updateClipping(); + } else { + // normal + elemStyle[key] = value; + } + + skipAttr = true; + + // class name + } else if (key === 'class' && nodeName === 'DIV') { + // IE8 Standards mode has problems retrieving the className + element.className = value; + + // stroke + } else if (key === 'stroke') { + + value = renderer.color(value, element, key); + + key = 'strokecolor'; + + // stroke width + } else if (key === 'stroke-width' || key === 'strokeWidth') { + element.stroked = value ? true : false; + key = 'strokeweight'; + wrapper[key] = value; // used in getter, issue #113 + if (isNumber(value)) { + value += PX; + } + + // dashStyle + } else if (key === 'dashstyle') { + var strokeElem = element.getElementsByTagName('stroke')[0] || + createElement(renderer.prepVML(['']), null, null, element); + strokeElem[key] = value || 'solid'; + wrapper.dashstyle = value; /* because changing stroke-width will change the dash length + and cause an epileptic effect */ + skipAttr = true; + + // fill + } else if (key === 'fill') { + + if (nodeName === 'SPAN') { // text color + elemStyle.color = value; + } else if (nodeName !== 'IMG') { // #1336 + element.filled = value !== NONE ? true : false; + + value = renderer.color(value, element, key, wrapper); + + key = 'fillcolor'; + } + + // opacity: don't bother - animation is too slow and filters introduce artifacts + } else if (key === 'opacity') { + /*css(element, { + opacity: value + });*/ + skipAttr = true; + + // rotation on VML elements + } else if (nodeName === 'shape' && key === 'rotation') { + + wrapper[key] = element.style[key] = value; // style is for #1873 + + // Correction for the 1x1 size of the shape container. Used in gauge needles. + element.style.left = -mathRound(mathSin(value * deg2rad) + 1) + PX; + element.style.top = mathRound(mathCos(value * deg2rad)) + PX; + + // translation for animation + } else if (key === 'translateX' || key === 'translateY' || key === 'rotation') { + wrapper[key] = value; + wrapper.updateTransform(); + + skipAttr = true; + + // text for rotated and non-rotated elements + } else if (key === 'text') { + this.bBox = null; + element.innerHTML = value; + skipAttr = true; + } + + + if (!skipAttr) { + if (docMode8) { // IE8 setAttribute bug + element[key] = value; + } else { + attr(element, key, value); + } + } + + } + } + } + return ret; + }, + + /** + * Set the element's clipping to a predefined rectangle + * + * @param {String} id The id of the clip rectangle + */ + clip: function (clipRect) { + var wrapper = this, + clipMembers, + cssRet; + + if (clipRect) { + clipMembers = clipRect.members; + erase(clipMembers, wrapper); // Ensure unique list of elements (#1258) + clipMembers.push(wrapper); + wrapper.destroyClip = function () { + erase(clipMembers, wrapper); + }; + cssRet = clipRect.getCSS(wrapper); + + } else { + if (wrapper.destroyClip) { + wrapper.destroyClip(); + } + cssRet = { clip: docMode8 ? 'inherit' : 'rect(auto)' }; // #1214 + } + + return wrapper.css(cssRet); + + }, + + /** + * Set styles for the element + * @param {Object} styles + */ + css: SVGElement.prototype.htmlCss, + + /** + * Removes a child either by removeChild or move to garbageBin. + * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not. + */ + safeRemoveChild: function (element) { + // discardElement will detach the node from its parent before attaching it + // to the garbage bin. Therefore it is important that the node is attached and have parent. + if (element.parentNode) { + discardElement(element); + } + }, + + /** + * Extend element.destroy by removing it from the clip members array + */ + destroy: function () { + if (this.destroyClip) { + this.destroyClip(); + } + + return SVGElement.prototype.destroy.apply(this); + }, + + /** + * Add an event listener. VML override for normalizing event parameters. + * @param {String} eventType + * @param {Function} handler + */ + on: function (eventType, handler) { + // simplest possible event model for internal use + this.element['on' + eventType] = function () { + var evt = win.event; + evt.target = evt.srcElement; + handler(evt); + }; + return this; + }, + + /** + * In stacked columns, cut off the shadows so that they don't overlap + */ + cutOffPath: function (path, length) { + + var len; + + path = path.split(/[ ,]/); + len = path.length; + + if (len === 9 || len === 11) { + path[len - 4] = path[len - 2] = pInt(path[len - 2]) - 10 * length; + } + return path.join(' '); + }, + + /** + * Apply a drop shadow by copying elements and giving them different strokes + * @param {Boolean|Object} shadowOptions + */ + shadow: function (shadowOptions, group, cutOff) { + var shadows = [], + i, + element = this.element, + renderer = this.renderer, + shadow, + elemStyle = element.style, + markup, + path = element.path, + strokeWidth, + modifiedPath, + shadowWidth, + shadowElementOpacity; + + // some times empty paths are not strings + if (path && typeof path.value !== 'string') { + path = 'x'; + } + modifiedPath = path; + + if (shadowOptions) { + shadowWidth = pick(shadowOptions.width, 3); + shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth; + for (i = 1; i <= 3; i++) { + + strokeWidth = (shadowWidth * 2) + 1 - (2 * i); + + // Cut off shadows for stacked column items + if (cutOff) { + modifiedPath = this.cutOffPath(path.value, strokeWidth + 0.5); + } + + markup = ['']; + + shadow = createElement(renderer.prepVML(markup), + null, { + left: pInt(elemStyle.left) + pick(shadowOptions.offsetX, 1), + top: pInt(elemStyle.top) + pick(shadowOptions.offsetY, 1) + } + ); + if (cutOff) { + shadow.cutOff = strokeWidth + 1; + } + + // apply the opacity + markup = ['']; + createElement(renderer.prepVML(markup), null, null, shadow); + + + // insert it + if (group) { + group.element.appendChild(shadow); + } else { + element.parentNode.insertBefore(shadow, element); + } + + // record it + shadows.push(shadow); + + } + + this.shadows = shadows; + } + return this; + + } +}; +VMLElement = extendClass(SVGElement, VMLElement); + +/** + * The VML renderer + */ +var VMLRendererExtension = { // inherit SVGRenderer + + Element: VMLElement, + isIE8: userAgent.indexOf('MSIE 8.0') > -1, + + + /** + * Initialize the VMLRenderer + * @param {Object} container + * @param {Number} width + * @param {Number} height + */ + init: function (container, width, height) { + var renderer = this, + boxWrapper, + box; + + renderer.alignedObjects = []; + + boxWrapper = renderer.createElement(DIV); + box = boxWrapper.element; + box.style.position = RELATIVE; // for freeform drawing using renderer directly + container.appendChild(boxWrapper.element); + + + // generate the containing box + renderer.isVML = true; + renderer.box = box; + renderer.boxWrapper = boxWrapper; + + + renderer.setSize(width, height, false); + + // The only way to make IE6 and IE7 print is to use a global namespace. However, + // with IE8 the only way to make the dynamic shapes visible in screen and print mode + // seems to be to add the xmlns attribute and the behaviour style inline. + if (!doc.namespaces.hcv) { + + doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml'); + + // Setup default CSS (#2153) + (doc.styleSheets.length ? doc.styleSheets[0] : doc.createStyleSheet()).cssText += + 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' + + '{ behavior:url(#default#VML); display: inline-block; } '; + + } + }, + + + /** + * Detect whether the renderer is hidden. This happens when one of the parent elements + * has display: none + */ + isHidden: function () { + return !this.box.offsetWidth; + }, + + /** + * Define a clipping rectangle. In VML it is accomplished by storing the values + * for setting the CSS style to all associated members. + * + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + clipRect: function (x, y, width, height) { + + // create a dummy element + var clipRect = this.createElement(), + isObj = isObject(x); + + // mimic a rectangle with its style object for automatic updating in attr + return extend(clipRect, { + members: [], + left: (isObj ? x.x : x) + 1, + top: (isObj ? x.y : y) + 1, + width: (isObj ? x.width : width) - 1, + height: (isObj ? x.height : height) - 1, + getCSS: function (wrapper) { + var element = wrapper.element, + nodeName = element.nodeName, + isShape = nodeName === 'shape', + inverted = wrapper.inverted, + rect = this, + top = rect.top - (isShape ? element.offsetTop : 0), + left = rect.left, + right = left + rect.width, + bottom = top + rect.height, + ret = { + clip: 'rect(' + + mathRound(inverted ? left : top) + 'px,' + + mathRound(inverted ? bottom : right) + 'px,' + + mathRound(inverted ? right : bottom) + 'px,' + + mathRound(inverted ? top : left) + 'px)' + }; + + // issue 74 workaround + if (!inverted && docMode8 && nodeName === 'DIV') { + extend(ret, { + width: right + PX, + height: bottom + PX + }); + } + return ret; + }, + + // used in attr and animation to update the clipping of all members + updateClipping: function () { + each(clipRect.members, function (member) { + member.css(clipRect.getCSS(member)); + }); + } + }); + + }, + + + /** + * Take a color and return it if it's a string, make it a gradient if it's a + * gradient configuration object, and apply opacity. + * + * @param {Object} color The color or config object + */ + color: function (color, elem, prop, wrapper) { + var renderer = this, + colorObject, + regexRgba = /^rgba/, + markup, + fillType, + ret = NONE; + + // Check for linear or radial gradient + if (color && color.linearGradient) { + fillType = 'gradient'; + } else if (color && color.radialGradient) { + fillType = 'pattern'; + } + + + if (fillType) { + + var stopColor, + stopOpacity, + gradient = color.linearGradient || color.radialGradient, + x1, + y1, + x2, + y2, + opacity1, + opacity2, + color1, + color2, + fillAttr = '', + stops = color.stops, + firstStop, + lastStop, + colors = [], + addFillNode = function () { + // Add the fill subnode. When colors attribute is used, the meanings of opacity and o:opacity2 + // are reversed. + markup = ['']; + createElement(renderer.prepVML(markup), null, null, elem); + }; + + // Extend from 0 to 1 + firstStop = stops[0]; + lastStop = stops[stops.length - 1]; + if (firstStop[0] > 0) { + stops.unshift([ + 0, + firstStop[1] + ]); + } + if (lastStop[0] < 1) { + stops.push([ + 1, + lastStop[1] + ]); + } + + // Compute the stops + each(stops, function (stop, i) { + if (regexRgba.test(stop[1])) { + colorObject = Color(stop[1]); + stopColor = colorObject.get('rgb'); + stopOpacity = colorObject.get('a'); + } else { + stopColor = stop[1]; + stopOpacity = 1; + } + + // Build the color attribute + colors.push((stop[0] * 100) + '% ' + stopColor); + + // Only start and end opacities are allowed, so we use the first and the last + if (!i) { + opacity1 = stopOpacity; + color2 = stopColor; + } else { + opacity2 = stopOpacity; + color1 = stopColor; + } + }); + + // Apply the gradient to fills only. + if (prop === 'fill') { + + // Handle linear gradient angle + if (fillType === 'gradient') { + x1 = gradient.x1 || gradient[0] || 0; + y1 = gradient.y1 || gradient[1] || 0; + x2 = gradient.x2 || gradient[2] || 0; + y2 = gradient.y2 || gradient[3] || 0; + fillAttr = 'angle="' + (90 - math.atan( + (y2 - y1) / // y vector + (x2 - x1) // x vector + ) * 180 / mathPI) + '"'; + + addFillNode(); + + // Radial (circular) gradient + } else { + + var r = gradient.r, + sizex = r * 2, + sizey = r * 2, + cx = gradient.cx, + cy = gradient.cy, + radialReference = elem.radialReference, + bBox, + applyRadialGradient = function () { + if (radialReference) { + bBox = wrapper.getBBox(); + cx += (radialReference[0] - bBox.x) / bBox.width - 0.5; + cy += (radialReference[1] - bBox.y) / bBox.height - 0.5; + sizex *= radialReference[2] / bBox.width; + sizey *= radialReference[2] / bBox.height; + } + fillAttr = 'src="' + defaultOptions.global.VMLRadialGradientURL + '" ' + + 'size="' + sizex + ',' + sizey + '" ' + + 'origin="0.5,0.5" ' + + 'position="' + cx + ',' + cy + '" ' + + 'color2="' + color2 + '" '; + + addFillNode(); + }; + + // Apply radial gradient + if (wrapper.added) { + applyRadialGradient(); + } else { + // We need to know the bounding box to get the size and position right + addEvent(wrapper, 'add', applyRadialGradient); + } + + // The fill element's color attribute is broken in IE8 standards mode, so we + // need to set the parent shape's fillcolor attribute instead. + ret = color1; + } + + // Gradients are not supported for VML stroke, return the first color. #722. + } else { + ret = stopColor; + } + + // if the color is an rgba color, split it and add a fill node + // to hold the opacity component + } else if (regexRgba.test(color) && elem.tagName !== 'IMG') { + + colorObject = Color(color); + + markup = ['<', prop, ' opacity="', colorObject.get('a'), '"/>']; + createElement(this.prepVML(markup), null, null, elem); + + ret = colorObject.get('rgb'); + + + } else { + var propNodes = elem.getElementsByTagName(prop); // 'stroke' or 'fill' node + if (propNodes.length) { + propNodes[0].opacity = 1; + propNodes[0].type = 'solid'; + } + ret = color; + } + + return ret; + }, + + /** + * Take a VML string and prepare it for either IE8 or IE6/IE7. + * @param {Array} markup A string array of the VML markup to prepare + */ + prepVML: function (markup) { + var vmlStyle = 'display:inline-block;behavior:url(#default#VML);', + isIE8 = this.isIE8; + + markup = markup.join(''); + + if (isIE8) { // add xmlns and style inline + markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />'); + if (markup.indexOf('style="') === -1) { + markup = markup.replace('/>', ' style="' + vmlStyle + '" />'); + } else { + markup = markup.replace('style="', 'style="' + vmlStyle); + } + + } else { // add namespace + markup = markup.replace('<', ' 1) { + obj.attr({ + x: x, + y: y, + width: width, + height: height + }); + } + return obj; + }, + + /** + * VML uses a shape for rect to overcome bugs and rotation problems + */ + rect: function (x, y, width, height, r, strokeWidth) { + + var wrapper = this.symbol('rect'); + wrapper.r = isObject(x) ? x.r : r; + + //return wrapper.attr(wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0))); + return wrapper.attr( + isObject(x) ? + x : + // do not crispify when an object is passed in (as in column charts) + wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0)) + ); + }, + + /** + * In the VML renderer, each child of an inverted div (group) is inverted + * @param {Object} element + * @param {Object} parentNode + */ + invertChild: function (element, parentNode) { + var parentStyle = parentNode.style; + css(element, { + flip: 'x', + left: pInt(parentStyle.width) - 1, + top: pInt(parentStyle.height) - 1, + rotation: -90 + }); + }, + + /** + * Symbol definitions that override the parent SVG renderer's symbols + * + */ + symbols: { + // VML specific arc function + arc: function (x, y, w, h, options) { + var start = options.start, + end = options.end, + radius = options.r || w || h, + innerRadius = options.innerR, + cosStart = mathCos(start), + sinStart = mathSin(start), + cosEnd = mathCos(end), + sinEnd = mathSin(end), + ret; + + if (end - start === 0) { // no angle, don't show it. + return ['x']; + } + + ret = [ + 'wa', // clockwise arc to + x - radius, // left + y - radius, // top + x + radius, // right + y + radius, // bottom + x + radius * cosStart, // start x + y + radius * sinStart, // start y + x + radius * cosEnd, // end x + y + radius * sinEnd // end y + ]; + + if (options.open && !innerRadius) { + ret.push( + 'e', + M, + x,// - innerRadius, + y// - innerRadius + ); + } + + ret.push( + 'at', // anti clockwise arc to + x - innerRadius, // left + y - innerRadius, // top + x + innerRadius, // right + y + innerRadius, // bottom + x + innerRadius * cosEnd, // start x + y + innerRadius * sinEnd, // start y + x + innerRadius * cosStart, // end x + y + innerRadius * sinStart, // end y + 'x', // finish path + 'e' // close + ); + + ret.isArc = true; + return ret; + + }, + // Add circle symbol path. This performs significantly faster than v:oval. + circle: function (x, y, w, h, wrapper) { + + if (wrapper) { + w = h = 2 * wrapper.r; + } + + // Center correction, #1682 + if (wrapper && wrapper.isCircle) { + x -= w / 2; + y -= h / 2; + } + + // Return the path + return [ + 'wa', // clockwisearcto + x, // left + y, // top + x + w, // right + y + h, // bottom + x + w, // start x + y + h / 2, // start y + x + w, // end x + y + h / 2, // end y + //'x', // finish path + 'e' // close + ]; + }, + /** + * Add rectangle symbol path which eases rotation and omits arcsize problems + * compared to the built-in VML roundrect shape + * + * @param {Number} left Left position + * @param {Number} top Top position + * @param {Number} r Border radius + * @param {Object} options Width and height + */ + + rect: function (left, top, width, height, options) { + + var right = left + width, + bottom = top + height, + ret, + r; + + // No radius, return the more lightweight square + if (!defined(options) || !options.r) { + ret = SVGRenderer.prototype.symbols.square.apply(0, arguments); + + // Has radius add arcs for the corners + } else { + + r = mathMin(options.r, width, height); + ret = [ + M, + left + r, top, + + L, + right - r, top, + 'wa', + right - 2 * r, top, + right, top + 2 * r, + right - r, top, + right, top + r, + + L, + right, bottom - r, + 'wa', + right - 2 * r, bottom - 2 * r, + right, bottom, + right, bottom - r, + right - r, bottom, + + L, + left + r, bottom, + 'wa', + left, bottom - 2 * r, + left + 2 * r, bottom, + left + r, bottom, + left, bottom - r, + + L, + left, top + r, + 'wa', + left, top, + left + 2 * r, top + 2 * r, + left, top + r, + left + r, top, + + + 'x', + 'e' + ]; + } + return ret; + } + } +}; +Highcharts.VMLRenderer = VMLRenderer = function () { + this.init.apply(this, arguments); +}; +VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension); + + // general renderer + Renderer = VMLRenderer; +} + +/* **************************************************************************** + * * + * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE * + * * + *****************************************************************************/ +/* **************************************************************************** + * * + * START OF ANDROID < 3 SPECIFIC CODE. THIS CAN BE REMOVED IF YOU'RE NOT * + * TARGETING THAT SYSTEM. * + * * + *****************************************************************************/ +var CanVGRenderer, + CanVGController; + +if (useCanVG) { + /** + * The CanVGRenderer is empty from start to keep the source footprint small. + * When requested, the CanVGController downloads the rest of the source packaged + * together with the canvg library. + */ + Highcharts.CanVGRenderer = CanVGRenderer = function () { + // Override the global SVG namespace to fake SVG/HTML that accepts CSS + SVG_NS = 'http://www.w3.org/1999/xhtml'; + }; + + /** + * Start with an empty symbols object. This is needed when exporting is used (exporting.src.js will add a few symbols), but + * the implementation from SvgRenderer will not be merged in until first render. + */ + CanVGRenderer.prototype.symbols = {}; + + /** + * Handles on demand download of canvg rendering support. + */ + CanVGController = (function () { + // List of renderering calls + var deferredRenderCalls = []; + + /** + * When downloaded, we are ready to draw deferred charts. + */ + function drawDeferred() { + var callLength = deferredRenderCalls.length, + callIndex; + + // Draw all pending render calls + for (callIndex = 0; callIndex < callLength; callIndex++) { + deferredRenderCalls[callIndex](); + } + // Clear the list + deferredRenderCalls = []; + } + + return { + push: function (func, scriptLocation) { + // Only get the script once + if (deferredRenderCalls.length === 0) { + getScript(scriptLocation, drawDeferred); + } + // Register render call + deferredRenderCalls.push(func); + } + }; + }()); + + Renderer = CanVGRenderer; +} // end CanVGRenderer + +/* **************************************************************************** + * * + * END OF ANDROID < 3 SPECIFIC CODE * + * * + *****************************************************************************/ + +/** + * The Tick class + */ +function Tick(axis, pos, type, noLabel) { + this.axis = axis; + this.pos = pos; + this.type = type || ''; + this.isNew = true; + + if (!type && !noLabel) { + this.addLabel(); + } +} + +Tick.prototype = { + /** + * Write the tick label + */ + addLabel: function () { + var tick = this, + axis = tick.axis, + options = axis.options, + chart = axis.chart, + horiz = axis.horiz, + categories = axis.categories, + names = axis.series[0] && axis.series[0].names, + pos = tick.pos, + labelOptions = options.labels, + str, + tickPositions = axis.tickPositions, + width = (horiz && categories && + !labelOptions.step && !labelOptions.staggerLines && + !labelOptions.rotation && + chart.plotWidth / tickPositions.length) || + (!horiz && (chart.margin[3] || chart.chartWidth * 0.33)), // #1580, #1931 + isFirst = pos === tickPositions[0], + isLast = pos === tickPositions[tickPositions.length - 1], + css, + attr, + value = categories ? + pick(categories[pos], names && names[pos], pos) : + pos, + label = tick.label, + tickPositionInfo = tickPositions.info, + dateTimeLabelFormat; + + // Set the datetime label format. If a higher rank is set for this position, use that. If not, + // use the general format. + if (axis.isDatetimeAxis && tickPositionInfo) { + dateTimeLabelFormat = options.dateTimeLabelFormats[tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName]; + } + + // set properties for access in render method + tick.isFirst = isFirst; + tick.isLast = isLast; + + // get the string + str = axis.labelFormatter.call({ + axis: axis, + chart: chart, + isFirst: isFirst, + isLast: isLast, + dateTimeLabelFormat: dateTimeLabelFormat, + value: axis.isLog ? correctFloat(lin2log(value)) : value + }); + + // prepare CSS + css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX }; + css = extend(css, labelOptions.style); + + // first call + if (!defined(label)) { + attr = { + align: axis.labelAlign + }; + if (isNumber(labelOptions.rotation)) { + attr.rotation = labelOptions.rotation; + } + if (width && labelOptions.ellipsis) { + attr._clipHeight = axis.len / tickPositions.length; + } + + tick.label = + defined(str) && labelOptions.enabled ? + chart.renderer.text( + str, + 0, + 0, + labelOptions.useHTML + ) + .attr(attr) + // without position absolute, IE export sometimes is wrong + .css(css) + .add(axis.labelGroup) : + null; + + // update + } else if (label) { + label.attr({ + text: str + }) + .css(css); + } + }, + + /** + * Get the offset height or width of the label + */ + getLabelSize: function () { + var label = this.label, + axis = this.axis; + return label ? + ((this.labelBBox = label.getBBox()))[axis.horiz ? 'height' : 'width'] : + 0; + }, + + /** + * Find how far the labels extend to the right and left of the tick's x position. Used for anti-collision + * detection with overflow logic. + */ + getLabelSides: function () { + var bBox = this.labelBBox, // assume getLabelSize has run at this point + axis = this.axis, + options = axis.options, + labelOptions = options.labels, + width = bBox.width, + leftSide = width * { left: 0, center: 0.5, right: 1 }[axis.labelAlign] - labelOptions.x; + + return [-leftSide, width - leftSide]; + }, + + /** + * Handle the label overflow by adjusting the labels to the left and right edge, or + * hide them if they collide into the neighbour label. + */ + handleOverflow: function (index, xy) { + var show = true, + axis = this.axis, + chart = axis.chart, + isFirst = this.isFirst, + isLast = this.isLast, + x = xy.x, + reversed = axis.reversed, + tickPositions = axis.tickPositions; + + if (isFirst || isLast) { + + var sides = this.getLabelSides(), + leftSide = sides[0], + rightSide = sides[1], + plotLeft = chart.plotLeft, + plotRight = plotLeft + axis.len, + neighbour = axis.ticks[tickPositions[index + (isFirst ? 1 : -1)]], + neighbourEdge = neighbour && neighbour.label.xy && neighbour.label.xy.x + neighbour.getLabelSides()[isFirst ? 0 : 1]; + + if ((isFirst && !reversed) || (isLast && reversed)) { + // Is the label spilling out to the left of the plot area? + if (x + leftSide < plotLeft) { + + // Align it to plot left + x = plotLeft - leftSide; + + // Hide it if it now overlaps the neighbour label + if (neighbour && x + rightSide > neighbourEdge) { + show = false; + } + } + + } else { + // Is the label spilling out to the right of the plot area? + if (x + rightSide > plotRight) { + + // Align it to plot right + x = plotRight - rightSide; + + // Hide it if it now overlaps the neighbour label + if (neighbour && x + leftSide < neighbourEdge) { + show = false; + } + + } + } + + // Set the modified x position of the label + xy.x = x; + } + return show; + }, + + /** + * Get the x and y position for ticks and labels + */ + getPosition: function (horiz, pos, tickmarkOffset, old) { + var axis = this.axis, + chart = axis.chart, + cHeight = (old && chart.oldChartHeight) || chart.chartHeight; + + return { + x: horiz ? + axis.translate(pos + tickmarkOffset, null, null, old) + axis.transB : + axis.left + axis.offset + (axis.opposite ? ((old && chart.oldChartWidth) || chart.chartWidth) - axis.right - axis.left : 0), + + y: horiz ? + cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) : + cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB + }; + + }, + + /** + * Get the x, y position of the tick label + */ + getLabelPosition: function (x, y, label, horiz, labelOptions, tickmarkOffset, index, step) { + var axis = this.axis, + transA = axis.transA, + reversed = axis.reversed, + staggerLines = axis.staggerLines, + baseline = axis.chart.renderer.fontMetrics(labelOptions.style.fontSize).b, + rotation = labelOptions.rotation; + + x = x + labelOptions.x - (tickmarkOffset && horiz ? + tickmarkOffset * transA * (reversed ? -1 : 1) : 0); + y = y + labelOptions.y - (tickmarkOffset && !horiz ? + tickmarkOffset * transA * (reversed ? 1 : -1) : 0); + + // Correct for rotation (#1764) + if (rotation && axis.side === 2) { + y -= baseline - baseline * mathCos(rotation * deg2rad); + } + + // Vertically centered + if (!defined(labelOptions.y) && !rotation) { // #1951 + y += baseline - label.getBBox().height / 2; + } + + // Correct for staggered labels + if (staggerLines) { + y += (index / (step || 1) % staggerLines) * (axis.labelOffset / staggerLines); + } + + return { + x: x, + y: y + }; + }, + + /** + * Extendible method to return the path of the marker + */ + getMarkPath: function (x, y, tickLength, tickWidth, horiz, renderer) { + return renderer.crispLine([ + M, + x, + y, + L, + x + (horiz ? 0 : -tickLength), + y + (horiz ? tickLength : 0) + ], tickWidth); + }, + + /** + * Put everything in place + * + * @param index {Number} + * @param old {Boolean} Use old coordinates to prepare an animation into new position + */ + render: function (index, old, opacity) { + var tick = this, + axis = tick.axis, + options = axis.options, + chart = axis.chart, + renderer = chart.renderer, + horiz = axis.horiz, + type = tick.type, + label = tick.label, + pos = tick.pos, + labelOptions = options.labels, + gridLine = tick.gridLine, + gridPrefix = type ? type + 'Grid' : 'grid', + tickPrefix = type ? type + 'Tick' : 'tick', + gridLineWidth = options[gridPrefix + 'LineWidth'], + gridLineColor = options[gridPrefix + 'LineColor'], + dashStyle = options[gridPrefix + 'LineDashStyle'], + tickLength = options[tickPrefix + 'Length'], + tickWidth = options[tickPrefix + 'Width'] || 0, + tickColor = options[tickPrefix + 'Color'], + tickPosition = options[tickPrefix + 'Position'], + gridLinePath, + mark = tick.mark, + markPath, + step = labelOptions.step, + attribs, + show = true, + tickmarkOffset = axis.tickmarkOffset, + xy = tick.getPosition(horiz, pos, tickmarkOffset, old), + x = xy.x, + y = xy.y, + reverseCrisp = ((horiz && x === axis.pos + axis.len) || (!horiz && y === axis.pos)) ? -1 : 1, // #1480, #1687 + staggerLines = axis.staggerLines; + + this.isActive = true; + + // create the grid line + if (gridLineWidth) { + gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLineWidth * reverseCrisp, old, true); + + if (gridLine === UNDEFINED) { + attribs = { + stroke: gridLineColor, + 'stroke-width': gridLineWidth + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + if (!type) { + attribs.zIndex = 1; + } + if (old) { + attribs.opacity = 0; + } + tick.gridLine = gridLine = + gridLineWidth ? + renderer.path(gridLinePath) + .attr(attribs).add(axis.gridGroup) : + null; + } + + // If the parameter 'old' is set, the current call will be followed + // by another call, therefore do not do any animations this time + if (!old && gridLine && gridLinePath) { + gridLine[tick.isNew ? 'attr' : 'animate']({ + d: gridLinePath, + opacity: opacity + }); + } + } + + // create the tick mark + if (tickWidth && tickLength) { + + // negate the length + if (tickPosition === 'inside') { + tickLength = -tickLength; + } + if (axis.opposite) { + tickLength = -tickLength; + } + + markPath = tick.getMarkPath(x, y, tickLength, tickWidth * reverseCrisp, horiz, renderer); + + if (mark) { // updating + mark.animate({ + d: markPath, + opacity: opacity + }); + } else { // first time + tick.mark = renderer.path( + markPath + ).attr({ + stroke: tickColor, + 'stroke-width': tickWidth, + opacity: opacity + }).add(axis.axisGroup); + } + } + + // the label is created on init - now move it into place + if (label && !isNaN(x)) { + label.xy = xy = tick.getLabelPosition(x, y, label, horiz, labelOptions, tickmarkOffset, index, step); + + // Apply show first and show last. If the tick is both first and last, it is + // a single centered tick, in which case we show the label anyway (#2100). + if ((tick.isFirst && !tick.isLast && !pick(options.showFirstLabel, 1)) || + (tick.isLast && !tick.isFirst && !pick(options.showLastLabel, 1))) { + show = false; + + // Handle label overflow and show or hide accordingly + } else if (!staggerLines && horiz && labelOptions.overflow === 'justify' && !tick.handleOverflow(index, xy)) { + show = false; + } + + // apply step + if (step && index % step) { + // show those indices dividable by step + show = false; + } + + // Set the new position, and show or hide + if (show && !isNaN(xy.y)) { + xy.opacity = opacity; + label[tick.isNew ? 'attr' : 'animate'](xy); + tick.isNew = false; + } else { + label.attr('y', -9999); // #1338 + } + } + }, + + /** + * Destructor for the tick prototype + */ + destroy: function () { + destroyObjectProperties(this, this.axis); + } +}; + +/** + * The object wrapper for plot lines and plot bands + * @param {Object} options + */ +function PlotLineOrBand(axis, options) { + this.axis = axis; + + if (options) { + this.options = options; + this.id = options.id; + } +} + +PlotLineOrBand.prototype = { + + /** + * Render the plot line or plot band. If it is already existing, + * move it. + */ + render: function () { + var plotLine = this, + axis = plotLine.axis, + horiz = axis.horiz, + halfPointRange = (axis.pointRange || 0) / 2, + options = plotLine.options, + optionsLabel = options.label, + label = plotLine.label, + width = options.width, + to = options.to, + from = options.from, + isBand = defined(from) && defined(to), + value = options.value, + dashStyle = options.dashStyle, + svgElem = plotLine.svgElem, + path = [], + addEvent, + eventType, + xs, + ys, + x, + y, + color = options.color, + zIndex = options.zIndex, + events = options.events, + attribs, + renderer = axis.chart.renderer; + + // logarithmic conversion + if (axis.isLog) { + from = log2lin(from); + to = log2lin(to); + value = log2lin(value); + } + + // plot line + if (width) { + path = axis.getPlotLinePath(value, width); + attribs = { + stroke: color, + 'stroke-width': width + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + } else if (isBand) { // plot band + + // keep within plot area + from = mathMax(from, axis.min - halfPointRange); + to = mathMin(to, axis.max + halfPointRange); + + path = axis.getPlotBandPath(from, to, options); + attribs = { + fill: color + }; + if (options.borderWidth) { + attribs.stroke = options.borderColor; + attribs['stroke-width'] = options.borderWidth; + } + } else { + return; + } + // zIndex + if (defined(zIndex)) { + attribs.zIndex = zIndex; + } + + // common for lines and bands + if (svgElem) { + if (path) { + svgElem.animate({ + d: path + }, null, svgElem.onGetPath); + } else { + svgElem.hide(); + svgElem.onGetPath = function () { + svgElem.show(); + }; + } + } else if (path && path.length) { + plotLine.svgElem = svgElem = renderer.path(path) + .attr(attribs).add(); + + // events + if (events) { + addEvent = function (eventType) { + svgElem.on(eventType, function (e) { + events[eventType].apply(plotLine, [e]); + }); + }; + for (eventType in events) { + addEvent(eventType); + } + } + } + + // the plot band/line label + if (optionsLabel && defined(optionsLabel.text) && path && path.length && axis.width > 0 && axis.height > 0) { + // apply defaults + optionsLabel = merge({ + align: horiz && isBand && 'center', + x: horiz ? !isBand && 4 : 10, + verticalAlign : !horiz && isBand && 'middle', + y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4, + rotation: horiz && !isBand && 90 + }, optionsLabel); + + // add the SVG element + if (!label) { + plotLine.label = label = renderer.text( + optionsLabel.text, + 0, + 0, + optionsLabel.useHTML + ) + .attr({ + align: optionsLabel.textAlign || optionsLabel.align, + rotation: optionsLabel.rotation, + zIndex: zIndex + }) + .css(optionsLabel.style) + .add(); + } + + // get the bounding box and align the label + xs = [path[1], path[4], pick(path[6], path[1])]; + ys = [path[2], path[5], pick(path[7], path[2])]; + x = arrayMin(xs); + y = arrayMin(ys); + + label.align(optionsLabel, false, { + x: x, + y: y, + width: arrayMax(xs) - x, + height: arrayMax(ys) - y + }); + label.show(); + + } else if (label) { // move out of sight + label.hide(); + } + + // chainable + return plotLine; + }, + + /** + * Remove the plot line or band + */ + destroy: function () { + // remove it from the lookup + erase(this.axis.plotLinesAndBands, this); + + delete this.axis; + destroyObjectProperties(this); + } +}; +/** + * The class for stack items + */ +function StackItem(axis, options, isNegative, x, stackOption, stacking) { + + var inverted = axis.chart.inverted; + + this.axis = axis; + + // Tells if the stack is negative + this.isNegative = isNegative; + + // Save the options to be able to style the label + this.options = options; + + // Save the x value to be able to position the label later + this.x = x; + + // Initialize total value + this.total = null; + + // This will keep each points' extremes stored by series.index + this.points = {}; + + // Save the stack option on the series configuration object, and whether to treat it as percent + this.stack = stackOption; + this.percent = stacking === 'percent'; + + // The align options and text align varies on whether the stack is negative and + // if the chart is inverted or not. + // First test the user supplied value, then use the dynamic. + this.alignOptions = { + align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'), + verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')), + y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)), + x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0) + }; + + this.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center'); +} + +StackItem.prototype = { + destroy: function () { + destroyObjectProperties(this, this.axis); + }, + + /** + * Renders the stack total label and adds it to the stack label group. + */ + render: function (group) { + var options = this.options, + formatOption = options.format, + str = formatOption ? + format(formatOption, this) : + options.formatter.call(this); // format the text in the label + + // Change the text to reflect the new total and set visibility to hidden in case the serie is hidden + if (this.label) { + this.label.attr({text: str, visibility: HIDDEN}); + // Create new label + } else { + this.label = + this.axis.chart.renderer.text(str, 0, 0, options.useHTML) // dummy positions, actual position updated with setOffset method in columnseries + .css(options.style) // apply style + .attr({ + align: this.textAlign, // fix the text-anchor + rotation: options.rotation, // rotation + visibility: HIDDEN // hidden until setOffset is called + }) + .add(group); // add to the labels-group + } + }, + + /** + * Sets the offset that the stack has from the x value and repositions the label. + */ + setOffset: function (xOffset, xWidth) { + var stackItem = this, + axis = stackItem.axis, + chart = axis.chart, + inverted = chart.inverted, + neg = this.isNegative, // special treatment is needed for negative stacks + y = axis.translate(this.percent ? 100 : this.total, 0, 0, 0, 1), // stack value translated mapped to chart coordinates + yZero = axis.translate(0), // stack origin + h = mathAbs(y - yZero), // stack height + x = chart.xAxis[0].translate(this.x) + xOffset, // stack x position + plotHeight = chart.plotHeight, + stackBox = { // this is the box for the complete stack + x: inverted ? (neg ? y : y - h) : x, + y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y), + width: inverted ? h : xWidth, + height: inverted ? xWidth : h + }, + label = this.label, + alignAttr; + + if (label) { + label.align(this.alignOptions, null, stackBox); // align the label to the box + + // Set visibility (#678) + alignAttr = label.alignAttr; + label.attr({ + visibility: this.options.crop === false || chart.isInsidePlot(alignAttr.x, alignAttr.y) ? + (hasSVG ? 'inherit' : VISIBLE) : + HIDDEN + }); + } + } +}; +/** + * Create a new axis object + * @param {Object} chart + * @param {Object} options + */ +function Axis() { + this.init.apply(this, arguments); +} + +Axis.prototype = { + + /** + * Default options for the X axis - the Y axis has extended defaults + */ + defaultOptions: { + // allowDecimals: null, + // alternateGridColor: null, + // categories: [], + dateTimeLabelFormats: { + millisecond: '%H:%M:%S.%L', + second: '%H:%M:%S', + minute: '%H:%M', + hour: '%H:%M', + day: '%e. %b', + week: '%e. %b', + month: '%b \'%y', + year: '%Y' + }, + endOnTick: false, + gridLineColor: '#C0C0C0', + // gridLineDashStyle: 'solid', + // gridLineWidth: 0, + // reversed: false, + + labels: defaultLabelOptions, + // { step: null }, + lineColor: '#C0D0E0', + lineWidth: 1, + //linkedTo: null, + //max: undefined, + //min: undefined, + minPadding: 0.01, + maxPadding: 0.01, + //minRange: null, + minorGridLineColor: '#E0E0E0', + // minorGridLineDashStyle: null, + minorGridLineWidth: 1, + minorTickColor: '#A0A0A0', + //minorTickInterval: null, + minorTickLength: 2, + minorTickPosition: 'outside', // inside or outside + //minorTickWidth: 0, + //opposite: false, + //offset: 0, + //plotBands: [{ + // events: {}, + // zIndex: 1, + // labels: { align, x, verticalAlign, y, style, rotation, textAlign } + //}], + //plotLines: [{ + // events: {} + // dashStyle: {} + // zIndex: + // labels: { align, x, verticalAlign, y, style, rotation, textAlign } + //}], + //reversed: false, + // showFirstLabel: true, + // showLastLabel: true, + startOfWeek: 1, + startOnTick: false, + tickColor: '#C0D0E0', + //tickInterval: null, + tickLength: 5, + tickmarkPlacement: 'between', // on or between + tickPixelInterval: 100, + tickPosition: 'outside', + tickWidth: 1, + title: { + //text: null, + align: 'middle', // low, middle or high + //margin: 0 for horizontal, 10 for vertical axes, + //rotation: 0, + //side: 'outside', + style: { + color: '#4d759e', + //font: defaultFont.replace('normal', 'bold') + fontWeight: 'bold' + } + //x: 0, + //y: 0 + }, + type: 'linear' // linear, logarithmic or datetime + }, + + /** + * This options set extends the defaultOptions for Y axes + */ + defaultYAxisOptions: { + endOnTick: true, + gridLineWidth: 1, + tickPixelInterval: 72, + showLastLabel: true, + labels: { + x: -8, + y: 3 + }, + lineWidth: 0, + maxPadding: 0.05, + minPadding: 0.05, + startOnTick: true, + tickWidth: 0, + title: { + rotation: 270, + text: 'Values' + }, + stackLabels: { + enabled: false, + //align: dynamic, + //y: dynamic, + //x: dynamic, + //verticalAlign: dynamic, + //textAlign: dynamic, + //rotation: 0, + formatter: function () { + return numberFormat(this.total, -1); + }, + style: defaultLabelOptions.style + } + }, + + /** + * These options extend the defaultOptions for left axes + */ + defaultLeftAxisOptions: { + labels: { + x: -8, + y: null + }, + title: { + rotation: 270 + } + }, + + /** + * These options extend the defaultOptions for right axes + */ + defaultRightAxisOptions: { + labels: { + x: 8, + y: null + }, + title: { + rotation: 90 + } + }, + + /** + * These options extend the defaultOptions for bottom axes + */ + defaultBottomAxisOptions: { + labels: { + x: 0, + y: 14 + // overflow: undefined, + // staggerLines: null + }, + title: { + rotation: 0 + } + }, + /** + * These options extend the defaultOptions for left axes + */ + defaultTopAxisOptions: { + labels: { + x: 0, + y: -5 + // overflow: undefined + // staggerLines: null + }, + title: { + rotation: 0 + } + }, + + /** + * Initialize the axis + */ + init: function (chart, userOptions) { + + + var isXAxis = userOptions.isX, + axis = this; + + // Flag, is the axis horizontal + axis.horiz = chart.inverted ? !isXAxis : isXAxis; + + // Flag, isXAxis + axis.isXAxis = isXAxis; + axis.xOrY = isXAxis ? 'x' : 'y'; + + + axis.opposite = userOptions.opposite; // needed in setOptions + axis.side = axis.horiz ? + (axis.opposite ? 0 : 2) : // top : bottom + (axis.opposite ? 1 : 3); // right : left + + axis.setOptions(userOptions); + + + var options = this.options, + type = options.type, + isDatetimeAxis = type === 'datetime'; + + axis.labelFormatter = options.labels.formatter || axis.defaultLabelFormatter; // can be overwritten by dynamic format + + + // Flag, stagger lines or not + axis.userOptions = userOptions; + + //axis.axisTitleMargin = UNDEFINED,// = options.title.margin, + axis.minPixelPadding = 0; + //axis.ignoreMinPadding = UNDEFINED; // can be set to true by a column or bar series + //axis.ignoreMaxPadding = UNDEFINED; + + axis.chart = chart; + axis.reversed = options.reversed; + axis.zoomEnabled = options.zoomEnabled !== false; + + // Initial categories + axis.categories = options.categories || type === 'category'; + + // Elements + //axis.axisGroup = UNDEFINED; + //axis.gridGroup = UNDEFINED; + //axis.axisTitle = UNDEFINED; + //axis.axisLine = UNDEFINED; + + // Shorthand types + axis.isLog = type === 'logarithmic'; + axis.isDatetimeAxis = isDatetimeAxis; + + // Flag, if axis is linked to another axis + axis.isLinked = defined(options.linkedTo); + // Linked axis. + //axis.linkedParent = UNDEFINED; + + // Tick positions + //axis.tickPositions = UNDEFINED; // array containing predefined positions + // Tick intervals + //axis.tickInterval = UNDEFINED; + //axis.minorTickInterval = UNDEFINED; + + axis.tickmarkOffset = (axis.categories && options.tickmarkPlacement === 'between') ? 0.5 : 0; + + // Major ticks + axis.ticks = {}; + // Minor ticks + axis.minorTicks = {}; + //axis.tickAmount = UNDEFINED; + + // List of plotLines/Bands + axis.plotLinesAndBands = []; + + // Alternate bands + axis.alternateBands = {}; + + // Axis metrics + //axis.left = UNDEFINED; + //axis.top = UNDEFINED; + //axis.width = UNDEFINED; + //axis.height = UNDEFINED; + //axis.bottom = UNDEFINED; + //axis.right = UNDEFINED; + //axis.transA = UNDEFINED; + //axis.transB = UNDEFINED; + //axis.oldTransA = UNDEFINED; + axis.len = 0; + //axis.oldMin = UNDEFINED; + //axis.oldMax = UNDEFINED; + //axis.oldUserMin = UNDEFINED; + //axis.oldUserMax = UNDEFINED; + //axis.oldAxisLength = UNDEFINED; + axis.minRange = axis.userMinRange = options.minRange || options.maxZoom; + axis.range = options.range; + axis.offset = options.offset || 0; + + + // Dictionary for stacks + axis.stacks = {}; + axis.oldStacks = {}; + + // Dictionary for stacks max values + axis.stackExtremes = {}; + + // Min and max in the data + //axis.dataMin = UNDEFINED, + //axis.dataMax = UNDEFINED, + + // The axis range + axis.max = null; + axis.min = null; + + // User set min and max + //axis.userMin = UNDEFINED, + //axis.userMax = UNDEFINED, + + // Run Axis + + var eventType, + events = axis.options.events; + + // Register + if (inArray(axis, chart.axes) === -1) { // don't add it again on Axis.update() + chart.axes.push(axis); + chart[isXAxis ? 'xAxis' : 'yAxis'].push(axis); + } + + axis.series = axis.series || []; // populated by Series + + // inverted charts have reversed xAxes as default + if (chart.inverted && isXAxis && axis.reversed === UNDEFINED) { + axis.reversed = true; + } + + axis.removePlotBand = axis.removePlotBandOrLine; + axis.removePlotLine = axis.removePlotBandOrLine; + + + // register event listeners + for (eventType in events) { + addEvent(axis, eventType, events[eventType]); + } + + // extend logarithmic axis + if (axis.isLog) { + axis.val2lin = log2lin; + axis.lin2val = lin2log; + } + }, + + /** + * Merge and set options + */ + setOptions: function (userOptions) { + this.options = merge( + this.defaultOptions, + this.isXAxis ? {} : this.defaultYAxisOptions, + [this.defaultTopAxisOptions, this.defaultRightAxisOptions, + this.defaultBottomAxisOptions, this.defaultLeftAxisOptions][this.side], + merge( + defaultOptions[this.isXAxis ? 'xAxis' : 'yAxis'], // if set in setOptions (#1053) + userOptions + ) + ); + }, + + /** + * Update the axis with a new options structure + */ + update: function (newOptions, redraw) { + var chart = this.chart; + + newOptions = chart.options[this.xOrY + 'Axis'][this.options.index] = merge(this.userOptions, newOptions); + + this.destroy(true); + this._addedPlotLB = this.userMin = this.userMax = UNDEFINED; // #1611, #2306 + + this.init(chart, extend(newOptions, { events: UNDEFINED })); + + chart.isDirtyBox = true; + if (pick(redraw, true)) { + chart.redraw(); + } + }, + + /** + * Remove the axis from the chart + */ + remove: function (redraw) { + var chart = this.chart, + key = this.xOrY + 'Axis'; // xAxis or yAxis + + // Remove associated series + each(this.series, function (series) { + series.remove(false); + }); + + // Remove the axis + erase(chart.axes, this); + erase(chart[key], this); + chart.options[key].splice(this.options.index, 1); + each(chart[key], function (axis, i) { // Re-index, #1706 + axis.options.index = i; + }); + this.destroy(); + chart.isDirtyBox = true; + + if (pick(redraw, true)) { + chart.redraw(); + } + }, + + /** + * The default label formatter. The context is a special config object for the label. + */ + defaultLabelFormatter: function () { + var axis = this.axis, + value = this.value, + categories = axis.categories, + dateTimeLabelFormat = this.dateTimeLabelFormat, + numericSymbols = defaultOptions.lang.numericSymbols, + i = numericSymbols && numericSymbols.length, + multi, + ret, + formatOption = axis.options.labels.format, + + // make sure the same symbol is added for all labels on a linear axis + numericSymbolDetector = axis.isLog ? value : axis.tickInterval; + + if (formatOption) { + ret = format(formatOption, this); + + } else if (categories) { + ret = value; + + } else if (dateTimeLabelFormat) { // datetime axis + ret = dateFormat(dateTimeLabelFormat, value); + + } else if (i && numericSymbolDetector >= 1000) { + // Decide whether we should add a numeric symbol like k (thousands) or M (millions). + // If we are to enable this in tooltip or other places as well, we can move this + // logic to the numberFormatter and enable it by a parameter. + while (i-- && ret === UNDEFINED) { + multi = Math.pow(1000, i + 1); + if (numericSymbolDetector >= multi && numericSymbols[i] !== null) { + ret = numberFormat(value / multi, -1) + numericSymbols[i]; + } + } + } + + if (ret === UNDEFINED) { + if (value >= 1000) { // add thousands separators + ret = numberFormat(value, 0); + + } else { // small numbers + ret = numberFormat(value, -1); + } + } + + return ret; + }, + + /** + * Get the minimum and maximum for the series of each axis + */ + getSeriesExtremes: function () { + var axis = this, + chart = axis.chart; + + axis.hasVisibleSeries = false; + + // reset dataMin and dataMax in case we're redrawing + axis.dataMin = axis.dataMax = null; + + // reset cached stacking extremes + axis.stackExtremes = {}; + + axis.buildStacks(); + + // loop through this axis' series + each(axis.series, function (series) { + + if (series.visible || !chart.options.chart.ignoreHiddenSeries) { + + var seriesOptions = series.options, + xData, + threshold = seriesOptions.threshold, + seriesDataMin, + seriesDataMax; + + axis.hasVisibleSeries = true; + + // Validate threshold in logarithmic axes + if (axis.isLog && threshold <= 0) { + threshold = null; + } + + // Get dataMin and dataMax for X axes + if (axis.isXAxis) { + xData = series.xData; + if (xData.length) { + axis.dataMin = mathMin(pick(axis.dataMin, xData[0]), arrayMin(xData)); + axis.dataMax = mathMax(pick(axis.dataMax, xData[0]), arrayMax(xData)); + } + + // Get dataMin and dataMax for Y axes, as well as handle stacking and processed data + } else { + + // Get this particular series extremes + series.getExtremes(); + seriesDataMax = series.dataMax; + seriesDataMin = series.dataMin; + + // Get the dataMin and dataMax so far. If percentage is used, the min and max are + // always 0 and 100. If seriesDataMin and seriesDataMax is null, then series + // doesn't have active y data, we continue with nulls + if (defined(seriesDataMin) && defined(seriesDataMax)) { + axis.dataMin = mathMin(pick(axis.dataMin, seriesDataMin), seriesDataMin); + axis.dataMax = mathMax(pick(axis.dataMax, seriesDataMax), seriesDataMax); + } + + // Adjust to threshold + if (defined(threshold)) { + if (axis.dataMin >= threshold) { + axis.dataMin = threshold; + axis.ignoreMinPadding = true; + } else if (axis.dataMax < threshold) { + axis.dataMax = threshold; + axis.ignoreMaxPadding = true; + } + } + } + } + }); + }, + + /** + * Translate from axis value to pixel position on the chart, or back + * + */ + translate: function (val, backwards, cvsCoord, old, handleLog, pointPlacement) { + var axis = this, + axisLength = axis.len, + sign = 1, + cvsOffset = 0, + localA = old ? axis.oldTransA : axis.transA, + localMin = old ? axis.oldMin : axis.min, + returnValue, + minPixelPadding = axis.minPixelPadding, + postTranslate = (axis.options.ordinal || (axis.isLog && handleLog)) && axis.lin2val; + + if (!localA) { + localA = axis.transA; + } + + // In vertical axes, the canvas coordinates start from 0 at the top like in + // SVG. + if (cvsCoord) { + sign *= -1; // canvas coordinates inverts the value + cvsOffset = axisLength; + } + + // Handle reversed axis + if (axis.reversed) { + sign *= -1; + cvsOffset -= sign * axisLength; + } + + // From pixels to value + if (backwards) { // reverse translation + + val = val * sign + cvsOffset; + val -= minPixelPadding; + returnValue = val / localA + localMin; // from chart pixel to value + if (postTranslate) { // log and ordinal axes + returnValue = axis.lin2val(returnValue); + } + + // From value to pixels + } else { + if (postTranslate) { // log and ordinal axes + val = axis.val2lin(val); + } + if (pointPlacement === 'between') { + pointPlacement = 0.5; + } + returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * minPixelPadding) + + (isNumber(pointPlacement) ? localA * pointPlacement * axis.pointRange : 0); + } + + return returnValue; + }, + + /** + * Utility method to translate an axis value to pixel position. + * @param {Number} value A value in terms of axis units + * @param {Boolean} paneCoordinates Whether to return the pixel coordinate relative to the chart + * or just the axis/pane itself. + */ + toPixels: function (value, paneCoordinates) { + return this.translate(value, false, !this.horiz, null, true) + (paneCoordinates ? 0 : this.pos); + }, + + /* + * Utility method to translate a pixel position in to an axis value + * @param {Number} pixel The pixel value coordinate + * @param {Boolean} paneCoordiantes Whether the input pixel is relative to the chart or just the + * axis/pane itself. + */ + toValue: function (pixel, paneCoordinates) { + return this.translate(pixel - (paneCoordinates ? 0 : this.pos), true, !this.horiz, null, true); + }, + + /** + * Create the path for a plot line that goes from the given value on + * this axis, across the plot to the opposite side + * @param {Number} value + * @param {Number} lineWidth Used for calculation crisp line + * @param {Number] old Use old coordinates (for resizing and rescaling) + */ + getPlotLinePath: function (value, lineWidth, old, force) { + var axis = this, + chart = axis.chart, + axisLeft = axis.left, + axisTop = axis.top, + x1, + y1, + x2, + y2, + translatedValue = axis.translate(value, null, null, old), + cHeight = (old && chart.oldChartHeight) || chart.chartHeight, + cWidth = (old && chart.oldChartWidth) || chart.chartWidth, + skip, + transB = axis.transB; + + x1 = x2 = mathRound(translatedValue + transB); + y1 = y2 = mathRound(cHeight - translatedValue - transB); + + if (isNaN(translatedValue)) { // no min or max + skip = true; + + } else if (axis.horiz) { + y1 = axisTop; + y2 = cHeight - axis.bottom; + if (x1 < axisLeft || x1 > axisLeft + axis.width) { + skip = true; + } + } else { + x1 = axisLeft; + x2 = cWidth - axis.right; + + if (y1 < axisTop || y1 > axisTop + axis.height) { + skip = true; + } + } + return skip && !force ? + null : + chart.renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 0); + }, + + /** + * Create the path for a plot band + */ + getPlotBandPath: function (from, to) { + + var toPath = this.getPlotLinePath(to), + path = this.getPlotLinePath(from); + + if (path && toPath) { + path.push( + toPath[4], + toPath[5], + toPath[1], + toPath[2] + ); + } else { // outside the axis area + path = null; + } + + return path; + }, + + /** + * Set the tick positions of a linear axis to round values like whole tens or every five. + */ + getLinearTickPositions: function (tickInterval, min, max) { + var pos, + lastPos, + roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval), + roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval), + tickPositions = []; + + // Populate the intermediate values + pos = roundedMin; + while (pos <= roundedMax) { + + // Place the tick on the rounded value + tickPositions.push(pos); + + // Always add the raw tickInterval, not the corrected one. + pos = correctFloat(pos + tickInterval); + + // If the interval is not big enough in the current min - max range to actually increase + // the loop variable, we need to break out to prevent endless loop. Issue #619 + if (pos === lastPos) { + break; + } + + // Record the last value + lastPos = pos; + } + return tickPositions; + }, + + /** + * Set the tick positions of a logarithmic axis + */ + getLogTickPositions: function (interval, min, max, minor) { + var axis = this, + options = axis.options, + axisLength = axis.len, + // Since we use this method for both major and minor ticks, + // use a local variable and return the result + positions = []; + + // Reset + if (!minor) { + axis._minorAutoInterval = null; + } + + // First case: All ticks fall on whole logarithms: 1, 10, 100 etc. + if (interval >= 0.5) { + interval = mathRound(interval); + positions = axis.getLinearTickPositions(interval, min, max); + + // Second case: We need intermediary ticks. For example + // 1, 2, 4, 6, 8, 10, 20, 40 etc. + } else if (interval >= 0.08) { + var roundedMin = mathFloor(min), + intermediate, + i, + j, + len, + pos, + lastPos, + break2; + + if (interval > 0.3) { + intermediate = [1, 2, 4]; + } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc + intermediate = [1, 2, 4, 6, 8]; + } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc + intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + } + + for (i = roundedMin; i < max + 1 && !break2; i++) { + len = intermediate.length; + for (j = 0; j < len && !break2; j++) { + pos = log2lin(lin2log(i) * intermediate[j]); + + if (pos > min && (!minor || lastPos <= max)) { // #1670 + positions.push(lastPos); + } + + if (lastPos > max) { + break2 = true; + } + lastPos = pos; + } + } + + // Third case: We are so deep in between whole logarithmic values that + // we might as well handle the tick positions like a linear axis. For + // example 1.01, 1.02, 1.03, 1.04. + } else { + var realMin = lin2log(min), + realMax = lin2log(max), + tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'], + filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption, + tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1), + totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength; + + interval = pick( + filteredTickIntervalOption, + axis._minorAutoInterval, + (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1) + ); + + interval = normalizeTickInterval( + interval, + null, + getMagnitude(interval) + ); + + positions = map(axis.getLinearTickPositions( + interval, + realMin, + realMax + ), log2lin); + + if (!minor) { + axis._minorAutoInterval = interval / 5; + } + } + + // Set the axis-level tickInterval variable + if (!minor) { + axis.tickInterval = interval; + } + return positions; + }, + + /** + * Return the minor tick positions. For logarithmic axes, reuse the same logic + * as for major ticks. + */ + getMinorTickPositions: function () { + var axis = this, + options = axis.options, + tickPositions = axis.tickPositions, + minorTickInterval = axis.minorTickInterval, + minorTickPositions = [], + pos, + i, + len; + + if (axis.isLog) { + len = tickPositions.length; + for (i = 1; i < len; i++) { + minorTickPositions = minorTickPositions.concat( + axis.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true) + ); + } + } else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314 + minorTickPositions = minorTickPositions.concat( + getTimeTicks( + normalizeTimeTickInterval(minorTickInterval), + axis.min, + axis.max, + options.startOfWeek + ) + ); + if (minorTickPositions[0] < axis.min) { + minorTickPositions.shift(); + } + } else { + for (pos = axis.min + (tickPositions[0] - axis.min) % minorTickInterval; pos <= axis.max; pos += minorTickInterval) { + minorTickPositions.push(pos); + } + } + return minorTickPositions; + }, + + /** + * Adjust the min and max for the minimum range. Keep in mind that the series data is + * not yet processed, so we don't have information on data cropping and grouping, or + * updated axis.pointRange or series.pointRange. The data can't be processed until + * we have finally established min and max. + */ + adjustForMinRange: function () { + var axis = this, + options = axis.options, + min = axis.min, + max = axis.max, + zoomOffset, + spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange, + closestDataRange, + i, + distance, + xData, + loopLength, + minArgs, + maxArgs; + + // Set the automatic minimum range based on the closest point distance + if (axis.isXAxis && axis.minRange === UNDEFINED && !axis.isLog) { + + if (defined(options.min) || defined(options.max)) { + axis.minRange = null; // don't do this again + + } else { + + // Find the closest distance between raw data points, as opposed to + // closestPointRange that applies to processed points (cropped and grouped) + each(axis.series, function (series) { + xData = series.xData; + loopLength = series.xIncrement ? 1 : xData.length - 1; + for (i = loopLength; i > 0; i--) { + distance = xData[i] - xData[i - 1]; + if (closestDataRange === UNDEFINED || distance < closestDataRange) { + closestDataRange = distance; + } + } + }); + axis.minRange = mathMin(closestDataRange * 5, axis.dataMax - axis.dataMin); + } + } + + // if minRange is exceeded, adjust + if (max - min < axis.minRange) { + var minRange = axis.minRange; + zoomOffset = (minRange - max + min) / 2; + + // if min and max options have been set, don't go beyond it + minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)]; + if (spaceAvailable) { // if space is available, stay within the data range + minArgs[2] = axis.dataMin; + } + min = arrayMax(minArgs); + + maxArgs = [min + minRange, pick(options.max, min + minRange)]; + if (spaceAvailable) { // if space is availabe, stay within the data range + maxArgs[2] = axis.dataMax; + } + + max = arrayMin(maxArgs); + + // now if the max is adjusted, adjust the min back + if (max - min < minRange) { + minArgs[0] = max - minRange; + minArgs[1] = pick(options.min, max - minRange); + min = arrayMax(minArgs); + } + } + + // Record modified extremes + axis.min = min; + axis.max = max; + }, + + /** + * Update translation information + */ + setAxisTranslation: function (saveOld) { + var axis = this, + range = axis.max - axis.min, + pointRange = 0, + closestPointRange, + minPointOffset = 0, + pointRangePadding = 0, + linkedParent = axis.linkedParent, + ordinalCorrection, + transA = axis.transA; + + // adjust translation for padding + if (axis.isXAxis) { + if (linkedParent) { + minPointOffset = linkedParent.minPointOffset; + pointRangePadding = linkedParent.pointRangePadding; + + } else { + each(axis.series, function (series) { + var seriesPointRange = series.pointRange, + pointPlacement = series.options.pointPlacement, + seriesClosestPointRange = series.closestPointRange; + + if (seriesPointRange > range) { // #1446 + seriesPointRange = 0; + } + pointRange = mathMax(pointRange, seriesPointRange); + + // minPointOffset is the value padding to the left of the axis in order to make + // room for points with a pointRange, typically columns. When the pointPlacement option + // is 'between' or 'on', this padding does not apply. + minPointOffset = mathMax( + minPointOffset, + isString(pointPlacement) ? 0 : seriesPointRange / 2 + ); + + // Determine the total padding needed to the length of the axis to make room for the + // pointRange. If the series' pointPlacement is 'on', no padding is added. + pointRangePadding = mathMax( + pointRangePadding, + pointPlacement === 'on' ? 0 : seriesPointRange + ); + + // Set the closestPointRange + if (!series.noSharedTooltip && defined(seriesClosestPointRange)) { + closestPointRange = defined(closestPointRange) ? + mathMin(closestPointRange, seriesClosestPointRange) : + seriesClosestPointRange; + } + }); + } + + // Record minPointOffset and pointRangePadding + ordinalCorrection = axis.ordinalSlope && closestPointRange ? axis.ordinalSlope / closestPointRange : 1; // #988, #1853 + axis.minPointOffset = minPointOffset = minPointOffset * ordinalCorrection; + axis.pointRangePadding = pointRangePadding = pointRangePadding * ordinalCorrection; + + // pointRange means the width reserved for each point, like in a column chart + axis.pointRange = mathMin(pointRange, range); + + // closestPointRange means the closest distance between points. In columns + // it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange + // is some other value + axis.closestPointRange = closestPointRange; + } + + // Secondary values + if (saveOld) { + axis.oldTransA = transA; + } + axis.translationSlope = axis.transA = transA = axis.len / ((range + pointRangePadding) || 1); + axis.transB = axis.horiz ? axis.left : axis.bottom; // translation addend + axis.minPixelPadding = transA * minPointOffset; + }, + + /** + * Set the tick positions to round values and optionally extend the extremes + * to the nearest tick + */ + setTickPositions: function (secondPass) { + var axis = this, + chart = axis.chart, + options = axis.options, + isLog = axis.isLog, + isDatetimeAxis = axis.isDatetimeAxis, + isXAxis = axis.isXAxis, + isLinked = axis.isLinked, + tickPositioner = axis.options.tickPositioner, + maxPadding = options.maxPadding, + minPadding = options.minPadding, + length, + linkedParentExtremes, + tickIntervalOption = options.tickInterval, + minTickIntervalOption = options.minTickInterval, + tickPixelIntervalOption = options.tickPixelInterval, + tickPositions, + keepTwoTicksOnly, + categories = axis.categories; + + // linked axis gets the extremes from the parent axis + if (isLinked) { + axis.linkedParent = chart[isXAxis ? 'xAxis' : 'yAxis'][options.linkedTo]; + linkedParentExtremes = axis.linkedParent.getExtremes(); + axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin); + axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax); + if (options.type !== axis.linkedParent.options.type) { + error(11, 1); // Can't link axes of different type + } + } else { // initial min and max from the extreme data values + axis.min = pick(axis.userMin, options.min, axis.dataMin); + axis.max = pick(axis.userMax, options.max, axis.dataMax); + } + + if (isLog) { + if (!secondPass && mathMin(axis.min, pick(axis.dataMin, axis.min)) <= 0) { // #978 + error(10, 1); // Can't plot negative values on log axis + } + axis.min = correctFloat(log2lin(axis.min)); // correctFloat cures #934 + axis.max = correctFloat(log2lin(axis.max)); + } + + // handle zoomed range + if (axis.range) { + axis.userMin = axis.min = mathMax(axis.min, axis.max - axis.range); // #618 + axis.userMax = axis.max; + if (secondPass) { + axis.range = null; // don't use it when running setExtremes + } + } + + // Hook for adjusting this.min and this.max. Used by bubble series. + if (axis.beforePadding) { + axis.beforePadding(); + } + + // adjust min and max for the minimum range + axis.adjustForMinRange(); + + // Pad the values to get clear of the chart's edges. To avoid tickInterval taking the padding + // into account, we do this after computing tick interval (#1337). + if (!categories && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) { + length = axis.max - axis.min; + if (length) { + if (!defined(options.min) && !defined(axis.userMin) && minPadding && (axis.dataMin < 0 || !axis.ignoreMinPadding)) { + axis.min -= length * minPadding; + } + if (!defined(options.max) && !defined(axis.userMax) && maxPadding && (axis.dataMax > 0 || !axis.ignoreMaxPadding)) { + axis.max += length * maxPadding; + } + } + } + + // get tickInterval + if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) { + axis.tickInterval = 1; + } else if (isLinked && !tickIntervalOption && + tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) { + axis.tickInterval = axis.linkedParent.tickInterval; + } else { + axis.tickInterval = pick( + tickIntervalOption, + categories ? // for categoried axis, 1 is default, for linear axis use tickPix + 1 : + // don't let it be more than the data range + (axis.max - axis.min) * tickPixelIntervalOption / mathMax(axis.len, tickPixelIntervalOption) + ); + // For squished axes, set only two ticks + if (!defined(tickIntervalOption) && axis.len < tickPixelIntervalOption && !this.isRadial) { + keepTwoTicksOnly = true; + axis.tickInterval /= 4; // tick extremes closer to the real values + } + } + + // Now we're finished detecting min and max, crop and group series data. This + // is in turn needed in order to find tick positions in ordinal axes. + if (isXAxis && !secondPass) { + each(axis.series, function (series) { + series.processData(axis.min !== axis.oldMin || axis.max !== axis.oldMax); + }); + } + + // set the translation factor used in translate function + axis.setAxisTranslation(true); + + // hook for ordinal axes and radial axes + if (axis.beforeSetTickPositions) { + axis.beforeSetTickPositions(); + } + + // hook for extensions, used in Highstock ordinal axes + if (axis.postProcessTickInterval) { + axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval); + } + + // In column-like charts, don't cramp in more ticks than there are points (#1943) + if (axis.pointRange) { + axis.tickInterval = mathMax(axis.pointRange, axis.tickInterval); + } + + // Before normalizing the tick interval, handle minimum tick interval. This applies only if tickInterval is not defined. + if (!tickIntervalOption && axis.tickInterval < minTickIntervalOption) { + axis.tickInterval = minTickIntervalOption; + } + + // for linear axes, get magnitude and normalize the interval + if (!isDatetimeAxis && !isLog) { // linear + if (!tickIntervalOption) { + axis.tickInterval = normalizeTickInterval(axis.tickInterval, null, getMagnitude(axis.tickInterval), options); + } + } + + // get minorTickInterval + axis.minorTickInterval = options.minorTickInterval === 'auto' && axis.tickInterval ? + axis.tickInterval / 5 : options.minorTickInterval; + + // find the tick positions + axis.tickPositions = tickPositions = options.tickPositions ? + [].concat(options.tickPositions) : // Work on a copy (#1565) + (tickPositioner && tickPositioner.apply(axis, [axis.min, axis.max])); + if (!tickPositions) { + + // Too many ticks + if (!axis.ordinalPositions && (axis.max - axis.min) / axis.tickInterval > mathMax(2 * axis.len, 200)) { + error(19, true); + } + + if (isDatetimeAxis) { + tickPositions = (axis.getNonLinearTimeTicks || getTimeTicks)( + normalizeTimeTickInterval(axis.tickInterval, options.units), + axis.min, + axis.max, + options.startOfWeek, + axis.ordinalPositions, + axis.closestPointRange, + true + ); + } else if (isLog) { + tickPositions = axis.getLogTickPositions(axis.tickInterval, axis.min, axis.max); + } else { + tickPositions = axis.getLinearTickPositions(axis.tickInterval, axis.min, axis.max); + } + if (keepTwoTicksOnly) { + tickPositions.splice(1, tickPositions.length - 2); + } + + axis.tickPositions = tickPositions; + } + + if (!isLinked) { + + // reset min/max or remove extremes based on start/end on tick + var roundedMin = tickPositions[0], + roundedMax = tickPositions[tickPositions.length - 1], + minPointOffset = axis.minPointOffset || 0, + singlePad; + + if (options.startOnTick) { + axis.min = roundedMin; + } else if (axis.min - minPointOffset > roundedMin) { + tickPositions.shift(); + } + + if (options.endOnTick) { + axis.max = roundedMax; + } else if (axis.max + minPointOffset < roundedMax) { + tickPositions.pop(); + } + + // When there is only one point, or all points have the same value on this axis, then min + // and max are equal and tickPositions.length is 1. In this case, add some padding + // in order to center the point, but leave it with one tick. #1337. + if (tickPositions.length === 1) { + singlePad = 0.001; // The lowest possible number to avoid extra padding on columns + axis.min -= singlePad; + axis.max += singlePad; + } + } + }, + + /** + * Set the max ticks of either the x and y axis collection + */ + setMaxTicks: function () { + + var chart = this.chart, + maxTicks = chart.maxTicks || {}, + tickPositions = this.tickPositions, + key = this._maxTicksKey = [this.xOrY, this.pos, this.len].join('-'); + + if (!this.isLinked && !this.isDatetimeAxis && tickPositions && tickPositions.length > (maxTicks[key] || 0) && this.options.alignTicks !== false) { + maxTicks[key] = tickPositions.length; + } + chart.maxTicks = maxTicks; + }, + + /** + * When using multiple axes, adjust the number of ticks to match the highest + * number of ticks in that group + */ + adjustTickAmount: function () { + var axis = this, + chart = axis.chart, + key = axis._maxTicksKey, + tickPositions = axis.tickPositions, + maxTicks = chart.maxTicks; + + if (maxTicks && maxTicks[key] && !axis.isDatetimeAxis && !axis.categories && !axis.isLinked && axis.options.alignTicks !== false) { // only apply to linear scale + var oldTickAmount = axis.tickAmount, + calculatedTickAmount = tickPositions.length, + tickAmount; + + // set the axis-level tickAmount to use below + axis.tickAmount = tickAmount = maxTicks[key]; + + if (calculatedTickAmount < tickAmount) { + while (tickPositions.length < tickAmount) { + tickPositions.push(correctFloat( + tickPositions[tickPositions.length - 1] + axis.tickInterval + )); + } + axis.transA *= (calculatedTickAmount - 1) / (tickAmount - 1); + axis.max = tickPositions[tickPositions.length - 1]; + + } + if (defined(oldTickAmount) && tickAmount !== oldTickAmount) { + axis.isDirty = true; + } + } + }, + + /** + * Set the scale based on data min and max, user set min and max or options + * + */ + setScale: function () { + var axis = this, + stacks = axis.stacks, + type, + i, + isDirtyData, + isDirtyAxisLength; + + axis.oldMin = axis.min; + axis.oldMax = axis.max; + axis.oldAxisLength = axis.len; + + // set the new axisLength + axis.setAxisSize(); + //axisLength = horiz ? axisWidth : axisHeight; + isDirtyAxisLength = axis.len !== axis.oldAxisLength; + + // is there new data? + each(axis.series, function (series) { + if (series.isDirtyData || series.isDirty || + series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well + isDirtyData = true; + } + }); + + // do we really need to go through all this? + if (isDirtyAxisLength || isDirtyData || axis.isLinked || axis.forceRedraw || + axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax) { + + // reset stacks + if (!axis.isXAxis) { + for (type in stacks) { + delete stacks[type]; + } + } + + axis.forceRedraw = false; + + // get data extremes if needed + axis.getSeriesExtremes(); + + // get fixed positions based on tickInterval + axis.setTickPositions(); + + // record old values to decide whether a rescale is necessary later on (#540) + axis.oldUserMin = axis.userMin; + axis.oldUserMax = axis.userMax; + + // Mark as dirty if it is not already set to dirty and extremes have changed. #595. + if (!axis.isDirty) { + axis.isDirty = isDirtyAxisLength || axis.min !== axis.oldMin || axis.max !== axis.oldMax; + } + } else if (!axis.isXAxis) { + if (axis.oldStacks) { + stacks = axis.stacks = axis.oldStacks; + } + + // reset stacks + for (type in stacks) { + for (i in stacks[type]) { + stacks[type][i].cum = stacks[type][i].total; + } + } + } + + // Set the maximum tick amount + axis.setMaxTicks(); + }, + + /** + * Set the extremes and optionally redraw + * @param {Number} newMin + * @param {Number} newMax + * @param {Boolean} redraw + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + * @param {Object} eventArguments + * + */ + setExtremes: function (newMin, newMax, redraw, animation, eventArguments) { + var axis = this, + chart = axis.chart; + + redraw = pick(redraw, true); // defaults to true + + // Extend the arguments with min and max + eventArguments = extend(eventArguments, { + min: newMin, + max: newMax + }); + + // Fire the event + fireEvent(axis, 'setExtremes', eventArguments, function () { // the default event handler + + axis.userMin = newMin; + axis.userMax = newMax; + axis.eventArgs = eventArguments; + + // Mark for running afterSetExtremes + axis.isDirtyExtremes = true; + + // redraw + if (redraw) { + chart.redraw(animation); + } + }); + }, + + /** + * Overridable method for zooming chart. Pulled out in a separate method to allow overriding + * in stock charts. + */ + zoom: function (newMin, newMax) { + + // Prevent pinch zooming out of range. Check for defined is for #1946. + if (!this.allowZoomOutside) { + if (defined(this.dataMin) && newMin <= this.dataMin) { + newMin = UNDEFINED; + } + if (defined(this.dataMax) && newMax >= this.dataMax) { + newMax = UNDEFINED; + } + } + + // In full view, displaying the reset zoom button is not required + this.displayBtn = newMin !== UNDEFINED || newMax !== UNDEFINED; + + // Do it + this.setExtremes( + newMin, + newMax, + false, + UNDEFINED, + { trigger: 'zoom' } + ); + return true; + }, + + /** + * Update the axis metrics + */ + setAxisSize: function () { + var chart = this.chart, + options = this.options, + offsetLeft = options.offsetLeft || 0, + offsetRight = options.offsetRight || 0, + horiz = this.horiz, + width, + height, + top, + left; + + // Expose basic values to use in Series object and navigator + this.left = left = pick(options.left, chart.plotLeft + offsetLeft); + this.top = top = pick(options.top, chart.plotTop); + this.width = width = pick(options.width, chart.plotWidth - offsetLeft + offsetRight); + this.height = height = pick(options.height, chart.plotHeight); + this.bottom = chart.chartHeight - height - top; + this.right = chart.chartWidth - width - left; + + // Direction agnostic properties + this.len = mathMax(horiz ? width : height, 0); // mathMax fixes #905 + this.pos = horiz ? left : top; // distance from SVG origin + }, + + /** + * Get the actual axis extremes + */ + getExtremes: function () { + var axis = this, + isLog = axis.isLog; + + return { + min: isLog ? correctFloat(lin2log(axis.min)) : axis.min, + max: isLog ? correctFloat(lin2log(axis.max)) : axis.max, + dataMin: axis.dataMin, + dataMax: axis.dataMax, + userMin: axis.userMin, + userMax: axis.userMax + }; + }, + + /** + * Get the zero plane either based on zero or on the min or max value. + * Used in bar and area plots + */ + getThreshold: function (threshold) { + var axis = this, + isLog = axis.isLog; + + var realMin = isLog ? lin2log(axis.min) : axis.min, + realMax = isLog ? lin2log(axis.max) : axis.max; + + if (realMin > threshold || threshold === null) { + threshold = realMin; + } else if (realMax < threshold) { + threshold = realMax; + } + + return axis.translate(threshold, 0, 1, 0, 1); + }, + + addPlotBand: function (options) { + this.addPlotBandOrLine(options, 'plotBands'); + }, + + addPlotLine: function (options) { + this.addPlotBandOrLine(options, 'plotLines'); + }, + + /** + * Add a plot band or plot line after render time + * + * @param options {Object} The plotBand or plotLine configuration object + */ + addPlotBandOrLine: function (options, coll) { + var obj = new PlotLineOrBand(this, options).render(), + userOptions = this.userOptions; + + if (obj) { // #2189 + // Add it to the user options for exporting and Axis.update + if (coll) { + userOptions[coll] = userOptions[coll] || []; + userOptions[coll].push(options); + } + this.plotLinesAndBands.push(obj); + } + + return obj; + }, + + /** + * Compute auto alignment for the axis label based on which side the axis is on + * and the given rotation for the label + */ + autoLabelAlign: function (rotation) { + var ret, + angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360; + + if (angle > 15 && angle < 165) { + ret = 'right'; + } else if (angle > 195 && angle < 345) { + ret = 'left'; + } else { + ret = 'center'; + } + return ret; + }, + + /** + * Render the tick labels to a preliminary position to get their sizes + */ + getOffset: function () { + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + options = axis.options, + tickPositions = axis.tickPositions, + ticks = axis.ticks, + horiz = axis.horiz, + side = axis.side, + invertedSide = chart.inverted ? [1, 0, 3, 2][side] : side, + hasData, + showAxis, + titleOffset = 0, + titleOffsetOption, + titleMargin = 0, + axisTitleOptions = options.title, + labelOptions = options.labels, + labelOffset = 0, // reset + axisOffset = chart.axisOffset, + clipOffset = chart.clipOffset, + directionFactor = [-1, 1, 1, -1][side], + n, + i, + autoStaggerLines = 1, + maxStaggerLines = pick(labelOptions.maxStaggerLines, 5), + sortedPositions, + lastRight, + overlap, + pos, + bBox, + x, + w, + lineNo; + + // For reuse in Axis.render + axis.hasData = hasData = (axis.hasVisibleSeries || (defined(axis.min) && defined(axis.max) && !!tickPositions)); + axis.showAxis = showAxis = hasData || pick(options.showEmpty, true); + + // Set/reset staggerLines + axis.staggerLines = axis.horiz && labelOptions.staggerLines; + + // Create the axisGroup and gridGroup elements on first iteration + if (!axis.axisGroup) { + axis.gridGroup = renderer.g('grid') + .attr({ zIndex: options.gridZIndex || 1 }) + .add(); + axis.axisGroup = renderer.g('axis') + .attr({ zIndex: options.zIndex || 2 }) + .add(); + axis.labelGroup = renderer.g('axis-labels') + .attr({ zIndex: labelOptions.zIndex || 7 }) + .add(); + } + + if (hasData || axis.isLinked) { + + // Set the explicit or automatic label alignment + axis.labelAlign = pick(labelOptions.align || axis.autoLabelAlign(labelOptions.rotation)); + + each(tickPositions, function (pos) { + if (!ticks[pos]) { + ticks[pos] = new Tick(axis, pos); + } else { + ticks[pos].addLabel(); // update labels depending on tick interval + } + }); + + // Handle automatic stagger lines + if (axis.horiz && !axis.staggerLines && maxStaggerLines && !labelOptions.rotation) { + sortedPositions = axis.reversed ? [].concat(tickPositions).reverse() : tickPositions; + while (autoStaggerLines < maxStaggerLines) { + lastRight = []; + overlap = false; + + for (i = 0; i < sortedPositions.length; i++) { + pos = sortedPositions[i]; + bBox = ticks[pos].label && ticks[pos].label.getBBox(); + w = bBox ? bBox.width : 0; + lineNo = i % autoStaggerLines; + + if (w) { + x = axis.translate(pos); // don't handle log + if (lastRight[lineNo] !== UNDEFINED && x < lastRight[lineNo]) { + overlap = true; + } + lastRight[lineNo] = x + w; + } + } + if (overlap) { + autoStaggerLines++; + } else { + break; + } + } + + if (autoStaggerLines > 1) { + axis.staggerLines = autoStaggerLines; + } + } + + + each(tickPositions, function (pos) { + // left side must be align: right and right side must have align: left for labels + if (side === 0 || side === 2 || { 1: 'left', 3: 'right' }[side] === axis.labelAlign) { + + // get the highest offset + labelOffset = mathMax( + ticks[pos].getLabelSize(), + labelOffset + ); + } + + }); + if (axis.staggerLines) { + labelOffset *= axis.staggerLines; + axis.labelOffset = labelOffset; + } + + + } else { // doesn't have data + for (n in ticks) { + ticks[n].destroy(); + delete ticks[n]; + } + } + + if (axisTitleOptions && axisTitleOptions.text && axisTitleOptions.enabled !== false) { + if (!axis.axisTitle) { + axis.axisTitle = renderer.text( + axisTitleOptions.text, + 0, + 0, + axisTitleOptions.useHTML + ) + .attr({ + zIndex: 7, + rotation: axisTitleOptions.rotation || 0, + align: + axisTitleOptions.textAlign || + { low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align] + }) + .css(axisTitleOptions.style) + .add(axis.axisGroup); + axis.axisTitle.isNew = true; + } + + if (showAxis) { + titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width']; + titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10); + titleOffsetOption = axisTitleOptions.offset; + } + + // hide or show the title depending on whether showEmpty is set + axis.axisTitle[showAxis ? 'show' : 'hide'](); + } + + // handle automatic or user set offset + axis.offset = directionFactor * pick(options.offset, axisOffset[side]); + + axis.axisTitleMargin = + pick(titleOffsetOption, + labelOffset + titleMargin + + (side !== 2 && labelOffset && directionFactor * options.labels[horiz ? 'y' : 'x']) + ); + + axisOffset[side] = mathMax( + axisOffset[side], + axis.axisTitleMargin + titleOffset + directionFactor * axis.offset + ); + clipOffset[invertedSide] = mathMax(clipOffset[invertedSide], mathFloor(options.lineWidth / 2) * 2); + }, + + /** + * Get the path for the axis line + */ + getLinePath: function (lineWidth) { + var chart = this.chart, + opposite = this.opposite, + offset = this.offset, + horiz = this.horiz, + lineLeft = this.left + (opposite ? this.width : 0) + offset, + lineTop = chart.chartHeight - this.bottom - (opposite ? this.height : 0) + offset; + + if (opposite) { + lineWidth *= -1; // crispify the other way - #1480, #1687 + } + + return chart.renderer.crispLine([ + M, + horiz ? + this.left : + lineLeft, + horiz ? + lineTop : + this.top, + L, + horiz ? + chart.chartWidth - this.right : + lineLeft, + horiz ? + lineTop : + chart.chartHeight - this.bottom + ], lineWidth); + }, + + /** + * Position the title + */ + getTitlePosition: function () { + // compute anchor points for each of the title align options + var horiz = this.horiz, + axisLeft = this.left, + axisTop = this.top, + axisLength = this.len, + axisTitleOptions = this.options.title, + margin = horiz ? axisLeft : axisTop, + opposite = this.opposite, + offset = this.offset, + fontSize = pInt(axisTitleOptions.style.fontSize || 12), + + // the position in the length direction of the axis + alongAxis = { + low: margin + (horiz ? 0 : axisLength), + middle: margin + axisLength / 2, + high: margin + (horiz ? axisLength : 0) + }[axisTitleOptions.align], + + // the position in the perpendicular direction of the axis + offAxis = (horiz ? axisTop + this.height : axisLeft) + + (horiz ? 1 : -1) * // horizontal axis reverses the margin + (opposite ? -1 : 1) * // so does opposite axes + this.axisTitleMargin + + (this.side === 2 ? fontSize : 0); + + return { + x: horiz ? + alongAxis : + offAxis + (opposite ? this.width : 0) + offset + + (axisTitleOptions.x || 0), // x + y: horiz ? + offAxis - (opposite ? this.height : 0) + offset : + alongAxis + (axisTitleOptions.y || 0) // y + }; + }, + + /** + * Render the axis + */ + render: function () { + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + options = axis.options, + isLog = axis.isLog, + isLinked = axis.isLinked, + tickPositions = axis.tickPositions, + axisTitle = axis.axisTitle, + stacks = axis.stacks, + ticks = axis.ticks, + minorTicks = axis.minorTicks, + alternateBands = axis.alternateBands, + stackLabelOptions = options.stackLabels, + alternateGridColor = options.alternateGridColor, + tickmarkOffset = axis.tickmarkOffset, + lineWidth = options.lineWidth, + linePath, + hasRendered = chart.hasRendered, + slideInTicks = hasRendered && defined(axis.oldMin) && !isNaN(axis.oldMin), + hasData = axis.hasData, + showAxis = axis.showAxis, + from, + to; + + // Mark all elements inActive before we go over and mark the active ones + each([ticks, minorTicks, alternateBands], function (coll) { + var pos; + for (pos in coll) { + coll[pos].isActive = false; + } + }); + + // If the series has data draw the ticks. Else only the line and title + if (hasData || isLinked) { + + // minor ticks + if (axis.minorTickInterval && !axis.categories) { + each(axis.getMinorTickPositions(), function (pos) { + if (!minorTicks[pos]) { + minorTicks[pos] = new Tick(axis, pos, 'minor'); + } + + // render new ticks in old position + if (slideInTicks && minorTicks[pos].isNew) { + minorTicks[pos].render(null, true); + } + + minorTicks[pos].render(null, false, 1); + }); + } + + // Major ticks. Pull out the first item and render it last so that + // we can get the position of the neighbour label. #808. + if (tickPositions.length) { // #1300 + each(tickPositions.slice(1).concat([tickPositions[0]]), function (pos, i) { + + // Reorganize the indices + i = (i === tickPositions.length - 1) ? 0 : i + 1; + + // linked axes need an extra check to find out if + if (!isLinked || (pos >= axis.min && pos <= axis.max)) { + + if (!ticks[pos]) { + ticks[pos] = new Tick(axis, pos); + } + + // render new ticks in old position + if (slideInTicks && ticks[pos].isNew) { + ticks[pos].render(i, true); + } + + ticks[pos].render(i, false, 1); + } + + }); + // In a categorized axis, the tick marks are displayed between labels. So + // we need to add a tick mark and grid line at the left edge of the X axis. + if (tickmarkOffset && axis.min === 0) { + if (!ticks[-1]) { + ticks[-1] = new Tick(axis, -1, null, true); + } + ticks[-1].render(-1); + } + + } + + // alternate grid color + if (alternateGridColor) { + each(tickPositions, function (pos, i) { + if (i % 2 === 0 && pos < axis.max) { + if (!alternateBands[pos]) { + alternateBands[pos] = new PlotLineOrBand(axis); + } + from = pos + tickmarkOffset; // #949 + to = tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] + tickmarkOffset : axis.max; + alternateBands[pos].options = { + from: isLog ? lin2log(from) : from, + to: isLog ? lin2log(to) : to, + color: alternateGridColor + }; + alternateBands[pos].render(); + alternateBands[pos].isActive = true; + } + }); + } + + // custom plot lines and bands + if (!axis._addedPlotLB) { // only first time + each((options.plotLines || []).concat(options.plotBands || []), function (plotLineOptions) { + axis.addPlotBandOrLine(plotLineOptions); + }); + axis._addedPlotLB = true; + } + + } // end if hasData + + // Remove inactive ticks + each([ticks, minorTicks, alternateBands], function (coll) { + var pos, + i, + forDestruction = [], + delay = globalAnimation ? globalAnimation.duration || 500 : 0, + destroyInactiveItems = function () { + i = forDestruction.length; + while (i--) { + // When resizing rapidly, the same items may be destroyed in different timeouts, + // or the may be reactivated + if (coll[forDestruction[i]] && !coll[forDestruction[i]].isActive) { + coll[forDestruction[i]].destroy(); + delete coll[forDestruction[i]]; + } + } + + }; + + for (pos in coll) { + + if (!coll[pos].isActive) { + // Render to zero opacity + coll[pos].render(pos, false, 0); + coll[pos].isActive = false; + forDestruction.push(pos); + } + } + + // When the objects are finished fading out, destroy them + if (coll === alternateBands || !chart.hasRendered || !delay) { + destroyInactiveItems(); + } else if (delay) { + setTimeout(destroyInactiveItems, delay); + } + }); + + // Static items. As the axis group is cleared on subsequent calls + // to render, these items are added outside the group. + // axis line + if (lineWidth) { + linePath = axis.getLinePath(lineWidth); + if (!axis.axisLine) { + axis.axisLine = renderer.path(linePath) + .attr({ + stroke: options.lineColor, + 'stroke-width': lineWidth, + zIndex: 7 + }) + .add(axis.axisGroup); + } else { + axis.axisLine.animate({ d: linePath }); + } + + // show or hide the line depending on options.showEmpty + axis.axisLine[showAxis ? 'show' : 'hide'](); + } + + if (axisTitle && showAxis) { + + axisTitle[axisTitle.isNew ? 'attr' : 'animate']( + axis.getTitlePosition() + ); + axisTitle.isNew = false; + } + + // Stacked totals: + if (stackLabelOptions && stackLabelOptions.enabled) { + var stackKey, oneStack, stackCategory, + stackTotalGroup = axis.stackTotalGroup; + + // Create a separate group for the stack total labels + if (!stackTotalGroup) { + axis.stackTotalGroup = stackTotalGroup = + renderer.g('stack-labels') + .attr({ + visibility: VISIBLE, + zIndex: 6 + }) + .add(); + } + + // plotLeft/Top will change when y axis gets wider so we need to translate the + // stackTotalGroup at every render call. See bug #506 and #516 + stackTotalGroup.translate(chart.plotLeft, chart.plotTop); + + // Render each stack total + for (stackKey in stacks) { + oneStack = stacks[stackKey]; + for (stackCategory in oneStack) { + oneStack[stackCategory].render(stackTotalGroup); + } + } + } + // End stacked totals + + axis.isDirty = false; + }, + + /** + * Remove a plot band or plot line from the chart by id + * @param {Object} id + */ + removePlotBandOrLine: function (id) { + var plotLinesAndBands = this.plotLinesAndBands, + options = this.options, + userOptions = this.userOptions, + i = plotLinesAndBands.length; + while (i--) { + if (plotLinesAndBands[i].id === id) { + plotLinesAndBands[i].destroy(); + } + } + each([options.plotLines || [], userOptions.plotLines || [], options.plotBands || [], userOptions.plotBands || []], function (arr) { + i = arr.length; + while (i--) { + if (arr[i].id === id) { + erase(arr, arr[i]); + } + } + }); + + }, + + /** + * Update the axis title by options + */ + setTitle: function (newTitleOptions, redraw) { + this.update({ title: newTitleOptions }, redraw); + }, + + /** + * Redraw the axis to reflect changes in the data or axis extremes + */ + redraw: function () { + var axis = this, + chart = axis.chart, + pointer = chart.pointer; + + // hide tooltip and hover states + if (pointer.reset) { + pointer.reset(true); + } + + // render the axis + axis.render(); + + // move plot lines and bands + each(axis.plotLinesAndBands, function (plotLine) { + plotLine.render(); + }); + + // mark associated series as dirty and ready for redraw + each(axis.series, function (series) { + series.isDirty = true; + }); + + }, + + /** + * Build the stacks from top down + */ + buildStacks: function () { + var series = this.series, + i = series.length; + if (!this.isXAxis) { + while (i--) { + series[i].setStackedPoints(); + } + // Loop up again to compute percent stack + if (this.usePercentage) { + for (i = 0; i < series.length; i++) { + series[i].setPercentStacks(); + } + } + } + }, + + /** + * Set new axis categories and optionally redraw + * @param {Array} categories + * @param {Boolean} redraw + */ + setCategories: function (categories, redraw) { + this.update({ categories: categories }, redraw); + }, + + /** + * Destroys an Axis instance. + */ + destroy: function (keepEvents) { + var axis = this, + stacks = axis.stacks, + stackKey, + plotLinesAndBands = axis.plotLinesAndBands, + i; + + // Remove the events + if (!keepEvents) { + removeEvent(axis); + } + + // Destroy each stack total + for (stackKey in stacks) { + destroyObjectProperties(stacks[stackKey]); + + stacks[stackKey] = null; + } + + // Destroy collections + each([axis.ticks, axis.minorTicks, axis.alternateBands], function (coll) { + destroyObjectProperties(coll); + }); + i = plotLinesAndBands.length; + while (i--) { // #1975 + plotLinesAndBands[i].destroy(); + } + + // Destroy local variables + each(['stackTotalGroup', 'axisLine', 'axisGroup', 'gridGroup', 'labelGroup', 'axisTitle'], function (prop) { + if (axis[prop]) { + axis[prop] = axis[prop].destroy(); + } + }); + } + + +}; // end Axis + +/** + * The tooltip object + * @param {Object} chart The chart instance + * @param {Object} options Tooltip options + */ +function Tooltip() { + this.init.apply(this, arguments); +} + +Tooltip.prototype = { + + init: function (chart, options) { + + var borderWidth = options.borderWidth, + style = options.style, + padding = pInt(style.padding); + + // Save the chart and options + this.chart = chart; + this.options = options; + + // Keep track of the current series + //this.currentSeries = UNDEFINED; + + // List of crosshairs + this.crosshairs = []; + + // Current values of x and y when animating + this.now = { x: 0, y: 0 }; + + // The tooltip is initially hidden + this.isHidden = true; + + + // create the label + this.label = chart.renderer.label('', 0, 0, options.shape, null, null, options.useHTML, null, 'tooltip') + .attr({ + padding: padding, + fill: options.backgroundColor, + 'stroke-width': borderWidth, + r: options.borderRadius, + zIndex: 8 + }) + .css(style) + .css({ padding: 0 }) // Remove it from VML, the padding is applied as an attribute instead (#1117) + .add() + .attr({ y: -999 }); // #2301 + + // When using canVG the shadow shows up as a gray circle + // even if the tooltip is hidden. + if (!useCanVG) { + this.label.shadow(options.shadow); + } + + // Public property for getting the shared state. + this.shared = options.shared; + }, + + /** + * Destroy the tooltip and its elements. + */ + destroy: function () { + each(this.crosshairs, function (crosshair) { + if (crosshair) { + crosshair.destroy(); + } + }); + + // Destroy and clear local variables + if (this.label) { + this.label = this.label.destroy(); + } + clearTimeout(this.hideTimer); + clearTimeout(this.tooltipTimeout); + }, + + /** + * Provide a soft movement for the tooltip + * + * @param {Number} x + * @param {Number} y + * @private + */ + move: function (x, y, anchorX, anchorY) { + var tooltip = this, + now = tooltip.now, + animate = tooltip.options.animation !== false && !tooltip.isHidden; + + // get intermediate values for animation + extend(now, { + x: animate ? (2 * now.x + x) / 3 : x, + y: animate ? (now.y + y) / 2 : y, + anchorX: animate ? (2 * now.anchorX + anchorX) / 3 : anchorX, + anchorY: animate ? (now.anchorY + anchorY) / 2 : anchorY + }); + + // move to the intermediate value + tooltip.label.attr(now); + + + // run on next tick of the mouse tracker + if (animate && (mathAbs(x - now.x) > 1 || mathAbs(y - now.y) > 1)) { + + // never allow two timeouts + clearTimeout(this.tooltipTimeout); + + // set the fixed interval ticking for the smooth tooltip + this.tooltipTimeout = setTimeout(function () { + // The interval function may still be running during destroy, so check that the chart is really there before calling. + if (tooltip) { + tooltip.move(x, y, anchorX, anchorY); + } + }, 32); + + } + }, + + /** + * Hide the tooltip + */ + hide: function () { + var tooltip = this, + hoverPoints; + + clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766) + if (!this.isHidden) { + hoverPoints = this.chart.hoverPoints; + + this.hideTimer = setTimeout(function () { + tooltip.label.fadeOut(); + tooltip.isHidden = true; + }, pick(this.options.hideDelay, 500)); + + // hide previous hoverPoints and set new + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + + this.chart.hoverPoints = null; + } + }, + + /** + * Hide the crosshairs + */ + hideCrosshairs: function () { + each(this.crosshairs, function (crosshair) { + if (crosshair) { + crosshair.hide(); + } + }); + }, + + /** + * Extendable method to get the anchor position of the tooltip + * from a point or set of points + */ + getAnchor: function (points, mouseEvent) { + var ret, + chart = this.chart, + inverted = chart.inverted, + plotTop = chart.plotTop, + plotX = 0, + plotY = 0, + yAxis; + + points = splat(points); + + // Pie uses a special tooltipPos + ret = points[0].tooltipPos; + + // When tooltip follows mouse, relate the position to the mouse + if (this.followPointer && mouseEvent) { + if (mouseEvent.chartX === UNDEFINED) { + mouseEvent = chart.pointer.normalize(mouseEvent); + } + ret = [ + mouseEvent.chartX - chart.plotLeft, + mouseEvent.chartY - plotTop + ]; + } + // When shared, use the average position + if (!ret) { + each(points, function (point) { + yAxis = point.series.yAxis; + plotX += point.plotX; + plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) + + (!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151 + }); + + plotX /= points.length; + plotY /= points.length; + + ret = [ + inverted ? chart.plotWidth - plotY : plotX, + this.shared && !inverted && points.length > 1 && mouseEvent ? + mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424) + inverted ? chart.plotHeight - plotX : plotY + ]; + } + + return map(ret, mathRound); + }, + + /** + * Place the tooltip in a chart without spilling over + * and not covering the point it self. + */ + getPosition: function (boxWidth, boxHeight, point) { + + // Set up the variables + var chart = this.chart, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + distance = pick(this.options.distance, 12), + pointX = point.plotX, + pointY = point.plotY, + x = pointX + plotLeft + (chart.inverted ? distance : -boxWidth - distance), + y = pointY - boxHeight + plotTop + 15, // 15 means the point is 15 pixels up from the bottom of the tooltip + alignedRight; + + // It is too far to the left, adjust it + if (x < 7) { + x = plotLeft + mathMax(pointX, 0) + distance; + } + + // Test to see if the tooltip is too far to the right, + // if it is, move it back to be inside and then up to not cover the point. + if ((x + boxWidth) > (plotLeft + plotWidth)) { + x -= (x + boxWidth) - (plotLeft + plotWidth); + y = pointY - boxHeight + plotTop - distance; + alignedRight = true; + } + + // If it is now above the plot area, align it to the top of the plot area + if (y < plotTop + 5) { + y = plotTop + 5; + + // If the tooltip is still covering the point, move it below instead + if (alignedRight && pointY >= y && pointY <= (y + boxHeight)) { + y = pointY + plotTop + distance; // below + } + } + + // Now if the tooltip is below the chart, move it up. It's better to cover the + // point than to disappear outside the chart. #834. + if (y + boxHeight > plotTop + plotHeight) { + y = mathMax(plotTop, plotTop + plotHeight - boxHeight - distance); // below + } + + return {x: x, y: y}; + }, + + /** + * In case no user defined formatter is given, this will be used. Note that the context + * here is an object holding point, series, x, y etc. + */ + defaultFormatter: function (tooltip) { + var items = this.points || splat(this), + series = items[0].series, + s; + + // build the header + s = [series.tooltipHeaderFormatter(items[0])]; + + // build the values + each(items, function (item) { + series = item.series; + s.push((series.tooltipFormatter && series.tooltipFormatter(item)) || + item.point.tooltipFormatter(series.tooltipOptions.pointFormat)); + }); + + // footer + s.push(tooltip.options.footerFormat || ''); + + return s.join(''); + }, + + /** + * Refresh the tooltip's text and position. + * @param {Object} point + */ + refresh: function (point, mouseEvent) { + var tooltip = this, + chart = tooltip.chart, + label = tooltip.label, + options = tooltip.options, + x, + y, + anchor, + textConfig = {}, + text, + pointConfig = [], + formatter = options.formatter || tooltip.defaultFormatter, + hoverPoints = chart.hoverPoints, + borderColor, + crosshairsOptions = options.crosshairs, + shared = tooltip.shared, + currentSeries; + + clearTimeout(this.hideTimer); + + // get the reference point coordinates (pie charts use tooltipPos) + tooltip.followPointer = splat(point)[0].series.tooltipOptions.followPointer; + anchor = tooltip.getAnchor(point, mouseEvent); + x = anchor[0]; + y = anchor[1]; + + // shared tooltip, array is sent over + if (shared && !(point.series && point.series.noSharedTooltip)) { + + // hide previous hoverPoints and set new + + chart.hoverPoints = point; + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + + each(point, function (item) { + item.setState(HOVER_STATE); + + pointConfig.push(item.getLabelConfig()); + }); + + textConfig = { + x: point[0].category, + y: point[0].y + }; + textConfig.points = pointConfig; + point = point[0]; + + // single point tooltip + } else { + textConfig = point.getLabelConfig(); + } + text = formatter.call(textConfig, tooltip); + + // register the current series + currentSeries = point.series; + + // update the inner HTML + if (text === false) { + this.hide(); + } else { + + // show it + if (tooltip.isHidden) { + stop(label); + label.attr('opacity', 1).show(); + } + + // update text + label.attr({ + text: text + }); + + // set the stroke color of the box + borderColor = options.borderColor || point.color || currentSeries.color || '#606060'; + label.attr({ + stroke: borderColor + }); + + tooltip.updatePosition({ plotX: x, plotY: y }); + + this.isHidden = false; + } + + // crosshairs + if (crosshairsOptions) { + crosshairsOptions = splat(crosshairsOptions); // [x, y] + + var path, + i = crosshairsOptions.length, + attribs, + axis, + val, + series; + + while (i--) { + series = point.series; + axis = series[i ? 'yAxis' : 'xAxis']; + if (crosshairsOptions[i] && axis) { + val = i ? pick(point.stackY, point.y) : point.x; // #814 + if (axis.isLog) { // #1671 + val = log2lin(val); + } + if (i === 1 && series.modifyValue) { // #1205, #2316 + val = series.modifyValue(val); + } + + path = axis.getPlotLinePath( + val, + 1 + ); + + if (tooltip.crosshairs[i]) { + tooltip.crosshairs[i].attr({ d: path, visibility: VISIBLE }); + } else { + attribs = { + 'stroke-width': crosshairsOptions[i].width || 1, + stroke: crosshairsOptions[i].color || '#C0C0C0', + zIndex: crosshairsOptions[i].zIndex || 2 + }; + if (crosshairsOptions[i].dashStyle) { + attribs.dashstyle = crosshairsOptions[i].dashStyle; + } + tooltip.crosshairs[i] = chart.renderer.path(path) + .attr(attribs) + .add(); + } + } + } + } + fireEvent(chart, 'tooltipRefresh', { + text: text, + x: x + chart.plotLeft, + y: y + chart.plotTop, + borderColor: borderColor + }); + }, + + /** + * Find the new position and perform the move + */ + updatePosition: function (point) { + var chart = this.chart, + label = this.label, + pos = (this.options.positioner || this.getPosition).call( + this, + label.width, + label.height, + point + ); + + // do the move + this.move( + mathRound(pos.x), + mathRound(pos.y), + point.plotX + chart.plotLeft, + point.plotY + chart.plotTop + ); + } +}; +/** + * The mouse tracker object. All methods starting with "on" are primary DOM event handlers. + * Subsequent methods should be named differently from what they are doing. + * @param {Object} chart The Chart instance + * @param {Object} options The root options object + */ +function Pointer(chart, options) { + this.init(chart, options); +} + +Pointer.prototype = { + /** + * Initialize Pointer + */ + init: function (chart, options) { + + var chartOptions = options.chart, + chartEvents = chartOptions.events, + zoomType = useCanVG ? '' : chartOptions.zoomType, + inverted = chart.inverted, + zoomX, + zoomY; + + // Store references + this.options = options; + this.chart = chart; + + // Zoom status + this.zoomX = zoomX = /x/.test(zoomType); + this.zoomY = zoomY = /y/.test(zoomType); + this.zoomHor = (zoomX && !inverted) || (zoomY && inverted); + this.zoomVert = (zoomY && !inverted) || (zoomX && inverted); + + // Do we need to handle click on a touch device? + this.runChartClick = chartEvents && !!chartEvents.click; + + this.pinchDown = []; + this.lastValidTouch = {}; + + if (options.tooltip.enabled) { + chart.tooltip = new Tooltip(chart, options.tooltip); + } + + this.setDOMEvents(); + }, + + /** + * Add crossbrowser support for chartX and chartY + * @param {Object} e The event object in standard browsers + */ + normalize: function (e, chartPosition) { + var chartX, + chartY, + ePos; + + // common IE normalizing + e = e || win.event; + if (!e.target) { + e.target = e.srcElement; + } + + // Framework specific normalizing (#1165) + e = washMouseEvent(e); + + // iOS + ePos = e.touches ? e.touches.item(0) : e; + + // Get mouse position + if (!chartPosition) { + this.chartPosition = chartPosition = offset(this.chart.container); + } + + // chartX and chartY + if (ePos.pageX === UNDEFINED) { // IE < 9. #886. + chartX = mathMax(e.x, e.clientX - chartPosition.left); // #2005, #2129: the second case is + // for IE10 quirks mode within framesets + chartY = e.y; + } else { + chartX = ePos.pageX - chartPosition.left; + chartY = ePos.pageY - chartPosition.top; + } + + return extend(e, { + chartX: mathRound(chartX), + chartY: mathRound(chartY) + }); + }, + + /** + * Get the click position in terms of axis values. + * + * @param {Object} e A pointer event + */ + getCoordinates: function (e) { + var coordinates = { + xAxis: [], + yAxis: [] + }; + + each(this.chart.axes, function (axis) { + coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({ + axis: axis, + value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY']) + }); + }); + return coordinates; + }, + + /** + * Return the index in the tooltipPoints array, corresponding to pixel position in + * the plot area. + */ + getIndex: function (e) { + var chart = this.chart; + return chart.inverted ? + chart.plotHeight + chart.plotTop - e.chartY : + e.chartX - chart.plotLeft; + }, + + /** + * With line type charts with a single tracker, get the point closest to the mouse. + * Run Point.onMouseOver and display tooltip for the point or points. + */ + runPointActions: function (e) { + var pointer = this, + chart = pointer.chart, + series = chart.series, + tooltip = chart.tooltip, + point, + points, + hoverPoint = chart.hoverPoint, + hoverSeries = chart.hoverSeries, + i, + j, + distance = chart.chartWidth, + index = pointer.getIndex(e), + anchor; + + // shared tooltip + if (tooltip && pointer.options.tooltip.shared && !(hoverSeries && hoverSeries.noSharedTooltip)) { + points = []; + + // loop over all series and find the ones with points closest to the mouse + i = series.length; + for (j = 0; j < i; j++) { + if (series[j].visible && + series[j].options.enableMouseTracking !== false && + !series[j].noSharedTooltip && series[j].tooltipPoints.length) { + point = series[j].tooltipPoints[index]; + if (point && point.series) { // not a dummy point, #1544 + point._dist = mathAbs(index - point.clientX); + distance = mathMin(distance, point._dist); + points.push(point); + } + } + } + // remove furthest points + i = points.length; + while (i--) { + if (points[i]._dist > distance) { + points.splice(i, 1); + } + } + // refresh the tooltip if necessary + if (points.length && (points[0].clientX !== pointer.hoverX)) { + tooltip.refresh(points, e); + pointer.hoverX = points[0].clientX; + } + } + + // separate tooltip and general mouse events + if (hoverSeries && hoverSeries.tracker) { // only use for line-type series with common tracker + + // get the point + point = hoverSeries.tooltipPoints[index]; + + // a new point is hovered, refresh the tooltip + if (point && point !== hoverPoint) { + + // trigger the events + point.onMouseOver(e); + + } + + } else if (tooltip && tooltip.followPointer && !tooltip.isHidden) { + anchor = tooltip.getAnchor([{}], e); + tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] }); + } + }, + + + + /** + * Reset the tracking by hiding the tooltip, the hover series state and the hover point + * + * @param allowMove {Boolean} Instead of destroying the tooltip altogether, allow moving it if possible + */ + reset: function (allowMove) { + var pointer = this, + chart = pointer.chart, + hoverSeries = chart.hoverSeries, + hoverPoint = chart.hoverPoint, + tooltip = chart.tooltip, + tooltipPoints = tooltip && tooltip.shared ? chart.hoverPoints : hoverPoint; + + // Narrow in allowMove + allowMove = allowMove && tooltip && tooltipPoints; + + // Check if the points have moved outside the plot area, #1003 + if (allowMove && splat(tooltipPoints)[0].plotX === UNDEFINED) { + allowMove = false; + } + + // Just move the tooltip, #349 + if (allowMove) { + tooltip.refresh(tooltipPoints); + + // Full reset + } else { + + if (hoverPoint) { + hoverPoint.onMouseOut(); + } + + if (hoverSeries) { + hoverSeries.onMouseOut(); + } + + if (tooltip) { + tooltip.hide(); + tooltip.hideCrosshairs(); + } + + pointer.hoverX = null; + + } + }, + + /** + * Scale series groups to a certain scale and translation + */ + scaleGroups: function (attribs, clip) { + + var chart = this.chart, + seriesAttribs; + + // Scale each series + each(chart.series, function (series) { + seriesAttribs = attribs || series.getPlotBox(); // #1701 + if (series.xAxis && series.xAxis.zoomEnabled) { + series.group.attr(seriesAttribs); + if (series.markerGroup) { + series.markerGroup.attr(seriesAttribs); + series.markerGroup.clip(clip ? chart.clipRect : null); + } + if (series.dataLabelsGroup) { + series.dataLabelsGroup.attr(seriesAttribs); + } + } + }); + + // Clip + chart.clipRect.attr(clip || chart.clipBox); + }, + + /** + * Run translation operations for each direction (horizontal and vertical) independently + */ + pinchTranslateDirection: function (horiz, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) { + var chart = this.chart, + xy = horiz ? 'x' : 'y', + XY = horiz ? 'X' : 'Y', + sChartXY = 'chart' + XY, + wh = horiz ? 'width' : 'height', + plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')], + selectionWH, + selectionXY, + clipXY, + scale = 1, + inverted = chart.inverted, + bounds = chart.bounds[horiz ? 'h' : 'v'], + singleTouch = pinchDown.length === 1, + touch0Start = pinchDown[0][sChartXY], + touch0Now = touches[0][sChartXY], + touch1Start = !singleTouch && pinchDown[1][sChartXY], + touch1Now = !singleTouch && touches[1][sChartXY], + outOfBounds, + transformScale, + scaleKey, + setScale = function () { + if (!singleTouch && mathAbs(touch0Start - touch1Start) > 20) { // Don't zoom if fingers are too close on this axis + scale = mathAbs(touch0Now - touch1Now) / mathAbs(touch0Start - touch1Start); + } + + clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start; + selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale; + }; + + // Set the scale, first pass + setScale(); + + selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not + + // Out of bounds + if (selectionXY < bounds.min) { + selectionXY = bounds.min; + outOfBounds = true; + } else if (selectionXY + selectionWH > bounds.max) { + selectionXY = bounds.max - selectionWH; + outOfBounds = true; + } + + // Is the chart dragged off its bounds, determined by dataMin and dataMax? + if (outOfBounds) { + + // Modify the touchNow position in order to create an elastic drag movement. This indicates + // to the user that the chart is responsive but can't be dragged further. + touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]); + if (!singleTouch) { + touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]); + } + + // Set the scale, second pass to adapt to the modified touchNow positions + setScale(); + + } else { + lastValidTouch[xy] = [touch0Now, touch1Now]; + } + + + // Set geometry for clipping, selection and transformation + if (!inverted) { // TODO: implement clipping for inverted charts + clip[xy] = clipXY - plotLeftTop; + clip[wh] = selectionWH; + } + scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY; + transformScale = inverted ? 1 / scale : scale; + + selectionMarker[wh] = selectionWH; + selectionMarker[xy] = selectionXY; + transform[scaleKey] = scale; + transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start)); + }, + + /** + * Handle touch events with two touches + */ + pinch: function (e) { + + var self = this, + chart = self.chart, + pinchDown = self.pinchDown, + followTouchMove = chart.tooltip && chart.tooltip.options.followTouchMove, + touches = e.touches, + touchesLength = touches.length, + lastValidTouch = self.lastValidTouch, + zoomHor = self.zoomHor || self.pinchHor, + zoomVert = self.zoomVert || self.pinchVert, + hasZoom = zoomHor || zoomVert, + selectionMarker = self.selectionMarker, + transform = {}, + fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, PREFIX + 'tracker') && + chart.runTrackerClick) || chart.runChartClick), + clip = {}; + + // On touch devices, only proceed to trigger click if a handler is defined + if ((hasZoom || followTouchMove) && !fireClickEvent) { + e.preventDefault(); + } + + // Normalize each touch + map(touches, function (e) { + return self.normalize(e); + }); + + // Register the touch start position + if (e.type === 'touchstart') { + each(touches, function (e, i) { + pinchDown[i] = { chartX: e.chartX, chartY: e.chartY }; + }); + lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX]; + lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY]; + + // Identify the data bounds in pixels + each(chart.axes, function (axis) { + if (axis.zoomEnabled) { + var bounds = chart.bounds[axis.horiz ? 'h' : 'v'], + minPixelPadding = axis.minPixelPadding, + min = axis.toPixels(axis.dataMin), + max = axis.toPixels(axis.dataMax), + absMin = mathMin(min, max), + absMax = mathMax(min, max); + + // Store the bounds for use in the touchmove handler + bounds.min = mathMin(axis.pos, absMin - minPixelPadding); + bounds.max = mathMax(axis.pos + axis.len, absMax + minPixelPadding); + } + }); + + // Event type is touchmove, handle panning and pinching + } else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first + + + // Set the marker + if (!selectionMarker) { + self.selectionMarker = selectionMarker = extend({ + destroy: noop + }, chart.plotBox); + } + + + + if (zoomHor) { + self.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); + } + if (zoomVert) { + self.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); + } + + self.hasPinched = hasZoom; + + // Scale and translate the groups to provide visual feedback during pinching + self.scaleGroups(transform, clip); + + // Optionally move the tooltip on touchmove + if (!hasZoom && followTouchMove && touchesLength === 1) { + this.runPointActions(self.normalize(e)); + } + } + }, + + /** + * Start a drag operation + */ + dragStart: function (e) { + var chart = this.chart; + + // Record the start position + chart.mouseIsDown = e.type; + chart.cancelClick = false; + chart.mouseDownX = this.mouseDownX = e.chartX; + chart.mouseDownY = this.mouseDownY = e.chartY; + }, + + /** + * Perform a drag operation in response to a mousemove event while the mouse is down + */ + drag: function (e) { + + var chart = this.chart, + chartOptions = chart.options.chart, + chartX = e.chartX, + chartY = e.chartY, + zoomHor = this.zoomHor, + zoomVert = this.zoomVert, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + clickedInside, + size, + mouseDownX = this.mouseDownX, + mouseDownY = this.mouseDownY; + + // If the mouse is outside the plot area, adjust to cooordinates + // inside to prevent the selection marker from going outside + if (chartX < plotLeft) { + chartX = plotLeft; + } else if (chartX > plotLeft + plotWidth) { + chartX = plotLeft + plotWidth; + } + + if (chartY < plotTop) { + chartY = plotTop; + } else if (chartY > plotTop + plotHeight) { + chartY = plotTop + plotHeight; + } + + // determine if the mouse has moved more than 10px + this.hasDragged = Math.sqrt( + Math.pow(mouseDownX - chartX, 2) + + Math.pow(mouseDownY - chartY, 2) + ); + if (this.hasDragged > 10) { + clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop); + + // make a selection + if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside) { + if (!this.selectionMarker) { + this.selectionMarker = chart.renderer.rect( + plotLeft, + plotTop, + zoomHor ? 1 : plotWidth, + zoomVert ? 1 : plotHeight, + 0 + ) + .attr({ + fill: chartOptions.selectionMarkerFill || 'rgba(69,114,167,0.25)', + zIndex: 7 + }) + .add(); + } + } + + // adjust the width of the selection marker + if (this.selectionMarker && zoomHor) { + size = chartX - mouseDownX; + this.selectionMarker.attr({ + width: mathAbs(size), + x: (size > 0 ? 0 : size) + mouseDownX + }); + } + // adjust the height of the selection marker + if (this.selectionMarker && zoomVert) { + size = chartY - mouseDownY; + this.selectionMarker.attr({ + height: mathAbs(size), + y: (size > 0 ? 0 : size) + mouseDownY + }); + } + + // panning + if (clickedInside && !this.selectionMarker && chartOptions.panning) { + chart.pan(e, chartOptions.panning); + } + } + }, + + /** + * On mouse up or touch end across the entire document, drop the selection. + */ + drop: function (e) { + var chart = this.chart, + hasPinched = this.hasPinched; + + if (this.selectionMarker) { + var selectionData = { + xAxis: [], + yAxis: [], + originalEvent: e.originalEvent || e + }, + selectionBox = this.selectionMarker, + selectionLeft = selectionBox.x, + selectionTop = selectionBox.y, + runZoom; + // a selection has been made + if (this.hasDragged || hasPinched) { + + // record each axis' min and max + each(chart.axes, function (axis) { + if (axis.zoomEnabled) { + var horiz = axis.horiz, + selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop)), + selectionMax = axis.toValue((horiz ? selectionLeft + selectionBox.width : selectionTop + selectionBox.height)); + + if (!isNaN(selectionMin) && !isNaN(selectionMax)) { // #859 + selectionData[axis.xOrY + 'Axis'].push({ + axis: axis, + min: mathMin(selectionMin, selectionMax), // for reversed axes, + max: mathMax(selectionMin, selectionMax) + }); + runZoom = true; + } + } + }); + if (runZoom) { + fireEvent(chart, 'selection', selectionData, function (args) { + chart.zoom(extend(args, hasPinched ? { animation: false } : null)); + }); + } + + } + this.selectionMarker = this.selectionMarker.destroy(); + + // Reset scaling preview + if (hasPinched) { + this.scaleGroups(); + } + } + + // Reset all + if (chart) { // it may be destroyed on mouse up - #877 + css(chart.container, { cursor: chart._cursor }); + chart.cancelClick = this.hasDragged > 10; // #370 + chart.mouseIsDown = this.hasDragged = this.hasPinched = false; + this.pinchDown = []; + } + }, + + onContainerMouseDown: function (e) { + + e = this.normalize(e); + + // issue #295, dragging not always working in Firefox + if (e.preventDefault) { + e.preventDefault(); + } + + this.dragStart(e); + }, + + + + onDocumentMouseUp: function (e) { + this.drop(e); + }, + + /** + * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea. + * Issue #149 workaround. The mouseleave event does not always fire. + */ + onDocumentMouseMove: function (e) { + var chart = this.chart, + chartPosition = this.chartPosition, + hoverSeries = chart.hoverSeries; + + e = this.normalize(e, chartPosition); + + // If we're outside, hide the tooltip + if (chartPosition && hoverSeries && !this.inClass(e.target, 'highcharts-tracker') && + !chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { + this.reset(); + } + }, + + /** + * When mouse leaves the container, hide the tooltip. + */ + onContainerMouseLeave: function () { + this.reset(); + this.chartPosition = null; // also reset the chart position, used in #149 fix + }, + + // The mousemove, touchmove and touchstart event handler + onContainerMouseMove: function (e) { + + var chart = this.chart; + + // normalize + e = this.normalize(e); + + // #295 + e.returnValue = false; + + + if (chart.mouseIsDown === 'mousedown') { + this.drag(e); + } + + // Show the tooltip and run mouse over events (#977) + if ((this.inClass(e.target, 'highcharts-tracker') || + chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) && !chart.openMenu) { + this.runPointActions(e); + } + }, + + /** + * Utility to detect whether an element has, or has a parent with, a specific + * class name. Used on detection of tracker objects and on deciding whether + * hovering the tooltip should cause the active series to mouse out. + */ + inClass: function (element, className) { + var elemClassName; + while (element) { + elemClassName = attr(element, 'class'); + if (elemClassName) { + if (elemClassName.indexOf(className) !== -1) { + return true; + } else if (elemClassName.indexOf(PREFIX + 'container') !== -1) { + return false; + } + } + element = element.parentNode; + } + }, + + onTrackerMouseOut: function (e) { + var series = this.chart.hoverSeries; + if (series && !series.options.stickyTracking && !this.inClass(e.toElement || e.relatedTarget, PREFIX + 'tooltip')) { + series.onMouseOut(); + } + }, + + onContainerClick: function (e) { + var chart = this.chart, + hoverPoint = chart.hoverPoint, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + inverted = chart.inverted, + chartPosition, + plotX, + plotY; + + e = this.normalize(e); + e.cancelBubble = true; // IE specific + + if (!chart.cancelClick) { + + // On tracker click, fire the series and point events. #783, #1583 + if (hoverPoint && this.inClass(e.target, PREFIX + 'tracker')) { + chartPosition = this.chartPosition; + plotX = hoverPoint.plotX; + plotY = hoverPoint.plotY; + + // add page position info + extend(hoverPoint, { + pageX: chartPosition.left + plotLeft + + (inverted ? chart.plotWidth - plotY : plotX), + pageY: chartPosition.top + plotTop + + (inverted ? chart.plotHeight - plotX : plotY) + }); + + // the series click event + fireEvent(hoverPoint.series, 'click', extend(e, { + point: hoverPoint + })); + + // the point click event + if (chart.hoverPoint) { // it may be destroyed (#1844) + hoverPoint.firePointEvent('click', e); + } + + // When clicking outside a tracker, fire a chart event + } else { + extend(e, this.getCoordinates(e)); + + // fire a click event in the chart + if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) { + fireEvent(chart, 'click', e); + } + } + + + } + }, + + onContainerTouchStart: function (e) { + var chart = this.chart; + + if (e.touches.length === 1) { + + e = this.normalize(e); + + if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { + + // Prevent the click pseudo event from firing unless it is set in the options + /*if (!chart.runChartClick) { + e.preventDefault(); + }*/ + + // Run mouse events and display tooltip etc + this.runPointActions(e); + + this.pinch(e); + + } else { + // Hide the tooltip on touching outside the plot area (#1203) + this.reset(); + } + + } else if (e.touches.length === 2) { + this.pinch(e); + } + }, + + onContainerTouchMove: function (e) { + if (e.touches.length === 1 || e.touches.length === 2) { + this.pinch(e); + } + }, + + onDocumentTouchEnd: function (e) { + this.drop(e); + }, + + /** + * Set the JS DOM events on the container and document. This method should contain + * a one-to-one assignment between methods and their handlers. Any advanced logic should + * be moved to the handler reflecting the event's name. + */ + setDOMEvents: function () { + + var pointer = this, + container = pointer.chart.container, + events; + + this._events = events = [ + [container, 'onmousedown', 'onContainerMouseDown'], + [container, 'onmousemove', 'onContainerMouseMove'], + [container, 'onclick', 'onContainerClick'], + [container, 'mouseleave', 'onContainerMouseLeave'], + [doc, 'mousemove', 'onDocumentMouseMove'], + [doc, 'mouseup', 'onDocumentMouseUp'] + ]; + + if (hasTouch) { + events.push( + [container, 'ontouchstart', 'onContainerTouchStart'], + [container, 'ontouchmove', 'onContainerTouchMove'], + [doc, 'touchend', 'onDocumentTouchEnd'] + ); + } + + each(events, function (eventConfig) { + + // First, create the callback function that in turn calls the method on Pointer + pointer['_' + eventConfig[2]] = function (e) { + pointer[eventConfig[2]](e); + }; + + // Now attach the function, either as a direct property or through addEvent + if (eventConfig[1].indexOf('on') === 0) { + eventConfig[0][eventConfig[1]] = pointer['_' + eventConfig[2]]; + } else { + addEvent(eventConfig[0], eventConfig[1], pointer['_' + eventConfig[2]]); + } + }); + + + }, + + /** + * Destroys the Pointer object and disconnects DOM events. + */ + destroy: function () { + var pointer = this; + + // Release all DOM events + each(pointer._events, function (eventConfig) { + if (eventConfig[1].indexOf('on') === 0) { + eventConfig[0][eventConfig[1]] = null; // delete breaks oldIE + } else { + removeEvent(eventConfig[0], eventConfig[1], pointer['_' + eventConfig[2]]); + } + }); + delete pointer._events; + + // memory and CPU leak + clearInterval(pointer.tooltipTimeout); + } +}; +/** + * The overview of the chart's series + */ +function Legend(chart, options) { + this.init(chart, options); +} + +Legend.prototype = { + + /** + * Initialize the legend + */ + init: function (chart, options) { + + var legend = this, + itemStyle = options.itemStyle, + padding = pick(options.padding, 8), + itemMarginTop = options.itemMarginTop || 0; + + this.options = options; + + if (!options.enabled) { + return; + } + + legend.baseline = pInt(itemStyle.fontSize) + 3 + itemMarginTop; // used in Series prototype + legend.itemStyle = itemStyle; + legend.itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle); + legend.itemMarginTop = itemMarginTop; + legend.padding = padding; + legend.initialItemX = padding; + legend.initialItemY = padding - 5; // 5 is the number of pixels above the text + legend.maxItemWidth = 0; + legend.chart = chart; + legend.itemHeight = 0; + legend.lastLineHeight = 0; + + // Render it + legend.render(); + + // move checkboxes + addEvent(legend.chart, 'endResize', function () { + legend.positionCheckboxes(); + }); + + }, + + /** + * Set the colors for the legend item + * @param {Object} item A Series or Point instance + * @param {Object} visible Dimmed or colored + */ + colorizeItem: function (item, visible) { + var legend = this, + options = legend.options, + legendItem = item.legendItem, + legendLine = item.legendLine, + legendSymbol = item.legendSymbol, + hiddenColor = legend.itemHiddenStyle.color, + textColor = visible ? options.itemStyle.color : hiddenColor, + symbolColor = visible ? item.color : hiddenColor, + markerOptions = item.options && item.options.marker, + symbolAttr = { + stroke: symbolColor, + fill: symbolColor + }, + key, + val; + + if (legendItem) { + legendItem.css({ fill: textColor, color: textColor }); // color for #1553, oldIE + } + if (legendLine) { + legendLine.attr({ stroke: symbolColor }); + } + + if (legendSymbol) { + + // Apply marker options + if (markerOptions && legendSymbol.isMarker) { // #585 + markerOptions = item.convertAttribs(markerOptions); + for (key in markerOptions) { + val = markerOptions[key]; + if (val !== UNDEFINED) { + symbolAttr[key] = val; + } + } + } + + legendSymbol.attr(symbolAttr); + } + }, + + /** + * Position the legend item + * @param {Object} item A Series or Point instance + */ + positionItem: function (item) { + var legend = this, + options = legend.options, + symbolPadding = options.symbolPadding, + ltr = !options.rtl, + legendItemPos = item._legendItemPos, + itemX = legendItemPos[0], + itemY = legendItemPos[1], + checkbox = item.checkbox; + + if (item.legendGroup) { + item.legendGroup.translate( + ltr ? itemX : legend.legendWidth - itemX - 2 * symbolPadding - 4, + itemY + ); + } + + if (checkbox) { + checkbox.x = itemX; + checkbox.y = itemY; + } + }, + + /** + * Destroy a single legend item + * @param {Object} item The series or point + */ + destroyItem: function (item) { + var checkbox = item.checkbox; + + // destroy SVG elements + each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function (key) { + if (item[key]) { + item[key] = item[key].destroy(); + } + }); + + if (checkbox) { + discardElement(item.checkbox); + } + }, + + /** + * Destroys the legend. + */ + destroy: function () { + var legend = this, + legendGroup = legend.group, + box = legend.box; + + if (box) { + legend.box = box.destroy(); + } + + if (legendGroup) { + legend.group = legendGroup.destroy(); + } + }, + + /** + * Position the checkboxes after the width is determined + */ + positionCheckboxes: function (scrollOffset) { + var alignAttr = this.group.alignAttr, + translateY, + clipHeight = this.clipHeight || this.legendHeight; + + if (alignAttr) { + translateY = alignAttr.translateY; + each(this.allItems, function (item) { + var checkbox = item.checkbox, + top; + + if (checkbox) { + top = (translateY + checkbox.y + (scrollOffset || 0) + 3); + css(checkbox, { + left: (alignAttr.translateX + item.legendItemWidth + checkbox.x - 20) + PX, + top: top + PX, + display: top > translateY - 6 && top < translateY + clipHeight - 6 ? '' : NONE + }); + } + }); + } + }, + + /** + * Render the legend title on top of the legend + */ + renderTitle: function () { + var options = this.options, + padding = this.padding, + titleOptions = options.title, + titleHeight = 0, + bBox; + + if (titleOptions.text) { + if (!this.title) { + this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, null, null, null, null, null, 'legend-title') + .attr({ zIndex: 1 }) + .css(titleOptions.style) + .add(this.group); + } + bBox = this.title.getBBox(); + titleHeight = bBox.height; + this.offsetWidth = bBox.width; // #1717 + this.contentGroup.attr({ translateY: titleHeight }); + } + this.titleHeight = titleHeight; + }, + + /** + * Render a single specific legend item + * @param {Object} item A series or point + */ + renderItem: function (item) { + var legend = this, + chart = legend.chart, + renderer = chart.renderer, + options = legend.options, + horizontal = options.layout === 'horizontal', + symbolWidth = options.symbolWidth, + symbolPadding = options.symbolPadding, + itemStyle = legend.itemStyle, + itemHiddenStyle = legend.itemHiddenStyle, + padding = legend.padding, + itemDistance = horizontal ? pick(options.itemDistance, 8) : 0, + ltr = !options.rtl, + itemHeight, + widthOption = options.width, + itemMarginBottom = options.itemMarginBottom || 0, + itemMarginTop = legend.itemMarginTop, + initialItemX = legend.initialItemX, + bBox, + itemWidth, + li = item.legendItem, + series = item.series || item, + itemOptions = series.options, + showCheckbox = itemOptions.showCheckbox, + useHTML = options.useHTML; + + if (!li) { // generate it once, later move it + + // Generate the group box + // A group to hold the symbol and text. Text is to be appended in Legend class. + item.legendGroup = renderer.g('legend-item') + .attr({ zIndex: 1 }) + .add(legend.scrollGroup); + + // Draw the legend symbol inside the group box + series.drawLegendSymbol(legend, item); + + // Generate the list item text and add it to the group + item.legendItem = li = renderer.text( + options.labelFormat ? format(options.labelFormat, item) : options.labelFormatter.call(item), + ltr ? symbolWidth + symbolPadding : -symbolPadding, + legend.baseline, + useHTML + ) + .css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021) + .attr({ + align: ltr ? 'left' : 'right', + zIndex: 2 + }) + .add(item.legendGroup); + + // Set the events on the item group, or in case of useHTML, the item itself (#1249) + (useHTML ? li : item.legendGroup).on('mouseover', function () { + item.setState(HOVER_STATE); + li.css(legend.options.itemHoverStyle); + }) + .on('mouseout', function () { + li.css(item.visible ? itemStyle : itemHiddenStyle); + item.setState(); + }) + .on('click', function (event) { + var strLegendItemClick = 'legendItemClick', + fnLegendItemClick = function () { + item.setVisible(); + }; + + // Pass over the click/touch event. #4. + event = { + browserEvent: event + }; + + // click the name or symbol + if (item.firePointEvent) { // point + item.firePointEvent(strLegendItemClick, event, fnLegendItemClick); + } else { + fireEvent(item, strLegendItemClick, event, fnLegendItemClick); + } + }); + + // Colorize the items + legend.colorizeItem(item, item.visible); + + // add the HTML checkbox on top + if (itemOptions && showCheckbox) { + item.checkbox = createElement('input', { + type: 'checkbox', + checked: item.selected, + defaultChecked: item.selected // required by IE7 + }, options.itemCheckboxStyle, chart.container); + + addEvent(item.checkbox, 'click', function (event) { + var target = event.target; + fireEvent(item, 'checkboxClick', { + checked: target.checked + }, + function () { + item.select(); + } + ); + }); + } + } + + // calculate the positions for the next line + bBox = li.getBBox(); + + itemWidth = item.legendItemWidth = + options.itemWidth || symbolWidth + symbolPadding + bBox.width + itemDistance + + (showCheckbox ? 20 : 0); + legend.itemHeight = itemHeight = bBox.height; + + // if the item exceeds the width, start a new line + if (horizontal && legend.itemX - initialItemX + itemWidth > + (widthOption || (chart.chartWidth - 2 * padding - initialItemX))) { + legend.itemX = initialItemX; + legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom; + legend.lastLineHeight = 0; // reset for next line + } + + // If the item exceeds the height, start a new column + /*if (!horizontal && legend.itemY + options.y + itemHeight > chart.chartHeight - spacingTop - spacingBottom) { + legend.itemY = legend.initialItemY; + legend.itemX += legend.maxItemWidth; + legend.maxItemWidth = 0; + }*/ + + // Set the edge positions + legend.maxItemWidth = mathMax(legend.maxItemWidth, itemWidth); + legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom; + legend.lastLineHeight = mathMax(itemHeight, legend.lastLineHeight); // #915 + + // cache the position of the newly generated or reordered items + item._legendItemPos = [legend.itemX, legend.itemY]; + + // advance + if (horizontal) { + legend.itemX += itemWidth; + + } else { + legend.itemY += itemMarginTop + itemHeight + itemMarginBottom; + legend.lastLineHeight = itemHeight; + } + + // the width of the widest item + legend.offsetWidth = widthOption || mathMax( + (horizontal ? legend.itemX - initialItemX - itemDistance : itemWidth) + padding, + legend.offsetWidth + ); + }, + + /** + * Render the legend. This method can be called both before and after + * chart.render. If called after, it will only rearrange items instead + * of creating new ones. + */ + render: function () { + var legend = this, + chart = legend.chart, + renderer = chart.renderer, + legendGroup = legend.group, + allItems, + display, + legendWidth, + legendHeight, + box = legend.box, + options = legend.options, + padding = legend.padding, + legendBorderWidth = options.borderWidth, + legendBackgroundColor = options.backgroundColor; + + legend.itemX = legend.initialItemX; + legend.itemY = legend.initialItemY; + legend.offsetWidth = 0; + legend.lastItemY = 0; + + if (!legendGroup) { + legend.group = legendGroup = renderer.g('legend') + .attr({ zIndex: 7 }) + .add(); + legend.contentGroup = renderer.g() + .attr({ zIndex: 1 }) // above background + .add(legendGroup); + legend.scrollGroup = renderer.g() + .add(legend.contentGroup); + } + + legend.renderTitle(); + + // add each series or point + allItems = []; + each(chart.series, function (serie) { + var seriesOptions = serie.options; + + if (!seriesOptions.showInLegend || defined(seriesOptions.linkedTo)) { + return; + } + + // use points or series for the legend item depending on legendType + allItems = allItems.concat( + serie.legendItems || + (seriesOptions.legendType === 'point' ? + serie.data : + serie) + ); + }); + + // sort by legendIndex + stableSort(allItems, function (a, b) { + return ((a.options && a.options.legendIndex) || 0) - ((b.options && b.options.legendIndex) || 0); + }); + + // reversed legend + if (options.reversed) { + allItems.reverse(); + } + + legend.allItems = allItems; + legend.display = display = !!allItems.length; + + // render the items + each(allItems, function (item) { + legend.renderItem(item); + }); + + // Draw the border + legendWidth = options.width || legend.offsetWidth; + legendHeight = legend.lastItemY + legend.lastLineHeight + legend.titleHeight; + + + legendHeight = legend.handleOverflow(legendHeight); + + if (legendBorderWidth || legendBackgroundColor) { + legendWidth += padding; + legendHeight += padding; + + if (!box) { + legend.box = box = renderer.rect( + 0, + 0, + legendWidth, + legendHeight, + options.borderRadius, + legendBorderWidth || 0 + ).attr({ + stroke: options.borderColor, + 'stroke-width': legendBorderWidth || 0, + fill: legendBackgroundColor || NONE + }) + .add(legendGroup) + .shadow(options.shadow); + box.isNew = true; + + } else if (legendWidth > 0 && legendHeight > 0) { + box[box.isNew ? 'attr' : 'animate']( + box.crisp(null, null, null, legendWidth, legendHeight) + ); + box.isNew = false; + } + + // hide the border if no items + box[display ? 'show' : 'hide'](); + } + + legend.legendWidth = legendWidth; + legend.legendHeight = legendHeight; + + // Now that the legend width and height are established, put the items in the + // final position + each(allItems, function (item) { + legend.positionItem(item); + }); + + // 1.x compatibility: positioning based on style + /*var props = ['left', 'right', 'top', 'bottom'], + prop, + i = 4; + while (i--) { + prop = props[i]; + if (options.style[prop] && options.style[prop] !== 'auto') { + options[i < 2 ? 'align' : 'verticalAlign'] = prop; + options[i < 2 ? 'x' : 'y'] = pInt(options.style[prop]) * (i % 2 ? -1 : 1); + } + }*/ + + if (display) { + legendGroup.align(extend({ + width: legendWidth, + height: legendHeight + }, options), true, 'spacingBox'); + } + + if (!chart.isResizing) { + this.positionCheckboxes(); + } + }, + + /** + * Set up the overflow handling by adding navigation with up and down arrows below the + * legend. + */ + handleOverflow: function (legendHeight) { + var legend = this, + chart = this.chart, + renderer = chart.renderer, + pageCount, + options = this.options, + optionsY = options.y, + alignTop = options.verticalAlign === 'top', + spaceHeight = chart.spacingBox.height + (alignTop ? -optionsY : optionsY) - this.padding, + maxHeight = options.maxHeight, + clipHeight, + clipRect = this.clipRect, + navOptions = options.navigation, + animation = pick(navOptions.animation, true), + arrowSize = navOptions.arrowSize || 12, + nav = this.nav; + + // Adjust the height + if (options.layout === 'horizontal') { + spaceHeight /= 2; + } + if (maxHeight) { + spaceHeight = mathMin(spaceHeight, maxHeight); + } + + // Reset the legend height and adjust the clipping rectangle + if (legendHeight > spaceHeight && !options.useHTML) { + + this.clipHeight = clipHeight = spaceHeight - 20 - this.titleHeight; + this.pageCount = pageCount = mathCeil(legendHeight / clipHeight); + this.currentPage = pick(this.currentPage, 1); + this.fullHeight = legendHeight; + + // Only apply clipping if needed. Clipping causes blurred legend in PDF export (#1787) + if (!clipRect) { + clipRect = legend.clipRect = renderer.clipRect(0, 0, 9999, 0); + legend.contentGroup.clip(clipRect); + } + clipRect.attr({ + height: clipHeight + }); + + // Add navigation elements + if (!nav) { + this.nav = nav = renderer.g().attr({ zIndex: 1 }).add(this.group); + this.up = renderer.symbol('triangle', 0, 0, arrowSize, arrowSize) + .on('click', function () { + legend.scroll(-1, animation); + }) + .add(nav); + this.pager = renderer.text('', 15, 10) + .css(navOptions.style) + .add(nav); + this.down = renderer.symbol('triangle-down', 0, 0, arrowSize, arrowSize) + .on('click', function () { + legend.scroll(1, animation); + }) + .add(nav); + } + + // Set initial position + legend.scroll(0); + + legendHeight = spaceHeight; + + } else if (nav) { + clipRect.attr({ + height: chart.chartHeight + }); + nav.hide(); + this.scrollGroup.attr({ + translateY: 1 + }); + this.clipHeight = 0; // #1379 + } + + return legendHeight; + }, + + /** + * Scroll the legend by a number of pages + * @param {Object} scrollBy + * @param {Object} animation + */ + scroll: function (scrollBy, animation) { + var pageCount = this.pageCount, + currentPage = this.currentPage + scrollBy, + clipHeight = this.clipHeight, + navOptions = this.options.navigation, + activeColor = navOptions.activeColor, + inactiveColor = navOptions.inactiveColor, + pager = this.pager, + padding = this.padding, + scrollOffset; + + // When resizing while looking at the last page + if (currentPage > pageCount) { + currentPage = pageCount; + } + + if (currentPage > 0) { + + if (animation !== UNDEFINED) { + setAnimation(animation, this.chart); + } + + this.nav.attr({ + translateX: padding, + translateY: clipHeight + 7 + this.titleHeight, + visibility: VISIBLE + }); + this.up.attr({ + fill: currentPage === 1 ? inactiveColor : activeColor + }) + .css({ + cursor: currentPage === 1 ? 'default' : 'pointer' + }); + pager.attr({ + text: currentPage + '/' + this.pageCount + }); + this.down.attr({ + x: 18 + this.pager.getBBox().width, // adjust to text width + fill: currentPage === pageCount ? inactiveColor : activeColor + }) + .css({ + cursor: currentPage === pageCount ? 'default' : 'pointer' + }); + + scrollOffset = -mathMin(clipHeight * (currentPage - 1), this.fullHeight - clipHeight + padding) + 1; + this.scrollGroup.animate({ + translateY: scrollOffset + }); + pager.attr({ + text: currentPage + '/' + pageCount + }); + + + this.currentPage = currentPage; + this.positionCheckboxes(scrollOffset); + } + + } + +}; + +// Workaround for #2030, horizontal legend items not displaying in IE11 Preview. +// TODO: When IE11 is released, check again for this bug, and remove the fix +// or make a better one. +if (/Trident.*?11\.0/.test(userAgent)) { + wrap(Legend.prototype, 'positionItem', function (proceed, item) { + var legend = this; + setTimeout(function () { + proceed.call(legend, item); + }); + }); +} + +/** + * The chart class + * @param {Object} options + * @param {Function} callback Function to run when the chart has loaded + */ +function Chart() { + this.init.apply(this, arguments); +} + +Chart.prototype = { + + /** + * Initialize the chart + */ + init: function (userOptions, callback) { + + // Handle regular options + var options, + seriesOptions = userOptions.series; // skip merging data points to increase performance + + userOptions.series = null; + options = merge(defaultOptions, userOptions); // do the merge + options.series = userOptions.series = seriesOptions; // set back the series data + + var optionsChart = options.chart; + + // Create margin & spacing array + this.margin = this.splashArray('margin', optionsChart); + this.spacing = this.splashArray('spacing', optionsChart); + + var chartEvents = optionsChart.events; + + //this.runChartClick = chartEvents && !!chartEvents.click; + this.bounds = { h: {}, v: {} }; // Pixel data bounds for touch zoom + + this.callback = callback; + this.isResizing = 0; + this.options = options; + //chartTitleOptions = UNDEFINED; + //chartSubtitleOptions = UNDEFINED; + + this.axes = []; + this.series = []; + this.hasCartesianSeries = optionsChart.showAxes; + //this.axisOffset = UNDEFINED; + //this.maxTicks = UNDEFINED; // handle the greatest amount of ticks on grouped axes + //this.inverted = UNDEFINED; + //this.loadingShown = UNDEFINED; + //this.container = UNDEFINED; + //this.chartWidth = UNDEFINED; + //this.chartHeight = UNDEFINED; + //this.marginRight = UNDEFINED; + //this.marginBottom = UNDEFINED; + //this.containerWidth = UNDEFINED; + //this.containerHeight = UNDEFINED; + //this.oldChartWidth = UNDEFINED; + //this.oldChartHeight = UNDEFINED; + + //this.renderTo = UNDEFINED; + //this.renderToClone = UNDEFINED; + + //this.spacingBox = UNDEFINED + + //this.legend = UNDEFINED; + + // Elements + //this.chartBackground = UNDEFINED; + //this.plotBackground = UNDEFINED; + //this.plotBGImage = UNDEFINED; + //this.plotBorder = UNDEFINED; + //this.loadingDiv = UNDEFINED; + //this.loadingSpan = UNDEFINED; + + var chart = this, + eventType; + + // Add the chart to the global lookup + chart.index = charts.length; + charts.push(chart); + + // Set up auto resize + if (optionsChart.reflow !== false) { + addEvent(chart, 'load', function () { + chart.initReflow(); + }); + } + + // Chart event handlers + if (chartEvents) { + for (eventType in chartEvents) { + addEvent(chart, eventType, chartEvents[eventType]); + } + } + + chart.xAxis = []; + chart.yAxis = []; + + // Expose methods and variables + chart.animation = useCanVG ? false : pick(optionsChart.animation, true); + chart.pointCount = 0; + chart.counters = new ChartCounters(); + + chart.firstRender(); + }, + + /** + * Initialize an individual series, called internally before render time + */ + initSeries: function (options) { + var chart = this, + optionsChart = chart.options.chart, + type = options.type || optionsChart.type || optionsChart.defaultSeriesType, + series, + constr = seriesTypes[type]; + + // No such series type + if (!constr) { + error(17, true); + } + + series = new constr(); + series.init(this, options); + return series; + }, + + /** + * Add a series dynamically after time + * + * @param {Object} options The config options + * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true. + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + * + * @return {Object} series The newly created series object + */ + addSeries: function (options, redraw, animation) { + var series, + chart = this; + + if (options) { + redraw = pick(redraw, true); // defaults to true + + fireEvent(chart, 'addSeries', { options: options }, function () { + series = chart.initSeries(options); + + chart.isDirtyLegend = true; // the series array is out of sync with the display + chart.linkSeries(); + if (redraw) { + chart.redraw(animation); + } + }); + } + + return series; + }, + + /** + * Add an axis to the chart + * @param {Object} options The axis option + * @param {Boolean} isX Whether it is an X axis or a value axis + */ + addAxis: function (options, isX, redraw, animation) { + var key = isX ? 'xAxis' : 'yAxis', + chartOptions = this.options, + axis; + + /*jslint unused: false*/ + axis = new Axis(this, merge(options, { + index: this[key].length, + isX: isX + })); + /*jslint unused: true*/ + + // Push the new axis options to the chart options + chartOptions[key] = splat(chartOptions[key] || {}); + chartOptions[key].push(options); + + if (pick(redraw, true)) { + this.redraw(animation); + } + }, + + /** + * Check whether a given point is within the plot area + * + * @param {Number} plotX Pixel x relative to the plot area + * @param {Number} plotY Pixel y relative to the plot area + * @param {Boolean} inverted Whether the chart is inverted + */ + isInsidePlot: function (plotX, plotY, inverted) { + var x = inverted ? plotY : plotX, + y = inverted ? plotX : plotY; + + return x >= 0 && + x <= this.plotWidth && + y >= 0 && + y <= this.plotHeight; + }, + + /** + * Adjust all axes tick amounts + */ + adjustTickAmounts: function () { + if (this.options.chart.alignTicks !== false) { + each(this.axes, function (axis) { + axis.adjustTickAmount(); + }); + } + this.maxTicks = null; + }, + + /** + * Redraw legend, axes or series based on updated data + * + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + redraw: function (animation) { + var chart = this, + axes = chart.axes, + series = chart.series, + pointer = chart.pointer, + legend = chart.legend, + redrawLegend = chart.isDirtyLegend, + hasStackedSeries, + hasDirtyStacks, + isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed? + seriesLength = series.length, + i = seriesLength, + serie, + renderer = chart.renderer, + isHiddenChart = renderer.isHidden(), + afterRedraw = []; + + setAnimation(animation, chart); + + if (isHiddenChart) { + chart.cloneRenderTo(); + } + + // Adjust title layout (reflow multiline text) + chart.layOutTitles(); + + // link stacked series + while (i--) { + serie = series[i]; + + if (serie.options.stacking) { + hasStackedSeries = true; + + if (serie.isDirty) { + hasDirtyStacks = true; + break; + } + } + } + if (hasDirtyStacks) { // mark others as dirty + i = seriesLength; + while (i--) { + serie = series[i]; + if (serie.options.stacking) { + serie.isDirty = true; + } + } + } + + // handle updated data in the series + each(series, function (serie) { + if (serie.isDirty) { // prepare the data so axis can read it + if (serie.options.legendType === 'point') { + redrawLegend = true; + } + } + }); + + // handle added or removed series + if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed + // draw legend graphics + legend.render(); + + chart.isDirtyLegend = false; + } + + // reset stacks + if (hasStackedSeries) { + chart.getStacks(); + } + + + if (chart.hasCartesianSeries) { + if (!chart.isResizing) { + + // reset maxTicks + chart.maxTicks = null; + + // set axes scales + each(axes, function (axis) { + axis.setScale(); + }); + } + + chart.adjustTickAmounts(); + chart.getMargins(); + + // If one axis is dirty, all axes must be redrawn (#792, #2169) + each(axes, function (axis) { + if (axis.isDirty) { + isDirtyBox = true; + } + }); + + // redraw axes + each(axes, function (axis) { + + // Fire 'afterSetExtremes' only if extremes are set + if (axis.isDirtyExtremes) { // #821 + axis.isDirtyExtremes = false; + afterRedraw.push(function () { // prevent a recursive call to chart.redraw() (#1119) + fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751 + delete axis.eventArgs; + }); + } + + if (isDirtyBox || hasStackedSeries) { + axis.redraw(); + } + }); + + + } + // the plot areas size has changed + if (isDirtyBox) { + chart.drawChartBox(); + } + + + // redraw affected series + each(series, function (serie) { + if (serie.isDirty && serie.visible && + (!serie.isCartesian || serie.xAxis)) { // issue #153 + serie.redraw(); + } + }); + + // move tooltip or reset + if (pointer && pointer.reset) { + pointer.reset(true); + } + + // redraw if canvas + renderer.draw(); + + // fire the event + fireEvent(chart, 'redraw'); // jQuery breaks this when calling it from addEvent. Overwrites chart.redraw + + if (isHiddenChart) { + chart.cloneRenderTo(true); + } + + // Fire callbacks that are put on hold until after the redraw + each(afterRedraw, function (callback) { + callback.call(); + }); + }, + + + + /** + * Dim the chart and show a loading text or symbol + * @param {String} str An optional text to show in the loading label instead of the default one + */ + showLoading: function (str) { + var chart = this, + options = chart.options, + loadingDiv = chart.loadingDiv; + + var loadingOptions = options.loading; + + // create the layer at the first call + if (!loadingDiv) { + chart.loadingDiv = loadingDiv = createElement(DIV, { + className: PREFIX + 'loading' + }, extend(loadingOptions.style, { + zIndex: 10, + display: NONE + }), chart.container); + + chart.loadingSpan = createElement( + 'span', + null, + loadingOptions.labelStyle, + loadingDiv + ); + + } + + // update text + chart.loadingSpan.innerHTML = str || options.lang.loading; + + // show it + if (!chart.loadingShown) { + css(loadingDiv, { + opacity: 0, + display: '', + left: chart.plotLeft + PX, + top: chart.plotTop + PX, + width: chart.plotWidth + PX, + height: chart.plotHeight + PX + }); + animate(loadingDiv, { + opacity: loadingOptions.style.opacity + }, { + duration: loadingOptions.showDuration || 0 + }); + chart.loadingShown = true; + } + }, + + /** + * Hide the loading layer + */ + hideLoading: function () { + var options = this.options, + loadingDiv = this.loadingDiv; + + if (loadingDiv) { + animate(loadingDiv, { + opacity: 0 + }, { + duration: options.loading.hideDuration || 100, + complete: function () { + css(loadingDiv, { display: NONE }); + } + }); + } + this.loadingShown = false; + }, + + /** + * Get an axis, series or point object by id. + * @param id {String} The id as given in the configuration options + */ + get: function (id) { + var chart = this, + axes = chart.axes, + series = chart.series; + + var i, + j, + points; + + // search axes + for (i = 0; i < axes.length; i++) { + if (axes[i].options.id === id) { + return axes[i]; + } + } + + // search series + for (i = 0; i < series.length; i++) { + if (series[i].options.id === id) { + return series[i]; + } + } + + // search points + for (i = 0; i < series.length; i++) { + points = series[i].points || []; + for (j = 0; j < points.length; j++) { + if (points[j].id === id) { + return points[j]; + } + } + } + return null; + }, + + /** + * Create the Axis instances based on the config options + */ + getAxes: function () { + var chart = this, + options = this.options, + xAxisOptions = options.xAxis = splat(options.xAxis || {}), + yAxisOptions = options.yAxis = splat(options.yAxis || {}), + optionsArray, + axis; + + // make sure the options are arrays and add some members + each(xAxisOptions, function (axis, i) { + axis.index = i; + axis.isX = true; + }); + + each(yAxisOptions, function (axis, i) { + axis.index = i; + }); + + // concatenate all axis options into one array + optionsArray = xAxisOptions.concat(yAxisOptions); + + each(optionsArray, function (axisOptions) { + axis = new Axis(chart, axisOptions); + }); + + chart.adjustTickAmounts(); + }, + + + /** + * Get the currently selected points from all series + */ + getSelectedPoints: function () { + var points = []; + each(this.series, function (serie) { + points = points.concat(grep(serie.points || [], function (point) { + return point.selected; + })); + }); + return points; + }, + + /** + * Get the currently selected series + */ + getSelectedSeries: function () { + return grep(this.series, function (serie) { + return serie.selected; + }); + }, + + /** + * Generate stacks for each series and calculate stacks total values + */ + getStacks: function () { + var chart = this; + + // reset stacks for each yAxis + each(chart.yAxis, function (axis) { + if (axis.stacks && axis.hasVisibleSeries) { + axis.oldStacks = axis.stacks; + } + }); + + each(chart.series, function (series) { + if (series.options.stacking && (series.visible === true || chart.options.chart.ignoreHiddenSeries === false)) { + series.stackKey = series.type + pick(series.options.stack, ''); + } + }); + }, + + /** + * Display the zoom button + */ + showResetZoom: function () { + var chart = this, + lang = defaultOptions.lang, + btnOptions = chart.options.chart.resetZoomButton, + theme = btnOptions.theme, + states = theme.states, + alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox'; + + this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, function () { chart.zoomOut(); }, theme, states && states.hover) + .attr({ + align: btnOptions.position.align, + title: lang.resetZoomTitle + }) + .add() + .align(btnOptions.position, false, alignTo); + + }, + + /** + * Zoom out to 1:1 + */ + zoomOut: function () { + var chart = this; + fireEvent(chart, 'selection', { resetSelection: true }, function () { + chart.zoom(); + }); + }, + + /** + * Zoom into a given portion of the chart given by axis coordinates + * @param {Object} event + */ + zoom: function (event) { + var chart = this, + hasZoomed, + pointer = chart.pointer, + displayButton = false, + resetZoomButton; + + // If zoom is called with no arguments, reset the axes + if (!event || event.resetSelection) { + each(chart.axes, function (axis) { + hasZoomed = axis.zoom(); + }); + } else { // else, zoom in on all axes + each(event.xAxis.concat(event.yAxis), function (axisData) { + var axis = axisData.axis, + isXAxis = axis.isXAxis; + + // don't zoom more than minRange + if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) { + hasZoomed = axis.zoom(axisData.min, axisData.max); + if (axis.displayBtn) { + displayButton = true; + } + } + }); + } + + // Show or hide the Reset zoom button + resetZoomButton = chart.resetZoomButton; + if (displayButton && !resetZoomButton) { + chart.showResetZoom(); + } else if (!displayButton && isObject(resetZoomButton)) { + chart.resetZoomButton = resetZoomButton.destroy(); + } + + + // Redraw + if (hasZoomed) { + chart.redraw( + pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation + ); + } + }, + + /** + * Pan the chart by dragging the mouse across the pane. This function is called + * on mouse move, and the distance to pan is computed from chartX compared to + * the first chartX position in the dragging operation. + */ + pan: function (e, panning) { + + var chart = this, + hoverPoints = chart.hoverPoints, + doRedraw; + + // remove active points for shared tooltip + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + + each(panning === 'xy' ? [1, 0] : [1], function (isX) { // xy is used in maps + var mousePos = e[isX ? 'chartX' : 'chartY'], + axis = chart[isX ? 'xAxis' : 'yAxis'][0], + startPos = chart[isX ? 'mouseDownX' : 'mouseDownY'], + halfPointRange = (axis.pointRange || 0) / 2, + extremes = axis.getExtremes(), + newMin = axis.toValue(startPos - mousePos, true) + halfPointRange, + newMax = axis.toValue(startPos + chart[isX ? 'plotWidth' : 'plotHeight'] - mousePos, true) - halfPointRange; + + if (axis.series.length && newMin > mathMin(extremes.dataMin, extremes.min) && newMax < mathMax(extremes.dataMax, extremes.max)) { + axis.setExtremes(newMin, newMax, false, false, { trigger: 'pan' }); + doRedraw = true; + } + + chart[isX ? 'mouseDownX' : 'mouseDownY'] = mousePos; // set new reference for next run + }); + + if (doRedraw) { + chart.redraw(false); + } + css(chart.container, { cursor: 'move' }); + }, + + /** + * Show the title and subtitle of the chart + * + * @param titleOptions {Object} New title options + * @param subtitleOptions {Object} New subtitle options + * + */ + setTitle: function (titleOptions, subtitleOptions) { + var chart = this, + options = chart.options, + chartTitleOptions, + chartSubtitleOptions; + + chartTitleOptions = options.title = merge(options.title, titleOptions); + chartSubtitleOptions = options.subtitle = merge(options.subtitle, subtitleOptions); + + // add title and subtitle + each([ + ['title', titleOptions, chartTitleOptions], + ['subtitle', subtitleOptions, chartSubtitleOptions] + ], function (arr) { + var name = arr[0], + title = chart[name], + titleOptions = arr[1], + chartTitleOptions = arr[2]; + + if (title && titleOptions) { + chart[name] = title = title.destroy(); // remove old + } + + if (chartTitleOptions && chartTitleOptions.text && !title) { + chart[name] = chart.renderer.text( + chartTitleOptions.text, + 0, + 0, + chartTitleOptions.useHTML + ) + .attr({ + align: chartTitleOptions.align, + 'class': PREFIX + name, + zIndex: chartTitleOptions.zIndex || 4 + }) + .css(chartTitleOptions.style) + .add(); + } + }); + chart.layOutTitles(); + }, + + /** + * Lay out the chart titles and cache the full offset height for use in getMargins + */ + layOutTitles: function () { + var titleOffset = 0, + title = this.title, + subtitle = this.subtitle, + options = this.options, + titleOptions = options.title, + subtitleOptions = options.subtitle, + autoWidth = this.spacingBox.width - 44; // 44 makes room for default context button + + if (title) { + title + .css({ width: (titleOptions.width || autoWidth) + PX }) + .align(extend({ y: 15 }, titleOptions), false, 'spacingBox'); + + if (!titleOptions.floating && !titleOptions.verticalAlign) { + titleOffset = title.getBBox().height; + + // Adjust for browser consistency + backwards compat after #776 fix + if (titleOffset >= 18 && titleOffset <= 25) { + titleOffset = 15; + } + } + } + if (subtitle) { + subtitle + .css({ width: (subtitleOptions.width || autoWidth) + PX }) + .align(extend({ y: titleOffset + titleOptions.margin }, subtitleOptions), false, 'spacingBox'); + + if (!subtitleOptions.floating && !subtitleOptions.verticalAlign) { + titleOffset = mathCeil(titleOffset + subtitle.getBBox().height); + } + } + + this.titleOffset = titleOffset; // used in getMargins + }, + + /** + * Get chart width and height according to options and container size + */ + getChartSize: function () { + var chart = this, + optionsChart = chart.options.chart, + renderTo = chart.renderToClone || chart.renderTo; + + // get inner width and height from jQuery (#824) + chart.containerWidth = adapterRun(renderTo, 'width'); + chart.containerHeight = adapterRun(renderTo, 'height'); + + chart.chartWidth = mathMax(0, optionsChart.width || chart.containerWidth || 600); // #1393, 1460 + chart.chartHeight = mathMax(0, pick(optionsChart.height, + // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7: + chart.containerHeight > 19 ? chart.containerHeight : 400)); + }, + + /** + * Create a clone of the chart's renderTo div and place it outside the viewport to allow + * size computation on chart.render and chart.redraw + */ + cloneRenderTo: function (revert) { + var clone = this.renderToClone, + container = this.container; + + // Destroy the clone and bring the container back to the real renderTo div + if (revert) { + if (clone) { + this.renderTo.appendChild(container); + discardElement(clone); + delete this.renderToClone; + } + + // Set up the clone + } else { + if (container && container.parentNode === this.renderTo) { + this.renderTo.removeChild(container); // do not clone this + } + this.renderToClone = clone = this.renderTo.cloneNode(0); + css(clone, { + position: ABSOLUTE, + top: '-9999px', + display: 'block' // #833 + }); + doc.body.appendChild(clone); + if (container) { + clone.appendChild(container); + } + } + }, + + /** + * Get the containing element, determine the size and create the inner container + * div to hold the chart + */ + getContainer: function () { + var chart = this, + container, + optionsChart = chart.options.chart, + chartWidth, + chartHeight, + renderTo, + indexAttrName = 'data-highcharts-chart', + oldChartIndex, + containerId; + + chart.renderTo = renderTo = optionsChart.renderTo; + containerId = PREFIX + idCounter++; + + if (isString(renderTo)) { + chart.renderTo = renderTo = doc.getElementById(renderTo); + } + + // Display an error if the renderTo is wrong + if (!renderTo) { + error(13, true); + } + + // If the container already holds a chart, destroy it + oldChartIndex = pInt(attr(renderTo, indexAttrName)); + if (!isNaN(oldChartIndex) && charts[oldChartIndex]) { + charts[oldChartIndex].destroy(); + } + + // Make a reference to the chart from the div + attr(renderTo, indexAttrName, chart.index); + + // remove previous chart + renderTo.innerHTML = ''; + + // If the container doesn't have an offsetWidth, it has or is a child of a node + // that has display:none. We need to temporarily move it out to a visible + // state to determine the size, else the legend and tooltips won't render + // properly + if (!renderTo.offsetWidth) { + chart.cloneRenderTo(); + } + + // get the width and height + chart.getChartSize(); + chartWidth = chart.chartWidth; + chartHeight = chart.chartHeight; + + // create the inner container + chart.container = container = createElement(DIV, { + className: PREFIX + 'container' + + (optionsChart.className ? ' ' + optionsChart.className : ''), + id: containerId + }, extend({ + position: RELATIVE, + overflow: HIDDEN, // needed for context menu (avoid scrollbars) and + // content overflow in IE + width: chartWidth + PX, + height: chartHeight + PX, + textAlign: 'left', + lineHeight: 'normal', // #427 + zIndex: 0, // #1072 + '-webkit-tap-highlight-color': 'rgba(0,0,0,0)' + }, optionsChart.style), + chart.renderToClone || renderTo + ); + + // cache the cursor (#1650) + chart._cursor = container.style.cursor; + + chart.renderer = + optionsChart.forExport ? // force SVG, used for SVG export + new SVGRenderer(container, chartWidth, chartHeight, true) : + new Renderer(container, chartWidth, chartHeight); + + if (useCanVG) { + // If we need canvg library, extend and configure the renderer + // to get the tracker for translating mouse events + chart.renderer.create(chart, container, chartWidth, chartHeight); + } + }, + + /** + * Calculate margins by rendering axis labels in a preliminary position. Title, + * subtitle and legend have already been rendered at this stage, but will be + * moved into their final positions + */ + getMargins: function () { + var chart = this, + spacing = chart.spacing, + axisOffset, + legend = chart.legend, + margin = chart.margin, + legendOptions = chart.options.legend, + legendMargin = pick(legendOptions.margin, 10), + legendX = legendOptions.x, + legendY = legendOptions.y, + align = legendOptions.align, + verticalAlign = legendOptions.verticalAlign, + titleOffset = chart.titleOffset; + + chart.resetMargins(); + axisOffset = chart.axisOffset; + + // Adjust for title and subtitle + if (titleOffset && !defined(margin[0])) { + chart.plotTop = mathMax(chart.plotTop, titleOffset + chart.options.title.margin + spacing[0]); + } + + // Adjust for legend + if (legend.display && !legendOptions.floating) { + if (align === 'right') { // horizontal alignment handled first + if (!defined(margin[1])) { + chart.marginRight = mathMax( + chart.marginRight, + legend.legendWidth - legendX + legendMargin + spacing[1] + ); + } + } else if (align === 'left') { + if (!defined(margin[3])) { + chart.plotLeft = mathMax( + chart.plotLeft, + legend.legendWidth + legendX + legendMargin + spacing[3] + ); + } + + } else if (verticalAlign === 'top') { + if (!defined(margin[0])) { + chart.plotTop = mathMax( + chart.plotTop, + legend.legendHeight + legendY + legendMargin + spacing[0] + ); + } + + } else if (verticalAlign === 'bottom') { + if (!defined(margin[2])) { + chart.marginBottom = mathMax( + chart.marginBottom, + legend.legendHeight - legendY + legendMargin + spacing[2] + ); + } + } + } + + // adjust for scroller + if (chart.extraBottomMargin) { + chart.marginBottom += chart.extraBottomMargin; + } + if (chart.extraTopMargin) { + chart.plotTop += chart.extraTopMargin; + } + + // pre-render axes to get labels offset width + if (chart.hasCartesianSeries) { + each(chart.axes, function (axis) { + axis.getOffset(); + }); + } + + if (!defined(margin[3])) { + chart.plotLeft += axisOffset[3]; + } + if (!defined(margin[0])) { + chart.plotTop += axisOffset[0]; + } + if (!defined(margin[2])) { + chart.marginBottom += axisOffset[2]; + } + if (!defined(margin[1])) { + chart.marginRight += axisOffset[1]; + } + + chart.setChartSize(); + + }, + + /** + * Add the event handlers necessary for auto resizing + * + */ + initReflow: function () { + var chart = this, + optionsChart = chart.options.chart, + renderTo = chart.renderTo, + reflowTimeout; + + function reflow(e) { + var width = optionsChart.width || adapterRun(renderTo, 'width'), + height = optionsChart.height || adapterRun(renderTo, 'height'), + target = e ? e.target : win; // #805 - MooTools doesn't supply e + + // Width and height checks for display:none. Target is doc in IE8 and Opera, + // win in Firefox, Chrome and IE9. + if (!chart.hasUserSize && width && height && (target === win || target === doc)) { + + if (width !== chart.containerWidth || height !== chart.containerHeight) { + clearTimeout(reflowTimeout); + chart.reflowTimeout = reflowTimeout = setTimeout(function () { + if (chart.container) { // It may have been destroyed in the meantime (#1257) + chart.setSize(width, height, false); + chart.hasUserSize = null; + } + }, 100); + } + chart.containerWidth = width; + chart.containerHeight = height; + } + } + chart.reflow = reflow; + addEvent(win, 'resize', reflow); + addEvent(chart, 'destroy', function () { + removeEvent(win, 'resize', reflow); + }); + }, + + /** + * Resize the chart to a given width and height + * @param {Number} width + * @param {Number} height + * @param {Object|Boolean} animation + */ + setSize: function (width, height, animation) { + var chart = this, + chartWidth, + chartHeight, + fireEndResize; + + // Handle the isResizing counter + chart.isResizing += 1; + fireEndResize = function () { + if (chart) { + fireEvent(chart, 'endResize', null, function () { + chart.isResizing -= 1; + }); + } + }; + + // set the animation for the current process + setAnimation(animation, chart); + + chart.oldChartHeight = chart.chartHeight; + chart.oldChartWidth = chart.chartWidth; + if (defined(width)) { + chart.chartWidth = chartWidth = mathMax(0, mathRound(width)); + chart.hasUserSize = !!chartWidth; + } + if (defined(height)) { + chart.chartHeight = chartHeight = mathMax(0, mathRound(height)); + } + + css(chart.container, { + width: chartWidth + PX, + height: chartHeight + PX + }); + chart.setChartSize(true); + chart.renderer.setSize(chartWidth, chartHeight, animation); + + // handle axes + chart.maxTicks = null; + each(chart.axes, function (axis) { + axis.isDirty = true; + axis.setScale(); + }); + + // make sure non-cartesian series are also handled + each(chart.series, function (serie) { + serie.isDirty = true; + }); + + chart.isDirtyLegend = true; // force legend redraw + chart.isDirtyBox = true; // force redraw of plot and chart border + + chart.getMargins(); + + chart.redraw(animation); + + + chart.oldChartHeight = null; + fireEvent(chart, 'resize'); + + // fire endResize and set isResizing back + // If animation is disabled, fire without delay + if (globalAnimation === false) { + fireEndResize(); + } else { // else set a timeout with the animation duration + setTimeout(fireEndResize, (globalAnimation && globalAnimation.duration) || 500); + } + }, + + /** + * Set the public chart properties. This is done before and after the pre-render + * to determine margin sizes + */ + setChartSize: function (skipAxes) { + var chart = this, + inverted = chart.inverted, + renderer = chart.renderer, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + optionsChart = chart.options.chart, + spacing = chart.spacing, + clipOffset = chart.clipOffset, + clipX, + clipY, + plotLeft, + plotTop, + plotWidth, + plotHeight, + plotBorderWidth; + + chart.plotLeft = plotLeft = mathRound(chart.plotLeft); + chart.plotTop = plotTop = mathRound(chart.plotTop); + chart.plotWidth = plotWidth = mathMax(0, mathRound(chartWidth - plotLeft - chart.marginRight)); + chart.plotHeight = plotHeight = mathMax(0, mathRound(chartHeight - plotTop - chart.marginBottom)); + + chart.plotSizeX = inverted ? plotHeight : plotWidth; + chart.plotSizeY = inverted ? plotWidth : plotHeight; + + chart.plotBorderWidth = optionsChart.plotBorderWidth || 0; + + // Set boxes used for alignment + chart.spacingBox = renderer.spacingBox = { + x: spacing[3], + y: spacing[0], + width: chartWidth - spacing[3] - spacing[1], + height: chartHeight - spacing[0] - spacing[2] + }; + chart.plotBox = renderer.plotBox = { + x: plotLeft, + y: plotTop, + width: plotWidth, + height: plotHeight + }; + + plotBorderWidth = 2 * mathFloor(chart.plotBorderWidth / 2); + clipX = mathCeil(mathMax(plotBorderWidth, clipOffset[3]) / 2); + clipY = mathCeil(mathMax(plotBorderWidth, clipOffset[0]) / 2); + chart.clipBox = { + x: clipX, + y: clipY, + width: mathFloor(chart.plotSizeX - mathMax(plotBorderWidth, clipOffset[1]) / 2 - clipX), + height: mathFloor(chart.plotSizeY - mathMax(plotBorderWidth, clipOffset[2]) / 2 - clipY) + }; + + if (!skipAxes) { + each(chart.axes, function (axis) { + axis.setAxisSize(); + axis.setAxisTranslation(); + }); + } + }, + + /** + * Initial margins before auto size margins are applied + */ + resetMargins: function () { + var chart = this, + spacing = chart.spacing, + margin = chart.margin; + + chart.plotTop = pick(margin[0], spacing[0]); + chart.marginRight = pick(margin[1], spacing[1]); + chart.marginBottom = pick(margin[2], spacing[2]); + chart.plotLeft = pick(margin[3], spacing[3]); + chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left + chart.clipOffset = [0, 0, 0, 0]; + }, + + /** + * Draw the borders and backgrounds for chart and plot area + */ + drawChartBox: function () { + var chart = this, + optionsChart = chart.options.chart, + renderer = chart.renderer, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + chartBackground = chart.chartBackground, + plotBackground = chart.plotBackground, + plotBorder = chart.plotBorder, + plotBGImage = chart.plotBGImage, + chartBorderWidth = optionsChart.borderWidth || 0, + chartBackgroundColor = optionsChart.backgroundColor, + plotBackgroundColor = optionsChart.plotBackgroundColor, + plotBackgroundImage = optionsChart.plotBackgroundImage, + plotBorderWidth = optionsChart.plotBorderWidth || 0, + mgn, + bgAttr, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + plotBox = chart.plotBox, + clipRect = chart.clipRect, + clipBox = chart.clipBox; + + // Chart area + mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0); + + if (chartBorderWidth || chartBackgroundColor) { + if (!chartBackground) { + + bgAttr = { + fill: chartBackgroundColor || NONE + }; + if (chartBorderWidth) { // #980 + bgAttr.stroke = optionsChart.borderColor; + bgAttr['stroke-width'] = chartBorderWidth; + } + chart.chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn, + optionsChart.borderRadius, chartBorderWidth) + .attr(bgAttr) + .add() + .shadow(optionsChart.shadow); + + } else { // resize + chartBackground.animate( + chartBackground.crisp(null, null, null, chartWidth - mgn, chartHeight - mgn) + ); + } + } + + + // Plot background + if (plotBackgroundColor) { + if (!plotBackground) { + chart.plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0) + .attr({ + fill: plotBackgroundColor + }) + .add() + .shadow(optionsChart.plotShadow); + } else { + plotBackground.animate(plotBox); + } + } + if (plotBackgroundImage) { + if (!plotBGImage) { + chart.plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight) + .add(); + } else { + plotBGImage.animate(plotBox); + } + } + + // Plot clip + if (!clipRect) { + chart.clipRect = renderer.clipRect(clipBox); + } else { + clipRect.animate({ + width: clipBox.width, + height: clipBox.height + }); + } + + // Plot area border + if (plotBorderWidth) { + if (!plotBorder) { + chart.plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, -plotBorderWidth) + .attr({ + stroke: optionsChart.plotBorderColor, + 'stroke-width': plotBorderWidth, + zIndex: 1 + }) + .add(); + } else { + plotBorder.animate( + plotBorder.crisp(null, plotLeft, plotTop, plotWidth, plotHeight) + ); + } + } + + // reset + chart.isDirtyBox = false; + }, + + /** + * Detect whether a certain chart property is needed based on inspecting its options + * and series. This mainly applies to the chart.invert property, and in extensions to + * the chart.angular and chart.polar properties. + */ + propFromSeries: function () { + var chart = this, + optionsChart = chart.options.chart, + klass, + seriesOptions = chart.options.series, + i, + value; + + + each(['inverted', 'angular', 'polar'], function (key) { + + // The default series type's class + klass = seriesTypes[optionsChart.type || optionsChart.defaultSeriesType]; + + // Get the value from available chart-wide properties + value = ( + chart[key] || // 1. it is set before + optionsChart[key] || // 2. it is set in the options + (klass && klass.prototype[key]) // 3. it's default series class requires it + ); + + // 4. Check if any the chart's series require it + i = seriesOptions && seriesOptions.length; + while (!value && i--) { + klass = seriesTypes[seriesOptions[i].type]; + if (klass && klass.prototype[key]) { + value = true; + } + } + + // Set the chart property + chart[key] = value; + }); + + }, + + /** + * Link two or more series together. This is done initially from Chart.render, + * and after Chart.addSeries and Series.remove. + */ + linkSeries: function () { + var chart = this, + chartSeries = chart.series; + + // Reset links + each(chartSeries, function (series) { + series.linkedSeries.length = 0; + }); + + // Apply new links + each(chartSeries, function (series) { + var linkedTo = series.options.linkedTo; + if (isString(linkedTo)) { + if (linkedTo === ':previous') { + linkedTo = chart.series[series.index - 1]; + } else { + linkedTo = chart.get(linkedTo); + } + if (linkedTo) { + linkedTo.linkedSeries.push(series); + series.linkedParent = linkedTo; + } + } + }); + }, + + /** + * Render all graphics for the chart + */ + render: function () { + var chart = this, + axes = chart.axes, + renderer = chart.renderer, + options = chart.options; + + var labels = options.labels, + credits = options.credits, + creditsHref; + + // Title + chart.setTitle(); + + + // Legend + chart.legend = new Legend(chart, options.legend); + + chart.getStacks(); // render stacks + + // Get margins by pre-rendering axes + // set axes scales + each(axes, function (axis) { + axis.setScale(); + }); + + chart.getMargins(); + + chart.maxTicks = null; // reset for second pass + each(axes, function (axis) { + axis.setTickPositions(true); // update to reflect the new margins + axis.setMaxTicks(); + }); + chart.adjustTickAmounts(); + chart.getMargins(); // second pass to check for new labels + + + // Draw the borders and backgrounds + chart.drawChartBox(); + + + // Axes + if (chart.hasCartesianSeries) { + each(axes, function (axis) { + axis.render(); + }); + } + + // The series + if (!chart.seriesGroup) { + chart.seriesGroup = renderer.g('series-group') + .attr({ zIndex: 3 }) + .add(); + } + each(chart.series, function (serie) { + serie.translate(); + serie.setTooltipPoints(); + serie.render(); + }); + + // Labels + if (labels.items) { + each(labels.items, function (label) { + var style = extend(labels.style, label.style), + x = pInt(style.left) + chart.plotLeft, + y = pInt(style.top) + chart.plotTop + 12; + + // delete to prevent rewriting in IE + delete style.left; + delete style.top; + + renderer.text( + label.html, + x, + y + ) + .attr({ zIndex: 2 }) + .css(style) + .add(); + + }); + } + + // Credits + if (credits.enabled && !chart.credits) { + creditsHref = credits.href; + chart.credits = renderer.text( + credits.text, + 0, + 0 + ) + .on('click', function () { + if (creditsHref) { + location.href = creditsHref; + } + }) + .attr({ + align: credits.position.align, + zIndex: 8 + }) + .css(credits.style) + .add() + .align(credits.position); + } + + // Set flag + chart.hasRendered = true; + + }, + + /** + * Clean up memory usage + */ + destroy: function () { + var chart = this, + axes = chart.axes, + series = chart.series, + container = chart.container, + i, + parentNode = container && container.parentNode; + + // fire the chart.destoy event + fireEvent(chart, 'destroy'); + + // Delete the chart from charts lookup array + charts[chart.index] = UNDEFINED; + chart.renderTo.removeAttribute('data-highcharts-chart'); + + // remove events + removeEvent(chart); + + // ==== Destroy collections: + // Destroy axes + i = axes.length; + while (i--) { + axes[i] = axes[i].destroy(); + } + + // Destroy each series + i = series.length; + while (i--) { + series[i] = series[i].destroy(); + } + + // ==== Destroy chart properties: + each(['title', 'subtitle', 'chartBackground', 'plotBackground', 'plotBGImage', + 'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'pointer', 'scroller', + 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'], function (name) { + var prop = chart[name]; + + if (prop && prop.destroy) { + chart[name] = prop.destroy(); + } + }); + + // remove container and all SVG + if (container) { // can break in IE when destroyed before finished loading + container.innerHTML = ''; + removeEvent(container); + if (parentNode) { + discardElement(container); + } + + } + + // clean it all up + for (i in chart) { + delete chart[i]; + } + + }, + + + /** + * VML namespaces can't be added until after complete. Listening + * for Perini's doScroll hack is not enough. + */ + isReadyToRender: function () { + var chart = this; + + // Note: in spite of JSLint's complaints, win == win.top is required + /*jslint eqeq: true*/ + if ((!hasSVG && (win == win.top && doc.readyState !== 'complete')) || (useCanVG && !win.canvg)) { + /*jslint eqeq: false*/ + if (useCanVG) { + // Delay rendering until canvg library is downloaded and ready + CanVGController.push(function () { chart.firstRender(); }, chart.options.global.canvasToolsURL); + } else { + doc.attachEvent('onreadystatechange', function () { + doc.detachEvent('onreadystatechange', chart.firstRender); + if (doc.readyState === 'complete') { + chart.firstRender(); + } + }); + } + return false; + } + return true; + }, + + /** + * Prepare for first rendering after all data are loaded + */ + firstRender: function () { + var chart = this, + options = chart.options, + callback = chart.callback; + + // Check whether the chart is ready to render + if (!chart.isReadyToRender()) { + return; + } + + // Create the container + chart.getContainer(); + + // Run an early event after the container and renderer are established + fireEvent(chart, 'init'); + + + chart.resetMargins(); + chart.setChartSize(); + + // Set the common chart properties (mainly invert) from the given series + chart.propFromSeries(); + + // get axes + chart.getAxes(); + + // Initialize the series + each(options.series || [], function (serieOptions) { + chart.initSeries(serieOptions); + }); + + chart.linkSeries(); + + // Run an event after axes and series are initialized, but before render. At this stage, + // the series data is indexed and cached in the xData and yData arrays, so we can access + // those before rendering. Used in Highstock. + fireEvent(chart, 'beforeRender'); + + // depends on inverted and on margins being set + chart.pointer = new Pointer(chart, options); + + chart.render(); + + // add canvas + chart.renderer.draw(); + // run callbacks + if (callback) { + callback.apply(chart, [chart]); + } + each(chart.callbacks, function (fn) { + fn.apply(chart, [chart]); + }); + + + // If the chart was rendered outside the top container, put it back in + chart.cloneRenderTo(true); + + fireEvent(chart, 'load'); + + }, + + /** + * Creates arrays for spacing and margin from given options. + */ + splashArray: function (target, options) { + var oVar = options[target], + tArray = isObject(oVar) ? oVar : [oVar, oVar, oVar, oVar]; + + return [pick(options[target + 'Top'], tArray[0]), + pick(options[target + 'Right'], tArray[1]), + pick(options[target + 'Bottom'], tArray[2]), + pick(options[target + 'Left'], tArray[3])]; + } +}; // end Chart + +// Hook for exporting module +Chart.prototype.callbacks = []; +/** + * The Point object and prototype. Inheritable and used as base for PiePoint + */ +var Point = function () {}; +Point.prototype = { + + /** + * Initialize the point + * @param {Object} series The series object containing this point + * @param {Object} options The data in either number, array or object format + */ + init: function (series, options, x) { + + var point = this, + colors; + point.series = series; + point.applyOptions(options, x); + point.pointAttr = {}; + + if (series.options.colorByPoint) { + colors = series.options.colors || series.chart.options.colors; + point.color = point.color || colors[series.colorCounter++]; + // loop back to zero + if (series.colorCounter === colors.length) { + series.colorCounter = 0; + } + } + + series.chart.pointCount++; + return point; + }, + /** + * Apply the options containing the x and y data and possible some extra properties. + * This is called on point init or from point.update. + * + * @param {Object} options + */ + applyOptions: function (options, x) { + var point = this, + series = point.series, + pointValKey = series.pointValKey; + + options = Point.prototype.optionsToObject.call(this, options); + + // copy options directly to point + extend(point, options); + point.options = point.options ? extend(point.options, options) : options; + + // For higher dimension series types. For instance, for ranges, point.y is mapped to point.low. + if (pointValKey) { + point.y = point[pointValKey]; + } + + // If no x is set by now, get auto incremented value. All points must have an + // x value, however the y value can be null to create a gap in the series + if (point.x === UNDEFINED && series) { + point.x = x === UNDEFINED ? series.autoIncrement() : x; + } + + return point; + }, + + /** + * Transform number or array configs into objects + */ + optionsToObject: function (options) { + var ret, + series = this.series, + pointArrayMap = series.pointArrayMap || ['y'], + valueCount = pointArrayMap.length, + firstItemType, + i = 0, + j = 0; + + if (typeof options === 'number' || options === null) { + ret = { y: options }; + + } else if (isArray(options)) { + ret = {}; + // with leading x value + if (options.length > valueCount) { + firstItemType = typeof options[0]; + if (firstItemType === 'string') { + ret.name = options[0]; + } else if (firstItemType === 'number') { + ret.x = options[0]; + } + i++; + } + while (j < valueCount) { + ret[pointArrayMap[j++]] = options[i++]; + } + } else if (typeof options === 'object') { + ret = options; + + // This is the fastest way to detect if there are individual point dataLabels that need + // to be considered in drawDataLabels. These can only occur in object configs. + if (options.dataLabels) { + series._hasPointLabels = true; + } + + // Same approach as above for markers + if (options.marker) { + series._hasPointMarkers = true; + } + } + return ret; + }, + + /** + * Destroy a point to clear memory. Its reference still stays in series.data. + */ + destroy: function () { + var point = this, + series = point.series, + chart = series.chart, + hoverPoints = chart.hoverPoints, + prop; + + chart.pointCount--; + + if (hoverPoints) { + point.setState(); + erase(hoverPoints, point); + if (!hoverPoints.length) { + chart.hoverPoints = null; + } + + } + if (point === chart.hoverPoint) { + point.onMouseOut(); + } + + // remove all events + if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive + removeEvent(point); + point.destroyElements(); + } + + if (point.legendItem) { // pies have legend items + chart.legend.destroyItem(point); + } + + for (prop in point) { + point[prop] = null; + } + + + }, + + /** + * Destroy SVG elements associated with the point + */ + destroyElements: function () { + var point = this, + props = ['graphic', 'dataLabel', 'dataLabelUpper', 'group', 'connector', 'shadowGroup'], + prop, + i = 6; + while (i--) { + prop = props[i]; + if (point[prop]) { + point[prop] = point[prop].destroy(); + } + } + }, + + /** + * Return the configuration hash needed for the data label and tooltip formatters + */ + getLabelConfig: function () { + var point = this; + return { + x: point.category, + y: point.y, + key: point.name || point.category, + series: point.series, + point: point, + percentage: point.percentage, + total: point.total || point.stackTotal + }; + }, + + /** + * Toggle the selection status of a point + * @param {Boolean} selected Whether to select or unselect the point. + * @param {Boolean} accumulate Whether to add to the previous selection. By default, + * this happens if the control key (Cmd on Mac) was pressed during clicking. + */ + select: function (selected, accumulate) { + var point = this, + series = point.series, + chart = series.chart; + + selected = pick(selected, !point.selected); + + // fire the event with the defalut handler + point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () { + point.selected = point.options.selected = selected; + series.options.data[inArray(point, series.data)] = point.options; + + point.setState(selected && SELECT_STATE); + + // unselect all other points unless Ctrl or Cmd + click + if (!accumulate) { + each(chart.getSelectedPoints(), function (loopPoint) { + if (loopPoint.selected && loopPoint !== point) { + loopPoint.selected = loopPoint.options.selected = false; + series.options.data[inArray(loopPoint, series.data)] = loopPoint.options; + loopPoint.setState(NORMAL_STATE); + loopPoint.firePointEvent('unselect'); + } + }); + } + }); + }, + + /** + * Runs on mouse over the point + */ + onMouseOver: function (e) { + var point = this, + series = point.series, + chart = series.chart, + tooltip = chart.tooltip, + hoverPoint = chart.hoverPoint; + + // set normal state to previous series + if (hoverPoint && hoverPoint !== point) { + hoverPoint.onMouseOut(); + } + + // trigger the event + point.firePointEvent('mouseOver'); + + // update the tooltip + if (tooltip && (!tooltip.shared || series.noSharedTooltip)) { + tooltip.refresh(point, e); + } + + // hover this + point.setState(HOVER_STATE); + chart.hoverPoint = point; + }, + + /** + * Runs on mouse out from the point + */ + onMouseOut: function () { + var chart = this.series.chart, + hoverPoints = chart.hoverPoints; + + if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887 + this.firePointEvent('mouseOut'); + + this.setState(); + chart.hoverPoint = null; + } + }, + + /** + * Extendable method for formatting each point's tooltip line + * + * @return {String} A string to be concatenated in to the common tooltip text + */ + tooltipFormatter: function (pointFormat) { + + // Insert options for valueDecimals, valuePrefix, and valueSuffix + var series = this.series, + seriesTooltipOptions = series.tooltipOptions, + valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''), + valuePrefix = seriesTooltipOptions.valuePrefix || '', + valueSuffix = seriesTooltipOptions.valueSuffix || ''; + + // Loop over the point array map and replace unformatted values with sprintf formatting markup + each(series.pointArrayMap || ['y'], function (key) { + key = '{point.' + key; // without the closing bracket + if (valuePrefix || valueSuffix) { + pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix); + } + pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}'); + }); + + return format(pointFormat, { + point: this, + series: this.series + }); + }, + + /** + * Update the point with new options (typically x/y data) and optionally redraw the series. + * + * @param {Object} options Point options as defined in the series.data array + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + * + */ + update: function (options, redraw, animation) { + var point = this, + series = point.series, + graphic = point.graphic, + i, + data = series.data, + chart = series.chart, + seriesOptions = series.options; + + redraw = pick(redraw, true); + + // fire the event with a default handler of doing the update + point.firePointEvent('update', { options: options }, function () { + + point.applyOptions(options); + + // update visuals + if (isObject(options)) { + series.getAttribs(); + if (graphic) { + if (options.marker && options.marker.symbol) { + point.graphic = graphic.destroy(); + } else { + graphic.attr(point.pointAttr[point.state || '']); + } + } + } + + // record changes in the parallel arrays + i = inArray(point, data); + series.xData[i] = point.x; + series.yData[i] = series.toYData ? series.toYData(point) : point.y; + series.zData[i] = point.z; + seriesOptions.data[i] = point.options; + + // redraw + series.isDirty = series.isDirtyData = true; + if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320 + chart.isDirtyBox = true; + } + + if (seriesOptions.legendType === 'point') { // #1831, #1885 + chart.legend.destroyItem(point); + } + if (redraw) { + chart.redraw(animation); + } + }); + }, + + /** + * Remove a point and optionally redraw the series and if necessary the axes + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + remove: function (redraw, animation) { + var point = this, + series = point.series, + points = series.points, + chart = series.chart, + i, + data = series.data; + + setAnimation(animation, chart); + redraw = pick(redraw, true); + + // fire the event with a default handler of removing the point + point.firePointEvent('remove', null, function () { + + // splice all the parallel arrays + i = inArray(point, data); + if (data.length === points.length) { + points.splice(i, 1); + } + data.splice(i, 1); + series.options.data.splice(i, 1); + series.xData.splice(i, 1); + series.yData.splice(i, 1); + series.zData.splice(i, 1); + + point.destroy(); + + + // redraw + series.isDirty = true; + series.isDirtyData = true; + if (redraw) { + chart.redraw(); + } + }); + + + }, + + /** + * Fire an event on the Point object. Must not be renamed to fireEvent, as this + * causes a name clash in MooTools + * @param {String} eventType + * @param {Object} eventArgs Additional event arguments + * @param {Function} defaultFunction Default event handler + */ + firePointEvent: function (eventType, eventArgs, defaultFunction) { + var point = this, + series = this.series, + seriesOptions = series.options; + + // load event handlers on demand to save time on mouseover/out + if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) { + this.importEvents(); + } + + // add default handler if in selection mode + if (eventType === 'click' && seriesOptions.allowPointSelect) { + defaultFunction = function (event) { + // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera + point.select(null, event.ctrlKey || event.metaKey || event.shiftKey); + }; + } + + fireEvent(this, eventType, eventArgs, defaultFunction); + }, + /** + * Import events from the series' and point's options. Only do it on + * demand, to save processing time on hovering. + */ + importEvents: function () { + if (!this.hasImportedEvents) { + var point = this, + options = merge(point.series.options.point, point.options), + events = options.events, + eventType; + + point.events = events; + + for (eventType in events) { + addEvent(point, eventType, events[eventType]); + } + this.hasImportedEvents = true; + + } + }, + + /** + * Set the point's state + * @param {String} state + */ + setState: function (state) { + var point = this, + plotX = point.plotX, + plotY = point.plotY, + series = point.series, + stateOptions = series.options.states, + markerOptions = defaultPlotOptions[series.type].marker && series.options.marker, + normalDisabled = markerOptions && !markerOptions.enabled, + markerStateOptions = markerOptions && markerOptions.states[state], + stateDisabled = markerStateOptions && markerStateOptions.enabled === false, + stateMarkerGraphic = series.stateMarkerGraphic, + pointMarker = point.marker || {}, + chart = series.chart, + radius, + newSymbol, + pointAttr = point.pointAttr; + + state = state || NORMAL_STATE; // empty string + + if ( + // already has this state + state === point.state || + // selected points don't respond to hover + (point.selected && state !== SELECT_STATE) || + // series' state options is disabled + (stateOptions[state] && stateOptions[state].enabled === false) || + // point marker's state options is disabled + (state && (stateDisabled || (normalDisabled && !markerStateOptions.enabled))) + + ) { + return; + } + + // apply hover styles to the existing point + if (point.graphic) { + radius = markerOptions && point.graphic.symbolName && pointAttr[state].r; + point.graphic.attr(merge( + pointAttr[state], + radius ? { // new symbol attributes (#507, #612) + x: plotX - radius, + y: plotY - radius, + width: 2 * radius, + height: 2 * radius + } : {} + )); + } else { + // if a graphic is not applied to each point in the normal state, create a shared + // graphic for the hover state + if (state && markerStateOptions) { + radius = markerStateOptions.radius; + newSymbol = pointMarker.symbol || series.symbol; + + // If the point has another symbol than the previous one, throw away the + // state marker graphic and force a new one (#1459) + if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) { + stateMarkerGraphic = stateMarkerGraphic.destroy(); + } + + // Add a new state marker graphic + if (!stateMarkerGraphic) { + series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol( + newSymbol, + plotX - radius, + plotY - radius, + 2 * radius, + 2 * radius + ) + .attr(pointAttr[state]) + .add(series.markerGroup); + stateMarkerGraphic.currentSymbol = newSymbol; + + // Move the existing graphic + } else { + stateMarkerGraphic.attr({ // #1054 + x: plotX - radius, + y: plotY - radius + }); + } + } + + if (stateMarkerGraphic) { + stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY) ? 'show' : 'hide'](); + } + } + + point.state = state; + } +}; + +/** + * @classDescription The base function which all other series types inherit from. The data in the series is stored + * in various arrays. + * + * - First, series.options.data contains all the original config options for + * each point whether added by options or methods like series.addPoint. + * - Next, series.data contains those values converted to points, but in case the series data length + * exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It + * only contains the points that have been created on demand. + * - Then there's series.points that contains all currently visible point objects. In case of cropping, + * the cropped-away points are not part of this array. The series.points array starts at series.cropStart + * compared to series.data and series.options.data. If however the series data is grouped, these can't + * be correlated one to one. + * - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points. + * - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points. + * + * @param {Object} chart + * @param {Object} options + */ +var Series = function () {}; + +Series.prototype = { + + isCartesian: true, + type: 'line', + pointClass: Point, + sorted: true, // requires the data to be sorted + requireSorting: true, + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'lineColor', + 'stroke-width': 'lineWidth', + fill: 'fillColor', + r: 'radius' + }, + colorCounter: 0, + init: function (chart, options) { + var series = this, + eventType, + events, + chartSeries = chart.series; + + series.chart = chart; + series.options = options = series.setOptions(options); // merge with plotOptions + series.linkedSeries = []; + + // bind the axes + series.bindAxes(); + + // set some variables + extend(series, { + name: options.name, + state: NORMAL_STATE, + pointAttr: {}, + visible: options.visible !== false, // true by default + selected: options.selected === true // false by default + }); + + // special + if (useCanVG) { + options.animation = false; + } + + // register event listeners + events = options.events; + for (eventType in events) { + addEvent(series, eventType, events[eventType]); + } + if ( + (events && events.click) || + (options.point && options.point.events && options.point.events.click) || + options.allowPointSelect + ) { + chart.runTrackerClick = true; + } + + series.getColor(); + series.getSymbol(); + + // set the data + series.setData(options.data, false); + + // Mark cartesian + if (series.isCartesian) { + chart.hasCartesianSeries = true; + } + + // Register it in the chart + chartSeries.push(series); + series._i = chartSeries.length - 1; + + // Sort series according to index option (#248, #1123) + stableSort(chartSeries, function (a, b) { + return pick(a.options.index, a._i) - pick(b.options.index, a._i); + }); + each(chartSeries, function (series, i) { + series.index = i; + series.name = series.name || 'Series ' + (i + 1); + }); + + }, + + /** + * Set the xAxis and yAxis properties of cartesian series, and register the series + * in the axis.series array + */ + bindAxes: function () { + var series = this, + seriesOptions = series.options, + chart = series.chart, + axisOptions; + + if (series.isCartesian) { + + each(['xAxis', 'yAxis'], function (AXIS) { // repeat for xAxis and yAxis + + each(chart[AXIS], function (axis) { // loop through the chart's axis objects + + axisOptions = axis.options; + + // apply if the series xAxis or yAxis option mathches the number of the + // axis, or if undefined, use the first axis + if ((seriesOptions[AXIS] === axisOptions.index) || + (seriesOptions[AXIS] !== UNDEFINED && seriesOptions[AXIS] === axisOptions.id) || + (seriesOptions[AXIS] === UNDEFINED && axisOptions.index === 0)) { + + // register this series in the axis.series lookup + axis.series.push(series); + + // set this series.xAxis or series.yAxis reference + series[AXIS] = axis; + + // mark dirty for redraw + axis.isDirty = true; + } + }); + + // The series needs an X and an Y axis + if (!series[AXIS]) { + error(18, true); + } + + }); + } + }, + + + /** + * Return an auto incremented x value based on the pointStart and pointInterval options. + * This is only used if an x value is not given for the point that calls autoIncrement. + */ + autoIncrement: function () { + var series = this, + options = series.options, + xIncrement = series.xIncrement; + + xIncrement = pick(xIncrement, options.pointStart, 0); + + series.pointInterval = pick(series.pointInterval, options.pointInterval, 1); + + series.xIncrement = xIncrement + series.pointInterval; + return xIncrement; + }, + + /** + * Divide the series data into segments divided by null values. + */ + getSegments: function () { + var series = this, + lastNull = -1, + segments = [], + i, + points = series.points, + pointsLength = points.length; + + if (pointsLength) { // no action required for [] + + // if connect nulls, just remove null points + if (series.options.connectNulls) { + i = pointsLength; + while (i--) { + if (points[i].y === null) { + points.splice(i, 1); + } + } + if (points.length) { + segments = [points]; + } + + // else, split on null points + } else { + each(points, function (point, i) { + if (point.y === null) { + if (i > lastNull + 1) { + segments.push(points.slice(lastNull + 1, i)); + } + lastNull = i; + } else if (i === pointsLength - 1) { // last value + segments.push(points.slice(lastNull + 1, i + 1)); + } + }); + } + } + + // register it + series.segments = segments; + }, + + /** + * Set the series options by merging from the options tree + * @param {Object} itemOptions + */ + setOptions: function (itemOptions) { + var chart = this.chart, + chartOptions = chart.options, + plotOptions = chartOptions.plotOptions, + typeOptions = plotOptions[this.type], + options; + + this.userOptions = itemOptions; + + options = merge( + typeOptions, + plotOptions.series, + itemOptions + ); + + // the tooltip options are merged between global and series specific options + this.tooltipOptions = merge(chartOptions.tooltip, options.tooltip); + + // Delte marker object if not allowed (#1125) + if (typeOptions.marker === null) { + delete options.marker; + } + + return options; + + }, + /** + * Get the series' color + */ + getColor: function () { + var options = this.options, + userOptions = this.userOptions, + defaultColors = this.chart.options.colors, + counters = this.chart.counters, + color, + colorIndex; + + color = options.color || defaultPlotOptions[this.type].color; + + if (!color && !options.colorByPoint) { + if (defined(userOptions._colorIndex)) { // after Series.update() + colorIndex = userOptions._colorIndex; + } else { + userOptions._colorIndex = counters.color; + colorIndex = counters.color++; + } + color = defaultColors[colorIndex]; + } + + this.color = color; + counters.wrapColor(defaultColors.length); + }, + /** + * Get the series' symbol + */ + getSymbol: function () { + var series = this, + userOptions = series.userOptions, + seriesMarkerOption = series.options.marker, + chart = series.chart, + defaultSymbols = chart.options.symbols, + counters = chart.counters, + symbolIndex; + + series.symbol = seriesMarkerOption.symbol; + if (!series.symbol) { + if (defined(userOptions._symbolIndex)) { // after Series.update() + symbolIndex = userOptions._symbolIndex; + } else { + userOptions._symbolIndex = counters.symbol; + symbolIndex = counters.symbol++; + } + series.symbol = defaultSymbols[symbolIndex]; + } + + // don't substract radius in image symbols (#604) + if (/^url/.test(series.symbol)) { + seriesMarkerOption.radius = 0; + } + counters.wrapSymbol(defaultSymbols.length); + }, + + /** + * Get the series' symbol in the legend. This method should be overridable to create custom + * symbols through Highcharts.seriesTypes[type].prototype.drawLegendSymbols. + * + * @param {Object} legend The legend object + */ + drawLegendSymbol: function (legend) { + + var options = this.options, + markerOptions = options.marker, + radius, + legendOptions = legend.options, + legendSymbol, + symbolWidth = legendOptions.symbolWidth, + renderer = this.chart.renderer, + legendItemGroup = this.legendGroup, + verticalCenter = legend.baseline - mathRound(renderer.fontMetrics(legendOptions.itemStyle.fontSize).b * 0.3), + attr; + + // Draw the line + if (options.lineWidth) { + attr = { + 'stroke-width': options.lineWidth + }; + if (options.dashStyle) { + attr.dashstyle = options.dashStyle; + } + this.legendLine = renderer.path([ + M, + 0, + verticalCenter, + L, + symbolWidth, + verticalCenter + ]) + .attr(attr) + .add(legendItemGroup); + } + + // Draw the marker + if (markerOptions && markerOptions.enabled) { + radius = markerOptions.radius; + this.legendSymbol = legendSymbol = renderer.symbol( + this.symbol, + (symbolWidth / 2) - radius, + verticalCenter - radius, + 2 * radius, + 2 * radius + ) + .add(legendItemGroup); + legendSymbol.isMarker = true; + } + }, + + /** + * Add a point dynamically after chart load time + * @param {Object} options Point options as given in series.data + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean} shift If shift is true, a point is shifted off the start + * of the series as one is appended to the end. + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + addPoint: function (options, redraw, shift, animation) { + var series = this, + seriesOptions = series.options, + data = series.data, + graph = series.graph, + area = series.area, + chart = series.chart, + xData = series.xData, + yData = series.yData, + zData = series.zData, + names = series.names, + currentShift = (graph && graph.shift) || 0, + dataOptions = seriesOptions.data, + point, + isInTheMiddle, + x, + i; + + setAnimation(animation, chart); + + // Make graph animate sideways + if (shift) { + each([graph, area, series.graphNeg, series.areaNeg], function (shape) { + if (shape) { + shape.shift = currentShift + 1; + } + }); + } + if (area) { + area.isArea = true; // needed in animation, both with and without shift + } + + // Optional redraw, defaults to true + redraw = pick(redraw, true); + + // Get options and push the point to xData, yData and series.options. In series.generatePoints + // the Point instance will be created on demand and pushed to the series.data array. + point = { series: series }; + series.pointClass.prototype.applyOptions.apply(point, [options]); + x = point.x; + + // Get the insertion point + i = xData.length; + if (series.requireSorting && x < xData[i - 1]) { + isInTheMiddle = true; + while (i && xData[i - 1] > x) { + i--; + } + } + + xData.splice(i, 0, x); + yData.splice(i, 0, series.toYData ? series.toYData(point) : point.y); + zData.splice(i, 0, point.z); + if (names) { + names[x] = point.name; + } + dataOptions.splice(i, 0, options); + + if (isInTheMiddle) { + series.data.splice(i, 0, null); + series.processData(); + } + + // Generate points to be added to the legend (#1329) + if (seriesOptions.legendType === 'point') { + series.generatePoints(); + } + + // Shift the first point off the parallel arrays + // todo: consider series.removePoint(i) method + if (shift) { + if (data[0] && data[0].remove) { + data[0].remove(false); + } else { + data.shift(); + xData.shift(); + yData.shift(); + zData.shift(); + dataOptions.shift(); + } + } + + // redraw + series.isDirty = true; + series.isDirtyData = true; + if (redraw) { + series.getAttribs(); // #1937 + chart.redraw(); + } + }, + + /** + * Replace the series data with a new set of data + * @param {Object} data + * @param {Object} redraw + */ + setData: function (data, redraw) { + var series = this, + oldData = series.points, + options = series.options, + chart = series.chart, + firstPoint = null, + xAxis = series.xAxis, + names = xAxis && xAxis.categories && !xAxis.categories.length ? [] : null, + i; + + // reset properties + series.xIncrement = null; + series.pointRange = xAxis && xAxis.categories ? 1 : options.pointRange; + + series.colorCounter = 0; // for series with colorByPoint (#1547) + + // parallel arrays + var xData = [], + yData = [], + zData = [], + dataLength = data ? data.length : [], + turboThreshold = pick(options.turboThreshold, 1000), + pt, + pointArrayMap = series.pointArrayMap, + valueCount = pointArrayMap && pointArrayMap.length, + hasToYData = !!series.toYData; + + // In turbo mode, only one- or twodimensional arrays of numbers are allowed. The + // first value is tested, and we assume that all the rest are defined the same + // way. Although the 'for' loops are similar, they are repeated inside each + // if-else conditional for max performance. + if (turboThreshold && dataLength > turboThreshold) { + + // find the first non-null point + i = 0; + while (firstPoint === null && i < dataLength) { + firstPoint = data[i]; + i++; + } + + + if (isNumber(firstPoint)) { // assume all points are numbers + var x = pick(options.pointStart, 0), + pointInterval = pick(options.pointInterval, 1); + + for (i = 0; i < dataLength; i++) { + xData[i] = x; + yData[i] = data[i]; + x += pointInterval; + } + series.xIncrement = x; + } else if (isArray(firstPoint)) { // assume all points are arrays + if (valueCount) { // [x, low, high] or [x, o, h, l, c] + for (i = 0; i < dataLength; i++) { + pt = data[i]; + xData[i] = pt[0]; + yData[i] = pt.slice(1, valueCount + 1); + } + } else { // [x, y] + for (i = 0; i < dataLength; i++) { + pt = data[i]; + xData[i] = pt[0]; + yData[i] = pt[1]; + } + } + } else { + error(12); // Highcharts expects configs to be numbers or arrays in turbo mode + } + } else { + for (i = 0; i < dataLength; i++) { + if (data[i] !== UNDEFINED) { // stray commas in oldIE + pt = { series: series }; + series.pointClass.prototype.applyOptions.apply(pt, [data[i]]); + xData[i] = pt.x; + yData[i] = hasToYData ? series.toYData(pt) : pt.y; + zData[i] = pt.z; + if (names && pt.name) { + names[pt.x] = pt.name; // #2046 + } + } + } + } + + // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON + if (isString(yData[0])) { + error(14, true); + } + + series.data = []; + series.options.data = data; + series.xData = xData; + series.yData = yData; + series.zData = zData; + series.names = names; + + // destroy old points + i = (oldData && oldData.length) || 0; + while (i--) { + if (oldData[i] && oldData[i].destroy) { + oldData[i].destroy(); + } + } + + // reset minRange (#878) + if (xAxis) { + xAxis.minRange = xAxis.userMinRange; + } + + // redraw + series.isDirty = series.isDirtyData = chart.isDirtyBox = true; + if (pick(redraw, true)) { + chart.redraw(false); + } + }, + + /** + * Remove a series and optionally redraw the chart + * + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + + remove: function (redraw, animation) { + var series = this, + chart = series.chart; + redraw = pick(redraw, true); + + if (!series.isRemoving) { /* prevent triggering native event in jQuery + (calling the remove function from the remove event) */ + series.isRemoving = true; + + // fire the event with a default handler of removing the point + fireEvent(series, 'remove', null, function () { + + + // destroy elements + series.destroy(); + + + // redraw + chart.isDirtyLegend = chart.isDirtyBox = true; + chart.linkSeries(); + + if (redraw) { + chart.redraw(animation); + } + }); + + } + series.isRemoving = false; + }, + + /** + * Process the data by cropping away unused data points if the series is longer + * than the crop threshold. This saves computing time for lage series. + */ + processData: function (force) { + var series = this, + processedXData = series.xData, // copied during slice operation below + processedYData = series.yData, + dataLength = processedXData.length, + croppedData, + cropStart = 0, + cropped, + distance, + closestPointRange, + xAxis = series.xAxis, + i, // loop variable + options = series.options, + cropThreshold = options.cropThreshold, + isCartesian = series.isCartesian; + + // If the series data or axes haven't changed, don't go through this. Return false to pass + // the message on to override methods like in data grouping. + if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) { + return false; + } + + + // optionally filter out points outside the plot area + if (isCartesian && series.sorted && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) { + var min = xAxis.min, + max = xAxis.max; + + // it's outside current extremes + if (processedXData[dataLength - 1] < min || processedXData[0] > max) { + processedXData = []; + processedYData = []; + + // only crop if it's actually spilling out + } else if (processedXData[0] < min || processedXData[dataLength - 1] > max) { + croppedData = this.cropData(series.xData, series.yData, min, max); + processedXData = croppedData.xData; + processedYData = croppedData.yData; + cropStart = croppedData.start; + cropped = true; + } + } + + + // Find the closest distance between processed points + for (i = processedXData.length - 1; i >= 0; i--) { + distance = processedXData[i] - processedXData[i - 1]; + if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) { + closestPointRange = distance; + + // Unsorted data is not supported by the line tooltip, as well as data grouping and + // navigation in Stock charts (#725) and width calculation of columns (#1900) + } else if (distance < 0 && series.requireSorting) { + error(15); + } + } + + // Record the properties + series.cropped = cropped; // undefined or true + series.cropStart = cropStart; + series.processedXData = processedXData; + series.processedYData = processedYData; + + if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC + series.pointRange = closestPointRange || 1; + } + series.closestPointRange = closestPointRange; + + }, + + /** + * Iterate over xData and crop values between min and max. Returns object containing crop start/end + * cropped xData with corresponding part of yData, dataMin and dataMax within the cropped range + */ + cropData: function (xData, yData, min, max) { + var dataLength = xData.length, + cropStart = 0, + cropEnd = dataLength, + cropShoulder = pick(this.cropShoulder, 1), // line-type series need one point outside + i; + + // iterate up to find slice start + for (i = 0; i < dataLength; i++) { + if (xData[i] >= min) { + cropStart = mathMax(0, i - cropShoulder); + break; + } + } + + // proceed to find slice end + for (; i < dataLength; i++) { + if (xData[i] > max) { + cropEnd = i + cropShoulder; + break; + } + } + + return { + xData: xData.slice(cropStart, cropEnd), + yData: yData.slice(cropStart, cropEnd), + start: cropStart, + end: cropEnd + }; + }, + + + /** + * Generate the data point after the data has been processed by cropping away + * unused points and optionally grouped in Highcharts Stock. + */ + generatePoints: function () { + var series = this, + options = series.options, + dataOptions = options.data, + data = series.data, + dataLength, + processedXData = series.processedXData, + processedYData = series.processedYData, + pointClass = series.pointClass, + processedDataLength = processedXData.length, + cropStart = series.cropStart || 0, + cursor, + hasGroupedData = series.hasGroupedData, + point, + points = [], + i; + + if (!data && !hasGroupedData) { + var arr = []; + arr.length = dataOptions.length; + data = series.data = arr; + } + + for (i = 0; i < processedDataLength; i++) { + cursor = cropStart + i; + if (!hasGroupedData) { + if (data[cursor]) { + point = data[cursor]; + } else if (dataOptions[cursor] !== UNDEFINED) { // #970 + data[cursor] = point = (new pointClass()).init(series, dataOptions[cursor], processedXData[i]); + } + points[i] = point; + } else { + // splat the y data in case of ohlc data array + points[i] = (new pointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i]))); + } + } + + // Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when + // swithching view from non-grouped data to grouped data (#637) + if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) { + for (i = 0; i < dataLength; i++) { + if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points + i += processedDataLength; + } + if (data[i]) { + data[i].destroyElements(); + data[i].plotX = UNDEFINED; // #1003 + } + } + } + + series.data = data; + series.points = points; + }, + + /** + * Adds series' points value to corresponding stack + */ + setStackedPoints: function () { + if (!this.options.stacking || (this.visible !== true && this.chart.options.chart.ignoreHiddenSeries !== false)) { + return; + } + + var series = this, + xData = series.processedXData, + yData = series.processedYData, + stackedYData = [], + yDataLength = yData.length, + seriesOptions = series.options, + threshold = seriesOptions.threshold, + stackOption = seriesOptions.stack, + stacking = seriesOptions.stacking, + stackKey = series.stackKey, + negKey = '-' + stackKey, + negStacks = series.negStacks, + yAxis = series.yAxis, + stacks = yAxis.stacks, + oldStacks = yAxis.oldStacks, + isNegative, + stack, + other, + key, + i, + x, + y; + + // loop over the non-null y values and read them into a local array + for (i = 0; i < yDataLength; i++) { + x = xData[i]; + y = yData[i]; + + // Read stacked values into a stack based on the x value, + // the sign of y and the stack key. Stacking is also handled for null values (#739) + isNegative = negStacks && y < threshold; + key = isNegative ? negKey : stackKey; + + // Create empty object for this stack if it doesn't exist yet + if (!stacks[key]) { + stacks[key] = {}; + } + + // Initialize StackItem for this x + if (!stacks[key][x]) { + if (oldStacks[key] && oldStacks[key][x]) { + stacks[key][x] = oldStacks[key][x]; + stacks[key][x].total = null; + } else { + stacks[key][x] = new StackItem(yAxis, yAxis.options.stackLabels, isNegative, x, stackOption, stacking); + } + } + + // If the StackItem doesn't exist, create it first + stack = stacks[key][x]; + stack.points[series.index] = [stack.cum || 0]; + + // Add value to the stack total + if (stacking === 'percent') { + + // Percent stacked column, totals are the same for the positive and negative stacks + other = isNegative ? stackKey : negKey; + if (negStacks && stacks[other] && stacks[other][x]) { + other = stacks[other][x]; + stack.total = other.total = mathMax(other.total, stack.total) + mathAbs(y) || 0; + + // Percent stacked areas + } else { + stack.total += mathAbs(y) || 0; + } + } else { + stack.total += y || 0; + } + + stack.cum = (stack.cum || 0) + (y || 0); + + stack.points[series.index].push(stack.cum); + stackedYData[i] = stack.cum; + + } + + if (stacking === 'percent') { + yAxis.usePercentage = true; + } + + this.stackedYData = stackedYData; // To be used in getExtremes + + // Reset old stacks + yAxis.oldStacks = {}; + }, + + /** + * Iterate over all stacks and compute the absolute values to percent + */ + setPercentStacks: function () { + var series = this, + stackKey = series.stackKey, + stacks = series.yAxis.stacks; + + each([stackKey, '-' + stackKey], function (key) { + var i = series.xData.length, + x, + stack, + pointExtremes, + totalFactor; + + while (i--) { + x = series.xData[i]; + stack = stacks[key] && stacks[key][x]; + pointExtremes = stack && stack.points[series.index]; + if (pointExtremes) { + totalFactor = stack.total ? 100 / stack.total : 0; + pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor); // Y bottom value + pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor); // Y value + series.stackedYData[i] = pointExtremes[1]; + } + } + }); + }, + + /** + * Calculate Y extremes for visible data + */ + getExtremes: function () { + var xAxis = this.xAxis, + yAxis = this.yAxis, + xData = this.processedXData, + yData = this.stackedYData || this.processedYData, + yDataLength = yData.length, + activeYData = [], + activeCounter = 0, + xExtremes = xAxis.getExtremes(), // #2117, need to compensate for log X axis + xMin = xExtremes.min, + xMax = xExtremes.max, + validValue, + withinRange, + dataMin, + dataMax, + x, + y, + i, + j; + + for (i = 0; i < yDataLength; i++) { + + x = xData[i]; + y = yData[i]; + + // For points within the visible range, including the first point outside the + // visible range, consider y extremes + validValue = y !== null && y !== UNDEFINED && (!yAxis.isLog || (y.length || y > 0)); + withinRange = this.getExtremesFromAll || this.cropped || ((xData[i + 1] || x) >= xMin && + (xData[i - 1] || x) <= xMax); + + if (validValue && withinRange) { + + j = y.length; + if (j) { // array, like ohlc or range data + while (j--) { + if (y[j] !== null) { + activeYData[activeCounter++] = y[j]; + } + } + } else { + activeYData[activeCounter++] = y; + } + } + } + this.dataMin = pick(dataMin, arrayMin(activeYData)); + this.dataMax = pick(dataMax, arrayMax(activeYData)); + }, + + /** + * Translate data points from raw data values to chart specific positioning data + * needed later in drawPoints, drawGraph and drawTracker. + */ + translate: function () { + if (!this.processedXData) { // hidden series + this.processData(); + } + this.generatePoints(); + var series = this, + options = series.options, + stacking = options.stacking, + xAxis = series.xAxis, + categories = xAxis.categories, + yAxis = series.yAxis, + points = series.points, + dataLength = points.length, + hasModifyValue = !!series.modifyValue, + i, + pointPlacement = options.pointPlacement, + dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement), + threshold = options.threshold; + + + // Translate each point + for (i = 0; i < dataLength; i++) { + var point = points[i], + xValue = point.x, + yValue = point.y, + yBottom = point.low, + stack = yAxis.stacks[(series.negStacks && yValue < threshold ? '-' : '') + series.stackKey], + pointStack, + stackValues; + + // Discard disallowed y values for log axes + if (yAxis.isLog && yValue <= 0) { + point.y = yValue = null; + } + + // Get the plotX translation + point.plotX = xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags'); // Math.round fixes #591 + + + // Calculate the bottom y value for stacked series + if (stacking && series.visible && stack && stack[xValue]) { + + pointStack = stack[xValue]; + stackValues = pointStack.points[series.index]; + yBottom = stackValues[0]; + yValue = stackValues[1]; + + if (yBottom === 0) { + yBottom = pick(threshold, yAxis.min); + } + if (yAxis.isLog && yBottom <= 0) { // #1200, #1232 + yBottom = null; + } + + point.percentage = stacking === 'percent' && yValue; + point.total = point.stackTotal = pointStack.total; + point.stackY = yValue; + + // Place the stack label + pointStack.setOffset(series.pointXOffset || 0, series.barW || 0); + + } + + // Set translated yBottom or remove it + point.yBottom = defined(yBottom) ? + yAxis.translate(yBottom, 0, 1, 0, 1) : + null; + + // general hook, used for Highstock compare mode + if (hasModifyValue) { + yValue = series.modifyValue(yValue, point); + } + + // Set the the plotY value, reset it for redraws + point.plotY = (typeof yValue === 'number' && yValue !== Infinity) ? + //mathRound(yAxis.translate(yValue, 0, 1, 0, 1) * 10) / 10 : // Math.round fixes #591 + yAxis.translate(yValue, 0, 1, 0, 1) : + UNDEFINED; + + // Set client related positions for mouse tracking + point.clientX = dynamicallyPlaced ? xAxis.translate(xValue, 0, 0, 0, 1) : point.plotX; // #1514 + + point.negative = point.y < (threshold || 0); + + // some API data + point.category = categories && categories[point.x] !== UNDEFINED ? + categories[point.x] : point.x; + + + } + + // now that we have the cropped data, build the segments + series.getSegments(); + }, + /** + * Memoize tooltip texts and positions + */ + setTooltipPoints: function (renew) { + var series = this, + points = [], + pointsLength, + low, + high, + xAxis = series.xAxis, + xExtremes = xAxis && xAxis.getExtremes(), + axisLength = xAxis ? (xAxis.tooltipLen || xAxis.len) : series.chart.plotSizeX, // tooltipLen and tooltipPosName used in polar + point, + pointX, + nextPoint, + i, + tooltipPoints = []; // a lookup array for each pixel in the x dimension + + // don't waste resources if tracker is disabled + if (series.options.enableMouseTracking === false) { + return; + } + + // renew + if (renew) { + series.tooltipPoints = null; + } + + // concat segments to overcome null values + each(series.segments || series.points, function (segment) { + points = points.concat(segment); + }); + + // Reverse the points in case the X axis is reversed + if (xAxis && xAxis.reversed) { + points = points.reverse(); + } + + // Polar needs additional shaping + if (series.orderTooltipPoints) { + series.orderTooltipPoints(points); + } + + // Assign each pixel position to the nearest point + pointsLength = points.length; + for (i = 0; i < pointsLength; i++) { + point = points[i]; + pointX = point.x; + if (pointX >= xExtremes.min && pointX <= xExtremes.max) { // #1149 + nextPoint = points[i + 1]; + + // Set this range's low to the last range's high plus one + low = high === UNDEFINED ? 0 : high + 1; + // Now find the new high + high = points[i + 1] ? + mathMin(mathMax(0, mathFloor( // #2070 + (point.clientX + (nextPoint ? (nextPoint.wrappedClientX || nextPoint.clientX) : axisLength)) / 2 + )), axisLength) : + axisLength; + + while (low >= 0 && low <= high) { + tooltipPoints[low++] = point; + } + } + } + series.tooltipPoints = tooltipPoints; + }, + + /** + * Format the header of the tooltip + */ + tooltipHeaderFormatter: function (point) { + var series = this, + tooltipOptions = series.tooltipOptions, + xDateFormat = tooltipOptions.xDateFormat, + dateTimeLabelFormats = tooltipOptions.dateTimeLabelFormats, + xAxis = series.xAxis, + isDateTime = xAxis && xAxis.options.type === 'datetime', + headerFormat = tooltipOptions.headerFormat, + closestPointRange = xAxis && xAxis.closestPointRange, + n; + + // Guess the best date format based on the closest point distance (#568) + if (isDateTime && !xDateFormat) { + if (closestPointRange) { + for (n in timeUnits) { + if (timeUnits[n] >= closestPointRange) { + xDateFormat = dateTimeLabelFormats[n]; + break; + } + } + } else { + xDateFormat = dateTimeLabelFormats.day; + } + } + + // Insert the header date format if any + if (isDateTime && xDateFormat && isNumber(point.key)) { + headerFormat = headerFormat.replace('{point.key}', '{point.key:' + xDateFormat + '}'); + } + + return format(headerFormat, { + point: point, + series: series + }); + }, + + /** + * Series mouse over handler + */ + onMouseOver: function () { + var series = this, + chart = series.chart, + hoverSeries = chart.hoverSeries; + + // set normal state to previous series + if (hoverSeries && hoverSeries !== series) { + hoverSeries.onMouseOut(); + } + + // trigger the event, but to save processing time, + // only if defined + if (series.options.events.mouseOver) { + fireEvent(series, 'mouseOver'); + } + + // hover this + series.setState(HOVER_STATE); + chart.hoverSeries = series; + }, + + /** + * Series mouse out handler + */ + onMouseOut: function () { + // trigger the event only if listeners exist + var series = this, + options = series.options, + chart = series.chart, + tooltip = chart.tooltip, + hoverPoint = chart.hoverPoint; + + // trigger mouse out on the point, which must be in this series + if (hoverPoint) { + hoverPoint.onMouseOut(); + } + + // fire the mouse out event + if (series && options.events.mouseOut) { + fireEvent(series, 'mouseOut'); + } + + + // hide the tooltip + if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) { + tooltip.hide(); + } + + // set normal state + series.setState(); + chart.hoverSeries = null; + }, + + /** + * Animate in the series + */ + animate: function (init) { + var series = this, + chart = series.chart, + renderer = chart.renderer, + clipRect, + markerClipRect, + animation = series.options.animation, + clipBox = chart.clipBox, + inverted = chart.inverted, + sharedClipKey; + + // Animation option is set to true + if (animation && !isObject(animation)) { + animation = defaultPlotOptions[series.type].animation; + } + sharedClipKey = '_sharedClip' + animation.duration + animation.easing; + + // Initialize the animation. Set up the clipping rectangle. + if (init) { + + // If a clipping rectangle with the same properties is currently present in the chart, use that. + clipRect = chart[sharedClipKey]; + markerClipRect = chart[sharedClipKey + 'm']; + if (!clipRect) { + chart[sharedClipKey] = clipRect = renderer.clipRect( + extend(clipBox, { width: 0 }) + ); + + chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect( + -99, // include the width of the first marker + inverted ? -chart.plotLeft : -chart.plotTop, + 99, + inverted ? chart.chartWidth : chart.chartHeight + ); + } + series.group.clip(clipRect); + series.markerGroup.clip(markerClipRect); + series.sharedClipKey = sharedClipKey; + + // Run the animation + } else { + clipRect = chart[sharedClipKey]; + if (clipRect) { + clipRect.animate({ + width: chart.plotSizeX + }, animation); + chart[sharedClipKey + 'm'].animate({ + width: chart.plotSizeX + 99 + }, animation); + } + + // Delete this function to allow it only once + series.animate = null; + + // Call the afterAnimate function on animation complete (but don't overwrite the animation.complete option + // which should be available to the user). + series.animationTimeout = setTimeout(function () { + series.afterAnimate(); + }, animation.duration); + } + }, + + /** + * This runs after animation to land on the final plot clipping + */ + afterAnimate: function () { + var chart = this.chart, + sharedClipKey = this.sharedClipKey, + group = this.group; + + if (group && this.options.clip !== false) { + group.clip(chart.clipRect); + this.markerGroup.clip(); // no clip + } + + // Remove the shared clipping rectancgle when all series are shown + setTimeout(function () { + if (sharedClipKey && chart[sharedClipKey]) { + chart[sharedClipKey] = chart[sharedClipKey].destroy(); + chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy(); + } + }, 100); + }, + + /** + * Draw the markers + */ + drawPoints: function () { + var series = this, + pointAttr, + points = series.points, + chart = series.chart, + plotX, + plotY, + i, + point, + radius, + symbol, + isImage, + graphic, + options = series.options, + seriesMarkerOptions = options.marker, + pointMarkerOptions, + enabled, + isInside, + markerGroup = series.markerGroup; + + if (seriesMarkerOptions.enabled || series._hasPointMarkers) { + + i = points.length; + while (i--) { + point = points[i]; + plotX = mathFloor(point.plotX); // #1843 + plotY = point.plotY; + graphic = point.graphic; + pointMarkerOptions = point.marker || {}; + enabled = (seriesMarkerOptions.enabled && pointMarkerOptions.enabled === UNDEFINED) || pointMarkerOptions.enabled; + isInside = chart.isInsidePlot(mathRound(plotX), plotY, chart.inverted); // #1858 + + // only draw the point if y is defined + if (enabled && plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) { + + // shortcuts + pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]; + radius = pointAttr.r; + symbol = pick(pointMarkerOptions.symbol, series.symbol); + isImage = symbol.indexOf('url') === 0; + + if (graphic) { // update + graphic + .attr({ // Since the marker group isn't clipped, each individual marker must be toggled + visibility: isInside ? (hasSVG ? 'inherit' : VISIBLE) : HIDDEN + }) + .animate(extend({ + x: plotX - radius, + y: plotY - radius + }, graphic.symbolName ? { // don't apply to image symbols #507 + width: 2 * radius, + height: 2 * radius + } : {})); + } else if (isInside && (radius > 0 || isImage)) { + point.graphic = graphic = chart.renderer.symbol( + symbol, + plotX - radius, + plotY - radius, + 2 * radius, + 2 * radius + ) + .attr(pointAttr) + .add(markerGroup); + } + + } else if (graphic) { + point.graphic = graphic.destroy(); // #1269 + } + } + } + + }, + + /** + * Convert state properties from API naming conventions to SVG attributes + * + * @param {Object} options API options object + * @param {Object} base1 SVG attribute object to inherit from + * @param {Object} base2 Second level SVG attribute object to inherit from + */ + convertAttribs: function (options, base1, base2, base3) { + var conversion = this.pointAttrToOptions, + attr, + option, + obj = {}; + + options = options || {}; + base1 = base1 || {}; + base2 = base2 || {}; + base3 = base3 || {}; + + for (attr in conversion) { + option = conversion[attr]; + obj[attr] = pick(options[option], base1[attr], base2[attr], base3[attr]); + } + return obj; + }, + + /** + * Get the state attributes. Each series type has its own set of attributes + * that are allowed to change on a point's state change. Series wide attributes are stored for + * all series, and additionally point specific attributes are stored for all + * points with individual marker options. If such options are not defined for the point, + * a reference to the series wide attributes is stored in point.pointAttr. + */ + getAttribs: function () { + var series = this, + seriesOptions = series.options, + normalOptions = defaultPlotOptions[series.type].marker ? seriesOptions.marker : seriesOptions, + stateOptions = normalOptions.states, + stateOptionsHover = stateOptions[HOVER_STATE], + pointStateOptionsHover, + seriesColor = series.color, + normalDefaults = { + stroke: seriesColor, + fill: seriesColor + }, + points = series.points || [], // #927 + i, + point, + seriesPointAttr = [], + pointAttr, + pointAttrToOptions = series.pointAttrToOptions, + hasPointSpecificOptions, + negativeColor = seriesOptions.negativeColor, + defaultLineColor = normalOptions.lineColor, + key; + + // series type specific modifications + if (seriesOptions.marker) { // line, spline, area, areaspline, scatter + + // if no hover radius is given, default to normal radius + 2 + stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + 2; + stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + 1; + + } else { // column, bar, pie + + // if no hover color is given, brighten the normal color + stateOptionsHover.color = stateOptionsHover.color || + Color(stateOptionsHover.color || seriesColor) + .brighten(stateOptionsHover.brightness).get(); + } + + // general point attributes for the series normal state + seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults); + + // HOVER_STATE and SELECT_STATE states inherit from normal state except the default radius + each([HOVER_STATE, SELECT_STATE], function (state) { + seriesPointAttr[state] = + series.convertAttribs(stateOptions[state], seriesPointAttr[NORMAL_STATE]); + }); + + // set it + series.pointAttr = seriesPointAttr; + + + // Generate the point-specific attribute collections if specific point + // options are given. If not, create a referance to the series wide point + // attributes + i = points.length; + while (i--) { + point = points[i]; + normalOptions = (point.options && point.options.marker) || point.options; + if (normalOptions && normalOptions.enabled === false) { + normalOptions.radius = 0; + } + + if (point.negative && negativeColor) { + point.color = point.fillColor = negativeColor; + } + + hasPointSpecificOptions = seriesOptions.colorByPoint || point.color; // #868 + + // check if the point has specific visual options + if (point.options) { + for (key in pointAttrToOptions) { + if (defined(normalOptions[pointAttrToOptions[key]])) { + hasPointSpecificOptions = true; + } + } + } + + // a specific marker config object is defined for the individual point: + // create it's own attribute collection + if (hasPointSpecificOptions) { + normalOptions = normalOptions || {}; + pointAttr = []; + stateOptions = normalOptions.states || {}; // reassign for individual point + pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {}; + + // Handle colors for column and pies + if (!seriesOptions.marker) { // column, bar, point + // if no hover color is given, brighten the normal color + pointStateOptionsHover.color = + Color(pointStateOptionsHover.color || point.color) + .brighten(pointStateOptionsHover.brightness || + stateOptionsHover.brightness).get(); + + } + + // normal point state inherits series wide normal state + pointAttr[NORMAL_STATE] = series.convertAttribs(extend({ + color: point.color, // #868 + fillColor: point.color, // Individual point color or negative color markers (#2219) + lineColor: defaultLineColor === null ? point.color : UNDEFINED // Bubbles take point color, line markers use white + }, normalOptions), seriesPointAttr[NORMAL_STATE]); + + // inherit from point normal and series hover + pointAttr[HOVER_STATE] = series.convertAttribs( + stateOptions[HOVER_STATE], + seriesPointAttr[HOVER_STATE], + pointAttr[NORMAL_STATE] + ); + + // inherit from point normal and series hover + pointAttr[SELECT_STATE] = series.convertAttribs( + stateOptions[SELECT_STATE], + seriesPointAttr[SELECT_STATE], + pointAttr[NORMAL_STATE] + ); + + + // no marker config object is created: copy a reference to the series-wide + // attribute collection + } else { + pointAttr = seriesPointAttr; + } + + point.pointAttr = pointAttr; + + } + + }, + /** + * Update the series with a new set of options + */ + update: function (newOptions, redraw) { + var chart = this.chart, + // must use user options when changing type because this.options is merged + // in with type specific plotOptions + oldOptions = this.userOptions, + oldType = this.type, + proto = seriesTypes[oldType].prototype, + n; + + // Do the merge, with some forced options + newOptions = merge(oldOptions, { + animation: false, + index: this.index, + pointStart: this.xData[0] // when updating after addPoint + }, { data: this.options.data }, newOptions); + + // Destroy the series and reinsert methods from the type prototype + this.remove(false); + for (n in proto) { // Overwrite series-type specific methods (#2270) + if (proto.hasOwnProperty(n)) { + this[n] = UNDEFINED; + } + } + extend(this, seriesTypes[newOptions.type || oldType].prototype); + + + this.init(chart, newOptions); + if (pick(redraw, true)) { + chart.redraw(false); + } + }, + + /** + * Clear DOM objects and free up memory + */ + destroy: function () { + var series = this, + chart = series.chart, + issue134 = /AppleWebKit\/533/.test(userAgent), + destroy, + i, + data = series.data || [], + point, + prop, + axis; + + // add event hook + fireEvent(series, 'destroy'); + + // remove all events + removeEvent(series); + + // erase from axes + each(['xAxis', 'yAxis'], function (AXIS) { + axis = series[AXIS]; + if (axis) { + erase(axis.series, series); + axis.isDirty = axis.forceRedraw = true; + axis.stacks = {}; // Rebuild stacks when updating (#2229) + } + }); + + // remove legend items + if (series.legendItem) { + series.chart.legend.destroyItem(series); + } + + // destroy all points with their elements + i = data.length; + while (i--) { + point = data[i]; + if (point && point.destroy) { + point.destroy(); + } + } + series.points = null; + + // Clear the animation timeout if we are destroying the series during initial animation + clearTimeout(series.animationTimeout); + + // destroy all SVGElements associated to the series + each(['area', 'graph', 'dataLabelsGroup', 'group', 'markerGroup', 'tracker', + 'graphNeg', 'areaNeg', 'posClip', 'negClip'], function (prop) { + if (series[prop]) { + + // issue 134 workaround + destroy = issue134 && prop === 'group' ? + 'hide' : + 'destroy'; + + series[prop][destroy](); + } + }); + + // remove from hoverSeries + if (chart.hoverSeries === series) { + chart.hoverSeries = null; + } + erase(chart.series, series); + + // clear all members + for (prop in series) { + delete series[prop]; + } + }, + + /** + * Draw the data labels + */ + drawDataLabels: function () { + + var series = this, + seriesOptions = series.options, + options = seriesOptions.dataLabels, + points = series.points, + pointOptions, + generalOptions, + str, + dataLabelsGroup; + + if (options.enabled || series._hasPointLabels) { + + // Process default alignment of data labels for columns + if (series.dlProcessOptions) { + series.dlProcessOptions(options); + } + + // Create a separate group for the data labels to avoid rotation + dataLabelsGroup = series.plotGroup( + 'dataLabelsGroup', + 'data-labels', + series.visible ? VISIBLE : HIDDEN, + options.zIndex || 6 + ); + + // Make the labels for each point + generalOptions = options; + each(points, function (point) { + + var enabled, + dataLabel = point.dataLabel, + labelConfig, + attr, + name, + rotation, + connector = point.connector, + isNew = true; + + // Determine if each data label is enabled + pointOptions = point.options && point.options.dataLabels; + enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled); // #2282 + + + // If the point is outside the plot area, destroy it. #678, #820 + if (dataLabel && !enabled) { + point.dataLabel = dataLabel.destroy(); + + // Individual labels are disabled if the are explicitly disabled + // in the point options, or if they fall outside the plot area. + } else if (enabled) { + + // Create individual options structure that can be extended without + // affecting others + options = merge(generalOptions, pointOptions); + + rotation = options.rotation; + + // Get the string + labelConfig = point.getLabelConfig(); + str = options.format ? + format(options.format, labelConfig) : + options.formatter.call(labelConfig, options); + + // Determine the color + options.style.color = pick(options.color, options.style.color, series.color, 'black'); + + + // update existing label + if (dataLabel) { + + if (defined(str)) { + dataLabel + .attr({ + text: str + }); + isNew = false; + + } else { // #1437 - the label is shown conditionally + point.dataLabel = dataLabel = dataLabel.destroy(); + if (connector) { + point.connector = connector.destroy(); + } + } + + // create new label + } else if (defined(str)) { + attr = { + //align: align, + fill: options.backgroundColor, + stroke: options.borderColor, + 'stroke-width': options.borderWidth, + r: options.borderRadius || 0, + rotation: rotation, + padding: options.padding, + zIndex: 1 + }; + // Remove unused attributes (#947) + for (name in attr) { + if (attr[name] === UNDEFINED) { + delete attr[name]; + } + } + + dataLabel = point.dataLabel = series.chart.renderer[rotation ? 'text' : 'label']( // labels don't support rotation + str, + 0, + -999, + null, + null, + null, + options.useHTML + ) + .attr(attr) + .css(options.style) + .add(dataLabelsGroup) + .shadow(options.shadow); + + } + + if (dataLabel) { + // Now the data label is created and placed at 0,0, so we need to align it + series.alignDataLabel(point, dataLabel, options, null, isNew); + } + } + }); + } + }, + + /** + * Align each individual data label + */ + alignDataLabel: function (point, dataLabel, options, alignTo, isNew) { + var chart = this.chart, + inverted = chart.inverted, + plotX = pick(point.plotX, -999), + plotY = pick(point.plotY, -999), + bBox = dataLabel.getBBox(), + visible = this.visible && chart.isInsidePlot(point.plotX, point.plotY, inverted), + alignAttr; // the final position; + + if (visible) { + + // The alignment box is a singular point + alignTo = extend({ + x: inverted ? chart.plotWidth - plotY : plotX, + y: mathRound(inverted ? chart.plotHeight - plotX : plotY), + width: 0, + height: 0 + }, alignTo); + + // Add the text size for alignment calculation + extend(options, { + width: bBox.width, + height: bBox.height + }); + + // Allow a hook for changing alignment in the last moment, then do the alignment + if (options.rotation) { // Fancy box alignment isn't supported for rotated text + alignAttr = { + align: options.align, + x: alignTo.x + options.x + alignTo.width / 2, + y: alignTo.y + options.y + alignTo.height / 2 + }; + dataLabel[isNew ? 'attr' : 'animate'](alignAttr); + } else { + dataLabel.align(options, null, alignTo); + alignAttr = dataLabel.alignAttr; + + // Handle justify or crop + if (pick(options.overflow, 'justify') === 'justify') { // docs: overflow: justify, also crop only applies when not justify + this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew); + + } else if (pick(options.crop, true)) { + // Now check that the data label is within the plot area + visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height); + + } + } + } + + // Show or hide based on the final aligned position + if (!visible) { + dataLabel.attr({ y: -999 }); + } + + }, + + /** + * If data labels fall partly outside the plot area, align them back in, in a way that + * doesn't hide the point. + */ + justifyDataLabel: function (dataLabel, options, alignAttr, bBox, alignTo, isNew) { + var chart = this.chart, + align = options.align, + verticalAlign = options.verticalAlign, + off, + justified; + + // Off left + off = alignAttr.x; + if (off < 0) { + if (align === 'right') { + options.align = 'left'; + } else { + options.x = -off; + } + justified = true; + } + + // Off right + off = alignAttr.x + bBox.width; + if (off > chart.plotWidth) { + if (align === 'left') { + options.align = 'right'; + } else { + options.x = chart.plotWidth - off; + } + justified = true; + } + + // Off top + off = alignAttr.y; + if (off < 0) { + if (verticalAlign === 'bottom') { + options.verticalAlign = 'top'; + } else { + options.y = -off; + } + justified = true; + } + + // Off bottom + off = alignAttr.y + bBox.height; + if (off > chart.plotHeight) { + if (verticalAlign === 'top') { + options.verticalAlign = 'bottom'; + } else { + options.y = chart.plotHeight - off; + } + justified = true; + } + + if (justified) { + dataLabel.placed = !isNew; + dataLabel.align(options, null, alignTo); + } + }, + + /** + * Return the graph path of a segment + */ + getSegmentPath: function (segment) { + var series = this, + segmentPath = [], + step = series.options.step; + + // build the segment line + each(segment, function (point, i) { + + var plotX = point.plotX, + plotY = point.plotY, + lastPoint; + + if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object + segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i)); + + } else { + + // moveTo or lineTo + segmentPath.push(i ? L : M); + + // step line? + if (step && i) { + lastPoint = segment[i - 1]; + if (step === 'right') { + segmentPath.push( + lastPoint.plotX, + plotY + ); + + } else if (step === 'center') { + segmentPath.push( + (lastPoint.plotX + plotX) / 2, + lastPoint.plotY, + (lastPoint.plotX + plotX) / 2, + plotY + ); + + } else { + segmentPath.push( + plotX, + lastPoint.plotY + ); + } + } + + // normal line to next point + segmentPath.push( + point.plotX, + point.plotY + ); + } + }); + + return segmentPath; + }, + + /** + * Get the graph path + */ + getGraphPath: function () { + var series = this, + graphPath = [], + segmentPath, + singlePoints = []; // used in drawTracker + + // Divide into segments and build graph and area paths + each(series.segments, function (segment) { + + segmentPath = series.getSegmentPath(segment); + + // add the segment to the graph, or a single point for tracking + if (segment.length > 1) { + graphPath = graphPath.concat(segmentPath); + } else { + singlePoints.push(segment[0]); + } + }); + + // Record it for use in drawGraph and drawTracker, and return graphPath + series.singlePoints = singlePoints; + series.graphPath = graphPath; + + return graphPath; + + }, + + /** + * Draw the actual graph + */ + drawGraph: function () { + var series = this, + options = this.options, + props = [['graph', options.lineColor || this.color]], + lineWidth = options.lineWidth, + dashStyle = options.dashStyle, + graphPath = this.getGraphPath(), + negativeColor = options.negativeColor; + + if (negativeColor) { + props.push(['graphNeg', negativeColor]); + } + + // draw the graph + each(props, function (prop, i) { + var graphKey = prop[0], + graph = series[graphKey], + attribs; + + if (graph) { + stop(graph); // cancel running animations, #459 + graph.animate({ d: graphPath }); + + } else if (lineWidth && graphPath.length) { // #1487 + attribs = { + stroke: prop[1], + 'stroke-width': lineWidth, + zIndex: 1 // #1069 + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } else { + attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round'; + } + + series[graphKey] = series.chart.renderer.path(graphPath) + .attr(attribs) + .add(series.group) + .shadow(!i && options.shadow); + } + }); + }, + + /** + * Clip the graphs into the positive and negative coloured graphs + */ + clipNeg: function () { + var options = this.options, + chart = this.chart, + renderer = chart.renderer, + negativeColor = options.negativeColor || options.negativeFillColor, + translatedThreshold, + posAttr, + negAttr, + graph = this.graph, + area = this.area, + posClip = this.posClip, + negClip = this.negClip, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + chartSizeMax = mathMax(chartWidth, chartHeight), + yAxis = this.yAxis, + above, + below; + + if (negativeColor && (graph || area)) { + translatedThreshold = mathRound(yAxis.toPixels(options.threshold || 0, true)); + above = { + x: 0, + y: 0, + width: chartSizeMax, + height: translatedThreshold + }; + below = { + x: 0, + y: translatedThreshold, + width: chartSizeMax, + height: chartSizeMax + }; + + if (chart.inverted) { + + above.height = below.y = chart.plotWidth - translatedThreshold; + if (renderer.isVML) { + above = { + x: chart.plotWidth - translatedThreshold - chart.plotLeft, + y: 0, + width: chartWidth, + height: chartHeight + }; + below = { + x: translatedThreshold + chart.plotLeft - chartWidth, + y: 0, + width: chart.plotLeft + translatedThreshold, + height: chartWidth + }; + } + } + + if (yAxis.reversed) { + posAttr = below; + negAttr = above; + } else { + posAttr = above; + negAttr = below; + } + + if (posClip) { // update + posClip.animate(posAttr); + negClip.animate(negAttr); + } else { + + this.posClip = posClip = renderer.clipRect(posAttr); + this.negClip = negClip = renderer.clipRect(negAttr); + + if (graph && this.graphNeg) { + graph.clip(posClip); + this.graphNeg.clip(negClip); + } + + if (area) { + area.clip(posClip); + this.areaNeg.clip(negClip); + } + } + } + }, + + /** + * Initialize and perform group inversion on series.group and series.markerGroup + */ + invertGroups: function () { + var series = this, + chart = series.chart; + + // Pie, go away (#1736) + if (!series.xAxis) { + return; + } + + // A fixed size is needed for inversion to work + function setInvert() { + var size = { + width: series.yAxis.len, + height: series.xAxis.len + }; + + each(['group', 'markerGroup'], function (groupName) { + if (series[groupName]) { + series[groupName].attr(size).invert(); + } + }); + } + + addEvent(chart, 'resize', setInvert); // do it on resize + addEvent(series, 'destroy', function () { + removeEvent(chart, 'resize', setInvert); + }); + + // Do it now + setInvert(); // do it now + + // On subsequent render and redraw, just do setInvert without setting up events again + series.invertGroups = setInvert; + }, + + /** + * General abstraction for creating plot groups like series.group, series.dataLabelsGroup and + * series.markerGroup. On subsequent calls, the group will only be adjusted to the updated plot size. + */ + plotGroup: function (prop, name, visibility, zIndex, parent) { + var group = this[prop], + isNew = !group; + + // Generate it on first call + if (isNew) { + this[prop] = group = this.chart.renderer.g(name) + .attr({ + visibility: visibility, + zIndex: zIndex || 0.1 // IE8 needs this + }) + .add(parent); + } + // Place it on first and subsequent (redraw) calls + group[isNew ? 'attr' : 'animate'](this.getPlotBox()); + return group; + }, + + /** + * Get the translation and scale for the plot area of this series + */ + getPlotBox: function () { + return { + translateX: this.xAxis ? this.xAxis.left : this.chart.plotLeft, + translateY: this.yAxis ? this.yAxis.top : this.chart.plotTop, + scaleX: 1, // #1623 + scaleY: 1 + }; + }, + + /** + * Render the graph and markers + */ + render: function () { + var series = this, + chart = series.chart, + group, + options = series.options, + animation = options.animation, + doAnimation = animation && !!series.animate && + chart.renderer.isSVG, // this animation doesn't work in IE8 quirks when the group div is hidden, + // and looks bad in other oldIE + visibility = series.visible ? VISIBLE : HIDDEN, + zIndex = options.zIndex, + hasRendered = series.hasRendered, + chartSeriesGroup = chart.seriesGroup; + + // the group + group = series.plotGroup( + 'group', + 'series', + visibility, + zIndex, + chartSeriesGroup + ); + + series.markerGroup = series.plotGroup( + 'markerGroup', + 'markers', + visibility, + zIndex, + chartSeriesGroup + ); + + // initiate the animation + if (doAnimation) { + series.animate(true); + } + + // cache attributes for shapes + series.getAttribs(); + + // SVGRenderer needs to know this before drawing elements (#1089, #1795) + group.inverted = series.isCartesian ? chart.inverted : false; + + // draw the graph if any + if (series.drawGraph) { + series.drawGraph(); + series.clipNeg(); + } + + // draw the data labels (inn pies they go before the points) + series.drawDataLabels(); + + // draw the points + series.drawPoints(); + + + // draw the mouse tracking area + if (series.options.enableMouseTracking !== false) { + series.drawTracker(); + } + + // Handle inverted series and tracker groups + if (chart.inverted) { + series.invertGroups(); + } + + // Initial clipping, must be defined after inverting groups for VML + if (options.clip !== false && !series.sharedClipKey && !hasRendered) { + group.clip(chart.clipRect); + } + + // Run the animation + if (doAnimation) { + series.animate(); + } else if (!hasRendered) { + series.afterAnimate(); + } + + series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see + // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see + series.hasRendered = true; + }, + + /** + * Redraw the series after an update in the axes. + */ + redraw: function () { + var series = this, + chart = series.chart, + wasDirtyData = series.isDirtyData, // cache it here as it is set to false in render, but used after + group = series.group, + xAxis = series.xAxis, + yAxis = series.yAxis; + + // reposition on resize + if (group) { + if (chart.inverted) { + group.attr({ + width: chart.plotWidth, + height: chart.plotHeight + }); + } + + group.animate({ + translateX: pick(xAxis && xAxis.left, chart.plotLeft), + translateY: pick(yAxis && yAxis.top, chart.plotTop) + }); + } + + series.translate(); + series.setTooltipPoints(true); + + series.render(); + if (wasDirtyData) { + fireEvent(series, 'updatedData'); + } + }, + + /** + * Set the state of the graph + */ + setState: function (state) { + var series = this, + options = series.options, + graph = series.graph, + graphNeg = series.graphNeg, + stateOptions = options.states, + lineWidth = options.lineWidth, + attribs; + + state = state || NORMAL_STATE; + + if (series.state !== state) { + series.state = state; + + if (stateOptions[state] && stateOptions[state].enabled === false) { + return; + } + + if (state) { + lineWidth = stateOptions[state].lineWidth || lineWidth + 1; + } + + if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML + attribs = { + 'stroke-width': lineWidth + }; + // use attr because animate will cause any other animation on the graph to stop + graph.attr(attribs); + if (graphNeg) { + graphNeg.attr(attribs); + } + } + } + }, + + /** + * Set the visibility of the graph + * + * @param vis {Boolean} True to show the series, false to hide. If UNDEFINED, + * the visibility is toggled. + */ + setVisible: function (vis, redraw) { + var series = this, + chart = series.chart, + legendItem = series.legendItem, + showOrHide, + ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries, + oldVisibility = series.visible; + + // if called without an argument, toggle visibility + series.visible = vis = series.userOptions.visible = vis === UNDEFINED ? !oldVisibility : vis; + showOrHide = vis ? 'show' : 'hide'; + + // show or hide elements + each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function (key) { + if (series[key]) { + series[key][showOrHide](); + } + }); + + + // hide tooltip (#1361) + if (chart.hoverSeries === series) { + series.onMouseOut(); + } + + + if (legendItem) { + chart.legend.colorizeItem(series, vis); + } + + + // rescale or adapt to resized chart + series.isDirty = true; + // in a stack, all other series are affected + if (series.options.stacking) { + each(chart.series, function (otherSeries) { + if (otherSeries.options.stacking && otherSeries.visible) { + otherSeries.isDirty = true; + } + }); + } + + // show or hide linked series + each(series.linkedSeries, function (otherSeries) { + otherSeries.setVisible(vis, false); + }); + + if (ignoreHiddenSeries) { + chart.isDirtyBox = true; + } + if (redraw !== false) { + chart.redraw(); + } + + fireEvent(series, showOrHide); + }, + + /** + * Show the graph + */ + show: function () { + this.setVisible(true); + }, + + /** + * Hide the graph + */ + hide: function () { + this.setVisible(false); + }, + + + /** + * Set the selected state of the graph + * + * @param selected {Boolean} True to select the series, false to unselect. If + * UNDEFINED, the selection state is toggled. + */ + select: function (selected) { + var series = this; + // if called without an argument, toggle + series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected; + + if (series.checkbox) { + series.checkbox.checked = selected; + } + + fireEvent(series, selected ? 'select' : 'unselect'); + }, + + /** + * Draw the tracker object that sits above all data labels and markers to + * track mouse events on the graph or points. For the line type charts + * the tracker uses the same graphPath, but with a greater stroke width + * for better control. + */ + drawTracker: function () { + var series = this, + options = series.options, + trackByArea = options.trackByArea, + trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath), + trackerPathLength = trackerPath.length, + chart = series.chart, + pointer = chart.pointer, + renderer = chart.renderer, + snap = chart.options.tooltip.snap, + tracker = series.tracker, + cursor = options.cursor, + css = cursor && { cursor: cursor }, + singlePoints = series.singlePoints, + singlePoint, + i, + onMouseOver = function () { + if (chart.hoverSeries !== series) { + series.onMouseOver(); + } + }; + + // Extend end points. A better way would be to use round linecaps, + // but those are not clickable in VML. + if (trackerPathLength && !trackByArea) { + i = trackerPathLength + 1; + while (i--) { + if (trackerPath[i] === M) { // extend left side + trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L); + } + if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side + trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]); + } + } + } + + // handle single points + for (i = 0; i < singlePoints.length; i++) { + singlePoint = singlePoints[i]; + trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY, + L, singlePoint.plotX + snap, singlePoint.plotY); + } + + + + // draw the tracker + if (tracker) { + tracker.attr({ d: trackerPath }); + + } else { // create + + series.tracker = renderer.path(trackerPath) + .attr({ + 'stroke-linejoin': 'round', // #1225 + visibility: series.visible ? VISIBLE : HIDDEN, + stroke: TRACKER_FILL, + fill: trackByArea ? TRACKER_FILL : NONE, + 'stroke-width' : options.lineWidth + (trackByArea ? 0 : 2 * snap), + zIndex: 2 + }) + .add(series.group); + + // The tracker is added to the series group, which is clipped, but is covered + // by the marker group. So the marker group also needs to capture events. + each([series.tracker, series.markerGroup], function (tracker) { + tracker.addClass(PREFIX + 'tracker') + .on('mouseover', onMouseOver) + .on('mouseout', function (e) { pointer.onTrackerMouseOut(e); }) + .css(css); + + if (hasTouch) { + tracker.on('touchstart', onMouseOver); + } + }); + } + + } + +}; // end Series prototype + + +/** + * LineSeries object + */ +var LineSeries = extendClass(Series); +seriesTypes.line = LineSeries; + +/** + * Set the default options for area + */ +defaultPlotOptions.area = merge(defaultSeriesOptions, { + threshold: 0 + // trackByArea: false, + // lineColor: null, // overrides color, but lets fillColor be unaltered + // fillOpacity: 0.75, + // fillColor: null +}); + +/** + * AreaSeries object + */ +var AreaSeries = extendClass(Series, { + type: 'area', + + /** + * For stacks, don't split segments on null values. Instead, draw null values with + * no marker. Also insert dummy points for any X position that exists in other series + * in the stack. + */ + getSegments: function () { + var segments = [], + segment = [], + keys = [], + xAxis = this.xAxis, + yAxis = this.yAxis, + stack = yAxis.stacks[this.stackKey], + pointMap = {}, + plotX, + plotY, + points = this.points, + connectNulls = this.options.connectNulls, + val, + i, + x; + + if (this.options.stacking && !this.cropped) { // cropped causes artefacts in Stock, and perf issue + // Create a map where we can quickly look up the points by their X value. + for (i = 0; i < points.length; i++) { + pointMap[points[i].x] = points[i]; + } + + // Sort the keys (#1651) + for (x in stack) { + keys.push(+x); + } + keys.sort(function (a, b) { + return a - b; + }); + + each(keys, function (x) { + if (connectNulls && (!pointMap[x] || pointMap[x].y === null)) { // #1836 + return; + + // The point exists, push it to the segment + } else if (pointMap[x]) { + segment.push(pointMap[x]); + + // There is no point for this X value in this series, so we + // insert a dummy point in order for the areas to be drawn + // correctly. + } else { + plotX = xAxis.translate(x); + val = stack[x].percent ? (stack[x].total ? stack[x].cum * 100 / stack[x].total : 0) : stack[x].cum; // #1991 + plotY = yAxis.toPixels(val, true); + segment.push({ + y: null, + plotX: plotX, + clientX: plotX, + plotY: plotY, + yBottom: plotY, + onMouseOver: noop + }); + } + }); + + if (segment.length) { + segments.push(segment); + } + + } else { + Series.prototype.getSegments.call(this); + segments = this.segments; + } + + this.segments = segments; + }, + + /** + * Extend the base Series getSegmentPath method by adding the path for the area. + * This path is pushed to the series.areaPath property. + */ + getSegmentPath: function (segment) { + + var segmentPath = Series.prototype.getSegmentPath.call(this, segment), // call base method + areaSegmentPath = [].concat(segmentPath), // work on a copy for the area path + i, + options = this.options, + segLength = segmentPath.length, + translatedThreshold = this.yAxis.getThreshold(options.threshold), // #2181 + yBottom; + + if (segLength === 3) { // for animation from 1 to two points + areaSegmentPath.push(L, segmentPath[1], segmentPath[2]); + } + if (options.stacking && !this.closedStacks) { + + // Follow stack back. Todo: implement areaspline. A general solution could be to + // reverse the entire graphPath of the previous series, though may be hard with + // splines and with series with different extremes + for (i = segment.length - 1; i >= 0; i--) { + + yBottom = pick(segment[i].yBottom, translatedThreshold); + + // step line? + if (i < segment.length - 1 && options.step) { + areaSegmentPath.push(segment[i + 1].plotX, yBottom); + } + + areaSegmentPath.push(segment[i].plotX, yBottom); + } + + } else { // follow zero line back + this.closeSegment(areaSegmentPath, segment, translatedThreshold); + } + this.areaPath = this.areaPath.concat(areaSegmentPath); + return segmentPath; + }, + + /** + * Extendable method to close the segment path of an area. This is overridden in polar + * charts. + */ + closeSegment: function (path, segment, translatedThreshold) { + path.push( + L, + segment[segment.length - 1].plotX, + translatedThreshold, + L, + segment[0].plotX, + translatedThreshold + ); + }, + + /** + * Draw the graph and the underlying area. This method calls the Series base + * function and adds the area. The areaPath is calculated in the getSegmentPath + * method called from Series.prototype.drawGraph. + */ + drawGraph: function () { + + // Define or reset areaPath + this.areaPath = []; + + // Call the base method + Series.prototype.drawGraph.apply(this); + + // Define local variables + var series = this, + areaPath = this.areaPath, + options = this.options, + negativeColor = options.negativeColor, + negativeFillColor = options.negativeFillColor, + props = [['area', this.color, options.fillColor]]; // area name, main color, fill color + + if (negativeColor || negativeFillColor) { + props.push(['areaNeg', negativeColor, negativeFillColor]); + } + + each(props, function (prop) { + var areaKey = prop[0], + area = series[areaKey]; + + // Create or update the area + if (area) { // update + area.animate({ d: areaPath }); + + } else { // create + series[areaKey] = series.chart.renderer.path(areaPath) + .attr({ + fill: pick( + prop[2], + Color(prop[1]).setOpacity(pick(options.fillOpacity, 0.75)).get() + ), + zIndex: 0 // #1069 + }).add(series.group); + } + }); + }, + + /** + * Get the series' symbol in the legend + * + * @param {Object} legend The legend object + * @param {Object} item The series (this) or point + */ + drawLegendSymbol: function (legend, item) { + + item.legendSymbol = this.chart.renderer.rect( + 0, + legend.baseline - 11, + legend.options.symbolWidth, + 12, + 2 + ).attr({ + zIndex: 3 + }).add(item.legendGroup); + + } +}); + +seriesTypes.area = AreaSeries;/** + * Set the default options for spline + */ +defaultPlotOptions.spline = merge(defaultSeriesOptions); + +/** + * SplineSeries object + */ +var SplineSeries = extendClass(Series, { + type: 'spline', + + /** + * Get the spline segment from a given point's previous neighbour to the given point + */ + getPointSpline: function (segment, point, i) { + var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc + denom = smoothing + 1, + plotX = point.plotX, + plotY = point.plotY, + lastPoint = segment[i - 1], + nextPoint = segment[i + 1], + leftContX, + leftContY, + rightContX, + rightContY, + ret; + + // find control points + if (lastPoint && nextPoint) { + + var lastX = lastPoint.plotX, + lastY = lastPoint.plotY, + nextX = nextPoint.plotX, + nextY = nextPoint.plotY, + correction; + + leftContX = (smoothing * plotX + lastX) / denom; + leftContY = (smoothing * plotY + lastY) / denom; + rightContX = (smoothing * plotX + nextX) / denom; + rightContY = (smoothing * plotY + nextY) / denom; + + // have the two control points make a straight line through main point + correction = ((rightContY - leftContY) * (rightContX - plotX)) / + (rightContX - leftContX) + plotY - rightContY; + + leftContY += correction; + rightContY += correction; + + // to prevent false extremes, check that control points are between + // neighbouring points' y values + if (leftContY > lastY && leftContY > plotY) { + leftContY = mathMax(lastY, plotY); + rightContY = 2 * plotY - leftContY; // mirror of left control point + } else if (leftContY < lastY && leftContY < plotY) { + leftContY = mathMin(lastY, plotY); + rightContY = 2 * plotY - leftContY; + } + if (rightContY > nextY && rightContY > plotY) { + rightContY = mathMax(nextY, plotY); + leftContY = 2 * plotY - rightContY; + } else if (rightContY < nextY && rightContY < plotY) { + rightContY = mathMin(nextY, plotY); + leftContY = 2 * plotY - rightContY; + } + + // record for drawing in next point + point.rightContX = rightContX; + point.rightContY = rightContY; + + } + + // Visualize control points for debugging + /* + if (leftContX) { + this.chart.renderer.circle(leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, 2) + .attr({ + stroke: 'red', + 'stroke-width': 1, + fill: 'none' + }) + .add(); + this.chart.renderer.path(['M', leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, + 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) + .attr({ + stroke: 'red', + 'stroke-width': 1 + }) + .add(); + this.chart.renderer.circle(rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, 2) + .attr({ + stroke: 'green', + 'stroke-width': 1, + fill: 'none' + }) + .add(); + this.chart.renderer.path(['M', rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, + 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) + .attr({ + stroke: 'green', + 'stroke-width': 1 + }) + .add(); + } + */ + + // moveTo or lineTo + if (!i) { + ret = [M, plotX, plotY]; + } else { // curve from last point to this + ret = [ + 'C', + lastPoint.rightContX || lastPoint.plotX, + lastPoint.rightContY || lastPoint.plotY, + leftContX || plotX, + leftContY || plotY, + plotX, + plotY + ]; + lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later + } + return ret; + } +}); +seriesTypes.spline = SplineSeries; + +/** + * Set the default options for areaspline + */ +defaultPlotOptions.areaspline = merge(defaultPlotOptions.area); + +/** + * AreaSplineSeries object + */ +var areaProto = AreaSeries.prototype, + AreaSplineSeries = extendClass(SplineSeries, { + type: 'areaspline', + closedStacks: true, // instead of following the previous graph back, follow the threshold back + + // Mix in methods from the area series + getSegmentPath: areaProto.getSegmentPath, + closeSegment: areaProto.closeSegment, + drawGraph: areaProto.drawGraph, + drawLegendSymbol: areaProto.drawLegendSymbol + }); +seriesTypes.areaspline = AreaSplineSeries; + +/** + * Set the default options for column + */ +defaultPlotOptions.column = merge(defaultSeriesOptions, { + borderColor: '#FFFFFF', + borderWidth: 1, + borderRadius: 0, + //colorByPoint: undefined, + groupPadding: 0.2, + //grouping: true, + marker: null, // point options are specified in the base options + pointPadding: 0.1, + //pointWidth: null, + minPointLength: 0, + cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes + pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories + states: { + hover: { + brightness: 0.1, + shadow: false + }, + select: { + color: '#C0C0C0', + borderColor: '#000000', + shadow: false + } + }, + dataLabels: { + align: null, // auto + verticalAlign: null, // auto + y: null + }, + stickyTracking: false, + threshold: 0 +}); + +/** + * ColumnSeries object + */ +var ColumnSeries = extendClass(Series, { + type: 'column', + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'borderColor', + 'stroke-width': 'borderWidth', + fill: 'color', + r: 'borderRadius' + }, + cropShoulder: 0, + trackerGroups: ['group', 'dataLabelsGroup'], + negStacks: true, // use separate negative stacks, unlike area stacks where a negative + // point is substracted from previous (#1910) + + /** + * Initialize the series + */ + init: function () { + Series.prototype.init.apply(this, arguments); + + var series = this, + chart = series.chart; + + // if the series is added dynamically, force redraw of other + // series affected by a new column + if (chart.hasRendered) { + each(chart.series, function (otherSeries) { + if (otherSeries.type === series.type) { + otherSeries.isDirty = true; + } + }); + } + }, + + /** + * Return the width and x offset of the columns adjusted for grouping, groupPadding, pointPadding, + * pointWidth etc. + */ + getColumnMetrics: function () { + + var series = this, + options = series.options, + xAxis = series.xAxis, + yAxis = series.yAxis, + reversedXAxis = xAxis.reversed, + stackKey, + stackGroups = {}, + columnIndex, + columnCount = 0; + + // Get the total number of column type series. + // This is called on every series. Consider moving this logic to a + // chart.orderStacks() function and call it on init, addSeries and removeSeries + if (options.grouping === false) { + columnCount = 1; + } else { + each(series.chart.series, function (otherSeries) { + var otherOptions = otherSeries.options, + otherYAxis = otherSeries.yAxis; + if (otherSeries.type === series.type && otherSeries.visible && + yAxis.len === otherYAxis.len && yAxis.pos === otherYAxis.pos) { // #642, #2086 + if (otherOptions.stacking) { + stackKey = otherSeries.stackKey; + if (stackGroups[stackKey] === UNDEFINED) { + stackGroups[stackKey] = columnCount++; + } + columnIndex = stackGroups[stackKey]; + } else if (otherOptions.grouping !== false) { // #1162 + columnIndex = columnCount++; + } + otherSeries.columnIndex = columnIndex; + } + }); + } + + var categoryWidth = mathMin( + mathAbs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || 1), + xAxis.len // #1535 + ), + groupPadding = categoryWidth * options.groupPadding, + groupWidth = categoryWidth - 2 * groupPadding, + pointOffsetWidth = groupWidth / columnCount, + optionPointWidth = options.pointWidth, + pointPadding = defined(optionPointWidth) ? (pointOffsetWidth - optionPointWidth) / 2 : + pointOffsetWidth * options.pointPadding, + pointWidth = pick(optionPointWidth, pointOffsetWidth - 2 * pointPadding), // exact point width, used in polar charts + colIndex = (reversedXAxis ? + columnCount - (series.columnIndex || 0) : // #1251 + series.columnIndex) || 0, + pointXOffset = pointPadding + (groupPadding + colIndex * + pointOffsetWidth - (categoryWidth / 2)) * + (reversedXAxis ? -1 : 1); + + // Save it for reading in linked series (Error bars particularly) + return (series.columnMetrics = { + width: pointWidth, + offset: pointXOffset + }); + + }, + + /** + * Translate each point to the plot area coordinate system and find shape positions + */ + translate: function () { + var series = this, + chart = series.chart, + options = series.options, + borderWidth = options.borderWidth, + yAxis = series.yAxis, + threshold = options.threshold, + translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold), + minPointLength = pick(options.minPointLength, 5), + metrics = series.getColumnMetrics(), + pointWidth = metrics.width, + seriesBarW = series.barW = mathCeil(mathMax(pointWidth, 1 + 2 * borderWidth)), // rounded and postprocessed for border width + pointXOffset = series.pointXOffset = metrics.offset, + xCrisp = -(borderWidth % 2 ? 0.5 : 0), + yCrisp = borderWidth % 2 ? 0.5 : 1; + + if (chart.renderer.isVML && chart.inverted) { + yCrisp += 1; + } + + Series.prototype.translate.apply(series); + + // record the new values + each(series.points, function (point) { + var yBottom = pick(point.yBottom, translatedThreshold), + plotY = mathMin(mathMax(-999 - yBottom, point.plotY), yAxis.len + 999 + yBottom), // Don't draw too far outside plot area (#1303, #2241) + barX = point.plotX + pointXOffset, + barW = seriesBarW, + barY = mathMin(plotY, yBottom), + right, + bottom, + fromTop, + fromLeft, + barH = mathMax(plotY, yBottom) - barY; + + // Handle options.minPointLength + if (mathAbs(barH) < minPointLength) { + if (minPointLength) { + barH = minPointLength; + barY = + mathRound(mathAbs(barY - translatedThreshold) > minPointLength ? // stacked + yBottom - minPointLength : // keep position + translatedThreshold - (yAxis.translate(point.y, 0, 1, 0, 1) <= translatedThreshold ? minPointLength : 0)); // use exact yAxis.translation (#1485) + } + } + + // Cache for access in polar + point.barX = barX; + point.pointWidth = pointWidth; + + + // Round off to obtain crisp edges + fromLeft = mathAbs(barX) < 0.5; + right = mathRound(barX + barW) + xCrisp; + barX = mathRound(barX) + xCrisp; + barW = right - barX; + + fromTop = mathAbs(barY) < 0.5; + bottom = mathRound(barY + barH) + yCrisp; + barY = mathRound(barY) + yCrisp; + barH = bottom - barY; + + // Top and left edges are exceptions + if (fromLeft) { + barX += 1; + barW -= 1; + } + if (fromTop) { + barY -= 1; + barH += 1; + } + + // Register shape type and arguments to be used in drawPoints + point.shapeType = 'rect'; + point.shapeArgs = { + x: barX, + y: barY, + width: barW, + height: barH + }; + }); + + }, + + getSymbol: noop, + + /** + * Use a solid rectangle like the area series types + */ + drawLegendSymbol: AreaSeries.prototype.drawLegendSymbol, + + + /** + * Columns have no graph + */ + drawGraph: noop, + + /** + * Draw the columns. For bars, the series.group is rotated, so the same coordinates + * apply for columns and bars. This method is inherited by scatter series. + * + */ + drawPoints: function () { + var series = this, + options = series.options, + renderer = series.chart.renderer, + shapeArgs; + + + // draw the columns + each(series.points, function (point) { + var plotY = point.plotY, + graphic = point.graphic; + + if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) { + shapeArgs = point.shapeArgs; + + if (graphic) { // update + stop(graphic); + graphic.animate(merge(shapeArgs)); + + } else { + point.graphic = graphic = renderer[point.shapeType](shapeArgs) + .attr(point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]) + .add(series.group) + .shadow(options.shadow, null, options.stacking && !options.borderRadius); + } + + } else if (graphic) { + point.graphic = graphic.destroy(); // #1269 + } + }); + }, + + /** + * Add tracking event listener to the series group, so the point graphics + * themselves act as trackers + */ + drawTracker: function () { + var series = this, + chart = series.chart, + pointer = chart.pointer, + cursor = series.options.cursor, + css = cursor && { cursor: cursor }, + onMouseOver = function (e) { + var target = e.target, + point; + + if (chart.hoverSeries !== series) { + series.onMouseOver(); + } + while (target && !point) { + point = target.point; + target = target.parentNode; + } + if (point !== UNDEFINED && point !== chart.hoverPoint) { // undefined on graph in scatterchart + point.onMouseOver(e); + } + }; + + // Add reference to the point + each(series.points, function (point) { + if (point.graphic) { + point.graphic.element.point = point; + } + if (point.dataLabel) { + point.dataLabel.element.point = point; + } + }); + + // Add the event listeners, we need to do this only once + if (!series._hasTracking) { + each(series.trackerGroups, function (key) { + if (series[key]) { // we don't always have dataLabelsGroup + series[key] + .addClass(PREFIX + 'tracker') + .on('mouseover', onMouseOver) + .on('mouseout', function (e) { pointer.onTrackerMouseOut(e); }) + .css(css); + if (hasTouch) { + series[key].on('touchstart', onMouseOver); + } + } + }); + series._hasTracking = true; + } + }, + + /** + * Override the basic data label alignment by adjusting for the position of the column + */ + alignDataLabel: function (point, dataLabel, options, alignTo, isNew) { + var chart = this.chart, + inverted = chart.inverted, + dlBox = point.dlBox || point.shapeArgs, // data label box for alignment + below = point.below || (point.plotY > pick(this.translatedThreshold, chart.plotSizeY)), + inside = pick(options.inside, !!this.options.stacking); // draw it inside the box? + + // Align to the column itself, or the top of it + if (dlBox) { // Area range uses this method but not alignTo + alignTo = merge(dlBox); + if (inverted) { + alignTo = { + x: chart.plotWidth - alignTo.y - alignTo.height, + y: chart.plotHeight - alignTo.x - alignTo.width, + width: alignTo.height, + height: alignTo.width + }; + } + + // Compute the alignment box + if (!inside) { + if (inverted) { + alignTo.x += below ? 0 : alignTo.width; + alignTo.width = 0; + } else { + alignTo.y += below ? alignTo.height : 0; + alignTo.height = 0; + } + } + } + + // When alignment is undefined (typically columns and bars), display the individual + // point below or above the point depending on the threshold + options.align = pick( + options.align, + !inverted || inside ? 'center' : below ? 'right' : 'left' + ); + options.verticalAlign = pick( + options.verticalAlign, + inverted || inside ? 'middle' : below ? 'top' : 'bottom' + ); + + // Call the parent method + Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew); + }, + + + /** + * Animate the column heights one by one from zero + * @param {Boolean} init Whether to initialize the animation or run it + */ + animate: function (init) { + var series = this, + yAxis = this.yAxis, + options = series.options, + inverted = this.chart.inverted, + attr = {}, + translatedThreshold; + + if (hasSVG) { // VML is too slow anyway + if (init) { + attr.scaleY = 0.001; + translatedThreshold = mathMin(yAxis.pos + yAxis.len, mathMax(yAxis.pos, yAxis.toPixels(options.threshold))); + if (inverted) { + attr.translateX = translatedThreshold - yAxis.len; + } else { + attr.translateY = translatedThreshold; + } + series.group.attr(attr); + + } else { // run the animation + + attr.scaleY = 1; + attr[inverted ? 'translateX' : 'translateY'] = yAxis.pos; + series.group.animate(attr, series.options.animation); + + // delete this function to allow it only once + series.animate = null; + } + } + }, + + /** + * Remove this series from the chart + */ + remove: function () { + var series = this, + chart = series.chart; + + // column and bar series affects other series of the same type + // as they are either stacked or grouped + if (chart.hasRendered) { + each(chart.series, function (otherSeries) { + if (otherSeries.type === series.type) { + otherSeries.isDirty = true; + } + }); + } + + Series.prototype.remove.apply(series, arguments); + } +}); +seriesTypes.column = ColumnSeries; +/** + * Set the default options for bar + */ +defaultPlotOptions.bar = merge(defaultPlotOptions.column); +/** + * The Bar series class + */ +var BarSeries = extendClass(ColumnSeries, { + type: 'bar', + inverted: true +}); +seriesTypes.bar = BarSeries; + +/** + * Set the default options for scatter + */ +defaultPlotOptions.scatter = merge(defaultSeriesOptions, { + lineWidth: 0, + tooltip: { + headerFormat: '{series.name}
              ', + pointFormat: 'x: {point.x}
              y: {point.y}
              ', + followPointer: true + }, + stickyTracking: false +}); + +/** + * The scatter series class + */ +var ScatterSeries = extendClass(Series, { + type: 'scatter', + sorted: false, + requireSorting: false, + noSharedTooltip: true, + trackerGroups: ['markerGroup'], + + drawTracker: ColumnSeries.prototype.drawTracker, + + setTooltipPoints: noop +}); +seriesTypes.scatter = ScatterSeries; + +/** + * Set the default options for pie + */ +defaultPlotOptions.pie = merge(defaultSeriesOptions, { + borderColor: '#FFFFFF', + borderWidth: 1, + center: [null, null], + clip: false, + colorByPoint: true, // always true for pies + dataLabels: { + // align: null, + // connectorWidth: 1, + // connectorColor: point.color, + // connectorPadding: 5, + distance: 30, + enabled: true, + formatter: function () { + return this.point.name; + } + // softConnector: true, + //y: 0 + }, + ignoreHiddenPoint: true, + //innerSize: 0, + legendType: 'point', + marker: null, // point options are specified in the base options + size: null, + showInLegend: false, + slicedOffset: 10, + states: { + hover: { + brightness: 0.1, + shadow: false + } + }, + stickyTracking: false, + tooltip: { + followPointer: true + } +}); + +/** + * Extended point object for pies + */ +var PiePoint = extendClass(Point, { + /** + * Initiate the pie slice + */ + init: function () { + + Point.prototype.init.apply(this, arguments); + + var point = this, + toggleSlice; + + // Disallow negative values (#1530) + if (point.y < 0) { + point.y = null; + } + + //visible: options.visible !== false, + extend(point, { + visible: point.visible !== false, + name: pick(point.name, 'Slice') + }); + + // add event listener for select + toggleSlice = function (e) { + point.slice(e.type === 'select'); + }; + addEvent(point, 'select', toggleSlice); + addEvent(point, 'unselect', toggleSlice); + + return point; + }, + + /** + * Toggle the visibility of the pie slice + * @param {Boolean} vis Whether to show the slice or not. If undefined, the + * visibility is toggled + */ + setVisible: function (vis) { + var point = this, + series = point.series, + chart = series.chart, + method; + + // if called without an argument, toggle visibility + point.visible = point.options.visible = vis = vis === UNDEFINED ? !point.visible : vis; + series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data + + method = vis ? 'show' : 'hide'; + + // Show and hide associated elements + each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function (key) { + if (point[key]) { + point[key][method](); + } + }); + + if (point.legendItem) { + chart.legend.colorizeItem(point, vis); + } + + // Handle ignore hidden slices + if (!series.isDirty && series.options.ignoreHiddenPoint) { + series.isDirty = true; + chart.redraw(); + } + }, + + /** + * Set or toggle whether the slice is cut out from the pie + * @param {Boolean} sliced When undefined, the slice state is toggled + * @param {Boolean} redraw Whether to redraw the chart. True by default. + */ + slice: function (sliced, redraw, animation) { + var point = this, + series = point.series, + chart = series.chart, + translation; + + setAnimation(animation, chart); + + // redraw is true by default + redraw = pick(redraw, true); + + // if called without an argument, toggle + point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced; + series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data + + translation = sliced ? point.slicedTranslation : { + translateX: 0, + translateY: 0 + }; + + point.graphic.animate(translation); + + if (point.shadowGroup) { + point.shadowGroup.animate(translation); + } + + } +}); + +/** + * The Pie series class + */ +var PieSeries = { + type: 'pie', + isCartesian: false, + pointClass: PiePoint, + requireSorting: false, + noSharedTooltip: true, + trackerGroups: ['group', 'dataLabelsGroup'], + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'borderColor', + 'stroke-width': 'borderWidth', + fill: 'color' + }, + + /** + * Pies have one color each point + */ + getColor: noop, + + /** + * Animate the pies in + */ + animate: function (init) { + var series = this, + points = series.points, + startAngleRad = series.startAngleRad; + + if (!init) { + each(points, function (point) { + var graphic = point.graphic, + args = point.shapeArgs; + + if (graphic) { + // start values + graphic.attr({ + r: series.center[3] / 2, // animate from inner radius (#779) + start: startAngleRad, + end: startAngleRad + }); + + // animate + graphic.animate({ + r: args.r, + start: args.start, + end: args.end + }, series.options.animation); + } + }); + + // delete this function to allow it only once + series.animate = null; + } + }, + + /** + * Extend the basic setData method by running processData and generatePoints immediately, + * in order to access the points from the legend. + */ + setData: function (data, redraw) { + Series.prototype.setData.call(this, data, false); + this.processData(); + this.generatePoints(); + if (pick(redraw, true)) { + this.chart.redraw(); + } + }, + + /** + * Extend the generatePoints method by adding total and percentage properties to each point + */ + generatePoints: function () { + var i, + total = 0, + points, + len, + point, + ignoreHiddenPoint = this.options.ignoreHiddenPoint; + + Series.prototype.generatePoints.call(this); + + // Populate local vars + points = this.points; + len = points.length; + + // Get the total sum + for (i = 0; i < len; i++) { + point = points[i]; + total += (ignoreHiddenPoint && !point.visible) ? 0 : point.y; + } + this.total = total; + + // Set each point's properties + for (i = 0; i < len; i++) { + point = points[i]; + point.percentage = total > 0 ? (point.y / total) * 100 : 0; + point.total = total; + } + + }, + + /** + * Get the center of the pie based on the size and center options relative to the + * plot area. Borrowed by the polar and gauge series types. + */ + getCenter: function () { + + var options = this.options, + chart = this.chart, + slicingRoom = 2 * (options.slicedOffset || 0), + handleSlicingRoom, + plotWidth = chart.plotWidth - 2 * slicingRoom, + plotHeight = chart.plotHeight - 2 * slicingRoom, + centerOption = options.center, + positions = [pick(centerOption[0], '50%'), pick(centerOption[1], '50%'), options.size || '100%', options.innerSize || 0], + smallestSize = mathMin(plotWidth, plotHeight), + isPercent; + + return map(positions, function (length, i) { + isPercent = /%$/.test(length); + handleSlicingRoom = i < 2 || (i === 2 && isPercent); + return (isPercent ? + // i == 0: centerX, relative to width + // i == 1: centerY, relative to height + // i == 2: size, relative to smallestSize + // i == 4: innerSize, relative to smallestSize + [plotWidth, plotHeight, smallestSize, smallestSize][i] * + pInt(length) / 100 : + length) + (handleSlicingRoom ? slicingRoom : 0); + }); + }, + + /** + * Do translation for pie slices + */ + translate: function (positions) { + this.generatePoints(); + + var series = this, + cumulative = 0, + precision = 1000, // issue #172 + options = series.options, + slicedOffset = options.slicedOffset, + connectorOffset = slicedOffset + options.borderWidth, + start, + end, + angle, + startAngle = options.startAngle || 0, + startAngleRad = series.startAngleRad = mathPI / 180 * (startAngle - 90), + endAngleRad = series.endAngleRad = mathPI / 180 * ((options.endAngle || (startAngle + 360)) - 90), // docs + circ = endAngleRad - startAngleRad, //2 * mathPI, + points = series.points, + radiusX, // the x component of the radius vector for a given point + radiusY, + labelDistance = options.dataLabels.distance, + ignoreHiddenPoint = options.ignoreHiddenPoint, + i, + len = points.length, + point; + + // Get positions - either an integer or a percentage string must be given. + // If positions are passed as a parameter, we're in a recursive loop for adjusting + // space for data labels. + if (!positions) { + series.center = positions = series.getCenter(); + } + + // utility for getting the x value from a given y, used for anticollision logic in data labels + series.getX = function (y, left) { + + angle = math.asin((y - positions[1]) / (positions[2] / 2 + labelDistance)); + + return positions[0] + + (left ? -1 : 1) * + (mathCos(angle) * (positions[2] / 2 + labelDistance)); + }; + + // Calculate the geometry for each point + for (i = 0; i < len; i++) { + + point = points[i]; + + // set start and end angle + start = startAngleRad + (cumulative * circ); + if (!ignoreHiddenPoint || point.visible) { + cumulative += point.percentage / 100; + } + end = startAngleRad + (cumulative * circ); + + // set the shape + point.shapeType = 'arc'; + point.shapeArgs = { + x: positions[0], + y: positions[1], + r: positions[2] / 2, + innerR: positions[3] / 2, + start: mathRound(start * precision) / precision, + end: mathRound(end * precision) / precision + }; + + // center for the sliced out slice + angle = (end + start) / 2; + if (angle > 0.75 * circ) { + angle -= 2 * mathPI; + } + point.slicedTranslation = { + translateX: mathRound(mathCos(angle) * slicedOffset), + translateY: mathRound(mathSin(angle) * slicedOffset) + }; + + // set the anchor point for tooltips + radiusX = mathCos(angle) * positions[2] / 2; + radiusY = mathSin(angle) * positions[2] / 2; + point.tooltipPos = [ + positions[0] + radiusX * 0.7, + positions[1] + radiusY * 0.7 + ]; + + point.half = angle < -mathPI / 2 || angle > mathPI / 2 ? 1 : 0; + point.angle = angle; + + // set the anchor point for data labels + connectorOffset = mathMin(connectorOffset, labelDistance / 2); // #1678 + point.labelPos = [ + positions[0] + radiusX + mathCos(angle) * labelDistance, // first break of connector + positions[1] + radiusY + mathSin(angle) * labelDistance, // a/a + positions[0] + radiusX + mathCos(angle) * connectorOffset, // second break, right outside pie + positions[1] + radiusY + mathSin(angle) * connectorOffset, // a/a + positions[0] + radiusX, // landing point for connector + positions[1] + radiusY, // a/a + labelDistance < 0 ? // alignment + 'center' : + point.half ? 'right' : 'left', // alignment + angle // center angle + ]; + + } + }, + + setTooltipPoints: noop, + drawGraph: null, + + /** + * Draw the data points + */ + drawPoints: function () { + var series = this, + chart = series.chart, + renderer = chart.renderer, + groupTranslation, + //center, + graphic, + //group, + shadow = series.options.shadow, + shadowGroup, + shapeArgs; + + if (shadow && !series.shadowGroup) { + series.shadowGroup = renderer.g('shadow') + .add(series.group); + } + + // draw the slices + each(series.points, function (point) { + graphic = point.graphic; + shapeArgs = point.shapeArgs; + shadowGroup = point.shadowGroup; + + // put the shadow behind all points + if (shadow && !shadowGroup) { + shadowGroup = point.shadowGroup = renderer.g('shadow') + .add(series.shadowGroup); + } + + // if the point is sliced, use special translation, else use plot area traslation + groupTranslation = point.sliced ? point.slicedTranslation : { + translateX: 0, + translateY: 0 + }; + + //group.translate(groupTranslation[0], groupTranslation[1]); + if (shadowGroup) { + shadowGroup.attr(groupTranslation); + } + + // draw the slice + if (graphic) { + graphic.animate(extend(shapeArgs, groupTranslation)); + } else { + point.graphic = graphic = renderer.arc(shapeArgs) + .setRadialReference(series.center) + .attr( + point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] + ) + .attr({ 'stroke-linejoin': 'round' }) + .attr(groupTranslation) + .add(series.group) + .shadow(shadow, shadowGroup); + } + + // detect point specific visibility + if (point.visible === false) { + point.setVisible(false); + } + + }); + + }, + + /** + * Utility for sorting data labels + */ + sortByAngle: function (points, sign) { + points.sort(function (a, b) { + return a.angle !== undefined && (b.angle - a.angle) * sign; + }); + }, + + /** + * Override the base drawDataLabels method by pie specific functionality + */ + drawDataLabels: function () { + var series = this, + data = series.data, + point, + chart = series.chart, + options = series.options.dataLabels, + connectorPadding = pick(options.connectorPadding, 10), + connectorWidth = pick(options.connectorWidth, 1), + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + connector, + connectorPath, + softConnector = pick(options.softConnector, true), + distanceOption = options.distance, + seriesCenter = series.center, + radius = seriesCenter[2] / 2, + centerY = seriesCenter[1], + outside = distanceOption > 0, + dataLabel, + dataLabelWidth, + labelPos, + labelHeight, + halves = [// divide the points into right and left halves for anti collision + [], // right + [] // left + ], + x, + y, + visibility, + rankArr, + i, + j, + overflow = [0, 0, 0, 0], // top, right, bottom, left + sort = function (a, b) { + return b.y - a.y; + }; + + // get out if not enabled + if (!series.visible || (!options.enabled && !series._hasPointLabels)) { + return; + } + + // run parent method + Series.prototype.drawDataLabels.apply(series); + + // arrange points for detection collision + each(data, function (point) { + if (point.dataLabel) { // it may have been cancelled in the base method (#407) + halves[point.half].push(point); + } + }); + + // assume equal label heights + i = 0; + while (!labelHeight && data[i]) { // #1569 + labelHeight = data[i] && data[i].dataLabel && (data[i].dataLabel.getBBox().height || 21); // 21 is for #968 + i++; + } + + /* Loop over the points in each half, starting from the top and bottom + * of the pie to detect overlapping labels. + */ + i = 2; + while (i--) { + + var slots = [], + slotsLength, + usedSlots = [], + points = halves[i], + pos, + length = points.length, + slotIndex; + + // Sort by angle + series.sortByAngle(points, i - 0.5); + + // Only do anti-collision when we are outside the pie and have connectors (#856) + if (distanceOption > 0) { + + // build the slots + for (pos = centerY - radius - distanceOption; pos <= centerY + radius + distanceOption; pos += labelHeight) { + slots.push(pos); + + // visualize the slot + /* + var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0), + slotY = pos + chart.plotTop; + if (!isNaN(slotX)) { + chart.renderer.rect(slotX, slotY - 7, 100, labelHeight, 1) + .attr({ + 'stroke-width': 1, + stroke: 'silver' + }) + .add(); + chart.renderer.text('Slot '+ (slots.length - 1), slotX, slotY + 4) + .attr({ + fill: 'silver' + }).add(); + } + */ + } + slotsLength = slots.length; + + // if there are more values than available slots, remove lowest values + if (length > slotsLength) { + // create an array for sorting and ranking the points within each quarter + rankArr = [].concat(points); + rankArr.sort(sort); + j = length; + while (j--) { + rankArr[j].rank = j; + } + j = length; + while (j--) { + if (points[j].rank >= slotsLength) { + points.splice(j, 1); + } + } + length = points.length; + } + + // The label goes to the nearest open slot, but not closer to the edge than + // the label's index. + for (j = 0; j < length; j++) { + + point = points[j]; + labelPos = point.labelPos; + + var closest = 9999, + distance, + slotI; + + // find the closest slot index + for (slotI = 0; slotI < slotsLength; slotI++) { + distance = mathAbs(slots[slotI] - labelPos[1]); + if (distance < closest) { + closest = distance; + slotIndex = slotI; + } + } + + // if that slot index is closer to the edges of the slots, move it + // to the closest appropriate slot + if (slotIndex < j && slots[j] !== null) { // cluster at the top + slotIndex = j; + } else if (slotsLength < length - j + slotIndex && slots[j] !== null) { // cluster at the bottom + slotIndex = slotsLength - length + j; + while (slots[slotIndex] === null) { // make sure it is not taken + slotIndex++; + } + } else { + // Slot is taken, find next free slot below. In the next run, the next slice will find the + // slot above these, because it is the closest one + while (slots[slotIndex] === null) { // make sure it is not taken + slotIndex++; + } + } + + usedSlots.push({ i: slotIndex, y: slots[slotIndex] }); + slots[slotIndex] = null; // mark as taken + } + // sort them in order to fill in from the top + usedSlots.sort(sort); + } + + // now the used slots are sorted, fill them up sequentially + for (j = 0; j < length; j++) { + + var slot, naturalY; + + point = points[j]; + labelPos = point.labelPos; + dataLabel = point.dataLabel; + visibility = point.visible === false ? HIDDEN : VISIBLE; + naturalY = labelPos[1]; + + if (distanceOption > 0) { + slot = usedSlots.pop(); + slotIndex = slot.i; + + // if the slot next to currrent slot is free, the y value is allowed + // to fall back to the natural position + y = slot.y; + if ((naturalY > y && slots[slotIndex + 1] !== null) || + (naturalY < y && slots[slotIndex - 1] !== null)) { + y = naturalY; + } + + } else { + y = naturalY; + } + + // get the x - use the natural x position for first and last slot, to prevent the top + // and botton slice connectors from touching each other on either side + x = options.justify ? + seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption) : + series.getX(slotIndex === 0 || slotIndex === slots.length - 1 ? naturalY : y, i); + + + // Record the placement and visibility + dataLabel._attr = { + visibility: visibility, + align: labelPos[6] + }; + dataLabel._pos = { + x: x + options.x + + ({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0), + y: y + options.y - 10 // 10 is for the baseline (label vs text) + }; + dataLabel.connX = x; + dataLabel.connY = y; + + + // Detect overflowing data labels + if (this.options.size === null) { + dataLabelWidth = dataLabel.width; + // Overflow left + if (x - dataLabelWidth < connectorPadding) { + overflow[3] = mathMax(mathRound(dataLabelWidth - x + connectorPadding), overflow[3]); + + // Overflow right + } else if (x + dataLabelWidth > plotWidth - connectorPadding) { + overflow[1] = mathMax(mathRound(x + dataLabelWidth - plotWidth + connectorPadding), overflow[1]); + } + + // Overflow top + if (y - labelHeight / 2 < 0) { + overflow[0] = mathMax(mathRound(-y + labelHeight / 2), overflow[0]); + + // Overflow left + } else if (y + labelHeight / 2 > plotHeight) { + overflow[2] = mathMax(mathRound(y + labelHeight / 2 - plotHeight), overflow[2]); + } + } + } // for each point + } // for each half + + // Do not apply the final placement and draw the connectors until we have verified + // that labels are not spilling over. + if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) { + + // Place the labels in the final position + this.placeDataLabels(); + + // Draw the connectors + if (outside && connectorWidth) { + each(this.points, function (point) { + connector = point.connector; + labelPos = point.labelPos; + dataLabel = point.dataLabel; + + if (dataLabel && dataLabel._pos) { + visibility = dataLabel._attr.visibility; + x = dataLabel.connX; + y = dataLabel.connY; + connectorPath = softConnector ? [ + M, + x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label + 'C', + x, y, // first break, next to the label + 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5], + labelPos[2], labelPos[3], // second break + L, + labelPos[4], labelPos[5] // base + ] : [ + M, + x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label + L, + labelPos[2], labelPos[3], // second break + L, + labelPos[4], labelPos[5] // base + ]; + + if (connector) { + connector.animate({ d: connectorPath }); + connector.attr('visibility', visibility); + + } else { + point.connector = connector = series.chart.renderer.path(connectorPath).attr({ + 'stroke-width': connectorWidth, + stroke: options.connectorColor || point.color || '#606060', + visibility: visibility + }) + .add(series.group); + } + } else if (connector) { + point.connector = connector.destroy(); + } + }); + } + } + }, + + /** + * Verify whether the data labels are allowed to draw, or we should run more translation and data + * label positioning to keep them inside the plot area. Returns true when data labels are ready + * to draw. + */ + verifyDataLabelOverflow: function (overflow) { + + var center = this.center, + options = this.options, + centerOption = options.center, + minSize = options.minSize || 80, + newSize = minSize, + ret; + + // Handle horizontal size and center + if (centerOption[0] !== null) { // Fixed center + newSize = mathMax(center[2] - mathMax(overflow[1], overflow[3]), minSize); + + } else { // Auto center + newSize = mathMax( + center[2] - overflow[1] - overflow[3], // horizontal overflow + minSize + ); + center[0] += (overflow[3] - overflow[1]) / 2; // horizontal center + } + + // Handle vertical size and center + if (centerOption[1] !== null) { // Fixed center + newSize = mathMax(mathMin(newSize, center[2] - mathMax(overflow[0], overflow[2])), minSize); + + } else { // Auto center + newSize = mathMax( + mathMin( + newSize, + center[2] - overflow[0] - overflow[2] // vertical overflow + ), + minSize + ); + center[1] += (overflow[0] - overflow[2]) / 2; // vertical center + } + + // If the size must be decreased, we need to run translate and drawDataLabels again + if (newSize < center[2]) { + center[2] = newSize; + this.translate(center); + each(this.points, function (point) { + if (point.dataLabel) { + point.dataLabel._pos = null; // reset + } + }); + this.drawDataLabels(); + + // Else, return true to indicate that the pie and its labels is within the plot area + } else { + ret = true; + } + return ret; + }, + + /** + * Perform the final placement of the data labels after we have verified that they + * fall within the plot area. + */ + placeDataLabels: function () { + each(this.points, function (point) { + var dataLabel = point.dataLabel, + _pos; + + if (dataLabel) { + _pos = dataLabel._pos; + if (_pos) { + dataLabel.attr(dataLabel._attr); + dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos); + dataLabel.moved = true; + } else if (dataLabel) { + dataLabel.attr({ y: -999 }); + } + } + }); + }, + + alignDataLabel: noop, + + /** + * Draw point specific tracker objects. Inherit directly from column series. + */ + drawTracker: ColumnSeries.prototype.drawTracker, + + /** + * Use a simple symbol from column prototype + */ + drawLegendSymbol: AreaSeries.prototype.drawLegendSymbol, + + /** + * Pies don't have point marker symbols + */ + getSymbol: noop + +}; +PieSeries = extendClass(Series, PieSeries); +seriesTypes.pie = PieSeries; + + +// global variables +extend(Highcharts, { + + // Constructors + Axis: Axis, + Chart: Chart, + Color: Color, + Legend: Legend, + Pointer: Pointer, + Point: Point, + Tick: Tick, + Tooltip: Tooltip, + Renderer: Renderer, + Series: Series, + SVGElement: SVGElement, + SVGRenderer: SVGRenderer, + + // Various + arrayMin: arrayMin, + arrayMax: arrayMax, + charts: charts, + dateFormat: dateFormat, + format: format, + pathAnim: pathAnim, + getOptions: getOptions, + hasBidiBug: hasBidiBug, + isTouchDevice: isTouchDevice, + numberFormat: numberFormat, + seriesTypes: seriesTypes, + setOptions: setOptions, + addEvent: addEvent, + removeEvent: removeEvent, + createElement: createElement, + discardElement: discardElement, + css: css, + each: each, + extend: extend, + map: map, + merge: merge, + pick: pick, + splat: splat, + extendClass: extendClass, + pInt: pInt, + wrap: wrap, + svg: hasSVG, + canvas: useCanVG, + vml: !hasSVG && !useCanVG, + product: PRODUCT, + version: VERSION +}); +}()); diff --git a/public/static/libs/ueditor/third-party/highcharts/modules/annotations.js b/public/static/libs/ueditor/third-party/highcharts/modules/annotations.js new file mode 100644 index 0000000..773f292 --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/modules/annotations.js @@ -0,0 +1,7 @@ +(function(i,C){function m(a){return typeof a==="number"}function n(a){return a!==D&&a!==null}var D,p,r,s=i.Chart,t=i.extend,z=i.each;r=["path","rect","circle"];p={top:0,left:0,center:0.5,middle:0.5,bottom:1,right:1};var u=C.inArray,A=i.merge,B=function(){this.init.apply(this,arguments)};B.prototype={init:function(a,d){var c=d.shape&&d.shape.type;this.chart=a;var b,f;f={xAxis:0,yAxis:0,title:{style:{},text:"",x:0,y:0},shape:{params:{stroke:"#000000",fill:"transparent",strokeWidth:2}}};b={circle:{params:{x:0, +y:0}}};if(b[c])f.shape=A(f.shape,b[c]);this.options=A({},f,d)},render:function(a){var d=this.chart,c=this.chart.renderer,b=this.group,f=this.title,e=this.shape,h=this.options,i=h.title,l=h.shape;if(!b)b=this.group=c.g();if(!e&&l&&u(l.type,r)!==-1)e=this.shape=c[h.shape.type](l.params),e.add(b);if(!f&&i)f=this.title=c.label(i),f.add(b);b.add(d.annotations.group);this.linkObjects();a!==!1&&this.redraw()},redraw:function(){var a=this.options,d=this.chart,c=this.group,b=this.title,f=this.shape,e=this.linkedObject, +h=d.xAxis[a.xAxis],v=d.yAxis[a.yAxis],l=a.width,w=a.height,x=p[a.anchorY],y=p[a.anchorX],j,o,g,q;if(e)j=e instanceof i.Point?"point":e instanceof i.Series?"series":null,j==="point"?(a.xValue=e.x,a.yValue=e.y,o=e.series):j==="series"&&(o=e),c.visibility!==o.group.visibility&&c.attr({visibility:o.group.visibility});e=n(a.xValue)?h.toPixels(a.xValue+h.minPointOffset)-h.minPixelPadding:a.x;j=n(a.yValue)?v.toPixels(a.yValue):a.y;if(!isNaN(e)&&!isNaN(j)&&m(e)&&m(j)){b&&(b.attr(a.title),b.css(a.title.style)); +if(f){b=t({},a.shape.params);if(a.units==="values"){for(g in b)u(g,["width","x"])>-1?b[g]=h.translate(b[g]):u(g,["height","y"])>-1&&(b[g]=v.translate(b[g]));b.width&&(b.width-=h.toPixels(0)-h.left);b.x&&(b.x+=h.minPixelPadding);if(a.shape.type==="path"){g=b.d;o=e;for(var r=j,s=g.length,k=0;k-1&&d.splice(c,1);z(["title","shape","group"],function(b){a[b]&&(a[b].destroy(),a[b]=null)});a.group=a.title=a.shape=a.chart=a.options=null},update:function(a,d){t(this.options,a);this.linkObjects();this.render(d)}, +linkObjects:function(){var a=this.chart,d=this.linkedObject,c=d&&(d.id||d.options.id),b=this.options.linkedTo;if(n(b)){if(!n(d)||b!==c)this.linkedObject=a.get(b)}else this.linkedObject=null}};t(s.prototype,{annotations:{add:function(a,d){var c=this.allItems,b=this.chart,f,e;Object.prototype.toString.call(a)==="[object Array]"||(a=[a]);for(e=a.length;e--;)f=new B(b,a[e]),c.push(f),f.render(d)},redraw:function(){z(this.allItems,function(a){a.redraw()})}}});s.prototype.callbacks.push(function(a){var d= +a.options.annotations,c;c=a.renderer.g("annotations");c.attr({zIndex:7});c.add();a.annotations.allItems=[];a.annotations.chart=a;a.annotations.group=c;Object.prototype.toString.call(d)==="[object Array]"&&d.length>0&&a.annotations.add(a.options.annotations);i.addEvent(a,"redraw",function(){a.annotations.redraw()})})})(Highcharts,HighchartsAdapter); diff --git a/public/static/libs/ueditor/third-party/highcharts/modules/annotations.src.js b/public/static/libs/ueditor/third-party/highcharts/modules/annotations.src.js new file mode 100644 index 0000000..38fea4e --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/modules/annotations.src.js @@ -0,0 +1,401 @@ +(function (Highcharts, HighchartsAdapter) { + +var UNDEFINED, + ALIGN_FACTOR, + ALLOWED_SHAPES, + Chart = Highcharts.Chart, + extend = Highcharts.extend, + each = Highcharts.each; + +ALLOWED_SHAPES = ["path", "rect", "circle"]; + +ALIGN_FACTOR = { + top: 0, + left: 0, + center: 0.5, + middle: 0.5, + bottom: 1, + right: 1 +}; + + +// Highcharts helper methods +var inArray = HighchartsAdapter.inArray, + merge = Highcharts.merge; + +function defaultOptions(shapeType) { + var shapeOptions, + options; + + options = { + xAxis: 0, + yAxis: 0, + title: { + style: {}, + text: "", + x: 0, + y: 0 + }, + shape: { + params: { + stroke: "#000000", + fill: "transparent", + strokeWidth: 2 + } + } + }; + + shapeOptions = { + circle: { + params: { + x: 0, + y: 0 + } + } + }; + + if (shapeOptions[shapeType]) { + options.shape = merge(options.shape, shapeOptions[shapeType]); + } + + return options; +} + +function isArray(obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; +} + +function isNumber(n) { + return typeof n === 'number'; +} + +function defined(obj) { + return obj !== UNDEFINED && obj !== null; +} + +function translatePath(d, xAxis, yAxis, xOffset, yOffset) { + var len = d.length, + i = 0; + + while (i < len) { + if (typeof d[i] === 'number' && typeof d[i + 1] === 'number') { + d[i] = xAxis.toPixels(d[i]) - xOffset; + d[i + 1] = yAxis.toPixels(d[i + 1]) - yOffset; + i += 2; + } else { + i += 1; + } + } + + return d; +} + + +// Define annotation prototype +var Annotation = function () { + this.init.apply(this, arguments); +}; +Annotation.prototype = { + /* + * Initialize the annotation + */ + init: function (chart, options) { + var shapeType = options.shape && options.shape.type; + + this.chart = chart; + this.options = merge({}, defaultOptions(shapeType), options); + }, + + /* + * Render the annotation + */ + render: function (redraw) { + var annotation = this, + chart = this.chart, + renderer = annotation.chart.renderer, + group = annotation.group, + title = annotation.title, + shape = annotation.shape, + options = annotation.options, + titleOptions = options.title, + shapeOptions = options.shape; + + if (!group) { + group = annotation.group = renderer.g(); + } + + + if (!shape && shapeOptions && inArray(shapeOptions.type, ALLOWED_SHAPES) !== -1) { + shape = annotation.shape = renderer[options.shape.type](shapeOptions.params); + shape.add(group); + } + + if (!title && titleOptions) { + title = annotation.title = renderer.label(titleOptions); + title.add(group); + } + + group.add(chart.annotations.group); + + // link annotations to point or series + annotation.linkObjects(); + + if (redraw !== false) { + annotation.redraw(); + } + }, + + /* + * Redraw the annotation title or shape after options update + */ + redraw: function () { + var options = this.options, + chart = this.chart, + group = this.group, + title = this.title, + shape = this.shape, + linkedTo = this.linkedObject, + xAxis = chart.xAxis[options.xAxis], + yAxis = chart.yAxis[options.yAxis], + width = options.width, + height = options.height, + anchorY = ALIGN_FACTOR[options.anchorY], + anchorX = ALIGN_FACTOR[options.anchorX], + resetBBox = false, + shapeParams, + linkType, + series, + param, + bbox, + x, + y; + + if (linkedTo) { + linkType = (linkedTo instanceof Highcharts.Point) ? 'point' : + (linkedTo instanceof Highcharts.Series) ? 'series' : null; + + if (linkType === 'point') { + options.xValue = linkedTo.x; + options.yValue = linkedTo.y; + series = linkedTo.series; + } else if (linkType === 'series') { + series = linkedTo; + } + + if (group.visibility !== series.group.visibility) { + group.attr({ + visibility: series.group.visibility + }); + } + } + + + // Based on given options find annotation pixel position + x = (defined(options.xValue) ? xAxis.toPixels(options.xValue + xAxis.minPointOffset) - xAxis.minPixelPadding : options.x); + y = defined(options.yValue) ? yAxis.toPixels(options.yValue) : options.y; + + if (isNaN(x) || isNaN(y) || !isNumber(x) || !isNumber(y)) { + return; + } + + + if (title) { + title.attr(options.title); + title.css(options.title.style); + resetBBox = true; + } + + if (shape) { + shapeParams = extend({}, options.shape.params); + + if (options.units === 'values') { + for (param in shapeParams) { + if (inArray(param, ['width', 'x']) > -1) { + shapeParams[param] = xAxis.translate(shapeParams[param]); + } else if (inArray(param, ['height', 'y']) > -1) { + shapeParams[param] = yAxis.translate(shapeParams[param]); + } + } + + if (shapeParams.width) { + shapeParams.width -= xAxis.toPixels(0) - xAxis.left; + } + + if (shapeParams.x) { + shapeParams.x += xAxis.minPixelPadding; + } + + if (options.shape.type === 'path') { + translatePath(shapeParams.d, xAxis, yAxis, x, y); + } + } + + // move the center of the circle to shape x/y + if (options.shape.type === 'circle') { + shapeParams.x += shapeParams.r; + shapeParams.y += shapeParams.r; + } + + resetBBox = true; + shape.attr(shapeParams); + } + + group.bBox = null; + + // If annotation width or height is not defined in options use bounding box size + if (!isNumber(width)) { + bbox = group.getBBox(); + width = bbox.width; + } + + if (!isNumber(height)) { + // get bbox only if it wasn't set before + if (!bbox) { + bbox = group.getBBox(); + } + + height = bbox.height; + } + + // Calculate anchor point + if (!isNumber(anchorX)) { + anchorX = ALIGN_FACTOR.center; + } + + if (!isNumber(anchorY)) { + anchorY = ALIGN_FACTOR.center; + } + + // Translate group according to its dimension and anchor point + x = x - width * anchorX; + y = y - height * anchorY; + + if (chart.animation && defined(group.translateX) && defined(group.translateY)) { + group.animate({ + translateX: x, + translateY: y + }); + } else { + group.translate(x, y); + } + }, + + /* + * Destroy the annotation + */ + destroy: function () { + var annotation = this, + chart = this.chart, + allItems = chart.annotations.allItems, + index = allItems.indexOf(annotation); + + if (index > -1) { + allItems.splice(index, 1); + } + + each(['title', 'shape', 'group'], function (element) { + if (annotation[element]) { + annotation[element].destroy(); + annotation[element] = null; + } + }); + + annotation.group = annotation.title = annotation.shape = annotation.chart = annotation.options = null; + }, + + /* + * Update the annotation with a given options + */ + update: function (options, redraw) { + extend(this.options, options); + + // update link to point or series + this.linkObjects(); + + this.render(redraw); + }, + + linkObjects: function () { + var annotation = this, + chart = annotation.chart, + linkedTo = annotation.linkedObject, + linkedId = linkedTo && (linkedTo.id || linkedTo.options.id), + options = annotation.options, + id = options.linkedTo; + + if (!defined(id)) { + annotation.linkedObject = null; + } else if (!defined(linkedTo) || id !== linkedId) { + annotation.linkedObject = chart.get(id); + } + } +}; + + +// Add annotations methods to chart prototype +extend(Chart.prototype, { + annotations: { + /* + * Unified method for adding annotations to the chart + */ + add: function (options, redraw) { + var annotations = this.allItems, + chart = this.chart, + item, + len; + + if (!isArray(options)) { + options = [options]; + } + + len = options.length; + + while (len--) { + item = new Annotation(chart, options[len]); + annotations.push(item); + item.render(redraw); + } + }, + + /** + * Redraw all annotations, method used in chart events + */ + redraw: function () { + each(this.allItems, function (annotation) { + annotation.redraw(); + }); + } + } +}); + + +// Initialize on chart load +Chart.prototype.callbacks.push(function (chart) { + var options = chart.options.annotations, + group; + + group = chart.renderer.g("annotations"); + group.attr({ + zIndex: 7 + }); + group.add(); + + // initialize empty array for annotations + chart.annotations.allItems = []; + + // link chart object to annotations + chart.annotations.chart = chart; + + // link annotations group element to the chart + chart.annotations.group = group; + + if (isArray(options) && options.length > 0) { + chart.annotations.add(chart.options.annotations); + } + + // update annotations after chart redraw + Highcharts.addEvent(chart, 'redraw', function () { + chart.annotations.redraw(); + }); +}); +}(Highcharts, HighchartsAdapter)); diff --git a/public/static/libs/ueditor/third-party/highcharts/modules/canvas-tools.js b/public/static/libs/ueditor/third-party/highcharts/modules/canvas-tools.js new file mode 100644 index 0000000..28c3893 --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/modules/canvas-tools.js @@ -0,0 +1,133 @@ +/* + A class to parse color values + @author Stoyan Stefanov + @link http://www.phpied.com/rgb-color-parser-in-javascript/ + Use it if you like it + + canvg.js - Javascript SVG parser and renderer on Canvas + MIT Licensed + Gabe Lerner (gabelerner@gmail.com) + http://code.google.com/p/canvg/ + + Requires: rgbcolor.js - http://www.phpied.com/rgb-color-parser-in-javascript/ + + Highcharts JS v3.0.6 (2013-10-04) + CanVGRenderer Extension module + + (c) 2011-2012 Torstein Hønsi, Erik Olsson + + License: www.highcharts.com/license +*/ +function RGBColor(m){this.ok=!1;m.charAt(0)=="#"&&(m=m.substr(1,6));var m=m.replace(/ /g,""),m=m.toLowerCase(),a={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"00ffff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000000",blanchedalmond:"ffebcd",blue:"0000ff",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"00ffff",darkblue:"00008b", +darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dodgerblue:"1e90ff",feldspar:"d19275",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"ff00ff", +gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgrey:"d3d3d3",lightgreen:"90ee90",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa", +lightslateblue:"8470ff",lightslategray:"778899",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"00ff00",limegreen:"32cd32",linen:"faf0e6",magenta:"ff00ff",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370d8",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080", +oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"d87093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",red:"ff0000",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd", +slategray:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",violetred:"d02090",wheat:"f5deb3",white:"ffffff",whitesmoke:"f5f5f5",yellow:"ffff00",yellowgreen:"9acd32"},c;for(c in a)m==c&&(m=a[c]);var d=[{re:/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,example:["rgb(123, 234, 45)","rgb(255,234,245)"],process:function(b){return[parseInt(b[1]),parseInt(b[2]),parseInt(b[3])]}},{re:/^(\w{2})(\w{2})(\w{2})$/, +example:["#00ff00","336699"],process:function(b){return[parseInt(b[1],16),parseInt(b[2],16),parseInt(b[3],16)]}},{re:/^(\w{1})(\w{1})(\w{1})$/,example:["#fb0","f0f"],process:function(b){return[parseInt(b[1]+b[1],16),parseInt(b[2]+b[2],16),parseInt(b[3]+b[3],16)]}}];for(c=0;c255?255:this.r;this.g=this.g<0||isNaN(this.g)?0: +this.g>255?255:this.g;this.b=this.b<0||isNaN(this.b)?0:this.b>255?255:this.b;this.toRGB=function(){return"rgb("+this.r+", "+this.g+", "+this.b+")"};this.toHex=function(){var b=this.r.toString(16),a=this.g.toString(16),d=this.b.toString(16);b.length==1&&(b="0"+b);a.length==1&&(a="0"+a);d.length==1&&(d="0"+d);return"#"+b+a+d};this.getHelpXML=function(){for(var b=[],k=0;k "+o.toRGB()+" -> "+o.toHex());l.appendChild(n);l.appendChild(q);c.appendChild(l)}catch(p){}return c}} +if(!window.console)window.console={},window.console.log=function(){},window.console.dir=function(){};if(!Array.prototype.indexOf)Array.prototype.indexOf=function(m){for(var a=0;a]*>/,""),d=new ActiveXObject("Microsoft.XMLDOM");d.async="false";d.loadXML(a);return d}};a.Property=function(c,d){this.name=c;this.value=d;this.hasValue=function(){return this.value!=null&&this.value!==""};this.numValue=function(){if(!this.hasValue())return 0;var b=parseFloat(this.value);(this.value+"").match(/%$/)&& +(b/=100);return b};this.valueOrDefault=function(b){return this.hasValue()?this.value:b};this.numValueOrDefault=function(b){return this.hasValue()?this.numValue():b};var b=this;this.Color={addOpacity:function(d){var c=b.value;if(d!=null&&d!=""){var f=new RGBColor(b.value);f.ok&&(c="rgba("+f.r+", "+f.g+", "+f.b+", "+d+")")}return new a.Property(b.name,c)}};this.Definition={getDefinition:function(){var d=b.value.replace(/^(url\()?#([^\)]+)\)?$/,"$2");return a.Definitions[d]},isUrl:function(){return b.value.indexOf("url(")== +0},getFillStyle:function(b){var d=this.getDefinition();return d!=null&&d.createGradient?d.createGradient(a.ctx,b):d!=null&&d.createPattern?d.createPattern(a.ctx,b):null}};this.Length={DPI:function(){return 96},EM:function(b){var d=12,c=new a.Property("fontSize",a.Font.Parse(a.ctx.font).fontSize);c.hasValue()&&(d=c.Length.toPixels(b));return d},toPixels:function(d){if(!b.hasValue())return 0;var c=b.value+"";return c.match(/em$/)?b.numValue()*this.EM(d):c.match(/ex$/)?b.numValue()*this.EM(d)/2:c.match(/px$/)? +b.numValue():c.match(/pt$/)?b.numValue()*1.25:c.match(/pc$/)?b.numValue()*15:c.match(/cm$/)?b.numValue()*this.DPI(d)/2.54:c.match(/mm$/)?b.numValue()*this.DPI(d)/25.4:c.match(/in$/)?b.numValue()*this.DPI(d):c.match(/%$/)?b.numValue()*a.ViewPort.ComputeSize(d):b.numValue()}};this.Time={toMilliseconds:function(){if(!b.hasValue())return 0;var a=b.value+"";if(a.match(/s$/))return b.numValue()*1E3;a.match(/ms$/);return b.numValue()}};this.Angle={toRadians:function(){if(!b.hasValue())return 0;var a=b.value+ +"";return a.match(/deg$/)?b.numValue()*(Math.PI/180):a.match(/grad$/)?b.numValue()*(Math.PI/200):a.match(/rad$/)?b.numValue():b.numValue()*(Math.PI/180)}}};a.Font=new function(){this.Styles=["normal","italic","oblique","inherit"];this.Variants=["normal","small-caps","inherit"];this.Weights="normal,bold,bolder,lighter,100,200,300,400,500,600,700,800,900,inherit".split(",");this.CreateFont=function(d,b,c,e,f,g){g=g!=null?this.Parse(g):this.CreateFont("","","","","",a.ctx.font);return{fontFamily:f|| +g.fontFamily,fontSize:e||g.fontSize,fontStyle:d||g.fontStyle,fontWeight:c||g.fontWeight,fontVariant:b||g.fontVariant,toString:function(){return[this.fontStyle,this.fontVariant,this.fontWeight,this.fontSize,this.fontFamily].join(" ")}}};var c=this;this.Parse=function(d){for(var b={},d=a.trim(a.compressSpaces(d||"")).split(" "),k=!1,e=!1,f=!1,g=!1,j="",h=0;hthis.x2)this.x2=b}if(a!=null){if(isNaN(this.y1)||isNaN(this.y2))this.y2=this.y1=a;if(athis.y2)this.y2=a}};this.addX=function(b){this.addPoint(b,null)};this.addY=function(b){this.addPoint(null,b)};this.addBoundingBox=function(b){this.addPoint(b.x1,b.y1);this.addPoint(b.x2,b.y2)};this.addQuadraticCurve=function(b,a,d,c,k,l){d=b+2/3*(d-b);c=a+2/3*(c- +a);this.addBezierCurve(b,a,d,d+1/3*(k-b),c,c+1/3*(l-a),k,l)};this.addBezierCurve=function(b,a,d,c,k,l,o,n){var q=[b,a],p=[d,c],t=[k,l],m=[o,n];this.addPoint(q[0],q[1]);this.addPoint(m[0],m[1]);for(i=0;i<=1;i++)b=function(b){return Math.pow(1-b,3)*q[i]+3*Math.pow(1-b,2)*b*p[i]+3*(1-b)*Math.pow(b,2)*t[i]+Math.pow(b,3)*m[i]},a=6*q[i]-12*p[i]+6*t[i],d=-3*q[i]+9*p[i]-9*t[i]+3*m[i],c=3*p[i]-3*q[i],d==0?a!=0&&(a=-c/a,0=this.tokens.length-1};this.isCommandOrEnd=function(){return this.isEnd()? +!0:this.tokens[this.i+1].match(/^[A-Za-z]$/)!=null};this.isRelativeCommand=function(){return this.command==this.command.toLowerCase()};this.getToken=function(){this.i+=1;return this.tokens[this.i]};this.getScalar=function(){return parseFloat(this.getToken())};this.nextCommand=function(){this.previousCommand=this.command;this.command=this.getToken()};this.getPoint=function(){return this.makeAbsolute(new a.Point(this.getScalar(),this.getScalar()))};this.getAsControlPoint=function(){var b=this.getPoint(); +return this.control=b};this.getAsCurrentPoint=function(){var b=this.getPoint();return this.current=b};this.getReflectedControlPoint=function(){return this.previousCommand.toLowerCase()!="c"&&this.previousCommand.toLowerCase()!="s"?this.current:new a.Point(2*this.current.x-this.control.x,2*this.current.y-this.control.y)};this.makeAbsolute=function(b){if(this.isRelativeCommand())b.x=this.current.x+b.x,b.y=this.current.y+b.y;return b};this.addMarker=function(b,a,d){d!=null&&this.angles.length>0&&this.angles[this.angles.length- +1]==null&&(this.angles[this.angles.length-1]=this.points[this.points.length-1].angleTo(d));this.addMarkerAngle(b,a==null?null:a.angleTo(b))};this.addMarkerAngle=function(b,a){this.points.push(b);this.angles.push(a)};this.getMarkerPoints=function(){return this.points};this.getMarkerAngles=function(){for(var b=0;b1&&(h*=Math.sqrt(q),l*=Math.sqrt(q));o=(o==j?-1:1)*Math.sqrt((Math.pow(h,2)*Math.pow(l,2)-Math.pow(h,2)*Math.pow(n.y,2)-Math.pow(l,2)*Math.pow(n.x,2))/(Math.pow(h,2)*Math.pow(n.y,2)+Math.pow(l,2)*Math.pow(n.x,2)));isNaN(o)&&(o=0);var p=new a.Point(o*h*n.y/l,o*-l*n.x/h),g=new a.Point((g.x+e.x)/2+Math.cos(f)* +p.x-Math.sin(f)*p.y,(g.y+e.y)/2+Math.sin(f)*p.x+Math.cos(f)*p.y),m=function(b,a){return(b[0]*a[0]+b[1]*a[1])/(Math.sqrt(Math.pow(b[0],2)+Math.pow(b[1],2))*Math.sqrt(Math.pow(a[0],2)+Math.pow(a[1],2)))},s=function(b,a){return(b[0]*a[1]=1&&(n=0);j==0&&n>0&&(n-=2*Math.PI);j==1&&n<0&&(n+=2*Math.PI);q=new a.Point(g.x-h*Math.cos((o+n)/ +2),g.y-l*Math.sin((o+n)/2));b.addMarkerAngle(q,(o+n)/2+(j==0?1:-1)*Math.PI/2);b.addMarkerAngle(e,n+(j==0?1:-1)*Math.PI/2);c.addPoint(e.x,e.y);d!=null&&(m=h>l?h:l,e=h>l?1:h/l,h=h>l?l/h:1,d.translate(g.x,g.y),d.rotate(f),d.scale(e,h),d.arc(0,0,m,o,o+n,1-j),d.scale(1/e,1/h),d.rotate(-f),d.translate(-g.x,-g.y))}break;case "Z":d!=null&&d.closePath(),b.current=b.start}return c};this.getMarkers=function(){for(var a=this.PathParser.getMarkerPoints(),b=this.PathParser.getMarkerAngles(),c=[],e=0;ethis.maxDuration)if(this.attribute("repeatCount").value=="indefinite")this.duration=0;else return this.attribute("fill").valueOrDefault("remove")=="remove"&&!this.removed?(this.removed=!0,this.getProperty().value=this.initialValue,!0):!1;this.duration+=a;a=!1;if(this.begin0&&b[c-1]!=" "&&c0&&b[c-1]!=" "&&(c==b.length-1||b[c+1]==" "))g="initial";typeof a.glyphs[e]!="undefined"&&(f=a.glyphs[e][g],f==null&&a.glyphs[e].type=="glyph"&&(f=a.glyphs[e]))}else f=a.glyphs[e];if(f==null)f=a.missingGlyph;return f};this.renderChildren=function(c){var b=this.parent.style("font-family").Definition.getDefinition();if(b!=null){var k=this.parent.style("font-size").numValueOrDefault(a.Font.Parse(a.ctx.font).fontSize), +e=this.parent.style("font-style").valueOrDefault(a.Font.Parse(a.ctx.font).fontStyle),f=this.getText();b.isRTL&&(f=f.split("").reverse().join(""));for(var g=a.ToNumberArray(this.parent.attribute("dx").value),j=0;j0?c.childNodes[0].nodeValue:c.text;this.getText=function(){return this.text}};a.Element.tspan.prototype=new a.Element.TextElementBase;a.Element.tref=function(c){this.base=a.Element.TextElementBase;this.base(c);this.getText=function(){var a=this.attribute("xlink:href").Definition.getDefinition();if(a!=null)return a.children[0].getText()}};a.Element.tref.prototype=new a.Element.TextElementBase; +a.Element.a=function(c){this.base=a.Element.TextElementBase;this.base(c);this.hasText=!0;for(var d=0;d1?c.childNodes[1].nodeValue: +""),c=c.replace(/(\/\*([^*]|[\r\n]|(\*+([^*\/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm,""),c=a.compressSpaces(c),c=c.split("}"),d=0;d0){l=g[j].indexOf("url");h=g[j].indexOf(")",l);l=g[j].substr(l+5,h-l-6);l=a.parseXml(a.ajax(l)).getElementsByTagName("font");for(h=0;h + * @link http://www.phpied.com/rgb-color-parser-in-javascript/ + * Use it if you like it + * + */ +function RGBColor(color_string) +{ + this.ok = false; + + // strip any leading # + if (color_string.charAt(0) == '#') { // remove # if any + color_string = color_string.substr(1,6); + } + + color_string = color_string.replace(/ /g,''); + color_string = color_string.toLowerCase(); + + // before getting into regexps, try simple matches + // and overwrite the input + var simple_colors = { + aliceblue: 'f0f8ff', + antiquewhite: 'faebd7', + aqua: '00ffff', + aquamarine: '7fffd4', + azure: 'f0ffff', + beige: 'f5f5dc', + bisque: 'ffe4c4', + black: '000000', + blanchedalmond: 'ffebcd', + blue: '0000ff', + blueviolet: '8a2be2', + brown: 'a52a2a', + burlywood: 'deb887', + cadetblue: '5f9ea0', + chartreuse: '7fff00', + chocolate: 'd2691e', + coral: 'ff7f50', + cornflowerblue: '6495ed', + cornsilk: 'fff8dc', + crimson: 'dc143c', + cyan: '00ffff', + darkblue: '00008b', + darkcyan: '008b8b', + darkgoldenrod: 'b8860b', + darkgray: 'a9a9a9', + darkgreen: '006400', + darkkhaki: 'bdb76b', + darkmagenta: '8b008b', + darkolivegreen: '556b2f', + darkorange: 'ff8c00', + darkorchid: '9932cc', + darkred: '8b0000', + darksalmon: 'e9967a', + darkseagreen: '8fbc8f', + darkslateblue: '483d8b', + darkslategray: '2f4f4f', + darkturquoise: '00ced1', + darkviolet: '9400d3', + deeppink: 'ff1493', + deepskyblue: '00bfff', + dimgray: '696969', + dodgerblue: '1e90ff', + feldspar: 'd19275', + firebrick: 'b22222', + floralwhite: 'fffaf0', + forestgreen: '228b22', + fuchsia: 'ff00ff', + gainsboro: 'dcdcdc', + ghostwhite: 'f8f8ff', + gold: 'ffd700', + goldenrod: 'daa520', + gray: '808080', + green: '008000', + greenyellow: 'adff2f', + honeydew: 'f0fff0', + hotpink: 'ff69b4', + indianred : 'cd5c5c', + indigo : '4b0082', + ivory: 'fffff0', + khaki: 'f0e68c', + lavender: 'e6e6fa', + lavenderblush: 'fff0f5', + lawngreen: '7cfc00', + lemonchiffon: 'fffacd', + lightblue: 'add8e6', + lightcoral: 'f08080', + lightcyan: 'e0ffff', + lightgoldenrodyellow: 'fafad2', + lightgrey: 'd3d3d3', + lightgreen: '90ee90', + lightpink: 'ffb6c1', + lightsalmon: 'ffa07a', + lightseagreen: '20b2aa', + lightskyblue: '87cefa', + lightslateblue: '8470ff', + lightslategray: '778899', + lightsteelblue: 'b0c4de', + lightyellow: 'ffffe0', + lime: '00ff00', + limegreen: '32cd32', + linen: 'faf0e6', + magenta: 'ff00ff', + maroon: '800000', + mediumaquamarine: '66cdaa', + mediumblue: '0000cd', + mediumorchid: 'ba55d3', + mediumpurple: '9370d8', + mediumseagreen: '3cb371', + mediumslateblue: '7b68ee', + mediumspringgreen: '00fa9a', + mediumturquoise: '48d1cc', + mediumvioletred: 'c71585', + midnightblue: '191970', + mintcream: 'f5fffa', + mistyrose: 'ffe4e1', + moccasin: 'ffe4b5', + navajowhite: 'ffdead', + navy: '000080', + oldlace: 'fdf5e6', + olive: '808000', + olivedrab: '6b8e23', + orange: 'ffa500', + orangered: 'ff4500', + orchid: 'da70d6', + palegoldenrod: 'eee8aa', + palegreen: '98fb98', + paleturquoise: 'afeeee', + palevioletred: 'd87093', + papayawhip: 'ffefd5', + peachpuff: 'ffdab9', + peru: 'cd853f', + pink: 'ffc0cb', + plum: 'dda0dd', + powderblue: 'b0e0e6', + purple: '800080', + red: 'ff0000', + rosybrown: 'bc8f8f', + royalblue: '4169e1', + saddlebrown: '8b4513', + salmon: 'fa8072', + sandybrown: 'f4a460', + seagreen: '2e8b57', + seashell: 'fff5ee', + sienna: 'a0522d', + silver: 'c0c0c0', + skyblue: '87ceeb', + slateblue: '6a5acd', + slategray: '708090', + snow: 'fffafa', + springgreen: '00ff7f', + steelblue: '4682b4', + tan: 'd2b48c', + teal: '008080', + thistle: 'd8bfd8', + tomato: 'ff6347', + turquoise: '40e0d0', + violet: 'ee82ee', + violetred: 'd02090', + wheat: 'f5deb3', + white: 'ffffff', + whitesmoke: 'f5f5f5', + yellow: 'ffff00', + yellowgreen: '9acd32' + }; + for (var key in simple_colors) { + if (color_string == key) { + color_string = simple_colors[key]; + } + } + // emd of simple type-in colors + + // array of color definition objects + var color_defs = [ + { + re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, + example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'], + process: function (bits){ + return [ + parseInt(bits[1]), + parseInt(bits[2]), + parseInt(bits[3]) + ]; + } + }, + { + re: /^(\w{2})(\w{2})(\w{2})$/, + example: ['#00ff00', '336699'], + process: function (bits){ + return [ + parseInt(bits[1], 16), + parseInt(bits[2], 16), + parseInt(bits[3], 16) + ]; + } + }, + { + re: /^(\w{1})(\w{1})(\w{1})$/, + example: ['#fb0', 'f0f'], + process: function (bits){ + return [ + parseInt(bits[1] + bits[1], 16), + parseInt(bits[2] + bits[2], 16), + parseInt(bits[3] + bits[3], 16) + ]; + } + } + ]; + + // search through the definitions to find a match + for (var i = 0; i < color_defs.length; i++) { + var re = color_defs[i].re; + var processor = color_defs[i].process; + var bits = re.exec(color_string); + if (bits) { + channels = processor(bits); + this.r = channels[0]; + this.g = channels[1]; + this.b = channels[2]; + this.ok = true; + } + + } + + // validate/cleanup values + this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r); + this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g); + this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b); + + // some getters + this.toRGB = function () { + return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; + } + this.toHex = function () { + var r = this.r.toString(16); + var g = this.g.toString(16); + var b = this.b.toString(16); + if (r.length == 1) r = '0' + r; + if (g.length == 1) g = '0' + g; + if (b.length == 1) b = '0' + b; + return '#' + r + g + b; + } + + // help + this.getHelpXML = function () { + + var examples = new Array(); + // add regexps + for (var i = 0; i < color_defs.length; i++) { + var example = color_defs[i].example; + for (var j = 0; j < example.length; j++) { + examples[examples.length] = example[j]; + } + } + // add type-in colors + for (var sc in simple_colors) { + examples[examples.length] = sc; + } + + var xml = document.createElement('ul'); + xml.setAttribute('id', 'rgbcolor-examples'); + for (var i = 0; i < examples.length; i++) { + try { + var list_item = document.createElement('li'); + var list_color = new RGBColor(examples[i]); + var example_div = document.createElement('div'); + example_div.style.cssText = + 'margin: 3px; ' + + 'border: 1px solid black; ' + + 'background:' + list_color.toHex() + '; ' + + 'color:' + list_color.toHex() + ; + example_div.appendChild(document.createTextNode('test')); + var list_item_value = document.createTextNode( + ' ' + examples[i] + ' -> ' + list_color.toRGB() + ' -> ' + list_color.toHex() + ); + list_item.appendChild(example_div); + list_item.appendChild(list_item_value); + xml.appendChild(list_item); + + } catch(e){} + } + return xml; + + } + +} + +/** + * @license canvg.js - Javascript SVG parser and renderer on Canvas + * MIT Licensed + * Gabe Lerner (gabelerner@gmail.com) + * http://code.google.com/p/canvg/ + * + * Requires: rgbcolor.js - http://www.phpied.com/rgb-color-parser-in-javascript/ + * + */ +if(!window.console) { + window.console = {}; + window.console.log = function(str) {}; + window.console.dir = function(str) {}; +} + +if(!Array.prototype.indexOf){ + Array.prototype.indexOf = function(obj){ + for(var i=0; i ignore mouse events + // ignoreAnimation: true => ignore animations + // ignoreDimensions: true => does not try to resize canvas + // ignoreClear: true => does not clear canvas + // offsetX: int => draws at a x offset + // offsetY: int => draws at a y offset + // scaleWidth: int => scales horizontally to width + // scaleHeight: int => scales vertically to height + // renderCallback: function => will call the function after the first render is completed + // forceRedraw: function => will call the function on every frame, if it returns true, will redraw + this.canvg = function (target, s, opts) { + // no parameters + if (target == null && s == null && opts == null) { + var svgTags = document.getElementsByTagName('svg'); + for (var i=0; i]*>/, ''); + var xmlDoc = new ActiveXObject('Microsoft.XMLDOM'); + xmlDoc.async = 'false'; + xmlDoc.loadXML(xml); + return xmlDoc; + } + } + + svg.Property = function(name, value) { + this.name = name; + this.value = value; + + this.hasValue = function() { + return (this.value != null && this.value !== ''); + } + + // return the numerical value of the property + this.numValue = function() { + if (!this.hasValue()) return 0; + + var n = parseFloat(this.value); + if ((this.value + '').match(/%$/)) { + n = n / 100.0; + } + return n; + } + + this.valueOrDefault = function(def) { + if (this.hasValue()) return this.value; + return def; + } + + this.numValueOrDefault = function(def) { + if (this.hasValue()) return this.numValue(); + return def; + } + + /* EXTENSIONS */ + var that = this; + + // color extensions + this.Color = { + // augment the current color value with the opacity + addOpacity: function(opacity) { + var newValue = that.value; + if (opacity != null && opacity != '') { + var color = new RGBColor(that.value); + if (color.ok) { + newValue = 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + opacity + ')'; + } + } + return new svg.Property(that.name, newValue); + } + } + + // definition extensions + this.Definition = { + // get the definition from the definitions table + getDefinition: function() { + var name = that.value.replace(/^(url\()?#([^\)]+)\)?$/, '$2'); + return svg.Definitions[name]; + }, + + isUrl: function() { + return that.value.indexOf('url(') == 0 + }, + + getFillStyle: function(e) { + var def = this.getDefinition(); + + // gradient + if (def != null && def.createGradient) { + return def.createGradient(svg.ctx, e); + } + + // pattern + if (def != null && def.createPattern) { + return def.createPattern(svg.ctx, e); + } + + return null; + } + } + + // length extensions + this.Length = { + DPI: function(viewPort) { + return 96.0; // TODO: compute? + }, + + EM: function(viewPort) { + var em = 12; + + var fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); + if (fontSize.hasValue()) em = fontSize.Length.toPixels(viewPort); + + return em; + }, + + // get the length as pixels + toPixels: function(viewPort) { + if (!that.hasValue()) return 0; + var s = that.value+''; + if (s.match(/em$/)) return that.numValue() * this.EM(viewPort); + if (s.match(/ex$/)) return that.numValue() * this.EM(viewPort) / 2.0; + if (s.match(/px$/)) return that.numValue(); + if (s.match(/pt$/)) return that.numValue() * 1.25; + if (s.match(/pc$/)) return that.numValue() * 15; + if (s.match(/cm$/)) return that.numValue() * this.DPI(viewPort) / 2.54; + if (s.match(/mm$/)) return that.numValue() * this.DPI(viewPort) / 25.4; + if (s.match(/in$/)) return that.numValue() * this.DPI(viewPort); + if (s.match(/%$/)) return that.numValue() * svg.ViewPort.ComputeSize(viewPort); + return that.numValue(); + } + } + + // time extensions + this.Time = { + // get the time as milliseconds + toMilliseconds: function() { + if (!that.hasValue()) return 0; + var s = that.value+''; + if (s.match(/s$/)) return that.numValue() * 1000; + if (s.match(/ms$/)) return that.numValue(); + return that.numValue(); + } + } + + // angle extensions + this.Angle = { + // get the angle as radians + toRadians: function() { + if (!that.hasValue()) return 0; + var s = that.value+''; + if (s.match(/deg$/)) return that.numValue() * (Math.PI / 180.0); + if (s.match(/grad$/)) return that.numValue() * (Math.PI / 200.0); + if (s.match(/rad$/)) return that.numValue(); + return that.numValue() * (Math.PI / 180.0); + } + } + } + + // fonts + svg.Font = new (function() { + this.Styles = ['normal','italic','oblique','inherit']; + this.Variants = ['normal','small-caps','inherit']; + this.Weights = ['normal','bold','bolder','lighter','100','200','300','400','500','600','700','800','900','inherit']; + + this.CreateFont = function(fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) { + var f = inherit != null ? this.Parse(inherit) : this.CreateFont('', '', '', '', '', svg.ctx.font); + return { + fontFamily: fontFamily || f.fontFamily, + fontSize: fontSize || f.fontSize, + fontStyle: fontStyle || f.fontStyle, + fontWeight: fontWeight || f.fontWeight, + fontVariant: fontVariant || f.fontVariant, + toString: function () { return [this.fontStyle, this.fontVariant, this.fontWeight, this.fontSize, this.fontFamily].join(' ') } + } + } + + var that = this; + this.Parse = function(s) { + var f = {}; + var d = svg.trim(svg.compressSpaces(s || '')).split(' '); + var set = { fontSize: false, fontStyle: false, fontWeight: false, fontVariant: false } + var ff = ''; + for (var i=0; i this.x2) this.x2 = x; + } + + if (y != null) { + if (isNaN(this.y1) || isNaN(this.y2)) { + this.y1 = y; + this.y2 = y; + } + if (y < this.y1) this.y1 = y; + if (y > this.y2) this.y2 = y; + } + } + this.addX = function(x) { this.addPoint(x, null); } + this.addY = function(y) { this.addPoint(null, y); } + + this.addBoundingBox = function(bb) { + this.addPoint(bb.x1, bb.y1); + this.addPoint(bb.x2, bb.y2); + } + + this.addQuadraticCurve = function(p0x, p0y, p1x, p1y, p2x, p2y) { + var cp1x = p0x + 2/3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0) + var cp1y = p0y + 2/3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0) + var cp2x = cp1x + 1/3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0) + var cp2y = cp1y + 1/3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0) + this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y); + } + + this.addBezierCurve = function(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) { + // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html + var p0 = [p0x, p0y], p1 = [p1x, p1y], p2 = [p2x, p2y], p3 = [p3x, p3y]; + this.addPoint(p0[0], p0[1]); + this.addPoint(p3[0], p3[1]); + + for (i=0; i<=1; i++) { + var f = function(t) { + return Math.pow(1-t, 3) * p0[i] + + 3 * Math.pow(1-t, 2) * t * p1[i] + + 3 * (1-t) * Math.pow(t, 2) * p2[i] + + Math.pow(t, 3) * p3[i]; + } + + var b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]; + var a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]; + var c = 3 * p1[i] - 3 * p0[i]; + + if (a == 0) { + if (b == 0) continue; + var t = -c / b; + if (0 < t && t < 1) { + if (i == 0) this.addX(f(t)); + if (i == 1) this.addY(f(t)); + } + continue; + } + + var b2ac = Math.pow(b, 2) - 4 * c * a; + if (b2ac < 0) continue; + var t1 = (-b + Math.sqrt(b2ac)) / (2 * a); + if (0 < t1 && t1 < 1) { + if (i == 0) this.addX(f(t1)); + if (i == 1) this.addY(f(t1)); + } + var t2 = (-b - Math.sqrt(b2ac)) / (2 * a); + if (0 < t2 && t2 < 1) { + if (i == 0) this.addX(f(t2)); + if (i == 1) this.addY(f(t2)); + } + } + } + + this.isPointInBox = function(x, y) { + return (this.x1 <= x && x <= this.x2 && this.y1 <= y && y <= this.y2); + } + + this.addPoint(x1, y1); + this.addPoint(x2, y2); + } + + // transforms + svg.Transform = function(v) { + var that = this; + this.Type = {} + + // translate + this.Type.translate = function(s) { + this.p = svg.CreatePoint(s); + this.apply = function(ctx) { + ctx.translate(this.p.x || 0.0, this.p.y || 0.0); + } + this.applyToPoint = function(p) { + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + } + } + + // rotate + this.Type.rotate = function(s) { + var a = svg.ToNumberArray(s); + this.angle = new svg.Property('angle', a[0]); + this.cx = a[1] || 0; + this.cy = a[2] || 0; + this.apply = function(ctx) { + ctx.translate(this.cx, this.cy); + ctx.rotate(this.angle.Angle.toRadians()); + ctx.translate(-this.cx, -this.cy); + } + this.applyToPoint = function(p) { + var a = this.angle.Angle.toRadians(); + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + p.applyTransform([Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 0, 0]); + p.applyTransform([1, 0, 0, 1, -this.p.x || 0.0, -this.p.y || 0.0]); + } + } + + this.Type.scale = function(s) { + this.p = svg.CreatePoint(s); + this.apply = function(ctx) { + ctx.scale(this.p.x || 1.0, this.p.y || this.p.x || 1.0); + } + this.applyToPoint = function(p) { + p.applyTransform([this.p.x || 0.0, 0, 0, this.p.y || 0.0, 0, 0]); + } + } + + this.Type.matrix = function(s) { + this.m = svg.ToNumberArray(s); + this.apply = function(ctx) { + ctx.transform(this.m[0], this.m[1], this.m[2], this.m[3], this.m[4], this.m[5]); + } + this.applyToPoint = function(p) { + p.applyTransform(this.m); + } + } + + this.Type.SkewBase = function(s) { + this.base = that.Type.matrix; + this.base(s); + this.angle = new svg.Property('angle', s); + } + this.Type.SkewBase.prototype = new this.Type.matrix; + + this.Type.skewX = function(s) { + this.base = that.Type.SkewBase; + this.base(s); + this.m = [1, 0, Math.tan(this.angle.Angle.toRadians()), 1, 0, 0]; + } + this.Type.skewX.prototype = new this.Type.SkewBase; + + this.Type.skewY = function(s) { + this.base = that.Type.SkewBase; + this.base(s); + this.m = [1, Math.tan(this.angle.Angle.toRadians()), 0, 1, 0, 0]; + } + this.Type.skewY.prototype = new this.Type.SkewBase; + + this.transforms = []; + + this.apply = function(ctx) { + for (var i=0; i= this.tokens.length - 1; + } + + this.isCommandOrEnd = function() { + if (this.isEnd()) return true; + return this.tokens[this.i + 1].match(/^[A-Za-z]$/) != null; + } + + this.isRelativeCommand = function() { + return this.command == this.command.toLowerCase(); + } + + this.getToken = function() { + this.i = this.i + 1; + return this.tokens[this.i]; + } + + this.getScalar = function() { + return parseFloat(this.getToken()); + } + + this.nextCommand = function() { + this.previousCommand = this.command; + this.command = this.getToken(); + } + + this.getPoint = function() { + var p = new svg.Point(this.getScalar(), this.getScalar()); + return this.makeAbsolute(p); + } + + this.getAsControlPoint = function() { + var p = this.getPoint(); + this.control = p; + return p; + } + + this.getAsCurrentPoint = function() { + var p = this.getPoint(); + this.current = p; + return p; + } + + this.getReflectedControlPoint = function() { + if (this.previousCommand.toLowerCase() != 'c' && this.previousCommand.toLowerCase() != 's') { + return this.current; + } + + // reflect point + var p = new svg.Point(2 * this.current.x - this.control.x, 2 * this.current.y - this.control.y); + return p; + } + + this.makeAbsolute = function(p) { + if (this.isRelativeCommand()) { + p.x = this.current.x + p.x; + p.y = this.current.y + p.y; + } + return p; + } + + this.addMarker = function(p, from, priorTo) { + // if the last angle isn't filled in because we didn't have this point yet ... + if (priorTo != null && this.angles.length > 0 && this.angles[this.angles.length-1] == null) { + this.angles[this.angles.length-1] = this.points[this.points.length-1].angleTo(priorTo); + } + this.addMarkerAngle(p, from == null ? null : from.angleTo(p)); + } + + this.addMarkerAngle = function(p, a) { + this.points.push(p); + this.angles.push(a); + } + + this.getMarkerPoints = function() { return this.points; } + this.getMarkerAngles = function() { + for (var i=0; i 1) { + rx *= Math.sqrt(l); + ry *= Math.sqrt(l); + } + // cx', cy' + var s = (largeArcFlag == sweepFlag ? -1 : 1) * Math.sqrt( + ((Math.pow(rx,2)*Math.pow(ry,2))-(Math.pow(rx,2)*Math.pow(currp.y,2))-(Math.pow(ry,2)*Math.pow(currp.x,2))) / + (Math.pow(rx,2)*Math.pow(currp.y,2)+Math.pow(ry,2)*Math.pow(currp.x,2)) + ); + if (isNaN(s)) s = 0; + var cpp = new svg.Point(s * rx * currp.y / ry, s * -ry * currp.x / rx); + // cx, cy + var centp = new svg.Point( + (curr.x + cp.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y, + (curr.y + cp.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y + ); + // vector magnitude + var m = function(v) { return Math.sqrt(Math.pow(v[0],2) + Math.pow(v[1],2)); } + // ratio between two vectors + var r = function(u, v) { return (u[0]*v[0]+u[1]*v[1]) / (m(u)*m(v)) } + // angle between two vectors + var a = function(u, v) { return (u[0]*v[1] < u[1]*v[0] ? -1 : 1) * Math.acos(r(u,v)); } + // initial angle + var a1 = a([1,0], [(currp.x-cpp.x)/rx,(currp.y-cpp.y)/ry]); + // angle delta + var u = [(currp.x-cpp.x)/rx,(currp.y-cpp.y)/ry]; + var v = [(-currp.x-cpp.x)/rx,(-currp.y-cpp.y)/ry]; + var ad = a(u, v); + if (r(u,v) <= -1) ad = Math.PI; + if (r(u,v) >= 1) ad = 0; + + if (sweepFlag == 0 && ad > 0) ad = ad - 2 * Math.PI; + if (sweepFlag == 1 && ad < 0) ad = ad + 2 * Math.PI; + + // for markers + var halfWay = new svg.Point( + centp.x - rx * Math.cos((a1 + ad) / 2), + centp.y - ry * Math.sin((a1 + ad) / 2) + ); + pp.addMarkerAngle(halfWay, (a1 + ad) / 2 + (sweepFlag == 0 ? 1 : -1) * Math.PI / 2); + pp.addMarkerAngle(cp, ad + (sweepFlag == 0 ? 1 : -1) * Math.PI / 2); + + bb.addPoint(cp.x, cp.y); // TODO: this is too naive, make it better + if (ctx != null) { + var r = rx > ry ? rx : ry; + var sx = rx > ry ? 1 : rx / ry; + var sy = rx > ry ? ry / rx : 1; + + ctx.translate(centp.x, centp.y); + ctx.rotate(xAxisRotation); + ctx.scale(sx, sy); + ctx.arc(0, 0, r, a1, a1 + ad, 1 - sweepFlag); + ctx.scale(1/sx, 1/sy); + ctx.rotate(-xAxisRotation); + ctx.translate(-centp.x, -centp.y); + } + } + break; + case 'Z': + if (ctx != null) ctx.closePath(); + pp.current = pp.start; + } + } + + return bb; + } + + this.getMarkers = function() { + var points = this.PathParser.getMarkerPoints(); + var angles = this.PathParser.getMarkerAngles(); + + var markers = []; + for (var i=0; i this.maxDuration) { + // loop for indefinitely repeating animations + if (this.attribute('repeatCount').value == 'indefinite') { + this.duration = 0.0 + } + else if (this.attribute('fill').valueOrDefault('remove') == 'remove' && !this.removed) { + this.removed = true; + this.getProperty().value = this.initialValue; + return true; + } + else { + return false; // no updates made + } + } + this.duration = this.duration + delta; + + // if we're past the begin time + var updated = false; + if (this.begin < this.duration) { + var newValue = this.calcValue(); // tween + + if (this.attribute('type').hasValue()) { + // for transform, etc. + var type = this.attribute('type').value; + newValue = type + '(' + newValue + ')'; + } + + this.getProperty().value = newValue; + updated = true; + } + + return updated; + } + + // fraction of duration we've covered + this.progress = function() { + return ((this.duration - this.begin) / (this.maxDuration - this.begin)); + } + } + svg.Element.AnimateBase.prototype = new svg.Element.ElementBase; + + // animate element + svg.Element.animate = function(node) { + this.base = svg.Element.AnimateBase; + this.base(node); + + this.calcValue = function() { + var from = this.attribute('from').numValue(); + var to = this.attribute('to').numValue(); + + // tween value linearly + return from + (to - from) * this.progress(); + }; + } + svg.Element.animate.prototype = new svg.Element.AnimateBase; + + // animate color element + svg.Element.animateColor = function(node) { + this.base = svg.Element.AnimateBase; + this.base(node); + + this.calcValue = function() { + var from = new RGBColor(this.attribute('from').value); + var to = new RGBColor(this.attribute('to').value); + + if (from.ok && to.ok) { + // tween color linearly + var r = from.r + (to.r - from.r) * this.progress(); + var g = from.g + (to.g - from.g) * this.progress(); + var b = from.b + (to.b - from.b) * this.progress(); + return 'rgb('+parseInt(r,10)+','+parseInt(g,10)+','+parseInt(b,10)+')'; + } + return this.attribute('from').value; + }; + } + svg.Element.animateColor.prototype = new svg.Element.AnimateBase; + + // animate transform element + svg.Element.animateTransform = function(node) { + this.base = svg.Element.animate; + this.base(node); + } + svg.Element.animateTransform.prototype = new svg.Element.animate; + + // font element + svg.Element.font = function(node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.horizAdvX = this.attribute('horiz-adv-x').numValue(); + + this.isRTL = false; + this.isArabic = false; + this.fontFace = null; + this.missingGlyph = null; + this.glyphs = []; + for (var i=0; i0 && text[i-1]!=' ' && i0 && text[i-1]!=' ' && (i == text.length-1 || text[i+1]==' ')) arabicForm = 'initial'; + if (typeof(font.glyphs[c]) != 'undefined') { + glyph = font.glyphs[c][arabicForm]; + if (glyph == null && font.glyphs[c].type == 'glyph') glyph = font.glyphs[c]; + } + } + else { + glyph = font.glyphs[c]; + } + if (glyph == null) glyph = font.missingGlyph; + return glyph; + } + + this.renderChildren = function(ctx) { + var customFont = this.parent.style('font-family').Definition.getDefinition(); + if (customFont != null) { + var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + var fontStyle = this.parent.style('font-style').valueOrDefault(svg.Font.Parse(svg.ctx.font).fontStyle); + var text = this.getText(); + if (customFont.isRTL) text = text.split("").reverse().join(""); + + var dx = svg.ToNumberArray(this.parent.attribute('dx').value); + for (var i=0; i 0 ? node.childNodes[0].nodeValue : // element + node.text; + this.getText = function() { + return this.text; + } + } + svg.Element.tspan.prototype = new svg.Element.TextElementBase; + + // tref + svg.Element.tref = function(node) { + this.base = svg.Element.TextElementBase; + this.base(node); + + this.getText = function() { + var element = this.attribute('xlink:href').Definition.getDefinition(); + if (element != null) return element.children[0].getText(); + } + } + svg.Element.tref.prototype = new svg.Element.TextElementBase; + + // a element + svg.Element.a = function(node) { + this.base = svg.Element.TextElementBase; + this.base(node); + + this.hasText = true; + for (var i=0; i 1 ? node.childNodes[1].nodeValue : ''); + css = css.replace(/(\/\*([^*]|[\r\n]|(\*+([^*\/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm, ''); // remove comments + css = svg.compressSpaces(css); // replace whitespace + var cssDefs = css.split('}'); + for (var i=0; i 0) { + var urlStart = srcs[s].indexOf('url'); + var urlEnd = srcs[s].indexOf(')', urlStart); + var url = srcs[s].substr(urlStart + 5, urlEnd - urlStart - 6); + var doc = svg.parseXml(svg.ajax(url)); + var fonts = doc.getElementsByTagName('font'); + for (var f=0; f
              "; + return div.firstChild.getAttribute("href") === "#" ; +}) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + }); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert(function( div ) { + div.innerHTML = ""; + div.firstChild.setAttribute( "value", "" ); + return div.firstChild.getAttribute( "value" ) === ""; +}) ) { + addHandle( "value", function( elem, name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + }); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert(function( div ) { + return div.getAttribute("disabled") == null; +}) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return (val = elem.getAttributeNode( name )) && val.specified ? + val.value : + elem[ name ] === true ? name.toLowerCase() : null; + } + }); +} + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.pseudos; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})( window ); +// String to Object options format cache +var optionsCache = {}; + +// Convert String-formatted options into Object-formatted ones and store in cache +function createOptions( options ) { + var object = optionsCache[ options ] = {}; + jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) { + object[ flag ] = true; + }); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + ( optionsCache[ options ] || createOptions( options ) ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // First callback to fire (used internally by add and fireWith) + firingStart, + // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = !options.once && [], + // Fire callbacks + fire = function( data ) { + memory = options.memory && data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { + memory = false; // To prevent further calls using add + break; + } + } + firing = false; + if ( list ) { + if ( stack ) { + if ( stack.length ) { + fire( stack.shift() ); + } + } else if ( memory ) { + list = []; + } else { + self.disable(); + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + // First, we save the current length + var start = list.length; + (function add( args ) { + jQuery.each( args, function( _, arg ) { + var type = jQuery.type( arg ); + if ( type === "function" ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && type !== "string" ) { + // Inspect recursively + add( arg ); + } + }); + })( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + jQuery.each( arguments, function( _, arg ) { + var index; + while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + // Handle firing indexes + if ( firing ) { + if ( index <= firingLength ) { + firingLength--; + } + if ( index <= firingIndex ) { + firingIndex--; + } + } + } + }); + } + return this; + }, + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); + }, + // Remove all callbacks from the list + empty: function() { + list = []; + firingLength = 0; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( list && ( !fired || stack ) ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; +jQuery.extend({ + + Deferred: function( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], + [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], + [ "notify", "progress", jQuery.Callbacks("memory") ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred(function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var action = tuple[ 0 ], + fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[1] ](function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); + } + }); + }); + fns = null; + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[1] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[0] ] = function() { + deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); + return this; + }; + deferred[ tuple[0] + "With" ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = core_slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + + // the master Deferred. If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; + if( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !( --remaining ) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, progressValues ) ); + } else { + --remaining; + } + } + } + + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } +}); +jQuery.support = (function( support ) { + + var all, a, input, select, fragment, opt, eventName, isSupported, i, + div = document.createElement("div"); + + // Setup + div.setAttribute( "className", "t" ); + div.innerHTML = "
              ').addClass('note-specialchar-node'); + var $tr = (idx % COLUMN_LENGTH === 0) ? $('
              a"; + + // Finish early in limited (non-browser) environments + all = div.getElementsByTagName("*") || []; + a = div.getElementsByTagName("a")[ 0 ]; + if ( !a || !a.style || !all.length ) { + return support; + } + + // First batch of tests + select = document.createElement("select"); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName("input")[ 0 ]; + + a.style.cssText = "top:1px;float:left;opacity:.5"; + + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + support.getSetAttribute = div.className !== "t"; + + // IE strips leading whitespace when .innerHTML is used + support.leadingWhitespace = div.firstChild.nodeType === 3; + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + support.tbody = !div.getElementsByTagName("tbody").length; + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + support.htmlSerialize = !!div.getElementsByTagName("link").length; + + // Get the style information from getAttribute + // (IE uses .cssText instead) + support.style = /top/.test( a.getAttribute("style") ); + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + support.hrefNormalized = a.getAttribute("href") === "/a"; + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + support.opacity = /^0.5/.test( a.style.opacity ); + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + support.cssFloat = !!a.style.cssFloat; + + // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) + support.checkOn = !!input.value; + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + support.optSelected = opt.selected; + + // Tests for enctype support on a form (#6743) + support.enctype = !!document.createElement("form").enctype; + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + support.html5Clone = document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>"; + + // Will be defined later + support.inlineBlockNeedsLayout = false; + support.shrinkWrapBlocks = false; + support.pixelPosition = false; + support.deleteExpando = true; + support.noCloneEvent = true; + support.reliableMarginRight = true; + support.boxSizingReliable = true; + + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Support: IE<9 + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + + // Check if we can trust getAttribute("value") + input = document.createElement("input"); + input.setAttribute( "value", "" ); + support.input = input.getAttribute( "value" ) === ""; + + // Check if an input maintains its value after becoming a radio + input.value = "t"; + input.setAttribute( "type", "radio" ); + support.radioValue = input.value === "t"; + + // #11217 - WebKit loses check when the name is after the checked attribute + input.setAttribute( "checked", "t" ); + input.setAttribute( "name", "t" ); + + fragment = document.createDocumentFragment(); + fragment.appendChild( input ); + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + // WebKit doesn't clone checked state correctly in fragments + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE<9 + // Opera does not clone events (and typeof div.attachEvent === undefined). + // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() + if ( div.attachEvent ) { + div.attachEvent( "onclick", function() { + support.noCloneEvent = false; + }); + + div.cloneNode( true ).click(); + } + + // Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event) + // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP) + for ( i in { submit: true, change: true, focusin: true }) { + div.setAttribute( eventName = "on" + i, "t" ); + + support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false; + } + + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + // Support: IE<9 + // Iteration over object's inherited properties before its own. + for ( i in jQuery( support ) ) { + break; + } + support.ownLast = i !== "0"; + + // Run tests that need a body at doc ready + jQuery(function() { + var container, marginDiv, tds, + divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;", + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } + + container = document.createElement("div"); + container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px"; + + body.appendChild( container ).appendChild( div ); + + // Support: IE8 + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + div.innerHTML = "
              t
              "; + tds = div.getElementsByTagName("td"); + tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Support: IE8 + // Check if empty table cells still have offsetWidth/Height + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Check box-sizing and margin behavior. + div.innerHTML = ""; + div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; + + // Workaround failing boxSizing test due to offsetWidth returning wrong value + // with some non-1 values of body zoom, ticket #13543 + jQuery.swap( body, body.style.zoom != null ? { zoom: 1 } : {}, function() { + support.boxSizing = div.offsetWidth === 4; + }); + + // Use window.getComputedStyle because jsdom on node.js will break without it. + if ( window.getComputedStyle ) { + support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; + support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. (#3333) + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + marginDiv = div.appendChild( document.createElement("div") ); + marginDiv.style.cssText = div.style.cssText = divReset; + marginDiv.style.marginRight = marginDiv.style.width = "0"; + div.style.width = "1px"; + + support.reliableMarginRight = + !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); + } + + if ( typeof div.style.zoom !== core_strundefined ) { + // Support: IE<8 + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + div.innerHTML = ""; + div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); + + // Support: IE6 + // Check if elements with layout shrink-wrap their children + div.style.display = "block"; + div.innerHTML = "
              "; + div.firstChild.style.width = "5px"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); + + if ( support.inlineBlockNeedsLayout ) { + // Prevent IE 6 from affecting layout for positioned elements #11048 + // Prevent IE from shrinking the body in IE 7 mode #12869 + // Support: IE<8 + body.style.zoom = 1; + } + } + + body.removeChild( container ); + + // Null elements to avoid leaks in IE + container = div = tds = marginDiv = null; + }); + + // Null elements to avoid leaks in IE + all = select = fragment = opt = a = input = null; + + return support; +})({}); + +var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, + rmultiDash = /([A-Z])/g; + +function internalData( elem, name, data, pvt /* Internal Use Only */ ){ + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var ret, thisCache, + internalKey = jQuery.expando, + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string" ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + id = elem[ internalKey ] = core_deletedIds.pop() || jQuery.guid++; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + // Avoid exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + cache[ id ] = isNode ? {} : { toJSON: jQuery.noop }; + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( typeof name === "string" ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; +} + +function internalRemoveData( elem, name, pvt ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, i, + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split(" "); + } + } + } else { + // If "name" is an array of keys... + // When data is initially created, via ("key", "val") signature, + // keys will be converted to camelCase. + // Since there is no way to tell _how_ a key was added, remove + // both plain key and camelCase key. #12786 + // This will only penalize the array argument path. + name = name.concat( jQuery.map( name, jQuery.camelCase ) ); + } + + i = name.length; + while ( i-- ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject( cache[ id ] ) ) { + return; + } + } + + // Destroy the cache + if ( isNode ) { + jQuery.cleanData( [ elem ], true ); + + // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) + /* jshint eqeqeq: false */ + } else if ( jQuery.support.deleteExpando || cache != cache.window ) { + /* jshint eqeqeq: true */ + delete cache[ id ]; + + // When all else fails, null + } else { + cache[ id ] = null; + } +} + +jQuery.extend({ + cache: {}, + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "applet": true, + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data ) { + return internalData( elem, name, data ); + }, + + removeData: function( elem, name ) { + return internalRemoveData( elem, name ); + }, + + // For internal use only. + _data: function( elem, name, data ) { + return internalData( elem, name, data, true ); + }, + + _removeData: function( elem, name ) { + return internalRemoveData( elem, name, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + // Do not set data on non-element because it will not be cleared (#8335). + if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) { + return false; + } + + var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; + + // nodes accept data unless otherwise specified; rejection can be conditional + return !noData || noData !== true && elem.getAttribute("classid") === noData; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var attrs, name, + data = null, + i = 0, + elem = this[0]; + + // Special expections of .data basically thwart jQuery.access, + // so implement the relevant behavior ourselves + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = jQuery.data( elem ); + + if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { + attrs = elem.attributes; + for ( ; i < attrs.length; i++ ) { + name = attrs[i].name; + + if ( name.indexOf("data-") === 0 ) { + name = jQuery.camelCase( name.slice(5) ); + + dataAttr( elem, name, data[ name ] ); + } + } + jQuery._data( elem, "parsedAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + return arguments.length > 1 ? + + // Sets one value + this.each(function() { + jQuery.data( this, key, value ); + }) : + + // Gets one value + // Try to fetch any internally stored data first + elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null; + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + var name; + for ( name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} +jQuery.extend({ + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray(data) ) { + queue = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // not intended for public consumption - generates a queueHooks object, or returns the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return jQuery._data( elem, key ) || jQuery._data( elem, key, { + empty: jQuery.Callbacks("once memory").add(function() { + jQuery._removeData( elem, type + "queue" ); + jQuery._removeData( elem, key ); + }) + }); + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } + + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); + + // ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while( i-- ) { + tmp = jQuery._data( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +}); +var nodeHook, boolHook, + rclass = /[\t\r\n\f]/g, + rreturn = /\r/g, + rfocusable = /^(?:input|select|textarea|button|object)$/i, + rclickable = /^(?:a|area)$/i, + ruseDefault = /^(?:checked|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + getSetInput = jQuery.support.input; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} + }); + }, + + addClass: function( value ) { + var classes, elem, cur, clazz, j, + i = 0, + len = this.length, + proceed = typeof value === "string" && value; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call( this, j, this.className ) ); + }); + } + + if ( proceed ) { + // The disjunction here is for better compressibility (see removeClass) + classes = ( value || "" ).match( core_rnotwhite ) || []; + + for ( ; i < len; i++ ) { + elem = this[ i ]; + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + " " + ); + + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + if ( cur.indexOf( " " + clazz + " " ) < 0 ) { + cur += clazz + " "; + } + } + elem.className = jQuery.trim( cur ); + + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classes, elem, cur, clazz, j, + i = 0, + len = this.length, + proceed = arguments.length === 0 || typeof value === "string" && value; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call( this, j, this.className ) ); + }); + } + if ( proceed ) { + classes = ( value || "" ).match( core_rnotwhite ) || []; + + for ( ; i < len; i++ ) { + elem = this[ i ]; + // This expression is here for better compressibility (see addClass) + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + "" + ); + + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + // Remove *all* instances + while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { + cur = cur.replace( " " + clazz + " ", " " ); + } + } + elem.className = value ? jQuery.trim( cur ) : ""; + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value; + + if ( typeof stateVal === "boolean" && type === "string" ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); + } + + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + classNames = value.match( core_rnotwhite ) || []; + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space separated list + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); + } + } + + // Toggle whole class name + } else if ( type === core_strundefined || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // If the element has a class name or if we're passed "false", + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + var ret, hooks, isFunction, + elem = this[0]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } + + ret = elem.value; + + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each(function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, jQuery( this ).val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // Use proper attribute retrieval(#6932, #12072) + var val = jQuery.find.attr( elem, "value" ); + return val != null ? + val : + elem.text; + } + }, + select: { + get: function( elem ) { + var value, option, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one" || index < 0, + values = one ? null : [], + max = one ? index + 1 : options.length, + i = index < 0 ? + max : + one ? index : 0; + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // oldIE doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + // Don't return options that are disabled or in a disabled optgroup + ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && + ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; + + while ( i-- ) { + option = options[ i ]; + if ( (option.selected = jQuery.inArray( jQuery(option).val(), values ) >= 0) ) { + optionSet = true; + } + } + + // force browsers to behave consistently when non-matching value is set + if ( !optionSet ) { + elem.selectedIndex = -1; + } + return values; + } + } + }, + + attr: function( elem, name, value ) { + var hooks, ret, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === core_strundefined ) { + return jQuery.prop( elem, name, value ); + } + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || + ( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + + } else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, value + "" ); + return value; + } + + } else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + ret = jQuery.find.attr( elem, name ); + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var name, propName, + i = 0, + attrNames = value && value.match( core_rnotwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( (name = attrNames[i++]) ) { + propName = jQuery.propFix[ name ] || name; + + // Boolean attributes get special treatment (#10870) + if ( jQuery.expr.match.bool.test( name ) ) { + // Set corresponding property to false + if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { + elem[ propName ] = false; + // Support: IE<9 + // Also clear defaultChecked/defaultSelected (if appropriate) + } else { + elem[ jQuery.camelCase( "default-" + name ) ] = + elem[ propName ] = false; + } + + // See #9699 for explanation of this approach (setting first, then removal) + } else { + jQuery.attr( elem, name, "" ); + } + + elem.removeAttribute( getSetAttribute ? name : propName ); + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to default in case type is set after value during creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + propFix: { + "for": "htmlFor", + "class": "className" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + return hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ? + ret : + ( elem[ name ] = value ); + + } else { + return hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ? + ret : + elem[ name ]; + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + // Use proper attribute retrieval(#12072) + var tabindex = jQuery.find.attr( elem, "tabindex" ); + + return tabindex ? + parseInt( tabindex, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + -1; + } + } + } +}); + +// Hooks for boolean attributes +boolHook = { + set: function( elem, value, name ) { + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { + // IE<8 needs the *property* name + elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name ); + + // Use defaultChecked and defaultSelected for oldIE + } else { + elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true; + } + + return name; + } +}; +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) { + var getter = jQuery.expr.attrHandle[ name ] || jQuery.find.attr; + + jQuery.expr.attrHandle[ name ] = getSetInput && getSetAttribute || !ruseDefault.test( name ) ? + function( elem, name, isXML ) { + var fn = jQuery.expr.attrHandle[ name ], + ret = isXML ? + undefined : + /* jshint eqeqeq: false */ + (jQuery.expr.attrHandle[ name ] = undefined) != + getter( elem, name, isXML ) ? + + name.toLowerCase() : + null; + jQuery.expr.attrHandle[ name ] = fn; + return ret; + } : + function( elem, name, isXML ) { + return isXML ? + undefined : + elem[ jQuery.camelCase( "default-" + name ) ] ? + name.toLowerCase() : + null; + }; +}); + +// fix oldIE attroperties +if ( !getSetInput || !getSetAttribute ) { + jQuery.attrHooks.value = { + set: function( elem, value, name ) { + if ( jQuery.nodeName( elem, "input" ) ) { + // Does not return so that setAttribute is also used + elem.defaultValue = value; + } else { + // Use nodeHook if defined (#1954); otherwise setAttribute is fine + return nodeHook && nodeHook.set( elem, value, name ); + } + } + }; +} + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = { + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + elem.setAttributeNode( + (ret = elem.ownerDocument.createAttribute( name )) + ); + } + + ret.value = value += ""; + + // Break association with cloned elements by also using setAttribute (#9646) + return name === "value" || value === elem.getAttribute( name ) ? + value : + undefined; + } + }; + jQuery.expr.attrHandle.id = jQuery.expr.attrHandle.name = jQuery.expr.attrHandle.coords = + // Some attributes are constructed with empty-string values when not defined + function( elem, name, isXML ) { + var ret; + return isXML ? + undefined : + (ret = elem.getAttributeNode( name )) && ret.value !== "" ? + ret.value : + null; + }; + jQuery.valHooks.button = { + get: function( elem, name ) { + var ret = elem.getAttributeNode( name ); + return ret && ret.specified ? + ret.value : + undefined; + }, + set: nodeHook.set + }; + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + set: function( elem, value, name ) { + nodeHook.set( elem, value === "" ? false : value, name ); + } + }; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }; + }); +} + + +// Some attributes require a special call on IE +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !jQuery.support.hrefNormalized ) { + // href/src property should get the full normalized URL (#10299/#12915) + jQuery.each([ "href", "src" ], function( i, name ) { + jQuery.propHooks[ name ] = { + get: function( elem ) { + return elem.getAttribute( name, 4 ); + } + }; + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Note: IE uppercases css property names, but if we were to .toLowerCase() + // .cssText, that would destroy case senstitivity in URL's, like in "background" + return elem.style.cssText || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = value + "" ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }; +} + +jQuery.each([ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +}); + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }; + if ( !jQuery.support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + // Support: Webkit + // "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + }; + } +}); +var rformElems = /^(?:input|select|textarea)$/i, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + var tmp, events, t, handleObjIn, + special, eventHandle, handleObj, + handlers, type, namespaces, origType, + elemData = jQuery._data( elem ); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if ( !elemData ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !(events = elemData.events) ) { + events = elemData.events = {}; + } + if ( !(eventHandle = elemData.handle) ) { + eventHandle = elemData.handle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( core_rnotwhite ) || [""]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !(handlers = events[ type ]) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + var j, handleObj, tmp, + origCount, t, events, + special, handlers, type, + namespaces, origType, + elemData = jQuery.hasData( elem ) && jQuery._data( elem ); + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( core_rnotwhite ) || [""]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + delete elemData.handle; + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery._removeData( elem, "events" ); + } + }, + + trigger: function( event, data, elem, onlyHandlers ) { + var handle, ontype, cur, + bubbleType, special, tmp, i, + eventPath = [ elem || document ], + type = core_hasOwn.call( event, "type" ) ? event.type : event, + namespaces = core_hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; + + cur = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf(".") >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf(":") < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join("."); + event.namespace_re = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === (elem.ownerDocument || document) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { + + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) && + jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + try { + elem[ type ](); + } catch ( e ) { + // IE<9 dies on focus/blur to hidden element (#1486,#12518) + // only reproducible on winXP IE8 native, not IE9 in IE8 mode + } + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); + + var i, ret, handleObj, matched, j, + handlerQueue = [], + args = core_slice.call( arguments ), + handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { + + // Triggered event must either 1) have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( (event.result = ret) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var sel, handleObj, matches, i, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { + + /* jshint eqeqeq: false */ + for ( ; cur != this; cur = cur.parentNode || this ) { + /* jshint eqeqeq: true */ + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) >= 0 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, handlers: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); + } + + return handlerQueue; + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; + + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = new jQuery.Event( originalEvent ); + + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Support: IE<9 + // Fix target property (#1925) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Support: Chrome 23+, Safari? + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // Support: IE<9 + // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) + event.metaKey = !!event.metaKey; + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var body, eventDoc, doc, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + special: { + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + focus: { + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== safeActiveElement() && this.focus ) { + try { + this.focus(); + return false; + } catch ( e ) { + // Support: IE<9 + // If we error on focus to hidden element (#1486, #12518), + // let .trigger() run the handlers + } + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === safeActiveElement() && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + click: { + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { + this.click(); + return false; + } + }, + + // For cross-browser consistency, don't fire native .click() on links + _default: function( event ) { + return jQuery.nodeName( event.target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Even when returnValue equals to undefined Firefox will still show alert + if ( event.result !== undefined ) { + event.originalEvent.returnValue = event.result; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + var name = "on" + type; + + if ( elem.detachEvent ) { + + // #8545, #7054, preventing memory leaks for custom events in IE6-8 + // detachEvent needed property on element, by name of that event, to properly expose it to GC + if ( typeof elem[ name ] === core_strundefined ) { + elem[ name ] = null; + } + + elem.detachEvent( name, handle ); + } + }; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + if ( !e ) { + return; + } + + // If preventDefault exists, run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // Support: IE + // Otherwise set the returnValue property of the original event to false + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + if ( !e ) { + return; + } + // If stopPropagation exists, run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + + // Support: IE + // Set the cancelBubble property of the original event to true + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + } +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// IE submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !jQuery._data( form, "submitBubbles" ) ) { + jQuery.event.add( form, "submit._submit", function( event ) { + event._submit_bubble = true; + }); + jQuery._data( form, "submitBubbles", true ); + } + }); + // return undefined since we don't need an event listener + }, + + postDispatch: function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( event._submit_bubble ) { + delete event._submit_bubble; + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + } + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !jQuery.support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + } + // Allow triggered, simulated change events (#11500) + jQuery.event.simulate( "change", this, event, true ); + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + jQuery._data( elem, "changeBubbles", true ); + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return !rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !jQuery.support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var type, origFn; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + var elem = this[0]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +}); +var isSimple = /^.[^:#\[\.,]*$/, + rparentsprev = /^(?:parents|prev(?:Until|All))/, + rneedsContext = jQuery.expr.match.needsContext, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var i, + ret = [], + self = this, + len = self.length; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }) ); + } + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); + ret.selector = this.selector ? this.selector + " " + selector : selector; + return ret; + }, + + has: function( target ) { + var i, + targets = jQuery( target, this ), + len = targets.length; + + return this.filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector || [], true) ); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector || [], false) ); + }, + + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + ret = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) { + // Always skip document fragments + if ( cur.nodeType < 11 && (pos ? + pos.index(cur) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector(cur, selectors)) ) { + + cur = ret.push( cur ); + break; + } + } + } + + return this.pushStack( ret.length > 1 ? jQuery.unique( ret ) : ret ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( jQuery.unique(all) ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); + } +}); + +function sibling( cur, dir ) { + do { + cur = cur[ dir ]; + } while ( cur && cur.nodeType !== 1 ); + + return cur; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + if ( this.length > 1 ) { + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + ret = jQuery.unique( ret ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + } + + return this.pushStack( ret ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 && elem.nodeType === 1 ? + jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : + jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + })); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + /* jshint -W018 */ + return !!qualifier.call( elem, i, elem ) !== not; + }); + + } + + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + }); + + } + + if ( typeof qualifier === "string" ) { + if ( isSimple.test( qualifier ) ) { + return jQuery.filter( qualifier, elements, not ); + } + + qualifier = jQuery.filter( qualifier, elements ); + } + + return jQuery.grep( elements, function( elem ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) !== not; + }); +} +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, + rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, + rtagName = /<([\w:]+)/, + rtbody = /\s*$/g, + + // We have to close these tags to support XHTML (#13200) + wrapMap = { + option: [ 1, "" ], + legend: [ 1, "
              ", "
              " ], + area: [ 1, "", "" ], + param: [ 1, "", "" ], + thead: [ 1, "", "
              " ], + tr: [ 2, "", "
              " ], + col: [ 2, "", "
              " ], + td: [ 3, "", "
              " ], + + // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, + // unless wrapped in a div with non-breaking characters in front of it. + _default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X
              ", "
              " ] + }, + safeFragment = createSafeFragment( document ), + fragmentDiv = safeFragment.appendChild( document.createElement("div") ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +jQuery.fn.extend({ + text: function( value ) { + return jQuery.access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); + }, null, value, arguments.length ); + }, + + append: function() { + return this.domManip( arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + }); + }, + + prepend: function() { + return this.domManip( arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + }); + }, + + before: function() { + return this.domManip( arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + }); + }, + + after: function() { + return this.domManip( arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + }); + }, + + // keepData is for internal use only--do not document + remove: function( selector, keepData ) { + var elem, + elems = selector ? jQuery.filter( selector, this ) : this, + i = 0; + + for ( ; (elem = elems[i]) != null; i++ ) { + + if ( !keepData && elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem ) ); + } + + if ( elem.parentNode ) { + if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { + setGlobalEval( getAll( elem, "script" ) ); + } + elem.parentNode.removeChild( elem ); + } + } + + return this; + }, + + empty: function() { + var elem, + i = 0; + + for ( ; (elem = this[i]) != null; i++ ) { + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + } + + // Remove any remaining nodes + while ( elem.firstChild ) { + elem.removeChild( elem.firstChild ); + } + + // If this is a select, ensure that it displays empty (#12336) + // Support: IE<9 + if ( elem.options && jQuery.nodeName( elem, "select" ) ) { + elem.options.length = 0; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function () { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + }); + }, + + html: function( value ) { + return jQuery.access( this, function( value ) { + var elem = this[0] || {}, + i = 0, + l = this.length; + + if ( value === undefined ) { + return elem.nodeType === 1 ? + elem.innerHTML.replace( rinlinejQuery, "" ) : + undefined; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + ( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) && + ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && + !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) { + + value = value.replace( rxhtmlTag, "<$1>" ); + + try { + for (; i < l; i++ ) { + // Remove element nodes and prevent memory leaks + elem = this[i] || {}; + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch(e) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var + // Snapshot the DOM in case .domManip sweeps something relevant into its fragment + args = jQuery.map( this, function( elem ) { + return [ elem.nextSibling, elem.parentNode ]; + }), + i = 0; + + // Make the changes, replacing each context element with the new content + this.domManip( arguments, function( elem ) { + var next = args[ i++ ], + parent = args[ i++ ]; + + if ( parent ) { + // Don't use the snapshot next if it has moved (#13810) + if ( next && next.parentNode !== parent ) { + next = this.nextSibling; + } + jQuery( this ).remove(); + parent.insertBefore( elem, next ); + } + // Allow new content to include elements from the context set + }, true ); + + // Force removal if there was no new content (e.g., from empty arguments) + return i ? this : this.remove(); + }, + + detach: function( selector ) { + return this.remove( selector, true ); + }, + + domManip: function( args, callback, allowIntersection ) { + + // Flatten any nested arrays + args = core_concat.apply( [], args ); + + var first, node, hasScripts, + scripts, doc, fragment, + i = 0, + l = this.length, + set = this, + iNoClone = l - 1, + value = args[0], + isFunction = jQuery.isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( isFunction || !( l <= 1 || typeof value !== "string" || jQuery.support.checkClone || !rchecked.test( value ) ) ) { + return this.each(function( index ) { + var self = set.eq( index ); + if ( isFunction ) { + args[0] = value.call( this, index, self.html() ); + } + self.domManip( args, callback, allowIntersection ); + }); + } + + if ( l ) { + fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, !allowIntersection && this ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + if ( first ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( this[i], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { + + if ( node.src ) { + // Hope ajax is available... + jQuery._evalUrl( node.src ); + } else { + jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); + } + } + } + } + + // Fix #11809: Avoid leaking memory + fragment = first = null; + } + } + + return this; + } +}); + +// Support: IE<8 +// Manipulating tables requires a tbody +function manipulationTarget( elem, content ) { + return jQuery.nodeName( elem, "table" ) && + jQuery.nodeName( content.nodeType === 1 ? content : content.firstChild, "tr" ) ? + + elem.getElementsByTagName("tbody")[0] || + elem.appendChild( elem.ownerDocument.createElement("tbody") ) : + elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = (jQuery.find.attr( elem, "type" ) !== null) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); + if ( match ) { + elem.type = match[1]; + } else { + elem.removeAttribute("type"); + } + return elem; +} + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var elem, + i = 0; + for ( ; (elem = elems[i]) != null; i++ ) { + jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) ); + } +} + +function cloneCopyEvent( src, dest ) { + + if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { + return; + } + + var type, i, l, + oldData = jQuery._data( src ), + curData = jQuery._data( dest, oldData ), + events = oldData.events; + + if ( events ) { + delete curData.handle; + curData.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + + // make the cloned public data object a copy from the original + if ( curData.data ) { + curData.data = jQuery.extend( {}, curData.data ); + } +} + +function fixCloneNodeIssues( src, dest ) { + var nodeName, e, data; + + // We do not need to do anything for non-Elements + if ( dest.nodeType !== 1 ) { + return; + } + + nodeName = dest.nodeName.toLowerCase(); + + // IE6-8 copies events bound via attachEvent when using cloneNode. + if ( !jQuery.support.noCloneEvent && dest[ jQuery.expando ] ) { + data = jQuery._data( dest ); + + for ( e in data.events ) { + jQuery.removeEvent( dest, e, data.handle ); + } + + // Event data gets referenced instead of copied if the expando gets copied too + dest.removeAttribute( jQuery.expando ); + } + + // IE blanks contents when cloning scripts, and tries to evaluate newly-set text + if ( nodeName === "script" && dest.text !== src.text ) { + disableScript( dest ).text = src.text; + restoreScript( dest ); + + // IE6-10 improperly clones children of object elements using classid. + // IE10 throws NoModificationAllowedError if parent is null, #12132. + } else if ( nodeName === "object" ) { + if ( dest.parentNode ) { + dest.outerHTML = src.outerHTML; + } + + // This path appears unavoidable for IE9. When cloning an object + // element in IE9, the outerHTML strategy above is not sufficient. + // If the src has innerHTML and the destination does not, + // copy the src.innerHTML into the dest.innerHTML. #10324 + if ( jQuery.support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) { + dest.innerHTML = src.innerHTML; + } + + } else if ( nodeName === "input" && manipulation_rcheckableType.test( src.type ) ) { + // IE6-8 fails to persist the checked state of a cloned checkbox + // or radio button. Worse, IE6-7 fail to give the cloned element + // a checked appearance if the defaultChecked value isn't also set + + dest.defaultChecked = dest.checked = src.checked; + + // IE6-7 get confused and end up setting the value of a cloned + // checkbox/radio button to an empty string instead of "on" + if ( dest.value !== src.value ) { + dest.value = src.value; + } + + // IE6-8 fails to return the selected option to the default selected + // state when cloning options + } else if ( nodeName === "option" ) { + dest.defaultSelected = dest.selected = src.defaultSelected; + + // IE6-8 fails to set the defaultValue to the correct value when + // cloning other types of input fields + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +jQuery.each({ + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + i = 0, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone(true); + jQuery( insert[i] )[ original ]( elems ); + + // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() + core_push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +}); + +function getAll( context, tag ) { + var elems, elem, + i = 0, + found = typeof context.getElementsByTagName !== core_strundefined ? context.getElementsByTagName( tag || "*" ) : + typeof context.querySelectorAll !== core_strundefined ? context.querySelectorAll( tag || "*" ) : + undefined; + + if ( !found ) { + for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) { + if ( !tag || jQuery.nodeName( elem, tag ) ) { + found.push( elem ); + } else { + jQuery.merge( found, getAll( elem, tag ) ); + } + } + } + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], found ) : + found; +} + +// Used in buildFragment, fixes the defaultChecked property +function fixDefaultChecked( elem ) { + if ( manipulation_rcheckableType.test( elem.type ) ) { + elem.defaultChecked = elem.checked; + } +} + +jQuery.extend({ + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var destElements, node, clone, i, srcElements, + inPage = jQuery.contains( elem.ownerDocument, elem ); + + if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { + clone = elem.cloneNode( true ); + + // IE<=8 does not properly clone detached, unknown element nodes + } else { + fragmentDiv.innerHTML = elem.outerHTML; + fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); + } + + if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && + (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { + + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + // Fix all IE cloning issues + for ( i = 0; (node = srcElements[i]) != null; ++i ) { + // Ensure that the destination node is not null; Fixes #9587 + if ( destElements[i] ) { + fixCloneNodeIssues( node, destElements[i] ); + } + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0; (node = srcElements[i]) != null; i++ ) { + cloneCopyEvent( node, destElements[i] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + destElements = srcElements = node = null; + + // Return the cloned set + return clone; + }, + + buildFragment: function( elems, context, scripts, selection ) { + var j, elem, contains, + tmp, tag, tbody, wrap, + l = elems.length, + + // Ensure a safe fragment + safe = createSafeFragment( context ), + + nodes = [], + i = 0; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || safe.appendChild( context.createElement("div") ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + + tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1>" ) + wrap[2]; + + // Descend through wrappers to the right content + j = wrap[0]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Manually add leading whitespace removed by IE + if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { + nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) ); + } + + // Remove IE's autoinserted from table fragments + if ( !jQuery.support.tbody ) { + + // String was a , *may* have spurious + elem = tag === "table" && !rtbody.test( elem ) ? + tmp.firstChild : + + // String was a bare or + wrap[1] === "
              " && !rtbody.test( elem ) ? + tmp : + 0; + + j = elem && elem.childNodes.length; + while ( j-- ) { + if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) { + elem.removeChild( tbody ); + } + } + } + + jQuery.merge( nodes, tmp.childNodes ); + + // Fix #12392 for WebKit and IE > 9 + tmp.textContent = ""; + + // Fix #12392 for oldIE + while ( tmp.firstChild ) { + tmp.removeChild( tmp.firstChild ); + } + + // Remember the top-level container for proper cleanup + tmp = safe.lastChild; + } + } + } + + // Fix #11356: Clear elements from fragment + if ( tmp ) { + safe.removeChild( tmp ); + } + + // Reset defaultChecked for any radios and checkboxes + // about to be appended to the DOM in IE 6/7 (#8060) + if ( !jQuery.support.appendChecked ) { + jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked ); + } + + i = 0; + while ( (elem = nodes[ i++ ]) ) { + + // #4087 - If origin and destination elements are the same, and this is + // that element, do not do anything + if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { + continue; + } + + contains = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + tmp = getAll( safe.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( (elem = tmp[ j++ ]) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + tmp = null; + + return safe; + }, + + cleanData: function( elems, /* internal */ acceptData ) { + var elem, type, id, data, + i = 0, + internalKey = jQuery.expando, + cache = jQuery.cache, + deleteExpando = jQuery.support.deleteExpando, + special = jQuery.event.special; + + for ( ; (elem = elems[i]) != null; i++ ) { + + if ( acceptData || jQuery.acceptData( elem ) ) { + + id = elem[ internalKey ]; + data = id && cache[ id ]; + + if ( data ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Remove cache only if it was not already removed by jQuery.event.remove + if ( cache[ id ] ) { + + delete cache[ id ]; + + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( deleteExpando ) { + delete elem[ internalKey ]; + + } else if ( typeof elem.removeAttribute !== core_strundefined ) { + elem.removeAttribute( internalKey ); + + } else { + elem[ internalKey ] = null; + } + + core_deletedIds.push( id ); + } + } + } + } + }, + + _evalUrl: function( url ) { + return jQuery.ajax({ + url: url, + type: "GET", + dataType: "script", + async: false, + global: false, + "throws": true + }); + } +}); +jQuery.fn.extend({ + wrapAll: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapAll( html.call(this, i) ); + }); + } + + if ( this[0] ) { + // The elements to wrap the target around + var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); + + if ( this[0].parentNode ) { + wrap.insertBefore( this[0] ); + } + + wrap.map(function() { + var elem = this; + + while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { + elem = elem.firstChild; + } + + return elem; + }).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapInner( html.call(this, i) ); + }); + } + + return this.each(function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + }); + }, + + wrap: function( html ) { + var isFunction = jQuery.isFunction( html ); + + return this.each(function(i) { + jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); + }); + }, + + unwrap: function() { + return this.parent().each(function() { + if ( !jQuery.nodeName( this, "body" ) ) { + jQuery( this ).replaceWith( this.childNodes ); + } + }).end(); + } +}); +var iframe, getStyles, curCSS, + ralpha = /alpha\([^)]*\)/i, + ropacity = /opacity\s*=\s*([^)]*)/, + rposition = /^(top|right|bottom|left)$/, + // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" + // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rmargin = /^margin/, + rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ), + rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ), + rrelNum = new RegExp( "^([+-])=(" + core_pnum + ")", "i" ), + elemdisplay = { BODY: "block" }, + + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: 0, + fontWeight: 400 + }, + + cssExpand = [ "Top", "Right", "Bottom", "Left" ], + cssPrefixes = [ "Webkit", "O", "Moz", "ms" ]; + +// return a css property mapped to a potentially vendor prefixed property +function vendorPropName( style, name ) { + + // shortcut for names that are not vendor prefixed + if ( name in style ) { + return name; + } + + // check for vendor prefixed names + var capName = name.charAt(0).toUpperCase() + name.slice(1), + origName = name, + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in style ) { + return name; + } + } + + return origName; +} + +function isHidden( elem, el ) { + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); +} + +function showHide( elements, show ) { + var display, elem, hidden, + values = [], + index = 0, + length = elements.length; + + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + + values[ index ] = jQuery._data( elem, "olddisplay" ); + display = elem.style.display; + if ( show ) { + // Reset the inline display of this element to learn if it is + // being hidden by cascaded rules or not + if ( !values[ index ] && display === "none" ) { + elem.style.display = ""; + } + + // Set elements which have been overridden with display: none + // in a stylesheet to whatever the default browser style is + // for such an element + if ( elem.style.display === "" && isHidden( elem ) ) { + values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) ); + } + } else { + + if ( !values[ index ] ) { + hidden = isHidden( elem ); + + if ( display && display !== "none" || !hidden ) { + jQuery._data( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) ); + } + } + } + } + + // Set the display of most of the elements in a second loop + // to avoid the constant reflow + for ( index = 0; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + if ( !show || elem.style.display === "none" || elem.style.display === "" ) { + elem.style.display = show ? values[ index ] || "" : "none"; + } + } + + return elements; +} + +jQuery.fn.extend({ + css: function( name, value ) { + return jQuery.access( this, function( elem, name, value ) { + var len, styles, + map = {}, + i = 0; + + if ( jQuery.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + }, + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + if ( typeof state === "boolean" ) { + return state ? this.show() : this.hide(); + } + + return this.each(function() { + if ( isHidden( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + }); + } +}); + +jQuery.extend({ + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + } + } + } + }, + + // Don't automatically add "px" to these possibly-unitless properties + cssNumber: { + "columnCount": true, + "fillOpacity": true, + "fontWeight": true, + "lineHeight": true, + "opacity": true, + "order": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: { + // normalize float css property + "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" + }, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = jQuery.camelCase( name ), + style = elem.style; + + name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); + + // gets hook for the prefixed version + // followed by the unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // convert relative number strings (+= or -=) to relative numbers. #7345 + if ( type === "string" && (ret = rrelNum.exec( value )) ) { + value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) ); + // Fixes bug #9237 + type = "number"; + } + + // Make sure that NaN and null values aren't set. See: #7116 + if ( value == null || type === "number" && isNaN( value ) ) { + return; + } + + // If a number was passed in, add 'px' to the (except for certain CSS properties) + if ( type === "number" && !jQuery.cssNumber[ origName ] ) { + value += "px"; + } + + // Fixes #8908, it can be done more correctly by specifing setters in cssHooks, + // but it would mean to define eight (for every problematic property) identical functions + if ( !jQuery.support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) { + style[ name ] = "inherit"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { + + // Wrapped to prevent IE from throwing errors when 'invalid' values are provided + // Fixes bug #5509 + try { + style[ name ] = value; + } catch(e) {} + } + + } else { + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var num, val, hooks, + origName = jQuery.camelCase( name ); + + // Make sure that we're working with the right name + name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) ); + + // gets hook for the prefixed version + // followed by the unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + //convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Return, converting to number if forced or a qualifier was provided and val looks numeric + if ( extra === "" || extra ) { + num = parseFloat( val ); + return extra === true || jQuery.isNumeric( num ) ? num || 0 : val; + } + return val; + } +}); + +// NOTE: we've included the "window" in window.getComputedStyle +// because jsdom on node.js will break without it. +if ( window.getComputedStyle ) { + getStyles = function( elem ) { + return window.getComputedStyle( elem, null ); + }; + + curCSS = function( elem, name, _computed ) { + var width, minWidth, maxWidth, + computed = _computed || getStyles( elem ), + + // getPropertyValue is only needed for .css('filter') in IE9, see #12537 + ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined, + style = elem.style; + + if ( computed ) { + + if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right + // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels + // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values + if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { + + // Remember the original values + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + // Put in the new values to get a computed value out + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + // Revert the changed values + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret; + }; +} else if ( document.documentElement.currentStyle ) { + getStyles = function( elem ) { + return elem.currentStyle; + }; + + curCSS = function( elem, name, _computed ) { + var left, rs, rsLeft, + computed = _computed || getStyles( elem ), + ret = computed ? computed[ name ] : undefined, + style = elem.style; + + // Avoid setting ret to empty string here + // so we don't default to auto + if ( ret == null && style && style[ name ] ) { + ret = style[ name ]; + } + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + // but not position css attributes, as those are proportional to the parent element instead + // and we can't measure the parent instead because it might trigger a "stacking dolls" problem + if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { + + // Remember the original values + left = style.left; + rs = elem.runtimeStyle; + rsLeft = rs && rs.left; + + // Put in the new values to get a computed value out + if ( rsLeft ) { + rs.left = elem.currentStyle.left; + } + style.left = name === "fontSize" ? "1em" : ret; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + if ( rsLeft ) { + rs.left = rsLeft; + } + } + + return ret === "" ? "auto" : ret; + }; +} + +function setPositiveNumber( elem, value, subtract ) { + var matches = rnumsplit.exec( value ); + return matches ? + // Guard against undefined "subtract", e.g., when used as in cssHooks + Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : + value; +} + +function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { + var i = extra === ( isBorderBox ? "border" : "content" ) ? + // If we already have the right measurement, avoid augmentation + 4 : + // Otherwise initialize for horizontal or vertical properties + name === "width" ? 1 : 0, + + val = 0; + + for ( ; i < 4; i += 2 ) { + // both box models exclude margin, so add it if we want it + if ( extra === "margin" ) { + val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); + } + + if ( isBorderBox ) { + // border-box includes padding, so remove it if we want content + if ( extra === "content" ) { + val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + // at this point, extra isn't border nor margin, so remove border + if ( extra !== "margin" ) { + val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } else { + // at this point, extra isn't content, so add padding + val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // at this point, extra isn't content nor padding, so add border + if ( extra !== "padding" ) { + val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + return val; +} + +function getWidthOrHeight( elem, name, extra ) { + + // Start with offset property, which is equivalent to the border-box value + var valueIsBorderBox = true, + val = name === "width" ? elem.offsetWidth : elem.offsetHeight, + styles = getStyles( elem ), + isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // some non-html elements return undefined for offsetWidth, so check for null/undefined + // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 + // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 + if ( val <= 0 || val == null ) { + // Fall back to computed then uncomputed css if necessary + val = curCSS( elem, name, styles ); + if ( val < 0 || val == null ) { + val = elem.style[ name ]; + } + + // Computed unit is not pixels. Stop here and return. + if ( rnumnonpx.test(val) ) { + return val; + } + + // we need the check for style in case a browser which returns unreliable values + // for getComputedStyle silently falls back to the reliable elem.style + valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] ); + + // Normalize "", auto, and prepare for extra + val = parseFloat( val ) || 0; + } + + // use the active box-sizing model to add/subtract irrelevant styles + return ( val + + augmentWidthOrHeight( + elem, + name, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles + ) + ) + "px"; +} + +// Try to determine the default display value of an element +function css_defaultDisplay( nodeName ) { + var doc = document, + display = elemdisplay[ nodeName ]; + + if ( !display ) { + display = actualDisplay( nodeName, doc ); + + // If the simple way fails, read from inside an iframe + if ( display === "none" || !display ) { + // Use the already-created iframe if possible + iframe = ( iframe || + jQuery("'; +// } +// +// function switchImgAndIframe( img2frame ) { +// var tmpdiv, +// nodes = domUtils.getElementsByTagName( me.document, !img2frame ? "iframe" : "img" ); +// for ( var i = 0, node; node = nodes[i++]; ) { +// if ( node.className != "edui-faked-webapp" ){ +// continue; +// } +// tmpdiv = me.document.createElement( "div" ); +// tmpdiv.innerHTML = createInsertStr( img2frame ? {url:node.getAttribute( "_url" ), width:node.width, height:node.height,title:node.title,logo:node.style.backgroundImage.replace("url(","").replace(")","")} : {url:node.getAttribute( "src", 2 ),title:node.title, width:node.width, height:node.height,logo:node.getAttribute("logo_url")}, img2frame ? true : false,false ); +// node.parentNode.replaceChild( tmpdiv.firstChild, node ); +// } +// } +// +// me.addListener( "beforegetcontent", function () { +// switchImgAndIframe( true ); +// } ); +// me.addListener( 'aftersetcontent', function () { +// switchImgAndIframe( false ); +// } ); +// me.addListener( 'aftergetcontent', function ( cmdName ) { +// if ( cmdName == 'aftergetcontent' && me.queryCommandState( 'source' ) ){ +// return; +// } +// switchImgAndIframe( false ); +// } ); +// +// me.commands['webapp'] = { +// execCommand:function ( cmd, obj ) { +// me.execCommand( "inserthtml", createInsertStr( obj, false,true ) ); +// } +// }; +//}; + +UE.plugin.register('webapp', function (){ + var me = this; + function createInsertStr(obj,toEmbed){ + return !toEmbed ? + '' + : + '' + + } + return { + outputRule: function(root){ + utils.each(root.getNodesByTagName('img'),function(node){ + var html; + if(node.getAttr('class') == 'edui-faked-webapp'){ + html = createInsertStr({ + title:node.getAttr('title'), + 'width':node.getAttr('width'), + 'height':node.getAttr('height'), + 'align':node.getAttr('align'), + 'cssfloat':node.getStyle('float'), + 'url':node.getAttr("_url"), + 'logo':node.getAttr('_logo_url') + },true); + var embed = UE.uNode.createElement(html); + node.parentNode.replaceChild(embed,node); + } + }) + }, + inputRule:function(root){ + utils.each(root.getNodesByTagName('iframe'),function(node){ + if(node.getAttr('class') == 'edui-faked-webapp'){ + var img = UE.uNode.createElement(createInsertStr({ + title:node.getAttr('title'), + 'width':node.getAttr('width'), + 'height':node.getAttr('height'), + 'align':node.getAttr('align'), + 'cssfloat':node.getStyle('float'), + 'url':node.getAttr("src"), + 'logo':node.getAttr('logo_url') + })); + node.parentNode.replaceChild(img,node); + } + }) + + }, + commands:{ + /** + * 插入百度应用 + * @command webapp + * @method execCommand + * @remind 需要百度APPKey + * @remind 百度应用主页: http://app.baidu.com/ + * @param { Object } appOptions 应用所需的参数项, 支持的key有: title=>应用标题, width=>应用容器宽度, + * height=>应用容器高度,logo=>应用logo,url=>应用地址 + * @example + * ```javascript + * //editor是编辑器实例 + * //在编辑器里插入一个“植物大战僵尸”的APP + * editor.execCommand( 'webapp' , { + * title: '植物大战僵尸', + * width: 560, + * height: 465, + * logo: '应用展示的图片', + * url: '百度应用的地址' + * } ); + * ``` + */ + 'webapp':{ + execCommand:function (cmd, obj) { + + var me = this, + str = createInsertStr(utils.extend(obj,{ + align:'none' + }), false); + me.execCommand("inserthtml",str); + }, + queryCommandState:function () { + var me = this, + img = me.selection.getRange().getClosedNode(), + flag = img && (img.className == "edui-faked-webapp"); + return flag ? 1 : 0; + } + } + } + } +}); + +// plugins/template.js +///import core +///import plugins\inserthtml.js +///import plugins\cleardoc.js +///commands 模板 +///commandsName template +///commandsTitle 模板 +///commandsDialog dialogs\template +UE.plugins['template'] = function () { + UE.commands['template'] = { + execCommand:function (cmd, obj) { + obj.html && this.execCommand("inserthtml", obj.html); + } + }; + this.addListener("click", function (type, evt) { + var el = evt.target || evt.srcElement, + range = this.selection.getRange(); + var tnode = domUtils.findParent(el, function (node) { + if (node.className && domUtils.hasClass(node, "ue_t")) { + return node; + } + }, true); + tnode && range.selectNode(tnode).shrinkBoundary().select(); + }); + this.addListener("keydown", function (type, evt) { + var range = this.selection.getRange(); + if (!range.collapsed) { + if (!evt.ctrlKey && !evt.metaKey && !evt.shiftKey && !evt.altKey) { + var tnode = domUtils.findParent(range.startContainer, function (node) { + if (node.className && domUtils.hasClass(node, "ue_t")) { + return node; + } + }, true); + if (tnode) { + domUtils.removeClasses(tnode, ["ue_t"]); + } + } + } + }); +}; + + +// plugins/music.js +/** + * 插入音乐命令 + * @file + */ +UE.plugin.register('music', function (){ + var me = this; + function creatInsertStr(url,width,height,align,cssfloat,toEmbed){ + return !toEmbed ? + '' + : + ''; + } + return { + outputRule: function(root){ + utils.each(root.getNodesByTagName('img'),function(node){ + var html; + if(node.getAttr('class') == 'edui-faked-music'){ + var cssfloat = node.getStyle('float'); + var align = node.getAttr('align'); + html = creatInsertStr(node.getAttr("_url"), node.getAttr('width'), node.getAttr('height'), align, cssfloat, true); + var embed = UE.uNode.createElement(html); + node.parentNode.replaceChild(embed,node); + } + }) + }, + inputRule:function(root){ + utils.each(root.getNodesByTagName('embed'),function(node){ + if(node.getAttr('class') == 'edui-faked-music'){ + var cssfloat = node.getStyle('float'); + var align = node.getAttr('align'); + html = creatInsertStr(node.getAttr("src"), node.getAttr('width'), node.getAttr('height'), align, cssfloat,false); + var img = UE.uNode.createElement(html); + node.parentNode.replaceChild(img,node); + } + }) + + }, + commands:{ + /** + * 插入音乐 + * @command music + * @method execCommand + * @param { Object } musicOptions 插入音乐的参数项, 支持的key有: url=>音乐地址; + * width=>音乐容器宽度;height=>音乐容器高度;align=>音乐文件的对齐方式, 可选值有: left, center, right, none + * @example + * ```javascript + * //editor是编辑器实例 + * //在编辑器里插入一个“植物大战僵尸”的APP + * editor.execCommand( 'music' , { + * width: 400, + * height: 95, + * align: "center", + * url: "音乐地址" + * } ); + * ``` + */ + 'music':{ + execCommand:function (cmd, musicObj) { + var me = this, + str = creatInsertStr(musicObj.url, musicObj.width || 400, musicObj.height || 95, "none", false); + me.execCommand("inserthtml",str); + }, + queryCommandState:function () { + var me = this, + img = me.selection.getRange().getClosedNode(), + flag = img && (img.className == "edui-faked-music"); + return flag ? 1 : 0; + } + } + } + } +}); + +// plugins/autoupload.js +/** + * @description + * 1.拖放文件到编辑区域,自动上传并插入到选区 + * 2.插入粘贴板的图片,自动上传并插入到选区 + * @author Jinqn + * @date 2013-10-14 + */ +UE.plugin.register('autoupload', function (){ + + function sendAndInsertFile(file, editor) { + var me = editor; + //模拟数据 + var fieldName, urlPrefix, maxSize, allowFiles, actionUrl, + loadingHtml, errorHandler, successHandler, + filetype = /image\/\w+/i.test(file.type) ? 'image':'file', + loadingId = 'loading_' + (+new Date()).toString(36); + + fieldName = me.getOpt(filetype + 'FieldName'); + urlPrefix = me.getOpt(filetype + 'UrlPrefix'); + maxSize = me.getOpt(filetype + 'MaxSize'); + allowFiles = me.getOpt(filetype + 'AllowFiles'); + actionUrl = me.getActionUrl(me.getOpt(filetype + 'ActionName')); + errorHandler = function(title) { + var loader = me.document.getElementById(loadingId); + loader && domUtils.remove(loader); + me.fireEvent('showmessage', { + 'id': loadingId, + 'content': title, + 'type': 'error', + 'timeout': 4000 + }); + }; + + if (filetype == 'image') { + loadingHtml = ''; + successHandler = function(data) { + var link = urlPrefix + data.url, + loader = me.document.getElementById(loadingId); + if (loader) { + loader.setAttribute('src', link); + loader.setAttribute('_src', link); + loader.setAttribute('title', data.title || ''); + loader.setAttribute('alt', data.original || ''); + loader.removeAttribute('id'); + domUtils.removeClasses(loader, 'loadingclass'); + } + }; + } else { + loadingHtml = '

              ' + + '' + + '

              '; + successHandler = function(data) { + var link = urlPrefix + data.url, + loader = me.document.getElementById(loadingId); + + var rng = me.selection.getRange(), + bk = rng.createBookmark(); + rng.selectNode(loader).select(); + me.execCommand('insertfile', {'url': link}); + rng.moveToBookmark(bk).select(); + }; + } + + /* 插入loading的占位符 */ + me.execCommand('inserthtml', loadingHtml); + + /* 判断后端配置是否没有加载成功 */ + if (!me.getOpt(filetype + 'ActionName')) { + errorHandler(me.getLang('autoupload.errorLoadConfig')); + return; + } + /* 判断文件大小是否超出限制 */ + if(file.size > maxSize) { + errorHandler(me.getLang('autoupload.exceedSizeError')); + return; + } + /* 判断文件格式是否超出允许 */ + var fileext = file.name ? file.name.substr(file.name.lastIndexOf('.')):''; + if ((fileext && filetype != 'image') || (allowFiles && (allowFiles.join('') + '.').indexOf(fileext.toLowerCase() + '.') == -1)) { + errorHandler(me.getLang('autoupload.exceedTypeError')); + return; + } + + /* 创建Ajax并提交 */ + var xhr = new XMLHttpRequest(), + fd = new FormData(), + params = utils.serializeParam(me.queryCommandValue('serverparam')) || '', + url = utils.formatUrl(actionUrl + (actionUrl.indexOf('?') == -1 ? '?':'&') + params); + + fd.append(fieldName, file, file.name || ('blob.' + file.type.substr('image/'.length))); + fd.append('type', 'ajax'); + xhr.open("post", url, true); + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + xhr.addEventListener('load', function (e) { + try{ + var json = (new Function("return " + utils.trim(e.target.response)))(); + if (json.state == 'SUCCESS' && json.url) { + successHandler(json); + } else { + errorHandler(json.state); + } + }catch(er){ + errorHandler(me.getLang('autoupload.loadError')); + } + }); + xhr.send(fd); + } + + function getPasteImage(e){ + return e.clipboardData && e.clipboardData.items && e.clipboardData.items.length == 1 && /^image\//.test(e.clipboardData.items[0].type) ? e.clipboardData.items:null; + } + function getDropImage(e){ + return e.dataTransfer && e.dataTransfer.files ? e.dataTransfer.files:null; + } + + return { + outputRule: function(root){ + utils.each(root.getNodesByTagName('img'),function(n){ + if (/\b(loaderrorclass)|(bloaderrorclass)\b/.test(n.getAttr('class'))) { + n.parentNode.removeChild(n); + } + }); + utils.each(root.getNodesByTagName('p'),function(n){ + if (/\bloadpara\b/.test(n.getAttr('class'))) { + n.parentNode.removeChild(n); + } + }); + }, + bindEvents:{ + //插入粘贴板的图片,拖放插入图片 + 'ready':function(e){ + var me = this; + if(window.FormData && window.FileReader) { + domUtils.on(me.body, 'paste drop', function(e){ + var hasImg = false, + items; + //获取粘贴板文件列表或者拖放文件列表 + items = e.type == 'paste' ? getPasteImage(e):getDropImage(e); + if(items){ + var len = items.length, + file; + while (len--){ + file = items[len]; + if(file.getAsFile) file = file.getAsFile(); + if(file && file.size > 0) { + sendAndInsertFile(file, me); + hasImg = true; + } + } + hasImg && e.preventDefault(); + } + + }); + //取消拖放图片时出现的文字光标位置提示 + domUtils.on(me.body, 'dragover', function (e) { + if(e.dataTransfer.types[0] == 'Files') { + e.preventDefault(); + } + }); + + //设置loading的样式 + utils.cssRule('loading', + '.loadingclass{display:inline-block;cursor:default;background: url(\'' + + this.options.themePath + + this.options.theme +'/images/loading.gif\') no-repeat center center transparent;border:1px solid #cccccc;margin-left:1px;height: 22px;width: 22px;}\n' + + '.loaderrorclass{display:inline-block;cursor:default;background: url(\'' + + this.options.themePath + + this.options.theme +'/images/loaderror.png\') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;' + + '}', + this.document); + } + } + } + } +}); + +// plugins/autosave.js +UE.plugin.register('autosave', function (){ + + var me = this, + //无限循环保护 + lastSaveTime = new Date(), + //最小保存间隔时间 + MIN_TIME = 20, + //auto save key + saveKey = null; + + function save ( editor ) { + + var saveData; + + if ( new Date() - lastSaveTime < MIN_TIME ) { + return; + } + + if ( !editor.hasContents() ) { + //这里不能调用命令来删除, 会造成事件死循环 + saveKey && me.removePreferences( saveKey ); + return; + } + + lastSaveTime = new Date(); + + editor._saveFlag = null; + + saveData = me.body.innerHTML; + + if ( editor.fireEvent( "beforeautosave", { + content: saveData + } ) === false ) { + return; + } + + me.setPreferences( saveKey, saveData ); + + editor.fireEvent( "afterautosave", { + content: saveData + } ); + + } + + return { + defaultOptions: { + //默认间隔时间 + saveInterval: 500 + }, + bindEvents:{ + 'ready':function(){ + + var _suffix = "-drafts-data", + key = null; + + if ( me.key ) { + key = me.key + _suffix; + } else { + key = ( me.container.parentNode.id || 'ue-common' ) + _suffix; + } + + //页面地址+编辑器ID 保持唯一 + saveKey = ( location.protocol + location.host + location.pathname ).replace( /[.:\/]/g, '_' ) + key; + + }, + + 'contentchange': function () { + + if ( !saveKey ) { + return; + } + + if ( me._saveFlag ) { + window.clearTimeout( me._saveFlag ); + } + + if ( me.options.saveInterval > 0 ) { + + me._saveFlag = window.setTimeout( function () { + + save( me ); + + }, me.options.saveInterval ); + + } else { + + save(me); + + } + + + } + }, + commands:{ + 'clearlocaldata':{ + execCommand:function (cmd, name) { + if ( saveKey && me.getPreferences( saveKey ) ) { + me.removePreferences( saveKey ) + } + }, + notNeedUndo: true, + ignoreContentChange:true + }, + + 'getlocaldata':{ + execCommand:function (cmd, name) { + return saveKey ? me.getPreferences( saveKey ) || '' : ''; + }, + notNeedUndo: true, + ignoreContentChange:true + }, + + 'drafts':{ + execCommand:function (cmd, name) { + if ( saveKey ) { + me.body.innerHTML = me.getPreferences( saveKey ) || '

              '+domUtils.fillHtml+'

              '; + me.focus(true); + } + }, + queryCommandState: function () { + return saveKey ? ( me.getPreferences( saveKey ) === null ? -1 : 0 ) : -1; + }, + notNeedUndo: true, + ignoreContentChange:true + } + } + } + +}); + +// plugins/charts.js +UE.plugin.register('charts', function (){ + + var me = this; + + return { + bindEvents: { + 'chartserror': function () { + } + }, + commands:{ + 'charts': { + execCommand: function ( cmd, data ) { + + var tableNode = domUtils.findParentByTagName(this.selection.getRange().startContainer, 'table', true), + flagText = [], + config = {}; + + if ( !tableNode ) { + return false; + } + + if ( !validData( tableNode ) ) { + me.fireEvent( "chartserror" ); + return false; + } + + config.title = data.title || ''; + config.subTitle = data.subTitle || ''; + config.xTitle = data.xTitle || ''; + config.yTitle = data.yTitle || ''; + config.suffix = data.suffix || ''; + config.tip = data.tip || ''; + //数据对齐方式 + config.dataFormat = data.tableDataFormat || ''; + //图表类型 + config.chartType = data.chartType || 0; + + for ( var key in config ) { + + if ( !config.hasOwnProperty( key ) ) { + continue; + } + + flagText.push( key+":"+config[ key ] ); + + } + + tableNode.setAttribute( "data-chart", flagText.join( ";" ) ); + domUtils.addClass( tableNode, "edui-charts-table" ); + + + + }, + queryCommandState: function ( cmd, name ) { + + var tableNode = domUtils.findParentByTagName(this.selection.getRange().startContainer, 'table', true); + return tableNode && validData( tableNode ) ? 0 : -1; + + } + } + }, + inputRule:function(root){ + utils.each(root.getNodesByTagName('table'),function( tableNode ){ + + if ( tableNode.getAttr("data-chart") !== undefined ) { + tableNode.setAttr("style"); + } + + }) + + }, + outputRule:function(root){ + utils.each(root.getNodesByTagName('table'),function( tableNode ){ + + if ( tableNode.getAttr("data-chart") !== undefined ) { + tableNode.setAttr("style", "display: none;"); + } + + }) + + } + } + + function validData ( table ) { + + var firstRows = null, + cellCount = 0; + + //行数不够 + if ( table.rows.length < 2 ) { + return false; + } + + //列数不够 + if ( table.rows[0].cells.length < 2 ) { + return false; + } + + //第一行所有cell必须是th + firstRows = table.rows[ 0 ].cells; + cellCount = firstRows.length; + + for ( var i = 0, cell; cell = firstRows[ i ]; i++ ) { + + if ( cell.tagName.toLowerCase() !== 'th' ) { + return false; + } + + } + + for ( var i = 1, row; row = table.rows[ i ]; i++ ) { + + //每行单元格数不匹配, 返回false + if ( row.cells.length != cellCount ) { + return false; + } + + //第一列不是th也返回false + if ( row.cells[0].tagName.toLowerCase() !== 'th' ) { + return false; + } + + for ( var j = 1, cell; cell = row.cells[ j ]; j++ ) { + + var value = utils.trim( ( cell.innerText || cell.textContent || '' ) ); + + value = value.replace( new RegExp( UE.dom.domUtils.fillChar, 'g' ), '' ).replace( /^\s+|\s+$/g, '' ); + + //必须是数字 + if ( !/^\d*\.?\d+$/.test( value ) ) { + return false; + } + + } + + } + + return true; + + } + +}); + +// plugins/section.js +/** + * 目录大纲支持插件 + * @file + * @since 1.3.0 + */ +UE.plugin.register('section', function (){ + /* 目录节点对象 */ + function Section(option){ + this.tag = ''; + this.level = -1, + this.dom = null; + this.nextSection = null; + this.previousSection = null; + this.parentSection = null; + this.startAddress = []; + this.endAddress = []; + this.children = []; + } + function getSection(option) { + var section = new Section(); + return utils.extend(section, option); + } + function getNodeFromAddress(startAddress, root) { + var current = root; + for(var i = 0;i < startAddress.length; i++) { + if(!current.childNodes) return null; + current = current.childNodes[startAddress[i]]; + } + return current; + } + + var me = this; + + return { + bindMultiEvents:{ + type: 'aftersetcontent afterscencerestore', + handler: function(){ + me.fireEvent('updateSections'); + } + }, + bindEvents:{ + /* 初始化、拖拽、粘贴、执行setcontent之后 */ + 'ready': function (){ + me.fireEvent('updateSections'); + domUtils.on(me.body, 'drop paste', function(){ + me.fireEvent('updateSections'); + }); + }, + /* 执行paragraph命令之后 */ + 'afterexeccommand': function (type, cmd) { + if(cmd == 'paragraph') { + me.fireEvent('updateSections'); + } + }, + /* 部分键盘操作,触发updateSections事件 */ + 'keyup': function (type, e) { + var me = this, + range = me.selection.getRange(); + if(range.collapsed != true) { + me.fireEvent('updateSections'); + } else { + var keyCode = e.keyCode || e.which; + if(keyCode == 13 || keyCode == 8 || keyCode == 46) { + me.fireEvent('updateSections'); + } + } + } + }, + commands:{ + 'getsections': { + execCommand: function (cmd, levels) { + var levelFn = levels || ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + + for (var i = 0; i < levelFn.length; i++) { + if (typeof levelFn[i] == 'string') { + levelFn[i] = function(fn){ + return function(node){ + return node.tagName == fn.toUpperCase() + }; + }(levelFn[i]); + } else if (typeof levelFn[i] != 'function') { + levelFn[i] = function (node) { + return null; + } + } + } + function getSectionLevel(node) { + for (var i = 0; i < levelFn.length; i++) { + if (levelFn[i](node)) return i; + } + return -1; + } + + var me = this, + Directory = getSection({'level':-1, 'title':'root'}), + previous = Directory; + + function traversal(node, Directory) { + var level, + tmpSection = null, + parent, + child, + children = node.childNodes; + for (var i = 0, len = children.length; i < len; i++) { + child = children[i]; + level = getSectionLevel(child); + if (level >= 0) { + var address = me.selection.getRange().selectNode(child).createAddress(true).startAddress, + current = getSection({ + 'tag': child.tagName, + 'title': child.innerText || child.textContent || '', + 'level': level, + 'dom': child, + 'startAddress': utils.clone(address, []), + 'endAddress': utils.clone(address, []), + 'children': [] + }); + previous.nextSection = current; + current.previousSection = previous; + parent = previous; + while(level <= parent.level){ + parent = parent.parentSection; + } + current.parentSection = parent; + parent.children.push(current); + tmpSection = previous = current; + } else { + child.nodeType === 1 && traversal(child, Directory); + tmpSection && tmpSection.endAddress[tmpSection.endAddress.length - 1] ++; + } + } + } + traversal(me.body, Directory); + return Directory; + }, + notNeedUndo: true + }, + 'movesection': { + execCommand: function (cmd, sourceSection, targetSection, isAfter) { + + var me = this, + targetAddress, + target; + + if(!sourceSection || !targetSection || targetSection.level == -1) return; + + targetAddress = isAfter ? targetSection.endAddress:targetSection.startAddress; + target = getNodeFromAddress(targetAddress, me.body); + + /* 判断目标地址是否被源章节包含 */ + if(!targetAddress || !target || isContainsAddress(sourceSection.startAddress, sourceSection.endAddress, targetAddress)) return; + + var startNode = getNodeFromAddress(sourceSection.startAddress, me.body), + endNode = getNodeFromAddress(sourceSection.endAddress, me.body), + current, + nextNode; + + if(isAfter) { + current = endNode; + while ( current && !(domUtils.getPosition( startNode, current ) & domUtils.POSITION_FOLLOWING) ) { + nextNode = current.previousSibling; + domUtils.insertAfter(target, current); + if(current == startNode) break; + current = nextNode; + } + } else { + current = startNode; + while ( current && !(domUtils.getPosition( current, endNode ) & domUtils.POSITION_FOLLOWING) ) { + nextNode = current.nextSibling; + target.parentNode.insertBefore(current, target); + if(current == endNode) break; + current = nextNode; + } + } + + me.fireEvent('updateSections'); + + /* 获取地址的包含关系 */ + function isContainsAddress(startAddress, endAddress, addressTarget){ + var isAfterStartAddress = false, + isBeforeEndAddress = false; + for(var i = 0; i< startAddress.length; i++){ + if(i >= addressTarget.length) break; + if(addressTarget[i] > startAddress[i]) { + isAfterStartAddress = true; + break; + } else if(addressTarget[i] < startAddress[i]) { + break; + } + } + for(var i = 0; i< endAddress.length; i++){ + if(i >= addressTarget.length) break; + if(addressTarget[i] < startAddress[i]) { + isBeforeEndAddress = true; + break; + } else if(addressTarget[i] > startAddress[i]) { + break; + } + } + return isAfterStartAddress && isBeforeEndAddress; + } + } + }, + 'deletesection': { + execCommand: function (cmd, section, keepChildren) { + var me = this; + + if(!section) return; + + function getNodeFromAddress(startAddress) { + var current = me.body; + for(var i = 0;i < startAddress.length; i++) { + if(!current.childNodes) return null; + current = current.childNodes[startAddress[i]]; + } + return current; + } + + var startNode = getNodeFromAddress(section.startAddress), + endNode = getNodeFromAddress(section.endAddress), + current = startNode, + nextNode; + + if(!keepChildren) { + while ( current && domUtils.inDoc(endNode, me.document) && !(domUtils.getPosition( current, endNode ) & domUtils.POSITION_FOLLOWING) ) { + nextNode = current.nextSibling; + domUtils.remove(current); + current = nextNode; + } + } else { + domUtils.remove(current); + } + + me.fireEvent('updateSections'); + } + }, + 'selectsection': { + execCommand: function (cmd, section) { + if(!section && !section.dom) return false; + var me = this, + range = me.selection.getRange(), + address = { + 'startAddress':utils.clone(section.startAddress, []), + 'endAddress':utils.clone(section.endAddress, []) + }; + address.endAddress[address.endAddress.length - 1]++; + range.moveToAddress(address).select().scrollToView(); + return true; + }, + notNeedUndo: true + }, + 'scrolltosection': { + execCommand: function (cmd, section) { + if(!section && !section.dom) return false; + var me = this, + range = me.selection.getRange(), + address = { + 'startAddress':section.startAddress, + 'endAddress':section.endAddress + }; + address.endAddress[address.endAddress.length - 1]++; + range.moveToAddress(address).scrollToView(); + return true; + }, + notNeedUndo: true + } + } + } +}); + +// plugins/simpleupload.js +/** + * @description + * 简单上传:点击按钮,直接选择文件上传 + * @author Jinqn + * @date 2014-03-31 + */ +UE.plugin.register('simpleupload', function (){ + var me = this, + isLoaded = false, + containerBtn; + + function initUploadBtn(){ + var w = containerBtn.offsetWidth || 20, + h = containerBtn.offsetHeight || 20, + btnIframe = document.createElement('iframe'), + btnStyle = 'display:block;width:' + w + 'px;height:' + h + 'px;overflow:hidden;border:0;margin:0;padding:0;position:absolute;top:0;left:0;filter:alpha(opacity=0);-moz-opacity:0;-khtml-opacity: 0;opacity: 0;cursor:pointer;'; + + domUtils.on(btnIframe, 'load', function(){ + + var timestrap = (+new Date()).toString(36), + wrapper, + btnIframeDoc, + btnIframeBody; + + btnIframeDoc = (btnIframe.contentDocument || btnIframe.contentWindow.document); + btnIframeBody = btnIframeDoc.body; + wrapper = btnIframeDoc.createElement('div'); + + wrapper.innerHTML = '' + + '' + + '' + + ''; + + wrapper.className = 'edui-' + me.options.theme; + wrapper.id = me.ui.id + '_iframeupload'; + btnIframeBody.style.cssText = btnStyle; + btnIframeBody.style.width = w + 'px'; + btnIframeBody.style.height = h + 'px'; + btnIframeBody.appendChild(wrapper); + + if (btnIframeBody.parentNode) { + btnIframeBody.parentNode.style.width = w + 'px'; + btnIframeBody.parentNode.style.height = w + 'px'; + } + + var form = btnIframeDoc.getElementById('edui_form_' + timestrap); + var input = btnIframeDoc.getElementById('edui_input_' + timestrap); + var iframe = btnIframeDoc.getElementById('edui_iframe_' + timestrap); + + domUtils.on(input, 'change', function(){ + if(!input.value) return; + var loadingId = 'loading_' + (+new Date()).toString(36); + var params = utils.serializeParam(me.queryCommandValue('serverparam')) || ''; + + var imageActionUrl = me.getActionUrl(me.getOpt('imageActionName')); + var allowFiles = me.getOpt('imageAllowFiles'); + + me.focus(); + me.execCommand('inserthtml', ''); + + function callback(){ + try{ + var link, json, loader, + body = (iframe.contentDocument || iframe.contentWindow.document).body, + result = body.innerText || body.textContent || ''; + json = (new Function("return " + result))(); + link = me.options.imageUrlPrefix + json.url; + if(json.state == 'SUCCESS' && json.url) { + loader = me.document.getElementById(loadingId); + loader.setAttribute('src', link); + loader.setAttribute('_src', link); + loader.setAttribute('title', json.title || ''); + loader.setAttribute('alt', json.original || ''); + loader.removeAttribute('id'); + domUtils.removeClasses(loader, 'loadingclass'); + } else { + showErrorLoader && showErrorLoader(json.state); + } + }catch(er){ + showErrorLoader && showErrorLoader(me.getLang('simpleupload.loadError')); + } + form.reset(); + domUtils.un(iframe, 'load', callback); + } + function showErrorLoader(title){ + if(loadingId) { + var loader = me.document.getElementById(loadingId); + loader && domUtils.remove(loader); + me.fireEvent('showmessage', { + 'id': loadingId, + 'content': title, + 'type': 'error', + 'timeout': 4000 + }); + } + } + + /* 判断后端配置是否没有加载成功 */ + if (!me.getOpt('imageActionName')) { + errorHandler(me.getLang('autoupload.errorLoadConfig')); + return; + } + // 判断文件格式是否错误 + var filename = input.value, + fileext = filename ? filename.substr(filename.lastIndexOf('.')):''; + if (!fileext || (allowFiles && (allowFiles.join('') + '.').indexOf(fileext.toLowerCase() + '.') == -1)) { + showErrorLoader(me.getLang('simpleupload.exceedTypeError')); + return; + } + + domUtils.on(iframe, 'load', callback); + form.action = utils.formatUrl(imageActionUrl + (imageActionUrl.indexOf('?') == -1 ? '?':'&') + params); + form.submit(); + }); + + var stateTimer; + me.addListener('selectionchange', function () { + clearTimeout(stateTimer); + stateTimer = setTimeout(function() { + var state = me.queryCommandState('simpleupload'); + if (state == -1) { + input.disabled = 'disabled'; + } else { + input.disabled = false; + } + }, 400); + }); + isLoaded = true; + }); + + btnIframe.style.cssText = btnStyle; + containerBtn.appendChild(btnIframe); + } + + return { + bindEvents:{ + 'ready': function() { + //设置loading的样式 + utils.cssRule('loading', + '.loadingclass{display:inline-block;cursor:default;background: url(\'' + + this.options.themePath + + this.options.theme +'/images/loading.gif\') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;}\n' + + '.loaderrorclass{display:inline-block;cursor:default;background: url(\'' + + this.options.themePath + + this.options.theme +'/images/loaderror.png\') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;' + + '}', + this.document); + }, + /* 初始化简单上传按钮 */ + 'simpleuploadbtnready': function(type, container) { + containerBtn = container; + me.afterConfigReady(initUploadBtn); + } + }, + outputRule: function(root){ + utils.each(root.getNodesByTagName('img'),function(n){ + if (/\b(loaderrorclass)|(bloaderrorclass)\b/.test(n.getAttr('class'))) { + n.parentNode.removeChild(n); + } + }); + }, + commands: { + 'simpleupload': { + queryCommandState: function () { + return isLoaded ? 0:-1; + } + } + } + } +}); + +// plugins/serverparam.js +/** + * 服务器提交的额外参数列表设置插件 + * @file + * @since 1.2.6.1 + */ +UE.plugin.register('serverparam', function (){ + + var me = this, + serverParam = {}; + + return { + commands:{ + /** + * 修改服务器提交的额外参数列表,清除所有项 + * @command serverparam + * @method execCommand + * @param { String } cmd 命令字符串 + * @example + * ```javascript + * editor.execCommand('serverparam'); + * editor.queryCommandValue('serverparam'); //返回空 + * ``` + */ + /** + * 修改服务器提交的额外参数列表,删除指定项 + * @command serverparam + * @method execCommand + * @param { String } cmd 命令字符串 + * @param { String } key 要清除的属性 + * @example + * ```javascript + * editor.execCommand('serverparam', 'name'); //删除属性name + * ``` + */ + /** + * 修改服务器提交的额外参数列表,使用键值添加项 + * @command serverparam + * @method execCommand + * @param { String } cmd 命令字符串 + * @param { String } key 要添加的属性 + * @param { String } value 要添加属性的值 + * @example + * ```javascript + * editor.execCommand('serverparam', 'name', 'hello'); + * editor.queryCommandValue('serverparam'); //返回对象 {'name': 'hello'} + * ``` + */ + /** + * 修改服务器提交的额外参数列表,传入键值对对象添加多项 + * @command serverparam + * @method execCommand + * @param { String } cmd 命令字符串 + * @param { Object } key 传入的键值对对象 + * @example + * ```javascript + * editor.execCommand('serverparam', {'name': 'hello'}); + * editor.queryCommandValue('serverparam'); //返回对象 {'name': 'hello'} + * ``` + */ + /** + * 修改服务器提交的额外参数列表,使用自定义函数添加多项 + * @command serverparam + * @method execCommand + * @param { String } cmd 命令字符串 + * @param { Function } key 自定义获取参数的函数 + * @example + * ```javascript + * editor.execCommand('serverparam', function(editor){ + * return {'key': 'value'}; + * }); + * editor.queryCommandValue('serverparam'); //返回对象 {'key': 'value'} + * ``` + */ + + /** + * 获取服务器提交的额外参数列表 + * @command serverparam + * @method queryCommandValue + * @param { String } cmd 命令字符串 + * @example + * ```javascript + * editor.queryCommandValue( 'serverparam' ); //返回对象 {'key': 'value'} + * ``` + */ + 'serverparam':{ + execCommand:function (cmd, key, value) { + if (key === undefined || key === null) { //不传参数,清空列表 + serverParam = {}; + } else if (utils.isString(key)) { //传入键值 + if(value === undefined || value === null) { + delete serverParam[key]; + } else { + serverParam[key] = value; + } + } else if (utils.isObject(key)) { //传入对象,覆盖列表项 + utils.extend(serverParam, key, true); + } else if (utils.isFunction(key)){ //传入函数,添加列表项 + utils.extend(serverParam, key(), true); + } + }, + queryCommandValue: function(){ + return serverParam || {}; + } + } + } + } +}); + + +// plugins/insertfile.js +/** + * 插入附件 + */ +UE.plugin.register('insertfile', function (){ + + var me = this; + + function getFileIcon(url){ + var ext = url.substr(url.lastIndexOf('.') + 1).toLowerCase(), + maps = { + "rar":"icon_rar.gif", + "zip":"icon_rar.gif", + "tar":"icon_rar.gif", + "gz":"icon_rar.gif", + "bz2":"icon_rar.gif", + "doc":"icon_doc.gif", + "docx":"icon_doc.gif", + "pdf":"icon_pdf.gif", + "mp3":"icon_mp3.gif", + "xls":"icon_xls.gif", + "chm":"icon_chm.gif", + "ppt":"icon_ppt.gif", + "pptx":"icon_ppt.gif", + "avi":"icon_mv.gif", + "rmvb":"icon_mv.gif", + "wmv":"icon_mv.gif", + "flv":"icon_mv.gif", + "swf":"icon_mv.gif", + "rm":"icon_mv.gif", + "exe":"icon_exe.gif", + "psd":"icon_psd.gif", + "txt":"icon_txt.gif", + "jpg":"icon_jpg.gif", + "png":"icon_jpg.gif", + "jpeg":"icon_jpg.gif", + "gif":"icon_jpg.gif", + "ico":"icon_jpg.gif", + "bmp":"icon_jpg.gif" + }; + return maps[ext] ? maps[ext]:maps['txt']; + } + + return { + commands:{ + 'insertfile': { + execCommand: function (command, filelist){ + filelist = utils.isArray(filelist) ? filelist : [filelist]; + + var i, item, icon, title, + html = '', + URL = me.getOpt('UEDITOR_HOME_URL'), + iconDir = URL + (URL.substr(URL.length - 1) == '/' ? '':'/') + 'dialogs/attachment/fileTypeImages/'; + for (i = 0; i < filelist.length; i++) { + item = filelist[i]; + icon = iconDir + getFileIcon(item.url); + title = item.title || item.url.substr(item.url.lastIndexOf('/') + 1); + html += '

              ' + + '' + + '' + title + '' + + '

              '; + } + me.execCommand('insertHtml', html); + } + } + } + } +}); + + + + +// plugins/xssFilter.js +/** + * @file xssFilter.js + * @desc xss过滤器 + * @author robbenmu + */ + +UE.plugins.xssFilter = function() { + + var config = UEDITOR_CONFIG; + var whitList = config.whitList; + + function filter(node) { + + var tagName = node.tagName; + var attrs = node.attrs; + + if (!whitList.hasOwnProperty(tagName)) { + node.parentNode.removeChild(node); + return false; + } + + UE.utils.each(attrs, function (val, key) { + + if (whitList[tagName].indexOf(key) === -1) { + node.setAttr(key); + } + }); + } + + // 添加inserthtml\paste等操作用的过滤规则 + if (whitList && config.xssFilterRules) { + this.options.filterRules = function () { + + var result = {}; + + UE.utils.each(whitList, function(val, key) { + result[key] = function (node) { + return filter(node); + }; + }); + + return result; + }(); + } + + var tagList = []; + + UE.utils.each(whitList, function (val, key) { + tagList.push(key); + }); + + // 添加input过滤规则 + // + if (whitList && config.inputXssFilter) { + this.addInputRule(function (root) { + + root.traversal(function(node) { + if (node.type !== 'element') { + return false; + } + filter(node); + }); + }); + } + // 添加output过滤规则 + // + if (whitList && config.outputXssFilter) { + this.addOutputRule(function (root) { + + root.traversal(function(node) { + if (node.type !== 'element') { + return false; + } + filter(node); + }); + }); + } + +}; + + +// ui/ui.js +var baidu = baidu || {}; +baidu.editor = baidu.editor || {}; +UE.ui = baidu.editor.ui = {}; + +// ui/uiutils.js +(function (){ + var browser = baidu.editor.browser, + domUtils = baidu.editor.dom.domUtils; + + var magic = '$EDITORUI'; + var root = window[magic] = {}; + var uidMagic = 'ID' + magic; + var uidCount = 0; + + var uiUtils = baidu.editor.ui.uiUtils = { + uid: function (obj){ + return (obj ? obj[uidMagic] || (obj[uidMagic] = ++ uidCount) : ++ uidCount); + }, + hook: function ( fn, callback ) { + var dg; + if (fn && fn._callbacks) { + dg = fn; + } else { + dg = function (){ + var q; + if (fn) { + q = fn.apply(this, arguments); + } + var callbacks = dg._callbacks; + var k = callbacks.length; + while (k --) { + var r = callbacks[k].apply(this, arguments); + if (q === undefined) { + q = r; + } + } + return q; + }; + dg._callbacks = []; + } + dg._callbacks.push(callback); + return dg; + }, + createElementByHtml: function (html){ + var el = document.createElement('div'); + el.innerHTML = html; + el = el.firstChild; + el.parentNode.removeChild(el); + return el; + }, + getViewportElement: function (){ + return (browser.ie && browser.quirks) ? + document.body : document.documentElement; + }, + getClientRect: function (element){ + var bcr; + //trace IE6下在控制编辑器显隐时可能会报错,catch一下 + try{ + bcr = element.getBoundingClientRect(); + }catch(e){ + bcr={left:0,top:0,height:0,width:0} + } + var rect = { + left: Math.round(bcr.left), + top: Math.round(bcr.top), + height: Math.round(bcr.bottom - bcr.top), + width: Math.round(bcr.right - bcr.left) + }; + var doc; + while ((doc = element.ownerDocument) !== document && + (element = domUtils.getWindow(doc).frameElement)) { + bcr = element.getBoundingClientRect(); + rect.left += bcr.left; + rect.top += bcr.top; + } + rect.bottom = rect.top + rect.height; + rect.right = rect.left + rect.width; + return rect; + }, + getViewportRect: function (){ + var viewportEl = uiUtils.getViewportElement(); + var width = (window.innerWidth || viewportEl.clientWidth) | 0; + var height = (window.innerHeight ||viewportEl.clientHeight) | 0; + return { + left: 0, + top: 0, + height: height, + width: width, + bottom: height, + right: width + }; + }, + setViewportOffset: function (element, offset){ + var rect; + var fixedLayer = uiUtils.getFixedLayer(); + if (element.parentNode === fixedLayer) { + element.style.left = offset.left + 'px'; + element.style.top = offset.top + 'px'; + } else { + domUtils.setViewportOffset(element, offset); + } + }, + getEventOffset: function (evt){ + var el = evt.target || evt.srcElement; + var rect = uiUtils.getClientRect(el); + var offset = uiUtils.getViewportOffsetByEvent(evt); + return { + left: offset.left - rect.left, + top: offset.top - rect.top + }; + }, + getViewportOffsetByEvent: function (evt){ + var el = evt.target || evt.srcElement; + var frameEl = domUtils.getWindow(el).frameElement; + var offset = { + left: evt.clientX, + top: evt.clientY + }; + if (frameEl && el.ownerDocument !== document) { + var rect = uiUtils.getClientRect(frameEl); + offset.left += rect.left; + offset.top += rect.top; + } + return offset; + }, + setGlobal: function (id, obj){ + root[id] = obj; + return magic + '["' + id + '"]'; + }, + unsetGlobal: function (id){ + delete root[id]; + }, + copyAttributes: function (tgt, src){ + var attributes = src.attributes; + var k = attributes.length; + while (k --) { + var attrNode = attributes[k]; + if ( attrNode.nodeName != 'style' && attrNode.nodeName != 'class' && (!browser.ie || attrNode.specified) ) { + tgt.setAttribute(attrNode.nodeName, attrNode.nodeValue); + } + } + if (src.className) { + domUtils.addClass(tgt,src.className); + } + if (src.style.cssText) { + tgt.style.cssText += ';' + src.style.cssText; + } + }, + removeStyle: function (el, styleName){ + if (el.style.removeProperty) { + el.style.removeProperty(styleName); + } else if (el.style.removeAttribute) { + el.style.removeAttribute(styleName); + } else throw ''; + }, + contains: function (elA, elB){ + return elA && elB && (elA === elB ? false : ( + elA.contains ? elA.contains(elB) : + elA.compareDocumentPosition(elB) & 16 + )); + }, + startDrag: function (evt, callbacks,doc){ + var doc = doc || document; + var startX = evt.clientX; + var startY = evt.clientY; + function handleMouseMove(evt){ + var x = evt.clientX - startX; + var y = evt.clientY - startY; + callbacks.ondragmove(x, y,evt); + if (evt.stopPropagation) { + evt.stopPropagation(); + } else { + evt.cancelBubble = true; + } + } + if (doc.addEventListener) { + function handleMouseUp(evt){ + doc.removeEventListener('mousemove', handleMouseMove, true); + doc.removeEventListener('mouseup', handleMouseUp, true); + window.removeEventListener('mouseup', handleMouseUp, true); + callbacks.ondragstop(); + } + doc.addEventListener('mousemove', handleMouseMove, true); + doc.addEventListener('mouseup', handleMouseUp, true); + window.addEventListener('mouseup', handleMouseUp, true); + + evt.preventDefault(); + } else { + var elm = evt.srcElement; + elm.setCapture(); + function releaseCaptrue(){ + elm.releaseCapture(); + elm.detachEvent('onmousemove', handleMouseMove); + elm.detachEvent('onmouseup', releaseCaptrue); + elm.detachEvent('onlosecaptrue', releaseCaptrue); + callbacks.ondragstop(); + } + elm.attachEvent('onmousemove', handleMouseMove); + elm.attachEvent('onmouseup', releaseCaptrue); + elm.attachEvent('onlosecaptrue', releaseCaptrue); + evt.returnValue = false; + } + callbacks.ondragstart(); + }, + getFixedLayer: function (){ + var layer = document.getElementById('edui_fixedlayer'); + if (layer == null) { + layer = document.createElement('div'); + layer.id = 'edui_fixedlayer'; + document.body.appendChild(layer); + if (browser.ie && browser.version <= 8) { + layer.style.position = 'absolute'; + bindFixedLayer(); + setTimeout(updateFixedOffset); + } else { + layer.style.position = 'fixed'; + } + layer.style.left = '0'; + layer.style.top = '0'; + layer.style.width = '0'; + layer.style.height = '0'; + } + return layer; + }, + makeUnselectable: function (element){ + if (browser.opera || (browser.ie && browser.version < 9)) { + element.unselectable = 'on'; + if (element.hasChildNodes()) { + for (var i=0; i'; + } + }; + utils.inherits(Separator, UIBase); + +})(); + + +// ui/mask.js +///import core +///import uicore +(function (){ + var utils = baidu.editor.utils, + domUtils = baidu.editor.dom.domUtils, + UIBase = baidu.editor.ui.UIBase, + uiUtils = baidu.editor.ui.uiUtils; + + var Mask = baidu.editor.ui.Mask = function (options){ + this.initOptions(options); + this.initUIBase(); + }; + Mask.prototype = { + getHtmlTpl: function (){ + return '
              '; + }, + postRender: function (){ + var me = this; + domUtils.on(window, 'resize', function (){ + setTimeout(function (){ + if (!me.isHidden()) { + me._fill(); + } + }); + }); + }, + show: function (zIndex){ + this._fill(); + this.getDom().style.display = ''; + this.getDom().style.zIndex = zIndex; + }, + hide: function (){ + this.getDom().style.display = 'none'; + this.getDom().style.zIndex = ''; + }, + isHidden: function (){ + return this.getDom().style.display == 'none'; + }, + _onMouseDown: function (){ + return false; + }, + _onClick: function (e, target){ + this.fireEvent('click', e, target); + }, + _fill: function (){ + var el = this.getDom(); + var vpRect = uiUtils.getViewportRect(); + el.style.width = vpRect.width + 'px'; + el.style.height = vpRect.height + 'px'; + } + }; + utils.inherits(Mask, UIBase); +})(); + + +// ui/popup.js +///import core +///import uicore +(function () { + var utils = baidu.editor.utils, + uiUtils = baidu.editor.ui.uiUtils, + domUtils = baidu.editor.dom.domUtils, + UIBase = baidu.editor.ui.UIBase, + Popup = baidu.editor.ui.Popup = function (options){ + this.initOptions(options); + this.initPopup(); + }; + + var allPopups = []; + function closeAllPopup( evt,el ){ + for ( var i = 0; i < allPopups.length; i++ ) { + var pop = allPopups[i]; + if (!pop.isHidden()) { + if (pop.queryAutoHide(el) !== false) { + if(evt&&/scroll/ig.test(evt.type)&&pop.className=="edui-wordpastepop") return; + pop.hide(); + } + } + } + + if(allPopups.length) + pop.editor.fireEvent("afterhidepop"); + } + + Popup.postHide = closeAllPopup; + + var ANCHOR_CLASSES = ['edui-anchor-topleft','edui-anchor-topright', + 'edui-anchor-bottomleft','edui-anchor-bottomright']; + Popup.prototype = { + SHADOW_RADIUS: 5, + content: null, + _hidden: false, + autoRender: true, + canSideLeft: true, + canSideUp: true, + initPopup: function (){ + this.initUIBase(); + allPopups.push( this ); + }, + getHtmlTpl: function (){ + return '
              ' + + '
              ' + + ' ' + + '
              ' + + '
              ' + + this.getContentHtmlTpl() + + '
              ' + + '
              ' + + '
              '; + }, + getContentHtmlTpl: function (){ + if(this.content){ + if (typeof this.content == 'string') { + return this.content; + } + return this.content.renderHtml(); + }else{ + return '' + } + + }, + _UIBase_postRender: UIBase.prototype.postRender, + postRender: function (){ + + + if (this.content instanceof UIBase) { + this.content.postRender(); + } + + //捕获鼠标滚轮 + if( this.captureWheel && !this.captured ) { + + this.captured = true; + + var winHeight = ( document.documentElement.clientHeight || document.body.clientHeight ) - 80, + _height = this.getDom().offsetHeight, + _top = uiUtils.getClientRect( this.combox.getDom() ).top, + content = this.getDom('content'), + ifr = this.getDom('body').getElementsByTagName('iframe'), + me = this; + + ifr.length && ( ifr = ifr[0] ); + + while( _top + _height > winHeight ) { + _height -= 30; + } + content.style.height = _height + 'px'; + //同步更改iframe高度 + ifr && ( ifr.style.height = _height + 'px' ); + + //阻止在combox上的鼠标滚轮事件, 防止用户的正常操作被误解 + if( window.XMLHttpRequest ) { + + domUtils.on( content, ( 'onmousewheel' in document.body ) ? 'mousewheel' :'DOMMouseScroll' , function(e){ + + if(e.preventDefault) { + e.preventDefault(); + } else { + e.returnValue = false; + } + + if( e.wheelDelta ) { + + content.scrollTop -= ( e.wheelDelta / 120 )*60; + + } else { + + content.scrollTop -= ( e.detail / -3 )*60; + + } + + }); + + } else { + + //ie6 + domUtils.on( this.getDom(), 'mousewheel' , function(e){ + + e.returnValue = false; + + me.getDom('content').scrollTop -= ( e.wheelDelta / 120 )*60; + + }); + + } + + } + this.fireEvent('postRenderAfter'); + this.hide(true); + this._UIBase_postRender(); + }, + _doAutoRender: function (){ + if (!this.getDom() && this.autoRender) { + this.render(); + } + }, + mesureSize: function (){ + var box = this.getDom('content'); + return uiUtils.getClientRect(box); + }, + fitSize: function (){ + if( this.captureWheel && this.sized ) { + return this.__size; + } + this.sized = true; + var popBodyEl = this.getDom('body'); + popBodyEl.style.width = ''; + popBodyEl.style.height = ''; + var size = this.mesureSize(); + if( this.captureWheel ) { + popBodyEl.style.width = -(-20 -size.width) + 'px'; + var height = parseInt( this.getDom('content').style.height, 10 ); + !window.isNaN( height ) && ( size.height = height ); + } else { + popBodyEl.style.width = size.width + 'px'; + } + popBodyEl.style.height = size.height + 'px'; + this.__size = size; + this.captureWheel && (this.getDom('content').style.overflow = 'auto'); + return size; + }, + showAnchor: function ( element, hoz ){ + this.showAnchorRect( uiUtils.getClientRect( element ), hoz ); + }, + showAnchorRect: function ( rect, hoz, adj ){ + this._doAutoRender(); + var vpRect = uiUtils.getViewportRect(); + this.getDom().style.visibility = 'hidden'; + this._show(); + var popSize = this.fitSize(); + + var sideLeft, sideUp, left, top; + if (hoz) { + sideLeft = this.canSideLeft && (rect.right + popSize.width > vpRect.right && rect.left > popSize.width); + sideUp = this.canSideUp && (rect.top + popSize.height > vpRect.bottom && rect.bottom > popSize.height); + left = (sideLeft ? rect.left - popSize.width : rect.right); + top = (sideUp ? rect.bottom - popSize.height : rect.top); + } else { + sideLeft = this.canSideLeft && (rect.right + popSize.width > vpRect.right && rect.left > popSize.width); + sideUp = this.canSideUp && (rect.top + popSize.height > vpRect.bottom && rect.bottom > popSize.height); + left = (sideLeft ? rect.right - popSize.width : rect.left); + top = (sideUp ? rect.top - popSize.height : rect.bottom); + } + + var popEl = this.getDom(); + uiUtils.setViewportOffset(popEl, { + left: left, + top: top + }); + domUtils.removeClasses(popEl, ANCHOR_CLASSES); + popEl.className += ' ' + ANCHOR_CLASSES[(sideUp ? 1 : 0) * 2 + (sideLeft ? 1 : 0)]; + if(this.editor){ + popEl.style.zIndex = this.editor.container.style.zIndex * 1 + 10; + baidu.editor.ui.uiUtils.getFixedLayer().style.zIndex = popEl.style.zIndex - 1; + } + this.getDom().style.visibility = 'visible'; + + }, + showAt: function (offset) { + var left = offset.left; + var top = offset.top; + var rect = { + left: left, + top: top, + right: left, + bottom: top, + height: 0, + width: 0 + }; + this.showAnchorRect(rect, false, true); + }, + _show: function (){ + if (this._hidden) { + var box = this.getDom(); + box.style.display = ''; + this._hidden = false; +// if (box.setActive) { +// box.setActive(); +// } + this.fireEvent('show'); + } + }, + isHidden: function (){ + return this._hidden; + }, + show: function (){ + this._doAutoRender(); + this._show(); + }, + hide: function (notNofity){ + if (!this._hidden && this.getDom()) { + this.getDom().style.display = 'none'; + this._hidden = true; + if (!notNofity) { + this.fireEvent('hide'); + } + } + }, + queryAutoHide: function (el){ + return !el || !uiUtils.contains(this.getDom(), el); + } + }; + utils.inherits(Popup, UIBase); + + domUtils.on( document, 'mousedown', function ( evt ) { + var el = evt.target || evt.srcElement; + closeAllPopup( evt,el ); + } ); + domUtils.on( window, 'scroll', function (evt,el) { + closeAllPopup( evt,el ); + } ); + +})(); + + +// ui/colorpicker.js +///import core +///import uicore +(function (){ + var utils = baidu.editor.utils, + UIBase = baidu.editor.ui.UIBase, + ColorPicker = baidu.editor.ui.ColorPicker = function (options){ + this.initOptions(options); + this.noColorText = this.noColorText || this.editor.getLang("clearColor"); + this.initUIBase(); + }; + + ColorPicker.prototype = { + getHtmlTpl: function (){ + return genColorPicker(this.noColorText,this.editor); + }, + _onTableClick: function (evt){ + var tgt = evt.target || evt.srcElement; + var color = tgt.getAttribute('data-color'); + if (color) { + this.fireEvent('pickcolor', color); + } + }, + _onTableOver: function (evt){ + var tgt = evt.target || evt.srcElement; + var color = tgt.getAttribute('data-color'); + if (color) { + this.getDom('preview').style.backgroundColor = color; + } + }, + _onTableOut: function (){ + this.getDom('preview').style.backgroundColor = ''; + }, + _onPickNoColor: function (){ + this.fireEvent('picknocolor'); + } + }; + utils.inherits(ColorPicker, UIBase); + + var COLORS = ( + 'ffffff,000000,eeece1,1f497d,4f81bd,c0504d,9bbb59,8064a2,4bacc6,f79646,' + + 'f2f2f2,7f7f7f,ddd9c3,c6d9f0,dbe5f1,f2dcdb,ebf1dd,e5e0ec,dbeef3,fdeada,' + + 'd8d8d8,595959,c4bd97,8db3e2,b8cce4,e5b9b7,d7e3bc,ccc1d9,b7dde8,fbd5b5,' + + 'bfbfbf,3f3f3f,938953,548dd4,95b3d7,d99694,c3d69b,b2a2c7,92cddc,fac08f,' + + 'a5a5a5,262626,494429,17365d,366092,953734,76923c,5f497a,31859b,e36c09,' + + '7f7f7f,0c0c0c,1d1b10,0f243e,244061,632423,4f6128,3f3151,205867,974806,' + + 'c00000,ff0000,ffc000,ffff00,92d050,00b050,00b0f0,0070c0,002060,7030a0,').split(','); + + function genColorPicker(noColorText,editor){ + var html = '
              ' + + '
              ' + + '
              ' + + '
              '+ noColorText +'
              ' + + '
              ' + + '
              ' + + ''+ + ''; + for (var i=0; i':'')+''; + } + html += i<70 ? '':''; + } + html += '
              '+editor.getLang("themeColor")+'
              '+editor.getLang("standardColor")+'
              '; + return html; + } +})(); + + +// ui/tablepicker.js +///import core +///import uicore +(function (){ + var utils = baidu.editor.utils, + uiUtils = baidu.editor.ui.uiUtils, + UIBase = baidu.editor.ui.UIBase; + + var TablePicker = baidu.editor.ui.TablePicker = function (options){ + this.initOptions(options); + this.initTablePicker(); + }; + TablePicker.prototype = { + defaultNumRows: 10, + defaultNumCols: 10, + maxNumRows: 20, + maxNumCols: 20, + numRows: 10, + numCols: 10, + lengthOfCellSide: 22, + initTablePicker: function (){ + this.initUIBase(); + }, + getHtmlTpl: function (){ + var me = this; + return '
              ' + + '
              ' + + '
              ' + + '' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              '; + }, + _UIBase_render: UIBase.prototype.render, + render: function (holder){ + this._UIBase_render(holder); + this.getDom('label').innerHTML = '0'+this.editor.getLang("t_row")+' x 0'+this.editor.getLang("t_col"); + }, + _track: function (numCols, numRows){ + var style = this.getDom('overlay').style; + var sideLen = this.lengthOfCellSide; + style.width = numCols * sideLen + 'px'; + style.height = numRows * sideLen + 'px'; + var label = this.getDom('label'); + label.innerHTML = numCols +this.editor.getLang("t_col")+' x ' + numRows + this.editor.getLang("t_row"); + this.numCols = numCols; + this.numRows = numRows; + }, + _onMouseOver: function (evt, el){ + var rel = evt.relatedTarget || evt.fromElement; + if (!uiUtils.contains(el, rel) && el !== rel) { + this.getDom('label').innerHTML = '0'+this.editor.getLang("t_col")+' x 0'+this.editor.getLang("t_row"); + this.getDom('overlay').style.visibility = ''; + } + }, + _onMouseOut: function (evt, el){ + var rel = evt.relatedTarget || evt.toElement; + if (!uiUtils.contains(el, rel) && el !== rel) { + this.getDom('label').innerHTML = '0'+this.editor.getLang("t_col")+' x 0'+this.editor.getLang("t_row"); + this.getDom('overlay').style.visibility = 'hidden'; + } + }, + _onMouseMove: function (evt, el){ + var style = this.getDom('overlay').style; + var offset = uiUtils.getEventOffset(evt); + var sideLen = this.lengthOfCellSide; + var numCols = Math.ceil(offset.left / sideLen); + var numRows = Math.ceil(offset.top / sideLen); + this._track(numCols, numRows); + }, + _onClick: function (){ + this.fireEvent('picktable', this.numCols, this.numRows); + } + }; + utils.inherits(TablePicker, UIBase); +})(); + + +// ui/stateful.js +(function (){ + var browser = baidu.editor.browser, + domUtils = baidu.editor.dom.domUtils, + uiUtils = baidu.editor.ui.uiUtils; + + var TPL_STATEFUL = 'onmousedown="$$.Stateful_onMouseDown(event, this);"' + + ' onmouseup="$$.Stateful_onMouseUp(event, this);"' + + ( browser.ie ? ( + ' onmouseenter="$$.Stateful_onMouseEnter(event, this);"' + + ' onmouseleave="$$.Stateful_onMouseLeave(event, this);"' ) + : ( + ' onmouseover="$$.Stateful_onMouseOver(event, this);"' + + ' onmouseout="$$.Stateful_onMouseOut(event, this);"' )); + + baidu.editor.ui.Stateful = { + alwalysHoverable: false, + target:null,//目标元素和this指向dom不一样 + Stateful_init: function (){ + this._Stateful_dGetHtmlTpl = this.getHtmlTpl; + this.getHtmlTpl = this.Stateful_getHtmlTpl; + }, + Stateful_getHtmlTpl: function (){ + var tpl = this._Stateful_dGetHtmlTpl(); + // 使用function避免$转义 + return tpl.replace(/stateful/g, function (){ return TPL_STATEFUL; }); + }, + Stateful_onMouseEnter: function (evt, el){ + this.target=el; + if (!this.isDisabled() || this.alwalysHoverable) { + this.addState('hover'); + this.fireEvent('over'); + } + }, + Stateful_onMouseLeave: function (evt, el){ + if (!this.isDisabled() || this.alwalysHoverable) { + this.removeState('hover'); + this.removeState('active'); + this.fireEvent('out'); + } + }, + Stateful_onMouseOver: function (evt, el){ + var rel = evt.relatedTarget; + if (!uiUtils.contains(el, rel) && el !== rel) { + this.Stateful_onMouseEnter(evt, el); + } + }, + Stateful_onMouseOut: function (evt, el){ + var rel = evt.relatedTarget; + if (!uiUtils.contains(el, rel) && el !== rel) { + this.Stateful_onMouseLeave(evt, el); + } + }, + Stateful_onMouseDown: function (evt, el){ + if (!this.isDisabled()) { + this.addState('active'); + } + }, + Stateful_onMouseUp: function (evt, el){ + if (!this.isDisabled()) { + this.removeState('active'); + } + }, + Stateful_postRender: function (){ + if (this.disabled && !this.hasState('disabled')) { + this.addState('disabled'); + } + }, + hasState: function (state){ + return domUtils.hasClass(this.getStateDom(), 'edui-state-' + state); + }, + addState: function (state){ + if (!this.hasState(state)) { + this.getStateDom().className += ' edui-state-' + state; + } + }, + removeState: function (state){ + if (this.hasState(state)) { + domUtils.removeClasses(this.getStateDom(), ['edui-state-' + state]); + } + }, + getStateDom: function (){ + return this.getDom('state'); + }, + isChecked: function (){ + return this.hasState('checked'); + }, + setChecked: function (checked){ + if (!this.isDisabled() && checked) { + this.addState('checked'); + } else { + this.removeState('checked'); + } + }, + isDisabled: function (){ + return this.hasState('disabled'); + }, + setDisabled: function (disabled){ + if (disabled) { + this.removeState('hover'); + this.removeState('checked'); + this.removeState('active'); + this.addState('disabled'); + } else { + this.removeState('disabled'); + } + } + }; +})(); + + +// ui/button.js +///import core +///import uicore +///import ui/stateful.js +(function (){ + var utils = baidu.editor.utils, + UIBase = baidu.editor.ui.UIBase, + Stateful = baidu.editor.ui.Stateful, + Button = baidu.editor.ui.Button = function (options){ + if(options.name){ + var btnName = options.name; + var cssRules = options.cssRules; + if(!options.className){ + options.className = 'edui-for-' + btnName; + } + options.cssRules = '.edui-default .edui-for-'+ btnName +' .edui-icon {'+ cssRules +'}' + } + this.initOptions(options); + this.initButton(); + }; + Button.prototype = { + uiName: 'button', + label: '', + title: '', + showIcon: true, + showText: true, + cssRules:'', + initButton: function (){ + this.initUIBase(); + this.Stateful_init(); + if(this.cssRules){ + utils.cssRule('edui-customize-'+this.name+'-style',this.cssRules); + } + }, + getHtmlTpl: function (){ + return '
              ' + + '
              ' + + '
              ' + + (this.showIcon ? '
              ' : '') + + (this.showText ? '
              ' + this.label + '
              ' : '') + + '
              ' + + '
              ' + + '
              '; + }, + postRender: function (){ + this.Stateful_postRender(); + this.setDisabled(this.disabled) + }, + _onMouseDown: function (e){ + var target = e.target || e.srcElement, + tagName = target && target.tagName && target.tagName.toLowerCase(); + if (tagName == 'input' || tagName == 'object' || tagName == 'object') { + return false; + } + }, + _onClick: function (){ + if (!this.isDisabled()) { + this.fireEvent('click'); + } + }, + setTitle: function(text){ + var label = this.getDom('label'); + label.innerHTML = text; + } + }; + utils.inherits(Button, UIBase); + utils.extend(Button.prototype, Stateful); + +})(); + + +// ui/splitbutton.js +///import core +///import uicore +///import ui/stateful.js +(function (){ + var utils = baidu.editor.utils, + uiUtils = baidu.editor.ui.uiUtils, + domUtils = baidu.editor.dom.domUtils, + UIBase = baidu.editor.ui.UIBase, + Stateful = baidu.editor.ui.Stateful, + SplitButton = baidu.editor.ui.SplitButton = function (options){ + this.initOptions(options); + this.initSplitButton(); + }; + SplitButton.prototype = { + popup: null, + uiName: 'splitbutton', + title: '', + initSplitButton: function (){ + this.initUIBase(); + this.Stateful_init(); + var me = this; + if (this.popup != null) { + var popup = this.popup; + this.popup = null; + this.setPopup(popup); + } + }, + _UIBase_postRender: UIBase.prototype.postRender, + postRender: function (){ + this.Stateful_postRender(); + this._UIBase_postRender(); + }, + setPopup: function (popup){ + if (this.popup === popup) return; + if (this.popup != null) { + this.popup.dispose(); + } + popup.addListener('show', utils.bind(this._onPopupShow, this)); + popup.addListener('hide', utils.bind(this._onPopupHide, this)); + popup.addListener('postrender', utils.bind(function (){ + popup.getDom('body').appendChild( + uiUtils.createElementByHtml('
              ') + ); + popup.getDom().className += ' ' + this.className; + }, this)); + this.popup = popup; + }, + _onPopupShow: function (){ + this.addState('opened'); + }, + _onPopupHide: function (){ + this.removeState('opened'); + }, + getHtmlTpl: function (){ + return '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              '; + }, + showPopup: function (){ + // 当popup往上弹出的时候,做特殊处理 + var rect = uiUtils.getClientRect(this.getDom()); + rect.top -= this.popup.SHADOW_RADIUS; + rect.height += this.popup.SHADOW_RADIUS; + this.popup.showAnchorRect(rect); + }, + _onArrowClick: function (event, el){ + if (!this.isDisabled()) { + this.showPopup(); + } + }, + _onButtonClick: function (){ + if (!this.isDisabled()) { + this.fireEvent('buttonclick'); + } + } + }; + utils.inherits(SplitButton, UIBase); + utils.extend(SplitButton.prototype, Stateful, true); + +})(); + + +// ui/colorbutton.js +///import core +///import uicore +///import ui/colorpicker.js +///import ui/popup.js +///import ui/splitbutton.js +(function (){ + var utils = baidu.editor.utils, + uiUtils = baidu.editor.ui.uiUtils, + ColorPicker = baidu.editor.ui.ColorPicker, + Popup = baidu.editor.ui.Popup, + SplitButton = baidu.editor.ui.SplitButton, + ColorButton = baidu.editor.ui.ColorButton = function (options){ + this.initOptions(options); + this.initColorButton(); + }; + ColorButton.prototype = { + initColorButton: function (){ + var me = this; + this.popup = new Popup({ + content: new ColorPicker({ + noColorText: me.editor.getLang("clearColor"), + editor:me.editor, + onpickcolor: function (t, color){ + me._onPickColor(color); + }, + onpicknocolor: function (t, color){ + me._onPickNoColor(color); + } + }), + editor:me.editor + }); + this.initSplitButton(); + }, + _SplitButton_postRender: SplitButton.prototype.postRender, + postRender: function (){ + this._SplitButton_postRender(); + this.getDom('button_body').appendChild( + uiUtils.createElementByHtml('
              ') + ); + this.getDom().className += ' edui-colorbutton'; + }, + setColor: function (color){ + this.getDom('colorlump').style.backgroundColor = color; + this.color = color; + }, + _onPickColor: function (color){ + if (this.fireEvent('pickcolor', color) !== false) { + this.setColor(color); + this.popup.hide(); + } + }, + _onPickNoColor: function (color){ + if (this.fireEvent('picknocolor') !== false) { + this.popup.hide(); + } + } + }; + utils.inherits(ColorButton, SplitButton); + +})(); + + +// ui/tablebutton.js +///import core +///import uicore +///import ui/popup.js +///import ui/tablepicker.js +///import ui/splitbutton.js +(function (){ + var utils = baidu.editor.utils, + Popup = baidu.editor.ui.Popup, + TablePicker = baidu.editor.ui.TablePicker, + SplitButton = baidu.editor.ui.SplitButton, + TableButton = baidu.editor.ui.TableButton = function (options){ + this.initOptions(options); + this.initTableButton(); + }; + TableButton.prototype = { + initTableButton: function (){ + var me = this; + this.popup = new Popup({ + content: new TablePicker({ + editor:me.editor, + onpicktable: function (t, numCols, numRows){ + me._onPickTable(numCols, numRows); + } + }), + 'editor':me.editor + }); + this.initSplitButton(); + }, + _onPickTable: function (numCols, numRows){ + if (this.fireEvent('picktable', numCols, numRows) !== false) { + this.popup.hide(); + } + } + }; + utils.inherits(TableButton, SplitButton); + +})(); + + +// ui/autotypesetpicker.js +///import core +///import uicore +(function () { + var utils = baidu.editor.utils, + UIBase = baidu.editor.ui.UIBase; + + var AutoTypeSetPicker = baidu.editor.ui.AutoTypeSetPicker = function (options) { + this.initOptions(options); + this.initAutoTypeSetPicker(); + }; + AutoTypeSetPicker.prototype = { + initAutoTypeSetPicker:function () { + this.initUIBase(); + }, + getHtmlTpl:function () { + var me = this.editor, + opt = me.options.autotypeset, + lang = me.getLang("autoTypeSet"); + + var textAlignInputName = 'textAlignValue' + me.uid, + imageBlockInputName = 'imageBlockLineValue' + me.uid, + symbolConverInputName = 'symbolConverValue' + me.uid; + + return '
              ' + + '
              ' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
              ' + lang.mergeLine + '' + lang.delLine + '
              ' + lang.removeFormat + '' + lang.indent + '
              ' + lang.alignment + '' + + '' + me.getLang("justifyleft") + + '' + me.getLang("justifycenter") + + '' + me.getLang("justifyright") + + '
              ' + lang.imageFloat + '' + + '' + me.getLang("default") + + '' + me.getLang("justifyleft") + + '' + me.getLang("justifycenter") + + '' + me.getLang("justifyright") + + '
              ' + lang.removeFontsize + '' + lang.removeFontFamily + '
              ' + lang.removeHtml + '
              ' + lang.pasteFilter + '
              ' + lang.symbol + '' + + '' + lang.bdc2sb + + '' + lang.tobdc + '' + + '
              ' + + '
              ' + + '
              '; + + + }, + _UIBase_render:UIBase.prototype.render + }; + utils.inherits(AutoTypeSetPicker, UIBase); +})(); + + +// ui/autotypesetbutton.js +///import core +///import uicore +///import ui/popup.js +///import ui/autotypesetpicker.js +///import ui/splitbutton.js +(function (){ + var utils = baidu.editor.utils, + Popup = baidu.editor.ui.Popup, + AutoTypeSetPicker = baidu.editor.ui.AutoTypeSetPicker, + SplitButton = baidu.editor.ui.SplitButton, + AutoTypeSetButton = baidu.editor.ui.AutoTypeSetButton = function (options){ + this.initOptions(options); + this.initAutoTypeSetButton(); + }; + function getPara(me){ + + var opt = {}, + cont = me.getDom(), + editorId = me.editor.uid, + inputType = null, + attrName = null, + ipts = domUtils.getElementsByTagName(cont,"input"); + for(var i=ipts.length-1,ipt;ipt=ipts[i--];){ + inputType = ipt.getAttribute("type"); + if(inputType=="checkbox"){ + attrName = ipt.getAttribute("name"); + opt[attrName] && delete opt[attrName]; + if(ipt.checked){ + var attrValue = document.getElementById( attrName + "Value" + editorId ); + if(attrValue){ + if(/input/ig.test(attrValue.tagName)){ + opt[attrName] = attrValue.value; + } else { + var iptChilds = attrValue.getElementsByTagName("input"); + for(var j=iptChilds.length-1,iptchild;iptchild=iptChilds[j--];){ + if(iptchild.checked){ + opt[attrName] = iptchild.value; + break; + } + } + } + } else { + opt[attrName] = true; + } + } else { + opt[attrName] = false; + } + } else { + opt[ipt.getAttribute("value")] = ipt.checked; + } + + } + + var selects = domUtils.getElementsByTagName(cont,"select"); + for(var i=0,si;si=selects[i++];){ + var attr = si.getAttribute('name'); + opt[attr] = opt[attr] ? si.value : ''; + } + + utils.extend(me.editor.options.autotypeset,opt); + + me.editor.setPreferences('autotypeset', opt); + } + + AutoTypeSetButton.prototype = { + initAutoTypeSetButton: function (){ + + var me = this; + this.popup = new Popup({ + //传入配置参数 + content: new AutoTypeSetPicker({editor:me.editor}), + 'editor':me.editor, + hide : function(){ + if (!this._hidden && this.getDom()) { + getPara(this); + this.getDom().style.display = 'none'; + this._hidden = true; + this.fireEvent('hide'); + } + } + }); + var flag = 0; + this.popup.addListener('postRenderAfter',function(){ + var popupUI = this; + if(flag)return; + var cont = this.getDom(), + btn = cont.getElementsByTagName('button')[0]; + + btn.onclick = function(){ + getPara(popupUI); + me.editor.execCommand('autotypeset'); + popupUI.hide() + }; + + domUtils.on(cont, 'click', function(e) { + var target = e.target || e.srcElement, + editorId = me.editor.uid; + if (target && target.tagName == 'INPUT') { + + // 点击图片浮动的checkbox,去除对应的radio + if (target.name == 'imageBlockLine' || target.name == 'textAlign' || target.name == 'symbolConver') { + var checked = target.checked, + radioTd = document.getElementById( target.name + 'Value' + editorId), + radios = radioTd.getElementsByTagName('input'), + defalutSelect = { + 'imageBlockLine': 'none', + 'textAlign': 'left', + 'symbolConver': 'tobdc' + }; + + for (var i = 0; i < radios.length; i++) { + if (checked) { + if (radios[i].value == defalutSelect[target.name]) { + radios[i].checked = 'checked'; + } + } else { + radios[i].checked = false; + } + } + } + // 点击radio,选中对应的checkbox + if (target.name == ('imageBlockLineValue' + editorId) || target.name == ('textAlignValue' + editorId) || target.name == 'bdc') { + var checkboxs = target.parentNode.previousSibling.getElementsByTagName('input'); + checkboxs && (checkboxs[0].checked = true); + } + + getPara(popupUI); + } + }); + + flag = 1; + }); + this.initSplitButton(); + } + }; + utils.inherits(AutoTypeSetButton, SplitButton); + +})(); + + +// ui/cellalignpicker.js +///import core +///import uicore +(function () { + var utils = baidu.editor.utils, + Popup = baidu.editor.ui.Popup, + Stateful = baidu.editor.ui.Stateful, + UIBase = baidu.editor.ui.UIBase; + + /** + * 该参数将新增一个参数: selected, 参数类型为一个Object, 形如{ 'align': 'center', 'valign': 'top' }, 表示单元格的初始 + * 对齐状态为: 竖直居上,水平居中; 其中 align的取值为:'center', 'left', 'right'; valign的取值为: 'top', 'middle', 'bottom' + * @update 2013/4/2 hancong03@baidu.com + */ + var CellAlignPicker = baidu.editor.ui.CellAlignPicker = function (options) { + this.initOptions(options); + this.initSelected(); + this.initCellAlignPicker(); + }; + CellAlignPicker.prototype = { + //初始化选中状态, 该方法将根据传递进来的参数获取到应该选中的对齐方式图标的索引 + initSelected: function(){ + + var status = { + + valign: { + top: 0, + middle: 1, + bottom: 2 + }, + align: { + left: 0, + center: 1, + right: 2 + }, + count: 3 + + }, + result = -1; + + if( this.selected ) { + this.selectedIndex = status.valign[ this.selected.valign ] * status.count + status.align[ this.selected.align ]; + } + + }, + initCellAlignPicker:function () { + this.initUIBase(); + this.Stateful_init(); + }, + getHtmlTpl:function () { + + var alignType = [ 'left', 'center', 'right' ], + COUNT = 9, + tempClassName = null, + tempIndex = -1, + tmpl = []; + + + for( var i= 0; i'); + + tmpl.push( '
              ' ); + + tempIndex === 2 && tmpl.push(''); + + } + + return '
              ' + + '
              ' + + '' + + tmpl.join('') + + '
              ' + + '
              ' + + '
              '; + }, + getStateDom: function (){ + return this.target; + }, + _onClick: function (evt){ + var target= evt.target || evt.srcElement; + if(/icon/.test(target.className)){ + this.items[target.parentNode.getAttribute("index")].onclick(); + Popup.postHide(evt); + } + }, + _UIBase_render:UIBase.prototype.render + }; + utils.inherits(CellAlignPicker, UIBase); + utils.extend(CellAlignPicker.prototype, Stateful,true); +})(); + + + + + +// ui/pastepicker.js +///import core +///import uicore +(function () { + var utils = baidu.editor.utils, + Stateful = baidu.editor.ui.Stateful, + uiUtils = baidu.editor.ui.uiUtils, + UIBase = baidu.editor.ui.UIBase; + + var PastePicker = baidu.editor.ui.PastePicker = function (options) { + this.initOptions(options); + this.initPastePicker(); + }; + PastePicker.prototype = { + initPastePicker:function () { + this.initUIBase(); + this.Stateful_init(); + }, + getHtmlTpl:function () { + return '
              ' + + '
              ' + + '
              ' + this.editor.getLang("pasteOpt") + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + }, + getStateDom:function () { + return this.target; + }, + format:function (param) { + this.editor.ui._isTransfer = true; + this.editor.fireEvent('pasteTransfer', param); + }, + _onClick:function (cur) { + var node = domUtils.getNextDomNode(cur), + screenHt = uiUtils.getViewportRect().height, + subPop = uiUtils.getClientRect(node); + + if ((subPop.top + subPop.height) > screenHt) + node.style.top = (-subPop.height - cur.offsetHeight) + "px"; + else + node.style.top = ""; + + if (/hidden/ig.test(domUtils.getComputedStyle(node, "visibility"))) { + node.style.visibility = "visible"; + domUtils.addClass(cur, "edui-state-opened"); + } else { + node.style.visibility = "hidden"; + domUtils.removeClasses(cur, "edui-state-opened") + } + }, + _UIBase_render:UIBase.prototype.render + }; + utils.inherits(PastePicker, UIBase); + utils.extend(PastePicker.prototype, Stateful, true); +})(); + + + + + + +// ui/toolbar.js +(function (){ + var utils = baidu.editor.utils, + uiUtils = baidu.editor.ui.uiUtils, + UIBase = baidu.editor.ui.UIBase, + Toolbar = baidu.editor.ui.Toolbar = function (options){ + this.initOptions(options); + this.initToolbar(); + }; + Toolbar.prototype = { + items: null, + initToolbar: function (){ + this.items = this.items || []; + this.initUIBase(); + }, + add: function (item,index){ + if(index === undefined){ + this.items.push(item); + }else{ + this.items.splice(index,0,item) + } + + }, + getHtmlTpl: function (){ + var buff = []; + for (var i=0; i' + + buff.join('') + + '
              ' + }, + postRender: function (){ + var box = this.getDom(); + for (var i=0; i
              '; + }, + postRender:function () { + }, + queryAutoHide:function () { + return true; + } + }; + Menu.prototype = { + items:null, + uiName:'menu', + initMenu:function () { + this.items = this.items || []; + this.initPopup(); + this.initItems(); + }, + initItems:function () { + for (var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + if (item == '-') { + this.items[i] = this.getSeparator(); + } else if (!(item instanceof MenuItem)) { + item.editor = this.editor; + item.theme = this.editor.options.theme; + this.items[i] = this.createItem(item); + } + } + }, + getSeparator:function () { + return menuSeparator; + }, + createItem:function (item) { + //新增一个参数menu, 该参数存储了menuItem所对应的menu引用 + item.menu = this; + return new MenuItem(item); + }, + _Popup_getContentHtmlTpl:Popup.prototype.getContentHtmlTpl, + getContentHtmlTpl:function () { + if (this.items.length == 0) { + return this._Popup_getContentHtmlTpl(); + } + var buff = []; + for (var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + buff[i] = item.renderHtml(); + } + return ('
              ' + buff.join('') + '
              '); + }, + _Popup_postRender:Popup.prototype.postRender, + postRender:function () { + var me = this; + for (var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + item.ownerMenu = this; + item.postRender(); + } + domUtils.on(this.getDom(), 'mouseover', function (evt) { + evt = evt || event; + var rel = evt.relatedTarget || evt.fromElement; + var el = me.getDom(); + if (!uiUtils.contains(el, rel) && el !== rel) { + me.fireEvent('over'); + } + }); + this._Popup_postRender(); + }, + queryAutoHide:function (el) { + if (el) { + if (uiUtils.contains(this.getDom(), el)) { + return false; + } + for (var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + if (item.queryAutoHide(el) === false) { + return false; + } + } + } + }, + clearItems:function () { + for (var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + clearTimeout(item._showingTimer); + clearTimeout(item._closingTimer); + if (item.subMenu) { + item.subMenu.destroy(); + } + } + this.items = []; + }, + destroy:function () { + if (this.getDom()) { + domUtils.remove(this.getDom()); + } + this.clearItems(); + }, + dispose:function () { + this.destroy(); + } + }; + utils.inherits(Menu, Popup); + + /** + * @update 2013/04/03 hancong03 新增一个参数menu, 该参数存储了menuItem所对应的menu引用 + * @type {Function} + */ + var MenuItem = baidu.editor.ui.MenuItem = function (options) { + this.initOptions(options); + this.initUIBase(); + this.Stateful_init(); + if (this.subMenu && !(this.subMenu instanceof Menu)) { + if (options.className && options.className.indexOf("aligntd") != -1) { + var me = this; + + //获取单元格对齐初始状态 + this.subMenu.selected = this.editor.queryCommandValue( 'cellalignment' ); + + this.subMenu = new Popup({ + content:new CellAlignPicker(this.subMenu), + parentMenu:me, + editor:me.editor, + destroy:function () { + if (this.getDom()) { + domUtils.remove(this.getDom()); + } + } + }); + this.subMenu.addListener("postRenderAfter", function () { + domUtils.on(this.getDom(), "mouseover", function () { + me.addState('opened'); + }); + }); + } else { + this.subMenu = new Menu(this.subMenu); + } + } + }; + MenuItem.prototype = { + label:'', + subMenu:null, + ownerMenu:null, + uiName:'menuitem', + alwalysHoverable:true, + getHtmlTpl:function () { + return '
              ' + + '
              ' + + this.renderLabelHtml() + + '
              ' + + '
              '; + }, + postRender:function () { + var me = this; + this.addListener('over', function () { + me.ownerMenu.fireEvent('submenuover', me); + if (me.subMenu) { + me.delayShowSubMenu(); + } + }); + if (this.subMenu) { + this.getDom().className += ' edui-hassubmenu'; + this.subMenu.render(); + this.addListener('out', function () { + me.delayHideSubMenu(); + }); + this.subMenu.addListener('over', function () { + clearTimeout(me._closingTimer); + me._closingTimer = null; + me.addState('opened'); + }); + this.ownerMenu.addListener('hide', function () { + me.hideSubMenu(); + }); + this.ownerMenu.addListener('submenuover', function (t, subMenu) { + if (subMenu !== me) { + me.delayHideSubMenu(); + } + }); + this.subMenu._bakQueryAutoHide = this.subMenu.queryAutoHide; + this.subMenu.queryAutoHide = function (el) { + if (el && uiUtils.contains(me.getDom(), el)) { + return false; + } + return this._bakQueryAutoHide(el); + }; + } + this.getDom().style.tabIndex = '-1'; + uiUtils.makeUnselectable(this.getDom()); + this.Stateful_postRender(); + }, + delayShowSubMenu:function () { + var me = this; + if (!me.isDisabled()) { + me.addState('opened'); + clearTimeout(me._showingTimer); + clearTimeout(me._closingTimer); + me._closingTimer = null; + me._showingTimer = setTimeout(function () { + me.showSubMenu(); + }, 250); + } + }, + delayHideSubMenu:function () { + var me = this; + if (!me.isDisabled()) { + me.removeState('opened'); + clearTimeout(me._showingTimer); + if (!me._closingTimer) { + me._closingTimer = setTimeout(function () { + if (!me.hasState('opened')) { + me.hideSubMenu(); + } + me._closingTimer = null; + }, 400); + } + } + }, + renderLabelHtml:function () { + return '
              ' + + '
              ' + + '
              ' + (this.label || '') + '
              '; + }, + getStateDom:function () { + return this.getDom(); + }, + queryAutoHide:function (el) { + if (this.subMenu && this.hasState('opened')) { + return this.subMenu.queryAutoHide(el); + } + }, + _onClick:function (event, this_) { + if (this.hasState('disabled')) return; + if (this.fireEvent('click', event, this_) !== false) { + if (this.subMenu) { + this.showSubMenu(); + } else { + Popup.postHide(event); + } + } + }, + showSubMenu:function () { + var rect = uiUtils.getClientRect(this.getDom()); + rect.right -= 5; + rect.left += 2; + rect.width -= 7; + rect.top -= 4; + rect.bottom += 4; + rect.height += 8; + this.subMenu.showAnchorRect(rect, true, true); + }, + hideSubMenu:function () { + this.subMenu.hide(); + } + }; + utils.inherits(MenuItem, UIBase); + utils.extend(MenuItem.prototype, Stateful, true); +})(); + + +// ui/combox.js +///import core +///import uicore +///import ui/menu.js +///import ui/splitbutton.js +(function (){ + // todo: menu和item提成通用list + var utils = baidu.editor.utils, + uiUtils = baidu.editor.ui.uiUtils, + Menu = baidu.editor.ui.Menu, + SplitButton = baidu.editor.ui.SplitButton, + Combox = baidu.editor.ui.Combox = function (options){ + this.initOptions(options); + this.initCombox(); + }; + Combox.prototype = { + uiName: 'combox', + onbuttonclick:function () { + this.showPopup(); + }, + initCombox: function (){ + var me = this; + this.items = this.items || []; + for (var i=0; i vpRect.right) { + left = vpRect.right - rect.width; + } + var top = offset.top; + if (top + rect.height > vpRect.bottom) { + top = vpRect.bottom - rect.height; + } + el.style.left = Math.max(left, 0) + 'px'; + el.style.top = Math.max(top, 0) + 'px'; + }, + showAtCenter: function (){ + + var vpRect = uiUtils.getViewportRect(); + + if ( !this.fullscreen ) { + this.getDom().style.display = ''; + var popSize = this.fitSize(); + var titleHeight = this.getDom('titlebar').offsetHeight | 0; + var left = vpRect.width / 2 - popSize.width / 2; + var top = vpRect.height / 2 - (popSize.height - titleHeight) / 2 - titleHeight; + var popEl = this.getDom(); + this.safeSetOffset({ + left: Math.max(left | 0, 0), + top: Math.max(top | 0, 0) + }); + if (!domUtils.hasClass(popEl, 'edui-state-centered')) { + popEl.className += ' edui-state-centered'; + } + } else { + var dialogWrapNode = this.getDom(), + contentNode = this.getDom('content'); + + dialogWrapNode.style.display = "block"; + + var wrapRect = UE.ui.uiUtils.getClientRect( dialogWrapNode ), + contentRect = UE.ui.uiUtils.getClientRect( contentNode ); + dialogWrapNode.style.left = "-100000px"; + + contentNode.style.width = ( vpRect.width - wrapRect.width + contentRect.width ) + "px"; + contentNode.style.height = ( vpRect.height - wrapRect.height + contentRect.height ) + "px"; + + dialogWrapNode.style.width = vpRect.width + "px"; + dialogWrapNode.style.height = vpRect.height + "px"; + dialogWrapNode.style.left = 0; + + //保存环境的overflow值 + this._originalContext = { + html: { + overflowX: document.documentElement.style.overflowX, + overflowY: document.documentElement.style.overflowY + }, + body: { + overflowX: document.body.style.overflowX, + overflowY: document.body.style.overflowY + } + }; + + document.documentElement.style.overflowX = 'hidden'; + document.documentElement.style.overflowY = 'hidden'; + document.body.style.overflowX = 'hidden'; + document.body.style.overflowY = 'hidden'; + + } + + this._show(); + }, + getContentHtml: function (){ + var contentHtml = ''; + if (typeof this.content == 'string') { + contentHtml = this.content; + } else if (this.iframeUrl) { + contentHtml = ''; + } + return contentHtml; + }, + getHtmlTpl: function (){ + var footHtml = ''; + + if (this.buttons) { + var buff = []; + for (var i=0; i' + buff.join('') + '
              ' + + '
              '; + } + + return '
              ' + + '
              ' + + '
              ' + + '
              ' + + '' + (this.title || '') + '' + + '
              ' + + this.closeButton.renderHtml() + + '
              ' + + '
              '+ ( this.autoReset ? '' : this.getContentHtml()) +'
              ' + + footHtml + + '
              '; + }, + postRender: function (){ + // todo: 保持居中/记住上次关闭位置选项 + if (!this.modalMask.getDom()) { + this.modalMask.render(); + this.modalMask.hide(); + } + if (!this.dragMask.getDom()) { + this.dragMask.render(); + this.dragMask.hide(); + } + var me = this; + this.addListener('show', function (){ + me.modalMask.show(this.getDom().style.zIndex - 2); + }); + this.addListener('hide', function (){ + me.modalMask.hide(); + }); + if (this.buttons) { + for (var i=0; i'; + me.editor.container.style.zIndex && (this.getDom().style.zIndex = me.editor.container.style.zIndex * 1 + 1); + } + } + // canSideUp:false, + // canSideLeft:false + }); + this.onbuttonclick = function(){ + this.showPopup(); + }; + this.initSplitButton(); + } + + }; + + utils.inherits(MultiMenuPop, SplitButton); +})(); + + +// ui/shortcutmenu.js +(function () { + var UI = baidu.editor.ui, + UIBase = UI.UIBase, + uiUtils = UI.uiUtils, + utils = baidu.editor.utils, + domUtils = baidu.editor.dom.domUtils; + + var allMenus = [],//存储所有快捷菜单 + timeID, + isSubMenuShow = false;//是否有子pop显示 + + var ShortCutMenu = UI.ShortCutMenu = function (options) { + this.initOptions (options); + this.initShortCutMenu (); + }; + + ShortCutMenu.postHide = hideAllMenu; + + ShortCutMenu.prototype = { + isHidden : true , + SPACE : 5 , + initShortCutMenu : function () { + this.items = this.items || []; + this.initUIBase (); + this.initItems (); + this.initEvent (); + allMenus.push (this); + } , + initEvent : function () { + var me = this, + doc = me.editor.document; + + domUtils.on (doc , "mousemove" , function (e) { + if (me.isHidden === false) { + //有pop显示就不隐藏快捷菜单 + if (me.getSubMenuMark () || me.eventType == "contextmenu") return; + + + var flag = true, + el = me.getDom (), + wt = el.offsetWidth, + ht = el.offsetHeight, + distanceX = wt / 2 + me.SPACE,//距离中心X标准 + distanceY = ht / 2,//距离中心Y标准 + x = Math.abs (e.screenX - me.left),//离中心距离横坐标 + y = Math.abs (e.screenY - me.top);//离中心距离纵坐标 + + clearTimeout (timeID); + timeID = setTimeout (function () { + if (y > 0 && y < distanceY) { + me.setOpacity (el , "1"); + } else if (y > distanceY && y < distanceY + 70) { + me.setOpacity (el , "0.5"); + flag = false; + } else if (y > distanceY + 70 && y < distanceY + 140) { + me.hide (); + } + + if (flag && x > 0 && x < distanceX) { + me.setOpacity (el , "1") + } else if (x > distanceX && x < distanceX + 70) { + me.setOpacity (el , "0.5") + } else if (x > distanceX + 70 && x < distanceX + 140) { + me.hide (); + } + }); + } + }); + + //ie\ff下 mouseout不准 + if (browser.chrome) { + domUtils.on (doc , "mouseout" , function (e) { + var relatedTgt = e.relatedTarget || e.toElement; + + if (relatedTgt == null || relatedTgt.tagName == "HTML") { + me.hide (); + } + }); + } + + me.editor.addListener ("afterhidepop" , function () { + if (!me.isHidden) { + isSubMenuShow = true; + } + }); + + } , + initItems : function () { + if (utils.isArray (this.items)) { + for (var i = 0, len = this.items.length ; i < len ; i++) { + var item = this.items[i].toLowerCase (); + + if (UI[item]) { + this.items[i] = new UI[item] (this.editor); + this.items[i].className += " edui-shortcutsubmenu "; + } + } + } + } , + setOpacity : function (el , value) { + if (browser.ie && browser.version < 9) { + el.style.filter = "alpha(opacity = " + parseFloat (value) * 100 + ");" + } else { + el.style.opacity = value; + } + } , + getSubMenuMark : function () { + isSubMenuShow = false; + var layerEle = uiUtils.getFixedLayer (); + var list = domUtils.getElementsByTagName (layerEle , "div" , function (node) { + return domUtils.hasClass (node , "edui-shortcutsubmenu edui-popup") + }); + + for (var i = 0, node ; node = list[i++] ;) { + if (node.style.display != "none") { + isSubMenuShow = true; + } + } + return isSubMenuShow; + } , + show : function (e , hasContextmenu) { + var me = this, + offset = {}, + el = this.getDom (), + fixedlayer = uiUtils.getFixedLayer (); + + function setPos (offset) { + if (offset.left < 0) { + offset.left = 0; + } + if (offset.top < 0) { + offset.top = 0; + } + el.style.cssText = "position:absolute;left:" + offset.left + "px;top:" + offset.top + "px;"; + } + + function setPosByCxtMenu (menu) { + if (!menu.tagName) { + menu = menu.getDom (); + } + offset.left = parseInt (menu.style.left); + offset.top = parseInt (menu.style.top); + offset.top -= el.offsetHeight + 15; + setPos (offset); + } + + + me.eventType = e.type; + el.style.cssText = "display:block;left:-9999px"; + + if (e.type == "contextmenu" && hasContextmenu) { + var menu = domUtils.getElementsByTagName (fixedlayer , "div" , "edui-contextmenu")[0]; + if (menu) { + setPosByCxtMenu (menu) + } else { + me.editor.addListener ("aftershowcontextmenu" , function (type , menu) { + setPosByCxtMenu (menu); + }); + } + } else { + offset = uiUtils.getViewportOffsetByEvent (e); + offset.top -= el.offsetHeight + me.SPACE; + offset.left += me.SPACE + 20; + setPos (offset); + me.setOpacity (el , 0.2); + } + + + me.isHidden = false; + me.left = e.screenX + el.offsetWidth / 2 - me.SPACE; + me.top = e.screenY - (el.offsetHeight / 2) - me.SPACE; + + if (me.editor) { + el.style.zIndex = me.editor.container.style.zIndex * 1 + 10; + fixedlayer.style.zIndex = el.style.zIndex - 1; + } + } , + hide : function () { + if (this.getDom ()) { + this.getDom ().style.display = "none"; + } + this.isHidden = true; + } , + postRender : function () { + if (utils.isArray (this.items)) { + for (var i = 0, item ; item = this.items[i++] ;) { + item.postRender (); + } + } + } , + getHtmlTpl : function () { + var buff; + if (utils.isArray (this.items)) { + buff = []; + for (var i = 0 ; i < this.items.length ; i++) { + buff[i] = this.items[i].renderHtml (); + } + buff = buff.join (""); + } else { + buff = this.items; + } + + return '
              ' + + buff + + '
              '; + } + }; + + utils.inherits (ShortCutMenu , UIBase); + + function hideAllMenu (e) { + var tgt = e.target || e.srcElement, + cur = domUtils.findParent (tgt , function (node) { + return domUtils.hasClass (node , "edui-shortcutmenu") || domUtils.hasClass (node , "edui-popup"); + } , true); + + if (!cur) { + for (var i = 0, menu ; menu = allMenus[i++] ;) { + menu.hide () + } + } + } + + domUtils.on (document , 'mousedown' , function (e) { + hideAllMenu (e); + }); + + domUtils.on (window , 'scroll' , function (e) { + hideAllMenu (e); + }); + +}) (); + + +// ui/breakline.js +(function (){ + var utils = baidu.editor.utils, + UIBase = baidu.editor.ui.UIBase, + Breakline = baidu.editor.ui.Breakline = function (options){ + this.initOptions(options); + this.initSeparator(); + }; + Breakline.prototype = { + uiName: 'Breakline', + initSeparator: function (){ + this.initUIBase(); + }, + getHtmlTpl: function (){ + return '
              '; + } + }; + utils.inherits(Breakline, UIBase); + +})(); + + +// ui/message.js +///import core +///import uicore +(function () { + var utils = baidu.editor.utils, + domUtils = baidu.editor.dom.domUtils, + UIBase = baidu.editor.ui.UIBase, + Message = baidu.editor.ui.Message = function (options){ + this.initOptions(options); + this.initMessage(); + }; + + Message.prototype = { + initMessage: function (){ + this.initUIBase(); + }, + getHtmlTpl: function (){ + return '
              ' + + '
              ×
              ' + + '
              ' + + ' ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              '; + }, + reset: function(opt){ + var me = this; + if (!opt.keepshow) { + clearTimeout(this.timer); + me.timer = setTimeout(function(){ + me.hide(); + }, opt.timeout || 4000); + } + + opt.content !== undefined && me.setContent(opt.content); + opt.type !== undefined && me.setType(opt.type); + + me.show(); + }, + postRender: function(){ + var me = this, + closer = this.getDom('closer'); + closer && domUtils.on(closer, 'click', function(){ + me.hide(); + }); + }, + setContent: function(content){ + this.getDom('content').innerHTML = content; + }, + setType: function(type){ + type = type || 'info'; + var body = this.getDom('body'); + body.className = body.className.replace(/edui-message-type-[\w-]+/, 'edui-message-type-' + type); + }, + getContent: function(){ + return this.getDom('content').innerHTML; + }, + getType: function(){ + var arr = this.getDom('body').match(/edui-message-type-([\w-]+)/); + return arr ? arr[1]:''; + }, + show: function (){ + this.getDom().style.display = 'block'; + }, + hide: function (){ + var dom = this.getDom(); + if (dom) { + dom.style.display = 'none'; + dom.parentNode && dom.parentNode.removeChild(dom); + } + } + }; + + utils.inherits(Message, UIBase); + +})(); + + +// adapter/editorui.js +//ui跟编辑器的适配層 +//那个按钮弹出是dialog,是下拉筐等都是在这个js中配置 +//自己写的ui也要在这里配置,放到baidu.editor.ui下边,当编辑器实例化的时候会根据ueditor.config中的toolbars找到相应的进行实例化 +(function () { + var utils = baidu.editor.utils; + var editorui = baidu.editor.ui; + var _Dialog = editorui.Dialog; + editorui.buttons = {}; + + editorui.Dialog = function (options) { + var dialog = new _Dialog(options); + dialog.addListener('hide', function () { + + if (dialog.editor) { + var editor = dialog.editor; + try { + if (browser.gecko) { + var y = editor.window.scrollY, + x = editor.window.scrollX; + editor.body.focus(); + editor.window.scrollTo(x, y); + } else { + editor.focus(); + } + + + } catch (ex) { + } + } + }); + return dialog; + }; + + var iframeUrlMap = { + 'anchor':'~/dialogs/anchor/anchor.html', + 'insertimage':'~/dialogs/image/image.html', + 'link':'~/dialogs/link/link.html', + 'spechars':'~/dialogs/spechars/spechars.html', + 'searchreplace':'~/dialogs/searchreplace/searchreplace.html', + 'map':'~/dialogs/map/map.html', + 'gmap':'~/dialogs/gmap/gmap.html', + 'insertvideo':'~/dialogs/video/video.html', + 'help':'~/dialogs/help/help.html', + 'preview':'~/dialogs/preview/preview.html', + 'emotion':'~/dialogs/emotion/emotion.html', + 'wordimage':'~/dialogs/wordimage/wordimage.html', + 'attachment':'~/dialogs/attachment/attachment.html', + 'insertframe':'~/dialogs/insertframe/insertframe.html', + 'edittip':'~/dialogs/table/edittip.html', + 'edittable':'~/dialogs/table/edittable.html', + 'edittd':'~/dialogs/table/edittd.html', + 'webapp':'~/dialogs/webapp/webapp.html', + 'snapscreen':'~/dialogs/snapscreen/snapscreen.html', + 'scrawl':'~/dialogs/scrawl/scrawl.html', + 'music':'~/dialogs/music/music.html', + 'template':'~/dialogs/template/template.html', + 'background':'~/dialogs/background/background.html', + 'charts': '~/dialogs/charts/charts.html' + }; + //为工具栏添加按钮,以下都是统一的按钮触发命令,所以写在一起 + var btnCmds = ['undo', 'redo', 'formatmatch', + 'bold', 'italic', 'underline', 'fontborder', 'touppercase', 'tolowercase', + 'strikethrough', 'subscript', 'superscript', 'source', 'indent', 'outdent', + 'blockquote', 'pasteplain', 'pagebreak', + 'selectall', 'print','horizontal', 'removeformat', 'time', 'date', 'unlink', + 'insertparagraphbeforetable', 'insertrow', 'insertcol', 'mergeright', 'mergedown', 'deleterow', + 'deletecol', 'splittorows', 'splittocols', 'splittocells', 'mergecells', 'deletetable', 'drafts']; + + for (var i = 0, ci; ci = btnCmds[i++];) { + ci = ci.toLowerCase(); + editorui[ci] = function (cmd) { + return function (editor) { + var ui = new editorui.Button({ + className:'edui-for-' + cmd, + title:editor.options.labelMap[cmd] || editor.getLang("labelMap." + cmd) || '', + onclick:function () { + editor.execCommand(cmd); + }, + theme:editor.options.theme, + showText:false + }); + editorui.buttons[cmd] = ui; + editor.addListener('selectionchange', function (type, causeByUi, uiReady) { + var state = editor.queryCommandState(cmd); + if (state == -1) { + ui.setDisabled(true); + ui.setChecked(false); + } else { + if (!uiReady) { + ui.setDisabled(false); + ui.setChecked(state); + } + } + }); + return ui; + }; + }(ci); + } + + //清除文档 + editorui.cleardoc = function (editor) { + var ui = new editorui.Button({ + className:'edui-for-cleardoc', + title:editor.options.labelMap.cleardoc || editor.getLang("labelMap.cleardoc") || '', + theme:editor.options.theme, + onclick:function () { + if (confirm(editor.getLang("confirmClear"))) { + editor.execCommand('cleardoc'); + } + } + }); + editorui.buttons["cleardoc"] = ui; + editor.addListener('selectionchange', function () { + ui.setDisabled(editor.queryCommandState('cleardoc') == -1); + }); + return ui; + }; + + //排版,图片排版,文字方向 + var typeset = { + 'justify':['left', 'right', 'center', 'justify'], + 'imagefloat':['none', 'left', 'center', 'right'], + 'directionality':['ltr', 'rtl'] + }; + + for (var p in typeset) { + + (function (cmd, val) { + for (var i = 0, ci; ci = val[i++];) { + (function (cmd2) { + editorui[cmd.replace('float', '') + cmd2] = function (editor) { + var ui = new editorui.Button({ + className:'edui-for-' + cmd.replace('float', '') + cmd2, + title:editor.options.labelMap[cmd.replace('float', '') + cmd2] || editor.getLang("labelMap." + cmd.replace('float', '') + cmd2) || '', + theme:editor.options.theme, + onclick:function () { + editor.execCommand(cmd, cmd2); + } + }); + editorui.buttons[cmd] = ui; + editor.addListener('selectionchange', function (type, causeByUi, uiReady) { + ui.setDisabled(editor.queryCommandState(cmd) == -1); + ui.setChecked(editor.queryCommandValue(cmd) == cmd2 && !uiReady); + }); + return ui; + }; + })(ci) + } + })(p, typeset[p]) + } + + //字体颜色和背景颜色 + for (var i = 0, ci; ci = ['backcolor', 'forecolor'][i++];) { + editorui[ci] = function (cmd) { + return function (editor) { + var ui = new editorui.ColorButton({ + className:'edui-for-' + cmd, + color:'default', + title:editor.options.labelMap[cmd] || editor.getLang("labelMap." + cmd) || '', + editor:editor, + onpickcolor:function (t, color) { + editor.execCommand(cmd, color); + }, + onpicknocolor:function () { + editor.execCommand(cmd, 'default'); + this.setColor('transparent'); + this.color = 'default'; + }, + onbuttonclick:function () { + editor.execCommand(cmd, this.color); + } + }); + editorui.buttons[cmd] = ui; + editor.addListener('selectionchange', function () { + ui.setDisabled(editor.queryCommandState(cmd) == -1); + }); + return ui; + }; + }(ci); + } + + + var dialogBtns = { + noOk:['searchreplace', 'help', 'spechars', 'webapp','preview'], + ok:['attachment', 'anchor', 'link', 'insertimage', 'map', 'gmap', 'insertframe', 'wordimage', + 'insertvideo', 'insertframe', 'edittip', 'edittable', 'edittd', 'scrawl', 'template', 'music', 'background', 'charts'] + }; + + for (var p in dialogBtns) { + (function (type, vals) { + for (var i = 0, ci; ci = vals[i++];) { + //todo opera下存在问题 + if (browser.opera && ci === "searchreplace") { + continue; + } + (function (cmd) { + editorui[cmd] = function (editor, iframeUrl, title) { + iframeUrl = iframeUrl || (editor.options.iframeUrlMap || {})[cmd] || iframeUrlMap[cmd]; + title = editor.options.labelMap[cmd] || editor.getLang("labelMap." + cmd) || ''; + + var dialog; + //没有iframeUrl不创建dialog + if (iframeUrl) { + dialog = new editorui.Dialog(utils.extend({ + iframeUrl:editor.ui.mapUrl(iframeUrl), + editor:editor, + className:'edui-for-' + cmd, + title:title, + holdScroll: cmd === 'insertimage', + fullscreen: /charts|preview/.test(cmd), + closeDialog:editor.getLang("closeDialog") + }, type == 'ok' ? { + buttons:[ + { + className:'edui-okbutton', + label:editor.getLang("ok"), + editor:editor, + onclick:function () { + dialog.close(true); + } + }, + { + className:'edui-cancelbutton', + label:editor.getLang("cancel"), + editor:editor, + onclick:function () { + dialog.close(false); + } + } + ] + } : {})); + + editor.ui._dialogs[cmd + "Dialog"] = dialog; + } + + var ui = new editorui.Button({ + className:'edui-for-' + cmd, + title:title, + onclick:function () { + if (dialog) { + switch (cmd) { + case "wordimage": + var images = editor.execCommand("wordimage"); + if (images && images.length) { + dialog.render(); + dialog.open(); + } + break; + case "scrawl": + if (editor.queryCommandState("scrawl") != -1) { + dialog.render(); + dialog.open(); + } + + break; + default: + dialog.render(); + dialog.open(); + } + } + }, + theme:editor.options.theme, + disabled:(cmd == 'scrawl' && editor.queryCommandState("scrawl") == -1) || ( cmd == 'charts' ) + }); + editorui.buttons[cmd] = ui; + editor.addListener('selectionchange', function () { + //只存在于右键菜单而无工具栏按钮的ui不需要检测状态 + var unNeedCheckState = {'edittable':1}; + if (cmd in unNeedCheckState)return; + + var state = editor.queryCommandState(cmd); + if (ui.getDom()) { + ui.setDisabled(state == -1); + ui.setChecked(state); + } + + }); + + return ui; + }; + })(ci.toLowerCase()) + } + })(p, dialogBtns[p]); + } + + editorui.snapscreen = function (editor, iframeUrl, title) { + title = editor.options.labelMap['snapscreen'] || editor.getLang("labelMap.snapscreen") || ''; + var ui = new editorui.Button({ + className:'edui-for-snapscreen', + title:title, + onclick:function () { + editor.execCommand("snapscreen"); + }, + theme:editor.options.theme + + }); + editorui.buttons['snapscreen'] = ui; + iframeUrl = iframeUrl || (editor.options.iframeUrlMap || {})["snapscreen"] || iframeUrlMap["snapscreen"]; + if (iframeUrl) { + var dialog = new editorui.Dialog({ + iframeUrl:editor.ui.mapUrl(iframeUrl), + editor:editor, + className:'edui-for-snapscreen', + title:title, + buttons:[ + { + className:'edui-okbutton', + label:editor.getLang("ok"), + editor:editor, + onclick:function () { + dialog.close(true); + } + }, + { + className:'edui-cancelbutton', + label:editor.getLang("cancel"), + editor:editor, + onclick:function () { + dialog.close(false); + } + } + ] + + }); + dialog.render(); + editor.ui._dialogs["snapscreenDialog"] = dialog; + } + editor.addListener('selectionchange', function () { + ui.setDisabled(editor.queryCommandState('snapscreen') == -1); + }); + return ui; + }; + + editorui.insertcode = function (editor, list, title) { + list = editor.options['insertcode'] || []; + title = editor.options.labelMap['insertcode'] || editor.getLang("labelMap.insertcode") || ''; + // if (!list.length) return; + var items = []; + utils.each(list,function(key,val){ + items.push({ + label:key, + value:val, + theme:editor.options.theme, + renderLabelHtml:function () { + return '
              ' + (this.label || '') + '
              '; + } + }); + }); + + var ui = new editorui.Combox({ + editor:editor, + items:items, + onselect:function (t, index) { + editor.execCommand('insertcode', this.items[index].value); + }, + onbuttonclick:function () { + this.showPopup(); + }, + title:title, + initValue:title, + className:'edui-for-insertcode', + indexByValue:function (value) { + if (value) { + for (var i = 0, ci; ci = this.items[i]; i++) { + if (ci.value.indexOf(value) != -1) + return i; + } + } + + return -1; + } + }); + editorui.buttons['insertcode'] = ui; + editor.addListener('selectionchange', function (type, causeByUi, uiReady) { + if (!uiReady) { + var state = editor.queryCommandState('insertcode'); + if (state == -1) { + ui.setDisabled(true); + } else { + ui.setDisabled(false); + var value = editor.queryCommandValue('insertcode'); + if(!value){ + ui.setValue(title); + return; + } + //trace:1871 ie下从源码模式切换回来时,字体会带单引号,而且会有逗号 + value && (value = value.replace(/['"]/g, '').split(',')[0]); + ui.setValue(value); + + } + } + + }); + return ui; + }; + editorui.fontfamily = function (editor, list, title) { + + list = editor.options['fontfamily'] || []; + title = editor.options.labelMap['fontfamily'] || editor.getLang("labelMap.fontfamily") || ''; + if (!list.length) return; + for (var i = 0, ci, items = []; ci = list[i]; i++) { + var langLabel = editor.getLang('fontfamily')[ci.name] || ""; + (function (key, val) { + items.push({ + label:key, + value:val, + theme:editor.options.theme, + renderLabelHtml:function () { + return '
              ' + (this.label || '') + '
              '; + } + }); + })(ci.label || langLabel, ci.val) + } + var ui = new editorui.Combox({ + editor:editor, + items:items, + onselect:function (t, index) { + editor.execCommand('FontFamily', this.items[index].value); + }, + onbuttonclick:function () { + this.showPopup(); + }, + title:title, + initValue:title, + className:'edui-for-fontfamily', + indexByValue:function (value) { + if (value) { + for (var i = 0, ci; ci = this.items[i]; i++) { + if (ci.value.indexOf(value) != -1) + return i; + } + } + + return -1; + } + }); + editorui.buttons['fontfamily'] = ui; + editor.addListener('selectionchange', function (type, causeByUi, uiReady) { + if (!uiReady) { + var state = editor.queryCommandState('FontFamily'); + if (state == -1) { + ui.setDisabled(true); + } else { + ui.setDisabled(false); + var value = editor.queryCommandValue('FontFamily'); + //trace:1871 ie下从源码模式切换回来时,字体会带单引号,而且会有逗号 + value && (value = value.replace(/['"]/g, '').split(',')[0]); + ui.setValue(value); + + } + } + + }); + return ui; + }; + + editorui.fontsize = function (editor, list, title) { + title = editor.options.labelMap['fontsize'] || editor.getLang("labelMap.fontsize") || ''; + list = list || editor.options['fontsize'] || []; + if (!list.length) return; + var items = []; + for (var i = 0; i < list.length; i++) { + var size = list[i] + 'px'; + items.push({ + label:size, + value:size, + theme:editor.options.theme, + renderLabelHtml:function () { + return '
              ' + (this.label || '') + '
              '; + } + }); + } + var ui = new editorui.Combox({ + editor:editor, + items:items, + title:title, + initValue:title, + onselect:function (t, index) { + editor.execCommand('FontSize', this.items[index].value); + }, + onbuttonclick:function () { + this.showPopup(); + }, + className:'edui-for-fontsize' + }); + editorui.buttons['fontsize'] = ui; + editor.addListener('selectionchange', function (type, causeByUi, uiReady) { + if (!uiReady) { + var state = editor.queryCommandState('FontSize'); + if (state == -1) { + ui.setDisabled(true); + } else { + ui.setDisabled(false); + ui.setValue(editor.queryCommandValue('FontSize')); + } + } + + }); + return ui; + }; + + editorui.paragraph = function (editor, list, title) { + title = editor.options.labelMap['paragraph'] || editor.getLang("labelMap.paragraph") || ''; + list = editor.options['paragraph'] || []; + if (utils.isEmptyObject(list)) return; + var items = []; + for (var i in list) { + items.push({ + value:i, + label:list[i] || editor.getLang("paragraph")[i], + theme:editor.options.theme, + renderLabelHtml:function () { + return '
              ' + (this.label || '') + '
              '; + } + }) + } + var ui = new editorui.Combox({ + editor:editor, + items:items, + title:title, + initValue:title, + className:'edui-for-paragraph', + onselect:function (t, index) { + editor.execCommand('Paragraph', this.items[index].value); + }, + onbuttonclick:function () { + this.showPopup(); + } + }); + editorui.buttons['paragraph'] = ui; + editor.addListener('selectionchange', function (type, causeByUi, uiReady) { + if (!uiReady) { + var state = editor.queryCommandState('Paragraph'); + if (state == -1) { + ui.setDisabled(true); + } else { + ui.setDisabled(false); + var value = editor.queryCommandValue('Paragraph'); + var index = ui.indexByValue(value); + if (index != -1) { + ui.setValue(value); + } else { + ui.setValue(ui.initValue); + } + } + } + + }); + return ui; + }; + + + //自定义标题 + editorui.customstyle = function (editor) { + var list = editor.options['customstyle'] || [], + title = editor.options.labelMap['customstyle'] || editor.getLang("labelMap.customstyle") || ''; + if (!list.length)return; + var langCs = editor.getLang('customstyle'); + for (var i = 0, items = [], t; t = list[i++];) { + (function (t) { + var ck = {}; + ck.label = t.label ? t.label : langCs[t.name]; + ck.style = t.style; + ck.className = t.className; + ck.tag = t.tag; + items.push({ + label:ck.label, + value:ck, + theme:editor.options.theme, + renderLabelHtml:function () { + return '
              ' + '<' + ck.tag + ' ' + (ck.className ? ' class="' + ck.className + '"' : "") + + (ck.style ? ' style="' + ck.style + '"' : "") + '>' + ck.label + "<\/" + ck.tag + ">" + + '
              '; + } + }); + })(t); + } + + var ui = new editorui.Combox({ + editor:editor, + items:items, + title:title, + initValue:title, + className:'edui-for-customstyle', + onselect:function (t, index) { + editor.execCommand('customstyle', this.items[index].value); + }, + onbuttonclick:function () { + this.showPopup(); + }, + indexByValue:function (value) { + for (var i = 0, ti; ti = this.items[i++];) { + if (ti.label == value) { + return i - 1 + } + } + return -1; + } + }); + editorui.buttons['customstyle'] = ui; + editor.addListener('selectionchange', function (type, causeByUi, uiReady) { + if (!uiReady) { + var state = editor.queryCommandState('customstyle'); + if (state == -1) { + ui.setDisabled(true); + } else { + ui.setDisabled(false); + var value = editor.queryCommandValue('customstyle'); + var index = ui.indexByValue(value); + if (index != -1) { + ui.setValue(value); + } else { + ui.setValue(ui.initValue); + } + } + } + + }); + return ui; + }; + editorui.inserttable = function (editor, iframeUrl, title) { + title = editor.options.labelMap['inserttable'] || editor.getLang("labelMap.inserttable") || ''; + var ui = new editorui.TableButton({ + editor:editor, + title:title, + className:'edui-for-inserttable', + onpicktable:function (t, numCols, numRows) { + editor.execCommand('InsertTable', {numRows:numRows, numCols:numCols, border:1}); + }, + onbuttonclick:function () { + this.showPopup(); + } + }); + editorui.buttons['inserttable'] = ui; + editor.addListener('selectionchange', function () { + ui.setDisabled(editor.queryCommandState('inserttable') == -1); + }); + return ui; + }; + + editorui.lineheight = function (editor) { + var val = editor.options.lineheight || []; + if (!val.length)return; + for (var i = 0, ci, items = []; ci = val[i++];) { + items.push({ + //todo:写死了 + label:ci, + value:ci, + theme:editor.options.theme, + onclick:function () { + editor.execCommand("lineheight", this.value); + } + }) + } + var ui = new editorui.MenuButton({ + editor:editor, + className:'edui-for-lineheight', + title:editor.options.labelMap['lineheight'] || editor.getLang("labelMap.lineheight") || '', + items:items, + onbuttonclick:function () { + var value = editor.queryCommandValue('LineHeight') || this.value; + editor.execCommand("LineHeight", value); + } + }); + editorui.buttons['lineheight'] = ui; + editor.addListener('selectionchange', function () { + var state = editor.queryCommandState('LineHeight'); + if (state == -1) { + ui.setDisabled(true); + } else { + ui.setDisabled(false); + var value = editor.queryCommandValue('LineHeight'); + value && ui.setValue((value + '').replace(/cm/, '')); + ui.setChecked(state) + } + }); + return ui; + }; + + var rowspacings = ['top', 'bottom']; + for (var r = 0, ri; ri = rowspacings[r++];) { + (function (cmd) { + editorui['rowspacing' + cmd] = function (editor) { + var val = editor.options['rowspacing' + cmd] || []; + if (!val.length) return null; + for (var i = 0, ci, items = []; ci = val[i++];) { + items.push({ + label:ci, + value:ci, + theme:editor.options.theme, + onclick:function () { + editor.execCommand("rowspacing", this.value, cmd); + } + }) + } + var ui = new editorui.MenuButton({ + editor:editor, + className:'edui-for-rowspacing' + cmd, + title:editor.options.labelMap['rowspacing' + cmd] || editor.getLang("labelMap.rowspacing" + cmd) || '', + items:items, + onbuttonclick:function () { + var value = editor.queryCommandValue('rowspacing', cmd) || this.value; + editor.execCommand("rowspacing", value, cmd); + } + }); + editorui.buttons[cmd] = ui; + editor.addListener('selectionchange', function () { + var state = editor.queryCommandState('rowspacing', cmd); + if (state == -1) { + ui.setDisabled(true); + } else { + ui.setDisabled(false); + var value = editor.queryCommandValue('rowspacing', cmd); + value && ui.setValue((value + '').replace(/%/, '')); + ui.setChecked(state) + } + }); + return ui; + } + })(ri) + } + //有序,无序列表 + var lists = ['insertorderedlist', 'insertunorderedlist']; + for (var l = 0, cl; cl = lists[l++];) { + (function (cmd) { + editorui[cmd] = function (editor) { + var vals = editor.options[cmd], + _onMenuClick = function () { + editor.execCommand(cmd, this.value); + }, items = []; + for (var i in vals) { + items.push({ + label:vals[i] || editor.getLang()[cmd][i] || "", + value:i, + theme:editor.options.theme, + onclick:_onMenuClick + }) + } + var ui = new editorui.MenuButton({ + editor:editor, + className:'edui-for-' + cmd, + title:editor.getLang("labelMap." + cmd) || '', + 'items':items, + onbuttonclick:function () { + var value = editor.queryCommandValue(cmd) || this.value; + editor.execCommand(cmd, value); + } + }); + editorui.buttons[cmd] = ui; + editor.addListener('selectionchange', function () { + var state = editor.queryCommandState(cmd); + if (state == -1) { + ui.setDisabled(true); + } else { + ui.setDisabled(false); + var value = editor.queryCommandValue(cmd); + ui.setValue(value); + ui.setChecked(state) + } + }); + return ui; + }; + })(cl) + } + + editorui.fullscreen = function (editor, title) { + title = editor.options.labelMap['fullscreen'] || editor.getLang("labelMap.fullscreen") || ''; + var ui = new editorui.Button({ + className:'edui-for-fullscreen', + title:title, + theme:editor.options.theme, + onclick:function () { + if (editor.ui) { + editor.ui.setFullScreen(!editor.ui.isFullScreen()); + } + this.setChecked(editor.ui.isFullScreen()); + } + }); + editorui.buttons['fullscreen'] = ui; + editor.addListener('selectionchange', function () { + var state = editor.queryCommandState('fullscreen'); + ui.setDisabled(state == -1); + ui.setChecked(editor.ui.isFullScreen()); + }); + return ui; + }; + + // 表情 + editorui["emotion"] = function (editor, iframeUrl) { + var cmd = "emotion"; + var ui = new editorui.MultiMenuPop({ + title:editor.options.labelMap[cmd] || editor.getLang("labelMap." + cmd + "") || '', + editor:editor, + className:'edui-for-' + cmd, + iframeUrl:editor.ui.mapUrl(iframeUrl || (editor.options.iframeUrlMap || {})[cmd] || iframeUrlMap[cmd]) + }); + editorui.buttons[cmd] = ui; + + editor.addListener('selectionchange', function () { + ui.setDisabled(editor.queryCommandState(cmd) == -1) + }); + return ui; + }; + + editorui.autotypeset = function (editor) { + var ui = new editorui.AutoTypeSetButton({ + editor:editor, + title:editor.options.labelMap['autotypeset'] || editor.getLang("labelMap.autotypeset") || '', + className:'edui-for-autotypeset', + onbuttonclick:function () { + editor.execCommand('autotypeset') + } + }); + editorui.buttons['autotypeset'] = ui; + editor.addListener('selectionchange', function () { + ui.setDisabled(editor.queryCommandState('autotypeset') == -1); + }); + return ui; + }; + + /* 简单上传插件 */ + editorui["simpleupload"] = function (editor) { + var name = 'simpleupload', + ui = new editorui.Button({ + className:'edui-for-' + name, + title:editor.options.labelMap[name] || editor.getLang("labelMap." + name) || '', + onclick:function () {}, + theme:editor.options.theme, + showText:false + }); + editorui.buttons[name] = ui; + editor.addListener('ready', function() { + var b = ui.getDom('body'), + iconSpan = b.children[0]; + editor.fireEvent('simpleuploadbtnready', iconSpan); + }); + editor.addListener('selectionchange', function (type, causeByUi, uiReady) { + var state = editor.queryCommandState(name); + if (state == -1) { + ui.setDisabled(true); + ui.setChecked(false); + } else { + if (!uiReady) { + ui.setDisabled(false); + ui.setChecked(state); + } + } + }); + return ui; + }; + +})(); + + +// adapter/editor.js +///import core +///commands 全屏 +///commandsName FullScreen +///commandsTitle 全屏 +(function () { + var utils = baidu.editor.utils, + uiUtils = baidu.editor.ui.uiUtils, + UIBase = baidu.editor.ui.UIBase, + domUtils = baidu.editor.dom.domUtils; + var nodeStack = []; + + function EditorUI(options) { + this.initOptions(options); + this.initEditorUI(); + } + + EditorUI.prototype = { + uiName:'editor', + initEditorUI:function () { + this.editor.ui = this; + this._dialogs = {}; + this.initUIBase(); + this._initToolbars(); + var editor = this.editor, + me = this; + + editor.addListener('ready', function () { + //提供getDialog方法 + editor.getDialog = function (name) { + return editor.ui._dialogs[name + "Dialog"]; + }; + domUtils.on(editor.window, 'scroll', function (evt) { + baidu.editor.ui.Popup.postHide(evt); + }); + //提供编辑器实时宽高(全屏时宽高不变化) + editor.ui._actualFrameWidth = editor.options.initialFrameWidth; + + UE.browser.ie && UE.browser.version === 6 && editor.container.ownerDocument.execCommand("BackgroundImageCache", false, true); + + //display bottom-bar label based on config + if (editor.options.elementPathEnabled) { + editor.ui.getDom('elementpath').innerHTML = '
              ' + editor.getLang("elementPathTip") + ':
              '; + } + if (editor.options.wordCount) { + function countFn() { + setCount(editor,me); + domUtils.un(editor.document, "click", arguments.callee); + } + domUtils.on(editor.document, "click", countFn); + editor.ui.getDom('wordcount').innerHTML = editor.getLang("wordCountTip"); + } + editor.ui._scale(); + if (editor.options.scaleEnabled) { + if (editor.autoHeightEnabled) { + editor.disableAutoHeight(); + } + me.enableScale(); + } else { + me.disableScale(); + } + if (!editor.options.elementPathEnabled && !editor.options.wordCount && !editor.options.scaleEnabled) { + editor.ui.getDom('elementpath').style.display = "none"; + editor.ui.getDom('wordcount').style.display = "none"; + editor.ui.getDom('scale').style.display = "none"; + } + + if (!editor.selection.isFocus())return; + editor.fireEvent('selectionchange', false, true); + + + }); + + editor.addListener('mousedown', function (t, evt) { + var el = evt.target || evt.srcElement; + baidu.editor.ui.Popup.postHide(evt, el); + baidu.editor.ui.ShortCutMenu.postHide(evt); + + }); + editor.addListener("delcells", function () { + if (UE.ui['edittip']) { + new UE.ui['edittip'](editor); + } + editor.getDialog('edittip').open(); + }); + + var pastePop, isPaste = false, timer; + editor.addListener("afterpaste", function () { + if(editor.queryCommandState('pasteplain')) + return; + if(baidu.editor.ui.PastePicker){ + pastePop = new baidu.editor.ui.Popup({ + content:new baidu.editor.ui.PastePicker({editor:editor}), + editor:editor, + className:'edui-wordpastepop' + }); + pastePop.render(); + } + isPaste = true; + }); + + editor.addListener("afterinserthtml", function () { + clearTimeout(timer); + timer = setTimeout(function () { + if (pastePop && (isPaste || editor.ui._isTransfer)) { + if(pastePop.isHidden()){ + var span = domUtils.createElement(editor.document, 'span', { + 'style':"line-height:0px;", + 'innerHTML':'\ufeff' + }), + range = editor.selection.getRange(); + range.insertNode(span); + var tmp= getDomNode(span, 'firstChild', 'previousSibling'); + tmp && pastePop.showAnchor(tmp.nodeType == 3 ? tmp.parentNode : tmp); + domUtils.remove(span); + }else{ + pastePop.show(); + } + delete editor.ui._isTransfer; + isPaste = false; + } + }, 200) + }); + editor.addListener('contextmenu', function (t, evt) { + baidu.editor.ui.Popup.postHide(evt); + }); + editor.addListener('keydown', function (t, evt) { + if (pastePop) pastePop.dispose(evt); + var keyCode = evt.keyCode || evt.which; + if(evt.altKey&&keyCode==90){ + UE.ui.buttons['fullscreen'].onclick(); + } + }); + editor.addListener('wordcount', function (type) { + setCount(this,me); + }); + function setCount(editor,ui) { + editor.setOpt({ + wordCount:true, + maximumWords:10000, + wordCountMsg:editor.options.wordCountMsg || editor.getLang("wordCountMsg"), + wordOverFlowMsg:editor.options.wordOverFlowMsg || editor.getLang("wordOverFlowMsg") + }); + var opt = editor.options, + max = opt.maximumWords, + msg = opt.wordCountMsg , + errMsg = opt.wordOverFlowMsg, + countDom = ui.getDom('wordcount'); + if (!opt.wordCount) { + return; + } + var count = editor.getContentLength(true); + if (count > max) { + countDom.innerHTML = errMsg; + editor.fireEvent("wordcountoverflow"); + } else { + countDom.innerHTML = msg.replace("{#leave}", max - count).replace("{#count}", count); + } + } + + editor.addListener('selectionchange', function () { + if (editor.options.elementPathEnabled) { + me[(editor.queryCommandState('elementpath') == -1 ? 'dis' : 'en') + 'ableElementPath']() + } + if (editor.options.scaleEnabled) { + me[(editor.queryCommandState('scale') == -1 ? 'dis' : 'en') + 'ableScale'](); + + } + }); + var popup = new baidu.editor.ui.Popup({ + editor:editor, + content:'', + className:'edui-bubble', + _onEditButtonClick:function () { + this.hide(); + editor.ui._dialogs.linkDialog.open(); + }, + _onImgEditButtonClick:function (name) { + this.hide(); + editor.ui._dialogs[name] && editor.ui._dialogs[name].open(); + + }, + _onImgSetFloat:function (value) { + this.hide(); + editor.execCommand("imagefloat", value); + + }, + _setIframeAlign:function (value) { + var frame = popup.anchorEl; + var newFrame = frame.cloneNode(true); + switch (value) { + case -2: + newFrame.setAttribute("align", ""); + break; + case -1: + newFrame.setAttribute("align", "left"); + break; + case 1: + newFrame.setAttribute("align", "right"); + break; + } + frame.parentNode.insertBefore(newFrame, frame); + domUtils.remove(frame); + popup.anchorEl = newFrame; + popup.showAnchor(popup.anchorEl); + }, + _updateIframe:function () { + var frame = editor._iframe = popup.anchorEl; + if(domUtils.hasClass(frame, 'ueditor_baidumap')) { + editor.selection.getRange().selectNode(frame).select(); + editor.ui._dialogs.mapDialog.open(); + popup.hide(); + } else { + editor.ui._dialogs.insertframeDialog.open(); + popup.hide(); + } + }, + _onRemoveButtonClick:function (cmdName) { + editor.execCommand(cmdName); + this.hide(); + }, + queryAutoHide:function (el) { + if (el && el.ownerDocument == editor.document) { + if (el.tagName.toLowerCase() == 'img' || domUtils.findParentByTagName(el, 'a', true)) { + return el !== popup.anchorEl; + } + } + return baidu.editor.ui.Popup.prototype.queryAutoHide.call(this, el); + } + }); + popup.render(); + if (editor.options.imagePopup) { + editor.addListener('mouseover', function (t, evt) { + evt = evt || window.event; + var el = evt.target || evt.srcElement; + if (editor.ui._dialogs.insertframeDialog && /iframe/ig.test(el.tagName)) { + var html = popup.formatHtml( + '' + editor.getLang("property") + ': ' + editor.getLang("default") + '  ' + editor.getLang("justifyleft") + '  ' + editor.getLang("justifyright") + '  ' + + ' ' + editor.getLang("modify") + ''); + if (html) { + popup.getDom('content').innerHTML = html; + popup.anchorEl = el; + popup.showAnchor(popup.anchorEl); + } else { + popup.hide(); + } + } + }); + editor.addListener('selectionchange', function (t, causeByUi) { + if (!causeByUi) return; + var html = '', str = "", + img = editor.selection.getRange().getClosedNode(), + dialogs = editor.ui._dialogs; + if (img && img.tagName == 'IMG') { + var dialogName = 'insertimageDialog'; + if (img.className.indexOf("edui-faked-video") != -1 || img.className.indexOf("edui-upload-video") != -1) { + dialogName = "insertvideoDialog" + } + if (img.className.indexOf("edui-faked-webapp") != -1) { + dialogName = "webappDialog" + } + if (img.src.indexOf("http://api.map.baidu.com") != -1) { + dialogName = "mapDialog" + } + if (img.className.indexOf("edui-faked-music") != -1) { + dialogName = "musicDialog" + } + if (img.src.indexOf("http://maps.google.com/maps/api/staticmap") != -1) { + dialogName = "gmapDialog" + } + if (img.getAttribute("anchorname")) { + dialogName = "anchorDialog"; + html = popup.formatHtml( + '' + editor.getLang("property") + ': ' + editor.getLang("modify") + '  ' + + '' + editor.getLang("delete") + ''); + } + if (img.getAttribute("word_img")) { + //todo 放到dialog去做查询 + editor.word_img = [img.getAttribute("word_img")]; + dialogName = "wordimageDialog" + } + if(domUtils.hasClass(img, 'loadingclass') || domUtils.hasClass(img, 'loaderrorclass')) { + dialogName = ""; + } + if (!dialogs[dialogName]) { + return; + } + str = '' + editor.getLang("property") + ': '+ + '' + editor.getLang("default") + '  ' + + '' + editor.getLang("justifyleft") + '  ' + + '' + editor.getLang("justifyright") + '  ' + + '' + editor.getLang("justifycenter") + '  '+ + '' + editor.getLang("modify") + ''; + + !html && (html = popup.formatHtml(str)) + + } + if (editor.ui._dialogs.linkDialog) { + var link = editor.queryCommandValue('link'); + var url; + if (link && (url = (link.getAttribute('_href') || link.getAttribute('href', 2)))) { + var txt = url; + if (url.length > 30) { + txt = url.substring(0, 20) + "..."; + } + if (html) { + html += '
              ' + } + html += popup.formatHtml( + '' + editor.getLang("anthorMsg") + ': ' + txt + '' + + ' ' + editor.getLang("modify") + '' + + ' ' + editor.getLang("clear") + ''); + popup.showAnchor(link); + } + } + + if (html) { + popup.getDom('content').innerHTML = html; + popup.anchorEl = img || link; + popup.showAnchor(popup.anchorEl); + } else { + popup.hide(); + } + }); + } + + }, + _initToolbars:function () { + var editor = this.editor; + var toolbars = this.toolbars || []; + var toolbarUis = []; + for (var i = 0; i < toolbars.length; i++) { + var toolbar = toolbars[i]; + var toolbarUi = new baidu.editor.ui.Toolbar({theme:editor.options.theme}); + for (var j = 0; j < toolbar.length; j++) { + var toolbarItem = toolbar[j]; + var toolbarItemUi = null; + if (typeof toolbarItem == 'string') { + toolbarItem = toolbarItem.toLowerCase(); + if (toolbarItem == '|') { + toolbarItem = 'Separator'; + } + if(toolbarItem == '||'){ + toolbarItem = 'Breakline'; + } + if (baidu.editor.ui[toolbarItem]) { + toolbarItemUi = new baidu.editor.ui[toolbarItem](editor); + } + + //fullscreen这里单独处理一下,放到首行去 + if (toolbarItem == 'fullscreen') { + if (toolbarUis && toolbarUis[0]) { + toolbarUis[0].items.splice(0, 0, toolbarItemUi); + } else { + toolbarItemUi && toolbarUi.items.splice(0, 0, toolbarItemUi); + } + + continue; + + + } + } else { + toolbarItemUi = toolbarItem; + } + if (toolbarItemUi && toolbarItemUi.id) { + + toolbarUi.add(toolbarItemUi); + } + } + toolbarUis[i] = toolbarUi; + } + + //接受外部定制的UI + + utils.each(UE._customizeUI,function(obj,key){ + var itemUI,index; + if(obj.id && obj.id != editor.key){ + return false; + } + itemUI = obj.execFn.call(editor,editor,key); + if(itemUI){ + index = obj.index; + if(index === undefined){ + index = toolbarUi.items.length; + } + toolbarUi.add(itemUI,index) + } + }); + + this.toolbars = toolbarUis; + }, + getHtmlTpl:function () { + return '
              ' + + '
              ' + + (this.toolbars.length ? + '
              ' + + this.renderToolbarBoxHtml() + + '
              ' : '') + + '' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + //modify wdcount by matao + '
              ' + + '' + + '' + + '' + + '
              ' + + '
              ' + + '
              '; + }, + showWordImageDialog:function () { + this._dialogs['wordimageDialog'].open(); + }, + renderToolbarBoxHtml:function () { + var buff = []; + for (var i = 0; i < this.toolbars.length; i++) { + buff.push(this.toolbars[i].renderHtml()); + } + return buff.join(''); + }, + setFullScreen:function (fullscreen) { + + var editor = this.editor, + container = editor.container.parentNode.parentNode; + if (this._fullscreen != fullscreen) { + this._fullscreen = fullscreen; + this.editor.fireEvent('beforefullscreenchange', fullscreen); + if (baidu.editor.browser.gecko) { + var bk = editor.selection.getRange().createBookmark(); + } + if (fullscreen) { + while (container.tagName != "BODY") { + var position = baidu.editor.dom.domUtils.getComputedStyle(container, "position"); + nodeStack.push(position); + container.style.position = "static"; + container = container.parentNode; + } + this._bakHtmlOverflow = document.documentElement.style.overflow; + this._bakBodyOverflow = document.body.style.overflow; + this._bakAutoHeight = this.editor.autoHeightEnabled; + this._bakScrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop); + + this._bakEditorContaninerWidth = editor.iframe.parentNode.offsetWidth; + if (this._bakAutoHeight) { + //当全屏时不能执行自动长高 + editor.autoHeightEnabled = false; + this.editor.disableAutoHeight(); + } + + document.documentElement.style.overflow = 'hidden'; + //修复,滚动条不收起的问题 + + window.scrollTo(0,window.scrollY); + this._bakCssText = this.getDom().style.cssText; + this._bakCssText1 = this.getDom('iframeholder').style.cssText; + editor.iframe.parentNode.style.width = ''; + this._updateFullScreen(); + } else { + while (container.tagName != "BODY") { + container.style.position = nodeStack.shift(); + container = container.parentNode; + } + this.getDom().style.cssText = this._bakCssText; + this.getDom('iframeholder').style.cssText = this._bakCssText1; + if (this._bakAutoHeight) { + editor.autoHeightEnabled = true; + this.editor.enableAutoHeight(); + } + + document.documentElement.style.overflow = this._bakHtmlOverflow; + document.body.style.overflow = this._bakBodyOverflow; + editor.iframe.parentNode.style.width = this._bakEditorContaninerWidth + 'px'; + window.scrollTo(0, this._bakScrollTop); + } + if (browser.gecko && editor.body.contentEditable === 'true') { + var input = document.createElement('input'); + document.body.appendChild(input); + editor.body.contentEditable = false; + setTimeout(function () { + input.focus(); + setTimeout(function () { + editor.body.contentEditable = true; + editor.fireEvent('fullscreenchanged', fullscreen); + editor.selection.getRange().moveToBookmark(bk).select(true); + baidu.editor.dom.domUtils.remove(input); + fullscreen && window.scroll(0, 0); + }, 0) + }, 0) + } + + if(editor.body.contentEditable === 'true'){ + this.editor.fireEvent('fullscreenchanged', fullscreen); + this.triggerLayout(); + } + + } + }, + _updateFullScreen:function () { + if (this._fullscreen) { + var vpRect = uiUtils.getViewportRect(); + this.getDom().style.cssText = 'border:0;position:absolute;left:0;top:' + (this.editor.options.topOffset || 0) + 'px;width:' + vpRect.width + 'px;height:' + vpRect.height + 'px;z-index:' + (this.getDom().style.zIndex * 1 + 100); + uiUtils.setViewportOffset(this.getDom(), { left:0, top:this.editor.options.topOffset || 0 }); + this.editor.setHeight(vpRect.height - this.getDom('toolbarbox').offsetHeight - this.getDom('bottombar').offsetHeight - (this.editor.options.topOffset || 0),true); + //不手动调一下,会导致全屏失效 + if(browser.gecko){ + try{ + window.onresize(); + }catch(e){ + + } + + } + } + }, + _updateElementPath:function () { + var bottom = this.getDom('elementpath'), list; + if (this.elementPathEnabled && (list = this.editor.queryCommandValue('elementpath'))) { + + var buff = []; + for (var i = 0, ci; ci = list[i]; i++) { + buff[i] = this.formatHtml('' + ci + ''); + } + bottom.innerHTML = '
              ' + this.editor.getLang("elementPathTip") + ': ' + buff.join(' > ') + '
              '; + + } else { + bottom.style.display = 'none' + } + }, + disableElementPath:function () { + var bottom = this.getDom('elementpath'); + bottom.innerHTML = ''; + bottom.style.display = 'none'; + this.elementPathEnabled = false; + + }, + enableElementPath:function () { + var bottom = this.getDom('elementpath'); + bottom.style.display = ''; + this.elementPathEnabled = true; + this._updateElementPath(); + }, + _scale:function () { + var doc = document, + editor = this.editor, + editorHolder = editor.container, + editorDocument = editor.document, + toolbarBox = this.getDom("toolbarbox"), + bottombar = this.getDom("bottombar"), + scale = this.getDom("scale"), + scalelayer = this.getDom("scalelayer"); + + var isMouseMove = false, + position = null, + minEditorHeight = 0, + minEditorWidth = editor.options.minFrameWidth, + pageX = 0, + pageY = 0, + scaleWidth = 0, + scaleHeight = 0; + + function down() { + position = domUtils.getXY(editorHolder); + + if (!minEditorHeight) { + minEditorHeight = editor.options.minFrameHeight + toolbarBox.offsetHeight + bottombar.offsetHeight; + } + + scalelayer.style.cssText = "position:absolute;left:0;display:;top:0;background-color:#41ABFF;opacity:0.4;filter: Alpha(opacity=40);width:" + editorHolder.offsetWidth + "px;height:" + + editorHolder.offsetHeight + "px;z-index:" + (editor.options.zIndex + 1); + + domUtils.on(doc, "mousemove", move); + domUtils.on(editorDocument, "mouseup", up); + domUtils.on(doc, "mouseup", up); + } + + var me = this; + //by xuheng 全屏时关掉缩放 + this.editor.addListener('fullscreenchanged', function (e, fullScreen) { + if (fullScreen) { + me.disableScale(); + + } else { + if (me.editor.options.scaleEnabled) { + me.enableScale(); + var tmpNode = me.editor.document.createElement('span'); + me.editor.body.appendChild(tmpNode); + me.editor.body.style.height = Math.max(domUtils.getXY(tmpNode).y, me.editor.iframe.offsetHeight - 20) + 'px'; + domUtils.remove(tmpNode) + } + } + }); + function move(event) { + clearSelection(); + var e = event || window.event; + pageX = e.pageX || (doc.documentElement.scrollLeft + e.clientX); + pageY = e.pageY || (doc.documentElement.scrollTop + e.clientY); + scaleWidth = pageX - position.x; + scaleHeight = pageY - position.y; + + if (scaleWidth >= minEditorWidth) { + isMouseMove = true; + scalelayer.style.width = scaleWidth + 'px'; + } + if (scaleHeight >= minEditorHeight) { + isMouseMove = true; + scalelayer.style.height = scaleHeight + "px"; + } + } + + function up() { + if (isMouseMove) { + isMouseMove = false; + editor.ui._actualFrameWidth = scalelayer.offsetWidth - 2; + editorHolder.style.width = editor.ui._actualFrameWidth + 'px'; + + editor.setHeight(scalelayer.offsetHeight - bottombar.offsetHeight - toolbarBox.offsetHeight - 2,true); + } + if (scalelayer) { + scalelayer.style.display = "none"; + } + clearSelection(); + domUtils.un(doc, "mousemove", move); + domUtils.un(editorDocument, "mouseup", up); + domUtils.un(doc, "mouseup", up); + } + + function clearSelection() { + if (browser.ie) + doc.selection.clear(); + else + window.getSelection().removeAllRanges(); + } + + this.enableScale = function () { + //trace:2868 + if (editor.queryCommandState("source") == 1) return; + scale.style.display = ""; + this.scaleEnabled = true; + domUtils.on(scale, "mousedown", down); + }; + this.disableScale = function () { + scale.style.display = "none"; + this.scaleEnabled = false; + domUtils.un(scale, "mousedown", down); + }; + }, + isFullScreen:function () { + return this._fullscreen; + }, + postRender:function () { + UIBase.prototype.postRender.call(this); + for (var i = 0; i < this.toolbars.length; i++) { + this.toolbars[i].postRender(); + } + var me = this; + var timerId, + domUtils = baidu.editor.dom.domUtils, + updateFullScreenTime = function () { + clearTimeout(timerId); + timerId = setTimeout(function () { + me._updateFullScreen(); + }); + }; + domUtils.on(window, 'resize', updateFullScreenTime); + + me.addListener('destroy', function () { + domUtils.un(window, 'resize', updateFullScreenTime); + clearTimeout(timerId); + }) + }, + showToolbarMsg:function (msg, flag) { + this.getDom('toolbarmsg_label').innerHTML = msg; + this.getDom('toolbarmsg').style.display = ''; + // + if (!flag) { + var w = this.getDom('upload_dialog'); + w.style.display = 'none'; + } + }, + hideToolbarMsg:function () { + this.getDom('toolbarmsg').style.display = 'none'; + }, + mapUrl:function (url) { + return url ? url.replace('~/', this.editor.options.UEDITOR_HOME_URL || '') : '' + }, + triggerLayout:function () { + var dom = this.getDom(); + if (dom.style.zoom == '1') { + dom.style.zoom = '100%'; + } else { + dom.style.zoom = '1'; + } + } + }; + utils.inherits(EditorUI, baidu.editor.ui.UIBase); + + + var instances = {}; + + + UE.ui.Editor = function (options) { + var editor = new UE.Editor(options); + editor.options.editor = editor; + utils.loadFile(document, { + href:editor.options.themePath + editor.options.theme + "/css/ueditor.css", + tag:"link", + type:"text/css", + rel:"stylesheet" + }); + + var oldRender = editor.render; + editor.render = function (holder) { + if (holder.constructor === String) { + editor.key = holder; + instances[holder] = editor; + } + utils.domReady(function () { + editor.langIsReady ? renderUI() : editor.addListener("langReady", renderUI); + function renderUI() { + editor.setOpt({ + labelMap:editor.options.labelMap || editor.getLang('labelMap') + }); + new EditorUI(editor.options); + if (holder) { + if (holder.constructor === String) { + holder = document.getElementById(holder); + } + holder && holder.getAttribute('name') && ( editor.options.textarea = holder.getAttribute('name')); + if (holder && /script|textarea/ig.test(holder.tagName)) { + var newDiv = document.createElement('div'); + holder.parentNode.insertBefore(newDiv, holder); + var cont = holder.value || holder.innerHTML; + editor.options.initialContent = /^[\t\r\n ]*$/.test(cont) ? editor.options.initialContent : + cont.replace(/>[\n\r\t]+([ ]{4})+/g, '>') + .replace(/[\n\r\t]+([ ]{4})+[\n\r\t]+<'); + holder.className && (newDiv.className = holder.className); + holder.style.cssText && (newDiv.style.cssText = holder.style.cssText); + if (/textarea/i.test(holder.tagName)) { + editor.textarea = holder; + editor.textarea.style.display = 'none'; + + + } else { + holder.parentNode.removeChild(holder); + + + } + if(holder.id){ + newDiv.id = holder.id; + domUtils.removeAttributes(holder,'id'); + } + holder = newDiv; + holder.innerHTML = ''; + } + + } + domUtils.addClass(holder, "edui-" + editor.options.theme); + editor.ui.render(holder); + var opt = editor.options; + //给实例添加一个编辑器的容器引用 + editor.container = editor.ui.getDom(); + var parents = domUtils.findParents(holder,true); + var displays = []; + for(var i = 0 ,ci;ci=parents[i];i++){ + displays[i] = ci.style.display; + ci.style.display = 'block' + } + if (opt.initialFrameWidth) { + opt.minFrameWidth = opt.initialFrameWidth; + } else { + opt.minFrameWidth = opt.initialFrameWidth = holder.offsetWidth; + var styleWidth = holder.style.width; + if(/%$/.test(styleWidth)) { + opt.initialFrameWidth = styleWidth; + } + } + if (opt.initialFrameHeight) { + opt.minFrameHeight = opt.initialFrameHeight; + } else { + opt.initialFrameHeight = opt.minFrameHeight = holder.offsetHeight; + } + for(var i = 0 ,ci;ci=parents[i];i++){ + ci.style.display = displays[i] + } + //编辑器最外容器设置了高度,会导致,编辑器不占位 + //todo 先去掉,没有找到原因 + if(holder.style.height){ + holder.style.height = '' + } + editor.container.style.width = opt.initialFrameWidth + (/%$/.test(opt.initialFrameWidth) ? '' : 'px'); + editor.container.style.zIndex = opt.zIndex; + oldRender.call(editor, editor.ui.getDom('iframeholder')); + editor.fireEvent("afteruiready"); + } + }) + }; + return editor; + }; + + + /** + * @file + * @name UE + * @short UE + * @desc UEditor的顶部命名空间 + */ + /** + * @name getEditor + * @since 1.2.4+ + * @grammar UE.getEditor(id,[opt]) => Editor实例 + * @desc 提供一个全局的方法得到编辑器实例 + * + * * ''id'' 放置编辑器的容器id, 如果容器下的编辑器已经存在,就直接返回 + * * ''opt'' 编辑器的可选参数 + * @example + * UE.getEditor('containerId',{onready:function(){//创建一个编辑器实例 + * this.setContent('hello') + * }}); + * UE.getEditor('containerId'); //返回刚创建的实例 + * + */ + UE.getEditor = function (id, opt) { + var editor = instances[id]; + if (!editor) { + editor = instances[id] = new UE.ui.Editor(opt); + editor.render(id); + } + return editor; + }; + + + UE.delEditor = function (id) { + var editor; + if (editor = instances[id]) { + editor.key && editor.destroy(); + delete instances[id] + } + }; + + UE.registerUI = function(uiName,fn,index,editorId){ + utils.each(uiName.split(/\s+/), function (name) { + UE._customizeUI[name] = { + id : editorId, + execFn:fn, + index:index + }; + }) + + } + +})(); + +// adapter/message.js +UE.registerUI('message', function(editor) { + + var editorui = baidu.editor.ui; + var Message = editorui.Message; + var holder; + var _messageItems = []; + var me = editor; + + me.addListener('ready', function(){ + holder = document.getElementById(me.ui.id + '_message_holder'); + updateHolderPos(); + setTimeout(function(){ + updateHolderPos(); + }, 500); + }); + + me.addListener('showmessage', function(type, opt){ + opt = utils.isString(opt) ? { + 'content': opt + } : opt; + var message = new Message({ + 'timeout': opt.timeout, + 'type': opt.type, + 'content': opt.content, + 'keepshow': opt.keepshow, + 'editor': me + }), + mid = opt.id || ('msg_' + (+new Date()).toString(36)); + message.render(holder); + _messageItems[mid] = message; + message.reset(opt); + updateHolderPos(); + return mid; + }); + + me.addListener('updatemessage',function(type, id, opt){ + opt = utils.isString(opt) ? { + 'content': opt + } : opt; + var message = _messageItems[id]; + message.render(holder); + message && message.reset(opt); + }); + + me.addListener('hidemessage',function(type, id){ + var message = _messageItems[id]; + message && message.hide(); + }); + + function updateHolderPos(){ + var toolbarbox = me.ui.getDom('toolbarbox'); + if (toolbarbox) { + holder.style.top = toolbarbox.offsetHeight + 3 + 'px'; + } + holder.style.zIndex = Math.max(me.options.zIndex, me.iframe.style.zIndex) + 1; + } + +}); + + +// adapter/autosave.js +UE.registerUI('autosave', function(editor) { + var timer = null,uid = null; + editor.on('afterautosave',function(){ + clearTimeout(timer); + + timer = setTimeout(function(){ + if(uid){ + editor.trigger('hidemessage',uid); + } + uid = editor.trigger('showmessage',{ + content : editor.getLang('autosave.success'), + timeout : 2000 + }); + + },2000) + }) + +}); + + + +})(); diff --git a/public/static/libs/ueditor/ueditor.all.min.js b/public/static/libs/ueditor/ueditor.all.min.js new file mode 100644 index 0000000..97c6dda --- /dev/null +++ b/public/static/libs/ueditor/ueditor.all.min.js @@ -0,0 +1,18 @@ +/*! + * UEditor + * version: ueditor + * build: Thu Jun 16 2016 12:33:51 GMT+0800 (CST) + */ + +!function(){function getListener(a,b,c){var d;return b=b.toLowerCase(),(d=a.__allListeners||c&&(a.__allListeners={}))&&(d[b]||c&&(d[b]=[]))}function getDomNode(a,b,c,d,e,f){var g,h=d&&a[b];for(!h&&(h=a[c]);!h&&(g=(g||a).parentNode);){if("BODY"==g.tagName||f&&!f(g))return null;h=g[c]}return h&&e&&!e(h)?getDomNode(h,b,c,!1,e):h}UEDITOR_CONFIG=window.UEDITOR_CONFIG||{};var baidu=window.baidu||{};window.baidu=baidu,window.UE=baidu.editor=window.UE||{},UE.plugins={},UE.commands={},UE.instants={},UE.I18N={},UE._customizeUI={},UE.version="1.4.3";var dom=UE.dom={},browser=UE.browser=function(){var a=navigator.userAgent.toLowerCase(),b=window.opera,c={ie:/(msie\s|trident.*rv:)([\w.]+)/.test(a),opera:!!b&&b.version,webkit:a.indexOf(" applewebkit/")>-1,mac:a.indexOf("macintosh")>-1,quirks:"BackCompat"==document.compatMode};c.gecko="Gecko"==navigator.product&&!c.webkit&&!c.opera&&!c.ie;var d=0;if(c.ie){var e=a.match(/(?:msie\s([\w.]+))/),f=a.match(/(?:trident.*rv:([\w.]+))/);d=e&&f&&e[1]&&f[1]?Math.max(1*e[1],1*f[1]):e&&e[1]?1*e[1]:f&&f[1]?1*f[1]:0,c.ie11Compat=11==document.documentMode,c.ie9Compat=9==document.documentMode,c.ie8=!!document.documentMode,c.ie8Compat=8==document.documentMode,c.ie7Compat=7==d&&!document.documentMode||7==document.documentMode,c.ie6Compat=7>d||c.quirks,c.ie9above=d>8,c.ie9below=9>d,c.ie11above=d>10,c.ie11below=11>d}if(c.gecko){var g=a.match(/rv:([\d\.]+)/);g&&(g=g[1].split("."),d=1e4*g[0]+100*(g[1]||0)+1*(g[2]||0))}return/chrome\/(\d+\.\d)/i.test(a)&&(c.chrome=+RegExp.$1),/(\d+\.\d)?(?:\.\d)?\s+safari\/?(\d+\.\d+)?/i.test(a)&&!/chrome/i.test(a)&&(c.safari=+(RegExp.$1||RegExp.$2)),c.opera&&(d=parseFloat(b.version())),c.webkit&&(d=parseFloat(a.match(/ applewebkit\/(\d+)/)[1])),c.version=d,c.isCompatible=!c.mobile&&(c.ie&&d>=6||c.gecko&&d>=10801||c.opera&&d>=9.5||c.air&&d>=1||c.webkit&&d>=522||!1),c}(),ie=browser.ie,webkit=browser.webkit,gecko=browser.gecko,opera=browser.opera,utils=UE.utils={each:function(a,b,c){if(null!=a)if(a.length===+a.length){for(var d=0,e=a.length;e>d;d++)if(b.call(c,a[d],d,a)===!1)return!1}else for(var f in a)if(a.hasOwnProperty(f)&&b.call(c,a[f],f,a)===!1)return!1},makeInstance:function(a){var b=new Function;return b.prototype=a,a=new b,b.prototype=null,a},extend:function(a,b,c){if(b)for(var d in b)c&&a.hasOwnProperty(d)||(a[d]=b[d]);return a},extend2:function(a){for(var b=arguments,c=1;c=c&&a===b?(d=e,!1):void 0}),d},removeItem:function(a,b){for(var c=0,d=a.length;d>c;c++)a[c]===b&&(a.splice(c,1),c--)},trim:function(a){return a.replace(/(^[ \t\n\r]+)|([ \t\n\r]+$)/g,"")},listToMap:function(a){if(!a)return{};a=utils.isArray(a)?a:a.split(",");for(var b,c=0,d={};b=a[c++];)d[b.toUpperCase()]=d[b]=1;return d},unhtml:function(a,b){return a?a.replace(b||/[&<">'](?:(amp|lt|quot|gt|#39|nbsp|#\d+);)?/g,function(a,b){return b?a:{"<":"<","&":"&",'"':""",">":">","'":"'"}[a]}):""},unhtmlForUrl:function(a,b){return a?a.replace(b||/[<">']/g,function(a){return{"<":"<","&":"&",'"':""",">":">","'":"'"}[a]}):""},html:function(a){return a?a.replace(/&((g|l|quo)t|amp|#39|nbsp);/g,function(a){return{"<":"<","&":"&",""":'"',">":">","'":"'"," ":" "}[a]}):""},cssStyleToDomStyle:function(){var a=document.createElement("div").style,b={"float":void 0!=a.cssFloat?"cssFloat":void 0!=a.styleFloat?"styleFloat":"float"};return function(a){return b[a]||(b[a]=a.toLowerCase().replace(/-./g,function(a){return a.charAt(1).toUpperCase()}))}}(),loadFile:function(){function a(a,c){try{for(var d,e=0;d=b[e++];)if(d.doc===a&&d.url==(c.src||c.href))return d}catch(f){return null}}var b=[];return function(c,d,e){var f=a(c,d);if(f)return void(f.ready?e&&e():f.funs.push(e));if(b.push({doc:c,url:d.src||d.href,funs:[e]}),!c.body){var g=[];for(var h in d)"tag"!=h&&g.push(h+'="'+d[h]+'"');return void c.write("<"+d.tag+" "+g.join(" ")+" >")}if(!d.id||!c.getElementById(d.id)){var i=c.createElement(d.tag);delete d.tag;for(var h in d)i.setAttribute(h,d[h]);i.onload=i.onreadystatechange=function(){if(!this.readyState||/loaded|complete/.test(this.readyState)){if(f=a(c,d),f.funs.length>0){f.ready=1;for(var b;b=f.funs.pop();)b()}i.onload=i.onreadystatechange=null}},i.onerror=function(){throw Error("The load "+(d.href||d.src)+" fails,check the url settings of file ueditor.config.js ")},c.getElementsByTagName("head")[0].appendChild(i)}}}(),isEmptyObject:function(a){if(null==a)return!0;if(this.isArray(a)||this.isString(a))return 0===a.length;for(var b in a)if(a.hasOwnProperty(b))return!1;return!0},fixColor:function(a,b){if(/color/i.test(a)&&/rgba?/.test(b)){var c=b.split(",");if(c.length>3)return"";b="#";for(var d,e=0;d=c[e++];)d=parseInt(d.replace(/[^\d]/gi,""),10).toString(16),b+=1==d.length?"0"+d:d;b=b.toUpperCase()}return b},optCss:function(a){function b(a,b){if(!a)return"";var c=a.top,d=a.bottom,e=a.left,f=a.right,g="";if(c&&e&&d&&f)g+=";"+b+":"+(c==d&&d==e&&e==f?c:c==d&&e==f?c+" "+e:e==f?c+" "+e+" "+d:c+" "+f+" "+d+" "+e)+";";else for(var h in a)g+=";"+b+"-"+h+":"+a[h]+";";return g}var c,d;return a=a.replace(/(padding|margin|border)\-([^:]+):([^;]+);?/gi,function(a,b,e,f){if(1==f.split(" ").length)switch(b){case"padding":return!c&&(c={}),c[e]=f,"";case"margin":return!d&&(d={}),d[e]=f,"";case"border":return"initial"==f?"":a}return a}),a+=b(c,"padding")+b(d,"margin"),a.replace(/^[ \n\r\t;]*|[ \n\r\t]*$/,"").replace(/;([ \n\r\t]+)|\1;/g,";").replace(/(&((l|g)t|quot|#39))?;{2,}/g,function(a,b){return b?b+";;":";"})},clone:function(a,b){var c;b=b||{};for(var d in a)a.hasOwnProperty(d)&&(c=a[d],"object"==typeof c?(b[d]=utils.isArray(c)?[]:{},utils.clone(a[d],b[d])):b[d]=c);return b},transUnitToPx:function(a){if(!/(pt|cm)/.test(a))return a;var b;switch(a.replace(/([\d.]+)(\w+)/,function(c,d,e){a=d,b=e}),b){case"cm":a=25*parseFloat(a);break;case"pt":a=Math.round(96*parseFloat(a)/72)}return a+(a?"px":"")},domReady:function(){function a(a){a.isReady=!0;for(var c;c=b.pop();c());}var b=[];return function(c,d){d=d||window;var e=d.document;c&&b.push(c),"complete"===e.readyState?a(e):(e.isReady&&a(e),browser.ie&&11!=browser.version?(!function(){if(!e.isReady){try{e.documentElement.doScroll("left")}catch(b){return void setTimeout(arguments.callee,0)}a(e)}}(),d.attachEvent("onload",function(){a(e)})):(e.addEventListener("DOMContentLoaded",function(){e.removeEventListener("DOMContentLoaded",arguments.callee,!1),a(e)},!1),d.addEventListener("load",function(){a(e)},!1)))}}(),cssRule:browser.ie&&11!=browser.version?function(a,b,c){var d,e;if(void 0===b||b&&b.nodeType&&9==b.nodeType){if(c=b&&b.nodeType&&9==b.nodeType?b:c||document,d=c.indexList||(c.indexList={}),e=d[a],void 0!==e)return c.styleSheets[e].cssText}else{if(c=c||document,d=c.indexList||(c.indexList={}),e=d[a],""===b)return void 0!==e?(c.styleSheets[e].cssText="",delete d[a],!0):!1;void 0!==e?sheetStyle=c.styleSheets[e]:(sheetStyle=c.createStyleSheet("",e=c.styleSheets.length),d[a]=e),sheetStyle.cssText=b}}:function(a,b,c){var d;return void 0===b||b&&b.nodeType&&9==b.nodeType?(c=b&&b.nodeType&&9==b.nodeType?b:c||document,d=c.getElementById(a),d?d.innerHTML:void 0):(c=c||document,d=c.getElementById(a),""===b?d?(d.parentNode.removeChild(d),!0):!1:void(d?d.innerHTML=b:(d=c.createElement("style"),d.id=a,d.innerHTML=b,c.getElementsByTagName("head")[0].appendChild(d))))},sort:function(a,b){b=b||function(a,b){return a.localeCompare(b)};for(var c=0,d=a.length;d>c;c++)for(var e=c,f=a.length;f>e;e++)if(b(a[c],a[e])>0){var g=a[c];a[c]=a[e],a[e]=g}return a},serializeParam:function(a){var b=[];for(var c in a)if("method"!=c&&"timeout"!=c&&"async"!=c)if("function"!=(typeof a[c]).toLowerCase()&&"object"!=(typeof a[c]).toLowerCase())b.push(encodeURIComponent(c)+"="+encodeURIComponent(a[c]));else if(utils.isArray(a[c]))for(var d=0;dc;c++)switch(d=a[c],typeof d){case"undefined":case"function":case"unknown":break;default:b&&e.push(","),e.push(utils.json2str(d)),b=1}return e.push("]"),e.join("")}function c(a){return 10>a?"0"+a:a}function d(a){return'"'+a.getFullYear()+"-"+c(a.getMonth()+1)+"-"+c(a.getDate())+"T"+c(a.getHours())+":"+c(a.getMinutes())+":"+c(a.getSeconds())+'"'}if(window.JSON)return JSON.stringify;var e={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"};return function(c){switch(typeof c){case"undefined":return"undefined";case"number":return isFinite(c)?String(c):"null";case"string":return a(c);case"boolean":return String(c);default:if(null===c)return"null";if(utils.isArray(c))return b(c);if(utils.isDate(c))return d(c);var e,f,g=["{"],h=utils.json2str;for(var i in c)if(Object.prototype.hasOwnProperty.call(c,i))switch(f=c[i],typeof f){case"undefined":case"unknown":case"function":break;default:e&&g.push(","),e=1,g.push(h(i)+":"+h(f))}return g.push("}"),g.join("")}}}()};utils.each(["String","Function","Array","Number","RegExp","Object","Date"],function(a){UE.utils["is"+a]=function(b){return Object.prototype.toString.apply(b)=="[object "+a+"]"}});var EventBase=UE.EventBase=function(){};EventBase.prototype={addListener:function(a,b){a=utils.trim(a).split(/\s+/);for(var c,d=0;c=a[d++];)getListener(this,c,!0).push(b)},on:function(a,b){return this.addListener(a,b)},off:function(a,b){return this.removeListener(a,b)},trigger:function(){return this.fireEvent.apply(this,arguments)},removeListener:function(a,b){a=utils.trim(a).split(/\s+/);for(var c,d=0;c=a[d++];)utils.removeItem(getListener(this,c)||[],b)},fireEvent:function(){var a=arguments[0];a=utils.trim(a).split(" ");for(var b,c=0;b=a[c++];){var d,e,f,g=getListener(this,b);if(g)for(f=g.length;f--;)if(g[f]){if(e=g[f].apply(this,arguments),e===!0)return e;void 0!==e&&(d=e)}(e=this["on"+b.toLowerCase()])&&(d=e.apply(this,arguments))}return d}};var dtd=dom.dtd=function(){function a(a){for(var b in a)a[b.toUpperCase()]=a[b];return a}var b=utils.extend2,c=a({isindex:1,fieldset:1}),d=a({input:1,button:1,select:1,textarea:1,label:1}),e=b(a({a:1}),d),f=b({iframe:1},e),g=a({hr:1,ul:1,menu:1,div:1,blockquote:1,noscript:1,table:1,center:1,address:1,dir:1,pre:1,h5:1,dl:1,h4:1,noframes:1,h6:1,ol:1,h1:1,h3:1,h2:1}),h=a({ins:1,del:1,script:1,style:1}),i=b(a({b:1,acronym:1,bdo:1,"var":1,"#":1,abbr:1,code:1,br:1,i:1,cite:1,kbd:1,u:1,strike:1,s:1,tt:1,strong:1,q:1,samp:1,em:1,dfn:1,span:1}),h),j=b(a({sub:1,img:1,embed:1,object:1,sup:1,basefont:1,map:1,applet:1,font:1,big:1,small:1}),i),k=b(a({p:1}),j),l=b(a({iframe:1}),j,d),m=a({img:1,embed:1,noscript:1,br:1,kbd:1,center:1,button:1,basefont:1,h5:1,h4:1,samp:1,h6:1,ol:1,h1:1,h3:1,h2:1,form:1,font:1,"#":1,select:1,menu:1,ins:1,abbr:1,label:1,code:1,table:1,script:1,cite:1,input:1,iframe:1,strong:1,textarea:1,noframes:1,big:1,small:1,span:1,hr:1,sub:1,bdo:1,"var":1,div:1,object:1,sup:1,strike:1,dir:1,map:1,dl:1,applet:1,del:1,isindex:1,fieldset:1,ul:1,b:1,acronym:1,a:1,blockquote:1,i:1,u:1,s:1,tt:1,address:1,q:1,pre:1,p:1,em:1,dfn:1}),n=b(a({a:0}),l),o=a({tr:1}),p=a({"#":1}),q=b(a({param:1}),m),r=b(a({form:1}),c,f,g,k),s=a({li:1,ol:1,ul:1}),t=a({style:1,script:1}),u=a({base:1,link:1,meta:1,title:1}),v=b(u,t),w=a({head:1,body:1}),x=a({html:1}),y=a({address:1,blockquote:1,center:1,dir:1,div:1,dl:1,fieldset:1,form:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,hr:1,isindex:1,menu:1,noframes:1,ol:1,p:1,pre:1,table:1,ul:1}),z=a({area:1,base:1,basefont:1,br:1,col:1,command:1,dialog:1,embed:1,hr:1,img:1,input:1,isindex:1,keygen:1,link:1,meta:1,param:1,source:1,track:1,wbr:1});return a({$nonBodyContent:b(x,w,u),$block:y,$inline:n,$inlineWithA:b(a({a:1}),n),$body:b(a({script:1,style:1}),y),$cdata:a({script:1,style:1}),$empty:z,$nonChild:a({iframe:1,textarea:1}),$listItem:a({dd:1,dt:1,li:1}),$list:a({ul:1,ol:1,dl:1}),$isNotEmpty:a({table:1,ul:1,ol:1,dl:1,iframe:1,area:1,base:1,col:1,hr:1,img:1,embed:1,input:1,link:1,meta:1,param:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1}),$removeEmpty:a({a:1,abbr:1,acronym:1,address:1,b:1,bdo:1,big:1,cite:1,code:1,del:1,dfn:1,em:1,font:1,i:1,ins:1,label:1,kbd:1,q:1,s:1,samp:1,small:1,span:1,strike:1,strong:1,sub:1,sup:1,tt:1,u:1,"var":1}),$removeEmptyBlock:a({p:1,div:1}),$tableContent:a({caption:1,col:1,colgroup:1,tbody:1,td:1,tfoot:1,th:1,thead:1,tr:1,table:1}),$notTransContent:a({pre:1,script:1,style:1,textarea:1}),html:w,head:v,style:p,script:p,body:r,base:{},link:{},meta:{},title:p,col:{},tr:a({td:1,th:1}),img:{},embed:{},colgroup:a({thead:1,col:1,tbody:1,tr:1,tfoot:1}),noscript:r,td:r,br:{},th:r,center:r,kbd:n,button:b(k,g),basefont:{},h5:n,h4:n,samp:n,h6:n,ol:s,h1:n,h3:n,option:p,h2:n,form:b(c,f,g,k),select:a({optgroup:1,option:1}),font:n,ins:n,menu:s,abbr:n,label:n,table:a({thead:1,col:1,tbody:1,tr:1,colgroup:1,caption:1,tfoot:1}),code:n,tfoot:o,cite:n,li:r,input:{},iframe:r,strong:n,textarea:p,noframes:r,big:n,small:n,span:a({"#":1,br:1,b:1,strong:1,u:1,i:1,em:1,sub:1,sup:1,strike:1,span:1}),hr:n,dt:n,sub:n,optgroup:a({option:1}),param:{},bdo:n,"var":n,div:r,object:q,sup:n,dd:r,strike:n,area:{},dir:s,map:b(a({area:1,form:1,p:1}),c,h,g),applet:q,dl:a({dt:1,dd:1}),del:n,isindex:{},fieldset:b(a({legend:1}),m),thead:o,ul:s,acronym:n,b:n,a:b(a({a:1}),l),blockquote:b(a({td:1,tr:1,tbody:1,li:1}),r),caption:n,i:n,u:n,tbody:o,s:n,address:b(f,k),tt:n,legend:n,q:n,pre:b(i,e),p:b(a({a:1}),n),em:n,dfn:n})}(),attrFix=ie&&browser.version<9?{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder"}:{tabindex:"tabIndex",readonly:"readOnly"},styleBlock=utils.listToMap(["-webkit-box","-moz-box","block","list-item","table","table-row-group","table-header-group","table-footer-group","table-row","table-column-group","table-column","table-cell","table-caption"]),domUtils=dom.domUtils={NODE_ELEMENT:1,NODE_DOCUMENT:9,NODE_TEXT:3,NODE_COMMENT:8,NODE_DOCUMENT_FRAGMENT:11,POSITION_IDENTICAL:0,POSITION_DISCONNECTED:1,POSITION_FOLLOWING:2,POSITION_PRECEDING:4,POSITION_IS_CONTAINED:8,POSITION_CONTAINS:16,fillChar:ie&&"6"==browser.version?"\ufeff":"​",keys:{8:1,46:1,16:1,17:1,18:1,37:1,38:1,39:1,40:1,13:1},getPosition:function(a,b){if(a===b)return 0;var c,d=[a],e=[b];for(c=a;c=c.parentNode;){if(c===b)return 10;d.push(c)}for(c=b;c=c.parentNode;){if(c===a)return 20;e.push(c)}if(d.reverse(),e.reverse(),d[0]!==e[0])return 1;for(var f=-1;f++,d[f]===e[f];);for(a=d[f],b=e[f];a=a.nextSibling;)if(a===b)return 4;return 2},getNodeIndex:function(a,b){for(var c=a,d=0;c=c.previousSibling;)b&&3==c.nodeType?c.nodeType!=c.nextSibling.nodeType&&d++:d++;return d},inDoc:function(a,b){return 10==domUtils.getPosition(a,b)},findParent:function(a,b,c){if(a&&!domUtils.isBody(a))for(a=c?a:a.parentNode;a;){if(!b||b(a)||domUtils.isBody(a))return b&&!b(a)&&domUtils.isBody(a)?null:a;a=a.parentNode}return null},findParentByTagName:function(a,b,c,d){return b=utils.listToMap(utils.isArray(b)?b:[b]),domUtils.findParent(a,function(a){return b[a.tagName]&&!(d&&d(a))},c)},findParents:function(a,b,c,d){for(var e=b&&(c&&c(a)||!c)?[a]:[];a=domUtils.findParent(a,c);)e.push(a);return d?e:e.reverse()},insertAfter:function(a,b){return a.nextSibling?a.parentNode.insertBefore(b,a.nextSibling):a.parentNode.appendChild(b)},remove:function(a,b){var c,d=a.parentNode;if(d){if(b&&a.hasChildNodes())for(;c=a.firstChild;)d.insertBefore(c,a);d.removeChild(a)}return a},getNextDomNode:function(a,b,c,d){return getDomNode(a,"firstChild","nextSibling",b,c,d)},getPreDomNode:function(a,b,c,d){return getDomNode(a,"lastChild","previousSibling",b,c,d)},isBookmarkNode:function(a){return 1==a.nodeType&&a.id&&/^_baidu_bookmark_/i.test(a.id)},getWindow:function(a){var b=a.ownerDocument||a;return b.defaultView||b.parentWindow},getCommonAncestor:function(a,b){if(a===b)return a;for(var c=[a],d=[b],e=a,f=-1;e=e.parentNode;){if(e===b)return e;c.push(e)}for(e=b;e=e.parentNode;){if(e===a)return e;d.push(e)}for(c.reverse(),d.reverse();f++,c[f]===d[f];);return 0==f?null:c[f-1]},clearEmptySibling:function(a,b,c){function d(a,b){for(var c;a&&!domUtils.isBookmarkNode(a)&&(domUtils.isEmptyInlineElement(a)||!new RegExp("[^ \n\r"+domUtils.fillChar+"]").test(a.nodeValue));)c=a[b],domUtils.remove(a),a=c}!b&&d(a.nextSibling,"nextSibling"),!c&&d(a.previousSibling,"previousSibling")},split:function(a,b){var c=a.ownerDocument;if(browser.ie&&b==a.nodeValue.length){var d=c.createTextNode("");return domUtils.insertAfter(a,d)}var e=a.splitText(b);if(browser.ie8){var f=c.createTextNode("");domUtils.insertAfter(e,f),domUtils.remove(f)}return e},isWhitespace:function(a){return!new RegExp("[^ \n\r"+domUtils.fillChar+"]").test(a.nodeValue)},getXY:function(a){for(var b=0,c=0;a.offsetParent;)c+=a.offsetTop,b+=a.offsetLeft,a=a.offsetParent;return{x:b,y:c}},on:function(a,b,c){var d=utils.isArray(b)?b:utils.trim(b).split(/\s+/),e=d.length;if(e)for(;e--;)if(b=d[e],a.addEventListener)a.addEventListener(b,c,!1);else{c._d||(c._d={els:[]});var f=b+c.toString(),g=utils.indexOf(c._d.els,a);c._d[f]&&-1!=g||(-1==g&&c._d.els.push(a),c._d[f]||(c._d[f]=function(a){return c.call(a.srcElement,a||window.event)}),a.attachEvent("on"+b,c._d[f]))}a=null},un:function(a,b,c){var d=utils.isArray(b)?b:utils.trim(b).split(/\s+/),e=d.length;if(e)for(;e--;)if(b=d[e],a.removeEventListener)a.removeEventListener(b,c,!1);else{var f=b+c.toString();try{a.detachEvent("on"+b,c._d?c._d[f]:c)}catch(g){}if(c._d&&c._d[f]){var h=utils.indexOf(c._d.els,a);-1!=h&&c._d.els.splice(h,1),0==c._d.els.length&&delete c._d[f]}}},isSameElement:function(a,b){if(a.tagName!=b.tagName)return!1;var c=a.attributes,d=b.attributes;if(!ie&&c.length!=d.length)return!1;for(var e,f,g=0,h=0,i=0;e=c[i++];){if("style"==e.nodeName){if(e.specified&&g++,domUtils.isSameStyle(a,b))continue;return!1}if(ie){if(!e.specified)continue;g++,f=d.getNamedItem(e.nodeName)}else f=b.attributes[e.nodeName];if(!f.specified||e.nodeValue!=f.nodeValue)return!1}if(ie){for(i=0;f=d[i++];)f.specified&&h++;if(g!=h)return!1}return!0},isSameStyle:function(a,b){var c=a.style.cssText.replace(/( ?; ?)/g,";").replace(/( ?: ?)/g,":"),d=b.style.cssText.replace(/( ?; ?)/g,";").replace(/( ?: ?)/g,":");if(browser.opera){if(c=a.style,d=b.style,c.length!=d.length)return!1;for(var e in c)if(!/^(\d+|csstext)$/i.test(e)&&c[e]!=d[e])return!1;return!0}if(!c||!d)return c==d;if(c=c.split(";"),d=d.split(";"),c.length!=d.length)return!1;for(var f,g=0;f=c[g++];)if(-1==utils.indexOf(d,f))return!1;return!0},isBlockElm:function(a){return 1==a.nodeType&&(dtd.$block[a.tagName]||styleBlock[domUtils.getComputedStyle(a,"display")])&&!dtd.$nonChild[a.tagName]},isBody:function(a){return a&&1==a.nodeType&&"body"==a.tagName.toLowerCase()},breakParent:function(a,b){var c,d,e,f=a,g=a;do{for(f=f.parentNode,d?(c=f.cloneNode(!1),c.appendChild(d),d=c,c=f.cloneNode(!1),c.appendChild(e),e=c):(d=f.cloneNode(!1),e=d.cloneNode(!1));c=g.previousSibling;)d.insertBefore(c,d.firstChild);for(;c=g.nextSibling;)e.appendChild(c);g=f}while(b!==f);return c=b.parentNode,c.insertBefore(d,b),c.insertBefore(e,b),c.insertBefore(a,e),domUtils.remove(b),a},isEmptyInlineElement:function(a){if(1!=a.nodeType||!dtd.$removeEmpty[a.tagName])return 0;for(a=a.firstChild;a;){if(domUtils.isBookmarkNode(a))return 0;if(1==a.nodeType&&!domUtils.isEmptyInlineElement(a)||3==a.nodeType&&!domUtils.isWhitespace(a))return 0;a=a.nextSibling}return 1},trimWhiteTextNode:function(a){function b(b){for(var c;(c=a[b])&&3==c.nodeType&&domUtils.isWhitespace(c);)a.removeChild(c)}b("firstChild"),b("lastChild")},mergeChild:function(a,b,c){for(var d,e=domUtils.getElementsByTagName(a,a.tagName.toLowerCase()),f=0;d=e[f++];)if(d.parentNode&&!domUtils.isBookmarkNode(d))if("span"!=d.tagName.toLowerCase())domUtils.isSameElement(a,d)&&domUtils.remove(d,!0);else{if(a===d.parentNode&&(domUtils.trimWhiteTextNode(a),1==a.childNodes.length)){a.style.cssText=d.style.cssText+";"+a.style.cssText,domUtils.remove(d,!0);continue}if(d.style.cssText=a.style.cssText+";"+d.style.cssText,c){var g=c.style;if(g){g=g.split(";");for(var h,i=0;h=g[i++];)d.style[utils.cssStyleToDomStyle(h.split(":")[0])]=h.split(":")[1]}}domUtils.isSameStyle(d,a)&&domUtils.remove(d,!0)}},getElementsByTagName:function(a,b,c){if(c&&utils.isString(c)){var d=c;c=function(a){return domUtils.hasClass(a,d)}}b=utils.trim(b).replace(/[ ]{2,}/g," ").split(" ");for(var e,f=[],g=0;e=b[g++];)for(var h,i=a.getElementsByTagName(e),j=0;h=i[j++];)c&&!c(h)||f.push(h);return f},mergeToParent:function(a){for(var b=a.parentNode;b&&dtd.$removeEmpty[b.tagName];){if(b.tagName==a.tagName||"A"==b.tagName){if(domUtils.trimWhiteTextNode(b),"SPAN"==b.tagName&&!domUtils.isSameStyle(b,a)||"A"==b.tagName&&"SPAN"==a.tagName){if(b.childNodes.length>1||b!==a.parentNode){a.style.cssText=b.style.cssText+";"+a.style.cssText,b=b.parentNode;continue}b.style.cssText+=";"+a.style.cssText,"A"==b.tagName&&(b.style.textDecoration="underline")}if("A"!=b.tagName){b===a.parentNode&&domUtils.remove(a,!0);break}}b=b.parentNode}},mergeSibling:function(a,b,c){function d(a,b,c){var d;if((d=c[a])&&!domUtils.isBookmarkNode(d)&&1==d.nodeType&&domUtils.isSameElement(c,d)){for(;d.firstChild;)"firstChild"==b?c.insertBefore(d.lastChild,c.firstChild):c.appendChild(d.firstChild);domUtils.remove(d)}}!b&&d("previousSibling","firstChild",a),!c&&d("nextSibling","lastChild",a)},unSelectable:ie&&browser.ie9below||browser.opera?function(a){a.onselectstart=function(){return!1},a.onclick=a.onkeyup=a.onkeydown=function(){return!1},a.unselectable="on",a.setAttribute("unselectable","on");for(var b,c=0;b=a.all[c++];)switch(b.tagName.toLowerCase()){case"iframe":case"textarea":case"input":case"select":break;default:b.unselectable="on",a.setAttribute("unselectable","on")}}:function(a){a.style.MozUserSelect=a.style.webkitUserSelect=a.style.msUserSelect=a.style.KhtmlUserSelect="none"},removeAttributes:function(a,b){b=utils.isArray(b)?b:utils.trim(b).replace(/[ ]{2,}/g," ").split(" ");for(var c,d=0;c=b[d++];){switch(c=attrFix[c]||c){case"className":a[c]="";break;case"style":a.style.cssText="";var e=a.getAttributeNode("style");!browser.ie&&e&&a.removeAttributeNode(e)}a.removeAttribute(c)}},createElement:function(a,b,c){return domUtils.setAttributes(a.createElement(b),c)},setAttributes:function(a,b){for(var c in b)if(b.hasOwnProperty(c)){var d=b[c];switch(c){case"class":a.className=d;break;case"style":a.style.cssText=a.style.cssText+";"+d;break;case"innerHTML":a[c]=d;break;case"value":a.value=d;break;default:a.setAttribute(attrFix[c]||c,d)}}return a},getComputedStyle:function(a,b){var c="width height top left";if(c.indexOf(b)>-1)return a["offset"+b.replace(/^\w/,function(a){return a.toUpperCase()})]+"px";if(3==a.nodeType&&(a=a.parentNode),browser.ie&&browser.version<9&&"font-size"==b&&!a.style.fontSize&&!dtd.$empty[a.tagName]&&!dtd.$nonChild[a.tagName]){var d=a.ownerDocument.createElement("span");d.style.cssText="padding:0;border:0;font-family:simsun;",d.innerHTML=".",a.appendChild(d);var e=d.offsetHeight;return a.removeChild(d),d=null,e+"px"}try{var f=domUtils.getStyle(a,b)||(window.getComputedStyle?domUtils.getWindow(a).getComputedStyle(a,"").getPropertyValue(b):(a.currentStyle||a.style)[utils.cssStyleToDomStyle(b)])}catch(g){return""}return utils.transUnitToPx(utils.fixColor(b,f))},removeClasses:function(a,b){b=utils.isArray(b)?b:utils.trim(b).replace(/[ ]{2,}/g," ").split(" ");for(var c,d=0,e=a.className;c=b[d++];)e=e.replace(new RegExp("\\b"+c+"\\b"),"");e=utils.trim(e).replace(/[ ]{2,}/g," "),e?a.className=e:domUtils.removeAttributes(a,["class"])},addClass:function(a,b){if(a){b=utils.trim(b).replace(/[ ]{2,}/g," ").split(" ");for(var c,d=0,e=a.className;c=b[d++];)new RegExp("\\b"+c+"\\b").test(e)||(e+=" "+c);a.className=utils.trim(e)}},hasClass:function(a,b){if(utils.isRegExp(b))return b.test(a.className);b=utils.trim(b).replace(/[ ]{2,}/g," ").split(" ");for(var c,d=0,e=a.className;c=b[d++];)if(!new RegExp("\\b"+c+"\\b","i").test(e))return!1;return d-1==b.length},preventDefault:function(a){a.preventDefault?a.preventDefault():a.returnValue=!1},removeStyle:function(a,b){browser.ie?("color"==b&&(b="(^|;)"+b),a.style.cssText=a.style.cssText.replace(new RegExp(b+"[^:]*:[^;]+;?","ig"),"")):a.style.removeProperty?a.style.removeProperty(b):a.style.removeAttribute(utils.cssStyleToDomStyle(b)),a.style.cssText||domUtils.removeAttributes(a,["style"])},getStyle:function(a,b){var c=a.style[utils.cssStyleToDomStyle(b)];return utils.fixColor(b,c)},setStyle:function(a,b,c){a.style[utils.cssStyleToDomStyle(b)]=c,utils.trim(a.style.cssText)||this.removeAttributes(a,"style")},setStyles:function(a,b){for(var c in b)b.hasOwnProperty(c)&&domUtils.setStyle(a,c,b[c])},removeDirtyAttr:function(a){for(var b,c=0,d=a.getElementsByTagName("*");b=d[c++];)b.removeAttribute("_moz_dirty");a.removeAttribute("_moz_dirty")},getChildCount:function(a,b){var c=0,d=a.firstChild;for(b=b||function(){return 1};d;)b(d)&&c++,d=d.nextSibling;return c},isEmptyNode:function(a){return!a.firstChild||0==domUtils.getChildCount(a,function(a){return!domUtils.isBr(a)&&!domUtils.isBookmarkNode(a)&&!domUtils.isWhitespace(a)})},clearSelectedArr:function(a){for(var b;b=a.pop();)domUtils.removeAttributes(b,["class"])},scrollToView:function(a,b,c){var d=function(){var a=b.document,c="CSS1Compat"==a.compatMode;return{width:(c?a.documentElement.clientWidth:a.body.clientWidth)||0,height:(c?a.documentElement.clientHeight:a.body.clientHeight)||0}},e=function(a){if("pageXOffset"in a)return{x:a.pageXOffset||0,y:a.pageYOffset||0};var b=a.document;return{x:b.documentElement.scrollLeft||b.body.scrollLeft||0,y:b.documentElement.scrollTop||b.body.scrollTop||0}},f=d().height,g=-1*f+c;g+=a.offsetHeight||0;var h=domUtils.getXY(a);g+=h.y;var i=e(b).y;(g>i||i-f>g)&&b.scrollTo(0,g+(0>g?-20:20))},isBr:function(a){return 1==a.nodeType&&"BR"==a.tagName},isFillChar:function(a,b){if(3!=a.nodeType)return!1;var c=a.nodeValue;return b?new RegExp("^"+domUtils.fillChar).test(c):!c.replace(new RegExp(domUtils.fillChar,"g"),"").length},isStartInblock:function(a){var b,c=a.cloneRange(),d=0,e=c.startContainer;if(1==e.nodeType&&e.childNodes[c.startOffset]){e=e.childNodes[c.startOffset];for(var f=e.previousSibling;f&&domUtils.isFillChar(f);)e=f,f=f.previousSibling}for(this.isFillChar(e,!0)&&1==c.startOffset&&(c.setStartBefore(e),e=c.startContainer);e&&domUtils.isFillChar(e);)b=e,e=e.previousSibling;for(b&&(c.setStartBefore(b),e=c.startContainer),1==e.nodeType&&domUtils.isEmptyNode(e)&&1==c.startOffset&&c.setStart(e,0).collapse(!0);!c.startOffset;){if(e=c.startContainer,domUtils.isBlockElm(e)||domUtils.isBody(e)){d=1;break}var g,f=c.startContainer.previousSibling;if(f){for(;f&&domUtils.isFillChar(f);)g=f,f=f.previousSibling;g?c.setStartBefore(g):c.setStartBefore(c.startContainer)}else c.setStartBefore(c.startContainer)}return d&&!domUtils.isBody(c.startContainer)?1:0},isEmptyBlock:function(a,b){if(1!=a.nodeType)return 0;if(b=b||new RegExp("[   \r\n"+domUtils.fillChar+"]","g"),a[browser.ie?"innerText":"textContent"].replace(b,"").length>0)return 0;for(var c in dtd.$isNotEmpty)if(a.getElementsByTagName(c).length)return 0;return 1},setViewportOffset:function(a,b){var c=0|parseInt(a.style.left),d=0|parseInt(a.style.top),e=a.getBoundingClientRect(),f=b.left-e.left,g=b.top-e.top;f&&(a.style.left=c+f+"px"),g&&(a.style.top=d+g+"px")},fillNode:function(a,b){var c=browser.ie?a.createTextNode(domUtils.fillChar):a.createElement("br");b.innerHTML="",b.appendChild(c)},moveChild:function(a,b,c){for(;a.firstChild;)c&&b.firstChild?b.insertBefore(a.lastChild,b.firstChild):b.appendChild(a.firstChild)},hasNoAttributes:function(a){return browser.ie?/^<\w+\s*?>/.test(a.outerHTML):0==a.attributes.length},isCustomeNode:function(a){return 1==a.nodeType&&a.getAttribute("_ue_custom_node_")},isTagNode:function(a,b){return 1==a.nodeType&&new RegExp("\\b"+a.tagName+"\\b","i").test(b)},filterNodeList:function(a,b,c){var d=[];if(!utils.isFunction(b)){var e=b;b=function(a){return-1!=utils.indexOf(utils.isArray(e)?e:e.split(" "),a.tagName.toLowerCase())}}return utils.each(a,function(a){b(a)&&d.push(a)}),0==d.length?null:1!=d.length&&c?d:d[0]},isInNodeEndBoundary:function(a,b){var c=a.startContainer;if(3==c.nodeType&&a.startOffset!=c.nodeValue.length)return 0;if(1==c.nodeType&&a.startOffset!=c.childNodes.length)return 0;for(;c!==b;){if(c.nextSibling)return 0;c=c.parentNode}return 1},isBoundaryNode:function(a,b){for(var c;!domUtils.isBody(a);)if(c=a,a=a.parentNode,c!==a[b])return!1;return!0},fillHtml:browser.ie11below?" ":"
              "},fillCharReg=new RegExp(domUtils.fillChar,"g");!function(){function a(a){a.collapsed=a.startContainer&&a.endContainer&&a.startContainer===a.endContainer&&a.startOffset==a.endOffset}function b(a){return!a.collapsed&&1==a.startContainer.nodeType&&a.startContainer===a.endContainer&&a.endOffset-a.startOffset==1}function c(b,c,d,e){return 1==c.nodeType&&(dtd.$empty[c.tagName]||dtd.$nonChild[c.tagName])&&(d=domUtils.getNodeIndex(c)+(b?0:1),c=c.parentNode),b?(e.startContainer=c,e.startOffset=d,e.endContainer||e.collapse(!0)):(e.endContainer=c,e.endOffset=d,e.startContainer||e.collapse(!1)),a(e),e}function d(a,b){var c,d,e=a.startContainer,f=a.endContainer,g=a.startOffset,h=a.endOffset,i=a.document,j=i.createDocumentFragment();if(1==e.nodeType&&(e=e.childNodes[g]||(c=e.appendChild(i.createTextNode("")))),1==f.nodeType&&(f=f.childNodes[h]||(d=f.appendChild(i.createTextNode("")))),e===f&&3==e.nodeType)return j.appendChild(i.createTextNode(e.substringData(g,h-g))),b&&(e.deleteData(g,h-g),a.collapse(!0)),j;for(var k,l,m=j,n=domUtils.findParents(e,!0),o=domUtils.findParents(f,!0),p=0;n[p]==o[p];)p++;for(var q,r=p;q=n[r];r++){for(k=q.nextSibling,q==e?c||(3==a.startContainer.nodeType?(m.appendChild(i.createTextNode(e.nodeValue.slice(g))),b&&e.deleteData(g,e.nodeValue.length-g)):m.appendChild(b?e:e.cloneNode(!0))):(l=q.cloneNode(!1),m.appendChild(l));k&&k!==f&&k!==o[r];)q=k.nextSibling,m.appendChild(b?k:k.cloneNode(!0)),k=q;m=l}m=j,n[p]||(m.appendChild(n[p-1].cloneNode(!1)),m=m.firstChild);for(var s,r=p;s=o[r];r++){if(k=s.previousSibling,s==f?d||3!=a.endContainer.nodeType||(m.appendChild(i.createTextNode(f.substringData(0,h))),b&&f.deleteData(0,h)):(l=s.cloneNode(!1),m.appendChild(l)),r!=p||!n[p])for(;k&&k!==e;)s=k.previousSibling,m.insertBefore(b?k:k.cloneNode(!0),m.firstChild),k=s;m=l}return b&&a.setStartBefore(o[p]?n[p]?o[p]:n[p-1]:o[p-1]).collapse(!0),c&&domUtils.remove(c),d&&domUtils.remove(d),j}function e(a,b){try{if(g&&domUtils.inDoc(g,a))if(g.nodeValue.replace(fillCharReg,"").length)g.nodeValue=g.nodeValue.replace(fillCharReg,"");else{var c=g.parentNode;for(domUtils.remove(g);c&&domUtils.isEmptyInlineElement(c)&&(browser.safari?!(domUtils.getPosition(c,b)&domUtils.POSITION_CONTAINS):!c.contains(b));)g=c.parentNode,domUtils.remove(c),c=g}}catch(d){} +}function f(a,b){var c;for(a=a[b];a&&domUtils.isFillChar(a);)c=a[b],domUtils.remove(a),a=c}var g,h=0,i=domUtils.fillChar,j=dom.Range=function(a){var b=this;b.startContainer=b.startOffset=b.endContainer=b.endOffset=null,b.document=a,b.collapsed=!0};j.prototype={cloneContents:function(){return this.collapsed?null:d(this,0)},deleteContents:function(){var a;return this.collapsed||d(this,1),browser.webkit&&(a=this.startContainer,3!=a.nodeType||a.nodeValue.length||(this.setStartBefore(a).collapse(!0),domUtils.remove(a))),this},extractContents:function(){return this.collapsed?null:d(this,2)},setStart:function(a,b){return c(!0,a,b,this)},setEnd:function(a,b){return c(!1,a,b,this)},setStartAfter:function(a){return this.setStart(a.parentNode,domUtils.getNodeIndex(a)+1)},setStartBefore:function(a){return this.setStart(a.parentNode,domUtils.getNodeIndex(a))},setEndAfter:function(a){return this.setEnd(a.parentNode,domUtils.getNodeIndex(a)+1)},setEndBefore:function(a){return this.setEnd(a.parentNode,domUtils.getNodeIndex(a))},setStartAtFirst:function(a){return this.setStart(a,0)},setStartAtLast:function(a){return this.setStart(a,3==a.nodeType?a.nodeValue.length:a.childNodes.length)},setEndAtFirst:function(a){return this.setEnd(a,0)},setEndAtLast:function(a){return this.setEnd(a,3==a.nodeType?a.nodeValue.length:a.childNodes.length)},selectNode:function(a){return this.setStartBefore(a).setEndAfter(a)},selectNodeContents:function(a){return this.setStart(a,0).setEndAtLast(a)},cloneRange:function(){var a=this;return new j(a.document).setStart(a.startContainer,a.startOffset).setEnd(a.endContainer,a.endOffset)},collapse:function(a){var b=this;return a?(b.endContainer=b.startContainer,b.endOffset=b.startOffset):(b.startContainer=b.endContainer,b.startOffset=b.endOffset),b.collapsed=!0,b},shrinkBoundary:function(a){function b(a){return 1==a.nodeType&&!domUtils.isBookmarkNode(a)&&!dtd.$empty[a.tagName]&&!dtd.$nonChild[a.tagName]}for(var c,d=this,e=d.collapsed;1==d.startContainer.nodeType&&(c=d.startContainer.childNodes[d.startOffset])&&b(c);)d.setStart(c,0);if(e)return d.collapse(!0);if(!a)for(;1==d.endContainer.nodeType&&d.endOffset>0&&(c=d.endContainer.childNodes[d.endOffset-1])&&b(c);)d.setEnd(c,c.childNodes.length);return d},getCommonAncestor:function(a,c){var d=this,e=d.startContainer,f=d.endContainer;return e===f?a&&b(this)&&(e=e.childNodes[d.startOffset],1==e.nodeType)?e:c&&3==e.nodeType?e.parentNode:e:domUtils.getCommonAncestor(e,f)},trimBoundary:function(a){this.txtToElmBoundary();var b=this.startContainer,c=this.startOffset,d=this.collapsed,e=this.endContainer;if(3==b.nodeType){if(0==c)this.setStartBefore(b);else if(c>=b.nodeValue.length)this.setStartAfter(b);else{var f=domUtils.split(b,c);b===e?this.setEnd(f,this.endOffset-c):b.parentNode===e&&(this.endOffset+=1),this.setStartBefore(f)}if(d)return this.collapse(!0)}return a||(c=this.endOffset,e=this.endContainer,3==e.nodeType&&(0==c?this.setEndBefore(e):(c=c.nodeValue.length&&a["set"+b.replace(/(\w)/,function(a){return a.toUpperCase()})+"After"](c):a["set"+b.replace(/(\w)/,function(a){return a.toUpperCase()})+"Before"](c))}return!a&&this.collapsed||(b(this,"start"),b(this,"end")),this},insertNode:function(a){var b=a,c=1;11==a.nodeType&&(b=a.firstChild,c=a.childNodes.length),this.trimBoundary(!0);var d=this.startContainer,e=this.startOffset,f=d.childNodes[e];return f?d.insertBefore(a,f):d.appendChild(a),b.parentNode===this.endContainer&&(this.endOffset=this.endOffset+c),this.setStartBefore(b)},setCursor:function(a,b){return this.collapse(!a).select(b)},createBookmark:function(a,b){var c,d=this.document.createElement("span");return d.style.cssText="display:none;line-height:0px;",d.appendChild(this.document.createTextNode("‍")),d.id="_baidu_bookmark_start_"+(b?"":h++),this.collapsed||(c=d.cloneNode(!0),c.id="_baidu_bookmark_end_"+(b?"":h++)),this.insertNode(d),c&&this.collapse().insertNode(c).setEndBefore(c),this.setStartAfter(d),{start:a?d.id:d,end:c?a?c.id:c:null,id:a}},moveToBookmark:function(a){var b=a.id?this.document.getElementById(a.start):a.start,c=a.end&&a.id?this.document.getElementById(a.end):a.end;return this.setStartBefore(b),domUtils.remove(b),c?(this.setEndBefore(c),domUtils.remove(c)):this.collapse(!0),this},enlarge:function(a,b){var c,d,e=domUtils.isBody,f=this.document.createTextNode("");if(a){for(d=this.startContainer,1==d.nodeType?d.childNodes[this.startOffset]?c=d=d.childNodes[this.startOffset]:(d.appendChild(f),c=d=f):c=d;;){if(domUtils.isBlockElm(d)){for(d=c;(c=d.previousSibling)&&!domUtils.isBlockElm(c);)d=c;this.setStartBefore(d);break}c=d,d=d.parentNode}for(d=this.endContainer,1==d.nodeType?((c=d.childNodes[this.endOffset])?d.insertBefore(f,c):d.appendChild(f),c=d=f):c=d;;){if(domUtils.isBlockElm(d)){for(d=c;(c=d.nextSibling)&&!domUtils.isBlockElm(c);)d=c;this.setEndAfter(d);break}c=d,d=d.parentNode}f.parentNode===this.endContainer&&this.endOffset--,domUtils.remove(f)}if(!this.collapsed){for(;!(0!=this.startOffset||b&&b(this.startContainer)||e(this.startContainer));)this.setStartBefore(this.startContainer);for(;!(this.endOffset!=(1==this.endContainer.nodeType?this.endContainer.childNodes.length:this.endContainer.nodeValue.length)||b&&b(this.endContainer)||e(this.endContainer));)this.setEndAfter(this.endContainer)}return this},enlargeToBlockElm:function(a){for(;!domUtils.isBlockElm(this.startContainer);)this.setStartBefore(this.startContainer);if(!a)for(;!domUtils.isBlockElm(this.endContainer);)this.setEndAfter(this.endContainer);return this},adjustmentBoundary:function(){if(!this.collapsed){for(;!domUtils.isBody(this.startContainer)&&this.startOffset==this.startContainer[3==this.startContainer.nodeType?"nodeValue":"childNodes"].length&&this.startContainer[3==this.startContainer.nodeType?"nodeValue":"childNodes"].length;)this.setStartAfter(this.startContainer);for(;!domUtils.isBody(this.endContainer)&&!this.endOffset&&this.endContainer[3==this.endContainer.nodeType?"nodeValue":"childNodes"].length;)this.setEndBefore(this.endContainer)}return this},applyInlineStyle:function(a,b,c){if(this.collapsed)return this;this.trimBoundary().enlarge(!1,function(a){return 1==a.nodeType&&domUtils.isBlockElm(a)}).adjustmentBoundary();for(var d,e,f=this.createBookmark(),g=f.end,h=function(a){return 1==a.nodeType?"br"!=a.tagName.toLowerCase():!domUtils.isWhitespace(a)},i=domUtils.getNextDomNode(f.start,!1,h),j=this.cloneRange();i&&domUtils.getPosition(i,g)&domUtils.POSITION_PRECEDING;)if(3==i.nodeType||dtd[a][i.tagName]){for(j.setStartBefore(i),d=i;d&&(3==d.nodeType||dtd[a][d.tagName])&&d!==g;)e=d,d=domUtils.getNextDomNode(d,1==d.nodeType,null,function(b){return dtd[a][b.tagName]});var k,l=j.setEndAfter(e).extractContents();if(c&&c.length>0){var m,n;n=m=c[0].cloneNode(!1);for(var o,p=1;o=c[p++];)m.appendChild(o.cloneNode(!1)),m=m.firstChild;k=m}else k=j.document.createElement(a);b&&domUtils.setAttributes(k,b),k.appendChild(l),j.insertNode(c?n:k);var q;if("span"==a&&b.style&&/text\-decoration/.test(b.style)&&(q=domUtils.findParentByTagName(k,"a",!0))?(domUtils.setAttributes(q,b),domUtils.remove(k,!0),k=q):(domUtils.mergeSibling(k),domUtils.clearEmptySibling(k)),domUtils.mergeChild(k,b),i=domUtils.getNextDomNode(k,!1,h),domUtils.mergeToParent(k),d===g)break}else i=domUtils.getNextDomNode(i,!0,h);return this.moveToBookmark(f)},removeInlineStyle:function(a){if(this.collapsed)return this;a=utils.isArray(a)?a:[a],this.shrinkBoundary().adjustmentBoundary();for(var b=this.startContainer,c=this.endContainer;;){if(1==b.nodeType){if(utils.indexOf(a,b.tagName.toLowerCase())>-1)break;if("body"==b.tagName.toLowerCase()){b=null;break}}b=b.parentNode}for(;;){if(1==c.nodeType){if(utils.indexOf(a,c.tagName.toLowerCase())>-1)break;if("body"==c.tagName.toLowerCase()){c=null;break}}c=c.parentNode}var d,e,f=this.createBookmark();b&&(e=this.cloneRange().setEndBefore(f.start).setStartBefore(b),d=e.extractContents(),e.insertNode(d),domUtils.clearEmptySibling(b,!0),b.parentNode.insertBefore(f.start,b)),c&&(e=this.cloneRange().setStartAfter(f.end).setEndAfter(c),d=e.extractContents(),e.insertNode(d),domUtils.clearEmptySibling(c,!1,!0),c.parentNode.insertBefore(f.end,c.nextSibling));for(var g,h=domUtils.getNextDomNode(f.start,!1,function(a){return 1==a.nodeType});h&&h!==f.end;)g=domUtils.getNextDomNode(h,!0,function(a){return 1==a.nodeType}),utils.indexOf(a,h.tagName.toLowerCase())>-1&&domUtils.remove(h,!0),h=g;return this.moveToBookmark(f)},getClosedNode:function(){var a;if(!this.collapsed){var c=this.cloneRange().adjustmentBoundary().shrinkBoundary();if(b(c)){var d=c.startContainer.childNodes[c.startOffset];d&&1==d.nodeType&&(dtd.$empty[d.tagName]||dtd.$nonChild[d.tagName])&&(a=d)}}return a},select:browser.ie?function(a,b){var c;this.collapsed||this.shrinkBoundary();var d=this.getClosedNode();if(d&&!b){try{c=this.document.body.createControlRange(),c.addElement(d),c.select()}catch(h){}return this}var j,k=this.createBookmark(),l=k.start;if(c=this.document.body.createTextRange(),c.moveToElementText(l),c.moveStart("character",1),this.collapsed){if(!a&&3!=this.startContainer.nodeType){var m=this.document.createTextNode(i),n=this.document.createElement("span");n.appendChild(this.document.createTextNode(i)),l.parentNode.insertBefore(n,l),l.parentNode.insertBefore(m,l),e(this.document,m),g=m,f(n,"previousSibling"),f(l,"nextSibling"),c.moveStart("character",-1),c.collapse(!0)}}else{var o=this.document.body.createTextRange();j=k.end,o.moveToElementText(j),c.setEndPoint("EndToEnd",o)}this.moveToBookmark(k),n&&domUtils.remove(n);try{c.select()}catch(h){}return this}:function(a){function b(a){function b(b,c,d){3==b.nodeType&&b.nodeValue.lengthi&&(i=0),g.push(i),g}var d={},e=this;return d.startAddress=c(!0),a||(d.endAddress=e.collapsed?[].concat(d.startAddress):c()),d},moveToAddress:function(a,b){function c(a,b){for(var c,e,f,g=d.document.body,h=0,i=a.length;i>h;h++)if(f=a[h],c=g,g=g.childNodes[f],!g){e=f;break}b?g?d.setStartBefore(g):d.setStart(c,e):g?d.setEndBefore(g):d.setEnd(c,e)}var d=this;return c(a.startAddress,!0),!b&&a.endAddress&&c(a.endAddress),d},equals:function(a){for(var b in this)if(this.hasOwnProperty(b)&&this[b]!==a[b])return!1;return!0},traversal:function(a,b){if(this.collapsed)return this;for(var c=this.createBookmark(),d=c.end,e=domUtils.getNextDomNode(c.start,!1,b);e&&e!==d&&domUtils.getPosition(e,d)&domUtils.POSITION_PRECEDING;){var f=domUtils.getNextDomNode(e,!1,b);a(e),e=f}return this.moveToBookmark(c)}}}(),function(){function a(a,b){var c=domUtils.getNodeIndex;a=a.duplicate(),a.collapse(b);var d=a.parentElement();if(!d.hasChildNodes())return{container:d,offset:0};for(var e,f,g=d.children,h=a.duplicate(),i=0,j=g.length-1,k=-1;j>=i;){k=Math.floor((i+j)/2),e=g[k],h.moveToElementText(e);var l=h.compareEndPoints("StartToStart",a);if(l>0)j=k-1;else{if(!(0>l))return{container:d,offset:c(e)};i=k+1}}if(-1==k){if(h.moveToElementText(d),h.setEndPoint("StartToStart",a),f=h.text.replace(/(\r\n|\r)/g,"\n").length,g=d.childNodes,!f)return e=g[g.length-1],{container:e,offset:e.nodeValue.length};for(var m=g.length;f>0;)f-=g[--m].nodeValue.length;return{container:g[m],offset:-f}}if(h.collapse(l>0),h.setEndPoint(l>0?"StartToStart":"EndToStart",a),f=h.text.replace(/(\r\n|\r)/g,"\n").length,!f)return dtd.$empty[e.tagName]||dtd.$nonChild[e.tagName]?{container:d,offset:c(e)+(l>0?0:1)}:{container:e,offset:l>0?0:e.childNodes.length};for(;f>0;)try{var n=e;e=e[l>0?"previousSibling":"nextSibling"],f-=e.nodeValue.length}catch(o){return{container:d,offset:c(n)}}return{container:e,offset:l>0?-f:e.nodeValue.length+f}}function b(b,c){if(b.item)c.selectNode(b.item(0));else{var d=a(b,!0);c.setStart(d.container,d.offset),0!=b.compareEndPoints("StartToEnd",b)&&(d=a(b,!1),c.setEnd(d.container,d.offset))}return c}function c(a){var b;try{b=a.getNative().createRange()}catch(c){return null}var d=b.item?b.item(0):b.parentElement();return(d.ownerDocument||d)===a.document?b:null}var d=dom.Selection=function(a){var b,d=this;d.document=a,browser.ie9below&&(b=domUtils.getWindow(a).frameElement,domUtils.on(b,"beforedeactivate",function(){d._bakIERange=d.getIERange()}),domUtils.on(b,"activate",function(){try{!c(d)&&d._bakIERange&&d._bakIERange.select()}catch(a){}d._bakIERange=null})),b=a=null};d.prototype={rangeInBody:function(a,b){var c=browser.ie9below||b?a.item?a.item():a.parentElement():a.startContainer;return c===this.document.body||domUtils.inDoc(c,this.document)},getNative:function(){var a=this.document;try{return a?browser.ie9below?a.selection:domUtils.getWindow(a).getSelection():null}catch(b){return null}},getIERange:function(){var a=c(this);return!a&&this._bakIERange?this._bakIERange:a},cache:function(){this.clear(),this._cachedRange=this.getRange(),this._cachedStartElement=this.getStart(),this._cachedStartElementPath=this.getStartElementPath()},getStartElementPath:function(){if(this._cachedStartElementPath)return this._cachedStartElementPath;var a=this.getStart();return a?domUtils.findParents(a,!0,null,!0):[]},clear:function(){this._cachedStartElementPath=this._cachedRange=this._cachedStartElement=null},isFocus:function(){try{if(browser.ie9below){var a=c(this);return!(!a||!this.rangeInBody(a))}return!!this.getNative().rangeCount}catch(b){return!1}},getRange:function(){function a(a){for(var b=c.document.body.firstChild,d=a.collapsed;b&&b.firstChild;)a.setStart(b,0),b=b.firstChild;a.startContainer||a.setStart(c.document.body,0),d&&a.collapse(!0)}var c=this;if(null!=c._cachedRange)return this._cachedRange;var d=new baidu.editor.dom.Range(c.document);if(browser.ie9below){var e=c.getIERange();if(e)try{b(e,d)}catch(f){a(d)}else a(d)}else{var g=c.getNative();if(g&&g.rangeCount){var h=g.getRangeAt(0),i=g.getRangeAt(g.rangeCount-1);d.setStart(h.startContainer,h.startOffset).setEnd(i.endContainer,i.endOffset),d.collapsed&&domUtils.isBody(d.startContainer)&&!d.startOffset&&a(d)}else{if(this._bakRange&&domUtils.inDoc(this._bakRange.startContainer,this.document))return this._bakRange;a(d)}}return this._bakRange=d},getStart:function(){if(this._cachedStartElement)return this._cachedStartElement;var a,b,c,d,e=browser.ie9below?this.getIERange():this.getRange();if(browser.ie9below){if(!e)return this.document.body.firstChild;if(e.item)return e.item(0);for(a=e.duplicate(),a.text.length>0&&a.moveStart("character",1),a.collapse(1),b=a.parentElement(),d=c=e.parentElement();c=c.parentNode;)if(c==b){b=d;break}}else if(e.shrinkBoundary(),b=e.startContainer,1==b.nodeType&&b.hasChildNodes()&&(b=b.childNodes[Math.min(b.childNodes.length-1,e.startOffset)]),3==b.nodeType)return b.parentNode;return b},getText:function(){var a,b;return this.isFocus()&&(a=this.getNative())?(b=browser.ie9below?a.createRange():a.getRangeAt(0),browser.ie9below?b.text:b.toString()):""},clearRange:function(){this.getNative()[browser.ie9below?"empty":"removeAllRanges"]()}}}(),function(){function a(a,b){var c;if(b.textarea)if(utils.isString(b.textarea)){for(var d,e=0,f=domUtils.getElementsByTagName(a,"textarea");d=f[e++];)if(d.id=="ueditor_textarea_"+b.options.textarea){c=d;break}}else c=b.textarea;c||(a.appendChild(c=domUtils.createElement(document,"textarea",{name:b.options.textarea,id:"ueditor_textarea_"+b.options.textarea,style:"display:none"})),b.textarea=c),!c.getAttribute("name")&&c.setAttribute("name",b.options.textarea),c.value=b.hasContents()?b.options.allHtmlEnabled?b.getAllHtml():b.getContent(null,null,!0):""}function b(a){for(var b in a)return b}function c(a){a.langIsReady=!0,a.fireEvent("langReady")}var d,e=0,f=UE.Editor=function(a){var d=this;d.uid=e++,EventBase.call(d),d.commands={},d.options=utils.extend(utils.clone(a||{}),UEDITOR_CONFIG,!0),d.shortcutkeys={},d.inputRules=[],d.outputRules=[],d.setOpt(f.defaultOptions(d)),d.loadServerConfig(),utils.isEmptyObject(UE.I18N)?utils.loadFile(document,{src:d.options.langPath+d.options.lang+"/"+d.options.lang+".js",tag:"script",type:"text/javascript",defer:"defer"},function(){UE.plugin.load(d),c(d)}):(d.options.lang=b(UE.I18N),UE.plugin.load(d),c(d)),UE.instants["ueditorInstant"+d.uid]=d};f.prototype={registerCommand:function(a,b){this.commands[a]=b},ready:function(a){var b=this;a&&(b.isReady?a.apply(b):b.addListener("ready",a))},setOpt:function(a,b){var c={};utils.isString(a)?c[a]=b:c=a,utils.extend(this.options,c,!0)},getOpt:function(a){return this.options[a]},destroy:function(){var a=this;a.fireEvent("destroy");var b=a.container.parentNode,c=a.textarea;c?c.style.display="":(c=document.createElement("textarea"),b.parentNode.insertBefore(c,b)),c.style.width=a.iframe.offsetWidth+"px",c.style.height=a.iframe.offsetHeight+"px",c.value=a.getContent(),c.id=a.key,b.innerHTML="",domUtils.remove(b);var d=a.key;for(var e in a)a.hasOwnProperty(e)&&delete this[e];UE.delEditor(d)},render:function(a){var b=this,c=b.options,d=function(b){return parseInt(domUtils.getComputedStyle(a,b))};if(utils.isString(a)&&(a=document.getElementById(a)),a){c.initialFrameWidth?c.minFrameWidth=c.initialFrameWidth:c.minFrameWidth=c.initialFrameWidth=a.offsetWidth,c.initialFrameHeight?c.minFrameHeight=c.initialFrameHeight:c.initialFrameHeight=c.minFrameHeight=a.offsetHeight,a.style.width=/%$/.test(c.initialFrameWidth)?"100%":c.initialFrameWidth-d("padding-left")-d("padding-right")+"px",a.style.height=/%$/.test(c.initialFrameHeight)?"100%":c.initialFrameHeight-d("padding-top")-d("padding-bottom")+"px",a.style.zIndex=c.zIndex;var e=(ie&&browser.version<9?"":"")+""+(c.iframeCssUrl?"":"")+(c.initialStyle?"":"")+"";a.appendChild(domUtils.createElement(document,"iframe",{id:"ueditor_"+b.uid,width:"100%",height:"100%",frameborder:"0",src:"javascript:void(function(){document.open();"+(c.customDomain&&document.domain!=location.hostname?'document.domain="'+document.domain+'";':"")+'document.write("'+e+'");document.close();}())'})),a.style.overflow="hidden",setTimeout(function(){/%$/.test(c.initialFrameWidth)&&(c.minFrameWidth=c.initialFrameWidth=a.offsetWidth),/%$/.test(c.initialFrameHeight)&&(c.minFrameHeight=c.initialFrameHeight=a.offsetHeight,a.style.height=c.initialFrameHeight+"px")})}},_setup:function(b){var c=this,d=c.options;ie?(b.body.disabled=!0,b.body.contentEditable=!0,b.body.disabled=!1):b.body.contentEditable=!0,b.body.spellcheck=!1,c.document=b,c.window=b.defaultView||b.parentWindow,c.iframe=c.window.frameElement,c.body=b.body,c.selection=new dom.Selection(b);var e;browser.gecko&&(e=this.selection.getNative())&&e.removeAllRanges(),this._initEvents();for(var f=this.iframe.parentNode;!domUtils.isBody(f);f=f.parentNode)if("FORM"==f.tagName){c.form=f,c.options.autoSyncData?domUtils.on(c.window,"blur",function(){a(f,c)}):domUtils.on(f,"submit",function(){a(this,c)});break}if(d.initialContent)if(d.autoClearinitialContent){var g=c.execCommand;c.execCommand=function(){return c.fireEvent("firstBeforeExecCommand"),g.apply(c,arguments)},this._setDefaultContent(d.initialContent)}else this.setContent(d.initialContent,!1,!0);domUtils.isEmptyNode(c.body)&&(c.body.innerHTML="

              "+(browser.ie?"":"
              ")+"

              "),d.focus&&setTimeout(function(){c.focus(c.options.focusInEnd),!c.options.autoClearinitialContent&&c._selectionChange()},0),c.container||(c.container=this.iframe.parentNode),d.fullscreen&&c.ui&&c.ui.setFullScreen(!0);try{c.document.execCommand("2D-position",!1,!1)}catch(h){}try{c.document.execCommand("enableInlineTableEditing",!1,!1)}catch(h){}try{c.document.execCommand("enableObjectResizing",!1,!1)}catch(h){}c._bindshortcutKeys(),c.isReady=1,c.fireEvent("ready"),d.onready&&d.onready.call(c),browser.ie9below||domUtils.on(c.window,["blur","focus"],function(a){if("blur"==a.type){c._bakRange=c.selection.getRange();try{c._bakNativeRange=c.selection.getNative().getRangeAt(0),c.selection.getNative().removeAllRanges()}catch(a){c._bakNativeRange=null}}else try{c._bakRange&&c._bakRange.select()}catch(a){}}),browser.gecko&&browser.version<=10902&&(c.body.contentEditable=!1,setTimeout(function(){c.body.contentEditable=!0},100),setInterval(function(){c.body.style.height=c.iframe.offsetHeight-20+"px"},100)),!d.isShow&&c.setHide(),d.readonly&&c.setDisabled()},sync:function(b){var c=this,d=b?document.getElementById(b):domUtils.findParent(c.iframe.parentNode,function(a){return"FORM"==a.tagName},!0);d&&a(d,c)},setHeight:function(a,b){a!==parseInt(this.iframe.parentNode.style.height)&&(this.iframe.parentNode.style.height=a+"px"),!b&&(this.options.minFrameHeight=this.options.initialFrameHeight=a),this.body.style.height=a+"px",!b&&this.trigger("setHeight")},addshortcutkey:function(a,b){var c={};b?c[a]=b:c=a,utils.extend(this.shortcutkeys,c)},_bindshortcutKeys:function(){var a=this,b=this.shortcutkeys;a.addListener("keydown",function(c,d){var e=d.keyCode||d.which;for(var f in b)for(var g,h=b[f].split(","),i=0;g=h[i++];){g=g.split(":");var j=g[0],k=g[1];(/^(ctrl)(\+shift)?\+(\d+)$/.test(j.toLowerCase())||/^(\d+)$/.test(j))&&(("ctrl"==RegExp.$1?d.ctrlKey||d.metaKey:0)&&(""!=RegExp.$2?d[RegExp.$2.slice(1)+"Key"]:1)&&e==RegExp.$3||e==RegExp.$1)&&(-1!=a.queryCommandState(f,k)&&a.execCommand(f,k),domUtils.preventDefault(d))}})},getContent:function(a,b,c,d,e){var f=this;if(a&&utils.isFunction(a)&&(b=a,a=""),b?!b():!this.hasContents())return"";f.fireEvent("beforegetcontent");var g=UE.htmlparser(f.body.innerHTML,d);return f.filterOutputRule(g),f.fireEvent("aftergetcontent",a,g),g.toHtml(e)},getAllHtml:function(){var a=this,b=[];if(a.fireEvent("getAllHtml",b),browser.ie&&browser.version>8){var c="";utils.each(a.document.styleSheets,function(a){c+=a.href?'':""}),utils.each(a.document.getElementsByTagName("script"),function(a){c+=a.outerHTML})}return""+(a.options.charset?'':"")+(c||a.document.getElementsByTagName("head")[0].innerHTML)+b.join("\n")+""+a.getContent(null,null,!0)+""},getPlainTxt:function(){var a=new RegExp(domUtils.fillChar,"g"),b=this.body.innerHTML.replace(/[\n\r]/g,"");return b=b.replace(/<(p|div)[^>]*>(| )<\/\1>/gi,"\n").replace(//gi,"\n").replace(/<[^>\/]+>/g,"").replace(/(\n)?<\/([^>]+)>/g,function(a,b,c){return dtd.$block[c]?"\n":b?b:""}),b.replace(a,"").replace(/\u00a0/g," ").replace(/ /g," ")},getContentTxt:function(){var a=new RegExp(domUtils.fillChar,"g");return this.body[browser.ie?"innerText":"textContent"].replace(a,"").replace(/\u00a0/g," ")},setContent:function(b,c,d){function e(a){return"DIV"==a.tagName&&a.getAttribute("cdata_tag")}var f=this;f.fireEvent("beforesetcontent",b);var g=UE.htmlparser(b);if(f.filterInputRule(g),b=g.toHtml(),f.body.innerHTML=(c?f.body.innerHTML:"")+b,"p"==f.options.enterTag){var h,i=this.body.firstChild;if(!i||1==i.nodeType&&(dtd.$cdata[i.tagName]||e(i)||domUtils.isCustomeNode(i))&&i===this.body.lastChild)this.body.innerHTML="

              "+(browser.ie?" ":"
              ")+"

              "+this.body.innerHTML;else for(var j=f.document.createElement("p");i;){for(;i&&(3==i.nodeType||1==i.nodeType&&dtd.p[i.tagName]&&!dtd.$cdata[i.tagName]);)h=i.nextSibling,j.appendChild(i),i=h;if(j.firstChild){if(!i){f.body.appendChild(j);break}i.parentNode.insertBefore(j,i),j=f.document.createElement("p")}i=i.nextSibling}}f.fireEvent("aftersetcontent"),f.fireEvent("contentchange"),!d&&f._selectionChange(),f._bakRange=f._bakIERange=f._bakNativeRange=null;var k;browser.gecko&&(k=this.selection.getNative())&&k.removeAllRanges(),f.options.autoSyncData&&f.form&&a(f.form,f)},focus:function(a){try{var b=this,c=b.selection.getRange();if(a){var d=b.body.lastChild;d&&1==d.nodeType&&!dtd.$empty[d.tagName]&&(domUtils.isEmptyBlock(d)?c.setStartAtFirst(d):c.setStartAtLast(d),c.collapse(!0)),c.setCursor(!0)}else{if(!c.collapsed&&domUtils.isBody(c.startContainer)&&0==c.startOffset){var d=b.body.firstChild;d&&1==d.nodeType&&!dtd.$empty[d.tagName]&&c.setStartAtFirst(d).collapse(!0)}c.select(!0)}this.fireEvent("focus selectionchange")}catch(e){}},isFocus:function(){return this.selection.isFocus()},blur:function(){var a=this.selection.getNative();if(a.empty&&browser.ie){var b=document.body.createTextRange();b.moveToElementText(document.body),b.collapse(!0),b.select(),a.empty()}else a.removeAllRanges()},_initEvents:function(){var a=this,b=a.document,c=a.window;a._proxyDomEvent=utils.bind(a._proxyDomEvent,a),domUtils.on(b,["click","contextmenu","mousedown","keydown","keyup","keypress","mouseup","mouseover","mouseout","selectstart"],a._proxyDomEvent),domUtils.on(c,["focus","blur"],a._proxyDomEvent),domUtils.on(a.body,"drop",function(b){browser.gecko&&b.stopPropagation&&b.stopPropagation(),a.fireEvent("contentchange")}),domUtils.on(b,["mouseup","keydown"],function(b){"keydown"==b.type&&(b.ctrlKey||b.metaKey||b.shiftKey||b.altKey)||2!=b.button&&a._selectionChange(250,b)})},_proxyDomEvent:function(a){return this.fireEvent("before"+a.type.replace(/^on/,"").toLowerCase())===!1?!1:this.fireEvent(a.type.replace(/^on/,""),a)===!1?!1:this.fireEvent("after"+a.type.replace(/^on/,"").toLowerCase())},_selectionChange:function(a,b){var c,e,f=this,g=!1;if(browser.ie&&browser.version<9&&b&&"mouseup"==b.type){var h=this.selection.getRange();h.collapsed||(g=!0,c=b.clientX,e=b.clientY)}clearTimeout(d),d=setTimeout(function(){if(f.selection&&f.selection.getNative()){var a;if(g&&"None"==f.selection.getNative().type){a=f.document.body.createTextRange();try{a.moveToPoint(c,e)}catch(d){a=null}}var h;a&&(h=f.selection.getIERange,f.selection.getIERange=function(){return a}),f.selection.cache(),h&&(f.selection.getIERange=h),f.selection._cachedRange&&f.selection._cachedStartElement&&(f.fireEvent("beforeselectionchange"),f.fireEvent("selectionchange",!!b),f.fireEvent("afterselectionchange"),f.selection.clear())}},a||50)},_callCmdFn:function(a,b){var c,d,e=b[0].toLowerCase();return c=this.commands[e]||UE.commands[e],d=c&&c[a],c&&d||"queryCommandState"!=a?d?d.apply(this,b):void 0:0},execCommand:function(a){a=a.toLowerCase();var b,c=this,d=c.commands[a]||UE.commands[a];return d&&d.execCommand?(d.notNeedUndo||c.__hasEnterExecCommand?(b=this._callCmdFn("execCommand",arguments),!c.__hasEnterExecCommand&&!d.ignoreContentChange&&!c._ignoreContentChange&&c.fireEvent("contentchange")):(c.__hasEnterExecCommand=!0,-1!=c.queryCommandState.apply(c,arguments)&&(c.fireEvent("saveScene"),c.fireEvent.apply(c,["beforeexeccommand",a].concat(arguments)),b=this._callCmdFn("execCommand",arguments),c.fireEvent.apply(c,["afterexeccommand",a].concat(arguments)),c.fireEvent("saveScene")),c.__hasEnterExecCommand=!1),!c.__hasEnterExecCommand&&!d.ignoreContentChange&&!c._ignoreContentChange&&c._selectionChange(),b):null},queryCommandState:function(a){return this._callCmdFn("queryCommandState",arguments)},queryCommandValue:function(a){return this._callCmdFn("queryCommandValue",arguments)},hasContents:function(a){if(a)for(var b,c=0;b=a[c++];)if(this.document.getElementsByTagName(b).length>0)return!0;if(!domUtils.isEmptyBlock(this.body))return!0;for(a=["div"],c=0;b=a[c++];)for(var d,e=domUtils.getElementsByTagName(this.document,b),f=0;d=e[f++];)if(domUtils.isCustomeNode(d))return!0;return!1},reset:function(){this.fireEvent("reset")},setEnabled:function(){var a,b=this;if("false"==b.body.contentEditable){b.body.contentEditable=!0,a=b.selection.getRange();try{a.moveToBookmark(b.lastBk),delete b.lastBk}catch(c){a.setStartAtFirst(b.body).collapse(!0)}a.select(!0),b.bkqueryCommandState&&(b.queryCommandState=b.bkqueryCommandState,delete b.bkqueryCommandState),b.bkqueryCommandValue&&(b.queryCommandValue=b.bkqueryCommandValue,delete b.bkqueryCommandValue),b.fireEvent("selectionchange")}},enable:function(){return this.setEnabled()},setDisabled:function(a){var b=this;a=a?utils.isArray(a)?a:[a]:[],"true"==b.body.contentEditable&&(b.lastBk||(b.lastBk=b.selection.getRange().createBookmark(!0)),b.body.contentEditable=!1,b.bkqueryCommandState=b.queryCommandState,b.bkqueryCommandValue=b.queryCommandValue,b.queryCommandState=function(c){return-1!=utils.indexOf(a,c)?b.bkqueryCommandState.apply(b,arguments):-1},b.queryCommandValue=function(c){return-1!=utils.indexOf(a,c)?b.bkqueryCommandValue.apply(b,arguments):null},b.fireEvent("selectionchange"))},disable:function(a){return this.setDisabled(a)},_setDefaultContent:function(){function a(){var b=this;b.document.getElementById("initContent")&&(b.body.innerHTML="

              "+(ie?"":"
              ")+"

              ",b.removeListener("firstBeforeExecCommand focus",a),setTimeout(function(){b.focus(),b._selectionChange()},0))}return function(b){var c=this;c.body.innerHTML='

              '+b+"

              ",c.addListener("firstBeforeExecCommand focus",a)}}(),setShow:function(){var a=this,b=a.selection.getRange();if("none"==a.container.style.display){try{b.moveToBookmark(a.lastBk),delete a.lastBk}catch(c){b.setStartAtFirst(a.body).collapse(!0)}setTimeout(function(){b.select(!0)},100),a.container.style.display=""}},show:function(){return this.setShow()},setHide:function(){ +var a=this;a.lastBk||(a.lastBk=a.selection.getRange().createBookmark(!0)),a.container.style.display="none"},hide:function(){return this.setHide()},getLang:function(a){var b=UE.I18N[this.options.lang];if(!b)throw Error("not import language file");a=(a||"").split(".");for(var c,d=0;(c=a[d++])&&(b=b[c],b););return b},getContentLength:function(a,b){var c=this.getContent(!1,!1,!0).length;if(a){b=(b||[]).concat(["hr","img","iframe"]),c=this.getContentTxt().replace(/[\t\r\n]+/g,"").length;for(var d,e=0;d=b[e++];)c+=this.document.getElementsByTagName(d).length}return c},addInputRule:function(a){this.inputRules.push(a)},filterInputRule:function(a){for(var b,c=0;b=this.inputRules[c++];)b.call(this,a)},addOutputRule:function(a){this.outputRules.push(a)},filterOutputRule:function(a){for(var b,c=0;b=this.outputRules[c++];)b.call(this,a)},getActionUrl:function(a){var b=this.getOpt(a)||a,c=this.getOpt("imageUrl"),d=this.getOpt("serverUrl");return!d&&c&&(d=c.replace(/^(.*[\/]).+([\.].+)$/,"$1controller$2")),d?(d=d+(-1==d.indexOf("?")?"?":"&")+"action="+(b||""),utils.formatUrl(d)):""}},utils.inherits(f,EventBase)}(),UE.Editor.defaultOptions=function(a){var b=a.options.UEDITOR_HOME_URL;return{isShow:!0,initialContent:"",initialStyle:"",autoClearinitialContent:!1,iframeCssUrl:b+"themes/iframe.css",textarea:"editorValue",focus:!1,focusInEnd:!0,autoClearEmptyNode:!0,fullscreen:!1,readonly:!1,zIndex:999,imagePopup:!0,enterTag:"p",customDomain:!1,lang:"zh-cn",langPath:b+"lang/",theme:"default",themePath:b+"themes/",allHtmlEnabled:!1,scaleEnabled:!1,tableNativeEditInFF:!1,autoSyncData:!0,fileNameFormat:"{time}{rand:6}"}},function(){UE.Editor.prototype.loadServerConfig=function(){function showErrorMsg(a){console&&console.error(a)}var me=this;setTimeout(function(){try{me.options.imageUrl&&me.setOpt("serverUrl",me.options.imageUrl.replace(/^(.*[\/]).+([\.].+)$/,"$1controller$2"));var configUrl=me.getActionUrl("config"),isJsonp=utils.isCrossDomainUrl(configUrl);me._serverConfigLoaded=!1,configUrl&&UE.ajax.request(configUrl,{method:"GET",dataType:isJsonp?"jsonp":"",onsuccess:function(r){try{var config=isJsonp?r:eval("("+r.responseText+")");utils.extend(me.options,config),me.fireEvent("serverConfigLoaded"),me._serverConfigLoaded=!0}catch(e){showErrorMsg(me.getLang("loadconfigFormatError"))}},onerror:function(){showErrorMsg(me.getLang("loadconfigHttpError"))}})}catch(e){showErrorMsg(me.getLang("loadconfigError"))}})},UE.Editor.prototype.isServerConfigLoaded=function(){var a=this;return a._serverConfigLoaded||!1},UE.Editor.prototype.afterConfigReady=function(a){if(a&&utils.isFunction(a)){var b=this,c=function(){a.apply(b,arguments),b.removeListener("serverConfigLoaded",c)};b.isServerConfigLoaded()?a.call(b,"serverConfigLoaded"):b.addListener("serverConfigLoaded",c)}}}(),UE.ajax=function(){function a(a){var b=[];for(var c in a)if("method"!=c&&"timeout"!=c&&"async"!=c&&"dataType"!=c&&"callback"!=c&&void 0!=a[c]&&null!=a[c])if("function"!=(typeof a[c]).toLowerCase()&&"object"!=(typeof a[c]).toLowerCase())b.push(encodeURIComponent(c)+"="+encodeURIComponent(a[c]));else if(utils.isArray(a[c]))for(var d=0;d/gi,"").replace(/]*>[\s\S]*?.<\/v:shape>/gi,function(a){if(browser.opera)return"";try{if(/Bitmap/i.test(a))return"";var c=a.match(/width:([ \d.]*p[tx])/i)[1],d=a.match(/height:([ \d.]*p[tx])/i)[1],e=a.match(/src=\s*"([^"]*)"/i)[1];return''}catch(f){return""}}).replace(/<\/?div[^>]*>/g,"").replace(/v:\w+=(["']?)[^'"]+\1/g,"").replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|xml|meta|link|style|\w+:\w+)(?=[\s\/>]))[^>]*>/gi,"").replace(/

              ]*class="?MsoHeading"?[^>]*>(.*?)<\/p>/gi,"

              $1

              ").replace(/\s+(class|lang|align)\s*=\s*(['"]?)([\w-]+)\2/gi,function(a,b,c,d){return"class"==b&&"MsoListParagraph"==d?a:""}).replace(/<(font|span)[^>]*>(\s*)<\/\1>/gi,function(a,b,c){return c.replace(/[\t\r\n ]+/g," ")}).replace(/(<[a-z][^>]*)\sstyle=(["'])([^\2]*?)\2/gi,function(a,c,d,e){for(var f,g=[],h=e.replace(/^\s+|\s+$/,"").replace(/'/g,"'").replace(/"/gi,"'").replace(/[\d.]+(cm|pt)/g,function(a){return utils.transUnitToPx(a)}).split(/;\s*/g),i=0;f=h[i];i++){var j,k,l=f.split(":");if(2==l.length){if(j=l[0].toLowerCase(),k=l[1].toLowerCase(),/^(background)\w*/.test(j)&&0==k.replace(/(initial|\s)/g,"").length||/^(margin)\w*/.test(j)&&/^0\w+$/.test(k))continue;switch(j){case"mso-padding-alt":case"mso-padding-top-alt":case"mso-padding-right-alt":case"mso-padding-bottom-alt":case"mso-padding-left-alt":case"mso-margin-alt":case"mso-margin-top-alt":case"mso-margin-right-alt":case"mso-margin-bottom-alt":case"mso-margin-left-alt":case"mso-height":case"mso-width":case"mso-vertical-align-alt":/c;c++)a.push(m)}function c(g,h,i,j){switch(g.type){case"root":for(var k,l=0;k=g.children[l++];)i&&"element"==k.type&&!dtd.$inlineWithA[k.tagName]&&l>1&&(a(h,j,!0),b(h,j)),c(k,h,i,j);break;case"text":d(g,h);break;case"element":e(g,h,i,j);break;case"comment":f(g,h,i)}return h}function d(a,b){"pre"==a.parentNode.tagName?b.push(a.data):b.push(l[a.parentNode.tagName]?utils.html(a.data):a.data.replace(/[ ]{2}/g,"  "))}function e(d,e,f,g){var h="";if(d.attrs){h=[];var i=d.attrs;for(var j in i)h.push(j+(void 0!==i[j]?'="'+(k[j]?utils.html(i[j]).replace(/["]/g,function(a){return"""}):utils.unhtml(i[j]))+'"':""));h=h.join(" ")}if(e.push("<"+d.tagName+(h?" "+h:"")+(dtd.$empty[d.tagName]?"/":"")+">"),f&&!dtd.$inlineWithA[d.tagName]&&"pre"!=d.tagName&&d.children&&d.children.length&&(g=a(e,g,!0),b(e,g)),d.children&&d.children.length)for(var l,m=0;l=d.children[m++];)f&&"element"==l.type&&!dtd.$inlineWithA[l.tagName]&&m>1&&(a(e,g),b(e,g)),c(l,e,f,g);dtd.$empty[d.tagName]||(f&&!dtd.$inlineWithA[d.tagName]&&"pre"!=d.tagName&&d.children&&d.children.length&&(g=a(e,g),b(e,g)),e.push(""))}function f(a,b){b.push("")}function g(a,b){var c;if("element"==a.type&&a.getAttr("id")==b)return a;if(a.children&&a.children.length)for(var d,e=0;d=a.children[e++];)if(c=g(d,b))return c}function h(a,b,c){if("element"==a.type&&a.tagName==b&&c.push(a),a.children&&a.children.length)for(var d,e=0;d=a.children[e++];)h(d,b,c)}function i(a,b){if(a.children&&a.children.length)for(var c,d=0;c=a.children[d];)i(c,b),c.parentNode&&(c.children&&c.children.length&&b(c),c.parentNode&&d++);else b(a)}var j=UE.uNode=function(a){this.type=a.type,this.data=a.data,this.tagName=a.tagName,this.parentNode=a.parentNode,this.attrs=a.attrs||{},this.children=a.children},k={href:1,src:1,_src:1,_href:1,cdata_data:1},l={style:1,script:1},m=" ",n="\n";j.createElement=function(a){return/[<>]/.test(a)?UE.htmlparser(a).children[0]:new j({type:"element",children:[],tagName:a})},j.createText=function(a,b){return new UE.uNode({type:"text",data:b?a:utils.unhtml(a||"")})},j.prototype={toHtml:function(a){var b=[];return c(this,b,a,0),b.join("")},innerHTML:function(a){if("element"!=this.type||dtd.$empty[this.tagName])return this;if(utils.isString(a)){if(this.children)for(var b,c=0;b=this.children[c++];)b.parentNode=null;this.children=[];for(var b,d=UE.htmlparser(a),c=0;b=d.children[c++];)this.children.push(b),b.parentNode=this;return this}var d=new UE.uNode({type:"root",children:this.children});return d.toHtml()},innerText:function(a,b){if("element"!=this.type||dtd.$empty[this.tagName])return this;if(a){if(this.children)for(var c,d=0;c=this.children[d++];)c.parentNode=null;return this.children=[],this.appendChild(j.createText(a,b)),this}return this.toHtml().replace(/<[^>]+>/g,"")},getData:function(){return"element"==this.type?"":this.data},firstChild:function(){return this.children?this.children[0]:null},lastChild:function(){return this.children?this.children[this.children.length-1]:null},previousSibling:function(){for(var a,b=this.parentNode,c=0;a=b.children[c];c++)if(a===this)return 0==c?null:b.children[c-1]},nextSibling:function(){for(var a,b=this.parentNode,c=0;a=b.children[c++];)if(a===this)return b.children[c]},replaceChild:function(a,b){if(this.children){a.parentNode&&a.parentNode.removeChild(a);for(var c,d=0;c=this.children[d];d++)if(c===b)return this.children.splice(d,1,a),b.parentNode=null,a.parentNode=this,a}},appendChild:function(a){if("root"==this.type||"element"==this.type&&!dtd.$empty[this.tagName]){this.children||(this.children=[]),a.parentNode&&a.parentNode.removeChild(a);for(var b,c=0;b=this.children[c];c++)if(b===a){this.children.splice(c,1);break}return this.children.push(a),a.parentNode=this,a}},insertBefore:function(a,b){if(this.children){a.parentNode&&a.parentNode.removeChild(a);for(var c,d=0;c=this.children[d];d++)if(c===b)return this.children.splice(d,0,a),a.parentNode=this,a}},insertAfter:function(a,b){if(this.children){a.parentNode&&a.parentNode.removeChild(a);for(var c,d=0;c=this.children[d];d++)if(c===b)return this.children.splice(d+1,0,a),a.parentNode=this,a}},removeChild:function(a,b){if(this.children)for(var c,d=0;c=this.children[d];d++)if(c===a){if(this.children.splice(d,1),c.parentNode=null,b&&c.children&&c.children.length)for(var e,f=0;e=c.children[f];f++)this.children.splice(d+f,0,e),e.parentNode=this;return c}},getAttr:function(a){return this.attrs&&this.attrs[a.toLowerCase()]},setAttr:function(a,b){if(!a)return void delete this.attrs;if(this.attrs||(this.attrs={}),utils.isObject(a))for(var c in a)a[c]?this.attrs[c.toLowerCase()]=a[c]:delete this.attrs[c];else b?this.attrs[a.toLowerCase()]=b:delete this.attrs[a]},getIndex:function(){for(var a,b=this.parentNode,c=0;a=b.children[c];c++)if(a===this)return c;return-1},getNodeById:function(a){var b;if(this.children&&this.children.length)for(var c,d=0;c=this.children[d++];)if(b=g(c,a))return b},getNodesByTagName:function(a){a=utils.trim(a).replace(/[ ]{2,}/g," ").split(" ");var b=[],c=this;return utils.each(a,function(a){if(c.children&&c.children.length)for(var d,e=0;d=c.children[e++];)h(d,a,b)}),b},getStyle:function(a){var b=this.getAttr("style");if(!b)return"";var c=new RegExp("(^|;)\\s*"+a+":([^;]+)","i"),d=b.match(c);return d&&d[0]?d[2]:""},setStyle:function(a,b){function c(a,b){var c=new RegExp("(^|;)\\s*"+a+":([^;]+;?)","gi");d=d.replace(c,"$1"),b&&(d=a+":"+utils.unhtml(b)+";"+d)}var d=this.getAttr("style");if(d||(d=""),utils.isObject(a))for(var e in a)c(e,a[e]);else c(a,b);this.setAttr("style",utils.trim(d))},traversal:function(a){return this.children&&this.children.length&&i(this,a),this}}}();var htmlparser=UE.htmlparser=function(a,b){function c(a,b){if(m[a.tagName]){var c=k.createElement(m[a.tagName]);a.appendChild(c),c.appendChild(k.createText(b)),a=c}else a.appendChild(k.createText(b))}function d(a,b,c){var e;if(e=l[b]){for(var f,h=a;"root"!=h.type;){if(utils.isArray(e)?-1!=utils.indexOf(e,h.tagName):e==h.tagName){a=h,f=!0;break}h=h.parentNode}f||(a=d(a,utils.isArray(e)?e[0]:e))}var i=new k({parentNode:a,type:"element",tagName:b.toLowerCase(),children:dtd.$empty[b]?null:[]});if(c){for(var m,n={};m=g.exec(c);)n[m[1].toLowerCase()]=j[m[1].toLowerCase()]?m[2]||m[3]||m[4]:utils.unhtml(m[2]||m[3]||m[4]);i.attrs=n}return a.children.push(i),dtd.$empty[b]?a:i}function e(a,b){a.children.push(new k({type:"comment",data:b,parentNode:a}))}var f=/<(?:(?:\/([^>]+)>)|(?:!--([\S|\s]*?)-->)|(?:([^\s\/<>]+)\s*((?:(?:"[^"]*")|(?:'[^']*')|[^"'<>])*)\/?>))/g,g=/([\w\-:.]+)(?:(?:\s*=\s*(?:(?:"([^"]*)")|(?:'([^']*)')|([^\s>]+)))|(?=\s|$))/g,h={b:1,code:1,i:1,u:1,strike:1,s:1,tt:1,strong:1,q:1,samp:1,em:1,span:1,sub:1,img:1,sup:1,font:1,big:1,small:1,iframe:1,a:1,br:1,pre:1};a=a.replace(new RegExp(domUtils.fillChar,"g"),""),b||(a=a.replace(new RegExp("[\\r\\t\\n"+(b?"":" ")+"]*]*)>[\\r\\t\\n"+(b?"":" ")+"]*","g"),function(a,c){return c&&h[c.toLowerCase()]?a.replace(/(^[\n\r]+)|([\n\r]+$)/g,""):a.replace(new RegExp("^[\\r\\n"+(b?"":" ")+"]+"),"").replace(new RegExp("[\\r\\n"+(b?"":" ")+"]+$"),"")}));for(var i,j={href:1,src:1},k=UE.uNode,l={td:"tr",tr:["tbody","thead","tfoot"],tbody:"table",th:"tr",thead:"table",tfoot:"table",caption:"table",li:["ul","ol"],dt:"dl",dd:"dl",option:"select"},m={ol:"li",ul:"li"},n=0,o=0,p=new k({type:"root",children:[]}),q=p;i=f.exec(a);){n=i.index;try{if(n>o&&c(q,a.slice(o,n)),i[3])dtd.$cdata[q.tagName]?c(q,i[0]):q=d(q,i[3].toLowerCase(),i[4]);else if(i[1]){if("root"!=q.type)if(dtd.$cdata[q.tagName]&&!dtd.$cdata[i[1]])c(q,i[0]);else{for(var r=q;"element"==q.type&&q.tagName!=i[1].toLowerCase();)if(q=q.parentNode,"root"==q.type)throw q=r,"break";q=q.parentNode}}else i[2]&&e(q,i[2])}catch(s){}o=f.lastIndex}return o");break;case"div":if(b.getAttr("cdata_tag"))break;if(d=b.getAttr("class"),d&&/^line number\d+/.test(d))break;if(!e)break;for(var f,g=UE.uNode.createElement("p");f=b.firstChild();)"text"!=f.type&&UE.dom.dtd.$block[f.tagName]?g.firstChild()?(b.parentNode.insertBefore(g,b),g=UE.uNode.createElement("p")):b.parentNode.insertBefore(f,b):g.appendChild(f);g.firstChild()&&b.parentNode.insertBefore(g,b),b.parentNode.removeChild(b);break;case"dl":b.tagName="ul";break;case"dt":case"dd":b.tagName="li";break;case"li":var h=b.getAttr("class");h&&/list\-/.test(h)||b.setAttr();var i=b.getNodesByTagName("ol ul");UE.utils.each(i,function(a){b.parentNode.insertAfter(a,b)});break;case"td":case"th":case"caption":b.children&&b.children.length||b.appendChild(browser.ie11below?UE.uNode.createText(" "):UE.uNode.createElement("br"));break;case"table":a.options.disabledTableInTable&&c(b)&&(b.parentNode.insertBefore(UE.uNode.createText(b.innerText()),b),b.parentNode.removeChild(b))}}})}),a.addOutputRule(function(b){var c;b.traversal(function(b){if("element"==b.type){if(a.options.autoClearEmptyNode&&dtd.$inline[b.tagName]&&!dtd.$empty[b.tagName]&&(!b.attrs||utils.isEmptyObject(b.attrs)))return void(b.firstChild()?"span"!=b.tagName||b.attrs&&!utils.isEmptyObject(b.attrs)||b.parentNode.removeChild(b,!0):b.parentNode.removeChild(b));switch(b.tagName){case"div":(c=b.getAttr("cdata_tag"))&&(b.tagName=c,b.appendChild(UE.uNode.createText(b.getAttr("cdata_data"))),b.setAttr({cdata_tag:"",cdata_data:"",_ue_custom_node_:""}));break;case"a":(c=b.getAttr("_href"))&&b.setAttr({href:utils.html(c),_href:""});break;case"span":c=b.getAttr("id"),c&&/^_baidu_bookmark_/i.test(c)&&b.parentNode.removeChild(b);break;case"img":(c=b.getAttr("_src"))&&b.setAttr({src:b.getAttr("_src"),_src:""})}}})})},UE.commands.inserthtml={execCommand:function(a,b,c){var d,e,f=this;if(b&&f.fireEvent("beforeinserthtml",b)!==!0){if(d=f.selection.getRange(),e=d.document.createElement("div"),e.style.display="inline",!c){var g=UE.htmlparser(b);f.options.filterRules&&UE.filterNode(g,f.options.filterRules),f.filterInputRule(g),b=g.toHtml()}if(e.innerHTML=utils.trim(b),!d.collapsed){var h=d.startContainer;if(domUtils.isFillChar(h)&&d.setStartBefore(h),h=d.endContainer,domUtils.isFillChar(h)&&d.setEndAfter(h),d.txtToElmBoundary(),d.endContainer&&1==d.endContainer.nodeType&&(h=d.endContainer.childNodes[d.endOffset],h&&domUtils.isBr(h)&&d.setEndAfter(h)),0==d.startOffset&&(h=d.startContainer,domUtils.isBoundaryNode(h,"firstChild")&&(h=d.endContainer,d.endOffset==(3==h.nodeType?h.nodeValue.length:h.childNodes.length)&&domUtils.isBoundaryNode(h,"lastChild")&&(f.body.innerHTML="

              "+(browser.ie?"":"
              ")+"

              ",d.setStart(f.body.firstChild,0).collapse(!0)))),!d.collapsed&&d.deleteContents(),1==d.startContainer.nodeType){var i,j=d.startContainer.childNodes[d.startOffset];if(j&&domUtils.isBlockElm(j)&&(i=j.previousSibling)&&domUtils.isBlockElm(i)){for(d.setEnd(i,i.childNodes.length).collapse();j.firstChild;)i.appendChild(j.firstChild);domUtils.remove(j)}}}var j,k,i,l,m,n=0;d.inFillChar()&&(j=d.startContainer,domUtils.isFillChar(j)?(d.setStartBefore(j).collapse(!0),domUtils.remove(j)):domUtils.isFillChar(j,!0)&&(j.nodeValue=j.nodeValue.replace(fillCharReg,""),d.startOffset--,d.collapsed&&d.collapse(!0)));var o=domUtils.findParentByTagName(d.startContainer,"li",!0);if(o){for(var p,q;j=e.firstChild;){for(;j&&(3==j.nodeType||!domUtils.isBlockElm(j)||"HR"==j.tagName);)p=j.nextSibling,d.insertNode(j).collapse(),q=j,j=p;if(j)if(/^(ol|ul)$/i.test(j.tagName)){for(;j.firstChild;)q=j.firstChild,domUtils.insertAfter(o,j.firstChild),o=o.nextSibling;domUtils.remove(j)}else{var r;p=j.nextSibling,r=f.document.createElement("li"),domUtils.insertAfter(o,r),r.appendChild(j),q=j,j=p,o=r}}o=domUtils.findParentByTagName(d.startContainer,"li",!0),domUtils.isEmptyBlock(o)&&domUtils.remove(o),q&&d.setStartAfter(q).collapse(!0).select(!0)}else{for(;j=e.firstChild;){if(n){for(var s=f.document.createElement("p");j&&(3==j.nodeType||!dtd.$block[j.tagName]);)m=j.nextSibling,s.appendChild(j),j=m;s.firstChild&&(j=s)}if(d.insertNode(j),m=j.nextSibling,!n&&j.nodeType==domUtils.NODE_ELEMENT&&domUtils.isBlockElm(j)&&(k=domUtils.findParent(j,function(a){return domUtils.isBlockElm(a)}),k&&"body"!=k.tagName.toLowerCase()&&(!dtd[k.tagName][j.nodeName]||j.parentNode!==k))){if(dtd[k.tagName][j.nodeName])for(l=j.parentNode;l!==k;)i=l,l=l.parentNode;else i=k;domUtils.breakParent(j,i||l);var i=j.previousSibling;domUtils.trimWhiteTextNode(i),i.childNodes.length||domUtils.remove(i),!browser.ie&&(p=j.nextSibling)&&domUtils.isBlockElm(p)&&p.lastChild&&!domUtils.isBr(p.lastChild)&&p.appendChild(f.document.createElement("br")),n=1}var p=j.nextSibling;if(!e.firstChild&&p&&domUtils.isBlockElm(p)){d.setStart(p,0).collapse(!0);break}d.setEndAfter(j).collapse()}if(j=d.startContainer,m&&domUtils.isBr(m)&&domUtils.remove(m),domUtils.isBlockElm(j)&&domUtils.isEmptyNode(j))if(m=j.nextSibling)domUtils.remove(j),1==m.nodeType&&dtd.$block[m.tagName]&&d.setStart(m,0).collapse(!0).shrinkBoundary();else try{j.innerHTML=browser.ie?domUtils.fillChar:"
              "}catch(t){d.setStartBefore(j),domUtils.remove(j)}try{d.select(!0)}catch(t){}}setTimeout(function(){d=f.selection.getRange(),d.scrollToView(f.autoHeightEnabled,f.autoHeightEnabled?domUtils.getXY(f.iframe).y:0),f.fireEvent("afterinserthtml",b)},200)}}},UE.plugins.autotypeset=function(){function a(a,b){return a&&3!=a.nodeType?domUtils.isBr(a)?1:a&&a.parentNode&&l[a.tagName.toLowerCase()]?g&&g.contains(a)||a.getAttribute("pagebreak")?0:b?!domUtils.isEmptyBlock(a):domUtils.isEmptyBlock(a,new RegExp("[\\s"+domUtils.fillChar+"]","g")):void 0:0}function b(a){a.style.cssText||(domUtils.removeAttributes(a,["style"]),"span"==a.tagName.toLowerCase()&&domUtils.hasNoAttributes(a)&&domUtils.remove(a,!0))}function c(c,f){var h,l=this;if(f){if(!i.pasteFilter)return;h=l.document.createElement("div"),h.innerHTML=f.html}else h=l.document.body;for(var m,n=domUtils.getElementsByTagName(h,"*"),o=0;m=n[o++];)if(l.fireEvent("excludeNodeinautotype",m)!==!0){if(i.clearFontSize&&m.style.fontSize&&(domUtils.removeStyle(m,"font-size"),b(m)),i.clearFontFamily&&m.style.fontFamily&&(domUtils.removeStyle(m,"font-family"),b(m)),a(m)){if(i.mergeEmptyline)for(var p,q=m.nextSibling,r=domUtils.isBr(m);a(q)&&(p=q,q=p.nextSibling,!r||q&&(!q||domUtils.isBr(q)));)domUtils.remove(p);if(i.removeEmptyline&&domUtils.inDoc(m,h)&&!k[m.parentNode.tagName.toLowerCase()]){if(domUtils.isBr(m)&&(q=m.nextSibling,q&&!domUtils.isBr(q)))continue;domUtils.remove(m);continue}}if(a(m,!0)&&"SPAN"!=m.tagName&&(i.indent&&(m.style.textIndent=i.indentValue),i.textAlign&&(m.style.textAlign=i.textAlign)),i.removeClass&&m.className&&!j[m.className.toLowerCase()]){if(g&&g.contains(m))continue;domUtils.removeAttributes(m,["class"])}if(i.imageBlockLine&&"img"==m.tagName.toLowerCase()&&!m.getAttribute("emotion"))if(f){var s=m;switch(i.imageBlockLine){case"left":case"right":case"none":for(var p,t,q,u=s.parentNode;dtd.$inline[u.tagName]||"A"==u.tagName;)u=u.parentNode;if(p=u,"P"==p.tagName&&"center"==domUtils.getStyle(p,"text-align")&&!domUtils.isBody(p)&&1==domUtils.getChildCount(p,function(a){return!domUtils.isBr(a)&&!domUtils.isWhitespace(a)}))if(t=p.previousSibling,q=p.nextSibling,t&&q&&1==t.nodeType&&1==q.nodeType&&t.tagName==q.tagName&&domUtils.isBlockElm(t)){for(t.appendChild(p.firstChild);q.firstChild;)t.appendChild(q.firstChild);domUtils.remove(p),domUtils.remove(q)}else domUtils.setStyle(p,"text-align","");domUtils.setStyle(s,"float",i.imageBlockLine);break;case"center":if("center"!=l.queryCommandValue("imagefloat")){for(u=s.parentNode,domUtils.setStyle(s,"float","none"),p=s;u&&1==domUtils.getChildCount(u,function(a){return!domUtils.isBr(a)&&!domUtils.isWhitespace(a)})&&(dtd.$inline[u.tagName]||"A"==u.tagName);)p=u,u=u.parentNode;var v=l.document.createElement("p");domUtils.setAttributes(v,{style:"text-align:center"}),p.parentNode.insertBefore(v,p),v.appendChild(p),domUtils.setStyle(p,"float","")}}}else{var w=l.selection.getRange();w.selectNode(m).select(),l.execCommand("imagefloat",i.imageBlockLine)}i.removeEmptyNode&&i.removeTagNames[m.tagName.toLowerCase()]&&domUtils.hasNoAttributes(m)&&domUtils.isEmptyBlock(m)&&domUtils.remove(m)}if(i.tobdc){var x=UE.htmlparser(h.innerHTML);x.traversal(function(a){"text"==a.type&&(a.data=e(a.data))}),h.innerHTML=x.toHtml()}if(i.bdc2sb){var x=UE.htmlparser(h.innerHTML);x.traversal(function(a){"text"==a.type&&(a.data=d(a.data))}),h.innerHTML=x.toHtml()}f&&(f.html=h.innerHTML)}function d(a){for(var b="",c=0;c=65281&&65373>=d?String.fromCharCode(a.charCodeAt(c)-65248):12288==d?String.fromCharCode(a.charCodeAt(c)-12288+32):a.charAt(c)}return b}function e(a){a=utils.html(a);for(var b="",c=0;c0?e.substring(e.indexOf(d.options.imagePath),e.length-1).replace(/"|\(|\)/gi,""):"none"!=e?e.replace(/url\("?|"?\)/gi,""):"";var g=' ",b.push(g)},aftersetcontent:function(){0==c&&b()}},inputRule:function(d){c=!1,utils.each(d.getNodesByTagName("p"),function(d){var e=d.getAttr("data-background");e&&(c=!0,b(a(e)),d.parentNode.removeChild(d))})},outputRule:function(a){var b=this,c=(utils.cssRule(e,b.document)||"").replace(/[\n\r]+/g,"").match(f);c&&a.appendChild(UE.uNode.createElement('


              '))},commands:{background:{execCommand:function(a,c){b(c)},queryCommandValue:function(){var b=this,c=(utils.cssRule(e,b.document)||"").replace(/[\n\r]+/g,"").match(f);return c?a(c[1]):null},notNeedUndo:!0}}}}),UE.commands.imagefloat={execCommand:function(a,b){var c=this,d=c.selection.getRange();if(!d.collapsed){var e=d.getClosedNode();if(e&&"IMG"==e.tagName)switch(b){case"left":case"right":case"none":for(var f,g,h,i=e.parentNode;dtd.$inline[i.tagName]||"A"==i.tagName;)i=i.parentNode;if(f=i,"P"==f.tagName&&"center"==domUtils.getStyle(f,"text-align")){if(!domUtils.isBody(f)&&1==domUtils.getChildCount(f,function(a){return!domUtils.isBr(a)&&!domUtils.isWhitespace(a)}))if(g=f.previousSibling,h=f.nextSibling,g&&h&&1==g.nodeType&&1==h.nodeType&&g.tagName==h.tagName&&domUtils.isBlockElm(g)){for(g.appendChild(f.firstChild);h.firstChild;)g.appendChild(h.firstChild);domUtils.remove(f),domUtils.remove(h)}else domUtils.setStyle(f,"text-align","");d.selectNode(e).select()}domUtils.setStyle(e,"float","none"==b?"":b),"none"==b&&domUtils.removeAttributes(e,"align");break;case"center":if("center"!=c.queryCommandValue("imagefloat")){for(i=e.parentNode,domUtils.setStyle(e,"float",""),domUtils.removeAttributes(e,"align"),f=e;i&&1==domUtils.getChildCount(i,function(a){return!domUtils.isBr(a)&&!domUtils.isWhitespace(a)})&&(dtd.$inline[i.tagName]||"A"==i.tagName);)f=i,i=i.parentNode;d.setStartBefore(f).setCursor(!1),i=c.document.createElement("div"),i.appendChild(f),domUtils.setStyle(f,"float",""),c.execCommand("insertHtml",'

              '+i.innerHTML+"

              "),f=c.document.getElementById("_img_parent_tmp"),f.removeAttribute("id"),f=f.firstChild,d.selectNode(f).select(),h=f.parentNode.nextSibling,h&&domUtils.isEmptyNode(h)&&domUtils.remove(h)}}}},queryCommandValue:function(){var a,b,c=this.selection.getRange();return c.collapsed?"none":(a=c.getClosedNode(),a&&1==a.nodeType&&"IMG"==a.tagName?(b=domUtils.getComputedStyle(a,"float")||a.getAttribute("align"),"none"==b&&(b="center"==domUtils.getComputedStyle(a.parentNode,"text-align")?"center":b),{left:1,right:1,center:1}[b]?b:"none"):"none")},queryCommandState:function(){var a,b=this.selection.getRange();return b.collapsed?-1:(a=b.getClosedNode(),a&&1==a.nodeType&&"IMG"==a.tagName?0:-1)}},UE.commands.insertimage={execCommand:function(a,b){function c(a){utils.each("width,height,border,hspace,vspace".split(","),function(b){a[b]&&(a[b]=parseInt(a[b],10)||0)}),utils.each("src,_src".split(","),function(b){a[b]&&(a[b]=utils.unhtmlForUrl(a[b]))}),utils.each("title,alt".split(","),function(b){a[b]&&(a[b]=utils.unhtml(a[b]))})}if(b=utils.isArray(b)?b:[b],b.length){var d=this,e=d.selection.getRange(),f=e.getClosedNode();if(d.fireEvent("beforeinsertimage",b)!==!0){if(!f||!/img/i.test(f.tagName)||"edui-faked-video"==f.className&&-1==f.className.indexOf("edui-upload-video")||f.getAttribute("word_img")){var g,h=[],i="";if(g=b[0],1==b.length)c(g),i=''+g.alt+'","center"==g.floatStyle&&(i='

              '+i+"

              "),h.push(i);else for(var j=0;g=b[j++];)c(g),i="

              ",h.push(i);d.execCommand("insertHtml",h.join(""))}else{var k=b.shift(),l=k.floatStyle;delete k.floatStyle,domUtils.setAttributes(f,k),d.execCommand("imagefloat",l),b.length>0&&(e.setStartAfter(f).setCursor(!1,!0),d.execCommand("insertimage",b))}d.fireEvent("afterinsertimage",b)}}}},UE.plugins.justify=function(){var a=domUtils.isBlockElm,b={left:1,right:1,center:1,justify:1},c=function(b,c){var d=b.createBookmark(),e=function(a){return 1==a.nodeType?"br"!=a.tagName.toLowerCase()&&!domUtils.isBookmarkNode(a):!domUtils.isWhitespace(a)};b.enlarge(!0);for(var f,g=b.createBookmark(),h=domUtils.getNextDomNode(g.start,!1,e),i=b.cloneRange();h&&!(domUtils.getPosition(h,g.end)&domUtils.POSITION_FOLLOWING);)if(3!=h.nodeType&&a(h))h=domUtils.getNextDomNode(h,!0,e);else{for(i.setStartBefore(h);h&&h!==g.end&&!a(h);)f=h,h=domUtils.getNextDomNode(h,!1,null,function(b){return!a(b)});i.setEndAfter(f);var j=i.getCommonAncestor();if(!domUtils.isBody(j)&&a(j))domUtils.setStyles(j,utils.isString(c)?{"text-align":c}:c),h=j;else{var k=b.document.createElement("p");domUtils.setStyles(k,utils.isString(c)?{"text-align":c}:c);var l=i.extractContents();k.appendChild(l),i.insertNode(k),h=k}h=domUtils.getNextDomNode(h,!1,e)}return b.moveToBookmark(g).moveToBookmark(d)};UE.commands.justify={execCommand:function(a,b){var d,e=this.selection.getRange();return e.collapsed&&(d=this.document.createTextNode("p"),e.insertNode(d)),c(e,b),d&&(e.setStartBefore(d).collapse(!0),domUtils.remove(d)),e.select(),!0},queryCommandValue:function(){var a=this.selection.getStart(),c=domUtils.getComputedStyle(a,"text-align");return b[c]?c:"left"},queryCommandState:function(){var a=this.selection.getStart(),b=a&&domUtils.findParentByTagName(a,["td","th","caption"],!0);return b?-1:0}}},UE.plugins.font=function(){function a(a){for(var b;(b=a.parentNode)&&"SPAN"==b.tagName&&1==domUtils.getChildCount(b,function(a){return!domUtils.isBookmarkNode(a)&&!domUtils.isBr(a)});)b.style.cssText+=a.style.cssText,domUtils.remove(a,!0),a=b}function b(a,b,c){if(g[b]&&(a.adjustmentBoundary(),!a.collapsed&&1==a.startContainer.nodeType)){var d=a.startContainer.childNodes[a.startOffset];if(d&&domUtils.isTagNode(d,"span")){var e=a.createBookmark();utils.each(domUtils.getElementsByTagName(d,"span"),function(a){a.parentNode&&!domUtils.isBookmarkNode(a)&&("backcolor"==b&&domUtils.getComputedStyle(a,"background-color").toLowerCase()===c||(domUtils.removeStyle(a,g[b]),0==a.style.cssText.replace(/^\s+$/,"").length&&domUtils.remove(a,!0)))}),a.moveToBookmark(e)}}}function c(c,d,e){var f,g=c.collapsed,h=c.createBookmark();if(g)for(f=h.start.parentNode;dtd.$inline[f.tagName];)f=f.parentNode;else f=domUtils.getCommonAncestor(h.start,h.end);utils.each(domUtils.getElementsByTagName(f,"span"),function(b){if(b.parentNode&&!domUtils.isBookmarkNode(b)){if(/\s*border\s*:\s*none;?\s*/i.test(b.style.cssText))return void(/^\s*border\s*:\s*none;?\s*$/.test(b.style.cssText)?domUtils.remove(b,!0):domUtils.removeStyle(b,"border"));if(/border/i.test(b.style.cssText)&&"SPAN"==b.parentNode.tagName&&/border/i.test(b.parentNode.style.cssText)&&(b.style.cssText=b.style.cssText.replace(/border[^:]*:[^;]+;?/gi,"")),"fontborder"!=d||"none"!=e)for(var c=b.nextSibling;c&&1==c.nodeType&&"SPAN"==c.tagName;)if(domUtils.isBookmarkNode(c)&&"fontborder"==d)b.appendChild(c),c=b.nextSibling;else{if(c.style.cssText==b.style.cssText&&(domUtils.moveChild(c,b),domUtils.remove(c)),b.nextSibling===c)break;c=b.nextSibling}if(a(b),browser.ie&&browser.version>8){var f=domUtils.findParent(b,function(a){return"SPAN"==a.tagName&&/background-color/.test(a.style.cssText)});f&&!/background-color/.test(b.style.cssText)&&(b.style.backgroundColor=f.style.backgroundColor)}}}),c.moveToBookmark(h),b(c,d,e)}var d=this,e={forecolor:"color",backcolor:"background-color",fontsize:"font-size",fontfamily:"font-family",underline:"text-decoration",strikethrough:"text-decoration",fontborder:"border"},f={underline:1,strikethrough:1,fontborder:1},g={forecolor:"color",backcolor:"background-color",fontsize:"font-size",fontfamily:"font-family"};d.setOpt({fontfamily:[{name:"songti",val:"宋体,SimSun"},{name:"yahei",val:"微软雅黑,Microsoft YaHei"},{name:"kaiti",val:"楷体,楷体_GB2312, SimKai"},{name:"heiti",val:"黑体, SimHei"},{name:"lishu",val:"隶书, SimLi"},{name:"andaleMono",val:"andale mono"},{name:"arial",val:"arial, helvetica,sans-serif"},{name:"arialBlack",val:"arial black,avant garde"},{name:"comicSansMs",val:"comic sans ms"},{name:"impact",val:"impact,chicago"},{name:"timesNewRoman",val:"times new roman"}],fontsize:[10,11,12,14,16,18,20,24,36]}),d.addInputRule(function(a){utils.each(a.getNodesByTagName("u s del font strike"),function(a){if("font"==a.tagName){var b=[];for(var c in a.attrs)switch(c){case"size":b.push("font-size:"+({1:"10",2:"12",3:"16",4:"18",5:"24",6:"32",7:"48"}[a.attrs[c]]||a.attrs[c])+"px");break;case"color":b.push("color:"+a.attrs[c]);break;case"face":b.push("font-family:"+a.attrs[c]);break;case"style":b.push(a.attrs[c])}a.attrs={style:b.join(";")}}else{var d="u"==a.tagName?"underline":"line-through";a.attrs={style:(a.getAttr("style")||"")+"text-decoration:"+d+";"}}a.tagName="span"})});for(var h in e)!function(a,b){UE.commands[a]={execCommand:function(d,e){e=e||(this.queryCommandState(d)?"none":"underline"==d?"underline":"fontborder"==d?"1px solid #000":"line-through");var g,h=this,i=this.selection.getRange();if("default"==e)i.collapsed&&(g=h.document.createTextNode("font"),i.insertNode(g).select()),h.execCommand("removeFormat","span,a",b),g&&(i.setStartBefore(g).collapse(!0),domUtils.remove(g)),c(i,d,e),i.select();else if(i.collapsed){var j=domUtils.findParentByTagName(i.startContainer,"span",!0);if(g=h.document.createTextNode("font"),!j||j.children.length||j[browser.ie?"innerText":"textContent"].replace(fillCharReg,"").length){if(i.insertNode(g),i.selectNode(g).select(),j=i.document.createElement("span"),f[a]){if(domUtils.findParentByTagName(g,"a",!0))return i.setStartBefore(g).setCursor(),void domUtils.remove(g);h.execCommand("removeFormat","span,a",b)}if(j.style.cssText=b+":"+e,g.parentNode.insertBefore(j,g),!browser.ie||browser.ie&&9==browser.version)for(var k=j.parentNode;!domUtils.isBlockElm(k);)"SPAN"==k.tagName&&(j.style.cssText=k.style.cssText+";"+j.style.cssText),k=k.parentNode;opera?setTimeout(function(){i.setStart(j,0).collapse(!0),c(i,d,e),i.select()}):(i.setStart(j,0).collapse(!0),c(i,d,e),i.select())}else i.insertNode(g),f[a]&&(i.selectNode(g).select(),h.execCommand("removeFormat","span,a",b,null),j=domUtils.findParentByTagName(g,"span",!0),i.setStartBefore(g)),j&&(j.style.cssText+=";"+b+":"+e),i.collapse(!0).select();domUtils.remove(g)}else f[a]&&h.queryCommandValue(a)&&h.execCommand("removeFormat","span,a",b),i=h.selection.getRange(),i.applyInlineStyle("span",{style:b+":"+e}),c(i,d,e),i.select();return!0},queryCommandValue:function(a){var c=this.selection.getStart();if("underline"==a||"strikethrough"==a){for(var d,e=c;e&&!domUtils.isBlockElm(e)&&!domUtils.isBody(e);){if(1==e.nodeType&&(d=domUtils.getComputedStyle(e,b),"none"!=d))return d;e=e.parentNode}return"none"}if("fontborder"==a){for(var f,g=c;g&&dtd.$inline[g.tagName];){if((f=domUtils.getComputedStyle(g,"border"))&&/1px/.test(f)&&/solid/.test(f))return f;g=g.parentNode}return""}if("FontSize"==a){var h=domUtils.getComputedStyle(c,b),g=/^([\d\.]+)(\w+)$/.exec(h);return g?Math.floor(g[1])+g[2]:h}return domUtils.getComputedStyle(c,b)},queryCommandState:function(a){if(!f[a])return 0;var b=this.queryCommandValue(a);return"fontborder"==a?/1px/.test(b)&&/solid/.test(b):"underline"==a?/underline/.test(b):/line\-through/.test(b)}}}(h,e[h])},UE.plugins.link=function(){function a(a){var b=a.startContainer,c=a.endContainer;(b=domUtils.findParentByTagName(b,"a",!0))&&a.setStartBefore(b),(c=domUtils.findParentByTagName(c,"a",!0))&&a.setEndAfter(c)}function b(b,c,d){var e=b.cloneRange(),f=d.queryCommandValue("link");a(b=b.adjustmentBoundary());var g=b.startContainer;if(1==g.nodeType&&f&&(g=g.childNodes[b.startOffset],g&&1==g.nodeType&&"A"==g.tagName&&/^(?:https?|ftp|file)\s*:\s*\/\//.test(g[browser.ie?"innerText":"textContent"])&&(g[browser.ie?"innerText":"textContent"]=utils.html(c.textValue||c.href))),e.collapsed&&!f||(b.removeInlineStyle("a"),e=b.cloneRange()),e.collapsed){var h=b.document.createElement("a"),i="";c.textValue?(i=utils.html(c.textValue),delete c.textValue):i=utils.html(c.href),domUtils.setAttributes(h,c),g=domUtils.findParentByTagName(e.startContainer,"a",!0),g&&domUtils.isInNodeEndBoundary(e,g)&&b.setStartAfter(g).collapse(!0),h[browser.ie?"innerText":"textContent"]=i,b.insertNode(h).selectNode(h)}else b.applyInlineStyle("a",c)}UE.commands.unlink={execCommand:function(){var b,c=this.selection.getRange();c.collapsed&&!domUtils.findParentByTagName(c.startContainer,"a",!0)||(b=c.createBookmark(),a(c),c.removeInlineStyle("a").moveToBookmark(b).select())},queryCommandState:function(){return!this.highlight&&this.queryCommandValue("link")?0:-1}},UE.commands.link={execCommand:function(a,c){var d;c._href&&(c._href=utils.unhtml(c._href,/[<">]/g)),c.href&&(c.href=utils.unhtml(c.href,/[<">]/g)),c.textValue&&(c.textValue=utils.unhtml(c.textValue,/[<">]/g)),b(d=this.selection.getRange(),c,this),d.collapse().select(!0)},queryCommandValue:function(){var a,b=this.selection.getRange();if(!b.collapsed){b.shrinkBoundary();var c=3!=b.startContainer.nodeType&&b.startContainer.childNodes[b.startOffset]?b.startContainer.childNodes[b.startOffset]:b.startContainer,d=3==b.endContainer.nodeType||0==b.endOffset?b.endContainer:b.endContainer.childNodes[b.endOffset-1],e=b.getCommonAncestor();if(a=domUtils.findParentByTagName(e,"a",!0),!a&&1==e.nodeType)for(var f,g,h,i=e.getElementsByTagName("a"),j=0;h=i[j++];)if(f=domUtils.getPosition(h,c),g=domUtils.getPosition(h,d),(f&domUtils.POSITION_FOLLOWING||f&domUtils.POSITION_CONTAINS)&&(g&domUtils.POSITION_PRECEDING||g&domUtils.POSITION_CONTAINS)){a=h;break}return a}return a=b.startContainer,a=1==a.nodeType?a:a.parentNode,a&&(a=domUtils.findParentByTagName(a,"a",!0))&&!domUtils.isInNodeEndBoundary(b,a)?a:void 0},queryCommandState:function(){var a=this.selection.getRange().getClosedNode(),b=a&&("edui-faked-video"==a.className||-1!=a.className.indexOf("edui-upload-video"));return b?-1:0}}},UE.plugins.insertframe=function(){function a(){b._iframe&&delete b._iframe}var b=this;b.addListener("selectionchange",function(){a()})},UE.commands.scrawl={queryCommandState:function(){return browser.ie&&browser.version<=8?-1:0}},UE.plugins.removeformat=function(){var a=this;a.setOpt({removeFormatTags:"b,big,code,del,dfn,em,font,i,ins,kbd,q,samp,small,span,strike,strong,sub,sup,tt,u,var",removeFormatAttributes:"class,style,lang,width,height,align,hspace,valign"}),a.commands.removeformat={execCommand:function(a,b,c,d,e){function f(a){if(3==a.nodeType||"span"!=a.tagName.toLowerCase())return 0;if(browser.ie){var b=a.attributes;if(b.length){for(var c=0,d=b.length;d>c;c++)if(b[c].specified)return 0;return 1}}return!a.attributes.length}function g(a){var b=a.createBookmark();if(a.collapsed&&a.enlarge(!0),!e){var d=domUtils.findParentByTagName(a.startContainer,"a",!0);d&&a.setStartBefore(d),d=domUtils.findParentByTagName(a.endContainer,"a",!0),d&&a.setEndAfter(d)}for(h=a.createBookmark(),p=h.start;(i=p.parentNode)&&!domUtils.isBlockElm(i);)domUtils.breakParent(p,i),domUtils.clearEmptySibling(p);if(h.end){for(p=h.end;(i=p.parentNode)&&!domUtils.isBlockElm(i);)domUtils.breakParent(p,i),domUtils.clearEmptySibling(p);for(var g,l=domUtils.getNextDomNode(h.start,!1,m);l&&l!=h.end;)g=domUtils.getNextDomNode(l,!0,m),dtd.$empty[l.tagName.toLowerCase()]||domUtils.isBookmarkNode(l)||(j.test(l.tagName)?c?(domUtils.removeStyle(l,c),f(l)&&"text-decoration"!=c&&domUtils.remove(l,!0)):domUtils.remove(l,!0):dtd.$tableContent[l.tagName]||dtd.$list[l.tagName]||(domUtils.removeAttributes(l,k),f(l)&&domUtils.remove(l,!0))),l=g}var n=h.start.parentNode;!domUtils.isBlockElm(n)||dtd.$tableContent[n.tagName]||dtd.$list[n.tagName]||domUtils.removeAttributes(n,k),n=h.end.parentNode,h.end&&domUtils.isBlockElm(n)&&!dtd.$tableContent[n.tagName]&&!dtd.$list[n.tagName]&&domUtils.removeAttributes(n,k),a.moveToBookmark(h).moveToBookmark(b);for(var o,p=a.startContainer,q=a.collapsed;1==p.nodeType&&domUtils.isEmptyNode(p)&&dtd.$removeEmpty[p.tagName];)o=p.parentNode,a.setStartBefore(p),a.startContainer===a.endContainer&&a.endOffset--,domUtils.remove(p),p=o;if(!q)for(p=a.endContainer;1==p.nodeType&&domUtils.isEmptyNode(p)&&dtd.$removeEmpty[p.tagName];)o=p.parentNode,a.setEndBefore(p),domUtils.remove(p),p=o}var h,i,j=new RegExp("^(?:"+(b||this.options.removeFormatTags).replace(/,/g,"|")+")$","i"),k=c?[]:(d||this.options.removeFormatAttributes).split(","),l=new dom.Range(this.document),m=function(a){return 1==a.nodeType};l=this.selection.getRange(),g(l),l.select()}}},UE.plugins.blockquote=function(){function a(a){return domUtils.filterNodeList(a.selection.getStartElementPath(),"blockquote")}var b=this;b.commands.blockquote={execCommand:function(b,c){var d=this.selection.getRange(),e=a(this),f=dtd.blockquote,g=d.createBookmark();if(e){var h=d.startContainer,i=domUtils.isBlockElm(h)?h:domUtils.findParent(h,function(a){return domUtils.isBlockElm(a)}),j=d.endContainer,k=domUtils.isBlockElm(j)?j:domUtils.findParent(j,function(a){return domUtils.isBlockElm(a)});i=domUtils.findParentByTagName(i,"li",!0)||i,k=domUtils.findParentByTagName(k,"li",!0)||k,"LI"==i.tagName||"TD"==i.tagName||i===e||domUtils.isBody(i)?domUtils.remove(e,!0):domUtils.breakParent(i,e),i!==k&&(e=domUtils.findParentByTagName(k,"blockquote"),e&&("LI"==k.tagName||"TD"==k.tagName||domUtils.isBody(k)?e.parentNode&&domUtils.remove(e,!0):domUtils.breakParent(k,e)));for(var l,m=domUtils.getElementsByTagName(this.document,"blockquote"),n=0;l=m[n++];)l.childNodes.length?domUtils.getPosition(l,i)&domUtils.POSITION_FOLLOWING&&domUtils.getPosition(l,k)&domUtils.POSITION_PRECEDING&&domUtils.remove(l,!0):domUtils.remove(l)}else{for(var o=d.cloneRange(),p=1==o.startContainer.nodeType?o.startContainer:o.startContainer.parentNode,q=p,r=1;;){if(domUtils.isBody(p)){q!==p?d.collapsed?(o.selectNode(q),r=0):o.setStartBefore(q):o.setStart(p,0);break}if(!f[p.tagName]){d.collapsed?o.selectNode(q):o.setStartBefore(q);break}q=p,p=p.parentNode}if(r)for(q=p=p=1==o.endContainer.nodeType?o.endContainer:o.endContainer.parentNode;;){if(domUtils.isBody(p)){q!==p?o.setEndAfter(q):o.setEnd(p,p.childNodes.length);break}if(!f[p.tagName]){o.setEndAfter(q);break}q=p,p=p.parentNode}p=d.document.createElement("blockquote"),domUtils.setAttributes(p,c),p.appendChild(o.extractContents()),o.insertNode(p);for(var s,t=domUtils.getElementsByTagName(p,"blockquote"),n=0;s=t[n++];)s.parentNode&&domUtils.remove(s,!0)}d.moveToBookmark(g).select()},queryCommandState:function(){return a(this)?1:0}}},UE.commands.touppercase=UE.commands.tolowercase={execCommand:function(a){var b=this,c=b.selection.getRange();if(c.collapsed)return c;for(var d=c.createBookmark(),e=d.end,f=function(a){return!domUtils.isBr(a)&&!domUtils.isWhitespace(a)},g=domUtils.getNextDomNode(d.start,!1,f);g&&domUtils.getPosition(g,e)&domUtils.POSITION_PRECEDING&&(3==g.nodeType&&(g.nodeValue=g.nodeValue["touppercase"==a?"toUpperCase":"toLowerCase"]()),g=domUtils.getNextDomNode(g,!0,f),g!==e););c.moveToBookmark(d).select()}},UE.commands.indent={execCommand:function(){var a=this,b=a.queryCommandState("indent")?"0em":a.options.indentValue||"2em";a.execCommand("Paragraph","p",{style:"text-indent:"+b})},queryCommandState:function(){var a=domUtils.filterNodeList(this.selection.getStartElementPath(),"p h1 h2 h3 h4 h5 h6");return a&&a.style.textIndent&&parseInt(a.style.textIndent)?1:0}},UE.commands.print={execCommand:function(){this.window.print()},notNeedUndo:1},UE.commands.preview={execCommand:function(){var a=window.open("","_blank",""),b=a.document;b.open(),b.write('
              "+this.getContent(null,null,!0)+"
              "),b.close()},notNeedUndo:1},UE.plugins.selectall=function(){var a=this;a.commands.selectall={execCommand:function(){var a=this,b=a.body,c=a.selection.getRange();c.selectNodeContents(b),domUtils.isEmptyBlock(b)&&(browser.opera&&b.firstChild&&1==b.firstChild.nodeType&&c.setStartAtFirst(b.firstChild),c.collapse(!0)),c.select(!0)},notNeedUndo:1},a.addshortcutkey({selectAll:"ctrl+65"})},UE.plugins.paragraph=function(){var a=this,b=domUtils.isBlockElm,c=["TD","LI","PRE"],d=function(a,d,e,f){var g,h=a.createBookmark(),i=function(a){return 1==a.nodeType?"br"!=a.tagName.toLowerCase()&&!domUtils.isBookmarkNode(a):!domUtils.isWhitespace(a)};a.enlarge(!0);for(var j,k=a.createBookmark(),l=domUtils.getNextDomNode(k.start,!1,i),m=a.cloneRange();l&&!(domUtils.getPosition(l,k.end)&domUtils.POSITION_FOLLOWING);)if(3!=l.nodeType&&b(l))l=domUtils.getNextDomNode(l,!0,i);else{for(m.setStartBefore(l);l&&l!==k.end&&!b(l);)j=l,l=domUtils.getNextDomNode(l,!1,null,function(a){return!b(a)});m.setEndAfter(j),g=a.document.createElement(d),e&&(domUtils.setAttributes(g,e),f&&"customstyle"==f&&e.style&&(g.style.cssText=e.style)),g.appendChild(m.extractContents()),domUtils.isEmptyNode(g)&&domUtils.fillChar(a.document,g),m.insertNode(g);var n=g.parentNode;b(n)&&!domUtils.isBody(g.parentNode)&&-1==utils.indexOf(c,n.tagName)&&(f&&"customstyle"==f||(n.getAttribute("dir")&&g.setAttribute("dir",n.getAttribute("dir")),n.style.cssText&&(g.style.cssText=n.style.cssText+";"+g.style.cssText),n.style.textAlign&&!g.style.textAlign&&(g.style.textAlign=n.style.textAlign),n.style.textIndent&&!g.style.textIndent&&(g.style.textIndent=n.style.textIndent),n.style.padding&&!g.style.padding&&(g.style.padding=n.style.padding)),e&&/h\d/i.test(n.tagName)&&!/h\d/i.test(g.tagName)?(domUtils.setAttributes(n,e),f&&"customstyle"==f&&e.style&&(n.style.cssText=e.style),domUtils.remove(g,!0),g=n):domUtils.remove(g.parentNode,!0)),l=-1!=utils.indexOf(c,n.tagName)?n:g,l=domUtils.getNextDomNode(l,!1,i)}return a.moveToBookmark(k).moveToBookmark(h)};a.setOpt("paragraph",{p:"",h1:"",h2:"",h3:"",h4:"",h5:"",h6:""}),a.commands.paragraph={execCommand:function(a,b,c,e){var f=this.selection.getRange();if(f.collapsed){var g=this.document.createTextNode("p");if(f.insertNode(g),browser.ie){var h=g.previousSibling;h&&domUtils.isWhitespace(h)&&domUtils.remove(h),h=g.nextSibling,h&&domUtils.isWhitespace(h)&&domUtils.remove(h)}}if(f=d(f,b,c,e),g&&(f.setStartBefore(g).collapse(!0),pN=g.parentNode,domUtils.remove(g),domUtils.isBlockElm(pN)&&domUtils.isEmptyNode(pN)&&domUtils.fillNode(this.document,pN)),browser.gecko&&f.collapsed&&1==f.startContainer.nodeType){var i=f.startContainer.childNodes[f.startOffset];i&&1==i.nodeType&&i.tagName.toLowerCase()==b&&f.setStart(i,0).collapse(!0)}return f.select(),!0},queryCommandValue:function(){var a=domUtils.filterNodeList(this.selection.getStartElementPath(),"p h1 h2 h3 h4 h5 h6");return a?a.tagName.toLowerCase():""}}},function(){var a=domUtils.isBlockElm,b=function(a){return domUtils.filterNodeList(a.selection.getStartElementPath(),function(a){return a&&1==a.nodeType&&a.getAttribute("dir")})},c=function(c,d,e){var f,g=function(a){return 1==a.nodeType?!domUtils.isBookmarkNode(a):!domUtils.isWhitespace(a)},h=b(d);if(h&&c.collapsed)return h.setAttribute("dir",e),c;f=c.createBookmark(),c.enlarge(!0);for(var i,j=c.createBookmark(),k=domUtils.getNextDomNode(j.start,!1,g),l=c.cloneRange();k&&!(domUtils.getPosition(k,j.end)&domUtils.POSITION_FOLLOWING);)if(3!=k.nodeType&&a(k))k=domUtils.getNextDomNode(k,!0,g);else{for(l.setStartBefore(k);k&&k!==j.end&&!a(k);)i=k,k=domUtils.getNextDomNode(k,!1,null,function(b){return!a(b)});l.setEndAfter(i);var m=l.getCommonAncestor();if(!domUtils.isBody(m)&&a(m))m.setAttribute("dir",e),k=m;else{var n=c.document.createElement("p");n.setAttribute("dir",e);var o=l.extractContents();n.appendChild(o),l.insertNode(n),k=n}k=domUtils.getNextDomNode(k,!1,g)}return c.moveToBookmark(j).moveToBookmark(f)};UE.commands.directionality={execCommand:function(a,b){var d=this.selection.getRange();if(d.collapsed){var e=this.document.createTextNode("d");d.insertNode(e)}return c(d,this,b),e&&(d.setStartBefore(e).collapse(!0),domUtils.remove(e)),d.select(),!0},queryCommandValue:function(){var a=b(this);return a?a.getAttribute("dir"):"ltr"}}}(),UE.plugins.horizontal=function(){var a=this;a.commands.horizontal={execCommand:function(a){var b=this;if(-1!==b.queryCommandState(a)){b.execCommand("insertHtml","
              ");var c=b.selection.getRange(),d=c.startContainer;if(1==d.nodeType&&!d.childNodes[c.startOffset]){var e;(e=d.childNodes[c.startOffset-1])&&1==e.nodeType&&"HR"==e.tagName&&("p"==b.options.enterTag?(e=b.document.createElement("p"),c.insertNode(e),c.setStart(e,0).setCursor()):(e=b.document.createElement("br"),c.insertNode(e),c.setStartBefore(e).setCursor()))}return!0}},queryCommandState:function(){return domUtils.filterNodeList(this.selection.getStartElementPath(),"table")?-1:0}},a.addListener("delkeydown",function(a,b){var c=this.selection.getRange();if(c.txtToElmBoundary(!0),domUtils.isStartInblock(c)){var d=c.startContainer,e=d.previousSibling;if(e&&domUtils.isTagNode(e,"hr"))return domUtils.remove(e),c.select(),domUtils.preventDefault(b),!0}})},UE.commands.time=UE.commands.date={execCommand:function(a,b){function c(a,b){var c=("0"+a.getHours()).slice(-2),d=("0"+a.getMinutes()).slice(-2),e=("0"+a.getSeconds()).slice(-2);return b=b||"hh:ii:ss",b.replace(/hh/gi,c).replace(/ii/gi,d).replace(/ss/gi,e)}function d(a,b){var c=("000"+a.getFullYear()).slice(-4),d=c.slice(-2),e=("0"+(a.getMonth()+1)).slice(-2),f=("0"+a.getDate()).slice(-2);return b=b||"yyyy-mm-dd",b.replace(/yyyy/gi,c).replace(/yy/gi,d).replace(/mm/gi,e).replace(/dd/gi,f)}var e=new Date;this.execCommand("insertHtml","time"==a?c(e,b):d(e,b))}},UE.plugins.rowspacing=function(){var a=this;a.setOpt({rowspacingtop:["5","10","15","20","25"],rowspacingbottom:["5","10","15","20","25"]}),a.commands.rowspacing={execCommand:function(a,b,c){return this.execCommand("paragraph","p",{style:"margin-"+c+":"+b+"px"}),!0},queryCommandValue:function(a,b){var c,d=domUtils.filterNodeList(this.selection.getStartElementPath(),function(a){return domUtils.isBlockElm(a)});return d?(c=domUtils.getComputedStyle(d,"margin-"+b).replace(/[^\d]/g,""),c?c:0):0}}},UE.plugins.lineheight=function(){var a=this;a.setOpt({lineheight:["1","1.5","1.75","2","3","4","5"]}),a.commands.lineheight={execCommand:function(a,b){return this.execCommand("paragraph","p",{style:"line-height:"+("1"==b?"normal":b+"em")}),!0},queryCommandValue:function(){var a=domUtils.filterNodeList(this.selection.getStartElementPath(),function(a){return domUtils.isBlockElm(a)});if(a){var b=domUtils.getComputedStyle(a,"line-height");return"normal"==b?1:b.replace(/[^\d.]*/gi,"")}}}},UE.plugins.insertcode=function(){var a=this;a.ready(function(){utils.cssRule("pre","pre{margin:.5em 0;padding:.4em .6em;border-radius:8px;background:#f8f8f8;}",a.document)}),a.setOpt("insertcode",{as3:"ActionScript3",bash:"Bash/Shell",cpp:"C/C++",css:"Css",cf:"CodeFunction","c#":"C#",delphi:"Delphi",diff:"Diff",erlang:"Erlang",groovy:"Groovy",html:"Html",java:"Java",jfx:"JavaFx",js:"Javascript",pl:"Perl",php:"Php",plain:"Plain Text",ps:"PowerShell",python:"Python",ruby:"Ruby",scala:"Scala",sql:"Sql",vb:"Vb",xml:"Xml"}),a.commands.insertcode={execCommand:function(a,b){var c=this,d=c.selection.getRange(),e=domUtils.findParentByTagName(d.startContainer,"pre",!0);if(e)e.className="brush:"+b+";toolbar:false;";else{var f="";if(d.collapsed)f=browser.ie&&browser.ie11below?browser.version<=8?" ":"":"
              ";else{var g=d.extractContents(),h=c.document.createElement("div");h.appendChild(g),utils.each(UE.filterNode(UE.htmlparser(h.innerHTML.replace(/[\r\t]/g,"")),c.options.filterTxtRules).children,function(a){if(browser.ie&&browser.ie11below&&browser.version>8)"element"==a.type?"br"==a.tagName?f+="\n":dtd.$empty[a.tagName]||(utils.each(a.children,function(b){"element"==b.type?"br"==b.tagName?f+="\n":dtd.$empty[a.tagName]||(f+=b.innerText()):f+=b.data}),/\n$/.test(f)||(f+="\n")):f+=a.data+"\n",!a.nextSibling()&&/\n$/.test(f)&&(f=f.replace(/\n$/,""));else if(browser.ie&&browser.ie11below)"element"==a.type?"br"==a.tagName?f+="
              ":dtd.$empty[a.tagName]||(utils.each(a.children,function(b){"element"==b.type?"br"==b.tagName?f+="
              ":dtd.$empty[a.tagName]||(f+=b.innerText()):f+=b.data}),/br>$/.test(f)||(f+="
              ")):f+=a.data+"
              ",!a.nextSibling()&&/
              $/.test(f)&&(f=f.replace(/
              $/,""));else if(f+="element"==a.type?dtd.$empty[a.tagName]?"":a.innerText():a.data,!/br\/?\s*>$/.test(f)){if(!a.nextSibling())return;f+="
              "}})}c.execCommand("inserthtml",'
              '+f+"
              ",!0),e=c.document.getElementById("coder"),domUtils.removeAttributes(e,"id");var i=e.previousSibling;i&&(3==i.nodeType&&1==i.nodeValue.length&&browser.ie&&6==browser.version||domUtils.isEmptyBlock(i))&&domUtils.remove(i);var d=c.selection.getRange();domUtils.isEmptyBlock(e)?d.setStart(e,0).setCursor(!1,!0):d.selectNodeContents(e).select()}},queryCommandValue:function(){var a=this.selection.getStartElementPath(),b="";return utils.each(a,function(a){if("PRE"==a.nodeName){var c=a.className.match(/brush:([^;]+)/);return b=c&&c[1]?c[1]:"",!1}}),b}},a.addInputRule(function(a){utils.each(a.getNodesByTagName("pre"),function(a){var b=a.getNodesByTagName("br");if(b.length)return void(browser.ie&&browser.ie11below&&browser.version>8&&utils.each(b,function(a){var b=UE.uNode.createText("\n");a.parentNode.insertBefore(b,a),a.parentNode.removeChild(a)}));if(!(browser.ie&&browser.ie11below&&browser.version>8)){var c=a.innerText().split(/\n/);a.innerHTML(""),utils.each(c,function(b){b.length&&a.appendChild(UE.uNode.createText(b)),a.appendChild(UE.uNode.createElement("br"))})}})}),a.addOutputRule(function(a){utils.each(a.getNodesByTagName("pre"),function(a){var b="";utils.each(a.children,function(a){b+="text"==a.type?a.data.replace(/[ ]/g," ").replace(/\n$/,""):"br"==a.tagName?"\n":dtd.$empty[a.tagName]?a.innerText():""}),a.innerText(b.replace(/( |\n)+$/,""))})}),a.notNeedCodeQuery={help:1,undo:1,redo:1,source:1,print:1,searchreplace:1,fullscreen:1,preview:1,insertparagraph:1,elementpath:1,insertcode:1,inserthtml:1,selectall:1};a.queryCommandState;a.queryCommandState=function(a){var b=this;return!b.notNeedCodeQuery[a.toLowerCase()]&&b.selection&&b.queryCommandValue("insertcode")?-1:UE.Editor.prototype.queryCommandState.apply(this,arguments)},a.addListener("beforeenterkeydown",function(){var b=a.selection.getRange(),c=domUtils.findParentByTagName(b.startContainer,"pre",!0);if(c){if(a.fireEvent("saveScene"),b.collapsed||b.deleteContents(),!browser.ie||browser.ie9above){var c,d=a.document.createElement("br");b.insertNode(d).setStartAfter(d).collapse(!0);var e=d.nextSibling;e||browser.ie&&!(browser.version>10)?b.setStartAfter(d):b.insertNode(d.cloneNode(!1)), +c=d.previousSibling;for(var f;c;)if(f=c,c=c.previousSibling,!c||"BR"==c.nodeName){c=f;break}if(c){for(var g="";c&&"BR"!=c.nodeName&&new RegExp("^[\\s"+domUtils.fillChar+"]*$").test(c.nodeValue);)g+=c.nodeValue,c=c.nextSibling;if("BR"!=c.nodeName){var h=c.nodeValue.match(new RegExp("^([\\s"+domUtils.fillChar+"]+)"));h&&h[1]&&(g+=h[1])}g&&(g=a.document.createTextNode(g),b.insertNode(g).setStartAfter(g))}b.collapse(!0).select(!0)}else if(browser.version>8){var i=a.document.createTextNode("\n"),j=b.startContainer;if(0==b.startOffset){var k=j.previousSibling;if(k){b.insertNode(i);var l=a.document.createTextNode(" ");b.setStartAfter(i).insertNode(l).setStart(l,0).collapse(!0).select(!0)}}else{b.insertNode(i).setStartAfter(i);var l=a.document.createTextNode(" ");j=b.startContainer.childNodes[b.startOffset],j&&!/^\n/.test(j.nodeValue)&&b.setStartBefore(i),b.insertNode(l).setStart(l,0).collapse(!0).select(!0)}}else{var d=a.document.createElement("br");b.insertNode(d),b.insertNode(a.document.createTextNode(domUtils.fillChar)),b.setStartAfter(d),c=d.previousSibling;for(var f;c;)if(f=c,c=c.previousSibling,!c||"BR"==c.nodeName){c=f;break}if(c){for(var g="";c&&"BR"!=c.nodeName&&new RegExp("^[ "+domUtils.fillChar+"]*$").test(c.nodeValue);)g+=c.nodeValue,c=c.nextSibling;if("BR"!=c.nodeName){var h=c.nodeValue.match(new RegExp("^([ "+domUtils.fillChar+"]+)"));h&&h[1]&&(g+=h[1])}g=a.document.createTextNode(g),b.insertNode(g).setStartAfter(g)}b.collapse(!0).select()}return a.fireEvent("saveScene"),!0}}),a.addListener("tabkeydown",function(b,c){var d=a.selection.getRange(),e=domUtils.findParentByTagName(d.startContainer,"pre",!0);if(e){if(a.fireEvent("saveScene"),c.shiftKey);else if(d.collapsed){var f=a.document.createTextNode(" ");d.insertNode(f).setStartAfter(f).collapse(!0).select(!0)}else{for(var g=d.createBookmark(),h=g.start.previousSibling;h;){if(e.firstChild===h&&!domUtils.isBr(h)){e.insertBefore(a.document.createTextNode(" "),h);break}if(domUtils.isBr(h)){e.insertBefore(a.document.createTextNode(" "),h.nextSibling);break}h=h.previousSibling}var i=g.end;for(h=g.start.nextSibling,e.firstChild===g.start&&e.insertBefore(a.document.createTextNode(" "),h.nextSibling);h&&h!==i;){if(domUtils.isBr(h)&&h.nextSibling){if(h.nextSibling===i)break;e.insertBefore(a.document.createTextNode(" "),h.nextSibling)}h=h.nextSibling}d.moveToBookmark(g).select()}return a.fireEvent("saveScene"),!0}}),a.addListener("beforeinserthtml",function(a,b){var c=this,d=c.selection.getRange(),e=domUtils.findParentByTagName(d.startContainer,"pre",!0);if(e){d.collapsed||d.deleteContents();var f="";if(browser.ie&&browser.version>8){utils.each(UE.filterNode(UE.htmlparser(b),c.options.filterTxtRules).children,function(a){"element"==a.type?"br"==a.tagName?f+="\n":dtd.$empty[a.tagName]||(utils.each(a.children,function(b){"element"==b.type?"br"==b.tagName?f+="\n":dtd.$empty[a.tagName]||(f+=b.innerText()):f+=b.data}),/\n$/.test(f)||(f+="\n")):f+=a.data+"\n",!a.nextSibling()&&/\n$/.test(f)&&(f=f.replace(/\n$/,""))});var g=c.document.createTextNode(utils.html(f.replace(/ /g," ")));d.insertNode(g).selectNode(g).select()}else{var h=c.document.createDocumentFragment();utils.each(UE.filterNode(UE.htmlparser(b),c.options.filterTxtRules).children,function(a){"element"==a.type?"br"==a.tagName?h.appendChild(c.document.createElement("br")):dtd.$empty[a.tagName]||(utils.each(a.children,function(b){"element"==b.type?"br"==b.tagName?h.appendChild(c.document.createElement("br")):dtd.$empty[a.tagName]||h.appendChild(c.document.createTextNode(utils.html(b.innerText().replace(/ /g," ")))):h.appendChild(c.document.createTextNode(utils.html(b.data.replace(/ /g," "))))}),"BR"!=h.lastChild.nodeName&&h.appendChild(c.document.createElement("br"))):h.appendChild(c.document.createTextNode(utils.html(a.data.replace(/ /g," ")))),a.nextSibling()||"BR"!=h.lastChild.nodeName||h.removeChild(h.lastChild)}),d.insertNode(h).select()}return!0}}),a.addListener("keydown",function(a,b){var c=this,d=b.keyCode||b.which;if(40==d){var e,f=c.selection.getRange(),g=f.startContainer;if(f.collapsed&&(e=domUtils.findParentByTagName(f.startContainer,"pre",!0))&&!e.nextSibling){for(var h=e.lastChild;h&&"BR"==h.nodeName;)h=h.previousSibling;(h===g||f.startContainer===e&&f.startOffset==e.childNodes.length)&&(c.execCommand("insertparagraph"),domUtils.preventDefault(b))}}}),a.addListener("delkeydown",function(b,c){var d=this.selection.getRange();d.txtToElmBoundary(!0);var e=d.startContainer;if(domUtils.isTagNode(e,"pre")&&d.collapsed&&domUtils.isStartInblock(d)){var f=a.document.createElement("p");return domUtils.fillNode(a.document,f),e.parentNode.insertBefore(f,e),domUtils.remove(e),d.setStart(f,0).setCursor(!1,!0),domUtils.preventDefault(c),!0}})},UE.commands.cleardoc={execCommand:function(a){var b=this,c=b.options.enterTag,d=b.selection.getRange();"br"==c?(b.body.innerHTML="
              ",d.setStart(b.body,0).setCursor()):(b.body.innerHTML="

              "+(ie?"":"
              ")+"

              ",d.setStart(b.body.firstChild,0).setCursor(!1,!0)),setTimeout(function(){b.fireEvent("clearDoc")},0)}},UE.plugin.register("anchor",function(){return{bindEvents:{ready:function(){utils.cssRule("anchor",".anchorclass{background: url('"+this.options.themePath+this.options.theme+"/images/anchor.gif') no-repeat scroll left center transparent;cursor: auto;display: inline-block;height: 16px;width: 15px;}",this.document)}},outputRule:function(a){utils.each(a.getNodesByTagName("img"),function(a){var b;(b=a.getAttr("anchorname"))&&(a.tagName="a",a.setAttr({anchorname:"",name:b,"class":""}))})},inputRule:function(a){utils.each(a.getNodesByTagName("a"),function(a){var b;(b=a.getAttr("name"))&&!a.getAttr("href")&&(a.tagName="img",a.setAttr({anchorname:a.getAttr("name"),"class":"anchorclass"}),a.setAttr("name"))})},commands:{anchor:{execCommand:function(a,b){var c=this.selection.getRange(),d=c.getClosedNode();if(d&&d.getAttribute("anchorname"))b?d.setAttribute("anchorname",b):(c.setStartBefore(d).setCursor(),domUtils.remove(d));else if(b){var e=this.document.createElement("img");c.collapse(!0),domUtils.setAttributes(e,{anchorname:b,"class":"anchorclass"}),c.insertNode(e).setStartAfter(e).setCursor(!1,!0)}}}}}}),UE.plugins.wordcount=function(){var a=this;a.setOpt("wordCount",!0),a.addListener("contentchange",function(){a.fireEvent("wordcount")});var b;a.addListener("ready",function(){var a=this;domUtils.on(a.body,"keyup",function(c){var d=c.keyCode||c.which,e={16:1,18:1,20:1,37:1,38:1,39:1,40:1};d in e||(clearTimeout(b),b=setTimeout(function(){a.fireEvent("wordcount")},200))})})},UE.plugins.pagebreak=function(){function a(a){if(domUtils.isEmptyBlock(a)){for(var b,d=a.firstChild;d&&1==d.nodeType&&domUtils.isEmptyBlock(d);)b=d,d=d.firstChild;!b&&(b=a),domUtils.fillNode(c.document,b)}}function b(a){return a&&1==a.nodeType&&"HR"==a.tagName&&"pagebreak"==a.className}var c=this,d=["td"];c.setOpt("pageBreakTag","_ueditor_page_break_tag_"),c.ready(function(){utils.cssRule("pagebreak",".pagebreak{display:block;clear:both !important;cursor:default !important;width: 100% !important;margin:0;}",c.document)}),c.addInputRule(function(a){a.traversal(function(a){if("text"==a.type&&a.data==c.options.pageBreakTag){var b=UE.uNode.createElement('
              ');a.parentNode.insertBefore(b,a),a.parentNode.removeChild(a)}})}),c.addOutputRule(function(a){utils.each(a.getNodesByTagName("hr"),function(a){if("pagebreak"==a.getAttr("class")){var b=UE.uNode.createText(c.options.pageBreakTag);a.parentNode.insertBefore(b,a),a.parentNode.removeChild(a)}})}),c.commands.pagebreak={execCommand:function(){var e=c.selection.getRange(),f=c.document.createElement("hr");domUtils.setAttributes(f,{"class":"pagebreak",noshade:"noshade",size:"5"}),domUtils.unSelectable(f);var g,h=domUtils.findParentByTagName(e.startContainer,d,!0),i=[];if(h)switch(h.tagName){case"TD":if(g=h.parentNode,g.previousSibling)g.parentNode.insertBefore(f,g),i=domUtils.findParents(f);else{var j=domUtils.findParentByTagName(g,"table");j.parentNode.insertBefore(f,j),i=domUtils.findParents(f,!0)}g=i[1],f!==g&&domUtils.breakParent(f,g),c.fireEvent("afteradjusttable",c.document)}else{if(!e.collapsed){e.deleteContents();for(var k=e.startContainer;!domUtils.isBody(k)&&domUtils.isBlockElm(k)&&domUtils.isEmptyNode(k);)e.setStartBefore(k).collapse(!0),domUtils.remove(k),k=e.startContainer}e.insertNode(f);for(var l,g=f.parentNode;!domUtils.isBody(g);)domUtils.breakParent(f,g),l=f.nextSibling,l&&domUtils.isEmptyBlock(l)&&domUtils.remove(l),g=f.parentNode;l=f.nextSibling;var m=f.previousSibling;if(b(m)?domUtils.remove(m):m&&a(m),l)b(l)?domUtils.remove(l):a(l),e.setEndAfter(f).collapse(!1);else{var n=c.document.createElement("p");f.parentNode.appendChild(n),domUtils.fillNode(c.document,n),e.setStart(n,0).collapse(!0)}e.select(!0)}}}},UE.plugin.register("wordimage",function(){var a=this,b=[];return{commands:{wordimage:{execCommand:function(){for(var b,c=domUtils.getElementsByTagName(a.body,"img"),d=[],e=0;b=c[e++];){var f=b.getAttribute("word_img");f&&d.push(f)}return d},queryCommandState:function(){b=domUtils.getElementsByTagName(a.body,"img");for(var c,d=0;c=b[d++];)if(c.getAttribute("word_img"))return 1;return-1},notNeedUndo:!0}},inputRule:function(b){utils.each(b.getNodesByTagName("img"),function(b){var c=b.attrs,d=parseInt(c.width)<128||parseInt(c.height)<43,e=a.options,f=e.UEDITOR_HOME_URL+"themes/default/images/spacer.gif";c.src&&/^(?:(file:\/+))/.test(c.src)&&b.setAttr({width:c.width,height:c.height,alt:c.alt,word_img:c.src,src:f,style:"background:url("+(d?e.themePath+e.theme+"/images/word.gif":e.langPath+e.lang+"/images/localimage.png")+") no-repeat center center;border:1px solid #ddd"})})}}}),UE.plugins.dragdrop=function(){var a=this;a.ready(function(){domUtils.on(this.body,"dragend",function(){var b=a.selection.getRange(),c=b.getClosedNode()||a.selection.getStart();if(c&&"IMG"==c.tagName){for(var d,e=c.previousSibling;(d=c.nextSibling)&&1==d.nodeType&&"SPAN"==d.tagName&&!d.firstChild;)domUtils.remove(d);(!e||1!=e.nodeType||domUtils.isEmptyBlock(e))&&e||d&&(!d||domUtils.isEmptyBlock(d))||(e&&"P"==e.tagName&&!domUtils.isEmptyBlock(e)?(e.appendChild(c),domUtils.moveChild(d,e),domUtils.remove(d)):d&&"P"==d.tagName&&!domUtils.isEmptyBlock(d)&&d.insertBefore(c,d.firstChild),e&&"P"==e.tagName&&domUtils.isEmptyBlock(e)&&domUtils.remove(e),d&&"P"==d.tagName&&domUtils.isEmptyBlock(d)&&domUtils.remove(d),b.selectNode(c).select(),a.fireEvent("saveScene"))}})}),a.addListener("keyup",function(b,c){var d=c.keyCode||c.which;if(13==d){var e,f=a.selection.getRange();(e=domUtils.findParentByTagName(f.startContainer,"p",!0))&&"center"==domUtils.getComputedStyle(e,"text-align")&&domUtils.removeStyle(e,"text-align")}})},UE.plugins.undo=function(){function a(a,b){if(a.length!=b.length)return 0;for(var c=0,d=a.length;d>c;c++)if(a[c]!=b[c])return 0;return 1}function b(b,c){return b.collapsed!=c.collapsed?0:a(b.startAddress,c.startAddress)&&a(b.endAddress,c.endAddress)?1:0}function c(){this.list=[],this.index=0,this.hasUndo=!1,this.hasRedo=!1,this.undo=function(){if(this.hasUndo){if(!this.list[this.index-1]&&1==this.list.length)return void this.reset();for(;this.list[this.index].content==this.list[this.index-1].content;)if(this.index--,0==this.index)return this.restore(0);this.restore(--this.index)}},this.redo=function(){if(this.hasRedo){for(;this.list[this.index].content==this.list[this.index+1].content;)if(this.index++,this.index==this.list.length-1)return this.restore(this.index);this.restore(++this.index)}},this.restore=function(){var a=this.editor,b=this.list[this.index],c=UE.htmlparser(b.content.replace(h,""));a.options.autoClearEmptyNode=!1,a.filterInputRule(c),a.options.autoClearEmptyNode=j,a.document.body.innerHTML=c.toHtml(),a.fireEvent("afterscencerestore"),browser.ie&&utils.each(domUtils.getElementsByTagName(a.document,"td th caption p"),function(b){domUtils.isEmptyNode(b)&&domUtils.fillNode(a.document,b)});try{var d=new dom.Range(a.document).moveToAddress(b.address);d.select(i[d.startContainer.nodeName.toLowerCase()])}catch(e){}this.update(),this.clearKey(),a.fireEvent("reset",!0)},this.getScene=function(){var a=this.editor,b=a.selection.getRange(),c=b.createAddress(!1,!0);a.fireEvent("beforegetscene");var d=UE.htmlparser(a.body.innerHTML);a.options.autoClearEmptyNode=!1,a.filterOutputRule(d),a.options.autoClearEmptyNode=j;var e=d.toHtml();return a.fireEvent("aftergetscene"),{address:c,content:e}},this.save=function(a,c){clearTimeout(d);var g=this.getScene(c),h=this.list[this.index];h&&h.content!=g.content&&e.trigger("contentchange"),h&&h.content==g.content&&(a?1:b(h.address,g.address))||(this.list=this.list.slice(0,this.index+1),this.list.push(g),this.list.length>f&&this.list.shift(),this.index=this.list.length-1,this.clearKey(),this.update())},this.update=function(){this.hasRedo=!!this.list[this.index+1],this.hasUndo=!!this.list[this.index-1]},this.reset=function(){this.list=[],this.index=0,this.hasUndo=!1,this.hasRedo=!1,this.clearKey()},this.clearKey=function(){m=0,k=null}}var d,e=this,f=e.options.maxUndoCount||20,g=e.options.maxInputCount||20,h=new RegExp(domUtils.fillChar+"|","gi"),i={ol:1,ul:1,table:1,tbody:1,tr:1,body:1},j=e.options.autoClearEmptyNode;e.undoManger=new c,e.undoManger.editor=e,e.addListener("saveScene",function(){var a=Array.prototype.splice.call(arguments,1);this.undoManger.save.apply(this.undoManger,a)}),e.addListener("reset",function(a,b){b||this.undoManger.reset()}),e.commands.redo=e.commands.undo={execCommand:function(a){this.undoManger[a]()},queryCommandState:function(a){return this.undoManger["has"+("undo"==a.toLowerCase()?"Undo":"Redo")]?0:-1},notNeedUndo:1};var k,l={16:1,17:1,18:1,37:1,38:1,39:1,40:1},m=0,n=!1;e.addListener("ready",function(){domUtils.on(this.body,"compositionstart",function(){n=!0}),domUtils.on(this.body,"compositionend",function(){n=!1})}),e.addshortcutkey({Undo:"ctrl+90",Redo:"ctrl+89"});var o=!0;e.addListener("keydown",function(a,b){function c(a){a.undoManger.save(!1,!0),a.fireEvent("selectionchange")}var e=this,f=b.keyCode||b.which;if(!(l[f]||b.ctrlKey||b.metaKey||b.shiftKey||b.altKey)){if(n)return;if(!e.selection.getRange().collapsed)return e.undoManger.save(!1,!0),void(o=!1);0==e.undoManger.list.length&&e.undoManger.save(!0),clearTimeout(d),d=setTimeout(function(){if(n)var a=setInterval(function(){n||(c(e),clearInterval(a))},300);else c(e)},200),k=f,m++,m>=g&&c(e)}}),e.addListener("keyup",function(a,b){var c=b.keyCode||b.which;if(!(l[c]||b.ctrlKey||b.metaKey||b.shiftKey||b.altKey)){if(n)return;o||(this.undoManger.save(!1,!0),o=!0)}}),e.stopCmdUndo=function(){e.__hasEnterExecCommand=!0},e.startCmdUndo=function(){e.__hasEnterExecCommand=!1}},UE.plugin.register("copy",function(){function a(){ZeroClipboard.config({debug:!1,swfPath:b.options.UEDITOR_HOME_URL+"third-party/zeroclipboard/ZeroClipboard.swf"});var a=b.zeroclipboard=new ZeroClipboard;a.on("copy",function(a){var c=a.client,d=b.selection.getRange(),e=document.createElement("div");e.appendChild(d.cloneContents()),c.setText(e.innerText||e.textContent),c.setHtml(e.innerHTML),d.select()}),a.on("mouseover mouseout",function(a){var b=a.target;"mouseover"==a.type?domUtils.addClass(b,"edui-state-hover"):"mouseout"==a.type&&domUtils.removeClasses(b,"edui-state-hover")}),a.on("wrongflash noflash",function(){ZeroClipboard.destroy()})}var b=this;return{bindEvents:{ready:function(){browser.ie||(window.ZeroClipboard?a():utils.loadFile(document,{src:b.options.UEDITOR_HOME_URL+"third-party/zeroclipboard/ZeroClipboard.js",tag:"script",type:"text/javascript",defer:"defer"},function(){a()}))}},commands:{copy:{execCommand:function(a){b.document.execCommand("copy")||alert(b.getLang("copymsg"))}}}}}),UE.plugins.paste=function(){function a(a){var b=this.document;if(!b.getElementById("baidu_pastebin")){var c=this.selection.getRange(),d=c.createBookmark(),e=b.createElement("div");e.id="baidu_pastebin",browser.webkit&&e.appendChild(b.createTextNode(domUtils.fillChar+domUtils.fillChar)),b.body.appendChild(e),d.start.style.display="",e.style.cssText="position:absolute;width:1px;height:1px;overflow:hidden;left:-1000px;white-space:nowrap;top:"+domUtils.getXY(d.start).y+"px",c.selectNodeContents(e).select(!0),setTimeout(function(){if(browser.webkit)for(var f,g=0,h=b.querySelectorAll("#baidu_pastebin");f=h[g++];){if(!domUtils.isEmptyNode(f)){e=f;break}domUtils.remove(f)}try{e.parentNode.removeChild(e)}catch(i){}c.moveToBookmark(d).select(!0),a(e)},0)}}function b(a){return a.replace(/<(\/?)([\w\-]+)([^>]*)>/gi,function(a,b,c,d){return c=c.toLowerCase(),{img:1}[c]?a:(d=d.replace(/([\w\-]*?)\s*=\s*(("([^"]*)")|('([^']*)')|([^\s>]+))/gi,function(a,b,c){return{src:1,href:1,name:1}[b.toLowerCase()]?b+"="+c+" ":""}),{span:1,div:1}[c]?"":"<"+b+c+" "+utils.trim(d)+">")})}function c(a){var c;if(a.firstChild){for(var h,i=domUtils.getElementsByTagName(a,"span"),j=0;h=i[j++];)"_baidu_cut_start"!=h.id&&"_baidu_cut_end"!=h.id||domUtils.remove(h);if(browser.webkit){for(var k,l=a.querySelectorAll("div br"),j=0;k=l[j++];){var m=k.parentNode;"DIV"==m.tagName&&1==m.childNodes.length&&(m.innerHTML="


              ",domUtils.remove(m))}for(var n,o=a.querySelectorAll("#baidu_pastebin"),j=0;n=o[j++];){var p=d.document.createElement("p");for(n.parentNode.insertBefore(p,n);n.firstChild;)p.appendChild(n.firstChild);domUtils.remove(n)}for(var q,r=a.querySelectorAll("meta"),j=0;q=r[j++];)domUtils.remove(q);var l=a.querySelectorAll("br");for(j=0;q=l[j++];)/^apple-/i.test(q.className)&&domUtils.remove(q)}if(browser.gecko){var s=a.querySelectorAll("[_moz_dirty]");for(j=0;q=s[j++];)q.removeAttribute("_moz_dirty")}if(!browser.ie)for(var q,t=a.querySelectorAll("span.Apple-style-span"),j=0;q=t[j++];)domUtils.remove(q,!0);c=a.innerHTML,c=UE.filterWord(c);var u=UE.htmlparser(c);if(d.options.filterRules&&UE.filterNode(u,d.options.filterRules),d.filterInputRule(u),browser.webkit){var v=u.lastChild();v&&"element"==v.type&&"br"==v.tagName&&u.removeChild(v),utils.each(d.body.querySelectorAll("div"),function(a){domUtils.isEmptyBlock(a)&&domUtils.remove(a,!0)})}if(c={html:u.toHtml()},d.fireEvent("beforepaste",c,u),!c.html)return;u=UE.htmlparser(c.html,!0),1===d.queryCommandState("pasteplain")?d.execCommand("insertHtml",UE.filterNode(u,d.options.filterTxtRules).toHtml(),!0):(UE.filterNode(u,d.options.filterTxtRules),e=u.toHtml(),f=c.html,g=d.selection.getRange().createAddress(!0),d.execCommand("insertHtml",d.getOpt("retainOnlyLabelPasted")===!0?b(f):f,!0)),d.fireEvent("afterpaste",c)}}var d=this;d.setOpt({retainOnlyLabelPasted:!1});var e,f,g;d.addListener("pasteTransfer",function(a,c){if(g&&e&&f&&e!=f){var h=d.selection.getRange();if(h.moveToAddress(g,!0),!h.collapsed){for(;!domUtils.isBody(h.startContainer);){var i=h.startContainer;if(1==i.nodeType){if(i=i.childNodes[h.startOffset],!i){h.setStartBefore(h.startContainer);continue}var j=i.previousSibling;j&&3==j.nodeType&&new RegExp("^[\n\r "+domUtils.fillChar+"]*$").test(j.nodeValue)&&h.setStartBefore(j)}if(0!=h.startOffset)break;h.setStartBefore(h.startContainer)}for(;!domUtils.isBody(h.endContainer);){var k=h.endContainer;if(1==k.nodeType){if(k=k.childNodes[h.endOffset],!k){h.setEndAfter(h.endContainer);continue}var l=k.nextSibling;l&&3==l.nodeType&&new RegExp("^[\n\r "+domUtils.fillChar+"]*$").test(l.nodeValue)&&h.setEndAfter(l)}if(h.endOffset!=h.endContainer[3==h.endContainer.nodeType?"nodeValue":"childNodes"].length)break;h.setEndAfter(h.endContainer)}}h.deleteContents(),h.select(!0),d.__hasEnterExecCommand=!0;var m=f;2===c?m=b(m):c&&(m=e),d.execCommand("inserthtml",m,!0),d.__hasEnterExecCommand=!1;for(var n=d.selection.getRange();!domUtils.isBody(n.startContainer)&&!n.startOffset&&n.startContainer[3==n.startContainer.nodeType?"nodeValue":"childNodes"].length;)n.setStartBefore(n.startContainer);var o=n.createAddress(!0);g.endAddress=o.startAddress}}),d.addListener("ready",function(){domUtils.on(d.body,"cut",function(){var a=d.selection.getRange();!a.collapsed&&d.undoManger&&d.undoManger.save()}),domUtils.on(d.body,browser.ie||browser.opera?"keydown":"paste",function(b){(!browser.ie&&!browser.opera||(b.ctrlKey||b.metaKey)&&"86"==b.keyCode)&&a.call(d,function(a){c(a)})})}),d.commands.paste={execCommand:function(b){browser.ie?(a.call(d,function(a){c(a)}),d.document.execCommand("paste")):alert(d.getLang("pastemsg"))}}},UE.plugins.pasteplain=function(){var a=this;a.setOpt({pasteplain:!1,filterTxtRules:function(){function a(a){a.tagName="p",a.setStyle()}function b(a){a.parentNode.removeChild(a,!0)}return{"-":"script style object iframe embed input select",p:{$:{}},br:{$:{}},div:function(a){for(var b,c=UE.uNode.createElement("p");b=a.firstChild();)"text"!=b.type&&UE.dom.dtd.$block[b.tagName]?c.firstChild()?(a.parentNode.insertBefore(c,a),c=UE.uNode.createElement("p")):a.parentNode.insertBefore(b,a):c.appendChild(b);c.firstChild()&&a.parentNode.insertBefore(c,a),a.parentNode.removeChild(a)},ol:b,ul:b,dl:b,dt:b,dd:b,li:b,caption:a,th:a,tr:a,h1:a,h2:a,h3:a,h4:a,h5:a,h6:a,td:function(a){var b=!!a.innerText();b&&a.parentNode.insertAfter(UE.uNode.createText("    "),a),a.parentNode.removeChild(a,a.innerText())}}}()});var b=a.options.pasteplain;a.commands.pasteplain={queryCommandState:function(){return b?1:0},execCommand:function(){b=0|!b},notNeedUndo:1}},UE.plugins.list=function(){function a(a){var b=[];for(var c in a)b.push(c);return b}function b(a){var b=a.className;return domUtils.hasClass(a,/custom_/)?b.match(/custom_(\w+)/)[1]:domUtils.getStyle(a,"list-style-type")}function c(a,c){utils.each(domUtils.getElementsByTagName(a,"ol ul"),function(f){if(domUtils.inDoc(f,a)){var g=f.parentNode;if(g.tagName==f.tagName){var h=b(f)||("OL"==f.tagName?"decimal":"disc"),i=b(g)||("OL"==g.tagName?"decimal":"disc");if(h==i){var l=utils.indexOf(k[f.tagName],h);l=l+1==k[f.tagName].length?0:l+1,e(f,k[f.tagName][l])}}var m=0,n=2;domUtils.hasClass(f,/custom_/)?/[ou]l/i.test(g.tagName)&&domUtils.hasClass(g,/custom_/)||(n=1):/[ou]l/i.test(g.tagName)&&domUtils.hasClass(g,/custom_/)&&(n=3);var o=domUtils.getStyle(f,"list-style-type");o&&(f.style.cssText="list-style-type:"+o),f.className=utils.trim(f.className.replace(/list-paddingleft-\w+/,""))+" list-paddingleft-"+n,utils.each(domUtils.getElementsByTagName(f,"li"),function(a){if(a.style.cssText&&(a.style.cssText=""),!a.firstChild)return void domUtils.remove(a);if(a.parentNode===f){if(m++,domUtils.hasClass(f,/custom_/)){var c=1,d=b(f);if("OL"==f.tagName){if(d)switch(d){case"cn":case"cn1":case"cn2":m>10&&(m%10==0||m>10&&20>m)?c=2:m>20&&(c=3);break;case"num2":m>9&&(c=2)}a.className="list-"+j[d]+m+" list-"+d+"-paddingleft-"+c}else a.className="list-"+j[d]+" list-"+d+"-paddingleft"}else a.className=a.className.replace(/list-[\w\-]+/gi,"");var e=a.getAttribute("class");null===e||e.replace(/\s/g,"")||domUtils.removeAttributes(a,"class")}}),!c&&d(f,f.tagName.toLowerCase(),b(f)||domUtils.getStyle(f,"list-style-type"),!0)}})}function d(a,d,e,f){var g=a.nextSibling;g&&1==g.nodeType&&g.tagName.toLowerCase()==d&&(b(g)||domUtils.getStyle(g,"list-style-type")||("ol"==d?"decimal":"disc"))==e&&(domUtils.moveChild(g,a),0==g.childNodes.length&&domUtils.remove(g)),g&&domUtils.isFillChar(g)&&domUtils.remove(g);var h=a.previousSibling;h&&1==h.nodeType&&h.tagName.toLowerCase()==d&&(b(h)||domUtils.getStyle(h,"list-style-type")||("ol"==d?"decimal":"disc"))==e&&domUtils.moveChild(a,h),h&&domUtils.isFillChar(h)&&domUtils.remove(h),!f&&domUtils.isEmptyBlock(a)&&domUtils.remove(a),b(a)&&c(a.ownerDocument,!0)}function e(a,b){j[b]&&(a.className="custom_"+b);try{domUtils.setStyle(a,"list-style-type",b)}catch(c){}}function f(a){var b=a.previousSibling;b&&domUtils.isEmptyBlock(b)&&domUtils.remove(b),b=a.nextSibling,b&&domUtils.isEmptyBlock(b)&&domUtils.remove(b)}function g(a){for(;a&&!domUtils.isBody(a);){if("TABLE"==a.nodeName)return null;if("LI"==a.nodeName)return a;a=a.parentNode}}var h=this,i={TD:1,PRE:1,BLOCKQUOTE:1},j={cn:"cn-1-",cn1:"cn-2-",cn2:"cn-3-",num:"num-1-",num1:"num-2-",num2:"num-3-",dash:"dash",dot:"dot"};h.setOpt({autoTransWordToList:!1,insertorderedlist:{num:"",num1:"",num2:"",cn:"",cn1:"",cn2:"",decimal:"","lower-alpha":"","lower-roman":"","upper-alpha":"","upper-roman":""},insertunorderedlist:{circle:"",disc:"",square:"",dash:"",dot:""},listDefaultPaddingLeft:"30",listiconpath:"http://bs.baidu.com/listicon/",maxListLevel:-1,disablePInList:!1});var k={OL:a(h.options.insertorderedlist),UL:a(h.options.insertunorderedlist)},l=h.options.listiconpath;for(var m in j)h.options.insertorderedlist.hasOwnProperty(m)||h.options.insertunorderedlist.hasOwnProperty(m)||delete j[m];h.ready(function(){var a=[];for(var b in j){if("dash"==b||"dot"==b)a.push("li.list-"+j[b]+"{background-image:url("+l+j[b]+".gif)}"),a.push("ul.custom_"+b+"{list-style:none;}ul.custom_"+b+" li{background-position:0 3px;background-repeat:no-repeat}");else{for(var c=0;99>c;c++)a.push("li.list-"+j[b]+c+"{background-image:url("+l+"list-"+j[b]+c+".gif)}");a.push("ol.custom_"+b+"{list-style:none;}ol.custom_"+b+" li{background-position:0 3px;background-repeat:no-repeat}")}switch(b){case"cn":a.push("li.list-"+b+"-paddingleft-1{padding-left:25px}"),a.push("li.list-"+b+"-paddingleft-2{padding-left:40px}"),a.push("li.list-"+b+"-paddingleft-3{padding-left:55px}");break;case"cn1":a.push("li.list-"+b+"-paddingleft-1{padding-left:30px}"),a.push("li.list-"+b+"-paddingleft-2{padding-left:40px}"),a.push("li.list-"+b+"-paddingleft-3{padding-left:55px}");break;case"cn2":a.push("li.list-"+b+"-paddingleft-1{padding-left:40px}"),a.push("li.list-"+b+"-paddingleft-2{padding-left:55px}"),a.push("li.list-"+b+"-paddingleft-3{padding-left:68px}");break;case"num":case"num1":a.push("li.list-"+b+"-paddingleft-1{padding-left:25px}");break;case"num2":a.push("li.list-"+b+"-paddingleft-1{padding-left:35px}"),a.push("li.list-"+b+"-paddingleft-2{padding-left:40px}");break;case"dash":a.push("li.list-"+b+"-paddingleft{padding-left:35px}");break;case"dot":a.push("li.list-"+b+"-paddingleft{padding-left:20px}")}}a.push(".list-paddingleft-1{padding-left:0}"),a.push(".list-paddingleft-2{padding-left:"+h.options.listDefaultPaddingLeft+"px}"),a.push(".list-paddingleft-3{padding-left:"+2*h.options.listDefaultPaddingLeft+"px}"),utils.cssRule("list","ol,ul{margin:0;pading:0;"+(browser.ie?"":"width:95%")+"}li{clear:both;}"+a.join("\n"),h.document)}),h.ready(function(){domUtils.on(h.body,"cut",function(){setTimeout(function(){var a,b=h.selection.getRange();if(!b.collapsed&&(a=domUtils.findParentByTagName(b.startContainer,"li",!0))&&!a.nextSibling&&domUtils.isEmptyBlock(a)){var c,d=a.parentNode;if(c=d.previousSibling)domUtils.remove(d),b.setStartAtLast(c).collapse(!0),b.select(!0);else if(c=d.nextSibling)domUtils.remove(d),b.setStartAtFirst(c).collapse(!0),b.select(!0);else{var e=h.document.createElement("p");domUtils.fillNode(h.document,e),d.parentNode.insertBefore(e,d),domUtils.remove(d),b.setStart(e,0).collapse(!0),b.select(!0)}}})})}),h.addListener("beforepaste",function(a,c){var d,e=this,f=e.selection.getRange(),g=UE.htmlparser(c.html,!0);if(d=domUtils.findParentByTagName(f.startContainer,"li",!0)){var h=d.parentNode,i="OL"==h.tagName?"ul":"ol";utils.each(g.getNodesByTagName(i),function(c){if(c.tagName=h.tagName,c.setAttr(),c.parentNode===g)a=b(h)||("OL"==h.tagName?"decimal":"disc");else{var d=c.parentNode.getAttr("class");a=d&&/custom_/.test(d)?d.match(/custom_(\w+)/)[1]:c.parentNode.getStyle("list-style-type"),a||(a="OL"==h.tagName?"decimal":"disc")}var e=utils.indexOf(k[h.tagName],a);c.parentNode!==g&&(e=e+1==k[h.tagName].length?0:e+1);var f=k[h.tagName][e];j[f]?c.setAttr("class","custom_"+f):c.setStyle("list-style-type",f)})}c.html=g.toHtml()}),h.getOpt("disablePInList")===!0&&h.addOutputRule(function(a){utils.each(a.getNodesByTagName("li"),function(a){var b=[],c=0;utils.each(a.children,function(d){if("p"==d.tagName){for(var e;e=d.children.pop();)b.splice(c,0,e),e.parentNode=a,lastNode=e;if(e=b[b.length-1],!e||"element"!=e.type||"br"!=e.tagName){var f=UE.uNode.createElement("br");f.parentNode=a,b.push(f)}c=b.length}}),b.length&&(a.children=b)})}),h.addInputRule(function(a){function b(a,b){var e=b.firstChild();if(e&&"element"==e.type&&"span"==e.tagName&&/Wingdings|Symbol/.test(e.getStyle("font-family"))){for(var f in d)if(d[f]==e.data)return f;return"disc"}for(var f in c)if(c[f].test(a))return f}if(utils.each(a.getNodesByTagName("li"),function(a){for(var b,c=UE.uNode.createElement("p"),d=0;b=a.children[d];)"text"==b.type||dtd.p[b.tagName]?c.appendChild(b):c.firstChild()?(a.insertBefore(c,b),c=UE.uNode.createElement("p"),d+=2):d++;(c.firstChild()&&!c.parentNode||!a.firstChild())&&a.appendChild(c),c.firstChild()||c.innerHTML(browser.ie?" ":"
              ");var e=a.firstChild(),f=e.lastChild();f&&"text"==f.type&&/^\s*$/.test(f.data)&&e.removeChild(f)}),h.options.autoTransWordToList){var c={num1:/^\d+\)/,decimal:/^\d+\./,"lower-alpha":/^[a-z]+\)/,"upper-alpha":/^[A-Z]+\./,cn:/^[\u4E00\u4E8C\u4E09\u56DB\u516d\u4e94\u4e03\u516b\u4e5d]+[\u3001]/,cn2:/^\([\u4E00\u4E8C\u4E09\u56DB\u516d\u4e94\u4e03\u516b\u4e5d]+\)/},d={square:"n"};utils.each(a.getNodesByTagName("p"),function(a){function d(a,b,d){if("ol"==a.tagName)if(browser.ie){var e=b.firstChild();"element"==e.type&&"span"==e.tagName&&c[d].test(e.innerText())&&b.removeChild(e)}else b.innerHTML(b.innerHTML().replace(c[d],""));else b.removeChild(b.firstChild());var f=UE.uNode.createElement("li");f.appendChild(b),a.appendChild(f)}if("MsoListParagraph"==a.getAttr("class")){a.setStyle("margin",""),a.setStyle("margin-left",""),a.setAttr("class","");var e,f=a,g=a;if("li"!=a.parentNode.tagName&&(e=b(a.innerText(),a))){var i=UE.uNode.createElement(h.options.insertorderedlist.hasOwnProperty(e)?"ol":"ul");for(j[e]?i.setAttr("class","custom_"+e):i.setStyle("list-style-type",e);a&&"li"!=a.parentNode.tagName&&b(a.innerText(),a);)f=a.nextSibling(),f||a.parentNode.insertBefore(i,a),d(i,a,e),a=f;!i.parentNode&&a&&a.parentNode&&a.parentNode.insertBefore(i,a)}var k=g.firstChild();k&&"element"==k.type&&"span"==k.tagName&&/^\s*( )+\s*$/.test(k.innerText())&&k.parentNode.removeChild(k)}})}}),h.addListener("contentchange",function(){c(h.document)}),h.addListener("keydown",function(a,b){function c(){b.preventDefault?b.preventDefault():b.returnValue=!1,h.fireEvent("contentchange"),h.undoManger&&h.undoManger.save()}function d(a,b){for(;a&&!domUtils.isBody(a);){if(b(a))return null;if(1==a.nodeType&&/[ou]l/i.test(a.tagName))return a;a=a.parentNode}return null}var e=b.keyCode||b.which;if(13==e&&!b.shiftKey){var g=h.selection.getRange(),i=domUtils.findParent(g.startContainer,function(a){return domUtils.isBlockElm(a)},!0),j=domUtils.findParentByTagName(g.startContainer,"li",!0);if(i&&"PRE"!=i.tagName&&!j){var k=i.innerHTML.replace(new RegExp(domUtils.fillChar,"g"),"");/^\s*1\s*\.[^\d]/.test(k)&&(i.innerHTML=k.replace(/^\s*1\s*\./,""),g.setStartAtLast(i).collapse(!0).select(),h.__hasEnterExecCommand=!0,h.execCommand("insertorderedlist"),h.__hasEnterExecCommand=!1)}var l=h.selection.getRange(),m=d(l.startContainer,function(a){return"TABLE"==a.tagName}),n=l.collapsed?m:d(l.endContainer,function(a){return"TABLE"==a.tagName});if(m&&n&&m===n){if(!l.collapsed){if(m=domUtils.findParentByTagName(l.startContainer,"li",!0),n=domUtils.findParentByTagName(l.endContainer,"li",!0),!m||!n||m!==n){var o=l.cloneRange(),p=o.collapse(!1).createBookmark();l.deleteContents(),o.moveToBookmark(p);var j=domUtils.findParentByTagName(o.startContainer,"li",!0);return f(j),o.select(),void c()}if(l.deleteContents(),j=domUtils.findParentByTagName(l.startContainer,"li",!0),j&&domUtils.isEmptyBlock(j))return v=j.previousSibling,next=j.nextSibling,s=h.document.createElement("p"),domUtils.fillNode(h.document,s),q=j.parentNode,v&&next?(l.setStart(next,0).collapse(!0).select(!0),domUtils.remove(j)):((v||next)&&v?j.parentNode.parentNode.insertBefore(s,q.nextSibling):q.parentNode.insertBefore(s,q),domUtils.remove(j),q.firstChild||domUtils.remove(q),l.setStart(s,0).setCursor()),void c()}if(j=domUtils.findParentByTagName(l.startContainer,"li",!0)){ +if(domUtils.isEmptyBlock(j)){p=l.createBookmark();var q=j.parentNode;if(j!==q.lastChild?(domUtils.breakParent(j,q),f(j)):(q.parentNode.insertBefore(j,q.nextSibling),domUtils.isEmptyNode(q)&&domUtils.remove(q)),!dtd.$list[j.parentNode.tagName])if(domUtils.isBlockElm(j.firstChild))domUtils.remove(j,!0);else{for(s=h.document.createElement("p"),j.parentNode.insertBefore(s,j);j.firstChild;)s.appendChild(j.firstChild);domUtils.remove(j)}l.moveToBookmark(p).select()}else{var r=j.firstChild;if(!r||!domUtils.isBlockElm(r)){var s=h.document.createElement("p");for(!j.firstChild&&domUtils.fillNode(h.document,s);j.firstChild;)s.appendChild(j.firstChild);j.appendChild(s),r=s}var t=h.document.createElement("span");l.insertNode(t),domUtils.breakParent(t,j);var u=t.nextSibling;r=u.firstChild,r||(s=h.document.createElement("p"),domUtils.fillNode(h.document,s),u.appendChild(s),r=s),domUtils.isEmptyNode(r)&&(r.innerHTML="",domUtils.fillNode(h.document,r)),l.setStart(r,0).collapse(!0).shrinkBoundary().select(),domUtils.remove(t);var v=u.previousSibling;v&&domUtils.isEmptyBlock(v)&&(v.innerHTML="

              ",domUtils.fillNode(h.document,v.firstChild))}c()}}}if(8==e&&(l=h.selection.getRange(),l.collapsed&&domUtils.isStartInblock(l)&&(o=l.cloneRange().trimBoundary(),j=domUtils.findParentByTagName(l.startContainer,"li",!0),j&&domUtils.isStartInblock(o)))){if(m=domUtils.findParentByTagName(l.startContainer,"p",!0),m&&m!==j.firstChild){var q=domUtils.findParentByTagName(m,["ol","ul"]);return domUtils.breakParent(m,q),f(m),h.fireEvent("contentchange"),l.setStart(m,0).setCursor(!1,!0),h.fireEvent("saveScene"),void domUtils.preventDefault(b)}if(j&&(v=j.previousSibling)){if(46==e&&j.childNodes.length)return;if(dtd.$list[v.tagName]&&(v=v.lastChild),h.undoManger&&h.undoManger.save(),r=j.firstChild,domUtils.isBlockElm(r))if(domUtils.isEmptyNode(r))for(v.appendChild(r),l.setStart(r,0).setCursor(!1,!0);j.firstChild;)v.appendChild(j.firstChild);else t=h.document.createElement("span"),l.insertNode(t),domUtils.isEmptyBlock(v)&&(v.innerHTML=""),domUtils.moveChild(j,v),l.setStartBefore(t).collapse(!0).select(!0),domUtils.remove(t);else if(domUtils.isEmptyNode(j)){var s=h.document.createElement("p");v.appendChild(s),l.setStart(s,0).setCursor()}else for(l.setEnd(v,v.childNodes.length).collapse().select(!0);j.firstChild;)v.appendChild(j.firstChild);return domUtils.remove(j),h.fireEvent("contentchange"),h.fireEvent("saveScene"),void domUtils.preventDefault(b)}if(j&&!j.previousSibling){var q=j.parentNode,p=l.createBookmark();if(domUtils.isTagNode(q.parentNode,"ol ul"))q.parentNode.insertBefore(j,q),domUtils.isEmptyNode(q)&&domUtils.remove(q);else{for(;j.firstChild;)q.parentNode.insertBefore(j.firstChild,q);domUtils.remove(j),domUtils.isEmptyNode(q)&&domUtils.remove(q)}return l.moveToBookmark(p).setCursor(!1,!0),h.fireEvent("contentchange"),h.fireEvent("saveScene"),void domUtils.preventDefault(b)}}}),h.addListener("keyup",function(a,c){var e=c.keyCode||c.which;if(8==e){var f,g=h.selection.getRange();(f=domUtils.findParentByTagName(g.startContainer,["ol","ul"],!0))&&d(f,f.tagName.toLowerCase(),b(f)||domUtils.getComputedStyle(f,"list-style-type"),!0)}}),h.addListener("tabkeydown",function(){function a(a){if(-1!=h.options.maxListLevel){for(var b=a.parentNode,c=0;/[ou]l/i.test(b.tagName);)c++,b=b.parentNode;if(c>=h.options.maxListLevel)return!0}}var c=h.selection.getRange(),f=domUtils.findParentByTagName(c.startContainer,"li",!0);if(f){var g;if(!c.collapsed){h.fireEvent("saveScene"),g=c.createBookmark();for(var i,j,l=0,m=domUtils.findParents(f);j=m[l++];)if(domUtils.isTagNode(j,"ol ul")){i=j;break}var n=f;if(g.end)for(;n&&!(domUtils.getPosition(n,g.end)&domUtils.POSITION_FOLLOWING);)if(a(n))n=domUtils.getNextDomNode(n,!1,null,function(a){return a!==i});else{var o=n.parentNode,p=h.document.createElement(o.tagName),q=utils.indexOf(k[p.tagName],b(o)||domUtils.getComputedStyle(o,"list-style-type")),r=q+1==k[p.tagName].length?0:q+1,s=k[p.tagName][r];for(e(p,s),o.insertBefore(p,n);n&&!(domUtils.getPosition(n,g.end)&domUtils.POSITION_FOLLOWING);){if(f=n.nextSibling,p.appendChild(n),!f||domUtils.isTagNode(f,"ol ul")){if(f)for(;(f=f.firstChild)&&"LI"!=f.tagName;);else f=domUtils.getNextDomNode(n,!1,null,function(a){return a!==i});break}n=f}d(p,p.tagName.toLowerCase(),s),n=f}return h.fireEvent("contentchange"),c.moveToBookmark(g).select(),!0}if(a(f))return!0;var o=f.parentNode,p=h.document.createElement(o.tagName),q=utils.indexOf(k[p.tagName],b(o)||domUtils.getComputedStyle(o,"list-style-type"));q=q+1==k[p.tagName].length?0:q+1;var s=k[p.tagName][q];if(e(p,s),domUtils.isStartInblock(c))return h.fireEvent("saveScene"),g=c.createBookmark(),o.insertBefore(p,f),p.appendChild(f),d(p,p.tagName.toLowerCase(),s),h.fireEvent("contentchange"),c.moveToBookmark(g).select(!0),!0}}),h.commands.insertorderedlist=h.commands.insertunorderedlist={execCommand:function(a,c){c||(c="insertorderedlist"==a.toLowerCase()?"decimal":"disc");var f=this,h=this.selection.getRange(),j=function(a){return 1==a.nodeType?"br"!=a.tagName.toLowerCase():!domUtils.isWhitespace(a)},k="insertorderedlist"==a.toLowerCase()?"ol":"ul",l=f.document.createDocumentFragment();h.adjustmentBoundary().shrinkBoundary();var m,n,o,p,q=h.createBookmark(!0),r=g(f.document.getElementById(q.start)),s=0,t=g(f.document.getElementById(q.end)),u=0;if(r||t){if(r&&(m=r.parentNode),q.end||(t=r),t&&(n=t.parentNode),m===n){for(;r!==t;){if(p=r,r=r.nextSibling,!domUtils.isBlockElm(p.firstChild)){for(var v=f.document.createElement("p");p.firstChild;)v.appendChild(p.firstChild);p.appendChild(v)}l.appendChild(p)}if(p=f.document.createElement("span"),m.insertBefore(p,t),!domUtils.isBlockElm(t.firstChild)){for(v=f.document.createElement("p");t.firstChild;)v.appendChild(t.firstChild);t.appendChild(v)}l.appendChild(t),domUtils.breakParent(p,m),domUtils.isEmptyNode(p.previousSibling)&&domUtils.remove(p.previousSibling),domUtils.isEmptyNode(p.nextSibling)&&domUtils.remove(p.nextSibling);var w=b(m)||domUtils.getComputedStyle(m,"list-style-type")||("insertorderedlist"==a.toLowerCase()?"decimal":"disc");if(m.tagName.toLowerCase()==k&&w==c){for(var x,y=0,z=f.document.createDocumentFragment();x=l.firstChild;)if(domUtils.isTagNode(x,"ol ul"))z.appendChild(x);else for(;x.firstChild;)z.appendChild(x.firstChild),domUtils.remove(x);p.parentNode.insertBefore(z,p)}else o=f.document.createElement(k),e(o,c),o.appendChild(l),p.parentNode.insertBefore(o,p);return domUtils.remove(p),o&&d(o,k,c),void h.moveToBookmark(q).select()}if(r){for(;r;){if(p=r.nextSibling,domUtils.isTagNode(r,"ol ul"))l.appendChild(r);else{for(var A=f.document.createDocumentFragment(),B=0;r.firstChild;)domUtils.isBlockElm(r.firstChild)&&(B=1),A.appendChild(r.firstChild);if(B)l.appendChild(A);else{var C=f.document.createElement("p");C.appendChild(A),l.appendChild(C)}domUtils.remove(r)}r=p}m.parentNode.insertBefore(l,m.nextSibling),domUtils.isEmptyNode(m)?(h.setStartBefore(m),domUtils.remove(m)):h.setStartAfter(m),s=1}if(t&&domUtils.inDoc(n,f.document)){for(r=n.firstChild;r&&r!==t;){if(p=r.nextSibling,domUtils.isTagNode(r,"ol ul"))l.appendChild(r);else{for(A=f.document.createDocumentFragment(),B=0;r.firstChild;)domUtils.isBlockElm(r.firstChild)&&(B=1),A.appendChild(r.firstChild);B?l.appendChild(A):(C=f.document.createElement("p"),C.appendChild(A),l.appendChild(C)),domUtils.remove(r)}r=p}var D=domUtils.createElement(f.document,"div",{tmpDiv:1});domUtils.moveChild(t,D),l.appendChild(D),domUtils.remove(t),n.parentNode.insertBefore(l,n),h.setEndBefore(n),domUtils.isEmptyNode(n)&&domUtils.remove(n),u=1}}s||h.setStartBefore(f.document.getElementById(q.start)),q.end&&!u&&h.setEndAfter(f.document.getElementById(q.end)),h.enlarge(!0,function(a){return i[a.tagName]}),l=f.document.createDocumentFragment();for(var E,F=h.createBookmark(),G=domUtils.getNextDomNode(F.start,!1,j),H=h.cloneRange(),I=domUtils.isBlockElm;G&&G!==F.end&&domUtils.getPosition(G,F.end)&domUtils.POSITION_PRECEDING;)if(3==G.nodeType||dtd.li[G.tagName]){if(1==G.nodeType&&dtd.$list[G.tagName]){for(;G.firstChild;)l.appendChild(G.firstChild);E=domUtils.getNextDomNode(G,!1,j),domUtils.remove(G),G=E;continue}for(E=G,H.setStartBefore(G);G&&G!==F.end&&(!I(G)||domUtils.isBookmarkNode(G));)E=G,G=domUtils.getNextDomNode(G,!1,null,function(a){return!i[a.tagName]});G&&I(G)&&(p=domUtils.getNextDomNode(E,!1,j),p&&domUtils.isBookmarkNode(p)&&(G=domUtils.getNextDomNode(p,!1,j),E=p)),H.setEndAfter(E),G=domUtils.getNextDomNode(E,!1,j);var J=h.document.createElement("li");if(J.appendChild(H.extractContents()),domUtils.isEmptyNode(J)){for(var E=h.document.createElement("p");J.firstChild;)E.appendChild(J.firstChild);J.appendChild(E)}l.appendChild(J)}else G=domUtils.getNextDomNode(G,!0,j);h.moveToBookmark(F).collapse(!0),o=f.document.createElement(k),e(o,c),o.appendChild(l),h.insertNode(o),d(o,k,c);for(var x,y=0,K=domUtils.getElementsByTagName(o,"div");x=K[y++];)x.getAttribute("tmpDiv")&&domUtils.remove(x,!0);h.moveToBookmark(q).select()},queryCommandState:function(a){for(var b,c="insertorderedlist"==a.toLowerCase()?"ol":"ul",d=this.selection.getStartElementPath(),e=0;b=d[e++];){if("TABLE"==b.nodeName)return 0;if(c==b.nodeName.toLowerCase())return 1}return 0},queryCommandValue:function(a){for(var c,d,e="insertorderedlist"==a.toLowerCase()?"ol":"ul",f=this.selection.getStartElementPath(),g=0;d=f[g++];){if("TABLE"==d.nodeName){c=null;break}if(e==d.nodeName.toLowerCase()){c=d;break}}return c?b(c)||domUtils.getComputedStyle(c,"list-style-type"):null}}},function(){var a={textarea:function(a,b){var c=b.ownerDocument.createElement("textarea");return c.style.cssText="position:absolute;resize:none;width:100%;height:100%;border:0;padding:0;margin:0;overflow-y:auto;",browser.ie&&browser.version<8&&(c.style.width=b.offsetWidth+"px",c.style.height=b.offsetHeight+"px",b.onresize=function(){c.style.width=b.offsetWidth+"px",c.style.height=b.offsetHeight+"px"}),b.appendChild(c),{setContent:function(a){c.value=a},getContent:function(){return c.value},select:function(){var a;browser.ie?(a=c.createTextRange(),a.collapse(!0),a.select()):(c.setSelectionRange(0,0),c.focus())},dispose:function(){b.removeChild(c),b.onresize=null,c=null,b=null}}},codemirror:function(a,b){var c=window.CodeMirror(b,{mode:"text/html",tabMode:"indent",lineNumbers:!0,lineWrapping:!0}),d=c.getWrapperElement();return d.style.cssText='position:absolute;left:0;top:0;width:100%;height:100%;font-family:consolas,"Courier new",monospace;font-size:13px;',c.getScrollerElement().style.cssText="position:absolute;left:0;top:0;width:100%;height:100%;",c.refresh(),{getCodeMirror:function(){return c},setContent:function(a){c.setValue(a)},getContent:function(){return c.getValue()},select:function(){c.focus()},dispose:function(){b.removeChild(d),d=null,c=null}}}};UE.plugins.source=function(){function b(b){return a["codemirror"==f.sourceEditor&&window.CodeMirror?"codemirror":"textarea"](e,b)}var c,d,e=this,f=this.options,g=!1;f.sourceEditor=browser.ie?"textarea":f.sourceEditor||"codemirror",e.setOpt({sourceEditorFirst:!1});var h,i,j;e.commands.source={execCommand:function(){if(g=!g){j=e.selection.getRange().createAddress(!1,!0),e.undoManger&&e.undoManger.save(!0),browser.gecko&&(e.body.contentEditable=!1),h=e.iframe.style.cssText,e.iframe.style.cssText+="position:absolute;left:-32768px;top:-32768px;",e.fireEvent("beforegetcontent");var a=UE.htmlparser(e.body.innerHTML);e.filterOutputRule(a),a.traversal(function(a){if("element"==a.type)switch(a.tagName){case"td":case"th":case"caption":a.children&&1==a.children.length&&"br"==a.firstChild().tagName&&a.removeChild(a.firstChild());break;case"pre":a.innerText(a.innerText().replace(/ /g," "))}}),e.fireEvent("aftergetcontent");var f=a.toHtml(!0);c=b(e.iframe.parentNode),c.setContent(f),d=e.setContent,e.setContent=function(a){var b=UE.htmlparser(a);e.filterInputRule(b),a=b.toHtml(),c.setContent(a)},setTimeout(function(){c.select(),e.addListener("fullscreenchanged",function(){try{c.getCodeMirror().refresh()}catch(a){}})}),i=e.getContent,e.getContent=function(){return c.getContent()||"

              "+(browser.ie?"":"
              ")+"

              "}}else{e.iframe.style.cssText=h;var k=c.getContent()||"

              "+(browser.ie?"":"
              ")+"

              ";k=k.replace(new RegExp("[\\r\\t\\n ]*]*)>","g"),function(a,b){return b&&!dtd.$inlineWithA[b.toLowerCase()]?a.replace(/(^[\n\r\t ]*)|([\n\r\t ]*$)/g,""):a.replace(/(^[\n\r\t]*)|([\n\r\t]*$)/g,"")}),e.setContent=d,e.setContent(k),c.dispose(),c=null,e.getContent=i;var l=e.body.firstChild;if(l||(e.body.innerHTML="

              "+(browser.ie?"":"
              ")+"

              ",l=e.body.firstChild),e.undoManger&&e.undoManger.save(!0),browser.gecko){var m=document.createElement("input");m.style.cssText="position:absolute;left:0;top:-32768px",document.body.appendChild(m),e.body.contentEditable=!1,setTimeout(function(){domUtils.setViewportOffset(m,{left:-32768,top:0}),m.focus(),setTimeout(function(){e.body.contentEditable=!0,e.selection.getRange().moveToAddress(j).select(!0),domUtils.remove(m)})})}else try{e.selection.getRange().moveToAddress(j).select(!0)}catch(n){}}this.fireEvent("sourcemodechanged",g)},queryCommandState:function(){return 0|g},notNeedUndo:1};var k=e.queryCommandState;e.queryCommandState=function(a){return a=a.toLowerCase(),g?a in{source:1,fullscreen:1}?1:-1:k.apply(this,arguments)},"codemirror"==f.sourceEditor&&e.addListener("ready",function(){utils.loadFile(document,{src:f.codeMirrorJsUrl||f.UEDITOR_HOME_URL+"third-party/codemirror/codemirror.js",tag:"script",type:"text/javascript",defer:"defer"},function(){f.sourceEditorFirst&&setTimeout(function(){e.execCommand("source")},0)}),utils.loadFile(document,{tag:"link",rel:"stylesheet",type:"text/css",href:f.codeMirrorCssUrl||f.UEDITOR_HOME_URL+"third-party/codemirror/codemirror.css"})})}}(),UE.plugins.enterkey=function(){var a,b=this,c=b.options.enterTag;b.addListener("keyup",function(c,d){var e=d.keyCode||d.which;if(13==e){var f,g=b.selection.getRange(),h=g.startContainer;if(browser.ie)b.fireEvent("saveScene",!0,!0);else{if(/h\d/i.test(a)){if(browser.gecko){var i=domUtils.findParentByTagName(h,["h1","h2","h3","h4","h5","h6","blockquote","caption","table"],!0);i||(b.document.execCommand("formatBlock",!1,"

              "),f=1)}else if(1==h.nodeType){var j,k=b.document.createTextNode("");if(g.insertNode(k),j=domUtils.findParentByTagName(k,"div",!0)){for(var l=b.document.createElement("p");j.firstChild;)l.appendChild(j.firstChild);j.parentNode.insertBefore(l,j),domUtils.remove(j),g.setStartBefore(k).setCursor(),f=1}domUtils.remove(k)}b.undoManger&&f&&b.undoManger.save()}browser.opera&&g.select()}}}),b.addListener("keydown",function(d,e){var f=e.keyCode||e.which;if(13==f){if(b.fireEvent("beforeenterkeydown"))return void domUtils.preventDefault(e);b.fireEvent("saveScene",!0,!0),a="";var g=b.selection.getRange();if(!g.collapsed){var h=g.startContainer,i=g.endContainer,j=domUtils.findParentByTagName(h,"td",!0),k=domUtils.findParentByTagName(i,"td",!0);if(j&&k&&j!==k||!j&&k||j&&!k)return void(e.preventDefault?e.preventDefault():e.returnValue=!1)}if("p"==c)browser.ie||(h=domUtils.findParentByTagName(g.startContainer,["ol","ul","p","h1","h2","h3","h4","h5","h6","blockquote","caption"],!0),h||browser.opera?(a=h.tagName,"p"==h.tagName.toLowerCase()&&browser.gecko&&domUtils.removeDirtyAttr(h)):(b.document.execCommand("formatBlock",!1,"

              "),browser.gecko&&(g=b.selection.getRange(),h=domUtils.findParentByTagName(g.startContainer,"p",!0),h&&domUtils.removeDirtyAttr(h))));else if(e.preventDefault?e.preventDefault():e.returnValue=!1,g.collapsed){m=g.document.createElement("br"),g.insertNode(m);var l=m.parentNode;l.lastChild===m?(m.parentNode.insertBefore(m.cloneNode(!0),m),g.setStartBefore(m)):g.setStartAfter(m),g.setCursor()}else if(g.deleteContents(),h=g.startContainer,1==h.nodeType&&(h=h.childNodes[g.startOffset])){for(;1==h.nodeType;){if(dtd.$empty[h.tagName])return g.setStartBefore(h).setCursor(),b.undoManger&&b.undoManger.save(),!1;if(!h.firstChild){var m=g.document.createElement("br");return h.appendChild(m),g.setStart(h,0).setCursor(),b.undoManger&&b.undoManger.save(),!1}h=h.firstChild}h===g.startContainer.childNodes[g.startOffset]?(m=g.document.createElement("br"),g.insertNode(m).setCursor()):g.setStart(h,0).setCursor()}else m=g.document.createElement("br"),g.insertNode(m).setStartAfter(m).setCursor()}})},UE.plugins.keystrokes=function(){var a=this,b=!0;a.addListener("keydown",function(c,d){var e=d.keyCode||d.which,f=a.selection.getRange();if(!f.collapsed&&!(d.ctrlKey||d.shiftKey||d.altKey||d.metaKey)&&(e>=65&&90>=e||e>=48&&57>=e||e>=96&&111>=e||{13:1,8:1,46:1}[e])){var g=f.startContainer;if(domUtils.isFillChar(g)&&f.setStartBefore(g),g=f.endContainer,domUtils.isFillChar(g)&&f.setEndAfter(g),f.txtToElmBoundary(),f.endContainer&&1==f.endContainer.nodeType&&(g=f.endContainer.childNodes[f.endOffset],g&&domUtils.isBr(g)&&f.setEndAfter(g)),0==f.startOffset&&(g=f.startContainer,domUtils.isBoundaryNode(g,"firstChild")&&(g=f.endContainer,f.endOffset==(3==g.nodeType?g.nodeValue.length:g.childNodes.length)&&domUtils.isBoundaryNode(g,"lastChild"))))return a.fireEvent("saveScene"),a.body.innerHTML="

              "+(browser.ie?"":"
              ")+"

              ",f.setStart(a.body.firstChild,0).setCursor(!1,!0),void a._selectionChange()}if(e==keymap.Backspace){if(f=a.selection.getRange(),b=f.collapsed,a.fireEvent("delkeydown",d))return;var h,i;if(f.collapsed&&f.inFillChar()&&(h=f.startContainer,domUtils.isFillChar(h)?(f.setStartBefore(h).shrinkBoundary(!0).collapse(!0),domUtils.remove(h)):(h.nodeValue=h.nodeValue.replace(new RegExp("^"+domUtils.fillChar),""),f.startOffset--,f.collapse(!0).select(!0))),h=f.getClosedNode())return a.fireEvent("saveScene"),f.setStartBefore(h),domUtils.remove(h),f.setCursor(),a.fireEvent("saveScene"),void domUtils.preventDefault(d);if(!browser.ie&&(h=domUtils.findParentByTagName(f.startContainer,"table",!0),i=domUtils.findParentByTagName(f.endContainer,"table",!0),h&&!i||!h&&i||h!==i))return void d.preventDefault()}if(e==keymap.Tab){var j={ol:1,ul:1,table:1};if(a.fireEvent("tabkeydown",d))return void domUtils.preventDefault(d);var k=a.selection.getRange();a.fireEvent("saveScene");for(var l=0,m="",n=a.options.tabSize||4,o=a.options.tabNode||" ";n>l;l++)m+=o;var p=a.document.createElement("span");if(p.innerHTML=m+domUtils.fillChar,k.collapsed)k.insertNode(p.cloneNode(!0).firstChild).setCursor(!0);else{var q=function(a){return domUtils.isBlockElm(a)&&!j[a.tagName.toLowerCase()]};if(h=domUtils.findParent(k.startContainer,q,!0),i=domUtils.findParent(k.endContainer,q,!0),h&&i&&h===i)k.deleteContents(),k.insertNode(p.cloneNode(!0).firstChild).setCursor(!0);else{var r=k.createBookmark();k.enlarge(!0);for(var s=k.createBookmark(),t=domUtils.getNextDomNode(s.start,!1,q);t&&!(domUtils.getPosition(t,s.end)&domUtils.POSITION_FOLLOWING);)t.insertBefore(p.cloneNode(!0).firstChild,t.firstChild),t=domUtils.getNextDomNode(t,!1,q);k.moveToBookmark(s).moveToBookmark(r).select()}}domUtils.preventDefault(d)}if(browser.gecko&&46==e&&(k=a.selection.getRange(),k.collapsed&&(h=k.startContainer,domUtils.isEmptyBlock(h)))){for(var u=h.parentNode;1==domUtils.getChildCount(u)&&!domUtils.isBody(u);)h=u,u=u.parentNode;return void(h===u.lastChild&&d.preventDefault())}}),a.addListener("keyup",function(a,c){var d,e=c.keyCode||c.which,f=this;if(e==keymap.Backspace){if(f.fireEvent("delkeyup"))return;if(d=f.selection.getRange(),d.collapsed){var g,h=["h1","h2","h3","h4","h5","h6"];if((g=domUtils.findParentByTagName(d.startContainer,h,!0))&&domUtils.isEmptyBlock(g)){var i=g.previousSibling;if(i&&"TABLE"!=i.nodeName)return domUtils.remove(g),void d.setStartAtLast(i).setCursor(!1,!0);var j=g.nextSibling;if(j&&"TABLE"!=j.nodeName)return domUtils.remove(g),void d.setStartAtFirst(j).setCursor(!1,!0)}if(domUtils.isBody(d.startContainer)){var g=domUtils.createElement(f.document,"p",{innerHTML:browser.ie?domUtils.fillChar:"
              "});d.insertNode(g).setStart(g,0).setCursor(!1,!0)}}if(!b&&(3==d.startContainer.nodeType||1==d.startContainer.nodeType&&domUtils.isEmptyBlock(d.startContainer)))if(browser.ie){var k=d.document.createElement("span");d.insertNode(k).setStartBefore(k).collapse(!0),d.select(),domUtils.remove(k)}else d.select()}})},UE.plugins.fiximgclick=function(){function a(){this.editor=null,this.resizer=null,this.cover=null,this.doc=document,this.prePos={x:0,y:0},this.startPos={x:0,y:0}}var b=!1;return function(){var c=[[0,0,-1,-1],[0,0,0,-1],[0,0,1,-1],[0,0,-1,0],[0,0,1,0],[0,0,-1,1],[0,0,0,1],[0,0,1,1]];a.prototype={init:function(a){var b=this;b.editor=a,b.startPos=this.prePos={x:0,y:0},b.dragId=-1;var c=[],d=b.cover=document.createElement("div"),e=b.resizer=document.createElement("div");for(d.id=b.editor.ui.id+"_imagescale_cover",d.style.cssText="position:absolute;display:none;z-index:"+b.editor.options.zIndex+";filter:alpha(opacity=0); opacity:0;background:#CCC;",domUtils.on(d,"mousedown click",function(){b.hide()}),i=0;i<8;i++)c.push('');e.id=b.editor.ui.id+"_imagescale",e.className="edui-editor-imagescale",e.innerHTML=c.join(""),e.style.cssText+=";display:none;border:1px solid #3b77ff;z-index:"+b.editor.options.zIndex+";",b.editor.ui.getDom().appendChild(d),b.editor.ui.getDom().appendChild(e),b.initStyle(),b.initEvents()},initStyle:function(){utils.cssRule("imagescale",".edui-editor-imagescale{display:none;position:absolute;border:1px solid #38B2CE;cursor:hand;-webkit-box-sizing: content-box;-moz-box-sizing: content-box;box-sizing: content-box;}.edui-editor-imagescale span{position:absolute;width:6px;height:6px;overflow:hidden;font-size:0px;display:block;background-color:#3C9DD0;}.edui-editor-imagescale .edui-editor-imagescale-hand0{cursor:nw-resize;top:0;margin-top:-4px;left:0;margin-left:-4px;}.edui-editor-imagescale .edui-editor-imagescale-hand1{cursor:n-resize;top:0;margin-top:-4px;left:50%;margin-left:-4px;}.edui-editor-imagescale .edui-editor-imagescale-hand2{cursor:ne-resize;top:0;margin-top:-4px;left:100%;margin-left:-3px;}.edui-editor-imagescale .edui-editor-imagescale-hand3{cursor:w-resize;top:50%;margin-top:-4px;left:0;margin-left:-4px;}.edui-editor-imagescale .edui-editor-imagescale-hand4{cursor:e-resize;top:50%;margin-top:-4px;left:100%;margin-left:-3px;}.edui-editor-imagescale .edui-editor-imagescale-hand5{cursor:sw-resize;top:100%;margin-top:-3px;left:0;margin-left:-4px;}.edui-editor-imagescale .edui-editor-imagescale-hand6{cursor:s-resize;top:100%;margin-top:-3px;left:50%;margin-left:-4px;}.edui-editor-imagescale .edui-editor-imagescale-hand7{cursor:se-resize;top:100%;margin-top:-3px;left:100%;margin-left:-3px;}")},initEvents:function(){var a=this;a.startPos.x=a.startPos.y=0,a.isDraging=!1},_eventHandler:function(a){var c=this;switch(a.type){case"mousedown":var d,d=a.target||a.srcElement;-1!=d.className.indexOf("edui-editor-imagescale-hand")&&-1==c.dragId&&(c.dragId=d.className.slice(-1),c.startPos.x=c.prePos.x=a.clientX,c.startPos.y=c.prePos.y=a.clientY,domUtils.on(c.doc,"mousemove",c.proxy(c._eventHandler,c)));break;case"mousemove":-1!=c.dragId&&(c.updateContainerStyle(c.dragId,{x:a.clientX-c.prePos.x,y:a.clientY-c.prePos.y}),c.prePos.x=a.clientX,c.prePos.y=a.clientY,b=!0,c.updateTargetElement());break;case"mouseup":-1!=c.dragId&&(c.updateContainerStyle(c.dragId,{x:a.clientX-c.prePos.x,y:a.clientY-c.prePos.y}),c.updateTargetElement(),c.target.parentNode&&c.attachTo(c.target),c.dragId=-1),domUtils.un(c.doc,"mousemove",c.proxy(c._eventHandler,c)),b&&(b=!1,c.editor.fireEvent("contentchange"))}},updateTargetElement:function(){var a=this;domUtils.setStyles(a.target,{width:a.resizer.style.width,height:a.resizer.style.height}),a.target.width=parseInt(a.resizer.style.width),a.target.height=parseInt(a.resizer.style.height),a.attachTo(a.target)},updateContainerStyle:function(a,b){var d,e=this,f=e.resizer;0!=c[a][0]&&(d=parseInt(f.style.left)+b.x,f.style.left=e._validScaledProp("left",d)+"px"),0!=c[a][1]&&(d=parseInt(f.style.top)+b.y,f.style.top=e._validScaledProp("top",d)+"px"),0!=c[a][2]&&(d=f.clientWidth+c[a][2]*b.x,f.style.width=e._validScaledProp("width",d)+"px"),0!=c[a][3]&&(d=f.clientHeight+c[a][3]*b.y,f.style.height=e._validScaledProp("height",d)+"px")},_validScaledProp:function(a,b){var c=this.resizer,d=document;switch(b=isNaN(b)?0:b,a){case"left":return 0>b?0:b+c.clientWidth>d.clientWidth?d.clientWidth-c.clientWidth:b;case"top":return 0>b?0:b+c.clientHeight>d.clientHeight?d.clientHeight-c.clientHeight:b;case"width":return 0>=b?1:b+c.offsetLeft>d.clientWidth?d.clientWidth-c.offsetLeft:b;case"height":return 0>=b?1:b+c.offsetTop>d.clientHeight?d.clientHeight-c.offsetTop:b}},hideCover:function(){this.cover.style.display="none"},showCover:function(){var a=this,b=domUtils.getXY(a.editor.ui.getDom()),c=domUtils.getXY(a.editor.iframe);domUtils.setStyles(a.cover,{width:a.editor.iframe.offsetWidth+"px",height:a.editor.iframe.offsetHeight+"px",top:c.y-b.y+"px",left:c.x-b.x+"px",position:"absolute",display:""})},show:function(a){var b=this;b.resizer.style.display="block",a&&b.attachTo(a),domUtils.on(this.resizer,"mousedown",b.proxy(b._eventHandler,b)),domUtils.on(b.doc,"mouseup",b.proxy(b._eventHandler,b)),b.showCover(),b.editor.fireEvent("afterscaleshow",b),b.editor.fireEvent("saveScene")},hide:function(){var a=this;a.hideCover(),a.resizer.style.display="none",domUtils.un(a.resizer,"mousedown",a.proxy(a._eventHandler,a)),domUtils.un(a.doc,"mouseup",a.proxy(a._eventHandler,a)),a.editor.fireEvent("afterscalehide",a)},proxy:function(a,b){return function(c){return a.apply(b||this,arguments)}},attachTo:function(a){var b=this,c=b.target=a,d=this.resizer,e=domUtils.getXY(c),f=domUtils.getXY(b.editor.iframe),g=domUtils.getXY(d.parentNode);domUtils.setStyles(d,{width:c.width+"px",height:c.height+"px",left:f.x+e.x-b.editor.document.body.scrollLeft-g.x-parseInt(d.style.borderLeftWidth)+"px",top:f.y+e.y-b.editor.document.body.scrollTop-g.y-parseInt(d.style.borderTopWidth)+"px"})}}}(),function(){var b,c=this;c.setOpt("imageScaleEnabled",!0),!browser.ie&&c.options.imageScaleEnabled&&c.addListener("click",function(d,e){var f=c.selection.getRange(),g=f.getClosedNode();if(g&&"IMG"==g.tagName&&"false"!=c.body.contentEditable){if(-1!=g.className.indexOf("edui-faked-music")||g.getAttribute("anchorname")||domUtils.hasClass(g,"loadingclass")||domUtils.hasClass(g,"loaderrorclass"))return;if(!b){b=new a,b.init(c),c.ui.getDom().appendChild(b.resizer);var h,i=function(a){b.hide(),b.target&&c.selection.getRange().selectNode(b.target).select()},j=function(a){var b=a.target||a.srcElement;!b||void 0!==b.className&&-1!=b.className.indexOf("edui-editor-imagescale")||i(a)};c.addListener("afterscaleshow",function(a){c.addListener("beforekeydown",i),c.addListener("beforemousedown",j),domUtils.on(document,"keydown",i),domUtils.on(document,"mousedown",j),c.selection.getNative().removeAllRanges()}),c.addListener("afterscalehide",function(a){c.removeListener("beforekeydown",i),c.removeListener("beforemousedown",j),domUtils.un(document,"keydown",i),domUtils.un(document,"mousedown",j);var d=b.target;d.parentNode&&c.selection.getRange().selectNode(d).select()}),domUtils.on(b.resizer,"mousedown",function(a){c.selection.getNative().removeAllRanges();var d=a.target||a.srcElement;d&&-1==d.className.indexOf("edui-editor-imagescale-hand")&&(h=setTimeout(function(){b.hide(),b.target&&c.selection.getRange().selectNode(d).select()},200))}),domUtils.on(b.resizer,"mouseup",function(a){var b=a.target||a.srcElement;b&&-1==b.className.indexOf("edui-editor-imagescale-hand")&&clearTimeout(h)})}b.show(g)}else b&&"none"!=b.resizer.style.display&&b.hide()}),browser.webkit&&c.addListener("click",function(a,b){if("IMG"==b.target.tagName&&"false"!=c.body.contentEditable){var d=new dom.Range(c.document);d.selectNode(b.target).select()}})}}(),UE.plugin.register("autolink",function(){var a=0;return browser.ie?{}:{bindEvents:{reset:function(){a=0},keydown:function(a,b){var c=this,d=b.keyCode||b.which;if(32==d||13==d){for(var e,f,g=c.selection.getNative(),h=g.getRangeAt(0).cloneRange(),i=h.startContainer;1==i.nodeType&&h.startOffset>0&&(i=h.startContainer.childNodes[h.startOffset-1]);)h.setStart(i,1==i.nodeType?i.childNodes.length:i.nodeValue.length),h.collapse(!0),i=h.startContainer;do{if(0==h.startOffset){for(i=h.startContainer.previousSibling;i&&1==i.nodeType;)i=i.lastChild;if(!i||domUtils.isFillChar(i))break;e=i.nodeValue.length}else i=h.startContainer,e=h.startOffset;h.setStart(i,e-1),f=h.toString().charCodeAt(0)}while(160!=f&&32!=f);if(h.toString().replace(new RegExp(domUtils.fillChar,"g"),"").match(/(?:https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.)/i)){for(;h.toString().length&&!/^(?:https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.)/i.test(h.toString());)try{h.setStart(h.startContainer,h.startOffset+1)}catch(j){for(var i=h.startContainer;!(next=i.nextSibling);){if(domUtils.isBody(i))return;i=i.parentNode}h.setStart(next,0)}if(domUtils.findParentByTagName(h.startContainer,"a",!0))return;var k,l=c.document.createElement("a"),m=c.document.createTextNode(" ");c.undoManger&&c.undoManger.save(),l.appendChild(h.extractContents()),l.href=l.innerHTML=l.innerHTML.replace(/<[^>]+>/g,""),k=l.getAttribute("href").replace(new RegExp(domUtils.fillChar,"g"),""),k=/^(?:https?:\/\/)/gi.test(k)?k:"http://"+k,l.setAttribute("_src",utils.html(k)),l.href=utils.html(k),h.insertNode(l),l.parentNode.insertBefore(m,l.nextSibling),h.setStart(m,0),h.collapse(!0),g.removeAllRanges(),g.addRange(h),c.undoManger&&c.undoManger.save()}}}}}},function(){function a(a){if(3==a.nodeType)return null;if("A"==a.nodeName)return a;for(var b=a.lastChild;b;){if("A"==b.nodeName)return b;if(3==b.nodeType){if(domUtils.isWhitespace(b)){b=b.previousSibling;continue}return null}b=b.lastChild}}var b={37:1,38:1,39:1,40:1,13:1,32:1};browser.ie&&this.addListener("keyup",function(c,d){var e=this,f=d.keyCode;if(b[f]){var g=e.selection.getRange(),h=g.startContainer;if(13==f){for(;h&&!domUtils.isBody(h)&&!domUtils.isBlockElm(h);)h=h.parentNode;if(h&&!domUtils.isBody(h)&&"P"==h.nodeName){var i=h.previousSibling;if(i&&1==i.nodeType){var i=a(i);i&&!i.getAttribute("_href")&&domUtils.remove(i,!0)}}}else if(32==f)3==h.nodeType&&/^\s$/.test(h.nodeValue)&&(h=h.previousSibling,h&&"A"==h.nodeName&&!h.getAttribute("_href")&&domUtils.remove(h,!0));else if(h=domUtils.findParentByTagName(h,"a",!0),h&&!h.getAttribute("_href")){var j=g.createBookmark();domUtils.remove(h,!0),g.moveToBookmark(j).select(!0)}}})}),UE.plugins.autoheight=function(){function a(){var a=this;clearTimeout(e),f||(!a.queryCommandState||a.queryCommandState&&1!=a.queryCommandState("source"))&&(e=setTimeout(function(){for(var b=a.body.lastChild;b&&1!=b.nodeType;)b=b.previousSibling;b&&1==b.nodeType&&(b.style.clear="both",d=Math.max(domUtils.getXY(b).y+b.offsetHeight+25,Math.max(h.minFrameHeight,h.initialFrameHeight)),d!=g&&(d!==parseInt(a.iframe.parentNode.style.height)&&(a.iframe.parentNode.style.height=d+"px"),a.body.style.height=d+"px",g=d),domUtils.removeStyle(b,"clear"))},50))}var b=this;if(b.autoHeightEnabled=b.options.autoHeightEnabled!==!1,b.autoHeightEnabled){var c,d,e,f,g=0,h=b.options;b.addListener("fullscreenchanged",function(a,b){f=b}),b.addListener("destroy",function(){b.removeListener("contentchange afterinserthtml keyup mouseup",a)}),b.enableAutoHeight=function(){var b=this;if(b.autoHeightEnabled){var d=b.document;b.autoHeightEnabled=!0,c=d.body.style.overflowY,d.body.style.overflowY="hidden",b.addListener("contentchange afterinserthtml keyup mouseup",a),setTimeout(function(){a.call(b)},browser.gecko?100:0),b.fireEvent("autoheightchanged",b.autoHeightEnabled)}},b.disableAutoHeight=function(){b.body.style.overflowY=c||"",b.removeListener("contentchange",a),b.removeListener("keyup",a),b.removeListener("mouseup",a),b.autoHeightEnabled=!1,b.fireEvent("autoheightchanged",b.autoHeightEnabled)},b.on("setHeight",function(){b.disableAutoHeight()}),b.addListener("ready",function(){b.enableAutoHeight();var c;domUtils.on(browser.ie?b.body:b.document,browser.webkit?"dragover":"drop",function(){clearTimeout(c),c=setTimeout(function(){a.call(b)},100)});var d;window.onscroll=function(){ +null===d?d=this.scrollY:0==this.scrollY&&0!=d&&(b.window.scrollTo(0,0),d=null)}})}},UE.plugins.autofloat=function(){function a(){return UE.ui?1:(alert(g.autofloatMsg),0)}function b(){var a=document.body.style;a.backgroundImage='url("about:blank")',a.backgroundAttachment="fixed"}function c(){var a=domUtils.getXY(k),b=domUtils.getComputedStyle(k,"position"),c=domUtils.getComputedStyle(k,"left");k.style.width=k.offsetWidth+"px",k.style.zIndex=1*f.options.zIndex+1,k.parentNode.insertBefore(q,k),o||p&&browser.ie?("absolute"!=k.style.position&&(k.style.position="absolute"),k.style.top=(document.body.scrollTop||document.documentElement.scrollTop)-l+i+"px"):(browser.ie7Compat&&r&&(r=!1,k.style.left=domUtils.getXY(k).x-document.documentElement.getBoundingClientRect().left+2+"px"),"fixed"!=k.style.position&&(k.style.position="fixed",k.style.top=i+"px",("absolute"==b||"relative"==b)&&parseFloat(c)&&(k.style.left=a.x+"px")))}function d(){r=!0,q.parentNode&&q.parentNode.removeChild(q),k.style.cssText=j}function e(){var a=m(f.container),b=f.options.toolbarTopOffset||0;a.top<0&&a.bottom-k.offsetHeight>b?c():d()}var f=this,g=f.getLang();f.setOpt({topOffset:0});var h=f.options.autoFloatEnabled!==!1,i=f.options.topOffset;if(h){var j,k,l,m,n=UE.ui.uiUtils,o=browser.ie&&browser.version<=6,p=browser.quirks,q=document.createElement("div"),r=!0,s=utils.defer(function(){e()},browser.ie?200:100,!0);f.addListener("destroy",function(){domUtils.un(window,["scroll","resize"],e),f.removeListener("keydown",s)}),f.addListener("ready",function(){if(a(f)){if(!f.ui)return;m=n.getClientRect,k=f.ui.getDom("toolbarbox"),l=m(k).top,j=k.style.cssText,q.style.height=k.offsetHeight+"px",o&&b(),domUtils.on(window,["scroll","resize"],e),f.addListener("keydown",s),f.addListener("beforefullscreenchange",function(a,b){b&&d()}),f.addListener("fullscreenchanged",function(a,b){b||e()}),f.addListener("sourcemodechanged",function(a,b){setTimeout(function(){e()},0)}),f.addListener("clearDoc",function(){setTimeout(function(){e()},0)})}})}},UE.plugins.video=function(){function a(a,b,d,e,f,g,h){a=utils.unhtmlForUrl(a),f=utils.unhtml(f),g=utils.unhtml(g),b=parseInt(b,10)||0,d=parseInt(d,10)||0;var i;switch(h){case"image":i="';break;case"embed":i='';break;case"video":var j=a.substr(a.lastIndexOf(".")+1);"ogv"==j&&(j="ogg"),i="'}return i}function b(b,c){utils.each(b.getNodesByTagName(c?"img":"embed video"),function(b){var d=b.getAttr("class");if(d&&-1!=d.indexOf("edui-faked-video")){var e=a(c?b.getAttr("_url"):b.getAttr("src"),b.getAttr("width"),b.getAttr("height"),null,b.getStyle("float")||"",d,c?"embed":"image");b.parentNode.replaceChild(UE.uNode.createElement(e),b)}if(d&&-1!=d.indexOf("edui-upload-video")){var e=a(c?b.getAttr("_url"):b.getAttr("src"),b.getAttr("width"),b.getAttr("height"),null,b.getStyle("float")||"",d,c?"video":"image");b.parentNode.replaceChild(UE.uNode.createElement(e),b)}})}var c=this;c.addOutputRule(function(a){b(a,!0)}),c.addInputRule(function(a){b(a)}),c.commands.insertvideo={execCommand:function(b,d,e){d=utils.isArray(d)?d:[d];for(var f,g,h=[],i="tmpVedio",j=0,k=d.length;k>j;j++)g=d[j],f="upload"==e?"edui-upload-video video-js vjs-default-skin":"edui-faked-video",h.push(a(g.url,g.width||420,g.height||280,i+j,null,f,"image"));c.execCommand("inserthtml",h.join(""),!0);for(var l=this.selection.getRange(),j=0,k=d.length;k>j;j++){var m=this.document.getElementById("tmpVedio"+j);domUtils.removeAttributes(m,"id"),l.selectNode(m).select(),c.execCommand("imagefloat",d[j].align)}},queryCommandState:function(){var a=c.selection.getRange().getClosedNode(),b=a&&("edui-faked-video"==a.className||-1!=a.className.indexOf("edui-upload-video"));return b?1:0}}},function(){function a(a){}var b=UE.UETable=function(a){this.table=a,this.indexTable=[],this.selectedTds=[],this.cellsRange={},this.update(a)};b.removeSelectedClass=function(a){utils.each(a,function(a){domUtils.removeClasses(a,"selectTdClass")})},b.addSelectedClass=function(a){utils.each(a,function(a){domUtils.addClass(a,"selectTdClass")})},b.isEmptyBlock=function(a){var b=new RegExp(domUtils.fillChar,"g");if(a[browser.ie?"innerText":"textContent"].replace(/^\s*$/,"").replace(b,"").length>0)return 0;for(var c in dtd.$isNotEmpty)if(dtd.$isNotEmpty.hasOwnProperty(c)&&a.getElementsByTagName(c).length)return 0;return 1},b.getWidth=function(a){return a?parseInt(domUtils.getComputedStyle(a,"width"),10):0},b.getTableCellAlignState=function(a){!utils.isArray(a)&&(a=[a]);var b={},c=["align","valign"],d=null,e=!0;return utils.each(a,function(a){return utils.each(c,function(c){if(d=a.getAttribute(c),!b[c]&&d)b[c]=d;else if(!b[c]||d!==b[c])return e=!1,!1}),e}),e?b:null},b.getTableItemsByRange=function(a){var b=a.selection.getStart();b&&b.id&&0===b.id.indexOf("_baidu_bookmark_start_")&&b.nextSibling&&(b=b.nextSibling);var c=b&&domUtils.findParentByTagName(b,["td","th"],!0),d=c&&c.parentNode,e=b&&domUtils.findParentByTagName(b,"caption",!0),f=e?e.parentNode:d&&d.parentNode.parentNode;return{cell:c,tr:d,table:f,caption:e}},b.getUETableBySelected=function(a){var c=b.getTableItemsByRange(a).table;return c&&c.ueTable&&c.ueTable.selectedTds.length?c.ueTable:null},b.getDefaultValue=function(a,b){var c,d,e,f,g={thin:"0px",medium:"1px",thick:"2px"};if(b)return h=b.getElementsByTagName("td")[0],f=domUtils.getComputedStyle(b,"border-left-width"),c=parseInt(g[f]||f,10),f=domUtils.getComputedStyle(h,"padding-left"),d=parseInt(g[f]||f,10),f=domUtils.getComputedStyle(h,"border-left-width"),e=parseInt(g[f]||f,10),{tableBorder:c,tdPadding:d,tdBorder:e};b=a.document.createElement("table"),b.insertRow(0).insertCell(0).innerHTML="xxx",a.body.appendChild(b);var h=b.getElementsByTagName("td")[0];return f=domUtils.getComputedStyle(b,"border-left-width"),c=parseInt(g[f]||f,10),f=domUtils.getComputedStyle(h,"padding-left"),d=parseInt(g[f]||f,10),f=domUtils.getComputedStyle(h,"border-left-width"),e=parseInt(g[f]||f,10),domUtils.remove(b),{tableBorder:c,tdPadding:d,tdBorder:e}},b.getUETable=function(a){var c=a.tagName.toLowerCase();return a="td"==c||"th"==c||"caption"==c?domUtils.findParentByTagName(a,"table",!0):a,a.ueTable||(a.ueTable=new b(a)),a.ueTable},b.cloneCell=function(a,b,c){if(!a||utils.isString(a))return this.table.ownerDocument.createElement(a||"td");var d=domUtils.hasClass(a,"selectTdClass");d&&domUtils.removeClasses(a,"selectTdClass");var e=a.cloneNode(!0);return b&&(e.rowSpan=e.colSpan=1),!c&&domUtils.removeAttributes(e,"width height"),!c&&domUtils.removeAttributes(e,"style"),e.style.borderLeftStyle="",e.style.borderTopStyle="",e.style.borderLeftColor=a.style.borderRightColor,e.style.borderLeftWidth=a.style.borderRightWidth,e.style.borderTopColor=a.style.borderBottomColor,e.style.borderTopWidth=a.style.borderBottomWidth,d&&domUtils.addClass(a,"selectTdClass"),e},b.prototype={getMaxRows:function(){for(var a,b=this.table.rows,c=1,d=0;a=b[d];d++){for(var e,f=1,g=0;e=a.cells[g++];)f=Math.max(e.rowSpan||1,f);c=Math.max(f+d,c)}return c},getMaxCols:function(){for(var a,b=this.table.rows,c=0,d={},e=0;a=b[e];e++){for(var f,g=0,h=0;f=a.cells[h++];)if(g+=f.colSpan||1,f.rowSpan&&f.rowSpan>1)for(var i=1;ithis.rowsNum-1)?null:(e=c?h?i.endRowIndex+1:g.rowIndex+g.rowSpan:h?i.beginRowIndex-1:g.rowIndex-1,f=h?i.beginColIndex:g.colIndex,this.getCell(this.indexTable[e][f].rowIndex,this.indexTable[e][f].cellIndex))}catch(j){a(j)}},getSameEndPosCells:function(b,c){try{for(var d="x"===c.toLowerCase(),e=domUtils.getXY(b)[d?"x":"y"]+b["offset"+(d?"Width":"Height")],f=this.table.rows,g=null,h=[],i=0;ie&&d)break;if((b==j||e==l)&&(1==j[d?"colSpan":"rowSpan"]&&h.push(j),d))break}}return h}catch(m){a(m)}},setCellContent:function(a,b){a.innerHTML=b||(browser.ie?domUtils.fillChar:"
              ")},cloneCell:b.cloneCell,getSameStartPosXCells:function(b){try{for(var c,d=domUtils.getXY(b).x+b.offsetWidth,e=this.table.rows,f=[],g=0;gd)break;if(j==d&&1==h.colSpan){f.push(h);break}}}return f}catch(k){a(k)}},update:function(a){this.table=a||this.table,this.selectedTds=[],this.cellsRange={},this.indexTable=[];for(var b=this.table.rows,c=this.getMaxRows(),d=c-b.length,e=this.getMaxCols();d--;)this.table.insertRow(b.length);this.rowsNum=c,this.colsNum=e;for(var f=0,g=b.length;g>f;f++)this.indexTable[f]=new Array(e);for(var h,i=0;h=b[i];i++)for(var j,k=0,l=h.cells;j=l[k];k++){j.rowSpan>c&&(j.rowSpan=c);for(var m=k,n=j.rowSpan||1,o=j.colSpan||1;this.indexTable[i][m];)m++;for(var p=0;n>p;p++)for(var q=0;o>q;q++)this.indexTable[i+p][m+q]={rowIndex:i,cellIndex:k,colIndex:m,rowSpan:n,colSpan:o}}for(p=0;c>p;p++)for(q=0;e>q;q++)void 0===this.indexTable[p][q]&&(h=b[p],j=h.cells[h.cells.length-1],j=j?j.cloneNode(!0):this.table.ownerDocument.createElement("td"),this.setCellContent(j),1!==j.colSpan&&(j.colSpan=1),1!==j.rowSpan&&(j.rowSpan=1),h.appendChild(j),this.indexTable[p][q]={rowIndex:p,cellIndex:j.cellIndex,colIndex:q,rowSpan:1,colSpan:1});var r=domUtils.getElementsByTagName(this.table,"td"),s=[];if(utils.each(r,function(a){domUtils.hasClass(a,"selectTdClass")&&s.push(a)}),s.length){var t=s[0],u=s[s.length-1],v=this.getCellInfo(t),w=this.getCellInfo(u);this.selectedTds=s,this.cellsRange={beginRowIndex:v.rowIndex,beginColIndex:v.colIndex,endRowIndex:w.rowIndex+w.rowSpan-1,endColIndex:w.colIndex+w.colSpan-1}}if(!domUtils.hasClass(this.table.rows[0],"firstRow")){domUtils.addClass(this.table.rows[0],"firstRow");for(var f=1;ff;f++){var g=d[f];if(g.rowIndex===c&&g.cellIndex===b)return g}},getCell:function(a,b){return a0)for(h=b;f>h;h++)g=d.indexTable[a][h],i=g.rowIndex,a>i&&(j=Math.min(i,j));if(fi;i++)g=d.indexTable[i][f],h=g.colIndex+g.colSpan-1,h>f&&(m=Math.max(h,m));if(eh;h++)g=d.indexTable[e][h],i=g.rowIndex+g.rowSpan-1,i>e&&(l=Math.max(i,l));if(b>0)for(i=a;e>i;i++)g=d.indexTable[i][b],h=g.colIndex,b>h&&(k=Math.min(g.colIndex,k));return j!=a||k!=b||l!=e||m!=f?c(j,k,l,m):{beginRowIndex:a,beginColIndex:b,endRowIndex:e,endColIndex:f}}try{var d=this,e=d.getCellInfo(a);if(a===b)return{beginRowIndex:e.rowIndex,beginColIndex:e.colIndex,endRowIndex:e.rowIndex+e.rowSpan-1,endColIndex:e.colIndex+e.colSpan-1};var f=d.getCellInfo(b),g=Math.min(e.rowIndex,f.rowIndex),h=Math.min(e.colIndex,f.colIndex),i=Math.max(e.rowIndex+e.rowSpan-1,f.rowIndex+f.rowSpan-1),j=Math.max(e.colIndex+e.colSpan-1,f.colIndex+f.colSpan-1);return c(g,h,i,j)}catch(k){}},getCells:function(a){this.clearSelected();for(var b,c,d,e=a.beginRowIndex,f=a.beginColIndex,g=a.endRowIndex,h=a.endColIndex,i={},j=[],k=e;g>=k;k++)for(var l=f;h>=l;l++){b=this.indexTable[k][l],c=b.rowIndex,d=b.colIndex;var m=c+"|"+d;if(!i[m]){if(i[m]=1,k>c||l>d||c+b.rowSpan-1>g||d+b.colSpan-1>h)return null;j.push(this.getCell(c,b.cellIndex))}}return j},clearSelected:function(){b.removeSelectedClass(this.selectedTds),this.selectedTds=[],this.cellsRange={}},setSelected:function(a){var c=this.getCells(a);b.addSelectedClass(c),this.selectedTds=c,this.cellsRange=a},isFullRow:function(){var a=this.cellsRange;return a.endColIndex-a.beginColIndex+1==this.colsNum},isFullCol:function(){var a=this.cellsRange,b=this.table,c=b.getElementsByTagName("th"),d=a.endRowIndex-a.beginRowIndex+1;return c.length?d==this.rowsNum||d==this.rowsNum-1:d==this.rowsNum},getNextCell:function(b,c,d){try{var e,f,g=this.getCellInfo(b),h=this.selectedTds.length&&!d,i=this.cellsRange;return!c&&0==g.rowIndex||c&&(h?i.endRowIndex==this.rowsNum-1:g.rowIndex+g.rowSpan>this.rowsNum-1)?null:(e=c?h?i.endRowIndex+1:g.rowIndex+g.rowSpan:h?i.beginRowIndex-1:g.rowIndex-1,f=h?i.beginColIndex:g.colIndex,this.getCell(this.indexTable[e][f].rowIndex,this.indexTable[e][f].cellIndex))}catch(j){a(j)}},getPreviewCell:function(b,c){try{var d,e,f=this.getCellInfo(b),g=this.selectedTds.length,h=this.cellsRange;return!c&&(g?!h.beginColIndex:!f.colIndex)||c&&(g?h.endColIndex==this.colsNum-1:f.rowIndex>this.colsNum-1)?null:(d=c?g?h.beginRowIndex:f.rowIndex<1?0:f.rowIndex-1:g?h.beginRowIndex:f.rowIndex,e=c?g?h.endColIndex+1:f.colIndex:g?h.beginColIndex-1:f.colIndex<1?0:f.colIndex-1,this.getCell(this.indexTable[d][e].rowIndex,this.indexTable[d][e].cellIndex))}catch(i){a(i)}},moveContent:function(a,c){if(!b.isEmptyBlock(c)){if(b.isEmptyBlock(a))return void(a.innerHTML=c.innerHTML);var d=a.lastChild;for(3!=d.nodeType&&dtd.$block[d.tagName]||a.appendChild(a.ownerDocument.createElement("br"));d=c.firstChild;)a.appendChild(d)}},mergeRight:function(a){var b=this.getCellInfo(a),c=b.colIndex+b.colSpan,d=this.indexTable[b.rowIndex][c],e=this.getCell(d.rowIndex,d.cellIndex);a.colSpan=b.colSpan+d.colSpan,a.removeAttribute("width"),this.moveContent(a,e),this.deleteCell(e,d.rowIndex),this.update()},mergeDown:function(a){var b=this.getCellInfo(a),c=b.rowIndex+b.rowSpan,d=this.indexTable[c][b.colIndex],e=this.getCell(d.rowIndex,d.cellIndex);a.rowSpan=b.rowSpan+d.rowSpan,a.removeAttribute("height"),this.moveContent(a,e),this.deleteCell(e,d.rowIndex),this.update()},mergeRange:function(){var a=this.cellsRange,b=this.getCell(a.beginRowIndex,this.indexTable[a.beginRowIndex][a.beginColIndex].cellIndex);if("TH"==b.tagName&&a.endRowIndex!==a.beginRowIndex){var c=this.indexTable,d=this.getCellInfo(b);b=this.getCell(1,c[1][d.colIndex].cellIndex),a=this.getCellsRange(b,this.getCell(c[this.rowsNum-1][d.colIndex].rowIndex,c[this.rowsNum-1][d.colIndex].cellIndex))}for(var e,f=this.getCells(a),g=0;e=f[g++];)e!==b&&(this.moveContent(b,e),this.deleteCell(e));if(b.rowSpan=a.endRowIndex-a.beginRowIndex+1,b.rowSpan>1&&b.removeAttribute("height"),b.colSpan=a.endColIndex-a.beginColIndex+1,b.colSpan>1&&b.removeAttribute("width"),b.rowSpan==this.rowsNum&&1!=b.colSpan&&(b.colSpan=1),b.colSpan==this.colsNum&&1!=b.rowSpan){var h=b.parentNode.rowIndex;if(this.table.deleteRow)for(var g=h+1,i=h+1,j=b.rowSpan;j>g;g++)this.table.deleteRow(i);else for(var g=0,j=b.rowSpan-1;j>g;g++){var k=this.table.rows[h+1];k.parentNode.removeChild(k)}b.rowSpan=1}this.update()},insertRow:function(a,b){function c(a,b,c){if(0==a){var d=c.nextSibling||c.previousSibling,e=d.cells[a];"TH"==e.tagName&&(e=b.ownerDocument.createElement("th"),e.appendChild(b.firstChild),c.insertBefore(e,b),domUtils.remove(b))}else if("TH"==b.tagName){var f=b.ownerDocument.createElement("td");f.appendChild(b.firstChild),c.insertBefore(f,b),domUtils.remove(b)}}var d,e=this.colsNum,f=this.table,g=f.insertRow(a),h="string"==typeof b&&"TH"==b.toUpperCase();if(0==a||a==this.rowsNum)for(var i=0;e>i;i++)d=this.cloneCell(b,!0),this.setCellContent(d),d.getAttribute("vAlign")&&d.setAttribute("vAlign",d.getAttribute("vAlign")),g.appendChild(d),h||c(i,d,g);else{var j=this.indexTable[a];for(i=0;e>i;i++){var k=j[i];k.rowIndexf;){var g=c[f],h=this.getCell(g.rowIndex,g.cellIndex);if(h.rowSpan>1&&g.rowIndex==a){var i=h.cloneNode(!0);i.rowSpan=h.rowSpan-1,i.innerHTML="",h.rowSpan=1;var j,k=a+1,l=this.table.rows[k],m=this.getPreviewMergedCellsNum(k,f)-e;f>m?(j=f-m-1,domUtils.insertAfter(l.cells[j],i)):l.cells.length&&l.insertBefore(i,l.cells[0]),e+=1}f+=h.colSpan||1}var n=[],o={};for(f=0;d>f;f++){var p=c[f].rowIndex,q=c[f].cellIndex,r=p+"_"+q;o[r]||(o[r]=1,h=this.getCell(p,q),n.push(h))}var s=[];utils.each(n,function(a){1==a.rowSpan?a.parentNode.removeChild(a):s.push(a)}),utils.each(s,function(a){a.rowSpan--}),b.parentNode.removeChild(b),this.update()},insertCol:function(a,b,c){function d(a,b,c){if(0==a){var d=b.nextSibling||b.previousSibling;"TH"==d.tagName&&(d=b.ownerDocument.createElement("th"),d.appendChild(b.firstChild),c.insertBefore(d,b),domUtils.remove(b))}else if("TH"==b.tagName){var e=b.ownerDocument.createElement("td");e.appendChild(b.firstChild),c.insertBefore(e,b),domUtils.remove(b)}}var e,f,g,h=this.rowsNum,i=0,j=parseInt((this.table.offsetWidth-20*(this.colsNum+1)-(this.colsNum+1))/(this.colsNum+1),10),k="string"==typeof b&&"TH"==b.toUpperCase();if(0==a||a==this.colsNum)for(;h>i;i++)e=this.table.rows[i],g=e.cells[0==a?a:e.cells.length],f=this.cloneCell(b,!0),this.setCellContent(f),f.setAttribute("vAlign",f.getAttribute("vAlign")),g&&f.setAttribute("width",g.getAttribute("width")),a?domUtils.insertAfter(e.cells[e.cells.length-1],f):e.insertBefore(f,e.cells[0]),k||d(i,f,e);else for(;h>i;i++){var l=this.indexTable[i][a];l.colIndexh;){var i=b[h],j=i[a],k=j.rowIndex+"_"+j.colIndex;if(!g[k]){g[k]=1;var l=this.getCell(j.rowIndex,j.cellIndex);e||(e=l&&parseInt(l.offsetWidth/l.colSpan,10).toFixed(0)),l.colSpan>1?l.colSpan--:c[h].deleteCell(j.cellIndex),h+=j.rowSpan||1}}this.table.setAttribute("width",d-e),this.update()},splitToCells:function(a){var b=this,c=this.splitToRows(a);utils.each(c,function(a){b.splitToCols(a)})},splitToRows:function(a){var b=this.getCellInfo(a),c=b.rowIndex,d=b.colIndex,e=[];a.rowSpan=1,e.push(a);for(var f=c,g=c+b.rowSpan;g>f;f++)if(f!=c){var h=this.table.rows[f],i=h.insertCell(d-this.getPreviewMergedCellsNum(f,d));i.colSpan=b.colSpan,this.setCellContent(i),i.setAttribute("vAlign",a.getAttribute("vAlign")),i.setAttribute("align",a.getAttribute("align")),a.style.cssText&&(i.style.cssText=a.style.cssText),e.push(i)}return this.update(),e},getPreviewMergedCellsNum:function(a,b){for(var c=this.indexTable[a],d=0,e=0;b>e;){var f=c[e].colSpan,g=c[e].rowIndex;d+=f-(g==a?1:0),e+=f}return d},splitToCols:function(a){var b=(a.offsetWidth/a.colSpan-22).toFixed(0),c=this.getCellInfo(a),d=c.rowIndex,e=c.colIndex,f=[];a.colSpan=1,a.setAttribute("width",b),f.push(a);for(var g=e,h=e+c.colSpan;h>g;g++)if(g!=e){var i=this.table.rows[d],j=i.insertCell(this.indexTable[d][g].cellIndex+1);if(j.rowSpan=c.rowSpan,this.setCellContent(j),j.setAttribute("vAlign",a.getAttribute("vAlign")),j.setAttribute("align",a.getAttribute("align")),j.setAttribute("width",b),a.style.cssText&&(j.style.cssText=a.style.cssText),"TH"==a.tagName){var k=a.ownerDocument.createElement("th");k.appendChild(j.firstChild),k.setAttribute("vAlign",a.getAttribute("vAlign")),k.rowSpan=j.rowSpan,i.insertBefore(k,j),domUtils.remove(j)}f.push(j)}return this.update(),f},isLastCell:function(a,b,c){b=b||this.rowsNum,c=c||this.colsNum;var d=this.getCellInfo(a);return d.rowIndex+d.rowSpan==b&&d.colIndex+d.colSpan==c},getLastCell:function(a){a=a||this.table.getElementsByTagName("td");var b,c=(this.getCellInfo(a[0]),this),d=a[0],e=d.parentNode,f=0,g=0;return utils.each(a,function(a){a.parentNode==e&&(g+=a.colSpan||1),f+=a.rowSpan*a.colSpan||1}),b=f/g,utils.each(a,function(a){return c.isLastCell(a,b,g)?(d=a,!1):void 0}),d},selectRow:function(a){var b=this.indexTable[a],c=this.getCell(b[0].rowIndex,b[0].cellIndex),d=this.getCell(b[this.colsNum-1].rowIndex,b[this.colsNum-1].cellIndex),e=this.getCellsRange(c,d);this.setSelected(e)},selectTable:function(){var a=this.table.getElementsByTagName("td"),b=this.getCellsRange(a[0],a[a.length-1]);this.setSelected(b)},setBackground:function(a,b){if("string"==typeof b)utils.each(a,function(a){a.style.backgroundColor=b});else if("object"==typeof b){b=utils.extend({repeat:!0,colorList:["#ddd","#fff"]},b);for(var c,d=this.getCellInfo(a[0]).rowIndex,e=0,f=b.colorList,g=function(a,b,c){return a[b]?a[b]:c?a[b%a.length]:""},h=0;c=a[h++];){var i=this.getCellInfo(c);c.style.backgroundColor=g(f,d+e==i.rowIndex?e:++e,b.repeat)}}},removeBackground:function(a){utils.each(a,function(a){a.style.backgroundColor=""})}}}(),function(){function a(a,c){var d=domUtils.getElementsByTagName(a,"td th");utils.each(d,function(a){a.removeAttribute("width")}),a.setAttribute("width",b(c,!0,g(c,a)));var e=[];setTimeout(function(){utils.each(d,function(a){1==a.colSpan&&e.push(a.offsetWidth)}),utils.each(d,function(a,b){1==a.colSpan&&a.setAttribute("width",e[b]+"")})},0)}function b(a,b,c){var d=a.body;return d.offsetWidth-(b?2*parseInt(domUtils.getComputedStyle(d,"margin-left"),10):0)-2*c.tableBorder-(a.options.offsetWidth||0)}function c(a){var b=e(a).cell;if(b){var c=h(b);return c.selectedTds.length?c.selectedTds:[b]}return[]}var d=UE.UETable,e=function(a){return d.getTableItemsByRange(a)},f=function(a){return d.getUETableBySelected(a)},g=function(a,b){return d.getDefaultValue(a,b)},h=function(a){return d.getUETable(a)};UE.commands.inserttable={queryCommandState:function(){return e(this).table?-1:0},execCommand:function(a,b){function c(a,b){for(var c=[],d=a.numRows,e=a.numCols,f=0;d>f;f++){c.push("");for(var g=0;e>g;g++)c.push('
              ");c.push("")}return"
              '+(browser.ie&&browser.version<11?domUtils.fillChar:"
              ")+"
              "+c.join("")+"
              "}b||(b=utils.extend({},{numCols:this.options.defaultCols,numRows:this.options.defaultRows,tdvalign:this.options.tdvalign}));var d=this,e=this.selection.getRange(),f=e.startContainer,h=domUtils.findParent(f,function(a){return domUtils.isBlockElm(a)},!0)||d.body,i=g(d),j=h.offsetWidth,k=Math.floor(j/b.numCols-2*i.tdPadding-i.tdBorder);!b.tdvalign&&(b.tdvalign=d.options.tdvalign),d.execCommand("inserthtml",c(b,k))}},UE.commands.insertparagraphbeforetable={queryCommandState:function(){return e(this).cell?0:-1},execCommand:function(){var a=e(this).table;if(a){var b=this.document.createElement("p");b.innerHTML=browser.ie?" ":"
              ",a.parentNode.insertBefore(b,a),this.selection.getRange().setStart(b,0).setCursor()}}},UE.commands.deletetable={queryCommandState:function(){var a=this.selection.getRange();return domUtils.findParentByTagName(a.startContainer,"table",!0)?0:-1},execCommand:function(a,b){var c=this.selection.getRange();if(b=b||domUtils.findParentByTagName(c.startContainer,"table",!0)){var d=b.nextSibling;d||(d=domUtils.createElement(this.document,"p",{innerHTML:browser.ie?domUtils.fillChar:"
              "}),b.parentNode.insertBefore(d,b)),domUtils.remove(b),c=this.selection.getRange(),3==d.nodeType?c.setStartBefore(d):c.setStart(d,0),c.setCursor(!1,!0),this.fireEvent("tablehasdeleted")}}},UE.commands.cellalign={queryCommandState:function(){return c(this).length?0:-1},execCommand:function(a,b){var d=c(this);if(d.length)for(var e,f=0;e=d[f++];)e.setAttribute("align",b)}},UE.commands.cellvalign={queryCommandState:function(){return c(this).length?0:-1},execCommand:function(a,b){var d=c(this);if(d.length)for(var e,f=0;e=d[f++];)e.setAttribute("vAlign",b)}},UE.commands.insertcaption={queryCommandState:function(){var a=e(this).table;return a&&0==a.getElementsByTagName("caption").length?1:-1},execCommand:function(){var a=e(this).table;if(a){var b=this.document.createElement("caption");b.innerHTML=browser.ie?domUtils.fillChar:"
              ",a.insertBefore(b,a.firstChild);var c=this.selection.getRange();c.setStart(b,0).setCursor()}}},UE.commands.deletecaption={queryCommandState:function(){var a=this.selection.getRange(),b=domUtils.findParentByTagName(a.startContainer,"table");return b?0==b.getElementsByTagName("caption").length?-1:1:-1},execCommand:function(){var a=this.selection.getRange(),b=domUtils.findParentByTagName(a.startContainer,"table");if(b){domUtils.remove(b.getElementsByTagName("caption")[0]);var c=this.selection.getRange();c.setStart(b.rows[0].cells[0],0).setCursor()}}},UE.commands.inserttitle={queryCommandState:function(){var a=e(this).table;if(a){var b=a.rows[0];return"th"!=b.cells[b.cells.length-1].tagName.toLowerCase()?0:-1}return-1},execCommand:function(){var a=e(this).table;a&&h(a).insertRow(0,"th");var b=a.getElementsByTagName("th")[0];this.selection.getRange().setStart(b,0).setCursor(!1,!0)}},UE.commands.deletetitle={queryCommandState:function(){var a=e(this).table;if(a){var b=a.rows[0];return"th"==b.cells[b.cells.length-1].tagName.toLowerCase()?0:-1}return-1},execCommand:function(){var a=e(this).table;a&&domUtils.remove(a.rows[0]);var b=a.getElementsByTagName("td")[0];this.selection.getRange().setStart(b,0).setCursor(!1,!0)}},UE.commands.inserttitlecol={queryCommandState:function(){var a=e(this).table;if(a){var b=a.rows[a.rows.length-1];return b.getElementsByTagName("th").length?-1:0}return-1},execCommand:function(b){var c=e(this).table;c&&h(c).insertCol(0,"th"),a(c,this);var d=c.getElementsByTagName("th")[0];this.selection.getRange().setStart(d,0).setCursor(!1,!0)}},UE.commands.deletetitlecol={queryCommandState:function(){var a=e(this).table;if(a){var b=a.rows[a.rows.length-1];return b.getElementsByTagName("th").length?0:-1}return-1},execCommand:function(){var b=e(this).table;if(b)for(var c=0;c=f.colsNum)return-1;var j=f.indexTable[g.rowIndex][i],k=c.rows[j.rowIndex].cells[j.cellIndex];return k&&d.tagName==k.tagName&&j.rowIndex==g.rowIndex&&j.rowSpan==g.rowSpan?0:-1},execCommand:function(a){var b=this.selection.getRange(),c=b.createBookmark(!0),d=e(this).cell,f=h(d);f.mergeRight(d),b.moveToBookmark(c).select()}},UE.commands.mergedown={queryCommandState:function(a){var b=e(this),c=b.table,d=b.cell;if(!c||!d)return-1;var f=h(c);if(f.selectedTds.length)return-1;var g=f.getCellInfo(d),i=g.rowIndex+g.rowSpan;if(i>=f.rowsNum)return-1;var j=f.indexTable[i][g.colIndex],k=c.rows[j.rowIndex].cells[j.cellIndex];return k&&d.tagName==k.tagName&&j.colIndex==g.colIndex&&j.colSpan==g.colSpan?0:-1},execCommand:function(){var a=this.selection.getRange(),b=a.createBookmark(!0),c=e(this).cell,d=h(c);d.mergeDown(c),a.moveToBookmark(b).select()}},UE.commands.mergecells={queryCommandState:function(){return f(this)?0:-1},execCommand:function(){var a=f(this);if(a&&a.selectedTds.length){var b=a.selectedTds[0];a.mergeRange();var c=this.selection.getRange();domUtils.isEmptyBlock(b)?c.setStart(b,0).collapse(!0):c.selectNodeContents(b),c.select()}}},UE.commands.insertrow={queryCommandState:function(){var a=e(this),b=a.cell;return b&&("TD"==b.tagName||"TH"==b.tagName&&a.tr!==a.table.rows[0])&&h(a.table).rowsNumk;k++)g.insertRow(j.beginRowIndex,d);else g.insertRow(i.rowIndex,d);a.moveToBookmark(b).select(),"enabled"===f.getAttribute("interlaced")&&this.fireEvent("interlacetable",f)}},UE.commands.insertrownext={queryCommandState:function(){var a=e(this),b=a.cell;return b&&"TD"==b.tagName&&h(a.table).rowsNumk;k++)g.insertRow(j.endRowIndex+1,d);else g.insertRow(i.rowIndex+i.rowSpan,d);a.moveToBookmark(b).select(),"enabled"===f.getAttribute("interlaced")&&this.fireEvent("interlacetable",f)}},UE.commands.deleterow={queryCommandState:function(){var a=e(this);return a.cell?0:-1},execCommand:function(){var a=e(this).cell,b=h(a),c=b.cellsRange,d=b.getCellInfo(a),f=b.getVSideCell(a),g=b.getVSideCell(a,!0),i=this.selection.getRange();if(utils.isEmptyObject(c))b.deleteRow(d.rowIndex);else for(var j=c.beginRowIndex;jj;j++)f.insertCol(i.beginColIndex,d);else f.insertCol(g.colIndex,d);b.moveToBookmark(c).select(!0)}}},UE.commands.insertcolnext={queryCommandState:function(){var a=e(this),b=a.cell;return b&&h(a.table).colsNumi;i++)d.insertCol(g.endColIndex+1,c);else d.insertCol(f.colIndex+f.colSpan,c);a.moveToBookmark(b).select()}},UE.commands.deletecol={queryCommandState:function(){var a=e(this);return a.cell?0:-1},execCommand:function(){var a=e(this).cell,b=h(a),c=b.cellsRange,d=b.getCellInfo(a),f=b.getHSideCell(a),g=b.getHSideCell(a,!0);if(utils.isEmptyObject(c))b.deleteCol(d.colIndex);else for(var i=c.beginColIndex;i0?-1:b&&(b.colSpan>1||b.rowSpan>1)?0:-1},execCommand:function(){var a=this.selection.getRange(),b=a.createBookmark(!0),c=e(this).cell,d=h(c);d.splitToCells(c),a.moveToBookmark(b).select()}},UE.commands.splittorows={queryCommandState:function(){var a=e(this),b=a.cell;if(!b)return-1;var c=h(a.table);return c.selectedTds.length>0?-1:b&&b.rowSpan>1?0:-1},execCommand:function(){var a=this.selection.getRange(),b=a.createBookmark(!0),c=e(this).cell,d=h(c);d.splitToRows(c),a.moveToBookmark(b).select()}},UE.commands.splittocols={queryCommandState:function(){var a=e(this),b=a.cell;if(!b)return-1;var c=h(a.table);return c.selectedTds.length>0?-1:b&&b.colSpan>1?0:-1},execCommand:function(){var a=this.selection.getRange(),b=a.createBookmark(!0),c=e(this).cell,d=h(c);d.splitToCols(c),a.moveToBookmark(b).select()}},UE.commands.adaptbytext=UE.commands.adaptbywindow={queryCommandState:function(){return e(this).table?0:-1},execCommand:function(b){var c=e(this),d=c.table;if(d)if("adaptbywindow"==b)a(d,this);else{var f=domUtils.getElementsByTagName(d,"td th");utils.each(f,function(a){a.removeAttribute("width")}),d.removeAttribute("width")}}},UE.commands.averagedistributecol={queryCommandState:function(){var a=f(this);return a&&(a.isFullRow()||a.isFullCol())?0:-1},execCommand:function(a){function b(){var a,b=e.table,c=0,f=0,h=g(d,b);if(e.isFullRow())c=b.offsetWidth,f=e.colsNum;else for(var i,j=e.cellsRange.beginColIndex,k=e.cellsRange.endColIndex,l=j;k>=l;)i=e.selectedTds[l],c+=i.offsetWidth,l+=i.colSpan,f+=1;return a=Math.ceil(c/f)-2*h.tdBorder-2*h.tdPadding}function c(a){utils.each(domUtils.getElementsByTagName(e.table,"th"),function(a){a.setAttribute("width","")});var b=e.isFullRow()?domUtils.getElementsByTagName(e.table,"td"):e.selectedTds;utils.each(b,function(b){1==b.colSpan&&b.setAttribute("width",a)})}var d=this,e=f(d);e&&e.selectedTds.length&&c(b())}},UE.commands.averagedistributerow={queryCommandState:function(){var a=f(this);return a?a.selectedTds&&/th/gi.test(a.selectedTds[0].tagName)?-1:a.isFullRow()||a.isFullCol()?0:-1:-1},execCommand:function(a){function b(){var a,b,c=0,f=e.table,h=g(d,f),i=parseInt(domUtils.getComputedStyle(f.getElementsByTagName("td")[0],"padding-top"));if(e.isFullCol()){var j,k,l=domUtils.getElementsByTagName(f,"caption"),m=domUtils.getElementsByTagName(f,"th");l.length>0&&(j=l[0].offsetHeight),m.length>0&&(k=m[0].offsetHeight),c=f.offsetHeight-(j||0)-(k||0),b=0==m.length?e.rowsNum:e.rowsNum-1}else{for(var n=e.cellsRange.beginRowIndex,o=e.cellsRange.endRowIndex,p=0,q=domUtils.getElementsByTagName(f,"tr"),r=n;o>=r;r++)c+=q[r].offsetHeight,p+=1;b=p}return a=browser.ie&&browser.version<9?Math.ceil(c/b):Math.ceil(c/b)-2*h.tdBorder-2*i}function c(a){var b=e.isFullCol()?domUtils.getElementsByTagName(e.table,"td"):e.selectedTds;utils.each(b,function(b){1==b.rowSpan&&b.setAttribute("height",a)})}var d=this,e=f(d);e&&e.selectedTds.length&&c(b())}},UE.commands.cellalignment={queryCommandState:function(){return e(this).table?0:-1},execCommand:function(a,b){var c=this,d=f(c);if(d)utils.each(d.selectedTds,function(a){domUtils.setAttributes(a,b)});else{var e=c.selection.getStart(),g=e&&domUtils.findParentByTagName(e,["td","th","caption"],!0);/caption/gi.test(g.tagName)?(g.style.textAlign=b.align,g.style.verticalAlign=b.vAlign):domUtils.setAttributes(g,b),c.selection.getRange().setCursor(!0)}},queryCommandValue:function(a){var b=e(this).cell;if(b||(b=c(this)[0]),b){var d=UE.UETable.getUETable(b).selectedTds;return!d.length&&(d=b),UE.UETable.getTableCellAlignState(d)}return null}},UE.commands.tablealignment={queryCommandState:function(){return browser.ie&&browser.version<8?-1:e(this).table?0:-1},execCommand:function(a,b){var c=this,d=c.selection.getStart(),e=d&&domUtils.findParentByTagName(d,["table"],!0);e&&e.setAttribute("align",b)}},UE.commands.edittable={queryCommandState:function(){return e(this).table?0:-1},execCommand:function(a,b){var c=this.selection.getRange(),d=domUtils.findParentByTagName(c.startContainer,"table");if(d){var e=domUtils.getElementsByTagName(d,"td").concat(domUtils.getElementsByTagName(d,"th"),domUtils.getElementsByTagName(d,"caption"));utils.each(e,function(a){a.style.borderColor=b})}}},UE.commands.edittd={queryCommandState:function(){return e(this).table?0:-1},execCommand:function(a,b){var c=this,d=f(c);if(d)utils.each(d.selectedTds,function(a){a.style.backgroundColor=b});else{var e=c.selection.getStart(),g=e&&domUtils.findParentByTagName(e,["td","th","caption"],!0);g&&(g.style.backgroundColor=b)}}},UE.commands.settablebackground={queryCommandState:function(){return c(this).length>1?0:-1},execCommand:function(a,b){var d,e;d=c(this),e=h(d[0]),e.setBackground(d,b)}},UE.commands.cleartablebackground={queryCommandState:function(){var a=c(this);if(!a.length)return-1;for(var b,d=0;b=a[d++];)if(""!==b.style.backgroundColor)return 0;return-1},execCommand:function(){var a=c(this),b=h(a[0]);b.removeBackground(a)}},UE.commands.interlacetable=UE.commands.uninterlacetable={queryCommandState:function(a){var b=e(this).table;if(!b)return-1;var c=b.getAttribute("interlaced");return"interlacetable"==a?"enabled"===c?-1:0:c&&"disabled"!==c?0:-1},execCommand:function(a,b){var c=e(this).table;"interlacetable"==a?(c.setAttribute("interlaced","enabled"),this.fireEvent("interlacetable",c,b)):(c.setAttribute("interlaced","disabled"),this.fireEvent("uninterlacetable",c))}},UE.commands.setbordervisible={queryCommandState:function(a){var b=e(this).table;return b?0:-1},execCommand:function(){var a=e(this).table;utils.each(domUtils.getElementsByTagName(a,"td"),function(a){a.style.borderWidth="1px",a.style.borderStyle="solid"})}}}(),UE.plugins.table=function(){function a(a){}function b(a,b){c(a,"width",!0),c(a,"height",!0)}function c(a,b,c){a.style[b]&&(c&&a.setAttribute(b,parseInt(a.style[b],10)),a.style[b]="")}function d(a){if("TD"==a.tagName||"TH"==a.tagName)return a;var b;return(b=domUtils.findParentByTagName(a,"td",!0)||domUtils.findParentByTagName(a,"th",!0))?b:null}function e(a){var b=new RegExp(domUtils.fillChar,"g");if(a[browser.ie?"innerText":"textContent"].replace(/^\s*$/,"").replace(b,"").length>0)return 0;for(var c in dtd.$isNotEmpty)if(a.getElementsByTagName(c).length)return 0;return 1}function f(a){return a.pageX||a.pageY?{x:a.pageX,y:a.pageY}:{x:a.clientX+N.document.body.scrollLeft-N.document.body.clientLeft,y:a.clientY+N.document.body.scrollTop-N.document.body.clientTop}}function g(b){if(!A())try{var c,e=d(b.target||b.srcElement);if(R&&(N.body.style.webkitUserSelect="none",(Math.abs(V.x-b.clientX)>T||Math.abs(V.y-b.clientY)>T)&&(t(),R=!1,U=0,v(b))),ca&&ha)return U=0,N.body.style.webkitUserSelect="none",N.selection.getNative()[browser.ie9below?"empty":"removeAllRanges"](),c=f(b),m(N,!0,ca,c,e),void("h"==ca?ga.style.left=k(ha,b)+"px":"v"==ca&&(ga.style.top=l(ha,b)+"px"));if(e){if(N.fireEvent("excludetable",e)===!0)return;c=f(b);var g=n(e,c),i=domUtils.findParentByTagName(e,"table",!0);if(j(i,e,b,!0)){if(N.fireEvent("excludetable",i)===!0)return;N.body.style.cursor="url("+N.options.cursorpath+"h.png),pointer"}else if(j(i,e,b)){if(N.fireEvent("excludetable",i)===!0)return;N.body.style.cursor="url("+N.options.cursorpath+"v.png),pointer"}else{N.body.style.cursor="text";/\d/.test(g)&&(g=g.replace(/\d/,""),e=Y(e).getPreviewCell(e,"v"==g)),m(N,e?!!g:!1,e?g:"",c,e)}}else h(!1,i,N)}catch(o){a(o)}}function h(a,b,c){if(a)i(b,c);else{if(fa)return;la=setTimeout(function(){!fa&&ea&&ea.parentNode&&ea.parentNode.removeChild(ea)},2e3)}}function i(a,b){function c(c,d){clearTimeout(g),g=setTimeout(function(){b.fireEvent("tableClicked",a,d)},300)}function d(c){clearTimeout(g);var d=Y(a),e=a.rows[0].cells[0],f=d.getLastCell(),h=d.getCellsRange(e,f);b.selection.getRange().setStart(e,0).setCursor(!1,!0),d.setSelected(h)}var e=domUtils.getXY(a),f=a.ownerDocument;if(ea&&ea.parentNode)return ea;ea=f.createElement("div"),ea.contentEditable=!1,ea.innerHTML="",ea.style.cssText="width:15px;height:15px;background-image:url("+b.options.UEDITOR_HOME_URL+"dialogs/table/dragicon.png);position: absolute;cursor:move;top:"+(e.y-15)+"px;left:"+e.x+"px;",domUtils.unSelectable(ea),ea.onmouseover=function(a){fa=!0},ea.onmouseout=function(a){fa=!1},domUtils.on(ea,"click",function(a,b){c(b,this)}),domUtils.on(ea,"dblclick",function(a,b){d(b)}),domUtils.on(ea,"dragstart",function(a,b){domUtils.preventDefault(b)});var g;f.body.appendChild(ea)}function j(a,b,c,d){var e=f(c),g=n(b,e);if(d){var h=a.getElementsByTagName("caption")[0],i=h?h.offsetHeight:0;return"v1"==g&&e.y-domUtils.getXY(a).y-i<8}return"h1"==g&&e.x-domUtils.getXY(a).x<8}function k(a,b){var c=Y(a);if(c){var d=c.getSameEndPosCells(a,"x")[0],e=c.getSameStartPosXCells(a)[0],g=f(b).x,h=(d?domUtils.getXY(d).x:domUtils.getXY(c.table).x)+20,i=e?domUtils.getXY(e).x+e.offsetWidth-20:N.body.offsetWidth+5||parseInt(domUtils.getComputedStyle(N.body,"width"),10);return h+=Q,i-=Q,h>g?h:g>i?i:g}}function l(b,c){try{var d=domUtils.getXY(b).y,e=f(c).y;return d>e?d:e}catch(g){a(g)}}function m(b,c,d,e,f){try{b.body.style.cursor="h"==d?"col-resize":"v"==d?"row-resize":"text",browser.ie&&(!d||ia||Z(b)?I(b):(H(b,b.document),J(d,f))),da=c}catch(g){a(g)}}function n(a,b){var c=domUtils.getXY(a);return c?c.x+a.offsetWidth-b.xk[c]?(a=!1,!1):void l.push(d)});var b=a?l:k;utils.each(i,function(a,c){a.width=b[c]-G()})},0)}}}}function q(a){if(_(domUtils.getElementsByTagName(N.body,"td th")),utils.each(N.document.getElementsByTagName("table"),function(a){a.ueTable=null}),aa=M(N,a)){var b=domUtils.findParentByTagName(aa,"table",!0);ut=Y(b),ut&&ut.clearSelected(),da?r(a):(N.document.body.style.webkitUserSelect="",ia=!0,N.addListener("mouseover",x))}}function r(a){browser.ie&&(a=u(a)),t(),R=!0,O=setTimeout(function(){v(a)},W)}function s(a,b){for(var c=[],d=null,e=0,f=a.length;f>e;e++)d=a[e][b],d&&c.push(d);return c}function t(){O&&clearTimeout(O),O=null}function u(a){var b=["pageX","pageY","clientX","clientY","srcElement","target"],c={};if(a)for(var d,e,f=0;d=b[f];f++)e=a[d],e&&(c[d]=e);return c}function v(a){if(R=!1,aa=a.target||a.srcElement){var b=n(aa,f(a));/\d/.test(b)&&(b=b.replace(/\d/,""),aa=Y(aa).getPreviewCell(aa,"v"==b)),I(N),H(N,N.document),N.fireEvent("saveScene"),J(b,aa),ia=!0,ca=b,ha=aa}}function w(a,b){if(!A()){if(t(),R=!1,da&&(U=++U%3,V={x:b.clientX,y:b.clientY},P=setTimeout(function(){U>0&&U--},W),2===U))return U=0,void p(b);if(2!=b.button){var c=this,d=c.selection.getRange(),e=domUtils.findParentByTagName(d.startContainer,"table",!0),f=domUtils.findParentByTagName(d.endContainer,"table",!0);if((e||f)&&(e===f?(e=domUtils.findParentByTagName(d.startContainer,["td","th","caption"],!0),f=domUtils.findParentByTagName(d.endContainer,["td","th","caption"],!0),e!==f&&c.selection.clearRange()):c.selection.clearRange()),ia=!1,c.document.body.style.webkitUserSelect="",ca&&ha&&(c.selection.getNative()[browser.ie9below?"empty":"removeAllRanges"](),U=0,ga=c.document.getElementById("ue_tableDragLine"))){var g=domUtils.getXY(ha),h=domUtils.getXY(ga);switch(ca){case"h":z(ha,h.x-g.x);break;case"v":B(ha,h.y-g.y-ha.offsetHeight)}return ca="",ha=null,I(c),void c.fireEvent("saveScene")}if(aa){var i=Y(aa),j=i?i.selectedTds[0]:null;if(j)d=new dom.Range(c.document),domUtils.isEmptyBlock(j)?d.setStart(j,0).setCursor(!1,!0):d.selectNodeContents(j).shrinkBoundary().setCursor(!1,!0);else if(d=c.selection.getRange().shrinkBoundary(),!d.collapsed){var e=domUtils.findParentByTagName(d.startContainer,["td","th"],!0),f=domUtils.findParentByTagName(d.endContainer,["td","th"],!0);(e&&!f||!e&&f||e&&f&&e!==f)&&d.setCursor(!1,!0)}aa=null,c.removeListener("mouseover",x)}else{var k=domUtils.findParentByTagName(b.target||b.srcElement,"td",!0);if(k||(k=domUtils.findParentByTagName(b.target||b.srcElement,"th",!0)),k&&("TD"==k.tagName||"TH"==k.tagName)){if(c.fireEvent("excludetable",k)===!0)return;d=new dom.Range(c.document),d.setStart(k,0).setCursor(!1,!0)}}c._selectionChange(250,b)}}}function x(a,b){if(!A()){var c=this,d=b.target||b.srcElement;if(ba=domUtils.findParentByTagName(d,"td",!0)||domUtils.findParentByTagName(d,"th",!0),aa&&ba&&("TD"==aa.tagName&&"TD"==ba.tagName||"TH"==aa.tagName&&"TH"==ba.tagName)&&domUtils.findParentByTagName(aa,"table")==domUtils.findParentByTagName(ba,"table")){var e=Y(ba);if(aa!=ba){c.document.body.style.webkitUserSelect="none",c.selection.getNative()[browser.ie9below?"empty":"removeAllRanges"]();var f=e.getCellsRange(aa,ba);e.setSelected(f)}else c.document.body.style.webkitUserSelect="",e.clearSelected()}b.preventDefault?b.preventDefault():b.returnValue=!1}}function y(a,b,c){var d=parseInt(domUtils.getComputedStyle(a,"line-height"),10),e=c+b;b=d>e?d:e,a.style.height&&(a.style.height=""),1==a.rowSpan?a.setAttribute("height",b):a.removeAttribute&&a.removeAttribute("height")}function z(a,b){var c=Y(a);if(c){var d=c.table,e=C(a,d);if(d.style.width="",d.removeAttribute("width"),b=D(b,a,e),a.nextSibling){utils.each(e,function(a){a.left.width=+a.left.width+b,a.right&&(a.right.width=+a.right.width-b)})}else utils.each(e,function(a){a.left.width-=-b})}}function A(){return"false"===N.body.contentEditable}function B(a,b){if(!(Math.abs(b)<10)){var c=Y(a);if(c)for(var d,e=c.getSameEndPosCells(a,"y"),f=e[0]?e[0].offsetHeight:0,g=0;d=e[g++];)y(d,b,f)}}function C(a,b,c){if(b||(b=domUtils.findParentByTagName(a,"table")),!b)return null;for(var d=(domUtils.getNodeIndex(a),a),e=b.rows,f=0;d;)1===d.nodeType&&(f+=d.colSpan||1),d=d.previousSibling;d=null;var g=[];return utils.each(e,function(a){var b=a.cells,d=0;utils.each(b,function(a){return d+=a.colSpan||1,d===f?(g.push({left:a,right:a.nextSibling||null}),!1):d>f?(c&&g.push({left:a}),!1):void 0})}),g}function D(a,b,c){if(a-=G(),0>a)return 0;a-=E(b);var d=0>a?"left":"right";return a=Math.abs(a),utils.each(c,function(b){var c=b[d];c&&(a=Math.min(a,E(c)-Q))}),a=0>a?0:a,"left"===d?-a:a}function E(a){var b=0,b=a.offsetWidth-G();a.nextSibling||(b-=F(a)),b=0>b?0:b;try{a.width=b}catch(c){}return b}function F(a){if(tab=domUtils.findParentByTagName(a,"table",!1),void 0===tab.offsetVal){var b=a.previousSibling;b?tab.offsetVal=a.offsetWidth-b.offsetWidth===X.borderWidth?X.borderWidth:0:tab.offsetVal=0}return tab.offsetVal}function G(){if(void 0===X.tabcellSpace){var a=N.document.createElement("table"),b=N.document.createElement("tbody"),c=N.document.createElement("tr"),d=N.document.createElement("td"),e=null;d.style.cssText="border: 0;",d.width=1,c.appendChild(d),c.appendChild(e=d.cloneNode(!1)),b.appendChild(c),a.appendChild(b),a.style.cssText="visibility: hidden;",N.body.appendChild(a),X.paddingSpace=d.offsetWidth-1;var f=a.offsetWidth;d.style.cssText="",e.style.cssText="",X.borderWidth=(a.offsetWidth-f)/3,X.tabcellSpace=X.paddingSpace+X.borderWidth,N.body.removeChild(a)}return G=function(){return X.tabcellSpace},X.tabcellSpace}function H(a,b){ia||(ga=a.document.createElement("div"),domUtils.setAttributes(ga,{id:"ue_tableDragLine",unselectable:"on",contenteditable:!1,onresizestart:"return false",ondragstart:"return false",onselectstart:"return false",style:"background-color:blue;position:absolute;padding:0;margin:0;background-image:none;border:0px none;opacity:0;filter:alpha(opacity=0)"}),a.body.appendChild(ga))}function I(a){if(!ia)for(var b;b=a.document.getElementById("ue_tableDragLine");)domUtils.remove(b)}function J(a,b){if(b){var c,d=domUtils.findParentByTagName(b,"table"),e=d.getElementsByTagName("caption"),f=d.offsetWidth,g=d.offsetHeight-(e.length>0?e[0].offsetHeight:0),h=domUtils.getXY(d),i=domUtils.getXY(b);switch(a){case"h":c="height:"+g+"px;top:"+(h.y+(e.length>0?e[0].offsetHeight:0))+"px;left:"+(i.x+b.offsetWidth),ga.style.cssText=c+"px;position: absolute;display:block;background-color:blue;width:1px;border:0; color:blue;opacity:.3;filter:alpha(opacity=30)";break;case"v":c="width:"+f+"px;left:"+h.x+"px;top:"+(i.y+b.offsetHeight),ga.style.cssText=c+"px;overflow:hidden;position: absolute;display:block;background-color:blue;height:1px;border:0;color:blue;opacity:.2;filter:alpha(opacity=20)"}}}function K(a,b){for(var c,d,e=domUtils.getElementsByTagName(a.body,"table"),f=0;d=e[f++];){var g=domUtils.getElementsByTagName(d,"td");g[0]&&(b?(c=g[0].style.borderColor.replace(/\s/g,""),/(#ffffff)|(rgb\(255,255,255\))/gi.test(c)&&domUtils.addClass(d,"noBorderTable")):domUtils.removeClasses(d,"noBorderTable"))}}function L(a,b,c){var d=a.body;return d.offsetWidth-(b?2*parseInt(domUtils.getComputedStyle(d,"margin-left"),10):0)-2*c.tableBorder-(a.options.offsetWidth||0)}function M(a,b){var c=domUtils.findParentByTagName(b.target||b.srcElement,["td","th"],!0),d=null;if(!c)return null;if(d=n(c,f(b)),!c)return null;if("h1"===d&&c.previousSibling){var e=domUtils.getXY(c),g=c.offsetWidth;Math.abs(e.x+g-b.clientX)>g/3&&(c=c.previousSibling)}else if("v1"===d&&c.parentNode.previousSibling){var e=domUtils.getXY(c),h=c.offsetHeight;Math.abs(e.y+h-b.clientY)>h/3&&(c=c.parentNode.previousSibling.firstChild)}return c&&a.fireEvent("excludetable",c)!==!0?c:null}var N=this,O=null,P=null,Q=5,R=!1,S=5,T=10,U=0,V=null,W=360,X=UE.UETable,Y=function(a){return X.getUETable(a)},Z=function(a){return X.getUETableBySelected(a)},$=function(a,b){return X.getDefaultValue(a,b)},_=function(a){return X.removeSelectedClass(a)};N.ready(function(){var a=this,b=a.selection.getText;a.selection.getText=function(){var c=Z(a);if(c){var d="";return utils.each(c.selectedTds,function(a){d+=a[browser.ie?"innerText":"textContent"]}),d}return b.call(a.selection)}});var aa=null,ba=null,ca="",da=!1,ea=null,fa=!1,ga=null,ha=null,ia=!1,ja=!0;N.setOpt({maxColNum:20,maxRowNum:100,defaultCols:5,defaultRows:5,tdvalign:"top",cursorpath:N.options.UEDITOR_HOME_URL+"themes/default/images/cursor_",tableDragable:!1,classList:["ue-table-interlace-color-single","ue-table-interlace-color-double"]}),N.getUETable=Y;var ka={deletetable:1,inserttable:1,cellvalign:1,insertcaption:1,deletecaption:1,inserttitle:1,deletetitle:1,mergeright:1,mergedown:1,mergecells:1,insertrow:1,insertrownext:1,deleterow:1,insertcol:1,insertcolnext:1,deletecol:1,splittocells:1,splittorows:1,splittocols:1,adaptbytext:1,adaptbywindow:1,adaptbycustomer:1,insertparagraph:1,insertparagraphbeforetable:1,averagedistributecol:1,averagedistributerow:1};N.ready(function(){utils.cssRule("table",".selectTdClass{background-color:#edf5fa !important}table.noBorderTable td,table.noBorderTable th,table.noBorderTable caption{border:1px dashed #ddd !important}table{margin-bottom:10px;border-collapse:collapse;display:table;}td,th{padding: 5px 10px;border: 1px solid #DDD;}caption{border:1px dashed #DDD;border-bottom:0;padding:3px;text-align:center;}th{border-top:1px solid #BBB;background-color:#F7F7F7;}table tr.firstRow th{border-top-width:2px;}.ue-table-interlace-color-single{ background-color: #fcfcfc; } .ue-table-interlace-color-double{ background-color: #f7faff; }td p{margin:0;padding:0;}",N.document);var a,c,f;N.addListener("keydown",function(b,d){var g=this,h=d.keyCode||d.which;if(8==h){var i=Z(g);i&&i.selectedTds.length&&(i.isFullCol()?g.execCommand("deletecol"):i.isFullRow()?g.execCommand("deleterow"):g.fireEvent("delcells"),domUtils.preventDefault(d));var j=domUtils.findParentByTagName(g.selection.getStart(),"caption",!0),k=g.selection.getRange();if(k.collapsed&&j&&e(j)){g.fireEvent("saveScene");var l=j.parentNode;domUtils.remove(j),l&&k.setStart(l.rows[0].cells[0],0).setCursor(!1,!0),g.fireEvent("saveScene")}}if(46==h&&(i=Z(g))){g.fireEvent("saveScene");for(var m,n=0;m=i.selectedTds[n++];)domUtils.fillNode(g.document,m);g.fireEvent("saveScene"),domUtils.preventDefault(d)}if(13==h){var o=g.selection.getRange(),j=domUtils.findParentByTagName(o.startContainer,"caption",!0);if(j){var l=domUtils.findParentByTagName(j,"table");return o.collapsed?j&&o.setStart(l.rows[0].cells[0],0).setCursor(!1,!0):(o.deleteContents(),g.fireEvent("saveScene")),void domUtils.preventDefault(d)}if(o.collapsed){var l=domUtils.findParentByTagName(o.startContainer,"table");if(l){var p=l.rows[0].cells[0],q=domUtils.findParentByTagName(g.selection.getStart(),["td","th"],!0),r=l.previousSibling;if(p===q&&(!r||1==r.nodeType&&"TABLE"==r.tagName)&&domUtils.isStartInblock(o)){var s=domUtils.findParent(g.selection.getStart(),function(a){return domUtils.isBlockElm(a)},!0);s&&(/t(h|d)/i.test(s.tagName)||s===q.firstChild)&&(g.execCommand("insertparagraphbeforetable"),domUtils.preventDefault(d))}}}}if((d.ctrlKey||d.metaKey)&&"67"==d.keyCode){a=null;var i=Z(g);if(i){var t=i.selectedTds;c=i.isFullCol(),f=i.isFullRow(),a=[[i.cloneCell(t[0],null,!0)]];for(var m,n=1;m=t[n];n++)m.parentNode!==t[n-1].parentNode?a.push([i.cloneCell(m,null,!0)]):a[a.length-1].push(i.cloneCell(m,null,!0))}}}),N.addListener("tablehasdeleted",function(){m(this,!1,"",null),ea&&domUtils.remove(ea)}),N.addListener("beforepaste",function(d,g){var h=this,i=h.selection.getRange();if(domUtils.findParentByTagName(i.startContainer,"caption",!0)){var j=h.document.createElement("div");return j.innerHTML=g.html,void(g.html=j[browser.ie9below?"innerText":"textContent"])}var k=Z(h);if(a){h.fireEvent("saveScene");var l,m,i=h.selection.getRange(),n=domUtils.findParentByTagName(i.startContainer,["td","th"],!0);if(n){var o=Y(n);if(f){var p=o.getCellInfo(n).rowIndex;"TH"==n.tagName&&p++;for(var q,r=0;q=a[r++];){for(var s,t=o.insertRow(p++,"td"),u=0;s=q[u];u++){var v=t.cells[u];v||(v=t.insertCell(u)),v.innerHTML=s.innerHTML,s.getAttribute("width")&&v.setAttribute("width",s.getAttribute("width")),s.getAttribute("vAlign")&&v.setAttribute("vAlign",s.getAttribute("vAlign")),s.getAttribute("align")&&v.setAttribute("align",s.getAttribute("align")),s.style.cssText&&(v.style.cssText=s.style.cssText)}for(var s,u=0;(s=t.cells[u])&&q[u];u++)s.innerHTML=q[u].innerHTML,q[u].getAttribute("width")&&s.setAttribute("width",q[u].getAttribute("width")),q[u].getAttribute("vAlign")&&s.setAttribute("vAlign",q[u].getAttribute("vAlign")),q[u].getAttribute("align")&&s.setAttribute("align",q[u].getAttribute("align")),q[u].style.cssText&&(s.style.cssText=q[u].style.cssText)}}else{if(c){y=o.getCellInfo(n);for(var s,w=0,u=0,q=a[0];s=q[u++];)w+=s.colSpan||1;for(h.__hasEnterExecCommand=!0,r=0;w>r;r++)h.execCommand("insertcol");h.__hasEnterExecCommand=!1,n=o.table.rows[0].cells[y.cellIndex],"TH"==n.tagName&&(n=o.table.rows[1].cells[y.cellIndex])}for(var q,r=0;q=a[r++];){l=n;for(var s,u=0;s=q[u++];)if(n)n.innerHTML=s.innerHTML,s.getAttribute("width")&&n.setAttribute("width",s.getAttribute("width")),s.getAttribute("vAlign")&&n.setAttribute("vAlign",s.getAttribute("vAlign")),s.getAttribute("align")&&n.setAttribute("align",s.getAttribute("align")),s.style.cssText&&(n.style.cssText=s.style.cssText),m=n,n=n.nextSibling;else{var x=s.cloneNode(!0);domUtils.removeAttributes(x,["class","rowSpan","colSpan"]),m.parentNode.appendChild(x)}if(n=o.getNextCell(l,!0,!0),!a[r])break;if(!n){var y=o.getCellInfo(l);o.table.insertRow(o.table.rows.length),o.update(),n=o.getVSideCell(l,!0)}}}o.update()}else{k=h.document.createElement("table");for(var q,r=0;q=a[r++];){for(var s,t=k.insertRow(k.rows.length),u=0;s=q[u++];)x=X.cloneCell(s,null,!0),domUtils.removeAttributes(x,["class"]),t.appendChild(x);2==u&&x.rowSpan>1&&(x.rowSpan=1)}var z=$(h),A=h.body.offsetWidth-(ja?2*parseInt(domUtils.getComputedStyle(h.body,"margin-left"),10):0)-2*z.tableBorder-(h.options.offsetWidth||0);h.execCommand("insertHTML",""+k.innerHTML.replace(/>\s*<").replace(/\bth\b/gi,"td")+"
              ")}return h.fireEvent("contentchange"),h.fireEvent("saveScene"),g.html="",!0}var B,j=h.document.createElement("div");j.innerHTML=g.html,B=j.getElementsByTagName("table"),domUtils.findParentByTagName(h.selection.getStart(),"table")?(utils.each(B,function(a){domUtils.remove(a)}),domUtils.findParentByTagName(h.selection.getStart(),"caption",!0)&&(j.innerHTML=j[browser.ie?"innerText":"textContent"])):utils.each(B,function(a){b(a,!0),domUtils.removeAttributes(a,["style","border"]),utils.each(domUtils.getElementsByTagName(a,"td"),function(a){e(a)&&domUtils.fillNode(h.document,a),b(a,!0)})}),g.html=j.innerHTML}),N.addListener("afterpaste",function(){utils.each(domUtils.getElementsByTagName(N.body,"table"),function(a){if(a.offsetWidth>N.body.offsetWidth){var b=$(N,a);a.style.width=N.body.offsetWidth-(ja?2*parseInt(domUtils.getComputedStyle(N.body,"margin-left"),10):0)-2*b.tableBorder-(N.options.offsetWidth||0)+"px"}})}),N.addListener("blur",function(){a=null});var i;N.addListener("keydown",function(){clearTimeout(i),i=setTimeout(function(){var a=N.selection.getRange(),b=domUtils.findParentByTagName(a.startContainer,["th","td"],!0);if(b){var c=b.parentNode.parentNode.parentNode;c.offsetWidth>c.getAttribute("width")&&(b.style.wordBreak="break-all")}},100)}),N.addListener("selectionchange",function(){m(N,!1,"",null)}),N.addListener("contentchange",function(){var a=this;if(I(a),!Z(a)){var b=a.selection.getRange(),c=b.startContainer;c=domUtils.findParentByTagName(c,["td","th"],!0),utils.each(domUtils.getElementsByTagName(a.document,"table"),function(b){a.fireEvent("excludetable",b)!==!0&&(b.ueTable=new X(b),b.onmouseover=function(){a.fireEvent("tablemouseover",b)},b.onmousemove=function(){a.fireEvent("tablemousemove",b),a.options.tableDragable&&h(!0,this,a),utils.defer(function(){a.fireEvent("contentchange",50)},!0)},b.onmouseout=function(){a.fireEvent("tablemouseout",b),m(a,!1,"",null),I(a)},b.onclick=function(b){b=a.window.event||b;var c=d(b.target||b.srcElement);if(c){var e,f=Y(c),g=f.table,h=f.getCellInfo(c),i=a.selection.getRange();if(j(g,c,b,!0)){var k=f.getCell(f.indexTable[f.rowsNum-1][h.colIndex].rowIndex,f.indexTable[f.rowsNum-1][h.colIndex].cellIndex);return void(b.shiftKey&&f.selectedTds.length?f.selectedTds[0]!==k?(e=f.getCellsRange(f.selectedTds[0],k),f.setSelected(e)):i&&i.selectNodeContents(k).select():c!==k?(e=f.getCellsRange(c,k),f.setSelected(e)):i&&i.selectNodeContents(k).select())}if(j(g,c,b)){var l=f.getCell(f.indexTable[h.rowIndex][f.colsNum-1].rowIndex,f.indexTable[h.rowIndex][f.colsNum-1].cellIndex);b.shiftKey&&f.selectedTds.length?f.selectedTds[0]!==l?(e=f.getCellsRange(f.selectedTds[0],l),f.setSelected(e)):i&&i.selectNodeContents(l).select():c!==l?(e=f.getCellsRange(c,l),f.setSelected(e)):i&&i.selectNodeContents(l).select()}}})}),K(a,!0)}}),domUtils.on(N.document,"mousemove",g),domUtils.on(N.document,"mouseout",function(a){var b=a.target||a.srcElement;"TABLE"==b.tagName&&m(N,!1,"",null)}),N.addListener("interlacetable",function(a,b,c){if(b)for(var d=this,e=b.rows,f=e.length,g=function(a,b,c){return a[b]?a[b]:c?a[b%a.length]:""},h=0;f>h;h++)e[h].className=g(c||d.options.classList,h,!0)}),N.addListener("uninterlacetable",function(a,b){if(b)for(var c=this,d=b.rows,e=c.options.classList,f=d.length,g=0;f>g;g++)domUtils.removeClasses(d[g],e)}),N.addListener("mousedown",o),N.addListener("mouseup",w),domUtils.on(N.body,"dragstart",function(a){w.call(N,"dragstart",a)}),N.addOutputRule(function(a){utils.each(a.getNodesByTagName("div"),function(a){"ue_tableDragLine"==a.getAttr("id")&&a.parentNode.removeChild(a)})});var k=0;N.addListener("mousedown",function(){k=0}),N.addListener("tabkeydown",function(){var a=this.selection.getRange(),b=a.getCommonAncestor(!0,!0),c=domUtils.findParentByTagName(b,"table");if(c){if(domUtils.findParentByTagName(b,"caption",!0)){var d=domUtils.getElementsByTagName(c,"th td");d&&d.length&&a.setStart(d[0],0).setCursor(!1,!0)}else{var d=domUtils.findParentByTagName(b,["td","th"],!0),f=Y(d);k=d.rowSpan>1?k:f.getCellInfo(d).rowIndex;var g=f.getTabNextCell(d,k);g?e(g)?a.setStart(g,0).setCursor(!1,!0):a.selectNodeContents(g).select():(N.fireEvent("saveScene"),N.__hasEnterExecCommand=!0,this.execCommand("insertrownext"),N.__hasEnterExecCommand=!1,a=this.selection.getRange(),a.setStart(c.rows[c.rows.length-1].cells[0],0).setCursor(),N.fireEvent("saveScene"))}return!0}}),browser.ie&&N.addListener("selectionchange",function(){m(this,!1,"",null)}),N.addListener("keydown",function(a,b){var c=this,d=b.keyCode||b.which;if(8!=d&&46!=d){var e=!(b.ctrlKey||b.metaKey||b.shiftKey||b.altKey);e&&_(domUtils.getElementsByTagName(c.body,"td"));var f=Z(c);f&&e&&f.clearSelected()}}),N.addListener("beforegetcontent",function(){K(this,!1),browser.ie&&utils.each(this.document.getElementsByTagName("caption"),function(a){domUtils.isEmptyNode(a)&&(a.innerHTML=" ")})}),N.addListener("aftergetcontent",function(){K(this,!0)}),N.addListener("getAllHtml",function(){_(N.document.getElementsByTagName("td"))}),N.addListener("fullscreenchanged",function(a,b){if(!b){var c=this.body.offsetWidth/document.body.offsetWidth,d=domUtils.getElementsByTagName(this.body,"table");utils.each(d,function(a){if(a.offsetWidthj;j++)e[j]=d[j];e.splice(0,h.beginRowIndex),g=h.endRowIndex+1===this.rowsNum?0:h.endRowIndex+1; +}else for(var j=0,i=d.length;i>j;j++)e[j]=d[j];var k={reversecurrent:function(a,b){return 1},orderbyasc:function(a,b){var c=a.innerText||a.textContent,d=b.innerText||b.textContent;return c.localeCompare(d)},reversebyasc:function(a,b){var c=a.innerHTML,d=b.innerHTML;return d.localeCompare(c)},orderbynum:function(a,b){var c=a[browser.ie?"innerText":"textContent"].match(/\d+/),d=b[browser.ie?"innerText":"textContent"].match(/\d+/);return c&&(c=+c[0]),d&&(d=+d[0]),(c||0)-(d||0)},reversebynum:function(a,b){var c=a[browser.ie?"innerText":"textContent"].match(/\d+/),d=b[browser.ie?"innerText":"textContent"].match(/\d+/);return c&&(c=+c[0]),d&&(d=+d[0]),(d||0)-(c||0)}};c.setAttribute("data-sort-type",b&&"string"==typeof b&&k[b]?b:""),f&&e.splice(0,1),e=utils.sort(e,function(c,d){var e;return e=b&&"function"==typeof b?b.call(this,c.cells[a],d.cells[a]):b&&"number"==typeof b?1:b&&"string"==typeof b&&k[b]?k[b].call(this,c.cells[a],d.cells[a]):k.orderbyasc.call(this,c.cells[a],d.cells[a])});for(var l=c.ownerDocument.createDocumentFragment(),m=0,i=e.length;i>m;m++)l.appendChild(e[m]);var n=c.getElementsByTagName("tbody")[0];g?n.insertBefore(l,d[g-h.endRowIndex+h.beginRowIndex-1]):n.appendChild(l)},UE.plugins.tablesort=function(){var a=this,b=UE.UETable,c=function(a){return b.getUETable(a)},d=function(a){return b.getTableItemsByRange(a)};a.ready(function(){utils.cssRule("tablesort","table.sortEnabled tr.firstRow th,table.sortEnabled tr.firstRow td{padding-right:20px;background-repeat: no-repeat;background-position: center right; background-image:url("+a.options.themePath+a.options.theme+"/images/sortable.png);}",a.document),a.addListener("afterexeccommand",function(a,b){"mergeright"!=b&&"mergedown"!=b&&"mergecells"!=b||this.execCommand("disablesort")})}),UE.commands.sorttable={queryCommandState:function(){var a=this,b=d(a);if(!b.cell)return-1;for(var c,e=b.table,f=e.getElementsByTagName("td"),g=0;c=f[g++];)if(1!=c.rowSpan||1!=c.colSpan)return-1;return 0},execCommand:function(a,b){var e=this,f=e.selection.getRange(),g=f.createBookmark(!0),h=d(e),i=h.cell,j=c(h.table),k=j.getCellInfo(i);j.sortTable(k.cellIndex,b),f.moveToBookmark(g);try{f.select()}catch(l){}}},UE.commands.enablesort=UE.commands.disablesort={queryCommandState:function(a){var b=d(this).table;if(b&&"enablesort"==a)for(var c=domUtils.getElementsByTagName(b,"th td"),e=0;e1||c[e].getAttribute("rowspan")>1)return-1;return b?"enablesort"==a^"sortEnabled"!=b.getAttribute("data-sort")?-1:0:-1},execCommand:function(a){var b=d(this).table;b.setAttribute("data-sort","enablesort"==a?"sortEnabled":"sortDisabled"),"enablesort"==a?domUtils.addClass(b,"sortEnabled"):domUtils.removeClasses(b,"sortEnabled")}}},UE.plugins.contextmenu=function(){var a=this;if(a.setOpt("enableContextMenu",!0),a.getOpt("enableContextMenu")!==!1){var b,c=a.getLang("contextMenu"),d=a.options.contextMenu||[{label:c.selectall,cmdName:"selectall"},{label:c.cleardoc,cmdName:"cleardoc",exec:function(){confirm(c.confirmclear)&&this.execCommand("cleardoc")}},"-",{label:c.unlink,cmdName:"unlink"},"-",{group:c.paragraph,icon:"justifyjustify",subMenu:[{label:c.justifyleft,cmdName:"justify",value:"left"},{label:c.justifyright,cmdName:"justify",value:"right"},{label:c.justifycenter,cmdName:"justify",value:"center"},{label:c.justifyjustify,cmdName:"justify",value:"justify"}]},"-",{group:c.table,icon:"table",subMenu:[{label:c.inserttable,cmdName:"inserttable"},{label:c.deletetable,cmdName:"deletetable"},"-",{label:c.deleterow,cmdName:"deleterow"},{label:c.deletecol,cmdName:"deletecol"},{label:c.insertcol,cmdName:"insertcol"},{label:c.insertcolnext,cmdName:"insertcolnext"},{label:c.insertrow,cmdName:"insertrow"},{label:c.insertrownext,cmdName:"insertrownext"},"-",{label:c.insertcaption,cmdName:"insertcaption"},{label:c.deletecaption,cmdName:"deletecaption"},{label:c.inserttitle,cmdName:"inserttitle"},{label:c.deletetitle,cmdName:"deletetitle"},{label:c.inserttitlecol,cmdName:"inserttitlecol"},{label:c.deletetitlecol,cmdName:"deletetitlecol"},"-",{label:c.mergecells,cmdName:"mergecells"},{label:c.mergeright,cmdName:"mergeright"},{label:c.mergedown,cmdName:"mergedown"},"-",{label:c.splittorows,cmdName:"splittorows"},{label:c.splittocols,cmdName:"splittocols"},{label:c.splittocells,cmdName:"splittocells"},"-",{label:c.averageDiseRow,cmdName:"averagedistributerow"},{label:c.averageDisCol,cmdName:"averagedistributecol"},"-",{label:c.edittd,cmdName:"edittd",exec:function(){UE.ui.edittd&&new UE.ui.edittd(this),this.getDialog("edittd").open()}},{label:c.edittable,cmdName:"edittable",exec:function(){UE.ui.edittable&&new UE.ui.edittable(this),this.getDialog("edittable").open()}},{label:c.setbordervisible,cmdName:"setbordervisible"}]},{group:c.tablesort,icon:"tablesort",subMenu:[{label:c.enablesort,cmdName:"enablesort"},{label:c.disablesort,cmdName:"disablesort"},"-",{label:c.reversecurrent,cmdName:"sorttable",value:"reversecurrent"},{label:c.orderbyasc,cmdName:"sorttable",value:"orderbyasc"},{label:c.reversebyasc,cmdName:"sorttable",value:"reversebyasc"},{label:c.orderbynum,cmdName:"sorttable",value:"orderbynum"},{label:c.reversebynum,cmdName:"sorttable",value:"reversebynum"}]},{group:c.borderbk,icon:"borderBack",subMenu:[{label:c.setcolor,cmdName:"interlacetable",exec:function(){this.execCommand("interlacetable")}},{label:c.unsetcolor,cmdName:"uninterlacetable",exec:function(){this.execCommand("uninterlacetable")}},{label:c.setbackground,cmdName:"settablebackground",exec:function(){this.execCommand("settablebackground",{repeat:!0,colorList:["#bbb","#ccc"]})}},{label:c.unsetbackground,cmdName:"cleartablebackground",exec:function(){this.execCommand("cleartablebackground")}},{label:c.redandblue,cmdName:"settablebackground",exec:function(){this.execCommand("settablebackground",{repeat:!0,colorList:["red","blue"]})}},{label:c.threecolorgradient,cmdName:"settablebackground",exec:function(){this.execCommand("settablebackground",{repeat:!0,colorList:["#aaa","#bbb","#ccc"]})}}]},{group:c.aligntd,icon:"aligntd",subMenu:[{cmdName:"cellalignment",value:{align:"left",vAlign:"top"}},{cmdName:"cellalignment",value:{align:"center",vAlign:"top"}},{cmdName:"cellalignment",value:{align:"right",vAlign:"top"}},{cmdName:"cellalignment",value:{align:"left",vAlign:"middle"}},{cmdName:"cellalignment",value:{align:"center",vAlign:"middle"}},{cmdName:"cellalignment",value:{align:"right",vAlign:"middle"}},{cmdName:"cellalignment",value:{align:"left",vAlign:"bottom"}},{cmdName:"cellalignment",value:{align:"center",vAlign:"bottom"}},{cmdName:"cellalignment",value:{align:"right",vAlign:"bottom"}}]},{group:c.aligntable,icon:"aligntable",subMenu:[{cmdName:"tablealignment",className:"left",label:c.tableleft,value:"left"},{cmdName:"tablealignment",className:"center",label:c.tablecenter,value:"center"},{cmdName:"tablealignment",className:"right",label:c.tableright,value:"right"}]},"-",{label:c.insertparagraphbefore,cmdName:"insertparagraph",value:!0},{label:c.insertparagraphafter,cmdName:"insertparagraph"},{label:c.copy,cmdName:"copy"},{label:c.paste,cmdName:"paste"}];if(d.length){var e=UE.ui.uiUtils;a.addListener("contextmenu",function(f,g){var h=e.getViewportOffsetByEvent(g);a.fireEvent("beforeselectionchange"),b&&b.destroy();for(var i,j=0,k=[];i=d[j];j++){var l;!function(b){function d(){switch(b.icon){case"table":return a.getLang("contextMenu.table");case"justifyjustify":return a.getLang("contextMenu.paragraph");case"aligntd":return a.getLang("contextMenu.aligntd");case"aligntable":return a.getLang("contextMenu.aligntable");case"tablesort":return c.tablesort;case"borderBack":return c.borderbk;default:return""}}if("-"==b)(l=k[k.length-1])&&"-"!==l&&k.push("-");else if(b.hasOwnProperty("group")){for(var e,f=0,g=[];e=b.subMenu[f];f++)!function(b){"-"==b?(l=g[g.length-1])&&"-"!==l?g.push("-"):g.splice(g.length-1):(a.commands[b.cmdName]||UE.commands[b.cmdName]||b.query)&&(b.query?b.query():a.queryCommandState(b.cmdName))>-1&&g.push({label:b.label||a.getLang("contextMenu."+b.cmdName+(b.value||""))||"",className:"edui-for-"+b.cmdName+(b.className?" edui-for-"+b.cmdName+"-"+b.className:""),onclick:b.exec?function(){b.exec.call(a)}:function(){a.execCommand(b.cmdName,b.value)}})}(e);g.length&&k.push({label:d(),className:"edui-for-"+b.icon,subMenu:{items:g,editor:a}})}else(a.commands[b.cmdName]||UE.commands[b.cmdName]||b.query)&&(b.query?b.query.call(a):a.queryCommandState(b.cmdName))>-1&&k.push({label:b.label||a.getLang("contextMenu."+b.cmdName),className:"edui-for-"+(b.icon?b.icon:b.cmdName+(b.value||"")),onclick:b.exec?function(){b.exec.call(a)}:function(){a.execCommand(b.cmdName,b.value)}})}(i)}if("-"==k[k.length-1]&&k.pop(),b=new UE.ui.Menu({items:k,className:"edui-contextmenu",editor:a}),b.render(),b.showAt(h),a.fireEvent("aftershowcontextmenu",b),domUtils.preventDefault(g),browser.ie){var m;try{m=a.selection.getNative().createRange()}catch(n){return}if(m.item){var o=new dom.Range(a.document);o.selectNode(m.item(0)).select(!0,!0)}}}),a.addListener("aftershowcontextmenu",function(b,c){if(a.zeroclipboard){var d=c.items;for(var e in d)"edui-for-copy"==d[e].className&&a.zeroclipboard.clip(d[e].getDom())}})}}},UE.plugins.shortcutmenu=function(){var a,b=this,c=b.options.shortcutMenu||[];c.length&&(b.addListener("contextmenu mouseup",function(b,d){var e=this,f={type:b,target:d.target||d.srcElement,screenX:d.screenX,screenY:d.screenY,clientX:d.clientX,clientY:d.clientY};if(setTimeout(function(){var d=e.selection.getRange();d.collapsed!==!1&&"contextmenu"!=b||(a||(a=new baidu.editor.ui.ShortCutMenu({editor:e,items:c,theme:e.options.theme,className:"edui-shortcutmenu"}),a.render(),e.fireEvent("afterrendershortcutmenu",a)),a.show(f,!!UE.plugins.contextmenu))}),"contextmenu"==b&&(domUtils.preventDefault(d),browser.ie9below)){var g;try{g=e.selection.getNative().createRange()}catch(d){return}if(g.item){var h=new dom.Range(e.document);h.selectNode(g.item(0)).select(!0,!0)}}}),b.addListener("keydown",function(b){"keydown"==b&&a&&!a.isHidden&&a.hide()}))},UE.plugins.basestyle=function(){var a={bold:["strong","b"],italic:["em","i"],subscript:["sub"],superscript:["sup"]},b=function(a,b){return domUtils.filterNodeList(a.selection.getStartElementPath(),b)},c=this;c.addshortcutkey({Bold:"ctrl+66",Italic:"ctrl+73",Underline:"ctrl+85"}),c.addInputRule(function(a){utils.each(a.getNodesByTagName("b i"),function(a){switch(a.tagName){case"b":a.tagName="strong";break;case"i":a.tagName="em"}})});for(var d in a)!function(a,d){c.commands[a]={execCommand:function(a){var e=c.selection.getRange(),f=b(this,d);if(e.collapsed){if(f){var g=c.document.createTextNode("");e.insertNode(g).removeInlineStyle(d),e.setStartBefore(g),domUtils.remove(g)}else{var h=e.document.createElement(d[0]);"superscript"!=a&&"subscript"!=a||(g=c.document.createTextNode(""),e.insertNode(g).removeInlineStyle(["sub","sup"]).setStartBefore(g).collapse(!0)),e.insertNode(h).setStart(h,0)}e.collapse(!0)}else"superscript"!=a&&"subscript"!=a||f&&f.tagName.toLowerCase()==a||e.removeInlineStyle(["sub","sup"]),f?e.removeInlineStyle(d):e.applyInlineStyle(d[0]);e.select()},queryCommandState:function(){return b(this,d)?1:0}}}(d,a[d])},UE.plugins.elementpath=function(){var a,b,c=this;c.setOpt("elementPathEnabled",!0),c.options.elementPathEnabled&&(c.commands.elementpath={execCommand:function(d,e){var f=b[e],g=c.selection.getRange();a=1*e,g.selectNode(f).select()},queryCommandValue:function(){var c=[].concat(this.selection.getStartElementPath()).reverse(),d=[];b=c;for(var e,f=0;e=c[f];f++)if(3!=e.nodeType){var g=e.tagName.toLowerCase();if("img"==g&&e.getAttribute("anchorname")&&(g="anchor"),d[f]=g,a==f){a=-1;break}}return d}})},UE.plugins.formatmatch=function(){function a(f,g){function h(a){return m&&a.selectNode(m),a.applyInlineStyle(d[d.length-1].tagName,null,d)}if(browser.webkit)var i="IMG"==g.target.tagName?g.target:null;c.undoManger&&c.undoManger.save();var j=c.selection.getRange(),k=i||j.getClosedNode();if(b&&k&&"IMG"==k.tagName)k.style.cssText+=";float:"+(b.style.cssFloat||b.style.styleFloat||"none")+";display:"+(b.style.display||"inline"),b=null;else if(!b){var l=j.collapsed;if(l){var m=c.document.createTextNode("match");j.insertNode(m).select()}c.__hasEnterExecCommand=!0;var n=c.options.removeFormatAttributes;c.options.removeFormatAttributes="",c.execCommand("removeformat"),c.options.removeFormatAttributes=n,c.__hasEnterExecCommand=!1,j=c.selection.getRange(),d.length&&h(j),m&&j.setStartBefore(m).collapse(!0),j.select(),m&&domUtils.remove(m)}c.undoManger&&c.undoManger.save(),c.removeListener("mouseup",a),e=0}var b,c=this,d=[],e=0;c.addListener("reset",function(){d=[],e=0}),c.commands.formatmatch={execCommand:function(f){if(e)return e=0,d=[],void c.removeListener("mouseup",a);var g=c.selection.getRange();if(b=g.getClosedNode(),!b||"IMG"!=b.tagName){g.collapse(!0).shrinkBoundary();var h=g.startContainer;d=domUtils.findParents(h,!0,function(a){return!domUtils.isBlockElm(a)&&1==a.nodeType});for(var i,j=0;i=d[j];j++)if("A"==i.tagName){d.splice(j,1);break}}c.addListener("mouseup",a),e=1},queryCommandState:function(){return e},notNeedUndo:1}},UE.plugin.register("searchreplace",function(){function a(a,b,c){var d=b.searchStr;-1==b.dir&&(a=a.split("").reverse().join(""),d=d.split("").reverse().join(""),c=a.length-c);for(var e,f=new RegExp(d,"g"+(b.casesensitive?"":"i"));e=f.exec(a);)if(e.index>=c)return-1==b.dir?a.length-e.index-b.searchStr.length:e.index;return-1}function b(b,c,d){var e,f,h=d.all||1==d.dir?"getNextDomNode":"getPreDomNode";domUtils.isBody(b)&&(b=b.firstChild);for(var i=1;b;){if(e=3==b.nodeType?b.nodeValue:b[browser.ie?"innerText":"textContent"],f=a(e,d,c),i=0,-1!=f)return{node:b,index:f};for(b=domUtils[h](b);b&&g[b.nodeName.toLowerCase()];)b=domUtils[h](b,!0);b&&(c=-1==d.dir?(3==b.nodeType?b.nodeValue:b[browser.ie?"innerText":"textContent"]).length:0)}}function c(a,b,d){for(var e,f=0,g=a.firstChild,h=0;g;){if(3==g.nodeType){if(h=g.nodeValue.replace(/(^[\t\r\n]+)|([\t\r\n]+$)/,"").length,f+=h,f>=b)return{node:g,index:h-(f-b)}}else if(!dtd.$empty[g.tagName]&&(h=g[browser.ie?"innerText":"textContent"].replace(/(^[\t\r\n]+)|([\t\r\n]+$)/,"").length,f+=h,f>=b&&(e=c(g,h-(f-b),d))))return e;g=domUtils.getNextDomNode(g)}}function d(a,d){var f,g=a.selection.getRange(),h=d.searchStr,i=a.document.createElement("span");if(i.innerHTML="$$ueditor_searchreplace_key$$",g.shrinkBoundary(!0),!g.collapsed){g.select();var j=a.selection.getText();if(new RegExp("^"+d.searchStr+"$",d.casesensitive?"":"i").test(j)){if(void 0!=d.replaceStr)return e(g,d.replaceStr),g.select(),!0;g.collapse(-1==d.dir)}}g.insertNode(i),g.enlargeToBlockElm(!0),f=g.startContainer;var k=f[browser.ie?"innerText":"textContent"].indexOf("$$ueditor_searchreplace_key$$");g.setStartBefore(i),domUtils.remove(i);var l=b(f,k,d);if(l){var m=c(l.node,l.index,h),n=c(l.node,l.index+h.length,h);return g.setStart(m.node,m.index).setEnd(n.node,n.index),void 0!==d.replaceStr&&e(g,d.replaceStr),g.select(),!0}g.setCursor()}function e(a,b){b=f.document.createTextNode(b),a.deleteContents().insertNode(b)}var f=this,g={table:1,tbody:1,tr:1,ol:1,ul:1};return{commands:{searchreplace:{execCommand:function(a,b){utils.extend(b,{all:!1,casesensitive:!1,dir:1},!0);var c=0;if(b.all){var e=f.selection.getRange(),g=f.body.firstChild;for(g&&1==g.nodeType?(e.setStart(g,0),e.shrinkBoundary(!0)):3==g.nodeType&&e.setStartBefore(g),e.collapse(!0).select(!0),void 0!==b.replaceStr&&f.fireEvent("saveScene");d(this,b);)c++;c&&f.fireEvent("saveScene")}else void 0!==b.replaceStr&&f.fireEvent("saveScene"),d(this,b)&&c++,c&&f.fireEvent("saveScene");return c},notNeedUndo:1}}}}),UE.plugins.customstyle=function(){var a=this;a.setOpt({customstyle:[{tag:"h1",name:"tc",style:"font-size:32px;font-weight:bold;border-bottom:#ccc 2px solid;padding:0 4px 0 0;text-align:center;margin:0 0 20px 0;"},{tag:"h1",name:"tl",style:"font-size:32px;font-weight:bold;border-bottom:#ccc 2px solid;padding:0 4px 0 0;text-align:left;margin:0 0 10px 0;"},{tag:"span",name:"im",style:"font-size:16px;font-style:italic;font-weight:bold;line-height:18px;"},{tag:"span",name:"hi",style:"font-size:16px;font-style:italic;font-weight:bold;color:rgb(51, 153, 204);line-height:18px;"}]}),a.commands.customstyle={execCommand:function(a,b){var c,d,e=this,f=b.tag,g=domUtils.findParent(e.selection.getStart(),function(a){return a.getAttribute("label")},!0),h={};for(var i in b)void 0!==b[i]&&(h[i]=b[i]);if(delete h.tag,g&&g.getAttribute("label")==b.label){if(c=this.selection.getRange(),d=c.createBookmark(),c.collapsed)if(dtd.$block[g.tagName]){var j=e.document.createElement("p");domUtils.moveChild(g,j),g.parentNode.insertBefore(j,g),domUtils.remove(g)}else domUtils.remove(g,!0);else{var k=domUtils.getCommonAncestor(d.start,d.end),l=domUtils.getElementsByTagName(k,f);new RegExp(f,"i").test(k.tagName)&&l.push(k);for(var m,n=0;m=l[n++];)if(m.getAttribute("label")==b.label){var o=domUtils.getPosition(m,d.start),p=domUtils.getPosition(m,d.end);if((o&domUtils.POSITION_FOLLOWING||o&domUtils.POSITION_CONTAINS)&&(p&domUtils.POSITION_PRECEDING||p&domUtils.POSITION_CONTAINS)&&dtd.$block[f]){var j=e.document.createElement("p");domUtils.moveChild(m,j),m.parentNode.insertBefore(j,m)}domUtils.remove(m,!0)}g=domUtils.findParent(k,function(a){return a.getAttribute("label")==b.label},!0),g&&domUtils.remove(g,!0)}c.moveToBookmark(d).select()}else if(dtd.$block[f]){if(this.execCommand("paragraph",f,h,"customstyle"),c=e.selection.getRange(),!c.collapsed){c.collapse(),g=domUtils.findParent(e.selection.getStart(),function(a){return a.getAttribute("label")==b.label},!0);var q=e.document.createElement("p");domUtils.insertAfter(g,q),domUtils.fillNode(e.document,q),c.setStart(q,0).setCursor()}}else{if(c=e.selection.getRange(),c.collapsed)return g=e.document.createElement(f),domUtils.setAttributes(g,h),void c.insertNode(g).setStart(g,0).setCursor();d=c.createBookmark(),c.applyInlineStyle(f,h).moveToBookmark(d).select()}},queryCommandValue:function(){var a=domUtils.filterNodeList(this.selection.getStartElementPath(),function(a){return a.getAttribute("label")});return a?a.getAttribute("label"):""}},a.addListener("keyup",function(b,c){var d=c.keyCode||c.which;if(32==d||13==d){var e=a.selection.getRange();if(e.collapsed){var f=domUtils.findParent(a.selection.getStart(),function(a){return a.getAttribute("label")},!0);if(f&&dtd.$block[f.tagName]&&domUtils.isEmptyNode(f)){var g=a.document.createElement("p");domUtils.insertAfter(f,g),domUtils.fillNode(a.document,g),domUtils.remove(f),e.setStart(g,0).setCursor()}}}})},UE.plugins.catchremoteimage=function(){var me=this,ajax=UE.ajax;me.options.catchRemoteImageEnable!==!1&&(me.setOpt({catchRemoteImageEnable:!1}),me.addListener("afterpaste",function(){me.fireEvent("catchRemoteImage")}),me.addListener("catchRemoteImage",function(){function catchremoteimage(a,b){var c=utils.serializeParam(me.queryCommandValue("serverparam"))||"",d=utils.formatUrl(catcherActionUrl+(-1==catcherActionUrl.indexOf("?")?"?":"&")+c),e=utils.isCrossDomainUrl(d),f={method:"POST",dataType:e?"jsonp":"",timeout:6e4,onsuccess:b.success,onerror:b.error};f[catcherFieldName]=a,ajax.request(d,f)}for(var catcherLocalDomain=me.getOpt("catcherLocalDomain"),catcherActionUrl=me.getActionUrl(me.getOpt("catcherActionName")),catcherUrlPrefix=me.getOpt("catcherUrlPrefix"),catcherFieldName=me.getOpt("catcherFieldName"),remoteImages=[],imgs=domUtils.getElementsByTagName(me.document,"img"),test=function(a,b){if(-1!=a.indexOf(location.host)||/(^\.)|(^\/)/.test(a))return!0;if(b)for(var c,d=0;c=b[d++];)if(-1!==a.indexOf(c))return!0;return!1},i=0,ci;ci=imgs[i++];)if(!ci.getAttribute("word_img")){var src=ci.getAttribute("_src")||ci.src||"";/^(https?|ftp):/i.test(src)&&!test(src,catcherLocalDomain)&&remoteImages.push(src)}remoteImages.length&&catchremoteimage(remoteImages,{success:function(r){try{var info=void 0!==r.state?r:eval("("+r.responseText+")")}catch(e){return}var i,j,ci,cj,oldSrc,newSrc,list=info.list;for(i=0;ci=imgs[i++];)for(oldSrc=ci.getAttribute("_src")||ci.src||"",j=0;cj=list[j++];)if(oldSrc==cj.source&&"SUCCESS"==cj.state){newSrc=catcherUrlPrefix+cj.url,domUtils.setAttributes(ci,{src:newSrc,_src:newSrc});break}me.fireEvent("catchremotesuccess")},error:function(){me.fireEvent("catchremoteerror")}})}))},UE.plugin.register("snapscreen",function(){function getLocation(a){var b,c=document.createElement("a"),d=utils.serializeParam(me.queryCommandValue("serverparam"))||"";return c.href=a,browser.ie&&(c.href=c.href),b=c.search,d&&(b=b+(-1==b.indexOf("?")?"?":"&")+d,b=b.replace(/[&]+/gi,"&")),{port:c.port,hostname:c.hostname,path:c.pathname+b||+c.hash}}var me=this,snapplugin;return{commands:{snapscreen:{execCommand:function(cmd){function onSuccess(rs){try{if(rs=eval("("+rs+")"),"SUCCESS"==rs.state){var opt=me.options;me.execCommand("insertimage",{src:opt.snapscreenUrlPrefix+rs.url,_src:opt.snapscreenUrlPrefix+rs.url,alt:rs.title||"",floatStyle:opt.snapscreenImgAlign})}else alert(rs.state)}catch(e){alert(lang.callBackErrorMsg)}}var url,local,res,lang=me.getLang("snapScreen_plugin");if(!snapplugin){var container=me.container,doc=me.container.ownerDocument||me.container.document;snapplugin=doc.createElement("object");try{snapplugin.type="application/x-pluginbaidusnap"}catch(e){return}snapplugin.style.cssText="position:absolute;left:-9999px;width:0;height:0;",snapplugin.setAttribute("width","0"),snapplugin.setAttribute("height","0"),container.appendChild(snapplugin)}url=me.getActionUrl(me.getOpt("snapscreenActionName")),local=getLocation(url),setTimeout(function(){try{res=snapplugin.saveSnapshot(local.hostname,local.path,local.port)}catch(a){return void me.ui._dialogs.snapscreenDialog.open()}onSuccess(res)},50)},queryCommandState:function(){return-1!=navigator.userAgent.indexOf("Windows",0)?0:-1}}}}}),UE.commands.insertparagraph={execCommand:function(a,b){for(var c,d=this,e=d.selection.getRange(),f=e.startContainer;f&&!domUtils.isBody(f);)c=f,f=f.parentNode;if(c){var g=d.document.createElement("p");b?c.parentNode.insertBefore(g,c):c.parentNode.insertBefore(g,c.nextSibling),domUtils.fillNode(d.document,g),e.setStart(g,0).setCursor(!1,!0)}}},UE.plugin.register("webapp",function(){function a(a,c){return c?'':'"}var b=this;return{outputRule:function(b){utils.each(b.getNodesByTagName("img"),function(b){var c;if("edui-faked-webapp"==b.getAttr("class")){c=a({title:b.getAttr("title"),width:b.getAttr("width"),height:b.getAttr("height"),align:b.getAttr("align"),cssfloat:b.getStyle("float"),url:b.getAttr("_url"),logo:b.getAttr("_logo_url")},!0);var d=UE.uNode.createElement(c);b.parentNode.replaceChild(d,b)}})},inputRule:function(b){utils.each(b.getNodesByTagName("iframe"),function(b){if("edui-faked-webapp"==b.getAttr("class")){var c=UE.uNode.createElement(a({title:b.getAttr("title"),width:b.getAttr("width"),height:b.getAttr("height"),align:b.getAttr("align"),cssfloat:b.getStyle("float"),url:b.getAttr("src"),logo:b.getAttr("logo_url")}));b.parentNode.replaceChild(c,b)}})},commands:{webapp:{execCommand:function(b,c){var d=this,e=a(utils.extend(c,{align:"none"}),!1);d.execCommand("inserthtml",e)},queryCommandState:function(){var a=this,b=a.selection.getRange().getClosedNode(),c=b&&"edui-faked-webapp"==b.className;return c?1:0}}}}}),UE.plugins.template=function(){UE.commands.template={execCommand:function(a,b){b.html&&this.execCommand("inserthtml",b.html)}},this.addListener("click",function(a,b){var c=b.target||b.srcElement,d=this.selection.getRange(),e=domUtils.findParent(c,function(a){return a.className&&domUtils.hasClass(a,"ue_t")?a:void 0},!0);e&&d.selectNode(e).shrinkBoundary().select()}),this.addListener("keydown",function(a,b){var c=this.selection.getRange();if(!c.collapsed&&!(b.ctrlKey||b.metaKey||b.shiftKey||b.altKey)){var d=domUtils.findParent(c.startContainer,function(a){return a.className&&domUtils.hasClass(a,"ue_t")?a:void 0},!0);d&&domUtils.removeClasses(d,["ue_t"])}})},UE.plugin.register("music",function(){function a(a,c,d,e,f,g){return g?'':"'}var b=this;return{outputRule:function(b){utils.each(b.getNodesByTagName("img"),function(b){var c;if("edui-faked-music"==b.getAttr("class")){var d=b.getStyle("float"),e=b.getAttr("align");c=a(b.getAttr("_url"),b.getAttr("width"),b.getAttr("height"),e,d,!0);var f=UE.uNode.createElement(c);b.parentNode.replaceChild(f,b)}})},inputRule:function(b){utils.each(b.getNodesByTagName("embed"),function(b){if("edui-faked-music"==b.getAttr("class")){var c=b.getStyle("float"),d=b.getAttr("align");html=a(b.getAttr("src"),b.getAttr("width"),b.getAttr("height"),d,c,!1);var e=UE.uNode.createElement(html);b.parentNode.replaceChild(e,b)}})},commands:{music:{execCommand:function(b,c){var d=this,e=a(c.url,c.width||400,c.height||95,"none",!1);d.execCommand("inserthtml",e)},queryCommandState:function(){var a=this,b=a.selection.getRange().getClosedNode(),c=b&&"edui-faked-music"==b.className;return c?1:0}}}}}),UE.plugin.register("autoupload",function(){function a(a,b){var c,d,e,f,g,h,i,j,k=b,l=/image\/\w+/i.test(a.type)?"image":"file",m="loading_"+(+new Date).toString(36);if(c=k.getOpt(l+"FieldName"),d=k.getOpt(l+"UrlPrefix"),e=k.getOpt(l+"MaxSize"),f=k.getOpt(l+"AllowFiles"),g=k.getActionUrl(k.getOpt(l+"ActionName")),i=function(a){var b=k.document.getElementById(m);b&&domUtils.remove(b),k.fireEvent("showmessage",{id:m,content:a,type:"error",timeout:4e3})},"image"==l?(h='',j=function(a){var b=d+a.url,c=k.document.getElementById(m);c&&(c.setAttribute("src",b),c.setAttribute("_src",b),c.setAttribute("title",a.title||""),c.setAttribute("alt",a.original||""),c.removeAttribute("id"),domUtils.removeClasses(c,"loadingclass"))}):(h='

              ',j=function(a){var b=d+a.url,c=k.document.getElementById(m),e=k.selection.getRange(),f=e.createBookmark();e.selectNode(c).select(),k.execCommand("insertfile",{url:b}),e.moveToBookmark(f).select()}),k.execCommand("inserthtml",h),!k.getOpt(l+"ActionName"))return void i(k.getLang("autoupload.errorLoadConfig"));if(a.size>e)return void i(k.getLang("autoupload.exceedSizeError"));var n=a.name?a.name.substr(a.name.lastIndexOf(".")):"";if(n&&"image"!=l||f&&-1==(f.join("")+".").indexOf(n.toLowerCase()+"."))return void i(k.getLang("autoupload.exceedTypeError"));var o=new XMLHttpRequest,p=new FormData,q=utils.serializeParam(k.queryCommandValue("serverparam"))||"",r=utils.formatUrl(g+(-1==g.indexOf("?")?"?":"&")+q);p.append(c,a,a.name||"blob."+a.type.substr("image/".length)),p.append("type","ajax"),o.open("post",r,!0),o.setRequestHeader("X-Requested-With","XMLHttpRequest"),o.addEventListener("load",function(a){try{var b=new Function("return "+utils.trim(a.target.response))();"SUCCESS"==b.state&&b.url?j(b):i(b.state)}catch(c){i(k.getLang("autoupload.loadError"))}}),o.send(p)}function b(a){return a.clipboardData&&a.clipboardData.items&&1==a.clipboardData.items.length&&/^image\//.test(a.clipboardData.items[0].type)?a.clipboardData.items:null}function c(a){return a.dataTransfer&&a.dataTransfer.files?a.dataTransfer.files:null}return{outputRule:function(a){utils.each(a.getNodesByTagName("img"),function(a){/\b(loaderrorclass)|(bloaderrorclass)\b/.test(a.getAttr("class"))&&a.parentNode.removeChild(a)}),utils.each(a.getNodesByTagName("p"),function(a){/\bloadpara\b/.test(a.getAttr("class"))&&a.parentNode.removeChild(a)})},bindEvents:{ready:function(d){var e=this;window.FormData&&window.FileReader&&(domUtils.on(e.body,"paste drop",function(d){var f,g=!1;if(f="paste"==d.type?b(d):c(d)){for(var h,i=f.length;i--;)h=f[i],h.getAsFile&&(h=h.getAsFile()),h&&h.size>0&&(a(h,e),g=!0);g&&d.preventDefault()}}),domUtils.on(e.body,"dragover",function(a){"Files"==a.dataTransfer.types[0]&&a.preventDefault()}),utils.cssRule("loading",".loadingclass{display:inline-block;cursor:default;background: url('"+this.options.themePath+this.options.theme+"/images/loading.gif') no-repeat center center transparent;border:1px solid #cccccc;margin-left:1px;height: 22px;width: 22px;}\n.loaderrorclass{display:inline-block;cursor:default;background: url('"+this.options.themePath+this.options.theme+"/images/loaderror.png') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;}",this.document))}}}}),UE.plugin.register("autosave",function(){function a(a){var f;if(!(new Date-c0?b._saveFlag=window.setTimeout(function(){a(b)},b.options.saveInterval):a(b))}},commands:{clearlocaldata:{execCommand:function(a,c){e&&b.getPreferences(e)&&b.removePreferences(e)},notNeedUndo:!0,ignoreContentChange:!0},getlocaldata:{execCommand:function(a,c){return e?b.getPreferences(e)||"":""},notNeedUndo:!0,ignoreContentChange:!0},drafts:{execCommand:function(a,c){e&&(b.body.innerHTML=b.getPreferences(e)||"

              "+domUtils.fillHtml+"

              ",b.focus(!0))},queryCommandState:function(){return e?null===b.getPreferences(e)?-1:0:-1},notNeedUndo:!0,ignoreContentChange:!0}}}}),UE.plugin.register("charts",function(){function a(a){var b=null,c=0;if(a.rows.length<2)return!1;if(a.rows[0].cells.length<2)return!1;b=a.rows[0].cells,c=b.length;for(var d,e=0;d=b[e];e++)if("th"!==d.tagName.toLowerCase())return!1;for(var f,e=1;f=a.rows[e];e++){if(f.cells.length!=c)return!1;if("th"!==f.cells[0].tagName.toLowerCase())return!1;for(var d,g=1;d=f.cells[g];g++){var h=utils.trim(d.innerText||d.textContent||"");if(h=h.replace(new RegExp(UE.dom.domUtils.fillChar,"g"),"").replace(/^\s+|\s+$/g,""),!/^\d*\.?\d+$/.test(h))return!1}}return!0}var b=this;return{bindEvents:{chartserror:function(){}},commands:{charts:{execCommand:function(c,d){var e=domUtils.findParentByTagName(this.selection.getRange().startContainer,"table",!0),f=[],g={};if(!e)return!1;if(!a(e))return b.fireEvent("chartserror"),!1;g.title=d.title||"",g.subTitle=d.subTitle||"",g.xTitle=d.xTitle||"",g.yTitle=d.yTitle||"",g.suffix=d.suffix||"",g.tip=d.tip||"",g.dataFormat=d.tableDataFormat||"",g.chartType=d.chartType||0;for(var h in g)g.hasOwnProperty(h)&&f.push(h+":"+g[h]);e.setAttribute("data-chart",f.join(";")),domUtils.addClass(e,"edui-charts-table"); +},queryCommandState:function(b,c){var d=domUtils.findParentByTagName(this.selection.getRange().startContainer,"table",!0);return d&&a(d)?0:-1}}},inputRule:function(a){utils.each(a.getNodesByTagName("table"),function(a){void 0!==a.getAttr("data-chart")&&a.setAttr("style")})},outputRule:function(a){utils.each(a.getNodesByTagName("table"),function(a){void 0!==a.getAttr("data-chart")&&a.setAttr("style","display: none;")})}}}),UE.plugin.register("section",function(){function a(a){this.tag="",this.level=-1,this.dom=null,this.nextSection=null,this.previousSection=null,this.parentSection=null,this.startAddress=[],this.endAddress=[],this.children=[]}function b(b){var c=new a;return utils.extend(c,b)}function c(a,b){for(var c=b,d=0;dm;m++)if(i=l[m],f=d(i),f>=0){var o=h.selection.getRange().selectNode(i).createAddress(!0).startAddress,p=b({tag:i.tagName,title:i.innerText||i.textContent||"",level:f,dom:i,startAddress:utils.clone(o,[]),endAddress:utils.clone(o,[]),children:[]});for(j.nextSection=p,p.previousSection=j,g=j;f<=g.level;)g=g.parentSection;p.parentSection=g,g.children.push(p),k=j=p}else 1===i.nodeType&&e(i,c),k&&k.endAddress[k.endAddress.length-1]++}for(var f=c||["h1","h2","h3","h4","h5","h6"],g=0;g=c.length);f++){if(c[f]>a[f]){d=!0;break}if(c[f]=c.length);f++){if(c[f]a[f])break}return d&&e}var g,h,i=this;if(b&&d&&-1!=d.level&&(g=e?d.endAddress:d.startAddress,h=c(g,i.body),g&&h&&!f(b.startAddress,b.endAddress,g))){var j,k,l=c(b.startAddress,i.body),m=c(b.endAddress,i.body);if(e)for(j=m;j&&!(domUtils.getPosition(l,j)&domUtils.POSITION_FOLLOWING)&&(k=j.previousSibling,domUtils.insertAfter(h,j),j!=l);)j=k;else for(j=l;j&&!(domUtils.getPosition(j,m)&domUtils.POSITION_FOLLOWING)&&(k=j.nextSibling,h.parentNode.insertBefore(j,h),j!=m);)j=k;i.fireEvent("updateSections")}}},deletesection:{execCommand:function(a,b,c){function d(a){for(var b=e.body,c=0;c',b.className="edui-"+c.options.theme,b.id=c.ui.id+"_iframeupload",i.style.cssText=g,i.style.width=a+"px",i.style.height=e+"px",i.appendChild(b),i.parentNode&&(i.parentNode.style.width=a+"px",i.parentNode.style.height=a+"px");var k=h.getElementById("edui_form_"+j),l=h.getElementById("edui_input_"+j),m=h.getElementById("edui_iframe_"+j);domUtils.on(l,"change",function(){function a(){try{var e,f,g,h=(m.contentDocument||m.contentWindow.document).body,i=h.innerText||h.textContent||"";f=new Function("return "+i)(),e=c.options.imageUrlPrefix+f.url,"SUCCESS"==f.state&&f.url?(g=c.document.getElementById(d),g.setAttribute("src",e),g.setAttribute("_src",e),g.setAttribute("title",f.title||""),g.setAttribute("alt",f.original||""),g.removeAttribute("id"),domUtils.removeClasses(g,"loadingclass")):b&&b(f.state)}catch(j){b&&b(c.getLang("simpleupload.loadError"))}k.reset(),domUtils.un(m,"load",a)}function b(a){if(d){var b=c.document.getElementById(d);b&&domUtils.remove(b),c.fireEvent("showmessage",{id:d,content:a,type:"error",timeout:4e3})}}if(l.value){var d="loading_"+(+new Date).toString(36),e=utils.serializeParam(c.queryCommandValue("serverparam"))||"",f=c.getActionUrl(c.getOpt("imageActionName")),g=c.getOpt("imageAllowFiles");if(c.focus(),c.execCommand("inserthtml",''),!c.getOpt("imageActionName"))return void errorHandler(c.getLang("autoupload.errorLoadConfig"));var h=l.value,i=h?h.substr(h.lastIndexOf(".")):"";if(!i||g&&-1==(g.join("")+".").indexOf(i.toLowerCase()+"."))return void b(c.getLang("simpleupload.exceedTypeError"));domUtils.on(m,"load",a),k.action=utils.formatUrl(f+(-1==f.indexOf("?")?"?":"&")+e),k.submit()}});var n;c.addListener("selectionchange",function(){clearTimeout(n),n=setTimeout(function(){var a=c.queryCommandState("simpleupload");-1==a?l.disabled="disabled":l.disabled=!1},400)}),d=!0}),f.style.cssText=g,b.appendChild(f)}var b,c=this,d=!1;return{bindEvents:{ready:function(){utils.cssRule("loading",".loadingclass{display:inline-block;cursor:default;background: url('"+this.options.themePath+this.options.theme+"/images/loading.gif') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;}\n.loaderrorclass{display:inline-block;cursor:default;background: url('"+this.options.themePath+this.options.theme+"/images/loaderror.png') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;}",this.document)},simpleuploadbtnready:function(d,e){b=e,c.afterConfigReady(a)}},outputRule:function(a){utils.each(a.getNodesByTagName("img"),function(a){/\b(loaderrorclass)|(bloaderrorclass)\b/.test(a.getAttr("class"))&&a.parentNode.removeChild(a)})},commands:{simpleupload:{queryCommandState:function(){return d?0:-1}}}}}),UE.plugin.register("serverparam",function(){var a={};return{commands:{serverparam:{execCommand:function(b,c,d){void 0===c||null===c?a={}:utils.isString(c)?void 0===d||null===d?delete a[c]:a[c]=d:utils.isObject(c)?utils.extend(a,c,!0):utils.isFunction(c)&&utils.extend(a,c(),!0)},queryCommandValue:function(){return a||{}}}}}}),UE.plugin.register("insertfile",function(){function a(a){var b=a.substr(a.lastIndexOf(".")+1).toLowerCase(),c={rar:"icon_rar.gif",zip:"icon_rar.gif",tar:"icon_rar.gif",gz:"icon_rar.gif",bz2:"icon_rar.gif",doc:"icon_doc.gif",docx:"icon_doc.gif",pdf:"icon_pdf.gif",mp3:"icon_mp3.gif",xls:"icon_xls.gif",chm:"icon_chm.gif",ppt:"icon_ppt.gif",pptx:"icon_ppt.gif",avi:"icon_mv.gif",rmvb:"icon_mv.gif",wmv:"icon_mv.gif",flv:"icon_mv.gif",swf:"icon_mv.gif",rm:"icon_mv.gif",exe:"icon_exe.gif",psd:"icon_psd.gif",txt:"icon_txt.gif",jpg:"icon_jpg.gif",png:"icon_jpg.gif",jpeg:"icon_jpg.gif",gif:"icon_jpg.gif",ico:"icon_jpg.gif",bmp:"icon_jpg.gif"};return c[b]?c[b]:c.txt}var b=this;return{commands:{insertfile:{execCommand:function(c,d){d=utils.isArray(d)?d:[d];var e,f,g,h,i="",j=b.getOpt("UEDITOR_HOME_URL"),k=j+("/"==j.substr(j.length-1)?"":"/")+"dialogs/attachment/fileTypeImages/";for(e=0;e'+h+"

              ";b.execCommand("insertHtml",i)}}}}}),UE.plugins.xssFilter=function(){function a(a){var b=a.tagName,d=a.attrs;return c.hasOwnProperty(b)?void UE.utils.each(d,function(d,e){-1===c[b].indexOf(e)&&a.setAttr(e)}):(a.parentNode.removeChild(a),!1)}var b=UEDITOR_CONFIG,c=b.whitList;c&&b.xssFilterRules&&(this.options.filterRules=function(){var b={};return UE.utils.each(c,function(c,d){b[d]=function(b){return a(b)}}),b}());var d=[];UE.utils.each(c,function(a,b){d.push(b)}),c&&b.inputXssFilter&&this.addInputRule(function(b){b.traversal(function(b){return"element"!==b.type?!1:void a(b)})}),c&&b.outputXssFilter&&this.addOutputRule(function(b){b.traversal(function(b){return"element"!==b.type?!1:void a(b)})})};var baidu=baidu||{};baidu.editor=baidu.editor||{},UE.ui=baidu.editor.ui={},function(){function a(){var a=document.getElementById("edui_fixedlayer");i.setViewportOffset(a,{left:0,top:0})}function b(b){d.on(window,"scroll",a),d.on(window,"resize",baidu.editor.utils.defer(a,0,!0))}var c=baidu.editor.browser,d=baidu.editor.dom.domUtils,e="$EDITORUI",f=window[e]={},g="ID"+e,h=0,i=baidu.editor.ui.uiUtils={uid:function(a){return a?a[g]||(a[g]=++h):++h},hook:function(a,b){var c;return a&&a._callbacks?c=a:(c=function(){var b;a&&(b=a.apply(this,arguments));for(var d=c._callbacks,e=d.length;e--;){var f=d[e].apply(this,arguments);void 0===b&&(b=f)}return b},c._callbacks=[]),c._callbacks.push(b),c},createElementByHtml:function(a){var b=document.createElement("div");return b.innerHTML=a,b=b.firstChild,b.parentNode.removeChild(b),b},getViewportElement:function(){return c.ie&&c.quirks?document.body:document.documentElement},getClientRect:function(a){var b;try{b=a.getBoundingClientRect()}catch(c){b={left:0,top:0,height:0,width:0}}for(var e,f={left:Math.round(b.left),top:Math.round(b.top),height:Math.round(b.bottom-b.top),width:Math.round(b.right-b.left)};(e=a.ownerDocument)!==document&&(a=d.getWindow(e).frameElement);)b=a.getBoundingClientRect(),f.left+=b.left,f.top+=b.top;return f.bottom=f.top+f.height,f.right=f.left+f.width,f},getViewportRect:function(){var a=i.getViewportElement(),b=0|(window.innerWidth||a.clientWidth),c=0|(window.innerHeight||a.clientHeight);return{left:0,top:0,height:c,width:b,bottom:c,right:b}},setViewportOffset:function(a,b){var c=i.getFixedLayer();a.parentNode===c?(a.style.left=b.left+"px",a.style.top=b.top+"px"):d.setViewportOffset(a,b)},getEventOffset:function(a){var b=a.target||a.srcElement,c=i.getClientRect(b),d=i.getViewportOffsetByEvent(a);return{left:d.left-c.left,top:d.top-c.top}},getViewportOffsetByEvent:function(a){var b=a.target||a.srcElement,c=d.getWindow(b).frameElement,e={left:a.clientX,top:a.clientY};if(c&&b.ownerDocument!==document){var f=i.getClientRect(c);e.left+=f.left,e.top+=f.top}return e},setGlobal:function(a,b){return f[a]=b,e+'["'+a+'"]'},unsetGlobal:function(a){delete f[a]},copyAttributes:function(a,b){for(var e=b.attributes,f=e.length;f--;){var g=e[f];"style"==g.nodeName||"class"==g.nodeName||c.ie&&!g.specified||a.setAttribute(g.nodeName,g.nodeValue)}b.className&&d.addClass(a,b.className),b.style.cssText&&(a.style.cssText+=";"+b.style.cssText)},removeStyle:function(a,b){if(a.style.removeProperty)a.style.removeProperty(b);else{if(!a.style.removeAttribute)throw"";a.style.removeAttribute(b)}},contains:function(a,b){return a&&b&&(a===b?!1:a.contains?a.contains(b):16&a.compareDocumentPosition(b))},startDrag:function(a,b,c){function d(a){var c=a.clientX-g,d=a.clientY-h;b.ondragmove(c,d,a),a.stopPropagation?a.stopPropagation():a.cancelBubble=!0}function e(a){c.removeEventListener("mousemove",d,!0),c.removeEventListener("mouseup",e,!0),window.removeEventListener("mouseup",e,!0),b.ondragstop()}function f(){i.releaseCapture(),i.detachEvent("onmousemove",d),i.detachEvent("onmouseup",f),i.detachEvent("onlosecaptrue",f),b.ondragstop()}var c=c||document,g=a.clientX,h=a.clientY;if(c.addEventListener)c.addEventListener("mousemove",d,!0),c.addEventListener("mouseup",e,!0),window.addEventListener("mouseup",e,!0),a.preventDefault();else{var i=a.srcElement;i.setCapture(),i.attachEvent("onmousemove",d),i.attachEvent("onmouseup",f),i.attachEvent("onlosecaptrue",f),a.returnValue=!1}b.ondragstart()},getFixedLayer:function(){var d=document.getElementById("edui_fixedlayer");return null==d&&(d=document.createElement("div"),d.id="edui_fixedlayer",document.body.appendChild(d),c.ie&&c.version<=8?(d.style.position="absolute",b(),setTimeout(a)):d.style.position="fixed",d.style.left="0",d.style.top="0",d.style.width="0",d.style.height="0"),d},makeUnselectable:function(a){if(c.opera||c.ie&&c.version<9){if(a.unselectable="on",a.hasChildNodes())for(var b=0;b
              '}},a.inherits(c,b)}(),function(){var a=baidu.editor.utils,b=baidu.editor.dom.domUtils,c=baidu.editor.ui.UIBase,d=baidu.editor.ui.uiUtils,e=baidu.editor.ui.Mask=function(a){this.initOptions(a),this.initUIBase()};e.prototype={getHtmlTpl:function(){return'
              '},postRender:function(){var a=this;b.on(window,"resize",function(){setTimeout(function(){a.isHidden()||a._fill()})})},show:function(a){this._fill(),this.getDom().style.display="",this.getDom().style.zIndex=a},hide:function(){this.getDom().style.display="none",this.getDom().style.zIndex=""},isHidden:function(){return"none"==this.getDom().style.display},_onMouseDown:function(){return!1},_onClick:function(a,b){this.fireEvent("click",a,b)},_fill:function(){var a=this.getDom(),b=d.getViewportRect();a.style.width=b.width+"px",a.style.height=b.height+"px"}},a.inherits(e,c)}(),function(){function a(a,b){for(var c=0;c
              '+this.getContentHtmlTpl()+"
              "},getContentHtmlTpl:function(){return this.content?"string"==typeof this.content?this.content:this.content.renderHtml():""},_UIBase_postRender:e.prototype.postRender,postRender:function(){if(this.content instanceof e&&this.content.postRender(),this.captureWheel&&!this.captured){this.captured=!0;var a=(document.documentElement.clientHeight||document.body.clientHeight)-80,b=this.getDom().offsetHeight,f=c.getClientRect(this.combox.getDom()).top,g=this.getDom("content"),h=this.getDom("body").getElementsByTagName("iframe"),i=this;for(h.length&&(h=h[0]);f+b>a;)b-=30;g.style.height=b+"px",h&&(h.style.height=b+"px"),window.XMLHttpRequest?d.on(g,"onmousewheel"in document.body?"mousewheel":"DOMMouseScroll",function(a){a.preventDefault?a.preventDefault():a.returnValue=!1,a.wheelDelta?g.scrollTop-=a.wheelDelta/120*60:g.scrollTop-=a.detail/-3*60}):d.on(this.getDom(),"mousewheel",function(a){a.returnValue=!1,i.getDom("content").scrollTop-=a.wheelDelta/120*60})}this.fireEvent("postRenderAfter"),this.hide(!0),this._UIBase_postRender()},_doAutoRender:function(){!this.getDom()&&this.autoRender&&this.render()},mesureSize:function(){var a=this.getDom("content");return c.getClientRect(a)},fitSize:function(){if(this.captureWheel&&this.sized)return this.__size;this.sized=!0;var a=this.getDom("body");a.style.width="",a.style.height="";var b=this.mesureSize();if(this.captureWheel){a.style.width=-(-20-b.width)+"px";var c=parseInt(this.getDom("content").style.height,10);!window.isNaN(c)&&(b.height=c)}else a.style.width=b.width+"px";return a.style.height=b.height+"px",this.__size=b,this.captureWheel&&(this.getDom("content").style.overflow="auto"),b},showAnchor:function(a,b){this.showAnchorRect(c.getClientRect(a),b)},showAnchorRect:function(a,b,e){this._doAutoRender();var f=c.getViewportRect();this.getDom().style.visibility="hidden",this._show();var g,i,j,k,l=this.fitSize();b?(g=this.canSideLeft&&a.right+l.width>f.right&&a.left>l.width,i=this.canSideUp&&a.top+l.height>f.bottom&&a.bottom>l.height,j=g?a.left-l.width:a.right,k=i?a.bottom-l.height:a.top):(g=this.canSideLeft&&a.right+l.width>f.right&&a.left>l.width,i=this.canSideUp&&a.top+l.height>f.bottom&&a.bottom>l.height,j=g?a.right-l.width:a.left,k=i?a.top-l.height:a.bottom);var m=this.getDom();c.setViewportOffset(m,{left:j,top:k}),d.removeClasses(m,h),m.className+=" "+h[2*(i?1:0)+(g?1:0)],this.editor&&(m.style.zIndex=1*this.editor.container.style.zIndex+10,baidu.editor.ui.uiUtils.getFixedLayer().style.zIndex=m.style.zIndex-1),this.getDom().style.visibility="visible"},showAt:function(a){var b=a.left,c=a.top,d={left:b,top:c,right:b,bottom:c,height:0,width:0};this.showAnchorRect(d,!1,!0)},_show:function(){if(this._hidden){var a=this.getDom();a.style.display="",this._hidden=!1,this.fireEvent("show")}},isHidden:function(){return this._hidden},show:function(){this._doAutoRender(),this._show()},hide:function(a){!this._hidden&&this.getDom()&&(this.getDom().style.display="none",this._hidden=!0,a||this.fireEvent("hide"))},queryAutoHide:function(a){return!a||!c.contains(this.getDom(),a)}},b.inherits(f,e),d.on(document,"mousedown",function(b){var c=b.target||b.srcElement;a(b,c)}),d.on(window,"scroll",function(b,c){a(b,c)})}(),function(){function a(a,b){for(var c='
              '+a+'
              ',d=0;d"+(60==d?'":"")+""),c+=70>d?'':"";return c+="
              '+b.getLang("themeColor")+'
              '+b.getLang("standardColor")+"
              d||d>=60?"border-width:1px;":d>=10&&20>d?"border-width:1px 1px 0 1px;":"border-width:0 1px 0 1px;")+'">
              "}var b=baidu.editor.utils,c=baidu.editor.ui.UIBase,d=baidu.editor.ui.ColorPicker=function(a){this.initOptions(a),this.noColorText=this.noColorText||this.editor.getLang("clearColor"),this.initUIBase()};d.prototype={getHtmlTpl:function(){return a(this.noColorText,this.editor)},_onTableClick:function(a){var b=a.target||a.srcElement,c=b.getAttribute("data-color");c&&this.fireEvent("pickcolor",c)},_onTableOver:function(a){var b=a.target||a.srcElement,c=b.getAttribute("data-color");c&&(this.getDom("preview").style.backgroundColor=c)},_onTableOut:function(){this.getDom("preview").style.backgroundColor=""},_onPickNoColor:function(){this.fireEvent("picknocolor")}},b.inherits(d,c);var e="ffffff,000000,eeece1,1f497d,4f81bd,c0504d,9bbb59,8064a2,4bacc6,f79646,f2f2f2,7f7f7f,ddd9c3,c6d9f0,dbe5f1,f2dcdb,ebf1dd,e5e0ec,dbeef3,fdeada,d8d8d8,595959,c4bd97,8db3e2,b8cce4,e5b9b7,d7e3bc,ccc1d9,b7dde8,fbd5b5,bfbfbf,3f3f3f,938953,548dd4,95b3d7,d99694,c3d69b,b2a2c7,92cddc,fac08f,a5a5a5,262626,494429,17365d,366092,953734,76923c,5f497a,31859b,e36c09,7f7f7f,0c0c0c,1d1b10,0f243e,244061,632423,4f6128,3f3151,205867,974806,c00000,ff0000,ffc000,ffff00,92d050,00b050,00b0f0,0070c0,002060,7030a0,".split(",")}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.uiUtils,c=baidu.editor.ui.UIBase,d=baidu.editor.ui.TablePicker=function(a){this.initOptions(a),this.initTablePicker()};d.prototype={defaultNumRows:10,defaultNumCols:10,maxNumRows:20,maxNumCols:20,numRows:10,numCols:10,lengthOfCellSide:22,initTablePicker:function(){this.initUIBase()},getHtmlTpl:function(){return'
              '},_UIBase_render:c.prototype.render,render:function(a){this._UIBase_render(a),this.getDom("label").innerHTML="0"+this.editor.getLang("t_row")+" x 0"+this.editor.getLang("t_col")},_track:function(a,b){var c=this.getDom("overlay").style,d=this.lengthOfCellSide;c.width=a*d+"px",c.height=b*d+"px";var e=this.getDom("label");e.innerHTML=a+this.editor.getLang("t_col")+" x "+b+this.editor.getLang("t_row"),this.numCols=a,this.numRows=b},_onMouseOver:function(a,c){var d=a.relatedTarget||a.fromElement;b.contains(c,d)||c===d||(this.getDom("label").innerHTML="0"+this.editor.getLang("t_col")+" x 0"+this.editor.getLang("t_row"),this.getDom("overlay").style.visibility="")},_onMouseOut:function(a,c){var d=a.relatedTarget||a.toElement;b.contains(c,d)||c===d||(this.getDom("label").innerHTML="0"+this.editor.getLang("t_col")+" x 0"+this.editor.getLang("t_row"),this.getDom("overlay").style.visibility="hidden")},_onMouseMove:function(a,c){var d=(this.getDom("overlay").style,b.getEventOffset(a)),e=this.lengthOfCellSide,f=Math.ceil(d.left/e),g=Math.ceil(d.top/e);this._track(f,g)},_onClick:function(){this.fireEvent("picktable",this.numCols,this.numRows)}},a.inherits(d,c)}(),function(){var a=baidu.editor.browser,b=baidu.editor.dom.domUtils,c=baidu.editor.ui.uiUtils,d='onmousedown="$$.Stateful_onMouseDown(event, this);" onmouseup="$$.Stateful_onMouseUp(event, this);"'+(a.ie?' onmouseenter="$$.Stateful_onMouseEnter(event, this);" onmouseleave="$$.Stateful_onMouseLeave(event, this);"':' onmouseover="$$.Stateful_onMouseOver(event, this);" onmouseout="$$.Stateful_onMouseOut(event, this);"');baidu.editor.ui.Stateful={alwalysHoverable:!1,target:null,Stateful_init:function(){this._Stateful_dGetHtmlTpl=this.getHtmlTpl,this.getHtmlTpl=this.Stateful_getHtmlTpl},Stateful_getHtmlTpl:function(){var a=this._Stateful_dGetHtmlTpl();return a.replace(/stateful/g,function(){return d})},Stateful_onMouseEnter:function(a,b){this.target=b,this.isDisabled()&&!this.alwalysHoverable||(this.addState("hover"),this.fireEvent("over"))},Stateful_onMouseLeave:function(a,b){this.isDisabled()&&!this.alwalysHoverable||(this.removeState("hover"),this.removeState("active"),this.fireEvent("out"))},Stateful_onMouseOver:function(a,b){var d=a.relatedTarget;c.contains(b,d)||b===d||this.Stateful_onMouseEnter(a,b)},Stateful_onMouseOut:function(a,b){var d=a.relatedTarget;c.contains(b,d)||b===d||this.Stateful_onMouseLeave(a,b)},Stateful_onMouseDown:function(a,b){this.isDisabled()||this.addState("active")},Stateful_onMouseUp:function(a,b){this.isDisabled()||this.removeState("active")},Stateful_postRender:function(){this.disabled&&!this.hasState("disabled")&&this.addState("disabled")},hasState:function(a){return b.hasClass(this.getStateDom(),"edui-state-"+a)},addState:function(a){this.hasState(a)||(this.getStateDom().className+=" edui-state-"+a)},removeState:function(a){this.hasState(a)&&b.removeClasses(this.getStateDom(),["edui-state-"+a])},getStateDom:function(){return this.getDom("state")},isChecked:function(){return this.hasState("checked")},setChecked:function(a){!this.isDisabled()&&a?this.addState("checked"):this.removeState("checked")},isDisabled:function(){return this.hasState("disabled")},setDisabled:function(a){a?(this.removeState("hover"),this.removeState("checked"),this.removeState("active"),this.addState("disabled")):this.removeState("disabled")}}}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.UIBase,c=baidu.editor.ui.Stateful,d=baidu.editor.ui.Button=function(a){if(a.name){var b=a.name,c=a.cssRules;a.className||(a.className="edui-for-"+b),a.cssRules=".edui-default .edui-for-"+b+" .edui-icon {"+c+"}"}this.initOptions(a),this.initButton()};d.prototype={uiName:"button",label:"",title:"",showIcon:!0,showText:!0,cssRules:"",initButton:function(){this.initUIBase(),this.Stateful_init(),this.cssRules&&a.cssRule("edui-customize-"+this.name+"-style",this.cssRules)},getHtmlTpl:function(){return'
              '+(this.showIcon?'
              ':"")+(this.showText?'
              '+this.label+"
              ":"")+"
              "},postRender:function(){this.Stateful_postRender(),this.setDisabled(this.disabled)},_onMouseDown:function(a){var b=a.target||a.srcElement,c=b&&b.tagName&&b.tagName.toLowerCase();return"input"==c||"object"==c||"object"==c?!1:void 0},_onClick:function(){this.isDisabled()||this.fireEvent("click")},setTitle:function(a){var b=this.getDom("label");b.innerHTML=a}},a.inherits(d,b),a.extend(d.prototype,c)}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.uiUtils,c=(baidu.editor.dom.domUtils,baidu.editor.ui.UIBase),d=baidu.editor.ui.Stateful,e=baidu.editor.ui.SplitButton=function(a){this.initOptions(a),this.initSplitButton()};e.prototype={popup:null,uiName:"splitbutton",title:"",initSplitButton:function(){this.initUIBase(),this.Stateful_init();if(null!=this.popup){var a=this.popup;this.popup=null,this.setPopup(a)}},_UIBase_postRender:c.prototype.postRender,postRender:function(){this.Stateful_postRender(),this._UIBase_postRender()},setPopup:function(c){this.popup!==c&&(null!=this.popup&&this.popup.dispose(),c.addListener("show",a.bind(this._onPopupShow,this)),c.addListener("hide",a.bind(this._onPopupHide,this)),c.addListener("postrender",a.bind(function(){c.getDom("body").appendChild(b.createElementByHtml('
              ')),c.getDom().className+=" "+this.className},this)),this.popup=c)},_onPopupShow:function(){this.addState("opened")},_onPopupHide:function(){this.removeState("opened")},getHtmlTpl:function(){return'
              '},showPopup:function(){var a=b.getClientRect(this.getDom());a.top-=this.popup.SHADOW_RADIUS,a.height+=this.popup.SHADOW_RADIUS,this.popup.showAnchorRect(a)},_onArrowClick:function(a,b){this.isDisabled()||this.showPopup()},_onButtonClick:function(){this.isDisabled()||this.fireEvent("buttonclick")}},a.inherits(e,c),a.extend(e.prototype,d,!0)}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.uiUtils,c=baidu.editor.ui.ColorPicker,d=baidu.editor.ui.Popup,e=baidu.editor.ui.SplitButton,f=baidu.editor.ui.ColorButton=function(a){this.initOptions(a),this.initColorButton()};f.prototype={initColorButton:function(){var a=this;this.popup=new d({content:new c({noColorText:a.editor.getLang("clearColor"),editor:a.editor,onpickcolor:function(b,c){a._onPickColor(c)},onpicknocolor:function(b,c){a._onPickNoColor(c)}}),editor:a.editor}),this.initSplitButton()},_SplitButton_postRender:e.prototype.postRender,postRender:function(){this._SplitButton_postRender(),this.getDom("button_body").appendChild(b.createElementByHtml('
              ')), +this.getDom().className+=" edui-colorbutton"},setColor:function(a){this.getDom("colorlump").style.backgroundColor=a,this.color=a},_onPickColor:function(a){this.fireEvent("pickcolor",a)!==!1&&(this.setColor(a),this.popup.hide())},_onPickNoColor:function(a){this.fireEvent("picknocolor")!==!1&&this.popup.hide()}},a.inherits(f,e)}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.Popup,c=baidu.editor.ui.TablePicker,d=baidu.editor.ui.SplitButton,e=baidu.editor.ui.TableButton=function(a){this.initOptions(a),this.initTableButton()};e.prototype={initTableButton:function(){var a=this;this.popup=new b({content:new c({editor:a.editor,onpicktable:function(b,c,d){a._onPickTable(c,d)}}),editor:a.editor}),this.initSplitButton()},_onPickTable:function(a,b){this.fireEvent("picktable",a,b)!==!1&&this.popup.hide()}},a.inherits(e,d)}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.UIBase,c=baidu.editor.ui.AutoTypeSetPicker=function(a){this.initOptions(a),this.initAutoTypeSetPicker()};c.prototype={initAutoTypeSetPicker:function(){this.initUIBase()},getHtmlTpl:function(){var a=this.editor,b=a.options.autotypeset,c=a.getLang("autoTypeSet"),d="textAlignValue"+a.uid,e="imageBlockLineValue"+a.uid,f="symbolConverValue"+a.uid;return'
              "+c.mergeLine+'"+c.delLine+'
              "+c.removeFormat+'"+c.indent+'
              "+c.alignment+'"+a.getLang("justifyleft")+'"+a.getLang("justifycenter")+'"+a.getLang("justifyright")+'
              "+c.imageFloat+'"+a.getLang("default")+'"+a.getLang("justifyleft")+'"+a.getLang("justifycenter")+'"+a.getLang("justifyright")+'
              "+c.removeFontsize+'"+c.removeFontFamily+'
              "+c.removeHtml+'
              "+c.pasteFilter+'
              "+c.symbol+'"+c.bdc2sb+'"+c.tobdc+'
              "},_UIBase_render:b.prototype.render},a.inherits(c,b)}(),function(){function a(a){for(var c,d={},e=a.getDom(),f=a.editor.uid,g=null,h=null,i=domUtils.getElementsByTagName(e,"input"),j=i.length-1;c=i[j--];)if(g=c.getAttribute("type"),"checkbox"==g)if(h=c.getAttribute("name"),d[h]&&delete d[h],c.checked){var k=document.getElementById(h+"Value"+f);if(k){if(/input/gi.test(k.tagName))d[h]=k.value;else for(var l,m=k.getElementsByTagName("input"),n=m.length-1;l=m[n--];)if(l.checked){d[h]=l.value;break}}else d[h]=!0}else d[h]=!1;else d[c.getAttribute("value")]=c.checked;for(var o,p=domUtils.getElementsByTagName(e,"select"),j=0;o=p[j++];){var q=o.getAttribute("name");d[q]=d[q]?o.value:""}b.extend(a.editor.options.autotypeset,d),a.editor.setPreferences("autotypeset",d)}var b=baidu.editor.utils,c=baidu.editor.ui.Popup,d=baidu.editor.ui.AutoTypeSetPicker,e=baidu.editor.ui.SplitButton,f=baidu.editor.ui.AutoTypeSetButton=function(a){this.initOptions(a),this.initAutoTypeSetButton()};f.prototype={initAutoTypeSetButton:function(){var b=this;this.popup=new c({content:new d({editor:b.editor}),editor:b.editor,hide:function(){!this._hidden&&this.getDom()&&(a(this),this.getDom().style.display="none",this._hidden=!0,this.fireEvent("hide"))}});var e=0;this.popup.addListener("postRenderAfter",function(){var c=this;if(!e){var d=this.getDom(),f=d.getElementsByTagName("button")[0];f.onclick=function(){a(c),b.editor.execCommand("autotypeset"),c.hide()},domUtils.on(d,"click",function(d){var e=d.target||d.srcElement,f=b.editor.uid;if(e&&"INPUT"==e.tagName){if("imageBlockLine"==e.name||"textAlign"==e.name||"symbolConver"==e.name)for(var g=e.checked,h=document.getElementById(e.name+"Value"+f),i=h.getElementsByTagName("input"),j={imageBlockLine:"none",textAlign:"left",symbolConver:"tobdc"},k=0;kf;f++)c=this.selectedIndex===f?' class="edui-cellalign-selected" ':"",d=f%3,0===d&&e.push(""),e.push('
              '),2===d&&e.push("");return'
              '+e.join("")+"
              "},getStateDom:function(){return this.target},_onClick:function(a){var c=a.target||a.srcElement;/icon/.test(c.className)&&(this.items[c.parentNode.getAttribute("index")].onclick(),b.postHide(a))},_UIBase_render:d.prototype.render},a.inherits(e,d),a.extend(e.prototype,c,!0)}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.Stateful,c=baidu.editor.ui.uiUtils,d=baidu.editor.ui.UIBase,e=baidu.editor.ui.PastePicker=function(a){this.initOptions(a),this.initPastePicker()};e.prototype={initPastePicker:function(){this.initUIBase(),this.Stateful_init()},getHtmlTpl:function(){return'
              '+this.editor.getLang("pasteOpt")+'
              '},getStateDom:function(){return this.target},format:function(a){this.editor.ui._isTransfer=!0,this.editor.fireEvent("pasteTransfer",a)},_onClick:function(a){var b=domUtils.getNextDomNode(a),d=c.getViewportRect().height,e=c.getClientRect(b);e.top+e.height>d?b.style.top=-e.height-a.offsetHeight+"px":b.style.top="",/hidden/gi.test(domUtils.getComputedStyle(b,"visibility"))?(b.style.visibility="visible",domUtils.addClass(a,"edui-state-opened")):(b.style.visibility="hidden",domUtils.removeClasses(a,"edui-state-opened"))},_UIBase_render:d.prototype.render},a.inherits(e,d),a.extend(e.prototype,b,!0)}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.uiUtils,c=baidu.editor.ui.UIBase,d=baidu.editor.ui.Toolbar=function(a){this.initOptions(a),this.initToolbar()};d.prototype={items:null,initToolbar:function(){this.items=this.items||[],this.initUIBase()},add:function(a,b){void 0===b?this.items.push(a):this.items.splice(b,0,a)},getHtmlTpl:function(){for(var a=[],b=0;b'+a.join("")+"
              "},postRender:function(){for(var a=this.getDom(),c=0;c
              '},postRender:function(){},queryAutoHide:function(){return!0}};h.prototype={items:null,uiName:"menu",initMenu:function(){this.items=this.items||[],this.initPopup(),this.initItems()},initItems:function(){for(var a=0;a'+a.join("")+"
              "},_Popup_postRender:e.prototype.postRender,postRender:function(){for(var a=this,d=0;d
              '+this.renderLabelHtml()+"
              "},postRender:function(){var a=this;this.addListener("over",function(){a.ownerMenu.fireEvent("submenuover",a),a.subMenu&&a.delayShowSubMenu()}),this.subMenu&&(this.getDom().className+=" edui-hassubmenu",this.subMenu.render(),this.addListener("out",function(){a.delayHideSubMenu()}),this.subMenu.addListener("over",function(){clearTimeout(a._closingTimer),a._closingTimer=null,a.addState("opened")}),this.ownerMenu.addListener("hide",function(){a.hideSubMenu()}),this.ownerMenu.addListener("submenuover",function(b,c){c!==a&&a.delayHideSubMenu()}),this.subMenu._bakQueryAutoHide=this.subMenu.queryAutoHide,this.subMenu.queryAutoHide=function(b){return b&&c.contains(a.getDom(),b)?!1:this._bakQueryAutoHide(b)}),this.getDom().style.tabIndex="-1",c.makeUnselectable(this.getDom()),this.Stateful_postRender()},delayShowSubMenu:function(){var a=this;a.isDisabled()||(a.addState("opened"),clearTimeout(a._showingTimer),clearTimeout(a._closingTimer),a._closingTimer=null,a._showingTimer=setTimeout(function(){a.showSubMenu()},250))},delayHideSubMenu:function(){var a=this;a.isDisabled()||(a.removeState("opened"),clearTimeout(a._showingTimer),a._closingTimer||(a._closingTimer=setTimeout(function(){a.hasState("opened")||a.hideSubMenu(),a._closingTimer=null},400)))},renderLabelHtml:function(){return'
              '+(this.label||"")+"
              "},getStateDom:function(){return this.getDom()},queryAutoHide:function(a){return this.subMenu&&this.hasState("opened")?this.subMenu.queryAutoHide(a):void 0},_onClick:function(a,b){this.hasState("disabled")||this.fireEvent("click",a,b)!==!1&&(this.subMenu?this.showSubMenu():e.postHide(a))},showSubMenu:function(){var a=c.getClientRect(this.getDom());a.right-=5,a.left+=2,a.width-=7,a.top-=4,a.bottom+=4,a.height+=8,this.subMenu.showAnchorRect(a,!0,!0)},hideSubMenu:function(){this.subMenu.hide()}},a.inherits(j,d),a.extend(j.prototype,f,!0)}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.uiUtils,c=baidu.editor.ui.Menu,d=baidu.editor.ui.SplitButton,e=baidu.editor.ui.Combox=function(a){this.initOptions(a),this.initCombox()};e.prototype={uiName:"combox",onbuttonclick:function(){this.showPopup()},initCombox:function(){var a=this;this.items=this.items||[];for(var b=0;bd.right&&(g=d.right-e.width);var h=a.top;h+e.height>d.bottom&&(h=d.bottom-e.height),c.style.left=Math.max(g,0)+"px",c.style.top=Math.max(h,0)+"px"},showAtCenter:function(){var a=f.getViewportRect();if(this.fullscreen){var b=this.getDom(),c=this.getDom("content");b.style.display="block";var d=UE.ui.uiUtils.getClientRect(b),g=UE.ui.uiUtils.getClientRect(c);b.style.left="-100000px",c.style.width=a.width-d.width+g.width+"px",c.style.height=a.height-d.height+g.height+"px",b.style.width=a.width+"px",b.style.height=a.height+"px",b.style.left=0,this._originalContext={html:{overflowX:document.documentElement.style.overflowX,overflowY:document.documentElement.style.overflowY},body:{overflowX:document.body.style.overflowX,overflowY:document.body.style.overflowY}},document.documentElement.style.overflowX="hidden",document.documentElement.style.overflowY="hidden",document.body.style.overflowX="hidden",document.body.style.overflowY="hidden"}else{this.getDom().style.display="";var h=this.fitSize(),i=0|this.getDom("titlebar").offsetHeight,j=a.width/2-h.width/2,k=a.height/2-(h.height-i)/2-i,l=this.getDom();this.safeSetOffset({left:Math.max(0|j,0),top:Math.max(0|k,0)}),e.hasClass(l,"edui-state-centered")||(l.className+=" edui-state-centered")}this._show()},getContentHtml:function(){var a="";return"string"==typeof this.content?a=this.content:this.iframeUrl&&(a=''),a},getHtmlTpl:function(){var a="";if(this.buttons){for(var b=[],c=0;c
              '+b.join("")+"
              "}return'
              '+(this.title||"")+"
              "+this.closeButton.renderHtml()+'
              '+(this.autoReset?"":this.getContentHtml())+"
              "+a+"
              "},postRender:function(){this.modalMask.getDom()||(this.modalMask.render(),this.modalMask.hide()),this.dragMask.getDom()||(this.dragMask.render(),this.dragMask.hide());var a=this;if(this.addListener("show",function(){a.modalMask.show(this.getDom().style.zIndex-2)}),this.addListener("hide",function(){a.modalMask.hide()}),this.buttons)for(var b=0;b',a.editor.container.style.zIndex&&(this.getDom().style.zIndex=1*a.editor.container.style.zIndex+1))}}),this.onbuttonclick=function(){this.showPopup()},this.initSplitButton()}},a.inherits(d,c)}(),function(){function a(a){var b=a.target||a.srcElement,c=g.findParent(b,function(a){return g.hasClass(a,"edui-shortcutmenu")||g.hasClass(a,"edui-popup")},!0);if(!c)for(var d,e=0;d=h[e++];)d.hide()}var b,c=baidu.editor.ui,d=c.UIBase,e=c.uiUtils,f=baidu.editor.utils,g=baidu.editor.dom.domUtils,h=[],i=!1,j=c.ShortCutMenu=function(a){this.initOptions(a),this.initShortCutMenu()};j.postHide=a,j.prototype={isHidden:!0,SPACE:5,initShortCutMenu:function(){this.items=this.items||[],this.initUIBase(),this.initItems(),this.initEvent(),h.push(this)},initEvent:function(){var a=this,c=a.editor.document;g.on(c,"mousemove",function(c){if(a.isHidden===!1){if(a.getSubMenuMark()||"contextmenu"==a.eventType)return;var d=!0,e=a.getDom(),f=e.offsetWidth,g=e.offsetHeight,h=f/2+a.SPACE,i=g/2,j=Math.abs(c.screenX-a.left),k=Math.abs(c.screenY-a.top);clearTimeout(b),b=setTimeout(function(){k>0&&i>k?a.setOpacity(e,"1"):k>i&&i+70>k?(a.setOpacity(e,"0.5"),d=!1):k>i+70&&i+140>k&&a.hide(),d&&j>0&&h>j?a.setOpacity(e,"1"):j>h&&h+70>j?a.setOpacity(e,"0.5"):j>h+70&&h+140>j&&a.hide()})}}),browser.chrome&&g.on(c,"mouseout",function(b){var c=b.relatedTarget||b.toElement;null!=c&&"HTML"!=c.tagName||a.hide()}),a.editor.addListener("afterhidepop",function(){a.isHidden||(i=!0)})},initItems:function(){if(f.isArray(this.items))for(var a=0,b=this.items.length;b>a;a++){var d=this.items[a].toLowerCase();c[d]&&(this.items[a]=new c[d](this.editor),this.items[a].className+=" edui-shortcutsubmenu ")}},setOpacity:function(a,b){browser.ie&&browser.version<9?a.style.filter="alpha(opacity = "+100*parseFloat(b)+");":a.style.opacity=b},getSubMenuMark:function(){i=!1;for(var a,b=e.getFixedLayer(),c=g.getElementsByTagName(b,"div",function(a){return g.hasClass(a,"edui-shortcutsubmenu edui-popup")}),d=0;a=c[d++];)"none"!=a.style.display&&(i=!0);return i},show:function(a,b){function c(a){a.left<0&&(a.left=0),a.top<0&&(a.top=0),i.style.cssText="position:absolute;left:"+a.left+"px;top:"+a.top+"px;"}function d(a){a.tagName||(a=a.getDom()),h.left=parseInt(a.style.left),h.top=parseInt(a.style.top),h.top-=i.offsetHeight+15,c(h)}var f=this,h={},i=this.getDom(),j=e.getFixedLayer();if(f.eventType=a.type,i.style.cssText="display:block;left:-9999px","contextmenu"==a.type&&b){var k=g.getElementsByTagName(j,"div","edui-contextmenu")[0];k?d(k):f.editor.addListener("aftershowcontextmenu",function(a,b){d(b)})}else h=e.getViewportOffsetByEvent(a),h.top-=i.offsetHeight+f.SPACE,h.left+=f.SPACE+20,c(h),f.setOpacity(i,.2);f.isHidden=!1,f.left=a.screenX+i.offsetWidth/2-f.SPACE,f.top=a.screenY-i.offsetHeight/2-f.SPACE,f.editor&&(i.style.zIndex=1*f.editor.container.style.zIndex+10,j.style.zIndex=i.style.zIndex-1)},hide:function(){this.getDom()&&(this.getDom().style.display="none"),this.isHidden=!0},postRender:function(){if(f.isArray(this.items))for(var a,b=0;a=this.items[b++];)a.postRender()},getHtmlTpl:function(){var a;if(f.isArray(this.items)){a=[];for(var b=0;b'+a+"
              "}},f.inherits(j,d),g.on(document,"mousedown",function(b){a(b)}),g.on(window,"scroll",function(b){a(b)})}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.UIBase,c=baidu.editor.ui.Breakline=function(a){this.initOptions(a),this.initSeparator()};c.prototype={uiName:"Breakline",initSeparator:function(){this.initUIBase()},getHtmlTpl:function(){return"
              "}},a.inherits(c,b)}(),function(){var a=baidu.editor.utils,b=baidu.editor.dom.domUtils,c=baidu.editor.ui.UIBase,d=baidu.editor.ui.Message=function(a){this.initOptions(a),this.initMessage()};d.prototype={initMessage:function(){this.initUIBase()},getHtmlTpl:function(){return'
              ×
              '},reset:function(a){var b=this;a.keepshow||(clearTimeout(this.timer),b.timer=setTimeout(function(){b.hide()},a.timeout||4e3)),void 0!==a.content&&b.setContent(a.content),void 0!==a.type&&b.setType(a.type),b.show()},postRender:function(){var a=this,c=this.getDom("closer");c&&b.on(c,"click",function(){a.hide()})},setContent:function(a){this.getDom("content").innerHTML=a},setType:function(a){a=a||"info";var b=this.getDom("body");b.className=b.className.replace(/edui-message-type-[\w-]+/,"edui-message-type-"+a)},getContent:function(){return this.getDom("content").innerHTML},getType:function(){var a=this.getDom("body").match(/edui-message-type-([\w-]+)/);return a?a[1]:""},show:function(){this.getDom().style.display="block"},hide:function(){var a=this.getDom();a&&(a.style.display="none",a.parentNode&&a.parentNode.removeChild(a))}},a.inherits(d,c)}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui,c=b.Dialog;b.buttons={},b.Dialog=function(a){var b=new c(a);return b.addListener("hide",function(){if(b.editor){var a=b.editor;try{if(browser.gecko){var c=a.window.scrollY,d=a.window.scrollX;a.body.focus(),a.window.scrollTo(d,c)}else a.focus()}catch(e){}}}),b};for(var d,e={anchor:"~/dialogs/anchor/anchor.html",insertimage:"~/dialogs/image/image.html",link:"~/dialogs/link/link.html",spechars:"~/dialogs/spechars/spechars.html",searchreplace:"~/dialogs/searchreplace/searchreplace.html",map:"~/dialogs/map/map.html",gmap:"~/dialogs/gmap/gmap.html",insertvideo:"~/dialogs/video/video.html",help:"~/dialogs/help/help.html",preview:"~/dialogs/preview/preview.html",emotion:"~/dialogs/emotion/emotion.html",wordimage:"~/dialogs/wordimage/wordimage.html",attachment:"~/dialogs/attachment/attachment.html",insertframe:"~/dialogs/insertframe/insertframe.html",edittip:"~/dialogs/table/edittip.html",edittable:"~/dialogs/table/edittable.html",edittd:"~/dialogs/table/edittd.html",webapp:"~/dialogs/webapp/webapp.html",snapscreen:"~/dialogs/snapscreen/snapscreen.html",scrawl:"~/dialogs/scrawl/scrawl.html",music:"~/dialogs/music/music.html",template:"~/dialogs/template/template.html",background:"~/dialogs/background/background.html",charts:"~/dialogs/charts/charts.html"},f=["undo","redo","formatmatch","bold","italic","underline","fontborder","touppercase","tolowercase","strikethrough","subscript","superscript","source","indent","outdent","blockquote","pasteplain","pagebreak","selectall","print","horizontal","removeformat","time","date","unlink","insertparagraphbeforetable","insertrow","insertcol","mergeright","mergedown","deleterow","deletecol","splittorows","splittocols","splittocells","mergecells","deletetable","drafts"],g=0;d=f[g++];)d=d.toLowerCase(),b[d]=function(a){return function(c){var d=new b.Button({className:"edui-for-"+a,title:c.options.labelMap[a]||c.getLang("labelMap."+a)||"",onclick:function(){c.execCommand(a)},theme:c.options.theme,showText:!1});return b.buttons[a]=d,c.addListener("selectionchange",function(b,e,f){var g=c.queryCommandState(a);-1==g?(d.setDisabled(!0),d.setChecked(!1)):f||(d.setDisabled(!1),d.setChecked(g))}),d}}(d);b.cleardoc=function(a){var c=new b.Button({className:"edui-for-cleardoc",title:a.options.labelMap.cleardoc||a.getLang("labelMap.cleardoc")||"",theme:a.options.theme,onclick:function(){confirm(a.getLang("confirmClear"))&&a.execCommand("cleardoc")}});return b.buttons.cleardoc=c,a.addListener("selectionchange",function(){c.setDisabled(-1==a.queryCommandState("cleardoc"))}),c};var h={justify:["left","right","center","justify"],imagefloat:["none","left","center","right"],directionality:["ltr","rtl"]};for(var i in h)!function(a,c){for(var d,e=0;d=c[e++];)!function(c){ +b[a.replace("float","")+c]=function(d){var e=new b.Button({className:"edui-for-"+a.replace("float","")+c,title:d.options.labelMap[a.replace("float","")+c]||d.getLang("labelMap."+a.replace("float","")+c)||"",theme:d.options.theme,onclick:function(){d.execCommand(a,c)}});return b.buttons[a]=e,d.addListener("selectionchange",function(b,f,g){e.setDisabled(-1==d.queryCommandState(a)),e.setChecked(d.queryCommandValue(a)==c&&!g)}),e}}(d)}(i,h[i]);for(var d,g=0;d=["backcolor","forecolor"][g++];)b[d]=function(a){return function(c){var d=new b.ColorButton({className:"edui-for-"+a,color:"default",title:c.options.labelMap[a]||c.getLang("labelMap."+a)||"",editor:c,onpickcolor:function(b,d){c.execCommand(a,d)},onpicknocolor:function(){c.execCommand(a,"default"),this.setColor("transparent"),this.color="default"},onbuttonclick:function(){c.execCommand(a,this.color)}});return b.buttons[a]=d,c.addListener("selectionchange",function(){d.setDisabled(-1==c.queryCommandState(a))}),d}}(d);var j={noOk:["searchreplace","help","spechars","webapp","preview"],ok:["attachment","anchor","link","insertimage","map","gmap","insertframe","wordimage","insertvideo","insertframe","edittip","edittable","edittd","scrawl","template","music","background","charts"]};for(var i in j)!function(c,d){for(var f,g=0;f=d[g++];)browser.opera&&"searchreplace"===f||!function(d){b[d]=function(f,g,h){g=g||(f.options.iframeUrlMap||{})[d]||e[d],h=f.options.labelMap[d]||f.getLang("labelMap."+d)||"";var i;g&&(i=new b.Dialog(a.extend({iframeUrl:f.ui.mapUrl(g),editor:f,className:"edui-for-"+d,title:h,holdScroll:"insertimage"===d,fullscreen:/charts|preview/.test(d),closeDialog:f.getLang("closeDialog")},"ok"==c?{buttons:[{className:"edui-okbutton",label:f.getLang("ok"),editor:f,onclick:function(){i.close(!0)}},{className:"edui-cancelbutton",label:f.getLang("cancel"),editor:f,onclick:function(){i.close(!1)}}]}:{})),f.ui._dialogs[d+"Dialog"]=i);var j=new b.Button({className:"edui-for-"+d,title:h,onclick:function(){if(i)switch(d){case"wordimage":var a=f.execCommand("wordimage");a&&a.length&&(i.render(),i.open());break;case"scrawl":-1!=f.queryCommandState("scrawl")&&(i.render(),i.open());break;default:i.render(),i.open()}},theme:f.options.theme,disabled:"scrawl"==d&&-1==f.queryCommandState("scrawl")||"charts"==d});return b.buttons[d]=j,f.addListener("selectionchange",function(){var a={edittable:1};if(!(d in a)){var b=f.queryCommandState(d);j.getDom()&&(j.setDisabled(-1==b),j.setChecked(b))}}),j}}(f.toLowerCase())}(i,j[i]);b.snapscreen=function(a,c,d){d=a.options.labelMap.snapscreen||a.getLang("labelMap.snapscreen")||"";var f=new b.Button({className:"edui-for-snapscreen",title:d,onclick:function(){a.execCommand("snapscreen")},theme:a.options.theme});if(b.buttons.snapscreen=f,c=c||(a.options.iframeUrlMap||{}).snapscreen||e.snapscreen){var g=new b.Dialog({iframeUrl:a.ui.mapUrl(c),editor:a,className:"edui-for-snapscreen",title:d,buttons:[{className:"edui-okbutton",label:a.getLang("ok"),editor:a,onclick:function(){g.close(!0)}},{className:"edui-cancelbutton",label:a.getLang("cancel"),editor:a,onclick:function(){g.close(!1)}}]});g.render(),a.ui._dialogs.snapscreenDialog=g}return a.addListener("selectionchange",function(){f.setDisabled(-1==a.queryCommandState("snapscreen"))}),f},b.insertcode=function(c,d,e){d=c.options.insertcode||[],e=c.options.labelMap.insertcode||c.getLang("labelMap.insertcode")||"";var f=[];a.each(d,function(a,b){f.push({label:a,value:b,theme:c.options.theme,renderLabelHtml:function(){return'
              '+(this.label||"")+"
              "}})});var g=new b.Combox({editor:c,items:f,onselect:function(a,b){c.execCommand("insertcode",this.items[b].value)},onbuttonclick:function(){this.showPopup()},title:e,initValue:e,className:"edui-for-insertcode",indexByValue:function(a){if(a)for(var b,c=0;b=this.items[c];c++)if(-1!=b.value.indexOf(a))return c;return-1}});return b.buttons.insertcode=g,c.addListener("selectionchange",function(a,b,d){if(!d){var f=c.queryCommandState("insertcode");if(-1==f)g.setDisabled(!0);else{g.setDisabled(!1);var h=c.queryCommandValue("insertcode");if(!h)return void g.setValue(e);h&&(h=h.replace(/['"]/g,"").split(",")[0]),g.setValue(h)}}}),g},b.fontfamily=function(c,d,e){if(d=c.options.fontfamily||[],e=c.options.labelMap.fontfamily||c.getLang("labelMap.fontfamily")||"",d.length){for(var f,g=0,h=[];f=d[g];g++){var i=c.getLang("fontfamily")[f.name]||"";!function(b,d){h.push({label:b,value:d,theme:c.options.theme,renderLabelHtml:function(){return'
              '+(this.label||"")+"
              "}})}(f.label||i,f.val)}var j=new b.Combox({editor:c,items:h,onselect:function(a,b){c.execCommand("FontFamily",this.items[b].value)},onbuttonclick:function(){this.showPopup()},title:e,initValue:e,className:"edui-for-fontfamily",indexByValue:function(a){if(a)for(var b,c=0;b=this.items[c];c++)if(-1!=b.value.indexOf(a))return c;return-1}});return b.buttons.fontfamily=j,c.addListener("selectionchange",function(a,b,d){if(!d){var e=c.queryCommandState("FontFamily");if(-1==e)j.setDisabled(!0);else{j.setDisabled(!1);var f=c.queryCommandValue("FontFamily");f&&(f=f.replace(/['"]/g,"").split(",")[0]),j.setValue(f)}}}),j}},b.fontsize=function(a,c,d){if(d=a.options.labelMap.fontsize||a.getLang("labelMap.fontsize")||"",c=c||a.options.fontsize||[],c.length){for(var e=[],f=0;f'+(this.label||"")+"
              "}})}var h=new b.Combox({editor:a,items:e,title:d,initValue:d,onselect:function(b,c){a.execCommand("FontSize",this.items[c].value)},onbuttonclick:function(){this.showPopup()},className:"edui-for-fontsize"});return b.buttons.fontsize=h,a.addListener("selectionchange",function(b,c,d){if(!d){var e=a.queryCommandState("FontSize");-1==e?h.setDisabled(!0):(h.setDisabled(!1),h.setValue(a.queryCommandValue("FontSize")))}}),h}},b.paragraph=function(c,d,e){if(e=c.options.labelMap.paragraph||c.getLang("labelMap.paragraph")||"",d=c.options.paragraph||[],!a.isEmptyObject(d)){var f=[];for(var g in d)f.push({value:g,label:d[g]||c.getLang("paragraph")[g],theme:c.options.theme,renderLabelHtml:function(){return'
              '+(this.label||"")+"
              "}});var h=new b.Combox({editor:c,items:f,title:e,initValue:e,className:"edui-for-paragraph",onselect:function(a,b){c.execCommand("Paragraph",this.items[b].value)},onbuttonclick:function(){this.showPopup()}});return b.buttons.paragraph=h,c.addListener("selectionchange",function(a,b,d){if(!d){var e=c.queryCommandState("Paragraph");if(-1==e)h.setDisabled(!0);else{h.setDisabled(!1);var f=c.queryCommandValue("Paragraph"),g=h.indexByValue(f);-1!=g?h.setValue(f):h.setValue(h.initValue)}}}),h}},b.customstyle=function(a){var c=a.options.customstyle||[],d=a.options.labelMap.customstyle||a.getLang("labelMap.customstyle")||"";if(c.length){for(var e,f=a.getLang("customstyle"),g=0,h=[];e=c[g++];)!function(b){var c={};c.label=b.label?b.label:f[b.name],c.style=b.style,c.className=b.className,c.tag=b.tag,h.push({label:c.label,value:c,theme:a.options.theme,renderLabelHtml:function(){return'
              <'+c.tag+" "+(c.className?' class="'+c.className+'"':"")+(c.style?' style="'+c.style+'"':"")+">"+c.label+"
              "}})}(e);var i=new b.Combox({editor:a,items:h,title:d,initValue:d,className:"edui-for-customstyle",onselect:function(b,c){a.execCommand("customstyle",this.items[c].value)},onbuttonclick:function(){this.showPopup()},indexByValue:function(a){for(var b,c=0;b=this.items[c++];)if(b.label==a)return c-1;return-1}});return b.buttons.customstyle=i,a.addListener("selectionchange",function(b,c,d){if(!d){var e=a.queryCommandState("customstyle");if(-1==e)i.setDisabled(!0);else{i.setDisabled(!1);var f=a.queryCommandValue("customstyle"),g=i.indexByValue(f);-1!=g?i.setValue(f):i.setValue(i.initValue)}}}),i}},b.inserttable=function(a,c,d){d=a.options.labelMap.inserttable||a.getLang("labelMap.inserttable")||"";var e=new b.TableButton({editor:a,title:d,className:"edui-for-inserttable",onpicktable:function(b,c,d){a.execCommand("InsertTable",{numRows:d,numCols:c,border:1})},onbuttonclick:function(){this.showPopup()}});return b.buttons.inserttable=e,a.addListener("selectionchange",function(){e.setDisabled(-1==a.queryCommandState("inserttable"))}),e},b.lineheight=function(a){var c=a.options.lineheight||[];if(c.length){for(var d,e=0,f=[];d=c[e++];)f.push({label:d,value:d,theme:a.options.theme,onclick:function(){a.execCommand("lineheight",this.value)}});var g=new b.MenuButton({editor:a,className:"edui-for-lineheight",title:a.options.labelMap.lineheight||a.getLang("labelMap.lineheight")||"",items:f,onbuttonclick:function(){var b=a.queryCommandValue("LineHeight")||this.value;a.execCommand("LineHeight",b)}});return b.buttons.lineheight=g,a.addListener("selectionchange",function(){var b=a.queryCommandState("LineHeight");if(-1==b)g.setDisabled(!0);else{g.setDisabled(!1);var c=a.queryCommandValue("LineHeight");c&&g.setValue((c+"").replace(/cm/,"")),g.setChecked(b)}}),g}};for(var k,l=["top","bottom"],m=0;k=l[m++];)!function(a){b["rowspacing"+a]=function(c){var d=c.options["rowspacing"+a]||[];if(!d.length)return null;for(var e,f=0,g=[];e=d[f++];)g.push({label:e,value:e,theme:c.options.theme,onclick:function(){c.execCommand("rowspacing",this.value,a)}});var h=new b.MenuButton({editor:c,className:"edui-for-rowspacing"+a,title:c.options.labelMap["rowspacing"+a]||c.getLang("labelMap.rowspacing"+a)||"",items:g,onbuttonclick:function(){var b=c.queryCommandValue("rowspacing",a)||this.value;c.execCommand("rowspacing",b,a)}});return b.buttons[a]=h,c.addListener("selectionchange",function(){var b=c.queryCommandState("rowspacing",a);if(-1==b)h.setDisabled(!0);else{h.setDisabled(!1);var d=c.queryCommandValue("rowspacing",a);d&&h.setValue((d+"").replace(/%/,"")),h.setChecked(b)}}),h}}(k);for(var n,o=["insertorderedlist","insertunorderedlist"],p=0;n=o[p++];)!function(a){b[a]=function(c){var d=c.options[a],e=function(){c.execCommand(a,this.value)},f=[];for(var g in d)f.push({label:d[g]||c.getLang()[a][g]||"",value:g,theme:c.options.theme,onclick:e});var h=new b.MenuButton({editor:c,className:"edui-for-"+a,title:c.getLang("labelMap."+a)||"",items:f,onbuttonclick:function(){var b=c.queryCommandValue(a)||this.value;c.execCommand(a,b)}});return b.buttons[a]=h,c.addListener("selectionchange",function(){var b=c.queryCommandState(a);if(-1==b)h.setDisabled(!0);else{h.setDisabled(!1);var d=c.queryCommandValue(a);h.setValue(d),h.setChecked(b)}}),h}}(n);b.fullscreen=function(a,c){c=a.options.labelMap.fullscreen||a.getLang("labelMap.fullscreen")||"";var d=new b.Button({className:"edui-for-fullscreen",title:c,theme:a.options.theme,onclick:function(){a.ui&&a.ui.setFullScreen(!a.ui.isFullScreen()),this.setChecked(a.ui.isFullScreen())}});return b.buttons.fullscreen=d,a.addListener("selectionchange",function(){var b=a.queryCommandState("fullscreen");d.setDisabled(-1==b),d.setChecked(a.ui.isFullScreen())}),d},b.emotion=function(a,c){var d="emotion",f=new b.MultiMenuPop({title:a.options.labelMap[d]||a.getLang("labelMap."+d)||"",editor:a,className:"edui-for-"+d,iframeUrl:a.ui.mapUrl(c||(a.options.iframeUrlMap||{})[d]||e[d])});return b.buttons[d]=f,a.addListener("selectionchange",function(){f.setDisabled(-1==a.queryCommandState(d))}),f},b.autotypeset=function(a){var c=new b.AutoTypeSetButton({editor:a,title:a.options.labelMap.autotypeset||a.getLang("labelMap.autotypeset")||"",className:"edui-for-autotypeset",onbuttonclick:function(){a.execCommand("autotypeset")}});return b.buttons.autotypeset=c,a.addListener("selectionchange",function(){c.setDisabled(-1==a.queryCommandState("autotypeset"))}),c},b.simpleupload=function(a){var c="simpleupload",d=new b.Button({className:"edui-for-"+c,title:a.options.labelMap[c]||a.getLang("labelMap."+c)||"",onclick:function(){},theme:a.options.theme,showText:!1});return b.buttons[c]=d,a.addListener("ready",function(){var b=d.getDom("body"),c=b.children[0];a.fireEvent("simpleuploadbtnready",c)}),a.addListener("selectionchange",function(b,e,f){var g=a.queryCommandState(c);-1==g?(d.setDisabled(!0),d.setChecked(!1)):f||(d.setDisabled(!1),d.setChecked(g))}),d}}(),function(){function a(a){this.initOptions(a),this.initEditorUI()}var b=baidu.editor.utils,c=baidu.editor.ui.uiUtils,d=baidu.editor.ui.UIBase,e=baidu.editor.dom.domUtils,f=[];a.prototype={uiName:"editor",initEditorUI:function(){function a(a,b){a.setOpt({wordCount:!0,maximumWords:1e4,wordCountMsg:a.options.wordCountMsg||a.getLang("wordCountMsg"),wordOverFlowMsg:a.options.wordOverFlowMsg||a.getLang("wordOverFlowMsg")});var c=a.options,d=c.maximumWords,e=c.wordCountMsg,f=c.wordOverFlowMsg,g=b.getDom("wordcount");if(c.wordCount){var h=a.getContentLength(!0);h>d?(g.innerHTML=f,a.fireEvent("wordcountoverflow")):g.innerHTML=e.replace("{#leave}",d-h).replace("{#count}",h)}}this.editor.ui=this,this._dialogs={},this.initUIBase(),this._initToolbars();var b=this.editor,c=this;b.addListener("ready",function(){function d(){a(b,c),e.un(b.document,"click",arguments.callee)}b.getDialog=function(a){return b.ui._dialogs[a+"Dialog"]},e.on(b.window,"scroll",function(a){baidu.editor.ui.Popup.postHide(a)}),b.ui._actualFrameWidth=b.options.initialFrameWidth,UE.browser.ie&&6===UE.browser.version&&b.container.ownerDocument.execCommand("BackgroundImageCache",!1,!0),b.options.elementPathEnabled&&(b.ui.getDom("elementpath").innerHTML='
              '+b.getLang("elementPathTip")+":
              "),b.options.wordCount&&(e.on(b.document,"click",d),b.ui.getDom("wordcount").innerHTML=b.getLang("wordCountTip")),b.ui._scale(),b.options.scaleEnabled?(b.autoHeightEnabled&&b.disableAutoHeight(),c.enableScale()):c.disableScale(),b.options.elementPathEnabled||b.options.wordCount||b.options.scaleEnabled||(b.ui.getDom("elementpath").style.display="none",b.ui.getDom("wordcount").style.display="none",b.ui.getDom("scale").style.display="none"),b.selection.isFocus()&&b.fireEvent("selectionchange",!1,!0)}),b.addListener("mousedown",function(a,b){var c=b.target||b.srcElement;baidu.editor.ui.Popup.postHide(b,c),baidu.editor.ui.ShortCutMenu.postHide(b)}),b.addListener("delcells",function(){UE.ui.edittip&&new UE.ui.edittip(b),b.getDialog("edittip").open()});var d,f,g=!1;b.addListener("afterpaste",function(){b.queryCommandState("pasteplain")||(baidu.editor.ui.PastePicker&&(d=new baidu.editor.ui.Popup({content:new baidu.editor.ui.PastePicker({editor:b}),editor:b,className:"edui-wordpastepop"}),d.render()),g=!0)}),b.addListener("afterinserthtml",function(){clearTimeout(f),f=setTimeout(function(){if(d&&(g||b.ui._isTransfer)){if(d.isHidden()){var a=e.createElement(b.document,"span",{style:"line-height:0px;",innerHTML:"\ufeff"}),c=b.selection.getRange();c.insertNode(a);var f=getDomNode(a,"firstChild","previousSibling");f&&d.showAnchor(3==f.nodeType?f.parentNode:f),e.remove(a)}else d.show();delete b.ui._isTransfer,g=!1}},200)}),b.addListener("contextmenu",function(a,b){baidu.editor.ui.Popup.postHide(b)}),b.addListener("keydown",function(a,b){d&&d.dispose(b);var c=b.keyCode||b.which;b.altKey&&90==c&&UE.ui.buttons.fullscreen.onclick()}),b.addListener("wordcount",function(b){a(this,c)}),b.addListener("selectionchange",function(){b.options.elementPathEnabled&&c[(-1==b.queryCommandState("elementpath")?"dis":"en")+"ableElementPath"](),b.options.scaleEnabled&&c[(-1==b.queryCommandState("scale")?"dis":"en")+"ableScale"]()});var h=new baidu.editor.ui.Popup({editor:b,content:"",className:"edui-bubble",_onEditButtonClick:function(){this.hide(),b.ui._dialogs.linkDialog.open()},_onImgEditButtonClick:function(a){this.hide(),b.ui._dialogs[a]&&b.ui._dialogs[a].open()},_onImgSetFloat:function(a){this.hide(),b.execCommand("imagefloat",a)},_setIframeAlign:function(a){var b=h.anchorEl,c=b.cloneNode(!0);switch(a){case-2:c.setAttribute("align","");break;case-1:c.setAttribute("align","left");break;case 1:c.setAttribute("align","right")}b.parentNode.insertBefore(c,b),e.remove(b),h.anchorEl=c,h.showAnchor(h.anchorEl)},_updateIframe:function(){var a=b._iframe=h.anchorEl;e.hasClass(a,"ueditor_baidumap")?(b.selection.getRange().selectNode(a).select(),b.ui._dialogs.mapDialog.open(),h.hide()):(b.ui._dialogs.insertframeDialog.open(),h.hide())},_onRemoveButtonClick:function(a){b.execCommand(a),this.hide()},queryAutoHide:function(a){return a&&a.ownerDocument==b.document&&("img"==a.tagName.toLowerCase()||e.findParentByTagName(a,"a",!0))?a!==h.anchorEl:baidu.editor.ui.Popup.prototype.queryAutoHide.call(this,a)}});h.render(),b.options.imagePopup&&(b.addListener("mouseover",function(a,c){c=c||window.event;var d=c.target||c.srcElement;if(b.ui._dialogs.insertframeDialog&&/iframe/gi.test(d.tagName)){var e=h.formatHtml(""+b.getLang("property")+': '+b.getLang("default")+'  '+b.getLang("justifyleft")+'  '+b.getLang("justifyright")+'   '+b.getLang("modify")+"");e?(h.getDom("content").innerHTML=e,h.anchorEl=d,h.showAnchor(h.anchorEl)):h.hide()}}),b.addListener("selectionchange",function(a,c){if(c){var d="",f="",g=b.selection.getRange().getClosedNode(),i=b.ui._dialogs;if(g&&"IMG"==g.tagName){var j="insertimageDialog";if(-1==g.className.indexOf("edui-faked-video")&&-1==g.className.indexOf("edui-upload-video")||(j="insertvideoDialog"),-1!=g.className.indexOf("edui-faked-webapp")&&(j="webappDialog"),-1!=g.src.indexOf("http://api.map.baidu.com")&&(j="mapDialog"),-1!=g.className.indexOf("edui-faked-music")&&(j="musicDialog"),-1!=g.src.indexOf("http://maps.google.com/maps/api/staticmap")&&(j="gmapDialog"),g.getAttribute("anchorname")&&(j="anchorDialog",d=h.formatHtml(""+b.getLang("property")+': '+b.getLang("modify")+"  "+b.getLang("delete")+"")),g.getAttribute("word_img")&&(b.word_img=[g.getAttribute("word_img")],j="wordimageDialog"),(e.hasClass(g,"loadingclass")||e.hasClass(g,"loaderrorclass"))&&(j=""),!i[j])return;f=""+b.getLang("property")+': '+b.getLang("default")+'  '+b.getLang("justifyleft")+'  '+b.getLang("justifyright")+'  '+b.getLang("justifycenter")+"  '+b.getLang("modify")+"",!d&&(d=h.formatHtml(f))}if(b.ui._dialogs.linkDialog){var k,l=b.queryCommandValue("link");if(l&&(k=l.getAttribute("_href")||l.getAttribute("href",2))){var m=k;k.length>30&&(m=k.substring(0,20)+"..."),d&&(d+='
              '),d+=h.formatHtml(""+b.getLang("anthorMsg")+': '+m+' '+b.getLang("modify")+' '+b.getLang("clear")+""),h.showAnchor(l)}}d?(h.getDom("content").innerHTML=d,h.anchorEl=g||l,h.showAnchor(h.anchorEl)):h.hide()}}))},_initToolbars:function(){for(var a=this.editor,c=this.toolbars||[],d=[],e=0;e
              '+(this.toolbars.length?'
              '+this.renderToolbarBoxHtml()+"
              ":"")+'
              '},showWordImageDialog:function(){this._dialogs.wordimageDialog.open()},renderToolbarBoxHtml:function(){for(var a=[],b=0;b'+c+"");b.innerHTML='
              '+this.editor.getLang("elementPathTip")+": "+d.join(" > ")+"
              "}else b.style.display="none"},disableElementPath:function(){var a=this.getDom("elementpath");a.innerHTML="",a.style.display="none",this.elementPathEnabled=!1},enableElementPath:function(){var a=this.getDom("elementpath");a.style.display="",this.elementPathEnabled=!0,this._updateElementPath()},_scale:function(){function a(){o=e.getXY(h),p||(p=g.options.minFrameHeight+j.offsetHeight+k.offsetHeight),m.style.cssText="position:absolute;left:0;display:;top:0;background-color:#41ABFF;opacity:0.4;filter: Alpha(opacity=40);width:"+h.offsetWidth+"px;height:"+h.offsetHeight+"px;z-index:"+(g.options.zIndex+1),e.on(f,"mousemove",b),e.on(i,"mouseup",c),e.on(f,"mouseup",c)}function b(a){d();var b=a||window.event;r=b.pageX||f.documentElement.scrollLeft+b.clientX,s=b.pageY||f.documentElement.scrollTop+b.clientY,t=r-o.x,u=s-o.y,t>=q&&(n=!0,m.style.width=t+"px"),u>=p&&(n=!0,m.style.height=u+"px")}function c(){n&&(n=!1,g.ui._actualFrameWidth=m.offsetWidth-2,h.style.width=g.ui._actualFrameWidth+"px",g.setHeight(m.offsetHeight-k.offsetHeight-j.offsetHeight-2,!0)),m&&(m.style.display="none"),d(),e.un(f,"mousemove",b),e.un(i,"mouseup",c),e.un(f,"mouseup",c)}function d(){browser.ie?f.selection.clear():window.getSelection().removeAllRanges()}var f=document,g=this.editor,h=g.container,i=g.document,j=this.getDom("toolbarbox"),k=this.getDom("bottombar"),l=this.getDom("scale"),m=this.getDom("scalelayer"),n=!1,o=null,p=0,q=g.options.minFrameWidth,r=0,s=0,t=0,u=0,v=this;this.editor.addListener("fullscreenchanged",function(a,b){if(b)v.disableScale();else if(v.editor.options.scaleEnabled){v.enableScale();var c=v.editor.document.createElement("span");v.editor.body.appendChild(c),v.editor.body.style.height=Math.max(e.getXY(c).y,v.editor.iframe.offsetHeight-20)+"px",e.remove(c)}}),this.enableScale=function(){1!=g.queryCommandState("source")&&(l.style.display="",this.scaleEnabled=!0,e.on(l,"mousedown",a))},this.disableScale=function(){l.style.display="none",this.scaleEnabled=!1,e.un(l,"mousedown",a)}},isFullScreen:function(){return this._fullscreen},postRender:function(){d.prototype.postRender.call(this);for(var a=0;a[\n\r\t]+([ ]{4})+/g,">").replace(/[\n\r\t]+([ ]{4})+[\n\r\t]+<"),c.className&&(b.className=c.className),c.style.cssText&&(b.style.cssText=c.style.cssText),/textarea/i.test(c.tagName)?(d.textarea=c,d.textarea.style.display="none"):c.parentNode.removeChild(c),c.id&&(b.id=c.id,e.removeAttributes(c,"id")),c=b,c.innerHTML=""}e.addClass(c,"edui-"+d.options.theme),d.ui.render(c);var h=d.options;d.container=d.ui.getDom();for(var i,j=e.findParents(c,!0),k=[],l=0;i=j[l];l++)k[l]=i.style.display,i.style.display="block";if(h.initialFrameWidth)h.minFrameWidth=h.initialFrameWidth;else{h.minFrameWidth=h.initialFrameWidth=c.offsetWidth;var m=c.style.width;/%$/.test(m)&&(h.initialFrameWidth=m)}h.initialFrameHeight?h.minFrameHeight=h.initialFrameHeight:h.initialFrameHeight=h.minFrameHeight=c.offsetHeight;for(var i,l=0;i=j[l];l++)i.style.display=k[l];c.style.height&&(c.style.height=""),d.container.style.width=h.initialFrameWidth+(/%$/.test(h.initialFrameWidth)?"":"px"),d.container.style.zIndex=h.zIndex,f.call(d,d.ui.getDom("iframeholder")),d.fireEvent("afteruiready")}d.langIsReady?b():d.addListener("langReady",b)})},d},UE.getEditor=function(a,b){var c=g[a];return c||(c=g[a]=new UE.ui.Editor(b),c.render(a)),c},UE.delEditor=function(a){var b;(b=g[a])&&(b.key&&b.destroy(),delete g[a])},UE.registerUI=function(a,c,d,e){b.each(a.split(/\s+/),function(a){UE._customizeUI[a]={id:e,execFn:c,index:d}})}}(),UE.registerUI("message",function(a){function b(){var a=g.ui.getDom("toolbarbox");a&&(c.style.top=a.offsetHeight+3+"px"),c.style.zIndex=Math.max(g.options.zIndex,g.iframe.style.zIndex)+1}var c,d=baidu.editor.ui,e=d.Message,f=[],g=a;g.addListener("ready",function(){c=document.getElementById(g.ui.id+"_message_holder"),b(),setTimeout(function(){b()},500)}),g.addListener("showmessage",function(a,d){d=utils.isString(d)?{content:d}:d;var h=new e({timeout:d.timeout,type:d.type,content:d.content,keepshow:d.keepshow,editor:g}),i=d.id||"msg_"+(+new Date).toString(36);return h.render(c),f[i]=h,h.reset(d),b(),i}),g.addListener("updatemessage",function(a,b,d){d=utils.isString(d)?{content:d}:d;var e=f[b];e.render(c),e&&e.reset(d)}),g.addListener("hidemessage",function(a,b){var c=f[b];c&&c.hide()})}),UE.registerUI("autosave",function(a){var b=null,c=null;a.on("afterautosave",function(){clearTimeout(b),b=setTimeout(function(){c&&a.trigger("hidemessage",c),c=a.trigger("showmessage",{content:a.getLang("autosave.success"),timeout:2e3})},2e3)})})}(); \ No newline at end of file diff --git a/public/static/libs/ueditor/ueditor.config.js b/public/static/libs/ueditor/ueditor.config.js new file mode 100644 index 0000000..7528c7f --- /dev/null +++ b/public/static/libs/ueditor/ueditor.config.js @@ -0,0 +1,499 @@ +/** + * ueditor完整配置项 + * 可以在这里配置整个编辑器的特性 + */ +/**************************提示******************************** + * 所有被注释的配置项均为UEditor默认值。 + * 修改默认配置请首先确保已经完全明确该参数的真实用途。 + * 主要有两种修改方案,一种是取消此处注释,然后修改成对应参数;另一种是在实例化编辑器时传入对应参数。 + * 当升级编辑器时,可直接使用旧版配置文件替换新版配置文件,不用担心旧版配置文件中因缺少新功能所需的参数而导致脚本报错。 + **************************提示********************************/ + +(function () { + + /** + * 编辑器资源文件根路径。它所表示的含义是:以编辑器实例化页面为当前路径,指向编辑器资源文件(即dialog等文件夹)的路径。 + * 鉴于很多同学在使用编辑器的时候出现的种种路径问题,此处强烈建议大家使用"相对于网站根目录的相对路径"进行配置。 + * "相对于网站根目录的相对路径"也就是以斜杠开头的形如"/myProject/ueditor/"这样的路径。 + * 如果站点中有多个不在同一层级的页面需要实例化编辑器,且引用了同一UEditor的时候,此处的URL可能不适用于每个页面的编辑器。 + * 因此,UEditor提供了针对不同页面的编辑器可单独配置的根路径,具体来说,在需要实例化编辑器的页面最顶部写上如下代码即可。当然,需要令此处的URL等于对应的配置。 + * window.UEDITOR_HOME_URL = "/xxxx/xxxx/"; + */ + var URL = window.UEDITOR_HOME_URL || getUEBasePath(); + + /** + * 配置项主体。注意,此处所有涉及到路径的配置别遗漏URL变量。 + */ + window.UEDITOR_CONFIG = { + + //为编辑器实例添加一个路径,这个不能被注释 + UEDITOR_HOME_URL: URL + + // 服务器统一请求接口路径 + , serverUrl: URL + "php/controller.php" + + //工具栏上的所有的功能按钮和下拉框,可以在new编辑器的实例时选择自己需要的重新定义 + , toolbars: [[ + 'fullscreen', 'source', '|', 'undo', 'redo', '|', + 'bold', 'italic', 'underline', 'fontborder', 'strikethrough', 'superscript', 'subscript', 'removeformat', 'formatmatch', 'autotypeset', 'blockquote', 'pasteplain', '|', 'forecolor', 'backcolor', 'insertorderedlist', 'insertunorderedlist', 'selectall', 'cleardoc', '|', + 'rowspacingtop', 'rowspacingbottom', 'lineheight', '|', + 'customstyle', 'paragraph', 'fontfamily', 'fontsize', '|', + 'directionalityltr', 'directionalityrtl', 'indent', '|', + 'justifyleft', 'justifycenter', 'justifyright', 'justifyjustify', '|', 'touppercase', 'tolowercase', '|', + 'link', 'unlink', 'anchor', '|', 'imagenone', 'imageleft', 'imageright', 'imagecenter', '|', + 'simpleupload', 'insertimage', 'emotion', 'scrawl', 'insertvideo', 'music', 'attachment', 'map', 'gmap', 'insertframe', 'insertcode', 'webapp', 'pagebreak', 'template', 'background', '|', + 'horizontal', 'date', 'time', 'spechars', 'snapscreen', 'wordimage', '|', + 'inserttable', 'deletetable', 'insertparagraphbeforetable', 'insertrow', 'deleterow', 'insertcol', 'deletecol', 'mergecells', 'mergeright', 'mergedown', 'splittocells', 'splittorows', 'splittocols', 'charts', '|', + 'print', 'preview', 'searchreplace', 'drafts', 'help' + ]] + //当鼠标放在工具栏上时显示的tooltip提示,留空支持自动多语言配置,否则以配置值为准 + //,labelMap:{ + // 'anchor':'', 'undo':'' + //} + + //语言配置项,默认是zh-cn。有需要的话也可以使用如下这样的方式来自动多语言切换,当然,前提条件是lang文件夹下存在对应的语言文件: + //lang值也可以通过自动获取 (navigator.language||navigator.browserLanguage ||navigator.userLanguage).toLowerCase() + //,lang:"zh-cn" + //,langPath:URL +"lang/" + + //主题配置项,默认是default。有需要的话也可以使用如下这样的方式来自动多主题切换,当然,前提条件是themes文件夹下存在对应的主题文件: + //现有如下皮肤:default + //,theme:'default' + //,themePath:URL +"themes/" + + //,zIndex : 900 //编辑器层级的基数,默认是900 + + //针对getAllHtml方法,会在对应的head标签中增加该编码设置。 + //,charset:"utf-8" + + //若实例化编辑器的页面手动修改的domain,此处需要设置为true + //,customDomain:false + + //常用配置项目 + //,isShow : true //默认显示编辑器 + + //,textarea:'editorValue' // 提交表单时,服务器获取编辑器提交内容的所用的参数,多实例时可以给容器name属性,会将name给定的值最为每个实例的键值,不用每次实例化的时候都设置这个值 + + //,initialContent:'欢迎使用ueditor!' //初始化编辑器的内容,也可以通过textarea/script给值,看官网例子 + + //,autoClearinitialContent:true //是否自动清除编辑器初始内容,注意:如果focus属性设置为true,这个也为真,那么编辑器一上来就会触发导致初始化的内容看不到了 + + //,focus:false //初始化时,是否让编辑器获得焦点true或false + + //如果自定义,最好给p标签如下的行高,要不输入中文时,会有跳动感 + //,initialStyle:'p{line-height:1em}'//编辑器层级的基数,可以用来改变字体等 + + //,iframeCssUrl: URL + '/themes/iframe.css' //给编辑区域的iframe引入一个css文件 + + //indentValue + //首行缩进距离,默认是2em + //,indentValue:'2em' + + //,initialFrameWidth:1000 //初始化编辑器宽度,默认1000 + //,initialFrameHeight:320 //初始化编辑器高度,默认320 + + //,readonly : false //编辑器初始化结束后,编辑区域是否是只读的,默认是false + + //,autoClearEmptyNode : true //getContent时,是否删除空的inlineElement节点(包括嵌套的情况) + + //启用自动保存 + //,enableAutoSave: true + //自动保存间隔时间, 单位ms + //,saveInterval: 500 + + //,fullscreen : false //是否开启初始化时即全屏,默认关闭 + + //,imagePopup:true //图片操作的浮层开关,默认打开 + + //,autoSyncData:true //自动同步编辑器要提交的数据 + //,emotionLocalization:false //是否开启表情本地化,默认关闭。若要开启请确保emotion文件夹下包含官网提供的images表情文件夹 + + //粘贴只保留标签,去除标签所有属性 + //,retainOnlyLabelPasted: false + + //,pasteplain:false //是否默认为纯文本粘贴。false为不使用纯文本粘贴,true为使用纯文本粘贴 + //纯文本粘贴模式下的过滤规则 + //'filterTxtRules' : function(){ + // function transP(node){ + // node.tagName = 'p'; + // node.setStyle(); + // } + // return { + // //直接删除及其字节点内容 + // '-' : 'script style object iframe embed input select', + // 'p': {$:{}}, + // 'br':{$:{}}, + // 'div':{'$':{}}, + // 'li':{'$':{}}, + // 'caption':transP, + // 'th':transP, + // 'tr':transP, + // 'h1':transP,'h2':transP,'h3':transP,'h4':transP,'h5':transP,'h6':transP, + // 'td':function(node){ + // //没有内容的td直接删掉 + // var txt = !!node.innerText(); + // if(txt){ + // node.parentNode.insertAfter(UE.uNode.createText('    '),node); + // } + // node.parentNode.removeChild(node,node.innerText()) + // } + // } + //}() + + //,allHtmlEnabled:false //提交到后台的数据是否包含整个html字符串 + + //insertorderedlist + //有序列表的下拉配置,值留空时支持多语言自动识别,若配置值,则以此值为准 + //,'insertorderedlist':{ + // //自定的样式 + // 'num':'1,2,3...', + // 'num1':'1),2),3)...', + // 'num2':'(1),(2),(3)...', + // 'cn':'一,二,三....', + // 'cn1':'一),二),三)....', + // 'cn2':'(一),(二),(三)....', + // //系统自带 + // 'decimal' : '' , //'1,2,3...' + // 'lower-alpha' : '' , // 'a,b,c...' + // 'lower-roman' : '' , //'i,ii,iii...' + // 'upper-alpha' : '' , lang //'A,B,C' + // 'upper-roman' : '' //'I,II,III...' + //} + + //insertunorderedlist + //无序列表的下拉配置,值留空时支持多语言自动识别,若配置值,则以此值为准 + //,insertunorderedlist : { //自定的样式 + // 'dash' :'— 破折号', //-破折号 + // 'dot':' 。 小圆圈', //系统自带 + // 'circle' : '', // '○ 小圆圈' + // 'disc' : '', // '● 小圆点' + // 'square' : '' //'■ 小方块' + //} + //,listDefaultPaddingLeft : '30'//默认的左边缩进的基数倍 + //,listiconpath : 'http://bs.baidu.com/listicon/'//自定义标号的路径 + //,maxListLevel : 3 //限制可以tab的级数, 设置-1为不限制 + + //,autoTransWordToList:false //禁止word中粘贴进来的列表自动变成列表标签 + + //fontfamily + //字体设置 label留空支持多语言自动切换,若配置,则以配置值为准 + //,'fontfamily':[ + // { label:'',name:'songti',val:'宋体,SimSun'}, + // { label:'',name:'kaiti',val:'楷体,楷体_GB2312, SimKai'}, + // { label:'',name:'yahei',val:'微软雅黑,Microsoft YaHei'}, + // { label:'',name:'heiti',val:'黑体, SimHei'}, + // { label:'',name:'lishu',val:'隶书, SimLi'}, + // { label:'',name:'andaleMono',val:'andale mono'}, + // { label:'',name:'arial',val:'arial, helvetica,sans-serif'}, + // { label:'',name:'arialBlack',val:'arial black,avant garde'}, + // { label:'',name:'comicSansMs',val:'comic sans ms'}, + // { label:'',name:'impact',val:'impact,chicago'}, + // { label:'',name:'timesNewRoman',val:'times new roman'} + //] + + //fontsize + //字号 + //,'fontsize':[10, 11, 12, 14, 16, 18, 20, 24, 36] + + //paragraph + //段落格式 值留空时支持多语言自动识别,若配置,则以配置值为准 + //,'paragraph':{'p':'', 'h1':'', 'h2':'', 'h3':'', 'h4':'', 'h5':'', 'h6':''} + + //rowspacingtop + //段间距 值和显示的名字相同 + //,'rowspacingtop':['5', '10', '15', '20', '25'] + + //rowspacingBottom + //段间距 值和显示的名字相同 + //,'rowspacingbottom':['5', '10', '15', '20', '25'] + + //lineheight + //行内间距 值和显示的名字相同 + //,'lineheight':['1', '1.5','1.75','2', '3', '4', '5'] + + //customstyle + //自定义样式,不支持国际化,此处配置值即可最后显示值 + //block的元素是依据设置段落的逻辑设置的,inline的元素依据BIU的逻辑设置 + //尽量使用一些常用的标签 + //参数说明 + //tag 使用的标签名字 + //label 显示的名字也是用来标识不同类型的标识符,注意这个值每个要不同, + //style 添加的样式 + //每一个对象就是一个自定义的样式 + //,'customstyle':[ + // {tag:'h1', name:'tc', label:'', style:'border-bottom:#ccc 2px solid;padding:0 4px 0 0;text-align:center;margin:0 0 20px 0;'}, + // {tag:'h1', name:'tl',label:'', style:'border-bottom:#ccc 2px solid;padding:0 4px 0 0;margin:0 0 10px 0;'}, + // {tag:'span',name:'im', label:'', style:'font-style:italic;font-weight:bold'}, + // {tag:'span',name:'hi', label:'', style:'font-style:italic;font-weight:bold;color:rgb(51, 153, 204)'} + //] + + //打开右键菜单功能 + //,enableContextMenu: true + //右键菜单的内容,可以参考plugins/contextmenu.js里边的默认菜单的例子,label留空支持国际化,否则以此配置为准 + //,contextMenu:[ + // { + // label:'', //显示的名称 + // cmdName:'selectall',//执行的command命令,当点击这个右键菜单时 + // //exec可选,有了exec就会在点击时执行这个function,优先级高于cmdName + // exec:function () { + // //this是当前编辑器的实例 + // //this.ui._dialogs['inserttableDialog'].open(); + // } + // } + //] + + //快捷菜单 + //,shortcutMenu:["fontfamily", "fontsize", "bold", "italic", "underline", "forecolor", "backcolor", "insertorderedlist", "insertunorderedlist"] + + //elementPathEnabled + //是否启用元素路径,默认是显示 + //,elementPathEnabled : true + + //wordCount + //,wordCount:true //是否开启字数统计 + //,maximumWords:10000 //允许的最大字符数 + //字数统计提示,{#count}代表当前字数,{#leave}代表还可以输入多少字符数,留空支持多语言自动切换,否则按此配置显示 + //,wordCountMsg:'' //当前已输入 {#count} 个字符,您还可以输入{#leave} 个字符 + //超出字数限制提示 留空支持多语言自动切换,否则按此配置显示 + //,wordOverFlowMsg:'' //你输入的字符个数已经超出最大允许值,服务器可能会拒绝保存! + + //tab + //点击tab键时移动的距离,tabSize倍数,tabNode什么字符做为单位 + //,tabSize:4 + //,tabNode:' ' + + //removeFormat + //清除格式时可以删除的标签和属性 + //removeForamtTags标签 + //,removeFormatTags:'b,big,code,del,dfn,em,font,i,ins,kbd,q,samp,small,span,strike,strong,sub,sup,tt,u,var' + //removeFormatAttributes属性 + //,removeFormatAttributes:'class,style,lang,width,height,align,hspace,valign' + + //undo + //可以最多回退的次数,默认20 + //,maxUndoCount:20 + //当输入的字符数超过该值时,保存一次现场 + //,maxInputCount:1 + + //autoHeightEnabled + // 是否自动长高,默认true + //,autoHeightEnabled:true + + //scaleEnabled + //是否可以拉伸长高,默认true(当开启时,自动长高失效) + //,scaleEnabled:false + //,minFrameWidth:800 //编辑器拖动时最小宽度,默认800 + //,minFrameHeight:220 //编辑器拖动时最小高度,默认220 + + //autoFloatEnabled + //是否保持toolbar的位置不动,默认true + //,autoFloatEnabled:true + //浮动时工具栏距离浏览器顶部的高度,用于某些具有固定头部的页面 + //,topOffset:30 + //编辑器底部距离工具栏高度(如果参数大于等于编辑器高度,则设置无效) + //,toolbarTopOffset:400 + + //设置远程图片是否抓取到本地保存 + //,catchRemoteImageEnable: true //设置是否抓取远程图片 + + //pageBreakTag + //分页标识符,默认是_ueditor_page_break_tag_ + //,pageBreakTag:'_ueditor_page_break_tag_' + + //autotypeset + //自动排版参数 + //,autotypeset: { + // mergeEmptyline: true, //合并空行 + // removeClass: true, //去掉冗余的class + // removeEmptyline: false, //去掉空行 + // textAlign:"left", //段落的排版方式,可以是 left,right,center,justify 去掉这个属性表示不执行排版 + // imageBlockLine: 'center', //图片的浮动方式,独占一行剧中,左右浮动,默认: center,left,right,none 去掉这个属性表示不执行排版 + // pasteFilter: false, //根据规则过滤没事粘贴进来的内容 + // clearFontSize: false, //去掉所有的内嵌字号,使用编辑器默认的字号 + // clearFontFamily: false, //去掉所有的内嵌字体,使用编辑器默认的字体 + // removeEmptyNode: false, // 去掉空节点 + // //可以去掉的标签 + // removeTagNames: {标签名字:1}, + // indent: false, // 行首缩进 + // indentValue : '2em', //行首缩进的大小 + // bdc2sb: false, + // tobdc: false + //} + + //tableDragable + //表格是否可以拖拽 + //,tableDragable: true + + + + //sourceEditor + //源码的查看方式,codemirror 是代码高亮,textarea是文本框,默认是codemirror + //注意默认codemirror只能在ie8+和非ie中使用 + //,sourceEditor:"codemirror" + //如果sourceEditor是codemirror,还用配置一下两个参数 + //codeMirrorJsUrl js加载的路径,默认是 URL + "third-party/codemirror/codemirror.js" + //,codeMirrorJsUrl:URL + "third-party/codemirror/codemirror.js" + //codeMirrorCssUrl css加载的路径,默认是 URL + "third-party/codemirror/codemirror.css" + //,codeMirrorCssUrl:URL + "third-party/codemirror/codemirror.css" + //编辑器初始化完成后是否进入源码模式,默认为否。 + //,sourceEditorFirst:false + + //iframeUrlMap + //dialog内容的路径 ~会被替换成URL,垓属性一旦打开,将覆盖所有的dialog的默认路径 + //,iframeUrlMap:{ + // 'anchor':'~/dialogs/anchor/anchor.html', + //} + + //allowLinkProtocol 允许的链接地址,有这些前缀的链接地址不会自动添加http + //, allowLinkProtocols: ['http:', 'https:', '#', '/', 'ftp:', 'mailto:', 'tel:', 'git:', 'svn:'] + + //webAppKey 百度应用的APIkey,每个站长必须首先去百度官网注册一个key后方能正常使用app功能,注册介绍,http://app.baidu.com/static/cms/getapikey.html + //, webAppKey: "" + + //默认过滤规则相关配置项目 + //,disabledTableInTable:true //禁止表格嵌套 + //,allowDivTransToP:true //允许进入编辑器的div标签自动变成p标签 + //,rgb2Hex:true //默认产出的数据中的color自动从rgb格式变成16进制格式 + + // xss 过滤是否开启,inserthtml等操作 + ,xssFilterRules: true + //input xss过滤 + ,inputXssFilter: true + //output xss过滤 + ,outputXssFilter: true + // xss过滤白名单 名单来源: https://raw.githubusercontent.com/leizongmin/js-xss/master/lib/default.js + ,whitList: { + a: ['target', 'href', 'title', 'class', 'style'], + abbr: ['title', 'class', 'style'], + address: ['class', 'style'], + area: ['shape', 'coords', 'href', 'alt'], + article: [], + aside: [], + audio: ['autoplay', 'controls', 'loop', 'preload', 'src', 'class', 'style'], + b: ['class', 'style'], + bdi: ['dir'], + bdo: ['dir'], + big: [], + blockquote: ['cite', 'class', 'style'], + br: [], + caption: ['class', 'style'], + center: [], + cite: [], + code: ['class', 'style'], + col: ['align', 'valign', 'span', 'width', 'class', 'style'], + colgroup: ['align', 'valign', 'span', 'width', 'class', 'style'], + dd: ['class', 'style'], + del: ['datetime'], + details: ['open'], + div: ['class', 'style'], + dl: ['class', 'style'], + dt: ['class', 'style'], + em: ['class', 'style'], + font: ['color', 'size', 'face'], + footer: [], + h1: ['class', 'style'], + h2: ['class', 'style'], + h3: ['class', 'style'], + h4: ['class', 'style'], + h5: ['class', 'style'], + h6: ['class', 'style'], + header: [], + hr: [], + i: ['class', 'style'], + img: ['src', 'alt', 'title', 'width', 'height', 'id', '_src', '_url', 'loadingclass', 'class'], + ins: ['datetime'], + li: ['class', 'style'], + mark: [], + nav: [], + ol: ['class', 'style'], + p: ['class', 'style'], + pre: ['class', 'style'], + s: [], + section:[], + small: [], + span: ['class', 'style'], + sub: ['class', 'style'], + sup: ['class', 'style'], + strong: ['class', 'style'], + table: ['width', 'border', 'align', 'valign', 'class', 'style'], + tbody: ['align', 'valign', 'class', 'style'], + td: ['width', 'rowspan', 'colspan', 'align', 'valign', 'class', 'style'], + tfoot: ['align', 'valign', 'class', 'style'], + th: ['width', 'rowspan', 'colspan', 'align', 'valign', 'class', 'style'], + thead: ['align', 'valign', 'class', 'style'], + tr: ['rowspan', 'align', 'valign', 'class', 'style'], + tt: [], + u: [], + ul: ['class', 'style'], + video: ['autoplay', 'controls', 'loop', 'preload', 'src', 'height', 'width', 'class', 'style'], + source: ['src', 'type'], + embed: ['type', 'class', 'pluginspage', 'src', 'width', 'height', 'align', 'style', 'wmode', 'play', 'loop', 'menu', 'allowscriptaccess', 'allowfullscreen'] + } + }; + + function getUEBasePath(docUrl, confUrl) { + + return getBasePath(docUrl || self.document.URL || self.location.href, confUrl || getConfigFilePath()); + + } + + function getConfigFilePath() { + + var configPath = document.getElementsByTagName('script'); + + return configPath[ configPath.length - 1 ].src; + + } + + function getBasePath(docUrl, confUrl) { + + var basePath = confUrl; + + + if (/^(\/|\\\\)/.test(confUrl)) { + + basePath = /^.+?\w(\/|\\\\)/.exec(docUrl)[0] + confUrl.replace(/^(\/|\\\\)/, ''); + + } else if (!/^[a-z]+:/i.test(confUrl)) { + + docUrl = docUrl.split("#")[0].split("?")[0].replace(/[^\\\/]+$/, ''); + + basePath = docUrl + "" + confUrl; + + } + + return optimizationPath(basePath); + + } + + function optimizationPath(path) { + + var protocol = /^[a-z]+:\/\//.exec(path)[ 0 ], + tmp = null, + res = []; + + path = path.replace(protocol, "").split("?")[0].split("#")[0]; + + path = path.replace(/\\/g, '/').split(/\//); + + path[ path.length - 1 ] = ""; + + while (path.length) { + + if (( tmp = path.shift() ) === "..") { + res.pop(); + } else if (tmp !== ".") { + res.push(tmp); + } + + } + + return protocol + res.join("/"); + + } + + window.UE = { + getUEBasePath: getUEBasePath + }; + +})(); diff --git a/public/static/libs/ueditor/ueditor.parse.js b/public/static/libs/ueditor/ueditor.parse.js new file mode 100644 index 0000000..b8c28d7 --- /dev/null +++ b/public/static/libs/ueditor/ueditor.parse.js @@ -0,0 +1,1022 @@ +/*! + * UEditor + * version: ueditor + * build: Thu Jun 16 2016 12:33:51 GMT+0800 (CST) + */ + +(function(){ + +(function(){ + UE = window.UE || {}; + var isIE = !!window.ActiveXObject; + //定义utils工具 + var utils = { + removeLastbs : function(url){ + return url.replace(/\/$/,'') + }, + extend : function(t,s){ + var a = arguments, + notCover = this.isBoolean(a[a.length - 1]) ? a[a.length - 1] : false, + len = this.isBoolean(a[a.length - 1]) ? a.length - 1 : a.length; + for (var i = 1; i < len; i++) { + var x = a[i]; + for (var k in x) { + if (!notCover || !t.hasOwnProperty(k)) { + t[k] = x[k]; + } + } + } + return t; + }, + isIE : isIE, + cssRule : isIE ? function(key,style,doc){ + var indexList,index; + doc = doc || document; + if(doc.indexList){ + indexList = doc.indexList; + }else{ + indexList = doc.indexList = {}; + } + var sheetStyle; + if(!indexList[key]){ + if(style === undefined){ + return '' + } + sheetStyle = doc.createStyleSheet('',index = doc.styleSheets.length); + indexList[key] = index; + }else{ + sheetStyle = doc.styleSheets[indexList[key]]; + } + if(style === undefined){ + return sheetStyle.cssText + } + sheetStyle.cssText = sheetStyle.cssText + '\n' + (style || '') + } : function(key,style,doc){ + doc = doc || document; + var head = doc.getElementsByTagName('head')[0],node; + if(!(node = doc.getElementById(key))){ + if(style === undefined){ + return '' + } + node = doc.createElement('style'); + node.id = key; + head.appendChild(node) + } + if(style === undefined){ + return node.innerHTML + } + if(style !== ''){ + node.innerHTML = node.innerHTML + '\n' + style; + }else{ + head.removeChild(node) + } + }, + domReady : function (onready) { + var doc = window.document; + if (doc.readyState === "complete") { + onready(); + }else{ + if (isIE) { + (function () { + if (doc.isReady) return; + try { + doc.documentElement.doScroll("left"); + } catch (error) { + setTimeout(arguments.callee, 0); + return; + } + onready(); + })(); + window.attachEvent('onload', function(){ + onready() + }); + } else { + doc.addEventListener("DOMContentLoaded", function () { + doc.removeEventListener("DOMContentLoaded", arguments.callee, false); + onready(); + }, false); + window.addEventListener('load', function(){onready()}, false); + } + } + + }, + each : function(obj, iterator, context) { + if (obj == null) return; + if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if(iterator.call(context, obj[i], i, obj) === false) + return false; + } + } else { + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + if(iterator.call(context, obj[key], key, obj) === false) + return false; + } + } + } + }, + inArray : function(arr,item){ + var index = -1; + this.each(arr,function(v,i){ + if(v === item){ + index = i; + return false; + } + }); + return index; + }, + pushItem : function(arr,item){ + if(this.inArray(arr,item)==-1){ + arr.push(item) + } + }, + trim: function (str) { + return str.replace(/(^[ \t\n\r]+)|([ \t\n\r]+$)/g, ''); + }, + indexOf: function (array, item, start) { + var index = -1; + start = this.isNumber(start) ? start : 0; + this.each(array, function (v, i) { + if (i >= start && v === item) { + index = i; + return false; + } + }); + return index; + }, + hasClass: function (element, className) { + className = className.replace(/(^[ ]+)|([ ]+$)/g, '').replace(/[ ]{2,}/g, ' ').split(' '); + for (var i = 0, ci, cls = element.className; ci = className[i++];) { + if (!new RegExp('\\b' + ci + '\\b', 'i').test(cls)) { + return false; + } + } + return i - 1 == className.length; + }, + addClass:function (elm, classNames) { + if(!elm)return; + classNames = this.trim(classNames).replace(/[ ]{2,}/g,' ').split(' '); + for(var i = 0,ci,cls = elm.className;ci=classNames[i++];){ + if(!new RegExp('\\b' + ci + '\\b').test(cls)){ + cls += ' ' + ci; + } + } + elm.className = utils.trim(cls); + }, + removeClass:function (elm, classNames) { + classNames = this.isArray(classNames) ? classNames : + this.trim(classNames).replace(/[ ]{2,}/g,' ').split(' '); + for(var i = 0,ci,cls = elm.className;ci=classNames[i++];){ + cls = cls.replace(new RegExp('\\b' + ci + '\\b'),'') + } + cls = this.trim(cls).replace(/[ ]{2,}/g,' '); + elm.className = cls; + !cls && elm.removeAttribute('className'); + }, + on: function (element, type, handler) { + var types = this.isArray(type) ? type : type.split(/\s+/), + k = types.length; + if (k) while (k--) { + type = types[k]; + if (element.addEventListener) { + element.addEventListener(type, handler, false); + } else { + if (!handler._d) { + handler._d = { + els : [] + }; + } + var key = type + handler.toString(),index = utils.indexOf(handler._d.els,element); + if (!handler._d[key] || index == -1) { + if(index == -1){ + handler._d.els.push(element); + } + if(!handler._d[key]){ + handler._d[key] = function (evt) { + return handler.call(evt.srcElement, evt || window.event); + }; + } + + + element.attachEvent('on' + type, handler._d[key]); + } + } + } + element = null; + }, + off: function (element, type, handler) { + var types = this.isArray(type) ? type : type.split(/\s+/), + k = types.length; + if (k) while (k--) { + type = types[k]; + if (element.removeEventListener) { + element.removeEventListener(type, handler, false); + } else { + var key = type + handler.toString(); + try{ + element.detachEvent('on' + type, handler._d ? handler._d[key] : handler); + }catch(e){} + if (handler._d && handler._d[key]) { + var index = utils.indexOf(handler._d.els,element); + if(index!=-1){ + handler._d.els.splice(index,1); + } + handler._d.els.length == 0 && delete handler._d[key]; + } + } + } + }, + loadFile : function () { + var tmpList = []; + function getItem(doc,obj){ + try{ + for(var i= 0,ci;ci=tmpList[i++];){ + if(ci.doc === doc && ci.url == (obj.src || obj.href)){ + return ci; + } + } + }catch(e){ + return null; + } + + } + return function (doc, obj, fn) { + var item = getItem(doc,obj); + if (item) { + if(item.ready){ + fn && fn(); + }else{ + item.funs.push(fn) + } + return; + } + tmpList.push({ + doc:doc, + url:obj.src||obj.href, + funs:[fn] + }); + if (!doc.body) { + var html = []; + for(var p in obj){ + if(p == 'tag')continue; + html.push(p + '="' + obj[p] + '"') + } + doc.write('<' + obj.tag + ' ' + html.join(' ') + ' >'); + return; + } + if (obj.id && doc.getElementById(obj.id)) { + return; + } + var element = doc.createElement(obj.tag); + delete obj.tag; + for (var p in obj) { + element.setAttribute(p, obj[p]); + } + element.onload = element.onreadystatechange = function () { + if (!this.readyState || /loaded|complete/.test(this.readyState)) { + item = getItem(doc,obj); + if (item.funs.length > 0) { + item.ready = 1; + for (var fi; fi = item.funs.pop();) { + fi(); + } + } + element.onload = element.onreadystatechange = null; + } + }; + element.onerror = function(){ + throw Error('The load '+(obj.href||obj.src)+' fails,check the url') + }; + doc.getElementsByTagName("head")[0].appendChild(element); + } + }() + }; + utils.each(['String', 'Function', 'Array', 'Number', 'RegExp', 'Object','Boolean'], function (v) { + utils['is' + v] = function (obj) { + return Object.prototype.toString.apply(obj) == '[object ' + v + ']'; + } + }); + var parselist = {}; + UE.parse = { + register : function(parseName,fn){ + parselist[parseName] = fn; + }, + load : function(opt){ + utils.each(parselist,function(v){ + v.call(opt,utils); + }) + } + }; + uParse = function(selector,opt){ + utils.domReady(function(){ + var contents; + if(document.querySelectorAll){ + contents = document.querySelectorAll(selector) + }else{ + if(/^#/.test(selector)){ + contents = [document.getElementById(selector.replace(/^#/,''))] + }else if(/^\./.test(selector)){ + var contents = []; + utils.each(document.getElementsByTagName('*'),function(node){ + if(node.className && new RegExp('\\b' + selector.replace(/^\./,'') + '\\b','i').test(node.className)){ + contents.push(node) + } + }) + }else{ + contents = document.getElementsByTagName(selector) + } + } + utils.each(contents,function(v){ + UE.parse.load(utils.extend({root:v,selector:selector},opt)) + }) + }) + } +})(); + +UE.parse.register('insertcode',function(utils){ + var pres = this.root.getElementsByTagName('pre'); + if(pres.length){ + if(typeof XRegExp == "undefined"){ + var jsurl,cssurl; + if(this.rootPath !== undefined){ + jsurl = utils.removeLastbs(this.rootPath) + '/third-party/SyntaxHighlighter/shCore.js'; + cssurl = utils.removeLastbs(this.rootPath) + '/third-party/SyntaxHighlighter/shCoreDefault.css'; + }else{ + jsurl = this.highlightJsUrl; + cssurl = this.highlightCssUrl; + } + utils.loadFile(document,{ + id : "syntaxhighlighter_css", + tag : "link", + rel : "stylesheet", + type : "text/css", + href : cssurl + }); + utils.loadFile(document,{ + id : "syntaxhighlighter_js", + src : jsurl, + tag : "script", + type : "text/javascript", + defer : "defer" + },function(){ + utils.each(pres,function(pi){ + if(pi && /brush/i.test(pi.className)){ + SyntaxHighlighter.highlight(pi); + } + }); + }); + }else{ + utils.each(pres,function(pi){ + if(pi && /brush/i.test(pi.className)){ + SyntaxHighlighter.highlight(pi); + } + }); + } + } + +}); +UE.parse.register('table', function (utils) { + var me = this, + root = this.root, + tables = root.getElementsByTagName('table'); + if (tables.length) { + var selector = this.selector; + //追加默认的表格样式 + utils.cssRule('table', + selector + ' table.noBorderTable td,' + + selector + ' table.noBorderTable th,' + + selector + ' table.noBorderTable caption{border:1px dashed #ddd !important}' + + selector + ' table.sortEnabled tr.firstRow th,' + selector + ' table.sortEnabled tr.firstRow td{padding-right:20px; background-repeat: no-repeat;' + + 'background-position: center right; background-image:url(' + this.rootPath + 'themes/default/images/sortable.png);}' + + selector + ' table.sortEnabled tr.firstRow th:hover,' + selector + ' table.sortEnabled tr.firstRow td:hover{background-color: #EEE;}' + + selector + ' table{margin-bottom:10px;border-collapse:collapse;display:table;}' + + selector + ' td,' + selector + ' th{ background:white; padding: 5px 10px;border: 1px solid #DDD;}' + + selector + ' caption{border:1px dashed #DDD;border-bottom:0;padding:3px;text-align:center;}' + + selector + ' th{border-top:1px solid #BBB;background:#F7F7F7;}' + + selector + ' table tr.firstRow th{border-top:2px solid #BBB;background:#F7F7F7;}' + + selector + ' tr.ue-table-interlace-color-single td{ background: #fcfcfc; }' + + selector + ' tr.ue-table-interlace-color-double td{ background: #f7faff; }' + + selector + ' td p{margin:0;padding:0;}', + document); + //填充空的单元格 + + utils.each('td th caption'.split(' '), function (tag) { + var cells = root.getElementsByTagName(tag); + cells.length && utils.each(cells, function (node) { + if (!node.firstChild) { + node.innerHTML = ' '; + + } + }) + }); + + //表格可排序 + var tables = root.getElementsByTagName('table'); + utils.each(tables, function (table) { + if (/\bsortEnabled\b/.test(table.className)) { + utils.on(table, 'click', function(e){ + var target = e.target || e.srcElement, + cell = findParentByTagName(target, ['td', 'th']); + var table = findParentByTagName(target, 'table'), + colIndex = utils.indexOf(table.rows[0].cells, cell), + sortType = table.getAttribute('data-sort-type'); + if(colIndex != -1) { + sortTable(table, colIndex, me.tableSortCompareFn || sortType); + updateTable(table); + } + }); + } + }); + + //按照标签名查找父节点 + function findParentByTagName(target, tagNames) { + var i, current = target; + tagNames = utils.isArray(tagNames) ? tagNames:[tagNames]; + while(current){ + for(i = 0;i < tagNames.length; i++) { + if(current.tagName == tagNames[i].toUpperCase()) return current; + } + current = current.parentNode; + } + return null; + } + //表格排序 + function sortTable(table, sortByCellIndex, compareFn) { + var rows = table.rows, + trArray = [], + flag = rows[0].cells[0].tagName === "TH", + lastRowIndex = 0; + + for (var i = 0,len = rows.length; i < len; i++) { + trArray[i] = rows[i]; + } + + var Fn = { + 'reversecurrent': function(td1,td2){ + return 1; + }, + 'orderbyasc': function(td1,td2){ + var value1 = td1.innerText||td1.textContent, + value2 = td2.innerText||td2.textContent; + return value1.localeCompare(value2); + }, + 'reversebyasc': function(td1,td2){ + var value1 = td1.innerHTML, + value2 = td2.innerHTML; + return value2.localeCompare(value1); + }, + 'orderbynum': function(td1,td2){ + var value1 = td1[utils.isIE ? 'innerText':'textContent'].match(/\d+/), + value2 = td2[utils.isIE ? 'innerText':'textContent'].match(/\d+/); + if(value1) value1 = +value1[0]; + if(value2) value2 = +value2[0]; + return (value1||0) - (value2||0); + }, + 'reversebynum': function(td1,td2){ + var value1 = td1[utils.isIE ? 'innerText':'textContent'].match(/\d+/), + value2 = td2[utils.isIE ? 'innerText':'textContent'].match(/\d+/); + if(value1) value1 = +value1[0]; + if(value2) value2 = +value2[0]; + return (value2||0) - (value1||0); + } + }; + + //对表格设置排序的标记data-sort-type + table.setAttribute('data-sort-type', compareFn && typeof compareFn === "string" && Fn[compareFn] ? compareFn:''); + + //th不参与排序 + flag && trArray.splice(0, 1); + trArray = sort(trArray,function (tr1, tr2) { + var result; + if (compareFn && typeof compareFn === "function") { + result = compareFn.call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); + } else if (compareFn && typeof compareFn === "number") { + result = 1; + } else if (compareFn && typeof compareFn === "string" && Fn[compareFn]) { + result = Fn[compareFn].call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); + } else { + result = Fn['orderbyasc'].call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); + } + return result; + }); + var fragment = table.ownerDocument.createDocumentFragment(); + for (var j = 0, len = trArray.length; j < len; j++) { + fragment.appendChild(trArray[j]); + } + var tbody = table.getElementsByTagName("tbody")[0]; + if(!lastRowIndex){ + tbody.appendChild(fragment); + }else{ + tbody.insertBefore(fragment,rows[lastRowIndex- range.endRowIndex + range.beginRowIndex - 1]) + } + } + //冒泡排序 + function sort(array, compareFn){ + compareFn = compareFn || function(item1, item2){ return item1.localeCompare(item2);}; + for(var i= 0,len = array.length; i 0){ + var t = array[i]; + array[i] = array[j]; + array[j] = t; + } + } + } + return array; + } + //更新表格 + function updateTable(table) { + //给第一行设置firstRow的样式名称,在排序图标的样式上使用到 + if(!utils.hasClass(table.rows[0], "firstRow")) { + for(var i = 1; i< table.rows.length; i++) { + utils.removeClass(table.rows[i], "firstRow"); + } + utils.addClass(table.rows[0], "firstRow"); + } + } + } +}); +UE.parse.register('charts',function( utils ){ + + utils.cssRule('chartsContainerHeight','.edui-chart-container { height:'+(this.chartContainerHeight||300)+'px}'); + var resourceRoot = this.rootPath, + containers = this.root, + sources = null; + + //不存在指定的根路径, 则直接退出 + if ( !resourceRoot ) { + return; + } + + if ( sources = parseSources() ) { + + loadResources(); + + } + + + function parseSources () { + + if ( !containers ) { + return null; + } + + return extractChartData( containers ); + + } + + /** + * 提取数据 + */ + function extractChartData ( rootNode ) { + + var data = [], + tables = rootNode.getElementsByTagName( "table" ); + + for ( var i = 0, tableNode; tableNode = tables[ i ]; i++ ) { + + if ( tableNode.getAttribute( "data-chart" ) !== null ) { + + data.push( formatData( tableNode ) ); + + } + + } + + return data.length ? data : null; + + } + + function formatData ( tableNode ) { + + var meta = tableNode.getAttribute( "data-chart" ), + metaConfig = {}, + data = []; + + //提取table数据 + for ( var i = 0, row; row = tableNode.rows[ i ]; i++ ) { + + var rowData = []; + + for ( var j = 0, cell; cell = row.cells[ j ]; j++ ) { + + var value = ( cell.innerText || cell.textContent || '' ); + rowData.push( cell.tagName == 'TH' ? value:(value | 0) ); + + } + + data.push( rowData ); + + } + + //解析元信息 + meta = meta.split( ";" ); + for ( var i = 0, metaData; metaData = meta[ i ]; i++ ) { + + metaData = metaData.split( ":" ); + metaConfig[ metaData[ 0 ] ] = metaData[ 1 ]; + + } + + + return { + table: tableNode, + meta: metaConfig, + data: data + }; + + } + + //加载资源 + function loadResources () { + + loadJQuery(); + + } + + function loadJQuery () { + + //不存在jquery, 则加载jquery + if ( !window.jQuery ) { + + utils.loadFile(document,{ + src : resourceRoot + "/third-party/jquery-1.10.2.min.js", + tag : "script", + type : "text/javascript", + defer : "defer" + },function(){ + + loadHighcharts(); + + }); + + } else { + + loadHighcharts(); + + } + + } + + function loadHighcharts () { + + //不存在Highcharts, 则加载Highcharts + if ( !window.Highcharts ) { + + utils.loadFile(document,{ + src : resourceRoot + "/third-party/highcharts/highcharts.js", + tag : "script", + type : "text/javascript", + defer : "defer" + },function(){ + + loadTypeConfig(); + + }); + + } else { + + loadTypeConfig(); + + } + + } + + //加载图表差异化配置文件 + function loadTypeConfig () { + + utils.loadFile(document,{ + src : resourceRoot + "/dialogs/charts/chart.config.js", + tag : "script", + type : "text/javascript", + defer : "defer" + },function(){ + + render(); + + }); + + } + + //渲染图表 + function render () { + + var config = null, + chartConfig = null, + container = null; + + for ( var i = 0, len = sources.length; i < len; i++ ) { + + config = sources[ i ]; + + chartConfig = analysisConfig( config ); + + container = createContainer( config.table ); + + renderChart( container, typeConfig[ config.meta.chartType ], chartConfig ); + + } + + + } + + /** + * 渲染图表 + * @param container 图表容器节点对象 + * @param typeConfig 图表类型配置 + * @param config 图表通用配置 + * */ + function renderChart ( container, typeConfig, config ) { + + + $( container ).highcharts( $.extend( {}, typeConfig, { + + credits: { + enabled: false + }, + exporting: { + enabled: false + }, + title: { + text: config.title, + x: -20 //center + }, + subtitle: { + text: config.subTitle, + x: -20 + }, + xAxis: { + title: { + text: config.xTitle + }, + categories: config.categories + }, + yAxis: { + title: { + text: config.yTitle + }, + plotLines: [{ + value: 0, + width: 1, + color: '#808080' + }] + }, + tooltip: { + enabled: true, + valueSuffix: config.suffix + }, + legend: { + layout: 'vertical', + align: 'right', + verticalAlign: 'middle', + borderWidth: 1 + }, + series: config.series + + } )); + + } + + /** + * 创建图表的容器 + * 新创建的容器会替换掉对应的table对象 + * */ + function createContainer ( tableNode ) { + + var container = document.createElement( "div" ); + container.className = "edui-chart-container"; + + tableNode.parentNode.replaceChild( container, tableNode ); + + return container; + + } + + //根据config解析出正确的类别和图表数据信息 + function analysisConfig ( config ) { + + var series = [], + //数据类别 + categories = [], + result = [], + data = config.data, + meta = config.meta; + + //数据对齐方式为相反的方式, 需要反转数据 + if ( meta.dataFormat != "1" ) { + + for ( var i = 0, len = data.length; i < len ; i++ ) { + + for ( var j = 0, jlen = data[ i ].length; j < jlen; j++ ) { + + if ( !result[ j ] ) { + result[ j ] = []; + } + + result[ j ][ i ] = data[ i ][ j ]; + + } + + } + + data = result; + + } + + result = {}; + + //普通图表 + if ( meta.chartType != typeConfig.length - 1 ) { + + categories = data[ 0 ].slice( 1 ); + + for ( var i = 1, curData; curData = data[ i ]; i++ ) { + series.push( { + name: curData[ 0 ], + data: curData.slice( 1 ) + } ); + } + + result.series = series; + result.categories = categories; + result.title = meta.title; + result.subTitle = meta.subTitle; + result.xTitle = meta.xTitle; + result.yTitle = meta.yTitle; + result.suffix = meta.suffix; + + } else { + + var curData = []; + + for ( var i = 1, len = data[ 0 ].length; i < len; i++ ) { + + curData.push( [ data[ 0 ][ i ], data[ 1 ][ i ] | 0 ] ); + + } + + //饼图 + series[ 0 ] = { + type: 'pie', + name: meta.tip, + data: curData + }; + + result.series = series; + result.title = meta.title; + result.suffix = meta.suffix; + + } + + return result; + + } + +}); +UE.parse.register('background', function (utils) { + var me = this, + root = me.root, + p = root.getElementsByTagName('p'), + styles; + + for (var i = 0,ci; ci = p[i++];) { + styles = ci.getAttribute('data-background'); + if (styles){ + ci.parentNode.removeChild(ci); + } + } + + //追加默认的表格样式 + styles && utils.cssRule('ueditor_background', me.selector + '{' + styles + '}', document); +}); +UE.parse.register('list',function(utils){ + var customCss = [], + customStyle = { + 'cn' : 'cn-1-', + 'cn1' : 'cn-2-', + 'cn2' : 'cn-3-', + 'num' : 'num-1-', + 'num1' : 'num-2-', + 'num2' : 'num-3-', + 'dash' : 'dash', + 'dot' : 'dot' + }; + + + utils.extend(this,{ + liiconpath : 'http://bs.baidu.com/listicon/', + listDefaultPaddingLeft : '20' + }); + + var root = this.root, + ols = root.getElementsByTagName('ol'), + uls = root.getElementsByTagName('ul'), + selector = this.selector; + + if(ols.length){ + applyStyle.call(this,ols); + } + + if(uls.length){ + applyStyle.call(this,uls); + } + + if(ols.length || uls.length){ + customCss.push(selector +' .list-paddingleft-1{padding-left:0}'); + customCss.push(selector +' .list-paddingleft-2{padding-left:'+ this.listDefaultPaddingLeft+'px}'); + customCss.push(selector +' .list-paddingleft-3{padding-left:'+ this.listDefaultPaddingLeft*2+'px}'); + + utils.cssRule('list', selector +' ol,'+selector +' ul{margin:0;padding:0;}li{clear:both;}'+customCss.join('\n'), document); + } + function applyStyle(nodes){ + var T = this; + utils.each(nodes,function(list){ + if(list.className && /custom_/i.test(list.className)){ + var listStyle = list.className.match(/custom_(\w+)/)[1]; + if(listStyle == 'dash' || listStyle == 'dot'){ + utils.pushItem(customCss,selector +' li.list-' + customStyle[listStyle] + '{background-image:url(' + T.liiconpath +customStyle[listStyle]+'.gif)}'); + utils.pushItem(customCss,selector +' ul.custom_'+listStyle+'{list-style:none;} '+ selector +' ul.custom_'+listStyle+' li{background-position:0 3px;background-repeat:no-repeat}'); + + }else{ + var index = 1; + utils.each(list.childNodes,function(li){ + if(li.tagName == 'LI'){ + utils.pushItem(customCss,selector + ' li.list-' + customStyle[listStyle] + index + '{background-image:url(' + T.liiconpath + 'list-'+customStyle[listStyle] +index + '.gif)}'); + index++; + } + }); + utils.pushItem(customCss,selector + ' ol.custom_'+listStyle+'{list-style:none;}'+selector+' ol.custom_'+listStyle+' li{background-position:0 3px;background-repeat:no-repeat}'); + } + switch(listStyle){ + case 'cn': + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:25px}'); + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-2{padding-left:40px}'); + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-3{padding-left:55px}'); + break; + case 'cn1': + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:30px}'); + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-2{padding-left:40px}'); + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-3{padding-left:55px}'); + break; + case 'cn2': + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:40px}'); + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-2{padding-left:55px}'); + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-3{padding-left:68px}'); + break; + case 'num': + case 'num1': + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:25px}'); + break; + case 'num2': + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:35px}'); + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-2{padding-left:40px}'); + break; + case 'dash': + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft{padding-left:35px}'); + break; + case 'dot': + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft{padding-left:20px}'); + } + } + }); + } + + +}); +UE.parse.register('vedio',function(utils){ + var video = this.root.getElementsByTagName('video'), + audio = this.root.getElementsByTagName('audio'); + + document.createElement('video');document.createElement('audio'); + if(video.length || audio.length){ + var sourcePath = utils.removeLastbs(this.rootPath), + jsurl = sourcePath + '/third-party/video-js/video.js', + cssurl = sourcePath + '/third-party/video-js/video-js.min.css', + swfUrl = sourcePath + '/third-party/video-js/video-js.swf'; + + if(window.videojs) { + videojs.autoSetup(); + } else { + utils.loadFile(document,{ + id : "video_css", + tag : "link", + rel : "stylesheet", + type : "text/css", + href : cssurl + }); + utils.loadFile(document,{ + id : "video_js", + src : jsurl, + tag : "script", + type : "text/javascript" + },function(){ + videojs.options.flash.swf = swfUrl; + videojs.autoSetup(); + }); + } + + } +}); + +})(); diff --git a/public/static/libs/ueditor/ueditor.parse.min.js b/public/static/libs/ueditor/ueditor.parse.min.js new file mode 100644 index 0000000..17ccc55 --- /dev/null +++ b/public/static/libs/ueditor/ueditor.parse.min.js @@ -0,0 +1,7 @@ +/*! + * UEditor + * version: ueditor + * build: Thu Jun 16 2016 12:33:53 GMT+0800 (CST) + */ + +!function(){!function(){UE=window.UE||{};var a=!!window.ActiveXObject,b={removeLastbs:function(a){return a.replace(/\/$/,"")},extend:function(a,b){for(var c=arguments,d=this.isBoolean(c[c.length-1])?c[c.length-1]:!1,e=this.isBoolean(c[c.length-1])?c.length-1:c.length,f=1;e>f;f++){var g=c[f];for(var h in g)d&&a.hasOwnProperty(h)||(a[h]=g[h])}return a},isIE:a,cssRule:a?function(a,b,c){var d,e;c=c||document,d=c.indexList?c.indexList:c.indexList={};var f;if(d[a])f=c.styleSheets[d[a]];else{if(void 0===b)return"";f=c.createStyleSheet("",e=c.styleSheets.length),d[a]=e}return void 0===b?f.cssText:void(f.cssText=f.cssText+"\n"+(b||""))}:function(a,b,c){c=c||document;var d,e=c.getElementsByTagName("head")[0];if(!(d=c.getElementById(a))){if(void 0===b)return"";d=c.createElement("style"),d.id=a,e.appendChild(d)}return void 0===b?d.innerHTML:void(""!==b?d.innerHTML=d.innerHTML+"\n"+b:e.removeChild(d))},domReady:function(b){var c=window.document;"complete"===c.readyState?b():a?(!function(){if(!c.isReady){try{c.documentElement.doScroll("left")}catch(a){return void setTimeout(arguments.callee,0)}b()}}(),window.attachEvent("onload",function(){b()})):(c.addEventListener("DOMContentLoaded",function(){c.removeEventListener("DOMContentLoaded",arguments.callee,!1),b()},!1),window.addEventListener("load",function(){b()},!1))},each:function(a,b,c){if(null!=a)if(a.length===+a.length){for(var d=0,e=a.length;e>d;d++)if(b.call(c,a[d],d,a)===!1)return!1}else for(var f in a)if(a.hasOwnProperty(f)&&b.call(c,a[f],f,a)===!1)return!1},inArray:function(a,b){var c=-1;return this.each(a,function(a,d){return a===b?(c=d,!1):void 0}),c},pushItem:function(a,b){-1==this.inArray(a,b)&&a.push(b)},trim:function(a){return a.replace(/(^[ \t\n\r]+)|([ \t\n\r]+$)/g,"")},indexOf:function(a,b,c){var d=-1;return c=this.isNumber(c)?c:0,this.each(a,function(a,e){return e>=c&&a===b?(d=e,!1):void 0}),d},hasClass:function(a,b){b=b.replace(/(^[ ]+)|([ ]+$)/g,"").replace(/[ ]{2,}/g," ").split(" ");for(var c,d=0,e=a.className;c=b[d++];)if(!new RegExp("\\b"+c+"\\b","i").test(e))return!1;return d-1==b.length},addClass:function(a,c){if(a){c=this.trim(c).replace(/[ ]{2,}/g," ").split(" ");for(var d,e=0,f=a.className;d=c[e++];)new RegExp("\\b"+d+"\\b").test(f)||(f+=" "+d);a.className=b.trim(f)}},removeClass:function(a,b){b=this.isArray(b)?b:this.trim(b).replace(/[ ]{2,}/g," ").split(" ");for(var c,d=0,e=a.className;c=b[d++];)e=e.replace(new RegExp("\\b"+c+"\\b"),"");e=this.trim(e).replace(/[ ]{2,}/g," "),a.className=e,!e&&a.removeAttribute("className")},on:function(a,c,d){var e=this.isArray(c)?c:c.split(/\s+/),f=e.length;if(f)for(;f--;)if(c=e[f],a.addEventListener)a.addEventListener(c,d,!1);else{d._d||(d._d={els:[]});var g=c+d.toString(),h=b.indexOf(d._d.els,a);d._d[g]&&-1!=h||(-1==h&&d._d.els.push(a),d._d[g]||(d._d[g]=function(a){return d.call(a.srcElement,a||window.event)}),a.attachEvent("on"+c,d._d[g]))}a=null},off:function(a,c,d){var e=this.isArray(c)?c:c.split(/\s+/),f=e.length;if(f)for(;f--;)if(c=e[f],a.removeEventListener)a.removeEventListener(c,d,!1);else{var g=c+d.toString();try{a.detachEvent("on"+c,d._d?d._d[g]:d)}catch(h){}if(d._d&&d._d[g]){var i=b.indexOf(d._d.els,a);-1!=i&&d._d.els.splice(i,1),0==d._d.els.length&&delete d._d[g]}}},loadFile:function(){function a(a,c){try{for(var d,e=0;d=b[e++];)if(d.doc===a&&d.url==(c.src||c.href))return d}catch(f){return null}}var b=[];return function(c,d,e){var f=a(c,d);if(f)return void(f.ready?e&&e():f.funs.push(e));if(b.push({doc:c,url:d.src||d.href,funs:[e]}),!c.body){var g=[];for(var h in d)"tag"!=h&&g.push(h+'="'+d[h]+'"');return void c.write("<"+d.tag+" "+g.join(" ")+" >")}if(!d.id||!c.getElementById(d.id)){var i=c.createElement(d.tag);delete d.tag;for(var h in d)i.setAttribute(h,d[h]);i.onload=i.onreadystatechange=function(){if(!this.readyState||/loaded|complete/.test(this.readyState)){if(f=a(c,d),f.funs.length>0){f.ready=1;for(var b;b=f.funs.pop();)b()}i.onload=i.onreadystatechange=null}},i.onerror=function(){throw Error("The load "+(d.href||d.src)+" fails,check the url")},c.getElementsByTagName("head")[0].appendChild(i)}}}()};b.each(["String","Function","Array","Number","RegExp","Object","Boolean"],function(a){b["is"+a]=function(b){return Object.prototype.toString.apply(b)=="[object "+a+"]"}});var c={};UE.parse={register:function(a,b){c[a]=b},load:function(a){b.each(c,function(c){c.call(a,b)})}},uParse=function(a,c){b.domReady(function(){var d;if(document.querySelectorAll)d=document.querySelectorAll(a);else if(/^#/.test(a))d=[document.getElementById(a.replace(/^#/,""))];else if(/^\./.test(a)){var d=[];b.each(document.getElementsByTagName("*"),function(b){b.className&&new RegExp("\\b"+a.replace(/^\./,"")+"\\b","i").test(b.className)&&d.push(b)})}else d=document.getElementsByTagName(a);b.each(d,function(d){UE.parse.load(b.extend({root:d,selector:a},c))})})}}(),UE.parse.register("insertcode",function(a){var b=this.root.getElementsByTagName("pre");if(b.length)if("undefined"==typeof XRegExp){var c,d;void 0!==this.rootPath?(c=a.removeLastbs(this.rootPath)+"/third-party/SyntaxHighlighter/shCore.js",d=a.removeLastbs(this.rootPath)+"/third-party/SyntaxHighlighter/shCoreDefault.css"):(c=this.highlightJsUrl,d=this.highlightCssUrl),a.loadFile(document,{id:"syntaxhighlighter_css",tag:"link",rel:"stylesheet",type:"text/css",href:d}),a.loadFile(document,{id:"syntaxhighlighter_js",src:c,tag:"script",type:"text/javascript",defer:"defer"},function(){a.each(b,function(a){a&&/brush/i.test(a.className)&&SyntaxHighlighter.highlight(a)})})}else a.each(b,function(a){a&&/brush/i.test(a.className)&&SyntaxHighlighter.highlight(a)})}),UE.parse.register("table",function(a){function b(b,c){var d,e=b;for(c=a.isArray(c)?c:[c];e;){for(d=0;dj;j++)g[j]=f[j];var l={reversecurrent:function(a,b){return 1},orderbyasc:function(a,b){var c=a.innerText||a.textContent,d=b.innerText||b.textContent;return c.localeCompare(d)},reversebyasc:function(a,b){var c=a.innerHTML,d=b.innerHTML;return d.localeCompare(c)},orderbynum:function(b,c){var d=b[a.isIE?"innerText":"textContent"].match(/\d+/),e=c[a.isIE?"innerText":"textContent"].match(/\d+/);return d&&(d=+d[0]),e&&(e=+e[0]),(d||0)-(e||0)},reversebynum:function(b,c){var d=b[a.isIE?"innerText":"textContent"].match(/\d+/),e=c[a.isIE?"innerText":"textContent"].match(/\d+/);return d&&(d=+d[0]),e&&(e=+e[0]),(e||0)-(d||0)}};b.setAttribute("data-sort-type",e&&"string"==typeof e&&l[e]?e:""),h&&g.splice(0,1),g=d(g,function(a,b){var d;return d=e&&"function"==typeof e?e.call(this,a.cells[c],b.cells[c]):e&&"number"==typeof e?1:e&&"string"==typeof e&&l[e]?l[e].call(this,a.cells[c],b.cells[c]):l.orderbyasc.call(this,a.cells[c],b.cells[c])});for(var m=b.ownerDocument.createDocumentFragment(),n=0,k=g.length;k>n;n++)m.appendChild(g[n]);var o=b.getElementsByTagName("tbody")[0];i?o.insertBefore(m,f[i-range.endRowIndex+range.beginRowIndex-1]):o.appendChild(m)}function d(a,b){b=b||function(a,b){return a.localeCompare(b)};for(var c=0,d=a.length;d>c;c++)for(var e=c,f=a.length;f>e;e++)if(b(a[c],a[e])>0){var g=a[c];a[c]=a[e],a[e]=g}return a}function e(b){if(!a.hasClass(b.rows[0],"firstRow")){for(var c=1;cd;d++)a=o[d],b=l(a),c=k(a.table),j(c,typeConfig[a.meta.chartType],b)}function j(a,b,c){$(a).highcharts($.extend({},b,{credits:{enabled:!1},exporting:{enabled:!1},title:{text:c.title,x:-20},subtitle:{text:c.subTitle,x:-20},xAxis:{title:{text:c.xTitle},categories:c.categories},yAxis:{title:{text:c.yTitle},plotLines:[{value:0,width:1,color:"#808080"}]},tooltip:{enabled:!0,valueSuffix:c.suffix},legend:{layout:"vertical",align:"right",verticalAlign:"middle",borderWidth:1},series:c.series}))}function k(a){var b=document.createElement("div");return b.className="edui-chart-container",a.parentNode.replaceChild(b,a),b}function l(a){var b=[],c=[],d=[],e=a.data,f=a.meta;if("1"!=f.dataFormat){for(var g=0,h=e.length;h>g;g++)for(var i=0,j=e[g].length;j>i;i++)d[i]||(d[i]=[]),d[i][g]=e[g][i];e=d}if(d={},f.chartType!=typeConfig.length-1){c=e[0].slice(1);for(var k,g=1;k=e[g];g++)b.push({name:k[0],data:k.slice(1)});d.series=b,d.categories=c,d.title=f.title,d.subTitle=f.subTitle,d.xTitle=f.xTitle,d.yTitle=f.yTitle,d.suffix=f.suffix}else{for(var k=[],g=1,h=e[0].length;h>g;g++)k.push([e[0][g],0|e[1][g]]);b[0]={type:"pie",name:f.tip,data:k},d.series=b,d.title=f.title,d.suffix=f.suffix}return d}a.cssRule("chartsContainerHeight",".edui-chart-container { height:"+(this.chartContainerHeight||300)+"px}");var m=this.rootPath,n=this.root,o=null;m&&(o=b())&&e()}),UE.parse.register("background",function(a){for(var b,c,d=this,e=d.root,f=e.getElementsByTagName("p"),g=0;c=f[g++];)b=c.getAttribute("data-background"),b&&c.parentNode.removeChild(c);b&&a.cssRule("ueditor_background",d.selector+"{"+b+"}",document)}),UE.parse.register("list",function(a){function b(b){var e=this;a.each(b,function(b){if(b.className&&/custom_/i.test(b.className)){var f=b.className.match(/custom_(\w+)/)[1];if("dash"==f||"dot"==f)a.pushItem(c,h+" li.list-"+d[f]+"{background-image:url("+e.liiconpath+d[f]+".gif)}"),a.pushItem(c,h+" ul.custom_"+f+"{list-style:none;} "+h+" ul.custom_"+f+" li{background-position:0 3px;background-repeat:no-repeat}");else{var g=1;a.each(b.childNodes,function(b){"LI"==b.tagName&&(a.pushItem(c,h+" li.list-"+d[f]+g+"{background-image:url("+e.liiconpath+"list-"+d[f]+g+".gif)}"),g++)}),a.pushItem(c,h+" ol.custom_"+f+"{list-style:none;}"+h+" ol.custom_"+f+" li{background-position:0 3px;background-repeat:no-repeat}")}switch(f){case"cn":a.pushItem(c,h+" li.list-"+f+"-paddingleft-1{padding-left:25px}"),a.pushItem(c,h+" li.list-"+f+"-paddingleft-2{padding-left:40px}"),a.pushItem(c,h+" li.list-"+f+"-paddingleft-3{padding-left:55px}");break;case"cn1":a.pushItem(c,h+" li.list-"+f+"-paddingleft-1{padding-left:30px}"),a.pushItem(c,h+" li.list-"+f+"-paddingleft-2{padding-left:40px}"),a.pushItem(c,h+" li.list-"+f+"-paddingleft-3{padding-left:55px}");break;case"cn2":a.pushItem(c,h+" li.list-"+f+"-paddingleft-1{padding-left:40px}"),a.pushItem(c,h+" li.list-"+f+"-paddingleft-2{padding-left:55px}"),a.pushItem(c,h+" li.list-"+f+"-paddingleft-3{padding-left:68px}");break;case"num":case"num1":a.pushItem(c,h+" li.list-"+f+"-paddingleft-1{padding-left:25px}");break;case"num2":a.pushItem(c,h+" li.list-"+f+"-paddingleft-1{padding-left:35px}"),a.pushItem(c,h+" li.list-"+f+"-paddingleft-2{padding-left:40px}");break;case"dash":a.pushItem(c,h+" li.list-"+f+"-paddingleft{padding-left:35px}");break;case"dot":a.pushItem(c,h+" li.list-"+f+"-paddingleft{padding-left:20px}")}}})}var c=[],d={cn:"cn-1-",cn1:"cn-2-",cn2:"cn-3-",num:"num-1-",num1:"num-2-",num2:"num-3-",dash:"dash",dot:"dot"};a.extend(this,{liiconpath:"http://bs.baidu.com/listicon/",listDefaultPaddingLeft:"20"});var e=this.root,f=e.getElementsByTagName("ol"),g=e.getElementsByTagName("ul"),h=this.selector;f.length&&b.call(this,f),g.length&&b.call(this,g),(f.length||g.length)&&(c.push(h+" .list-paddingleft-1{padding-left:0}"),c.push(h+" .list-paddingleft-2{padding-left:"+this.listDefaultPaddingLeft+"px}"),c.push(h+" .list-paddingleft-3{padding-left:"+2*this.listDefaultPaddingLeft+"px}"),a.cssRule("list",h+" ol,"+h+" ul{margin:0;padding:0;}li{clear:both;}"+c.join("\n"),document))}),UE.parse.register("vedio",function(a){var b=this.root.getElementsByTagName("video"),c=this.root.getElementsByTagName("audio");if(document.createElement("video"),document.createElement("audio"),b.length||c.length){var d=a.removeLastbs(this.rootPath),e=d+"/third-party/video-js/video.js",f=d+"/third-party/video-js/video-js.min.css",g=d+"/third-party/video-js/video-js.swf";window.videojs?videojs.autoSetup():(a.loadFile(document,{id:"video_css",tag:"link",rel:"stylesheet",type:"text/css",href:f}),a.loadFile(document,{id:"video_js",src:e,tag:"script",type:"text/javascript"},function(){videojs.options.flash.swf=g,videojs.autoSetup()}))}})}(); \ No newline at end of file diff --git a/public/static/libs/viewer/viewer.common.js b/public/static/libs/viewer/viewer.common.js new file mode 100644 index 0000000..6afdead --- /dev/null +++ b/public/static/libs/viewer/viewer.common.js @@ -0,0 +1,2052 @@ +/*! + * Viewer v0.6.0 + * https://github.com/fengyuanchen/viewer + * + * Copyright (c) 2014-2017 Fengyuan Chen + * Released under the MIT license + * + * Date: 2017-10-07T09:53:36.889Z + */ +'use strict'; + +function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } + +var $ = _interopDefault(require('jquery')); + +var DEFAULTS = { + // Enable inline mode + inline: false, + + // Show the button on the top-right of the viewer + button: true, + + // Show the navbar + navbar: true, + + // Show the title + title: true, + + // Show the toolbar + toolbar: true, + + // Show the tooltip with image ratio (percentage) when zoom in or zoom out + tooltip: true, + + // Enable to move the image + movable: true, + + // Enable to zoom the image + zoomable: true, + + // Enable to rotate the image + rotatable: true, + + // Enable to scale the image + scalable: true, + + // Enable CSS3 Transition for some special elements + transition: true, + + // Enable to request fullscreen when play + fullscreen: true, + + // Enable keyboard support + keyboard: true, + + // Define interval of each image when playing + interval: 5000, + + // Min width of the viewer in inline mode + minWidth: 200, + + // Min height of the viewer in inline mode + minHeight: 100, + + // Define the ratio when zoom the image by wheeling mouse + zoomRatio: 0.1, + + // Define the min ratio of the image when zoom out + minZoomRatio: 0.01, + + // Define the max ratio of the image when zoom in + maxZoomRatio: 100, + + // Define the CSS `z-index` value of viewer in modal mode. + zIndex: 2015, + + // Define the CSS `z-index` value of viewer in inline mode. + zIndexInline: 0, + + // Define where to get the original image URL for viewing + // Type: String (an image attribute) or Function (should return an image URL) + url: 'src', + + // Event shortcuts + ready: null, + show: null, + shown: null, + hide: null, + hidden: null, + view: null, + viewed: null +}; + +var TEMPLATE = '
              ' + '
              ' + '' + '
              ' + '
              ' + '
              ' + '
              '; + +var _window = window; +var PointerEvent = _window.PointerEvent; + + +var NAMESPACE = 'viewer'; + +// Actions +var ACTION_MOVE = 'move'; +var ACTION_SWITCH = 'switch'; +var ACTION_ZOOM = 'zoom'; + +// Classes +var CLASS_ACTIVE = NAMESPACE + '-active'; +var CLASS_CLOSE = NAMESPACE + '-close'; +var CLASS_FADE = NAMESPACE + '-fade'; +var CLASS_FIXED = NAMESPACE + '-fixed'; +var CLASS_FULLSCREEN = NAMESPACE + '-fullscreen'; +var CLASS_FULLSCREEN_EXIT = NAMESPACE + '-fullscreen-exit'; +var CLASS_HIDE = NAMESPACE + '-hide'; +var CLASS_HIDE_MD_DOWN = NAMESPACE + '-hide-md-down'; +var CLASS_HIDE_SM_DOWN = NAMESPACE + '-hide-sm-down'; +var CLASS_HIDE_XS_DOWN = NAMESPACE + '-hide-xs-down'; +var CLASS_IN = NAMESPACE + '-in'; +var CLASS_INVISIBLE = NAMESPACE + '-invisible'; +var CLASS_MOVE = NAMESPACE + '-move'; +var CLASS_OPEN = NAMESPACE + '-open'; +var CLASS_SHOW = NAMESPACE + '-show'; +var CLASS_TRANSITION = NAMESPACE + '-transition'; + +// Events +var EVENT_READY = 'ready'; +var EVENT_SHOW = 'show'; +var EVENT_SHOWN = 'shown'; +var EVENT_HIDE = 'hide'; +var EVENT_HIDDEN = 'hidden'; +var EVENT_VIEW = 'view'; +var EVENT_VIEWED = 'viewed'; +var EVENT_CLICK = 'click'; +var EVENT_DRAG_START = 'dragstart'; +var EVENT_KEY_DOWN = 'keydown'; +var EVENT_LOAD = 'load'; +var EVENT_POINTER_DOWN = PointerEvent ? 'pointerdown' : 'touchstart mousedown'; +var EVENT_POINTER_MOVE = PointerEvent ? 'pointermove' : 'mousemove touchmove'; +var EVENT_POINTER_UP = PointerEvent ? 'pointerup pointercancel' : 'touchend touchcancel mouseup'; +var EVENT_RESIZE = 'resize'; +var EVENT_TRANSITIONEND = 'transitionend'; +var EVENT_WHEEL = 'wheel mousewheel DOMMouseScroll'; + +/** + * Check if the given value is a string. + * @param {*} value - The value to check. + * @returns {boolean} Returns `true` if the given value is a string, else `false`. + */ +function isString(value) { + return typeof value === 'string'; +} + +/** + * Check if the given value is not a number. + */ +var isNaN = Number.isNaN || window.isNaN; + +/** + * Check if the given value is a number. + * @param {*} value - The value to check. + * @returns {boolean} Returns `true` if the given value is a number, else `false`. + */ +function isNumber(value) { + return typeof value === 'number' && !isNaN(value); +} + +/** + * Check if the given value is undefined. + * @param {*} value - The value to check. + * @returns {boolean} Returns `true` if the given value is undefined, else `false`. + */ +function isUndefined(value) { + return typeof value === 'undefined'; +} + +/** + * Takes a function and returns a new one that will always have a particular context. + * Custom proxy to avoid jQuery's guid. + * @param {Function} fn - The target function. + * @param {Object} context - The new context for the function. + * @returns {boolean} The new function. + */ +function proxy(fn, context) { + for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { + args[_key - 2] = arguments[_key]; + } + + return function () { + for (var _len2 = arguments.length, args2 = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args2[_key2] = arguments[_key2]; + } + + return fn.apply(context, args.concat(args2)); + }; +} + +/** + * Get the own enumerable properties of a given object. + * @param {Object} obj - The target object. + * @returns {Array} All the own enumerable properties of the given object. + */ +var objectKeys = Object.keys || function objectKeys(obj) { + var keys = []; + + $.each(obj, function (key) { + keys.push(key); + }); + + return keys; +}; + +/** + * Get transform values from an object. + * @param {Object} obj - The target object. + * @returns {string} A string contains transform values. + */ +function getTransformValues(_ref) { + var rotate = _ref.rotate, + scaleX = _ref.scaleX, + scaleY = _ref.scaleY; + + var values = []; + + if (isNumber(rotate) && rotate !== 0) { + values.push('rotate(' + rotate + 'deg)'); + } + + if (isNumber(scaleX) && scaleX !== 1) { + values.push('scaleX(' + scaleX + ')'); + } + + if (isNumber(scaleY) && scaleY !== 1) { + values.push('scaleY(' + scaleY + ')'); + } + + return values.length > 0 ? values.join(' ') : 'none'; +} + +/** + * Get an image name from an image url. + * @param {string} url - The target url. + * @example + * // picture.jpg + * getImageNameFromURL('http://domain.com/path/to/picture.jpg?size=1280×960') + * @returns {string} A string contains the image name. + */ +function getImageNameFromURL(url) { + return isString(url) ? url.replace(/^.*\//, '').replace(/[?&#].*$/, '') : ''; +} + +/** + * Get an image's natural sizes. + * @param {string} image - The target image. + * @param {Function} callback - The callback function. + */ +function getImageNaturalSizes(image, callback) { + // Modern browsers and IE9+ + if (image.naturalWidth) { + callback(image.naturalWidth, image.naturalHeight); + return; + } + + // IE8 (Don't use `new Image()` here) + var newImage = document.createElement('img'); + + newImage.onload = function () { + callback(newImage.width, newImage.height); + }; + + newImage.src = image.src; +} + +/** + * Get the related class name of a responsive type number. + * @param {string} type - The responsive type. + * @returns {string} The related class name. + */ +function getResponsiveClass(type) { + switch (type) { + case 2: + return CLASS_HIDE_XS_DOWN; + + case 3: + return CLASS_HIDE_SM_DOWN; + + case 4: + return CLASS_HIDE_MD_DOWN; + + default: + return ''; + } +} + +/** + * Get the max ratio of a group of pointers. + * @param {string} pointers - The target pointers. + * @returns {number} The result ratio. + */ +function getMaxZoomRatio(pointers) { + var pointers2 = $.extend({}, pointers); + var ratios = []; + + $.each(pointers, function (pointerId, pointer) { + delete pointers2[pointerId]; + + $.each(pointers2, function (pointerId2, pointer2) { + var x1 = Math.abs(pointer.startX - pointer2.startX); + var y1 = Math.abs(pointer.startY - pointer2.startY); + var x2 = Math.abs(pointer.endX - pointer2.endX); + var y2 = Math.abs(pointer.endY - pointer2.endY); + var z1 = Math.sqrt(x1 * x1 + y1 * y1); + var z2 = Math.sqrt(x2 * x2 + y2 * y2); + var ratio = (z2 - z1) / z1; + + ratios.push(ratio); + }); + }); + + ratios.sort(function (a, b) { + return Math.abs(a) < Math.abs(b); + }); + + return ratios[0]; +} + +/** + * Get a pointer from an event object. + * @param {Object} event - The target event object. + * @param {boolean} endOnly - Indicates if only returns the end point coordinate or not. + * @returns {Object} The result pointer contains start and/or end point coordinates. + */ +function getPointer(_ref2, endOnly) { + var pageX = _ref2.pageX, + pageY = _ref2.pageY; + + var end = { + endX: pageX, + endY: pageY + }; + + if (endOnly) { + return end; + } + + return $.extend({ + startX: pageX, + startY: pageY + }, end); +} + +/** + * Get the center point coordinate of a group of pointers. + * @param {Object} pointers - The target pointers. + * @returns {Object} The center point coordinate. + */ +function getPointersCenter(pointers) { + var pageX = 0; + var pageY = 0; + var count = 0; + + $.each(pointers, function (pointerId, _ref3) { + var startX = _ref3.startX, + startY = _ref3.startY; + + pageX += startX; + pageY += startY; + count += 1; + }); + + pageX /= count; + pageY /= count; + + return { + pageX: pageX, + pageY: pageY + }; +} + +var render = { + render: function render() { + this.initContainer(); + this.initViewer(); + this.initList(); + this.renderViewer(); + }, + initContainer: function initContainer() { + var $window = $(window); + + this.container = { + width: $window.innerWidth(), + height: $window.innerHeight() + }; + }, + initViewer: function initViewer() { + var options = this.options, + $parent = this.$parent; + + var viewer = void 0; + + if (options.inline) { + viewer = { + width: Math.max($parent.width(), options.minWidth), + height: Math.max($parent.height(), options.minHeight) + }; + this.parent = viewer; + } + + if (this.fulled || !viewer) { + viewer = this.container; + } + + this.viewer = $.extend({}, viewer); + }, + renderViewer: function renderViewer() { + if (this.options.inline && !this.fulled) { + this.$viewer.css(this.viewer); + } + }, + initList: function initList() { + var $element = this.$element, + options = this.options, + $list = this.$list; + + var list = []; + + this.$images.each(function (i, image) { + var alt = image.alt || getImageNameFromURL(image); + var src = image.src; + var url = options.url; + + + if (!src) { + return; + } + + if (isString(url)) { + url = image.getAttribute(url); + } else if ($.isFunction(url)) { + url = url.call(image, image); + } + + list.push('
            • ' + '' + '
            • '); + }); + + $list.html(list.join('')).find('img').one(EVENT_LOAD, { + filled: true + }, $.proxy(this.loadImage, this)); + + this.$items = $list.children(); + + if (options.transition) { + $element.one(EVENT_VIEWED, function () { + $list.addClass(CLASS_TRANSITION); + }); + } + }, + renderList: function renderList(index) { + var i = index || this.index; + var width = this.$items.eq(i).width(); + + // 1 pixel of `margin-left` width + var outerWidth = width + 1; + + // Place the active item in the center of the screen + this.$list.css({ + width: outerWidth * this.length, + marginLeft: (this.viewer.width - width) / 2 - outerWidth * i + }); + }, + resetList: function resetList() { + this.$list.empty().removeClass(CLASS_TRANSITION).css('margin-left', 0); + }, + initImage: function initImage(callback) { + var _this = this; + + var options = this.options, + $image = this.$image, + viewer = this.viewer; + + var footerHeight = this.$footer.height(); + var viewerWidth = viewer.width; + var viewerHeight = Math.max(viewer.height - footerHeight, footerHeight); + var oldImage = this.image || {}; + + getImageNaturalSizes($image[0], function (naturalWidth, naturalHeight) { + var aspectRatio = naturalWidth / naturalHeight; + var width = viewerWidth; + var height = viewerHeight; + + if (viewerHeight * aspectRatio > viewerWidth) { + height = viewerWidth / aspectRatio; + } else { + width = viewerHeight * aspectRatio; + } + + width = Math.min(width * 0.9, naturalWidth); + height = Math.min(height * 0.9, naturalHeight); + + var image = { + naturalWidth: naturalWidth, + naturalHeight: naturalHeight, + aspectRatio: aspectRatio, + ratio: width / naturalWidth, + width: width, + height: height, + left: (viewerWidth - width) / 2, + top: (viewerHeight - height) / 2 + }; + var initialImage = $.extend({}, image); + + if (options.rotatable) { + image.rotate = oldImage.rotate || 0; + initialImage.rotate = 0; + } + + if (options.scalable) { + image.scaleX = oldImage.scaleX || 1; + image.scaleY = oldImage.scaleY || 1; + initialImage.scaleX = 1; + initialImage.scaleY = 1; + } + + _this.image = image; + _this.initialImage = initialImage; + + if ($.isFunction(callback)) { + callback(); + } + }); + }, + renderImage: function renderImage(callback) { + var image = this.image, + $image = this.$image; + + + $image.css({ + width: image.width, + height: image.height, + marginLeft: image.left, + marginTop: image.top, + transform: getTransformValues(image) + }); + + if ($.isFunction(callback)) { + if (this.transitioning) { + $image.one(EVENT_TRANSITIONEND, callback); + } else { + callback(); + } + } + }, + resetImage: function resetImage() { + if (this.$image) { + this.$image.remove(); + this.$image = null; + } + } +}; + +var events = { + bind: function bind() { + var $element = this.$element, + options = this.options; + + + if ($.isFunction(options.view)) { + $element.on(EVENT_VIEW, options.view); + } + + if ($.isFunction(options.viewed)) { + $element.on(EVENT_VIEWED, options.viewed); + } + + this.$viewer.on(EVENT_CLICK, $.proxy(this.click, this)).on(EVENT_WHEEL, $.proxy(this.wheel, this)).on(EVENT_DRAG_START, $.proxy(this.dragstart, this)); + + this.$canvas.on(EVENT_POINTER_DOWN, $.proxy(this.pointerdown, this)); + + $(document).on(EVENT_POINTER_MOVE, this.onPointerMove = proxy(this.pointermove, this)).on(EVENT_POINTER_UP, this.onPointerUp = proxy(this.pointerup, this)).on(EVENT_KEY_DOWN, this.onKeyDown = proxy(this.keydown, this)); + + $(window).on(EVENT_RESIZE, this.onResize = proxy(this.resize, this)); + }, + unbind: function unbind() { + var $element = this.$element, + options = this.options; + + + if ($.isFunction(options.view)) { + $element.off(EVENT_VIEW, options.view); + } + + if ($.isFunction(options.viewed)) { + $element.off(EVENT_VIEWED, options.viewed); + } + + this.$viewer.off(EVENT_CLICK, this.click).off(EVENT_WHEEL, this.wheel).off(EVENT_DRAG_START, this.dragstart); + + this.$canvas.off(EVENT_POINTER_DOWN, this.pointerdown); + + $(document).off(EVENT_POINTER_MOVE, this.onPointerMove).off(EVENT_POINTER_UP, this.onPointerUp).off(EVENT_KEY_DOWN, this.onKeyDown); + + $(window).off(EVENT_RESIZE, this.onResize); + } +}; + +var handlers = { + click: function click(e) { + var $target = $(e.target); + var action = $target.data('action'); + var image = this.image; + + + switch (action) { + case 'mix': + if (this.played) { + this.stop(); + } else if (this.options.inline) { + if (this.fulled) { + this.exit(); + } else { + this.full(); + } + } else { + this.hide(); + } + + break; + + case 'view': + this.view($target.data('index')); + break; + + case 'zoom-in': + this.zoom(0.1, true); + break; + + case 'zoom-out': + this.zoom(-0.1, true); + break; + + case 'one-to-one': + this.toggle(); + break; + + case 'reset': + this.reset(); + break; + + case 'prev': + this.prev(); + break; + + case 'play': + this.play(); + break; + + case 'next': + this.next(); + break; + + case 'rotate-left': + this.rotate(-90); + break; + + case 'rotate-right': + this.rotate(90); + break; + + case 'flip-horizontal': + this.scaleX(-image.scaleX || -1); + break; + + case 'flip-vertical': + this.scaleY(-image.scaleY || -1); + break; + + default: + if (this.played) { + this.stop(); + } + } + }, + dragstart: function dragstart(e) { + if ($(e.target).is('img')) { + e.preventDefault(); + } + }, + keydown: function keydown(e) { + var options = this.options; + + + if (!this.fulled || !options.keyboard) { + return; + } + + switch (e.which) { + // (Key: Esc) + case 27: + if (this.played) { + this.stop(); + } else if (options.inline) { + if (this.fulled) { + this.exit(); + } + } else { + this.hide(); + } + + break; + + // (Key: Space) + case 32: + if (this.played) { + this.stop(); + } + + break; + + // View previous (Key: ←) + case 37: + this.prev(); + break; + + // Zoom in (Key: ↑) + case 38: + // Prevent scroll on Firefox + e.preventDefault(); + + this.zoom(options.zoomRatio, true); + break; + + // View next (Key: →) + case 39: + this.next(); + break; + + // Zoom out (Key: ↓) + case 40: + // Prevent scroll on Firefox + e.preventDefault(); + + this.zoom(-options.zoomRatio, true); + break; + + // 48: Zoom out to initial size (Key: Ctrl + 0) + // 49: Zoom in to natural size (Key: Ctrl + 1) + case 48: + case 49: + if (e.ctrlKey || e.shiftKey) { + e.preventDefault(); + this.toggle(); + } + + break; + + default: + } + }, + load: function load() { + var _this = this; + + var options = this.options, + viewer = this.viewer, + $image = this.$image; + + + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = false; + } + + if (!$image) { + return; + } + + $image.removeClass(CLASS_INVISIBLE).css('cssText', '' + ('width:0;' + 'height:0;' + 'margin-left:') + viewer.width / 2 + 'px;' + ('margin-top:' + viewer.height / 2 + 'px;') + 'max-width:none!important;' + 'visibility:visible;'); + + this.initImage(function () { + $image.toggleClass(CLASS_TRANSITION, options.transition).toggleClass(CLASS_MOVE, options.movable); + + _this.renderImage(function () { + _this.viewed = true; + _this.trigger(EVENT_VIEWED); + }); + }); + }, + loadImage: function loadImage(e) { + var image = e.target; + var $image = $(image); + var $parent = $image.parent(); + var parentWidth = $parent.width(); + var parentHeight = $parent.height(); + var filled = e.data && e.data.filled; + + getImageNaturalSizes(image, function (naturalWidth, naturalHeight) { + var aspectRatio = naturalWidth / naturalHeight; + var width = parentWidth; + var height = parentHeight; + + if (parentHeight * aspectRatio > parentWidth) { + if (filled) { + width = parentHeight * aspectRatio; + } else { + height = parentWidth / aspectRatio; + } + } else if (filled) { + height = parentWidth / aspectRatio; + } else { + width = parentHeight * aspectRatio; + } + + $image.css({ + width: width, + height: height, + marginLeft: (parentWidth - width) / 2, + marginTop: (parentHeight - height) / 2 + }); + }); + }, + pointerdown: function pointerdown(e) { + if (!this.viewed || this.transitioning) { + return; + } + + var options = this.options, + pointers = this.pointers; + var originalEvent = e.originalEvent; + + + if (originalEvent && originalEvent.changedTouches) { + $.each(originalEvent.changedTouches, function (i, touch) { + pointers[touch.identifier] = getPointer(touch); + }); + } else { + pointers[originalEvent && originalEvent.pointerId || 0] = getPointer(originalEvent || e); + } + + var action = options.movable ? ACTION_MOVE : false; + + if (objectKeys(pointers).length > 1) { + action = ACTION_ZOOM; + } else if ((e.pointerType === 'touch' || e.type === 'touchmove') && this.isSwitchable()) { + action = ACTION_SWITCH; + } + + this.action = action; + }, + pointermove: function pointermove(e) { + var $image = this.$image, + action = this.action, + pointers = this.pointers; + + + if (!this.viewed || !action) { + return; + } + + e.preventDefault(); + + var originalEvent = e.originalEvent; + + + if (originalEvent && originalEvent.changedTouches) { + $.each(originalEvent.changedTouches, function (i, touch) { + $.extend(pointers[touch.identifier], getPointer(touch, true)); + }); + } else { + $.extend(pointers[originalEvent && originalEvent.pointerId || 0], getPointer(e, true)); + } + + if (action === ACTION_MOVE && this.options.transition && $image.hasClass(CLASS_TRANSITION)) { + $image.removeClass(CLASS_TRANSITION); + } + + this.change(e); + }, + pointerup: function pointerup(e) { + if (!this.viewed) { + return; + } + + var action = this.action, + pointers = this.pointers; + var originalEvent = e.originalEvent; + + + if (originalEvent && originalEvent.changedTouches) { + $.each(originalEvent.changedTouches, function (i, touch) { + delete pointers[touch.identifier]; + }); + } else { + delete pointers[originalEvent && originalEvent.pointerId || 0]; + } + + if (!action) { + return; + } + + if (action === ACTION_MOVE && this.options.transition) { + this.$image.addClass(CLASS_TRANSITION); + } + + this.action = false; + }, + resize: function resize() { + var _this2 = this; + + this.initContainer(); + this.initViewer(); + this.renderViewer(); + this.renderList(); + + if (this.viewed) { + this.initImage(function () { + _this2.renderImage(); + }); + } + + if (this.played) { + if (this.options.fullscreen && this.fulled && !document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) { + this.stop(); + return; + } + + this.$player.find('img').one(EVENT_LOAD, $.proxy(this.loadImage, this)).trigger(EVENT_LOAD); + } + }, + start: function start(_ref) { + var target = _ref.target; + + if ($(target).is('img')) { + this.target = target; + this.show(); + } + }, + wheel: function wheel(e) { + var _this3 = this; + + if (!this.viewed) { + return; + } + + e.preventDefault(); + + // Limit wheel speed to prevent zoom too fast + if (this.wheeling) { + return; + } + + this.wheeling = true; + + setTimeout(function () { + _this3.wheeling = false; + }, 50); + + var originalEvent = e.originalEvent || e; + var delta = 1; + + if (originalEvent.deltaY) { + delta = originalEvent.deltaY > 0 ? 1 : -1; + } else if (originalEvent.wheelDelta) { + delta = -originalEvent.wheelDelta / 120; + } else if (originalEvent.detail) { + delta = originalEvent.detail > 0 ? 1 : -1; + } + + this.zoom(-delta * (Number(this.options.zoomRatio) || 0.1), true, e); + } +}; + +var methods = { + /** + * Show the viewer (only available in modal mode). + */ + show: function show() { + var _this = this; + + var $element = this.$element, + options = this.options; + + + if (options.inline || this.transitioning) { + return; + } + + if (!this.ready) { + this.build(); + } + + var $viewer = this.$viewer; + + + if ($.isFunction(options.show)) { + $element.one(EVENT_SHOW, options.show); + } + + if (this.trigger(EVENT_SHOW).isDefaultPrevented()) { + return; + } + + this.$body.addClass(CLASS_OPEN); + $viewer.removeClass(CLASS_HIDE); + $element.one(EVENT_SHOWN, function () { + _this.view(_this.target ? _this.$images.index(_this.target) : _this.index); + _this.target = false; + }); + + if (options.transition) { + this.transitioning = true; + $viewer.addClass(CLASS_TRANSITION); + + // Force reflow to enable CSS3 transition + // eslint-disable-next-line + $viewer[0].offsetWidth; + $viewer.one(EVENT_TRANSITIONEND, $.proxy(this.shown, this)).addClass(CLASS_IN); + } else { + $viewer.addClass(CLASS_IN); + this.shown(); + } + }, + + + /** + * Hide the viewer (only available in modal mode). + */ + hide: function hide() { + var _this2 = this; + + var options = this.options, + $viewer = this.$viewer; + + + if (options.inline || this.transitioning || !this.visible) { + return; + } + + if ($.isFunction(options.hide)) { + this.$element.one(EVENT_HIDE, options.hide); + } + + if (this.trigger(EVENT_HIDE).isDefaultPrevented()) { + return; + } + + if (this.viewed && options.transition) { + this.transitioning = true; + this.$image.one(EVENT_TRANSITIONEND, function () { + $viewer.one(EVENT_TRANSITIONEND, $.proxy(_this2.hidden, _this2)).removeClass(CLASS_IN); + }); + this.zoomTo(0, false, false, true); + } else { + $viewer.removeClass(CLASS_IN); + this.hidden(); + } + }, + + + /** + * View one of the images with image's index. + * @param {number} index - The image index. + */ + view: function view(index) { + var _this3 = this; + + index = Number(index) || 0; + + if (!this.visible || this.played || index < 0 || index >= this.length || this.viewed && index === this.index) { + return; + } + + if (this.trigger(EVENT_VIEW).isDefaultPrevented()) { + return; + } + + var $item = this.$items.eq(index); + var $img = $item.find('img'); + var alt = $img.attr('alt'); + var $image = $('' + alt + ''); + + this.$image = $image; + this.$items.eq(this.index).removeClass(CLASS_ACTIVE); + $item.addClass(CLASS_ACTIVE); + this.viewed = false; + this.index = index; + this.image = null; + this.$canvas.html($image.addClass(CLASS_INVISIBLE)); + + // Center current item + this.renderList(); + + var $title = this.$title; + + // Clear title + + $title.empty(); + + // Generate title after viewed + this.$element.one(EVENT_VIEWED, function () { + var _image = _this3.image, + naturalWidth = _image.naturalWidth, + naturalHeight = _image.naturalHeight; + + + $title.html(alt + ' (' + naturalWidth + ' × ' + naturalHeight + ')'); + }); + + if ($image[0].complete) { + this.load(); + } else { + $image.one(EVENT_LOAD, $.proxy(this.load, this)); + + if (this.timeout) { + clearTimeout(this.timeout); + } + + // Make the image visible if it fails to load within 1s + this.timeout = setTimeout(function () { + $image.removeClass(CLASS_INVISIBLE); + _this3.timeout = false; + }, 1000); + } + }, + + + /** + * View the previous image. + */ + prev: function prev() { + this.view(Math.max(this.index - 1, 0)); + }, + + + /** + * View the next image. + */ + next: function next() { + this.view(Math.min(this.index + 1, this.length - 1)); + }, + + + /** + * Move the image with relative offsets. + * @param {number} offsetX - The relative offset distance on the x-axis. + * @param {number} offsetY - The relative offset distance on the y-axis. + */ + move: function move(offsetX, offsetY) { + var _image2 = this.image, + left = _image2.left, + top = _image2.top; + + + this.moveTo(isUndefined(offsetX) ? offsetX : left + Number(offsetX), isUndefined(offsetY) ? offsetY : top + Number(offsetY)); + }, + + + /** + * Move the image to an absolute point. + * @param {number} x - The x-axis coordinate. + * @param {number} [y=x] - The y-axis coordinate. + */ + moveTo: function moveTo(x) { + var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : x; + + if (!this.viewed || this.played || !this.options.movable) { + return; + } + + var image = this.image; + + var changed = false; + + x = Number(x); + y = Number(y); + + if (isNumber(x)) { + image.left = x; + changed = true; + } + + if (isNumber(y)) { + image.top = y; + changed = true; + } + + if (changed) { + this.renderImage(); + } + }, + + + /** + * Zoom the image with a relative ratio. + * @param {number} ratio - The target ratio. + * @param {boolean} [hasTooltip] - Indicates if it has a tooltip or not. + * @param {Event} [_event] - The related event if any. + */ + zoom: function zoom(ratio) { + var hasTooltip = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + + var _event = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; + + var image = this.image; + + + ratio = Number(ratio); + + if (ratio < 0) { + ratio = 1 / (1 - ratio); + } else { + ratio = 1 + ratio; + } + + this.zoomTo(image.width * ratio / image.naturalWidth, hasTooltip, _event); + }, + + + /** + * Zoom the image to an absolute ratio. + * @param {number} ratio - The target ratio. + * @param {boolean} [hasTooltip] - Indicates if it has a tooltip or not. + * @param {Event} [_event] - The related event if any. + * @param {Event} [_zoomable] - Indicates if the current zoom is available or not. + */ + zoomTo: function zoomTo(ratio) { + var hasTooltip = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + + var _event = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; + + var _zoomable = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; + + var options = this.options, + image = this.image, + pointers = this.pointers; + + + ratio = Math.max(0, ratio); + + if (isNumber(ratio) && this.viewed && !this.played && (_zoomable || options.zoomable)) { + if (!_zoomable) { + var minZoomRatio = Math.max(0.01, options.minZoomRatio); + var maxZoomRatio = Math.min(100, options.maxZoomRatio); + + ratio = Math.min(Math.max(ratio, minZoomRatio), maxZoomRatio); + } + + if (_event && ratio > 0.95 && ratio < 1.05) { + ratio = 1; + } + + var newWidth = image.naturalWidth * ratio; + var newHeight = image.naturalHeight * ratio; + + if (_event && _event.originalEvent) { + var offset = this.$viewer.offset(); + var center = pointers && objectKeys(pointers).length > 0 ? getPointersCenter(pointers) : { + pageX: _event.pageX || _event.originalEvent.pageX || 0, + pageY: _event.pageY || _event.originalEvent.pageY || 0 + }; + + // Zoom from the triggering point of the event + image.left -= (newWidth - image.width) * ((center.pageX - offset.left - image.left) / image.width); + image.top -= (newHeight - image.height) * ((center.pageY - offset.top - image.top) / image.height); + } else { + // Zoom from the center of the image + image.left -= (newWidth - image.width) / 2; + image.top -= (newHeight - image.height) / 2; + } + + image.width = newWidth; + image.height = newHeight; + image.ratio = ratio; + this.renderImage(); + + if (hasTooltip) { + this.tooltip(); + } + } + }, + + + /** + * Rotate the image with a relative degree. + * @param {number} degree - The rotate degree. + */ + rotate: function rotate(degree) { + this.rotateTo((this.image.rotate || 0) + Number(degree)); + }, + + + /** + * Rotate the image to an absolute degree. + * @param {number} degree - The rotate degree. + */ + rotateTo: function rotateTo(degree) { + var image = this.image; + + + degree = Number(degree); + + if (isNumber(degree) && this.viewed && !this.played && this.options.rotatable) { + image.rotate = degree; + this.renderImage(); + } + }, + + + /** + * Scale the image on the x-axis. + * @param {number} scaleX - The scale ratio on the x-axis. + */ + scaleX: function scaleX(_scaleX) { + this.scale(_scaleX, this.image.scaleY); + }, + + + /** + * Scale the image on the y-axis. + * @param {number} scaleY - The scale ratio on the y-axis. + */ + scaleY: function scaleY(_scaleY) { + this.scale(this.image.scaleX, _scaleY); + }, + + + /** + * Scale the image. + * @param {number} scaleX - The scale ratio on the x-axis. + * @param {number} [scaleY=scaleX] - The scale ratio on the y-axis. + */ + scale: function scale(scaleX) { + var scaleY = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : scaleX; + + if (!this.viewed || this.played || !this.options.scalable) { + return; + } + + var image = this.image; + + var changed = false; + + scaleX = Number(scaleX); + scaleY = Number(scaleY); + + if (isNumber(scaleX)) { + image.scaleX = scaleX; + changed = true; + } + + if (isNumber(scaleY)) { + image.scaleY = scaleY; + changed = true; + } + + if (changed) { + this.renderImage(); + } + }, + + + /** + * Play the images. + */ + play: function play() { + var _this4 = this; + + if (!this.visible || this.played) { + return; + } + + var options = this.options, + $items = this.$items, + $player = this.$player; + + + if (options.fullscreen) { + this.requestFullscreen(); + } + + this.played = true; + $player.addClass(CLASS_SHOW); + + var list = []; + var index = 0; + + $items.each(function (i, item) { + var $item = $(item); + var $img = $item.find('img'); + var $image = $('' + $img.attr('alt') + ''); + + $image.addClass(CLASS_FADE).toggleClass(CLASS_TRANSITION, options.transition); + + if ($item.hasClass(CLASS_ACTIVE)) { + $image.addClass(CLASS_IN); + index = i; + } + + list.push($image); + $image.one(EVENT_LOAD, { + filled: false + }, $.proxy(_this4.loadImage, _this4)); + $player.append($image); + }); + + if (isNumber(options.interval) && options.interval > 0) { + var length = $items.length; + + var playing = function playing() { + _this4.playing = setTimeout(function () { + list[index].removeClass(CLASS_IN); + index += 1; + index = index < length ? index : 0; + list[index].addClass(CLASS_IN); + playing(); + }, options.interval); + }; + + if (length > 1) { + playing(); + } + } + }, + + + /** + * Stop play. + */ + stop: function stop() { + if (!this.played) { + return; + } + + if (this.options.fullscreen) { + this.exitFullscreen(); + } + + this.played = false; + clearTimeout(this.playing); + this.$player.removeClass(CLASS_SHOW).empty(); + }, + + + /** + * Enter modal mode (only available in inline mode). + */ + full: function full() { + var _this5 = this; + + var options = this.options, + $image = this.$image, + $list = this.$list; + + + if (!this.visible || this.played || this.fulled || !options.inline) { + return; + } + + this.fulled = true; + this.$body.addClass(CLASS_OPEN); + this.$button.addClass(CLASS_FULLSCREEN_EXIT); + + if (options.transition) { + $image.removeClass(CLASS_TRANSITION); + $list.removeClass(CLASS_TRANSITION); + } + + this.$viewer.addClass(CLASS_FIXED).removeAttr('style').css('z-index', options.zIndex); + this.initContainer(); + this.viewer = $.extend({}, this.container); + this.renderList(); + this.initImage(function () { + _this5.renderImage(function () { + if (options.transition) { + setTimeout(function () { + $image.addClass(CLASS_TRANSITION); + $list.addClass(CLASS_TRANSITION); + }, 0); + } + }); + }); + }, + + + /** + * Exit modal mode (only available in inline mode). + */ + exit: function exit() { + var _this6 = this; + + if (!this.fulled) { + return; + } + + var options = this.options, + $image = this.$image, + $list = this.$list; + + + this.fulled = false; + this.$body.removeClass(CLASS_OPEN); + this.$button.removeClass(CLASS_FULLSCREEN_EXIT); + + if (options.transition) { + $image.removeClass(CLASS_TRANSITION); + $list.removeClass(CLASS_TRANSITION); + } + + this.$viewer.removeClass(CLASS_FIXED).css('z-index', options.zIndexInline); + this.viewer = $.extend({}, this.parent); + this.renderViewer(); + this.renderList(); + this.initImage(function () { + _this6.renderImage(function () { + if (options.transition) { + setTimeout(function () { + $image.addClass(CLASS_TRANSITION); + $list.addClass(CLASS_TRANSITION); + }, 0); + } + }); + }); + }, + + + /** + * Show the current ratio of the image with percentage. + */ + tooltip: function tooltip() { + var _this7 = this; + + var options = this.options, + $tooltip = this.$tooltip, + image = this.image; + + var classes = [CLASS_SHOW, CLASS_FADE, CLASS_TRANSITION].join(' '); + + if (!this.viewed || this.played || !options.tooltip) { + return; + } + + $tooltip.text(Math.round(image.ratio * 100) + '%'); + + if (!this.tooltiping) { + if (options.transition) { + if (this.fading) { + $tooltip.trigger(EVENT_TRANSITIONEND); + } + + $tooltip.addClass(classes); + + // Force reflow to enable CSS3 transition + // eslint-disable-next-line + $tooltip[0].offsetWidth; + $tooltip.addClass(CLASS_IN); + } else { + $tooltip.addClass(CLASS_SHOW); + } + } else { + clearTimeout(this.tooltiping); + } + + this.tooltiping = setTimeout(function () { + if (options.transition) { + $tooltip.one(EVENT_TRANSITIONEND, function () { + $tooltip.removeClass(classes); + _this7.fading = false; + }).removeClass(CLASS_IN); + + _this7.fading = true; + } else { + $tooltip.removeClass(CLASS_SHOW); + } + + _this7.tooltiping = false; + }, 1000); + }, + + + /** + * Toggle the image size between its natural size and initial size. + */ + toggle: function toggle() { + if (this.image.ratio === 1) { + this.zoomTo(this.initialImage.ratio, true); + } else { + this.zoomTo(1, true); + } + }, + + + /** + * Reset the image to its initial state. + */ + reset: function reset() { + if (this.viewed && !this.played) { + this.image = $.extend({}, this.initialImage); + this.renderImage(); + } + }, + + + /** + * Update viewer when images changed. + */ + update: function update() { + var $element = this.$element; + var $images = this.$images; + + + if (this.isImg) { + // Destroy viewer if the target image was deleted + if (!$element.parent().length) { + this.destroy(); + return; + } + } else { + $images = $element.find('img'); + this.$images = $images; + this.length = $images.length; + } + + if (this.ready) { + var indexes = []; + var index = void 0; + + $.each(this.$items, function (i, item) { + var img = $(item).find('img')[0]; + var image = $images[i]; + + if (image) { + if (image.src !== img.src) { + indexes.push(i); + } + } else { + indexes.push(i); + } + }); + + this.$list.width('auto'); + this.initList(); + + if (this.visible) { + if (this.length) { + if (this.viewed) { + index = $.inArray(this.index, indexes); + + if (index >= 0) { + this.viewed = false; + this.view(Math.max(this.index - (index + 1), 0)); + } else { + this.$items.eq(this.index).addClass(CLASS_ACTIVE); + } + } + } else { + this.$image = null; + this.viewed = false; + this.index = 0; + this.image = null; + this.$canvas.empty(); + this.$title.empty(); + } + } + } + }, + + + /** + * Destroy the viewer instance. + */ + destroy: function destroy() { + var $element = this.$element; + + + if (this.options.inline) { + this.unbind(); + } else { + if (this.visible) { + this.unbind(); + } + + $element.off(EVENT_CLICK, this.start); + } + + this.unbuild(); + $element.removeData(NAMESPACE); + } +}; + +var _window$1 = window; +var document$1 = _window$1.document; + + +var others = { + // A shortcut for triggering custom events + trigger: function trigger(type, data) { + var e = $.Event(type, data); + + this.$element.trigger(e); + + return e; + }, + shown: function shown() { + var options = this.options; + + + this.transitioning = false; + this.fulled = true; + this.visible = true; + this.render(); + this.bind(); + + if ($.isFunction(options.shown)) { + this.$element.one(EVENT_SHOWN, options.shown); + } + + this.trigger(EVENT_SHOWN); + }, + hidden: function hidden() { + var options = this.options; + + + this.transitioning = false; + this.viewed = false; + this.fulled = false; + this.visible = false; + this.unbind(); + this.$body.removeClass(CLASS_OPEN); + this.$viewer.addClass(CLASS_HIDE); + this.resetList(); + this.resetImage(); + + if ($.isFunction(options.hidden)) { + this.$element.one(EVENT_HIDDEN, options.hidden); + } + + this.trigger(EVENT_HIDDEN); + }, + requestFullscreen: function requestFullscreen() { + if (this.fulled && !document$1.fullscreenElement && !document$1.mozFullScreenElement && !document$1.webkitFullscreenElement && !document$1.msFullscreenElement) { + var documentElement = document$1.documentElement; + + + if (documentElement.requestFullscreen) { + documentElement.requestFullscreen(); + } else if (documentElement.msRequestFullscreen) { + documentElement.msRequestFullscreen(); + } else if (documentElement.mozRequestFullScreen) { + documentElement.mozRequestFullScreen(); + } else if (documentElement.webkitRequestFullscreen) { + documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + } + } + }, + exitFullscreen: function exitFullscreen() { + if (this.fulled) { + if (document$1.exitFullscreen) { + document$1.exitFullscreen(); + } else if (document$1.msExitFullscreen) { + document$1.msExitFullscreen(); + } else if (document$1.mozCancelFullScreen) { + document$1.mozCancelFullScreen(); + } else if (document$1.webkitExitFullscreen) { + document$1.webkitExitFullscreen(); + } + } + }, + change: function change(event) { + var pointers = this.pointers; + + var pointer = pointers[Object.keys(pointers)[0]]; + var offsetX = pointer.endX - pointer.startX; + var offsetY = pointer.endY - pointer.startY; + + switch (this.action) { + // Move the current image + case ACTION_MOVE: + this.move(offsetX, offsetY); + break; + + // Zoom the current image + case ACTION_ZOOM: + this.zoom(getMaxZoomRatio(pointers), false, event); + + this.startX2 = this.endX2; + this.startY2 = this.endY2; + break; + + case ACTION_SWITCH: + this.action = 'switched'; + + if (Math.abs(offsetX) > Math.abs(offsetY)) { + if (offsetX > 1) { + this.prev(); + } else if (offsetX < -1) { + this.next(); + } + } + + break; + + default: + } + + // Override + $.each(pointers, function (i, p) { + p.startX = p.endX; + p.startY = p.endY; + }); + }, + isSwitchable: function isSwitchable() { + var image = this.image, + viewer = this.viewer; + + + return image.left >= 0 && image.top >= 0 && image.width <= viewer.width && image.height <= viewer.height; + } +}; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Viewer = function () { + /** + * Start the new Viewer. + * @param {Element} element - The target element for viewing. + * @param {Object} [options={}] - The configuration options. + */ + function Viewer(element) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + _classCallCheck(this, Viewer); + + if (!element || element.nodeType !== 1) { + throw new Error('The first argument is required and must be an element.'); + } + + this.element = element; + this.$element = $(element); + this.options = $.extend({}, DEFAULTS, $.isPlainObject(options) && options); + this.action = ''; + this.target = null; + this.timeout = null; + this.index = 0; + this.length = 0; + this.ready = false; + this.fading = false; + this.fulled = false; + this.isImg = false; + this.played = false; + this.playing = false; + this.tooltiping = false; + this.transitioning = false; + this.viewed = false; + this.visible = false; + this.wheeling = false; + this.pointers = {}; + this.init(); + } + + _createClass(Viewer, [{ + key: 'init', + value: function init() { + var _this = this; + + var $element = this.$element, + options = this.options; + + var isImg = $element.is('img'); + var $images = isImg ? $element : $element.find('img'); + var length = $images.length; + + + if (!length) { + return; + } + + // Override `transition` option if it is not supported + if (typeof document.createElement(NAMESPACE).style.transition === 'undefined') { + options.transition = false; + } + + this.isImg = isImg; + this.length = length; + this.count = 0; + this.$images = $images; + this.$body = $('body'); + + if (options.inline) { + $element.one(EVENT_READY, function () { + _this.view(); + }); + + $images.each(function (i, image) { + if (image.complete) { + _this.progress(); + } else { + $(image).one(EVENT_LOAD, $.proxy(_this.progress, _this)); + } + }); + } else { + $element.on(EVENT_CLICK, $.proxy(this.start, this)); + } + } + }, { + key: 'progress', + value: function progress() { + this.count += 1; + + if (this.count === this.length) { + this.build(); + } + } + }, { + key: 'build', + value: function build() { + var $element = this.$element, + options = this.options; + + + if (this.ready) { + return; + } + + var $parent = $element.parent(); + var $viewer = $(TEMPLATE); + var $button = $viewer.find('.' + NAMESPACE + '-button'); + var $navbar = $viewer.find('.' + NAMESPACE + '-navbar'); + var $title = $viewer.find('.' + NAMESPACE + '-title'); + var $toolbar = $viewer.find('.' + NAMESPACE + '-toolbar'); + + this.$parent = $parent; + this.$viewer = $viewer; + this.$button = $button; + this.$navbar = $navbar; + this.$title = $title; + this.$toolbar = $toolbar; + this.$canvas = $viewer.find('.' + NAMESPACE + '-canvas'); + this.$footer = $viewer.find('.' + NAMESPACE + '-footer'); + this.$list = $viewer.find('.' + NAMESPACE + '-list'); + this.$player = $viewer.find('.' + NAMESPACE + '-player'); + this.$tooltip = $viewer.find('.' + NAMESPACE + '-tooltip'); + + $title.addClass(!options.title ? CLASS_HIDE : getResponsiveClass(options.title)); + $toolbar.addClass(!options.toolbar ? CLASS_HIDE : getResponsiveClass(options.toolbar)); + $toolbar.find('li[class*=zoom]').toggleClass(CLASS_INVISIBLE, !options.zoomable); + $toolbar.find('li[class*=flip]').toggleClass(CLASS_INVISIBLE, !options.scalable); + + if (!options.rotatable) { + $toolbar.find('li[class*=rotate]').addClass(CLASS_INVISIBLE).appendTo($toolbar); + } + + $navbar.addClass(!options.navbar ? CLASS_HIDE : getResponsiveClass(options.navbar)); + $button.toggleClass(CLASS_HIDE, !options.button); + + if (options.inline) { + $button.addClass(CLASS_FULLSCREEN); + $viewer.css('z-index', options.zIndexInline); + + if ($parent.css('position') === 'static') { + $parent.css('position', 'relative'); + } + + $element.after($viewer); + } else { + $button.addClass(CLASS_CLOSE); + $viewer.css('z-index', options.zIndex).addClass([CLASS_FIXED, CLASS_FADE, CLASS_HIDE].join(' ')).appendTo('body'); + } + + if (options.inline) { + this.render(); + this.bind(); + this.visible = true; + } + + this.ready = true; + + if ($.isFunction(options.ready)) { + $element.one(EVENT_READY, options.ready); + } + + this.trigger(EVENT_READY); + } + }, { + key: 'unbuild', + value: function unbuild() { + if (!this.ready) { + return; + } + + this.ready = false; + this.$viewer.remove(); + } + + /** + * Change the default options. + * @static + * @param {Object} options - The new default options. + */ + + }], [{ + key: 'setDefaults', + value: function setDefaults(options) { + $.extend(DEFAULTS, options); + } + }]); + + return Viewer; +}(); + +$.extend(Viewer.prototype, render, events, handlers, methods, others); + +var AnotherViewer = $.fn.viewer; + +$.fn.viewer = function jQueryViewer(option) { + for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + var result = void 0; + + this.each(function (i, element) { + var $element = $(element); + var data = $element.data(NAMESPACE); + + if (!data) { + if (/destroy/.test(option)) { + return; + } + + var options = $.extend({}, $element.data(), $.isPlainObject(option) && option); + + data = new Viewer(element, options); + $element.data(NAMESPACE, data); + } + + if (isString(option)) { + var fn = data[option]; + + if ($.isFunction(fn)) { + result = fn.apply(data, args); + } + } + }); + + return isUndefined(result) ? this : result; +}; + +$.fn.viewer.Constructor = Viewer; +$.fn.viewer.setDefaults = Viewer.setDefaults; +$.fn.viewer.noConflict = function noConflict() { + $.fn.viewer = AnotherViewer; + return this; +}; diff --git a/public/static/libs/viewer/viewer.css b/public/static/libs/viewer/viewer.css new file mode 100644 index 0000000..a806bc7 --- /dev/null +++ b/public/static/libs/viewer/viewer.css @@ -0,0 +1,369 @@ +/*! + * Viewer v0.6.0 + * https://github.com/fengyuanchen/viewer + * + * Copyright (c) 2014-2017 Fengyuan Chen + * Released under the MIT license + * + * Date: 2017-10-07T09:53:32.834Z + */ + +.viewer-zoom-in::before, +.viewer-zoom-out::before, +.viewer-one-to-one::before, +.viewer-reset::before, +.viewer-prev::before, +.viewer-play::before, +.viewer-next::before, +.viewer-rotate-left::before, +.viewer-rotate-right::before, +.viewer-flip-horizontal::before, +.viewer-flip-vertical::before, +.viewer-fullscreen::before, +.viewer-fullscreen-exit::before, +.viewer-close::before { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARgAAAAUCAYAAABWOyJDAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNui8sowAAAQPSURBVHic7Zs/iFxVFMa/0U2UaJGksUgnIVhYxVhpjDbZCBmLdAYECxsRFBTUamcXUiSNncgKQbSxsxH8gzAP3FU2jY0kKKJNiiiIghFlccnP4p3nPCdv3p9778vsLOcHB2bfveeb7955c3jvvNkBIMdxnD64a94GHMfZu3iBcRynN7zAOI7TG15gHCeeNUkr8zaxG2lbYDYsdgMbktBsP03jdQwljSXdtBhLOmtjowC9Mg9L+knSlcD8TNKpSA9lBpK2JF2VdDSR5n5J64m0qli399hNFMUlpshQii5jbXTbHGviB0nLNeNDSd9VO4A2UdB2fp+x0eCnaXxWXGA2X0au/3HgN9P4LFCjIANOJdrLr0zzZ+BEpNYDwKbpnQMeAw4m8HjQtM6Z9qa917zPQwFr3M5KgA6J5rTJCdFZJj9/lyvGhsDvwFNVuV2MhhjrK6b9bFiE+j1r87eBl4HDwCF7/U/k+ofAX5b/EXBv5JoLMuILzf3Ap6Z3EzgdqHMCuF7hcQf4HDgeoHnccncqdK/TvSDWffFXI/exICY/xZyqc6XLWF1UFZna4gJ7q8BsRvgd2/xXpo6P+D9dfT7PpECtA3cnWPM0GXGFZh/wgWltA+cDNC7X+AP4GzjZQe+k5dRxuYPeiuXU7e1qwLpDz7dFjXKRaSwuMLvAlG8zZlG+YmiK1HoFqT7wP2z+4Q45TfEGcMt01xLoNZEBTwRqD4BLpnMLeC1A41UmVxsXgXeBayV/Wx20rpTyrpnWRft7p6O/FdqzGrDukPNtkaMoMo3FBdBSQMOnYBCReyf05s126fU9ytfX98+mY54Kxnp7S9K3kj6U9KYdG0h6UdLbkh7poFXMfUnSOyVvL0h6VtIXHbS6nOP+s/Zm9mvyXW1uuC9ohZ72E9uDmXWLJOB1GxsH+DxPftsB8B6wlGDN02TAkxG6+4D3TWsbeC5CS8CDFce+AW500LhhOW2020TRjK3b21HEmgti9m0RonxbdMZeVzV+/4tF3cBpP7E9mKHNL5q8h5g0eYsCMQz0epq8gQrwMXAgcs0FGXGFRcB9wCemF9PkbYqM/Bas7fxLwNeJPdTdpo4itQti8lPMqTpXuozVRVXPpbHI3KkNTB1NfkL81j2mvhDp91HgV9MKuRIqrykj3WPq4rHyL+axj8/qGPmTqi6F9YDlHOvJU6oYcTsh/TYSzWmTE6JT19CtLTJt32D6CmHe0eQn1O8z5AXgT4sx4Vcu0/EQecMydB8z0hUWkTd2t4CrwNEePqMBcAR4mrBbwyXLPWJa8zrXmmLEhNBmfpkuY2102xxrih+pb+ieAb6vGhuA97UcJ5KR8gZ77K+99xxeYBzH6Q3/Z0fHcXrDC4zjOL3hBcZxnN74F+zlvXFWXF9PAAAAAElFTkSuQmCC'); + background-repeat: no-repeat; + color: transparent; + display: block; + font-size: 0; + height: 20px; + line-height: 0; + width: 20px; +} + +.viewer-zoom-in::before { + background-position: 0 0; + content: 'Zoom In'; +} + +.viewer-zoom-out::before { + background-position: -20px 0; + content: 'Zoom Out'; +} + +.viewer-one-to-one::before { + background-position: -40px 0; + content: 'One to One'; +} + +.viewer-reset::before { + background-position: -60px 0; + content: 'Reset'; +} + +.viewer-prev::before { + background-position: -80px 0; + content: 'Previous'; +} + +.viewer-play::before { + background-position: -100px 0; + content: 'Play'; +} + +.viewer-next::before { + background-position: -120px 0; + content: 'Next'; +} + +.viewer-rotate-left::before { + background-position: -140px 0; + content: 'Rotate Left'; +} + +.viewer-rotate-right::before { + background-position: -160px 0; + content: 'Rotate Right'; +} + +.viewer-flip-horizontal::before { + background-position: -180px 0; + content: 'Flip Horizontal'; +} + +.viewer-flip-vertical::before { + background-position: -200px 0; + content: 'Flip Vertical'; +} + +.viewer-fullscreen::before { + background-position: -220px 0; + content: 'Enter Full Screen'; +} + +.viewer-fullscreen-exit::before { + background-position: -240px 0; + content: 'Exit Full Screen'; +} + +.viewer-close::before { + background-position: -260px 0; + content: 'Close'; +} + +.viewer-container { + background-color: rgba(0, 0, 0, .5); + bottom: 0; + direction: ltr; + font-size: 0; + left: 0; + line-height: 0; + overflow: hidden; + position: absolute; + right: 0; + -webkit-tap-highlight-color: transparent; + top: 0; + -webkit-touch-callout: none; + -ms-touch-action: none; + touch-action: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.viewer-container::-moz-selection, +.viewer-container *::-moz-selection { + background-color: transparent; +} + +.viewer-container::selection, +.viewer-container *::selection { + background-color: transparent; +} + +.viewer-container img { + display: block; + height: auto; + max-height: none !important; + max-width: none !important; + min-height: 0 !important; + min-width: 0 !important; + width: 100%; +} + +.viewer-canvas { + bottom: 0; + left: 0; + overflow: hidden; + position: absolute; + right: 0; + top: 0; +} + +.viewer-canvas > img { + height: auto; + margin: 15px auto; + max-width: 90% !important; + width: auto; +} + +.viewer-footer { + bottom: 0; + left: 0; + overflow: hidden; + position: absolute; + right: 0; + text-align: center; +} + +.viewer-navbar { + background-color: rgba(0, 0, 0, .5); + overflow: hidden; +} + +.viewer-list { + -webkit-box-sizing: content-box; + box-sizing: content-box; + height: 50px; + margin: 0; + overflow: hidden; + padding: 1px 0; +} + +.viewer-list > li { + color: transparent; + cursor: pointer; + float: left; + font-size: 0; + height: 50px; + line-height: 0; + opacity: .5; + overflow: hidden; + width: 30px; +} + +.viewer-list > li + li { + margin-left: 1px; +} + +.viewer-list > .viewer-active { + opacity: 1; +} + +.viewer-player { + background-color: #000; + bottom: 0; + cursor: none; + display: none; + left: 0; + position: absolute; + right: 0; + top: 0; +} + +.viewer-player > img { + left: 0; + position: absolute; + top: 0; +} + +.viewer-toolbar { + margin: 0 auto 5px; + overflow: hidden; + padding: 3px 0; + width: 280px; +} + +.viewer-toolbar > li { + background-color: rgba(0, 0, 0, .5); + border-radius: 50%; + cursor: pointer; + float: left; + height: 24px; + overflow: hidden; + width: 24px; +} + +.viewer-toolbar > li:hover { + background-color: rgba(0, 0, 0, .8); +} + +.viewer-toolbar > li::before { + margin: 2px; +} + +.viewer-toolbar > li + li { + margin-left: 1px; +} + +.viewer-toolbar > .viewer-play { + height: 30px; + margin-bottom: -3px; + margin-top: -3px; + width: 30px; +} + +.viewer-toolbar > .viewer-play::before { + margin: 5px; +} + +.viewer-tooltip { + background-color: rgba(0, 0, 0, .8); + border-radius: 10px; + color: #fff; + display: none; + font-size: 12px; + height: 20px; + left: 50%; + line-height: 20px; + margin-left: -25px; + margin-top: -10px; + position: absolute; + text-align: center; + top: 50%; + width: 50px; +} + +.viewer-title { + color: #ccc; + display: inline-block; + font-size: 12px; + line-height: 1; + margin: 0 5% 5px; + max-width: 90%; + opacity: .8; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.viewer-title:hover { + opacity: 1; +} + +.viewer-button { + background-color: rgba(0, 0, 0, .5); + border-radius: 50%; + cursor: pointer; + height: 80px; + overflow: hidden; + position: absolute; + right: -40px; + top: -40px; + width: 80px; +} + +.viewer-button::before { + bottom: 15px; + left: 15px; + position: absolute; +} + +.viewer-fixed { + position: fixed; +} + +.viewer-open { + overflow: hidden; +} + +.viewer-show { + display: block; +} + +.viewer-hide { + display: none; +} + +.viewer-invisible { + visibility: hidden; +} + +.viewer-move { + cursor: move; + cursor: -webkit-grab; + cursor: grab; +} + +.viewer-fade { + opacity: 0; +} + +.viewer-in { + opacity: 1; +} + +.viewer-transition { + -webkit-transition: all .3s; + transition: all .3s; +} + +@media (max-width: 767px) { + .viewer-hide-xs-down { + display: none; + } +} + +@media (max-width: 991px) { + .viewer-hide-sm-down { + display: none; + } +} + +@media (max-width: 1199px) { + .viewer-hide-md-down { + display: none; + } +} diff --git a/public/static/libs/viewer/viewer.esm.js b/public/static/libs/viewer/viewer.esm.js new file mode 100644 index 0000000..160908a --- /dev/null +++ b/public/static/libs/viewer/viewer.esm.js @@ -0,0 +1,2048 @@ +/*! + * Viewer v0.6.0 + * https://github.com/fengyuanchen/viewer + * + * Copyright (c) 2014-2017 Fengyuan Chen + * Released under the MIT license + * + * Date: 2017-10-07T09:53:36.889Z + */ +import $ from 'jquery'; + +var DEFAULTS = { + // Enable inline mode + inline: false, + + // Show the button on the top-right of the viewer + button: true, + + // Show the navbar + navbar: true, + + // Show the title + title: true, + + // Show the toolbar + toolbar: true, + + // Show the tooltip with image ratio (percentage) when zoom in or zoom out + tooltip: true, + + // Enable to move the image + movable: true, + + // Enable to zoom the image + zoomable: true, + + // Enable to rotate the image + rotatable: true, + + // Enable to scale the image + scalable: true, + + // Enable CSS3 Transition for some special elements + transition: true, + + // Enable to request fullscreen when play + fullscreen: true, + + // Enable keyboard support + keyboard: true, + + // Define interval of each image when playing + interval: 5000, + + // Min width of the viewer in inline mode + minWidth: 200, + + // Min height of the viewer in inline mode + minHeight: 100, + + // Define the ratio when zoom the image by wheeling mouse + zoomRatio: 0.1, + + // Define the min ratio of the image when zoom out + minZoomRatio: 0.01, + + // Define the max ratio of the image when zoom in + maxZoomRatio: 100, + + // Define the CSS `z-index` value of viewer in modal mode. + zIndex: 2015, + + // Define the CSS `z-index` value of viewer in inline mode. + zIndexInline: 0, + + // Define where to get the original image URL for viewing + // Type: String (an image attribute) or Function (should return an image URL) + url: 'src', + + // Event shortcuts + ready: null, + show: null, + shown: null, + hide: null, + hidden: null, + view: null, + viewed: null +}; + +var TEMPLATE = '
              ' + '
              ' + '' + '
              ' + '
              ' + '
              ' + '
              '; + +var _window = window; +var PointerEvent = _window.PointerEvent; + + +var NAMESPACE = 'viewer'; + +// Actions +var ACTION_MOVE = 'move'; +var ACTION_SWITCH = 'switch'; +var ACTION_ZOOM = 'zoom'; + +// Classes +var CLASS_ACTIVE = NAMESPACE + '-active'; +var CLASS_CLOSE = NAMESPACE + '-close'; +var CLASS_FADE = NAMESPACE + '-fade'; +var CLASS_FIXED = NAMESPACE + '-fixed'; +var CLASS_FULLSCREEN = NAMESPACE + '-fullscreen'; +var CLASS_FULLSCREEN_EXIT = NAMESPACE + '-fullscreen-exit'; +var CLASS_HIDE = NAMESPACE + '-hide'; +var CLASS_HIDE_MD_DOWN = NAMESPACE + '-hide-md-down'; +var CLASS_HIDE_SM_DOWN = NAMESPACE + '-hide-sm-down'; +var CLASS_HIDE_XS_DOWN = NAMESPACE + '-hide-xs-down'; +var CLASS_IN = NAMESPACE + '-in'; +var CLASS_INVISIBLE = NAMESPACE + '-invisible'; +var CLASS_MOVE = NAMESPACE + '-move'; +var CLASS_OPEN = NAMESPACE + '-open'; +var CLASS_SHOW = NAMESPACE + '-show'; +var CLASS_TRANSITION = NAMESPACE + '-transition'; + +// Events +var EVENT_READY = 'ready'; +var EVENT_SHOW = 'show'; +var EVENT_SHOWN = 'shown'; +var EVENT_HIDE = 'hide'; +var EVENT_HIDDEN = 'hidden'; +var EVENT_VIEW = 'view'; +var EVENT_VIEWED = 'viewed'; +var EVENT_CLICK = 'click'; +var EVENT_DRAG_START = 'dragstart'; +var EVENT_KEY_DOWN = 'keydown'; +var EVENT_LOAD = 'load'; +var EVENT_POINTER_DOWN = PointerEvent ? 'pointerdown' : 'touchstart mousedown'; +var EVENT_POINTER_MOVE = PointerEvent ? 'pointermove' : 'mousemove touchmove'; +var EVENT_POINTER_UP = PointerEvent ? 'pointerup pointercancel' : 'touchend touchcancel mouseup'; +var EVENT_RESIZE = 'resize'; +var EVENT_TRANSITIONEND = 'transitionend'; +var EVENT_WHEEL = 'wheel mousewheel DOMMouseScroll'; + +/** + * Check if the given value is a string. + * @param {*} value - The value to check. + * @returns {boolean} Returns `true` if the given value is a string, else `false`. + */ +function isString(value) { + return typeof value === 'string'; +} + +/** + * Check if the given value is not a number. + */ +var isNaN = Number.isNaN || window.isNaN; + +/** + * Check if the given value is a number. + * @param {*} value - The value to check. + * @returns {boolean} Returns `true` if the given value is a number, else `false`. + */ +function isNumber(value) { + return typeof value === 'number' && !isNaN(value); +} + +/** + * Check if the given value is undefined. + * @param {*} value - The value to check. + * @returns {boolean} Returns `true` if the given value is undefined, else `false`. + */ +function isUndefined(value) { + return typeof value === 'undefined'; +} + +/** + * Takes a function and returns a new one that will always have a particular context. + * Custom proxy to avoid jQuery's guid. + * @param {Function} fn - The target function. + * @param {Object} context - The new context for the function. + * @returns {boolean} The new function. + */ +function proxy(fn, context) { + for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { + args[_key - 2] = arguments[_key]; + } + + return function () { + for (var _len2 = arguments.length, args2 = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args2[_key2] = arguments[_key2]; + } + + return fn.apply(context, args.concat(args2)); + }; +} + +/** + * Get the own enumerable properties of a given object. + * @param {Object} obj - The target object. + * @returns {Array} All the own enumerable properties of the given object. + */ +var objectKeys = Object.keys || function objectKeys(obj) { + var keys = []; + + $.each(obj, function (key) { + keys.push(key); + }); + + return keys; +}; + +/** + * Get transform values from an object. + * @param {Object} obj - The target object. + * @returns {string} A string contains transform values. + */ +function getTransformValues(_ref) { + var rotate = _ref.rotate, + scaleX = _ref.scaleX, + scaleY = _ref.scaleY; + + var values = []; + + if (isNumber(rotate) && rotate !== 0) { + values.push('rotate(' + rotate + 'deg)'); + } + + if (isNumber(scaleX) && scaleX !== 1) { + values.push('scaleX(' + scaleX + ')'); + } + + if (isNumber(scaleY) && scaleY !== 1) { + values.push('scaleY(' + scaleY + ')'); + } + + return values.length > 0 ? values.join(' ') : 'none'; +} + +/** + * Get an image name from an image url. + * @param {string} url - The target url. + * @example + * // picture.jpg + * getImageNameFromURL('http://domain.com/path/to/picture.jpg?size=1280×960') + * @returns {string} A string contains the image name. + */ +function getImageNameFromURL(url) { + return isString(url) ? url.replace(/^.*\//, '').replace(/[?&#].*$/, '') : ''; +} + +/** + * Get an image's natural sizes. + * @param {string} image - The target image. + * @param {Function} callback - The callback function. + */ +function getImageNaturalSizes(image, callback) { + // Modern browsers and IE9+ + if (image.naturalWidth) { + callback(image.naturalWidth, image.naturalHeight); + return; + } + + // IE8 (Don't use `new Image()` here) + var newImage = document.createElement('img'); + + newImage.onload = function () { + callback(newImage.width, newImage.height); + }; + + newImage.src = image.src; +} + +/** + * Get the related class name of a responsive type number. + * @param {string} type - The responsive type. + * @returns {string} The related class name. + */ +function getResponsiveClass(type) { + switch (type) { + case 2: + return CLASS_HIDE_XS_DOWN; + + case 3: + return CLASS_HIDE_SM_DOWN; + + case 4: + return CLASS_HIDE_MD_DOWN; + + default: + return ''; + } +} + +/** + * Get the max ratio of a group of pointers. + * @param {string} pointers - The target pointers. + * @returns {number} The result ratio. + */ +function getMaxZoomRatio(pointers) { + var pointers2 = $.extend({}, pointers); + var ratios = []; + + $.each(pointers, function (pointerId, pointer) { + delete pointers2[pointerId]; + + $.each(pointers2, function (pointerId2, pointer2) { + var x1 = Math.abs(pointer.startX - pointer2.startX); + var y1 = Math.abs(pointer.startY - pointer2.startY); + var x2 = Math.abs(pointer.endX - pointer2.endX); + var y2 = Math.abs(pointer.endY - pointer2.endY); + var z1 = Math.sqrt(x1 * x1 + y1 * y1); + var z2 = Math.sqrt(x2 * x2 + y2 * y2); + var ratio = (z2 - z1) / z1; + + ratios.push(ratio); + }); + }); + + ratios.sort(function (a, b) { + return Math.abs(a) < Math.abs(b); + }); + + return ratios[0]; +} + +/** + * Get a pointer from an event object. + * @param {Object} event - The target event object. + * @param {boolean} endOnly - Indicates if only returns the end point coordinate or not. + * @returns {Object} The result pointer contains start and/or end point coordinates. + */ +function getPointer(_ref2, endOnly) { + var pageX = _ref2.pageX, + pageY = _ref2.pageY; + + var end = { + endX: pageX, + endY: pageY + }; + + if (endOnly) { + return end; + } + + return $.extend({ + startX: pageX, + startY: pageY + }, end); +} + +/** + * Get the center point coordinate of a group of pointers. + * @param {Object} pointers - The target pointers. + * @returns {Object} The center point coordinate. + */ +function getPointersCenter(pointers) { + var pageX = 0; + var pageY = 0; + var count = 0; + + $.each(pointers, function (pointerId, _ref3) { + var startX = _ref3.startX, + startY = _ref3.startY; + + pageX += startX; + pageY += startY; + count += 1; + }); + + pageX /= count; + pageY /= count; + + return { + pageX: pageX, + pageY: pageY + }; +} + +var render = { + render: function render() { + this.initContainer(); + this.initViewer(); + this.initList(); + this.renderViewer(); + }, + initContainer: function initContainer() { + var $window = $(window); + + this.container = { + width: $window.innerWidth(), + height: $window.innerHeight() + }; + }, + initViewer: function initViewer() { + var options = this.options, + $parent = this.$parent; + + var viewer = void 0; + + if (options.inline) { + viewer = { + width: Math.max($parent.width(), options.minWidth), + height: Math.max($parent.height(), options.minHeight) + }; + this.parent = viewer; + } + + if (this.fulled || !viewer) { + viewer = this.container; + } + + this.viewer = $.extend({}, viewer); + }, + renderViewer: function renderViewer() { + if (this.options.inline && !this.fulled) { + this.$viewer.css(this.viewer); + } + }, + initList: function initList() { + var $element = this.$element, + options = this.options, + $list = this.$list; + + var list = []; + + this.$images.each(function (i, image) { + var alt = image.alt || getImageNameFromURL(image); + var src = image.src; + var url = options.url; + + + if (!src) { + return; + } + + if (isString(url)) { + url = image.getAttribute(url); + } else if ($.isFunction(url)) { + url = url.call(image, image); + } + + list.push('
            • ' + '' + '
            • '); + }); + + $list.html(list.join('')).find('img').one(EVENT_LOAD, { + filled: true + }, $.proxy(this.loadImage, this)); + + this.$items = $list.children(); + + if (options.transition) { + $element.one(EVENT_VIEWED, function () { + $list.addClass(CLASS_TRANSITION); + }); + } + }, + renderList: function renderList(index) { + var i = index || this.index; + var width = this.$items.eq(i).width(); + + // 1 pixel of `margin-left` width + var outerWidth = width + 1; + + // Place the active item in the center of the screen + this.$list.css({ + width: outerWidth * this.length, + marginLeft: (this.viewer.width - width) / 2 - outerWidth * i + }); + }, + resetList: function resetList() { + this.$list.empty().removeClass(CLASS_TRANSITION).css('margin-left', 0); + }, + initImage: function initImage(callback) { + var _this = this; + + var options = this.options, + $image = this.$image, + viewer = this.viewer; + + var footerHeight = this.$footer.height(); + var viewerWidth = viewer.width; + var viewerHeight = Math.max(viewer.height - footerHeight, footerHeight); + var oldImage = this.image || {}; + + getImageNaturalSizes($image[0], function (naturalWidth, naturalHeight) { + var aspectRatio = naturalWidth / naturalHeight; + var width = viewerWidth; + var height = viewerHeight; + + if (viewerHeight * aspectRatio > viewerWidth) { + height = viewerWidth / aspectRatio; + } else { + width = viewerHeight * aspectRatio; + } + + width = Math.min(width * 0.9, naturalWidth); + height = Math.min(height * 0.9, naturalHeight); + + var image = { + naturalWidth: naturalWidth, + naturalHeight: naturalHeight, + aspectRatio: aspectRatio, + ratio: width / naturalWidth, + width: width, + height: height, + left: (viewerWidth - width) / 2, + top: (viewerHeight - height) / 2 + }; + var initialImage = $.extend({}, image); + + if (options.rotatable) { + image.rotate = oldImage.rotate || 0; + initialImage.rotate = 0; + } + + if (options.scalable) { + image.scaleX = oldImage.scaleX || 1; + image.scaleY = oldImage.scaleY || 1; + initialImage.scaleX = 1; + initialImage.scaleY = 1; + } + + _this.image = image; + _this.initialImage = initialImage; + + if ($.isFunction(callback)) { + callback(); + } + }); + }, + renderImage: function renderImage(callback) { + var image = this.image, + $image = this.$image; + + + $image.css({ + width: image.width, + height: image.height, + marginLeft: image.left, + marginTop: image.top, + transform: getTransformValues(image) + }); + + if ($.isFunction(callback)) { + if (this.transitioning) { + $image.one(EVENT_TRANSITIONEND, callback); + } else { + callback(); + } + } + }, + resetImage: function resetImage() { + if (this.$image) { + this.$image.remove(); + this.$image = null; + } + } +}; + +var events = { + bind: function bind() { + var $element = this.$element, + options = this.options; + + + if ($.isFunction(options.view)) { + $element.on(EVENT_VIEW, options.view); + } + + if ($.isFunction(options.viewed)) { + $element.on(EVENT_VIEWED, options.viewed); + } + + this.$viewer.on(EVENT_CLICK, $.proxy(this.click, this)).on(EVENT_WHEEL, $.proxy(this.wheel, this)).on(EVENT_DRAG_START, $.proxy(this.dragstart, this)); + + this.$canvas.on(EVENT_POINTER_DOWN, $.proxy(this.pointerdown, this)); + + $(document).on(EVENT_POINTER_MOVE, this.onPointerMove = proxy(this.pointermove, this)).on(EVENT_POINTER_UP, this.onPointerUp = proxy(this.pointerup, this)).on(EVENT_KEY_DOWN, this.onKeyDown = proxy(this.keydown, this)); + + $(window).on(EVENT_RESIZE, this.onResize = proxy(this.resize, this)); + }, + unbind: function unbind() { + var $element = this.$element, + options = this.options; + + + if ($.isFunction(options.view)) { + $element.off(EVENT_VIEW, options.view); + } + + if ($.isFunction(options.viewed)) { + $element.off(EVENT_VIEWED, options.viewed); + } + + this.$viewer.off(EVENT_CLICK, this.click).off(EVENT_WHEEL, this.wheel).off(EVENT_DRAG_START, this.dragstart); + + this.$canvas.off(EVENT_POINTER_DOWN, this.pointerdown); + + $(document).off(EVENT_POINTER_MOVE, this.onPointerMove).off(EVENT_POINTER_UP, this.onPointerUp).off(EVENT_KEY_DOWN, this.onKeyDown); + + $(window).off(EVENT_RESIZE, this.onResize); + } +}; + +var handlers = { + click: function click(e) { + var $target = $(e.target); + var action = $target.data('action'); + var image = this.image; + + + switch (action) { + case 'mix': + if (this.played) { + this.stop(); + } else if (this.options.inline) { + if (this.fulled) { + this.exit(); + } else { + this.full(); + } + } else { + this.hide(); + } + + break; + + case 'view': + this.view($target.data('index')); + break; + + case 'zoom-in': + this.zoom(0.1, true); + break; + + case 'zoom-out': + this.zoom(-0.1, true); + break; + + case 'one-to-one': + this.toggle(); + break; + + case 'reset': + this.reset(); + break; + + case 'prev': + this.prev(); + break; + + case 'play': + this.play(); + break; + + case 'next': + this.next(); + break; + + case 'rotate-left': + this.rotate(-90); + break; + + case 'rotate-right': + this.rotate(90); + break; + + case 'flip-horizontal': + this.scaleX(-image.scaleX || -1); + break; + + case 'flip-vertical': + this.scaleY(-image.scaleY || -1); + break; + + default: + if (this.played) { + this.stop(); + } + } + }, + dragstart: function dragstart(e) { + if ($(e.target).is('img')) { + e.preventDefault(); + } + }, + keydown: function keydown(e) { + var options = this.options; + + + if (!this.fulled || !options.keyboard) { + return; + } + + switch (e.which) { + // (Key: Esc) + case 27: + if (this.played) { + this.stop(); + } else if (options.inline) { + if (this.fulled) { + this.exit(); + } + } else { + this.hide(); + } + + break; + + // (Key: Space) + case 32: + if (this.played) { + this.stop(); + } + + break; + + // View previous (Key: ←) + case 37: + this.prev(); + break; + + // Zoom in (Key: ↑) + case 38: + // Prevent scroll on Firefox + e.preventDefault(); + + this.zoom(options.zoomRatio, true); + break; + + // View next (Key: →) + case 39: + this.next(); + break; + + // Zoom out (Key: ↓) + case 40: + // Prevent scroll on Firefox + e.preventDefault(); + + this.zoom(-options.zoomRatio, true); + break; + + // 48: Zoom out to initial size (Key: Ctrl + 0) + // 49: Zoom in to natural size (Key: Ctrl + 1) + case 48: + case 49: + if (e.ctrlKey || e.shiftKey) { + e.preventDefault(); + this.toggle(); + } + + break; + + default: + } + }, + load: function load() { + var _this = this; + + var options = this.options, + viewer = this.viewer, + $image = this.$image; + + + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = false; + } + + if (!$image) { + return; + } + + $image.removeClass(CLASS_INVISIBLE).css('cssText', '' + ('width:0;' + 'height:0;' + 'margin-left:') + viewer.width / 2 + 'px;' + ('margin-top:' + viewer.height / 2 + 'px;') + 'max-width:none!important;' + 'visibility:visible;'); + + this.initImage(function () { + $image.toggleClass(CLASS_TRANSITION, options.transition).toggleClass(CLASS_MOVE, options.movable); + + _this.renderImage(function () { + _this.viewed = true; + _this.trigger(EVENT_VIEWED); + }); + }); + }, + loadImage: function loadImage(e) { + var image = e.target; + var $image = $(image); + var $parent = $image.parent(); + var parentWidth = $parent.width(); + var parentHeight = $parent.height(); + var filled = e.data && e.data.filled; + + getImageNaturalSizes(image, function (naturalWidth, naturalHeight) { + var aspectRatio = naturalWidth / naturalHeight; + var width = parentWidth; + var height = parentHeight; + + if (parentHeight * aspectRatio > parentWidth) { + if (filled) { + width = parentHeight * aspectRatio; + } else { + height = parentWidth / aspectRatio; + } + } else if (filled) { + height = parentWidth / aspectRatio; + } else { + width = parentHeight * aspectRatio; + } + + $image.css({ + width: width, + height: height, + marginLeft: (parentWidth - width) / 2, + marginTop: (parentHeight - height) / 2 + }); + }); + }, + pointerdown: function pointerdown(e) { + if (!this.viewed || this.transitioning) { + return; + } + + var options = this.options, + pointers = this.pointers; + var originalEvent = e.originalEvent; + + + if (originalEvent && originalEvent.changedTouches) { + $.each(originalEvent.changedTouches, function (i, touch) { + pointers[touch.identifier] = getPointer(touch); + }); + } else { + pointers[originalEvent && originalEvent.pointerId || 0] = getPointer(originalEvent || e); + } + + var action = options.movable ? ACTION_MOVE : false; + + if (objectKeys(pointers).length > 1) { + action = ACTION_ZOOM; + } else if ((e.pointerType === 'touch' || e.type === 'touchmove') && this.isSwitchable()) { + action = ACTION_SWITCH; + } + + this.action = action; + }, + pointermove: function pointermove(e) { + var $image = this.$image, + action = this.action, + pointers = this.pointers; + + + if (!this.viewed || !action) { + return; + } + + e.preventDefault(); + + var originalEvent = e.originalEvent; + + + if (originalEvent && originalEvent.changedTouches) { + $.each(originalEvent.changedTouches, function (i, touch) { + $.extend(pointers[touch.identifier], getPointer(touch, true)); + }); + } else { + $.extend(pointers[originalEvent && originalEvent.pointerId || 0], getPointer(e, true)); + } + + if (action === ACTION_MOVE && this.options.transition && $image.hasClass(CLASS_TRANSITION)) { + $image.removeClass(CLASS_TRANSITION); + } + + this.change(e); + }, + pointerup: function pointerup(e) { + if (!this.viewed) { + return; + } + + var action = this.action, + pointers = this.pointers; + var originalEvent = e.originalEvent; + + + if (originalEvent && originalEvent.changedTouches) { + $.each(originalEvent.changedTouches, function (i, touch) { + delete pointers[touch.identifier]; + }); + } else { + delete pointers[originalEvent && originalEvent.pointerId || 0]; + } + + if (!action) { + return; + } + + if (action === ACTION_MOVE && this.options.transition) { + this.$image.addClass(CLASS_TRANSITION); + } + + this.action = false; + }, + resize: function resize() { + var _this2 = this; + + this.initContainer(); + this.initViewer(); + this.renderViewer(); + this.renderList(); + + if (this.viewed) { + this.initImage(function () { + _this2.renderImage(); + }); + } + + if (this.played) { + if (this.options.fullscreen && this.fulled && !document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) { + this.stop(); + return; + } + + this.$player.find('img').one(EVENT_LOAD, $.proxy(this.loadImage, this)).trigger(EVENT_LOAD); + } + }, + start: function start(_ref) { + var target = _ref.target; + + if ($(target).is('img')) { + this.target = target; + this.show(); + } + }, + wheel: function wheel(e) { + var _this3 = this; + + if (!this.viewed) { + return; + } + + e.preventDefault(); + + // Limit wheel speed to prevent zoom too fast + if (this.wheeling) { + return; + } + + this.wheeling = true; + + setTimeout(function () { + _this3.wheeling = false; + }, 50); + + var originalEvent = e.originalEvent || e; + var delta = 1; + + if (originalEvent.deltaY) { + delta = originalEvent.deltaY > 0 ? 1 : -1; + } else if (originalEvent.wheelDelta) { + delta = -originalEvent.wheelDelta / 120; + } else if (originalEvent.detail) { + delta = originalEvent.detail > 0 ? 1 : -1; + } + + this.zoom(-delta * (Number(this.options.zoomRatio) || 0.1), true, e); + } +}; + +var methods = { + /** + * Show the viewer (only available in modal mode). + */ + show: function show() { + var _this = this; + + var $element = this.$element, + options = this.options; + + + if (options.inline || this.transitioning) { + return; + } + + if (!this.ready) { + this.build(); + } + + var $viewer = this.$viewer; + + + if ($.isFunction(options.show)) { + $element.one(EVENT_SHOW, options.show); + } + + if (this.trigger(EVENT_SHOW).isDefaultPrevented()) { + return; + } + + this.$body.addClass(CLASS_OPEN); + $viewer.removeClass(CLASS_HIDE); + $element.one(EVENT_SHOWN, function () { + _this.view(_this.target ? _this.$images.index(_this.target) : _this.index); + _this.target = false; + }); + + if (options.transition) { + this.transitioning = true; + $viewer.addClass(CLASS_TRANSITION); + + // Force reflow to enable CSS3 transition + // eslint-disable-next-line + $viewer[0].offsetWidth; + $viewer.one(EVENT_TRANSITIONEND, $.proxy(this.shown, this)).addClass(CLASS_IN); + } else { + $viewer.addClass(CLASS_IN); + this.shown(); + } + }, + + + /** + * Hide the viewer (only available in modal mode). + */ + hide: function hide() { + var _this2 = this; + + var options = this.options, + $viewer = this.$viewer; + + + if (options.inline || this.transitioning || !this.visible) { + return; + } + + if ($.isFunction(options.hide)) { + this.$element.one(EVENT_HIDE, options.hide); + } + + if (this.trigger(EVENT_HIDE).isDefaultPrevented()) { + return; + } + + if (this.viewed && options.transition) { + this.transitioning = true; + this.$image.one(EVENT_TRANSITIONEND, function () { + $viewer.one(EVENT_TRANSITIONEND, $.proxy(_this2.hidden, _this2)).removeClass(CLASS_IN); + }); + this.zoomTo(0, false, false, true); + } else { + $viewer.removeClass(CLASS_IN); + this.hidden(); + } + }, + + + /** + * View one of the images with image's index. + * @param {number} index - The image index. + */ + view: function view(index) { + var _this3 = this; + + index = Number(index) || 0; + + if (!this.visible || this.played || index < 0 || index >= this.length || this.viewed && index === this.index) { + return; + } + + if (this.trigger(EVENT_VIEW).isDefaultPrevented()) { + return; + } + + var $item = this.$items.eq(index); + var $img = $item.find('img'); + var alt = $img.attr('alt'); + var $image = $('' + alt + ''); + + this.$image = $image; + this.$items.eq(this.index).removeClass(CLASS_ACTIVE); + $item.addClass(CLASS_ACTIVE); + this.viewed = false; + this.index = index; + this.image = null; + this.$canvas.html($image.addClass(CLASS_INVISIBLE)); + + // Center current item + this.renderList(); + + var $title = this.$title; + + // Clear title + + $title.empty(); + + // Generate title after viewed + this.$element.one(EVENT_VIEWED, function () { + var _image = _this3.image, + naturalWidth = _image.naturalWidth, + naturalHeight = _image.naturalHeight; + + + $title.html(alt + ' (' + naturalWidth + ' × ' + naturalHeight + ')'); + }); + + if ($image[0].complete) { + this.load(); + } else { + $image.one(EVENT_LOAD, $.proxy(this.load, this)); + + if (this.timeout) { + clearTimeout(this.timeout); + } + + // Make the image visible if it fails to load within 1s + this.timeout = setTimeout(function () { + $image.removeClass(CLASS_INVISIBLE); + _this3.timeout = false; + }, 1000); + } + }, + + + /** + * View the previous image. + */ + prev: function prev() { + this.view(Math.max(this.index - 1, 0)); + }, + + + /** + * View the next image. + */ + next: function next() { + this.view(Math.min(this.index + 1, this.length - 1)); + }, + + + /** + * Move the image with relative offsets. + * @param {number} offsetX - The relative offset distance on the x-axis. + * @param {number} offsetY - The relative offset distance on the y-axis. + */ + move: function move(offsetX, offsetY) { + var _image2 = this.image, + left = _image2.left, + top = _image2.top; + + + this.moveTo(isUndefined(offsetX) ? offsetX : left + Number(offsetX), isUndefined(offsetY) ? offsetY : top + Number(offsetY)); + }, + + + /** + * Move the image to an absolute point. + * @param {number} x - The x-axis coordinate. + * @param {number} [y=x] - The y-axis coordinate. + */ + moveTo: function moveTo(x) { + var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : x; + + if (!this.viewed || this.played || !this.options.movable) { + return; + } + + var image = this.image; + + var changed = false; + + x = Number(x); + y = Number(y); + + if (isNumber(x)) { + image.left = x; + changed = true; + } + + if (isNumber(y)) { + image.top = y; + changed = true; + } + + if (changed) { + this.renderImage(); + } + }, + + + /** + * Zoom the image with a relative ratio. + * @param {number} ratio - The target ratio. + * @param {boolean} [hasTooltip] - Indicates if it has a tooltip or not. + * @param {Event} [_event] - The related event if any. + */ + zoom: function zoom(ratio) { + var hasTooltip = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + + var _event = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; + + var image = this.image; + + + ratio = Number(ratio); + + if (ratio < 0) { + ratio = 1 / (1 - ratio); + } else { + ratio = 1 + ratio; + } + + this.zoomTo(image.width * ratio / image.naturalWidth, hasTooltip, _event); + }, + + + /** + * Zoom the image to an absolute ratio. + * @param {number} ratio - The target ratio. + * @param {boolean} [hasTooltip] - Indicates if it has a tooltip or not. + * @param {Event} [_event] - The related event if any. + * @param {Event} [_zoomable] - Indicates if the current zoom is available or not. + */ + zoomTo: function zoomTo(ratio) { + var hasTooltip = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + + var _event = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; + + var _zoomable = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; + + var options = this.options, + image = this.image, + pointers = this.pointers; + + + ratio = Math.max(0, ratio); + + if (isNumber(ratio) && this.viewed && !this.played && (_zoomable || options.zoomable)) { + if (!_zoomable) { + var minZoomRatio = Math.max(0.01, options.minZoomRatio); + var maxZoomRatio = Math.min(100, options.maxZoomRatio); + + ratio = Math.min(Math.max(ratio, minZoomRatio), maxZoomRatio); + } + + if (_event && ratio > 0.95 && ratio < 1.05) { + ratio = 1; + } + + var newWidth = image.naturalWidth * ratio; + var newHeight = image.naturalHeight * ratio; + + if (_event && _event.originalEvent) { + var offset = this.$viewer.offset(); + var center = pointers && objectKeys(pointers).length > 0 ? getPointersCenter(pointers) : { + pageX: _event.pageX || _event.originalEvent.pageX || 0, + pageY: _event.pageY || _event.originalEvent.pageY || 0 + }; + + // Zoom from the triggering point of the event + image.left -= (newWidth - image.width) * ((center.pageX - offset.left - image.left) / image.width); + image.top -= (newHeight - image.height) * ((center.pageY - offset.top - image.top) / image.height); + } else { + // Zoom from the center of the image + image.left -= (newWidth - image.width) / 2; + image.top -= (newHeight - image.height) / 2; + } + + image.width = newWidth; + image.height = newHeight; + image.ratio = ratio; + this.renderImage(); + + if (hasTooltip) { + this.tooltip(); + } + } + }, + + + /** + * Rotate the image with a relative degree. + * @param {number} degree - The rotate degree. + */ + rotate: function rotate(degree) { + this.rotateTo((this.image.rotate || 0) + Number(degree)); + }, + + + /** + * Rotate the image to an absolute degree. + * @param {number} degree - The rotate degree. + */ + rotateTo: function rotateTo(degree) { + var image = this.image; + + + degree = Number(degree); + + if (isNumber(degree) && this.viewed && !this.played && this.options.rotatable) { + image.rotate = degree; + this.renderImage(); + } + }, + + + /** + * Scale the image on the x-axis. + * @param {number} scaleX - The scale ratio on the x-axis. + */ + scaleX: function scaleX(_scaleX) { + this.scale(_scaleX, this.image.scaleY); + }, + + + /** + * Scale the image on the y-axis. + * @param {number} scaleY - The scale ratio on the y-axis. + */ + scaleY: function scaleY(_scaleY) { + this.scale(this.image.scaleX, _scaleY); + }, + + + /** + * Scale the image. + * @param {number} scaleX - The scale ratio on the x-axis. + * @param {number} [scaleY=scaleX] - The scale ratio on the y-axis. + */ + scale: function scale(scaleX) { + var scaleY = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : scaleX; + + if (!this.viewed || this.played || !this.options.scalable) { + return; + } + + var image = this.image; + + var changed = false; + + scaleX = Number(scaleX); + scaleY = Number(scaleY); + + if (isNumber(scaleX)) { + image.scaleX = scaleX; + changed = true; + } + + if (isNumber(scaleY)) { + image.scaleY = scaleY; + changed = true; + } + + if (changed) { + this.renderImage(); + } + }, + + + /** + * Play the images. + */ + play: function play() { + var _this4 = this; + + if (!this.visible || this.played) { + return; + } + + var options = this.options, + $items = this.$items, + $player = this.$player; + + + if (options.fullscreen) { + this.requestFullscreen(); + } + + this.played = true; + $player.addClass(CLASS_SHOW); + + var list = []; + var index = 0; + + $items.each(function (i, item) { + var $item = $(item); + var $img = $item.find('img'); + var $image = $('' + $img.attr('alt') + ''); + + $image.addClass(CLASS_FADE).toggleClass(CLASS_TRANSITION, options.transition); + + if ($item.hasClass(CLASS_ACTIVE)) { + $image.addClass(CLASS_IN); + index = i; + } + + list.push($image); + $image.one(EVENT_LOAD, { + filled: false + }, $.proxy(_this4.loadImage, _this4)); + $player.append($image); + }); + + if (isNumber(options.interval) && options.interval > 0) { + var length = $items.length; + + var playing = function playing() { + _this4.playing = setTimeout(function () { + list[index].removeClass(CLASS_IN); + index += 1; + index = index < length ? index : 0; + list[index].addClass(CLASS_IN); + playing(); + }, options.interval); + }; + + if (length > 1) { + playing(); + } + } + }, + + + /** + * Stop play. + */ + stop: function stop() { + if (!this.played) { + return; + } + + if (this.options.fullscreen) { + this.exitFullscreen(); + } + + this.played = false; + clearTimeout(this.playing); + this.$player.removeClass(CLASS_SHOW).empty(); + }, + + + /** + * Enter modal mode (only available in inline mode). + */ + full: function full() { + var _this5 = this; + + var options = this.options, + $image = this.$image, + $list = this.$list; + + + if (!this.visible || this.played || this.fulled || !options.inline) { + return; + } + + this.fulled = true; + this.$body.addClass(CLASS_OPEN); + this.$button.addClass(CLASS_FULLSCREEN_EXIT); + + if (options.transition) { + $image.removeClass(CLASS_TRANSITION); + $list.removeClass(CLASS_TRANSITION); + } + + this.$viewer.addClass(CLASS_FIXED).removeAttr('style').css('z-index', options.zIndex); + this.initContainer(); + this.viewer = $.extend({}, this.container); + this.renderList(); + this.initImage(function () { + _this5.renderImage(function () { + if (options.transition) { + setTimeout(function () { + $image.addClass(CLASS_TRANSITION); + $list.addClass(CLASS_TRANSITION); + }, 0); + } + }); + }); + }, + + + /** + * Exit modal mode (only available in inline mode). + */ + exit: function exit() { + var _this6 = this; + + if (!this.fulled) { + return; + } + + var options = this.options, + $image = this.$image, + $list = this.$list; + + + this.fulled = false; + this.$body.removeClass(CLASS_OPEN); + this.$button.removeClass(CLASS_FULLSCREEN_EXIT); + + if (options.transition) { + $image.removeClass(CLASS_TRANSITION); + $list.removeClass(CLASS_TRANSITION); + } + + this.$viewer.removeClass(CLASS_FIXED).css('z-index', options.zIndexInline); + this.viewer = $.extend({}, this.parent); + this.renderViewer(); + this.renderList(); + this.initImage(function () { + _this6.renderImage(function () { + if (options.transition) { + setTimeout(function () { + $image.addClass(CLASS_TRANSITION); + $list.addClass(CLASS_TRANSITION); + }, 0); + } + }); + }); + }, + + + /** + * Show the current ratio of the image with percentage. + */ + tooltip: function tooltip() { + var _this7 = this; + + var options = this.options, + $tooltip = this.$tooltip, + image = this.image; + + var classes = [CLASS_SHOW, CLASS_FADE, CLASS_TRANSITION].join(' '); + + if (!this.viewed || this.played || !options.tooltip) { + return; + } + + $tooltip.text(Math.round(image.ratio * 100) + '%'); + + if (!this.tooltiping) { + if (options.transition) { + if (this.fading) { + $tooltip.trigger(EVENT_TRANSITIONEND); + } + + $tooltip.addClass(classes); + + // Force reflow to enable CSS3 transition + // eslint-disable-next-line + $tooltip[0].offsetWidth; + $tooltip.addClass(CLASS_IN); + } else { + $tooltip.addClass(CLASS_SHOW); + } + } else { + clearTimeout(this.tooltiping); + } + + this.tooltiping = setTimeout(function () { + if (options.transition) { + $tooltip.one(EVENT_TRANSITIONEND, function () { + $tooltip.removeClass(classes); + _this7.fading = false; + }).removeClass(CLASS_IN); + + _this7.fading = true; + } else { + $tooltip.removeClass(CLASS_SHOW); + } + + _this7.tooltiping = false; + }, 1000); + }, + + + /** + * Toggle the image size between its natural size and initial size. + */ + toggle: function toggle() { + if (this.image.ratio === 1) { + this.zoomTo(this.initialImage.ratio, true); + } else { + this.zoomTo(1, true); + } + }, + + + /** + * Reset the image to its initial state. + */ + reset: function reset() { + if (this.viewed && !this.played) { + this.image = $.extend({}, this.initialImage); + this.renderImage(); + } + }, + + + /** + * Update viewer when images changed. + */ + update: function update() { + var $element = this.$element; + var $images = this.$images; + + + if (this.isImg) { + // Destroy viewer if the target image was deleted + if (!$element.parent().length) { + this.destroy(); + return; + } + } else { + $images = $element.find('img'); + this.$images = $images; + this.length = $images.length; + } + + if (this.ready) { + var indexes = []; + var index = void 0; + + $.each(this.$items, function (i, item) { + var img = $(item).find('img')[0]; + var image = $images[i]; + + if (image) { + if (image.src !== img.src) { + indexes.push(i); + } + } else { + indexes.push(i); + } + }); + + this.$list.width('auto'); + this.initList(); + + if (this.visible) { + if (this.length) { + if (this.viewed) { + index = $.inArray(this.index, indexes); + + if (index >= 0) { + this.viewed = false; + this.view(Math.max(this.index - (index + 1), 0)); + } else { + this.$items.eq(this.index).addClass(CLASS_ACTIVE); + } + } + } else { + this.$image = null; + this.viewed = false; + this.index = 0; + this.image = null; + this.$canvas.empty(); + this.$title.empty(); + } + } + } + }, + + + /** + * Destroy the viewer instance. + */ + destroy: function destroy() { + var $element = this.$element; + + + if (this.options.inline) { + this.unbind(); + } else { + if (this.visible) { + this.unbind(); + } + + $element.off(EVENT_CLICK, this.start); + } + + this.unbuild(); + $element.removeData(NAMESPACE); + } +}; + +var _window$1 = window; +var document$1 = _window$1.document; + + +var others = { + // A shortcut for triggering custom events + trigger: function trigger(type, data) { + var e = $.Event(type, data); + + this.$element.trigger(e); + + return e; + }, + shown: function shown() { + var options = this.options; + + + this.transitioning = false; + this.fulled = true; + this.visible = true; + this.render(); + this.bind(); + + if ($.isFunction(options.shown)) { + this.$element.one(EVENT_SHOWN, options.shown); + } + + this.trigger(EVENT_SHOWN); + }, + hidden: function hidden() { + var options = this.options; + + + this.transitioning = false; + this.viewed = false; + this.fulled = false; + this.visible = false; + this.unbind(); + this.$body.removeClass(CLASS_OPEN); + this.$viewer.addClass(CLASS_HIDE); + this.resetList(); + this.resetImage(); + + if ($.isFunction(options.hidden)) { + this.$element.one(EVENT_HIDDEN, options.hidden); + } + + this.trigger(EVENT_HIDDEN); + }, + requestFullscreen: function requestFullscreen() { + if (this.fulled && !document$1.fullscreenElement && !document$1.mozFullScreenElement && !document$1.webkitFullscreenElement && !document$1.msFullscreenElement) { + var documentElement = document$1.documentElement; + + + if (documentElement.requestFullscreen) { + documentElement.requestFullscreen(); + } else if (documentElement.msRequestFullscreen) { + documentElement.msRequestFullscreen(); + } else if (documentElement.mozRequestFullScreen) { + documentElement.mozRequestFullScreen(); + } else if (documentElement.webkitRequestFullscreen) { + documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + } + } + }, + exitFullscreen: function exitFullscreen() { + if (this.fulled) { + if (document$1.exitFullscreen) { + document$1.exitFullscreen(); + } else if (document$1.msExitFullscreen) { + document$1.msExitFullscreen(); + } else if (document$1.mozCancelFullScreen) { + document$1.mozCancelFullScreen(); + } else if (document$1.webkitExitFullscreen) { + document$1.webkitExitFullscreen(); + } + } + }, + change: function change(event) { + var pointers = this.pointers; + + var pointer = pointers[Object.keys(pointers)[0]]; + var offsetX = pointer.endX - pointer.startX; + var offsetY = pointer.endY - pointer.startY; + + switch (this.action) { + // Move the current image + case ACTION_MOVE: + this.move(offsetX, offsetY); + break; + + // Zoom the current image + case ACTION_ZOOM: + this.zoom(getMaxZoomRatio(pointers), false, event); + + this.startX2 = this.endX2; + this.startY2 = this.endY2; + break; + + case ACTION_SWITCH: + this.action = 'switched'; + + if (Math.abs(offsetX) > Math.abs(offsetY)) { + if (offsetX > 1) { + this.prev(); + } else if (offsetX < -1) { + this.next(); + } + } + + break; + + default: + } + + // Override + $.each(pointers, function (i, p) { + p.startX = p.endX; + p.startY = p.endY; + }); + }, + isSwitchable: function isSwitchable() { + var image = this.image, + viewer = this.viewer; + + + return image.left >= 0 && image.top >= 0 && image.width <= viewer.width && image.height <= viewer.height; + } +}; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Viewer = function () { + /** + * Start the new Viewer. + * @param {Element} element - The target element for viewing. + * @param {Object} [options={}] - The configuration options. + */ + function Viewer(element) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + _classCallCheck(this, Viewer); + + if (!element || element.nodeType !== 1) { + throw new Error('The first argument is required and must be an element.'); + } + + this.element = element; + this.$element = $(element); + this.options = $.extend({}, DEFAULTS, $.isPlainObject(options) && options); + this.action = ''; + this.target = null; + this.timeout = null; + this.index = 0; + this.length = 0; + this.ready = false; + this.fading = false; + this.fulled = false; + this.isImg = false; + this.played = false; + this.playing = false; + this.tooltiping = false; + this.transitioning = false; + this.viewed = false; + this.visible = false; + this.wheeling = false; + this.pointers = {}; + this.init(); + } + + _createClass(Viewer, [{ + key: 'init', + value: function init() { + var _this = this; + + var $element = this.$element, + options = this.options; + + var isImg = $element.is('img'); + var $images = isImg ? $element : $element.find('img'); + var length = $images.length; + + + if (!length) { + return; + } + + // Override `transition` option if it is not supported + if (typeof document.createElement(NAMESPACE).style.transition === 'undefined') { + options.transition = false; + } + + this.isImg = isImg; + this.length = length; + this.count = 0; + this.$images = $images; + this.$body = $('body'); + + if (options.inline) { + $element.one(EVENT_READY, function () { + _this.view(); + }); + + $images.each(function (i, image) { + if (image.complete) { + _this.progress(); + } else { + $(image).one(EVENT_LOAD, $.proxy(_this.progress, _this)); + } + }); + } else { + $element.on(EVENT_CLICK, $.proxy(this.start, this)); + } + } + }, { + key: 'progress', + value: function progress() { + this.count += 1; + + if (this.count === this.length) { + this.build(); + } + } + }, { + key: 'build', + value: function build() { + var $element = this.$element, + options = this.options; + + + if (this.ready) { + return; + } + + var $parent = $element.parent(); + var $viewer = $(TEMPLATE); + var $button = $viewer.find('.' + NAMESPACE + '-button'); + var $navbar = $viewer.find('.' + NAMESPACE + '-navbar'); + var $title = $viewer.find('.' + NAMESPACE + '-title'); + var $toolbar = $viewer.find('.' + NAMESPACE + '-toolbar'); + + this.$parent = $parent; + this.$viewer = $viewer; + this.$button = $button; + this.$navbar = $navbar; + this.$title = $title; + this.$toolbar = $toolbar; + this.$canvas = $viewer.find('.' + NAMESPACE + '-canvas'); + this.$footer = $viewer.find('.' + NAMESPACE + '-footer'); + this.$list = $viewer.find('.' + NAMESPACE + '-list'); + this.$player = $viewer.find('.' + NAMESPACE + '-player'); + this.$tooltip = $viewer.find('.' + NAMESPACE + '-tooltip'); + + $title.addClass(!options.title ? CLASS_HIDE : getResponsiveClass(options.title)); + $toolbar.addClass(!options.toolbar ? CLASS_HIDE : getResponsiveClass(options.toolbar)); + $toolbar.find('li[class*=zoom]').toggleClass(CLASS_INVISIBLE, !options.zoomable); + $toolbar.find('li[class*=flip]').toggleClass(CLASS_INVISIBLE, !options.scalable); + + if (!options.rotatable) { + $toolbar.find('li[class*=rotate]').addClass(CLASS_INVISIBLE).appendTo($toolbar); + } + + $navbar.addClass(!options.navbar ? CLASS_HIDE : getResponsiveClass(options.navbar)); + $button.toggleClass(CLASS_HIDE, !options.button); + + if (options.inline) { + $button.addClass(CLASS_FULLSCREEN); + $viewer.css('z-index', options.zIndexInline); + + if ($parent.css('position') === 'static') { + $parent.css('position', 'relative'); + } + + $element.after($viewer); + } else { + $button.addClass(CLASS_CLOSE); + $viewer.css('z-index', options.zIndex).addClass([CLASS_FIXED, CLASS_FADE, CLASS_HIDE].join(' ')).appendTo('body'); + } + + if (options.inline) { + this.render(); + this.bind(); + this.visible = true; + } + + this.ready = true; + + if ($.isFunction(options.ready)) { + $element.one(EVENT_READY, options.ready); + } + + this.trigger(EVENT_READY); + } + }, { + key: 'unbuild', + value: function unbuild() { + if (!this.ready) { + return; + } + + this.ready = false; + this.$viewer.remove(); + } + + /** + * Change the default options. + * @static + * @param {Object} options - The new default options. + */ + + }], [{ + key: 'setDefaults', + value: function setDefaults(options) { + $.extend(DEFAULTS, options); + } + }]); + + return Viewer; +}(); + +$.extend(Viewer.prototype, render, events, handlers, methods, others); + +var AnotherViewer = $.fn.viewer; + +$.fn.viewer = function jQueryViewer(option) { + for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + var result = void 0; + + this.each(function (i, element) { + var $element = $(element); + var data = $element.data(NAMESPACE); + + if (!data) { + if (/destroy/.test(option)) { + return; + } + + var options = $.extend({}, $element.data(), $.isPlainObject(option) && option); + + data = new Viewer(element, options); + $element.data(NAMESPACE, data); + } + + if (isString(option)) { + var fn = data[option]; + + if ($.isFunction(fn)) { + result = fn.apply(data, args); + } + } + }); + + return isUndefined(result) ? this : result; +}; + +$.fn.viewer.Constructor = Viewer; +$.fn.viewer.setDefaults = Viewer.setDefaults; +$.fn.viewer.noConflict = function noConflict() { + $.fn.viewer = AnotherViewer; + return this; +}; diff --git a/public/static/libs/viewer/viewer.js b/public/static/libs/viewer/viewer.js new file mode 100644 index 0000000..4a5c010 --- /dev/null +++ b/public/static/libs/viewer/viewer.js @@ -0,0 +1,2056 @@ +/*! + * Viewer v0.6.0 + * https://github.com/fengyuanchen/viewer + * + * Copyright (c) 2014-2017 Fengyuan Chen + * Released under the MIT license + * + * Date: 2017-10-07T09:53:36.889Z + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('jquery')) : + typeof define === 'function' && define.amd ? define(['jquery'], factory) : + (factory(global.jQuery)); +}(this, (function ($) { 'use strict'; + +$ = $ && $.hasOwnProperty('default') ? $['default'] : $; + +var DEFAULTS = { + // Enable inline mode + inline: false, + + // Show the button on the top-right of the viewer + button: true, + + // Show the navbar + navbar: true, + + // Show the title + title: true, + + // Show the toolbar + toolbar: true, + + // Show the tooltip with image ratio (percentage) when zoom in or zoom out + tooltip: true, + + // Enable to move the image + movable: true, + + // Enable to zoom the image + zoomable: true, + + // Enable to rotate the image + rotatable: true, + + // Enable to scale the image + scalable: true, + + // Enable CSS3 Transition for some special elements + transition: true, + + // Enable to request fullscreen when play + fullscreen: true, + + // Enable keyboard support + keyboard: true, + + // Define interval of each image when playing + interval: 5000, + + // Min width of the viewer in inline mode + minWidth: 200, + + // Min height of the viewer in inline mode + minHeight: 100, + + // Define the ratio when zoom the image by wheeling mouse + zoomRatio: 0.1, + + // Define the min ratio of the image when zoom out + minZoomRatio: 0.01, + + // Define the max ratio of the image when zoom in + maxZoomRatio: 100, + + // Define the CSS `z-index` value of viewer in modal mode. + zIndex: 2015, + + // Define the CSS `z-index` value of viewer in inline mode. + zIndexInline: 0, + + // Define where to get the original image URL for viewing + // Type: String (an image attribute) or Function (should return an image URL) + url: 'src', + + // Event shortcuts + ready: null, + show: null, + shown: null, + hide: null, + hidden: null, + view: null, + viewed: null +}; + +var TEMPLATE = '
              ' + '
              ' + '' + '
              ' + '
              ' + '
              ' + '
              '; + +var _window = window; +var PointerEvent = _window.PointerEvent; + + +var NAMESPACE = 'viewer'; + +// Actions +var ACTION_MOVE = 'move'; +var ACTION_SWITCH = 'switch'; +var ACTION_ZOOM = 'zoom'; + +// Classes +var CLASS_ACTIVE = NAMESPACE + '-active'; +var CLASS_CLOSE = NAMESPACE + '-close'; +var CLASS_FADE = NAMESPACE + '-fade'; +var CLASS_FIXED = NAMESPACE + '-fixed'; +var CLASS_FULLSCREEN = NAMESPACE + '-fullscreen'; +var CLASS_FULLSCREEN_EXIT = NAMESPACE + '-fullscreen-exit'; +var CLASS_HIDE = NAMESPACE + '-hide'; +var CLASS_HIDE_MD_DOWN = NAMESPACE + '-hide-md-down'; +var CLASS_HIDE_SM_DOWN = NAMESPACE + '-hide-sm-down'; +var CLASS_HIDE_XS_DOWN = NAMESPACE + '-hide-xs-down'; +var CLASS_IN = NAMESPACE + '-in'; +var CLASS_INVISIBLE = NAMESPACE + '-invisible'; +var CLASS_MOVE = NAMESPACE + '-move'; +var CLASS_OPEN = NAMESPACE + '-open'; +var CLASS_SHOW = NAMESPACE + '-show'; +var CLASS_TRANSITION = NAMESPACE + '-transition'; + +// Events +var EVENT_READY = 'ready'; +var EVENT_SHOW = 'show'; +var EVENT_SHOWN = 'shown'; +var EVENT_HIDE = 'hide'; +var EVENT_HIDDEN = 'hidden'; +var EVENT_VIEW = 'view'; +var EVENT_VIEWED = 'viewed'; +var EVENT_CLICK = 'click'; +var EVENT_DRAG_START = 'dragstart'; +var EVENT_KEY_DOWN = 'keydown'; +var EVENT_LOAD = 'load'; +var EVENT_POINTER_DOWN = PointerEvent ? 'pointerdown' : 'touchstart mousedown'; +var EVENT_POINTER_MOVE = PointerEvent ? 'pointermove' : 'mousemove touchmove'; +var EVENT_POINTER_UP = PointerEvent ? 'pointerup pointercancel' : 'touchend touchcancel mouseup'; +var EVENT_RESIZE = 'resize'; +var EVENT_TRANSITIONEND = 'transitionend'; +var EVENT_WHEEL = 'wheel mousewheel DOMMouseScroll'; + +/** + * Check if the given value is a string. + * @param {*} value - The value to check. + * @returns {boolean} Returns `true` if the given value is a string, else `false`. + */ +function isString(value) { + return typeof value === 'string'; +} + +/** + * Check if the given value is not a number. + */ +var isNaN = Number.isNaN || window.isNaN; + +/** + * Check if the given value is a number. + * @param {*} value - The value to check. + * @returns {boolean} Returns `true` if the given value is a number, else `false`. + */ +function isNumber(value) { + return typeof value === 'number' && !isNaN(value); +} + +/** + * Check if the given value is undefined. + * @param {*} value - The value to check. + * @returns {boolean} Returns `true` if the given value is undefined, else `false`. + */ +function isUndefined(value) { + return typeof value === 'undefined'; +} + +/** + * Takes a function and returns a new one that will always have a particular context. + * Custom proxy to avoid jQuery's guid. + * @param {Function} fn - The target function. + * @param {Object} context - The new context for the function. + * @returns {boolean} The new function. + */ +function proxy(fn, context) { + for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { + args[_key - 2] = arguments[_key]; + } + + return function () { + for (var _len2 = arguments.length, args2 = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args2[_key2] = arguments[_key2]; + } + + return fn.apply(context, args.concat(args2)); + }; +} + +/** + * Get the own enumerable properties of a given object. + * @param {Object} obj - The target object. + * @returns {Array} All the own enumerable properties of the given object. + */ +var objectKeys = Object.keys || function objectKeys(obj) { + var keys = []; + + $.each(obj, function (key) { + keys.push(key); + }); + + return keys; +}; + +/** + * Get transform values from an object. + * @param {Object} obj - The target object. + * @returns {string} A string contains transform values. + */ +function getTransformValues(_ref) { + var rotate = _ref.rotate, + scaleX = _ref.scaleX, + scaleY = _ref.scaleY; + + var values = []; + + if (isNumber(rotate) && rotate !== 0) { + values.push('rotate(' + rotate + 'deg)'); + } + + if (isNumber(scaleX) && scaleX !== 1) { + values.push('scaleX(' + scaleX + ')'); + } + + if (isNumber(scaleY) && scaleY !== 1) { + values.push('scaleY(' + scaleY + ')'); + } + + return values.length > 0 ? values.join(' ') : 'none'; +} + +/** + * Get an image name from an image url. + * @param {string} url - The target url. + * @example + * // picture.jpg + * getImageNameFromURL('http://domain.com/path/to/picture.jpg?size=1280×960') + * @returns {string} A string contains the image name. + */ +function getImageNameFromURL(url) { + return isString(url) ? url.replace(/^.*\//, '').replace(/[?&#].*$/, '') : ''; +} + +/** + * Get an image's natural sizes. + * @param {string} image - The target image. + * @param {Function} callback - The callback function. + */ +function getImageNaturalSizes(image, callback) { + // Modern browsers and IE9+ + if (image.naturalWidth) { + callback(image.naturalWidth, image.naturalHeight); + return; + } + + // IE8 (Don't use `new Image()` here) + var newImage = document.createElement('img'); + + newImage.onload = function () { + callback(newImage.width, newImage.height); + }; + + newImage.src = image.src; +} + +/** + * Get the related class name of a responsive type number. + * @param {string} type - The responsive type. + * @returns {string} The related class name. + */ +function getResponsiveClass(type) { + switch (type) { + case 2: + return CLASS_HIDE_XS_DOWN; + + case 3: + return CLASS_HIDE_SM_DOWN; + + case 4: + return CLASS_HIDE_MD_DOWN; + + default: + return ''; + } +} + +/** + * Get the max ratio of a group of pointers. + * @param {string} pointers - The target pointers. + * @returns {number} The result ratio. + */ +function getMaxZoomRatio(pointers) { + var pointers2 = $.extend({}, pointers); + var ratios = []; + + $.each(pointers, function (pointerId, pointer) { + delete pointers2[pointerId]; + + $.each(pointers2, function (pointerId2, pointer2) { + var x1 = Math.abs(pointer.startX - pointer2.startX); + var y1 = Math.abs(pointer.startY - pointer2.startY); + var x2 = Math.abs(pointer.endX - pointer2.endX); + var y2 = Math.abs(pointer.endY - pointer2.endY); + var z1 = Math.sqrt(x1 * x1 + y1 * y1); + var z2 = Math.sqrt(x2 * x2 + y2 * y2); + var ratio = (z2 - z1) / z1; + + ratios.push(ratio); + }); + }); + + ratios.sort(function (a, b) { + return Math.abs(a) < Math.abs(b); + }); + + return ratios[0]; +} + +/** + * Get a pointer from an event object. + * @param {Object} event - The target event object. + * @param {boolean} endOnly - Indicates if only returns the end point coordinate or not. + * @returns {Object} The result pointer contains start and/or end point coordinates. + */ +function getPointer(_ref2, endOnly) { + var pageX = _ref2.pageX, + pageY = _ref2.pageY; + + var end = { + endX: pageX, + endY: pageY + }; + + if (endOnly) { + return end; + } + + return $.extend({ + startX: pageX, + startY: pageY + }, end); +} + +/** + * Get the center point coordinate of a group of pointers. + * @param {Object} pointers - The target pointers. + * @returns {Object} The center point coordinate. + */ +function getPointersCenter(pointers) { + var pageX = 0; + var pageY = 0; + var count = 0; + + $.each(pointers, function (pointerId, _ref3) { + var startX = _ref3.startX, + startY = _ref3.startY; + + pageX += startX; + pageY += startY; + count += 1; + }); + + pageX /= count; + pageY /= count; + + return { + pageX: pageX, + pageY: pageY + }; +} + +var render = { + render: function render() { + this.initContainer(); + this.initViewer(); + this.initList(); + this.renderViewer(); + }, + initContainer: function initContainer() { + var $window = $(window); + + this.container = { + width: $window.innerWidth(), + height: $window.innerHeight() + }; + }, + initViewer: function initViewer() { + var options = this.options, + $parent = this.$parent; + + var viewer = void 0; + + if (options.inline) { + viewer = { + width: Math.max($parent.width(), options.minWidth), + height: Math.max($parent.height(), options.minHeight) + }; + this.parent = viewer; + } + + if (this.fulled || !viewer) { + viewer = this.container; + } + + this.viewer = $.extend({}, viewer); + }, + renderViewer: function renderViewer() { + if (this.options.inline && !this.fulled) { + this.$viewer.css(this.viewer); + } + }, + initList: function initList() { + var $element = this.$element, + options = this.options, + $list = this.$list; + + var list = []; + + this.$images.each(function (i, image) { + var alt = image.alt || getImageNameFromURL(image); + var src = image.src; + var url = options.url; + + + if (!src) { + return; + } + + if (isString(url)) { + url = image.getAttribute(url); + } else if ($.isFunction(url)) { + url = url.call(image, image); + } + + list.push('
            • ' + '' + '
            • '); + }); + + $list.html(list.join('')).find('img').one(EVENT_LOAD, { + filled: true + }, $.proxy(this.loadImage, this)); + + this.$items = $list.children(); + + if (options.transition) { + $element.one(EVENT_VIEWED, function () { + $list.addClass(CLASS_TRANSITION); + }); + } + }, + renderList: function renderList(index) { + var i = index || this.index; + var width = this.$items.eq(i).width(); + + // 1 pixel of `margin-left` width + var outerWidth = width + 1; + + // Place the active item in the center of the screen + this.$list.css({ + width: outerWidth * this.length, + marginLeft: (this.viewer.width - width) / 2 - outerWidth * i + }); + }, + resetList: function resetList() { + this.$list.empty().removeClass(CLASS_TRANSITION).css('margin-left', 0); + }, + initImage: function initImage(callback) { + var _this = this; + + var options = this.options, + $image = this.$image, + viewer = this.viewer; + + var footerHeight = this.$footer.height(); + var viewerWidth = viewer.width; + var viewerHeight = Math.max(viewer.height - footerHeight, footerHeight); + var oldImage = this.image || {}; + + getImageNaturalSizes($image[0], function (naturalWidth, naturalHeight) { + var aspectRatio = naturalWidth / naturalHeight; + var width = viewerWidth; + var height = viewerHeight; + + if (viewerHeight * aspectRatio > viewerWidth) { + height = viewerWidth / aspectRatio; + } else { + width = viewerHeight * aspectRatio; + } + + width = Math.min(width * 0.9, naturalWidth); + height = Math.min(height * 0.9, naturalHeight); + + var image = { + naturalWidth: naturalWidth, + naturalHeight: naturalHeight, + aspectRatio: aspectRatio, + ratio: width / naturalWidth, + width: width, + height: height, + left: (viewerWidth - width) / 2, + top: (viewerHeight - height) / 2 + }; + var initialImage = $.extend({}, image); + + if (options.rotatable) { + image.rotate = oldImage.rotate || 0; + initialImage.rotate = 0; + } + + if (options.scalable) { + image.scaleX = oldImage.scaleX || 1; + image.scaleY = oldImage.scaleY || 1; + initialImage.scaleX = 1; + initialImage.scaleY = 1; + } + + _this.image = image; + _this.initialImage = initialImage; + + if ($.isFunction(callback)) { + callback(); + } + }); + }, + renderImage: function renderImage(callback) { + var image = this.image, + $image = this.$image; + + + $image.css({ + width: image.width, + height: image.height, + marginLeft: image.left, + marginTop: image.top, + transform: getTransformValues(image) + }); + + if ($.isFunction(callback)) { + if (this.transitioning) { + $image.one(EVENT_TRANSITIONEND, callback); + } else { + callback(); + } + } + }, + resetImage: function resetImage() { + if (this.$image) { + this.$image.remove(); + this.$image = null; + } + } +}; + +var events = { + bind: function bind() { + var $element = this.$element, + options = this.options; + + + if ($.isFunction(options.view)) { + $element.on(EVENT_VIEW, options.view); + } + + if ($.isFunction(options.viewed)) { + $element.on(EVENT_VIEWED, options.viewed); + } + + this.$viewer.on(EVENT_CLICK, $.proxy(this.click, this)).on(EVENT_WHEEL, $.proxy(this.wheel, this)).on(EVENT_DRAG_START, $.proxy(this.dragstart, this)); + + this.$canvas.on(EVENT_POINTER_DOWN, $.proxy(this.pointerdown, this)); + + $(document).on(EVENT_POINTER_MOVE, this.onPointerMove = proxy(this.pointermove, this)).on(EVENT_POINTER_UP, this.onPointerUp = proxy(this.pointerup, this)).on(EVENT_KEY_DOWN, this.onKeyDown = proxy(this.keydown, this)); + + $(window).on(EVENT_RESIZE, this.onResize = proxy(this.resize, this)); + }, + unbind: function unbind() { + var $element = this.$element, + options = this.options; + + + if ($.isFunction(options.view)) { + $element.off(EVENT_VIEW, options.view); + } + + if ($.isFunction(options.viewed)) { + $element.off(EVENT_VIEWED, options.viewed); + } + + this.$viewer.off(EVENT_CLICK, this.click).off(EVENT_WHEEL, this.wheel).off(EVENT_DRAG_START, this.dragstart); + + this.$canvas.off(EVENT_POINTER_DOWN, this.pointerdown); + + $(document).off(EVENT_POINTER_MOVE, this.onPointerMove).off(EVENT_POINTER_UP, this.onPointerUp).off(EVENT_KEY_DOWN, this.onKeyDown); + + $(window).off(EVENT_RESIZE, this.onResize); + } +}; + +var handlers = { + click: function click(e) { + var $target = $(e.target); + var action = $target.data('action'); + var image = this.image; + + + switch (action) { + case 'mix': + if (this.played) { + this.stop(); + } else if (this.options.inline) { + if (this.fulled) { + this.exit(); + } else { + this.full(); + } + } else { + this.hide(); + } + + break; + + case 'view': + this.view($target.data('index')); + break; + + case 'zoom-in': + this.zoom(0.1, true); + break; + + case 'zoom-out': + this.zoom(-0.1, true); + break; + + case 'one-to-one': + this.toggle(); + break; + + case 'reset': + this.reset(); + break; + + case 'prev': + this.prev(); + break; + + case 'play': + this.play(); + break; + + case 'next': + this.next(); + break; + + case 'rotate-left': + this.rotate(-90); + break; + + case 'rotate-right': + this.rotate(90); + break; + + case 'flip-horizontal': + this.scaleX(-image.scaleX || -1); + break; + + case 'flip-vertical': + this.scaleY(-image.scaleY || -1); + break; + + default: + if (this.played) { + this.stop(); + } + } + }, + dragstart: function dragstart(e) { + if ($(e.target).is('img')) { + e.preventDefault(); + } + }, + keydown: function keydown(e) { + var options = this.options; + + + if (!this.fulled || !options.keyboard) { + return; + } + + switch (e.which) { + // (Key: Esc) + case 27: + if (this.played) { + this.stop(); + } else if (options.inline) { + if (this.fulled) { + this.exit(); + } + } else { + this.hide(); + } + + break; + + // (Key: Space) + case 32: + if (this.played) { + this.stop(); + } + + break; + + // View previous (Key: ←) + case 37: + this.prev(); + break; + + // Zoom in (Key: ↑) + case 38: + // Prevent scroll on Firefox + e.preventDefault(); + + this.zoom(options.zoomRatio, true); + break; + + // View next (Key: →) + case 39: + this.next(); + break; + + // Zoom out (Key: ↓) + case 40: + // Prevent scroll on Firefox + e.preventDefault(); + + this.zoom(-options.zoomRatio, true); + break; + + // 48: Zoom out to initial size (Key: Ctrl + 0) + // 49: Zoom in to natural size (Key: Ctrl + 1) + case 48: + case 49: + if (e.ctrlKey || e.shiftKey) { + e.preventDefault(); + this.toggle(); + } + + break; + + default: + } + }, + load: function load() { + var _this = this; + + var options = this.options, + viewer = this.viewer, + $image = this.$image; + + + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = false; + } + + if (!$image) { + return; + } + + $image.removeClass(CLASS_INVISIBLE).css('cssText', '' + ('width:0;' + 'height:0;' + 'margin-left:') + viewer.width / 2 + 'px;' + ('margin-top:' + viewer.height / 2 + 'px;') + 'max-width:none!important;' + 'visibility:visible;'); + + this.initImage(function () { + $image.toggleClass(CLASS_TRANSITION, options.transition).toggleClass(CLASS_MOVE, options.movable); + + _this.renderImage(function () { + _this.viewed = true; + _this.trigger(EVENT_VIEWED); + }); + }); + }, + loadImage: function loadImage(e) { + var image = e.target; + var $image = $(image); + var $parent = $image.parent(); + var parentWidth = $parent.width(); + var parentHeight = $parent.height(); + var filled = e.data && e.data.filled; + + getImageNaturalSizes(image, function (naturalWidth, naturalHeight) { + var aspectRatio = naturalWidth / naturalHeight; + var width = parentWidth; + var height = parentHeight; + + if (parentHeight * aspectRatio > parentWidth) { + if (filled) { + width = parentHeight * aspectRatio; + } else { + height = parentWidth / aspectRatio; + } + } else if (filled) { + height = parentWidth / aspectRatio; + } else { + width = parentHeight * aspectRatio; + } + + $image.css({ + width: width, + height: height, + marginLeft: (parentWidth - width) / 2, + marginTop: (parentHeight - height) / 2 + }); + }); + }, + pointerdown: function pointerdown(e) { + if (!this.viewed || this.transitioning) { + return; + } + + var options = this.options, + pointers = this.pointers; + var originalEvent = e.originalEvent; + + + if (originalEvent && originalEvent.changedTouches) { + $.each(originalEvent.changedTouches, function (i, touch) { + pointers[touch.identifier] = getPointer(touch); + }); + } else { + pointers[originalEvent && originalEvent.pointerId || 0] = getPointer(originalEvent || e); + } + + var action = options.movable ? ACTION_MOVE : false; + + if (objectKeys(pointers).length > 1) { + action = ACTION_ZOOM; + } else if ((e.pointerType === 'touch' || e.type === 'touchmove') && this.isSwitchable()) { + action = ACTION_SWITCH; + } + + this.action = action; + }, + pointermove: function pointermove(e) { + var $image = this.$image, + action = this.action, + pointers = this.pointers; + + + if (!this.viewed || !action) { + return; + } + + e.preventDefault(); + + var originalEvent = e.originalEvent; + + + if (originalEvent && originalEvent.changedTouches) { + $.each(originalEvent.changedTouches, function (i, touch) { + $.extend(pointers[touch.identifier], getPointer(touch, true)); + }); + } else { + $.extend(pointers[originalEvent && originalEvent.pointerId || 0], getPointer(e, true)); + } + + if (action === ACTION_MOVE && this.options.transition && $image.hasClass(CLASS_TRANSITION)) { + $image.removeClass(CLASS_TRANSITION); + } + + this.change(e); + }, + pointerup: function pointerup(e) { + if (!this.viewed) { + return; + } + + var action = this.action, + pointers = this.pointers; + var originalEvent = e.originalEvent; + + + if (originalEvent && originalEvent.changedTouches) { + $.each(originalEvent.changedTouches, function (i, touch) { + delete pointers[touch.identifier]; + }); + } else { + delete pointers[originalEvent && originalEvent.pointerId || 0]; + } + + if (!action) { + return; + } + + if (action === ACTION_MOVE && this.options.transition) { + this.$image.addClass(CLASS_TRANSITION); + } + + this.action = false; + }, + resize: function resize() { + var _this2 = this; + + this.initContainer(); + this.initViewer(); + this.renderViewer(); + this.renderList(); + + if (this.viewed) { + this.initImage(function () { + _this2.renderImage(); + }); + } + + if (this.played) { + if (this.options.fullscreen && this.fulled && !document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) { + this.stop(); + return; + } + + this.$player.find('img').one(EVENT_LOAD, $.proxy(this.loadImage, this)).trigger(EVENT_LOAD); + } + }, + start: function start(_ref) { + var target = _ref.target; + + if ($(target).is('img')) { + this.target = target; + this.show(); + } + }, + wheel: function wheel(e) { + var _this3 = this; + + if (!this.viewed) { + return; + } + + e.preventDefault(); + + // Limit wheel speed to prevent zoom too fast + if (this.wheeling) { + return; + } + + this.wheeling = true; + + setTimeout(function () { + _this3.wheeling = false; + }, 50); + + var originalEvent = e.originalEvent || e; + var delta = 1; + + if (originalEvent.deltaY) { + delta = originalEvent.deltaY > 0 ? 1 : -1; + } else if (originalEvent.wheelDelta) { + delta = -originalEvent.wheelDelta / 120; + } else if (originalEvent.detail) { + delta = originalEvent.detail > 0 ? 1 : -1; + } + + this.zoom(-delta * (Number(this.options.zoomRatio) || 0.1), true, e); + } +}; + +var methods = { + /** + * Show the viewer (only available in modal mode). + */ + show: function show() { + var _this = this; + + var $element = this.$element, + options = this.options; + + + if (options.inline || this.transitioning) { + return; + } + + if (!this.ready) { + this.build(); + } + + var $viewer = this.$viewer; + + + if ($.isFunction(options.show)) { + $element.one(EVENT_SHOW, options.show); + } + + if (this.trigger(EVENT_SHOW).isDefaultPrevented()) { + return; + } + + this.$body.addClass(CLASS_OPEN); + $viewer.removeClass(CLASS_HIDE); + $element.one(EVENT_SHOWN, function () { + _this.view(_this.target ? _this.$images.index(_this.target) : _this.index); + _this.target = false; + }); + + if (options.transition) { + this.transitioning = true; + $viewer.addClass(CLASS_TRANSITION); + + // Force reflow to enable CSS3 transition + // eslint-disable-next-line + $viewer[0].offsetWidth; + $viewer.one(EVENT_TRANSITIONEND, $.proxy(this.shown, this)).addClass(CLASS_IN); + } else { + $viewer.addClass(CLASS_IN); + this.shown(); + } + }, + + + /** + * Hide the viewer (only available in modal mode). + */ + hide: function hide() { + var _this2 = this; + + var options = this.options, + $viewer = this.$viewer; + + + if (options.inline || this.transitioning || !this.visible) { + return; + } + + if ($.isFunction(options.hide)) { + this.$element.one(EVENT_HIDE, options.hide); + } + + if (this.trigger(EVENT_HIDE).isDefaultPrevented()) { + return; + } + + if (this.viewed && options.transition) { + this.transitioning = true; + this.$image.one(EVENT_TRANSITIONEND, function () { + $viewer.one(EVENT_TRANSITIONEND, $.proxy(_this2.hidden, _this2)).removeClass(CLASS_IN); + }); + this.zoomTo(0, false, false, true); + } else { + $viewer.removeClass(CLASS_IN); + this.hidden(); + } + }, + + + /** + * View one of the images with image's index. + * @param {number} index - The image index. + */ + view: function view(index) { + var _this3 = this; + + index = Number(index) || 0; + + if (!this.visible || this.played || index < 0 || index >= this.length || this.viewed && index === this.index) { + return; + } + + if (this.trigger(EVENT_VIEW).isDefaultPrevented()) { + return; + } + + var $item = this.$items.eq(index); + var $img = $item.find('img'); + var alt = $img.attr('alt'); + var $image = $('' + alt + ''); + + this.$image = $image; + this.$items.eq(this.index).removeClass(CLASS_ACTIVE); + $item.addClass(CLASS_ACTIVE); + this.viewed = false; + this.index = index; + this.image = null; + this.$canvas.html($image.addClass(CLASS_INVISIBLE)); + + // Center current item + this.renderList(); + + var $title = this.$title; + + // Clear title + + $title.empty(); + + // Generate title after viewed + this.$element.one(EVENT_VIEWED, function () { + var _image = _this3.image, + naturalWidth = _image.naturalWidth, + naturalHeight = _image.naturalHeight; + + + $title.html(alt + ' (' + naturalWidth + ' × ' + naturalHeight + ')'); + }); + + if ($image[0].complete) { + this.load(); + } else { + $image.one(EVENT_LOAD, $.proxy(this.load, this)); + + if (this.timeout) { + clearTimeout(this.timeout); + } + + // Make the image visible if it fails to load within 1s + this.timeout = setTimeout(function () { + $image.removeClass(CLASS_INVISIBLE); + _this3.timeout = false; + }, 1000); + } + }, + + + /** + * View the previous image. + */ + prev: function prev() { + this.view(Math.max(this.index - 1, 0)); + }, + + + /** + * View the next image. + */ + next: function next() { + this.view(Math.min(this.index + 1, this.length - 1)); + }, + + + /** + * Move the image with relative offsets. + * @param {number} offsetX - The relative offset distance on the x-axis. + * @param {number} offsetY - The relative offset distance on the y-axis. + */ + move: function move(offsetX, offsetY) { + var _image2 = this.image, + left = _image2.left, + top = _image2.top; + + + this.moveTo(isUndefined(offsetX) ? offsetX : left + Number(offsetX), isUndefined(offsetY) ? offsetY : top + Number(offsetY)); + }, + + + /** + * Move the image to an absolute point. + * @param {number} x - The x-axis coordinate. + * @param {number} [y=x] - The y-axis coordinate. + */ + moveTo: function moveTo(x) { + var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : x; + + if (!this.viewed || this.played || !this.options.movable) { + return; + } + + var image = this.image; + + var changed = false; + + x = Number(x); + y = Number(y); + + if (isNumber(x)) { + image.left = x; + changed = true; + } + + if (isNumber(y)) { + image.top = y; + changed = true; + } + + if (changed) { + this.renderImage(); + } + }, + + + /** + * Zoom the image with a relative ratio. + * @param {number} ratio - The target ratio. + * @param {boolean} [hasTooltip] - Indicates if it has a tooltip or not. + * @param {Event} [_event] - The related event if any. + */ + zoom: function zoom(ratio) { + var hasTooltip = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + + var _event = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; + + var image = this.image; + + + ratio = Number(ratio); + + if (ratio < 0) { + ratio = 1 / (1 - ratio); + } else { + ratio = 1 + ratio; + } + + this.zoomTo(image.width * ratio / image.naturalWidth, hasTooltip, _event); + }, + + + /** + * Zoom the image to an absolute ratio. + * @param {number} ratio - The target ratio. + * @param {boolean} [hasTooltip] - Indicates if it has a tooltip or not. + * @param {Event} [_event] - The related event if any. + * @param {Event} [_zoomable] - Indicates if the current zoom is available or not. + */ + zoomTo: function zoomTo(ratio) { + var hasTooltip = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + + var _event = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; + + var _zoomable = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; + + var options = this.options, + image = this.image, + pointers = this.pointers; + + + ratio = Math.max(0, ratio); + + if (isNumber(ratio) && this.viewed && !this.played && (_zoomable || options.zoomable)) { + if (!_zoomable) { + var minZoomRatio = Math.max(0.01, options.minZoomRatio); + var maxZoomRatio = Math.min(100, options.maxZoomRatio); + + ratio = Math.min(Math.max(ratio, minZoomRatio), maxZoomRatio); + } + + if (_event && ratio > 0.95 && ratio < 1.05) { + ratio = 1; + } + + var newWidth = image.naturalWidth * ratio; + var newHeight = image.naturalHeight * ratio; + + if (_event && _event.originalEvent) { + var offset = this.$viewer.offset(); + var center = pointers && objectKeys(pointers).length > 0 ? getPointersCenter(pointers) : { + pageX: _event.pageX || _event.originalEvent.pageX || 0, + pageY: _event.pageY || _event.originalEvent.pageY || 0 + }; + + // Zoom from the triggering point of the event + image.left -= (newWidth - image.width) * ((center.pageX - offset.left - image.left) / image.width); + image.top -= (newHeight - image.height) * ((center.pageY - offset.top - image.top) / image.height); + } else { + // Zoom from the center of the image + image.left -= (newWidth - image.width) / 2; + image.top -= (newHeight - image.height) / 2; + } + + image.width = newWidth; + image.height = newHeight; + image.ratio = ratio; + this.renderImage(); + + if (hasTooltip) { + this.tooltip(); + } + } + }, + + + /** + * Rotate the image with a relative degree. + * @param {number} degree - The rotate degree. + */ + rotate: function rotate(degree) { + this.rotateTo((this.image.rotate || 0) + Number(degree)); + }, + + + /** + * Rotate the image to an absolute degree. + * @param {number} degree - The rotate degree. + */ + rotateTo: function rotateTo(degree) { + var image = this.image; + + + degree = Number(degree); + + if (isNumber(degree) && this.viewed && !this.played && this.options.rotatable) { + image.rotate = degree; + this.renderImage(); + } + }, + + + /** + * Scale the image on the x-axis. + * @param {number} scaleX - The scale ratio on the x-axis. + */ + scaleX: function scaleX(_scaleX) { + this.scale(_scaleX, this.image.scaleY); + }, + + + /** + * Scale the image on the y-axis. + * @param {number} scaleY - The scale ratio on the y-axis. + */ + scaleY: function scaleY(_scaleY) { + this.scale(this.image.scaleX, _scaleY); + }, + + + /** + * Scale the image. + * @param {number} scaleX - The scale ratio on the x-axis. + * @param {number} [scaleY=scaleX] - The scale ratio on the y-axis. + */ + scale: function scale(scaleX) { + var scaleY = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : scaleX; + + if (!this.viewed || this.played || !this.options.scalable) { + return; + } + + var image = this.image; + + var changed = false; + + scaleX = Number(scaleX); + scaleY = Number(scaleY); + + if (isNumber(scaleX)) { + image.scaleX = scaleX; + changed = true; + } + + if (isNumber(scaleY)) { + image.scaleY = scaleY; + changed = true; + } + + if (changed) { + this.renderImage(); + } + }, + + + /** + * Play the images. + */ + play: function play() { + var _this4 = this; + + if (!this.visible || this.played) { + return; + } + + var options = this.options, + $items = this.$items, + $player = this.$player; + + + if (options.fullscreen) { + this.requestFullscreen(); + } + + this.played = true; + $player.addClass(CLASS_SHOW); + + var list = []; + var index = 0; + + $items.each(function (i, item) { + var $item = $(item); + var $img = $item.find('img'); + var $image = $('' + $img.attr('alt') + ''); + + $image.addClass(CLASS_FADE).toggleClass(CLASS_TRANSITION, options.transition); + + if ($item.hasClass(CLASS_ACTIVE)) { + $image.addClass(CLASS_IN); + index = i; + } + + list.push($image); + $image.one(EVENT_LOAD, { + filled: false + }, $.proxy(_this4.loadImage, _this4)); + $player.append($image); + }); + + if (isNumber(options.interval) && options.interval > 0) { + var length = $items.length; + + var playing = function playing() { + _this4.playing = setTimeout(function () { + list[index].removeClass(CLASS_IN); + index += 1; + index = index < length ? index : 0; + list[index].addClass(CLASS_IN); + playing(); + }, options.interval); + }; + + if (length > 1) { + playing(); + } + } + }, + + + /** + * Stop play. + */ + stop: function stop() { + if (!this.played) { + return; + } + + if (this.options.fullscreen) { + this.exitFullscreen(); + } + + this.played = false; + clearTimeout(this.playing); + this.$player.removeClass(CLASS_SHOW).empty(); + }, + + + /** + * Enter modal mode (only available in inline mode). + */ + full: function full() { + var _this5 = this; + + var options = this.options, + $image = this.$image, + $list = this.$list; + + + if (!this.visible || this.played || this.fulled || !options.inline) { + return; + } + + this.fulled = true; + this.$body.addClass(CLASS_OPEN); + this.$button.addClass(CLASS_FULLSCREEN_EXIT); + + if (options.transition) { + $image.removeClass(CLASS_TRANSITION); + $list.removeClass(CLASS_TRANSITION); + } + + this.$viewer.addClass(CLASS_FIXED).removeAttr('style').css('z-index', options.zIndex); + this.initContainer(); + this.viewer = $.extend({}, this.container); + this.renderList(); + this.initImage(function () { + _this5.renderImage(function () { + if (options.transition) { + setTimeout(function () { + $image.addClass(CLASS_TRANSITION); + $list.addClass(CLASS_TRANSITION); + }, 0); + } + }); + }); + }, + + + /** + * Exit modal mode (only available in inline mode). + */ + exit: function exit() { + var _this6 = this; + + if (!this.fulled) { + return; + } + + var options = this.options, + $image = this.$image, + $list = this.$list; + + + this.fulled = false; + this.$body.removeClass(CLASS_OPEN); + this.$button.removeClass(CLASS_FULLSCREEN_EXIT); + + if (options.transition) { + $image.removeClass(CLASS_TRANSITION); + $list.removeClass(CLASS_TRANSITION); + } + + this.$viewer.removeClass(CLASS_FIXED).css('z-index', options.zIndexInline); + this.viewer = $.extend({}, this.parent); + this.renderViewer(); + this.renderList(); + this.initImage(function () { + _this6.renderImage(function () { + if (options.transition) { + setTimeout(function () { + $image.addClass(CLASS_TRANSITION); + $list.addClass(CLASS_TRANSITION); + }, 0); + } + }); + }); + }, + + + /** + * Show the current ratio of the image with percentage. + */ + tooltip: function tooltip() { + var _this7 = this; + + var options = this.options, + $tooltip = this.$tooltip, + image = this.image; + + var classes = [CLASS_SHOW, CLASS_FADE, CLASS_TRANSITION].join(' '); + + if (!this.viewed || this.played || !options.tooltip) { + return; + } + + $tooltip.text(Math.round(image.ratio * 100) + '%'); + + if (!this.tooltiping) { + if (options.transition) { + if (this.fading) { + $tooltip.trigger(EVENT_TRANSITIONEND); + } + + $tooltip.addClass(classes); + + // Force reflow to enable CSS3 transition + // eslint-disable-next-line + $tooltip[0].offsetWidth; + $tooltip.addClass(CLASS_IN); + } else { + $tooltip.addClass(CLASS_SHOW); + } + } else { + clearTimeout(this.tooltiping); + } + + this.tooltiping = setTimeout(function () { + if (options.transition) { + $tooltip.one(EVENT_TRANSITIONEND, function () { + $tooltip.removeClass(classes); + _this7.fading = false; + }).removeClass(CLASS_IN); + + _this7.fading = true; + } else { + $tooltip.removeClass(CLASS_SHOW); + } + + _this7.tooltiping = false; + }, 1000); + }, + + + /** + * Toggle the image size between its natural size and initial size. + */ + toggle: function toggle() { + if (this.image.ratio === 1) { + this.zoomTo(this.initialImage.ratio, true); + } else { + this.zoomTo(1, true); + } + }, + + + /** + * Reset the image to its initial state. + */ + reset: function reset() { + if (this.viewed && !this.played) { + this.image = $.extend({}, this.initialImage); + this.renderImage(); + } + }, + + + /** + * Update viewer when images changed. + */ + update: function update() { + var $element = this.$element; + var $images = this.$images; + + + if (this.isImg) { + // Destroy viewer if the target image was deleted + if (!$element.parent().length) { + this.destroy(); + return; + } + } else { + $images = $element.find('img'); + this.$images = $images; + this.length = $images.length; + } + + if (this.ready) { + var indexes = []; + var index = void 0; + + $.each(this.$items, function (i, item) { + var img = $(item).find('img')[0]; + var image = $images[i]; + + if (image) { + if (image.src !== img.src) { + indexes.push(i); + } + } else { + indexes.push(i); + } + }); + + this.$list.width('auto'); + this.initList(); + + if (this.visible) { + if (this.length) { + if (this.viewed) { + index = $.inArray(this.index, indexes); + + if (index >= 0) { + this.viewed = false; + this.view(Math.max(this.index - (index + 1), 0)); + } else { + this.$items.eq(this.index).addClass(CLASS_ACTIVE); + } + } + } else { + this.$image = null; + this.viewed = false; + this.index = 0; + this.image = null; + this.$canvas.empty(); + this.$title.empty(); + } + } + } + }, + + + /** + * Destroy the viewer instance. + */ + destroy: function destroy() { + var $element = this.$element; + + + if (this.options.inline) { + this.unbind(); + } else { + if (this.visible) { + this.unbind(); + } + + $element.off(EVENT_CLICK, this.start); + } + + this.unbuild(); + $element.removeData(NAMESPACE); + } +}; + +var _window$1 = window; +var document$1 = _window$1.document; + + +var others = { + // A shortcut for triggering custom events + trigger: function trigger(type, data) { + var e = $.Event(type, data); + + this.$element.trigger(e); + + return e; + }, + shown: function shown() { + var options = this.options; + + + this.transitioning = false; + this.fulled = true; + this.visible = true; + this.render(); + this.bind(); + + if ($.isFunction(options.shown)) { + this.$element.one(EVENT_SHOWN, options.shown); + } + + this.trigger(EVENT_SHOWN); + }, + hidden: function hidden() { + var options = this.options; + + + this.transitioning = false; + this.viewed = false; + this.fulled = false; + this.visible = false; + this.unbind(); + this.$body.removeClass(CLASS_OPEN); + this.$viewer.addClass(CLASS_HIDE); + this.resetList(); + this.resetImage(); + + if ($.isFunction(options.hidden)) { + this.$element.one(EVENT_HIDDEN, options.hidden); + } + + this.trigger(EVENT_HIDDEN); + }, + requestFullscreen: function requestFullscreen() { + if (this.fulled && !document$1.fullscreenElement && !document$1.mozFullScreenElement && !document$1.webkitFullscreenElement && !document$1.msFullscreenElement) { + var documentElement = document$1.documentElement; + + + if (documentElement.requestFullscreen) { + documentElement.requestFullscreen(); + } else if (documentElement.msRequestFullscreen) { + documentElement.msRequestFullscreen(); + } else if (documentElement.mozRequestFullScreen) { + documentElement.mozRequestFullScreen(); + } else if (documentElement.webkitRequestFullscreen) { + documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + } + } + }, + exitFullscreen: function exitFullscreen() { + if (this.fulled) { + if (document$1.exitFullscreen) { + document$1.exitFullscreen(); + } else if (document$1.msExitFullscreen) { + document$1.msExitFullscreen(); + } else if (document$1.mozCancelFullScreen) { + document$1.mozCancelFullScreen(); + } else if (document$1.webkitExitFullscreen) { + document$1.webkitExitFullscreen(); + } + } + }, + change: function change(event) { + var pointers = this.pointers; + + var pointer = pointers[Object.keys(pointers)[0]]; + var offsetX = pointer.endX - pointer.startX; + var offsetY = pointer.endY - pointer.startY; + + switch (this.action) { + // Move the current image + case ACTION_MOVE: + this.move(offsetX, offsetY); + break; + + // Zoom the current image + case ACTION_ZOOM: + this.zoom(getMaxZoomRatio(pointers), false, event); + + this.startX2 = this.endX2; + this.startY2 = this.endY2; + break; + + case ACTION_SWITCH: + this.action = 'switched'; + + if (Math.abs(offsetX) > Math.abs(offsetY)) { + if (offsetX > 1) { + this.prev(); + } else if (offsetX < -1) { + this.next(); + } + } + + break; + + default: + } + + // Override + $.each(pointers, function (i, p) { + p.startX = p.endX; + p.startY = p.endY; + }); + }, + isSwitchable: function isSwitchable() { + var image = this.image, + viewer = this.viewer; + + + return image.left >= 0 && image.top >= 0 && image.width <= viewer.width && image.height <= viewer.height; + } +}; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Viewer = function () { + /** + * Start the new Viewer. + * @param {Element} element - The target element for viewing. + * @param {Object} [options={}] - The configuration options. + */ + function Viewer(element) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + _classCallCheck(this, Viewer); + + if (!element || element.nodeType !== 1) { + throw new Error('The first argument is required and must be an element.'); + } + + this.element = element; + this.$element = $(element); + this.options = $.extend({}, DEFAULTS, $.isPlainObject(options) && options); + this.action = ''; + this.target = null; + this.timeout = null; + this.index = 0; + this.length = 0; + this.ready = false; + this.fading = false; + this.fulled = false; + this.isImg = false; + this.played = false; + this.playing = false; + this.tooltiping = false; + this.transitioning = false; + this.viewed = false; + this.visible = false; + this.wheeling = false; + this.pointers = {}; + this.init(); + } + + _createClass(Viewer, [{ + key: 'init', + value: function init() { + var _this = this; + + var $element = this.$element, + options = this.options; + + var isImg = $element.is('img'); + var $images = isImg ? $element : $element.find('img'); + var length = $images.length; + + + if (!length) { + return; + } + + // Override `transition` option if it is not supported + if (typeof document.createElement(NAMESPACE).style.transition === 'undefined') { + options.transition = false; + } + + this.isImg = isImg; + this.length = length; + this.count = 0; + this.$images = $images; + this.$body = $('body'); + + if (options.inline) { + $element.one(EVENT_READY, function () { + _this.view(); + }); + + $images.each(function (i, image) { + if (image.complete) { + _this.progress(); + } else { + $(image).one(EVENT_LOAD, $.proxy(_this.progress, _this)); + } + }); + } else { + $element.on(EVENT_CLICK, $.proxy(this.start, this)); + } + } + }, { + key: 'progress', + value: function progress() { + this.count += 1; + + if (this.count === this.length) { + this.build(); + } + } + }, { + key: 'build', + value: function build() { + var $element = this.$element, + options = this.options; + + + if (this.ready) { + return; + } + + var $parent = $element.parent(); + var $viewer = $(TEMPLATE); + var $button = $viewer.find('.' + NAMESPACE + '-button'); + var $navbar = $viewer.find('.' + NAMESPACE + '-navbar'); + var $title = $viewer.find('.' + NAMESPACE + '-title'); + var $toolbar = $viewer.find('.' + NAMESPACE + '-toolbar'); + + this.$parent = $parent; + this.$viewer = $viewer; + this.$button = $button; + this.$navbar = $navbar; + this.$title = $title; + this.$toolbar = $toolbar; + this.$canvas = $viewer.find('.' + NAMESPACE + '-canvas'); + this.$footer = $viewer.find('.' + NAMESPACE + '-footer'); + this.$list = $viewer.find('.' + NAMESPACE + '-list'); + this.$player = $viewer.find('.' + NAMESPACE + '-player'); + this.$tooltip = $viewer.find('.' + NAMESPACE + '-tooltip'); + + $title.addClass(!options.title ? CLASS_HIDE : getResponsiveClass(options.title)); + $toolbar.addClass(!options.toolbar ? CLASS_HIDE : getResponsiveClass(options.toolbar)); + $toolbar.find('li[class*=zoom]').toggleClass(CLASS_INVISIBLE, !options.zoomable); + $toolbar.find('li[class*=flip]').toggleClass(CLASS_INVISIBLE, !options.scalable); + + if (!options.rotatable) { + $toolbar.find('li[class*=rotate]').addClass(CLASS_INVISIBLE).appendTo($toolbar); + } + + $navbar.addClass(!options.navbar ? CLASS_HIDE : getResponsiveClass(options.navbar)); + $button.toggleClass(CLASS_HIDE, !options.button); + + if (options.inline) { + $button.addClass(CLASS_FULLSCREEN); + $viewer.css('z-index', options.zIndexInline); + + if ($parent.css('position') === 'static') { + $parent.css('position', 'relative'); + } + + $element.after($viewer); + } else { + $button.addClass(CLASS_CLOSE); + $viewer.css('z-index', options.zIndex).addClass([CLASS_FIXED, CLASS_FADE, CLASS_HIDE].join(' ')).appendTo('body'); + } + + if (options.inline) { + this.render(); + this.bind(); + this.visible = true; + } + + this.ready = true; + + if ($.isFunction(options.ready)) { + $element.one(EVENT_READY, options.ready); + } + + this.trigger(EVENT_READY); + } + }, { + key: 'unbuild', + value: function unbuild() { + if (!this.ready) { + return; + } + + this.ready = false; + this.$viewer.remove(); + } + + /** + * Change the default options. + * @static + * @param {Object} options - The new default options. + */ + + }], [{ + key: 'setDefaults', + value: function setDefaults(options) { + $.extend(DEFAULTS, options); + } + }]); + + return Viewer; +}(); + +$.extend(Viewer.prototype, render, events, handlers, methods, others); + +var AnotherViewer = $.fn.viewer; + +$.fn.viewer = function jQueryViewer(option) { + for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + var result = void 0; + + this.each(function (i, element) { + var $element = $(element); + var data = $element.data(NAMESPACE); + + if (!data) { + if (/destroy/.test(option)) { + return; + } + + var options = $.extend({}, $element.data(), $.isPlainObject(option) && option); + + data = new Viewer(element, options); + $element.data(NAMESPACE, data); + } + + if (isString(option)) { + var fn = data[option]; + + if ($.isFunction(fn)) { + result = fn.apply(data, args); + } + } + }); + + return isUndefined(result) ? this : result; +}; + +$.fn.viewer.Constructor = Viewer; +$.fn.viewer.setDefaults = Viewer.setDefaults; +$.fn.viewer.noConflict = function noConflict() { + $.fn.viewer = AnotherViewer; + return this; +}; + +}))); diff --git a/public/static/libs/viewer/viewer.min.css b/public/static/libs/viewer/viewer.min.css new file mode 100644 index 0000000..72d5968 --- /dev/null +++ b/public/static/libs/viewer/viewer.min.css @@ -0,0 +1,10 @@ +/*! + * Viewer v0.6.0 + * https://github.com/fengyuanchen/viewer + * + * Copyright (c) 2014-2017 Fengyuan Chen + * Released under the MIT license + * + * Date: 2017-10-07T09:53:32.834Z + */.viewer-close:before,.viewer-flip-horizontal:before,.viewer-flip-vertical:before,.viewer-fullscreen-exit:before,.viewer-fullscreen:before,.viewer-next:before,.viewer-one-to-one:before,.viewer-play:before,.viewer-prev:before,.viewer-reset:before,.viewer-rotate-left:before,.viewer-rotate-right:before,.viewer-zoom-in:before,.viewer-zoom-out:before{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARgAAAAUCAYAAABWOyJDAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNui8sowAAAQPSURBVHic7Zs/iFxVFMa/0U2UaJGksUgnIVhYxVhpjDbZCBmLdAYECxsRFBTUamcXUiSNncgKQbSxsxH8gzAP3FU2jY0kKKJNiiiIghFlccnP4p3nPCdv3p9778vsLOcHB2bfveeb7955c3jvvNkBIMdxnD64a94GHMfZu3iBcRynN7zAOI7TG15gHCeeNUkr8zaxG2lbYDYsdgMbktBsP03jdQwljSXdtBhLOmtjowC9Mg9L+knSlcD8TNKpSA9lBpK2JF2VdDSR5n5J64m0qli399hNFMUlpshQii5jbXTbHGviB0nLNeNDSd9VO4A2UdB2fp+x0eCnaXxWXGA2X0au/3HgN9P4LFCjIANOJdrLr0zzZ+BEpNYDwKbpnQMeAw4m8HjQtM6Z9qa917zPQwFr3M5KgA6J5rTJCdFZJj9/lyvGhsDvwFNVuV2MhhjrK6b9bFiE+j1r87eBl4HDwCF7/U/k+ofAX5b/EXBv5JoLMuILzf3Ap6Z3EzgdqHMCuF7hcQf4HDgeoHnccncqdK/TvSDWffFXI/exICY/xZyqc6XLWF1UFZna4gJ7q8BsRvgd2/xXpo6P+D9dfT7PpECtA3cnWPM0GXGFZh/wgWltA+cDNC7X+AP4GzjZQe+k5dRxuYPeiuXU7e1qwLpDz7dFjXKRaSwuMLvAlG8zZlG+YmiK1HoFqT7wP2z+4Q45TfEGcMt01xLoNZEBTwRqD4BLpnMLeC1A41UmVxsXgXeBayV/Wx20rpTyrpnWRft7p6O/FdqzGrDukPNtkaMoMo3FBdBSQMOnYBCReyf05s126fU9ytfX98+mY54Kxnp7S9K3kj6U9KYdG0h6UdLbkh7poFXMfUnSOyVvL0h6VtIXHbS6nOP+s/Zm9mvyXW1uuC9ohZ72E9uDmXWLJOB1GxsH+DxPftsB8B6wlGDN02TAkxG6+4D3TWsbeC5CS8CDFce+AW500LhhOW2020TRjK3b21HEmgti9m0RonxbdMZeVzV+/4tF3cBpP7E9mKHNL5q8h5g0eYsCMQz0epq8gQrwMXAgcs0FGXGFRcB9wCemF9PkbYqM/Bas7fxLwNeJPdTdpo4itQti8lPMqTpXuozVRVXPpbHI3KkNTB1NfkL81j2mvhDp91HgV9MKuRIqrykj3WPq4rHyL+axj8/qGPmTqi6F9YDlHOvJU6oYcTsh/TYSzWmTE6JT19CtLTJt32D6CmHe0eQn1O8z5AXgT4sx4Vcu0/EQecMydB8z0hUWkTd2t4CrwNEePqMBcAR4mrBbwyXLPWJa8zrXmmLEhNBmfpkuY2102xxrih+pb+ieAb6vGhuA97UcJ5KR8gZ77K+99xxeYBzH6Q3/Z0fHcXrDC4zjOL3hBcZxnN74F+zlvXFWXF9PAAAAAElFTkSuQmCC");background-repeat:no-repeat;color:transparent;display:block;font-size:0;height:20px;line-height:0;width:20px}.viewer-zoom-in:before{background-position:0 0;content:"Zoom In"}.viewer-zoom-out:before{background-position:-20px 0;content:"Zoom Out"}.viewer-one-to-one:before{background-position:-40px 0;content:"One to One"}.viewer-reset:before{background-position:-60px 0;content:"Reset"}.viewer-prev:before{background-position:-80px 0;content:"Previous"}.viewer-play:before{background-position:-100px 0;content:"Play"}.viewer-next:before{background-position:-120px 0;content:"Next"}.viewer-rotate-left:before{background-position:-140px 0;content:"Rotate Left"}.viewer-rotate-right:before{background-position:-160px 0;content:"Rotate Right"}.viewer-flip-horizontal:before{background-position:-180px 0;content:"Flip Horizontal"}.viewer-flip-vertical:before{background-position:-200px 0;content:"Flip Vertical"}.viewer-fullscreen:before{background-position:-220px 0;content:"Enter Full Screen"}.viewer-fullscreen-exit:before{background-position:-240px 0;content:"Exit Full Screen"}.viewer-close:before{background-position:-260px 0;content:"Close"}.viewer-container{background-color:rgba(0,0,0,.5);bottom:0;direction:ltr;font-size:0;left:0;line-height:0;overflow:hidden;position:absolute;right:0;-webkit-tap-highlight-color:transparent;top:0;-webkit-touch-callout:none;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.viewer-container::-moz-selection,.viewer-container ::-moz-selection{background-color:transparent}.viewer-container::selection,.viewer-container ::selection{background-color:transparent}.viewer-container img{display:block;height:auto;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.viewer-canvas{bottom:0;left:0;overflow:hidden;position:absolute;right:0;top:0}.viewer-canvas>img{height:auto;margin:15px auto;max-width:90%!important;width:auto}.viewer-footer{bottom:0;left:0;overflow:hidden;position:absolute;right:0;text-align:center}.viewer-navbar{background-color:rgba(0,0,0,.5);overflow:hidden}.viewer-list{box-sizing:content-box;height:50px;margin:0;overflow:hidden;padding:1px 0}.viewer-list>li{color:transparent;cursor:pointer;float:left;font-size:0;height:50px;line-height:0;opacity:.5;overflow:hidden;width:30px}.viewer-list>li+li{margin-left:1px}.viewer-list>.viewer-active{opacity:1}.viewer-player{background-color:#000;bottom:0;cursor:none;display:none;right:0}.viewer-player,.viewer-player>img{left:0;position:absolute;top:0}.viewer-toolbar{margin:0 auto 5px;overflow:hidden;padding:3px 0;width:280px}.viewer-toolbar>li{background-color:rgba(0,0,0,.5);border-radius:50%;cursor:pointer;float:left;height:24px;overflow:hidden;width:24px}.viewer-toolbar>li:hover{background-color:rgba(0,0,0,.8)}.viewer-toolbar>li:before{margin:2px}.viewer-toolbar>li+li{margin-left:1px}.viewer-toolbar>.viewer-play{height:30px;margin-bottom:-3px;margin-top:-3px;width:30px}.viewer-toolbar>.viewer-play:before{margin:5px}.viewer-tooltip{background-color:rgba(0,0,0,.8);border-radius:10px;color:#fff;display:none;font-size:12px;height:20px;left:50%;line-height:20px;margin-left:-25px;margin-top:-10px;position:absolute;text-align:center;top:50%;width:50px}.viewer-title{color:#ccc;display:inline-block;font-size:12px;line-height:1;margin:0 5% 5px;max-width:90%;opacity:.8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.viewer-title:hover{opacity:1}.viewer-button{background-color:rgba(0,0,0,.5);border-radius:50%;cursor:pointer;height:80px;overflow:hidden;position:absolute;right:-40px;top:-40px;width:80px}.viewer-button:before{bottom:15px;left:15px;position:absolute}.viewer-fixed{position:fixed}.viewer-open{overflow:hidden}.viewer-show{display:block}.viewer-hide{display:none}.viewer-invisible{visibility:hidden}.viewer-move{cursor:move;cursor:-webkit-grab;cursor:grab}.viewer-fade{opacity:0}.viewer-in{opacity:1}.viewer-transition{transition:all .3s}@media (max-width:767px){.viewer-hide-xs-down{display:none}}@media (max-width:991px){.viewer-hide-sm-down{display:none}}@media (max-width:1199px){.viewer-hide-md-down{display:none}} +/*# sourceMappingURL=viewer.min.css.map */ \ No newline at end of file diff --git a/public/static/libs/viewer/viewer.min.js b/public/static/libs/viewer/viewer.min.js new file mode 100644 index 0000000..af0f8ec --- /dev/null +++ b/public/static/libs/viewer/viewer.min.js @@ -0,0 +1,10 @@ +/*! + * Viewer v0.6.0 + * https://github.com/fengyuanchen/viewer + * + * Copyright (c) 2014-2017 Fengyuan Chen + * Released under the MIT license + * + * Date: 2017-10-07T09:53:36.889Z + */ +!function(i,e){"object"==typeof exports&&"undefined"!=typeof module?e(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],e):e(i.jQuery)}(this,function(i){"use strict";function e(i){return"string"==typeof i}function t(i){return"number"==typeof i&&!C(i)}function s(i){return void 0===i}function n(i,e){for(var t=arguments.length,s=Array(t>2?t-2:0),n=2;n0?a.join(" "):"none"}function o(i){return e(i)?i.replace(/^.*\//,"").replace(/[?&#].*$/,""):""}function r(i,e){if(i.naturalWidth)e(i.naturalWidth,i.naturalHeight);else{var t=document.createElement("img");t.onload=function(){e(t.width,t.height)},t.src=i.src}}function h(i){switch(i){case 2:return g;case 3:return w;case 4:return m;default:return""}}function l(e){var t=i.extend({},e),s=[];return i.each(e,function(e,n){delete t[e],i.each(t,function(i,e){var t=Math.abs(n.startX-e.startX),a=Math.abs(n.startY-e.startY),o=Math.abs(n.endX-e.endX),r=Math.abs(n.endY-e.endY),h=Math.sqrt(t*t+a*a),l=(Math.sqrt(o*o+r*r)-h)/h;s.push(l)})}),s.sort(function(i,e){return Math.abs(i)'+r+''))}),n.html(a.join("")).find("img").one("load",{filled:!0},i.proxy(this.loadImage,this)),this.$items=n.children(),s.transition&&t.one("viewed",function(){n.addClass(b)})},renderList:function(i){var e=i||this.index,t=this.$items.eq(e).width(),s=t+1;this.$list.css({width:s*this.length,marginLeft:(this.viewer.width-t)/2-s*e})},resetList:function(){this.$list.empty().removeClass(b).css("margin-left",0)},initImage:function(e){var t=this,s=this.options,n=this.$image,a=this.viewer,o=this.$footer.height(),h=a.width,l=Math.max(a.height-o,o),d=this.image||{};r(n[0],function(n,a){var o=n/a,r=h,c=l;l*o>h?c=h/o:r=l*o;var u={naturalWidth:n,naturalHeight:a,aspectRatio:o,ratio:(r=Math.min(.9*r,n))/n,width:r,height:c=Math.min(.9*c,a),left:(h-r)/2,top:(l-c)/2},v=i.extend({},u);s.rotatable&&(u.rotate=d.rotate||0,v.rotate=0),s.scalable&&(u.scaleX=d.scaleX||1,u.scaleY=d.scaleY||1,v.scaleX=1,v.scaleY=1),t.image=u,t.initialImage=v,i.isFunction(e)&&e()})},renderImage:function(e){var t=this.image,s=this.$image;s.css({width:t.width,height:t.height,marginLeft:t.left,marginTop:t.top,transform:a(t)}),i.isFunction(e)&&(this.transitioning?s.one("transitionend",e):e())},resetImage:function(){this.$image&&(this.$image.remove(),this.$image=null)}},I={bind:function(){var e=this.$element,t=this.options;i.isFunction(t.view)&&e.on("view",t.view),i.isFunction(t.viewed)&&e.on("viewed",t.viewed),this.$viewer.on("click",i.proxy(this.click,this)).on("wheel mousewheel DOMMouseScroll",i.proxy(this.wheel,this)).on("dragstart",i.proxy(this.dragstart,this)),this.$canvas.on(y,i.proxy(this.pointerdown,this)),i(document).on(x,this.onPointerMove=n(this.pointermove,this)).on($,this.onPointerUp=n(this.pointerup,this)).on("keydown",this.onKeyDown=n(this.keydown,this)),i(window).on("resize",this.onResize=n(this.resize,this))},unbind:function(){var e=this.$element,t=this.options;i.isFunction(t.view)&&e.off("view",t.view),i.isFunction(t.viewed)&&e.off("viewed",t.viewed),this.$viewer.off("click",this.click).off("wheel mousewheel DOMMouseScroll",this.wheel).off("dragstart",this.dragstart),this.$canvas.off(y,this.pointerdown),i(document).off(x,this.onPointerMove).off($,this.onPointerUp).off("keydown",this.onKeyDown),i(window).off("resize",this.onResize)}},F={click:function(e){var t=i(e.target),s=t.data("action"),n=this.image;switch(s){case"mix":this.played?this.stop():this.options.inline?this.fulled?this.exit():this.full():this.hide();break;case"view":this.view(t.data("index"));break;case"zoom-in":this.zoom(.1,!0);break;case"zoom-out":this.zoom(-.1,!0);break;case"one-to-one":this.toggle();break;case"reset":this.reset();break;case"prev":this.prev();break;case"play":this.play();break;case"next":this.next();break;case"rotate-left":this.rotate(-90);break;case"rotate-right":this.rotate(90);break;case"flip-horizontal":this.scaleX(-n.scaleX||-1);break;case"flip-vertical":this.scaleY(-n.scaleY||-1);break;default:this.played&&this.stop()}},dragstart:function(e){i(e.target).is("img")&&e.preventDefault()},keydown:function(i){var e=this.options;if(this.fulled&&e.keyboard)switch(i.which){case 27:this.played?this.stop():e.inline?this.fulled&&this.exit():this.hide();break;case 32:this.played&&this.stop();break;case 37:this.prev();break;case 38:i.preventDefault(),this.zoom(e.zoomRatio,!0);break;case 39:this.next();break;case 40:i.preventDefault(),this.zoom(-e.zoomRatio,!0);break;case 48:case 49:(i.ctrlKey||i.shiftKey)&&(i.preventDefault(),this.toggle())}},load:function(){var i=this,e=this.options,t=this.viewer,s=this.$image;this.timeout&&(clearTimeout(this.timeout),this.timeout=!1),s&&(s.removeClass("viewer-invisible").css("cssText","width:0;height:0;margin-left:"+t.width/2+"px;margin-top:"+t.height/2+"px;max-width:none!important;visibility:visible;"),this.initImage(function(){s.toggleClass(b,e.transition).toggleClass("viewer-move",e.movable),i.renderImage(function(){i.viewed=!0,i.trigger("viewed")})}))},loadImage:function(e){var t=e.target,s=i(t),n=s.parent(),a=n.width(),o=n.height(),h=e.data&&e.data.filled;r(t,function(i,e){var t=i/e,n=a,r=o;o*t>a?h?n=o*t:r=a/t:h?r=a/t:n=o*t,s.css({width:n,height:r,marginLeft:(a-n)/2,marginTop:(o-r)/2})})},pointerdown:function(e){if(this.viewed&&!this.transitioning){var t=this.options,s=this.pointers,n=e.originalEvent;n&&n.changedTouches?i.each(n.changedTouches,function(i,e){s[e.identifier]=d(e)}):s[n&&n.pointerId||0]=d(n||e);var a=!!t.movable&&"move";z(s).length>1?a="zoom":"touch"!==e.pointerType&&"touchmove"!==e.type||!this.isSwitchable()||(a="switch"),this.action=a}},pointermove:function(e){var t=this.$image,s=this.action,n=this.pointers;if(this.viewed&&s){e.preventDefault();var a=e.originalEvent;a&&a.changedTouches?i.each(a.changedTouches,function(e,t){i.extend(n[t.identifier],d(t,!0))}):i.extend(n[a&&a.pointerId||0],d(e,!0)),"move"===s&&this.options.transition&&t.hasClass(b)&&t.removeClass(b),this.change(e)}},pointerup:function(e){if(this.viewed){var t=this.action,s=this.pointers,n=e.originalEvent;n&&n.changedTouches?i.each(n.changedTouches,function(i,e){delete s[e.identifier]}):delete s[n&&n.pointerId||0],t&&("move"===t&&this.options.transition&&this.$image.addClass(b),this.action=!1)}},resize:function(){var e=this;if(this.initContainer(),this.initViewer(),this.renderViewer(),this.renderList(),this.viewed&&this.initImage(function(){e.renderImage()}),this.played){if(this.options.fullscreen&&this.fulled&&!document.fullscreenElement&&!document.mozFullScreenElement&&!document.webkitFullscreenElement&&!document.msFullscreenElement)return void this.stop();this.$player.find("img").one("load",i.proxy(this.loadImage,this)).trigger("load")}},start:function(e){var t=e.target;i(t).is("img")&&(this.target=t,this.show())},wheel:function(i){var e=this;if(this.viewed&&(i.preventDefault(),!this.wheeling)){this.wheeling=!0,setTimeout(function(){e.wheeling=!1},50);var t=i.originalEvent||i,s=1;t.deltaY?s=t.deltaY>0?1:-1:t.wheelDelta?s=-t.wheelDelta/120:t.detail&&(s=t.detail>0?1:-1),this.zoom(-s*(Number(this.options.zoomRatio)||.1),!0,i)}}},T={show:function(){var e=this,t=this.$element,s=this.options;if(!s.inline&&!this.transitioning){this.ready||this.build();var n=this.$viewer;i.isFunction(s.show)&&t.one("show",s.show),this.trigger("show").isDefaultPrevented()||(this.$body.addClass("viewer-open"),n.removeClass("viewer-hide"),t.one("shown",function(){e.view(e.target?e.$images.index(e.target):e.index),e.target=!1}),s.transition?(this.transitioning=!0,n.addClass(b),n[0].offsetWidth,n.one("transitionend",i.proxy(this.shown,this)).addClass(p)):(n.addClass(p),this.shown()))}},hide:function(){var e=this,t=this.options,s=this.$viewer;t.inline||this.transitioning||!this.visible||(i.isFunction(t.hide)&&this.$element.one("hide",t.hide),this.trigger("hide").isDefaultPrevented()||(this.viewed&&t.transition?(this.transitioning=!0,this.$image.one("transitionend",function(){s.one("transitionend",i.proxy(e.hidden,e)).removeClass(p)}),this.zoomTo(0,!1,!1,!0)):(s.removeClass(p),this.hidden())))},view:function(e){var t=this;if(e=Number(e)||0,!(!this.visible||this.played||e<0||e>=this.length||this.viewed&&e===this.index||this.trigger("view").isDefaultPrevented())){var s=this.$items.eq(e),n=s.find("img"),a=n.attr("alt"),o=i(''+a+'');this.$image=o,this.$items.eq(this.index).removeClass("viewer-active"),s.addClass("viewer-active"),this.viewed=!1,this.index=e,this.image=null,this.$canvas.html(o.addClass("viewer-invisible")),this.renderList();var r=this.$title;r.empty(),this.$element.one("viewed",function(){var i=t.image,e=i.naturalWidth,s=i.naturalHeight;r.html(a+" ("+e+" × "+s+")")}),o[0].complete?this.load():(o.one("load",i.proxy(this.load,this)),this.timeout&&clearTimeout(this.timeout),this.timeout=setTimeout(function(){o.removeClass("viewer-invisible"),t.timeout=!1},1e3))}},prev:function(){this.view(Math.max(this.index-1,0))},next:function(){this.view(Math.min(this.index+1,this.length-1))},move:function(i,e){var t=this.image,n=t.left,a=t.top;this.moveTo(s(i)?i:n+Number(i),s(e)?e:a+Number(e))},moveTo:function(i){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:i;if(this.viewed&&!this.played&&this.options.movable){var s=this.image,n=!1;i=Number(i),e=Number(e),t(i)&&(s.left=i,n=!0),t(e)&&(s.top=e,n=!0),n&&this.renderImage()}},zoom:function(i){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,s=this.image;i=(i=Number(i))<0?1/(1-i):1+i,this.zoomTo(s.width*i/s.naturalWidth,e,t)},zoomTo:function(i){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,n=arguments.length>3&&void 0!==arguments[3]&&arguments[3],a=this.options,o=this.image,r=this.pointers;if(i=Math.max(0,i),t(i)&&this.viewed&&!this.played&&(n||a.zoomable)){if(!n){var h=Math.max(.01,a.minZoomRatio),l=Math.min(100,a.maxZoomRatio);i=Math.min(Math.max(i,h),l)}s&&i>.95&&i<1.05&&(i=1);var d=o.naturalWidth*i,u=o.naturalHeight*i;if(s&&s.originalEvent){var v=this.$viewer.offset(),f=r&&z(r).length>0?c(r):{pageX:s.pageX||s.originalEvent.pageX||0,pageY:s.pageY||s.originalEvent.pageY||0};o.left-=(d-o.width)*((f.pageX-v.left-o.left)/o.width),o.top-=(u-o.height)*((f.pageY-v.top-o.top)/o.height)}else o.left-=(d-o.width)/2,o.top-=(u-o.height)/2;o.width=d,o.height=u,o.ratio=i,this.renderImage(),e&&this.tooltip()}},rotate:function(i){this.rotateTo((this.image.rotate||0)+Number(i))},rotateTo:function(i){var e=this.image;t(i=Number(i))&&this.viewed&&!this.played&&this.options.rotatable&&(e.rotate=i,this.renderImage())},scaleX:function(i){this.scale(i,this.image.scaleY)},scaleY:function(i){this.scale(this.image.scaleX,i)},scale:function(i){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:i;if(this.viewed&&!this.played&&this.options.scalable){var s=this.image,n=!1;i=Number(i),e=Number(e),t(i)&&(s.scaleX=i,n=!0),t(e)&&(s.scaleY=e,n=!0),n&&this.renderImage()}},play:function(){var e=this;if(this.visible&&!this.played){var s=this.options,n=this.$items,a=this.$player;s.fullscreen&&this.requestFullscreen(),this.played=!0,a.addClass("viewer-show");var o=[],r=0;if(n.each(function(t,n){var h=i(n),l=h.find("img"),d=i(''+l.attr(');d.addClass("viewer-fade").toggleClass(b,s.transition),h.hasClass("viewer-active")&&(d.addClass(p),r=t),o.push(d),d.one("load",{filled:!1},i.proxy(e.loadImage,e)),a.append(d)}),t(s.interval)&&s.interval>0){var h=n.length;h>1&&function i(){e.playing=setTimeout(function(){o[r].removeClass(p),o[r=(r+=1)=0?(this.viewed=!1,this.view(Math.max(this.index-(n+1),0))):this.$items.eq(this.index).addClass("viewer-active")):(this.$image=null,this.viewed=!1,this.index=0,this.image=null,this.$canvas.empty(),this.$title.empty()))}},destroy:function(){var i=this.$element;this.options.inline?this.unbind():(this.visible&&this.unbind(),i.off("click",this.start)),this.unbuild(),i.removeData("viewer")}},Y=window.document,M={trigger:function(e,t){var s=i.Event(e,t);return this.$element.trigger(s),s},shown:function(){var e=this.options;this.transitioning=!1,this.fulled=!0,this.visible=!0,this.render(),this.bind(),i.isFunction(e.shown)&&this.$element.one("shown",e.shown),this.trigger("shown")},hidden:function(){var e=this.options;this.transitioning=!1,this.viewed=!1,this.fulled=!1,this.visible=!1,this.unbind(),this.$body.removeClass("viewer-open"),this.$viewer.addClass("viewer-hide"),this.resetList(),this.resetImage(),i.isFunction(e.hidden)&&this.$element.one("hidden",e.hidden),this.trigger("hidden")},requestFullscreen:function(){if(this.fulled&&!Y.fullscreenElement&&!Y.mozFullScreenElement&&!Y.webkitFullscreenElement&&!Y.msFullscreenElement){var i=Y.documentElement;i.requestFullscreen?i.requestFullscreen():i.msRequestFullscreen?i.msRequestFullscreen():i.mozRequestFullScreen?i.mozRequestFullScreen():i.webkitRequestFullscreen&&i.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT)}},exitFullscreen:function(){this.fulled&&(Y.exitFullscreen?Y.exitFullscreen():Y.msExitFullscreen?Y.msExitFullscreen():Y.mozCancelFullScreen?Y.mozCancelFullScreen():Y.webkitExitFullscreen&&Y.webkitExitFullscreen())},change:function(e){var t=this.pointers,s=t[Object.keys(t)[0]],n=s.endX-s.startX,a=s.endY-s.startY;switch(this.action){case"move":this.move(n,a);break;case"zoom":this.zoom(l(t),!1,e),this.startX2=this.endX2,this.startY2=this.endY2;break;case"switch":this.action="switched",Math.abs(n)>Math.abs(a)&&(n>1?this.prev():n<-1&&this.next())}i.each(t,function(i,e){e.startX=e.endX,e.startY=e.endY})},isSwitchable:function(){var i=this.image,e=this.viewer;return i.left>=0&&i.top>=0&&i.width<=e.width&&i.height<=e.height}},X=function(){function i(i,e){for(var t=0;t1&&void 0!==arguments[1]?arguments[1]:{};if(u(this,e),!t||1!==t.nodeType)throw new Error("The first argument is required and must be an element.");this.element=t,this.$element=i(t),this.options=i.extend({},v,i.isPlainObject(s)&&s),this.action="",this.target=null,this.timeout=null,this.index=0,this.length=0,this.ready=!1,this.fading=!1,this.fulled=!1,this.isImg=!1,this.played=!1,this.playing=!1,this.tooltiping=!1,this.transitioning=!1,this.viewed=!1,this.visible=!1,this.wheeling=!1,this.pointers={},this.init()}return X(e,[{key:"init",value:function(){var e=this,t=this.$element,s=this.options,n=t.is("img"),a=n?t:t.find("img"),o=a.length;o&&(void 0===document.createElement("viewer").style.transition&&(s.transition=!1),this.isImg=n,this.length=o,this.count=0,this.$images=a,this.$body=i("body"),s.inline?(t.one("ready",function(){e.view()}),a.each(function(t,s){s.complete?e.progress():i(s).one("load",i.proxy(e.progress,e))})):t.on("click",i.proxy(this.start,this)))}},{key:"progress",value:function(){this.count+=1,this.count===this.length&&this.build()}},{key:"build",value:function(){var e=this.$element,t=this.options;if(!this.ready){var s=e.parent(),n=i('
              '),a=n.find(".viewer-button"),o=n.find(".viewer-navbar"),r=n.find(".viewer-title"),l=n.find(".viewer-toolbar");this.$parent=s,this.$viewer=n,this.$button=a,this.$navbar=o,this.$title=r,this.$toolbar=l,this.$canvas=n.find(".viewer-canvas"),this.$footer=n.find(".viewer-footer"),this.$list=n.find(".viewer-list"),this.$player=n.find(".viewer-player"),this.$tooltip=n.find(".viewer-tooltip"),r.addClass(t.title?h(t.title):"viewer-hide"),l.addClass(t.toolbar?h(t.toolbar):"viewer-hide"),l.find("li[class*=zoom]").toggleClass("viewer-invisible",!t.zoomable),l.find("li[class*=flip]").toggleClass("viewer-invisible",!t.scalable),t.rotatable||l.find("li[class*=rotate]").addClass("viewer-invisible").appendTo(l),o.addClass(t.navbar?h(t.navbar):"viewer-hide"),a.toggleClass("viewer-hide",!t.button),t.inline?(a.addClass("viewer-fullscreen"),n.css("z-index",t.zIndexInline),"static"===s.css("position")&&s.css("position","relative"),e.after(n)):(a.addClass("viewer-close"),n.css("z-index",t.zIndex).addClass(["viewer-fixed","viewer-fade","viewer-hide"].join(" ")).appendTo("body")),t.inline&&(this.render(),this.bind(),this.visible=!0),this.ready=!0,i.isFunction(t.ready)&&e.one("ready",t.ready),this.trigger("ready")}}},{key:"unbuild",value:function(){this.ready&&(this.ready=!1,this.$viewer.remove())}}],[{key:"setDefaults",value:function(e){i.extend(v,e)}}]),e}();i.extend(E.prototype,k,I,F,T,M);var q=i.fn.viewer;i.fn.viewer=function(t){for(var n=arguments.length,a=Array(n>1?n-1:0),o=1;o + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/static/libs/wang-editor/fonts/icomoon.ttf b/public/static/libs/wang-editor/fonts/icomoon.ttf new file mode 100644 index 0000000..b4ea1e1 Binary files /dev/null and b/public/static/libs/wang-editor/fonts/icomoon.ttf differ diff --git a/public/static/libs/wang-editor/fonts/icomoon.woff b/public/static/libs/wang-editor/fonts/icomoon.woff new file mode 100644 index 0000000..1b16ccb Binary files /dev/null and b/public/static/libs/wang-editor/fonts/icomoon.woff differ diff --git a/public/static/libs/wang-editor/js/lib/jquery-1.10.2.min.js b/public/static/libs/wang-editor/js/lib/jquery-1.10.2.min.js new file mode 100644 index 0000000..5448af7 --- /dev/null +++ b/public/static/libs/wang-editor/js/lib/jquery-1.10.2.min.js @@ -0,0 +1,14 @@ +/*! jQuery v1.10.2 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license +*/ +(function (e, t) { + var n, r, i = typeof t, o = e.location, a = e.document, s = a.documentElement, l = e.jQuery, u = e.$, c = {}, p = [], f = "1.10.2", d = p.concat, h = p.push, g = p.slice, m = p.indexOf, y = c.toString, v = c.hasOwnProperty, b = f.trim, x = function (e, t) { return new x.fn.init(e, t, r) }, w = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, T = /\S+/g, C = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, N = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, k = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, E = /^[\],:{}\s]*$/, S = /(?:^|:|,)(?:\s*\[)+/g, A = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, j = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g, D = /^-ms-/, L = /-([\da-z])/gi, H = function (e, t) { return t.toUpperCase() }, q = function (e) { (a.addEventListener || "load" === e.type || "complete" === a.readyState) && (_(), x.ready()) }, _ = function () { a.addEventListener ? (a.removeEventListener("DOMContentLoaded", q, !1), e.removeEventListener("load", q, !1)) : (a.detachEvent("onreadystatechange", q), e.detachEvent("onload", q)) }; x.fn = x.prototype = { jquery: f, constructor: x, init: function (e, n, r) { var i, o; if (!e) return this; if ("string" == typeof e) { if (i = "<" === e.charAt(0) && ">" === e.charAt(e.length - 1) && e.length >= 3 ? [null, e, null] : N.exec(e), !i || !i[1] && n) return !n || n.jquery ? (n || r).find(e) : this.constructor(n).find(e); if (i[1]) { if (n = n instanceof x ? n[0] : n, x.merge(this, x.parseHTML(i[1], n && n.nodeType ? n.ownerDocument || n : a, !0)), k.test(i[1]) && x.isPlainObject(n)) for (i in n) x.isFunction(this[i]) ? this[i](n[i]) : this.attr(i, n[i]); return this } if (o = a.getElementById(i[2]), o && o.parentNode) { if (o.id !== i[2]) return r.find(e); this.length = 1, this[0] = o } return this.context = a, this.selector = e, this } return e.nodeType ? (this.context = this[0] = e, this.length = 1, this) : x.isFunction(e) ? r.ready(e) : (e.selector !== t && (this.selector = e.selector, this.context = e.context), x.makeArray(e, this)) }, selector: "", length: 0, toArray: function () { return g.call(this) }, get: function (e) { return null == e ? this.toArray() : 0 > e ? this[this.length + e] : this[e] }, pushStack: function (e) { var t = x.merge(this.constructor(), e); return t.prevObject = this, t.context = this.context, t }, each: function (e, t) { return x.each(this, e, t) }, ready: function (e) { return x.ready.promise().done(e), this }, slice: function () { return this.pushStack(g.apply(this, arguments)) }, first: function () { return this.eq(0) }, last: function () { return this.eq(-1) }, eq: function (e) { var t = this.length, n = +e + (0 > e ? t : 0); return this.pushStack(n >= 0 && t > n ? [this[n]] : []) }, map: function (e) { return this.pushStack(x.map(this, function (t, n) { return e.call(t, n, t) })) }, end: function () { return this.prevObject || this.constructor(null) }, push: h, sort: [].sort, splice: [].splice }, x.fn.init.prototype = x.fn, x.extend = x.fn.extend = function () { var e, n, r, i, o, a, s = arguments[0] || {}, l = 1, u = arguments.length, c = !1; for ("boolean" == typeof s && (c = s, s = arguments[1] || {}, l = 2), "object" == typeof s || x.isFunction(s) || (s = {}), u === l && (s = this, --l) ; u > l; l++) if (null != (o = arguments[l])) for (i in o) e = s[i], r = o[i], s !== r && (c && r && (x.isPlainObject(r) || (n = x.isArray(r))) ? (n ? (n = !1, a = e && x.isArray(e) ? e : []) : a = e && x.isPlainObject(e) ? e : {}, s[i] = x.extend(c, a, r)) : r !== t && (s[i] = r)); return s }, x.extend({ expando: "jQuery" + (f + Math.random()).replace(/\D/g, ""), noConflict: function (t) { return e.$ === x && (e.$ = u), t && e.jQuery === x && (e.jQuery = l), x }, isReady: !1, readyWait: 1, holdReady: function (e) { e ? x.readyWait++ : x.ready(!0) }, ready: function (e) { if (e === !0 ? !--x.readyWait : !x.isReady) { if (!a.body) return setTimeout(x.ready); x.isReady = !0, e !== !0 && --x.readyWait > 0 || (n.resolveWith(a, [x]), x.fn.trigger && x(a).trigger("ready").off("ready")) } }, isFunction: function (e) { return "function" === x.type(e) }, isArray: Array.isArray || function (e) { return "array" === x.type(e) }, isWindow: function (e) { return null != e && e == e.window }, isNumeric: function (e) { return !isNaN(parseFloat(e)) && isFinite(e) }, type: function (e) { return null == e ? e + "" : "object" == typeof e || "function" == typeof e ? c[y.call(e)] || "object" : typeof e }, isPlainObject: function (e) { var n; if (!e || "object" !== x.type(e) || e.nodeType || x.isWindow(e)) return !1; try { if (e.constructor && !v.call(e, "constructor") && !v.call(e.constructor.prototype, "isPrototypeOf")) return !1 } catch (r) { return !1 } if (x.support.ownLast) for (n in e) return v.call(e, n); for (n in e); return n === t || v.call(e, n) }, isEmptyObject: function (e) { var t; for (t in e) return !1; return !0 }, error: function (e) { throw Error(e) }, parseHTML: function (e, t, n) { if (!e || "string" != typeof e) return null; "boolean" == typeof t && (n = t, t = !1), t = t || a; var r = k.exec(e), i = !n && []; return r ? [t.createElement(r[1])] : (r = x.buildFragment([e], t, i), i && x(i).remove(), x.merge([], r.childNodes)) }, parseJSON: function (n) { return e.JSON && e.JSON.parse ? e.JSON.parse(n) : null === n ? n : "string" == typeof n && (n = x.trim(n), n && E.test(n.replace(A, "@").replace(j, "]").replace(S, ""))) ? Function("return " + n)() : (x.error("Invalid JSON: " + n), t) }, parseXML: function (n) { var r, i; if (!n || "string" != typeof n) return null; try { e.DOMParser ? (i = new DOMParser, r = i.parseFromString(n, "text/xml")) : (r = new ActiveXObject("Microsoft.XMLDOM"), r.async = "false", r.loadXML(n)) } catch (o) { r = t } return r && r.documentElement && !r.getElementsByTagName("parsererror").length || x.error("Invalid XML: " + n), r }, noop: function () { }, globalEval: function (t) { t && x.trim(t) && (e.execScript || function (t) { e.eval.call(e, t) })(t) }, camelCase: function (e) { return e.replace(D, "ms-").replace(L, H) }, nodeName: function (e, t) { return e.nodeName && e.nodeName.toLowerCase() === t.toLowerCase() }, each: function (e, t, n) { var r, i = 0, o = e.length, a = M(e); if (n) { if (a) { for (; o > i; i++) if (r = t.apply(e[i], n), r === !1) break } else for (i in e) if (r = t.apply(e[i], n), r === !1) break } else if (a) { for (; o > i; i++) if (r = t.call(e[i], i, e[i]), r === !1) break } else for (i in e) if (r = t.call(e[i], i, e[i]), r === !1) break; return e }, trim: b && !b.call("\ufeff\u00a0") ? function (e) { return null == e ? "" : b.call(e) } : function (e) { return null == e ? "" : (e + "").replace(C, "") }, makeArray: function (e, t) { var n = t || []; return null != e && (M(Object(e)) ? x.merge(n, "string" == typeof e ? [e] : e) : h.call(n, e)), n }, inArray: function (e, t, n) { var r; if (t) { if (m) return m.call(t, e, n); for (r = t.length, n = n ? 0 > n ? Math.max(0, r + n) : n : 0; r > n; n++) if (n in t && t[n] === e) return n } return -1 }, merge: function (e, n) { var r = n.length, i = e.length, o = 0; if ("number" == typeof r) for (; r > o; o++) e[i++] = n[o]; else while (n[o] !== t) e[i++] = n[o++]; return e.length = i, e }, grep: function (e, t, n) { var r, i = [], o = 0, a = e.length; for (n = !!n; a > o; o++) r = !!t(e[o], o), n !== r && i.push(e[o]); return i }, map: function (e, t, n) { var r, i = 0, o = e.length, a = M(e), s = []; if (a) for (; o > i; i++) r = t(e[i], i, n), null != r && (s[s.length] = r); else for (i in e) r = t(e[i], i, n), null != r && (s[s.length] = r); return d.apply([], s) }, guid: 1, proxy: function (e, n) { var r, i, o; return "string" == typeof n && (o = e[n], n = e, e = o), x.isFunction(e) ? (r = g.call(arguments, 2), i = function () { return e.apply(n || this, r.concat(g.call(arguments))) }, i.guid = e.guid = e.guid || x.guid++, i) : t }, access: function (e, n, r, i, o, a, s) { var l = 0, u = e.length, c = null == r; if ("object" === x.type(r)) { o = !0; for (l in r) x.access(e, n, l, r[l], !0, a, s) } else if (i !== t && (o = !0, x.isFunction(i) || (s = !0), c && (s ? (n.call(e, i), n = null) : (c = n, n = function (e, t, n) { return c.call(x(e), n) })), n)) for (; u > l; l++) n(e[l], r, s ? i : i.call(e[l], l, n(e[l], r))); return o ? e : c ? n.call(e) : u ? n(e[0], r) : a }, now: function () { return (new Date).getTime() }, swap: function (e, t, n, r) { var i, o, a = {}; for (o in t) a[o] = e.style[o], e.style[o] = t[o]; i = n.apply(e, r || []); for (o in t) e.style[o] = a[o]; return i } }), x.ready.promise = function (t) { if (!n) if (n = x.Deferred(), "complete" === a.readyState) setTimeout(x.ready); else if (a.addEventListener) a.addEventListener("DOMContentLoaded", q, !1), e.addEventListener("load", q, !1); else { a.attachEvent("onreadystatechange", q), e.attachEvent("onload", q); var r = !1; try { r = null == e.frameElement && a.documentElement } catch (i) { } r && r.doScroll && function o() { if (!x.isReady) { try { r.doScroll("left") } catch (e) { return setTimeout(o, 50) } _(), x.ready() } }() } return n.promise(t) }, x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function (e, t) { c["[object " + t + "]"] = t.toLowerCase() }); function M(e) { var t = e.length, n = x.type(e); return x.isWindow(e) ? !1 : 1 === e.nodeType && t ? !0 : "array" === n || "function" !== n && (0 === t || "number" == typeof t && t > 0 && t - 1 in e) } r = x(a), function (e, t) { var n, r, i, o, a, s, l, u, c, p, f, d, h, g, m, y, v, b = "sizzle" + -new Date, w = e.document, T = 0, C = 0, N = st(), k = st(), E = st(), S = !1, A = function (e, t) { return e === t ? (S = !0, 0) : 0 }, j = typeof t, D = 1 << 31, L = {}.hasOwnProperty, H = [], q = H.pop, _ = H.push, M = H.push, O = H.slice, F = H.indexOf || function (e) { var t = 0, n = this.length; for (; n > t; t++) if (this[t] === e) return t; return -1 }, B = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", P = "[\\x20\\t\\r\\n\\f]", R = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", W = R.replace("w", "w#"), $ = "\\[" + P + "*(" + R + ")" + P + "*(?:([*^$|!~]?=)" + P + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + W + ")|)|)" + P + "*\\]", I = ":(" + R + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + $.replace(3, 8) + ")*)|.*)\\)|)", z = RegExp("^" + P + "+|((?:^|[^\\\\])(?:\\\\.)*)" + P + "+$", "g"), X = RegExp("^" + P + "*," + P + "*"), U = RegExp("^" + P + "*([>+~]|" + P + ")" + P + "*"), V = RegExp(P + "*[+~]"), Y = RegExp("=" + P + "*([^\\]'\"]*)" + P + "*\\]", "g"), J = RegExp(I), G = RegExp("^" + W + "$"), Q = { ID: RegExp("^#(" + R + ")"), CLASS: RegExp("^\\.(" + R + ")"), TAG: RegExp("^(" + R.replace("w", "w*") + ")"), ATTR: RegExp("^" + $), PSEUDO: RegExp("^" + I), CHILD: RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + P + "*(even|odd|(([+-]|)(\\d*)n|)" + P + "*(?:([+-]|)" + P + "*(\\d+)|))" + P + "*\\)|)", "i"), bool: RegExp("^(?:" + B + ")$", "i"), needsContext: RegExp("^" + P + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + P + "*((?:-\\d)?\\d*)" + P + "*\\)|)(?=[^-]|$)", "i") }, K = /^[^{]+\{\s*\[native \w/, Z = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, et = /^(?:input|select|textarea|button)$/i, tt = /^h\d$/i, nt = /'|\\/g, rt = RegExp("\\\\([\\da-f]{1,6}" + P + "?|(" + P + ")|.)", "ig"), it = function (e, t, n) { var r = "0x" + t - 65536; return r !== r || n ? t : 0 > r ? String.fromCharCode(r + 65536) : String.fromCharCode(55296 | r >> 10, 56320 | 1023 & r) }; try { M.apply(H = O.call(w.childNodes), w.childNodes), H[w.childNodes.length].nodeType } catch (ot) { M = { apply: H.length ? function (e, t) { _.apply(e, O.call(t)) } : function (e, t) { var n = e.length, r = 0; while (e[n++] = t[r++]); e.length = n - 1 } } } function at(e, t, n, i) { var o, a, s, l, u, c, d, m, y, x; if ((t ? t.ownerDocument || t : w) !== f && p(t), t = t || f, n = n || [], !e || "string" != typeof e) return n; if (1 !== (l = t.nodeType) && 9 !== l) return []; if (h && !i) { if (o = Z.exec(e)) if (s = o[1]) { if (9 === l) { if (a = t.getElementById(s), !a || !a.parentNode) return n; if (a.id === s) return n.push(a), n } else if (t.ownerDocument && (a = t.ownerDocument.getElementById(s)) && v(t, a) && a.id === s) return n.push(a), n } else { if (o[2]) return M.apply(n, t.getElementsByTagName(e)), n; if ((s = o[3]) && r.getElementsByClassName && t.getElementsByClassName) return M.apply(n, t.getElementsByClassName(s)), n } if (r.qsa && (!g || !g.test(e))) { if (m = d = b, y = t, x = 9 === l && e, 1 === l && "object" !== t.nodeName.toLowerCase()) { c = mt(e), (d = t.getAttribute("id")) ? m = d.replace(nt, "\\$&") : t.setAttribute("id", m), m = "[id='" + m + "'] ", u = c.length; while (u--) c[u] = m + yt(c[u]); y = V.test(e) && t.parentNode || t, x = c.join(",") } if (x) try { return M.apply(n, y.querySelectorAll(x)), n } catch (T) { } finally { d || t.removeAttribute("id") } } } return kt(e.replace(z, "$1"), t, n, i) } function st() { var e = []; function t(n, r) { return e.push(n += " ") > o.cacheLength && delete t[e.shift()], t[n] = r } return t } function lt(e) { return e[b] = !0, e } function ut(e) { var t = f.createElement("div"); try { return !!e(t) } catch (n) { return !1 } finally { t.parentNode && t.parentNode.removeChild(t), t = null } } function ct(e, t) { var n = e.split("|"), r = e.length; while (r--) o.attrHandle[n[r]] = t } function pt(e, t) { var n = t && e, r = n && 1 === e.nodeType && 1 === t.nodeType && (~t.sourceIndex || D) - (~e.sourceIndex || D); if (r) return r; if (n) while (n = n.nextSibling) if (n === t) return -1; return e ? 1 : -1 } function ft(e) { return function (t) { var n = t.nodeName.toLowerCase(); return "input" === n && t.type === e } } function dt(e) { return function (t) { var n = t.nodeName.toLowerCase(); return ("input" === n || "button" === n) && t.type === e } } function ht(e) { return lt(function (t) { return t = +t, lt(function (n, r) { var i, o = e([], n.length, t), a = o.length; while (a--) n[i = o[a]] && (n[i] = !(r[i] = n[i])) }) }) } s = at.isXML = function (e) { var t = e && (e.ownerDocument || e).documentElement; return t ? "HTML" !== t.nodeName : !1 }, r = at.support = {}, p = at.setDocument = function (e) { var n = e ? e.ownerDocument || e : w, i = n.defaultView; return n !== f && 9 === n.nodeType && n.documentElement ? (f = n, d = n.documentElement, h = !s(n), i && i.attachEvent && i !== i.top && i.attachEvent("onbeforeunload", function () { p() }), r.attributes = ut(function (e) { return e.className = "i", !e.getAttribute("className") }), r.getElementsByTagName = ut(function (e) { return e.appendChild(n.createComment("")), !e.getElementsByTagName("*").length }), r.getElementsByClassName = ut(function (e) { return e.innerHTML = "
              ", e.firstChild.className = "i", 2 === e.getElementsByClassName("i").length }), r.getById = ut(function (e) { return d.appendChild(e).id = b, !n.getElementsByName || !n.getElementsByName(b).length }), r.getById ? (o.find.ID = function (e, t) { if (typeof t.getElementById !== j && h) { var n = t.getElementById(e); return n && n.parentNode ? [n] : [] } }, o.filter.ID = function (e) { var t = e.replace(rt, it); return function (e) { return e.getAttribute("id") === t } }) : (delete o.find.ID, o.filter.ID = function (e) { var t = e.replace(rt, it); return function (e) { var n = typeof e.getAttributeNode !== j && e.getAttributeNode("id"); return n && n.value === t } }), o.find.TAG = r.getElementsByTagName ? function (e, n) { return typeof n.getElementsByTagName !== j ? n.getElementsByTagName(e) : t } : function (e, t) { var n, r = [], i = 0, o = t.getElementsByTagName(e); if ("*" === e) { while (n = o[i++]) 1 === n.nodeType && r.push(n); return r } return o }, o.find.CLASS = r.getElementsByClassName && function (e, n) { return typeof n.getElementsByClassName !== j && h ? n.getElementsByClassName(e) : t }, m = [], g = [], (r.qsa = K.test(n.querySelectorAll)) && (ut(function (e) { e.innerHTML = "", e.querySelectorAll("[selected]").length || g.push("\\[" + P + "*(?:value|" + B + ")"), e.querySelectorAll(":checked").length || g.push(":checked") }), ut(function (e) { var t = n.createElement("input"); t.setAttribute("type", "hidden"), e.appendChild(t).setAttribute("t", ""), e.querySelectorAll("[t^='']").length && g.push("[*^$]=" + P + "*(?:''|\"\")"), e.querySelectorAll(":enabled").length || g.push(":enabled", ":disabled"), e.querySelectorAll("*,:x"), g.push(",.*:") })), (r.matchesSelector = K.test(y = d.webkitMatchesSelector || d.mozMatchesSelector || d.oMatchesSelector || d.msMatchesSelector)) && ut(function (e) { r.disconnectedMatch = y.call(e, "div"), y.call(e, "[s!='']:x"), m.push("!=", I) }), g = g.length && RegExp(g.join("|")), m = m.length && RegExp(m.join("|")), v = K.test(d.contains) || d.compareDocumentPosition ? function (e, t) { var n = 9 === e.nodeType ? e.documentElement : e, r = t && t.parentNode; return e === r || !(!r || 1 !== r.nodeType || !(n.contains ? n.contains(r) : e.compareDocumentPosition && 16 & e.compareDocumentPosition(r))) } : function (e, t) { if (t) while (t = t.parentNode) if (t === e) return !0; return !1 }, A = d.compareDocumentPosition ? function (e, t) { if (e === t) return S = !0, 0; var i = t.compareDocumentPosition && e.compareDocumentPosition && e.compareDocumentPosition(t); return i ? 1 & i || !r.sortDetached && t.compareDocumentPosition(e) === i ? e === n || v(w, e) ? -1 : t === n || v(w, t) ? 1 : c ? F.call(c, e) - F.call(c, t) : 0 : 4 & i ? -1 : 1 : e.compareDocumentPosition ? -1 : 1 } : function (e, t) { var r, i = 0, o = e.parentNode, a = t.parentNode, s = [e], l = [t]; if (e === t) return S = !0, 0; if (!o || !a) return e === n ? -1 : t === n ? 1 : o ? -1 : a ? 1 : c ? F.call(c, e) - F.call(c, t) : 0; if (o === a) return pt(e, t); r = e; while (r = r.parentNode) s.unshift(r); r = t; while (r = r.parentNode) l.unshift(r); while (s[i] === l[i]) i++; return i ? pt(s[i], l[i]) : s[i] === w ? -1 : l[i] === w ? 1 : 0 }, n) : f }, at.matches = function (e, t) { return at(e, null, null, t) }, at.matchesSelector = function (e, t) { if ((e.ownerDocument || e) !== f && p(e), t = t.replace(Y, "='$1']"), !(!r.matchesSelector || !h || m && m.test(t) || g && g.test(t))) try { var n = y.call(e, t); if (n || r.disconnectedMatch || e.document && 11 !== e.document.nodeType) return n } catch (i) { } return at(t, f, null, [e]).length > 0 }, at.contains = function (e, t) { return (e.ownerDocument || e) !== f && p(e), v(e, t) }, at.attr = function (e, n) { (e.ownerDocument || e) !== f && p(e); var i = o.attrHandle[n.toLowerCase()], a = i && L.call(o.attrHandle, n.toLowerCase()) ? i(e, n, !h) : t; return a === t ? r.attributes || !h ? e.getAttribute(n) : (a = e.getAttributeNode(n)) && a.specified ? a.value : null : a }, at.error = function (e) { throw Error("Syntax error, unrecognized expression: " + e) }, at.uniqueSort = function (e) { var t, n = [], i = 0, o = 0; if (S = !r.detectDuplicates, c = !r.sortStable && e.slice(0), e.sort(A), S) { while (t = e[o++]) t === e[o] && (i = n.push(o)); while (i--) e.splice(n[i], 1) } return e }, a = at.getText = function (e) { var t, n = "", r = 0, i = e.nodeType; if (i) { if (1 === i || 9 === i || 11 === i) { if ("string" == typeof e.textContent) return e.textContent; for (e = e.firstChild; e; e = e.nextSibling) n += a(e) } else if (3 === i || 4 === i) return e.nodeValue } else for (; t = e[r]; r++) n += a(t); return n }, o = at.selectors = { cacheLength: 50, createPseudo: lt, match: Q, attrHandle: {}, find: {}, relative: { ">": { dir: "parentNode", first: !0 }, " ": { dir: "parentNode" }, "+": { dir: "previousSibling", first: !0 }, "~": { dir: "previousSibling" } }, preFilter: { ATTR: function (e) { return e[1] = e[1].replace(rt, it), e[3] = (e[4] || e[5] || "").replace(rt, it), "~=" === e[2] && (e[3] = " " + e[3] + " "), e.slice(0, 4) }, CHILD: function (e) { return e[1] = e[1].toLowerCase(), "nth" === e[1].slice(0, 3) ? (e[3] || at.error(e[0]), e[4] = +(e[4] ? e[5] + (e[6] || 1) : 2 * ("even" === e[3] || "odd" === e[3])), e[5] = +(e[7] + e[8] || "odd" === e[3])) : e[3] && at.error(e[0]), e }, PSEUDO: function (e) { var n, r = !e[5] && e[2]; return Q.CHILD.test(e[0]) ? null : (e[3] && e[4] !== t ? e[2] = e[4] : r && J.test(r) && (n = mt(r, !0)) && (n = r.indexOf(")", r.length - n) - r.length) && (e[0] = e[0].slice(0, n), e[2] = r.slice(0, n)), e.slice(0, 3)) } }, filter: { TAG: function (e) { var t = e.replace(rt, it).toLowerCase(); return "*" === e ? function () { return !0 } : function (e) { return e.nodeName && e.nodeName.toLowerCase() === t } }, CLASS: function (e) { var t = N[e + " "]; return t || (t = RegExp("(^|" + P + ")" + e + "(" + P + "|$)")) && N(e, function (e) { return t.test("string" == typeof e.className && e.className || typeof e.getAttribute !== j && e.getAttribute("class") || "") }) }, ATTR: function (e, t, n) { return function (r) { var i = at.attr(r, e); return null == i ? "!=" === t : t ? (i += "", "=" === t ? i === n : "!=" === t ? i !== n : "^=" === t ? n && 0 === i.indexOf(n) : "*=" === t ? n && i.indexOf(n) > -1 : "$=" === t ? n && i.slice(-n.length) === n : "~=" === t ? (" " + i + " ").indexOf(n) > -1 : "|=" === t ? i === n || i.slice(0, n.length + 1) === n + "-" : !1) : !0 } }, CHILD: function (e, t, n, r, i) { var o = "nth" !== e.slice(0, 3), a = "last" !== e.slice(-4), s = "of-type" === t; return 1 === r && 0 === i ? function (e) { return !!e.parentNode } : function (t, n, l) { var u, c, p, f, d, h, g = o !== a ? "nextSibling" : "previousSibling", m = t.parentNode, y = s && t.nodeName.toLowerCase(), v = !l && !s; if (m) { if (o) { while (g) { p = t; while (p = p[g]) if (s ? p.nodeName.toLowerCase() === y : 1 === p.nodeType) return !1; h = g = "only" === e && !h && "nextSibling" } return !0 } if (h = [a ? m.firstChild : m.lastChild], a && v) { c = m[b] || (m[b] = {}), u = c[e] || [], d = u[0] === T && u[1], f = u[0] === T && u[2], p = d && m.childNodes[d]; while (p = ++d && p && p[g] || (f = d = 0) || h.pop()) if (1 === p.nodeType && ++f && p === t) { c[e] = [T, d, f]; break } } else if (v && (u = (t[b] || (t[b] = {}))[e]) && u[0] === T) f = u[1]; else while (p = ++d && p && p[g] || (f = d = 0) || h.pop()) if ((s ? p.nodeName.toLowerCase() === y : 1 === p.nodeType) && ++f && (v && ((p[b] || (p[b] = {}))[e] = [T, f]), p === t)) break; return f -= i, f === r || 0 === f % r && f / r >= 0 } } }, PSEUDO: function (e, t) { var n, r = o.pseudos[e] || o.setFilters[e.toLowerCase()] || at.error("unsupported pseudo: " + e); return r[b] ? r(t) : r.length > 1 ? (n = [e, e, "", t], o.setFilters.hasOwnProperty(e.toLowerCase()) ? lt(function (e, n) { var i, o = r(e, t), a = o.length; while (a--) i = F.call(e, o[a]), e[i] = !(n[i] = o[a]) }) : function (e) { return r(e, 0, n) }) : r } }, pseudos: { not: lt(function (e) { var t = [], n = [], r = l(e.replace(z, "$1")); return r[b] ? lt(function (e, t, n, i) { var o, a = r(e, null, i, []), s = e.length; while (s--) (o = a[s]) && (e[s] = !(t[s] = o)) }) : function (e, i, o) { return t[0] = e, r(t, null, o, n), !n.pop() } }), has: lt(function (e) { return function (t) { return at(e, t).length > 0 } }), contains: lt(function (e) { return function (t) { return (t.textContent || t.innerText || a(t)).indexOf(e) > -1 } }), lang: lt(function (e) { return G.test(e || "") || at.error("unsupported lang: " + e), e = e.replace(rt, it).toLowerCase(), function (t) { var n; do if (n = h ? t.lang : t.getAttribute("xml:lang") || t.getAttribute("lang")) return n = n.toLowerCase(), n === e || 0 === n.indexOf(e + "-"); while ((t = t.parentNode) && 1 === t.nodeType); return !1 } }), target: function (t) { var n = e.location && e.location.hash; return n && n.slice(1) === t.id }, root: function (e) { return e === d }, focus: function (e) { return e === f.activeElement && (!f.hasFocus || f.hasFocus()) && !!(e.type || e.href || ~e.tabIndex) }, enabled: function (e) { return e.disabled === !1 }, disabled: function (e) { return e.disabled === !0 }, checked: function (e) { var t = e.nodeName.toLowerCase(); return "input" === t && !!e.checked || "option" === t && !!e.selected }, selected: function (e) { return e.parentNode && e.parentNode.selectedIndex, e.selected === !0 }, empty: function (e) { for (e = e.firstChild; e; e = e.nextSibling) if (e.nodeName > "@" || 3 === e.nodeType || 4 === e.nodeType) return !1; return !0 }, parent: function (e) { return !o.pseudos.empty(e) }, header: function (e) { return tt.test(e.nodeName) }, input: function (e) { return et.test(e.nodeName) }, button: function (e) { var t = e.nodeName.toLowerCase(); return "input" === t && "button" === e.type || "button" === t }, text: function (e) { var t; return "input" === e.nodeName.toLowerCase() && "text" === e.type && (null == (t = e.getAttribute("type")) || t.toLowerCase() === e.type) }, first: ht(function () { return [0] }), last: ht(function (e, t) { return [t - 1] }), eq: ht(function (e, t, n) { return [0 > n ? n + t : n] }), even: ht(function (e, t) { var n = 0; for (; t > n; n += 2) e.push(n); return e }), odd: ht(function (e, t) { var n = 1; for (; t > n; n += 2) e.push(n); return e }), lt: ht(function (e, t, n) { var r = 0 > n ? n + t : n; for (; --r >= 0;) e.push(r); return e }), gt: ht(function (e, t, n) { var r = 0 > n ? n + t : n; for (; t > ++r;) e.push(r); return e }) } }, o.pseudos.nth = o.pseudos.eq; for (n in { radio: !0, checkbox: !0, file: !0, password: !0, image: !0 }) o.pseudos[n] = ft(n); for (n in { submit: !0, reset: !0 }) o.pseudos[n] = dt(n); function gt() { } gt.prototype = o.filters = o.pseudos, o.setFilters = new gt; function mt(e, t) { var n, r, i, a, s, l, u, c = k[e + " "]; if (c) return t ? 0 : c.slice(0); s = e, l = [], u = o.preFilter; while (s) { (!n || (r = X.exec(s))) && (r && (s = s.slice(r[0].length) || s), l.push(i = [])), n = !1, (r = U.exec(s)) && (n = r.shift(), i.push({ value: n, type: r[0].replace(z, " ") }), s = s.slice(n.length)); for (a in o.filter) !(r = Q[a].exec(s)) || u[a] && !(r = u[a](r)) || (n = r.shift(), i.push({ value: n, type: a, matches: r }), s = s.slice(n.length)); if (!n) break } return t ? s.length : s ? at.error(e) : k(e, l).slice(0) } function yt(e) { var t = 0, n = e.length, r = ""; for (; n > t; t++) r += e[t].value; return r } function vt(e, t, n) { var r = t.dir, o = n && "parentNode" === r, a = C++; return t.first ? function (t, n, i) { while (t = t[r]) if (1 === t.nodeType || o) return e(t, n, i) } : function (t, n, s) { var l, u, c, p = T + " " + a; if (s) { while (t = t[r]) if ((1 === t.nodeType || o) && e(t, n, s)) return !0 } else while (t = t[r]) if (1 === t.nodeType || o) if (c = t[b] || (t[b] = {}), (u = c[r]) && u[0] === p) { if ((l = u[1]) === !0 || l === i) return l === !0 } else if (u = c[r] = [p], u[1] = e(t, n, s) || i, u[1] === !0) return !0 } } function bt(e) { return e.length > 1 ? function (t, n, r) { var i = e.length; while (i--) if (!e[i](t, n, r)) return !1; return !0 } : e[0] } function xt(e, t, n, r, i) { var o, a = [], s = 0, l = e.length, u = null != t; for (; l > s; s++) (o = e[s]) && (!n || n(o, r, i)) && (a.push(o), u && t.push(s)); return a } function wt(e, t, n, r, i, o) { return r && !r[b] && (r = wt(r)), i && !i[b] && (i = wt(i, o)), lt(function (o, a, s, l) { var u, c, p, f = [], d = [], h = a.length, g = o || Nt(t || "*", s.nodeType ? [s] : s, []), m = !e || !o && t ? g : xt(g, f, e, s, l), y = n ? i || (o ? e : h || r) ? [] : a : m; if (n && n(m, y, s, l), r) { u = xt(y, d), r(u, [], s, l), c = u.length; while (c--) (p = u[c]) && (y[d[c]] = !(m[d[c]] = p)) } if (o) { if (i || e) { if (i) { u = [], c = y.length; while (c--) (p = y[c]) && u.push(m[c] = p); i(null, y = [], u, l) } c = y.length; while (c--) (p = y[c]) && (u = i ? F.call(o, p) : f[c]) > -1 && (o[u] = !(a[u] = p)) } } else y = xt(y === a ? y.splice(h, y.length) : y), i ? i(null, a, y, l) : M.apply(a, y) }) } function Tt(e) { var t, n, r, i = e.length, a = o.relative[e[0].type], s = a || o.relative[" "], l = a ? 1 : 0, c = vt(function (e) { return e === t }, s, !0), p = vt(function (e) { return F.call(t, e) > -1 }, s, !0), f = [function (e, n, r) { return !a && (r || n !== u) || ((t = n).nodeType ? c(e, n, r) : p(e, n, r)) }]; for (; i > l; l++) if (n = o.relative[e[l].type]) f = [vt(bt(f), n)]; else { if (n = o.filter[e[l].type].apply(null, e[l].matches), n[b]) { for (r = ++l; i > r; r++) if (o.relative[e[r].type]) break; return wt(l > 1 && bt(f), l > 1 && yt(e.slice(0, l - 1).concat({ value: " " === e[l - 2].type ? "*" : "" })).replace(z, "$1"), n, r > l && Tt(e.slice(l, r)), i > r && Tt(e = e.slice(r)), i > r && yt(e)) } f.push(n) } return bt(f) } function Ct(e, t) { var n = 0, r = t.length > 0, a = e.length > 0, s = function (s, l, c, p, d) { var h, g, m, y = [], v = 0, b = "0", x = s && [], w = null != d, C = u, N = s || a && o.find.TAG("*", d && l.parentNode || l), k = T += null == C ? 1 : Math.random() || .1; for (w && (u = l !== f && l, i = n) ; null != (h = N[b]) ; b++) { if (a && h) { g = 0; while (m = e[g++]) if (m(h, l, c)) { p.push(h); break } w && (T = k, i = ++n) } r && ((h = !m && h) && v--, s && x.push(h)) } if (v += b, r && b !== v) { g = 0; while (m = t[g++]) m(x, y, l, c); if (s) { if (v > 0) while (b--) x[b] || y[b] || (y[b] = q.call(p)); y = xt(y) } M.apply(p, y), w && !s && y.length > 0 && v + t.length > 1 && at.uniqueSort(p) } return w && (T = k, u = C), x }; return r ? lt(s) : s } l = at.compile = function (e, t) { var n, r = [], i = [], o = E[e + " "]; if (!o) { t || (t = mt(e)), n = t.length; while (n--) o = Tt(t[n]), o[b] ? r.push(o) : i.push(o); o = E(e, Ct(i, r)) } return o }; function Nt(e, t, n) { var r = 0, i = t.length; for (; i > r; r++) at(e, t[r], n); return n } function kt(e, t, n, i) { var a, s, u, c, p, f = mt(e); if (!i && 1 === f.length) { if (s = f[0] = f[0].slice(0), s.length > 2 && "ID" === (u = s[0]).type && r.getById && 9 === t.nodeType && h && o.relative[s[1].type]) { if (t = (o.find.ID(u.matches[0].replace(rt, it), t) || [])[0], !t) return n; e = e.slice(s.shift().value.length) } a = Q.needsContext.test(e) ? 0 : s.length; while (a--) { if (u = s[a], o.relative[c = u.type]) break; if ((p = o.find[c]) && (i = p(u.matches[0].replace(rt, it), V.test(s[0].type) && t.parentNode || t))) { if (s.splice(a, 1), e = i.length && yt(s), !e) return M.apply(n, i), n; break } } } return l(e, f)(i, t, !h, n, V.test(e)), n } r.sortStable = b.split("").sort(A).join("") === b, r.detectDuplicates = S, p(), r.sortDetached = ut(function (e) { return 1 & e.compareDocumentPosition(f.createElement("div")) }), ut(function (e) { return e.innerHTML = "", "#" === e.firstChild.getAttribute("href") }) || ct("type|href|height|width", function (e, n, r) { return r ? t : e.getAttribute(n, "type" === n.toLowerCase() ? 1 : 2) }), r.attributes && ut(function (e) { return e.innerHTML = "", e.firstChild.setAttribute("value", ""), "" === e.firstChild.getAttribute("value") }) || ct("value", function (e, n, r) { return r || "input" !== e.nodeName.toLowerCase() ? t : e.defaultValue }), ut(function (e) { return null == e.getAttribute("disabled") }) || ct(B, function (e, n, r) { var i; return r ? t : (i = e.getAttributeNode(n)) && i.specified ? i.value : e[n] === !0 ? n.toLowerCase() : null }), x.find = at, x.expr = at.selectors, x.expr[":"] = x.expr.pseudos, x.unique = at.uniqueSort, x.text = at.getText, x.isXMLDoc = at.isXML, x.contains = at.contains }(e); var O = {}; function F(e) { var t = O[e] = {}; return x.each(e.match(T) || [], function (e, n) { t[n] = !0 }), t } x.Callbacks = function (e) { e = "string" == typeof e ? O[e] || F(e) : x.extend({}, e); var n, r, i, o, a, s, l = [], u = !e.once && [], c = function (t) { for (r = e.memory && t, i = !0, a = s || 0, s = 0, o = l.length, n = !0; l && o > a; a++) if (l[a].apply(t[0], t[1]) === !1 && e.stopOnFalse) { r = !1; break } n = !1, l && (u ? u.length && c(u.shift()) : r ? l = [] : p.disable()) }, p = { add: function () { if (l) { var t = l.length; (function i(t) { x.each(t, function (t, n) { var r = x.type(n); "function" === r ? e.unique && p.has(n) || l.push(n) : n && n.length && "string" !== r && i(n) }) })(arguments), n ? o = l.length : r && (s = t, c(r)) } return this }, remove: function () { return l && x.each(arguments, function (e, t) { var r; while ((r = x.inArray(t, l, r)) > -1) l.splice(r, 1), n && (o >= r && o--, a >= r && a--) }), this }, has: function (e) { return e ? x.inArray(e, l) > -1 : !(!l || !l.length) }, empty: function () { return l = [], o = 0, this }, disable: function () { return l = u = r = t, this }, disabled: function () { return !l }, lock: function () { return u = t, r || p.disable(), this }, locked: function () { return !u }, fireWith: function (e, t) { return !l || i && !u || (t = t || [], t = [e, t.slice ? t.slice() : t], n ? u.push(t) : c(t)), this }, fire: function () { return p.fireWith(this, arguments), this }, fired: function () { return !!i } }; return p }, x.extend({ Deferred: function (e) { var t = [["resolve", "done", x.Callbacks("once memory"), "resolved"], ["reject", "fail", x.Callbacks("once memory"), "rejected"], ["notify", "progress", x.Callbacks("memory")]], n = "pending", r = { state: function () { return n }, always: function () { return i.done(arguments).fail(arguments), this }, then: function () { var e = arguments; return x.Deferred(function (n) { x.each(t, function (t, o) { var a = o[0], s = x.isFunction(e[t]) && e[t]; i[o[1]](function () { var e = s && s.apply(this, arguments); e && x.isFunction(e.promise) ? e.promise().done(n.resolve).fail(n.reject).progress(n.notify) : n[a + "With"](this === r ? n.promise() : this, s ? [e] : arguments) }) }), e = null }).promise() }, promise: function (e) { return null != e ? x.extend(e, r) : r } }, i = {}; return r.pipe = r.then, x.each(t, function (e, o) { var a = o[2], s = o[3]; r[o[1]] = a.add, s && a.add(function () { n = s }, t[1 ^ e][2].disable, t[2][2].lock), i[o[0]] = function () { return i[o[0] + "With"](this === i ? r : this, arguments), this }, i[o[0] + "With"] = a.fireWith }), r.promise(i), e && e.call(i, i), i }, when: function (e) { var t = 0, n = g.call(arguments), r = n.length, i = 1 !== r || e && x.isFunction(e.promise) ? r : 0, o = 1 === i ? e : x.Deferred(), a = function (e, t, n) { return function (r) { t[e] = this, n[e] = arguments.length > 1 ? g.call(arguments) : r, n === s ? o.notifyWith(t, n) : --i || o.resolveWith(t, n) } }, s, l, u; if (r > 1) for (s = Array(r), l = Array(r), u = Array(r) ; r > t; t++) n[t] && x.isFunction(n[t].promise) ? n[t].promise().done(a(t, u, n)).fail(o.reject).progress(a(t, l, s)) : --i; return i || o.resolveWith(u, n), o.promise() } }), x.support = function (t) { + var n, r, o, s, l, u, c, p, f, d = a.createElement("div"); if (d.setAttribute("className", "t"), d.innerHTML = "
              a", n = d.getElementsByTagName("*") || [], r = d.getElementsByTagName("a")[0], !r || !r.style || !n.length) return t; s = a.createElement("select"), u = s.appendChild(a.createElement("option")), o = d.getElementsByTagName("input")[0], r.style.cssText = "top:1px;float:left;opacity:.5", t.getSetAttribute = "t" !== d.className, t.leadingWhitespace = 3 === d.firstChild.nodeType, t.tbody = !d.getElementsByTagName("tbody").length, t.htmlSerialize = !!d.getElementsByTagName("link").length, t.style = /top/.test(r.getAttribute("style")), t.hrefNormalized = "/a" === r.getAttribute("href"), t.opacity = /^0.5/.test(r.style.opacity), t.cssFloat = !!r.style.cssFloat, t.checkOn = !!o.value, t.optSelected = u.selected, t.enctype = !!a.createElement("form").enctype, t.html5Clone = "<:nav>" !== a.createElement("nav").cloneNode(!0).outerHTML, t.inlineBlockNeedsLayout = !1, t.shrinkWrapBlocks = !1, t.pixelPosition = !1, t.deleteExpando = !0, t.noCloneEvent = !0, t.reliableMarginRight = !0, t.boxSizingReliable = !0, o.checked = !0, t.noCloneChecked = o.cloneNode(!0).checked, s.disabled = !0, t.optDisabled = !u.disabled; try { delete d.test } catch (h) { t.deleteExpando = !1 } o = a.createElement("input"), o.setAttribute("value", ""), t.input = "" === o.getAttribute("value"), o.value = "t", o.setAttribute("type", "radio"), t.radioValue = "t" === o.value, o.setAttribute("checked", "t"), o.setAttribute("name", "t"), l = a.createDocumentFragment(), l.appendChild(o), t.appendChecked = o.checked, t.checkClone = l.cloneNode(!0).cloneNode(!0).lastChild.checked, d.attachEvent && (d.attachEvent("onclick", function () { t.noCloneEvent = !1 }), d.cloneNode(!0).click()); for (f in { submit: !0, change: !0, focusin: !0 }) d.setAttribute(c = "on" + f, "t"), t[f + "Bubbles"] = c in e || d.attributes[c].expando === !1; d.style.backgroundClip = "content-box", d.cloneNode(!0).style.backgroundClip = "", t.clearCloneStyle = "content-box" === d.style.backgroundClip; for (f in x(t)) break; return t.ownLast = "0" !== f, x(function () { var n, r, o, s = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;", l = a.getElementsByTagName("body")[0]; l && (n = a.createElement("div"), n.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px", l.appendChild(n).appendChild(d), d.innerHTML = "
              t
              ", o = d.getElementsByTagName("td"), o[0].style.cssText = "padding:0;margin:0;border:0;display:none", p = 0 === o[0].offsetHeight, o[0].style.display = "", o[1].style.display = "none", t.reliableHiddenOffsets = p && 0 === o[0].offsetHeight, d.innerHTML = "", d.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;", x.swap(l, null != l.style.zoom ? { zoom: 1 } : {}, function () { t.boxSizing = 4 === d.offsetWidth }), e.getComputedStyle && (t.pixelPosition = "1%" !== (e.getComputedStyle(d, null) || {}).top, t.boxSizingReliable = "4px" === (e.getComputedStyle(d, null) || { width: "4px" }).width, r = d.appendChild(a.createElement("div")), r.style.cssText = d.style.cssText = s, r.style.marginRight = r.style.width = "0", d.style.width = "1px", t.reliableMarginRight = !parseFloat((e.getComputedStyle(r, null) || {}).marginRight)), typeof d.style.zoom !== i && (d.innerHTML = "", d.style.cssText = s + "width:1px;padding:1px;display:inline;zoom:1", t.inlineBlockNeedsLayout = 3 === d.offsetWidth, d.style.display = "block", d.innerHTML = "
              ", d.firstChild.style.width = "5px", t.shrinkWrapBlocks = 3 !== d.offsetWidth, t.inlineBlockNeedsLayout && (l.style.zoom = 1)), l.removeChild(n), n = d = o = r = null) }), n = s = l = u = r = o = null, t + }({}); var B = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, P = /([A-Z])/g; function R(e, n, r, i) { if (x.acceptData(e)) { var o, a, s = x.expando, l = e.nodeType, u = l ? x.cache : e, c = l ? e[s] : e[s] && s; if (c && u[c] && (i || u[c].data) || r !== t || "string" != typeof n) return c || (c = l ? e[s] = p.pop() || x.guid++ : s), u[c] || (u[c] = l ? {} : { toJSON: x.noop }), ("object" == typeof n || "function" == typeof n) && (i ? u[c] = x.extend(u[c], n) : u[c].data = x.extend(u[c].data, n)), a = u[c], i || (a.data || (a.data = {}), a = a.data), r !== t && (a[x.camelCase(n)] = r), "string" == typeof n ? (o = a[n], null == o && (o = a[x.camelCase(n)])) : o = a, o } } function W(e, t, n) { if (x.acceptData(e)) { var r, i, o = e.nodeType, a = o ? x.cache : e, s = o ? e[x.expando] : x.expando; if (a[s]) { if (t && (r = n ? a[s] : a[s].data)) { x.isArray(t) ? t = t.concat(x.map(t, x.camelCase)) : t in r ? t = [t] : (t = x.camelCase(t), t = t in r ? [t] : t.split(" ")), i = t.length; while (i--) delete r[t[i]]; if (n ? !I(r) : !x.isEmptyObject(r)) return } (n || (delete a[s].data, I(a[s]))) && (o ? x.cleanData([e], !0) : x.support.deleteExpando || a != a.window ? delete a[s] : a[s] = null) } } } x.extend({ cache: {}, noData: { applet: !0, embed: !0, object: "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" }, hasData: function (e) { return e = e.nodeType ? x.cache[e[x.expando]] : e[x.expando], !!e && !I(e) }, data: function (e, t, n) { return R(e, t, n) }, removeData: function (e, t) { return W(e, t) }, _data: function (e, t, n) { return R(e, t, n, !0) }, _removeData: function (e, t) { return W(e, t, !0) }, acceptData: function (e) { if (e.nodeType && 1 !== e.nodeType && 9 !== e.nodeType) return !1; var t = e.nodeName && x.noData[e.nodeName.toLowerCase()]; return !t || t !== !0 && e.getAttribute("classid") === t } }), x.fn.extend({ data: function (e, n) { var r, i, o = null, a = 0, s = this[0]; if (e === t) { if (this.length && (o = x.data(s), 1 === s.nodeType && !x._data(s, "parsedAttrs"))) { for (r = s.attributes; r.length > a; a++) i = r[a].name, 0 === i.indexOf("data-") && (i = x.camelCase(i.slice(5)), $(s, i, o[i])); x._data(s, "parsedAttrs", !0) } return o } return "object" == typeof e ? this.each(function () { x.data(this, e) }) : arguments.length > 1 ? this.each(function () { x.data(this, e, n) }) : s ? $(s, e, x.data(s, e)) : null }, removeData: function (e) { return this.each(function () { x.removeData(this, e) }) } }); function $(e, n, r) { if (r === t && 1 === e.nodeType) { var i = "data-" + n.replace(P, "-$1").toLowerCase(); if (r = e.getAttribute(i), "string" == typeof r) { try { r = "true" === r ? !0 : "false" === r ? !1 : "null" === r ? null : +r + "" === r ? +r : B.test(r) ? x.parseJSON(r) : r } catch (o) { } x.data(e, n, r) } else r = t } return r } function I(e) { var t; for (t in e) if (("data" !== t || !x.isEmptyObject(e[t])) && "toJSON" !== t) return !1; return !0 } x.extend({ queue: function (e, n, r) { var i; return e ? (n = (n || "fx") + "queue", i = x._data(e, n), r && (!i || x.isArray(r) ? i = x._data(e, n, x.makeArray(r)) : i.push(r)), i || []) : t }, dequeue: function (e, t) { t = t || "fx"; var n = x.queue(e, t), r = n.length, i = n.shift(), o = x._queueHooks(e, t), a = function () { x.dequeue(e, t) }; "inprogress" === i && (i = n.shift(), r--), i && ("fx" === t && n.unshift("inprogress"), delete o.stop, i.call(e, a, o)), !r && o && o.empty.fire() }, _queueHooks: function (e, t) { var n = t + "queueHooks"; return x._data(e, n) || x._data(e, n, { empty: x.Callbacks("once memory").add(function () { x._removeData(e, t + "queue"), x._removeData(e, n) }) }) } }), x.fn.extend({ queue: function (e, n) { var r = 2; return "string" != typeof e && (n = e, e = "fx", r--), r > arguments.length ? x.queue(this[0], e) : n === t ? this : this.each(function () { var t = x.queue(this, e, n); x._queueHooks(this, e), "fx" === e && "inprogress" !== t[0] && x.dequeue(this, e) }) }, dequeue: function (e) { return this.each(function () { x.dequeue(this, e) }) }, delay: function (e, t) { return e = x.fx ? x.fx.speeds[e] || e : e, t = t || "fx", this.queue(t, function (t, n) { var r = setTimeout(t, e); n.stop = function () { clearTimeout(r) } }) }, clearQueue: function (e) { return this.queue(e || "fx", []) }, promise: function (e, n) { var r, i = 1, o = x.Deferred(), a = this, s = this.length, l = function () { --i || o.resolveWith(a, [a]) }; "string" != typeof e && (n = e, e = t), e = e || "fx"; while (s--) r = x._data(a[s], e + "queueHooks"), r && r.empty && (i++, r.empty.add(l)); return l(), o.promise(n) } }); var z, X, U = /[\t\r\n\f]/g, V = /\r/g, Y = /^(?:input|select|textarea|button|object)$/i, J = /^(?:a|area)$/i, G = /^(?:checked|selected)$/i, Q = x.support.getSetAttribute, K = x.support.input; x.fn.extend({ attr: function (e, t) { return x.access(this, x.attr, e, t, arguments.length > 1) }, removeAttr: function (e) { return this.each(function () { x.removeAttr(this, e) }) }, prop: function (e, t) { return x.access(this, x.prop, e, t, arguments.length > 1) }, removeProp: function (e) { return e = x.propFix[e] || e, this.each(function () { try { this[e] = t, delete this[e] } catch (n) { } }) }, addClass: function (e) { var t, n, r, i, o, a = 0, s = this.length, l = "string" == typeof e && e; if (x.isFunction(e)) return this.each(function (t) { x(this).addClass(e.call(this, t, this.className)) }); if (l) for (t = (e || "").match(T) || []; s > a; a++) if (n = this[a], r = 1 === n.nodeType && (n.className ? (" " + n.className + " ").replace(U, " ") : " ")) { o = 0; while (i = t[o++]) 0 > r.indexOf(" " + i + " ") && (r += i + " "); n.className = x.trim(r) } return this }, removeClass: function (e) { var t, n, r, i, o, a = 0, s = this.length, l = 0 === arguments.length || "string" == typeof e && e; if (x.isFunction(e)) return this.each(function (t) { x(this).removeClass(e.call(this, t, this.className)) }); if (l) for (t = (e || "").match(T) || []; s > a; a++) if (n = this[a], r = 1 === n.nodeType && (n.className ? (" " + n.className + " ").replace(U, " ") : "")) { o = 0; while (i = t[o++]) while (r.indexOf(" " + i + " ") >= 0) r = r.replace(" " + i + " ", " "); n.className = e ? x.trim(r) : "" } return this }, toggleClass: function (e, t) { var n = typeof e; return "boolean" == typeof t && "string" === n ? t ? this.addClass(e) : this.removeClass(e) : x.isFunction(e) ? this.each(function (n) { x(this).toggleClass(e.call(this, n, this.className, t), t) }) : this.each(function () { if ("string" === n) { var t, r = 0, o = x(this), a = e.match(T) || []; while (t = a[r++]) o.hasClass(t) ? o.removeClass(t) : o.addClass(t) } else (n === i || "boolean" === n) && (this.className && x._data(this, "__className__", this.className), this.className = this.className || e === !1 ? "" : x._data(this, "__className__") || "") }) }, hasClass: function (e) { var t = " " + e + " ", n = 0, r = this.length; for (; r > n; n++) if (1 === this[n].nodeType && (" " + this[n].className + " ").replace(U, " ").indexOf(t) >= 0) return !0; return !1 }, val: function (e) { var n, r, i, o = this[0]; { if (arguments.length) return i = x.isFunction(e), this.each(function (n) { var o; 1 === this.nodeType && (o = i ? e.call(this, n, x(this).val()) : e, null == o ? o = "" : "number" == typeof o ? o += "" : x.isArray(o) && (o = x.map(o, function (e) { return null == e ? "" : e + "" })), r = x.valHooks[this.type] || x.valHooks[this.nodeName.toLowerCase()], r && "set" in r && r.set(this, o, "value") !== t || (this.value = o)) }); if (o) return r = x.valHooks[o.type] || x.valHooks[o.nodeName.toLowerCase()], r && "get" in r && (n = r.get(o, "value")) !== t ? n : (n = o.value, "string" == typeof n ? n.replace(V, "") : null == n ? "" : n) } } }), x.extend({ valHooks: { option: { get: function (e) { var t = x.find.attr(e, "value"); return null != t ? t : e.text } }, select: { get: function (e) { var t, n, r = e.options, i = e.selectedIndex, o = "select-one" === e.type || 0 > i, a = o ? null : [], s = o ? i + 1 : r.length, l = 0 > i ? s : o ? i : 0; for (; s > l; l++) if (n = r[l], !(!n.selected && l !== i || (x.support.optDisabled ? n.disabled : null !== n.getAttribute("disabled")) || n.parentNode.disabled && x.nodeName(n.parentNode, "optgroup"))) { if (t = x(n).val(), o) return t; a.push(t) } return a }, set: function (e, t) { var n, r, i = e.options, o = x.makeArray(t), a = i.length; while (a--) r = i[a], (r.selected = x.inArray(x(r).val(), o) >= 0) && (n = !0); return n || (e.selectedIndex = -1), o } } }, attr: function (e, n, r) { var o, a, s = e.nodeType; if (e && 3 !== s && 8 !== s && 2 !== s) return typeof e.getAttribute === i ? x.prop(e, n, r) : (1 === s && x.isXMLDoc(e) || (n = n.toLowerCase(), o = x.attrHooks[n] || (x.expr.match.bool.test(n) ? X : z)), r === t ? o && "get" in o && null !== (a = o.get(e, n)) ? a : (a = x.find.attr(e, n), null == a ? t : a) : null !== r ? o && "set" in o && (a = o.set(e, r, n)) !== t ? a : (e.setAttribute(n, r + ""), r) : (x.removeAttr(e, n), t)) }, removeAttr: function (e, t) { var n, r, i = 0, o = t && t.match(T); if (o && 1 === e.nodeType) while (n = o[i++]) r = x.propFix[n] || n, x.expr.match.bool.test(n) ? K && Q || !G.test(n) ? e[r] = !1 : e[x.camelCase("default-" + n)] = e[r] = !1 : x.attr(e, n, ""), e.removeAttribute(Q ? n : r) }, attrHooks: { type: { set: function (e, t) { if (!x.support.radioValue && "radio" === t && x.nodeName(e, "input")) { var n = e.value; return e.setAttribute("type", t), n && (e.value = n), t } } } }, propFix: { "for": "htmlFor", "class": "className" }, prop: function (e, n, r) { var i, o, a, s = e.nodeType; if (e && 3 !== s && 8 !== s && 2 !== s) return a = 1 !== s || !x.isXMLDoc(e), a && (n = x.propFix[n] || n, o = x.propHooks[n]), r !== t ? o && "set" in o && (i = o.set(e, r, n)) !== t ? i : e[n] = r : o && "get" in o && null !== (i = o.get(e, n)) ? i : e[n] }, propHooks: { tabIndex: { get: function (e) { var t = x.find.attr(e, "tabindex"); return t ? parseInt(t, 10) : Y.test(e.nodeName) || J.test(e.nodeName) && e.href ? 0 : -1 } } } }), X = { set: function (e, t, n) { return t === !1 ? x.removeAttr(e, n) : K && Q || !G.test(n) ? e.setAttribute(!Q && x.propFix[n] || n, n) : e[x.camelCase("default-" + n)] = e[n] = !0, n } }, x.each(x.expr.match.bool.source.match(/\w+/g), function (e, n) { var r = x.expr.attrHandle[n] || x.find.attr; x.expr.attrHandle[n] = K && Q || !G.test(n) ? function (e, n, i) { var o = x.expr.attrHandle[n], a = i ? t : (x.expr.attrHandle[n] = t) != r(e, n, i) ? n.toLowerCase() : null; return x.expr.attrHandle[n] = o, a } : function (e, n, r) { return r ? t : e[x.camelCase("default-" + n)] ? n.toLowerCase() : null } }), K && Q || (x.attrHooks.value = { set: function (e, n, r) { return x.nodeName(e, "input") ? (e.defaultValue = n, t) : z && z.set(e, n, r) } }), Q || (z = { set: function (e, n, r) { var i = e.getAttributeNode(r); return i || e.setAttributeNode(i = e.ownerDocument.createAttribute(r)), i.value = n += "", "value" === r || n === e.getAttribute(r) ? n : t } }, x.expr.attrHandle.id = x.expr.attrHandle.name = x.expr.attrHandle.coords = function (e, n, r) { var i; return r ? t : (i = e.getAttributeNode(n)) && "" !== i.value ? i.value : null }, x.valHooks.button = { get: function (e, n) { var r = e.getAttributeNode(n); return r && r.specified ? r.value : t }, set: z.set }, x.attrHooks.contenteditable = { set: function (e, t, n) { z.set(e, "" === t ? !1 : t, n) } }, x.each(["width", "height"], function (e, n) { x.attrHooks[n] = { set: function (e, r) { return "" === r ? (e.setAttribute(n, "auto"), r) : t } } })), x.support.hrefNormalized || x.each(["href", "src"], function (e, t) { x.propHooks[t] = { get: function (e) { return e.getAttribute(t, 4) } } }), x.support.style || (x.attrHooks.style = { get: function (e) { return e.style.cssText || t }, set: function (e, t) { return e.style.cssText = t + "" } }), x.support.optSelected || (x.propHooks.selected = { get: function (e) { var t = e.parentNode; return t && (t.selectedIndex, t.parentNode && t.parentNode.selectedIndex), null } }), x.each(["tabIndex", "readOnly", "maxLength", "cellSpacing", "cellPadding", "rowSpan", "colSpan", "useMap", "frameBorder", "contentEditable"], function () { x.propFix[this.toLowerCase()] = this }), x.support.enctype || (x.propFix.enctype = "encoding"), x.each(["radio", "checkbox"], function () { x.valHooks[this] = { set: function (e, n) { return x.isArray(n) ? e.checked = x.inArray(x(e).val(), n) >= 0 : t } }, x.support.checkOn || (x.valHooks[this].get = function (e) { return null === e.getAttribute("value") ? "on" : e.value }) }); var Z = /^(?:input|select|textarea)$/i, et = /^key/, tt = /^(?:mouse|contextmenu)|click/, nt = /^(?:focusinfocus|focusoutblur)$/, rt = /^([^.]*)(?:\.(.+)|)$/; function it() { return !0 } function ot() { return !1 } function at() { try { return a.activeElement } catch (e) { } } x.event = { global: {}, add: function (e, n, r, o, a) { var s, l, u, c, p, f, d, h, g, m, y, v = x._data(e); if (v) { r.handler && (c = r, r = c.handler, a = c.selector), r.guid || (r.guid = x.guid++), (l = v.events) || (l = v.events = {}), (f = v.handle) || (f = v.handle = function (e) { return typeof x === i || e && x.event.triggered === e.type ? t : x.event.dispatch.apply(f.elem, arguments) }, f.elem = e), n = (n || "").match(T) || [""], u = n.length; while (u--) s = rt.exec(n[u]) || [], g = y = s[1], m = (s[2] || "").split(".").sort(), g && (p = x.event.special[g] || {}, g = (a ? p.delegateType : p.bindType) || g, p = x.event.special[g] || {}, d = x.extend({ type: g, origType: y, data: o, handler: r, guid: r.guid, selector: a, needsContext: a && x.expr.match.needsContext.test(a), namespace: m.join(".") }, c), (h = l[g]) || (h = l[g] = [], h.delegateCount = 0, p.setup && p.setup.call(e, o, m, f) !== !1 || (e.addEventListener ? e.addEventListener(g, f, !1) : e.attachEvent && e.attachEvent("on" + g, f))), p.add && (p.add.call(e, d), d.handler.guid || (d.handler.guid = r.guid)), a ? h.splice(h.delegateCount++, 0, d) : h.push(d), x.event.global[g] = !0); e = null } }, remove: function (e, t, n, r, i) { var o, a, s, l, u, c, p, f, d, h, g, m = x.hasData(e) && x._data(e); if (m && (c = m.events)) { t = (t || "").match(T) || [""], u = t.length; while (u--) if (s = rt.exec(t[u]) || [], d = g = s[1], h = (s[2] || "").split(".").sort(), d) { p = x.event.special[d] || {}, d = (r ? p.delegateType : p.bindType) || d, f = c[d] || [], s = s[2] && RegExp("(^|\\.)" + h.join("\\.(?:.*\\.|)") + "(\\.|$)"), l = o = f.length; while (o--) a = f[o], !i && g !== a.origType || n && n.guid !== a.guid || s && !s.test(a.namespace) || r && r !== a.selector && ("**" !== r || !a.selector) || (f.splice(o, 1), a.selector && f.delegateCount--, p.remove && p.remove.call(e, a)); l && !f.length && (p.teardown && p.teardown.call(e, h, m.handle) !== !1 || x.removeEvent(e, d, m.handle), delete c[d]) } else for (d in c) x.event.remove(e, d + t[u], n, r, !0); x.isEmptyObject(c) && (delete m.handle, x._removeData(e, "events")) } }, trigger: function (n, r, i, o) { var s, l, u, c, p, f, d, h = [i || a], g = v.call(n, "type") ? n.type : n, m = v.call(n, "namespace") ? n.namespace.split(".") : []; if (u = f = i = i || a, 3 !== i.nodeType && 8 !== i.nodeType && !nt.test(g + x.event.triggered) && (g.indexOf(".") >= 0 && (m = g.split("."), g = m.shift(), m.sort()), l = 0 > g.indexOf(":") && "on" + g, n = n[x.expando] ? n : new x.Event(g, "object" == typeof n && n), n.isTrigger = o ? 2 : 3, n.namespace = m.join("."), n.namespace_re = n.namespace ? RegExp("(^|\\.)" + m.join("\\.(?:.*\\.|)") + "(\\.|$)") : null, n.result = t, n.target || (n.target = i), r = null == r ? [n] : x.makeArray(r, [n]), p = x.event.special[g] || {}, o || !p.trigger || p.trigger.apply(i, r) !== !1)) { if (!o && !p.noBubble && !x.isWindow(i)) { for (c = p.delegateType || g, nt.test(c + g) || (u = u.parentNode) ; u; u = u.parentNode) h.push(u), f = u; f === (i.ownerDocument || a) && h.push(f.defaultView || f.parentWindow || e) } d = 0; while ((u = h[d++]) && !n.isPropagationStopped()) n.type = d > 1 ? c : p.bindType || g, s = (x._data(u, "events") || {})[n.type] && x._data(u, "handle"), s && s.apply(u, r), s = l && u[l], s && x.acceptData(u) && s.apply && s.apply(u, r) === !1 && n.preventDefault(); if (n.type = g, !o && !n.isDefaultPrevented() && (!p._default || p._default.apply(h.pop(), r) === !1) && x.acceptData(i) && l && i[g] && !x.isWindow(i)) { f = i[l], f && (i[l] = null), x.event.triggered = g; try { i[g]() } catch (y) { } x.event.triggered = t, f && (i[l] = f) } return n.result } }, dispatch: function (e) { e = x.event.fix(e); var n, r, i, o, a, s = [], l = g.call(arguments), u = (x._data(this, "events") || {})[e.type] || [], c = x.event.special[e.type] || {}; if (l[0] = e, e.delegateTarget = this, !c.preDispatch || c.preDispatch.call(this, e) !== !1) { s = x.event.handlers.call(this, e, u), n = 0; while ((o = s[n++]) && !e.isPropagationStopped()) { e.currentTarget = o.elem, a = 0; while ((i = o.handlers[a++]) && !e.isImmediatePropagationStopped()) (!e.namespace_re || e.namespace_re.test(i.namespace)) && (e.handleObj = i, e.data = i.data, r = ((x.event.special[i.origType] || {}).handle || i.handler).apply(o.elem, l), r !== t && (e.result = r) === !1 && (e.preventDefault(), e.stopPropagation())) } return c.postDispatch && c.postDispatch.call(this, e), e.result } }, handlers: function (e, n) { var r, i, o, a, s = [], l = n.delegateCount, u = e.target; if (l && u.nodeType && (!e.button || "click" !== e.type)) for (; u != this; u = u.parentNode || this) if (1 === u.nodeType && (u.disabled !== !0 || "click" !== e.type)) { for (o = [], a = 0; l > a; a++) i = n[a], r = i.selector + " ", o[r] === t && (o[r] = i.needsContext ? x(r, this).index(u) >= 0 : x.find(r, this, null, [u]).length), o[r] && o.push(i); o.length && s.push({ elem: u, handlers: o }) } return n.length > l && s.push({ elem: this, handlers: n.slice(l) }), s }, fix: function (e) { if (e[x.expando]) return e; var t, n, r, i = e.type, o = e, s = this.fixHooks[i]; s || (this.fixHooks[i] = s = tt.test(i) ? this.mouseHooks : et.test(i) ? this.keyHooks : {}), r = s.props ? this.props.concat(s.props) : this.props, e = new x.Event(o), t = r.length; while (t--) n = r[t], e[n] = o[n]; return e.target || (e.target = o.srcElement || a), 3 === e.target.nodeType && (e.target = e.target.parentNode), e.metaKey = !!e.metaKey, s.filter ? s.filter(e, o) : e }, props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), fixHooks: {}, keyHooks: { props: "char charCode key keyCode".split(" "), filter: function (e, t) { return null == e.which && (e.which = null != t.charCode ? t.charCode : t.keyCode), e } }, mouseHooks: { props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), filter: function (e, n) { var r, i, o, s = n.button, l = n.fromElement; return null == e.pageX && null != n.clientX && (i = e.target.ownerDocument || a, o = i.documentElement, r = i.body, e.pageX = n.clientX + (o && o.scrollLeft || r && r.scrollLeft || 0) - (o && o.clientLeft || r && r.clientLeft || 0), e.pageY = n.clientY + (o && o.scrollTop || r && r.scrollTop || 0) - (o && o.clientTop || r && r.clientTop || 0)), !e.relatedTarget && l && (e.relatedTarget = l === e.target ? n.toElement : l), e.which || s === t || (e.which = 1 & s ? 1 : 2 & s ? 3 : 4 & s ? 2 : 0), e } }, special: { load: { noBubble: !0 }, focus: { trigger: function () { if (this !== at() && this.focus) try { return this.focus(), !1 } catch (e) { } }, delegateType: "focusin" }, blur: { trigger: function () { return this === at() && this.blur ? (this.blur(), !1) : t }, delegateType: "focusout" }, click: { trigger: function () { return x.nodeName(this, "input") && "checkbox" === this.type && this.click ? (this.click(), !1) : t }, _default: function (e) { return x.nodeName(e.target, "a") } }, beforeunload: { postDispatch: function (e) { e.result !== t && (e.originalEvent.returnValue = e.result) } } }, simulate: function (e, t, n, r) { var i = x.extend(new x.Event, n, { type: e, isSimulated: !0, originalEvent: {} }); r ? x.event.trigger(i, null, t) : x.event.dispatch.call(t, i), i.isDefaultPrevented() && n.preventDefault() } }, x.removeEvent = a.removeEventListener ? function (e, t, n) { e.removeEventListener && e.removeEventListener(t, n, !1) } : function (e, t, n) { var r = "on" + t; e.detachEvent && (typeof e[r] === i && (e[r] = null), e.detachEvent(r, n)) }, x.Event = function (e, n) { return this instanceof x.Event ? (e && e.type ? (this.originalEvent = e, this.type = e.type, this.isDefaultPrevented = e.defaultPrevented || e.returnValue === !1 || e.getPreventDefault && e.getPreventDefault() ? it : ot) : this.type = e, n && x.extend(this, n), this.timeStamp = e && e.timeStamp || x.now(), this[x.expando] = !0, t) : new x.Event(e, n) }, x.Event.prototype = { isDefaultPrevented: ot, isPropagationStopped: ot, isImmediatePropagationStopped: ot, preventDefault: function () { var e = this.originalEvent; this.isDefaultPrevented = it, e && (e.preventDefault ? e.preventDefault() : e.returnValue = !1) }, stopPropagation: function () { var e = this.originalEvent; this.isPropagationStopped = it, e && (e.stopPropagation && e.stopPropagation(), e.cancelBubble = !0) }, stopImmediatePropagation: function () { this.isImmediatePropagationStopped = it, this.stopPropagation() } }, x.each({ mouseenter: "mouseover", mouseleave: "mouseout" }, function (e, t) { x.event.special[e] = { delegateType: t, bindType: t, handle: function (e) { var n, r = this, i = e.relatedTarget, o = e.handleObj; return (!i || i !== r && !x.contains(r, i)) && (e.type = o.origType, n = o.handler.apply(this, arguments), e.type = t), n } } }), x.support.submitBubbles || (x.event.special.submit = { setup: function () { return x.nodeName(this, "form") ? !1 : (x.event.add(this, "click._submit keypress._submit", function (e) { var n = e.target, r = x.nodeName(n, "input") || x.nodeName(n, "button") ? n.form : t; r && !x._data(r, "submitBubbles") && (x.event.add(r, "submit._submit", function (e) { e._submit_bubble = !0 }), x._data(r, "submitBubbles", !0)) }), t) }, postDispatch: function (e) { e._submit_bubble && (delete e._submit_bubble, this.parentNode && !e.isTrigger && x.event.simulate("submit", this.parentNode, e, !0)) }, teardown: function () { return x.nodeName(this, "form") ? !1 : (x.event.remove(this, "._submit"), t) } }), x.support.changeBubbles || (x.event.special.change = { setup: function () { return Z.test(this.nodeName) ? (("checkbox" === this.type || "radio" === this.type) && (x.event.add(this, "propertychange._change", function (e) { "checked" === e.originalEvent.propertyName && (this._just_changed = !0) }), x.event.add(this, "click._change", function (e) { this._just_changed && !e.isTrigger && (this._just_changed = !1), x.event.simulate("change", this, e, !0) })), !1) : (x.event.add(this, "beforeactivate._change", function (e) { var t = e.target; Z.test(t.nodeName) && !x._data(t, "changeBubbles") && (x.event.add(t, "change._change", function (e) { !this.parentNode || e.isSimulated || e.isTrigger || x.event.simulate("change", this.parentNode, e, !0) }), x._data(t, "changeBubbles", !0)) }), t) }, handle: function (e) { var n = e.target; return this !== n || e.isSimulated || e.isTrigger || "radio" !== n.type && "checkbox" !== n.type ? e.handleObj.handler.apply(this, arguments) : t }, teardown: function () { return x.event.remove(this, "._change"), !Z.test(this.nodeName) } }), x.support.focusinBubbles || x.each({ focus: "focusin", blur: "focusout" }, function (e, t) { var n = 0, r = function (e) { x.event.simulate(t, e.target, x.event.fix(e), !0) }; x.event.special[t] = { setup: function () { 0 === n++ && a.addEventListener(e, r, !0) }, teardown: function () { 0 === --n && a.removeEventListener(e, r, !0) } } }), x.fn.extend({ on: function (e, n, r, i, o) { var a, s; if ("object" == typeof e) { "string" != typeof n && (r = r || n, n = t); for (a in e) this.on(a, n, r, e[a], o); return this } if (null == r && null == i ? (i = n, r = n = t) : null == i && ("string" == typeof n ? (i = r, r = t) : (i = r, r = n, n = t)), i === !1) i = ot; else if (!i) return this; return 1 === o && (s = i, i = function (e) { return x().off(e), s.apply(this, arguments) }, i.guid = s.guid || (s.guid = x.guid++)), this.each(function () { x.event.add(this, e, i, r, n) }) }, one: function (e, t, n, r) { return this.on(e, t, n, r, 1) }, off: function (e, n, r) { var i, o; if (e && e.preventDefault && e.handleObj) return i = e.handleObj, x(e.delegateTarget).off(i.namespace ? i.origType + "." + i.namespace : i.origType, i.selector, i.handler), this; if ("object" == typeof e) { for (o in e) this.off(o, n, e[o]); return this } return (n === !1 || "function" == typeof n) && (r = n, n = t), r === !1 && (r = ot), this.each(function () { x.event.remove(this, e, r, n) }) }, trigger: function (e, t) { return this.each(function () { x.event.trigger(e, t, this) }) }, triggerHandler: function (e, n) { var r = this[0]; return r ? x.event.trigger(e, n, r, !0) : t } }); var st = /^.[^:#\[\.,]*$/, lt = /^(?:parents|prev(?:Until|All))/, ut = x.expr.match.needsContext, ct = { children: !0, contents: !0, next: !0, prev: !0 }; x.fn.extend({ find: function (e) { var t, n = [], r = this, i = r.length; if ("string" != typeof e) return this.pushStack(x(e).filter(function () { for (t = 0; i > t; t++) if (x.contains(r[t], this)) return !0 })); for (t = 0; i > t; t++) x.find(e, r[t], n); return n = this.pushStack(i > 1 ? x.unique(n) : n), n.selector = this.selector ? this.selector + " " + e : e, n }, has: function (e) { var t, n = x(e, this), r = n.length; return this.filter(function () { for (t = 0; r > t; t++) if (x.contains(this, n[t])) return !0 }) }, not: function (e) { return this.pushStack(ft(this, e || [], !0)) }, filter: function (e) { return this.pushStack(ft(this, e || [], !1)) }, is: function (e) { return !!ft(this, "string" == typeof e && ut.test(e) ? x(e) : e || [], !1).length }, closest: function (e, t) { var n, r = 0, i = this.length, o = [], a = ut.test(e) || "string" != typeof e ? x(e, t || this.context) : 0; for (; i > r; r++) for (n = this[r]; n && n !== t; n = n.parentNode) if (11 > n.nodeType && (a ? a.index(n) > -1 : 1 === n.nodeType && x.find.matchesSelector(n, e))) { n = o.push(n); break } return this.pushStack(o.length > 1 ? x.unique(o) : o) }, index: function (e) { return e ? "string" == typeof e ? x.inArray(this[0], x(e)) : x.inArray(e.jquery ? e[0] : e, this) : this[0] && this[0].parentNode ? this.first().prevAll().length : -1 }, add: function (e, t) { var n = "string" == typeof e ? x(e, t) : x.makeArray(e && e.nodeType ? [e] : e), r = x.merge(this.get(), n); return this.pushStack(x.unique(r)) }, addBack: function (e) { return this.add(null == e ? this.prevObject : this.prevObject.filter(e)) } }); function pt(e, t) { do e = e[t]; while (e && 1 !== e.nodeType); return e } x.each({ parent: function (e) { var t = e.parentNode; return t && 11 !== t.nodeType ? t : null }, parents: function (e) { return x.dir(e, "parentNode") }, parentsUntil: function (e, t, n) { return x.dir(e, "parentNode", n) }, next: function (e) { return pt(e, "nextSibling") }, prev: function (e) { return pt(e, "previousSibling") }, nextAll: function (e) { return x.dir(e, "nextSibling") }, prevAll: function (e) { return x.dir(e, "previousSibling") }, nextUntil: function (e, t, n) { return x.dir(e, "nextSibling", n) }, prevUntil: function (e, t, n) { return x.dir(e, "previousSibling", n) }, siblings: function (e) { return x.sibling((e.parentNode || {}).firstChild, e) }, children: function (e) { return x.sibling(e.firstChild) }, contents: function (e) { return x.nodeName(e, "iframe") ? e.contentDocument || e.contentWindow.document : x.merge([], e.childNodes) } }, function (e, t) { x.fn[e] = function (n, r) { var i = x.map(this, t, n); return "Until" !== e.slice(-5) && (r = n), r && "string" == typeof r && (i = x.filter(r, i)), this.length > 1 && (ct[e] || (i = x.unique(i)), lt.test(e) && (i = i.reverse())), this.pushStack(i) } }), x.extend({ filter: function (e, t, n) { var r = t[0]; return n && (e = ":not(" + e + ")"), 1 === t.length && 1 === r.nodeType ? x.find.matchesSelector(r, e) ? [r] : [] : x.find.matches(e, x.grep(t, function (e) { return 1 === e.nodeType })) }, dir: function (e, n, r) { var i = [], o = e[n]; while (o && 9 !== o.nodeType && (r === t || 1 !== o.nodeType || !x(o).is(r))) 1 === o.nodeType && i.push(o), o = o[n]; return i }, sibling: function (e, t) { var n = []; for (; e; e = e.nextSibling) 1 === e.nodeType && e !== t && n.push(e); return n } }); function ft(e, t, n) { if (x.isFunction(t)) return x.grep(e, function (e, r) { return !!t.call(e, r, e) !== n }); if (t.nodeType) return x.grep(e, function (e) { return e === t !== n }); if ("string" == typeof t) { if (st.test(t)) return x.filter(t, e, n); t = x.filter(t, e) } return x.grep(e, function (e) { return x.inArray(e, t) >= 0 !== n }) } function dt(e) { var t = ht.split("|"), n = e.createDocumentFragment(); if (n.createElement) while (t.length) n.createElement(t.pop()); return n } var ht = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", gt = / jQuery\d+="(?:null|\d+)"/g, mt = RegExp("<(?:" + ht + ")[\\s/>]", "i"), yt = /^\s+/, vt = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, bt = /<([\w:]+)/, xt = /\s*$/g, At = { option: [1, ""], legend: [1, "
              ", "
              "], area: [1, "", ""], param: [1, "", ""], thead: [1, "", "
              "], tr: [2, "", "
              "], col: [2, "", "
              "], td: [3, "", "
              "], _default: x.support.htmlSerialize ? [0, "", ""] : [1, "X
              ", "
              "] }, jt = dt(a), Dt = jt.appendChild(a.createElement("div")); At.optgroup = At.option, At.tbody = At.tfoot = At.colgroup = At.caption = At.thead, At.th = At.td, x.fn.extend({ text: function (e) { return x.access(this, function (e) { return e === t ? x.text(this) : this.empty().append((this[0] && this[0].ownerDocument || a).createTextNode(e)) }, null, e, arguments.length) }, append: function () { return this.domManip(arguments, function (e) { if (1 === this.nodeType || 11 === this.nodeType || 9 === this.nodeType) { var t = Lt(this, e); t.appendChild(e) } }) }, prepend: function () { return this.domManip(arguments, function (e) { if (1 === this.nodeType || 11 === this.nodeType || 9 === this.nodeType) { var t = Lt(this, e); t.insertBefore(e, t.firstChild) } }) }, before: function () { return this.domManip(arguments, function (e) { this.parentNode && this.parentNode.insertBefore(e, this) }) }, after: function () { return this.domManip(arguments, function (e) { this.parentNode && this.parentNode.insertBefore(e, this.nextSibling) }) }, remove: function (e, t) { var n, r = e ? x.filter(e, this) : this, i = 0; for (; null != (n = r[i]) ; i++) t || 1 !== n.nodeType || x.cleanData(Ft(n)), n.parentNode && (t && x.contains(n.ownerDocument, n) && _t(Ft(n, "script")), n.parentNode.removeChild(n)); return this }, empty: function () { var e, t = 0; for (; null != (e = this[t]) ; t++) { 1 === e.nodeType && x.cleanData(Ft(e, !1)); while (e.firstChild) e.removeChild(e.firstChild); e.options && x.nodeName(e, "select") && (e.options.length = 0) } return this }, clone: function (e, t) { return e = null == e ? !1 : e, t = null == t ? e : t, this.map(function () { return x.clone(this, e, t) }) }, html: function (e) { return x.access(this, function (e) { var n = this[0] || {}, r = 0, i = this.length; if (e === t) return 1 === n.nodeType ? n.innerHTML.replace(gt, "") : t; if (!("string" != typeof e || Tt.test(e) || !x.support.htmlSerialize && mt.test(e) || !x.support.leadingWhitespace && yt.test(e) || At[(bt.exec(e) || ["", ""])[1].toLowerCase()])) { e = e.replace(vt, "<$1>"); try { for (; i > r; r++) n = this[r] || {}, 1 === n.nodeType && (x.cleanData(Ft(n, !1)), n.innerHTML = e); n = 0 } catch (o) { } } n && this.empty().append(e) }, null, e, arguments.length) }, replaceWith: function () { var e = x.map(this, function (e) { return [e.nextSibling, e.parentNode] }), t = 0; return this.domManip(arguments, function (n) { var r = e[t++], i = e[t++]; i && (r && r.parentNode !== i && (r = this.nextSibling), x(this).remove(), i.insertBefore(n, r)) }, !0), t ? this : this.remove() }, detach: function (e) { return this.remove(e, !0) }, domManip: function (e, t, n) { e = d.apply([], e); var r, i, o, a, s, l, u = 0, c = this.length, p = this, f = c - 1, h = e[0], g = x.isFunction(h); if (g || !(1 >= c || "string" != typeof h || x.support.checkClone) && Nt.test(h)) return this.each(function (r) { var i = p.eq(r); g && (e[0] = h.call(this, r, i.html())), i.domManip(e, t, n) }); if (c && (l = x.buildFragment(e, this[0].ownerDocument, !1, !n && this), r = l.firstChild, 1 === l.childNodes.length && (l = r), r)) { for (a = x.map(Ft(l, "script"), Ht), o = a.length; c > u; u++) i = l, u !== f && (i = x.clone(i, !0, !0), o && x.merge(a, Ft(i, "script"))), t.call(this[u], i, u); if (o) for (s = a[a.length - 1].ownerDocument, x.map(a, qt), u = 0; o > u; u++) i = a[u], kt.test(i.type || "") && !x._data(i, "globalEval") && x.contains(s, i) && (i.src ? x._evalUrl(i.src) : x.globalEval((i.text || i.textContent || i.innerHTML || "").replace(St, ""))); l = r = null } return this } }); function Lt(e, t) { return x.nodeName(e, "table") && x.nodeName(1 === t.nodeType ? t : t.firstChild, "tr") ? e.getElementsByTagName("tbody")[0] || e.appendChild(e.ownerDocument.createElement("tbody")) : e } function Ht(e) { return e.type = (null !== x.find.attr(e, "type")) + "/" + e.type, e } function qt(e) { var t = Et.exec(e.type); return t ? e.type = t[1] : e.removeAttribute("type"), e } function _t(e, t) { var n, r = 0; for (; null != (n = e[r]) ; r++) x._data(n, "globalEval", !t || x._data(t[r], "globalEval")) } function Mt(e, t) { if (1 === t.nodeType && x.hasData(e)) { var n, r, i, o = x._data(e), a = x._data(t, o), s = o.events; if (s) { delete a.handle, a.events = {}; for (n in s) for (r = 0, i = s[n].length; i > r; r++) x.event.add(t, n, s[n][r]) } a.data && (a.data = x.extend({}, a.data)) } } function Ot(e, t) { var n, r, i; if (1 === t.nodeType) { if (n = t.nodeName.toLowerCase(), !x.support.noCloneEvent && t[x.expando]) { i = x._data(t); for (r in i.events) x.removeEvent(t, r, i.handle); t.removeAttribute(x.expando) } "script" === n && t.text !== e.text ? (Ht(t).text = e.text, qt(t)) : "object" === n ? (t.parentNode && (t.outerHTML = e.outerHTML), x.support.html5Clone && e.innerHTML && !x.trim(t.innerHTML) && (t.innerHTML = e.innerHTML)) : "input" === n && Ct.test(e.type) ? (t.defaultChecked = t.checked = e.checked, t.value !== e.value && (t.value = e.value)) : "option" === n ? t.defaultSelected = t.selected = e.defaultSelected : ("input" === n || "textarea" === n) && (t.defaultValue = e.defaultValue) } } x.each({ appendTo: "append", prependTo: "prepend", insertBefore: "before", insertAfter: "after", replaceAll: "replaceWith" }, function (e, t) { x.fn[e] = function (e) { var n, r = 0, i = [], o = x(e), a = o.length - 1; for (; a >= r; r++) n = r === a ? this : this.clone(!0), x(o[r])[t](n), h.apply(i, n.get()); return this.pushStack(i) } }); function Ft(e, n) { var r, o, a = 0, s = typeof e.getElementsByTagName !== i ? e.getElementsByTagName(n || "*") : typeof e.querySelectorAll !== i ? e.querySelectorAll(n || "*") : t; if (!s) for (s = [], r = e.childNodes || e; null != (o = r[a]) ; a++) !n || x.nodeName(o, n) ? s.push(o) : x.merge(s, Ft(o, n)); return n === t || n && x.nodeName(e, n) ? x.merge([e], s) : s } function Bt(e) { Ct.test(e.type) && (e.defaultChecked = e.checked) } x.extend({ + clone: function (e, t, n) { var r, i, o, a, s, l = x.contains(e.ownerDocument, e); if (x.support.html5Clone || x.isXMLDoc(e) || !mt.test("<" + e.nodeName + ">") ? o = e.cloneNode(!0) : (Dt.innerHTML = e.outerHTML, Dt.removeChild(o = Dt.firstChild)), !(x.support.noCloneEvent && x.support.noCloneChecked || 1 !== e.nodeType && 11 !== e.nodeType || x.isXMLDoc(e))) for (r = Ft(o), s = Ft(e), a = 0; null != (i = s[a]) ; ++a) r[a] && Ot(i, r[a]); if (t) if (n) for (s = s || Ft(e), r = r || Ft(o), a = 0; null != (i = s[a]) ; a++) Mt(i, r[a]); else Mt(e, o); return r = Ft(o, "script"), r.length > 0 && _t(r, !l && Ft(e, "script")), r = s = i = null, o }, buildFragment: function (e, t, n, r) { var i, o, a, s, l, u, c, p = e.length, f = dt(t), d = [], h = 0; for (; p > h; h++) if (o = e[h], o || 0 === o) if ("object" === x.type(o)) x.merge(d, o.nodeType ? [o] : o); else if (wt.test(o)) { s = s || f.appendChild(t.createElement("div")), l = (bt.exec(o) || ["", ""])[1].toLowerCase(), c = At[l] || At._default, s.innerHTML = c[1] + o.replace(vt, "<$1>") + c[2], i = c[0]; while (i--) s = s.lastChild; if (!x.support.leadingWhitespace && yt.test(o) && d.push(t.createTextNode(yt.exec(o)[0])), !x.support.tbody) { o = "table" !== l || xt.test(o) ? "" !== c[1] || xt.test(o) ? 0 : s : s.firstChild, i = o && o.childNodes.length; while (i--) x.nodeName(u = o.childNodes[i], "tbody") && !u.childNodes.length && o.removeChild(u) } x.merge(d, s.childNodes), s.textContent = ""; while (s.firstChild) s.removeChild(s.firstChild); s = f.lastChild } else d.push(t.createTextNode(o)); s && f.removeChild(s), x.support.appendChecked || x.grep(Ft(d, "input"), Bt), h = 0; while (o = d[h++]) if ((!r || -1 === x.inArray(o, r)) && (a = x.contains(o.ownerDocument, o), s = Ft(f.appendChild(o), "script"), a && _t(s), n)) { i = 0; while (o = s[i++]) kt.test(o.type || "") && n.push(o) } return s = null, f }, cleanData: function (e, t) { + var n, r, o, a, s = 0, l = x.expando, u = x.cache, c = x.support.deleteExpando, f = x.event.special; for (; null != (n = e[s]) ; s++) if ((t || x.acceptData(n)) && (o = n[l], a = o && u[o])) { + if (a.events) for (r in a.events) f[r] ? x.event.remove(n, r) : x.removeEvent(n, r, a.handle); + u[o] && (delete u[o], c ? delete n[l] : typeof n.removeAttribute !== i ? n.removeAttribute(l) : n[l] = null, p.push(o)) + } + }, _evalUrl: function (e) { return x.ajax({ url: e, type: "GET", dataType: "script", async: !1, global: !1, "throws": !0 }) } + }), x.fn.extend({ wrapAll: function (e) { if (x.isFunction(e)) return this.each(function (t) { x(this).wrapAll(e.call(this, t)) }); if (this[0]) { var t = x(e, this[0].ownerDocument).eq(0).clone(!0); this[0].parentNode && t.insertBefore(this[0]), t.map(function () { var e = this; while (e.firstChild && 1 === e.firstChild.nodeType) e = e.firstChild; return e }).append(this) } return this }, wrapInner: function (e) { return x.isFunction(e) ? this.each(function (t) { x(this).wrapInner(e.call(this, t)) }) : this.each(function () { var t = x(this), n = t.contents(); n.length ? n.wrapAll(e) : t.append(e) }) }, wrap: function (e) { var t = x.isFunction(e); return this.each(function (n) { x(this).wrapAll(t ? e.call(this, n) : e) }) }, unwrap: function () { return this.parent().each(function () { x.nodeName(this, "body") || x(this).replaceWith(this.childNodes) }).end() } }); var Pt, Rt, Wt, $t = /alpha\([^)]*\)/i, It = /opacity\s*=\s*([^)]*)/, zt = /^(top|right|bottom|left)$/, Xt = /^(none|table(?!-c[ea]).+)/, Ut = /^margin/, Vt = RegExp("^(" + w + ")(.*)$", "i"), Yt = RegExp("^(" + w + ")(?!px)[a-z%]+$", "i"), Jt = RegExp("^([+-])=(" + w + ")", "i"), Gt = { BODY: "block" }, Qt = { position: "absolute", visibility: "hidden", display: "block" }, Kt = { letterSpacing: 0, fontWeight: 400 }, Zt = ["Top", "Right", "Bottom", "Left"], en = ["Webkit", "O", "Moz", "ms"]; function tn(e, t) { if (t in e) return t; var n = t.charAt(0).toUpperCase() + t.slice(1), r = t, i = en.length; while (i--) if (t = en[i] + n, t in e) return t; return r } function nn(e, t) { return e = t || e, "none" === x.css(e, "display") || !x.contains(e.ownerDocument, e) } function rn(e, t) { var n, r, i, o = [], a = 0, s = e.length; for (; s > a; a++) r = e[a], r.style && (o[a] = x._data(r, "olddisplay"), n = r.style.display, t ? (o[a] || "none" !== n || (r.style.display = ""), "" === r.style.display && nn(r) && (o[a] = x._data(r, "olddisplay", ln(r.nodeName)))) : o[a] || (i = nn(r), (n && "none" !== n || !i) && x._data(r, "olddisplay", i ? n : x.css(r, "display")))); for (a = 0; s > a; a++) r = e[a], r.style && (t && "none" !== r.style.display && "" !== r.style.display || (r.style.display = t ? o[a] || "" : "none")); return e } x.fn.extend({ css: function (e, n) { return x.access(this, function (e, n, r) { var i, o, a = {}, s = 0; if (x.isArray(n)) { for (o = Rt(e), i = n.length; i > s; s++) a[n[s]] = x.css(e, n[s], !1, o); return a } return r !== t ? x.style(e, n, r) : x.css(e, n) }, e, n, arguments.length > 1) }, show: function () { return rn(this, !0) }, hide: function () { return rn(this) }, toggle: function (e) { return "boolean" == typeof e ? e ? this.show() : this.hide() : this.each(function () { nn(this) ? x(this).show() : x(this).hide() }) } }), x.extend({ cssHooks: { opacity: { get: function (e, t) { if (t) { var n = Wt(e, "opacity"); return "" === n ? "1" : n } } } }, cssNumber: { columnCount: !0, fillOpacity: !0, fontWeight: !0, lineHeight: !0, opacity: !0, order: !0, orphans: !0, widows: !0, zIndex: !0, zoom: !0 }, cssProps: { "float": x.support.cssFloat ? "cssFloat" : "styleFloat" }, style: function (e, n, r, i) { if (e && 3 !== e.nodeType && 8 !== e.nodeType && e.style) { var o, a, s, l = x.camelCase(n), u = e.style; if (n = x.cssProps[l] || (x.cssProps[l] = tn(u, l)), s = x.cssHooks[n] || x.cssHooks[l], r === t) return s && "get" in s && (o = s.get(e, !1, i)) !== t ? o : u[n]; if (a = typeof r, "string" === a && (o = Jt.exec(r)) && (r = (o[1] + 1) * o[2] + parseFloat(x.css(e, n)), a = "number"), !(null == r || "number" === a && isNaN(r) || ("number" !== a || x.cssNumber[l] || (r += "px"), x.support.clearCloneStyle || "" !== r || 0 !== n.indexOf("background") || (u[n] = "inherit"), s && "set" in s && (r = s.set(e, r, i)) === t))) try { u[n] = r } catch (c) { } } }, css: function (e, n, r, i) { var o, a, s, l = x.camelCase(n); return n = x.cssProps[l] || (x.cssProps[l] = tn(e.style, l)), s = x.cssHooks[n] || x.cssHooks[l], s && "get" in s && (a = s.get(e, !0, r)), a === t && (a = Wt(e, n, i)), "normal" === a && n in Kt && (a = Kt[n]), "" === r || r ? (o = parseFloat(a), r === !0 || x.isNumeric(o) ? o || 0 : a) : a } }), e.getComputedStyle ? (Rt = function (t) { return e.getComputedStyle(t, null) }, Wt = function (e, n, r) { var i, o, a, s = r || Rt(e), l = s ? s.getPropertyValue(n) || s[n] : t, u = e.style; return s && ("" !== l || x.contains(e.ownerDocument, e) || (l = x.style(e, n)), Yt.test(l) && Ut.test(n) && (i = u.width, o = u.minWidth, a = u.maxWidth, u.minWidth = u.maxWidth = u.width = l, l = s.width, u.width = i, u.minWidth = o, u.maxWidth = a)), l }) : a.documentElement.currentStyle && (Rt = function (e) { return e.currentStyle }, Wt = function (e, n, r) { var i, o, a, s = r || Rt(e), l = s ? s[n] : t, u = e.style; return null == l && u && u[n] && (l = u[n]), Yt.test(l) && !zt.test(n) && (i = u.left, o = e.runtimeStyle, a = o && o.left, a && (o.left = e.currentStyle.left), u.left = "fontSize" === n ? "1em" : l, l = u.pixelLeft + "px", u.left = i, a && (o.left = a)), "" === l ? "auto" : l }); function on(e, t, n) { var r = Vt.exec(t); return r ? Math.max(0, r[1] - (n || 0)) + (r[2] || "px") : t } function an(e, t, n, r, i) { var o = n === (r ? "border" : "content") ? 4 : "width" === t ? 1 : 0, a = 0; for (; 4 > o; o += 2) "margin" === n && (a += x.css(e, n + Zt[o], !0, i)), r ? ("content" === n && (a -= x.css(e, "padding" + Zt[o], !0, i)), "margin" !== n && (a -= x.css(e, "border" + Zt[o] + "Width", !0, i))) : (a += x.css(e, "padding" + Zt[o], !0, i), "padding" !== n && (a += x.css(e, "border" + Zt[o] + "Width", !0, i))); return a } function sn(e, t, n) { var r = !0, i = "width" === t ? e.offsetWidth : e.offsetHeight, o = Rt(e), a = x.support.boxSizing && "border-box" === x.css(e, "boxSizing", !1, o); if (0 >= i || null == i) { if (i = Wt(e, t, o), (0 > i || null == i) && (i = e.style[t]), Yt.test(i)) return i; r = a && (x.support.boxSizingReliable || i === e.style[t]), i = parseFloat(i) || 0 } return i + an(e, t, n || (a ? "border" : "content"), r, o) + "px" } function ln(e) { var t = a, n = Gt[e]; return n || (n = un(e, t), "none" !== n && n || (Pt = (Pt || x("\'/>'); + $linkInputContainer.append($linkInput); + var $sizeContainer = $('
              '); + var $widthInput = $(''); + var $heightInput = $(''); + $sizeContainer.append(' ' + lang.width + ' ') + .append($widthInput) + .append(' px    ') + .append(' ' + lang.height + ' ') + .append($heightInput) + .append(' px '); + var $btnContainer = $('
              '); + var $howToCopy = $('如何复制视频链接?'); + var $btnSubmit = $(''); + var $btnCancel = $(''); + $btnContainer.append($howToCopy).append($btnSubmit).append($btnCancel); + $content.append($linkInputContainer).append($sizeContainer).append($btnContainer); + + // 取消按钮 + $btnCancel.click(function (e) { + e.preventDefault(); + $linkInput.val(''); + menu.dropPanel.hide(); + }); + + // 确定按钮 + $btnSubmit.click(function (e) { + e.preventDefault(); + var link = $.trim($linkInput.val()); + var $link; + var width = parseInt($widthInput.val()); + var height = parseInt($heightInput.val()); + var $div = $('
              '); + var html = '

              {content}

              '; + + // 验证数据 + if (!link) { + menu.dropPanel.focusFirstInput(); + return; + } + + if (!reg.test(link)) { + alert('视频链接格式错误!'); + menu.dropPanel.focusFirstInput(); + return; + } + + if (isNaN(width) || isNaN(height)) { + alert('宽度或高度不是数字!'); + return; + } + + $link = $(link); + + // 设置高度和宽度 + $link.attr('width', width) + .attr('height', height); + + // 拼接字符串 + html = html.replace('{content}', $div.append($link).html()); + + // 执行命令 + editor.command(e, 'insertHtml', html); + $linkInput.val(''); + }); + + // 创建panel + menu.dropPanel = new E.DropPanel(editor, menu, { + $content: $content, + width: 400 + }); + + // 增加到editor对象中 + editor.menus[menuId] = menu; + }); + +}); +// location 菜单 +_e(function (E, $) { + + // 判断浏览器的 input 是否支持 keyup + var inputKeyup = (function (input) { + return 'onkeyup' in input; + })(document.createElement('input')); + + // 百度地图的key + E.baiduMapAk = 'TVhjYjq1ICT2qqL5LdS8mwas'; + + // 一个页面中,如果有多个编辑器,地图会出现问题。这个参数记录一下,如果超过 1 就提示 + E.numberOfLocation = 0; + + E.createMenu(function (check) { + var menuId = 'location'; + if (!check(menuId)) { + return; + } + + if (++E.numberOfLocation > 1) { + E.error('目前不支持在一个页面多个编辑器上同时使用地图,可通过自定义菜单配置去掉地图菜单'); + return; + } + + var editor = this; + var config = editor.config; + var lang = config.lang; + var ak = config.mapAk; + + // 地图的变量存储到这个地方 + editor.mapData = {}; + var mapData = editor.mapData; + + // ---------- 地图事件 ---------- + mapData.markers = []; + mapData.mapContainerId = 'map' + E.random(); + + mapData.clearLocations = function () { + var map = mapData.map; + if (!map) { + return; + } + map.clearOverlays(); + + //同时,清空marker数组 + mapData.markers = []; + }; + + mapData.searchMap = function () { + var map = mapData.map; + if (!map) { + return; + } + + var BMap = window.BMap; + var cityName = $cityInput.val(); + var locationName = $searchInput.val(); + var myGeo, marker; + + if(cityName !== ''){ + if(!locationName || locationName === ''){ + map.centerAndZoom(cityName, 11); + } + + //地址解析 + if(locationName && locationName !== ''){ + myGeo = new BMap.Geocoder(); + // 将地址解析结果显示在地图上,并调整地图视野 + myGeo.getPoint(locationName, function(point){ + if (point) { + map.centerAndZoom(point, 13); + marker = new BMap.Marker(point); + map.addOverlay(marker); + marker.enableDragging(); //允许拖拽 + mapData.markers.push(marker); //将marker加入到数组中 + }else{ + // alert('未找到'); + map.centerAndZoom(cityName, 11); //找不到则重新定位到城市 + } + }, cityName); + } + } // if(cityName !== '') + }; + + // load script 之后的 callback + var hasCallback = false; + window.baiduMapCallBack = function(){ + // 避免重复加载 + if (hasCallback) { + return; + } else { + hasCallback = true; + } + + var BMap = window.BMap; + if (!mapData.map) { + // 创建Map实例 + mapData.map = new BMap.Map(mapData.mapContainerId); + } + var map = mapData.map; + + map.centerAndZoom(new BMap.Point(116.404, 39.915), 11); // 初始化地图,设置中心点坐标和地图级别 + map.addControl(new BMap.MapTypeControl()); //添加地图类型控件 + map.setCurrentCity("北京"); // 设置地图显示的城市 此项是必须设置的 + map.enableScrollWheelZoom(true); //开启鼠标滚轮缩放 + + //根据IP定位 + function locationFun(result){ + var cityName = result.name; + map.setCenter(cityName); + + // 设置城市名称 + $cityInput.val(cityName); + if (E.placeholder) { + $searchInput.focus(); + } + var timeoutId, searchFn; + if (inputKeyup) { + // 并绑定搜索事件 - input 支持 keyup + searchFn = function (e) { + if (e.type === 'keyup' && e.keyCode === 13) { + e.preventDefault(); + } + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(mapData.searchMap, 500); + }; + $cityInput.on('keyup change paste', searchFn); + $searchInput.on('keyup change paste', searchFn); + } else { + // 并绑定搜索事件 - input 不支持 keyup + searchFn = function () { + if (!$content.is(':visible')) { + // panel 不显示了,就不用再监控了 + clearTimeout(timeoutId); + return; + } + + var currentCity = ''; + var currentSearch = ''; + var city = $cityInput.val(); + var search = $searchInput.val(); + + if (city !== currentCity || search !== currentSearch) { + // 刚获取的数据和之前的数据不一致,执行查询 + mapData.searchMap(); + // 更新数据 + currentCity = city; + currentSearch = search; + } + + // 继续监控 + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(searchFn, 1000); + }; + // 开始监控 + timeoutId = setTimeout(searchFn, 1000); + } + } + var myCity = new BMap.LocalCity(); + myCity.get(locationFun); + + //鼠标点击,创建位置 + map.addEventListener("click", function(e){ + var marker = new BMap.Marker(new BMap.Point(e.point.lng, e.point.lat)); + map.addOverlay(marker); + marker.enableDragging(); + mapData.markers.push(marker); //加入到数组中 + }, false); + }; + + mapData.loadMapScript = function () { + var script = document.createElement("script"); + script.type = "text/javascript"; + script.src = "https://api.map.baidu.com/api?v=2.0&ak=" + ak + "&s=1&callback=baiduMapCallBack"; // baiduMapCallBack是一个本地函数 + try { + // IE10- 报错 + document.body.appendChild(script); + } catch (ex) { + E.error('加载地图过程中发生错误'); + } + }; + + // 初始化地图 + mapData.initMap = function () { + if (window.BMap) { + // 不是第一次,直接处理地图即可 + window.baiduMapCallBack(); + } else { + // 第一次,先加载地图 script,再处理地图(script加载完自动执行处理) + mapData.loadMapScript(); + } + }; + + // ---------- 创建 menu 对象 ---------- + + var menu = new E.Menu({ + editor: editor, + id: menuId, + title: lang.location + }); + + editor.menus[menuId] = menu; + + // ---------- 构建UI ---------- + + // panel content + var $content = $('
              '); + + // 搜索框 + var $inputContainer = $('
              '); + var $cityInput = $(''); + $cityInput.css({ + width: '80px', + 'text-align': 'center' + }); + var $searchInput = $(''); + $searchInput.css({ + width: '300px', + 'margin-left': '10px' + }).attr('placeholder', lang.searchlocation); + var $clearBtn = $(''); + $inputContainer.append($clearBtn) + .append($cityInput) + .append($searchInput); + $content.append($inputContainer); + + // 清除位置按钮 + $clearBtn.click(function (e) { + $searchInput.val(''); + $searchInput.focus(); + mapData.clearLocations(); + e.preventDefault(); + }); + + // 地图 + var $map = $('
              '); + $map.css({ + height: '260px', + width: '100%', + position: 'relative', + 'margin-top': '10px', + border: '1px solid #f1f1f1' + }); + var $mapLoading = $('' + lang.loading + ''); + $mapLoading.css({ + position: 'absolute', + width: '100px', + 'text-align': 'center', + top: '45%', + left: '50%', + 'margin-left': '-50px' + }); + $map.append($mapLoading); + $content.append($map); + + // 按钮 + var $btnContainer = $('
              '); + var $btnSubmit = $(''); + var $btnCancel = $(''); + var $checkLabel = $(''); + var $check = $(''); + $checkLabel.append($check).append(' ' + lang.dynamicMap + ''); + $btnContainer.append($checkLabel) + .append($btnSubmit) + .append($btnCancel); + $content.append($btnContainer); + + function callback() { + $searchInput.val(''); + } + + // 『取消』按钮事件 + $btnCancel.click(function (e) { + e.preventDefault(); + callback(); + menu.dropPanel.hide(); + }); + + // 『确定』按钮事件 + $btnSubmit.click(function (e) { + e.preventDefault(); + var map = mapData.map, + isDynamic = $check.is(':checked'), + markers = mapData.markers, + + center = map.getCenter(), + centerLng = center.lng, + centerLat = center.lat, + + zoom = map.getZoom(), + + size = map.getSize(), + sizeWidth = size.width, + sizeHeight = size.height, + + position, + src, + iframe; + + if(isDynamic){ + //动态地址 + src = 'http://ueditor.baidu.com/ueditor/dialogs/map/show.html#'; + }else{ + //静态地址 + src = 'http://api.map.baidu.com/staticimage?'; + } + + //src参数 + src = src +'center=' + centerLng + ',' + centerLat + + '&zoom=' + zoom + + '&width=' + sizeWidth + + '&height=' + sizeHeight; + if(markers.length > 0){ + src = src + '&markers='; + + //添加所有的marker + $.each(markers, function(key, value){ + position = value.getPosition(); + if(key > 0){ + src = src + '|'; + } + src = src + position.lng + ',' + position.lat; + }); + } + + if(isDynamic){ + if(markers.length > 1){ + alert( lang.langDynamicOneLocation ); + return; + } + + src += '&markerStyles=l,A'; + + //插入iframe + iframe = ''; + iframe = iframe.replace('{src}', src); + editor.command(e, 'insertHtml', iframe, callback); + }else{ + //插入图片 + editor.command(e, 'insertHtml', '', callback); + } + }); + + // 根据 UI 创建菜单 panel + menu.dropPanel = new E.DropPanel(editor, menu, { + $content: $content, + width: 500 + }); + + // ---------- 事件 ---------- + + // render 时执行事件 + menu.onRender = function () { + if (ak === E.baiduMapAk) { + E.warn('建议在配置中自定义百度地图的mapAk,否则可能影响地图功能,文档:' + E.docsite); + } + }; + + // click 事件 + menu.clickEvent = function (e) { + var menu = this; + var dropPanel = menu.dropPanel; + var firstTime = false; + + // -------------隐藏------------- + if (dropPanel.isShowing) { + dropPanel.hide(); + return; + } + + // -------------显示------------- + if (!mapData.map) { + // 第一次,先加载地图 + firstTime = true; + } + + dropPanel.show(); + mapData.initMap(); + + if (!firstTime) { + $searchInput.focus(); + } + }; + + }); + +}); +// insertcode 菜单 +_e(function (E, $) { + + // 加载 highlightjs 代码 + function loadHljs() { + if (E.userAgent.indexOf('MSIE 8') > 0) { + // 不支持 IE8 + return; + } + if (window.hljs) { + // 不要重复加载 + return; + } + var script = document.createElement("script"); + script.type = "text/javascript"; + script.src = "//cdn.bootcss.com/highlight.js/9.2.0/highlight.min.js"; + document.body.appendChild(script); + } + + + E.createMenu(function (check) { + var menuId = 'insertcode'; + if (!check(menuId)) { + return; + } + + // 加载 highlightjs 代码 + setTimeout(loadHljs, 0); + + var editor = this; + var config = editor.config; + var lang = config.lang; + var $txt = editor.txt.$txt; + + // 创建 menu 对象 + var menu = new E.Menu({ + editor: editor, + id: menuId, + title: lang.insertcode + }); + + // click 事件 + menu.clickEvent = function (e) { + var menu = this; + var dropPanel = menu.dropPanel; + + // 隐藏 + if (dropPanel.isShowing) { + dropPanel.hide(); + return; + } + + // 显示 + $textarea.val(''); + dropPanel.show(); + + // highlightjs 语言列表 + var hljs = window.hljs; + if (hljs && hljs.listLanguages) { + if ($langSelect.children().length !== 0) { + return; + } + $langSelect.css({ + 'margin-top': '9px', + 'margin-left': '5px' + }); + $.each(hljs.listLanguages(), function (key, lang) { + if (lang === 'xml') { + lang = 'html'; + } + if (lang === config.codeDefaultLang) { + $langSelect.append(''); + } else { + $langSelect.append(''); + } + }); + } else { + $langSelect.hide(); + } + }; + + // 选中状态下的 click 事件 + menu.clickEventSelected = function (e) { + var menu = this; + var dropPanel = menu.dropPanel; + + // 隐藏 + if (dropPanel.isShowing) { + dropPanel.hide(); + return; + } + + // 显示 + dropPanel.show(); + + var rangeElem = editor.getRangeElem(); + var targetElem = editor.getSelfOrParentByName(rangeElem, 'pre'); + var $targetElem; + var className; + if (targetElem) { + // 确定找到 pre 之后,再找 code + targetElem = editor.getSelfOrParentByName(rangeElem, 'code'); + } + if (!targetElem) { + return; + } + $targetElem = $(targetElem); + + // 赋值内容 + $textarea.val($targetElem.text()); + if ($langSelect) { + // 赋值语言 + className = $targetElem.attr('class'); + if (className) { + $langSelect.val(className.split(' ')[0]); + } + } + }; + + // 定义更新选中状态的事件 + menu.updateSelectedEvent = function () { + var self = this; //菜单对象 + var editor = self.editor; + var rangeElem; + + rangeElem = editor.getRangeElem(); + rangeElem = editor.getSelfOrParentByName(rangeElem, 'pre'); + + if (rangeElem) { + return true; + } + + return false; + }; + + // 创建 panel + var $content = $('
              '); + var $textarea = $(''); + var $langSelect = $(''); + contentHandle($content); + menu.dropPanel = new E.DropPanel(editor, menu, { + $content: $content, + width: 500 + }); + + // 增加到editor对象中 + editor.menus[menuId] = menu; + + // ------ 增加 content 内容 ------ + function contentHandle($content) { + // textarea 区域 + var $textareaContainer = $('
              '); + $textareaContainer.css({ + margin: '15px 5px 5px 5px', + height: '160px', + 'text-align': 'center' + }); + $textarea.css({ + width: '100%', + height: '100%', + padding: '10px' + }); + $textarea.on('keydown', function (e) { + // 取消 tab 键默认行为 + if (e.keyCode === 9) { + e.preventDefault(); + } + }); + $textareaContainer.append($textarea); + $content.append($textareaContainer); + + // 按钮区域 + var $btnContainer = $('
              '); + var $btnSubmit = $(''); + var $btnCancel = $(''); + + $btnContainer.append($btnSubmit).append($btnCancel).append($langSelect); + $content.append($btnContainer); + + // 取消按钮 + $btnCancel.click(function (e) { + e.preventDefault(); + menu.dropPanel.hide(); + }); + + // 确定按钮 + var codeTpl = '
              {#content}
              '; + $btnSubmit.click(function (e) { + e.preventDefault(); + var val = $textarea.val(); + if (!val) { + // 无内容 + $textarea.focus(); + return; + } + + var rangeElem = editor.getRangeElem(); + if ($.trim($(rangeElem).text()) && codeTpl.indexOf('


              ') !== 0) { + codeTpl = '


              ' + codeTpl; + } + + var lang = $langSelect ? $langSelect.val() : ''; // 获取高亮语言 + var langClass = ''; + var doHightlight = function () { + $txt.find('pre code').each(function (i, block) { + var $block = $(block); + if ($block.attr('codemark')) { + // 有 codemark 标记的代码块,就不再重新格式化了 + return; + } else if (window.hljs) { + // 新代码块,格式化之后,立即标记 codemark + window.hljs.highlightBlock(block); + $block.attr('codemark', '1'); + } + }); + }; + + // 语言高亮样式 + if (lang) { + langClass = ' class="' + lang + ' hljs"'; + } + + // 替换标签 + val = val.replace(/&/gm, '&') + .replace(//gm, '>'); + + // ---- menu 未选中状态 ---- + if (!menu.selected) { + // 拼接html + var html = codeTpl.replace('{#langClass}', langClass).replace('{#content}', val); + editor.command(e, 'insertHtml', html, doHightlight); + return; + } + + // ---- menu 选中状态 ---- + var targetElem = editor.getSelfOrParentByName(rangeElem, 'pre'); + var $targetElem; + if (targetElem) { + // 确定找到 pre 之后,再找 code + targetElem = editor.getSelfOrParentByName(rangeElem, 'code'); + } + if (!targetElem) { + return; + } + $targetElem = $(targetElem); + + function commandFn() { + var className; + if (lang) { + className = $targetElem.attr('class'); + if (className !== lang + ' hljs') { + $targetElem.attr('class', lang + ' hljs'); + } + } + $targetElem.html(val); + } + function callback() { + editor.restoreSelectionByElem(targetElem); + doHightlight(); + } + editor.customCommand(e, commandFn, callback); + }); + } + + // ------ enter 时,不另起标签,只换行 ------ + $txt.on('keydown', function (e) { + if (e.keyCode !== 13) { + return; + } + var rangeElem = editor.getRangeElem(); + var targetElem = editor.getSelfOrParentByName(rangeElem, 'code'); + if (!targetElem) { + return; + } + + editor.command(e, 'insertHtml', '\n'); + }); + + // ------ 点击时,禁用其他标签 ------ + function updateMenu() { + var rangeElem = editor.getRangeElem(); + var targetElem = editor.getSelfOrParentByName(rangeElem, 'code'); + if (targetElem) { + // 在 code 之内,禁用其他菜单 + editor.disableMenusExcept('insertcode'); + } else { + // 不是在 code 之内,启用其他菜单 + editor.enableMenusExcept('insertcode'); + } + } + $txt.on('keydown click', function (e) { + // 此处必须使用 setTimeout 异步处理,否则不对 + setTimeout(updateMenu); + }); + }); + +}); +// undo 菜单 +_e(function (E, $) { + + E.createMenu(function (check) { + var menuId = 'undo'; + if (!check(menuId)) { + return; + } + var editor = this; + var lang = editor.config.lang; + + // 创建 menu 对象 + var menu = new E.Menu({ + editor: editor, + id: menuId, + title: lang.undo + }); + + // click 事件 + menu.clickEvent = function (e) { + editor.undo(); + }; + + // 增加到editor对象中 + editor.menus[menuId] = menu; + + + // ------------ 初始化时、enter 时、打字中断时,做记录 ------------ + // ------------ ctrl + z 是调用记录撤销,而不是使用浏览器默认的撤销 ------------ + editor.ready(function () { + var editor = this; + var $txt = editor.txt.$txt; + var timeoutId; + + // 执行undo记录 + function undo() { + editor.undoRecord(); + } + + $txt.on('keydown', function (e) { + var keyCode = e.keyCode; + + // 撤销 ctrl + z + if (e.ctrlKey && keyCode === 90) { + editor.undo(); + return; + } + + if (keyCode === 13) { + // enter 做记录 + undo(); + } else { + // keyup 之后 1s 之内不操作,则做一次记录 + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(undo, 1000); + } + }); + + // 初始化做记录 + editor.undoRecord(); + }); + }); + +}); +// redo 菜单 +_e(function (E, $) { + + E.createMenu(function (check) { + var menuId = 'redo'; + if (!check(menuId)) { + return; + } + var editor = this; + var lang = editor.config.lang; + + // 创建 menu 对象 + var menu = new E.Menu({ + editor: editor, + id: menuId, + title: lang.redo + }); + + // click 事件 + menu.clickEvent = function (e) { + editor.redo(); + }; + + // 增加到editor对象中 + editor.menus[menuId] = menu; + }); + +}); +// 全屏 菜单 +_e(function (E, $) { + + // 记录全屏时的scrollTop + var scrollTopWhenFullScreen; + + E.createMenu(function (check) { + var menuId = 'fullscreen'; + if (!check(menuId)) { + return; + } + var editor = this; + var $txt = editor.txt.$txt; + var config = editor.config; + var zIndexConfig = config.zindex || 10000; + var lang = config.lang; + + var isSelected = false; + var zIndex; + + var maxHeight; + + // 创建 menu 对象 + var menu = new E.Menu({ + editor: editor, + id: menuId, + title: lang.fullscreen + }); + + // 定义click事件 + menu.clickEvent = function (e) { + // 增加样式 + var $editorContainer = editor.$editorContainer; + $editorContainer.addClass('wangEditor-fullscreen'); + + // (先保存当前的)再设置z-index + zIndex = $editorContainer.css('z-index'); + $editorContainer.css('z-index', zIndexConfig); + + var $wrapper; + var txtHeight = $txt.height(); + var txtOuterHeight = $txt.outerHeight(); + + if (editor.useMaxHeight) { + // 记录 max-height,并暂时去掉maxheight + maxHeight = $txt.css('max-height'); + $txt.css('max-height', 'none'); + + // 如果使用了maxHeight, 将$txt从它的父元素中移出来 + $wrapper = $txt.parent(); + $wrapper.after($txt); + $wrapper.remove(); + $txt.css('overflow-y', 'auto'); + } + + // 设置高度到全屏 + var menuContainer = editor.menuContainer; + $txt.height( + E.$window.height() - + menuContainer.height() - + (txtOuterHeight - txtHeight) // 去掉内边距和外边距 + ); + + // 取消menuContainer的内联样式(menu吸顶时,会为 menuContainer 设置一些内联样式) + editor.menuContainer.$menuContainer.attr('style', ''); + + // 保存状态 + isSelected = true; + + // 记录编辑器是否全屏 + editor.isFullScreen = true; + + // 记录设置全屏时的高度 + scrollTopWhenFullScreen = E.$window.scrollTop(); + }; + + // 定义选中状态的 click 事件 + menu.clickEventSelected = function (e) { + // 取消样式 + var $editorContainer = editor.$editorContainer; + $editorContainer.removeClass('wangEditor-fullscreen'); + $editorContainer.css('z-index', zIndex); + + // 还原height + if (editor.useMaxHeight) { + $txt.css('max-height', maxHeight); + } else { + // editor.valueContainerHeight 在 editor.txt.initHeight() 中事先保存了 + editor.$valueContainer.css('height', editor.valueContainerHeight); + } + + // 重新计算高度 + editor.txt.initHeight(); + + // 保存状态 + isSelected = false; + + // 记录编辑器是否全屏 + editor.isFullScreen = false; + + // 还原scrollTop + if (scrollTopWhenFullScreen != null) { + E.$window.scrollTop(scrollTopWhenFullScreen); + } + }; + + // 定义选中事件 + menu.updateSelectedEvent = function (e) { + return isSelected; + }; + + // 增加到editor对象中 + editor.menus[menuId] = menu; + }); + +}); +// 渲染menus +_e(function (E, $) { + + E.fn.renderMenus = function () { + + var editor = this; + var menus = editor.menus; + var menuIds = editor.config.menus; + var menuContainer = editor.menuContainer; + + var menu; + var groupIdx = 0; + $.each(menuIds, function (k, v) { + if (v === '|') { + groupIdx++; + return; + } + + menu = menus[v]; + if (menu) { + menu.render(groupIdx); + } + }); + }; + +}); +// 渲染menus +_e(function (E, $) { + + E.fn.renderMenuContainer = function () { + + var editor = this; + var menuContainer = editor.menuContainer; + var $editorContainer = editor.$editorContainer; + + menuContainer.render(); + + }; + +}); +// 渲染 txt +_e(function (E, $) { + + E.fn.renderTxt = function () { + + var editor = this; + var txt = editor.txt; + + txt.render(); + + // ready 时候,计算txt的高度 + editor.ready(function () { + txt.initHeight(); + }); + }; + +}); +// 渲染 container +_e(function (E, $) { + + E.fn.renderEditorContainer = function () { + + var editor = this; + var $valueContainer = editor.$valueContainer; + var $editorContainer = editor.$editorContainer; + var $txt = editor.txt.$txt; + var $prev, $parent; + + // 将编辑器渲染到页面中 + if ($valueContainer === $txt) { + $prev = editor.$prev; + $parent = editor.$parent; + + if ($prev && $prev.length) { + // 有前置节点,就插入到前置节点的后面 + $prev.after($editorContainer); + } else { + // 没有前置节点,就直接插入到父元素 + $parent.prepend($editorContainer); + } + + } else { + $valueContainer.after($editorContainer); + $valueContainer.hide(); + } + + // 设置宽度(这样设置宽度有问题) + // $editorContainer.css('width', $valueContainer.css('width')); + }; + +}); +// 菜单事件 +_e(function (E, $) { + + // 绑定每个菜单的click事件 + E.fn.eventMenus = function () { + + var menus = this.menus; + + // 绑定菜单的点击事件 + $.each(menus, function (k, v) { + v.bindEvent(); + }); + + }; + +}); +// 菜单container事件 +_e(function (E, $) { + + E.fn.eventMenuContainer = function () { + + }; + +}); +// 编辑区域事件 +_e(function (E, $) { + + E.fn.eventTxt = function () { + + var txt = this.txt; + + // txt内容变化时,保存选区 + txt.saveSelectionEvent(); + + // txt内容变化时,随时更新 value + txt.updateValueEvent(); + + // txt内容变化时,随时更新 menu style + txt.updateMenuStyleEvent(); + + // // 鼠标hover时,显示 p head 高度(暂时关闭这个功能) + // if (!/ie/i.test(E.userAgent)) { + // // 暂时不支持IE + // txt.showHeightOnHover(); + // } + }; + +}); +// 上传图片事件 +_e(function (E, $) { + + E.plugin(function () { + var editor = this; + var fns = editor.config.uploadImgFns; // editor.config.uploadImgFns = {} 在config文件中定义了 + + // -------- 定义load函数 -------- + fns.onload || (fns.onload = function (resultText, xhr) { + E.log('上传结束,返回结果为 ' + resultText); + + var editor = this; + var originalName = editor.uploadImgOriginalName || ''; // 上传图片时,已经将图片的名字存在 editor.uploadImgOriginalName + var img; + if (resultText.indexOf('error|') === 0) { + // 提示错误 + E.warn('上传失败:' + resultText.split('|')[1]); + alert(resultText.split('|')[1]); + } else { + E.log('上传成功,即将插入编辑区域,结果为:' + resultText); + + // 将结果插入编辑器 + img = document.createElement('img'); + img.onload = function () { + var html = '' + originalName + ''; + editor.command(null, 'insertHtml', html); + + E.log('已插入图片,地址 ' + resultText); + img = null; + }; + img.onerror = function () { + E.error('使用返回的结果获取图片,发生错误。请确认以下结果是否正确:' + resultText); + img = null; + }; + img.src = resultText; + } + + }); + + // -------- 定义tiemout函数 -------- + fns.ontimeout || (fns.ontimeout = function (xhr) { + E.error('上传图片超时'); + alert('上传图片超时'); + }); + + // -------- 定义error函数 -------- + fns.onerror || (fns.onerror = function (xhr) { + E.error('上传上图片发生错误'); + alert('上传上图片发生错误'); + }); + + }); +}); +// xhr 上传图片 +_e(function (E, $) { + + if (!window.FileReader || !window.FormData) { + // 如果不支持html5的文档操作,直接返回 + return; + } + + E.plugin(function () { + + var editor = this; + var config = editor.config; + var uploadImgUrl = config.uploadImgUrl; + var uploadTimeout = config.uploadTimeout; + + // 获取配置中的上传事件 + var uploadImgFns = config.uploadImgFns; + var onload = uploadImgFns.onload; + var ontimeout = uploadImgFns.ontimeout; + var onerror = uploadImgFns.onerror; + + if (!uploadImgUrl) { + return; + } + + // -------- 将以base64的图片url数据转换为Blob -------- + function convertBase64UrlToBlob(urlData, filetype){ + //去掉url的头,并转换为byte + var bytes = window.atob(urlData.split(',')[1]); + + //处理异常,将ascii码小于0的转换为大于0 + var ab = new ArrayBuffer(bytes.length); + var ia = new Uint8Array(ab); + var i; + for (i = 0; i < bytes.length; i++) { + ia[i] = bytes.charCodeAt(i); + } + + return new Blob([ab], {type : filetype}); + } + + // -------- 插入图片的方法 -------- + function insertImg(src, event) { + var img = document.createElement('img'); + img.onload = function () { + var html = ''; + editor.command(event, 'insertHtml', html); + + E.log('已插入图片,地址 ' + src); + img = null; + }; + img.onerror = function () { + E.error('使用返回的结果获取图片,发生错误。请确认以下结果是否正确:' + src); + img = null; + }; + img.src = src; + } + + // -------- onprogress 事件 -------- + function updateProgress(e) { + if (e.lengthComputable) { + var percentComplete = e.loaded / e.total; + editor.showUploadProgress(percentComplete * 100); + } + } + + // -------- xhr 上传图片 -------- + editor.xhrUploadImg = function (opt) { + // opt 数据 + var event = opt.event; + var fileName = opt.filename || ''; + var base64 = opt.base64; + var fileType = opt.fileType || 'image/png'; // 无扩展名则默认使用 png + var name = opt.name || 'wangEditor_upload_file'; + var loadfn = opt.loadfn || onload; + var errorfn = opt.errorfn || onerror; + var timeoutfn = opt.timeoutfn || ontimeout; + + // 上传参数(如 token) + var params = editor.config.uploadParams || {}; + + // headers + var headers = editor.config.uploadHeaders || {}; + + // 获取文件扩展名 + var fileExt = 'png'; // 默认为 png + if (fileName.indexOf('.') > 0) { + // 原来的文件名有扩展名 + fileExt = fileName.slice(fileName.lastIndexOf('.') - fileName.length + 1); + } else if (fileType.indexOf('/') > 0 && fileType.split('/')[1]) { + // 文件名没有扩展名,通过类型获取,如从 'image/png' 取 'png' + fileExt = fileType.split('/')[1]; + } + + // 过滤图片后缀 + // author:caiweiming <314013107@qq.com> + if (editor.config.imgExt) { + var imgExt = editor.config.imgExt.split(','); + var result = false; + for (var ext in imgExt) { + if (imgExt[ext] == fileExt) { + result = true; + break; + } + } + if (result === false) { + alert("只允许上传后缀名为:"+editor.config.imgExt + '的图片'); + return false; + } + } + + // ------------ begin 预览模拟上传 ------------ + if (E.isOnWebsite) { + E.log('预览模拟上传'); + insertImg(base64, event); + return; + } + // ------------ end 预览模拟上传 ------------ + + // 变量声明 + var xhr = new XMLHttpRequest(); + var timeoutId; + var src; + var formData = new FormData(); + + // 超时处理 + function timeoutCallback() { + if (timeoutId) { + clearTimeout(timeoutId); + } + if (xhr && xhr.abort) { + xhr.abort(); + } + + // 超时了就阻止默认行为 + event.preventDefault(); + + // 执行回调函数,提示什么内容,都应该在回调函数中定义 + timeoutfn && timeoutfn.call(editor, xhr); + + // 隐藏进度条 + editor.hideUploadProgress(); + } + + xhr.onload = function () { + if (timeoutId) { + clearTimeout(timeoutId); + } + + // 记录文件名到 editor.uploadImgOriginalName ,插入图片时,可做 alt 属性用 + editor.uploadImgOriginalName = fileName; + if (fileName.indexOf('.') > 0) { + editor.uploadImgOriginalName = fileName.split('.')[0]; + } + + // 执行load函数,任何操作,都应该在load函数中定义 + loadfn && loadfn.call(editor, xhr.responseText, xhr); + + // 隐藏进度条 + editor.hideUploadProgress(); + }; + xhr.onerror = function () { + if (timeoutId) { + clearTimeout(timeoutId); + } + + // 超时了就阻止默认行为 + event.preventDefault(); + + // 执行error函数,错误提示,应该在error函数中定义 + errorfn && errorfn.call(editor, xhr); + + // 隐藏进度条 + editor.hideUploadProgress(); + }; + // xhr.onprogress = updateProgress; + xhr.upload.onprogress = updateProgress; + + // 填充数据 + // formData.append(name, convertBase64UrlToBlob(base64, fileType), E.random() + '.' + fileExt); + fileName = fileName || E.random() + '.' + fileExt; + formData.append(name, convertBase64UrlToBlob(base64, fileType), fileName); + + // 添加参数 + $.each(params, function (key, value) { + formData.append(key, value); + }); + + // 开始上传 + xhr.open('POST', uploadImgUrl, true); + // xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded"); // 将参数解析成传统form的方式上传 + + // 修改自定义配置的headers + $.each(headers, function (key, value) { + xhr.setRequestHeader(key, value); + }); + + // 跨域上传时,传cookie + xhr.withCredentials = true; + + // 发送数据 + xhr.send(formData); + timeoutId = setTimeout(timeoutCallback, uploadTimeout); + + E.log('开始上传...并开始超时计算'); + }; + }); +}); +// 进度条 +_e(function (E, $) { + + E.plugin(function () { + + var editor = this; + var menuContainer = editor.menuContainer; + var menuHeight = menuContainer.height(); + var $editorContainer = editor.$editorContainer; + var width = $editorContainer.width(); + var $progress = $('
              '); + + // 渲染事件 + var isRender = false; + function render() { + if (isRender) { + return; + } + isRender = true; + + $progress.css({ + top: menuHeight + 'px' + }); + $editorContainer.append($progress); + } + + // ------ 显示进度 ------ + editor.showUploadProgress = function (progress) { + if (timeoutId) { + clearTimeout(timeoutId); + } + + // 显示之前,先判断是否渲染 + render(); + + $progress.show(); + $progress.width(progress * width / 100); + }; + + // ------ 隐藏进度条 ------ + var timeoutId; + function hideProgress() { + $progress.hide(); + timeoutId = null; + } + editor.hideUploadProgress = function (time) { + if (timeoutId) { + clearTimeout(timeoutId); + } + time = time || 750; + timeoutId = setTimeout(hideProgress, time); + }; + }); +}); +// upload img 插件 +_e(function (E, $) { + + E.plugin(function () { + var editor = this; + var config = editor.config; + var uploadImgUrl = config.uploadImgUrl; + var uploadTimeout = config.uploadTimeout; + var event; + + if (!uploadImgUrl) { + return; + } + + // 获取editor的上传dom + var $uploadContent = editor.$uploadContent; + if (!$uploadContent) { + return; + } + + // 自定义UI,并添加到上传dom节点上 + var $uploadIcon = $('
              '); + $uploadContent.append($uploadIcon); + + // ---------- 构建上传对象 ---------- + var upfile = new E.UploadFile({ + editor: editor, + uploadUrl: uploadImgUrl, + timeout: uploadTimeout, + fileAccept: 'image/*' // 只允许选择图片 + }); + + // 选择本地文件,上传 + $uploadIcon.click(function (e) { + event = e; + upfile.selectFiles(); + }); + }); +}); +// h5 方式上传图片 +_e(function (E, $) { + + if (!window.FileReader || !window.FormData) { + // 如果不支持html5的文档操作,直接返回 + return; + } + + // 构造函数 + var UploadFile = function (opt) { + this.editor = opt.editor; + this.uploadUrl = opt.uploadUrl; + this.timeout = opt.timeout; + this.fileAccept = opt.fileAccept; + this.multiple = true; + }; + + UploadFile.fn = UploadFile.prototype; + + // clear + UploadFile.fn.clear = function () { + this.$input.val(''); + E.log('input value 已清空'); + }; + + // 渲染 + UploadFile.fn.render = function () { + var self = this; + if (self._hasRender) { + // 不要重复渲染 + return; + } + + E.log('渲染dom'); + + var fileAccept = self.fileAccept; + var acceptTpl = fileAccept ? 'accept="' + fileAccept + '"' : ''; + var multiple = self.multiple; + var multipleTpl = multiple ? 'multiple="multiple"' : ''; + var $input = $(''); + var $container = $('
              '); + + $container.append($input); + E.$body.append($container); + + // onchange 事件 + $input.on('change', function (e) { + self.selected(e, $input.get(0)); + }); + + // 记录对象数据 + self.$input = $input; + + // 记录 + self._hasRender = true; + }; + + // 选择 + UploadFile.fn.selectFiles = function () { + var self = this; + + E.log('使用 html5 方式上传'); + + // 先渲染 + self.render(); + + // 选择 + E.log('选择文件'); + self.$input.click(); + }; + + // 选中文件之后 + UploadFile.fn.selected = function (e, input) { + var self = this; + var files = input.files || []; + if (files.length === 0) { + return; + } + + E.log('选中 ' + files.length + ' 个文件'); + + // 遍历选中的文件,预览、上传 + $.each(files, function (key, value) { + self.upload(value); + }); + }; + + // 上传单个文件 + UploadFile.fn.upload = function (file) { + var self = this; + var editor = self.editor; + var filename = file.name || ''; + var fileType = file.type || ''; + var uploadImgFns = editor.config.uploadImgFns; + var uploadFileName = editor.config.uploadImgFileName || 'wangEditorH5File'; + var onload = uploadImgFns.onload; + var ontimeout = uploadImgFns.ontimeout; + var onerror = uploadImgFns.onerror; + var reader = new FileReader(); + + if (!onload || !ontimeout || !onerror) { + E.error('请为编辑器配置上传图片的 onload ontimeout onerror 回调事件'); + return; + } + + + E.log('开始执行 ' + filename + ' 文件的上传'); + + // 清空 input 数据 + function clearInput() { + self.clear(); + } + + // onload事件 + reader.onload = function (e) { + E.log('已读取' + filename + '文件'); + + var base64 = e.target.result || this.result; + editor.xhrUploadImg({ + event: e, + filename: filename, + base64: base64, + fileType: fileType, + name: uploadFileName, + loadfn: function (resultText, xhr) { + clearInput(); + // 执行配置中的方法 + var editor = this; + onload.call(editor, resultText, xhr); + }, + errorfn: function (xhr) { + clearInput(); + if (E.isOnWebsite) { + alert('wangEditor官网暂时没有服务端,因此报错。实际项目中不会发生'); + } + // 执行配置中的方法 + var editor = this; + onerror.call(editor, xhr); + }, + timeoutfn: function (xhr) { + clearInput(); + if (E.isOnWebsite) { + alert('wangEditor官网暂时没有服务端,因此超时。实际项目中不会发生'); + } + // 执行配置中的方法 + var editor = this; + ontimeout(editor, xhr); + } + }); + }; + + // 开始取文件 + reader.readAsDataURL(file); + }; + + // 暴露给 E + E.UploadFile = UploadFile; + +}); +// form方式上传图片 +_e(function (E, $) { + + if (window.FileReader && window.FormData) { + // 如果支持 html5 上传,则返回 + return; + } + + // 构造函数 + var UploadFile = function (opt) { + this.editor = opt.editor; + this.uploadUrl = opt.uploadUrl; + this.timeout = opt.timeout; + this.fileAccept = opt.fileAccept; + this.multiple = false; + }; + + UploadFile.fn = UploadFile.prototype; + + // clear + UploadFile.fn.clear = function () { + this.$input.val(''); + E.log('input value 已清空'); + }; + + // 隐藏modal + UploadFile.fn.hideModal = function () { + this.modal.hide(); + }; + + // 渲染 + UploadFile.fn.render = function () { + var self = this; + var editor = self.editor; + var uploadFileName = editor.config.uploadImgFileName || 'wangEditorFormFile'; + if (self._hasRender) { + // 不要重复渲染 + return; + } + + // 服务器端路径 + var uploadUrl = self.uploadUrl; + + E.log('渲染dom'); + + // 创建 form 和 iframe + var iframeId = 'iframe' + E.random(); + var $iframe = $(''); + var multiple = self.multiple; + var multipleTpl = multiple ? 'multiple="multiple"' : ''; + var $p = $('

              选择图片并上传

              '); + var $input = $(''); + var $btn = $(''); + var $form = $('
              '); + var $container = $('
              '); + + $form.append($p).append($input).append($btn); + + // 增加用户配置的参数,如 token + $.each(editor.config.uploadParams, function (key, value) { + $form.append( $('') ); + }); + + $container.append($form); + $container.append($iframe); + + self.$input = $input; + self.$iframe = $iframe; + + // 生成 modal + var modal = new E.Modal(editor, undefined, { + $content: $container + }); + self.modal = modal; + + // 记录 + self._hasRender = true; + }; + + // 绑定 iframe load 事件 + UploadFile.fn.bindLoadEvent = function () { + var self = this; + if (self._hasBindLoad) { + // 不要重复绑定 + return; + } + + var editor = self.editor; + var $iframe = self.$iframe; + var iframe = $iframe.get(0); + var iframeWindow = iframe.contentWindow; + var onload = editor.config.uploadImgFns.onload; + + // 定义load事件 + function onloadFn() { + var resultText = $.trim(iframeWindow.document.body.innerHTML); + if (!resultText) { + return; + } + + // 获取文件名 + var fileFullName = self.$input.val(); // 结果如 C:\folder\abc.png 格式 + var fileOriginalName = fileFullName; + if (fileFullName.lastIndexOf('\\') >= 0) { + // 获取 abc.png 格式 + fileOriginalName = fileFullName.slice(fileFullName.lastIndexOf('\\') + 1); + if (fileOriginalName.indexOf('.') > 0) { + // 获取 abc (即不带扩展名的文件名) + fileOriginalName = fileOriginalName.split('.')[0]; + } + } + + // 将文件名暂存到 editor.uploadImgOriginalName ,插入图片时,可作为 alt 属性来用 + editor.uploadImgOriginalName = fileOriginalName; + + // 执行load函数,插入图片的操作,应该在load函数中执行 + onload.call(editor, resultText); + + // 清空 input 数据 + self.clear(); + + // 隐藏modal + self.hideModal(); + } + + // 绑定 load 事件 + if (iframe.attachEvent) { + iframe.attachEvent('onload', onloadFn); + } else { + iframe.onload = onloadFn; + } + + // 记录 + self._hasBindLoad = true; + }; + + UploadFile.fn.show = function () { + var self = this; + var modal = self.modal; + + function show() { + modal.show(); + self.bindLoadEvent(); + } + setTimeout(show); + }; + + // 选择 + UploadFile.fn.selectFiles = function () { + var self = this; + + E.log('使用 form 方式上传'); + + // 先渲染 + self.render(); + + // 先清空 + self.clear(); + + // 显示 + self.show(); + }; + + // 暴露给 E + E.UploadFile = UploadFile; + +}); +// upload img 插件 粘贴图片 +_e(function (E, $) { + + E.plugin(function () { + var editor = this; + var txt = editor.txt; + var $txt = txt.$txt; + var config = editor.config; + var uploadImgUrl = config.uploadImgUrl; + var uploadFileName = config.uploadImgFileName || 'wangEditorPasteFile'; + var pasteEvent; + var $imgsBeforePaste; + + // 未配置上传图片url,则忽略 + if (!uploadImgUrl) { + return; + } + + // -------- 非 chrome 下,通过查找粘贴的图片的方式上传 -------- + function findPasteImgAndUpload() { + var reg = /^data:(image\/\w+);base64/; + var $imgs = $txt.find('img'); + + E.log('粘贴后,检查到编辑器有' + $imgs.length + '个图片。开始遍历图片,试图找到刚刚粘贴过来的图片'); + + $.each($imgs, function () { + var img = this; + var $img = $(img); + var flag; + var base64 = $img.attr('src'); + var type; + + // 判断当前图片是否是粘贴之前的 + $imgsBeforePaste.each(function () { + if (img === this) { + // 当前图片是粘贴之前的 + flag = true; + return false; + } + }); + + // 当前图片是粘贴之前的,则忽略 + if (flag) { + return; + } + + E.log('找到一个粘贴过来的图片'); + + if (reg.test(base64)) { + // 得到的粘贴的图片是 base64 格式,符合要求 + E.log('src 是 base64 格式,可以上传'); + type = base64.match(reg)[1]; + editor.xhrUploadImg({ + event: pasteEvent, + base64: base64, + fileType: type, + name: uploadFileName + }); + } else { + E.log('src 为 ' + base64 + ' ,不是 base64 格式,暂时不支持上传'); + } + + // 最终移除原图片 + $img.remove(); + }); + + E.log('遍历结束'); + } + + // 开始监控粘贴事件 + $txt.on('paste', function (e) { + pasteEvent = e; + var data = pasteEvent.clipboardData || pasteEvent.originalEvent.clipboardData; + var text; + var items; + + // -------- 试图获取剪切板中的文字,有文字的情况下,就不处理图片粘贴 -------- + if (data == null) { + text = window.clipboardData && window.clipboardData.getData('text'); + } else { + text = data.getData('text/plain') || data.getData('text/html'); + } + if (text) { + return; + } + + items = data && data.items; + if (items) { + // -------- chrome 可以用 data.items 取出图片 ----- + E.log('通过 data.items 得到了数据'); + + $.each(items, function (key, value) { + var fileType = value.type || ''; + if(fileType.indexOf('image') < 0) { + // 不是图片 + return; + } + + var file = value.getAsFile(); + var reader = new FileReader(); + + E.log('得到一个粘贴图片'); + + reader.onload = function (e) { + E.log('读取到粘贴的图片'); + + // 执行上传 + var base64 = e.target.result || this.result; + editor.xhrUploadImg({ + event: pasteEvent, + base64: base64, + fileType: fileType, + name: uploadFileName + }); + }; + + //读取粘贴的文件 + reader.readAsDataURL(file); + }); + } else { + // -------- 非 chrome 不能用 data.items 取图片 ----- + + E.log('未从 data.items 得到数据,使用检测粘贴图片的方式'); + + // 获取 + $imgsBeforePaste = $txt.find('img'); + E.log('粘贴前,检查到编辑器有' + $imgsBeforePaste.length + '个图片'); + + // 异步上传找到的图片 + setTimeout(findPasteImgAndUpload, 0); + } + }); + + }); +}); +// 拖拽上传图片 插件 +_e(function (E, $) { + + E.plugin(function () { + + var editor = this; + var txt = editor.txt; + var $txt = txt.$txt; + var config = editor.config; + var uploadImgUrl = config.uploadImgUrl; + var uploadFileName = config.uploadImgFileName || 'wangEditorDragFile'; + + // 未配置上传图片url,则忽略 + if (!uploadImgUrl) { + return; + } + + // 阻止浏览器默认行为 + E.$document.on('dragleave drop dragenter dragover', function (e) { + e.preventDefault(); + }); + + // 监控 $txt drop 事件 + $txt.on('drop', function (dragEvent) { + dragEvent.preventDefault(); + + var originalEvent = dragEvent.originalEvent; + var files = originalEvent.dataTransfer && originalEvent.dataTransfer.files; + + if (!files || !files.length) { + return; + } + + $.each(files, function (k, file) { + var type = file.type; + var name = file.name; + + if (type.indexOf('image/') < 0) { + // 只接收图片 + return; + } + + E.log('得到图片 ' + name); + + var reader = new FileReader(); + reader.onload = function (e) { + E.log('读取到图片 ' + name); + + // 执行上传 + var base64 = e.target.result || this.result; + editor.xhrUploadImg({ + event: dragEvent, + base64: base64, + fileType: type, + name: uploadFileName + }); + }; + + //读取粘贴的文件 + reader.readAsDataURL(file); + + }); + }); + }); + +}); +// 编辑器区域 table toolbar +_e(function (E, $) { + + E.plugin(function () { + var editor = this; + var txt = editor.txt; + var $txt = txt.$txt; + // 说明:设置了 max-height 之后,$txt.parent() 负责滚动处理 + var $currentTxt = editor.useMaxHeight ? $txt.parent() : $txt; + var $currentTable; + + // 用到的dom节点 + var isRendered = false; + var $toolbar = $('
              '); + var $triangle = $('
              '); + var $delete = $(''); + var $zoomSmall = $(''); + var $zoomBig = $(''); + + // 渲染到页面 + function render() { + if (isRendered) { + return; + } + + // 绑定事件 + bindEvent(); + + // 拼接 渲染到页面上 + $toolbar.append($triangle) + .append($delete) + .append($zoomSmall) + .append($zoomBig); + editor.$editorContainer.append($toolbar); + isRendered = true; + } + + // 绑定事件 + function bindEvent() { + // 统一执行命令的方法 + var commandFn; + function command(e, callback) { + if (commandFn) { + editor.customCommand(e, commandFn, callback); + } + } + + // 删除 + $delete.click(function (e) { + commandFn = function () { + $currentTable.remove(); + }; + command(e, function () { + setTimeout(hide, 100); + }); + }); + + // 放大 + $zoomBig.click(function (e) { + commandFn = function () { + $currentTable.css({ + width: '100%' + }); + }; + command(e, function () { + setTimeout(show); + }); + }); + + // 缩小 + $zoomSmall.click(function (e) { + commandFn = function () { + $currentTable.css({ + width: 'auto' + }); + }; + command(e, function () { + setTimeout(show); + }); + }); + } + + // 显示 toolbar + function show() { + if (editor._disabled) { + // 编辑器已经被禁用,则不让显示 + return; + } + if ($currentTable == null) { + return; + } + $currentTable.addClass('clicked'); + var tablePosition = $currentTable.position(); + var tableTop = tablePosition.top; + var tableLeft = tablePosition.left; + var tableHeight = $currentTable.outerHeight(); + var tableWidth = $currentTable.outerWidth(); + + // --- 定位 toolbar --- + + // 计算初步结果 + var top = tableTop + tableHeight; + var left = tableLeft; + var marginLeft = 0; + + var txtTop = $currentTxt.position().top; + var txtHeight = $currentTxt.outerHeight(); + if (top > (txtTop + txtHeight)) { + // top 不得超出编辑范围 + top = txtTop + txtHeight; + } + + // 显示(方便计算 margin) + $toolbar.show(); + + // 计算 margin + var width = $toolbar.outerWidth(); + marginLeft = tableWidth / 2 - width / 2; + + // 定位 + $toolbar.css({ + top: top + 5, + left: left, + 'margin-left': marginLeft + }); + // 如果定位太靠左了 + if (marginLeft < 0) { + // 得到三角形的margin-left + $toolbar.css('margin-left', '0'); + $triangle.hide(); + } else { + $triangle.show(); + } + } + + // 隐藏 toolbar + function hide() { + if ($currentTable == null) { + return; + } + $currentTable.removeClass('clicked'); + $currentTable = null; + $toolbar.hide(); + } + + // click table 事件 + $currentTxt.on('click', 'table', function (e) { + var $table = $(e.currentTarget); + + // 渲染 + render(); + + if ($currentTable && ($currentTable.get(0) === $table.get(0))) { + setTimeout(hide, 100); + return; + } + + // 显示 toolbar + $currentTable = $table; + show(); + + // 阻止冒泡 + e.preventDefault(); + e.stopPropagation(); + + }).on('click keydown scroll', function (e) { + setTimeout(hide, 100); + }); + E.$body.on('click keydown scroll', function (e) { + setTimeout(hide, 100); + }); + }); + +}); +// 编辑器区域 img toolbar +_e(function (E, $) { + + if (E.userAgent.indexOf('MSIE 8') > 0) { + return; + } + + E.plugin(function () { + var editor = this; + var lang = editor.config.lang; + var txt = editor.txt; + var $txt = txt.$txt; + // 说明:设置了 max-height 之后,$txt.parent() 负责滚动处理 + var $currentTxt = editor.useMaxHeight ? $txt.parent() : $txt; + var $editorContainer = editor.$editorContainer; + var $currentImg; + var currentLink = ''; + + // 用到的dom节点 + var isRendered = false; + var $dragPoint = $('
              '); + + var $toolbar = $('
              '); + var $triangle = $('
              '); + + var $menuContainer = $('
              '); + var $delete = $(''); + var $zoomSmall = $(''); + var $zoomBig = $(''); + // var $floatLeft = $(''); + // var $noFloat = $(''); + // var $floatRight = $(''); + var $alignLeft = $(''); + var $alignCenter = $(''); + var $alignRight = $(''); + var $link = $(''); + var $unLink = $(''); + + var $linkInputContainer = $('
              '); + var $linkInput = $(''); + var $linkBtnSubmit = $(''); + var $linkBtnCancel = $(''); + + // 记录是否正在拖拽 + var isOnDrag = false; + + // 获取 / 设置 链接 + function imgLink(e, url) { + if (!$currentImg) { + return; + } + var commandFn; + var callback = function () { + // 及时保存currentLink + if (url != null) { + currentLink = url; + } + }; + var $link; + var inLink = false; + var $parent = $currentImg.parent(); + if ($parent.get(0).nodeName.toLowerCase() === 'a') { + // 父元素就是图片链接 + $link = $parent; + inLink = true; + } else { + // 父元素不是图片链接,则重新创建一个链接 + $link = $(''); + } + + if (url == null) { + // url 无值,是获取链接 + return $link.attr('href') || ''; + } else if (url === '') { + // url 是空字符串,是取消链接 + if (inLink) { + commandFn = function () { + $currentImg.unwrap(); + }; + } + } else { + // url 有值,是设置链接 + if (url === currentLink) { + return; + } + commandFn = function () { + $link.attr('href', url); + + if (!inLink) { + // 当前图片未包含在链接中,则包含进来 + $currentImg.wrap($link); + } + }; + } + + // 执行命令 + if (commandFn) { + editor.customCommand(e, commandFn, callback); + } + } + + // 渲染到页面 + function render() { + if (isRendered) { + return; + } + + // 绑定事件 + bindToolbarEvent(); + bindDragEvent(); + + // 菜单放入 container + $menuContainer.append($delete) + .append($zoomSmall) + .append($zoomBig) + // .append($floatLeft) + // .append($noFloat) + // .append($floatRight); + .append($alignLeft) + .append($alignCenter) + .append($alignRight) + .append($link) + .append($unLink); + + // 链接input放入container + $linkInputContainer.append($linkInput) + .append($linkBtnCancel) + .append($linkBtnSubmit); + + // 拼接 渲染到页面上 + $toolbar.append($triangle) + .append($menuContainer) + .append($linkInputContainer); + + editor.$editorContainer.append($toolbar).append($dragPoint); + isRendered = true; + } + + // 绑定toolbar事件 + function bindToolbarEvent() { + // 统一执行命令的方法 + var commandFn; + function customCommand(e, callback) { + if (commandFn) { + editor.customCommand(e, commandFn, callback); + } + } + + // 删除 + $delete.click(function (e) { + // 删除之前先unlink + imgLink(e, ''); + + // 删除图片 + commandFn = function () { + $currentImg.remove(); + }; + customCommand(e, function () { + setTimeout(hide, 100); + }); + }); + + // 放大 + $zoomBig.click(function (e) { + commandFn = function () { + var img = $currentImg.get(0); + var width = img.width; + var height = img.height; + width = width * 1.1; + height = height * 1.1; + + $currentImg.css({ + width: width + 'px', + height: height + 'px' + }); + }; + customCommand(e, function () { + setTimeout(show); + }); + }); + + // 缩小 + $zoomSmall.click(function (e) { + commandFn = function () { + var img = $currentImg.get(0); + var width = img.width; + var height = img.height; + width = width * 0.9; + height = height * 0.9; + + $currentImg.css({ + width: width + 'px', + height: height + 'px' + }); + }; + customCommand(e, function () { + setTimeout(show); + }); + }); + + // // 左浮动 + // $floatLeft.click(function (e) { + // commandFn = function () { + // $currentImg.css({ + // float: 'left' + // }); + // }; + // customCommand(e, function () { + // setTimeout(hide, 100); + // }); + // }); + + // alignLeft + $alignLeft.click(function (e) { + commandFn = function () { + // 如果 img 增加了链接,那么 img.parent() 就是 a 标签,设置 align 没用的,因此必须找到 P 父节点来设置 align + $currentImg.parents('p').css({ + 'text-align': 'left' + }).attr('align', 'left'); + }; + customCommand(e, function () { + setTimeout(hide, 100); + }); + }); + + // // 右浮动 + // $floatRight.click(function (e) { + // commandFn = function () { + // $currentImg.css({ + // float: 'right' + // }); + // }; + // customCommand(e, function () { + // setTimeout(hide, 100); + // }); + // }); + + // alignRight + $alignRight.click(function (e) { + commandFn = function () { + // 如果 img 增加了链接,那么 img.parent() 就是 a 标签,设置 align 没用的,因此必须找到 P 父节点来设置 align + $currentImg.parents('p').css({ + 'text-align': 'right' + }).attr('align', 'right'); + }; + customCommand(e, function () { + setTimeout(hide, 100); + }); + }); + + // // 无浮动 + // $noFloat.click(function (e) { + // commandFn = function () { + // $currentImg.css({ + // float: 'none' + // }); + // }; + // customCommand(e, function () { + // setTimeout(hide, 100); + // }); + // }); + + // alignCenter + $alignCenter.click(function (e) { + commandFn = function () { + // 如果 img 增加了链接,那么 img.parent() 就是 a 标签,设置 align 没用的,因此必须找到 P 父节点来设置 align + $currentImg.parents('p').css({ + 'text-align': 'center' + }).attr('align', 'center'); + }; + customCommand(e, function () { + setTimeout(hide, 100); + }); + }); + + // link + // 显示链接input + $link.click(function (e) { + e.preventDefault(); + + // 获取当前链接,并显示 + currentLink = imgLink(e); + $linkInput.val(currentLink); + + $menuContainer.hide(); + $linkInputContainer.show(); + }); + // 设置链接 + $linkBtnSubmit.click(function (e) { + e.preventDefault(); + + var url = $.trim($linkInput.val()); + if (url) { + // 设置链接,同时会自动更新 currentLink 的值 + imgLink(e, url); + } + + // 隐藏 toolbar + setTimeout(hide); + }); + // 取消设置链接 + $linkBtnCancel.click(function (e) { + e.preventDefault(); + + // 重置链接 input + $linkInput.val(currentLink); + + $menuContainer.show(); + $linkInputContainer.hide(); + }); + + // unlink + $unLink.click(function (e) { + e.preventDefault(); + + // 执行 unlink + imgLink(e, ''); + + // 隐藏 toolbar + setTimeout(hide); + }); + } + + // 绑定drag事件 + function bindDragEvent() { + var _x, _y; + var dragMarginLeft, dragMarginTop; + var imgWidth, imgHeight; + + function mousemove (e) { + var diffX, diffY; + + // 计算差额 + diffX = e.pageX - _x; + diffY = e.pageY - _y; + + // --------- 计算拖拽点的位置 --------- + var currentDragMarginLeft = dragMarginLeft + diffX; + var currentDragMarginTop = dragMarginTop + diffY; + $dragPoint.css({ + 'margin-left': currentDragMarginLeft, + 'margin-top': currentDragMarginTop + }); + + // --------- 计算图片的大小 --------- + var currentImgWidth = imgWidth + diffX; + var currentImggHeight = imgHeight + diffY; + $currentImg && $currentImg.css({ + width: currentImgWidth, + height: currentImggHeight + }); + } + + $dragPoint.on('mousedown', function(e){ + if (!$currentImg) { + return; + } + // 当前鼠标位置 + _x = e.pageX; + _y = e.pageY; + + // 当前拖拽点的位置 + dragMarginLeft = parseFloat($dragPoint.css('margin-left'), 10); + dragMarginTop = parseFloat($dragPoint.css('margin-top'), 10); + + // 当前图片的大小 + imgWidth = $currentImg.width(); + imgHeight = $currentImg.height(); + + // 隐藏 $toolbar + $toolbar.hide(); + + // 绑定计算事件 + E.$document.on('mousemove._dragResizeImg', mousemove); + E.$document.on('mouseup._dragResizeImg', function (e) { + // 取消绑定 + E.$document.off('mousemove._dragResizeImg'); + E.$document.off('mouseup._dragResizeImg'); + + // 隐藏,并还原拖拽点的位置 + hide(); + $dragPoint.css({ + 'margin-left': dragMarginLeft, + 'margin-top': dragMarginTop + }); + + // 记录 + isOnDrag = false; + }); + + // 记录 + isOnDrag = true; + }); + } + + // 显示 toolbar + function show() { + if (editor._disabled) { + // 编辑器已经被禁用,则不让显示 + return; + } + if ($currentImg == null) { + return; + } + $currentImg.addClass('clicked'); + var imgPosition = $currentImg.position(); + var imgTop = imgPosition.top; + var imgLeft = imgPosition.left; + var imgHeight = $currentImg.outerHeight(); + var imgWidth = $currentImg.outerWidth(); + + + // --- 定位 dragpoint --- + $dragPoint.css({ + top: imgTop + imgHeight, + left: imgLeft + imgWidth + }); + + // --- 定位 toolbar --- + + // 计算初步结果 + var top = imgTop + imgHeight; + var left = imgLeft; + var marginLeft = 0; + + var txtTop = $currentTxt.position().top; + var txtHeight = $currentTxt.outerHeight(); + if (top > (txtTop + txtHeight)) { + // top 不得超出编辑范围 + top = txtTop + txtHeight; + } else { + // top 超出编辑范围,dragPoint就不显示了 + $dragPoint.show(); + } + + // 显示(方便计算 margin) + $toolbar.show(); + + // 计算 margin + var width = $toolbar.outerWidth(); + marginLeft = imgWidth / 2 - width / 2; + + // 定位 + $toolbar.css({ + top: top + 5, + left: left, + 'margin-left': marginLeft + }); + // 如果定位太靠左了 + if (marginLeft < 0) { + // 得到三角形的margin-left + $toolbar.css('margin-left', '0'); + $triangle.hide(); + } else { + $triangle.show(); + } + + // disable 菜单 + editor.disableMenusExcept(); + } + + // 隐藏 toolbar + function hide() { + if ($currentImg == null) { + return; + } + $currentImg.removeClass('clicked'); + $currentImg = null; + + $toolbar.hide(); + $dragPoint.hide(); + + // enable 菜单 + editor.enableMenusExcept(); + } + + // 判断img是否是一个表情 + function isEmotion(imgSrc) { + var result = false; + if (!editor.emotionUrls) { + return result; + } + $.each(editor.emotionUrls, function (index, url) { + var flag = false; + if (imgSrc === url) { + result = true; + flag = true; + } + if (flag) { + return false; // break 循环 + } + }); + return result; + } + + // click img 事件 + $currentTxt.on('mousedown', 'img', function (e) { + e.preventDefault(); + }).on('click', 'img', function (e) { + var $img = $(e.currentTarget); + var src = $img.attr('src'); + + if (!src || isEmotion(src)) { + // 是一个表情图标 + return; + } + + // ---------- 不是表情图标 ---------- + + // 渲染 + render(); + + if ($currentImg && ($currentImg.get(0) === $img.get(0))) { + setTimeout(hide, 100); + return; + } + + // 显示 toolbar + $currentImg = $img; + show(); + + // 默认显示menuContainer,其他默认隐藏 + $menuContainer.show(); + $linkInputContainer.hide(); + + // 阻止冒泡 + e.preventDefault(); + e.stopPropagation(); + + }).on('click keydown scroll', function (e) { + if (!isOnDrag) { + setTimeout(hide, 100); + } + }); + + }); + +}); +// 编辑区域 link toolbar +_e(function (E, $) { + E.plugin(function () { + var editor = this; + var lang = editor.config.lang; + var $txt = editor.txt.$txt; + + // 当前命中的链接 + var $currentLink; + + var $toolbar = $('
              '); + var $triangle = $('
              '); + var $triggerLink = $(' ' + lang.openLink + ''); + var isRendered; + + // 记录当前的显示/隐藏状态 + var isShow = false; + + var showTimeoutId, hideTimeoutId; + var showTimeoutIdByToolbar, hideTimeoutIdByToolbar; + + // 渲染 dom + function render() { + if (isRendered) { + return; + } + + $toolbar.append($triangle) + .append($triggerLink); + + editor.$editorContainer.append($toolbar); + + isRendered = true; + } + + // 定位 + function setPosition() { + if (!$currentLink) { + return; + } + + var position = $currentLink.position(); + var left = position.left; + var top = position.top; + var height = $currentLink.height(); + + // 初步计算top值 + var topResult = top + height + 5; + + // 判断 toolbar 是否超过了编辑器区域的下边界 + var menuHeight = editor.menuContainer.height(); + var txtHeight = editor.txt.$txt.outerHeight(); + if (topResult > menuHeight + txtHeight) { + topResult = menuHeight + txtHeight + 5; + } + + // 最终设置 + $toolbar.css({ + top: topResult, + left: left + }); + } + + // 显示 toolbar + function show() { + if (isShow) { + return; + } + + if (!$currentLink) { + return; + } + + render(); + + $toolbar.show(); + + // 设置链接 + var href = $currentLink.attr('href'); + $triggerLink.attr('href', href); + + // 定位 + setPosition(); + + isShow = true; + } + + // 隐藏 toolbar + function hide() { + if (!isShow) { + return; + } + + if (!$currentLink) { + return; + } + + $toolbar.hide(); + isShow = false; + } + + // $txt 绑定事件 + $txt.on('mouseenter', 'a', function (e) { + // 延时 500ms 显示toolbar + if (showTimeoutId) { + clearTimeout(showTimeoutId); + } + showTimeoutId = setTimeout(function () { + var a = e.currentTarget; + var $a = $(a); + $currentLink = $a; + + var $img = $a.children('img'); + if ($img.length) { + // 该链接下包含一个图片 + + // 图片点击时,隐藏toolbar + $img.click(function (e) { + hide(); + }); + + if ($img.hasClass('clicked')) { + // 图片还处于clicked状态,则不显示toolbar + return; + } + } + + // 显示toolbar + show(); + }, 500); + }).on('mouseleave', 'a', function (e) { + // 延时 500ms 隐藏toolbar + if (hideTimeoutId) { + clearTimeout(hideTimeoutId); + } + hideTimeoutId = setTimeout(hide, 500); + }).on('click keydown scroll', function (e) { + setTimeout(hide, 100); + }); + // $toolbar 绑定事件 + $toolbar.on('mouseenter', function (e) { + // 先中断掉 $txt.mouseleave 导致的隐藏 + if (hideTimeoutId) { + clearTimeout(hideTimeoutId); + } + }).on('mouseleave', function (e) { + // 延时 500ms 显示toolbar + if (showTimeoutIdByToolbar) { + clearTimeout(showTimeoutIdByToolbar); + } + showTimeoutIdByToolbar = setTimeout(hide, 500); + }); + }); +}); +// menu吸顶 +_e(function (E, $) { + + E.plugin(function () { + var editor = this; + var menuFixed = editor.config.menuFixed; + if (menuFixed === false || typeof menuFixed !== 'number') { + // 没有配置菜单吸顶 + return; + } + var bodyMarginTop = parseFloat(E.$body.css('margin-top'), 10); + if (isNaN(bodyMarginTop)) { + bodyMarginTop = 0; + } + + var $editorContainer = editor.$editorContainer; + var editorTop = $editorContainer.offset().top; + var editorHeight = $editorContainer.outerHeight(); + + var $menuContainer = editor.menuContainer.$menuContainer; + var menuCssPosition = $menuContainer.css('position'); + var menuCssTop = $menuContainer.css('top'); + var menuTop = $menuContainer.offset().top; + var menuHeight = $menuContainer.outerHeight(); + + var $txt = editor.txt.$txt; + + E.$window.scroll(function () { + //全屏模式不支持 + if (editor.isFullScreen) { + return; + } + + var sTop = E.$window.scrollTop(); + + // 需要重新计算宽度,因为浏览器可能此时出现滚动条 + var menuWidth = $menuContainer.width(); + + // 如果 menuTop === 0 说明此前编辑器一直隐藏,后来显示出来了,要重新计算相关数据 + if (menuTop === 0) { + menuTop = $menuContainer.offset().top; + editorTop = $editorContainer.offset().top; + editorHeight = $editorContainer.outerHeight(); + menuHeight = $menuContainer.outerHeight(); + } + + if (sTop >= menuTop && sTop + menuFixed + menuHeight + 30 < editorTop + editorHeight) { + // 吸顶 + $menuContainer.css({ + position: 'fixed', + top: menuFixed + }); + + // 固定宽度 + $menuContainer.width(menuWidth); + + // 增加body margin-top + E.$body.css({ + 'margin-top': bodyMarginTop + menuHeight + }); + + // 记录 + if (!editor._isMenufixed) { + editor._isMenufixed = true; + } + } else { + // 取消吸顶 + $menuContainer.css({ + position: menuCssPosition, + top: menuCssTop + }); + + // 取消宽度固定 + $menuContainer.css('width', '100%'); + + // 还原 body margin-top + E.$body.css({ + 'margin-top': bodyMarginTop + }); + + // 撤销记录 + if (editor._isMenufixed) { + editor._isMenufixed = false; + } + } + }); + }); + +}); +// 缩进 菜单插件 +_e(function (E, $) { + + // 用 createMenu 方法创建菜单 + E.createMenu(function (check) { + + // 定义菜单id,不要和其他菜单id重复。编辑器自带的所有菜单id,可通过『参数配置-自定义菜单』一节查看 + var menuId = 'indent'; + + // check将检查菜单配置(『参数配置-自定义菜单』一节描述)中是否该菜单id,如果没有,则忽略下面的代码。 + if (!check(menuId)) { + return; + } + + // this 指向 editor 对象自身 + var editor = this; + + // 创建 menu 对象 + var menu = new E.Menu({ + editor: editor, // 编辑器对象 + id: menuId, // 菜单id + title: '缩进', // 菜单标题 + + // 正常状态和选中装下的dom对象,样式需要自定义 + $domNormal: $(''), + $domSelected: $('') + }); + + // 菜单正常状态下,点击将触发该事件 + menu.clickEvent = function (e) { + var elem = editor.getRangeElem(); + var p = editor.getSelfOrParentByName(elem, 'p'); + var $p; + + if (!p) { + // 未找到 p 元素,则忽略 + return e.preventDefault(); + } + $p = $(p); + + // 使用自定义命令 + function commandFn() { + $p.css('text-indent', '2em'); + } + editor.customCommand(e, commandFn); + }; + + // 菜单选中状态下,点击将触发该事件 + menu.clickEventSelected = function (e) { + var elem = editor.getRangeElem(); + var p = editor.getSelfOrParentByName(elem, 'p'); + var $p; + + if (!p) { + // 未找到 p 元素,则忽略 + return e.preventDefault(); + } + $p = $(p); + + // 使用自定义命令 + function commandFn() { + $p.css('text-indent', '0'); + } + editor.customCommand(e, commandFn); + }; + + // 根据当前选区,自定义更新菜单的选中状态或者正常状态 + menu.updateSelectedEvent = function () { + // 获取当前选区所在的父元素 + var elem = editor.getRangeElem(); + var p = editor.getSelfOrParentByName(elem, 'p'); + var $p; + var indent; + + if (!p) { + // 未找到 p 元素,则标记为未处于选中状态 + return false; + } + $p = $(p); + indent = $p.css('text-indent'); + + if (!indent || indent === '0px') { + // 得到的p,text-indent 属性是 0,则标记为未处于选中状态 + return false; + } + + // 找到 p 元素,并且 text-indent 不是 0,则标记为选中状态 + return true; + }; + + // 增加到editor对象中 + editor.menus[menuId] = menu; + }); + +}); +// 行高 菜单插件 +_e(function (E, $) { + + // 用 createMenu 方法创建菜单 + E.createMenu(function (check) { + + // 定义菜单id,不要和其他菜单id重复。编辑器自带的所有菜单id,可通过『参数配置-自定义菜单』一节查看 + var menuId = 'lineheight'; + + // check将检查菜单配置(『参数配置-自定义菜单』一节描述)中是否该菜单id,如果没有,则忽略下面的代码。 + if (!check(menuId)) { + return; + } + + // this 指向 editor 对象自身 + var editor = this; + + // 由于浏览器自身不支持 lineHeight 命令,因此要做一个hook + editor.commandHooks.lineHeight = function (value) { + var rangeElem = editor.getRangeElem(); + var targetElem = editor.getSelfOrParentByName(rangeElem, 'p,h1,h2,h3,h4,h5,pre'); + if (!targetElem) { + return; + } + $(targetElem).css('line-height', value + ''); + }; + + // 创建 menu 对象 + var menu = new E.Menu({ + editor: editor, // 编辑器对象 + id: menuId, // 菜单id + title: '行高', // 菜单标题 + commandName: 'lineHeight', // 命令名称 + + // 正常状态和选中装下的dom对象,样式需要自定义 + $domNormal: $(''), + $domSelected: $('') + }); + + // 数据源 + var data = { + // 格式: 'value' : 'title' + '1.0': '1.0倍', + '1.5': '1.5倍', + '1.8': '1.8倍', + '2.0': '2.0倍', + '2.5': '2.5倍', + '3.0': '3.0倍' + }; + + // 为menu创建droplist对象 + var tpl = '{#title}'; + menu.dropList = new E.DropList(editor, menu, { + data: data, // 传入数据源 + tpl: tpl // 传入模板 + }); + + // 增加到editor对象中 + editor.menus[menuId] = menu; + + }); + +}); +// 自定义上传 +_e(function (E, $) { + + E.plugin(function () { + + var editor = this; + var customUpload = editor.config.customUpload; + if (!customUpload) { + return; + } else if (editor.config.uploadImgUrl) { + alert('自定义上传无效,详看浏览器日志console.log'); + E.error('已经配置了 uploadImgUrl ,就不能再配置 customUpload ,两者冲突。将导致自定义上传无效。'); + return; + } + + var $uploadContent = editor.$uploadContent; + if (!$uploadContent) { + E.error('自定义上传,无法获取 editor.$uploadContent'); + } + + // UI + var $uploadIcon = $('
              '); + $uploadContent.append($uploadIcon); + + // 设置id,并暴露 + var btnId = 'upload' + E.random(); + var containerId = 'upload' + E.random(); + $uploadIcon.attr('id', btnId); + $uploadContent.attr('id', containerId); + + editor.customUploadBtnId = btnId; + editor.customUploadContainerId = containerId; + }); + +}); +// 版权提示 +_e(function (E, $) { + E.info('本页面富文本编辑器由 wangEditor 提供 http://wangeditor.github.io/ '); +}); + + // 最终返回wangEditor构造函数 + return window.wangEditor; +}); \ No newline at end of file diff --git a/public/static/libs/wang-editor/js/wangEditor.min.js b/public/static/libs/wang-editor/js/wangEditor.min.js new file mode 100644 index 0000000..d5d4e4c --- /dev/null +++ b/public/static/libs/wang-editor/js/wangEditor.min.js @@ -0,0 +1,3 @@ +!function(a){"function"==typeof window.define?window.define.amd?window.define("wangEditor",["jquery"],a):window.define.cmd?window.define(function(){return a}):a(window.jQuery):"object"==typeof module&&"object"==typeof module.exports?(window.wangEditorCssPath?require(window.wangEditorCssPath):require("../css/wangEditor.css"),module.exports=a(window.wangEditorJQueryPath?require(window.wangEditorJQueryPath):require("jquery"))):a(window.jQuery)}(function(a){if(!a||!a.fn||!a.fn.jquery)return alert("在引用wangEditor.js之前,先引用jQuery,否则无法使用 wangEditor"),void 0;var b=function(b){var c=window.wangEditor;c&&b(c,a)};return function(a,b){if(a.wangEditor)return alert("一个页面不能重复引用 wangEditor.js 或 wangEditor.min.js !!!"),void 0;var c=function(a){var c,d;"string"==typeof a&&(a="#"+a),c=b(a),1===c.length&&(d=c[0].nodeName,("TEXTAREA"===d||"DIV"===d)&&(this.valueNodeName=d.toLowerCase(),this.$valueContainer=c,this.$prev=c.prev(),this.$parent=c.parent(),this.init()))};c.fn=c.prototype,c.$body=b("body"),c.$document=b(document),c.$window=b(a),c.userAgent=navigator.userAgent,c.getComputedStyle=a.getComputedStyle,c.w3cRange="function"==typeof document.createRange,c.hostname=location.hostname.toLowerCase(),c.websiteHost="wangeditor.github.io|www.wangeditor.com|wangeditor.coding.me",c.isOnWebsite=c.websiteHost.indexOf(c.hostname)>=0,c.docsite="http://www.kancloud.cn/wangfupeng/wangeditor2/113961",a.wangEditor=c,c.plugin=function(a){c._plugins||(c._plugins=[]),"function"==typeof a&&c._plugins.push(a)}}(window,a),b(function(a){a.fn.init=function(){this.initDefaultConfig(),this.addEditorContainer(),this.addTxt(),this.addMenuContainer(),this.menus={},this.commandHooks()}}),b(function(a,b){a.fn.ready=function(a){this.readyFns||(this.readyFns=[]),this.readyFns.push(a)},a.fn.readyHeadler=function(){for(var a=this.readyFns;a.length;)a.shift().call(this)},a.fn.updateValue=function(){var d,a=this,b=a.$valueContainer,c=a.txt.$txt;b!==c&&(d=c.html(),b.val(d))},a.fn.getInitValue=function(){var a=this,b=a.$valueContainer,c="",d=a.valueNodeName;return"div"===d?c=b.html():"textarea"===d&&(c=b.val()),c},a.fn.updateMenuStyle=function(){var a=this.menus;b.each(a,function(a,b){b.updateSelected()})},a.fn.enableMenusExcept=function(a){this._disabled||(a=a||[],"string"==typeof a&&(a=[a]),b.each(this.menus,function(b,c){a.indexOf(b)>=0||c.disabled(!1)}))},a.fn.disableMenusExcept=function(a){this._disabled||(a=a||[],"string"==typeof a&&(a=[a]),b.each(this.menus,function(b,c){a.indexOf(b)>=0||c.disabled(!0)}))},a.fn.hideDropPanelAndModal=function(){var a=this.menus;b.each(a,function(a,b){var c=b.dropPanel||b.dropList||b.modal;c&&c.hide&&c.hide()})}}),b(function(a,b){function d(){}var c=!a.w3cRange;a.fn.currentRange=function(a){return a?(this._rangeData=a,void 0):this._rangeData},a.fn.collapseRange=function(a,b){b=b||"end",b="start"===b?!0:!1,a=a||this.currentRange(),a&&(a.collapse(b),this.currentRange(a))},a.fn.getRangeText=c?d:function(a){return(a=a||this.currentRange())?a.toString():void 0},a.fn.getRangeElem=c?d:function(a){a=a||this.currentRange();var b=a.commonAncestorContainer;return 1===b.nodeType?b:b.parentNode},a.fn.isRangeEmpty=c?d:function(a){return a=a||this.currentRange(),a&&a.startContainer&&a.startContainer===a.endContainer&&a.startOffset===a.endOffset?!0:!1},a.fn.saveSelection=c?d:function(a){var d,e,c=this,f=c.txt.$txt.get(0);a?d=a.commonAncestorContainer:(e=document.getSelection(),e.getRangeAt&&e.rangeCount&&(a=document.getSelection().getRangeAt(0),d=a.commonAncestorContainer)),d&&(b.contains(f,d)||f===d)&&c.currentRange(a)},a.fn.restoreSelection=c?d:function(b){var c;if(b=b||this.currentRange())try{c=document.getSelection(),c.removeAllRanges(),c.addRange(b)}catch(d){a.error("执行 editor.restoreSelection 时,IE可能会有异常,不影响使用")}},a.fn.restoreSelectionByElem=c?d:function(a,b){a&&(b=b||"end",this.setRangeByElem(a),"start"===b&&this.collapseRange(this.currentRange(),"start"),"end"===b&&this.collapseRange(this.currentRange(),"end"),this.restoreSelection())},a.fn.initSelection=c?d:function(){var c,d,a=this;a.currentRange()||(c=a.txt.$txt,d=c.children().first(),d.length&&a.restoreSelectionByElem(d.get(0)))},a.fn.setRangeByElem=c?d:function(a){var e,f,g,c=this,d=c.txt.$txt.get(0);if(a&&b.contains(d,a)){for(e=a.firstChild;e&&3!==e.nodeType;)e=e.firstChild;for(f=a.lastChild;f&&3!==f.nodeType;)f=f.lastChild;g=document.createRange(),e&&f?(g.setStart(e,0),g.setEnd(f,f.textContent.length)):(g.setStart(a,0),g.setEnd(a,0)),c.saveSelection(g)}}}),b(function(a,b){a.w3cRange||(a.fn.getRangeText=function(a){return(a=a||this.currentRange())?a.text:void 0},a.fn.getRangeElem=function(a){if(a=a||this.currentRange()){var b=a.parentElement();return 1===b.nodeType?b:b.parentNode}},a.fn.isRangeEmpty=function(a){return a=a||this.currentRange(),a&&a.text?!1:!0},a.fn.saveSelection=function(a){var d,c=this,f=c.txt.$txt.get(0);a?d=a.parentElement():(a=document.selection.createRange(),d="undefined"==typeof a.parentElement?null:a.parentElement()),d&&(b.contains(f,d)||f===d)&&c.currentRange(a)},a.fn.restoreSelection=function(a){var d,b=this;if(a=a||b.currentRange()){d=document.selection.createRange();try{d.setEndPoint("EndToEnd",a)}catch(e){}if(0===a.text.length)try{d.collapse(!1)}catch(e){}else d.setEndPoint("StartToStart",a);d.select()}})}),b(function(a,b){a.fn.commandHooks=function(){var a=this,c={};c.insertHtml=function(c){var d=b(c),e=a.getRangeElem(),f=a.getLegalTags(e);f&&b(f).after(d)},a.commandHooks=c}}),b(function(a){a.fn.command=function(a,b,c,d){function g(){b&&(e.queryCommandSupported(b)?document.execCommand(b,!1,c):(f=e.commandHooks,b in f&&f[b](c)))}var f,e=this;this.customCommand(a,g,d)},a.fn.commandForElem=function(a,b,c,d,e){var f,g,h;"string"==typeof a?f=a:(f=a.selector,g=a.check),h=this.getRangeElem(),h=this.getSelfOrParentByName(h,f,g),h&&this.setRangeByElem(h),this.command(b,c,d,e)},a.fn.customCommand=function(a,b,c){function f(){d.hideDropPanelAndModal()}var d=this,e=d.currentRange();return e?(d.undoRecord(),this.restoreSelection(e),b.call(d),this.saveSelection(),this.restoreSelection(),c&&"function"==typeof c&&c.call(d),d.txt.insertEmptyP(),d.txt.wrapImgAndText(),d.updateValue(),d.updateMenuStyle(),setTimeout(f,200),a&&a.preventDefault(),void 0):(a&&a.preventDefault(),void 0)},a.fn.queryCommandValue=function(a){var b="";try{b=document.queryCommandValue(a)}catch(c){}return b},a.fn.queryCommandState=function(a){var b=!1;try{b=document.queryCommandState(a)}catch(c){}return b},a.fn.queryCommandSupported=function(a){var b=!1;try{b=document.queryCommandSupported(a)}catch(c){}return b}}),b(function(a,b){function d(a){var c=this,d=b(a),e=!1;return d.each(function(){return this===c?(e=!0,!1):void 0}),e}var c;a.fn.getLegalTags=function(b){var c=this.config.legalTags;return c?this.getSelfOrParentByName(b,c):(a.error("配置项中缺少 legalTags 的配置"),void 0)},a.fn.getSelfOrParentByName=function(a,e,f){if(a&&e){c||(c=a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.matchesSelector),c||(c=d);for(var g=this.txt.$txt.get(0);a&&g!==a&&b.contains(g,a);){if(c.call(a,e)){if(!f)return a;if(f(a))return a}a=a.parentNode}}}}),b(function(a){function d(a){return null==a._redoList&&(a._redoList=[]),a._redoList}function e(a){return null==a._undoList&&(a._undoList=[]),a._undoList}function f(a,b,c){var d=b.val,e=a.txt.$txt.html();if(null!=d){if(d===e)return"redo"===c?(a.redo(),void 0):"undo"===c?(a.undo(),void 0):void 0;a.txt.$txt.html(d),a.updateValue(),a.onchange&&"function"==typeof a.onchange&&a.onchange.call(a)}}var c=20;a.fn.undoRecord=function(){var a=this,b=a.txt.$txt,f=b.html(),g=e(a),h=d(a),i=g.length?g[0]:"";f!==i.val&&(h.length&&(h=[]),g.unshift({range:a.currentRange(),val:f}),g.length>c&&g.pop())},a.fn.undo=function(){var g,a=this,b=e(a),c=d(a);b.length&&(g=b.shift(),c.unshift(g),f(this,g,"undo"))},a.fn.redo=function(){var g,a=this,b=e(a),c=d(a);c.length&&(g=c.shift(),b.unshift(g),f(this,g,"redo"))}}),b(function(a,b){a.fn.create=function(){var d,c=this;a.$body&&0!==a.$body.length||(a.$body=b("body"),a.$document=b(document),a.$window=b(window)),c.addMenus(),c.renderMenus(),c.renderMenuContainer(),c.renderTxt(),c.renderEditorContainer(),c.eventMenus(),c.eventMenuContainer(),c.eventTxt(),c.readyHeadler(),c.initSelection(),c.$txt=c.txt.$txt,d=a._plugins,d&&d.length&&b.each(d,function(a,b){b.call(c)})},a.fn.disable=function(){this.txt.$txt.removeAttr("contenteditable"),this.disableMenusExcept(),this._disabled=!0},a.fn.enable=function(){this._disabled=!1,this.txt.$txt.attr("contenteditable","true"),this.enableMenusExcept()},a.fn.destroy=function(){var a=this,b=a.$valueContainer,c=a.$editorContainer,d=a.valueNodeName;"div"===d?(b.removeAttr("contenteditable"),c.after(b),c.hide()):(b.show(),c.hide())},a.fn.undestroy=function(){var a=this,b=a.$valueContainer,c=a.$editorContainer,d=a.menuContainer.$menuContainer,e=a.valueNodeName;"div"===e?(b.attr("contenteditable","true"),d.after(b),c.show()):(b.hide(),c.show())},a.fn.clear=function(){var a=this,b=a.txt.$txt;b.html("


              "),a.restoreSelectionByElem(b.find("p").get(0))}}),b(function(a){var c=function(a){this.editor=a,this.init()};c.fn=c.prototype,a.MenuContainer=c}),b(function(a,b){var c=a.MenuContainer;c.fn.init=function(){var a=this,c=b('
              ');a.$menuContainer=c,a.changeShadow()},c.fn.changeShadow=function(){var a=this.$menuContainer,b=this.editor,c=b.txt.$txt;c.on("scroll",function(){c.scrollTop()>10?a.addClass("wangEditor-menu-shadow"):a.removeClass("wangEditor-menu-shadow")})}}),b(function(a,b){var c=a.MenuContainer;c.fn.render=function(){var a=this.$menuContainer,b=this.editor.$editorContainer;b.append(a)},c.fn.height=function(){var a=this.$menuContainer;return a.height()},c.fn.appendMenu=function(a,b){return this._addGroup(a),this._addOneMenu(b)},c.fn._addGroup=function(a){var d,c=this.$menuContainer;this.$currentGroup&&this.currentGroupIdx===a||(d=b(''),c.append(d),this.$currentGroup=d,this.currentGroupIdx=a)},c.fn._addOneMenu=function(a){var c=a.$domNormal,d=a.$domSelected,e=this.$currentGroup,f=b('');return d.hide(),f.append(c).append(d),e.append(f),f}}),b(function(a){var c=function(a){this.editor=a.editor,this.id=a.id,this.title=a.title,this.$domNormal=a.$domNormal,this.$domSelected=a.$domSelected||a.$domNormal,this.commandName=a.commandName,this.commandValue=a.commandValue,this.commandNameSelected=a.commandNameSelected||a.commandName,this.commandValueSelected=a.commandValueSelected||a.commandValue};c.fn=c.prototype,a.Menu=c}),b(function(a,b){var c=a.Menu;c.fn.initUI=function(){var c=this.editor,d=c.UI.menus,e=this.id,f=d[e];this.$domNormal&&this.$domSelected||(null==f&&(a.warn('editor.UI配置中,没有菜单 "'+e+'" 的UI配置,只能取默认值'),f=d["default"]),this.$domNormal=b(f.normal),this.$domSelected=/^\./.test(f.selected)?this.$domNormal.clone().addClass(f.selected.slice(1)):b(f.selected))}}),b(function(a,b){var c=a.Menu;c.fn.render=function(a){var b,c,d,e;this.initUI(),b=this.editor,c=b.menuContainer,d=c.appendMenu(a,this),e=this.onRender,this._renderTip(d),e&&"function"==typeof e&&e.call(this)},c.fn._renderTip=function(c){function i(){g.show()}function j(){g.hide()}var h,k,d=this,e=d.editor,f=d.title,g=b('');d.tipWidth||(h=b('

              '+f+"

              "),a.$body.append(h),e.ready(function(){var b=h.outerWidth()+5,c=g.outerWidth(),e=parseFloat(g.css("margin-left"),10);h.remove(),h=null,g.css({width:b,"margin-left":e+(c-b)/2}),d.tipWidth=b})),g.append(f),c.append(g),c.find("a").on("mouseenter",function(){d.active()||d.disabled()||(k=setTimeout(i,200))}).on("mouseleave",function(){k&&clearTimeout(k),j()}).on("click",j)},c.fn.bindEvent=function(){var f,b=this,c=b.$domNormal,d=b.$domSelected,e=b.clickEvent;e||(e=function(c){var e,f,g,h,d=b.dropPanel||b.dropList||b.modal;return d&&d.show?(d.isShowing?d.hide():d.show(),void 0):(e=b.editor,h=b.selected,h?(f=b.commandNameSelected,g=b.commandValueSelected):(f=b.commandName,g=b.commandValue),f?e.command(c,f,g):(a.warn('菜单 "'+b.id+'" 未定义click事件'),c.preventDefault()),void 0)}),f=b.clickEventSelected||e,c.click(function(a){b.disabled()||(e.call(b,a),b.updateSelected()),a.preventDefault()}),d.click(function(a){b.disabled()||(f.call(b,a),b.updateSelected()),a.preventDefault()})},c.fn.updateSelected=function(){var c,d,a=this;a.editor,c=a.updateSelectedEvent,c||(c=function(){var a=this,b=a.editor,c=a.commandName,d=a.commandValue;if(d){if(b.queryCommandValue(c).toLowerCase()===d.toLowerCase())return!0}else if(b.queryCommandState(c))return!0;return!1}),d=c.call(a),d=!!d,a.changeSelectedState(d)},c.fn.changeSelectedState=function(a){var b=this,c=b.selected;if(null!=a&&"boolean"==typeof a){if(c===a)return;b.selected=a,a?(b.$domNormal.hide(),b.$domSelected.show()):(b.$domNormal.show(),b.$domSelected.hide())}},c.fn.active=function(a){return null==a?this._activeState:(this._activeState=a,void 0)},c.fn.activeStyle=function(a){var c,d;this.selected,c=this.$domNormal,d=this.$domSelected,a?(c.addClass("active"),d.addClass("active")):(c.removeClass("active"),d.removeClass("active")),this.active(a)},c.fn.disabled=function(a){var b,c;return null==a?!!this._disabled:(this._disabled!==a&&(b=this.$domNormal,c=this.$domSelected,a?(b.addClass("disable"),c.addClass("disable")):(b.removeClass("disable"),c.removeClass("disable")),this._disabled=a),void 0)}}),b(function(a){var c=function(a,b,c){this.editor=a,this.menu=b,this.data=c.data,this.tpl=c.tpl,this.selectorForELemCommand=c.selectorForELemCommand,this.beforeEvent=c.beforeEvent,this.afterEvent=c.afterEvent,this.init()};c.fn=c.prototype,a.DropList=c}),b(function(a,b){var c=a.DropList;c.fn.init=function(){var a=this;a.initDOM(),a.bindEvent(),a.initHideEvent()},c.fn.initDOM=function(){var f,g,a=this,c=a.data,d=a.tpl||"{#title}",e=b('
              ');b.each(c,function(a,c){f=d.replace(/{#commandValue}/gi,a).replace(/{#title}/gi,c),g=b(''),g.append(f),e.append(g)}),a.$list=e},c.fn.bindEvent=function(){var a=this,c=a.editor,d=a.menu,e=d.commandName,f=a.selectorForELemCommand,g=a.$list,h=a.beforeEvent,i=a.afterEvent;g.on("click","a[commandValue]",function(a){h&&"function"==typeof h&&h.call(a);var g=b(a.currentTarget).attr("commandValue");d.selected&&c.isRangeEmpty()&&f?c.commandForElem(f,a,e,g):c.command(a,e,g),i&&"function"==typeof i&&i.call(a)})},c.fn.initHideEvent=function(){var c=this,d=c.$list.get(0);a.$body.on("click",function(a){var e,f,g;c.isShowing&&(e=a.target,f=c.menu,g=f.selected?f.$domSelected.get(0):f.$domNormal.get(0),g===e||b.contains(g,e)||d===e||b.contains(d,e)||c.hide())}),a.$window.scroll(function(){c.hide()}),a.$window.on("resize",function(){c.hide()})}}),b(function(a){var c=a.DropList;c.fn._render=function(){var a=this,b=a.editor,c=a.$list;b.$editorContainer.append(c),a.rendered=!0},c.fn._position=function(){var a=this,b=a.$list,c=a.editor,d=a.menu,e=c.menuContainer.$menuContainer,f=d.selected?d.$domSelected:d.$domNormal,g=f.offsetParent().position(),h=g.top,i=g.left,j=f.offsetParent().height(),k=f.offsetParent().width(),l=b.outerWidth(),m=c.txt.$txt.outerWidth(),n=h+j,o=i+k/2,p=0-k/2,q=o+l-m;q>-10&&(p=p-q-10),b.css({top:n,left:o,"margin-left":p}),c._isMenufixed&&(n+=e.offset().top+e.outerHeight()-b.offset().top,b.css({top:n}))},c.fn.show=function(){var c,a=this,b=a.menu;a.rendered||a._render(),a.isShowing||(c=a.$list,c.show(),a._position(),a.isShowing=!0,b.activeStyle(!0))},c.fn.hide=function(){var c,a=this,b=a.menu;a.isShowing&&(c=a.$list,c.hide(),a.isShowing=!1,b.activeStyle(!1))}}),b(function(a){var c=function(a,b,c){this.editor=a,this.menu=b,this.$content=c.$content,this.width=c.width||200,this.height=c.height,this.onRender=c.onRender,this.init()};c.fn=c.prototype,a.DropPanel=c}),b(function(a,b){var c=a.DropPanel;c.fn.init=function(){var a=this;a.initDOM(),a.initHideEvent()},c.fn.initDOM=function(){var a=this,c=a.$content,d=a.width,e=a.height,f=b('
              '),g=b('
              ');f.css({width:d,height:e?e:"auto"}),f.append(g),f.append(c),a.$panel=f,a.$triangle=g},c.fn.initHideEvent=function(){var c=this,d=c.$panel.get(0);a.$body.on("click",function(a){var e,f,g;c.isShowing&&(e=a.target,f=c.menu,g=f.selected?f.$domSelected.get(0):f.$domNormal.get(0),g===e||b.contains(g,e)||d===e||b.contains(d,e)||c.hide())}),a.$window.scroll(function(){c.hide()}),a.$window.on("resize",function(){c.hide()})}}),b(function(a,b){var c=a.DropPanel;c.fn._render=function(){var a=this,b=a.onRender,c=a.editor,d=a.$panel;c.$editorContainer.append(d),b&&b.call(a),a.rendered=!0},c.fn._position=function(){var s,a=this,b=a.$panel,c=a.$triangle,d=a.editor,e=d.menuContainer.$menuContainer,f=a.menu,g=f.selected?f.$domSelected:f.$domNormal,h=g.offsetParent().position(),i=h.top,j=h.left,k=g.offsetParent().height(),l=g.offsetParent().width(),m=b.outerWidth(),n=d.txt.$txt.outerWidth(),o=i+k,p=j+l/2,q=0-m/2,r=q;0-q>p-10&&(q=0-(p-10)),s=p+m+q-n,s>-10&&(q=q-s-10),b.css({top:o,left:p,"margin-left":q}),d._isMenufixed&&(o+=e.offset().top+e.outerHeight()-b.offset().top,b.css({top:o})),c.css({"margin-left":r-q-5})},c.fn.focusFirstInput=function(){var a=this,c=a.$panel;c.find("input[type=text],textarea").each(function(){var a=b(this);return null==a.attr("disabled")?(a.focus(),!1):void 0})},c.fn.show=function(){var d,b=this,c=b.menu;b.rendered||b._render(),b.isShowing||(d=b.$panel,d.show(),b._position(),b.isShowing=!0,c.activeStyle(!0),a.w3cRange?b.focusFirstInput():a.placeholderForIE8(d))},c.fn.hide=function(){var c,a=this,b=a.menu;a.isShowing&&(c=a.$panel,c.hide(),a.isShowing=!1,b.activeStyle(!1))}}),b(function(a){var c=function(a,b,c){this.editor=a,this.menu=b,this.$content=c.$content,this.init()};c.fn=c.prototype,a.Modal=c}),b(function(a,b){var c=a.Modal;c.fn.init=function(){var a=this;a.initDom(),a.initHideEvent()},c.fn.initDom=function(){var a=this,c=a.$content,d=b('
              '),e=b('
              ');d.append(e),d.append(c),a.$modal=d,a.$close=e},c.fn.initHideEvent=function(){var c=this,d=c.$close,e=c.$modal.get(0);d.click(function(){c.hide()}),a.$body.on("click",function(a){var d,f,g;c.isShowing&&(d=a.target,f=c.menu,f&&(g=f.selected?f.$domSelected.get(0):f.$domNormal.get(0),g===d||b.contains(g,d))||e===d||b.contains(e,d)||c.hide())})}}),b(function(a){var c=a.Modal;c.fn._render=function(){var b=this,c=b.editor,d=b.$modal;d.css("z-index",c.config.zindex+10+""),a.$body.append(d),b.rendered=!0},c.fn._position=function(){var b=this,c=b.$modal,d=c.offset().top,e=c.outerWidth(),f=c.outerHeight(),g=0-e/2,h=0-f/2,i=a.$window.scrollTop();f/2>d&&(h=0-d),c.css({"margin-left":g+"px","margin-top":h+i+"px"})},c.fn.show=function(){var c,a=this,b=a.menu;a.rendered||a._render(),a.isShowing||(a.isShowing=!0,c=a.$modal,c.show(),a._position(),b&&b.activeStyle(!0))},c.fn.hide=function(){var c,a=this,b=a.menu;a.isShowing&&(a.isShowing=!1,c=a.$modal,c.hide(),b&&b.activeStyle(!1))}}),b(function(a){var c=function(a){this.editor=a,this.init()};c.fn=c.prototype,a.Txt=c}),b(function(a,b){var c=a.Txt;c.fn.init=function(){var f,a=this,c=a.editor,d=c.$valueContainer,e=c.getInitValue();"DIV"===d.get(0).nodeName?(f=d,f.addClass("wangEditor-txt"),f.attr("contentEditable","true")):f=b('
              '+e+"
              "),c.ready(function(){a.insertEmptyP()}),a.$txt=f,a.contentEmptyHandle(),a.bindEnterForDiv(),a.bindEnterForText(),a.bindTabEvent(),a.bindPasteFilter(),a.bindFormatText(),a.bindHtml()},c.fn.contentEmptyHandle=function(){var e,a=this,c=a.editor,d=a.$txt;d.on("keydown",function(a){if(8===a.keyCode){var c=b.trim(d.html().toLowerCase());return"


              "===c?(a.preventDefault(),void 0):void 0}}),d.on("keyup",function(a){if(8===a.keyCode){var f=b.trim(d.html().toLowerCase());f&&"
              "!==f||(e=b("


              "),d.html(""),d.append(e),c.restoreSelectionByElem(e.get(0)))}})},c.fn.bindEnterForDiv=function(){function h(){if(g){var a=b("

              "+g.html()+"

              ");g.after(a),g.remove()}}var d,e,f,g;a.config.legalTags,d=this,e=d.editor,f=d.$txt,f.on("keydown keyup",function(a){var c,d,f,i;if(13===a.keyCode&&(c=e.getRangeElem(),d=e.getLegalTags(c),!d)){if(d=e.getSelfOrParentByName(c,"div"),!d)return;f=b(d),"keydown"===a.type&&(g=f,setTimeout(h,0)),"keyup"===a.type&&(i=b("

              "+f.html()+"

              "),f.after(i),f.remove(),e.restoreSelectionByElem(i.get(0),"start"))}})},c.fn.bindEnterForText=function(){var c,a=this,b=a.$txt;b.on("keyup",function(b){13===b.keyCode&&(c||(c=function(){a.wrapImgAndText()}),setTimeout(c))})},c.fn.bindTabEvent=function(){var a=this,b=a.editor,c=a.$txt;c.on("keydown",function(a){9===a.keyCode&&b.queryCommandSupported("insertHtml")&&b.command(a,"insertHtml","    ")})},c.fn.bindPasteFilter=function(){function h(a){var c,e,f,k;if(a&&a.nodeType&&a.nodeName&&(e=a.nodeName.toLowerCase(),f=a.nodeType,3===f||1===f)){if(c=b(a),"div"===e)return k=[],b.each(a.childNodes,function(a,b){k.push(b)}),b.each(k,function(){h(this)}),void 0;if(g.indexOf(e)>=0)d+=i(a);else if(3===f)d+="

              "+a.textContent+"

              ";else if("br"===e)d+="
              ";else{if(["meta","style","script","object","form","iframe","hr"].indexOf(e)>=0)return;c=b(j(a)),d+=b("
              ").append(c.clone()).html()}}}function i(a){var d,c=a.nodeName.toLowerCase(),e="",f="";return["blockquote"].indexOf(c)>=0?(d=b(a),"<"+c+">"+d.text()+""):["p","h1","h2","h3","h4","h5"].indexOf(c)>=0?(a=j(a),d=b(a),e=d.html(),e=e.replace(/<.*?>/gi,function(a){return""===a||0===a.indexOf(""+e+""):["ul","ol"].indexOf(c)>=0?(d=b(a),d.children().each(function(){var a=b(j(this)),c=a.html();c=c.replace(/<.*?>/gi,function(a){return""===a||0===a.indexOf(""}),"<"+c+">"+f+""):(d=b(j(a)),b("
              ").append(d).html())}function j(a){var f,c=a.attributes||[],d=[],e=["href","target","src","alt","rowspan","colspan"];return b.each(c,function(a,b){b&&2===b.nodeType&&d.push(b.nodeName)}),b.each(d,function(b,c){e.indexOf(c)<0&&a.removeAttribute(c)}),f=a.childNodes,f.length&&b.each(f,function(a,b){j(b)}),a}var a=this,c=a.editor,d="",e=a.$txt,f=c.config.legalTags,g=f.split(",");e.on("paste",function(e){var f,g,i,j,k;if(c.config.pasteFilter&&(f=c.getRangeElem().nodeName,"TD"!==f&&"TH"!==f)){if(d="",j=e.clipboardData||e.originalEvent.clipboardData,k=window.clipboardData,c.config.pasteText){if(j&&j.getData)g=j.getData("text/plain");else{if(!k||!k.getData)return;g=k.getData("text")}g&&(d="

              "+g+"

              ")}else if(j&&j.getData)g=j.getData("text/html"),g?(i=b("
              "+g+"
              "),h(i.get(0))):(g=j.getData("text/plain"),g&&(g=g.replace(/[ ]/g," ").replace(//g,">").replace(/\n/g,"

              "),d="

              "+g+"

              ",d=d.replace(/

              (https?:\/\/.*?)<\/p>/gi,function(a,b){return'

              '+b+"

              "})));else{if(!k||!k.getData)return;if(d=k.getData("text"),!d)return;d="

              "+d+"

              ",d=d.replace(new RegExp("\n","g"),"

              ")}d&&(c.command(e,"insertHtml",d),a.clearEmptyOrNestP())}})},c.fn.bindFormatText=function(){var e,f,g,i,c=this;c.editor,e=c.$txt,f=a.config.legalTags,g=f.split(","),g.length,i=[],b.each(g,function(a,b){var c=">\\s*<("+b+")>";i.push(new RegExp(c,"ig"))}),i.push(new RegExp(">\\s*<(li)>","ig")),i.push(new RegExp(">\\s*<(tr)>","ig")),i.push(new RegExp(">\\s*<(code)>","ig")),e.formatText=function(){var a=b("

              "),c=e.html();return c=c.replace(/\s*\n<"+b+">"}))}),a.html(c),a.text()}},c.fn.bindHtml=function(){var a=this,c=a.editor,d=a.$txt,e=c.$valueContainer,f=c.valueNodeName;d.html=function(a){var c;return"div"===f&&(c=b.fn.html.call(d,a)),void 0===a?(c=b.fn.html.call(d),c=c.replace(/(href|src)\=\"(.*)\"/gim,function(a,b,c){return b+'="'+c.replace("&","&")+'"'})):(c=b.fn.html.call(d,a),e.val(a)),void 0===a?c:(d.change(),void 0)}}}),b(function(a,b){var c=a.Txt,d="propertychange change click keyup input paste";c.fn.render=function(){var a=this.$txt,b=this.editor.$editorContainer;b.append(a)},c.fn.initHeight=function(){var a=this.editor,b=this.$txt,c=a.$valueContainer.height(),d=a.menuContainer.height(),e=c-d;e=50>e?50:e,b.height(e),a.valueContainerHeight=c,this.initMaxHeight(e,d)},c.fn.initMaxHeight=function(c,d){var i,e=this.editor,f=e.menuContainer.$menuContainer,g=this.$txt,h=b("
              ");if(window.getComputedStyle&&"max-height"in window.getComputedStyle(g.get(0))){if(i=parseInt(e.$valueContainer.css("max-height")),isNaN(i))return;if(e.menus.fullscreen)return a.warn("max-height和『全屏』菜单一起使用时,会有一些问题尚未解决,请暂时不要两个同时使用"),void 0;e.useMaxHeight=!0,h.css({"max-height":i-d+"px","overflow-y":"auto"}),g.css({height:"auto","overflow-y":"visible","min-height":c+"px"}),h.on("scroll",function(){g.parent().scrollTop()>10?f.addClass("wangEditor-menu-shadow"):f.removeClass("wangEditor-menu-shadow")}),g.wrap(h)}},c.fn.saveSelectionEvent=function(){function f(){b.saveSelection()}function g(){Date.now()-e<100||(e=Date.now(),f())}function h(){c&&clearTimeout(c),c=setTimeout(f,300)}var c,a=this.$txt,b=this.editor,e=Date.now();a.on(d+" focus blur",function(){g(),h()}),a.on("mousedown",function(){a.on("mouseleave.saveSelection",function(){g(),h(),b.updateMenuStyle()})}).on("mouseup",function(){a.off("mouseleave.saveSelection")})},c.fn.updateValueEvent=function(){function f(){var c=a.html();e!==c&&(b.onchange&&"function"==typeof b.onchange&&b.onchange.call(b),b.updateValue(),e=c)}var c,e,a=this.$txt,b=this.editor;a.on(d,function(){null==e&&(e=a.html()),c&&clearTimeout(c),c=setTimeout(f,100)})},c.fn.updateMenuStyleEvent=function(){var a=this.$txt,b=this.editor;a.on(d,function(){b.updateMenuStyle()})},c.fn.insertEmptyP=function(){var a=this.$txt,c=a.children();return 0===c.length?(a.append(b("


              ")),void 0):("
              "!==b.trim(c.last().html()).toLowerCase()&&a.append(b("


              ")),void 0)},c.fn.wrapImgAndText=function(){var g,h,a=this.$txt,c=a.children("img"),d=a[0],e=d.childNodes,f=e.length;for(c.length&&c.each(function(){b(this).wrap("

              ")}),g=0;f>g;g++)h=e[g],3===h.nodeType&&h.textContent&&b.trim(h.textContent)&&b(h).wrap("

              ")},c.fn.clearEmptyOrNestP=function(){var a=this.$txt,c=a.find("p");c.each(function(){var e,a=b(this),c=a.children(),d=c.length,f=b.trim(a.html());return f?(1===d&&(e=c.first(),e.get(0)&&"P"===e.get(0).nodeName&&a.html(e.html())),void 0):(a.remove(),void 0)})},c.fn.scrollTop=function(a){var b=this,c=b.editor,d=b.$txt;return c.useMaxHeight?d.parent().scrollTop(a):d.scrollTop(a)},c.fn.showHeightOnHover=function(){function h(a){var i,j,k,l,m,n;g||(c.append(f),g=!0),e.position().top,e.outerHeight(),i=a.height(),j=a.position().top,k=parseInt(a.css("margin-top"),10),l=parseInt(a.css("padding-top"),10),m=parseInt(a.css("margin-bottom"),10),n=parseInt(a.css("padding-bottom"),10),j+d.height(),f.css({height:i+l+k+n+m,top:j+d.height()})}function i(){g&&(f.remove(),g=!1)}var a=this.editor,c=a.$editorContainer,d=a.menuContainer,e=this.$txt,f=b(''),g=!1;e.on("mouseenter","ul,ol,blockquote,p,h1,h2,h3,h4,h5,table,pre",function(a){h(b(a.currentTarget))}).on("mouseleave",function(){i()})}}),b(function(a,b){var c,d;Array.prototype.indexOf||(Array.prototype.indexOf=function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},Array.prototype.lastIndexOf=function(a){var b=this.length;for(b-=1;b>=0;b--)if(this[b]===a)return b;return-1}),Date.now||(Date.now=function(){return(new Date).valueOf()}),c=window.console,d=function(){},b.each(["info","log","warn","error"],function(b,e){a[e]=null==c?d:function(b){a.config&&a.config.printLog&&c[e]("wangEditor提示: "+b)}}),a.random=function(){return Math.random().toString().slice(2)},a.placeholder="placeholder"in document.createElement("input"),a.placeholderForIE8=function(c){a.placeholder||c.find("input[placeholder]").each(function(){var a=b(this),c=a.attr("placeholder");""===a.val()&&(a.css("color","#666"),a.val(c),a.on("focus.placeholder click.placeholder",function(){a.val(""),a.css("color","#333"),a.off("focus.placeholder click.placeholder")}))})}}),b(function(a){a.langs={},a.langs["zh-cn"]={bold:"粗体",underline:"下划线",italic:"斜体",forecolor:"文字颜色",bgcolor:"背景色",strikethrough:"删除线",eraser:"清空格式",source:"源码",quote:"引用",fontfamily:"字体",fontsize:"字号",head:"标题",orderlist:"有序列表",unorderlist:"无序列表",alignleft:"左对齐",aligncenter:"居中",alignright:"右对齐",link:"链接",text:"文本",submit:"提交",cancel:"取消",unlink:"取消链接",table:"表格",emotion:"表情",img:"图片",video:"视频",width:"宽",height:"高",location:"位置",loading:"加载中",searchlocation:"搜索位置",dynamicMap:"动态地图",clearLocation:"清除位置",langDynamicOneLocation:"动态地图只能显示一个位置",insertcode:"插入代码",undo:"撤销",redo:"重复",fullscreen:"全屏",openLink:"打开链接"},a.langs.en={bold:"Bold",underline:"Underline",italic:"Italic",forecolor:"Color",bgcolor:"Backcolor",strikethrough:"Strikethrough",eraser:"Eraser",source:"Codeview",quote:"Quote",fontfamily:"Font family",fontsize:"Font size",head:"Head",orderlist:"Ordered list",unorderlist:"Unordered list",alignleft:"Align left",aligncenter:"Align center",alignright:"Align right",link:"Insert link",text:"Text",submit:"Submit",cancel:"Cancel",unlink:"Unlink",table:"Table",emotion:"Emotions",img:"Image",video:"Video",width:"width",height:"height",location:"Location",loading:"Loading",searchlocation:"search",dynamicMap:"Dynamic",clearLocation:"Clear",langDynamicOneLocation:"Only one location in dynamic map",insertcode:"Insert Code",undo:"Undo",redo:"Redo",fullscreen:"Full screnn",openLink:"open link"}}),b(function(a){a.config={},a.config.zindex=1e4,a.config.printLog=!0,a.config.menuFixed=0,a.config.jsFilter=!0,a.config.legalTags="p,h1,h2,h3,h4,h5,h6,blockquote,table,ul,ol,pre",a.config.lang=a.langs["zh-cn"],a.config.menus=["source","|","bold","underline","italic","strikethrough","eraser","forecolor","bgcolor","|","quote","fontfamily","fontsize","head","unorderlist","orderlist","alignleft","aligncenter","alignright","|","link","unlink","table","emotion","|","img","video","location","insertcode","|","undo","redo","fullscreen"],a.config.colors={"#880000":"暗红色","#800080":"紫色","#ff0000":"红色","#ff00ff":"鲜粉色","#000080":"深蓝色","#0000ff":"蓝色","#00ffff":"湖蓝色","#008080":"蓝绿色","#008000":"绿色","#808000":"橄榄色","#00ff00":"浅绿色","#ffcc00":"橙黄色","#808080":"灰色","#c0c0c0":"银色","#000000":"黑色","#ffffff":"白色"},a.config.familys=["宋体","黑体","楷体","微软雅黑","Arial","Verdana","Georgia","Times New Roman","Microsoft JhengHei","Trebuchet MS","Courier New","Impact","Comic Sans MS","Consolas"],a.config.fontsizes={1:"12px",2:"13px",3:"16px",4:"18px",5:"24px",6:"32px",7:"48px"},a.config.emotionsShow="icon",a.config.emotions={weibo:{title:"微博表情",data:[{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/7a/shenshou_thumb.gif",value:"[草泥马]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/60/horse2_thumb.gif",value:"[神马]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/bc/fuyun_thumb.gif",value:"[浮云]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/c9/geili_thumb.gif",value:"[给力]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/f2/wg_thumb.gif",value:"[围观]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/70/vw_thumb.gif",value:"[威武]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/6e/panda_thumb.gif",value:"[熊猫]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/81/rabbit_thumb.gif",value:"[兔子]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/bc/otm_thumb.gif",value:"[奥特曼]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/15/j_thumb.gif",value:"[囧]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/89/hufen_thumb.gif",value:"[互粉]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/c4/liwu_thumb.gif",value:"[礼物]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/ac/smilea_thumb.gif",value:"[呵呵]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/0b/tootha_thumb.gif",value:"[哈哈]"}]}},a.config.mapAk="TVhjYjq1ICT2qqL5LdS8mwas",a.config.uploadImgUrl="",a.config.uploadTimeout=2e4,a.config.uploadImgFns={},a.config.customUpload=!1,a.config.uploadParams={},a.config.uploadHeaders={},a.config.hideLinkImg=!1,a.config.pasteFilter=!0,a.config.pasteText=!1,a.config.codeDefaultLang="javascript" +}),b(function(a){a.UI={},a.UI.menus={"default":{normal:'',selected:".selected"},bold:{normal:'',selected:".selected"},underline:{normal:'',selected:".selected"},italic:{normal:'',selected:".selected"},forecolor:{normal:'',selected:".selected"},bgcolor:{normal:'',selected:".selected"},strikethrough:{normal:'',selected:".selected"},eraser:{normal:'',selected:".selected"},quote:{normal:'',selected:".selected"},source:{normal:'',selected:".selected"},fontfamily:{normal:'',selected:".selected"},fontsize:{normal:'',selected:".selected"},head:{normal:'',selected:".selected"},orderlist:{normal:'',selected:".selected"},unorderlist:{normal:'',selected:".selected"},alignleft:{normal:'',selected:".selected"},aligncenter:{normal:'',selected:".selected"},alignright:{normal:'',selected:".selected"},link:{normal:'',selected:".selected"},unlink:{normal:'',selected:".selected"},table:{normal:'',selected:".selected"},emotion:{normal:'',selected:".selected"},img:{normal:'',selected:".selected"},video:{normal:'',selected:".selected"},location:{normal:'',selected:".selected"},insertcode:{normal:'',selected:".selected"},undo:{normal:'',selected:".selected"},redo:{normal:'',selected:".selected"},fullscreen:{normal:'',selected:''}}}),b(function(a,b){a.fn.initDefaultConfig=function(){var c=this;c.config=b.extend({},a.config),c.UI=b.extend({},a.UI)}}),b(function(a,b){a.fn.addEditorContainer=function(){this.$editorContainer=b('

              ')}}),b(function(a){a.fn.addTxt=function(){var b=this,c=new a.Txt(b);b.txt=c}}),b(function(a){a.fn.addMenuContainer=function(){var b=this;b.menuContainer=new a.MenuContainer(b)}}),b(function(a,b){a.createMenuFns=[],a.createMenu=function(b){a.createMenuFns.push(b)},a.fn.addMenus=function(){function e(a){return d.indexOf(a)>=0?!0:!1}var c=this,d=c.config.menus;b.each(a.createMenuFns,function(a,b){b.call(c,e)})}}),b(function(a){a.createMenu(function(b){var d,e,f,c="bold";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.bold,commandName:"Bold"}),f.clickEventSelected=function(a){var b=d.isRangeEmpty();b?d.commandForElem("b,strong,h1,h2,h3,h4,h5",a,"Bold"):d.command(a,"Bold")},d.menus[c]=f)})}),b(function(a){a.createMenu(function(b){var d,e,f,c="underline";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.underline,commandName:"Underline"}),f.clickEventSelected=function(a){var b=d.isRangeEmpty();b?d.commandForElem("u,a",a,"Underline"):d.command(a,"Underline")},d.menus[c]=f)})}),b(function(a){a.createMenu(function(b){var d,e,f,c="italic";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.italic,commandName:"Italic"}),f.clickEventSelected=function(a){var b=d.isRangeEmpty();b?d.commandForElem("i",a,"Italic"):d.command(a,"Italic")},d.menus[c]=f)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,h,i,d="forecolor";c(d)&&(e=this,f=e.config.lang,g=e.config.colors,h=new a.Menu({editor:e,id:d,title:f.forecolor}),i=b("
              "),b.each(g,function(a,b){i.append([''].join(""))}),i.on("click","a[commandValue]",function(a){var c=b(this),d=c.attr("commandValue");h.selected&&e.isRangeEmpty()?e.commandForElem("font[color]",a,"forecolor",d):e.command(a,"forecolor",d)}),h.dropPanel=new a.DropPanel(e,h,{$content:i,width:125}),h.updateSelectedEvent=function(){var a=e.getRangeElem();return a=e.getSelfOrParentByName(a,"font[color]"),a?!0:!1},e.menus[d]=h)})}),b(function(a,b){a.createMenu(function(c){function h(a){var b;return a&&a.style&&null!=a.style.cssText&&(b=a.style.cssText,b&&b.indexOf("background-color:")>=0)?!0:!1}var e,f,g,i,j,d="bgcolor";c(d)&&(e=this,f=e.config.lang,g=e.config.colors,i=new a.Menu({editor:e,id:d,title:f.bgcolor}),j=b("
              "),b.each(g,function(a,b){j.append([''].join(""))}),j.on("click","a[commandValue]",function(a){var c=b(this),d=c.attr("commandValue");i.selected&&e.isRangeEmpty()?e.commandForElem({selector:"span,font",check:h},a,"BackColor",d):e.command(a,"BackColor",d)}),i.dropPanel=new a.DropPanel(e,i,{$content:j,width:125}),i.updateSelectedEvent=function(){var a=e.getRangeElem();return a=e.getSelfOrParentByName(a,"span,font",h),a?!0:!1},e.menus[d]=i)})}),b(function(a){a.createMenu(function(b){var d,e,f,c="strikethrough";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.strikethrough,commandName:"StrikeThrough"}),f.clickEventSelected=function(a){var b=d.isRangeEmpty();b?d.commandForElem("strike",a,"StrikeThrough"):d.command(a,"StrikeThrough")},d.menus[c]=f)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,d="eraser";c(d)&&(e=this,f=e.config.lang,g=new a.Menu({editor:e,id:d,title:f.eraser,commandName:"RemoveFormat"}),g.clickEvent=function(a){function f(){var e,f,h,i,j,a=this,c=a.getRangeElem(),g=a.getSelfOrParentByName(c,"blockquote");g&&(h=b(g),d=b("

              "+h.text()+"

              "),h.after(d).remove()),e=a.getSelfOrParentByName(c,"p,h1,h2,h3,h4,h5"),e&&(f=b(e),d=b("

              "+f.text()+"

              "),f.after(d).remove()),i=a.getSelfOrParentByName(c,"ul,ol"),i&&(j=b(i),d=b("

              "+j.text()+"

              "),j.after(d).remove())}function g(){var a=this;d&&a.restoreSelectionByElem(d.get(0))}var d,c=e.isRangeEmpty();return c?(e.customCommand(a,f,g),void 0):(e.command(a,"RemoveFormat"),void 0)},e.menus[d]=g)})}),b(function(a,b){a.createMenu(function(c){function i(){var a=h.$codeTextarea,c=e.txt.$txt,d=b.trim(a.val());d||(d="


              "),e.config.jsFilter&&(d=d.replace(//gi,"")),c.html(d)}var e,f,g,h,d="source";c(d)&&(e=this,f=e.config.lang,h=new a.Menu({editor:e,id:d,title:f.source}),h.isShowCode=!1,h.clickEvent=function(){var j,c=this,d=c.editor,e=d.txt.$txt,f=e.outerHeight(),i=e.height();c.$codeTextarea||(c.$codeTextarea=b('')),j=c.$codeTextarea,j.css({height:i,"margin-top":f-i}),j.val(e.html()),e.after(j).hide(),j.show(),h.isShowCode=!0,this.updateSelected(),d.disableMenusExcept("source"),g=e.html()},h.clickEventSelected=function(){var b=this,c=b.editor,d=c.txt.$txt,e=b.$codeTextarea;e&&(i(),e.after(d).hide(),d.show(),h.isShowCode=!1,this.updateSelected(),c.enableMenusExcept("source"),d.html()!==g&&c.onchange&&"function"==typeof c.onchange&&c.onchange.call(c))},h.updateSelectedEvent=function(){return this.isShowCode},e.menus[d]=h)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,d="quote";c(d)&&(e=this,f=e.config.lang,g=new a.Menu({editor:e,id:d,title:f.quote,commandName:"formatBlock",commandValue:"blockquote"}),g.clickEvent=function(a){function h(){g=b("

              "+d.text()+"

              "),d.after(g).remove(),g.wrap("
              ")}function i(){var a=this;g&&a.restoreSelectionByElem(g.get(0))}var d,f,g,c=e.getRangeElem();return c?(f=e.getSelfOrParentByName(c,"blockquote"))?(a.preventDefault(),void 0):(c=e.getLegalTags(c),d=b(c),d.text()?c?(e.customCommand(a,h,i),void 0):(e.command(a,"formatBlock","blockquote"),void 0):void 0):(a.preventDefault(),void 0)},g.clickEventSelected=function(a){function g(){var a=b(d),c=a.children();return c.length?(c.each(function(){var d=b(this);"P"===d.get(0).nodeName?a.after(d):a.after("

              "+d.text()+"

              "),f=d}),a.remove(),void 0):void 0}function h(){var a=this;f&&a.restoreSelectionByElem(f.get(0))}var f,c=e.getRangeElem(),d=e.getSelfOrParentByName(c,"blockquote");return d?(e.customCommand(a,g,h),void 0):(a.preventDefault(),void 0)},g.updateSelectedEvent=function(){var a=this,b=a.editor,c=b.getRangeElem();return c=b.getSelfOrParentByName(c,"blockquote"),c?!0:!1},e.menus[d]=g,e.ready(function(){var a=this,c=a.txt.$txt,d=!1;c.on("keydown",function(c){var e,f,g;return 13!==c.keyCode?(d=!1,void 0):(e=a.getRangeElem(),(e=a.getSelfOrParentByName(e,"blockquote"))?d?(f=a.getRangeElem(),g=b(f),g.length&&g.parent().after(g),a.restoreSelectionByElem(f,"start"),d=!1,c.preventDefault(),void 0):(d=!0,void 0):(d=!1,void 0))})}),e.ready(function(){function e(){d&&d.remove()}function f(){if(d){var b=d.prev();b.length?a.restoreSelectionByElem(b.get(0)):a.initSelection()}}var d,a=this,c=a.txt.$txt;c.on("keydown",function(c){var g,h;8===c.keyCode&&(g=a.getRangeElem(),g=a.getSelfOrParentByName(g,"blockquote"),g&&(d=b(g),h=d.text(),h||a.customCommand(c,e,f)))})}))})}),b(function(a,b){a.createMenu(function(c){var e,f,g,h,i,j,d="fontfamily";c(d)&&(e=this,f=e.config.lang,g=e.config.familys,h=new a.Menu({editor:e,id:d,title:f.fontfamily,commandName:"fontName"}),i={},b.each(g,function(a,b){i[b]=b}),j='{#title}',h.dropList=new a.DropList(e,h,{data:i,tpl:j,selectorForELemCommand:"font[face]"}),h.updateSelectedEvent=function(){var a=e.getRangeElem();return a=e.getSelfOrParentByName(a,"font[face]"),a?!0:!1},e.menus[d]=h)})}),b(function(a){a.createMenu(function(b){var d,e,f,g,h,i,c="fontsize";b(c)&&(d=this,e=d.config.lang,f=d.config.fontsizes,g=new a.Menu({editor:d,id:c,title:e.fontsize,commandName:"fontSize"}),h=f,i='{#title}',g.dropList=new a.DropList(d,g,{data:h,tpl:i,selectorForELemCommand:"font[size]"}),g.updateSelectedEvent=function(){var a=d.getRangeElem();return a=d.getSelfOrParentByName(a,"font[size]"),a?!0:!1},d.menus[c]=g)})}),b(function(a){a.createMenu(function(b){function i(a){d.queryCommandState("InsertOrderedList")?(h=!0,d.command(a,"InsertOrderedList")):h=!1}function j(a){h&&d.command(a,"InsertOrderedList")}var d,e,f,g,h,k,c="head";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.head,commandName:"formatBlock"}),g={"

              ":"标题1","

              ":"标题2","

              ":"标题3","

              ":"标题4","

              ":"标题5"},k="{#commandValue}{#title}",f.dropList=new a.DropList(d,f,{data:g,tpl:k,beforeEvent:i,afterEvent:j}),f.updateSelectedEvent=function(){var a=d.getRangeElem();return a=d.getSelfOrParentByName(a,"h1,h2,h3,h4,h5"),a?!0:!1},d.menus[c]=f)})}),b(function(a){a.createMenu(function(b){var d,e,f,c="unorderlist";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.unorderlist,commandName:"InsertUnorderedList"}),d.menus[c]=f)})}),b(function(a){a.createMenu(function(b){var d,e,f,c="orderlist";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.orderlist,commandName:"InsertOrderedList"}),d.menus[c]=f)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,d="alignleft";c(d)&&(e=this,f=e.config.lang,g=new a.Menu({editor:e,id:d,title:f.alignleft,commandName:"JustifyLeft"}),g.updateSelectedEvent=function(){var a=e.getRangeElem();return a=e.getSelfOrParentByName(a,"p,h1,h2,h3,h4,h5,li",function(a){var c;return a&&a.style&&null!=a.style.cssText&&(c=a.style.cssText,c&&/text-align:\s*left;/.test(c))?!0:"left"===b(a).attr("align")?!0:!1}),a?!0:!1},e.menus[d]=g)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,d="aligncenter";c(d)&&(e=this,f=e.config.lang,g=new a.Menu({editor:e,id:d,title:f.aligncenter,commandName:"JustifyCenter"}),g.updateSelectedEvent=function(){var a=e.getRangeElem();return a=e.getSelfOrParentByName(a,"p,h1,h2,h3,h4,h5,li",function(a){var c;return a&&a.style&&null!=a.style.cssText&&(c=a.style.cssText,c&&/text-align:\s*center;/.test(c))?!0:"center"===b(a).attr("align")?!0:!1}),a?!0:!1},e.menus[d]=g)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,d="alignright";c(d)&&(e=this,f=e.config.lang,g=new a.Menu({editor:e,id:d,title:f.alignright,commandName:"JustifyRight"}),g.updateSelectedEvent=function(){var a=e.getRangeElem();return a=e.getSelfOrParentByName(a,"p,h1,h2,h3,h4,h5,li",function(a){var c;return a&&a.style&&null!=a.style.cssText&&(c=a.style.cssText,c&&/text-align:\s*right;/.test(c))?!0:"right"===b(a).attr("align")?!0:!1}),a?!0:!1},e.menus[d]=g)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,h,i,j,k,l,m,n,o,d="link";c(d)&&(e=this,f=e.config.lang,g=new a.Menu({editor:e,id:d,title:f.link}),h=b("
              "),i=b('
              '),j=i.clone(),k=i.clone().css("margin","0 10px"),l=b(''),m=b(''),n=b('"),o=b('"),i.append(l),j.append(m),k.append(n).append(o),h.append(i).append(j).append(k),g.dropPanel=new a.DropPanel(e,g,{$content:h,width:300}),g.clickEvent=function(){var d,f,g,h,b=this,c=b.dropPanel;return c.isShowing?(c.hide(),void 0):(l.val(""),m.val("http://"),d="",f=e.getRangeElem(),f=e.getSelfOrParentByName(f,"a"),f&&(d=f.href||""),g="",h=e.isRangeEmpty(),h?f&&(g=f.textContent||f.innerHTML):g=e.getRangeText()||"",d&&m.val(d),g&&l.val(g),h?l.removeAttr("disabled"):l.attr("disabled",!0),c.show(),void 0)},g.updateSelectedEvent=function(){var a=e.getRangeElem();return a=e.getSelfOrParentByName(a,"a"),a?!0:!1},o.click(function(a){a.preventDefault(),g.dropPanel.hide()}),n.click(function(c){var d,f,h,i,j,k,n,o,p,q,r,s,t;return c.preventDefault(),d=e.getRangeElem(),f=e.getSelfOrParentByName(d,"a"),h=e.isRangeEmpty(),o=e.txt.$txt,r="link"+a.random(),s=b.trim(m.val()),t=b.trim(l.val()),s?(t||(t=s),h?f?(i=b(f),k=function(){i.attr("href",s),i.text(t)},n=function(){var a=this;a.restoreSelectionByElem(f)},e.customCommand(c,k,n)):(j=''+t+"",a.userAgent.indexOf("Firefox")>0&&(j+=" "),e.command(c,"insertHtml",j)):(p=o.find("a"),p.attr(r,"1"),e.command(c,"createLink",s),q=o.find("a").not("["+r+"]"),q.attr("target","_blank"),p.removeAttr(r)),void 0):(g.dropPanel.focusFirstInput(),void 0)}),e.menus[d]=g)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,d="unlink";c(d)&&(e=this,f=e.config.lang,g=new a.Menu({editor:e,id:d,title:f.unlink,commandName:"unLink"}),g.clickEvent=function(a){function i(){g.after(h).remove()}function j(){e.restoreSelectionByElem(h.get(0))}var d,f,g,h,c=e.isRangeEmpty();return c?(d=e.getRangeElem(),(f=e.getSelfOrParentByName(d,"a"))?(g=b(f),h=b(""+g.text()+""),e.customCommand(a,i,j),void 0):(a.preventDefault(),void 0)):(e.command(a,"unLink"),void 0)},e.menus[d]=g)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,h,i,j,k,l,m,n,o,p,d="table";if(c(d)){for(e=this,f=e.config.lang,g=new a.Menu({editor:e,id:d,title:f.table}),h=b('
              '),i=b('
              '),j=b("0"),k=b(""),l=b("0"),m=b(""),o=0;15>o;o++){for(n=b(''),p=0;20>p;p++)n.append(b('\n\n\n"; + } + + /** + * Create a HTML h1 tag + * + * @param string $title Text to be in the h1 + * @param int $level Error level + * @return string + */ + protected function addTitle(string $title, int $level): string + { + $title = htmlspecialchars($title, ENT_NOQUOTES, 'UTF-8'); + + return '

              '.$title.'

              '; + } + + /** + * Formats a log record. + * + * @return string The formatted record + */ + public function format(array $record): string + { + $output = $this->addTitle($record['level_name'], $record['level']); + $output .= '
              '));i.append(n)}h.append(i),h.append(j).append(k).append(l).append(m),i.on("mouseenter","td",function(a){var c=b(a.currentTarget),d=c.attr("index"),e=c.parent(),f=e.attr("index");j.text(f),l.text(d),i.find("tr").each(function(){var a=b(this),c=a.attr("index");parseInt(c,10)<=parseInt(f,10)?a.find("td").each(function(){var a=b(this),c=a.attr("index");parseInt(c,10)<=parseInt(d,10)?a.addClass("active"):a.removeClass("active")}):a.find("td").removeClass("active")})}).on("mouseleave",function(){i.find("td").removeClass("active"),j.text(0),l.text(0)}),i.on("click","td",function(a){var j,k,c=b(a.currentTarget),d=c.attr("index"),f=c.parent(),g=f.attr("index"),h=parseInt(g,10),i=parseInt(d,10),l="";for(j=0;h>j;j++){for(l+="",k=0;i>k;k++)l+="";l+=""}l+="
               
              ",e.command(a,"insertHtml",l)}),g.dropPanel=new a.DropPanel(e,g,{$content:h,width:262}),e.menus[d]=g}})}),b(function(a,b){a.createMenu(function(c){function k(a,c){b.each(a,function(a,d){var f=d.icon||d.url,g=d.value||d.title,h="icon"===i?f:g,j=b(''),k=b("");k.attr("_src",f),j.append(k),c.append(j),e.emotionUrls.push(f)})}var e,f,g,h,i,j,l,m,n,d="emotion";c(d)&&(e=this,f=e.config,g=f.lang,h=f.emotions,i=f.emotionsShow,e.emotionUrls=[],j=new a.Menu({editor:e,id:d,title:g.emotion}),l=b('
              '),m=b('
              '),n=b('
              '),b.each(h,function(c,d){var g,h,e=d.title,f=d.data;if(a.log("正在处理 "+e+" 表情的数据..."),g=b(''+e+" "),m.append(g),h=b('
              '),n.append(h),g.click(function(a){m.children().removeClass("selected"),n.children().removeClass("selected"),h.addClass("selected"),g.addClass("selected"),a.preventDefault()}),"string"==typeof f)a.log("将通过 "+f+" 地址ajax下载表情包"),b.get(f,function(c){c=b.parseJSON(c),a.log("下载完毕,得到 "+c.length+" 个表情"),k(c,h)});else{if(!(Object.prototype.toString.call(f).toLowerCase().indexOf("array")>0))return a.error("data 数据格式错误,请修改为正确格式,参考文档:"+a.docsite),void 0;k(f,h)}}),l.append(m).append(n),m.children().first().addClass("selected"),n.children().first().addClass("selected"),n.on("click","a[commandValue]",function(a){var c=b(a.currentTarget),d=c.attr("commandValue");"icon"===i?e.command(a,"InsertImage",d):e.command(a,"insertHtml",""+d+""),a.preventDefault()}),j.dropPanel=new a.DropPanel(e,j,{$content:l,width:350}),j.clickEvent=function(){var d=this,e=d.dropPanel;return e.isShowing?(e.hide(),void 0):(e.show(),d.imgLoaded||(n.find("img").each(function(){var c=b(this),d=c.attr("_src");c.on("error",function(){a.error("加载不出表情图片 "+d)}),c.attr("src",d),c.removeAttr("_src")}),d.imgLoaded=!0),void 0)},e.menus[d]=j)})}),b(function(a,b){function c(a,c,d){function j(){g.val("")}var h,i,e=a.config.lang,f=b('
              '),g=b('');f.append(g),h=b('"),i=b('"),d.append(f).append(h).append(i),i.click(function(a){a.preventDefault(),c.dropPanel.hide()}),h.click(function(c){var d,e;return c.preventDefault(),(d=b.trim(g.val()))?(e='',a.command(c,"insertHtml",e,j),void 0):(g.focus(),void 0)})}a.createMenu(function(d){function p(){l.click(function(a){j.children().removeClass("selected"),k.children().removeClass("selected"),n.addClass("selected"),l.addClass("selected"),a.preventDefault()}),m.click(function(b){j.children().removeClass("selected"),k.children().removeClass("selected"),o.addClass("selected"),m.addClass("selected"),b.preventDefault(),a.placeholder&&o.find("input[type=text]").focus()}),l.click()}function q(){j.remove(),n.remove(),o.addClass("selected")}function r(){j.remove(),o.remove(),n.addClass("selected")}var f,g,h,i,j,k,l,m,n,o,e="img";d(e)&&(f=this,g=f.config.lang,h=new a.Menu({editor:f,id:e,title:g.img}),i=b('
              '),j=b('
              '),k=b('
              '),i.append(j).append(k),l=b('上传图片'),m=b('网络图片'),j.append(l).append(m),n=b('
              '),k.append(n),o=b('
              '),k.append(o),c(f,h,o),h.dropPanel=new a.DropPanel(f,h,{$content:i,width:400,onRender:function(){var a=f.config.customUploadInit;a&&a.call(f)}}),f.menus[e]=h,f.ready(function(){function g(){h.dropPanel.hide()}var a=this,b=a.config,c=b.uploadImgUrl,d=b.customUpload,e=b.hideLinkImg;c||d?(a.$uploadContent=n,p(),e&&r()):q(),n.click(function(){setTimeout(g)})}))})}),b(function(a,b){a.createMenu(function(c){var e,f,g,h,i,j,k,l,m,n,o,p,q,r,d="video";c(d)&&(e=this,f=e.config.lang,g=/^<(iframe)|(embed)/i,h=new a.Menu({editor:e,id:d,title:f.video}),i=b("
              "),j=b('
              '),k=b('\'/>'),j.append(k),l=b('
              '),m=b(''),n=b(''),l.append(" "+f.width+" ").append(m).append(" px    ").append(" "+f.height+" ").append(n).append(" px "),o=b("
              "),p=b('如何复制视频链接?'),q=b('"),r=b('"),o.append(p).append(q).append(r),i.append(j).append(l).append(o),r.click(function(a){a.preventDefault(),k.val(""),h.dropPanel.hide()}),q.click(function(a){var c,d,f,i,j,l;return a.preventDefault(),c=b.trim(k.val()),f=parseInt(m.val()),i=parseInt(n.val()),j=b("
              "),l="

              {content}

              ",c?g.test(c)?isNaN(f)||isNaN(i)?(alert("宽度或高度不是数字!"),void 0):(d=b(c),d.attr("width",f).attr("height",i),l=l.replace("{content}",j.append(d).html()),e.command(a,"insertHtml",l),k.val(""),void 0):(alert("视频链接格式错误!"),h.dropPanel.focusFirstInput(),void 0):(h.dropPanel.focusFirstInput(),void 0)}),h.dropPanel=new a.DropPanel(e,h,{$content:i,width:400}),e.menus[d]=h)})}),b(function(a,b){var c=function(a){return"onkeyup"in a}(document.createElement("input"));a.baiduMapAk="TVhjYjq1ICT2qqL5LdS8mwas",a.numberOfLocation=0,a.createMenu(function(d){function y(){p.val("")}var f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,e="location";if(d(e)){if(++a.numberOfLocation>1)return a.error("目前不支持在一个页面多个编辑器上同时使用地图,可通过自定义菜单配置去掉地图菜单"),void 0;f=this,g=f.config,h=g.lang,i=g.mapAk,f.mapData={},j=f.mapData,j.markers=[],j.mapContainerId="map"+a.random(),j.clearLocations=function(){var a=j.map;a&&(a.clearOverlays(),j.markers=[])},j.searchMap=function(){var b,c,d,e,f,a=j.map;a&&(b=window.BMap,c=o.val(),d=p.val(),""!==c&&(d&&""!==d||a.centerAndZoom(c,11),d&&""!==d&&(e=new b.Geocoder,e.getPoint(d,function(d){d?(a.centerAndZoom(d,13),f=new b.Marker(d),a.addOverlay(f),f.enableDragging(),j.markers.push(f)):a.centerAndZoom(c,11)},c))))},k=!1,window.baiduMapCallBack=function(){function e(b){var f,g,e=b.name;d.setCenter(e),o.val(e),a.placeholder&&p.focus(),c?(g=function(a){"keyup"===a.type&&13===a.keyCode&&a.preventDefault(),f&&clearTimeout(f),f=setTimeout(j.searchMap,500)},o.on("keyup change paste",g),p.on("keyup change paste",g)):(g=function(){var a,b,c,d;return m.is(":visible")?(a="",b="",c=o.val(),d=p.val(),(c!==a||d!==b)&&(j.searchMap(),a=c,b=d),f&&clearTimeout(f),f=setTimeout(g,1e3),void 0):(clearTimeout(f),void 0)},f=setTimeout(g,1e3))}var b,d,f;k||(k=!0,b=window.BMap,j.map||(j.map=new b.Map(j.mapContainerId)),d=j.map,d.centerAndZoom(new b.Point(116.404,39.915),11),d.addControl(new b.MapTypeControl),d.setCurrentCity("北京"),d.enableScrollWheelZoom(!0),f=new b.LocalCity,f.get(e),d.addEventListener("click",function(a){var c=new b.Marker(new b.Point(a.point.lng,a.point.lat));d.addOverlay(c),c.enableDragging(),j.markers.push(c)},!1))},j.loadMapScript=function(){var b=document.createElement("script");b.type="text/javascript",b.src="https://api.map.baidu.com/api?v=2.0&ak="+i+"&s=1&callback=baiduMapCallBack";try{document.body.appendChild(b)}catch(c){a.error("加载地图过程中发生错误")}},j.initMap=function(){window.BMap?window.baiduMapCallBack():j.loadMapScript()},l=new a.Menu({editor:f,id:e,title:h.location}),f.menus[e]=l,m=b("
              "),n=b('
              '),o=b(''),o.css({width:"80px","text-align":"center"}),p=b(''),p.css({width:"300px","margin-left":"10px"}).attr("placeholder",h.searchlocation),q=b('"),n.append(q).append(o).append(p),m.append(n),q.click(function(a){p.val(""),p.focus(),j.clearLocations(),a.preventDefault()}),r=b('
              '),r.css({height:"260px",width:"100%",position:"relative","margin-top":"10px",border:"1px solid #f1f1f1"}),s=b(""+h.loading+""),s.css({position:"absolute",width:"100px","text-align":"center",top:"45%",left:"50%","margin-left":"-50px"}),r.append(s),m.append(r),t=b('
              '),u=b('"),v=b('"),w=b(''),x=b(''),w.append(x).append(' '+h.dynamicMap+""),t.append(w).append(u).append(v),m.append(t),v.click(function(a){a.preventDefault(),y(),l.dropPanel.hide()}),u.click(function(a){a.preventDefault();var p,q,r,c=j.map,d=x.is(":checked"),e=j.markers,g=c.getCenter(),i=g.lng,k=g.lat,l=c.getZoom(),m=c.getSize(),n=m.width,o=m.height;if(q=d?"http://ueditor.baidu.com/ueditor/dialogs/map/show.html#":"http://api.map.baidu.com/staticimage?",q=q+"center="+i+","+k+"&zoom="+l+"&width="+n+"&height="+o,e.length>0&&(q+="&markers=",b.each(e,function(a,b){p=b.getPosition(),a>0&&(q+="|"),q=q+p.lng+","+p.lat})),d){if(e.length>1)return alert(h.langDynamicOneLocation),void 0;q+="&markerStyles=l,A",r='',r=r.replace("{src}",q),f.command(a,"insertHtml",r,y)}else f.command(a,"insertHtml",'',y)}),l.dropPanel=new a.DropPanel(f,l,{$content:m,width:500}),l.onRender=function(){i===a.baiduMapAk&&a.warn("建议在配置中自定义百度地图的mapAk,否则可能影响地图功能,文档:"+a.docsite)},l.clickEvent=function(){var b=this,c=b.dropPanel,d=!1;return c.isShowing?(c.hide(),void 0):(j.map||(d=!0),c.show(),j.initMap(),d||p.focus(),void 0)}}})}),b(function(a,b){function c(){if(!(a.userAgent.indexOf("MSIE 8")>0||window.hljs)){var b=document.createElement("script");b.type="text/javascript",b.src="//cdn.bootcss.com/highlight.js/9.2.0/highlight.min.js",document.body.appendChild(b)}}a.createMenu(function(d){function n(a){var d,e,g,k,c=b("
              ");c.css({margin:"15px 5px 5px 5px",height:"160px","text-align":"center"}),l.css({width:"100%",height:"100%",padding:"10px"}),l.on("keydown",function(a){9===a.keyCode&&a.preventDefault()}),c.append(l),a.append(c),d=b("
              "),e=b('"),g=b('"),d.append(e).append(g).append(m),a.append(d),g.click(function(a){a.preventDefault(),j.dropPanel.hide()}),k='
              {#content}
              ',e.click(function(a){function q(){var a;e&&(a=p.attr("class"),a!==e+" hljs"&&p.attr("class",e+" hljs")),p.html(c)}function r(){f.restoreSelectionByElem(o),h()}var c,d,e,g,h,n,o,p;return a.preventDefault(),(c=l.val())?(d=f.getRangeElem(),b.trim(b(d).text())&&0!==k.indexOf("


              ")&&(k="


              "+k),e=m?m.val():"",g="",h=function(){i.find("pre code").each(function(a,c){var d=b(c);d.attr("codemark")||window.hljs&&(window.hljs.highlightBlock(c),d.attr("codemark","1"))})},e&&(g=' class="'+e+' hljs"'),c=c.replace(/&/gm,"&").replace(//gm,">"),j.selected?(o=f.getSelfOrParentByName(d,"pre"),o&&(o=f.getSelfOrParentByName(d,"code")),o&&(p=b(o),f.customCommand(a,q,r)),void 0):(n=k.replace("{#langClass}",g).replace("{#content}",c),f.command(a,"insertHtml",n,h),void 0)):(l.focus(),void 0)})}function o(){var a=f.getRangeElem(),b=f.getSelfOrParentByName(a,"code");b?f.disableMenusExcept("insertcode"):f.enableMenusExcept("insertcode")}var f,g,h,i,j,k,l,m,e="insertcode";d(e)&&(setTimeout(c,0),f=this,g=f.config,h=g.lang,i=f.txt.$txt,j=new a.Menu({editor:f,id:e,title:h.insertcode}),j.clickEvent=function(){var e,c=this,d=c.dropPanel;if(d.isShowing)return d.hide(),void 0;if(l.val(""),d.show(),e=window.hljs,e&&e.listLanguages){if(0!==m.children().length)return;m.css({"margin-top":"9px","margin-left":"5px"}),b.each(e.listLanguages(),function(a,b){"xml"===b&&(b="html"),b===g.codeDefaultLang?m.append('"):m.append('")})}else m.hide()},j.clickEventSelected=function(){var e,g,h,i,c=this,d=c.dropPanel;return d.isShowing?(d.hide(),void 0):(d.show(),e=f.getRangeElem(),g=f.getSelfOrParentByName(e,"pre"),g&&(g=f.getSelfOrParentByName(e,"code")),g&&(h=b(g),l.val(h.text()),m&&(i=h.attr("class"),i&&m.val(i.split(" ")[0]))),void 0)},j.updateSelectedEvent=function(){var a=this,b=a.editor,c=b.getRangeElem();return c=b.getSelfOrParentByName(c,"pre"),c?!0:!1},k=b("
              "),l=b(""),m=b(""),n(k),j.dropPanel=new a.DropPanel(f,j,{$content:k,width:500}),f.menus[e]=j,i.on("keydown",function(a){var b,c;13===a.keyCode&&(b=f.getRangeElem(),c=f.getSelfOrParentByName(b,"code"),c&&f.command(a,"insertHtml","\n"))}),i.on("keydown click",function(){setTimeout(o)}))})}),b(function(a){a.createMenu(function(b){var d,e,f,c="undo";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.undo}),f.clickEvent=function(){d.undo()},d.menus[c]=f,d.ready(function(){function d(){a.undoRecord()}var c,a=this,b=a.txt.$txt;b.on("keydown",function(b){var e=b.keyCode;return b.ctrlKey&&90===e?(a.undo(),void 0):(13===e?d():(c&&clearTimeout(c),c=setTimeout(d,1e3)),void 0)}),a.undoRecord()}))})}),b(function(a){a.createMenu(function(b){var d,e,f,c="redo";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.redo}),f.clickEvent=function(){d.redo()},d.menus[c]=f)})}),b(function(a){var c;a.createMenu(function(b){var e,f,g,h,i,j,k,l,m,d="fullscreen";b(d)&&(e=this,f=e.txt.$txt,g=e.config,h=g.zindex||1e4,i=g.lang,j=!1,m=new a.Menu({editor:e,id:d,title:i.fullscreen}),m.clickEvent=function(){var g,i,m,n,d=e.$editorContainer;d.addClass("wangEditor-fullscreen"),k=d.css("z-index"),d.css("z-index",h),i=f.height(),m=f.outerHeight(),e.useMaxHeight&&(l=f.css("max-height"),f.css("max-height","none"),g=f.parent(),g.after(f),g.remove(),f.css("overflow-y","auto")),n=e.menuContainer,f.height(a.$window.height()-n.height()-(m-i)),e.menuContainer.$menuContainer.attr("style",""),j=!0,e.isFullScreen=!0,c=a.$window.scrollTop() +},m.clickEventSelected=function(){var d=e.$editorContainer;d.removeClass("wangEditor-fullscreen"),d.css("z-index",k),e.useMaxHeight?f.css("max-height",l):e.$valueContainer.css("height",e.valueContainerHeight),e.txt.initHeight(),j=!1,e.isFullScreen=!1,null!=c&&a.$window.scrollTop(c)},m.updateSelectedEvent=function(){return j},e.menus[d]=m)})}),b(function(a,b){a.fn.renderMenus=function(){var f,g,a=this,c=a.menus,d=a.config.menus;a.menuContainer,g=0,b.each(d,function(a,b){return"|"===b?(g++,void 0):(f=c[b],f&&f.render(g),void 0)})}}),b(function(a){a.fn.renderMenuContainer=function(){var a=this,b=a.menuContainer;a.$editorContainer,b.render()}}),b(function(a){a.fn.renderTxt=function(){var a=this,b=a.txt;b.render(),a.ready(function(){b.initHeight()})}}),b(function(a){a.fn.renderEditorContainer=function(){var e,f,a=this,b=a.$valueContainer,c=a.$editorContainer,d=a.txt.$txt;b===d?(e=a.$prev,f=a.$parent,e&&e.length?e.after(c):f.prepend(c)):(b.after(c),b.hide())}}),b(function(a,b){a.fn.eventMenus=function(){var a=this.menus;b.each(a,function(a,b){b.bindEvent()})}}),b(function(a){a.fn.eventMenuContainer=function(){}}),b(function(a){a.fn.eventTxt=function(){var a=this.txt;a.saveSelectionEvent(),a.updateValueEvent(),a.updateMenuStyleEvent()}}),b(function(a){a.plugin(function(){var b=this,c=b.config.uploadImgFns;c.onload||(c.onload=function(b){var d,e,f;a.log("上传结束,返回结果为 "+b),d=this,e=d.uploadImgOriginalName||"",0===b.indexOf("error|")?(a.warn("上传失败:"+b.split("|")[1]),alert(b.split("|")[1])):(a.log("上传成功,即将插入编辑区域,结果为:"+b),f=document.createElement("img"),f.onload=function(){var c=''+e+'';d.command(null,"insertHtml",c),a.log("已插入图片,地址 "+b),f=null},f.onerror=function(){a.error("使用返回的结果获取图片,发生错误。请确认以下结果是否正确:"+b),f=null},f.src=b)}),c.ontimeout||(c.ontimeout=function(){a.error("上传图片超时"),alert("上传图片超时")}),c.onerror||(c.onerror=function(){a.error("上传上图片发生错误"),alert("上传上图片发生错误")})})}),b(function(a,b){window.FileReader&&window.FormData&&a.plugin(function(){function k(a,b){var f,c=window.atob(a.split(",")[1]),d=new ArrayBuffer(c.length),e=new Uint8Array(d);for(f=0;f';c.command(d,"insertHtml",f),a.log("已插入图片,地址 "+b),e=null},e.onerror=function(){a.error("使用返回的结果获取图片,发生错误。请确认以下结果是否正确:"+b),e=null},e.src=b}function m(a){if(a.lengthComputable){var b=a.loaded/a.total;c.showUploadProgress(100*b)}}var c=this,d=c.config,e=d.uploadImgUrl,f=d.uploadTimeout,g=d.uploadImgFns,h=g.onload,i=g.ontimeout,j=g.onerror;e&&(c.xhrUploadImg=function(d){function E(){B&&clearTimeout(B),A&&A.abort&&A.abort(),g.preventDefault(),t&&t.call(c,A),c.hideUploadProgress()}var x,y,z,A,B,D,g=d.event,n=d.filename||"",o=d.base64,p=d.fileType||"image/png",q=d.name||"wangEditor_upload_file",r=d.loadfn||h,s=d.errorfn||j,t=d.timeoutfn||i,u=c.config.uploadParams||{},v=c.config.uploadHeaders||{},w="png";if(n.indexOf(".")>0?w=n.slice(n.lastIndexOf(".")-n.length+1):p.indexOf("/")>0&&p.split("/")[1]&&(w=p.split("/")[1]),c.config.imgExt){x=c.config.imgExt.split(","),y=!1;for(z in x)if(x[z]==w){y=!0;break}if(y===!1)return alert("只允许上传后缀名为:"+c.config.imgExt+"的图片"),!1}return a.isOnWebsite?(a.log("预览模拟上传"),l(o,g),void 0):(A=new XMLHttpRequest,D=new FormData,A.onload=function(){B&&clearTimeout(B),c.uploadImgOriginalName=n,n.indexOf(".")>0&&(c.uploadImgOriginalName=n.split(".")[0]),r&&r.call(c,A.responseText,A),c.hideUploadProgress()},A.onerror=function(){B&&clearTimeout(B),g.preventDefault(),s&&s.call(c,A),c.hideUploadProgress()},A.upload.onprogress=m,n=n||a.random()+"."+w,D.append(q,k(o,p),n),b.each(u,function(a,b){D.append(a,b)}),A.open("POST",e,!0),b.each(v,function(a,b){A.setRequestHeader(a,b)}),A.withCredentials=!0,A.send(D),B=setTimeout(E,f),a.log("开始上传...并开始超时计算"),void 0)})})}),b(function(a,b){a.plugin(function(){function i(){h||(h=!0,g.css({top:d+"px"}),e.append(g))}function k(){g.hide(),j=null}var j,a=this,c=a.menuContainer,d=c.height(),e=a.$editorContainer,f=e.width(),g=b('
              '),h=!1;a.showUploadProgress=function(a){j&&clearTimeout(j),i(),g.show(),g.width(a*f/100)},a.hideUploadProgress=function(a){j&&clearTimeout(j),a=a||750,j=setTimeout(k,a)}})}),b(function(a,b){a.plugin(function(){var g,h,i,j,c=this,d=c.config,e=d.uploadImgUrl,f=d.uploadTimeout;e&&(h=c.$uploadContent,h&&(i=b('
              '),h.append(i),j=new a.UploadFile({editor:c,uploadUrl:e,timeout:f,fileAccept:"image/*"}),i.click(function(a){g=a,j.selectFiles()})))})}),b(function(a,b){if(window.FileReader&&window.FormData){var c=function(a){this.editor=a.editor,this.uploadUrl=a.uploadUrl,this.timeout=a.timeout,this.fileAccept=a.fileAccept,this.multiple=!0};c.fn=c.prototype,c.fn.clear=function(){this.$input.val(""),a.log("input value 已清空")},c.fn.render=function(){var d,e,f,g,h,i,c=this;c._hasRender||(a.log("渲染dom"),d=c.fileAccept,e=d?'accept="'+d+'"':"",f=c.multiple,g=f?'multiple="multiple"':"",h=b('"),i=b('
              '),i.append(h),a.$body.append(i),h.on("change",function(a){c.selected(a,h.get(0))}),c.$input=h,c._hasRender=!0)},c.fn.selectFiles=function(){var b=this;a.log("使用 html5 方式上传"),b.render(),a.log("选择文件"),b.$input.click()},c.fn.selected=function(c,d){var e=this,f=d.files||[];0!==f.length&&(a.log("选中 "+f.length+" 个文件"),b.each(f,function(a,b){e.upload(b)}))},c.fn.upload=function(b){function m(){c.clear()}var c=this,d=c.editor,e=b.name||"",f=b.type||"",g=d.config.uploadImgFns,h=d.config.uploadImgFileName||"wangEditorH5File",i=g.onload,j=g.ontimeout,k=g.onerror,l=new FileReader;return i&&j&&k?(a.log("开始执行 "+e+" 文件的上传"),l.onload=function(b){a.log("已读取"+e+"文件");var c=b.target.result||this.result;d.xhrUploadImg({event:b,filename:e,base64:c,fileType:f,name:h,loadfn:function(a,b){m();var c=this;i.call(c,a,b)},errorfn:function(b){m(),a.isOnWebsite&&alert("wangEditor官网暂时没有服务端,因此报错。实际项目中不会发生");var c=this;k.call(c,b)},timeoutfn:function(b){m(),a.isOnWebsite&&alert("wangEditor官网暂时没有服务端,因此超时。实际项目中不会发生");var c=this;j(c,b)}})},l.readAsDataURL(b),void 0):(a.error("请为编辑器配置上传图片的 onload ontimeout onerror 回调事件"),void 0)},a.UploadFile=c}}),b(function(a,b){if(!window.FileReader||!window.FormData){var c=function(a){this.editor=a.editor,this.uploadUrl=a.uploadUrl,this.timeout=a.timeout,this.fileAccept=a.fileAccept,this.multiple=!1};c.fn=c.prototype,c.fn.clear=function(){this.$input.val(""),a.log("input value 已清空")},c.fn.hideModal=function(){this.modal.hide()},c.fn.render=function(){var f,g,h,i,j,k,l,m,n,o,p,c=this,d=c.editor,e=d.config.uploadImgFileName||"wangEditorFormFile";c._hasRender||(f=c.uploadUrl,a.log("渲染dom"),g="iframe"+a.random(),h=b(''),i=c.multiple,j=i?'multiple="multiple"':"",k=b("

              选择图片并上传

              "),l=b(''),m=b(''),n=b('
              '),o=b('
              '),n.append(k).append(l).append(m),b.each(d.config.uploadParams,function(a,c){n.append(b(''))}),o.append(n),o.append(h),c.$input=l,c.$iframe=h,p=new a.Modal(d,void 0,{$content:o}),c.modal=p,c._hasRender=!0)},c.fn.bindLoadEvent=function(){function h(){var e,h,d=b.trim(f.document.body.innerHTML);d&&(e=a.$input.val(),h=e,e.lastIndexOf("\\")>=0&&(h=e.slice(e.lastIndexOf("\\")+1),h.indexOf(".")>0&&(h=h.split(".")[0])),c.uploadImgOriginalName=h,g.call(c,d),a.clear(),a.hideModal())}var c,d,e,f,g,a=this;a._hasBindLoad||(c=a.editor,d=a.$iframe,e=d.get(0),f=e.contentWindow,g=c.config.uploadImgFns.onload,e.attachEvent?e.attachEvent("onload",h):e.onload=h,a._hasBindLoad=!0)},c.fn.show=function(){function c(){b.show(),a.bindLoadEvent()}var a=this,b=a.modal;setTimeout(c)},c.fn.selectFiles=function(){var b=this;a.log("使用 form 方式上传"),b.render(),b.clear(),b.show()},a.UploadFile=c}}),b(function(a,b){a.plugin(function(){function k(){var d=/^data:(image\/\w+);base64/,f=e.find("img");a.log("粘贴后,检查到编辑器有"+f.length+"个图片。开始遍历图片,试图找到刚刚粘贴过来的图片"),b.each(f,function(){var g,l,e=this,f=b(e),k=f.attr("src");j.each(function(){return e===this?(g=!0,!1):void 0}),g||(a.log("找到一个粘贴过来的图片"),d.test(k)?(a.log("src 是 base64 格式,可以上传"),l=k.match(d)[1],c.xhrUploadImg({event:i,base64:k,fileType:l,name:h})):a.log("src 为 "+k+" ,不是 base64 格式,暂时不支持上传"),f.remove())}),a.log("遍历结束")}var i,j,c=this,d=c.txt,e=d.$txt,f=c.config,g=f.uploadImgUrl,h=f.uploadImgFileName||"wangEditorPasteFile";g&&e.on("paste",function(d){var f,g,l;i=d,f=i.clipboardData||i.originalEvent.clipboardData,g=null==f?window.clipboardData&&window.clipboardData.getData("text"):f.getData("text/plain")||f.getData("text/html"),g||(l=f&&f.items,l?(a.log("通过 data.items 得到了数据"),b.each(l,function(b,d){var f,g,e=d.type||"";e.indexOf("image")<0||(f=d.getAsFile(),g=new FileReader,a.log("得到一个粘贴图片"),g.onload=function(b){a.log("读取到粘贴的图片");var d=b.target.result||this.result;c.xhrUploadImg({event:i,base64:d,fileType:e,name:h})},g.readAsDataURL(f))})):(a.log("未从 data.items 得到数据,使用检测粘贴图片的方式"),j=e.find("img"),a.log("粘贴前,检查到编辑器有"+j.length+"个图片"),setTimeout(k,0)))})})}),b(function(a,b){a.plugin(function(){var c=this,d=c.txt,e=d.$txt,f=c.config,g=f.uploadImgUrl,h=f.uploadImgFileName||"wangEditorDragFile";g&&(a.$document.on("dragleave drop dragenter dragover",function(a){a.preventDefault()}),e.on("drop",function(d){var e,f;d.preventDefault(),e=d.originalEvent,f=e.dataTransfer&&e.dataTransfer.files,f&&f.length&&b.each(f,function(b,e){var i,f=e.type,g=e.name;f.indexOf("image/")<0||(a.log("得到图片 "+g),i=new FileReader,i.onload=function(b){a.log("读取到图片 "+g);var e=b.target.result||this.result;c.xhrUploadImg({event:d,base64:e,fileType:f,name:h})},i.readAsDataURL(e))})}))})}),b(function(a,b){a.plugin(function(){function n(){h||(o(),i.append(j).append(k).append(l).append(m),c.$editorContainer.append(i),h=!0)}function o(){function b(b,d){a&&c.customCommand(b,a,d)}var a;k.click(function(c){a=function(){g.remove()},b(c,function(){setTimeout(q,100)})}),m.click(function(c){a=function(){g.css({width:"100%"})},b(c,function(){setTimeout(p)})}),l.click(function(c){a=function(){g.css({width:"auto"})},b(c,function(){setTimeout(p)})})}function p(){var a,b,d,e,h,k,l,m,n,o,p;c._disabled||null!=g&&(g.addClass("clicked"),a=g.position(),b=a.top,d=a.left,e=g.outerHeight(),h=g.outerWidth(),k=b+e,l=d,m=0,n=f.position().top,o=f.outerHeight(),k>n+o&&(k=n+o),i.show(),p=i.outerWidth(),m=h/2-p/2,i.css({top:k+5,left:l,"margin-left":m}),0>m?(i.css("margin-left","0"),j.hide()):j.show())}function q(){null!=g&&(g.removeClass("clicked"),g=null,i.hide())}var g,c=this,d=c.txt,e=d.$txt,f=c.useMaxHeight?e.parent():e,h=!1,i=b('
              '),j=b('
              '),k=b(''),l=b(''),m=b('');f.on("click","table",function(a){var c=b(a.currentTarget);return n(),g&&g.get(0)===c.get(0)?(setTimeout(q,100),void 0):(g=c,p(),a.preventDefault(),a.stopPropagation(),void 0)}).on("click keydown scroll",function(){setTimeout(q,100)}),a.$body.on("click keydown scroll",function(){setTimeout(q,100)})})}),b(function(a,b){a.userAgent.indexOf("MSIE 8")>0||a.plugin(function(){function C(a,d){var e,f,g,h,k;if(i){if(f=function(){null!=d&&(j=d)},h=!1,k=i.parent(),"a"===k.get(0).nodeName.toLowerCase()?(g=k,h=!0):g=b(''),null==d)return g.attr("href")||"";if(""===d)h&&(e=function(){i.unwrap()});else{if(d===j)return;e=function(){g.attr("href",d),h||i.wrap(g)}}e&&c.customCommand(a,e,f)}}function D(){k||(E(),F(),o.append(p).append(q).append(r).append(s).append(t).append(u).append(v).append(w),x.append(y).append(A).append(z),m.append(n).append(o).append(x),c.$editorContainer.append(m).append(l),k=!0)}function E(){function d(b,d){a&&c.customCommand(b,a,d)}var a;p.click(function(b){C(b,""),a=function(){i.remove()},d(b,function(){setTimeout(H,100)})}),r.click(function(b){a=function(){var a=i.get(0),b=a.width,c=a.height;b=1.1*b,c=1.1*c,i.css({width:b+"px",height:c+"px"})},d(b,function(){setTimeout(G)})}),q.click(function(b){a=function(){var a=i.get(0),b=a.width,c=a.height;b=.9*b,c=.9*c,i.css({width:b+"px",height:c+"px"})},d(b,function(){setTimeout(G)})}),s.click(function(b){a=function(){i.parents("p").css({"text-align":"left"}).attr("align","left")},d(b,function(){setTimeout(H,100)})}),u.click(function(b){a=function(){i.parents("p").css({"text-align":"right"}).attr("align","right")},d(b,function(){setTimeout(H,100)})}),t.click(function(b){a=function(){i.parents("p").css({"text-align":"center"}).attr("align","center")},d(b,function(){setTimeout(H,100)})}),v.click(function(a){a.preventDefault(),j=C(a),y.val(j),o.hide(),x.show()}),z.click(function(a){a.preventDefault();var c=b.trim(y.val());c&&C(a,c),setTimeout(H)}),A.click(function(a){a.preventDefault(),y.val(j),o.show(),x.hide()}),w.click(function(a){a.preventDefault(),C(a,""),setTimeout(H)})}function F(){function h(a){var n,o,h=a.pageX-b,j=a.pageY-c,k=d+h,m=e+j;l.css({"margin-left":k,"margin-top":m}),n=f+h,o=g+j,i&&i.css({width:n,height:o})}var b,c,d,e,f,g;l.on("mousedown",function(j){i&&(b=j.pageX,c=j.pageY,d=parseFloat(l.css("margin-left"),10),e=parseFloat(l.css("margin-top"),10),f=i.width(),g=i.height(),m.hide(),a.$document.on("mousemove._dragResizeImg",h),a.$document.on("mouseup._dragResizeImg",function(){a.$document.off("mousemove._dragResizeImg"),a.$document.off("mouseup._dragResizeImg"),H(),l.css({"margin-left":d,"margin-top":e}),B=!1}),B=!0)})}function G(){var a,b,d,e,f,h,j,k,o,p,q;c._disabled||null!=i&&(i.addClass("clicked"),a=i.position(),b=a.top,d=a.left,e=i.outerHeight(),f=i.outerWidth(),l.css({top:b+e,left:d+f}),h=b+e,j=d,k=0,o=g.position().top,p=g.outerHeight(),h>o+p?h=o+p:l.show(),m.show(),q=m.outerWidth(),k=f/2-q/2,m.css({top:h+5,left:j,"margin-left":k}),0>k?(m.css("margin-left","0"),n.hide()):n.show(),c.disableMenusExcept())}function H(){null!=i&&(i.removeClass("clicked"),i=null,m.hide(),l.hide(),c.enableMenusExcept())}function I(a){var d=!1;return c.emotionUrls?(b.each(c.emotionUrls,function(b,c){var e=!1;return a===c&&(d=!0,e=!0),e?!1:void 0}),d):d}var i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,c=this,d=c.config.lang,e=c.txt,f=e.$txt,g=c.useMaxHeight?f.parent():f;c.$editorContainer,j="",k=!1,l=b('
              '),m=b('
              '),n=b('
              '),o=b("
              "),p=b(''),q=b(''),r=b(''),s=b(''),t=b(''),u=b(''),v=b(''),w=b(''),x=b('
              '),y=b(''),z=b('"),A=b('"),B=!1,g.on("mousedown","img",function(a){a.preventDefault()}).on("click","img",function(a){var c=b(a.currentTarget),d=c.attr("src");if(d&&!I(d)){if(D(),i&&i.get(0)===c.get(0))return setTimeout(H,100),void 0;i=c,G(),o.show(),x.hide(),a.preventDefault(),a.stopPropagation()}}).on("click keydown scroll",function(){B||setTimeout(H,100)})})}),b(function(a,b){a.plugin(function(){function o(){i||(f.append(g).append(h),a.$editorContainer.append(f),i=!0)}function p(){var b,c,d,g,h,i,j;e&&(b=e.position(),c=b.left,d=b.top,g=e.height(),h=d+g+5,i=a.menuContainer.height(),j=a.txt.$txt.outerHeight(),h>i+j&&(h=i+j+5),f.css({top:h,left:c}))}function q(){if(!j&&e){o(),f.show();var a=e.attr("href");h.attr("href",a),p(),j=!0}}function r(){j&&e&&(f.hide(),j=!1)}var e,i,k,l,m,a=this,c=a.config.lang,d=a.txt.$txt,f=b('
              '),g=b('
              '),h=b(' '+c.openLink+""),j=!1;d.on("mouseenter","a",function(a){k&&clearTimeout(k),k=setTimeout(function(){var f,c=a.currentTarget,d=b(c);e=d,f=d.children("img"),f.length&&(f.click(function(){r()}),f.hasClass("clicked"))||q()},500)}).on("mouseleave","a",function(){l&&clearTimeout(l),l=setTimeout(r,500)}).on("click keydown scroll",function(){setTimeout(r,100)}),f.on("mouseenter",function(){l&&clearTimeout(l)}).on("mouseleave",function(){m&&clearTimeout(m),m=setTimeout(r,500)})})}),b(function(a){a.plugin(function(){var d,e,f,g,h,i,j,k,l,b=this,c=b.config.menuFixed;c!==!1&&"number"==typeof c&&(d=parseFloat(a.$body.css("margin-top"),10),isNaN(d)&&(d=0),e=b.$editorContainer,f=e.offset().top,g=e.outerHeight(),h=b.menuContainer.$menuContainer,i=h.css("position"),j=h.css("top"),k=h.offset().top,l=h.outerHeight(),b.txt.$txt,a.$window.scroll(function(){var m,n;b.isFullScreen||(m=a.$window.scrollTop(),n=h.width(),0===k&&(k=h.offset().top,f=e.offset().top,g=e.outerHeight(),l=h.outerHeight()),m>=k&&f+g>m+c+l+30?(h.css({position:"fixed",top:c}),h.width(n),a.$body.css({"margin-top":d+l}),b._isMenufixed||(b._isMenufixed=!0)):(h.css({position:i,top:j}),h.css("width","100%"),a.$body.css({"margin-top":d}),b._isMenufixed&&(b._isMenufixed=!1)))}))})}),b(function(a,b){a.createMenu(function(c){var e,f,d="indent";c(d)&&(e=this,f=new a.Menu({editor:e,id:d,title:"缩进",$domNormal:b(''),$domSelected:b('')}),f.clickEvent=function(a){function g(){f.css("text-indent","2em")}var f,c=e.getRangeElem(),d=e.getSelfOrParentByName(c,"p");return d?(f=b(d),e.customCommand(a,g),void 0):a.preventDefault()},f.clickEventSelected=function(a){function g(){f.css("text-indent","0")}var f,c=e.getRangeElem(),d=e.getSelfOrParentByName(c,"p");return d?(f=b(d),e.customCommand(a,g),void 0):a.preventDefault()},f.updateSelectedEvent=function(){var d,f,a=e.getRangeElem(),c=e.getSelfOrParentByName(a,"p");return c?(d=b(c),f=d.css("text-indent"),f&&"0px"!==f?!0:!1):!1},e.menus[d]=f)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,h,d="lineheight";c(d)&&(e=this,e.commandHooks.lineHeight=function(a){var c=e.getRangeElem(),d=e.getSelfOrParentByName(c,"p,h1,h2,h3,h4,h5,pre");d&&b(d).css("line-height",a+"")},f=new a.Menu({editor:e,id:d,title:"行高",commandName:"lineHeight",$domNormal:b(''),$domSelected:b('')}),g={"1.0":"1.0倍",1.5:"1.5倍",1.8:"1.8倍","2.0":"2.0倍",2.5:"2.5倍","3.0":"3.0倍"},h='{#title}',f.dropList=new a.DropList(e,f,{data:g,tpl:h}),e.menus[d]=f)})}),b(function(a,b){a.plugin(function(){var e,f,g,h,c=this,d=c.config.customUpload;if(d){if(c.config.uploadImgUrl)return alert("自定义上传无效,详看浏览器日志console.log"),a.error("已经配置了 uploadImgUrl ,就不能再配置 customUpload ,两者冲突。将导致自定义上传无效。"),void 0;e=c.$uploadContent,e||a.error("自定义上传,无法获取 editor.$uploadContent"),f=b('
              '),e.append(f),g="upload"+a.random(),h="upload"+a.random(),f.attr("id",g),e.attr("id",h),c.customUploadBtnId=g,c.customUploadContainerId=h}})}),window.wangEditor}); \ No newline at end of file diff --git a/public/static/libs/webuploader/README.md b/public/static/libs/webuploader/README.md new file mode 100644 index 0000000..7f660e6 --- /dev/null +++ b/public/static/libs/webuploader/README.md @@ -0,0 +1,25 @@ +目录说明 +======================== + +```bash +├── Uploader.swf # SWF文件,当使用Flash运行时需要引入。 +├ +├── webuploader.js # 完全版本。 +├── webuploader.min.js # min版本 +├ +├── webuploader.flashonly.js # 只有Flash实现的版本。 +├── webuploader.flashonly.min.js # min版本 +├ +├── webuploader.html5only.js # 只有Html5实现的版本。 +├── webuploader.html5only.min.js # min版本 +├ +├── webuploader.noimage.js # 去除图片处理的版本,包括HTML5和FLASH. +├── webuploader.noimage.min.js # min版本 +├ +├── webuploader.custom.js # 自定义打包方案,请查看 Gruntfile.js,满足移动端使用。 +└── webuploader.custom.min.js # min版本 +``` + +## 示例 + +请把整个 Git 包下载下来放在 php 服务器下,因为默认提供的文件接受是用 php 编写的,打开 examples 页面便能查看示例效果。 \ No newline at end of file diff --git a/public/static/libs/webuploader/Uploader.swf b/public/static/libs/webuploader/Uploader.swf new file mode 100644 index 0000000..caef004 Binary files /dev/null and b/public/static/libs/webuploader/Uploader.swf differ diff --git a/public/static/libs/webuploader/webuploader.css b/public/static/libs/webuploader/webuploader.css new file mode 100644 index 0000000..43d744d --- /dev/null +++ b/public/static/libs/webuploader/webuploader.css @@ -0,0 +1,28 @@ +.webuploader-container { + position: relative; +} +.webuploader-element-invisible { + position: absolute !important; + clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ + clip: rect(1px,1px,1px,1px); +} +.webuploader-pick { + position: relative; + display: inline-block; + cursor: pointer; + background: #00b7ee; + padding: 10px 15px; + color: #fff; + text-align: center; + border-radius: 3px; + overflow: hidden; +} +.webuploader-pick-hover { + background: #00a2d4; +} + +.webuploader-pick-disable { + opacity: 0.6; + pointer-events:none; +} + diff --git a/public/static/libs/webuploader/webuploader.custom.js b/public/static/libs/webuploader/webuploader.custom.js new file mode 100644 index 0000000..3be04d4 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.custom.js @@ -0,0 +1,6530 @@ +/*! WebUploader 0.1.6 */ + + +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + * @require "jquery" + * @require "zepto" + */ + define('dollar-third',[],function() { + var req = window.require; + var $ = window.__dollar || + window.jQuery || + window.Zepto || + req('jquery') || + req('zepto'); + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * 直接来源于jquery的代码。 + * @fileOverview Promise/A+ + * @beta + */ + define('promise-builtin',[ + 'dollar' + ], function( $ ) { + + var api; + + // 简单版Callbacks, 默认memory,可选once. + function Callbacks( once ) { + var list = [], + stack = !once && [], + fire = function( data ) { + memory = data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ); + } + firing = false; + + if ( list ) { + if ( stack ) { + stack.length && fire( stack.shift() ); + } else { + list = []; + } + } + }, + self = { + add: function() { + if ( list ) { + var start = list.length; + (function add ( args ) { + $.each( args, function( _, arg ) { + var type = $.type( arg ); + if ( type === 'function' ) { + list.push( arg ); + } else if ( arg && arg.length && + type !== 'string' ) { + + add( arg ); + } + }); + })( arguments ); + + if ( firing ) { + firingLength = list.length; + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + + disable: function() { + list = stack = memory = undefined; + return this; + }, + + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + + fireWith: function( context, args ) { + if ( list && (!fired || stack) ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + + fire: function() { + self.fireWith( this, arguments ); + return this; + } + }, + + fired, firing, firingStart, firingLength, firingIndex, memory; + + return self; + } + + function Deferred( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ 'resolve', 'done', Callbacks( true ), 'resolved' ], + [ 'reject', 'fail', Callbacks( true ), 'rejected' ], + [ 'notify', 'progress', Callbacks() ] + ], + state = 'pending', + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return Deferred(function( newDefer ) { + $.each( tuples, function( i, tuple ) { + var action = tuple[ 0 ], + fn = $.isFunction( fns[ i ] ) && fns[ i ]; + + // deferred[ done | fail | progress ] for + // forwarding actions to newDefer + deferred[ tuple[ 1 ] ](function() { + var returned; + + returned = fn && fn.apply( this, arguments ); + + if ( returned && + $.isFunction( returned.promise ) ) { + + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ action + 'With' ]( + this === promise ? + newDefer.promise() : + this, + fn ? [ returned ] : arguments ); + } + }); + }); + fns = null; + }).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + + return obj != null ? $.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + $.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + 'With' ]( this === deferred ? promise : + this, arguments ); + return this; + }; + deferred[ tuple[ 0 ] + 'With' ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + } + + api = { + /** + * 创建一个[Deferred](http://api.jquery.com/category/deferred-object/)对象。 + * 详细的Deferred用法说明,请参照jQuery的API文档。 + * + * Deferred对象在钩子回掉函数中经常要用到,用来处理需要等待的异步操作。 + * + * @for Base + * @method Deferred + * @grammar Base.Deferred() => Deferred + * @example + * // 在文件开始发送前做些异步操作。 + * // WebUploader会等待此异步操作完成后,开始发送文件。 + * Uploader.register({ + * 'before-send-file': 'doSomthingAsync' + * }, { + * + * doSomthingAsync: function() { + * var deferred = Base.Deferred(); + * + * // 模拟一次异步操作。 + * setTimeout(deferred.resolve, 2000); + * + * return deferred.promise(); + * } + * }); + */ + Deferred: Deferred, + + /** + * 判断传入的参数是否为一个promise对象。 + * @method isPromise + * @grammar Base.isPromise( anything ) => Boolean + * @param {*} anything 检测对象。 + * @return {Boolean} + * @for Base + * @example + * console.log( Base.isPromise() ); // => false + * console.log( Base.isPromise({ key: '123' }) ); // => false + * console.log( Base.isPromise( Base.Deferred().promise() ) ); // => true + * + * // Deferred也是一个Promise + * console.log( Base.isPromise( Base.Deferred() ) ); // => true + */ + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + }, + + /** + * 返回一个promise,此promise在所有传入的promise都完成了后完成。 + * 详细请查看[这里](http://api.jquery.com/jQuery.when/)。 + * + * @method when + * @for Base + * @grammar Base.when( promise1[, promise2[, promise3...]] ) => Promise + */ + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + slice = [].slice, + resolveValues = slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || (subordinate && + $.isFunction( subordinate.promise )) ? length : 0, + + // the master Deferred. If resolveValues consist of + // only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? + slice.call( arguments ) : value; + + if ( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !(--remaining) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && + $.isFunction( resolveValues[ i ].promise ) ) { + + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, + resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, + progressValues ) ); + } else { + --remaining; + } + } + } + + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } + }; + + return api; + }); + define('promise',[ + 'promise-builtin' + ], function( $ ) { + return $; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.6', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClient, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClient.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file', + style: 'webuploader-pick' //pick element class attribute, default is "webuploader-pick" + }; + + Base.inherits( RuntimeClient, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button, + style = opts.style; + + if (style) + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + if (style) + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + if (style) + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addBtn + * @for Uploader + * @grammar addBtn( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addBtn({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.on('dialogopen', function() { + me.owner.trigger('dialogOpen', picker.button); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview Image + */ + define('lib/image',[ + 'base', + 'runtime/client', + 'lib/blob' + ], function( Base, RuntimeClient, Blob ) { + var $ = Base.$; + + // 构造器。 + function Image( opts ) { + this.options = $.extend({}, Image.options, opts ); + RuntimeClient.call( this, 'Image' ); + + this.on( 'load', function() { + this._info = this.exec('info'); + this._meta = this.exec('meta'); + }); + } + + // 默认选项。 + Image.options = { + + // 默认的图片处理质量 + quality: 90, + + // 是否裁剪 + crop: false, + + // 是否保留头部信息 + preserveHeaders: false, + + // 是否允许放大。 + allowMagnify: false + }; + + // 继承RuntimeClient. + Base.inherits( RuntimeClient, { + constructor: Image, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + loadFromBlob: function( blob ) { + var me = this, + ruid = blob.getRuid(); + + this.connectRuntime( ruid, function() { + me.exec( 'init', me.options ); + me.exec( 'loadFromBlob', blob ); + }); + }, + + resize: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'resize' ].concat( args ) ); + }, + + crop: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'crop' ].concat( args ) ); + }, + + getAsDataUrl: function( type ) { + return this.exec( 'getAsDataUrl', type ); + }, + + getAsBlob: function( type ) { + var blob = this.exec( 'getAsBlob', type ); + + return new Blob( this.getRuid(), blob ); + } + }); + + return Image; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/image',[ + 'base', + 'uploader', + 'lib/image', + 'widgets/widget' + ], function( Base, Uploader, Image ) { + + var $ = Base.$, + throttle; + + // 根据要处理的文件大小来节流,一次不能处理太多,会卡。 + throttle = (function( max ) { + var occupied = 0, + waiting = [], + tick = function() { + var item; + + while ( waiting.length && occupied < max ) { + item = waiting.shift(); + occupied += item[ 0 ]; + item[ 1 ](); + } + }; + + return function( emiter, size, cb ) { + waiting.push([ size, cb ]); + emiter.once( 'destroy', function() { + occupied -= size; + setTimeout( tick, 1 ); + }); + setTimeout( tick, 1 ); + }; + })( 5 * 1024 * 1024 ); + + $.extend( Uploader.options, { + + /** + * @property {Object} [thumb] + * @namespace options + * @for Uploader + * @description 配置生成缩略图的选项。 + * + * 默认为: + * + * ```javascript + * { + * width: 110, + * height: 110, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 70, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: true, + * + * // 是否允许裁剪。 + * crop: true, + * + * // 为空的话则保留原有图片格式。 + * // 否则强制转换成指定的类型。 + * type: 'image/jpeg' + * } + * ``` + */ + thumb: { + width: 110, + height: 110, + quality: 70, + allowMagnify: true, + crop: true, + preserveHeaders: false, + + // 为空的话则保留原有图片格式。 + // 否则强制转换成指定的类型。 + // IE 8下面 base64 大小不能超过 32K 否则预览失败,而非 jpeg 编码的图片很可 + // 能会超过 32k, 所以这里设置成预览的时候都是 image/jpeg + type: 'image/jpeg' + }, + + /** + * @property {Object} [compress] + * @namespace options + * @for Uploader + * @description 配置压缩的图片的选项。如果此选项为`false`, 则图片在上传前不进行压缩。 + * + * 默认为: + * + * ```javascript + * { + * width: 1600, + * height: 1600, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 90, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: false, + * + * // 是否允许裁剪。 + * crop: false, + * + * // 是否保留头部meta信息。 + * preserveHeaders: true, + * + * // 如果发现压缩后文件大小比原来还大,则使用原来图片 + * // 此属性可能会影响图片自动纠正功能 + * noCompressIfLarger: false, + * + * // 单位字节,如果图片大小小于此值,不会采用压缩。 + * compressSize: 0 + * } + * ``` + */ + compress: { + width: 1600, + height: 1600, + quality: 90, + allowMagnify: false, + crop: false, + preserveHeaders: true + } + }); + + return Uploader.register({ + + name: 'image', + + + /** + * 生成缩略图,此过程为异步,所以需要传入`callback`。 + * 通常情况在图片加入队里后调用此方法来生成预览图以增强交互效果。 + * + * 当 width 或者 height 的值介于 0 - 1 时,被当成百分比使用。 + * + * `callback`中可以接收到两个参数。 + * * 第一个为error,如果生成缩略图有错误,此error将为真。 + * * 第二个为ret, 缩略图的Data URL值。 + * + * **注意** + * Date URL在IE6/7中不支持,所以不用调用此方法了,直接显示一张暂不支持预览图片好了。 + * 也可以借助服务端,将 base64 数据传给服务端,生成一个临时文件供预览。 + * + * @method makeThumb + * @grammar makeThumb( file, callback ) => undefined + * @grammar makeThumb( file, callback, width, height ) => undefined + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.makeThumb( file, function( error, ret ) { + * if ( error ) { + * $li.text('预览错误'); + * } else { + * $li.append(''); + * } + * }); + * + * }); + */ + makeThumb: function( file, cb, width, height ) { + var opts, image; + + file = this.request( 'get-file', file ); + + // 只预览图片格式。 + if ( !file.type.match( /^image/ ) ) { + cb( true ); + return; + } + + opts = $.extend({}, this.options.thumb ); + + // 如果传入的是object. + if ( $.isPlainObject( width ) ) { + opts = $.extend( opts, width ); + width = null; + } + + width = width || opts.width; + height = height || opts.height; + + image = new Image( opts ); + + image.once( 'load', function() { + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + // 当 resize 完后 + image.once( 'complete', function() { + cb( false, image.getAsDataUrl( opts.type ) ); + image.destroy(); + }); + + image.once( 'error', function( reason ) { + cb( reason || true ); + image.destroy(); + }); + + throttle( image, file.source.size, function() { + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + image.loadFromBlob( file.source ); + }); + }, + + beforeSendFile: function( file ) { + var opts = this.options.compress || this.options.resize, + compressSize = opts && opts.compressSize || 0, + noCompressIfLarger = opts && opts.noCompressIfLarger || false, + image, deferred; + + file = this.request( 'get-file', file ); + + // 只压缩 jpeg 图片格式。 + // gif 可能会丢失针 + // bmp png 基本上尺寸都不大,且压缩比比较小。 + if ( !opts || !~'image/jpeg,image/jpg'.indexOf( file.type ) || + file.size < compressSize || + file._compressed ) { + return; + } + + opts = $.extend({}, opts ); + deferred = Base.Deferred(); + + image = new Image( opts ); + + deferred.always(function() { + image.destroy(); + image = null; + }); + image.once( 'error', deferred.reject ); + image.once( 'load', function() { + var width = opts.width, + height = opts.height; + + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + image.once( 'complete', function() { + var blob, size; + + // 移动端 UC / qq 浏览器的无图模式下 + // ctx.getImageData 处理大图的时候会报 Exception + // INDEX_SIZE_ERR: DOM Exception 1 + try { + blob = image.getAsBlob( opts.type ); + + size = file.size; + + // 如果压缩后,比原来还大则不用压缩后的。 + if ( !noCompressIfLarger || blob.size < size ) { + // file.source.destroy && file.source.destroy(); + file.source = blob; + file.size = blob.size; + + file.trigger( 'resize', blob.size, size ); + } + + // 标记,避免重复压缩。 + file._compressed = true; + deferred.resolve(); + } catch ( e ) { + // 出错了直接继续,让其上传原始图片 + deferred.resolve(); + } + }); + + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + + image.loadFromBlob( file.source ); + return deferred.promise(); + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + if ( files.length ) { + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + // 销毁上传相关的属性。 + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定的文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + file.setStatus( Status.QUEUED ); + + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + file.setStatus( Status.PROGRESS ); + }); + + + } else if (file.getStatus() !== Status.PROGRESS) { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return Base.nextTick( me.__tick ); + } + + me.runing = true; + var files = []; + + // 如果有暂停的,则续传 + file || $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + me._trigged = false; + files.push(file); + v.transport && v.transport.send(); + } + }); + + $.each(files, function() { + this.setStatus( Status.PROGRESS ); + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this, + block; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + + + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file === file) { + block = v; + return false; + } + }); + + block.transport && block.transport.abort(); + + if (interrupt) { + me._putback(block); + me._popBlock(block); + } + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + // 正在准备中的文件。 + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + + /** + * @fileOverview 日志组件,主要用来收集错误信息,可以帮助 webuploader 更好的定位问题和发展。 + * + * 如果您不想要启用此功能,请在打包的时候去掉 log 模块。 + * + * 或者可以在初始化的时候通过 options.disableWidgets 属性禁用。 + * + * 如: + * WebUploader.create({ + * ... + * + * disableWidgets: 'log', + * + * ... + * }) + */ + define('widgets/log',[ + 'base', + 'uploader', + 'widgets/widget' + ], function( Base, Uploader ) { + var $ = Base.$, + logUrl = ' http://static.tieba.baidu.com/tb/pms/img/st.gif??', + product = (location.hostname || location.host || 'protected').toLowerCase(), + + // 只针对 baidu 内部产品用户做统计功能。 + enable = product && /baidu/i.exec(product), + base; + + if (!enable) { + return; + } + + base = { + dv: 3, + master: 'webuploader', + online: /test/.exec(product) ? 0 : 1, + module: '', + product: product, + type: 0 + }; + + function send(data) { + var obj = $.extend({}, base, data), + url = logUrl.replace(/^(.*)\?/, '$1' + $.param( obj )), + image = new Image(); + + image.src = url; + } + + return Uploader.register({ + name: 'log', + + init: function() { + var owner = this.owner, + count = 0, + size = 0; + + owner + .on('error', function(code) { + send({ + type: 2, + c_error_code: code + }); + }) + .on('uploadError', function(file, reason) { + send({ + type: 2, + c_error_code: 'UPLOAD_ERROR', + c_reason: '' + reason + }); + }) + .on('uploadComplete', function(file) { + count++; + size += file.size; + }). + on('uploadFinished', function() { + send({ + c_count: count, + c_size: size + }); + count = size = 0; + }); + + send({ + c_usage: 1 + }); + } + }); + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'capture', 'camera'); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function(e) { + input.trigger('click'); + e.stopPropagation(); + owner.trigger('dialogopen'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/util',[ + 'base' + ], function( Base ) { + + var urlAPI = window.createObjectURL && window || + window.URL && URL.revokeObjectURL && URL || + window.webkitURL, + createObjectURL = Base.noop, + revokeObjectURL = createObjectURL; + + if ( urlAPI ) { + + // 更安全的方式调用,比如android里面就能把context改成其他的对象。 + createObjectURL = function() { + return urlAPI.createObjectURL.apply( urlAPI, arguments ); + }; + + revokeObjectURL = function() { + return urlAPI.revokeObjectURL.apply( urlAPI, arguments ); + }; + } + + return { + createObjectURL: createObjectURL, + revokeObjectURL: revokeObjectURL, + + dataURL2Blob: function( dataURI ) { + var byteStr, intArray, ab, i, mimetype, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + ab = new ArrayBuffer( byteStr.length ); + intArray = new Uint8Array( ab ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + mimetype = parts[ 0 ].split(':')[ 1 ].split(';')[ 0 ]; + + return this.arrayBufferToBlob( ab, mimetype ); + }, + + dataURL2ArrayBuffer: function( dataURI ) { + var byteStr, intArray, i, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + intArray = new Uint8Array( byteStr.length ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + return intArray.buffer; + }, + + arrayBufferToBlob: function( buffer, type ) { + var builder = window.BlobBuilder || window.WebKitBlobBuilder, + bb; + + // android不支持直接new Blob, 只能借助blobbuilder. + if ( builder ) { + bb = new builder(); + bb.append( buffer ); + return bb.getBlob( type ); + } + + return new Blob([ buffer ], type ? { type: type } : {} ); + }, + + // 抽出来主要是为了解决android下面canvas.toDataUrl不支持jpeg. + // 你得到的结果是png. + canvasToDataUrl: function( canvas, type, quality ) { + return canvas.toDataURL( type, quality / 100 ); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + parseMeta: function( blob, callback ) { + callback( false, {}); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + updateImageHead: function( data ) { + return data; + } + }; + }); + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/imagemeta',[ + 'runtime/html5/util' + ], function( Util ) { + + var api; + + api = { + parsers: { + 0xffe1: [] + }, + + maxMetaDataSize: 262144, + + parse: function( blob, cb ) { + var me = this, + fr = new FileReader(); + + fr.onload = function() { + cb( false, me._parse( this.result ) ); + fr = fr.onload = fr.onerror = null; + }; + + fr.onerror = function( e ) { + cb( e.message ); + fr = fr.onload = fr.onerror = null; + }; + + blob = blob.slice( 0, me.maxMetaDataSize ); + fr.readAsArrayBuffer( blob.getSource() ); + }, + + _parse: function( buffer, noParse ) { + if ( buffer.byteLength < 6 ) { + return; + } + + var dataview = new DataView( buffer ), + offset = 2, + maxOffset = dataview.byteLength - 4, + headLength = offset, + ret = {}, + markerBytes, markerLength, parsers, i; + + if ( dataview.getUint16( 0 ) === 0xffd8 ) { + + while ( offset < maxOffset ) { + markerBytes = dataview.getUint16( offset ); + + if ( markerBytes >= 0xffe0 && markerBytes <= 0xffef || + markerBytes === 0xfffe ) { + + markerLength = dataview.getUint16( offset + 2 ) + 2; + + if ( offset + markerLength > dataview.byteLength ) { + break; + } + + parsers = api.parsers[ markerBytes ]; + + if ( !noParse && parsers ) { + for ( i = 0; i < parsers.length; i += 1 ) { + parsers[ i ].call( api, dataview, offset, + markerLength, ret ); + } + } + + offset += markerLength; + headLength = offset; + } else { + break; + } + } + + if ( headLength > 6 ) { + if ( buffer.slice ) { + ret.imageHead = buffer.slice( 2, headLength ); + } else { + // Workaround for IE10, which does not yet + // support ArrayBuffer.slice: + ret.imageHead = new Uint8Array( buffer ) + .subarray( 2, headLength ); + } + } + } + + return ret; + }, + + updateImageHead: function( buffer, head ) { + var data = this._parse( buffer, true ), + buf1, buf2, bodyoffset; + + + bodyoffset = 2; + if ( data.imageHead ) { + bodyoffset = 2 + data.imageHead.byteLength; + } + + if ( buffer.slice ) { + buf2 = buffer.slice( bodyoffset ); + } else { + buf2 = new Uint8Array( buffer ).subarray( bodyoffset ); + } + + buf1 = new Uint8Array( head.byteLength + 2 + buf2.byteLength ); + + buf1[ 0 ] = 0xFF; + buf1[ 1 ] = 0xD8; + buf1.set( new Uint8Array( head ), 2 ); + buf1.set( new Uint8Array( buf2 ), head.byteLength + 2 ); + + return buf1.buffer; + } + }; + + Util.parseMeta = function() { + return api.parse.apply( api, arguments ); + }; + + Util.updateImageHead = function() { + return api.updateImageHead.apply( api, arguments ); + }; + + return api; + }); + /** + * 代码来自于:https://github.com/blueimp/JavaScript-Load-Image + * 暂时项目中只用了orientation. + * + * 去除了 Exif Sub IFD Pointer, GPS Info IFD Pointer, Exif Thumbnail. + * @fileOverview EXIF解析 + */ + + // Sample + // ==================================== + // Make : Apple + // Model : iPhone 4S + // Orientation : 1 + // XResolution : 72 [72/1] + // YResolution : 72 [72/1] + // ResolutionUnit : 2 + // Software : QuickTime 7.7.1 + // DateTime : 2013:09:01 22:53:55 + // ExifIFDPointer : 190 + // ExposureTime : 0.058823529411764705 [1/17] + // FNumber : 2.4 [12/5] + // ExposureProgram : Normal program + // ISOSpeedRatings : 800 + // ExifVersion : 0220 + // DateTimeOriginal : 2013:09:01 22:52:51 + // DateTimeDigitized : 2013:09:01 22:52:51 + // ComponentsConfiguration : YCbCr + // ShutterSpeedValue : 4.058893515764426 + // ApertureValue : 2.5260688216892597 [4845/1918] + // BrightnessValue : -0.3126686601998395 + // MeteringMode : Pattern + // Flash : Flash did not fire, compulsory flash mode + // FocalLength : 4.28 [107/25] + // SubjectArea : [4 values] + // FlashpixVersion : 0100 + // ColorSpace : 1 + // PixelXDimension : 2448 + // PixelYDimension : 3264 + // SensingMethod : One-chip color area sensor + // ExposureMode : 0 + // WhiteBalance : Auto white balance + // FocalLengthIn35mmFilm : 35 + // SceneCaptureType : Standard + define('runtime/html5/imagemeta/exif',[ + 'base', + 'runtime/html5/imagemeta' + ], function( Base, ImageMeta ) { + + var EXIF = {}; + + EXIF.ExifMap = function() { + return this; + }; + + EXIF.ExifMap.prototype.map = { + 'Orientation': 0x0112 + }; + + EXIF.ExifMap.prototype.get = function( id ) { + return this[ id ] || this[ this.map[ id ] ]; + }; + + EXIF.exifTagTypes = { + // byte, 8-bit unsigned int: + 1: { + getValue: function( dataView, dataOffset ) { + return dataView.getUint8( dataOffset ); + }, + size: 1 + }, + + // ascii, 8-bit byte: + 2: { + getValue: function( dataView, dataOffset ) { + return String.fromCharCode( dataView.getUint8( dataOffset ) ); + }, + size: 1, + ascii: true + }, + + // short, 16 bit int: + 3: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint16( dataOffset, littleEndian ); + }, + size: 2 + }, + + // long, 32 bit int: + 4: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // rational = two long values, + // first is numerator, second is denominator: + 5: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ) / + dataView.getUint32( dataOffset + 4, littleEndian ); + }, + size: 8 + }, + + // slong, 32 bit signed int: + 9: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // srational, two slongs, first is numerator, second is denominator: + 10: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ) / + dataView.getInt32( dataOffset + 4, littleEndian ); + }, + size: 8 + } + }; + + // undefined, 8-bit byte, value depending on field: + EXIF.exifTagTypes[ 7 ] = EXIF.exifTagTypes[ 1 ]; + + EXIF.getExifValue = function( dataView, tiffOffset, offset, type, length, + littleEndian ) { + + var tagType = EXIF.exifTagTypes[ type ], + tagSize, dataOffset, values, i, str, c; + + if ( !tagType ) { + Base.log('Invalid Exif data: Invalid tag type.'); + return; + } + + tagSize = tagType.size * length; + + // Determine if the value is contained in the dataOffset bytes, + // or if the value at the dataOffset is a pointer to the actual data: + dataOffset = tagSize > 4 ? tiffOffset + dataView.getUint32( offset + 8, + littleEndian ) : (offset + 8); + + if ( dataOffset + tagSize > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid data offset.'); + return; + } + + if ( length === 1 ) { + return tagType.getValue( dataView, dataOffset, littleEndian ); + } + + values = []; + + for ( i = 0; i < length; i += 1 ) { + values[ i ] = tagType.getValue( dataView, + dataOffset + i * tagType.size, littleEndian ); + } + + if ( tagType.ascii ) { + str = ''; + + // Concatenate the chars: + for ( i = 0; i < values.length; i += 1 ) { + c = values[ i ]; + + // Ignore the terminating NULL byte(s): + if ( c === '\u0000' ) { + break; + } + str += c; + } + + return str; + } + return values; + }; + + EXIF.parseExifTag = function( dataView, tiffOffset, offset, littleEndian, + data ) { + + var tag = dataView.getUint16( offset, littleEndian ); + data.exif[ tag ] = EXIF.getExifValue( dataView, tiffOffset, offset, + dataView.getUint16( offset + 2, littleEndian ), // tag type + dataView.getUint32( offset + 4, littleEndian ), // tag length + littleEndian ); + }; + + EXIF.parseExifTags = function( dataView, tiffOffset, dirOffset, + littleEndian, data ) { + + var tagsNumber, dirEndOffset, i; + + if ( dirOffset + 6 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory offset.'); + return; + } + + tagsNumber = dataView.getUint16( dirOffset, littleEndian ); + dirEndOffset = dirOffset + 2 + 12 * tagsNumber; + + if ( dirEndOffset + 4 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory size.'); + return; + } + + for ( i = 0; i < tagsNumber; i += 1 ) { + this.parseExifTag( dataView, tiffOffset, + dirOffset + 2 + 12 * i, // tag offset + littleEndian, data ); + } + + // Return the offset to the next directory: + return dataView.getUint32( dirEndOffset, littleEndian ); + }; + + // EXIF.getExifThumbnail = function(dataView, offset, length) { + // var hexData, + // i, + // b; + // if (!length || offset + length > dataView.byteLength) { + // Base.log('Invalid Exif data: Invalid thumbnail data.'); + // return; + // } + // hexData = []; + // for (i = 0; i < length; i += 1) { + // b = dataView.getUint8(offset + i); + // hexData.push((b < 16 ? '0' : '') + b.toString(16)); + // } + // return 'data:image/jpeg,%' + hexData.join('%'); + // }; + + EXIF.parseExifData = function( dataView, offset, length, data ) { + + var tiffOffset = offset + 10, + littleEndian, dirOffset; + + // Check for the ASCII code for "Exif" (0x45786966): + if ( dataView.getUint32( offset + 4 ) !== 0x45786966 ) { + // No Exif data, might be XMP data instead + return; + } + if ( tiffOffset + 8 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid segment size.'); + return; + } + + // Check for the two null bytes: + if ( dataView.getUint16( offset + 8 ) !== 0x0000 ) { + Base.log('Invalid Exif data: Missing byte alignment offset.'); + return; + } + + // Check the byte alignment: + switch ( dataView.getUint16( tiffOffset ) ) { + case 0x4949: + littleEndian = true; + break; + + case 0x4D4D: + littleEndian = false; + break; + + default: + Base.log('Invalid Exif data: Invalid byte alignment marker.'); + return; + } + + // Check for the TIFF tag marker (0x002A): + if ( dataView.getUint16( tiffOffset + 2, littleEndian ) !== 0x002A ) { + Base.log('Invalid Exif data: Missing TIFF marker.'); + return; + } + + // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: + dirOffset = dataView.getUint32( tiffOffset + 4, littleEndian ); + // Create the exif object to store the tags: + data.exif = new EXIF.ExifMap(); + // Parse the tags of the main image directory and retrieve the + // offset to the next directory, usually the thumbnail directory: + dirOffset = EXIF.parseExifTags( dataView, tiffOffset, + tiffOffset + dirOffset, littleEndian, data ); + + // 尝试读取缩略图 + // if ( dirOffset ) { + // thumbnailData = {exif: {}}; + // dirOffset = EXIF.parseExifTags( + // dataView, + // tiffOffset, + // tiffOffset + dirOffset, + // littleEndian, + // thumbnailData + // ); + + // // Check for JPEG Thumbnail offset: + // if (thumbnailData.exif[0x0201]) { + // data.exif.Thumbnail = EXIF.getExifThumbnail( + // dataView, + // tiffOffset + thumbnailData.exif[0x0201], + // thumbnailData.exif[0x0202] // Thumbnail data length + // ); + // } + // } + }; + + ImageMeta.parsers[ 0xffe1 ].push( EXIF.parseExifData ); + return EXIF; + }); + /** + * @fileOverview Image + */ + define('runtime/html5/image',[ + 'base', + 'runtime/html5/runtime', + 'runtime/html5/util' + ], function( Base, Html5Runtime, Util ) { + + var BLANK = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D'; + + return Html5Runtime.register( 'Image', { + + // flag: 标记是否被修改过。 + modified: false, + + init: function() { + var me = this, + img = new Image(); + + img.onload = function() { + + me._info = { + type: me.type, + width: this.width, + height: this.height + }; + + //debugger; + + // 读取meta信息。 + if ( !me._metas && 'image/jpeg' === me.type ) { + Util.parseMeta( me._blob, function( error, ret ) { + me._metas = ret; + me.owner.trigger('load'); + }); + } else { + me.owner.trigger('load'); + } + }; + + img.onerror = function() { + me.owner.trigger('error'); + }; + + me._img = img; + }, + + loadFromBlob: function( blob ) { + var me = this, + img = me._img; + + me._blob = blob; + me.type = blob.type; + img.src = Util.createObjectURL( blob.getSource() ); + me.owner.once( 'load', function() { + Util.revokeObjectURL( img.src ); + }); + }, + + resize: function( width, height ) { + var canvas = this._canvas || + (this._canvas = document.createElement('canvas')); + + this._resize( this._img, canvas, width, height ); + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'resize' ); + }, + + crop: function( x, y, w, h, s ) { + var cvs = this._canvas || + (this._canvas = document.createElement('canvas')), + opts = this.options, + img = this._img, + iw = img.naturalWidth, + ih = img.naturalHeight, + orientation = this.getOrientation(); + + s = s || 1; + + // todo 解决 orientation 的问题。 + // values that require 90 degree rotation + // if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // switch ( orientation ) { + // case 6: + // tmp = x; + // x = y; + // y = iw * s - tmp - w; + // console.log(ih * s, tmp, w) + // break; + // } + + // (w ^= h, h ^= w, w ^= h); + // } + + cvs.width = w; + cvs.height = h; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + this._renderImageToCanvas( cvs, img, -x, -y, iw * s, ih * s ); + + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'crop' ); + }, + + getAsBlob: function( type ) { + var blob = this._blob, + opts = this.options, + canvas; + + type = type || this.type; + + // blob需要重新生成。 + if ( this.modified || this.type !== type ) { + canvas = this._canvas; + + if ( type === 'image/jpeg' ) { + + blob = Util.canvasToDataUrl( canvas, type, opts.quality ); + + if ( opts.preserveHeaders && this._metas && + this._metas.imageHead ) { + + blob = Util.dataURL2ArrayBuffer( blob ); + blob = Util.updateImageHead( blob, + this._metas.imageHead ); + blob = Util.arrayBufferToBlob( blob, type ); + return blob; + } + } else { + blob = Util.canvasToDataUrl( canvas, type ); + } + + blob = Util.dataURL2Blob( blob ); + } + + return blob; + }, + + getAsDataUrl: function( type ) { + var opts = this.options; + + type = type || this.type; + + if ( type === 'image/jpeg' ) { + return Util.canvasToDataUrl( this._canvas, type, opts.quality ); + } else { + return this._canvas.toDataURL( type ); + } + }, + + getOrientation: function() { + return this._metas && this._metas.exif && + this._metas.exif.get('Orientation') || 1; + }, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._metas = val; + return this; + } + + // getter + return this._metas; + }, + + destroy: function() { + var canvas = this._canvas; + this._img.onload = null; + + if ( canvas ) { + canvas.getContext('2d') + .clearRect( 0, 0, canvas.width, canvas.height ); + canvas.width = canvas.height = 0; + this._canvas = null; + } + + // 释放内存。非常重要,否则释放不了image的内存。 + this._img.src = BLANK; + this._img = this._blob = null; + }, + + _resize: function( img, cvs, width, height ) { + var opts = this.options, + naturalWidth = img.width, + naturalHeight = img.height, + orientation = this.getOrientation(), + scale, w, h, x, y; + + // values that require 90 degree rotation + if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // 交换width, height的值。 + width ^= height; + height ^= width; + width ^= height; + } + + scale = Math[ opts.crop ? 'max' : 'min' ]( width / naturalWidth, + height / naturalHeight ); + + // 不允许放大。 + opts.allowMagnify || (scale = Math.min( 1, scale )); + + w = naturalWidth * scale; + h = naturalHeight * scale; + + if ( opts.crop ) { + cvs.width = width; + cvs.height = height; + } else { + cvs.width = w; + cvs.height = h; + } + + x = (cvs.width - w) / 2; + y = (cvs.height - h) / 2; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + + this._renderImageToCanvas( cvs, img, x, y, w, h ); + }, + + _rotate2Orientaion: function( canvas, orientation ) { + var width = canvas.width, + height = canvas.height, + ctx = canvas.getContext('2d'); + + switch ( orientation ) { + case 5: + case 6: + case 7: + case 8: + canvas.width = height; + canvas.height = width; + break; + } + + 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( Math.PI ); + break; + + case 4: // vertical flip + ctx.translate( 0, height ); + ctx.scale( 1, -1 ); + break; + + case 5: // vertical flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.scale( 1, -1 ); + break; + + case 6: // 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( 0, -height ); + break; + + case 7: // horizontal flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( width, -height ); + ctx.scale( -1, 1 ); + break; + + case 8: // 90 rotate left + ctx.rotate( -0.5 * Math.PI ); + ctx.translate( -width, 0 ); + break; + } + }, + + // https://github.com/stomita/ios-imagefile-megapixel/ + // blob/master/src/megapix-image.js + _renderImageToCanvas: (function() { + + // 如果不是ios, 不需要这么复杂! + if ( !Base.os.ios ) { + return function( canvas ) { + var args = Base.slice( arguments, 1 ), + ctx = canvas.getContext('2d'); + + ctx.drawImage.apply( ctx, args ); + }; + } + + /** + * Detecting vertical squash in loaded image. + * Fixes a bug which squash image vertically while drawing into + * canvas for some images. + */ + function detectVerticalSquash( img, iw, ih ) { + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'), + sy = 0, + ey = ih, + py = ih, + data, alpha, ratio; + + + canvas.width = 1; + canvas.height = ih; + ctx.drawImage( img, 0, 0 ); + data = ctx.getImageData( 0, 0, 1, ih ).data; + + // search image edge pixel position in case + // it is squashed vertically. + while ( py > sy ) { + alpha = data[ (py - 1) * 4 + 3 ]; + + if ( alpha === 0 ) { + ey = py; + } else { + sy = py; + } + + py = (ey + sy) >> 1; + } + + ratio = (py / ih); + return (ratio === 0) ? 1 : ratio; + } + + // fix ie7 bug + // http://stackoverflow.com/questions/11929099/ + // html5-canvas-drawimage-ratio-bug-ios + if ( Base.os.ios >= 7 ) { + return function( canvas, img, x, y, w, h ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + vertSquashRatio = detectVerticalSquash( img, iw, ih ); + + return canvas.getContext('2d').drawImage( img, 0, 0, + iw * vertSquashRatio, ih * vertSquashRatio, + x, y, w, h ); + }; + } + + /** + * Detect subsampling in loaded image. + * In iOS, larger images than 2M pixels may be + * subsampled in rendering. + */ + function detectSubsampling( img ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + canvas, ctx; + + // subsampling may happen overmegapixel image + if ( iw * ih > 1024 * 1024 ) { + canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + ctx = canvas.getContext('2d'); + ctx.drawImage( img, -iw + 1, 0 ); + + // subsampled image becomes half smaller in rendering size. + // check alpha channel value to confirm image is covering + // edge pixel or not. if alpha value is 0 + // image is not covering, hence subsampled. + return ctx.getImageData( 0, 0, 1, 1 ).data[ 3 ] === 0; + } else { + return false; + } + } + + + return function( canvas, img, x, y, width, height ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + ctx = canvas.getContext('2d'), + subsampled = detectSubsampling( img ), + doSquash = this.type === 'image/jpeg', + d = 1024, + sy = 0, + dy = 0, + tmpCanvas, tmpCtx, vertSquashRatio, dw, dh, sx, dx; + + if ( subsampled ) { + iw /= 2; + ih /= 2; + } + + ctx.save(); + tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = tmpCanvas.height = d; + + tmpCtx = tmpCanvas.getContext('2d'); + vertSquashRatio = doSquash ? + detectVerticalSquash( img, iw, ih ) : 1; + + dw = Math.ceil( d * width / iw ); + dh = Math.ceil( d * height / ih / vertSquashRatio ); + + while ( sy < ih ) { + sx = 0; + dx = 0; + while ( sx < iw ) { + tmpCtx.clearRect( 0, 0, d, d ); + tmpCtx.drawImage( img, -sx, -sy ); + ctx.drawImage( tmpCanvas, 0, 0, d, d, + x + dx, y + dy, dw, dh ); + sx += d; + dx += dw; + } + sy += d; + dy += dh; + } + ctx.restore(); + tmpCanvas = tmpCtx = null; + }; + })() + }); + }); + + /** + * 这个方式性能不行,但是可以解决android里面的toDataUrl的bug + * android里面toDataUrl('image/jpege')得到的结果却是png. + * + * 所以这里没辙,只能借助这个工具 + * @fileOverview jpeg encoder + */ + define('runtime/html5/jpegencoder',[], function( require, exports, module ) { + + /* + Copyright (c) 2008, Adobe Systems Incorporated + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of Adobe Systems Incorporated nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + /* + JPEG encoder ported to JavaScript and optimized by Andreas Ritter, www.bytestrom.eu, 11/2009 + + Basic GUI blocking jpeg encoder + */ + + function JPEGEncoder(quality) { + var self = this; + var fround = Math.round; + var ffloor = Math.floor; + var YTable = new Array(64); + var UVTable = new Array(64); + var fdtbl_Y = new Array(64); + var fdtbl_UV = new Array(64); + var YDC_HT; + var UVDC_HT; + var YAC_HT; + var UVAC_HT; + + var bitcode = new Array(65535); + var category = new Array(65535); + var outputfDCTQuant = new Array(64); + var DU = new Array(64); + var byteout = []; + var bytenew = 0; + var bytepos = 7; + + var YDU = new Array(64); + var UDU = new Array(64); + var VDU = new Array(64); + var clt = new Array(256); + var RGB_YUV_TABLE = new Array(2048); + var currentQuality; + + var ZigZag = [ + 0, 1, 5, 6,14,15,27,28, + 2, 4, 7,13,16,26,29,42, + 3, 8,12,17,25,30,41,43, + 9,11,18,24,31,40,44,53, + 10,19,23,32,39,45,52,54, + 20,22,33,38,46,51,55,60, + 21,34,37,47,50,56,59,61, + 35,36,48,49,57,58,62,63 + ]; + + var std_dc_luminance_nrcodes = [0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0]; + var std_dc_luminance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_luminance_nrcodes = [0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d]; + var std_ac_luminance_values = [ + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12, + 0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07, + 0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08, + 0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0, + 0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16, + 0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39, + 0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49, + 0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69, + 0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79, + 0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98, + 0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7, + 0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6, + 0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5, + 0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4, + 0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea, + 0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + var std_dc_chrominance_nrcodes = [0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0]; + var std_dc_chrominance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_chrominance_nrcodes = [0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77]; + var std_ac_chrominance_values = [ + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21, + 0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71, + 0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0, + 0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34, + 0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38, + 0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48, + 0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68, + 0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78, + 0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96, + 0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5, + 0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4, + 0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3, + 0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2, + 0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9, + 0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + function initQuantTables(sf){ + var YQT = [ + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68,109,103, 77, + 24, 35, 55, 64, 81,104,113, 92, + 49, 64, 78, 87,103,121,120,101, + 72, 92, 95, 98,112,100,103, 99 + ]; + + for (var i = 0; i < 64; i++) { + var t = ffloor((YQT[i]*sf+50)/100); + if (t < 1) { + t = 1; + } else if (t > 255) { + t = 255; + } + YTable[ZigZag[i]] = t; + } + var UVQT = [ + 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99 + ]; + for (var j = 0; j < 64; j++) { + var u = ffloor((UVQT[j]*sf+50)/100); + if (u < 1) { + u = 1; + } else if (u > 255) { + u = 255; + } + UVTable[ZigZag[j]] = u; + } + var aasf = [ + 1.0, 1.387039845, 1.306562965, 1.175875602, + 1.0, 0.785694958, 0.541196100, 0.275899379 + ]; + var k = 0; + for (var row = 0; row < 8; row++) + { + for (var col = 0; col < 8; col++) + { + fdtbl_Y[k] = (1.0 / (YTable [ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + fdtbl_UV[k] = (1.0 / (UVTable[ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + k++; + } + } + } + + function computeHuffmanTbl(nrcodes, std_table){ + var codevalue = 0; + var pos_in_table = 0; + var HT = new Array(); + for (var k = 1; k <= 16; k++) { + for (var j = 1; j <= nrcodes[k]; j++) { + HT[std_table[pos_in_table]] = []; + HT[std_table[pos_in_table]][0] = codevalue; + HT[std_table[pos_in_table]][1] = k; + pos_in_table++; + codevalue++; + } + codevalue*=2; + } + return HT; + } + + function initHuffmanTbl() + { + YDC_HT = computeHuffmanTbl(std_dc_luminance_nrcodes,std_dc_luminance_values); + UVDC_HT = computeHuffmanTbl(std_dc_chrominance_nrcodes,std_dc_chrominance_values); + YAC_HT = computeHuffmanTbl(std_ac_luminance_nrcodes,std_ac_luminance_values); + UVAC_HT = computeHuffmanTbl(std_ac_chrominance_nrcodes,std_ac_chrominance_values); + } + + function initCategoryNumber() + { + var nrlower = 1; + var nrupper = 2; + for (var cat = 1; cat <= 15; cat++) { + //Positive numbers + for (var nr = nrlower; nr>0] = 38470 * i; + RGB_YUV_TABLE[(i+ 512)>>0] = 7471 * i + 0x8000; + RGB_YUV_TABLE[(i+ 768)>>0] = -11059 * i; + RGB_YUV_TABLE[(i+1024)>>0] = -21709 * i; + RGB_YUV_TABLE[(i+1280)>>0] = 32768 * i + 0x807FFF; + RGB_YUV_TABLE[(i+1536)>>0] = -27439 * i; + RGB_YUV_TABLE[(i+1792)>>0] = - 5329 * i; + } + } + + // IO functions + function writeBits(bs) + { + var value = bs[0]; + var posval = bs[1]-1; + while ( posval >= 0 ) { + if (value & (1 << posval) ) { + bytenew |= (1 << bytepos); + } + posval--; + bytepos--; + if (bytepos < 0) { + if (bytenew == 0xFF) { + writeByte(0xFF); + writeByte(0); + } + else { + writeByte(bytenew); + } + bytepos=7; + bytenew=0; + } + } + } + + function writeByte(value) + { + byteout.push(clt[value]); // write char directly instead of converting later + } + + function writeWord(value) + { + writeByte((value>>8)&0xFF); + writeByte((value )&0xFF); + } + + // DCT & quantization core + function fDCTQuant(data, fdtbl) + { + var d0, d1, d2, d3, d4, d5, d6, d7; + /* Pass 1: process rows. */ + var dataOff=0; + var i; + var I8 = 8; + var I64 = 64; + for (i=0; i 0.0) ? ((fDCTQuant + 0.5)|0) : ((fDCTQuant - 0.5)|0); + //outputfDCTQuant[i] = fround(fDCTQuant); + + } + return outputfDCTQuant; + } + + function writeAPP0() + { + writeWord(0xFFE0); // marker + writeWord(16); // length + writeByte(0x4A); // J + writeByte(0x46); // F + writeByte(0x49); // I + writeByte(0x46); // F + writeByte(0); // = "JFIF",'\0' + writeByte(1); // versionhi + writeByte(1); // versionlo + writeByte(0); // xyunits + writeWord(1); // xdensity + writeWord(1); // ydensity + writeByte(0); // thumbnwidth + writeByte(0); // thumbnheight + } + + function writeSOF0(width, height) + { + writeWord(0xFFC0); // marker + writeWord(17); // length, truecolor YUV JPG + writeByte(8); // precision + writeWord(height); + writeWord(width); + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0x11); // HVY + writeByte(0); // QTY + writeByte(2); // IdU + writeByte(0x11); // HVU + writeByte(1); // QTU + writeByte(3); // IdV + writeByte(0x11); // HVV + writeByte(1); // QTV + } + + function writeDQT() + { + writeWord(0xFFDB); // marker + writeWord(132); // length + writeByte(0); + for (var i=0; i<64; i++) { + writeByte(YTable[i]); + } + writeByte(1); + for (var j=0; j<64; j++) { + writeByte(UVTable[j]); + } + } + + function writeDHT() + { + writeWord(0xFFC4); // marker + writeWord(0x01A2); // length + + writeByte(0); // HTYDCinfo + for (var i=0; i<16; i++) { + writeByte(std_dc_luminance_nrcodes[i+1]); + } + for (var j=0; j<=11; j++) { + writeByte(std_dc_luminance_values[j]); + } + + writeByte(0x10); // HTYACinfo + for (var k=0; k<16; k++) { + writeByte(std_ac_luminance_nrcodes[k+1]); + } + for (var l=0; l<=161; l++) { + writeByte(std_ac_luminance_values[l]); + } + + writeByte(1); // HTUDCinfo + for (var m=0; m<16; m++) { + writeByte(std_dc_chrominance_nrcodes[m+1]); + } + for (var n=0; n<=11; n++) { + writeByte(std_dc_chrominance_values[n]); + } + + writeByte(0x11); // HTUACinfo + for (var o=0; o<16; o++) { + writeByte(std_ac_chrominance_nrcodes[o+1]); + } + for (var p=0; p<=161; p++) { + writeByte(std_ac_chrominance_values[p]); + } + } + + function writeSOS() + { + writeWord(0xFFDA); // marker + writeWord(12); // length + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0); // HTY + writeByte(2); // IdU + writeByte(0x11); // HTU + writeByte(3); // IdV + writeByte(0x11); // HTV + writeByte(0); // Ss + writeByte(0x3f); // Se + writeByte(0); // Bf + } + + function processDU(CDU, fdtbl, DC, HTDC, HTAC){ + var EOB = HTAC[0x00]; + var M16zeroes = HTAC[0xF0]; + var pos; + var I16 = 16; + var I63 = 63; + var I64 = 64; + var DU_DCT = fDCTQuant(CDU, fdtbl); + //ZigZag reorder + for (var j=0;j0)&&(DU[end0pos]==0); end0pos--) {}; + //end0pos = first element in reverse order !=0 + if ( end0pos == 0) { + writeBits(EOB); + return DC; + } + var i = 1; + var lng; + while ( i <= end0pos ) { + var startpos = i; + for (; (DU[i]==0) && (i<=end0pos); ++i) {} + var nrzeroes = i-startpos; + if ( nrzeroes >= I16 ) { + lng = nrzeroes>>4; + for (var nrmarker=1; nrmarker <= lng; ++nrmarker) + writeBits(M16zeroes); + nrzeroes = nrzeroes&0xF; + } + pos = 32767+DU[i]; + writeBits(HTAC[(nrzeroes<<4)+category[pos]]); + writeBits(bitcode[pos]); + i++; + } + if ( end0pos != I63 ) { + writeBits(EOB); + } + return DC; + } + + function initCharLookupTable(){ + var sfcc = String.fromCharCode; + for(var i=0; i < 256; i++){ ///// ACHTUNG // 255 + clt[i] = sfcc(i); + } + } + + this.encode = function(image,quality) // image data object + { + // var time_start = new Date().getTime(); + + if(quality) setQuality(quality); + + // Initialize bit writer + byteout = new Array(); + bytenew=0; + bytepos=7; + + // Add JPEG headers + writeWord(0xFFD8); // SOI + writeAPP0(); + writeDQT(); + writeSOF0(image.width,image.height); + writeDHT(); + writeSOS(); + + + // Encode 8x8 macroblocks + var DCY=0; + var DCU=0; + var DCV=0; + + bytenew=0; + bytepos=7; + + + this.encode.displayName = "_encode_"; + + var imageData = image.data; + var width = image.width; + var height = image.height; + + var quadWidth = width*4; + var tripleWidth = width*3; + + var x, y = 0; + var r, g, b; + var start,p, col,row,pos; + while(y < height){ + x = 0; + while(x < quadWidth){ + start = quadWidth * y + x; + p = start; + col = -1; + row = 0; + + for(pos=0; pos < 64; pos++){ + row = pos >> 3;// /8 + col = ( pos & 7 ) * 4; // %8 + p = start + ( row * quadWidth ) + col; + + if(y+row >= height){ // padding bottom + p-= (quadWidth*(y+1+row-height)); + } + + if(x+col >= quadWidth){ // padding right + p-= ((x+col) - quadWidth +4) + } + + r = imageData[ p++ ]; + g = imageData[ p++ ]; + b = imageData[ p++ ]; + + + /* // calculate YUV values dynamically + YDU[pos]=((( 0.29900)*r+( 0.58700)*g+( 0.11400)*b))-128; //-0x80 + UDU[pos]=(((-0.16874)*r+(-0.33126)*g+( 0.50000)*b)); + VDU[pos]=((( 0.50000)*r+(-0.41869)*g+(-0.08131)*b)); + */ + + // use lookup table (slightly faster) + YDU[pos] = ((RGB_YUV_TABLE[r] + RGB_YUV_TABLE[(g + 256)>>0] + RGB_YUV_TABLE[(b + 512)>>0]) >> 16)-128; + UDU[pos] = ((RGB_YUV_TABLE[(r + 768)>>0] + RGB_YUV_TABLE[(g + 1024)>>0] + RGB_YUV_TABLE[(b + 1280)>>0]) >> 16)-128; + VDU[pos] = ((RGB_YUV_TABLE[(r + 1280)>>0] + RGB_YUV_TABLE[(g + 1536)>>0] + RGB_YUV_TABLE[(b + 1792)>>0]) >> 16)-128; + + } + + DCY = processDU(YDU, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCU = processDU(UDU, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = processDU(VDU, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + x+=32; + } + y+=8; + } + + + //////////////////////////////////////////////////////////////// + + // Do the bit alignment of the EOI marker + if ( bytepos >= 0 ) { + var fillbits = []; + fillbits[1] = bytepos+1; + fillbits[0] = (1<<(bytepos+1))-1; + writeBits(fillbits); + } + + writeWord(0xFFD9); //EOI + + var jpegDataUri = 'data:image/jpeg;base64,' + btoa(byteout.join('')); + + byteout = []; + + // benchmarking + // var duration = new Date().getTime() - time_start; + // console.log('Encoding time: '+ currentQuality + 'ms'); + // + + return jpegDataUri + } + + function setQuality(quality){ + if (quality <= 0) { + quality = 1; + } + if (quality > 100) { + quality = 100; + } + + if(currentQuality == quality) return // don't recalc if unchanged + + var sf = 0; + if (quality < 50) { + sf = Math.floor(5000 / quality); + } else { + sf = Math.floor(200 - quality*2); + } + + initQuantTables(sf); + currentQuality = quality; + // console.log('Quality set to: '+quality +'%'); + } + + function init(){ + // var time_start = new Date().getTime(); + if(!quality) quality = 50; + // Create tables + initCharLookupTable() + initHuffmanTbl(); + initCategoryNumber(); + initRGBYUVTable(); + + setQuality(quality); + // var duration = new Date().getTime() - time_start; + // console.log('Initialization '+ duration + 'ms'); + } + + init(); + + }; + + JPEGEncoder.encode = function( data, quality ) { + var encoder = new JPEGEncoder( quality ); + + return encoder.encode( data ); + } + + return JPEGEncoder; + }); + /** + * @fileOverview Fix android canvas.toDataUrl bug. + */ + define('runtime/html5/androidpatch',[ + 'runtime/html5/util', + 'runtime/html5/jpegencoder', + 'base' + ], function( Util, encoder, Base ) { + var origin = Util.canvasToDataUrl, + supportJpeg; + + Util.canvasToDataUrl = function( canvas, type, quality ) { + var ctx, w, h, fragement, parts; + + // 非android手机直接跳过。 + if ( !Base.os.android ) { + return origin.apply( null, arguments ); + } + + // 检测是否canvas支持jpeg导出,根据数据格式来判断。 + // JPEG 前两位分别是:255, 216 + if ( type === 'image/jpeg' && typeof supportJpeg === 'undefined' ) { + fragement = origin.apply( null, arguments ); + + parts = fragement.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + fragement = atob( parts[ 1 ] ); + } else { + fragement = decodeURIComponent( parts[ 1 ] ); + } + + fragement = fragement.substring( 0, 2 ); + + supportJpeg = fragement.charCodeAt( 0 ) === 255 && + fragement.charCodeAt( 1 ) === 216; + } + + // 只有在android环境下才修复 + if ( type === 'image/jpeg' && !supportJpeg ) { + w = canvas.width; + h = canvas.height; + ctx = canvas.getContext('2d'); + + return encoder.encode( ctx.getImageData( 0, 0, w, h ), quality ); + } + + return origin.apply( null, arguments ); + }; + }); + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + define('webuploader',[ + 'base', + 'widgets/filepicker', + 'widgets/image', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/log', + 'runtime/html5/blob', + 'runtime/html5/filepicker', + 'runtime/html5/imagemeta/exif', + 'runtime/html5/image', + 'runtime/html5/androidpatch', + 'runtime/html5/transport' + ], function( Base ) { + return Base; + }); + return require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.custom.min.js b/public/static/libs/webuploader/webuploader.custom.min.js new file mode 100644 index 0000000..559bd8d --- /dev/null +++ b/public/static/libs/webuploader/webuploader.custom.min.js @@ -0,0 +1,2 @@ +/* WebUploader 0.1.6 */!function(a,b){var c,d={},e=function(a,b){var c,d,e;if("string"==typeof a)return h(a);for(c=[],d=a.length,e=0;d>e;e++)c.push(h(a[e]));return b.apply(null,c)},f=function(a,b,c){2===arguments.length&&(c=b,b=null),e(b||[],function(){g(a,c,arguments)})},g=function(a,b,c){var f,g={exports:b};"function"==typeof b&&(c.length||(c=[e,g.exports,g]),f=b.apply(null,c),void 0!==f&&(g.exports=f)),d[a]=g.exports},h=function(b){var c=d[b]||a[b];if(!c)throw new Error("`"+b+"` is undefined");return c},i=function(a){var b,c,e,f,g,h;h=function(a){return a&&a.charAt(0).toUpperCase()+a.substr(1)};for(b in d)if(c=a,d.hasOwnProperty(b)){for(e=b.split("/"),g=h(e.pop());f=h(e.shift());)c[f]=c[f]||{},c=c[f];c[g]=d[b]}return a},j=function(c){return a.__dollar=c,i(b(a,f,e))};"object"==typeof module&&"object"==typeof module.exports?module.exports=j():"function"==typeof define&&define.amd?define(["jquery"],j):(c=a.WebUploader,a.WebUploader=j(),a.WebUploader.noConflict=function(){a.WebUploader=c})}(window,function(a,b,c){return b("dollar-third",[],function(){var b=a.require,c=a.__dollar||a.jQuery||a.Zepto||b("jquery")||b("zepto");if(!c)throw new Error("jQuery or Zepto not found!");return c}),b("dollar",["dollar-third"],function(a){return a}),b("promise-builtin",["dollar"],function(a){function b(b){var c,d,e,f,g,h,i=[],j=!b&&[],k=function(a){for(h=a,c=!0,g=e||0,e=0,f=i.length,d=!0;i&&f>g;g++)i[g].apply(a[0],a[1]);d=!1,i&&(j?j.length&&k(j.shift()):i=[])},l={add:function(){if(i){var b=i.length;!function c(b){a.each(b,function(b,d){var e=a.type(d);"function"===e?i.push(d):d&&d.length&&"string"!==e&&c(d)})}(arguments),d?f=i.length:h&&(e=b,k(h))}return this},disable:function(){return i=j=h=void 0,this},lock:function(){return j=void 0,h||l.disable(),this},fireWith:function(a,b){return!i||c&&!j||(b=b||[],b=[a,b.slice?b.slice():b],d?j.push(b):k(b)),this},fire:function(){return l.fireWith(this,arguments),this}};return l}function c(d){var e=[["resolve","done",b(!0),"resolved"],["reject","fail",b(!0),"rejected"],["notify","progress",b()]],f="pending",g={state:function(){return f},always:function(){return h.done(arguments).fail(arguments),this},then:function(){var b=arguments;return c(function(c){a.each(e,function(d,e){var f=e[0],i=a.isFunction(b[d])&&b[d];h[e[1]](function(){var b;b=i&&i.apply(this,arguments),b&&a.isFunction(b.promise)?b.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f+"With"](this===g?c.promise():this,i?[b]:arguments)})}),b=null}).promise()},promise:function(b){return null!=b?a.extend(b,g):g}},h={};return g.pipe=g.then,a.each(e,function(a,b){var c=b[2],d=b[3];g[b[1]]=c.add,d&&c.add(function(){f=d},e[1^a][2].disable,e[2][2].lock),h[b[0]]=function(){return h[b[0]+"With"](this===h?g:this,arguments),this},h[b[0]+"With"]=c.fireWith}),g.promise(h),d&&d.call(h,h),h}var d;return d={Deferred:c,isPromise:function(a){return a&&"function"==typeof a.then},when:function(b){var d,e,f,g=0,h=[].slice,i=h.call(arguments),j=i.length,k=1!==j||b&&a.isFunction(b.promise)?j:0,l=1===k?b:c(),m=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?h.call(arguments):e,c===d?l.notifyWith(b,c):--k||l.resolveWith(b,c)}};if(j>1)for(d=new Array(j),e=new Array(j),f=new Array(j);j>g;g++)i[g]&&a.isFunction(i[g].promise)?i[g].promise().done(m(g,f,i)).fail(l.reject).progress(m(g,e,d)):--k;return k||l.resolveWith(f,i),l.promise()}}}),b("promise",["promise-builtin"],function(a){return a}),b("base",["dollar","promise"],function(b,c){function d(a){return function(){return h.apply(a,arguments)}}function e(a,b){return function(){return a.apply(b,arguments)}}function f(a){var b;return Object.create?Object.create(a):(b=function(){},b.prototype=a,new b)}var g=function(){},h=Function.call;return{version:"0.1.6",$:b,Deferred:c.Deferred,isPromise:c.isPromise,when:c.when,browser:function(a){var b={},c=a.match(/WebKit\/([\d.]+)/),d=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),e=a.match(/MSIE\s([\d\.]+)/)||a.match(/(?:trident)(?:.*rv:([\w.]+))?/i),f=a.match(/Firefox\/([\d.]+)/),g=a.match(/Safari\/([\d.]+)/),h=a.match(/OPR\/([\d.]+)/);return c&&(b.webkit=parseFloat(c[1])),d&&(b.chrome=parseFloat(d[1])),e&&(b.ie=parseFloat(e[1])),f&&(b.firefox=parseFloat(f[1])),g&&(b.safari=parseFloat(g[1])),h&&(b.opera=parseFloat(h[1])),b}(navigator.userAgent),os:function(a){var b={},c=a.match(/(?:Android);?[\s\/]+([\d.]+)?/),d=a.match(/(?:iPad|iPod|iPhone).*OS\s([\d_]+)/);return c&&(b.android=parseFloat(c[1])),d&&(b.ios=parseFloat(d[1].replace(/_/g,"."))),b}(navigator.userAgent),inherits:function(a,c,d){var e;return"function"==typeof c?(e=c,c=null):e=c&&c.hasOwnProperty("constructor")?c.constructor:function(){return a.apply(this,arguments)},b.extend(!0,e,a,d||{}),e.__super__=a.prototype,e.prototype=f(a.prototype),c&&b.extend(!0,e.prototype,c),e},noop:g,bindFn:e,log:function(){return a.console?e(console.log,console):g}(),nextTick:function(){return function(a){setTimeout(a,1)}}(),slice:d([].slice),guid:function(){var a=0;return function(b){for(var c=(+new Date).toString(32),d=0;5>d;d++)c+=Math.floor(65535*Math.random()).toString(32);return(b||"wu_")+c+(a++).toString(32)}}(),formatSize:function(a,b,c){var d;for(c=c||["B","K","M","G","TB"];(d=c.shift())&&a>1024;)a/=1024;return("B"===d?a:a.toFixed(b||2))+d}}}),b("mediator",["base"],function(a){function b(a,b,c,d){return f.grep(a,function(a){return!(!a||b&&a.e!==b||c&&a.cb!==c&&a.cb._cb!==c||d&&a.ctx!==d)})}function c(a,b,c){f.each((a||"").split(h),function(a,d){c(d,b)})}function d(a,b){for(var c,d=!1,e=-1,f=a.length;++e1?(d.isPlainObject(b)&&d.isPlainObject(c[a])?d.extend(c[a],b):c[a]=b,void 0):a?c[a]:c},getStats:function(){var a=this.request("get-stats");return a?{successNum:a.numOfSuccess,progressNum:a.numOfProgress,cancelNum:a.numOfCancel,invalidNum:a.numOfInvalid,uploadFailNum:a.numOfUploadFailed,queueNum:a.numOfQueue,interruptNum:a.numofInterrupt}:{}},trigger:function(a){var c=[].slice.call(arguments,1),e=this.options,f="on"+a.substring(0,1).toUpperCase()+a.substring(1);return b.trigger.apply(this,arguments)===!1||d.isFunction(e[f])&&e[f].apply(this,c)===!1||d.isFunction(this[f])&&this[f].apply(this,c)===!1||b.trigger.apply(b,[this,a].concat(c))===!1?!1:!0},destroy:function(){this.request("destroy",arguments),this.off()},request:a.noop}),a.create=c.create=function(a){return new c(a)},a.Uploader=c,c}),b("runtime/runtime",["base","mediator"],function(a,b){function c(b){this.options=d.extend({container:document.body},b),this.uid=a.guid("rt_")}var d=a.$,e={},f=function(a){for(var b in a)if(a.hasOwnProperty(b))return b;return null};return d.extend(c.prototype,{getContainer:function(){var a,b,c=this.options;return this._container?this._container:(a=d(c.container||document.body),b=d(document.createElement("div")),b.attr("id","rt_"+this.uid),b.css({position:"absolute",top:"0px",left:"0px",width:"1px",height:"1px",overflow:"hidden"}),a.append(b),a.addClass("webuploader-container"),this._container=b,this._parent=a,b)},init:a.noop,exec:a.noop,destroy:function(){this._container&&this._container.remove(),this._parent&&this._parent.removeClass("webuploader-container"),this.off()}}),c.orders="html5,flash",c.addRuntime=function(a,b){e[a]=b},c.hasRuntime=function(a){return!!(a?e[a]:f(e))},c.create=function(a,b){var g,h;if(b=b||c.orders,d.each(b.split(/\s*,\s*/g),function(){return e[this]?(g=this,!1):void 0}),g=g||f(e),!g)throw new Error("Runtime Error");return h=new e[g](a)},b.installTo(c.prototype),c}),b("runtime/client",["base","mediator","runtime/runtime"],function(a,b,c){function d(b,d){var f,g=a.Deferred();this.uid=a.guid("client_"),this.runtimeReady=function(a){return g.done(a)},this.connectRuntime=function(b,h){if(f)throw new Error("already connected!");return g.done(h),"string"==typeof b&&e.get(b)&&(f=e.get(b)),f=f||e.get(null,d),f?(a.$.extend(f.options,b),f.__promise.then(g.resolve),f.__client++):(f=c.create(b,b.runtimeOrder),f.__promise=g.promise(),f.once("ready",g.resolve),f.init(),e.add(f),f.__client=1),d&&(f.__standalone=d),f},this.getRuntime=function(){return f},this.disconnectRuntime=function(){f&&(f.__client--,f.__client<=0&&(e.remove(f),delete f.__promise,f.destroy()),f=null)},this.exec=function(){if(f){var c=a.slice(arguments);return b&&c.unshift(b),f.exec.apply(this,c)}},this.getRuid=function(){return f&&f.uid},this.destroy=function(a){return function(){a&&a.apply(this,arguments),this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()}}(this.destroy)}var e;return e=function(){var a={};return{add:function(b){a[b.uid]=b},get:function(b,c){var d;if(b)return a[b];for(d in a)if(!c||!a[d].__standalone)return a[d];return null},remove:function(b){delete a[b.uid]}}}(),b.installTo(d.prototype),d}),b("lib/blob",["base","runtime/client"],function(a,b){function c(a,c){var d=this;d.source=c,d.ruid=a,this.size=c.size||0,this.type=!c.type&&this.ext&&~"jpg,jpeg,png,gif,bmp".indexOf(this.ext)?"image/"+("jpg"===this.ext?"jpeg":this.ext):c.type||"application/octet-stream",b.call(d,"Blob"),this.uid=c.uid||this.uid,a&&d.connectRuntime(a)}return a.inherits(b,{constructor:c,slice:function(a,b){return this.exec("slice",a,b)},getSource:function(){return this.source}}),c}),b("lib/file",["base","lib/blob"],function(a,b){function c(a,c){var f;this.name=c.name||"untitled"+d++,f=e.exec(c.name)?RegExp.$1.toLowerCase():"",!f&&c.type&&(f=/\/(jpg|jpeg|png|gif|bmp)$/i.exec(c.type)?RegExp.$1.toLowerCase():"",this.name+="."+f),this.ext=f,this.lastModifiedDate=c.lastModifiedDate||(new Date).toLocaleString(),b.apply(this,arguments)}var d=1,e=/\.([^.]+)$/;return a.inherits(b,c)}),b("lib/filepicker",["base","runtime/client","lib/file"],function(b,c,d){function e(a){if(a=this.options=f.extend({},e.options,a),a.container=f(a.id),!a.container.length)throw new Error("按钮指定错误");a.innerHTML=a.innerHTML||a.label||a.container.html()||"",a.button=f(a.button||document.createElement("div")),a.button.html(a.innerHTML),a.container.html(a.button),c.call(this,"FilePicker",!0)}var f=b.$;return e.options={button:null,container:null,label:null,innerHTML:null,multiple:!0,accept:null,name:"file",style:"webuploader-pick"},b.inherits(c,{constructor:e,init:function(){var c=this,e=c.options,g=e.button,h=e.style;h&&g.addClass("webuploader-pick"),c.on("all",function(a){var b;switch(a){case"mouseenter":h&&g.addClass("webuploader-pick-hover");break;case"mouseleave":h&&g.removeClass("webuploader-pick-hover");break;case"change":b=c.exec("getFiles"),c.trigger("select",f.map(b,function(a){return a=new d(c.getRuid(),a),a._refer=e.container,a}),e.container)}}),c.connectRuntime(e,function(){c.refresh(),c.exec("init",e),c.trigger("ready")}),this._resizeHandler=b.bindFn(this.refresh,this),f(a).on("resize",this._resizeHandler)},refresh:function(){var a=this.getRuntime().getContainer(),b=this.options.button,c=b.outerWidth?b.outerWidth():b.width(),d=b.outerHeight?b.outerHeight():b.height(),e=b.offset();c&&d&&a.css({bottom:"auto",right:"auto",width:c+"px",height:d+"px"}).offset(e)},enable:function(){var a=this.options.button;a.removeClass("webuploader-pick-disable"),this.refresh()},disable:function(){var a=this.options.button;this.getRuntime().getContainer().css({top:"-99999px"}),a.addClass("webuploader-pick-disable")},destroy:function(){var b=this.options.button;f(a).off("resize",this._resizeHandler),b.removeClass("webuploader-pick-disable webuploader-pick-hover webuploader-pick")}}),e}),b("widgets/widget",["base","uploader"],function(a,b){function c(a){if(!a)return!1;var b=a.length,c=e.type(a);return 1===a.nodeType&&b?!0:"array"===c||"function"!==c&&"string"!==c&&(0===b||"number"==typeof b&&b>0&&b-1 in a)}function d(a){this.owner=a,this.options=a.options}var e=a.$,f=b.prototype._init,g=b.prototype.destroy,h={},i=[];return e.extend(d.prototype,{init:a.noop,invoke:function(a,b){var c=this.responseMap;return c&&a in c&&c[a]in this&&e.isFunction(this[c[a]])?this[c[a]].apply(this,b):h},request:function(){return this.owner.request.apply(this.owner,arguments)}}),e.extend(b.prototype,{_init:function(){var a=this,b=a._widgets=[],c=a.options.disableWidgets||"";return e.each(i,function(d,e){(!c||!~c.indexOf(e._name))&&b.push(new e(a))}),f.apply(a,arguments)},request:function(b,d,e){var f,g,i,j,k=0,l=this._widgets,m=l&&l.length,n=[],o=[];for(d=c(d)?d:[d];m>k;k++)f=l[k],g=f.invoke(b,d),g!==h&&(a.isPromise(g)?o.push(g):n.push(g));return e||o.length?(i=a.when.apply(a,o),j=i.pipe?"pipe":"then",i[j](function(){var b=a.Deferred(),c=arguments;return 1===c.length&&(c=c[0]),setTimeout(function(){b.resolve(c)},1),b.promise()})[e?j:"done"](e||a.noop)):n[0]},destroy:function(){g.apply(this,arguments),this._widgets=null}}),b.register=d.register=function(b,c){var f,g={init:"init",destroy:"destroy",name:"anonymous"};return 1===arguments.length?(c=b,e.each(c,function(a){return"_"===a[0]||"name"===a?("name"===a&&(g.name=c.name),void 0):(g[a.replace(/[A-Z]/g,"-$&").toLowerCase()]=a,void 0)})):g=e.extend(g,b),c.responseMap=g,f=a.inherits(d,c),f._name=g.name,i.push(f),f},b.unRegister=d.unRegister=function(a){if(a&&"anonymous"!==a)for(var b=i.length;b--;)i[b]._name===a&&i.splice(b,1)},d}),b("widgets/filepicker",["base","uploader","lib/filepicker","widgets/widget"],function(a,b,c){var d=a.$;return d.extend(b.options,{pick:null,accept:null}),b.register({name:"picker",init:function(a){return this.pickers=[],a.pick&&this.addBtn(a.pick)},refresh:function(){d.each(this.pickers,function(){this.refresh()})},addBtn:function(b){var e=this,f=e.options,g=f.accept,h=[];if(b)return d.isPlainObject(b)||(b={id:b}),d(b.id).each(function(){var i,j,k;k=a.Deferred(),i=d.extend({},b,{accept:d.isPlainObject(g)?[g]:g,swf:f.swf,runtimeOrder:f.runtimeOrder,id:this}),j=new c(i),j.once("ready",k.resolve),j.on("select",function(a){e.owner.request("add-file",[a])}),j.on("dialogopen",function(){e.owner.trigger("dialogOpen",j.button)}),j.init(),e.pickers.push(j),h.push(k.promise())}),a.when.apply(a,h)},disable:function(){d.each(this.pickers,function(){this.disable()})},enable:function(){d.each(this.pickers,function(){this.enable()})},destroy:function(){d.each(this.pickers,function(){this.destroy()}),this.pickers=null}})}),b("lib/image",["base","runtime/client","lib/blob"],function(a,b,c){function d(a){this.options=e.extend({},d.options,a),b.call(this,"Image"),this.on("load",function(){this._info=this.exec("info"),this._meta=this.exec("meta")})}var e=a.$;return d.options={quality:90,crop:!1,preserveHeaders:!1,allowMagnify:!1},a.inherits(b,{constructor:d,info:function(a){return a?(this._info=a,this):this._info},meta:function(a){return a?(this._meta=a,this):this._meta},loadFromBlob:function(a){var b=this,c=a.getRuid();this.connectRuntime(c,function(){b.exec("init",b.options),b.exec("loadFromBlob",a)})},resize:function(){var b=a.slice(arguments);return this.exec.apply(this,["resize"].concat(b))},crop:function(){var b=a.slice(arguments);return this.exec.apply(this,["crop"].concat(b))},getAsDataUrl:function(a){return this.exec("getAsDataUrl",a)},getAsBlob:function(a){var b=this.exec("getAsBlob",a);return new c(this.getRuid(),b)}}),d}),b("widgets/image",["base","uploader","lib/image","widgets/widget"],function(a,b,c){var d,e=a.$;return d=function(a){var b=0,c=[],d=function(){for(var d;c.length&&a>b;)d=c.shift(),b+=d[0],d[1]()};return function(a,e,f){c.push([e,f]),a.once("destroy",function(){b-=e,setTimeout(d,1)}),setTimeout(d,1)}}(5242880),e.extend(b.options,{thumb:{width:110,height:110,quality:70,allowMagnify:!0,crop:!0,preserveHeaders:!1,type:"image/jpeg"},compress:{width:1600,height:1600,quality:90,allowMagnify:!1,crop:!1,preserveHeaders:!0}}),b.register({name:"image",makeThumb:function(a,b,f,g){var h,i;return a=this.request("get-file",a),a.type.match(/^image/)?(h=e.extend({},this.options.thumb),e.isPlainObject(f)&&(h=e.extend(h,f),f=null),f=f||h.width,g=g||h.height,i=new c(h),i.once("load",function(){a._info=a._info||i.info(),a._meta=a._meta||i.meta(),1>=f&&f>0&&(f=a._info.width*f),1>=g&&g>0&&(g=a._info.height*g),i.resize(f,g)}),i.once("complete",function(){b(!1,i.getAsDataUrl(h.type)),i.destroy()}),i.once("error",function(a){b(a||!0),i.destroy()}),d(i,a.source.size,function(){a._info&&i.info(a._info),a._meta&&i.meta(a._meta),i.loadFromBlob(a.source)}),void 0):(b(!0),void 0)},beforeSendFile:function(b){var d,f,g=this.options.compress||this.options.resize,h=g&&g.compressSize||0,i=g&&g.noCompressIfLarger||!1;return b=this.request("get-file",b),!g||!~"image/jpeg,image/jpg".indexOf(b.type)||b.size=a&&a>0&&(a=b._info.width*a),1>=c&&c>0&&(c=b._info.height*c),d.resize(a,c)}),d.once("complete",function(){var a,c;try{a=d.getAsBlob(g.type),c=b.size,(!i||a.sizeb;b++)if(c=this._queue[b],a===c.getStatus())return c;return null},sort:function(a){"function"==typeof a&&this._queue.sort(a)},getFiles:function(){for(var a,b=[].slice.call(arguments,0),c=[],d=0,f=this._queue.length;f>d;d++)a=this._queue[d],(!b.length||~e.inArray(a.getStatus(),b))&&c.push(a);return c},removeFile:function(a){var b=this._map[a.id];b&&(delete this._map[a.id],a.destroy(),this.stats.numofDeleted++)},_fileAdded:function(a){var b=this,c=this._map[a.id];c||(this._map[a.id]=a,a.on("statuschange",function(a,c){b._onFileStatusChange(a,c)}))},_onFileStatusChange:function(a,b){var c=this.stats;switch(b){case f.PROGRESS:c.numOfProgress--;break;case f.QUEUED:c.numOfQueue--;break;case f.ERROR:c.numOfUploadFailed--;break;case f.INVALID:c.numOfInvalid--;break;case f.INTERRUPT:c.numofInterrupt--}switch(a){case f.QUEUED:c.numOfQueue++;break;case f.PROGRESS:c.numOfProgress++;break;case f.ERROR:c.numOfUploadFailed++;break;case f.COMPLETE:c.numOfSuccess++;break;case f.CANCELLED:c.numOfCancel++;break;case f.INVALID:c.numOfInvalid++;break;case f.INTERRUPT:c.numofInterrupt++}}}),b.installTo(d.prototype),d}),b("widgets/queue",["base","uploader","queue","file","lib/file","runtime/client","widgets/widget"],function(a,b,c,d,e,f){var g=a.$,h=/\.\w+$/,i=d.Status;return b.register({name:"queue",init:function(b){var d,e,h,i,j,k,l,m=this;if(g.isPlainObject(b.accept)&&(b.accept=[b.accept]),b.accept){for(j=[],h=0,e=b.accept.length;e>h;h++)i=b.accept[h].extensions,i&&j.push(i);j.length&&(k="\\."+j.join(",").replace(/,/g,"$|\\.").replace(/\*/g,".*")+"$"),m.accept=new RegExp(k,"i")}return m.queue=new c,m.stats=m.queue.stats,"html5"===this.request("predict-runtime-type")?(d=a.Deferred(),this.placeholder=l=new f("Placeholder"),l.connectRuntime({runtimeOrder:"html5"},function(){m._ruid=l.getRuid(),d.resolve()}),d.promise()):void 0},_wrapFile:function(a){if(!(a instanceof d)){if(!(a instanceof e)){if(!this._ruid)throw new Error("Can't add external files.");a=new e(this._ruid,a)}a=new d(a)}return a},acceptFile:function(a){var b=!a||!a.size||this.accept&&h.exec(a.name)&&!this.accept.test(a.name);return!b},_addFile:function(a){var b=this;return a=b._wrapFile(a),b.owner.trigger("beforeFileQueued",a)?b.acceptFile(a)?(b.queue.append(a),b.owner.trigger("fileQueued",a),a):(b.owner.trigger("error","Q_TYPE_DENIED",a),void 0):void 0},getFile:function(a){return this.queue.getFile(a)},addFile:function(a){var b=this;a.length||(a=[a]),a=g.map(a,function(a){return b._addFile(a)}),a.length&&(b.owner.trigger("filesQueued",a),b.options.auto&&setTimeout(function(){b.request("start-upload")},20))},getStats:function(){return this.stats},removeFile:function(a,b){var c=this;a=a.id?a:c.queue.getFile(a),this.request("cancel-file",a),b&&this.queue.removeFile(a)},getFiles:function(){return this.queue.getFiles.apply(this.queue,arguments)},fetchFile:function(){return this.queue.fetch.apply(this.queue,arguments)},retry:function(a,b){var c,d,e,f=this;if(a)return a=a.id?a:f.queue.getFile(a),a.setStatus(i.QUEUED),b||f.request("start-upload"),void 0;for(c=f.queue.getFiles(i.ERROR),d=0,e=c.length;e>d;d++)a=c[d],a.setStatus(i.QUEUED);f.request("start-upload")},sortFiles:function(){return this.queue.sort.apply(this.queue,arguments)},reset:function(){this.owner.trigger("reset"),this.queue=new c,this.stats=this.queue.stats},destroy:function(){this.reset(),this.placeholder&&this.placeholder.destroy()}})}),b("widgets/runtime",["uploader","runtime/runtime","widgets/widget"],function(a,b){return a.support=function(){return b.hasRuntime.apply(b,arguments)},a.register({name:"runtime",init:function(){if(!this.predictRuntimeType())throw Error("Runtime Error")},predictRuntimeType:function(){var a,c,d=this.options.runtimeOrder||b.orders,e=this.type;if(!e)for(d=d.split(/\s*,\s*/g),a=0,c=d.length;c>a;a++)if(b.hasRuntime(d[a])){this.type=e=d[a];break}return e}})}),b("lib/transport",["base","runtime/client","mediator"],function(a,b,c){function d(a){var c=this;a=c.options=e.extend(!0,{},d.options,a||{}),b.call(this,"Transport"),this._blob=null,this._formData=a.formData||{},this._headers=a.headers||{},this.on("progress",this._timeout),this.on("load error",function(){c.trigger("progress",1),clearTimeout(c._timer)})}var e=a.$;return d.options={server:"",method:"POST",withCredentials:!1,fileVal:"file",timeout:12e4,formData:{},headers:{},sendAsBinary:!1},e.extend(d.prototype,{appendBlob:function(a,b,c){var d=this,e=d.options;d.getRuid()&&d.disconnectRuntime(),d.connectRuntime(b.ruid,function(){d.exec("init")}),d._blob=b,e.fileVal=a||e.fileVal,e.filename=c||e.filename},append:function(a,b){"object"==typeof a?e.extend(this._formData,a):this._formData[a]=b},setRequestHeader:function(a,b){"object"==typeof a?e.extend(this._headers,a):this._headers[a]=b},send:function(a){this.exec("send",a),this._timeout()},abort:function(){return clearTimeout(this._timer),this.exec("abort")},destroy:function(){this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()},getResponse:function(){return this.exec("getResponse")},getResponseAsJson:function(){return this.exec("getResponseAsJson")},getStatus:function(){return this.exec("getStatus")},_timeout:function(){var a=this,b=a.options.timeout;b&&(clearTimeout(a._timer),a._timer=setTimeout(function(){a.abort(),a.trigger("error","timeout")},b))}}),c.installTo(d.prototype),d}),b("widgets/upload",["base","uploader","file","lib/transport","widgets/widget"],function(a,b,c,d){function e(a,b){var c,d,e=[],f=a.source,g=f.size,h=b?Math.ceil(g/b):1,i=0,j=0;for(d={file:a,has:function(){return!!e.length},shift:function(){return e.shift()},unshift:function(a){e.unshift(a)}};h>j;)c=Math.min(b,g-i),e.push({file:a,start:i,end:b?i+c:g,total:g,chunks:h,chunk:j++,cuted:d}),i+=c;return a.blocks=e.concat(),a.remaning=e.length,d}var f=a.$,g=a.isPromise,h=c.Status;f.extend(b.options,{prepareNextFile:!1,chunked:!1,chunkSize:5242880,chunkRetry:2,threads:3,formData:{}}),b.register({name:"upload",init:function(){var b=this.owner,c=this;this.runing=!1,this.progress=!1,b.on("startUpload",function(){c.progress=!0}).on("uploadFinished",function(){c.progress=!1}),this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this.__tick=a.bindFn(this._tick,this),b.on("uploadComplete",function(a){a.blocks&&f.each(a.blocks,function(a,b){b.transport&&(b.transport.abort(),b.transport.destroy()),delete b.transport}),delete a.blocks,delete a.remaning})},reset:function(){this.request("stop-upload",!0),this.runing=!1,this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this._trigged=!1,this._promise=null},startUpload:function(b){var c=this;if(f.each(c.request("get-files",h.INVALID),function(){c.request("remove-file",this)}),b?(b=b.id?b:c.request("get-file",b),b.getStatus()===h.INTERRUPT?(b.setStatus(h.QUEUED),f.each(c.pool,function(a,c){c.file===b&&(c.transport&&c.transport.send(),b.setStatus(h.PROGRESS))})):b.getStatus()!==h.PROGRESS&&b.setStatus(h.QUEUED)):f.each(c.request("get-files",[h.INITED]),function(){this.setStatus(h.QUEUED)}),c.runing)return a.nextTick(c.__tick);c.runing=!0;var d=[];b||f.each(c.pool,function(a,b){var e=b.file;e.getStatus()===h.INTERRUPT&&(c._trigged=!1,d.push(e),b.transport&&b.transport.send())}),f.each(d,function(){this.setStatus(h.PROGRESS)}),b||f.each(c.request("get-files",h.INTERRUPT),function(){this.setStatus(h.PROGRESS)}),c._trigged=!1,a.nextTick(c.__tick),c.owner.trigger("startUpload")},stopUpload:function(b,c){var d,e=this;if(b===!0&&(c=b,b=null),e.runing!==!1){if(b){if(b=b.id?b:e.request("get-file",b),b.getStatus()!==h.PROGRESS&&b.getStatus()!==h.QUEUED)return;return b.setStatus(h.INTERRUPT),f.each(e.pool,function(a,c){return c.file===b?(d=c,!1):void 0}),d.transport&&d.transport.abort(),c&&(e._putback(d),e._popBlock(d)),a.nextTick(e.__tick)}e.runing=!1,this._promise&&this._promise.file&&this._promise.file.setStatus(h.INTERRUPT),c&&f.each(e.pool,function(a,b){b.transport&&b.transport.abort(),b.file.setStatus(h.INTERRUPT)}),e.owner.trigger("stopUpload")}},cancelFile:function(a){a=a.id?a:this.request("get-file",a),a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),a.setStatus(h.CANCELLED),this.owner.trigger("fileDequeued",a)},isInProgress:function(){return!!this.progress},_getStats:function(){return this.request("get-stats")},skipFile:function(a,b){a=a.id?a:this.request("get-file",a),a.setStatus(b||h.COMPLETE),a.skipped=!0,a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),this.owner.trigger("uploadSkip",a)},_tick:function(){var b,c,d=this,e=d.options;return d._promise?d._promise.always(d.__tick):(d.pool.length1&&~"http,abort".indexOf(a)&&b.retried1&&f.extend(m,{chunks:b.chunks,chunk:b.chunk}),i.trigger("uploadBeforeSend",b,m,n),l.appendBlob(j.fileVal,b.blob,k.name),l.append(m),l.setRequestHeader(n),l.send() +},_finishFile:function(a,b,c){var d=this.owner;return d.request("after-send-file",arguments,function(){a.setStatus(h.COMPLETE),d.trigger("uploadSuccess",a,b,c)}).fail(function(b){a.getStatus()===h.PROGRESS&&a.setStatus(h.ERROR,b),d.trigger("uploadError",a,b)}).always(function(){d.trigger("uploadComplete",a)})},updateFileProgress:function(a){var b=0,c=0;a.blocks&&(f.each(a.blocks,function(a,b){c+=(b.percentage||0)*(b.end-b.start)}),b=c/a.size,this.owner.trigger("uploadProgress",a,b||0))}})}),b("widgets/log",["base","uploader","widgets/widget"],function(a,b){function c(a){var b=e.extend({},d,a),c=f.replace(/^(.*)\?/,"$1"+e.param(b)),g=new Image;g.src=c}var d,e=a.$,f=" http://static.tieba.baidu.com/tb/pms/img/st.gif??",g=(location.hostname||location.host||"protected").toLowerCase(),h=g&&/baidu/i.exec(g);if(h)return d={dv:3,master:"webuploader",online:/test/.exec(g)?0:1,module:"",product:g,type:0},b.register({name:"log",init:function(){var a=this.owner,b=0,d=0;a.on("error",function(a){c({type:2,c_error_code:a})}).on("uploadError",function(a,b){c({type:2,c_error_code:"UPLOAD_ERROR",c_reason:""+b})}).on("uploadComplete",function(a){b++,d+=a.size}).on("uploadFinished",function(){c({c_count:b,c_size:d}),b=d=0}),c({c_usage:1})}})}),b("runtime/compbase",[],function(){function a(a,b){this.owner=a,this.options=a.options,this.getRuntime=function(){return b},this.getRuid=function(){return b.uid},this.trigger=function(){return a.trigger.apply(a,arguments)}}return a}),b("runtime/html5/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a={},d=this,e=this.destroy;c.apply(d,arguments),d.type=f,d.exec=function(c,e){var f,h=this,i=h.uid,j=b.slice(arguments,2);return g[c]&&(f=a[i]=a[i]||new g[c](h,d),f[e])?f[e].apply(f,j):void 0},d.destroy=function(){return e&&e.apply(this,arguments)}}var f="html5",g={};return b.inherits(c,{constructor:e,init:function(){var a=this;setTimeout(function(){a.trigger("ready")},1)}}),e.register=function(a,c){var e=g[a]=b.inherits(d,c);return e},a.Blob&&a.FileReader&&a.DataView&&c.addRuntime(f,e),e}),b("runtime/html5/blob",["runtime/html5/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.owner.source,e=d.slice||d.webkitSlice||d.mozSlice;return d=e.call(d,a,c),new b(this.getRuid(),d)}})}),b("runtime/html5/filepicker",["base","runtime/html5/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(){var a,b,d,e,f=this.getRuntime().getContainer(),g=this,h=g.owner,i=g.options,j=this.label=c(document.createElement("label")),k=this.input=c(document.createElement("input"));if(k.attr("type","file"),k.attr("capture","camera"),k.attr("name",i.name),k.addClass("webuploader-element-invisible"),j.on("click",function(a){k.trigger("click"),a.stopPropagation(),h.trigger("dialogopen")}),j.css({opacity:0,width:"100%",height:"100%",display:"block",cursor:"pointer",background:"#ffffff"}),i.multiple&&k.attr("multiple","multiple"),i.accept&&i.accept.length>0){for(a=[],b=0,d=i.accept.length;d>b;b++)a.push(i.accept[b].mimeTypes);k.attr("accept",a.join(","))}f.append(k),f.append(j),e=function(a){h.trigger(a.type)},k.on("change",function(a){var b,d=arguments.callee;g.files=a.target.files,b=this.cloneNode(!0),b.value=null,this.parentNode.replaceChild(b,this),k.off(),k=c(b).on("change",d).on("mouseenter mouseleave",e),h.trigger("change")}),j.on("mouseenter mouseleave",e)},getFiles:function(){return this.files},destroy:function(){this.input.off(),this.label.off()}})}),b("runtime/html5/util",["base"],function(b){var c=a.createObjectURL&&a||a.URL&&URL.revokeObjectURL&&URL||a.webkitURL,d=b.noop,e=d;return c&&(d=function(){return c.createObjectURL.apply(c,arguments)},e=function(){return c.revokeObjectURL.apply(c,arguments)}),{createObjectURL:d,revokeObjectURL:e,dataURL2Blob:function(a){var b,c,d,e,f,g;for(g=a.split(","),b=~g[0].indexOf("base64")?atob(g[1]):decodeURIComponent(g[1]),d=new ArrayBuffer(b.length),c=new Uint8Array(d),e=0;ei&&(d=h.getUint16(i),d>=65504&&65519>=d||65534===d)&&(e=h.getUint16(i+2)+2,!(i+e>h.byteLength));){if(f=b.parsers[d],!c&&f)for(g=0;g6&&(l.imageHead=a.slice?a.slice(2,k):new Uint8Array(a).subarray(2,k))}return l}},updateImageHead:function(a,b){var c,d,e,f=this._parse(a,!0);return e=2,f.imageHead&&(e=2+f.imageHead.byteLength),d=a.slice?a.slice(e):new Uint8Array(a).subarray(e),c=new Uint8Array(b.byteLength+2+d.byteLength),c[0]=255,c[1]=216,c.set(new Uint8Array(b),2),c.set(new Uint8Array(d),b.byteLength+2),c.buffer}},a.parseMeta=function(){return b.parse.apply(b,arguments)},a.updateImageHead=function(){return b.updateImageHead.apply(b,arguments)},b}),b("runtime/html5/imagemeta/exif",["base","runtime/html5/imagemeta"],function(a,b){var c={};return c.ExifMap=function(){return this},c.ExifMap.prototype.map={Orientation:274},c.ExifMap.prototype.get=function(a){return this[a]||this[this.map[a]]},c.exifTagTypes={1:{getValue:function(a,b){return a.getUint8(b)},size:1},2:{getValue:function(a,b){return String.fromCharCode(a.getUint8(b))},size:1,ascii:!0},3:{getValue:function(a,b,c){return a.getUint16(b,c)},size:2},4:{getValue:function(a,b,c){return a.getUint32(b,c)},size:4},5:{getValue:function(a,b,c){return a.getUint32(b,c)/a.getUint32(b+4,c)},size:8},9:{getValue:function(a,b,c){return a.getInt32(b,c)},size:4},10:{getValue:function(a,b,c){return a.getInt32(b,c)/a.getInt32(b+4,c)},size:8}},c.exifTagTypes[7]=c.exifTagTypes[1],c.getExifValue=function(b,d,e,f,g,h){var i,j,k,l,m,n,o=c.exifTagTypes[f];if(!o)return a.log("Invalid Exif data: Invalid tag type."),void 0;if(i=o.size*g,j=i>4?d+b.getUint32(e+8,h):e+8,j+i>b.byteLength)return a.log("Invalid Exif data: Invalid data offset."),void 0;if(1===g)return o.getValue(b,j,h);for(k=[],l=0;g>l;l+=1)k[l]=o.getValue(b,j+l*o.size,h);if(o.ascii){for(m="",l=0;lb.byteLength)return a.log("Invalid Exif data: Invalid directory offset."),void 0;if(g=b.getUint16(d,e),h=d+2+12*g,h+4>b.byteLength)return a.log("Invalid Exif data: Invalid directory size."),void 0;for(i=0;g>i;i+=1)this.parseExifTag(b,c,d+2+12*i,e,f);return b.getUint32(h,e)},c.parseExifData=function(b,d,e,f){var g,h,i=d+10;if(1165519206===b.getUint32(d+4)){if(i+8>b.byteLength)return a.log("Invalid Exif data: Invalid segment size."),void 0;if(0!==b.getUint16(d+8))return a.log("Invalid Exif data: Missing byte alignment offset."),void 0;switch(b.getUint16(i)){case 18761:g=!0;break;case 19789:g=!1;break;default:return a.log("Invalid Exif data: Invalid byte alignment marker."),void 0}if(42!==b.getUint16(i+2,g))return a.log("Invalid Exif data: Missing TIFF marker."),void 0;h=b.getUint32(i+4,g),f.exif=new c.ExifMap,h=c.parseExifTags(b,i,i+h,g,f)}},b.parsers[65505].push(c.parseExifData),c}),b("runtime/html5/image",["base","runtime/html5/runtime","runtime/html5/util"],function(a,b,c){var d="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D";return b.register("Image",{modified:!1,init:function(){var a=this,b=new Image;b.onload=function(){a._info={type:a.type,width:this.width,height:this.height},a._metas||"image/jpeg"!==a.type?a.owner.trigger("load"):c.parseMeta(a._blob,function(b,c){a._metas=c,a.owner.trigger("load")})},b.onerror=function(){a.owner.trigger("error")},a._img=b},loadFromBlob:function(a){var b=this,d=b._img;b._blob=a,b.type=a.type,d.src=c.createObjectURL(a.getSource()),b.owner.once("load",function(){c.revokeObjectURL(d.src)})},resize:function(a,b){var c=this._canvas||(this._canvas=document.createElement("canvas"));this._resize(this._img,c,a,b),this._blob=null,this.modified=!0,this.owner.trigger("complete","resize")},crop:function(a,b,c,d,e){var f=this._canvas||(this._canvas=document.createElement("canvas")),g=this.options,h=this._img,i=h.naturalWidth,j=h.naturalHeight,k=this.getOrientation();e=e||1,f.width=c,f.height=d,g.preserveHeaders||this._rotate2Orientaion(f,k),this._renderImageToCanvas(f,h,-a,-b,i*e,j*e),this._blob=null,this.modified=!0,this.owner.trigger("complete","crop")},getAsBlob:function(a){var b,d=this._blob,e=this.options;if(a=a||this.type,this.modified||this.type!==a){if(b=this._canvas,"image/jpeg"===a){if(d=c.canvasToDataUrl(b,a,e.quality),e.preserveHeaders&&this._metas&&this._metas.imageHead)return d=c.dataURL2ArrayBuffer(d),d=c.updateImageHead(d,this._metas.imageHead),d=c.arrayBufferToBlob(d,a)}else d=c.canvasToDataUrl(b,a);d=c.dataURL2Blob(d)}return d},getAsDataUrl:function(a){var b=this.options;return a=a||this.type,"image/jpeg"===a?c.canvasToDataUrl(this._canvas,a,b.quality):this._canvas.toDataURL(a)},getOrientation:function(){return this._metas&&this._metas.exif&&this._metas.exif.get("Orientation")||1},info:function(a){return a?(this._info=a,this):this._info},meta:function(a){return a?(this._metas=a,this):this._metas},destroy:function(){var a=this._canvas;this._img.onload=null,a&&(a.getContext("2d").clearRect(0,0,a.width,a.height),a.width=a.height=0,this._canvas=null),this._img.src=d,this._img=this._blob=null},_resize:function(a,b,c,d){var e,f,g,h,i,j=this.options,k=a.width,l=a.height,m=this.getOrientation();~[5,6,7,8].indexOf(m)&&(c^=d,d^=c,c^=d),e=Math[j.crop?"max":"min"](c/k,d/l),j.allowMagnify||(e=Math.min(1,e)),f=k*e,g=l*e,j.crop?(b.width=c,b.height=d):(b.width=f,b.height=g),h=(b.width-f)/2,i=(b.height-g)/2,j.preserveHeaders||this._rotate2Orientaion(b,m),this._renderImageToCanvas(b,a,h,i,f,g)},_rotate2Orientaion:function(a,b){var c=a.width,d=a.height,e=a.getContext("2d");switch(b){case 5:case 6:case 7:case 8:a.width=d,a.height=c}switch(b){case 2:e.translate(c,0),e.scale(-1,1);break;case 3:e.translate(c,d),e.rotate(Math.PI);break;case 4:e.translate(0,d),e.scale(1,-1);break;case 5:e.rotate(.5*Math.PI),e.scale(1,-1);break;case 6:e.rotate(.5*Math.PI),e.translate(0,-d);break;case 7:e.rotate(.5*Math.PI),e.translate(c,-d),e.scale(-1,1);break;case 8:e.rotate(-.5*Math.PI),e.translate(-c,0)}},_renderImageToCanvas:function(){function b(a,b,c){var d,e,f,g=document.createElement("canvas"),h=g.getContext("2d"),i=0,j=c,k=c;for(g.width=1,g.height=c,h.drawImage(a,0,0),d=h.getImageData(0,0,1,c).data;k>i;)e=d[4*(k-1)+3],0===e?j=k:i=k,k=j+i>>1;return f=k/c,0===f?1:f}function c(a){var b,c,d=a.naturalWidth,e=a.naturalHeight;return d*e>1048576?(b=document.createElement("canvas"),b.width=b.height=1,c=b.getContext("2d"),c.drawImage(a,-d+1,0),0===c.getImageData(0,0,1,1).data[3]):!1}return a.os.ios?a.os.ios>=7?function(a,c,d,e,f,g){var h=c.naturalWidth,i=c.naturalHeight,j=b(c,h,i);return a.getContext("2d").drawImage(c,0,0,h*j,i*j,d,e,f,g)}:function(a,d,e,f,g,h){var i,j,k,l,m,n,o,p=d.naturalWidth,q=d.naturalHeight,r=a.getContext("2d"),s=c(d),t="image/jpeg"===this.type,u=1024,v=0,w=0;for(s&&(p/=2,q/=2),r.save(),i=document.createElement("canvas"),i.width=i.height=u,j=i.getContext("2d"),k=t?b(d,p,q):1,l=Math.ceil(u*g/p),m=Math.ceil(u*h/q/k);q>v;){for(n=0,o=0;p>n;)j.clearRect(0,0,u,u),j.drawImage(d,-n,-v),r.drawImage(i,0,0,u,u,e+o,f+w,l,m),n+=u,o+=l;v+=u,w+=m}r.restore(),i=j=null}:function(b){var c=a.slice(arguments,1),d=b.getContext("2d");d.drawImage.apply(d,c)}}()})}),b("runtime/html5/jpegencoder",[],function(){function a(a){function b(a){for(var b=[16,11,10,16,24,40,51,61,12,12,14,19,26,58,60,55,14,13,16,24,40,57,69,56,14,17,22,29,51,87,80,62,18,22,37,56,68,109,103,77,24,35,55,64,81,104,113,92,49,64,78,87,103,121,120,101,72,92,95,98,112,100,103,99],c=0;64>c;c++){var d=y((b[c]*a+50)/100);1>d?d=1:d>255&&(d=255),z[P[c]]=d}for(var e=[17,18,24,47,99,99,99,99,18,21,26,66,99,99,99,99,24,26,56,99,99,99,99,99,47,66,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99],f=0;64>f;f++){var g=y((e[f]*a+50)/100);1>g?g=1:g>255&&(g=255),A[P[f]]=g}for(var h=[1,1.387039845,1.306562965,1.175875602,1,.785694958,.5411961,.275899379],i=0,j=0;8>j;j++)for(var k=0;8>k;k++)B[i]=1/(8*z[P[i]]*h[j]*h[k]),C[i]=1/(8*A[P[i]]*h[j]*h[k]),i++}function c(a,b){for(var c=0,d=0,e=new Array,f=1;16>=f;f++){for(var g=1;g<=a[f];g++)e[b[d]]=[],e[b[d]][0]=c,e[b[d]][1]=f,d++,c++;c*=2}return e}function d(){t=c(Q,R),u=c(U,V),v=c(S,T),w=c(W,X)}function e(){for(var a=1,b=2,c=1;15>=c;c++){for(var d=a;b>d;d++)E[32767+d]=c,D[32767+d]=[],D[32767+d][1]=c,D[32767+d][0]=d;for(var e=-(b-1);-a>=e;e++)E[32767+e]=c,D[32767+e]=[],D[32767+e][1]=c,D[32767+e][0]=b-1+e;a<<=1,b<<=1}}function f(){for(var a=0;256>a;a++)O[a]=19595*a,O[a+256>>0]=38470*a,O[a+512>>0]=7471*a+32768,O[a+768>>0]=-11059*a,O[a+1024>>0]=-21709*a,O[a+1280>>0]=32768*a+8421375,O[a+1536>>0]=-27439*a,O[a+1792>>0]=-5329*a}function g(a){for(var b=a[0],c=a[1]-1;c>=0;)b&1<J&&(255==I?(h(255),h(0)):h(I),J=7,I=0)}function h(a){H.push(N[a])}function i(a){h(255&a>>8),h(255&a)}function j(a,b){var c,d,e,f,g,h,i,j,k,l=0,m=8,n=64;for(k=0;m>k;++k){c=a[l],d=a[l+1],e=a[l+2],f=a[l+3],g=a[l+4],h=a[l+5],i=a[l+6],j=a[l+7];var o=c+j,p=c-j,q=d+i,r=d-i,s=e+h,t=e-h,u=f+g,v=f-g,w=o+u,x=o-u,y=q+s,z=q-s;a[l]=w+y,a[l+4]=w-y;var A=.707106781*(z+x);a[l+2]=x+A,a[l+6]=x-A,w=v+t,y=t+r,z=r+p;var B=.382683433*(w-z),C=.5411961*w+B,D=1.306562965*z+B,E=.707106781*y,G=p+E,H=p-E;a[l+5]=H+C,a[l+3]=H-C,a[l+1]=G+D,a[l+7]=G-D,l+=8}for(l=0,k=0;m>k;++k){c=a[l],d=a[l+8],e=a[l+16],f=a[l+24],g=a[l+32],h=a[l+40],i=a[l+48],j=a[l+56];var I=c+j,J=c-j,K=d+i,L=d-i,M=e+h,N=e-h,O=f+g,P=f-g,Q=I+O,R=I-O,S=K+M,T=K-M;a[l]=Q+S,a[l+32]=Q-S;var U=.707106781*(T+R);a[l+16]=R+U,a[l+48]=R-U,Q=P+N,S=N+L,T=L+J;var V=.382683433*(Q-T),W=.5411961*Q+V,X=1.306562965*T+V,Y=.707106781*S,Z=J+Y,$=J-Y;a[l+40]=$+W,a[l+24]=$-W,a[l+8]=Z+X,a[l+56]=Z-X,l++}var _;for(k=0;n>k;++k)_=a[k]*b[k],F[k]=_>0?0|_+.5:0|_-.5;return F}function k(){i(65504),i(16),h(74),h(70),h(73),h(70),h(0),h(1),h(1),h(0),i(1),i(1),h(0),h(0)}function l(a,b){i(65472),i(17),h(8),i(b),i(a),h(3),h(1),h(17),h(0),h(2),h(17),h(1),h(3),h(17),h(1)}function m(){i(65499),i(132),h(0);for(var a=0;64>a;a++)h(z[a]);h(1);for(var b=0;64>b;b++)h(A[b])}function n(){i(65476),i(418),h(0);for(var a=0;16>a;a++)h(Q[a+1]);for(var b=0;11>=b;b++)h(R[b]);h(16);for(var c=0;16>c;c++)h(S[c+1]);for(var d=0;161>=d;d++)h(T[d]);h(1);for(var e=0;16>e;e++)h(U[e+1]);for(var f=0;11>=f;f++)h(V[f]);h(17);for(var g=0;16>g;g++)h(W[g+1]);for(var j=0;161>=j;j++)h(X[j])}function o(){i(65498),i(12),h(3),h(1),h(0),h(2),h(17),h(3),h(17),h(0),h(63),h(0)}function p(a,b,c,d,e){for(var f,h=e[0],i=e[240],k=16,l=63,m=64,n=j(a,b),o=0;m>o;++o)G[P[o]]=n[o];var p=G[0]-c;c=G[0],0==p?g(d[0]):(f=32767+p,g(d[E[f]]),g(D[f]));for(var q=63;q>0&&0==G[q];q--);if(0==q)return g(h),c;for(var r,s=1;q>=s;){for(var t=s;0==G[s]&&q>=s;++s);var u=s-t;if(u>=k){r=u>>4;for(var v=1;r>=v;++v)g(i);u=15&u}f=32767+G[s],g(e[(u<<4)+E[f]]),g(D[f]),s++}return q!=l&&g(h),c}function q(){for(var a=String.fromCharCode,b=0;256>b;b++)N[b]=a(b)}function r(a){if(0>=a&&(a=1),a>100&&(a=100),x!=a){var c=0;c=50>a?Math.floor(5e3/a):Math.floor(200-2*a),b(c),x=a}}function s(){a||(a=50),q(),d(),e(),f(),r(a)}Math.round;var t,u,v,w,x,y=Math.floor,z=new Array(64),A=new Array(64),B=new Array(64),C=new Array(64),D=new Array(65535),E=new Array(65535),F=new Array(64),G=new Array(64),H=[],I=0,J=7,K=new Array(64),L=new Array(64),M=new Array(64),N=new Array(256),O=new Array(2048),P=[0,1,5,6,14,15,27,28,2,4,7,13,16,26,29,42,3,8,12,17,25,30,41,43,9,11,18,24,31,40,44,53,10,19,23,32,39,45,52,54,20,22,33,38,46,51,55,60,21,34,37,47,50,56,59,61,35,36,48,49,57,58,62,63],Q=[0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0],R=[0,1,2,3,4,5,6,7,8,9,10,11],S=[0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,125],T=[1,2,3,0,4,17,5,18,33,49,65,6,19,81,97,7,34,113,20,50,129,145,161,8,35,66,177,193,21,82,209,240,36,51,98,114,130,9,10,22,23,24,25,26,37,38,39,40,41,42,52,53,54,55,56,57,58,67,68,69,70,71,72,73,74,83,84,85,86,87,88,89,90,99,100,101,102,103,104,105,106,115,116,117,118,119,120,121,122,131,132,133,134,135,136,137,138,146,147,148,149,150,151,152,153,154,162,163,164,165,166,167,168,169,170,178,179,180,181,182,183,184,185,186,194,195,196,197,198,199,200,201,202,210,211,212,213,214,215,216,217,218,225,226,227,228,229,230,231,232,233,234,241,242,243,244,245,246,247,248,249,250],U=[0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0],V=[0,1,2,3,4,5,6,7,8,9,10,11],W=[0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,119],X=[0,1,2,3,17,4,5,33,49,6,18,65,81,7,97,113,19,34,50,129,8,20,66,145,161,177,193,9,35,51,82,240,21,98,114,209,10,22,36,52,225,37,241,23,24,25,26,38,39,40,41,42,53,54,55,56,57,58,67,68,69,70,71,72,73,74,83,84,85,86,87,88,89,90,99,100,101,102,103,104,105,106,115,116,117,118,119,120,121,122,130,131,132,133,134,135,136,137,138,146,147,148,149,150,151,152,153,154,162,163,164,165,166,167,168,169,170,178,179,180,181,182,183,184,185,186,194,195,196,197,198,199,200,201,202,210,211,212,213,214,215,216,217,218,226,227,228,229,230,231,232,233,234,242,243,244,245,246,247,248,249,250];this.encode=function(a,b){b&&r(b),H=new Array,I=0,J=7,i(65496),k(),m(),l(a.width,a.height),n(),o();var c=0,d=0,e=0;I=0,J=7,this.encode.displayName="_encode_";for(var f,h,j,q,s,x,y,z,A,D=a.data,E=a.width,F=a.height,G=4*E,N=0;F>N;){for(f=0;G>f;){for(s=G*N+f,x=s,y=-1,z=0,A=0;64>A;A++)z=A>>3,y=4*(7&A),x=s+z*G+y,N+z>=F&&(x-=G*(N+1+z-F)),f+y>=G&&(x-=f+y-G+4),h=D[x++],j=D[x++],q=D[x++],K[A]=(O[h]+O[j+256>>0]+O[q+512>>0]>>16)-128,L[A]=(O[h+768>>0]+O[j+1024>>0]+O[q+1280>>0]>>16)-128,M[A]=(O[h+1280>>0]+O[j+1536>>0]+O[q+1792>>0]>>16)-128;c=p(K,B,c,t,v),d=p(L,C,d,u,w),e=p(M,C,e,u,w),f+=32}N+=8}if(J>=0){var P=[];P[1]=J+1,P[0]=(1<=200&&b.status<300?(a._response=b.responseText,a.trigger("load")):b.status>=500&&b.status<600?(a._response=b.responseText,a.trigger("error","server")):a.trigger("error",a._status?"http":"abort")):void 0},a._xhr=b,b},_setRequestHeader:function(a,b){d.each(b,function(b,c){a.setRequestHeader(b,c)})},_parseJson:function(a){var b;try{b=JSON.parse(a)}catch(c){b={}}return b}})}),b("webuploader",["base","widgets/filepicker","widgets/image","widgets/queue","widgets/runtime","widgets/upload","widgets/log","runtime/html5/blob","runtime/html5/filepicker","runtime/html5/imagemeta/exif","runtime/html5/image","runtime/html5/androidpatch","runtime/html5/transport"],function(a){return a}),c("webuploader")}); \ No newline at end of file diff --git a/public/static/libs/webuploader/webuploader.fis.js b/public/static/libs/webuploader/webuploader.fis.js new file mode 100644 index 0000000..cdfd8a4 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.fis.js @@ -0,0 +1,8117 @@ +/*! WebUploader 0.1.6 */ + + +var jQuery = require('example:widget/ui/jquery/jquery.js'); + +module.exports = (function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }; + + return makeExport( jQuery ); +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + * @require "jquery" + * @require "zepto" + */ + define('dollar-third',[],function() { + var req = window.require; + var $ = window.__dollar || + window.jQuery || + window.Zepto || + req('jquery') || + req('zepto'); + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 使用jQuery的Promise + */ + define('promise-third',[ + 'dollar' + ], function( $ ) { + return { + Deferred: $.Deferred, + when: $.when, + + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + } + }; + }); + /** + * @fileOverview Promise/A+ + */ + define('promise',[ + 'promise-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.6', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview 错误信息 + */ + define('lib/dnd',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function DragAndDrop( opts ) { + opts = this.options = $.extend({}, DragAndDrop.options, opts ); + + opts.container = $( opts.container ); + + if ( !opts.container.length ) { + return; + } + + RuntimeClent.call( this, 'DragAndDrop' ); + } + + DragAndDrop.options = { + accept: null, + disableGlobalDnd: false + }; + + Base.inherits( RuntimeClent, { + constructor: DragAndDrop, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( DragAndDrop.prototype ); + + return DragAndDrop; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview DragAndDrop Widget。 + */ + define('widgets/filednd',[ + 'base', + 'uploader', + 'lib/dnd', + 'widgets/widget' + ], function( Base, Uploader, Dnd ) { + var $ = Base.$; + + Uploader.options.dnd = ''; + + /** + * @property {Selector} [dnd=undefined] 指定Drag And Drop拖拽的容器,如果不指定,则不启动。 + * @namespace options + * @for Uploader + */ + + /** + * @property {Selector} [disableGlobalDnd=false] 是否禁掉整个页面的拖拽功能,如果不禁用,图片拖进来的时候会默认被浏览器打开。 + * @namespace options + * @for Uploader + */ + + /** + * @event dndAccept + * @param {DataTransferItemList} items DataTransferItem + * @description 阻止此事件可以拒绝某些类型的文件拖入进来。目前只有 chrome 提供这样的 API,且只能通过 mime-type 验证。 + * @for Uploader + */ + return Uploader.register({ + name: 'dnd', + + init: function( opts ) { + + if ( !opts.dnd || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + disableGlobalDnd: opts.disableGlobalDnd, + container: opts.dnd, + accept: opts.accept + }), + dnd; + + this.dnd = dnd = new Dnd( options ); + + dnd.once( 'ready', deferred.resolve ); + dnd.on( 'drop', function( files ) { + me.request( 'add-file', [ files ]); + }); + + // 检测文件是否全部允许添加。 + dnd.on( 'accept', function( items ) { + return me.owner.trigger( 'dndAccept', items ); + }); + + dnd.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.dnd && this.dnd.destroy(); + } + }); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepaste',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function FilePaste( opts ) { + opts = this.options = $.extend({}, opts ); + opts.container = $( opts.container || document.body ); + RuntimeClent.call( this, 'FilePaste' ); + } + + Base.inherits( RuntimeClent, { + constructor: FilePaste, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( FilePaste.prototype ); + + return FilePaste; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/filepaste',[ + 'base', + 'uploader', + 'lib/filepaste', + 'widgets/widget' + ], function( Base, Uploader, FilePaste ) { + var $ = Base.$; + + /** + * @property {Selector} [paste=undefined] 指定监听paste事件的容器,如果不指定,不启用此功能。此功能为通过粘贴来添加截屏的图片。建议设置为`document.body`. + * @namespace options + * @for Uploader + */ + return Uploader.register({ + name: 'paste', + + init: function( opts ) { + + if ( !opts.paste || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + container: opts.paste, + accept: opts.accept + }), + paste; + + this.paste = paste = new FilePaste( options ); + + paste.once( 'ready', deferred.resolve ); + paste.on( 'paste', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + paste.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.paste && this.paste.destroy(); + } + }); + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClient, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClient.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file', + style: 'webuploader-pick' //pick element class attribute, default is "webuploader-pick" + }; + + Base.inherits( RuntimeClient, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button, + style = opts.style; + + if (style) + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + if (style) + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + if (style) + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addBtn + * @for Uploader + * @grammar addBtn( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addBtn({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.on('dialogopen', function() { + me.owner.trigger('dialogOpen', picker.button); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview Image + */ + define('lib/image',[ + 'base', + 'runtime/client', + 'lib/blob' + ], function( Base, RuntimeClient, Blob ) { + var $ = Base.$; + + // 构造器。 + function Image( opts ) { + this.options = $.extend({}, Image.options, opts ); + RuntimeClient.call( this, 'Image' ); + + this.on( 'load', function() { + this._info = this.exec('info'); + this._meta = this.exec('meta'); + }); + } + + // 默认选项。 + Image.options = { + + // 默认的图片处理质量 + quality: 90, + + // 是否裁剪 + crop: false, + + // 是否保留头部信息 + preserveHeaders: false, + + // 是否允许放大。 + allowMagnify: false + }; + + // 继承RuntimeClient. + Base.inherits( RuntimeClient, { + constructor: Image, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + loadFromBlob: function( blob ) { + var me = this, + ruid = blob.getRuid(); + + this.connectRuntime( ruid, function() { + me.exec( 'init', me.options ); + me.exec( 'loadFromBlob', blob ); + }); + }, + + resize: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'resize' ].concat( args ) ); + }, + + crop: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'crop' ].concat( args ) ); + }, + + getAsDataUrl: function( type ) { + return this.exec( 'getAsDataUrl', type ); + }, + + getAsBlob: function( type ) { + var blob = this.exec( 'getAsBlob', type ); + + return new Blob( this.getRuid(), blob ); + } + }); + + return Image; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/image',[ + 'base', + 'uploader', + 'lib/image', + 'widgets/widget' + ], function( Base, Uploader, Image ) { + + var $ = Base.$, + throttle; + + // 根据要处理的文件大小来节流,一次不能处理太多,会卡。 + throttle = (function( max ) { + var occupied = 0, + waiting = [], + tick = function() { + var item; + + while ( waiting.length && occupied < max ) { + item = waiting.shift(); + occupied += item[ 0 ]; + item[ 1 ](); + } + }; + + return function( emiter, size, cb ) { + waiting.push([ size, cb ]); + emiter.once( 'destroy', function() { + occupied -= size; + setTimeout( tick, 1 ); + }); + setTimeout( tick, 1 ); + }; + })( 5 * 1024 * 1024 ); + + $.extend( Uploader.options, { + + /** + * @property {Object} [thumb] + * @namespace options + * @for Uploader + * @description 配置生成缩略图的选项。 + * + * 默认为: + * + * ```javascript + * { + * width: 110, + * height: 110, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 70, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: true, + * + * // 是否允许裁剪。 + * crop: true, + * + * // 为空的话则保留原有图片格式。 + * // 否则强制转换成指定的类型。 + * type: 'image/jpeg' + * } + * ``` + */ + thumb: { + width: 110, + height: 110, + quality: 70, + allowMagnify: true, + crop: true, + preserveHeaders: false, + + // 为空的话则保留原有图片格式。 + // 否则强制转换成指定的类型。 + // IE 8下面 base64 大小不能超过 32K 否则预览失败,而非 jpeg 编码的图片很可 + // 能会超过 32k, 所以这里设置成预览的时候都是 image/jpeg + type: 'image/jpeg' + }, + + /** + * @property {Object} [compress] + * @namespace options + * @for Uploader + * @description 配置压缩的图片的选项。如果此选项为`false`, 则图片在上传前不进行压缩。 + * + * 默认为: + * + * ```javascript + * { + * width: 1600, + * height: 1600, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 90, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: false, + * + * // 是否允许裁剪。 + * crop: false, + * + * // 是否保留头部meta信息。 + * preserveHeaders: true, + * + * // 如果发现压缩后文件大小比原来还大,则使用原来图片 + * // 此属性可能会影响图片自动纠正功能 + * noCompressIfLarger: false, + * + * // 单位字节,如果图片大小小于此值,不会采用压缩。 + * compressSize: 0 + * } + * ``` + */ + compress: { + width: 1600, + height: 1600, + quality: 90, + allowMagnify: false, + crop: false, + preserveHeaders: true + } + }); + + return Uploader.register({ + + name: 'image', + + + /** + * 生成缩略图,此过程为异步,所以需要传入`callback`。 + * 通常情况在图片加入队里后调用此方法来生成预览图以增强交互效果。 + * + * 当 width 或者 height 的值介于 0 - 1 时,被当成百分比使用。 + * + * `callback`中可以接收到两个参数。 + * * 第一个为error,如果生成缩略图有错误,此error将为真。 + * * 第二个为ret, 缩略图的Data URL值。 + * + * **注意** + * Date URL在IE6/7中不支持,所以不用调用此方法了,直接显示一张暂不支持预览图片好了。 + * 也可以借助服务端,将 base64 数据传给服务端,生成一个临时文件供预览。 + * + * @method makeThumb + * @grammar makeThumb( file, callback ) => undefined + * @grammar makeThumb( file, callback, width, height ) => undefined + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.makeThumb( file, function( error, ret ) { + * if ( error ) { + * $li.text('预览错误'); + * } else { + * $li.append(''); + * } + * }); + * + * }); + */ + makeThumb: function( file, cb, width, height ) { + var opts, image; + + file = this.request( 'get-file', file ); + + // 只预览图片格式。 + if ( !file.type.match( /^image/ ) ) { + cb( true ); + return; + } + + opts = $.extend({}, this.options.thumb ); + + // 如果传入的是object. + if ( $.isPlainObject( width ) ) { + opts = $.extend( opts, width ); + width = null; + } + + width = width || opts.width; + height = height || opts.height; + + image = new Image( opts ); + + image.once( 'load', function() { + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + // 当 resize 完后 + image.once( 'complete', function() { + cb( false, image.getAsDataUrl( opts.type ) ); + image.destroy(); + }); + + image.once( 'error', function( reason ) { + cb( reason || true ); + image.destroy(); + }); + + throttle( image, file.source.size, function() { + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + image.loadFromBlob( file.source ); + }); + }, + + beforeSendFile: function( file ) { + var opts = this.options.compress || this.options.resize, + compressSize = opts && opts.compressSize || 0, + noCompressIfLarger = opts && opts.noCompressIfLarger || false, + image, deferred; + + file = this.request( 'get-file', file ); + + // 只压缩 jpeg 图片格式。 + // gif 可能会丢失针 + // bmp png 基本上尺寸都不大,且压缩比比较小。 + if ( !opts || !~'image/jpeg,image/jpg'.indexOf( file.type ) || + file.size < compressSize || + file._compressed ) { + return; + } + + opts = $.extend({}, opts ); + deferred = Base.Deferred(); + + image = new Image( opts ); + + deferred.always(function() { + image.destroy(); + image = null; + }); + image.once( 'error', deferred.reject ); + image.once( 'load', function() { + var width = opts.width, + height = opts.height; + + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + image.once( 'complete', function() { + var blob, size; + + // 移动端 UC / qq 浏览器的无图模式下 + // ctx.getImageData 处理大图的时候会报 Exception + // INDEX_SIZE_ERR: DOM Exception 1 + try { + blob = image.getAsBlob( opts.type ); + + size = file.size; + + // 如果压缩后,比原来还大则不用压缩后的。 + if ( !noCompressIfLarger || blob.size < size ) { + // file.source.destroy && file.source.destroy(); + file.source = blob; + file.size = blob.size; + + file.trigger( 'resize', blob.size, size ); + } + + // 标记,避免重复压缩。 + file._compressed = true; + deferred.resolve(); + } catch ( e ) { + // 出错了直接继续,让其上传原始图片 + deferred.resolve(); + } + }); + + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + + image.loadFromBlob( file.source ); + return deferred.promise(); + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + if ( files.length ) { + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + // 销毁上传相关的属性。 + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定的文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + file.setStatus( Status.QUEUED ); + + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + file.setStatus( Status.PROGRESS ); + }); + + + } else if (file.getStatus() !== Status.PROGRESS) { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return Base.nextTick( me.__tick ); + } + + me.runing = true; + var files = []; + + // 如果有暂停的,则续传 + file || $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + me._trigged = false; + files.push(file); + v.transport && v.transport.send(); + } + }); + + $.each(files, function() { + this.setStatus( Status.PROGRESS ); + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this, + block; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + + + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file === file) { + block = v; + return false; + } + }); + + block.transport && block.transport.abort(); + + if (interrupt) { + me._putback(block); + me._popBlock(block); + } + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + // 正在准备中的文件。 + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + + /** + * @fileOverview Md5 + */ + define('lib/md5',[ + 'runtime/client', + 'mediator' + ], function( RuntimeClient, Mediator ) { + + function Md5() { + RuntimeClient.call( this, 'Md5' ); + } + + // 让 Md5 具备事件功能。 + Mediator.installTo( Md5.prototype ); + + Md5.prototype.loadFromBlob = function( blob ) { + var me = this; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + me.exec( 'loadFromBlob', blob ); + }); + }; + + Md5.prototype.getResult = function() { + return this.exec('getResult'); + }; + + return Md5; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/md5',[ + 'base', + 'uploader', + 'lib/md5', + 'lib/blob', + 'widgets/widget' + ], function( Base, Uploader, Md5, Blob ) { + + return Uploader.register({ + name: 'md5', + + + /** + * 计算文件 md5 值,返回一个 promise 对象,可以监听 progress 进度。 + * + * + * @method md5File + * @grammar md5File( file[, start[, end]] ) => promise + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.md5File( file ) + * + * // 及时显示进度 + * .progress(function(percentage) { + * console.log('Percentage:', percentage); + * }) + * + * // 完成 + * .then(function(val) { + * console.log('md5 result:', val); + * }); + * + * }); + */ + md5File: function( file, start, end ) { + var md5 = new Md5(), + deferred = Base.Deferred(), + blob = (file instanceof Blob) ? file : + this.request( 'get-file', file ).source; + + md5.on( 'progress load', function( e ) { + e = e || {}; + deferred.notify( e.total ? e.loaded / e.total : 1 ); + }); + + md5.on( 'complete', function() { + deferred.resolve( md5.getResult() ); + }); + + md5.on( 'error', function( reason ) { + deferred.reject( reason ); + }); + + if ( arguments.length > 1 ) { + start = start || 0; + end = end || 0; + start < 0 && (start = blob.size + start); + end < 0 && (end = blob.size + end); + end = Math.min( end, blob.size ); + blob = blob.slice( start, end ); + } + + md5.loadFromBlob( blob ); + + return deferred.promise(); + } + }); + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePaste + */ + define('runtime/html5/dnd',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + var $ = Base.$, + prefix = 'webuploader-dnd-'; + + return Html5Runtime.register( 'DragAndDrop', { + init: function() { + var elem = this.elem = this.options.container; + + this.dragEnterHandler = Base.bindFn( this._dragEnterHandler, this ); + this.dragOverHandler = Base.bindFn( this._dragOverHandler, this ); + this.dragLeaveHandler = Base.bindFn( this._dragLeaveHandler, this ); + this.dropHandler = Base.bindFn( this._dropHandler, this ); + this.dndOver = false; + + elem.on( 'dragenter', this.dragEnterHandler ); + elem.on( 'dragover', this.dragOverHandler ); + elem.on( 'dragleave', this.dragLeaveHandler ); + elem.on( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).on( 'dragover', this.dragOverHandler ); + $( document ).on( 'drop', this.dropHandler ); + } + }, + + _dragEnterHandler: function( e ) { + var me = this, + denied = me._denied || false, + items; + + e = e.originalEvent || e; + + if ( !me.dndOver ) { + me.dndOver = true; + + // 注意只有 chrome 支持。 + items = e.dataTransfer.items; + + if ( items && items.length ) { + me._denied = denied = !me.trigger( 'accept', items ); + } + + me.elem.addClass( prefix + 'over' ); + me.elem[ denied ? 'addClass' : + 'removeClass' ]( prefix + 'denied' ); + } + + e.dataTransfer.dropEffect = denied ? 'none' : 'copy'; + + return false; + }, + + _dragOverHandler: function( e ) { + // 只处理框内的。 + var parentElem = this.elem.parent().get( 0 ); + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + clearTimeout( this._leaveTimer ); + this._dragEnterHandler.call( this, e ); + + return false; + }, + + _dragLeaveHandler: function() { + var me = this, + handler; + + handler = function() { + me.dndOver = false; + me.elem.removeClass( prefix + 'over ' + prefix + 'denied' ); + }; + + clearTimeout( me._leaveTimer ); + me._leaveTimer = setTimeout( handler, 100 ); + return false; + }, + + _dropHandler: function( e ) { + var me = this, + ruid = me.getRuid(), + parentElem = me.elem.parent().get( 0 ), + dataTransfer, data; + + // 只处理框内的。 + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + e = e.originalEvent || e; + dataTransfer = e.dataTransfer; + + // 如果是页面内拖拽,还不能处理,不阻止事件。 + // 此处 ie11 下会报参数错误, + try { + data = dataTransfer.getData('text/html'); + } catch( err ) { + } + + me.dndOver = false; + me.elem.removeClass( prefix + 'over' ); + + if ( data ) { + return; + } + + me._getTansferFiles( dataTransfer, function( results ) { + me.trigger( 'drop', $.map( results, function( file ) { + return new File( ruid, file ); + }) ); + }); + + return false; + }, + + // 如果传入 callback 则去查看文件夹,否则只管当前文件夹。 + _getTansferFiles: function( dataTransfer, callback ) { + var results = [], + promises = [], + items, files, file, item, i, len, canAccessFolder; + + items = dataTransfer.items; + files = dataTransfer.files; + + canAccessFolder = !!(items && items[ 0 ].webkitGetAsEntry); + + for ( i = 0, len = files.length; i < len; i++ ) { + file = files[ i ]; + item = items && items[ i ]; + + if ( canAccessFolder && item.webkitGetAsEntry().isDirectory ) { + + promises.push( this._traverseDirectoryTree( + item.webkitGetAsEntry(), results ) ); + } else { + results.push( file ); + } + } + + Base.when.apply( Base, promises ).done(function() { + + if ( !results.length ) { + return; + } + + callback( results ); + }); + }, + + _traverseDirectoryTree: function( entry, results ) { + var deferred = Base.Deferred(), + me = this; + + if ( entry.isFile ) { + entry.file(function( file ) { + results.push( file ); + deferred.resolve(); + }); + } else if ( entry.isDirectory ) { + entry.createReader().readEntries(function( entries ) { + var len = entries.length, + promises = [], + arr = [], // 为了保证顺序。 + i; + + for ( i = 0; i < len; i++ ) { + promises.push( me._traverseDirectoryTree( + entries[ i ], arr ) ); + } + + Base.when.apply( Base, promises ).then(function() { + results.push.apply( results, arr ); + deferred.resolve(); + }, deferred.reject ); + }); + } + + return deferred.promise(); + }, + + destroy: function() { + var elem = this.elem; + + // 还没 init 就调用 destroy + if (!elem) { + return; + } + + elem.off( 'dragenter', this.dragEnterHandler ); + elem.off( 'dragover', this.dragOverHandler ); + elem.off( 'dragleave', this.dragLeaveHandler ); + elem.off( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).off( 'dragover', this.dragOverHandler ); + $( document ).off( 'drop', this.dropHandler ); + } + } + }); + }); + + /** + * @fileOverview FilePaste + */ + define('runtime/html5/filepaste',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + return Html5Runtime.register( 'FilePaste', { + init: function() { + var opts = this.options, + elem = this.elem = opts.container, + accept = '.*', + arr, i, len, item; + + // accetp的mimeTypes中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].mimeTypes; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = arr.join(','); + accept = accept.replace( /,/g, '|' ).replace( /\*/g, '.*' ); + } + } + this.accept = accept = new RegExp( accept, 'i' ); + this.hander = Base.bindFn( this._pasteHander, this ); + elem.on( 'paste', this.hander ); + }, + + _pasteHander: function( e ) { + var allowed = [], + ruid = this.getRuid(), + items, item, blob, i, len; + + e = e.originalEvent || e; + items = e.clipboardData.items; + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + + if ( item.kind !== 'file' || !(blob = item.getAsFile()) ) { + continue; + } + + allowed.push( new File( ruid, blob ) ); + } + + if ( allowed.length ) { + // 不阻止非文件粘贴(文字粘贴)的事件冒泡 + e.preventDefault(); + e.stopPropagation(); + this.trigger( 'paste', allowed ); + } + }, + + destroy: function() { + this.elem.off( 'paste', this.hander ); + } + }); + }); + + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'capture', 'camera'); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function(e) { + input.trigger('click'); + e.stopPropagation(); + owner.trigger('dialogopen'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/util',[ + 'base' + ], function( Base ) { + + var urlAPI = window.createObjectURL && window || + window.URL && URL.revokeObjectURL && URL || + window.webkitURL, + createObjectURL = Base.noop, + revokeObjectURL = createObjectURL; + + if ( urlAPI ) { + + // 更安全的方式调用,比如android里面就能把context改成其他的对象。 + createObjectURL = function() { + return urlAPI.createObjectURL.apply( urlAPI, arguments ); + }; + + revokeObjectURL = function() { + return urlAPI.revokeObjectURL.apply( urlAPI, arguments ); + }; + } + + return { + createObjectURL: createObjectURL, + revokeObjectURL: revokeObjectURL, + + dataURL2Blob: function( dataURI ) { + var byteStr, intArray, ab, i, mimetype, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + ab = new ArrayBuffer( byteStr.length ); + intArray = new Uint8Array( ab ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + mimetype = parts[ 0 ].split(':')[ 1 ].split(';')[ 0 ]; + + return this.arrayBufferToBlob( ab, mimetype ); + }, + + dataURL2ArrayBuffer: function( dataURI ) { + var byteStr, intArray, i, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + intArray = new Uint8Array( byteStr.length ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + return intArray.buffer; + }, + + arrayBufferToBlob: function( buffer, type ) { + var builder = window.BlobBuilder || window.WebKitBlobBuilder, + bb; + + // android不支持直接new Blob, 只能借助blobbuilder. + if ( builder ) { + bb = new builder(); + bb.append( buffer ); + return bb.getBlob( type ); + } + + return new Blob([ buffer ], type ? { type: type } : {} ); + }, + + // 抽出来主要是为了解决android下面canvas.toDataUrl不支持jpeg. + // 你得到的结果是png. + canvasToDataUrl: function( canvas, type, quality ) { + return canvas.toDataURL( type, quality / 100 ); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + parseMeta: function( blob, callback ) { + callback( false, {}); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + updateImageHead: function( data ) { + return data; + } + }; + }); + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/imagemeta',[ + 'runtime/html5/util' + ], function( Util ) { + + var api; + + api = { + parsers: { + 0xffe1: [] + }, + + maxMetaDataSize: 262144, + + parse: function( blob, cb ) { + var me = this, + fr = new FileReader(); + + fr.onload = function() { + cb( false, me._parse( this.result ) ); + fr = fr.onload = fr.onerror = null; + }; + + fr.onerror = function( e ) { + cb( e.message ); + fr = fr.onload = fr.onerror = null; + }; + + blob = blob.slice( 0, me.maxMetaDataSize ); + fr.readAsArrayBuffer( blob.getSource() ); + }, + + _parse: function( buffer, noParse ) { + if ( buffer.byteLength < 6 ) { + return; + } + + var dataview = new DataView( buffer ), + offset = 2, + maxOffset = dataview.byteLength - 4, + headLength = offset, + ret = {}, + markerBytes, markerLength, parsers, i; + + if ( dataview.getUint16( 0 ) === 0xffd8 ) { + + while ( offset < maxOffset ) { + markerBytes = dataview.getUint16( offset ); + + if ( markerBytes >= 0xffe0 && markerBytes <= 0xffef || + markerBytes === 0xfffe ) { + + markerLength = dataview.getUint16( offset + 2 ) + 2; + + if ( offset + markerLength > dataview.byteLength ) { + break; + } + + parsers = api.parsers[ markerBytes ]; + + if ( !noParse && parsers ) { + for ( i = 0; i < parsers.length; i += 1 ) { + parsers[ i ].call( api, dataview, offset, + markerLength, ret ); + } + } + + offset += markerLength; + headLength = offset; + } else { + break; + } + } + + if ( headLength > 6 ) { + if ( buffer.slice ) { + ret.imageHead = buffer.slice( 2, headLength ); + } else { + // Workaround for IE10, which does not yet + // support ArrayBuffer.slice: + ret.imageHead = new Uint8Array( buffer ) + .subarray( 2, headLength ); + } + } + } + + return ret; + }, + + updateImageHead: function( buffer, head ) { + var data = this._parse( buffer, true ), + buf1, buf2, bodyoffset; + + + bodyoffset = 2; + if ( data.imageHead ) { + bodyoffset = 2 + data.imageHead.byteLength; + } + + if ( buffer.slice ) { + buf2 = buffer.slice( bodyoffset ); + } else { + buf2 = new Uint8Array( buffer ).subarray( bodyoffset ); + } + + buf1 = new Uint8Array( head.byteLength + 2 + buf2.byteLength ); + + buf1[ 0 ] = 0xFF; + buf1[ 1 ] = 0xD8; + buf1.set( new Uint8Array( head ), 2 ); + buf1.set( new Uint8Array( buf2 ), head.byteLength + 2 ); + + return buf1.buffer; + } + }; + + Util.parseMeta = function() { + return api.parse.apply( api, arguments ); + }; + + Util.updateImageHead = function() { + return api.updateImageHead.apply( api, arguments ); + }; + + return api; + }); + /** + * 代码来自于:https://github.com/blueimp/JavaScript-Load-Image + * 暂时项目中只用了orientation. + * + * 去除了 Exif Sub IFD Pointer, GPS Info IFD Pointer, Exif Thumbnail. + * @fileOverview EXIF解析 + */ + + // Sample + // ==================================== + // Make : Apple + // Model : iPhone 4S + // Orientation : 1 + // XResolution : 72 [72/1] + // YResolution : 72 [72/1] + // ResolutionUnit : 2 + // Software : QuickTime 7.7.1 + // DateTime : 2013:09:01 22:53:55 + // ExifIFDPointer : 190 + // ExposureTime : 0.058823529411764705 [1/17] + // FNumber : 2.4 [12/5] + // ExposureProgram : Normal program + // ISOSpeedRatings : 800 + // ExifVersion : 0220 + // DateTimeOriginal : 2013:09:01 22:52:51 + // DateTimeDigitized : 2013:09:01 22:52:51 + // ComponentsConfiguration : YCbCr + // ShutterSpeedValue : 4.058893515764426 + // ApertureValue : 2.5260688216892597 [4845/1918] + // BrightnessValue : -0.3126686601998395 + // MeteringMode : Pattern + // Flash : Flash did not fire, compulsory flash mode + // FocalLength : 4.28 [107/25] + // SubjectArea : [4 values] + // FlashpixVersion : 0100 + // ColorSpace : 1 + // PixelXDimension : 2448 + // PixelYDimension : 3264 + // SensingMethod : One-chip color area sensor + // ExposureMode : 0 + // WhiteBalance : Auto white balance + // FocalLengthIn35mmFilm : 35 + // SceneCaptureType : Standard + define('runtime/html5/imagemeta/exif',[ + 'base', + 'runtime/html5/imagemeta' + ], function( Base, ImageMeta ) { + + var EXIF = {}; + + EXIF.ExifMap = function() { + return this; + }; + + EXIF.ExifMap.prototype.map = { + 'Orientation': 0x0112 + }; + + EXIF.ExifMap.prototype.get = function( id ) { + return this[ id ] || this[ this.map[ id ] ]; + }; + + EXIF.exifTagTypes = { + // byte, 8-bit unsigned int: + 1: { + getValue: function( dataView, dataOffset ) { + return dataView.getUint8( dataOffset ); + }, + size: 1 + }, + + // ascii, 8-bit byte: + 2: { + getValue: function( dataView, dataOffset ) { + return String.fromCharCode( dataView.getUint8( dataOffset ) ); + }, + size: 1, + ascii: true + }, + + // short, 16 bit int: + 3: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint16( dataOffset, littleEndian ); + }, + size: 2 + }, + + // long, 32 bit int: + 4: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // rational = two long values, + // first is numerator, second is denominator: + 5: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ) / + dataView.getUint32( dataOffset + 4, littleEndian ); + }, + size: 8 + }, + + // slong, 32 bit signed int: + 9: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // srational, two slongs, first is numerator, second is denominator: + 10: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ) / + dataView.getInt32( dataOffset + 4, littleEndian ); + }, + size: 8 + } + }; + + // undefined, 8-bit byte, value depending on field: + EXIF.exifTagTypes[ 7 ] = EXIF.exifTagTypes[ 1 ]; + + EXIF.getExifValue = function( dataView, tiffOffset, offset, type, length, + littleEndian ) { + + var tagType = EXIF.exifTagTypes[ type ], + tagSize, dataOffset, values, i, str, c; + + if ( !tagType ) { + Base.log('Invalid Exif data: Invalid tag type.'); + return; + } + + tagSize = tagType.size * length; + + // Determine if the value is contained in the dataOffset bytes, + // or if the value at the dataOffset is a pointer to the actual data: + dataOffset = tagSize > 4 ? tiffOffset + dataView.getUint32( offset + 8, + littleEndian ) : (offset + 8); + + if ( dataOffset + tagSize > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid data offset.'); + return; + } + + if ( length === 1 ) { + return tagType.getValue( dataView, dataOffset, littleEndian ); + } + + values = []; + + for ( i = 0; i < length; i += 1 ) { + values[ i ] = tagType.getValue( dataView, + dataOffset + i * tagType.size, littleEndian ); + } + + if ( tagType.ascii ) { + str = ''; + + // Concatenate the chars: + for ( i = 0; i < values.length; i += 1 ) { + c = values[ i ]; + + // Ignore the terminating NULL byte(s): + if ( c === '\u0000' ) { + break; + } + str += c; + } + + return str; + } + return values; + }; + + EXIF.parseExifTag = function( dataView, tiffOffset, offset, littleEndian, + data ) { + + var tag = dataView.getUint16( offset, littleEndian ); + data.exif[ tag ] = EXIF.getExifValue( dataView, tiffOffset, offset, + dataView.getUint16( offset + 2, littleEndian ), // tag type + dataView.getUint32( offset + 4, littleEndian ), // tag length + littleEndian ); + }; + + EXIF.parseExifTags = function( dataView, tiffOffset, dirOffset, + littleEndian, data ) { + + var tagsNumber, dirEndOffset, i; + + if ( dirOffset + 6 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory offset.'); + return; + } + + tagsNumber = dataView.getUint16( dirOffset, littleEndian ); + dirEndOffset = dirOffset + 2 + 12 * tagsNumber; + + if ( dirEndOffset + 4 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory size.'); + return; + } + + for ( i = 0; i < tagsNumber; i += 1 ) { + this.parseExifTag( dataView, tiffOffset, + dirOffset + 2 + 12 * i, // tag offset + littleEndian, data ); + } + + // Return the offset to the next directory: + return dataView.getUint32( dirEndOffset, littleEndian ); + }; + + // EXIF.getExifThumbnail = function(dataView, offset, length) { + // var hexData, + // i, + // b; + // if (!length || offset + length > dataView.byteLength) { + // Base.log('Invalid Exif data: Invalid thumbnail data.'); + // return; + // } + // hexData = []; + // for (i = 0; i < length; i += 1) { + // b = dataView.getUint8(offset + i); + // hexData.push((b < 16 ? '0' : '') + b.toString(16)); + // } + // return 'data:image/jpeg,%' + hexData.join('%'); + // }; + + EXIF.parseExifData = function( dataView, offset, length, data ) { + + var tiffOffset = offset + 10, + littleEndian, dirOffset; + + // Check for the ASCII code for "Exif" (0x45786966): + if ( dataView.getUint32( offset + 4 ) !== 0x45786966 ) { + // No Exif data, might be XMP data instead + return; + } + if ( tiffOffset + 8 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid segment size.'); + return; + } + + // Check for the two null bytes: + if ( dataView.getUint16( offset + 8 ) !== 0x0000 ) { + Base.log('Invalid Exif data: Missing byte alignment offset.'); + return; + } + + // Check the byte alignment: + switch ( dataView.getUint16( tiffOffset ) ) { + case 0x4949: + littleEndian = true; + break; + + case 0x4D4D: + littleEndian = false; + break; + + default: + Base.log('Invalid Exif data: Invalid byte alignment marker.'); + return; + } + + // Check for the TIFF tag marker (0x002A): + if ( dataView.getUint16( tiffOffset + 2, littleEndian ) !== 0x002A ) { + Base.log('Invalid Exif data: Missing TIFF marker.'); + return; + } + + // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: + dirOffset = dataView.getUint32( tiffOffset + 4, littleEndian ); + // Create the exif object to store the tags: + data.exif = new EXIF.ExifMap(); + // Parse the tags of the main image directory and retrieve the + // offset to the next directory, usually the thumbnail directory: + dirOffset = EXIF.parseExifTags( dataView, tiffOffset, + tiffOffset + dirOffset, littleEndian, data ); + + // 尝试读取缩略图 + // if ( dirOffset ) { + // thumbnailData = {exif: {}}; + // dirOffset = EXIF.parseExifTags( + // dataView, + // tiffOffset, + // tiffOffset + dirOffset, + // littleEndian, + // thumbnailData + // ); + + // // Check for JPEG Thumbnail offset: + // if (thumbnailData.exif[0x0201]) { + // data.exif.Thumbnail = EXIF.getExifThumbnail( + // dataView, + // tiffOffset + thumbnailData.exif[0x0201], + // thumbnailData.exif[0x0202] // Thumbnail data length + // ); + // } + // } + }; + + ImageMeta.parsers[ 0xffe1 ].push( EXIF.parseExifData ); + return EXIF; + }); + /** + * 这个方式性能不行,但是可以解决android里面的toDataUrl的bug + * android里面toDataUrl('image/jpege')得到的结果却是png. + * + * 所以这里没辙,只能借助这个工具 + * @fileOverview jpeg encoder + */ + define('runtime/html5/jpegencoder',[], function( require, exports, module ) { + + /* + Copyright (c) 2008, Adobe Systems Incorporated + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of Adobe Systems Incorporated nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + /* + JPEG encoder ported to JavaScript and optimized by Andreas Ritter, www.bytestrom.eu, 11/2009 + + Basic GUI blocking jpeg encoder + */ + + function JPEGEncoder(quality) { + var self = this; + var fround = Math.round; + var ffloor = Math.floor; + var YTable = new Array(64); + var UVTable = new Array(64); + var fdtbl_Y = new Array(64); + var fdtbl_UV = new Array(64); + var YDC_HT; + var UVDC_HT; + var YAC_HT; + var UVAC_HT; + + var bitcode = new Array(65535); + var category = new Array(65535); + var outputfDCTQuant = new Array(64); + var DU = new Array(64); + var byteout = []; + var bytenew = 0; + var bytepos = 7; + + var YDU = new Array(64); + var UDU = new Array(64); + var VDU = new Array(64); + var clt = new Array(256); + var RGB_YUV_TABLE = new Array(2048); + var currentQuality; + + var ZigZag = [ + 0, 1, 5, 6,14,15,27,28, + 2, 4, 7,13,16,26,29,42, + 3, 8,12,17,25,30,41,43, + 9,11,18,24,31,40,44,53, + 10,19,23,32,39,45,52,54, + 20,22,33,38,46,51,55,60, + 21,34,37,47,50,56,59,61, + 35,36,48,49,57,58,62,63 + ]; + + var std_dc_luminance_nrcodes = [0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0]; + var std_dc_luminance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_luminance_nrcodes = [0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d]; + var std_ac_luminance_values = [ + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12, + 0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07, + 0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08, + 0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0, + 0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16, + 0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39, + 0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49, + 0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69, + 0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79, + 0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98, + 0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7, + 0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6, + 0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5, + 0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4, + 0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea, + 0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + var std_dc_chrominance_nrcodes = [0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0]; + var std_dc_chrominance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_chrominance_nrcodes = [0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77]; + var std_ac_chrominance_values = [ + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21, + 0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71, + 0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0, + 0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34, + 0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38, + 0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48, + 0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68, + 0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78, + 0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96, + 0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5, + 0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4, + 0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3, + 0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2, + 0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9, + 0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + function initQuantTables(sf){ + var YQT = [ + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68,109,103, 77, + 24, 35, 55, 64, 81,104,113, 92, + 49, 64, 78, 87,103,121,120,101, + 72, 92, 95, 98,112,100,103, 99 + ]; + + for (var i = 0; i < 64; i++) { + var t = ffloor((YQT[i]*sf+50)/100); + if (t < 1) { + t = 1; + } else if (t > 255) { + t = 255; + } + YTable[ZigZag[i]] = t; + } + var UVQT = [ + 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99 + ]; + for (var j = 0; j < 64; j++) { + var u = ffloor((UVQT[j]*sf+50)/100); + if (u < 1) { + u = 1; + } else if (u > 255) { + u = 255; + } + UVTable[ZigZag[j]] = u; + } + var aasf = [ + 1.0, 1.387039845, 1.306562965, 1.175875602, + 1.0, 0.785694958, 0.541196100, 0.275899379 + ]; + var k = 0; + for (var row = 0; row < 8; row++) + { + for (var col = 0; col < 8; col++) + { + fdtbl_Y[k] = (1.0 / (YTable [ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + fdtbl_UV[k] = (1.0 / (UVTable[ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + k++; + } + } + } + + function computeHuffmanTbl(nrcodes, std_table){ + var codevalue = 0; + var pos_in_table = 0; + var HT = new Array(); + for (var k = 1; k <= 16; k++) { + for (var j = 1; j <= nrcodes[k]; j++) { + HT[std_table[pos_in_table]] = []; + HT[std_table[pos_in_table]][0] = codevalue; + HT[std_table[pos_in_table]][1] = k; + pos_in_table++; + codevalue++; + } + codevalue*=2; + } + return HT; + } + + function initHuffmanTbl() + { + YDC_HT = computeHuffmanTbl(std_dc_luminance_nrcodes,std_dc_luminance_values); + UVDC_HT = computeHuffmanTbl(std_dc_chrominance_nrcodes,std_dc_chrominance_values); + YAC_HT = computeHuffmanTbl(std_ac_luminance_nrcodes,std_ac_luminance_values); + UVAC_HT = computeHuffmanTbl(std_ac_chrominance_nrcodes,std_ac_chrominance_values); + } + + function initCategoryNumber() + { + var nrlower = 1; + var nrupper = 2; + for (var cat = 1; cat <= 15; cat++) { + //Positive numbers + for (var nr = nrlower; nr>0] = 38470 * i; + RGB_YUV_TABLE[(i+ 512)>>0] = 7471 * i + 0x8000; + RGB_YUV_TABLE[(i+ 768)>>0] = -11059 * i; + RGB_YUV_TABLE[(i+1024)>>0] = -21709 * i; + RGB_YUV_TABLE[(i+1280)>>0] = 32768 * i + 0x807FFF; + RGB_YUV_TABLE[(i+1536)>>0] = -27439 * i; + RGB_YUV_TABLE[(i+1792)>>0] = - 5329 * i; + } + } + + // IO functions + function writeBits(bs) + { + var value = bs[0]; + var posval = bs[1]-1; + while ( posval >= 0 ) { + if (value & (1 << posval) ) { + bytenew |= (1 << bytepos); + } + posval--; + bytepos--; + if (bytepos < 0) { + if (bytenew == 0xFF) { + writeByte(0xFF); + writeByte(0); + } + else { + writeByte(bytenew); + } + bytepos=7; + bytenew=0; + } + } + } + + function writeByte(value) + { + byteout.push(clt[value]); // write char directly instead of converting later + } + + function writeWord(value) + { + writeByte((value>>8)&0xFF); + writeByte((value )&0xFF); + } + + // DCT & quantization core + function fDCTQuant(data, fdtbl) + { + var d0, d1, d2, d3, d4, d5, d6, d7; + /* Pass 1: process rows. */ + var dataOff=0; + var i; + var I8 = 8; + var I64 = 64; + for (i=0; i 0.0) ? ((fDCTQuant + 0.5)|0) : ((fDCTQuant - 0.5)|0); + //outputfDCTQuant[i] = fround(fDCTQuant); + + } + return outputfDCTQuant; + } + + function writeAPP0() + { + writeWord(0xFFE0); // marker + writeWord(16); // length + writeByte(0x4A); // J + writeByte(0x46); // F + writeByte(0x49); // I + writeByte(0x46); // F + writeByte(0); // = "JFIF",'\0' + writeByte(1); // versionhi + writeByte(1); // versionlo + writeByte(0); // xyunits + writeWord(1); // xdensity + writeWord(1); // ydensity + writeByte(0); // thumbnwidth + writeByte(0); // thumbnheight + } + + function writeSOF0(width, height) + { + writeWord(0xFFC0); // marker + writeWord(17); // length, truecolor YUV JPG + writeByte(8); // precision + writeWord(height); + writeWord(width); + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0x11); // HVY + writeByte(0); // QTY + writeByte(2); // IdU + writeByte(0x11); // HVU + writeByte(1); // QTU + writeByte(3); // IdV + writeByte(0x11); // HVV + writeByte(1); // QTV + } + + function writeDQT() + { + writeWord(0xFFDB); // marker + writeWord(132); // length + writeByte(0); + for (var i=0; i<64; i++) { + writeByte(YTable[i]); + } + writeByte(1); + for (var j=0; j<64; j++) { + writeByte(UVTable[j]); + } + } + + function writeDHT() + { + writeWord(0xFFC4); // marker + writeWord(0x01A2); // length + + writeByte(0); // HTYDCinfo + for (var i=0; i<16; i++) { + writeByte(std_dc_luminance_nrcodes[i+1]); + } + for (var j=0; j<=11; j++) { + writeByte(std_dc_luminance_values[j]); + } + + writeByte(0x10); // HTYACinfo + for (var k=0; k<16; k++) { + writeByte(std_ac_luminance_nrcodes[k+1]); + } + for (var l=0; l<=161; l++) { + writeByte(std_ac_luminance_values[l]); + } + + writeByte(1); // HTUDCinfo + for (var m=0; m<16; m++) { + writeByte(std_dc_chrominance_nrcodes[m+1]); + } + for (var n=0; n<=11; n++) { + writeByte(std_dc_chrominance_values[n]); + } + + writeByte(0x11); // HTUACinfo + for (var o=0; o<16; o++) { + writeByte(std_ac_chrominance_nrcodes[o+1]); + } + for (var p=0; p<=161; p++) { + writeByte(std_ac_chrominance_values[p]); + } + } + + function writeSOS() + { + writeWord(0xFFDA); // marker + writeWord(12); // length + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0); // HTY + writeByte(2); // IdU + writeByte(0x11); // HTU + writeByte(3); // IdV + writeByte(0x11); // HTV + writeByte(0); // Ss + writeByte(0x3f); // Se + writeByte(0); // Bf + } + + function processDU(CDU, fdtbl, DC, HTDC, HTAC){ + var EOB = HTAC[0x00]; + var M16zeroes = HTAC[0xF0]; + var pos; + var I16 = 16; + var I63 = 63; + var I64 = 64; + var DU_DCT = fDCTQuant(CDU, fdtbl); + //ZigZag reorder + for (var j=0;j0)&&(DU[end0pos]==0); end0pos--) {}; + //end0pos = first element in reverse order !=0 + if ( end0pos == 0) { + writeBits(EOB); + return DC; + } + var i = 1; + var lng; + while ( i <= end0pos ) { + var startpos = i; + for (; (DU[i]==0) && (i<=end0pos); ++i) {} + var nrzeroes = i-startpos; + if ( nrzeroes >= I16 ) { + lng = nrzeroes>>4; + for (var nrmarker=1; nrmarker <= lng; ++nrmarker) + writeBits(M16zeroes); + nrzeroes = nrzeroes&0xF; + } + pos = 32767+DU[i]; + writeBits(HTAC[(nrzeroes<<4)+category[pos]]); + writeBits(bitcode[pos]); + i++; + } + if ( end0pos != I63 ) { + writeBits(EOB); + } + return DC; + } + + function initCharLookupTable(){ + var sfcc = String.fromCharCode; + for(var i=0; i < 256; i++){ ///// ACHTUNG // 255 + clt[i] = sfcc(i); + } + } + + this.encode = function(image,quality) // image data object + { + // var time_start = new Date().getTime(); + + if(quality) setQuality(quality); + + // Initialize bit writer + byteout = new Array(); + bytenew=0; + bytepos=7; + + // Add JPEG headers + writeWord(0xFFD8); // SOI + writeAPP0(); + writeDQT(); + writeSOF0(image.width,image.height); + writeDHT(); + writeSOS(); + + + // Encode 8x8 macroblocks + var DCY=0; + var DCU=0; + var DCV=0; + + bytenew=0; + bytepos=7; + + + this.encode.displayName = "_encode_"; + + var imageData = image.data; + var width = image.width; + var height = image.height; + + var quadWidth = width*4; + var tripleWidth = width*3; + + var x, y = 0; + var r, g, b; + var start,p, col,row,pos; + while(y < height){ + x = 0; + while(x < quadWidth){ + start = quadWidth * y + x; + p = start; + col = -1; + row = 0; + + for(pos=0; pos < 64; pos++){ + row = pos >> 3;// /8 + col = ( pos & 7 ) * 4; // %8 + p = start + ( row * quadWidth ) + col; + + if(y+row >= height){ // padding bottom + p-= (quadWidth*(y+1+row-height)); + } + + if(x+col >= quadWidth){ // padding right + p-= ((x+col) - quadWidth +4) + } + + r = imageData[ p++ ]; + g = imageData[ p++ ]; + b = imageData[ p++ ]; + + + /* // calculate YUV values dynamically + YDU[pos]=((( 0.29900)*r+( 0.58700)*g+( 0.11400)*b))-128; //-0x80 + UDU[pos]=(((-0.16874)*r+(-0.33126)*g+( 0.50000)*b)); + VDU[pos]=((( 0.50000)*r+(-0.41869)*g+(-0.08131)*b)); + */ + + // use lookup table (slightly faster) + YDU[pos] = ((RGB_YUV_TABLE[r] + RGB_YUV_TABLE[(g + 256)>>0] + RGB_YUV_TABLE[(b + 512)>>0]) >> 16)-128; + UDU[pos] = ((RGB_YUV_TABLE[(r + 768)>>0] + RGB_YUV_TABLE[(g + 1024)>>0] + RGB_YUV_TABLE[(b + 1280)>>0]) >> 16)-128; + VDU[pos] = ((RGB_YUV_TABLE[(r + 1280)>>0] + RGB_YUV_TABLE[(g + 1536)>>0] + RGB_YUV_TABLE[(b + 1792)>>0]) >> 16)-128; + + } + + DCY = processDU(YDU, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCU = processDU(UDU, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = processDU(VDU, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + x+=32; + } + y+=8; + } + + + //////////////////////////////////////////////////////////////// + + // Do the bit alignment of the EOI marker + if ( bytepos >= 0 ) { + var fillbits = []; + fillbits[1] = bytepos+1; + fillbits[0] = (1<<(bytepos+1))-1; + writeBits(fillbits); + } + + writeWord(0xFFD9); //EOI + + var jpegDataUri = 'data:image/jpeg;base64,' + btoa(byteout.join('')); + + byteout = []; + + // benchmarking + // var duration = new Date().getTime() - time_start; + // console.log('Encoding time: '+ currentQuality + 'ms'); + // + + return jpegDataUri + } + + function setQuality(quality){ + if (quality <= 0) { + quality = 1; + } + if (quality > 100) { + quality = 100; + } + + if(currentQuality == quality) return // don't recalc if unchanged + + var sf = 0; + if (quality < 50) { + sf = Math.floor(5000 / quality); + } else { + sf = Math.floor(200 - quality*2); + } + + initQuantTables(sf); + currentQuality = quality; + // console.log('Quality set to: '+quality +'%'); + } + + function init(){ + // var time_start = new Date().getTime(); + if(!quality) quality = 50; + // Create tables + initCharLookupTable() + initHuffmanTbl(); + initCategoryNumber(); + initRGBYUVTable(); + + setQuality(quality); + // var duration = new Date().getTime() - time_start; + // console.log('Initialization '+ duration + 'ms'); + } + + init(); + + }; + + JPEGEncoder.encode = function( data, quality ) { + var encoder = new JPEGEncoder( quality ); + + return encoder.encode( data ); + } + + return JPEGEncoder; + }); + /** + * @fileOverview Fix android canvas.toDataUrl bug. + */ + define('runtime/html5/androidpatch',[ + 'runtime/html5/util', + 'runtime/html5/jpegencoder', + 'base' + ], function( Util, encoder, Base ) { + var origin = Util.canvasToDataUrl, + supportJpeg; + + Util.canvasToDataUrl = function( canvas, type, quality ) { + var ctx, w, h, fragement, parts; + + // 非android手机直接跳过。 + if ( !Base.os.android ) { + return origin.apply( null, arguments ); + } + + // 检测是否canvas支持jpeg导出,根据数据格式来判断。 + // JPEG 前两位分别是:255, 216 + if ( type === 'image/jpeg' && typeof supportJpeg === 'undefined' ) { + fragement = origin.apply( null, arguments ); + + parts = fragement.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + fragement = atob( parts[ 1 ] ); + } else { + fragement = decodeURIComponent( parts[ 1 ] ); + } + + fragement = fragement.substring( 0, 2 ); + + supportJpeg = fragement.charCodeAt( 0 ) === 255 && + fragement.charCodeAt( 1 ) === 216; + } + + // 只有在android环境下才修复 + if ( type === 'image/jpeg' && !supportJpeg ) { + w = canvas.width; + h = canvas.height; + ctx = canvas.getContext('2d'); + + return encoder.encode( ctx.getImageData( 0, 0, w, h ), quality ); + } + + return origin.apply( null, arguments ); + }; + }); + /** + * @fileOverview Image + */ + define('runtime/html5/image',[ + 'base', + 'runtime/html5/runtime', + 'runtime/html5/util' + ], function( Base, Html5Runtime, Util ) { + + var BLANK = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D'; + + return Html5Runtime.register( 'Image', { + + // flag: 标记是否被修改过。 + modified: false, + + init: function() { + var me = this, + img = new Image(); + + img.onload = function() { + + me._info = { + type: me.type, + width: this.width, + height: this.height + }; + + //debugger; + + // 读取meta信息。 + if ( !me._metas && 'image/jpeg' === me.type ) { + Util.parseMeta( me._blob, function( error, ret ) { + me._metas = ret; + me.owner.trigger('load'); + }); + } else { + me.owner.trigger('load'); + } + }; + + img.onerror = function() { + me.owner.trigger('error'); + }; + + me._img = img; + }, + + loadFromBlob: function( blob ) { + var me = this, + img = me._img; + + me._blob = blob; + me.type = blob.type; + img.src = Util.createObjectURL( blob.getSource() ); + me.owner.once( 'load', function() { + Util.revokeObjectURL( img.src ); + }); + }, + + resize: function( width, height ) { + var canvas = this._canvas || + (this._canvas = document.createElement('canvas')); + + this._resize( this._img, canvas, width, height ); + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'resize' ); + }, + + crop: function( x, y, w, h, s ) { + var cvs = this._canvas || + (this._canvas = document.createElement('canvas')), + opts = this.options, + img = this._img, + iw = img.naturalWidth, + ih = img.naturalHeight, + orientation = this.getOrientation(); + + s = s || 1; + + // todo 解决 orientation 的问题。 + // values that require 90 degree rotation + // if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // switch ( orientation ) { + // case 6: + // tmp = x; + // x = y; + // y = iw * s - tmp - w; + // console.log(ih * s, tmp, w) + // break; + // } + + // (w ^= h, h ^= w, w ^= h); + // } + + cvs.width = w; + cvs.height = h; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + this._renderImageToCanvas( cvs, img, -x, -y, iw * s, ih * s ); + + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'crop' ); + }, + + getAsBlob: function( type ) { + var blob = this._blob, + opts = this.options, + canvas; + + type = type || this.type; + + // blob需要重新生成。 + if ( this.modified || this.type !== type ) { + canvas = this._canvas; + + if ( type === 'image/jpeg' ) { + + blob = Util.canvasToDataUrl( canvas, type, opts.quality ); + + if ( opts.preserveHeaders && this._metas && + this._metas.imageHead ) { + + blob = Util.dataURL2ArrayBuffer( blob ); + blob = Util.updateImageHead( blob, + this._metas.imageHead ); + blob = Util.arrayBufferToBlob( blob, type ); + return blob; + } + } else { + blob = Util.canvasToDataUrl( canvas, type ); + } + + blob = Util.dataURL2Blob( blob ); + } + + return blob; + }, + + getAsDataUrl: function( type ) { + var opts = this.options; + + type = type || this.type; + + if ( type === 'image/jpeg' ) { + return Util.canvasToDataUrl( this._canvas, type, opts.quality ); + } else { + return this._canvas.toDataURL( type ); + } + }, + + getOrientation: function() { + return this._metas && this._metas.exif && + this._metas.exif.get('Orientation') || 1; + }, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._metas = val; + return this; + } + + // getter + return this._metas; + }, + + destroy: function() { + var canvas = this._canvas; + this._img.onload = null; + + if ( canvas ) { + canvas.getContext('2d') + .clearRect( 0, 0, canvas.width, canvas.height ); + canvas.width = canvas.height = 0; + this._canvas = null; + } + + // 释放内存。非常重要,否则释放不了image的内存。 + this._img.src = BLANK; + this._img = this._blob = null; + }, + + _resize: function( img, cvs, width, height ) { + var opts = this.options, + naturalWidth = img.width, + naturalHeight = img.height, + orientation = this.getOrientation(), + scale, w, h, x, y; + + // values that require 90 degree rotation + if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // 交换width, height的值。 + width ^= height; + height ^= width; + width ^= height; + } + + scale = Math[ opts.crop ? 'max' : 'min' ]( width / naturalWidth, + height / naturalHeight ); + + // 不允许放大。 + opts.allowMagnify || (scale = Math.min( 1, scale )); + + w = naturalWidth * scale; + h = naturalHeight * scale; + + if ( opts.crop ) { + cvs.width = width; + cvs.height = height; + } else { + cvs.width = w; + cvs.height = h; + } + + x = (cvs.width - w) / 2; + y = (cvs.height - h) / 2; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + + this._renderImageToCanvas( cvs, img, x, y, w, h ); + }, + + _rotate2Orientaion: function( canvas, orientation ) { + var width = canvas.width, + height = canvas.height, + ctx = canvas.getContext('2d'); + + switch ( orientation ) { + case 5: + case 6: + case 7: + case 8: + canvas.width = height; + canvas.height = width; + break; + } + + 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( Math.PI ); + break; + + case 4: // vertical flip + ctx.translate( 0, height ); + ctx.scale( 1, -1 ); + break; + + case 5: // vertical flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.scale( 1, -1 ); + break; + + case 6: // 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( 0, -height ); + break; + + case 7: // horizontal flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( width, -height ); + ctx.scale( -1, 1 ); + break; + + case 8: // 90 rotate left + ctx.rotate( -0.5 * Math.PI ); + ctx.translate( -width, 0 ); + break; + } + }, + + // https://github.com/stomita/ios-imagefile-megapixel/ + // blob/master/src/megapix-image.js + _renderImageToCanvas: (function() { + + // 如果不是ios, 不需要这么复杂! + if ( !Base.os.ios ) { + return function( canvas ) { + var args = Base.slice( arguments, 1 ), + ctx = canvas.getContext('2d'); + + ctx.drawImage.apply( ctx, args ); + }; + } + + /** + * Detecting vertical squash in loaded image. + * Fixes a bug which squash image vertically while drawing into + * canvas for some images. + */ + function detectVerticalSquash( img, iw, ih ) { + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'), + sy = 0, + ey = ih, + py = ih, + data, alpha, ratio; + + + canvas.width = 1; + canvas.height = ih; + ctx.drawImage( img, 0, 0 ); + data = ctx.getImageData( 0, 0, 1, ih ).data; + + // search image edge pixel position in case + // it is squashed vertically. + while ( py > sy ) { + alpha = data[ (py - 1) * 4 + 3 ]; + + if ( alpha === 0 ) { + ey = py; + } else { + sy = py; + } + + py = (ey + sy) >> 1; + } + + ratio = (py / ih); + return (ratio === 0) ? 1 : ratio; + } + + // fix ie7 bug + // http://stackoverflow.com/questions/11929099/ + // html5-canvas-drawimage-ratio-bug-ios + if ( Base.os.ios >= 7 ) { + return function( canvas, img, x, y, w, h ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + vertSquashRatio = detectVerticalSquash( img, iw, ih ); + + return canvas.getContext('2d').drawImage( img, 0, 0, + iw * vertSquashRatio, ih * vertSquashRatio, + x, y, w, h ); + }; + } + + /** + * Detect subsampling in loaded image. + * In iOS, larger images than 2M pixels may be + * subsampled in rendering. + */ + function detectSubsampling( img ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + canvas, ctx; + + // subsampling may happen overmegapixel image + if ( iw * ih > 1024 * 1024 ) { + canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + ctx = canvas.getContext('2d'); + ctx.drawImage( img, -iw + 1, 0 ); + + // subsampled image becomes half smaller in rendering size. + // check alpha channel value to confirm image is covering + // edge pixel or not. if alpha value is 0 + // image is not covering, hence subsampled. + return ctx.getImageData( 0, 0, 1, 1 ).data[ 3 ] === 0; + } else { + return false; + } + } + + + return function( canvas, img, x, y, width, height ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + ctx = canvas.getContext('2d'), + subsampled = detectSubsampling( img ), + doSquash = this.type === 'image/jpeg', + d = 1024, + sy = 0, + dy = 0, + tmpCanvas, tmpCtx, vertSquashRatio, dw, dh, sx, dx; + + if ( subsampled ) { + iw /= 2; + ih /= 2; + } + + ctx.save(); + tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = tmpCanvas.height = d; + + tmpCtx = tmpCanvas.getContext('2d'); + vertSquashRatio = doSquash ? + detectVerticalSquash( img, iw, ih ) : 1; + + dw = Math.ceil( d * width / iw ); + dh = Math.ceil( d * height / ih / vertSquashRatio ); + + while ( sy < ih ) { + sx = 0; + dx = 0; + while ( sx < iw ) { + tmpCtx.clearRect( 0, 0, d, d ); + tmpCtx.drawImage( img, -sx, -sy ); + ctx.drawImage( tmpCanvas, 0, 0, d, d, + x + dx, y + dy, dw, dh ); + sx += d; + dx += dw; + } + sy += d; + dy += dh; + } + ctx.restore(); + tmpCanvas = tmpCtx = null; + }; + })() + }); + }); + + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/html5/md5',[ + 'runtime/html5/runtime' + ], function( FlashRuntime ) { + + /* + * Fastest md5 implementation around (JKM md5) + * Credits: Joseph Myers + * + * @see http://www.myersdaily.org/joseph/javascript/md5-text.html + * @see http://jsperf.com/md5-shootout/7 + */ + + /* this function is much faster, + so if possible we use it. Some IEs + are the only ones I know of that + need the idiotic second function, + generated by an if clause. */ + var add32 = function (a, b) { + return (a + b) & 0xFFFFFFFF; + }, + + cmn = function (q, a, b, x, s, t) { + a = add32(add32(a, q), add32(x, t)); + return add32((a << s) | (a >>> (32 - s)), b); + }, + + ff = function (a, b, c, d, x, s, t) { + return cmn((b & c) | ((~b) & d), a, b, x, s, t); + }, + + gg = function (a, b, c, d, x, s, t) { + return cmn((b & d) | (c & (~d)), a, b, x, s, t); + }, + + hh = function (a, b, c, d, x, s, t) { + return cmn(b ^ c ^ d, a, b, x, s, t); + }, + + ii = function (a, b, c, d, x, s, t) { + return cmn(c ^ (b | (~d)), a, b, x, s, t); + }, + + md5cycle = function (x, k) { + var a = x[0], + b = x[1], + c = x[2], + d = x[3]; + + a = ff(a, b, c, d, k[0], 7, -680876936); + d = ff(d, a, b, c, k[1], 12, -389564586); + c = ff(c, d, a, b, k[2], 17, 606105819); + b = ff(b, c, d, a, k[3], 22, -1044525330); + a = ff(a, b, c, d, k[4], 7, -176418897); + d = ff(d, a, b, c, k[5], 12, 1200080426); + c = ff(c, d, a, b, k[6], 17, -1473231341); + b = ff(b, c, d, a, k[7], 22, -45705983); + a = ff(a, b, c, d, k[8], 7, 1770035416); + d = ff(d, a, b, c, k[9], 12, -1958414417); + c = ff(c, d, a, b, k[10], 17, -42063); + b = ff(b, c, d, a, k[11], 22, -1990404162); + a = ff(a, b, c, d, k[12], 7, 1804603682); + d = ff(d, a, b, c, k[13], 12, -40341101); + c = ff(c, d, a, b, k[14], 17, -1502002290); + b = ff(b, c, d, a, k[15], 22, 1236535329); + + a = gg(a, b, c, d, k[1], 5, -165796510); + d = gg(d, a, b, c, k[6], 9, -1069501632); + c = gg(c, d, a, b, k[11], 14, 643717713); + b = gg(b, c, d, a, k[0], 20, -373897302); + a = gg(a, b, c, d, k[5], 5, -701558691); + d = gg(d, a, b, c, k[10], 9, 38016083); + c = gg(c, d, a, b, k[15], 14, -660478335); + b = gg(b, c, d, a, k[4], 20, -405537848); + a = gg(a, b, c, d, k[9], 5, 568446438); + d = gg(d, a, b, c, k[14], 9, -1019803690); + c = gg(c, d, a, b, k[3], 14, -187363961); + b = gg(b, c, d, a, k[8], 20, 1163531501); + a = gg(a, b, c, d, k[13], 5, -1444681467); + d = gg(d, a, b, c, k[2], 9, -51403784); + c = gg(c, d, a, b, k[7], 14, 1735328473); + b = gg(b, c, d, a, k[12], 20, -1926607734); + + a = hh(a, b, c, d, k[5], 4, -378558); + d = hh(d, a, b, c, k[8], 11, -2022574463); + c = hh(c, d, a, b, k[11], 16, 1839030562); + b = hh(b, c, d, a, k[14], 23, -35309556); + a = hh(a, b, c, d, k[1], 4, -1530992060); + d = hh(d, a, b, c, k[4], 11, 1272893353); + c = hh(c, d, a, b, k[7], 16, -155497632); + b = hh(b, c, d, a, k[10], 23, -1094730640); + a = hh(a, b, c, d, k[13], 4, 681279174); + d = hh(d, a, b, c, k[0], 11, -358537222); + c = hh(c, d, a, b, k[3], 16, -722521979); + b = hh(b, c, d, a, k[6], 23, 76029189); + a = hh(a, b, c, d, k[9], 4, -640364487); + d = hh(d, a, b, c, k[12], 11, -421815835); + c = hh(c, d, a, b, k[15], 16, 530742520); + b = hh(b, c, d, a, k[2], 23, -995338651); + + a = ii(a, b, c, d, k[0], 6, -198630844); + d = ii(d, a, b, c, k[7], 10, 1126891415); + c = ii(c, d, a, b, k[14], 15, -1416354905); + b = ii(b, c, d, a, k[5], 21, -57434055); + a = ii(a, b, c, d, k[12], 6, 1700485571); + d = ii(d, a, b, c, k[3], 10, -1894986606); + c = ii(c, d, a, b, k[10], 15, -1051523); + b = ii(b, c, d, a, k[1], 21, -2054922799); + a = ii(a, b, c, d, k[8], 6, 1873313359); + d = ii(d, a, b, c, k[15], 10, -30611744); + c = ii(c, d, a, b, k[6], 15, -1560198380); + b = ii(b, c, d, a, k[13], 21, 1309151649); + a = ii(a, b, c, d, k[4], 6, -145523070); + d = ii(d, a, b, c, k[11], 10, -1120210379); + c = ii(c, d, a, b, k[2], 15, 718787259); + b = ii(b, c, d, a, k[9], 21, -343485551); + + x[0] = add32(a, x[0]); + x[1] = add32(b, x[1]); + x[2] = add32(c, x[2]); + x[3] = add32(d, x[3]); + }, + + /* there needs to be support for Unicode here, + * unless we pretend that we can redefine the MD-5 + * algorithm for multi-byte characters (perhaps + * by adding every four 16-bit characters and + * shortening the sum to 32 bits). Otherwise + * I suggest performing MD-5 as if every character + * was two bytes--e.g., 0040 0025 = @%--but then + * how will an ordinary MD-5 sum be matched? + * There is no way to standardize text to something + * like UTF-8 before transformation; speed cost is + * utterly prohibitive. The JavaScript standard + * itself needs to look at this: it should start + * providing access to strings as preformed UTF-8 + * 8-bit unsigned value arrays. + */ + md5blk = function (s) { + var md5blks = [], + i; /* Andy King said do it this way. */ + + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24); + } + return md5blks; + }, + + md5blk_array = function (a) { + var md5blks = [], + i; /* Andy King said do it this way. */ + + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = a[i] + (a[i + 1] << 8) + (a[i + 2] << 16) + (a[i + 3] << 24); + } + return md5blks; + }, + + md51 = function (s) { + var n = s.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i, + length, + tail, + tmp, + lo, + hi; + + for (i = 64; i <= n; i += 64) { + md5cycle(state, md5blk(s.substring(i - 64, i))); + } + s = s.substring(i - 64); + length = s.length; + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); + } + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Beware that the final length might not fit in 32 bits so we take care of that + tmp = n * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + + md5cycle(state, tail); + return state; + }, + + md51_array = function (a) { + var n = a.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i, + length, + tail, + tmp, + lo, + hi; + + for (i = 64; i <= n; i += 64) { + md5cycle(state, md5blk_array(a.subarray(i - 64, i))); + } + + // Not sure if it is a bug, however IE10 will always produce a sub array of length 1 + // containing the last element of the parent array if the sub array specified starts + // beyond the length of the parent array - weird. + // https://connect.microsoft.com/IE/feedback/details/771452/typed-array-subarray-issue + a = (i - 64) < n ? a.subarray(i - 64) : new Uint8Array(0); + + length = a.length; + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= a[i] << ((i % 4) << 3); + } + + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Beware that the final length might not fit in 32 bits so we take care of that + tmp = n * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + + md5cycle(state, tail); + + return state; + }, + + hex_chr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'], + + rhex = function (n) { + var s = '', + j; + for (j = 0; j < 4; j += 1) { + s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F]; + } + return s; + }, + + hex = function (x) { + var i; + for (i = 0; i < x.length; i += 1) { + x[i] = rhex(x[i]); + } + return x.join(''); + }, + + md5 = function (s) { + return hex(md51(s)); + }, + + + + //////////////////////////////////////////////////////////////////////////// + + /** + * SparkMD5 OOP implementation. + * + * Use this class to perform an incremental md5, otherwise use the + * static methods instead. + */ + SparkMD5 = function () { + // call reset to init the instance + this.reset(); + }; + + + // In some cases the fast add32 function cannot be used.. + if (md5('hello') !== '5d41402abc4b2a76b9719d911017c592') { + add32 = function (x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF), + msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + }; + } + + + /** + * Appends a string. + * A conversion will be applied if an utf8 string is detected. + * + * @param {String} str The string to be appended + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.append = function (str) { + // converts the string to utf8 bytes if necessary + if (/[\u0080-\uFFFF]/.test(str)) { + str = unescape(encodeURIComponent(str)); + } + + // then append as binary + this.appendBinary(str); + + return this; + }; + + /** + * Appends a binary string. + * + * @param {String} contents The binary string to be appended + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.appendBinary = function (contents) { + this._buff += contents; + this._length += contents.length; + + var length = this._buff.length, + i; + + for (i = 64; i <= length; i += 64) { + md5cycle(this._state, md5blk(this._buff.substring(i - 64, i))); + } + + this._buff = this._buff.substr(i - 64); + + return this; + }; + + /** + * Finishes the incremental computation, reseting the internal state and + * returning the result. + * Use the raw parameter to obtain the raw result instead of the hex one. + * + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.prototype.end = function (raw) { + var buff = this._buff, + length = buff.length, + i, + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ret; + + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= buff.charCodeAt(i) << ((i % 4) << 3); + } + + this._finish(tail, length); + ret = !!raw ? this._state : hex(this._state); + + this.reset(); + + return ret; + }; + + /** + * Finish the final calculation based on the tail. + * + * @param {Array} tail The tail (will be modified) + * @param {Number} length The length of the remaining buffer + */ + SparkMD5.prototype._finish = function (tail, length) { + var i = length, + tmp, + lo, + hi; + + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(this._state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Do the final computation based on the tail and length + // Beware that the final length may not fit in 32 bits so we take care of that + tmp = this._length * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + md5cycle(this._state, tail); + }; + + /** + * Resets the internal state of the computation. + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.reset = function () { + this._buff = ""; + this._length = 0; + this._state = [1732584193, -271733879, -1732584194, 271733878]; + + return this; + }; + + /** + * Releases memory used by the incremental buffer and other aditional + * resources. If you plan to use the instance again, use reset instead. + */ + SparkMD5.prototype.destroy = function () { + delete this._state; + delete this._buff; + delete this._length; + }; + + + /** + * Performs the md5 hash on a string. + * A conversion will be applied if utf8 string is detected. + * + * @param {String} str The string + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.hash = function (str, raw) { + // converts the string to utf8 bytes if necessary + if (/[\u0080-\uFFFF]/.test(str)) { + str = unescape(encodeURIComponent(str)); + } + + var hash = md51(str); + + return !!raw ? hash : hex(hash); + }; + + /** + * Performs the md5 hash on a binary string. + * + * @param {String} content The binary string + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.hashBinary = function (content, raw) { + var hash = md51(content); + + return !!raw ? hash : hex(hash); + }; + + /** + * SparkMD5 OOP implementation for array buffers. + * + * Use this class to perform an incremental md5 ONLY for array buffers. + */ + SparkMD5.ArrayBuffer = function () { + // call reset to init the instance + this.reset(); + }; + + //////////////////////////////////////////////////////////////////////////// + + /** + * Appends an array buffer. + * + * @param {ArrayBuffer} arr The array to be appended + * + * @return {SparkMD5.ArrayBuffer} The instance itself + */ + SparkMD5.ArrayBuffer.prototype.append = function (arr) { + // TODO: we could avoid the concatenation here but the algorithm would be more complex + // if you find yourself needing extra performance, please make a PR. + var buff = this._concatArrayBuffer(this._buff, arr), + length = buff.length, + i; + + this._length += arr.byteLength; + + for (i = 64; i <= length; i += 64) { + md5cycle(this._state, md5blk_array(buff.subarray(i - 64, i))); + } + + // Avoids IE10 weirdness (documented above) + this._buff = (i - 64) < length ? buff.subarray(i - 64) : new Uint8Array(0); + + return this; + }; + + /** + * Finishes the incremental computation, reseting the internal state and + * returning the result. + * Use the raw parameter to obtain the raw result instead of the hex one. + * + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.ArrayBuffer.prototype.end = function (raw) { + var buff = this._buff, + length = buff.length, + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + i, + ret; + + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= buff[i] << ((i % 4) << 3); + } + + this._finish(tail, length); + ret = !!raw ? this._state : hex(this._state); + + this.reset(); + + return ret; + }; + + SparkMD5.ArrayBuffer.prototype._finish = SparkMD5.prototype._finish; + + /** + * Resets the internal state of the computation. + * + * @return {SparkMD5.ArrayBuffer} The instance itself + */ + SparkMD5.ArrayBuffer.prototype.reset = function () { + this._buff = new Uint8Array(0); + this._length = 0; + this._state = [1732584193, -271733879, -1732584194, 271733878]; + + return this; + }; + + /** + * Releases memory used by the incremental buffer and other aditional + * resources. If you plan to use the instance again, use reset instead. + */ + SparkMD5.ArrayBuffer.prototype.destroy = SparkMD5.prototype.destroy; + + /** + * Concats two array buffers, returning a new one. + * + * @param {ArrayBuffer} first The first array buffer + * @param {ArrayBuffer} second The second array buffer + * + * @return {ArrayBuffer} The new array buffer + */ + SparkMD5.ArrayBuffer.prototype._concatArrayBuffer = function (first, second) { + var firstLength = first.length, + result = new Uint8Array(firstLength + second.byteLength); + + result.set(first); + result.set(new Uint8Array(second), firstLength); + + return result; + }; + + /** + * Performs the md5 hash on an array buffer. + * + * @param {ArrayBuffer} arr The array buffer + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.ArrayBuffer.hash = function (arr, raw) { + var hash = md51_array(new Uint8Array(arr)); + + return !!raw ? hash : hex(hash); + }; + + return FlashRuntime.register( 'Md5', { + init: function() { + // do nothing. + }, + + loadFromBlob: function( file ) { + var blob = file.getSource(), + chunkSize = 2 * 1024 * 1024, + chunks = Math.ceil( blob.size / chunkSize ), + chunk = 0, + owner = this.owner, + spark = new SparkMD5.ArrayBuffer(), + me = this, + blobSlice = blob.mozSlice || blob.webkitSlice || blob.slice, + loadNext, fr; + + fr = new FileReader(); + + loadNext = function() { + var start, end; + + start = chunk * chunkSize; + end = Math.min( start + chunkSize, blob.size ); + + fr.onload = function( e ) { + spark.append( e.target.result ); + owner.trigger( 'progress', { + total: file.size, + loaded: end + }); + }; + + fr.onloadend = function() { + fr.onloadend = fr.onload = null; + + if ( ++chunk < chunks ) { + setTimeout( loadNext, 1 ); + } else { + setTimeout(function(){ + owner.trigger('load'); + me.result = spark.end(); + loadNext = file = blob = spark = null; + owner.trigger('complete'); + }, 50 ); + } + }; + + fr.readAsArrayBuffer( blobSlice.call( blob, start, end ) ); + }; + + loadNext(); + }, + + getResult: function() { + return this.result; + } + }); + }); + /** + * @fileOverview FlashRuntime + */ + define('runtime/flash/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var $ = Base.$, + type = 'flash', + components = {}; + + + function getFlashVersion() { + var version; + + try { + version = navigator.plugins[ 'Shockwave Flash' ]; + version = version.description; + } catch ( ex ) { + try { + version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash') + .GetVariable('$version'); + } catch ( ex2 ) { + version = '0.0'; + } + } + version = version.match( /\d+/g ); + return parseFloat( version[ 0 ] + '.' + version[ 1 ], 10 ); + } + + function FlashRuntime() { + var pool = {}, + clients = {}, + destroy = this.destroy, + me = this, + jsreciver = Base.guid('webuploader_'); + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/ ) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + clients[ uid ] = client; + + if ( components[ comp ] ) { + if ( !pool[ uid ] ) { + pool[ uid ] = new components[ comp ]( client, me ); + } + + instance = pool[ uid ]; + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + + return me.flashExec.apply( client, arguments ); + }; + + function handler( evt, obj ) { + var type = evt.type || evt, + parts, uid; + + parts = type.split('::'); + uid = parts[ 0 ]; + type = parts[ 1 ]; + + // console.log.apply( console, arguments ); + + if ( type === 'Ready' && uid === me.uid ) { + me.trigger('ready'); + } else if ( clients[ uid ] ) { + clients[ uid ].trigger( type.toLowerCase(), evt, obj ); + } + + // Base.log( evt, obj ); + } + + // flash的接受器。 + window[ jsreciver ] = function() { + var args = arguments; + + // 为了能捕获得到。 + setTimeout(function() { + handler.apply( null, args ); + }, 1 ); + }; + + this.jsreciver = jsreciver; + + this.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + + this.flashExec = function( comp, fn ) { + var flash = me.getFlash(), + args = Base.slice( arguments, 2 ); + + return flash.exec( this.uid, comp, fn, args ); + }; + + // @todo + } + + Base.inherits( Runtime, { + constructor: FlashRuntime, + + init: function() { + var container = this.getContainer(), + opts = this.options, + html; + + // 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) + container.css({ + position: 'absolute', + top: '-8px', + left: '-8px', + width: '9px', + height: '9px', + overflow: 'hidden' + }); + + // insert flash object + html = '' + + '' + + '' + + '' + + ''; + + container.html( html ); + }, + + getFlash: function() { + if ( this._flash ) { + return this._flash; + } + + this._flash = $( '#' + this.uid ).get( 0 ); + return this._flash; + } + + }); + + FlashRuntime.register = function( name, component ) { + component = components[ name ] = Base.inherits( CompBase, $.extend({ + + // @todo fix this later + flashExec: function() { + var owner = this.owner, + runtime = this.getRuntime(); + + return runtime.flashExec.apply( owner, arguments ); + } + }, component ) ); + + return component; + }; + + if ( getFlashVersion() >= 11.4 ) { + Runtime.addRuntime( type, FlashRuntime ); + } + + return FlashRuntime; + }); + /** + * @fileOverview FilePicker + */ + define('runtime/flash/filepicker',[ + 'base', + 'runtime/flash/runtime' + ], function( Base, FlashRuntime ) { + var $ = Base.$; + + return FlashRuntime.register( 'FilePicker', { + init: function( opts ) { + var copy = $.extend({}, opts ), + len, i; + + // 修复Flash再没有设置title的情况下无法弹出flash文件选择框的bug. + len = copy.accept && copy.accept.length; + for ( i = 0; i < len; i++ ) { + if ( !copy.accept[ i ].title ) { + copy.accept[ i ].title = 'Files'; + } + } + + delete copy.button; + delete copy.id; + delete copy.container; + + this.flashExec( 'FilePicker', 'init', copy ); + }, + + destroy: function() { + this.flashExec( 'FilePicker', 'destroy' ); + } + }); + }); + /** + * @fileOverview 图片压缩 + */ + define('runtime/flash/image',[ + 'runtime/flash/runtime' + ], function( FlashRuntime ) { + + return FlashRuntime.register( 'Image', { + // init: function( options ) { + // var owner = this.owner; + + // this.flashExec( 'Image', 'init', options ); + // owner.on( 'load', function() { + // debugger; + // }); + // }, + + loadFromBlob: function( blob ) { + var owner = this.owner; + + owner.info() && this.flashExec( 'Image', 'info', owner.info() ); + owner.meta() && this.flashExec( 'Image', 'meta', owner.meta() ); + + this.flashExec( 'Image', 'loadFromBlob', blob.uid ); + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/flash/transport',[ + 'base', + 'runtime/flash/runtime', + 'runtime/client' + ], function( Base, FlashRuntime, RuntimeClient ) { + var $ = Base.$; + + return FlashRuntime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + this._responseJson = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + binary; + + xhr.connectRuntime( blob.ruid ); + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.uid; + } else { + $.each( owner._formData, function( k, v ) { + xhr.exec( 'append', k, v ); + }); + + xhr.exec( 'appendBlob', opts.fileVal, blob.uid, + opts.filename || owner._formData.name || '' ); + } + + this._setRequestHeader( xhr, opts.headers ); + xhr.exec( 'send', { + method: opts.method, + url: server, + forceURLStream: opts.forceURLStream, + mimeType: 'application/octet-stream' + }, binary ); + }, + + getStatus: function() { + return this._status; + }, + + getResponse: function() { + return this._response || ''; + }, + + getResponseAsJson: function() { + return this._responseJson; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.exec('abort'); + xhr.destroy(); + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new RuntimeClient('XMLHttpRequest'); + + xhr.on( 'uploadprogress progress', function( e ) { + var percent = e.loaded / e.total; + percent = Math.min( 1, Math.max( 0, percent ) ); + return me.trigger( 'progress', percent ); + }); + + xhr.on( 'load', function() { + var status = xhr.exec('getStatus'), + readBody = false, + err = '', + p; + + xhr.off(); + me._xhr = null; + + if ( status >= 200 && status < 300 ) { + readBody = true; + } else if ( status >= 500 && status < 600 ) { + readBody = true; + err = 'server'; + } else { + err = 'http'; + } + + if ( readBody ) { + me._response = xhr.exec('getResponse'); + me._response = decodeURIComponent( me._response ); + + // flash 处理可能存在 bug, 没辙只能靠 js 了 + // try { + // me._responseJson = xhr.exec('getResponseAsJson'); + // } catch ( error ) { + + p = function( s ) { + try { + if (window.JSON && window.JSON.parse) { + return JSON.parse(s); + } + + return new Function('return ' + s).call(); + } catch ( err ) { + return {}; + } + }; + me._responseJson = me._response ? p(me._response) : {}; + + // } + } + + xhr.destroy(); + xhr = null; + + return err ? me.trigger( 'error', err ) : me.trigger('load'); + }); + + xhr.on( 'error', function() { + xhr.off(); + me._xhr = null; + me.trigger( 'error', 'http' ); + }); + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.exec( 'setRequestHeader', key, val ); + }); + } + }); + }); + + /** + * @fileOverview Blob Html实现 + */ + define('runtime/flash/blob',[ + 'runtime/flash/runtime', + 'lib/blob' + ], function( FlashRuntime, Blob ) { + + return FlashRuntime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.flashExec( 'Blob', 'slice', start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview Md5 flash实现 + */ + define('runtime/flash/md5',[ + 'runtime/flash/runtime' + ], function( FlashRuntime ) { + + return FlashRuntime.register( 'Md5', { + init: function() { + // do nothing. + }, + + loadFromBlob: function( blob ) { + return this.flashExec( 'Md5', 'loadFromBlob', blob.uid ); + } + }); + }); + /** + * @fileOverview 完全版本。 + */ + define('preset/all',[ + 'base', + + // widgets + 'widgets/filednd', + 'widgets/filepaste', + 'widgets/filepicker', + 'widgets/image', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + 'widgets/md5', + + // runtimes + // html5 + 'runtime/html5/blob', + 'runtime/html5/dnd', + 'runtime/html5/filepaste', + 'runtime/html5/filepicker', + 'runtime/html5/imagemeta/exif', + 'runtime/html5/androidpatch', + 'runtime/html5/image', + 'runtime/html5/transport', + 'runtime/html5/md5', + + // flash + 'runtime/flash/filepicker', + 'runtime/flash/image', + 'runtime/flash/transport', + 'runtime/flash/blob', + 'runtime/flash/md5' + ], function( Base ) { + return Base; + }); + /** + * @fileOverview 日志组件,主要用来收集错误信息,可以帮助 webuploader 更好的定位问题和发展。 + * + * 如果您不想要启用此功能,请在打包的时候去掉 log 模块。 + * + * 或者可以在初始化的时候通过 options.disableWidgets 属性禁用。 + * + * 如: + * WebUploader.create({ + * ... + * + * disableWidgets: 'log', + * + * ... + * }) + */ + define('widgets/log',[ + 'base', + 'uploader', + 'widgets/widget' + ], function( Base, Uploader ) { + var $ = Base.$, + logUrl = ' http://static.tieba.baidu.com/tb/pms/img/st.gif??', + product = (location.hostname || location.host || 'protected').toLowerCase(), + + // 只针对 baidu 内部产品用户做统计功能。 + enable = product && /baidu/i.exec(product), + base; + + if (!enable) { + return; + } + + base = { + dv: 3, + master: 'webuploader', + online: /test/.exec(product) ? 0 : 1, + module: '', + product: product, + type: 0 + }; + + function send(data) { + var obj = $.extend({}, base, data), + url = logUrl.replace(/^(.*)\?/, '$1' + $.param( obj )), + image = new Image(); + + image.src = url; + } + + return Uploader.register({ + name: 'log', + + init: function() { + var owner = this.owner, + count = 0, + size = 0; + + owner + .on('error', function(code) { + send({ + type: 2, + c_error_code: code + }); + }) + .on('uploadError', function(file, reason) { + send({ + type: 2, + c_error_code: 'UPLOAD_ERROR', + c_reason: '' + reason + }); + }) + .on('uploadComplete', function(file) { + count++; + size += file.size; + }). + on('uploadFinished', function() { + send({ + c_count: count, + c_size: size + }); + count = size = 0; + }); + + send({ + c_usage: 1 + }); + } + }); + }); + /** + * @fileOverview Uploader上传类 + */ + define('webuploader',[ + 'preset/all', + 'widgets/log' + ], function( preset ) { + return preset; + }); + + var _require = require; + return _require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.flashonly.js b/public/static/libs/webuploader/webuploader.flashonly.js new file mode 100644 index 0000000..460af87 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.flashonly.js @@ -0,0 +1,4648 @@ +/*! WebUploader 0.1.6 */ + + +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + * @require "jquery" + * @require "zepto" + */ + define('dollar-third',[],function() { + var req = window.require; + var $ = window.__dollar || + window.jQuery || + window.Zepto || + req('jquery') || + req('zepto'); + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 使用jQuery的Promise + */ + define('promise-third',[ + 'dollar' + ], function( $ ) { + return { + Deferred: $.Deferred, + when: $.when, + + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + } + }; + }); + /** + * @fileOverview Promise/A+ + */ + define('promise',[ + 'promise-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.6', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClient, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClient.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file', + style: 'webuploader-pick' //pick element class attribute, default is "webuploader-pick" + }; + + Base.inherits( RuntimeClient, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button, + style = opts.style; + + if (style) + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + if (style) + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + if (style) + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addBtn + * @for Uploader + * @grammar addBtn( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addBtn({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.on('dialogopen', function() { + me.owner.trigger('dialogOpen', picker.button); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview Image + */ + define('lib/image',[ + 'base', + 'runtime/client', + 'lib/blob' + ], function( Base, RuntimeClient, Blob ) { + var $ = Base.$; + + // 构造器。 + function Image( opts ) { + this.options = $.extend({}, Image.options, opts ); + RuntimeClient.call( this, 'Image' ); + + this.on( 'load', function() { + this._info = this.exec('info'); + this._meta = this.exec('meta'); + }); + } + + // 默认选项。 + Image.options = { + + // 默认的图片处理质量 + quality: 90, + + // 是否裁剪 + crop: false, + + // 是否保留头部信息 + preserveHeaders: false, + + // 是否允许放大。 + allowMagnify: false + }; + + // 继承RuntimeClient. + Base.inherits( RuntimeClient, { + constructor: Image, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + loadFromBlob: function( blob ) { + var me = this, + ruid = blob.getRuid(); + + this.connectRuntime( ruid, function() { + me.exec( 'init', me.options ); + me.exec( 'loadFromBlob', blob ); + }); + }, + + resize: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'resize' ].concat( args ) ); + }, + + crop: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'crop' ].concat( args ) ); + }, + + getAsDataUrl: function( type ) { + return this.exec( 'getAsDataUrl', type ); + }, + + getAsBlob: function( type ) { + var blob = this.exec( 'getAsBlob', type ); + + return new Blob( this.getRuid(), blob ); + } + }); + + return Image; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/image',[ + 'base', + 'uploader', + 'lib/image', + 'widgets/widget' + ], function( Base, Uploader, Image ) { + + var $ = Base.$, + throttle; + + // 根据要处理的文件大小来节流,一次不能处理太多,会卡。 + throttle = (function( max ) { + var occupied = 0, + waiting = [], + tick = function() { + var item; + + while ( waiting.length && occupied < max ) { + item = waiting.shift(); + occupied += item[ 0 ]; + item[ 1 ](); + } + }; + + return function( emiter, size, cb ) { + waiting.push([ size, cb ]); + emiter.once( 'destroy', function() { + occupied -= size; + setTimeout( tick, 1 ); + }); + setTimeout( tick, 1 ); + }; + })( 5 * 1024 * 1024 ); + + $.extend( Uploader.options, { + + /** + * @property {Object} [thumb] + * @namespace options + * @for Uploader + * @description 配置生成缩略图的选项。 + * + * 默认为: + * + * ```javascript + * { + * width: 110, + * height: 110, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 70, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: true, + * + * // 是否允许裁剪。 + * crop: true, + * + * // 为空的话则保留原有图片格式。 + * // 否则强制转换成指定的类型。 + * type: 'image/jpeg' + * } + * ``` + */ + thumb: { + width: 110, + height: 110, + quality: 70, + allowMagnify: true, + crop: true, + preserveHeaders: false, + + // 为空的话则保留原有图片格式。 + // 否则强制转换成指定的类型。 + // IE 8下面 base64 大小不能超过 32K 否则预览失败,而非 jpeg 编码的图片很可 + // 能会超过 32k, 所以这里设置成预览的时候都是 image/jpeg + type: 'image/jpeg' + }, + + /** + * @property {Object} [compress] + * @namespace options + * @for Uploader + * @description 配置压缩的图片的选项。如果此选项为`false`, 则图片在上传前不进行压缩。 + * + * 默认为: + * + * ```javascript + * { + * width: 1600, + * height: 1600, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 90, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: false, + * + * // 是否允许裁剪。 + * crop: false, + * + * // 是否保留头部meta信息。 + * preserveHeaders: true, + * + * // 如果发现压缩后文件大小比原来还大,则使用原来图片 + * // 此属性可能会影响图片自动纠正功能 + * noCompressIfLarger: false, + * + * // 单位字节,如果图片大小小于此值,不会采用压缩。 + * compressSize: 0 + * } + * ``` + */ + compress: { + width: 1600, + height: 1600, + quality: 90, + allowMagnify: false, + crop: false, + preserveHeaders: true + } + }); + + return Uploader.register({ + + name: 'image', + + + /** + * 生成缩略图,此过程为异步,所以需要传入`callback`。 + * 通常情况在图片加入队里后调用此方法来生成预览图以增强交互效果。 + * + * 当 width 或者 height 的值介于 0 - 1 时,被当成百分比使用。 + * + * `callback`中可以接收到两个参数。 + * * 第一个为error,如果生成缩略图有错误,此error将为真。 + * * 第二个为ret, 缩略图的Data URL值。 + * + * **注意** + * Date URL在IE6/7中不支持,所以不用调用此方法了,直接显示一张暂不支持预览图片好了。 + * 也可以借助服务端,将 base64 数据传给服务端,生成一个临时文件供预览。 + * + * @method makeThumb + * @grammar makeThumb( file, callback ) => undefined + * @grammar makeThumb( file, callback, width, height ) => undefined + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.makeThumb( file, function( error, ret ) { + * if ( error ) { + * $li.text('预览错误'); + * } else { + * $li.append(''); + * } + * }); + * + * }); + */ + makeThumb: function( file, cb, width, height ) { + var opts, image; + + file = this.request( 'get-file', file ); + + // 只预览图片格式。 + if ( !file.type.match( /^image/ ) ) { + cb( true ); + return; + } + + opts = $.extend({}, this.options.thumb ); + + // 如果传入的是object. + if ( $.isPlainObject( width ) ) { + opts = $.extend( opts, width ); + width = null; + } + + width = width || opts.width; + height = height || opts.height; + + image = new Image( opts ); + + image.once( 'load', function() { + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + // 当 resize 完后 + image.once( 'complete', function() { + cb( false, image.getAsDataUrl( opts.type ) ); + image.destroy(); + }); + + image.once( 'error', function( reason ) { + cb( reason || true ); + image.destroy(); + }); + + throttle( image, file.source.size, function() { + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + image.loadFromBlob( file.source ); + }); + }, + + beforeSendFile: function( file ) { + var opts = this.options.compress || this.options.resize, + compressSize = opts && opts.compressSize || 0, + noCompressIfLarger = opts && opts.noCompressIfLarger || false, + image, deferred; + + file = this.request( 'get-file', file ); + + // 只压缩 jpeg 图片格式。 + // gif 可能会丢失针 + // bmp png 基本上尺寸都不大,且压缩比比较小。 + if ( !opts || !~'image/jpeg,image/jpg'.indexOf( file.type ) || + file.size < compressSize || + file._compressed ) { + return; + } + + opts = $.extend({}, opts ); + deferred = Base.Deferred(); + + image = new Image( opts ); + + deferred.always(function() { + image.destroy(); + image = null; + }); + image.once( 'error', deferred.reject ); + image.once( 'load', function() { + var width = opts.width, + height = opts.height; + + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + image.once( 'complete', function() { + var blob, size; + + // 移动端 UC / qq 浏览器的无图模式下 + // ctx.getImageData 处理大图的时候会报 Exception + // INDEX_SIZE_ERR: DOM Exception 1 + try { + blob = image.getAsBlob( opts.type ); + + size = file.size; + + // 如果压缩后,比原来还大则不用压缩后的。 + if ( !noCompressIfLarger || blob.size < size ) { + // file.source.destroy && file.source.destroy(); + file.source = blob; + file.size = blob.size; + + file.trigger( 'resize', blob.size, size ); + } + + // 标记,避免重复压缩。 + file._compressed = true; + deferred.resolve(); + } catch ( e ) { + // 出错了直接继续,让其上传原始图片 + deferred.resolve(); + } + }); + + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + + image.loadFromBlob( file.source ); + return deferred.promise(); + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + if ( files.length ) { + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + // 销毁上传相关的属性。 + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定的文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + file.setStatus( Status.QUEUED ); + + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + file.setStatus( Status.PROGRESS ); + }); + + + } else if (file.getStatus() !== Status.PROGRESS) { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return Base.nextTick( me.__tick ); + } + + me.runing = true; + var files = []; + + // 如果有暂停的,则续传 + file || $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + me._trigged = false; + files.push(file); + v.transport && v.transport.send(); + } + }); + + $.each(files, function() { + this.setStatus( Status.PROGRESS ); + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this, + block; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + + + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file === file) { + block = v; + return false; + } + }); + + block.transport && block.transport.abort(); + + if (interrupt) { + me._putback(block); + me._popBlock(block); + } + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + // 正在准备中的文件。 + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview FlashRuntime + */ + define('runtime/flash/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var $ = Base.$, + type = 'flash', + components = {}; + + + function getFlashVersion() { + var version; + + try { + version = navigator.plugins[ 'Shockwave Flash' ]; + version = version.description; + } catch ( ex ) { + try { + version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash') + .GetVariable('$version'); + } catch ( ex2 ) { + version = '0.0'; + } + } + version = version.match( /\d+/g ); + return parseFloat( version[ 0 ] + '.' + version[ 1 ], 10 ); + } + + function FlashRuntime() { + var pool = {}, + clients = {}, + destroy = this.destroy, + me = this, + jsreciver = Base.guid('webuploader_'); + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/ ) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + clients[ uid ] = client; + + if ( components[ comp ] ) { + if ( !pool[ uid ] ) { + pool[ uid ] = new components[ comp ]( client, me ); + } + + instance = pool[ uid ]; + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + + return me.flashExec.apply( client, arguments ); + }; + + function handler( evt, obj ) { + var type = evt.type || evt, + parts, uid; + + parts = type.split('::'); + uid = parts[ 0 ]; + type = parts[ 1 ]; + + // console.log.apply( console, arguments ); + + if ( type === 'Ready' && uid === me.uid ) { + me.trigger('ready'); + } else if ( clients[ uid ] ) { + clients[ uid ].trigger( type.toLowerCase(), evt, obj ); + } + + // Base.log( evt, obj ); + } + + // flash的接受器。 + window[ jsreciver ] = function() { + var args = arguments; + + // 为了能捕获得到。 + setTimeout(function() { + handler.apply( null, args ); + }, 1 ); + }; + + this.jsreciver = jsreciver; + + this.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + + this.flashExec = function( comp, fn ) { + var flash = me.getFlash(), + args = Base.slice( arguments, 2 ); + + return flash.exec( this.uid, comp, fn, args ); + }; + + // @todo + } + + Base.inherits( Runtime, { + constructor: FlashRuntime, + + init: function() { + var container = this.getContainer(), + opts = this.options, + html; + + // 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) + container.css({ + position: 'absolute', + top: '-8px', + left: '-8px', + width: '9px', + height: '9px', + overflow: 'hidden' + }); + + // insert flash object + html = '' + + '' + + '' + + '' + + ''; + + container.html( html ); + }, + + getFlash: function() { + if ( this._flash ) { + return this._flash; + } + + this._flash = $( '#' + this.uid ).get( 0 ); + return this._flash; + } + + }); + + FlashRuntime.register = function( name, component ) { + component = components[ name ] = Base.inherits( CompBase, $.extend({ + + // @todo fix this later + flashExec: function() { + var owner = this.owner, + runtime = this.getRuntime(); + + return runtime.flashExec.apply( owner, arguments ); + } + }, component ) ); + + return component; + }; + + if ( getFlashVersion() >= 11.4 ) { + Runtime.addRuntime( type, FlashRuntime ); + } + + return FlashRuntime; + }); + /** + * @fileOverview FilePicker + */ + define('runtime/flash/filepicker',[ + 'base', + 'runtime/flash/runtime' + ], function( Base, FlashRuntime ) { + var $ = Base.$; + + return FlashRuntime.register( 'FilePicker', { + init: function( opts ) { + var copy = $.extend({}, opts ), + len, i; + + // 修复Flash再没有设置title的情况下无法弹出flash文件选择框的bug. + len = copy.accept && copy.accept.length; + for ( i = 0; i < len; i++ ) { + if ( !copy.accept[ i ].title ) { + copy.accept[ i ].title = 'Files'; + } + } + + delete copy.button; + delete copy.id; + delete copy.container; + + this.flashExec( 'FilePicker', 'init', copy ); + }, + + destroy: function() { + this.flashExec( 'FilePicker', 'destroy' ); + } + }); + }); + /** + * @fileOverview 图片压缩 + */ + define('runtime/flash/image',[ + 'runtime/flash/runtime' + ], function( FlashRuntime ) { + + return FlashRuntime.register( 'Image', { + // init: function( options ) { + // var owner = this.owner; + + // this.flashExec( 'Image', 'init', options ); + // owner.on( 'load', function() { + // debugger; + // }); + // }, + + loadFromBlob: function( blob ) { + var owner = this.owner; + + owner.info() && this.flashExec( 'Image', 'info', owner.info() ); + owner.meta() && this.flashExec( 'Image', 'meta', owner.meta() ); + + this.flashExec( 'Image', 'loadFromBlob', blob.uid ); + } + }); + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/flash/blob',[ + 'runtime/flash/runtime', + 'lib/blob' + ], function( FlashRuntime, Blob ) { + + return FlashRuntime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.flashExec( 'Blob', 'slice', start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/flash/transport',[ + 'base', + 'runtime/flash/runtime', + 'runtime/client' + ], function( Base, FlashRuntime, RuntimeClient ) { + var $ = Base.$; + + return FlashRuntime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + this._responseJson = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + binary; + + xhr.connectRuntime( blob.ruid ); + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.uid; + } else { + $.each( owner._formData, function( k, v ) { + xhr.exec( 'append', k, v ); + }); + + xhr.exec( 'appendBlob', opts.fileVal, blob.uid, + opts.filename || owner._formData.name || '' ); + } + + this._setRequestHeader( xhr, opts.headers ); + xhr.exec( 'send', { + method: opts.method, + url: server, + forceURLStream: opts.forceURLStream, + mimeType: 'application/octet-stream' + }, binary ); + }, + + getStatus: function() { + return this._status; + }, + + getResponse: function() { + return this._response || ''; + }, + + getResponseAsJson: function() { + return this._responseJson; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.exec('abort'); + xhr.destroy(); + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new RuntimeClient('XMLHttpRequest'); + + xhr.on( 'uploadprogress progress', function( e ) { + var percent = e.loaded / e.total; + percent = Math.min( 1, Math.max( 0, percent ) ); + return me.trigger( 'progress', percent ); + }); + + xhr.on( 'load', function() { + var status = xhr.exec('getStatus'), + readBody = false, + err = '', + p; + + xhr.off(); + me._xhr = null; + + if ( status >= 200 && status < 300 ) { + readBody = true; + } else if ( status >= 500 && status < 600 ) { + readBody = true; + err = 'server'; + } else { + err = 'http'; + } + + if ( readBody ) { + me._response = xhr.exec('getResponse'); + me._response = decodeURIComponent( me._response ); + + // flash 处理可能存在 bug, 没辙只能靠 js 了 + // try { + // me._responseJson = xhr.exec('getResponseAsJson'); + // } catch ( error ) { + + p = function( s ) { + try { + if (window.JSON && window.JSON.parse) { + return JSON.parse(s); + } + + return new Function('return ' + s).call(); + } catch ( err ) { + return {}; + } + }; + me._responseJson = me._response ? p(me._response) : {}; + + // } + } + + xhr.destroy(); + xhr = null; + + return err ? me.trigger( 'error', err ) : me.trigger('load'); + }); + + xhr.on( 'error', function() { + xhr.off(); + me._xhr = null; + me.trigger( 'error', 'http' ); + }); + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.exec( 'setRequestHeader', key, val ); + }); + } + }); + }); + + /** + * @fileOverview 只有flash实现的文件版本。 + */ + define('preset/flashonly',[ + 'base', + + // widgets + 'widgets/filepicker', + 'widgets/image', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + + // runtimes + + // flash + 'runtime/flash/filepicker', + 'runtime/flash/image', + 'runtime/flash/blob', + 'runtime/flash/transport' + ], function( Base ) { + return Base; + }); + define('webuploader',[ + 'preset/flashonly' + ], function( preset ) { + return preset; + }); + return require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.flashonly.min.js b/public/static/libs/webuploader/webuploader.flashonly.min.js new file mode 100644 index 0000000..90f369e --- /dev/null +++ b/public/static/libs/webuploader/webuploader.flashonly.min.js @@ -0,0 +1,2 @@ +/* WebUploader 0.1.6 */!function(a,b){var c,d={},e=function(a,b){var c,d,e;if("string"==typeof a)return h(a);for(c=[],d=a.length,e=0;d>e;e++)c.push(h(a[e]));return b.apply(null,c)},f=function(a,b,c){2===arguments.length&&(c=b,b=null),e(b||[],function(){g(a,c,arguments)})},g=function(a,b,c){var f,g={exports:b};"function"==typeof b&&(c.length||(c=[e,g.exports,g]),f=b.apply(null,c),void 0!==f&&(g.exports=f)),d[a]=g.exports},h=function(b){var c=d[b]||a[b];if(!c)throw new Error("`"+b+"` is undefined");return c},i=function(a){var b,c,e,f,g,h;h=function(a){return a&&a.charAt(0).toUpperCase()+a.substr(1)};for(b in d)if(c=a,d.hasOwnProperty(b)){for(e=b.split("/"),g=h(e.pop());f=h(e.shift());)c[f]=c[f]||{},c=c[f];c[g]=d[b]}return a},j=function(c){return a.__dollar=c,i(b(a,f,e))};"object"==typeof module&&"object"==typeof module.exports?module.exports=j():"function"==typeof define&&define.amd?define(["jquery"],j):(c=a.WebUploader,a.WebUploader=j(),a.WebUploader.noConflict=function(){a.WebUploader=c})}(window,function(a,b,c){return b("dollar-third",[],function(){var b=a.require,c=a.__dollar||a.jQuery||a.Zepto||b("jquery")||b("zepto");if(!c)throw new Error("jQuery or Zepto not found!");return c}),b("dollar",["dollar-third"],function(a){return a}),b("promise-third",["dollar"],function(a){return{Deferred:a.Deferred,when:a.when,isPromise:function(a){return a&&"function"==typeof a.then}}}),b("promise",["promise-third"],function(a){return a}),b("base",["dollar","promise"],function(b,c){function d(a){return function(){return h.apply(a,arguments)}}function e(a,b){return function(){return a.apply(b,arguments)}}function f(a){var b;return Object.create?Object.create(a):(b=function(){},b.prototype=a,new b)}var g=function(){},h=Function.call;return{version:"0.1.6",$:b,Deferred:c.Deferred,isPromise:c.isPromise,when:c.when,browser:function(a){var b={},c=a.match(/WebKit\/([\d.]+)/),d=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),e=a.match(/MSIE\s([\d\.]+)/)||a.match(/(?:trident)(?:.*rv:([\w.]+))?/i),f=a.match(/Firefox\/([\d.]+)/),g=a.match(/Safari\/([\d.]+)/),h=a.match(/OPR\/([\d.]+)/);return c&&(b.webkit=parseFloat(c[1])),d&&(b.chrome=parseFloat(d[1])),e&&(b.ie=parseFloat(e[1])),f&&(b.firefox=parseFloat(f[1])),g&&(b.safari=parseFloat(g[1])),h&&(b.opera=parseFloat(h[1])),b}(navigator.userAgent),os:function(a){var b={},c=a.match(/(?:Android);?[\s\/]+([\d.]+)?/),d=a.match(/(?:iPad|iPod|iPhone).*OS\s([\d_]+)/);return c&&(b.android=parseFloat(c[1])),d&&(b.ios=parseFloat(d[1].replace(/_/g,"."))),b}(navigator.userAgent),inherits:function(a,c,d){var e;return"function"==typeof c?(e=c,c=null):e=c&&c.hasOwnProperty("constructor")?c.constructor:function(){return a.apply(this,arguments)},b.extend(!0,e,a,d||{}),e.__super__=a.prototype,e.prototype=f(a.prototype),c&&b.extend(!0,e.prototype,c),e},noop:g,bindFn:e,log:function(){return a.console?e(console.log,console):g}(),nextTick:function(){return function(a){setTimeout(a,1)}}(),slice:d([].slice),guid:function(){var a=0;return function(b){for(var c=(+new Date).toString(32),d=0;5>d;d++)c+=Math.floor(65535*Math.random()).toString(32);return(b||"wu_")+c+(a++).toString(32)}}(),formatSize:function(a,b,c){var d;for(c=c||["B","K","M","G","TB"];(d=c.shift())&&a>1024;)a/=1024;return("B"===d?a:a.toFixed(b||2))+d}}}),b("mediator",["base"],function(a){function b(a,b,c,d){return f.grep(a,function(a){return!(!a||b&&a.e!==b||c&&a.cb!==c&&a.cb._cb!==c||d&&a.ctx!==d)})}function c(a,b,c){f.each((a||"").split(h),function(a,d){c(d,b)})}function d(a,b){for(var c,d=!1,e=-1,f=a.length;++e1?(d.isPlainObject(b)&&d.isPlainObject(c[a])?d.extend(c[a],b):c[a]=b,void 0):a?c[a]:c},getStats:function(){var a=this.request("get-stats");return a?{successNum:a.numOfSuccess,progressNum:a.numOfProgress,cancelNum:a.numOfCancel,invalidNum:a.numOfInvalid,uploadFailNum:a.numOfUploadFailed,queueNum:a.numOfQueue,interruptNum:a.numofInterrupt}:{}},trigger:function(a){var c=[].slice.call(arguments,1),e=this.options,f="on"+a.substring(0,1).toUpperCase()+a.substring(1);return b.trigger.apply(this,arguments)===!1||d.isFunction(e[f])&&e[f].apply(this,c)===!1||d.isFunction(this[f])&&this[f].apply(this,c)===!1||b.trigger.apply(b,[this,a].concat(c))===!1?!1:!0},destroy:function(){this.request("destroy",arguments),this.off()},request:a.noop}),a.create=c.create=function(a){return new c(a)},a.Uploader=c,c}),b("runtime/runtime",["base","mediator"],function(a,b){function c(b){this.options=d.extend({container:document.body},b),this.uid=a.guid("rt_")}var d=a.$,e={},f=function(a){for(var b in a)if(a.hasOwnProperty(b))return b;return null};return d.extend(c.prototype,{getContainer:function(){var a,b,c=this.options;return this._container?this._container:(a=d(c.container||document.body),b=d(document.createElement("div")),b.attr("id","rt_"+this.uid),b.css({position:"absolute",top:"0px",left:"0px",width:"1px",height:"1px",overflow:"hidden"}),a.append(b),a.addClass("webuploader-container"),this._container=b,this._parent=a,b)},init:a.noop,exec:a.noop,destroy:function(){this._container&&this._container.remove(),this._parent&&this._parent.removeClass("webuploader-container"),this.off()}}),c.orders="html5,flash",c.addRuntime=function(a,b){e[a]=b},c.hasRuntime=function(a){return!!(a?e[a]:f(e))},c.create=function(a,b){var g,h;if(b=b||c.orders,d.each(b.split(/\s*,\s*/g),function(){return e[this]?(g=this,!1):void 0}),g=g||f(e),!g)throw new Error("Runtime Error");return h=new e[g](a)},b.installTo(c.prototype),c}),b("runtime/client",["base","mediator","runtime/runtime"],function(a,b,c){function d(b,d){var f,g=a.Deferred();this.uid=a.guid("client_"),this.runtimeReady=function(a){return g.done(a)},this.connectRuntime=function(b,h){if(f)throw new Error("already connected!");return g.done(h),"string"==typeof b&&e.get(b)&&(f=e.get(b)),f=f||e.get(null,d),f?(a.$.extend(f.options,b),f.__promise.then(g.resolve),f.__client++):(f=c.create(b,b.runtimeOrder),f.__promise=g.promise(),f.once("ready",g.resolve),f.init(),e.add(f),f.__client=1),d&&(f.__standalone=d),f},this.getRuntime=function(){return f},this.disconnectRuntime=function(){f&&(f.__client--,f.__client<=0&&(e.remove(f),delete f.__promise,f.destroy()),f=null)},this.exec=function(){if(f){var c=a.slice(arguments);return b&&c.unshift(b),f.exec.apply(this,c)}},this.getRuid=function(){return f&&f.uid},this.destroy=function(a){return function(){a&&a.apply(this,arguments),this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()}}(this.destroy)}var e;return e=function(){var a={};return{add:function(b){a[b.uid]=b},get:function(b,c){var d;if(b)return a[b];for(d in a)if(!c||!a[d].__standalone)return a[d];return null},remove:function(b){delete a[b.uid]}}}(),b.installTo(d.prototype),d}),b("lib/blob",["base","runtime/client"],function(a,b){function c(a,c){var d=this;d.source=c,d.ruid=a,this.size=c.size||0,this.type=!c.type&&this.ext&&~"jpg,jpeg,png,gif,bmp".indexOf(this.ext)?"image/"+("jpg"===this.ext?"jpeg":this.ext):c.type||"application/octet-stream",b.call(d,"Blob"),this.uid=c.uid||this.uid,a&&d.connectRuntime(a)}return a.inherits(b,{constructor:c,slice:function(a,b){return this.exec("slice",a,b)},getSource:function(){return this.source}}),c}),b("lib/file",["base","lib/blob"],function(a,b){function c(a,c){var f;this.name=c.name||"untitled"+d++,f=e.exec(c.name)?RegExp.$1.toLowerCase():"",!f&&c.type&&(f=/\/(jpg|jpeg|png|gif|bmp)$/i.exec(c.type)?RegExp.$1.toLowerCase():"",this.name+="."+f),this.ext=f,this.lastModifiedDate=c.lastModifiedDate||(new Date).toLocaleString(),b.apply(this,arguments)}var d=1,e=/\.([^.]+)$/;return a.inherits(b,c)}),b("lib/filepicker",["base","runtime/client","lib/file"],function(b,c,d){function e(a){if(a=this.options=f.extend({},e.options,a),a.container=f(a.id),!a.container.length)throw new Error("按钮指定错误");a.innerHTML=a.innerHTML||a.label||a.container.html()||"",a.button=f(a.button||document.createElement("div")),a.button.html(a.innerHTML),a.container.html(a.button),c.call(this,"FilePicker",!0)}var f=b.$;return e.options={button:null,container:null,label:null,innerHTML:null,multiple:!0,accept:null,name:"file",style:"webuploader-pick"},b.inherits(c,{constructor:e,init:function(){var c=this,e=c.options,g=e.button,h=e.style;h&&g.addClass("webuploader-pick"),c.on("all",function(a){var b;switch(a){case"mouseenter":h&&g.addClass("webuploader-pick-hover");break;case"mouseleave":h&&g.removeClass("webuploader-pick-hover");break;case"change":b=c.exec("getFiles"),c.trigger("select",f.map(b,function(a){return a=new d(c.getRuid(),a),a._refer=e.container,a}),e.container)}}),c.connectRuntime(e,function(){c.refresh(),c.exec("init",e),c.trigger("ready")}),this._resizeHandler=b.bindFn(this.refresh,this),f(a).on("resize",this._resizeHandler)},refresh:function(){var a=this.getRuntime().getContainer(),b=this.options.button,c=b.outerWidth?b.outerWidth():b.width(),d=b.outerHeight?b.outerHeight():b.height(),e=b.offset();c&&d&&a.css({bottom:"auto",right:"auto",width:c+"px",height:d+"px"}).offset(e)},enable:function(){var a=this.options.button;a.removeClass("webuploader-pick-disable"),this.refresh()},disable:function(){var a=this.options.button;this.getRuntime().getContainer().css({top:"-99999px"}),a.addClass("webuploader-pick-disable")},destroy:function(){var b=this.options.button;f(a).off("resize",this._resizeHandler),b.removeClass("webuploader-pick-disable webuploader-pick-hover webuploader-pick")}}),e}),b("widgets/widget",["base","uploader"],function(a,b){function c(a){if(!a)return!1;var b=a.length,c=e.type(a);return 1===a.nodeType&&b?!0:"array"===c||"function"!==c&&"string"!==c&&(0===b||"number"==typeof b&&b>0&&b-1 in a)}function d(a){this.owner=a,this.options=a.options}var e=a.$,f=b.prototype._init,g=b.prototype.destroy,h={},i=[];return e.extend(d.prototype,{init:a.noop,invoke:function(a,b){var c=this.responseMap;return c&&a in c&&c[a]in this&&e.isFunction(this[c[a]])?this[c[a]].apply(this,b):h},request:function(){return this.owner.request.apply(this.owner,arguments)}}),e.extend(b.prototype,{_init:function(){var a=this,b=a._widgets=[],c=a.options.disableWidgets||"";return e.each(i,function(d,e){(!c||!~c.indexOf(e._name))&&b.push(new e(a))}),f.apply(a,arguments)},request:function(b,d,e){var f,g,i,j,k=0,l=this._widgets,m=l&&l.length,n=[],o=[];for(d=c(d)?d:[d];m>k;k++)f=l[k],g=f.invoke(b,d),g!==h&&(a.isPromise(g)?o.push(g):n.push(g));return e||o.length?(i=a.when.apply(a,o),j=i.pipe?"pipe":"then",i[j](function(){var b=a.Deferred(),c=arguments;return 1===c.length&&(c=c[0]),setTimeout(function(){b.resolve(c)},1),b.promise()})[e?j:"done"](e||a.noop)):n[0]},destroy:function(){g.apply(this,arguments),this._widgets=null}}),b.register=d.register=function(b,c){var f,g={init:"init",destroy:"destroy",name:"anonymous"};return 1===arguments.length?(c=b,e.each(c,function(a){return"_"===a[0]||"name"===a?("name"===a&&(g.name=c.name),void 0):(g[a.replace(/[A-Z]/g,"-$&").toLowerCase()]=a,void 0)})):g=e.extend(g,b),c.responseMap=g,f=a.inherits(d,c),f._name=g.name,i.push(f),f},b.unRegister=d.unRegister=function(a){if(a&&"anonymous"!==a)for(var b=i.length;b--;)i[b]._name===a&&i.splice(b,1)},d}),b("widgets/filepicker",["base","uploader","lib/filepicker","widgets/widget"],function(a,b,c){var d=a.$;return d.extend(b.options,{pick:null,accept:null}),b.register({name:"picker",init:function(a){return this.pickers=[],a.pick&&this.addBtn(a.pick)},refresh:function(){d.each(this.pickers,function(){this.refresh()})},addBtn:function(b){var e=this,f=e.options,g=f.accept,h=[];if(b)return d.isPlainObject(b)||(b={id:b}),d(b.id).each(function(){var i,j,k;k=a.Deferred(),i=d.extend({},b,{accept:d.isPlainObject(g)?[g]:g,swf:f.swf,runtimeOrder:f.runtimeOrder,id:this}),j=new c(i),j.once("ready",k.resolve),j.on("select",function(a){e.owner.request("add-file",[a])}),j.on("dialogopen",function(){e.owner.trigger("dialogOpen",j.button)}),j.init(),e.pickers.push(j),h.push(k.promise())}),a.when.apply(a,h)},disable:function(){d.each(this.pickers,function(){this.disable()})},enable:function(){d.each(this.pickers,function(){this.enable()})},destroy:function(){d.each(this.pickers,function(){this.destroy()}),this.pickers=null}})}),b("lib/image",["base","runtime/client","lib/blob"],function(a,b,c){function d(a){this.options=e.extend({},d.options,a),b.call(this,"Image"),this.on("load",function(){this._info=this.exec("info"),this._meta=this.exec("meta")})}var e=a.$;return d.options={quality:90,crop:!1,preserveHeaders:!1,allowMagnify:!1},a.inherits(b,{constructor:d,info:function(a){return a?(this._info=a,this):this._info},meta:function(a){return a?(this._meta=a,this):this._meta},loadFromBlob:function(a){var b=this,c=a.getRuid();this.connectRuntime(c,function(){b.exec("init",b.options),b.exec("loadFromBlob",a)})},resize:function(){var b=a.slice(arguments);return this.exec.apply(this,["resize"].concat(b))},crop:function(){var b=a.slice(arguments);return this.exec.apply(this,["crop"].concat(b))},getAsDataUrl:function(a){return this.exec("getAsDataUrl",a)},getAsBlob:function(a){var b=this.exec("getAsBlob",a);return new c(this.getRuid(),b)}}),d}),b("widgets/image",["base","uploader","lib/image","widgets/widget"],function(a,b,c){var d,e=a.$;return d=function(a){var b=0,c=[],d=function(){for(var d;c.length&&a>b;)d=c.shift(),b+=d[0],d[1]()};return function(a,e,f){c.push([e,f]),a.once("destroy",function(){b-=e,setTimeout(d,1)}),setTimeout(d,1)}}(5242880),e.extend(b.options,{thumb:{width:110,height:110,quality:70,allowMagnify:!0,crop:!0,preserveHeaders:!1,type:"image/jpeg"},compress:{width:1600,height:1600,quality:90,allowMagnify:!1,crop:!1,preserveHeaders:!0}}),b.register({name:"image",makeThumb:function(a,b,f,g){var h,i;return a=this.request("get-file",a),a.type.match(/^image/)?(h=e.extend({},this.options.thumb),e.isPlainObject(f)&&(h=e.extend(h,f),f=null),f=f||h.width,g=g||h.height,i=new c(h),i.once("load",function(){a._info=a._info||i.info(),a._meta=a._meta||i.meta(),1>=f&&f>0&&(f=a._info.width*f),1>=g&&g>0&&(g=a._info.height*g),i.resize(f,g)}),i.once("complete",function(){b(!1,i.getAsDataUrl(h.type)),i.destroy()}),i.once("error",function(a){b(a||!0),i.destroy()}),d(i,a.source.size,function(){a._info&&i.info(a._info),a._meta&&i.meta(a._meta),i.loadFromBlob(a.source)}),void 0):(b(!0),void 0)},beforeSendFile:function(b){var d,f,g=this.options.compress||this.options.resize,h=g&&g.compressSize||0,i=g&&g.noCompressIfLarger||!1;return b=this.request("get-file",b),!g||!~"image/jpeg,image/jpg".indexOf(b.type)||b.size=a&&a>0&&(a=b._info.width*a),1>=c&&c>0&&(c=b._info.height*c),d.resize(a,c)}),d.once("complete",function(){var a,c;try{a=d.getAsBlob(g.type),c=b.size,(!i||a.sizeb;b++)if(c=this._queue[b],a===c.getStatus())return c;return null},sort:function(a){"function"==typeof a&&this._queue.sort(a)},getFiles:function(){for(var a,b=[].slice.call(arguments,0),c=[],d=0,f=this._queue.length;f>d;d++)a=this._queue[d],(!b.length||~e.inArray(a.getStatus(),b))&&c.push(a);return c},removeFile:function(a){var b=this._map[a.id];b&&(delete this._map[a.id],a.destroy(),this.stats.numofDeleted++)},_fileAdded:function(a){var b=this,c=this._map[a.id];c||(this._map[a.id]=a,a.on("statuschange",function(a,c){b._onFileStatusChange(a,c)}))},_onFileStatusChange:function(a,b){var c=this.stats;switch(b){case f.PROGRESS:c.numOfProgress--;break;case f.QUEUED:c.numOfQueue--;break;case f.ERROR:c.numOfUploadFailed--;break;case f.INVALID:c.numOfInvalid--;break;case f.INTERRUPT:c.numofInterrupt--}switch(a){case f.QUEUED:c.numOfQueue++;break;case f.PROGRESS:c.numOfProgress++;break;case f.ERROR:c.numOfUploadFailed++;break;case f.COMPLETE:c.numOfSuccess++;break;case f.CANCELLED:c.numOfCancel++;break;case f.INVALID:c.numOfInvalid++;break;case f.INTERRUPT:c.numofInterrupt++}}}),b.installTo(d.prototype),d}),b("widgets/queue",["base","uploader","queue","file","lib/file","runtime/client","widgets/widget"],function(a,b,c,d,e,f){var g=a.$,h=/\.\w+$/,i=d.Status;return b.register({name:"queue",init:function(b){var d,e,h,i,j,k,l,m=this;if(g.isPlainObject(b.accept)&&(b.accept=[b.accept]),b.accept){for(j=[],h=0,e=b.accept.length;e>h;h++)i=b.accept[h].extensions,i&&j.push(i);j.length&&(k="\\."+j.join(",").replace(/,/g,"$|\\.").replace(/\*/g,".*")+"$"),m.accept=new RegExp(k,"i")}return m.queue=new c,m.stats=m.queue.stats,"html5"===this.request("predict-runtime-type")?(d=a.Deferred(),this.placeholder=l=new f("Placeholder"),l.connectRuntime({runtimeOrder:"html5"},function(){m._ruid=l.getRuid(),d.resolve()}),d.promise()):void 0},_wrapFile:function(a){if(!(a instanceof d)){if(!(a instanceof e)){if(!this._ruid)throw new Error("Can't add external files.");a=new e(this._ruid,a)}a=new d(a)}return a},acceptFile:function(a){var b=!a||!a.size||this.accept&&h.exec(a.name)&&!this.accept.test(a.name);return!b},_addFile:function(a){var b=this;return a=b._wrapFile(a),b.owner.trigger("beforeFileQueued",a)?b.acceptFile(a)?(b.queue.append(a),b.owner.trigger("fileQueued",a),a):(b.owner.trigger("error","Q_TYPE_DENIED",a),void 0):void 0},getFile:function(a){return this.queue.getFile(a)},addFile:function(a){var b=this;a.length||(a=[a]),a=g.map(a,function(a){return b._addFile(a)}),a.length&&(b.owner.trigger("filesQueued",a),b.options.auto&&setTimeout(function(){b.request("start-upload")},20))},getStats:function(){return this.stats},removeFile:function(a,b){var c=this;a=a.id?a:c.queue.getFile(a),this.request("cancel-file",a),b&&this.queue.removeFile(a)},getFiles:function(){return this.queue.getFiles.apply(this.queue,arguments)},fetchFile:function(){return this.queue.fetch.apply(this.queue,arguments)},retry:function(a,b){var c,d,e,f=this;if(a)return a=a.id?a:f.queue.getFile(a),a.setStatus(i.QUEUED),b||f.request("start-upload"),void 0;for(c=f.queue.getFiles(i.ERROR),d=0,e=c.length;e>d;d++)a=c[d],a.setStatus(i.QUEUED);f.request("start-upload")},sortFiles:function(){return this.queue.sort.apply(this.queue,arguments)},reset:function(){this.owner.trigger("reset"),this.queue=new c,this.stats=this.queue.stats},destroy:function(){this.reset(),this.placeholder&&this.placeholder.destroy()}})}),b("widgets/runtime",["uploader","runtime/runtime","widgets/widget"],function(a,b){return a.support=function(){return b.hasRuntime.apply(b,arguments)},a.register({name:"runtime",init:function(){if(!this.predictRuntimeType())throw Error("Runtime Error")},predictRuntimeType:function(){var a,c,d=this.options.runtimeOrder||b.orders,e=this.type;if(!e)for(d=d.split(/\s*,\s*/g),a=0,c=d.length;c>a;a++)if(b.hasRuntime(d[a])){this.type=e=d[a];break}return e}})}),b("lib/transport",["base","runtime/client","mediator"],function(a,b,c){function d(a){var c=this;a=c.options=e.extend(!0,{},d.options,a||{}),b.call(this,"Transport"),this._blob=null,this._formData=a.formData||{},this._headers=a.headers||{},this.on("progress",this._timeout),this.on("load error",function(){c.trigger("progress",1),clearTimeout(c._timer)})}var e=a.$;return d.options={server:"",method:"POST",withCredentials:!1,fileVal:"file",timeout:12e4,formData:{},headers:{},sendAsBinary:!1},e.extend(d.prototype,{appendBlob:function(a,b,c){var d=this,e=d.options;d.getRuid()&&d.disconnectRuntime(),d.connectRuntime(b.ruid,function(){d.exec("init")}),d._blob=b,e.fileVal=a||e.fileVal,e.filename=c||e.filename},append:function(a,b){"object"==typeof a?e.extend(this._formData,a):this._formData[a]=b},setRequestHeader:function(a,b){"object"==typeof a?e.extend(this._headers,a):this._headers[a]=b},send:function(a){this.exec("send",a),this._timeout()},abort:function(){return clearTimeout(this._timer),this.exec("abort")},destroy:function(){this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()},getResponse:function(){return this.exec("getResponse")},getResponseAsJson:function(){return this.exec("getResponseAsJson")},getStatus:function(){return this.exec("getStatus")},_timeout:function(){var a=this,b=a.options.timeout;b&&(clearTimeout(a._timer),a._timer=setTimeout(function(){a.abort(),a.trigger("error","timeout")},b))}}),c.installTo(d.prototype),d}),b("widgets/upload",["base","uploader","file","lib/transport","widgets/widget"],function(a,b,c,d){function e(a,b){var c,d,e=[],f=a.source,g=f.size,h=b?Math.ceil(g/b):1,i=0,j=0;for(d={file:a,has:function(){return!!e.length},shift:function(){return e.shift()},unshift:function(a){e.unshift(a)}};h>j;)c=Math.min(b,g-i),e.push({file:a,start:i,end:b?i+c:g,total:g,chunks:h,chunk:j++,cuted:d}),i+=c;return a.blocks=e.concat(),a.remaning=e.length,d}var f=a.$,g=a.isPromise,h=c.Status;f.extend(b.options,{prepareNextFile:!1,chunked:!1,chunkSize:5242880,chunkRetry:2,threads:3,formData:{}}),b.register({name:"upload",init:function(){var b=this.owner,c=this;this.runing=!1,this.progress=!1,b.on("startUpload",function(){c.progress=!0}).on("uploadFinished",function(){c.progress=!1}),this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this.__tick=a.bindFn(this._tick,this),b.on("uploadComplete",function(a){a.blocks&&f.each(a.blocks,function(a,b){b.transport&&(b.transport.abort(),b.transport.destroy()),delete b.transport}),delete a.blocks,delete a.remaning})},reset:function(){this.request("stop-upload",!0),this.runing=!1,this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this._trigged=!1,this._promise=null},startUpload:function(b){var c=this;if(f.each(c.request("get-files",h.INVALID),function(){c.request("remove-file",this)}),b?(b=b.id?b:c.request("get-file",b),b.getStatus()===h.INTERRUPT?(b.setStatus(h.QUEUED),f.each(c.pool,function(a,c){c.file===b&&(c.transport&&c.transport.send(),b.setStatus(h.PROGRESS))})):b.getStatus()!==h.PROGRESS&&b.setStatus(h.QUEUED)):f.each(c.request("get-files",[h.INITED]),function(){this.setStatus(h.QUEUED)}),c.runing)return a.nextTick(c.__tick);c.runing=!0;var d=[];b||f.each(c.pool,function(a,b){var e=b.file;e.getStatus()===h.INTERRUPT&&(c._trigged=!1,d.push(e),b.transport&&b.transport.send())}),f.each(d,function(){this.setStatus(h.PROGRESS)}),b||f.each(c.request("get-files",h.INTERRUPT),function(){this.setStatus(h.PROGRESS)}),c._trigged=!1,a.nextTick(c.__tick),c.owner.trigger("startUpload")},stopUpload:function(b,c){var d,e=this;if(b===!0&&(c=b,b=null),e.runing!==!1){if(b){if(b=b.id?b:e.request("get-file",b),b.getStatus()!==h.PROGRESS&&b.getStatus()!==h.QUEUED)return;return b.setStatus(h.INTERRUPT),f.each(e.pool,function(a,c){return c.file===b?(d=c,!1):void 0}),d.transport&&d.transport.abort(),c&&(e._putback(d),e._popBlock(d)),a.nextTick(e.__tick)}e.runing=!1,this._promise&&this._promise.file&&this._promise.file.setStatus(h.INTERRUPT),c&&f.each(e.pool,function(a,b){b.transport&&b.transport.abort(),b.file.setStatus(h.INTERRUPT)}),e.owner.trigger("stopUpload")}},cancelFile:function(a){a=a.id?a:this.request("get-file",a),a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),a.setStatus(h.CANCELLED),this.owner.trigger("fileDequeued",a)},isInProgress:function(){return!!this.progress},_getStats:function(){return this.request("get-stats")},skipFile:function(a,b){a=a.id?a:this.request("get-file",a),a.setStatus(b||h.COMPLETE),a.skipped=!0,a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),this.owner.trigger("uploadSkip",a)},_tick:function(){var b,c,d=this,e=d.options;return d._promise?d._promise.always(d.__tick):(d.pool.length1&&~"http,abort".indexOf(a)&&b.retried1&&f.extend(m,{chunks:b.chunks,chunk:b.chunk}),i.trigger("uploadBeforeSend",b,m,n),l.appendBlob(j.fileVal,b.blob,k.name),l.append(m),l.setRequestHeader(n),l.send()},_finishFile:function(a,b,c){var d=this.owner;return d.request("after-send-file",arguments,function(){a.setStatus(h.COMPLETE),d.trigger("uploadSuccess",a,b,c)}).fail(function(b){a.getStatus()===h.PROGRESS&&a.setStatus(h.ERROR,b),d.trigger("uploadError",a,b)}).always(function(){d.trigger("uploadComplete",a)})},updateFileProgress:function(a){var b=0,c=0;a.blocks&&(f.each(a.blocks,function(a,b){c+=(b.percentage||0)*(b.end-b.start)}),b=c/a.size,this.owner.trigger("uploadProgress",a,b||0))}})}),b("widgets/validator",["base","uploader","file","widgets/widget"],function(a,b,c){var d,e=a.$,f={};return d={addValidator:function(a,b){f[a]=b},removeValidator:function(a){delete f[a]}},b.register({name:"validator",init:function(){var b=this;a.nextTick(function(){e.each(f,function(){this.call(b.owner)})})}}),d.addValidator("fileNumLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileNumLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){return c>=d&&e&&(e=!1,this.trigger("error","Q_EXCEED_NUM_LIMIT",d,a),setTimeout(function(){e=!0},1)),c>=d?!1:!0}),a.on("fileQueued",function(){c++}),a.on("fileDequeued",function(){c--}),a.on("reset",function(){c=0}))}),d.addValidator("fileSizeLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileSizeLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){var b=c+a.size>d;return b&&e&&(e=!1,this.trigger("error","Q_EXCEED_SIZE_LIMIT",d,a),setTimeout(function(){e=!0},1)),b?!1:!0}),a.on("fileQueued",function(a){c+=a.size}),a.on("fileDequeued",function(a){c-=a.size}),a.on("reset",function(){c=0}))}),d.addValidator("fileSingleSizeLimit",function(){var a=this,b=a.options,d=b.fileSingleSizeLimit;d&&a.on("beforeFileQueued",function(a){return a.size>d?(a.setStatus(c.Status.INVALID,"exceed_size"),this.trigger("error","F_EXCEED_SIZE",d,a),!1):void 0})}),d.addValidator("duplicate",function(){function a(a){for(var b,c=0,d=0,e=a.length;e>d;d++)b=a.charCodeAt(d),c=b+(c<<6)+(c<<16)-c; +return c}var b=this,c=b.options,d={};c.duplicate||(b.on("beforeFileQueued",function(b){var c=b.__hash||(b.__hash=a(b.name+b.size+b.lastModifiedDate));return d[c]?(this.trigger("error","F_DUPLICATE",b),!1):void 0}),b.on("fileQueued",function(a){var b=a.__hash;b&&(d[b]=!0)}),b.on("fileDequeued",function(a){var b=a.__hash;b&&delete d[b]}),b.on("reset",function(){d={}}))}),d}),b("runtime/compbase",[],function(){function a(a,b){this.owner=a,this.options=a.options,this.getRuntime=function(){return b},this.getRuid=function(){return b.uid},this.trigger=function(){return a.trigger.apply(a,arguments)}}return a}),b("runtime/flash/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a;try{a=navigator.plugins["Shockwave Flash"],a=a.description}catch(b){try{a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash").GetVariable("$version")}catch(c){a="0.0"}}return a=a.match(/\d+/g),parseFloat(a[0]+"."+a[1],10)}function f(){function d(a,b){var c,d,e=a.type||a;c=e.split("::"),d=c[0],e=c[1],"Ready"===e&&d===j.uid?j.trigger("ready"):f[d]&&f[d].trigger(e.toLowerCase(),a,b)}var e={},f={},g=this.destroy,j=this,k=b.guid("webuploader_");c.apply(j,arguments),j.type=h,j.exec=function(a,c){var d,g=this,h=g.uid,k=b.slice(arguments,2);return f[h]=g,i[a]&&(e[h]||(e[h]=new i[a](g,j)),d=e[h],d[c])?d[c].apply(d,k):j.flashExec.apply(g,arguments)},a[k]=function(){var a=arguments;setTimeout(function(){d.apply(null,a)},1)},this.jsreciver=k,this.destroy=function(){return g&&g.apply(this,arguments)},this.flashExec=function(a,c){var d=j.getFlash(),e=b.slice(arguments,2);return d.exec(this.uid,a,c,e)}}var g=b.$,h="flash",i={};return b.inherits(c,{constructor:f,init:function(){var a,c=this.getContainer(),d=this.options;c.css({position:"absolute",top:"-8px",left:"-8px",width:"9px",height:"9px",overflow:"hidden"}),a=''+''+''+''+"",c.html(a)},getFlash:function(){return this._flash?this._flash:(this._flash=g("#"+this.uid).get(0),this._flash)}}),f.register=function(a,c){return c=i[a]=b.inherits(d,g.extend({flashExec:function(){var a=this.owner,b=this.getRuntime();return b.flashExec.apply(a,arguments)}},c))},e()>=11.4&&c.addRuntime(h,f),f}),b("runtime/flash/filepicker",["base","runtime/flash/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(a){var b,d,e=c.extend({},a);for(b=e.accept&&e.accept.length,d=0;b>d;d++)e.accept[d].title||(e.accept[d].title="Files");delete e.button,delete e.id,delete e.container,this.flashExec("FilePicker","init",e)},destroy:function(){this.flashExec("FilePicker","destroy")}})}),b("runtime/flash/image",["runtime/flash/runtime"],function(a){return a.register("Image",{loadFromBlob:function(a){var b=this.owner;b.info()&&this.flashExec("Image","info",b.info()),b.meta()&&this.flashExec("Image","meta",b.meta()),this.flashExec("Image","loadFromBlob",a.uid)}})}),b("runtime/flash/blob",["runtime/flash/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.flashExec("Blob","slice",a,c);return new b(this.getRuid(),d)}})}),b("runtime/flash/transport",["base","runtime/flash/runtime","runtime/client"],function(b,c,d){var e=b.$;return c.register("Transport",{init:function(){this._status=0,this._response=null,this._responseJson=null},send:function(){var a,b=this.owner,c=this.options,d=this._initAjax(),f=b._blob,g=c.server;d.connectRuntime(f.ruid),c.sendAsBinary?(g+=(/\?/.test(g)?"&":"?")+e.param(b._formData),a=f.uid):(e.each(b._formData,function(a,b){d.exec("append",a,b)}),d.exec("appendBlob",c.fileVal,f.uid,c.filename||b._formData.name||"")),this._setRequestHeader(d,c.headers),d.exec("send",{method:c.method,url:g,forceURLStream:c.forceURLStream,mimeType:"application/octet-stream"},a)},getStatus:function(){return this._status},getResponse:function(){return this._response||""},getResponseAsJson:function(){return this._responseJson},abort:function(){var a=this._xhr;a&&(a.exec("abort"),a.destroy(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var b=this,c=new d("XMLHttpRequest");return c.on("uploadprogress progress",function(a){var c=a.loaded/a.total;return c=Math.min(1,Math.max(0,c)),b.trigger("progress",c)}),c.on("load",function(){var d,e=c.exec("getStatus"),f=!1,g="";return c.off(),b._xhr=null,e>=200&&300>e?f=!0:e>=500&&600>e?(f=!0,g="server"):g="http",f&&(b._response=c.exec("getResponse"),b._response=decodeURIComponent(b._response),d=function(b){try{return a.JSON&&a.JSON.parse?JSON.parse(b):new Function("return "+b).call()}catch(c){return{}}},b._responseJson=b._response?d(b._response):{}),c.destroy(),c=null,g?b.trigger("error",g):b.trigger("load")}),c.on("error",function(){c.off(),b._xhr=null,b.trigger("error","http")}),b._xhr=c,c},_setRequestHeader:function(a,b){e.each(b,function(b,c){a.exec("setRequestHeader",b,c)})}})}),b("preset/flashonly",["base","widgets/filepicker","widgets/image","widgets/queue","widgets/runtime","widgets/upload","widgets/validator","runtime/flash/filepicker","runtime/flash/image","runtime/flash/blob","runtime/flash/transport"],function(a){return a}),b("webuploader",["preset/flashonly"],function(a){return a}),c("webuploader")}); \ No newline at end of file diff --git a/public/static/libs/webuploader/webuploader.html5nodepend.js b/public/static/libs/webuploader/webuploader.html5nodepend.js new file mode 100644 index 0000000..0356f30 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.html5nodepend.js @@ -0,0 +1,6589 @@ +/*! WebUploader 0.1.6 */ + + +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jq-bridge 主要实现像jQuery一样的功能方法,可以替换成jQuery, + * 这里只实现了此组件所需的部分。 + * + * **此文件的代码还不可用,还是直接用jquery吧** + * @beta + */ + define('dollar-builtin',[],function() { + var doc = window.document, + emptyArray = [], + slice = emptyArray.slice, + class2type = {}, + hasOwn = class2type.hasOwnProperty, + toString = class2type.toString, + rId = /^#(.*)$/; + + function each( obj, iterator ) { + var i; + + //add guard here + if(!obj) { + return; + } + + // like array + if ( typeof obj !== 'function' && typeof obj.length === 'number' ) { + for ( i = 0; i < obj.length; i++ ) { + if ( iterator.call( obj[ i ], i, obj[ i ] ) === false ) { + return obj; + } + } + } else { + for ( i in obj ) { + if ( hasOwn.call( obj, i ) && iterator.call( obj[ i ], i, + obj[ i ] ) === false ) { + return obj; + } + } + } + + return obj; + } + + function extend( target, source, deep ) { + each( source, function( key, val ) { + if ( deep && typeof val === 'object' ) { + if ( typeof target[ key ] !== 'object' ) { + target[ key ] = type( val ) === 'array' ? [] : {}; + } + extend( target[ key ], val, deep ); + } else { + target[ key ] = val; + } + }); + } + + each( ('Boolean Number String Function Array Date RegExp Object' + + ' Error').split(' '), function( i, name ) { + class2type[ '[object ' + name + ']' ] = name.toLowerCase(); + }); + + function setAttribute( node, name, value ) { + value == null ? node.removeAttribute( name ) : + node.setAttribute( name, value ); + } + + /** + * 只支持ID选择。 + */ + function $( elem ) { + var api = {}; + + elem = typeof elem === 'string' && rId.test( elem ) ? + doc.getElementById( RegExp.$1 ) : elem; + + if ( elem ) { + api[ 0 ] = elem; + api.length = 1; + } + + return $.extend( api, { + _wrap: true, + + get: function() { + return elem; + }, + + /** + * 添加className + */ + addClass: function( classname ) { + elem.classList.add( classname ); + return this; + }, + + removeClass: function( classname ) { + elem.classList.remove( classname ); + return this; + }, + + //$(...).each is used in the source + each: function(callback){ + [].every.call(this, function(el, idx){ + return callback.call(el, idx, el) !== false + }) + return this + }, + + html: function( html ) { + if ( html ) { + elem.innerHTML = html; + } + return elem.innerHTML; + }, + + attr: function( key, val ) { + if ( $.isObject( key ) ) { + $.each( key, function( k, v ) { + setAttribute( elem, k, v ); + }); + } else { + setAttribute( elem, key, val ); + } + }, + + empty: function() { + elem.innerHTML = ''; + return this; + }, + + before: function( el ) { + elem.parentNode.insertBefore( el, elem ); + }, + + append: function( el ) { + el = el._wrap ? el.get() : el; + elem.appendChild( el ); + }, + + text: function() { + return elem.textContent; + }, + + // on + on: function( type, fn ) { + if ( elem.addEventListener ) { + elem.addEventListener( type, fn, false ); + } else if ( elem.attachEvent ) { + elem.attachEvent( 'on' + type, fn ); + } + + return this; + }, + + // off + off: function( type, fn ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, fn, false ); + } else if ( elem.attachEvent ) { + elem.detachEvent( 'on' + type, fn ); + } + return this; + } + + }); + } + + $.each = each; + $.extend = function( /*[deep, ]*/target/*, source...*/ ) { + var args = slice.call( arguments, 1 ), + deep; + + if ( typeof target === 'boolean' ) { + deep = target; + target = args.shift(); + } + + args.forEach(function( arg ) { + arg && extend( target, arg, deep ); + }); + + return target; + }; + + function type( obj ) { + + /*jshint eqnull:true*/ + return obj == null ? String( obj ) : + class2type[ toString.call( obj ) ] || 'object'; + } + $.type = type; + + //$.grep is used in the source + $.grep = function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + } + + $.isWindow = function( obj ) { + return obj && obj.window === obj; + }; + + $.isPlainObject = function( obj ) { + if ( type( obj ) !== 'object' || obj.nodeType || $.isWindow( obj ) ) { + return false; + } + + try { + if ( obj.constructor && !hasOwn.call( obj.constructor.prototype, + 'isPrototypeOf' ) ) { + return false; + } + } catch ( ex ) { + return false; + } + + return true; + }; + + $.isObject = function( anything ) { + return type( anything ) === 'object'; + }; + + $.trim = function( str ) { + return str ? str.trim() : ''; + }; + + $.isFunction = function( obj ) { + return type( obj ) === 'function'; + }; + + emptyArray = null; + + return $; + }); + + define('dollar',[ + 'dollar-builtin' + ], function( $ ) { + return $; + }); + /** + * 直接来源于jquery的代码。 + * @fileOverview Promise/A+ + * @beta + */ + define('promise-builtin',[ + 'dollar' + ], function( $ ) { + + var api; + + // 简单版Callbacks, 默认memory,可选once. + function Callbacks( once ) { + var list = [], + stack = !once && [], + fire = function( data ) { + memory = data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ); + } + firing = false; + + if ( list ) { + if ( stack ) { + stack.length && fire( stack.shift() ); + } else { + list = []; + } + } + }, + self = { + add: function() { + if ( list ) { + var start = list.length; + (function add ( args ) { + $.each( args, function( _, arg ) { + var type = $.type( arg ); + if ( type === 'function' ) { + list.push( arg ); + } else if ( arg && arg.length && + type !== 'string' ) { + + add( arg ); + } + }); + })( arguments ); + + if ( firing ) { + firingLength = list.length; + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + + disable: function() { + list = stack = memory = undefined; + return this; + }, + + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + + fireWith: function( context, args ) { + if ( list && (!fired || stack) ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + + fire: function() { + self.fireWith( this, arguments ); + return this; + } + }, + + fired, firing, firingStart, firingLength, firingIndex, memory; + + return self; + } + + function Deferred( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ 'resolve', 'done', Callbacks( true ), 'resolved' ], + [ 'reject', 'fail', Callbacks( true ), 'rejected' ], + [ 'notify', 'progress', Callbacks() ] + ], + state = 'pending', + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return Deferred(function( newDefer ) { + $.each( tuples, function( i, tuple ) { + var action = tuple[ 0 ], + fn = $.isFunction( fns[ i ] ) && fns[ i ]; + + // deferred[ done | fail | progress ] for + // forwarding actions to newDefer + deferred[ tuple[ 1 ] ](function() { + var returned; + + returned = fn && fn.apply( this, arguments ); + + if ( returned && + $.isFunction( returned.promise ) ) { + + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ action + 'With' ]( + this === promise ? + newDefer.promise() : + this, + fn ? [ returned ] : arguments ); + } + }); + }); + fns = null; + }).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + + return obj != null ? $.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + $.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + 'With' ]( this === deferred ? promise : + this, arguments ); + return this; + }; + deferred[ tuple[ 0 ] + 'With' ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + } + + api = { + /** + * 创建一个[Deferred](http://api.jquery.com/category/deferred-object/)对象。 + * 详细的Deferred用法说明,请参照jQuery的API文档。 + * + * Deferred对象在钩子回掉函数中经常要用到,用来处理需要等待的异步操作。 + * + * @for Base + * @method Deferred + * @grammar Base.Deferred() => Deferred + * @example + * // 在文件开始发送前做些异步操作。 + * // WebUploader会等待此异步操作完成后,开始发送文件。 + * Uploader.register({ + * 'before-send-file': 'doSomthingAsync' + * }, { + * + * doSomthingAsync: function() { + * var deferred = Base.Deferred(); + * + * // 模拟一次异步操作。 + * setTimeout(deferred.resolve, 2000); + * + * return deferred.promise(); + * } + * }); + */ + Deferred: Deferred, + + /** + * 判断传入的参数是否为一个promise对象。 + * @method isPromise + * @grammar Base.isPromise( anything ) => Boolean + * @param {*} anything 检测对象。 + * @return {Boolean} + * @for Base + * @example + * console.log( Base.isPromise() ); // => false + * console.log( Base.isPromise({ key: '123' }) ); // => false + * console.log( Base.isPromise( Base.Deferred().promise() ) ); // => true + * + * // Deferred也是一个Promise + * console.log( Base.isPromise( Base.Deferred() ) ); // => true + */ + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + }, + + /** + * 返回一个promise,此promise在所有传入的promise都完成了后完成。 + * 详细请查看[这里](http://api.jquery.com/jQuery.when/)。 + * + * @method when + * @for Base + * @grammar Base.when( promise1[, promise2[, promise3...]] ) => Promise + */ + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + slice = [].slice, + resolveValues = slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || (subordinate && + $.isFunction( subordinate.promise )) ? length : 0, + + // the master Deferred. If resolveValues consist of + // only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? + slice.call( arguments ) : value; + + if ( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !(--remaining) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && + $.isFunction( resolveValues[ i ].promise ) ) { + + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, + resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, + progressValues ) ); + } else { + --remaining; + } + } + } + + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } + }; + + return api; + }); + define('promise',[ + 'promise-builtin' + ], function( $ ) { + return $; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.6', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview 错误信息 + */ + define('lib/dnd',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function DragAndDrop( opts ) { + opts = this.options = $.extend({}, DragAndDrop.options, opts ); + + opts.container = $( opts.container ); + + if ( !opts.container.length ) { + return; + } + + RuntimeClent.call( this, 'DragAndDrop' ); + } + + DragAndDrop.options = { + accept: null, + disableGlobalDnd: false + }; + + Base.inherits( RuntimeClent, { + constructor: DragAndDrop, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( DragAndDrop.prototype ); + + return DragAndDrop; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview DragAndDrop Widget。 + */ + define('widgets/filednd',[ + 'base', + 'uploader', + 'lib/dnd', + 'widgets/widget' + ], function( Base, Uploader, Dnd ) { + var $ = Base.$; + + Uploader.options.dnd = ''; + + /** + * @property {Selector} [dnd=undefined] 指定Drag And Drop拖拽的容器,如果不指定,则不启动。 + * @namespace options + * @for Uploader + */ + + /** + * @property {Selector} [disableGlobalDnd=false] 是否禁掉整个页面的拖拽功能,如果不禁用,图片拖进来的时候会默认被浏览器打开。 + * @namespace options + * @for Uploader + */ + + /** + * @event dndAccept + * @param {DataTransferItemList} items DataTransferItem + * @description 阻止此事件可以拒绝某些类型的文件拖入进来。目前只有 chrome 提供这样的 API,且只能通过 mime-type 验证。 + * @for Uploader + */ + return Uploader.register({ + name: 'dnd', + + init: function( opts ) { + + if ( !opts.dnd || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + disableGlobalDnd: opts.disableGlobalDnd, + container: opts.dnd, + accept: opts.accept + }), + dnd; + + this.dnd = dnd = new Dnd( options ); + + dnd.once( 'ready', deferred.resolve ); + dnd.on( 'drop', function( files ) { + me.request( 'add-file', [ files ]); + }); + + // 检测文件是否全部允许添加。 + dnd.on( 'accept', function( items ) { + return me.owner.trigger( 'dndAccept', items ); + }); + + dnd.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.dnd && this.dnd.destroy(); + } + }); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepaste',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function FilePaste( opts ) { + opts = this.options = $.extend({}, opts ); + opts.container = $( opts.container || document.body ); + RuntimeClent.call( this, 'FilePaste' ); + } + + Base.inherits( RuntimeClent, { + constructor: FilePaste, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( FilePaste.prototype ); + + return FilePaste; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/filepaste',[ + 'base', + 'uploader', + 'lib/filepaste', + 'widgets/widget' + ], function( Base, Uploader, FilePaste ) { + var $ = Base.$; + + /** + * @property {Selector} [paste=undefined] 指定监听paste事件的容器,如果不指定,不启用此功能。此功能为通过粘贴来添加截屏的图片。建议设置为`document.body`. + * @namespace options + * @for Uploader + */ + return Uploader.register({ + name: 'paste', + + init: function( opts ) { + + if ( !opts.paste || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + container: opts.paste, + accept: opts.accept + }), + paste; + + this.paste = paste = new FilePaste( options ); + + paste.once( 'ready', deferred.resolve ); + paste.on( 'paste', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + paste.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.paste && this.paste.destroy(); + } + }); + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClient, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClient.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file', + style: 'webuploader-pick' //pick element class attribute, default is "webuploader-pick" + }; + + Base.inherits( RuntimeClient, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button, + style = opts.style; + + if (style) + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + if (style) + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + if (style) + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addBtn + * @for Uploader + * @grammar addBtn( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addBtn({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.on('dialogopen', function() { + me.owner.trigger('dialogOpen', picker.button); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview Image + */ + define('lib/image',[ + 'base', + 'runtime/client', + 'lib/blob' + ], function( Base, RuntimeClient, Blob ) { + var $ = Base.$; + + // 构造器。 + function Image( opts ) { + this.options = $.extend({}, Image.options, opts ); + RuntimeClient.call( this, 'Image' ); + + this.on( 'load', function() { + this._info = this.exec('info'); + this._meta = this.exec('meta'); + }); + } + + // 默认选项。 + Image.options = { + + // 默认的图片处理质量 + quality: 90, + + // 是否裁剪 + crop: false, + + // 是否保留头部信息 + preserveHeaders: false, + + // 是否允许放大。 + allowMagnify: false + }; + + // 继承RuntimeClient. + Base.inherits( RuntimeClient, { + constructor: Image, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + loadFromBlob: function( blob ) { + var me = this, + ruid = blob.getRuid(); + + this.connectRuntime( ruid, function() { + me.exec( 'init', me.options ); + me.exec( 'loadFromBlob', blob ); + }); + }, + + resize: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'resize' ].concat( args ) ); + }, + + crop: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'crop' ].concat( args ) ); + }, + + getAsDataUrl: function( type ) { + return this.exec( 'getAsDataUrl', type ); + }, + + getAsBlob: function( type ) { + var blob = this.exec( 'getAsBlob', type ); + + return new Blob( this.getRuid(), blob ); + } + }); + + return Image; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/image',[ + 'base', + 'uploader', + 'lib/image', + 'widgets/widget' + ], function( Base, Uploader, Image ) { + + var $ = Base.$, + throttle; + + // 根据要处理的文件大小来节流,一次不能处理太多,会卡。 + throttle = (function( max ) { + var occupied = 0, + waiting = [], + tick = function() { + var item; + + while ( waiting.length && occupied < max ) { + item = waiting.shift(); + occupied += item[ 0 ]; + item[ 1 ](); + } + }; + + return function( emiter, size, cb ) { + waiting.push([ size, cb ]); + emiter.once( 'destroy', function() { + occupied -= size; + setTimeout( tick, 1 ); + }); + setTimeout( tick, 1 ); + }; + })( 5 * 1024 * 1024 ); + + $.extend( Uploader.options, { + + /** + * @property {Object} [thumb] + * @namespace options + * @for Uploader + * @description 配置生成缩略图的选项。 + * + * 默认为: + * + * ```javascript + * { + * width: 110, + * height: 110, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 70, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: true, + * + * // 是否允许裁剪。 + * crop: true, + * + * // 为空的话则保留原有图片格式。 + * // 否则强制转换成指定的类型。 + * type: 'image/jpeg' + * } + * ``` + */ + thumb: { + width: 110, + height: 110, + quality: 70, + allowMagnify: true, + crop: true, + preserveHeaders: false, + + // 为空的话则保留原有图片格式。 + // 否则强制转换成指定的类型。 + // IE 8下面 base64 大小不能超过 32K 否则预览失败,而非 jpeg 编码的图片很可 + // 能会超过 32k, 所以这里设置成预览的时候都是 image/jpeg + type: 'image/jpeg' + }, + + /** + * @property {Object} [compress] + * @namespace options + * @for Uploader + * @description 配置压缩的图片的选项。如果此选项为`false`, 则图片在上传前不进行压缩。 + * + * 默认为: + * + * ```javascript + * { + * width: 1600, + * height: 1600, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 90, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: false, + * + * // 是否允许裁剪。 + * crop: false, + * + * // 是否保留头部meta信息。 + * preserveHeaders: true, + * + * // 如果发现压缩后文件大小比原来还大,则使用原来图片 + * // 此属性可能会影响图片自动纠正功能 + * noCompressIfLarger: false, + * + * // 单位字节,如果图片大小小于此值,不会采用压缩。 + * compressSize: 0 + * } + * ``` + */ + compress: { + width: 1600, + height: 1600, + quality: 90, + allowMagnify: false, + crop: false, + preserveHeaders: true + } + }); + + return Uploader.register({ + + name: 'image', + + + /** + * 生成缩略图,此过程为异步,所以需要传入`callback`。 + * 通常情况在图片加入队里后调用此方法来生成预览图以增强交互效果。 + * + * 当 width 或者 height 的值介于 0 - 1 时,被当成百分比使用。 + * + * `callback`中可以接收到两个参数。 + * * 第一个为error,如果生成缩略图有错误,此error将为真。 + * * 第二个为ret, 缩略图的Data URL值。 + * + * **注意** + * Date URL在IE6/7中不支持,所以不用调用此方法了,直接显示一张暂不支持预览图片好了。 + * 也可以借助服务端,将 base64 数据传给服务端,生成一个临时文件供预览。 + * + * @method makeThumb + * @grammar makeThumb( file, callback ) => undefined + * @grammar makeThumb( file, callback, width, height ) => undefined + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.makeThumb( file, function( error, ret ) { + * if ( error ) { + * $li.text('预览错误'); + * } else { + * $li.append(''); + * } + * }); + * + * }); + */ + makeThumb: function( file, cb, width, height ) { + var opts, image; + + file = this.request( 'get-file', file ); + + // 只预览图片格式。 + if ( !file.type.match( /^image/ ) ) { + cb( true ); + return; + } + + opts = $.extend({}, this.options.thumb ); + + // 如果传入的是object. + if ( $.isPlainObject( width ) ) { + opts = $.extend( opts, width ); + width = null; + } + + width = width || opts.width; + height = height || opts.height; + + image = new Image( opts ); + + image.once( 'load', function() { + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + // 当 resize 完后 + image.once( 'complete', function() { + cb( false, image.getAsDataUrl( opts.type ) ); + image.destroy(); + }); + + image.once( 'error', function( reason ) { + cb( reason || true ); + image.destroy(); + }); + + throttle( image, file.source.size, function() { + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + image.loadFromBlob( file.source ); + }); + }, + + beforeSendFile: function( file ) { + var opts = this.options.compress || this.options.resize, + compressSize = opts && opts.compressSize || 0, + noCompressIfLarger = opts && opts.noCompressIfLarger || false, + image, deferred; + + file = this.request( 'get-file', file ); + + // 只压缩 jpeg 图片格式。 + // gif 可能会丢失针 + // bmp png 基本上尺寸都不大,且压缩比比较小。 + if ( !opts || !~'image/jpeg,image/jpg'.indexOf( file.type ) || + file.size < compressSize || + file._compressed ) { + return; + } + + opts = $.extend({}, opts ); + deferred = Base.Deferred(); + + image = new Image( opts ); + + deferred.always(function() { + image.destroy(); + image = null; + }); + image.once( 'error', deferred.reject ); + image.once( 'load', function() { + var width = opts.width, + height = opts.height; + + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + image.once( 'complete', function() { + var blob, size; + + // 移动端 UC / qq 浏览器的无图模式下 + // ctx.getImageData 处理大图的时候会报 Exception + // INDEX_SIZE_ERR: DOM Exception 1 + try { + blob = image.getAsBlob( opts.type ); + + size = file.size; + + // 如果压缩后,比原来还大则不用压缩后的。 + if ( !noCompressIfLarger || blob.size < size ) { + // file.source.destroy && file.source.destroy(); + file.source = blob; + file.size = blob.size; + + file.trigger( 'resize', blob.size, size ); + } + + // 标记,避免重复压缩。 + file._compressed = true; + deferred.resolve(); + } catch ( e ) { + // 出错了直接继续,让其上传原始图片 + deferred.resolve(); + } + }); + + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + + image.loadFromBlob( file.source ); + return deferred.promise(); + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + if ( files.length ) { + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + // 销毁上传相关的属性。 + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定的文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + file.setStatus( Status.QUEUED ); + + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + file.setStatus( Status.PROGRESS ); + }); + + + } else if (file.getStatus() !== Status.PROGRESS) { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return Base.nextTick( me.__tick ); + } + + me.runing = true; + var files = []; + + // 如果有暂停的,则续传 + file || $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + me._trigged = false; + files.push(file); + v.transport && v.transport.send(); + } + }); + + $.each(files, function() { + this.setStatus( Status.PROGRESS ); + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this, + block; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + + + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file === file) { + block = v; + return false; + } + }); + + block.transport && block.transport.abort(); + + if (interrupt) { + me._putback(block); + me._popBlock(block); + } + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + // 正在准备中的文件。 + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePaste + */ + define('runtime/html5/dnd',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + var $ = Base.$, + prefix = 'webuploader-dnd-'; + + return Html5Runtime.register( 'DragAndDrop', { + init: function() { + var elem = this.elem = this.options.container; + + this.dragEnterHandler = Base.bindFn( this._dragEnterHandler, this ); + this.dragOverHandler = Base.bindFn( this._dragOverHandler, this ); + this.dragLeaveHandler = Base.bindFn( this._dragLeaveHandler, this ); + this.dropHandler = Base.bindFn( this._dropHandler, this ); + this.dndOver = false; + + elem.on( 'dragenter', this.dragEnterHandler ); + elem.on( 'dragover', this.dragOverHandler ); + elem.on( 'dragleave', this.dragLeaveHandler ); + elem.on( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).on( 'dragover', this.dragOverHandler ); + $( document ).on( 'drop', this.dropHandler ); + } + }, + + _dragEnterHandler: function( e ) { + var me = this, + denied = me._denied || false, + items; + + e = e.originalEvent || e; + + if ( !me.dndOver ) { + me.dndOver = true; + + // 注意只有 chrome 支持。 + items = e.dataTransfer.items; + + if ( items && items.length ) { + me._denied = denied = !me.trigger( 'accept', items ); + } + + me.elem.addClass( prefix + 'over' ); + me.elem[ denied ? 'addClass' : + 'removeClass' ]( prefix + 'denied' ); + } + + e.dataTransfer.dropEffect = denied ? 'none' : 'copy'; + + return false; + }, + + _dragOverHandler: function( e ) { + // 只处理框内的。 + var parentElem = this.elem.parent().get( 0 ); + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + clearTimeout( this._leaveTimer ); + this._dragEnterHandler.call( this, e ); + + return false; + }, + + _dragLeaveHandler: function() { + var me = this, + handler; + + handler = function() { + me.dndOver = false; + me.elem.removeClass( prefix + 'over ' + prefix + 'denied' ); + }; + + clearTimeout( me._leaveTimer ); + me._leaveTimer = setTimeout( handler, 100 ); + return false; + }, + + _dropHandler: function( e ) { + var me = this, + ruid = me.getRuid(), + parentElem = me.elem.parent().get( 0 ), + dataTransfer, data; + + // 只处理框内的。 + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + e = e.originalEvent || e; + dataTransfer = e.dataTransfer; + + // 如果是页面内拖拽,还不能处理,不阻止事件。 + // 此处 ie11 下会报参数错误, + try { + data = dataTransfer.getData('text/html'); + } catch( err ) { + } + + me.dndOver = false; + me.elem.removeClass( prefix + 'over' ); + + if ( data ) { + return; + } + + me._getTansferFiles( dataTransfer, function( results ) { + me.trigger( 'drop', $.map( results, function( file ) { + return new File( ruid, file ); + }) ); + }); + + return false; + }, + + // 如果传入 callback 则去查看文件夹,否则只管当前文件夹。 + _getTansferFiles: function( dataTransfer, callback ) { + var results = [], + promises = [], + items, files, file, item, i, len, canAccessFolder; + + items = dataTransfer.items; + files = dataTransfer.files; + + canAccessFolder = !!(items && items[ 0 ].webkitGetAsEntry); + + for ( i = 0, len = files.length; i < len; i++ ) { + file = files[ i ]; + item = items && items[ i ]; + + if ( canAccessFolder && item.webkitGetAsEntry().isDirectory ) { + + promises.push( this._traverseDirectoryTree( + item.webkitGetAsEntry(), results ) ); + } else { + results.push( file ); + } + } + + Base.when.apply( Base, promises ).done(function() { + + if ( !results.length ) { + return; + } + + callback( results ); + }); + }, + + _traverseDirectoryTree: function( entry, results ) { + var deferred = Base.Deferred(), + me = this; + + if ( entry.isFile ) { + entry.file(function( file ) { + results.push( file ); + deferred.resolve(); + }); + } else if ( entry.isDirectory ) { + entry.createReader().readEntries(function( entries ) { + var len = entries.length, + promises = [], + arr = [], // 为了保证顺序。 + i; + + for ( i = 0; i < len; i++ ) { + promises.push( me._traverseDirectoryTree( + entries[ i ], arr ) ); + } + + Base.when.apply( Base, promises ).then(function() { + results.push.apply( results, arr ); + deferred.resolve(); + }, deferred.reject ); + }); + } + + return deferred.promise(); + }, + + destroy: function() { + var elem = this.elem; + + // 还没 init 就调用 destroy + if (!elem) { + return; + } + + elem.off( 'dragenter', this.dragEnterHandler ); + elem.off( 'dragover', this.dragOverHandler ); + elem.off( 'dragleave', this.dragLeaveHandler ); + elem.off( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).off( 'dragover', this.dragOverHandler ); + $( document ).off( 'drop', this.dropHandler ); + } + } + }); + }); + + /** + * @fileOverview FilePaste + */ + define('runtime/html5/filepaste',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + return Html5Runtime.register( 'FilePaste', { + init: function() { + var opts = this.options, + elem = this.elem = opts.container, + accept = '.*', + arr, i, len, item; + + // accetp的mimeTypes中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].mimeTypes; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = arr.join(','); + accept = accept.replace( /,/g, '|' ).replace( /\*/g, '.*' ); + } + } + this.accept = accept = new RegExp( accept, 'i' ); + this.hander = Base.bindFn( this._pasteHander, this ); + elem.on( 'paste', this.hander ); + }, + + _pasteHander: function( e ) { + var allowed = [], + ruid = this.getRuid(), + items, item, blob, i, len; + + e = e.originalEvent || e; + items = e.clipboardData.items; + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + + if ( item.kind !== 'file' || !(blob = item.getAsFile()) ) { + continue; + } + + allowed.push( new File( ruid, blob ) ); + } + + if ( allowed.length ) { + // 不阻止非文件粘贴(文字粘贴)的事件冒泡 + e.preventDefault(); + e.stopPropagation(); + this.trigger( 'paste', allowed ); + } + }, + + destroy: function() { + this.elem.off( 'paste', this.hander ); + } + }); + }); + + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'capture', 'camera'); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function(e) { + input.trigger('click'); + e.stopPropagation(); + owner.trigger('dialogopen'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/util',[ + 'base' + ], function( Base ) { + + var urlAPI = window.createObjectURL && window || + window.URL && URL.revokeObjectURL && URL || + window.webkitURL, + createObjectURL = Base.noop, + revokeObjectURL = createObjectURL; + + if ( urlAPI ) { + + // 更安全的方式调用,比如android里面就能把context改成其他的对象。 + createObjectURL = function() { + return urlAPI.createObjectURL.apply( urlAPI, arguments ); + }; + + revokeObjectURL = function() { + return urlAPI.revokeObjectURL.apply( urlAPI, arguments ); + }; + } + + return { + createObjectURL: createObjectURL, + revokeObjectURL: revokeObjectURL, + + dataURL2Blob: function( dataURI ) { + var byteStr, intArray, ab, i, mimetype, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + ab = new ArrayBuffer( byteStr.length ); + intArray = new Uint8Array( ab ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + mimetype = parts[ 0 ].split(':')[ 1 ].split(';')[ 0 ]; + + return this.arrayBufferToBlob( ab, mimetype ); + }, + + dataURL2ArrayBuffer: function( dataURI ) { + var byteStr, intArray, i, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + intArray = new Uint8Array( byteStr.length ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + return intArray.buffer; + }, + + arrayBufferToBlob: function( buffer, type ) { + var builder = window.BlobBuilder || window.WebKitBlobBuilder, + bb; + + // android不支持直接new Blob, 只能借助blobbuilder. + if ( builder ) { + bb = new builder(); + bb.append( buffer ); + return bb.getBlob( type ); + } + + return new Blob([ buffer ], type ? { type: type } : {} ); + }, + + // 抽出来主要是为了解决android下面canvas.toDataUrl不支持jpeg. + // 你得到的结果是png. + canvasToDataUrl: function( canvas, type, quality ) { + return canvas.toDataURL( type, quality / 100 ); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + parseMeta: function( blob, callback ) { + callback( false, {}); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + updateImageHead: function( data ) { + return data; + } + }; + }); + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/imagemeta',[ + 'runtime/html5/util' + ], function( Util ) { + + var api; + + api = { + parsers: { + 0xffe1: [] + }, + + maxMetaDataSize: 262144, + + parse: function( blob, cb ) { + var me = this, + fr = new FileReader(); + + fr.onload = function() { + cb( false, me._parse( this.result ) ); + fr = fr.onload = fr.onerror = null; + }; + + fr.onerror = function( e ) { + cb( e.message ); + fr = fr.onload = fr.onerror = null; + }; + + blob = blob.slice( 0, me.maxMetaDataSize ); + fr.readAsArrayBuffer( blob.getSource() ); + }, + + _parse: function( buffer, noParse ) { + if ( buffer.byteLength < 6 ) { + return; + } + + var dataview = new DataView( buffer ), + offset = 2, + maxOffset = dataview.byteLength - 4, + headLength = offset, + ret = {}, + markerBytes, markerLength, parsers, i; + + if ( dataview.getUint16( 0 ) === 0xffd8 ) { + + while ( offset < maxOffset ) { + markerBytes = dataview.getUint16( offset ); + + if ( markerBytes >= 0xffe0 && markerBytes <= 0xffef || + markerBytes === 0xfffe ) { + + markerLength = dataview.getUint16( offset + 2 ) + 2; + + if ( offset + markerLength > dataview.byteLength ) { + break; + } + + parsers = api.parsers[ markerBytes ]; + + if ( !noParse && parsers ) { + for ( i = 0; i < parsers.length; i += 1 ) { + parsers[ i ].call( api, dataview, offset, + markerLength, ret ); + } + } + + offset += markerLength; + headLength = offset; + } else { + break; + } + } + + if ( headLength > 6 ) { + if ( buffer.slice ) { + ret.imageHead = buffer.slice( 2, headLength ); + } else { + // Workaround for IE10, which does not yet + // support ArrayBuffer.slice: + ret.imageHead = new Uint8Array( buffer ) + .subarray( 2, headLength ); + } + } + } + + return ret; + }, + + updateImageHead: function( buffer, head ) { + var data = this._parse( buffer, true ), + buf1, buf2, bodyoffset; + + + bodyoffset = 2; + if ( data.imageHead ) { + bodyoffset = 2 + data.imageHead.byteLength; + } + + if ( buffer.slice ) { + buf2 = buffer.slice( bodyoffset ); + } else { + buf2 = new Uint8Array( buffer ).subarray( bodyoffset ); + } + + buf1 = new Uint8Array( head.byteLength + 2 + buf2.byteLength ); + + buf1[ 0 ] = 0xFF; + buf1[ 1 ] = 0xD8; + buf1.set( new Uint8Array( head ), 2 ); + buf1.set( new Uint8Array( buf2 ), head.byteLength + 2 ); + + return buf1.buffer; + } + }; + + Util.parseMeta = function() { + return api.parse.apply( api, arguments ); + }; + + Util.updateImageHead = function() { + return api.updateImageHead.apply( api, arguments ); + }; + + return api; + }); + /** + * 代码来自于:https://github.com/blueimp/JavaScript-Load-Image + * 暂时项目中只用了orientation. + * + * 去除了 Exif Sub IFD Pointer, GPS Info IFD Pointer, Exif Thumbnail. + * @fileOverview EXIF解析 + */ + + // Sample + // ==================================== + // Make : Apple + // Model : iPhone 4S + // Orientation : 1 + // XResolution : 72 [72/1] + // YResolution : 72 [72/1] + // ResolutionUnit : 2 + // Software : QuickTime 7.7.1 + // DateTime : 2013:09:01 22:53:55 + // ExifIFDPointer : 190 + // ExposureTime : 0.058823529411764705 [1/17] + // FNumber : 2.4 [12/5] + // ExposureProgram : Normal program + // ISOSpeedRatings : 800 + // ExifVersion : 0220 + // DateTimeOriginal : 2013:09:01 22:52:51 + // DateTimeDigitized : 2013:09:01 22:52:51 + // ComponentsConfiguration : YCbCr + // ShutterSpeedValue : 4.058893515764426 + // ApertureValue : 2.5260688216892597 [4845/1918] + // BrightnessValue : -0.3126686601998395 + // MeteringMode : Pattern + // Flash : Flash did not fire, compulsory flash mode + // FocalLength : 4.28 [107/25] + // SubjectArea : [4 values] + // FlashpixVersion : 0100 + // ColorSpace : 1 + // PixelXDimension : 2448 + // PixelYDimension : 3264 + // SensingMethod : One-chip color area sensor + // ExposureMode : 0 + // WhiteBalance : Auto white balance + // FocalLengthIn35mmFilm : 35 + // SceneCaptureType : Standard + define('runtime/html5/imagemeta/exif',[ + 'base', + 'runtime/html5/imagemeta' + ], function( Base, ImageMeta ) { + + var EXIF = {}; + + EXIF.ExifMap = function() { + return this; + }; + + EXIF.ExifMap.prototype.map = { + 'Orientation': 0x0112 + }; + + EXIF.ExifMap.prototype.get = function( id ) { + return this[ id ] || this[ this.map[ id ] ]; + }; + + EXIF.exifTagTypes = { + // byte, 8-bit unsigned int: + 1: { + getValue: function( dataView, dataOffset ) { + return dataView.getUint8( dataOffset ); + }, + size: 1 + }, + + // ascii, 8-bit byte: + 2: { + getValue: function( dataView, dataOffset ) { + return String.fromCharCode( dataView.getUint8( dataOffset ) ); + }, + size: 1, + ascii: true + }, + + // short, 16 bit int: + 3: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint16( dataOffset, littleEndian ); + }, + size: 2 + }, + + // long, 32 bit int: + 4: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // rational = two long values, + // first is numerator, second is denominator: + 5: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ) / + dataView.getUint32( dataOffset + 4, littleEndian ); + }, + size: 8 + }, + + // slong, 32 bit signed int: + 9: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // srational, two slongs, first is numerator, second is denominator: + 10: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ) / + dataView.getInt32( dataOffset + 4, littleEndian ); + }, + size: 8 + } + }; + + // undefined, 8-bit byte, value depending on field: + EXIF.exifTagTypes[ 7 ] = EXIF.exifTagTypes[ 1 ]; + + EXIF.getExifValue = function( dataView, tiffOffset, offset, type, length, + littleEndian ) { + + var tagType = EXIF.exifTagTypes[ type ], + tagSize, dataOffset, values, i, str, c; + + if ( !tagType ) { + Base.log('Invalid Exif data: Invalid tag type.'); + return; + } + + tagSize = tagType.size * length; + + // Determine if the value is contained in the dataOffset bytes, + // or if the value at the dataOffset is a pointer to the actual data: + dataOffset = tagSize > 4 ? tiffOffset + dataView.getUint32( offset + 8, + littleEndian ) : (offset + 8); + + if ( dataOffset + tagSize > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid data offset.'); + return; + } + + if ( length === 1 ) { + return tagType.getValue( dataView, dataOffset, littleEndian ); + } + + values = []; + + for ( i = 0; i < length; i += 1 ) { + values[ i ] = tagType.getValue( dataView, + dataOffset + i * tagType.size, littleEndian ); + } + + if ( tagType.ascii ) { + str = ''; + + // Concatenate the chars: + for ( i = 0; i < values.length; i += 1 ) { + c = values[ i ]; + + // Ignore the terminating NULL byte(s): + if ( c === '\u0000' ) { + break; + } + str += c; + } + + return str; + } + return values; + }; + + EXIF.parseExifTag = function( dataView, tiffOffset, offset, littleEndian, + data ) { + + var tag = dataView.getUint16( offset, littleEndian ); + data.exif[ tag ] = EXIF.getExifValue( dataView, tiffOffset, offset, + dataView.getUint16( offset + 2, littleEndian ), // tag type + dataView.getUint32( offset + 4, littleEndian ), // tag length + littleEndian ); + }; + + EXIF.parseExifTags = function( dataView, tiffOffset, dirOffset, + littleEndian, data ) { + + var tagsNumber, dirEndOffset, i; + + if ( dirOffset + 6 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory offset.'); + return; + } + + tagsNumber = dataView.getUint16( dirOffset, littleEndian ); + dirEndOffset = dirOffset + 2 + 12 * tagsNumber; + + if ( dirEndOffset + 4 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory size.'); + return; + } + + for ( i = 0; i < tagsNumber; i += 1 ) { + this.parseExifTag( dataView, tiffOffset, + dirOffset + 2 + 12 * i, // tag offset + littleEndian, data ); + } + + // Return the offset to the next directory: + return dataView.getUint32( dirEndOffset, littleEndian ); + }; + + // EXIF.getExifThumbnail = function(dataView, offset, length) { + // var hexData, + // i, + // b; + // if (!length || offset + length > dataView.byteLength) { + // Base.log('Invalid Exif data: Invalid thumbnail data.'); + // return; + // } + // hexData = []; + // for (i = 0; i < length; i += 1) { + // b = dataView.getUint8(offset + i); + // hexData.push((b < 16 ? '0' : '') + b.toString(16)); + // } + // return 'data:image/jpeg,%' + hexData.join('%'); + // }; + + EXIF.parseExifData = function( dataView, offset, length, data ) { + + var tiffOffset = offset + 10, + littleEndian, dirOffset; + + // Check for the ASCII code for "Exif" (0x45786966): + if ( dataView.getUint32( offset + 4 ) !== 0x45786966 ) { + // No Exif data, might be XMP data instead + return; + } + if ( tiffOffset + 8 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid segment size.'); + return; + } + + // Check for the two null bytes: + if ( dataView.getUint16( offset + 8 ) !== 0x0000 ) { + Base.log('Invalid Exif data: Missing byte alignment offset.'); + return; + } + + // Check the byte alignment: + switch ( dataView.getUint16( tiffOffset ) ) { + case 0x4949: + littleEndian = true; + break; + + case 0x4D4D: + littleEndian = false; + break; + + default: + Base.log('Invalid Exif data: Invalid byte alignment marker.'); + return; + } + + // Check for the TIFF tag marker (0x002A): + if ( dataView.getUint16( tiffOffset + 2, littleEndian ) !== 0x002A ) { + Base.log('Invalid Exif data: Missing TIFF marker.'); + return; + } + + // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: + dirOffset = dataView.getUint32( tiffOffset + 4, littleEndian ); + // Create the exif object to store the tags: + data.exif = new EXIF.ExifMap(); + // Parse the tags of the main image directory and retrieve the + // offset to the next directory, usually the thumbnail directory: + dirOffset = EXIF.parseExifTags( dataView, tiffOffset, + tiffOffset + dirOffset, littleEndian, data ); + + // 尝试读取缩略图 + // if ( dirOffset ) { + // thumbnailData = {exif: {}}; + // dirOffset = EXIF.parseExifTags( + // dataView, + // tiffOffset, + // tiffOffset + dirOffset, + // littleEndian, + // thumbnailData + // ); + + // // Check for JPEG Thumbnail offset: + // if (thumbnailData.exif[0x0201]) { + // data.exif.Thumbnail = EXIF.getExifThumbnail( + // dataView, + // tiffOffset + thumbnailData.exif[0x0201], + // thumbnailData.exif[0x0202] // Thumbnail data length + // ); + // } + // } + }; + + ImageMeta.parsers[ 0xffe1 ].push( EXIF.parseExifData ); + return EXIF; + }); + /** + * @fileOverview Image + */ + define('runtime/html5/image',[ + 'base', + 'runtime/html5/runtime', + 'runtime/html5/util' + ], function( Base, Html5Runtime, Util ) { + + var BLANK = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D'; + + return Html5Runtime.register( 'Image', { + + // flag: 标记是否被修改过。 + modified: false, + + init: function() { + var me = this, + img = new Image(); + + img.onload = function() { + + me._info = { + type: me.type, + width: this.width, + height: this.height + }; + + //debugger; + + // 读取meta信息。 + if ( !me._metas && 'image/jpeg' === me.type ) { + Util.parseMeta( me._blob, function( error, ret ) { + me._metas = ret; + me.owner.trigger('load'); + }); + } else { + me.owner.trigger('load'); + } + }; + + img.onerror = function() { + me.owner.trigger('error'); + }; + + me._img = img; + }, + + loadFromBlob: function( blob ) { + var me = this, + img = me._img; + + me._blob = blob; + me.type = blob.type; + img.src = Util.createObjectURL( blob.getSource() ); + me.owner.once( 'load', function() { + Util.revokeObjectURL( img.src ); + }); + }, + + resize: function( width, height ) { + var canvas = this._canvas || + (this._canvas = document.createElement('canvas')); + + this._resize( this._img, canvas, width, height ); + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'resize' ); + }, + + crop: function( x, y, w, h, s ) { + var cvs = this._canvas || + (this._canvas = document.createElement('canvas')), + opts = this.options, + img = this._img, + iw = img.naturalWidth, + ih = img.naturalHeight, + orientation = this.getOrientation(); + + s = s || 1; + + // todo 解决 orientation 的问题。 + // values that require 90 degree rotation + // if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // switch ( orientation ) { + // case 6: + // tmp = x; + // x = y; + // y = iw * s - tmp - w; + // console.log(ih * s, tmp, w) + // break; + // } + + // (w ^= h, h ^= w, w ^= h); + // } + + cvs.width = w; + cvs.height = h; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + this._renderImageToCanvas( cvs, img, -x, -y, iw * s, ih * s ); + + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'crop' ); + }, + + getAsBlob: function( type ) { + var blob = this._blob, + opts = this.options, + canvas; + + type = type || this.type; + + // blob需要重新生成。 + if ( this.modified || this.type !== type ) { + canvas = this._canvas; + + if ( type === 'image/jpeg' ) { + + blob = Util.canvasToDataUrl( canvas, type, opts.quality ); + + if ( opts.preserveHeaders && this._metas && + this._metas.imageHead ) { + + blob = Util.dataURL2ArrayBuffer( blob ); + blob = Util.updateImageHead( blob, + this._metas.imageHead ); + blob = Util.arrayBufferToBlob( blob, type ); + return blob; + } + } else { + blob = Util.canvasToDataUrl( canvas, type ); + } + + blob = Util.dataURL2Blob( blob ); + } + + return blob; + }, + + getAsDataUrl: function( type ) { + var opts = this.options; + + type = type || this.type; + + if ( type === 'image/jpeg' ) { + return Util.canvasToDataUrl( this._canvas, type, opts.quality ); + } else { + return this._canvas.toDataURL( type ); + } + }, + + getOrientation: function() { + return this._metas && this._metas.exif && + this._metas.exif.get('Orientation') || 1; + }, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._metas = val; + return this; + } + + // getter + return this._metas; + }, + + destroy: function() { + var canvas = this._canvas; + this._img.onload = null; + + if ( canvas ) { + canvas.getContext('2d') + .clearRect( 0, 0, canvas.width, canvas.height ); + canvas.width = canvas.height = 0; + this._canvas = null; + } + + // 释放内存。非常重要,否则释放不了image的内存。 + this._img.src = BLANK; + this._img = this._blob = null; + }, + + _resize: function( img, cvs, width, height ) { + var opts = this.options, + naturalWidth = img.width, + naturalHeight = img.height, + orientation = this.getOrientation(), + scale, w, h, x, y; + + // values that require 90 degree rotation + if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // 交换width, height的值。 + width ^= height; + height ^= width; + width ^= height; + } + + scale = Math[ opts.crop ? 'max' : 'min' ]( width / naturalWidth, + height / naturalHeight ); + + // 不允许放大。 + opts.allowMagnify || (scale = Math.min( 1, scale )); + + w = naturalWidth * scale; + h = naturalHeight * scale; + + if ( opts.crop ) { + cvs.width = width; + cvs.height = height; + } else { + cvs.width = w; + cvs.height = h; + } + + x = (cvs.width - w) / 2; + y = (cvs.height - h) / 2; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + + this._renderImageToCanvas( cvs, img, x, y, w, h ); + }, + + _rotate2Orientaion: function( canvas, orientation ) { + var width = canvas.width, + height = canvas.height, + ctx = canvas.getContext('2d'); + + switch ( orientation ) { + case 5: + case 6: + case 7: + case 8: + canvas.width = height; + canvas.height = width; + break; + } + + 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( Math.PI ); + break; + + case 4: // vertical flip + ctx.translate( 0, height ); + ctx.scale( 1, -1 ); + break; + + case 5: // vertical flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.scale( 1, -1 ); + break; + + case 6: // 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( 0, -height ); + break; + + case 7: // horizontal flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( width, -height ); + ctx.scale( -1, 1 ); + break; + + case 8: // 90 rotate left + ctx.rotate( -0.5 * Math.PI ); + ctx.translate( -width, 0 ); + break; + } + }, + + // https://github.com/stomita/ios-imagefile-megapixel/ + // blob/master/src/megapix-image.js + _renderImageToCanvas: (function() { + + // 如果不是ios, 不需要这么复杂! + if ( !Base.os.ios ) { + return function( canvas ) { + var args = Base.slice( arguments, 1 ), + ctx = canvas.getContext('2d'); + + ctx.drawImage.apply( ctx, args ); + }; + } + + /** + * Detecting vertical squash in loaded image. + * Fixes a bug which squash image vertically while drawing into + * canvas for some images. + */ + function detectVerticalSquash( img, iw, ih ) { + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'), + sy = 0, + ey = ih, + py = ih, + data, alpha, ratio; + + + canvas.width = 1; + canvas.height = ih; + ctx.drawImage( img, 0, 0 ); + data = ctx.getImageData( 0, 0, 1, ih ).data; + + // search image edge pixel position in case + // it is squashed vertically. + while ( py > sy ) { + alpha = data[ (py - 1) * 4 + 3 ]; + + if ( alpha === 0 ) { + ey = py; + } else { + sy = py; + } + + py = (ey + sy) >> 1; + } + + ratio = (py / ih); + return (ratio === 0) ? 1 : ratio; + } + + // fix ie7 bug + // http://stackoverflow.com/questions/11929099/ + // html5-canvas-drawimage-ratio-bug-ios + if ( Base.os.ios >= 7 ) { + return function( canvas, img, x, y, w, h ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + vertSquashRatio = detectVerticalSquash( img, iw, ih ); + + return canvas.getContext('2d').drawImage( img, 0, 0, + iw * vertSquashRatio, ih * vertSquashRatio, + x, y, w, h ); + }; + } + + /** + * Detect subsampling in loaded image. + * In iOS, larger images than 2M pixels may be + * subsampled in rendering. + */ + function detectSubsampling( img ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + canvas, ctx; + + // subsampling may happen overmegapixel image + if ( iw * ih > 1024 * 1024 ) { + canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + ctx = canvas.getContext('2d'); + ctx.drawImage( img, -iw + 1, 0 ); + + // subsampled image becomes half smaller in rendering size. + // check alpha channel value to confirm image is covering + // edge pixel or not. if alpha value is 0 + // image is not covering, hence subsampled. + return ctx.getImageData( 0, 0, 1, 1 ).data[ 3 ] === 0; + } else { + return false; + } + } + + + return function( canvas, img, x, y, width, height ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + ctx = canvas.getContext('2d'), + subsampled = detectSubsampling( img ), + doSquash = this.type === 'image/jpeg', + d = 1024, + sy = 0, + dy = 0, + tmpCanvas, tmpCtx, vertSquashRatio, dw, dh, sx, dx; + + if ( subsampled ) { + iw /= 2; + ih /= 2; + } + + ctx.save(); + tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = tmpCanvas.height = d; + + tmpCtx = tmpCanvas.getContext('2d'); + vertSquashRatio = doSquash ? + detectVerticalSquash( img, iw, ih ) : 1; + + dw = Math.ceil( d * width / iw ); + dh = Math.ceil( d * height / ih / vertSquashRatio ); + + while ( sy < ih ) { + sx = 0; + dx = 0; + while ( sx < iw ) { + tmpCtx.clearRect( 0, 0, d, d ); + tmpCtx.drawImage( img, -sx, -sy ); + ctx.drawImage( tmpCanvas, 0, 0, d, d, + x + dx, y + dy, dw, dh ); + sx += d; + dx += dw; + } + sy += d; + dy += dh; + } + ctx.restore(); + tmpCanvas = tmpCtx = null; + }; + })() + }); + }); + + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + /** + * @fileOverview 只有html5实现的文件版本。 + */ + define('preset/html5only',[ + 'base', + + // widgets + 'widgets/filednd', + 'widgets/filepaste', + 'widgets/filepicker', + 'widgets/image', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + + // runtimes + // html5 + 'runtime/html5/blob', + 'runtime/html5/dnd', + 'runtime/html5/filepaste', + 'runtime/html5/filepicker', + 'runtime/html5/imagemeta/exif', + 'runtime/html5/image', + 'runtime/html5/transport' + ], function( Base ) { + return Base; + }); + define('webuploader',[ + 'preset/html5only' + ], function( preset ) { + return preset; + }); + return require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.html5only.js b/public/static/libs/webuploader/webuploader.html5only.js new file mode 100644 index 0000000..15c021f --- /dev/null +++ b/public/static/libs/webuploader/webuploader.html5only.js @@ -0,0 +1,6059 @@ +/*! WebUploader 0.1.6 */ + + +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + * @require "jquery" + * @require "zepto" + */ + define('dollar-third',[],function() { + var req = window.require; + var $ = window.__dollar || + window.jQuery || + window.Zepto || + req('jquery') || + req('zepto'); + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 使用jQuery的Promise + */ + define('promise-third',[ + 'dollar' + ], function( $ ) { + return { + Deferred: $.Deferred, + when: $.when, + + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + } + }; + }); + /** + * @fileOverview Promise/A+ + */ + define('promise',[ + 'promise-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.6', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview 错误信息 + */ + define('lib/dnd',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function DragAndDrop( opts ) { + opts = this.options = $.extend({}, DragAndDrop.options, opts ); + + opts.container = $( opts.container ); + + if ( !opts.container.length ) { + return; + } + + RuntimeClent.call( this, 'DragAndDrop' ); + } + + DragAndDrop.options = { + accept: null, + disableGlobalDnd: false + }; + + Base.inherits( RuntimeClent, { + constructor: DragAndDrop, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( DragAndDrop.prototype ); + + return DragAndDrop; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview DragAndDrop Widget。 + */ + define('widgets/filednd',[ + 'base', + 'uploader', + 'lib/dnd', + 'widgets/widget' + ], function( Base, Uploader, Dnd ) { + var $ = Base.$; + + Uploader.options.dnd = ''; + + /** + * @property {Selector} [dnd=undefined] 指定Drag And Drop拖拽的容器,如果不指定,则不启动。 + * @namespace options + * @for Uploader + */ + + /** + * @property {Selector} [disableGlobalDnd=false] 是否禁掉整个页面的拖拽功能,如果不禁用,图片拖进来的时候会默认被浏览器打开。 + * @namespace options + * @for Uploader + */ + + /** + * @event dndAccept + * @param {DataTransferItemList} items DataTransferItem + * @description 阻止此事件可以拒绝某些类型的文件拖入进来。目前只有 chrome 提供这样的 API,且只能通过 mime-type 验证。 + * @for Uploader + */ + return Uploader.register({ + name: 'dnd', + + init: function( opts ) { + + if ( !opts.dnd || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + disableGlobalDnd: opts.disableGlobalDnd, + container: opts.dnd, + accept: opts.accept + }), + dnd; + + this.dnd = dnd = new Dnd( options ); + + dnd.once( 'ready', deferred.resolve ); + dnd.on( 'drop', function( files ) { + me.request( 'add-file', [ files ]); + }); + + // 检测文件是否全部允许添加。 + dnd.on( 'accept', function( items ) { + return me.owner.trigger( 'dndAccept', items ); + }); + + dnd.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.dnd && this.dnd.destroy(); + } + }); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepaste',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function FilePaste( opts ) { + opts = this.options = $.extend({}, opts ); + opts.container = $( opts.container || document.body ); + RuntimeClent.call( this, 'FilePaste' ); + } + + Base.inherits( RuntimeClent, { + constructor: FilePaste, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( FilePaste.prototype ); + + return FilePaste; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/filepaste',[ + 'base', + 'uploader', + 'lib/filepaste', + 'widgets/widget' + ], function( Base, Uploader, FilePaste ) { + var $ = Base.$; + + /** + * @property {Selector} [paste=undefined] 指定监听paste事件的容器,如果不指定,不启用此功能。此功能为通过粘贴来添加截屏的图片。建议设置为`document.body`. + * @namespace options + * @for Uploader + */ + return Uploader.register({ + name: 'paste', + + init: function( opts ) { + + if ( !opts.paste || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + container: opts.paste, + accept: opts.accept + }), + paste; + + this.paste = paste = new FilePaste( options ); + + paste.once( 'ready', deferred.resolve ); + paste.on( 'paste', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + paste.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.paste && this.paste.destroy(); + } + }); + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClient, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClient.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file', + style: 'webuploader-pick' //pick element class attribute, default is "webuploader-pick" + }; + + Base.inherits( RuntimeClient, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button, + style = opts.style; + + if (style) + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + if (style) + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + if (style) + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addBtn + * @for Uploader + * @grammar addBtn( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addBtn({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.on('dialogopen', function() { + me.owner.trigger('dialogOpen', picker.button); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview Image + */ + define('lib/image',[ + 'base', + 'runtime/client', + 'lib/blob' + ], function( Base, RuntimeClient, Blob ) { + var $ = Base.$; + + // 构造器。 + function Image( opts ) { + this.options = $.extend({}, Image.options, opts ); + RuntimeClient.call( this, 'Image' ); + + this.on( 'load', function() { + this._info = this.exec('info'); + this._meta = this.exec('meta'); + }); + } + + // 默认选项。 + Image.options = { + + // 默认的图片处理质量 + quality: 90, + + // 是否裁剪 + crop: false, + + // 是否保留头部信息 + preserveHeaders: false, + + // 是否允许放大。 + allowMagnify: false + }; + + // 继承RuntimeClient. + Base.inherits( RuntimeClient, { + constructor: Image, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + loadFromBlob: function( blob ) { + var me = this, + ruid = blob.getRuid(); + + this.connectRuntime( ruid, function() { + me.exec( 'init', me.options ); + me.exec( 'loadFromBlob', blob ); + }); + }, + + resize: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'resize' ].concat( args ) ); + }, + + crop: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'crop' ].concat( args ) ); + }, + + getAsDataUrl: function( type ) { + return this.exec( 'getAsDataUrl', type ); + }, + + getAsBlob: function( type ) { + var blob = this.exec( 'getAsBlob', type ); + + return new Blob( this.getRuid(), blob ); + } + }); + + return Image; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/image',[ + 'base', + 'uploader', + 'lib/image', + 'widgets/widget' + ], function( Base, Uploader, Image ) { + + var $ = Base.$, + throttle; + + // 根据要处理的文件大小来节流,一次不能处理太多,会卡。 + throttle = (function( max ) { + var occupied = 0, + waiting = [], + tick = function() { + var item; + + while ( waiting.length && occupied < max ) { + item = waiting.shift(); + occupied += item[ 0 ]; + item[ 1 ](); + } + }; + + return function( emiter, size, cb ) { + waiting.push([ size, cb ]); + emiter.once( 'destroy', function() { + occupied -= size; + setTimeout( tick, 1 ); + }); + setTimeout( tick, 1 ); + }; + })( 5 * 1024 * 1024 ); + + $.extend( Uploader.options, { + + /** + * @property {Object} [thumb] + * @namespace options + * @for Uploader + * @description 配置生成缩略图的选项。 + * + * 默认为: + * + * ```javascript + * { + * width: 110, + * height: 110, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 70, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: true, + * + * // 是否允许裁剪。 + * crop: true, + * + * // 为空的话则保留原有图片格式。 + * // 否则强制转换成指定的类型。 + * type: 'image/jpeg' + * } + * ``` + */ + thumb: { + width: 110, + height: 110, + quality: 70, + allowMagnify: true, + crop: true, + preserveHeaders: false, + + // 为空的话则保留原有图片格式。 + // 否则强制转换成指定的类型。 + // IE 8下面 base64 大小不能超过 32K 否则预览失败,而非 jpeg 编码的图片很可 + // 能会超过 32k, 所以这里设置成预览的时候都是 image/jpeg + type: 'image/jpeg' + }, + + /** + * @property {Object} [compress] + * @namespace options + * @for Uploader + * @description 配置压缩的图片的选项。如果此选项为`false`, 则图片在上传前不进行压缩。 + * + * 默认为: + * + * ```javascript + * { + * width: 1600, + * height: 1600, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 90, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: false, + * + * // 是否允许裁剪。 + * crop: false, + * + * // 是否保留头部meta信息。 + * preserveHeaders: true, + * + * // 如果发现压缩后文件大小比原来还大,则使用原来图片 + * // 此属性可能会影响图片自动纠正功能 + * noCompressIfLarger: false, + * + * // 单位字节,如果图片大小小于此值,不会采用压缩。 + * compressSize: 0 + * } + * ``` + */ + compress: { + width: 1600, + height: 1600, + quality: 90, + allowMagnify: false, + crop: false, + preserveHeaders: true + } + }); + + return Uploader.register({ + + name: 'image', + + + /** + * 生成缩略图,此过程为异步,所以需要传入`callback`。 + * 通常情况在图片加入队里后调用此方法来生成预览图以增强交互效果。 + * + * 当 width 或者 height 的值介于 0 - 1 时,被当成百分比使用。 + * + * `callback`中可以接收到两个参数。 + * * 第一个为error,如果生成缩略图有错误,此error将为真。 + * * 第二个为ret, 缩略图的Data URL值。 + * + * **注意** + * Date URL在IE6/7中不支持,所以不用调用此方法了,直接显示一张暂不支持预览图片好了。 + * 也可以借助服务端,将 base64 数据传给服务端,生成一个临时文件供预览。 + * + * @method makeThumb + * @grammar makeThumb( file, callback ) => undefined + * @grammar makeThumb( file, callback, width, height ) => undefined + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.makeThumb( file, function( error, ret ) { + * if ( error ) { + * $li.text('预览错误'); + * } else { + * $li.append(''); + * } + * }); + * + * }); + */ + makeThumb: function( file, cb, width, height ) { + var opts, image; + + file = this.request( 'get-file', file ); + + // 只预览图片格式。 + if ( !file.type.match( /^image/ ) ) { + cb( true ); + return; + } + + opts = $.extend({}, this.options.thumb ); + + // 如果传入的是object. + if ( $.isPlainObject( width ) ) { + opts = $.extend( opts, width ); + width = null; + } + + width = width || opts.width; + height = height || opts.height; + + image = new Image( opts ); + + image.once( 'load', function() { + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + // 当 resize 完后 + image.once( 'complete', function() { + cb( false, image.getAsDataUrl( opts.type ) ); + image.destroy(); + }); + + image.once( 'error', function( reason ) { + cb( reason || true ); + image.destroy(); + }); + + throttle( image, file.source.size, function() { + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + image.loadFromBlob( file.source ); + }); + }, + + beforeSendFile: function( file ) { + var opts = this.options.compress || this.options.resize, + compressSize = opts && opts.compressSize || 0, + noCompressIfLarger = opts && opts.noCompressIfLarger || false, + image, deferred; + + file = this.request( 'get-file', file ); + + // 只压缩 jpeg 图片格式。 + // gif 可能会丢失针 + // bmp png 基本上尺寸都不大,且压缩比比较小。 + if ( !opts || !~'image/jpeg,image/jpg'.indexOf( file.type ) || + file.size < compressSize || + file._compressed ) { + return; + } + + opts = $.extend({}, opts ); + deferred = Base.Deferred(); + + image = new Image( opts ); + + deferred.always(function() { + image.destroy(); + image = null; + }); + image.once( 'error', deferred.reject ); + image.once( 'load', function() { + var width = opts.width, + height = opts.height; + + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + image.once( 'complete', function() { + var blob, size; + + // 移动端 UC / qq 浏览器的无图模式下 + // ctx.getImageData 处理大图的时候会报 Exception + // INDEX_SIZE_ERR: DOM Exception 1 + try { + blob = image.getAsBlob( opts.type ); + + size = file.size; + + // 如果压缩后,比原来还大则不用压缩后的。 + if ( !noCompressIfLarger || blob.size < size ) { + // file.source.destroy && file.source.destroy(); + file.source = blob; + file.size = blob.size; + + file.trigger( 'resize', blob.size, size ); + } + + // 标记,避免重复压缩。 + file._compressed = true; + deferred.resolve(); + } catch ( e ) { + // 出错了直接继续,让其上传原始图片 + deferred.resolve(); + } + }); + + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + + image.loadFromBlob( file.source ); + return deferred.promise(); + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + if ( files.length ) { + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + // 销毁上传相关的属性。 + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定的文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + file.setStatus( Status.QUEUED ); + + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + file.setStatus( Status.PROGRESS ); + }); + + + } else if (file.getStatus() !== Status.PROGRESS) { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return Base.nextTick( me.__tick ); + } + + me.runing = true; + var files = []; + + // 如果有暂停的,则续传 + file || $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + me._trigged = false; + files.push(file); + v.transport && v.transport.send(); + } + }); + + $.each(files, function() { + this.setStatus( Status.PROGRESS ); + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this, + block; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + + + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file === file) { + block = v; + return false; + } + }); + + block.transport && block.transport.abort(); + + if (interrupt) { + me._putback(block); + me._popBlock(block); + } + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + // 正在准备中的文件。 + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePaste + */ + define('runtime/html5/dnd',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + var $ = Base.$, + prefix = 'webuploader-dnd-'; + + return Html5Runtime.register( 'DragAndDrop', { + init: function() { + var elem = this.elem = this.options.container; + + this.dragEnterHandler = Base.bindFn( this._dragEnterHandler, this ); + this.dragOverHandler = Base.bindFn( this._dragOverHandler, this ); + this.dragLeaveHandler = Base.bindFn( this._dragLeaveHandler, this ); + this.dropHandler = Base.bindFn( this._dropHandler, this ); + this.dndOver = false; + + elem.on( 'dragenter', this.dragEnterHandler ); + elem.on( 'dragover', this.dragOverHandler ); + elem.on( 'dragleave', this.dragLeaveHandler ); + elem.on( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).on( 'dragover', this.dragOverHandler ); + $( document ).on( 'drop', this.dropHandler ); + } + }, + + _dragEnterHandler: function( e ) { + var me = this, + denied = me._denied || false, + items; + + e = e.originalEvent || e; + + if ( !me.dndOver ) { + me.dndOver = true; + + // 注意只有 chrome 支持。 + items = e.dataTransfer.items; + + if ( items && items.length ) { + me._denied = denied = !me.trigger( 'accept', items ); + } + + me.elem.addClass( prefix + 'over' ); + me.elem[ denied ? 'addClass' : + 'removeClass' ]( prefix + 'denied' ); + } + + e.dataTransfer.dropEffect = denied ? 'none' : 'copy'; + + return false; + }, + + _dragOverHandler: function( e ) { + // 只处理框内的。 + var parentElem = this.elem.parent().get( 0 ); + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + clearTimeout( this._leaveTimer ); + this._dragEnterHandler.call( this, e ); + + return false; + }, + + _dragLeaveHandler: function() { + var me = this, + handler; + + handler = function() { + me.dndOver = false; + me.elem.removeClass( prefix + 'over ' + prefix + 'denied' ); + }; + + clearTimeout( me._leaveTimer ); + me._leaveTimer = setTimeout( handler, 100 ); + return false; + }, + + _dropHandler: function( e ) { + var me = this, + ruid = me.getRuid(), + parentElem = me.elem.parent().get( 0 ), + dataTransfer, data; + + // 只处理框内的。 + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + e = e.originalEvent || e; + dataTransfer = e.dataTransfer; + + // 如果是页面内拖拽,还不能处理,不阻止事件。 + // 此处 ie11 下会报参数错误, + try { + data = dataTransfer.getData('text/html'); + } catch( err ) { + } + + me.dndOver = false; + me.elem.removeClass( prefix + 'over' ); + + if ( data ) { + return; + } + + me._getTansferFiles( dataTransfer, function( results ) { + me.trigger( 'drop', $.map( results, function( file ) { + return new File( ruid, file ); + }) ); + }); + + return false; + }, + + // 如果传入 callback 则去查看文件夹,否则只管当前文件夹。 + _getTansferFiles: function( dataTransfer, callback ) { + var results = [], + promises = [], + items, files, file, item, i, len, canAccessFolder; + + items = dataTransfer.items; + files = dataTransfer.files; + + canAccessFolder = !!(items && items[ 0 ].webkitGetAsEntry); + + for ( i = 0, len = files.length; i < len; i++ ) { + file = files[ i ]; + item = items && items[ i ]; + + if ( canAccessFolder && item.webkitGetAsEntry().isDirectory ) { + + promises.push( this._traverseDirectoryTree( + item.webkitGetAsEntry(), results ) ); + } else { + results.push( file ); + } + } + + Base.when.apply( Base, promises ).done(function() { + + if ( !results.length ) { + return; + } + + callback( results ); + }); + }, + + _traverseDirectoryTree: function( entry, results ) { + var deferred = Base.Deferred(), + me = this; + + if ( entry.isFile ) { + entry.file(function( file ) { + results.push( file ); + deferred.resolve(); + }); + } else if ( entry.isDirectory ) { + entry.createReader().readEntries(function( entries ) { + var len = entries.length, + promises = [], + arr = [], // 为了保证顺序。 + i; + + for ( i = 0; i < len; i++ ) { + promises.push( me._traverseDirectoryTree( + entries[ i ], arr ) ); + } + + Base.when.apply( Base, promises ).then(function() { + results.push.apply( results, arr ); + deferred.resolve(); + }, deferred.reject ); + }); + } + + return deferred.promise(); + }, + + destroy: function() { + var elem = this.elem; + + // 还没 init 就调用 destroy + if (!elem) { + return; + } + + elem.off( 'dragenter', this.dragEnterHandler ); + elem.off( 'dragover', this.dragOverHandler ); + elem.off( 'dragleave', this.dragLeaveHandler ); + elem.off( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).off( 'dragover', this.dragOverHandler ); + $( document ).off( 'drop', this.dropHandler ); + } + } + }); + }); + + /** + * @fileOverview FilePaste + */ + define('runtime/html5/filepaste',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + return Html5Runtime.register( 'FilePaste', { + init: function() { + var opts = this.options, + elem = this.elem = opts.container, + accept = '.*', + arr, i, len, item; + + // accetp的mimeTypes中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].mimeTypes; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = arr.join(','); + accept = accept.replace( /,/g, '|' ).replace( /\*/g, '.*' ); + } + } + this.accept = accept = new RegExp( accept, 'i' ); + this.hander = Base.bindFn( this._pasteHander, this ); + elem.on( 'paste', this.hander ); + }, + + _pasteHander: function( e ) { + var allowed = [], + ruid = this.getRuid(), + items, item, blob, i, len; + + e = e.originalEvent || e; + items = e.clipboardData.items; + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + + if ( item.kind !== 'file' || !(blob = item.getAsFile()) ) { + continue; + } + + allowed.push( new File( ruid, blob ) ); + } + + if ( allowed.length ) { + // 不阻止非文件粘贴(文字粘贴)的事件冒泡 + e.preventDefault(); + e.stopPropagation(); + this.trigger( 'paste', allowed ); + } + }, + + destroy: function() { + this.elem.off( 'paste', this.hander ); + } + }); + }); + + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'capture', 'camera'); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function(e) { + input.trigger('click'); + e.stopPropagation(); + owner.trigger('dialogopen'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/util',[ + 'base' + ], function( Base ) { + + var urlAPI = window.createObjectURL && window || + window.URL && URL.revokeObjectURL && URL || + window.webkitURL, + createObjectURL = Base.noop, + revokeObjectURL = createObjectURL; + + if ( urlAPI ) { + + // 更安全的方式调用,比如android里面就能把context改成其他的对象。 + createObjectURL = function() { + return urlAPI.createObjectURL.apply( urlAPI, arguments ); + }; + + revokeObjectURL = function() { + return urlAPI.revokeObjectURL.apply( urlAPI, arguments ); + }; + } + + return { + createObjectURL: createObjectURL, + revokeObjectURL: revokeObjectURL, + + dataURL2Blob: function( dataURI ) { + var byteStr, intArray, ab, i, mimetype, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + ab = new ArrayBuffer( byteStr.length ); + intArray = new Uint8Array( ab ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + mimetype = parts[ 0 ].split(':')[ 1 ].split(';')[ 0 ]; + + return this.arrayBufferToBlob( ab, mimetype ); + }, + + dataURL2ArrayBuffer: function( dataURI ) { + var byteStr, intArray, i, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + intArray = new Uint8Array( byteStr.length ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + return intArray.buffer; + }, + + arrayBufferToBlob: function( buffer, type ) { + var builder = window.BlobBuilder || window.WebKitBlobBuilder, + bb; + + // android不支持直接new Blob, 只能借助blobbuilder. + if ( builder ) { + bb = new builder(); + bb.append( buffer ); + return bb.getBlob( type ); + } + + return new Blob([ buffer ], type ? { type: type } : {} ); + }, + + // 抽出来主要是为了解决android下面canvas.toDataUrl不支持jpeg. + // 你得到的结果是png. + canvasToDataUrl: function( canvas, type, quality ) { + return canvas.toDataURL( type, quality / 100 ); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + parseMeta: function( blob, callback ) { + callback( false, {}); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + updateImageHead: function( data ) { + return data; + } + }; + }); + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/imagemeta',[ + 'runtime/html5/util' + ], function( Util ) { + + var api; + + api = { + parsers: { + 0xffe1: [] + }, + + maxMetaDataSize: 262144, + + parse: function( blob, cb ) { + var me = this, + fr = new FileReader(); + + fr.onload = function() { + cb( false, me._parse( this.result ) ); + fr = fr.onload = fr.onerror = null; + }; + + fr.onerror = function( e ) { + cb( e.message ); + fr = fr.onload = fr.onerror = null; + }; + + blob = blob.slice( 0, me.maxMetaDataSize ); + fr.readAsArrayBuffer( blob.getSource() ); + }, + + _parse: function( buffer, noParse ) { + if ( buffer.byteLength < 6 ) { + return; + } + + var dataview = new DataView( buffer ), + offset = 2, + maxOffset = dataview.byteLength - 4, + headLength = offset, + ret = {}, + markerBytes, markerLength, parsers, i; + + if ( dataview.getUint16( 0 ) === 0xffd8 ) { + + while ( offset < maxOffset ) { + markerBytes = dataview.getUint16( offset ); + + if ( markerBytes >= 0xffe0 && markerBytes <= 0xffef || + markerBytes === 0xfffe ) { + + markerLength = dataview.getUint16( offset + 2 ) + 2; + + if ( offset + markerLength > dataview.byteLength ) { + break; + } + + parsers = api.parsers[ markerBytes ]; + + if ( !noParse && parsers ) { + for ( i = 0; i < parsers.length; i += 1 ) { + parsers[ i ].call( api, dataview, offset, + markerLength, ret ); + } + } + + offset += markerLength; + headLength = offset; + } else { + break; + } + } + + if ( headLength > 6 ) { + if ( buffer.slice ) { + ret.imageHead = buffer.slice( 2, headLength ); + } else { + // Workaround for IE10, which does not yet + // support ArrayBuffer.slice: + ret.imageHead = new Uint8Array( buffer ) + .subarray( 2, headLength ); + } + } + } + + return ret; + }, + + updateImageHead: function( buffer, head ) { + var data = this._parse( buffer, true ), + buf1, buf2, bodyoffset; + + + bodyoffset = 2; + if ( data.imageHead ) { + bodyoffset = 2 + data.imageHead.byteLength; + } + + if ( buffer.slice ) { + buf2 = buffer.slice( bodyoffset ); + } else { + buf2 = new Uint8Array( buffer ).subarray( bodyoffset ); + } + + buf1 = new Uint8Array( head.byteLength + 2 + buf2.byteLength ); + + buf1[ 0 ] = 0xFF; + buf1[ 1 ] = 0xD8; + buf1.set( new Uint8Array( head ), 2 ); + buf1.set( new Uint8Array( buf2 ), head.byteLength + 2 ); + + return buf1.buffer; + } + }; + + Util.parseMeta = function() { + return api.parse.apply( api, arguments ); + }; + + Util.updateImageHead = function() { + return api.updateImageHead.apply( api, arguments ); + }; + + return api; + }); + /** + * 代码来自于:https://github.com/blueimp/JavaScript-Load-Image + * 暂时项目中只用了orientation. + * + * 去除了 Exif Sub IFD Pointer, GPS Info IFD Pointer, Exif Thumbnail. + * @fileOverview EXIF解析 + */ + + // Sample + // ==================================== + // Make : Apple + // Model : iPhone 4S + // Orientation : 1 + // XResolution : 72 [72/1] + // YResolution : 72 [72/1] + // ResolutionUnit : 2 + // Software : QuickTime 7.7.1 + // DateTime : 2013:09:01 22:53:55 + // ExifIFDPointer : 190 + // ExposureTime : 0.058823529411764705 [1/17] + // FNumber : 2.4 [12/5] + // ExposureProgram : Normal program + // ISOSpeedRatings : 800 + // ExifVersion : 0220 + // DateTimeOriginal : 2013:09:01 22:52:51 + // DateTimeDigitized : 2013:09:01 22:52:51 + // ComponentsConfiguration : YCbCr + // ShutterSpeedValue : 4.058893515764426 + // ApertureValue : 2.5260688216892597 [4845/1918] + // BrightnessValue : -0.3126686601998395 + // MeteringMode : Pattern + // Flash : Flash did not fire, compulsory flash mode + // FocalLength : 4.28 [107/25] + // SubjectArea : [4 values] + // FlashpixVersion : 0100 + // ColorSpace : 1 + // PixelXDimension : 2448 + // PixelYDimension : 3264 + // SensingMethod : One-chip color area sensor + // ExposureMode : 0 + // WhiteBalance : Auto white balance + // FocalLengthIn35mmFilm : 35 + // SceneCaptureType : Standard + define('runtime/html5/imagemeta/exif',[ + 'base', + 'runtime/html5/imagemeta' + ], function( Base, ImageMeta ) { + + var EXIF = {}; + + EXIF.ExifMap = function() { + return this; + }; + + EXIF.ExifMap.prototype.map = { + 'Orientation': 0x0112 + }; + + EXIF.ExifMap.prototype.get = function( id ) { + return this[ id ] || this[ this.map[ id ] ]; + }; + + EXIF.exifTagTypes = { + // byte, 8-bit unsigned int: + 1: { + getValue: function( dataView, dataOffset ) { + return dataView.getUint8( dataOffset ); + }, + size: 1 + }, + + // ascii, 8-bit byte: + 2: { + getValue: function( dataView, dataOffset ) { + return String.fromCharCode( dataView.getUint8( dataOffset ) ); + }, + size: 1, + ascii: true + }, + + // short, 16 bit int: + 3: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint16( dataOffset, littleEndian ); + }, + size: 2 + }, + + // long, 32 bit int: + 4: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // rational = two long values, + // first is numerator, second is denominator: + 5: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ) / + dataView.getUint32( dataOffset + 4, littleEndian ); + }, + size: 8 + }, + + // slong, 32 bit signed int: + 9: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // srational, two slongs, first is numerator, second is denominator: + 10: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ) / + dataView.getInt32( dataOffset + 4, littleEndian ); + }, + size: 8 + } + }; + + // undefined, 8-bit byte, value depending on field: + EXIF.exifTagTypes[ 7 ] = EXIF.exifTagTypes[ 1 ]; + + EXIF.getExifValue = function( dataView, tiffOffset, offset, type, length, + littleEndian ) { + + var tagType = EXIF.exifTagTypes[ type ], + tagSize, dataOffset, values, i, str, c; + + if ( !tagType ) { + Base.log('Invalid Exif data: Invalid tag type.'); + return; + } + + tagSize = tagType.size * length; + + // Determine if the value is contained in the dataOffset bytes, + // or if the value at the dataOffset is a pointer to the actual data: + dataOffset = tagSize > 4 ? tiffOffset + dataView.getUint32( offset + 8, + littleEndian ) : (offset + 8); + + if ( dataOffset + tagSize > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid data offset.'); + return; + } + + if ( length === 1 ) { + return tagType.getValue( dataView, dataOffset, littleEndian ); + } + + values = []; + + for ( i = 0; i < length; i += 1 ) { + values[ i ] = tagType.getValue( dataView, + dataOffset + i * tagType.size, littleEndian ); + } + + if ( tagType.ascii ) { + str = ''; + + // Concatenate the chars: + for ( i = 0; i < values.length; i += 1 ) { + c = values[ i ]; + + // Ignore the terminating NULL byte(s): + if ( c === '\u0000' ) { + break; + } + str += c; + } + + return str; + } + return values; + }; + + EXIF.parseExifTag = function( dataView, tiffOffset, offset, littleEndian, + data ) { + + var tag = dataView.getUint16( offset, littleEndian ); + data.exif[ tag ] = EXIF.getExifValue( dataView, tiffOffset, offset, + dataView.getUint16( offset + 2, littleEndian ), // tag type + dataView.getUint32( offset + 4, littleEndian ), // tag length + littleEndian ); + }; + + EXIF.parseExifTags = function( dataView, tiffOffset, dirOffset, + littleEndian, data ) { + + var tagsNumber, dirEndOffset, i; + + if ( dirOffset + 6 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory offset.'); + return; + } + + tagsNumber = dataView.getUint16( dirOffset, littleEndian ); + dirEndOffset = dirOffset + 2 + 12 * tagsNumber; + + if ( dirEndOffset + 4 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory size.'); + return; + } + + for ( i = 0; i < tagsNumber; i += 1 ) { + this.parseExifTag( dataView, tiffOffset, + dirOffset + 2 + 12 * i, // tag offset + littleEndian, data ); + } + + // Return the offset to the next directory: + return dataView.getUint32( dirEndOffset, littleEndian ); + }; + + // EXIF.getExifThumbnail = function(dataView, offset, length) { + // var hexData, + // i, + // b; + // if (!length || offset + length > dataView.byteLength) { + // Base.log('Invalid Exif data: Invalid thumbnail data.'); + // return; + // } + // hexData = []; + // for (i = 0; i < length; i += 1) { + // b = dataView.getUint8(offset + i); + // hexData.push((b < 16 ? '0' : '') + b.toString(16)); + // } + // return 'data:image/jpeg,%' + hexData.join('%'); + // }; + + EXIF.parseExifData = function( dataView, offset, length, data ) { + + var tiffOffset = offset + 10, + littleEndian, dirOffset; + + // Check for the ASCII code for "Exif" (0x45786966): + if ( dataView.getUint32( offset + 4 ) !== 0x45786966 ) { + // No Exif data, might be XMP data instead + return; + } + if ( tiffOffset + 8 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid segment size.'); + return; + } + + // Check for the two null bytes: + if ( dataView.getUint16( offset + 8 ) !== 0x0000 ) { + Base.log('Invalid Exif data: Missing byte alignment offset.'); + return; + } + + // Check the byte alignment: + switch ( dataView.getUint16( tiffOffset ) ) { + case 0x4949: + littleEndian = true; + break; + + case 0x4D4D: + littleEndian = false; + break; + + default: + Base.log('Invalid Exif data: Invalid byte alignment marker.'); + return; + } + + // Check for the TIFF tag marker (0x002A): + if ( dataView.getUint16( tiffOffset + 2, littleEndian ) !== 0x002A ) { + Base.log('Invalid Exif data: Missing TIFF marker.'); + return; + } + + // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: + dirOffset = dataView.getUint32( tiffOffset + 4, littleEndian ); + // Create the exif object to store the tags: + data.exif = new EXIF.ExifMap(); + // Parse the tags of the main image directory and retrieve the + // offset to the next directory, usually the thumbnail directory: + dirOffset = EXIF.parseExifTags( dataView, tiffOffset, + tiffOffset + dirOffset, littleEndian, data ); + + // 尝试读取缩略图 + // if ( dirOffset ) { + // thumbnailData = {exif: {}}; + // dirOffset = EXIF.parseExifTags( + // dataView, + // tiffOffset, + // tiffOffset + dirOffset, + // littleEndian, + // thumbnailData + // ); + + // // Check for JPEG Thumbnail offset: + // if (thumbnailData.exif[0x0201]) { + // data.exif.Thumbnail = EXIF.getExifThumbnail( + // dataView, + // tiffOffset + thumbnailData.exif[0x0201], + // thumbnailData.exif[0x0202] // Thumbnail data length + // ); + // } + // } + }; + + ImageMeta.parsers[ 0xffe1 ].push( EXIF.parseExifData ); + return EXIF; + }); + /** + * @fileOverview Image + */ + define('runtime/html5/image',[ + 'base', + 'runtime/html5/runtime', + 'runtime/html5/util' + ], function( Base, Html5Runtime, Util ) { + + var BLANK = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D'; + + return Html5Runtime.register( 'Image', { + + // flag: 标记是否被修改过。 + modified: false, + + init: function() { + var me = this, + img = new Image(); + + img.onload = function() { + + me._info = { + type: me.type, + width: this.width, + height: this.height + }; + + //debugger; + + // 读取meta信息。 + if ( !me._metas && 'image/jpeg' === me.type ) { + Util.parseMeta( me._blob, function( error, ret ) { + me._metas = ret; + me.owner.trigger('load'); + }); + } else { + me.owner.trigger('load'); + } + }; + + img.onerror = function() { + me.owner.trigger('error'); + }; + + me._img = img; + }, + + loadFromBlob: function( blob ) { + var me = this, + img = me._img; + + me._blob = blob; + me.type = blob.type; + img.src = Util.createObjectURL( blob.getSource() ); + me.owner.once( 'load', function() { + Util.revokeObjectURL( img.src ); + }); + }, + + resize: function( width, height ) { + var canvas = this._canvas || + (this._canvas = document.createElement('canvas')); + + this._resize( this._img, canvas, width, height ); + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'resize' ); + }, + + crop: function( x, y, w, h, s ) { + var cvs = this._canvas || + (this._canvas = document.createElement('canvas')), + opts = this.options, + img = this._img, + iw = img.naturalWidth, + ih = img.naturalHeight, + orientation = this.getOrientation(); + + s = s || 1; + + // todo 解决 orientation 的问题。 + // values that require 90 degree rotation + // if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // switch ( orientation ) { + // case 6: + // tmp = x; + // x = y; + // y = iw * s - tmp - w; + // console.log(ih * s, tmp, w) + // break; + // } + + // (w ^= h, h ^= w, w ^= h); + // } + + cvs.width = w; + cvs.height = h; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + this._renderImageToCanvas( cvs, img, -x, -y, iw * s, ih * s ); + + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'crop' ); + }, + + getAsBlob: function( type ) { + var blob = this._blob, + opts = this.options, + canvas; + + type = type || this.type; + + // blob需要重新生成。 + if ( this.modified || this.type !== type ) { + canvas = this._canvas; + + if ( type === 'image/jpeg' ) { + + blob = Util.canvasToDataUrl( canvas, type, opts.quality ); + + if ( opts.preserveHeaders && this._metas && + this._metas.imageHead ) { + + blob = Util.dataURL2ArrayBuffer( blob ); + blob = Util.updateImageHead( blob, + this._metas.imageHead ); + blob = Util.arrayBufferToBlob( blob, type ); + return blob; + } + } else { + blob = Util.canvasToDataUrl( canvas, type ); + } + + blob = Util.dataURL2Blob( blob ); + } + + return blob; + }, + + getAsDataUrl: function( type ) { + var opts = this.options; + + type = type || this.type; + + if ( type === 'image/jpeg' ) { + return Util.canvasToDataUrl( this._canvas, type, opts.quality ); + } else { + return this._canvas.toDataURL( type ); + } + }, + + getOrientation: function() { + return this._metas && this._metas.exif && + this._metas.exif.get('Orientation') || 1; + }, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._metas = val; + return this; + } + + // getter + return this._metas; + }, + + destroy: function() { + var canvas = this._canvas; + this._img.onload = null; + + if ( canvas ) { + canvas.getContext('2d') + .clearRect( 0, 0, canvas.width, canvas.height ); + canvas.width = canvas.height = 0; + this._canvas = null; + } + + // 释放内存。非常重要,否则释放不了image的内存。 + this._img.src = BLANK; + this._img = this._blob = null; + }, + + _resize: function( img, cvs, width, height ) { + var opts = this.options, + naturalWidth = img.width, + naturalHeight = img.height, + orientation = this.getOrientation(), + scale, w, h, x, y; + + // values that require 90 degree rotation + if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // 交换width, height的值。 + width ^= height; + height ^= width; + width ^= height; + } + + scale = Math[ opts.crop ? 'max' : 'min' ]( width / naturalWidth, + height / naturalHeight ); + + // 不允许放大。 + opts.allowMagnify || (scale = Math.min( 1, scale )); + + w = naturalWidth * scale; + h = naturalHeight * scale; + + if ( opts.crop ) { + cvs.width = width; + cvs.height = height; + } else { + cvs.width = w; + cvs.height = h; + } + + x = (cvs.width - w) / 2; + y = (cvs.height - h) / 2; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + + this._renderImageToCanvas( cvs, img, x, y, w, h ); + }, + + _rotate2Orientaion: function( canvas, orientation ) { + var width = canvas.width, + height = canvas.height, + ctx = canvas.getContext('2d'); + + switch ( orientation ) { + case 5: + case 6: + case 7: + case 8: + canvas.width = height; + canvas.height = width; + break; + } + + 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( Math.PI ); + break; + + case 4: // vertical flip + ctx.translate( 0, height ); + ctx.scale( 1, -1 ); + break; + + case 5: // vertical flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.scale( 1, -1 ); + break; + + case 6: // 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( 0, -height ); + break; + + case 7: // horizontal flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( width, -height ); + ctx.scale( -1, 1 ); + break; + + case 8: // 90 rotate left + ctx.rotate( -0.5 * Math.PI ); + ctx.translate( -width, 0 ); + break; + } + }, + + // https://github.com/stomita/ios-imagefile-megapixel/ + // blob/master/src/megapix-image.js + _renderImageToCanvas: (function() { + + // 如果不是ios, 不需要这么复杂! + if ( !Base.os.ios ) { + return function( canvas ) { + var args = Base.slice( arguments, 1 ), + ctx = canvas.getContext('2d'); + + ctx.drawImage.apply( ctx, args ); + }; + } + + /** + * Detecting vertical squash in loaded image. + * Fixes a bug which squash image vertically while drawing into + * canvas for some images. + */ + function detectVerticalSquash( img, iw, ih ) { + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'), + sy = 0, + ey = ih, + py = ih, + data, alpha, ratio; + + + canvas.width = 1; + canvas.height = ih; + ctx.drawImage( img, 0, 0 ); + data = ctx.getImageData( 0, 0, 1, ih ).data; + + // search image edge pixel position in case + // it is squashed vertically. + while ( py > sy ) { + alpha = data[ (py - 1) * 4 + 3 ]; + + if ( alpha === 0 ) { + ey = py; + } else { + sy = py; + } + + py = (ey + sy) >> 1; + } + + ratio = (py / ih); + return (ratio === 0) ? 1 : ratio; + } + + // fix ie7 bug + // http://stackoverflow.com/questions/11929099/ + // html5-canvas-drawimage-ratio-bug-ios + if ( Base.os.ios >= 7 ) { + return function( canvas, img, x, y, w, h ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + vertSquashRatio = detectVerticalSquash( img, iw, ih ); + + return canvas.getContext('2d').drawImage( img, 0, 0, + iw * vertSquashRatio, ih * vertSquashRatio, + x, y, w, h ); + }; + } + + /** + * Detect subsampling in loaded image. + * In iOS, larger images than 2M pixels may be + * subsampled in rendering. + */ + function detectSubsampling( img ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + canvas, ctx; + + // subsampling may happen overmegapixel image + if ( iw * ih > 1024 * 1024 ) { + canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + ctx = canvas.getContext('2d'); + ctx.drawImage( img, -iw + 1, 0 ); + + // subsampled image becomes half smaller in rendering size. + // check alpha channel value to confirm image is covering + // edge pixel or not. if alpha value is 0 + // image is not covering, hence subsampled. + return ctx.getImageData( 0, 0, 1, 1 ).data[ 3 ] === 0; + } else { + return false; + } + } + + + return function( canvas, img, x, y, width, height ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + ctx = canvas.getContext('2d'), + subsampled = detectSubsampling( img ), + doSquash = this.type === 'image/jpeg', + d = 1024, + sy = 0, + dy = 0, + tmpCanvas, tmpCtx, vertSquashRatio, dw, dh, sx, dx; + + if ( subsampled ) { + iw /= 2; + ih /= 2; + } + + ctx.save(); + tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = tmpCanvas.height = d; + + tmpCtx = tmpCanvas.getContext('2d'); + vertSquashRatio = doSquash ? + detectVerticalSquash( img, iw, ih ) : 1; + + dw = Math.ceil( d * width / iw ); + dh = Math.ceil( d * height / ih / vertSquashRatio ); + + while ( sy < ih ) { + sx = 0; + dx = 0; + while ( sx < iw ) { + tmpCtx.clearRect( 0, 0, d, d ); + tmpCtx.drawImage( img, -sx, -sy ); + ctx.drawImage( tmpCanvas, 0, 0, d, d, + x + dx, y + dy, dw, dh ); + sx += d; + dx += dw; + } + sy += d; + dy += dh; + } + ctx.restore(); + tmpCanvas = tmpCtx = null; + }; + })() + }); + }); + + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + /** + * @fileOverview 只有html5实现的文件版本。 + */ + define('preset/html5only',[ + 'base', + + // widgets + 'widgets/filednd', + 'widgets/filepaste', + 'widgets/filepicker', + 'widgets/image', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + + // runtimes + // html5 + 'runtime/html5/blob', + 'runtime/html5/dnd', + 'runtime/html5/filepaste', + 'runtime/html5/filepicker', + 'runtime/html5/imagemeta/exif', + 'runtime/html5/image', + 'runtime/html5/transport' + ], function( Base ) { + return Base; + }); + define('webuploader',[ + 'preset/html5only' + ], function( preset ) { + return preset; + }); + return require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.html5only.min.js b/public/static/libs/webuploader/webuploader.html5only.min.js new file mode 100644 index 0000000..686fef5 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.html5only.min.js @@ -0,0 +1,2 @@ +/* WebUploader 0.1.6 */!function(a,b){var c,d={},e=function(a,b){var c,d,e;if("string"==typeof a)return h(a);for(c=[],d=a.length,e=0;d>e;e++)c.push(h(a[e]));return b.apply(null,c)},f=function(a,b,c){2===arguments.length&&(c=b,b=null),e(b||[],function(){g(a,c,arguments)})},g=function(a,b,c){var f,g={exports:b};"function"==typeof b&&(c.length||(c=[e,g.exports,g]),f=b.apply(null,c),void 0!==f&&(g.exports=f)),d[a]=g.exports},h=function(b){var c=d[b]||a[b];if(!c)throw new Error("`"+b+"` is undefined");return c},i=function(a){var b,c,e,f,g,h;h=function(a){return a&&a.charAt(0).toUpperCase()+a.substr(1)};for(b in d)if(c=a,d.hasOwnProperty(b)){for(e=b.split("/"),g=h(e.pop());f=h(e.shift());)c[f]=c[f]||{},c=c[f];c[g]=d[b]}return a},j=function(c){return a.__dollar=c,i(b(a,f,e))};"object"==typeof module&&"object"==typeof module.exports?module.exports=j():"function"==typeof define&&define.amd?define(["jquery"],j):(c=a.WebUploader,a.WebUploader=j(),a.WebUploader.noConflict=function(){a.WebUploader=c})}(window,function(a,b,c){return b("dollar-third",[],function(){var b=a.require,c=a.__dollar||a.jQuery||a.Zepto||b("jquery")||b("zepto");if(!c)throw new Error("jQuery or Zepto not found!");return c}),b("dollar",["dollar-third"],function(a){return a}),b("promise-third",["dollar"],function(a){return{Deferred:a.Deferred,when:a.when,isPromise:function(a){return a&&"function"==typeof a.then}}}),b("promise",["promise-third"],function(a){return a}),b("base",["dollar","promise"],function(b,c){function d(a){return function(){return h.apply(a,arguments)}}function e(a,b){return function(){return a.apply(b,arguments)}}function f(a){var b;return Object.create?Object.create(a):(b=function(){},b.prototype=a,new b)}var g=function(){},h=Function.call;return{version:"0.1.6",$:b,Deferred:c.Deferred,isPromise:c.isPromise,when:c.when,browser:function(a){var b={},c=a.match(/WebKit\/([\d.]+)/),d=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),e=a.match(/MSIE\s([\d\.]+)/)||a.match(/(?:trident)(?:.*rv:([\w.]+))?/i),f=a.match(/Firefox\/([\d.]+)/),g=a.match(/Safari\/([\d.]+)/),h=a.match(/OPR\/([\d.]+)/);return c&&(b.webkit=parseFloat(c[1])),d&&(b.chrome=parseFloat(d[1])),e&&(b.ie=parseFloat(e[1])),f&&(b.firefox=parseFloat(f[1])),g&&(b.safari=parseFloat(g[1])),h&&(b.opera=parseFloat(h[1])),b}(navigator.userAgent),os:function(a){var b={},c=a.match(/(?:Android);?[\s\/]+([\d.]+)?/),d=a.match(/(?:iPad|iPod|iPhone).*OS\s([\d_]+)/);return c&&(b.android=parseFloat(c[1])),d&&(b.ios=parseFloat(d[1].replace(/_/g,"."))),b}(navigator.userAgent),inherits:function(a,c,d){var e;return"function"==typeof c?(e=c,c=null):e=c&&c.hasOwnProperty("constructor")?c.constructor:function(){return a.apply(this,arguments)},b.extend(!0,e,a,d||{}),e.__super__=a.prototype,e.prototype=f(a.prototype),c&&b.extend(!0,e.prototype,c),e},noop:g,bindFn:e,log:function(){return a.console?e(console.log,console):g}(),nextTick:function(){return function(a){setTimeout(a,1)}}(),slice:d([].slice),guid:function(){var a=0;return function(b){for(var c=(+new Date).toString(32),d=0;5>d;d++)c+=Math.floor(65535*Math.random()).toString(32);return(b||"wu_")+c+(a++).toString(32)}}(),formatSize:function(a,b,c){var d;for(c=c||["B","K","M","G","TB"];(d=c.shift())&&a>1024;)a/=1024;return("B"===d?a:a.toFixed(b||2))+d}}}),b("mediator",["base"],function(a){function b(a,b,c,d){return f.grep(a,function(a){return!(!a||b&&a.e!==b||c&&a.cb!==c&&a.cb._cb!==c||d&&a.ctx!==d)})}function c(a,b,c){f.each((a||"").split(h),function(a,d){c(d,b)})}function d(a,b){for(var c,d=!1,e=-1,f=a.length;++e1?(d.isPlainObject(b)&&d.isPlainObject(c[a])?d.extend(c[a],b):c[a]=b,void 0):a?c[a]:c},getStats:function(){var a=this.request("get-stats");return a?{successNum:a.numOfSuccess,progressNum:a.numOfProgress,cancelNum:a.numOfCancel,invalidNum:a.numOfInvalid,uploadFailNum:a.numOfUploadFailed,queueNum:a.numOfQueue,interruptNum:a.numofInterrupt}:{}},trigger:function(a){var c=[].slice.call(arguments,1),e=this.options,f="on"+a.substring(0,1).toUpperCase()+a.substring(1);return b.trigger.apply(this,arguments)===!1||d.isFunction(e[f])&&e[f].apply(this,c)===!1||d.isFunction(this[f])&&this[f].apply(this,c)===!1||b.trigger.apply(b,[this,a].concat(c))===!1?!1:!0},destroy:function(){this.request("destroy",arguments),this.off()},request:a.noop}),a.create=c.create=function(a){return new c(a)},a.Uploader=c,c}),b("runtime/runtime",["base","mediator"],function(a,b){function c(b){this.options=d.extend({container:document.body},b),this.uid=a.guid("rt_")}var d=a.$,e={},f=function(a){for(var b in a)if(a.hasOwnProperty(b))return b;return null};return d.extend(c.prototype,{getContainer:function(){var a,b,c=this.options;return this._container?this._container:(a=d(c.container||document.body),b=d(document.createElement("div")),b.attr("id","rt_"+this.uid),b.css({position:"absolute",top:"0px",left:"0px",width:"1px",height:"1px",overflow:"hidden"}),a.append(b),a.addClass("webuploader-container"),this._container=b,this._parent=a,b)},init:a.noop,exec:a.noop,destroy:function(){this._container&&this._container.remove(),this._parent&&this._parent.removeClass("webuploader-container"),this.off()}}),c.orders="html5,flash",c.addRuntime=function(a,b){e[a]=b},c.hasRuntime=function(a){return!!(a?e[a]:f(e))},c.create=function(a,b){var g,h;if(b=b||c.orders,d.each(b.split(/\s*,\s*/g),function(){return e[this]?(g=this,!1):void 0}),g=g||f(e),!g)throw new Error("Runtime Error");return h=new e[g](a)},b.installTo(c.prototype),c}),b("runtime/client",["base","mediator","runtime/runtime"],function(a,b,c){function d(b,d){var f,g=a.Deferred();this.uid=a.guid("client_"),this.runtimeReady=function(a){return g.done(a)},this.connectRuntime=function(b,h){if(f)throw new Error("already connected!");return g.done(h),"string"==typeof b&&e.get(b)&&(f=e.get(b)),f=f||e.get(null,d),f?(a.$.extend(f.options,b),f.__promise.then(g.resolve),f.__client++):(f=c.create(b,b.runtimeOrder),f.__promise=g.promise(),f.once("ready",g.resolve),f.init(),e.add(f),f.__client=1),d&&(f.__standalone=d),f},this.getRuntime=function(){return f},this.disconnectRuntime=function(){f&&(f.__client--,f.__client<=0&&(e.remove(f),delete f.__promise,f.destroy()),f=null)},this.exec=function(){if(f){var c=a.slice(arguments);return b&&c.unshift(b),f.exec.apply(this,c)}},this.getRuid=function(){return f&&f.uid},this.destroy=function(a){return function(){a&&a.apply(this,arguments),this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()}}(this.destroy)}var e;return e=function(){var a={};return{add:function(b){a[b.uid]=b},get:function(b,c){var d;if(b)return a[b];for(d in a)if(!c||!a[d].__standalone)return a[d];return null},remove:function(b){delete a[b.uid]}}}(),b.installTo(d.prototype),d}),b("lib/dnd",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},d.options,a),a.container=e(a.container),a.container.length&&c.call(this,"DragAndDrop")}var e=a.$;return d.options={accept:null,disableGlobalDnd:!1},a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/widget",["base","uploader"],function(a,b){function c(a){if(!a)return!1;var b=a.length,c=e.type(a);return 1===a.nodeType&&b?!0:"array"===c||"function"!==c&&"string"!==c&&(0===b||"number"==typeof b&&b>0&&b-1 in a)}function d(a){this.owner=a,this.options=a.options}var e=a.$,f=b.prototype._init,g=b.prototype.destroy,h={},i=[];return e.extend(d.prototype,{init:a.noop,invoke:function(a,b){var c=this.responseMap;return c&&a in c&&c[a]in this&&e.isFunction(this[c[a]])?this[c[a]].apply(this,b):h},request:function(){return this.owner.request.apply(this.owner,arguments)}}),e.extend(b.prototype,{_init:function(){var a=this,b=a._widgets=[],c=a.options.disableWidgets||"";return e.each(i,function(d,e){(!c||!~c.indexOf(e._name))&&b.push(new e(a))}),f.apply(a,arguments)},request:function(b,d,e){var f,g,i,j,k=0,l=this._widgets,m=l&&l.length,n=[],o=[];for(d=c(d)?d:[d];m>k;k++)f=l[k],g=f.invoke(b,d),g!==h&&(a.isPromise(g)?o.push(g):n.push(g));return e||o.length?(i=a.when.apply(a,o),j=i.pipe?"pipe":"then",i[j](function(){var b=a.Deferred(),c=arguments;return 1===c.length&&(c=c[0]),setTimeout(function(){b.resolve(c)},1),b.promise()})[e?j:"done"](e||a.noop)):n[0]},destroy:function(){g.apply(this,arguments),this._widgets=null}}),b.register=d.register=function(b,c){var f,g={init:"init",destroy:"destroy",name:"anonymous"};return 1===arguments.length?(c=b,e.each(c,function(a){return"_"===a[0]||"name"===a?("name"===a&&(g.name=c.name),void 0):(g[a.replace(/[A-Z]/g,"-$&").toLowerCase()]=a,void 0)})):g=e.extend(g,b),c.responseMap=g,f=a.inherits(d,c),f._name=g.name,i.push(f),f},b.unRegister=d.unRegister=function(a){if(a&&"anonymous"!==a)for(var b=i.length;b--;)i[b]._name===a&&i.splice(b,1)},d}),b("widgets/filednd",["base","uploader","lib/dnd","widgets/widget"],function(a,b,c){var d=a.$;return b.options.dnd="",b.register({name:"dnd",init:function(b){if(b.dnd&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{disableGlobalDnd:b.disableGlobalDnd,container:b.dnd,accept:b.accept});return this.dnd=e=new c(h),e.once("ready",g.resolve),e.on("drop",function(a){f.request("add-file",[a])}),e.on("accept",function(a){return f.owner.trigger("dndAccept",a)}),e.init(),g.promise()}},destroy:function(){this.dnd&&this.dnd.destroy()}})}),b("lib/filepaste",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},a),a.container=e(a.container||document.body),c.call(this,"FilePaste")}var e=a.$;return a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/filepaste",["base","uploader","lib/filepaste","widgets/widget"],function(a,b,c){var d=a.$;return b.register({name:"paste",init:function(b){if(b.paste&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{container:b.paste,accept:b.accept});return this.paste=e=new c(h),e.once("ready",g.resolve),e.on("paste",function(a){f.owner.request("add-file",[a])}),e.init(),g.promise()}},destroy:function(){this.paste&&this.paste.destroy()}})}),b("lib/blob",["base","runtime/client"],function(a,b){function c(a,c){var d=this;d.source=c,d.ruid=a,this.size=c.size||0,this.type=!c.type&&this.ext&&~"jpg,jpeg,png,gif,bmp".indexOf(this.ext)?"image/"+("jpg"===this.ext?"jpeg":this.ext):c.type||"application/octet-stream",b.call(d,"Blob"),this.uid=c.uid||this.uid,a&&d.connectRuntime(a)}return a.inherits(b,{constructor:c,slice:function(a,b){return this.exec("slice",a,b)},getSource:function(){return this.source}}),c}),b("lib/file",["base","lib/blob"],function(a,b){function c(a,c){var f;this.name=c.name||"untitled"+d++,f=e.exec(c.name)?RegExp.$1.toLowerCase():"",!f&&c.type&&(f=/\/(jpg|jpeg|png|gif|bmp)$/i.exec(c.type)?RegExp.$1.toLowerCase():"",this.name+="."+f),this.ext=f,this.lastModifiedDate=c.lastModifiedDate||(new Date).toLocaleString(),b.apply(this,arguments)}var d=1,e=/\.([^.]+)$/;return a.inherits(b,c)}),b("lib/filepicker",["base","runtime/client","lib/file"],function(b,c,d){function e(a){if(a=this.options=f.extend({},e.options,a),a.container=f(a.id),!a.container.length)throw new Error("按钮指定错误");a.innerHTML=a.innerHTML||a.label||a.container.html()||"",a.button=f(a.button||document.createElement("div")),a.button.html(a.innerHTML),a.container.html(a.button),c.call(this,"FilePicker",!0)}var f=b.$;return e.options={button:null,container:null,label:null,innerHTML:null,multiple:!0,accept:null,name:"file",style:"webuploader-pick"},b.inherits(c,{constructor:e,init:function(){var c=this,e=c.options,g=e.button,h=e.style;h&&g.addClass("webuploader-pick"),c.on("all",function(a){var b;switch(a){case"mouseenter":h&&g.addClass("webuploader-pick-hover");break;case"mouseleave":h&&g.removeClass("webuploader-pick-hover");break;case"change":b=c.exec("getFiles"),c.trigger("select",f.map(b,function(a){return a=new d(c.getRuid(),a),a._refer=e.container,a}),e.container)}}),c.connectRuntime(e,function(){c.refresh(),c.exec("init",e),c.trigger("ready")}),this._resizeHandler=b.bindFn(this.refresh,this),f(a).on("resize",this._resizeHandler)},refresh:function(){var a=this.getRuntime().getContainer(),b=this.options.button,c=b.outerWidth?b.outerWidth():b.width(),d=b.outerHeight?b.outerHeight():b.height(),e=b.offset();c&&d&&a.css({bottom:"auto",right:"auto",width:c+"px",height:d+"px"}).offset(e)},enable:function(){var a=this.options.button;a.removeClass("webuploader-pick-disable"),this.refresh()},disable:function(){var a=this.options.button;this.getRuntime().getContainer().css({top:"-99999px"}),a.addClass("webuploader-pick-disable")},destroy:function(){var b=this.options.button;f(a).off("resize",this._resizeHandler),b.removeClass("webuploader-pick-disable webuploader-pick-hover webuploader-pick")}}),e}),b("widgets/filepicker",["base","uploader","lib/filepicker","widgets/widget"],function(a,b,c){var d=a.$;return d.extend(b.options,{pick:null,accept:null}),b.register({name:"picker",init:function(a){return this.pickers=[],a.pick&&this.addBtn(a.pick)},refresh:function(){d.each(this.pickers,function(){this.refresh()})},addBtn:function(b){var e=this,f=e.options,g=f.accept,h=[];if(b)return d.isPlainObject(b)||(b={id:b}),d(b.id).each(function(){var i,j,k;k=a.Deferred(),i=d.extend({},b,{accept:d.isPlainObject(g)?[g]:g,swf:f.swf,runtimeOrder:f.runtimeOrder,id:this}),j=new c(i),j.once("ready",k.resolve),j.on("select",function(a){e.owner.request("add-file",[a])}),j.on("dialogopen",function(){e.owner.trigger("dialogOpen",j.button)}),j.init(),e.pickers.push(j),h.push(k.promise())}),a.when.apply(a,h)},disable:function(){d.each(this.pickers,function(){this.disable()})},enable:function(){d.each(this.pickers,function(){this.enable()})},destroy:function(){d.each(this.pickers,function(){this.destroy()}),this.pickers=null}})}),b("lib/image",["base","runtime/client","lib/blob"],function(a,b,c){function d(a){this.options=e.extend({},d.options,a),b.call(this,"Image"),this.on("load",function(){this._info=this.exec("info"),this._meta=this.exec("meta")})}var e=a.$;return d.options={quality:90,crop:!1,preserveHeaders:!1,allowMagnify:!1},a.inherits(b,{constructor:d,info:function(a){return a?(this._info=a,this):this._info},meta:function(a){return a?(this._meta=a,this):this._meta},loadFromBlob:function(a){var b=this,c=a.getRuid();this.connectRuntime(c,function(){b.exec("init",b.options),b.exec("loadFromBlob",a)})},resize:function(){var b=a.slice(arguments);return this.exec.apply(this,["resize"].concat(b))},crop:function(){var b=a.slice(arguments);return this.exec.apply(this,["crop"].concat(b))},getAsDataUrl:function(a){return this.exec("getAsDataUrl",a)},getAsBlob:function(a){var b=this.exec("getAsBlob",a);return new c(this.getRuid(),b)}}),d}),b("widgets/image",["base","uploader","lib/image","widgets/widget"],function(a,b,c){var d,e=a.$;return d=function(a){var b=0,c=[],d=function(){for(var d;c.length&&a>b;)d=c.shift(),b+=d[0],d[1]()};return function(a,e,f){c.push([e,f]),a.once("destroy",function(){b-=e,setTimeout(d,1)}),setTimeout(d,1)}}(5242880),e.extend(b.options,{thumb:{width:110,height:110,quality:70,allowMagnify:!0,crop:!0,preserveHeaders:!1,type:"image/jpeg"},compress:{width:1600,height:1600,quality:90,allowMagnify:!1,crop:!1,preserveHeaders:!0}}),b.register({name:"image",makeThumb:function(a,b,f,g){var h,i;return a=this.request("get-file",a),a.type.match(/^image/)?(h=e.extend({},this.options.thumb),e.isPlainObject(f)&&(h=e.extend(h,f),f=null),f=f||h.width,g=g||h.height,i=new c(h),i.once("load",function(){a._info=a._info||i.info(),a._meta=a._meta||i.meta(),1>=f&&f>0&&(f=a._info.width*f),1>=g&&g>0&&(g=a._info.height*g),i.resize(f,g)}),i.once("complete",function(){b(!1,i.getAsDataUrl(h.type)),i.destroy()}),i.once("error",function(a){b(a||!0),i.destroy()}),d(i,a.source.size,function(){a._info&&i.info(a._info),a._meta&&i.meta(a._meta),i.loadFromBlob(a.source)}),void 0):(b(!0),void 0)},beforeSendFile:function(b){var d,f,g=this.options.compress||this.options.resize,h=g&&g.compressSize||0,i=g&&g.noCompressIfLarger||!1;return b=this.request("get-file",b),!g||!~"image/jpeg,image/jpg".indexOf(b.type)||b.size=a&&a>0&&(a=b._info.width*a),1>=c&&c>0&&(c=b._info.height*c),d.resize(a,c)}),d.once("complete",function(){var a,c;try{a=d.getAsBlob(g.type),c=b.size,(!i||a.sizeb;b++)if(c=this._queue[b],a===c.getStatus())return c;return null},sort:function(a){"function"==typeof a&&this._queue.sort(a)},getFiles:function(){for(var a,b=[].slice.call(arguments,0),c=[],d=0,f=this._queue.length;f>d;d++)a=this._queue[d],(!b.length||~e.inArray(a.getStatus(),b))&&c.push(a);return c},removeFile:function(a){var b=this._map[a.id];b&&(delete this._map[a.id],a.destroy(),this.stats.numofDeleted++)},_fileAdded:function(a){var b=this,c=this._map[a.id];c||(this._map[a.id]=a,a.on("statuschange",function(a,c){b._onFileStatusChange(a,c)}))},_onFileStatusChange:function(a,b){var c=this.stats;switch(b){case f.PROGRESS:c.numOfProgress--;break;case f.QUEUED:c.numOfQueue--;break;case f.ERROR:c.numOfUploadFailed--;break;case f.INVALID:c.numOfInvalid--;break;case f.INTERRUPT:c.numofInterrupt--}switch(a){case f.QUEUED:c.numOfQueue++;break;case f.PROGRESS:c.numOfProgress++;break;case f.ERROR:c.numOfUploadFailed++;break;case f.COMPLETE:c.numOfSuccess++;break;case f.CANCELLED:c.numOfCancel++;break;case f.INVALID:c.numOfInvalid++;break;case f.INTERRUPT:c.numofInterrupt++}}}),b.installTo(d.prototype),d}),b("widgets/queue",["base","uploader","queue","file","lib/file","runtime/client","widgets/widget"],function(a,b,c,d,e,f){var g=a.$,h=/\.\w+$/,i=d.Status;return b.register({name:"queue",init:function(b){var d,e,h,i,j,k,l,m=this;if(g.isPlainObject(b.accept)&&(b.accept=[b.accept]),b.accept){for(j=[],h=0,e=b.accept.length;e>h;h++)i=b.accept[h].extensions,i&&j.push(i);j.length&&(k="\\."+j.join(",").replace(/,/g,"$|\\.").replace(/\*/g,".*")+"$"),m.accept=new RegExp(k,"i")}return m.queue=new c,m.stats=m.queue.stats,"html5"===this.request("predict-runtime-type")?(d=a.Deferred(),this.placeholder=l=new f("Placeholder"),l.connectRuntime({runtimeOrder:"html5"},function(){m._ruid=l.getRuid(),d.resolve()}),d.promise()):void 0},_wrapFile:function(a){if(!(a instanceof d)){if(!(a instanceof e)){if(!this._ruid)throw new Error("Can't add external files.");a=new e(this._ruid,a)}a=new d(a)}return a},acceptFile:function(a){var b=!a||!a.size||this.accept&&h.exec(a.name)&&!this.accept.test(a.name);return!b},_addFile:function(a){var b=this;return a=b._wrapFile(a),b.owner.trigger("beforeFileQueued",a)?b.acceptFile(a)?(b.queue.append(a),b.owner.trigger("fileQueued",a),a):(b.owner.trigger("error","Q_TYPE_DENIED",a),void 0):void 0},getFile:function(a){return this.queue.getFile(a)},addFile:function(a){var b=this;a.length||(a=[a]),a=g.map(a,function(a){return b._addFile(a)}),a.length&&(b.owner.trigger("filesQueued",a),b.options.auto&&setTimeout(function(){b.request("start-upload")},20))},getStats:function(){return this.stats},removeFile:function(a,b){var c=this;a=a.id?a:c.queue.getFile(a),this.request("cancel-file",a),b&&this.queue.removeFile(a)},getFiles:function(){return this.queue.getFiles.apply(this.queue,arguments)},fetchFile:function(){return this.queue.fetch.apply(this.queue,arguments)},retry:function(a,b){var c,d,e,f=this;if(a)return a=a.id?a:f.queue.getFile(a),a.setStatus(i.QUEUED),b||f.request("start-upload"),void 0;for(c=f.queue.getFiles(i.ERROR),d=0,e=c.length;e>d;d++)a=c[d],a.setStatus(i.QUEUED);f.request("start-upload")},sortFiles:function(){return this.queue.sort.apply(this.queue,arguments)},reset:function(){this.owner.trigger("reset"),this.queue=new c,this.stats=this.queue.stats},destroy:function(){this.reset(),this.placeholder&&this.placeholder.destroy()}})}),b("widgets/runtime",["uploader","runtime/runtime","widgets/widget"],function(a,b){return a.support=function(){return b.hasRuntime.apply(b,arguments)},a.register({name:"runtime",init:function(){if(!this.predictRuntimeType())throw Error("Runtime Error")},predictRuntimeType:function(){var a,c,d=this.options.runtimeOrder||b.orders,e=this.type;if(!e)for(d=d.split(/\s*,\s*/g),a=0,c=d.length;c>a;a++)if(b.hasRuntime(d[a])){this.type=e=d[a];break}return e}})}),b("lib/transport",["base","runtime/client","mediator"],function(a,b,c){function d(a){var c=this;a=c.options=e.extend(!0,{},d.options,a||{}),b.call(this,"Transport"),this._blob=null,this._formData=a.formData||{},this._headers=a.headers||{},this.on("progress",this._timeout),this.on("load error",function(){c.trigger("progress",1),clearTimeout(c._timer)})}var e=a.$;return d.options={server:"",method:"POST",withCredentials:!1,fileVal:"file",timeout:12e4,formData:{},headers:{},sendAsBinary:!1},e.extend(d.prototype,{appendBlob:function(a,b,c){var d=this,e=d.options;d.getRuid()&&d.disconnectRuntime(),d.connectRuntime(b.ruid,function(){d.exec("init")}),d._blob=b,e.fileVal=a||e.fileVal,e.filename=c||e.filename},append:function(a,b){"object"==typeof a?e.extend(this._formData,a):this._formData[a]=b},setRequestHeader:function(a,b){"object"==typeof a?e.extend(this._headers,a):this._headers[a]=b},send:function(a){this.exec("send",a),this._timeout()},abort:function(){return clearTimeout(this._timer),this.exec("abort")},destroy:function(){this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()},getResponse:function(){return this.exec("getResponse")},getResponseAsJson:function(){return this.exec("getResponseAsJson")},getStatus:function(){return this.exec("getStatus")},_timeout:function(){var a=this,b=a.options.timeout;b&&(clearTimeout(a._timer),a._timer=setTimeout(function(){a.abort(),a.trigger("error","timeout")},b))}}),c.installTo(d.prototype),d}),b("widgets/upload",["base","uploader","file","lib/transport","widgets/widget"],function(a,b,c,d){function e(a,b){var c,d,e=[],f=a.source,g=f.size,h=b?Math.ceil(g/b):1,i=0,j=0;for(d={file:a,has:function(){return!!e.length},shift:function(){return e.shift()},unshift:function(a){e.unshift(a)}};h>j;)c=Math.min(b,g-i),e.push({file:a,start:i,end:b?i+c:g,total:g,chunks:h,chunk:j++,cuted:d}),i+=c;return a.blocks=e.concat(),a.remaning=e.length,d}var f=a.$,g=a.isPromise,h=c.Status;f.extend(b.options,{prepareNextFile:!1,chunked:!1,chunkSize:5242880,chunkRetry:2,threads:3,formData:{}}),b.register({name:"upload",init:function(){var b=this.owner,c=this;this.runing=!1,this.progress=!1,b.on("startUpload",function(){c.progress=!0}).on("uploadFinished",function(){c.progress=!1}),this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this.__tick=a.bindFn(this._tick,this),b.on("uploadComplete",function(a){a.blocks&&f.each(a.blocks,function(a,b){b.transport&&(b.transport.abort(),b.transport.destroy()),delete b.transport}),delete a.blocks,delete a.remaning})},reset:function(){this.request("stop-upload",!0),this.runing=!1,this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this._trigged=!1,this._promise=null},startUpload:function(b){var c=this;if(f.each(c.request("get-files",h.INVALID),function(){c.request("remove-file",this)}),b?(b=b.id?b:c.request("get-file",b),b.getStatus()===h.INTERRUPT?(b.setStatus(h.QUEUED),f.each(c.pool,function(a,c){c.file===b&&(c.transport&&c.transport.send(),b.setStatus(h.PROGRESS))})):b.getStatus()!==h.PROGRESS&&b.setStatus(h.QUEUED)):f.each(c.request("get-files",[h.INITED]),function(){this.setStatus(h.QUEUED)}),c.runing)return a.nextTick(c.__tick);c.runing=!0;var d=[];b||f.each(c.pool,function(a,b){var e=b.file;e.getStatus()===h.INTERRUPT&&(c._trigged=!1,d.push(e),b.transport&&b.transport.send())}),f.each(d,function(){this.setStatus(h.PROGRESS)}),b||f.each(c.request("get-files",h.INTERRUPT),function(){this.setStatus(h.PROGRESS)}),c._trigged=!1,a.nextTick(c.__tick),c.owner.trigger("startUpload")},stopUpload:function(b,c){var d,e=this;if(b===!0&&(c=b,b=null),e.runing!==!1){if(b){if(b=b.id?b:e.request("get-file",b),b.getStatus()!==h.PROGRESS&&b.getStatus()!==h.QUEUED)return;return b.setStatus(h.INTERRUPT),f.each(e.pool,function(a,c){return c.file===b?(d=c,!1):void 0}),d.transport&&d.transport.abort(),c&&(e._putback(d),e._popBlock(d)),a.nextTick(e.__tick)}e.runing=!1,this._promise&&this._promise.file&&this._promise.file.setStatus(h.INTERRUPT),c&&f.each(e.pool,function(a,b){b.transport&&b.transport.abort(),b.file.setStatus(h.INTERRUPT)}),e.owner.trigger("stopUpload")}},cancelFile:function(a){a=a.id?a:this.request("get-file",a),a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),a.setStatus(h.CANCELLED),this.owner.trigger("fileDequeued",a)},isInProgress:function(){return!!this.progress},_getStats:function(){return this.request("get-stats")},skipFile:function(a,b){a=a.id?a:this.request("get-file",a),a.setStatus(b||h.COMPLETE),a.skipped=!0,a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),this.owner.trigger("uploadSkip",a)},_tick:function(){var b,c,d=this,e=d.options;return d._promise?d._promise.always(d.__tick):(d.pool.length1&&~"http,abort".indexOf(a)&&b.retried1&&f.extend(m,{chunks:b.chunks,chunk:b.chunk}),i.trigger("uploadBeforeSend",b,m,n),l.appendBlob(j.fileVal,b.blob,k.name),l.append(m),l.setRequestHeader(n),l.send() +},_finishFile:function(a,b,c){var d=this.owner;return d.request("after-send-file",arguments,function(){a.setStatus(h.COMPLETE),d.trigger("uploadSuccess",a,b,c)}).fail(function(b){a.getStatus()===h.PROGRESS&&a.setStatus(h.ERROR,b),d.trigger("uploadError",a,b)}).always(function(){d.trigger("uploadComplete",a)})},updateFileProgress:function(a){var b=0,c=0;a.blocks&&(f.each(a.blocks,function(a,b){c+=(b.percentage||0)*(b.end-b.start)}),b=c/a.size,this.owner.trigger("uploadProgress",a,b||0))}})}),b("widgets/validator",["base","uploader","file","widgets/widget"],function(a,b,c){var d,e=a.$,f={};return d={addValidator:function(a,b){f[a]=b},removeValidator:function(a){delete f[a]}},b.register({name:"validator",init:function(){var b=this;a.nextTick(function(){e.each(f,function(){this.call(b.owner)})})}}),d.addValidator("fileNumLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileNumLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){return c>=d&&e&&(e=!1,this.trigger("error","Q_EXCEED_NUM_LIMIT",d,a),setTimeout(function(){e=!0},1)),c>=d?!1:!0}),a.on("fileQueued",function(){c++}),a.on("fileDequeued",function(){c--}),a.on("reset",function(){c=0}))}),d.addValidator("fileSizeLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileSizeLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){var b=c+a.size>d;return b&&e&&(e=!1,this.trigger("error","Q_EXCEED_SIZE_LIMIT",d,a),setTimeout(function(){e=!0},1)),b?!1:!0}),a.on("fileQueued",function(a){c+=a.size}),a.on("fileDequeued",function(a){c-=a.size}),a.on("reset",function(){c=0}))}),d.addValidator("fileSingleSizeLimit",function(){var a=this,b=a.options,d=b.fileSingleSizeLimit;d&&a.on("beforeFileQueued",function(a){return a.size>d?(a.setStatus(c.Status.INVALID,"exceed_size"),this.trigger("error","F_EXCEED_SIZE",d,a),!1):void 0})}),d.addValidator("duplicate",function(){function a(a){for(var b,c=0,d=0,e=a.length;e>d;d++)b=a.charCodeAt(d),c=b+(c<<6)+(c<<16)-c;return c}var b=this,c=b.options,d={};c.duplicate||(b.on("beforeFileQueued",function(b){var c=b.__hash||(b.__hash=a(b.name+b.size+b.lastModifiedDate));return d[c]?(this.trigger("error","F_DUPLICATE",b),!1):void 0}),b.on("fileQueued",function(a){var b=a.__hash;b&&(d[b]=!0)}),b.on("fileDequeued",function(a){var b=a.__hash;b&&delete d[b]}),b.on("reset",function(){d={}}))}),d}),b("runtime/compbase",[],function(){function a(a,b){this.owner=a,this.options=a.options,this.getRuntime=function(){return b},this.getRuid=function(){return b.uid},this.trigger=function(){return a.trigger.apply(a,arguments)}}return a}),b("runtime/html5/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a={},d=this,e=this.destroy;c.apply(d,arguments),d.type=f,d.exec=function(c,e){var f,h=this,i=h.uid,j=b.slice(arguments,2);return g[c]&&(f=a[i]=a[i]||new g[c](h,d),f[e])?f[e].apply(f,j):void 0},d.destroy=function(){return e&&e.apply(this,arguments)}}var f="html5",g={};return b.inherits(c,{constructor:e,init:function(){var a=this;setTimeout(function(){a.trigger("ready")},1)}}),e.register=function(a,c){var e=g[a]=b.inherits(d,c);return e},a.Blob&&a.FileReader&&a.DataView&&c.addRuntime(f,e),e}),b("runtime/html5/blob",["runtime/html5/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.owner.source,e=d.slice||d.webkitSlice||d.mozSlice;return d=e.call(d,a,c),new b(this.getRuid(),d)}})}),b("runtime/html5/dnd",["base","runtime/html5/runtime","lib/file"],function(a,b,c){var d=a.$,e="webuploader-dnd-";return b.register("DragAndDrop",{init:function(){var b=this.elem=this.options.container;this.dragEnterHandler=a.bindFn(this._dragEnterHandler,this),this.dragOverHandler=a.bindFn(this._dragOverHandler,this),this.dragLeaveHandler=a.bindFn(this._dragLeaveHandler,this),this.dropHandler=a.bindFn(this._dropHandler,this),this.dndOver=!1,b.on("dragenter",this.dragEnterHandler),b.on("dragover",this.dragOverHandler),b.on("dragleave",this.dragLeaveHandler),b.on("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).on("dragover",this.dragOverHandler),d(document).on("drop",this.dropHandler))},_dragEnterHandler:function(a){var b,c=this,d=c._denied||!1;return a=a.originalEvent||a,c.dndOver||(c.dndOver=!0,b=a.dataTransfer.items,b&&b.length&&(c._denied=d=!c.trigger("accept",b)),c.elem.addClass(e+"over"),c.elem[d?"addClass":"removeClass"](e+"denied")),a.dataTransfer.dropEffect=d?"none":"copy",!1},_dragOverHandler:function(a){var b=this.elem.parent().get(0);return b&&!d.contains(b,a.currentTarget)?!1:(clearTimeout(this._leaveTimer),this._dragEnterHandler.call(this,a),!1)},_dragLeaveHandler:function(){var a,b=this;return a=function(){b.dndOver=!1,b.elem.removeClass(e+"over "+e+"denied")},clearTimeout(b._leaveTimer),b._leaveTimer=setTimeout(a,100),!1},_dropHandler:function(a){var b,f,g=this,h=g.getRuid(),i=g.elem.parent().get(0);if(i&&!d.contains(i,a.currentTarget))return!1;a=a.originalEvent||a,b=a.dataTransfer;try{f=b.getData("text/html")}catch(j){}return g.dndOver=!1,g.elem.removeClass(e+"over"),f?void 0:(g._getTansferFiles(b,function(a){g.trigger("drop",d.map(a,function(a){return new c(h,a)}))}),!1)},_getTansferFiles:function(b,c){var d,e,f,g,h,i,j,k=[],l=[];for(d=b.items,e=b.files,j=!(!d||!d[0].webkitGetAsEntry),h=0,i=e.length;i>h;h++)f=e[h],g=d&&d[h],j&&g.webkitGetAsEntry().isDirectory?l.push(this._traverseDirectoryTree(g.webkitGetAsEntry(),k)):k.push(f);a.when.apply(a,l).done(function(){k.length&&c(k)})},_traverseDirectoryTree:function(b,c){var d=a.Deferred(),e=this;return b.isFile?b.file(function(a){c.push(a),d.resolve()}):b.isDirectory&&b.createReader().readEntries(function(b){var f,g=b.length,h=[],i=[];for(f=0;g>f;f++)h.push(e._traverseDirectoryTree(b[f],i));a.when.apply(a,h).then(function(){c.push.apply(c,i),d.resolve()},d.reject)}),d.promise()},destroy:function(){var a=this.elem;a&&(a.off("dragenter",this.dragEnterHandler),a.off("dragover",this.dragOverHandler),a.off("dragleave",this.dragLeaveHandler),a.off("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).off("dragover",this.dragOverHandler),d(document).off("drop",this.dropHandler)))}})}),b("runtime/html5/filepaste",["base","runtime/html5/runtime","lib/file"],function(a,b,c){return b.register("FilePaste",{init:function(){var b,c,d,e,f=this.options,g=this.elem=f.container,h=".*";if(f.accept){for(b=[],c=0,d=f.accept.length;d>c;c++)e=f.accept[c].mimeTypes,e&&b.push(e);b.length&&(h=b.join(","),h=h.replace(/,/g,"|").replace(/\*/g,".*"))}this.accept=h=new RegExp(h,"i"),this.hander=a.bindFn(this._pasteHander,this),g.on("paste",this.hander)},_pasteHander:function(a){var b,d,e,f,g,h=[],i=this.getRuid();for(a=a.originalEvent||a,b=a.clipboardData.items,f=0,g=b.length;g>f;f++)d=b[f],"file"===d.kind&&(e=d.getAsFile())&&h.push(new c(i,e));h.length&&(a.preventDefault(),a.stopPropagation(),this.trigger("paste",h))},destroy:function(){this.elem.off("paste",this.hander)}})}),b("runtime/html5/filepicker",["base","runtime/html5/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(){var a,b,d,e,f=this.getRuntime().getContainer(),g=this,h=g.owner,i=g.options,j=this.label=c(document.createElement("label")),k=this.input=c(document.createElement("input"));if(k.attr("type","file"),k.attr("capture","camera"),k.attr("name",i.name),k.addClass("webuploader-element-invisible"),j.on("click",function(a){k.trigger("click"),a.stopPropagation(),h.trigger("dialogopen")}),j.css({opacity:0,width:"100%",height:"100%",display:"block",cursor:"pointer",background:"#ffffff"}),i.multiple&&k.attr("multiple","multiple"),i.accept&&i.accept.length>0){for(a=[],b=0,d=i.accept.length;d>b;b++)a.push(i.accept[b].mimeTypes);k.attr("accept",a.join(","))}f.append(k),f.append(j),e=function(a){h.trigger(a.type)},k.on("change",function(a){var b,d=arguments.callee;g.files=a.target.files,b=this.cloneNode(!0),b.value=null,this.parentNode.replaceChild(b,this),k.off(),k=c(b).on("change",d).on("mouseenter mouseleave",e),h.trigger("change")}),j.on("mouseenter mouseleave",e)},getFiles:function(){return this.files},destroy:function(){this.input.off(),this.label.off()}})}),b("runtime/html5/util",["base"],function(b){var c=a.createObjectURL&&a||a.URL&&URL.revokeObjectURL&&URL||a.webkitURL,d=b.noop,e=d;return c&&(d=function(){return c.createObjectURL.apply(c,arguments)},e=function(){return c.revokeObjectURL.apply(c,arguments)}),{createObjectURL:d,revokeObjectURL:e,dataURL2Blob:function(a){var b,c,d,e,f,g;for(g=a.split(","),b=~g[0].indexOf("base64")?atob(g[1]):decodeURIComponent(g[1]),d=new ArrayBuffer(b.length),c=new Uint8Array(d),e=0;ei&&(d=h.getUint16(i),d>=65504&&65519>=d||65534===d)&&(e=h.getUint16(i+2)+2,!(i+e>h.byteLength));){if(f=b.parsers[d],!c&&f)for(g=0;g6&&(l.imageHead=a.slice?a.slice(2,k):new Uint8Array(a).subarray(2,k))}return l}},updateImageHead:function(a,b){var c,d,e,f=this._parse(a,!0);return e=2,f.imageHead&&(e=2+f.imageHead.byteLength),d=a.slice?a.slice(e):new Uint8Array(a).subarray(e),c=new Uint8Array(b.byteLength+2+d.byteLength),c[0]=255,c[1]=216,c.set(new Uint8Array(b),2),c.set(new Uint8Array(d),b.byteLength+2),c.buffer}},a.parseMeta=function(){return b.parse.apply(b,arguments)},a.updateImageHead=function(){return b.updateImageHead.apply(b,arguments)},b}),b("runtime/html5/imagemeta/exif",["base","runtime/html5/imagemeta"],function(a,b){var c={};return c.ExifMap=function(){return this},c.ExifMap.prototype.map={Orientation:274},c.ExifMap.prototype.get=function(a){return this[a]||this[this.map[a]]},c.exifTagTypes={1:{getValue:function(a,b){return a.getUint8(b)},size:1},2:{getValue:function(a,b){return String.fromCharCode(a.getUint8(b))},size:1,ascii:!0},3:{getValue:function(a,b,c){return a.getUint16(b,c)},size:2},4:{getValue:function(a,b,c){return a.getUint32(b,c)},size:4},5:{getValue:function(a,b,c){return a.getUint32(b,c)/a.getUint32(b+4,c)},size:8},9:{getValue:function(a,b,c){return a.getInt32(b,c)},size:4},10:{getValue:function(a,b,c){return a.getInt32(b,c)/a.getInt32(b+4,c)},size:8}},c.exifTagTypes[7]=c.exifTagTypes[1],c.getExifValue=function(b,d,e,f,g,h){var i,j,k,l,m,n,o=c.exifTagTypes[f];if(!o)return a.log("Invalid Exif data: Invalid tag type."),void 0;if(i=o.size*g,j=i>4?d+b.getUint32(e+8,h):e+8,j+i>b.byteLength)return a.log("Invalid Exif data: Invalid data offset."),void 0;if(1===g)return o.getValue(b,j,h);for(k=[],l=0;g>l;l+=1)k[l]=o.getValue(b,j+l*o.size,h);if(o.ascii){for(m="",l=0;lb.byteLength)return a.log("Invalid Exif data: Invalid directory offset."),void 0;if(g=b.getUint16(d,e),h=d+2+12*g,h+4>b.byteLength)return a.log("Invalid Exif data: Invalid directory size."),void 0;for(i=0;g>i;i+=1)this.parseExifTag(b,c,d+2+12*i,e,f);return b.getUint32(h,e)},c.parseExifData=function(b,d,e,f){var g,h,i=d+10;if(1165519206===b.getUint32(d+4)){if(i+8>b.byteLength)return a.log("Invalid Exif data: Invalid segment size."),void 0;if(0!==b.getUint16(d+8))return a.log("Invalid Exif data: Missing byte alignment offset."),void 0;switch(b.getUint16(i)){case 18761:g=!0;break;case 19789:g=!1;break;default:return a.log("Invalid Exif data: Invalid byte alignment marker."),void 0}if(42!==b.getUint16(i+2,g))return a.log("Invalid Exif data: Missing TIFF marker."),void 0;h=b.getUint32(i+4,g),f.exif=new c.ExifMap,h=c.parseExifTags(b,i,i+h,g,f)}},b.parsers[65505].push(c.parseExifData),c}),b("runtime/html5/image",["base","runtime/html5/runtime","runtime/html5/util"],function(a,b,c){var d="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D";return b.register("Image",{modified:!1,init:function(){var a=this,b=new Image;b.onload=function(){a._info={type:a.type,width:this.width,height:this.height},a._metas||"image/jpeg"!==a.type?a.owner.trigger("load"):c.parseMeta(a._blob,function(b,c){a._metas=c,a.owner.trigger("load")})},b.onerror=function(){a.owner.trigger("error")},a._img=b},loadFromBlob:function(a){var b=this,d=b._img;b._blob=a,b.type=a.type,d.src=c.createObjectURL(a.getSource()),b.owner.once("load",function(){c.revokeObjectURL(d.src)})},resize:function(a,b){var c=this._canvas||(this._canvas=document.createElement("canvas"));this._resize(this._img,c,a,b),this._blob=null,this.modified=!0,this.owner.trigger("complete","resize")},crop:function(a,b,c,d,e){var f=this._canvas||(this._canvas=document.createElement("canvas")),g=this.options,h=this._img,i=h.naturalWidth,j=h.naturalHeight,k=this.getOrientation();e=e||1,f.width=c,f.height=d,g.preserveHeaders||this._rotate2Orientaion(f,k),this._renderImageToCanvas(f,h,-a,-b,i*e,j*e),this._blob=null,this.modified=!0,this.owner.trigger("complete","crop")},getAsBlob:function(a){var b,d=this._blob,e=this.options;if(a=a||this.type,this.modified||this.type!==a){if(b=this._canvas,"image/jpeg"===a){if(d=c.canvasToDataUrl(b,a,e.quality),e.preserveHeaders&&this._metas&&this._metas.imageHead)return d=c.dataURL2ArrayBuffer(d),d=c.updateImageHead(d,this._metas.imageHead),d=c.arrayBufferToBlob(d,a)}else d=c.canvasToDataUrl(b,a);d=c.dataURL2Blob(d)}return d},getAsDataUrl:function(a){var b=this.options;return a=a||this.type,"image/jpeg"===a?c.canvasToDataUrl(this._canvas,a,b.quality):this._canvas.toDataURL(a)},getOrientation:function(){return this._metas&&this._metas.exif&&this._metas.exif.get("Orientation")||1},info:function(a){return a?(this._info=a,this):this._info},meta:function(a){return a?(this._metas=a,this):this._metas},destroy:function(){var a=this._canvas;this._img.onload=null,a&&(a.getContext("2d").clearRect(0,0,a.width,a.height),a.width=a.height=0,this._canvas=null),this._img.src=d,this._img=this._blob=null},_resize:function(a,b,c,d){var e,f,g,h,i,j=this.options,k=a.width,l=a.height,m=this.getOrientation();~[5,6,7,8].indexOf(m)&&(c^=d,d^=c,c^=d),e=Math[j.crop?"max":"min"](c/k,d/l),j.allowMagnify||(e=Math.min(1,e)),f=k*e,g=l*e,j.crop?(b.width=c,b.height=d):(b.width=f,b.height=g),h=(b.width-f)/2,i=(b.height-g)/2,j.preserveHeaders||this._rotate2Orientaion(b,m),this._renderImageToCanvas(b,a,h,i,f,g)},_rotate2Orientaion:function(a,b){var c=a.width,d=a.height,e=a.getContext("2d");switch(b){case 5:case 6:case 7:case 8:a.width=d,a.height=c}switch(b){case 2:e.translate(c,0),e.scale(-1,1);break;case 3:e.translate(c,d),e.rotate(Math.PI);break;case 4:e.translate(0,d),e.scale(1,-1);break;case 5:e.rotate(.5*Math.PI),e.scale(1,-1);break;case 6:e.rotate(.5*Math.PI),e.translate(0,-d);break;case 7:e.rotate(.5*Math.PI),e.translate(c,-d),e.scale(-1,1);break;case 8:e.rotate(-.5*Math.PI),e.translate(-c,0)}},_renderImageToCanvas:function(){function b(a,b,c){var d,e,f,g=document.createElement("canvas"),h=g.getContext("2d"),i=0,j=c,k=c;for(g.width=1,g.height=c,h.drawImage(a,0,0),d=h.getImageData(0,0,1,c).data;k>i;)e=d[4*(k-1)+3],0===e?j=k:i=k,k=j+i>>1;return f=k/c,0===f?1:f}function c(a){var b,c,d=a.naturalWidth,e=a.naturalHeight;return d*e>1048576?(b=document.createElement("canvas"),b.width=b.height=1,c=b.getContext("2d"),c.drawImage(a,-d+1,0),0===c.getImageData(0,0,1,1).data[3]):!1}return a.os.ios?a.os.ios>=7?function(a,c,d,e,f,g){var h=c.naturalWidth,i=c.naturalHeight,j=b(c,h,i);return a.getContext("2d").drawImage(c,0,0,h*j,i*j,d,e,f,g)}:function(a,d,e,f,g,h){var i,j,k,l,m,n,o,p=d.naturalWidth,q=d.naturalHeight,r=a.getContext("2d"),s=c(d),t="image/jpeg"===this.type,u=1024,v=0,w=0;for(s&&(p/=2,q/=2),r.save(),i=document.createElement("canvas"),i.width=i.height=u,j=i.getContext("2d"),k=t?b(d,p,q):1,l=Math.ceil(u*g/p),m=Math.ceil(u*h/q/k);q>v;){for(n=0,o=0;p>n;)j.clearRect(0,0,u,u),j.drawImage(d,-n,-v),r.drawImage(i,0,0,u,u,e+o,f+w,l,m),n+=u,o+=l;v+=u,w+=m}r.restore(),i=j=null}:function(b){var c=a.slice(arguments,1),d=b.getContext("2d");d.drawImage.apply(d,c)}}()})}),b("runtime/html5/transport",["base","runtime/html5/runtime"],function(a,b){var c=a.noop,d=a.$;return b.register("Transport",{init:function(){this._status=0,this._response=null},send:function(){var b,c,e,f=this.owner,g=this.options,h=this._initAjax(),i=f._blob,j=g.server;g.sendAsBinary?(j+=(/\?/.test(j)?"&":"?")+d.param(f._formData),c=i.getSource()):(b=new FormData,d.each(f._formData,function(a,c){b.append(a,c)}),b.append(g.fileVal,i.getSource(),g.filename||f._formData.name||"")),g.withCredentials&&"withCredentials"in h?(h.open(g.method,j,!0),h.withCredentials=!0):h.open(g.method,j),this._setRequestHeader(h,g.headers),c?(h.overrideMimeType&&h.overrideMimeType("application/octet-stream"),a.os.android?(e=new FileReader,e.onload=function(){h.send(this.result),e=e.onload=null},e.readAsArrayBuffer(c)):h.send(c)):h.send(b)},getResponse:function(){return this._response},getResponseAsJson:function(){return this._parseJson(this._response)},getStatus:function(){return this._status},abort:function(){var a=this._xhr;a&&(a.upload.onprogress=c,a.onreadystatechange=c,a.abort(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var a=this,b=new XMLHttpRequest,d=this.options;return!d.withCredentials||"withCredentials"in b||"undefined"==typeof XDomainRequest||(b=new XDomainRequest),b.upload.onprogress=function(b){var c=0;return b.lengthComputable&&(c=b.loaded/b.total),a.trigger("progress",c)},b.onreadystatechange=function(){return 4===b.readyState?(b.upload.onprogress=c,b.onreadystatechange=c,a._xhr=null,a._status=b.status,b.status>=200&&b.status<300?(a._response=b.responseText,a.trigger("load")):b.status>=500&&b.status<600?(a._response=b.responseText,a.trigger("error","server")):a.trigger("error",a._status?"http":"abort")):void 0},a._xhr=b,b},_setRequestHeader:function(a,b){d.each(b,function(b,c){a.setRequestHeader(b,c)})},_parseJson:function(a){var b;try{b=JSON.parse(a)}catch(c){b={}}return b}})}),b("preset/html5only",["base","widgets/filednd","widgets/filepaste","widgets/filepicker","widgets/image","widgets/queue","widgets/runtime","widgets/upload","widgets/validator","runtime/html5/blob","runtime/html5/dnd","runtime/html5/filepaste","runtime/html5/filepicker","runtime/html5/imagemeta/exif","runtime/html5/image","runtime/html5/transport"],function(a){return a}),b("webuploader",["preset/html5only"],function(a){return a}),c("webuploader")}); \ No newline at end of file diff --git a/public/static/libs/webuploader/webuploader.js b/public/static/libs/webuploader/webuploader.js new file mode 100644 index 0000000..734f736 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.js @@ -0,0 +1,8140 @@ +/*! WebUploader 0.1.6 */ + + +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + * @require "jquery" + * @require "zepto" + */ + define('dollar-third',[],function() { + var req = window.require; + var $ = window.__dollar || + window.jQuery || + window.Zepto || + req('jquery') || + req('zepto'); + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 使用jQuery的Promise + */ + define('promise-third',[ + 'dollar' + ], function( $ ) { + return { + Deferred: $.Deferred, + when: $.when, + + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + } + }; + }); + /** + * @fileOverview Promise/A+ + */ + define('promise',[ + 'promise-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.6', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview 错误信息 + */ + define('lib/dnd',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function DragAndDrop( opts ) { + opts = this.options = $.extend({}, DragAndDrop.options, opts ); + + opts.container = $( opts.container ); + + if ( !opts.container.length ) { + return; + } + + RuntimeClent.call( this, 'DragAndDrop' ); + } + + DragAndDrop.options = { + accept: null, + disableGlobalDnd: false + }; + + Base.inherits( RuntimeClent, { + constructor: DragAndDrop, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( DragAndDrop.prototype ); + + return DragAndDrop; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview DragAndDrop Widget。 + */ + define('widgets/filednd',[ + 'base', + 'uploader', + 'lib/dnd', + 'widgets/widget' + ], function( Base, Uploader, Dnd ) { + var $ = Base.$; + + Uploader.options.dnd = ''; + + /** + * @property {Selector} [dnd=undefined] 指定Drag And Drop拖拽的容器,如果不指定,则不启动。 + * @namespace options + * @for Uploader + */ + + /** + * @property {Selector} [disableGlobalDnd=false] 是否禁掉整个页面的拖拽功能,如果不禁用,图片拖进来的时候会默认被浏览器打开。 + * @namespace options + * @for Uploader + */ + + /** + * @event dndAccept + * @param {DataTransferItemList} items DataTransferItem + * @description 阻止此事件可以拒绝某些类型的文件拖入进来。目前只有 chrome 提供这样的 API,且只能通过 mime-type 验证。 + * @for Uploader + */ + return Uploader.register({ + name: 'dnd', + + init: function( opts ) { + + if ( !opts.dnd || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + disableGlobalDnd: opts.disableGlobalDnd, + container: opts.dnd, + accept: opts.accept + }), + dnd; + + this.dnd = dnd = new Dnd( options ); + + dnd.once( 'ready', deferred.resolve ); + dnd.on( 'drop', function( files ) { + me.request( 'add-file', [ files ]); + }); + + // 检测文件是否全部允许添加。 + dnd.on( 'accept', function( items ) { + return me.owner.trigger( 'dndAccept', items ); + }); + + dnd.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.dnd && this.dnd.destroy(); + } + }); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepaste',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function FilePaste( opts ) { + opts = this.options = $.extend({}, opts ); + opts.container = $( opts.container || document.body ); + RuntimeClent.call( this, 'FilePaste' ); + } + + Base.inherits( RuntimeClent, { + constructor: FilePaste, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( FilePaste.prototype ); + + return FilePaste; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/filepaste',[ + 'base', + 'uploader', + 'lib/filepaste', + 'widgets/widget' + ], function( Base, Uploader, FilePaste ) { + var $ = Base.$; + + /** + * @property {Selector} [paste=undefined] 指定监听paste事件的容器,如果不指定,不启用此功能。此功能为通过粘贴来添加截屏的图片。建议设置为`document.body`. + * @namespace options + * @for Uploader + */ + return Uploader.register({ + name: 'paste', + + init: function( opts ) { + + if ( !opts.paste || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + container: opts.paste, + accept: opts.accept + }), + paste; + + this.paste = paste = new FilePaste( options ); + + paste.once( 'ready', deferred.resolve ); + paste.on( 'paste', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + paste.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.paste && this.paste.destroy(); + } + }); + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClient, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClient.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file', + style: 'webuploader-pick' //pick element class attribute, default is "webuploader-pick" + }; + + Base.inherits( RuntimeClient, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button, + style = opts.style; + + if (style) + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + if (style) + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + if (style) + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addBtn + * @for Uploader + * @grammar addBtn( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addBtn({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.on('dialogopen', function() { + me.owner.trigger('dialogOpen', picker.button); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview Image + */ + define('lib/image',[ + 'base', + 'runtime/client', + 'lib/blob' + ], function( Base, RuntimeClient, Blob ) { + var $ = Base.$; + + // 构造器。 + function Image( opts ) { + this.options = $.extend({}, Image.options, opts ); + RuntimeClient.call( this, 'Image' ); + + this.on( 'load', function() { + this._info = this.exec('info'); + this._meta = this.exec('meta'); + }); + } + + // 默认选项。 + Image.options = { + + // 默认的图片处理质量 + quality: 90, + + // 是否裁剪 + crop: false, + + // 是否保留头部信息 + preserveHeaders: false, + + // 是否允许放大。 + allowMagnify: false + }; + + // 继承RuntimeClient. + Base.inherits( RuntimeClient, { + constructor: Image, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + loadFromBlob: function( blob ) { + var me = this, + ruid = blob.getRuid(); + + this.connectRuntime( ruid, function() { + me.exec( 'init', me.options ); + me.exec( 'loadFromBlob', blob ); + }); + }, + + resize: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'resize' ].concat( args ) ); + }, + + crop: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'crop' ].concat( args ) ); + }, + + getAsDataUrl: function( type ) { + return this.exec( 'getAsDataUrl', type ); + }, + + getAsBlob: function( type ) { + var blob = this.exec( 'getAsBlob', type ); + + return new Blob( this.getRuid(), blob ); + } + }); + + return Image; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/image',[ + 'base', + 'uploader', + 'lib/image', + 'widgets/widget' + ], function( Base, Uploader, Image ) { + + var $ = Base.$, + throttle; + + // 根据要处理的文件大小来节流,一次不能处理太多,会卡。 + throttle = (function( max ) { + var occupied = 0, + waiting = [], + tick = function() { + var item; + + while ( waiting.length && occupied < max ) { + item = waiting.shift(); + occupied += item[ 0 ]; + item[ 1 ](); + } + }; + + return function( emiter, size, cb ) { + waiting.push([ size, cb ]); + emiter.once( 'destroy', function() { + occupied -= size; + setTimeout( tick, 1 ); + }); + setTimeout( tick, 1 ); + }; + })( 5 * 1024 * 1024 ); + + $.extend( Uploader.options, { + + /** + * @property {Object} [thumb] + * @namespace options + * @for Uploader + * @description 配置生成缩略图的选项。 + * + * 默认为: + * + * ```javascript + * { + * width: 110, + * height: 110, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 70, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: true, + * + * // 是否允许裁剪。 + * crop: true, + * + * // 为空的话则保留原有图片格式。 + * // 否则强制转换成指定的类型。 + * type: 'image/jpeg' + * } + * ``` + */ + thumb: { + width: 110, + height: 110, + quality: 70, + allowMagnify: true, + crop: true, + preserveHeaders: false, + + // 为空的话则保留原有图片格式。 + // 否则强制转换成指定的类型。 + // IE 8下面 base64 大小不能超过 32K 否则预览失败,而非 jpeg 编码的图片很可 + // 能会超过 32k, 所以这里设置成预览的时候都是 image/jpeg + type: 'image/jpeg' + }, + + /** + * @property {Object} [compress] + * @namespace options + * @for Uploader + * @description 配置压缩的图片的选项。如果此选项为`false`, 则图片在上传前不进行压缩。 + * + * 默认为: + * + * ```javascript + * { + * width: 1600, + * height: 1600, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 90, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: false, + * + * // 是否允许裁剪。 + * crop: false, + * + * // 是否保留头部meta信息。 + * preserveHeaders: true, + * + * // 如果发现压缩后文件大小比原来还大,则使用原来图片 + * // 此属性可能会影响图片自动纠正功能 + * noCompressIfLarger: false, + * + * // 单位字节,如果图片大小小于此值,不会采用压缩。 + * compressSize: 0 + * } + * ``` + */ + compress: { + width: 1600, + height: 1600, + quality: 90, + allowMagnify: false, + crop: false, + preserveHeaders: true + } + }); + + return Uploader.register({ + + name: 'image', + + + /** + * 生成缩略图,此过程为异步,所以需要传入`callback`。 + * 通常情况在图片加入队里后调用此方法来生成预览图以增强交互效果。 + * + * 当 width 或者 height 的值介于 0 - 1 时,被当成百分比使用。 + * + * `callback`中可以接收到两个参数。 + * * 第一个为error,如果生成缩略图有错误,此error将为真。 + * * 第二个为ret, 缩略图的Data URL值。 + * + * **注意** + * Date URL在IE6/7中不支持,所以不用调用此方法了,直接显示一张暂不支持预览图片好了。 + * 也可以借助服务端,将 base64 数据传给服务端,生成一个临时文件供预览。 + * + * @method makeThumb + * @grammar makeThumb( file, callback ) => undefined + * @grammar makeThumb( file, callback, width, height ) => undefined + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.makeThumb( file, function( error, ret ) { + * if ( error ) { + * $li.text('预览错误'); + * } else { + * $li.append(''); + * } + * }); + * + * }); + */ + makeThumb: function( file, cb, width, height ) { + var opts, image; + + file = this.request( 'get-file', file ); + + // 只预览图片格式。 + if ( !file.type.match( /^image/ ) ) { + cb( true ); + return; + } + + opts = $.extend({}, this.options.thumb ); + + // 如果传入的是object. + if ( $.isPlainObject( width ) ) { + opts = $.extend( opts, width ); + width = null; + } + + width = width || opts.width; + height = height || opts.height; + + image = new Image( opts ); + + image.once( 'load', function() { + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + // 当 resize 完后 + image.once( 'complete', function() { + cb( false, image.getAsDataUrl( opts.type ) ); + image.destroy(); + }); + + image.once( 'error', function( reason ) { + cb( reason || true ); + image.destroy(); + }); + + throttle( image, file.source.size, function() { + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + image.loadFromBlob( file.source ); + }); + }, + + beforeSendFile: function( file ) { + var opts = this.options.compress || this.options.resize, + compressSize = opts && opts.compressSize || 0, + noCompressIfLarger = opts && opts.noCompressIfLarger || false, + image, deferred; + + file = this.request( 'get-file', file ); + + // 只压缩 jpeg 图片格式。 + // gif 可能会丢失针 + // bmp png 基本上尺寸都不大,且压缩比比较小。 + if ( !opts || !~'image/jpeg,image/jpg'.indexOf( file.type ) || + file.size < compressSize || + file._compressed ) { + return; + } + + opts = $.extend({}, opts ); + deferred = Base.Deferred(); + + image = new Image( opts ); + + deferred.always(function() { + image.destroy(); + image = null; + }); + image.once( 'error', deferred.reject ); + image.once( 'load', function() { + var width = opts.width, + height = opts.height; + + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + image.once( 'complete', function() { + var blob, size; + + // 移动端 UC / qq 浏览器的无图模式下 + // ctx.getImageData 处理大图的时候会报 Exception + // INDEX_SIZE_ERR: DOM Exception 1 + try { + blob = image.getAsBlob( opts.type ); + + size = file.size; + + // 如果压缩后,比原来还大则不用压缩后的。 + if ( !noCompressIfLarger || blob.size < size ) { + // file.source.destroy && file.source.destroy(); + file.source = blob; + file.size = blob.size; + + file.trigger( 'resize', blob.size, size ); + } + + // 标记,避免重复压缩。 + file._compressed = true; + deferred.resolve(); + } catch ( e ) { + // 出错了直接继续,让其上传原始图片 + deferred.resolve(); + } + }); + + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + + image.loadFromBlob( file.source ); + return deferred.promise(); + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + if ( files.length ) { + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + // 销毁上传相关的属性。 + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定的文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + file.setStatus( Status.QUEUED ); + + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + file.setStatus( Status.PROGRESS ); + }); + + + } else if (file.getStatus() !== Status.PROGRESS) { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return Base.nextTick( me.__tick ); + } + + me.runing = true; + var files = []; + + // 如果有暂停的,则续传 + file || $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + me._trigged = false; + files.push(file); + v.transport && v.transport.send(); + } + }); + + $.each(files, function() { + this.setStatus( Status.PROGRESS ); + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this, + block; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + + + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file === file) { + block = v; + return false; + } + }); + + block.transport && block.transport.abort(); + + if (interrupt) { + me._putback(block); + me._popBlock(block); + } + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + // 正在准备中的文件。 + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + + /** + * @fileOverview Md5 + */ + define('lib/md5',[ + 'runtime/client', + 'mediator' + ], function( RuntimeClient, Mediator ) { + + function Md5() { + RuntimeClient.call( this, 'Md5' ); + } + + // 让 Md5 具备事件功能。 + Mediator.installTo( Md5.prototype ); + + Md5.prototype.loadFromBlob = function( blob ) { + var me = this; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + me.exec( 'loadFromBlob', blob ); + }); + }; + + Md5.prototype.getResult = function() { + return this.exec('getResult'); + }; + + return Md5; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/md5',[ + 'base', + 'uploader', + 'lib/md5', + 'lib/blob', + 'widgets/widget' + ], function( Base, Uploader, Md5, Blob ) { + + return Uploader.register({ + name: 'md5', + + + /** + * 计算文件 md5 值,返回一个 promise 对象,可以监听 progress 进度。 + * + * + * @method md5File + * @grammar md5File( file[, start[, end]] ) => promise + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.md5File( file ) + * + * // 及时显示进度 + * .progress(function(percentage) { + * console.log('Percentage:', percentage); + * }) + * + * // 完成 + * .then(function(val) { + * console.log('md5 result:', val); + * }); + * + * }); + */ + md5File: function( file, start, end ) { + var md5 = new Md5(), + deferred = Base.Deferred(), + blob = (file instanceof Blob) ? file : + this.request( 'get-file', file ).source; + + md5.on( 'progress load', function( e ) { + e = e || {}; + deferred.notify( e.total ? e.loaded / e.total : 1 ); + }); + + md5.on( 'complete', function() { + deferred.resolve( md5.getResult() ); + }); + + md5.on( 'error', function( reason ) { + deferred.reject( reason ); + }); + + if ( arguments.length > 1 ) { + start = start || 0; + end = end || 0; + start < 0 && (start = blob.size + start); + end < 0 && (end = blob.size + end); + end = Math.min( end, blob.size ); + blob = blob.slice( start, end ); + } + + md5.loadFromBlob( blob ); + + return deferred.promise(); + } + }); + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePaste + */ + define('runtime/html5/dnd',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + var $ = Base.$, + prefix = 'webuploader-dnd-'; + + return Html5Runtime.register( 'DragAndDrop', { + init: function() { + var elem = this.elem = this.options.container; + + this.dragEnterHandler = Base.bindFn( this._dragEnterHandler, this ); + this.dragOverHandler = Base.bindFn( this._dragOverHandler, this ); + this.dragLeaveHandler = Base.bindFn( this._dragLeaveHandler, this ); + this.dropHandler = Base.bindFn( this._dropHandler, this ); + this.dndOver = false; + + elem.on( 'dragenter', this.dragEnterHandler ); + elem.on( 'dragover', this.dragOverHandler ); + elem.on( 'dragleave', this.dragLeaveHandler ); + elem.on( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).on( 'dragover', this.dragOverHandler ); + $( document ).on( 'drop', this.dropHandler ); + } + }, + + _dragEnterHandler: function( e ) { + var me = this, + denied = me._denied || false, + items; + + e = e.originalEvent || e; + + if ( !me.dndOver ) { + me.dndOver = true; + + // 注意只有 chrome 支持。 + items = e.dataTransfer.items; + + if ( items && items.length ) { + me._denied = denied = !me.trigger( 'accept', items ); + } + + me.elem.addClass( prefix + 'over' ); + me.elem[ denied ? 'addClass' : + 'removeClass' ]( prefix + 'denied' ); + } + + e.dataTransfer.dropEffect = denied ? 'none' : 'copy'; + + return false; + }, + + _dragOverHandler: function( e ) { + // 只处理框内的。 + var parentElem = this.elem.parent().get( 0 ); + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + clearTimeout( this._leaveTimer ); + this._dragEnterHandler.call( this, e ); + + return false; + }, + + _dragLeaveHandler: function() { + var me = this, + handler; + + handler = function() { + me.dndOver = false; + me.elem.removeClass( prefix + 'over ' + prefix + 'denied' ); + }; + + clearTimeout( me._leaveTimer ); + me._leaveTimer = setTimeout( handler, 100 ); + return false; + }, + + _dropHandler: function( e ) { + var me = this, + ruid = me.getRuid(), + parentElem = me.elem.parent().get( 0 ), + dataTransfer, data; + + // 只处理框内的。 + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + e = e.originalEvent || e; + dataTransfer = e.dataTransfer; + + // 如果是页面内拖拽,还不能处理,不阻止事件。 + // 此处 ie11 下会报参数错误, + try { + data = dataTransfer.getData('text/html'); + } catch( err ) { + } + + me.dndOver = false; + me.elem.removeClass( prefix + 'over' ); + + if ( data ) { + return; + } + + me._getTansferFiles( dataTransfer, function( results ) { + me.trigger( 'drop', $.map( results, function( file ) { + return new File( ruid, file ); + }) ); + }); + + return false; + }, + + // 如果传入 callback 则去查看文件夹,否则只管当前文件夹。 + _getTansferFiles: function( dataTransfer, callback ) { + var results = [], + promises = [], + items, files, file, item, i, len, canAccessFolder; + + items = dataTransfer.items; + files = dataTransfer.files; + + canAccessFolder = !!(items && items[ 0 ].webkitGetAsEntry); + + for ( i = 0, len = files.length; i < len; i++ ) { + file = files[ i ]; + item = items && items[ i ]; + + if ( canAccessFolder && item.webkitGetAsEntry().isDirectory ) { + + promises.push( this._traverseDirectoryTree( + item.webkitGetAsEntry(), results ) ); + } else { + results.push( file ); + } + } + + Base.when.apply( Base, promises ).done(function() { + + if ( !results.length ) { + return; + } + + callback( results ); + }); + }, + + _traverseDirectoryTree: function( entry, results ) { + var deferred = Base.Deferred(), + me = this; + + if ( entry.isFile ) { + entry.file(function( file ) { + results.push( file ); + deferred.resolve(); + }); + } else if ( entry.isDirectory ) { + entry.createReader().readEntries(function( entries ) { + var len = entries.length, + promises = [], + arr = [], // 为了保证顺序。 + i; + + for ( i = 0; i < len; i++ ) { + promises.push( me._traverseDirectoryTree( + entries[ i ], arr ) ); + } + + Base.when.apply( Base, promises ).then(function() { + results.push.apply( results, arr ); + deferred.resolve(); + }, deferred.reject ); + }); + } + + return deferred.promise(); + }, + + destroy: function() { + var elem = this.elem; + + // 还没 init 就调用 destroy + if (!elem) { + return; + } + + elem.off( 'dragenter', this.dragEnterHandler ); + elem.off( 'dragover', this.dragOverHandler ); + elem.off( 'dragleave', this.dragLeaveHandler ); + elem.off( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).off( 'dragover', this.dragOverHandler ); + $( document ).off( 'drop', this.dropHandler ); + } + } + }); + }); + + /** + * @fileOverview FilePaste + */ + define('runtime/html5/filepaste',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + return Html5Runtime.register( 'FilePaste', { + init: function() { + var opts = this.options, + elem = this.elem = opts.container, + accept = '.*', + arr, i, len, item; + + // accetp的mimeTypes中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].mimeTypes; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = arr.join(','); + accept = accept.replace( /,/g, '|' ).replace( /\*/g, '.*' ); + } + } + this.accept = accept = new RegExp( accept, 'i' ); + this.hander = Base.bindFn( this._pasteHander, this ); + elem.on( 'paste', this.hander ); + }, + + _pasteHander: function( e ) { + var allowed = [], + ruid = this.getRuid(), + items, item, blob, i, len; + + e = e.originalEvent || e; + items = e.clipboardData.items; + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + + if ( item.kind !== 'file' || !(blob = item.getAsFile()) ) { + continue; + } + + allowed.push( new File( ruid, blob ) ); + } + + if ( allowed.length ) { + // 不阻止非文件粘贴(文字粘贴)的事件冒泡 + e.preventDefault(); + e.stopPropagation(); + this.trigger( 'paste', allowed ); + } + }, + + destroy: function() { + this.elem.off( 'paste', this.hander ); + } + }); + }); + + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'capture', 'camera'); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function(e) { + input.trigger('click'); + e.stopPropagation(); + owner.trigger('dialogopen'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/util',[ + 'base' + ], function( Base ) { + + var urlAPI = window.createObjectURL && window || + window.URL && URL.revokeObjectURL && URL || + window.webkitURL, + createObjectURL = Base.noop, + revokeObjectURL = createObjectURL; + + if ( urlAPI ) { + + // 更安全的方式调用,比如android里面就能把context改成其他的对象。 + createObjectURL = function() { + return urlAPI.createObjectURL.apply( urlAPI, arguments ); + }; + + revokeObjectURL = function() { + return urlAPI.revokeObjectURL.apply( urlAPI, arguments ); + }; + } + + return { + createObjectURL: createObjectURL, + revokeObjectURL: revokeObjectURL, + + dataURL2Blob: function( dataURI ) { + var byteStr, intArray, ab, i, mimetype, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + ab = new ArrayBuffer( byteStr.length ); + intArray = new Uint8Array( ab ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + mimetype = parts[ 0 ].split(':')[ 1 ].split(';')[ 0 ]; + + return this.arrayBufferToBlob( ab, mimetype ); + }, + + dataURL2ArrayBuffer: function( dataURI ) { + var byteStr, intArray, i, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + intArray = new Uint8Array( byteStr.length ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + return intArray.buffer; + }, + + arrayBufferToBlob: function( buffer, type ) { + var builder = window.BlobBuilder || window.WebKitBlobBuilder, + bb; + + // android不支持直接new Blob, 只能借助blobbuilder. + if ( builder ) { + bb = new builder(); + bb.append( buffer ); + return bb.getBlob( type ); + } + + return new Blob([ buffer ], type ? { type: type } : {} ); + }, + + // 抽出来主要是为了解决android下面canvas.toDataUrl不支持jpeg. + // 你得到的结果是png. + canvasToDataUrl: function( canvas, type, quality ) { + return canvas.toDataURL( type, quality / 100 ); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + parseMeta: function( blob, callback ) { + callback( false, {}); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + updateImageHead: function( data ) { + return data; + } + }; + }); + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/imagemeta',[ + 'runtime/html5/util' + ], function( Util ) { + + var api; + + api = { + parsers: { + 0xffe1: [] + }, + + maxMetaDataSize: 262144, + + parse: function( blob, cb ) { + var me = this, + fr = new FileReader(); + + fr.onload = function() { + cb( false, me._parse( this.result ) ); + fr = fr.onload = fr.onerror = null; + }; + + fr.onerror = function( e ) { + cb( e.message ); + fr = fr.onload = fr.onerror = null; + }; + + blob = blob.slice( 0, me.maxMetaDataSize ); + fr.readAsArrayBuffer( blob.getSource() ); + }, + + _parse: function( buffer, noParse ) { + if ( buffer.byteLength < 6 ) { + return; + } + + var dataview = new DataView( buffer ), + offset = 2, + maxOffset = dataview.byteLength - 4, + headLength = offset, + ret = {}, + markerBytes, markerLength, parsers, i; + + if ( dataview.getUint16( 0 ) === 0xffd8 ) { + + while ( offset < maxOffset ) { + markerBytes = dataview.getUint16( offset ); + + if ( markerBytes >= 0xffe0 && markerBytes <= 0xffef || + markerBytes === 0xfffe ) { + + markerLength = dataview.getUint16( offset + 2 ) + 2; + + if ( offset + markerLength > dataview.byteLength ) { + break; + } + + parsers = api.parsers[ markerBytes ]; + + if ( !noParse && parsers ) { + for ( i = 0; i < parsers.length; i += 1 ) { + parsers[ i ].call( api, dataview, offset, + markerLength, ret ); + } + } + + offset += markerLength; + headLength = offset; + } else { + break; + } + } + + if ( headLength > 6 ) { + if ( buffer.slice ) { + ret.imageHead = buffer.slice( 2, headLength ); + } else { + // Workaround for IE10, which does not yet + // support ArrayBuffer.slice: + ret.imageHead = new Uint8Array( buffer ) + .subarray( 2, headLength ); + } + } + } + + return ret; + }, + + updateImageHead: function( buffer, head ) { + var data = this._parse( buffer, true ), + buf1, buf2, bodyoffset; + + + bodyoffset = 2; + if ( data.imageHead ) { + bodyoffset = 2 + data.imageHead.byteLength; + } + + if ( buffer.slice ) { + buf2 = buffer.slice( bodyoffset ); + } else { + buf2 = new Uint8Array( buffer ).subarray( bodyoffset ); + } + + buf1 = new Uint8Array( head.byteLength + 2 + buf2.byteLength ); + + buf1[ 0 ] = 0xFF; + buf1[ 1 ] = 0xD8; + buf1.set( new Uint8Array( head ), 2 ); + buf1.set( new Uint8Array( buf2 ), head.byteLength + 2 ); + + return buf1.buffer; + } + }; + + Util.parseMeta = function() { + return api.parse.apply( api, arguments ); + }; + + Util.updateImageHead = function() { + return api.updateImageHead.apply( api, arguments ); + }; + + return api; + }); + /** + * 代码来自于:https://github.com/blueimp/JavaScript-Load-Image + * 暂时项目中只用了orientation. + * + * 去除了 Exif Sub IFD Pointer, GPS Info IFD Pointer, Exif Thumbnail. + * @fileOverview EXIF解析 + */ + + // Sample + // ==================================== + // Make : Apple + // Model : iPhone 4S + // Orientation : 1 + // XResolution : 72 [72/1] + // YResolution : 72 [72/1] + // ResolutionUnit : 2 + // Software : QuickTime 7.7.1 + // DateTime : 2013:09:01 22:53:55 + // ExifIFDPointer : 190 + // ExposureTime : 0.058823529411764705 [1/17] + // FNumber : 2.4 [12/5] + // ExposureProgram : Normal program + // ISOSpeedRatings : 800 + // ExifVersion : 0220 + // DateTimeOriginal : 2013:09:01 22:52:51 + // DateTimeDigitized : 2013:09:01 22:52:51 + // ComponentsConfiguration : YCbCr + // ShutterSpeedValue : 4.058893515764426 + // ApertureValue : 2.5260688216892597 [4845/1918] + // BrightnessValue : -0.3126686601998395 + // MeteringMode : Pattern + // Flash : Flash did not fire, compulsory flash mode + // FocalLength : 4.28 [107/25] + // SubjectArea : [4 values] + // FlashpixVersion : 0100 + // ColorSpace : 1 + // PixelXDimension : 2448 + // PixelYDimension : 3264 + // SensingMethod : One-chip color area sensor + // ExposureMode : 0 + // WhiteBalance : Auto white balance + // FocalLengthIn35mmFilm : 35 + // SceneCaptureType : Standard + define('runtime/html5/imagemeta/exif',[ + 'base', + 'runtime/html5/imagemeta' + ], function( Base, ImageMeta ) { + + var EXIF = {}; + + EXIF.ExifMap = function() { + return this; + }; + + EXIF.ExifMap.prototype.map = { + 'Orientation': 0x0112 + }; + + EXIF.ExifMap.prototype.get = function( id ) { + return this[ id ] || this[ this.map[ id ] ]; + }; + + EXIF.exifTagTypes = { + // byte, 8-bit unsigned int: + 1: { + getValue: function( dataView, dataOffset ) { + return dataView.getUint8( dataOffset ); + }, + size: 1 + }, + + // ascii, 8-bit byte: + 2: { + getValue: function( dataView, dataOffset ) { + return String.fromCharCode( dataView.getUint8( dataOffset ) ); + }, + size: 1, + ascii: true + }, + + // short, 16 bit int: + 3: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint16( dataOffset, littleEndian ); + }, + size: 2 + }, + + // long, 32 bit int: + 4: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // rational = two long values, + // first is numerator, second is denominator: + 5: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ) / + dataView.getUint32( dataOffset + 4, littleEndian ); + }, + size: 8 + }, + + // slong, 32 bit signed int: + 9: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // srational, two slongs, first is numerator, second is denominator: + 10: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ) / + dataView.getInt32( dataOffset + 4, littleEndian ); + }, + size: 8 + } + }; + + // undefined, 8-bit byte, value depending on field: + EXIF.exifTagTypes[ 7 ] = EXIF.exifTagTypes[ 1 ]; + + EXIF.getExifValue = function( dataView, tiffOffset, offset, type, length, + littleEndian ) { + + var tagType = EXIF.exifTagTypes[ type ], + tagSize, dataOffset, values, i, str, c; + + if ( !tagType ) { + Base.log('Invalid Exif data: Invalid tag type.'); + return; + } + + tagSize = tagType.size * length; + + // Determine if the value is contained in the dataOffset bytes, + // or if the value at the dataOffset is a pointer to the actual data: + dataOffset = tagSize > 4 ? tiffOffset + dataView.getUint32( offset + 8, + littleEndian ) : (offset + 8); + + if ( dataOffset + tagSize > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid data offset.'); + return; + } + + if ( length === 1 ) { + return tagType.getValue( dataView, dataOffset, littleEndian ); + } + + values = []; + + for ( i = 0; i < length; i += 1 ) { + values[ i ] = tagType.getValue( dataView, + dataOffset + i * tagType.size, littleEndian ); + } + + if ( tagType.ascii ) { + str = ''; + + // Concatenate the chars: + for ( i = 0; i < values.length; i += 1 ) { + c = values[ i ]; + + // Ignore the terminating NULL byte(s): + if ( c === '\u0000' ) { + break; + } + str += c; + } + + return str; + } + return values; + }; + + EXIF.parseExifTag = function( dataView, tiffOffset, offset, littleEndian, + data ) { + + var tag = dataView.getUint16( offset, littleEndian ); + data.exif[ tag ] = EXIF.getExifValue( dataView, tiffOffset, offset, + dataView.getUint16( offset + 2, littleEndian ), // tag type + dataView.getUint32( offset + 4, littleEndian ), // tag length + littleEndian ); + }; + + EXIF.parseExifTags = function( dataView, tiffOffset, dirOffset, + littleEndian, data ) { + + var tagsNumber, dirEndOffset, i; + + if ( dirOffset + 6 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory offset.'); + return; + } + + tagsNumber = dataView.getUint16( dirOffset, littleEndian ); + dirEndOffset = dirOffset + 2 + 12 * tagsNumber; + + if ( dirEndOffset + 4 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory size.'); + return; + } + + for ( i = 0; i < tagsNumber; i += 1 ) { + this.parseExifTag( dataView, tiffOffset, + dirOffset + 2 + 12 * i, // tag offset + littleEndian, data ); + } + + // Return the offset to the next directory: + return dataView.getUint32( dirEndOffset, littleEndian ); + }; + + // EXIF.getExifThumbnail = function(dataView, offset, length) { + // var hexData, + // i, + // b; + // if (!length || offset + length > dataView.byteLength) { + // Base.log('Invalid Exif data: Invalid thumbnail data.'); + // return; + // } + // hexData = []; + // for (i = 0; i < length; i += 1) { + // b = dataView.getUint8(offset + i); + // hexData.push((b < 16 ? '0' : '') + b.toString(16)); + // } + // return 'data:image/jpeg,%' + hexData.join('%'); + // }; + + EXIF.parseExifData = function( dataView, offset, length, data ) { + + var tiffOffset = offset + 10, + littleEndian, dirOffset; + + // Check for the ASCII code for "Exif" (0x45786966): + if ( dataView.getUint32( offset + 4 ) !== 0x45786966 ) { + // No Exif data, might be XMP data instead + return; + } + if ( tiffOffset + 8 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid segment size.'); + return; + } + + // Check for the two null bytes: + if ( dataView.getUint16( offset + 8 ) !== 0x0000 ) { + Base.log('Invalid Exif data: Missing byte alignment offset.'); + return; + } + + // Check the byte alignment: + switch ( dataView.getUint16( tiffOffset ) ) { + case 0x4949: + littleEndian = true; + break; + + case 0x4D4D: + littleEndian = false; + break; + + default: + Base.log('Invalid Exif data: Invalid byte alignment marker.'); + return; + } + + // Check for the TIFF tag marker (0x002A): + if ( dataView.getUint16( tiffOffset + 2, littleEndian ) !== 0x002A ) { + Base.log('Invalid Exif data: Missing TIFF marker.'); + return; + } + + // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: + dirOffset = dataView.getUint32( tiffOffset + 4, littleEndian ); + // Create the exif object to store the tags: + data.exif = new EXIF.ExifMap(); + // Parse the tags of the main image directory and retrieve the + // offset to the next directory, usually the thumbnail directory: + dirOffset = EXIF.parseExifTags( dataView, tiffOffset, + tiffOffset + dirOffset, littleEndian, data ); + + // 尝试读取缩略图 + // if ( dirOffset ) { + // thumbnailData = {exif: {}}; + // dirOffset = EXIF.parseExifTags( + // dataView, + // tiffOffset, + // tiffOffset + dirOffset, + // littleEndian, + // thumbnailData + // ); + + // // Check for JPEG Thumbnail offset: + // if (thumbnailData.exif[0x0201]) { + // data.exif.Thumbnail = EXIF.getExifThumbnail( + // dataView, + // tiffOffset + thumbnailData.exif[0x0201], + // thumbnailData.exif[0x0202] // Thumbnail data length + // ); + // } + // } + }; + + ImageMeta.parsers[ 0xffe1 ].push( EXIF.parseExifData ); + return EXIF; + }); + /** + * 这个方式性能不行,但是可以解决android里面的toDataUrl的bug + * android里面toDataUrl('image/jpege')得到的结果却是png. + * + * 所以这里没辙,只能借助这个工具 + * @fileOverview jpeg encoder + */ + define('runtime/html5/jpegencoder',[], function( require, exports, module ) { + + /* + Copyright (c) 2008, Adobe Systems Incorporated + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of Adobe Systems Incorporated nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + /* + JPEG encoder ported to JavaScript and optimized by Andreas Ritter, www.bytestrom.eu, 11/2009 + + Basic GUI blocking jpeg encoder + */ + + function JPEGEncoder(quality) { + var self = this; + var fround = Math.round; + var ffloor = Math.floor; + var YTable = new Array(64); + var UVTable = new Array(64); + var fdtbl_Y = new Array(64); + var fdtbl_UV = new Array(64); + var YDC_HT; + var UVDC_HT; + var YAC_HT; + var UVAC_HT; + + var bitcode = new Array(65535); + var category = new Array(65535); + var outputfDCTQuant = new Array(64); + var DU = new Array(64); + var byteout = []; + var bytenew = 0; + var bytepos = 7; + + var YDU = new Array(64); + var UDU = new Array(64); + var VDU = new Array(64); + var clt = new Array(256); + var RGB_YUV_TABLE = new Array(2048); + var currentQuality; + + var ZigZag = [ + 0, 1, 5, 6,14,15,27,28, + 2, 4, 7,13,16,26,29,42, + 3, 8,12,17,25,30,41,43, + 9,11,18,24,31,40,44,53, + 10,19,23,32,39,45,52,54, + 20,22,33,38,46,51,55,60, + 21,34,37,47,50,56,59,61, + 35,36,48,49,57,58,62,63 + ]; + + var std_dc_luminance_nrcodes = [0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0]; + var std_dc_luminance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_luminance_nrcodes = [0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d]; + var std_ac_luminance_values = [ + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12, + 0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07, + 0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08, + 0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0, + 0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16, + 0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39, + 0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49, + 0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69, + 0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79, + 0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98, + 0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7, + 0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6, + 0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5, + 0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4, + 0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea, + 0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + var std_dc_chrominance_nrcodes = [0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0]; + var std_dc_chrominance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_chrominance_nrcodes = [0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77]; + var std_ac_chrominance_values = [ + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21, + 0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71, + 0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0, + 0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34, + 0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38, + 0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48, + 0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68, + 0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78, + 0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96, + 0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5, + 0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4, + 0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3, + 0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2, + 0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9, + 0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + function initQuantTables(sf){ + var YQT = [ + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68,109,103, 77, + 24, 35, 55, 64, 81,104,113, 92, + 49, 64, 78, 87,103,121,120,101, + 72, 92, 95, 98,112,100,103, 99 + ]; + + for (var i = 0; i < 64; i++) { + var t = ffloor((YQT[i]*sf+50)/100); + if (t < 1) { + t = 1; + } else if (t > 255) { + t = 255; + } + YTable[ZigZag[i]] = t; + } + var UVQT = [ + 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99 + ]; + for (var j = 0; j < 64; j++) { + var u = ffloor((UVQT[j]*sf+50)/100); + if (u < 1) { + u = 1; + } else if (u > 255) { + u = 255; + } + UVTable[ZigZag[j]] = u; + } + var aasf = [ + 1.0, 1.387039845, 1.306562965, 1.175875602, + 1.0, 0.785694958, 0.541196100, 0.275899379 + ]; + var k = 0; + for (var row = 0; row < 8; row++) + { + for (var col = 0; col < 8; col++) + { + fdtbl_Y[k] = (1.0 / (YTable [ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + fdtbl_UV[k] = (1.0 / (UVTable[ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + k++; + } + } + } + + function computeHuffmanTbl(nrcodes, std_table){ + var codevalue = 0; + var pos_in_table = 0; + var HT = new Array(); + for (var k = 1; k <= 16; k++) { + for (var j = 1; j <= nrcodes[k]; j++) { + HT[std_table[pos_in_table]] = []; + HT[std_table[pos_in_table]][0] = codevalue; + HT[std_table[pos_in_table]][1] = k; + pos_in_table++; + codevalue++; + } + codevalue*=2; + } + return HT; + } + + function initHuffmanTbl() + { + YDC_HT = computeHuffmanTbl(std_dc_luminance_nrcodes,std_dc_luminance_values); + UVDC_HT = computeHuffmanTbl(std_dc_chrominance_nrcodes,std_dc_chrominance_values); + YAC_HT = computeHuffmanTbl(std_ac_luminance_nrcodes,std_ac_luminance_values); + UVAC_HT = computeHuffmanTbl(std_ac_chrominance_nrcodes,std_ac_chrominance_values); + } + + function initCategoryNumber() + { + var nrlower = 1; + var nrupper = 2; + for (var cat = 1; cat <= 15; cat++) { + //Positive numbers + for (var nr = nrlower; nr>0] = 38470 * i; + RGB_YUV_TABLE[(i+ 512)>>0] = 7471 * i + 0x8000; + RGB_YUV_TABLE[(i+ 768)>>0] = -11059 * i; + RGB_YUV_TABLE[(i+1024)>>0] = -21709 * i; + RGB_YUV_TABLE[(i+1280)>>0] = 32768 * i + 0x807FFF; + RGB_YUV_TABLE[(i+1536)>>0] = -27439 * i; + RGB_YUV_TABLE[(i+1792)>>0] = - 5329 * i; + } + } + + // IO functions + function writeBits(bs) + { + var value = bs[0]; + var posval = bs[1]-1; + while ( posval >= 0 ) { + if (value & (1 << posval) ) { + bytenew |= (1 << bytepos); + } + posval--; + bytepos--; + if (bytepos < 0) { + if (bytenew == 0xFF) { + writeByte(0xFF); + writeByte(0); + } + else { + writeByte(bytenew); + } + bytepos=7; + bytenew=0; + } + } + } + + function writeByte(value) + { + byteout.push(clt[value]); // write char directly instead of converting later + } + + function writeWord(value) + { + writeByte((value>>8)&0xFF); + writeByte((value )&0xFF); + } + + // DCT & quantization core + function fDCTQuant(data, fdtbl) + { + var d0, d1, d2, d3, d4, d5, d6, d7; + /* Pass 1: process rows. */ + var dataOff=0; + var i; + var I8 = 8; + var I64 = 64; + for (i=0; i 0.0) ? ((fDCTQuant + 0.5)|0) : ((fDCTQuant - 0.5)|0); + //outputfDCTQuant[i] = fround(fDCTQuant); + + } + return outputfDCTQuant; + } + + function writeAPP0() + { + writeWord(0xFFE0); // marker + writeWord(16); // length + writeByte(0x4A); // J + writeByte(0x46); // F + writeByte(0x49); // I + writeByte(0x46); // F + writeByte(0); // = "JFIF",'\0' + writeByte(1); // versionhi + writeByte(1); // versionlo + writeByte(0); // xyunits + writeWord(1); // xdensity + writeWord(1); // ydensity + writeByte(0); // thumbnwidth + writeByte(0); // thumbnheight + } + + function writeSOF0(width, height) + { + writeWord(0xFFC0); // marker + writeWord(17); // length, truecolor YUV JPG + writeByte(8); // precision + writeWord(height); + writeWord(width); + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0x11); // HVY + writeByte(0); // QTY + writeByte(2); // IdU + writeByte(0x11); // HVU + writeByte(1); // QTU + writeByte(3); // IdV + writeByte(0x11); // HVV + writeByte(1); // QTV + } + + function writeDQT() + { + writeWord(0xFFDB); // marker + writeWord(132); // length + writeByte(0); + for (var i=0; i<64; i++) { + writeByte(YTable[i]); + } + writeByte(1); + for (var j=0; j<64; j++) { + writeByte(UVTable[j]); + } + } + + function writeDHT() + { + writeWord(0xFFC4); // marker + writeWord(0x01A2); // length + + writeByte(0); // HTYDCinfo + for (var i=0; i<16; i++) { + writeByte(std_dc_luminance_nrcodes[i+1]); + } + for (var j=0; j<=11; j++) { + writeByte(std_dc_luminance_values[j]); + } + + writeByte(0x10); // HTYACinfo + for (var k=0; k<16; k++) { + writeByte(std_ac_luminance_nrcodes[k+1]); + } + for (var l=0; l<=161; l++) { + writeByte(std_ac_luminance_values[l]); + } + + writeByte(1); // HTUDCinfo + for (var m=0; m<16; m++) { + writeByte(std_dc_chrominance_nrcodes[m+1]); + } + for (var n=0; n<=11; n++) { + writeByte(std_dc_chrominance_values[n]); + } + + writeByte(0x11); // HTUACinfo + for (var o=0; o<16; o++) { + writeByte(std_ac_chrominance_nrcodes[o+1]); + } + for (var p=0; p<=161; p++) { + writeByte(std_ac_chrominance_values[p]); + } + } + + function writeSOS() + { + writeWord(0xFFDA); // marker + writeWord(12); // length + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0); // HTY + writeByte(2); // IdU + writeByte(0x11); // HTU + writeByte(3); // IdV + writeByte(0x11); // HTV + writeByte(0); // Ss + writeByte(0x3f); // Se + writeByte(0); // Bf + } + + function processDU(CDU, fdtbl, DC, HTDC, HTAC){ + var EOB = HTAC[0x00]; + var M16zeroes = HTAC[0xF0]; + var pos; + var I16 = 16; + var I63 = 63; + var I64 = 64; + var DU_DCT = fDCTQuant(CDU, fdtbl); + //ZigZag reorder + for (var j=0;j0)&&(DU[end0pos]==0); end0pos--) {}; + //end0pos = first element in reverse order !=0 + if ( end0pos == 0) { + writeBits(EOB); + return DC; + } + var i = 1; + var lng; + while ( i <= end0pos ) { + var startpos = i; + for (; (DU[i]==0) && (i<=end0pos); ++i) {} + var nrzeroes = i-startpos; + if ( nrzeroes >= I16 ) { + lng = nrzeroes>>4; + for (var nrmarker=1; nrmarker <= lng; ++nrmarker) + writeBits(M16zeroes); + nrzeroes = nrzeroes&0xF; + } + pos = 32767+DU[i]; + writeBits(HTAC[(nrzeroes<<4)+category[pos]]); + writeBits(bitcode[pos]); + i++; + } + if ( end0pos != I63 ) { + writeBits(EOB); + } + return DC; + } + + function initCharLookupTable(){ + var sfcc = String.fromCharCode; + for(var i=0; i < 256; i++){ ///// ACHTUNG // 255 + clt[i] = sfcc(i); + } + } + + this.encode = function(image,quality) // image data object + { + // var time_start = new Date().getTime(); + + if(quality) setQuality(quality); + + // Initialize bit writer + byteout = new Array(); + bytenew=0; + bytepos=7; + + // Add JPEG headers + writeWord(0xFFD8); // SOI + writeAPP0(); + writeDQT(); + writeSOF0(image.width,image.height); + writeDHT(); + writeSOS(); + + + // Encode 8x8 macroblocks + var DCY=0; + var DCU=0; + var DCV=0; + + bytenew=0; + bytepos=7; + + + this.encode.displayName = "_encode_"; + + var imageData = image.data; + var width = image.width; + var height = image.height; + + var quadWidth = width*4; + var tripleWidth = width*3; + + var x, y = 0; + var r, g, b; + var start,p, col,row,pos; + while(y < height){ + x = 0; + while(x < quadWidth){ + start = quadWidth * y + x; + p = start; + col = -1; + row = 0; + + for(pos=0; pos < 64; pos++){ + row = pos >> 3;// /8 + col = ( pos & 7 ) * 4; // %8 + p = start + ( row * quadWidth ) + col; + + if(y+row >= height){ // padding bottom + p-= (quadWidth*(y+1+row-height)); + } + + if(x+col >= quadWidth){ // padding right + p-= ((x+col) - quadWidth +4) + } + + r = imageData[ p++ ]; + g = imageData[ p++ ]; + b = imageData[ p++ ]; + + + /* // calculate YUV values dynamically + YDU[pos]=((( 0.29900)*r+( 0.58700)*g+( 0.11400)*b))-128; //-0x80 + UDU[pos]=(((-0.16874)*r+(-0.33126)*g+( 0.50000)*b)); + VDU[pos]=((( 0.50000)*r+(-0.41869)*g+(-0.08131)*b)); + */ + + // use lookup table (slightly faster) + YDU[pos] = ((RGB_YUV_TABLE[r] + RGB_YUV_TABLE[(g + 256)>>0] + RGB_YUV_TABLE[(b + 512)>>0]) >> 16)-128; + UDU[pos] = ((RGB_YUV_TABLE[(r + 768)>>0] + RGB_YUV_TABLE[(g + 1024)>>0] + RGB_YUV_TABLE[(b + 1280)>>0]) >> 16)-128; + VDU[pos] = ((RGB_YUV_TABLE[(r + 1280)>>0] + RGB_YUV_TABLE[(g + 1536)>>0] + RGB_YUV_TABLE[(b + 1792)>>0]) >> 16)-128; + + } + + DCY = processDU(YDU, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCU = processDU(UDU, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = processDU(VDU, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + x+=32; + } + y+=8; + } + + + //////////////////////////////////////////////////////////////// + + // Do the bit alignment of the EOI marker + if ( bytepos >= 0 ) { + var fillbits = []; + fillbits[1] = bytepos+1; + fillbits[0] = (1<<(bytepos+1))-1; + writeBits(fillbits); + } + + writeWord(0xFFD9); //EOI + + var jpegDataUri = 'data:image/jpeg;base64,' + btoa(byteout.join('')); + + byteout = []; + + // benchmarking + // var duration = new Date().getTime() - time_start; + // console.log('Encoding time: '+ currentQuality + 'ms'); + // + + return jpegDataUri + } + + function setQuality(quality){ + if (quality <= 0) { + quality = 1; + } + if (quality > 100) { + quality = 100; + } + + if(currentQuality == quality) return // don't recalc if unchanged + + var sf = 0; + if (quality < 50) { + sf = Math.floor(5000 / quality); + } else { + sf = Math.floor(200 - quality*2); + } + + initQuantTables(sf); + currentQuality = quality; + // console.log('Quality set to: '+quality +'%'); + } + + function init(){ + // var time_start = new Date().getTime(); + if(!quality) quality = 50; + // Create tables + initCharLookupTable() + initHuffmanTbl(); + initCategoryNumber(); + initRGBYUVTable(); + + setQuality(quality); + // var duration = new Date().getTime() - time_start; + // console.log('Initialization '+ duration + 'ms'); + } + + init(); + + }; + + JPEGEncoder.encode = function( data, quality ) { + var encoder = new JPEGEncoder( quality ); + + return encoder.encode( data ); + } + + return JPEGEncoder; + }); + /** + * @fileOverview Fix android canvas.toDataUrl bug. + */ + define('runtime/html5/androidpatch',[ + 'runtime/html5/util', + 'runtime/html5/jpegencoder', + 'base' + ], function( Util, encoder, Base ) { + var origin = Util.canvasToDataUrl, + supportJpeg; + + Util.canvasToDataUrl = function( canvas, type, quality ) { + var ctx, w, h, fragement, parts; + + // 非android手机直接跳过。 + if ( !Base.os.android ) { + return origin.apply( null, arguments ); + } + + // 检测是否canvas支持jpeg导出,根据数据格式来判断。 + // JPEG 前两位分别是:255, 216 + if ( type === 'image/jpeg' && typeof supportJpeg === 'undefined' ) { + fragement = origin.apply( null, arguments ); + + parts = fragement.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + fragement = atob( parts[ 1 ] ); + } else { + fragement = decodeURIComponent( parts[ 1 ] ); + } + + fragement = fragement.substring( 0, 2 ); + + supportJpeg = fragement.charCodeAt( 0 ) === 255 && + fragement.charCodeAt( 1 ) === 216; + } + + // 只有在android环境下才修复 + if ( type === 'image/jpeg' && !supportJpeg ) { + w = canvas.width; + h = canvas.height; + ctx = canvas.getContext('2d'); + + return encoder.encode( ctx.getImageData( 0, 0, w, h ), quality ); + } + + return origin.apply( null, arguments ); + }; + }); + /** + * @fileOverview Image + */ + define('runtime/html5/image',[ + 'base', + 'runtime/html5/runtime', + 'runtime/html5/util' + ], function( Base, Html5Runtime, Util ) { + + var BLANK = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D'; + + return Html5Runtime.register( 'Image', { + + // flag: 标记是否被修改过。 + modified: false, + + init: function() { + var me = this, + img = new Image(); + + img.onload = function() { + + me._info = { + type: me.type, + width: this.width, + height: this.height + }; + + //debugger; + + // 读取meta信息。 + if ( !me._metas && 'image/jpeg' === me.type ) { + Util.parseMeta( me._blob, function( error, ret ) { + me._metas = ret; + me.owner.trigger('load'); + }); + } else { + me.owner.trigger('load'); + } + }; + + img.onerror = function() { + me.owner.trigger('error'); + }; + + me._img = img; + }, + + loadFromBlob: function( blob ) { + var me = this, + img = me._img; + + me._blob = blob; + me.type = blob.type; + img.src = Util.createObjectURL( blob.getSource() ); + me.owner.once( 'load', function() { + Util.revokeObjectURL( img.src ); + }); + }, + + resize: function( width, height ) { + var canvas = this._canvas || + (this._canvas = document.createElement('canvas')); + + this._resize( this._img, canvas, width, height ); + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'resize' ); + }, + + crop: function( x, y, w, h, s ) { + var cvs = this._canvas || + (this._canvas = document.createElement('canvas')), + opts = this.options, + img = this._img, + iw = img.naturalWidth, + ih = img.naturalHeight, + orientation = this.getOrientation(); + + s = s || 1; + + // todo 解决 orientation 的问题。 + // values that require 90 degree rotation + // if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // switch ( orientation ) { + // case 6: + // tmp = x; + // x = y; + // y = iw * s - tmp - w; + // console.log(ih * s, tmp, w) + // break; + // } + + // (w ^= h, h ^= w, w ^= h); + // } + + cvs.width = w; + cvs.height = h; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + this._renderImageToCanvas( cvs, img, -x, -y, iw * s, ih * s ); + + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'crop' ); + }, + + getAsBlob: function( type ) { + var blob = this._blob, + opts = this.options, + canvas; + + type = type || this.type; + + // blob需要重新生成。 + if ( this.modified || this.type !== type ) { + canvas = this._canvas; + + if ( type === 'image/jpeg' ) { + + blob = Util.canvasToDataUrl( canvas, type, opts.quality ); + + if ( opts.preserveHeaders && this._metas && + this._metas.imageHead ) { + + blob = Util.dataURL2ArrayBuffer( blob ); + blob = Util.updateImageHead( blob, + this._metas.imageHead ); + blob = Util.arrayBufferToBlob( blob, type ); + return blob; + } + } else { + blob = Util.canvasToDataUrl( canvas, type ); + } + + blob = Util.dataURL2Blob( blob ); + } + + return blob; + }, + + getAsDataUrl: function( type ) { + var opts = this.options; + + type = type || this.type; + + if ( type === 'image/jpeg' ) { + return Util.canvasToDataUrl( this._canvas, type, opts.quality ); + } else { + return this._canvas.toDataURL( type ); + } + }, + + getOrientation: function() { + return this._metas && this._metas.exif && + this._metas.exif.get('Orientation') || 1; + }, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._metas = val; + return this; + } + + // getter + return this._metas; + }, + + destroy: function() { + var canvas = this._canvas; + this._img.onload = null; + + if ( canvas ) { + canvas.getContext('2d') + .clearRect( 0, 0, canvas.width, canvas.height ); + canvas.width = canvas.height = 0; + this._canvas = null; + } + + // 释放内存。非常重要,否则释放不了image的内存。 + this._img.src = BLANK; + this._img = this._blob = null; + }, + + _resize: function( img, cvs, width, height ) { + var opts = this.options, + naturalWidth = img.width, + naturalHeight = img.height, + orientation = this.getOrientation(), + scale, w, h, x, y; + + // values that require 90 degree rotation + if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // 交换width, height的值。 + width ^= height; + height ^= width; + width ^= height; + } + + scale = Math[ opts.crop ? 'max' : 'min' ]( width / naturalWidth, + height / naturalHeight ); + + // 不允许放大。 + opts.allowMagnify || (scale = Math.min( 1, scale )); + + w = naturalWidth * scale; + h = naturalHeight * scale; + + if ( opts.crop ) { + cvs.width = width; + cvs.height = height; + } else { + cvs.width = w; + cvs.height = h; + } + + x = (cvs.width - w) / 2; + y = (cvs.height - h) / 2; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + + this._renderImageToCanvas( cvs, img, x, y, w, h ); + }, + + _rotate2Orientaion: function( canvas, orientation ) { + var width = canvas.width, + height = canvas.height, + ctx = canvas.getContext('2d'); + + switch ( orientation ) { + case 5: + case 6: + case 7: + case 8: + canvas.width = height; + canvas.height = width; + break; + } + + 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( Math.PI ); + break; + + case 4: // vertical flip + ctx.translate( 0, height ); + ctx.scale( 1, -1 ); + break; + + case 5: // vertical flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.scale( 1, -1 ); + break; + + case 6: // 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( 0, -height ); + break; + + case 7: // horizontal flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( width, -height ); + ctx.scale( -1, 1 ); + break; + + case 8: // 90 rotate left + ctx.rotate( -0.5 * Math.PI ); + ctx.translate( -width, 0 ); + break; + } + }, + + // https://github.com/stomita/ios-imagefile-megapixel/ + // blob/master/src/megapix-image.js + _renderImageToCanvas: (function() { + + // 如果不是ios, 不需要这么复杂! + if ( !Base.os.ios ) { + return function( canvas ) { + var args = Base.slice( arguments, 1 ), + ctx = canvas.getContext('2d'); + + ctx.drawImage.apply( ctx, args ); + }; + } + + /** + * Detecting vertical squash in loaded image. + * Fixes a bug which squash image vertically while drawing into + * canvas for some images. + */ + function detectVerticalSquash( img, iw, ih ) { + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'), + sy = 0, + ey = ih, + py = ih, + data, alpha, ratio; + + + canvas.width = 1; + canvas.height = ih; + ctx.drawImage( img, 0, 0 ); + data = ctx.getImageData( 0, 0, 1, ih ).data; + + // search image edge pixel position in case + // it is squashed vertically. + while ( py > sy ) { + alpha = data[ (py - 1) * 4 + 3 ]; + + if ( alpha === 0 ) { + ey = py; + } else { + sy = py; + } + + py = (ey + sy) >> 1; + } + + ratio = (py / ih); + return (ratio === 0) ? 1 : ratio; + } + + // fix ie7 bug + // http://stackoverflow.com/questions/11929099/ + // html5-canvas-drawimage-ratio-bug-ios + if ( Base.os.ios >= 7 ) { + return function( canvas, img, x, y, w, h ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + vertSquashRatio = detectVerticalSquash( img, iw, ih ); + + return canvas.getContext('2d').drawImage( img, 0, 0, + iw * vertSquashRatio, ih * vertSquashRatio, + x, y, w, h ); + }; + } + + /** + * Detect subsampling in loaded image. + * In iOS, larger images than 2M pixels may be + * subsampled in rendering. + */ + function detectSubsampling( img ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + canvas, ctx; + + // subsampling may happen overmegapixel image + if ( iw * ih > 1024 * 1024 ) { + canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + ctx = canvas.getContext('2d'); + ctx.drawImage( img, -iw + 1, 0 ); + + // subsampled image becomes half smaller in rendering size. + // check alpha channel value to confirm image is covering + // edge pixel or not. if alpha value is 0 + // image is not covering, hence subsampled. + return ctx.getImageData( 0, 0, 1, 1 ).data[ 3 ] === 0; + } else { + return false; + } + } + + + return function( canvas, img, x, y, width, height ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + ctx = canvas.getContext('2d'), + subsampled = detectSubsampling( img ), + doSquash = this.type === 'image/jpeg', + d = 1024, + sy = 0, + dy = 0, + tmpCanvas, tmpCtx, vertSquashRatio, dw, dh, sx, dx; + + if ( subsampled ) { + iw /= 2; + ih /= 2; + } + + ctx.save(); + tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = tmpCanvas.height = d; + + tmpCtx = tmpCanvas.getContext('2d'); + vertSquashRatio = doSquash ? + detectVerticalSquash( img, iw, ih ) : 1; + + dw = Math.ceil( d * width / iw ); + dh = Math.ceil( d * height / ih / vertSquashRatio ); + + while ( sy < ih ) { + sx = 0; + dx = 0; + while ( sx < iw ) { + tmpCtx.clearRect( 0, 0, d, d ); + tmpCtx.drawImage( img, -sx, -sy ); + ctx.drawImage( tmpCanvas, 0, 0, d, d, + x + dx, y + dy, dw, dh ); + sx += d; + dx += dw; + } + sy += d; + dy += dh; + } + ctx.restore(); + tmpCanvas = tmpCtx = null; + }; + })() + }); + }); + + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/html5/md5',[ + 'runtime/html5/runtime' + ], function( FlashRuntime ) { + + /* + * Fastest md5 implementation around (JKM md5) + * Credits: Joseph Myers + * + * @see http://www.myersdaily.org/joseph/javascript/md5-text.html + * @see http://jsperf.com/md5-shootout/7 + */ + + /* this function is much faster, + so if possible we use it. Some IEs + are the only ones I know of that + need the idiotic second function, + generated by an if clause. */ + var add32 = function (a, b) { + return (a + b) & 0xFFFFFFFF; + }, + + cmn = function (q, a, b, x, s, t) { + a = add32(add32(a, q), add32(x, t)); + return add32((a << s) | (a >>> (32 - s)), b); + }, + + ff = function (a, b, c, d, x, s, t) { + return cmn((b & c) | ((~b) & d), a, b, x, s, t); + }, + + gg = function (a, b, c, d, x, s, t) { + return cmn((b & d) | (c & (~d)), a, b, x, s, t); + }, + + hh = function (a, b, c, d, x, s, t) { + return cmn(b ^ c ^ d, a, b, x, s, t); + }, + + ii = function (a, b, c, d, x, s, t) { + return cmn(c ^ (b | (~d)), a, b, x, s, t); + }, + + md5cycle = function (x, k) { + var a = x[0], + b = x[1], + c = x[2], + d = x[3]; + + a = ff(a, b, c, d, k[0], 7, -680876936); + d = ff(d, a, b, c, k[1], 12, -389564586); + c = ff(c, d, a, b, k[2], 17, 606105819); + b = ff(b, c, d, a, k[3], 22, -1044525330); + a = ff(a, b, c, d, k[4], 7, -176418897); + d = ff(d, a, b, c, k[5], 12, 1200080426); + c = ff(c, d, a, b, k[6], 17, -1473231341); + b = ff(b, c, d, a, k[7], 22, -45705983); + a = ff(a, b, c, d, k[8], 7, 1770035416); + d = ff(d, a, b, c, k[9], 12, -1958414417); + c = ff(c, d, a, b, k[10], 17, -42063); + b = ff(b, c, d, a, k[11], 22, -1990404162); + a = ff(a, b, c, d, k[12], 7, 1804603682); + d = ff(d, a, b, c, k[13], 12, -40341101); + c = ff(c, d, a, b, k[14], 17, -1502002290); + b = ff(b, c, d, a, k[15], 22, 1236535329); + + a = gg(a, b, c, d, k[1], 5, -165796510); + d = gg(d, a, b, c, k[6], 9, -1069501632); + c = gg(c, d, a, b, k[11], 14, 643717713); + b = gg(b, c, d, a, k[0], 20, -373897302); + a = gg(a, b, c, d, k[5], 5, -701558691); + d = gg(d, a, b, c, k[10], 9, 38016083); + c = gg(c, d, a, b, k[15], 14, -660478335); + b = gg(b, c, d, a, k[4], 20, -405537848); + a = gg(a, b, c, d, k[9], 5, 568446438); + d = gg(d, a, b, c, k[14], 9, -1019803690); + c = gg(c, d, a, b, k[3], 14, -187363961); + b = gg(b, c, d, a, k[8], 20, 1163531501); + a = gg(a, b, c, d, k[13], 5, -1444681467); + d = gg(d, a, b, c, k[2], 9, -51403784); + c = gg(c, d, a, b, k[7], 14, 1735328473); + b = gg(b, c, d, a, k[12], 20, -1926607734); + + a = hh(a, b, c, d, k[5], 4, -378558); + d = hh(d, a, b, c, k[8], 11, -2022574463); + c = hh(c, d, a, b, k[11], 16, 1839030562); + b = hh(b, c, d, a, k[14], 23, -35309556); + a = hh(a, b, c, d, k[1], 4, -1530992060); + d = hh(d, a, b, c, k[4], 11, 1272893353); + c = hh(c, d, a, b, k[7], 16, -155497632); + b = hh(b, c, d, a, k[10], 23, -1094730640); + a = hh(a, b, c, d, k[13], 4, 681279174); + d = hh(d, a, b, c, k[0], 11, -358537222); + c = hh(c, d, a, b, k[3], 16, -722521979); + b = hh(b, c, d, a, k[6], 23, 76029189); + a = hh(a, b, c, d, k[9], 4, -640364487); + d = hh(d, a, b, c, k[12], 11, -421815835); + c = hh(c, d, a, b, k[15], 16, 530742520); + b = hh(b, c, d, a, k[2], 23, -995338651); + + a = ii(a, b, c, d, k[0], 6, -198630844); + d = ii(d, a, b, c, k[7], 10, 1126891415); + c = ii(c, d, a, b, k[14], 15, -1416354905); + b = ii(b, c, d, a, k[5], 21, -57434055); + a = ii(a, b, c, d, k[12], 6, 1700485571); + d = ii(d, a, b, c, k[3], 10, -1894986606); + c = ii(c, d, a, b, k[10], 15, -1051523); + b = ii(b, c, d, a, k[1], 21, -2054922799); + a = ii(a, b, c, d, k[8], 6, 1873313359); + d = ii(d, a, b, c, k[15], 10, -30611744); + c = ii(c, d, a, b, k[6], 15, -1560198380); + b = ii(b, c, d, a, k[13], 21, 1309151649); + a = ii(a, b, c, d, k[4], 6, -145523070); + d = ii(d, a, b, c, k[11], 10, -1120210379); + c = ii(c, d, a, b, k[2], 15, 718787259); + b = ii(b, c, d, a, k[9], 21, -343485551); + + x[0] = add32(a, x[0]); + x[1] = add32(b, x[1]); + x[2] = add32(c, x[2]); + x[3] = add32(d, x[3]); + }, + + /* there needs to be support for Unicode here, + * unless we pretend that we can redefine the MD-5 + * algorithm for multi-byte characters (perhaps + * by adding every four 16-bit characters and + * shortening the sum to 32 bits). Otherwise + * I suggest performing MD-5 as if every character + * was two bytes--e.g., 0040 0025 = @%--but then + * how will an ordinary MD-5 sum be matched? + * There is no way to standardize text to something + * like UTF-8 before transformation; speed cost is + * utterly prohibitive. The JavaScript standard + * itself needs to look at this: it should start + * providing access to strings as preformed UTF-8 + * 8-bit unsigned value arrays. + */ + md5blk = function (s) { + var md5blks = [], + i; /* Andy King said do it this way. */ + + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24); + } + return md5blks; + }, + + md5blk_array = function (a) { + var md5blks = [], + i; /* Andy King said do it this way. */ + + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = a[i] + (a[i + 1] << 8) + (a[i + 2] << 16) + (a[i + 3] << 24); + } + return md5blks; + }, + + md51 = function (s) { + var n = s.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i, + length, + tail, + tmp, + lo, + hi; + + for (i = 64; i <= n; i += 64) { + md5cycle(state, md5blk(s.substring(i - 64, i))); + } + s = s.substring(i - 64); + length = s.length; + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); + } + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Beware that the final length might not fit in 32 bits so we take care of that + tmp = n * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + + md5cycle(state, tail); + return state; + }, + + md51_array = function (a) { + var n = a.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i, + length, + tail, + tmp, + lo, + hi; + + for (i = 64; i <= n; i += 64) { + md5cycle(state, md5blk_array(a.subarray(i - 64, i))); + } + + // Not sure if it is a bug, however IE10 will always produce a sub array of length 1 + // containing the last element of the parent array if the sub array specified starts + // beyond the length of the parent array - weird. + // https://connect.microsoft.com/IE/feedback/details/771452/typed-array-subarray-issue + a = (i - 64) < n ? a.subarray(i - 64) : new Uint8Array(0); + + length = a.length; + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= a[i] << ((i % 4) << 3); + } + + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Beware that the final length might not fit in 32 bits so we take care of that + tmp = n * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + + md5cycle(state, tail); + + return state; + }, + + hex_chr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'], + + rhex = function (n) { + var s = '', + j; + for (j = 0; j < 4; j += 1) { + s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F]; + } + return s; + }, + + hex = function (x) { + var i; + for (i = 0; i < x.length; i += 1) { + x[i] = rhex(x[i]); + } + return x.join(''); + }, + + md5 = function (s) { + return hex(md51(s)); + }, + + + + //////////////////////////////////////////////////////////////////////////// + + /** + * SparkMD5 OOP implementation. + * + * Use this class to perform an incremental md5, otherwise use the + * static methods instead. + */ + SparkMD5 = function () { + // call reset to init the instance + this.reset(); + }; + + + // In some cases the fast add32 function cannot be used.. + if (md5('hello') !== '5d41402abc4b2a76b9719d911017c592') { + add32 = function (x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF), + msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + }; + } + + + /** + * Appends a string. + * A conversion will be applied if an utf8 string is detected. + * + * @param {String} str The string to be appended + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.append = function (str) { + // converts the string to utf8 bytes if necessary + if (/[\u0080-\uFFFF]/.test(str)) { + str = unescape(encodeURIComponent(str)); + } + + // then append as binary + this.appendBinary(str); + + return this; + }; + + /** + * Appends a binary string. + * + * @param {String} contents The binary string to be appended + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.appendBinary = function (contents) { + this._buff += contents; + this._length += contents.length; + + var length = this._buff.length, + i; + + for (i = 64; i <= length; i += 64) { + md5cycle(this._state, md5blk(this._buff.substring(i - 64, i))); + } + + this._buff = this._buff.substr(i - 64); + + return this; + }; + + /** + * Finishes the incremental computation, reseting the internal state and + * returning the result. + * Use the raw parameter to obtain the raw result instead of the hex one. + * + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.prototype.end = function (raw) { + var buff = this._buff, + length = buff.length, + i, + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ret; + + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= buff.charCodeAt(i) << ((i % 4) << 3); + } + + this._finish(tail, length); + ret = !!raw ? this._state : hex(this._state); + + this.reset(); + + return ret; + }; + + /** + * Finish the final calculation based on the tail. + * + * @param {Array} tail The tail (will be modified) + * @param {Number} length The length of the remaining buffer + */ + SparkMD5.prototype._finish = function (tail, length) { + var i = length, + tmp, + lo, + hi; + + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(this._state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Do the final computation based on the tail and length + // Beware that the final length may not fit in 32 bits so we take care of that + tmp = this._length * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + md5cycle(this._state, tail); + }; + + /** + * Resets the internal state of the computation. + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.reset = function () { + this._buff = ""; + this._length = 0; + this._state = [1732584193, -271733879, -1732584194, 271733878]; + + return this; + }; + + /** + * Releases memory used by the incremental buffer and other aditional + * resources. If you plan to use the instance again, use reset instead. + */ + SparkMD5.prototype.destroy = function () { + delete this._state; + delete this._buff; + delete this._length; + }; + + + /** + * Performs the md5 hash on a string. + * A conversion will be applied if utf8 string is detected. + * + * @param {String} str The string + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.hash = function (str, raw) { + // converts the string to utf8 bytes if necessary + if (/[\u0080-\uFFFF]/.test(str)) { + str = unescape(encodeURIComponent(str)); + } + + var hash = md51(str); + + return !!raw ? hash : hex(hash); + }; + + /** + * Performs the md5 hash on a binary string. + * + * @param {String} content The binary string + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.hashBinary = function (content, raw) { + var hash = md51(content); + + return !!raw ? hash : hex(hash); + }; + + /** + * SparkMD5 OOP implementation for array buffers. + * + * Use this class to perform an incremental md5 ONLY for array buffers. + */ + SparkMD5.ArrayBuffer = function () { + // call reset to init the instance + this.reset(); + }; + + //////////////////////////////////////////////////////////////////////////// + + /** + * Appends an array buffer. + * + * @param {ArrayBuffer} arr The array to be appended + * + * @return {SparkMD5.ArrayBuffer} The instance itself + */ + SparkMD5.ArrayBuffer.prototype.append = function (arr) { + // TODO: we could avoid the concatenation here but the algorithm would be more complex + // if you find yourself needing extra performance, please make a PR. + var buff = this._concatArrayBuffer(this._buff, arr), + length = buff.length, + i; + + this._length += arr.byteLength; + + for (i = 64; i <= length; i += 64) { + md5cycle(this._state, md5blk_array(buff.subarray(i - 64, i))); + } + + // Avoids IE10 weirdness (documented above) + this._buff = (i - 64) < length ? buff.subarray(i - 64) : new Uint8Array(0); + + return this; + }; + + /** + * Finishes the incremental computation, reseting the internal state and + * returning the result. + * Use the raw parameter to obtain the raw result instead of the hex one. + * + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.ArrayBuffer.prototype.end = function (raw) { + var buff = this._buff, + length = buff.length, + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + i, + ret; + + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= buff[i] << ((i % 4) << 3); + } + + this._finish(tail, length); + ret = !!raw ? this._state : hex(this._state); + + this.reset(); + + return ret; + }; + + SparkMD5.ArrayBuffer.prototype._finish = SparkMD5.prototype._finish; + + /** + * Resets the internal state of the computation. + * + * @return {SparkMD5.ArrayBuffer} The instance itself + */ + SparkMD5.ArrayBuffer.prototype.reset = function () { + this._buff = new Uint8Array(0); + this._length = 0; + this._state = [1732584193, -271733879, -1732584194, 271733878]; + + return this; + }; + + /** + * Releases memory used by the incremental buffer and other aditional + * resources. If you plan to use the instance again, use reset instead. + */ + SparkMD5.ArrayBuffer.prototype.destroy = SparkMD5.prototype.destroy; + + /** + * Concats two array buffers, returning a new one. + * + * @param {ArrayBuffer} first The first array buffer + * @param {ArrayBuffer} second The second array buffer + * + * @return {ArrayBuffer} The new array buffer + */ + SparkMD5.ArrayBuffer.prototype._concatArrayBuffer = function (first, second) { + var firstLength = first.length, + result = new Uint8Array(firstLength + second.byteLength); + + result.set(first); + result.set(new Uint8Array(second), firstLength); + + return result; + }; + + /** + * Performs the md5 hash on an array buffer. + * + * @param {ArrayBuffer} arr The array buffer + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.ArrayBuffer.hash = function (arr, raw) { + var hash = md51_array(new Uint8Array(arr)); + + return !!raw ? hash : hex(hash); + }; + + return FlashRuntime.register( 'Md5', { + init: function() { + // do nothing. + }, + + loadFromBlob: function( file ) { + var blob = file.getSource(), + chunkSize = 2 * 1024 * 1024, + chunks = Math.ceil( blob.size / chunkSize ), + chunk = 0, + owner = this.owner, + spark = new SparkMD5.ArrayBuffer(), + me = this, + blobSlice = blob.mozSlice || blob.webkitSlice || blob.slice, + loadNext, fr; + + fr = new FileReader(); + + loadNext = function() { + var start, end; + + start = chunk * chunkSize; + end = Math.min( start + chunkSize, blob.size ); + + fr.onload = function( e ) { + spark.append( e.target.result ); + owner.trigger( 'progress', { + total: file.size, + loaded: end + }); + }; + + fr.onloadend = function() { + fr.onloadend = fr.onload = null; + + if ( ++chunk < chunks ) { + setTimeout( loadNext, 1 ); + } else { + setTimeout(function(){ + owner.trigger('load'); + me.result = spark.end(); + loadNext = file = blob = spark = null; + owner.trigger('complete'); + }, 50 ); + } + }; + + fr.readAsArrayBuffer( blobSlice.call( blob, start, end ) ); + }; + + loadNext(); + }, + + getResult: function() { + return this.result; + } + }); + }); + /** + * @fileOverview FlashRuntime + */ + define('runtime/flash/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var $ = Base.$, + type = 'flash', + components = {}; + + + function getFlashVersion() { + var version; + + try { + version = navigator.plugins[ 'Shockwave Flash' ]; + version = version.description; + } catch ( ex ) { + try { + version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash') + .GetVariable('$version'); + } catch ( ex2 ) { + version = '0.0'; + } + } + version = version.match( /\d+/g ); + return parseFloat( version[ 0 ] + '.' + version[ 1 ], 10 ); + } + + function FlashRuntime() { + var pool = {}, + clients = {}, + destroy = this.destroy, + me = this, + jsreciver = Base.guid('webuploader_'); + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/ ) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + clients[ uid ] = client; + + if ( components[ comp ] ) { + if ( !pool[ uid ] ) { + pool[ uid ] = new components[ comp ]( client, me ); + } + + instance = pool[ uid ]; + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + + return me.flashExec.apply( client, arguments ); + }; + + function handler( evt, obj ) { + var type = evt.type || evt, + parts, uid; + + parts = type.split('::'); + uid = parts[ 0 ]; + type = parts[ 1 ]; + + // console.log.apply( console, arguments ); + + if ( type === 'Ready' && uid === me.uid ) { + me.trigger('ready'); + } else if ( clients[ uid ] ) { + clients[ uid ].trigger( type.toLowerCase(), evt, obj ); + } + + // Base.log( evt, obj ); + } + + // flash的接受器。 + window[ jsreciver ] = function() { + var args = arguments; + + // 为了能捕获得到。 + setTimeout(function() { + handler.apply( null, args ); + }, 1 ); + }; + + this.jsreciver = jsreciver; + + this.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + + this.flashExec = function( comp, fn ) { + var flash = me.getFlash(), + args = Base.slice( arguments, 2 ); + + return flash.exec( this.uid, comp, fn, args ); + }; + + // @todo + } + + Base.inherits( Runtime, { + constructor: FlashRuntime, + + init: function() { + var container = this.getContainer(), + opts = this.options, + html; + + // 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) + container.css({ + position: 'absolute', + top: '-8px', + left: '-8px', + width: '9px', + height: '9px', + overflow: 'hidden' + }); + + // insert flash object + html = '' + + '' + + '' + + '' + + ''; + + container.html( html ); + }, + + getFlash: function() { + if ( this._flash ) { + return this._flash; + } + + this._flash = $( '#' + this.uid ).get( 0 ); + return this._flash; + } + + }); + + FlashRuntime.register = function( name, component ) { + component = components[ name ] = Base.inherits( CompBase, $.extend({ + + // @todo fix this later + flashExec: function() { + var owner = this.owner, + runtime = this.getRuntime(); + + return runtime.flashExec.apply( owner, arguments ); + } + }, component ) ); + + return component; + }; + + if ( getFlashVersion() >= 11.4 ) { + Runtime.addRuntime( type, FlashRuntime ); + } + + return FlashRuntime; + }); + /** + * @fileOverview FilePicker + */ + define('runtime/flash/filepicker',[ + 'base', + 'runtime/flash/runtime' + ], function( Base, FlashRuntime ) { + var $ = Base.$; + + return FlashRuntime.register( 'FilePicker', { + init: function( opts ) { + var copy = $.extend({}, opts ), + len, i; + + // 修复Flash再没有设置title的情况下无法弹出flash文件选择框的bug. + len = copy.accept && copy.accept.length; + for ( i = 0; i < len; i++ ) { + if ( !copy.accept[ i ].title ) { + copy.accept[ i ].title = 'Files'; + } + } + + delete copy.button; + delete copy.id; + delete copy.container; + + this.flashExec( 'FilePicker', 'init', copy ); + }, + + destroy: function() { + this.flashExec( 'FilePicker', 'destroy' ); + } + }); + }); + /** + * @fileOverview 图片压缩 + */ + define('runtime/flash/image',[ + 'runtime/flash/runtime' + ], function( FlashRuntime ) { + + return FlashRuntime.register( 'Image', { + // init: function( options ) { + // var owner = this.owner; + + // this.flashExec( 'Image', 'init', options ); + // owner.on( 'load', function() { + // debugger; + // }); + // }, + + loadFromBlob: function( blob ) { + var owner = this.owner; + + owner.info() && this.flashExec( 'Image', 'info', owner.info() ); + owner.meta() && this.flashExec( 'Image', 'meta', owner.meta() ); + + this.flashExec( 'Image', 'loadFromBlob', blob.uid ); + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/flash/transport',[ + 'base', + 'runtime/flash/runtime', + 'runtime/client' + ], function( Base, FlashRuntime, RuntimeClient ) { + var $ = Base.$; + + return FlashRuntime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + this._responseJson = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + binary; + + xhr.connectRuntime( blob.ruid ); + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.uid; + } else { + $.each( owner._formData, function( k, v ) { + xhr.exec( 'append', k, v ); + }); + + xhr.exec( 'appendBlob', opts.fileVal, blob.uid, + opts.filename || owner._formData.name || '' ); + } + + this._setRequestHeader( xhr, opts.headers ); + xhr.exec( 'send', { + method: opts.method, + url: server, + forceURLStream: opts.forceURLStream, + mimeType: 'application/octet-stream' + }, binary ); + }, + + getStatus: function() { + return this._status; + }, + + getResponse: function() { + return this._response || ''; + }, + + getResponseAsJson: function() { + return this._responseJson; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.exec('abort'); + xhr.destroy(); + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new RuntimeClient('XMLHttpRequest'); + + xhr.on( 'uploadprogress progress', function( e ) { + var percent = e.loaded / e.total; + percent = Math.min( 1, Math.max( 0, percent ) ); + return me.trigger( 'progress', percent ); + }); + + xhr.on( 'load', function() { + var status = xhr.exec('getStatus'), + readBody = false, + err = '', + p; + + xhr.off(); + me._xhr = null; + + if ( status >= 200 && status < 300 ) { + readBody = true; + } else if ( status >= 500 && status < 600 ) { + readBody = true; + err = 'server'; + } else { + err = 'http'; + } + + if ( readBody ) { + me._response = xhr.exec('getResponse'); + me._response = decodeURIComponent( me._response ); + + // flash 处理可能存在 bug, 没辙只能靠 js 了 + // try { + // me._responseJson = xhr.exec('getResponseAsJson'); + // } catch ( error ) { + + p = function( s ) { + try { + if (window.JSON && window.JSON.parse) { + return JSON.parse(s); + } + + return new Function('return ' + s).call(); + } catch ( err ) { + return {}; + } + }; + me._responseJson = me._response ? p(me._response) : {}; + + // } + } + + xhr.destroy(); + xhr = null; + + return err ? me.trigger( 'error', err ) : me.trigger('load'); + }); + + xhr.on( 'error', function() { + xhr.off(); + me._xhr = null; + me.trigger( 'error', 'http' ); + }); + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.exec( 'setRequestHeader', key, val ); + }); + } + }); + }); + + /** + * @fileOverview Blob Html实现 + */ + define('runtime/flash/blob',[ + 'runtime/flash/runtime', + 'lib/blob' + ], function( FlashRuntime, Blob ) { + + return FlashRuntime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.flashExec( 'Blob', 'slice', start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview Md5 flash实现 + */ + define('runtime/flash/md5',[ + 'runtime/flash/runtime' + ], function( FlashRuntime ) { + + return FlashRuntime.register( 'Md5', { + init: function() { + // do nothing. + }, + + loadFromBlob: function( blob ) { + return this.flashExec( 'Md5', 'loadFromBlob', blob.uid ); + } + }); + }); + /** + * @fileOverview 完全版本。 + */ + define('preset/all',[ + 'base', + + // widgets + 'widgets/filednd', + 'widgets/filepaste', + 'widgets/filepicker', + 'widgets/image', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + 'widgets/md5', + + // runtimes + // html5 + 'runtime/html5/blob', + 'runtime/html5/dnd', + 'runtime/html5/filepaste', + 'runtime/html5/filepicker', + 'runtime/html5/imagemeta/exif', + 'runtime/html5/androidpatch', + 'runtime/html5/image', + 'runtime/html5/transport', + 'runtime/html5/md5', + + // flash + 'runtime/flash/filepicker', + 'runtime/flash/image', + 'runtime/flash/transport', + 'runtime/flash/blob', + 'runtime/flash/md5' + ], function( Base ) { + return Base; + }); + /** + * @fileOverview 日志组件,主要用来收集错误信息,可以帮助 webuploader 更好的定位问题和发展。 + * + * 如果您不想要启用此功能,请在打包的时候去掉 log 模块。 + * + * 或者可以在初始化的时候通过 options.disableWidgets 属性禁用。 + * + * 如: + * WebUploader.create({ + * ... + * + * disableWidgets: 'log', + * + * ... + * }) + */ + define('widgets/log',[ + 'base', + 'uploader', + 'widgets/widget' + ], function( Base, Uploader ) { + var $ = Base.$, + logUrl = ' http://static.tieba.baidu.com/tb/pms/img/st.gif??', + product = (location.hostname || location.host || 'protected').toLowerCase(), + + // 只针对 baidu 内部产品用户做统计功能。 + enable = product && /baidu/i.exec(product), + base; + + if (!enable) { + return; + } + + base = { + dv: 3, + master: 'webuploader', + online: /test/.exec(product) ? 0 : 1, + module: '', + product: product, + type: 0 + }; + + function send(data) { + var obj = $.extend({}, base, data), + url = logUrl.replace(/^(.*)\?/, '$1' + $.param( obj )), + image = new Image(); + + image.src = url; + } + + return Uploader.register({ + name: 'log', + + init: function() { + var owner = this.owner, + count = 0, + size = 0; + + owner + .on('error', function(code) { + send({ + type: 2, + c_error_code: code + }); + }) + .on('uploadError', function(file, reason) { + send({ + type: 2, + c_error_code: 'UPLOAD_ERROR', + c_reason: '' + reason + }); + }) + .on('uploadComplete', function(file) { + count++; + size += file.size; + }). + on('uploadFinished', function() { + send({ + c_count: count, + c_size: size + }); + count = size = 0; + }); + + send({ + c_usage: 1 + }); + } + }); + }); + /** + * @fileOverview Uploader上传类 + */ + define('webuploader',[ + 'preset/all', + 'widgets/log' + ], function( preset ) { + return preset; + }); + return require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.min.js b/public/static/libs/webuploader/webuploader.min.js new file mode 100644 index 0000000..edaa0fa --- /dev/null +++ b/public/static/libs/webuploader/webuploader.min.js @@ -0,0 +1,3 @@ +/* WebUploader 0.1.6 */!function(a,b){var c,d={},e=function(a,b){var c,d,e;if("string"==typeof a)return h(a);for(c=[],d=a.length,e=0;d>e;e++)c.push(h(a[e]));return b.apply(null,c)},f=function(a,b,c){2===arguments.length&&(c=b,b=null),e(b||[],function(){g(a,c,arguments)})},g=function(a,b,c){var f,g={exports:b};"function"==typeof b&&(c.length||(c=[e,g.exports,g]),f=b.apply(null,c),void 0!==f&&(g.exports=f)),d[a]=g.exports},h=function(b){var c=d[b]||a[b];if(!c)throw new Error("`"+b+"` is undefined");return c},i=function(a){var b,c,e,f,g,h;h=function(a){return a&&a.charAt(0).toUpperCase()+a.substr(1)};for(b in d)if(c=a,d.hasOwnProperty(b)){for(e=b.split("/"),g=h(e.pop());f=h(e.shift());)c[f]=c[f]||{},c=c[f];c[g]=d[b]}return a},j=function(c){return a.__dollar=c,i(b(a,f,e))};"object"==typeof module&&"object"==typeof module.exports?module.exports=j():"function"==typeof define&&define.amd?define(["jquery"],j):(c=a.WebUploader,a.WebUploader=j(),a.WebUploader.noConflict=function(){a.WebUploader=c})}(window,function(a,b,c){return b("dollar-third",[],function(){var b=a.require,c=a.__dollar||a.jQuery||a.Zepto||b("jquery")||b("zepto");if(!c)throw new Error("jQuery or Zepto not found!");return c}),b("dollar",["dollar-third"],function(a){return a}),b("promise-third",["dollar"],function(a){return{Deferred:a.Deferred,when:a.when,isPromise:function(a){return a&&"function"==typeof a.then}}}),b("promise",["promise-third"],function(a){return a}),b("base",["dollar","promise"],function(b,c){function d(a){return function(){return h.apply(a,arguments)}}function e(a,b){return function(){return a.apply(b,arguments)}}function f(a){var b;return Object.create?Object.create(a):(b=function(){},b.prototype=a,new b)}var g=function(){},h=Function.call;return{version:"0.1.6",$:b,Deferred:c.Deferred,isPromise:c.isPromise,when:c.when,browser:function(a){var b={},c=a.match(/WebKit\/([\d.]+)/),d=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),e=a.match(/MSIE\s([\d\.]+)/)||a.match(/(?:trident)(?:.*rv:([\w.]+))?/i),f=a.match(/Firefox\/([\d.]+)/),g=a.match(/Safari\/([\d.]+)/),h=a.match(/OPR\/([\d.]+)/);return c&&(b.webkit=parseFloat(c[1])),d&&(b.chrome=parseFloat(d[1])),e&&(b.ie=parseFloat(e[1])),f&&(b.firefox=parseFloat(f[1])),g&&(b.safari=parseFloat(g[1])),h&&(b.opera=parseFloat(h[1])),b}(navigator.userAgent),os:function(a){var b={},c=a.match(/(?:Android);?[\s\/]+([\d.]+)?/),d=a.match(/(?:iPad|iPod|iPhone).*OS\s([\d_]+)/);return c&&(b.android=parseFloat(c[1])),d&&(b.ios=parseFloat(d[1].replace(/_/g,"."))),b}(navigator.userAgent),inherits:function(a,c,d){var e;return"function"==typeof c?(e=c,c=null):e=c&&c.hasOwnProperty("constructor")?c.constructor:function(){return a.apply(this,arguments)},b.extend(!0,e,a,d||{}),e.__super__=a.prototype,e.prototype=f(a.prototype),c&&b.extend(!0,e.prototype,c),e},noop:g,bindFn:e,log:function(){return a.console?e(console.log,console):g}(),nextTick:function(){return function(a){setTimeout(a,1)}}(),slice:d([].slice),guid:function(){var a=0;return function(b){for(var c=(+new Date).toString(32),d=0;5>d;d++)c+=Math.floor(65535*Math.random()).toString(32);return(b||"wu_")+c+(a++).toString(32)}}(),formatSize:function(a,b,c){var d;for(c=c||["B","K","M","G","TB"];(d=c.shift())&&a>1024;)a/=1024;return("B"===d?a:a.toFixed(b||2))+d}}}),b("mediator",["base"],function(a){function b(a,b,c,d){return f.grep(a,function(a){return!(!a||b&&a.e!==b||c&&a.cb!==c&&a.cb._cb!==c||d&&a.ctx!==d)})}function c(a,b,c){f.each((a||"").split(h),function(a,d){c(d,b)})}function d(a,b){for(var c,d=!1,e=-1,f=a.length;++e1?(d.isPlainObject(b)&&d.isPlainObject(c[a])?d.extend(c[a],b):c[a]=b,void 0):a?c[a]:c},getStats:function(){var a=this.request("get-stats");return a?{successNum:a.numOfSuccess,progressNum:a.numOfProgress,cancelNum:a.numOfCancel,invalidNum:a.numOfInvalid,uploadFailNum:a.numOfUploadFailed,queueNum:a.numOfQueue,interruptNum:a.numofInterrupt}:{}},trigger:function(a){var c=[].slice.call(arguments,1),e=this.options,f="on"+a.substring(0,1).toUpperCase()+a.substring(1);return b.trigger.apply(this,arguments)===!1||d.isFunction(e[f])&&e[f].apply(this,c)===!1||d.isFunction(this[f])&&this[f].apply(this,c)===!1||b.trigger.apply(b,[this,a].concat(c))===!1?!1:!0},destroy:function(){this.request("destroy",arguments),this.off()},request:a.noop}),a.create=c.create=function(a){return new c(a)},a.Uploader=c,c}),b("runtime/runtime",["base","mediator"],function(a,b){function c(b){this.options=d.extend({container:document.body},b),this.uid=a.guid("rt_")}var d=a.$,e={},f=function(a){for(var b in a)if(a.hasOwnProperty(b))return b;return null};return d.extend(c.prototype,{getContainer:function(){var a,b,c=this.options;return this._container?this._container:(a=d(c.container||document.body),b=d(document.createElement("div")),b.attr("id","rt_"+this.uid),b.css({position:"absolute",top:"0px",left:"0px",width:"1px",height:"1px",overflow:"hidden"}),a.append(b),a.addClass("webuploader-container"),this._container=b,this._parent=a,b)},init:a.noop,exec:a.noop,destroy:function(){this._container&&this._container.remove(),this._parent&&this._parent.removeClass("webuploader-container"),this.off()}}),c.orders="html5,flash",c.addRuntime=function(a,b){e[a]=b},c.hasRuntime=function(a){return!!(a?e[a]:f(e))},c.create=function(a,b){var g,h;if(b=b||c.orders,d.each(b.split(/\s*,\s*/g),function(){return e[this]?(g=this,!1):void 0}),g=g||f(e),!g)throw new Error("Runtime Error");return h=new e[g](a)},b.installTo(c.prototype),c}),b("runtime/client",["base","mediator","runtime/runtime"],function(a,b,c){function d(b,d){var f,g=a.Deferred();this.uid=a.guid("client_"),this.runtimeReady=function(a){return g.done(a)},this.connectRuntime=function(b,h){if(f)throw new Error("already connected!");return g.done(h),"string"==typeof b&&e.get(b)&&(f=e.get(b)),f=f||e.get(null,d),f?(a.$.extend(f.options,b),f.__promise.then(g.resolve),f.__client++):(f=c.create(b,b.runtimeOrder),f.__promise=g.promise(),f.once("ready",g.resolve),f.init(),e.add(f),f.__client=1),d&&(f.__standalone=d),f},this.getRuntime=function(){return f},this.disconnectRuntime=function(){f&&(f.__client--,f.__client<=0&&(e.remove(f),delete f.__promise,f.destroy()),f=null)},this.exec=function(){if(f){var c=a.slice(arguments);return b&&c.unshift(b),f.exec.apply(this,c)}},this.getRuid=function(){return f&&f.uid},this.destroy=function(a){return function(){a&&a.apply(this,arguments),this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()}}(this.destroy)}var e;return e=function(){var a={};return{add:function(b){a[b.uid]=b},get:function(b,c){var d;if(b)return a[b];for(d in a)if(!c||!a[d].__standalone)return a[d];return null},remove:function(b){delete a[b.uid]}}}(),b.installTo(d.prototype),d}),b("lib/dnd",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},d.options,a),a.container=e(a.container),a.container.length&&c.call(this,"DragAndDrop")}var e=a.$;return d.options={accept:null,disableGlobalDnd:!1},a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/widget",["base","uploader"],function(a,b){function c(a){if(!a)return!1;var b=a.length,c=e.type(a);return 1===a.nodeType&&b?!0:"array"===c||"function"!==c&&"string"!==c&&(0===b||"number"==typeof b&&b>0&&b-1 in a)}function d(a){this.owner=a,this.options=a.options}var e=a.$,f=b.prototype._init,g=b.prototype.destroy,h={},i=[];return e.extend(d.prototype,{init:a.noop,invoke:function(a,b){var c=this.responseMap;return c&&a in c&&c[a]in this&&e.isFunction(this[c[a]])?this[c[a]].apply(this,b):h},request:function(){return this.owner.request.apply(this.owner,arguments)}}),e.extend(b.prototype,{_init:function(){var a=this,b=a._widgets=[],c=a.options.disableWidgets||"";return e.each(i,function(d,e){(!c||!~c.indexOf(e._name))&&b.push(new e(a))}),f.apply(a,arguments)},request:function(b,d,e){var f,g,i,j,k=0,l=this._widgets,m=l&&l.length,n=[],o=[];for(d=c(d)?d:[d];m>k;k++)f=l[k],g=f.invoke(b,d),g!==h&&(a.isPromise(g)?o.push(g):n.push(g));return e||o.length?(i=a.when.apply(a,o),j=i.pipe?"pipe":"then",i[j](function(){var b=a.Deferred(),c=arguments;return 1===c.length&&(c=c[0]),setTimeout(function(){b.resolve(c)},1),b.promise()})[e?j:"done"](e||a.noop)):n[0]},destroy:function(){g.apply(this,arguments),this._widgets=null}}),b.register=d.register=function(b,c){var f,g={init:"init",destroy:"destroy",name:"anonymous"};return 1===arguments.length?(c=b,e.each(c,function(a){return"_"===a[0]||"name"===a?("name"===a&&(g.name=c.name),void 0):(g[a.replace(/[A-Z]/g,"-$&").toLowerCase()]=a,void 0)})):g=e.extend(g,b),c.responseMap=g,f=a.inherits(d,c),f._name=g.name,i.push(f),f},b.unRegister=d.unRegister=function(a){if(a&&"anonymous"!==a)for(var b=i.length;b--;)i[b]._name===a&&i.splice(b,1)},d}),b("widgets/filednd",["base","uploader","lib/dnd","widgets/widget"],function(a,b,c){var d=a.$;return b.options.dnd="",b.register({name:"dnd",init:function(b){if(b.dnd&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{disableGlobalDnd:b.disableGlobalDnd,container:b.dnd,accept:b.accept});return this.dnd=e=new c(h),e.once("ready",g.resolve),e.on("drop",function(a){f.request("add-file",[a])}),e.on("accept",function(a){return f.owner.trigger("dndAccept",a)}),e.init(),g.promise()}},destroy:function(){this.dnd&&this.dnd.destroy()}})}),b("lib/filepaste",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},a),a.container=e(a.container||document.body),c.call(this,"FilePaste")}var e=a.$;return a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/filepaste",["base","uploader","lib/filepaste","widgets/widget"],function(a,b,c){var d=a.$;return b.register({name:"paste",init:function(b){if(b.paste&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{container:b.paste,accept:b.accept});return this.paste=e=new c(h),e.once("ready",g.resolve),e.on("paste",function(a){f.owner.request("add-file",[a])}),e.init(),g.promise()}},destroy:function(){this.paste&&this.paste.destroy()}})}),b("lib/blob",["base","runtime/client"],function(a,b){function c(a,c){var d=this;d.source=c,d.ruid=a,this.size=c.size||0,this.type=!c.type&&this.ext&&~"jpg,jpeg,png,gif,bmp".indexOf(this.ext)?"image/"+("jpg"===this.ext?"jpeg":this.ext):c.type||"application/octet-stream",b.call(d,"Blob"),this.uid=c.uid||this.uid,a&&d.connectRuntime(a)}return a.inherits(b,{constructor:c,slice:function(a,b){return this.exec("slice",a,b)},getSource:function(){return this.source}}),c}),b("lib/file",["base","lib/blob"],function(a,b){function c(a,c){var f;this.name=c.name||"untitled"+d++,f=e.exec(c.name)?RegExp.$1.toLowerCase():"",!f&&c.type&&(f=/\/(jpg|jpeg|png|gif|bmp)$/i.exec(c.type)?RegExp.$1.toLowerCase():"",this.name+="."+f),this.ext=f,this.lastModifiedDate=c.lastModifiedDate||(new Date).toLocaleString(),b.apply(this,arguments)}var d=1,e=/\.([^.]+)$/;return a.inherits(b,c)}),b("lib/filepicker",["base","runtime/client","lib/file"],function(b,c,d){function e(a){if(a=this.options=f.extend({},e.options,a),a.container=f(a.id),!a.container.length)throw new Error("按钮指定错误");a.innerHTML=a.innerHTML||a.label||a.container.html()||"",a.button=f(a.button||document.createElement("div")),a.button.html(a.innerHTML),a.container.html(a.button),c.call(this,"FilePicker",!0)}var f=b.$;return e.options={button:null,container:null,label:null,innerHTML:null,multiple:!0,accept:null,name:"file",style:"webuploader-pick"},b.inherits(c,{constructor:e,init:function(){var c=this,e=c.options,g=e.button,h=e.style;h&&g.addClass("webuploader-pick"),c.on("all",function(a){var b;switch(a){case"mouseenter":h&&g.addClass("webuploader-pick-hover");break;case"mouseleave":h&&g.removeClass("webuploader-pick-hover");break;case"change":b=c.exec("getFiles"),c.trigger("select",f.map(b,function(a){return a=new d(c.getRuid(),a),a._refer=e.container,a}),e.container)}}),c.connectRuntime(e,function(){c.refresh(),c.exec("init",e),c.trigger("ready")}),this._resizeHandler=b.bindFn(this.refresh,this),f(a).on("resize",this._resizeHandler)},refresh:function(){var a=this.getRuntime().getContainer(),b=this.options.button,c=b.outerWidth?b.outerWidth():b.width(),d=b.outerHeight?b.outerHeight():b.height(),e=b.offset();c&&d&&a.css({bottom:"auto",right:"auto",width:c+"px",height:d+"px"}).offset(e)},enable:function(){var a=this.options.button;a.removeClass("webuploader-pick-disable"),this.refresh()},disable:function(){var a=this.options.button;this.getRuntime().getContainer().css({top:"-99999px"}),a.addClass("webuploader-pick-disable")},destroy:function(){var b=this.options.button;f(a).off("resize",this._resizeHandler),b.removeClass("webuploader-pick-disable webuploader-pick-hover webuploader-pick")}}),e}),b("widgets/filepicker",["base","uploader","lib/filepicker","widgets/widget"],function(a,b,c){var d=a.$;return d.extend(b.options,{pick:null,accept:null}),b.register({name:"picker",init:function(a){return this.pickers=[],a.pick&&this.addBtn(a.pick)},refresh:function(){d.each(this.pickers,function(){this.refresh()})},addBtn:function(b){var e=this,f=e.options,g=f.accept,h=[];if(b)return d.isPlainObject(b)||(b={id:b}),d(b.id).each(function(){var i,j,k;k=a.Deferred(),i=d.extend({},b,{accept:d.isPlainObject(g)?[g]:g,swf:f.swf,runtimeOrder:f.runtimeOrder,id:this}),j=new c(i),j.once("ready",k.resolve),j.on("select",function(a){e.owner.request("add-file",[a])}),j.on("dialogopen",function(){e.owner.trigger("dialogOpen",j.button)}),j.init(),e.pickers.push(j),h.push(k.promise())}),a.when.apply(a,h)},disable:function(){d.each(this.pickers,function(){this.disable()})},enable:function(){d.each(this.pickers,function(){this.enable()})},destroy:function(){d.each(this.pickers,function(){this.destroy()}),this.pickers=null}})}),b("lib/image",["base","runtime/client","lib/blob"],function(a,b,c){function d(a){this.options=e.extend({},d.options,a),b.call(this,"Image"),this.on("load",function(){this._info=this.exec("info"),this._meta=this.exec("meta")})}var e=a.$;return d.options={quality:90,crop:!1,preserveHeaders:!1,allowMagnify:!1},a.inherits(b,{constructor:d,info:function(a){return a?(this._info=a,this):this._info},meta:function(a){return a?(this._meta=a,this):this._meta},loadFromBlob:function(a){var b=this,c=a.getRuid();this.connectRuntime(c,function(){b.exec("init",b.options),b.exec("loadFromBlob",a)})},resize:function(){var b=a.slice(arguments);return this.exec.apply(this,["resize"].concat(b))},crop:function(){var b=a.slice(arguments);return this.exec.apply(this,["crop"].concat(b))},getAsDataUrl:function(a){return this.exec("getAsDataUrl",a)},getAsBlob:function(a){var b=this.exec("getAsBlob",a);return new c(this.getRuid(),b)}}),d}),b("widgets/image",["base","uploader","lib/image","widgets/widget"],function(a,b,c){var d,e=a.$;return d=function(a){var b=0,c=[],d=function(){for(var d;c.length&&a>b;)d=c.shift(),b+=d[0],d[1]()};return function(a,e,f){c.push([e,f]),a.once("destroy",function(){b-=e,setTimeout(d,1)}),setTimeout(d,1)}}(5242880),e.extend(b.options,{thumb:{width:110,height:110,quality:70,allowMagnify:!0,crop:!0,preserveHeaders:!1,type:"image/jpeg"},compress:{width:1600,height:1600,quality:90,allowMagnify:!1,crop:!1,preserveHeaders:!0}}),b.register({name:"image",makeThumb:function(a,b,f,g){var h,i;return a=this.request("get-file",a),a.type.match(/^image/)?(h=e.extend({},this.options.thumb),e.isPlainObject(f)&&(h=e.extend(h,f),f=null),f=f||h.width,g=g||h.height,i=new c(h),i.once("load",function(){a._info=a._info||i.info(),a._meta=a._meta||i.meta(),1>=f&&f>0&&(f=a._info.width*f),1>=g&&g>0&&(g=a._info.height*g),i.resize(f,g)}),i.once("complete",function(){b(!1,i.getAsDataUrl(h.type)),i.destroy()}),i.once("error",function(a){b(a||!0),i.destroy()}),d(i,a.source.size,function(){a._info&&i.info(a._info),a._meta&&i.meta(a._meta),i.loadFromBlob(a.source)}),void 0):(b(!0),void 0)},beforeSendFile:function(b){var d,f,g=this.options.compress||this.options.resize,h=g&&g.compressSize||0,i=g&&g.noCompressIfLarger||!1;return b=this.request("get-file",b),!g||!~"image/jpeg,image/jpg".indexOf(b.type)||b.size=a&&a>0&&(a=b._info.width*a),1>=c&&c>0&&(c=b._info.height*c),d.resize(a,c)}),d.once("complete",function(){var a,c;try{a=d.getAsBlob(g.type),c=b.size,(!i||a.sizeb;b++)if(c=this._queue[b],a===c.getStatus())return c;return null},sort:function(a){"function"==typeof a&&this._queue.sort(a)},getFiles:function(){for(var a,b=[].slice.call(arguments,0),c=[],d=0,f=this._queue.length;f>d;d++)a=this._queue[d],(!b.length||~e.inArray(a.getStatus(),b))&&c.push(a);return c},removeFile:function(a){var b=this._map[a.id];b&&(delete this._map[a.id],a.destroy(),this.stats.numofDeleted++)},_fileAdded:function(a){var b=this,c=this._map[a.id];c||(this._map[a.id]=a,a.on("statuschange",function(a,c){b._onFileStatusChange(a,c)}))},_onFileStatusChange:function(a,b){var c=this.stats;switch(b){case f.PROGRESS:c.numOfProgress--;break;case f.QUEUED:c.numOfQueue--;break;case f.ERROR:c.numOfUploadFailed--;break;case f.INVALID:c.numOfInvalid--;break;case f.INTERRUPT:c.numofInterrupt--}switch(a){case f.QUEUED:c.numOfQueue++;break;case f.PROGRESS:c.numOfProgress++;break;case f.ERROR:c.numOfUploadFailed++;break;case f.COMPLETE:c.numOfSuccess++;break;case f.CANCELLED:c.numOfCancel++;break;case f.INVALID:c.numOfInvalid++;break;case f.INTERRUPT:c.numofInterrupt++}}}),b.installTo(d.prototype),d}),b("widgets/queue",["base","uploader","queue","file","lib/file","runtime/client","widgets/widget"],function(a,b,c,d,e,f){var g=a.$,h=/\.\w+$/,i=d.Status;return b.register({name:"queue",init:function(b){var d,e,h,i,j,k,l,m=this;if(g.isPlainObject(b.accept)&&(b.accept=[b.accept]),b.accept){for(j=[],h=0,e=b.accept.length;e>h;h++)i=b.accept[h].extensions,i&&j.push(i);j.length&&(k="\\."+j.join(",").replace(/,/g,"$|\\.").replace(/\*/g,".*")+"$"),m.accept=new RegExp(k,"i")}return m.queue=new c,m.stats=m.queue.stats,"html5"===this.request("predict-runtime-type")?(d=a.Deferred(),this.placeholder=l=new f("Placeholder"),l.connectRuntime({runtimeOrder:"html5"},function(){m._ruid=l.getRuid(),d.resolve()}),d.promise()):void 0},_wrapFile:function(a){if(!(a instanceof d)){if(!(a instanceof e)){if(!this._ruid)throw new Error("Can't add external files.");a=new e(this._ruid,a)}a=new d(a)}return a},acceptFile:function(a){var b=!a||!a.size||this.accept&&h.exec(a.name)&&!this.accept.test(a.name);return!b},_addFile:function(a){var b=this;return a=b._wrapFile(a),b.owner.trigger("beforeFileQueued",a)?b.acceptFile(a)?(b.queue.append(a),b.owner.trigger("fileQueued",a),a):(b.owner.trigger("error","Q_TYPE_DENIED",a),void 0):void 0},getFile:function(a){return this.queue.getFile(a)},addFile:function(a){var b=this;a.length||(a=[a]),a=g.map(a,function(a){return b._addFile(a)}),a.length&&(b.owner.trigger("filesQueued",a),b.options.auto&&setTimeout(function(){b.request("start-upload")},20))},getStats:function(){return this.stats},removeFile:function(a,b){var c=this;a=a.id?a:c.queue.getFile(a),this.request("cancel-file",a),b&&this.queue.removeFile(a)},getFiles:function(){return this.queue.getFiles.apply(this.queue,arguments)},fetchFile:function(){return this.queue.fetch.apply(this.queue,arguments)},retry:function(a,b){var c,d,e,f=this;if(a)return a=a.id?a:f.queue.getFile(a),a.setStatus(i.QUEUED),b||f.request("start-upload"),void 0;for(c=f.queue.getFiles(i.ERROR),d=0,e=c.length;e>d;d++)a=c[d],a.setStatus(i.QUEUED);f.request("start-upload")},sortFiles:function(){return this.queue.sort.apply(this.queue,arguments)},reset:function(){this.owner.trigger("reset"),this.queue=new c,this.stats=this.queue.stats},destroy:function(){this.reset(),this.placeholder&&this.placeholder.destroy()}})}),b("widgets/runtime",["uploader","runtime/runtime","widgets/widget"],function(a,b){return a.support=function(){return b.hasRuntime.apply(b,arguments)},a.register({name:"runtime",init:function(){if(!this.predictRuntimeType())throw Error("Runtime Error")},predictRuntimeType:function(){var a,c,d=this.options.runtimeOrder||b.orders,e=this.type;if(!e)for(d=d.split(/\s*,\s*/g),a=0,c=d.length;c>a;a++)if(b.hasRuntime(d[a])){this.type=e=d[a];break}return e}})}),b("lib/transport",["base","runtime/client","mediator"],function(a,b,c){function d(a){var c=this;a=c.options=e.extend(!0,{},d.options,a||{}),b.call(this,"Transport"),this._blob=null,this._formData=a.formData||{},this._headers=a.headers||{},this.on("progress",this._timeout),this.on("load error",function(){c.trigger("progress",1),clearTimeout(c._timer)})}var e=a.$;return d.options={server:"",method:"POST",withCredentials:!1,fileVal:"file",timeout:12e4,formData:{},headers:{},sendAsBinary:!1},e.extend(d.prototype,{appendBlob:function(a,b,c){var d=this,e=d.options;d.getRuid()&&d.disconnectRuntime(),d.connectRuntime(b.ruid,function(){d.exec("init")}),d._blob=b,e.fileVal=a||e.fileVal,e.filename=c||e.filename},append:function(a,b){"object"==typeof a?e.extend(this._formData,a):this._formData[a]=b},setRequestHeader:function(a,b){"object"==typeof a?e.extend(this._headers,a):this._headers[a]=b},send:function(a){this.exec("send",a),this._timeout()},abort:function(){return clearTimeout(this._timer),this.exec("abort")},destroy:function(){this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()},getResponse:function(){return this.exec("getResponse")},getResponseAsJson:function(){return this.exec("getResponseAsJson")},getStatus:function(){return this.exec("getStatus")},_timeout:function(){var a=this,b=a.options.timeout;b&&(clearTimeout(a._timer),a._timer=setTimeout(function(){a.abort(),a.trigger("error","timeout")},b))}}),c.installTo(d.prototype),d}),b("widgets/upload",["base","uploader","file","lib/transport","widgets/widget"],function(a,b,c,d){function e(a,b){var c,d,e=[],f=a.source,g=f.size,h=b?Math.ceil(g/b):1,i=0,j=0;for(d={file:a,has:function(){return!!e.length},shift:function(){return e.shift()},unshift:function(a){e.unshift(a)}};h>j;)c=Math.min(b,g-i),e.push({file:a,start:i,end:b?i+c:g,total:g,chunks:h,chunk:j++,cuted:d}),i+=c;return a.blocks=e.concat(),a.remaning=e.length,d}var f=a.$,g=a.isPromise,h=c.Status;f.extend(b.options,{prepareNextFile:!1,chunked:!1,chunkSize:5242880,chunkRetry:2,threads:3,formData:{}}),b.register({name:"upload",init:function(){var b=this.owner,c=this;this.runing=!1,this.progress=!1,b.on("startUpload",function(){c.progress=!0}).on("uploadFinished",function(){c.progress=!1}),this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this.__tick=a.bindFn(this._tick,this),b.on("uploadComplete",function(a){a.blocks&&f.each(a.blocks,function(a,b){b.transport&&(b.transport.abort(),b.transport.destroy()),delete b.transport}),delete a.blocks,delete a.remaning})},reset:function(){this.request("stop-upload",!0),this.runing=!1,this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this._trigged=!1,this._promise=null},startUpload:function(b){var c=this;if(f.each(c.request("get-files",h.INVALID),function(){c.request("remove-file",this)}),b?(b=b.id?b:c.request("get-file",b),b.getStatus()===h.INTERRUPT?(b.setStatus(h.QUEUED),f.each(c.pool,function(a,c){c.file===b&&(c.transport&&c.transport.send(),b.setStatus(h.PROGRESS))})):b.getStatus()!==h.PROGRESS&&b.setStatus(h.QUEUED)):f.each(c.request("get-files",[h.INITED]),function(){this.setStatus(h.QUEUED)}),c.runing)return a.nextTick(c.__tick);c.runing=!0;var d=[];b||f.each(c.pool,function(a,b){var e=b.file;e.getStatus()===h.INTERRUPT&&(c._trigged=!1,d.push(e),b.transport&&b.transport.send())}),f.each(d,function(){this.setStatus(h.PROGRESS)}),b||f.each(c.request("get-files",h.INTERRUPT),function(){this.setStatus(h.PROGRESS)}),c._trigged=!1,a.nextTick(c.__tick),c.owner.trigger("startUpload")},stopUpload:function(b,c){var d,e=this;if(b===!0&&(c=b,b=null),e.runing!==!1){if(b){if(b=b.id?b:e.request("get-file",b),b.getStatus()!==h.PROGRESS&&b.getStatus()!==h.QUEUED)return;return b.setStatus(h.INTERRUPT),f.each(e.pool,function(a,c){return c.file===b?(d=c,!1):void 0}),d.transport&&d.transport.abort(),c&&(e._putback(d),e._popBlock(d)),a.nextTick(e.__tick)}e.runing=!1,this._promise&&this._promise.file&&this._promise.file.setStatus(h.INTERRUPT),c&&f.each(e.pool,function(a,b){b.transport&&b.transport.abort(),b.file.setStatus(h.INTERRUPT)}),e.owner.trigger("stopUpload")}},cancelFile:function(a){a=a.id?a:this.request("get-file",a),a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),a.setStatus(h.CANCELLED),this.owner.trigger("fileDequeued",a)},isInProgress:function(){return!!this.progress},_getStats:function(){return this.request("get-stats")},skipFile:function(a,b){a=a.id?a:this.request("get-file",a),a.setStatus(b||h.COMPLETE),a.skipped=!0,a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),this.owner.trigger("uploadSkip",a)},_tick:function(){var b,c,d=this,e=d.options;return d._promise?d._promise.always(d.__tick):(d.pool.length1&&~"http,abort".indexOf(a)&&b.retried1&&f.extend(m,{chunks:b.chunks,chunk:b.chunk}),i.trigger("uploadBeforeSend",b,m,n),l.appendBlob(j.fileVal,b.blob,k.name),l.append(m),l.setRequestHeader(n),l.send() +},_finishFile:function(a,b,c){var d=this.owner;return d.request("after-send-file",arguments,function(){a.setStatus(h.COMPLETE),d.trigger("uploadSuccess",a,b,c)}).fail(function(b){a.getStatus()===h.PROGRESS&&a.setStatus(h.ERROR,b),d.trigger("uploadError",a,b)}).always(function(){d.trigger("uploadComplete",a)})},updateFileProgress:function(a){var b=0,c=0;a.blocks&&(f.each(a.blocks,function(a,b){c+=(b.percentage||0)*(b.end-b.start)}),b=c/a.size,this.owner.trigger("uploadProgress",a,b||0))}})}),b("widgets/validator",["base","uploader","file","widgets/widget"],function(a,b,c){var d,e=a.$,f={};return d={addValidator:function(a,b){f[a]=b},removeValidator:function(a){delete f[a]}},b.register({name:"validator",init:function(){var b=this;a.nextTick(function(){e.each(f,function(){this.call(b.owner)})})}}),d.addValidator("fileNumLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileNumLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){return c>=d&&e&&(e=!1,this.trigger("error","Q_EXCEED_NUM_LIMIT",d,a),setTimeout(function(){e=!0},1)),c>=d?!1:!0}),a.on("fileQueued",function(){c++}),a.on("fileDequeued",function(){c--}),a.on("reset",function(){c=0}))}),d.addValidator("fileSizeLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileSizeLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){var b=c+a.size>d;return b&&e&&(e=!1,this.trigger("error","Q_EXCEED_SIZE_LIMIT",d,a),setTimeout(function(){e=!0},1)),b?!1:!0}),a.on("fileQueued",function(a){c+=a.size}),a.on("fileDequeued",function(a){c-=a.size}),a.on("reset",function(){c=0}))}),d.addValidator("fileSingleSizeLimit",function(){var a=this,b=a.options,d=b.fileSingleSizeLimit;d&&a.on("beforeFileQueued",function(a){return a.size>d?(a.setStatus(c.Status.INVALID,"exceed_size"),this.trigger("error","F_EXCEED_SIZE",d,a),!1):void 0})}),d.addValidator("duplicate",function(){function a(a){for(var b,c=0,d=0,e=a.length;e>d;d++)b=a.charCodeAt(d),c=b+(c<<6)+(c<<16)-c;return c}var b=this,c=b.options,d={};c.duplicate||(b.on("beforeFileQueued",function(b){var c=b.__hash||(b.__hash=a(b.name+b.size+b.lastModifiedDate));return d[c]?(this.trigger("error","F_DUPLICATE",b),!1):void 0}),b.on("fileQueued",function(a){var b=a.__hash;b&&(d[b]=!0)}),b.on("fileDequeued",function(a){var b=a.__hash;b&&delete d[b]}),b.on("reset",function(){d={}}))}),d}),b("lib/md5",["runtime/client","mediator"],function(a,b){function c(){a.call(this,"Md5")}return b.installTo(c.prototype),c.prototype.loadFromBlob=function(a){var b=this;b.getRuid()&&b.disconnectRuntime(),b.connectRuntime(a.ruid,function(){b.exec("init"),b.exec("loadFromBlob",a)})},c.prototype.getResult=function(){return this.exec("getResult")},c}),b("widgets/md5",["base","uploader","lib/md5","lib/blob","widgets/widget"],function(a,b,c,d){return b.register({name:"md5",md5File:function(b,e,f){var g=new c,h=a.Deferred(),i=b instanceof d?b:this.request("get-file",b).source;return g.on("progress load",function(a){a=a||{},h.notify(a.total?a.loaded/a.total:1)}),g.on("complete",function(){h.resolve(g.getResult())}),g.on("error",function(a){h.reject(a)}),arguments.length>1&&(e=e||0,f=f||0,0>e&&(e=i.size+e),0>f&&(f=i.size+f),f=Math.min(f,i.size),i=i.slice(e,f)),g.loadFromBlob(i),h.promise()}})}),b("runtime/compbase",[],function(){function a(a,b){this.owner=a,this.options=a.options,this.getRuntime=function(){return b},this.getRuid=function(){return b.uid},this.trigger=function(){return a.trigger.apply(a,arguments)}}return a}),b("runtime/html5/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a={},d=this,e=this.destroy;c.apply(d,arguments),d.type=f,d.exec=function(c,e){var f,h=this,i=h.uid,j=b.slice(arguments,2);return g[c]&&(f=a[i]=a[i]||new g[c](h,d),f[e])?f[e].apply(f,j):void 0},d.destroy=function(){return e&&e.apply(this,arguments)}}var f="html5",g={};return b.inherits(c,{constructor:e,init:function(){var a=this;setTimeout(function(){a.trigger("ready")},1)}}),e.register=function(a,c){var e=g[a]=b.inherits(d,c);return e},a.Blob&&a.FileReader&&a.DataView&&c.addRuntime(f,e),e}),b("runtime/html5/blob",["runtime/html5/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.owner.source,e=d.slice||d.webkitSlice||d.mozSlice;return d=e.call(d,a,c),new b(this.getRuid(),d)}})}),b("runtime/html5/dnd",["base","runtime/html5/runtime","lib/file"],function(a,b,c){var d=a.$,e="webuploader-dnd-";return b.register("DragAndDrop",{init:function(){var b=this.elem=this.options.container;this.dragEnterHandler=a.bindFn(this._dragEnterHandler,this),this.dragOverHandler=a.bindFn(this._dragOverHandler,this),this.dragLeaveHandler=a.bindFn(this._dragLeaveHandler,this),this.dropHandler=a.bindFn(this._dropHandler,this),this.dndOver=!1,b.on("dragenter",this.dragEnterHandler),b.on("dragover",this.dragOverHandler),b.on("dragleave",this.dragLeaveHandler),b.on("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).on("dragover",this.dragOverHandler),d(document).on("drop",this.dropHandler))},_dragEnterHandler:function(a){var b,c=this,d=c._denied||!1;return a=a.originalEvent||a,c.dndOver||(c.dndOver=!0,b=a.dataTransfer.items,b&&b.length&&(c._denied=d=!c.trigger("accept",b)),c.elem.addClass(e+"over"),c.elem[d?"addClass":"removeClass"](e+"denied")),a.dataTransfer.dropEffect=d?"none":"copy",!1},_dragOverHandler:function(a){var b=this.elem.parent().get(0);return b&&!d.contains(b,a.currentTarget)?!1:(clearTimeout(this._leaveTimer),this._dragEnterHandler.call(this,a),!1)},_dragLeaveHandler:function(){var a,b=this;return a=function(){b.dndOver=!1,b.elem.removeClass(e+"over "+e+"denied")},clearTimeout(b._leaveTimer),b._leaveTimer=setTimeout(a,100),!1},_dropHandler:function(a){var b,f,g=this,h=g.getRuid(),i=g.elem.parent().get(0);if(i&&!d.contains(i,a.currentTarget))return!1;a=a.originalEvent||a,b=a.dataTransfer;try{f=b.getData("text/html")}catch(j){}return g.dndOver=!1,g.elem.removeClass(e+"over"),f?void 0:(g._getTansferFiles(b,function(a){g.trigger("drop",d.map(a,function(a){return new c(h,a)}))}),!1)},_getTansferFiles:function(b,c){var d,e,f,g,h,i,j,k=[],l=[];for(d=b.items,e=b.files,j=!(!d||!d[0].webkitGetAsEntry),h=0,i=e.length;i>h;h++)f=e[h],g=d&&d[h],j&&g.webkitGetAsEntry().isDirectory?l.push(this._traverseDirectoryTree(g.webkitGetAsEntry(),k)):k.push(f);a.when.apply(a,l).done(function(){k.length&&c(k)})},_traverseDirectoryTree:function(b,c){var d=a.Deferred(),e=this;return b.isFile?b.file(function(a){c.push(a),d.resolve()}):b.isDirectory&&b.createReader().readEntries(function(b){var f,g=b.length,h=[],i=[];for(f=0;g>f;f++)h.push(e._traverseDirectoryTree(b[f],i));a.when.apply(a,h).then(function(){c.push.apply(c,i),d.resolve()},d.reject)}),d.promise()},destroy:function(){var a=this.elem;a&&(a.off("dragenter",this.dragEnterHandler),a.off("dragover",this.dragOverHandler),a.off("dragleave",this.dragLeaveHandler),a.off("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).off("dragover",this.dragOverHandler),d(document).off("drop",this.dropHandler)))}})}),b("runtime/html5/filepaste",["base","runtime/html5/runtime","lib/file"],function(a,b,c){return b.register("FilePaste",{init:function(){var b,c,d,e,f=this.options,g=this.elem=f.container,h=".*";if(f.accept){for(b=[],c=0,d=f.accept.length;d>c;c++)e=f.accept[c].mimeTypes,e&&b.push(e);b.length&&(h=b.join(","),h=h.replace(/,/g,"|").replace(/\*/g,".*"))}this.accept=h=new RegExp(h,"i"),this.hander=a.bindFn(this._pasteHander,this),g.on("paste",this.hander)},_pasteHander:function(a){var b,d,e,f,g,h=[],i=this.getRuid();for(a=a.originalEvent||a,b=a.clipboardData.items,f=0,g=b.length;g>f;f++)d=b[f],"file"===d.kind&&(e=d.getAsFile())&&h.push(new c(i,e));h.length&&(a.preventDefault(),a.stopPropagation(),this.trigger("paste",h))},destroy:function(){this.elem.off("paste",this.hander)}})}),b("runtime/html5/filepicker",["base","runtime/html5/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(){var a,b,d,e,f=this.getRuntime().getContainer(),g=this,h=g.owner,i=g.options,j=this.label=c(document.createElement("label")),k=this.input=c(document.createElement("input"));if(k.attr("type","file"),k.attr("capture","camera"),k.attr("name",i.name),k.addClass("webuploader-element-invisible"),j.on("click",function(a){k.trigger("click"),a.stopPropagation(),h.trigger("dialogopen")}),j.css({opacity:0,width:"100%",height:"100%",display:"block",cursor:"pointer",background:"#ffffff"}),i.multiple&&k.attr("multiple","multiple"),i.accept&&i.accept.length>0){for(a=[],b=0,d=i.accept.length;d>b;b++)a.push(i.accept[b].mimeTypes);k.attr("accept",a.join(","))}f.append(k),f.append(j),e=function(a){h.trigger(a.type)},k.on("change",function(a){var b,d=arguments.callee;g.files=a.target.files,b=this.cloneNode(!0),b.value=null,this.parentNode.replaceChild(b,this),k.off(),k=c(b).on("change",d).on("mouseenter mouseleave",e),h.trigger("change")}),j.on("mouseenter mouseleave",e)},getFiles:function(){return this.files},destroy:function(){this.input.off(),this.label.off()}})}),b("runtime/html5/util",["base"],function(b){var c=a.createObjectURL&&a||a.URL&&URL.revokeObjectURL&&URL||a.webkitURL,d=b.noop,e=d;return c&&(d=function(){return c.createObjectURL.apply(c,arguments)},e=function(){return c.revokeObjectURL.apply(c,arguments)}),{createObjectURL:d,revokeObjectURL:e,dataURL2Blob:function(a){var b,c,d,e,f,g;for(g=a.split(","),b=~g[0].indexOf("base64")?atob(g[1]):decodeURIComponent(g[1]),d=new ArrayBuffer(b.length),c=new Uint8Array(d),e=0;ei&&(d=h.getUint16(i),d>=65504&&65519>=d||65534===d)&&(e=h.getUint16(i+2)+2,!(i+e>h.byteLength));){if(f=b.parsers[d],!c&&f)for(g=0;g6&&(l.imageHead=a.slice?a.slice(2,k):new Uint8Array(a).subarray(2,k))}return l}},updateImageHead:function(a,b){var c,d,e,f=this._parse(a,!0);return e=2,f.imageHead&&(e=2+f.imageHead.byteLength),d=a.slice?a.slice(e):new Uint8Array(a).subarray(e),c=new Uint8Array(b.byteLength+2+d.byteLength),c[0]=255,c[1]=216,c.set(new Uint8Array(b),2),c.set(new Uint8Array(d),b.byteLength+2),c.buffer}},a.parseMeta=function(){return b.parse.apply(b,arguments)},a.updateImageHead=function(){return b.updateImageHead.apply(b,arguments)},b}),b("runtime/html5/imagemeta/exif",["base","runtime/html5/imagemeta"],function(a,b){var c={};return c.ExifMap=function(){return this},c.ExifMap.prototype.map={Orientation:274},c.ExifMap.prototype.get=function(a){return this[a]||this[this.map[a]]},c.exifTagTypes={1:{getValue:function(a,b){return a.getUint8(b)},size:1},2:{getValue:function(a,b){return String.fromCharCode(a.getUint8(b))},size:1,ascii:!0},3:{getValue:function(a,b,c){return a.getUint16(b,c)},size:2},4:{getValue:function(a,b,c){return a.getUint32(b,c)},size:4},5:{getValue:function(a,b,c){return a.getUint32(b,c)/a.getUint32(b+4,c)},size:8},9:{getValue:function(a,b,c){return a.getInt32(b,c)},size:4},10:{getValue:function(a,b,c){return a.getInt32(b,c)/a.getInt32(b+4,c)},size:8}},c.exifTagTypes[7]=c.exifTagTypes[1],c.getExifValue=function(b,d,e,f,g,h){var i,j,k,l,m,n,o=c.exifTagTypes[f];if(!o)return a.log("Invalid Exif data: Invalid tag type."),void 0;if(i=o.size*g,j=i>4?d+b.getUint32(e+8,h):e+8,j+i>b.byteLength)return a.log("Invalid Exif data: Invalid data offset."),void 0;if(1===g)return o.getValue(b,j,h);for(k=[],l=0;g>l;l+=1)k[l]=o.getValue(b,j+l*o.size,h);if(o.ascii){for(m="",l=0;lb.byteLength)return a.log("Invalid Exif data: Invalid directory offset."),void 0;if(g=b.getUint16(d,e),h=d+2+12*g,h+4>b.byteLength)return a.log("Invalid Exif data: Invalid directory size."),void 0;for(i=0;g>i;i+=1)this.parseExifTag(b,c,d+2+12*i,e,f);return b.getUint32(h,e)},c.parseExifData=function(b,d,e,f){var g,h,i=d+10;if(1165519206===b.getUint32(d+4)){if(i+8>b.byteLength)return a.log("Invalid Exif data: Invalid segment size."),void 0;if(0!==b.getUint16(d+8))return a.log("Invalid Exif data: Missing byte alignment offset."),void 0;switch(b.getUint16(i)){case 18761:g=!0;break;case 19789:g=!1;break;default:return a.log("Invalid Exif data: Invalid byte alignment marker."),void 0}if(42!==b.getUint16(i+2,g))return a.log("Invalid Exif data: Missing TIFF marker."),void 0;h=b.getUint32(i+4,g),f.exif=new c.ExifMap,h=c.parseExifTags(b,i,i+h,g,f)}},b.parsers[65505].push(c.parseExifData),c}),b("runtime/html5/jpegencoder",[],function(){function a(a){function b(a){for(var b=[16,11,10,16,24,40,51,61,12,12,14,19,26,58,60,55,14,13,16,24,40,57,69,56,14,17,22,29,51,87,80,62,18,22,37,56,68,109,103,77,24,35,55,64,81,104,113,92,49,64,78,87,103,121,120,101,72,92,95,98,112,100,103,99],c=0;64>c;c++){var d=y((b[c]*a+50)/100);1>d?d=1:d>255&&(d=255),z[P[c]]=d}for(var e=[17,18,24,47,99,99,99,99,18,21,26,66,99,99,99,99,24,26,56,99,99,99,99,99,47,66,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99],f=0;64>f;f++){var g=y((e[f]*a+50)/100);1>g?g=1:g>255&&(g=255),A[P[f]]=g}for(var h=[1,1.387039845,1.306562965,1.175875602,1,.785694958,.5411961,.275899379],i=0,j=0;8>j;j++)for(var k=0;8>k;k++)B[i]=1/(8*z[P[i]]*h[j]*h[k]),C[i]=1/(8*A[P[i]]*h[j]*h[k]),i++}function c(a,b){for(var c=0,d=0,e=new Array,f=1;16>=f;f++){for(var g=1;g<=a[f];g++)e[b[d]]=[],e[b[d]][0]=c,e[b[d]][1]=f,d++,c++;c*=2}return e}function d(){t=c(Q,R),u=c(U,V),v=c(S,T),w=c(W,X)}function e(){for(var a=1,b=2,c=1;15>=c;c++){for(var d=a;b>d;d++)E[32767+d]=c,D[32767+d]=[],D[32767+d][1]=c,D[32767+d][0]=d;for(var e=-(b-1);-a>=e;e++)E[32767+e]=c,D[32767+e]=[],D[32767+e][1]=c,D[32767+e][0]=b-1+e;a<<=1,b<<=1}}function f(){for(var a=0;256>a;a++)O[a]=19595*a,O[a+256>>0]=38470*a,O[a+512>>0]=7471*a+32768,O[a+768>>0]=-11059*a,O[a+1024>>0]=-21709*a,O[a+1280>>0]=32768*a+8421375,O[a+1536>>0]=-27439*a,O[a+1792>>0]=-5329*a}function g(a){for(var b=a[0],c=a[1]-1;c>=0;)b&1<J&&(255==I?(h(255),h(0)):h(I),J=7,I=0)}function h(a){H.push(N[a])}function i(a){h(255&a>>8),h(255&a)}function j(a,b){var c,d,e,f,g,h,i,j,k,l=0,m=8,n=64;for(k=0;m>k;++k){c=a[l],d=a[l+1],e=a[l+2],f=a[l+3],g=a[l+4],h=a[l+5],i=a[l+6],j=a[l+7];var o=c+j,p=c-j,q=d+i,r=d-i,s=e+h,t=e-h,u=f+g,v=f-g,w=o+u,x=o-u,y=q+s,z=q-s;a[l]=w+y,a[l+4]=w-y;var A=.707106781*(z+x);a[l+2]=x+A,a[l+6]=x-A,w=v+t,y=t+r,z=r+p;var B=.382683433*(w-z),C=.5411961*w+B,D=1.306562965*z+B,E=.707106781*y,G=p+E,H=p-E;a[l+5]=H+C,a[l+3]=H-C,a[l+1]=G+D,a[l+7]=G-D,l+=8}for(l=0,k=0;m>k;++k){c=a[l],d=a[l+8],e=a[l+16],f=a[l+24],g=a[l+32],h=a[l+40],i=a[l+48],j=a[l+56];var I=c+j,J=c-j,K=d+i,L=d-i,M=e+h,N=e-h,O=f+g,P=f-g,Q=I+O,R=I-O,S=K+M,T=K-M;a[l]=Q+S,a[l+32]=Q-S;var U=.707106781*(T+R);a[l+16]=R+U,a[l+48]=R-U,Q=P+N,S=N+L,T=L+J;var V=.382683433*(Q-T),W=.5411961*Q+V,X=1.306562965*T+V,Y=.707106781*S,Z=J+Y,$=J-Y;a[l+40]=$+W,a[l+24]=$-W,a[l+8]=Z+X,a[l+56]=Z-X,l++}var _;for(k=0;n>k;++k)_=a[k]*b[k],F[k]=_>0?0|_+.5:0|_-.5;return F}function k(){i(65504),i(16),h(74),h(70),h(73),h(70),h(0),h(1),h(1),h(0),i(1),i(1),h(0),h(0)}function l(a,b){i(65472),i(17),h(8),i(b),i(a),h(3),h(1),h(17),h(0),h(2),h(17),h(1),h(3),h(17),h(1)}function m(){i(65499),i(132),h(0);for(var a=0;64>a;a++)h(z[a]);h(1);for(var b=0;64>b;b++)h(A[b])}function n(){i(65476),i(418),h(0);for(var a=0;16>a;a++)h(Q[a+1]);for(var b=0;11>=b;b++)h(R[b]);h(16);for(var c=0;16>c;c++)h(S[c+1]);for(var d=0;161>=d;d++)h(T[d]);h(1);for(var e=0;16>e;e++)h(U[e+1]);for(var f=0;11>=f;f++)h(V[f]);h(17);for(var g=0;16>g;g++)h(W[g+1]);for(var j=0;161>=j;j++)h(X[j])}function o(){i(65498),i(12),h(3),h(1),h(0),h(2),h(17),h(3),h(17),h(0),h(63),h(0)}function p(a,b,c,d,e){for(var f,h=e[0],i=e[240],k=16,l=63,m=64,n=j(a,b),o=0;m>o;++o)G[P[o]]=n[o];var p=G[0]-c;c=G[0],0==p?g(d[0]):(f=32767+p,g(d[E[f]]),g(D[f]));for(var q=63;q>0&&0==G[q];q--);if(0==q)return g(h),c;for(var r,s=1;q>=s;){for(var t=s;0==G[s]&&q>=s;++s);var u=s-t;if(u>=k){r=u>>4;for(var v=1;r>=v;++v)g(i);u=15&u}f=32767+G[s],g(e[(u<<4)+E[f]]),g(D[f]),s++}return q!=l&&g(h),c}function q(){for(var a=String.fromCharCode,b=0;256>b;b++)N[b]=a(b)}function r(a){if(0>=a&&(a=1),a>100&&(a=100),x!=a){var c=0;c=50>a?Math.floor(5e3/a):Math.floor(200-2*a),b(c),x=a}}function s(){a||(a=50),q(),d(),e(),f(),r(a)}Math.round;var t,u,v,w,x,y=Math.floor,z=new Array(64),A=new Array(64),B=new Array(64),C=new Array(64),D=new Array(65535),E=new Array(65535),F=new Array(64),G=new Array(64),H=[],I=0,J=7,K=new Array(64),L=new Array(64),M=new Array(64),N=new Array(256),O=new Array(2048),P=[0,1,5,6,14,15,27,28,2,4,7,13,16,26,29,42,3,8,12,17,25,30,41,43,9,11,18,24,31,40,44,53,10,19,23,32,39,45,52,54,20,22,33,38,46,51,55,60,21,34,37,47,50,56,59,61,35,36,48,49,57,58,62,63],Q=[0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0],R=[0,1,2,3,4,5,6,7,8,9,10,11],S=[0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,125],T=[1,2,3,0,4,17,5,18,33,49,65,6,19,81,97,7,34,113,20,50,129,145,161,8,35,66,177,193,21,82,209,240,36,51,98,114,130,9,10,22,23,24,25,26,37,38,39,40,41,42,52,53,54,55,56,57,58,67,68,69,70,71,72,73,74,83,84,85,86,87,88,89,90,99,100,101,102,103,104,105,106,115,116,117,118,119,120,121,122,131,132,133,134,135,136,137,138,146,147,148,149,150,151,152,153,154,162,163,164,165,166,167,168,169,170,178,179,180,181,182,183,184,185,186,194,195,196,197,198,199,200,201,202,210,211,212,213,214,215,216,217,218,225,226,227,228,229,230,231,232,233,234,241,242,243,244,245,246,247,248,249,250],U=[0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0],V=[0,1,2,3,4,5,6,7,8,9,10,11],W=[0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,119],X=[0,1,2,3,17,4,5,33,49,6,18,65,81,7,97,113,19,34,50,129,8,20,66,145,161,177,193,9,35,51,82,240,21,98,114,209,10,22,36,52,225,37,241,23,24,25,26,38,39,40,41,42,53,54,55,56,57,58,67,68,69,70,71,72,73,74,83,84,85,86,87,88,89,90,99,100,101,102,103,104,105,106,115,116,117,118,119,120,121,122,130,131,132,133,134,135,136,137,138,146,147,148,149,150,151,152,153,154,162,163,164,165,166,167,168,169,170,178,179,180,181,182,183,184,185,186,194,195,196,197,198,199,200,201,202,210,211,212,213,214,215,216,217,218,226,227,228,229,230,231,232,233,234,242,243,244,245,246,247,248,249,250];this.encode=function(a,b){b&&r(b),H=new Array,I=0,J=7,i(65496),k(),m(),l(a.width,a.height),n(),o();var c=0,d=0,e=0;I=0,J=7,this.encode.displayName="_encode_";for(var f,h,j,q,s,x,y,z,A,D=a.data,E=a.width,F=a.height,G=4*E,N=0;F>N;){for(f=0;G>f;){for(s=G*N+f,x=s,y=-1,z=0,A=0;64>A;A++)z=A>>3,y=4*(7&A),x=s+z*G+y,N+z>=F&&(x-=G*(N+1+z-F)),f+y>=G&&(x-=f+y-G+4),h=D[x++],j=D[x++],q=D[x++],K[A]=(O[h]+O[j+256>>0]+O[q+512>>0]>>16)-128,L[A]=(O[h+768>>0]+O[j+1024>>0]+O[q+1280>>0]>>16)-128,M[A]=(O[h+1280>>0]+O[j+1536>>0]+O[q+1792>>0]>>16)-128;c=p(K,B,c,t,v),d=p(L,C,d,u,w),e=p(M,C,e,u,w),f+=32}N+=8}if(J>=0){var P=[];P[1]=J+1,P[0]=(1<i;)e=d[4*(k-1)+3],0===e?j=k:i=k,k=j+i>>1;return f=k/c,0===f?1:f}function c(a){var b,c,d=a.naturalWidth,e=a.naturalHeight;return d*e>1048576?(b=document.createElement("canvas"),b.width=b.height=1,c=b.getContext("2d"),c.drawImage(a,-d+1,0),0===c.getImageData(0,0,1,1).data[3]):!1}return a.os.ios?a.os.ios>=7?function(a,c,d,e,f,g){var h=c.naturalWidth,i=c.naturalHeight,j=b(c,h,i);return a.getContext("2d").drawImage(c,0,0,h*j,i*j,d,e,f,g)}:function(a,d,e,f,g,h){var i,j,k,l,m,n,o,p=d.naturalWidth,q=d.naturalHeight,r=a.getContext("2d"),s=c(d),t="image/jpeg"===this.type,u=1024,v=0,w=0;for(s&&(p/=2,q/=2),r.save(),i=document.createElement("canvas"),i.width=i.height=u,j=i.getContext("2d"),k=t?b(d,p,q):1,l=Math.ceil(u*g/p),m=Math.ceil(u*h/q/k);q>v;){for(n=0,o=0;p>n;)j.clearRect(0,0,u,u),j.drawImage(d,-n,-v),r.drawImage(i,0,0,u,u,e+o,f+w,l,m),n+=u,o+=l;v+=u,w+=m}r.restore(),i=j=null}:function(b){var c=a.slice(arguments,1),d=b.getContext("2d");d.drawImage.apply(d,c)}}()})}),b("runtime/html5/transport",["base","runtime/html5/runtime"],function(a,b){var c=a.noop,d=a.$;return b.register("Transport",{init:function(){this._status=0,this._response=null},send:function(){var b,c,e,f=this.owner,g=this.options,h=this._initAjax(),i=f._blob,j=g.server;g.sendAsBinary?(j+=(/\?/.test(j)?"&":"?")+d.param(f._formData),c=i.getSource()):(b=new FormData,d.each(f._formData,function(a,c){b.append(a,c)}),b.append(g.fileVal,i.getSource(),g.filename||f._formData.name||"")),g.withCredentials&&"withCredentials"in h?(h.open(g.method,j,!0),h.withCredentials=!0):h.open(g.method,j),this._setRequestHeader(h,g.headers),c?(h.overrideMimeType&&h.overrideMimeType("application/octet-stream"),a.os.android?(e=new FileReader,e.onload=function(){h.send(this.result),e=e.onload=null},e.readAsArrayBuffer(c)):h.send(c)):h.send(b)},getResponse:function(){return this._response},getResponseAsJson:function(){return this._parseJson(this._response)},getStatus:function(){return this._status},abort:function(){var a=this._xhr;a&&(a.upload.onprogress=c,a.onreadystatechange=c,a.abort(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var a=this,b=new XMLHttpRequest,d=this.options;return!d.withCredentials||"withCredentials"in b||"undefined"==typeof XDomainRequest||(b=new XDomainRequest),b.upload.onprogress=function(b){var c=0;return b.lengthComputable&&(c=b.loaded/b.total),a.trigger("progress",c)},b.onreadystatechange=function(){return 4===b.readyState?(b.upload.onprogress=c,b.onreadystatechange=c,a._xhr=null,a._status=b.status,b.status>=200&&b.status<300?(a._response=b.responseText,a.trigger("load")):b.status>=500&&b.status<600?(a._response=b.responseText,a.trigger("error","server")):a.trigger("error",a._status?"http":"abort")):void 0},a._xhr=b,b},_setRequestHeader:function(a,b){d.each(b,function(b,c){a.setRequestHeader(b,c)})},_parseJson:function(a){var b;try{b=JSON.parse(a)}catch(c){b={}}return b}})}),b("runtime/html5/md5",["runtime/html5/runtime"],function(a){var b=function(a,b){return 4294967295&a+b},c=function(a,c,d,e,f,g){return c=b(b(c,a),b(e,g)),b(c<>>32-f,d)},d=function(a,b,d,e,f,g,h){return c(b&d|~b&e,a,b,f,g,h)},e=function(a,b,d,e,f,g,h){return c(b&e|d&~e,a,b,f,g,h)},f=function(a,b,d,e,f,g,h){return c(b^d^e,a,b,f,g,h)},g=function(a,b,d,e,f,g,h){return c(d^(b|~e),a,b,f,g,h)},h=function(a,c){var h=a[0],i=a[1],j=a[2],k=a[3];h=d(h,i,j,k,c[0],7,-680876936),k=d(k,h,i,j,c[1],12,-389564586),j=d(j,k,h,i,c[2],17,606105819),i=d(i,j,k,h,c[3],22,-1044525330),h=d(h,i,j,k,c[4],7,-176418897),k=d(k,h,i,j,c[5],12,1200080426),j=d(j,k,h,i,c[6],17,-1473231341),i=d(i,j,k,h,c[7],22,-45705983),h=d(h,i,j,k,c[8],7,1770035416),k=d(k,h,i,j,c[9],12,-1958414417),j=d(j,k,h,i,c[10],17,-42063),i=d(i,j,k,h,c[11],22,-1990404162),h=d(h,i,j,k,c[12],7,1804603682),k=d(k,h,i,j,c[13],12,-40341101),j=d(j,k,h,i,c[14],17,-1502002290),i=d(i,j,k,h,c[15],22,1236535329),h=e(h,i,j,k,c[1],5,-165796510),k=e(k,h,i,j,c[6],9,-1069501632),j=e(j,k,h,i,c[11],14,643717713),i=e(i,j,k,h,c[0],20,-373897302),h=e(h,i,j,k,c[5],5,-701558691),k=e(k,h,i,j,c[10],9,38016083),j=e(j,k,h,i,c[15],14,-660478335),i=e(i,j,k,h,c[4],20,-405537848),h=e(h,i,j,k,c[9],5,568446438),k=e(k,h,i,j,c[14],9,-1019803690),j=e(j,k,h,i,c[3],14,-187363961),i=e(i,j,k,h,c[8],20,1163531501),h=e(h,i,j,k,c[13],5,-1444681467),k=e(k,h,i,j,c[2],9,-51403784),j=e(j,k,h,i,c[7],14,1735328473),i=e(i,j,k,h,c[12],20,-1926607734),h=f(h,i,j,k,c[5],4,-378558),k=f(k,h,i,j,c[8],11,-2022574463),j=f(j,k,h,i,c[11],16,1839030562),i=f(i,j,k,h,c[14],23,-35309556),h=f(h,i,j,k,c[1],4,-1530992060),k=f(k,h,i,j,c[4],11,1272893353),j=f(j,k,h,i,c[7],16,-155497632),i=f(i,j,k,h,c[10],23,-1094730640),h=f(h,i,j,k,c[13],4,681279174),k=f(k,h,i,j,c[0],11,-358537222),j=f(j,k,h,i,c[3],16,-722521979),i=f(i,j,k,h,c[6],23,76029189),h=f(h,i,j,k,c[9],4,-640364487),k=f(k,h,i,j,c[12],11,-421815835),j=f(j,k,h,i,c[15],16,530742520),i=f(i,j,k,h,c[2],23,-995338651),h=g(h,i,j,k,c[0],6,-198630844),k=g(k,h,i,j,c[7],10,1126891415),j=g(j,k,h,i,c[14],15,-1416354905),i=g(i,j,k,h,c[5],21,-57434055),h=g(h,i,j,k,c[12],6,1700485571),k=g(k,h,i,j,c[3],10,-1894986606),j=g(j,k,h,i,c[10],15,-1051523),i=g(i,j,k,h,c[1],21,-2054922799),h=g(h,i,j,k,c[8],6,1873313359),k=g(k,h,i,j,c[15],10,-30611744),j=g(j,k,h,i,c[6],15,-1560198380),i=g(i,j,k,h,c[13],21,1309151649),h=g(h,i,j,k,c[4],6,-145523070),k=g(k,h,i,j,c[11],10,-1120210379),j=g(j,k,h,i,c[2],15,718787259),i=g(i,j,k,h,c[9],21,-343485551),a[0]=b(h,a[0]),a[1]=b(i,a[1]),a[2]=b(j,a[2]),a[3]=b(k,a[3])},i=function(a){var b,c=[];for(b=0;64>b;b+=4)c[b>>2]=a.charCodeAt(b)+(a.charCodeAt(b+1)<<8)+(a.charCodeAt(b+2)<<16)+(a.charCodeAt(b+3)<<24);return c},j=function(a){var b,c=[];for(b=0;64>b;b+=4)c[b>>2]=a[b]+(a[b+1]<<8)+(a[b+2]<<16)+(a[b+3]<<24);return c},k=function(a){var b,c,d,e,f,g,j=a.length,k=[1732584193,-271733879,-1732584194,271733878];for(b=64;j>=b;b+=64)h(k,i(a.substring(b-64,b)));for(a=a.substring(b-64),c=a.length,d=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],b=0;c>b;b+=1)d[b>>2]|=a.charCodeAt(b)<<(b%4<<3);if(d[b>>2]|=128<<(b%4<<3),b>55)for(h(k,d),b=0;16>b;b+=1)d[b]=0;return e=8*j,e=e.toString(16).match(/(.*?)(.{0,8})$/),f=parseInt(e[2],16),g=parseInt(e[1],16)||0,d[14]=f,d[15]=g,h(k,d),k},l=function(a){var b,c,d,e,f,g,i=a.length,k=[1732584193,-271733879,-1732584194,271733878];for(b=64;i>=b;b+=64)h(k,j(a.subarray(b-64,b)));for(a=i>b-64?a.subarray(b-64):new Uint8Array(0),c=a.length,d=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],b=0;c>b;b+=1)d[b>>2]|=a[b]<<(b%4<<3);if(d[b>>2]|=128<<(b%4<<3),b>55)for(h(k,d),b=0;16>b;b+=1)d[b]=0;return e=8*i,e=e.toString(16).match(/(.*?)(.{0,8})$/),f=parseInt(e[2],16),g=parseInt(e[1],16)||0,d[14]=f,d[15]=g,h(k,d),k},m=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"],n=function(a){var b,c="";for(b=0;4>b;b+=1)c+=m[15&a>>8*b+4]+m[15&a>>8*b];return c},o=function(a){var b;for(b=0;b>16)+(b>>16)+(c>>16);return d<<16|65535&c}),q.prototype.append=function(a){return/[\u0080-\uFFFF]/.test(a)&&(a=unescape(encodeURIComponent(a))),this.appendBinary(a),this},q.prototype.appendBinary=function(a){this._buff+=a,this._length+=a.length;var b,c=this._buff.length;for(b=64;c>=b;b+=64)h(this._state,i(this._buff.substring(b-64,b)));return this._buff=this._buff.substr(b-64),this},q.prototype.end=function(a){var b,c,d=this._buff,e=d.length,f=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(b=0;e>b;b+=1)f[b>>2]|=d.charCodeAt(b)<<(b%4<<3);return this._finish(f,e),c=a?this._state:o(this._state),this.reset(),c},q.prototype._finish=function(a,b){var c,d,e,f=b;if(a[f>>2]|=128<<(f%4<<3),f>55)for(h(this._state,a),f=0;16>f;f+=1)a[f]=0;c=8*this._length,c=c.toString(16).match(/(.*?)(.{0,8})$/),d=parseInt(c[2],16),e=parseInt(c[1],16)||0,a[14]=d,a[15]=e,h(this._state,a)},q.prototype.reset=function(){return this._buff="",this._length=0,this._state=[1732584193,-271733879,-1732584194,271733878],this},q.prototype.destroy=function(){delete this._state,delete this._buff,delete this._length},q.hash=function(a,b){/[\u0080-\uFFFF]/.test(a)&&(a=unescape(encodeURIComponent(a)));var c=k(a);return b?c:o(c) +},q.hashBinary=function(a,b){var c=k(a);return b?c:o(c)},q.ArrayBuffer=function(){this.reset()},q.ArrayBuffer.prototype.append=function(a){var b,c=this._concatArrayBuffer(this._buff,a),d=c.length;for(this._length+=a.byteLength,b=64;d>=b;b+=64)h(this._state,j(c.subarray(b-64,b)));return this._buff=d>b-64?c.subarray(b-64):new Uint8Array(0),this},q.ArrayBuffer.prototype.end=function(a){var b,c,d=this._buff,e=d.length,f=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(b=0;e>b;b+=1)f[b>>2]|=d[b]<<(b%4<<3);return this._finish(f,e),c=a?this._state:o(this._state),this.reset(),c},q.ArrayBuffer.prototype._finish=q.prototype._finish,q.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._state=[1732584193,-271733879,-1732584194,271733878],this},q.ArrayBuffer.prototype.destroy=q.prototype.destroy,q.ArrayBuffer.prototype._concatArrayBuffer=function(a,b){var c=a.length,d=new Uint8Array(c+b.byteLength);return d.set(a),d.set(new Uint8Array(b),c),d},q.ArrayBuffer.hash=function(a,b){var c=l(new Uint8Array(a));return b?c:o(c)},a.register("Md5",{init:function(){},loadFromBlob:function(a){var b,c,d=a.getSource(),e=2097152,f=Math.ceil(d.size/e),g=0,h=this.owner,i=new q.ArrayBuffer,j=this,k=d.mozSlice||d.webkitSlice||d.slice;c=new FileReader,b=function(){var l,m;l=g*e,m=Math.min(l+e,d.size),c.onload=function(b){i.append(b.target.result),h.trigger("progress",{total:a.size,loaded:m})},c.onloadend=function(){c.onloadend=c.onload=null,++g'+''+''+''+"",c.html(a)},getFlash:function(){return this._flash?this._flash:(this._flash=g("#"+this.uid).get(0),this._flash)}}),f.register=function(a,c){return c=i[a]=b.inherits(d,g.extend({flashExec:function(){var a=this.owner,b=this.getRuntime();return b.flashExec.apply(a,arguments)}},c))},e()>=11.4&&c.addRuntime(h,f),f}),b("runtime/flash/filepicker",["base","runtime/flash/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(a){var b,d,e=c.extend({},a);for(b=e.accept&&e.accept.length,d=0;b>d;d++)e.accept[d].title||(e.accept[d].title="Files");delete e.button,delete e.id,delete e.container,this.flashExec("FilePicker","init",e)},destroy:function(){this.flashExec("FilePicker","destroy")}})}),b("runtime/flash/image",["runtime/flash/runtime"],function(a){return a.register("Image",{loadFromBlob:function(a){var b=this.owner;b.info()&&this.flashExec("Image","info",b.info()),b.meta()&&this.flashExec("Image","meta",b.meta()),this.flashExec("Image","loadFromBlob",a.uid)}})}),b("runtime/flash/transport",["base","runtime/flash/runtime","runtime/client"],function(b,c,d){var e=b.$;return c.register("Transport",{init:function(){this._status=0,this._response=null,this._responseJson=null},send:function(){var a,b=this.owner,c=this.options,d=this._initAjax(),f=b._blob,g=c.server;d.connectRuntime(f.ruid),c.sendAsBinary?(g+=(/\?/.test(g)?"&":"?")+e.param(b._formData),a=f.uid):(e.each(b._formData,function(a,b){d.exec("append",a,b)}),d.exec("appendBlob",c.fileVal,f.uid,c.filename||b._formData.name||"")),this._setRequestHeader(d,c.headers),d.exec("send",{method:c.method,url:g,forceURLStream:c.forceURLStream,mimeType:"application/octet-stream"},a)},getStatus:function(){return this._status},getResponse:function(){return this._response||""},getResponseAsJson:function(){return this._responseJson},abort:function(){var a=this._xhr;a&&(a.exec("abort"),a.destroy(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var b=this,c=new d("XMLHttpRequest");return c.on("uploadprogress progress",function(a){var c=a.loaded/a.total;return c=Math.min(1,Math.max(0,c)),b.trigger("progress",c)}),c.on("load",function(){var d,e=c.exec("getStatus"),f=!1,g="";return c.off(),b._xhr=null,e>=200&&300>e?f=!0:e>=500&&600>e?(f=!0,g="server"):g="http",f&&(b._response=c.exec("getResponse"),b._response=decodeURIComponent(b._response),d=function(b){try{return a.JSON&&a.JSON.parse?JSON.parse(b):new Function("return "+b).call()}catch(c){return{}}},b._responseJson=b._response?d(b._response):{}),c.destroy(),c=null,g?b.trigger("error",g):b.trigger("load")}),c.on("error",function(){c.off(),b._xhr=null,b.trigger("error","http")}),b._xhr=c,c},_setRequestHeader:function(a,b){e.each(b,function(b,c){a.exec("setRequestHeader",b,c)})}})}),b("runtime/flash/blob",["runtime/flash/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.flashExec("Blob","slice",a,c);return new b(this.getRuid(),d)}})}),b("runtime/flash/md5",["runtime/flash/runtime"],function(a){return a.register("Md5",{init:function(){},loadFromBlob:function(a){return this.flashExec("Md5","loadFromBlob",a.uid)}})}),b("preset/all",["base","widgets/filednd","widgets/filepaste","widgets/filepicker","widgets/image","widgets/queue","widgets/runtime","widgets/upload","widgets/validator","widgets/md5","runtime/html5/blob","runtime/html5/dnd","runtime/html5/filepaste","runtime/html5/filepicker","runtime/html5/imagemeta/exif","runtime/html5/androidpatch","runtime/html5/image","runtime/html5/transport","runtime/html5/md5","runtime/flash/filepicker","runtime/flash/image","runtime/flash/transport","runtime/flash/blob","runtime/flash/md5"],function(a){return a}),b("widgets/log",["base","uploader","widgets/widget"],function(a,b){function c(a){var b=e.extend({},d,a),c=f.replace(/^(.*)\?/,"$1"+e.param(b)),g=new Image;g.src=c}var d,e=a.$,f=" http://static.tieba.baidu.com/tb/pms/img/st.gif??",g=(location.hostname||location.host||"protected").toLowerCase(),h=g&&/baidu/i.exec(g);if(h)return d={dv:3,master:"webuploader",online:/test/.exec(g)?0:1,module:"",product:g,type:0},b.register({name:"log",init:function(){var a=this.owner,b=0,d=0;a.on("error",function(a){c({type:2,c_error_code:a})}).on("uploadError",function(a,b){c({type:2,c_error_code:"UPLOAD_ERROR",c_reason:""+b})}).on("uploadComplete",function(a){b++,d+=a.size}).on("uploadFinished",function(){c({c_count:b,c_size:d}),b=d=0}),c({c_usage:1})}})}),b("webuploader",["preset/all","widgets/log"],function(a){return a}),c("webuploader")}); \ No newline at end of file diff --git a/public/static/libs/webuploader/webuploader.noimage.js b/public/static/libs/webuploader/webuploader.noimage.js new file mode 100644 index 0000000..2572638 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.noimage.js @@ -0,0 +1,5057 @@ +/*! WebUploader 0.1.6 */ + + +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + * @require "jquery" + * @require "zepto" + */ + define('dollar-third',[],function() { + var req = window.require; + var $ = window.__dollar || + window.jQuery || + window.Zepto || + req('jquery') || + req('zepto'); + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 使用jQuery的Promise + */ + define('promise-third',[ + 'dollar' + ], function( $ ) { + return { + Deferred: $.Deferred, + when: $.when, + + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + } + }; + }); + /** + * @fileOverview Promise/A+ + */ + define('promise',[ + 'promise-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.6', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview 错误信息 + */ + define('lib/dnd',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function DragAndDrop( opts ) { + opts = this.options = $.extend({}, DragAndDrop.options, opts ); + + opts.container = $( opts.container ); + + if ( !opts.container.length ) { + return; + } + + RuntimeClent.call( this, 'DragAndDrop' ); + } + + DragAndDrop.options = { + accept: null, + disableGlobalDnd: false + }; + + Base.inherits( RuntimeClent, { + constructor: DragAndDrop, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( DragAndDrop.prototype ); + + return DragAndDrop; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview DragAndDrop Widget。 + */ + define('widgets/filednd',[ + 'base', + 'uploader', + 'lib/dnd', + 'widgets/widget' + ], function( Base, Uploader, Dnd ) { + var $ = Base.$; + + Uploader.options.dnd = ''; + + /** + * @property {Selector} [dnd=undefined] 指定Drag And Drop拖拽的容器,如果不指定,则不启动。 + * @namespace options + * @for Uploader + */ + + /** + * @property {Selector} [disableGlobalDnd=false] 是否禁掉整个页面的拖拽功能,如果不禁用,图片拖进来的时候会默认被浏览器打开。 + * @namespace options + * @for Uploader + */ + + /** + * @event dndAccept + * @param {DataTransferItemList} items DataTransferItem + * @description 阻止此事件可以拒绝某些类型的文件拖入进来。目前只有 chrome 提供这样的 API,且只能通过 mime-type 验证。 + * @for Uploader + */ + return Uploader.register({ + name: 'dnd', + + init: function( opts ) { + + if ( !opts.dnd || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + disableGlobalDnd: opts.disableGlobalDnd, + container: opts.dnd, + accept: opts.accept + }), + dnd; + + this.dnd = dnd = new Dnd( options ); + + dnd.once( 'ready', deferred.resolve ); + dnd.on( 'drop', function( files ) { + me.request( 'add-file', [ files ]); + }); + + // 检测文件是否全部允许添加。 + dnd.on( 'accept', function( items ) { + return me.owner.trigger( 'dndAccept', items ); + }); + + dnd.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.dnd && this.dnd.destroy(); + } + }); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepaste',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function FilePaste( opts ) { + opts = this.options = $.extend({}, opts ); + opts.container = $( opts.container || document.body ); + RuntimeClent.call( this, 'FilePaste' ); + } + + Base.inherits( RuntimeClent, { + constructor: FilePaste, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( FilePaste.prototype ); + + return FilePaste; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/filepaste',[ + 'base', + 'uploader', + 'lib/filepaste', + 'widgets/widget' + ], function( Base, Uploader, FilePaste ) { + var $ = Base.$; + + /** + * @property {Selector} [paste=undefined] 指定监听paste事件的容器,如果不指定,不启用此功能。此功能为通过粘贴来添加截屏的图片。建议设置为`document.body`. + * @namespace options + * @for Uploader + */ + return Uploader.register({ + name: 'paste', + + init: function( opts ) { + + if ( !opts.paste || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + container: opts.paste, + accept: opts.accept + }), + paste; + + this.paste = paste = new FilePaste( options ); + + paste.once( 'ready', deferred.resolve ); + paste.on( 'paste', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + paste.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.paste && this.paste.destroy(); + } + }); + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClient, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClient.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file', + style: 'webuploader-pick' //pick element class attribute, default is "webuploader-pick" + }; + + Base.inherits( RuntimeClient, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button, + style = opts.style; + + if (style) + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + if (style) + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + if (style) + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addBtn + * @for Uploader + * @grammar addBtn( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addBtn({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.on('dialogopen', function() { + me.owner.trigger('dialogOpen', picker.button); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + if ( files.length ) { + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + // 销毁上传相关的属性。 + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定的文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + file.setStatus( Status.QUEUED ); + + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + file.setStatus( Status.PROGRESS ); + }); + + + } else if (file.getStatus() !== Status.PROGRESS) { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return Base.nextTick( me.__tick ); + } + + me.runing = true; + var files = []; + + // 如果有暂停的,则续传 + file || $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + me._trigged = false; + files.push(file); + v.transport && v.transport.send(); + } + }); + + $.each(files, function() { + this.setStatus( Status.PROGRESS ); + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this, + block; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + + + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file === file) { + block = v; + return false; + } + }); + + block.transport && block.transport.abort(); + + if (interrupt) { + me._putback(block); + me._popBlock(block); + } + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + // 正在准备中的文件。 + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePaste + */ + define('runtime/html5/dnd',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + var $ = Base.$, + prefix = 'webuploader-dnd-'; + + return Html5Runtime.register( 'DragAndDrop', { + init: function() { + var elem = this.elem = this.options.container; + + this.dragEnterHandler = Base.bindFn( this._dragEnterHandler, this ); + this.dragOverHandler = Base.bindFn( this._dragOverHandler, this ); + this.dragLeaveHandler = Base.bindFn( this._dragLeaveHandler, this ); + this.dropHandler = Base.bindFn( this._dropHandler, this ); + this.dndOver = false; + + elem.on( 'dragenter', this.dragEnterHandler ); + elem.on( 'dragover', this.dragOverHandler ); + elem.on( 'dragleave', this.dragLeaveHandler ); + elem.on( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).on( 'dragover', this.dragOverHandler ); + $( document ).on( 'drop', this.dropHandler ); + } + }, + + _dragEnterHandler: function( e ) { + var me = this, + denied = me._denied || false, + items; + + e = e.originalEvent || e; + + if ( !me.dndOver ) { + me.dndOver = true; + + // 注意只有 chrome 支持。 + items = e.dataTransfer.items; + + if ( items && items.length ) { + me._denied = denied = !me.trigger( 'accept', items ); + } + + me.elem.addClass( prefix + 'over' ); + me.elem[ denied ? 'addClass' : + 'removeClass' ]( prefix + 'denied' ); + } + + e.dataTransfer.dropEffect = denied ? 'none' : 'copy'; + + return false; + }, + + _dragOverHandler: function( e ) { + // 只处理框内的。 + var parentElem = this.elem.parent().get( 0 ); + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + clearTimeout( this._leaveTimer ); + this._dragEnterHandler.call( this, e ); + + return false; + }, + + _dragLeaveHandler: function() { + var me = this, + handler; + + handler = function() { + me.dndOver = false; + me.elem.removeClass( prefix + 'over ' + prefix + 'denied' ); + }; + + clearTimeout( me._leaveTimer ); + me._leaveTimer = setTimeout( handler, 100 ); + return false; + }, + + _dropHandler: function( e ) { + var me = this, + ruid = me.getRuid(), + parentElem = me.elem.parent().get( 0 ), + dataTransfer, data; + + // 只处理框内的。 + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + e = e.originalEvent || e; + dataTransfer = e.dataTransfer; + + // 如果是页面内拖拽,还不能处理,不阻止事件。 + // 此处 ie11 下会报参数错误, + try { + data = dataTransfer.getData('text/html'); + } catch( err ) { + } + + me.dndOver = false; + me.elem.removeClass( prefix + 'over' ); + + if ( data ) { + return; + } + + me._getTansferFiles( dataTransfer, function( results ) { + me.trigger( 'drop', $.map( results, function( file ) { + return new File( ruid, file ); + }) ); + }); + + return false; + }, + + // 如果传入 callback 则去查看文件夹,否则只管当前文件夹。 + _getTansferFiles: function( dataTransfer, callback ) { + var results = [], + promises = [], + items, files, file, item, i, len, canAccessFolder; + + items = dataTransfer.items; + files = dataTransfer.files; + + canAccessFolder = !!(items && items[ 0 ].webkitGetAsEntry); + + for ( i = 0, len = files.length; i < len; i++ ) { + file = files[ i ]; + item = items && items[ i ]; + + if ( canAccessFolder && item.webkitGetAsEntry().isDirectory ) { + + promises.push( this._traverseDirectoryTree( + item.webkitGetAsEntry(), results ) ); + } else { + results.push( file ); + } + } + + Base.when.apply( Base, promises ).done(function() { + + if ( !results.length ) { + return; + } + + callback( results ); + }); + }, + + _traverseDirectoryTree: function( entry, results ) { + var deferred = Base.Deferred(), + me = this; + + if ( entry.isFile ) { + entry.file(function( file ) { + results.push( file ); + deferred.resolve(); + }); + } else if ( entry.isDirectory ) { + entry.createReader().readEntries(function( entries ) { + var len = entries.length, + promises = [], + arr = [], // 为了保证顺序。 + i; + + for ( i = 0; i < len; i++ ) { + promises.push( me._traverseDirectoryTree( + entries[ i ], arr ) ); + } + + Base.when.apply( Base, promises ).then(function() { + results.push.apply( results, arr ); + deferred.resolve(); + }, deferred.reject ); + }); + } + + return deferred.promise(); + }, + + destroy: function() { + var elem = this.elem; + + // 还没 init 就调用 destroy + if (!elem) { + return; + } + + elem.off( 'dragenter', this.dragEnterHandler ); + elem.off( 'dragover', this.dragOverHandler ); + elem.off( 'dragleave', this.dragLeaveHandler ); + elem.off( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).off( 'dragover', this.dragOverHandler ); + $( document ).off( 'drop', this.dropHandler ); + } + } + }); + }); + + /** + * @fileOverview FilePaste + */ + define('runtime/html5/filepaste',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + return Html5Runtime.register( 'FilePaste', { + init: function() { + var opts = this.options, + elem = this.elem = opts.container, + accept = '.*', + arr, i, len, item; + + // accetp的mimeTypes中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].mimeTypes; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = arr.join(','); + accept = accept.replace( /,/g, '|' ).replace( /\*/g, '.*' ); + } + } + this.accept = accept = new RegExp( accept, 'i' ); + this.hander = Base.bindFn( this._pasteHander, this ); + elem.on( 'paste', this.hander ); + }, + + _pasteHander: function( e ) { + var allowed = [], + ruid = this.getRuid(), + items, item, blob, i, len; + + e = e.originalEvent || e; + items = e.clipboardData.items; + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + + if ( item.kind !== 'file' || !(blob = item.getAsFile()) ) { + continue; + } + + allowed.push( new File( ruid, blob ) ); + } + + if ( allowed.length ) { + // 不阻止非文件粘贴(文字粘贴)的事件冒泡 + e.preventDefault(); + e.stopPropagation(); + this.trigger( 'paste', allowed ); + } + }, + + destroy: function() { + this.elem.off( 'paste', this.hander ); + } + }); + }); + + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'capture', 'camera'); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function(e) { + input.trigger('click'); + e.stopPropagation(); + owner.trigger('dialogopen'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + /** + * @fileOverview FlashRuntime + */ + define('runtime/flash/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var $ = Base.$, + type = 'flash', + components = {}; + + + function getFlashVersion() { + var version; + + try { + version = navigator.plugins[ 'Shockwave Flash' ]; + version = version.description; + } catch ( ex ) { + try { + version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash') + .GetVariable('$version'); + } catch ( ex2 ) { + version = '0.0'; + } + } + version = version.match( /\d+/g ); + return parseFloat( version[ 0 ] + '.' + version[ 1 ], 10 ); + } + + function FlashRuntime() { + var pool = {}, + clients = {}, + destroy = this.destroy, + me = this, + jsreciver = Base.guid('webuploader_'); + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/ ) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + clients[ uid ] = client; + + if ( components[ comp ] ) { + if ( !pool[ uid ] ) { + pool[ uid ] = new components[ comp ]( client, me ); + } + + instance = pool[ uid ]; + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + + return me.flashExec.apply( client, arguments ); + }; + + function handler( evt, obj ) { + var type = evt.type || evt, + parts, uid; + + parts = type.split('::'); + uid = parts[ 0 ]; + type = parts[ 1 ]; + + // console.log.apply( console, arguments ); + + if ( type === 'Ready' && uid === me.uid ) { + me.trigger('ready'); + } else if ( clients[ uid ] ) { + clients[ uid ].trigger( type.toLowerCase(), evt, obj ); + } + + // Base.log( evt, obj ); + } + + // flash的接受器。 + window[ jsreciver ] = function() { + var args = arguments; + + // 为了能捕获得到。 + setTimeout(function() { + handler.apply( null, args ); + }, 1 ); + }; + + this.jsreciver = jsreciver; + + this.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + + this.flashExec = function( comp, fn ) { + var flash = me.getFlash(), + args = Base.slice( arguments, 2 ); + + return flash.exec( this.uid, comp, fn, args ); + }; + + // @todo + } + + Base.inherits( Runtime, { + constructor: FlashRuntime, + + init: function() { + var container = this.getContainer(), + opts = this.options, + html; + + // 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) + container.css({ + position: 'absolute', + top: '-8px', + left: '-8px', + width: '9px', + height: '9px', + overflow: 'hidden' + }); + + // insert flash object + html = '' + + '' + + '' + + '' + + ''; + + container.html( html ); + }, + + getFlash: function() { + if ( this._flash ) { + return this._flash; + } + + this._flash = $( '#' + this.uid ).get( 0 ); + return this._flash; + } + + }); + + FlashRuntime.register = function( name, component ) { + component = components[ name ] = Base.inherits( CompBase, $.extend({ + + // @todo fix this later + flashExec: function() { + var owner = this.owner, + runtime = this.getRuntime(); + + return runtime.flashExec.apply( owner, arguments ); + } + }, component ) ); + + return component; + }; + + if ( getFlashVersion() >= 11.4 ) { + Runtime.addRuntime( type, FlashRuntime ); + } + + return FlashRuntime; + }); + /** + * @fileOverview FilePicker + */ + define('runtime/flash/filepicker',[ + 'base', + 'runtime/flash/runtime' + ], function( Base, FlashRuntime ) { + var $ = Base.$; + + return FlashRuntime.register( 'FilePicker', { + init: function( opts ) { + var copy = $.extend({}, opts ), + len, i; + + // 修复Flash再没有设置title的情况下无法弹出flash文件选择框的bug. + len = copy.accept && copy.accept.length; + for ( i = 0; i < len; i++ ) { + if ( !copy.accept[ i ].title ) { + copy.accept[ i ].title = 'Files'; + } + } + + delete copy.button; + delete copy.id; + delete copy.container; + + this.flashExec( 'FilePicker', 'init', copy ); + }, + + destroy: function() { + this.flashExec( 'FilePicker', 'destroy' ); + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/flash/transport',[ + 'base', + 'runtime/flash/runtime', + 'runtime/client' + ], function( Base, FlashRuntime, RuntimeClient ) { + var $ = Base.$; + + return FlashRuntime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + this._responseJson = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + binary; + + xhr.connectRuntime( blob.ruid ); + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.uid; + } else { + $.each( owner._formData, function( k, v ) { + xhr.exec( 'append', k, v ); + }); + + xhr.exec( 'appendBlob', opts.fileVal, blob.uid, + opts.filename || owner._formData.name || '' ); + } + + this._setRequestHeader( xhr, opts.headers ); + xhr.exec( 'send', { + method: opts.method, + url: server, + forceURLStream: opts.forceURLStream, + mimeType: 'application/octet-stream' + }, binary ); + }, + + getStatus: function() { + return this._status; + }, + + getResponse: function() { + return this._response || ''; + }, + + getResponseAsJson: function() { + return this._responseJson; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.exec('abort'); + xhr.destroy(); + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new RuntimeClient('XMLHttpRequest'); + + xhr.on( 'uploadprogress progress', function( e ) { + var percent = e.loaded / e.total; + percent = Math.min( 1, Math.max( 0, percent ) ); + return me.trigger( 'progress', percent ); + }); + + xhr.on( 'load', function() { + var status = xhr.exec('getStatus'), + readBody = false, + err = '', + p; + + xhr.off(); + me._xhr = null; + + if ( status >= 200 && status < 300 ) { + readBody = true; + } else if ( status >= 500 && status < 600 ) { + readBody = true; + err = 'server'; + } else { + err = 'http'; + } + + if ( readBody ) { + me._response = xhr.exec('getResponse'); + me._response = decodeURIComponent( me._response ); + + // flash 处理可能存在 bug, 没辙只能靠 js 了 + // try { + // me._responseJson = xhr.exec('getResponseAsJson'); + // } catch ( error ) { + + p = function( s ) { + try { + if (window.JSON && window.JSON.parse) { + return JSON.parse(s); + } + + return new Function('return ' + s).call(); + } catch ( err ) { + return {}; + } + }; + me._responseJson = me._response ? p(me._response) : {}; + + // } + } + + xhr.destroy(); + xhr = null; + + return err ? me.trigger( 'error', err ) : me.trigger('load'); + }); + + xhr.on( 'error', function() { + xhr.off(); + me._xhr = null; + me.trigger( 'error', 'http' ); + }); + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.exec( 'setRequestHeader', key, val ); + }); + } + }); + }); + + /** + * @fileOverview Blob Html实现 + */ + define('runtime/flash/blob',[ + 'runtime/flash/runtime', + 'lib/blob' + ], function( FlashRuntime, Blob ) { + + return FlashRuntime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.flashExec( 'Blob', 'slice', start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview 没有图像处理的版本。 + */ + define('preset/withoutimage',[ + 'base', + + // widgets + 'widgets/filednd', + 'widgets/filepaste', + 'widgets/filepicker', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + + // runtimes + // html5 + 'runtime/html5/blob', + 'runtime/html5/dnd', + 'runtime/html5/filepaste', + 'runtime/html5/filepicker', + 'runtime/html5/transport', + + // flash + 'runtime/flash/filepicker', + 'runtime/flash/transport', + 'runtime/flash/blob' + ], function( Base ) { + return Base; + }); + define('webuploader',[ + 'preset/withoutimage' + ], function( preset ) { + return preset; + }); + return require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.noimage.min.js b/public/static/libs/webuploader/webuploader.noimage.min.js new file mode 100644 index 0000000..aefecb7 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.noimage.min.js @@ -0,0 +1,2 @@ +/* WebUploader 0.1.6 */!function(a,b){var c,d={},e=function(a,b){var c,d,e;if("string"==typeof a)return h(a);for(c=[],d=a.length,e=0;d>e;e++)c.push(h(a[e]));return b.apply(null,c)},f=function(a,b,c){2===arguments.length&&(c=b,b=null),e(b||[],function(){g(a,c,arguments)})},g=function(a,b,c){var f,g={exports:b};"function"==typeof b&&(c.length||(c=[e,g.exports,g]),f=b.apply(null,c),void 0!==f&&(g.exports=f)),d[a]=g.exports},h=function(b){var c=d[b]||a[b];if(!c)throw new Error("`"+b+"` is undefined");return c},i=function(a){var b,c,e,f,g,h;h=function(a){return a&&a.charAt(0).toUpperCase()+a.substr(1)};for(b in d)if(c=a,d.hasOwnProperty(b)){for(e=b.split("/"),g=h(e.pop());f=h(e.shift());)c[f]=c[f]||{},c=c[f];c[g]=d[b]}return a},j=function(c){return a.__dollar=c,i(b(a,f,e))};"object"==typeof module&&"object"==typeof module.exports?module.exports=j():"function"==typeof define&&define.amd?define(["jquery"],j):(c=a.WebUploader,a.WebUploader=j(),a.WebUploader.noConflict=function(){a.WebUploader=c})}(window,function(a,b,c){return b("dollar-third",[],function(){var b=a.require,c=a.__dollar||a.jQuery||a.Zepto||b("jquery")||b("zepto");if(!c)throw new Error("jQuery or Zepto not found!");return c}),b("dollar",["dollar-third"],function(a){return a}),b("promise-third",["dollar"],function(a){return{Deferred:a.Deferred,when:a.when,isPromise:function(a){return a&&"function"==typeof a.then}}}),b("promise",["promise-third"],function(a){return a}),b("base",["dollar","promise"],function(b,c){function d(a){return function(){return h.apply(a,arguments)}}function e(a,b){return function(){return a.apply(b,arguments)}}function f(a){var b;return Object.create?Object.create(a):(b=function(){},b.prototype=a,new b)}var g=function(){},h=Function.call;return{version:"0.1.6",$:b,Deferred:c.Deferred,isPromise:c.isPromise,when:c.when,browser:function(a){var b={},c=a.match(/WebKit\/([\d.]+)/),d=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),e=a.match(/MSIE\s([\d\.]+)/)||a.match(/(?:trident)(?:.*rv:([\w.]+))?/i),f=a.match(/Firefox\/([\d.]+)/),g=a.match(/Safari\/([\d.]+)/),h=a.match(/OPR\/([\d.]+)/);return c&&(b.webkit=parseFloat(c[1])),d&&(b.chrome=parseFloat(d[1])),e&&(b.ie=parseFloat(e[1])),f&&(b.firefox=parseFloat(f[1])),g&&(b.safari=parseFloat(g[1])),h&&(b.opera=parseFloat(h[1])),b}(navigator.userAgent),os:function(a){var b={},c=a.match(/(?:Android);?[\s\/]+([\d.]+)?/),d=a.match(/(?:iPad|iPod|iPhone).*OS\s([\d_]+)/);return c&&(b.android=parseFloat(c[1])),d&&(b.ios=parseFloat(d[1].replace(/_/g,"."))),b}(navigator.userAgent),inherits:function(a,c,d){var e;return"function"==typeof c?(e=c,c=null):e=c&&c.hasOwnProperty("constructor")?c.constructor:function(){return a.apply(this,arguments)},b.extend(!0,e,a,d||{}),e.__super__=a.prototype,e.prototype=f(a.prototype),c&&b.extend(!0,e.prototype,c),e},noop:g,bindFn:e,log:function(){return a.console?e(console.log,console):g}(),nextTick:function(){return function(a){setTimeout(a,1)}}(),slice:d([].slice),guid:function(){var a=0;return function(b){for(var c=(+new Date).toString(32),d=0;5>d;d++)c+=Math.floor(65535*Math.random()).toString(32);return(b||"wu_")+c+(a++).toString(32)}}(),formatSize:function(a,b,c){var d;for(c=c||["B","K","M","G","TB"];(d=c.shift())&&a>1024;)a/=1024;return("B"===d?a:a.toFixed(b||2))+d}}}),b("mediator",["base"],function(a){function b(a,b,c,d){return f.grep(a,function(a){return!(!a||b&&a.e!==b||c&&a.cb!==c&&a.cb._cb!==c||d&&a.ctx!==d)})}function c(a,b,c){f.each((a||"").split(h),function(a,d){c(d,b)})}function d(a,b){for(var c,d=!1,e=-1,f=a.length;++e1?(d.isPlainObject(b)&&d.isPlainObject(c[a])?d.extend(c[a],b):c[a]=b,void 0):a?c[a]:c},getStats:function(){var a=this.request("get-stats");return a?{successNum:a.numOfSuccess,progressNum:a.numOfProgress,cancelNum:a.numOfCancel,invalidNum:a.numOfInvalid,uploadFailNum:a.numOfUploadFailed,queueNum:a.numOfQueue,interruptNum:a.numofInterrupt}:{}},trigger:function(a){var c=[].slice.call(arguments,1),e=this.options,f="on"+a.substring(0,1).toUpperCase()+a.substring(1);return b.trigger.apply(this,arguments)===!1||d.isFunction(e[f])&&e[f].apply(this,c)===!1||d.isFunction(this[f])&&this[f].apply(this,c)===!1||b.trigger.apply(b,[this,a].concat(c))===!1?!1:!0},destroy:function(){this.request("destroy",arguments),this.off()},request:a.noop}),a.create=c.create=function(a){return new c(a)},a.Uploader=c,c}),b("runtime/runtime",["base","mediator"],function(a,b){function c(b){this.options=d.extend({container:document.body},b),this.uid=a.guid("rt_")}var d=a.$,e={},f=function(a){for(var b in a)if(a.hasOwnProperty(b))return b;return null};return d.extend(c.prototype,{getContainer:function(){var a,b,c=this.options;return this._container?this._container:(a=d(c.container||document.body),b=d(document.createElement("div")),b.attr("id","rt_"+this.uid),b.css({position:"absolute",top:"0px",left:"0px",width:"1px",height:"1px",overflow:"hidden"}),a.append(b),a.addClass("webuploader-container"),this._container=b,this._parent=a,b)},init:a.noop,exec:a.noop,destroy:function(){this._container&&this._container.remove(),this._parent&&this._parent.removeClass("webuploader-container"),this.off()}}),c.orders="html5,flash",c.addRuntime=function(a,b){e[a]=b},c.hasRuntime=function(a){return!!(a?e[a]:f(e))},c.create=function(a,b){var g,h;if(b=b||c.orders,d.each(b.split(/\s*,\s*/g),function(){return e[this]?(g=this,!1):void 0}),g=g||f(e),!g)throw new Error("Runtime Error");return h=new e[g](a)},b.installTo(c.prototype),c}),b("runtime/client",["base","mediator","runtime/runtime"],function(a,b,c){function d(b,d){var f,g=a.Deferred();this.uid=a.guid("client_"),this.runtimeReady=function(a){return g.done(a)},this.connectRuntime=function(b,h){if(f)throw new Error("already connected!");return g.done(h),"string"==typeof b&&e.get(b)&&(f=e.get(b)),f=f||e.get(null,d),f?(a.$.extend(f.options,b),f.__promise.then(g.resolve),f.__client++):(f=c.create(b,b.runtimeOrder),f.__promise=g.promise(),f.once("ready",g.resolve),f.init(),e.add(f),f.__client=1),d&&(f.__standalone=d),f},this.getRuntime=function(){return f},this.disconnectRuntime=function(){f&&(f.__client--,f.__client<=0&&(e.remove(f),delete f.__promise,f.destroy()),f=null)},this.exec=function(){if(f){var c=a.slice(arguments);return b&&c.unshift(b),f.exec.apply(this,c)}},this.getRuid=function(){return f&&f.uid},this.destroy=function(a){return function(){a&&a.apply(this,arguments),this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()}}(this.destroy)}var e;return e=function(){var a={};return{add:function(b){a[b.uid]=b},get:function(b,c){var d;if(b)return a[b];for(d in a)if(!c||!a[d].__standalone)return a[d];return null},remove:function(b){delete a[b.uid]}}}(),b.installTo(d.prototype),d}),b("lib/dnd",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},d.options,a),a.container=e(a.container),a.container.length&&c.call(this,"DragAndDrop")}var e=a.$;return d.options={accept:null,disableGlobalDnd:!1},a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/widget",["base","uploader"],function(a,b){function c(a){if(!a)return!1;var b=a.length,c=e.type(a);return 1===a.nodeType&&b?!0:"array"===c||"function"!==c&&"string"!==c&&(0===b||"number"==typeof b&&b>0&&b-1 in a)}function d(a){this.owner=a,this.options=a.options}var e=a.$,f=b.prototype._init,g=b.prototype.destroy,h={},i=[];return e.extend(d.prototype,{init:a.noop,invoke:function(a,b){var c=this.responseMap;return c&&a in c&&c[a]in this&&e.isFunction(this[c[a]])?this[c[a]].apply(this,b):h},request:function(){return this.owner.request.apply(this.owner,arguments)}}),e.extend(b.prototype,{_init:function(){var a=this,b=a._widgets=[],c=a.options.disableWidgets||"";return e.each(i,function(d,e){(!c||!~c.indexOf(e._name))&&b.push(new e(a))}),f.apply(a,arguments)},request:function(b,d,e){var f,g,i,j,k=0,l=this._widgets,m=l&&l.length,n=[],o=[];for(d=c(d)?d:[d];m>k;k++)f=l[k],g=f.invoke(b,d),g!==h&&(a.isPromise(g)?o.push(g):n.push(g));return e||o.length?(i=a.when.apply(a,o),j=i.pipe?"pipe":"then",i[j](function(){var b=a.Deferred(),c=arguments;return 1===c.length&&(c=c[0]),setTimeout(function(){b.resolve(c)},1),b.promise()})[e?j:"done"](e||a.noop)):n[0]},destroy:function(){g.apply(this,arguments),this._widgets=null}}),b.register=d.register=function(b,c){var f,g={init:"init",destroy:"destroy",name:"anonymous"};return 1===arguments.length?(c=b,e.each(c,function(a){return"_"===a[0]||"name"===a?("name"===a&&(g.name=c.name),void 0):(g[a.replace(/[A-Z]/g,"-$&").toLowerCase()]=a,void 0)})):g=e.extend(g,b),c.responseMap=g,f=a.inherits(d,c),f._name=g.name,i.push(f),f},b.unRegister=d.unRegister=function(a){if(a&&"anonymous"!==a)for(var b=i.length;b--;)i[b]._name===a&&i.splice(b,1)},d}),b("widgets/filednd",["base","uploader","lib/dnd","widgets/widget"],function(a,b,c){var d=a.$;return b.options.dnd="",b.register({name:"dnd",init:function(b){if(b.dnd&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{disableGlobalDnd:b.disableGlobalDnd,container:b.dnd,accept:b.accept});return this.dnd=e=new c(h),e.once("ready",g.resolve),e.on("drop",function(a){f.request("add-file",[a])}),e.on("accept",function(a){return f.owner.trigger("dndAccept",a)}),e.init(),g.promise()}},destroy:function(){this.dnd&&this.dnd.destroy()}})}),b("lib/filepaste",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},a),a.container=e(a.container||document.body),c.call(this,"FilePaste")}var e=a.$;return a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/filepaste",["base","uploader","lib/filepaste","widgets/widget"],function(a,b,c){var d=a.$;return b.register({name:"paste",init:function(b){if(b.paste&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{container:b.paste,accept:b.accept});return this.paste=e=new c(h),e.once("ready",g.resolve),e.on("paste",function(a){f.owner.request("add-file",[a])}),e.init(),g.promise()}},destroy:function(){this.paste&&this.paste.destroy()}})}),b("lib/blob",["base","runtime/client"],function(a,b){function c(a,c){var d=this;d.source=c,d.ruid=a,this.size=c.size||0,this.type=!c.type&&this.ext&&~"jpg,jpeg,png,gif,bmp".indexOf(this.ext)?"image/"+("jpg"===this.ext?"jpeg":this.ext):c.type||"application/octet-stream",b.call(d,"Blob"),this.uid=c.uid||this.uid,a&&d.connectRuntime(a)}return a.inherits(b,{constructor:c,slice:function(a,b){return this.exec("slice",a,b)},getSource:function(){return this.source}}),c}),b("lib/file",["base","lib/blob"],function(a,b){function c(a,c){var f;this.name=c.name||"untitled"+d++,f=e.exec(c.name)?RegExp.$1.toLowerCase():"",!f&&c.type&&(f=/\/(jpg|jpeg|png|gif|bmp)$/i.exec(c.type)?RegExp.$1.toLowerCase():"",this.name+="."+f),this.ext=f,this.lastModifiedDate=c.lastModifiedDate||(new Date).toLocaleString(),b.apply(this,arguments)}var d=1,e=/\.([^.]+)$/;return a.inherits(b,c)}),b("lib/filepicker",["base","runtime/client","lib/file"],function(b,c,d){function e(a){if(a=this.options=f.extend({},e.options,a),a.container=f(a.id),!a.container.length)throw new Error("按钮指定错误");a.innerHTML=a.innerHTML||a.label||a.container.html()||"",a.button=f(a.button||document.createElement("div")),a.button.html(a.innerHTML),a.container.html(a.button),c.call(this,"FilePicker",!0)}var f=b.$;return e.options={button:null,container:null,label:null,innerHTML:null,multiple:!0,accept:null,name:"file",style:"webuploader-pick"},b.inherits(c,{constructor:e,init:function(){var c=this,e=c.options,g=e.button,h=e.style;h&&g.addClass("webuploader-pick"),c.on("all",function(a){var b;switch(a){case"mouseenter":h&&g.addClass("webuploader-pick-hover");break;case"mouseleave":h&&g.removeClass("webuploader-pick-hover");break;case"change":b=c.exec("getFiles"),c.trigger("select",f.map(b,function(a){return a=new d(c.getRuid(),a),a._refer=e.container,a}),e.container)}}),c.connectRuntime(e,function(){c.refresh(),c.exec("init",e),c.trigger("ready")}),this._resizeHandler=b.bindFn(this.refresh,this),f(a).on("resize",this._resizeHandler)},refresh:function(){var a=this.getRuntime().getContainer(),b=this.options.button,c=b.outerWidth?b.outerWidth():b.width(),d=b.outerHeight?b.outerHeight():b.height(),e=b.offset();c&&d&&a.css({bottom:"auto",right:"auto",width:c+"px",height:d+"px"}).offset(e)},enable:function(){var a=this.options.button;a.removeClass("webuploader-pick-disable"),this.refresh()},disable:function(){var a=this.options.button;this.getRuntime().getContainer().css({top:"-99999px"}),a.addClass("webuploader-pick-disable")},destroy:function(){var b=this.options.button;f(a).off("resize",this._resizeHandler),b.removeClass("webuploader-pick-disable webuploader-pick-hover webuploader-pick")}}),e}),b("widgets/filepicker",["base","uploader","lib/filepicker","widgets/widget"],function(a,b,c){var d=a.$;return d.extend(b.options,{pick:null,accept:null}),b.register({name:"picker",init:function(a){return this.pickers=[],a.pick&&this.addBtn(a.pick)},refresh:function(){d.each(this.pickers,function(){this.refresh()})},addBtn:function(b){var e=this,f=e.options,g=f.accept,h=[];if(b)return d.isPlainObject(b)||(b={id:b}),d(b.id).each(function(){var i,j,k;k=a.Deferred(),i=d.extend({},b,{accept:d.isPlainObject(g)?[g]:g,swf:f.swf,runtimeOrder:f.runtimeOrder,id:this}),j=new c(i),j.once("ready",k.resolve),j.on("select",function(a){e.owner.request("add-file",[a])}),j.on("dialogopen",function(){e.owner.trigger("dialogOpen",j.button)}),j.init(),e.pickers.push(j),h.push(k.promise())}),a.when.apply(a,h)},disable:function(){d.each(this.pickers,function(){this.disable()})},enable:function(){d.each(this.pickers,function(){this.enable()})},destroy:function(){d.each(this.pickers,function(){this.destroy()}),this.pickers=null}})}),b("file",["base","mediator"],function(a,b){function c(){return f+g++}function d(a){this.name=a.name||"Untitled",this.size=a.size||0,this.type=a.type||"application/octet-stream",this.lastModifiedDate=a.lastModifiedDate||1*new Date,this.id=c(),this.ext=h.exec(this.name)?RegExp.$1:"",this.statusText="",i[this.id]=d.Status.INITED,this.source=a,this.loaded=0,this.on("error",function(a){this.setStatus(d.Status.ERROR,a)})}var e=a.$,f="WU_FILE_",g=0,h=/\.([^.]+)$/,i={};return e.extend(d.prototype,{setStatus:function(a,b){var c=i[this.id];"undefined"!=typeof b&&(this.statusText=b),a!==c&&(i[this.id]=a,this.trigger("statuschange",a,c))},getStatus:function(){return i[this.id]},getSource:function(){return this.source},destroy:function(){this.off(),delete i[this.id]}}),b.installTo(d.prototype),d.Status={INITED:"inited",QUEUED:"queued",PROGRESS:"progress",ERROR:"error",COMPLETE:"complete",CANCELLED:"cancelled",INTERRUPT:"interrupt",INVALID:"invalid"},d}),b("queue",["base","mediator","file"],function(a,b,c){function d(){this.stats={numOfQueue:0,numOfSuccess:0,numOfCancel:0,numOfProgress:0,numOfUploadFailed:0,numOfInvalid:0,numofDeleted:0,numofInterrupt:0},this._queue=[],this._map={}}var e=a.$,f=c.Status;return e.extend(d.prototype,{append:function(a){return this._queue.push(a),this._fileAdded(a),this},prepend:function(a){return this._queue.unshift(a),this._fileAdded(a),this},getFile:function(a){return"string"!=typeof a?a:this._map[a]},fetch:function(a){var b,c,d=this._queue.length;for(a=a||f.QUEUED,b=0;d>b;b++)if(c=this._queue[b],a===c.getStatus())return c;return null},sort:function(a){"function"==typeof a&&this._queue.sort(a)},getFiles:function(){for(var a,b=[].slice.call(arguments,0),c=[],d=0,f=this._queue.length;f>d;d++)a=this._queue[d],(!b.length||~e.inArray(a.getStatus(),b))&&c.push(a);return c},removeFile:function(a){var b=this._map[a.id];b&&(delete this._map[a.id],a.destroy(),this.stats.numofDeleted++)},_fileAdded:function(a){var b=this,c=this._map[a.id];c||(this._map[a.id]=a,a.on("statuschange",function(a,c){b._onFileStatusChange(a,c)}))},_onFileStatusChange:function(a,b){var c=this.stats;switch(b){case f.PROGRESS:c.numOfProgress--;break;case f.QUEUED:c.numOfQueue--;break;case f.ERROR:c.numOfUploadFailed--;break;case f.INVALID:c.numOfInvalid--;break;case f.INTERRUPT:c.numofInterrupt--}switch(a){case f.QUEUED:c.numOfQueue++;break;case f.PROGRESS:c.numOfProgress++;break;case f.ERROR:c.numOfUploadFailed++;break;case f.COMPLETE:c.numOfSuccess++;break;case f.CANCELLED:c.numOfCancel++;break;case f.INVALID:c.numOfInvalid++;break;case f.INTERRUPT:c.numofInterrupt++}}}),b.installTo(d.prototype),d}),b("widgets/queue",["base","uploader","queue","file","lib/file","runtime/client","widgets/widget"],function(a,b,c,d,e,f){var g=a.$,h=/\.\w+$/,i=d.Status;return b.register({name:"queue",init:function(b){var d,e,h,i,j,k,l,m=this;if(g.isPlainObject(b.accept)&&(b.accept=[b.accept]),b.accept){for(j=[],h=0,e=b.accept.length;e>h;h++)i=b.accept[h].extensions,i&&j.push(i);j.length&&(k="\\."+j.join(",").replace(/,/g,"$|\\.").replace(/\*/g,".*")+"$"),m.accept=new RegExp(k,"i")}return m.queue=new c,m.stats=m.queue.stats,"html5"===this.request("predict-runtime-type")?(d=a.Deferred(),this.placeholder=l=new f("Placeholder"),l.connectRuntime({runtimeOrder:"html5"},function(){m._ruid=l.getRuid(),d.resolve()}),d.promise()):void 0},_wrapFile:function(a){if(!(a instanceof d)){if(!(a instanceof e)){if(!this._ruid)throw new Error("Can't add external files.");a=new e(this._ruid,a)}a=new d(a)}return a},acceptFile:function(a){var b=!a||!a.size||this.accept&&h.exec(a.name)&&!this.accept.test(a.name);return!b},_addFile:function(a){var b=this;return a=b._wrapFile(a),b.owner.trigger("beforeFileQueued",a)?b.acceptFile(a)?(b.queue.append(a),b.owner.trigger("fileQueued",a),a):(b.owner.trigger("error","Q_TYPE_DENIED",a),void 0):void 0},getFile:function(a){return this.queue.getFile(a)},addFile:function(a){var b=this;a.length||(a=[a]),a=g.map(a,function(a){return b._addFile(a)}),a.length&&(b.owner.trigger("filesQueued",a),b.options.auto&&setTimeout(function(){b.request("start-upload")},20))},getStats:function(){return this.stats},removeFile:function(a,b){var c=this;a=a.id?a:c.queue.getFile(a),this.request("cancel-file",a),b&&this.queue.removeFile(a)},getFiles:function(){return this.queue.getFiles.apply(this.queue,arguments)},fetchFile:function(){return this.queue.fetch.apply(this.queue,arguments)},retry:function(a,b){var c,d,e,f=this;if(a)return a=a.id?a:f.queue.getFile(a),a.setStatus(i.QUEUED),b||f.request("start-upload"),void 0;for(c=f.queue.getFiles(i.ERROR),d=0,e=c.length;e>d;d++)a=c[d],a.setStatus(i.QUEUED);f.request("start-upload")},sortFiles:function(){return this.queue.sort.apply(this.queue,arguments)},reset:function(){this.owner.trigger("reset"),this.queue=new c,this.stats=this.queue.stats},destroy:function(){this.reset(),this.placeholder&&this.placeholder.destroy()}})}),b("widgets/runtime",["uploader","runtime/runtime","widgets/widget"],function(a,b){return a.support=function(){return b.hasRuntime.apply(b,arguments)},a.register({name:"runtime",init:function(){if(!this.predictRuntimeType())throw Error("Runtime Error")},predictRuntimeType:function(){var a,c,d=this.options.runtimeOrder||b.orders,e=this.type;if(!e)for(d=d.split(/\s*,\s*/g),a=0,c=d.length;c>a;a++)if(b.hasRuntime(d[a])){this.type=e=d[a];break}return e}})}),b("lib/transport",["base","runtime/client","mediator"],function(a,b,c){function d(a){var c=this;a=c.options=e.extend(!0,{},d.options,a||{}),b.call(this,"Transport"),this._blob=null,this._formData=a.formData||{},this._headers=a.headers||{},this.on("progress",this._timeout),this.on("load error",function(){c.trigger("progress",1),clearTimeout(c._timer)})}var e=a.$;return d.options={server:"",method:"POST",withCredentials:!1,fileVal:"file",timeout:12e4,formData:{},headers:{},sendAsBinary:!1},e.extend(d.prototype,{appendBlob:function(a,b,c){var d=this,e=d.options;d.getRuid()&&d.disconnectRuntime(),d.connectRuntime(b.ruid,function(){d.exec("init")}),d._blob=b,e.fileVal=a||e.fileVal,e.filename=c||e.filename},append:function(a,b){"object"==typeof a?e.extend(this._formData,a):this._formData[a]=b},setRequestHeader:function(a,b){"object"==typeof a?e.extend(this._headers,a):this._headers[a]=b},send:function(a){this.exec("send",a),this._timeout()},abort:function(){return clearTimeout(this._timer),this.exec("abort")},destroy:function(){this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()},getResponse:function(){return this.exec("getResponse")},getResponseAsJson:function(){return this.exec("getResponseAsJson")},getStatus:function(){return this.exec("getStatus")},_timeout:function(){var a=this,b=a.options.timeout;b&&(clearTimeout(a._timer),a._timer=setTimeout(function(){a.abort(),a.trigger("error","timeout")},b))}}),c.installTo(d.prototype),d}),b("widgets/upload",["base","uploader","file","lib/transport","widgets/widget"],function(a,b,c,d){function e(a,b){var c,d,e=[],f=a.source,g=f.size,h=b?Math.ceil(g/b):1,i=0,j=0;for(d={file:a,has:function(){return!!e.length},shift:function(){return e.shift()},unshift:function(a){e.unshift(a)}};h>j;)c=Math.min(b,g-i),e.push({file:a,start:i,end:b?i+c:g,total:g,chunks:h,chunk:j++,cuted:d}),i+=c;return a.blocks=e.concat(),a.remaning=e.length,d}var f=a.$,g=a.isPromise,h=c.Status;f.extend(b.options,{prepareNextFile:!1,chunked:!1,chunkSize:5242880,chunkRetry:2,threads:3,formData:{}}),b.register({name:"upload",init:function(){var b=this.owner,c=this;this.runing=!1,this.progress=!1,b.on("startUpload",function(){c.progress=!0}).on("uploadFinished",function(){c.progress=!1}),this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this.__tick=a.bindFn(this._tick,this),b.on("uploadComplete",function(a){a.blocks&&f.each(a.blocks,function(a,b){b.transport&&(b.transport.abort(),b.transport.destroy()),delete b.transport}),delete a.blocks,delete a.remaning})},reset:function(){this.request("stop-upload",!0),this.runing=!1,this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this._trigged=!1,this._promise=null},startUpload:function(b){var c=this;if(f.each(c.request("get-files",h.INVALID),function(){c.request("remove-file",this)}),b?(b=b.id?b:c.request("get-file",b),b.getStatus()===h.INTERRUPT?(b.setStatus(h.QUEUED),f.each(c.pool,function(a,c){c.file===b&&(c.transport&&c.transport.send(),b.setStatus(h.PROGRESS))})):b.getStatus()!==h.PROGRESS&&b.setStatus(h.QUEUED)):f.each(c.request("get-files",[h.INITED]),function(){this.setStatus(h.QUEUED)}),c.runing)return a.nextTick(c.__tick);c.runing=!0;var d=[];b||f.each(c.pool,function(a,b){var e=b.file;e.getStatus()===h.INTERRUPT&&(c._trigged=!1,d.push(e),b.transport&&b.transport.send())}),f.each(d,function(){this.setStatus(h.PROGRESS)}),b||f.each(c.request("get-files",h.INTERRUPT),function(){this.setStatus(h.PROGRESS)}),c._trigged=!1,a.nextTick(c.__tick),c.owner.trigger("startUpload")},stopUpload:function(b,c){var d,e=this;if(b===!0&&(c=b,b=null),e.runing!==!1){if(b){if(b=b.id?b:e.request("get-file",b),b.getStatus()!==h.PROGRESS&&b.getStatus()!==h.QUEUED)return;return b.setStatus(h.INTERRUPT),f.each(e.pool,function(a,c){return c.file===b?(d=c,!1):void 0}),d.transport&&d.transport.abort(),c&&(e._putback(d),e._popBlock(d)),a.nextTick(e.__tick)}e.runing=!1,this._promise&&this._promise.file&&this._promise.file.setStatus(h.INTERRUPT),c&&f.each(e.pool,function(a,b){b.transport&&b.transport.abort(),b.file.setStatus(h.INTERRUPT)}),e.owner.trigger("stopUpload")}},cancelFile:function(a){a=a.id?a:this.request("get-file",a),a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),a.setStatus(h.CANCELLED),this.owner.trigger("fileDequeued",a)},isInProgress:function(){return!!this.progress},_getStats:function(){return this.request("get-stats")},skipFile:function(a,b){a=a.id?a:this.request("get-file",a),a.setStatus(b||h.COMPLETE),a.skipped=!0,a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),this.owner.trigger("uploadSkip",a)},_tick:function(){var b,c,d=this,e=d.options;return d._promise?d._promise.always(d.__tick):(d.pool.length1&&~"http,abort".indexOf(a)&&b.retried1&&f.extend(m,{chunks:b.chunks,chunk:b.chunk}),i.trigger("uploadBeforeSend",b,m,n),l.appendBlob(j.fileVal,b.blob,k.name),l.append(m),l.setRequestHeader(n),l.send()},_finishFile:function(a,b,c){var d=this.owner;return d.request("after-send-file",arguments,function(){a.setStatus(h.COMPLETE),d.trigger("uploadSuccess",a,b,c)}).fail(function(b){a.getStatus()===h.PROGRESS&&a.setStatus(h.ERROR,b),d.trigger("uploadError",a,b)}).always(function(){d.trigger("uploadComplete",a)})},updateFileProgress:function(a){var b=0,c=0;a.blocks&&(f.each(a.blocks,function(a,b){c+=(b.percentage||0)*(b.end-b.start)}),b=c/a.size,this.owner.trigger("uploadProgress",a,b||0))}})}),b("widgets/validator",["base","uploader","file","widgets/widget"],function(a,b,c){var d,e=a.$,f={};return d={addValidator:function(a,b){f[a]=b},removeValidator:function(a){delete f[a]}},b.register({name:"validator",init:function(){var b=this;a.nextTick(function(){e.each(f,function(){this.call(b.owner)})})}}),d.addValidator("fileNumLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileNumLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){return c>=d&&e&&(e=!1,this.trigger("error","Q_EXCEED_NUM_LIMIT",d,a),setTimeout(function(){e=!0},1)),c>=d?!1:!0}),a.on("fileQueued",function(){c++}),a.on("fileDequeued",function(){c--}),a.on("reset",function(){c=0}))}),d.addValidator("fileSizeLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileSizeLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){var b=c+a.size>d;return b&&e&&(e=!1,this.trigger("error","Q_EXCEED_SIZE_LIMIT",d,a),setTimeout(function(){e=!0},1)),b?!1:!0}),a.on("fileQueued",function(a){c+=a.size}),a.on("fileDequeued",function(a){c-=a.size}),a.on("reset",function(){c=0}))}),d.addValidator("fileSingleSizeLimit",function(){var a=this,b=a.options,d=b.fileSingleSizeLimit;d&&a.on("beforeFileQueued",function(a){return a.size>d?(a.setStatus(c.Status.INVALID,"exceed_size"),this.trigger("error","F_EXCEED_SIZE",d,a),!1):void 0})}),d.addValidator("duplicate",function(){function a(a){for(var b,c=0,d=0,e=a.length;e>d;d++)b=a.charCodeAt(d),c=b+(c<<6)+(c<<16)-c;return c}var b=this,c=b.options,d={};c.duplicate||(b.on("beforeFileQueued",function(b){var c=b.__hash||(b.__hash=a(b.name+b.size+b.lastModifiedDate));return d[c]?(this.trigger("error","F_DUPLICATE",b),!1):void 0}),b.on("fileQueued",function(a){var b=a.__hash;b&&(d[b]=!0)}),b.on("fileDequeued",function(a){var b=a.__hash;b&&delete d[b]}),b.on("reset",function(){d={}}))}),d}),b("runtime/compbase",[],function(){function a(a,b){this.owner=a,this.options=a.options,this.getRuntime=function(){return b},this.getRuid=function(){return b.uid},this.trigger=function(){return a.trigger.apply(a,arguments)}}return a}),b("runtime/html5/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a={},d=this,e=this.destroy;c.apply(d,arguments),d.type=f,d.exec=function(c,e){var f,h=this,i=h.uid,j=b.slice(arguments,2);return g[c]&&(f=a[i]=a[i]||new g[c](h,d),f[e])?f[e].apply(f,j):void 0},d.destroy=function(){return e&&e.apply(this,arguments) +}}var f="html5",g={};return b.inherits(c,{constructor:e,init:function(){var a=this;setTimeout(function(){a.trigger("ready")},1)}}),e.register=function(a,c){var e=g[a]=b.inherits(d,c);return e},a.Blob&&a.FileReader&&a.DataView&&c.addRuntime(f,e),e}),b("runtime/html5/blob",["runtime/html5/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.owner.source,e=d.slice||d.webkitSlice||d.mozSlice;return d=e.call(d,a,c),new b(this.getRuid(),d)}})}),b("runtime/html5/dnd",["base","runtime/html5/runtime","lib/file"],function(a,b,c){var d=a.$,e="webuploader-dnd-";return b.register("DragAndDrop",{init:function(){var b=this.elem=this.options.container;this.dragEnterHandler=a.bindFn(this._dragEnterHandler,this),this.dragOverHandler=a.bindFn(this._dragOverHandler,this),this.dragLeaveHandler=a.bindFn(this._dragLeaveHandler,this),this.dropHandler=a.bindFn(this._dropHandler,this),this.dndOver=!1,b.on("dragenter",this.dragEnterHandler),b.on("dragover",this.dragOverHandler),b.on("dragleave",this.dragLeaveHandler),b.on("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).on("dragover",this.dragOverHandler),d(document).on("drop",this.dropHandler))},_dragEnterHandler:function(a){var b,c=this,d=c._denied||!1;return a=a.originalEvent||a,c.dndOver||(c.dndOver=!0,b=a.dataTransfer.items,b&&b.length&&(c._denied=d=!c.trigger("accept",b)),c.elem.addClass(e+"over"),c.elem[d?"addClass":"removeClass"](e+"denied")),a.dataTransfer.dropEffect=d?"none":"copy",!1},_dragOverHandler:function(a){var b=this.elem.parent().get(0);return b&&!d.contains(b,a.currentTarget)?!1:(clearTimeout(this._leaveTimer),this._dragEnterHandler.call(this,a),!1)},_dragLeaveHandler:function(){var a,b=this;return a=function(){b.dndOver=!1,b.elem.removeClass(e+"over "+e+"denied")},clearTimeout(b._leaveTimer),b._leaveTimer=setTimeout(a,100),!1},_dropHandler:function(a){var b,f,g=this,h=g.getRuid(),i=g.elem.parent().get(0);if(i&&!d.contains(i,a.currentTarget))return!1;a=a.originalEvent||a,b=a.dataTransfer;try{f=b.getData("text/html")}catch(j){}return g.dndOver=!1,g.elem.removeClass(e+"over"),f?void 0:(g._getTansferFiles(b,function(a){g.trigger("drop",d.map(a,function(a){return new c(h,a)}))}),!1)},_getTansferFiles:function(b,c){var d,e,f,g,h,i,j,k=[],l=[];for(d=b.items,e=b.files,j=!(!d||!d[0].webkitGetAsEntry),h=0,i=e.length;i>h;h++)f=e[h],g=d&&d[h],j&&g.webkitGetAsEntry().isDirectory?l.push(this._traverseDirectoryTree(g.webkitGetAsEntry(),k)):k.push(f);a.when.apply(a,l).done(function(){k.length&&c(k)})},_traverseDirectoryTree:function(b,c){var d=a.Deferred(),e=this;return b.isFile?b.file(function(a){c.push(a),d.resolve()}):b.isDirectory&&b.createReader().readEntries(function(b){var f,g=b.length,h=[],i=[];for(f=0;g>f;f++)h.push(e._traverseDirectoryTree(b[f],i));a.when.apply(a,h).then(function(){c.push.apply(c,i),d.resolve()},d.reject)}),d.promise()},destroy:function(){var a=this.elem;a&&(a.off("dragenter",this.dragEnterHandler),a.off("dragover",this.dragOverHandler),a.off("dragleave",this.dragLeaveHandler),a.off("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).off("dragover",this.dragOverHandler),d(document).off("drop",this.dropHandler)))}})}),b("runtime/html5/filepaste",["base","runtime/html5/runtime","lib/file"],function(a,b,c){return b.register("FilePaste",{init:function(){var b,c,d,e,f=this.options,g=this.elem=f.container,h=".*";if(f.accept){for(b=[],c=0,d=f.accept.length;d>c;c++)e=f.accept[c].mimeTypes,e&&b.push(e);b.length&&(h=b.join(","),h=h.replace(/,/g,"|").replace(/\*/g,".*"))}this.accept=h=new RegExp(h,"i"),this.hander=a.bindFn(this._pasteHander,this),g.on("paste",this.hander)},_pasteHander:function(a){var b,d,e,f,g,h=[],i=this.getRuid();for(a=a.originalEvent||a,b=a.clipboardData.items,f=0,g=b.length;g>f;f++)d=b[f],"file"===d.kind&&(e=d.getAsFile())&&h.push(new c(i,e));h.length&&(a.preventDefault(),a.stopPropagation(),this.trigger("paste",h))},destroy:function(){this.elem.off("paste",this.hander)}})}),b("runtime/html5/filepicker",["base","runtime/html5/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(){var a,b,d,e,f=this.getRuntime().getContainer(),g=this,h=g.owner,i=g.options,j=this.label=c(document.createElement("label")),k=this.input=c(document.createElement("input"));if(k.attr("type","file"),k.attr("capture","camera"),k.attr("name",i.name),k.addClass("webuploader-element-invisible"),j.on("click",function(a){k.trigger("click"),a.stopPropagation(),h.trigger("dialogopen")}),j.css({opacity:0,width:"100%",height:"100%",display:"block",cursor:"pointer",background:"#ffffff"}),i.multiple&&k.attr("multiple","multiple"),i.accept&&i.accept.length>0){for(a=[],b=0,d=i.accept.length;d>b;b++)a.push(i.accept[b].mimeTypes);k.attr("accept",a.join(","))}f.append(k),f.append(j),e=function(a){h.trigger(a.type)},k.on("change",function(a){var b,d=arguments.callee;g.files=a.target.files,b=this.cloneNode(!0),b.value=null,this.parentNode.replaceChild(b,this),k.off(),k=c(b).on("change",d).on("mouseenter mouseleave",e),h.trigger("change")}),j.on("mouseenter mouseleave",e)},getFiles:function(){return this.files},destroy:function(){this.input.off(),this.label.off()}})}),b("runtime/html5/transport",["base","runtime/html5/runtime"],function(a,b){var c=a.noop,d=a.$;return b.register("Transport",{init:function(){this._status=0,this._response=null},send:function(){var b,c,e,f=this.owner,g=this.options,h=this._initAjax(),i=f._blob,j=g.server;g.sendAsBinary?(j+=(/\?/.test(j)?"&":"?")+d.param(f._formData),c=i.getSource()):(b=new FormData,d.each(f._formData,function(a,c){b.append(a,c)}),b.append(g.fileVal,i.getSource(),g.filename||f._formData.name||"")),g.withCredentials&&"withCredentials"in h?(h.open(g.method,j,!0),h.withCredentials=!0):h.open(g.method,j),this._setRequestHeader(h,g.headers),c?(h.overrideMimeType&&h.overrideMimeType("application/octet-stream"),a.os.android?(e=new FileReader,e.onload=function(){h.send(this.result),e=e.onload=null},e.readAsArrayBuffer(c)):h.send(c)):h.send(b)},getResponse:function(){return this._response},getResponseAsJson:function(){return this._parseJson(this._response)},getStatus:function(){return this._status},abort:function(){var a=this._xhr;a&&(a.upload.onprogress=c,a.onreadystatechange=c,a.abort(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var a=this,b=new XMLHttpRequest,d=this.options;return!d.withCredentials||"withCredentials"in b||"undefined"==typeof XDomainRequest||(b=new XDomainRequest),b.upload.onprogress=function(b){var c=0;return b.lengthComputable&&(c=b.loaded/b.total),a.trigger("progress",c)},b.onreadystatechange=function(){return 4===b.readyState?(b.upload.onprogress=c,b.onreadystatechange=c,a._xhr=null,a._status=b.status,b.status>=200&&b.status<300?(a._response=b.responseText,a.trigger("load")):b.status>=500&&b.status<600?(a._response=b.responseText,a.trigger("error","server")):a.trigger("error",a._status?"http":"abort")):void 0},a._xhr=b,b},_setRequestHeader:function(a,b){d.each(b,function(b,c){a.setRequestHeader(b,c)})},_parseJson:function(a){var b;try{b=JSON.parse(a)}catch(c){b={}}return b}})}),b("runtime/flash/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a;try{a=navigator.plugins["Shockwave Flash"],a=a.description}catch(b){try{a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash").GetVariable("$version")}catch(c){a="0.0"}}return a=a.match(/\d+/g),parseFloat(a[0]+"."+a[1],10)}function f(){function d(a,b){var c,d,e=a.type||a;c=e.split("::"),d=c[0],e=c[1],"Ready"===e&&d===j.uid?j.trigger("ready"):f[d]&&f[d].trigger(e.toLowerCase(),a,b)}var e={},f={},g=this.destroy,j=this,k=b.guid("webuploader_");c.apply(j,arguments),j.type=h,j.exec=function(a,c){var d,g=this,h=g.uid,k=b.slice(arguments,2);return f[h]=g,i[a]&&(e[h]||(e[h]=new i[a](g,j)),d=e[h],d[c])?d[c].apply(d,k):j.flashExec.apply(g,arguments)},a[k]=function(){var a=arguments;setTimeout(function(){d.apply(null,a)},1)},this.jsreciver=k,this.destroy=function(){return g&&g.apply(this,arguments)},this.flashExec=function(a,c){var d=j.getFlash(),e=b.slice(arguments,2);return d.exec(this.uid,a,c,e)}}var g=b.$,h="flash",i={};return b.inherits(c,{constructor:f,init:function(){var a,c=this.getContainer(),d=this.options;c.css({position:"absolute",top:"-8px",left:"-8px",width:"9px",height:"9px",overflow:"hidden"}),a=''+''+''+''+"",c.html(a)},getFlash:function(){return this._flash?this._flash:(this._flash=g("#"+this.uid).get(0),this._flash)}}),f.register=function(a,c){return c=i[a]=b.inherits(d,g.extend({flashExec:function(){var a=this.owner,b=this.getRuntime();return b.flashExec.apply(a,arguments)}},c))},e()>=11.4&&c.addRuntime(h,f),f}),b("runtime/flash/filepicker",["base","runtime/flash/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(a){var b,d,e=c.extend({},a);for(b=e.accept&&e.accept.length,d=0;b>d;d++)e.accept[d].title||(e.accept[d].title="Files");delete e.button,delete e.id,delete e.container,this.flashExec("FilePicker","init",e)},destroy:function(){this.flashExec("FilePicker","destroy")}})}),b("runtime/flash/transport",["base","runtime/flash/runtime","runtime/client"],function(b,c,d){var e=b.$;return c.register("Transport",{init:function(){this._status=0,this._response=null,this._responseJson=null},send:function(){var a,b=this.owner,c=this.options,d=this._initAjax(),f=b._blob,g=c.server;d.connectRuntime(f.ruid),c.sendAsBinary?(g+=(/\?/.test(g)?"&":"?")+e.param(b._formData),a=f.uid):(e.each(b._formData,function(a,b){d.exec("append",a,b)}),d.exec("appendBlob",c.fileVal,f.uid,c.filename||b._formData.name||"")),this._setRequestHeader(d,c.headers),d.exec("send",{method:c.method,url:g,forceURLStream:c.forceURLStream,mimeType:"application/octet-stream"},a)},getStatus:function(){return this._status},getResponse:function(){return this._response||""},getResponseAsJson:function(){return this._responseJson},abort:function(){var a=this._xhr;a&&(a.exec("abort"),a.destroy(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var b=this,c=new d("XMLHttpRequest");return c.on("uploadprogress progress",function(a){var c=a.loaded/a.total;return c=Math.min(1,Math.max(0,c)),b.trigger("progress",c)}),c.on("load",function(){var d,e=c.exec("getStatus"),f=!1,g="";return c.off(),b._xhr=null,e>=200&&300>e?f=!0:e>=500&&600>e?(f=!0,g="server"):g="http",f&&(b._response=c.exec("getResponse"),b._response=decodeURIComponent(b._response),d=function(b){try{return a.JSON&&a.JSON.parse?JSON.parse(b):new Function("return "+b).call()}catch(c){return{}}},b._responseJson=b._response?d(b._response):{}),c.destroy(),c=null,g?b.trigger("error",g):b.trigger("load")}),c.on("error",function(){c.off(),b._xhr=null,b.trigger("error","http")}),b._xhr=c,c},_setRequestHeader:function(a,b){e.each(b,function(b,c){a.exec("setRequestHeader",b,c)})}})}),b("runtime/flash/blob",["runtime/flash/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.flashExec("Blob","slice",a,c);return new b(this.getRuid(),d)}})}),b("preset/withoutimage",["base","widgets/filednd","widgets/filepaste","widgets/filepicker","widgets/queue","widgets/runtime","widgets/upload","widgets/validator","runtime/html5/blob","runtime/html5/dnd","runtime/html5/filepaste","runtime/html5/filepicker","runtime/html5/transport","runtime/flash/filepicker","runtime/flash/transport","runtime/flash/blob"],function(a){return a}),b("webuploader",["preset/withoutimage"],function(a){return a}),c("webuploader")}); \ No newline at end of file diff --git a/public/static/libs/webuploader/webuploader.nolog.js b/public/static/libs/webuploader/webuploader.nolog.js new file mode 100644 index 0000000..e68d592 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.nolog.js @@ -0,0 +1,8046 @@ +/*! WebUploader 0.1.6 */ + + +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + * @require "jquery" + * @require "zepto" + */ + define('dollar-third',[],function() { + var req = window.require; + var $ = window.__dollar || + window.jQuery || + window.Zepto || + req('jquery') || + req('zepto'); + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 使用jQuery的Promise + */ + define('promise-third',[ + 'dollar' + ], function( $ ) { + return { + Deferred: $.Deferred, + when: $.when, + + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + } + }; + }); + /** + * @fileOverview Promise/A+ + */ + define('promise',[ + 'promise-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.6', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview 错误信息 + */ + define('lib/dnd',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function DragAndDrop( opts ) { + opts = this.options = $.extend({}, DragAndDrop.options, opts ); + + opts.container = $( opts.container ); + + if ( !opts.container.length ) { + return; + } + + RuntimeClent.call( this, 'DragAndDrop' ); + } + + DragAndDrop.options = { + accept: null, + disableGlobalDnd: false + }; + + Base.inherits( RuntimeClent, { + constructor: DragAndDrop, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( DragAndDrop.prototype ); + + return DragAndDrop; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview DragAndDrop Widget。 + */ + define('widgets/filednd',[ + 'base', + 'uploader', + 'lib/dnd', + 'widgets/widget' + ], function( Base, Uploader, Dnd ) { + var $ = Base.$; + + Uploader.options.dnd = ''; + + /** + * @property {Selector} [dnd=undefined] 指定Drag And Drop拖拽的容器,如果不指定,则不启动。 + * @namespace options + * @for Uploader + */ + + /** + * @property {Selector} [disableGlobalDnd=false] 是否禁掉整个页面的拖拽功能,如果不禁用,图片拖进来的时候会默认被浏览器打开。 + * @namespace options + * @for Uploader + */ + + /** + * @event dndAccept + * @param {DataTransferItemList} items DataTransferItem + * @description 阻止此事件可以拒绝某些类型的文件拖入进来。目前只有 chrome 提供这样的 API,且只能通过 mime-type 验证。 + * @for Uploader + */ + return Uploader.register({ + name: 'dnd', + + init: function( opts ) { + + if ( !opts.dnd || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + disableGlobalDnd: opts.disableGlobalDnd, + container: opts.dnd, + accept: opts.accept + }), + dnd; + + this.dnd = dnd = new Dnd( options ); + + dnd.once( 'ready', deferred.resolve ); + dnd.on( 'drop', function( files ) { + me.request( 'add-file', [ files ]); + }); + + // 检测文件是否全部允许添加。 + dnd.on( 'accept', function( items ) { + return me.owner.trigger( 'dndAccept', items ); + }); + + dnd.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.dnd && this.dnd.destroy(); + } + }); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepaste',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function FilePaste( opts ) { + opts = this.options = $.extend({}, opts ); + opts.container = $( opts.container || document.body ); + RuntimeClent.call( this, 'FilePaste' ); + } + + Base.inherits( RuntimeClent, { + constructor: FilePaste, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( FilePaste.prototype ); + + return FilePaste; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/filepaste',[ + 'base', + 'uploader', + 'lib/filepaste', + 'widgets/widget' + ], function( Base, Uploader, FilePaste ) { + var $ = Base.$; + + /** + * @property {Selector} [paste=undefined] 指定监听paste事件的容器,如果不指定,不启用此功能。此功能为通过粘贴来添加截屏的图片。建议设置为`document.body`. + * @namespace options + * @for Uploader + */ + return Uploader.register({ + name: 'paste', + + init: function( opts ) { + + if ( !opts.paste || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + container: opts.paste, + accept: opts.accept + }), + paste; + + this.paste = paste = new FilePaste( options ); + + paste.once( 'ready', deferred.resolve ); + paste.on( 'paste', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + paste.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.paste && this.paste.destroy(); + } + }); + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClient, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClient.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file', + style: 'webuploader-pick' //pick element class attribute, default is "webuploader-pick" + }; + + Base.inherits( RuntimeClient, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button, + style = opts.style; + + if (style) + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + if (style) + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + if (style) + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addBtn + * @for Uploader + * @grammar addBtn( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addBtn({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.on('dialogopen', function() { + me.owner.trigger('dialogOpen', picker.button); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview Image + */ + define('lib/image',[ + 'base', + 'runtime/client', + 'lib/blob' + ], function( Base, RuntimeClient, Blob ) { + var $ = Base.$; + + // 构造器。 + function Image( opts ) { + this.options = $.extend({}, Image.options, opts ); + RuntimeClient.call( this, 'Image' ); + + this.on( 'load', function() { + this._info = this.exec('info'); + this._meta = this.exec('meta'); + }); + } + + // 默认选项。 + Image.options = { + + // 默认的图片处理质量 + quality: 90, + + // 是否裁剪 + crop: false, + + // 是否保留头部信息 + preserveHeaders: false, + + // 是否允许放大。 + allowMagnify: false + }; + + // 继承RuntimeClient. + Base.inherits( RuntimeClient, { + constructor: Image, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + loadFromBlob: function( blob ) { + var me = this, + ruid = blob.getRuid(); + + this.connectRuntime( ruid, function() { + me.exec( 'init', me.options ); + me.exec( 'loadFromBlob', blob ); + }); + }, + + resize: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'resize' ].concat( args ) ); + }, + + crop: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'crop' ].concat( args ) ); + }, + + getAsDataUrl: function( type ) { + return this.exec( 'getAsDataUrl', type ); + }, + + getAsBlob: function( type ) { + var blob = this.exec( 'getAsBlob', type ); + + return new Blob( this.getRuid(), blob ); + } + }); + + return Image; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/image',[ + 'base', + 'uploader', + 'lib/image', + 'widgets/widget' + ], function( Base, Uploader, Image ) { + + var $ = Base.$, + throttle; + + // 根据要处理的文件大小来节流,一次不能处理太多,会卡。 + throttle = (function( max ) { + var occupied = 0, + waiting = [], + tick = function() { + var item; + + while ( waiting.length && occupied < max ) { + item = waiting.shift(); + occupied += item[ 0 ]; + item[ 1 ](); + } + }; + + return function( emiter, size, cb ) { + waiting.push([ size, cb ]); + emiter.once( 'destroy', function() { + occupied -= size; + setTimeout( tick, 1 ); + }); + setTimeout( tick, 1 ); + }; + })( 5 * 1024 * 1024 ); + + $.extend( Uploader.options, { + + /** + * @property {Object} [thumb] + * @namespace options + * @for Uploader + * @description 配置生成缩略图的选项。 + * + * 默认为: + * + * ```javascript + * { + * width: 110, + * height: 110, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 70, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: true, + * + * // 是否允许裁剪。 + * crop: true, + * + * // 为空的话则保留原有图片格式。 + * // 否则强制转换成指定的类型。 + * type: 'image/jpeg' + * } + * ``` + */ + thumb: { + width: 110, + height: 110, + quality: 70, + allowMagnify: true, + crop: true, + preserveHeaders: false, + + // 为空的话则保留原有图片格式。 + // 否则强制转换成指定的类型。 + // IE 8下面 base64 大小不能超过 32K 否则预览失败,而非 jpeg 编码的图片很可 + // 能会超过 32k, 所以这里设置成预览的时候都是 image/jpeg + type: 'image/jpeg' + }, + + /** + * @property {Object} [compress] + * @namespace options + * @for Uploader + * @description 配置压缩的图片的选项。如果此选项为`false`, 则图片在上传前不进行压缩。 + * + * 默认为: + * + * ```javascript + * { + * width: 1600, + * height: 1600, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 90, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: false, + * + * // 是否允许裁剪。 + * crop: false, + * + * // 是否保留头部meta信息。 + * preserveHeaders: true, + * + * // 如果发现压缩后文件大小比原来还大,则使用原来图片 + * // 此属性可能会影响图片自动纠正功能 + * noCompressIfLarger: false, + * + * // 单位字节,如果图片大小小于此值,不会采用压缩。 + * compressSize: 0 + * } + * ``` + */ + compress: { + width: 1600, + height: 1600, + quality: 90, + allowMagnify: false, + crop: false, + preserveHeaders: true + } + }); + + return Uploader.register({ + + name: 'image', + + + /** + * 生成缩略图,此过程为异步,所以需要传入`callback`。 + * 通常情况在图片加入队里后调用此方法来生成预览图以增强交互效果。 + * + * 当 width 或者 height 的值介于 0 - 1 时,被当成百分比使用。 + * + * `callback`中可以接收到两个参数。 + * * 第一个为error,如果生成缩略图有错误,此error将为真。 + * * 第二个为ret, 缩略图的Data URL值。 + * + * **注意** + * Date URL在IE6/7中不支持,所以不用调用此方法了,直接显示一张暂不支持预览图片好了。 + * 也可以借助服务端,将 base64 数据传给服务端,生成一个临时文件供预览。 + * + * @method makeThumb + * @grammar makeThumb( file, callback ) => undefined + * @grammar makeThumb( file, callback, width, height ) => undefined + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.makeThumb( file, function( error, ret ) { + * if ( error ) { + * $li.text('预览错误'); + * } else { + * $li.append(''); + * } + * }); + * + * }); + */ + makeThumb: function( file, cb, width, height ) { + var opts, image; + + file = this.request( 'get-file', file ); + + // 只预览图片格式。 + if ( !file.type.match( /^image/ ) ) { + cb( true ); + return; + } + + opts = $.extend({}, this.options.thumb ); + + // 如果传入的是object. + if ( $.isPlainObject( width ) ) { + opts = $.extend( opts, width ); + width = null; + } + + width = width || opts.width; + height = height || opts.height; + + image = new Image( opts ); + + image.once( 'load', function() { + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + // 当 resize 完后 + image.once( 'complete', function() { + cb( false, image.getAsDataUrl( opts.type ) ); + image.destroy(); + }); + + image.once( 'error', function( reason ) { + cb( reason || true ); + image.destroy(); + }); + + throttle( image, file.source.size, function() { + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + image.loadFromBlob( file.source ); + }); + }, + + beforeSendFile: function( file ) { + var opts = this.options.compress || this.options.resize, + compressSize = opts && opts.compressSize || 0, + noCompressIfLarger = opts && opts.noCompressIfLarger || false, + image, deferred; + + file = this.request( 'get-file', file ); + + // 只压缩 jpeg 图片格式。 + // gif 可能会丢失针 + // bmp png 基本上尺寸都不大,且压缩比比较小。 + if ( !opts || !~'image/jpeg,image/jpg'.indexOf( file.type ) || + file.size < compressSize || + file._compressed ) { + return; + } + + opts = $.extend({}, opts ); + deferred = Base.Deferred(); + + image = new Image( opts ); + + deferred.always(function() { + image.destroy(); + image = null; + }); + image.once( 'error', deferred.reject ); + image.once( 'load', function() { + var width = opts.width, + height = opts.height; + + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + image.once( 'complete', function() { + var blob, size; + + // 移动端 UC / qq 浏览器的无图模式下 + // ctx.getImageData 处理大图的时候会报 Exception + // INDEX_SIZE_ERR: DOM Exception 1 + try { + blob = image.getAsBlob( opts.type ); + + size = file.size; + + // 如果压缩后,比原来还大则不用压缩后的。 + if ( !noCompressIfLarger || blob.size < size ) { + // file.source.destroy && file.source.destroy(); + file.source = blob; + file.size = blob.size; + + file.trigger( 'resize', blob.size, size ); + } + + // 标记,避免重复压缩。 + file._compressed = true; + deferred.resolve(); + } catch ( e ) { + // 出错了直接继续,让其上传原始图片 + deferred.resolve(); + } + }); + + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + + image.loadFromBlob( file.source ); + return deferred.promise(); + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + if ( files.length ) { + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + // 销毁上传相关的属性。 + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定的文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + file.setStatus( Status.QUEUED ); + + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + file.setStatus( Status.PROGRESS ); + }); + + + } else if (file.getStatus() !== Status.PROGRESS) { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return Base.nextTick( me.__tick ); + } + + me.runing = true; + var files = []; + + // 如果有暂停的,则续传 + file || $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + me._trigged = false; + files.push(file); + v.transport && v.transport.send(); + } + }); + + $.each(files, function() { + this.setStatus( Status.PROGRESS ); + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this, + block; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + + + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file === file) { + block = v; + return false; + } + }); + + block.transport && block.transport.abort(); + + if (interrupt) { + me._putback(block); + me._popBlock(block); + } + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + // 正在准备中的文件。 + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + + /** + * @fileOverview Md5 + */ + define('lib/md5',[ + 'runtime/client', + 'mediator' + ], function( RuntimeClient, Mediator ) { + + function Md5() { + RuntimeClient.call( this, 'Md5' ); + } + + // 让 Md5 具备事件功能。 + Mediator.installTo( Md5.prototype ); + + Md5.prototype.loadFromBlob = function( blob ) { + var me = this; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + me.exec( 'loadFromBlob', blob ); + }); + }; + + Md5.prototype.getResult = function() { + return this.exec('getResult'); + }; + + return Md5; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/md5',[ + 'base', + 'uploader', + 'lib/md5', + 'lib/blob', + 'widgets/widget' + ], function( Base, Uploader, Md5, Blob ) { + + return Uploader.register({ + name: 'md5', + + + /** + * 计算文件 md5 值,返回一个 promise 对象,可以监听 progress 进度。 + * + * + * @method md5File + * @grammar md5File( file[, start[, end]] ) => promise + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.md5File( file ) + * + * // 及时显示进度 + * .progress(function(percentage) { + * console.log('Percentage:', percentage); + * }) + * + * // 完成 + * .then(function(val) { + * console.log('md5 result:', val); + * }); + * + * }); + */ + md5File: function( file, start, end ) { + var md5 = new Md5(), + deferred = Base.Deferred(), + blob = (file instanceof Blob) ? file : + this.request( 'get-file', file ).source; + + md5.on( 'progress load', function( e ) { + e = e || {}; + deferred.notify( e.total ? e.loaded / e.total : 1 ); + }); + + md5.on( 'complete', function() { + deferred.resolve( md5.getResult() ); + }); + + md5.on( 'error', function( reason ) { + deferred.reject( reason ); + }); + + if ( arguments.length > 1 ) { + start = start || 0; + end = end || 0; + start < 0 && (start = blob.size + start); + end < 0 && (end = blob.size + end); + end = Math.min( end, blob.size ); + blob = blob.slice( start, end ); + } + + md5.loadFromBlob( blob ); + + return deferred.promise(); + } + }); + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePaste + */ + define('runtime/html5/dnd',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + var $ = Base.$, + prefix = 'webuploader-dnd-'; + + return Html5Runtime.register( 'DragAndDrop', { + init: function() { + var elem = this.elem = this.options.container; + + this.dragEnterHandler = Base.bindFn( this._dragEnterHandler, this ); + this.dragOverHandler = Base.bindFn( this._dragOverHandler, this ); + this.dragLeaveHandler = Base.bindFn( this._dragLeaveHandler, this ); + this.dropHandler = Base.bindFn( this._dropHandler, this ); + this.dndOver = false; + + elem.on( 'dragenter', this.dragEnterHandler ); + elem.on( 'dragover', this.dragOverHandler ); + elem.on( 'dragleave', this.dragLeaveHandler ); + elem.on( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).on( 'dragover', this.dragOverHandler ); + $( document ).on( 'drop', this.dropHandler ); + } + }, + + _dragEnterHandler: function( e ) { + var me = this, + denied = me._denied || false, + items; + + e = e.originalEvent || e; + + if ( !me.dndOver ) { + me.dndOver = true; + + // 注意只有 chrome 支持。 + items = e.dataTransfer.items; + + if ( items && items.length ) { + me._denied = denied = !me.trigger( 'accept', items ); + } + + me.elem.addClass( prefix + 'over' ); + me.elem[ denied ? 'addClass' : + 'removeClass' ]( prefix + 'denied' ); + } + + e.dataTransfer.dropEffect = denied ? 'none' : 'copy'; + + return false; + }, + + _dragOverHandler: function( e ) { + // 只处理框内的。 + var parentElem = this.elem.parent().get( 0 ); + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + clearTimeout( this._leaveTimer ); + this._dragEnterHandler.call( this, e ); + + return false; + }, + + _dragLeaveHandler: function() { + var me = this, + handler; + + handler = function() { + me.dndOver = false; + me.elem.removeClass( prefix + 'over ' + prefix + 'denied' ); + }; + + clearTimeout( me._leaveTimer ); + me._leaveTimer = setTimeout( handler, 100 ); + return false; + }, + + _dropHandler: function( e ) { + var me = this, + ruid = me.getRuid(), + parentElem = me.elem.parent().get( 0 ), + dataTransfer, data; + + // 只处理框内的。 + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + e = e.originalEvent || e; + dataTransfer = e.dataTransfer; + + // 如果是页面内拖拽,还不能处理,不阻止事件。 + // 此处 ie11 下会报参数错误, + try { + data = dataTransfer.getData('text/html'); + } catch( err ) { + } + + me.dndOver = false; + me.elem.removeClass( prefix + 'over' ); + + if ( data ) { + return; + } + + me._getTansferFiles( dataTransfer, function( results ) { + me.trigger( 'drop', $.map( results, function( file ) { + return new File( ruid, file ); + }) ); + }); + + return false; + }, + + // 如果传入 callback 则去查看文件夹,否则只管当前文件夹。 + _getTansferFiles: function( dataTransfer, callback ) { + var results = [], + promises = [], + items, files, file, item, i, len, canAccessFolder; + + items = dataTransfer.items; + files = dataTransfer.files; + + canAccessFolder = !!(items && items[ 0 ].webkitGetAsEntry); + + for ( i = 0, len = files.length; i < len; i++ ) { + file = files[ i ]; + item = items && items[ i ]; + + if ( canAccessFolder && item.webkitGetAsEntry().isDirectory ) { + + promises.push( this._traverseDirectoryTree( + item.webkitGetAsEntry(), results ) ); + } else { + results.push( file ); + } + } + + Base.when.apply( Base, promises ).done(function() { + + if ( !results.length ) { + return; + } + + callback( results ); + }); + }, + + _traverseDirectoryTree: function( entry, results ) { + var deferred = Base.Deferred(), + me = this; + + if ( entry.isFile ) { + entry.file(function( file ) { + results.push( file ); + deferred.resolve(); + }); + } else if ( entry.isDirectory ) { + entry.createReader().readEntries(function( entries ) { + var len = entries.length, + promises = [], + arr = [], // 为了保证顺序。 + i; + + for ( i = 0; i < len; i++ ) { + promises.push( me._traverseDirectoryTree( + entries[ i ], arr ) ); + } + + Base.when.apply( Base, promises ).then(function() { + results.push.apply( results, arr ); + deferred.resolve(); + }, deferred.reject ); + }); + } + + return deferred.promise(); + }, + + destroy: function() { + var elem = this.elem; + + // 还没 init 就调用 destroy + if (!elem) { + return; + } + + elem.off( 'dragenter', this.dragEnterHandler ); + elem.off( 'dragover', this.dragOverHandler ); + elem.off( 'dragleave', this.dragLeaveHandler ); + elem.off( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).off( 'dragover', this.dragOverHandler ); + $( document ).off( 'drop', this.dropHandler ); + } + } + }); + }); + + /** + * @fileOverview FilePaste + */ + define('runtime/html5/filepaste',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + return Html5Runtime.register( 'FilePaste', { + init: function() { + var opts = this.options, + elem = this.elem = opts.container, + accept = '.*', + arr, i, len, item; + + // accetp的mimeTypes中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].mimeTypes; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = arr.join(','); + accept = accept.replace( /,/g, '|' ).replace( /\*/g, '.*' ); + } + } + this.accept = accept = new RegExp( accept, 'i' ); + this.hander = Base.bindFn( this._pasteHander, this ); + elem.on( 'paste', this.hander ); + }, + + _pasteHander: function( e ) { + var allowed = [], + ruid = this.getRuid(), + items, item, blob, i, len; + + e = e.originalEvent || e; + items = e.clipboardData.items; + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + + if ( item.kind !== 'file' || !(blob = item.getAsFile()) ) { + continue; + } + + allowed.push( new File( ruid, blob ) ); + } + + if ( allowed.length ) { + // 不阻止非文件粘贴(文字粘贴)的事件冒泡 + e.preventDefault(); + e.stopPropagation(); + this.trigger( 'paste', allowed ); + } + }, + + destroy: function() { + this.elem.off( 'paste', this.hander ); + } + }); + }); + + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'capture', 'camera'); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function(e) { + input.trigger('click'); + e.stopPropagation(); + owner.trigger('dialogopen'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/util',[ + 'base' + ], function( Base ) { + + var urlAPI = window.createObjectURL && window || + window.URL && URL.revokeObjectURL && URL || + window.webkitURL, + createObjectURL = Base.noop, + revokeObjectURL = createObjectURL; + + if ( urlAPI ) { + + // 更安全的方式调用,比如android里面就能把context改成其他的对象。 + createObjectURL = function() { + return urlAPI.createObjectURL.apply( urlAPI, arguments ); + }; + + revokeObjectURL = function() { + return urlAPI.revokeObjectURL.apply( urlAPI, arguments ); + }; + } + + return { + createObjectURL: createObjectURL, + revokeObjectURL: revokeObjectURL, + + dataURL2Blob: function( dataURI ) { + var byteStr, intArray, ab, i, mimetype, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + ab = new ArrayBuffer( byteStr.length ); + intArray = new Uint8Array( ab ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + mimetype = parts[ 0 ].split(':')[ 1 ].split(';')[ 0 ]; + + return this.arrayBufferToBlob( ab, mimetype ); + }, + + dataURL2ArrayBuffer: function( dataURI ) { + var byteStr, intArray, i, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + intArray = new Uint8Array( byteStr.length ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + return intArray.buffer; + }, + + arrayBufferToBlob: function( buffer, type ) { + var builder = window.BlobBuilder || window.WebKitBlobBuilder, + bb; + + // android不支持直接new Blob, 只能借助blobbuilder. + if ( builder ) { + bb = new builder(); + bb.append( buffer ); + return bb.getBlob( type ); + } + + return new Blob([ buffer ], type ? { type: type } : {} ); + }, + + // 抽出来主要是为了解决android下面canvas.toDataUrl不支持jpeg. + // 你得到的结果是png. + canvasToDataUrl: function( canvas, type, quality ) { + return canvas.toDataURL( type, quality / 100 ); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + parseMeta: function( blob, callback ) { + callback( false, {}); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + updateImageHead: function( data ) { + return data; + } + }; + }); + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/imagemeta',[ + 'runtime/html5/util' + ], function( Util ) { + + var api; + + api = { + parsers: { + 0xffe1: [] + }, + + maxMetaDataSize: 262144, + + parse: function( blob, cb ) { + var me = this, + fr = new FileReader(); + + fr.onload = function() { + cb( false, me._parse( this.result ) ); + fr = fr.onload = fr.onerror = null; + }; + + fr.onerror = function( e ) { + cb( e.message ); + fr = fr.onload = fr.onerror = null; + }; + + blob = blob.slice( 0, me.maxMetaDataSize ); + fr.readAsArrayBuffer( blob.getSource() ); + }, + + _parse: function( buffer, noParse ) { + if ( buffer.byteLength < 6 ) { + return; + } + + var dataview = new DataView( buffer ), + offset = 2, + maxOffset = dataview.byteLength - 4, + headLength = offset, + ret = {}, + markerBytes, markerLength, parsers, i; + + if ( dataview.getUint16( 0 ) === 0xffd8 ) { + + while ( offset < maxOffset ) { + markerBytes = dataview.getUint16( offset ); + + if ( markerBytes >= 0xffe0 && markerBytes <= 0xffef || + markerBytes === 0xfffe ) { + + markerLength = dataview.getUint16( offset + 2 ) + 2; + + if ( offset + markerLength > dataview.byteLength ) { + break; + } + + parsers = api.parsers[ markerBytes ]; + + if ( !noParse && parsers ) { + for ( i = 0; i < parsers.length; i += 1 ) { + parsers[ i ].call( api, dataview, offset, + markerLength, ret ); + } + } + + offset += markerLength; + headLength = offset; + } else { + break; + } + } + + if ( headLength > 6 ) { + if ( buffer.slice ) { + ret.imageHead = buffer.slice( 2, headLength ); + } else { + // Workaround for IE10, which does not yet + // support ArrayBuffer.slice: + ret.imageHead = new Uint8Array( buffer ) + .subarray( 2, headLength ); + } + } + } + + return ret; + }, + + updateImageHead: function( buffer, head ) { + var data = this._parse( buffer, true ), + buf1, buf2, bodyoffset; + + + bodyoffset = 2; + if ( data.imageHead ) { + bodyoffset = 2 + data.imageHead.byteLength; + } + + if ( buffer.slice ) { + buf2 = buffer.slice( bodyoffset ); + } else { + buf2 = new Uint8Array( buffer ).subarray( bodyoffset ); + } + + buf1 = new Uint8Array( head.byteLength + 2 + buf2.byteLength ); + + buf1[ 0 ] = 0xFF; + buf1[ 1 ] = 0xD8; + buf1.set( new Uint8Array( head ), 2 ); + buf1.set( new Uint8Array( buf2 ), head.byteLength + 2 ); + + return buf1.buffer; + } + }; + + Util.parseMeta = function() { + return api.parse.apply( api, arguments ); + }; + + Util.updateImageHead = function() { + return api.updateImageHead.apply( api, arguments ); + }; + + return api; + }); + /** + * 代码来自于:https://github.com/blueimp/JavaScript-Load-Image + * 暂时项目中只用了orientation. + * + * 去除了 Exif Sub IFD Pointer, GPS Info IFD Pointer, Exif Thumbnail. + * @fileOverview EXIF解析 + */ + + // Sample + // ==================================== + // Make : Apple + // Model : iPhone 4S + // Orientation : 1 + // XResolution : 72 [72/1] + // YResolution : 72 [72/1] + // ResolutionUnit : 2 + // Software : QuickTime 7.7.1 + // DateTime : 2013:09:01 22:53:55 + // ExifIFDPointer : 190 + // ExposureTime : 0.058823529411764705 [1/17] + // FNumber : 2.4 [12/5] + // ExposureProgram : Normal program + // ISOSpeedRatings : 800 + // ExifVersion : 0220 + // DateTimeOriginal : 2013:09:01 22:52:51 + // DateTimeDigitized : 2013:09:01 22:52:51 + // ComponentsConfiguration : YCbCr + // ShutterSpeedValue : 4.058893515764426 + // ApertureValue : 2.5260688216892597 [4845/1918] + // BrightnessValue : -0.3126686601998395 + // MeteringMode : Pattern + // Flash : Flash did not fire, compulsory flash mode + // FocalLength : 4.28 [107/25] + // SubjectArea : [4 values] + // FlashpixVersion : 0100 + // ColorSpace : 1 + // PixelXDimension : 2448 + // PixelYDimension : 3264 + // SensingMethod : One-chip color area sensor + // ExposureMode : 0 + // WhiteBalance : Auto white balance + // FocalLengthIn35mmFilm : 35 + // SceneCaptureType : Standard + define('runtime/html5/imagemeta/exif',[ + 'base', + 'runtime/html5/imagemeta' + ], function( Base, ImageMeta ) { + + var EXIF = {}; + + EXIF.ExifMap = function() { + return this; + }; + + EXIF.ExifMap.prototype.map = { + 'Orientation': 0x0112 + }; + + EXIF.ExifMap.prototype.get = function( id ) { + return this[ id ] || this[ this.map[ id ] ]; + }; + + EXIF.exifTagTypes = { + // byte, 8-bit unsigned int: + 1: { + getValue: function( dataView, dataOffset ) { + return dataView.getUint8( dataOffset ); + }, + size: 1 + }, + + // ascii, 8-bit byte: + 2: { + getValue: function( dataView, dataOffset ) { + return String.fromCharCode( dataView.getUint8( dataOffset ) ); + }, + size: 1, + ascii: true + }, + + // short, 16 bit int: + 3: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint16( dataOffset, littleEndian ); + }, + size: 2 + }, + + // long, 32 bit int: + 4: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // rational = two long values, + // first is numerator, second is denominator: + 5: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ) / + dataView.getUint32( dataOffset + 4, littleEndian ); + }, + size: 8 + }, + + // slong, 32 bit signed int: + 9: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // srational, two slongs, first is numerator, second is denominator: + 10: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ) / + dataView.getInt32( dataOffset + 4, littleEndian ); + }, + size: 8 + } + }; + + // undefined, 8-bit byte, value depending on field: + EXIF.exifTagTypes[ 7 ] = EXIF.exifTagTypes[ 1 ]; + + EXIF.getExifValue = function( dataView, tiffOffset, offset, type, length, + littleEndian ) { + + var tagType = EXIF.exifTagTypes[ type ], + tagSize, dataOffset, values, i, str, c; + + if ( !tagType ) { + Base.log('Invalid Exif data: Invalid tag type.'); + return; + } + + tagSize = tagType.size * length; + + // Determine if the value is contained in the dataOffset bytes, + // or if the value at the dataOffset is a pointer to the actual data: + dataOffset = tagSize > 4 ? tiffOffset + dataView.getUint32( offset + 8, + littleEndian ) : (offset + 8); + + if ( dataOffset + tagSize > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid data offset.'); + return; + } + + if ( length === 1 ) { + return tagType.getValue( dataView, dataOffset, littleEndian ); + } + + values = []; + + for ( i = 0; i < length; i += 1 ) { + values[ i ] = tagType.getValue( dataView, + dataOffset + i * tagType.size, littleEndian ); + } + + if ( tagType.ascii ) { + str = ''; + + // Concatenate the chars: + for ( i = 0; i < values.length; i += 1 ) { + c = values[ i ]; + + // Ignore the terminating NULL byte(s): + if ( c === '\u0000' ) { + break; + } + str += c; + } + + return str; + } + return values; + }; + + EXIF.parseExifTag = function( dataView, tiffOffset, offset, littleEndian, + data ) { + + var tag = dataView.getUint16( offset, littleEndian ); + data.exif[ tag ] = EXIF.getExifValue( dataView, tiffOffset, offset, + dataView.getUint16( offset + 2, littleEndian ), // tag type + dataView.getUint32( offset + 4, littleEndian ), // tag length + littleEndian ); + }; + + EXIF.parseExifTags = function( dataView, tiffOffset, dirOffset, + littleEndian, data ) { + + var tagsNumber, dirEndOffset, i; + + if ( dirOffset + 6 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory offset.'); + return; + } + + tagsNumber = dataView.getUint16( dirOffset, littleEndian ); + dirEndOffset = dirOffset + 2 + 12 * tagsNumber; + + if ( dirEndOffset + 4 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory size.'); + return; + } + + for ( i = 0; i < tagsNumber; i += 1 ) { + this.parseExifTag( dataView, tiffOffset, + dirOffset + 2 + 12 * i, // tag offset + littleEndian, data ); + } + + // Return the offset to the next directory: + return dataView.getUint32( dirEndOffset, littleEndian ); + }; + + // EXIF.getExifThumbnail = function(dataView, offset, length) { + // var hexData, + // i, + // b; + // if (!length || offset + length > dataView.byteLength) { + // Base.log('Invalid Exif data: Invalid thumbnail data.'); + // return; + // } + // hexData = []; + // for (i = 0; i < length; i += 1) { + // b = dataView.getUint8(offset + i); + // hexData.push((b < 16 ? '0' : '') + b.toString(16)); + // } + // return 'data:image/jpeg,%' + hexData.join('%'); + // }; + + EXIF.parseExifData = function( dataView, offset, length, data ) { + + var tiffOffset = offset + 10, + littleEndian, dirOffset; + + // Check for the ASCII code for "Exif" (0x45786966): + if ( dataView.getUint32( offset + 4 ) !== 0x45786966 ) { + // No Exif data, might be XMP data instead + return; + } + if ( tiffOffset + 8 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid segment size.'); + return; + } + + // Check for the two null bytes: + if ( dataView.getUint16( offset + 8 ) !== 0x0000 ) { + Base.log('Invalid Exif data: Missing byte alignment offset.'); + return; + } + + // Check the byte alignment: + switch ( dataView.getUint16( tiffOffset ) ) { + case 0x4949: + littleEndian = true; + break; + + case 0x4D4D: + littleEndian = false; + break; + + default: + Base.log('Invalid Exif data: Invalid byte alignment marker.'); + return; + } + + // Check for the TIFF tag marker (0x002A): + if ( dataView.getUint16( tiffOffset + 2, littleEndian ) !== 0x002A ) { + Base.log('Invalid Exif data: Missing TIFF marker.'); + return; + } + + // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: + dirOffset = dataView.getUint32( tiffOffset + 4, littleEndian ); + // Create the exif object to store the tags: + data.exif = new EXIF.ExifMap(); + // Parse the tags of the main image directory and retrieve the + // offset to the next directory, usually the thumbnail directory: + dirOffset = EXIF.parseExifTags( dataView, tiffOffset, + tiffOffset + dirOffset, littleEndian, data ); + + // 尝试读取缩略图 + // if ( dirOffset ) { + // thumbnailData = {exif: {}}; + // dirOffset = EXIF.parseExifTags( + // dataView, + // tiffOffset, + // tiffOffset + dirOffset, + // littleEndian, + // thumbnailData + // ); + + // // Check for JPEG Thumbnail offset: + // if (thumbnailData.exif[0x0201]) { + // data.exif.Thumbnail = EXIF.getExifThumbnail( + // dataView, + // tiffOffset + thumbnailData.exif[0x0201], + // thumbnailData.exif[0x0202] // Thumbnail data length + // ); + // } + // } + }; + + ImageMeta.parsers[ 0xffe1 ].push( EXIF.parseExifData ); + return EXIF; + }); + /** + * 这个方式性能不行,但是可以解决android里面的toDataUrl的bug + * android里面toDataUrl('image/jpege')得到的结果却是png. + * + * 所以这里没辙,只能借助这个工具 + * @fileOverview jpeg encoder + */ + define('runtime/html5/jpegencoder',[], function( require, exports, module ) { + + /* + Copyright (c) 2008, Adobe Systems Incorporated + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of Adobe Systems Incorporated nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + /* + JPEG encoder ported to JavaScript and optimized by Andreas Ritter, www.bytestrom.eu, 11/2009 + + Basic GUI blocking jpeg encoder + */ + + function JPEGEncoder(quality) { + var self = this; + var fround = Math.round; + var ffloor = Math.floor; + var YTable = new Array(64); + var UVTable = new Array(64); + var fdtbl_Y = new Array(64); + var fdtbl_UV = new Array(64); + var YDC_HT; + var UVDC_HT; + var YAC_HT; + var UVAC_HT; + + var bitcode = new Array(65535); + var category = new Array(65535); + var outputfDCTQuant = new Array(64); + var DU = new Array(64); + var byteout = []; + var bytenew = 0; + var bytepos = 7; + + var YDU = new Array(64); + var UDU = new Array(64); + var VDU = new Array(64); + var clt = new Array(256); + var RGB_YUV_TABLE = new Array(2048); + var currentQuality; + + var ZigZag = [ + 0, 1, 5, 6,14,15,27,28, + 2, 4, 7,13,16,26,29,42, + 3, 8,12,17,25,30,41,43, + 9,11,18,24,31,40,44,53, + 10,19,23,32,39,45,52,54, + 20,22,33,38,46,51,55,60, + 21,34,37,47,50,56,59,61, + 35,36,48,49,57,58,62,63 + ]; + + var std_dc_luminance_nrcodes = [0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0]; + var std_dc_luminance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_luminance_nrcodes = [0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d]; + var std_ac_luminance_values = [ + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12, + 0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07, + 0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08, + 0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0, + 0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16, + 0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39, + 0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49, + 0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69, + 0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79, + 0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98, + 0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7, + 0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6, + 0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5, + 0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4, + 0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea, + 0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + var std_dc_chrominance_nrcodes = [0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0]; + var std_dc_chrominance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_chrominance_nrcodes = [0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77]; + var std_ac_chrominance_values = [ + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21, + 0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71, + 0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0, + 0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34, + 0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38, + 0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48, + 0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68, + 0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78, + 0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96, + 0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5, + 0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4, + 0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3, + 0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2, + 0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9, + 0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + function initQuantTables(sf){ + var YQT = [ + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68,109,103, 77, + 24, 35, 55, 64, 81,104,113, 92, + 49, 64, 78, 87,103,121,120,101, + 72, 92, 95, 98,112,100,103, 99 + ]; + + for (var i = 0; i < 64; i++) { + var t = ffloor((YQT[i]*sf+50)/100); + if (t < 1) { + t = 1; + } else if (t > 255) { + t = 255; + } + YTable[ZigZag[i]] = t; + } + var UVQT = [ + 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99 + ]; + for (var j = 0; j < 64; j++) { + var u = ffloor((UVQT[j]*sf+50)/100); + if (u < 1) { + u = 1; + } else if (u > 255) { + u = 255; + } + UVTable[ZigZag[j]] = u; + } + var aasf = [ + 1.0, 1.387039845, 1.306562965, 1.175875602, + 1.0, 0.785694958, 0.541196100, 0.275899379 + ]; + var k = 0; + for (var row = 0; row < 8; row++) + { + for (var col = 0; col < 8; col++) + { + fdtbl_Y[k] = (1.0 / (YTable [ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + fdtbl_UV[k] = (1.0 / (UVTable[ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + k++; + } + } + } + + function computeHuffmanTbl(nrcodes, std_table){ + var codevalue = 0; + var pos_in_table = 0; + var HT = new Array(); + for (var k = 1; k <= 16; k++) { + for (var j = 1; j <= nrcodes[k]; j++) { + HT[std_table[pos_in_table]] = []; + HT[std_table[pos_in_table]][0] = codevalue; + HT[std_table[pos_in_table]][1] = k; + pos_in_table++; + codevalue++; + } + codevalue*=2; + } + return HT; + } + + function initHuffmanTbl() + { + YDC_HT = computeHuffmanTbl(std_dc_luminance_nrcodes,std_dc_luminance_values); + UVDC_HT = computeHuffmanTbl(std_dc_chrominance_nrcodes,std_dc_chrominance_values); + YAC_HT = computeHuffmanTbl(std_ac_luminance_nrcodes,std_ac_luminance_values); + UVAC_HT = computeHuffmanTbl(std_ac_chrominance_nrcodes,std_ac_chrominance_values); + } + + function initCategoryNumber() + { + var nrlower = 1; + var nrupper = 2; + for (var cat = 1; cat <= 15; cat++) { + //Positive numbers + for (var nr = nrlower; nr>0] = 38470 * i; + RGB_YUV_TABLE[(i+ 512)>>0] = 7471 * i + 0x8000; + RGB_YUV_TABLE[(i+ 768)>>0] = -11059 * i; + RGB_YUV_TABLE[(i+1024)>>0] = -21709 * i; + RGB_YUV_TABLE[(i+1280)>>0] = 32768 * i + 0x807FFF; + RGB_YUV_TABLE[(i+1536)>>0] = -27439 * i; + RGB_YUV_TABLE[(i+1792)>>0] = - 5329 * i; + } + } + + // IO functions + function writeBits(bs) + { + var value = bs[0]; + var posval = bs[1]-1; + while ( posval >= 0 ) { + if (value & (1 << posval) ) { + bytenew |= (1 << bytepos); + } + posval--; + bytepos--; + if (bytepos < 0) { + if (bytenew == 0xFF) { + writeByte(0xFF); + writeByte(0); + } + else { + writeByte(bytenew); + } + bytepos=7; + bytenew=0; + } + } + } + + function writeByte(value) + { + byteout.push(clt[value]); // write char directly instead of converting later + } + + function writeWord(value) + { + writeByte((value>>8)&0xFF); + writeByte((value )&0xFF); + } + + // DCT & quantization core + function fDCTQuant(data, fdtbl) + { + var d0, d1, d2, d3, d4, d5, d6, d7; + /* Pass 1: process rows. */ + var dataOff=0; + var i; + var I8 = 8; + var I64 = 64; + for (i=0; i 0.0) ? ((fDCTQuant + 0.5)|0) : ((fDCTQuant - 0.5)|0); + //outputfDCTQuant[i] = fround(fDCTQuant); + + } + return outputfDCTQuant; + } + + function writeAPP0() + { + writeWord(0xFFE0); // marker + writeWord(16); // length + writeByte(0x4A); // J + writeByte(0x46); // F + writeByte(0x49); // I + writeByte(0x46); // F + writeByte(0); // = "JFIF",'\0' + writeByte(1); // versionhi + writeByte(1); // versionlo + writeByte(0); // xyunits + writeWord(1); // xdensity + writeWord(1); // ydensity + writeByte(0); // thumbnwidth + writeByte(0); // thumbnheight + } + + function writeSOF0(width, height) + { + writeWord(0xFFC0); // marker + writeWord(17); // length, truecolor YUV JPG + writeByte(8); // precision + writeWord(height); + writeWord(width); + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0x11); // HVY + writeByte(0); // QTY + writeByte(2); // IdU + writeByte(0x11); // HVU + writeByte(1); // QTU + writeByte(3); // IdV + writeByte(0x11); // HVV + writeByte(1); // QTV + } + + function writeDQT() + { + writeWord(0xFFDB); // marker + writeWord(132); // length + writeByte(0); + for (var i=0; i<64; i++) { + writeByte(YTable[i]); + } + writeByte(1); + for (var j=0; j<64; j++) { + writeByte(UVTable[j]); + } + } + + function writeDHT() + { + writeWord(0xFFC4); // marker + writeWord(0x01A2); // length + + writeByte(0); // HTYDCinfo + for (var i=0; i<16; i++) { + writeByte(std_dc_luminance_nrcodes[i+1]); + } + for (var j=0; j<=11; j++) { + writeByte(std_dc_luminance_values[j]); + } + + writeByte(0x10); // HTYACinfo + for (var k=0; k<16; k++) { + writeByte(std_ac_luminance_nrcodes[k+1]); + } + for (var l=0; l<=161; l++) { + writeByte(std_ac_luminance_values[l]); + } + + writeByte(1); // HTUDCinfo + for (var m=0; m<16; m++) { + writeByte(std_dc_chrominance_nrcodes[m+1]); + } + for (var n=0; n<=11; n++) { + writeByte(std_dc_chrominance_values[n]); + } + + writeByte(0x11); // HTUACinfo + for (var o=0; o<16; o++) { + writeByte(std_ac_chrominance_nrcodes[o+1]); + } + for (var p=0; p<=161; p++) { + writeByte(std_ac_chrominance_values[p]); + } + } + + function writeSOS() + { + writeWord(0xFFDA); // marker + writeWord(12); // length + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0); // HTY + writeByte(2); // IdU + writeByte(0x11); // HTU + writeByte(3); // IdV + writeByte(0x11); // HTV + writeByte(0); // Ss + writeByte(0x3f); // Se + writeByte(0); // Bf + } + + function processDU(CDU, fdtbl, DC, HTDC, HTAC){ + var EOB = HTAC[0x00]; + var M16zeroes = HTAC[0xF0]; + var pos; + var I16 = 16; + var I63 = 63; + var I64 = 64; + var DU_DCT = fDCTQuant(CDU, fdtbl); + //ZigZag reorder + for (var j=0;j0)&&(DU[end0pos]==0); end0pos--) {}; + //end0pos = first element in reverse order !=0 + if ( end0pos == 0) { + writeBits(EOB); + return DC; + } + var i = 1; + var lng; + while ( i <= end0pos ) { + var startpos = i; + for (; (DU[i]==0) && (i<=end0pos); ++i) {} + var nrzeroes = i-startpos; + if ( nrzeroes >= I16 ) { + lng = nrzeroes>>4; + for (var nrmarker=1; nrmarker <= lng; ++nrmarker) + writeBits(M16zeroes); + nrzeroes = nrzeroes&0xF; + } + pos = 32767+DU[i]; + writeBits(HTAC[(nrzeroes<<4)+category[pos]]); + writeBits(bitcode[pos]); + i++; + } + if ( end0pos != I63 ) { + writeBits(EOB); + } + return DC; + } + + function initCharLookupTable(){ + var sfcc = String.fromCharCode; + for(var i=0; i < 256; i++){ ///// ACHTUNG // 255 + clt[i] = sfcc(i); + } + } + + this.encode = function(image,quality) // image data object + { + // var time_start = new Date().getTime(); + + if(quality) setQuality(quality); + + // Initialize bit writer + byteout = new Array(); + bytenew=0; + bytepos=7; + + // Add JPEG headers + writeWord(0xFFD8); // SOI + writeAPP0(); + writeDQT(); + writeSOF0(image.width,image.height); + writeDHT(); + writeSOS(); + + + // Encode 8x8 macroblocks + var DCY=0; + var DCU=0; + var DCV=0; + + bytenew=0; + bytepos=7; + + + this.encode.displayName = "_encode_"; + + var imageData = image.data; + var width = image.width; + var height = image.height; + + var quadWidth = width*4; + var tripleWidth = width*3; + + var x, y = 0; + var r, g, b; + var start,p, col,row,pos; + while(y < height){ + x = 0; + while(x < quadWidth){ + start = quadWidth * y + x; + p = start; + col = -1; + row = 0; + + for(pos=0; pos < 64; pos++){ + row = pos >> 3;// /8 + col = ( pos & 7 ) * 4; // %8 + p = start + ( row * quadWidth ) + col; + + if(y+row >= height){ // padding bottom + p-= (quadWidth*(y+1+row-height)); + } + + if(x+col >= quadWidth){ // padding right + p-= ((x+col) - quadWidth +4) + } + + r = imageData[ p++ ]; + g = imageData[ p++ ]; + b = imageData[ p++ ]; + + + /* // calculate YUV values dynamically + YDU[pos]=((( 0.29900)*r+( 0.58700)*g+( 0.11400)*b))-128; //-0x80 + UDU[pos]=(((-0.16874)*r+(-0.33126)*g+( 0.50000)*b)); + VDU[pos]=((( 0.50000)*r+(-0.41869)*g+(-0.08131)*b)); + */ + + // use lookup table (slightly faster) + YDU[pos] = ((RGB_YUV_TABLE[r] + RGB_YUV_TABLE[(g + 256)>>0] + RGB_YUV_TABLE[(b + 512)>>0]) >> 16)-128; + UDU[pos] = ((RGB_YUV_TABLE[(r + 768)>>0] + RGB_YUV_TABLE[(g + 1024)>>0] + RGB_YUV_TABLE[(b + 1280)>>0]) >> 16)-128; + VDU[pos] = ((RGB_YUV_TABLE[(r + 1280)>>0] + RGB_YUV_TABLE[(g + 1536)>>0] + RGB_YUV_TABLE[(b + 1792)>>0]) >> 16)-128; + + } + + DCY = processDU(YDU, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCU = processDU(UDU, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = processDU(VDU, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + x+=32; + } + y+=8; + } + + + //////////////////////////////////////////////////////////////// + + // Do the bit alignment of the EOI marker + if ( bytepos >= 0 ) { + var fillbits = []; + fillbits[1] = bytepos+1; + fillbits[0] = (1<<(bytepos+1))-1; + writeBits(fillbits); + } + + writeWord(0xFFD9); //EOI + + var jpegDataUri = 'data:image/jpeg;base64,' + btoa(byteout.join('')); + + byteout = []; + + // benchmarking + // var duration = new Date().getTime() - time_start; + // console.log('Encoding time: '+ currentQuality + 'ms'); + // + + return jpegDataUri + } + + function setQuality(quality){ + if (quality <= 0) { + quality = 1; + } + if (quality > 100) { + quality = 100; + } + + if(currentQuality == quality) return // don't recalc if unchanged + + var sf = 0; + if (quality < 50) { + sf = Math.floor(5000 / quality); + } else { + sf = Math.floor(200 - quality*2); + } + + initQuantTables(sf); + currentQuality = quality; + // console.log('Quality set to: '+quality +'%'); + } + + function init(){ + // var time_start = new Date().getTime(); + if(!quality) quality = 50; + // Create tables + initCharLookupTable() + initHuffmanTbl(); + initCategoryNumber(); + initRGBYUVTable(); + + setQuality(quality); + // var duration = new Date().getTime() - time_start; + // console.log('Initialization '+ duration + 'ms'); + } + + init(); + + }; + + JPEGEncoder.encode = function( data, quality ) { + var encoder = new JPEGEncoder( quality ); + + return encoder.encode( data ); + } + + return JPEGEncoder; + }); + /** + * @fileOverview Fix android canvas.toDataUrl bug. + */ + define('runtime/html5/androidpatch',[ + 'runtime/html5/util', + 'runtime/html5/jpegencoder', + 'base' + ], function( Util, encoder, Base ) { + var origin = Util.canvasToDataUrl, + supportJpeg; + + Util.canvasToDataUrl = function( canvas, type, quality ) { + var ctx, w, h, fragement, parts; + + // 非android手机直接跳过。 + if ( !Base.os.android ) { + return origin.apply( null, arguments ); + } + + // 检测是否canvas支持jpeg导出,根据数据格式来判断。 + // JPEG 前两位分别是:255, 216 + if ( type === 'image/jpeg' && typeof supportJpeg === 'undefined' ) { + fragement = origin.apply( null, arguments ); + + parts = fragement.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + fragement = atob( parts[ 1 ] ); + } else { + fragement = decodeURIComponent( parts[ 1 ] ); + } + + fragement = fragement.substring( 0, 2 ); + + supportJpeg = fragement.charCodeAt( 0 ) === 255 && + fragement.charCodeAt( 1 ) === 216; + } + + // 只有在android环境下才修复 + if ( type === 'image/jpeg' && !supportJpeg ) { + w = canvas.width; + h = canvas.height; + ctx = canvas.getContext('2d'); + + return encoder.encode( ctx.getImageData( 0, 0, w, h ), quality ); + } + + return origin.apply( null, arguments ); + }; + }); + /** + * @fileOverview Image + */ + define('runtime/html5/image',[ + 'base', + 'runtime/html5/runtime', + 'runtime/html5/util' + ], function( Base, Html5Runtime, Util ) { + + var BLANK = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D'; + + return Html5Runtime.register( 'Image', { + + // flag: 标记是否被修改过。 + modified: false, + + init: function() { + var me = this, + img = new Image(); + + img.onload = function() { + + me._info = { + type: me.type, + width: this.width, + height: this.height + }; + + //debugger; + + // 读取meta信息。 + if ( !me._metas && 'image/jpeg' === me.type ) { + Util.parseMeta( me._blob, function( error, ret ) { + me._metas = ret; + me.owner.trigger('load'); + }); + } else { + me.owner.trigger('load'); + } + }; + + img.onerror = function() { + me.owner.trigger('error'); + }; + + me._img = img; + }, + + loadFromBlob: function( blob ) { + var me = this, + img = me._img; + + me._blob = blob; + me.type = blob.type; + img.src = Util.createObjectURL( blob.getSource() ); + me.owner.once( 'load', function() { + Util.revokeObjectURL( img.src ); + }); + }, + + resize: function( width, height ) { + var canvas = this._canvas || + (this._canvas = document.createElement('canvas')); + + this._resize( this._img, canvas, width, height ); + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'resize' ); + }, + + crop: function( x, y, w, h, s ) { + var cvs = this._canvas || + (this._canvas = document.createElement('canvas')), + opts = this.options, + img = this._img, + iw = img.naturalWidth, + ih = img.naturalHeight, + orientation = this.getOrientation(); + + s = s || 1; + + // todo 解决 orientation 的问题。 + // values that require 90 degree rotation + // if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // switch ( orientation ) { + // case 6: + // tmp = x; + // x = y; + // y = iw * s - tmp - w; + // console.log(ih * s, tmp, w) + // break; + // } + + // (w ^= h, h ^= w, w ^= h); + // } + + cvs.width = w; + cvs.height = h; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + this._renderImageToCanvas( cvs, img, -x, -y, iw * s, ih * s ); + + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'crop' ); + }, + + getAsBlob: function( type ) { + var blob = this._blob, + opts = this.options, + canvas; + + type = type || this.type; + + // blob需要重新生成。 + if ( this.modified || this.type !== type ) { + canvas = this._canvas; + + if ( type === 'image/jpeg' ) { + + blob = Util.canvasToDataUrl( canvas, type, opts.quality ); + + if ( opts.preserveHeaders && this._metas && + this._metas.imageHead ) { + + blob = Util.dataURL2ArrayBuffer( blob ); + blob = Util.updateImageHead( blob, + this._metas.imageHead ); + blob = Util.arrayBufferToBlob( blob, type ); + return blob; + } + } else { + blob = Util.canvasToDataUrl( canvas, type ); + } + + blob = Util.dataURL2Blob( blob ); + } + + return blob; + }, + + getAsDataUrl: function( type ) { + var opts = this.options; + + type = type || this.type; + + if ( type === 'image/jpeg' ) { + return Util.canvasToDataUrl( this._canvas, type, opts.quality ); + } else { + return this._canvas.toDataURL( type ); + } + }, + + getOrientation: function() { + return this._metas && this._metas.exif && + this._metas.exif.get('Orientation') || 1; + }, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._metas = val; + return this; + } + + // getter + return this._metas; + }, + + destroy: function() { + var canvas = this._canvas; + this._img.onload = null; + + if ( canvas ) { + canvas.getContext('2d') + .clearRect( 0, 0, canvas.width, canvas.height ); + canvas.width = canvas.height = 0; + this._canvas = null; + } + + // 释放内存。非常重要,否则释放不了image的内存。 + this._img.src = BLANK; + this._img = this._blob = null; + }, + + _resize: function( img, cvs, width, height ) { + var opts = this.options, + naturalWidth = img.width, + naturalHeight = img.height, + orientation = this.getOrientation(), + scale, w, h, x, y; + + // values that require 90 degree rotation + if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // 交换width, height的值。 + width ^= height; + height ^= width; + width ^= height; + } + + scale = Math[ opts.crop ? 'max' : 'min' ]( width / naturalWidth, + height / naturalHeight ); + + // 不允许放大。 + opts.allowMagnify || (scale = Math.min( 1, scale )); + + w = naturalWidth * scale; + h = naturalHeight * scale; + + if ( opts.crop ) { + cvs.width = width; + cvs.height = height; + } else { + cvs.width = w; + cvs.height = h; + } + + x = (cvs.width - w) / 2; + y = (cvs.height - h) / 2; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + + this._renderImageToCanvas( cvs, img, x, y, w, h ); + }, + + _rotate2Orientaion: function( canvas, orientation ) { + var width = canvas.width, + height = canvas.height, + ctx = canvas.getContext('2d'); + + switch ( orientation ) { + case 5: + case 6: + case 7: + case 8: + canvas.width = height; + canvas.height = width; + break; + } + + 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( Math.PI ); + break; + + case 4: // vertical flip + ctx.translate( 0, height ); + ctx.scale( 1, -1 ); + break; + + case 5: // vertical flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.scale( 1, -1 ); + break; + + case 6: // 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( 0, -height ); + break; + + case 7: // horizontal flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( width, -height ); + ctx.scale( -1, 1 ); + break; + + case 8: // 90 rotate left + ctx.rotate( -0.5 * Math.PI ); + ctx.translate( -width, 0 ); + break; + } + }, + + // https://github.com/stomita/ios-imagefile-megapixel/ + // blob/master/src/megapix-image.js + _renderImageToCanvas: (function() { + + // 如果不是ios, 不需要这么复杂! + if ( !Base.os.ios ) { + return function( canvas ) { + var args = Base.slice( arguments, 1 ), + ctx = canvas.getContext('2d'); + + ctx.drawImage.apply( ctx, args ); + }; + } + + /** + * Detecting vertical squash in loaded image. + * Fixes a bug which squash image vertically while drawing into + * canvas for some images. + */ + function detectVerticalSquash( img, iw, ih ) { + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'), + sy = 0, + ey = ih, + py = ih, + data, alpha, ratio; + + + canvas.width = 1; + canvas.height = ih; + ctx.drawImage( img, 0, 0 ); + data = ctx.getImageData( 0, 0, 1, ih ).data; + + // search image edge pixel position in case + // it is squashed vertically. + while ( py > sy ) { + alpha = data[ (py - 1) * 4 + 3 ]; + + if ( alpha === 0 ) { + ey = py; + } else { + sy = py; + } + + py = (ey + sy) >> 1; + } + + ratio = (py / ih); + return (ratio === 0) ? 1 : ratio; + } + + // fix ie7 bug + // http://stackoverflow.com/questions/11929099/ + // html5-canvas-drawimage-ratio-bug-ios + if ( Base.os.ios >= 7 ) { + return function( canvas, img, x, y, w, h ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + vertSquashRatio = detectVerticalSquash( img, iw, ih ); + + return canvas.getContext('2d').drawImage( img, 0, 0, + iw * vertSquashRatio, ih * vertSquashRatio, + x, y, w, h ); + }; + } + + /** + * Detect subsampling in loaded image. + * In iOS, larger images than 2M pixels may be + * subsampled in rendering. + */ + function detectSubsampling( img ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + canvas, ctx; + + // subsampling may happen overmegapixel image + if ( iw * ih > 1024 * 1024 ) { + canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + ctx = canvas.getContext('2d'); + ctx.drawImage( img, -iw + 1, 0 ); + + // subsampled image becomes half smaller in rendering size. + // check alpha channel value to confirm image is covering + // edge pixel or not. if alpha value is 0 + // image is not covering, hence subsampled. + return ctx.getImageData( 0, 0, 1, 1 ).data[ 3 ] === 0; + } else { + return false; + } + } + + + return function( canvas, img, x, y, width, height ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + ctx = canvas.getContext('2d'), + subsampled = detectSubsampling( img ), + doSquash = this.type === 'image/jpeg', + d = 1024, + sy = 0, + dy = 0, + tmpCanvas, tmpCtx, vertSquashRatio, dw, dh, sx, dx; + + if ( subsampled ) { + iw /= 2; + ih /= 2; + } + + ctx.save(); + tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = tmpCanvas.height = d; + + tmpCtx = tmpCanvas.getContext('2d'); + vertSquashRatio = doSquash ? + detectVerticalSquash( img, iw, ih ) : 1; + + dw = Math.ceil( d * width / iw ); + dh = Math.ceil( d * height / ih / vertSquashRatio ); + + while ( sy < ih ) { + sx = 0; + dx = 0; + while ( sx < iw ) { + tmpCtx.clearRect( 0, 0, d, d ); + tmpCtx.drawImage( img, -sx, -sy ); + ctx.drawImage( tmpCanvas, 0, 0, d, d, + x + dx, y + dy, dw, dh ); + sx += d; + dx += dw; + } + sy += d; + dy += dh; + } + ctx.restore(); + tmpCanvas = tmpCtx = null; + }; + })() + }); + }); + + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/html5/md5',[ + 'runtime/html5/runtime' + ], function( FlashRuntime ) { + + /* + * Fastest md5 implementation around (JKM md5) + * Credits: Joseph Myers + * + * @see http://www.myersdaily.org/joseph/javascript/md5-text.html + * @see http://jsperf.com/md5-shootout/7 + */ + + /* this function is much faster, + so if possible we use it. Some IEs + are the only ones I know of that + need the idiotic second function, + generated by an if clause. */ + var add32 = function (a, b) { + return (a + b) & 0xFFFFFFFF; + }, + + cmn = function (q, a, b, x, s, t) { + a = add32(add32(a, q), add32(x, t)); + return add32((a << s) | (a >>> (32 - s)), b); + }, + + ff = function (a, b, c, d, x, s, t) { + return cmn((b & c) | ((~b) & d), a, b, x, s, t); + }, + + gg = function (a, b, c, d, x, s, t) { + return cmn((b & d) | (c & (~d)), a, b, x, s, t); + }, + + hh = function (a, b, c, d, x, s, t) { + return cmn(b ^ c ^ d, a, b, x, s, t); + }, + + ii = function (a, b, c, d, x, s, t) { + return cmn(c ^ (b | (~d)), a, b, x, s, t); + }, + + md5cycle = function (x, k) { + var a = x[0], + b = x[1], + c = x[2], + d = x[3]; + + a = ff(a, b, c, d, k[0], 7, -680876936); + d = ff(d, a, b, c, k[1], 12, -389564586); + c = ff(c, d, a, b, k[2], 17, 606105819); + b = ff(b, c, d, a, k[3], 22, -1044525330); + a = ff(a, b, c, d, k[4], 7, -176418897); + d = ff(d, a, b, c, k[5], 12, 1200080426); + c = ff(c, d, a, b, k[6], 17, -1473231341); + b = ff(b, c, d, a, k[7], 22, -45705983); + a = ff(a, b, c, d, k[8], 7, 1770035416); + d = ff(d, a, b, c, k[9], 12, -1958414417); + c = ff(c, d, a, b, k[10], 17, -42063); + b = ff(b, c, d, a, k[11], 22, -1990404162); + a = ff(a, b, c, d, k[12], 7, 1804603682); + d = ff(d, a, b, c, k[13], 12, -40341101); + c = ff(c, d, a, b, k[14], 17, -1502002290); + b = ff(b, c, d, a, k[15], 22, 1236535329); + + a = gg(a, b, c, d, k[1], 5, -165796510); + d = gg(d, a, b, c, k[6], 9, -1069501632); + c = gg(c, d, a, b, k[11], 14, 643717713); + b = gg(b, c, d, a, k[0], 20, -373897302); + a = gg(a, b, c, d, k[5], 5, -701558691); + d = gg(d, a, b, c, k[10], 9, 38016083); + c = gg(c, d, a, b, k[15], 14, -660478335); + b = gg(b, c, d, a, k[4], 20, -405537848); + a = gg(a, b, c, d, k[9], 5, 568446438); + d = gg(d, a, b, c, k[14], 9, -1019803690); + c = gg(c, d, a, b, k[3], 14, -187363961); + b = gg(b, c, d, a, k[8], 20, 1163531501); + a = gg(a, b, c, d, k[13], 5, -1444681467); + d = gg(d, a, b, c, k[2], 9, -51403784); + c = gg(c, d, a, b, k[7], 14, 1735328473); + b = gg(b, c, d, a, k[12], 20, -1926607734); + + a = hh(a, b, c, d, k[5], 4, -378558); + d = hh(d, a, b, c, k[8], 11, -2022574463); + c = hh(c, d, a, b, k[11], 16, 1839030562); + b = hh(b, c, d, a, k[14], 23, -35309556); + a = hh(a, b, c, d, k[1], 4, -1530992060); + d = hh(d, a, b, c, k[4], 11, 1272893353); + c = hh(c, d, a, b, k[7], 16, -155497632); + b = hh(b, c, d, a, k[10], 23, -1094730640); + a = hh(a, b, c, d, k[13], 4, 681279174); + d = hh(d, a, b, c, k[0], 11, -358537222); + c = hh(c, d, a, b, k[3], 16, -722521979); + b = hh(b, c, d, a, k[6], 23, 76029189); + a = hh(a, b, c, d, k[9], 4, -640364487); + d = hh(d, a, b, c, k[12], 11, -421815835); + c = hh(c, d, a, b, k[15], 16, 530742520); + b = hh(b, c, d, a, k[2], 23, -995338651); + + a = ii(a, b, c, d, k[0], 6, -198630844); + d = ii(d, a, b, c, k[7], 10, 1126891415); + c = ii(c, d, a, b, k[14], 15, -1416354905); + b = ii(b, c, d, a, k[5], 21, -57434055); + a = ii(a, b, c, d, k[12], 6, 1700485571); + d = ii(d, a, b, c, k[3], 10, -1894986606); + c = ii(c, d, a, b, k[10], 15, -1051523); + b = ii(b, c, d, a, k[1], 21, -2054922799); + a = ii(a, b, c, d, k[8], 6, 1873313359); + d = ii(d, a, b, c, k[15], 10, -30611744); + c = ii(c, d, a, b, k[6], 15, -1560198380); + b = ii(b, c, d, a, k[13], 21, 1309151649); + a = ii(a, b, c, d, k[4], 6, -145523070); + d = ii(d, a, b, c, k[11], 10, -1120210379); + c = ii(c, d, a, b, k[2], 15, 718787259); + b = ii(b, c, d, a, k[9], 21, -343485551); + + x[0] = add32(a, x[0]); + x[1] = add32(b, x[1]); + x[2] = add32(c, x[2]); + x[3] = add32(d, x[3]); + }, + + /* there needs to be support for Unicode here, + * unless we pretend that we can redefine the MD-5 + * algorithm for multi-byte characters (perhaps + * by adding every four 16-bit characters and + * shortening the sum to 32 bits). Otherwise + * I suggest performing MD-5 as if every character + * was two bytes--e.g., 0040 0025 = @%--but then + * how will an ordinary MD-5 sum be matched? + * There is no way to standardize text to something + * like UTF-8 before transformation; speed cost is + * utterly prohibitive. The JavaScript standard + * itself needs to look at this: it should start + * providing access to strings as preformed UTF-8 + * 8-bit unsigned value arrays. + */ + md5blk = function (s) { + var md5blks = [], + i; /* Andy King said do it this way. */ + + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24); + } + return md5blks; + }, + + md5blk_array = function (a) { + var md5blks = [], + i; /* Andy King said do it this way. */ + + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = a[i] + (a[i + 1] << 8) + (a[i + 2] << 16) + (a[i + 3] << 24); + } + return md5blks; + }, + + md51 = function (s) { + var n = s.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i, + length, + tail, + tmp, + lo, + hi; + + for (i = 64; i <= n; i += 64) { + md5cycle(state, md5blk(s.substring(i - 64, i))); + } + s = s.substring(i - 64); + length = s.length; + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); + } + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Beware that the final length might not fit in 32 bits so we take care of that + tmp = n * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + + md5cycle(state, tail); + return state; + }, + + md51_array = function (a) { + var n = a.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i, + length, + tail, + tmp, + lo, + hi; + + for (i = 64; i <= n; i += 64) { + md5cycle(state, md5blk_array(a.subarray(i - 64, i))); + } + + // Not sure if it is a bug, however IE10 will always produce a sub array of length 1 + // containing the last element of the parent array if the sub array specified starts + // beyond the length of the parent array - weird. + // https://connect.microsoft.com/IE/feedback/details/771452/typed-array-subarray-issue + a = (i - 64) < n ? a.subarray(i - 64) : new Uint8Array(0); + + length = a.length; + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= a[i] << ((i % 4) << 3); + } + + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Beware that the final length might not fit in 32 bits so we take care of that + tmp = n * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + + md5cycle(state, tail); + + return state; + }, + + hex_chr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'], + + rhex = function (n) { + var s = '', + j; + for (j = 0; j < 4; j += 1) { + s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F]; + } + return s; + }, + + hex = function (x) { + var i; + for (i = 0; i < x.length; i += 1) { + x[i] = rhex(x[i]); + } + return x.join(''); + }, + + md5 = function (s) { + return hex(md51(s)); + }, + + + + //////////////////////////////////////////////////////////////////////////// + + /** + * SparkMD5 OOP implementation. + * + * Use this class to perform an incremental md5, otherwise use the + * static methods instead. + */ + SparkMD5 = function () { + // call reset to init the instance + this.reset(); + }; + + + // In some cases the fast add32 function cannot be used.. + if (md5('hello') !== '5d41402abc4b2a76b9719d911017c592') { + add32 = function (x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF), + msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + }; + } + + + /** + * Appends a string. + * A conversion will be applied if an utf8 string is detected. + * + * @param {String} str The string to be appended + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.append = function (str) { + // converts the string to utf8 bytes if necessary + if (/[\u0080-\uFFFF]/.test(str)) { + str = unescape(encodeURIComponent(str)); + } + + // then append as binary + this.appendBinary(str); + + return this; + }; + + /** + * Appends a binary string. + * + * @param {String} contents The binary string to be appended + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.appendBinary = function (contents) { + this._buff += contents; + this._length += contents.length; + + var length = this._buff.length, + i; + + for (i = 64; i <= length; i += 64) { + md5cycle(this._state, md5blk(this._buff.substring(i - 64, i))); + } + + this._buff = this._buff.substr(i - 64); + + return this; + }; + + /** + * Finishes the incremental computation, reseting the internal state and + * returning the result. + * Use the raw parameter to obtain the raw result instead of the hex one. + * + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.prototype.end = function (raw) { + var buff = this._buff, + length = buff.length, + i, + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ret; + + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= buff.charCodeAt(i) << ((i % 4) << 3); + } + + this._finish(tail, length); + ret = !!raw ? this._state : hex(this._state); + + this.reset(); + + return ret; + }; + + /** + * Finish the final calculation based on the tail. + * + * @param {Array} tail The tail (will be modified) + * @param {Number} length The length of the remaining buffer + */ + SparkMD5.prototype._finish = function (tail, length) { + var i = length, + tmp, + lo, + hi; + + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(this._state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Do the final computation based on the tail and length + // Beware that the final length may not fit in 32 bits so we take care of that + tmp = this._length * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + md5cycle(this._state, tail); + }; + + /** + * Resets the internal state of the computation. + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.reset = function () { + this._buff = ""; + this._length = 0; + this._state = [1732584193, -271733879, -1732584194, 271733878]; + + return this; + }; + + /** + * Releases memory used by the incremental buffer and other aditional + * resources. If you plan to use the instance again, use reset instead. + */ + SparkMD5.prototype.destroy = function () { + delete this._state; + delete this._buff; + delete this._length; + }; + + + /** + * Performs the md5 hash on a string. + * A conversion will be applied if utf8 string is detected. + * + * @param {String} str The string + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.hash = function (str, raw) { + // converts the string to utf8 bytes if necessary + if (/[\u0080-\uFFFF]/.test(str)) { + str = unescape(encodeURIComponent(str)); + } + + var hash = md51(str); + + return !!raw ? hash : hex(hash); + }; + + /** + * Performs the md5 hash on a binary string. + * + * @param {String} content The binary string + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.hashBinary = function (content, raw) { + var hash = md51(content); + + return !!raw ? hash : hex(hash); + }; + + /** + * SparkMD5 OOP implementation for array buffers. + * + * Use this class to perform an incremental md5 ONLY for array buffers. + */ + SparkMD5.ArrayBuffer = function () { + // call reset to init the instance + this.reset(); + }; + + //////////////////////////////////////////////////////////////////////////// + + /** + * Appends an array buffer. + * + * @param {ArrayBuffer} arr The array to be appended + * + * @return {SparkMD5.ArrayBuffer} The instance itself + */ + SparkMD5.ArrayBuffer.prototype.append = function (arr) { + // TODO: we could avoid the concatenation here but the algorithm would be more complex + // if you find yourself needing extra performance, please make a PR. + var buff = this._concatArrayBuffer(this._buff, arr), + length = buff.length, + i; + + this._length += arr.byteLength; + + for (i = 64; i <= length; i += 64) { + md5cycle(this._state, md5blk_array(buff.subarray(i - 64, i))); + } + + // Avoids IE10 weirdness (documented above) + this._buff = (i - 64) < length ? buff.subarray(i - 64) : new Uint8Array(0); + + return this; + }; + + /** + * Finishes the incremental computation, reseting the internal state and + * returning the result. + * Use the raw parameter to obtain the raw result instead of the hex one. + * + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.ArrayBuffer.prototype.end = function (raw) { + var buff = this._buff, + length = buff.length, + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + i, + ret; + + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= buff[i] << ((i % 4) << 3); + } + + this._finish(tail, length); + ret = !!raw ? this._state : hex(this._state); + + this.reset(); + + return ret; + }; + + SparkMD5.ArrayBuffer.prototype._finish = SparkMD5.prototype._finish; + + /** + * Resets the internal state of the computation. + * + * @return {SparkMD5.ArrayBuffer} The instance itself + */ + SparkMD5.ArrayBuffer.prototype.reset = function () { + this._buff = new Uint8Array(0); + this._length = 0; + this._state = [1732584193, -271733879, -1732584194, 271733878]; + + return this; + }; + + /** + * Releases memory used by the incremental buffer and other aditional + * resources. If you plan to use the instance again, use reset instead. + */ + SparkMD5.ArrayBuffer.prototype.destroy = SparkMD5.prototype.destroy; + + /** + * Concats two array buffers, returning a new one. + * + * @param {ArrayBuffer} first The first array buffer + * @param {ArrayBuffer} second The second array buffer + * + * @return {ArrayBuffer} The new array buffer + */ + SparkMD5.ArrayBuffer.prototype._concatArrayBuffer = function (first, second) { + var firstLength = first.length, + result = new Uint8Array(firstLength + second.byteLength); + + result.set(first); + result.set(new Uint8Array(second), firstLength); + + return result; + }; + + /** + * Performs the md5 hash on an array buffer. + * + * @param {ArrayBuffer} arr The array buffer + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.ArrayBuffer.hash = function (arr, raw) { + var hash = md51_array(new Uint8Array(arr)); + + return !!raw ? hash : hex(hash); + }; + + return FlashRuntime.register( 'Md5', { + init: function() { + // do nothing. + }, + + loadFromBlob: function( file ) { + var blob = file.getSource(), + chunkSize = 2 * 1024 * 1024, + chunks = Math.ceil( blob.size / chunkSize ), + chunk = 0, + owner = this.owner, + spark = new SparkMD5.ArrayBuffer(), + me = this, + blobSlice = blob.mozSlice || blob.webkitSlice || blob.slice, + loadNext, fr; + + fr = new FileReader(); + + loadNext = function() { + var start, end; + + start = chunk * chunkSize; + end = Math.min( start + chunkSize, blob.size ); + + fr.onload = function( e ) { + spark.append( e.target.result ); + owner.trigger( 'progress', { + total: file.size, + loaded: end + }); + }; + + fr.onloadend = function() { + fr.onloadend = fr.onload = null; + + if ( ++chunk < chunks ) { + setTimeout( loadNext, 1 ); + } else { + setTimeout(function(){ + owner.trigger('load'); + me.result = spark.end(); + loadNext = file = blob = spark = null; + owner.trigger('complete'); + }, 50 ); + } + }; + + fr.readAsArrayBuffer( blobSlice.call( blob, start, end ) ); + }; + + loadNext(); + }, + + getResult: function() { + return this.result; + } + }); + }); + /** + * @fileOverview FlashRuntime + */ + define('runtime/flash/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var $ = Base.$, + type = 'flash', + components = {}; + + + function getFlashVersion() { + var version; + + try { + version = navigator.plugins[ 'Shockwave Flash' ]; + version = version.description; + } catch ( ex ) { + try { + version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash') + .GetVariable('$version'); + } catch ( ex2 ) { + version = '0.0'; + } + } + version = version.match( /\d+/g ); + return parseFloat( version[ 0 ] + '.' + version[ 1 ], 10 ); + } + + function FlashRuntime() { + var pool = {}, + clients = {}, + destroy = this.destroy, + me = this, + jsreciver = Base.guid('webuploader_'); + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/ ) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + clients[ uid ] = client; + + if ( components[ comp ] ) { + if ( !pool[ uid ] ) { + pool[ uid ] = new components[ comp ]( client, me ); + } + + instance = pool[ uid ]; + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + + return me.flashExec.apply( client, arguments ); + }; + + function handler( evt, obj ) { + var type = evt.type || evt, + parts, uid; + + parts = type.split('::'); + uid = parts[ 0 ]; + type = parts[ 1 ]; + + // console.log.apply( console, arguments ); + + if ( type === 'Ready' && uid === me.uid ) { + me.trigger('ready'); + } else if ( clients[ uid ] ) { + clients[ uid ].trigger( type.toLowerCase(), evt, obj ); + } + + // Base.log( evt, obj ); + } + + // flash的接受器。 + window[ jsreciver ] = function() { + var args = arguments; + + // 为了能捕获得到。 + setTimeout(function() { + handler.apply( null, args ); + }, 1 ); + }; + + this.jsreciver = jsreciver; + + this.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + + this.flashExec = function( comp, fn ) { + var flash = me.getFlash(), + args = Base.slice( arguments, 2 ); + + return flash.exec( this.uid, comp, fn, args ); + }; + + // @todo + } + + Base.inherits( Runtime, { + constructor: FlashRuntime, + + init: function() { + var container = this.getContainer(), + opts = this.options, + html; + + // 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) + container.css({ + position: 'absolute', + top: '-8px', + left: '-8px', + width: '9px', + height: '9px', + overflow: 'hidden' + }); + + // insert flash object + html = '' + + '' + + '' + + '' + + ''; + + container.html( html ); + }, + + getFlash: function() { + if ( this._flash ) { + return this._flash; + } + + this._flash = $( '#' + this.uid ).get( 0 ); + return this._flash; + } + + }); + + FlashRuntime.register = function( name, component ) { + component = components[ name ] = Base.inherits( CompBase, $.extend({ + + // @todo fix this later + flashExec: function() { + var owner = this.owner, + runtime = this.getRuntime(); + + return runtime.flashExec.apply( owner, arguments ); + } + }, component ) ); + + return component; + }; + + if ( getFlashVersion() >= 11.4 ) { + Runtime.addRuntime( type, FlashRuntime ); + } + + return FlashRuntime; + }); + /** + * @fileOverview FilePicker + */ + define('runtime/flash/filepicker',[ + 'base', + 'runtime/flash/runtime' + ], function( Base, FlashRuntime ) { + var $ = Base.$; + + return FlashRuntime.register( 'FilePicker', { + init: function( opts ) { + var copy = $.extend({}, opts ), + len, i; + + // 修复Flash再没有设置title的情况下无法弹出flash文件选择框的bug. + len = copy.accept && copy.accept.length; + for ( i = 0; i < len; i++ ) { + if ( !copy.accept[ i ].title ) { + copy.accept[ i ].title = 'Files'; + } + } + + delete copy.button; + delete copy.id; + delete copy.container; + + this.flashExec( 'FilePicker', 'init', copy ); + }, + + destroy: function() { + this.flashExec( 'FilePicker', 'destroy' ); + } + }); + }); + /** + * @fileOverview 图片压缩 + */ + define('runtime/flash/image',[ + 'runtime/flash/runtime' + ], function( FlashRuntime ) { + + return FlashRuntime.register( 'Image', { + // init: function( options ) { + // var owner = this.owner; + + // this.flashExec( 'Image', 'init', options ); + // owner.on( 'load', function() { + // debugger; + // }); + // }, + + loadFromBlob: function( blob ) { + var owner = this.owner; + + owner.info() && this.flashExec( 'Image', 'info', owner.info() ); + owner.meta() && this.flashExec( 'Image', 'meta', owner.meta() ); + + this.flashExec( 'Image', 'loadFromBlob', blob.uid ); + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/flash/transport',[ + 'base', + 'runtime/flash/runtime', + 'runtime/client' + ], function( Base, FlashRuntime, RuntimeClient ) { + var $ = Base.$; + + return FlashRuntime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + this._responseJson = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + binary; + + xhr.connectRuntime( blob.ruid ); + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.uid; + } else { + $.each( owner._formData, function( k, v ) { + xhr.exec( 'append', k, v ); + }); + + xhr.exec( 'appendBlob', opts.fileVal, blob.uid, + opts.filename || owner._formData.name || '' ); + } + + this._setRequestHeader( xhr, opts.headers ); + xhr.exec( 'send', { + method: opts.method, + url: server, + forceURLStream: opts.forceURLStream, + mimeType: 'application/octet-stream' + }, binary ); + }, + + getStatus: function() { + return this._status; + }, + + getResponse: function() { + return this._response || ''; + }, + + getResponseAsJson: function() { + return this._responseJson; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.exec('abort'); + xhr.destroy(); + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new RuntimeClient('XMLHttpRequest'); + + xhr.on( 'uploadprogress progress', function( e ) { + var percent = e.loaded / e.total; + percent = Math.min( 1, Math.max( 0, percent ) ); + return me.trigger( 'progress', percent ); + }); + + xhr.on( 'load', function() { + var status = xhr.exec('getStatus'), + readBody = false, + err = '', + p; + + xhr.off(); + me._xhr = null; + + if ( status >= 200 && status < 300 ) { + readBody = true; + } else if ( status >= 500 && status < 600 ) { + readBody = true; + err = 'server'; + } else { + err = 'http'; + } + + if ( readBody ) { + me._response = xhr.exec('getResponse'); + me._response = decodeURIComponent( me._response ); + + // flash 处理可能存在 bug, 没辙只能靠 js 了 + // try { + // me._responseJson = xhr.exec('getResponseAsJson'); + // } catch ( error ) { + + p = function( s ) { + try { + if (window.JSON && window.JSON.parse) { + return JSON.parse(s); + } + + return new Function('return ' + s).call(); + } catch ( err ) { + return {}; + } + }; + me._responseJson = me._response ? p(me._response) : {}; + + // } + } + + xhr.destroy(); + xhr = null; + + return err ? me.trigger( 'error', err ) : me.trigger('load'); + }); + + xhr.on( 'error', function() { + xhr.off(); + me._xhr = null; + me.trigger( 'error', 'http' ); + }); + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.exec( 'setRequestHeader', key, val ); + }); + } + }); + }); + + /** + * @fileOverview Blob Html实现 + */ + define('runtime/flash/blob',[ + 'runtime/flash/runtime', + 'lib/blob' + ], function( FlashRuntime, Blob ) { + + return FlashRuntime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.flashExec( 'Blob', 'slice', start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview Md5 flash实现 + */ + define('runtime/flash/md5',[ + 'runtime/flash/runtime' + ], function( FlashRuntime ) { + + return FlashRuntime.register( 'Md5', { + init: function() { + // do nothing. + }, + + loadFromBlob: function( blob ) { + return this.flashExec( 'Md5', 'loadFromBlob', blob.uid ); + } + }); + }); + /** + * @fileOverview 完全版本。 + */ + define('preset/all',[ + 'base', + + // widgets + 'widgets/filednd', + 'widgets/filepaste', + 'widgets/filepicker', + 'widgets/image', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + 'widgets/md5', + + // runtimes + // html5 + 'runtime/html5/blob', + 'runtime/html5/dnd', + 'runtime/html5/filepaste', + 'runtime/html5/filepicker', + 'runtime/html5/imagemeta/exif', + 'runtime/html5/androidpatch', + 'runtime/html5/image', + 'runtime/html5/transport', + 'runtime/html5/md5', + + // flash + 'runtime/flash/filepicker', + 'runtime/flash/image', + 'runtime/flash/transport', + 'runtime/flash/blob', + 'runtime/flash/md5' + ], function( Base ) { + return Base; + }); + define('webuploader',[ + 'preset/all' + ], function( preset ) { + return preset; + }); + return require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.nolog.min.js b/public/static/libs/webuploader/webuploader.nolog.min.js new file mode 100644 index 0000000..472a9ad --- /dev/null +++ b/public/static/libs/webuploader/webuploader.nolog.min.js @@ -0,0 +1,3 @@ +/* WebUploader 0.1.6 */!function(a,b){var c,d={},e=function(a,b){var c,d,e;if("string"==typeof a)return h(a);for(c=[],d=a.length,e=0;d>e;e++)c.push(h(a[e]));return b.apply(null,c)},f=function(a,b,c){2===arguments.length&&(c=b,b=null),e(b||[],function(){g(a,c,arguments)})},g=function(a,b,c){var f,g={exports:b};"function"==typeof b&&(c.length||(c=[e,g.exports,g]),f=b.apply(null,c),void 0!==f&&(g.exports=f)),d[a]=g.exports},h=function(b){var c=d[b]||a[b];if(!c)throw new Error("`"+b+"` is undefined");return c},i=function(a){var b,c,e,f,g,h;h=function(a){return a&&a.charAt(0).toUpperCase()+a.substr(1)};for(b in d)if(c=a,d.hasOwnProperty(b)){for(e=b.split("/"),g=h(e.pop());f=h(e.shift());)c[f]=c[f]||{},c=c[f];c[g]=d[b]}return a},j=function(c){return a.__dollar=c,i(b(a,f,e))};"object"==typeof module&&"object"==typeof module.exports?module.exports=j():"function"==typeof define&&define.amd?define(["jquery"],j):(c=a.WebUploader,a.WebUploader=j(),a.WebUploader.noConflict=function(){a.WebUploader=c})}(window,function(a,b,c){return b("dollar-third",[],function(){var b=a.require,c=a.__dollar||a.jQuery||a.Zepto||b("jquery")||b("zepto");if(!c)throw new Error("jQuery or Zepto not found!");return c}),b("dollar",["dollar-third"],function(a){return a}),b("promise-third",["dollar"],function(a){return{Deferred:a.Deferred,when:a.when,isPromise:function(a){return a&&"function"==typeof a.then}}}),b("promise",["promise-third"],function(a){return a}),b("base",["dollar","promise"],function(b,c){function d(a){return function(){return h.apply(a,arguments)}}function e(a,b){return function(){return a.apply(b,arguments)}}function f(a){var b;return Object.create?Object.create(a):(b=function(){},b.prototype=a,new b)}var g=function(){},h=Function.call;return{version:"0.1.6",$:b,Deferred:c.Deferred,isPromise:c.isPromise,when:c.when,browser:function(a){var b={},c=a.match(/WebKit\/([\d.]+)/),d=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),e=a.match(/MSIE\s([\d\.]+)/)||a.match(/(?:trident)(?:.*rv:([\w.]+))?/i),f=a.match(/Firefox\/([\d.]+)/),g=a.match(/Safari\/([\d.]+)/),h=a.match(/OPR\/([\d.]+)/);return c&&(b.webkit=parseFloat(c[1])),d&&(b.chrome=parseFloat(d[1])),e&&(b.ie=parseFloat(e[1])),f&&(b.firefox=parseFloat(f[1])),g&&(b.safari=parseFloat(g[1])),h&&(b.opera=parseFloat(h[1])),b}(navigator.userAgent),os:function(a){var b={},c=a.match(/(?:Android);?[\s\/]+([\d.]+)?/),d=a.match(/(?:iPad|iPod|iPhone).*OS\s([\d_]+)/);return c&&(b.android=parseFloat(c[1])),d&&(b.ios=parseFloat(d[1].replace(/_/g,"."))),b}(navigator.userAgent),inherits:function(a,c,d){var e;return"function"==typeof c?(e=c,c=null):e=c&&c.hasOwnProperty("constructor")?c.constructor:function(){return a.apply(this,arguments)},b.extend(!0,e,a,d||{}),e.__super__=a.prototype,e.prototype=f(a.prototype),c&&b.extend(!0,e.prototype,c),e},noop:g,bindFn:e,log:function(){return a.console?e(console.log,console):g}(),nextTick:function(){return function(a){setTimeout(a,1)}}(),slice:d([].slice),guid:function(){var a=0;return function(b){for(var c=(+new Date).toString(32),d=0;5>d;d++)c+=Math.floor(65535*Math.random()).toString(32);return(b||"wu_")+c+(a++).toString(32)}}(),formatSize:function(a,b,c){var d;for(c=c||["B","K","M","G","TB"];(d=c.shift())&&a>1024;)a/=1024;return("B"===d?a:a.toFixed(b||2))+d}}}),b("mediator",["base"],function(a){function b(a,b,c,d){return f.grep(a,function(a){return!(!a||b&&a.e!==b||c&&a.cb!==c&&a.cb._cb!==c||d&&a.ctx!==d)})}function c(a,b,c){f.each((a||"").split(h),function(a,d){c(d,b)})}function d(a,b){for(var c,d=!1,e=-1,f=a.length;++e1?(d.isPlainObject(b)&&d.isPlainObject(c[a])?d.extend(c[a],b):c[a]=b,void 0):a?c[a]:c},getStats:function(){var a=this.request("get-stats");return a?{successNum:a.numOfSuccess,progressNum:a.numOfProgress,cancelNum:a.numOfCancel,invalidNum:a.numOfInvalid,uploadFailNum:a.numOfUploadFailed,queueNum:a.numOfQueue,interruptNum:a.numofInterrupt}:{}},trigger:function(a){var c=[].slice.call(arguments,1),e=this.options,f="on"+a.substring(0,1).toUpperCase()+a.substring(1);return b.trigger.apply(this,arguments)===!1||d.isFunction(e[f])&&e[f].apply(this,c)===!1||d.isFunction(this[f])&&this[f].apply(this,c)===!1||b.trigger.apply(b,[this,a].concat(c))===!1?!1:!0},destroy:function(){this.request("destroy",arguments),this.off()},request:a.noop}),a.create=c.create=function(a){return new c(a)},a.Uploader=c,c}),b("runtime/runtime",["base","mediator"],function(a,b){function c(b){this.options=d.extend({container:document.body},b),this.uid=a.guid("rt_")}var d=a.$,e={},f=function(a){for(var b in a)if(a.hasOwnProperty(b))return b;return null};return d.extend(c.prototype,{getContainer:function(){var a,b,c=this.options;return this._container?this._container:(a=d(c.container||document.body),b=d(document.createElement("div")),b.attr("id","rt_"+this.uid),b.css({position:"absolute",top:"0px",left:"0px",width:"1px",height:"1px",overflow:"hidden"}),a.append(b),a.addClass("webuploader-container"),this._container=b,this._parent=a,b)},init:a.noop,exec:a.noop,destroy:function(){this._container&&this._container.remove(),this._parent&&this._parent.removeClass("webuploader-container"),this.off()}}),c.orders="html5,flash",c.addRuntime=function(a,b){e[a]=b},c.hasRuntime=function(a){return!!(a?e[a]:f(e))},c.create=function(a,b){var g,h;if(b=b||c.orders,d.each(b.split(/\s*,\s*/g),function(){return e[this]?(g=this,!1):void 0}),g=g||f(e),!g)throw new Error("Runtime Error");return h=new e[g](a)},b.installTo(c.prototype),c}),b("runtime/client",["base","mediator","runtime/runtime"],function(a,b,c){function d(b,d){var f,g=a.Deferred();this.uid=a.guid("client_"),this.runtimeReady=function(a){return g.done(a)},this.connectRuntime=function(b,h){if(f)throw new Error("already connected!");return g.done(h),"string"==typeof b&&e.get(b)&&(f=e.get(b)),f=f||e.get(null,d),f?(a.$.extend(f.options,b),f.__promise.then(g.resolve),f.__client++):(f=c.create(b,b.runtimeOrder),f.__promise=g.promise(),f.once("ready",g.resolve),f.init(),e.add(f),f.__client=1),d&&(f.__standalone=d),f},this.getRuntime=function(){return f},this.disconnectRuntime=function(){f&&(f.__client--,f.__client<=0&&(e.remove(f),delete f.__promise,f.destroy()),f=null)},this.exec=function(){if(f){var c=a.slice(arguments);return b&&c.unshift(b),f.exec.apply(this,c)}},this.getRuid=function(){return f&&f.uid},this.destroy=function(a){return function(){a&&a.apply(this,arguments),this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()}}(this.destroy)}var e;return e=function(){var a={};return{add:function(b){a[b.uid]=b},get:function(b,c){var d;if(b)return a[b];for(d in a)if(!c||!a[d].__standalone)return a[d];return null},remove:function(b){delete a[b.uid]}}}(),b.installTo(d.prototype),d}),b("lib/dnd",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},d.options,a),a.container=e(a.container),a.container.length&&c.call(this,"DragAndDrop")}var e=a.$;return d.options={accept:null,disableGlobalDnd:!1},a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/widget",["base","uploader"],function(a,b){function c(a){if(!a)return!1;var b=a.length,c=e.type(a);return 1===a.nodeType&&b?!0:"array"===c||"function"!==c&&"string"!==c&&(0===b||"number"==typeof b&&b>0&&b-1 in a)}function d(a){this.owner=a,this.options=a.options}var e=a.$,f=b.prototype._init,g=b.prototype.destroy,h={},i=[];return e.extend(d.prototype,{init:a.noop,invoke:function(a,b){var c=this.responseMap;return c&&a in c&&c[a]in this&&e.isFunction(this[c[a]])?this[c[a]].apply(this,b):h},request:function(){return this.owner.request.apply(this.owner,arguments)}}),e.extend(b.prototype,{_init:function(){var a=this,b=a._widgets=[],c=a.options.disableWidgets||"";return e.each(i,function(d,e){(!c||!~c.indexOf(e._name))&&b.push(new e(a))}),f.apply(a,arguments)},request:function(b,d,e){var f,g,i,j,k=0,l=this._widgets,m=l&&l.length,n=[],o=[];for(d=c(d)?d:[d];m>k;k++)f=l[k],g=f.invoke(b,d),g!==h&&(a.isPromise(g)?o.push(g):n.push(g));return e||o.length?(i=a.when.apply(a,o),j=i.pipe?"pipe":"then",i[j](function(){var b=a.Deferred(),c=arguments;return 1===c.length&&(c=c[0]),setTimeout(function(){b.resolve(c)},1),b.promise()})[e?j:"done"](e||a.noop)):n[0]},destroy:function(){g.apply(this,arguments),this._widgets=null}}),b.register=d.register=function(b,c){var f,g={init:"init",destroy:"destroy",name:"anonymous"};return 1===arguments.length?(c=b,e.each(c,function(a){return"_"===a[0]||"name"===a?("name"===a&&(g.name=c.name),void 0):(g[a.replace(/[A-Z]/g,"-$&").toLowerCase()]=a,void 0)})):g=e.extend(g,b),c.responseMap=g,f=a.inherits(d,c),f._name=g.name,i.push(f),f},b.unRegister=d.unRegister=function(a){if(a&&"anonymous"!==a)for(var b=i.length;b--;)i[b]._name===a&&i.splice(b,1)},d}),b("widgets/filednd",["base","uploader","lib/dnd","widgets/widget"],function(a,b,c){var d=a.$;return b.options.dnd="",b.register({name:"dnd",init:function(b){if(b.dnd&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{disableGlobalDnd:b.disableGlobalDnd,container:b.dnd,accept:b.accept});return this.dnd=e=new c(h),e.once("ready",g.resolve),e.on("drop",function(a){f.request("add-file",[a])}),e.on("accept",function(a){return f.owner.trigger("dndAccept",a)}),e.init(),g.promise()}},destroy:function(){this.dnd&&this.dnd.destroy()}})}),b("lib/filepaste",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},a),a.container=e(a.container||document.body),c.call(this,"FilePaste")}var e=a.$;return a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/filepaste",["base","uploader","lib/filepaste","widgets/widget"],function(a,b,c){var d=a.$;return b.register({name:"paste",init:function(b){if(b.paste&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{container:b.paste,accept:b.accept});return this.paste=e=new c(h),e.once("ready",g.resolve),e.on("paste",function(a){f.owner.request("add-file",[a])}),e.init(),g.promise()}},destroy:function(){this.paste&&this.paste.destroy()}})}),b("lib/blob",["base","runtime/client"],function(a,b){function c(a,c){var d=this;d.source=c,d.ruid=a,this.size=c.size||0,this.type=!c.type&&this.ext&&~"jpg,jpeg,png,gif,bmp".indexOf(this.ext)?"image/"+("jpg"===this.ext?"jpeg":this.ext):c.type||"application/octet-stream",b.call(d,"Blob"),this.uid=c.uid||this.uid,a&&d.connectRuntime(a)}return a.inherits(b,{constructor:c,slice:function(a,b){return this.exec("slice",a,b)},getSource:function(){return this.source}}),c}),b("lib/file",["base","lib/blob"],function(a,b){function c(a,c){var f;this.name=c.name||"untitled"+d++,f=e.exec(c.name)?RegExp.$1.toLowerCase():"",!f&&c.type&&(f=/\/(jpg|jpeg|png|gif|bmp)$/i.exec(c.type)?RegExp.$1.toLowerCase():"",this.name+="."+f),this.ext=f,this.lastModifiedDate=c.lastModifiedDate||(new Date).toLocaleString(),b.apply(this,arguments)}var d=1,e=/\.([^.]+)$/;return a.inherits(b,c)}),b("lib/filepicker",["base","runtime/client","lib/file"],function(b,c,d){function e(a){if(a=this.options=f.extend({},e.options,a),a.container=f(a.id),!a.container.length)throw new Error("按钮指定错误");a.innerHTML=a.innerHTML||a.label||a.container.html()||"",a.button=f(a.button||document.createElement("div")),a.button.html(a.innerHTML),a.container.html(a.button),c.call(this,"FilePicker",!0)}var f=b.$;return e.options={button:null,container:null,label:null,innerHTML:null,multiple:!0,accept:null,name:"file",style:"webuploader-pick"},b.inherits(c,{constructor:e,init:function(){var c=this,e=c.options,g=e.button,h=e.style;h&&g.addClass("webuploader-pick"),c.on("all",function(a){var b;switch(a){case"mouseenter":h&&g.addClass("webuploader-pick-hover");break;case"mouseleave":h&&g.removeClass("webuploader-pick-hover");break;case"change":b=c.exec("getFiles"),c.trigger("select",f.map(b,function(a){return a=new d(c.getRuid(),a),a._refer=e.container,a}),e.container)}}),c.connectRuntime(e,function(){c.refresh(),c.exec("init",e),c.trigger("ready")}),this._resizeHandler=b.bindFn(this.refresh,this),f(a).on("resize",this._resizeHandler)},refresh:function(){var a=this.getRuntime().getContainer(),b=this.options.button,c=b.outerWidth?b.outerWidth():b.width(),d=b.outerHeight?b.outerHeight():b.height(),e=b.offset();c&&d&&a.css({bottom:"auto",right:"auto",width:c+"px",height:d+"px"}).offset(e)},enable:function(){var a=this.options.button;a.removeClass("webuploader-pick-disable"),this.refresh()},disable:function(){var a=this.options.button;this.getRuntime().getContainer().css({top:"-99999px"}),a.addClass("webuploader-pick-disable")},destroy:function(){var b=this.options.button;f(a).off("resize",this._resizeHandler),b.removeClass("webuploader-pick-disable webuploader-pick-hover webuploader-pick")}}),e}),b("widgets/filepicker",["base","uploader","lib/filepicker","widgets/widget"],function(a,b,c){var d=a.$;return d.extend(b.options,{pick:null,accept:null}),b.register({name:"picker",init:function(a){return this.pickers=[],a.pick&&this.addBtn(a.pick)},refresh:function(){d.each(this.pickers,function(){this.refresh()})},addBtn:function(b){var e=this,f=e.options,g=f.accept,h=[];if(b)return d.isPlainObject(b)||(b={id:b}),d(b.id).each(function(){var i,j,k;k=a.Deferred(),i=d.extend({},b,{accept:d.isPlainObject(g)?[g]:g,swf:f.swf,runtimeOrder:f.runtimeOrder,id:this}),j=new c(i),j.once("ready",k.resolve),j.on("select",function(a){e.owner.request("add-file",[a])}),j.on("dialogopen",function(){e.owner.trigger("dialogOpen",j.button)}),j.init(),e.pickers.push(j),h.push(k.promise())}),a.when.apply(a,h)},disable:function(){d.each(this.pickers,function(){this.disable()})},enable:function(){d.each(this.pickers,function(){this.enable()})},destroy:function(){d.each(this.pickers,function(){this.destroy()}),this.pickers=null}})}),b("lib/image",["base","runtime/client","lib/blob"],function(a,b,c){function d(a){this.options=e.extend({},d.options,a),b.call(this,"Image"),this.on("load",function(){this._info=this.exec("info"),this._meta=this.exec("meta")})}var e=a.$;return d.options={quality:90,crop:!1,preserveHeaders:!1,allowMagnify:!1},a.inherits(b,{constructor:d,info:function(a){return a?(this._info=a,this):this._info},meta:function(a){return a?(this._meta=a,this):this._meta},loadFromBlob:function(a){var b=this,c=a.getRuid();this.connectRuntime(c,function(){b.exec("init",b.options),b.exec("loadFromBlob",a)})},resize:function(){var b=a.slice(arguments);return this.exec.apply(this,["resize"].concat(b))},crop:function(){var b=a.slice(arguments);return this.exec.apply(this,["crop"].concat(b))},getAsDataUrl:function(a){return this.exec("getAsDataUrl",a)},getAsBlob:function(a){var b=this.exec("getAsBlob",a);return new c(this.getRuid(),b)}}),d}),b("widgets/image",["base","uploader","lib/image","widgets/widget"],function(a,b,c){var d,e=a.$;return d=function(a){var b=0,c=[],d=function(){for(var d;c.length&&a>b;)d=c.shift(),b+=d[0],d[1]()};return function(a,e,f){c.push([e,f]),a.once("destroy",function(){b-=e,setTimeout(d,1)}),setTimeout(d,1)}}(5242880),e.extend(b.options,{thumb:{width:110,height:110,quality:70,allowMagnify:!0,crop:!0,preserveHeaders:!1,type:"image/jpeg"},compress:{width:1600,height:1600,quality:90,allowMagnify:!1,crop:!1,preserveHeaders:!0}}),b.register({name:"image",makeThumb:function(a,b,f,g){var h,i;return a=this.request("get-file",a),a.type.match(/^image/)?(h=e.extend({},this.options.thumb),e.isPlainObject(f)&&(h=e.extend(h,f),f=null),f=f||h.width,g=g||h.height,i=new c(h),i.once("load",function(){a._info=a._info||i.info(),a._meta=a._meta||i.meta(),1>=f&&f>0&&(f=a._info.width*f),1>=g&&g>0&&(g=a._info.height*g),i.resize(f,g)}),i.once("complete",function(){b(!1,i.getAsDataUrl(h.type)),i.destroy()}),i.once("error",function(a){b(a||!0),i.destroy()}),d(i,a.source.size,function(){a._info&&i.info(a._info),a._meta&&i.meta(a._meta),i.loadFromBlob(a.source)}),void 0):(b(!0),void 0)},beforeSendFile:function(b){var d,f,g=this.options.compress||this.options.resize,h=g&&g.compressSize||0,i=g&&g.noCompressIfLarger||!1;return b=this.request("get-file",b),!g||!~"image/jpeg,image/jpg".indexOf(b.type)||b.size=a&&a>0&&(a=b._info.width*a),1>=c&&c>0&&(c=b._info.height*c),d.resize(a,c)}),d.once("complete",function(){var a,c;try{a=d.getAsBlob(g.type),c=b.size,(!i||a.sizeb;b++)if(c=this._queue[b],a===c.getStatus())return c;return null},sort:function(a){"function"==typeof a&&this._queue.sort(a)},getFiles:function(){for(var a,b=[].slice.call(arguments,0),c=[],d=0,f=this._queue.length;f>d;d++)a=this._queue[d],(!b.length||~e.inArray(a.getStatus(),b))&&c.push(a);return c},removeFile:function(a){var b=this._map[a.id];b&&(delete this._map[a.id],a.destroy(),this.stats.numofDeleted++)},_fileAdded:function(a){var b=this,c=this._map[a.id];c||(this._map[a.id]=a,a.on("statuschange",function(a,c){b._onFileStatusChange(a,c)}))},_onFileStatusChange:function(a,b){var c=this.stats;switch(b){case f.PROGRESS:c.numOfProgress--;break;case f.QUEUED:c.numOfQueue--;break;case f.ERROR:c.numOfUploadFailed--;break;case f.INVALID:c.numOfInvalid--;break;case f.INTERRUPT:c.numofInterrupt--}switch(a){case f.QUEUED:c.numOfQueue++;break;case f.PROGRESS:c.numOfProgress++;break;case f.ERROR:c.numOfUploadFailed++;break;case f.COMPLETE:c.numOfSuccess++;break;case f.CANCELLED:c.numOfCancel++;break;case f.INVALID:c.numOfInvalid++;break;case f.INTERRUPT:c.numofInterrupt++}}}),b.installTo(d.prototype),d}),b("widgets/queue",["base","uploader","queue","file","lib/file","runtime/client","widgets/widget"],function(a,b,c,d,e,f){var g=a.$,h=/\.\w+$/,i=d.Status;return b.register({name:"queue",init:function(b){var d,e,h,i,j,k,l,m=this;if(g.isPlainObject(b.accept)&&(b.accept=[b.accept]),b.accept){for(j=[],h=0,e=b.accept.length;e>h;h++)i=b.accept[h].extensions,i&&j.push(i);j.length&&(k="\\."+j.join(",").replace(/,/g,"$|\\.").replace(/\*/g,".*")+"$"),m.accept=new RegExp(k,"i")}return m.queue=new c,m.stats=m.queue.stats,"html5"===this.request("predict-runtime-type")?(d=a.Deferred(),this.placeholder=l=new f("Placeholder"),l.connectRuntime({runtimeOrder:"html5"},function(){m._ruid=l.getRuid(),d.resolve()}),d.promise()):void 0},_wrapFile:function(a){if(!(a instanceof d)){if(!(a instanceof e)){if(!this._ruid)throw new Error("Can't add external files.");a=new e(this._ruid,a)}a=new d(a)}return a},acceptFile:function(a){var b=!a||!a.size||this.accept&&h.exec(a.name)&&!this.accept.test(a.name);return!b},_addFile:function(a){var b=this;return a=b._wrapFile(a),b.owner.trigger("beforeFileQueued",a)?b.acceptFile(a)?(b.queue.append(a),b.owner.trigger("fileQueued",a),a):(b.owner.trigger("error","Q_TYPE_DENIED",a),void 0):void 0},getFile:function(a){return this.queue.getFile(a)},addFile:function(a){var b=this;a.length||(a=[a]),a=g.map(a,function(a){return b._addFile(a)}),a.length&&(b.owner.trigger("filesQueued",a),b.options.auto&&setTimeout(function(){b.request("start-upload")},20))},getStats:function(){return this.stats},removeFile:function(a,b){var c=this;a=a.id?a:c.queue.getFile(a),this.request("cancel-file",a),b&&this.queue.removeFile(a)},getFiles:function(){return this.queue.getFiles.apply(this.queue,arguments)},fetchFile:function(){return this.queue.fetch.apply(this.queue,arguments)},retry:function(a,b){var c,d,e,f=this;if(a)return a=a.id?a:f.queue.getFile(a),a.setStatus(i.QUEUED),b||f.request("start-upload"),void 0;for(c=f.queue.getFiles(i.ERROR),d=0,e=c.length;e>d;d++)a=c[d],a.setStatus(i.QUEUED);f.request("start-upload")},sortFiles:function(){return this.queue.sort.apply(this.queue,arguments)},reset:function(){this.owner.trigger("reset"),this.queue=new c,this.stats=this.queue.stats},destroy:function(){this.reset(),this.placeholder&&this.placeholder.destroy()}})}),b("widgets/runtime",["uploader","runtime/runtime","widgets/widget"],function(a,b){return a.support=function(){return b.hasRuntime.apply(b,arguments)},a.register({name:"runtime",init:function(){if(!this.predictRuntimeType())throw Error("Runtime Error")},predictRuntimeType:function(){var a,c,d=this.options.runtimeOrder||b.orders,e=this.type;if(!e)for(d=d.split(/\s*,\s*/g),a=0,c=d.length;c>a;a++)if(b.hasRuntime(d[a])){this.type=e=d[a];break}return e}})}),b("lib/transport",["base","runtime/client","mediator"],function(a,b,c){function d(a){var c=this;a=c.options=e.extend(!0,{},d.options,a||{}),b.call(this,"Transport"),this._blob=null,this._formData=a.formData||{},this._headers=a.headers||{},this.on("progress",this._timeout),this.on("load error",function(){c.trigger("progress",1),clearTimeout(c._timer)})}var e=a.$;return d.options={server:"",method:"POST",withCredentials:!1,fileVal:"file",timeout:12e4,formData:{},headers:{},sendAsBinary:!1},e.extend(d.prototype,{appendBlob:function(a,b,c){var d=this,e=d.options;d.getRuid()&&d.disconnectRuntime(),d.connectRuntime(b.ruid,function(){d.exec("init")}),d._blob=b,e.fileVal=a||e.fileVal,e.filename=c||e.filename},append:function(a,b){"object"==typeof a?e.extend(this._formData,a):this._formData[a]=b},setRequestHeader:function(a,b){"object"==typeof a?e.extend(this._headers,a):this._headers[a]=b},send:function(a){this.exec("send",a),this._timeout()},abort:function(){return clearTimeout(this._timer),this.exec("abort")},destroy:function(){this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()},getResponse:function(){return this.exec("getResponse")},getResponseAsJson:function(){return this.exec("getResponseAsJson")},getStatus:function(){return this.exec("getStatus")},_timeout:function(){var a=this,b=a.options.timeout;b&&(clearTimeout(a._timer),a._timer=setTimeout(function(){a.abort(),a.trigger("error","timeout")},b))}}),c.installTo(d.prototype),d}),b("widgets/upload",["base","uploader","file","lib/transport","widgets/widget"],function(a,b,c,d){function e(a,b){var c,d,e=[],f=a.source,g=f.size,h=b?Math.ceil(g/b):1,i=0,j=0;for(d={file:a,has:function(){return!!e.length},shift:function(){return e.shift()},unshift:function(a){e.unshift(a)}};h>j;)c=Math.min(b,g-i),e.push({file:a,start:i,end:b?i+c:g,total:g,chunks:h,chunk:j++,cuted:d}),i+=c;return a.blocks=e.concat(),a.remaning=e.length,d}var f=a.$,g=a.isPromise,h=c.Status;f.extend(b.options,{prepareNextFile:!1,chunked:!1,chunkSize:5242880,chunkRetry:2,threads:3,formData:{}}),b.register({name:"upload",init:function(){var b=this.owner,c=this;this.runing=!1,this.progress=!1,b.on("startUpload",function(){c.progress=!0}).on("uploadFinished",function(){c.progress=!1}),this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this.__tick=a.bindFn(this._tick,this),b.on("uploadComplete",function(a){a.blocks&&f.each(a.blocks,function(a,b){b.transport&&(b.transport.abort(),b.transport.destroy()),delete b.transport}),delete a.blocks,delete a.remaning})},reset:function(){this.request("stop-upload",!0),this.runing=!1,this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this._trigged=!1,this._promise=null},startUpload:function(b){var c=this;if(f.each(c.request("get-files",h.INVALID),function(){c.request("remove-file",this)}),b?(b=b.id?b:c.request("get-file",b),b.getStatus()===h.INTERRUPT?(b.setStatus(h.QUEUED),f.each(c.pool,function(a,c){c.file===b&&(c.transport&&c.transport.send(),b.setStatus(h.PROGRESS))})):b.getStatus()!==h.PROGRESS&&b.setStatus(h.QUEUED)):f.each(c.request("get-files",[h.INITED]),function(){this.setStatus(h.QUEUED)}),c.runing)return a.nextTick(c.__tick);c.runing=!0;var d=[];b||f.each(c.pool,function(a,b){var e=b.file;e.getStatus()===h.INTERRUPT&&(c._trigged=!1,d.push(e),b.transport&&b.transport.send())}),f.each(d,function(){this.setStatus(h.PROGRESS)}),b||f.each(c.request("get-files",h.INTERRUPT),function(){this.setStatus(h.PROGRESS)}),c._trigged=!1,a.nextTick(c.__tick),c.owner.trigger("startUpload")},stopUpload:function(b,c){var d,e=this;if(b===!0&&(c=b,b=null),e.runing!==!1){if(b){if(b=b.id?b:e.request("get-file",b),b.getStatus()!==h.PROGRESS&&b.getStatus()!==h.QUEUED)return;return b.setStatus(h.INTERRUPT),f.each(e.pool,function(a,c){return c.file===b?(d=c,!1):void 0}),d.transport&&d.transport.abort(),c&&(e._putback(d),e._popBlock(d)),a.nextTick(e.__tick)}e.runing=!1,this._promise&&this._promise.file&&this._promise.file.setStatus(h.INTERRUPT),c&&f.each(e.pool,function(a,b){b.transport&&b.transport.abort(),b.file.setStatus(h.INTERRUPT)}),e.owner.trigger("stopUpload")}},cancelFile:function(a){a=a.id?a:this.request("get-file",a),a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),a.setStatus(h.CANCELLED),this.owner.trigger("fileDequeued",a)},isInProgress:function(){return!!this.progress},_getStats:function(){return this.request("get-stats")},skipFile:function(a,b){a=a.id?a:this.request("get-file",a),a.setStatus(b||h.COMPLETE),a.skipped=!0,a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),this.owner.trigger("uploadSkip",a)},_tick:function(){var b,c,d=this,e=d.options;return d._promise?d._promise.always(d.__tick):(d.pool.length1&&~"http,abort".indexOf(a)&&b.retried1&&f.extend(m,{chunks:b.chunks,chunk:b.chunk}),i.trigger("uploadBeforeSend",b,m,n),l.appendBlob(j.fileVal,b.blob,k.name),l.append(m),l.setRequestHeader(n),l.send() +},_finishFile:function(a,b,c){var d=this.owner;return d.request("after-send-file",arguments,function(){a.setStatus(h.COMPLETE),d.trigger("uploadSuccess",a,b,c)}).fail(function(b){a.getStatus()===h.PROGRESS&&a.setStatus(h.ERROR,b),d.trigger("uploadError",a,b)}).always(function(){d.trigger("uploadComplete",a)})},updateFileProgress:function(a){var b=0,c=0;a.blocks&&(f.each(a.blocks,function(a,b){c+=(b.percentage||0)*(b.end-b.start)}),b=c/a.size,this.owner.trigger("uploadProgress",a,b||0))}})}),b("widgets/validator",["base","uploader","file","widgets/widget"],function(a,b,c){var d,e=a.$,f={};return d={addValidator:function(a,b){f[a]=b},removeValidator:function(a){delete f[a]}},b.register({name:"validator",init:function(){var b=this;a.nextTick(function(){e.each(f,function(){this.call(b.owner)})})}}),d.addValidator("fileNumLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileNumLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){return c>=d&&e&&(e=!1,this.trigger("error","Q_EXCEED_NUM_LIMIT",d,a),setTimeout(function(){e=!0},1)),c>=d?!1:!0}),a.on("fileQueued",function(){c++}),a.on("fileDequeued",function(){c--}),a.on("reset",function(){c=0}))}),d.addValidator("fileSizeLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileSizeLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){var b=c+a.size>d;return b&&e&&(e=!1,this.trigger("error","Q_EXCEED_SIZE_LIMIT",d,a),setTimeout(function(){e=!0},1)),b?!1:!0}),a.on("fileQueued",function(a){c+=a.size}),a.on("fileDequeued",function(a){c-=a.size}),a.on("reset",function(){c=0}))}),d.addValidator("fileSingleSizeLimit",function(){var a=this,b=a.options,d=b.fileSingleSizeLimit;d&&a.on("beforeFileQueued",function(a){return a.size>d?(a.setStatus(c.Status.INVALID,"exceed_size"),this.trigger("error","F_EXCEED_SIZE",d,a),!1):void 0})}),d.addValidator("duplicate",function(){function a(a){for(var b,c=0,d=0,e=a.length;e>d;d++)b=a.charCodeAt(d),c=b+(c<<6)+(c<<16)-c;return c}var b=this,c=b.options,d={};c.duplicate||(b.on("beforeFileQueued",function(b){var c=b.__hash||(b.__hash=a(b.name+b.size+b.lastModifiedDate));return d[c]?(this.trigger("error","F_DUPLICATE",b),!1):void 0}),b.on("fileQueued",function(a){var b=a.__hash;b&&(d[b]=!0)}),b.on("fileDequeued",function(a){var b=a.__hash;b&&delete d[b]}),b.on("reset",function(){d={}}))}),d}),b("lib/md5",["runtime/client","mediator"],function(a,b){function c(){a.call(this,"Md5")}return b.installTo(c.prototype),c.prototype.loadFromBlob=function(a){var b=this;b.getRuid()&&b.disconnectRuntime(),b.connectRuntime(a.ruid,function(){b.exec("init"),b.exec("loadFromBlob",a)})},c.prototype.getResult=function(){return this.exec("getResult")},c}),b("widgets/md5",["base","uploader","lib/md5","lib/blob","widgets/widget"],function(a,b,c,d){return b.register({name:"md5",md5File:function(b,e,f){var g=new c,h=a.Deferred(),i=b instanceof d?b:this.request("get-file",b).source;return g.on("progress load",function(a){a=a||{},h.notify(a.total?a.loaded/a.total:1)}),g.on("complete",function(){h.resolve(g.getResult())}),g.on("error",function(a){h.reject(a)}),arguments.length>1&&(e=e||0,f=f||0,0>e&&(e=i.size+e),0>f&&(f=i.size+f),f=Math.min(f,i.size),i=i.slice(e,f)),g.loadFromBlob(i),h.promise()}})}),b("runtime/compbase",[],function(){function a(a,b){this.owner=a,this.options=a.options,this.getRuntime=function(){return b},this.getRuid=function(){return b.uid},this.trigger=function(){return a.trigger.apply(a,arguments)}}return a}),b("runtime/html5/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a={},d=this,e=this.destroy;c.apply(d,arguments),d.type=f,d.exec=function(c,e){var f,h=this,i=h.uid,j=b.slice(arguments,2);return g[c]&&(f=a[i]=a[i]||new g[c](h,d),f[e])?f[e].apply(f,j):void 0},d.destroy=function(){return e&&e.apply(this,arguments)}}var f="html5",g={};return b.inherits(c,{constructor:e,init:function(){var a=this;setTimeout(function(){a.trigger("ready")},1)}}),e.register=function(a,c){var e=g[a]=b.inherits(d,c);return e},a.Blob&&a.FileReader&&a.DataView&&c.addRuntime(f,e),e}),b("runtime/html5/blob",["runtime/html5/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.owner.source,e=d.slice||d.webkitSlice||d.mozSlice;return d=e.call(d,a,c),new b(this.getRuid(),d)}})}),b("runtime/html5/dnd",["base","runtime/html5/runtime","lib/file"],function(a,b,c){var d=a.$,e="webuploader-dnd-";return b.register("DragAndDrop",{init:function(){var b=this.elem=this.options.container;this.dragEnterHandler=a.bindFn(this._dragEnterHandler,this),this.dragOverHandler=a.bindFn(this._dragOverHandler,this),this.dragLeaveHandler=a.bindFn(this._dragLeaveHandler,this),this.dropHandler=a.bindFn(this._dropHandler,this),this.dndOver=!1,b.on("dragenter",this.dragEnterHandler),b.on("dragover",this.dragOverHandler),b.on("dragleave",this.dragLeaveHandler),b.on("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).on("dragover",this.dragOverHandler),d(document).on("drop",this.dropHandler))},_dragEnterHandler:function(a){var b,c=this,d=c._denied||!1;return a=a.originalEvent||a,c.dndOver||(c.dndOver=!0,b=a.dataTransfer.items,b&&b.length&&(c._denied=d=!c.trigger("accept",b)),c.elem.addClass(e+"over"),c.elem[d?"addClass":"removeClass"](e+"denied")),a.dataTransfer.dropEffect=d?"none":"copy",!1},_dragOverHandler:function(a){var b=this.elem.parent().get(0);return b&&!d.contains(b,a.currentTarget)?!1:(clearTimeout(this._leaveTimer),this._dragEnterHandler.call(this,a),!1)},_dragLeaveHandler:function(){var a,b=this;return a=function(){b.dndOver=!1,b.elem.removeClass(e+"over "+e+"denied")},clearTimeout(b._leaveTimer),b._leaveTimer=setTimeout(a,100),!1},_dropHandler:function(a){var b,f,g=this,h=g.getRuid(),i=g.elem.parent().get(0);if(i&&!d.contains(i,a.currentTarget))return!1;a=a.originalEvent||a,b=a.dataTransfer;try{f=b.getData("text/html")}catch(j){}return g.dndOver=!1,g.elem.removeClass(e+"over"),f?void 0:(g._getTansferFiles(b,function(a){g.trigger("drop",d.map(a,function(a){return new c(h,a)}))}),!1)},_getTansferFiles:function(b,c){var d,e,f,g,h,i,j,k=[],l=[];for(d=b.items,e=b.files,j=!(!d||!d[0].webkitGetAsEntry),h=0,i=e.length;i>h;h++)f=e[h],g=d&&d[h],j&&g.webkitGetAsEntry().isDirectory?l.push(this._traverseDirectoryTree(g.webkitGetAsEntry(),k)):k.push(f);a.when.apply(a,l).done(function(){k.length&&c(k)})},_traverseDirectoryTree:function(b,c){var d=a.Deferred(),e=this;return b.isFile?b.file(function(a){c.push(a),d.resolve()}):b.isDirectory&&b.createReader().readEntries(function(b){var f,g=b.length,h=[],i=[];for(f=0;g>f;f++)h.push(e._traverseDirectoryTree(b[f],i));a.when.apply(a,h).then(function(){c.push.apply(c,i),d.resolve()},d.reject)}),d.promise()},destroy:function(){var a=this.elem;a&&(a.off("dragenter",this.dragEnterHandler),a.off("dragover",this.dragOverHandler),a.off("dragleave",this.dragLeaveHandler),a.off("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).off("dragover",this.dragOverHandler),d(document).off("drop",this.dropHandler)))}})}),b("runtime/html5/filepaste",["base","runtime/html5/runtime","lib/file"],function(a,b,c){return b.register("FilePaste",{init:function(){var b,c,d,e,f=this.options,g=this.elem=f.container,h=".*";if(f.accept){for(b=[],c=0,d=f.accept.length;d>c;c++)e=f.accept[c].mimeTypes,e&&b.push(e);b.length&&(h=b.join(","),h=h.replace(/,/g,"|").replace(/\*/g,".*"))}this.accept=h=new RegExp(h,"i"),this.hander=a.bindFn(this._pasteHander,this),g.on("paste",this.hander)},_pasteHander:function(a){var b,d,e,f,g,h=[],i=this.getRuid();for(a=a.originalEvent||a,b=a.clipboardData.items,f=0,g=b.length;g>f;f++)d=b[f],"file"===d.kind&&(e=d.getAsFile())&&h.push(new c(i,e));h.length&&(a.preventDefault(),a.stopPropagation(),this.trigger("paste",h))},destroy:function(){this.elem.off("paste",this.hander)}})}),b("runtime/html5/filepicker",["base","runtime/html5/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(){var a,b,d,e,f=this.getRuntime().getContainer(),g=this,h=g.owner,i=g.options,j=this.label=c(document.createElement("label")),k=this.input=c(document.createElement("input"));if(k.attr("type","file"),k.attr("capture","camera"),k.attr("name",i.name),k.addClass("webuploader-element-invisible"),j.on("click",function(a){k.trigger("click"),a.stopPropagation(),h.trigger("dialogopen")}),j.css({opacity:0,width:"100%",height:"100%",display:"block",cursor:"pointer",background:"#ffffff"}),i.multiple&&k.attr("multiple","multiple"),i.accept&&i.accept.length>0){for(a=[],b=0,d=i.accept.length;d>b;b++)a.push(i.accept[b].mimeTypes);k.attr("accept",a.join(","))}f.append(k),f.append(j),e=function(a){h.trigger(a.type)},k.on("change",function(a){var b,d=arguments.callee;g.files=a.target.files,b=this.cloneNode(!0),b.value=null,this.parentNode.replaceChild(b,this),k.off(),k=c(b).on("change",d).on("mouseenter mouseleave",e),h.trigger("change")}),j.on("mouseenter mouseleave",e)},getFiles:function(){return this.files},destroy:function(){this.input.off(),this.label.off()}})}),b("runtime/html5/util",["base"],function(b){var c=a.createObjectURL&&a||a.URL&&URL.revokeObjectURL&&URL||a.webkitURL,d=b.noop,e=d;return c&&(d=function(){return c.createObjectURL.apply(c,arguments)},e=function(){return c.revokeObjectURL.apply(c,arguments)}),{createObjectURL:d,revokeObjectURL:e,dataURL2Blob:function(a){var b,c,d,e,f,g;for(g=a.split(","),b=~g[0].indexOf("base64")?atob(g[1]):decodeURIComponent(g[1]),d=new ArrayBuffer(b.length),c=new Uint8Array(d),e=0;ei&&(d=h.getUint16(i),d>=65504&&65519>=d||65534===d)&&(e=h.getUint16(i+2)+2,!(i+e>h.byteLength));){if(f=b.parsers[d],!c&&f)for(g=0;g6&&(l.imageHead=a.slice?a.slice(2,k):new Uint8Array(a).subarray(2,k))}return l}},updateImageHead:function(a,b){var c,d,e,f=this._parse(a,!0);return e=2,f.imageHead&&(e=2+f.imageHead.byteLength),d=a.slice?a.slice(e):new Uint8Array(a).subarray(e),c=new Uint8Array(b.byteLength+2+d.byteLength),c[0]=255,c[1]=216,c.set(new Uint8Array(b),2),c.set(new Uint8Array(d),b.byteLength+2),c.buffer}},a.parseMeta=function(){return b.parse.apply(b,arguments)},a.updateImageHead=function(){return b.updateImageHead.apply(b,arguments)},b}),b("runtime/html5/imagemeta/exif",["base","runtime/html5/imagemeta"],function(a,b){var c={};return c.ExifMap=function(){return this},c.ExifMap.prototype.map={Orientation:274},c.ExifMap.prototype.get=function(a){return this[a]||this[this.map[a]]},c.exifTagTypes={1:{getValue:function(a,b){return a.getUint8(b)},size:1},2:{getValue:function(a,b){return String.fromCharCode(a.getUint8(b))},size:1,ascii:!0},3:{getValue:function(a,b,c){return a.getUint16(b,c)},size:2},4:{getValue:function(a,b,c){return a.getUint32(b,c)},size:4},5:{getValue:function(a,b,c){return a.getUint32(b,c)/a.getUint32(b+4,c)},size:8},9:{getValue:function(a,b,c){return a.getInt32(b,c)},size:4},10:{getValue:function(a,b,c){return a.getInt32(b,c)/a.getInt32(b+4,c)},size:8}},c.exifTagTypes[7]=c.exifTagTypes[1],c.getExifValue=function(b,d,e,f,g,h){var i,j,k,l,m,n,o=c.exifTagTypes[f];if(!o)return a.log("Invalid Exif data: Invalid tag type."),void 0;if(i=o.size*g,j=i>4?d+b.getUint32(e+8,h):e+8,j+i>b.byteLength)return a.log("Invalid Exif data: Invalid data offset."),void 0;if(1===g)return o.getValue(b,j,h);for(k=[],l=0;g>l;l+=1)k[l]=o.getValue(b,j+l*o.size,h);if(o.ascii){for(m="",l=0;lb.byteLength)return a.log("Invalid Exif data: Invalid directory offset."),void 0;if(g=b.getUint16(d,e),h=d+2+12*g,h+4>b.byteLength)return a.log("Invalid Exif data: Invalid directory size."),void 0;for(i=0;g>i;i+=1)this.parseExifTag(b,c,d+2+12*i,e,f);return b.getUint32(h,e)},c.parseExifData=function(b,d,e,f){var g,h,i=d+10;if(1165519206===b.getUint32(d+4)){if(i+8>b.byteLength)return a.log("Invalid Exif data: Invalid segment size."),void 0;if(0!==b.getUint16(d+8))return a.log("Invalid Exif data: Missing byte alignment offset."),void 0;switch(b.getUint16(i)){case 18761:g=!0;break;case 19789:g=!1;break;default:return a.log("Invalid Exif data: Invalid byte alignment marker."),void 0}if(42!==b.getUint16(i+2,g))return a.log("Invalid Exif data: Missing TIFF marker."),void 0;h=b.getUint32(i+4,g),f.exif=new c.ExifMap,h=c.parseExifTags(b,i,i+h,g,f)}},b.parsers[65505].push(c.parseExifData),c}),b("runtime/html5/jpegencoder",[],function(){function a(a){function b(a){for(var b=[16,11,10,16,24,40,51,61,12,12,14,19,26,58,60,55,14,13,16,24,40,57,69,56,14,17,22,29,51,87,80,62,18,22,37,56,68,109,103,77,24,35,55,64,81,104,113,92,49,64,78,87,103,121,120,101,72,92,95,98,112,100,103,99],c=0;64>c;c++){var d=y((b[c]*a+50)/100);1>d?d=1:d>255&&(d=255),z[P[c]]=d}for(var e=[17,18,24,47,99,99,99,99,18,21,26,66,99,99,99,99,24,26,56,99,99,99,99,99,47,66,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99],f=0;64>f;f++){var g=y((e[f]*a+50)/100);1>g?g=1:g>255&&(g=255),A[P[f]]=g}for(var h=[1,1.387039845,1.306562965,1.175875602,1,.785694958,.5411961,.275899379],i=0,j=0;8>j;j++)for(var k=0;8>k;k++)B[i]=1/(8*z[P[i]]*h[j]*h[k]),C[i]=1/(8*A[P[i]]*h[j]*h[k]),i++}function c(a,b){for(var c=0,d=0,e=new Array,f=1;16>=f;f++){for(var g=1;g<=a[f];g++)e[b[d]]=[],e[b[d]][0]=c,e[b[d]][1]=f,d++,c++;c*=2}return e}function d(){t=c(Q,R),u=c(U,V),v=c(S,T),w=c(W,X)}function e(){for(var a=1,b=2,c=1;15>=c;c++){for(var d=a;b>d;d++)E[32767+d]=c,D[32767+d]=[],D[32767+d][1]=c,D[32767+d][0]=d;for(var e=-(b-1);-a>=e;e++)E[32767+e]=c,D[32767+e]=[],D[32767+e][1]=c,D[32767+e][0]=b-1+e;a<<=1,b<<=1}}function f(){for(var a=0;256>a;a++)O[a]=19595*a,O[a+256>>0]=38470*a,O[a+512>>0]=7471*a+32768,O[a+768>>0]=-11059*a,O[a+1024>>0]=-21709*a,O[a+1280>>0]=32768*a+8421375,O[a+1536>>0]=-27439*a,O[a+1792>>0]=-5329*a}function g(a){for(var b=a[0],c=a[1]-1;c>=0;)b&1<J&&(255==I?(h(255),h(0)):h(I),J=7,I=0)}function h(a){H.push(N[a])}function i(a){h(255&a>>8),h(255&a)}function j(a,b){var c,d,e,f,g,h,i,j,k,l=0,m=8,n=64;for(k=0;m>k;++k){c=a[l],d=a[l+1],e=a[l+2],f=a[l+3],g=a[l+4],h=a[l+5],i=a[l+6],j=a[l+7];var o=c+j,p=c-j,q=d+i,r=d-i,s=e+h,t=e-h,u=f+g,v=f-g,w=o+u,x=o-u,y=q+s,z=q-s;a[l]=w+y,a[l+4]=w-y;var A=.707106781*(z+x);a[l+2]=x+A,a[l+6]=x-A,w=v+t,y=t+r,z=r+p;var B=.382683433*(w-z),C=.5411961*w+B,D=1.306562965*z+B,E=.707106781*y,G=p+E,H=p-E;a[l+5]=H+C,a[l+3]=H-C,a[l+1]=G+D,a[l+7]=G-D,l+=8}for(l=0,k=0;m>k;++k){c=a[l],d=a[l+8],e=a[l+16],f=a[l+24],g=a[l+32],h=a[l+40],i=a[l+48],j=a[l+56];var I=c+j,J=c-j,K=d+i,L=d-i,M=e+h,N=e-h,O=f+g,P=f-g,Q=I+O,R=I-O,S=K+M,T=K-M;a[l]=Q+S,a[l+32]=Q-S;var U=.707106781*(T+R);a[l+16]=R+U,a[l+48]=R-U,Q=P+N,S=N+L,T=L+J;var V=.382683433*(Q-T),W=.5411961*Q+V,X=1.306562965*T+V,Y=.707106781*S,Z=J+Y,$=J-Y;a[l+40]=$+W,a[l+24]=$-W,a[l+8]=Z+X,a[l+56]=Z-X,l++}var _;for(k=0;n>k;++k)_=a[k]*b[k],F[k]=_>0?0|_+.5:0|_-.5;return F}function k(){i(65504),i(16),h(74),h(70),h(73),h(70),h(0),h(1),h(1),h(0),i(1),i(1),h(0),h(0)}function l(a,b){i(65472),i(17),h(8),i(b),i(a),h(3),h(1),h(17),h(0),h(2),h(17),h(1),h(3),h(17),h(1)}function m(){i(65499),i(132),h(0);for(var a=0;64>a;a++)h(z[a]);h(1);for(var b=0;64>b;b++)h(A[b])}function n(){i(65476),i(418),h(0);for(var a=0;16>a;a++)h(Q[a+1]);for(var b=0;11>=b;b++)h(R[b]);h(16);for(var c=0;16>c;c++)h(S[c+1]);for(var d=0;161>=d;d++)h(T[d]);h(1);for(var e=0;16>e;e++)h(U[e+1]);for(var f=0;11>=f;f++)h(V[f]);h(17);for(var g=0;16>g;g++)h(W[g+1]);for(var j=0;161>=j;j++)h(X[j])}function o(){i(65498),i(12),h(3),h(1),h(0),h(2),h(17),h(3),h(17),h(0),h(63),h(0)}function p(a,b,c,d,e){for(var f,h=e[0],i=e[240],k=16,l=63,m=64,n=j(a,b),o=0;m>o;++o)G[P[o]]=n[o];var p=G[0]-c;c=G[0],0==p?g(d[0]):(f=32767+p,g(d[E[f]]),g(D[f]));for(var q=63;q>0&&0==G[q];q--);if(0==q)return g(h),c;for(var r,s=1;q>=s;){for(var t=s;0==G[s]&&q>=s;++s);var u=s-t;if(u>=k){r=u>>4;for(var v=1;r>=v;++v)g(i);u=15&u}f=32767+G[s],g(e[(u<<4)+E[f]]),g(D[f]),s++}return q!=l&&g(h),c}function q(){for(var a=String.fromCharCode,b=0;256>b;b++)N[b]=a(b)}function r(a){if(0>=a&&(a=1),a>100&&(a=100),x!=a){var c=0;c=50>a?Math.floor(5e3/a):Math.floor(200-2*a),b(c),x=a}}function s(){a||(a=50),q(),d(),e(),f(),r(a)}Math.round;var t,u,v,w,x,y=Math.floor,z=new Array(64),A=new Array(64),B=new Array(64),C=new Array(64),D=new Array(65535),E=new Array(65535),F=new Array(64),G=new Array(64),H=[],I=0,J=7,K=new Array(64),L=new Array(64),M=new Array(64),N=new Array(256),O=new Array(2048),P=[0,1,5,6,14,15,27,28,2,4,7,13,16,26,29,42,3,8,12,17,25,30,41,43,9,11,18,24,31,40,44,53,10,19,23,32,39,45,52,54,20,22,33,38,46,51,55,60,21,34,37,47,50,56,59,61,35,36,48,49,57,58,62,63],Q=[0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0],R=[0,1,2,3,4,5,6,7,8,9,10,11],S=[0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,125],T=[1,2,3,0,4,17,5,18,33,49,65,6,19,81,97,7,34,113,20,50,129,145,161,8,35,66,177,193,21,82,209,240,36,51,98,114,130,9,10,22,23,24,25,26,37,38,39,40,41,42,52,53,54,55,56,57,58,67,68,69,70,71,72,73,74,83,84,85,86,87,88,89,90,99,100,101,102,103,104,105,106,115,116,117,118,119,120,121,122,131,132,133,134,135,136,137,138,146,147,148,149,150,151,152,153,154,162,163,164,165,166,167,168,169,170,178,179,180,181,182,183,184,185,186,194,195,196,197,198,199,200,201,202,210,211,212,213,214,215,216,217,218,225,226,227,228,229,230,231,232,233,234,241,242,243,244,245,246,247,248,249,250],U=[0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0],V=[0,1,2,3,4,5,6,7,8,9,10,11],W=[0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,119],X=[0,1,2,3,17,4,5,33,49,6,18,65,81,7,97,113,19,34,50,129,8,20,66,145,161,177,193,9,35,51,82,240,21,98,114,209,10,22,36,52,225,37,241,23,24,25,26,38,39,40,41,42,53,54,55,56,57,58,67,68,69,70,71,72,73,74,83,84,85,86,87,88,89,90,99,100,101,102,103,104,105,106,115,116,117,118,119,120,121,122,130,131,132,133,134,135,136,137,138,146,147,148,149,150,151,152,153,154,162,163,164,165,166,167,168,169,170,178,179,180,181,182,183,184,185,186,194,195,196,197,198,199,200,201,202,210,211,212,213,214,215,216,217,218,226,227,228,229,230,231,232,233,234,242,243,244,245,246,247,248,249,250];this.encode=function(a,b){b&&r(b),H=new Array,I=0,J=7,i(65496),k(),m(),l(a.width,a.height),n(),o();var c=0,d=0,e=0;I=0,J=7,this.encode.displayName="_encode_";for(var f,h,j,q,s,x,y,z,A,D=a.data,E=a.width,F=a.height,G=4*E,N=0;F>N;){for(f=0;G>f;){for(s=G*N+f,x=s,y=-1,z=0,A=0;64>A;A++)z=A>>3,y=4*(7&A),x=s+z*G+y,N+z>=F&&(x-=G*(N+1+z-F)),f+y>=G&&(x-=f+y-G+4),h=D[x++],j=D[x++],q=D[x++],K[A]=(O[h]+O[j+256>>0]+O[q+512>>0]>>16)-128,L[A]=(O[h+768>>0]+O[j+1024>>0]+O[q+1280>>0]>>16)-128,M[A]=(O[h+1280>>0]+O[j+1536>>0]+O[q+1792>>0]>>16)-128;c=p(K,B,c,t,v),d=p(L,C,d,u,w),e=p(M,C,e,u,w),f+=32}N+=8}if(J>=0){var P=[];P[1]=J+1,P[0]=(1<i;)e=d[4*(k-1)+3],0===e?j=k:i=k,k=j+i>>1;return f=k/c,0===f?1:f}function c(a){var b,c,d=a.naturalWidth,e=a.naturalHeight;return d*e>1048576?(b=document.createElement("canvas"),b.width=b.height=1,c=b.getContext("2d"),c.drawImage(a,-d+1,0),0===c.getImageData(0,0,1,1).data[3]):!1}return a.os.ios?a.os.ios>=7?function(a,c,d,e,f,g){var h=c.naturalWidth,i=c.naturalHeight,j=b(c,h,i);return a.getContext("2d").drawImage(c,0,0,h*j,i*j,d,e,f,g)}:function(a,d,e,f,g,h){var i,j,k,l,m,n,o,p=d.naturalWidth,q=d.naturalHeight,r=a.getContext("2d"),s=c(d),t="image/jpeg"===this.type,u=1024,v=0,w=0;for(s&&(p/=2,q/=2),r.save(),i=document.createElement("canvas"),i.width=i.height=u,j=i.getContext("2d"),k=t?b(d,p,q):1,l=Math.ceil(u*g/p),m=Math.ceil(u*h/q/k);q>v;){for(n=0,o=0;p>n;)j.clearRect(0,0,u,u),j.drawImage(d,-n,-v),r.drawImage(i,0,0,u,u,e+o,f+w,l,m),n+=u,o+=l;v+=u,w+=m}r.restore(),i=j=null}:function(b){var c=a.slice(arguments,1),d=b.getContext("2d");d.drawImage.apply(d,c)}}()})}),b("runtime/html5/transport",["base","runtime/html5/runtime"],function(a,b){var c=a.noop,d=a.$;return b.register("Transport",{init:function(){this._status=0,this._response=null},send:function(){var b,c,e,f=this.owner,g=this.options,h=this._initAjax(),i=f._blob,j=g.server;g.sendAsBinary?(j+=(/\?/.test(j)?"&":"?")+d.param(f._formData),c=i.getSource()):(b=new FormData,d.each(f._formData,function(a,c){b.append(a,c)}),b.append(g.fileVal,i.getSource(),g.filename||f._formData.name||"")),g.withCredentials&&"withCredentials"in h?(h.open(g.method,j,!0),h.withCredentials=!0):h.open(g.method,j),this._setRequestHeader(h,g.headers),c?(h.overrideMimeType&&h.overrideMimeType("application/octet-stream"),a.os.android?(e=new FileReader,e.onload=function(){h.send(this.result),e=e.onload=null},e.readAsArrayBuffer(c)):h.send(c)):h.send(b)},getResponse:function(){return this._response},getResponseAsJson:function(){return this._parseJson(this._response)},getStatus:function(){return this._status},abort:function(){var a=this._xhr;a&&(a.upload.onprogress=c,a.onreadystatechange=c,a.abort(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var a=this,b=new XMLHttpRequest,d=this.options;return!d.withCredentials||"withCredentials"in b||"undefined"==typeof XDomainRequest||(b=new XDomainRequest),b.upload.onprogress=function(b){var c=0;return b.lengthComputable&&(c=b.loaded/b.total),a.trigger("progress",c)},b.onreadystatechange=function(){return 4===b.readyState?(b.upload.onprogress=c,b.onreadystatechange=c,a._xhr=null,a._status=b.status,b.status>=200&&b.status<300?(a._response=b.responseText,a.trigger("load")):b.status>=500&&b.status<600?(a._response=b.responseText,a.trigger("error","server")):a.trigger("error",a._status?"http":"abort")):void 0},a._xhr=b,b},_setRequestHeader:function(a,b){d.each(b,function(b,c){a.setRequestHeader(b,c)})},_parseJson:function(a){var b;try{b=JSON.parse(a)}catch(c){b={}}return b}})}),b("runtime/html5/md5",["runtime/html5/runtime"],function(a){var b=function(a,b){return 4294967295&a+b},c=function(a,c,d,e,f,g){return c=b(b(c,a),b(e,g)),b(c<>>32-f,d)},d=function(a,b,d,e,f,g,h){return c(b&d|~b&e,a,b,f,g,h)},e=function(a,b,d,e,f,g,h){return c(b&e|d&~e,a,b,f,g,h)},f=function(a,b,d,e,f,g,h){return c(b^d^e,a,b,f,g,h)},g=function(a,b,d,e,f,g,h){return c(d^(b|~e),a,b,f,g,h)},h=function(a,c){var h=a[0],i=a[1],j=a[2],k=a[3];h=d(h,i,j,k,c[0],7,-680876936),k=d(k,h,i,j,c[1],12,-389564586),j=d(j,k,h,i,c[2],17,606105819),i=d(i,j,k,h,c[3],22,-1044525330),h=d(h,i,j,k,c[4],7,-176418897),k=d(k,h,i,j,c[5],12,1200080426),j=d(j,k,h,i,c[6],17,-1473231341),i=d(i,j,k,h,c[7],22,-45705983),h=d(h,i,j,k,c[8],7,1770035416),k=d(k,h,i,j,c[9],12,-1958414417),j=d(j,k,h,i,c[10],17,-42063),i=d(i,j,k,h,c[11],22,-1990404162),h=d(h,i,j,k,c[12],7,1804603682),k=d(k,h,i,j,c[13],12,-40341101),j=d(j,k,h,i,c[14],17,-1502002290),i=d(i,j,k,h,c[15],22,1236535329),h=e(h,i,j,k,c[1],5,-165796510),k=e(k,h,i,j,c[6],9,-1069501632),j=e(j,k,h,i,c[11],14,643717713),i=e(i,j,k,h,c[0],20,-373897302),h=e(h,i,j,k,c[5],5,-701558691),k=e(k,h,i,j,c[10],9,38016083),j=e(j,k,h,i,c[15],14,-660478335),i=e(i,j,k,h,c[4],20,-405537848),h=e(h,i,j,k,c[9],5,568446438),k=e(k,h,i,j,c[14],9,-1019803690),j=e(j,k,h,i,c[3],14,-187363961),i=e(i,j,k,h,c[8],20,1163531501),h=e(h,i,j,k,c[13],5,-1444681467),k=e(k,h,i,j,c[2],9,-51403784),j=e(j,k,h,i,c[7],14,1735328473),i=e(i,j,k,h,c[12],20,-1926607734),h=f(h,i,j,k,c[5],4,-378558),k=f(k,h,i,j,c[8],11,-2022574463),j=f(j,k,h,i,c[11],16,1839030562),i=f(i,j,k,h,c[14],23,-35309556),h=f(h,i,j,k,c[1],4,-1530992060),k=f(k,h,i,j,c[4],11,1272893353),j=f(j,k,h,i,c[7],16,-155497632),i=f(i,j,k,h,c[10],23,-1094730640),h=f(h,i,j,k,c[13],4,681279174),k=f(k,h,i,j,c[0],11,-358537222),j=f(j,k,h,i,c[3],16,-722521979),i=f(i,j,k,h,c[6],23,76029189),h=f(h,i,j,k,c[9],4,-640364487),k=f(k,h,i,j,c[12],11,-421815835),j=f(j,k,h,i,c[15],16,530742520),i=f(i,j,k,h,c[2],23,-995338651),h=g(h,i,j,k,c[0],6,-198630844),k=g(k,h,i,j,c[7],10,1126891415),j=g(j,k,h,i,c[14],15,-1416354905),i=g(i,j,k,h,c[5],21,-57434055),h=g(h,i,j,k,c[12],6,1700485571),k=g(k,h,i,j,c[3],10,-1894986606),j=g(j,k,h,i,c[10],15,-1051523),i=g(i,j,k,h,c[1],21,-2054922799),h=g(h,i,j,k,c[8],6,1873313359),k=g(k,h,i,j,c[15],10,-30611744),j=g(j,k,h,i,c[6],15,-1560198380),i=g(i,j,k,h,c[13],21,1309151649),h=g(h,i,j,k,c[4],6,-145523070),k=g(k,h,i,j,c[11],10,-1120210379),j=g(j,k,h,i,c[2],15,718787259),i=g(i,j,k,h,c[9],21,-343485551),a[0]=b(h,a[0]),a[1]=b(i,a[1]),a[2]=b(j,a[2]),a[3]=b(k,a[3])},i=function(a){var b,c=[];for(b=0;64>b;b+=4)c[b>>2]=a.charCodeAt(b)+(a.charCodeAt(b+1)<<8)+(a.charCodeAt(b+2)<<16)+(a.charCodeAt(b+3)<<24);return c},j=function(a){var b,c=[];for(b=0;64>b;b+=4)c[b>>2]=a[b]+(a[b+1]<<8)+(a[b+2]<<16)+(a[b+3]<<24);return c},k=function(a){var b,c,d,e,f,g,j=a.length,k=[1732584193,-271733879,-1732584194,271733878];for(b=64;j>=b;b+=64)h(k,i(a.substring(b-64,b)));for(a=a.substring(b-64),c=a.length,d=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],b=0;c>b;b+=1)d[b>>2]|=a.charCodeAt(b)<<(b%4<<3);if(d[b>>2]|=128<<(b%4<<3),b>55)for(h(k,d),b=0;16>b;b+=1)d[b]=0;return e=8*j,e=e.toString(16).match(/(.*?)(.{0,8})$/),f=parseInt(e[2],16),g=parseInt(e[1],16)||0,d[14]=f,d[15]=g,h(k,d),k},l=function(a){var b,c,d,e,f,g,i=a.length,k=[1732584193,-271733879,-1732584194,271733878];for(b=64;i>=b;b+=64)h(k,j(a.subarray(b-64,b)));for(a=i>b-64?a.subarray(b-64):new Uint8Array(0),c=a.length,d=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],b=0;c>b;b+=1)d[b>>2]|=a[b]<<(b%4<<3);if(d[b>>2]|=128<<(b%4<<3),b>55)for(h(k,d),b=0;16>b;b+=1)d[b]=0;return e=8*i,e=e.toString(16).match(/(.*?)(.{0,8})$/),f=parseInt(e[2],16),g=parseInt(e[1],16)||0,d[14]=f,d[15]=g,h(k,d),k},m=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"],n=function(a){var b,c="";for(b=0;4>b;b+=1)c+=m[15&a>>8*b+4]+m[15&a>>8*b];return c},o=function(a){var b;for(b=0;b>16)+(b>>16)+(c>>16);return d<<16|65535&c}),q.prototype.append=function(a){return/[\u0080-\uFFFF]/.test(a)&&(a=unescape(encodeURIComponent(a))),this.appendBinary(a),this},q.prototype.appendBinary=function(a){this._buff+=a,this._length+=a.length;var b,c=this._buff.length;for(b=64;c>=b;b+=64)h(this._state,i(this._buff.substring(b-64,b)));return this._buff=this._buff.substr(b-64),this},q.prototype.end=function(a){var b,c,d=this._buff,e=d.length,f=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(b=0;e>b;b+=1)f[b>>2]|=d.charCodeAt(b)<<(b%4<<3);return this._finish(f,e),c=a?this._state:o(this._state),this.reset(),c},q.prototype._finish=function(a,b){var c,d,e,f=b;if(a[f>>2]|=128<<(f%4<<3),f>55)for(h(this._state,a),f=0;16>f;f+=1)a[f]=0;c=8*this._length,c=c.toString(16).match(/(.*?)(.{0,8})$/),d=parseInt(c[2],16),e=parseInt(c[1],16)||0,a[14]=d,a[15]=e,h(this._state,a)},q.prototype.reset=function(){return this._buff="",this._length=0,this._state=[1732584193,-271733879,-1732584194,271733878],this},q.prototype.destroy=function(){delete this._state,delete this._buff,delete this._length},q.hash=function(a,b){/[\u0080-\uFFFF]/.test(a)&&(a=unescape(encodeURIComponent(a)));var c=k(a);return b?c:o(c) +},q.hashBinary=function(a,b){var c=k(a);return b?c:o(c)},q.ArrayBuffer=function(){this.reset()},q.ArrayBuffer.prototype.append=function(a){var b,c=this._concatArrayBuffer(this._buff,a),d=c.length;for(this._length+=a.byteLength,b=64;d>=b;b+=64)h(this._state,j(c.subarray(b-64,b)));return this._buff=d>b-64?c.subarray(b-64):new Uint8Array(0),this},q.ArrayBuffer.prototype.end=function(a){var b,c,d=this._buff,e=d.length,f=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(b=0;e>b;b+=1)f[b>>2]|=d[b]<<(b%4<<3);return this._finish(f,e),c=a?this._state:o(this._state),this.reset(),c},q.ArrayBuffer.prototype._finish=q.prototype._finish,q.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._state=[1732584193,-271733879,-1732584194,271733878],this},q.ArrayBuffer.prototype.destroy=q.prototype.destroy,q.ArrayBuffer.prototype._concatArrayBuffer=function(a,b){var c=a.length,d=new Uint8Array(c+b.byteLength);return d.set(a),d.set(new Uint8Array(b),c),d},q.ArrayBuffer.hash=function(a,b){var c=l(new Uint8Array(a));return b?c:o(c)},a.register("Md5",{init:function(){},loadFromBlob:function(a){var b,c,d=a.getSource(),e=2097152,f=Math.ceil(d.size/e),g=0,h=this.owner,i=new q.ArrayBuffer,j=this,k=d.mozSlice||d.webkitSlice||d.slice;c=new FileReader,b=function(){var l,m;l=g*e,m=Math.min(l+e,d.size),c.onload=function(b){i.append(b.target.result),h.trigger("progress",{total:a.size,loaded:m})},c.onloadend=function(){c.onloadend=c.onload=null,++g'+''+''+''+"",c.html(a)},getFlash:function(){return this._flash?this._flash:(this._flash=g("#"+this.uid).get(0),this._flash)}}),f.register=function(a,c){return c=i[a]=b.inherits(d,g.extend({flashExec:function(){var a=this.owner,b=this.getRuntime();return b.flashExec.apply(a,arguments)}},c))},e()>=11.4&&c.addRuntime(h,f),f}),b("runtime/flash/filepicker",["base","runtime/flash/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(a){var b,d,e=c.extend({},a);for(b=e.accept&&e.accept.length,d=0;b>d;d++)e.accept[d].title||(e.accept[d].title="Files");delete e.button,delete e.id,delete e.container,this.flashExec("FilePicker","init",e)},destroy:function(){this.flashExec("FilePicker","destroy")}})}),b("runtime/flash/image",["runtime/flash/runtime"],function(a){return a.register("Image",{loadFromBlob:function(a){var b=this.owner;b.info()&&this.flashExec("Image","info",b.info()),b.meta()&&this.flashExec("Image","meta",b.meta()),this.flashExec("Image","loadFromBlob",a.uid)}})}),b("runtime/flash/transport",["base","runtime/flash/runtime","runtime/client"],function(b,c,d){var e=b.$;return c.register("Transport",{init:function(){this._status=0,this._response=null,this._responseJson=null},send:function(){var a,b=this.owner,c=this.options,d=this._initAjax(),f=b._blob,g=c.server;d.connectRuntime(f.ruid),c.sendAsBinary?(g+=(/\?/.test(g)?"&":"?")+e.param(b._formData),a=f.uid):(e.each(b._formData,function(a,b){d.exec("append",a,b)}),d.exec("appendBlob",c.fileVal,f.uid,c.filename||b._formData.name||"")),this._setRequestHeader(d,c.headers),d.exec("send",{method:c.method,url:g,forceURLStream:c.forceURLStream,mimeType:"application/octet-stream"},a)},getStatus:function(){return this._status},getResponse:function(){return this._response||""},getResponseAsJson:function(){return this._responseJson},abort:function(){var a=this._xhr;a&&(a.exec("abort"),a.destroy(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var b=this,c=new d("XMLHttpRequest");return c.on("uploadprogress progress",function(a){var c=a.loaded/a.total;return c=Math.min(1,Math.max(0,c)),b.trigger("progress",c)}),c.on("load",function(){var d,e=c.exec("getStatus"),f=!1,g="";return c.off(),b._xhr=null,e>=200&&300>e?f=!0:e>=500&&600>e?(f=!0,g="server"):g="http",f&&(b._response=c.exec("getResponse"),b._response=decodeURIComponent(b._response),d=function(b){try{return a.JSON&&a.JSON.parse?JSON.parse(b):new Function("return "+b).call()}catch(c){return{}}},b._responseJson=b._response?d(b._response):{}),c.destroy(),c=null,g?b.trigger("error",g):b.trigger("load")}),c.on("error",function(){c.off(),b._xhr=null,b.trigger("error","http")}),b._xhr=c,c},_setRequestHeader:function(a,b){e.each(b,function(b,c){a.exec("setRequestHeader",b,c)})}})}),b("runtime/flash/blob",["runtime/flash/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.flashExec("Blob","slice",a,c);return new b(this.getRuid(),d)}})}),b("runtime/flash/md5",["runtime/flash/runtime"],function(a){return a.register("Md5",{init:function(){},loadFromBlob:function(a){return this.flashExec("Md5","loadFromBlob",a.uid)}})}),b("preset/all",["base","widgets/filednd","widgets/filepaste","widgets/filepicker","widgets/image","widgets/queue","widgets/runtime","widgets/upload","widgets/validator","widgets/md5","runtime/html5/blob","runtime/html5/dnd","runtime/html5/filepaste","runtime/html5/filepicker","runtime/html5/imagemeta/exif","runtime/html5/androidpatch","runtime/html5/image","runtime/html5/transport","runtime/html5/md5","runtime/flash/filepicker","runtime/flash/image","runtime/flash/transport","runtime/flash/blob","runtime/flash/md5"],function(a){return a}),b("webuploader",["preset/all"],function(a){return a}),c("webuploader")}); \ No newline at end of file diff --git a/public/static/libs/webuploader/webuploader.withoutimage.js b/public/static/libs/webuploader/webuploader.withoutimage.js new file mode 100644 index 0000000..2e78c83 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.withoutimage.js @@ -0,0 +1,4993 @@ +/*! WebUploader 0.1.5 */ + + +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + */ + define('dollar-third',[],function() { + var $ = window.__dollar || window.jQuery || window.Zepto; + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 使用jQuery的Promise + */ + define('promise-third',[ + 'dollar' + ], function( $ ) { + return { + Deferred: $.Deferred, + when: $.when, + + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + } + }; + }); + /** + * @fileOverview Promise/A+ + */ + define('promise',[ + 'promise-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.5', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview 错误信息 + */ + define('lib/dnd',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function DragAndDrop( opts ) { + opts = this.options = $.extend({}, DragAndDrop.options, opts ); + + opts.container = $( opts.container ); + + if ( !opts.container.length ) { + return; + } + + RuntimeClent.call( this, 'DragAndDrop' ); + } + + DragAndDrop.options = { + accept: null, + disableGlobalDnd: false + }; + + Base.inherits( RuntimeClent, { + constructor: DragAndDrop, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( DragAndDrop.prototype ); + + return DragAndDrop; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview DragAndDrop Widget。 + */ + define('widgets/filednd',[ + 'base', + 'uploader', + 'lib/dnd', + 'widgets/widget' + ], function( Base, Uploader, Dnd ) { + var $ = Base.$; + + Uploader.options.dnd = ''; + + /** + * @property {Selector} [dnd=undefined] 指定Drag And Drop拖拽的容器,如果不指定,则不启动。 + * @namespace options + * @for Uploader + */ + + /** + * @property {Selector} [disableGlobalDnd=false] 是否禁掉整个页面的拖拽功能,如果不禁用,图片拖进来的时候会默认被浏览器打开。 + * @namespace options + * @for Uploader + */ + + /** + * @event dndAccept + * @param {DataTransferItemList} items DataTransferItem + * @description 阻止此事件可以拒绝某些类型的文件拖入进来。目前只有 chrome 提供这样的 API,且只能通过 mime-type 验证。 + * @for Uploader + */ + return Uploader.register({ + name: 'dnd', + + init: function( opts ) { + + if ( !opts.dnd || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + disableGlobalDnd: opts.disableGlobalDnd, + container: opts.dnd, + accept: opts.accept + }), + dnd; + + this.dnd = dnd = new Dnd( options ); + + dnd.once( 'ready', deferred.resolve ); + dnd.on( 'drop', function( files ) { + me.request( 'add-file', [ files ]); + }); + + // 检测文件是否全部允许添加。 + dnd.on( 'accept', function( items ) { + return me.owner.trigger( 'dndAccept', items ); + }); + + dnd.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.dnd && this.dnd.destroy(); + } + }); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepaste',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function FilePaste( opts ) { + opts = this.options = $.extend({}, opts ); + opts.container = $( opts.container || document.body ); + RuntimeClent.call( this, 'FilePaste' ); + } + + Base.inherits( RuntimeClent, { + constructor: FilePaste, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( FilePaste.prototype ); + + return FilePaste; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/filepaste',[ + 'base', + 'uploader', + 'lib/filepaste', + 'widgets/widget' + ], function( Base, Uploader, FilePaste ) { + var $ = Base.$; + + /** + * @property {Selector} [paste=undefined] 指定监听paste事件的容器,如果不指定,不启用此功能。此功能为通过粘贴来添加截屏的图片。建议设置为`document.body`. + * @namespace options + * @for Uploader + */ + return Uploader.register({ + name: 'paste', + + init: function( opts ) { + + if ( !opts.paste || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + container: opts.paste, + accept: opts.accept + }), + paste; + + this.paste = paste = new FilePaste( options ); + + paste.once( 'ready', deferred.resolve ); + paste.on( 'paste', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + paste.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.paste && this.paste.destroy(); + } + }); + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClent, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClent.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file' + }; + + Base.inherits( RuntimeClent, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button; + + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor} 指定选择文件的按钮容器,不指定则不创建按钮。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addButton + * @for Uploader + * @grammar addButton( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addButton({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0, + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [method='POST'] + * @namespace options + * @for Uploader + * @description 文件上传方式,`POST`或者`GET`。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + }); + + file.setStatus( Status.QUEUED ); + } else if (file.getStatus() === Status.PROGRESS) { + return; + } else { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return; + } + + me.runing = true; + + // 如果有暂停的,则续传 + $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + file.setStatus( Status.PROGRESS ); + me._trigged = false; + v.transport && v.transport.send(); + } + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.abort(); + me._putback(v); + me._popBlock(v); + }); + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + var totalPercent = 0, + uploaded = 0; + + // 可能没有abort掉,progress还是执行进来了。 + // if ( !file.blocks ) { + // return; + // } + + totalPercent = block.percentage = percentage; + + if ( block.chunks > 1 ) { // 计算文件的整体速度。 + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + } + + owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + } + + }); + }); + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePaste + */ + define('runtime/html5/dnd',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + var $ = Base.$, + prefix = 'webuploader-dnd-'; + + return Html5Runtime.register( 'DragAndDrop', { + init: function() { + var elem = this.elem = this.options.container; + + this.dragEnterHandler = Base.bindFn( this._dragEnterHandler, this ); + this.dragOverHandler = Base.bindFn( this._dragOverHandler, this ); + this.dragLeaveHandler = Base.bindFn( this._dragLeaveHandler, this ); + this.dropHandler = Base.bindFn( this._dropHandler, this ); + this.dndOver = false; + + elem.on( 'dragenter', this.dragEnterHandler ); + elem.on( 'dragover', this.dragOverHandler ); + elem.on( 'dragleave', this.dragLeaveHandler ); + elem.on( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).on( 'dragover', this.dragOverHandler ); + $( document ).on( 'drop', this.dropHandler ); + } + }, + + _dragEnterHandler: function( e ) { + var me = this, + denied = me._denied || false, + items; + + e = e.originalEvent || e; + + if ( !me.dndOver ) { + me.dndOver = true; + + // 注意只有 chrome 支持。 + items = e.dataTransfer.items; + + if ( items && items.length ) { + me._denied = denied = !me.trigger( 'accept', items ); + } + + me.elem.addClass( prefix + 'over' ); + me.elem[ denied ? 'addClass' : + 'removeClass' ]( prefix + 'denied' ); + } + + e.dataTransfer.dropEffect = denied ? 'none' : 'copy'; + + return false; + }, + + _dragOverHandler: function( e ) { + // 只处理框内的。 + var parentElem = this.elem.parent().get( 0 ); + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + clearTimeout( this._leaveTimer ); + this._dragEnterHandler.call( this, e ); + + return false; + }, + + _dragLeaveHandler: function() { + var me = this, + handler; + + handler = function() { + me.dndOver = false; + me.elem.removeClass( prefix + 'over ' + prefix + 'denied' ); + }; + + clearTimeout( me._leaveTimer ); + me._leaveTimer = setTimeout( handler, 100 ); + return false; + }, + + _dropHandler: function( e ) { + var me = this, + ruid = me.getRuid(), + parentElem = me.elem.parent().get( 0 ), + dataTransfer, data; + + // 只处理框内的。 + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + e = e.originalEvent || e; + dataTransfer = e.dataTransfer; + + // 如果是页面内拖拽,还不能处理,不阻止事件。 + // 此处 ie11 下会报参数错误, + try { + data = dataTransfer.getData('text/html'); + } catch( err ) { + } + + if ( data ) { + return; + } + + me._getTansferFiles( dataTransfer, function( results ) { + me.trigger( 'drop', $.map( results, function( file ) { + return new File( ruid, file ); + }) ); + }); + + me.dndOver = false; + me.elem.removeClass( prefix + 'over' ); + return false; + }, + + // 如果传入 callback 则去查看文件夹,否则只管当前文件夹。 + _getTansferFiles: function( dataTransfer, callback ) { + var results = [], + promises = [], + items, files, file, item, i, len, canAccessFolder; + + items = dataTransfer.items; + files = dataTransfer.files; + + canAccessFolder = !!(items && items[ 0 ].webkitGetAsEntry); + + for ( i = 0, len = files.length; i < len; i++ ) { + file = files[ i ]; + item = items && items[ i ]; + + if ( canAccessFolder && item.webkitGetAsEntry().isDirectory ) { + + promises.push( this._traverseDirectoryTree( + item.webkitGetAsEntry(), results ) ); + } else { + results.push( file ); + } + } + + Base.when.apply( Base, promises ).done(function() { + + if ( !results.length ) { + return; + } + + callback( results ); + }); + }, + + _traverseDirectoryTree: function( entry, results ) { + var deferred = Base.Deferred(), + me = this; + + if ( entry.isFile ) { + entry.file(function( file ) { + results.push( file ); + deferred.resolve(); + }); + } else if ( entry.isDirectory ) { + entry.createReader().readEntries(function( entries ) { + var len = entries.length, + promises = [], + arr = [], // 为了保证顺序。 + i; + + for ( i = 0; i < len; i++ ) { + promises.push( me._traverseDirectoryTree( + entries[ i ], arr ) ); + } + + Base.when.apply( Base, promises ).then(function() { + results.push.apply( results, arr ); + deferred.resolve(); + }, deferred.reject ); + }); + } + + return deferred.promise(); + }, + + destroy: function() { + var elem = this.elem; + + // 还没 init 就调用 destroy + if (!elem) { + return; + } + + elem.off( 'dragenter', this.dragEnterHandler ); + elem.off( 'dragover', this.dragOverHandler ); + elem.off( 'dragleave', this.dragLeaveHandler ); + elem.off( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).off( 'dragover', this.dragOverHandler ); + $( document ).off( 'drop', this.dropHandler ); + } + } + }); + }); + + /** + * @fileOverview FilePaste + */ + define('runtime/html5/filepaste',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + return Html5Runtime.register( 'FilePaste', { + init: function() { + var opts = this.options, + elem = this.elem = opts.container, + accept = '.*', + arr, i, len, item; + + // accetp的mimeTypes中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].mimeTypes; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = arr.join(','); + accept = accept.replace( /,/g, '|' ).replace( /\*/g, '.*' ); + } + } + this.accept = accept = new RegExp( accept, 'i' ); + this.hander = Base.bindFn( this._pasteHander, this ); + elem.on( 'paste', this.hander ); + }, + + _pasteHander: function( e ) { + var allowed = [], + ruid = this.getRuid(), + items, item, blob, i, len; + + e = e.originalEvent || e; + items = e.clipboardData.items; + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + + if ( item.kind !== 'file' || !(blob = item.getAsFile()) ) { + continue; + } + + allowed.push( new File( ruid, blob ) ); + } + + if ( allowed.length ) { + // 不阻止非文件粘贴(文字粘贴)的事件冒泡 + e.preventDefault(); + e.stopPropagation(); + this.trigger( 'paste', allowed ); + } + }, + + destroy: function() { + this.elem.off( 'paste', this.hander ); + } + }); + }); + + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function() { + input.trigger('click'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + /** + * @fileOverview FlashRuntime + */ + define('runtime/flash/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var $ = Base.$, + type = 'flash', + components = {}; + + + function getFlashVersion() { + var version; + + try { + version = navigator.plugins[ 'Shockwave Flash' ]; + version = version.description; + } catch ( ex ) { + try { + version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash') + .GetVariable('$version'); + } catch ( ex2 ) { + version = '0.0'; + } + } + version = version.match( /\d+/g ); + return parseFloat( version[ 0 ] + '.' + version[ 1 ], 10 ); + } + + function FlashRuntime() { + var pool = {}, + clients = {}, + destroy = this.destroy, + me = this, + jsreciver = Base.guid('webuploader_'); + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/ ) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + clients[ uid ] = client; + + if ( components[ comp ] ) { + if ( !pool[ uid ] ) { + pool[ uid ] = new components[ comp ]( client, me ); + } + + instance = pool[ uid ]; + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + + return me.flashExec.apply( client, arguments ); + }; + + function handler( evt, obj ) { + var type = evt.type || evt, + parts, uid; + + parts = type.split('::'); + uid = parts[ 0 ]; + type = parts[ 1 ]; + + // console.log.apply( console, arguments ); + + if ( type === 'Ready' && uid === me.uid ) { + me.trigger('ready'); + } else if ( clients[ uid ] ) { + clients[ uid ].trigger( type.toLowerCase(), evt, obj ); + } + + // Base.log( evt, obj ); + } + + // flash的接受器。 + window[ jsreciver ] = function() { + var args = arguments; + + // 为了能捕获得到。 + setTimeout(function() { + handler.apply( null, args ); + }, 1 ); + }; + + this.jsreciver = jsreciver; + + this.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + + this.flashExec = function( comp, fn ) { + var flash = me.getFlash(), + args = Base.slice( arguments, 2 ); + + return flash.exec( this.uid, comp, fn, args ); + }; + + // @todo + } + + Base.inherits( Runtime, { + constructor: FlashRuntime, + + init: function() { + var container = this.getContainer(), + opts = this.options, + html; + + // 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) + container.css({ + position: 'absolute', + top: '-8px', + left: '-8px', + width: '9px', + height: '9px', + overflow: 'hidden' + }); + + // insert flash object + html = '' + + '' + + '' + + '' + + ''; + + container.html( html ); + }, + + getFlash: function() { + if ( this._flash ) { + return this._flash; + } + + this._flash = $( '#' + this.uid ).get( 0 ); + return this._flash; + } + + }); + + FlashRuntime.register = function( name, component ) { + component = components[ name ] = Base.inherits( CompBase, $.extend({ + + // @todo fix this later + flashExec: function() { + var owner = this.owner, + runtime = this.getRuntime(); + + return runtime.flashExec.apply( owner, arguments ); + } + }, component ) ); + + return component; + }; + + if ( getFlashVersion() >= 11.4 ) { + Runtime.addRuntime( type, FlashRuntime ); + } + + return FlashRuntime; + }); + /** + * @fileOverview FilePicker + */ + define('runtime/flash/filepicker',[ + 'base', + 'runtime/flash/runtime' + ], function( Base, FlashRuntime ) { + var $ = Base.$; + + return FlashRuntime.register( 'FilePicker', { + init: function( opts ) { + var copy = $.extend({}, opts ), + len, i; + + // 修复Flash再没有设置title的情况下无法弹出flash文件选择框的bug. + len = copy.accept && copy.accept.length; + for ( i = 0; i < len; i++ ) { + if ( !copy.accept[ i ].title ) { + copy.accept[ i ].title = 'Files'; + } + } + + delete copy.button; + delete copy.id; + delete copy.container; + + this.flashExec( 'FilePicker', 'init', copy ); + }, + + destroy: function() { + this.flashExec( 'FilePicker', 'destroy' ); + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/flash/transport',[ + 'base', + 'runtime/flash/runtime', + 'runtime/client' + ], function( Base, FlashRuntime, RuntimeClient ) { + var $ = Base.$; + + return FlashRuntime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + this._responseJson = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + binary; + + xhr.connectRuntime( blob.ruid ); + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.uid; + } else { + $.each( owner._formData, function( k, v ) { + xhr.exec( 'append', k, v ); + }); + + xhr.exec( 'appendBlob', opts.fileVal, blob.uid, + opts.filename || owner._formData.name || '' ); + } + + this._setRequestHeader( xhr, opts.headers ); + xhr.exec( 'send', { + method: opts.method, + url: server, + forceURLStream: opts.forceURLStream, + mimeType: 'application/octet-stream' + }, binary ); + }, + + getStatus: function() { + return this._status; + }, + + getResponse: function() { + return this._response || ''; + }, + + getResponseAsJson: function() { + return this._responseJson; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.exec('abort'); + xhr.destroy(); + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new RuntimeClient('XMLHttpRequest'); + + xhr.on( 'uploadprogress progress', function( e ) { + var percent = e.loaded / e.total; + percent = Math.min( 1, Math.max( 0, percent ) ); + return me.trigger( 'progress', percent ); + }); + + xhr.on( 'load', function() { + var status = xhr.exec('getStatus'), + readBody = false, + err = '', + p; + + xhr.off(); + me._xhr = null; + + if ( status >= 200 && status < 300 ) { + readBody = true; + } else if ( status >= 500 && status < 600 ) { + readBody = true; + err = 'server'; + } else { + err = 'http'; + } + + if ( readBody ) { + me._response = xhr.exec('getResponse'); + me._response = decodeURIComponent( me._response ); + + // flash 处理可能存在 bug, 没辙只能靠 js 了 + // try { + // me._responseJson = xhr.exec('getResponseAsJson'); + // } catch ( error ) { + + p = window.JSON && window.JSON.parse || function( s ) { + try { + return new Function('return ' + s).call(); + } catch ( err ) { + return {}; + } + }; + me._responseJson = me._response ? p(me._response) : {}; + + // } + } + + xhr.destroy(); + xhr = null; + + return err ? me.trigger( 'error', err ) : me.trigger('load'); + }); + + xhr.on( 'error', function() { + xhr.off(); + me._xhr = null; + me.trigger( 'error', 'http' ); + }); + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.exec( 'setRequestHeader', key, val ); + }); + } + }); + }); + /** + * @fileOverview 没有图像处理的版本。 + */ + define('preset/withoutimage',[ + 'base', + + // widgets + 'widgets/filednd', + 'widgets/filepaste', + 'widgets/filepicker', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + + // runtimes + // html5 + 'runtime/html5/blob', + 'runtime/html5/dnd', + 'runtime/html5/filepaste', + 'runtime/html5/filepicker', + 'runtime/html5/transport', + + // flash + 'runtime/flash/filepicker', + 'runtime/flash/transport' + ], function( Base ) { + return Base; + }); + define('webuploader',[ + 'preset/withoutimage' + ], function( preset ) { + return preset; + }); + return require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.withoutimage.min.js b/public/static/libs/webuploader/webuploader.withoutimage.min.js new file mode 100644 index 0000000..97361a6 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.withoutimage.min.js @@ -0,0 +1,2 @@ +/* WebUploader 0.1.5 */!function(a,b){var c,d={},e=function(a,b){var c,d,e;if("string"==typeof a)return h(a);for(c=[],d=a.length,e=0;d>e;e++)c.push(h(a[e]));return b.apply(null,c)},f=function(a,b,c){2===arguments.length&&(c=b,b=null),e(b||[],function(){g(a,c,arguments)})},g=function(a,b,c){var f,g={exports:b};"function"==typeof b&&(c.length||(c=[e,g.exports,g]),f=b.apply(null,c),void 0!==f&&(g.exports=f)),d[a]=g.exports},h=function(b){var c=d[b]||a[b];if(!c)throw new Error("`"+b+"` is undefined");return c},i=function(a){var b,c,e,f,g,h;h=function(a){return a&&a.charAt(0).toUpperCase()+a.substr(1)};for(b in d)if(c=a,d.hasOwnProperty(b)){for(e=b.split("/"),g=h(e.pop());f=h(e.shift());)c[f]=c[f]||{},c=c[f];c[g]=d[b]}return a},j=function(c){return a.__dollar=c,i(b(a,f,e))};"object"==typeof module&&"object"==typeof module.exports?module.exports=j():"function"==typeof define&&define.amd?define(["jquery"],j):(c=a.WebUploader,a.WebUploader=j(),a.WebUploader.noConflict=function(){a.WebUploader=c})}(window,function(a,b,c){return b("dollar-third",[],function(){var b=a.__dollar||a.jQuery||a.Zepto;if(!b)throw new Error("jQuery or Zepto not found!");return b}),b("dollar",["dollar-third"],function(a){return a}),b("promise-third",["dollar"],function(a){return{Deferred:a.Deferred,when:a.when,isPromise:function(a){return a&&"function"==typeof a.then}}}),b("promise",["promise-third"],function(a){return a}),b("base",["dollar","promise"],function(b,c){function d(a){return function(){return h.apply(a,arguments)}}function e(a,b){return function(){return a.apply(b,arguments)}}function f(a){var b;return Object.create?Object.create(a):(b=function(){},b.prototype=a,new b)}var g=function(){},h=Function.call;return{version:"0.1.5",$:b,Deferred:c.Deferred,isPromise:c.isPromise,when:c.when,browser:function(a){var b={},c=a.match(/WebKit\/([\d.]+)/),d=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),e=a.match(/MSIE\s([\d\.]+)/)||a.match(/(?:trident)(?:.*rv:([\w.]+))?/i),f=a.match(/Firefox\/([\d.]+)/),g=a.match(/Safari\/([\d.]+)/),h=a.match(/OPR\/([\d.]+)/);return c&&(b.webkit=parseFloat(c[1])),d&&(b.chrome=parseFloat(d[1])),e&&(b.ie=parseFloat(e[1])),f&&(b.firefox=parseFloat(f[1])),g&&(b.safari=parseFloat(g[1])),h&&(b.opera=parseFloat(h[1])),b}(navigator.userAgent),os:function(a){var b={},c=a.match(/(?:Android);?[\s\/]+([\d.]+)?/),d=a.match(/(?:iPad|iPod|iPhone).*OS\s([\d_]+)/);return c&&(b.android=parseFloat(c[1])),d&&(b.ios=parseFloat(d[1].replace(/_/g,"."))),b}(navigator.userAgent),inherits:function(a,c,d){var e;return"function"==typeof c?(e=c,c=null):e=c&&c.hasOwnProperty("constructor")?c.constructor:function(){return a.apply(this,arguments)},b.extend(!0,e,a,d||{}),e.__super__=a.prototype,e.prototype=f(a.prototype),c&&b.extend(!0,e.prototype,c),e},noop:g,bindFn:e,log:function(){return a.console?e(console.log,console):g}(),nextTick:function(){return function(a){setTimeout(a,1)}}(),slice:d([].slice),guid:function(){var a=0;return function(b){for(var c=(+new Date).toString(32),d=0;5>d;d++)c+=Math.floor(65535*Math.random()).toString(32);return(b||"wu_")+c+(a++).toString(32)}}(),formatSize:function(a,b,c){var d;for(c=c||["B","K","M","G","TB"];(d=c.shift())&&a>1024;)a/=1024;return("B"===d?a:a.toFixed(b||2))+d}}}),b("mediator",["base"],function(a){function b(a,b,c,d){return f.grep(a,function(a){return!(!a||b&&a.e!==b||c&&a.cb!==c&&a.cb._cb!==c||d&&a.ctx!==d)})}function c(a,b,c){f.each((a||"").split(h),function(a,d){c(d,b)})}function d(a,b){for(var c,d=!1,e=-1,f=a.length;++e1?void(d.isPlainObject(b)&&d.isPlainObject(c[a])?d.extend(c[a],b):c[a]=b):a?c[a]:c},getStats:function(){var a=this.request("get-stats");return a?{successNum:a.numOfSuccess,progressNum:a.numOfProgress,cancelNum:a.numOfCancel,invalidNum:a.numOfInvalid,uploadFailNum:a.numOfUploadFailed,queueNum:a.numOfQueue,interruptNum:a.numofInterrupt}:{}},trigger:function(a){var c=[].slice.call(arguments,1),e=this.options,f="on"+a.substring(0,1).toUpperCase()+a.substring(1);return b.trigger.apply(this,arguments)===!1||d.isFunction(e[f])&&e[f].apply(this,c)===!1||d.isFunction(this[f])&&this[f].apply(this,c)===!1||b.trigger.apply(b,[this,a].concat(c))===!1?!1:!0},destroy:function(){this.request("destroy",arguments),this.off()},request:a.noop}),a.create=c.create=function(a){return new c(a)},a.Uploader=c,c}),b("runtime/runtime",["base","mediator"],function(a,b){function c(b){this.options=d.extend({container:document.body},b),this.uid=a.guid("rt_")}var d=a.$,e={},f=function(a){for(var b in a)if(a.hasOwnProperty(b))return b;return null};return d.extend(c.prototype,{getContainer:function(){var a,b,c=this.options;return this._container?this._container:(a=d(c.container||document.body),b=d(document.createElement("div")),b.attr("id","rt_"+this.uid),b.css({position:"absolute",top:"0px",left:"0px",width:"1px",height:"1px",overflow:"hidden"}),a.append(b),a.addClass("webuploader-container"),this._container=b,this._parent=a,b)},init:a.noop,exec:a.noop,destroy:function(){this._container&&this._container.remove(),this._parent&&this._parent.removeClass("webuploader-container"),this.off()}}),c.orders="html5,flash",c.addRuntime=function(a,b){e[a]=b},c.hasRuntime=function(a){return!!(a?e[a]:f(e))},c.create=function(a,b){var g,h;if(b=b||c.orders,d.each(b.split(/\s*,\s*/g),function(){return e[this]?(g=this,!1):void 0}),g=g||f(e),!g)throw new Error("Runtime Error");return h=new e[g](a)},b.installTo(c.prototype),c}),b("runtime/client",["base","mediator","runtime/runtime"],function(a,b,c){function d(b,d){var f,g=a.Deferred();this.uid=a.guid("client_"),this.runtimeReady=function(a){return g.done(a)},this.connectRuntime=function(b,h){if(f)throw new Error("already connected!");return g.done(h),"string"==typeof b&&e.get(b)&&(f=e.get(b)),f=f||e.get(null,d),f?(a.$.extend(f.options,b),f.__promise.then(g.resolve),f.__client++):(f=c.create(b,b.runtimeOrder),f.__promise=g.promise(),f.once("ready",g.resolve),f.init(),e.add(f),f.__client=1),d&&(f.__standalone=d),f},this.getRuntime=function(){return f},this.disconnectRuntime=function(){f&&(f.__client--,f.__client<=0&&(e.remove(f),delete f.__promise,f.destroy()),f=null)},this.exec=function(){if(f){var c=a.slice(arguments);return b&&c.unshift(b),f.exec.apply(this,c)}},this.getRuid=function(){return f&&f.uid},this.destroy=function(a){return function(){a&&a.apply(this,arguments),this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()}}(this.destroy)}var e;return e=function(){var a={};return{add:function(b){a[b.uid]=b},get:function(b,c){var d;if(b)return a[b];for(d in a)if(!c||!a[d].__standalone)return a[d];return null},remove:function(b){delete a[b.uid]}}}(),b.installTo(d.prototype),d}),b("lib/dnd",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},d.options,a),a.container=e(a.container),a.container.length&&c.call(this,"DragAndDrop")}var e=a.$;return d.options={accept:null,disableGlobalDnd:!1},a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/widget",["base","uploader"],function(a,b){function c(a){if(!a)return!1;var b=a.length,c=e.type(a);return 1===a.nodeType&&b?!0:"array"===c||"function"!==c&&"string"!==c&&(0===b||"number"==typeof b&&b>0&&b-1 in a)}function d(a){this.owner=a,this.options=a.options}var e=a.$,f=b.prototype._init,g=b.prototype.destroy,h={},i=[];return e.extend(d.prototype,{init:a.noop,invoke:function(a,b){var c=this.responseMap;return c&&a in c&&c[a]in this&&e.isFunction(this[c[a]])?this[c[a]].apply(this,b):h},request:function(){return this.owner.request.apply(this.owner,arguments)}}),e.extend(b.prototype,{_init:function(){var a=this,b=a._widgets=[],c=a.options.disableWidgets||"";return e.each(i,function(d,e){(!c||!~c.indexOf(e._name))&&b.push(new e(a))}),f.apply(a,arguments)},request:function(b,d,e){var f,g,i,j,k=0,l=this._widgets,m=l&&l.length,n=[],o=[];for(d=c(d)?d:[d];m>k;k++)f=l[k],g=f.invoke(b,d),g!==h&&(a.isPromise(g)?o.push(g):n.push(g));return e||o.length?(i=a.when.apply(a,o),j=i.pipe?"pipe":"then",i[j](function(){var b=a.Deferred(),c=arguments;return 1===c.length&&(c=c[0]),setTimeout(function(){b.resolve(c)},1),b.promise()})[e?j:"done"](e||a.noop)):n[0]},destroy:function(){g.apply(this,arguments),this._widgets=null}}),b.register=d.register=function(b,c){var f,g={init:"init",destroy:"destroy",name:"anonymous"};return 1===arguments.length?(c=b,e.each(c,function(a){return"_"===a[0]||"name"===a?void("name"===a&&(g.name=c.name)):void(g[a.replace(/[A-Z]/g,"-$&").toLowerCase()]=a)})):g=e.extend(g,b),c.responseMap=g,f=a.inherits(d,c),f._name=g.name,i.push(f),f},b.unRegister=d.unRegister=function(a){if(a&&"anonymous"!==a)for(var b=i.length;b--;)i[b]._name===a&&i.splice(b,1)},d}),b("widgets/filednd",["base","uploader","lib/dnd","widgets/widget"],function(a,b,c){var d=a.$;return b.options.dnd="",b.register({name:"dnd",init:function(b){if(b.dnd&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{disableGlobalDnd:b.disableGlobalDnd,container:b.dnd,accept:b.accept});return this.dnd=e=new c(h),e.once("ready",g.resolve),e.on("drop",function(a){f.request("add-file",[a])}),e.on("accept",function(a){return f.owner.trigger("dndAccept",a)}),e.init(),g.promise()}},destroy:function(){this.dnd&&this.dnd.destroy()}})}),b("lib/filepaste",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},a),a.container=e(a.container||document.body),c.call(this,"FilePaste")}var e=a.$;return a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/filepaste",["base","uploader","lib/filepaste","widgets/widget"],function(a,b,c){var d=a.$;return b.register({name:"paste",init:function(b){if(b.paste&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{container:b.paste,accept:b.accept});return this.paste=e=new c(h),e.once("ready",g.resolve),e.on("paste",function(a){f.owner.request("add-file",[a])}),e.init(),g.promise()}},destroy:function(){this.paste&&this.paste.destroy()}})}),b("lib/blob",["base","runtime/client"],function(a,b){function c(a,c){var d=this;d.source=c,d.ruid=a,this.size=c.size||0,this.type=!c.type&&this.ext&&~"jpg,jpeg,png,gif,bmp".indexOf(this.ext)?"image/"+("jpg"===this.ext?"jpeg":this.ext):c.type||"application/octet-stream",b.call(d,"Blob"),this.uid=c.uid||this.uid,a&&d.connectRuntime(a)}return a.inherits(b,{constructor:c,slice:function(a,b){return this.exec("slice",a,b)},getSource:function(){return this.source}}),c}),b("lib/file",["base","lib/blob"],function(a,b){function c(a,c){var f;this.name=c.name||"untitled"+d++,f=e.exec(c.name)?RegExp.$1.toLowerCase():"",!f&&c.type&&(f=/\/(jpg|jpeg|png|gif|bmp)$/i.exec(c.type)?RegExp.$1.toLowerCase():"",this.name+="."+f),this.ext=f,this.lastModifiedDate=c.lastModifiedDate||(new Date).toLocaleString(),b.apply(this,arguments)}var d=1,e=/\.([^.]+)$/;return a.inherits(b,c)}),b("lib/filepicker",["base","runtime/client","lib/file"],function(b,c,d){function e(a){if(a=this.options=f.extend({},e.options,a),a.container=f(a.id),!a.container.length)throw new Error("按钮指定错误");a.innerHTML=a.innerHTML||a.label||a.container.html()||"",a.button=f(a.button||document.createElement("div")),a.button.html(a.innerHTML),a.container.html(a.button),c.call(this,"FilePicker",!0)}var f=b.$;return e.options={button:null,container:null,label:null,innerHTML:null,multiple:!0,accept:null,name:"file"},b.inherits(c,{constructor:e,init:function(){var c=this,e=c.options,g=e.button;g.addClass("webuploader-pick"),c.on("all",function(a){var b;switch(a){case"mouseenter":g.addClass("webuploader-pick-hover");break;case"mouseleave":g.removeClass("webuploader-pick-hover");break;case"change":b=c.exec("getFiles"),c.trigger("select",f.map(b,function(a){return a=new d(c.getRuid(),a),a._refer=e.container,a}),e.container)}}),c.connectRuntime(e,function(){c.refresh(),c.exec("init",e),c.trigger("ready")}),this._resizeHandler=b.bindFn(this.refresh,this),f(a).on("resize",this._resizeHandler)},refresh:function(){var a=this.getRuntime().getContainer(),b=this.options.button,c=b.outerWidth?b.outerWidth():b.width(),d=b.outerHeight?b.outerHeight():b.height(),e=b.offset();c&&d&&a.css({bottom:"auto",right:"auto",width:c+"px",height:d+"px"}).offset(e)},enable:function(){var a=this.options.button;a.removeClass("webuploader-pick-disable"),this.refresh()},disable:function(){var a=this.options.button;this.getRuntime().getContainer().css({top:"-99999px"}),a.addClass("webuploader-pick-disable")},destroy:function(){var b=this.options.button;f(a).off("resize",this._resizeHandler),b.removeClass("webuploader-pick-disable webuploader-pick-hover webuploader-pick")}}),e}),b("widgets/filepicker",["base","uploader","lib/filepicker","widgets/widget"],function(a,b,c){var d=a.$;return d.extend(b.options,{pick:null,accept:null}),b.register({name:"picker",init:function(a){return this.pickers=[],a.pick&&this.addBtn(a.pick)},refresh:function(){d.each(this.pickers,function(){this.refresh()})},addBtn:function(b){var e=this,f=e.options,g=f.accept,h=[];if(b)return d.isPlainObject(b)||(b={id:b}),d(b.id).each(function(){var i,j,k;k=a.Deferred(),i=d.extend({},b,{accept:d.isPlainObject(g)?[g]:g,swf:f.swf,runtimeOrder:f.runtimeOrder,id:this}),j=new c(i),j.once("ready",k.resolve),j.on("select",function(a){e.owner.request("add-file",[a])}),j.init(),e.pickers.push(j),h.push(k.promise())}),a.when.apply(a,h)},disable:function(){d.each(this.pickers,function(){this.disable()})},enable:function(){d.each(this.pickers,function(){this.enable()})},destroy:function(){d.each(this.pickers,function(){this.destroy()}),this.pickers=null}})}),b("file",["base","mediator"],function(a,b){function c(){return f+g++}function d(a){this.name=a.name||"Untitled",this.size=a.size||0,this.type=a.type||"application/octet-stream",this.lastModifiedDate=a.lastModifiedDate||1*new Date,this.id=c(),this.ext=h.exec(this.name)?RegExp.$1:"",this.statusText="",i[this.id]=d.Status.INITED,this.source=a,this.loaded=0,this.on("error",function(a){this.setStatus(d.Status.ERROR,a)})}var e=a.$,f="WU_FILE_",g=0,h=/\.([^.]+)$/,i={};return e.extend(d.prototype,{setStatus:function(a,b){var c=i[this.id];"undefined"!=typeof b&&(this.statusText=b),a!==c&&(i[this.id]=a,this.trigger("statuschange",a,c))},getStatus:function(){return i[this.id]},getSource:function(){return this.source},destroy:function(){this.off(),delete i[this.id]}}),b.installTo(d.prototype),d.Status={INITED:"inited",QUEUED:"queued",PROGRESS:"progress",ERROR:"error",COMPLETE:"complete",CANCELLED:"cancelled",INTERRUPT:"interrupt",INVALID:"invalid"},d}),b("queue",["base","mediator","file"],function(a,b,c){function d(){this.stats={numOfQueue:0,numOfSuccess:0,numOfCancel:0,numOfProgress:0,numOfUploadFailed:0,numOfInvalid:0,numofDeleted:0,numofInterrupt:0},this._queue=[],this._map={}}var e=a.$,f=c.Status;return e.extend(d.prototype,{append:function(a){return this._queue.push(a),this._fileAdded(a),this},prepend:function(a){return this._queue.unshift(a),this._fileAdded(a),this},getFile:function(a){return"string"!=typeof a?a:this._map[a]},fetch:function(a){var b,c,d=this._queue.length;for(a=a||f.QUEUED,b=0;d>b;b++)if(c=this._queue[b],a===c.getStatus())return c;return null},sort:function(a){"function"==typeof a&&this._queue.sort(a)},getFiles:function(){for(var a,b=[].slice.call(arguments,0),c=[],d=0,f=this._queue.length;f>d;d++)a=this._queue[d],(!b.length||~e.inArray(a.getStatus(),b))&&c.push(a);return c},removeFile:function(a){var b=this._map[a.id];b&&(delete this._map[a.id],a.destroy(),this.stats.numofDeleted++)},_fileAdded:function(a){var b=this,c=this._map[a.id];c||(this._map[a.id]=a,a.on("statuschange",function(a,c){b._onFileStatusChange(a,c)}))},_onFileStatusChange:function(a,b){var c=this.stats;switch(b){case f.PROGRESS:c.numOfProgress--;break;case f.QUEUED:c.numOfQueue--;break;case f.ERROR:c.numOfUploadFailed--;break;case f.INVALID:c.numOfInvalid--;break;case f.INTERRUPT:c.numofInterrupt--}switch(a){case f.QUEUED:c.numOfQueue++;break;case f.PROGRESS:c.numOfProgress++;break;case f.ERROR:c.numOfUploadFailed++;break;case f.COMPLETE:c.numOfSuccess++;break;case f.CANCELLED:c.numOfCancel++;break;case f.INVALID:c.numOfInvalid++;break;case f.INTERRUPT:c.numofInterrupt++}}}),b.installTo(d.prototype),d}),b("widgets/queue",["base","uploader","queue","file","lib/file","runtime/client","widgets/widget"],function(a,b,c,d,e,f){var g=a.$,h=/\.\w+$/,i=d.Status;return b.register({name:"queue",init:function(b){var d,e,h,i,j,k,l,m=this;if(g.isPlainObject(b.accept)&&(b.accept=[b.accept]),b.accept){for(j=[],h=0,e=b.accept.length;e>h;h++)i=b.accept[h].extensions,i&&j.push(i);j.length&&(k="\\."+j.join(",").replace(/,/g,"$|\\.").replace(/\*/g,".*")+"$"),m.accept=new RegExp(k,"i")}return m.queue=new c,m.stats=m.queue.stats,"html5"===this.request("predict-runtime-type")?(d=a.Deferred(),this.placeholder=l=new f("Placeholder"),l.connectRuntime({runtimeOrder:"html5"},function(){m._ruid=l.getRuid(),d.resolve()}),d.promise()):void 0},_wrapFile:function(a){if(!(a instanceof d)){if(!(a instanceof e)){if(!this._ruid)throw new Error("Can't add external files.");a=new e(this._ruid,a)}a=new d(a)}return a},acceptFile:function(a){var b=!a||!a.size||this.accept&&h.exec(a.name)&&!this.accept.test(a.name);return!b},_addFile:function(a){var b=this;return a=b._wrapFile(a),b.owner.trigger("beforeFileQueued",a)?b.acceptFile(a)?(b.queue.append(a),b.owner.trigger("fileQueued",a),a):void b.owner.trigger("error","Q_TYPE_DENIED",a):void 0},getFile:function(a){return this.queue.getFile(a)},addFile:function(a){var b=this;a.length||(a=[a]),a=g.map(a,function(a){return b._addFile(a)}),b.owner.trigger("filesQueued",a),b.options.auto&&setTimeout(function(){b.request("start-upload")},20)},getStats:function(){return this.stats},removeFile:function(a,b){var c=this;a=a.id?a:c.queue.getFile(a),this.request("cancel-file",a),b&&this.queue.removeFile(a)},getFiles:function(){return this.queue.getFiles.apply(this.queue,arguments)},fetchFile:function(){return this.queue.fetch.apply(this.queue,arguments)},retry:function(a,b){var c,d,e,f=this;if(a)return a=a.id?a:f.queue.getFile(a),a.setStatus(i.QUEUED),void(b||f.request("start-upload"));for(c=f.queue.getFiles(i.ERROR),d=0,e=c.length;e>d;d++)a=c[d],a.setStatus(i.QUEUED);f.request("start-upload")},sortFiles:function(){return this.queue.sort.apply(this.queue,arguments)},reset:function(){this.owner.trigger("reset"),this.queue=new c,this.stats=this.queue.stats},destroy:function(){this.reset(),this.placeholder&&this.placeholder.destroy()}})}),b("widgets/runtime",["uploader","runtime/runtime","widgets/widget"],function(a,b){return a.support=function(){return b.hasRuntime.apply(b,arguments)},a.register({name:"runtime",init:function(){if(!this.predictRuntimeType())throw Error("Runtime Error")},predictRuntimeType:function(){var a,c,d=this.options.runtimeOrder||b.orders,e=this.type;if(!e)for(d=d.split(/\s*,\s*/g),a=0,c=d.length;c>a;a++)if(b.hasRuntime(d[a])){this.type=e=d[a];break}return e}})}),b("lib/transport",["base","runtime/client","mediator"],function(a,b,c){function d(a){var c=this;a=c.options=e.extend(!0,{},d.options,a||{}),b.call(this,"Transport"),this._blob=null,this._formData=a.formData||{},this._headers=a.headers||{},this.on("progress",this._timeout),this.on("load error",function(){c.trigger("progress",1),clearTimeout(c._timer)})}var e=a.$;return d.options={server:"",method:"POST",withCredentials:!1,fileVal:"file",timeout:12e4,formData:{},headers:{},sendAsBinary:!1},e.extend(d.prototype,{appendBlob:function(a,b,c){var d=this,e=d.options;d.getRuid()&&d.disconnectRuntime(),d.connectRuntime(b.ruid,function(){d.exec("init")}),d._blob=b,e.fileVal=a||e.fileVal,e.filename=c||e.filename},append:function(a,b){"object"==typeof a?e.extend(this._formData,a):this._formData[a]=b},setRequestHeader:function(a,b){"object"==typeof a?e.extend(this._headers,a):this._headers[a]=b},send:function(a){this.exec("send",a),this._timeout()},abort:function(){return clearTimeout(this._timer),this.exec("abort")},destroy:function(){this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()},getResponse:function(){return this.exec("getResponse")},getResponseAsJson:function(){return this.exec("getResponseAsJson")},getStatus:function(){return this.exec("getStatus")},_timeout:function(){var a=this,b=a.options.timeout;b&&(clearTimeout(a._timer),a._timer=setTimeout(function(){a.abort(),a.trigger("error","timeout")},b))}}),c.installTo(d.prototype),d}),b("widgets/upload",["base","uploader","file","lib/transport","widgets/widget"],function(a,b,c,d){function e(a,b){var c,d,e=[],f=a.source,g=f.size,h=b?Math.ceil(g/b):1,i=0,j=0;for(d={file:a,has:function(){return!!e.length},shift:function(){return e.shift()},unshift:function(a){e.unshift(a)}};h>j;)c=Math.min(b,g-i),e.push({file:a,start:i,end:b?i+c:g,total:g,chunks:h,chunk:j++,cuted:d}),i+=c;return a.blocks=e.concat(),a.remaning=e.length,d}var f=a.$,g=a.isPromise,h=c.Status;f.extend(b.options,{prepareNextFile:!1,chunked:!1,chunkSize:5242880,chunkRetry:2,threads:3,formData:{}}),b.register({name:"upload",init:function(){var b=this.owner,c=this;this.runing=!1,this.progress=!1,b.on("startUpload",function(){c.progress=!0}).on("uploadFinished",function(){c.progress=!1}),this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this.__tick=a.bindFn(this._tick,this),b.on("uploadComplete",function(a){a.blocks&&f.each(a.blocks,function(a,b){b.transport&&(b.transport.abort(),b.transport.destroy()),delete b.transport}),delete a.blocks,delete a.remaning})},reset:function(){this.request("stop-upload",!0),this.runing=!1,this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this._trigged=!1,this._promise=null},startUpload:function(b){var c=this;if(f.each(c.request("get-files",h.INVALID),function(){c.request("remove-file",this)}),b)if(b=b.id?b:c.request("get-file",b),b.getStatus()===h.INTERRUPT)f.each(c.pool,function(a,c){c.file===b&&c.transport&&c.transport.send()}),b.setStatus(h.QUEUED);else{if(b.getStatus()===h.PROGRESS)return;b.setStatus(h.QUEUED)}else f.each(c.request("get-files",[h.INITED]),function(){this.setStatus(h.QUEUED)});c.runing||(c.runing=!0,f.each(c.pool,function(a,b){var d=b.file;d.getStatus()===h.INTERRUPT&&(d.setStatus(h.PROGRESS),c._trigged=!1,b.transport&&b.transport.send())}),b||f.each(c.request("get-files",h.INTERRUPT),function(){this.setStatus(h.PROGRESS)}),c._trigged=!1,a.nextTick(c.__tick),c.owner.trigger("startUpload"))},stopUpload:function(b,c){var d=this;if(b===!0&&(c=b,b=null),d.runing!==!1){if(b){if(b=b.id?b:d.request("get-file",b),b.getStatus()!==h.PROGRESS&&b.getStatus()!==h.QUEUED)return;return b.setStatus(h.INTERRUPT),f.each(d.pool,function(a,c){c.file===b&&(c.transport&&c.transport.abort(),d._putback(c),d._popBlock(c))}),a.nextTick(d.__tick)}d.runing=!1,this._promise&&this._promise.file&&this._promise.file.setStatus(h.INTERRUPT),c&&f.each(d.pool,function(a,b){b.transport&&b.transport.abort(),b.file.setStatus(h.INTERRUPT)}),d.owner.trigger("stopUpload")}},cancelFile:function(a){a=a.id?a:this.request("get-file",a),a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),a.setStatus(h.CANCELLED),this.owner.trigger("fileDequeued",a)},isInProgress:function(){return!!this.progress},_getStats:function(){return this.request("get-stats")},skipFile:function(a,b){a=a.id?a:this.request("get-file",a),a.setStatus(b||h.COMPLETE),a.skipped=!0,a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),this.owner.trigger("uploadSkip",a)},_tick:function(){var b,c,d=this,e=d.options;return d._promise?d._promise.always(d.__tick):void(d.pool.length1&&(f.each(k.blocks,function(a,b){d+=(b.percentage||0)*(b.end-b.start)}),c=d/k.size),i.trigger("uploadProgress",k,c||0)}),c=function(a){var c;return e=l.getResponseAsJson()||{},e._raw=l.getResponse(),c=function(b){a=b},i.trigger("uploadAccept",b,e,c)||(a=a||"server"),a},l.on("error",function(a,d){b.retried=b.retried||0,b.chunks>1&&~"http,abort".indexOf(a)&&b.retried1&&f.extend(m,{chunks:b.chunks,chunk:b.chunk}),i.trigger("uploadBeforeSend",b,m,n),l.appendBlob(j.fileVal,b.blob,k.name),l.append(m),l.setRequestHeader(n),l.send()},_finishFile:function(a,b,c){var d=this.owner;return d.request("after-send-file",arguments,function(){a.setStatus(h.COMPLETE),d.trigger("uploadSuccess",a,b,c)}).fail(function(b){a.getStatus()===h.PROGRESS&&a.setStatus(h.ERROR,b),d.trigger("uploadError",a,b)}).always(function(){d.trigger("uploadComplete",a)})}})}),b("widgets/validator",["base","uploader","file","widgets/widget"],function(a,b,c){var d,e=a.$,f={};return d={addValidator:function(a,b){f[a]=b},removeValidator:function(a){delete f[a]}},b.register({name:"validator",init:function(){var b=this;a.nextTick(function(){e.each(f,function(){this.call(b.owner)})})}}),d.addValidator("fileNumLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileNumLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){return c>=d&&e&&(e=!1,this.trigger("error","Q_EXCEED_NUM_LIMIT",d,a),setTimeout(function(){e=!0},1)),c>=d?!1:!0}),a.on("fileQueued",function(){c++}),a.on("fileDequeued",function(){c--}),a.on("reset",function(){c=0}))}),d.addValidator("fileSizeLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileSizeLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){var b=c+a.size>d;return b&&e&&(e=!1,this.trigger("error","Q_EXCEED_SIZE_LIMIT",d,a),setTimeout(function(){e=!0},1)),b?!1:!0}),a.on("fileQueued",function(a){c+=a.size}),a.on("fileDequeued",function(a){c-=a.size}),a.on("reset",function(){c=0}))}),d.addValidator("fileSingleSizeLimit",function(){var a=this,b=a.options,d=b.fileSingleSizeLimit;d&&a.on("beforeFileQueued",function(a){return a.size>d?(a.setStatus(c.Status.INVALID,"exceed_size"),this.trigger("error","F_EXCEED_SIZE",d,a),!1):void 0})}),d.addValidator("duplicate",function(){function a(a){for(var b,c=0,d=0,e=a.length;e>d;d++)b=a.charCodeAt(d),c=b+(c<<6)+(c<<16)-c;return c}var b=this,c=b.options,d={};c.duplicate||(b.on("beforeFileQueued",function(b){var c=b.__hash||(b.__hash=a(b.name+b.size+b.lastModifiedDate));return d[c]?(this.trigger("error","F_DUPLICATE",b),!1):void 0}),b.on("fileQueued",function(a){var b=a.__hash;b&&(d[b]=!0)}),b.on("fileDequeued",function(a){var b=a.__hash;b&&delete d[b]}),b.on("reset",function(){d={}}))}),d}),b("runtime/compbase",[],function(){function a(a,b){this.owner=a,this.options=a.options,this.getRuntime=function(){return b},this.getRuid=function(){return b.uid},this.trigger=function(){return a.trigger.apply(a,arguments)}}return a}),b("runtime/html5/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a={},d=this,e=this.destroy;c.apply(d,arguments),d.type=f,d.exec=function(c,e){var f,h=this,i=h.uid,j=b.slice(arguments,2);return g[c]&&(f=a[i]=a[i]||new g[c](h,d),f[e])?f[e].apply(f,j):void 0},d.destroy=function(){return e&&e.apply(this,arguments)}}var f="html5",g={};return b.inherits(c,{constructor:e,init:function(){var a=this;setTimeout(function(){a.trigger("ready")},1)}}),e.register=function(a,c){var e=g[a]=b.inherits(d,c);return e},a.Blob&&a.FileReader&&a.DataView&&c.addRuntime(f,e),e}),b("runtime/html5/blob",["runtime/html5/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.owner.source,e=d.slice||d.webkitSlice||d.mozSlice; +return d=e.call(d,a,c),new b(this.getRuid(),d)}})}),b("runtime/html5/dnd",["base","runtime/html5/runtime","lib/file"],function(a,b,c){var d=a.$,e="webuploader-dnd-";return b.register("DragAndDrop",{init:function(){var b=this.elem=this.options.container;this.dragEnterHandler=a.bindFn(this._dragEnterHandler,this),this.dragOverHandler=a.bindFn(this._dragOverHandler,this),this.dragLeaveHandler=a.bindFn(this._dragLeaveHandler,this),this.dropHandler=a.bindFn(this._dropHandler,this),this.dndOver=!1,b.on("dragenter",this.dragEnterHandler),b.on("dragover",this.dragOverHandler),b.on("dragleave",this.dragLeaveHandler),b.on("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).on("dragover",this.dragOverHandler),d(document).on("drop",this.dropHandler))},_dragEnterHandler:function(a){var b,c=this,d=c._denied||!1;return a=a.originalEvent||a,c.dndOver||(c.dndOver=!0,b=a.dataTransfer.items,b&&b.length&&(c._denied=d=!c.trigger("accept",b)),c.elem.addClass(e+"over"),c.elem[d?"addClass":"removeClass"](e+"denied")),a.dataTransfer.dropEffect=d?"none":"copy",!1},_dragOverHandler:function(a){var b=this.elem.parent().get(0);return b&&!d.contains(b,a.currentTarget)?!1:(clearTimeout(this._leaveTimer),this._dragEnterHandler.call(this,a),!1)},_dragLeaveHandler:function(){var a,b=this;return a=function(){b.dndOver=!1,b.elem.removeClass(e+"over "+e+"denied")},clearTimeout(b._leaveTimer),b._leaveTimer=setTimeout(a,100),!1},_dropHandler:function(a){var b,f,g=this,h=g.getRuid(),i=g.elem.parent().get(0);if(i&&!d.contains(i,a.currentTarget))return!1;a=a.originalEvent||a,b=a.dataTransfer;try{f=b.getData("text/html")}catch(j){}return f?void 0:(g._getTansferFiles(b,function(a){g.trigger("drop",d.map(a,function(a){return new c(h,a)}))}),g.dndOver=!1,g.elem.removeClass(e+"over"),!1)},_getTansferFiles:function(b,c){var d,e,f,g,h,i,j,k=[],l=[];for(d=b.items,e=b.files,j=!(!d||!d[0].webkitGetAsEntry),h=0,i=e.length;i>h;h++)f=e[h],g=d&&d[h],j&&g.webkitGetAsEntry().isDirectory?l.push(this._traverseDirectoryTree(g.webkitGetAsEntry(),k)):k.push(f);a.when.apply(a,l).done(function(){k.length&&c(k)})},_traverseDirectoryTree:function(b,c){var d=a.Deferred(),e=this;return b.isFile?b.file(function(a){c.push(a),d.resolve()}):b.isDirectory&&b.createReader().readEntries(function(b){var f,g=b.length,h=[],i=[];for(f=0;g>f;f++)h.push(e._traverseDirectoryTree(b[f],i));a.when.apply(a,h).then(function(){c.push.apply(c,i),d.resolve()},d.reject)}),d.promise()},destroy:function(){var a=this.elem;a&&(a.off("dragenter",this.dragEnterHandler),a.off("dragover",this.dragOverHandler),a.off("dragleave",this.dragLeaveHandler),a.off("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).off("dragover",this.dragOverHandler),d(document).off("drop",this.dropHandler)))}})}),b("runtime/html5/filepaste",["base","runtime/html5/runtime","lib/file"],function(a,b,c){return b.register("FilePaste",{init:function(){var b,c,d,e,f=this.options,g=this.elem=f.container,h=".*";if(f.accept){for(b=[],c=0,d=f.accept.length;d>c;c++)e=f.accept[c].mimeTypes,e&&b.push(e);b.length&&(h=b.join(","),h=h.replace(/,/g,"|").replace(/\*/g,".*"))}this.accept=h=new RegExp(h,"i"),this.hander=a.bindFn(this._pasteHander,this),g.on("paste",this.hander)},_pasteHander:function(a){var b,d,e,f,g,h=[],i=this.getRuid();for(a=a.originalEvent||a,b=a.clipboardData.items,f=0,g=b.length;g>f;f++)d=b[f],"file"===d.kind&&(e=d.getAsFile())&&h.push(new c(i,e));h.length&&(a.preventDefault(),a.stopPropagation(),this.trigger("paste",h))},destroy:function(){this.elem.off("paste",this.hander)}})}),b("runtime/html5/filepicker",["base","runtime/html5/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(){var a,b,d,e,f=this.getRuntime().getContainer(),g=this,h=g.owner,i=g.options,j=this.label=c(document.createElement("label")),k=this.input=c(document.createElement("input"));if(k.attr("type","file"),k.attr("name",i.name),k.addClass("webuploader-element-invisible"),j.on("click",function(){k.trigger("click")}),j.css({opacity:0,width:"100%",height:"100%",display:"block",cursor:"pointer",background:"#ffffff"}),i.multiple&&k.attr("multiple","multiple"),i.accept&&i.accept.length>0){for(a=[],b=0,d=i.accept.length;d>b;b++)a.push(i.accept[b].mimeTypes);k.attr("accept",a.join(","))}f.append(k),f.append(j),e=function(a){h.trigger(a.type)},k.on("change",function(a){var b,d=arguments.callee;g.files=a.target.files,b=this.cloneNode(!0),b.value=null,this.parentNode.replaceChild(b,this),k.off(),k=c(b).on("change",d).on("mouseenter mouseleave",e),h.trigger("change")}),j.on("mouseenter mouseleave",e)},getFiles:function(){return this.files},destroy:function(){this.input.off(),this.label.off()}})}),b("runtime/html5/transport",["base","runtime/html5/runtime"],function(a,b){var c=a.noop,d=a.$;return b.register("Transport",{init:function(){this._status=0,this._response=null},send:function(){var b,c,e,f=this.owner,g=this.options,h=this._initAjax(),i=f._blob,j=g.server;g.sendAsBinary?(j+=(/\?/.test(j)?"&":"?")+d.param(f._formData),c=i.getSource()):(b=new FormData,d.each(f._formData,function(a,c){b.append(a,c)}),b.append(g.fileVal,i.getSource(),g.filename||f._formData.name||"")),g.withCredentials&&"withCredentials"in h?(h.open(g.method,j,!0),h.withCredentials=!0):h.open(g.method,j),this._setRequestHeader(h,g.headers),c?(h.overrideMimeType&&h.overrideMimeType("application/octet-stream"),a.os.android?(e=new FileReader,e.onload=function(){h.send(this.result),e=e.onload=null},e.readAsArrayBuffer(c)):h.send(c)):h.send(b)},getResponse:function(){return this._response},getResponseAsJson:function(){return this._parseJson(this._response)},getStatus:function(){return this._status},abort:function(){var a=this._xhr;a&&(a.upload.onprogress=c,a.onreadystatechange=c,a.abort(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var a=this,b=new XMLHttpRequest,d=this.options;return!d.withCredentials||"withCredentials"in b||"undefined"==typeof XDomainRequest||(b=new XDomainRequest),b.upload.onprogress=function(b){var c=0;return b.lengthComputable&&(c=b.loaded/b.total),a.trigger("progress",c)},b.onreadystatechange=function(){return 4===b.readyState?(b.upload.onprogress=c,b.onreadystatechange=c,a._xhr=null,a._status=b.status,b.status>=200&&b.status<300?(a._response=b.responseText,a.trigger("load")):b.status>=500&&b.status<600?(a._response=b.responseText,a.trigger("error","server")):a.trigger("error",a._status?"http":"abort")):void 0},a._xhr=b,b},_setRequestHeader:function(a,b){d.each(b,function(b,c){a.setRequestHeader(b,c)})},_parseJson:function(a){var b;try{b=JSON.parse(a)}catch(c){b={}}return b}})}),b("runtime/flash/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a;try{a=navigator.plugins["Shockwave Flash"],a=a.description}catch(b){try{a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash").GetVariable("$version")}catch(c){a="0.0"}}return a=a.match(/\d+/g),parseFloat(a[0]+"."+a[1],10)}function f(){function d(a,b){var c,d,e=a.type||a;c=e.split("::"),d=c[0],e=c[1],"Ready"===e&&d===j.uid?j.trigger("ready"):f[d]&&f[d].trigger(e.toLowerCase(),a,b)}var e={},f={},g=this.destroy,j=this,k=b.guid("webuploader_");c.apply(j,arguments),j.type=h,j.exec=function(a,c){var d,g=this,h=g.uid,k=b.slice(arguments,2);return f[h]=g,i[a]&&(e[h]||(e[h]=new i[a](g,j)),d=e[h],d[c])?d[c].apply(d,k):j.flashExec.apply(g,arguments)},a[k]=function(){var a=arguments;setTimeout(function(){d.apply(null,a)},1)},this.jsreciver=k,this.destroy=function(){return g&&g.apply(this,arguments)},this.flashExec=function(a,c){var d=j.getFlash(),e=b.slice(arguments,2);return d.exec(this.uid,a,c,e)}}var g=b.$,h="flash",i={};return b.inherits(c,{constructor:f,init:function(){var a,c=this.getContainer(),d=this.options;c.css({position:"absolute",top:"-8px",left:"-8px",width:"9px",height:"9px",overflow:"hidden"}),a='',c.html(a)},getFlash:function(){return this._flash?this._flash:(this._flash=g("#"+this.uid).get(0),this._flash)}}),f.register=function(a,c){return c=i[a]=b.inherits(d,g.extend({flashExec:function(){var a=this.owner,b=this.getRuntime();return b.flashExec.apply(a,arguments)}},c))},e()>=11.4&&c.addRuntime(h,f),f}),b("runtime/flash/filepicker",["base","runtime/flash/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(a){var b,d,e=c.extend({},a);for(b=e.accept&&e.accept.length,d=0;b>d;d++)e.accept[d].title||(e.accept[d].title="Files");delete e.button,delete e.id,delete e.container,this.flashExec("FilePicker","init",e)},destroy:function(){this.flashExec("FilePicker","destroy")}})}),b("runtime/flash/transport",["base","runtime/flash/runtime","runtime/client"],function(b,c,d){var e=b.$;return c.register("Transport",{init:function(){this._status=0,this._response=null,this._responseJson=null},send:function(){var a,b=this.owner,c=this.options,d=this._initAjax(),f=b._blob,g=c.server;d.connectRuntime(f.ruid),c.sendAsBinary?(g+=(/\?/.test(g)?"&":"?")+e.param(b._formData),a=f.uid):(e.each(b._formData,function(a,b){d.exec("append",a,b)}),d.exec("appendBlob",c.fileVal,f.uid,c.filename||b._formData.name||"")),this._setRequestHeader(d,c.headers),d.exec("send",{method:c.method,url:g,forceURLStream:c.forceURLStream,mimeType:"application/octet-stream"},a)},getStatus:function(){return this._status},getResponse:function(){return this._response||""},getResponseAsJson:function(){return this._responseJson},abort:function(){var a=this._xhr;a&&(a.exec("abort"),a.destroy(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var b=this,c=new d("XMLHttpRequest");return c.on("uploadprogress progress",function(a){var c=a.loaded/a.total;return c=Math.min(1,Math.max(0,c)),b.trigger("progress",c)}),c.on("load",function(){var d,e=c.exec("getStatus"),f=!1,g="";return c.off(),b._xhr=null,e>=200&&300>e?f=!0:e>=500&&600>e?(f=!0,g="server"):g="http",f&&(b._response=c.exec("getResponse"),b._response=decodeURIComponent(b._response),d=a.JSON&&a.JSON.parse||function(a){try{return new Function("return "+a).call()}catch(b){return{}}},b._responseJson=b._response?d(b._response):{}),c.destroy(),c=null,g?b.trigger("error",g):b.trigger("load")}),c.on("error",function(){c.off(),b._xhr=null,b.trigger("error","http")}),b._xhr=c,c},_setRequestHeader:function(a,b){e.each(b,function(b,c){a.exec("setRequestHeader",b,c)})}})}),b("preset/withoutimage",["base","widgets/filednd","widgets/filepaste","widgets/filepicker","widgets/queue","widgets/runtime","widgets/upload","widgets/validator","runtime/html5/blob","runtime/html5/dnd","runtime/html5/filepaste","runtime/html5/filepicker","runtime/html5/transport","runtime/flash/filepicker","runtime/flash/transport"],function(a){return a}),b("webuploader",["preset/withoutimage"],function(a){return a}),c("webuploader")}); \ No newline at end of file diff --git a/public/static/plugins/README.md b/public/static/plugins/README.md new file mode 100644 index 0000000..acce0ee --- /dev/null +++ b/public/static/plugins/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 插件静态目录 diff --git a/public/uploads/LICENSE.txt b/public/uploads/LICENSE.txt new file mode 100644 index 0000000..2255550 --- /dev/null +++ b/public/uploads/LICENSE.txt @@ -0,0 +1,27 @@ +版权所有 (c) 2016~2017,河源市卓锐科技有限公司保留所有权利。 + +DolphinPHP 用户协议 + +版权所有 (c) 2016~2017,河源市卓锐科技有限公司保留所有权利。 + +感谢您选择DolphinPHP,希望我们的努力能为您提供一个极简极致极速的PHP快速开发解决方案。 + +用户须知:本协议是您与河源市卓锐科技有限公司(以下简称卓锐公司)之间关于您使用DolphinPHP产品及服务的法律协议。无论您是个人或组织、盈利与否、用途如何(包括以学习和研究为目的),均需仔细阅读本协议,包括免除或者限制卓锐公司责任的免责条款及对您的权利限制。请您审阅并接受或不接受本服务条款。如您不同意本服务条款及/或卓锐公司随时对其的修改,您应不使用或主动取消DolphinPHP产品。否则,您的任何对DolphinPHP的相关服务的注册、登陆、下载、查看等使用行为将被视为您对本服务条款全部的完全接受,包括接受卓锐公司对服务条款随时所做的任何修改。 + +本服务条款一旦发生变更, 卓锐公司将在产品官网上公布修改内容。修改后的服务条款一旦在网站公布即有效代替原来的服务条款。您可随时登陆官网查阅最新版服务条款。如果您选择接受本条款,即表示您同意接受协议各项条件的约束。如果您不同意本服务条款,则不能获得使用本服务的权利。您若有违反本条款规定,卓锐公司有权随时中止或终止您对DolphinPHP产品的使用资格并保留追究相关法律责任的权利。 + +在理解、同意、并遵守本协议的全部条款后,方可开始使用DolphinPHP产品。您也可能与卓锐公司直接签订另一书面协议,以补充或者取代本协议的全部或者任何部分。 + +卓锐公司拥有DolphinPHP的全部知识产权,包括商标和著作权。本软件只供许可协议,并非出售。卓锐公司只允许您在遵守本协议各项条款的情况下复制、下载、安装、使用或者以其他方式受益于本软件的功能或者知识产权。 + +个人非商业用途可免费使用(但不包括其衍生产品、插件或者服务),也可以根据个人实际情况选择是否购买授权。 + +非个人用户(泛指非个人的团体,如企业、政府单位、教育机构、协会团体、厂矿、工作室等)必须购买软件授权后方可使用。 + +您可以在协议规定的约束和限制范围内修改DolphinPHP以适应您的网站要求,但免费版必须保留软件版本信息的正常显示及相关连接正常。 + +禁止去除DolphinPHP源码里的版权信息,商业授权版本可去除后台界面及前台界面的相关版权信息。 + +禁止在DolphinPHP整体或任何部分基础上发展任何派生版本、修改版本或第三方版本用于重新分发。 + +未经官方许可,不得对本软件或与之关联的商业授权进行出租、出售、抵押或发放子许可证。 \ No newline at end of file diff --git a/public/uploads/README.md b/public/uploads/README.md new file mode 100644 index 0000000..4134c28 --- /dev/null +++ b/public/uploads/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 系统上传目录 diff --git a/public/uploads/files/20241016/064d44be4ab28389a531a1029713fd82.pem b/public/uploads/files/20241016/064d44be4ab28389a531a1029713fd82.pem new file mode 100644 index 0000000..9bbe745 --- /dev/null +++ b/public/uploads/files/20241016/064d44be4ab28389a531a1029713fd82.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDB/15zTJLWbGEh +ai3ebgaDtmyvjkmNBff8IHXtxgdp5U/ALCMDyYqO33e0sy3tlK2tlqRj8LwQoKo5 +mdFlUnlLfe/8DVHUCGAgWx7b3v7xHdXqA7d9JdJ38Iu0Gb+1a3uKz6dnjR78P1JY +teIWQW6I/totQl57ACRsUnLLH9Q7gHFsTxCQWScBfK+EzM2no7W4jOeI7igWjKJ2 +QbAl62oyQBnLihTyV/d4MYap+BAN99vbtp4y07SdanEvfgHyuICJMDyHeu9tlTuf +Jxgwmb8KTjEpgvUGgGAS4E0wA1FhIdo+trQhC+GC/OQYo2zhbHnUo9jqj5hcQRgH +m3rVSt3BAgMBAAECggEAQfjlFtKwkMjUkzd3l8/U2U/dUCpUKugfuBcV46FXfhSk +dVbNJYmRXBr0q3fPo57PHVxAVVhVrFXm7aW4hvtwElMmnJGvBSs//WDfcqg4DKk3 +yNgwuxd0pqMf70ReZITDw6XwaQ+dvOyPxWk9FJJ105LPgVqzpKKcwFWdgiQ12XGy +CNJN/mnIK+gSWSWt9ybJql3kcJpPoMGYTB/L8Q7c3kdALslLso16m+B1jGRR8LNN ++gRFsjN5VOho5PD8KluzuxvVEkZa5EQ4IpczRVnd6N0GUKuprjK3/jp6oTVqQ83W +StWYxBXymS1w2fpMyArIausPcKr5N2g8jxSQ0hIYvQKBgQDinTGv2wTQWN0BK3q9 +24me0mEh7ZZylrtTrUtBytCxcpGPhTTQE0CDn9fPm6UGxBnqze+/60igt5qUACJr +FJteVsV81JWG3fV2u8YQYAYFWEzuA3bVkUG5hFUDuLvJZYOilOv2aipWjK6Vkeh7 +c5MU230XxDMnS4AKnUlbTOPZPwKBgQDbJ2tQ3ySYBUJ87nAtlXC+7v9pxyTAjG1f +23F/MmAc0VLfV+zIOLUXdHlyimRJA/f8OfxOjgLXbaSbqXAXbqliB5mrTxuchNZE +m+UDHzZQY9n3AgyCZ4lnFTrEo7J/6rVuOlzO5t/+6sXhjISIwpE4KALJPU5w4JYA +VwuERhSI/wKBgQC6q2Q/iPnpKho7GXbNKCewFXp/uTeCAtoibpCcjJXmZgqzn7XG +ZOVyx1u9n2eMMENtIOCKRiUYhsWlforbjCkDyBR75J2bcAEAImNgH8k14+vS8DWW +bUYnrHNfUnAV0TJi3auV2xQvIUm62WsZLjYNK1RbHHpKty2tEK1ZsPfLfwKBgQDI +DnoGv0y2gcaqKTe6RPtBMQdz1lkUdJy7rqUzChd29xcBz0/Vjv6xRKBBEVhu1vKg +C4bpZvXtFqf3eXtbyKdTV8DOA08fWJwI0Y8DhJCeXihxNiuZN07VDSoyq4SdBPNO +hnSGY3gDSuoM9ate9M6ARvYUIqTiogIMmWln/FlunQKBgQCIBrZaZu1DKrsFMBSu +i30UC05+Wkue/rQQSFyCX8Crvc+WNo1mtLLnlQTOojI8Lg3zjhorIqFfb8RNsrId +m/KV8ViPi/OQZN/vuIS7weW/atlwejF+4YHOW8yhTqHPYFOe7xMACo4sdECO+B5z +BiakZNZXN0koncnygcKvQ62t0Q== +-----END PRIVATE KEY----- diff --git a/public/uploads/files/20241016/a3d24189e4797089276384686261e409.pem b/public/uploads/files/20241016/a3d24189e4797089276384686261e409.pem new file mode 100644 index 0000000..bc7c07e --- /dev/null +++ b/public/uploads/files/20241016/a3d24189e4797089276384686261e409.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEKDCCAxCgAwIBAgIUZc6WB212rRC4Ce3OxbJWXIbQFM8wDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT +FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg +Q0EwHhcNMjQxMDE2MDEyMjAxWhcNMjkxMDE1MDEyMjAxWjCBgTETMBEGA1UEAwwK +MTY5NTQzODI0MTEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMS0wKwYDVQQL +DCTkuIrmtbfkuK3lq4Tkv6Hmga/np5HmioDmnInpmZDlhazlj7gxCzAJBgNVBAYT +AkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAMH/XnNMktZsYSFqLd5uBoO2bK+OSY0F9/wgde3GB2nlT8AsIwPJio7f +d7SzLe2Ura2WpGPwvBCgqjmZ0WVSeUt97/wNUdQIYCBbHtve/vEd1eoDt30l0nfw +i7QZv7Vre4rPp2eNHvw/Uli14hZBboj+2i1CXnsAJGxScssf1DuAcWxPEJBZJwF8 +r4TMzaejtbiM54juKBaMonZBsCXrajJAGcuKFPJX93gxhqn4EA3329u2njLTtJ1q +cS9+AfK4gIkwPId6722VO58nGDCZvwpOMSmC9QaAYBLgTTADUWEh2j62tCEL4YL8 +5BijbOFsedSj2OqPmFxBGAebetVK3cECAwEAAaOBuTCBtjAJBgNVHRMEAjAAMAsG +A1UdDwQEAwID+DCBmwYDVR0fBIGTMIGQMIGNoIGKoIGHhoGEaHR0cDovL2V2Y2Eu +aXRydXMuY29tLmNuL3B1YmxpYy9pdHJ1c2NybD9DQT0xQkQ0MjIwRTUwREJDMDRC +MDZBRDM5NzU0OTg0NkMwMUMzRThFQkQyJnNnPUhBQ0M0NzFCNjU0MjJFMTJCMjdB +OUQzM0E4N0FEMUNERjU5MjZFMTQwMzcxMA0GCSqGSIb3DQEBCwUAA4IBAQBXYqM/ +wUYFsyleaM/jdh3PzcYMWl9odAqdUbhkI/mRiDOTcXwoLd7sxk0VGvGUXh8oXzIN +K/KpClxJzU70tEPOw1rWTdrtWDwWCGCTb+V2ZMvjT5jHmB0ISxA3TPLPtRx7BpBR +Jtg/xee6wMRs2Qh3RnvSrlXmY2wpqGr/mfdo2Ls4Ee2bMh7nyzC+A8iu55/ui5B5 +Ai+eatGLPthmejuOTC0PuErHmx/KbcN7tpslk2ICJlwJP6V0B0siSG0HfvGzNihU +yu+qEea/fhp9vxrUavlUFrd9/WIX3r74P+2LMRgfRRjNgdz0M9AVXobW9+mAZOBe +eir1OxSMez2X/Oc3 +-----END CERTIFICATE----- diff --git a/public/uploads/files/20241024/46f75bc62fd6cbdf7df870c1578204fc.pem b/public/uploads/files/20241024/46f75bc62fd6cbdf7df870c1578204fc.pem new file mode 100644 index 0000000..4afa110 --- /dev/null +++ b/public/uploads/files/20241024/46f75bc62fd6cbdf7df870c1578204fc.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIELjCCAxagAwIBAgIUF/EcugAuri9RzX3SF7ep8MTf3ngwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT +FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg +Q0EwHhcNMjQxMDI0MDMwMzIwWhcNMjkxMDIzMDMwMzIwWjCBhzETMBEGA1UEAwwK +MTY5NjA2ODU5MzEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMTMwMQYDVQQL +DCrkuIrmtbfovrDpo47kupLlqLHnvZHnu5znp5HmioDmnInpmZDlhazlj7gxCzAJ +BgNVBAYTAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAOEOX44tpz1v9zFhcmZvdEAEv/hTsvFkUJsDCWYxERw5WUXC +oEMgMf4pElF6iz4lWB3947lkieByBcN+bKItZozUKtc5AV3UCbPsL5jyT92NhHiH +bzy6J7ekh8mB6+duyinmaSzkMhUTOC/j6Fk6rZQCCf0imRGtSctf6e2vbuRH2N3n +CbDvaft0It4BFcHYy+RsjPChdJVMi9PjyZ954ERHrzFk24phScVNoABV8iovUjc3 +Z/q2FHAgUbtX2VUXZ9wk73CLA5pRNI0cd/kN23t5gweCgj7x00SmWW3PZFPpomOK +fAiE85YOxUsc77JkJrsRGkS8bzlw62pNZ3hfwjUCAwEAAaOBuTCBtjAJBgNVHRME +AjAAMAsGA1UdDwQEAwID+DCBmwYDVR0fBIGTMIGQMIGNoIGKoIGHhoGEaHR0cDov +L2V2Y2EuaXRydXMuY29tLmNuL3B1YmxpYy9pdHJ1c2NybD9DQT0xQkQ0MjIwRTUw +REJDMDRCMDZBRDM5NzU0OTg0NkMwMUMzRThFQkQyJnNnPUhBQ0M0NzFCNjU0MjJF +MTJCMjdBOUQzM0E4N0FEMUNERjU5MjZFMTQwMzcxMA0GCSqGSIb3DQEBCwUAA4IB +AQBswCxSeozXJBuiGEnfu8NjS41KbF63kX9A+p1T8EiEfSq9Uv8lBv88pUcO/ar8 +P5+nvK8Gj4HtR8UaoPUnY1YmDy+ArSr6CEioZtcokleOH2jv4ktfq5cCeCsh16P9 +Q2z4WX+6Nb4Va6cEfi4xb/FFgkrwX7bXs05WyGZZMnEHmYkN5k4dQ6pFC0n2A4d9 +ra+GHZE93gnjYZthd3qf5pvXkJ2C9wVIQOYA/wzA4bV3JsNNAMty/iR/dE2oww64 +pO/hQEnmLF4PQ2IVDTYatGLhMyL7ZqzzzlvEAibzWvai0dki0ucqVB+3C2g43YJH +BnQSK0FKco9I+EKPCT0mSYzC +-----END CERTIFICATE----- diff --git a/public/uploads/files/20241024/da3ad7981ac92c07744bb3cd0b768e6e.pem b/public/uploads/files/20241024/da3ad7981ac92c07744bb3cd0b768e6e.pem new file mode 100644 index 0000000..de99b7a --- /dev/null +++ b/public/uploads/files/20241024/da3ad7981ac92c07744bb3cd0b768e6e.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDhDl+OLac9b/cx +YXJmb3RABL/4U7LxZFCbAwlmMREcOVlFwqBDIDH+KRJReos+JVgd/eO5ZIngcgXD +fmyiLWaM1CrXOQFd1Amz7C+Y8k/djYR4h288uie3pIfJgevnbsop5mks5DIVEzgv +4+hZOq2UAgn9IpkRrUnLX+ntr27kR9jd5wmw72n7dCLeARXB2MvkbIzwoXSVTIvT +48mfeeBER68xZNuKYUnFTaAAVfIqL1I3N2f6thRwIFG7V9lVF2fcJO9wiwOaUTSN +HHf5Ddt7eYMHgoI+8dNEplltz2RT6aJjinwIhPOWDsVLHO+yZCa7ERpEvG85cOtq +TWd4X8I1AgMBAAECggEBALxH9hFQiFWR0k/xom+oVq0oTCJIg7AHqJfGlppz+kiO +ge0mwkTmLmzfl3+q8crwGyQcP+PeBKtNOR0wK1oSeXTgG2crPcVtmyB3O+rM7ZwA +xQ7A34MCii/M6fzfQKjBu0gKh3sS/gM7rQoDtWLRAF6OxiSMSVrMiqwmdeJvnmpD +T30orPxH+rCThvJNlAZFSqwNXSDj+J3jtEgMG4lW8EHwlHwII88oIHq7ObBz84jR +ugqvHf6NL5d11MOrxRILIfIFpfecMPct+zQHcpN5+H3sdb0c+lKXKvJU8oAAl3Xl +o8yYjVOAGIzhpFscD2H1OIevmL7xzGVs1+hxrRgWbRECgYEA+Nn+Urp0C/h+QFNn +xcETkaWy8QIt1hNtE6T6HLdkj3kt29gcaDmKcxj/I+gnSw7BQy8/hx1lZkCIk2jk +qgC7DgDsW5qCvx5pgHDeF16XEoxQWhqbZIyDPT81m/2ZFZuldXDpfhVaJsnUzDHP +QT9r9FGdaz24Rc6V/v+n1gdwWUcCgYEA54Vkn7S8XddD/h5A58UJWvXK9ELdW+Xd +qpevx6nSmGn6jLYHLTA/tZVfhA1cX4QkxvAE/HXxrmgx4Voyv0e8AzXSsl3ND17n +v5sQYiiXHT2nbxE8GNydjOszRX4q9GNgRqLlmc5GN4xviB7iw9fR/d7VapDbNQTp +naH6Y9fmxqMCgYAI+uznlVzstanwrlHXXBFVtzIO6nI3AHp2NATmlmOlncze2xyj +UjhB0QwcApuwZLbqssDLIm9vUjLbhvSBggc+CSB+acQAG5vZC7moBDzVaYTYy48C +AOQXiLlpiKxGkFhXNZzndOPM9ImeWMnO4/gEWBGjNhX5Ruw4yIcI/Tm7rwKBgHk1 +rhuq3N9oXxGAt0xgLOixPEMraoa9TpMMfRvUPgxLl7/pYNJ4qzIe9PXGpPDg1vxM +FuAxw4kixdKMjXr1TGqF2DxHvZ7pef0naqlpRMwrRihw5nzBq75mON6OEmEGUhZJ +HFms07grTwz91ozfxfHaovL6ybj1THWZouR42TpvAoGAEWhRR0x539ovinwV+EHJ +YaOqjXU9YcsWq5zkclR6mvJWP8iflp8J+PrxPzJSv3HN9/xDiUk2Z9OhJzRXeTqz +b4P6lRnJAmfHVSbxXZGWp+x1yYPA5U4TJ5bqW9xqS+OMd3BqDnZVOYzcjPqp1lpl +pWYWC/ZQToxQM5kdBXx5zHE= +-----END PRIVATE KEY----- diff --git a/public/uploads/files/README.md b/public/uploads/files/README.md new file mode 100644 index 0000000..c5e0979 --- /dev/null +++ b/public/uploads/files/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 文件目录 diff --git a/public/uploads/flashs/README.md b/public/uploads/flashs/README.md new file mode 100644 index 0000000..99dcfb4 --- /dev/null +++ b/public/uploads/flashs/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# flash目录 diff --git a/public/uploads/images/20240903/1782e43877f444c01432186b8b7db6cc.png b/public/uploads/images/20240903/1782e43877f444c01432186b8b7db6cc.png new file mode 100644 index 0000000..614d1a0 Binary files /dev/null and b/public/uploads/images/20240903/1782e43877f444c01432186b8b7db6cc.png differ diff --git a/public/uploads/images/20240903/353f726e244c5faabde08981aeb46328.png b/public/uploads/images/20240903/353f726e244c5faabde08981aeb46328.png new file mode 100644 index 0000000..dde504a Binary files /dev/null and b/public/uploads/images/20240903/353f726e244c5faabde08981aeb46328.png differ diff --git a/public/uploads/images/20240903/a51c81bf8e0086cad7b94a70be8a3a1e.png b/public/uploads/images/20240903/a51c81bf8e0086cad7b94a70be8a3a1e.png new file mode 100644 index 0000000..000b81b Binary files /dev/null and b/public/uploads/images/20240903/a51c81bf8e0086cad7b94a70be8a3a1e.png differ diff --git a/public/uploads/images/20240903/cf883cf494af4caeedb04149e7ab8e3b.png b/public/uploads/images/20240903/cf883cf494af4caeedb04149e7ab8e3b.png new file mode 100644 index 0000000..b144de2 Binary files /dev/null and b/public/uploads/images/20240903/cf883cf494af4caeedb04149e7ab8e3b.png differ diff --git a/public/uploads/images/20240904/05fa36d863ac0d558bce730dcae0f375.png b/public/uploads/images/20240904/05fa36d863ac0d558bce730dcae0f375.png new file mode 100644 index 0000000..905f181 Binary files /dev/null and b/public/uploads/images/20240904/05fa36d863ac0d558bce730dcae0f375.png differ diff --git a/public/uploads/images/20240907/2393b0803244ff2fd1370954f174d429.jpeg b/public/uploads/images/20240907/2393b0803244ff2fd1370954f174d429.jpeg new file mode 100644 index 0000000..5d80df8 Binary files /dev/null and b/public/uploads/images/20240907/2393b0803244ff2fd1370954f174d429.jpeg differ diff --git a/public/uploads/images/20241014/0b0ba7b22176ce5b3a514ca3b0c76743.png b/public/uploads/images/20241014/0b0ba7b22176ce5b3a514ca3b0c76743.png new file mode 100644 index 0000000..b5659a4 Binary files /dev/null and b/public/uploads/images/20241014/0b0ba7b22176ce5b3a514ca3b0c76743.png differ diff --git a/public/uploads/images/20241014/5689a36209f51f924f0ee56ee079416b.png b/public/uploads/images/20241014/5689a36209f51f924f0ee56ee079416b.png new file mode 100644 index 0000000..adf1707 Binary files /dev/null and b/public/uploads/images/20241014/5689a36209f51f924f0ee56ee079416b.png differ diff --git a/public/uploads/images/20241014/c34551a14ab5ffb4abfa5814eaf3a993.png b/public/uploads/images/20241014/c34551a14ab5ffb4abfa5814eaf3a993.png new file mode 100644 index 0000000..5602388 Binary files /dev/null and b/public/uploads/images/20241014/c34551a14ab5ffb4abfa5814eaf3a993.png differ diff --git a/public/uploads/images/20241014/d044f72f71e871618ff8af051d9cedb2.png b/public/uploads/images/20241014/d044f72f71e871618ff8af051d9cedb2.png new file mode 100644 index 0000000..f9ca924 Binary files /dev/null and b/public/uploads/images/20241014/d044f72f71e871618ff8af051d9cedb2.png differ diff --git a/public/uploads/images/20241014/dedd5eee802bc5e36d2046089e5f4115.png b/public/uploads/images/20241014/dedd5eee802bc5e36d2046089e5f4115.png new file mode 100644 index 0000000..67b7128 Binary files /dev/null and b/public/uploads/images/20241014/dedd5eee802bc5e36d2046089e5f4115.png differ diff --git a/public/uploads/images/20241014/ef0dc0aa6493ddc3f39efedc5eab7592.png b/public/uploads/images/20241014/ef0dc0aa6493ddc3f39efedc5eab7592.png new file mode 100644 index 0000000..e0b2ffa Binary files /dev/null and b/public/uploads/images/20241014/ef0dc0aa6493ddc3f39efedc5eab7592.png differ diff --git a/public/uploads/images/20241015/399b671e021f63065001cdfa54f2a37b.gif b/public/uploads/images/20241015/399b671e021f63065001cdfa54f2a37b.gif new file mode 100644 index 0000000..7d13d91 Binary files /dev/null and b/public/uploads/images/20241015/399b671e021f63065001cdfa54f2a37b.gif differ diff --git a/public/uploads/images/20241015/4b7b1635e2b50e022dc778e0295d9ee9.gif b/public/uploads/images/20241015/4b7b1635e2b50e022dc778e0295d9ee9.gif new file mode 100644 index 0000000..df3f4d6 Binary files /dev/null and b/public/uploads/images/20241015/4b7b1635e2b50e022dc778e0295d9ee9.gif differ diff --git a/public/uploads/images/20241015/81f164bf80b004a5da1a396db8272207.png b/public/uploads/images/20241015/81f164bf80b004a5da1a396db8272207.png new file mode 100644 index 0000000..238398c Binary files /dev/null and b/public/uploads/images/20241015/81f164bf80b004a5da1a396db8272207.png differ diff --git a/public/uploads/images/20241015/dbe6d33a3c6e2284d4b7d65d76183794.png b/public/uploads/images/20241015/dbe6d33a3c6e2284d4b7d65d76183794.png new file mode 100644 index 0000000..a171268 Binary files /dev/null and b/public/uploads/images/20241015/dbe6d33a3c6e2284d4b7d65d76183794.png differ diff --git a/public/uploads/images/20241023/2cb59f295b94f6b0ae7d341b545c40a2.png b/public/uploads/images/20241023/2cb59f295b94f6b0ae7d341b545c40a2.png new file mode 100644 index 0000000..7721c5e Binary files /dev/null and b/public/uploads/images/20241023/2cb59f295b94f6b0ae7d341b545c40a2.png differ diff --git a/public/uploads/images/20241023/b35ff0b0af3cec734aaf0e7210eeb63c.jpg b/public/uploads/images/20241023/b35ff0b0af3cec734aaf0e7210eeb63c.jpg new file mode 100644 index 0000000..4e54abf Binary files /dev/null and b/public/uploads/images/20241023/b35ff0b0af3cec734aaf0e7210eeb63c.jpg differ diff --git a/public/uploads/images/20241026/0143f7f59b1fcf23ca27c1d6503692a7.png b/public/uploads/images/20241026/0143f7f59b1fcf23ca27c1d6503692a7.png new file mode 100644 index 0000000..77fb15a Binary files /dev/null and b/public/uploads/images/20241026/0143f7f59b1fcf23ca27c1d6503692a7.png differ diff --git a/public/uploads/images/20241026/0498ead3621e1e5d84da704721a5f8fe.png b/public/uploads/images/20241026/0498ead3621e1e5d84da704721a5f8fe.png new file mode 100644 index 0000000..58ca4bf Binary files /dev/null and b/public/uploads/images/20241026/0498ead3621e1e5d84da704721a5f8fe.png differ diff --git a/public/uploads/images/20241026/1201a8217fd688eaf72666fc31f919de.png b/public/uploads/images/20241026/1201a8217fd688eaf72666fc31f919de.png new file mode 100644 index 0000000..ae7551b Binary files /dev/null and b/public/uploads/images/20241026/1201a8217fd688eaf72666fc31f919de.png differ diff --git a/public/uploads/images/20241026/2a873937446a75962f6192be13919b2c.png b/public/uploads/images/20241026/2a873937446a75962f6192be13919b2c.png new file mode 100644 index 0000000..5612bf6 Binary files /dev/null and b/public/uploads/images/20241026/2a873937446a75962f6192be13919b2c.png differ diff --git a/public/uploads/images/20241026/2d589688ced9ff1f3287e11db69cf97c.png b/public/uploads/images/20241026/2d589688ced9ff1f3287e11db69cf97c.png new file mode 100644 index 0000000..e1f146a Binary files /dev/null and b/public/uploads/images/20241026/2d589688ced9ff1f3287e11db69cf97c.png differ diff --git a/public/uploads/images/20241026/43abbbe7ecbf30b61684a518a765fca8.png b/public/uploads/images/20241026/43abbbe7ecbf30b61684a518a765fca8.png new file mode 100644 index 0000000..fa9c5ee Binary files /dev/null and b/public/uploads/images/20241026/43abbbe7ecbf30b61684a518a765fca8.png differ diff --git a/public/uploads/images/20241026/4c75f786d5e1a033c9dbbb5cd4d3ded0.png b/public/uploads/images/20241026/4c75f786d5e1a033c9dbbb5cd4d3ded0.png new file mode 100644 index 0000000..e2f6a07 Binary files /dev/null and b/public/uploads/images/20241026/4c75f786d5e1a033c9dbbb5cd4d3ded0.png differ diff --git a/public/uploads/images/20241026/6e4a8b8f907ea4266de6e5f8f9ec43af.png b/public/uploads/images/20241026/6e4a8b8f907ea4266de6e5f8f9ec43af.png new file mode 100644 index 0000000..5f6fa9e Binary files /dev/null and b/public/uploads/images/20241026/6e4a8b8f907ea4266de6e5f8f9ec43af.png differ diff --git a/public/uploads/images/20241026/70c8e9a4cce269503b3a6aaa4e7e2636.png b/public/uploads/images/20241026/70c8e9a4cce269503b3a6aaa4e7e2636.png new file mode 100644 index 0000000..22a8404 Binary files /dev/null and b/public/uploads/images/20241026/70c8e9a4cce269503b3a6aaa4e7e2636.png differ diff --git a/public/uploads/images/20241026/730e4a5acdc850ba9a3a4f43cbfa2dce.png b/public/uploads/images/20241026/730e4a5acdc850ba9a3a4f43cbfa2dce.png new file mode 100644 index 0000000..868e3cb Binary files /dev/null and b/public/uploads/images/20241026/730e4a5acdc850ba9a3a4f43cbfa2dce.png differ diff --git a/public/uploads/images/20241026/81b99c763be1099f05d368d81c526842.png b/public/uploads/images/20241026/81b99c763be1099f05d368d81c526842.png new file mode 100644 index 0000000..cbbc89d Binary files /dev/null and b/public/uploads/images/20241026/81b99c763be1099f05d368d81c526842.png differ diff --git a/public/uploads/images/20241026/9ab039d742f7144e9f17eeb59bc9040c.png b/public/uploads/images/20241026/9ab039d742f7144e9f17eeb59bc9040c.png new file mode 100644 index 0000000..0dcea67 Binary files /dev/null and b/public/uploads/images/20241026/9ab039d742f7144e9f17eeb59bc9040c.png differ diff --git a/public/uploads/images/20241026/ac85cdac12edd9cedda186adb0081ce5.png b/public/uploads/images/20241026/ac85cdac12edd9cedda186adb0081ce5.png new file mode 100644 index 0000000..c852f51 Binary files /dev/null and b/public/uploads/images/20241026/ac85cdac12edd9cedda186adb0081ce5.png differ diff --git a/public/uploads/images/20241026/c7b14d7c76f4fd4b8cec72b13f3b9814.png b/public/uploads/images/20241026/c7b14d7c76f4fd4b8cec72b13f3b9814.png new file mode 100644 index 0000000..35071ae Binary files /dev/null and b/public/uploads/images/20241026/c7b14d7c76f4fd4b8cec72b13f3b9814.png differ diff --git a/public/uploads/images/20241026/d3d6377eb9b4b28d8841617064cdd7ee.png b/public/uploads/images/20241026/d3d6377eb9b4b28d8841617064cdd7ee.png new file mode 100644 index 0000000..fe87631 Binary files /dev/null and b/public/uploads/images/20241026/d3d6377eb9b4b28d8841617064cdd7ee.png differ diff --git a/public/uploads/images/20241026/e8beafbdfe413101edfdf220ad95c8db.png b/public/uploads/images/20241026/e8beafbdfe413101edfdf220ad95c8db.png new file mode 100644 index 0000000..3431bb1 Binary files /dev/null and b/public/uploads/images/20241026/e8beafbdfe413101edfdf220ad95c8db.png differ diff --git a/public/uploads/images/20241026/ed15ba1277727b4b26804b10d25637e8.png b/public/uploads/images/20241026/ed15ba1277727b4b26804b10d25637e8.png new file mode 100644 index 0000000..9653b23 Binary files /dev/null and b/public/uploads/images/20241026/ed15ba1277727b4b26804b10d25637e8.png differ diff --git a/public/uploads/images/20241026/ed1bc4026cb076fafce6e84fdb61ba86.png b/public/uploads/images/20241026/ed1bc4026cb076fafce6e84fdb61ba86.png new file mode 100644 index 0000000..1f2d04b Binary files /dev/null and b/public/uploads/images/20241026/ed1bc4026cb076fafce6e84fdb61ba86.png differ diff --git a/public/uploads/images/20241026/ee6cc87204c00dd473c7550273740fc2.png b/public/uploads/images/20241026/ee6cc87204c00dd473c7550273740fc2.png new file mode 100644 index 0000000..654bc1a Binary files /dev/null and b/public/uploads/images/20241026/ee6cc87204c00dd473c7550273740fc2.png differ diff --git a/public/uploads/images/20241026/f14fcfe08157c0e66d225ae135525d5f.png b/public/uploads/images/20241026/f14fcfe08157c0e66d225ae135525d5f.png new file mode 100644 index 0000000..13d9a23 Binary files /dev/null and b/public/uploads/images/20241026/f14fcfe08157c0e66d225ae135525d5f.png differ diff --git a/public/uploads/images/20241026/fe184b93274d57be669f84fda58e220d.jpg b/public/uploads/images/20241026/fe184b93274d57be669f84fda58e220d.jpg new file mode 100644 index 0000000..857b1f7 Binary files /dev/null and b/public/uploads/images/20241026/fe184b93274d57be669f84fda58e220d.jpg differ diff --git a/public/uploads/images/20241027/58346abc1866a50a0c82819a7e873e98.jpg b/public/uploads/images/20241027/58346abc1866a50a0c82819a7e873e98.jpg new file mode 100644 index 0000000..3b6d039 Binary files /dev/null and b/public/uploads/images/20241027/58346abc1866a50a0c82819a7e873e98.jpg differ diff --git a/public/uploads/images/20241029/35872fe9aa6ac1171148d88b82021127.png b/public/uploads/images/20241029/35872fe9aa6ac1171148d88b82021127.png new file mode 100644 index 0000000..b4594b3 Binary files /dev/null and b/public/uploads/images/20241029/35872fe9aa6ac1171148d88b82021127.png differ diff --git a/public/uploads/images/20241029/b342fc3c3969f43ec817079a025d5268.jpg b/public/uploads/images/20241029/b342fc3c3969f43ec817079a025d5268.jpg new file mode 100644 index 0000000..63b11bb Binary files /dev/null and b/public/uploads/images/20241029/b342fc3c3969f43ec817079a025d5268.jpg differ diff --git a/public/uploads/images/20241029/defdef6cca36c4a20a24fed907c4954f.jpg b/public/uploads/images/20241029/defdef6cca36c4a20a24fed907c4954f.jpg new file mode 100644 index 0000000..d8571ba Binary files /dev/null and b/public/uploads/images/20241029/defdef6cca36c4a20a24fed907c4954f.jpg differ diff --git a/public/uploads/images/20241031/b2f25a73dfef3c120bee75fbef69ab84.jpg b/public/uploads/images/20241031/b2f25a73dfef3c120bee75fbef69ab84.jpg new file mode 100644 index 0000000..39f45a9 Binary files /dev/null and b/public/uploads/images/20241031/b2f25a73dfef3c120bee75fbef69ab84.jpg differ diff --git a/public/uploads/images/20241101/59410e9cea082927252f61fc769b1e1d.png b/public/uploads/images/20241101/59410e9cea082927252f61fc769b1e1d.png new file mode 100644 index 0000000..f82d2b8 Binary files /dev/null and b/public/uploads/images/20241101/59410e9cea082927252f61fc769b1e1d.png differ diff --git a/public/uploads/images/20241101/6187b4b5f569bfae4e444549b4a01629.jpg b/public/uploads/images/20241101/6187b4b5f569bfae4e444549b4a01629.jpg new file mode 100644 index 0000000..1366c89 Binary files /dev/null and b/public/uploads/images/20241101/6187b4b5f569bfae4e444549b4a01629.jpg differ diff --git a/public/uploads/images/20241102/22729c622d7c880f05429c8a7e7dc873.jpeg b/public/uploads/images/20241102/22729c622d7c880f05429c8a7e7dc873.jpeg new file mode 100644 index 0000000..a5b4fd4 Binary files /dev/null and b/public/uploads/images/20241102/22729c622d7c880f05429c8a7e7dc873.jpeg differ diff --git a/public/uploads/images/20241113/1058f603c34a72d3bb4f683428129087.png b/public/uploads/images/20241113/1058f603c34a72d3bb4f683428129087.png new file mode 100644 index 0000000..38ddf19 Binary files /dev/null and b/public/uploads/images/20241113/1058f603c34a72d3bb4f683428129087.png differ diff --git a/public/uploads/images/20241113/1354759901fe65d6ba2e44d3afc18058.png b/public/uploads/images/20241113/1354759901fe65d6ba2e44d3afc18058.png new file mode 100644 index 0000000..4bb1fea Binary files /dev/null and b/public/uploads/images/20241113/1354759901fe65d6ba2e44d3afc18058.png differ diff --git a/public/uploads/images/20241113/3763aa7fe9917da07983784205da150e.png b/public/uploads/images/20241113/3763aa7fe9917da07983784205da150e.png new file mode 100644 index 0000000..48af526 Binary files /dev/null and b/public/uploads/images/20241113/3763aa7fe9917da07983784205da150e.png differ diff --git a/public/uploads/images/20241113/48b1bde3225db183b6d6f327dc2196f2.png b/public/uploads/images/20241113/48b1bde3225db183b6d6f327dc2196f2.png new file mode 100644 index 0000000..4742fa4 Binary files /dev/null and b/public/uploads/images/20241113/48b1bde3225db183b6d6f327dc2196f2.png differ diff --git a/public/uploads/images/20241113/4d4020f54cbd49e4e43bfa3fd6cf7837.png b/public/uploads/images/20241113/4d4020f54cbd49e4e43bfa3fd6cf7837.png new file mode 100644 index 0000000..77c5f39 Binary files /dev/null and b/public/uploads/images/20241113/4d4020f54cbd49e4e43bfa3fd6cf7837.png differ diff --git a/public/uploads/images/20241113/6738900eeeecd1ea48a38ced2b7fe148.png b/public/uploads/images/20241113/6738900eeeecd1ea48a38ced2b7fe148.png new file mode 100644 index 0000000..9b81fbb Binary files /dev/null and b/public/uploads/images/20241113/6738900eeeecd1ea48a38ced2b7fe148.png differ diff --git a/public/uploads/images/20241113/819dbb8557cb9d3643b55dc4bcf4e511.png b/public/uploads/images/20241113/819dbb8557cb9d3643b55dc4bcf4e511.png new file mode 100644 index 0000000..06989d1 Binary files /dev/null and b/public/uploads/images/20241113/819dbb8557cb9d3643b55dc4bcf4e511.png differ diff --git a/public/uploads/images/20241113/8a6bf8b600ad04249c00afcc0bf64214.png b/public/uploads/images/20241113/8a6bf8b600ad04249c00afcc0bf64214.png new file mode 100644 index 0000000..72dbc29 Binary files /dev/null and b/public/uploads/images/20241113/8a6bf8b600ad04249c00afcc0bf64214.png differ diff --git a/public/uploads/images/20241113/9406435060e197d172d82ef80802c00b.png b/public/uploads/images/20241113/9406435060e197d172d82ef80802c00b.png new file mode 100644 index 0000000..b2d53ad Binary files /dev/null and b/public/uploads/images/20241113/9406435060e197d172d82ef80802c00b.png differ diff --git a/public/uploads/images/20241113/975a391c8ec2e714aacf44a76a8ec618.png b/public/uploads/images/20241113/975a391c8ec2e714aacf44a76a8ec618.png new file mode 100644 index 0000000..de17963 Binary files /dev/null and b/public/uploads/images/20241113/975a391c8ec2e714aacf44a76a8ec618.png differ diff --git a/public/uploads/images/20241113/aa67a06ee7c296f8be5f1aba9bf1a9c7.png b/public/uploads/images/20241113/aa67a06ee7c296f8be5f1aba9bf1a9c7.png new file mode 100644 index 0000000..7dad35d Binary files /dev/null and b/public/uploads/images/20241113/aa67a06ee7c296f8be5f1aba9bf1a9c7.png differ diff --git a/public/uploads/images/20241113/bf8570a0f94aab030b25e8883a70d20e.png b/public/uploads/images/20241113/bf8570a0f94aab030b25e8883a70d20e.png new file mode 100644 index 0000000..f06280e Binary files /dev/null and b/public/uploads/images/20241113/bf8570a0f94aab030b25e8883a70d20e.png differ diff --git a/public/uploads/images/20241113/d52815046d29f51dc6f572e2c1c72a4a.png b/public/uploads/images/20241113/d52815046d29f51dc6f572e2c1c72a4a.png new file mode 100644 index 0000000..854989c Binary files /dev/null and b/public/uploads/images/20241113/d52815046d29f51dc6f572e2c1c72a4a.png differ diff --git a/public/uploads/images/20241113/f15cf90b65361cded6869dc347cdfbcd.png b/public/uploads/images/20241113/f15cf90b65361cded6869dc347cdfbcd.png new file mode 100644 index 0000000..aef08c8 Binary files /dev/null and b/public/uploads/images/20241113/f15cf90b65361cded6869dc347cdfbcd.png differ diff --git a/public/uploads/images/20241114/0c738cf969e9c220264c8cef8f9b19cb.png b/public/uploads/images/20241114/0c738cf969e9c220264c8cef8f9b19cb.png new file mode 100644 index 0000000..675ebfd Binary files /dev/null and b/public/uploads/images/20241114/0c738cf969e9c220264c8cef8f9b19cb.png differ diff --git a/public/uploads/images/20241114/31c69f3df6dd85a1d6443b3b4efb487f.png b/public/uploads/images/20241114/31c69f3df6dd85a1d6443b3b4efb487f.png new file mode 100644 index 0000000..4153416 Binary files /dev/null and b/public/uploads/images/20241114/31c69f3df6dd85a1d6443b3b4efb487f.png differ diff --git a/public/uploads/images/20241114/36264a98dc9da2f8deced9bb160780a9.png b/public/uploads/images/20241114/36264a98dc9da2f8deced9bb160780a9.png new file mode 100644 index 0000000..dcc6c63 Binary files /dev/null and b/public/uploads/images/20241114/36264a98dc9da2f8deced9bb160780a9.png differ diff --git a/public/uploads/images/20241114/5a207712663b465bbc23d60d77d81a75.png b/public/uploads/images/20241114/5a207712663b465bbc23d60d77d81a75.png new file mode 100644 index 0000000..12d05a6 Binary files /dev/null and b/public/uploads/images/20241114/5a207712663b465bbc23d60d77d81a75.png differ diff --git a/public/uploads/images/20241114/611d4d71a5654f499c11ea96e25358eb.png b/public/uploads/images/20241114/611d4d71a5654f499c11ea96e25358eb.png new file mode 100644 index 0000000..0ec7615 Binary files /dev/null and b/public/uploads/images/20241114/611d4d71a5654f499c11ea96e25358eb.png differ diff --git a/public/uploads/images/20241114/755f6381659a55cdb102590a51a0955a.png b/public/uploads/images/20241114/755f6381659a55cdb102590a51a0955a.png new file mode 100644 index 0000000..f1c6956 Binary files /dev/null and b/public/uploads/images/20241114/755f6381659a55cdb102590a51a0955a.png differ diff --git a/public/uploads/images/20241114/8c901ad9c12df28f414a18d25c849cae.png b/public/uploads/images/20241114/8c901ad9c12df28f414a18d25c849cae.png new file mode 100644 index 0000000..ed04e08 Binary files /dev/null and b/public/uploads/images/20241114/8c901ad9c12df28f414a18d25c849cae.png differ diff --git a/public/uploads/images/20241114/cf79418a589c2b05eac5fe03308010f4.png b/public/uploads/images/20241114/cf79418a589c2b05eac5fe03308010f4.png new file mode 100644 index 0000000..0bca119 Binary files /dev/null and b/public/uploads/images/20241114/cf79418a589c2b05eac5fe03308010f4.png differ diff --git a/public/uploads/images/20241115/1ea3a398cc8bd251a446c14adb1f6c2e.png b/public/uploads/images/20241115/1ea3a398cc8bd251a446c14adb1f6c2e.png new file mode 100644 index 0000000..5914968 Binary files /dev/null and b/public/uploads/images/20241115/1ea3a398cc8bd251a446c14adb1f6c2e.png differ diff --git a/public/uploads/images/20241115/4cad6a1dad295e05f2f07c43cd91ac82.jpg b/public/uploads/images/20241115/4cad6a1dad295e05f2f07c43cd91ac82.jpg new file mode 100644 index 0000000..6f0f079 Binary files /dev/null and b/public/uploads/images/20241115/4cad6a1dad295e05f2f07c43cd91ac82.jpg differ diff --git a/public/uploads/images/20241115/6771c583120c7408b77effa036195207.jpg b/public/uploads/images/20241115/6771c583120c7408b77effa036195207.jpg new file mode 100644 index 0000000..203a915 Binary files /dev/null and b/public/uploads/images/20241115/6771c583120c7408b77effa036195207.jpg differ diff --git a/public/uploads/images/20241120/9d04adcd4c68d8ff6e9feee7ba33e2dc.jpg b/public/uploads/images/20241120/9d04adcd4c68d8ff6e9feee7ba33e2dc.jpg new file mode 100644 index 0000000..945644f Binary files /dev/null and b/public/uploads/images/20241120/9d04adcd4c68d8ff6e9feee7ba33e2dc.jpg differ diff --git a/public/uploads/images/20241121/1bc92aa93319821fc5d0d63365b6a5e2.png b/public/uploads/images/20241121/1bc92aa93319821fc5d0d63365b6a5e2.png new file mode 100644 index 0000000..9325773 Binary files /dev/null and b/public/uploads/images/20241121/1bc92aa93319821fc5d0d63365b6a5e2.png differ diff --git a/public/uploads/images/20241121/6689a9329f548eef58d1a5433d05aef0.png b/public/uploads/images/20241121/6689a9329f548eef58d1a5433d05aef0.png new file mode 100644 index 0000000..89bce5a Binary files /dev/null and b/public/uploads/images/20241121/6689a9329f548eef58d1a5433d05aef0.png differ diff --git a/public/uploads/images/20241121/aa5e5d6ed02a955de7fea9c4b48f9e63.png b/public/uploads/images/20241121/aa5e5d6ed02a955de7fea9c4b48f9e63.png new file mode 100644 index 0000000..4ef88ee Binary files /dev/null and b/public/uploads/images/20241121/aa5e5d6ed02a955de7fea9c4b48f9e63.png differ diff --git a/public/uploads/images/20241125/07132a7d306daf68ad47752df768e027.jpg b/public/uploads/images/20241125/07132a7d306daf68ad47752df768e027.jpg new file mode 100644 index 0000000..bc73858 Binary files /dev/null and b/public/uploads/images/20241125/07132a7d306daf68ad47752df768e027.jpg differ diff --git a/public/uploads/images/20241125/8b6964054be9227035e8501219e3683a.jpg b/public/uploads/images/20241125/8b6964054be9227035e8501219e3683a.jpg new file mode 100644 index 0000000..d69249e Binary files /dev/null and b/public/uploads/images/20241125/8b6964054be9227035e8501219e3683a.jpg differ diff --git a/public/uploads/images/20241125/8c0468c25d66a8a888c099d1f4c859ad.png b/public/uploads/images/20241125/8c0468c25d66a8a888c099d1f4c859ad.png new file mode 100644 index 0000000..01cf5bb Binary files /dev/null and b/public/uploads/images/20241125/8c0468c25d66a8a888c099d1f4c859ad.png differ diff --git a/public/uploads/images/20241125/9b5ee63b43afaad181500f58f3918c26.jpg b/public/uploads/images/20241125/9b5ee63b43afaad181500f58f3918c26.jpg new file mode 100644 index 0000000..ee203cd Binary files /dev/null and b/public/uploads/images/20241125/9b5ee63b43afaad181500f58f3918c26.jpg differ diff --git a/public/uploads/images/20241125/b1f6fba74ff10c257f3eac41c696ee68.png b/public/uploads/images/20241125/b1f6fba74ff10c257f3eac41c696ee68.png new file mode 100644 index 0000000..bb489ba Binary files /dev/null and b/public/uploads/images/20241125/b1f6fba74ff10c257f3eac41c696ee68.png differ diff --git a/public/uploads/images/README.md b/public/uploads/images/README.md new file mode 100644 index 0000000..448e367 --- /dev/null +++ b/public/uploads/images/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 图片目录 diff --git a/public/uploads/qrcode/1729749677_wx.jpg b/public/uploads/qrcode/1729749677_wx.jpg new file mode 100644 index 0000000..8d67ba3 Binary files /dev/null and b/public/uploads/qrcode/1729749677_wx.jpg differ diff --git a/public/uploads/qrcode/1730098521_wx.jpg b/public/uploads/qrcode/1730098521_wx.jpg new file mode 100644 index 0000000..79308bc Binary files /dev/null and b/public/uploads/qrcode/1730098521_wx.jpg differ diff --git a/public/uploads/qrcode/1730109264_wx.jpg b/public/uploads/qrcode/1730109264_wx.jpg new file mode 100644 index 0000000..b18f5bd Binary files /dev/null and b/public/uploads/qrcode/1730109264_wx.jpg differ diff --git a/public/uploads/qrcode/1730109366_wx.jpg b/public/uploads/qrcode/1730109366_wx.jpg new file mode 100644 index 0000000..16d0c01 Binary files /dev/null and b/public/uploads/qrcode/1730109366_wx.jpg differ diff --git a/public/uploads/qrcode/1730109374_wx.jpg b/public/uploads/qrcode/1730109374_wx.jpg new file mode 100644 index 0000000..b04466b Binary files /dev/null and b/public/uploads/qrcode/1730109374_wx.jpg differ diff --git a/public/uploads/qrcode/1730109695_wx.jpg b/public/uploads/qrcode/1730109695_wx.jpg new file mode 100644 index 0000000..2446a14 Binary files /dev/null and b/public/uploads/qrcode/1730109695_wx.jpg differ diff --git a/public/uploads/qrcode/1730166917_wx.jpg b/public/uploads/qrcode/1730166917_wx.jpg new file mode 100644 index 0000000..700dde0 Binary files /dev/null and b/public/uploads/qrcode/1730166917_wx.jpg differ diff --git a/public/uploads/qrcode/1730167006_wx.jpg b/public/uploads/qrcode/1730167006_wx.jpg new file mode 100644 index 0000000..ef58095 Binary files /dev/null and b/public/uploads/qrcode/1730167006_wx.jpg differ diff --git a/public/uploads/qrcode/1730174419_wx.jpg b/public/uploads/qrcode/1730174419_wx.jpg new file mode 100644 index 0000000..21c35ff Binary files /dev/null and b/public/uploads/qrcode/1730174419_wx.jpg differ diff --git a/public/uploads/qrcode/1730174664_wx.jpg b/public/uploads/qrcode/1730174664_wx.jpg new file mode 100644 index 0000000..2a7732f Binary files /dev/null and b/public/uploads/qrcode/1730174664_wx.jpg differ diff --git a/public/uploads/qrcode/1730176280_wx.jpg b/public/uploads/qrcode/1730176280_wx.jpg new file mode 100644 index 0000000..6b2eb97 Binary files /dev/null and b/public/uploads/qrcode/1730176280_wx.jpg differ diff --git a/public/uploads/qrcode/1730176398_wx.jpg b/public/uploads/qrcode/1730176398_wx.jpg new file mode 100644 index 0000000..cfcdf10 Binary files /dev/null and b/public/uploads/qrcode/1730176398_wx.jpg differ diff --git a/public/uploads/qrcode/1730183207_wx.jpg b/public/uploads/qrcode/1730183207_wx.jpg new file mode 100644 index 0000000..700e6e8 Binary files /dev/null and b/public/uploads/qrcode/1730183207_wx.jpg differ diff --git a/public/uploads/qrcode/1730183462_wx.jpg b/public/uploads/qrcode/1730183462_wx.jpg new file mode 100644 index 0000000..fa299b9 Binary files /dev/null and b/public/uploads/qrcode/1730183462_wx.jpg differ diff --git a/public/uploads/qrcode/1730185297_wx.jpg b/public/uploads/qrcode/1730185297_wx.jpg new file mode 100644 index 0000000..7c28308 Binary files /dev/null and b/public/uploads/qrcode/1730185297_wx.jpg differ diff --git a/public/uploads/qrcode/1730551833_wx.jpg b/public/uploads/qrcode/1730551833_wx.jpg new file mode 100644 index 0000000..7d6c447 Binary files /dev/null and b/public/uploads/qrcode/1730551833_wx.jpg differ diff --git a/public/uploads/qrcode/1730703150_wx.jpg b/public/uploads/qrcode/1730703150_wx.jpg new file mode 100644 index 0000000..1e095ed Binary files /dev/null and b/public/uploads/qrcode/1730703150_wx.jpg differ diff --git a/public/uploads/qrcode/1732414011_wx.jpg b/public/uploads/qrcode/1732414011_wx.jpg new file mode 100644 index 0000000..fae8ea4 Binary files /dev/null and b/public/uploads/qrcode/1732414011_wx.jpg differ diff --git a/public/uploads/temp/README.md b/public/uploads/temp/README.md new file mode 100644 index 0000000..9ae0365 --- /dev/null +++ b/public/uploads/temp/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 图片裁剪临时目录 diff --git a/public/uploads/videos/README.md b/public/uploads/videos/README.md new file mode 100644 index 0000000..5c04737 --- /dev/null +++ b/public/uploads/videos/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 视频目录 diff --git a/public/uploads/voices/README.md b/public/uploads/voices/README.md new file mode 100644 index 0000000..150c736 --- /dev/null +++ b/public/uploads/voices/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 音频目录 diff --git a/public/uploads/xcximg/20240904/7974044b74bc691dacdb38de836f4b29.png b/public/uploads/xcximg/20240904/7974044b74bc691dacdb38de836f4b29.png new file mode 100644 index 0000000..eed2e17 Binary files /dev/null and b/public/uploads/xcximg/20240904/7974044b74bc691dacdb38de836f4b29.png differ diff --git a/public/uploads/xcximg/20240905/0da96da8b99ae7dbaffb66be1ae6df77.png b/public/uploads/xcximg/20240905/0da96da8b99ae7dbaffb66be1ae6df77.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240905/0da96da8b99ae7dbaffb66be1ae6df77.png differ diff --git a/public/uploads/xcximg/20240905/20fd6ae98e76b60ad3e8eac9564ec524.png b/public/uploads/xcximg/20240905/20fd6ae98e76b60ad3e8eac9564ec524.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240905/20fd6ae98e76b60ad3e8eac9564ec524.png differ diff --git a/public/uploads/xcximg/20240905/400a05b24c0051c8e4531c78eaff6021.png b/public/uploads/xcximg/20240905/400a05b24c0051c8e4531c78eaff6021.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240905/400a05b24c0051c8e4531c78eaff6021.png differ diff --git a/public/uploads/xcximg/20240905/5d36f98ab63597601f9f8f53493e4522.png b/public/uploads/xcximg/20240905/5d36f98ab63597601f9f8f53493e4522.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240905/5d36f98ab63597601f9f8f53493e4522.png differ diff --git a/public/uploads/xcximg/20240905/6df5983526be1fecc9e95ba6d15c6700.png b/public/uploads/xcximg/20240905/6df5983526be1fecc9e95ba6d15c6700.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240905/6df5983526be1fecc9e95ba6d15c6700.png differ diff --git a/public/uploads/xcximg/20240905/77e5127501b5eb88cf273ee774be6624.png b/public/uploads/xcximg/20240905/77e5127501b5eb88cf273ee774be6624.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240905/77e5127501b5eb88cf273ee774be6624.png differ diff --git a/public/uploads/xcximg/20240905/7e3e7b4d7dbe0d511d02b526cb5a6f96.png b/public/uploads/xcximg/20240905/7e3e7b4d7dbe0d511d02b526cb5a6f96.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240905/7e3e7b4d7dbe0d511d02b526cb5a6f96.png differ diff --git a/public/uploads/xcximg/20240905/7f676946995fa1fde38295f994928220.png b/public/uploads/xcximg/20240905/7f676946995fa1fde38295f994928220.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240905/7f676946995fa1fde38295f994928220.png differ diff --git a/public/uploads/xcximg/20240905/7f8d81ab2854c2a039834bd3018b0c9a.png b/public/uploads/xcximg/20240905/7f8d81ab2854c2a039834bd3018b0c9a.png new file mode 100644 index 0000000..8959da6 Binary files /dev/null and b/public/uploads/xcximg/20240905/7f8d81ab2854c2a039834bd3018b0c9a.png differ diff --git a/public/uploads/xcximg/20240905/b37b3168d1680e038e6629dc12a3f359.png b/public/uploads/xcximg/20240905/b37b3168d1680e038e6629dc12a3f359.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240905/b37b3168d1680e038e6629dc12a3f359.png differ diff --git a/public/uploads/xcximg/20240905/c57fc262e39135f112c8fc3d8bc72e94.png b/public/uploads/xcximg/20240905/c57fc262e39135f112c8fc3d8bc72e94.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240905/c57fc262e39135f112c8fc3d8bc72e94.png differ diff --git a/public/uploads/xcximg/20240905/d3b69b846c239ecb0cf6810073582c60.png b/public/uploads/xcximg/20240905/d3b69b846c239ecb0cf6810073582c60.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240905/d3b69b846c239ecb0cf6810073582c60.png differ diff --git a/public/uploads/xcximg/20240905/d4c3b94a970b58565e90532041031422.png b/public/uploads/xcximg/20240905/d4c3b94a970b58565e90532041031422.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240905/d4c3b94a970b58565e90532041031422.png differ diff --git a/public/uploads/xcximg/20240905/db2344365db91ce5ce88572d9431ea94.png b/public/uploads/xcximg/20240905/db2344365db91ce5ce88572d9431ea94.png new file mode 100644 index 0000000..8959da6 Binary files /dev/null and b/public/uploads/xcximg/20240905/db2344365db91ce5ce88572d9431ea94.png differ diff --git a/public/uploads/xcximg/20240905/dc815546ad1a0fe2be2878529b1b4b1c.png b/public/uploads/xcximg/20240905/dc815546ad1a0fe2be2878529b1b4b1c.png new file mode 100644 index 0000000..8959da6 Binary files /dev/null and b/public/uploads/xcximg/20240905/dc815546ad1a0fe2be2878529b1b4b1c.png differ diff --git a/public/uploads/xcximg/20240905/e9a87590afa7909e4e0a4c2b0ac4be36.png b/public/uploads/xcximg/20240905/e9a87590afa7909e4e0a4c2b0ac4be36.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240905/e9a87590afa7909e4e0a4c2b0ac4be36.png differ diff --git a/public/uploads/xcximg/20240905/edf813083eca28f385a3666a2b014e53.png b/public/uploads/xcximg/20240905/edf813083eca28f385a3666a2b014e53.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240905/edf813083eca28f385a3666a2b014e53.png differ diff --git a/public/uploads/xcximg/20240905/f4f5783c97f164482eb4b03c29460d62.png b/public/uploads/xcximg/20240905/f4f5783c97f164482eb4b03c29460d62.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240905/f4f5783c97f164482eb4b03c29460d62.png differ diff --git a/public/uploads/xcximg/20240906/06c483d38ce117b1cb8ea8f19cc2ca37.jpg b/public/uploads/xcximg/20240906/06c483d38ce117b1cb8ea8f19cc2ca37.jpg new file mode 100644 index 0000000..4a8f5f4 Binary files /dev/null and b/public/uploads/xcximg/20240906/06c483d38ce117b1cb8ea8f19cc2ca37.jpg differ diff --git a/public/uploads/xcximg/20240906/097bfb7068e0ac1f96dffd2993eff7ac.png b/public/uploads/xcximg/20240906/097bfb7068e0ac1f96dffd2993eff7ac.png new file mode 100644 index 0000000..8959da6 Binary files /dev/null and b/public/uploads/xcximg/20240906/097bfb7068e0ac1f96dffd2993eff7ac.png differ diff --git a/public/uploads/xcximg/20240906/0e223efc017275e5af27b40d5ec1ec1d.png b/public/uploads/xcximg/20240906/0e223efc017275e5af27b40d5ec1ec1d.png new file mode 100644 index 0000000..8959da6 Binary files /dev/null and b/public/uploads/xcximg/20240906/0e223efc017275e5af27b40d5ec1ec1d.png differ diff --git a/public/uploads/xcximg/20240906/0ff51b5effee87d15ae1c84a40d3be8c.png b/public/uploads/xcximg/20240906/0ff51b5effee87d15ae1c84a40d3be8c.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240906/0ff51b5effee87d15ae1c84a40d3be8c.png differ diff --git a/public/uploads/xcximg/20240906/1725587895_66da61b74e2bd.png b/public/uploads/xcximg/20240906/1725587895_66da61b74e2bd.png new file mode 100644 index 0000000..cb8e14b Binary files /dev/null and b/public/uploads/xcximg/20240906/1725587895_66da61b74e2bd.png differ diff --git a/public/uploads/xcximg/20240906/1725588149_66da62b51f599.png b/public/uploads/xcximg/20240906/1725588149_66da62b51f599.png new file mode 100644 index 0000000..1fc91b9 Binary files /dev/null and b/public/uploads/xcximg/20240906/1725588149_66da62b51f599.png differ diff --git a/public/uploads/xcximg/20240906/1725588359_66da63874ec6b.png b/public/uploads/xcximg/20240906/1725588359_66da63874ec6b.png new file mode 100644 index 0000000..83616f2 Binary files /dev/null and b/public/uploads/xcximg/20240906/1725588359_66da63874ec6b.png differ diff --git a/public/uploads/xcximg/20240906/1725588371_66da6393eff97.png b/public/uploads/xcximg/20240906/1725588371_66da6393eff97.png new file mode 100644 index 0000000..3807e8e Binary files /dev/null and b/public/uploads/xcximg/20240906/1725588371_66da6393eff97.png differ diff --git a/public/uploads/xcximg/20240906/1725589196_66da66ccf2e4d.png b/public/uploads/xcximg/20240906/1725589196_66da66ccf2e4d.png new file mode 100644 index 0000000..ada8414 Binary files /dev/null and b/public/uploads/xcximg/20240906/1725589196_66da66ccf2e4d.png differ diff --git a/public/uploads/xcximg/20240906/1725589204_66da66d4b0ff5.png b/public/uploads/xcximg/20240906/1725589204_66da66d4b0ff5.png new file mode 100644 index 0000000..679e3bd Binary files /dev/null and b/public/uploads/xcximg/20240906/1725589204_66da66d4b0ff5.png differ diff --git a/public/uploads/xcximg/20240906/1725589236_66da66f4ea724.png b/public/uploads/xcximg/20240906/1725589236_66da66f4ea724.png new file mode 100644 index 0000000..ef25e32 Binary files /dev/null and b/public/uploads/xcximg/20240906/1725589236_66da66f4ea724.png differ diff --git a/public/uploads/xcximg/20240906/1725589244_66da66fc232a5.png b/public/uploads/xcximg/20240906/1725589244_66da66fc232a5.png new file mode 100644 index 0000000..1419b1c Binary files /dev/null and b/public/uploads/xcximg/20240906/1725589244_66da66fc232a5.png differ diff --git a/public/uploads/xcximg/20240906/1725589876_66da6974c04f4.png b/public/uploads/xcximg/20240906/1725589876_66da6974c04f4.png new file mode 100644 index 0000000..fc9f372 Binary files /dev/null and b/public/uploads/xcximg/20240906/1725589876_66da6974c04f4.png differ diff --git a/public/uploads/xcximg/20240906/1725589885_66da697dafa14.png b/public/uploads/xcximg/20240906/1725589885_66da697dafa14.png new file mode 100644 index 0000000..affb853 Binary files /dev/null and b/public/uploads/xcximg/20240906/1725589885_66da697dafa14.png differ diff --git a/public/uploads/xcximg/20240906/1725604390_66daa22646a61.png b/public/uploads/xcximg/20240906/1725604390_66daa22646a61.png new file mode 100644 index 0000000..c90aa8b Binary files /dev/null and b/public/uploads/xcximg/20240906/1725604390_66daa22646a61.png differ diff --git a/public/uploads/xcximg/20240906/1725604407_66daa2370521b.png b/public/uploads/xcximg/20240906/1725604407_66daa2370521b.png new file mode 100644 index 0000000..a6b699b Binary files /dev/null and b/public/uploads/xcximg/20240906/1725604407_66daa2370521b.png differ diff --git a/public/uploads/xcximg/20240906/1725614966_66dacb762f3cf.png b/public/uploads/xcximg/20240906/1725614966_66dacb762f3cf.png new file mode 100644 index 0000000..0b9832e Binary files /dev/null and b/public/uploads/xcximg/20240906/1725614966_66dacb762f3cf.png differ diff --git a/public/uploads/xcximg/20240906/1725614992_66dacb90b762a.png b/public/uploads/xcximg/20240906/1725614992_66dacb90b762a.png new file mode 100644 index 0000000..03ba9ea Binary files /dev/null and b/public/uploads/xcximg/20240906/1725614992_66dacb90b762a.png differ diff --git a/public/uploads/xcximg/20240906/1725616423_66dad127af24a.png b/public/uploads/xcximg/20240906/1725616423_66dad127af24a.png new file mode 100644 index 0000000..c836e19 Binary files /dev/null and b/public/uploads/xcximg/20240906/1725616423_66dad127af24a.png differ diff --git a/public/uploads/xcximg/20240906/1725616429_66dad12dea853.png b/public/uploads/xcximg/20240906/1725616429_66dad12dea853.png new file mode 100644 index 0000000..4642f32 Binary files /dev/null and b/public/uploads/xcximg/20240906/1725616429_66dad12dea853.png differ diff --git a/public/uploads/xcximg/20240906/1725617277_66dad47d9add6.png b/public/uploads/xcximg/20240906/1725617277_66dad47d9add6.png new file mode 100644 index 0000000..6c0e061 Binary files /dev/null and b/public/uploads/xcximg/20240906/1725617277_66dad47d9add6.png differ diff --git a/public/uploads/xcximg/20240906/1725618151_66dad7e7369d3.png b/public/uploads/xcximg/20240906/1725618151_66dad7e7369d3.png new file mode 100644 index 0000000..9d96015 Binary files /dev/null and b/public/uploads/xcximg/20240906/1725618151_66dad7e7369d3.png differ diff --git a/public/uploads/xcximg/20240906/1725618622_66dad9bee8cee.png b/public/uploads/xcximg/20240906/1725618622_66dad9bee8cee.png new file mode 100644 index 0000000..5d58bc1 Binary files /dev/null and b/public/uploads/xcximg/20240906/1725618622_66dad9bee8cee.png differ diff --git a/public/uploads/xcximg/20240906/1725618644_66dad9d447882.png b/public/uploads/xcximg/20240906/1725618644_66dad9d447882.png new file mode 100644 index 0000000..7cbabd2 Binary files /dev/null and b/public/uploads/xcximg/20240906/1725618644_66dad9d447882.png differ diff --git a/public/uploads/xcximg/20240906/1732deb930cd4c700b476a4fe0a77be1.jpg b/public/uploads/xcximg/20240906/1732deb930cd4c700b476a4fe0a77be1.jpg new file mode 100644 index 0000000..275c15e Binary files /dev/null and b/public/uploads/xcximg/20240906/1732deb930cd4c700b476a4fe0a77be1.jpg differ diff --git a/public/uploads/xcximg/20240906/1b26ccce4a0f37bebb76734b33e2b13b.jpg b/public/uploads/xcximg/20240906/1b26ccce4a0f37bebb76734b33e2b13b.jpg new file mode 100644 index 0000000..59fac52 Binary files /dev/null and b/public/uploads/xcximg/20240906/1b26ccce4a0f37bebb76734b33e2b13b.jpg differ diff --git a/public/uploads/xcximg/20240906/1bed14d8fc27c22339b4e6fc7fa883f0.png b/public/uploads/xcximg/20240906/1bed14d8fc27c22339b4e6fc7fa883f0.png new file mode 100644 index 0000000..bcebc74 Binary files /dev/null and b/public/uploads/xcximg/20240906/1bed14d8fc27c22339b4e6fc7fa883f0.png differ diff --git a/public/uploads/xcximg/20240906/1c1d9caa4befe97b3405563861304a19.jpg b/public/uploads/xcximg/20240906/1c1d9caa4befe97b3405563861304a19.jpg new file mode 100644 index 0000000..02d1e7a Binary files /dev/null and b/public/uploads/xcximg/20240906/1c1d9caa4befe97b3405563861304a19.jpg differ diff --git a/public/uploads/xcximg/20240906/1cc415732dd4db534dcfccba6c8cda67.jpg b/public/uploads/xcximg/20240906/1cc415732dd4db534dcfccba6c8cda67.jpg new file mode 100644 index 0000000..f359a93 Binary files /dev/null and b/public/uploads/xcximg/20240906/1cc415732dd4db534dcfccba6c8cda67.jpg differ diff --git a/public/uploads/xcximg/20240906/23538e53a7e4445794a478bd996dd4f2.png b/public/uploads/xcximg/20240906/23538e53a7e4445794a478bd996dd4f2.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240906/23538e53a7e4445794a478bd996dd4f2.png differ diff --git a/public/uploads/xcximg/20240906/28eae389377af8ea3f04a870f3de35d4.jpg b/public/uploads/xcximg/20240906/28eae389377af8ea3f04a870f3de35d4.jpg new file mode 100644 index 0000000..b13cea8 Binary files /dev/null and b/public/uploads/xcximg/20240906/28eae389377af8ea3f04a870f3de35d4.jpg differ diff --git a/public/uploads/xcximg/20240906/2b4a016070f418179c429c2429d9d2f0.png b/public/uploads/xcximg/20240906/2b4a016070f418179c429c2429d9d2f0.png new file mode 100644 index 0000000..bcebc74 Binary files /dev/null and b/public/uploads/xcximg/20240906/2b4a016070f418179c429c2429d9d2f0.png differ diff --git a/public/uploads/xcximg/20240906/35ca92a641c0184fbf583ba39aaa831e.jpg b/public/uploads/xcximg/20240906/35ca92a641c0184fbf583ba39aaa831e.jpg new file mode 100644 index 0000000..49ab7ad Binary files /dev/null and b/public/uploads/xcximg/20240906/35ca92a641c0184fbf583ba39aaa831e.jpg differ diff --git a/public/uploads/xcximg/20240906/390050aad0a46988fb728ff0c43c5a72.png b/public/uploads/xcximg/20240906/390050aad0a46988fb728ff0c43c5a72.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20240906/390050aad0a46988fb728ff0c43c5a72.png differ diff --git a/public/uploads/xcximg/20240906/39580cc3a3d11736ef4c2f1e092828df.jpg b/public/uploads/xcximg/20240906/39580cc3a3d11736ef4c2f1e092828df.jpg new file mode 100644 index 0000000..5e9cf3e Binary files /dev/null and b/public/uploads/xcximg/20240906/39580cc3a3d11736ef4c2f1e092828df.jpg differ diff --git a/public/uploads/xcximg/20240906/3cb641e45b5fecb9af2c46a042afdf58.jpg b/public/uploads/xcximg/20240906/3cb641e45b5fecb9af2c46a042afdf58.jpg new file mode 100644 index 0000000..22bba8a Binary files /dev/null and b/public/uploads/xcximg/20240906/3cb641e45b5fecb9af2c46a042afdf58.jpg differ diff --git a/public/uploads/xcximg/20240906/40085b02ffb9c523115c6566c5814177.jpg b/public/uploads/xcximg/20240906/40085b02ffb9c523115c6566c5814177.jpg new file mode 100644 index 0000000..56eabb1 Binary files /dev/null and b/public/uploads/xcximg/20240906/40085b02ffb9c523115c6566c5814177.jpg differ diff --git a/public/uploads/xcximg/20240906/44b7538084b7b9a9ec58156708f4068c.jpg b/public/uploads/xcximg/20240906/44b7538084b7b9a9ec58156708f4068c.jpg new file mode 100644 index 0000000..ab5b5b8 Binary files /dev/null and b/public/uploads/xcximg/20240906/44b7538084b7b9a9ec58156708f4068c.jpg differ diff --git a/public/uploads/xcximg/20240906/4506f29dfca7ba8b44b083dd031adc3b.jpg b/public/uploads/xcximg/20240906/4506f29dfca7ba8b44b083dd031adc3b.jpg new file mode 100644 index 0000000..f9a8467 Binary files /dev/null and b/public/uploads/xcximg/20240906/4506f29dfca7ba8b44b083dd031adc3b.jpg differ diff --git a/public/uploads/xcximg/20240906/46f64616956220a5f5dbee9026c83894.jpg b/public/uploads/xcximg/20240906/46f64616956220a5f5dbee9026c83894.jpg new file mode 100644 index 0000000..9ee6727 Binary files /dev/null and b/public/uploads/xcximg/20240906/46f64616956220a5f5dbee9026c83894.jpg differ diff --git a/public/uploads/xcximg/20240906/49f420d84a7d903f4e6e946d8a198c9d.jpg b/public/uploads/xcximg/20240906/49f420d84a7d903f4e6e946d8a198c9d.jpg new file mode 100644 index 0000000..9c9e683 Binary files /dev/null and b/public/uploads/xcximg/20240906/49f420d84a7d903f4e6e946d8a198c9d.jpg differ diff --git a/public/uploads/xcximg/20240906/4a7edb872d403b025468cf20602b96d0.jpg b/public/uploads/xcximg/20240906/4a7edb872d403b025468cf20602b96d0.jpg new file mode 100644 index 0000000..07cb779 Binary files /dev/null and b/public/uploads/xcximg/20240906/4a7edb872d403b025468cf20602b96d0.jpg differ diff --git a/public/uploads/xcximg/20240906/71ebeae3f52fe93d498700e1b90d2135.jpg b/public/uploads/xcximg/20240906/71ebeae3f52fe93d498700e1b90d2135.jpg new file mode 100644 index 0000000..8cb908f Binary files /dev/null and b/public/uploads/xcximg/20240906/71ebeae3f52fe93d498700e1b90d2135.jpg differ diff --git a/public/uploads/xcximg/20240906/73be4904fe8683ae5026faaa59c59ee3.jpg b/public/uploads/xcximg/20240906/73be4904fe8683ae5026faaa59c59ee3.jpg new file mode 100644 index 0000000..8eab281 Binary files /dev/null and b/public/uploads/xcximg/20240906/73be4904fe8683ae5026faaa59c59ee3.jpg differ diff --git a/public/uploads/xcximg/20240906/772dd617e7ec717618ff494d082c2161.png b/public/uploads/xcximg/20240906/772dd617e7ec717618ff494d082c2161.png new file mode 100644 index 0000000..bcebc74 Binary files /dev/null and b/public/uploads/xcximg/20240906/772dd617e7ec717618ff494d082c2161.png differ diff --git a/public/uploads/xcximg/20240906/7bde64d1c5dac6dd6c8d33496893a045.png b/public/uploads/xcximg/20240906/7bde64d1c5dac6dd6c8d33496893a045.png new file mode 100644 index 0000000..38cdd67 Binary files /dev/null and b/public/uploads/xcximg/20240906/7bde64d1c5dac6dd6c8d33496893a045.png differ diff --git a/public/uploads/xcximg/20240906/7d157e9f29cf37ad393b65d297d95239.png b/public/uploads/xcximg/20240906/7d157e9f29cf37ad393b65d297d95239.png new file mode 100644 index 0000000..8959da6 Binary files /dev/null and b/public/uploads/xcximg/20240906/7d157e9f29cf37ad393b65d297d95239.png differ diff --git a/public/uploads/xcximg/20240906/85867be8b2776ec78720851c4d9ba6b0.jpg b/public/uploads/xcximg/20240906/85867be8b2776ec78720851c4d9ba6b0.jpg new file mode 100644 index 0000000..670298e Binary files /dev/null and b/public/uploads/xcximg/20240906/85867be8b2776ec78720851c4d9ba6b0.jpg differ diff --git a/public/uploads/xcximg/20240906/88028155335aadc7ba3954fc36ae277a.jpg b/public/uploads/xcximg/20240906/88028155335aadc7ba3954fc36ae277a.jpg new file mode 100644 index 0000000..0f6a9bd Binary files /dev/null and b/public/uploads/xcximg/20240906/88028155335aadc7ba3954fc36ae277a.jpg differ diff --git a/public/uploads/xcximg/20240906/8921365a26e8b3a98f5a2a9d9960b616.png b/public/uploads/xcximg/20240906/8921365a26e8b3a98f5a2a9d9960b616.png new file mode 100644 index 0000000..bcebc74 Binary files /dev/null and b/public/uploads/xcximg/20240906/8921365a26e8b3a98f5a2a9d9960b616.png differ diff --git a/public/uploads/xcximg/20240906/8a04e7a9a7618db7d00ee49fe7333d32.jpg b/public/uploads/xcximg/20240906/8a04e7a9a7618db7d00ee49fe7333d32.jpg new file mode 100644 index 0000000..3c7a1ec Binary files /dev/null and b/public/uploads/xcximg/20240906/8a04e7a9a7618db7d00ee49fe7333d32.jpg differ diff --git a/public/uploads/xcximg/20240906/8a084f57078433e3f45689ddf96412b5.jpg b/public/uploads/xcximg/20240906/8a084f57078433e3f45689ddf96412b5.jpg new file mode 100644 index 0000000..f981f30 Binary files /dev/null and b/public/uploads/xcximg/20240906/8a084f57078433e3f45689ddf96412b5.jpg differ diff --git a/public/uploads/xcximg/20240906/8fc87e5763976b810a06504a9f4b58f9.png b/public/uploads/xcximg/20240906/8fc87e5763976b810a06504a9f4b58f9.png new file mode 100644 index 0000000..6d44981 Binary files /dev/null and b/public/uploads/xcximg/20240906/8fc87e5763976b810a06504a9f4b58f9.png differ diff --git a/public/uploads/xcximg/20240906/92ee600ab923d98d4a1a58780259aa96.jpg b/public/uploads/xcximg/20240906/92ee600ab923d98d4a1a58780259aa96.jpg new file mode 100644 index 0000000..2a810ec Binary files /dev/null and b/public/uploads/xcximg/20240906/92ee600ab923d98d4a1a58780259aa96.jpg differ diff --git a/public/uploads/xcximg/20240906/9891d1b5db71ae57a15ab12e60467696.jpg b/public/uploads/xcximg/20240906/9891d1b5db71ae57a15ab12e60467696.jpg new file mode 100644 index 0000000..5e3ff16 Binary files /dev/null and b/public/uploads/xcximg/20240906/9891d1b5db71ae57a15ab12e60467696.jpg differ diff --git a/public/uploads/xcximg/20240906/9d183baaecfcd4d4f4374f0fa6e5a7dd.jpg b/public/uploads/xcximg/20240906/9d183baaecfcd4d4f4374f0fa6e5a7dd.jpg new file mode 100644 index 0000000..8ecc957 Binary files /dev/null and b/public/uploads/xcximg/20240906/9d183baaecfcd4d4f4374f0fa6e5a7dd.jpg differ diff --git a/public/uploads/xcximg/20240906/9df44625cd120222506aab58520a5327.jpg b/public/uploads/xcximg/20240906/9df44625cd120222506aab58520a5327.jpg new file mode 100644 index 0000000..95ed17e Binary files /dev/null and b/public/uploads/xcximg/20240906/9df44625cd120222506aab58520a5327.jpg differ diff --git a/public/uploads/xcximg/20240906/a1550947ed5f7f352e70f51ec07a62e6.png b/public/uploads/xcximg/20240906/a1550947ed5f7f352e70f51ec07a62e6.png new file mode 100644 index 0000000..bcebc74 Binary files /dev/null and b/public/uploads/xcximg/20240906/a1550947ed5f7f352e70f51ec07a62e6.png differ diff --git a/public/uploads/xcximg/20240906/a34a0649fde97aff2fcca20170a17263.jpg b/public/uploads/xcximg/20240906/a34a0649fde97aff2fcca20170a17263.jpg new file mode 100644 index 0000000..56eabb1 Binary files /dev/null and b/public/uploads/xcximg/20240906/a34a0649fde97aff2fcca20170a17263.jpg differ diff --git a/public/uploads/xcximg/20240906/a636d6a3089afd875e3c741e34ba18fe.png b/public/uploads/xcximg/20240906/a636d6a3089afd875e3c741e34ba18fe.png new file mode 100644 index 0000000..b88ef66 Binary files /dev/null and b/public/uploads/xcximg/20240906/a636d6a3089afd875e3c741e34ba18fe.png differ diff --git a/public/uploads/xcximg/20240906/a7fc7cfeb24c7b197899a6d0b922d0ea.jpg b/public/uploads/xcximg/20240906/a7fc7cfeb24c7b197899a6d0b922d0ea.jpg new file mode 100644 index 0000000..de69f45 Binary files /dev/null and b/public/uploads/xcximg/20240906/a7fc7cfeb24c7b197899a6d0b922d0ea.jpg differ diff --git a/public/uploads/xcximg/20240906/a8f17cbfba86a6b005d83402970519d3.jpg b/public/uploads/xcximg/20240906/a8f17cbfba86a6b005d83402970519d3.jpg new file mode 100644 index 0000000..5906d16 Binary files /dev/null and b/public/uploads/xcximg/20240906/a8f17cbfba86a6b005d83402970519d3.jpg differ diff --git a/public/uploads/xcximg/20240906/abb9e87c0a68fbe59ee318080e840761.jpg b/public/uploads/xcximg/20240906/abb9e87c0a68fbe59ee318080e840761.jpg new file mode 100644 index 0000000..3fce888 Binary files /dev/null and b/public/uploads/xcximg/20240906/abb9e87c0a68fbe59ee318080e840761.jpg differ diff --git a/public/uploads/xcximg/20240906/b24852a63205fb65f9b99827149c90eb.jpg b/public/uploads/xcximg/20240906/b24852a63205fb65f9b99827149c90eb.jpg new file mode 100644 index 0000000..612e004 Binary files /dev/null and b/public/uploads/xcximg/20240906/b24852a63205fb65f9b99827149c90eb.jpg differ diff --git a/public/uploads/xcximg/20240906/b5272e5ed8c92df40e90dd492a014d01.jpg b/public/uploads/xcximg/20240906/b5272e5ed8c92df40e90dd492a014d01.jpg new file mode 100644 index 0000000..0e92f89 Binary files /dev/null and b/public/uploads/xcximg/20240906/b5272e5ed8c92df40e90dd492a014d01.jpg differ diff --git a/public/uploads/xcximg/20240906/b6e1bc7dceeae3af375783bd24cfdea8.png b/public/uploads/xcximg/20240906/b6e1bc7dceeae3af375783bd24cfdea8.png new file mode 100644 index 0000000..137341b Binary files /dev/null and b/public/uploads/xcximg/20240906/b6e1bc7dceeae3af375783bd24cfdea8.png differ diff --git a/public/uploads/xcximg/20240906/c22fb650802c98e27d1d53ec6a7e41a4.jpg b/public/uploads/xcximg/20240906/c22fb650802c98e27d1d53ec6a7e41a4.jpg new file mode 100644 index 0000000..4d22142 Binary files /dev/null and b/public/uploads/xcximg/20240906/c22fb650802c98e27d1d53ec6a7e41a4.jpg differ diff --git a/public/uploads/xcximg/20240906/c5ca257728579e01ad1b00e057fecafc.png b/public/uploads/xcximg/20240906/c5ca257728579e01ad1b00e057fecafc.png new file mode 100644 index 0000000..8959da6 Binary files /dev/null and b/public/uploads/xcximg/20240906/c5ca257728579e01ad1b00e057fecafc.png differ diff --git a/public/uploads/xcximg/20240906/cb5a5f45458baf2b05bb482a9d677c2e.jpg b/public/uploads/xcximg/20240906/cb5a5f45458baf2b05bb482a9d677c2e.jpg new file mode 100644 index 0000000..5c7a385 Binary files /dev/null and b/public/uploads/xcximg/20240906/cb5a5f45458baf2b05bb482a9d677c2e.jpg differ diff --git a/public/uploads/xcximg/20240906/cc45b197749923bd744a71e08d785830.png b/public/uploads/xcximg/20240906/cc45b197749923bd744a71e08d785830.png new file mode 100644 index 0000000..a20fd1a Binary files /dev/null and b/public/uploads/xcximg/20240906/cc45b197749923bd744a71e08d785830.png differ diff --git a/public/uploads/xcximg/20240906/d7a19a563de4cd6f978b6863b95542c8.jpg b/public/uploads/xcximg/20240906/d7a19a563de4cd6f978b6863b95542c8.jpg new file mode 100644 index 0000000..b751ba0 Binary files /dev/null and b/public/uploads/xcximg/20240906/d7a19a563de4cd6f978b6863b95542c8.jpg differ diff --git a/public/uploads/xcximg/20240906/d8c7dfb9c29f5e3424c8063e235ed58c.jpg b/public/uploads/xcximg/20240906/d8c7dfb9c29f5e3424c8063e235ed58c.jpg new file mode 100644 index 0000000..9e0964c Binary files /dev/null and b/public/uploads/xcximg/20240906/d8c7dfb9c29f5e3424c8063e235ed58c.jpg differ diff --git a/public/uploads/xcximg/20240906/e0627ceebe64c3f2d9f13b883804d720.jpg b/public/uploads/xcximg/20240906/e0627ceebe64c3f2d9f13b883804d720.jpg new file mode 100644 index 0000000..b89b000 Binary files /dev/null and b/public/uploads/xcximg/20240906/e0627ceebe64c3f2d9f13b883804d720.jpg differ diff --git a/public/uploads/xcximg/20240906/e5e6c901f938f41601324c9a842668e2.png b/public/uploads/xcximg/20240906/e5e6c901f938f41601324c9a842668e2.png new file mode 100644 index 0000000..38cdd67 Binary files /dev/null and b/public/uploads/xcximg/20240906/e5e6c901f938f41601324c9a842668e2.png differ diff --git a/public/uploads/xcximg/20240906/f1dfd6afb2c5703d30ad230576569036.png b/public/uploads/xcximg/20240906/f1dfd6afb2c5703d30ad230576569036.png new file mode 100644 index 0000000..bcebc74 Binary files /dev/null and b/public/uploads/xcximg/20240906/f1dfd6afb2c5703d30ad230576569036.png differ diff --git a/public/uploads/xcximg/20240906/f892e831c4a60fc235523b85cc48c61b.jpg b/public/uploads/xcximg/20240906/f892e831c4a60fc235523b85cc48c61b.jpg new file mode 100644 index 0000000..9e09bdb Binary files /dev/null and b/public/uploads/xcximg/20240906/f892e831c4a60fc235523b85cc48c61b.jpg differ diff --git a/public/uploads/xcximg/20240906/ff9e48094920b50e6a301d0a414194a6.png b/public/uploads/xcximg/20240906/ff9e48094920b50e6a301d0a414194a6.png new file mode 100644 index 0000000..38cdd67 Binary files /dev/null and b/public/uploads/xcximg/20240906/ff9e48094920b50e6a301d0a414194a6.png differ diff --git a/public/uploads/xcximg/20240910/8676372aadf2c035bec8a4aae9a1a883.png b/public/uploads/xcximg/20240910/8676372aadf2c035bec8a4aae9a1a883.png new file mode 100644 index 0000000..b88ef66 Binary files /dev/null and b/public/uploads/xcximg/20240910/8676372aadf2c035bec8a4aae9a1a883.png differ diff --git a/public/uploads/xcximg/20240910/b28b6010238371a810cddc4f1a9423e1.png b/public/uploads/xcximg/20240910/b28b6010238371a810cddc4f1a9423e1.png new file mode 100644 index 0000000..38cdd67 Binary files /dev/null and b/public/uploads/xcximg/20240910/b28b6010238371a810cddc4f1a9423e1.png differ diff --git a/public/uploads/xcximg/20240910/bbd18d0782ef50b1813b063c5fbc571c.png b/public/uploads/xcximg/20240910/bbd18d0782ef50b1813b063c5fbc571c.png new file mode 100644 index 0000000..38cdd67 Binary files /dev/null and b/public/uploads/xcximg/20240910/bbd18d0782ef50b1813b063c5fbc571c.png differ diff --git a/public/uploads/xcximg/20240923/05d7c5ba1a4447c44b070c705c9e1050.jpg b/public/uploads/xcximg/20240923/05d7c5ba1a4447c44b070c705c9e1050.jpg new file mode 100644 index 0000000..aa0e2d4 Binary files /dev/null and b/public/uploads/xcximg/20240923/05d7c5ba1a4447c44b070c705c9e1050.jpg differ diff --git a/public/uploads/xcximg/20240923/0dd28e3e8693ac6bddf9525b54af0a1c.jpg b/public/uploads/xcximg/20240923/0dd28e3e8693ac6bddf9525b54af0a1c.jpg new file mode 100644 index 0000000..96ffe36 Binary files /dev/null and b/public/uploads/xcximg/20240923/0dd28e3e8693ac6bddf9525b54af0a1c.jpg differ diff --git a/public/uploads/xcximg/20240923/103f741c47957a4d89b8576ed70c5da6.jpg b/public/uploads/xcximg/20240923/103f741c47957a4d89b8576ed70c5da6.jpg new file mode 100644 index 0000000..b73cac6 Binary files /dev/null and b/public/uploads/xcximg/20240923/103f741c47957a4d89b8576ed70c5da6.jpg differ diff --git a/public/uploads/xcximg/20240923/1727102307_66f17d63881a5.png b/public/uploads/xcximg/20240923/1727102307_66f17d63881a5.png new file mode 100644 index 0000000..65f3add Binary files /dev/null and b/public/uploads/xcximg/20240923/1727102307_66f17d63881a5.png differ diff --git a/public/uploads/xcximg/20240923/1727102331_66f17d7b163c1.png b/public/uploads/xcximg/20240923/1727102331_66f17d7b163c1.png new file mode 100644 index 0000000..32caff4 Binary files /dev/null and b/public/uploads/xcximg/20240923/1727102331_66f17d7b163c1.png differ diff --git a/public/uploads/xcximg/20240923/2bbde3051b5b5580bbb338db433c31e8.jpg b/public/uploads/xcximg/20240923/2bbde3051b5b5580bbb338db433c31e8.jpg new file mode 100644 index 0000000..2e8b8fb Binary files /dev/null and b/public/uploads/xcximg/20240923/2bbde3051b5b5580bbb338db433c31e8.jpg differ diff --git a/public/uploads/xcximg/20240923/381a4aefe6dc50e3fb79c2b1accb3b72.jpg b/public/uploads/xcximg/20240923/381a4aefe6dc50e3fb79c2b1accb3b72.jpg new file mode 100644 index 0000000..66bbe5e Binary files /dev/null and b/public/uploads/xcximg/20240923/381a4aefe6dc50e3fb79c2b1accb3b72.jpg differ diff --git a/public/uploads/xcximg/20240923/a19e7ff558ceb07a56a8b53887493b70.jpg b/public/uploads/xcximg/20240923/a19e7ff558ceb07a56a8b53887493b70.jpg new file mode 100644 index 0000000..950526c Binary files /dev/null and b/public/uploads/xcximg/20240923/a19e7ff558ceb07a56a8b53887493b70.jpg differ diff --git a/public/uploads/xcximg/20240923/d62b9c07d7066ec610685727c15ff815.jpg b/public/uploads/xcximg/20240923/d62b9c07d7066ec610685727c15ff815.jpg new file mode 100644 index 0000000..6240722 Binary files /dev/null and b/public/uploads/xcximg/20240923/d62b9c07d7066ec610685727c15ff815.jpg differ diff --git a/public/uploads/xcximg/20241011/1728616967_67089a077c416.png b/public/uploads/xcximg/20241011/1728616967_67089a077c416.png new file mode 100644 index 0000000..1e1e3f7 Binary files /dev/null and b/public/uploads/xcximg/20241011/1728616967_67089a077c416.png differ diff --git a/public/uploads/xcximg/20241011/1728616979_67089a137bf7d.png b/public/uploads/xcximg/20241011/1728616979_67089a137bf7d.png new file mode 100644 index 0000000..471c048 Binary files /dev/null and b/public/uploads/xcximg/20241011/1728616979_67089a137bf7d.png differ diff --git a/public/uploads/xcximg/20241011/cff7ad4d6f916ecc3f531ccf8d14245f.jpg b/public/uploads/xcximg/20241011/cff7ad4d6f916ecc3f531ccf8d14245f.jpg new file mode 100644 index 0000000..4a81167 Binary files /dev/null and b/public/uploads/xcximg/20241011/cff7ad4d6f916ecc3f531ccf8d14245f.jpg differ diff --git a/public/uploads/xcximg/20241011/f48c1432f08deb62e6291781deac2cf3.jpg b/public/uploads/xcximg/20241011/f48c1432f08deb62e6291781deac2cf3.jpg new file mode 100644 index 0000000..f1772a7 Binary files /dev/null and b/public/uploads/xcximg/20241011/f48c1432f08deb62e6291781deac2cf3.jpg differ diff --git a/public/uploads/xcximg/20241011/f9325e481539f56844d6166fbff059ca.jpg b/public/uploads/xcximg/20241011/f9325e481539f56844d6166fbff059ca.jpg new file mode 100644 index 0000000..d12c1c1 Binary files /dev/null and b/public/uploads/xcximg/20241011/f9325e481539f56844d6166fbff059ca.jpg differ diff --git a/public/uploads/xcximg/20241015/60368462ced765c1743b0d498373cc75.png b/public/uploads/xcximg/20241015/60368462ced765c1743b0d498373cc75.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241015/60368462ced765c1743b0d498373cc75.png differ diff --git a/public/uploads/xcximg/20241015/8415d1cf0d92552c74a31b3fcec91f03.png b/public/uploads/xcximg/20241015/8415d1cf0d92552c74a31b3fcec91f03.png new file mode 100644 index 0000000..6923011 Binary files /dev/null and b/public/uploads/xcximg/20241015/8415d1cf0d92552c74a31b3fcec91f03.png differ diff --git a/public/uploads/xcximg/20241015/8f6585ad8464df714948ddcacf50f3ae.png b/public/uploads/xcximg/20241015/8f6585ad8464df714948ddcacf50f3ae.png new file mode 100644 index 0000000..6e97750 Binary files /dev/null and b/public/uploads/xcximg/20241015/8f6585ad8464df714948ddcacf50f3ae.png differ diff --git a/public/uploads/xcximg/20241016/08f6977fe97e861a15f9441bd2f4d139.png b/public/uploads/xcximg/20241016/08f6977fe97e861a15f9441bd2f4d139.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241016/08f6977fe97e861a15f9441bd2f4d139.png differ diff --git a/public/uploads/xcximg/20241016/0901595b1c8a237461d55cb3a70b125c.jpeg b/public/uploads/xcximg/20241016/0901595b1c8a237461d55cb3a70b125c.jpeg new file mode 100644 index 0000000..50d72ce Binary files /dev/null and b/public/uploads/xcximg/20241016/0901595b1c8a237461d55cb3a70b125c.jpeg differ diff --git a/public/uploads/xcximg/20241016/0a20a68fd9d67d4b2ea841061918d657.png b/public/uploads/xcximg/20241016/0a20a68fd9d67d4b2ea841061918d657.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241016/0a20a68fd9d67d4b2ea841061918d657.png differ diff --git a/public/uploads/xcximg/20241016/0d13abcdc05972b6799382f42558124c.jpeg b/public/uploads/xcximg/20241016/0d13abcdc05972b6799382f42558124c.jpeg new file mode 100644 index 0000000..3d91478 Binary files /dev/null and b/public/uploads/xcximg/20241016/0d13abcdc05972b6799382f42558124c.jpeg differ diff --git a/public/uploads/xcximg/20241016/148f25dcac7d08db9ba21aaedf9eff26.jpeg b/public/uploads/xcximg/20241016/148f25dcac7d08db9ba21aaedf9eff26.jpeg new file mode 100644 index 0000000..3b68346 Binary files /dev/null and b/public/uploads/xcximg/20241016/148f25dcac7d08db9ba21aaedf9eff26.jpeg differ diff --git a/public/uploads/xcximg/20241016/18d28bc519d81e4a13f3d14d4712eba5.png b/public/uploads/xcximg/20241016/18d28bc519d81e4a13f3d14d4712eba5.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/18d28bc519d81e4a13f3d14d4712eba5.png differ diff --git a/public/uploads/xcximg/20241016/19c8c185bf378793bc61bb397fb4d72f.jpeg b/public/uploads/xcximg/20241016/19c8c185bf378793bc61bb397fb4d72f.jpeg new file mode 100644 index 0000000..1659c63 Binary files /dev/null and b/public/uploads/xcximg/20241016/19c8c185bf378793bc61bb397fb4d72f.jpeg differ diff --git a/public/uploads/xcximg/20241016/1b7c0906060fa10d635cc3b3b6b62c45.jpeg b/public/uploads/xcximg/20241016/1b7c0906060fa10d635cc3b3b6b62c45.jpeg new file mode 100644 index 0000000..0eda425 Binary files /dev/null and b/public/uploads/xcximg/20241016/1b7c0906060fa10d635cc3b3b6b62c45.jpeg differ diff --git a/public/uploads/xcximg/20241016/1e3fce6da710a776ee1dc6691f2c3a79.jpeg b/public/uploads/xcximg/20241016/1e3fce6da710a776ee1dc6691f2c3a79.jpeg new file mode 100644 index 0000000..6419e0c Binary files /dev/null and b/public/uploads/xcximg/20241016/1e3fce6da710a776ee1dc6691f2c3a79.jpeg differ diff --git a/public/uploads/xcximg/20241016/1f3db2f8b613ba5bd754615dc42b5f56.png b/public/uploads/xcximg/20241016/1f3db2f8b613ba5bd754615dc42b5f56.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/1f3db2f8b613ba5bd754615dc42b5f56.png differ diff --git a/public/uploads/xcximg/20241016/1fb7738fecb3d319e88d3d40f91e926e.png b/public/uploads/xcximg/20241016/1fb7738fecb3d319e88d3d40f91e926e.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241016/1fb7738fecb3d319e88d3d40f91e926e.png differ diff --git a/public/uploads/xcximg/20241016/2461c4d703aa3723acd7027825a61e76.png b/public/uploads/xcximg/20241016/2461c4d703aa3723acd7027825a61e76.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/2461c4d703aa3723acd7027825a61e76.png differ diff --git a/public/uploads/xcximg/20241016/28a23551e971600228454f332ac183a2.png b/public/uploads/xcximg/20241016/28a23551e971600228454f332ac183a2.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241016/28a23551e971600228454f332ac183a2.png differ diff --git a/public/uploads/xcximg/20241016/2c3e13e497982ce945a051b1214ec1cc.png b/public/uploads/xcximg/20241016/2c3e13e497982ce945a051b1214ec1cc.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/2c3e13e497982ce945a051b1214ec1cc.png differ diff --git a/public/uploads/xcximg/20241016/2d6467ef631bce7e59ad2d9dc8b9b85d.jpeg b/public/uploads/xcximg/20241016/2d6467ef631bce7e59ad2d9dc8b9b85d.jpeg new file mode 100644 index 0000000..8062607 Binary files /dev/null and b/public/uploads/xcximg/20241016/2d6467ef631bce7e59ad2d9dc8b9b85d.jpeg differ diff --git a/public/uploads/xcximg/20241016/2f105b1731eece6d7f409076fb35419b.jpeg b/public/uploads/xcximg/20241016/2f105b1731eece6d7f409076fb35419b.jpeg new file mode 100644 index 0000000..c1bd3fc Binary files /dev/null and b/public/uploads/xcximg/20241016/2f105b1731eece6d7f409076fb35419b.jpeg differ diff --git a/public/uploads/xcximg/20241016/3fbdeab78ff1905a6f117bab0e75eb69.jpeg b/public/uploads/xcximg/20241016/3fbdeab78ff1905a6f117bab0e75eb69.jpeg new file mode 100644 index 0000000..63bcd91 Binary files /dev/null and b/public/uploads/xcximg/20241016/3fbdeab78ff1905a6f117bab0e75eb69.jpeg differ diff --git a/public/uploads/xcximg/20241016/42363a0c134cd7f9862a03f105c0c87f.png b/public/uploads/xcximg/20241016/42363a0c134cd7f9862a03f105c0c87f.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/42363a0c134cd7f9862a03f105c0c87f.png differ diff --git a/public/uploads/xcximg/20241016/45911f9472bf4b469f565345bd4b9085.png b/public/uploads/xcximg/20241016/45911f9472bf4b469f565345bd4b9085.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/45911f9472bf4b469f565345bd4b9085.png differ diff --git a/public/uploads/xcximg/20241016/477870c823fc3c2ec7b811dac00082e5.png b/public/uploads/xcximg/20241016/477870c823fc3c2ec7b811dac00082e5.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241016/477870c823fc3c2ec7b811dac00082e5.png differ diff --git a/public/uploads/xcximg/20241016/486fc6997437de96dfb97a58fbe5076c.png b/public/uploads/xcximg/20241016/486fc6997437de96dfb97a58fbe5076c.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/486fc6997437de96dfb97a58fbe5076c.png differ diff --git a/public/uploads/xcximg/20241016/5385af5f9ecb20f8c57eedad296c3f19.png b/public/uploads/xcximg/20241016/5385af5f9ecb20f8c57eedad296c3f19.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/5385af5f9ecb20f8c57eedad296c3f19.png differ diff --git a/public/uploads/xcximg/20241016/586e4c70baed272ccef7ea9dde782ebd.jpeg b/public/uploads/xcximg/20241016/586e4c70baed272ccef7ea9dde782ebd.jpeg new file mode 100644 index 0000000..cd07827 Binary files /dev/null and b/public/uploads/xcximg/20241016/586e4c70baed272ccef7ea9dde782ebd.jpeg differ diff --git a/public/uploads/xcximg/20241016/595832cfa5e2d99a71ce598d90e62afd.png b/public/uploads/xcximg/20241016/595832cfa5e2d99a71ce598d90e62afd.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/595832cfa5e2d99a71ce598d90e62afd.png differ diff --git a/public/uploads/xcximg/20241016/5a9a87b046b45f463eebe77b4d6c75f0.jpeg b/public/uploads/xcximg/20241016/5a9a87b046b45f463eebe77b4d6c75f0.jpeg new file mode 100644 index 0000000..250ba7a Binary files /dev/null and b/public/uploads/xcximg/20241016/5a9a87b046b45f463eebe77b4d6c75f0.jpeg differ diff --git a/public/uploads/xcximg/20241016/5b733a5c7cf51ee9ae3db0efc7a28bc5.png b/public/uploads/xcximg/20241016/5b733a5c7cf51ee9ae3db0efc7a28bc5.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241016/5b733a5c7cf51ee9ae3db0efc7a28bc5.png differ diff --git a/public/uploads/xcximg/20241016/65b27d8ce3bfe27722a554d73e55213f.jpeg b/public/uploads/xcximg/20241016/65b27d8ce3bfe27722a554d73e55213f.jpeg new file mode 100644 index 0000000..6b1726e Binary files /dev/null and b/public/uploads/xcximg/20241016/65b27d8ce3bfe27722a554d73e55213f.jpeg differ diff --git a/public/uploads/xcximg/20241016/67ff9cf3fe43e5551ae50f03e8a8dc74.jpeg b/public/uploads/xcximg/20241016/67ff9cf3fe43e5551ae50f03e8a8dc74.jpeg new file mode 100644 index 0000000..7199d06 Binary files /dev/null and b/public/uploads/xcximg/20241016/67ff9cf3fe43e5551ae50f03e8a8dc74.jpeg differ diff --git a/public/uploads/xcximg/20241016/6eb9a00a63e29d0bf7f857c7ba9eee47.jpeg b/public/uploads/xcximg/20241016/6eb9a00a63e29d0bf7f857c7ba9eee47.jpeg new file mode 100644 index 0000000..f923856 Binary files /dev/null and b/public/uploads/xcximg/20241016/6eb9a00a63e29d0bf7f857c7ba9eee47.jpeg differ diff --git a/public/uploads/xcximg/20241016/73664c83cf9ec7845a34dd3a8d44a1f8.png b/public/uploads/xcximg/20241016/73664c83cf9ec7845a34dd3a8d44a1f8.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/73664c83cf9ec7845a34dd3a8d44a1f8.png differ diff --git a/public/uploads/xcximg/20241016/771e2662d165cd254019229e1a5748e0.jpeg b/public/uploads/xcximg/20241016/771e2662d165cd254019229e1a5748e0.jpeg new file mode 100644 index 0000000..dfe42d4 Binary files /dev/null and b/public/uploads/xcximg/20241016/771e2662d165cd254019229e1a5748e0.jpeg differ diff --git a/public/uploads/xcximg/20241016/7aab2dfe9c2d1ab56769617cfeda7264.jpeg b/public/uploads/xcximg/20241016/7aab2dfe9c2d1ab56769617cfeda7264.jpeg new file mode 100644 index 0000000..c8d606a Binary files /dev/null and b/public/uploads/xcximg/20241016/7aab2dfe9c2d1ab56769617cfeda7264.jpeg differ diff --git a/public/uploads/xcximg/20241016/7c395412bb167efc95ec5f4b5ae4e70f.jpeg b/public/uploads/xcximg/20241016/7c395412bb167efc95ec5f4b5ae4e70f.jpeg new file mode 100644 index 0000000..22f3190 Binary files /dev/null and b/public/uploads/xcximg/20241016/7c395412bb167efc95ec5f4b5ae4e70f.jpeg differ diff --git a/public/uploads/xcximg/20241016/7d2f623929081c1e4c6e3c67c095f920.png b/public/uploads/xcximg/20241016/7d2f623929081c1e4c6e3c67c095f920.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/7d2f623929081c1e4c6e3c67c095f920.png differ diff --git a/public/uploads/xcximg/20241016/7dfa514467717ee6458165efc5871d95.jpeg b/public/uploads/xcximg/20241016/7dfa514467717ee6458165efc5871d95.jpeg new file mode 100644 index 0000000..23c6cb1 Binary files /dev/null and b/public/uploads/xcximg/20241016/7dfa514467717ee6458165efc5871d95.jpeg differ diff --git a/public/uploads/xcximg/20241016/817ddc27877ec33840d29ad3656b6011.jpeg b/public/uploads/xcximg/20241016/817ddc27877ec33840d29ad3656b6011.jpeg new file mode 100644 index 0000000..5d9f69e Binary files /dev/null and b/public/uploads/xcximg/20241016/817ddc27877ec33840d29ad3656b6011.jpeg differ diff --git a/public/uploads/xcximg/20241016/8357cb8826eaa3dc57de8af7c71a1ec6.jpeg b/public/uploads/xcximg/20241016/8357cb8826eaa3dc57de8af7c71a1ec6.jpeg new file mode 100644 index 0000000..a8f3dde Binary files /dev/null and b/public/uploads/xcximg/20241016/8357cb8826eaa3dc57de8af7c71a1ec6.jpeg differ diff --git a/public/uploads/xcximg/20241016/876d1eb3ae8ee444a18b7ec73201389c.png b/public/uploads/xcximg/20241016/876d1eb3ae8ee444a18b7ec73201389c.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241016/876d1eb3ae8ee444a18b7ec73201389c.png differ diff --git a/public/uploads/xcximg/20241016/8f8921a031b9774b2ab2246847a74e11.jpeg b/public/uploads/xcximg/20241016/8f8921a031b9774b2ab2246847a74e11.jpeg new file mode 100644 index 0000000..a5dc939 Binary files /dev/null and b/public/uploads/xcximg/20241016/8f8921a031b9774b2ab2246847a74e11.jpeg differ diff --git a/public/uploads/xcximg/20241016/911024095345505aed91a5d93afc5971.png b/public/uploads/xcximg/20241016/911024095345505aed91a5d93afc5971.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241016/911024095345505aed91a5d93afc5971.png differ diff --git a/public/uploads/xcximg/20241016/921a39f6ea64a0711168bcfb7422f878.png b/public/uploads/xcximg/20241016/921a39f6ea64a0711168bcfb7422f878.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241016/921a39f6ea64a0711168bcfb7422f878.png differ diff --git a/public/uploads/xcximg/20241016/93c5b8294834fa907d5cd6fdd2446b31.jpeg b/public/uploads/xcximg/20241016/93c5b8294834fa907d5cd6fdd2446b31.jpeg new file mode 100644 index 0000000..73cadd2 Binary files /dev/null and b/public/uploads/xcximg/20241016/93c5b8294834fa907d5cd6fdd2446b31.jpeg differ diff --git a/public/uploads/xcximg/20241016/96b9d864503688a6e657f4194f2d0c90.jpeg b/public/uploads/xcximg/20241016/96b9d864503688a6e657f4194f2d0c90.jpeg new file mode 100644 index 0000000..3f0015b Binary files /dev/null and b/public/uploads/xcximg/20241016/96b9d864503688a6e657f4194f2d0c90.jpeg differ diff --git a/public/uploads/xcximg/20241016/98adc3711a929fb405ac6be9867fdfc1.jpeg b/public/uploads/xcximg/20241016/98adc3711a929fb405ac6be9867fdfc1.jpeg new file mode 100644 index 0000000..0a4cfe7 Binary files /dev/null and b/public/uploads/xcximg/20241016/98adc3711a929fb405ac6be9867fdfc1.jpeg differ diff --git a/public/uploads/xcximg/20241016/99fdd67f132aad2c703a57e56c0b3386.png b/public/uploads/xcximg/20241016/99fdd67f132aad2c703a57e56c0b3386.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241016/99fdd67f132aad2c703a57e56c0b3386.png differ diff --git a/public/uploads/xcximg/20241016/9b9779ed1418ae4932bb4250aaa1d3b7.jpg b/public/uploads/xcximg/20241016/9b9779ed1418ae4932bb4250aaa1d3b7.jpg new file mode 100644 index 0000000..912c2db Binary files /dev/null and b/public/uploads/xcximg/20241016/9b9779ed1418ae4932bb4250aaa1d3b7.jpg differ diff --git a/public/uploads/xcximg/20241016/9f060e2d205bf30bf70b1ae399cffd50.png b/public/uploads/xcximg/20241016/9f060e2d205bf30bf70b1ae399cffd50.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241016/9f060e2d205bf30bf70b1ae399cffd50.png differ diff --git a/public/uploads/xcximg/20241016/9fceec4b9b5e51eb2d26923ef45940aa.png b/public/uploads/xcximg/20241016/9fceec4b9b5e51eb2d26923ef45940aa.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241016/9fceec4b9b5e51eb2d26923ef45940aa.png differ diff --git a/public/uploads/xcximg/20241016/a891c18cfbd75529c4d450267bd0fb8c.jpeg b/public/uploads/xcximg/20241016/a891c18cfbd75529c4d450267bd0fb8c.jpeg new file mode 100644 index 0000000..f3582c3 Binary files /dev/null and b/public/uploads/xcximg/20241016/a891c18cfbd75529c4d450267bd0fb8c.jpeg differ diff --git a/public/uploads/xcximg/20241016/a8b8e07084d57f521fbad1a5a3b7e72a.jpeg b/public/uploads/xcximg/20241016/a8b8e07084d57f521fbad1a5a3b7e72a.jpeg new file mode 100644 index 0000000..56acf4b Binary files /dev/null and b/public/uploads/xcximg/20241016/a8b8e07084d57f521fbad1a5a3b7e72a.jpeg differ diff --git a/public/uploads/xcximg/20241016/a9a01379954e0027e959499ee80a924e.png b/public/uploads/xcximg/20241016/a9a01379954e0027e959499ee80a924e.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241016/a9a01379954e0027e959499ee80a924e.png differ diff --git a/public/uploads/xcximg/20241016/aa2c7df1a0932986165282ce907e2d65.jpeg b/public/uploads/xcximg/20241016/aa2c7df1a0932986165282ce907e2d65.jpeg new file mode 100644 index 0000000..2cbbcf8 Binary files /dev/null and b/public/uploads/xcximg/20241016/aa2c7df1a0932986165282ce907e2d65.jpeg differ diff --git a/public/uploads/xcximg/20241016/ab084a036f09eed6e6b65177c61d9580.png b/public/uploads/xcximg/20241016/ab084a036f09eed6e6b65177c61d9580.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/ab084a036f09eed6e6b65177c61d9580.png differ diff --git a/public/uploads/xcximg/20241016/adcbfb4453b008ff47fd51fc44cedb4a.png b/public/uploads/xcximg/20241016/adcbfb4453b008ff47fd51fc44cedb4a.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241016/adcbfb4453b008ff47fd51fc44cedb4a.png differ diff --git a/public/uploads/xcximg/20241016/b15c4e385ae44cdca0fbfc0472107b2d.jpeg b/public/uploads/xcximg/20241016/b15c4e385ae44cdca0fbfc0472107b2d.jpeg new file mode 100644 index 0000000..b8a5878 Binary files /dev/null and b/public/uploads/xcximg/20241016/b15c4e385ae44cdca0fbfc0472107b2d.jpeg differ diff --git a/public/uploads/xcximg/20241016/b2e3a61d32176ee402d9d5836180cb9c.png b/public/uploads/xcximg/20241016/b2e3a61d32176ee402d9d5836180cb9c.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/b2e3a61d32176ee402d9d5836180cb9c.png differ diff --git a/public/uploads/xcximg/20241016/b4ebf24d4d8d9b59824554abdbccf93c.png b/public/uploads/xcximg/20241016/b4ebf24d4d8d9b59824554abdbccf93c.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/b4ebf24d4d8d9b59824554abdbccf93c.png differ diff --git a/public/uploads/xcximg/20241016/bf22f092be7e3cec6cb84fb2716e4ceb.jpeg b/public/uploads/xcximg/20241016/bf22f092be7e3cec6cb84fb2716e4ceb.jpeg new file mode 100644 index 0000000..d11085f Binary files /dev/null and b/public/uploads/xcximg/20241016/bf22f092be7e3cec6cb84fb2716e4ceb.jpeg differ diff --git a/public/uploads/xcximg/20241016/c01e53de7eb41cbe55ffa02fdc24c3df.png b/public/uploads/xcximg/20241016/c01e53de7eb41cbe55ffa02fdc24c3df.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/c01e53de7eb41cbe55ffa02fdc24c3df.png differ diff --git a/public/uploads/xcximg/20241016/c1e2f7c14eaee79c62dc882c068aba62.png b/public/uploads/xcximg/20241016/c1e2f7c14eaee79c62dc882c068aba62.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241016/c1e2f7c14eaee79c62dc882c068aba62.png differ diff --git a/public/uploads/xcximg/20241016/c431d496945b4aa931a5923a1aecfc7b.jpeg b/public/uploads/xcximg/20241016/c431d496945b4aa931a5923a1aecfc7b.jpeg new file mode 100644 index 0000000..407f093 Binary files /dev/null and b/public/uploads/xcximg/20241016/c431d496945b4aa931a5923a1aecfc7b.jpeg differ diff --git a/public/uploads/xcximg/20241016/c87e1422933b4636b599ea3b88cefa0d.png b/public/uploads/xcximg/20241016/c87e1422933b4636b599ea3b88cefa0d.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241016/c87e1422933b4636b599ea3b88cefa0d.png differ diff --git a/public/uploads/xcximg/20241016/cb9848ae84602f593ce2d75c0d8da689.jpg b/public/uploads/xcximg/20241016/cb9848ae84602f593ce2d75c0d8da689.jpg new file mode 100644 index 0000000..a34165b Binary files /dev/null and b/public/uploads/xcximg/20241016/cb9848ae84602f593ce2d75c0d8da689.jpg differ diff --git a/public/uploads/xcximg/20241016/cc759cc36751304e2e086c9a1b478cda.png b/public/uploads/xcximg/20241016/cc759cc36751304e2e086c9a1b478cda.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/cc759cc36751304e2e086c9a1b478cda.png differ diff --git a/public/uploads/xcximg/20241016/d47147b2e1ef332d88a88b9ad6484790.jpeg b/public/uploads/xcximg/20241016/d47147b2e1ef332d88a88b9ad6484790.jpeg new file mode 100644 index 0000000..6cb8604 Binary files /dev/null and b/public/uploads/xcximg/20241016/d47147b2e1ef332d88a88b9ad6484790.jpeg differ diff --git a/public/uploads/xcximg/20241016/d54120e9a546ce20b92b935a1e03deac.jpeg b/public/uploads/xcximg/20241016/d54120e9a546ce20b92b935a1e03deac.jpeg new file mode 100644 index 0000000..fd2e900 Binary files /dev/null and b/public/uploads/xcximg/20241016/d54120e9a546ce20b92b935a1e03deac.jpeg differ diff --git a/public/uploads/xcximg/20241016/e5a649e25869f10f52f5af8ddad0a9e8.jpeg b/public/uploads/xcximg/20241016/e5a649e25869f10f52f5af8ddad0a9e8.jpeg new file mode 100644 index 0000000..ffe85df Binary files /dev/null and b/public/uploads/xcximg/20241016/e5a649e25869f10f52f5af8ddad0a9e8.jpeg differ diff --git a/public/uploads/xcximg/20241016/e5b8db0202439061981afd9223271410.png b/public/uploads/xcximg/20241016/e5b8db0202439061981afd9223271410.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241016/e5b8db0202439061981afd9223271410.png differ diff --git a/public/uploads/xcximg/20241016/f0349e05470d90a75de201617b8d2044.png b/public/uploads/xcximg/20241016/f0349e05470d90a75de201617b8d2044.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/f0349e05470d90a75de201617b8d2044.png differ diff --git a/public/uploads/xcximg/20241016/f33ac745c39296afecbd1d6a8f86d4c2.png b/public/uploads/xcximg/20241016/f33ac745c39296afecbd1d6a8f86d4c2.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241016/f33ac745c39296afecbd1d6a8f86d4c2.png differ diff --git a/public/uploads/xcximg/20241016/f58d6209aa91de024113e4fc73f3b284.png b/public/uploads/xcximg/20241016/f58d6209aa91de024113e4fc73f3b284.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/f58d6209aa91de024113e4fc73f3b284.png differ diff --git a/public/uploads/xcximg/20241016/f79972d1fc6274887c56d1a49f966455.png b/public/uploads/xcximg/20241016/f79972d1fc6274887c56d1a49f966455.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241016/f79972d1fc6274887c56d1a49f966455.png differ diff --git a/public/uploads/xcximg/20241016/f84c8a1a12bb15243df4f920a2d02e3c.jpeg b/public/uploads/xcximg/20241016/f84c8a1a12bb15243df4f920a2d02e3c.jpeg new file mode 100644 index 0000000..f907d4d Binary files /dev/null and b/public/uploads/xcximg/20241016/f84c8a1a12bb15243df4f920a2d02e3c.jpeg differ diff --git a/public/uploads/xcximg/20241016/ffc7a0c792dc6a323ce9ac51fc075dd0.png b/public/uploads/xcximg/20241016/ffc7a0c792dc6a323ce9ac51fc075dd0.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241016/ffc7a0c792dc6a323ce9ac51fc075dd0.png differ diff --git a/public/uploads/xcximg/20241017/02be95478faafabdc2a6e724405f5ff6.png b/public/uploads/xcximg/20241017/02be95478faafabdc2a6e724405f5ff6.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241017/02be95478faafabdc2a6e724405f5ff6.png differ diff --git a/public/uploads/xcximg/20241017/04f753f2fba7eae1b1882676eda61b18.png b/public/uploads/xcximg/20241017/04f753f2fba7eae1b1882676eda61b18.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241017/04f753f2fba7eae1b1882676eda61b18.png differ diff --git a/public/uploads/xcximg/20241017/0ac5052fbc1d567b3d30e2dc934d5e5c.png b/public/uploads/xcximg/20241017/0ac5052fbc1d567b3d30e2dc934d5e5c.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241017/0ac5052fbc1d567b3d30e2dc934d5e5c.png differ diff --git a/public/uploads/xcximg/20241017/15a54b543536bd25d4f13ccd506a90a9.jpeg b/public/uploads/xcximg/20241017/15a54b543536bd25d4f13ccd506a90a9.jpeg new file mode 100644 index 0000000..93cbb9e Binary files /dev/null and b/public/uploads/xcximg/20241017/15a54b543536bd25d4f13ccd506a90a9.jpeg differ diff --git a/public/uploads/xcximg/20241017/1aa7126bac87be08d890632e6b677556.png b/public/uploads/xcximg/20241017/1aa7126bac87be08d890632e6b677556.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241017/1aa7126bac87be08d890632e6b677556.png differ diff --git a/public/uploads/xcximg/20241017/256972ddbe331169bef319c2d3f9ca23.png b/public/uploads/xcximg/20241017/256972ddbe331169bef319c2d3f9ca23.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241017/256972ddbe331169bef319c2d3f9ca23.png differ diff --git a/public/uploads/xcximg/20241017/3863beffab76924acb79aeb7bd8474d5.jpeg b/public/uploads/xcximg/20241017/3863beffab76924acb79aeb7bd8474d5.jpeg new file mode 100644 index 0000000..b191296 Binary files /dev/null and b/public/uploads/xcximg/20241017/3863beffab76924acb79aeb7bd8474d5.jpeg differ diff --git a/public/uploads/xcximg/20241017/393ef38ffac5c0ab9891dced90fe7ad2.png b/public/uploads/xcximg/20241017/393ef38ffac5c0ab9891dced90fe7ad2.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241017/393ef38ffac5c0ab9891dced90fe7ad2.png differ diff --git a/public/uploads/xcximg/20241017/465b28300b92c4637d205d923a5478d8.png b/public/uploads/xcximg/20241017/465b28300b92c4637d205d923a5478d8.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241017/465b28300b92c4637d205d923a5478d8.png differ diff --git a/public/uploads/xcximg/20241017/480a84fb6f2c6b1425deccf273035f2b.png b/public/uploads/xcximg/20241017/480a84fb6f2c6b1425deccf273035f2b.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241017/480a84fb6f2c6b1425deccf273035f2b.png differ diff --git a/public/uploads/xcximg/20241017/510efb7ea48ab7f90ad44250a191569e.png b/public/uploads/xcximg/20241017/510efb7ea48ab7f90ad44250a191569e.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241017/510efb7ea48ab7f90ad44250a191569e.png differ diff --git a/public/uploads/xcximg/20241017/5154d655029971d8f0961cc95a9e074e.png b/public/uploads/xcximg/20241017/5154d655029971d8f0961cc95a9e074e.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241017/5154d655029971d8f0961cc95a9e074e.png differ diff --git a/public/uploads/xcximg/20241017/64cd3070fc0aa2fd027f79887c2467ab.jpeg b/public/uploads/xcximg/20241017/64cd3070fc0aa2fd027f79887c2467ab.jpeg new file mode 100644 index 0000000..b5c4143 Binary files /dev/null and b/public/uploads/xcximg/20241017/64cd3070fc0aa2fd027f79887c2467ab.jpeg differ diff --git a/public/uploads/xcximg/20241017/70c40afecdc0545f0a636ad7f307def2.png b/public/uploads/xcximg/20241017/70c40afecdc0545f0a636ad7f307def2.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241017/70c40afecdc0545f0a636ad7f307def2.png differ diff --git a/public/uploads/xcximg/20241017/756103d3d426330dfe3746216c627496.png b/public/uploads/xcximg/20241017/756103d3d426330dfe3746216c627496.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241017/756103d3d426330dfe3746216c627496.png differ diff --git a/public/uploads/xcximg/20241017/7784636260e7e28801bdfae73a046cba.png b/public/uploads/xcximg/20241017/7784636260e7e28801bdfae73a046cba.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241017/7784636260e7e28801bdfae73a046cba.png differ diff --git a/public/uploads/xcximg/20241017/8e38ce8ca9a5a57fae3bccea0398d91d.png b/public/uploads/xcximg/20241017/8e38ce8ca9a5a57fae3bccea0398d91d.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241017/8e38ce8ca9a5a57fae3bccea0398d91d.png differ diff --git a/public/uploads/xcximg/20241017/9519acb3a2e96a959f1801060fb01d39.jpeg b/public/uploads/xcximg/20241017/9519acb3a2e96a959f1801060fb01d39.jpeg new file mode 100644 index 0000000..42bfd87 Binary files /dev/null and b/public/uploads/xcximg/20241017/9519acb3a2e96a959f1801060fb01d39.jpeg differ diff --git a/public/uploads/xcximg/20241017/a1d23beb81609acaa48e1b3b8192b7a5.png b/public/uploads/xcximg/20241017/a1d23beb81609acaa48e1b3b8192b7a5.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241017/a1d23beb81609acaa48e1b3b8192b7a5.png differ diff --git a/public/uploads/xcximg/20241017/a4cee8cfff96722790a93ee9e818f214.png b/public/uploads/xcximg/20241017/a4cee8cfff96722790a93ee9e818f214.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241017/a4cee8cfff96722790a93ee9e818f214.png differ diff --git a/public/uploads/xcximg/20241017/ba930ec76ea71f6976c92fac5495b15d.png b/public/uploads/xcximg/20241017/ba930ec76ea71f6976c92fac5495b15d.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241017/ba930ec76ea71f6976c92fac5495b15d.png differ diff --git a/public/uploads/xcximg/20241017/bb8339fbf032bd6d7c6ef445f218cbc8.png b/public/uploads/xcximg/20241017/bb8339fbf032bd6d7c6ef445f218cbc8.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241017/bb8339fbf032bd6d7c6ef445f218cbc8.png differ diff --git a/public/uploads/xcximg/20241017/bd994e9a9fcfab6ece4a5f0b5a138a05.png b/public/uploads/xcximg/20241017/bd994e9a9fcfab6ece4a5f0b5a138a05.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241017/bd994e9a9fcfab6ece4a5f0b5a138a05.png differ diff --git a/public/uploads/xcximg/20241017/c302c9e354ab061ac6afe7c5622c332c.png b/public/uploads/xcximg/20241017/c302c9e354ab061ac6afe7c5622c332c.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241017/c302c9e354ab061ac6afe7c5622c332c.png differ diff --git a/public/uploads/xcximg/20241017/ce39dc7735b7bb5e62dbf2eb066649fe.jpeg b/public/uploads/xcximg/20241017/ce39dc7735b7bb5e62dbf2eb066649fe.jpeg new file mode 100644 index 0000000..0f693ea Binary files /dev/null and b/public/uploads/xcximg/20241017/ce39dc7735b7bb5e62dbf2eb066649fe.jpeg differ diff --git a/public/uploads/xcximg/20241017/d0c5944c0bde80b84adb0ea0413a9af0.png b/public/uploads/xcximg/20241017/d0c5944c0bde80b84adb0ea0413a9af0.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241017/d0c5944c0bde80b84adb0ea0413a9af0.png differ diff --git a/public/uploads/xcximg/20241017/ea7f9ecbc26320090823ed20fca93b1f.jpeg b/public/uploads/xcximg/20241017/ea7f9ecbc26320090823ed20fca93b1f.jpeg new file mode 100644 index 0000000..82f92b2 Binary files /dev/null and b/public/uploads/xcximg/20241017/ea7f9ecbc26320090823ed20fca93b1f.jpeg differ diff --git a/public/uploads/xcximg/20241017/f8d93ab2f56827180a17263e60427917.png b/public/uploads/xcximg/20241017/f8d93ab2f56827180a17263e60427917.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241017/f8d93ab2f56827180a17263e60427917.png differ diff --git a/public/uploads/xcximg/20241017/fb087c1af8269b21f7d3bbc06f9ebb34.png b/public/uploads/xcximg/20241017/fb087c1af8269b21f7d3bbc06f9ebb34.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241017/fb087c1af8269b21f7d3bbc06f9ebb34.png differ diff --git a/public/uploads/xcximg/20241017/fb09b8a3a5afd48abc690bf64c9a9515.png b/public/uploads/xcximg/20241017/fb09b8a3a5afd48abc690bf64c9a9515.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241017/fb09b8a3a5afd48abc690bf64c9a9515.png differ diff --git a/public/uploads/xcximg/20241017/fe1a145a83bb04a2b90810d9502a8923.png b/public/uploads/xcximg/20241017/fe1a145a83bb04a2b90810d9502a8923.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241017/fe1a145a83bb04a2b90810d9502a8923.png differ diff --git a/public/uploads/xcximg/20241018/00da3d9fd8e50d20ac3703fe07899450.png b/public/uploads/xcximg/20241018/00da3d9fd8e50d20ac3703fe07899450.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/00da3d9fd8e50d20ac3703fe07899450.png differ diff --git a/public/uploads/xcximg/20241018/047db88d19fcb5472821334d6fed2e7e.jpeg b/public/uploads/xcximg/20241018/047db88d19fcb5472821334d6fed2e7e.jpeg new file mode 100644 index 0000000..351ff57 Binary files /dev/null and b/public/uploads/xcximg/20241018/047db88d19fcb5472821334d6fed2e7e.jpeg differ diff --git a/public/uploads/xcximg/20241018/057cd7bfdfa118705bdba5a4e2c50e4e.png b/public/uploads/xcximg/20241018/057cd7bfdfa118705bdba5a4e2c50e4e.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241018/057cd7bfdfa118705bdba5a4e2c50e4e.png differ diff --git a/public/uploads/xcximg/20241018/05954b28b3412cebaa90987382de85be.png b/public/uploads/xcximg/20241018/05954b28b3412cebaa90987382de85be.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241018/05954b28b3412cebaa90987382de85be.png differ diff --git a/public/uploads/xcximg/20241018/05d1d038d208e40854998e13ae4fdce9.png b/public/uploads/xcximg/20241018/05d1d038d208e40854998e13ae4fdce9.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241018/05d1d038d208e40854998e13ae4fdce9.png differ diff --git a/public/uploads/xcximg/20241018/0a05f70237084335b7c7169366dc7b52.png b/public/uploads/xcximg/20241018/0a05f70237084335b7c7169366dc7b52.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241018/0a05f70237084335b7c7169366dc7b52.png differ diff --git a/public/uploads/xcximg/20241018/0bbfd346fde0e3aa69d03dee3a943f5c.png b/public/uploads/xcximg/20241018/0bbfd346fde0e3aa69d03dee3a943f5c.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/0bbfd346fde0e3aa69d03dee3a943f5c.png differ diff --git a/public/uploads/xcximg/20241018/0c6070377af9948436371171bdcfd5cb.png b/public/uploads/xcximg/20241018/0c6070377af9948436371171bdcfd5cb.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/0c6070377af9948436371171bdcfd5cb.png differ diff --git a/public/uploads/xcximg/20241018/10ea26ddce71ee21f45160086922dcfe.png b/public/uploads/xcximg/20241018/10ea26ddce71ee21f45160086922dcfe.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241018/10ea26ddce71ee21f45160086922dcfe.png differ diff --git a/public/uploads/xcximg/20241018/14389c900d9608a3f0d8653e2cddd35f.png b/public/uploads/xcximg/20241018/14389c900d9608a3f0d8653e2cddd35f.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241018/14389c900d9608a3f0d8653e2cddd35f.png differ diff --git a/public/uploads/xcximg/20241018/148b32d37c17b8aa70339f134ec8a553.png b/public/uploads/xcximg/20241018/148b32d37c17b8aa70339f134ec8a553.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/148b32d37c17b8aa70339f134ec8a553.png differ diff --git a/public/uploads/xcximg/20241018/1a0d5019da11d98dd56513728bf4d50c.jpg b/public/uploads/xcximg/20241018/1a0d5019da11d98dd56513728bf4d50c.jpg new file mode 100644 index 0000000..a597d3e Binary files /dev/null and b/public/uploads/xcximg/20241018/1a0d5019da11d98dd56513728bf4d50c.jpg differ diff --git a/public/uploads/xcximg/20241018/281f9be64f3529f88ed8f7c388b3f29a.png b/public/uploads/xcximg/20241018/281f9be64f3529f88ed8f7c388b3f29a.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241018/281f9be64f3529f88ed8f7c388b3f29a.png differ diff --git a/public/uploads/xcximg/20241018/320dc8bead057868468b875fa2f2900c.jpeg b/public/uploads/xcximg/20241018/320dc8bead057868468b875fa2f2900c.jpeg new file mode 100644 index 0000000..7826e24 Binary files /dev/null and b/public/uploads/xcximg/20241018/320dc8bead057868468b875fa2f2900c.jpeg differ diff --git a/public/uploads/xcximg/20241018/334f54e045a188df08be26be4263bcbe.png b/public/uploads/xcximg/20241018/334f54e045a188df08be26be4263bcbe.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241018/334f54e045a188df08be26be4263bcbe.png differ diff --git a/public/uploads/xcximg/20241018/351117ebac040dfa7c92ed53e7fde245.png b/public/uploads/xcximg/20241018/351117ebac040dfa7c92ed53e7fde245.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241018/351117ebac040dfa7c92ed53e7fde245.png differ diff --git a/public/uploads/xcximg/20241018/378280e1d17feefe63dd20627676f527.jpeg b/public/uploads/xcximg/20241018/378280e1d17feefe63dd20627676f527.jpeg new file mode 100644 index 0000000..07c6e1c Binary files /dev/null and b/public/uploads/xcximg/20241018/378280e1d17feefe63dd20627676f527.jpeg differ diff --git a/public/uploads/xcximg/20241018/3a2eb6af1304077f7cbdfa17edf8f811.png b/public/uploads/xcximg/20241018/3a2eb6af1304077f7cbdfa17edf8f811.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241018/3a2eb6af1304077f7cbdfa17edf8f811.png differ diff --git a/public/uploads/xcximg/20241018/44f30e51b2fda7b4adc77c2bf642d7f2.png b/public/uploads/xcximg/20241018/44f30e51b2fda7b4adc77c2bf642d7f2.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/44f30e51b2fda7b4adc77c2bf642d7f2.png differ diff --git a/public/uploads/xcximg/20241018/47f0ea7f20dbf27f01c0693ea79e0476.jpeg b/public/uploads/xcximg/20241018/47f0ea7f20dbf27f01c0693ea79e0476.jpeg new file mode 100644 index 0000000..0d7d048 Binary files /dev/null and b/public/uploads/xcximg/20241018/47f0ea7f20dbf27f01c0693ea79e0476.jpeg differ diff --git a/public/uploads/xcximg/20241018/4f76cb9a302abed1c4eb7a4092667fd3.png b/public/uploads/xcximg/20241018/4f76cb9a302abed1c4eb7a4092667fd3.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241018/4f76cb9a302abed1c4eb7a4092667fd3.png differ diff --git a/public/uploads/xcximg/20241018/50a6f9674e38579f9b4bb6a66069511d.jpeg b/public/uploads/xcximg/20241018/50a6f9674e38579f9b4bb6a66069511d.jpeg new file mode 100644 index 0000000..ec78a3e Binary files /dev/null and b/public/uploads/xcximg/20241018/50a6f9674e38579f9b4bb6a66069511d.jpeg differ diff --git a/public/uploads/xcximg/20241018/52ba53c2a553745bf5667bd0bdec143f.png b/public/uploads/xcximg/20241018/52ba53c2a553745bf5667bd0bdec143f.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/52ba53c2a553745bf5667bd0bdec143f.png differ diff --git a/public/uploads/xcximg/20241018/54d8f4340e23de0e7253793dd270694a.png b/public/uploads/xcximg/20241018/54d8f4340e23de0e7253793dd270694a.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/54d8f4340e23de0e7253793dd270694a.png differ diff --git a/public/uploads/xcximg/20241018/5846c2eae1b243d1ccff556e38db0857.png b/public/uploads/xcximg/20241018/5846c2eae1b243d1ccff556e38db0857.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241018/5846c2eae1b243d1ccff556e38db0857.png differ diff --git a/public/uploads/xcximg/20241018/58b9effea89147b0882e7a5ac3f2df5f.jpeg b/public/uploads/xcximg/20241018/58b9effea89147b0882e7a5ac3f2df5f.jpeg new file mode 100644 index 0000000..637c2d4 Binary files /dev/null and b/public/uploads/xcximg/20241018/58b9effea89147b0882e7a5ac3f2df5f.jpeg differ diff --git a/public/uploads/xcximg/20241018/625c06f2ad982e83d581df50dc29ed64.png b/public/uploads/xcximg/20241018/625c06f2ad982e83d581df50dc29ed64.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241018/625c06f2ad982e83d581df50dc29ed64.png differ diff --git a/public/uploads/xcximg/20241018/63f7b458d57eb98db992ae14fe276cd6.jpeg b/public/uploads/xcximg/20241018/63f7b458d57eb98db992ae14fe276cd6.jpeg new file mode 100644 index 0000000..ec9ebf9 Binary files /dev/null and b/public/uploads/xcximg/20241018/63f7b458d57eb98db992ae14fe276cd6.jpeg differ diff --git a/public/uploads/xcximg/20241018/6a5b23c041b15ae03e6941f290a23644.png b/public/uploads/xcximg/20241018/6a5b23c041b15ae03e6941f290a23644.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/6a5b23c041b15ae03e6941f290a23644.png differ diff --git a/public/uploads/xcximg/20241018/6f081c17d3821bfa8ef4e1adf01ea5de.jpeg b/public/uploads/xcximg/20241018/6f081c17d3821bfa8ef4e1adf01ea5de.jpeg new file mode 100644 index 0000000..4dc6662 Binary files /dev/null and b/public/uploads/xcximg/20241018/6f081c17d3821bfa8ef4e1adf01ea5de.jpeg differ diff --git a/public/uploads/xcximg/20241018/71635394575cd9919a7ab48e4001ad7b.jpeg b/public/uploads/xcximg/20241018/71635394575cd9919a7ab48e4001ad7b.jpeg new file mode 100644 index 0000000..7694c5a Binary files /dev/null and b/public/uploads/xcximg/20241018/71635394575cd9919a7ab48e4001ad7b.jpeg differ diff --git a/public/uploads/xcximg/20241018/7343a24817b6f4e3b9a7499011cf816a.jpeg b/public/uploads/xcximg/20241018/7343a24817b6f4e3b9a7499011cf816a.jpeg new file mode 100644 index 0000000..9ed181b Binary files /dev/null and b/public/uploads/xcximg/20241018/7343a24817b6f4e3b9a7499011cf816a.jpeg differ diff --git a/public/uploads/xcximg/20241018/764e4d6571b9164a3f9e3175c40bb062.jpeg b/public/uploads/xcximg/20241018/764e4d6571b9164a3f9e3175c40bb062.jpeg new file mode 100644 index 0000000..7e2662f Binary files /dev/null and b/public/uploads/xcximg/20241018/764e4d6571b9164a3f9e3175c40bb062.jpeg differ diff --git a/public/uploads/xcximg/20241018/77e3dc0157d86a95f66c47939652ea97.png b/public/uploads/xcximg/20241018/77e3dc0157d86a95f66c47939652ea97.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/77e3dc0157d86a95f66c47939652ea97.png differ diff --git a/public/uploads/xcximg/20241018/7a8c6d7c1ec9ba0d38e282404805e4e1.png b/public/uploads/xcximg/20241018/7a8c6d7c1ec9ba0d38e282404805e4e1.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/7a8c6d7c1ec9ba0d38e282404805e4e1.png differ diff --git a/public/uploads/xcximg/20241018/7b7a712aceeeb2877b7b978cc9183667.png b/public/uploads/xcximg/20241018/7b7a712aceeeb2877b7b978cc9183667.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241018/7b7a712aceeeb2877b7b978cc9183667.png differ diff --git a/public/uploads/xcximg/20241018/7ef7c6abc08894fa0e3220f03fcdf9f8.png b/public/uploads/xcximg/20241018/7ef7c6abc08894fa0e3220f03fcdf9f8.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241018/7ef7c6abc08894fa0e3220f03fcdf9f8.png differ diff --git a/public/uploads/xcximg/20241018/83c4f88c222a82973f78c399347dee3c.png b/public/uploads/xcximg/20241018/83c4f88c222a82973f78c399347dee3c.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241018/83c4f88c222a82973f78c399347dee3c.png differ diff --git a/public/uploads/xcximg/20241018/84418db48557f48c1c580fc99ed5b349.png b/public/uploads/xcximg/20241018/84418db48557f48c1c580fc99ed5b349.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/84418db48557f48c1c580fc99ed5b349.png differ diff --git a/public/uploads/xcximg/20241018/8d9ff35ef932e0322483a5022f61c749.png b/public/uploads/xcximg/20241018/8d9ff35ef932e0322483a5022f61c749.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241018/8d9ff35ef932e0322483a5022f61c749.png differ diff --git a/public/uploads/xcximg/20241018/9112e4cb6a27c1de3e089d6709d3a6a1.png b/public/uploads/xcximg/20241018/9112e4cb6a27c1de3e089d6709d3a6a1.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/9112e4cb6a27c1de3e089d6709d3a6a1.png differ diff --git a/public/uploads/xcximg/20241018/9ea6fde6e60079a3a4d9fe80d2a7bfe4.png b/public/uploads/xcximg/20241018/9ea6fde6e60079a3a4d9fe80d2a7bfe4.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241018/9ea6fde6e60079a3a4d9fe80d2a7bfe4.png differ diff --git a/public/uploads/xcximg/20241018/ae1a9f5dfe741a089e157ebaa617de1a.png b/public/uploads/xcximg/20241018/ae1a9f5dfe741a089e157ebaa617de1a.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241018/ae1a9f5dfe741a089e157ebaa617de1a.png differ diff --git a/public/uploads/xcximg/20241018/af6a4e937d6d808765ffe52859a515b0.jpeg b/public/uploads/xcximg/20241018/af6a4e937d6d808765ffe52859a515b0.jpeg new file mode 100644 index 0000000..8d1d800 Binary files /dev/null and b/public/uploads/xcximg/20241018/af6a4e937d6d808765ffe52859a515b0.jpeg differ diff --git a/public/uploads/xcximg/20241018/b3120d449a5294c28f757f811579497d.png b/public/uploads/xcximg/20241018/b3120d449a5294c28f757f811579497d.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/b3120d449a5294c28f757f811579497d.png differ diff --git a/public/uploads/xcximg/20241018/b8c7f443e93d57d65e8ded5f6c6e08a1.jpeg b/public/uploads/xcximg/20241018/b8c7f443e93d57d65e8ded5f6c6e08a1.jpeg new file mode 100644 index 0000000..c940e8c Binary files /dev/null and b/public/uploads/xcximg/20241018/b8c7f443e93d57d65e8ded5f6c6e08a1.jpeg differ diff --git a/public/uploads/xcximg/20241018/bd371775f70954eae20d5d8971cc110c.png b/public/uploads/xcximg/20241018/bd371775f70954eae20d5d8971cc110c.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/bd371775f70954eae20d5d8971cc110c.png differ diff --git a/public/uploads/xcximg/20241018/be20548b02b8e03e297fd15f4024596a.png b/public/uploads/xcximg/20241018/be20548b02b8e03e297fd15f4024596a.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241018/be20548b02b8e03e297fd15f4024596a.png differ diff --git a/public/uploads/xcximg/20241018/bf2450dd9235090a30ff219d5d434b37.jpg b/public/uploads/xcximg/20241018/bf2450dd9235090a30ff219d5d434b37.jpg new file mode 100644 index 0000000..d349b41 Binary files /dev/null and b/public/uploads/xcximg/20241018/bf2450dd9235090a30ff219d5d434b37.jpg differ diff --git a/public/uploads/xcximg/20241018/c03c117a4f7f0dce3feda5674931d828.png b/public/uploads/xcximg/20241018/c03c117a4f7f0dce3feda5674931d828.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241018/c03c117a4f7f0dce3feda5674931d828.png differ diff --git a/public/uploads/xcximg/20241018/c638ce19640ea5e9e98d0bf74f8220f5.png b/public/uploads/xcximg/20241018/c638ce19640ea5e9e98d0bf74f8220f5.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/c638ce19640ea5e9e98d0bf74f8220f5.png differ diff --git a/public/uploads/xcximg/20241018/c6bbd38ae5269081876dfb82a35d793a.png b/public/uploads/xcximg/20241018/c6bbd38ae5269081876dfb82a35d793a.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/c6bbd38ae5269081876dfb82a35d793a.png differ diff --git a/public/uploads/xcximg/20241018/c781d5f18d2968d50645bdb8c4acfd19.png b/public/uploads/xcximg/20241018/c781d5f18d2968d50645bdb8c4acfd19.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241018/c781d5f18d2968d50645bdb8c4acfd19.png differ diff --git a/public/uploads/xcximg/20241018/cc70b4484b06c490a6946036aa9fff27.png b/public/uploads/xcximg/20241018/cc70b4484b06c490a6946036aa9fff27.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241018/cc70b4484b06c490a6946036aa9fff27.png differ diff --git a/public/uploads/xcximg/20241018/ccb46f84aa4837be4daec739b59b8fe8.jpeg b/public/uploads/xcximg/20241018/ccb46f84aa4837be4daec739b59b8fe8.jpeg new file mode 100644 index 0000000..b75cac4 Binary files /dev/null and b/public/uploads/xcximg/20241018/ccb46f84aa4837be4daec739b59b8fe8.jpeg differ diff --git a/public/uploads/xcximg/20241018/d5fe8de88facfab3a549884ad0373f77.png b/public/uploads/xcximg/20241018/d5fe8de88facfab3a549884ad0373f77.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241018/d5fe8de88facfab3a549884ad0373f77.png differ diff --git a/public/uploads/xcximg/20241018/d75fd387f134d45f032b04d8d8097522.jpeg b/public/uploads/xcximg/20241018/d75fd387f134d45f032b04d8d8097522.jpeg new file mode 100644 index 0000000..ef75720 Binary files /dev/null and b/public/uploads/xcximg/20241018/d75fd387f134d45f032b04d8d8097522.jpeg differ diff --git a/public/uploads/xcximg/20241018/d8b8e429ef655c831d76dd99195aac39.png b/public/uploads/xcximg/20241018/d8b8e429ef655c831d76dd99195aac39.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241018/d8b8e429ef655c831d76dd99195aac39.png differ diff --git a/public/uploads/xcximg/20241018/e3b68635cf82ef5de4497367103a703c.png b/public/uploads/xcximg/20241018/e3b68635cf82ef5de4497367103a703c.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241018/e3b68635cf82ef5de4497367103a703c.png differ diff --git a/public/uploads/xcximg/20241018/e6bcca720b1c8b61127c5dd4f7042b51.png b/public/uploads/xcximg/20241018/e6bcca720b1c8b61127c5dd4f7042b51.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241018/e6bcca720b1c8b61127c5dd4f7042b51.png differ diff --git a/public/uploads/xcximg/20241018/e7388815c601c06a5b00f1145e514e45.png b/public/uploads/xcximg/20241018/e7388815c601c06a5b00f1145e514e45.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/e7388815c601c06a5b00f1145e514e45.png differ diff --git a/public/uploads/xcximg/20241018/e8640ac39d5c8c12935c23138c4e16c3.png b/public/uploads/xcximg/20241018/e8640ac39d5c8c12935c23138c4e16c3.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241018/e8640ac39d5c8c12935c23138c4e16c3.png differ diff --git a/public/uploads/xcximg/20241018/ef102fa5f6f12b92b1cbe5feacbe6d71.png b/public/uploads/xcximg/20241018/ef102fa5f6f12b92b1cbe5feacbe6d71.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241018/ef102fa5f6f12b92b1cbe5feacbe6d71.png differ diff --git a/public/uploads/xcximg/20241018/ef24cd4df404dc2129b9a32a8a53dc8e.png b/public/uploads/xcximg/20241018/ef24cd4df404dc2129b9a32a8a53dc8e.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241018/ef24cd4df404dc2129b9a32a8a53dc8e.png differ diff --git a/public/uploads/xcximg/20241018/f03bcaf40d419cc66e5edc09c42d7b9d.jpeg b/public/uploads/xcximg/20241018/f03bcaf40d419cc66e5edc09c42d7b9d.jpeg new file mode 100644 index 0000000..3d6972a Binary files /dev/null and b/public/uploads/xcximg/20241018/f03bcaf40d419cc66e5edc09c42d7b9d.jpeg differ diff --git a/public/uploads/xcximg/20241018/f4bb62e42fc2c7be5413ce9d23a5560d.png b/public/uploads/xcximg/20241018/f4bb62e42fc2c7be5413ce9d23a5560d.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241018/f4bb62e42fc2c7be5413ce9d23a5560d.png differ diff --git a/public/uploads/xcximg/20241018/f79ee5e133fef11e68379b69b68a7e67.jpeg b/public/uploads/xcximg/20241018/f79ee5e133fef11e68379b69b68a7e67.jpeg new file mode 100644 index 0000000..a6ea17b Binary files /dev/null and b/public/uploads/xcximg/20241018/f79ee5e133fef11e68379b69b68a7e67.jpeg differ diff --git a/public/uploads/xcximg/20241019/06571c2741ba8b5ad10c573abfe7de8d.jpeg b/public/uploads/xcximg/20241019/06571c2741ba8b5ad10c573abfe7de8d.jpeg new file mode 100644 index 0000000..dbd60ed Binary files /dev/null and b/public/uploads/xcximg/20241019/06571c2741ba8b5ad10c573abfe7de8d.jpeg differ diff --git a/public/uploads/xcximg/20241019/26bf830102ecb6c889268b8e5cbd0160.png b/public/uploads/xcximg/20241019/26bf830102ecb6c889268b8e5cbd0160.png new file mode 100644 index 0000000..1f86387 Binary files /dev/null and b/public/uploads/xcximg/20241019/26bf830102ecb6c889268b8e5cbd0160.png differ diff --git a/public/uploads/xcximg/20241019/36778411d8a89a5883a8f00284de02da.png b/public/uploads/xcximg/20241019/36778411d8a89a5883a8f00284de02da.png new file mode 100644 index 0000000..1f01094 Binary files /dev/null and b/public/uploads/xcximg/20241019/36778411d8a89a5883a8f00284de02da.png differ diff --git a/public/uploads/xcximg/20241019/3702d0fc17da45fe8800574a390bbadb.png b/public/uploads/xcximg/20241019/3702d0fc17da45fe8800574a390bbadb.png new file mode 100644 index 0000000..64e39ff Binary files /dev/null and b/public/uploads/xcximg/20241019/3702d0fc17da45fe8800574a390bbadb.png differ diff --git a/public/uploads/xcximg/20241019/61b9dc5a465b7f2bf520f8b158ffb84d.png b/public/uploads/xcximg/20241019/61b9dc5a465b7f2bf520f8b158ffb84d.png new file mode 100644 index 0000000..14ed47d Binary files /dev/null and b/public/uploads/xcximg/20241019/61b9dc5a465b7f2bf520f8b158ffb84d.png differ diff --git a/public/uploads/xcximg/20241019/643dc3528ad9b178bf389eba3a628be6.png b/public/uploads/xcximg/20241019/643dc3528ad9b178bf389eba3a628be6.png new file mode 100644 index 0000000..06e389f Binary files /dev/null and b/public/uploads/xcximg/20241019/643dc3528ad9b178bf389eba3a628be6.png differ diff --git a/public/uploads/xcximg/20241019/7352efbd07cb68b352fc8ef92b9f6f7d.png b/public/uploads/xcximg/20241019/7352efbd07cb68b352fc8ef92b9f6f7d.png new file mode 100644 index 0000000..14ed47d Binary files /dev/null and b/public/uploads/xcximg/20241019/7352efbd07cb68b352fc8ef92b9f6f7d.png differ diff --git a/public/uploads/xcximg/20241019/74a009eb6f62479004006e7625abace9.jpeg b/public/uploads/xcximg/20241019/74a009eb6f62479004006e7625abace9.jpeg new file mode 100644 index 0000000..b983c41 Binary files /dev/null and b/public/uploads/xcximg/20241019/74a009eb6f62479004006e7625abace9.jpeg differ diff --git a/public/uploads/xcximg/20241019/75dc0650ca6031ba58360ceba5b23cb1.png b/public/uploads/xcximg/20241019/75dc0650ca6031ba58360ceba5b23cb1.png new file mode 100644 index 0000000..64e39ff Binary files /dev/null and b/public/uploads/xcximg/20241019/75dc0650ca6031ba58360ceba5b23cb1.png differ diff --git a/public/uploads/xcximg/20241019/88ac88b4ddcdfb4f1d45b7abacf31666.png b/public/uploads/xcximg/20241019/88ac88b4ddcdfb4f1d45b7abacf31666.png new file mode 100644 index 0000000..1f01094 Binary files /dev/null and b/public/uploads/xcximg/20241019/88ac88b4ddcdfb4f1d45b7abacf31666.png differ diff --git a/public/uploads/xcximg/20241019/8ff88db31d5a31a24e3fa239e2855112.jpeg b/public/uploads/xcximg/20241019/8ff88db31d5a31a24e3fa239e2855112.jpeg new file mode 100644 index 0000000..410053f Binary files /dev/null and b/public/uploads/xcximg/20241019/8ff88db31d5a31a24e3fa239e2855112.jpeg differ diff --git a/public/uploads/xcximg/20241019/a3a018d734299add4f99c85bab775158.jpg b/public/uploads/xcximg/20241019/a3a018d734299add4f99c85bab775158.jpg new file mode 100644 index 0000000..1af6d6f Binary files /dev/null and b/public/uploads/xcximg/20241019/a3a018d734299add4f99c85bab775158.jpg differ diff --git a/public/uploads/xcximg/20241019/a6718245ae39a123d9f5d71f84fe32b0.jpg b/public/uploads/xcximg/20241019/a6718245ae39a123d9f5d71f84fe32b0.jpg new file mode 100644 index 0000000..e333286 Binary files /dev/null and b/public/uploads/xcximg/20241019/a6718245ae39a123d9f5d71f84fe32b0.jpg differ diff --git a/public/uploads/xcximg/20241019/c316ca386fe49bd7289897d99912af7f.jpeg b/public/uploads/xcximg/20241019/c316ca386fe49bd7289897d99912af7f.jpeg new file mode 100644 index 0000000..8a42d93 Binary files /dev/null and b/public/uploads/xcximg/20241019/c316ca386fe49bd7289897d99912af7f.jpeg differ diff --git a/public/uploads/xcximg/20241019/eeb1b2204beb1379d6414231c80cc55e.png b/public/uploads/xcximg/20241019/eeb1b2204beb1379d6414231c80cc55e.png new file mode 100644 index 0000000..323a37b Binary files /dev/null and b/public/uploads/xcximg/20241019/eeb1b2204beb1379d6414231c80cc55e.png differ diff --git a/public/uploads/xcximg/20241019/f6c6d1861ac572d1486b0f96a8cdf242.jpeg b/public/uploads/xcximg/20241019/f6c6d1861ac572d1486b0f96a8cdf242.jpeg new file mode 100644 index 0000000..4ef8da1 Binary files /dev/null and b/public/uploads/xcximg/20241019/f6c6d1861ac572d1486b0f96a8cdf242.jpeg differ diff --git a/public/uploads/xcximg/20241020/06072b35244ff72e43fa15dfebc588bb.png b/public/uploads/xcximg/20241020/06072b35244ff72e43fa15dfebc588bb.png new file mode 100644 index 0000000..2cbfe06 Binary files /dev/null and b/public/uploads/xcximg/20241020/06072b35244ff72e43fa15dfebc588bb.png differ diff --git a/public/uploads/xcximg/20241020/0e833e0a1e2e86f0e2319d745038ba56.png b/public/uploads/xcximg/20241020/0e833e0a1e2e86f0e2319d745038ba56.png new file mode 100644 index 0000000..1f01094 Binary files /dev/null and b/public/uploads/xcximg/20241020/0e833e0a1e2e86f0e2319d745038ba56.png differ diff --git a/public/uploads/xcximg/20241020/1164e258c3c2f43d6e4c2081b0b9227e.jpg b/public/uploads/xcximg/20241020/1164e258c3c2f43d6e4c2081b0b9227e.jpg new file mode 100644 index 0000000..79fbbb2 Binary files /dev/null and b/public/uploads/xcximg/20241020/1164e258c3c2f43d6e4c2081b0b9227e.jpg differ diff --git a/public/uploads/xcximg/20241020/129178bfc3cab6ae2bffde4dfb2f9b1d.png b/public/uploads/xcximg/20241020/129178bfc3cab6ae2bffde4dfb2f9b1d.png new file mode 100644 index 0000000..df0d5be Binary files /dev/null and b/public/uploads/xcximg/20241020/129178bfc3cab6ae2bffde4dfb2f9b1d.png differ diff --git a/public/uploads/xcximg/20241020/1488afcc4c1cb48b1696247b84180838.png b/public/uploads/xcximg/20241020/1488afcc4c1cb48b1696247b84180838.png new file mode 100644 index 0000000..06e389f Binary files /dev/null and b/public/uploads/xcximg/20241020/1488afcc4c1cb48b1696247b84180838.png differ diff --git a/public/uploads/xcximg/20241020/15a1573f93ee82efbdad151a6b4e318e.png b/public/uploads/xcximg/20241020/15a1573f93ee82efbdad151a6b4e318e.png new file mode 100644 index 0000000..820b840 Binary files /dev/null and b/public/uploads/xcximg/20241020/15a1573f93ee82efbdad151a6b4e318e.png differ diff --git a/public/uploads/xcximg/20241020/1e9c5871d058be5b0eff1545b80e7595.png b/public/uploads/xcximg/20241020/1e9c5871d058be5b0eff1545b80e7595.png new file mode 100644 index 0000000..06e389f Binary files /dev/null and b/public/uploads/xcximg/20241020/1e9c5871d058be5b0eff1545b80e7595.png differ diff --git a/public/uploads/xcximg/20241020/21843ce33b1083c8215137fbca35bf89.png b/public/uploads/xcximg/20241020/21843ce33b1083c8215137fbca35bf89.png new file mode 100644 index 0000000..26e9dc2 Binary files /dev/null and b/public/uploads/xcximg/20241020/21843ce33b1083c8215137fbca35bf89.png differ diff --git a/public/uploads/xcximg/20241020/2fef4ce0988370845738c93c47449f1e.png b/public/uploads/xcximg/20241020/2fef4ce0988370845738c93c47449f1e.png new file mode 100644 index 0000000..1f01094 Binary files /dev/null and b/public/uploads/xcximg/20241020/2fef4ce0988370845738c93c47449f1e.png differ diff --git a/public/uploads/xcximg/20241020/32074eef4f8ebe45e05f9ba70682c6e2.png b/public/uploads/xcximg/20241020/32074eef4f8ebe45e05f9ba70682c6e2.png new file mode 100644 index 0000000..1f86387 Binary files /dev/null and b/public/uploads/xcximg/20241020/32074eef4f8ebe45e05f9ba70682c6e2.png differ diff --git a/public/uploads/xcximg/20241020/3cc426bdcafe221142dadda4ae3ea6ba.png b/public/uploads/xcximg/20241020/3cc426bdcafe221142dadda4ae3ea6ba.png new file mode 100644 index 0000000..06e389f Binary files /dev/null and b/public/uploads/xcximg/20241020/3cc426bdcafe221142dadda4ae3ea6ba.png differ diff --git a/public/uploads/xcximg/20241020/3f01447046fcd4634e5cc9ed7834b157.png b/public/uploads/xcximg/20241020/3f01447046fcd4634e5cc9ed7834b157.png new file mode 100644 index 0000000..0825c7b Binary files /dev/null and b/public/uploads/xcximg/20241020/3f01447046fcd4634e5cc9ed7834b157.png differ diff --git a/public/uploads/xcximg/20241020/465d6b7fccfea6b3025729bff53daac2.png b/public/uploads/xcximg/20241020/465d6b7fccfea6b3025729bff53daac2.png new file mode 100644 index 0000000..0825c7b Binary files /dev/null and b/public/uploads/xcximg/20241020/465d6b7fccfea6b3025729bff53daac2.png differ diff --git a/public/uploads/xcximg/20241020/5ba10c54fd25bb326157c3610d8c49ab.png b/public/uploads/xcximg/20241020/5ba10c54fd25bb326157c3610d8c49ab.png new file mode 100644 index 0000000..7997cd5 Binary files /dev/null and b/public/uploads/xcximg/20241020/5ba10c54fd25bb326157c3610d8c49ab.png differ diff --git a/public/uploads/xcximg/20241020/5d1dcc306430cb0bd5da099effc9e92b.png b/public/uploads/xcximg/20241020/5d1dcc306430cb0bd5da099effc9e92b.png new file mode 100644 index 0000000..df0d5be Binary files /dev/null and b/public/uploads/xcximg/20241020/5d1dcc306430cb0bd5da099effc9e92b.png differ diff --git a/public/uploads/xcximg/20241020/615abe65ae5ad37378eb175bae2f9f8e.png b/public/uploads/xcximg/20241020/615abe65ae5ad37378eb175bae2f9f8e.png new file mode 100644 index 0000000..df0d5be Binary files /dev/null and b/public/uploads/xcximg/20241020/615abe65ae5ad37378eb175bae2f9f8e.png differ diff --git a/public/uploads/xcximg/20241020/6b6af019d870d748b183dca13e5bb033.png b/public/uploads/xcximg/20241020/6b6af019d870d748b183dca13e5bb033.png new file mode 100644 index 0000000..8b5cd58 Binary files /dev/null and b/public/uploads/xcximg/20241020/6b6af019d870d748b183dca13e5bb033.png differ diff --git a/public/uploads/xcximg/20241020/7730aade3398ea42618f6134e056e43c.png b/public/uploads/xcximg/20241020/7730aade3398ea42618f6134e056e43c.png new file mode 100644 index 0000000..1f86387 Binary files /dev/null and b/public/uploads/xcximg/20241020/7730aade3398ea42618f6134e056e43c.png differ diff --git a/public/uploads/xcximg/20241020/7a754133ad46edd258e1d570295d05c4.png b/public/uploads/xcximg/20241020/7a754133ad46edd258e1d570295d05c4.png new file mode 100644 index 0000000..803ea5a Binary files /dev/null and b/public/uploads/xcximg/20241020/7a754133ad46edd258e1d570295d05c4.png differ diff --git a/public/uploads/xcximg/20241020/7e5826101a4fb65d76b294717a947416.png b/public/uploads/xcximg/20241020/7e5826101a4fb65d76b294717a947416.png new file mode 100644 index 0000000..1f01094 Binary files /dev/null and b/public/uploads/xcximg/20241020/7e5826101a4fb65d76b294717a947416.png differ diff --git a/public/uploads/xcximg/20241020/7ed9c9742cf5cce3f6ec6b910b44812b.png b/public/uploads/xcximg/20241020/7ed9c9742cf5cce3f6ec6b910b44812b.png new file mode 100644 index 0000000..06e389f Binary files /dev/null and b/public/uploads/xcximg/20241020/7ed9c9742cf5cce3f6ec6b910b44812b.png differ diff --git a/public/uploads/xcximg/20241020/805ac8b23310cfae801426d86f71d0ce.jpeg b/public/uploads/xcximg/20241020/805ac8b23310cfae801426d86f71d0ce.jpeg new file mode 100644 index 0000000..e1b9c32 Binary files /dev/null and b/public/uploads/xcximg/20241020/805ac8b23310cfae801426d86f71d0ce.jpeg differ diff --git a/public/uploads/xcximg/20241020/8207211a82cfebdd867d24b32549c2b1.png b/public/uploads/xcximg/20241020/8207211a82cfebdd867d24b32549c2b1.png new file mode 100644 index 0000000..45ce7b9 Binary files /dev/null and b/public/uploads/xcximg/20241020/8207211a82cfebdd867d24b32549c2b1.png differ diff --git a/public/uploads/xcximg/20241020/83cdd3eeb746f504abe76b00af583fdd.png b/public/uploads/xcximg/20241020/83cdd3eeb746f504abe76b00af583fdd.png new file mode 100644 index 0000000..dd03984 Binary files /dev/null and b/public/uploads/xcximg/20241020/83cdd3eeb746f504abe76b00af583fdd.png differ diff --git a/public/uploads/xcximg/20241020/98fe5ad761d265f2db28f8ba98d0ad9a.png b/public/uploads/xcximg/20241020/98fe5ad761d265f2db28f8ba98d0ad9a.png new file mode 100644 index 0000000..803ea5a Binary files /dev/null and b/public/uploads/xcximg/20241020/98fe5ad761d265f2db28f8ba98d0ad9a.png differ diff --git a/public/uploads/xcximg/20241020/9f08ccb0ef6566c31c04d918db326e57.png b/public/uploads/xcximg/20241020/9f08ccb0ef6566c31c04d918db326e57.png new file mode 100644 index 0000000..a636e12 Binary files /dev/null and b/public/uploads/xcximg/20241020/9f08ccb0ef6566c31c04d918db326e57.png differ diff --git a/public/uploads/xcximg/20241020/9fd3f72de4034a5c243ed8a28bec4b55.png b/public/uploads/xcximg/20241020/9fd3f72de4034a5c243ed8a28bec4b55.png new file mode 100644 index 0000000..8663c9b Binary files /dev/null and b/public/uploads/xcximg/20241020/9fd3f72de4034a5c243ed8a28bec4b55.png differ diff --git a/public/uploads/xcximg/20241020/a28a9f99c600f7e514aaeec73faa1877.png b/public/uploads/xcximg/20241020/a28a9f99c600f7e514aaeec73faa1877.png new file mode 100644 index 0000000..cf10cff Binary files /dev/null and b/public/uploads/xcximg/20241020/a28a9f99c600f7e514aaeec73faa1877.png differ diff --git a/public/uploads/xcximg/20241020/a695ec355ed9710993b49263acd44d20.png b/public/uploads/xcximg/20241020/a695ec355ed9710993b49263acd44d20.png new file mode 100644 index 0000000..32c8b64 Binary files /dev/null and b/public/uploads/xcximg/20241020/a695ec355ed9710993b49263acd44d20.png differ diff --git a/public/uploads/xcximg/20241020/b706ff62896446ec3e7a89fc0dc8ede8.jpg b/public/uploads/xcximg/20241020/b706ff62896446ec3e7a89fc0dc8ede8.jpg new file mode 100644 index 0000000..815a780 Binary files /dev/null and b/public/uploads/xcximg/20241020/b706ff62896446ec3e7a89fc0dc8ede8.jpg differ diff --git a/public/uploads/xcximg/20241020/b75183924099c41a786bf95522fc35be.png b/public/uploads/xcximg/20241020/b75183924099c41a786bf95522fc35be.png new file mode 100644 index 0000000..8a87d36 Binary files /dev/null and b/public/uploads/xcximg/20241020/b75183924099c41a786bf95522fc35be.png differ diff --git a/public/uploads/xcximg/20241020/bebf25071610bc490764136e870d6af8.png b/public/uploads/xcximg/20241020/bebf25071610bc490764136e870d6af8.png new file mode 100644 index 0000000..a636e12 Binary files /dev/null and b/public/uploads/xcximg/20241020/bebf25071610bc490764136e870d6af8.png differ diff --git a/public/uploads/xcximg/20241020/c0f80555f8adfa5fb53e9ab9a0176683.png b/public/uploads/xcximg/20241020/c0f80555f8adfa5fb53e9ab9a0176683.png new file mode 100644 index 0000000..b619b64 Binary files /dev/null and b/public/uploads/xcximg/20241020/c0f80555f8adfa5fb53e9ab9a0176683.png differ diff --git a/public/uploads/xcximg/20241020/c6ba023251b67b36c5bd96d0435d7a92.png b/public/uploads/xcximg/20241020/c6ba023251b67b36c5bd96d0435d7a92.png new file mode 100644 index 0000000..e219bb5 Binary files /dev/null and b/public/uploads/xcximg/20241020/c6ba023251b67b36c5bd96d0435d7a92.png differ diff --git a/public/uploads/xcximg/20241020/caaaf4321c74c5fccd2b729e032f6f70.png b/public/uploads/xcximg/20241020/caaaf4321c74c5fccd2b729e032f6f70.png new file mode 100644 index 0000000..06e389f Binary files /dev/null and b/public/uploads/xcximg/20241020/caaaf4321c74c5fccd2b729e032f6f70.png differ diff --git a/public/uploads/xcximg/20241020/cd9dfb2d63ecafc602930db9ab8fa47d.png b/public/uploads/xcximg/20241020/cd9dfb2d63ecafc602930db9ab8fa47d.png new file mode 100644 index 0000000..5107a39 Binary files /dev/null and b/public/uploads/xcximg/20241020/cd9dfb2d63ecafc602930db9ab8fa47d.png differ diff --git a/public/uploads/xcximg/20241020/d29b3cdc442ba6d5658c9fe1b061a671.png b/public/uploads/xcximg/20241020/d29b3cdc442ba6d5658c9fe1b061a671.png new file mode 100644 index 0000000..06e389f Binary files /dev/null and b/public/uploads/xcximg/20241020/d29b3cdc442ba6d5658c9fe1b061a671.png differ diff --git a/public/uploads/xcximg/20241020/db9276271410050c7ece18b5beb8956d.png b/public/uploads/xcximg/20241020/db9276271410050c7ece18b5beb8956d.png new file mode 100644 index 0000000..e2905d3 Binary files /dev/null and b/public/uploads/xcximg/20241020/db9276271410050c7ece18b5beb8956d.png differ diff --git a/public/uploads/xcximg/20241020/dc35de14fc5980fe77c293b6b0522fd1.png b/public/uploads/xcximg/20241020/dc35de14fc5980fe77c293b6b0522fd1.png new file mode 100644 index 0000000..e219bb5 Binary files /dev/null and b/public/uploads/xcximg/20241020/dc35de14fc5980fe77c293b6b0522fd1.png differ diff --git a/public/uploads/xcximg/20241020/df6002d7ec1bfbcd61f1424343f4ae6b.png b/public/uploads/xcximg/20241020/df6002d7ec1bfbcd61f1424343f4ae6b.png new file mode 100644 index 0000000..06e389f Binary files /dev/null and b/public/uploads/xcximg/20241020/df6002d7ec1bfbcd61f1424343f4ae6b.png differ diff --git a/public/uploads/xcximg/20241020/e641ac61e90fb760bb4dc12bc8823f47.png b/public/uploads/xcximg/20241020/e641ac61e90fb760bb4dc12bc8823f47.png new file mode 100644 index 0000000..7997cd5 Binary files /dev/null and b/public/uploads/xcximg/20241020/e641ac61e90fb760bb4dc12bc8823f47.png differ diff --git a/public/uploads/xcximg/20241020/e9203cd17b1b03a3d1ebe01af3dc3c46.png b/public/uploads/xcximg/20241020/e9203cd17b1b03a3d1ebe01af3dc3c46.png new file mode 100644 index 0000000..0825c7b Binary files /dev/null and b/public/uploads/xcximg/20241020/e9203cd17b1b03a3d1ebe01af3dc3c46.png differ diff --git a/public/uploads/xcximg/20241020/e983ba07e3e02afda71b85237042f1a8.png b/public/uploads/xcximg/20241020/e983ba07e3e02afda71b85237042f1a8.png new file mode 100644 index 0000000..1f01094 Binary files /dev/null and b/public/uploads/xcximg/20241020/e983ba07e3e02afda71b85237042f1a8.png differ diff --git a/public/uploads/xcximg/20241020/f03f76968227a0d2f739a019736bd857.png b/public/uploads/xcximg/20241020/f03f76968227a0d2f739a019736bd857.png new file mode 100644 index 0000000..dd03984 Binary files /dev/null and b/public/uploads/xcximg/20241020/f03f76968227a0d2f739a019736bd857.png differ diff --git a/public/uploads/xcximg/20241020/f360a7a4b8390dcf1c3975e8ebbbba62.png b/public/uploads/xcximg/20241020/f360a7a4b8390dcf1c3975e8ebbbba62.png new file mode 100644 index 0000000..e219bb5 Binary files /dev/null and b/public/uploads/xcximg/20241020/f360a7a4b8390dcf1c3975e8ebbbba62.png differ diff --git a/public/uploads/xcximg/20241020/f521199fd0aa6d40b581057648cab411.png b/public/uploads/xcximg/20241020/f521199fd0aa6d40b581057648cab411.png new file mode 100644 index 0000000..e219bb5 Binary files /dev/null and b/public/uploads/xcximg/20241020/f521199fd0aa6d40b581057648cab411.png differ diff --git a/public/uploads/xcximg/20241021/18df9a9fa13e736fc41cb4310523c226.jpeg b/public/uploads/xcximg/20241021/18df9a9fa13e736fc41cb4310523c226.jpeg new file mode 100644 index 0000000..3401752 Binary files /dev/null and b/public/uploads/xcximg/20241021/18df9a9fa13e736fc41cb4310523c226.jpeg differ diff --git a/public/uploads/xcximg/20241021/1b2b531fb8634bd9d3d755097b353606.jpeg b/public/uploads/xcximg/20241021/1b2b531fb8634bd9d3d755097b353606.jpeg new file mode 100644 index 0000000..5f4ac24 Binary files /dev/null and b/public/uploads/xcximg/20241021/1b2b531fb8634bd9d3d755097b353606.jpeg differ diff --git a/public/uploads/xcximg/20241021/4d8ca2d9ba2b7a94d4fe3104f5df7d64.jpeg b/public/uploads/xcximg/20241021/4d8ca2d9ba2b7a94d4fe3104f5df7d64.jpeg new file mode 100644 index 0000000..c7c0a3e Binary files /dev/null and b/public/uploads/xcximg/20241021/4d8ca2d9ba2b7a94d4fe3104f5df7d64.jpeg differ diff --git a/public/uploads/xcximg/20241021/63b66be57e4cc3916bfaeea0606143b6.jpeg b/public/uploads/xcximg/20241021/63b66be57e4cc3916bfaeea0606143b6.jpeg new file mode 100644 index 0000000..c591d4e Binary files /dev/null and b/public/uploads/xcximg/20241021/63b66be57e4cc3916bfaeea0606143b6.jpeg differ diff --git a/public/uploads/xcximg/20241021/8d65516b95c236a0132584ff48182b00.jpg b/public/uploads/xcximg/20241021/8d65516b95c236a0132584ff48182b00.jpg new file mode 100644 index 0000000..4402436 Binary files /dev/null and b/public/uploads/xcximg/20241021/8d65516b95c236a0132584ff48182b00.jpg differ diff --git a/public/uploads/xcximg/20241021/a4d48008a72927747dc52660f729a1d5.jpg b/public/uploads/xcximg/20241021/a4d48008a72927747dc52660f729a1d5.jpg new file mode 100644 index 0000000..a658402 Binary files /dev/null and b/public/uploads/xcximg/20241021/a4d48008a72927747dc52660f729a1d5.jpg differ diff --git a/public/uploads/xcximg/20241021/b5eeadd238e53f74372b2b7dbae744d2.jpeg b/public/uploads/xcximg/20241021/b5eeadd238e53f74372b2b7dbae744d2.jpeg new file mode 100644 index 0000000..a6b34b9 Binary files /dev/null and b/public/uploads/xcximg/20241021/b5eeadd238e53f74372b2b7dbae744d2.jpeg differ diff --git a/public/uploads/xcximg/20241021/b8637f24c05c497f1dba0acba0a4fcfa.jpg b/public/uploads/xcximg/20241021/b8637f24c05c497f1dba0acba0a4fcfa.jpg new file mode 100644 index 0000000..ba9463d Binary files /dev/null and b/public/uploads/xcximg/20241021/b8637f24c05c497f1dba0acba0a4fcfa.jpg differ diff --git a/public/uploads/xcximg/20241021/be672a2944aec4025767819a162bbb23.jpeg b/public/uploads/xcximg/20241021/be672a2944aec4025767819a162bbb23.jpeg new file mode 100644 index 0000000..700f87a Binary files /dev/null and b/public/uploads/xcximg/20241021/be672a2944aec4025767819a162bbb23.jpeg differ diff --git a/public/uploads/xcximg/20241021/ce91eac136731b47e52ba71264a41680.png b/public/uploads/xcximg/20241021/ce91eac136731b47e52ba71264a41680.png new file mode 100644 index 0000000..0a14d65 Binary files /dev/null and b/public/uploads/xcximg/20241021/ce91eac136731b47e52ba71264a41680.png differ diff --git a/public/uploads/xcximg/20241021/d71438a5131c26f4419bb87f87c20c79.jpeg b/public/uploads/xcximg/20241021/d71438a5131c26f4419bb87f87c20c79.jpeg new file mode 100644 index 0000000..28cca96 Binary files /dev/null and b/public/uploads/xcximg/20241021/d71438a5131c26f4419bb87f87c20c79.jpeg differ diff --git a/public/uploads/xcximg/20241022/8780b95bd9d8d95269529ee630f0dcf7.jpeg b/public/uploads/xcximg/20241022/8780b95bd9d8d95269529ee630f0dcf7.jpeg new file mode 100644 index 0000000..a84557c Binary files /dev/null and b/public/uploads/xcximg/20241022/8780b95bd9d8d95269529ee630f0dcf7.jpeg differ diff --git a/public/uploads/xcximg/20241022/a23784c949a072553304510a3e2cecdb.jpeg b/public/uploads/xcximg/20241022/a23784c949a072553304510a3e2cecdb.jpeg new file mode 100644 index 0000000..08f37fa Binary files /dev/null and b/public/uploads/xcximg/20241022/a23784c949a072553304510a3e2cecdb.jpeg differ diff --git a/public/uploads/xcximg/20241022/d77b7a0bcf6ae7f7c899f9bcc5c5adbb.jpeg b/public/uploads/xcximg/20241022/d77b7a0bcf6ae7f7c899f9bcc5c5adbb.jpeg new file mode 100644 index 0000000..cb0aa7c Binary files /dev/null and b/public/uploads/xcximg/20241022/d77b7a0bcf6ae7f7c899f9bcc5c5adbb.jpeg differ diff --git a/public/uploads/xcximg/20241024/0079cf867b670863f10e2db5f0706a26.jpeg b/public/uploads/xcximg/20241024/0079cf867b670863f10e2db5f0706a26.jpeg new file mode 100644 index 0000000..9fb94c5 Binary files /dev/null and b/public/uploads/xcximg/20241024/0079cf867b670863f10e2db5f0706a26.jpeg differ diff --git a/public/uploads/xcximg/20241024/03826e1e6a2501dd80200c8c0d1a3ff8.png b/public/uploads/xcximg/20241024/03826e1e6a2501dd80200c8c0d1a3ff8.png new file mode 100644 index 0000000..69174d3 Binary files /dev/null and b/public/uploads/xcximg/20241024/03826e1e6a2501dd80200c8c0d1a3ff8.png differ diff --git a/public/uploads/xcximg/20241024/062250dac27fef28716a84c1f01ce9eb.png b/public/uploads/xcximg/20241024/062250dac27fef28716a84c1f01ce9eb.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241024/062250dac27fef28716a84c1f01ce9eb.png differ diff --git a/public/uploads/xcximg/20241024/0df8451b4c258a74f680c859cdae096f.png b/public/uploads/xcximg/20241024/0df8451b4c258a74f680c859cdae096f.png new file mode 100644 index 0000000..8f0f2f2 Binary files /dev/null and b/public/uploads/xcximg/20241024/0df8451b4c258a74f680c859cdae096f.png differ diff --git a/public/uploads/xcximg/20241024/14902dfd5814a136b3c74d9a0eef0da8.png b/public/uploads/xcximg/20241024/14902dfd5814a136b3c74d9a0eef0da8.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241024/14902dfd5814a136b3c74d9a0eef0da8.png differ diff --git a/public/uploads/xcximg/20241024/15fa5946d2f6e24d80c539e39e766228.jpeg b/public/uploads/xcximg/20241024/15fa5946d2f6e24d80c539e39e766228.jpeg new file mode 100644 index 0000000..902ba8e Binary files /dev/null and b/public/uploads/xcximg/20241024/15fa5946d2f6e24d80c539e39e766228.jpeg differ diff --git a/public/uploads/xcximg/20241024/16cdffea5f24890ab5c8213cd85566b6.jpeg b/public/uploads/xcximg/20241024/16cdffea5f24890ab5c8213cd85566b6.jpeg new file mode 100644 index 0000000..0f72a56 Binary files /dev/null and b/public/uploads/xcximg/20241024/16cdffea5f24890ab5c8213cd85566b6.jpeg differ diff --git a/public/uploads/xcximg/20241024/1750242e6c24a35afa7811da4b113bc8.jpg b/public/uploads/xcximg/20241024/1750242e6c24a35afa7811da4b113bc8.jpg new file mode 100644 index 0000000..1039d4b Binary files /dev/null and b/public/uploads/xcximg/20241024/1750242e6c24a35afa7811da4b113bc8.jpg differ diff --git a/public/uploads/xcximg/20241024/1851e20fe005db8283bb2365fe8086ee.jpeg b/public/uploads/xcximg/20241024/1851e20fe005db8283bb2365fe8086ee.jpeg new file mode 100644 index 0000000..d16e302 Binary files /dev/null and b/public/uploads/xcximg/20241024/1851e20fe005db8283bb2365fe8086ee.jpeg differ diff --git a/public/uploads/xcximg/20241024/18de797a1e56a2d2dd68a9f679c89d91.jpeg b/public/uploads/xcximg/20241024/18de797a1e56a2d2dd68a9f679c89d91.jpeg new file mode 100644 index 0000000..902436f Binary files /dev/null and b/public/uploads/xcximg/20241024/18de797a1e56a2d2dd68a9f679c89d91.jpeg differ diff --git a/public/uploads/xcximg/20241024/26bd2434c00130640174b85edef422f0.jpg b/public/uploads/xcximg/20241024/26bd2434c00130640174b85edef422f0.jpg new file mode 100644 index 0000000..f6dac26 Binary files /dev/null and b/public/uploads/xcximg/20241024/26bd2434c00130640174b85edef422f0.jpg differ diff --git a/public/uploads/xcximg/20241024/2769e5c5880f3b618fdd1e16b2ba7764.jpeg b/public/uploads/xcximg/20241024/2769e5c5880f3b618fdd1e16b2ba7764.jpeg new file mode 100644 index 0000000..4829dae Binary files /dev/null and b/public/uploads/xcximg/20241024/2769e5c5880f3b618fdd1e16b2ba7764.jpeg differ diff --git a/public/uploads/xcximg/20241024/279a099e54bf0aa3caf0227abef6435a.png b/public/uploads/xcximg/20241024/279a099e54bf0aa3caf0227abef6435a.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241024/279a099e54bf0aa3caf0227abef6435a.png differ diff --git a/public/uploads/xcximg/20241024/2b8cd500ab18b3500ea172a2e26918f7.jpeg b/public/uploads/xcximg/20241024/2b8cd500ab18b3500ea172a2e26918f7.jpeg new file mode 100644 index 0000000..2d9c66b Binary files /dev/null and b/public/uploads/xcximg/20241024/2b8cd500ab18b3500ea172a2e26918f7.jpeg differ diff --git a/public/uploads/xcximg/20241024/2d3efffba9ea08529380f56dce5cbcd4.jpeg b/public/uploads/xcximg/20241024/2d3efffba9ea08529380f56dce5cbcd4.jpeg new file mode 100644 index 0000000..1fed8d2 Binary files /dev/null and b/public/uploads/xcximg/20241024/2d3efffba9ea08529380f56dce5cbcd4.jpeg differ diff --git a/public/uploads/xcximg/20241024/303e655107b30a88896f3bb97db7d751.jpeg b/public/uploads/xcximg/20241024/303e655107b30a88896f3bb97db7d751.jpeg new file mode 100644 index 0000000..6aeec9e Binary files /dev/null and b/public/uploads/xcximg/20241024/303e655107b30a88896f3bb97db7d751.jpeg differ diff --git a/public/uploads/xcximg/20241024/31d8df6d1f6a1e90a28ba596457fa913.jpeg b/public/uploads/xcximg/20241024/31d8df6d1f6a1e90a28ba596457fa913.jpeg new file mode 100644 index 0000000..a9f4583 Binary files /dev/null and b/public/uploads/xcximg/20241024/31d8df6d1f6a1e90a28ba596457fa913.jpeg differ diff --git a/public/uploads/xcximg/20241024/37640b990eca01b6a5d3400b45b36ded.png b/public/uploads/xcximg/20241024/37640b990eca01b6a5d3400b45b36ded.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241024/37640b990eca01b6a5d3400b45b36ded.png differ diff --git a/public/uploads/xcximg/20241024/3ea86bdf66f6cc08de91a21b9b1ec6b6.png b/public/uploads/xcximg/20241024/3ea86bdf66f6cc08de91a21b9b1ec6b6.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241024/3ea86bdf66f6cc08de91a21b9b1ec6b6.png differ diff --git a/public/uploads/xcximg/20241024/48941b15e3b16530bfacfd72e139cdb7.jpeg b/public/uploads/xcximg/20241024/48941b15e3b16530bfacfd72e139cdb7.jpeg new file mode 100644 index 0000000..9265969 Binary files /dev/null and b/public/uploads/xcximg/20241024/48941b15e3b16530bfacfd72e139cdb7.jpeg differ diff --git a/public/uploads/xcximg/20241024/49220dd2e387a6c4c58bdb91301a79fb.jpg b/public/uploads/xcximg/20241024/49220dd2e387a6c4c58bdb91301a79fb.jpg new file mode 100644 index 0000000..a597d3e Binary files /dev/null and b/public/uploads/xcximg/20241024/49220dd2e387a6c4c58bdb91301a79fb.jpg differ diff --git a/public/uploads/xcximg/20241024/4c02e26e7cd76a440061e62a68d77b6b.jpeg b/public/uploads/xcximg/20241024/4c02e26e7cd76a440061e62a68d77b6b.jpeg new file mode 100644 index 0000000..643d6c4 Binary files /dev/null and b/public/uploads/xcximg/20241024/4c02e26e7cd76a440061e62a68d77b6b.jpeg differ diff --git a/public/uploads/xcximg/20241024/4f0c30ac14b3a9b409b4569cd1c745a0.jpeg b/public/uploads/xcximg/20241024/4f0c30ac14b3a9b409b4569cd1c745a0.jpeg new file mode 100644 index 0000000..17a6151 Binary files /dev/null and b/public/uploads/xcximg/20241024/4f0c30ac14b3a9b409b4569cd1c745a0.jpeg differ diff --git a/public/uploads/xcximg/20241024/52379abcbfb28fe00caa93bc75da986d.png b/public/uploads/xcximg/20241024/52379abcbfb28fe00caa93bc75da986d.png new file mode 100644 index 0000000..2ab0fea Binary files /dev/null and b/public/uploads/xcximg/20241024/52379abcbfb28fe00caa93bc75da986d.png differ diff --git a/public/uploads/xcximg/20241024/5b313a2a53517f1751a1e007d73387a0.png b/public/uploads/xcximg/20241024/5b313a2a53517f1751a1e007d73387a0.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241024/5b313a2a53517f1751a1e007d73387a0.png differ diff --git a/public/uploads/xcximg/20241024/5cfad42182d65a66ba54cfb5245f9359.png b/public/uploads/xcximg/20241024/5cfad42182d65a66ba54cfb5245f9359.png new file mode 100644 index 0000000..1335fb1 Binary files /dev/null and b/public/uploads/xcximg/20241024/5cfad42182d65a66ba54cfb5245f9359.png differ diff --git a/public/uploads/xcximg/20241024/64a8871eac93f9e3bbf45590db2a7457.jpeg b/public/uploads/xcximg/20241024/64a8871eac93f9e3bbf45590db2a7457.jpeg new file mode 100644 index 0000000..df308a8 Binary files /dev/null and b/public/uploads/xcximg/20241024/64a8871eac93f9e3bbf45590db2a7457.jpeg differ diff --git a/public/uploads/xcximg/20241024/64e66b8266fc2c953d4be63edb9a6d3a.jpeg b/public/uploads/xcximg/20241024/64e66b8266fc2c953d4be63edb9a6d3a.jpeg new file mode 100644 index 0000000..16bfb23 Binary files /dev/null and b/public/uploads/xcximg/20241024/64e66b8266fc2c953d4be63edb9a6d3a.jpeg differ diff --git a/public/uploads/xcximg/20241024/679bae93c1ff2f49c0df8a4b7b3fc5c7.png b/public/uploads/xcximg/20241024/679bae93c1ff2f49c0df8a4b7b3fc5c7.png new file mode 100644 index 0000000..f5bc558 Binary files /dev/null and b/public/uploads/xcximg/20241024/679bae93c1ff2f49c0df8a4b7b3fc5c7.png differ diff --git a/public/uploads/xcximg/20241024/6c0117b45f12b247f79faeabef1b68f9.png b/public/uploads/xcximg/20241024/6c0117b45f12b247f79faeabef1b68f9.png new file mode 100644 index 0000000..f29a176 Binary files /dev/null and b/public/uploads/xcximg/20241024/6c0117b45f12b247f79faeabef1b68f9.png differ diff --git a/public/uploads/xcximg/20241024/6e4d7dae6fd468647a886e6ec84ff706.jpg b/public/uploads/xcximg/20241024/6e4d7dae6fd468647a886e6ec84ff706.jpg new file mode 100644 index 0000000..b25690a Binary files /dev/null and b/public/uploads/xcximg/20241024/6e4d7dae6fd468647a886e6ec84ff706.jpg differ diff --git a/public/uploads/xcximg/20241024/7dfcb077b776fd594d4e74a3f65841f1.png b/public/uploads/xcximg/20241024/7dfcb077b776fd594d4e74a3f65841f1.png new file mode 100644 index 0000000..916c87b Binary files /dev/null and b/public/uploads/xcximg/20241024/7dfcb077b776fd594d4e74a3f65841f1.png differ diff --git a/public/uploads/xcximg/20241024/8b5b8f85386dfb61a1e20b69b8745d69.png b/public/uploads/xcximg/20241024/8b5b8f85386dfb61a1e20b69b8745d69.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241024/8b5b8f85386dfb61a1e20b69b8745d69.png differ diff --git a/public/uploads/xcximg/20241024/9165bdff6ecf9ebe9b7dc18868798344.jpeg b/public/uploads/xcximg/20241024/9165bdff6ecf9ebe9b7dc18868798344.jpeg new file mode 100644 index 0000000..c289917 Binary files /dev/null and b/public/uploads/xcximg/20241024/9165bdff6ecf9ebe9b7dc18868798344.jpeg differ diff --git a/public/uploads/xcximg/20241024/965bd90334c665ac264b85115597b012.png b/public/uploads/xcximg/20241024/965bd90334c665ac264b85115597b012.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241024/965bd90334c665ac264b85115597b012.png differ diff --git a/public/uploads/xcximg/20241024/99249aca0e465a878b2092c642b188cd.jpeg b/public/uploads/xcximg/20241024/99249aca0e465a878b2092c642b188cd.jpeg new file mode 100644 index 0000000..4002e35 Binary files /dev/null and b/public/uploads/xcximg/20241024/99249aca0e465a878b2092c642b188cd.jpeg differ diff --git a/public/uploads/xcximg/20241024/a3457a1fa876a1da0b2547b1fc4e27b4.png b/public/uploads/xcximg/20241024/a3457a1fa876a1da0b2547b1fc4e27b4.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241024/a3457a1fa876a1da0b2547b1fc4e27b4.png differ diff --git a/public/uploads/xcximg/20241024/a39c16d134dda721aa5c277e4ba70197.jpg b/public/uploads/xcximg/20241024/a39c16d134dda721aa5c277e4ba70197.jpg new file mode 100644 index 0000000..bc43e90 Binary files /dev/null and b/public/uploads/xcximg/20241024/a39c16d134dda721aa5c277e4ba70197.jpg differ diff --git a/public/uploads/xcximg/20241024/a54ff338a71fd1c7763346e820250f56.jpeg b/public/uploads/xcximg/20241024/a54ff338a71fd1c7763346e820250f56.jpeg new file mode 100644 index 0000000..edd05c1 Binary files /dev/null and b/public/uploads/xcximg/20241024/a54ff338a71fd1c7763346e820250f56.jpeg differ diff --git a/public/uploads/xcximg/20241024/abd718c5e08ffe09ef7f420c35d96936.png b/public/uploads/xcximg/20241024/abd718c5e08ffe09ef7f420c35d96936.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241024/abd718c5e08ffe09ef7f420c35d96936.png differ diff --git a/public/uploads/xcximg/20241024/b59b7102b939b2b165ec1425d504006e.jpg b/public/uploads/xcximg/20241024/b59b7102b939b2b165ec1425d504006e.jpg new file mode 100644 index 0000000..791c2ed Binary files /dev/null and b/public/uploads/xcximg/20241024/b59b7102b939b2b165ec1425d504006e.jpg differ diff --git a/public/uploads/xcximg/20241024/b79421ccda1c035b77cb69a441ecc619.jpeg b/public/uploads/xcximg/20241024/b79421ccda1c035b77cb69a441ecc619.jpeg new file mode 100644 index 0000000..9793245 Binary files /dev/null and b/public/uploads/xcximg/20241024/b79421ccda1c035b77cb69a441ecc619.jpeg differ diff --git a/public/uploads/xcximg/20241024/b916f31d12fbe05f8e58fc9ec4a7def7.png b/public/uploads/xcximg/20241024/b916f31d12fbe05f8e58fc9ec4a7def7.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241024/b916f31d12fbe05f8e58fc9ec4a7def7.png differ diff --git a/public/uploads/xcximg/20241024/bb0d4ab046c751ba162b15f4c4fee3ec.png b/public/uploads/xcximg/20241024/bb0d4ab046c751ba162b15f4c4fee3ec.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241024/bb0d4ab046c751ba162b15f4c4fee3ec.png differ diff --git a/public/uploads/xcximg/20241024/c0501fe54c8195172b02aafcf18de85c.jpg b/public/uploads/xcximg/20241024/c0501fe54c8195172b02aafcf18de85c.jpg new file mode 100644 index 0000000..bc43e90 Binary files /dev/null and b/public/uploads/xcximg/20241024/c0501fe54c8195172b02aafcf18de85c.jpg differ diff --git a/public/uploads/xcximg/20241024/c7efe3dd584965d6ae12c5159ecd571c.jpeg b/public/uploads/xcximg/20241024/c7efe3dd584965d6ae12c5159ecd571c.jpeg new file mode 100644 index 0000000..35b0f24 Binary files /dev/null and b/public/uploads/xcximg/20241024/c7efe3dd584965d6ae12c5159ecd571c.jpeg differ diff --git a/public/uploads/xcximg/20241024/cabd759c636b9eaa75c08f01480059fa.jpg b/public/uploads/xcximg/20241024/cabd759c636b9eaa75c08f01480059fa.jpg new file mode 100644 index 0000000..ad63081 Binary files /dev/null and b/public/uploads/xcximg/20241024/cabd759c636b9eaa75c08f01480059fa.jpg differ diff --git a/public/uploads/xcximg/20241024/d0318b148141a6cd7e7a3c6f355365f8.jpeg b/public/uploads/xcximg/20241024/d0318b148141a6cd7e7a3c6f355365f8.jpeg new file mode 100644 index 0000000..c505085 Binary files /dev/null and b/public/uploads/xcximg/20241024/d0318b148141a6cd7e7a3c6f355365f8.jpeg differ diff --git a/public/uploads/xcximg/20241024/d4c953ca9e8637f8419ca5a8a8563e45.png b/public/uploads/xcximg/20241024/d4c953ca9e8637f8419ca5a8a8563e45.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241024/d4c953ca9e8637f8419ca5a8a8563e45.png differ diff --git a/public/uploads/xcximg/20241024/dc40ba18c59f01f2848b404fd571eb6b.jpeg b/public/uploads/xcximg/20241024/dc40ba18c59f01f2848b404fd571eb6b.jpeg new file mode 100644 index 0000000..a2491f7 Binary files /dev/null and b/public/uploads/xcximg/20241024/dc40ba18c59f01f2848b404fd571eb6b.jpeg differ diff --git a/public/uploads/xcximg/20241024/dce96fb21c6a33824f9d619eba1b23cb.jpeg b/public/uploads/xcximg/20241024/dce96fb21c6a33824f9d619eba1b23cb.jpeg new file mode 100644 index 0000000..f62f244 Binary files /dev/null and b/public/uploads/xcximg/20241024/dce96fb21c6a33824f9d619eba1b23cb.jpeg differ diff --git a/public/uploads/xcximg/20241024/e1414f8b2e5815b3b8c448341486f5bd.png b/public/uploads/xcximg/20241024/e1414f8b2e5815b3b8c448341486f5bd.png new file mode 100644 index 0000000..6220905 Binary files /dev/null and b/public/uploads/xcximg/20241024/e1414f8b2e5815b3b8c448341486f5bd.png differ diff --git a/public/uploads/xcximg/20241024/eae1e6a17d5033d3727857ec8422451a.jpeg b/public/uploads/xcximg/20241024/eae1e6a17d5033d3727857ec8422451a.jpeg new file mode 100644 index 0000000..85dd28a Binary files /dev/null and b/public/uploads/xcximg/20241024/eae1e6a17d5033d3727857ec8422451a.jpeg differ diff --git a/public/uploads/xcximg/20241024/ef767aa8f9b1563f04a64793f4712cdf.png b/public/uploads/xcximg/20241024/ef767aa8f9b1563f04a64793f4712cdf.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241024/ef767aa8f9b1563f04a64793f4712cdf.png differ diff --git a/public/uploads/xcximg/20241024/f2d2454f330bdbf07de3e3534514fd33.png b/public/uploads/xcximg/20241024/f2d2454f330bdbf07de3e3534514fd33.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241024/f2d2454f330bdbf07de3e3534514fd33.png differ diff --git a/public/uploads/xcximg/20241024/f948456b09100c00d6844bb94debbb67.jpg b/public/uploads/xcximg/20241024/f948456b09100c00d6844bb94debbb67.jpg new file mode 100644 index 0000000..ad63081 Binary files /dev/null and b/public/uploads/xcximg/20241024/f948456b09100c00d6844bb94debbb67.jpg differ diff --git a/public/uploads/xcximg/20241024/fe46612485d2d9727ee9b5345a7b72e1.jpg b/public/uploads/xcximg/20241024/fe46612485d2d9727ee9b5345a7b72e1.jpg new file mode 100644 index 0000000..2307989 Binary files /dev/null and b/public/uploads/xcximg/20241024/fe46612485d2d9727ee9b5345a7b72e1.jpg differ diff --git a/public/uploads/xcximg/20241024/feaafd38306bd14660d7b0262a5d5dbb.jpeg b/public/uploads/xcximg/20241024/feaafd38306bd14660d7b0262a5d5dbb.jpeg new file mode 100644 index 0000000..03a8dda Binary files /dev/null and b/public/uploads/xcximg/20241024/feaafd38306bd14660d7b0262a5d5dbb.jpeg differ diff --git a/public/uploads/xcximg/20241025/02b1cfa8c58238ed351855a46159e47b.jpg b/public/uploads/xcximg/20241025/02b1cfa8c58238ed351855a46159e47b.jpg new file mode 100644 index 0000000..cf31b1c Binary files /dev/null and b/public/uploads/xcximg/20241025/02b1cfa8c58238ed351855a46159e47b.jpg differ diff --git a/public/uploads/xcximg/20241025/0ac679a1dfe78f414304c1ccd6688349.jpg b/public/uploads/xcximg/20241025/0ac679a1dfe78f414304c1ccd6688349.jpg new file mode 100644 index 0000000..ab18aa6 Binary files /dev/null and b/public/uploads/xcximg/20241025/0ac679a1dfe78f414304c1ccd6688349.jpg differ diff --git a/public/uploads/xcximg/20241025/0e2dd59e9110d443b57e8a8fe153c3d9.png b/public/uploads/xcximg/20241025/0e2dd59e9110d443b57e8a8fe153c3d9.png new file mode 100644 index 0000000..ee5f391 Binary files /dev/null and b/public/uploads/xcximg/20241025/0e2dd59e9110d443b57e8a8fe153c3d9.png differ diff --git a/public/uploads/xcximg/20241025/10cd8fdf5dc92785888c1a6a42acd24d.png b/public/uploads/xcximg/20241025/10cd8fdf5dc92785888c1a6a42acd24d.png new file mode 100644 index 0000000..8959da6 Binary files /dev/null and b/public/uploads/xcximg/20241025/10cd8fdf5dc92785888c1a6a42acd24d.png differ diff --git a/public/uploads/xcximg/20241025/22281ea8f9598158b04e1683f2d592ee.jpg b/public/uploads/xcximg/20241025/22281ea8f9598158b04e1683f2d592ee.jpg new file mode 100644 index 0000000..cf31b1c Binary files /dev/null and b/public/uploads/xcximg/20241025/22281ea8f9598158b04e1683f2d592ee.jpg differ diff --git a/public/uploads/xcximg/20241025/22a40779ef1592daeaaa42492bf22689.png b/public/uploads/xcximg/20241025/22a40779ef1592daeaaa42492bf22689.png new file mode 100644 index 0000000..8959da6 Binary files /dev/null and b/public/uploads/xcximg/20241025/22a40779ef1592daeaaa42492bf22689.png differ diff --git a/public/uploads/xcximg/20241025/3d70b305487af30490643435ccab5755.png b/public/uploads/xcximg/20241025/3d70b305487af30490643435ccab5755.png new file mode 100644 index 0000000..8959da6 Binary files /dev/null and b/public/uploads/xcximg/20241025/3d70b305487af30490643435ccab5755.png differ diff --git a/public/uploads/xcximg/20241025/426e54462ba0dab8cdf3f0f69c19c9e1.jpg b/public/uploads/xcximg/20241025/426e54462ba0dab8cdf3f0f69c19c9e1.jpg new file mode 100644 index 0000000..ab18aa6 Binary files /dev/null and b/public/uploads/xcximg/20241025/426e54462ba0dab8cdf3f0f69c19c9e1.jpg differ diff --git a/public/uploads/xcximg/20241025/4c5b6cdbc72f757cdd7a3b8de235107c.jpeg b/public/uploads/xcximg/20241025/4c5b6cdbc72f757cdd7a3b8de235107c.jpeg new file mode 100644 index 0000000..3d6b45c Binary files /dev/null and b/public/uploads/xcximg/20241025/4c5b6cdbc72f757cdd7a3b8de235107c.jpeg differ diff --git a/public/uploads/xcximg/20241025/53a73032d1051122113bf21014219686.jpg b/public/uploads/xcximg/20241025/53a73032d1051122113bf21014219686.jpg new file mode 100644 index 0000000..8d8c90e Binary files /dev/null and b/public/uploads/xcximg/20241025/53a73032d1051122113bf21014219686.jpg differ diff --git a/public/uploads/xcximg/20241025/5849236ddf199a59c5961a9bf9b58949.jpg b/public/uploads/xcximg/20241025/5849236ddf199a59c5961a9bf9b58949.jpg new file mode 100644 index 0000000..13593a1 Binary files /dev/null and b/public/uploads/xcximg/20241025/5849236ddf199a59c5961a9bf9b58949.jpg differ diff --git a/public/uploads/xcximg/20241025/5a60702de5593318319c6920923c3fb3.jpg b/public/uploads/xcximg/20241025/5a60702de5593318319c6920923c3fb3.jpg new file mode 100644 index 0000000..f8ccc1d Binary files /dev/null and b/public/uploads/xcximg/20241025/5a60702de5593318319c6920923c3fb3.jpg differ diff --git a/public/uploads/xcximg/20241025/6d8a9564aeed1091a07799d6f024a384.jpg b/public/uploads/xcximg/20241025/6d8a9564aeed1091a07799d6f024a384.jpg new file mode 100644 index 0000000..ab18aa6 Binary files /dev/null and b/public/uploads/xcximg/20241025/6d8a9564aeed1091a07799d6f024a384.jpg differ diff --git a/public/uploads/xcximg/20241025/78b5706ccdea6d0715300ddef6d70520.jpg b/public/uploads/xcximg/20241025/78b5706ccdea6d0715300ddef6d70520.jpg new file mode 100644 index 0000000..ad63081 Binary files /dev/null and b/public/uploads/xcximg/20241025/78b5706ccdea6d0715300ddef6d70520.jpg differ diff --git a/public/uploads/xcximg/20241025/7a57591b1ed0ce2b80736c02ff159188.jpeg b/public/uploads/xcximg/20241025/7a57591b1ed0ce2b80736c02ff159188.jpeg new file mode 100644 index 0000000..d86b0b0 Binary files /dev/null and b/public/uploads/xcximg/20241025/7a57591b1ed0ce2b80736c02ff159188.jpeg differ diff --git a/public/uploads/xcximg/20241025/7cb0b7b95f341b7ccad1c552e7f0f9d3.png b/public/uploads/xcximg/20241025/7cb0b7b95f341b7ccad1c552e7f0f9d3.png new file mode 100644 index 0000000..8959da6 Binary files /dev/null and b/public/uploads/xcximg/20241025/7cb0b7b95f341b7ccad1c552e7f0f9d3.png differ diff --git a/public/uploads/xcximg/20241025/7fedd1617854b082167160657045179e.jpg b/public/uploads/xcximg/20241025/7fedd1617854b082167160657045179e.jpg new file mode 100644 index 0000000..670a384 Binary files /dev/null and b/public/uploads/xcximg/20241025/7fedd1617854b082167160657045179e.jpg differ diff --git a/public/uploads/xcximg/20241025/915141448199d6ee3ce7098491dd0e1d.jpeg b/public/uploads/xcximg/20241025/915141448199d6ee3ce7098491dd0e1d.jpeg new file mode 100644 index 0000000..c595a64 Binary files /dev/null and b/public/uploads/xcximg/20241025/915141448199d6ee3ce7098491dd0e1d.jpeg differ diff --git a/public/uploads/xcximg/20241025/9bc8cf8da03953c00131399f51545bc0.jpeg b/public/uploads/xcximg/20241025/9bc8cf8da03953c00131399f51545bc0.jpeg new file mode 100644 index 0000000..e4c4d24 Binary files /dev/null and b/public/uploads/xcximg/20241025/9bc8cf8da03953c00131399f51545bc0.jpeg differ diff --git a/public/uploads/xcximg/20241025/b5782869c3d6e247a8496051500bea7f.jpeg b/public/uploads/xcximg/20241025/b5782869c3d6e247a8496051500bea7f.jpeg new file mode 100644 index 0000000..40fd670 Binary files /dev/null and b/public/uploads/xcximg/20241025/b5782869c3d6e247a8496051500bea7f.jpeg differ diff --git a/public/uploads/xcximg/20241025/b9a1b746812bf1b48b422251826cd6ba.png b/public/uploads/xcximg/20241025/b9a1b746812bf1b48b422251826cd6ba.png new file mode 100644 index 0000000..8959da6 Binary files /dev/null and b/public/uploads/xcximg/20241025/b9a1b746812bf1b48b422251826cd6ba.png differ diff --git a/public/uploads/xcximg/20241025/c47e3175aba7978c01d3765330bd2009.jpg b/public/uploads/xcximg/20241025/c47e3175aba7978c01d3765330bd2009.jpg new file mode 100644 index 0000000..8d8c90e Binary files /dev/null and b/public/uploads/xcximg/20241025/c47e3175aba7978c01d3765330bd2009.jpg differ diff --git a/public/uploads/xcximg/20241025/d240f182f6a100f04249c7ad148d4711.jpeg b/public/uploads/xcximg/20241025/d240f182f6a100f04249c7ad148d4711.jpeg new file mode 100644 index 0000000..173f89a Binary files /dev/null and b/public/uploads/xcximg/20241025/d240f182f6a100f04249c7ad148d4711.jpeg differ diff --git a/public/uploads/xcximg/20241025/db37b28eb2ed1e1dc00bd23ba28a8a18.png b/public/uploads/xcximg/20241025/db37b28eb2ed1e1dc00bd23ba28a8a18.png new file mode 100644 index 0000000..8959da6 Binary files /dev/null and b/public/uploads/xcximg/20241025/db37b28eb2ed1e1dc00bd23ba28a8a18.png differ diff --git a/public/uploads/xcximg/20241025/f2cdd5e00ee29f3534e792cc5479722d.png b/public/uploads/xcximg/20241025/f2cdd5e00ee29f3534e792cc5479722d.png new file mode 100644 index 0000000..8959da6 Binary files /dev/null and b/public/uploads/xcximg/20241025/f2cdd5e00ee29f3534e792cc5479722d.png differ diff --git a/public/uploads/xcximg/20241026/0b71ba8311085440507955c552140b0f.jpeg b/public/uploads/xcximg/20241026/0b71ba8311085440507955c552140b0f.jpeg new file mode 100644 index 0000000..840155f Binary files /dev/null and b/public/uploads/xcximg/20241026/0b71ba8311085440507955c552140b0f.jpeg differ diff --git a/public/uploads/xcximg/20241026/2483e15e4ecd420d2b46be5f4362b50c.jpeg b/public/uploads/xcximg/20241026/2483e15e4ecd420d2b46be5f4362b50c.jpeg new file mode 100644 index 0000000..b117414 Binary files /dev/null and b/public/uploads/xcximg/20241026/2483e15e4ecd420d2b46be5f4362b50c.jpeg differ diff --git a/public/uploads/xcximg/20241026/3587a6fc65fe1ea4927d3164ed2aba50.jpeg b/public/uploads/xcximg/20241026/3587a6fc65fe1ea4927d3164ed2aba50.jpeg new file mode 100644 index 0000000..c3b9e5b Binary files /dev/null and b/public/uploads/xcximg/20241026/3587a6fc65fe1ea4927d3164ed2aba50.jpeg differ diff --git a/public/uploads/xcximg/20241026/44f33e6eeb0c777383c46c4d596abf73.png b/public/uploads/xcximg/20241026/44f33e6eeb0c777383c46c4d596abf73.png new file mode 100644 index 0000000..7997cd5 Binary files /dev/null and b/public/uploads/xcximg/20241026/44f33e6eeb0c777383c46c4d596abf73.png differ diff --git a/public/uploads/xcximg/20241026/4dc0009d719ddabb3727e440d54b5e7a.jpeg b/public/uploads/xcximg/20241026/4dc0009d719ddabb3727e440d54b5e7a.jpeg new file mode 100644 index 0000000..43faa27 Binary files /dev/null and b/public/uploads/xcximg/20241026/4dc0009d719ddabb3727e440d54b5e7a.jpeg differ diff --git a/public/uploads/xcximg/20241026/74548bc1e41d2f82234beccb9da72334.jpeg b/public/uploads/xcximg/20241026/74548bc1e41d2f82234beccb9da72334.jpeg new file mode 100644 index 0000000..c2fa1d0 Binary files /dev/null and b/public/uploads/xcximg/20241026/74548bc1e41d2f82234beccb9da72334.jpeg differ diff --git a/public/uploads/xcximg/20241026/754f34452fa036a2639adaa944718539.jpeg b/public/uploads/xcximg/20241026/754f34452fa036a2639adaa944718539.jpeg new file mode 100644 index 0000000..f86c02a Binary files /dev/null and b/public/uploads/xcximg/20241026/754f34452fa036a2639adaa944718539.jpeg differ diff --git a/public/uploads/xcximg/20241026/97bc6858a893438314099b877c50f7ca.jpeg b/public/uploads/xcximg/20241026/97bc6858a893438314099b877c50f7ca.jpeg new file mode 100644 index 0000000..4173f52 Binary files /dev/null and b/public/uploads/xcximg/20241026/97bc6858a893438314099b877c50f7ca.jpeg differ diff --git a/public/uploads/xcximg/20241026/b74348b7f712f63be2fb4e2fc0c41dff.jpeg b/public/uploads/xcximg/20241026/b74348b7f712f63be2fb4e2fc0c41dff.jpeg new file mode 100644 index 0000000..70eaa3b Binary files /dev/null and b/public/uploads/xcximg/20241026/b74348b7f712f63be2fb4e2fc0c41dff.jpeg differ diff --git a/public/uploads/xcximg/20241026/e1f313129d0e2d1b97e6c059b0ff2ae0.jpeg b/public/uploads/xcximg/20241026/e1f313129d0e2d1b97e6c059b0ff2ae0.jpeg new file mode 100644 index 0000000..3e26b78 Binary files /dev/null and b/public/uploads/xcximg/20241026/e1f313129d0e2d1b97e6c059b0ff2ae0.jpeg differ diff --git a/public/uploads/xcximg/20241026/ee84c719372ef5453704dd0ba82817c1.jpeg b/public/uploads/xcximg/20241026/ee84c719372ef5453704dd0ba82817c1.jpeg new file mode 100644 index 0000000..9883995 Binary files /dev/null and b/public/uploads/xcximg/20241026/ee84c719372ef5453704dd0ba82817c1.jpeg differ diff --git a/public/uploads/xcximg/20241027/01c287b5b37d13bb3a3f09ef354d8323.jpg b/public/uploads/xcximg/20241027/01c287b5b37d13bb3a3f09ef354d8323.jpg new file mode 100644 index 0000000..150eac0 Binary files /dev/null and b/public/uploads/xcximg/20241027/01c287b5b37d13bb3a3f09ef354d8323.jpg differ diff --git a/public/uploads/xcximg/20241027/38c6c0fe8fcda69440694bfad0ae9aeb.jpeg b/public/uploads/xcximg/20241027/38c6c0fe8fcda69440694bfad0ae9aeb.jpeg new file mode 100644 index 0000000..ffda12f Binary files /dev/null and b/public/uploads/xcximg/20241027/38c6c0fe8fcda69440694bfad0ae9aeb.jpeg differ diff --git a/public/uploads/xcximg/20241027/4992186a8bd2dec384a780dd991114a9.jpg b/public/uploads/xcximg/20241027/4992186a8bd2dec384a780dd991114a9.jpg new file mode 100644 index 0000000..96aa041 Binary files /dev/null and b/public/uploads/xcximg/20241027/4992186a8bd2dec384a780dd991114a9.jpg differ diff --git a/public/uploads/xcximg/20241027/4fc845902d6309778b13b863eb92f154.jpeg b/public/uploads/xcximg/20241027/4fc845902d6309778b13b863eb92f154.jpeg new file mode 100644 index 0000000..684d3fd Binary files /dev/null and b/public/uploads/xcximg/20241027/4fc845902d6309778b13b863eb92f154.jpeg differ diff --git a/public/uploads/xcximg/20241027/51d86fea743ca94828b7982a1bc1d665.jpeg b/public/uploads/xcximg/20241027/51d86fea743ca94828b7982a1bc1d665.jpeg new file mode 100644 index 0000000..2e0c50d Binary files /dev/null and b/public/uploads/xcximg/20241027/51d86fea743ca94828b7982a1bc1d665.jpeg differ diff --git a/public/uploads/xcximg/20241027/6a25a5cb51bbf72cc4bc2ee7f2338ec1.jpeg b/public/uploads/xcximg/20241027/6a25a5cb51bbf72cc4bc2ee7f2338ec1.jpeg new file mode 100644 index 0000000..7bc333b Binary files /dev/null and b/public/uploads/xcximg/20241027/6a25a5cb51bbf72cc4bc2ee7f2338ec1.jpeg differ diff --git a/public/uploads/xcximg/20241027/6d89178b60c7f9f46ecf35fdc99c96ed.jpg b/public/uploads/xcximg/20241027/6d89178b60c7f9f46ecf35fdc99c96ed.jpg new file mode 100644 index 0000000..69e457f Binary files /dev/null and b/public/uploads/xcximg/20241027/6d89178b60c7f9f46ecf35fdc99c96ed.jpg differ diff --git a/public/uploads/xcximg/20241027/80f4217896a8d2b7ae0d23c9d6ee2e25.jpeg b/public/uploads/xcximg/20241027/80f4217896a8d2b7ae0d23c9d6ee2e25.jpeg new file mode 100644 index 0000000..86ef598 Binary files /dev/null and b/public/uploads/xcximg/20241027/80f4217896a8d2b7ae0d23c9d6ee2e25.jpeg differ diff --git a/public/uploads/xcximg/20241027/81ffae2a24be1aac6dc412dbabd4b79f.jpeg b/public/uploads/xcximg/20241027/81ffae2a24be1aac6dc412dbabd4b79f.jpeg new file mode 100644 index 0000000..ce34dd9 Binary files /dev/null and b/public/uploads/xcximg/20241027/81ffae2a24be1aac6dc412dbabd4b79f.jpeg differ diff --git a/public/uploads/xcximg/20241027/8e5b01a59af524910245bc63588d76d5.jpeg b/public/uploads/xcximg/20241027/8e5b01a59af524910245bc63588d76d5.jpeg new file mode 100644 index 0000000..6e9e070 Binary files /dev/null and b/public/uploads/xcximg/20241027/8e5b01a59af524910245bc63588d76d5.jpeg differ diff --git a/public/uploads/xcximg/20241027/9895a4c3cf1381cdd59c457973a3df4c.jpeg b/public/uploads/xcximg/20241027/9895a4c3cf1381cdd59c457973a3df4c.jpeg new file mode 100644 index 0000000..666ab0e Binary files /dev/null and b/public/uploads/xcximg/20241027/9895a4c3cf1381cdd59c457973a3df4c.jpeg differ diff --git a/public/uploads/xcximg/20241027/9fe9489820594dcb6227d998eb53caf9.jpeg b/public/uploads/xcximg/20241027/9fe9489820594dcb6227d998eb53caf9.jpeg new file mode 100644 index 0000000..61c45eb Binary files /dev/null and b/public/uploads/xcximg/20241027/9fe9489820594dcb6227d998eb53caf9.jpeg differ diff --git a/public/uploads/xcximg/20241027/c0a29b180f15ee1c08b30e8aa17d62ef.jpg b/public/uploads/xcximg/20241027/c0a29b180f15ee1c08b30e8aa17d62ef.jpg new file mode 100644 index 0000000..0c92861 Binary files /dev/null and b/public/uploads/xcximg/20241027/c0a29b180f15ee1c08b30e8aa17d62ef.jpg differ diff --git a/public/uploads/xcximg/20241027/c119ec440e3d24d3ad2db443ad80a1f0.jpeg b/public/uploads/xcximg/20241027/c119ec440e3d24d3ad2db443ad80a1f0.jpeg new file mode 100644 index 0000000..2427e85 Binary files /dev/null and b/public/uploads/xcximg/20241027/c119ec440e3d24d3ad2db443ad80a1f0.jpeg differ diff --git a/public/uploads/xcximg/20241027/d05934f1057e17ad89eb127657d3149b.jpeg b/public/uploads/xcximg/20241027/d05934f1057e17ad89eb127657d3149b.jpeg new file mode 100644 index 0000000..ec3a1a1 Binary files /dev/null and b/public/uploads/xcximg/20241027/d05934f1057e17ad89eb127657d3149b.jpeg differ diff --git a/public/uploads/xcximg/20241027/f5376dc60711795acec596e0abdbc165.jpg b/public/uploads/xcximg/20241027/f5376dc60711795acec596e0abdbc165.jpg new file mode 100644 index 0000000..35eb204 Binary files /dev/null and b/public/uploads/xcximg/20241027/f5376dc60711795acec596e0abdbc165.jpg differ diff --git a/public/uploads/xcximg/20241027/fd4cacca91d388f85bc8e21a275086f7.jpeg b/public/uploads/xcximg/20241027/fd4cacca91d388f85bc8e21a275086f7.jpeg new file mode 100644 index 0000000..76dc152 Binary files /dev/null and b/public/uploads/xcximg/20241027/fd4cacca91d388f85bc8e21a275086f7.jpeg differ diff --git a/public/uploads/xcximg/20241028/25233e5897d481b1b243246793c67271.jpg b/public/uploads/xcximg/20241028/25233e5897d481b1b243246793c67271.jpg new file mode 100644 index 0000000..f9bb5b3 Binary files /dev/null and b/public/uploads/xcximg/20241028/25233e5897d481b1b243246793c67271.jpg differ diff --git a/public/uploads/xcximg/20241028/3255e4ffdb8032b344b4ec30c1135aef.jpeg b/public/uploads/xcximg/20241028/3255e4ffdb8032b344b4ec30c1135aef.jpeg new file mode 100644 index 0000000..214bc23 Binary files /dev/null and b/public/uploads/xcximg/20241028/3255e4ffdb8032b344b4ec30c1135aef.jpeg differ diff --git a/public/uploads/xcximg/20241028/401a6b626c14df167e66dc4d9123a5c0.jpeg b/public/uploads/xcximg/20241028/401a6b626c14df167e66dc4d9123a5c0.jpeg new file mode 100644 index 0000000..063010b Binary files /dev/null and b/public/uploads/xcximg/20241028/401a6b626c14df167e66dc4d9123a5c0.jpeg differ diff --git a/public/uploads/xcximg/20241028/5cb1278dfc691c03851fb78d55877ce1.jpeg b/public/uploads/xcximg/20241028/5cb1278dfc691c03851fb78d55877ce1.jpeg new file mode 100644 index 0000000..e145810 Binary files /dev/null and b/public/uploads/xcximg/20241028/5cb1278dfc691c03851fb78d55877ce1.jpeg differ diff --git a/public/uploads/xcximg/20241028/7318f011f55dd8000493121b3f17868b.jpg b/public/uploads/xcximg/20241028/7318f011f55dd8000493121b3f17868b.jpg new file mode 100644 index 0000000..85d6b1c Binary files /dev/null and b/public/uploads/xcximg/20241028/7318f011f55dd8000493121b3f17868b.jpg differ diff --git a/public/uploads/xcximg/20241028/7b2963695d5e9cc367ccb111ab1b9952.jpg b/public/uploads/xcximg/20241028/7b2963695d5e9cc367ccb111ab1b9952.jpg new file mode 100644 index 0000000..376f47c Binary files /dev/null and b/public/uploads/xcximg/20241028/7b2963695d5e9cc367ccb111ab1b9952.jpg differ diff --git a/public/uploads/xcximg/20241028/92bbc9aa37d725aa9f22af655aece88d.jpeg b/public/uploads/xcximg/20241028/92bbc9aa37d725aa9f22af655aece88d.jpeg new file mode 100644 index 0000000..6b75cc4 Binary files /dev/null and b/public/uploads/xcximg/20241028/92bbc9aa37d725aa9f22af655aece88d.jpeg differ diff --git a/public/uploads/xcximg/20241028/93ca84ce416684e29912785d1e1ad428.jpeg b/public/uploads/xcximg/20241028/93ca84ce416684e29912785d1e1ad428.jpeg new file mode 100644 index 0000000..cc2a3f2 Binary files /dev/null and b/public/uploads/xcximg/20241028/93ca84ce416684e29912785d1e1ad428.jpeg differ diff --git a/public/uploads/xcximg/20241028/96f21ebaa0da3bfa7ed28481972a7cd0.jpeg b/public/uploads/xcximg/20241028/96f21ebaa0da3bfa7ed28481972a7cd0.jpeg new file mode 100644 index 0000000..c055c82 Binary files /dev/null and b/public/uploads/xcximg/20241028/96f21ebaa0da3bfa7ed28481972a7cd0.jpeg differ diff --git a/public/uploads/xcximg/20241028/b63b8c5f05d6af3f46f7968ffd6e64d6.jpg b/public/uploads/xcximg/20241028/b63b8c5f05d6af3f46f7968ffd6e64d6.jpg new file mode 100644 index 0000000..de8dd1b Binary files /dev/null and b/public/uploads/xcximg/20241028/b63b8c5f05d6af3f46f7968ffd6e64d6.jpg differ diff --git a/public/uploads/xcximg/20241028/c087705c2bfb33a72ee6205ed2cd3978.jpeg b/public/uploads/xcximg/20241028/c087705c2bfb33a72ee6205ed2cd3978.jpeg new file mode 100644 index 0000000..190755d Binary files /dev/null and b/public/uploads/xcximg/20241028/c087705c2bfb33a72ee6205ed2cd3978.jpeg differ diff --git a/public/uploads/xcximg/20241028/d3f8df33bdf67e26afaaf4904fef8a1d.jpeg b/public/uploads/xcximg/20241028/d3f8df33bdf67e26afaaf4904fef8a1d.jpeg new file mode 100644 index 0000000..1c965e4 Binary files /dev/null and b/public/uploads/xcximg/20241028/d3f8df33bdf67e26afaaf4904fef8a1d.jpeg differ diff --git a/public/uploads/xcximg/20241029/01c0a7552ed75e6b7e02df0bfeb54529.jpeg b/public/uploads/xcximg/20241029/01c0a7552ed75e6b7e02df0bfeb54529.jpeg new file mode 100644 index 0000000..16abf79 Binary files /dev/null and b/public/uploads/xcximg/20241029/01c0a7552ed75e6b7e02df0bfeb54529.jpeg differ diff --git a/public/uploads/xcximg/20241029/05dc7ae06f73bf89f5c05685b34c8d37.jpeg b/public/uploads/xcximg/20241029/05dc7ae06f73bf89f5c05685b34c8d37.jpeg new file mode 100644 index 0000000..3b0dbc4 Binary files /dev/null and b/public/uploads/xcximg/20241029/05dc7ae06f73bf89f5c05685b34c8d37.jpeg differ diff --git a/public/uploads/xcximg/20241029/06b0365c05a7927838baccf59f8c2a76.jpg b/public/uploads/xcximg/20241029/06b0365c05a7927838baccf59f8c2a76.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241029/06b0365c05a7927838baccf59f8c2a76.jpg differ diff --git a/public/uploads/xcximg/20241029/085012de191c286d47c6d6fb7cdbdad7.jpg b/public/uploads/xcximg/20241029/085012de191c286d47c6d6fb7cdbdad7.jpg new file mode 100644 index 0000000..ad63081 Binary files /dev/null and b/public/uploads/xcximg/20241029/085012de191c286d47c6d6fb7cdbdad7.jpg differ diff --git a/public/uploads/xcximg/20241029/0b6bbe6e7feb94f28112777ab3a179a1.jpg b/public/uploads/xcximg/20241029/0b6bbe6e7feb94f28112777ab3a179a1.jpg new file mode 100644 index 0000000..b95564f Binary files /dev/null and b/public/uploads/xcximg/20241029/0b6bbe6e7feb94f28112777ab3a179a1.jpg differ diff --git a/public/uploads/xcximg/20241029/0ce170e27f6db2394f5f684397672929.jpg b/public/uploads/xcximg/20241029/0ce170e27f6db2394f5f684397672929.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241029/0ce170e27f6db2394f5f684397672929.jpg differ diff --git a/public/uploads/xcximg/20241029/1222495dec7fa971b4322f26193dad07.jpg b/public/uploads/xcximg/20241029/1222495dec7fa971b4322f26193dad07.jpg new file mode 100644 index 0000000..f6dac26 Binary files /dev/null and b/public/uploads/xcximg/20241029/1222495dec7fa971b4322f26193dad07.jpg differ diff --git a/public/uploads/xcximg/20241029/1760b90d66eda21819861098927fcb3c.jpg b/public/uploads/xcximg/20241029/1760b90d66eda21819861098927fcb3c.jpg new file mode 100644 index 0000000..5440e18 Binary files /dev/null and b/public/uploads/xcximg/20241029/1760b90d66eda21819861098927fcb3c.jpg differ diff --git a/public/uploads/xcximg/20241029/1a092fe873b184f27a78dcaf7896a213.jpeg b/public/uploads/xcximg/20241029/1a092fe873b184f27a78dcaf7896a213.jpeg new file mode 100644 index 0000000..9ff68af Binary files /dev/null and b/public/uploads/xcximg/20241029/1a092fe873b184f27a78dcaf7896a213.jpeg differ diff --git a/public/uploads/xcximg/20241029/1ea39b00d07cafc908a646d902c82ed5.jpg b/public/uploads/xcximg/20241029/1ea39b00d07cafc908a646d902c82ed5.jpg new file mode 100644 index 0000000..435243f Binary files /dev/null and b/public/uploads/xcximg/20241029/1ea39b00d07cafc908a646d902c82ed5.jpg differ diff --git a/public/uploads/xcximg/20241029/1eb556c1d0cc9f0778cbf49e0467f7a8.jpg b/public/uploads/xcximg/20241029/1eb556c1d0cc9f0778cbf49e0467f7a8.jpg new file mode 100644 index 0000000..ab18aa6 Binary files /dev/null and b/public/uploads/xcximg/20241029/1eb556c1d0cc9f0778cbf49e0467f7a8.jpg differ diff --git a/public/uploads/xcximg/20241029/205ddb16918c5f289d0527db96af7239.jpg b/public/uploads/xcximg/20241029/205ddb16918c5f289d0527db96af7239.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241029/205ddb16918c5f289d0527db96af7239.jpg differ diff --git a/public/uploads/xcximg/20241029/22c512af77b12d9e98ac8bcaa81c45eb.jpeg b/public/uploads/xcximg/20241029/22c512af77b12d9e98ac8bcaa81c45eb.jpeg new file mode 100644 index 0000000..ea94f63 Binary files /dev/null and b/public/uploads/xcximg/20241029/22c512af77b12d9e98ac8bcaa81c45eb.jpeg differ diff --git a/public/uploads/xcximg/20241029/24312c68844b6a1e367313973204ac1d.png b/public/uploads/xcximg/20241029/24312c68844b6a1e367313973204ac1d.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241029/24312c68844b6a1e367313973204ac1d.png differ diff --git a/public/uploads/xcximg/20241029/296c1dd0c0a88b65773c374353a3080b.jpg b/public/uploads/xcximg/20241029/296c1dd0c0a88b65773c374353a3080b.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241029/296c1dd0c0a88b65773c374353a3080b.jpg differ diff --git a/public/uploads/xcximg/20241029/298d68190931c5a27e44890b09293374.jpg b/public/uploads/xcximg/20241029/298d68190931c5a27e44890b09293374.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241029/298d68190931c5a27e44890b09293374.jpg differ diff --git a/public/uploads/xcximg/20241029/2b156941bad0cf8199f57558461e1081.png b/public/uploads/xcximg/20241029/2b156941bad0cf8199f57558461e1081.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241029/2b156941bad0cf8199f57558461e1081.png differ diff --git a/public/uploads/xcximg/20241029/2bb22efbba787f431e95d83e27cb36fa.jpg b/public/uploads/xcximg/20241029/2bb22efbba787f431e95d83e27cb36fa.jpg new file mode 100644 index 0000000..4b97462 Binary files /dev/null and b/public/uploads/xcximg/20241029/2bb22efbba787f431e95d83e27cb36fa.jpg differ diff --git a/public/uploads/xcximg/20241029/2f955a3a6ca5f467aff682f890f797a9.jpg b/public/uploads/xcximg/20241029/2f955a3a6ca5f467aff682f890f797a9.jpg new file mode 100644 index 0000000..ab18aa6 Binary files /dev/null and b/public/uploads/xcximg/20241029/2f955a3a6ca5f467aff682f890f797a9.jpg differ diff --git a/public/uploads/xcximg/20241029/3e3f8d89c528b39db60e222f4ce2367b.jpg b/public/uploads/xcximg/20241029/3e3f8d89c528b39db60e222f4ce2367b.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241029/3e3f8d89c528b39db60e222f4ce2367b.jpg differ diff --git a/public/uploads/xcximg/20241029/3f068fe0df75eb248571f9ff708c9990.jpg b/public/uploads/xcximg/20241029/3f068fe0df75eb248571f9ff708c9990.jpg new file mode 100644 index 0000000..15da5fa Binary files /dev/null and b/public/uploads/xcximg/20241029/3f068fe0df75eb248571f9ff708c9990.jpg differ diff --git a/public/uploads/xcximg/20241029/4386fb6dee8253be0b2f41365b13bc17.jpg b/public/uploads/xcximg/20241029/4386fb6dee8253be0b2f41365b13bc17.jpg new file mode 100644 index 0000000..f8ccc1d Binary files /dev/null and b/public/uploads/xcximg/20241029/4386fb6dee8253be0b2f41365b13bc17.jpg differ diff --git a/public/uploads/xcximg/20241029/45a96da0df1b70db14300ade2eded1d1.jpeg b/public/uploads/xcximg/20241029/45a96da0df1b70db14300ade2eded1d1.jpeg new file mode 100644 index 0000000..6e3662b Binary files /dev/null and b/public/uploads/xcximg/20241029/45a96da0df1b70db14300ade2eded1d1.jpeg differ diff --git a/public/uploads/xcximg/20241029/4967c83efdc2bdd5390e8f1c998c7d3f.png b/public/uploads/xcximg/20241029/4967c83efdc2bdd5390e8f1c998c7d3f.png new file mode 100644 index 0000000..047b764 Binary files /dev/null and b/public/uploads/xcximg/20241029/4967c83efdc2bdd5390e8f1c998c7d3f.png differ diff --git a/public/uploads/xcximg/20241029/4a93cd2fe8c56aeb18cb87de05571fd3.jpeg b/public/uploads/xcximg/20241029/4a93cd2fe8c56aeb18cb87de05571fd3.jpeg new file mode 100644 index 0000000..fe5dd45 Binary files /dev/null and b/public/uploads/xcximg/20241029/4a93cd2fe8c56aeb18cb87de05571fd3.jpeg differ diff --git a/public/uploads/xcximg/20241029/50f638d79b0af1f74418661536a69d22.jpg b/public/uploads/xcximg/20241029/50f638d79b0af1f74418661536a69d22.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241029/50f638d79b0af1f74418661536a69d22.jpg differ diff --git a/public/uploads/xcximg/20241029/519124a69e53a664b0d08c5e8003966f.jpg b/public/uploads/xcximg/20241029/519124a69e53a664b0d08c5e8003966f.jpg new file mode 100644 index 0000000..ab18aa6 Binary files /dev/null and b/public/uploads/xcximg/20241029/519124a69e53a664b0d08c5e8003966f.jpg differ diff --git a/public/uploads/xcximg/20241029/52019cc3375df2e024d2b1e3ba92e0aa.jpg b/public/uploads/xcximg/20241029/52019cc3375df2e024d2b1e3ba92e0aa.jpg new file mode 100644 index 0000000..4b97462 Binary files /dev/null and b/public/uploads/xcximg/20241029/52019cc3375df2e024d2b1e3ba92e0aa.jpg differ diff --git a/public/uploads/xcximg/20241029/53b996e75420dc6280c28778c23b7440.png b/public/uploads/xcximg/20241029/53b996e75420dc6280c28778c23b7440.png new file mode 100644 index 0000000..7956bf0 Binary files /dev/null and b/public/uploads/xcximg/20241029/53b996e75420dc6280c28778c23b7440.png differ diff --git a/public/uploads/xcximg/20241029/5c611d30750f559455ca121cb881224b.jpg b/public/uploads/xcximg/20241029/5c611d30750f559455ca121cb881224b.jpg new file mode 100644 index 0000000..7471652 Binary files /dev/null and b/public/uploads/xcximg/20241029/5c611d30750f559455ca121cb881224b.jpg differ diff --git a/public/uploads/xcximg/20241029/5c899e1400bd2fd430e2beee6e76f0cb.jpeg b/public/uploads/xcximg/20241029/5c899e1400bd2fd430e2beee6e76f0cb.jpeg new file mode 100644 index 0000000..036d57e Binary files /dev/null and b/public/uploads/xcximg/20241029/5c899e1400bd2fd430e2beee6e76f0cb.jpeg differ diff --git a/public/uploads/xcximg/20241029/5d8200512df4ecd2a6ba5a9d0f9d5e7e.jpg b/public/uploads/xcximg/20241029/5d8200512df4ecd2a6ba5a9d0f9d5e7e.jpg new file mode 100644 index 0000000..4d328b7 Binary files /dev/null and b/public/uploads/xcximg/20241029/5d8200512df4ecd2a6ba5a9d0f9d5e7e.jpg differ diff --git a/public/uploads/xcximg/20241029/5db29ee50db4f4d203eb640c3abf4831.jpeg b/public/uploads/xcximg/20241029/5db29ee50db4f4d203eb640c3abf4831.jpeg new file mode 100644 index 0000000..29ab4c8 Binary files /dev/null and b/public/uploads/xcximg/20241029/5db29ee50db4f4d203eb640c3abf4831.jpeg differ diff --git a/public/uploads/xcximg/20241029/6165fe5a07227fea277b5901da20eb62.jpg b/public/uploads/xcximg/20241029/6165fe5a07227fea277b5901da20eb62.jpg new file mode 100644 index 0000000..1f7f99f Binary files /dev/null and b/public/uploads/xcximg/20241029/6165fe5a07227fea277b5901da20eb62.jpg differ diff --git a/public/uploads/xcximg/20241029/6cbf3c8929143e80a6766abfa16703ce.jpg b/public/uploads/xcximg/20241029/6cbf3c8929143e80a6766abfa16703ce.jpg new file mode 100644 index 0000000..4b97462 Binary files /dev/null and b/public/uploads/xcximg/20241029/6cbf3c8929143e80a6766abfa16703ce.jpg differ diff --git a/public/uploads/xcximg/20241029/6fca8f3d6e07f148d77490b3ae3f1293.jpeg b/public/uploads/xcximg/20241029/6fca8f3d6e07f148d77490b3ae3f1293.jpeg new file mode 100644 index 0000000..cb9ef9d Binary files /dev/null and b/public/uploads/xcximg/20241029/6fca8f3d6e07f148d77490b3ae3f1293.jpeg differ diff --git a/public/uploads/xcximg/20241029/70dfcc3a52f3f3235b3a68412a8db22f.jpg b/public/uploads/xcximg/20241029/70dfcc3a52f3f3235b3a68412a8db22f.jpg new file mode 100644 index 0000000..1171640 Binary files /dev/null and b/public/uploads/xcximg/20241029/70dfcc3a52f3f3235b3a68412a8db22f.jpg differ diff --git a/public/uploads/xcximg/20241029/7207e281f78fa4bdfc8edd6d897e729a.jpeg b/public/uploads/xcximg/20241029/7207e281f78fa4bdfc8edd6d897e729a.jpeg new file mode 100644 index 0000000..5862027 Binary files /dev/null and b/public/uploads/xcximg/20241029/7207e281f78fa4bdfc8edd6d897e729a.jpeg differ diff --git a/public/uploads/xcximg/20241029/7d71d3b57799693c79360a29004712b8.jpg b/public/uploads/xcximg/20241029/7d71d3b57799693c79360a29004712b8.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241029/7d71d3b57799693c79360a29004712b8.jpg differ diff --git a/public/uploads/xcximg/20241029/7f71f0ac36865fdb1d129e73a9985bac.jpg b/public/uploads/xcximg/20241029/7f71f0ac36865fdb1d129e73a9985bac.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241029/7f71f0ac36865fdb1d129e73a9985bac.jpg differ diff --git a/public/uploads/xcximg/20241029/8614a8327e6f830731365569e79f7c25.jpg b/public/uploads/xcximg/20241029/8614a8327e6f830731365569e79f7c25.jpg new file mode 100644 index 0000000..f11ba55 Binary files /dev/null and b/public/uploads/xcximg/20241029/8614a8327e6f830731365569e79f7c25.jpg differ diff --git a/public/uploads/xcximg/20241029/864ce24d935cd5e9c5a3539fb461df6a.jpg b/public/uploads/xcximg/20241029/864ce24d935cd5e9c5a3539fb461df6a.jpg new file mode 100644 index 0000000..ab18aa6 Binary files /dev/null and b/public/uploads/xcximg/20241029/864ce24d935cd5e9c5a3539fb461df6a.jpg differ diff --git a/public/uploads/xcximg/20241029/87f51bc3bbc2ac87db3786293480901b.jpg b/public/uploads/xcximg/20241029/87f51bc3bbc2ac87db3786293480901b.jpg new file mode 100644 index 0000000..4d328b7 Binary files /dev/null and b/public/uploads/xcximg/20241029/87f51bc3bbc2ac87db3786293480901b.jpg differ diff --git a/public/uploads/xcximg/20241029/886d975723a13062c596b2c6a333591a.jpeg b/public/uploads/xcximg/20241029/886d975723a13062c596b2c6a333591a.jpeg new file mode 100644 index 0000000..d6157f5 Binary files /dev/null and b/public/uploads/xcximg/20241029/886d975723a13062c596b2c6a333591a.jpeg differ diff --git a/public/uploads/xcximg/20241029/8db882d44065952110ffd05f1c50be2a.png b/public/uploads/xcximg/20241029/8db882d44065952110ffd05f1c50be2a.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241029/8db882d44065952110ffd05f1c50be2a.png differ diff --git a/public/uploads/xcximg/20241029/94a4971e6b99e830435c00279caf8fd2.jpg b/public/uploads/xcximg/20241029/94a4971e6b99e830435c00279caf8fd2.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241029/94a4971e6b99e830435c00279caf8fd2.jpg differ diff --git a/public/uploads/xcximg/20241029/94b16e1e082479c13ddae32812f237dd.jpg b/public/uploads/xcximg/20241029/94b16e1e082479c13ddae32812f237dd.jpg new file mode 100644 index 0000000..b25690a Binary files /dev/null and b/public/uploads/xcximg/20241029/94b16e1e082479c13ddae32812f237dd.jpg differ diff --git a/public/uploads/xcximg/20241029/969a640770b140f0eb856979c07b4399.jpeg b/public/uploads/xcximg/20241029/969a640770b140f0eb856979c07b4399.jpeg new file mode 100644 index 0000000..8bf3159 Binary files /dev/null and b/public/uploads/xcximg/20241029/969a640770b140f0eb856979c07b4399.jpeg differ diff --git a/public/uploads/xcximg/20241029/a1afa1f050b9c5e19416fcb3eba4ee04.jpg b/public/uploads/xcximg/20241029/a1afa1f050b9c5e19416fcb3eba4ee04.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241029/a1afa1f050b9c5e19416fcb3eba4ee04.jpg differ diff --git a/public/uploads/xcximg/20241029/a2c5b38808d4702c3ac28e00f8a7d2c2.jpg b/public/uploads/xcximg/20241029/a2c5b38808d4702c3ac28e00f8a7d2c2.jpg new file mode 100644 index 0000000..e92f3dc Binary files /dev/null and b/public/uploads/xcximg/20241029/a2c5b38808d4702c3ac28e00f8a7d2c2.jpg differ diff --git a/public/uploads/xcximg/20241029/a3ce6d266f52bf9dcfc64f6f7927285f.jpg b/public/uploads/xcximg/20241029/a3ce6d266f52bf9dcfc64f6f7927285f.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241029/a3ce6d266f52bf9dcfc64f6f7927285f.jpg differ diff --git a/public/uploads/xcximg/20241029/a6d8f3e6ffdf92ec08709b80eb23d704.png b/public/uploads/xcximg/20241029/a6d8f3e6ffdf92ec08709b80eb23d704.png new file mode 100644 index 0000000..4895d11 Binary files /dev/null and b/public/uploads/xcximg/20241029/a6d8f3e6ffdf92ec08709b80eb23d704.png differ diff --git a/public/uploads/xcximg/20241029/ab85d3cf0940531723f20087aeb3f505.jpeg b/public/uploads/xcximg/20241029/ab85d3cf0940531723f20087aeb3f505.jpeg new file mode 100644 index 0000000..95bb966 Binary files /dev/null and b/public/uploads/xcximg/20241029/ab85d3cf0940531723f20087aeb3f505.jpeg differ diff --git a/public/uploads/xcximg/20241029/abffbadee9d22d301ba306c44e5765a5.jpg b/public/uploads/xcximg/20241029/abffbadee9d22d301ba306c44e5765a5.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241029/abffbadee9d22d301ba306c44e5765a5.jpg differ diff --git a/public/uploads/xcximg/20241029/ad3888d1df50750a2975b7f8200afcbe.jpg b/public/uploads/xcximg/20241029/ad3888d1df50750a2975b7f8200afcbe.jpg new file mode 100644 index 0000000..d317a89 Binary files /dev/null and b/public/uploads/xcximg/20241029/ad3888d1df50750a2975b7f8200afcbe.jpg differ diff --git a/public/uploads/xcximg/20241029/ae8be6672be41fa3055bf511b11adf2b.jpg b/public/uploads/xcximg/20241029/ae8be6672be41fa3055bf511b11adf2b.jpg new file mode 100644 index 0000000..791c2ed Binary files /dev/null and b/public/uploads/xcximg/20241029/ae8be6672be41fa3055bf511b11adf2b.jpg differ diff --git a/public/uploads/xcximg/20241029/b1ba663b7030fda0663fa30438628fdd.jpeg b/public/uploads/xcximg/20241029/b1ba663b7030fda0663fa30438628fdd.jpeg new file mode 100644 index 0000000..923940e Binary files /dev/null and b/public/uploads/xcximg/20241029/b1ba663b7030fda0663fa30438628fdd.jpeg differ diff --git a/public/uploads/xcximg/20241029/b27c7b9d69abdb1fbc7ab06d496d4afe.jpg b/public/uploads/xcximg/20241029/b27c7b9d69abdb1fbc7ab06d496d4afe.jpg new file mode 100644 index 0000000..2307989 Binary files /dev/null and b/public/uploads/xcximg/20241029/b27c7b9d69abdb1fbc7ab06d496d4afe.jpg differ diff --git a/public/uploads/xcximg/20241029/b5406117ae23ac52d8a16ac8c677a245.jpg b/public/uploads/xcximg/20241029/b5406117ae23ac52d8a16ac8c677a245.jpg new file mode 100644 index 0000000..8297fc9 Binary files /dev/null and b/public/uploads/xcximg/20241029/b5406117ae23ac52d8a16ac8c677a245.jpg differ diff --git a/public/uploads/xcximg/20241029/b6a09f3fe46385f5f765e2bad71d38a6.jpg b/public/uploads/xcximg/20241029/b6a09f3fe46385f5f765e2bad71d38a6.jpg new file mode 100644 index 0000000..1039d4b Binary files /dev/null and b/public/uploads/xcximg/20241029/b6a09f3fe46385f5f765e2bad71d38a6.jpg differ diff --git a/public/uploads/xcximg/20241029/b98ca6da87b5ce8f9563e33376aa4b65.jpg b/public/uploads/xcximg/20241029/b98ca6da87b5ce8f9563e33376aa4b65.jpg new file mode 100644 index 0000000..84c9c76 Binary files /dev/null and b/public/uploads/xcximg/20241029/b98ca6da87b5ce8f9563e33376aa4b65.jpg differ diff --git a/public/uploads/xcximg/20241029/ba6fa3609c0efbaa5dd57c7e0b98d48c.jpg b/public/uploads/xcximg/20241029/ba6fa3609c0efbaa5dd57c7e0b98d48c.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241029/ba6fa3609c0efbaa5dd57c7e0b98d48c.jpg differ diff --git a/public/uploads/xcximg/20241029/bc887678d5cd043cea0755765d17489f.png b/public/uploads/xcximg/20241029/bc887678d5cd043cea0755765d17489f.png new file mode 100644 index 0000000..436b097 Binary files /dev/null and b/public/uploads/xcximg/20241029/bc887678d5cd043cea0755765d17489f.png differ diff --git a/public/uploads/xcximg/20241029/bf9ee39aaf144444f39710a25a6bb391.jpg b/public/uploads/xcximg/20241029/bf9ee39aaf144444f39710a25a6bb391.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241029/bf9ee39aaf144444f39710a25a6bb391.jpg differ diff --git a/public/uploads/xcximg/20241029/c1b08aa04ba70a83aa4609a53d378412.jpg b/public/uploads/xcximg/20241029/c1b08aa04ba70a83aa4609a53d378412.jpg new file mode 100644 index 0000000..e92f3dc Binary files /dev/null and b/public/uploads/xcximg/20241029/c1b08aa04ba70a83aa4609a53d378412.jpg differ diff --git a/public/uploads/xcximg/20241029/c34ef320aa0dc36d7172dfa29c09f655.jpg b/public/uploads/xcximg/20241029/c34ef320aa0dc36d7172dfa29c09f655.jpg new file mode 100644 index 0000000..4b97462 Binary files /dev/null and b/public/uploads/xcximg/20241029/c34ef320aa0dc36d7172dfa29c09f655.jpg differ diff --git a/public/uploads/xcximg/20241029/c422314ac457705754dd587ebf781f8a.jpg b/public/uploads/xcximg/20241029/c422314ac457705754dd587ebf781f8a.jpg new file mode 100644 index 0000000..16175bb Binary files /dev/null and b/public/uploads/xcximg/20241029/c422314ac457705754dd587ebf781f8a.jpg differ diff --git a/public/uploads/xcximg/20241029/c53151504cf291b051039db0fcfbe86d.png b/public/uploads/xcximg/20241029/c53151504cf291b051039db0fcfbe86d.png new file mode 100644 index 0000000..b270fc3 Binary files /dev/null and b/public/uploads/xcximg/20241029/c53151504cf291b051039db0fcfbe86d.png differ diff --git a/public/uploads/xcximg/20241029/c703c2646eb389be1c60e01cb52dab45.jpg b/public/uploads/xcximg/20241029/c703c2646eb389be1c60e01cb52dab45.jpg new file mode 100644 index 0000000..b4c78df Binary files /dev/null and b/public/uploads/xcximg/20241029/c703c2646eb389be1c60e01cb52dab45.jpg differ diff --git a/public/uploads/xcximg/20241029/cda9a3c23829e38840ea330a1b5d636e.jpeg b/public/uploads/xcximg/20241029/cda9a3c23829e38840ea330a1b5d636e.jpeg new file mode 100644 index 0000000..51cc6f2 Binary files /dev/null and b/public/uploads/xcximg/20241029/cda9a3c23829e38840ea330a1b5d636e.jpeg differ diff --git a/public/uploads/xcximg/20241029/d26618585d112b02c81240aabdb43963.jpg b/public/uploads/xcximg/20241029/d26618585d112b02c81240aabdb43963.jpg new file mode 100644 index 0000000..791c2ed Binary files /dev/null and b/public/uploads/xcximg/20241029/d26618585d112b02c81240aabdb43963.jpg differ diff --git a/public/uploads/xcximg/20241029/d5d4778661e2161bc4fdb8aa20dd2161.png b/public/uploads/xcximg/20241029/d5d4778661e2161bc4fdb8aa20dd2161.png new file mode 100644 index 0000000..ee5f391 Binary files /dev/null and b/public/uploads/xcximg/20241029/d5d4778661e2161bc4fdb8aa20dd2161.png differ diff --git a/public/uploads/xcximg/20241029/d8348a894b44b090486e5e02ef1919e6.jpeg b/public/uploads/xcximg/20241029/d8348a894b44b090486e5e02ef1919e6.jpeg new file mode 100644 index 0000000..22dd4f1 Binary files /dev/null and b/public/uploads/xcximg/20241029/d8348a894b44b090486e5e02ef1919e6.jpeg differ diff --git a/public/uploads/xcximg/20241029/da3ca4b4f2822c8864d2028d29d8740f.jpg b/public/uploads/xcximg/20241029/da3ca4b4f2822c8864d2028d29d8740f.jpg new file mode 100644 index 0000000..de9c150 Binary files /dev/null and b/public/uploads/xcximg/20241029/da3ca4b4f2822c8864d2028d29d8740f.jpg differ diff --git a/public/uploads/xcximg/20241029/defdd5af0487581dbf2dcc3352815787.jpeg b/public/uploads/xcximg/20241029/defdd5af0487581dbf2dcc3352815787.jpeg new file mode 100644 index 0000000..f6c4cd9 Binary files /dev/null and b/public/uploads/xcximg/20241029/defdd5af0487581dbf2dcc3352815787.jpeg differ diff --git a/public/uploads/xcximg/20241029/e30354237e4ca3ece8a55c2011291fe0.jpg b/public/uploads/xcximg/20241029/e30354237e4ca3ece8a55c2011291fe0.jpg new file mode 100644 index 0000000..e92f3dc Binary files /dev/null and b/public/uploads/xcximg/20241029/e30354237e4ca3ece8a55c2011291fe0.jpg differ diff --git a/public/uploads/xcximg/20241029/e6a56e9bd316b2b7c258aa67b1dcad77.jpg b/public/uploads/xcximg/20241029/e6a56e9bd316b2b7c258aa67b1dcad77.jpg new file mode 100644 index 0000000..ad63081 Binary files /dev/null and b/public/uploads/xcximg/20241029/e6a56e9bd316b2b7c258aa67b1dcad77.jpg differ diff --git a/public/uploads/xcximg/20241029/eb71551557ef03cca169e77638ce9c00.jpg b/public/uploads/xcximg/20241029/eb71551557ef03cca169e77638ce9c00.jpg new file mode 100644 index 0000000..1ae4469 Binary files /dev/null and b/public/uploads/xcximg/20241029/eb71551557ef03cca169e77638ce9c00.jpg differ diff --git a/public/uploads/xcximg/20241029/ebd5b4364fa664ad4450019b4bc7a704.jpeg b/public/uploads/xcximg/20241029/ebd5b4364fa664ad4450019b4bc7a704.jpeg new file mode 100644 index 0000000..43d6d40 Binary files /dev/null and b/public/uploads/xcximg/20241029/ebd5b4364fa664ad4450019b4bc7a704.jpeg differ diff --git a/public/uploads/xcximg/20241029/ee0c9c68ca818ae6bbc5028438fe30f2.jpg b/public/uploads/xcximg/20241029/ee0c9c68ca818ae6bbc5028438fe30f2.jpg new file mode 100644 index 0000000..cb2d29d Binary files /dev/null and b/public/uploads/xcximg/20241029/ee0c9c68ca818ae6bbc5028438fe30f2.jpg differ diff --git a/public/uploads/xcximg/20241029/f00b02d6d00152c88c85321668393f13.jpg b/public/uploads/xcximg/20241029/f00b02d6d00152c88c85321668393f13.jpg new file mode 100644 index 0000000..f694611 Binary files /dev/null and b/public/uploads/xcximg/20241029/f00b02d6d00152c88c85321668393f13.jpg differ diff --git a/public/uploads/xcximg/20241029/f688c9c64d6b837a1f06fe76a1050111.jpeg b/public/uploads/xcximg/20241029/f688c9c64d6b837a1f06fe76a1050111.jpeg new file mode 100644 index 0000000..2c02ec9 Binary files /dev/null and b/public/uploads/xcximg/20241029/f688c9c64d6b837a1f06fe76a1050111.jpeg differ diff --git a/public/uploads/xcximg/20241029/fa4f8f897578fe2229f74aee6f585310.jpg b/public/uploads/xcximg/20241029/fa4f8f897578fe2229f74aee6f585310.jpg new file mode 100644 index 0000000..d317a89 Binary files /dev/null and b/public/uploads/xcximg/20241029/fa4f8f897578fe2229f74aee6f585310.jpg differ diff --git a/public/uploads/xcximg/20241029/fa85ab0d7c7325117fe7ef4fd90ddab4.jpeg b/public/uploads/xcximg/20241029/fa85ab0d7c7325117fe7ef4fd90ddab4.jpeg new file mode 100644 index 0000000..b9279b8 Binary files /dev/null and b/public/uploads/xcximg/20241029/fa85ab0d7c7325117fe7ef4fd90ddab4.jpeg differ diff --git a/public/uploads/xcximg/20241029/fb83cc81fbf8ae626550849afb83efe6.jpeg b/public/uploads/xcximg/20241029/fb83cc81fbf8ae626550849afb83efe6.jpeg new file mode 100644 index 0000000..81e65a2 Binary files /dev/null and b/public/uploads/xcximg/20241029/fb83cc81fbf8ae626550849afb83efe6.jpeg differ diff --git a/public/uploads/xcximg/20241031/11317bb7cb78d4f305dc27406ec7588c.png b/public/uploads/xcximg/20241031/11317bb7cb78d4f305dc27406ec7588c.png new file mode 100644 index 0000000..a988152 Binary files /dev/null and b/public/uploads/xcximg/20241031/11317bb7cb78d4f305dc27406ec7588c.png differ diff --git a/public/uploads/xcximg/20241031/120921c969d9a2febc40b35eab07b9e9.png b/public/uploads/xcximg/20241031/120921c969d9a2febc40b35eab07b9e9.png new file mode 100644 index 0000000..18b418e Binary files /dev/null and b/public/uploads/xcximg/20241031/120921c969d9a2febc40b35eab07b9e9.png differ diff --git a/public/uploads/xcximg/20241031/24fc210d8bddd4a2d9a4b1813dfa9300.png b/public/uploads/xcximg/20241031/24fc210d8bddd4a2d9a4b1813dfa9300.png new file mode 100644 index 0000000..dbab4af Binary files /dev/null and b/public/uploads/xcximg/20241031/24fc210d8bddd4a2d9a4b1813dfa9300.png differ diff --git a/public/uploads/xcximg/20241031/39edf061dec4911b6fc3d7aab27a5dea.png b/public/uploads/xcximg/20241031/39edf061dec4911b6fc3d7aab27a5dea.png new file mode 100644 index 0000000..e59cfbe Binary files /dev/null and b/public/uploads/xcximg/20241031/39edf061dec4911b6fc3d7aab27a5dea.png differ diff --git a/public/uploads/xcximg/20241031/3d52af626f2287df0d553b33f03fc1a5.png b/public/uploads/xcximg/20241031/3d52af626f2287df0d553b33f03fc1a5.png new file mode 100644 index 0000000..e59cfbe Binary files /dev/null and b/public/uploads/xcximg/20241031/3d52af626f2287df0d553b33f03fc1a5.png differ diff --git a/public/uploads/xcximg/20241031/3e5155da30f0ccab828caccfcf92a008.png b/public/uploads/xcximg/20241031/3e5155da30f0ccab828caccfcf92a008.png new file mode 100644 index 0000000..e59cfbe Binary files /dev/null and b/public/uploads/xcximg/20241031/3e5155da30f0ccab828caccfcf92a008.png differ diff --git a/public/uploads/xcximg/20241031/679b84d42e6ab34014ce309e090c4197.png b/public/uploads/xcximg/20241031/679b84d42e6ab34014ce309e090c4197.png new file mode 100644 index 0000000..e739d9f Binary files /dev/null and b/public/uploads/xcximg/20241031/679b84d42e6ab34014ce309e090c4197.png differ diff --git a/public/uploads/xcximg/20241031/9ea9ff0a89db5da5e6d16a6dea11d1ef.png b/public/uploads/xcximg/20241031/9ea9ff0a89db5da5e6d16a6dea11d1ef.png new file mode 100644 index 0000000..dfa7649 Binary files /dev/null and b/public/uploads/xcximg/20241031/9ea9ff0a89db5da5e6d16a6dea11d1ef.png differ diff --git a/public/uploads/xcximg/20241031/a0a1d199e4a8a84c6bd9d239e228f04d.png b/public/uploads/xcximg/20241031/a0a1d199e4a8a84c6bd9d239e228f04d.png new file mode 100644 index 0000000..e59cfbe Binary files /dev/null and b/public/uploads/xcximg/20241031/a0a1d199e4a8a84c6bd9d239e228f04d.png differ diff --git a/public/uploads/xcximg/20241031/b15d40ce2e52dca589e217d750fbca1d.png b/public/uploads/xcximg/20241031/b15d40ce2e52dca589e217d750fbca1d.png new file mode 100644 index 0000000..e739d9f Binary files /dev/null and b/public/uploads/xcximg/20241031/b15d40ce2e52dca589e217d750fbca1d.png differ diff --git a/public/uploads/xcximg/20241031/b562d3eea7c61957336712c3341a587a.jpg b/public/uploads/xcximg/20241031/b562d3eea7c61957336712c3341a587a.jpg new file mode 100644 index 0000000..46732fb Binary files /dev/null and b/public/uploads/xcximg/20241031/b562d3eea7c61957336712c3341a587a.jpg differ diff --git a/public/uploads/xcximg/20241031/c5b96e54c7f59d886d77104251b56f01.png b/public/uploads/xcximg/20241031/c5b96e54c7f59d886d77104251b56f01.png new file mode 100644 index 0000000..dbab4af Binary files /dev/null and b/public/uploads/xcximg/20241031/c5b96e54c7f59d886d77104251b56f01.png differ diff --git a/public/uploads/xcximg/20241031/c6eca6e91b72a9ea3c97362185f0c9c5.jpg b/public/uploads/xcximg/20241031/c6eca6e91b72a9ea3c97362185f0c9c5.jpg new file mode 100644 index 0000000..cbbfb71 Binary files /dev/null and b/public/uploads/xcximg/20241031/c6eca6e91b72a9ea3c97362185f0c9c5.jpg differ diff --git a/public/uploads/xcximg/20241031/c72947293fa09c550b0bb9f9bc7c460d.jpg b/public/uploads/xcximg/20241031/c72947293fa09c550b0bb9f9bc7c460d.jpg new file mode 100644 index 0000000..8a4651c Binary files /dev/null and b/public/uploads/xcximg/20241031/c72947293fa09c550b0bb9f9bc7c460d.jpg differ diff --git a/public/uploads/xcximg/20241031/c8fd2dcb2e1967420d759dfb2ad71a9f.jpg b/public/uploads/xcximg/20241031/c8fd2dcb2e1967420d759dfb2ad71a9f.jpg new file mode 100644 index 0000000..b98a543 Binary files /dev/null and b/public/uploads/xcximg/20241031/c8fd2dcb2e1967420d759dfb2ad71a9f.jpg differ diff --git a/public/uploads/xcximg/20241031/d03421aa7fac922a2bd9af501e948f6b.png b/public/uploads/xcximg/20241031/d03421aa7fac922a2bd9af501e948f6b.png new file mode 100644 index 0000000..e739d9f Binary files /dev/null and b/public/uploads/xcximg/20241031/d03421aa7fac922a2bd9af501e948f6b.png differ diff --git a/public/uploads/xcximg/20241031/d44c6d9ff7ebddea07b6d35493d2d33c.png b/public/uploads/xcximg/20241031/d44c6d9ff7ebddea07b6d35493d2d33c.png new file mode 100644 index 0000000..e739d9f Binary files /dev/null and b/public/uploads/xcximg/20241031/d44c6d9ff7ebddea07b6d35493d2d33c.png differ diff --git a/public/uploads/xcximg/20241031/df18e39d1330d308e59566f732962fea.png b/public/uploads/xcximg/20241031/df18e39d1330d308e59566f732962fea.png new file mode 100644 index 0000000..18b418e Binary files /dev/null and b/public/uploads/xcximg/20241031/df18e39d1330d308e59566f732962fea.png differ diff --git a/public/uploads/xcximg/20241031/e816bfd23f236153f5568e5edeba6dbc.png b/public/uploads/xcximg/20241031/e816bfd23f236153f5568e5edeba6dbc.png new file mode 100644 index 0000000..e739d9f Binary files /dev/null and b/public/uploads/xcximg/20241031/e816bfd23f236153f5568e5edeba6dbc.png differ diff --git a/public/uploads/xcximg/20241031/e8e0b460c3eca159d0e3318a7fe5ffe1.png b/public/uploads/xcximg/20241031/e8e0b460c3eca159d0e3318a7fe5ffe1.png new file mode 100644 index 0000000..e59cfbe Binary files /dev/null and b/public/uploads/xcximg/20241031/e8e0b460c3eca159d0e3318a7fe5ffe1.png differ diff --git a/public/uploads/xcximg/20241031/ea909de868ccc94afa4bdf3478c6ca1d.png b/public/uploads/xcximg/20241031/ea909de868ccc94afa4bdf3478c6ca1d.png new file mode 100644 index 0000000..fe50733 Binary files /dev/null and b/public/uploads/xcximg/20241031/ea909de868ccc94afa4bdf3478c6ca1d.png differ diff --git a/public/uploads/xcximg/20241102/4ad37497e2efc32b1ea2a754d6605a6a.jpeg b/public/uploads/xcximg/20241102/4ad37497e2efc32b1ea2a754d6605a6a.jpeg new file mode 100644 index 0000000..0f3ff74 Binary files /dev/null and b/public/uploads/xcximg/20241102/4ad37497e2efc32b1ea2a754d6605a6a.jpeg differ diff --git a/public/uploads/xcximg/20241102/5b0e2579695890a30559f90f3eaaa9a0.jpeg b/public/uploads/xcximg/20241102/5b0e2579695890a30559f90f3eaaa9a0.jpeg new file mode 100644 index 0000000..242935b Binary files /dev/null and b/public/uploads/xcximg/20241102/5b0e2579695890a30559f90f3eaaa9a0.jpeg differ diff --git a/public/uploads/xcximg/20241102/6fe183e147a043433a34b549cd361af3.jpeg b/public/uploads/xcximg/20241102/6fe183e147a043433a34b549cd361af3.jpeg new file mode 100644 index 0000000..976a3f1 Binary files /dev/null and b/public/uploads/xcximg/20241102/6fe183e147a043433a34b549cd361af3.jpeg differ diff --git a/public/uploads/xcximg/20241102/74c7721d30a85df724513fd878140bd8.jpeg b/public/uploads/xcximg/20241102/74c7721d30a85df724513fd878140bd8.jpeg new file mode 100644 index 0000000..216e573 Binary files /dev/null and b/public/uploads/xcximg/20241102/74c7721d30a85df724513fd878140bd8.jpeg differ diff --git a/public/uploads/xcximg/20241102/7fe95f2bed5709cbc3bd819025a7ce8b.png b/public/uploads/xcximg/20241102/7fe95f2bed5709cbc3bd819025a7ce8b.png new file mode 100644 index 0000000..96217a0 Binary files /dev/null and b/public/uploads/xcximg/20241102/7fe95f2bed5709cbc3bd819025a7ce8b.png differ diff --git a/public/uploads/xcximg/20241102/8752378fc81c851c3594aad89c3275e8.jpeg b/public/uploads/xcximg/20241102/8752378fc81c851c3594aad89c3275e8.jpeg new file mode 100644 index 0000000..26c5ba2 Binary files /dev/null and b/public/uploads/xcximg/20241102/8752378fc81c851c3594aad89c3275e8.jpeg differ diff --git a/public/uploads/xcximg/20241102/ab12314b545abdf699012d56706ae36d.png b/public/uploads/xcximg/20241102/ab12314b545abdf699012d56706ae36d.png new file mode 100644 index 0000000..96217a0 Binary files /dev/null and b/public/uploads/xcximg/20241102/ab12314b545abdf699012d56706ae36d.png differ diff --git a/public/uploads/xcximg/20241102/af31383b96f57da36af0264e4f07bcd5.png b/public/uploads/xcximg/20241102/af31383b96f57da36af0264e4f07bcd5.png new file mode 100644 index 0000000..96217a0 Binary files /dev/null and b/public/uploads/xcximg/20241102/af31383b96f57da36af0264e4f07bcd5.png differ diff --git a/public/uploads/xcximg/20241102/e6a6eb0fd62bce3aa770ee482ffe1e76.png b/public/uploads/xcximg/20241102/e6a6eb0fd62bce3aa770ee482ffe1e76.png new file mode 100644 index 0000000..96217a0 Binary files /dev/null and b/public/uploads/xcximg/20241102/e6a6eb0fd62bce3aa770ee482ffe1e76.png differ diff --git a/public/uploads/xcximg/20241104/002326e86b00ea9e4827d15fd9f005a3.jpg b/public/uploads/xcximg/20241104/002326e86b00ea9e4827d15fd9f005a3.jpg new file mode 100644 index 0000000..78fa77a Binary files /dev/null and b/public/uploads/xcximg/20241104/002326e86b00ea9e4827d15fd9f005a3.jpg differ diff --git a/public/uploads/xcximg/20241104/1ceab13540a049aa85872e61b6e7a55c.jpg b/public/uploads/xcximg/20241104/1ceab13540a049aa85872e61b6e7a55c.jpg new file mode 100644 index 0000000..08e7035 Binary files /dev/null and b/public/uploads/xcximg/20241104/1ceab13540a049aa85872e61b6e7a55c.jpg differ diff --git a/public/uploads/xcximg/20241104/1e3c8f03f646b3dca204a59545ccd761.jpg b/public/uploads/xcximg/20241104/1e3c8f03f646b3dca204a59545ccd761.jpg new file mode 100644 index 0000000..bca0913 Binary files /dev/null and b/public/uploads/xcximg/20241104/1e3c8f03f646b3dca204a59545ccd761.jpg differ diff --git a/public/uploads/xcximg/20241104/1f0dcdf48bd597cade27b5ebeed2a0b4.jpeg b/public/uploads/xcximg/20241104/1f0dcdf48bd597cade27b5ebeed2a0b4.jpeg new file mode 100644 index 0000000..b4c5be9 Binary files /dev/null and b/public/uploads/xcximg/20241104/1f0dcdf48bd597cade27b5ebeed2a0b4.jpeg differ diff --git a/public/uploads/xcximg/20241104/278478fbaf0cd5c7cccc8c755d9b0a5c.jpg b/public/uploads/xcximg/20241104/278478fbaf0cd5c7cccc8c755d9b0a5c.jpg new file mode 100644 index 0000000..7569e7e Binary files /dev/null and b/public/uploads/xcximg/20241104/278478fbaf0cd5c7cccc8c755d9b0a5c.jpg differ diff --git a/public/uploads/xcximg/20241104/282c84dd9230a5582ab1a8fdcaa32476.jpeg b/public/uploads/xcximg/20241104/282c84dd9230a5582ab1a8fdcaa32476.jpeg new file mode 100644 index 0000000..a5e053f Binary files /dev/null and b/public/uploads/xcximg/20241104/282c84dd9230a5582ab1a8fdcaa32476.jpeg differ diff --git a/public/uploads/xcximg/20241104/2d68ba0a58d9605456f91ce65670c316.jpg b/public/uploads/xcximg/20241104/2d68ba0a58d9605456f91ce65670c316.jpg new file mode 100644 index 0000000..a2c235e Binary files /dev/null and b/public/uploads/xcximg/20241104/2d68ba0a58d9605456f91ce65670c316.jpg differ diff --git a/public/uploads/xcximg/20241104/35a376d456838abc56d0e74c1b6f2120.jpeg b/public/uploads/xcximg/20241104/35a376d456838abc56d0e74c1b6f2120.jpeg new file mode 100644 index 0000000..36e7766 Binary files /dev/null and b/public/uploads/xcximg/20241104/35a376d456838abc56d0e74c1b6f2120.jpeg differ diff --git a/public/uploads/xcximg/20241104/3f6b03aba61467c31699468e0a9ac8bf.jpg b/public/uploads/xcximg/20241104/3f6b03aba61467c31699468e0a9ac8bf.jpg new file mode 100644 index 0000000..a0b1511 Binary files /dev/null and b/public/uploads/xcximg/20241104/3f6b03aba61467c31699468e0a9ac8bf.jpg differ diff --git a/public/uploads/xcximg/20241104/402089a337e18b54feae1605522be34d.jpg b/public/uploads/xcximg/20241104/402089a337e18b54feae1605522be34d.jpg new file mode 100644 index 0000000..103518c Binary files /dev/null and b/public/uploads/xcximg/20241104/402089a337e18b54feae1605522be34d.jpg differ diff --git a/public/uploads/xcximg/20241104/41353f573ad9f6c83ccdb576d5ccec08.jpg b/public/uploads/xcximg/20241104/41353f573ad9f6c83ccdb576d5ccec08.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241104/41353f573ad9f6c83ccdb576d5ccec08.jpg differ diff --git a/public/uploads/xcximg/20241104/4d60345608b123ed4554ac3dd961e5be.jpg b/public/uploads/xcximg/20241104/4d60345608b123ed4554ac3dd961e5be.jpg new file mode 100644 index 0000000..7961f8b Binary files /dev/null and b/public/uploads/xcximg/20241104/4d60345608b123ed4554ac3dd961e5be.jpg differ diff --git a/public/uploads/xcximg/20241104/52680c7d6552b715900b319650d9eb2b.jpg b/public/uploads/xcximg/20241104/52680c7d6552b715900b319650d9eb2b.jpg new file mode 100644 index 0000000..c97921b Binary files /dev/null and b/public/uploads/xcximg/20241104/52680c7d6552b715900b319650d9eb2b.jpg differ diff --git a/public/uploads/xcximg/20241104/53253eca51ea7c6b7211dfbd516d05d7.jpg b/public/uploads/xcximg/20241104/53253eca51ea7c6b7211dfbd516d05d7.jpg new file mode 100644 index 0000000..9881ee9 Binary files /dev/null and b/public/uploads/xcximg/20241104/53253eca51ea7c6b7211dfbd516d05d7.jpg differ diff --git a/public/uploads/xcximg/20241104/5a8025b0d6010a09158dbbed48776050.jpg b/public/uploads/xcximg/20241104/5a8025b0d6010a09158dbbed48776050.jpg new file mode 100644 index 0000000..c97921b Binary files /dev/null and b/public/uploads/xcximg/20241104/5a8025b0d6010a09158dbbed48776050.jpg differ diff --git a/public/uploads/xcximg/20241104/5d4780d754fbfa09e4b17a6d586da4e5.png b/public/uploads/xcximg/20241104/5d4780d754fbfa09e4b17a6d586da4e5.png new file mode 100644 index 0000000..0bda2d9 Binary files /dev/null and b/public/uploads/xcximg/20241104/5d4780d754fbfa09e4b17a6d586da4e5.png differ diff --git a/public/uploads/xcximg/20241104/5ea1c52389e9901f8e22a17fa20d9e41.jpeg b/public/uploads/xcximg/20241104/5ea1c52389e9901f8e22a17fa20d9e41.jpeg new file mode 100644 index 0000000..10149c7 Binary files /dev/null and b/public/uploads/xcximg/20241104/5ea1c52389e9901f8e22a17fa20d9e41.jpeg differ diff --git a/public/uploads/xcximg/20241104/67e8f46b4cebfc037ea927331e98351c.jpg b/public/uploads/xcximg/20241104/67e8f46b4cebfc037ea927331e98351c.jpg new file mode 100644 index 0000000..ad63081 Binary files /dev/null and b/public/uploads/xcximg/20241104/67e8f46b4cebfc037ea927331e98351c.jpg differ diff --git a/public/uploads/xcximg/20241104/697ea1026c69dde9dd89fb795bcaad65.jpg b/public/uploads/xcximg/20241104/697ea1026c69dde9dd89fb795bcaad65.jpg new file mode 100644 index 0000000..b98a543 Binary files /dev/null and b/public/uploads/xcximg/20241104/697ea1026c69dde9dd89fb795bcaad65.jpg differ diff --git a/public/uploads/xcximg/20241104/69865e14d3cf3cd6f64d74a0c914870a.jpg b/public/uploads/xcximg/20241104/69865e14d3cf3cd6f64d74a0c914870a.jpg new file mode 100644 index 0000000..f39b026 Binary files /dev/null and b/public/uploads/xcximg/20241104/69865e14d3cf3cd6f64d74a0c914870a.jpg differ diff --git a/public/uploads/xcximg/20241104/6bbfd4d29006b1e10fb1330f53696505.jpeg b/public/uploads/xcximg/20241104/6bbfd4d29006b1e10fb1330f53696505.jpeg new file mode 100644 index 0000000..06c3a02 Binary files /dev/null and b/public/uploads/xcximg/20241104/6bbfd4d29006b1e10fb1330f53696505.jpeg differ diff --git a/public/uploads/xcximg/20241104/6ee2fb9721445e5310414418566c9524.jpeg b/public/uploads/xcximg/20241104/6ee2fb9721445e5310414418566c9524.jpeg new file mode 100644 index 0000000..0ccd8e9 Binary files /dev/null and b/public/uploads/xcximg/20241104/6ee2fb9721445e5310414418566c9524.jpeg differ diff --git a/public/uploads/xcximg/20241104/9bcbc7d98b5209817d83333c3c0e397a.png b/public/uploads/xcximg/20241104/9bcbc7d98b5209817d83333c3c0e397a.png new file mode 100644 index 0000000..39f24d2 Binary files /dev/null and b/public/uploads/xcximg/20241104/9bcbc7d98b5209817d83333c3c0e397a.png differ diff --git a/public/uploads/xcximg/20241104/9c82fc04408bed1fb6d863b19a84c1e2.jpg b/public/uploads/xcximg/20241104/9c82fc04408bed1fb6d863b19a84c1e2.jpg new file mode 100644 index 0000000..86b9a2c Binary files /dev/null and b/public/uploads/xcximg/20241104/9c82fc04408bed1fb6d863b19a84c1e2.jpg differ diff --git a/public/uploads/xcximg/20241104/a0358430710d7dad89e77e2e5996b183.jpeg b/public/uploads/xcximg/20241104/a0358430710d7dad89e77e2e5996b183.jpeg new file mode 100644 index 0000000..42a4c26 Binary files /dev/null and b/public/uploads/xcximg/20241104/a0358430710d7dad89e77e2e5996b183.jpeg differ diff --git a/public/uploads/xcximg/20241104/a60291acd319891c40da1e837df4996d.jpg b/public/uploads/xcximg/20241104/a60291acd319891c40da1e837df4996d.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241104/a60291acd319891c40da1e837df4996d.jpg differ diff --git a/public/uploads/xcximg/20241104/a87440683756bfce2363eb0d7c61203d.png b/public/uploads/xcximg/20241104/a87440683756bfce2363eb0d7c61203d.png new file mode 100644 index 0000000..18b418e Binary files /dev/null and b/public/uploads/xcximg/20241104/a87440683756bfce2363eb0d7c61203d.png differ diff --git a/public/uploads/xcximg/20241104/ab1bcaa1192628a41b2bf950894d44f6.jpeg b/public/uploads/xcximg/20241104/ab1bcaa1192628a41b2bf950894d44f6.jpeg new file mode 100644 index 0000000..12deace Binary files /dev/null and b/public/uploads/xcximg/20241104/ab1bcaa1192628a41b2bf950894d44f6.jpeg differ diff --git a/public/uploads/xcximg/20241104/b44765703fbd4f11b53f94bffafe445d.jpg b/public/uploads/xcximg/20241104/b44765703fbd4f11b53f94bffafe445d.jpg new file mode 100644 index 0000000..7e4a7cf Binary files /dev/null and b/public/uploads/xcximg/20241104/b44765703fbd4f11b53f94bffafe445d.jpg differ diff --git a/public/uploads/xcximg/20241104/b8404d18a434eb9df4a15ff71c264ff9.jpg b/public/uploads/xcximg/20241104/b8404d18a434eb9df4a15ff71c264ff9.jpg new file mode 100644 index 0000000..cf2cad5 Binary files /dev/null and b/public/uploads/xcximg/20241104/b8404d18a434eb9df4a15ff71c264ff9.jpg differ diff --git a/public/uploads/xcximg/20241104/bac9c53c67d0f547b6897cc6f8e4dde7.jpg b/public/uploads/xcximg/20241104/bac9c53c67d0f547b6897cc6f8e4dde7.jpg new file mode 100644 index 0000000..996d857 Binary files /dev/null and b/public/uploads/xcximg/20241104/bac9c53c67d0f547b6897cc6f8e4dde7.jpg differ diff --git a/public/uploads/xcximg/20241104/cdc0991d4873244825ed3e43eabbda5a.jpg b/public/uploads/xcximg/20241104/cdc0991d4873244825ed3e43eabbda5a.jpg new file mode 100644 index 0000000..7beeac9 Binary files /dev/null and b/public/uploads/xcximg/20241104/cdc0991d4873244825ed3e43eabbda5a.jpg differ diff --git a/public/uploads/xcximg/20241104/d0b63d2cc2cad5be5c0ca8920aa2a991.jpg b/public/uploads/xcximg/20241104/d0b63d2cc2cad5be5c0ca8920aa2a991.jpg new file mode 100644 index 0000000..7e1b74f Binary files /dev/null and b/public/uploads/xcximg/20241104/d0b63d2cc2cad5be5c0ca8920aa2a991.jpg differ diff --git a/public/uploads/xcximg/20241104/eef70fee8c1cb943704c6efbb236e9f8.jpg b/public/uploads/xcximg/20241104/eef70fee8c1cb943704c6efbb236e9f8.jpg new file mode 100644 index 0000000..cc3cebf Binary files /dev/null and b/public/uploads/xcximg/20241104/eef70fee8c1cb943704c6efbb236e9f8.jpg differ diff --git a/public/uploads/xcximg/20241104/f0add26474c5c307eb68bdb20007b904.jpg b/public/uploads/xcximg/20241104/f0add26474c5c307eb68bdb20007b904.jpg new file mode 100644 index 0000000..7ab6f76 Binary files /dev/null and b/public/uploads/xcximg/20241104/f0add26474c5c307eb68bdb20007b904.jpg differ diff --git a/public/uploads/xcximg/20241104/f4872d803f28b774e2168d09096bc522.jpg b/public/uploads/xcximg/20241104/f4872d803f28b774e2168d09096bc522.jpg new file mode 100644 index 0000000..c97921b Binary files /dev/null and b/public/uploads/xcximg/20241104/f4872d803f28b774e2168d09096bc522.jpg differ diff --git a/public/uploads/xcximg/20241104/f8e5b5d5081dac4d1450f781b29d464d.jpeg b/public/uploads/xcximg/20241104/f8e5b5d5081dac4d1450f781b29d464d.jpeg new file mode 100644 index 0000000..9c2b2ee Binary files /dev/null and b/public/uploads/xcximg/20241104/f8e5b5d5081dac4d1450f781b29d464d.jpeg differ diff --git a/public/uploads/xcximg/20241104/fe02d6b97c53491f5c6c053567a8b2ad.jpg b/public/uploads/xcximg/20241104/fe02d6b97c53491f5c6c053567a8b2ad.jpg new file mode 100644 index 0000000..7569e7e Binary files /dev/null and b/public/uploads/xcximg/20241104/fe02d6b97c53491f5c6c053567a8b2ad.jpg differ diff --git a/public/uploads/xcximg/20241104/fe9ff84cebe30746cd72ded4e0202094.jpeg b/public/uploads/xcximg/20241104/fe9ff84cebe30746cd72ded4e0202094.jpeg new file mode 100644 index 0000000..c605e18 Binary files /dev/null and b/public/uploads/xcximg/20241104/fe9ff84cebe30746cd72ded4e0202094.jpeg differ diff --git a/public/uploads/xcximg/20241104/ffbf12365b6a4723158be078659a8c09.jpeg b/public/uploads/xcximg/20241104/ffbf12365b6a4723158be078659a8c09.jpeg new file mode 100644 index 0000000..efbd084 Binary files /dev/null and b/public/uploads/xcximg/20241104/ffbf12365b6a4723158be078659a8c09.jpeg differ diff --git a/public/uploads/xcximg/20241105/63e5f270d6f040e35ea871c7f3d3703e.jpg b/public/uploads/xcximg/20241105/63e5f270d6f040e35ea871c7f3d3703e.jpg new file mode 100644 index 0000000..0d6704f Binary files /dev/null and b/public/uploads/xcximg/20241105/63e5f270d6f040e35ea871c7f3d3703e.jpg differ diff --git a/public/uploads/xcximg/20241105/663ece7444a37eeaf43d2e8dd9a348c6.jpg b/public/uploads/xcximg/20241105/663ece7444a37eeaf43d2e8dd9a348c6.jpg new file mode 100644 index 0000000..c49ecdf Binary files /dev/null and b/public/uploads/xcximg/20241105/663ece7444a37eeaf43d2e8dd9a348c6.jpg differ diff --git a/public/uploads/xcximg/20241105/766d3904205a23d29430256d9fbd6b9b.jpg b/public/uploads/xcximg/20241105/766d3904205a23d29430256d9fbd6b9b.jpg new file mode 100644 index 0000000..c49ecdf Binary files /dev/null and b/public/uploads/xcximg/20241105/766d3904205a23d29430256d9fbd6b9b.jpg differ diff --git a/public/uploads/xcximg/20241105/a90c4decb084d2e27c7866995218370b.jpg b/public/uploads/xcximg/20241105/a90c4decb084d2e27c7866995218370b.jpg new file mode 100644 index 0000000..480f249 Binary files /dev/null and b/public/uploads/xcximg/20241105/a90c4decb084d2e27c7866995218370b.jpg differ diff --git a/public/uploads/xcximg/20241105/e490edc1566a284ef212de3abafb77ca.jpg b/public/uploads/xcximg/20241105/e490edc1566a284ef212de3abafb77ca.jpg new file mode 100644 index 0000000..480f249 Binary files /dev/null and b/public/uploads/xcximg/20241105/e490edc1566a284ef212de3abafb77ca.jpg differ diff --git a/public/uploads/xcximg/20241105/e801c3e3170993cd39922cec24a279a9.jpg b/public/uploads/xcximg/20241105/e801c3e3170993cd39922cec24a279a9.jpg new file mode 100644 index 0000000..62cd74a Binary files /dev/null and b/public/uploads/xcximg/20241105/e801c3e3170993cd39922cec24a279a9.jpg differ diff --git a/public/uploads/xcximg/20241105/f756f66fe12ca704c0b4c60f6293d77e.jpg b/public/uploads/xcximg/20241105/f756f66fe12ca704c0b4c60f6293d77e.jpg new file mode 100644 index 0000000..e9ce826 Binary files /dev/null and b/public/uploads/xcximg/20241105/f756f66fe12ca704c0b4c60f6293d77e.jpg differ diff --git a/public/uploads/xcximg/20241111/08780c8e08b6a649a77841eb0418e4ff.jpg b/public/uploads/xcximg/20241111/08780c8e08b6a649a77841eb0418e4ff.jpg new file mode 100644 index 0000000..c97921b Binary files /dev/null and b/public/uploads/xcximg/20241111/08780c8e08b6a649a77841eb0418e4ff.jpg differ diff --git a/public/uploads/xcximg/20241111/3b7b2cea904ed61a5835c4c234ca9ab1.jpg b/public/uploads/xcximg/20241111/3b7b2cea904ed61a5835c4c234ca9ab1.jpg new file mode 100644 index 0000000..57b9e96 Binary files /dev/null and b/public/uploads/xcximg/20241111/3b7b2cea904ed61a5835c4c234ca9ab1.jpg differ diff --git a/public/uploads/xcximg/20241113/58a37b82901d44d64dadfc9a1d3f6311.jpeg b/public/uploads/xcximg/20241113/58a37b82901d44d64dadfc9a1d3f6311.jpeg new file mode 100644 index 0000000..80c53ba Binary files /dev/null and b/public/uploads/xcximg/20241113/58a37b82901d44d64dadfc9a1d3f6311.jpeg differ diff --git a/public/uploads/xcximg/20241114/10b4f8bea1c66a6bbcb483d111cfdfb0.jpeg b/public/uploads/xcximg/20241114/10b4f8bea1c66a6bbcb483d111cfdfb0.jpeg new file mode 100644 index 0000000..55ab270 Binary files /dev/null and b/public/uploads/xcximg/20241114/10b4f8bea1c66a6bbcb483d111cfdfb0.jpeg differ diff --git a/public/uploads/xcximg/20241114/6878ff227da25a7772acf39a4941a6ae.jpeg b/public/uploads/xcximg/20241114/6878ff227da25a7772acf39a4941a6ae.jpeg new file mode 100644 index 0000000..8511c01 Binary files /dev/null and b/public/uploads/xcximg/20241114/6878ff227da25a7772acf39a4941a6ae.jpeg differ diff --git a/public/uploads/xcximg/20241119/303699824d715d2f8f685a4340565b7c.jpg b/public/uploads/xcximg/20241119/303699824d715d2f8f685a4340565b7c.jpg new file mode 100644 index 0000000..51b2f4b Binary files /dev/null and b/public/uploads/xcximg/20241119/303699824d715d2f8f685a4340565b7c.jpg differ diff --git a/public/uploads/xcximg/20241120/43ff94bd294388953c5b955125c89181.jpg b/public/uploads/xcximg/20241120/43ff94bd294388953c5b955125c89181.jpg new file mode 100644 index 0000000..6f3bca7 Binary files /dev/null and b/public/uploads/xcximg/20241120/43ff94bd294388953c5b955125c89181.jpg differ diff --git a/public/uploads/xcximg/20241124/820d87501e777ae18c60beabade35bf3.jpg b/public/uploads/xcximg/20241124/820d87501e777ae18c60beabade35bf3.jpg new file mode 100644 index 0000000..de40a2f Binary files /dev/null and b/public/uploads/xcximg/20241124/820d87501e777ae18c60beabade35bf3.jpg differ diff --git a/public/uploads/xcximg/20241125/91c26dc767b6b6a0692ed7570ee4be43.jpeg b/public/uploads/xcximg/20241125/91c26dc767b6b6a0692ed7570ee4be43.jpeg new file mode 100644 index 0000000..02ab9fb Binary files /dev/null and b/public/uploads/xcximg/20241125/91c26dc767b6b6a0692ed7570ee4be43.jpeg differ diff --git a/public/uploads/xcximg/20241126/145faf17ebd0199b357e69b94a7ec950.jpeg b/public/uploads/xcximg/20241126/145faf17ebd0199b357e69b94a7ec950.jpeg new file mode 100644 index 0000000..d8c1afe Binary files /dev/null and b/public/uploads/xcximg/20241126/145faf17ebd0199b357e69b94a7ec950.jpeg differ diff --git a/public/uploads/xcximg/20241126/2cc465641029ba3c5692192352c93eb5.jpg b/public/uploads/xcximg/20241126/2cc465641029ba3c5692192352c93eb5.jpg new file mode 100644 index 0000000..6a79f2c Binary files /dev/null and b/public/uploads/xcximg/20241126/2cc465641029ba3c5692192352c93eb5.jpg differ diff --git a/public/uploads/xcximg/20241126/45ed62fc8dd79c33e114c1271638bc5c.jpg b/public/uploads/xcximg/20241126/45ed62fc8dd79c33e114c1271638bc5c.jpg new file mode 100644 index 0000000..b282351 Binary files /dev/null and b/public/uploads/xcximg/20241126/45ed62fc8dd79c33e114c1271638bc5c.jpg differ diff --git a/public/uploads/xcximg/20241126/6772633478d68cbe2e0ac386b5da4c91.jpg b/public/uploads/xcximg/20241126/6772633478d68cbe2e0ac386b5da4c91.jpg new file mode 100644 index 0000000..4824407 Binary files /dev/null and b/public/uploads/xcximg/20241126/6772633478d68cbe2e0ac386b5da4c91.jpg differ diff --git a/public/uploads/xcximg/20241126/7ecbfae10fac39825f8c418ea46dea51.jpg b/public/uploads/xcximg/20241126/7ecbfae10fac39825f8c418ea46dea51.jpg new file mode 100644 index 0000000..7e66a15 Binary files /dev/null and b/public/uploads/xcximg/20241126/7ecbfae10fac39825f8c418ea46dea51.jpg differ diff --git a/public/uploads/xcximg/20241126/9687d240bfa20ea480ae984f6833662a.jpg b/public/uploads/xcximg/20241126/9687d240bfa20ea480ae984f6833662a.jpg new file mode 100644 index 0000000..029a452 Binary files /dev/null and b/public/uploads/xcximg/20241126/9687d240bfa20ea480ae984f6833662a.jpg differ diff --git a/public/uploads/xcximg/20241126/b4df66cf9c0e108570d38e019cf5aa2b.jpg b/public/uploads/xcximg/20241126/b4df66cf9c0e108570d38e019cf5aa2b.jpg new file mode 100644 index 0000000..6a79f2c Binary files /dev/null and b/public/uploads/xcximg/20241126/b4df66cf9c0e108570d38e019cf5aa2b.jpg differ diff --git a/public/uploads/xcximg/20241126/d80eab38f13e40c16e4b87d32dab329d.jpg b/public/uploads/xcximg/20241126/d80eab38f13e40c16e4b87d32dab329d.jpg new file mode 100644 index 0000000..ae09b4d Binary files /dev/null and b/public/uploads/xcximg/20241126/d80eab38f13e40c16e4b87d32dab329d.jpg differ diff --git a/route/route.php b/route/route.php new file mode 100644 index 0000000..0b344fe --- /dev/null +++ b/route/route.php @@ -0,0 +1,20 @@ + +// +---------------------------------------------------------------------- + +Route::get('think', function () { + return 'hello,ThinkPHP5!'; +}); + +Route::get('hello/:name', 'index/hello'); + +return [ + +]; diff --git a/store/.gitignore b/store/.gitignore new file mode 100644 index 0000000..b722e9e --- /dev/null +++ b/store/.gitignore @@ -0,0 +1 @@ +!.gitignore \ No newline at end of file diff --git a/store/README.md b/store/README.md new file mode 100644 index 0000000..a70d44f --- /dev/null +++ b/store/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 云商店资源存放目录 diff --git a/store/data/README.md b/store/data/README.md new file mode 100644 index 0000000..2e714e0 --- /dev/null +++ b/store/data/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 云商店数据包存放目录 diff --git a/store/model/README.md b/store/model/README.md new file mode 100644 index 0000000..4d7fca0 --- /dev/null +++ b/store/model/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 云商店模块存放目录 diff --git a/store/plugin/README.md b/store/plugin/README.md new file mode 100644 index 0000000..66d5d64 --- /dev/null +++ b/store/plugin/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 云商店插件存放目录 diff --git a/think b/think new file mode 100644 index 0000000..e028d4c --- /dev/null +++ b/think @@ -0,0 +1,22 @@ +#!/usr/bin/env php + +// +---------------------------------------------------------------------- + +namespace think; + +// 加载基础文件 +require __DIR__ . '/thinkphp/base.php'; + +// 应用初始化 +Container::get('app')->path(__DIR__ . '/application/')->initialize(); + +// 控制台初始化 +Console::init(); \ No newline at end of file diff --git a/thinkphp/.gitignore b/thinkphp/.gitignore new file mode 100644 index 0000000..f7775ba --- /dev/null +++ b/thinkphp/.gitignore @@ -0,0 +1,8 @@ +/vendor +composer.phar +composer.lock +.DS_Store +Thumbs.db +/phpunit.xml +/.idea +/.vscode \ No newline at end of file diff --git a/thinkphp/.htaccess b/thinkphp/.htaccess new file mode 100644 index 0000000..3418e55 --- /dev/null +++ b/thinkphp/.htaccess @@ -0,0 +1 @@ +deny from all \ No newline at end of file diff --git a/thinkphp/CONTRIBUTING.md b/thinkphp/CONTRIBUTING.md new file mode 100644 index 0000000..6cefcb3 --- /dev/null +++ b/thinkphp/CONTRIBUTING.md @@ -0,0 +1,119 @@ +如何贡献我的源代码 +=== + +此文档介绍了 ThinkPHP 团队的组成以及运转机制,您提交的代码将给 ThinkPHP 项目带来什么好处,以及如何才能加入我们的行列。 + +## 通过 Github 贡献代码 + +ThinkPHP 目前使用 Git 来控制程序版本,如果你想为 ThinkPHP 贡献源代码,请先大致了解 Git 的使用方法。我们目前把项目托管在 GitHub 上,任何 GitHub 用户都可以向我们贡献代码。 + +参与的方式很简单,`fork`一份 ThinkPHP 的代码到你的仓库中,修改后提交,并向我们发起`pull request`申请,我们会及时对代码进行审查并处理你的申请并。审查通过后,你的代码将被`merge`进我们的仓库中,这样你就会自动出现在贡献者名单里了,非常方便。 + +我们希望你贡献的代码符合: + +* ThinkPHP 的编码规范 +* 适当的注释,能让其他人读懂 +* 遵循 Apache2 开源协议 + +**如果想要了解更多细节或有任何疑问,请继续阅读下面的内容** + +### 注意事项 + +* 本项目代码格式化标准选用 [**PSR-2**](http://www.kancloud.cn/thinkphp/php-fig-psr/3141); +* 类名和类文件名遵循 [**PSR-4**](http://www.kancloud.cn/thinkphp/php-fig-psr/3144); +* 对于 Issues 的处理,请使用诸如 `fix #xxx(Issue ID)` 的 commit title 直接关闭 issue。 +* 系统会自动在 PHP 5.4 5.5 5.6 7.0 和 HHVM 上测试修改,其中 HHVM 下的测试容许报错,请确保你的修改符合 PHP 5.4 ~ 5.6 和 PHP 7.0 的语法规范; +* 管理员不会合并造成 CI faild 的修改,若出现 CI faild 请检查自己的源代码或修改相应的[单元测试文件](tests); + +## GitHub Issue + +GitHub 提供了 Issue 功能,该功能可以用于: + +* 提出 bug +* 提出功能改进 +* 反馈使用体验 + +该功能不应该用于: + + * 提出修改意见(涉及代码署名和修订追溯问题) + * 不友善的言论 + +## 快速修改 + +**GitHub 提供了快速编辑文件的功能** + +1. 登录 GitHub 帐号; +2. 浏览项目文件,找到要进行修改的文件; +3. 点击右上角铅笔图标进行修改; +4. 填写 `Commit changes` 相关内容(Title 必填); +5. 提交修改,等待 CI 验证和管理员合并。 + +**若您需要一次提交大量修改,请继续阅读下面的内容** + +## 完整流程 + +1. `fork`本项目; +2. 克隆(`clone`)你 `fork` 的项目到本地; +3. 新建分支(`branch`)并检出(`checkout`)新分支; +4. 添加本项目到你的本地 git 仓库作为上游(`upstream`); +5. 进行修改,若你的修改包含方法或函数的增减,请记得修改[单元测试文件](tests); +6. 变基(衍合 `rebase`)你的分支到上游 master 分支; +7. `push` 你的本地仓库到 GitHub; +8. 提交 `pull request`; +9. 等待 CI 验证(若不通过则重复 5~7,GitHub 会自动更新你的 `pull request`); +10. 等待管理员处理,并及时 `rebase` 你的分支到上游 master 分支(若上游 master 分支有修改)。 + +*若有必要,可以 `git push -f` 强行推送 rebase 后的分支到自己的 `fork`* + +*绝对不可以使用 `git push -f` 强行推送修改到上游* + +### 注意事项 + +* 若对上述流程有任何不清楚的地方,请查阅 GIT 教程,如 [这个](http://backlogtool.com/git-guide/cn/); +* 对于代码**不同方面**的修改,请在自己 `fork` 的项目中**创建不同的分支**(原因参见`完整流程`第9条备注部分); +* 变基及交互式变基操作参见 [Git 交互式变基](http://pakchoi.me/2015/03/17/git-interactive-rebase/) + +## 推荐资源 + +### 开发环境 + +* XAMPP for Windows 5.5.x +* WampServer (for Windows) +* upupw Apache PHP5.4 ( for Windows) + +或自行安装 + +- Apache / Nginx +- PHP 5.4 ~ 5.6 +- MySQL / MariaDB + +*Windows 用户推荐添加 PHP bin 目录到 PATH,方便使用 composer* + +*Linux 用户自行配置环境, Mac 用户推荐使用内置 Apache 配合 Homebrew 安装 PHP 和 MariaDB* + +### 编辑器 + +Sublime Text 3 + phpfmt 插件 + +phpfmt 插件参数 + +```json +{ + "autocomplete": true, + "enable_auto_align": true, + "format_on_save": true, + "indent_with_space": true, + "psr1_naming": false, + "psr2": true, + "version": 4 +} +``` + +或其他 编辑器 / IDE 配合 PSR2 自动格式化工具 + +### Git GUI + +* SourceTree +* GitHub Desktop + +或其他 Git 图形界面客户端 diff --git a/thinkphp/LICENSE.txt b/thinkphp/LICENSE.txt new file mode 100644 index 0000000..774fa76 --- /dev/null +++ b/thinkphp/LICENSE.txt @@ -0,0 +1,32 @@ + +ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 +版权所有Copyright © 2006-2018 by ThinkPHP (http://thinkphp.cn) +All rights reserved。 +ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 + +Apache Licence是著名的非盈利开源组织Apache采用的协议。 +该协议和BSD类似,鼓励代码共享和尊重原作者的著作权, +允许代码修改,再作为开源或商业软件发布。需要满足 +的条件: +1. 需要给代码的用户一份Apache Licence ; +2. 如果你修改了代码,需要在被修改的文件中说明; +3. 在延伸的代码中(修改和有源代码衍生的代码中)需要 +带有原来代码中的协议,商标,专利声明和其他原来作者规 +定需要包含的说明; +4. 如果再发布的产品中包含一个Notice文件,则在Notice文 +件中需要带有本协议内容。你可以在Notice中增加自己的 +许可,但不可以表现为对Apache Licence构成更改。 +具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0 + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/thinkphp/README.md b/thinkphp/README.md new file mode 100644 index 0000000..1339e6c --- /dev/null +++ b/thinkphp/README.md @@ -0,0 +1,99 @@ +![](https://box.kancloud.cn/5a0aaa69a5ff42657b5c4715f3d49221) + +ThinkPHP 5.1(LTS) —— 12载初心,你值得信赖的PHP框架 +=============== + +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/top-think/framework/badges/quality-score.png?b=5.1)](https://scrutinizer-ci.com/g/top-think/framework/?branch=5.1) +[![Build Status](https://travis-ci.org/top-think/framework.svg?branch=master)](https://travis-ci.org/top-think/framework) +[![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) +[![PHP Version](https://img.shields.io/badge/php-%3E%3D5.6-8892BF.svg)](http://www.php.net/) +[![License](https://poser.pugx.org/topthink/framework/license)](https://packagist.org/packages/topthink/framework) + +ThinkPHP5.1对底层架构做了进一步的改进,减少依赖,其主要特性包括: + + + 采用容器统一管理对象 + + 支持Facade + + 更易用的路由 + + 注解路由支持 + + 路由跨域请求支持 + + 验证类增强 + + 配置和路由目录独立 + + 取消系统常量 + + 类库别名机制 + + 模型和数据库增强 + + 依赖注入完善 + + 支持PSR-3日志规范 + + 中间件支持(`V5.1.6+`) + + 支持`Swoole`/`Workerman`运行(`V5.1.18+`) + +官方已经正式宣布`5.1.27`版本为LTS版本。 + +### 废除的功能: + + + 聚合模型 + + 内置控制器扩展类 + + 模型自动验证 + +> ThinkPHP5.1的运行环境要求PHP5.6+ 兼容PHP8.0。 + + +## 安装 + +使用composer安装 + +~~~ +composer create-project topthink/think tp +~~~ + +启动服务 + +~~~ +cd tp +php think run +~~~ + +然后就可以在浏览器中访问 + +~~~ +http://localhost:8000 +~~~ + +更新框架 +~~~ +composer update topthink/framework +~~~ + + +## 在线手册 + ++ [完全开发手册](https://www.kancloud.cn/manual/thinkphp5_1/content) ++ [升级指导](https://www.kancloud.cn/manual/thinkphp5_1/354155) + + +## 官方服务 + ++ [应用服务市场](https://market.topthink.com/) ++ [ThinkAPI——统一API服务](https://docs.topthink.com/think-api) + +## 命名规范 + +`ThinkPHP5.1`遵循PSR-2命名规范和PSR-4自动加载规范。 + +## 参与开发 + +请参阅 [ThinkPHP5 核心框架包](https://github.com/top-think/framework)。 + +## 版权信息 + +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 new file mode 100644 index 0000000..d7238cc --- /dev/null +++ b/thinkphp/base.php @@ -0,0 +1,52 @@ + +// +---------------------------------------------------------------------- +namespace think; + +// 载入Loader类 +require __DIR__ . '/library/think/Loader.php'; + +// 注册自动加载 +Loader::register(); + +// 注册错误和异常处理机制 +Error::register(); + +// 实现日志接口 +if (interface_exists('Psr\Log\LoggerInterface')) { + interface LoggerInterface extends \Psr\Log\LoggerInterface + {} +} else { + interface LoggerInterface + {} +} + +// 注册类库别名 +Loader::addClassAlias([ + 'App' => facade\App::class, + 'Build' => facade\Build::class, + 'Cache' => facade\Cache::class, + 'Config' => facade\Config::class, + 'Cookie' => facade\Cookie::class, + 'Db' => Db::class, + 'Debug' => facade\Debug::class, + 'Env' => facade\Env::class, + 'Facade' => Facade::class, + 'Hook' => facade\Hook::class, + 'Lang' => facade\Lang::class, + 'Log' => facade\Log::class, + 'Request' => facade\Request::class, + 'Response' => facade\Response::class, + 'Route' => facade\Route::class, + 'Session' => facade\Session::class, + 'Url' => facade\Url::class, + 'Validate' => facade\Validate::class, + 'View' => facade\View::class, +]); diff --git a/thinkphp/composer.json b/thinkphp/composer.json new file mode 100644 index 0000000..33477b1 --- /dev/null +++ b/thinkphp/composer.json @@ -0,0 +1,35 @@ +{ + "name": "topthink/framework", + "description": "the new thinkphp framework", + "type": "think-framework", + "keywords": [ + "framework", + "thinkphp", + "ORM" + ], + "homepage": "http://thinkphp.cn/", + "license": "Apache-2.0", + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + }, + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "require": { + "php": ">=5.6.0", + "topthink/think-installer": "2.*" + }, + "require-dev": { + "phpunit/phpunit": "^5.0|^6.0", + "johnkary/phpunit-speedtrap": "^1.0", + "mikey179/vfsstream": "~1.6", + "phploc/phploc": "2.*", + "sebastian/phpcpd": "2.*", + "squizlabs/php_codesniffer": "2.*", + "phpdocumentor/reflection-docblock": "^2.0" + } +} diff --git a/thinkphp/convention.php b/thinkphp/convention.php new file mode 100644 index 0000000..1d85e56 --- /dev/null +++ b/thinkphp/convention.php @@ -0,0 +1,327 @@ + [ + // 应用名称 + 'app_name' => '', + // 应用地址 + 'app_host' => '', + // 应用调试模式 + 'app_debug' => false, + // 应用Trace + 'app_trace' => false, + // 应用模式状态 + 'app_status' => '', + // 是否HTTPS + 'is_https' => false, + // 入口自动绑定模块 + 'auto_bind_module' => false, + // 注册的根命名空间 + 'root_namespace' => [], + // 默认输出类型 + 'default_return_type' => 'html', + // 默认AJAX 数据返回格式,可选json xml ... + 'default_ajax_return' => 'json', + // 默认JSONP格式返回的处理方法 + 'default_jsonp_handler' => 'jsonpReturn', + // 默认JSONP处理方法 + 'var_jsonp_handler' => 'callback', + // 默认时区 + 'default_timezone' => 'Asia/Shanghai', + // 是否开启多语言 + 'lang_switch_on' => false, + // 默认验证器 + 'default_validate' => '', + // 默认语言 + 'default_lang' => 'zh-cn', + + // +---------------------------------------------------------------------- + // | 模块设置 + // +---------------------------------------------------------------------- + + // 自动搜索控制器 + 'controller_auto_search' => false, + // 操作方法前缀 + 'use_action_prefix' => false, + // 操作方法后缀 + 'action_suffix' => '', + // 默认的空控制器名 + 'empty_controller' => 'Error', + // 默认的空模块名 + 'empty_module' => '', + // 默认模块名 + 'default_module' => 'index', + // 是否支持多模块 + 'app_multi_module' => true, + // 禁止访问模块 + 'deny_module_list' => ['common'], + // 默认控制器名 + 'default_controller' => 'Index', + // 默认操作名 + 'default_action' => 'index', + // 是否自动转换URL中的控制器和操作名 + 'url_convert' => true, + // 默认的访问控制器层 + 'url_controller_layer' => 'controller', + // 应用类库后缀 + 'class_suffix' => false, + // 控制器类后缀 + 'controller_suffix' => false, + + // +---------------------------------------------------------------------- + // | URL请求设置 + // +---------------------------------------------------------------------- + + // 默认全局过滤方法 用逗号分隔多个 + 'default_filter' => '', + // PATHINFO变量名 用于兼容模式 + 'var_pathinfo' => 's', + // 兼容PATH_INFO获取 + 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'], + // HTTPS代理标识 + 'https_agent_name' => '', + // IP代理获取标识 + 'http_agent_ip' => 'HTTP_X_REAL_IP', + // URL伪静态后缀 + 'url_html_suffix' => 'html', + // 域名根,如thinkphp.cn + 'url_domain_root' => '', + // 表单请求类型伪装变量 + 'var_method' => '_method', + // 表单ajax伪装变量 + 'var_ajax' => '_ajax', + // 表单pjax伪装变量 + 'var_pjax' => '_pjax', + // 是否开启请求缓存 true自动缓存 支持设置请求缓存规则 + 'request_cache' => false, + // 请求缓存有效期 + 'request_cache_expire' => null, + // 全局请求缓存排除规则 + 'request_cache_except' => [], + + // +---------------------------------------------------------------------- + // | 路由设置 + // +---------------------------------------------------------------------- + + // pathinfo分隔符 + 'pathinfo_depr' => '/', + // URL普通方式参数 用于自动生成 + 'url_common_param' => false, + // URL参数方式 0 按名称成对解析 1 按顺序解析 + 'url_param_type' => 0, + // 是否开启路由延迟解析 + 'url_lazy_route' => false, + // 是否强制使用路由 + 'url_route_must' => false, + // 合并路由规则 + 'route_rule_merge' => false, + // 路由是否完全匹配 + 'route_complete_match' => false, + // 使用注解路由 + 'route_annotation' => false, + // 默认的路由变量规则 + 'default_route_pattern' => '\w+', + // 是否开启路由缓存 + 'route_check_cache' => false, + // 路由缓存的Key自定义设置(闭包),默认为当前URL和请求类型的md5 + 'route_check_cache_key' => '', + // 路由缓存的设置 + 'route_cache_option' => [], + + // +---------------------------------------------------------------------- + // | 异常及错误设置 + // +---------------------------------------------------------------------- + + // 默认跳转页面对应的模板文件 + 'dispatch_success_tmpl' => __DIR__ . '/tpl/dispatch_jump.tpl', + 'dispatch_error_tmpl' => __DIR__ . '/tpl/dispatch_jump.tpl', + // 异常页面的模板文件 + 'exception_tmpl' => __DIR__ . '/tpl/think_exception.tpl', + // 错误显示信息,非调试模式有效 + 'error_message' => '页面错误!请稍后再试~', + // 显示错误信息 + 'show_error_msg' => false, + // 异常处理handle类 留空使用 \think\exception\Handle + 'exception_handle' => '', + ], + + // +---------------------------------------------------------------------- + // | 模板设置 + // +---------------------------------------------------------------------- + + 'template' => [ + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 + 'auto_rule' => 1, + // 模板引擎类型 支持 php think 支持扩展 + 'type' => 'Think', + // 视图基础目录,配置目录为所有模块的视图起始目录 + 'view_base' => '', + // 当前模板的视图目录 留空为自动获取 + 'view_path' => '', + // 模板后缀 + 'view_suffix' => 'html', + // 模板文件名分隔符 + 'view_depr' => DIRECTORY_SEPARATOR, + // 模板引擎普通标签开始标记 + 'tpl_begin' => '{', + // 模板引擎普通标签结束标记 + 'tpl_end' => '}', + // 标签库标签开始标记 + 'taglib_begin' => '{', + // 标签库标签结束标记 + 'taglib_end' => '}', + ], + + // +---------------------------------------------------------------------- + // | 日志设置 + // +---------------------------------------------------------------------- + + 'log' => [ + // 日志记录方式,内置 file socket 支持扩展 + 'type' => 'File', + // 日志保存目录 + //'path' => LOG_PATH, + // 日志记录级别 + 'level' => [], + // 是否记录trace信息到日志 + 'record_trace' => false, + // 是否JSON格式记录 + 'json' => false, + ], + + // +---------------------------------------------------------------------- + // | Trace设置 开启 app_trace 后 有效 + // +---------------------------------------------------------------------- + + 'trace' => [ + // 内置Html Console 支持扩展 + 'type' => 'Html', + 'file' => __DIR__ . '/tpl/page_trace.tpl', + ], + + // +---------------------------------------------------------------------- + // | 缓存设置 + // +---------------------------------------------------------------------- + + 'cache' => [ + // 驱动方式 + 'type' => 'File', + // 缓存保存目录 + //'path' => CACHE_PATH, + // 缓存前缀 + 'prefix' => '', + // 缓存有效期 0表示永久缓存 + 'expire' => 0, + ], + + // +---------------------------------------------------------------------- + // | 会话设置 + // +---------------------------------------------------------------------- + + 'session' => [ + 'id' => '', + // SESSION_ID的提交变量,解决flash上传跨域 + 'var_session_id' => '', + // SESSION 前缀 + 'prefix' => 'think', + // 驱动方式 支持redis memcache memcached + 'type' => '', + // 是否自动开启 SESSION + 'auto_start' => true, + 'httponly' => true, + 'secure' => false, + ], + + // +---------------------------------------------------------------------- + // | Cookie设置 + // +---------------------------------------------------------------------- + + 'cookie' => [ + // cookie 名称前缀 + 'prefix' => '', + // cookie 保存时间 + 'expire' => 0, + // cookie 保存路径 + 'path' => '/', + // cookie 有效域名 + 'domain' => '', + // cookie 启用安全传输 + 'secure' => false, + // httponly设置 + 'httponly' => '', + // 是否使用 setcookie + 'setcookie' => true, + ], + + // +---------------------------------------------------------------------- + // | 数据库设置 + // +---------------------------------------------------------------------- + + 'database' => [ + // 数据库类型 + 'type' => 'mysql', + // 数据库连接DSN配置 + 'dsn' => '', + // 服务器地址 + 'hostname' => '127.0.0.1', + // 数据库名 + 'database' => '', + // 数据库用户名 + 'username' => 'root', + // 数据库密码 + 'password' => '', + // 数据库连接端口 + 'hostport' => '', + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => '', + // 数据库调试模式 + 'debug' => false, + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 数据集返回类型 + 'resultset_type' => 'array', + // 自动写入时间戳字段 + 'auto_timestamp' => false, + // 时间字段取出后的默认时间格式 + 'datetime_format' => 'Y-m-d H:i:s', + // 是否需要进行SQL性能分析 + 'sql_explain' => false, + // 查询对象 + 'query' => '\\think\\db\\Query', + ], + + //分页配置 + 'paginate' => [ + 'type' => 'bootstrap', + 'var_page' => 'page', + 'list_rows' => 15, + ], + + //控制台配置 + 'console' => [ + 'name' => 'Think Console', + 'version' => '0.1', + 'user' => null, + 'auto_path' => '', + ], + + // 中间件配置 + 'middleware' => [ + 'default_namespace' => 'app\\http\\middleware\\', + ], +]; diff --git a/thinkphp/helper.php b/thinkphp/helper.php new file mode 100644 index 0000000..72b9e9f --- /dev/null +++ b/thinkphp/helper.php @@ -0,0 +1,726 @@ + +// +---------------------------------------------------------------------- + +//------------------------ +// ThinkPHP 助手函数 +//------------------------- + +use think\Container; +use think\Db; +use think\exception\HttpException; +use think\exception\HttpResponseException; +use think\facade\Cache; +use think\facade\Config; +use think\facade\Cookie; +use think\facade\Debug; +use think\facade\Env; +use think\facade\Hook; +use think\facade\Lang; +use think\facade\Log; +use think\facade\Request; +use think\facade\Route; +use think\facade\Session; +use think\facade\Url; +use think\Response; +use think\route\RuleItem; + +if (!function_exists('abort')) { + /** + * 抛出HTTP异常 + * @param integer|Response $code 状态码 或者 Response对象实例 + * @param string $message 错误信息 + * @param array $header 参数 + */ + function abort($code, $message = null, $header = []) + { + if ($code instanceof Response) { + throw new HttpResponseException($code); + } else { + throw new HttpException($code, $message, null, $header); + } + } +} + +if (!function_exists('action')) { + /** + * 调用模块的操作方法 参数格式 [模块/控制器/]操作 + * @param string $url 调用地址 + * @param string|array $vars 调用参数 支持字符串和数组 + * @param string $layer 要调用的控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return mixed + */ + function action($url, $vars = [], $layer = 'controller', $appendSuffix = false) + { + return app()->action($url, $vars, $layer, $appendSuffix); + } +} + +if (!function_exists('app')) { + /** + * 快速获取容器中的实例 支持依赖注入 + * @param string $name 类名或标识 默认获取当前应用实例 + * @param array $args 参数 + * @param bool $newInstance 是否每次创建新的实例 + * @return mixed|\think\App + */ + function app($name = 'think\App', $args = [], $newInstance = false) + { + return Container::get($name, $args, $newInstance); + } +} + +if (!function_exists('behavior')) { + /** + * 执行某个行为(run方法) 支持依赖注入 + * @param mixed $behavior 行为类名或者别名 + * @param mixed $args 参数 + * @return mixed + */ + function behavior($behavior, $args = null) + { + return Hook::exec($behavior, $args); + } +} + +if (!function_exists('bind')) { + /** + * 绑定一个类到容器 + * @access public + * @param string $abstract 类标识、接口 + * @param mixed $concrete 要绑定的类、闭包或者实例 + * @return Container + */ + function bind($abstract, $concrete = null) + { + return Container::getInstance()->bindTo($abstract, $concrete); + } +} + +if (!function_exists('cache')) { + /** + * 缓存管理 + * @param mixed $name 缓存名称,如果为数组表示进行缓存设置 + * @param mixed $value 缓存值 + * @param mixed $options 缓存参数 + * @param string $tag 缓存标签 + * @return mixed + */ + function cache($name, $value = '', $options = null, $tag = null) + { + if (is_array($options)) { + // 缓存操作的同时初始化 + Cache::connect($options); + } elseif (is_array($name)) { + // 缓存初始化 + return Cache::connect($name); + } + + if ('' === $value) { + // 获取缓存 + return 0 === strpos($name, '?') ? Cache::has(substr($name, 1)) : Cache::get($name); + } elseif (is_null($value)) { + // 删除缓存 + return Cache::rm($name); + } + + // 缓存数据 + if (is_array($options)) { + $expire = isset($options['expire']) ? $options['expire'] : null; //修复查询缓存无法设置过期时间 + } else { + $expire = is_numeric($options) ? $options : null; //默认快捷缓存设置过期时间 + } + + if (is_null($tag)) { + return Cache::set($name, $value, $expire); + } else { + return Cache::tag($tag)->set($name, $value, $expire); + } + } +} + +if (!function_exists('call')) { + /** + * 调用反射执行callable 支持依赖注入 + * @param mixed $callable 支持闭包等callable写法 + * @param array $args 参数 + * @return mixed + */ + function call($callable, $args = []) + { + return Container::getInstance()->invoke($callable, $args); + } +} + +if (!function_exists('class_basename')) { + /** + * 获取类名(不包含命名空间) + * + * @param string|object $class + * @return string + */ + function class_basename($class) + { + $class = is_object($class) ? get_class($class) : $class; + return basename(str_replace('\\', '/', $class)); + } +} + +if (!function_exists('class_uses_recursive')) { + /** + *获取一个类里所有用到的trait,包括父类的 + * + * @param $class + * @return array + */ + function class_uses_recursive($class) + { + if (is_object($class)) { + $class = get_class($class); + } + + $results = []; + $classes = array_merge([$class => $class], class_parents($class)); + foreach ($classes as $class) { + $results += trait_uses_recursive($class); + } + + return array_unique($results); + } +} + +if (!function_exists('config')) { + /** + * 获取和设置配置参数 + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @return mixed + */ + function config($name = '', $value = null) + { + if (is_null($value) && is_string($name)) { + if ('.' == substr($name, -1)) { + return Config::pull(substr($name, 0, -1)); + } + + return 0 === strpos($name, '?') ? Config::has(substr($name, 1)) : Config::get($name); + } else { + return Config::set($name, $value); + } + } +} + +if (!function_exists('container')) { + /** + * 获取容器对象实例 + * @return Container + */ + function container() + { + return Container::getInstance(); + } +} + +if (!function_exists('controller')) { + /** + * 实例化控制器 格式:[模块/]控制器 + * @param string $name 资源地址 + * @param string $layer 控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return \think\Controller + */ + function controller($name, $layer = 'controller', $appendSuffix = false) + { + return app()->controller($name, $layer, $appendSuffix); + } +} + +if (!function_exists('cookie')) { + /** + * Cookie管理 + * @param string|array $name cookie名称,如果为数组表示进行cookie设置 + * @param mixed $value cookie值 + * @param mixed $option 参数 + * @return mixed + */ + function cookie($name, $value = '', $option = null) + { + if (is_array($name)) { + // 初始化 + Cookie::init($name); + } elseif (is_null($name)) { + // 清除 + Cookie::clear($value); + } elseif ('' === $value) { + // 获取 + return 0 === strpos($name, '?') ? Cookie::has(substr($name, 1), $option) : Cookie::get($name); + } elseif (is_null($value)) { + // 删除 + return Cookie::delete($name); + } else { + // 设置 + return Cookie::set($name, $value, $option); + } + } +} + +if (!function_exists('db')) { + /** + * 实例化数据库类 + * @param string $name 操作的数据表名称(不含前缀) + * @param array|string $config 数据库配置参数 + * @param bool $force 是否强制重新连接 + * @return \think\db\Query + */ + function db($name = '', $config = [], $force = true) + { + return Db::connect($config, $force)->name($name); + } +} + +if (!function_exists('debug')) { + /** + * 记录时间(微秒)和内存使用情况 + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 如果是m 表示统计内存占用 + * @return mixed + */ + function debug($start, $end = '', $dec = 6) + { + if ('' == $end) { + Debug::remark($start); + } else { + return 'm' == $dec ? Debug::getRangeMem($start, $end) : Debug::getRangeTime($start, $end, $dec); + } + } +} + +if (!function_exists('download')) { + /** + * 获取\think\response\Download对象实例 + * @param string $filename 要下载的文件 + * @param string $name 显示文件名 + * @param bool $content 是否为内容 + * @param integer $expire 有效期(秒) + * @return \think\response\Download + */ + function download($filename, $name = '', $content = false, $expire = 360, $openinBrowser = false) + { + return Response::create($filename, 'download')->name($name)->isContent($content)->expire($expire)->openinBrowser($openinBrowser); + } +} + +if (!function_exists('dump')) { + /** + * 浏览器友好的变量输出 + * @param mixed $var 变量 + * @param boolean $echo 是否输出 默认为true 如果为false 则返回输出字符串 + * @param string $label 标签 默认为空 + * @return void|string + */ + function dump($var, $echo = true, $label = null) + { + return Debug::dump($var, $echo, $label); + } +} + +if (!function_exists('env')) { + /** + * 获取环境变量值 + * @access public + * @param string $name 环境变量名(支持二级 .号分割) + * @param string $default 默认值 + * @return mixed + */ + function env($name = null, $default = null) + { + return Env::get($name, $default); + } +} + +if (!function_exists('exception')) { + /** + * 抛出异常处理 + * + * @param string $msg 异常消息 + * @param integer $code 异常代码 默认为0 + * @param string $exception 异常类 + * + * @throws Exception + */ + function exception($msg, $code = 0, $exception = '') + { + $e = $exception ?: '\think\Exception'; + throw new $e($msg, $code); + } +} + +if (!function_exists('halt')) { + /** + * 调试变量并且中断输出 + * @param mixed $var 调试变量或者信息 + */ + function halt($var) + { + dump($var); + + throw new HttpResponseException(new Response); + } +} + +if (!function_exists('input')) { + /** + * 获取输入数据 支持默认值和过滤 + * @param string $key 获取的变量名 + * @param mixed $default 默认值 + * @param string $filter 过滤方法 + * @return mixed + */ + function input($key = '', $default = null, $filter = '') + { + if (0 === strpos($key, '?')) { + $key = substr($key, 1); + $has = true; + } + + if ($pos = strpos($key, '.')) { + // 指定参数来源 + $method = substr($key, 0, $pos); + if (in_array($method, ['get', 'post', 'put', 'patch', 'delete', 'route', 'param', 'request', 'session', 'cookie', 'server', 'env', 'path', 'file'])) { + $key = substr($key, $pos + 1); + } else { + $method = 'param'; + } + } else { + // 默认为自动判断 + $method = 'param'; + } + + if (isset($has)) { + return request()->has($key, $method, $default); + } else { + return request()->$method($key, $default, $filter); + } + } +} + +if (!function_exists('json')) { + /** + * 获取\think\response\Json对象实例 + * @param mixed $data 返回的数据 + * @param integer $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Json + */ + function json($data = [], $code = 200, $header = [], $options = []) + { + return Response::create($data, 'json', $code, $header, $options); + } +} + +if (!function_exists('jsonp')) { + /** + * 获取\think\response\Jsonp对象实例 + * @param mixed $data 返回的数据 + * @param integer $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Jsonp + */ + function jsonp($data = [], $code = 200, $header = [], $options = []) + { + return Response::create($data, 'jsonp', $code, $header, $options); + } +} + +if (!function_exists('lang')) { + /** + * 获取语言变量值 + * @param string $name 语言变量名 + * @param array $vars 动态变量值 + * @param string $lang 语言 + * @return mixed + */ + function lang($name, $vars = [], $lang = '') + { + return Lang::get($name, $vars, $lang); + } +} + +if (!function_exists('model')) { + /** + * 实例化Model + * @param string $name Model名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return \think\Model + */ + function model($name = '', $layer = 'model', $appendSuffix = false) + { + return app()->model($name, $layer, $appendSuffix); + } +} + +if (!function_exists('parse_name')) { + /** + * 字符串命名风格转换 + * type 0 将Java风格转换为C的风格 1 将C风格转换为Java的风格 + * @param string $name 字符串 + * @param integer $type 转换类型 + * @param bool $ucfirst 首字母是否大写(驼峰规则) + * @return string + */ + function parse_name($name, $type = 0, $ucfirst = true) + { + if ($type) { + $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), "_")); + } + } +} + +if (!function_exists('redirect')) { + /** + * 获取\think\response\Redirect对象实例 + * @param mixed $url 重定向地址 支持Url::build方法的地址 + * @param array|integer $params 额外参数 + * @param integer $code 状态码 + * @return \think\response\Redirect + */ + function redirect($url = [], $params = [], $code = 302) + { + if (is_integer($params)) { + $code = $params; + $params = []; + } + + return Response::create($url, 'redirect', $code)->params($params); + } +} + +if (!function_exists('request')) { + /** + * 获取当前Request对象实例 + * @return Request + */ + function request() + { + return app('request'); + } +} + +if (!function_exists('response')) { + /** + * 创建普通 Response 对象实例 + * @param mixed $data 输出数据 + * @param int|string $code 状态码 + * @param array $header 头信息 + * @param string $type + * @return Response + */ + function response($data = '', $code = 200, $header = [], $type = 'html') + { + return Response::create($data, $type, $code, $header); + } +} + +if (!function_exists('route')) { + /** + * 路由注册 + * @param string $rule 路由规则 + * @param mixed $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + function route($rule, $route, $option = [], $pattern = []) + { + return Route::rule($rule, $route, '*', $option, $pattern); + } +} + +if (!function_exists('session')) { + /** + * Session管理 + * @param string|array $name session名称,如果为数组表示进行session设置 + * @param mixed $value session值 + * @param string $prefix 前缀 + * @return mixed + */ + function session($name, $value = '', $prefix = null) + { + if (is_array($name)) { + // 初始化 + Session::init($name); + } elseif (is_null($name)) { + // 清除 + Session::clear($value); + } elseif ('' === $value) { + // 判断或获取 + return 0 === strpos($name, '?') ? Session::has(substr($name, 1), $prefix) : Session::get($name, $prefix); + } elseif (is_null($value)) { + // 删除 + return Session::delete($name, $prefix); + } else { + // 设置 + return Session::set($name, $value, $prefix); + } + } +} + +if (!function_exists('token')) { + /** + * 生成表单令牌 + * @param string $name 令牌名称 + * @param mixed $type 令牌生成方法 + * @return string + */ + function token($name = '__token__', $type = 'md5') + { + $token = Request::token($name, $type); + + return ''; + } +} + +if (!function_exists('trace')) { + /** + * 记录日志信息 + * @param mixed $log log信息 支持字符串和数组 + * @param string $level 日志级别 + * @return array|void + */ + function trace($log = '[think]', $level = 'log') + { + if ('[think]' === $log) { + return Log::getLog(); + } else { + Log::record($log, $level); + } + } +} + +if (!function_exists('trait_uses_recursive')) { + /** + * 获取一个trait里所有引用到的trait + * + * @param string $trait + * @return array + */ + function trait_uses_recursive($trait) + { + $traits = class_uses($trait); + foreach ($traits as $trait) { + $traits += trait_uses_recursive($trait); + } + + return $traits; + } +} + +if (!function_exists('url')) { + /** + * Url生成 + * @param string $url 路由地址 + * @param string|array $vars 变量 + * @param bool|string $suffix 生成的URL后缀 + * @param bool|string $domain 域名 + * @return string + */ + function url($url = '', $vars = '', $suffix = true, $domain = false) + { + return Url::build($url, $vars, $suffix, $domain); + } +} + +if (!function_exists('validate')) { + /** + * 实例化验证器 + * @param string $name 验证器名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return \think\Validate + */ + function validate($name = '', $layer = 'validate', $appendSuffix = false) + { + return app()->validate($name, $layer, $appendSuffix); + } +} + +if (!function_exists('view')) { + /** + * 渲染模板输出 + * @param string $template 模板文件 + * @param array $vars 模板变量 + * @param integer $code 状态码 + * @param callable $filter 内容过滤 + * @return \think\response\View + */ + function view($template = '', $vars = [], $code = 200, $filter = null) + { + return Response::create($template, 'view', $code)->assign($vars)->filter($filter); + } +} + +if (!function_exists('widget')) { + /** + * 渲染输出Widget + * @param string $name Widget名称 + * @param array $data 传入的参数 + * @return mixed + */ + function widget($name, $data = []) + { + $result = app()->action($name, $data, 'widget'); + + if (is_object($result)) { + $result = $result->getContent(); + } + + return $result; + } +} + +if (!function_exists('xml')) { + /** + * 获取\think\response\Xml对象实例 + * @param mixed $data 返回的数据 + * @param integer $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Xml + */ + function xml($data = [], $code = 200, $header = [], $options = []) + { + return Response::create($data, 'xml', $code, $header, $options); + } +} + +if (!function_exists('yaconf')) { + /** + * 获取yaconf配置 + * + * @param string $name 配置参数名 + * @param mixed $default 默认值 + * @return mixed + */ + function yaconf($name, $default = null) + { + return Config::yaconf($name, $default); + } +} diff --git a/thinkphp/lang/zh-cn.php b/thinkphp/lang/zh-cn.php new file mode 100644 index 0000000..1e05082 --- /dev/null +++ b/thinkphp/lang/zh-cn.php @@ -0,0 +1,144 @@ + +// +---------------------------------------------------------------------- + +// 核心中文语言包 +return [ + // 系统错误提示 + 'Undefined variable' => '未定义变量', + 'Undefined index' => '未定义数组索引', + 'Undefined offset' => '未定义数组下标', + 'Parse error' => '语法解析错误', + 'Type error' => '类型错误', + 'Fatal error' => '致命错误', + 'syntax error' => '语法错误', + + // 框架核心错误提示 + 'dispatch type not support' => '不支持的调度类型', + 'method param miss' => '方法参数错误', + 'method not exists' => '方法不存在', + 'function not exists' => '函数不存在', + 'file 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' => '查询表达式错误', + 'order express error' => '排序表达式错误', + 'no data to update' => '没有任何数据需要更新', + 'miss data to insert' => '缺少需要写入的数据', + 'not support data' => '不支持的数据表达式', + '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方法', + 'route pattern error' => '路由变量规则定义错误', + 'route behavior will not support' => '路由行为废弃(使用中间件替代)', + '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} 创建失败!', + + 'The middleware must return Response instance' => '中间件方法必须返回Response对象实例', + 'The queue was exhausted, with no response returned' => '中间件队列为空', + // Validate Error Message + ':attribute require' => ':attribute不能为空', + ':attribute must' => ':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 new file mode 100644 index 0000000..f037a95 --- /dev/null +++ b/thinkphp/library/think/App.php @@ -0,0 +1,979 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ClassNotFoundException; +use think\exception\HttpResponseException; +use think\route\Dispatch; + +/** + * App 应用管理 + */ +class App extends Container +{ + const VERSION = '5.1.42 LTS'; + + /** + * 当前模块路径 + * @var string + */ + protected $modulePath; + + /** + * 应用调试模式 + * @var bool + */ + protected $appDebug = true; + + /** + * 应用开始时间 + * @var float + */ + protected $beginTime; + + /** + * 应用内存初始占用 + * @var integer + */ + protected $beginMem; + + /** + * 应用类库命名空间 + * @var string + */ + protected $namespace = 'app'; + + /** + * 应用类库后缀 + * @var bool + */ + protected $suffix = false; + + /** + * 严格路由检测 + * @var bool + */ + protected $routeMust; + + /** + * 应用类库目录 + * @var string + */ + protected $appPath; + + /** + * 框架目录 + * @var string + */ + protected $thinkPath; + + /** + * 应用根目录 + * @var string + */ + protected $rootPath; + + /** + * 运行时目录 + * @var string + */ + protected $runtimePath; + + /** + * 配置目录 + * @var string + */ + protected $configPath; + + /** + * 路由目录 + * @var string + */ + protected $routePath; + + /** + * 配置后缀 + * @var string + */ + protected $configExt; + + /** + * 应用调度实例 + * @var Dispatch + */ + protected $dispatch; + + /** + * 绑定模块(控制器) + * @var string + */ + protected $bindModule; + + /** + * 初始化 + * @var bool + */ + protected $initialized = false; + + public function __construct($appPath = '') + { + $this->thinkPath = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR; + $this->path($appPath); + } + + /** + * 绑定模块或者控制器 + * @access public + * @param string $bind + * @return $this + */ + public function bind($bind) + { + $this->bindModule = $bind; + return $this; + } + + /** + * 设置应用类库目录 + * @access public + * @param string $path 路径 + * @return $this + */ + public function path($path) + { + $this->appPath = $path ? realpath($path) . DIRECTORY_SEPARATOR : $this->getAppPath(); + + return $this; + } + + /** + * 初始化应用 + * @access public + * @return void + */ + public function initialize() + { + if ($this->initialized) { + return; + } + + $this->initialized = true; + $this->beginTime = microtime(true); + $this->beginMem = memory_get_usage(); + + $this->rootPath = dirname($this->appPath) . DIRECTORY_SEPARATOR; + $this->runtimePath = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR; + $this->routePath = $this->rootPath . 'route' . DIRECTORY_SEPARATOR; + $this->configPath = $this->rootPath . 'config' . DIRECTORY_SEPARATOR; + + static::setInstance($this); + + $this->instance('app', $this); + + // 加载环境变量配置文件 + if (is_file($this->rootPath . '.env')) { + $this->env->load($this->rootPath . '.env'); + } + + $this->configExt = $this->env->get('config_ext', '.php'); + + // 加载惯例配置文件 + $this->config->set(include $this->thinkPath . 'convention.php'); + + // 设置路径环境变量 + $this->env->set([ + 'think_path' => $this->thinkPath, + 'root_path' => $this->rootPath, + 'app_path' => $this->appPath, + 'config_path' => $this->configPath, + 'route_path' => $this->routePath, + 'runtime_path' => $this->runtimePath, + 'extend_path' => $this->rootPath . 'extend' . DIRECTORY_SEPARATOR, + 'vendor_path' => $this->rootPath . 'vendor' . DIRECTORY_SEPARATOR, + ]); + + $this->namespace = $this->env->get('app_namespace', $this->namespace); + $this->env->set('app_namespace', $this->namespace); + + // 注册应用命名空间 + Loader::addNamespace($this->namespace, $this->appPath); + + // 初始化应用 + $this->init(); + + // 开启类名后缀 + $this->suffix = $this->config('app.class_suffix'); + + // 应用调试模式 + $this->appDebug = $this->env->get('app_debug', $this->config('app.app_debug')); + $this->env->set('app_debug', $this->appDebug); + + if (!$this->appDebug) { + ini_set('display_errors', 'Off'); + } elseif (PHP_SAPI != 'cli') { + //重新申请一块比较大的buffer + if (ob_get_level() > 0) { + $output = ob_get_clean(); + } + ob_start(); + if (!empty($output)) { + echo $output; + } + } + + // 注册异常处理类 + if ($this->config('app.exception_handle')) { + Error::setExceptionHandler($this->config('app.exception_handle')); + } + + // 注册根命名空间 + if (!empty($this->config('app.root_namespace'))) { + Loader::addNamespace($this->config('app.root_namespace')); + } + + // 加载composer autofile文件 + Loader::loadComposerAutoloadFiles(); + + // 注册类库别名 + Loader::addClassAlias($this->config->pull('alias')); + + // 数据库配置初始化 + Db::init($this->config->pull('database')); + + // 设置系统时区 + date_default_timezone_set($this->config('app.default_timezone')); + + // 读取语言包 + $this->loadLangPack(); + + // 路由初始化 + $this->routeInit(); + } + + /** + * 初始化应用或模块 + * @access public + * @param string $module 模块名 + * @return void + */ + public function init($module = '') + { + // 定位模块目录 + $module = $module ? $module . DIRECTORY_SEPARATOR : ''; + $path = $this->appPath . $module; + + // 加载初始化文件 + if (is_file($path . 'init.php')) { + include $path . 'init.php'; + } elseif (is_file($this->runtimePath . $module . 'init.php')) { + include $this->runtimePath . $module . 'init.php'; + } else { + // 加载行为扩展文件 + if (is_file($path . 'tags.php')) { + $tags = include $path . 'tags.php'; + if (is_array($tags)) { + $this->hook->import($tags); + } + } + + // 加载公共文件 + if (is_file($path . 'common.php')) { + include_once $path . 'common.php'; + } + + if ('' == $module) { + // 加载系统助手函数 + include $this->thinkPath . 'helper.php'; + } + + // 加载中间件 + if (is_file($path . 'middleware.php')) { + $middleware = include $path . 'middleware.php'; + if (is_array($middleware)) { + $this->middleware->import($middleware); + } + } + + // 注册服务的容器对象实例 + if (is_file($path . 'provider.php')) { + $provider = include $path . 'provider.php'; + if (is_array($provider)) { + $this->bindTo($provider); + } + } + + // 自动读取配置文件 + if (is_dir($path . 'config')) { + $dir = $path . 'config' . DIRECTORY_SEPARATOR; + } elseif (is_dir($this->configPath . $module)) { + $dir = $this->configPath . $module; + } + + $files = isset($dir) ? scandir($dir) : []; + + foreach ($files as $file) { + if ('.' . pathinfo($file, PATHINFO_EXTENSION) === $this->configExt) { + $this->config->load($dir . $file, pathinfo($file, PATHINFO_FILENAME)); + } + } + } + + $this->setModulePath($path); + + if ($module) { + // 对容器中的对象实例进行配置更新 + $this->containerConfigUpdate($module); + } + } + + protected function containerConfigUpdate($module) + { + $config = $this->config->get(); + + // 注册异常处理类 + if ($config['app']['exception_handle']) { + Error::setExceptionHandler($config['app']['exception_handle']); + } + + Db::init($config['database']); + $this->middleware->setConfig($config['middleware']); + $this->route->setConfig($config['app']); + $this->request->init($config['app']); + $this->cookie->init($config['cookie']); + $this->view->init($config['template']); + $this->log->init($config['log']); + $this->session->setConfig($config['session']); + $this->debug->setConfig($config['trace']); + $this->cache->init($config['cache'], true); + + // 加载当前模块语言包 + $this->lang->load($this->appPath . $module . DIRECTORY_SEPARATOR . 'lang' . DIRECTORY_SEPARATOR . $this->request->langset() . '.php'); + + // 模块请求缓存检查 + $this->checkRequestCache( + $config['app']['request_cache'], + $config['app']['request_cache_expire'], + $config['app']['request_cache_except'] + ); + } + + /** + * 执行应用程序 + * @access public + * @return Response + * @throws Exception + */ + public function run() + { + try { + // 初始化应用 + $this->initialize(); + + // 监听app_init + $this->hook->listen('app_init'); + + if ($this->bindModule) { + // 模块/控制器绑定 + $this->route->bind($this->bindModule); + } elseif ($this->config('app.auto_bind_module')) { + // 入口自动绑定 + $name = pathinfo($this->request->baseFile(), PATHINFO_FILENAME); + if ($name && 'index' != $name && is_dir($this->appPath . $name)) { + $this->route->bind($name); + } + } + + // 监听app_dispatch + $this->hook->listen('app_dispatch'); + + $dispatch = $this->dispatch; + + if (empty($dispatch)) { + // 路由检测 + $dispatch = $this->routeCheck()->init(); + } + + // 记录当前调度信息 + $this->request->dispatch($dispatch); + + // 记录路由和请求信息 + if ($this->appDebug) { + $this->log('[ ROUTE ] ' . var_export($this->request->routeInfo(), true)); + $this->log('[ HEADER ] ' . var_export($this->request->header(), true)); + $this->log('[ PARAM ] ' . var_export($this->request->param(), true)); + } + + // 监听app_begin + $this->hook->listen('app_begin'); + + // 请求缓存检查 + $this->checkRequestCache( + $this->config('request_cache'), + $this->config('request_cache_expire'), + $this->config('request_cache_except') + ); + + $data = null; + } catch (HttpResponseException $exception) { + $dispatch = null; + $data = $exception->getResponse(); + } + + $this->middleware->add(function (Request $request, $next) use ($dispatch, $data) { + return is_null($data) ? $dispatch->run() : $data; + }); + + $response = $this->middleware->dispatch($this->request); + + // 监听app_end + $this->hook->listen('app_end', $response); + + return $response; + } + + protected function getRouteCacheKey() + { + if ($this->config->get('route_check_cache_key')) { + $closure = $this->config->get('route_check_cache_key'); + $routeKey = $closure($this->request); + } else { + $routeKey = md5($this->request->baseUrl(true) . ':' . $this->request->method()); + } + + return $routeKey; + } + + protected function loadLangPack() + { + // 读取默认语言 + $this->lang->range($this->config('app.default_lang')); + + if ($this->config('app.lang_switch_on')) { + // 开启多语言机制 检测当前语言 + $this->lang->detect(); + } + + $this->request->setLangset($this->lang->range()); + + // 加载系统语言包 + $this->lang->load([ + $this->thinkPath . 'lang' . DIRECTORY_SEPARATOR . $this->request->langset() . '.php', + $this->appPath . 'lang' . DIRECTORY_SEPARATOR . $this->request->langset() . '.php', + ]); + } + + /** + * 设置当前地址的请求缓存 + * @access public + * @param string $key 缓存标识,支持变量规则 ,例如 item/:name/:id + * @param mixed $expire 缓存有效期 + * @param array $except 缓存排除 + * @param string $tag 缓存标签 + * @return void + */ + public function checkRequestCache($key, $expire = null, $except = [], $tag = null) + { + $cache = $this->request->cache($key, $expire, $except, $tag); + + if ($cache) { + $this->setResponseCache($cache); + } + } + + public function setResponseCache($cache) + { + list($key, $expire, $tag) = $cache; + + if (strtotime($this->request->server('HTTP_IF_MODIFIED_SINCE')) + $expire > $this->request->server('REQUEST_TIME')) { + // 读取缓存 + $response = Response::create()->code(304); + throw new HttpResponseException($response); + } elseif ($this->cache->has($key)) { + list($content, $header) = $this->cache->get($key); + + $response = Response::create($content)->header($header); + throw new HttpResponseException($response); + } + } + + /** + * 设置当前请求的调度信息 + * @access public + * @param Dispatch $dispatch 调度信息 + * @return $this + */ + public function dispatch(Dispatch $dispatch) + { + $this->dispatch = $dispatch; + return $this; + } + + /** + * 记录调试信息 + * @access public + * @param mixed $msg 调试信息 + * @param string $type 信息类型 + * @return void + */ + public function log($msg, $type = 'info') + { + $this->appDebug && $this->log->record($msg, $type); + } + + /** + * 获取配置参数 为空则获取所有配置 + * @access public + * @param string $name 配置参数名(支持二级配置 .号分割) + * @return mixed + */ + public function config($name = '') + { + return $this->config->get($name); + } + + /** + * 路由初始化 导入路由定义规则 + * @access public + * @return void + */ + public function routeInit() + { + // 路由检测 + if (is_dir($this->routePath)) { + $files = glob($this->routePath . '*.php'); + foreach ($files as $file) { + $rules = include $file; + if (is_array($rules)) { + $this->route->import($rules); + } + } + } + + if ($this->route->config('route_annotation')) { + // 自动生成路由定义 + if ($this->appDebug) { + $suffix = $this->route->config('controller_suffix') || $this->route->config('class_suffix'); + $this->build->buildRoute($suffix); + } + + $filename = $this->runtimePath . 'build_route.php'; + + if (is_file($filename)) { + include $filename; + } + } + } + + /** + * URL路由检测(根据PATH_INFO) + * @access public + * @return Dispatch + */ + public function routeCheck() + { + // 检测路由缓存 + if (!$this->appDebug && $this->config->get('route_check_cache')) { + $routeKey = $this->getRouteCacheKey(); + $option = $this->config->get('route_cache_option'); + + if ($option && $this->cache->connect($option)->has($routeKey)) { + return $this->cache->connect($option)->get($routeKey); + } elseif ($this->cache->has($routeKey)) { + return $this->cache->get($routeKey); + } + } + + // 获取应用调度信息 + $path = $this->request->path(); + + // 是否强制路由模式 + $must = !is_null($this->routeMust) ? $this->routeMust : $this->route->config('url_route_must'); + + // 路由检测 返回一个Dispatch对象 + $dispatch = $this->route->check($path, $must); + + if (!empty($routeKey)) { + try { + if ($option) { + $this->cache->connect($option)->tag('route_cache')->set($routeKey, $dispatch); + } else { + $this->cache->tag('route_cache')->set($routeKey, $dispatch); + } + } catch (\Exception $e) { + // 存在闭包的时候缓存无效 + } + } + + return $dispatch; + } + + /** + * 设置应用的路由检测机制 + * @access public + * @param bool $must 是否强制检测路由 + * @return $this + */ + public function routeMust($must = false) + { + $this->routeMust = $must; + return $this; + } + + /** + * 解析模块和类名 + * @access protected + * @param string $name 资源地址 + * @param string $layer 验证层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return array + */ + protected function parseModuleAndClass($name, $layer, $appendSuffix) + { + if (false !== strpos($name, '\\')) { + $class = $name; + $module = $this->request->module(); + } else { + if (strpos($name, '/')) { + list($module, $name) = explode('/', $name, 2); + } else { + $module = $this->request->module(); + } + + $class = $this->parseClass($module, $layer, $name, $appendSuffix); + } + + return [$module, $class]; + } + + /** + * 实例化应用类库 + * @access public + * @param string $name 类名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $common 公共模块名 + * @return object + * @throws ClassNotFoundException + */ + public function create($name, $layer, $appendSuffix = false, $common = 'common') + { + $guid = $name . $layer; + + if ($this->__isset($guid)) { + return $this->__get($guid); + } + + list($module, $class) = $this->parseModuleAndClass($name, $layer, $appendSuffix); + + if (class_exists($class)) { + $object = $this->__get($class); + } else { + $class = str_replace('\\' . $module . '\\', '\\' . $common . '\\', $class); + if (class_exists($class)) { + $object = $this->__get($class); + } else { + throw new ClassNotFoundException('class not exists:' . $class, $class); + } + } + + $this->__set($guid, $class); + + return $object; + } + + /** + * 实例化(分层)模型 + * @access public + * @param string $name Model名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $common 公共模块名 + * @return Model + * @throws ClassNotFoundException + */ + public function model($name = '', $layer = 'model', $appendSuffix = false, $common = 'common') + { + return $this->create($name, $layer, $appendSuffix, $common); + } + + /** + * 实例化(分层)控制器 格式:[模块名/]控制器名 + * @access public + * @param string $name 资源地址 + * @param string $layer 控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $empty 空控制器名称 + * @return object + * @throws ClassNotFoundException + */ + public function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '') + { + list($module, $class) = $this->parseModuleAndClass($name, $layer, $appendSuffix); + + if (class_exists($class)) { + return $this->make($class, true); + } elseif ($empty && class_exists($emptyClass = $this->parseClass($module, $layer, $empty, $appendSuffix))) { + return $this->make($emptyClass, true); + } + + throw new ClassNotFoundException('class not exists:' . $class, $class); + } + + /** + * 实例化验证类 格式:[模块名/]验证器名 + * @access public + * @param string $name 资源地址 + * @param string $layer 验证层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $common 公共模块名 + * @return Validate + * @throws ClassNotFoundException + */ + public function validate($name = '', $layer = 'validate', $appendSuffix = false, $common = 'common') + { + $name = $name ?: $this->config('default_validate'); + + if (empty($name)) { + return new Validate; + } + + return $this->create($name, $layer, $appendSuffix, $common); + } + + /** + * 数据库初始化 + * @access public + * @param mixed $config 数据库配置 + * @param bool|string $name 连接标识 true 强制重新连接 + * @return \think\db\Query + */ + public function db($config = [], $name = false) + { + return Db::connect($config, $name); + } + + /** + * 远程调用模块的操作方法 参数格式 [模块/控制器/]操作 + * @access public + * @param string $url 调用地址 + * @param string|array $vars 调用参数 支持字符串和数组 + * @param string $layer 要调用的控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return mixed + * @throws ClassNotFoundException + */ + public function action($url, $vars = [], $layer = 'controller', $appendSuffix = false) + { + $info = pathinfo($url); + $action = $info['basename']; + $module = '.' != $info['dirname'] ? $info['dirname'] : $this->request->controller(); + $class = $this->controller($module, $layer, $appendSuffix); + + if (is_scalar($vars)) { + if (strpos($vars, '=')) { + parse_str($vars, $vars); + } else { + $vars = [$vars]; + } + } + + return $this->invokeMethod([$class, $action . $this->config('action_suffix')], $vars); + } + + /** + * 解析应用类的类名 + * @access public + * @param string $module 模块名 + * @param string $layer 层名 controller model ... + * @param string $name 类名 + * @param bool $appendSuffix + * @return string + */ + public function parseClass($module, $layer, $name, $appendSuffix = false) + { + $name = str_replace(['/', '.'], '\\', $name); + $array = explode('\\', $name); + $class = Loader::parseName(array_pop($array), 1) . ($this->suffix || $appendSuffix ? ucfirst($layer) : ''); + $path = $array ? implode('\\', $array) . '\\' : ''; + + return $this->namespace . '\\' . ($module ? $module . '\\' : '') . $layer . '\\' . $path . $class; + } + + /** + * 获取框架版本 + * @access public + * @return string + */ + public function version() + { + return static::VERSION; + } + + /** + * 是否为调试模式 + * @access public + * @return bool + */ + public function isDebug() + { + return $this->appDebug; + } + + /** + * 获取模块路径 + * @access public + * @return string + */ + public function getModulePath() + { + return $this->modulePath; + } + + /** + * 设置模块路径 + * @access public + * @param string $path 路径 + * @return void + */ + public function setModulePath($path) + { + $this->modulePath = $path; + $this->env->set('module_path', $path); + } + + /** + * 获取应用根目录 + * @access public + * @return string + */ + public function getRootPath() + { + return $this->rootPath; + } + + /** + * 获取应用类库目录 + * @access public + * @return string + */ + public function getAppPath() + { + if (is_null($this->appPath)) { + $this->appPath = Loader::getRootPath() . 'application' . DIRECTORY_SEPARATOR; + } + + return $this->appPath; + } + + /** + * 获取应用运行时目录 + * @access public + * @return string + */ + public function getRuntimePath() + { + return $this->runtimePath; + } + + /** + * 获取核心框架目录 + * @access public + * @return string + */ + public function getThinkPath() + { + return $this->thinkPath; + } + + /** + * 获取路由目录 + * @access public + * @return string + */ + public function getRoutePath() + { + return $this->routePath; + } + + /** + * 获取应用配置目录 + * @access public + * @return string + */ + public function getConfigPath() + { + return $this->configPath; + } + + /** + * 获取配置后缀 + * @access public + * @return string + */ + public function getConfigExt() + { + return $this->configExt; + } + + /** + * 获取应用类库命名空间 + * @access public + * @return string + */ + public function getNamespace() + { + return $this->namespace; + } + + /** + * 设置应用类库命名空间 + * @access public + * @param string $namespace 命名空间名称 + * @return $this + */ + public function setNamespace($namespace) + { + $this->namespace = $namespace; + return $this; + } + + /** + * 是否启用类库后缀 + * @access public + * @return bool + */ + public function getSuffix() + { + return $this->suffix; + } + + /** + * 获取应用开启时间 + * @access public + * @return float + */ + public function getBeginTime() + { + return $this->beginTime; + } + + /** + * 获取应用初始内存占用 + * @access public + * @return integer + */ + public function getBeginMem() + { + return $this->beginMem; + } + +} diff --git a/thinkphp/library/think/Build.php b/thinkphp/library/think/Build.php new file mode 100644 index 0000000..7a531d7 --- /dev/null +++ b/thinkphp/library/think/Build.php @@ -0,0 +1,415 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Build +{ + /** + * 应用对象 + * @var App + */ + protected $app; + + /** + * 应用目录 + * @var string + */ + protected $basePath; + + public function __construct(App $app) + { + $this->app = $app; + $this->basePath = $this->app->getAppPath(); + } + + /** + * 根据传入的build资料创建目录和文件 + * @access public + * @param array $build build列表 + * @param string $namespace 应用类库命名空间 + * @param bool $suffix 类库后缀 + * @return void + */ + public function run(array $build = [], $namespace = 'app', $suffix = false) + { + // 锁定 + $lockfile = $this->basePath . 'build.lock'; + + if (is_writable($lockfile)) { + return; + } elseif (!touch($lockfile)) { + throw new Exception('应用目录[' . $this->basePath . ']不可写,目录无法自动生成!
              请手动生成项目目录~', 10006); + } + + foreach ($build as $module => $list) { + if ('__dir__' == $module) { + // 创建目录列表 + $this->buildDir($list); + } elseif ('__file__' == $module) { + // 创建文件列表 + $this->buildFile($list); + } else { + // 创建模块 + $this->module($module, $list, $namespace, $suffix); + } + } + + // 解除锁定 + unlink($lockfile); + } + + /** + * 创建目录 + * @access protected + * @param array $list 目录列表 + * @return void + */ + protected function buildDir($list) + { + foreach ($list as $dir) { + $this->checkDirBuild($this->basePath . $dir); + } + } + + /** + * 创建文件 + * @access protected + * @param array $list 文件列表 + * @return void + */ + protected function buildFile($list) + { + foreach ($list as $file) { + if (!is_dir($this->basePath . dirname($file))) { + // 创建目录 + mkdir($this->basePath . dirname($file), 0755, true); + } + + if (!is_file($this->basePath . $file)) { + file_put_contents($this->basePath . $file, 'php' == pathinfo($file, PATHINFO_EXTENSION) ? "basePath . $module)) { + // 创建模块目录 + mkdir($this->basePath . $module); + } + + if (basename($this->app->getRuntimePath()) != $module) { + // 创建配置文件和公共文件 + $this->buildCommon($module); + // 创建模块的默认页面 + $this->buildHello($module, $namespace, $suffix); + } + + if (empty($list)) { + // 创建默认的模块目录和文件 + $list = [ + '__file__' => ['common.php'], + '__dir__' => ['controller', 'model', 'view', 'config'], + ]; + } + + // 创建子目录和文件 + foreach ($list as $path => $file) { + $modulePath = $this->basePath . $module . DIRECTORY_SEPARATOR; + if ('__dir__' == $path) { + // 生成子目录 + foreach ($file as $dir) { + $this->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) ? "checkDirBuild(dirname($filename)); + $content = ''; + break; + default: + // 其他文件 + $content = "app->getNameSpace(); + $content = 'app->config('app.url_controller_layer'); + } + + if ($this->app->config('app.app_multi_module')) { + $modules = glob($this->basePath . '*', GLOB_ONLYDIR); + + foreach ($modules as $module) { + $module = basename($module); + + if (in_array($module, $this->app->config('app.deny_module_list'))) { + continue; + } + + $path = $this->basePath . $module . DIRECTORY_SEPARATOR . $layer . DIRECTORY_SEPARATOR; + $content .= $this->buildDirRoute($path, $namespace, $module, $suffix, $layer); + } + } else { + $path = $this->basePath . $layer . DIRECTORY_SEPARATOR; + $content .= $this->buildDirRoute($path, $namespace, '', $suffix, $layer); + } + + $filename = $this->app->getRuntimePath() . 'build_route.php'; + file_put_contents($filename, $content); + + return $filename; + } + + /** + * 生成子目录控制器类的路由规则 + * @access protected + * @param string $path 控制器目录 + * @param string $namespace 应用命名空间 + * @param string $module 模块 + * @param bool $suffix 类库后缀 + * @param string $layer 控制器层目录名 + * @return string + */ + protected function buildDirRoute($path, $namespace, $module, $suffix, $layer) + { + $content = ''; + $controllers = glob($path . '*.php'); + + foreach ($controllers as $controller) { + $controller = basename($controller, '.php'); + + $class = new \ReflectionClass($namespace . '\\' . ($module ? $module . '\\' : '') . $layer . '\\' . $controller); + + if (strpos($layer, '\\')) { + // 多级控制器 + $level = str_replace(DIRECTORY_SEPARATOR, '.', substr($layer, 11)); + $controller = $level . '.' . $controller; + $length = strlen(strstr($layer, '\\', true)); + } else { + $length = strlen($layer); + } + + if ($suffix) { + $controller = substr($controller, 0, -$length); + } + + $content .= $this->getControllerRoute($class, $module, $controller); + } + + $subDir = glob($path . '*', GLOB_ONLYDIR); + + foreach ($subDir as $dir) { + $content .= $this->buildDirRoute($dir . DIRECTORY_SEPARATOR, $namespace, $module, $suffix, $layer . '\\' . basename($dir)); + } + + return $content; + } + + /** + * 生成控制器类的路由规则 + * @access protected + * @param string $class 控制器完整类名 + * @param string $module 模块名 + * @param string $controller 控制器名 + * @return string + */ + protected function getControllerRoute($class, $module, $controller) + { + $content = ''; + $comment = $class->getDocComment(); + + if (false !== strpos($comment, '@route(')) { + $comment = $this->parseRouteComment($comment); + $route = ($module ? $module . '/' : '') . $controller; + $comment = preg_replace('/route\(\s?([\'\"][\-\_\/\:\<\>\?\$\[\]\w]+[\'\"])\s?\)/is', 'Route::resource(\1,\'' . $route . '\')', $comment); + $content .= PHP_EOL . $comment; + } elseif (false !== strpos($comment, '@alias(')) { + $comment = $this->parseRouteComment($comment, '@alias('); + $route = ($module ? $module . '/' : '') . $controller; + $comment = preg_replace('/alias\(\s?([\'\"][\-\_\/\w]+[\'\"])\s?\)/is', 'Route::alias(\1,\'' . $route . '\')', $comment); + $content .= PHP_EOL . $comment; + } + + $methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC); + + foreach ($methods as $method) { + $comment = $this->getMethodRouteComment($module, $controller, $method); + if ($comment) { + $content .= PHP_EOL . $comment; + } + } + + return $content; + } + + /** + * 解析路由注释 + * @access protected + * @param string $comment + * @param string $tag + * @return string + */ + protected function parseRouteComment($comment, $tag = '@route(') + { + $comment = substr($comment, 3, -2); + $comment = explode(PHP_EOL, substr(strstr(trim($comment), $tag), 1)); + $comment = array_map(function ($item) {return trim(trim($item), ' \t*');}, $comment); + + if (count($comment) > 1) { + $key = array_search('', $comment); + $comment = array_slice($comment, 0, false === $key ? 1 : $key); + } + + $comment = implode(PHP_EOL . "\t", $comment) . ';'; + + if (strpos($comment, '{')) { + $comment = preg_replace_callback('/\{\s?.*?\s?\}/s', function ($matches) { + return false !== strpos($matches[0], '"') ? '[' . substr(var_export(json_decode($matches[0], true), true), 7, -1) . ']' : $matches[0]; + }, $comment); + } + return $comment; + } + + /** + * 获取方法的路由注释 + * @access protected + * @param string $module 模块 + * @param string $controller 控制器名 + * @param \ReflectMethod $reflectMethod + * @return string|void + */ + protected function getMethodRouteComment($module, $controller, $reflectMethod) + { + $comment = $reflectMethod->getDocComment(); + + if (false !== strpos($comment, '@route(')) { + $comment = $this->parseRouteComment($comment); + $action = $reflectMethod->getName(); + + if ($suffix = $this->app->config('app.action_suffix')) { + $action = substr($action, 0, -strlen($suffix)); + } + + $route = ($module ? $module . '/' : '') . $controller . '/' . $action; + $comment = preg_replace('/route\s?\(\s?([\'\"][\-\_\/\:\<\>\?\$\[\]\w]+[\'\"])\s?\,?\s?[\'\"]?(\w+?)[\'\"]?\s?\)/is', 'Route::\2(\1,\'' . $route . '\')', $comment); + $comment = preg_replace('/route\s?\(\s?([\'\"][\-\_\/\:\<\>\?\$\[\]\w]+[\'\"])\s?\)/is', 'Route::rule(\1,\'' . $route . '\')', $comment); + + return $comment; + } + } + + /** + * 创建模块的欢迎页面 + * @access protected + * @param string $module 模块名 + * @param string $namespace 应用类库命名空间 + * @param bool $suffix 类库后缀 + * @return void + */ + protected function buildHello($module, $namespace, $suffix = false) + { + $filename = $this->basePath . ($module ? $module . DIRECTORY_SEPARATOR : '') . 'controller' . DIRECTORY_SEPARATOR . 'Index' . ($suffix ? 'Controller' : '') . '.php'; + if (!is_file($filename)) { + $content = file_get_contents($this->app->getThinkPath() . 'tpl' . DIRECTORY_SEPARATOR . 'default_index.tpl'); + $content = str_replace(['{$app}', '{$module}', '{layer}', '{$suffix}'], [$namespace, $module ? $module . '\\' : '', 'controller', $suffix ? 'Controller' : ''], $content); + $this->checkDirBuild(dirname($filename)); + + file_put_contents($filename, $content); + } + } + + /** + * 创建模块的公共文件 + * @access protected + * @param string $module 模块名 + * @return void + */ + protected function buildCommon($module) + { + $filename = $this->app->getConfigPath() . ($module ? $module . DIRECTORY_SEPARATOR : '') . 'app.php'; + $this->checkDirBuild(dirname($filename)); + + if (!is_file($filename)) { + file_put_contents($filename, "basePath . ($module ? $module . DIRECTORY_SEPARATOR : '') . 'common.php'; + + if (!is_file($filename)) { + file_put_contents($filename, " +// +---------------------------------------------------------------------- + +namespace think; + +use think\cache\Driver; + +/** + * Class Cache + * + * @package think + * + * @mixin Driver + * @mixin \think\cache\driver\File + */ +class Cache +{ + /** + * 缓存实例 + * @var array + */ + protected $instance = []; + + /** + * 缓存配置 + * @var array + */ + protected $config = []; + + /** + * 操作句柄 + * @var object + */ + protected $handler; + + public function __construct(array $config = []) + { + $this->config = $config; + $this->init($config); + } + + /** + * 连接缓存 + * @access public + * @param array $options 配置数组 + * @param bool|string $name 缓存连接标识 true 强制重新连接 + * @return Driver + */ + public function connect(array $options = [], $name = false) + { + if (false === $name) { + $name = md5(serialize($options)); + } + + if (true === $name || !isset($this->instance[$name])) { + $type = !empty($options['type']) ? $options['type'] : 'File'; + + if (true === $name) { + $name = md5(serialize($options)); + } + + $this->instance[$name] = Loader::factory($type, '\\think\\cache\\driver\\', $options); + } + + return $this->instance[$name]; + } + + /** + * 自动初始化缓存 + * @access public + * @param array $options 配置数组 + * @param bool $force 强制更新 + * @return Driver + */ + public function init(array $options = [], $force = false) + { + if (is_null($this->handler) || $force) { + + if ('complex' == $options['type']) { + $default = $options['default']; + $options = isset($options[$default['type']]) ? $options[$default['type']] : $default; + } + + $this->handler = $this->connect($options); + } + + return $this->handler; + } + + public static function __make(Config $config) + { + return new static($config->pull('cache')); + } + + public function getConfig() + { + return $this->config; + } + + public function setConfig(array $config) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 切换缓存类型 需要配置 cache.type 为 complex + * @access public + * @param string $name 缓存标识 + * @return Driver + */ + public function store($name = '') + { + if ('' !== $name && 'complex' == $this->config['type']) { + return $this->connect($this->config[$name], strtolower($name)); + } + + return $this->init(); + } + + public function __call($method, $args) + { + return call_user_func_array([$this->init(), $method], $args); + } + +} diff --git a/thinkphp/library/think/Collection.php b/thinkphp/library/think/Collection.php new file mode 100644 index 0000000..d7454ec --- /dev/null +++ b/thinkphp/library/think/Collection.php @@ -0,0 +1,552 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use ArrayAccess; +use ArrayIterator; +use Countable; +use IteratorAggregate; +use JsonSerializable; + +class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable +{ + /** + * 数据集数据 + * @var array + */ + protected $items = []; + + public function __construct($items = []) + { + $this->items = $this->convertToArray($items); + } + + public static function make($items = []) + { + return new static($items); + } + + /** + * 是否为空 + * @access public + * @return bool + */ + public function isEmpty() + { + return empty($this->items); + } + + public function toArray() + { + return array_map(function ($value) { + return ($value instanceof Model || $value instanceof self) ? $value->toArray() : $value; + }, $this->items); + } + + public function all() + { + return $this->items; + } + + /** + * 合并数组 + * + * @access public + * @param mixed $items + * @return static + */ + public function merge($items) + { + return new static(array_merge($this->items, $this->convertToArray($items))); + } + + /** + * 交换数组中的键和值 + * + * @access public + * @return static + */ + public function flip() + { + return new static(array_flip($this->items)); + } + + /** + * 按指定键整理数据 + * + * @access public + * @param mixed $items 数据 + * @param string $indexKey 键名 + * @return array + */ + public function dictionary($items = null, &$indexKey = null) + { + if ($items instanceof self || $items instanceof Paginator) { + $items = $items->all(); + } + + $items = is_null($items) ? $this->items : $items; + + if ($items && empty($indexKey)) { + $indexKey = is_array($items[0]) ? 'id' : $items[0]->getPk(); + } + + if (isset($indexKey) && is_string($indexKey)) { + return array_column($items, null, $indexKey); + } + + return $items; + } + + /** + * 比较数组,返回差集 + * + * @access public + * @param mixed $items 数据 + * @param string $indexKey 指定比较的键名 + * @return static + */ + public function diff($items, $indexKey = null) + { + if ($this->isEmpty() || is_scalar($this->items[0])) { + return new static(array_diff($this->items, $this->convertToArray($items))); + } + + $diff = []; + $dictionary = $this->dictionary($items, $indexKey); + + if (is_string($indexKey)) { + foreach ($this->items as $item) { + if (!isset($dictionary[$item[$indexKey]])) { + $diff[] = $item; + } + } + } + + return new static($diff); + } + + /** + * 比较数组,返回交集 + * + * @access public + * @param mixed $items 数据 + * @param string $indexKey 指定比较的键名 + * @return static + */ + public function intersect($items, $indexKey = null) + { + if ($this->isEmpty() || is_scalar($this->items[0])) { + return new static(array_diff($this->items, $this->convertToArray($items))); + } + + $intersect = []; + $dictionary = $this->dictionary($items, $indexKey); + + if (is_string($indexKey)) { + foreach ($this->items as $item) { + if (isset($dictionary[$item[$indexKey]])) { + $intersect[] = $item; + } + } + } + + return new static($intersect); + } + + /** + * 返回数组中所有的键名 + * + * @access public + * @return array + */ + public function keys() + { + $current = current($this->items); + + if (is_scalar($current)) { + $array = $this->items; + } elseif (is_array($current)) { + $array = $current; + } else { + $array = $current->toArray(); + } + + return array_keys($array); + } + + /** + * 删除数组的最后一个元素(出栈) + * + * @access public + * @return mixed + */ + public function pop() + { + return array_pop($this->items); + } + + /** + * 通过使用用户自定义函数,以字符串返回数组 + * + * @access public + * @param callable $callback + * @param mixed $initial + * @return mixed + */ + public function reduce(callable $callback, $initial = null) + { + return array_reduce($this->items, $callback, $initial); + } + + /** + * 以相反的顺序返回数组。 + * + * @access public + * @return static + */ + public function reverse() + { + return new static(array_reverse($this->items)); + } + + /** + * 删除数组中首个元素,并返回被删除元素的值 + * + * @access public + * @return mixed + */ + public function shift() + { + return array_shift($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 int $size + * @param bool $preserveKeys + * @return static + */ + public function chunk($size, $preserveKeys = false) + { + $chunks = []; + + foreach (array_chunk($this->items, $size, $preserveKeys) as $chunk) { + $chunks[] = new static($chunk); + } + + return new static($chunks); + } + + /** + * 在数组开头插入一个元素 + * @access public + * @param mixed $value + * @param mixed $key + * @return void + */ + public function unshift($value, $key = null) + { + if (is_null($key)) { + array_unshift($this->items, $value); + } else { + $this->items = [$key => $value] + $this->items; + } + } + + /** + * 给每个元素执行个回调 + * + * @access public + * @param callable $callback + * @return $this + */ + public function each(callable $callback) + { + foreach ($this->items as $key => $item) { + $result = $callback($item, $key); + + if (false === $result) { + break; + } elseif (!is_object($item)) { + $this->items[$key] = $result; + } + } + + return $this; + } + + /** + * 用回调函数处理数组中的元素 + * @access public + * @param callable|null $callback + * @return static + */ + public function map(callable $callback) + { + return new static(array_map($callback, $this->items)); + } + + /** + * 用回调函数过滤数组中的元素 + * @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)); + } + + /** + * 根据字段条件过滤数组中的元素 + * @access public + * @param string $field 字段名 + * @param mixed $operator 操作符 + * @param mixed $value 数据 + * @return static + */ + public function where($field, $operator, $value = null) + { + if (is_null($value)) { + $value = $operator; + $operator = '='; + } + + return $this->filter(function ($data) use ($field, $operator, $value) { + if (strpos($field, '.')) { + list($field, $relation) = explode('.', $field); + + $result = isset($data[$field][$relation]) ? $data[$field][$relation] : null; + } else { + $result = isset($data[$field]) ? $data[$field] : null; + } + + switch (strtolower($operator)) { + case '===': + return $result === $value; + case '!==': + return $result !== $value; + case '!=': + case '<>': + return $result != $value; + case '>': + return $result > $value; + case '>=': + return $result >= $value; + case '<': + return $result < $value; + case '<=': + return $result <= $value; + case 'like': + return is_string($result) && false !== strpos($result, $value); + case 'not like': + return is_string($result) && false === strpos($result, $value); + case 'in': + return is_scalar($result) && in_array($result, $value, true); + case 'not in': + return is_scalar($result) && !in_array($result, $value, true); + case 'between': + list($min, $max) = is_string($value) ? explode(',', $value) : $value; + return is_scalar($result) && $result >= $min && $result <= $max; + case 'not between': + list($min, $max) = is_string($value) ? explode(',', $value) : $value; + return is_scalar($result) && $result > $max || $result < $min; + case '==': + case '=': + default: + return $result == $value; + } + }); + } + + /** + * 返回数据中指定的一列 + * @access public + * @param mixed $columnKey 键名 + * @param mixed $indexKey 作为索引值的列 + * @return array + */ + public function column($columnKey, $indexKey = null) + { + return array_column($this->toArray(), $columnKey, $indexKey); + } + + /** + * 对数组排序 + * + * @access public + * @param callable|null $callback + * @return static + */ + public function sort(callable $callback = null) + { + $items = $this->items; + + $callback = $callback ?: function ($a, $b) { + return $a == $b ? 0 : (($a < $b) ? -1 : 1); + + }; + + uasort($items, $callback); + + return new static($items); + } + + /** + * 指定字段排序 + * @access public + * @param string $field 排序字段 + * @param string $order 排序 + * @param bool $intSort 是否为数字排序 + * @return $this + */ + public function order($field, $order = null, $intSort = true) + { + return $this->sort(function ($a, $b) use ($field, $order, $intSort) { + $fieldA = isset($a[$field]) ? $a[$field] : null; + $fieldB = isset($b[$field]) ? $b[$field] : null; + + if ($intSort) { + return 'desc' == strtolower($order) ? $fieldB >= $fieldA : $fieldA >= $fieldB; + } else { + return 'desc' == strtolower($order) ? strcmp($fieldB, $fieldA) : strcmp($fieldA, $fieldB); + } + }); + } + + /** + * 将数组打乱 + * + * @access public + * @return static + */ + public function shuffle() + { + $items = $this->items; + + shuffle($items); + + return new static($items); + } + + /** + * 截取数组 + * + * @access public + * @param int $offset + * @param int $length + * @param bool $preserveKeys + * @return static + */ + public function slice($offset, $length = null, $preserveKeys = false) + { + return new static(array_slice($this->items, $offset, $length, $preserveKeys)); + } + + // ArrayAccess + public function offsetExists($offset) + { + return array_key_exists($offset, $this->items); + } + + public function offsetGet($offset) + { + return $this->items[$offset]; + } + + public function offsetSet($offset, $value) + { + if (is_null($offset)) { + $this->items[] = $value; + } else { + $this->items[$offset] = $value; + } + } + + public function offsetUnset($offset) + { + unset($this->items[$offset]); + } + + //Countable + public function count() + { + return count($this->items); + } + + //IteratorAggregate + public function getIterator() + { + return new ArrayIterator($this->items); + } + + //JsonSerializable + public function jsonSerialize() + { + return $this->toArray(); + } + + /** + * 转换当前数据集为JSON字符串 + * @access public + * @param integer $options json参数 + * @return string + */ + public function toJson($options = JSON_UNESCAPED_UNICODE) + { + return json_encode($this->toArray(), $options); + } + + public function __toString() + { + return $this->toJson(); + } + + /** + * 转换成数组 + * + * @access public + * @param mixed $items + * @return array + */ + protected function convertToArray($items) + { + if ($items instanceof self) { + return $items->all(); + } + + return (array) $items; + } +} diff --git a/thinkphp/library/think/Config.php b/thinkphp/library/think/Config.php new file mode 100644 index 0000000..bec6222 --- /dev/null +++ b/thinkphp/library/think/Config.php @@ -0,0 +1,398 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use Yaconf; + +class Config implements \ArrayAccess +{ + /** + * 配置参数 + * @var array + */ + protected $config = []; + + /** + * 配置前缀 + * @var string + */ + protected $prefix = 'app'; + + /** + * 配置文件目录 + * @var string + */ + protected $path; + + /** + * 配置文件后缀 + * @var string + */ + protected $ext; + + /** + * 是否支持Yaconf + * @var bool + */ + protected $yaconf; + + /** + * 构造方法 + * @access public + */ + public function __construct($path = '', $ext = '.php') + { + $this->path = $path; + $this->ext = $ext; + $this->yaconf = class_exists('Yaconf'); + } + + public static function __make(App $app) + { + $path = $app->getConfigPath(); + $ext = $app->getConfigExt(); + return new static($path, $ext); + } + + /** + * 设置开启Yaconf + * @access public + * @param bool|string $yaconf 是否使用Yaconf + * @return void + */ + public function setYaconf($yaconf) + { + if ($this->yaconf) { + $this->yaconf = $yaconf; + } + } + + /** + * 设置配置参数默认前缀 + * @access public + * @param string $prefix 前缀 + * @return void + */ + public function setDefaultPrefix($prefix) + { + $this->prefix = $prefix; + } + + /** + * 解析配置文件或内容 + * @access public + * @param string $config 配置文件路径或内容 + * @param string $type 配置解析类型 + * @param string $name 配置名(如设置即表示二级配置) + * @return mixed + */ + public function parse($config, $type = '', $name = '') + { + if (empty($type)) { + $type = pathinfo($config, PATHINFO_EXTENSION); + } + + $object = Loader::factory($type, '\\think\\config\\driver\\', $config); + + return $this->set($object->parse(), $name); + } + + /** + * 加载配置文件(多种格式) + * @access public + * @param string $file 配置文件名 + * @param string $name 一级配置名 + * @return mixed + */ + public function load($file, $name = '') + { + if (is_file($file)) { + $filename = $file; + } elseif (is_file($this->path . $file . $this->ext)) { + $filename = $this->path . $file . $this->ext; + } + + if (isset($filename)) { + return $this->loadFile($filename, $name); + } elseif ($this->yaconf && Yaconf::has($file)) { + return $this->set(Yaconf::get($file), $name); + } + + return $this->config; + } + + /** + * 获取实际的yaconf配置参数 + * @access protected + * @param string $name 配置参数名 + * @return string + */ + protected function getYaconfName($name) + { + if ($this->yaconf && is_string($this->yaconf)) { + return $this->yaconf . '.' . $name; + } + + return $name; + } + + /** + * 获取yaconf配置 + * @access public + * @param string $name 配置参数名 + * @param mixed $default 默认值 + * @return mixed + */ + public function yaconf($name, $default = null) + { + if ($this->yaconf) { + $yaconfName = $this->getYaconfName($name); + + if (Yaconf::has($yaconfName)) { + return Yaconf::get($yaconfName); + } + } + + return $default; + } + + protected function loadFile($file, $name) + { + $name = strtolower($name); + $type = pathinfo($file, PATHINFO_EXTENSION); + + if ('php' == $type) { + return $this->set(include $file, $name); + } elseif ('yaml' == $type && function_exists('yaml_parse_file')) { + return $this->set(yaml_parse_file($file), $name); + } + + return $this->parse($file, $type, $name); + } + + /** + * 检测配置是否存在 + * @access public + * @param string $name 配置参数名(支持多级配置 .号分割) + * @return bool + */ + public function has($name) + { + if (false === strpos($name, '.')) { + $name = $this->prefix . '.' . $name; + } + + return !is_null($this->get($name)); + } + + /** + * 获取一级配置 + * @access public + * @param string $name 一级配置名 + * @return array + */ + public function pull($name) + { + $name = strtolower($name); + + if ($this->yaconf) { + $yaconfName = $this->getYaconfName($name); + + if (Yaconf::has($yaconfName)) { + $config = Yaconf::get($yaconfName); + return isset($this->config[$name]) ? array_merge($this->config[$name], $config) : $config; + } + } + + return isset($this->config[$name]) ? $this->config[$name] : []; + } + + /** + * 获取配置参数 为空则获取所有配置 + * @access public + * @param string $name 配置参数名(支持多级配置 .号分割) + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name = null, $default = null) + { + if ($name && false === strpos($name, '.')) { + $name = $this->prefix . '.' . $name; + } + + // 无参数时获取所有 + if (empty($name)) { + return $this->config; + } + + if ('.' == substr($name, -1)) { + return $this->pull(substr($name, 0, -1)); + } + + if ($this->yaconf) { + $yaconfName = $this->getYaconfName($name); + + if (Yaconf::has($yaconfName)) { + return Yaconf::get($yaconfName); + } + } + + $name = explode('.', $name); + $name[0] = strtolower($name[0]); + $config = $this->config; + + // 按.拆分成多维数组进行判断 + foreach ($name as $val) { + if (isset($config[$val])) { + $config = $config[$val]; + } else { + return $default; + } + } + + return $config; + } + + /** + * 设置配置参数 name为数组则为批量设置 + * @access public + * @param string|array $name 配置参数名(支持三级配置 .号分割) + * @param mixed $value 配置值 + * @return mixed + */ + public function set($name, $value = null) + { + if (is_string($name)) { + if (false === strpos($name, '.')) { + $name = $this->prefix . '.' . $name; + } + + $name = explode('.', $name, 3); + + if (count($name) == 2) { + $this->config[strtolower($name[0])][$name[1]] = $value; + } else { + $this->config[strtolower($name[0])][$name[1]][$name[2]] = $value; + } + + return $value; + } elseif (is_array($name)) { + // 批量设置 + if (!empty($value)) { + if (isset($this->config[$value])) { + $result = array_merge($this->config[$value], $name); + } else { + $result = $name; + } + + $this->config[$value] = $result; + } else { + $result = $this->config = array_merge($this->config, $name); + } + } else { + // 为空直接返回 已有配置 + $result = $this->config; + } + + return $result; + } + + /** + * 移除配置 + * @access public + * @param string $name 配置参数名(支持三级配置 .号分割) + * @return void + */ + public function remove($name) + { + if (false === strpos($name, '.')) { + $name = $this->prefix . '.' . $name; + } + + $name = explode('.', $name, 3); + + if (count($name) == 2) { + unset($this->config[strtolower($name[0])][$name[1]]); + } else { + unset($this->config[strtolower($name[0])][$name[1]][$name[2]]); + } + } + + /** + * 重置配置参数 + * @access public + * @param string $prefix 配置前缀名 + * @return void + */ + public function reset($prefix = '') + { + if ('' === $prefix) { + $this->config = []; + } else { + $this->config[$prefix] = []; + } + } + + /** + * 设置配置 + * @access public + * @param string $name 参数名 + * @param mixed $value 值 + */ + public function __set($name, $value) + { + return $this->set($name, $value); + } + + /** + * 获取配置参数 + * @access public + * @param string $name 参数名 + * @return mixed + */ + public function __get($name) + { + return $this->get($name); + } + + /** + * 检测是否存在参数 + * @access public + * @param string $name 参数名 + * @return bool + */ + public function __isset($name) + { + return $this->has($name); + } + + // ArrayAccess + public function offsetSet($name, $value) + { + $this->set($name, $value); + } + + public function offsetExists($name) + { + return $this->has($name); + } + + public function offsetUnset($name) + { + $this->remove($name); + } + + public function offsetGet($name) + { + return $this->get($name); + } +} diff --git a/thinkphp/library/think/Console.php b/thinkphp/library/think/Console.php new file mode 100644 index 0000000..22f3e2c --- /dev/null +++ b/thinkphp/library/think/Console.php @@ -0,0 +1,829 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\console\Command; +use think\console\command\Help as HelpCommand; +use think\console\Input; +use think\console\input\Argument as InputArgument; +use think\console\input\Definition as InputDefinition; +use think\console\input\Option as InputOption; +use think\console\Output; +use think\console\output\driver\Buffer; + +class Console +{ + + private $name; + private $version; + + /** @var Command[] */ + private $commands = []; + + private $wantHelps = false; + + private $catchExceptions = true; + private $autoExit = true; + private $definition; + private $defaultCommand; + + private static $defaultCommands = [ + 'help' => "think\\console\\command\\Help", + 'list' => "think\\console\\command\\Lists", + 'build' => "think\\console\\command\\Build", + 'clear' => "think\\console\\command\\Clear", + 'make:command' => "think\\console\\command\\make\\Command", + 'make:controller' => "think\\console\\command\\make\\Controller", + 'make:model' => "think\\console\\command\\make\\Model", + 'make:middleware' => "think\\console\\command\\make\\Middleware", + 'make:validate' => "think\\console\\command\\make\\Validate", + 'optimize:autoload' => "think\\console\\command\\optimize\\Autoload", + 'optimize:config' => "think\\console\\command\\optimize\\Config", + 'optimize:schema' => "think\\console\\command\\optimize\\Schema", + 'optimize:route' => "think\\console\\command\\optimize\\Route", + 'run' => "think\\console\\command\\RunServer", + 'version' => "think\\console\\command\\Version", + 'route:list' => "think\\console\\command\\RouteList", + ]; + + /** + * 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(); + } + + /** + * 设置执行用户 + * @param $user + */ + public function setUser($user) + { + if (DIRECTORY_SEPARATOR == '\\') { + return; + } + + $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) { + $config = Container::get('config')->pull('console'); + $console = new self($config['name'], $config['version'], $config['user']); + + $commands = $console->getDefinedCommands($config); + + // 添加指令集 + $console->addCommands($commands); + } + + if ($run) { + // 运行 + return $console->run(); + } else { + return $console; + } + } + + /** + * @access public + * @param array $config + * @return array + */ + public function getDefinedCommands(array $config = []) + { + $commands = self::$defaultCommands; + + if (!empty($config['auto_path']) && is_dir($config['auto_path'])) { + // 自动加载指令类 + $files = scandir($config['auto_path']); + + if (count($files) > 2) { + $beforeClass = get_declared_classes(); + + foreach ($files as $file) { + if (pathinfo($file, PATHINFO_EXTENSION) == 'php') { + include $config['auto_path'] . $file; + } + } + + $afterClass = get_declared_classes(); + $commands = array_merge($commands, array_diff($afterClass, $beforeClass)); + } + } + + $file = Container::get('env')->get('app_path') . 'command.php'; + + if (is_file($file)) { + $appCommands = include $file; + + if (is_array($appCommands)) { + $commands = array_merge($commands, $appCommands); + } + } + + return $commands; + } + + /** + * @access public + * @param string $command + * @param array $parameters + * @param string $driver + * @return Output|Buffer + */ + public static function call($command, array $parameters = [], $driver = 'buffer') + { + $console = self::init(false); + + array_unshift($parameters, $command); + + $input = new Input($parameters); + $output = new Output($driver); + + $console->setCatchExceptions(false); + $console->find($command)->run($input, $output); + + return $output; + } + + /** + * 执行当前的指令 + * @access public + * @return int + * @throws \Exception + * @api + */ + public function run() + { + $input = new Input(); + $output = new Output(); + + $this->configureIO($input, $output); + + try { + $exitCode = $this->doRun($input, $output); + } catch (\Exception $e) { + if (!$this->catchExceptions) { + throw $e; + } + + $output->renderException($e); + + $exitCode = $e->getCode(); + if (is_numeric($exitCode)) { + $exitCode = (int) $exitCode; + if (0 === $exitCode) { + $exitCode = 1; + } + } else { + $exitCode = 1; + } + } + + if ($this->autoExit) { + if ($exitCode > 255) { + $exitCode = 255; + } + + exit($exitCode); + } + + return $exitCode; + } + + /** + * 执行指令 + * @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()); + + return 0; + } + + $name = $this->getCommandName($input); + + if (true === $input->hasParameterOption(['--help', '-h'])) { + if (!$name) { + $name = 'help'; + $input = new Input(['help']); + } else { + $this->wantHelps = true; + } + } + + if (!$name) { + $name = $this->defaultCommand; + $input = new Input([$this->defaultCommand]); + } + + $command = $this->find($name); + + $exitCode = $this->doRunCommand($command, $input, $output); + + return $exitCode; + } + + /** + * 设置输入参数定义 + * @access public + * @param InputDefinition $definition + */ + public function setDefinition(InputDefinition $definition) + { + $this->definition = $definition; + } + + /** + * 获取输入参数定义 + * @access public + * @return InputDefinition The InputDefinition instance + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * Gets the help message. + * @access public + * @return string A help message. + */ + public function getHelp() + { + return $this->getLongVersion(); + } + + /** + * 是否捕获异常 + * @access public + * @param bool $boolean + * @api + */ + public function setCatchExceptions($boolean) + { + $this->catchExceptions = (bool) $boolean; + } + + /** + * 是否自动退出 + * @access public + * @param bool $boolean + * @api + */ + public function setAutoExit($boolean) + { + $this->autoExit = (bool) $boolean; + } + + /** + * 获取名称 + * @access public + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 设置名称 + * @access public + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * 获取版本 + * @access public + * @return string + * @api + */ + public function getVersion() + { + return $this->version; + } + + /** + * 设置版本 + * @access public + * @param string $version + */ + public function setVersion($version) + { + $this->version = $version; + } + + /** + * 获取完整的版本号 + * @access public + * @return string + */ + public function getLongVersion() + { + if ('UNKNOWN' !== $this->getName() && 'UNKNOWN' !== $this->getVersion()) { + return sprintf('%s version %s', $this->getName(), $this->getVersion()); + } + + return 'Console Tool'; + } + + /** + * 注册一个指令 (便于动态创建指令) + * @access public + * @param string $name 指令名 + * @return Command + */ + public function register($name) + { + return $this->add(new Command($name)); + } + + /** + * 添加指令集 + * @access public + * @param array $commands + */ + public function addCommands(array $commands) + { + foreach ($commands as $key => $command) { + if (is_subclass_of($command, "\\think\\console\\Command")) { + // 注册指令 + $this->add($command, is_numeric($key) ? '' : $key); + } + } + } + + /** + * 注册一个指令(对象) + * @access public + * @param mixed $command 指令对象或者指令类名 + * @param string $name 指令名 留空则自动获取 + * @return mixed + */ + public function add($command, $name) + { + if ($name) { + $this->commands[$name] = $command; + return; + } + + if (is_string($command)) { + $command = new $command(); + } + + $command->setConsole($this); + + if (!$command->isEnabled()) { + $command->setConsole(null); + return; + } + + 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))); + } + + $this->commands[$command->getName()] = $command; + + foreach ($command->getAliases() as $alias) { + $this->commands[$alias] = $command; + } + + return $command; + } + + /** + * 获取指令 + * @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)); + } + + $command = $this->commands[$name]; + + if (is_string($command)) { + $command = new $command(); + } + + $command->setConsole($this); + + if ($this->wantHelps) { + $this->wantHelps = false; + + /** @var HelpCommand $helpCommand */ + $helpCommand = $this->get('help'); + $helpCommand->setCommand($command); + + return $helpCommand; + } + + return $command; + } + + /** + * 某个指令是否存在 + * @access public + * @param string $name 指令名称 + * @return bool + */ + public function has($name) + { + return isset($this->commands[$name]); + } + + /** + * 获取所有的命名空间 + * @access public + * @return array + */ + public function getNamespaces() + { + $namespaces = []; + foreach ($this->commands as $name => $command) { + if (is_string($command)) { + $namespaces = array_merge($namespaces, $this->extractAllNamespaces($name)); + } else { + $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName())); + + foreach ($command->getAliases() as $alias) { + $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias)); + } + } + + } + + return array_values(array_unique(array_filter($namespaces))); + } + + /** + * 查找注册命名空间中的名称或缩写。 + * @access public + * @param string $namespace + * @return string + * @throws \InvalidArgumentException + */ + public function findNamespace($namespace) + { + $allNamespaces = $this->getNamespaces(); + $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { + return preg_quote($matches[1]) . '[^:]*'; + }, $namespace); + $namespaces = preg_grep('{^' . $expr . '}', $allNamespaces); + + if (empty($namespaces)) { + $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); + + if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) { + if (1 == count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + + $message .= implode("\n ", $alternatives); + } + + throw new \InvalidArgumentException($message); + } + + $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)))); + } + + return $exact ? $namespace : reset($namespaces); + } + + /** + * 查找指令 + * @access public + * @param string $name 名称或者别名 + * @return Command + * @throws \InvalidArgumentException + */ + public function find($name) + { + $allCommands = array_keys($this->commands); + + $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { + return preg_quote($matches[1]) . '[^:]*'; + }, $name); + + $commands = preg_grep('{^' . $expr . '}', $allCommands); + + if (empty($commands) || count(preg_grep('{^' . $expr . '$}', $commands)) < 1) { + if (false !== $pos = strrpos($name, ':')) { + $this->findNamespace(substr($name, 0, $pos)); + } + + $message = sprintf('Command "%s" is not defined.', $name); + + if ($alternatives = $this->findAlternatives($name, $allCommands)) { + if (1 == count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + $message .= implode("\n ", $alternatives); + } + + throw new \InvalidArgumentException($message); + } + + $exact = in_array($name, $commands, true); + if (count($commands) > 1 && !$exact) { + $suggestions = $this->getAbbreviationSuggestions(array_values($commands)); + + throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions)); + } + + return $this->get($exact ? $name : reset($commands)); + } + + /** + * 获取所有的指令 + * @access public + * @param string $namespace 命名空间 + * @return Command[] + * @api + */ + public function all($namespace = null) + { + 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; + } + } + + return $commands; + } + + /** + * 获取可能的指令名 + * @access public + * @param array $names + * @return array + */ + public static function getAbbreviations($names) + { + $abbrevs = []; + foreach ($names as $name) { + for ($len = strlen($name); $len > 0; --$len) { + $abbrev = substr($name, 0, $len); + $abbrevs[$abbrev][] = $name; + } + } + + return $abbrevs; + } + + /** + * 配置基于用户的参数和选项的输入和输出实例。 + * @access protected + * @param Input $input 输入实例 + * @param Output $output 输出实例 + */ + protected function configureIO(Input $input, Output $output) + { + if (true === $input->hasParameterOption(['--ansi'])) { + $output->setDecorated(true); + } elseif (true === $input->hasParameterOption(['--no-ansi'])) { + $output->setDecorated(false); + } + + if (true === $input->hasParameterOption(['--no-interaction', '-n'])) { + $input->setInteractive(false); + } + + if (true === $input->hasParameterOption(['--quiet', '-q'])) { + $output->setVerbosity(Output::VERBOSITY_QUIET); + } else { + if ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3') || $input->getParameterOption('--verbose') === 3) { + $output->setVerbosity(Output::VERBOSITY_DEBUG); + } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2') || $input->getParameterOption('--verbose') === 2) { + $output->setVerbosity(Output::VERBOSITY_VERY_VERBOSE); + } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1') || $input->hasParameterOption('--verbose') || $input->getParameterOption('--verbose')) { + $output->setVerbosity(Output::VERBOSITY_VERBOSE); + } + } + } + + /** + * 执行指令 + * @access protected + * @param Command $command 指令实例 + * @param Input $input 输入实例 + * @param Output $output 输出实例 + * @return int + * @throws \Exception + */ + protected function doRunCommand(Command $command, Input $input, Output $output) + { + return $command->run($input, $output); + } + + /** + * 获取指令的基础名称 + * @access protected + * @param Input $input + * @return string + */ + protected function getCommandName(Input $input) + { + return $input->getFirstArgument(); + } + + /** + * 获取默认输入定义 + * @access protected + * @return InputDefinition + */ + protected function getDefaultInputDefinition() + { + return new InputDefinition([ + new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), + new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'), + new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this console version'), + new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'), + new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'), + new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'), + new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'), + new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'), + ]); + } + + public static function addDefaultCommands(array $classnames) + { + self::$defaultCommands = array_merge(self::$defaultCommands, $classnames); + } + + /** + * 获取可能的建议 + * @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) : ''); + } + + /** + * 返回命名空间部分 + * @access public + * @param string $name 指令 + * @param string $limit 部分的命名空间的最大数量 + * @return string + */ + public function extractNamespace($name, $limit = null) + { + $parts = explode(':', $name); + array_pop($parts); + + return implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit)); + } + + /** + * 查找可替代的建议 + * @access private + * @param string $name + * @param array|\Traversable $collection + * @return array + */ + private function findAlternatives($name, $collection) + { + $threshold = 1e3; + $alternatives = []; + + $collectionParts = []; + foreach ($collection as $item) { + $collectionParts[$item] = explode(':', $item); + } + + foreach (explode(':', $name) as $i => $subname) { + foreach ($collectionParts as $collectionName => $parts) { + $exists = isset($alternatives[$collectionName]); + if (!isset($parts[$i]) && $exists) { + $alternatives[$collectionName] += $threshold; + continue; + } elseif (!isset($parts[$i])) { + continue; + } + + $lev = levenshtein($subname, $parts[$i]); + if ($lev <= strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) { + $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev; + } elseif ($exists) { + $alternatives[$collectionName] += $threshold; + } + } + } + + 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 = array_filter($alternatives, function ($lev) use ($threshold) { + return $lev < 2 * $threshold; + }); + asort($alternatives); + + return array_keys($alternatives); + } + + /** + * 设置默认的指令 + * @access public + * @param string $commandName The Command name + */ + public function setDefaultCommand($commandName) + { + $this->defaultCommand = $commandName; + } + + /** + * 返回所有的命名空间 + * @access private + * @param string $name + * @return array + */ + private function extractAllNamespaces($name) + { + $parts = explode(':', $name, -1); + $namespaces = []; + + foreach ($parts as $part) { + if (count($namespaces)) { + $namespaces[] = end($namespaces) . ':' . $part; + } else { + $namespaces[] = $part; + } + } + + return $namespaces; + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['commands'], $data['definition']); + + return $data; + } +} diff --git a/thinkphp/library/think/Container.php b/thinkphp/library/think/Container.php new file mode 100644 index 0000000..91b32aa --- /dev/null +++ b/thinkphp/library/think/Container.php @@ -0,0 +1,618 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use ArrayAccess; +use ArrayIterator; +use Closure; +use Countable; +use InvalidArgumentException; +use IteratorAggregate; +use ReflectionClass; +use ReflectionException; +use ReflectionFunction; +use ReflectionMethod; +use think\exception\ClassNotFoundException; + +/** + * @package think + * @property Build $build + * @property Cache $cache + * @property Config $config + * @property Cookie $cookie + * @property Debug $debug + * @property Env $env + * @property Hook $hook + * @property Lang $lang + * @property Middleware $middleware + * @property Request $request + * @property Response $response + * @property Route $route + * @property Session $session + * @property Template $template + * @property Url $url + * @property Validate $validate + * @property View $view + * @property route\RuleName $rule_name + * @property Log $log + */ +class Container implements ArrayAccess, IteratorAggregate, Countable +{ + /** + * 容器对象实例 + * @var Container + */ + protected static $instance; + + /** + * 容器中的对象实例 + * @var array + */ + protected $instances = []; + + /** + * 容器绑定标识 + * @var array + */ + protected $bind = [ + 'app' => App::class, + 'build' => Build::class, + 'cache' => Cache::class, + 'config' => Config::class, + 'cookie' => Cookie::class, + 'debug' => Debug::class, + 'env' => Env::class, + 'hook' => Hook::class, + 'lang' => Lang::class, + 'log' => Log::class, + 'middleware' => Middleware::class, + 'request' => Request::class, + 'response' => Response::class, + 'route' => Route::class, + 'session' => Session::class, + 'template' => Template::class, + 'url' => Url::class, + 'validate' => Validate::class, + 'view' => View::class, + 'rule_name' => route\RuleName::class, + // 接口依赖注入 + 'think\LoggerInterface' => Log::class, + ]; + + /** + * 容器标识别名 + * @var array + */ + protected $name = []; + + /** + * 获取当前容器的实例(单例) + * @access public + * @return static + */ + public static function getInstance() + { + if (is_null(static::$instance)) { + static::$instance = new static; + } + + return static::$instance; + } + + /** + * 设置当前容器的实例 + * @access public + * @param object $instance + * @return void + */ + public static function setInstance($instance) + { + static::$instance = $instance; + } + + /** + * 获取容器中的对象实例 + * @access public + * @param string $abstract 类名或者标识 + * @param array|true $vars 变量 + * @param bool $newInstance 是否每次创建新的实例 + * @return object + */ + public static function get($abstract, $vars = [], $newInstance = false) + { + return static::getInstance()->make($abstract, $vars, $newInstance); + } + + /** + * 绑定一个类、闭包、实例、接口实现到容器 + * @access public + * @param string $abstract 类标识、接口 + * @param mixed $concrete 要绑定的类、闭包或者实例 + * @return Container + */ + public static function set($abstract, $concrete = null) + { + return static::getInstance()->bindTo($abstract, $concrete); + } + + /** + * 移除容器中的对象实例 + * @access public + * @param string $abstract 类标识、接口 + * @return void + */ + public static function remove($abstract) + { + return static::getInstance()->delete($abstract); + } + + /** + * 清除容器中的对象实例 + * @access public + * @return void + */ + public static function clear() + { + return static::getInstance()->flush(); + } + + /** + * 绑定一个类、闭包、实例、接口实现到容器 + * @access public + * @param string|array $abstract 类标识、接口 + * @param mixed $concrete 要绑定的类、闭包或者实例 + * @return $this + */ + public function bindTo($abstract, $concrete = null) + { + if (is_array($abstract)) { + $this->bind = array_merge($this->bind, $abstract); + } elseif ($concrete instanceof Closure) { + $this->bind[$abstract] = $concrete; + } elseif (is_object($concrete)) { + if (isset($this->bind[$abstract])) { + $abstract = $this->bind[$abstract]; + } + $this->instances[$abstract] = $concrete; + } else { + $this->bind[$abstract] = $concrete; + } + + return $this; + } + + /** + * 绑定一个类实例当容器 + * @access public + * @param string $abstract 类名或者标识 + * @param object|\Closure $instance 类的实例 + * @return $this + */ + public function instance($abstract, $instance) + { + if ($instance instanceof \Closure) { + $this->bind[$abstract] = $instance; + } else { + if (isset($this->bind[$abstract])) { + $abstract = $this->bind[$abstract]; + } + + $this->instances[$abstract] = $instance; + } + + return $this; + } + + /** + * 判断容器中是否存在类及标识 + * @access public + * @param string $abstract 类名或者标识 + * @return bool + */ + public function bound($abstract) + { + return isset($this->bind[$abstract]) || isset($this->instances[$abstract]); + } + + /** + * 判断容器中是否存在对象实例 + * @access public + * @param string $abstract 类名或者标识 + * @return bool + */ + public function exists($abstract) + { + if (isset($this->bind[$abstract])) { + $abstract = $this->bind[$abstract]; + } + + return isset($this->instances[$abstract]); + } + + /** + * 判断容器中是否存在类及标识 + * @access public + * @param string $name 类名或者标识 + * @return bool + */ + public function has($name) + { + return $this->bound($name); + } + + /** + * 创建类的实例 + * @access public + * @param string $abstract 类名或者标识 + * @param array|true $vars 变量 + * @param bool $newInstance 是否每次创建新的实例 + * @return object + */ + public function make($abstract, $vars = [], $newInstance = false) + { + if (true === $vars) { + // 总是创建新的实例化对象 + $newInstance = true; + $vars = []; + } + + $abstract = isset($this->name[$abstract]) ? $this->name[$abstract] : $abstract; + + if (isset($this->instances[$abstract]) && !$newInstance) { + return $this->instances[$abstract]; + } + + if (isset($this->bind[$abstract])) { + $concrete = $this->bind[$abstract]; + + if ($concrete instanceof Closure) { + $object = $this->invokeFunction($concrete, $vars); + } else { + $this->name[$abstract] = $concrete; + return $this->make($concrete, $vars, $newInstance); + } + } else { + $object = $this->invokeClass($abstract, $vars); + } + + if (!$newInstance) { + $this->instances[$abstract] = $object; + } + + return $object; + } + + /** + * 删除容器中的对象实例 + * @access public + * @param string|array $abstract 类名或者标识 + * @return void + */ + public function delete($abstract) + { + foreach ((array) $abstract as $name) { + $name = isset($this->name[$name]) ? $this->name[$name] : $name; + + if (isset($this->instances[$name])) { + unset($this->instances[$name]); + } + } + } + + /** + * 获取容器中的对象实例 + * @access public + * @return array + */ + public function all() + { + return $this->instances; + } + + /** + * 清除容器中的对象实例 + * @access public + * @return void + */ + public function flush() + { + $this->instances = []; + $this->bind = []; + $this->name = []; + } + + /** + * 执行函数或者闭包方法 支持参数调用 + * @access public + * @param mixed $function 函数或者闭包 + * @param array $vars 参数 + * @return mixed + */ + public function invokeFunction($function, $vars = []) + { + try { + $reflect = new ReflectionFunction($function); + + $args = $this->bindParams($reflect, $vars); + + return call_user_func_array($function, $args); + } catch (ReflectionException $e) { + throw new Exception('function not exists: ' . $function . '()'); + } + } + + /** + * 调用反射执行类的方法 支持参数绑定 + * @access public + * @param mixed $method 方法 + * @param array $vars 参数 + * @return mixed + */ + public function invokeMethod($method, $vars = []) + { + try { + if (is_array($method)) { + $class = is_object($method[0]) ? $method[0] : $this->invokeClass($method[0]); + $reflect = new ReflectionMethod($class, $method[1]); + } else { + // 静态方法 + $reflect = new ReflectionMethod($method); + } + + $args = $this->bindParams($reflect, $vars); + + return $reflect->invokeArgs(isset($class) ? $class : null, $args); + } catch (ReflectionException $e) { + if (is_array($method) && is_object($method[0])) { + $method[0] = get_class($method[0]); + } + + throw new Exception('method not exists: ' . (is_array($method) ? $method[0] . '::' . $method[1] : $method) . '()'); + } + } + + /** + * 调用反射执行类的方法 支持参数绑定 + * @access public + * @param object $instance 对象实例 + * @param mixed $reflect 反射类 + * @param array $vars 参数 + * @return mixed + */ + public function invokeReflectMethod($instance, $reflect, $vars = []) + { + $args = $this->bindParams($reflect, $vars); + + return $reflect->invokeArgs($instance, $args); + } + + /** + * 调用反射执行callable 支持参数绑定 + * @access public + * @param mixed $callable + * @param array $vars 参数 + * @return mixed + */ + public function invoke($callable, $vars = []) + { + if ($callable instanceof Closure) { + return $this->invokeFunction($callable, $vars); + } + + return $this->invokeMethod($callable, $vars); + } + + /** + * 调用反射执行类的实例化 支持依赖注入 + * @access public + * @param string $class 类名 + * @param array $vars 参数 + * @return mixed + */ + public function invokeClass($class, $vars = []) + { + try { + $reflect = new ReflectionClass($class); + + if ($reflect->hasMethod('__make')) { + $method = new ReflectionMethod($class, '__make'); + + if ($method->isPublic() && $method->isStatic()) { + $args = $this->bindParams($method, $vars); + return $method->invokeArgs(null, $args); + } + } + + $constructor = $reflect->getConstructor(); + + $args = $constructor ? $this->bindParams($constructor, $vars) : []; + + return $reflect->newInstanceArgs($args); + + } catch (ReflectionException $e) { + throw new ClassNotFoundException('class not exists: ' . $class, $class); + } + } + + /** + * 绑定参数 + * @access protected + * @param \ReflectionMethod|\ReflectionFunction $reflect 反射类 + * @param array $vars 参数 + * @return array + */ + protected function bindParams($reflect, $vars = []) + { + if ($reflect->getNumberOfParameters() == 0) { + return []; + } + + // 判断数组类型 数字数组时按顺序绑定参数 + reset($vars); + $type = key($vars) === 0 ? 1 : 0; + $params = $reflect->getParameters(); + + if (PHP_VERSION > 8.0) { + $args = $this->parseParamsForPHP8($params, $vars, $type); + } else { + $args = $this->parseParams($params, $vars, $type); + } + + return $args; + } + + /** + * 解析参数 + * @access protected + * @param array $params 参数列表 + * @param array $vars 参数数据 + * @param int $type 参数类别 + * @return array + */ + protected function parseParams($params, $vars, $type) + { + foreach ($params as $param) { + $name = $param->getName(); + $lowerName = Loader::parseName($name); + $class = $param->getClass(); + + if ($class) { + $args[] = $this->getObjectParam($class->getName(), $vars); + } elseif (1 == $type && !empty($vars)) { + $args[] = array_shift($vars); + } elseif (0 == $type && isset($vars[$name])) { + $args[] = $vars[$name]; + } elseif (0 == $type && isset($vars[$lowerName])) { + $args[] = $vars[$lowerName]; + } elseif ($param->isDefaultValueAvailable()) { + $args[] = $param->getDefaultValue(); + } else { + throw new InvalidArgumentException('method param miss:' . $name); + } + } + return $args; + } + + /** + * 解析参数 + * @access protected + * @param array $params 参数列表 + * @param array $vars 参数数据 + * @param int $type 参数类别 + * @return array + */ + protected function parseParamsForPHP8($params, $vars, $type) + { + foreach ($params as $param) { + $name = $param->getName(); + $lowerName = Loader::parseName($name); + $reflectionType = $param->getType(); + + if ($reflectionType && $reflectionType->isBuiltin() === false) { + $args[] = $this->getObjectParam($reflectionType->getName(), $vars); + } elseif (1 == $type && !empty($vars)) { + $args[] = array_shift($vars); + } elseif (0 == $type && array_key_exists($name, $vars)) { + $args[] = $vars[$name]; + } elseif (0 == $type && array_key_exists($lowerName, $vars)) { + $args[] = $vars[$lowerName]; + } elseif ($param->isDefaultValueAvailable()) { + $args[] = $param->getDefaultValue(); + } else { + throw new InvalidArgumentException('method param miss:' . $name); + } + } + return $args; + } + + /** + * 获取对象类型的参数值 + * @access protected + * @param string $className 类名 + * @param array $vars 参数 + * @return mixed + */ + protected function getObjectParam($className, &$vars) + { + $array = $vars; + $value = array_shift($array); + + if ($value instanceof $className) { + $result = $value; + array_shift($vars); + } else { + $result = $this->make($className); + } + + return $result; + } + + public function __set($name, $value) + { + $this->bindTo($name, $value); + } + + public function __get($name) + { + return $this->make($name); + } + + public function __isset($name) + { + return $this->bound($name); + } + + public function __unset($name) + { + $this->delete($name); + } + + public function offsetExists($key) + { + return $this->__isset($key); + } + + public function offsetGet($key) + { + return $this->__get($key); + } + + public function offsetSet($key, $value) + { + $this->__set($key, $value); + } + + public function offsetUnset($key) + { + $this->__unset($key); + } + + //Countable + public function count() + { + return count($this->instances); + } + + //IteratorAggregate + public function getIterator() + { + return new ArrayIterator($this->instances); + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['instances'], $data['instance']); + + return $data; + } +} diff --git a/thinkphp/library/think/Controller.php b/thinkphp/library/think/Controller.php new file mode 100644 index 0000000..966eaaa --- /dev/null +++ b/thinkphp/library/think/Controller.php @@ -0,0 +1,287 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ValidateException; +use traits\controller\Jump; + +class Controller +{ + use Jump; + + /** + * 视图类实例 + * @var \think\View + */ + protected $view; + + /** + * Request实例 + * @var \think\Request + */ + protected $request; + + /** + * 验证失败是否抛出异常 + * @var bool + */ + protected $failException = false; + + /** + * 是否批量验证 + * @var bool + */ + protected $batchValidate = false; + + /** + * 前置操作方法列表(即将废弃) + * @var array $beforeActionList + */ + protected $beforeActionList = []; + + /** + * 控制器中间件 + * @var array + */ + protected $middleware = []; + + /** + * 构造方法 + * @access public + */ + public function __construct(App $app = null) + { + $this->app = $app ?: Container::get('app'); + $this->request = $this->app['request']; + $this->view = $this->app['view']; + + // 控制器初始化 + $this->initialize(); + + $this->registerMiddleware(); + + // 前置操作方法 即将废弃 + foreach ((array) $this->beforeActionList as $method => $options) { + is_numeric($method) ? + $this->beforeAction($options) : + $this->beforeAction($method, $options); + } + } + + // 初始化 + protected function initialize() + {} + + // 注册控制器中间件 + public function registerMiddleware() + { + foreach ($this->middleware as $key => $val) { + if (!is_int($key)) { + $only = $except = null; + + if (isset($val['only'])) { + $only = array_map(function ($item) { + return strtolower($item); + }, $val['only']); + } elseif (isset($val['except'])) { + $except = array_map(function ($item) { + return strtolower($item); + }, $val['except']); + } + + if (isset($only) && !in_array($this->request->action(), $only)) { + continue; + } elseif (isset($except) && in_array($this->request->action(), $except)) { + continue; + } else { + $val = $key; + } + } + + $this->app['middleware']->controller($val); + } + } + + /** + * 前置操作 + * @access protected + * @param string $method 前置操作方法名 + * @param array $options 调用参数 ['only'=>[...]] 或者['except'=>[...]] + */ + protected function beforeAction($method, $options = []) + { + if (isset($options['only'])) { + if (is_string($options['only'])) { + $options['only'] = explode(',', $options['only']); + } + + $only = array_map(function ($val) { + return strtolower($val); + }, $options['only']); + + if (!in_array($this->request->action(), $only)) { + return; + } + } elseif (isset($options['except'])) { + if (is_string($options['except'])) { + $options['except'] = explode(',', $options['except']); + } + + $except = array_map(function ($val) { + return strtolower($val); + }, $options['except']); + + if (in_array($this->request->action(), $except)) { + return; + } + } + + call_user_func([$this, $method]); + } + + /** + * 加载模板输出 + * @access protected + * @param string $template 模板文件名 + * @param array $vars 模板输出变量 + * @param array $config 模板参数 + * @return mixed + */ + protected function fetch($template = '', $vars = [], $config = []) + { + return Response::create($template, 'view')->assign($vars)->config($config); + } + + /** + * 渲染内容输出 + * @access protected + * @param string $content 模板内容 + * @param array $vars 模板输出变量 + * @param array $config 模板参数 + * @return mixed + */ + protected function display($content = '', $vars = [], $config = []) + { + return Response::create($content, 'view')->assign($vars)->config($config)->isContent(true); + } + + /** + * 模板变量赋值 + * @access protected + * @param mixed $name 要显示的模板变量 + * @param mixed $value 变量的值 + * @return $this + */ + protected function assign($name, $value = '') + { + $this->view->assign($name, $value); + + return $this; + } + + /** + * 视图过滤 + * @access protected + * @param Callable $filter 过滤方法或闭包 + * @return $this + */ + protected function filter($filter) + { + $this->view->filter($filter); + + return $this; + } + + /** + * 初始化模板引擎 + * @access protected + * @param array|string $engine 引擎参数 + * @return $this + */ + protected function engine($engine) + { + $this->view->engine($engine); + + return $this; + } + + /** + * 设置验证失败后是否抛出异常 + * @access protected + * @param bool $fail 是否抛出异常 + * @return $this + */ + 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 回调方法(闭包) + * @return array|string|true + * @throws ValidateException + */ + protected function validate($data, $validate, $message = [], $batch = false, $callback = null) + { + if (is_array($validate)) { + $v = $this->app->validate(); + $v->rule($validate); + } else { + if (strpos($validate, '.')) { + // 支持场景 + list($validate, $scene) = explode('.', $validate); + } + $v = $this->app->validate($validate); + if (!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]); + } + + if (!$v->check($data)) { + if ($this->failException) { + throw new ValidateException($v->getError()); + } + return $v->getError(); + } + + return true; + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app'], $data['request']); + + return $data; + } +} diff --git a/thinkphp/library/think/Cookie.php b/thinkphp/library/think/Cookie.php new file mode 100644 index 0000000..6a9fb1e --- /dev/null +++ b/thinkphp/library/think/Cookie.php @@ -0,0 +1,268 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Cookie +{ + /** + * 配置参数 + * @var array + */ + protected $config = [ + // cookie 名称前缀 + 'prefix' => '', + // cookie 保存时间 + 'expire' => 0, + // cookie 保存路径 + 'path' => '/', + // cookie 有效域名 + 'domain' => '', + // cookie 启用安全传输 + 'secure' => false, + // httponly设置 + 'httponly' => false, + // 是否使用 setcookie + 'setcookie' => true, + ]; + + /** + * 构造方法 + * @access public + */ + public function __construct(array $config = []) + { + $this->init($config); + } + + /** + * Cookie初始化 + * @access public + * @param array $config + * @return void + */ + public function init(array $config = []) + { + $this->config = array_merge($this->config, array_change_key_case($config)); + + if (!empty($this->config['httponly']) && PHP_SESSION_ACTIVE != session_status()) { + ini_set('session.cookie_httponly', 1); + } + } + + public static function __make(Config $config) + { + return new static($config->pull('cookie')); + } + + /** + * 设置或者获取cookie作用域(前缀) + * @access public + * @param string $prefix + * @return string|void + */ + public function prefix($prefix = '') + { + if (empty($prefix)) { + return $this->config['prefix']; + } + + $this->config['prefix'] = $prefix; + } + + /** + * Cookie 设置、获取、删除 + * + * @access public + * @param string $name cookie名称 + * @param mixed $value cookie值 + * @param mixed $option 可选参数 可能会是 null|integer|string + * @return void + */ + public function set($name, $value = '', $option = null) + { + // 参数设置(会覆盖黙认设置) + if (!is_null($option)) { + if (is_numeric($option)) { + $option = ['expire' => $option]; + } elseif (is_string($option)) { + parse_str($option, $option); + } + + $config = array_merge($this->config, array_change_key_case($option)); + } else { + $config = $this->config; + } + + $name = $config['prefix'] . $name; + + // 设置cookie + if (is_array($value)) { + array_walk_recursive($value, [$this, 'jsonFormatProtect'], 'encode'); + $value = 'think:' . json_encode($value); + } + + $expire = !empty($config['expire']) ? $_SERVER['REQUEST_TIME'] + intval($config['expire']) : 0; + + if ($config['setcookie']) { + $this->setCookie($name, $value, $expire, $config); + } + + $_COOKIE[$name] = $value; + } + + /** + * Cookie 设置保存 + * + * @access public + * @param string $name cookie名称 + * @param mixed $value cookie值 + * @param array $option 可选参数 + * @return void + */ + protected function setCookie($name, $value, $expire, $option = []) + { + setcookie($name, $value, $expire, $option['path'], $option['domain'], $option['secure'], $option['httponly']); + } + + /** + * 永久保存Cookie数据 + * @access public + * @param string $name cookie名称 + * @param mixed $value cookie值 + * @param mixed $option 可选参数 可能会是 null|integer|string + * @return void + */ + public function forever($name, $value = '', $option = null) + { + if (is_null($option) || is_numeric($option)) { + $option = []; + } + + $option['expire'] = 315360000; + + $this->set($name, $value, $option); + } + + /** + * 判断Cookie数据 + * @access public + * @param string $name cookie名称 + * @param string|null $prefix cookie前缀 + * @return bool + */ + public function has($name, $prefix = null) + { + $prefix = !is_null($prefix) ? $prefix : $this->config['prefix']; + $name = $prefix . $name; + + return isset($_COOKIE[$name]); + } + + /** + * Cookie获取 + * @access public + * @param string $name cookie名称 留空获取全部 + * @param string|null $prefix cookie前缀 + * @return mixed + */ + public function get($name = '', $prefix = null) + { + $prefix = !is_null($prefix) ? $prefix : $this->config['prefix']; + $key = $prefix . $name; + + if ('' == $name) { + 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); + array_walk_recursive($value, [$this, 'jsonFormatProtect'], 'decode'); + } + } else { + $value = null; + } + + return $value; + } + + /** + * Cookie删除 + * @access public + * @param string $name cookie名称 + * @param string|null $prefix cookie前缀 + * @return void + */ + public function delete($name, $prefix = null) + { + $config = $this->config; + $prefix = !is_null($prefix) ? $prefix : $config['prefix']; + $name = $prefix . $name; + + if ($config['setcookie']) { + $this->setcookie($name, '', $_SERVER['REQUEST_TIME'] - 3600, $config); + } + + // 删除指定cookie + unset($_COOKIE[$name]); + } + + /** + * Cookie清空 + * @access public + * @param string|null $prefix cookie前缀 + * @return void + */ + public function clear($prefix = null) + { + // 清除指定前缀的所有cookie + if (empty($_COOKIE)) { + return; + } + + // 要删除的cookie前缀,不指定则删除config设置的指定前缀 + $config = $this->config; + $prefix = !is_null($prefix) ? $prefix : $config['prefix']; + + if ($prefix) { + // 如果前缀为空字符串将不作处理直接返回 + foreach ($_COOKIE as $key => $val) { + if (0 === strpos($key, $prefix)) { + if ($config['setcookie']) { + $this->setcookie($key, '', $_SERVER['REQUEST_TIME'] - 3600, $config); + } + unset($_COOKIE[$key]); + } + } + } + + return; + } + + private 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 new file mode 100644 index 0000000..9280eac --- /dev/null +++ b/thinkphp/library/think/Db.php @@ -0,0 +1,197 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\db\Connection; + +/** + * Class Db + * @package think + * @method \think\db\Query master() static 从主服务器读取数据 + * @method \think\db\Query readMaster(bool $all = false) static 后续从主服务器读取数据 + * @method \think\db\Query table(string $table) static 指定数据表(含前缀) + * @method \think\db\Query name(string $name) static 指定数据表(不含前缀) + * @method \think\db\Expression raw(string $value) static 使用表达式设置数据 + * @method \think\db\Query where(mixed $field, string $op = null, mixed $condition = null) static 查询条件 + * @method \think\db\Query whereRaw(string $where, array $bind = []) static 表达式查询 + * @method \think\db\Query whereExp(string $field, string $condition, array $bind = []) static 字段表达式查询 + * @method \think\db\Query when(mixed $condition, mixed $query, mixed $otherwise = null) static 条件查询 + * @method \think\db\Query join(mixed $join, mixed $condition = null, string $type = 'INNER') static JOIN查询 + * @method \think\db\Query view(mixed $join, mixed $field = null, mixed $on = null, string $type = 'INNER') static 视图查询 + * @method \think\db\Query field(mixed $field, boolean $except = false) static 指定查询字段 + * @method \think\db\Query fieldRaw(string $field, array $bind = []) static 指定查询字段 + * @method \think\db\Query union(mixed $union, boolean $all = false) static UNION查询 + * @method \think\db\Query limit(mixed $offset, integer $length = null) static 查询LIMIT + * @method \think\db\Query order(mixed $field, string $order = null) static 查询ORDER + * @method \think\db\Query orderRaw(string $field, array $bind = []) static 查询ORDER + * @method \think\db\Query cache(mixed $key = null , integer $expire = null) static 设置查询缓存 + * @method \think\db\Query withAttr(string $name,callable $callback = null) static 使用获取器获取数据 + * @method mixed value(string $field) static 获取某个字段的值 + * @method array column(string $field, string $key = '') static 获取某个列的值 + * @method mixed find(mixed $data = null) static 查询单个记录 + * @method mixed select(mixed $data = null) static 查询多个记录 + * @method integer insert(array $data, boolean $replace = false, boolean $getLastInsID = false, string $sequence = null) static 插入一条记录 + * @method integer insertGetId(array $data, boolean $replace = false, string $sequence = null) static 插入一条记录并返回自增ID + * @method integer insertAll(array $dataSet) static 插入多条记录 + * @method integer update(array $data) static 更新记录 + * @method integer delete(mixed $data = null) static 删除记录 + * @method boolean chunk(integer $count, callable $callback, string $column = null) static 分块获取数据 + * @method \Generator cursor(mixed $data = null) static 使用游标查找记录 + * @method mixed query(string $sql, array $bind = [], boolean $master = false, bool $pdo = false) static SQL查询 + * @method integer execute(string $sql, array $bind = [], boolean $fetch = false, boolean $getLastInsID = false, string $sequence = null) static SQL执行 + * @method \think\Paginator paginate(integer $listRows = 15, mixed $simple = null, array $config = []) static 分页查询 + * @method mixed transaction(callable $callback) static 执行数据库事务 + * @method void startTrans() static 启动事务 + * @method void commit() static 用于非自动提交状态下面的查询提交 + * @method void rollback() static 事务回滚 + * @method boolean batchQuery(array $sqlArray) static 批处理执行SQL语句 + * @method string getLastInsID(string $sequence = null) static 获取最近插入的ID + */ +class Db +{ + /** + * 当前数据库连接对象 + * @var Connection + */ + protected static $connection; + + /** + * 数据库配置 + * @var array + */ + protected static $config = []; + + /** + * 查询次数 + * @var integer + */ + public static $queryTimes = 0; + + /** + * 执行次数 + * @var integer + */ + public static $executeTimes = 0; + + /** + * 配置 + * @access public + * @param mixed $config + * @return void + */ + public static function init($config = []) + { + self::$config = $config; + + if (empty($config['query'])) { + self::$config['query'] = '\\think\\db\\Query'; + } + } + + /** + * 获取数据库配置 + * @access public + * @param string $config 配置名称 + * @return mixed + */ + public static function getConfig($name = '') + { + if ('' === $name) { + return self::$config; + } + + return isset(self::$config[$name]) ? self::$config[$name] : null; + } + + /** + * 切换数据库连接 + * @access public + * @param mixed $config 连接配置 + * @param bool|string $name 连接标识 true 强制重新连接 + * @param string $query 查询对象类名 + * @return mixed 返回查询对象实例 + * @throws Exception + */ + public static function connect($config = [], $name = false, $query = '') + { + // 解析配置参数 + $options = self::parseConfig($config ?: self::$config); + + $query = $query ?: $options['query']; + + // 创建数据库连接对象实例 + self::$connection = Connection::instance($options, $name); + + return new $query(self::$connection); + } + + /** + * 数据库连接参数解析 + * @access private + * @param mixed $config + * @return array + */ + private static function parseConfig($config) + { + if (is_string($config) && false === strpos($config, '/')) { + // 支持读取配置参数 + $config = isset(self::$config[$config]) ? self::$config[$config] : self::$config; + } + + $result = is_string($config) ? self::parseDsnConfig($config) : $config; + + if (empty($result['query'])) { + $result['query'] = self::$config['query']; + } + + return $result; + } + + /** + * DSN解析 + * 格式: mysql://username:passwd@localhost:3306/DbName?param1=val1¶m2=val2#utf8 + * @access private + * @param string $dsnStr + * @return array + */ + private static function parseDsnConfig($dsnStr) + { + $info = parse_url($dsnStr); + + if (!$info) { + return []; + } + + $dsn = [ + 'type' => $info['scheme'], + 'username' => isset($info['user']) ? $info['user'] : '', + 'password' => isset($info['pass']) ? $info['pass'] : '', + 'hostname' => isset($info['host']) ? $info['host'] : '', + 'hostport' => isset($info['port']) ? $info['port'] : '', + 'database' => !empty($info['path']) ? ltrim($info['path'], '/') : '', + 'charset' => isset($info['fragment']) ? $info['fragment'] : 'utf8', + ]; + + if (isset($info['query'])) { + parse_str($info['query'], $dsn['params']); + } else { + $dsn['params'] = []; + } + + return $dsn; + } + + public static function __callStatic($method, $args) + { + return call_user_func_array([static::connect(), $method], $args); + } +} diff --git a/thinkphp/library/think/Debug.php b/thinkphp/library/think/Debug.php new file mode 100644 index 0000000..776e178 --- /dev/null +++ b/thinkphp/library/think/Debug.php @@ -0,0 +1,278 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\model\Collection as ModelCollection; +use think\response\Redirect; + +class Debug +{ + /** + * 配置参数 + * @var array + */ + protected $config = []; + + /** + * 区间时间信息 + * @var array + */ + protected $info = []; + + /** + * 区间内存信息 + * @var array + */ + protected $mem = []; + + /** + * 应用对象 + * @var App + */ + protected $app; + + public function __construct(App $app, array $config = []) + { + $this->app = $app; + $this->config = $config; + } + + public static function __make(App $app, Config $config) + { + return new static($app, $config->pull('trace')); + } + + public function setConfig(array $config) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 记录时间(微秒)和内存使用情况 + * @access public + * @param string $name 标记位置 + * @param mixed $value 标记值 留空则取当前 time 表示仅记录时间 否则同时记录时间和内存 + * @return void + */ + public function remark($name, $value = '') + { + // 记录时间和内存使用 + $this->info[$name] = is_float($value) ? $value : microtime(true); + + if ('time' != $value) { + $this->mem['mem'][$name] = is_float($value) ? $value : memory_get_usage(); + $this->mem['peak'][$name] = memory_get_peak_usage(); + } + } + + /** + * 统计某个区间的时间(微秒)使用情况 + * @access public + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 + * @return integer + */ + public function getRangeTime($start, $end, $dec = 6) + { + if (!isset($this->info[$end])) { + $this->info[$end] = microtime(true); + } + + return number_format(($this->info[$end] - $this->info[$start]), $dec); + } + + /** + * 统计从开始到统计时的时间(微秒)使用情况 + * @access public + * @param integer|string $dec 小数位 + * @return integer + */ + public function getUseTime($dec = 6) + { + return number_format((microtime(true) - $this->app->getBeginTime()), $dec); + } + + /** + * 获取当前访问的吞吐率情况 + * @access public + * @return string + */ + public function getThroughputRate() + { + return number_format(1 / $this->getUseTime(), 2) . 'req/s'; + } + + /** + * 记录区间的内存使用情况 + * @access public + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 + * @return string + */ + public function getRangeMem($start, $end, $dec = 2) + { + if (!isset($this->mem['mem'][$end])) { + $this->mem['mem'][$end] = memory_get_usage(); + } + + $size = $this->mem['mem'][$end] - $this->mem['mem'][$start]; + $a = ['B', 'KB', 'MB', 'GB', 'TB']; + $pos = 0; + + while ($size >= 1024) { + $size /= 1024; + $pos++; + } + + return round($size, $dec) . " " . $a[$pos]; + } + + /** + * 统计从开始到统计时的内存使用情况 + * @access public + * @param integer|string $dec 小数位 + * @return string + */ + public function getUseMem($dec = 2) + { + $size = memory_get_usage() - $this->app->getBeginMem(); + $a = ['B', 'KB', 'MB', 'GB', 'TB']; + $pos = 0; + + while ($size >= 1024) { + $size /= 1024; + $pos++; + } + + return round($size, $dec) . " " . $a[$pos]; + } + + /** + * 统计区间的内存峰值情况 + * @access public + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 + * @return string + */ + public function getMemPeak($start, $end, $dec = 2) + { + if (!isset($this->mem['peak'][$end])) { + $this->mem['peak'][$end] = memory_get_peak_usage(); + } + + $size = $this->mem['peak'][$end] - $this->mem['peak'][$start]; + $a = ['B', 'KB', 'MB', 'GB', 'TB']; + $pos = 0; + + while ($size >= 1024) { + $size /= 1024; + $pos++; + } + + return round($size, $dec) . " " . $a[$pos]; + } + + /** + * 获取文件加载信息 + * @access public + * @param bool $detail 是否显示详细 + * @return integer|array + */ + public function getFile($detail = false) + { + if ($detail) { + $files = get_included_files(); + $info = []; + + foreach ($files as $key => $file) { + $info[] = $file . ' ( ' . number_format(filesize($file) / 1024, 2) . ' KB )'; + } + + return $info; + } + + return count(get_included_files()); + } + + /** + * 浏览器友好的变量输出 + * @access public + * @param mixed $var 变量 + * @param boolean $echo 是否输出 默认为true 如果为false 则返回输出字符串 + * @param string $label 标签 默认为空 + * @param integer $flags htmlspecialchars flags + * @return void|string + */ + public function dump($var, $echo = true, $label = null, $flags = ENT_SUBSTITUTE) + { + $label = (null === $label) ? '' : rtrim($label) . ':'; + if ($var instanceof Model || $var instanceof ModelCollection) { + $var = $var->toArray(); + } + + ob_start(); + var_dump($var); + + $output = ob_get_clean(); + $output = preg_replace('/\]\=\>\n(\s+)/m', '] => ', $output); + + if (PHP_SAPI == '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; + } + return $output; + } + + public function inject(Response $response, &$content) + { + $config = $this->config; + $type = isset($config['type']) ? $config['type'] : 'Html'; + + unset($config['type']); + + $trace = Loader::factory($type, '\\think\\debug\\', $config); + + if ($response instanceof Redirect) { + //TODO 记录 + } else { + $output = $trace->output($response, $this->app['log']->getLog()); + if (is_string($output)) { + // trace调试信息注入 + $pos = strripos($content, ''); + if (false !== $pos) { + $content = substr($content, 0, $pos) . $output . substr($content, $pos); + } else { + $content = $content . $output; + } + } + } + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app']); + + return $data; + } +} diff --git a/thinkphp/library/think/Env.php b/thinkphp/library/think/Env.php new file mode 100644 index 0000000..eaeee94 --- /dev/null +++ b/thinkphp/library/think/Env.php @@ -0,0 +1,113 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Env +{ + /** + * 环境变量数据 + * @var array + */ + protected $data = []; + + public function __construct() + { + $this->data = $_ENV; + } + + /** + * 读取环境变量定义文件 + * @access public + * @param string $file 环境变量定义文件 + * @return void + */ + public function load($file) + { + $env = parse_ini_file($file, true); + $this->set($env); + } + + /** + * 获取环境变量值 + * @access public + * @param string $name 环境变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name = null, $default = null, $php_prefix = true) + { + if (is_null($name)) { + return $this->data; + } + + $name = strtoupper(str_replace('.', '_', $name)); + + if (isset($this->data[$name])) { + return $this->data[$name]; + } + + return $this->getEnv($name, $default, $php_prefix); + } + + protected function getEnv($name, $default = null, $php_prefix = true) + { + if ($php_prefix) { + $name = 'PHP_' . $name; + } + + $result = getenv($name); + + if (false === $result) { + return $default; + } + + if ('false' === $result) { + $result = false; + } elseif ('true' === $result) { + $result = true; + } + + if (!isset($this->data[$name])) { + $this->data[$name] = $result; + } + + return $result; + } + + /** + * 设置环境变量值 + * @access public + * @param string|array $env 环境变量 + * @param mixed $value 值 + * @return void + */ + public function set($env, $value = null) + { + if (is_array($env)) { + $env = array_change_key_case($env, CASE_UPPER); + + foreach ($env as $key => $val) { + if (is_array($val)) { + foreach ($val as $k => $v) { + $this->data[$key . '_' . strtoupper($k)] = $v; + } + } else { + $this->data[$key] = $val; + } + } + } else { + $name = strtoupper(str_replace('.', '_', $env)); + + $this->data[$name] = $value; + } + } +} diff --git a/thinkphp/library/think/Error.php b/thinkphp/library/think/Error.php new file mode 100644 index 0000000..ea3328e --- /dev/null +++ b/thinkphp/library/think/Error.php @@ -0,0 +1,147 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\console\Output as ConsoleOutput; +use think\exception\ErrorException; +use think\exception\Handle; +use think\exception\ThrowableError; + +class Error +{ + /** + * 配置参数 + * @var array + */ + protected static $exceptionHandler; + + /** + * 注册异常处理 + * @access public + * @return void + */ + public static function register() + { + error_reporting(E_ALL); + set_error_handler([__CLASS__, 'appError']); + set_exception_handler([__CLASS__, 'appException']); + register_shutdown_function([__CLASS__, 'appShutdown']); + } + + /** + * Exception Handler + * @access public + * @param \Exception|\Throwable $e + */ + public static function appException($e) + { + if (!$e instanceof \Exception) { + $e = new ThrowableError($e); + } + + self::getExceptionHandler()->report($e); + + if (PHP_SAPI == 'cli') { + self::getExceptionHandler()->renderForConsole(new ConsoleOutput, $e); + } else { + self::getExceptionHandler()->render($e)->send(); + } + } + + /** + * Error Handler + * @access public + * @param integer $errno 错误编号 + * @param integer $errstr 详细错误信息 + * @param string $errfile 出错的文件 + * @param integer $errline 出错行号 + * @throws ErrorException + */ + public static function appError($errno, $errstr, $errfile = '', $errline = 0) + { + $exception = new ErrorException($errno, $errstr, $errfile, $errline); + if (error_reporting() & $errno) { + // 将错误信息托管至 think\exception\ErrorException + throw $exception; + } + + self::getExceptionHandler()->report($exception); + } + + /** + * Shutdown Handler + * @access public + */ + public static function appShutdown() + { + 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); + } + + // 写入日志 + Container::get('log')->save(); + } + + /** + * 确定错误类型是否致命 + * + * @access protected + * @param int $type + * @return bool + */ + protected static function isFatal($type) + { + return in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE]); + } + + /** + * 设置异常处理类 + * + * @access public + * @param mixed $handle + * @return void + */ + public static function setExceptionHandler($handle) + { + self::$exceptionHandler = $handle; + } + + /** + * Get an instance of the exception handler. + * + * @access public + * @return Handle + */ + public static function getExceptionHandler() + { + static $handle; + + if (!$handle) { + // 异常处理handle + $class = self::$exceptionHandler; + + 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 new file mode 100644 index 0000000..414a090 --- /dev/null +++ b/thinkphp/library/think/Exception.php @@ -0,0 +1,56 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Exception extends \Exception +{ + + /** + * 保存异常页面显示的额外Debug数据 + * @var array + */ + protected $data = []; + + /** + * 设置异常额外的Debug数据 + * 数据将会显示为下面的格式 + * + * Exception Data + * -------------------------------------------------- + * Label 1 + * key1 value1 + * key2 value2 + * Label 2 + * key1 value1 + * key2 value2 + * + * @access protected + * @param string $label 数据分类,用于异常页面显示 + * @param array $data 需要显示的数据,必须为关联数组 + */ + final protected function setData($label, array $data) + { + $this->data[$label] = $data; + } + + /** + * 获取异常额外Debug数据 + * 主要用于输出到异常页面便于调试 + * @access public + * @return array 由setData设置的Debug数据 + */ + final public function getData() + { + return $this->data; + } + +} diff --git a/thinkphp/library/think/Facade.php b/thinkphp/library/think/Facade.php new file mode 100644 index 0000000..ac5ae28 --- /dev/null +++ b/thinkphp/library/think/Facade.php @@ -0,0 +1,125 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Facade +{ + /** + * 绑定对象 + * @var array + */ + protected static $bind = []; + + /** + * 始终创建新的对象实例 + * @var bool + */ + protected static $alwaysNewInstance; + + /** + * 绑定类的静态代理 + * @static + * @access public + * @param string|array $name 类标识 + * @param string $class 类名 + * @return object + */ + public static function bind($name, $class = null) + { + if (__CLASS__ != static::class) { + return self::__callStatic('bind', func_get_args()); + } + + if (is_array($name)) { + self::$bind = array_merge(self::$bind, $name); + } else { + self::$bind[$name] = $class; + } + } + + /** + * 创建Facade实例 + * @static + * @access protected + * @param string $class 类名或标识 + * @param array $args 变量 + * @param bool $newInstance 是否每次创建新的实例 + * @return object + */ + protected static function createFacade($class = '', $args = [], $newInstance = false) + { + $class = $class ?: static::class; + + $facadeClass = static::getFacadeClass(); + + if ($facadeClass) { + $class = $facadeClass; + } elseif (isset(self::$bind[$class])) { + $class = self::$bind[$class]; + } + + if (static::$alwaysNewInstance) { + $newInstance = true; + } + + return Container::getInstance()->make($class, $args, $newInstance); + } + + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + {} + + /** + * 带参数实例化当前Facade类 + * @access public + * @return mixed + */ + public static function instance(...$args) + { + if (__CLASS__ != static::class) { + return self::createFacade('', $args); + } + } + + /** + * 调用类的实例 + * @access public + * @param string $class 类名或者标识 + * @param array|true $args 变量 + * @param bool $newInstance 是否每次创建新的实例 + * @return mixed + */ + public static function make($class, $args = [], $newInstance = false) + { + if (__CLASS__ != static::class) { + return self::__callStatic('make', func_get_args()); + } + + if (true === $args) { + // 总是创建新的实例化对象 + $newInstance = true; + $args = []; + } + + return self::createFacade($class, $args, $newInstance); + } + + // 调用实际类的方法 + public static function __callStatic($method, $params) + { + return call_user_func_array([static::createFacade(), $method], $params); + } +} diff --git a/thinkphp/library/think/File.php b/thinkphp/library/think/File.php new file mode 100644 index 0000000..b24b777 --- /dev/null +++ b/thinkphp/library/think/File.php @@ -0,0 +1,496 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use SplFileObject; + +class File extends SplFileObject +{ + /** + * 错误信息 + * @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 + */ + protected $hash = []; + + public function __construct($filename, $mode = 'r') + { + parent::__construct($filename, $mode); + + $this->filename = $this->getRealPath() ?: $this->getPathname(); + } + + /** + * 是否测试 + * @access public + * @param bool $test 是否测试 + * @return $this + */ + public function isTest($test = false) + { + $this->isTest = $test; + + return $this; + } + + /** + * 设置上传信息 + * @access public + * @param array $info 上传文件信息 + * @return $this + */ + public function setUploadInfo($info) + { + $this->info = $info; + + return $this; + } + + /** + * 获取上传文件的信息 + * @access public + * @param string $name + * @return array|string + */ + public function getInfo($name = '') + { + return isset($this->info[$name]) ? $this->info[$name] : $this->info; + } + + /** + * 获取上传文件的文件名 + * @access public + * @return string + */ + public function getSaveName() + { + return $this->saveName; + } + + /** + * 设置上传文件的保存文件名 + * @access public + * @param string $saveName + * @return $this + */ + public function setSaveName($saveName) + { + $this->saveName = $saveName; + + return $this; + } + + /** + * 获取文件的哈希散列值 + * @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]; + } + + /** + * 检查目录是否可写 + * @access protected + * @param string $path 目录 + * @return boolean + */ + protected function checkPath($path) + { + if (is_dir($path)) { + return true; + } + + if (mkdir($path, 0755, true)) { + return true; + } + + $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); + } + + /** + * 设置文件的命名规则 + * @access public + * @param string $rule 文件命名规则 + * @return $this + */ + public function rule($rule) + { + $this->rule = $rule; + + return $this; + } + + /** + * 设置上传文件的验证规则 + * @access public + * @param array $rule 验证规则 + * @return $this + */ + public function validate($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); + } + + /** + * 检测上传文件 + * @access public + * @param array $rule 验证规则 + * @return bool + */ + public function check($rule = []) + { + $rule = $rule ?: $this->validate; + + if ((isset($rule['size']) && !$this->checkSize($rule['size'])) + || (isset($rule['type']) && !$this->checkMime($rule['type'])) + || (isset($rule['ext']) && !$this->checkExt($rule['ext'])) + || !$this->checkImg()) { + return false; + } + + return true; + } + + /** + * 检测上传文件后缀 + * @access public + * @param array|string $ext 允许后缀 + * @return bool + */ + public function checkExt($ext) + { + if (is_string($ext)) { + $ext = explode(',', $ext); + } + + $extension = strtolower(pathinfo($this->getInfo('name'), PATHINFO_EXTENSION)); + + if (!in_array($extension, $ext)) { + $this->error = 'extensions to upload is not allowed'; + return false; + } + + return true; + } + + /** + * 检测图像文件 + * @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, 13])) { + $this->error = 'illegal image files'; + return false; + } + + return true; + } + + // 判断图像类型 + protected function getImageType($image) + { + if (function_exists('exif_imagetype')) { + return exif_imagetype($image); + } + + try { + $info = getimagesize($image); + return $info ? $info[2] : false; + } catch (\Exception $e) { + return false; + } + } + + /** + * 检测上传文件大小 + * @access public + * @param integer $size 最大大小 + * @return bool + */ + public function checkSize($size) + { + if ($this->getSize() > (int) $size) { + $this->error = 'filesize not match'; + return false; + } + + return true; + } + + /** + * 检测上传文件类型 + * @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)) { + $this->error = 'mimetype to upload is not allowed'; + return false; + } + + return true; + } + + /** + * 移动文件 + * @access public + * @param string $path 保存路径 + * @param string|bool $savename 保存的文件名 默认自动生成 + * @param boolean $replace 同名文件是否覆盖 + * @param bool $autoAppendExt 自动补充扩展名 + * @return false|File false-失败 否则返回File实例 + */ + public function move($path, $savename = true, $replace = true, $autoAppendExt = true) + { + // 文件上传失败,捕获错误代码 + if (!empty($this->info['error'])) { + $this->error($this->info['error']); + return false; + } + + // 检测合法性 + if (!$this->isValid()) { + $this->error = 'upload illegal files'; + return false; + } + + // 验证上传 + if (!$this->check()) { + return false; + } + + $path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + // 文件保存命名规则 + $saveName = $this->buildSaveName($savename, $autoAppendExt); + $filename = $path . $saveName; + + // 检测目录 + if (false === $this->checkPath(dirname($filename))) { + return false; + } + + /* 不覆盖同名文件 */ + if (!$replace && is_file($filename)) { + $this->error = ['has the same filename: {:filename}', ['filename' => $filename]]; + return false; + } + + /* 移动文件 */ + if ($this->isTest) { + rename($this->filename, $filename); + } elseif (!move_uploaded_file($this->filename, $filename)) { + $this->error = 'upload write error'; + return false; + } + + // 返回 File对象实例 + $file = new self($filename); + $file->setSaveName($saveName); + $file->setUploadInfo($this->info); + + return $file; + } + + /** + * 获取保存文件名 + * @access protected + * @param string|bool $savename 保存的文件名 默认自动生成 + * @param bool $autoAppendExt 自动补充扩展名 + * @return string + */ + protected function buildSaveName($savename, $autoAppendExt = true) + { + if (true === $savename) { + // 自动生成文件名 + $savename = $this->autoBuildName(); + } elseif ('' === $savename || false === $savename) { + // 保留原文件名 + $savename = $this->getInfo('name'); + } + + if ($autoAppendExt && false === strpos($savename, '.')) { + $savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION); + } + + return $savename; + } + + /** + * 自动生成文件名 + * @access protected + * @return string + */ + protected function autoBuildName() + { + if ($this->rule instanceof \Closure) { + $savename = call_user_func_array($this->rule, [$this]); + } else { + switch ($this->rule) { + case 'date': + $savename = date('Ymd') . DIRECTORY_SEPARATOR . md5(microtime(true)); + break; + default: + if (in_array($this->rule, hash_algos())) { + $hash = $this->hash($this->rule); + $savename = substr($hash, 0, 2) . DIRECTORY_SEPARATOR . substr($hash, 2); + } elseif (is_callable($this->rule)) { + $savename = call_user_func($this->rule); + } else { + $savename = date('Ymd') . DIRECTORY_SEPARATOR . md5(microtime(true)); + } + } + } + + return $savename; + } + + /** + * 获取错误代码信息 + * @access private + * @param int $errorNo 错误号 + */ + private function error($errorNo) + { + switch ($errorNo) { + case 1: + case 2: + $this->error = 'upload File size exceeds the maximum value'; + break; + case 3: + $this->error = 'only the portion of file is uploaded'; + break; + case 4: + $this->error = 'no file to uploaded'; + break; + case 6: + $this->error = 'upload temp dir not found'; + break; + case 7: + $this->error = 'file write error'; + break; + default: + $this->error = 'unknown upload error'; + } + } + + /** + * 获取错误信息(支持多语言) + * @access public + * @return string + */ + public function getError() + { + $lang = Container::get('lang'); + + if (is_array($this->error)) { + list($msg, $vars) = $this->error; + } else { + $msg = $this->error; + $vars = []; + } + + return $lang->has($msg) ? $lang->get($msg, $vars) : $msg; + } + + public function __call($method, $args) + { + return $this->hash($method); + } +} diff --git a/thinkphp/library/think/Hook.php b/thinkphp/library/think/Hook.php new file mode 100644 index 0000000..1d01141 --- /dev/null +++ b/thinkphp/library/think/Hook.php @@ -0,0 +1,220 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Hook +{ + /** + * 钩子行为定义 + * @var array + */ + private $tags = []; + + /** + * 绑定行为列表 + * @var array + */ + protected $bind = []; + + /** + * 入口方法名称 + * @var string + */ + private static $portal = 'run'; + + /** + * 应用对象 + * @var App + */ + protected $app; + + public function __construct(App $app) + { + $this->app = $app; + } + + /** + * 指定入口方法名称 + * @access public + * @param string $name 方法名 + * @return $this + */ + public function portal($name) + { + self::$portal = $name; + return $this; + } + + /** + * 指定行为标识 便于调用 + * @access public + * @param string|array $name 行为标识 + * @param mixed $behavior 行为 + * @return $this + */ + public function alias($name, $behavior = null) + { + if (is_array($name)) { + $this->bind = array_merge($this->bind, $name); + } else { + $this->bind[$name] = $behavior; + } + + return $this; + } + + /** + * 动态添加行为扩展到某个标签 + * @access public + * @param string $tag 标签名称 + * @param mixed $behavior 行为名称 + * @param bool $first 是否放到开头执行 + * @return void + */ + public function add($tag, $behavior, $first = false) + { + isset($this->tags[$tag]) || $this->tags[$tag] = []; + + if (is_array($behavior) && !is_callable($behavior)) { + if (!array_key_exists('_overlay', $behavior)) { + $this->tags[$tag] = array_merge($this->tags[$tag], $behavior); + } else { + unset($behavior['_overlay']); + $this->tags[$tag] = $behavior; + } + } elseif ($first) { + array_unshift($this->tags[$tag], $behavior); + } else { + $this->tags[$tag][] = $behavior; + } + } + + /** + * 批量导入插件 + * @access public + * @param array $tags 插件信息 + * @param bool $recursive 是否递归合并 + * @return void + */ + public function import(array $tags, $recursive = true) + { + if ($recursive) { + foreach ($tags as $tag => $behavior) { + $this->add($tag, $behavior); + } + } else { + $this->tags = $tags + $this->tags; + } + } + + /** + * 获取插件信息 + * @access public + * @param string $tag 插件位置 留空获取全部 + * @return array + */ + public function get($tag = '') + { + if (empty($tag)) { + //获取全部的插件信息 + return $this->tags; + } + + return array_key_exists($tag, $this->tags) ? $this->tags[$tag] : []; + } + + /** + * 监听标签的行为 + * @access public + * @param string $tag 标签名称 + * @param mixed $params 传入参数 + * @param bool $once 只获取一个有效返回值 + * @return mixed + */ + public function listen($tag, $params = null, $once = false) + { + $results = []; + $tags = $this->get($tag); + + foreach ($tags as $key => $name) { + $results[$key] = $this->execTag($name, $tag, $params); + + if (false === $results[$key] || (!is_null($results[$key]) && $once)) { + break; + } + } + + return $once ? end($results) : $results; + } + + /** + * 执行行为 + * @access public + * @param mixed $class 行为 + * @param mixed $params 参数 + * @return mixed + */ + public function exec($class, $params = null) + { + if ($class instanceof \Closure || is_array($class)) { + $method = $class; + } else { + if (isset($this->bind[$class])) { + $class = $this->bind[$class]; + } + $method = [$class, self::$portal]; + } + + return $this->app->invoke($method, [$params]); + } + + /** + * 执行某个标签的行为 + * @access protected + * @param mixed $class 要执行的行为 + * @param string $tag 方法名(标签名) + * @param mixed $params 参数 + * @return mixed + */ + protected function execTag($class, $tag = '', $params = null) + { + $method = Loader::parseName($tag, 1, false); + + if ($class instanceof \Closure) { + $call = $class; + $class = 'Closure'; + } elseif (is_array($class) || strpos($class, '::')) { + $call = $class; + } else { + $obj = Container::get($class); + + if (!is_callable([$obj, $method])) { + $method = self::$portal; + } + + $call = [$class, $method]; + $class = $class . '->' . $method; + } + + $result = $this->app->invoke($call, [$params]); + + return $result; + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app']); + + return $data; + } +} diff --git a/thinkphp/library/think/Lang.php b/thinkphp/library/think/Lang.php new file mode 100644 index 0000000..ed36dd8 --- /dev/null +++ b/thinkphp/library/think/Lang.php @@ -0,0 +1,290 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Lang +{ + /** + * 多语言信息 + * @var array + */ + private $lang = []; + + /** + * 当前语言 + * @var string + */ + private $range = 'zh-cn'; + + /** + * 多语言自动侦测变量名 + * @var string + */ + protected $langDetectVar = 'lang'; + + /** + * 多语言cookie变量 + * @var string + */ + protected $langCookieVar = 'think_var'; + + /** + * 允许的多语言列表 + * @var array + */ + protected $allowLangList = []; + + /** + * Accept-Language转义为对应语言包名称 系统默认配置 + * @var string + */ + protected $acceptLanguage = [ + 'zh-hans-cn' => 'zh-cn', + ]; + + /** + * 应用对象 + * @var App + */ + protected $app; + + public function __construct(App $app) + { + $this->app = $app; + } + + // 设定当前的语言 + public function range($range = '') + { + if ('' == $range) { + return $this->range; + } else { + $this->range = $range; + } + } + + /** + * 设置语言定义(不区分大小写) + * @access public + * @param string|array $name 语言变量 + * @param string $value 语言值 + * @param string $range 语言作用域 + * @return mixed + */ + public function set($name, $value = null, $range = '') + { + $range = $range ?: $this->range; + // 批量定义 + if (!isset($this->lang[$range])) { + $this->lang[$range] = []; + } + + if (is_array($name)) { + return $this->lang[$range] = array_change_key_case($name) + $this->lang[$range]; + } + + return $this->lang[$range][strtolower($name)] = $value; + } + + /** + * 加载语言定义(不区分大小写) + * @access public + * @param string|array $file 语言文件 + * @param string $range 语言作用域 + * @return array + */ + public function load($file, $range = '') + { + $range = $range ?: $this->range; + if (!isset($this->lang[$range])) { + $this->lang[$range] = []; + } + + // 批量定义 + if (is_string($file)) { + $file = [$file]; + } + + $lang = []; + + foreach ($file as $_file) { + if (is_file($_file)) { + // 记录加载信息 + $this->app->log('[ LANG ] ' . $_file); + $_lang = include $_file; + if (is_array($_lang)) { + $lang = array_change_key_case($_lang) + $lang; + } + } + } + + if (!empty($lang)) { + $this->lang[$range] = $lang + $this->lang[$range]; + } + + return $this->lang[$range]; + } + + /** + * 获取语言定义(不区分大小写) + * @access public + * @param string|null $name 语言变量 + * @param string $range 语言作用域 + * @return bool + */ + public function has($name, $range = '') + { + $range = $range ?: $this->range; + + return isset($this->lang[$range][strtolower($name)]); + } + + /** + * 获取语言定义(不区分大小写) + * @access public + * @param string|null $name 语言变量 + * @param array $vars 变量替换 + * @param string $range 语言作用域 + * @return mixed + */ + public function get($name = null, $vars = [], $range = '') + { + $range = $range ?: $this->range; + + // 空参数返回所有定义 + if (is_null($name)) { + return $this->lang[$range]; + } + + $key = strtolower($name); + $value = isset($this->lang[$range][$key]) ? $this->lang[$range][$key] : $name; + + // 变量解析 + if (!empty($vars) && is_array($vars)) { + /** + * Notes: + * 为了检测的方便,数字索引的判断仅仅是参数数组的第一个元素的key为数字0 + * 数字索引采用的是系统的 sprintf 函数替换,用法请参考 sprintf 函数 + */ + if (key($vars) === 0) { + // 数字索引解析 + array_unshift($vars, $value); + $value = call_user_func_array('sprintf', $vars); + } else { + // 关联索引解析 + $replace = array_keys($vars); + foreach ($replace as &$v) { + $v = "{:{$v}}"; + } + $value = str_replace($replace, $vars, $value); + } + } + + return $value; + } + + /** + * 自动侦测设置获取语言选择 + * @access public + * @return string + */ + public function detect() + { + // 自动侦测设置获取语言选择 + $langSet = ''; + + if (isset($_GET[$this->langDetectVar])) { + // url中设置了语言变量 + $langSet = strtolower($_GET[$this->langDetectVar]); + } elseif (isset($_COOKIE[$this->langCookieVar])) { + // Cookie中设置了语言变量 + $langSet = strtolower($_COOKIE[$this->langCookieVar]); + } elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + // 自动侦测浏览器语言 + preg_match('/^([a-z\d\-]+)/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $matches); + $langSet = strtolower($matches[1]); + if (isset($this->acceptLanguage[$langSet])) { + $langSet = $this->acceptLanguage[$langSet]; + } + } + + if (preg_match('/^([a-z\d\-]+)/i', $langSet, $matches)) { + $langSet = strtolower($matches[1]); + } else { + $langSet = $this->range; + } + + if (empty($this->allowLangList) || in_array($langSet, $this->allowLangList)) { + // 合法的语言 + $this->range = $langSet ?: $this->range; + } + + return $this->range; + } + + /** + * 设置当前语言到Cookie + * @access public + * @param string $lang 语言 + * @return void + */ + public function saveToCookie($lang = null) + { + $range = $lang ?: $this->range; + + $_COOKIE[$this->langCookieVar] = $range; + } + + /** + * 设置语言自动侦测的变量 + * @access public + * @param string $var 变量名称 + * @return void + */ + public function setLangDetectVar($var) + { + $this->langDetectVar = $var; + } + + /** + * 设置语言的cookie保存变量 + * @access public + * @param string $var 变量名称 + * @return void + */ + public function setLangCookieVar($var) + { + $this->langCookieVar = $var; + } + + /** + * 设置允许的语言列表 + * @access public + * @param array $list 语言列表 + * @return void + */ + public function setAllowLangList(array $list) + { + $this->allowLangList = $list; + } + + /** + * 设置转义的语言列表 + * @access public + * @param array $list 语言列表 + * @return void + */ + public function setAcceptLanguage(array $list) + { + $this->acceptLanguage = array_merge($this->acceptLanguage, $list); + } +} diff --git a/thinkphp/library/think/Loader.php b/thinkphp/library/think/Loader.php new file mode 100644 index 0000000..d807db6 --- /dev/null +++ b/thinkphp/library/think/Loader.php @@ -0,0 +1,417 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ClassNotFoundException; + +class Loader +{ + /** + * 类名映射信息 + * @var array + */ + protected static $classMap = []; + + /** + * 类库别名 + * @var array + */ + protected static $classAlias = []; + + /** + * PSR-4 + * @var array + */ + private static $prefixLengthsPsr4 = []; + private static $prefixDirsPsr4 = []; + private static $fallbackDirsPsr4 = []; + + /** + * PSR-0 + * @var array + */ + private static $prefixesPsr0 = []; + private static $fallbackDirsPsr0 = []; + + /** + * 需要加载的文件 + * @var array + */ + private static $files = []; + + /** + * Composer安装路径 + * @var string + */ + private static $composerPath; + + // 获取应用根目录 + public static function getRootPath() + { + if ('cli' == PHP_SAPI) { + $scriptName = realpath($_SERVER['argv'][0]); + } else { + $scriptName = $_SERVER['SCRIPT_FILENAME']; + } + + $path = realpath(dirname($scriptName)); + + if (!is_file($path . DIRECTORY_SEPARATOR . 'think')) { + $path = dirname($path); + } + + return $path . DIRECTORY_SEPARATOR; + } + + // 注册自动加载机制 + public static function register($autoload = '') + { + // 注册系统自动加载 + spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true); + + $rootPath = self::getRootPath(); + + self::$composerPath = $rootPath . 'vendor' . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR; + + // Composer自动加载支持 + if (is_dir(self::$composerPath)) { + if (is_file(self::$composerPath . 'autoload_static.php')) { + require self::$composerPath . '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::$composerPath); + } + } + + // 注册命名空间定义 + self::addNamespace([ + 'think' => __DIR__, + 'traits' => dirname(__DIR__) . DIRECTORY_SEPARATOR . 'traits', + ]); + + // 加载类库映射文件 + if (is_file($rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'classmap.php')) { + self::addClassMap(__include_file($rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'classmap.php')); + } + + // 自动加载extend目录 + self::addAutoLoadDir($rootPath . 'extend'); + } + + // 自动加载 + public static function autoload($class) + { + if (isset(self::$classAlias[$class])) { + return class_alias(self::$classAlias[$class], $class); + } + + if ($file = self::findFile($class)) { + + // Win环境严格区分大小写 + if (strpos(PHP_OS, 'WIN') !== false && pathinfo($file, PATHINFO_FILENAME) != pathinfo(realpath($file), PATHINFO_FILENAME)) { + return false; + } + + __include_file($file); + return true; + } + } + + /** + * 查找文件 + * @access private + * @param string $class + * @return string|false + */ + private static function findFile($class) + { + if (!empty(self::$classMap[$class])) { + // 类库映射 + return self::$classMap[$class]; + } + + // 查找 PSR-4 + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . '.php'; + + $first = $class[0]; + if (isset(self::$prefixLengthsPsr4[$first])) { + foreach (self::$prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach (self::$prefixDirsPsr4[$prefix] as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // 查找 PSR-4 fallback dirs + foreach (self::$fallbackDirsPsr4 as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // 查找 PSR-0 + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . '.php'; + } + + if (isset(self::$prefixesPsr0[$first])) { + foreach (self::$prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // 查找 PSR-0 fallback dirs + foreach (self::$fallbackDirsPsr0 as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + return self::$classMap[$class] = false; + } + + // 注册classmap + public static function addClassMap($class, $map = '') + { + if (is_array($class)) { + self::$classMap = array_merge(self::$classMap, $class); + } else { + self::$classMap[$class] = $map; + } + } + + // 注册命名空间 + public static function addNamespace($namespace, $path = '') + { + if (is_array($namespace)) { + foreach ($namespace as $prefix => $paths) { + self::addPsr4($prefix . '\\', rtrim($paths, DIRECTORY_SEPARATOR), true); + } + } else { + self::addPsr4($namespace . '\\', rtrim($path, DIRECTORY_SEPARATOR), true); + } + } + + // 添加Ps0空间 + private static function addPsr0($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + self::$fallbackDirsPsr0 = array_merge( + (array) $paths, + self::$fallbackDirsPsr0 + ); + } else { + self::$fallbackDirsPsr0 = array_merge( + self::$fallbackDirsPsr0, + (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空间 + 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 + ); + } + } 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."); + } + + 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 { + // Append directories for an already registered namespace. + self::$prefixDirsPsr4[$prefix] = array_merge( + self::$prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + // 注册自动加载类库目录 + public static function addAutoLoadDir($path) + { + self::$fallbackDirsPsr4[] = $path; + } + + // 注册类别名 + public static function addClassAlias($alias, $class = null) + { + if (is_array($alias)) { + self::$classAlias = array_merge(self::$classAlias, $alias); + } else { + self::$classAlias[$alias] = $class; + } + } + + // 注册composer自动加载 + public static function registerComposerLoader($composerPath) + { + if (is_file($composerPath . 'autoload_namespaces.php')) { + $map = require $composerPath . 'autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + self::addPsr0($namespace, $path); + } + } + + if (is_file($composerPath . 'autoload_psr4.php')) { + $map = require $composerPath . 'autoload_psr4.php'; + foreach ($map as $namespace => $path) { + self::addPsr4($namespace, $path); + } + } + + if (is_file($composerPath . 'autoload_classmap.php')) { + $classMap = require $composerPath . 'autoload_classmap.php'; + if ($classMap) { + self::addClassMap($classMap); + } + } + + if (is_file($composerPath . 'autoload_files.php')) { + self::$files = require $composerPath . '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; + } + } + } + + /** + * 字符串命名风格转换 + * 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) + { + if ($type) { + $name = preg_replace_callback('/_([a-zA-Z])/', function ($match) { + return strtoupper($match[1]); + }, $name); + return $ucfirst ? ucfirst($name) : lcfirst($name); + } + + return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_")); + } + + /** + * 创建工厂对象实例 + * @access public + * @param string $name 工厂类名 + * @param string $namespace 默认命名空间 + * @return mixed + */ + public static function factory($name, $namespace = '', ...$args) + { + $class = false !== strpos($name, '\\') ? $name : $namespace . ucwords($name); + + if (class_exists($class)) { + return Container::getInstance()->invokeClass($class, $args); + } else { + throw new ClassNotFoundException('class not exists:' . $class, $class); + } + } +} + +/** + * 作用范围隔离 + * + * @param $file + * @return mixed + */ +function __include_file($file) +{ + return include $file; +} + +function __require_file($file) +{ + return require $file; +} diff --git a/thinkphp/library/think/Log.php b/thinkphp/library/think/Log.php new file mode 100644 index 0000000..8902e97 --- /dev/null +++ b/thinkphp/library/think/Log.php @@ -0,0 +1,389 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Log implements LoggerInterface +{ + const EMERGENCY = 'emergency'; + const ALERT = 'alert'; + const CRITICAL = 'critical'; + const ERROR = 'error'; + const WARNING = 'warning'; + const NOTICE = 'notice'; + const INFO = 'info'; + const DEBUG = 'debug'; + const SQL = 'sql'; + + /** + * 日志信息 + * @var array + */ + protected $log = []; + + /** + * 配置参数 + * @var array + */ + protected $config = []; + + /** + * 日志写入驱动 + * @var object + */ + protected $driver; + + /** + * 日志授权key + * @var string + */ + protected $key; + + /** + * 是否允许日志写入 + * @var bool + */ + protected $allowWrite = true; + + /** + * 应用对象 + * @var App + */ + protected $app; + + public function __construct(App $app) + { + $this->app = $app; + } + + public static function __make(App $app, Config $config) + { + return (new static($app))->init($config->pull('log')); + } + + /** + * 日志初始化 + * @access public + * @param array $config + * @return $this + */ + public function init($config = []) + { + $type = isset($config['type']) ? $config['type'] : 'File'; + + $this->config = $config; + + unset($config['type']); + + if (!empty($config['close'])) { + $this->allowWrite = false; + } + + $this->driver = Loader::factory($type, '\\think\\log\\driver\\', $config); + + return $this; + } + + /** + * 获取日志信息 + * @access public + * @param string $type 信息类型 + * @return array + */ + public function getLog($type = '') + { + return $type ? $this->log[$type] : $this->log; + } + + /** + * 记录日志信息 + * @access public + * @param mixed $msg 日志信息 + * @param string $type 日志级别 + * @param array $context 替换内容 + * @return $this + */ + public function record($msg, $type = 'info', array $context = []) + { + if (!$this->allowWrite) { + return; + } + + if (is_string($msg) && !empty($context)) { + $replace = []; + foreach ($context as $key => $val) { + $replace['{' . $key . '}'] = $val; + } + + $msg = strtr($msg, $replace); + } + + if (PHP_SAPI == 'cli') { + if (empty($this->config['level']) || in_array($type, $this->config['level'])) { + // 命令行日志实时写入 + $this->write($msg, $type, true); + } + } else { + $this->log[$type][] = $msg; + } + + return $this; + } + + /** + * 清空日志信息 + * @access public + * @return $this + */ + public function clear() + { + $this->log = []; + + return $this; + } + + /** + * 当前日志记录的授权key + * @access public + * @param string $key 授权key + * @return $this + */ + public function key($key) + { + $this->key = $key; + + return $this; + } + + /** + * 检查日志写入权限 + * @access public + * @param array $config 当前日志配置参数 + * @return bool + */ + public function check($config) + { + if ($this->key && !empty($config['allow_key']) && !in_array($this->key, $config['allow_key'])) { + return false; + } + + return true; + } + + /** + * 关闭本次请求日志写入 + * @access public + * @return $this + */ + public function close() + { + $this->allowWrite = false; + $this->log = []; + + return $this; + } + + /** + * 保存调试信息 + * @access public + * @return bool + */ + public function save() + { + if (empty($this->log) || !$this->allowWrite) { + return true; + } + + if (!$this->check($this->config)) { + // 检测日志写入权限 + return false; + } + + $log = []; + + foreach ($this->log as $level => $info) { + if (!$this->app->isDebug() && 'debug' == $level) { + continue; + } + + if (empty($this->config['level']) || in_array($level, $this->config['level'])) { + $log[$level] = $info; + + $this->app['hook']->listen('log_level', [$level, $info]); + } + } + + $result = $this->driver->save($log, true); + + if ($result) { + $this->log = []; + } + + return $result; + } + + /** + * 实时写入日志信息 并支持行为 + * @access public + * @param mixed $msg 调试信息 + * @param string $type 日志级别 + * @param bool $force 是否强制写入 + * @return bool + */ + public function write($msg, $type = 'info', $force = false) + { + // 封装日志信息 + if (empty($this->config['level'])) { + $force = true; + } + + if (true === $force || in_array($type, $this->config['level'])) { + $log[$type][] = $msg; + } else { + return false; + } + + // 监听log_write + $this->app['hook']->listen('log_write', $log); + + // 写入日志 + return $this->driver->save($log, false); + } + + /** + * 记录日志信息 + * @access public + * @param string $level 日志级别 + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function log($level, $message, array $context = []) + { + $this->record($message, $level, $context); + } + + /** + * 记录emergency信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function emergency($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录警报信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function alert($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录紧急情况 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function critical($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录错误信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function error($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录warning信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function warning($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录notice信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function notice($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录一般信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function info($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录调试信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function debug($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录sql信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function sql($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app']); + + return $data; + } +} diff --git a/thinkphp/library/think/Middleware.php b/thinkphp/library/think/Middleware.php new file mode 100644 index 0000000..d3f4360 --- /dev/null +++ b/thinkphp/library/think/Middleware.php @@ -0,0 +1,205 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use InvalidArgumentException; +use LogicException; +use think\exception\HttpResponseException; + +class Middleware +{ + protected $queue = []; + protected $app; + protected $config = [ + 'default_namespace' => 'app\\http\\middleware\\', + ]; + + public function __construct(App $app, array $config = []) + { + $this->app = $app; + $this->config = array_merge($this->config, $config); + } + + public static function __make(App $app, Config $config) + { + return new static($app, $config->pull('middleware')); + } + + public function setConfig(array $config) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 导入中间件 + * @access public + * @param array $middlewares + * @param string $type 中间件类型 + */ + public function import(array $middlewares = [], $type = 'route') + { + foreach ($middlewares as $middleware) { + $this->add($middleware, $type); + } + } + + /** + * 注册中间件 + * @access public + * @param mixed $middleware + * @param string $type 中间件类型 + */ + public function add($middleware, $type = 'route') + { + if (is_null($middleware)) { + return; + } + + $middleware = $this->buildMiddleware($middleware, $type); + + if ($middleware) { + $this->queue[$type][] = $middleware; + } + } + + /** + * 注册控制器中间件 + * @access public + * @param mixed $middleware + */ + public function controller($middleware) + { + return $this->add($middleware, 'controller'); + } + + /** + * 移除中间件 + * @access public + * @param mixed $middleware + * @param string $type 中间件类型 + */ + public function unshift($middleware, $type = 'route') + { + if (is_null($middleware)) { + return; + } + + $middleware = $this->buildMiddleware($middleware, $type); + + if ($middleware) { + array_unshift($this->queue[$type], $middleware); + } + } + + /** + * 获取注册的中间件 + * @access public + * @param string $type 中间件类型 + */ + public function all($type = 'route') + { + return $this->queue[$type] ?: []; + } + + /** + * 清除中间件 + * @access public + */ + public function clear() + { + $this->queue = []; + } + + /** + * 中间件调度 + * @access public + * @param Request $request + * @param string $type 中间件类型 + */ + public function dispatch(Request $request, $type = 'route') + { + return call_user_func($this->resolve($type), $request); + } + + /** + * 解析中间件 + * @access protected + * @param mixed $middleware + * @param string $type 中间件类型 + */ + protected function buildMiddleware($middleware, $type = 'route') + { + if (is_array($middleware)) { + list($middleware, $param) = $middleware; + } + + if ($middleware instanceof \Closure) { + return [$middleware, isset($param) ? $param : null]; + } + + if (!is_string($middleware)) { + throw new InvalidArgumentException('The middleware is invalid'); + } + + if (false === strpos($middleware, '\\')) { + if (isset($this->config[$middleware])) { + $middleware = $this->config[$middleware]; + } else { + $middleware = $this->config['default_namespace'] . $middleware; + } + } + + if (is_array($middleware)) { + return $this->import($middleware, $type); + } + + if (strpos($middleware, ':')) { + list($middleware, $param) = explode(':', $middleware, 2); + } + + return [[$this->app->make($middleware), 'handle'], isset($param) ? $param : null]; + } + + protected function resolve($type = 'route') + { + return function (Request $request) use ($type) { + + $middleware = array_shift($this->queue[$type]); + + if (null === $middleware) { + throw new InvalidArgumentException('The queue was exhausted, with no response returned'); + } + + list($call, $param) = $middleware; + + try { + $response = call_user_func_array($call, [$request, $this->resolve($type), $param]); + } catch (HttpResponseException $exception) { + $response = $exception->getResponse(); + } + + if (!$response instanceof Response) { + throw new LogicException('The middleware must return Response instance'); + } + + return $response; + }; + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app']); + + return $data; + } +} diff --git a/thinkphp/library/think/Model.php b/thinkphp/library/think/Model.php new file mode 100644 index 0000000..50f2ca1 --- /dev/null +++ b/thinkphp/library/think/Model.php @@ -0,0 +1,1125 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use InvalidArgumentException; +use think\db\Query; + +/** + * Class Model + * @package think + * @mixin Query + * @method $this scope(string|array $scope) static 查询范围 + * @method $this where(mixed $field, string $op = null, mixed $condition = null) static 查询条件 + * @method $this whereRaw(string $where, array $bind = [], string $logic = 'AND') static 表达式查询 + * @method $this whereExp(string $field, string $condition, array $bind = [], string $logic = 'AND') static 字段表达式查询 + * @method $this when(mixed $condition, mixed $query, mixed $otherwise = null) static 条件查询 + * @method $this join(mixed $join, mixed $condition = null, string $type = 'INNER', array $bind = []) static JOIN查询 + * @method $this view(mixed $join, mixed $field = null, mixed $on = null, string $type = 'INNER') static 视图查询 + * @method $this with(mixed $with, callable $callback = null) static 关联预载入 + * @method $this count(string $field = '*') static Count统计查询 + * @method $this min(string $field, bool $force = true) static Min统计查询 + * @method $this max(string $field, bool $force = true) static Max统计查询 + * @method $this sum(string $field) static SUM统计查询 + * @method $this avg(string $field) static Avg统计查询 + * @method $this field(mixed $field, boolean $except = false, string $tableName = '', string $prefix = '', string $alias = '') static 指定查询字段 + * @method $this fieldRaw(string $field) static 指定查询字段 + * @method $this union(mixed $union, boolean $all = false) static UNION查询 + * @method $this limit(mixed $offset, integer $length = null) static 查询LIMIT + * @method $this order(mixed $field, string $order = null) static 查询ORDER + * @method $this orderRaw(string $field, array $bind = []) static 查询ORDER + * @method $this cache(mixed $key = null, integer|\DateTime $expire = null, string $tag = null) static 设置查询缓存 + * @method mixed value(string $field, mixed $default = null) static 获取某个字段的值 + * @method array column(string $field, string $key = '') static 获取某个列的值 + * @method $this find(mixed $data = null) static 查询单个记录 + * @method $this findOrFail(mixed $data = null) 查询单个记录 + * @method Collection|$this[] select(mixed $data = null) static 查询多个记录 + * @method $this get(mixed $data = null, mixed $with = [], bool $cache = false, bool $failException = false) static 查询单个记录 支持关联预载入 + * @method $this getOrFail(mixed $data = null, mixed $with = [], bool $cache = false) static 查询单个记录 不存在则抛出异常 + * @method $this findOrEmpty(mixed $data = null) static 查询单个记录 不存在则返回空模型 + * @method Collection|$this[] all(mixed $data = null, mixed $with = [], bool $cache = false) static 查询多个记录 支持关联预载入 + * @method $this withAttr(array $name, \Closure $closure = null) static 动态定义获取器 + * @method $this withJoin(string|array $with, string $joinType = '') static + * @method $this withCount(string|array $relation, bool $subQuery = true) static 关联统计 + * @method $this withSum(string|array $relation, string $field, bool $subQuery = true) static 关联SUM统计 + * @method $this withMax(string|array $relation, string $field, bool $subQuery = true) static 关联MAX统计 + * @method $this withMin(string|array $relation, string $field, bool $subQuery = true) static 关联Min统计 + * @method $this withAvg(string|array $relation, string $field, bool $subQuery = true) static 关联Avg统计 + * @method Paginator|$this paginate(int|array $listRows = null, int|bool $simple = false, array $config = []) static 分页 + */ +abstract class Model implements \JsonSerializable, \ArrayAccess +{ + use model\concern\Attribute; + use model\concern\RelationShip; + use model\concern\ModelEvent; + use model\concern\TimeStamp; + use model\concern\Conversion; + + /** + * 是否存在数据 + * @var bool + */ + private $exists = false; + + /** + * 是否Replace + * @var bool + */ + private $replace = false; + + /** + * 是否强制更新所有数据 + * @var bool + */ + private $force = false; + + /** + * 更新条件 + * @var array + */ + private $updateWhere; + + /** + * 数据库配置信息 + * @var array|string + */ + protected $connection = []; + + /** + * 数据库查询对象类名 + * @var string + */ + protected $query; + + /** + * 模型名称 + * @var string + */ + protected $name; + + /** + * 数据表名称 + * @var string + */ + protected $table; + + /** + * 写入自动完成定义 + * @var array + */ + protected $auto = []; + + /** + * 新增自动完成定义 + * @var array + */ + protected $insert = []; + + /** + * 更新自动完成定义 + * @var array + */ + protected $update = []; + + /** + * 初始化过的模型. + * @var array + */ + protected static $initialized = []; + + /** + * 是否从主库读取(主从分布式有效) + * @var array + */ + protected static $readMaster; + + /** + * 查询对象实例 + * @var Query + */ + protected $queryInstance; + + /** + * 错误信息 + * @var mixed + */ + protected $error; + + /** + * 软删除字段默认值 + * @var mixed + */ + protected $defaultSoftDelete; + + /** + * 全局查询范围 + * @var array + */ + protected $globalScope = []; + + /** + * 架构函数 + * @access public + * @param array|object $data 数据 + */ + public function __construct($data = []) + { + if (is_object($data)) { + $this->data = get_object_vars($data); + } 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; + + $config = Db::getConfig(); + + if (empty($this->name)) { + // 当前模型名 + $name = str_replace('\\', '/', static::class); + $this->name = basename($name); + if (Container::get('config')->get('class_suffix')) { + $suffix = basename(dirname($name)); + $this->name = substr($this->name, 0, -strlen($suffix)); + } + } + + if (is_null($this->autoWriteTimestamp)) { + // 自动写入时间戳 + $this->autoWriteTimestamp = $config['auto_timestamp']; + } + + if (is_null($this->dateFormat)) { + // 设置时间戳格式 + $this->dateFormat = $config['datetime_format']; + } + + if (is_null($this->resultSetType)) { + $this->resultSetType = $config['resultset_type']; + } + + if (!empty($this->connection) && is_array($this->connection)) { + // 设置模型的数据库连接 + $this->connection = array_merge($config, $this->connection); + } + + if ($this->observerClass) { + // 注册模型观察者 + static::observe($this->observerClass); + } + + // 执行初始化操作 + $this->initialize(); + } + + /** + * 获取当前模型名称 + * @access public + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 是否从主库读取数据(主从分布有效) + * @access public + * @param bool $all 是否所有模型有效 + * @return $this + */ + public function readMaster($all = false) + { + $model = $all ? '*' : static::class; + + static::$readMaster[$model] = true; + + return $this; + } + + /** + * 创建新的模型实例 + * @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 protected + * @return Query + */ + protected function buildQuery() + { + // 设置当前模型 确保查询返回模型对象 + $query = Db::connect($this->connection, false, $this->query); + $query->model($this) + ->name($this->name) + ->json($this->json, $this->jsonAssoc) + ->setJsonFieldType($this->jsonType); + + if (isset(static::$readMaster['*']) || isset(static::$readMaster[static::class])) { + $query->master(true); + } + + // 设置当前数据表和模型名 + if (!empty($this->table)) { + $query->table($this->table); + } + + if (!empty($this->pk)) { + $query->pk($this->pk); + } + + return $query; + } + + /** + * 获取当前模型的数据库查询对象 + * @access public + * @param Query $query 查询对象实例 + * @return $this + */ + public function setQuery($query) + { + $this->queryInstance = $query; + return $this; + } + + /** + * 获取当前模型的数据库查询对象 + * @access public + * @param bool|array $useBaseQuery 是否调用全局查询范围(或者指定查询范围名称) + * @return Query + */ + public function db($useBaseQuery = true) + { + if ($this->queryInstance) { + return $this->queryInstance; + } + + $query = $this->buildQuery(); + + // 软删除 + if (property_exists($this, 'withTrashed') && !$this->withTrashed) { + $this->withNoTrashed($query); + } + + // 全局作用域 + if (true === $useBaseQuery && method_exists($this, 'base')) { + call_user_func_array([$this, 'base'], [ & $query]); + } + + $globalScope = is_array($useBaseQuery) && $useBaseQuery ? $useBaseQuery : $this->globalScope; + + if ($globalScope && false !== $useBaseQuery) { + $query->scope($globalScope); + } + + // 返回当前模型的数据库查询对象 + return $query; + } + + /** + * 初始化模型 + * @access protected + * @return void + */ + protected function initialize() + { + if (!isset(static::$initialized[static::class])) { + static::$initialized[static::class] = true; + static::init(); + } + } + + /** + * 初始化处理 + * @access protected + * @return void + */ + protected static function init() + {} + + /** + * 数据自动完成 + * @access protected + * @param array $auto 要自动更新的字段列表 + * @return void + */ + protected function autoCompleteData($auto = []) + { + foreach ($auto as $field => $value) { + if (is_integer($field)) { + $field = $value; + $value = null; + } + + if (!isset($this->data[$field])) { + $default = null; + } else { + $default = $this->data[$field]; + } + + $this->setAttr($field, !is_null($value) ? $value : $default); + } + } + + /** + * 更新是否强制写入数据 而不做比较 + * @access public + * @param bool $force + * @return $this + */ + public function force($force = true) + { + $this->force = $force; + return $this; + } + + /** + * 判断force + * @access public + * @return bool + */ + public function isForce() + { + return $this->force; + } + + /** + * 新增数据是否使用Replace + * @access public + * @param bool $replace + * @return $this + */ + public function replace($replace = true) + { + $this->replace = $replace; + return $this; + } + + /** + * 设置数据是否存在 + * @access public + * @param bool $exists + * @return $this + */ + public function exists($exists) + { + $this->exists = $exists; + return $this; + } + + /** + * 判断数据是否存在数据库 + * @access public + * @return bool + */ + public function isExists() + { + return $this->exists; + } + + /** + * 判断模型是否为空 + * @access public + * @return bool + */ + public function isEmpty() + { + return empty($this->data); + } + + /** + * 保存当前数据对象 + * @access public + * @param array $data 数据 + * @param array $where 更新条件 + * @param string $sequence 自增序列名 + * @return bool + */ + public function save($data = [], $where = [], $sequence = null) + { + if (is_string($data)) { + $sequence = $data; + $data = []; + } + + if (!$this->checkBeforeSave($data, $where)) { + return false; + } + + $result = $this->exists ? $this->updateData($where) : $this->insertData($sequence); + + if (false === $result) { + return false; + } + + // 写入回调 + $this->trigger('after_write'); + + // 重新记录原始数据 + $this->origin = $this->data; + $this->set = []; + + return true; + } + + /** + * 写入之前检查数据 + * @access protected + * @param array $data 数据 + * @param array $where 保存条件 + * @return bool + */ + protected function checkBeforeSave($data, $where) + { + if (!empty($data)) { + // 数据对象赋值 + foreach ($data as $key => $value) { + $this->setAttr($key, $value, $data); + } + + if (!empty($where)) { + $this->exists = true; + $this->updateWhere = $where; + } + } + + // 数据自动完成 + $this->autoCompleteData($this->auto); + + // 事件回调 + if (false === $this->trigger('before_write')) { + return false; + } + + return true; + } + + /** + * 检查数据是否允许写入 + * @access protected + * @param array $append 自动完成的字段列表 + * @return array + */ + protected function checkAllowFields(array $append = []) + { + // 检测字段 + if (empty($this->field) || true === $this->field) { + $query = $this->db(false); + $table = $this->table ?: $query->getTable(); + + $this->field = $query->getConnection()->getTableFields($table); + + $field = $this->field; + } else { + $field = array_merge($this->field, $append); + + if ($this->autoWriteTimestamp) { + array_push($field, $this->createTime, $this->updateTime); + } + } + + if ($this->disuse) { + // 废弃字段 + $field = array_diff($field, (array) $this->disuse); + } + + return $field; + } + + /** + * 更新写入数据 + * @access protected + * @param mixed $where 更新条件 + * @return bool + */ + protected function updateData($where) + { + // 自动更新 + $this->autoCompleteData($this->update); + + // 事件回调 + if (false === $this->trigger('before_update')) { + return false; + } + + // 获取有更新的数据 + $data = $this->getChangedData(); + + if (empty($data)) { + // 关联更新 + if (!empty($this->relationWrite)) { + $this->autoRelationUpdate(); + } + + return true; + } elseif ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) { + // 自动写入更新时间 + $data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime); + + $this->data[$this->updateTime] = $data[$this->updateTime]; + } + + if (empty($where) && !empty($this->updateWhere)) { + $where = $this->updateWhere; + } + + // 检查允许字段 + $allowFields = $this->checkAllowFields(array_merge($this->auto, $this->update)); + + // 保留主键数据 + foreach ($this->data as $key => $val) { + if ($this->isPk($key)) { + $data[$key] = $val; + } + } + + $pk = $this->getPk(); + $array = []; + + foreach ((array) $pk as $key) { + if (isset($data[$key])) { + $array[] = [$key, '=', $data[$key]]; + unset($data[$key]); + } + } + + if (!empty($array)) { + $where = $array; + } + + foreach ((array) $this->relationWrite as $name => $val) { + if (is_array($val)) { + foreach ($val as $key) { + if (isset($data[$key])) { + unset($data[$key]); + } + } + } + } + + // 模型更新 + $db = $this->db(false); + $db->startTrans(); + + try { + $db->where($where) + ->strict(false) + ->field($allowFields) + ->update($data); + + // 关联更新 + if (!empty($this->relationWrite)) { + $this->autoRelationUpdate(); + } + + $db->commit(); + + // 更新回调 + $this->trigger('after_update'); + + return true; + } catch (\Exception $e) { + $db->rollback(); + throw $e; + } + } + + /** + * 新增写入数据 + * @access protected + * @param string $sequence 自增序列名 + * @return bool + */ + protected function insertData($sequence) + { + // 自动写入 + $this->autoCompleteData($this->insert); + + // 时间戳自动写入 + $this->checkTimeStampWrite(); + + if (false === $this->trigger('before_insert')) { + return false; + } + + // 检查允许字段 + $allowFields = $this->checkAllowFields(array_merge($this->auto, $this->insert)); + + $db = $this->db(false); + $db->startTrans(); + + try { + $result = $db->strict(false) + ->field($allowFields) + ->insert($this->data, $this->replace, false, $sequence); + + // 获取自动增长主键 + if ($result && $insertId = $db->getLastInsID($sequence)) { + $pk = $this->getPk(); + + foreach ((array) $pk as $key) { + if (!isset($this->data[$key]) || '' == $this->data[$key]) { + $this->data[$key] = $insertId; + } + } + } + + // 关联写入 + if (!empty($this->relationWrite)) { + $this->autoRelationInsert(); + } + + $db->commit(); + + // 标记为更新 + $this->exists = true; + + // 新增回调 + $this->trigger('after_insert'); + + return true; + } catch (\Exception $e) { + $db->rollback(); + throw $e; + } + } + + /** + * 字段值(延迟)增长 + * @access public + * @param string $field 字段名 + * @param integer $step 增长值 + * @param integer $lazyTime 延时时间(s) + * @return bool + * @throws Exception + */ + public function setInc($field, $step = 1, $lazyTime = 0) + { + // 读取更新条件 + $where = $this->getWhere(); + + // 事件回调 + if (false === $this->trigger('before_update')) { + return false; + } + + $result = $this->db(false) + ->where($where) + ->setInc($field, $step, $lazyTime); + + if (true !== $result) { + $this->data[$field] += $step; + } + + // 更新回调 + $this->trigger('after_update'); + + return true; + } + + /** + * 字段值(延迟)减少 + * @access public + * @param string $field 字段名 + * @param integer $step 减少值 + * @param integer $lazyTime 延时时间(s) + * @return bool + * @throws Exception + */ + public function setDec($field, $step = 1, $lazyTime = 0) + { + // 读取更新条件 + $where = $this->getWhere(); + + // 事件回调 + if (false === $this->trigger('before_update')) { + return false; + } + + $result = $this->db(false) + ->where($where) + ->setDec($field, $step, $lazyTime); + + if (true !== $result) { + $this->data[$field] -= $step; + } + + // 更新回调 + $this->trigger('after_update'); + + return true; + } + + /** + * 获取当前的更新条件 + * @access protected + * @return mixed + */ + protected function getWhere() + { + // 删除条件 + $pk = $this->getPk(); + + $where = []; + if (is_string($pk) && isset($this->data[$pk])) { + $where[] = [$pk, '=', $this->data[$pk]]; + } elseif (is_array($pk)) { + foreach ($pk as $field) { + if (isset($this->data[$field])) { + $where[] = [$field, '=', $this->data[$field]]; + } + } + } + + if (empty($where)) { + $where = empty($this->updateWhere) ? null : $this->updateWhere; + } + + return $where; + } + + /** + * 保存多个数据到当前数据对象 + * @access public + * @param array $dataSet 数据 + * @param boolean $replace 是否自动识别更新和写入 + * @return Collection + * @throws \Exception + */ + public function saveAll($dataSet, $replace = true) + { + $db = $this->db(false); + $db->startTrans(); + + try { + $pk = $this->getPk(); + + if (is_string($pk) && $replace) { + $auto = true; + } + + $result = []; + + foreach ($dataSet as $key => $data) { + if ($this->exists || (!empty($auto) && isset($data[$pk]))) { + $result[$key] = self::update($data, [], $this->field); + } else { + $result[$key] = self::create($data, $this->field, $this->replace); + } + } + + $db->commit(); + + return $this->toCollection($result); + } catch (\Exception $e) { + $db->rollback(); + throw $e; + } + } + + /** + * 是否为更新数据 + * @access public + * @param mixed $update + * @param mixed $where + * @return $this + */ + public function isUpdate($update = true, $where = null) + { + if (is_bool($update)) { + $this->exists = $update; + + if (!empty($where)) { + $this->updateWhere = $where; + } + } else { + $this->exists = true; + $this->updateWhere = $update; + } + + return $this; + } + + /** + * 删除当前的记录 + * @access public + * @return bool + */ + public function delete() + { + if (!$this->exists || false === $this->trigger('before_delete')) { + return false; + } + + // 读取更新条件 + $where = $this->getWhere(); + + $db = $this->db(false); + $db->startTrans(); + + try { + // 删除当前模型数据 + $db->where($where)->delete(); + + // 关联删除 + if (!empty($this->relationWrite)) { + $this->autoRelationDelete(); + } + + $db->commit(); + + $this->trigger('after_delete'); + + $this->exists = false; + + return true; + } catch (\Exception $e) { + $db->rollback(); + throw $e; + } + } + + /** + * 设置自动完成的字段( 规则通过修改器定义) + * @access public + * @param array $fields 需要自动完成的字段 + * @return $this + */ + public function auto($fields) + { + $this->auto = $fields; + + return $this; + } + + /** + * 写入数据 + * @access public + * @param array $data 数据数组 + * @param array|true $field 允许字段 + * @param bool $replace 使用Replace + * @return static + */ + public static function create($data = [], $field = null, $replace = false) + { + $model = new static(); + + if (!empty($field)) { + $model->allowField($field); + } + + $model->isUpdate(false)->replace($replace)->save($data, []); + + return $model; + } + + /** + * 更新数据 + * @access public + * @param array $data 数据数组 + * @param array $where 更新条件 + * @param array|true $field 允许字段 + * @return static + */ + public static function update($data = [], $where = [], $field = null) + { + $model = new static(); + + if (!empty($field)) { + $model->allowField($field); + } + + $model->isUpdate(true)->save($data, $where); + + return $model; + } + + /** + * 删除记录 + * @access public + * @param mixed $data 主键列表 支持闭包查询条件 + * @return bool + */ + public static function destroy($data) + { + if (empty($data) && 0 !== $data) { + return false; + } + + $model = new static(); + + $query = $model->db(); + + if (is_array($data) && key($data) !== 0) { + $query->where($data); + $data = null; + } elseif ($data instanceof \Closure) { + $data($query); + $data = null; + } + + $resultSet = $query->select($data); + + if ($resultSet) { + foreach ($resultSet as $data) { + $data->delete(); + } + } + + return true; + } + + /** + * 获取错误信息 + * @access public + * @return mixed + */ + public function getError() + { + return $this->error; + } + + /** + * 解序列化后处理 + */ + public function __wakeup() + { + $this->initialize(); + } + + public function __debugInfo() + { + return [ + 'data' => $this->data, + 'relation' => $this->relation, + ]; + } + + /** + * 修改器 设置数据对象的值 + * @access public + * @param string $name 名称 + * @param mixed $value 值 + * @return void + */ + public function __set($name, $value) + { + $this->setAttr($name, $value); + } + + /** + * 获取器 获取数据对象的值 + * @access public + * @param string $name 名称 + * @return mixed + */ + public function __get($name) + { + return $this->getAttr($name); + } + + /** + * 检测数据对象的值 + * @access public + * @param string $name 名称 + * @return boolean + */ + public function __isset($name) + { + try { + return !is_null($this->getAttr($name)); + } catch (InvalidArgumentException $e) { + return false; + } + } + + /** + * 销毁数据对象的值 + * @access public + * @param string $name 名称 + * @return void + */ + public function __unset($name) + { + unset($this->data[$name], $this->relation[$name]); + } + + // ArrayAccess + public function offsetSet($name, $value) + { + $this->setAttr($name, $value); + } + + public function offsetExists($name) + { + return $this->__isset($name); + } + + public function offsetUnset($name) + { + $this->__unset($name); + } + + public function offsetGet($name) + { + return $this->getAttr($name); + } + + /** + * 设置是否使用全局查询范围 + * @access public + * @param bool|array $use 是否启用全局查询范围(或者用数组指定查询范围名称) + * @return Query + */ + public static function useGlobalScope($use) + { + $model = new static(); + + return $model->db($use); + } + + public function __call($method, $args) + { + if ('withattr' == strtolower($method)) { + return call_user_func_array([$this, 'withAttribute'], $args); + } + + return call_user_func_array([$this->db(), $method], $args); + } + + public static function __callStatic($method, $args) + { + $model = new static(); + + return call_user_func_array([$model->db(), $method], $args); + } +} diff --git a/thinkphp/library/think/Paginator.php b/thinkphp/library/think/Paginator.php new file mode 100644 index 0000000..bbe63e2 --- /dev/null +++ b/thinkphp/library/think/Paginator.php @@ -0,0 +1,445 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use ArrayAccess; +use ArrayIterator; +use Countable; +use IteratorAggregate; +use JsonSerializable; +use Traversable; + +abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable +{ + /** + * 是否简洁模式 + * @var bool + */ + protected $simple = false; + + /** + * 数据集 + * @var Collection + */ + protected $items; + + /** + * 当前页 + * @var integer + */ + protected $currentPage; + + /** + * 最后一页 + * @var integer + */ + protected $lastPage; + + /** + * 数据总数 + * @var integer|null + */ + protected $total; + + /** + * 每页数量 + * @var integer + */ + protected $listRows; + + /** + * 是否有下一页 + * @var bool + */ + protected $hasMore; + + /** + * 分页配置 + * @var array + */ + protected $options = [ + 'var_page' => 'page', + 'path' => '/', + 'query' => [], + 'fragment' => '', + ]; + + public function __construct($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = []) + { + $this->options = array_merge($this->options, $options); + + $this->options['path'] = '/' != $this->options['path'] ? rtrim($this->options['path'], '/') : $this->options['path']; + + $this->simple = $simple; + $this->listRows = $listRows; + + if (!$items instanceof Collection) { + $items = Collection::make($items); + } + + if ($simple) { + $this->currentPage = $this->setCurrentPage($currentPage); + $this->hasMore = count($items) > ($this->listRows); + $items = $items->slice(0, $this->listRows); + } else { + $this->total = $total; + $this->lastPage = (int) ceil($total / $listRows); + $this->currentPage = $this->setCurrentPage($currentPage); + $this->hasMore = $this->currentPage < $this->lastPage; + } + $this->items = $items; + } + + /** + * @access public + * @param $items + * @param $listRows + * @param null $currentPage + * @param null $total + * @param bool $simple + * @param array $options + * @return Paginator + */ + public static function make($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = []) + { + return new static($items, $listRows, $currentPage, $total, $simple, $options); + } + + protected function setCurrentPage($currentPage) + { + if (!$this->simple && $currentPage > $this->lastPage) { + return $this->lastPage > 0 ? $this->lastPage : 1; + } + + return $currentPage; + } + + /** + * 获取页码对应的链接 + * + * @access protected + * @param $page + * @return string + */ + protected function url($page) + { + if ($page <= 0) { + $page = 1; + } + + if (strpos($this->options['path'], '[PAGE]') === false) { + $parameters = [$this->options['var_page'] => $page]; + $path = $this->options['path']; + } else { + $parameters = []; + $path = str_replace('[PAGE]', $page, $this->options['path']); + } + + if (count($this->options['query']) > 0) { + $parameters = array_merge($this->options['query'], $parameters); + } + + $url = $path; + if (!empty($parameters)) { + $url .= '?' . http_build_query($parameters, null, '&'); + } + + return $url . $this->buildFragment(); + } + + /** + * 自动获取当前页码 + * @access public + * @param string $varPage + * @param int $default + * @return int + */ + public static function getCurrentPage($varPage = 'page', $default = 1) + { + $page = Container::get('request')->param($varPage); + + if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) { + return $page; + } + + return $default; + } + + /** + * 自动获取当前的path + * @access public + * @return string + */ + public static function getCurrentPath() + { + return Container::get('request')->baseUrl(); + } + + public function total() + { + if ($this->simple) { + throw new \DomainException('not support total'); + } + + return $this->total; + } + + public function listRows() + { + return $this->listRows; + } + + public function currentPage() + { + return $this->currentPage; + } + + public function lastPage() + { + if ($this->simple) { + throw new \DomainException('not support last'); + } + + return $this->lastPage; + } + + /** + * 数据是否足够分页 + * @access public + * @return boolean + */ + public function hasPages() + { + return !(1 == $this->currentPage && !$this->hasMore); + } + + /** + * 创建一组分页链接 + * + * @access public + * @param int $start + * @param int $end + * @return array + */ + public function getUrlRange($start, $end) + { + $urls = []; + + for ($page = $start; $page <= $end; $page++) { + $urls[$page] = $this->url($page); + } + + return $urls; + } + + /** + * 设置URL锚点 + * + * @access public + * @param string|null $fragment + * @return $this + */ + public function fragment($fragment) + { + $this->options['fragment'] = $fragment; + + return $this; + } + + /** + * 添加URL参数 + * + * @access public + * @param array|string $key + * @param string|null $value + * @return $this + */ + public function appends($key, $value = null) + { + if (!is_array($key)) { + $queries = [$key => $value]; + } else { + $queries = $key; + } + + foreach ($queries as $k => $v) { + if ($k !== $this->options['var_page']) { + $this->options['query'][$k] = $v; + } + } + + return $this; + } + + /** + * 构造锚点字符串 + * + * @access public + * @return string + */ + protected function buildFragment() + { + return $this->options['fragment'] ? '#' . $this->options['fragment'] : ''; + } + + /** + * 渲染分页html + * @access public + * @return mixed + */ + abstract public function render(); + + public function items() + { + return $this->items->all(); + } + + public function getCollection() + { + return $this->items; + } + + public function isEmpty() + { + return $this->items->isEmpty(); + } + + /** + * 给每个元素执行个回调 + * + * @access public + * @param callable $callback + * @return $this + */ + public function each(callable $callback) + { + foreach ($this->items as $key => $item) { + $result = $callback($item, $key); + + if (false === $result) { + break; + } elseif (!is_object($item)) { + $this->items[$key] = $result; + } + } + + return $this; + } + + /** + * Retrieve an external iterator + * @access public + * @return Traversable An instance of an object implementing Iterator or + * Traversable + */ + public function getIterator() + { + return new ArrayIterator($this->items->all()); + } + + /** + * Whether a offset exists + * @access public + * @param mixed $offset + * @return bool + */ + public function offsetExists($offset) + { + return $this->items->offsetExists($offset); + } + + /** + * Offset to retrieve + * @access public + * @param mixed $offset + * @return mixed + */ + public function offsetGet($offset) + { + return $this->items->offsetGet($offset); + } + + /** + * Offset to set + * @access public + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet($offset, $value) + { + $this->items->offsetSet($offset, $value); + } + + /** + * Offset to unset + * @access public + * @param mixed $offset + * @return void + * @since 5.0.0 + */ + public function offsetUnset($offset) + { + $this->items->offsetUnset($offset); + } + + /** + * Count elements of an object + */ + public function count() + { + return $this->items->count(); + } + + public function __toString() + { + return (string) $this->render(); + } + + public function toArray() + { + try { + $total = $this->total(); + } catch (\DomainException $e) { + $total = null; + } + + return [ + 'total' => $total, + 'per_page' => $this->listRows(), + 'current_page' => $this->currentPage(), + 'last_page' => $this->lastPage, + 'data' => $this->items->toArray(), + ]; + } + + /** + * Specify data which should be serialized to JSON + */ + public function jsonSerialize() + { + return $this->toArray(); + } + + public function __call($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/Process.php b/thinkphp/library/think/Process.php new file mode 100644 index 0000000..3b574db --- /dev/null +++ b/thinkphp/library/think/Process.php @@ -0,0 +1,1268 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\process\exception\Failed as ProcessFailedException; +use think\process\exception\Timeout as ProcessTimeoutException; +use think\process\pipes\Pipes; +use think\process\pipes\Unix as UnixPipes; +use think\process\pipes\Windows as WindowsPipes; +use think\process\Utils; + +class Process +{ + + const ERR = 'err'; + const OUT = 'out'; + + const STATUS_READY = 'ready'; + const STATUS_STARTED = 'started'; + const STATUS_TERMINATED = 'terminated'; + + const STDIN = 0; + const STDOUT = 1; + const STDERR = 2; + + const TIMEOUT_PRECISION = 0.2; + + private $callback; + private $commandline; + private $cwd; + private $env; + private $input; + private $starttime; + private $lastOutputTime; + private $timeout; + private $idleTimeout; + private $options; + private $exitcode; + private $fallbackExitcode; + private $processInformation; + private $outputDisabled = false; + private $stdout; + private $stderr; + private $enhanceWindowsCompatibility = true; + private $enhanceSigchildCompatibility; + private $process; + private $status = self::STATUS_READY; + private $incrementalOutputOffset = 0; + private $incrementalErrorOutputOffset = 0; + private $tty; + private $pty; + + private $useFileHandles = false; + + /** @var Pipes */ + private $processPipes; + + private $latestSignal; + + private static $sigchild; + + /** + * @var array + */ + public static $exitCodes = [ + 0 => 'OK', + 1 => 'General error', + 2 => 'Misuse of shell builtins', + 126 => 'Invoked command cannot execute', + 127 => 'Command not found', + 128 => 'Invalid exit argument', + // signals + 129 => 'Hangup', + 130 => 'Interrupt', + 131 => 'Quit and dump core', + 132 => 'Illegal instruction', + 133 => 'Trace/breakpoint trap', + 134 => 'Process aborted', + 135 => 'Bus error: "access to undefined portion of memory object"', + 136 => 'Floating point exception: "erroneous arithmetic operation"', + 137 => 'Kill (terminate immediately)', + 138 => 'User-defined 1', + 139 => 'Segmentation violation', + 140 => 'User-defined 2', + 141 => 'Write to pipe with no one reading', + 142 => 'Signal raised by alarm', + 143 => 'Termination (request to terminate)', + // 144 - not defined + 145 => 'Child process terminated, stopped (or continued*)', + 146 => 'Continue if stopped', + 147 => 'Stop executing temporarily', + 148 => 'Terminal stop signal', + 149 => 'Background process attempting to read from tty ("in")', + 150 => 'Background process attempting to write to tty ("out")', + 151 => 'Urgent data available on socket', + 152 => 'CPU time limit exceeded', + 153 => 'File size limit exceeded', + 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', + 155 => 'Profiling timer expired', + // 156 - not defined + 157 => 'Pollable event', + // 158 - not defined + 159 => 'Bad syscall', + ]; + + /** + * 构造方法 + * @access public + * @param string $commandline 指令 + * @param string|null $cwd 工作目录 + * @param array|null $env 环境变量 + * @param string|null $input 输入 + * @param int|float|null $timeout 超时时间 + * @param array $options proc_open的选项 + * @throws \RuntimeException + * @api + */ + public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = []) + { + if (!function_exists('proc_open')) { + throw new \RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.'); + } + + $this->commandline = $commandline; + $this->cwd = $cwd; + + if (null === $this->cwd && (defined('ZEND_THREAD_SAFE') || '\\' === DIRECTORY_SEPARATOR)) { + $this->cwd = getcwd(); + } + if (null !== $env) { + $this->setEnv($env); + } + + $this->input = $input; + $this->setTimeout($timeout); + $this->useFileHandles = '\\' === DIRECTORY_SEPARATOR; + $this->pty = false; + $this->enhanceWindowsCompatibility = true; + $this->enhanceSigchildCompatibility = '\\' !== DIRECTORY_SEPARATOR && $this->isSigchildEnabled(); + $this->options = array_replace([ + 'suppress_errors' => true, + 'binary_pipes' => true, + ], $options); + } + + public function __destruct() + { + $this->stop(); + } + + public function __clone() + { + $this->resetProcessData(); + } + + /** + * 运行指令 + * @access public + * @param callback|null $callback + * @return int + */ + public function run($callback = null) + { + $this->start($callback); + + return $this->wait(); + } + + /** + * 运行指令 + * @access public + * @param callable|null $callback + * @return self + * @throws \RuntimeException + * @throws ProcessFailedException + */ + public function mustRun($callback = null) + { + if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); + } + + if (0 !== $this->run($callback)) { + throw new ProcessFailedException($this); + } + + return $this; + } + + /** + * 启动进程并写到 STDIN 输入后返回。 + * @access public + * @param callable|null $callback + * @throws \RuntimeException + * @throws \RuntimeException + * @throws \LogicException + */ + public function start($callback = null) + { + if ($this->isRunning()) { + throw new \RuntimeException('Process is already running'); + } + if ($this->outputDisabled && null !== $callback) { + throw new \LogicException('Output has been disabled, enable it to allow the use of a callback.'); + } + + $this->resetProcessData(); + $this->starttime = $this->lastOutputTime = microtime(true); + $this->callback = $this->buildCallback($callback); + $descriptors = $this->getDescriptors(); + + $commandline = $this->commandline; + + if ('\\' === DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) { + $commandline = 'cmd /V:ON /E:ON /C "(' . $commandline . ')'; + foreach ($this->processPipes->getFiles() as $offset => $filename) { + $commandline .= ' ' . $offset . '>' . Utils::escapeArgument($filename); + } + $commandline .= '"'; + + if (!isset($this->options['bypass_shell'])) { + $this->options['bypass_shell'] = true; + } + } + + $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options); + + if (!is_resource($this->process)) { + throw new \RuntimeException('Unable to launch a new process.'); + } + $this->status = self::STATUS_STARTED; + + if ($this->tty) { + return; + } + + $this->updateStatus(false); + $this->checkTimeout(); + } + + /** + * 重启进程 + * @access public + * @param callable|null $callback + * @return Process + * @throws \RuntimeException + * @throws \RuntimeException + */ + public function restart($callback = null) + { + if ($this->isRunning()) { + throw new \RuntimeException('Process is already running'); + } + + $process = clone $this; + $process->start($callback); + + return $process; + } + + /** + * 等待要终止的进程 + * @access public + * @param callable|null $callback + * @return int + */ + public function wait($callback = null) + { + $this->requireProcessIsStarted(__FUNCTION__); + + $this->updateStatus(false); + if (null !== $callback) { + $this->callback = $this->buildCallback($callback); + } + + do { + $this->checkTimeout(); + $running = '\\' === DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); + $close = '\\' !== DIRECTORY_SEPARATOR || !$running; + $this->readPipes(true, $close); + } while ($running); + + while ($this->isRunning()) { + usleep(1000); + } + + if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) { + throw new \RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig'])); + } + + return $this->exitcode; + } + + /** + * 获取PID + * @access public + * @return int|null + * @throws \RuntimeException + */ + public function getPid() + { + if ($this->isSigchildEnabled()) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->isRunning() ? $this->processInformation['pid'] : null; + } + + /** + * 将一个 POSIX 信号发送到进程中 + * @access public + * @param int $signal + * @return Process + */ + public function signal($signal) + { + $this->doSignal($signal, true); + + return $this; + } + + /** + * 禁用从底层过程获取输出和错误输出。 + * @access public + * @return Process + */ + public function disableOutput() + { + if ($this->isRunning()) { + throw new \RuntimeException('Disabling output while the process is running is not possible.'); + } + if (null !== $this->idleTimeout) { + throw new \LogicException('Output can not be disabled while an idle timeout is set.'); + } + + $this->outputDisabled = true; + + return $this; + } + + /** + * 开启从底层过程获取输出和错误输出。 + * @access public + * @return Process + * @throws \RuntimeException + */ + public function enableOutput() + { + if ($this->isRunning()) { + throw new \RuntimeException('Enabling output while the process is running is not possible.'); + } + + $this->outputDisabled = false; + + return $this; + } + + /** + * 输出是否禁用 + * @access public + * @return bool + */ + public function isOutputDisabled() + { + return $this->outputDisabled; + } + + /** + * 获取当前的输出管道 + * @access public + * @return string + * @throws \LogicException + * @api + */ + public function getOutput() + { + if ($this->outputDisabled) { + throw new \LogicException('Output has been disabled.'); + } + + $this->requireProcessIsStarted(__FUNCTION__); + + $this->readPipes(false, '\\' === DIRECTORY_SEPARATOR ? !$this->processInformation['running'] : true); + + return $this->stdout; + } + + /** + * 以增量方式返回的输出结果。 + * @access public + * @return string + */ + public function getIncrementalOutput() + { + $this->requireProcessIsStarted(__FUNCTION__); + + $data = $this->getOutput(); + + $latest = substr($data, $this->incrementalOutputOffset); + + if (false === $latest) { + return ''; + } + + $this->incrementalOutputOffset = strlen($data); + + return $latest; + } + + /** + * 清空输出 + * @access public + * @return Process + */ + public function clearOutput() + { + $this->stdout = ''; + $this->incrementalOutputOffset = 0; + + return $this; + } + + /** + * 返回当前的错误输出的过程 (STDERR)。 + * @access public + * @return string + */ + public function getErrorOutput() + { + if ($this->outputDisabled) { + throw new \LogicException('Output has been disabled.'); + } + + $this->requireProcessIsStarted(__FUNCTION__); + + $this->readPipes(false, '\\' === DIRECTORY_SEPARATOR ? !$this->processInformation['running'] : true); + + return $this->stderr; + } + + /** + * 以增量方式返回 errorOutput + * @access public + * @return string + */ + public function getIncrementalErrorOutput() + { + $this->requireProcessIsStarted(__FUNCTION__); + + $data = $this->getErrorOutput(); + + $latest = substr($data, $this->incrementalErrorOutputOffset); + + if (false === $latest) { + return ''; + } + + $this->incrementalErrorOutputOffset = strlen($data); + + return $latest; + } + + /** + * 清空 errorOutput + * @access public + * @return Process + */ + public function clearErrorOutput() + { + $this->stderr = ''; + $this->incrementalErrorOutputOffset = 0; + + return $this; + } + + /** + * 获取退出码 + * @access public + * @return null|int + */ + public function getExitCode() + { + if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); + } + + $this->updateStatus(false); + + return $this->exitcode; + } + + /** + * 获取退出文本 + * @access public + * @return null|string + */ + public function getExitCodeText() + { + if (null === $exitcode = $this->getExitCode()) { + return; + } + + return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error'; + } + + /** + * 检查是否成功 + * @access public + * @return bool + */ + public function isSuccessful() + { + return 0 === $this->getExitCode(); + } + + /** + * 是否未捕获的信号已被终止子进程 + * @access public + * @return bool + */ + public function hasBeenSignaled() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + if ($this->isSigchildEnabled()) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->processInformation['signaled']; + } + + /** + * 返回导致子进程终止其执行的数。 + * @access public + * @return int + */ + public function getTermSignal() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + if ($this->isSigchildEnabled()) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->processInformation['termsig']; + } + + /** + * 检查子进程信号是否已停止 + * @access public + * @return bool + */ + public function hasBeenStopped() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + $this->updateStatus(false); + + return $this->processInformation['stopped']; + } + + /** + * 返回导致子进程停止其执行的数。 + * @access public + * @return int + */ + public function getStopSignal() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + $this->updateStatus(false); + + return $this->processInformation['stopsig']; + } + + /** + * 检查是否正在运行 + * @access public + * @return bool + */ + public function isRunning() + { + if (self::STATUS_STARTED !== $this->status) { + return false; + } + + $this->updateStatus(false); + + return $this->processInformation['running']; + } + + /** + * 检查是否已开始 + * @access public + * @return bool + */ + public function isStarted() + { + return self::STATUS_READY != $this->status; + } + + /** + * 检查是否已终止 + * @access public + * @return bool + */ + public function isTerminated() + { + $this->updateStatus(false); + + return self::STATUS_TERMINATED == $this->status; + } + + /** + * 获取当前的状态 + * @access public + * @return string + */ + public function getStatus() + { + $this->updateStatus(false); + + return $this->status; + } + + /** + * 终止进程 + * @access public + */ + public function stop() + { + if ($this->isRunning()) { + if ('\\' === DIRECTORY_SEPARATOR && !$this->isSigchildEnabled()) { + exec(sprintf('taskkill /F /T /PID %d 2>&1', $this->getPid()), $output, $exitCode); + if ($exitCode > 0) { + throw new \RuntimeException('Unable to kill the process'); + } + } else { + $pids = preg_split('/\s+/', `ps -o pid --no-heading --ppid {$this->getPid()}`); + foreach ($pids as $pid) { + if (is_numeric($pid)) { + posix_kill($pid, 9); + } + } + } + } + + $this->updateStatus(false); + if ($this->processInformation['running']) { + $this->close(); + } + + return $this->exitcode; + } + + /** + * 添加一行输出 + * @access public + * @param string $line + */ + public function addOutput($line) +{ + $this->lastOutputTime = microtime(true); + $this->stdout .= $line; + } + + /** + * 添加一行错误输出 + * @access public + * @param string $line + */ + public function addErrorOutput($line) +{ + $this->lastOutputTime = microtime(true); + $this->stderr .= $line; + } + + /** + * 获取被执行的指令 + * @access public + * @return string + */ + public function getCommandLine() +{ + return $this->commandline; + } + + /** + * 设置指令 + * @access public + * @param string $commandline + * @return self + */ + public function setCommandLine($commandline) +{ + $this->commandline = $commandline; + + return $this; + } + + /** + * 获取超时时间 + * @access public + * @return float|null + */ + public function getTimeout() +{ + return $this->timeout; + } + + /** + * 获取idle超时时间 + * @access public + * @return float|null + */ + public function getIdleTimeout() +{ + return $this->idleTimeout; + } + + /** + * 设置超时时间 + * @access public + * @param int|float|null $timeout + * @return self + */ + public function setTimeout($timeout) +{ + $this->timeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * 设置idle超时时间 + * @access public + * @param int|float|null $timeout + * @return self + */ + public function setIdleTimeout($timeout) +{ + if (null !== $timeout && $this->outputDisabled) { + throw new \LogicException('Idle timeout can not be set while the output is disabled.'); + } + + $this->idleTimeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * 设置TTY + * @access public + * @param bool $tty + * @return self + */ + public function setTty($tty) +{ + if ('\\' === DIRECTORY_SEPARATOR && $tty) { + throw new \RuntimeException('TTY mode is not supported on Windows platform.'); + } + if ($tty && (!file_exists('/dev/tty') || !is_readable('/dev/tty'))) { + throw new \RuntimeException('TTY mode requires /dev/tty to be readable.'); + } + + $this->tty = (bool) $tty; + + return $this; + } + + /** + * 检查是否是tty模式 + * @access public + * @return bool + */ + public function isTty() +{ + return $this->tty; + } + + /** + * 设置pty模式 + * @access public + * @param bool $bool + * @return self + */ + public function setPty($bool) +{ + $this->pty = (bool) $bool; + + return $this; + } + + /** + * 是否是pty模式 + * @access public + * @return bool + */ + public function isPty() +{ + return $this->pty; + } + + /** + * 获取工作目录 + * @access public + * @return string|null + */ + public function getWorkingDirectory() +{ + if (null === $this->cwd) { + return getcwd() ?: null; + } + + return $this->cwd; + } + + /** + * 设置工作目录 + * @access public + * @param string $cwd + * @return self + */ + public function setWorkingDirectory($cwd) +{ + $this->cwd = $cwd; + + return $this; + } + + /** + * 获取环境变量 + * @access public + * @return array + */ + public function getEnv() +{ + return $this->env; + } + + /** + * 设置环境变量 + * @access public + * @param array $env + * @return self + */ + public function setEnv(array $env) +{ + $env = array_filter($env, function ($value) { + return !is_array($value); + }); + + $this->env = []; + foreach ($env as $key => $value) { + $this->env[(binary) $key] = (binary) $value; + } + + return $this; + } + + /** + * 获取输入 + * @access public + * @return null|string + */ + public function getInput() +{ + return $this->input; + } + + /** + * 设置输入 + * @access public + * @param mixed $input + * @return self + */ + public function setInput($input) +{ + if ($this->isRunning()) { + throw new \LogicException('Input can not be set while the process is running.'); + } + + $this->input = Utils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input); + + return $this; + } + + /** + * 获取proc_open的选项 + * @access public + * @return array + */ + public function getOptions() +{ + return $this->options; + } + + /** + * 设置proc_open的选项 + * @access public + * @param array $options + * @return self + */ + public function setOptions(array $options) +{ + $this->options = $options; + + return $this; + } + + /** + * 是否兼容windows + * @access public + * @return bool + */ + public function getEnhanceWindowsCompatibility() +{ + return $this->enhanceWindowsCompatibility; + } + + /** + * 设置是否兼容windows + * @access public + * @param bool $enhance + * @return self + */ + public function setEnhanceWindowsCompatibility($enhance) +{ + $this->enhanceWindowsCompatibility = (bool) $enhance; + + return $this; + } + + /** + * 返回是否 sigchild 兼容模式激活 + * @access public + * @return bool + */ + public function getEnhanceSigchildCompatibility() +{ + return $this->enhanceSigchildCompatibility; + } + + /** + * 激活 sigchild 兼容性模式。 + * @access public + * @param bool $enhance + * @return self + */ + public function setEnhanceSigchildCompatibility($enhance) +{ + $this->enhanceSigchildCompatibility = (bool) $enhance; + + return $this; + } + + /** + * 是否超时 + */ + public function checkTimeout() +{ + if (self::STATUS_STARTED !== $this->status) { + return; + } + + if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { + $this->stop(); + + throw new ProcessTimeoutException($this, ProcessTimeoutException::TYPE_GENERAL); + } + + if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { + $this->stop(); + + throw new ProcessTimeoutException($this, ProcessTimeoutException::TYPE_IDLE); + } + } + + /** + * 是否支持pty + * @access public + * @return bool + */ + public static function isPtySupported() +{ + static $result; + + if (null !== $result) { + return $result; + } + + if ('\\' === DIRECTORY_SEPARATOR) { + return $result = false; + } + + $proc = @proc_open('echo 1', [['pty'], ['pty'], ['pty']], $pipes); + if (is_resource($proc)) { + proc_close($proc); + + return $result = true; + } + + return $result = false; + } + + /** + * 创建所需的 proc_open 的描述符 + * @access private + * @return array + */ + private function getDescriptors() +{ + if ('\\' === DIRECTORY_SEPARATOR) { + $this->processPipes = WindowsPipes::create($this, $this->input); + } else { + $this->processPipes = UnixPipes::create($this, $this->input); + } + $descriptors = $this->processPipes->getDescriptors($this->outputDisabled); + + if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { + + $descriptors = array_merge($descriptors, [['pipe', 'w']]); + + $this->commandline = '(' . $this->commandline . ') 3>/dev/null; code=$?; echo $code >&3; exit $code'; + } + + return $descriptors; + } + + /** + * 建立 wait () 使用的回调。 + * @access protected + * @param callable|null $callback + * @return callable + */ + protected function buildCallback($callback) +{ + $out = self::OUT; + $callback = function ($type, $data) use ($callback, $out) { + if ($out == $type) { + $this->addOutput($data); + } else { + $this->addErrorOutput($data); + } + + if (null !== $callback) { + call_user_func($callback, $type, $data); + } + }; + + return $callback; + } + + /** + * 更新状态 + * @access protected + * @param bool $blocking + */ + protected function updateStatus($blocking) +{ + if (self::STATUS_STARTED !== $this->status) { + return; + } + + $this->processInformation = proc_get_status($this->process); + $this->captureExitCode(); + + $this->readPipes($blocking, '\\' === DIRECTORY_SEPARATOR ? !$this->processInformation['running'] : true); + + if (!$this->processInformation['running']) { + $this->close(); + } + } + + /** + * 是否开启 '--enable-sigchild' + * @access protected + * @return bool + */ + protected function isSigchildEnabled() +{ + if (null !== self::$sigchild) { + return self::$sigchild; + } + + if (!function_exists('phpinfo')) { + return self::$sigchild = false; + } + + ob_start(); + phpinfo(INFO_GENERAL); + + return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); + } + + /** + * 验证是否超时 + * @access private + * @param int|float|null $timeout + * @return float|null + */ + private function validateTimeout($timeout) +{ + $timeout = (float) $timeout; + + if (0.0 === $timeout) { + $timeout = null; + } elseif ($timeout < 0) { + throw new \InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); + } + + return $timeout; + } + + /** + * 读取pipes + * @access private + * @param bool $blocking + * @param bool $close + */ + private function readPipes($blocking, $close) +{ + $result = $this->processPipes->readAndWrite($blocking, $close); + + $callback = $this->callback; + foreach ($result as $type => $data) { + if (3 == $type) { + $this->fallbackExitcode = (int) $data; + } else { + $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data); + } + } + } + + /** + * 捕获退出码 + */ + private function captureExitCode() +{ + if (isset($this->processInformation['exitcode']) && -1 != $this->processInformation['exitcode']) { + $this->exitcode = $this->processInformation['exitcode']; + } + } + + /** + * 关闭资源 + * @access private + * @return int 退出码 + */ + private function close() +{ + $this->processPipes->close(); + if (is_resource($this->process)) { + $exitcode = proc_close($this->process); + } else { + $exitcode = -1; + } + + $this->exitcode = -1 !== $exitcode ? $exitcode : (null !== $this->exitcode ? $this->exitcode : -1); + $this->status = self::STATUS_TERMINATED; + + if (-1 === $this->exitcode && null !== $this->fallbackExitcode) { + $this->exitcode = $this->fallbackExitcode; + } elseif (-1 === $this->exitcode && $this->processInformation['signaled'] + && 0 < $this->processInformation['termsig'] + ) { + $this->exitcode = 128 + $this->processInformation['termsig']; + } + + return $this->exitcode; + } + + /** + * 重置数据 + */ + private function resetProcessData() +{ + $this->starttime = null; + $this->callback = null; + $this->exitcode = null; + $this->fallbackExitcode = null; + $this->processInformation = null; + $this->stdout = null; + $this->stderr = null; + $this->process = null; + $this->latestSignal = null; + $this->status = self::STATUS_READY; + $this->incrementalOutputOffset = 0; + $this->incrementalErrorOutputOffset = 0; + } + + /** + * 将一个 POSIX 信号发送到进程中。 + * @access private + * @param int $signal + * @param bool $throwException + * @return bool + */ + private function doSignal($signal, $throwException) +{ + if (!$this->isRunning()) { + if ($throwException) { + throw new \LogicException('Can not send signal on a non running process.'); + } + + return false; + } + + if ($this->isSigchildEnabled()) { + if ($throwException) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + } + + return false; + } + + if (true !== @proc_terminate($this->process, $signal)) { + if ($throwException) { + throw new \RuntimeException(sprintf('Error while sending signal `%s`.', $signal)); + } + + return false; + } + + $this->latestSignal = $signal; + + return true; + } + + /** + * 确保进程已经开启 + * @access private + * @param string $functionName + */ + private function requireProcessIsStarted($functionName) +{ + if (!$this->isStarted()) { + throw new \LogicException(sprintf('Process must be started before calling %s.', $functionName)); + } + } + + /** + * 确保进程已经终止 + * @access private + * @param string $functionName + */ + private function requireProcessIsTerminated($functionName) +{ + if (!$this->isTerminated()) { + throw new \LogicException(sprintf('Process must be terminated before calling %s.', $functionName)); + } + } +} diff --git a/thinkphp/library/think/Request.php b/thinkphp/library/think/Request.php new file mode 100644 index 0000000..6b6dd4b --- /dev/null +++ b/thinkphp/library/think/Request.php @@ -0,0 +1,2267 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\facade\Cookie; +use think\facade\Session; + +class Request +{ + /** + * 配置参数 + * @var array + */ + protected $config = [ + // 表单请求类型伪装变量 + 'var_method' => '_method', + // 表单ajax伪装变量 + 'var_ajax' => '_ajax', + // 表单pjax伪装变量 + 'var_pjax' => '_pjax', + // PATHINFO变量名 用于兼容模式 + 'var_pathinfo' => 's', + // 兼容PATH_INFO获取 + 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'], + // 默认全局过滤方法 用逗号分隔多个 + 'default_filter' => '', + // 域名根,如thinkphp.cn + 'url_domain_root' => '', + // HTTPS代理标识 + 'https_agent_name' => '', + // IP代理获取标识 + 'http_agent_ip' => 'HTTP_X_REAL_IP', + // URL伪静态后缀 + 'url_html_suffix' => 'html', + ]; + + /** + * 请求类型 + * @var string + */ + protected $method; + + /** + * 主机名(含端口) + * @var string + */ + protected $host; + + /** + * 域名(含协议及端口) + * @var string + */ + protected $domain; + + /** + * 子域名 + * @var string + */ + protected $subDomain; + + /** + * 泛域名 + * @var string + */ + protected $panDomain; + + /** + * 当前URL地址 + * @var string + */ + protected $url; + + /** + * 基础URL + * @var string + */ + protected $baseUrl; + + /** + * 当前执行的文件 + * @var string + */ + protected $baseFile; + + /** + * 访问的ROOT地址 + * @var string + */ + protected $root; + + /** + * pathinfo + * @var string + */ + protected $pathinfo; + + /** + * pathinfo(不含后缀) + * @var string + */ + protected $path; + + /** + * 当前路由信息 + * @var array + */ + protected $routeInfo = []; + + /** + * 当前调度信息 + * @var \think\route\Dispatch + */ + protected $dispatch; + + /** + * 当前模块名 + * @var string + */ + protected $module; + + /** + * 当前控制器名 + * @var string + */ + protected $controller; + + /** + * 当前操作名 + * @var string + */ + protected $action; + + /** + * 当前语言集 + * @var string + */ + protected $langset; + + /** + * 当前请求参数 + * @var array + */ + protected $param = []; + + /** + * 当前GET参数 + * @var array + */ + protected $get = []; + + /** + * 当前POST参数 + * @var array + */ + protected $post = []; + + /** + * 当前REQUEST参数 + * @var array + */ + protected $request = []; + + /** + * 当前ROUTE参数 + * @var array + */ + protected $route = []; + + /** + * 当前PUT参数 + * @var array + */ + protected $put; + + /** + * 当前SESSION参数 + * @var array + */ + protected $session = []; + + /** + * 当前FILE参数 + * @var array + */ + protected $file = []; + + /** + * 当前COOKIE参数 + * @var array + */ + protected $cookie = []; + + /** + * 当前SERVER参数 + * @var array + */ + protected $server = []; + + /** + * 当前ENV参数 + * @var array + */ + protected $env = []; + + /** + * 当前HEADER参数 + * @var array + */ + protected $header = []; + + /** + * 资源类型定义 + * @var array + */ + protected $mimeType = [ + 'xml' => 'application/xml,text/xml,application/x-xml', + 'json' => 'application/json,text/x-json,application/jsonrequest,text/json', + 'js' => 'text/javascript,application/javascript,application/x-javascript', + 'css' => 'text/css', + 'rss' => 'application/rss+xml', + 'yaml' => 'application/x-yaml,text/yaml', + 'atom' => 'application/atom+xml', + 'pdf' => 'application/pdf', + 'text' => 'text/plain', + 'image' => 'image/png,image/jpg,image/jpeg,image/pjpeg,image/gif,image/webp,image/*', + 'csv' => 'text/csv', + 'html' => 'text/html,application/xhtml+xml,*/*', + ]; + + /** + * 当前请求内容 + * @var string + */ + protected $content; + + /** + * 全局过滤规则 + * @var array + */ + protected $filter; + + /** + * 扩展方法 + * @var array + */ + protected $hook = []; + + /** + * php://input内容 + * @var string + */ + protected $input; + + /** + * 请求缓存 + * @var array + */ + protected $cache; + + /** + * 缓存是否检查 + * @var bool + */ + protected $isCheckCache; + + /** + * 请求安全Key + * @var string + */ + protected $secureKey; + + /** + * 是否合并Param + * @var bool + */ + protected $mergeParam = false; + + /** + * 架构函数 + * @access public + * @param array $options 参数 + */ + public function __construct(array $options = []) + { + $this->init($options); + + // 保存 php://input + $this->input = file_get_contents('php://input'); + } + + public function init(array $options = []) + { + $this->config = array_merge($this->config, $options); + + if (is_null($this->filter) && !empty($this->config['default_filter'])) { + $this->filter = $this->config['default_filter']; + } + } + + public function config($name = null) + { + if (is_null($name)) { + return $this->config; + } + return isset($this->config[$name]) ? $this->config[$name] : null; + } + + public static function __make(App $app, Config $config) + { + $request = new static($config->pull('app')); + + $request->server = $_SERVER; + $request->env = $app['env']->get(); + + return $request; + } + + public function __call($method, $args) + { + if (array_key_exists($method, $this->hook)) { + array_unshift($args, $this); + return call_user_func_array($this->hook[$method], $args); + } + + throw new Exception('method not exists:' . static::class . '->' . $method); + } + + /** + * Hook 方法注入 + * @access public + * @param string|array $method 方法名 + * @param mixed $callback callable + * @return void + */ + public function hook($method, $callback = null) + { + if (is_array($method)) { + $this->hook = array_merge($this->hook, $method); + } else { + $this->hook[$method] = $callback; + } + } + + /** + * 创建一个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 + * @return \think\Request + */ + public function create($uri, $method = 'GET', $params = [], $cookie = [], $files = [], $server = [], $content = null) + { + $server['PATH_INFO'] = ''; + $server['REQUEST_METHOD'] = strtoupper($method); + $info = parse_url($uri); + + if (isset($info['host'])) { + $server['SERVER_NAME'] = $info['host']; + $server['HTTP_HOST'] = $info['host']; + } + + if (isset($info['scheme'])) { + if ('https' === $info['scheme']) { + $server['HTTPS'] = 'on'; + $server['SERVER_PORT'] = 443; + } else { + unset($server['HTTPS']); + $server['SERVER_PORT'] = 80; + } + } + + if (isset($info['port'])) { + $server['SERVER_PORT'] = $info['port']; + $server['HTTP_HOST'] = $server['HTTP_HOST'] . ':' . $info['port']; + } + + if (isset($info['user'])) { + $server['PHP_AUTH_USER'] = $info['user']; + } + + if (isset($info['pass'])) { + $server['PHP_AUTH_PW'] = $info['pass']; + } + + if (!isset($info['path'])) { + $info['path'] = '/'; + } + + $options = []; + $queryString = ''; + + $options[strtolower($method)] = $params; + + if (isset($info['query'])) { + parse_str(html_entity_decode($info['query']), $query); + if (!empty($params)) { + $params = array_replace($query, $params); + $queryString = http_build_query($params, '', '&'); + } else { + $params = $query; + $queryString = $info['query']; + } + } elseif (!empty($params)) { + $queryString = http_build_query($params, '', '&'); + } + + if ($queryString) { + parse_str($queryString, $get); + $options['get'] = isset($options['get']) ? array_merge($get, $options['get']) : $get; + } + + $server['REQUEST_URI'] = $info['path'] . ('' !== $queryString ? '?' . $queryString : ''); + $server['QUERY_STRING'] = $queryString; + $options['cookie'] = $cookie; + $options['param'] = $params; + $options['file'] = $files; + $options['server'] = $server; + $options['url'] = $server['REQUEST_URI']; + $options['baseUrl'] = $info['path']; + $options['pathinfo'] = '/' == $info['path'] ? '/' : ltrim($info['path'], '/'); + $options['method'] = $server['REQUEST_METHOD']; + $options['domain'] = isset($info['scheme']) ? $info['scheme'] . '://' . $server['HTTP_HOST'] : ''; + $options['content'] = $content; + + $request = new static(); + foreach ($options as $name => $item) { + if (property_exists($request, $name)) { + $request->$name = $item; + } + } + + return $request; + } + + /** + * 获取当前包含协议、端口的域名 + * @access public + * @param bool $port 是否需要去除端口号 + * @return string + */ + public function domain($port = false) + { + return $this->scheme() . '://' . $this->host($port); + } + + /** + * 获取当前根域名 + * @access public + * @return string + */ + public function rootDomain() + { + $root = $this->config['url_domain_root']; + + if (!$root) { + $item = explode('.', $this->host(true)); + $count = count($item); + $root = $count > 1 ? $item[$count - 2] . '.' . $item[$count - 1] : $item[0]; + } + + return $root; + } + + /** + * 获取当前子域名 + * @access public + * @return string + */ + public function subDomain() + { + if (is_null($this->subDomain)) { + // 获取当前主域名 + $rootDomain = $this->config['url_domain_root']; + + if ($rootDomain) { + // 配置域名根 例如 thinkphp.cn 163.com.cn 如果是国家级域名 com.cn net.cn 之类的域名需要配置 + $domain = explode('.', rtrim(stristr($this->host(true), $rootDomain, true), '.')); + } else { + $domain = explode('.', $this->host(true), -2); + } + + $this->subDomain = implode('.', $domain); + } + + return $this->subDomain; + } + + /** + * 设置当前泛域名的值 + * @access public + * @param string $domain 域名 + * @return $this + */ + public function setPanDomain($domain) + { + $this->panDomain = $domain; + return $this; + } + + /** + * 获取当前泛域名的值 + * @access public + * @return string + */ + public function panDomain() + { + return $this->panDomain; + } + + /** + * 设置当前完整URL 包括QUERY_STRING + * @access public + * @param string $url URL + * @return $this + */ + public function setUrl($url) + { + $this->url = $url; + return $this; + } + + /** + * 获取当前完整URL 包括QUERY_STRING + * @access public + * @param bool $complete 是否包含域名 + * @return string + */ + public function url($complete = false) + { + if (!$this->url) { + if ($this->isCli()) { + $this->url = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : ''; + } elseif ($this->server('HTTP_X_REWRITE_URL')) { + $this->url = $this->server('HTTP_X_REWRITE_URL'); + } elseif ($this->server('REQUEST_URI')) { + $this->url = $this->server('REQUEST_URI'); + } elseif ($this->server('ORIG_PATH_INFO')) { + $this->url = $this->server('ORIG_PATH_INFO') . (!empty($this->server('QUERY_STRING')) ? '?' . $this->server('QUERY_STRING') : ''); + } else { + $this->url = ''; + } + } + + return $complete ? $this->domain() . $this->url : $this->url; + } + + /** + * 设置当前完整URL 不包括QUERY_STRING + * @access public + * @param string $url URL + * @return $this + */ + public function setBaseUrl($url) + { + $this->baseUrl = $url; + return $this; + } + + /** + * 获取当前URL 不含QUERY_STRING + * @access public + * @param bool $domain 是否包含域名 + * @return string|$this + */ + public function baseUrl($domain = false) + { + if (!$this->baseUrl) { + $str = $this->url(); + $this->baseUrl = strpos($str, '?') ? strstr($str, '?', true) : $str; + } + + return $domain ? $this->domain() . $this->baseUrl : $this->baseUrl; + } + + /** + * 设置或获取当前执行的文件 SCRIPT_NAME + * @access public + * @param bool $domain 是否包含域名 + * @return string|$this + */ + public function baseFile($domain = false) + { + if (!$this->baseFile) { + $url = ''; + if (!$this->isCli()) { + $script_name = basename($this->server('SCRIPT_FILENAME')); + if (basename($this->server('SCRIPT_NAME')) === $script_name) { + $url = $this->server('SCRIPT_NAME'); + } elseif (basename($this->server('PHP_SELF')) === $script_name) { + $url = $this->server('PHP_SELF'); + } elseif (basename($this->server('ORIG_SCRIPT_NAME')) === $script_name) { + $url = $this->server('ORIG_SCRIPT_NAME'); + } elseif (($pos = strpos($this->server('PHP_SELF'), '/' . $script_name)) !== false) { + $url = substr($this->server('SCRIPT_NAME'), 0, $pos) . '/' . $script_name; + } elseif ($this->server('DOCUMENT_ROOT') && strpos($this->server('SCRIPT_FILENAME'), $this->server('DOCUMENT_ROOT')) === 0) { + $url = str_replace('\\', '/', str_replace($this->server('DOCUMENT_ROOT'), '', $this->server('SCRIPT_FILENAME'))); + } + } + $this->baseFile = $url; + } + + return $domain ? $this->domain() . $this->baseFile : $this->baseFile; + } + + /** + * 设置URL访问根地址 + * @access public + * @param string $url URL地址 + * @return string|$this + */ + public function setRoot($url = null) + { + $this->root = $url; + return $this; + } + + /** + * 获取URL访问根地址 + * @access public + * @param bool $domain 是否包含域名 + * @return string|$this + */ + public function root($domain = false) + { + if (!$this->root) { + $file = $this->baseFile(); + if ($file && 0 !== strpos($this->url(), $file)) { + $file = str_replace('\\', '/', dirname($file)); + } + $this->root = rtrim($file, '/'); + } + + return $domain ? $this->domain() . $this->root : $this->root; + } + + /** + * 获取URL访问根目录 + * @access public + * @return string + */ + public function rootUrl() + { + $base = $this->root(); + $root = strpos($base, '.') ? ltrim(dirname($base), DIRECTORY_SEPARATOR) : $base; + + if ('' != $root) { + $root = '/' . ltrim($root, '/'); + } + + return $root; + } + + public function setPathinfo($pathinfo) + { + $this->pathinfo = $pathinfo; + return $this; + } + + /** + * 获取当前请求URL的pathinfo信息(含URL后缀) + * @access public + * @return string + */ + public function pathinfo() + { + if (is_null($this->pathinfo)) { + if (isset($_GET[$this->config['var_pathinfo']])) { + // 判断URL里面是否有兼容模式参数 + $pathinfo = $_GET[$this->config['var_pathinfo']]; + unset($_GET[$this->config['var_pathinfo']]); + unset($this->get[$this->config['var_pathinfo']]); + } elseif ($this->isCli()) { + // CLI模式下 index.php module/controller/action/params/... + $pathinfo = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : ''; + } elseif ('cli-server' == PHP_SAPI) { + $pathinfo = strpos($this->server('REQUEST_URI'), '?') ? strstr($this->server('REQUEST_URI'), '?', true) : $this->server('REQUEST_URI'); + } elseif ($this->server('PATH_INFO')) { + $pathinfo = $this->server('PATH_INFO'); + } + + // 分析PATHINFO信息 + if (!isset($pathinfo)) { + foreach ($this->config['pathinfo_fetch'] as $type) { + if ($this->server($type)) { + $pathinfo = (0 === strpos($this->server($type), $this->server('SCRIPT_NAME'))) ? + substr($this->server($type), strlen($this->server('SCRIPT_NAME'))) : $this->server($type); + break; + } + } + } + + if (!empty($pathinfo)) { + unset($this->get[$pathinfo], $this->request[$pathinfo]); + } + + $this->pathinfo = empty($pathinfo) || '/' == $pathinfo ? '' : ltrim($pathinfo, '/'); + } + + return $this->pathinfo; + } + + /** + * 获取当前请求URL的pathinfo信息(不含URL后缀) + * @access public + * @return string + */ + public function path() + { + if (is_null($this->path)) { + $suffix = $this->config['url_html_suffix']; + $pathinfo = $this->pathinfo(); + + if (false === $suffix) { + // 禁止伪静态访问 + $this->path = $pathinfo; + } elseif ($suffix) { + // 去除正常的URL后缀 + $this->path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo); + } else { + // 允许任何后缀访问 + $this->path = preg_replace('/\.' . $this->ext() . '$/i', '', $pathinfo); + } + } + + return $this->path; + } + + /** + * 当前URL的访问后缀 + * @access public + * @return string + */ + public function ext() + { + return pathinfo($this->pathinfo(), PATHINFO_EXTENSION); + } + + /** + * 获取当前请求的时间 + * @access public + * @param bool $float 是否使用浮点类型 + * @return integer|float + */ + public function time($float = false) + { + return $float ? $this->server('REQUEST_TIME_FLOAT') : $this->server('REQUEST_TIME'); + } + + /** + * 当前请求的资源类型 + * @access public + * @return false|string + */ + public function type() + { + $accept = $this->server('HTTP_ACCEPT'); + + if (empty($accept)) { + return false; + } + + foreach ($this->mimeType as $key => $val) { + $array = explode(',', $val); + foreach ($array as $k => $v) { + if (stristr($accept, $v)) { + return $key; + } + } + } + + return false; + } + + /** + * 设置资源类型 + * @access public + * @param string|array $type 资源类型名 + * @param string $val 资源类型 + * @return void + */ + public function mimeType($type, $val = '') + { + if (is_array($type)) { + $this->mimeType = array_merge($this->mimeType, $type); + } else { + $this->mimeType[$type] = $val; + } + } + + /** + * 当前的请求类型 + * @access public + * @param bool $origin 是否获取原始请求类型 + * @return string + */ + public function method($origin = false) + { + if ($origin) { + // 获取原始请求类型 + return $this->server('REQUEST_METHOD') ?: 'GET'; + } elseif (!$this->method) { + if (isset($_POST[$this->config['var_method']])) { + $method = strtolower($_POST[$this->config['var_method']]); + if (in_array($method, ['get', 'post', 'put', 'patch', 'delete'])) { + $this->method = strtoupper($method); + $this->{$method} = $_POST; + } else { + $this->method = 'POST'; + } + unset($_POST[$this->config['var_method']]); + } elseif ($this->server('HTTP_X_HTTP_METHOD_OVERRIDE')) { + $this->method = strtoupper($this->server('HTTP_X_HTTP_METHOD_OVERRIDE')); + } else { + $this->method = $this->server('REQUEST_METHOD') ?: 'GET'; + } + } + + return $this->method; + } + + /** + * 是否为GET请求 + * @access public + * @return bool + */ + public function isGet() + { + return $this->method() == 'GET'; + } + + /** + * 是否为POST请求 + * @access public + * @return bool + */ + public function isPost() + { + return $this->method() == 'POST'; + } + + /** + * 是否为PUT请求 + * @access public + * @return bool + */ + public function isPut() + { + return $this->method() == 'PUT'; + } + + /** + * 是否为DELTE请求 + * @access public + * @return bool + */ + public function isDelete() + { + return $this->method() == 'DELETE'; + } + + /** + * 是否为HEAD请求 + * @access public + * @return bool + */ + public function isHead() + { + return $this->method() == 'HEAD'; + } + + /** + * 是否为PATCH请求 + * @access public + * @return bool + */ + public function isPatch() + { + return $this->method() == 'PATCH'; + } + + /** + * 是否为OPTIONS请求 + * @access public + * @return bool + */ + public function isOptions() + { + return $this->method() == 'OPTIONS'; + } + + /** + * 是否为cli + * @access public + * @return bool + */ + public function isCli() + { + return PHP_SAPI == 'cli'; + } + + /** + * 是否为cgi + * @access public + * @return bool + */ + public function isCgi() + { + return strpos(PHP_SAPI, 'cgi') === 0; + } + + /** + * 获取当前请求的参数 + * @access public + * @param mixed $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function param($name = '', $default = null, $filter = '') + { + if (!$this->mergeParam) { + $method = $this->method(true); + + // 自动获取请求变量 + switch ($method) { + case 'POST': + $vars = $this->post(false); + break; + case 'PUT': + case 'DELETE': + case 'PATCH': + $vars = $this->put(false); + break; + default: + $vars = []; + } + + // 当前请求参数和URL地址中的参数合并 + $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false)); + + $this->mergeParam = true; + } + + if (true === $name) { + // 获取包含文件上传信息的数组 + $file = $this->file(); + $data = is_array($file) ? array_merge($this->param, $file) : $this->param; + + return $this->input($data, '', $default, $filter); + } + + return $this->input($this->param, $name, $default, $filter); + } + + /** + * 设置路由变量 + * @access public + * @param array $route 路由变量 + * @return $this + */ + public function setRouteVars(array $route) + { + $this->route = array_merge($this->route, $route); + return $this; + } + + /** + * 获取路由参数 + * @access public + * @param string|false $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function route($name = '', $default = null, $filter = '') + { + return $this->input($this->route, $name, $default, $filter); + } + + /** + * 获取GET参数 + * @access public + * @param string|false $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function get($name = '', $default = null, $filter = '') + { + if (empty($this->get)) { + $this->get = $_GET; + } + + return $this->input($this->get, $name, $default, $filter); + } + + /** + * 获取POST参数 + * @access public + * @param string|false $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function post($name = '', $default = null, $filter = '') + { + if (empty($this->post)) { + $this->post = !empty($_POST) ? $_POST : $this->getInputData($this->input); + } + + return $this->input($this->post, $name, $default, $filter); + } + + /** + * 获取PUT参数 + * @access public + * @param string|false $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function put($name = '', $default = null, $filter = '') + { + if (is_null($this->put)) { + $this->put = $this->getInputData($this->input); + } + + return $this->input($this->put, $name, $default, $filter); + } + + protected function getInputData($content) + { + if (false !== strpos($this->contentType(), 'json')) { + return (array) json_decode($content, true); + } elseif (strpos($content, '=')) { + parse_str($content, $data); + return $data; + } + + return []; + } + + /** + * 获取DELETE参数 + * @access public + * @param string|false $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function delete($name = '', $default = null, $filter = '') + { + return $this->put($name, $default, $filter); + } + + /** + * 获取PATCH参数 + * @access public + * @param string|false $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function patch($name = '', $default = null, $filter = '') + { + return $this->put($name, $default, $filter); + } + + /** + * 获取request变量 + * @access public + * @param string|false $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function request($name = '', $default = null, $filter = '') + { + if (empty($this->request)) { + $this->request = $_REQUEST; + } + + return $this->input($this->request, $name, $default, $filter); + } + + /** + * 获取session数据 + * @access public + * @param string $name 数据名称 + * @param string $default 默认值 + * @return mixed + */ + public function session($name = '', $default = null) + { + if (empty($this->session)) { + $this->session = Session::get(); + } + + if ('' === $name) { + return $this->session; + } + + $data = $this->getData($this->session, $name); + + return is_null($data) ? $default : $data; + } + + /** + * 获取cookie参数 + * @access public + * @param string $name 变量名 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function cookie($name = '', $default = null, $filter = '') + { + if (empty($this->cookie)) { + $this->cookie = Cookie::get(); + } + + if (!empty($name)) { + $data = Cookie::has($name) ? Cookie::get($name) : $default; + } else { + $data = $this->cookie; + } + + // 解析过滤器 + $filter = $this->getFilter($filter, $default); + + if (is_array($data)) { + array_walk_recursive($data, [$this, 'filterValue'], $filter); + reset($data); + } else { + $this->filterValue($data, $name, $filter); + } + + return $data; + } + + /** + * 获取server参数 + * @access public + * @param string $name 数据名称 + * @param string $default 默认值 + * @return mixed + */ + public function server($name = '', $default = null) + { + if (empty($name)) { + return $this->server; + } else { + $name = strtoupper($name); + } + + return isset($this->server[$name]) ? $this->server[$name] : $default; + } + + /** + * 获取上传的文件信息 + * @access public + * @param string $name 名称 + * @return null|array|\think\File + */ + public function file($name = '') + { + if (empty($this->file)) { + $this->file = isset($_FILES) ? $_FILES : []; + } + + $files = $this->file; + if (!empty($files)) { + if (strpos($name, '.')) { + list($name, $sub) = explode('.', $name); + } + + // 处理上传文件 + $array = $this->dealUploadFile($files, $name); + + if ('' === $name) { + // 获取全部文件 + return $array; + } elseif (isset($sub) && isset($array[$name][$sub])) { + return $array[$name][$sub]; + } elseif (isset($array[$name])) { + return $array[$name]; + } + } + + return; + } + + protected function dealUploadFile($files, $name) + { + $array = []; + foreach ($files as $key => $file) { + if ($file instanceof File) { + $array[$key] = $file; + } elseif (is_array($file['name'])) { + $item = []; + $keys = array_keys($file); + $count = count($file['name']); + + for ($i = 0; $i < $count; $i++) { + if ($file['error'][$i] > 0) { + if ($name == $key) { + $this->throwUploadFileError($file['error'][$i]); + } else { + continue; + } + } + + $temp['key'] = $key; + + foreach ($keys as $_key) { + $temp[$_key] = $file[$_key][$i]; + } + + $item[] = (new File($temp['tmp_name']))->setUploadInfo($temp); + } + + $array[$key] = $item; + } else { + if ($file['error'] > 0) { + if ($key == $name) { + $this->throwUploadFileError($file['error']); + } else { + continue; + } + } + + $array[$key] = (new File($file['tmp_name']))->setUploadInfo($file); + } + } + + return $array; + } + + protected function throwUploadFileError($error) + { + static $fileUploadErrors = [ + 1 => 'upload File size exceeds the maximum value', + 2 => 'upload File size exceeds the maximum value', + 3 => 'only the portion of file is uploaded', + 4 => 'no file to uploaded', + 6 => 'upload temp dir not found', + 7 => 'file write error', + ]; + + $msg = $fileUploadErrors[$error]; + + throw new Exception($msg); + } + + /** + * 获取环境变量 + * @access public + * @param string $name 数据名称 + * @param string $default 默认值 + * @return mixed + */ + public function env($name = '', $default = null) + { + if (empty($name)) { + return $this->env; + } else { + $name = strtoupper($name); + } + + return isset($this->env[$name]) ? $this->env[$name] : $default; + } + + /** + * 获取当前的Header + * @access public + * @param string $name header名称 + * @param string $default 默认值 + * @return string|array + */ + public function header($name = '', $default = null) + { + if (empty($this->header)) { + $header = []; + if (function_exists('apache_request_headers') && $result = apache_request_headers()) { + $header = $result; + } else { + $server = $this->server; + foreach ($server as $key => $val) { + if (0 === strpos($key, 'HTTP_')) { + $key = str_replace('_', '-', strtolower(substr($key, 5))); + $header[$key] = $val; + } + } + if (isset($server['CONTENT_TYPE'])) { + $header['content-type'] = $server['CONTENT_TYPE']; + } + if (isset($server['CONTENT_LENGTH'])) { + $header['content-length'] = $server['CONTENT_LENGTH']; + } + } + $this->header = array_change_key_case($header); + } + + if ('' === $name) { + return $this->header; + } + + $name = str_replace('_', '-', strtolower($name)); + + return isset($this->header[$name]) ? $this->header[$name] : $default; + } + + /** + * 递归重置数组指针 + * @access public + * @param array $data 数据源 + * @return void + */ + public function arrayReset(array &$data) + { + foreach ($data as &$value) { + if (is_array($value)) { + $this->arrayReset($value); + } + } + reset($data); + } + + /** + * 获取变量 支持过滤和默认值 + * @access public + * @param array $data 数据源 + * @param string|false $name 字段名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤函数 + * @return mixed + */ + public function input($data = [], $name = '', $default = null, $filter = '') + { + if (false === $name) { + // 获取原始数据 + return $data; + } + + $name = (string) $name; + if ('' != $name) { + // 解析name + if (strpos($name, '/')) { + list($name, $type) = explode('/', $name); + } + + $data = $this->getData($data, $name); + + if (is_null($data)) { + return $default; + } + + if (is_object($data)) { + return $data; + } + } + + // 解析过滤器 + $filter = $this->getFilter($filter, $default); + + if (is_array($data)) { + array_walk_recursive($data, [$this, 'filterValue'], $filter); + if (version_compare(PHP_VERSION, '7.1.0', '<')) { + // 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针 + $this->arrayReset($data); + } + } else { + $this->filterValue($data, $name, $filter); + } + + if (isset($type) && $data !== $default) { + // 强制类型转换 + $this->typeCast($data, $type); + } + + return $data; + } + + /** + * 获取数据 + * @access public + * @param array $data 数据源 + * @param string|false $name 字段名 + * @return mixed + */ + protected function getData(array $data, $name) + { + foreach (explode('.', $name) as $val) { + if (isset($data[$val])) { + $data = $data[$val]; + } else { + return; + } + } + + return $data; + } + + /** + * 设置或获取当前的过滤规则 + * @access public + * @param mixed $filter 过滤规则 + * @return mixed + */ + public function filter($filter = null) + { + if (is_null($filter)) { + return $this->filter; + } + + $this->filter = $filter; + } + + protected function getFilter($filter, $default) + { + if (is_null($filter)) { + $filter = []; + } else { + $filter = $filter ?: $this->filter; + if (is_string($filter) && false === strpos($filter, '/')) { + $filter = explode(',', $filter); + } else { + $filter = (array) $filter; + } + } + + $filter[] = $default; + + return $filter; + } + + /** + * 递归过滤给定的值 + * @access public + * @param mixed $value 键值 + * @param mixed $key 键名 + * @param array $filters 过滤方法+默认值 + * @return mixed + */ + private function filterValue(&$value, $key, $filters) + { + $default = array_pop($filters); + + foreach ($filters as $filter) { + if (is_callable($filter)) { + // 调用函数或者方法过滤 + $value = call_user_func($filter, $value); + } elseif (is_scalar($value)) { + if (false !== strpos($filter, '/')) { + // 正则过滤 + if (!preg_match($filter, $value)) { + // 匹配不成功返回默认值 + $value = $default; + break; + } + } elseif (!empty($filter)) { + // filter函数不存在时, 则使用filter_var进行过滤 + // filter为非整形值时, 调用filter_id取得过滤id + $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter)); + if (false === $value) { + $value = $default; + break; + } + } + } + } + + return $value; + } + + /** + * 强制类型转换 + * @access public + * @param string $data + * @param string $type + * @return mixed + */ + private function typeCast(&$data, $type) + { + switch (strtolower($type)) { + // 数组 + case 'a': + $data = (array) $data; + break; + // 数字 + case 'd': + $data = (int) $data; + break; + // 浮点 + case 'f': + $data = (float) $data; + break; + // 布尔 + case 'b': + $data = (boolean) $data; + break; + // 字符串 + case 's': + if (is_scalar($data)) { + $data = (string) $data; + } else { + throw new \InvalidArgumentException('variable type error:' . gettype($data)); + } + break; + } + } + + /** + * 是否存在某个请求参数 + * @access public + * @param string $name 变量名 + * @param string $type 变量类型 + * @param bool $checkEmpty 是否检测空值 + * @return mixed + */ + public function has($name, $type = 'param', $checkEmpty = false) + { + if (!in_array($type, ['param', 'get', 'post', 'request', 'put', 'patch', 'file', 'session', 'cookie', 'env', 'header', 'route'])) { + return false; + } + + if (empty($this->$type)) { + $param = $this->$type(); + } else { + $param = $this->$type; + } + + // 按.拆分成多维数组进行判断 + foreach (explode('.', $name) as $val) { + if (isset($param[$val])) { + $param = $param[$val]; + } else { + return false; + } + } + + return ($checkEmpty && '' === $param) ? false : true; + } + + /** + * 获取指定的参数 + * @access public + * @param string|array $name 变量名 + * @param string $type 变量类型 + * @return mixed + */ + public function only($name, $type = 'param') + { + $param = $this->$type(); + + if (is_string($name)) { + $name = explode(',', $name); + } + + $item = []; + foreach ($name as $key => $val) { + + if (is_int($key)) { + $default = null; + $key = $val; + } else { + $default = $val; + } + + if (isset($param[$key])) { + $item[$key] = $param[$key]; + } elseif (isset($default)) { + $item[$key] = $default; + } + } + + return $item; + } + + /** + * 排除指定参数获取 + * @access public + * @param string|array $name 变量名 + * @param string $type 变量类型 + * @return mixed + */ + public function except($name, $type = 'param') + { + $param = $this->$type(); + if (is_string($name)) { + $name = explode(',', $name); + } + + foreach ($name as $key) { + if (isset($param[$key])) { + unset($param[$key]); + } + } + + return $param; + } + + /** + * 当前是否ssl + * @access public + * @return bool + */ + public function isSsl() + { + if ($this->server('HTTPS') && ('1' == $this->server('HTTPS') || 'on' == strtolower($this->server('HTTPS')))) { + return true; + } elseif ('https' == $this->server('REQUEST_SCHEME')) { + return true; + } elseif ('443' == $this->server('SERVER_PORT')) { + return true; + } elseif ('https' == $this->server('HTTP_X_FORWARDED_PROTO')) { + return true; + } elseif ($this->config['https_agent_name'] && $this->server($this->config['https_agent_name'])) { + return true; + } + + return false; + } + + /** + * 当前是否JSON请求 + * @access public + * @return bool + */ + public function isJson() + { + return false !== strpos($this->type(), 'json'); + } + + /** + * 当前是否Ajax请求 + * @access public + * @param bool $ajax true 获取原始ajax请求 + * @return bool + */ + public function isAjax($ajax = false) + { + $value = $this->server('HTTP_X_REQUESTED_WITH'); + $result = 'xmlhttprequest' == strtolower($value) ? true : false; + + if (true === $ajax) { + return $result; + } + + $result = $this->param($this->config['var_ajax']) ? true : $result; + $this->mergeParam = false; + return $result; + } + + /** + * 当前是否Pjax请求 + * @access public + * @param bool $pjax true 获取原始pjax请求 + * @return bool + */ + public function isPjax($pjax = false) + { + $result = !is_null($this->server('HTTP_X_PJAX')) ? true : false; + + if (true === $pjax) { + return $result; + } + + $result = $this->param($this->config['var_pjax']) ? true : $result; + $this->mergeParam = false; + return $result; + } + + /** + * 获取客户端IP地址 + * @access public + * @param integer $type 返回类型 0 返回IP地址 1 返回IPV4地址数字 + * @param boolean $adv 是否进行高级模式获取(有可能被伪装) + * @return mixed + */ + public function ip($type = 0, $adv = true) + { + $type = $type ? 1 : 0; + static $ip = null; + + if (null !== $ip) { + return $ip[$type]; + } + + $httpAgentIp = $this->config['http_agent_ip']; + + if ($httpAgentIp && $this->server($httpAgentIp)) { + $ip = $this->server($httpAgentIp); + } elseif ($adv) { + if ($this->server('HTTP_X_FORWARDED_FOR')) { + $arr = explode(',', $this->server('HTTP_X_FORWARDED_FOR')); + $pos = array_search('unknown', $arr); + if (false !== $pos) { + unset($arr[$pos]); + } + $ip = trim(current($arr)); + } elseif ($this->server('HTTP_CLIENT_IP')) { + $ip = $this->server('HTTP_CLIENT_IP'); + } elseif ($this->server('REMOTE_ADDR')) { + $ip = $this->server('REMOTE_ADDR'); + } + } elseif ($this->server('REMOTE_ADDR')) { + $ip = $this->server('REMOTE_ADDR'); + } + + // IP地址类型 + $ip_mode = (strpos($ip, ':') === false) ? 'ipv4' : 'ipv6'; + + // IP地址合法验证 + if (filter_var($ip, FILTER_VALIDATE_IP) !== $ip) { + $ip = ('ipv4' === $ip_mode) ? '0.0.0.0' : '::'; + } + + // 如果是ipv4地址,则直接使用ip2long返回int类型ip;如果是ipv6地址,暂时不支持,直接返回0 + $long_ip = ('ipv4' === $ip_mode) ? sprintf("%u", ip2long($ip)) : 0; + + $ip = [$ip, $long_ip]; + + return $ip[$type]; + } + + /** + * 检测是否使用手机访问 + * @access public + * @return bool + */ + public function isMobile() + { + if ($this->server('HTTP_VIA') && stristr($this->server('HTTP_VIA'), "wap")) { + return true; + } elseif ($this->server('HTTP_ACCEPT') && strpos(strtoupper($this->server('HTTP_ACCEPT')), "VND.WAP.WML")) { + return true; + } elseif ($this->server('HTTP_X_WAP_PROFILE') || $this->server('HTTP_PROFILE')) { + return true; + } elseif ($this->server('HTTP_USER_AGENT') && preg_match('/(blackberry|configuration\/cldc|hp |hp-|htc |htc_|htc-|iemobile|kindle|midp|mmp|motorola|mobile|nokia|opera mini|opera |Googlebot-Mobile|YahooSeeker\/M1A1-R2D2|android|iphone|ipod|mobi|palm|palmos|pocket|portalmmm|ppc;|smartphone|sonyericsson|sqh|spv|symbian|treo|up.browser|up.link|vodafone|windows ce|xda |xda_)/i', $this->server('HTTP_USER_AGENT'))) { + return true; + } + + return false; + } + + /** + * 当前URL地址中的scheme参数 + * @access public + * @return string + */ + public function scheme() + { + return $this->isSsl() ? 'https' : 'http'; + } + + /** + * 当前请求URL地址中的query参数 + * @access public + * @return string + */ + public function query() + { + return $this->server('QUERY_STRING'); + } + + /** + * 设置当前请求的host(包含端口) + * @access public + * @param string $host 主机名(含端口) + * @return $this + */ + public function setHost($host) + { + $this->host = $host; + + return $this; + } + + /** + * 当前请求的host + * @access public + * @param bool $strict true 仅仅获取HOST + * @return string + */ + public function host($strict = false) + { + if (!$this->host) { + $this->host = $this->server('HTTP_X_REAL_HOST') ?: $this->server('HTTP_X_FORWARDED_HOST') ?: $this->server('HTTP_HOST'); + } + + return true === $strict && strpos($this->host, ':') ? strstr($this->host, ':', true) : $this->host; + } + + /** + * 当前请求URL地址中的port参数 + * @access public + * @return integer + */ + public function port() + { + return $this->server('SERVER_PORT'); + } + + /** + * 当前请求 SERVER_PROTOCOL + * @access public + * @return string + */ + public function protocol() + { + return $this->server('SERVER_PROTOCOL'); + } + + /** + * 当前请求 REMOTE_PORT + * @access public + * @return integer + */ + public function remotePort() + { + return $this->server('REMOTE_PORT'); + } + + /** + * 当前请求 HTTP_CONTENT_TYPE + * @access public + * @return string + */ + public function contentType() + { + $contentType = $this->server('CONTENT_TYPE'); + + if ($contentType) { + if (strpos($contentType, ';')) { + list($type) = explode(';', $contentType); + } else { + $type = $contentType; + } + return trim($type); + } + + return ''; + } + + /** + * 获取当前请求的路由信息 + * @access public + * @param array $route 路由名称 + * @return array + */ + public function routeInfo(array $route = []) + { + if (!empty($route)) { + $this->routeInfo = $route; + } + + return $this->routeInfo; + } + + /** + * 设置或者获取当前请求的调度信息 + * @access public + * @param \think\route\Dispatch $dispatch 调度信息 + * @return \think\route\Dispatch + */ + public function dispatch($dispatch = null) + { + if (!is_null($dispatch)) { + $this->dispatch = $dispatch; + } + + return $this->dispatch; + } + + /** + * 获取当前请求的安全Key + * @access public + * @return string + */ + public function secureKey() + { + if (is_null($this->secureKey)) { + $this->secureKey = uniqid('', true); + } + + return $this->secureKey; + } + + /** + * 设置当前的模块名 + * @access public + * @param string $module 模块名 + * @return $this + */ + public function setModule($module) + { + $this->module = $module; + return $this; + } + + /** + * 设置当前的控制器名 + * @access public + * @param string $controller 控制器名 + * @return $this + */ + public function setController($controller) + { + $this->controller = $controller; + return $this; + } + + /** + * 设置当前的操作名 + * @access public + * @param string $action 操作名 + * @return $this + */ + public function setAction($action) + { + $this->action = $action; + return $this; + } + + /** + * 获取当前的模块名 + * @access public + * @return string + */ + public function module() + { + return $this->module ?: ''; + } + + /** + * 获取当前的控制器名 + * @access public + * @param bool $convert 转换为小写 + * @return string + */ + public function controller($convert = false) + { + $name = $this->controller ?: ''; + return $convert ? strtolower($name) : $name; + } + + /** + * 获取当前的操作名 + * @access public + * @param bool $convert 转换为驼峰 + * @return string + */ + public function action($convert = false) + { + $name = $this->action ?: ''; + return $convert ? $name : strtolower($name); + } + + /** + * 设置当前的语言 + * @access public + * @param string $lang 语言名 + * @return $this + */ + public function setLangset($lang) + { + $this->langset = $lang; + return $this; + } + + /** + * 获取当前的语言 + * @access public + * @return string + */ + public function langset() + { + return $this->langset ?: ''; + } + + /** + * 设置或者获取当前请求的content + * @access public + * @return string + */ + public function getContent() + { + if (is_null($this->content)) { + $this->content = $this->input; + } + + return $this->content; + } + + /** + * 获取当前请求的php://input + * @access public + * @return string + */ + public function getInput() + { + return $this->input; + } + + /** + * 生成请求令牌 + * @access public + * @param string $name 令牌名称 + * @param mixed $type 令牌生成方法 + * @return string + */ + public function token($name = '__token__', $type = null) + { + $type = is_callable($type) ? $type : 'md5'; + $token = call_user_func($type, $this->server('REQUEST_TIME_FLOAT')); + + if ($this->isAjax()) { + header($name . ': ' . $token); + } + + facade\Session::set($name, $token); + + return $token; + } + + /** + * 设置当前地址的请求缓存 + * @access public + * @param string $key 缓存标识,支持变量规则 ,例如 item/:name/:id + * @param mixed $expire 缓存有效期 + * @param array $except 缓存排除 + * @param string $tag 缓存标签 + * @return mixed + */ + public function cache($key, $expire = null, $except = [], $tag = null) + { + if (!is_array($except)) { + $tag = $except; + $except = []; + } + + if (false === $key || !$this->isGet() || $this->isCheckCache || false === $expire) { + // 关闭当前缓存 + return; + } + + // 标记请求缓存检查 + $this->isCheckCache = true; + + foreach ($except as $rule) { + if (0 === stripos($this->url(), $rule)) { + return; + } + } + + if ($key instanceof \Closure) { + $key = call_user_func_array($key, [$this]); + } elseif (true === $key) { + // 自动缓存功能 + $key = '__URL__'; + } elseif (strpos($key, '|')) { + list($key, $fun) = explode('|', $key); + } + + // 特殊规则替换 + if (false !== strpos($key, '__')) { + $key = str_replace(['__MODULE__', '__CONTROLLER__', '__ACTION__', '__URL__'], [$this->module, $this->controller, $this->action, md5($this->url(true))], $key); + } + + if (false !== strpos($key, ':')) { + $param = $this->param(); + foreach ($param as $item => $val) { + if (is_string($val) && false !== strpos($key, ':' . $item)) { + $key = str_replace(':' . $item, $val, $key); + } + } + } elseif (strpos($key, ']')) { + if ('[' . $this->ext() . ']' == $key) { + // 缓存某个后缀的请求 + $key = md5($this->url()); + } else { + return; + } + } + + if (isset($fun)) { + $key = $fun($key); + } + + $this->cache = [$key, $expire, $tag]; + return $this->cache; + } + + /** + * 读取请求缓存设置 + * @access public + * @return array + */ + public function getCache() + { + return $this->cache; + } + + /** + * 设置GET数据 + * @access public + * @param array $get 数据 + * @return $this + */ + public function withGet(array $get) + { + $this->get = $get; + return $this; + } + + /** + * 设置POST数据 + * @access public + * @param array $post 数据 + * @return $this + */ + public function withPost(array $post) + { + $this->post = $post; + return $this; + } + + /** + * 设置php://input数据 + * @access public + * @param string $input RAW数据 + * @return $this + */ + public function withInput($input) + { + $this->input = $input; + return $this; + } + + /** + * 设置文件上传数据 + * @access public + * @param array $files 上传信息 + * @return $this + */ + public function withFiles(array $files) + { + $this->file = $files; + return $this; + } + + /** + * 设置COOKIE数据 + * @access public + * @param array $cookie 数据 + * @return $this + */ + public function withCookie(array $cookie) + { + $this->cookie = $cookie; + return $this; + } + + /** + * 设置SERVER数据 + * @access public + * @param array $server 数据 + * @return $this + */ + public function withServer(array $server) + { + $this->server = array_change_key_case($server, CASE_UPPER); + return $this; + } + + /** + * 设置HEADER数据 + * @access public + * @param array $header 数据 + * @return $this + */ + public function withHeader(array $header) + { + $this->header = array_change_key_case($header); + return $this; + } + + /** + * 设置ENV数据 + * @access public + * @param array $env 数据 + * @return $this + */ + public function withEnv(array $env) + { + $this->env = $env; + return $this; + } + + /** + * 设置ROUTE变量 + * @access public + * @param array $route 数据 + * @return $this + */ + public function withRoute(array $route) + { + $this->route = $route; + return $this; + } + + /** + * 设置请求数据 + * @access public + * @param string $name 参数名 + * @param mixed $value 值 + */ + public function __set($name, $value) + { + return $this->param[$name] = $value; + } + + /** + * 获取请求数据的值 + * @access public + * @param string $name 参数名 + * @return mixed + */ + public function __get($name) + { + return $this->param($name); + } + + /** + * 检测请求数据的值 + * @access public + * @param string $name 名称 + * @return boolean + */ + public function __isset($name) + { + return isset($this->param[$name]); + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['dispatch'], $data['config']); + + return $data; + } +} diff --git a/thinkphp/library/think/Response.php b/thinkphp/library/think/Response.php new file mode 100644 index 0000000..5fa5402 --- /dev/null +++ b/thinkphp/library/think/Response.php @@ -0,0 +1,429 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\response\Redirect as RedirectResponse; + +class Response +{ + /** + * 原始数据 + * @var mixed + */ + protected $data; + + /** + * 应用对象实例 + * @var App + */ + protected $app; + + /** + * 当前contentType + * @var string + */ + protected $contentType = 'text/html'; + + /** + * 字符集 + * @var string + */ + protected $charset = 'utf-8'; + + /** + * 状态码 + * @var integer + */ + protected $code = 200; + + /** + * 是否允许请求缓存 + * @var bool + */ + protected $allowCache = true; + + /** + * 输出参数 + * @var array + */ + protected $options = []; + + /** + * header参数 + * @var array + */ + protected $header = []; + + /** + * 输出内容 + * @var string + */ + protected $content = null; + + /** + * 架构函数 + * @access public + * @param mixed $data 输出数据 + * @param int $code + * @param array $header + * @param array $options 输出参数 + */ + public function __construct($data = '', $code = 200, array $header = [], $options = []) + { + $this->data($data); + + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + $this->contentType($this->contentType, $this->charset); + + $this->code = $code; + $this->app = Container::get('app'); + $this->header = array_merge($this->header, $header); + } + + /** + * 创建Response对象 + * @access public + * @param mixed $data 输出数据 + * @param string $type 输出类型 + * @param int $code + * @param array $header + * @param array $options 输出参数 + * @return Response + */ + public static function create($data = '', $type = '', $code = 200, array $header = [], $options = []) + { + $class = false !== strpos($type, '\\') ? $type : '\\think\\response\\' . ucfirst(strtolower($type)); + + if (class_exists($class)) { + return new $class($data, $code, $header, $options); + } + + return new static($data, $code, $header, $options); + } + + /** + * 发送数据到客户端 + * @access public + * @return void + * @throws \InvalidArgumentException + */ + public function send() + { + // 监听response_send + $this->app['hook']->listen('response_send', $this); + + // 处理输出数据 + $data = $this->getContent(); + + // Trace调试注入 + if ('cli' != PHP_SAPI && $this->app['env']->get('app_trace', $this->app->config('app.app_trace'))) { + $this->app['debug']->inject($this, $data); + } + + if (200 == $this->code && $this->allowCache) { + $cache = $this->app['request']->getCache(); + if ($cache) { + $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'; + + $this->app['cache']->tag($cache[2])->set($cache[0], [$data, $this->header], $cache[1]); + } + } + + if (!headers_sent() && !empty($this->header)) { + // 发送状态码 + http_response_code($this->code); + // 发送头部信息 + foreach ($this->header as $name => $val) { + header($name . (!is_null($val) ? ':' . $val : '')); + } + } + + $this->sendData($data); + + if (function_exists('fastcgi_finish_request')) { + // 提高页面响应 + fastcgi_finish_request(); + } + + // 监听response_end + $this->app['hook']->listen('response_end', $this); + + // 清空当次请求有效的数据 + if (!($this instanceof RedirectResponse)) { + $this->app['session']->flush(); + } + } + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + return $data; + } + + /** + * 输出数据 + * @access protected + * @param string $data 要处理的数据 + * @return void + */ + protected function sendData($data) + { + echo $data; + } + + /** + * 输出的参数 + * @access public + * @param mixed $options 输出参数 + * @return $this + */ + public function options($options = []) + { + $this->options = array_merge($this->options, $options); + + return $this; + } + + /** + * 输出数据设置 + * @access public + * @param mixed $data 输出数据 + * @return $this + */ + public function data($data) + { + $this->data = $data; + + return $this; + } + + /** + * 是否允许请求缓存 + * @access public + * @param bool $cache 允许请求缓存 + * @return $this + */ + public function allowCache($cache) + { + $this->allowCache = $cache; + + return $this; + } + + /** + * 设置响应头 + * @access public + * @param string|array $name 参数名 + * @param string $value 参数值 + * @return $this + */ + public function header($name, $value = null) + { + if (is_array($name)) { + $this->header = array_merge($this->header, $name); + } else { + $this->header[$name] = $value; + } + + return $this; + } + + /** + * 设置页面输出内容 + * @access public + * @param mixed $content + * @return $this + */ + public function content($content) + { + if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([ + $content, + '__toString', + ]) + ) { + throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content))); + } + + $this->content = (string) $content; + + return $this; + } + + /** + * 发送HTTP状态 + * @access public + * @param integer $code 状态码 + * @return $this + */ + public function code($code) + { + $this->code = $code; + + return $this; + } + + /** + * LastModified + * @access public + * @param string $time + * @return $this + */ + public function lastModified($time) + { + $this->header['Last-Modified'] = $time; + + return $this; + } + + /** + * Expires + * @access public + * @param string $time + * @return $this + */ + public function expires($time) + { + $this->header['Expires'] = $time; + + return $this; + } + + /** + * ETag + * @access public + * @param string $eTag + * @return $this + */ + public function eTag($eTag) + { + $this->header['ETag'] = $eTag; + + return $this; + } + + /** + * 页面缓存控制 + * @access public + * @param string $cache 缓存设置 + * @return $this + */ + public function cacheControl($cache) + { + $this->header['Cache-control'] = $cache; + + return $this; + } + + /** + * 设置页面不做任何缓存 + * @access public + * @return $this + */ + public function noCache() + { + $this->header['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'; + $this->header['Pragma'] = 'no-cache'; + + return $this; + } + + /** + * 页面输出类型 + * @access public + * @param string $contentType 输出类型 + * @param string $charset 输出编码 + * @return $this + */ + public function contentType($contentType, $charset = 'utf-8') + { + $this->header['Content-Type'] = $contentType . '; charset=' . $charset; + + return $this; + } + + /** + * 获取头部信息 + * @access public + * @param string $name 头部名称 + * @return mixed + */ + public function getHeader($name = '') + { + if (!empty($name)) { + return isset($this->header[$name]) ? $this->header[$name] : null; + } + + return $this->header; + } + + /** + * 获取原始数据 + * @access public + * @return mixed + */ + public function getData() + { + return $this->data; + } + + /** + * 获取输出数据 + * @access public + * @return mixed + */ + public function getContent() + { + if (null == $this->content) { + $content = $this->output($this->data); + + if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([ + $content, + '__toString', + ]) + ) { + throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content))); + } + + $this->content = (string) $content; + } + + return $this->content; + } + + /** + * 获取状态码 + * @access public + * @return integer + */ + public function getCode() + { + return $this->code; + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app']); + + return $data; + } +} diff --git a/thinkphp/library/think/Route.php b/thinkphp/library/think/Route.php new file mode 100644 index 0000000..97f6dc7 --- /dev/null +++ b/thinkphp/library/think/Route.php @@ -0,0 +1,992 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\RouteNotFoundException; +use think\route\AliasRule; +use think\route\Dispatch; +use think\route\dispatch\Url as UrlDispatch; +use think\route\Domain; +use think\route\Resource; +use think\route\Rule; +use think\route\RuleGroup; +use think\route\RuleItem; + +class Route +{ + /** + * REST定义 + * @var array + */ + protected $rest = [ + 'index' => ['get', '', 'index'], + 'create' => ['get', '/create', 'create'], + 'edit' => ['get', '//edit', 'edit'], + 'read' => ['get', '/', 'read'], + 'save' => ['post', '', 'save'], + 'update' => ['put', '/', 'update'], + 'delete' => ['delete', '/', 'delete'], + ]; + + /** + * 请求方法前缀定义 + * @var array + */ + protected $methodPrefix = [ + 'get' => 'get', + 'post' => 'post', + 'put' => 'put', + 'delete' => 'delete', + 'patch' => 'patch', + ]; + + /** + * 应用对象 + * @var App + */ + protected $app; + + /** + * 请求对象 + * @var Request + */ + protected $request; + + /** + * 当前HOST + * @var string + */ + protected $host; + + /** + * 当前域名 + * @var string + */ + protected $domain; + + /** + * 当前分组对象 + * @var RuleGroup + */ + protected $group; + + /** + * 配置参数 + * @var array + */ + protected $config = []; + + /** + * 路由绑定 + * @var array + */ + protected $bind = []; + + /** + * 域名对象 + * @var array + */ + protected $domains = []; + + /** + * 跨域路由规则 + * @var RuleGroup + */ + protected $cross; + + /** + * 路由别名 + * @var array + */ + protected $alias = []; + + /** + * 路由是否延迟解析 + * @var bool + */ + protected $lazy = true; + + /** + * 路由是否测试模式 + * @var bool + */ + protected $isTest; + + /** + * (分组)路由规则是否合并解析 + * @var bool + */ + protected $mergeRuleRegex = true; + + /** + * 路由解析自动搜索多级控制器 + * @var bool + */ + protected $autoSearchController = true; + + public function __construct(App $app, array $config = []) + { + $this->app = $app; + $this->request = $app['request']; + $this->config = $config; + + $this->host = $this->request->host(true) ?: $config['app_host']; + + $this->setDefaultDomain(); + } + + public function config($name = null) + { + if (is_null($name)) { + return $this->config; + } + + return isset($this->config[$name]) ? $this->config[$name] : null; + } + + /** + * 配置 + * @access public + * @param array $config + * @return void + */ + public function setConfig(array $config = []) + { + $this->config = array_merge($this->config, array_change_key_case($config)); + } + + public static function __make(App $app, Config $config) + { + $config = $config->pull('app'); + $route = new static($app, $config); + + $route->lazy($config['url_lazy_route']) + ->autoSearchController($config['controller_auto_search']) + ->mergeRuleRegex($config['route_rule_merge']); + + return $route; + } + + /** + * 设置路由的请求对象实例 + * @access public + * @param Request $request 请求对象实例 + * @return void + */ + public function setRequest($request) + { + $this->request = $request; + } + + /** + * 设置路由域名及分组(包括资源路由)是否延迟解析 + * @access public + * @param bool $lazy 路由是否延迟解析 + * @return $this + */ + public function lazy($lazy = true) + { + $this->lazy = $lazy; + return $this; + } + + /** + * 设置路由为测试模式 + * @access public + * @param bool $test 路由是否测试模式 + * @return void + */ + public function setTestMode($test) + { + $this->isTest = $test; + } + + /** + * 检查路由是否为测试模式 + * @access public + * @return bool + */ + public function isTest() + { + return $this->isTest; + } + + /** + * 设置路由域名及分组(包括资源路由)是否合并解析 + * @access public + * @param bool $merge 路由是否合并解析 + * @return $this + */ + public function mergeRuleRegex($merge = true) + { + $this->mergeRuleRegex = $merge; + $this->group->mergeRuleRegex($merge); + + return $this; + } + + /** + * 设置路由自动解析是否搜索多级控制器 + * @access public + * @param bool $auto 是否自动搜索多级控制器 + * @return $this + */ + public function autoSearchController($auto = true) + { + $this->autoSearchController = $auto; + return $this; + } + + /** + * 初始化默认域名 + * @access protected + * @return void + */ + protected function setDefaultDomain() + { + // 默认域名 + $this->domain = $this->host; + + // 注册默认域名 + $domain = new Domain($this, $this->host); + + $this->domains[$this->host] = $domain; + + // 默认分组 + $this->group = $domain; + } + + /** + * 设置当前域名 + * @access public + * @param RuleGroup $group 域名 + * @return void + */ + public function setGroup(RuleGroup $group) + { + $this->group = $group; + } + + /** + * 获取当前分组 + * @access public + * @return RuleGroup + */ + public function getGroup() + { + return $this->group; + } + + /** + * 注册变量规则 + * @access public + * @param string|array $name 变量名 + * @param string $rule 变量规则 + * @return $this + */ + public function pattern($name, $rule = '') + { + $this->group->pattern($name, $rule); + + return $this; + } + + /** + * 注册路由参数 + * @access public + * @param string|array $name 参数名 + * @param mixed $value 值 + * @return $this + */ + public function option($name, $value = '') + { + $this->group->option($name, $value); + + return $this; + } + + /** + * 注册域名路由 + * @access public + * @param string|array $name 子域名 + * @param mixed $rule 路由规则 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return Domain + */ + public function domain($name, $rule = '', $option = [], $pattern = []) + { + // 支持多个域名使用相同路由规则 + $domainName = is_array($name) ? array_shift($name) : $name; + + if ('*' != $domainName && false === strpos($domainName, '.')) { + $domainName .= '.' . $this->request->rootDomain(); + } + + if (!isset($this->domains[$domainName])) { + $domain = (new Domain($this, $domainName, $rule, $option, $pattern)) + ->lazy($this->lazy) + ->mergeRuleRegex($this->mergeRuleRegex); + + $this->domains[$domainName] = $domain; + } else { + $domain = $this->domains[$domainName]; + $domain->parseGroupRule($rule); + } + + if (is_array($name) && !empty($name)) { + $root = $this->request->rootDomain(); + foreach ($name as $item) { + if (false === strpos($item, '.')) { + $item .= '.' . $root; + } + + $this->domains[$item] = $domainName; + } + } + + // 返回域名对象 + return $domain; + } + + /** + * 获取域名 + * @access public + * @return array + */ + public function getDomains() + { + return $this->domains; + } + + /** + * 设置路由绑定 + * @access public + * @param string $bind 绑定信息 + * @param string $domain 域名 + * @return $this + */ + public function bind($bind, $domain = null) + { + $domain = is_null($domain) ? $this->domain : $domain; + + $this->bind[$domain] = $bind; + + return $this; + } + + /** + * 读取路由绑定 + * @access public + * @param string $domain 域名 + * @return string|null + */ + public function getBind($domain = null) + { + if (is_null($domain)) { + $domain = $this->domain; + } elseif (true === $domain) { + return $this->bind; + } elseif (false === strpos($domain, '.')) { + $domain .= '.' . $this->request->rootDomain(); + } + + $subDomain = $this->request->subDomain(); + + if (strpos($subDomain, '.')) { + $name = '*' . strstr($subDomain, '.'); + } + + if (isset($this->bind[$domain])) { + $result = $this->bind[$domain]; + } elseif (isset($name) && isset($this->bind[$name])) { + $result = $this->bind[$name]; + } elseif (!empty($subDomain) && isset($this->bind['*'])) { + $result = $this->bind['*']; + } else { + $result = null; + } + + return $result; + } + + /** + * 读取路由标识 + * @access public + * @param string $name 路由标识 + * @param string $domain 域名 + * @return mixed + */ + public function getName($name = null, $domain = null, $method = '*') + { + return $this->app['rule_name']->get($name, $domain, $method); + } + + /** + * 读取路由 + * @access public + * @param string $rule 路由规则 + * @param string $domain 域名 + * @return array + */ + public function getRule($rule, $domain = null) + { + if (is_null($domain)) { + $domain = $this->domain; + } + + return $this->app['rule_name']->getRule($rule, $domain); + } + + /** + * 读取路由 + * @access public + * @param string $domain 域名 + * @return array + */ + public function getRuleList($domain = null) + { + return $this->app['rule_name']->getRuleList($domain); + } + + /** + * 批量导入路由标识 + * @access public + * @param array $name 路由标识 + * @return $this + */ + public function setName($name) + { + $this->app['rule_name']->import($name); + return $this; + } + + /** + * 导入配置文件的路由规则 + * @access public + * @param array $rules 路由规则 + * @param string $type 请求类型 + * @return void + */ + public function import(array $rules, $type = '*') + { + // 检查域名部署 + if (isset($rules['__domain__'])) { + foreach ($rules['__domain__'] as $key => $rule) { + $this->domain($key, $rule); + } + unset($rules['__domain__']); + } + + // 检查变量规则 + if (isset($rules['__pattern__'])) { + $this->pattern($rules['__pattern__']); + unset($rules['__pattern__']); + } + + // 检查路由别名 + if (isset($rules['__alias__'])) { + foreach ($rules['__alias__'] as $key => $val) { + $this->alias($key, $val); + } + unset($rules['__alias__']); + } + + // 检查资源路由 + if (isset($rules['__rest__'])) { + foreach ($rules['__rest__'] as $key => $rule) { + $this->resource($key, $rule); + } + unset($rules['__rest__']); + } + + // 检查路由规则(包含分组) + foreach ($rules as $key => $val) { + if (is_numeric($key)) { + $key = array_shift($val); + } + + if (empty($val)) { + continue; + } + + if (is_string($key) && 0 === strpos($key, '[')) { + $key = substr($key, 1, -1); + $this->group($key, $val); + } elseif (is_array($val)) { + $this->rule($key, $val[0], $type, $val[1], isset($val[2]) ? $val[2] : []); + } else { + $this->rule($key, $val, $type); + } + } + } + + /** + * 注册路由规则 + * @access public + * @param string $rule 路由规则 + * @param mixed $route 路由地址 + * @param string $method 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function rule($rule, $route, $method = '*', array $option = [], array $pattern = []) + { + return $this->group->addRule($rule, $route, $method, $option, $pattern); + } + + /** + * 设置跨域有效路由规则 + * @access public + * @param Rule $rule 路由规则 + * @param string $method 请求类型 + * @return $this + */ + public function setCrossDomainRule($rule, $method = '*') + { + if (!isset($this->cross)) { + $this->cross = (new RuleGroup($this))->mergeRuleRegex($this->mergeRuleRegex); + } + + $this->cross->addRuleItem($rule, $method); + + return $this; + } + + /** + * 批量注册路由规则 + * @access public + * @param array $rules 路由规则 + * @param string $method 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public function rules($rules, $method = '*', array $option = [], array $pattern = []) + { + $this->group->addRules($rules, $method, $option, $pattern); + } + + /** + * 注册路由分组 + * @access public + * @param string|array $name 分组名称或者参数 + * @param array|\Closure $route 分组路由 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleGroup + */ + public function group($name, $route, array $option = [], array $pattern = []) + { + if (is_array($name)) { + $option = $name; + $name = isset($option['name']) ? $option['name'] : ''; + } + + return (new RuleGroup($this, $this->group, $name, $route, $option, $pattern)) + ->lazy($this->lazy) + ->mergeRuleRegex($this->mergeRuleRegex); + } + + /** + * 注册路由 + * @access public + * @param string $rule 路由规则 + * @param mixed $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function any($rule, $route = '', array $option = [], array $pattern = []) + { + return $this->rule($rule, $route, '*', $option, $pattern); + } + + /** + * 注册GET路由 + * @access public + * @param string $rule 路由规则 + * @param mixed $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function get($rule, $route = '', array $option = [], array $pattern = []) + { + return $this->rule($rule, $route, 'GET', $option, $pattern); + } + + /** + * 注册POST路由 + * @access public + * @param string $rule 路由规则 + * @param mixed $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function post($rule, $route = '', array $option = [], array $pattern = []) + { + return $this->rule($rule, $route, 'POST', $option, $pattern); + } + + /** + * 注册PUT路由 + * @access public + * @param string $rule 路由规则 + * @param mixed $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function put($rule, $route = '', array $option = [], array $pattern = []) + { + return $this->rule($rule, $route, 'PUT', $option, $pattern); + } + + /** + * 注册DELETE路由 + * @access public + * @param string $rule 路由规则 + * @param mixed $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function delete($rule, $route = '', array $option = [], array $pattern = []) + { + return $this->rule($rule, $route, 'DELETE', $option, $pattern); + } + + /** + * 注册PATCH路由 + * @access public + * @param string $rule 路由规则 + * @param mixed $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function patch($rule, $route = '', array $option = [], array $pattern = []) + { + return $this->rule($rule, $route, 'PATCH', $option, $pattern); + } + + /** + * 注册资源路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return Resource + */ + public function resource($rule, $route = '', array $option = [], array $pattern = []) + { + return (new Resource($this, $this->group, $rule, $route, $option, $pattern, $this->rest)) + ->lazy($this->lazy); + } + + /** + * 注册控制器路由 操作方法对应不同的请求前缀 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleGroup + */ + public function controller($rule, $route = '', array $option = [], array $pattern = []) + { + $group = new RuleGroup($this, $this->group, $rule, null, $option, $pattern); + + foreach ($this->methodPrefix as $type => $val) { + $group->addRule('', $val . '', $type); + } + + return $group->prefix($route . '/'); + } + + /** + * 注册视图路由 + * @access public + * @param string|array $rule 路由规则 + * @param string $template 路由模板地址 + * @param array $vars 模板变量 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function view($rule, $template = '', array $vars = [], array $option = [], array $pattern = []) + { + return $this->rule($rule, $template, 'GET', $option, $pattern)->view($vars); + } + + /** + * 注册重定向路由 + * @access public + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param array $status 状态码 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function redirect($rule, $route = '', $status = 301, array $option = [], array $pattern = []) + { + return $this->rule($rule, $route, '*', $option, $pattern)->redirect()->status($status); + } + + /** + * 注册别名路由 + * @access public + * @param string $rule 路由别名 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @return AliasRule + */ + public function alias($rule, $route, array $option = []) + { + $aliasRule = new AliasRule($this, $this->group, $rule, $route, $option); + + $this->alias[$rule] = $aliasRule; + + return $aliasRule; + } + + /** + * 获取别名路由定义 + * @access public + * @param string $name 路由别名 + * @return string|array|null + */ + public function getAlias($name = null) + { + if (is_null($name)) { + return $this->alias; + } + + return isset($this->alias[$name]) ? $this->alias[$name] : null; + } + + /** + * 设置不同请求类型下面的方法前缀 + * @access public + * @param string|array $method 请求类型 + * @param string $prefix 类型前缀 + * @return $this + */ + public function setMethodPrefix($method, $prefix = '') + { + if (is_array($method)) { + $this->methodPrefix = array_merge($this->methodPrefix, array_change_key_case($method)); + } else { + $this->methodPrefix[strtolower($method)] = $prefix; + } + + return $this; + } + + /** + * 获取请求类型的方法前缀 + * @access public + * @param string $method 请求类型 + * @param string $prefix 类型前缀 + * @return string|null + */ + public function getMethodPrefix($method) + { + $method = strtolower($method); + + return isset($this->methodPrefix[$method]) ? $this->methodPrefix[$method] : null; + } + + /** + * rest方法定义和修改 + * @access public + * @param string $name 方法名称 + * @param array|bool $resource 资源 + * @return $this + */ + public function rest($name, $resource = []) + { + if (is_array($name)) { + $this->rest = $resource ? $name : array_merge($this->rest, $name); + } else { + $this->rest[$name] = $resource; + } + + return $this; + } + + /** + * 获取rest方法定义的参数 + * @access public + * @param string $name 方法名称 + * @return array|null + */ + public function getRest($name = null) + { + if (is_null($name)) { + return $this->rest; + } + + return isset($this->rest[$name]) ? $this->rest[$name] : null; + } + + /** + * 注册未匹配路由规则后的处理 + * @access public + * @param string $route 路由地址 + * @param string $method 请求类型 + * @param array $option 路由参数 + * @return RuleItem + */ + public function miss($route, $method = '*', array $option = []) + { + return $this->group->addMissRule($route, $method, $option); + } + + /** + * 注册一个自动解析的URL路由 + * @access public + * @param string $route 路由地址 + * @return RuleItem + */ + public function auto($route) + { + return $this->group->addAutoRule($route); + } + + /** + * 检测URL路由 + * @access public + * @param string $url URL地址 + * @param bool $must 是否强制路由 + * @return Dispatch + * @throws RouteNotFoundException + */ + public function check($url, $must = false) + { + // 自动检测域名路由 + $domain = $this->checkDomain(); + $url = str_replace($this->config['pathinfo_depr'], '|', $url); + + $completeMatch = $this->config['route_complete_match']; + + $result = $domain->check($this->request, $url, $completeMatch); + + if (false === $result && !empty($this->cross)) { + // 检测跨域路由 + $result = $this->cross->check($this->request, $url, $completeMatch); + } + + if (false !== $result) { + // 路由匹配 + return $result; + } elseif ($must) { + // 强制路由不匹配则抛出异常 + throw new RouteNotFoundException(); + } + + // 默认路由解析 + return new UrlDispatch($this->request, $this->group, $url, [ + 'auto_search' => $this->autoSearchController, + ]); + } + + /** + * 检测域名的路由规则 + * @access protected + * @return Domain + */ + protected function checkDomain() + { + // 获取当前子域名 + $subDomain = $this->request->subDomain(); + + $item = false; + + if ($subDomain && count($this->domains) > 1) { + $domain = explode('.', $subDomain); + $domain2 = array_pop($domain); + + if ($domain) { + // 存在三级域名 + $domain3 = array_pop($domain); + } + + if ($subDomain && isset($this->domains[$subDomain])) { + // 子域名配置 + $item = $this->domains[$subDomain]; + } elseif (isset($this->domains['*.' . $domain2]) && !empty($domain3)) { + // 泛三级域名 + $item = $this->domains['*.' . $domain2]; + $panDomain = $domain3; + } elseif (isset($this->domains['*']) && !empty($domain2)) { + // 泛二级域名 + if ('www' != $domain2) { + $item = $this->domains['*']; + $panDomain = $domain2; + } + } + + if (isset($panDomain)) { + // 保存当前泛域名 + $this->request->setPanDomain($panDomain); + } + } + + if (false === $item) { + // 检测当前完整域名 + $item = $this->domains[$this->host]; + } + + if (is_string($item)) { + $item = $this->domains[$item]; + } + + return $item; + } + + /** + * 清空路由规则 + * @access public + * @return void + */ + public function clear() + { + $this->app['rule_name']->clear(); + $this->group->clear(); + } + + /** + * 设置全局的路由分组参数 + * @access public + * @param string $method 方法名 + * @param array $args 调用参数 + * @return RuleGroup + */ + public function __call($method, $args) + { + return call_user_func_array([$this->group, $method], $args); + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app'], $data['request']); + + return $data; + } +} diff --git a/thinkphp/library/think/Session.php b/thinkphp/library/think/Session.php new file mode 100644 index 0000000..63ee7a0 --- /dev/null +++ b/thinkphp/library/think/Session.php @@ -0,0 +1,579 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ClassNotFoundException; + +class Session +{ + /** + * 配置参数 + * @var array + */ + protected $config = []; + + /** + * 前缀 + * @var string + */ + protected $prefix = ''; + + /** + * 是否初始化 + * @var bool + */ + protected $init = null; + + /** + * 锁驱动 + * @var object + */ + protected $lockDriver = null; + + /** + * 锁key + * @var string + */ + protected $sessKey = 'PHPSESSID'; + + /** + * 锁超时时间 + * @var integer + */ + protected $lockTimeout = 3; + + /** + * 是否启用锁机制 + * @var bool + */ + protected $lock = false; + + public function __construct(array $config = []) + { + $this->config = $config; + } + + /** + * 设置或者获取session作用域(前缀) + * @access public + * @param string $prefix + * @return string|void + */ + public function prefix($prefix = '') + { + empty($this->init) && $this->boot(); + + if (empty($prefix) && null !== $prefix) { + return $this->prefix; + } else { + $this->prefix = $prefix; + } + } + + public static function __make(Config $config) + { + return new static($config->pull('session')); + } + + /** + * 配置 + * @access public + * @param array $config + * @return void + */ + public function setConfig(array $config = []) + { + $this->config = array_merge($this->config, array_change_key_case($config)); + + if (isset($config['prefix'])) { + $this->prefix = $config['prefix']; + } + + if (isset($config['use_lock'])) { + $this->lock = $config['use_lock']; + } + } + + /** + * 设置已经初始化 + * @access public + * @return void + */ + public function inited() + { + $this->init = true; + } + + /** + * session初始化 + * @access public + * @param array $config + * @return void + * @throws \think\Exception + */ + public function init(array $config = []) + { + $config = $config ?: $this->config; + + $isDoStart = false; + if (isset($config['use_trans_sid'])) { + ini_set('session.use_trans_sid', $config['use_trans_sid'] ? 1 : 0); + } + + // 启动session + if (!empty($config['auto_start']) && PHP_SESSION_ACTIVE != session_status()) { + ini_set('session.auto_start', 0); + $isDoStart = true; + } + + if (isset($config['prefix'])) { + $this->prefix = $config['prefix']; + } + + if (isset($config['use_lock'])) { + $this->lock = $config['use_lock']; + } + + if (isset($config['var_session_id']) && isset($_REQUEST[$config['var_session_id']])) { + session_id($_REQUEST[$config['var_session_id']]); + } elseif (isset($config['id']) && !empty($config['id'])) { + session_id($config['id']); + } + + if (isset($config['name'])) { + session_name($config['name']); + } + + if (isset($config['path'])) { + session_save_path($config['path']); + } + + if (isset($config['domain'])) { + ini_set('session.cookie_domain', $config['domain']); + } + + if (isset($config['expire'])) { + ini_set('session.gc_maxlifetime', $config['expire']); + ini_set('session.cookie_lifetime', $config['expire']); + } + + if (isset($config['secure'])) { + ini_set('session.cookie_secure', $config['secure']); + } + + if (isset($config['httponly'])) { + ini_set('session.cookie_httponly', $config['httponly']); + } + + if (isset($config['use_cookies'])) { + ini_set('session.use_cookies', $config['use_cookies'] ? 1 : 0); + } + + if (isset($config['cache_limiter'])) { + session_cache_limiter($config['cache_limiter']); + } + + if (isset($config['cache_expire'])) { + session_cache_expire($config['cache_expire']); + } + + if (!empty($config['type'])) { + // 读取session驱动 + $class = false !== strpos($config['type'], '\\') ? $config['type'] : '\\think\\session\\driver\\' . ucwords($config['type']); + + // 检查驱动类 + if (!class_exists($class) || !session_set_save_handler(new $class($config))) { + throw new ClassNotFoundException('error session handler:' . $class, $class); + } + } + + if ($isDoStart) { + $this->start(); + } else { + $this->init = false; + } + + return $this; + } + + /** + * session自动启动或者初始化 + * @access public + * @return void + */ + public function boot() + { + if (is_null($this->init)) { + $this->init(); + } + + if (false === $this->init) { + if (PHP_SESSION_ACTIVE != session_status()) { + $this->start(); + } + $this->init = true; + } + } + + /** + * session设置 + * @access public + * @param string $name session名称 + * @param mixed $value session值 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public function set($name, $value, $prefix = null) + { + $this->lock(); + + empty($this->init) && $this->boot(); + + $prefix = !is_null($prefix) ? $prefix : $this->prefix; + + if (strpos($name, '.')) { + // 二维数组赋值 + list($name1, $name2) = explode('.', $name); + if ($prefix) { + $_SESSION[$prefix][$name1][$name2] = $value; + } else { + $_SESSION[$name1][$name2] = $value; + } + } elseif ($prefix) { + $_SESSION[$prefix][$name] = $value; + } else { + $_SESSION[$name] = $value; + } + + $this->unlock(); + } + + /** + * session获取 + * @access public + * @param string $name session名称 + * @param string|null $prefix 作用域(前缀) + * @return mixed + */ + public function get($name = '', $prefix = null) + { + $this->lock(); + + empty($this->init) && $this->boot(); + + $prefix = !is_null($prefix) ? $prefix : $this->prefix; + + $value = $prefix ? (!empty($_SESSION[$prefix]) ? $_SESSION[$prefix] : []) : $_SESSION; + + if ('' != $name) { + $name = explode('.', $name); + + foreach ($name as $val) { + if (isset($value[$val])) { + $value = $value[$val]; + } else { + $value = null; + break; + } + } + } + + $this->unlock(); + + return $value; + } + + /** + * session 读写锁驱动实例化 + */ + protected function initDriver() + { + $config = $this->config; + + if (!empty($config['type']) && isset($config['use_lock']) && $config['use_lock']) { + // 读取session驱动 + $class = false !== strpos($config['type'], '\\') ? $config['type'] : '\\think\\session\\driver\\' . ucwords($config['type']); + + // 检查驱动类及类中是否存在 lock 和 unlock 函数 + if (class_exists($class) && method_exists($class, 'lock') && method_exists($class, 'unlock')) { + $this->lockDriver = new $class($config); + } + } + + // 通过cookie获得session_id + if (isset($config['name']) && $config['name']) { + $this->sessKey = $config['name']; + } + + if (isset($config['lock_timeout']) && $config['lock_timeout'] > 0) { + $this->lockTimeout = $config['lock_timeout']; + } + } + + /** + * session 读写加锁 + * @access protected + * @return void + */ + protected function lock() + { + if (empty($this->lock)) { + return; + } + + $this->initDriver(); + + if (null !== $this->lockDriver && method_exists($this->lockDriver, 'lock')) { + $t = time(); + // 使用 session_id 作为互斥条件,即只对同一 session_id 的会话互斥。第一次请求没有 session_id + $sessID = isset($_COOKIE[$this->sessKey]) ? $_COOKIE[$this->sessKey] : ''; + + do { + if (time() - $t > $this->lockTimeout) { + $this->unlock(); + } + } while (!$this->lockDriver->lock($sessID, $this->lockTimeout)); + } + } + + /** + * session 读写解锁 + * @access protected + * @return void + */ + protected function unlock() + { + if (empty($this->lock)) { + return; + } + + $this->pause(); + + if ($this->lockDriver && method_exists($this->lockDriver, 'unlock')) { + $sessID = isset($_COOKIE[$this->sessKey]) ? $_COOKIE[$this->sessKey] : ''; + $this->lockDriver->unlock($sessID); + } + } + + /** + * session获取并删除 + * @access public + * @param string $name session名称 + * @param string|null $prefix 作用域(前缀) + * @return mixed + */ + public function pull($name, $prefix = null) + { + $result = $this->get($name, $prefix); + + if ($result) { + $this->delete($name, $prefix); + return $result; + } else { + return; + } + } + + /** + * session设置 下一次请求有效 + * @access public + * @param string $name session名称 + * @param mixed $value session值 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public function flash($name, $value) + { + $this->set($name, $value); + + if (!$this->has('__flash__.__time__')) { + $this->set('__flash__.__time__', $_SERVER['REQUEST_TIME_FLOAT']); + } + + $this->push('__flash__', $name); + } + + /** + * 清空当前请求的session数据 + * @access public + * @return void + */ + public function flush() + { + if (!$this->init) { + return; + } + + $item = $this->get('__flash__'); + + if (!empty($item)) { + $time = $item['__time__']; + + if ($_SERVER['REQUEST_TIME_FLOAT'] > $time) { + unset($item['__time__']); + $this->delete($item); + $this->set('__flash__', []); + } + } + } + + /** + * 删除session数据 + * @access public + * @param string|array $name session名称 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public function delete($name, $prefix = null) + { + empty($this->init) && $this->boot(); + + $prefix = !is_null($prefix) ? $prefix : $this->prefix; + + if (is_array($name)) { + foreach ($name as $key) { + $this->delete($key, $prefix); + } + } elseif (strpos($name, '.')) { + list($name1, $name2) = explode('.', $name); + if ($prefix) { + unset($_SESSION[$prefix][$name1][$name2]); + } else { + unset($_SESSION[$name1][$name2]); + } + } else { + if ($prefix) { + unset($_SESSION[$prefix][$name]); + } else { + unset($_SESSION[$name]); + } + } + } + + /** + * 清空session数据 + * @access public + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public function clear($prefix = null) + { + empty($this->init) && $this->boot(); + $prefix = !is_null($prefix) ? $prefix : $this->prefix; + + if ($prefix) { + unset($_SESSION[$prefix]); + } else { + $_SESSION = []; + } + } + + /** + * 判断session数据 + * @access public + * @param string $name session名称 + * @param string|null $prefix + * @return bool + */ + public function has($name, $prefix = null) + { + empty($this->init) && $this->boot(); + + $prefix = !is_null($prefix) ? $prefix : $this->prefix; + $value = $prefix ? (!empty($_SESSION[$prefix]) ? $_SESSION[$prefix] : []) : $_SESSION; + + $name = explode('.', $name); + + foreach ($name as $val) { + if (!isset($value[$val])) { + return false; + } else { + $value = $value[$val]; + } + } + + return true; + } + + /** + * 添加数据到一个session数组 + * @access public + * @param string $key + * @param mixed $value + * @return void + */ + public function push($key, $value) + { + $array = $this->get($key); + + if (is_null($array)) { + $array = []; + } + + $array[] = $value; + + $this->set($key, $array); + } + + /** + * 启动session + * @access public + * @return void + */ + public function start() + { + session_start(); + + $this->init = true; + } + + /** + * 销毁session + * @access public + * @return void + */ + public function destroy() + { + if (!empty($_SESSION)) { + $_SESSION = []; + } + + session_unset(); + session_destroy(); + + $this->init = null; + $this->lockDriver = null; + } + + /** + * 重新生成session_id + * @access public + * @param bool $delete 是否删除关联会话文件 + * @return void + */ + public function regenerate($delete = false) + { + session_regenerate_id($delete); + } + + /** + * 暂停session + * @access public + * @return void + */ + public function pause() + { + // 暂停session + session_write_close(); + $this->init = false; + } +} diff --git a/thinkphp/library/think/Template.php b/thinkphp/library/think/Template.php new file mode 100644 index 0000000..2855cbc --- /dev/null +++ b/thinkphp/library/think/Template.php @@ -0,0 +1,1318 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\TemplateNotFoundException; + +/** + * ThinkPHP分离出来的模板引擎 + * 支持XML标签和普通标签的模板解析 + * 编译型模板引擎 支持动态缓存 + */ +class Template +{ + protected $app; + /** + * 模板变量 + * @var array + */ + protected $data = []; + + /** + * 模板配置参数 + * @var array + */ + protected $config = [ + 'view_path' => '', // 模板路径 + 'view_base' => '', + 'view_suffix' => 'html', // 默认模板文件后缀 + 'view_depr' => DIRECTORY_SEPARATOR, + 'cache_suffix' => 'php', // 默认模板缓存后缀 + 'tpl_deny_func_list' => 'echo,exit', // 模板引擎禁用函数 + 'tpl_deny_php' => false, // 默认模板引擎是否禁用PHP原生代码 + 'tpl_begin' => '{', // 模板引擎普通标签开始标记 + 'tpl_end' => '}', // 模板引擎普通标签结束标记 + 'strip_space' => false, // 是否去除模板文件里面的html空格与换行 + 'tpl_cache' => true, // 是否开启模板编译缓存,设为false则每次都会重新编译 + 'compile_type' => 'file', // 模板编译类型 + 'cache_prefix' => '', // 模板缓存前缀标识,可以动态改变 + 'cache_time' => 0, // 模板缓存有效期 0 为永久,(以数字为值,单位:秒) + 'layout_on' => false, // 布局模板开关 + 'layout_name' => 'layout', // 布局模板入口文件 + 'layout_item' => '{__CONTENT__}', // 布局模板的内容替换标识 + 'taglib_begin' => '{', // 标签库标签开始标记 + 'taglib_end' => '}', // 标签库标签结束标记 + 'taglib_load' => true, // 是否使用内置标签库之外的其它标签库,默认自动检测 + 'taglib_build_in' => 'cx', // 内置标签库名称(标签使用不必指定标签库名称),以逗号分隔 注意解析顺序 + 'taglib_pre_load' => '', // 需要额外加载的标签库(须指定标签库名称),多个以逗号分隔 + 'display_cache' => false, // 模板渲染缓存 + 'cache_id' => '', // 模板缓存ID + 'tpl_replace_string' => [], + 'tpl_var_identify' => 'array', // .语法变量识别,array|object|'', 为空时自动识别 + 'default_filter' => 'htmlentities', // 默认过滤方法 用于普通标签输出 + ]; + + /** + * 保留内容信息 + * @var array + */ + private $literal = []; + + /** + * 模板包含信息 + * @var array + */ + private $includeFile = []; + + /** + * 模板存储对象 + * @var object + */ + protected $storage; + + /** + * 架构函数 + * @access public + * @param array $config + */ + public function __construct(App $app, array $config = []) + { + $this->app = $app; + $this->config['cache_path'] = $app->getRuntimePath() . 'temp/'; + $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'; + + $this->storage = Loader::factory($type, '\\think\\template\\driver\\', null); + } + + public static function __make(Config $config) + { + return new static($config->pull('template')); + } + + /** + * 模板变量赋值 + * @access public + * @param mixed $name + * @param mixed $value + * @return void + */ + public function assign($name, $value = '') + { + if (is_array($name)) { + $this->data = array_merge($this->data, $name); + } else { + $this->data[$name] = $value; + } + } + + /** + * 模板引擎参数赋值 + * @access public + * @param mixed $name + * @param mixed $value + */ + public function __set($name, $value) + { + $this->config[$name] = $value; + } + + /** + * 模板引擎配置项 + * @access public + * @param array|string $config + * @return void|array + */ + public function config($config) + { + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } elseif (isset($this->config[$config])) { + return $this->config[$config]; + } + } + + /** + * 模板变量获取 + * @access public + * @param string $name 变量名 + * @return mixed + */ + public function get($name = '') + { + if ('' == $name) { + return $this->data; + } + + $data = $this->data; + + foreach (explode('.', $name) as $key => $val) { + if (isset($data[$val])) { + $data = $data[$val]; + } else { + $data = null; + break; + } + } + + return $data; + } + + /** + * 渲染模板文件 + * @access public + * @param string $template 模板文件 + * @param array $vars 模板变量 + * @param array $config 模板参数 + * @return void + */ + public function fetch($template, $vars = [], $config = []) + { + if ($vars) { + $this->data = $vars; + } + + if ($config) { + $this->config($config); + } + + $cache = $this->app['cache']; + + if (!empty($this->config['cache_id']) && $this->config['display_cache']) { + // 读取渲染缓存 + $cacheContent = $cache->get($this->config['cache_id']); + + if (false !== $cacheContent) { + echo $cacheContent; + return; + } + } + + $template = $this->parseTemplateFile($template); + + if ($template) { + $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($this->config['layout_on'] . $this->config['layout_name'] . $template) . '.' . ltrim($this->config['cache_suffix'], '.'); + + if (!$this->checkCache($cacheFile)) { + // 缓存无效 重新模板编译 + $content = file_get_contents($template); + $this->compiler($content, $cacheFile); + } + + // 页面缓存 + ob_start(); + ob_implicit_flush(0); + + // 读取编译存储 + $this->storage->read($cacheFile, $this->data); + + // 获取并清空缓存 + $content = ob_get_clean(); + + if (!empty($this->config['cache_id']) && $this->config['display_cache']) { + // 缓存页面输出 + $cache->set($this->config['cache_id'], $content, $this->config['cache_time']); + } + + echo $content; + } + } + + /** + * 渲染模板内容 + * @access public + * @param string $content 模板内容 + * @param array $vars 模板变量 + * @param array $config 模板参数 + * @return void + */ + public function display($content, $vars = [], $config = []) + { + if ($vars) { + $this->data = $vars; + } + + if ($config) { + $this->config($config); + } + + $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($content) . '.' . ltrim($this->config['cache_suffix'], '.'); + + if (!$this->checkCache($cacheFile)) { + // 缓存无效 模板编译 + $this->compiler($content, $cacheFile); + } + + // 读取编译存储 + $this->storage->read($cacheFile, $this->data); + } + + /** + * 设置布局 + * @access public + * @param mixed $name 布局模板名称 false 则关闭布局 + * @param string $replace 布局模板内容替换标识 + * @return object + */ + public function layout($name, $replace = '') + { + if (false === $name) { + // 关闭布局 + $this->config['layout_on'] = false; + } else { + // 开启布局 + $this->config['layout_on'] = true; + + // 名称必须为字符串 + if (is_string($name)) { + $this->config['layout_name'] = $name; + } + + if (!empty($replace)) { + $this->config['layout_item'] = $replace; + } + } + + return $this; + } + + /** + * 检查编译缓存是否有效 + * 如果无效则需要重新编译 + * @access private + * @param string $cacheFile 缓存文件名 + * @return boolean + */ + private function checkCache($cacheFile) + { + if (!$this->config['tpl_cache'] || !is_file($cacheFile) || !$handle = @fopen($cacheFile, "r")) { + return false; + } + + // 读取第一行 + preg_match('/\/\*(.+?)\*\//', fgets($handle), $matches); + + if (!isset($matches[1])) { + return false; + } + + $includeFile = unserialize($matches[1]); + + if (!is_array($includeFile)) { + return false; + } + + // 检查模板文件是否有更新 + foreach ($includeFile as $path => $time) { + if (is_file($path) && filemtime($path) > $time) { + // 模板文件如果有更新则缓存需要更新 + return false; + } + } + + // 检查编译存储是否有效 + return $this->storage->check($cacheFile, $this->config['cache_time']); + } + + /** + * 检查编译缓存是否存在 + * @access public + * @param string $cacheId 缓存的id + * @return boolean + */ + public function isCache($cacheId) + { + if ($cacheId && $this->config['display_cache']) { + // 缓存页面输出 + return $this->app['cache']->has($cacheId); + } + + return false; + } + + /** + * 编译模板文件内容 + * @access private + * @param string $content 模板内容 + * @param string $cacheFile 缓存文件名 + * @return void + */ + private function compiler(&$content, $cacheFile) + { + // 判断是否启用布局 + if ($this->config['layout_on']) { + if (false !== strpos($content, '{__NOLAYOUT__}')) { + // 可以单独定义不使用布局 + $content = str_replace('{__NOLAYOUT__}', '', $content); + } else { + // 读取布局模板 + $layoutFile = $this->parseTemplateFile($this->config['layout_name']); + + if ($layoutFile) { + // 替换布局的主体内容 + $content = str_replace($this->config['layout_item'], $content, file_get_contents($layoutFile)); + } + } + } else { + $content = str_replace('{__NOLAYOUT__}', '', $content); + } + + // 模板解析 + $this->parse($content); + + if ($this->config['strip_space']) { + /* 去除html空格与换行 */ + $find = ['~>\s+<~', '~>(\s+\n|\r)~']; + $replace = ['><', '>']; + $content = preg_replace($find, $replace, $content); + } + + // 优化生成的php代码 + $content = preg_replace('/\?>\s*<\?php\s(?!echo\b|\bend)/s', '', $content); + + // 模板过滤输出 + $replace = $this->config['tpl_replace_string']; + $content = str_replace(array_keys($replace), array_values($replace), $content); + + // 添加安全代码及模板引用记录 + $content = 'includeFile) . '*/ ?>' . "\n" . $content; + // 编译存储 + $this->storage->write($cacheFile, $content); + + $this->includeFile = []; + } + + /** + * 模板解析入口 + * 支持普通标签和TagLib解析 支持自定义标签库 + * @access public + * @param string $content 要解析的模板内容 + * @return void + */ + public function parse(&$content) + { + // 内容为空不解析 + if (empty($content)) { + return; + } + + // 替换literal标签内容 + $this->parseLiteral($content); + + // 解析继承 + $this->parseExtend($content); + + // 解析布局 + $this->parseLayout($content); + + // 检查include语法 + $this->parseInclude($content); + + // 替换包含文件中literal标签内容 + $this->parseLiteral($content); + + // 检查PHP语法 + $this->parsePhp($content); + + // 获取需要引入的标签库列表 + // 标签库只需要定义一次,允许引入多个一次 + // 一般放在文件的最前面 + // 格式: + // 当TAGLIB_LOAD配置为true时才会进行检测 + if ($this->config['taglib_load']) { + $tagLibs = $this->getIncludeTagLib($content); + + if (!empty($tagLibs)) { + // 对导入的TagLib进行解析 + foreach ($tagLibs as $tagLibName) { + $this->parseTagLib($tagLibName, $content); + } + } + } + + // 预先加载的标签库 无需在每个模板中使用taglib标签加载 但必须使用标签库XML前缀 + if ($this->config['taglib_pre_load']) { + $tagLibs = explode(',', $this->config['taglib_pre_load']); + + foreach ($tagLibs as $tag) { + $this->parseTagLib($tag, $content); + } + } + + // 内置标签库 无需使用taglib标签导入就可以使用 并且不需使用标签库XML前缀 + $tagLibs = explode(',', $this->config['taglib_build_in']); + + foreach ($tagLibs as $tag) { + $this->parseTagLib($tag, $content, true); + } + + // 解析普通模板标签 {$tagName} + $this->parseTag($content); + + // 还原被替换的Literal标签 + $this->parseLiteral($content, true); + } + + /** + * 检查PHP语法 + * @access private + * @param string $content 要解析的模板内容 + * @return void + * @throws \think\Exception + */ + private function parsePhp(&$content) + { + // 短标签的情况要将' . "\n", $content); + + // PHP语法检查 + if ($this->config['tpl_deny_php'] && false !== strpos($content, 'getRegex('layout'), $content, $matches)) { + // 替换Layout标签 + $content = str_replace($matches[0], '', $content); + // 解析Layout标签 + $array = $this->parseAttr($matches[0]); + + if (!$this->config['layout_on'] || $this->config['layout_name'] != $array['name']) { + // 读取布局模板 + $layoutFile = $this->parseTemplateFile($array['name']); + + if ($layoutFile) { + $replace = isset($array['replace']) ? $array['replace'] : $this->config['layout_item']; + // 替换布局的主体内容 + $content = str_replace($replace, $content, file_get_contents($layoutFile)); + } + } + } else { + $content = str_replace('{__NOLAYOUT__}', '', $content); + } + } + + /** + * 解析模板中的include标签 + * @access private + * @param string $content 要解析的模板内容 + * @return void + */ + private function parseInclude(&$content) + { + $regex = $this->getRegex('include'); + $func = function ($template) use (&$func, &$regex, &$content) { + if (preg_match_all($regex, $template, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $array = $this->parseAttr($match[0]); + $file = $array['file']; + unset($array['file']); + + // 分析模板文件名并读取内容 + $parseStr = $this->parseTemplateName($file); + + foreach ($array as $k => $v) { + // 以$开头字符串转换成模板变量 + if (0 === strpos($v, '$')) { + $v = $this->get(substr($v, 1)); + } + + $parseStr = str_replace('[' . $k . ']', $v, $parseStr); + } + + $content = str_replace($match[0], $parseStr, $content); + // 再次对包含文件进行模板分析 + $func($parseStr); + } + unset($matches); + } + }; + + // 替换模板中的include标签 + $func($content); + } + + /** + * 解析模板中的extend标签 + * @access private + * @param string $content 要解析的模板内容 + * @return void + */ + private function parseExtend(&$content) + { + $regex = $this->getRegex('extend'); + $array = $blocks = $baseBlocks = []; + $extend = ''; + + $func = function ($template) use (&$func, &$regex, &$array, &$extend, &$blocks, &$baseBlocks) { + if (preg_match($regex, $template, $matches)) { + if (!isset($array[$matches['name']])) { + $array[$matches['name']] = 1; + // 读取继承模板 + $extend = $this->parseTemplateName($matches['name']); + + // 递归检查继承 + $func($extend); + + // 取得block标签内容 + $blocks = array_merge($blocks, $this->parseBlock($template)); + + return; + } + } else { + // 取得顶层模板block标签内容 + $baseBlocks = $this->parseBlock($template, true); + + if (empty($extend)) { + // 无extend标签但有block标签的情况 + $extend = $template; + } + } + }; + + $func($content); + + if (!empty($extend)) { + if ($baseBlocks) { + $children = []; + foreach ($baseBlocks as $name => $val) { + $replace = $val['content']; + + if (!empty($children[$name])) { + // 如果包含有子block标签 + foreach ($children[$name] as $key) { + $replace = str_replace($baseBlocks[$key]['begin'] . $baseBlocks[$key]['content'] . $baseBlocks[$key]['end'], $blocks[$key]['content'], $replace); + } + } + + if (isset($blocks[$name])) { + // 带有{__block__}表示与所继承模板的相应标签合并,而不是覆盖 + $replace = str_replace(['{__BLOCK__}', '{__block__}'], $replace, $blocks[$name]['content']); + + if (!empty($val['parent'])) { + // 如果不是最顶层的block标签 + $parent = $val['parent']; + + if (isset($blocks[$parent])) { + $blocks[$parent]['content'] = str_replace($blocks[$name]['begin'] . $blocks[$name]['content'] . $blocks[$name]['end'], $replace, $blocks[$parent]['content']); + } + + $blocks[$name]['content'] = $replace; + $children[$parent][] = $name; + + continue; + } + } elseif (!empty($val['parent'])) { + // 如果子标签没有被继承则用原值 + $children[$val['parent']][] = $name; + $blocks[$name] = $val; + } + + if (!$val['parent']) { + // 替换模板中的顶级block标签 + $extend = str_replace($val['begin'] . $val['content'] . $val['end'], $replace, $extend); + } + } + } + + $content = $extend; + unset($blocks, $baseBlocks); + } + } + + /** + * 替换页面中的literal标签 + * @access private + * @param string $content 模板内容 + * @param boolean $restore 是否为还原 + * @return void + */ + private function parseLiteral(&$content, $restore = false) + { + $regex = $this->getRegex($restore ? 'restoreliteral' : 'literal'); + + if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) { + if (!$restore) { + $count = count($this->literal); + + // 替换literal标签 + foreach ($matches as $match) { + $this->literal[] = substr($match[0], strlen($match[1]), -strlen($match[2])); + $content = str_replace($match[0], "", $content); + $count++; + } + } else { + // 还原literal标签 + foreach ($matches as $match) { + $content = str_replace($match[0], $this->literal[$match[1]], $content); + } + + // 清空literal记录 + $this->literal = []; + } + + unset($matches); + } + } + + /** + * 获取模板中的block标签 + * @access private + * @param string $content 模板内容 + * @param boolean $sort 是否排序 + * @return array + */ + private function parseBlock(&$content, $sort = false) + { + $regex = $this->getRegex('block'); + $result = []; + + if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { + $right = $keys = []; + + foreach ($matches as $match) { + if (empty($match['name'][0])) { + if (count($right) > 0) { + $tag = array_pop($right); + $start = $tag['offset'] + strlen($tag['tag']); + $length = $match[0][1] - $start; + + $result[$tag['name']] = [ + 'begin' => $tag['tag'], + 'content' => substr($content, $start, $length), + 'end' => $match[0][0], + 'parent' => count($right) ? end($right)['name'] : '', + ]; + + $keys[$tag['name']] = $match[0][1]; + } + } else { + // 标签头压入栈 + $right[] = [ + 'name' => $match[2][0], + 'offset' => $match[0][1], + 'tag' => $match[0][0], + ]; + } + } + + unset($right, $matches); + + if ($sort) { + // 按block标签结束符在模板中的位置排序 + array_multisort($keys, $result); + } + } + + return $result; + } + + /** + * 搜索模板页面中包含的TagLib库 + * 并返回列表 + * @access private + * @param string $content 模板内容 + * @return array|null + */ + private function getIncludeTagLib(&$content) + { + // 搜索是否有TagLib标签 + if (preg_match($this->getRegex('taglib'), $content, $matches)) { + // 替换TagLib标签 + $content = str_replace($matches[0], '', $content); + + return explode(',', $matches['name']); + } + } + + /** + * TagLib库解析 + * @access public + * @param string $tagLib 要解析的标签库 + * @param string $content 要解析的模板内容 + * @param boolean $hide 是否隐藏标签库前缀 + * @return void + */ + public function parseTagLib($tagLib, &$content, $hide = false) + { + if (false !== strpos($tagLib, '\\')) { + // 支持指定标签库的命名空间 + $className = $tagLib; + $tagLib = substr($tagLib, strrpos($tagLib, '\\') + 1); + } else { + $className = '\\think\\template\\taglib\\' . ucwords($tagLib); + } + + $tLib = new $className($this); + + $tLib->parseTag($content, $hide ? '' : $tagLib); + } + + /** + * 分析标签属性 + * @access public + * @param string $str 属性字符串 + * @param string $name 不为空时返回指定的属性名 + * @return array + */ + public function parseAttr($str, $name = null) + { + $regex = '/\s+(?>(?P[\w-]+)\s*)=(?>\s*)([\"\'])(?P(?:(?!\\2).)*)\\2/is'; + $array = []; + + if (preg_match_all($regex, $str, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $array[$match['name']] = $match['value']; + } + unset($matches); + } + + if (!empty($name) && isset($array[$name])) { + return $array[$name]; + } + + return $array; + } + + /** + * 模板标签解析 + * 格式: {TagName:args [|content] } + * @access private + * @param string $content 要解析的模板内容 + * @return void + */ + private function parseTag(&$content) + { + $regex = $this->getRegex('tag'); + + if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $str = stripslashes($match[1]); + $flag = substr($str, 0, 1); + + switch ($flag) { + case '$': + // 解析模板变量 格式 {$varName} + // 是否带有?号 + if (false !== $pos = strpos($str, '?')) { + $array = preg_split('/([!=]={1,2}|(?<]={0,1})/', substr($str, 0, $pos), 2, PREG_SPLIT_DELIM_CAPTURE); + $name = $array[0]; + + $this->parseVar($name); + //$this->parseVarFunction($name); + + $str = trim(substr($str, $pos + 1)); + $this->parseVar($str); + $first = substr($str, 0, 1); + + if (strpos($name, ')')) { + // $name为对象或是自动识别,或者含有函数 + if (isset($array[1])) { + $this->parseVar($array[2]); + $name .= $array[1] . $array[2]; + } + + switch ($first) { + case '?': + $this->parseVarFunction($name); + $str = ''; + break; + case '=': + $str = ''; + break; + default: + $str = ''; + } + } else { + if (isset($array[1])) { + $express = true; + $this->parseVar($array[2]); + $express = $name . $array[1] . $array[2]; + } else { + $express = false; + } + + if (in_array($first, ['?', '=', ':'])) { + $str = trim(substr($str, 1)); + if ('$' == substr($str, 0, 1)) { + $str = $this->parseVarFunction($str); + } + } + + // $name为数组 + switch ($first) { + case '?': + // {$varname??'xxx'} $varname有定义则输出$varname,否则输出xxx + $str = 'parseVarFunction($name) . ' : ' . $str . '; ?>'; + break; + case '=': + // {$varname?='xxx'} $varname为真时才输出xxx + $str = ''; + break; + case ':': + // {$varname?:'xxx'} $varname为真时输出$varname,否则输出xxx + $str = 'parseVarFunction($name) . ' : ' . $str . '; ?>'; + break; + default: + if (strpos($str, ':')) { + // {$varname ? 'a' : 'b'} $varname为真时输出a,否则输出b + $array = explode(':', $str, 2); + + $array[0] = '$' == substr(trim($array[0]), 0, 1) ? $this->parseVarFunction($array[0]) : $array[0]; + $array[1] = '$' == substr(trim($array[1]), 0, 1) ? $this->parseVarFunction($array[1]) : $array[1]; + + $str = implode(' : ', $array); + } + $str = ''; + } + } + } else { + $this->parseVar($str); + $this->parseVarFunction($str); + $str = ''; + } + break; + case ':': + // 输出某个函数的结果 + $str = substr($str, 1); + $this->parseVar($str); + $str = ''; + break; + case '~': + // 执行某个函数 + $str = substr($str, 1); + $this->parseVar($str); + $str = ''; + break; + case '-': + case '+': + // 输出计算 + $this->parseVar($str); + $str = ''; + break; + case '/': + // 注释标签 + $flag2 = substr($str, 1, 1); + if ('/' == $flag2 || ('*' == $flag2 && substr(rtrim($str), -2) == '*/')) { + $str = ''; + } + break; + default: + // 未识别的标签直接返回 + $str = $this->config['tpl_begin'] . $str . $this->config['tpl_end']; + break; + } + + $content = str_replace($match[0], $str, $content); + } + + unset($matches); + } + } + + /** + * 模板变量解析,支持使用函数 + * 格式: {$varname|function1|function2=arg1,arg2} + * @access public + * @param string $varStr 变量数据 + * @return void + */ + public function parseVar(&$varStr) + { + $varStr = trim($varStr); + + if (preg_match_all('/\$[a-zA-Z_](?>\w*)(?:[:\.][0-9a-zA-Z_](?>\w*))+/', $varStr, $matches, PREG_OFFSET_CAPTURE)) { + static $_varParseList = []; + + while ($matches[0]) { + $match = array_pop($matches[0]); + + //如果已经解析过该变量字串,则直接返回变量值 + if (isset($_varParseList[$match[0]])) { + $parseStr = $_varParseList[$match[0]]; + } else { + if (strpos($match[0], '.')) { + $vars = explode('.', $match[0]); + $first = array_shift($vars); + + if ('$Think' == $first) { + // 所有以Think.打头的以特殊变量对待 无需模板赋值就可以输出 + $parseStr = $this->parseThinkVar($vars); + } elseif ('$Request' == $first) { + // 获取Request请求对象参数 + $method = array_shift($vars); + if (!empty($vars)) { + $params = implode('.', $vars); + if ('true' != $params) { + $params = '\'' . $params . '\''; + } + } else { + $params = ''; + } + + $parseStr = 'app(\'request\')->' . $method . '(' . $params . ')'; + } else { + switch ($this->config['tpl_var_identify']) { + case 'array': // 识别为数组 + $parseStr = $first . '[\'' . implode('\'][\'', $vars) . '\']'; + break; + case 'obj': // 识别为对象 + $parseStr = $first . '->' . implode('->', $vars); + break; + default: // 自动判断数组或对象 + $parseStr = '(is_array(' . $first . ')?' . $first . '[\'' . implode('\'][\'', $vars) . '\']:' . $first . '->' . implode('->', $vars) . ')'; + } + } + } else { + $parseStr = str_replace(':', '->', $match[0]); + } + + $_varParseList[$match[0]] = $parseStr; + } + + $varStr = substr_replace($varStr, $parseStr, $match[1], strlen($match[0])); + } + unset($matches); + } + } + + /** + * 对模板中使用了函数的变量进行解析 + * 格式 {$varname|function1|function2=arg1,arg2} + * @access public + * @param string $varStr 变量字符串 + * @param bool $autoescape 自动转义 + * @return void + */ + public function parseVarFunction(&$varStr, $autoescape = true) + { + if (!$autoescape && false === strpos($varStr, '|')) { + return $varStr; + } elseif ($autoescape && !preg_match('/\|(\s)?raw(\||\s)?/i', $varStr)) { + $varStr .= '|' . $this->config['default_filter']; + } + + static $_varFunctionList = []; + + $_key = md5($varStr); + + //如果已经解析过该变量字串,则直接返回变量值 + if (isset($_varFunctionList[$_key])) { + $varStr = $_varFunctionList[$_key]; + } else { + $varArray = explode('|', $varStr); + + // 取得变量名称 + $name = trim(array_shift($varArray)); + + // 对变量使用函数 + $length = count($varArray); + + // 取得模板禁止使用函数列表 + $template_deny_funs = explode(',', $this->config['tpl_deny_func_list']); + + for ($i = 0; $i < $length; $i++) { + $args = explode('=', $varArray[$i], 2); + + // 模板函数过滤 + $fun = trim($args[0]); + if (in_array($fun, $template_deny_funs)) { + continue; + } + + switch (strtolower($fun)) { + case 'raw': + break; + case 'date': + $name = 'date(' . $args[1] . ',!is_numeric(' . $name . ')? strtotime(' . $name . ') : ' . $name . ')'; + break; + case 'first': + $name = 'current(' . $name . ')'; + break; + case 'last': + $name = 'end(' . $name . ')'; + break; + case 'upper': + $name = 'strtoupper(' . $name . ')'; + break; + case 'lower': + $name = 'strtolower(' . $name . ')'; + break; + case 'format': + $name = 'sprintf(' . $args[1] . ',' . $name . ')'; + break; + case 'default': // 特殊模板函数 + if (false === strpos($name, '(')) { + $name = '(isset(' . $name . ') && (' . $name . ' !== \'\')?' . $name . ':' . $args[1] . ')'; + } else { + $name = '(' . $name . ' ?: ' . $args[1] . ')'; + } + break; + default: // 通用模板函数 + if (isset($args[1])) { + if (strstr($args[1], '###')) { + $args[1] = str_replace('###', $name, $args[1]); + $name = "$fun($args[1])"; + } else { + $name = "$fun($name,$args[1])"; + } + } else { + if (!empty($args[0])) { + $name = "$fun($name)"; + } + } + } + } + + $_varFunctionList[$_key] = $name; + $varStr = $name; + } + return $varStr; + } + + /** + * 特殊模板变量解析 + * 格式 以 $Think. 打头的变量属于特殊模板变量 + * @access public + * @param array $vars 变量数组 + * @return string + */ + public function parseThinkVar($vars) + { + $type = strtoupper(trim(array_shift($vars))); + $param = implode('.', $vars); + + if ($vars) { + switch ($type) { + case 'SERVER': + $parseStr = 'app(\'request\')->server(\'' . $param . '\')'; + break; + case 'GET': + $parseStr = 'app(\'request\')->get(\'' . $param . '\')'; + break; + case 'POST': + $parseStr = 'app(\'request\')->post(\'' . $param . '\')'; + break; + case 'COOKIE': + $parseStr = 'app(\'cookie\')->get(\'' . $param . '\')'; + break; + case 'SESSION': + $parseStr = 'app(\'session\')->get(\'' . $param . '\')'; + break; + case 'ENV': + $parseStr = 'app(\'request\')->env(\'' . $param . '\')'; + break; + case 'REQUEST': + $parseStr = 'app(\'request\')->request(\'' . $param . '\')'; + break; + case 'CONST': + $parseStr = strtoupper($param); + break; + case 'LANG': + $parseStr = 'app(\'lang\')->get(\'' . $param . '\')'; + break; + case 'CONFIG': + $parseStr = 'app(\'config\')->get(\'' . $param . '\')'; + break; + default: + $parseStr = '\'\''; + break; + } + } else { + switch ($type) { + case 'NOW': + $parseStr = "date('Y-m-d g:i a',time())"; + break; + case 'VERSION': + $parseStr = 'app()->version()'; + break; + case 'LDELIM': + $parseStr = '\'' . ltrim($this->config['tpl_begin'], '\\') . '\''; + break; + case 'RDELIM': + $parseStr = '\'' . ltrim($this->config['tpl_end'], '\\') . '\''; + break; + default: + if (defined($type)) { + $parseStr = $type; + } else { + $parseStr = ''; + } + } + } + + return $parseStr; + } + + /** + * 分析加载的模板文件并读取内容 支持多个模板文件读取 + * @access private + * @param string $templateName 模板文件名 + * @return string + */ + private function parseTemplateName($templateName) + { + $array = explode(',', $templateName); + $parseStr = ''; + + foreach ($array as $templateName) { + if (empty($templateName)) { + continue; + } + + if (0 === strpos($templateName, '$')) { + //支持加载变量文件名 + $templateName = $this->get(substr($templateName, 1)); + } + + $template = $this->parseTemplateFile($templateName); + + if ($template) { + // 获取模板文件内容 + $parseStr .= file_get_contents($template); + } + } + + return $parseStr; + } + + /** + * 解析模板文件名 + * @access private + * @param string $template 文件名 + * @return string|false + */ + private function parseTemplateFile($template) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + if (strpos($template, '@')) { + list($module, $template) = explode('@', $template); + } + + if (0 !== strpos($template, '/')) { + $template = str_replace(['/', ':'], $this->config['view_depr'], $template); + } else { + $template = str_replace(['/', ':'], $this->config['view_depr'], substr($template, 1)); + } + + if ($this->config['view_base']) { + $module = isset($module) ? $module : $this->app['request']->module(); + $path = $this->config['view_base'] . ($module ? $module . DIRECTORY_SEPARATOR : ''); + } else { + $path = isset($module) ? $this->app->getAppPath() . $module . DIRECTORY_SEPARATOR . basename($this->config['view_path']) . DIRECTORY_SEPARATOR : $this->config['view_path']; + } + + $template = $path . $template . '.' . ltrim($this->config['view_suffix'], '.'); + } + + if (is_file($template)) { + // 记录模板文件的更新时间 + $this->includeFile[$template] = filemtime($template); + + return $template; + } + + throw new TemplateNotFoundException('template not exists:' . $template, $template); + } + + /** + * 按标签生成正则 + * @access private + * @param string $tagName 标签名 + * @return string + */ + private function getRegex($tagName) + { + $regex = ''; + if ('tag' == $tagName) { + $begin = $this->config['tpl_begin']; + $end = $this->config['tpl_end']; + + if (strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1) { + $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>[^' . $end . ']*))' . $end; + } else { + $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>(?:(?!' . $end . ').)*))' . $end; + } + } else { + $begin = $this->config['taglib_begin']; + $end = $this->config['taglib_end']; + $single = strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1 ? true : false; + + switch ($tagName) { + case 'block': + if ($single) { + $regex = $begin . '(?:' . $tagName . '\b\s+(?>(?:(?!name=).)*)\bname=([\'\"])(?P[\$\w\-\/\.]+)\\1(?>[^' . $end . ']*)|\/' . $tagName . ')' . $end; + } else { + $regex = $begin . '(?:' . $tagName . '\b\s+(?>(?:(?!name=).)*)\bname=([\'\"])(?P[\$\w\-\/\.]+)\\1(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end; + } + break; + case 'literal': + if ($single) { + $regex = '(' . $begin . $tagName . '\b(?>[^' . $end . ']*)' . $end . ')'; + $regex .= '(?:(?>[^' . $begin . ']*)(?>(?!' . $begin . '(?>' . $tagName . '\b[^' . $end . ']*|\/' . $tagName . ')' . $end . ')' . $begin . '[^' . $begin . ']*)*)'; + $regex .= '(' . $begin . '\/' . $tagName . $end . ')'; + } else { + $regex = '(' . $begin . $tagName . '\b(?>(?:(?!' . $end . ').)*)' . $end . ')'; + $regex .= '(?:(?>(?:(?!' . $begin . ').)*)(?>(?!' . $begin . '(?>' . $tagName . '\b(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end . ')' . $begin . '(?>(?:(?!' . $begin . ').)*))*)'; + $regex .= '(' . $begin . '\/' . $tagName . $end . ')'; + } + break; + case 'restoreliteral': + $regex = ''; + break; + case 'include': + $name = 'file'; + case 'taglib': + case 'layout': + case 'extend': + if (empty($name)) { + $name = 'name'; + } + if ($single) { + $regex = $begin . $tagName . '\b\s+(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P[\$\w\-\/\.\:@,\\\\]+)\\1(?>[^' . $end . ']*)' . $end; + } else { + $regex = $begin . $tagName . '\b\s+(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P[\$\w\-\/\.\:@,\\\\]+)\\1(?>(?:(?!' . $end . ').)*)' . $end; + } + break; + } + } + + return '/' . $regex . '/is'; + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app'], $data['storage']); + + return $data; + } +} diff --git a/thinkphp/library/think/Url.php b/thinkphp/library/think/Url.php new file mode 100644 index 0000000..acd510a --- /dev/null +++ b/thinkphp/library/think/Url.php @@ -0,0 +1,412 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Url +{ + /** + * 配置参数 + * @var array + */ + protected $config = []; + + /** + * ROOT地址 + * @var string + */ + protected $root; + + /** + * 绑定检查 + * @var bool + */ + protected $bindCheck; + + /** + * 应用对象 + * @var App + */ + protected $app; + + public function __construct(App $app, array $config = []) + { + $this->app = $app; + $this->config = $config; + + if (is_file($app->getRuntimePath() . 'route.php')) { + // 读取路由映射文件 + $app['route']->setName(include $app->getRuntimePath() . 'route.php'); + } + } + + /** + * 初始化 + * @access public + * @param array $config + * @return void + */ + public function init(array $config = []) + { + $this->config = array_merge($this->config, array_change_key_case($config)); + } + + public static function __make(App $app, Config $config) + { + return new static($app, $config->pull('app')); + } + + /** + * URL生成 支持路由反射 + * @access public + * @param string $url 路由地址 + * @param string|array $vars 参数(支持数组和字符串)a=val&b=val2... ['a'=>'val1', 'b'=>'val2'] + * @param string|bool $suffix 伪静态后缀,默认为true表示获取配置值 + * @param boolean|string $domain 是否显示域名 或者直接传入域名 + * @return string + */ + public function build($url = '', $vars = '', $suffix = true, $domain = false) + { + // 解析URL + if (0 === strpos($url, '[') && $pos = strpos($url, ']')) { + // [name] 表示使用路由命名标识生成URL + $name = substr($url, 1, $pos - 1); + $url = 'name' . substr($url, $pos + 1); + } + + if (false === strpos($url, '://') && 0 !== strpos($url, '/')) { + $info = parse_url($url); + $url = !empty($info['path']) ? $info['path'] : ''; + + if (isset($info['fragment'])) { + // 解析锚点 + $anchor = $info['fragment']; + + if (false !== strpos($anchor, '?')) { + // 解析参数 + list($anchor, $info['query']) = explode('?', $anchor, 2); + } + + if (false !== strpos($anchor, '@')) { + // 解析域名 + list($anchor, $domain) = explode('@', $anchor, 2); + } + } elseif (strpos($url, '@') && false === strpos($url, '\\')) { + // 解析域名 + list($url, $domain) = explode('@', $url, 2); + } + } + + // 解析参数 + if (is_string($vars)) { + // aaa=1&bbb=2 转换成数组 + parse_str($vars, $vars); + } + + if ($url) { + $checkName = isset($name) ? $name : $url . (isset($info['query']) ? '?' . $info['query'] : ''); + $checkDomain = $domain && is_string($domain) ? $domain : null; + + $rule = $this->app['route']->getName($checkName, $checkDomain); + + if (is_null($rule) && isset($info['query'])) { + $rule = $this->app['route']->getName($url); + // 解析地址里面参数 合并到vars + parse_str($info['query'], $params); + $vars = array_merge($params, $vars); + unset($info['query']); + } + } + + if (!empty($rule) && $match = $this->getRuleUrl($rule, $vars, $domain)) { + // 匹配路由命名标识 + $url = $match[0]; + + if ($domain) { + $domain = $match[1]; + } + + if (!is_null($match[2])) { + $suffix = $match[2]; + } + } elseif (!empty($rule) && isset($name)) { + throw new \InvalidArgumentException('route name not exists:' . $name); + } else { + // 检查别名路由 + $alias = $this->app['route']->getAlias(); + $matchAlias = false; + + if ($alias) { + // 别名路由解析 + foreach ($alias as $key => $item) { + $val = $item->getRoute(); + + if (0 === strpos($url, $val)) { + $url = $key . substr($url, strlen($val)); + $matchAlias = true; + break; + } + } + } + + if (!$matchAlias) { + // 路由标识不存在 直接解析 + $url = $this->parseUrl($url); + } + + // 检测URL绑定 + if (!$this->bindCheck) { + $bind = $this->app['route']->getBind($domain && is_string($domain) ? $domain : null); + + if ($bind && 0 === strpos($url, $bind)) { + $url = substr($url, strlen($bind) + 1); + } else { + $binds = $this->app['route']->getBind(true); + + foreach ($binds as $key => $val) { + if (is_string($val) && 0 === strpos($url, $val) && substr_count($val, '/') > 1) { + $url = substr($url, strlen($val) + 1); + $domain = $key; + break; + } + } + } + } + + if (isset($info['query'])) { + // 解析地址里面参数 合并到vars + parse_str($info['query'], $params); + $vars = array_merge($params, $vars); + } + } + + // 还原URL分隔符 + $depr = $this->config['pathinfo_depr']; + $url = str_replace('/', $depr, $url); + + // URL后缀 + if ('/' == substr($url, -1) || '' == $url) { + $suffix = ''; + } else { + $suffix = $this->parseSuffix($suffix); + } + + // 锚点 + $anchor = !empty($anchor) ? '#' . $anchor : ''; + + // 参数组装 + if (!empty($vars)) { + // 添加参数 + if ($this->config['url_common_param']) { + $vars = http_build_query($vars); + $url .= $suffix . '?' . $vars . $anchor; + } else { + $paramType = $this->config['url_param_type']; + + foreach ($vars as $var => $val) { + if ('' !== trim($val)) { + if ($paramType) { + $url .= $depr . urlencode($val); + } else { + $url .= $depr . $var . $depr . urlencode($val); + } + } + } + + $url .= $suffix . $anchor; + } + } else { + $url .= $suffix . $anchor; + } + + // 检测域名 + $domain = $this->parseDomain($url, $domain); + + // URL组装 + $url = $domain . rtrim($this->root ?: $this->app['request']->root(), '/') . '/' . ltrim($url, '/'); + + $this->bindCheck = false; + + return $url; + } + + // 直接解析URL地址 + protected function parseUrl($url) + { + $request = $this->app['request']; + + if (0 === strpos($url, '/')) { + // 直接作为路由地址解析 + $url = substr($url, 1); + } elseif (false !== strpos($url, '\\')) { + // 解析到类 + $url = ltrim(str_replace('\\', '/', $url), '/'); + } elseif (0 === strpos($url, '@')) { + // 解析到控制器 + $url = substr($url, 1); + } else { + // 解析到 模块/控制器/操作 + $module = $request->module(); + $module = $module ? $module . '/' : ''; + $controller = $request->controller(); + + if ('' == $url) { + $action = $request->action(); + } else { + $path = explode('/', $url); + $action = array_pop($path); + $controller = empty($path) ? $controller : array_pop($path); + $module = empty($path) ? $module : array_pop($path) . '/'; + } + + if ($this->config['url_convert']) { + $action = strtolower($action); + $controller = Loader::parseName($controller); + } + + $url = $module . $controller . '/' . $action; + } + + return $url; + } + + // 检测域名 + protected function parseDomain(&$url, $domain) + { + if (!$domain) { + return ''; + } + + $rootDomain = $this->app['request']->rootDomain(); + if (true === $domain) { + // 自动判断域名 + $domain = $this->config['app_host'] ?: $this->app['request']->host(); + + $domains = $this->app['route']->getDomains(); + + if ($domains) { + $route_domain = array_keys($domains); + foreach ($route_domain as $domain_prefix) { + if (0 === strpos($domain_prefix, '*.') && strpos($domain, ltrim($domain_prefix, '*.')) !== false) { + foreach ($domains as $key => $rule) { + $rule = is_array($rule) ? $rule[0] : $rule; + if (is_string($rule) && false === strpos($key, '*') && 0 === strpos($url, $rule)) { + $url = ltrim($url, $rule); + $domain = $key; + + // 生成对应子域名 + if (!empty($rootDomain)) { + $domain .= $rootDomain; + } + break; + } elseif (false !== strpos($key, '*')) { + if (!empty($rootDomain)) { + $domain .= $rootDomain; + } + + break; + } + } + } + } + } + } elseif (0 !== strpos($domain, $rootDomain) && false === strpos($domain, '.')) { + $domain .= '.' . $rootDomain; + } + + if (false !== strpos($domain, '://')) { + $scheme = ''; + } else { + $scheme = $this->app['request']->isSsl() || $this->config['is_https'] ? 'https://' : 'http://'; + + } + + return $scheme . $domain; + } + + // 解析URL后缀 + protected function parseSuffix($suffix) + { + if ($suffix) { + $suffix = true === $suffix ? $this->config['url_html_suffix'] : $suffix; + + if ($pos = strpos($suffix, '|')) { + $suffix = substr($suffix, 0, $pos); + } + } + + return (empty($suffix) || 0 === strpos($suffix, '.')) ? $suffix : '.' . $suffix; + } + + // 匹配路由地址 + public function getRuleUrl($rule, &$vars = [], $allowDomain = '') + { + $port = $this->app['request']->port(); + foreach ($rule as $item) { + list($url, $pattern, $domain, $suffix, $method) = $item; + + if (is_string($allowDomain) && $domain != $allowDomain) { + continue; + } + + if ($port && !in_array($port, [80, 443])) { + $domain .= ':' . $port; + } + + if (empty($pattern)) { + return [rtrim($url, '?/-'), $domain, $suffix]; + } + + $type = $this->config['url_common_param']; + $keys = []; + + foreach ($pattern as $key => $val) { + if (isset($vars[$key])) { + $url = str_replace(['[:' . $key . ']', '<' . $key . '?>', ':' . $key, '<' . $key . '>'], $type ? $vars[$key] : urlencode($vars[$key]), $url); + $keys[] = $key; + $url = str_replace(['/?', '-?'], ['/', '-'], $url); + $result = [rtrim($url, '?/-'), $domain, $suffix]; + } elseif (2 == $val) { + $url = str_replace(['/[:' . $key . ']', '[:' . $key . ']', '<' . $key . '?>'], '', $url); + $url = str_replace(['/?', '-?'], ['/', '-'], $url); + $result = [rtrim($url, '?/-'), $domain, $suffix]; + } else { + $result = null; + $keys = []; + break; + } + } + + $vars = array_diff_key($vars, array_flip($keys)); + + if (isset($result)) { + return $result; + } + } + + return false; + } + + // 指定当前生成URL地址的root + public function root($root) + { + $this->root = $root; + $this->app['request']->setRoot($root); + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app']); + + return $data; + } +} diff --git a/thinkphp/library/think/Validate.php b/thinkphp/library/think/Validate.php new file mode 100644 index 0000000..5fde7f3 --- /dev/null +++ b/thinkphp/library/think/Validate.php @@ -0,0 +1,1556 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ClassNotFoundException; +use think\validate\ValidateRule; + +class Validate +{ + + /** + * 自定义验证类型 + * @var array + */ + protected static $type = []; + + /** + * 验证类型别名 + * @var array + */ + protected $alias = [ + '>' => 'gt', '>=' => 'egt', '<' => 'lt', '<=' => 'elt', '=' => 'eq', 'same' => 'eq', + ]; + + /** + * 当前验证规则 + * @var array + */ + protected $rule = []; + + /** + * 验证提示信息 + * @var array + */ + protected $message = []; + + /** + * 验证字段描述 + * @var array + */ + protected $field = []; + + /** + * 默认规则提示 + * @var array + */ + protected static $typeMsg = [ + 'require' => ':attribute require', + 'must' => ':attribute must', + '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', + 'mobile' => ':attribute not a valid mobile', + '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', + ]; + + /** + * 当前验证场景 + * @var array + */ + protected $currentScene = null; + + /** + * Filter_var 规则 + * @var array + */ + protected $filter = [ + 'email' => FILTER_VALIDATE_EMAIL, + 'ip' => [FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6], + 'integer' => FILTER_VALIDATE_INT, + 'url' => FILTER_VALIDATE_URL, + 'macAddr' => FILTER_VALIDATE_MAC, + 'float' => FILTER_VALIDATE_FLOAT, + ]; + + /** + * 内置正则验证规则 + * @var array + */ + protected $defaultRegex = [ + 'alphaDash' => '/^[A-Za-z0-9\-\_]+$/', + 'chs' => '/^[\x{4e00}-\x{9fa5}]+$/u', + 'chsAlpha' => '/^[\x{4e00}-\x{9fa5}a-zA-Z]+$/u', + 'chsAlphaNum' => '/^[\x{4e00}-\x{9fa5}a-zA-Z0-9]+$/u', + 'chsDash' => '/^[\x{4e00}-\x{9fa5}a-zA-Z0-9\_\-]+$/u', + 'mobile' => '/^1[3-9][0-9]\d{8}$/', + 'idCard' => '/(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$)/', + 'zip' => '/\d{6}/', + ]; + + /** + * 验证场景定义 + * @var array + */ + protected $scene = []; + + /** + * 验证失败错误信息 + * @var array + */ + protected $error = []; + + /** + * 是否批量验证 + * @var bool + */ + protected $batch = false; + + /** + * 场景需要验证的规则 + * @var array + */ + protected $only = []; + + /** + * 场景需要移除的验证规则 + * @var array + */ + protected $remove = []; + + /** + * 场景需要追加的验证规则 + * @var array + */ + protected $append = []; + + /** + * 验证正则定义 + * @var array + */ + protected $regex = []; + + /** + * 架构函数 + * @access public + * @param array $rules 验证规则 + * @param array $message 验证提示信息 + * @param array $field 验证字段描述信息 + */ + public function __construct(array $rules = [], array $message = [], array $field = []) + { + $this->rule = $rules + $this->rule; + $this->message = array_merge($this->message, $message); + $this->field = array_merge($this->field, $field); + } + + /** + * 创建一个验证器类 + * @access public + * @param array $rules 验证规则 + * @param array $message 验证提示信息 + * @param array $field 验证字段描述信息 + * @return Validate + */ + public static function make(array $rules = [], array $message = [], array $field = []) + { + return new self($rules, $message, $field); + } + + /** + * 添加字段验证规则 + * @access protected + * @param string|array $name 字段名称或者规则数组 + * @param mixed $rule 验证规则或者字段描述信息 + * @return $this + */ + public function rule($name, $rule = '') + { + if (is_array($name)) { + $this->rule = $name + $this->rule; + if (is_array($rule)) { + $this->field = array_merge($this->field, $rule); + } + } else { + $this->rule[$name] = $rule; + } + + return $this; + } + + /** + * 注册扩展验证(类型)规则 + * @access public + * @param string $type 验证规则类型 + * @param mixed $callback callback方法(或闭包) + * @return void + */ + public static function extend($type, $callback = null) + { + if (is_array($type)) { + self::$type = array_merge(self::$type, $type); + } else { + self::$type[$type] = $callback; + } + } + + /** + * 设置验证规则的默认提示信息 + * @access public + * @param string|array $type 验证规则类型名称或者数组 + * @param string $msg 验证提示信息 + * @return void + */ + public static function setTypeMsg($type, $msg = null) + { + if (is_array($type)) { + self::$typeMsg = array_merge(self::$typeMsg, $type); + } else { + self::$typeMsg[$type] = $msg; + } + } + + /** + * 设置提示信息 + * @access public + * @param string|array $name 字段名称 + * @param string $message 提示信息 + * @return Validate + */ + public function message($name, $message = '') + { + if (is_array($name)) { + $this->message = array_merge($this->message, $name); + } else { + $this->message[$name] = $message; + } + + return $this; + } + + /** + * 设置验证场景 + * @access public + * @param string $name 场景名 + * @return $this + */ + public function scene($name) + { + // 设置当前场景 + $this->currentScene = $name; + + return $this; + } + + /** + * 判断是否存在某个验证场景 + * @access public + * @param string $name 场景名 + * @return bool + */ + public function hasScene($name) + { + return isset($this->scene[$name]) || method_exists($this, 'scene' . $name); + } + + /** + * 设置批量验证 + * @access public + * @param bool $batch 是否批量验证 + * @return $this + */ + public function batch($batch = true) + { + $this->batch = $batch; + + return $this; + } + + /** + * 指定需要验证的字段列表 + * @access public + * @param array $fields 字段名 + * @return $this + */ + public function only($fields) + { + $this->only = $fields; + + return $this; + } + + /** + * 移除某个字段的验证规则 + * @access public + * @param string|array $field 字段名 + * @param mixed $rule 验证规则 null 移除所有规则 + * @return $this + */ + public function remove($field, $rule = null) + { + if (is_array($field)) { + foreach ($field as $key => $rule) { + if (is_int($key)) { + $this->remove($rule); + } else { + $this->remove($key, $rule); + } + } + } else { + if (is_string($rule)) { + $rule = explode('|', $rule); + } + + $this->remove[$field] = $rule; + } + + return $this; + } + + /** + * 追加某个字段的验证规则 + * @access public + * @param string|array $field 字段名 + * @param mixed $rule 验证规则 + * @return $this + */ + public function append($field, $rule = null) + { + if (is_array($field)) { + foreach ($field as $key => $rule) { + $this->append($key, $rule); + } + } else { + if (is_string($rule)) { + $rule = explode('|', $rule); + } + + $this->append[$field] = $rule; + } + + return $this; + } + + /** + * 数据自动验证 + * @access public + * @param array $data 数据 + * @param mixed $rules 验证规则 + * @param string $scene 验证场景 + * @return bool + */ + public function check($data, $rules = [], $scene = '') + { + $this->error = []; + + if (empty($rules)) { + // 读取验证规则 + $rules = $this->rule; + } + + // 获取场景定义 + $this->getScene($scene); + + foreach ($this->append as $key => $rule) { + if (!isset($rules[$key])) { + $rules[$key] = $rule; + unset($this->append[$key]); + } + } + + foreach ($rules as $key => $rule) { + // field => 'rule1|rule2...' field => ['rule1','rule2',...] + if (strpos($key, '|')) { + // 字段|描述 用于指定属性名称 + list($key, $title) = explode('|', $key); + } else { + $title = isset($this->field[$key]) ? $this->field[$key] : $key; + } + + // 场景检测 + if (!empty($this->only) && !in_array($key, $this->only)) { + continue; + } + + // 获取数据 支持多维数组 + $value = $this->getDataValue($data, $key); + + // 字段验证 + if ($rule instanceof \Closure) { + $result = call_user_func_array($rule, [$value, $data, $title, $this]); + } elseif ($rule instanceof ValidateRule) { + // 验证因子 + $result = $this->checkItem($key, $value, $rule->getRule(), $data, $rule->getTitle() ?: $title, $rule->getMsg()); + } else { + $result = $this->checkItem($key, $value, $rule, $data, $title); + } + + if (true !== $result) { + // 没有返回true 则表示验证失败 + if (!empty($this->batch)) { + // 批量验证 + if (is_array($result)) { + $this->error = array_merge($this->error, $result); + } else { + $this->error[$key] = $result; + } + } else { + $this->error = $result; + return false; + } + } + } + + return !empty($this->error) ? false : true; + } + + /** + * 根据验证规则验证数据 + * @access public + * @param mixed $value 字段值 + * @param mixed $rules 验证规则 + * @return bool + */ + public function checkRule($value, $rules) + { + if ($rules instanceof \Closure) { + return call_user_func_array($rules, [$value]); + } elseif ($rules instanceof ValidateRule) { + $rules = $rules->getRule(); + } 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 + * @param string $field 字段名 + * @param mixed $value 字段值 + * @param mixed $rules 验证规则 + * @param array $data 数据 + * @param string $title 字段描述 + * @param array $msg 提示信息 + * @return mixed + */ + protected function checkItem($field, $value, $rules, $data, $title = '', $msg = []) + { + if (isset($this->remove[$field]) && true === $this->remove[$field] && empty($this->append[$field])) { + // 字段已经移除 无需验证 + return true; + } + + // 支持多规则验证 require|in:a,b,c|... 或者 ['require','in'=>'a,b,c',...] + if (is_string($rules)) { + $rules = explode('|', $rules); + } + + if (isset($this->append[$field])) { + // 追加额外的验证规则 + $rules = array_unique(array_merge($rules, $this->append[$field]), SORT_REGULAR); + unset($this->append[$field]); + } + + $i = 0; + $result = true; + + foreach ($rules as $key => $rule) { + if ($rule instanceof \Closure) { + $result = call_user_func_array($rule, [$value, $data]); + $info = is_numeric($key) ? '' : $key; + } else { + // 判断验证类型 + list($type, $rule, $info) = $this->getValidateType($key, $rule); + + if (isset($this->append[$field]) && in_array($info, $this->append[$field])) { + + } elseif (array_key_exists($field, $this->remove) && (null === $this->remove[$field] || in_array($info, $this->remove[$field]))) { + // 规则已经移除 + $i++; + continue; + } + + // 验证类型 + if (isset(self::$type[$type])) { + $result = call_user_func_array(self::$type[$type], [$value, $rule, $data, $field, $title]); + } elseif ('must' == $info || 0 === strpos($info, 'require') || (!is_null($value) && '' !== $value)) { + // 验证数据 + $result = call_user_func_array([$this, $type], [$value, $rule, $data, $field, $title]); + } else { + $result = true; + } + } + + if (false === $result) { + // 验证失败 返回错误信息 + if (!empty($msg[$i])) { + $message = $msg[$i]; + if (is_string($message) && strpos($message, '{%') === 0) { + $message = facade\Lang::get(substr($message, 2, -1)); + } + } else { + $message = $this->getRuleMsg($field, $title, $info, $rule); + } + + return $message; + } elseif (true !== $result) { + // 返回自定义错误信息 + if (is_string($result) && false !== strpos($result, ':')) { + $result = str_replace(':attribute', $title, $result); + + if (strpos($result, ':rule') && is_scalar($rule)) { + $result = str_replace(':rule', (string) $rule, $result); + } + } + + return $result; + } + $i++; + } + + 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 public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @param string $field 字段名 + * @return bool + */ + public function confirm($value, $rule, $data = [], $field = '') + { + if ('' == $rule) { + if (strpos($field, '_confirm')) { + $rule = strstr($field, '_confirm', true); + } else { + $rule = $field . '_confirm'; + } + } + + return $this->getDataValue($data, $rule) === $value; + } + + /** + * 验证是否和某个字段的值是否不同 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function different($value, $rule, $data = []) + { + return $this->getDataValue($data, $rule) != $value; + } + + /** + * 验证是否大于等于某个值 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function egt($value, $rule, $data = []) + { + return $value >= $this->getDataValue($data, $rule); + } + + /** + * 验证是否大于某个值 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function gt($value, $rule, $data) + { + return $value > $this->getDataValue($data, $rule); + } + + /** + * 验证是否小于等于某个值 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function elt($value, $rule, $data = []) + { + return $value <= $this->getDataValue($data, $rule); + } + + /** + * 验证是否小于某个值 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function lt($value, $rule, $data = []) + { + return $value < $this->getDataValue($data, $rule); + } + + /** + * 验证是否等于某个值 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function eq($value, $rule) + { + return $value == $rule; + } + + /** + * 必须验证 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function must($value, $rule = null) + { + return !empty($value) || '0' == $value; + } + + /** + * 验证字段值是否为有效格式 + * @access public + * @param mixed $value 字段值 + * @param string $rule 验证规则 + * @param array $data 验证数据 + * @return bool + */ + public function is($value, $rule, $data = []) + { + switch (Loader::parseName($rule, 1, false)) { + case 'require': + // 必须 + $result = !empty($value) || '0' == $value; + break; + case 'accepted': + // 接受 + $result = in_array($value, ['1', 'on', 'yes']); + break; + case 'date': + // 是否是一个有效日期 + $result = false !== strtotime($value); + break; + case 'activeUrl': + // 是否为有效的网址 + $result = checkdnsrr($value); + break; + case 'boolean': + case 'bool': + // 是否为布尔值 + $result = in_array($value, [true, false, 0, 1, '0', '1'], true); + break; + case 'number': + $result = ctype_digit((string) $value); + break; + case 'alphaNum': + $result = ctype_alnum($value); + break; + case 'array': + // 是否为数组 + $result = is_array($value); + break; + case 'file': + $result = $value instanceof File; + break; + case 'image': + $result = $value instanceof File && in_array($this->getImageType($value->getRealPath()), [1, 2, 3, 6]); + break; + case 'token': + $result = $this->token($value, '__token__', $data); + break; + default: + if (isset(self::$type[$rule])) { + // 注册的验证规则 + $result = call_user_func_array(self::$type[$rule], [$value]); + } elseif (function_exists('ctype_' . $rule)) { + // ctype验证规则 + $ctypeFun = 'ctype_' . $rule; + $result = $ctypeFun($value); + } elseif (isset($this->filter[$rule])) { + // Filter_var验证规则 + $result = $this->filter($value, $this->filter[$rule]); + } else { + // 正则验证 + $result = $this->regex($value, $rule); + } + } + + return $result; + } + + // 判断图像类型 + protected function getImageType($image) + { + if (function_exists('exif_imagetype')) { + return exif_imagetype($image); + } + + try { + $info = getimagesize($image); + return $info ? $info[2] : false; + } catch (\Exception $e) { + return false; + } + } + + /** + * 验证是否为合格的域名或者IP 支持A,MX,NS,SOA,PTR,CNAME,AAAA,A6, SRV,NAPTR,TXT 或者 ANY类型 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function activeUrl($value, $rule = 'MX') + { + if (!in_array($rule, ['A', 'MX', 'NS', 'SOA', 'PTR', 'CNAME', 'AAAA', 'A6', 'SRV', 'NAPTR', 'TXT', 'ANY'])) { + $rule = 'MX'; + } + + return checkdnsrr($value, $rule); + } + + /** + * 验证是否有效IP + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 ipv4 ipv6 + * @return bool + */ + public function ip($value, $rule = 'ipv4') + { + if (!in_array($rule, ['ipv4', 'ipv6'])) { + $rule = 'ipv4'; + } + + return $this->filter($value, [FILTER_VALIDATE_IP, 'ipv6' == $rule ? FILTER_FLAG_IPV6 : FILTER_FLAG_IPV4]); + } + + /** + * 验证上传文件后缀 + * @access public + * @param mixed $file 上传文件 + * @param mixed $rule 验证规则 + * @return bool + */ + public function fileExt($file, $rule) + { + if (is_array($file)) { + foreach ($file as $item) { + if (!($item instanceof File) || !$item->checkExt($rule)) { + return false; + } + } + return true; + } elseif ($file instanceof File) { + return $file->checkExt($rule); + } + + return false; + } + + /** + * 验证上传文件类型 + * @access public + * @param mixed $file 上传文件 + * @param mixed $rule 验证规则 + * @return bool + */ + public function fileMime($file, $rule) + { + if (is_array($file)) { + foreach ($file as $item) { + if (!($item instanceof File) || !$item->checkMime($rule)) { + return false; + } + } + return true; + } elseif ($file instanceof File) { + return $file->checkMime($rule); + } + + return false; + } + + /** + * 验证上传文件大小 + * @access public + * @param mixed $file 上传文件 + * @param mixed $rule 验证规则 + * @return bool + */ + public function fileSize($file, $rule) + { + if (is_array($file)) { + foreach ($file as $item) { + if (!($item instanceof File) || !$item->checkSize($rule)) { + return false; + } + } + return true; + } elseif ($file instanceof File) { + return $file->checkSize($rule); + } + + return false; + } + + /** + * 验证图片的宽高及类型 + * @access public + * @param mixed $file 上传文件 + * @param mixed $rule 验证规则 + * @return bool + */ + public function image($file, $rule) + { + if (!($file instanceof File)) { + return false; + } + + if ($rule) { + $rule = explode(',', $rule); + + list($width, $height, $type) = getimagesize($file->getRealPath()); + + if (isset($rule[2])) { + $imageType = strtolower($rule[2]); + + if ('jpg' == $imageType) { + $imageType = 'jpeg'; + } + + if (image_type_to_extension($type, false) != $imageType) { + return false; + } + } + + list($w, $h) = $rule; + + return $w == $width && $h == $height; + } + + return in_array($this->getImageType($file->getRealPath()), [1, 2, 3, 6]); + } + + /** + * 验证请求类型 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function method($value, $rule) + { + $method = Container::get('request')->method(); + return strtoupper($rule) == $method; + } + + /** + * 验证时间和日期是否符合指定格式 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function dateFormat($value, $rule) + { + $info = date_parse_from_format($rule, $value); + return 0 == $info['warning_count'] && 0 == $info['error_count']; + } + + /** + * 验证是否唯一 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 格式:数据表,字段名,排除ID,主键名 + * @param array $data 数据 + * @param string $field 验证字段名 + * @return bool + */ + public function unique($value, $rule, $data, $field) + { + if (is_string($rule)) { + $rule = explode(',', $rule); + } + + if (false !== strpos($rule[0], '\\')) { + // 指定模型类 + $db = new $rule[0]; + } else { + try { + $db = Container::get('app')->model($rule[0]); + } catch (ClassNotFoundException $e) { + $db = Db::name($rule[0]); + } + } + + $key = isset($rule[1]) ? $rule[1] : $field; + + if (strpos($key, '^')) { + // 支持多个字段验证 + $fields = explode('^', $key); + foreach ($fields as $key) { + if (isset($data[$key])) { + $map[] = [$key, '=', $data[$key]]; + } + } + } elseif (strpos($key, '=')) { + parse_str($key, $map); + } elseif (isset($data[$field])) { + $map[] = [$key, '=', $data[$field]]; + } else { + $map = []; + } + + $pk = !empty($rule[3]) ? $rule[3] : $db->getPk(); + + if (is_string($pk)) { + if (isset($rule[2])) { + $map[] = [$pk, '<>', $rule[2]]; + } elseif (isset($data[$pk])) { + $map[] = [$pk, '<>', $data[$pk]]; + } + } + + if ($db->where($map)->field($pk)->find()) { + return false; + } + + return true; + } + + /** + * 使用行为类验证 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return mixed + */ + public function behavior($value, $rule, $data) + { + return Container::get('hook')->exec($rule, $data); + } + + /** + * 使用filter_var方式验证 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function filter($value, $rule) + { + if (is_string($rule) && strpos($rule, ',')) { + list($rule, $param) = explode(',', $rule); + } elseif (is_array($rule)) { + $param = isset($rule[1]) ? $rule[1] : null; + $rule = $rule[0]; + } else { + $param = null; + } + + return false !== filter_var($value, is_int($rule) ? $rule : filter_id($rule), $param); + } + + /** + * 验证某个字段等于某个值的时候必须 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function requireIf($value, $rule, $data) + { + list($field, $val) = explode(',', $rule); + + if ($this->getDataValue($data, $field) == $val) { + return !empty($value) || '0' == $value; + } + + return true; + } + + /** + * 通过回调方法验证某个字段是否必须 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function requireCallback($value, $rule, $data) + { + $result = call_user_func_array([$this, $rule], [$value, $data]); + + if ($result) { + return !empty($value) || '0' == $value; + } + + return true; + } + + /** + * 验证某个字段有值的情况下必须 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function requireWith($value, $rule, $data) + { + $val = $this->getDataValue($data, $rule); + + if (!empty($val)) { + return !empty($value) || '0' == $value; + } + + return true; + } + + /** + * 验证是否在范围内 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function in($value, $rule) + { + return in_array($value, is_array($rule) ? $rule : explode(',', $rule)); + } + + /** + * 验证是否不在某个范围 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function notIn($value, $rule) + { + return !in_array($value, is_array($rule) ? $rule : explode(',', $rule)); + } + + /** + * between验证数据 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function between($value, $rule) + { + if (is_string($rule)) { + $rule = explode(',', $rule); + } + list($min, $max) = $rule; + + return $value >= $min && $value <= $max; + } + + /** + * 使用notbetween验证数据 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function notBetween($value, $rule) + { + if (is_string($rule)) { + $rule = explode(',', $rule); + } + list($min, $max) = $rule; + + return $value < $min || $value > $max; + } + + /** + * 验证数据长度 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function length($value, $rule) + { + if (is_array($value)) { + $length = count($value); + } elseif ($value instanceof File) { + $length = $value->getSize(); + } else { + $length = mb_strlen((string) $value); + } + + if (strpos($rule, ',')) { + // 长度区间 + list($min, $max) = explode(',', $rule); + return $length >= $min && $length <= $max; + } + + // 指定长度 + return $length == $rule; + } + + /** + * 验证数据最大长度 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function max($value, $rule) + { + if (is_array($value)) { + $length = count($value); + } elseif ($value instanceof File) { + $length = $value->getSize(); + } else { + $length = mb_strlen((string) $value); + } + + return $length <= $rule; + } + + /** + * 验证数据最小长度 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function min($value, $rule) + { + if (is_array($value)) { + $length = count($value); + } elseif ($value instanceof File) { + $length = $value->getSize(); + } else { + $length = mb_strlen((string) $value); + } + + return $length >= $rule; + } + + /** + * 验证日期 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function after($value, $rule, $data) + { + return strtotime($value) >= strtotime($rule); + } + + /** + * 验证日期 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public 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 public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function expire($value, $rule) + { + if (is_string($rule)) { + $rule = explode(',', $rule); + } + + list($start, $end) = $rule; + + if (!is_numeric($start)) { + $start = strtotime($start); + } + + if (!is_numeric($end)) { + $end = strtotime($end); + } + + return $_SERVER['REQUEST_TIME'] >= $start && $_SERVER['REQUEST_TIME'] <= $end; + } + + /** + * 验证IP许可 + * @access public + * @param string $value 字段值 + * @param mixed $rule 验证规则 + * @return mixed + */ + public function allowIp($value, $rule) + { + return in_array($value, is_array($rule) ? $rule : explode(',', $rule)); + } + + /** + * 验证IP禁用 + * @access public + * @param string $value 字段值 + * @param mixed $rule 验证规则 + * @return mixed + */ + public function denyIp($value, $rule) + { + return !in_array($value, is_array($rule) ? $rule : explode(',', $rule)); + } + + /** + * 使用正则验证数据 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 正则规则或者预定义正则名 + * @return bool + */ + public function regex($value, $rule) + { + if (isset($this->regex[$rule])) { + $rule = $this->regex[$rule]; + } elseif (isset($this->defaultRegex[$rule])) { + $rule = $this->defaultRegex[$rule]; + } + + if (0 !== strpos($rule, '/') && !preg_match('/\/[imsU]{0,4}$/', $rule)) { + // 不是正则表达式则两端补上/ + $rule = '/^' . $rule . '$/'; + } + + return is_scalar($value) && 1 === preg_match($rule, (string) $value); + } + + /** + * 验证表单令牌 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function token($value, $rule, $data) + { + $rule = !empty($rule) ? $rule : '__token__'; + $session = Container::get('session'); + + if (!isset($data[$rule]) || !$session->has($rule)) { + // 令牌数据无效 + return false; + } + + // 令牌验证 + if (isset($data[$rule]) && $session->get($rule) === $data[$rule]) { + // 防止重复提交 + $session->delete($rule); // 验证完成销毁session + return true; + } + + // 开启TOKEN重置 + $session->delete($rule); + + return false; + } + + // 获取错误信息 + public function getError() + { + return $this->error; + } + + /** + * 获取数据值 + * @access protected + * @param array $data 数据 + * @param string $key 数据标识 支持多维 + * @return mixed + */ + protected function getDataValue($data, $key) + { + if (is_numeric($key)) { + $value = $key; + } elseif (strpos($key, '.')) { + // 支持多维数组验证 + foreach (explode('.', $key) as $key) { + if (!isset($data[$key])) { + $value = null; + break; + } + $value = $data = $data[$key]; + } + } else { + $value = isset($data[$key]) ? $data[$key] : null; + } + + return $value; + } + + /** + * 获取验证规则的错误提示信息 + * @access protected + * @param string $attribute 字段英文名 + * @param string $title 字段描述名 + * @param string $type 验证规则名称 + * @param mixed $rule 验证规则数据 + * @return string + */ + protected function getRuleMsg($attribute, $title, $type, $rule) + { + $lang = Container::get('lang'); + + if (isset($this->message[$attribute . '.' . $type])) { + $msg = $this->message[$attribute . '.' . $type]; + } elseif (isset($this->message[$attribute][$type])) { + $msg = $this->message[$attribute][$type]; + } elseif (isset($this->message[$attribute])) { + $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 . $lang->get('not conform to the rules'); + } + + if (!is_string($msg)) { + return $msg; + } + + if (0 === strpos($msg, '{%')) { + $msg = $lang->get(substr($msg, 2, -1)); + } elseif ($lang->has($msg)) { + $msg = $lang->get($msg); + } + + if (is_scalar($rule) && false !== strpos($msg, ':')) { + // 变量替换 + if (is_string($rule) && strpos($rule, ',')) { + $array = array_pad(explode(',', $rule), 3, ''); + } else { + $array = array_pad([], 3, ''); + } + $msg = str_replace( + [':attribute', ':1', ':2', ':3'], + [$title, $array[0], $array[1], $array[2]], + $msg); + if (strpos($msg, ':rule')) { + $msg = str_replace(':rule', (string) $rule, $msg); + } + } + + return $msg; + } + + /** + * 获取数据验证的场景 + * @access protected + * @param string $scene 验证场景 + * @return void + */ + protected function getScene($scene = '') + { + if (empty($scene)) { + // 读取指定场景 + $scene = $this->currentScene; + } + + $this->only = $this->append = $this->remove = []; + + if (empty($scene)) { + return; + } + + if (method_exists($this, 'scene' . $scene)) { + call_user_func([$this, 'scene' . $scene]); + } elseif (isset($this->scene[$scene])) { + // 如果设置了验证适用场景 + $scene = $this->scene[$scene]; + + if (is_string($scene)) { + $scene = explode(',', $scene); + } + + $this->only = $scene; + } + } + + /** + * 动态方法 直接调用is方法进行验证 + * @access public + * @param string $method 方法名 + * @param array $args 调用参数 + * @return bool + */ + public function __call($method, $args) + { + if ('is' == strtolower(substr($method, 0, 2))) { + $method = substr($method, 2); + } + + array_push($args, lcfirst($method)); + + return call_user_func_array([$this, 'is'], $args); + } +} diff --git a/thinkphp/library/think/View.php b/thinkphp/library/think/View.php new file mode 100644 index 0000000..284dd41 --- /dev/null +++ b/thinkphp/library/think/View.php @@ -0,0 +1,253 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class View +{ + /** + * 模板引擎实例 + * @var object + */ + public $engine; + + /** + * 模板变量 + * @var array + */ + protected $data = []; + + /** + * 内容过滤 + * @var mixed + */ + protected $filter; + + /** + * 全局模板变量 + * @var array + */ + protected static $var = []; + + /** + * 初始化 + * @access public + * @param mixed $engine 模板引擎参数 + * @return $this + */ + public function init($engine = []) + { + // 初始化模板引擎 + $this->engine($engine); + + return $this; + } + + public static function __make(Config $config) + { + return (new static())->init($config->pull('template')); + } + + /** + * 模板变量静态赋值 + * @access public + * @param mixed $name 变量名 + * @param mixed $value 变量值 + * @return $this + */ + public function share($name, $value = '') + { + if (is_array($name)) { + self::$var = array_merge(self::$var, $name); + } else { + self::$var[$name] = $value; + } + + return $this; + } + + /** + * 清理模板变量 + * @access public + * @return void + */ + public function clear() + { + self::$var = []; + $this->data = []; + } + + /** + * 模板变量赋值 + * @access public + * @param mixed $name 变量名 + * @param mixed $value 变量值 + * @return $this + */ + public function assign($name, $value = '') + { + if (is_array($name)) { + $this->data = array_merge($this->data, $name); + } else { + $this->data[$name] = $value; + } + + return $this; + } + + /** + * 设置当前模板解析的引擎 + * @access public + * @param array|string $options 引擎参数 + * @return $this + */ + public function engine($options = []) + { + if (is_string($options)) { + $type = $options; + $options = []; + } else { + $type = !empty($options['type']) ? $options['type'] : 'Think'; + } + + if (isset($options['type'])) { + unset($options['type']); + } + + $this->engine = Loader::factory($type, '\\think\\view\\driver\\', $options); + + return $this; + } + + /** + * 配置模板引擎 + * @access public + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @return $this + */ + public function config($name, $value = null) + { + $this->engine->config($name, $value); + + return $this; + } + + /** + * 检查模板是否存在 + * @access public + * @param string|array $name 参数名 + * @return bool + */ + public function exists($name) + { + return $this->engine->exists($name); + } + + /** + * 视图过滤 + * @access public + * @param Callable $filter 过滤方法或闭包 + * @return $this + */ + public function filter($filter) + { + if ($filter) { + $this->filter = $filter; + } + + return $this; + } + + /** + * 解析和获取模板内容 用于输出 + * @access public + * @param string $template 模板文件名或者内容 + * @param array $vars 模板输出变量 + * @param array $config 模板参数 + * @param bool $renderContent 是否渲染内容 + * @return string + * @throws \Exception + */ + public function fetch($template = '', $vars = [], $config = [], $renderContent = false) + { + // 模板变量 + $vars = array_merge(self::$var, $this->data, $vars); + + // 页面缓存 + ob_start(); + ob_implicit_flush(0); + + // 渲染输出 + try { + $method = $renderContent ? 'display' : 'fetch'; + $this->engine->$method($template, $vars, $config); + } catch (\Exception $e) { + ob_end_clean(); + throw $e; + } + + // 获取并清空缓存 + $content = ob_get_clean(); + + if ($this->filter) { + $content = call_user_func_array($this->filter, [$content]); + } + + return $content; + } + + /** + * 渲染内容输出 + * @access public + * @param string $content 内容 + * @param array $vars 模板输出变量 + * @param array $config 模板参数 + * @return mixed + */ + public function display($content, $vars = [], $config = []) + { + return $this->fetch($content, $vars, $config, true); + } + + /** + * 模板变量赋值 + * @access public + * @param string $name 变量名 + * @param mixed $value 变量值 + */ + public function __set($name, $value) + { + $this->data[$name] = $value; + } + + /** + * 取得模板显示变量的值 + * @access protected + * @param string $name 模板变量 + * @return mixed + */ + public function __get($name) + { + return $this->data[$name]; + } + + /** + * 检测模板变量是否设置 + * @access public + * @param string $name 模板变量名 + * @return bool + */ + public function __isset($name) + { + return isset($this->data[$name]); + } +} diff --git a/thinkphp/library/think/cache/Driver.php b/thinkphp/library/think/cache/Driver.php new file mode 100644 index 0000000..6421681 --- /dev/null +++ b/thinkphp/library/think/cache/Driver.php @@ -0,0 +1,366 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache; + +use think\Container; + +/** + * 缓存基础类 + */ +abstract class Driver +{ + /** + * 驱动句柄 + * @var object + */ + protected $handler = null; + + /** + * 缓存读取次数 + * @var integer + */ + protected $readTimes = 0; + + /** + * 缓存写入次数 + * @var integer + */ + protected $writeTimes = 0; + + /** + * 缓存参数 + * @var array + */ + protected $options = []; + + /** + * 缓存标签 + * @var string + */ + protected $tag; + + /** + * 序列化方法 + * @var array + */ + protected static $serialize = ['serialize', 'unserialize', 'think_serialize:', 16]; + + /** + * 判断缓存是否存在 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + abstract public function has($name); + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + abstract public function get($name, $default = false); + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 + * @return boolean + */ + abstract public function set($name, $value, $expire = null); + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + abstract public function inc($name, $step = 1); + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + abstract public function dec($name, $step = 1); + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + abstract public function rm($name); + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + abstract public function clear($tag = null); + + /** + * 获取有效期 + * @access protected + * @param integer|\DateTime $expire 有效期 + * @return integer + */ + protected function getExpireTime($expire) + { + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp() - time(); + } + + return $expire; + } + + /** + * 获取实际的缓存标识 + * @access protected + * @param string $name 缓存名 + * @return string + */ + protected function getCacheKey($name) + { + return $this->options['prefix'] . $name; + } + + /** + * 读取缓存并删除 + * @access public + * @param string $name 缓存变量名 + * @return mixed + */ + public function pull($name) + { + $result = $this->get($name, false); + + if ($result) { + $this->rm($name); + return $result; + } else { + return; + } + } + + /** + * 如果不存在则写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 + * @return mixed + */ + public function remember($name, $value, $expire = null) + { + if (!$this->has($name)) { + $time = time(); + while ($time + 5 > time() && $this->has($name . '_lock')) { + // 存在锁定则等待 + usleep(200000); + } + + try { + // 锁定 + $this->set($name . '_lock', true); + + if ($value instanceof \Closure) { + // 获取缓存数据 + $value = Container::getInstance()->invokeFunction($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; + } + } else { + $value = $this->get($name); + } + + return $value; + } + + /** + * 缓存标签 + * @access public + * @param string $name 标签名 + * @param string|array $keys 缓存标识 + * @param bool $overlay 是否覆盖 + * @return $this + */ + public function tag($name, $keys = null, $overlay = false) + { + if (is_null($name)) { + + } elseif (is_null($keys)) { + $this->tag = $name; + } else { + $key = $this->getTagkey($name); + + if (is_string($keys)) { + $keys = explode(',', $keys); + } + + $keys = array_map([$this, 'getCacheKey'], $keys); + + if ($overlay) { + $value = $keys; + } else { + $value = array_unique(array_merge($this->getTagItem($name), $keys)); + } + + $this->set($key, implode(',', $value), 0); + } + + return $this; + } + + /** + * 更新标签 + * @access protected + * @param string $name 缓存标识 + * @return void + */ + protected function setTagItem($name) + { + if ($this->tag) { + $key = $this->getTagkey($this->tag); + $this->tag = null; + + if ($this->has($key)) { + $value = explode(',', $this->get($key)); + $value[] = $name; + + if (count($value) > 1000) { + array_shift($value); + } + + $value = implode(',', array_unique($value)); + } else { + $value = $name; + } + + $this->set($key, $value, 0); + } + } + + /** + * 获取标签包含的缓存标识 + * @access protected + * @param string $tag 缓存标签 + * @return array + */ + protected function getTagItem($tag) + { + $key = $this->getTagkey($tag); + $value = $this->get($key); + + if ($value) { + return array_filter(explode(',', $value)); + } else { + return []; + } + } + + protected function getTagKey($tag) + { + return 'tag_' . md5($tag); + } + + /** + * 序列化数据 + * @access protected + * @param mixed $data + * @return string + */ + protected function serialize($data) + { + if (is_scalar($data) || !$this->options['serialize']) { + return $data; + } + + $serialize = self::$serialize[0]; + + return self::$serialize[2] . $serialize($data); + } + + /** + * 反序列化数据 + * @access protected + * @param string $data + * @return mixed + */ + protected function unserialize($data) + { + if ($this->options['serialize'] && 0 === strpos($data, self::$serialize[2])) { + $unserialize = self::$serialize[1]; + + return $unserialize(substr($data, self::$serialize[3])); + } else { + return $data; + } + } + + /** + * 注册序列化机制 + * @access public + * @param callable $serialize 序列化方法 + * @param callable $unserialize 反序列化方法 + * @param string $prefix 序列化前缀标识 + * @return $this + */ + public static function registerSerialize($serialize, $unserialize, $prefix = 'think_serialize:') + { + self::$serialize = [$serialize, $unserialize, $prefix, strlen($prefix)]; + } + + /** + * 返回句柄对象,可执行其它高级方法 + * + * @access public + * @return object + */ + public function handler() + { + return $this->handler; + } + + public function getReadTimes() + { + return $this->readTimes; + } + + public function getWriteTimes() + { + return $this->writeTimes; + } + + public function __call($method, $args) + { + return call_user_func_array([$this->handler, $method], $args); + } +} diff --git a/thinkphp/library/think/cache/driver/File.php b/thinkphp/library/think/cache/driver/File.php new file mode 100644 index 0000000..60be08d --- /dev/null +++ b/thinkphp/library/think/cache/driver/File.php @@ -0,0 +1,307 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; +use think\Container; + +/** + * 文件类型缓存类 + * @author liu21st + */ +class File extends Driver +{ + protected $options = [ + 'expire' => 0, + 'cache_subdir' => true, + 'prefix' => '', + 'path' => '', + 'hash_type' => 'md5', + 'data_compress' => false, + 'serialize' => true, + ]; + + protected $expire; + + /** + * 架构函数 + * @param array $options + */ + public function __construct($options = []) + { + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + if (empty($this->options['path'])) { + $this->options['path'] = Container::get('app')->getRuntimePath() . 'cache' . DIRECTORY_SEPARATOR; + } elseif (substr($this->options['path'], -1) != DIRECTORY_SEPARATOR) { + $this->options['path'] .= DIRECTORY_SEPARATOR; + } + + $this->init(); + } + + /** + * 初始化检查 + * @access private + * @return boolean + */ + private function init() + { + // 创建项目缓存目录 + try { + if (!is_dir($this->options['path']) && mkdir($this->options['path'], 0755, true)) { + return true; + } + } catch (\Exception $e) { + } + + return false; + } + + /** + * 取得变量的存储文件名 + * @access protected + * @param string $name 缓存变量名 + * @param bool $auto 是否自动创建目录 + * @return string + */ + protected function getCacheKey($name, $auto = false) + { + $name = hash($this->options['hash_type'], $name); + + if ($this->options['cache_subdir']) { + // 使用子目录 + $name = substr($name, 0, 2) . DIRECTORY_SEPARATOR . substr($name, 2); + } + + if ($this->options['prefix']) { + $name = $this->options['prefix'] . DIRECTORY_SEPARATOR . $name; + } + + $filename = $this->options['path'] . $name . '.php'; + $dir = dirname($filename); + + if ($auto && !is_dir($dir)) { + try { + mkdir($dir, 0755, true); + } catch (\Exception $e) { + } + } + + return $filename; + } + + /** + * 判断缓存是否存在 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + return false !== $this->get($name) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $this->readTimes++; + + $filename = $this->getCacheKey($name); + + if (!is_file($filename)) { + return $default; + } + + $content = file_get_contents($filename); + $this->expire = null; + + if (false !== $content) { + $expire = (int) substr($content, 8, 12); + if (0 != $expire && time() > filemtime($filename) + $expire) { + //缓存过期删除缓存文件 + $this->unlink($filename); + return $default; + } + + $this->expire = $expire; + $content = substr($content, 32); + + if ($this->options['data_compress'] && function_exists('gzcompress')) { + //启用数据压缩 + $content = gzuncompress($content); + } + return $this->unserialize($content); + } else { + return $default; + } + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int|\DateTime $expire 有效时间 0为永久 + * @return boolean + */ + public function set($name, $value, $expire = null) + { + $this->writeTimes++; + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + $expire = $this->getExpireTime($expire); + $filename = $this->getCacheKey($name, true); + + if ($this->tag && !is_file($filename)) { + $first = true; + } + + $data = $this->serialize($value); + + if ($this->options['data_compress'] && function_exists('gzcompress')) { + //数据压缩 + $data = gzcompress($data, 3); + } + + $data = "\n" . $data; + $result = file_put_contents($filename, $data); + + if ($result) { + isset($first) && $this->setTagItem($filename); + clearstatcache(); + return true; + } else { + return false; + } + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) + $step; + $expire = $this->expire; + } else { + $value = $step; + $expire = 0; + } + + return $this->set($name, $value, $expire) ? $value : false; + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) - $step; + $expire = $this->expire; + } else { + $value = -$step; + $expire = 0; + } + + return $this->set($name, $value, $expire) ? $value : false; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + $this->writeTimes++; + + try { + return $this->unlink($this->getCacheKey($name)); + } catch (\Exception $e) { + } + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + foreach ($keys as $key) { + $this->unlink($key); + } + $this->rm($this->getTagKey($tag)); + return true; + } + + $this->writeTimes++; + + $files = (array) glob($this->options['path'] . ($this->options['prefix'] ? $this->options['prefix'] . DIRECTORY_SEPARATOR : '') . '*'); + + foreach ($files as $path) { + if (is_dir($path)) { + $matches = glob($path . DIRECTORY_SEPARATOR . '*.php'); + if (is_array($matches)) { + array_map(function ($v) { + $this->unlink($v); + }, $matches); + } + rmdir($path); + } else { + $this->unlink($path); + } + } + + return true; + } + + /** + * 判断文件是否存在后,删除 + * @access private + * @param string $path + * @return bool + * @author byron sampson + * @return boolean + */ + private function unlink($path) + { + return is_file($path) && unlink($path); + } + +} diff --git a/thinkphp/library/think/cache/driver/Lite.php b/thinkphp/library/think/cache/driver/Lite.php new file mode 100644 index 0000000..0cfe390 --- /dev/null +++ b/thinkphp/library/think/cache/driver/Lite.php @@ -0,0 +1,209 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * 文件类型缓存类 + * @author liu21st + */ +class Lite extends Driver +{ + protected $options = [ + 'prefix' => '', + 'path' => '', + 'expire' => 0, // 等于 10*365*24*3600(10年) + ]; + + /** + * 架构函数 + * @access public + * + * @param array $options + */ + public function __construct($options = []) + { + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + if (substr($this->options['path'], -1) != DIRECTORY_SEPARATOR) { + $this->options['path'] .= DIRECTORY_SEPARATOR; + } + + } + + /** + * 取得变量的存储文件名 + * @access protected + * @param string $name 缓存变量名 + * @return string + */ + protected function getCacheKey($name) + { + return $this->options['path'] . $this->options['prefix'] . md5($name) . '.php'; + } + + /** + * 判断缓存是否存在 + * @access public + * @param string $name 缓存变量名 + * @return mixed + */ + public function has($name) + { + return $this->get($name) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $this->readTimes++; + + $filename = $this->getCacheKey($name); + + if (is_file($filename)) { + // 判断是否过期 + $mtime = filemtime($filename); + + if ($mtime < time()) { + // 清除已经过期的文件 + unlink($filename); + return $default; + } + + return include $filename; + } else { + return $default; + } + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int|\DateTime $expire 有效时间 0为永久 + * @return bool + */ + public function set($name, $value, $expire = null) + { + $this->writeTimes++; + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + 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)) { + $first = true; + } + + $ret = file_put_contents($filename, ("setTagItem($filename); + touch($filename, $expire); + } + + return $ret; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) + $step; + } else { + $value = $step; + } + + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) - $step; + } else { + $value = -$step; + } + + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + $this->writeTimes++; + + return unlink($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return bool + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + foreach ($keys as $key) { + unlink($key); + } + + $this->rm($this->getTagKey($tag)); + return true; + } + + $this->writeTimes++; + + array_map("unlink", glob($this->options['path'] . ($this->options['prefix'] ? $this->options['prefix'] . DIRECTORY_SEPARATOR : '') . '*.php')); + } +} diff --git a/thinkphp/library/think/cache/driver/Memcache.php b/thinkphp/library/think/cache/driver/Memcache.php new file mode 100644 index 0000000..1c53559 --- /dev/null +++ b/thinkphp/library/think/cache/driver/Memcache.php @@ -0,0 +1,206 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +class Memcache extends Driver +{ + protected $options = [ + 'host' => '127.0.0.1', + 'port' => 11211, + 'expire' => 0, + 'timeout' => 0, // 超时时间(单位:毫秒) + 'persistent' => true, + 'prefix' => '', + 'serialize' => true, + ]; + + /** + * 架构函数 + * @access public + * @param array $options 缓存参数 + * @throws \BadFunctionCallException + */ + public function __construct($options = []) + { + if (!extension_loaded('memcache')) { + throw new \BadFunctionCallException('not support: memcache'); + } + + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + $this->handler = new \Memcache; + + // 支持集群 + $hosts = explode(',', $this->options['host']); + $ports = explode(',', $this->options['port']); + + if (empty($ports[0])) { + $ports[0] = 11211; + } + + // 建立连接 + foreach ((array) $hosts as $i => $host) { + $port = isset($ports[$i]) ? $ports[$i] : $ports[0]; + $this->options['timeout'] > 0 ? + $this->handler->addServer($host, $port, $this->options['persistent'], 1, $this->options['timeout']) : + $this->handler->addServer($host, $port, $this->options['persistent'], 1); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $key = $this->getCacheKey($name); + + return false !== $this->handler->get($key); + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $this->readTimes++; + + $result = $this->handler->get($this->getCacheKey($name)); + + return false !== $result ? $this->unserialize($result) : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int|DateTime $expire 有效时间(秒) + * @return bool + */ + public function set($name, $value, $expire = null) + { + $this->writeTimes++; + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + if ($this->tag && !$this->has($name)) { + $first = true; + } + + $key = $this->getCacheKey($name); + $expire = $this->getExpireTime($expire); + $value = $this->serialize($value); + + if ($this->handler->set($key, $value, 0, $expire)) { + isset($first) && $this->setTagItem($key); + return true; + } + + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + if ($this->handler->get($key)) { + return $this->handler->increment($key, $step); + } + + return $this->handler->set($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + $value = $this->handler->get($key) - $step; + $res = $this->handler->set($key, $value); + + return !$res ? false : $value; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @param bool|false $ttl + * @return bool + */ + public function rm($name, $ttl = false) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return false === $ttl ? + $this->handler->delete($key) : + $this->handler->delete($key, $ttl); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return bool + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + + foreach ($keys as $key) { + $this->handler->delete($key); + } + + $tagName = $this->getTagKey($tag); + $this->rm($tagName); + return true; + } + + $this->writeTimes++; + + return $this->handler->flush(); + } + +} diff --git a/thinkphp/library/think/cache/driver/Memcached.php b/thinkphp/library/think/cache/driver/Memcached.php new file mode 100644 index 0000000..4533e78 --- /dev/null +++ b/thinkphp/library/think/cache/driver/Memcached.php @@ -0,0 +1,279 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +class Memcached extends Driver +{ + protected $options = [ + 'host' => '127.0.0.1', + 'port' => 11211, + 'expire' => 0, + 'timeout' => 0, // 超时时间(单位:毫秒) + 'prefix' => '', + 'username' => '', //账号 + 'password' => '', //密码 + 'option' => [], + 'serialize' => true, + ]; + + /** + * 架构函数 + * @access public + * @param array $options 缓存参数 + */ + public function __construct($options = []) + { + if (!extension_loaded('memcached')) { + throw new \BadFunctionCallException('not support: memcached'); + } + + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + $this->handler = new \Memcached; + + if (!empty($this->options['option'])) { + $this->handler->setOptions($this->options['option']); + } + + // 设置连接超时时间(单位:毫秒) + if ($this->options['timeout'] > 0) { + $this->handler->setOption(\Memcached::OPT_CONNECT_TIMEOUT, $this->options['timeout']); + } + + // 支持集群 + $hosts = explode(',', $this->options['host']); + $ports = explode(',', $this->options['port']); + if (empty($ports[0])) { + $ports[0] = 11211; + } + + // 建立连接 + $servers = []; + foreach ((array) $hosts as $i => $host) { + $servers[] = [$host, (isset($ports[$i]) ? $ports[$i] : $ports[0]), 1]; + } + + $this->handler->addServers($servers); + $this->handler->setOption(\Memcached::OPT_COMPRESSION, false); + if ('' != $this->options['username']) { + $this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); + $this->handler->setSaslAuthData($this->options['username'], $this->options['password']); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $key = $this->getCacheKey($name); + + return $this->handler->get($key) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $this->readTimes++; + + $result = $this->handler->get($this->getCacheKey($name)); + + return false !== $result ? $this->unserialize($result) : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) + * @return bool + */ + public function set($name, $value, $expire = null) + { + $this->writeTimes++; + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + if ($this->tag && !$this->has($name)) { + $first = true; + } + + $key = $this->getCacheKey($name); + $expire = $this->getExpireTime($expire); + $value = $this->serialize($value); + + if ($this->handler->set($key, $value, $expire)) { + isset($first) && $this->setTagItem($key); + return true; + } + + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + if ($this->handler->get($key)) { + return $this->handler->increment($key, $step); + } + + return $this->handler->set($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + $value = $this->handler->get($key) - $step; + $res = $this->handler->set($key, $value); + + return !$res ? false : $value; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @param bool|false $ttl + * @return bool + */ + public function rm($name, $ttl = false) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return false === $ttl ? + $this->handler->delete($key) : + $this->handler->delete($key, $ttl); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return bool + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + + $this->handler->deleteMulti($keys); + $this->rm($this->getTagKey($tag)); + + return true; + } + + $this->writeTimes++; + + return $this->handler->flush(); + } + + /** + * 缓存标签 + * @access public + * @param string $name 标签名 + * @param string|array $keys 缓存标识 + * @param bool $overlay 是否覆盖 + * @return $this + */ + public function tag($name, $keys = null, $overlay = false) + { + if (is_null($keys)) { + $this->tag = $name; + } else { + $tagName = $this->getTagKey($name); + if ($overlay) { + $this->handler->delete($tagName); + } + + if (!$this->has($tagName)) { + $this->handler->set($tagName, ''); + } + + foreach ($keys as $key) { + $this->handler->append($tagName, ',' . $key); + } + } + + return $this; + } + + /** + * 更新标签 + * @access protected + * @param string $name 缓存标识 + * @return void + */ + protected function setTagItem($name) + { + if ($this->tag) { + $tagName = $this->getTagKey($this->tag); + + if ($this->has($tagName)) { + $this->handler->append($tagName, ',' . $name); + } else { + $this->handler->set($tagName, $name); + } + + $this->tag = null; + } + } + + /** + * 获取标签包含的缓存标识 + * @access public + * @param string $tag 缓存标签 + * @return array + */ + public function getTagItem($tag) + { + $tagName = $this->getTagKey($tag); + return explode(',', trim($this->handler->get($tagName), ',')); + } +} diff --git a/thinkphp/library/think/cache/driver/Redis.php b/thinkphp/library/think/cache/driver/Redis.php new file mode 100644 index 0000000..4eff2cf --- /dev/null +++ b/thinkphp/library/think/cache/driver/Redis.php @@ -0,0 +1,272 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * Redis缓存驱动,适合单机部署、有前端代理实现高可用的场景,性能最好 + * 有需要在业务层实现读写分离、或者使用RedisCluster的需求,请使用Redisd驱动 + * + * 要求安装phpredis扩展:https://github.com/nicolasff/phpredis + * @author 尘缘 <130775@qq.com> + */ +class Redis extends Driver +{ + protected $options = [ + 'host' => '127.0.0.1', + 'port' => 6379, + 'password' => '', + 'select' => 0, + 'timeout' => 0, + 'expire' => 0, + 'persistent' => false, + 'prefix' => '', + 'serialize' => true, + ]; + + /** + * 架构函数 + * @access public + * @param array $options 缓存参数 + */ + public function __construct($options = []) + { + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + if (extension_loaded('redis')) { + $this->handler = new \Redis; + + 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']); + } + + if (0 != $this->options['select']) { + $this->handler->select($this->options['select']); + } + } elseif (class_exists('\Predis\Client')) { + $params = []; + foreach ($this->options as $key => $val) { + if (in_array($key, ['aggregate', 'cluster', 'connections', 'exceptions', 'prefix', 'profile', 'replication', 'parameters'])) { + $params[$key] = $val; + unset($this->options[$key]); + } + } + + if ('' == $this->options['password']) { + unset($this->options['password']); + } + + $this->handler = new \Predis\Client($this->options, $params); + + $this->options['prefix'] = ''; + } else { + throw new \BadFunctionCallException('not support: redis'); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + return $this->handler->exists($this->getCacheKey($name)) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $this->readTimes++; + + $value = $this->handler->get($this->getCacheKey($name)); + + if (is_null($value) || false === $value) { + return $default; + } + + return $this->unserialize($value); + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) + * @return boolean + */ + public function set($name, $value, $expire = null) + { + $this->writeTimes++; + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + if ($this->tag && !$this->has($name)) { + $first = true; + } + + $key = $this->getCacheKey($name); + $expire = $this->getExpireTime($expire); + + $value = $this->serialize($value); + + if ($expire) { + $result = $this->handler->setex($key, $expire, $value); + } else { + $result = $this->handler->set($key, $value); + } + + isset($first) && $this->setTagItem($key); + + return $result; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return $this->handler->incrby($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return $this->handler->decrby($key, $step); + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + $this->writeTimes++; + + return $this->handler->del($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + + $this->handler->del($keys); + + $tagName = $this->getTagKey($tag); + $this->handler->del($tagName); + return true; + } + + $this->writeTimes++; + + return $this->handler->flushDB(); + } + + /** + * 缓存标签 + * @access public + * @param string $name 标签名 + * @param string|array $keys 缓存标识 + * @param bool $overlay 是否覆盖 + * @return $this + */ + public function tag($name, $keys = null, $overlay = false) + { + if (is_null($keys)) { + $this->tag = $name; + } else { + $tagName = $this->getTagKey($name); + if ($overlay) { + $this->handler->del($tagName); + } + + foreach ($keys as $key) { + $this->handler->sAdd($tagName, $key); + } + } + + return $this; + } + + /** + * 更新标签 + * @access protected + * @param string $name 缓存标识 + * @return void + */ + protected function setTagItem($name) + { + if ($this->tag) { + $tagName = $this->getTagKey($this->tag); + $this->handler->sAdd($tagName, $name); + } + } + + /** + * 获取标签包含的缓存标识 + * @access protected + * @param string $tag 缓存标签 + * @return array + */ + protected function getTagItem($tag) + { + $tagName = $this->getTagKey($tag); + return $this->handler->sMembers($tagName); + } +} diff --git a/thinkphp/library/think/cache/driver/Sqlite.php b/thinkphp/library/think/cache/driver/Sqlite.php new file mode 100644 index 0000000..f57361e --- /dev/null +++ b/thinkphp/library/think/cache/driver/Sqlite.php @@ -0,0 +1,233 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * Sqlite缓存驱动 + * @author liu21st + */ +class Sqlite extends Driver +{ + protected $options = [ + 'db' => ':memory:', + 'table' => 'sharedmemory', + 'prefix' => '', + 'expire' => 0, + 'persistent' => false, + 'serialize' => true, + ]; + + /** + * 架构函数 + * @access public + * @param array $options 缓存参数 + * @throws \BadFunctionCallException + */ + public function __construct($options = []) + { + if (!extension_loaded('sqlite')) { + throw new \BadFunctionCallException('not support: sqlite'); + } + + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + $func = $this->options['persistent'] ? 'sqlite_popen' : 'sqlite_open'; + + $this->handler = $func($this->options['db']); + } + + /** + * 获取实际的缓存标识 + * @access public + * @param string $name 缓存名 + * @return string + */ + protected function getCacheKey($name) + { + return $this->options['prefix'] . sqlite_escape_string($name); + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $name = $this->getCacheKey($name); + + $sql = 'SELECT value FROM ' . $this->options['table'] . ' WHERE var=\'' . $name . '\' AND (expire=0 OR expire >' . time() . ') LIMIT 1'; + $result = sqlite_query($this->handler, $sql); + + return sqlite_num_rows($result); + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $this->readTimes++; + + $name = $this->getCacheKey($name); + + $sql = 'SELECT value FROM ' . $this->options['table'] . ' WHERE var=\'' . $name . '\' AND (expire=0 OR expire >' . time() . ') LIMIT 1'; + + $result = sqlite_query($this->handler, $sql); + + if (sqlite_num_rows($result)) { + $content = sqlite_fetch_single($result); + if (function_exists('gzcompress')) { + //启用数据压缩 + $content = gzuncompress($content); + } + + return $this->unserialize($content); + } + + return $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) + * @return boolean + */ + public function set($name, $value, $expire = null) + { + $this->writeTimes++; + + $name = $this->getCacheKey($name); + + $value = sqlite_escape_string($this->serialize($value)); + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp(); + } else { + $expire = (0 == $expire) ? 0 : (time() + $expire); //缓存有效期为0表示永久缓存 + } + + if (function_exists('gzcompress')) { + //数据压缩 + $value = gzcompress($value, 3); + } + + if ($this->tag) { + $tag = $this->tag; + $this->tag = null; + } else { + $tag = ''; + } + + $sql = 'REPLACE INTO ' . $this->options['table'] . ' (var, value, expire, tag) VALUES (\'' . $name . '\', \'' . $value . '\', \'' . $expire . '\', \'' . $tag . '\')'; + + if (sqlite_query($this->handler, $sql)) { + return true; + } + + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) + $step; + } else { + $value = $step; + } + + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) - $step; + } else { + $value = -$step; + } + + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + $this->writeTimes++; + + $name = $this->getCacheKey($name); + + $sql = 'DELETE FROM ' . $this->options['table'] . ' WHERE var=\'' . $name . '\''; + sqlite_query($this->handler, $sql); + + return true; + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + $name = sqlite_escape_string($this->getTagKey($tag)); + $sql = 'DELETE FROM ' . $this->options['table'] . ' WHERE tag=\'' . $name . '\''; + sqlite_query($this->handler, $sql); + return true; + } + + $this->writeTimes++; + + $sql = 'DELETE FROM ' . $this->options['table']; + + sqlite_query($this->handler, $sql); + + return true; + } +} diff --git a/thinkphp/library/think/cache/driver/Wincache.php b/thinkphp/library/think/cache/driver/Wincache.php new file mode 100644 index 0000000..ef15784 --- /dev/null +++ b/thinkphp/library/think/cache/driver/Wincache.php @@ -0,0 +1,175 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * Wincache缓存驱动 + * @author liu21st + */ +class Wincache extends Driver +{ + protected $options = [ + 'prefix' => '', + 'expire' => 0, + 'serialize' => true, + ]; + + /** + * 架构函数 + * @access public + * @param array $options 缓存参数 + * @throws \BadFunctionCallException + */ + public function __construct($options = []) + { + if (!function_exists('wincache_ucache_info')) { + throw new \BadFunctionCallException('not support: WinCache'); + } + + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $this->readTimes++; + + $key = $this->getCacheKey($name); + + return wincache_ucache_exists($key); + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $this->readTimes++; + + $key = $this->getCacheKey($name); + + return wincache_ucache_exists($key) ? $this->unserialize(wincache_ucache_get($key)) : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) + * @return boolean + */ + public function set($name, $value, $expire = null) + { + $this->writeTimes++; + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + $key = $this->getCacheKey($name); + $expire = $this->getExpireTime($expire); + $value = $this->serialize($value); + + if ($this->tag && !$this->has($name)) { + $first = true; + } + + if (wincache_ucache_set($key, $value, $expire)) { + isset($first) && $this->setTagItem($key); + return true; + } + + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return wincache_ucache_inc($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return wincache_ucache_dec($key, $step); + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + $this->writeTimes++; + + return wincache_ucache_delete($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + $keys = $this->getTagItem($tag); + + wincache_ucache_delete($keys); + + $tagName = $this->getTagkey($tag); + $this->rm($tagName); + return true; + } + + $this->writeTimes++; + return wincache_ucache_clear(); + } + +} diff --git a/thinkphp/library/think/cache/driver/Xcache.php b/thinkphp/library/think/cache/driver/Xcache.php new file mode 100644 index 0000000..4e69859 --- /dev/null +++ b/thinkphp/library/think/cache/driver/Xcache.php @@ -0,0 +1,179 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * Xcache缓存驱动 + * @author liu21st + */ +class Xcache extends Driver +{ + protected $options = [ + 'prefix' => '', + 'expire' => 0, + 'serialize' => true, + ]; + + /** + * 架构函数 + * @access public + * @param array $options 缓存参数 + * @throws \BadFunctionCallException + */ + public function __construct($options = []) + { + if (!function_exists('xcache_info')) { + throw new \BadFunctionCallException('not support: Xcache'); + } + + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $key = $this->getCacheKey($name); + + return xcache_isset($key); + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $this->readTimes++; + + $key = $this->getCacheKey($name); + + return xcache_isset($key) ? $this->unserialize(xcache_get($key)) : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) + * @return boolean + */ + public function set($name, $value, $expire = null) + { + $this->writeTimes++; + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + if ($this->tag && !$this->has($name)) { + $first = true; + } + + $key = $this->getCacheKey($name); + $expire = $this->getExpireTime($expire); + $value = $this->serialize($value); + + if (xcache_set($key, $value, $expire)) { + isset($first) && $this->setTagItem($key); + return true; + } + + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return xcache_inc($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return xcache_dec($key, $step); + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + $this->writeTimes++; + + return xcache_unset($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + + foreach ($keys as $key) { + xcache_unset($key); + } + + $this->rm($this->getTagKey($tag)); + return true; + } + + $this->writeTimes++; + + if (function_exists('xcache_unset_by_prefix')) { + return xcache_unset_by_prefix($this->options['prefix']); + } else { + return false; + } + } +} diff --git a/thinkphp/library/think/config/driver/Ini.php b/thinkphp/library/think/config/driver/Ini.php new file mode 100644 index 0000000..b2a647d --- /dev/null +++ b/thinkphp/library/think/config/driver/Ini.php @@ -0,0 +1,31 @@ + +// +---------------------------------------------------------------------- + +namespace think\config\driver; + +class Ini +{ + protected $config; + + public function __construct($config) + { + $this->config = $config; + } + + public function parse() + { + if (is_file($this->config)) { + return parse_ini_file($this->config, true); + } else { + return parse_ini_string($this->config, true); + } + } +} diff --git a/thinkphp/library/think/config/driver/Json.php b/thinkphp/library/think/config/driver/Json.php new file mode 100644 index 0000000..0d77c8e --- /dev/null +++ b/thinkphp/library/think/config/driver/Json.php @@ -0,0 +1,31 @@ + +// +---------------------------------------------------------------------- + +namespace think\config\driver; + +class Json +{ + protected $config; + + public function __construct($config) + { + if (is_file($config)) { + $config = file_get_contents($config); + } + + $this->config = $config; + } + + public function parse() + { + return json_decode($this->config, true); + } +} diff --git a/thinkphp/library/think/config/driver/Xml.php b/thinkphp/library/think/config/driver/Xml.php new file mode 100644 index 0000000..9d69633 --- /dev/null +++ b/thinkphp/library/think/config/driver/Xml.php @@ -0,0 +1,40 @@ + +// +---------------------------------------------------------------------- + +namespace think\config\driver; + +class Xml +{ + protected $config; + + public function __construct($config) + { + $this->config = $config; + } + + public function parse() + { + if (is_file($this->config)) { + $content = simplexml_load_file($this->config); + } else { + $content = simplexml_load_string($this->config); + } + + $result = (array) $content; + foreach ($result as $key => $val) { + if (is_object($val)) { + $result[$key] = (array) $val; + } + } + + return $result; + } +} diff --git a/thinkphp/library/think/console/Command.php b/thinkphp/library/think/console/Command.php new file mode 100644 index 0000000..a208e7b --- /dev/null +++ b/thinkphp/library/think/console/Command.php @@ -0,0 +1,482 @@ + +// +---------------------------------------------------------------------- + +namespace think\console; + +use think\Console; +use think\console\input\Argument; +use think\console\input\Definition; +use think\console\input\Option; + +class Command +{ + + /** @var Console */ + private $console; + private $name; + private $aliases = []; + private $definition; + private $help; + private $description; + private $ignoreValidationErrors = false; + private $consoleDefinitionMerged = false; + private $consoleDefinitionMergedWithArgs = false; + private $code; + private $synopsis = []; + private $usages = []; + + /** @var Input */ + protected $input; + + /** @var Output */ + protected $output; + + /** + * 构造方法 + * @param string|null $name 命令名称,如果没有设置则比如在 configure() 里设置 + * @throws \LogicException + * @api + */ + public function __construct($name = null) + { + $this->definition = new Definition(); + + if (null !== $name) { + $this->setName($name); + } + + $this->configure(); + + if (!$this->name) { + throw new \LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($this))); + } + } + + /** + * 忽略验证错误 + */ + public function ignoreValidationErrors() + { + $this->ignoreValidationErrors = true; + } + + /** + * 设置控制台 + * @param Console $console + */ + public function setConsole(Console $console = null) + { + $this->console = $console; + } + + /** + * 获取控制台 + * @return Console + * @api + */ + public function getConsole() + { + return $this->console; + } + + /** + * 是否有效 + * @return bool + */ + public function isEnabled() + { + return true; + } + + /** + * 配置指令 + */ + protected function configure() + { + } + + /** + * 执行指令 + * @param Input $input + * @param Output $output + * @return null|int + * @throws \LogicException + * @see setCode() + */ + protected function execute(Input $input, Output $output) + { + throw new \LogicException('You must override the execute() method in the concrete command class.'); + } + + /** + * 用户验证 + * @param Input $input + * @param Output $output + */ + protected function interact(Input $input, Output $output) + { + } + + /** + * 初始化 + * @param Input $input An InputInterface instance + * @param Output $output An OutputInterface instance + */ + protected function initialize(Input $input, Output $output) + { + } + + /** + * 执行 + * @param Input $input + * @param Output $output + * @return int + * @throws \Exception + * @see setCode() + * @see execute() + */ + public function run(Input $input, Output $output) + { + $this->input = $input; + $this->output = $output; + + $this->getSynopsis(true); + $this->getSynopsis(false); + + $this->mergeConsoleDefinition(); + + try { + $input->bind($this->definition); + } catch (\Exception $e) { + if (!$this->ignoreValidationErrors) { + throw $e; + } + } + + $this->initialize($input, $output); + + if ($input->isInteractive()) { + $this->interact($input, $output); + } + + $input->validate(); + + if ($this->code) { + $statusCode = call_user_func($this->code, $input, $output); + } else { + $statusCode = $this->execute($input, $output); + } + + return is_numeric($statusCode) ? (int) $statusCode : 0; + } + + /** + * 设置执行代码 + * @param callable $code callable(InputInterface $input, OutputInterface $output) + * @return Command + * @throws \InvalidArgumentException + * @see execute() + */ + public function setCode(callable $code) + { + if (!is_callable($code)) { + throw new \InvalidArgumentException('Invalid callable provided to Command::setCode.'); + } + + if (PHP_VERSION_ID >= 50400 && $code instanceof \Closure) { + $r = new \ReflectionFunction($code); + if (null === $r->getClosureThis()) { + $code = \Closure::bind($code, $this); + } + } + + $this->code = $code; + + return $this; + } + + /** + * 合并参数定义 + * @param bool $mergeArgs + */ + public function mergeConsoleDefinition($mergeArgs = true) + { + if (null === $this->console + || (true === $this->consoleDefinitionMerged + && ($this->consoleDefinitionMergedWithArgs || !$mergeArgs)) + ) { + return; + } + + if ($mergeArgs) { + $currentArguments = $this->definition->getArguments(); + $this->definition->setArguments($this->console->getDefinition()->getArguments()); + $this->definition->addArguments($currentArguments); + } + + $this->definition->addOptions($this->console->getDefinition()->getOptions()); + + $this->consoleDefinitionMerged = true; + if ($mergeArgs) { + $this->consoleDefinitionMergedWithArgs = true; + } + } + + /** + * 设置参数定义 + * @param array|Definition $definition + * @return Command + * @api + */ + public function setDefinition($definition) + { + if ($definition instanceof Definition) { + $this->definition = $definition; + } else { + $this->definition->setDefinition($definition); + } + + $this->consoleDefinitionMerged = false; + + return $this; + } + + /** + * 获取参数定义 + * @return Definition + * @api + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * 获取当前指令的参数定义 + * @return Definition + */ + public function getNativeDefinition() + { + return $this->getDefinition(); + } + + /** + * 添加参数 + * @param string $name 名称 + * @param int $mode 类型 + * @param string $description 描述 + * @param mixed $default 默认值 + * @return Command + */ + public function addArgument($name, $mode = null, $description = '', $default = null) + { + $this->definition->addArgument(new Argument($name, $mode, $description, $default)); + + return $this; + } + + /** + * 添加选项 + * @param string $name 选项名称 + * @param string $shortcut 别名 + * @param int $mode 类型 + * @param string $description 描述 + * @param mixed $default 默认值 + * @return Command + */ + public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null) + { + $this->definition->addOption(new Option($name, $shortcut, $mode, $description, $default)); + + return $this; + } + + /** + * 设置指令名称 + * @param string $name + * @return Command + * @throws \InvalidArgumentException + */ + public function setName($name) + { + $this->validateName($name); + + $this->name = $name; + + return $this; + } + + /** + * 获取指令名称 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 设置描述 + * @param string $description + * @return Command + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + /** + * 获取描述 + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * 设置帮助信息 + * @param string $help + * @return Command + */ + public function setHelp($help) + { + $this->help = $help; + + return $this; + } + + /** + * 获取帮助信息 + * @return string + */ + public function getHelp() + { + return $this->help; + } + + /** + * 描述信息 + * @return string + */ + public function getProcessedHelp() + { + $name = $this->name; + + $placeholders = [ + '%command.name%', + '%command.full_name%', + ]; + $replacements = [ + $name, + $_SERVER['PHP_SELF'] . ' ' . $name, + ]; + + return str_replace($placeholders, $replacements, $this->getHelp()); + } + + /** + * 设置别名 + * @param string[] $aliases + * @return Command + * @throws \InvalidArgumentException + */ + public function setAliases($aliases) + { + if (!is_array($aliases) && !$aliases instanceof \Traversable) { + throw new \InvalidArgumentException('$aliases must be an array or an instance of \Traversable'); + } + + foreach ($aliases as $alias) { + $this->validateName($alias); + } + + $this->aliases = $aliases; + + return $this; + } + + /** + * 获取别名 + * @return array + */ + public function getAliases() + { + return $this->aliases; + } + + /** + * 获取简介 + * @param bool $short 是否简单的 + * @return string + */ + public function getSynopsis($short = false) + { + $key = $short ? 'short' : 'long'; + + if (!isset($this->synopsis[$key])) { + $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short))); + } + + return $this->synopsis[$key]; + } + + /** + * 添加用法介绍 + * @param string $usage + * @return $this + */ + public function addUsage($usage) + { + if (0 !== strpos($usage, $this->name)) { + $usage = sprintf('%s %s', $this->name, $usage); + } + + $this->usages[] = $usage; + + return $this; + } + + /** + * 获取用法介绍 + * @return array + */ + public function getUsages() + { + return $this->usages; + } + + /** + * 验证指令名称 + * @param string $name + * @throws \InvalidArgumentException + */ + private function validateName($name) + { + if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) { + throw new \InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name)); + } + } + + /** + * 输出表格 + * @param Table $table + * @return string + */ + protected function table(Table $table) + { + $content = $table->render(); + $this->output->writeln($content); + return $content; + } +} diff --git a/thinkphp/library/think/console/Input.php b/thinkphp/library/think/console/Input.php new file mode 100644 index 0000000..2482dfd --- /dev/null +++ b/thinkphp/library/think/console/Input.php @@ -0,0 +1,464 @@ + +// +---------------------------------------------------------------------- + +namespace think\console; + +use think\console\input\Argument; +use think\console\input\Definition; +use think\console\input\Option; + +class Input +{ + + /** + * @var Definition + */ + protected $definition; + + /** + * @var Option[] + */ + protected $options = []; + + /** + * @var Argument[] + */ + protected $arguments = []; + + protected $interactive = true; + + private $tokens; + private $parsed; + + public function __construct($argv = null) + { + if (null === $argv) { + $argv = $_SERVER['argv']; + // 去除命令名 + array_shift($argv); + } + + $this->tokens = $argv; + + $this->definition = new Definition(); + } + + protected function setTokens(array $tokens) + { + $this->tokens = $tokens; + } + + /** + * 绑定实例 + * @param Definition $definition A InputDefinition instance + */ + public function bind(Definition $definition) + { + $this->arguments = []; + $this->options = []; + $this->definition = $definition; + + $this->parse(); + } + + /** + * 解析参数 + */ + protected function parse() + { + $parseOptions = true; + $this->parsed = $this->tokens; + while (null !== $token = array_shift($this->parsed)) { + if ($parseOptions && '' == $token) { + $this->parseArgument($token); + } elseif ($parseOptions && '--' == $token) { + $parseOptions = false; + } elseif ($parseOptions && 0 === strpos($token, '--')) { + $this->parseLongOption($token); + } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) { + $this->parseShortOption($token); + } else { + $this->parseArgument($token); + } + } + } + + /** + * 解析短选项 + * @param string $token 当前的指令. + */ + private function parseShortOption($token) + { + $name = substr($token, 1); + + if (strlen($name) > 1) { + if ($this->definition->hasShortcut($name[0]) + && $this->definition->getOptionForShortcut($name[0])->acceptValue() + ) { + $this->addShortOption($name[0], substr($name, 1)); + } else { + $this->parseShortOptionSet($name); + } + } else { + $this->addShortOption($name, null); + } + } + + /** + * 解析短选项 + * @param string $name 当前指令 + * @throws \RuntimeException + */ + private function parseShortOptionSet($name) + { + $len = strlen($name); + for ($i = 0; $i < $len; ++$i) { + if (!$this->definition->hasShortcut($name[$i])) { + throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $name[$i])); + } + + $option = $this->definition->getOptionForShortcut($name[$i]); + if ($option->acceptValue()) { + $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1)); + + break; + } else { + $this->addLongOption($option->getName(), null); + } + } + } + + /** + * 解析完整选项 + * @param string $token 当前指令 + */ + private function parseLongOption($token) + { + $name = substr($token, 2); + + if (false !== $pos = strpos($name, '=')) { + $this->addLongOption(substr($name, 0, $pos), substr($name, $pos + 1)); + } else { + $this->addLongOption($name, null); + } + } + + /** + * 解析参数 + * @param string $token 当前指令 + * @throws \RuntimeException + */ + private function parseArgument($token) + { + $c = count($this->arguments); + + if ($this->definition->hasArgument($c)) { + $arg = $this->definition->getArgument($c); + + $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token; + + } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) { + $arg = $this->definition->getArgument($c - 1); + + $this->arguments[$arg->getName()][] = $token; + } else { + throw new \RuntimeException('Too many arguments.'); + } + } + + /** + * 添加一个短选项的值 + * @param string $shortcut 短名称 + * @param mixed $value 值 + * @throws \RuntimeException + */ + private function addShortOption($shortcut, $value) + { + if (!$this->definition->hasShortcut($shortcut)) { + throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); + } + + /** + * 添加一个完整选项的值 + * @param string $name 选项名 + * @param mixed $value 值 + * @throws \RuntimeException + */ + private function addLongOption($name, $value) + { + if (!$this->definition->hasOption($name)) { + throw new \RuntimeException(sprintf('The "--%s" option does not exist.', $name)); + } + + $option = $this->definition->getOption($name); + + if (false === $value) { + $value = null; + } + + if (null !== $value && !$option->acceptValue()) { + throw new \RuntimeException(sprintf('The "--%s" option does not accept a value.', $name, $value)); + } + + if (null === $value && $option->acceptValue() && count($this->parsed)) { + $next = array_shift($this->parsed); + if (isset($next[0]) && '-' !== $next[0]) { + $value = $next; + } elseif (empty($next)) { + $value = ''; + } else { + array_unshift($this->parsed, $next); + } + } + + if (null === $value) { + if ($option->isValueRequired()) { + throw new \RuntimeException(sprintf('The "--%s" option requires a value.', $name)); + } + + if (!$option->isArray()) { + $value = $option->isValueOptional() ? $option->getDefault() : true; + } + } + + if ($option->isArray()) { + $this->options[$name][] = $value; + } else { + $this->options[$name] = $value; + } + } + + /** + * 获取第一个参数 + * @return string|null + */ + public function getFirstArgument() + { + foreach ($this->tokens as $token) { + if ($token && '-' === $token[0]) { + continue; + } + + return $token; + } + return; + } + + /** + * 检查原始参数是否包含某个值 + * @param string|array $values 需要检查的值 + * @return bool + */ + public function hasParameterOption($values) + { + $values = (array) $values; + + foreach ($this->tokens as $token) { + foreach ($values as $value) { + if ($token === $value || 0 === strpos($token, $value . '=')) { + return true; + } + } + } + + return false; + } + + /** + * 获取原始选项的值 + * @param string|array $values 需要检查的值 + * @param mixed $default 默认值 + * @return mixed The option value + */ + public function getParameterOption($values, $default = false) + { + $values = (array) $values; + $tokens = $this->tokens; + + while (0 < count($tokens)) { + $token = array_shift($tokens); + + foreach ($values as $value) { + if ($token === $value || 0 === strpos($token, $value . '=')) { + if (false !== $pos = strpos($token, '=')) { + return substr($token, $pos + 1); + } + + return array_shift($tokens); + } + } + } + + return $default; + } + + /** + * 验证输入 + * @throws \RuntimeException + */ + public function validate() + { + if (count($this->arguments) < $this->definition->getArgumentRequiredCount()) { + throw new \RuntimeException('Not enough arguments.'); + } + } + + /** + * 检查输入是否是交互的 + * @return bool + */ + public function isInteractive() + { + return $this->interactive; + } + + /** + * 设置输入的交互 + * @param bool + */ + public function setInteractive($interactive) + { + $this->interactive = (bool) $interactive; + } + + /** + * 获取所有的参数 + * @return Argument[] + */ + public function getArguments() + { + return array_merge($this->definition->getArgumentDefaults(), $this->arguments); + } + + /** + * 根据名称获取参数 + * @param string $name 参数名 + * @return mixed + * @throws \InvalidArgumentException + */ + public function getArgument($name) + { + if (!$this->definition->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + return isset($this->arguments[$name]) ? $this->arguments[$name] : $this->definition->getArgument($name) + ->getDefault(); + } + + /** + * 设置参数的值 + * @param string $name 参数名 + * @param string $value 值 + * @throws \InvalidArgumentException + */ + public function setArgument($name, $value) + { + if (!$this->definition->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $this->arguments[$name] = $value; + } + + /** + * 检查是否存在某个参数 + * @param string|int $name 参数名或位置 + * @return bool + */ + public function hasArgument($name) + { + return $this->definition->hasArgument($name); + } + + /** + * 获取所有的选项 + * @return Option[] + */ + public function getOptions() + { + return array_merge($this->definition->getOptionDefaults(), $this->options); + } + + /** + * 获取选项值 + * @param string $name 选项名称 + * @return mixed + * @throws \InvalidArgumentException + */ + public function getOption($name) + { + if (!$this->definition->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + return isset($this->options[$name]) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); + } + + /** + * 设置选项值 + * @param string $name 选项名 + * @param string|bool $value 值 + * @throws \InvalidArgumentException + */ + public function setOption($name, $value) + { + if (!$this->definition->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + $this->options[$name] = $value; + } + + /** + * 是否有某个选项 + * @param string $name 选项名 + * @return bool + */ + public function hasOption($name) + { + return $this->definition->hasOption($name) && isset($this->options[$name]); + } + + /** + * 转义指令 + * @param string $token + * @return string + */ + public function escapeToken($token) + { + return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token); + } + + /** + * 返回传递给命令的参数的字符串 + * @return string + */ + public function __toString() + { + $tokens = array_map(function ($token) { + if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) { + return $match[1] . $this->escapeToken($match[2]); + } + + if ($token && '-' !== $token[0]) { + return $this->escapeToken($token); + } + + return $token; + }, $this->tokens); + + return implode(' ', $tokens); + } +} diff --git a/thinkphp/library/think/console/LICENSE b/thinkphp/library/think/console/LICENSE new file mode 100644 index 0000000..0abe056 --- /dev/null +++ b/thinkphp/library/think/console/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2016 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/thinkphp/library/think/console/Output.php b/thinkphp/library/think/console/Output.php new file mode 100644 index 0000000..65dc9fb --- /dev/null +++ b/thinkphp/library/think/console/Output.php @@ -0,0 +1,222 @@ + +// +---------------------------------------------------------------------- + +namespace think\console; + +use Exception; +use think\console\output\Ask; +use think\console\output\Descriptor; +use think\console\output\driver\Buffer; +use think\console\output\driver\Console; +use think\console\output\driver\Nothing; +use think\console\output\Question; +use think\console\output\question\Choice; +use think\console\output\question\Confirmation; + +/** + * Class Output + * @package think\console + * + * @see \think\console\output\driver\Console::setDecorated + * @method void setDecorated($decorated) + * + * @see \think\console\output\driver\Buffer::fetch + * @method string fetch() + * + * @method void info($message) + * @method void error($message) + * @method void comment($message) + * @method void warning($message) + * @method void highlight($message) + * @method void question($message) + */ +class Output +{ + const VERBOSITY_QUIET = 0; + const VERBOSITY_NORMAL = 1; + const VERBOSITY_VERBOSE = 2; + const VERBOSITY_VERY_VERBOSE = 3; + const VERBOSITY_DEBUG = 4; + + const OUTPUT_NORMAL = 0; + const OUTPUT_RAW = 1; + const OUTPUT_PLAIN = 2; + + private $verbosity = self::VERBOSITY_NORMAL; + + /** @var Buffer|Console|Nothing */ + private $handle = null; + + protected $styles = [ + 'info', + 'error', + 'comment', + 'question', + 'highlight', + 'warning' + ]; + + public function __construct($driver = 'console') + { + $class = '\\think\\console\\output\\driver\\' . ucwords($driver); + + $this->handle = new $class($this); + } + + public function ask(Input $input, $question, $default = null, $validator = null) + { + $question = new Question($question, $default); + $question->setValidator($validator); + + return $this->askQuestion($input, $question); + } + + public function askHidden(Input $input, $question, $validator = null) + { + $question = new Question($question); + + $question->setHidden(true); + $question->setValidator($validator); + + return $this->askQuestion($input, $question); + } + + public function confirm(Input $input, $question, $default = true) + { + return $this->askQuestion($input, new Confirmation($question, $default)); + } + + /** + * {@inheritdoc} + */ + public function choice(Input $input, $question, array $choices, $default = null) + { + if (null !== $default) { + $values = array_flip($choices); + $default = $values[$default]; + } + + return $this->askQuestion($input, new Choice($question, $choices, $default)); + } + + protected function askQuestion(Input $input, Question $question) + { + $ask = new Ask($input, $this, $question); + $answer = $ask->run(); + + if ($input->isInteractive()) { + $this->newLine(); + } + + return $answer; + } + + protected function block($style, $message) + { + $this->writeln("<{$style}>{$message}"); + } + + /** + * 输出空行 + * @param int $count + */ + public function newLine($count = 1) + { + $this->write(str_repeat(PHP_EOL, $count)); + } + + /** + * 输出信息并换行 + * @param string $messages + * @param int $type + */ + public function writeln($messages, $type = self::OUTPUT_NORMAL) + { + $this->write($messages, true, $type); + } + + /** + * 输出信息 + * @param string $messages + * @param bool $newline + * @param int $type + */ + public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL) + { + $this->handle->write($messages, $newline, $type); + } + + public function renderException(\Exception $e) + { + $this->handle->renderException($e); + } + + /** + * {@inheritdoc} + */ + public function setVerbosity($level) + { + $this->verbosity = (int) $level; + } + + /** + * {@inheritdoc} + */ + public function getVerbosity() + { + return $this->verbosity; + } + + public function isQuiet() + { + return self::VERBOSITY_QUIET === $this->verbosity; + } + + public function isVerbose() + { + return self::VERBOSITY_VERBOSE <= $this->verbosity; + } + + public function isVeryVerbose() + { + return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity; + } + + public function isDebug() + { + return self::VERBOSITY_DEBUG <= $this->verbosity; + } + + public function describe($object, array $options = []) + { + $descriptor = new Descriptor(); + $options = array_merge([ + 'raw_text' => false, + ], $options); + + $descriptor->describe($this, $object, $options); + } + + public function __call($method, $args) + { + if (in_array($method, $this->styles)) { + array_unshift($args, $method); + return call_user_func_array([$this, 'block'], $args); + } + + if ($this->handle && method_exists($this->handle, $method)) { + return call_user_func_array([$this->handle, $method], $args); + } else { + throw new Exception('method not exists:' . __CLASS__ . '->' . $method); + } + } + +} diff --git a/thinkphp/library/think/console/Table.php b/thinkphp/library/think/console/Table.php new file mode 100644 index 0000000..9e28e26 --- /dev/null +++ b/thinkphp/library/think/console/Table.php @@ -0,0 +1,281 @@ + +// +---------------------------------------------------------------------- + +namespace think\console; + +class Table +{ + const ALIGN_LEFT = 1; + const ALIGN_RIGHT = 0; + const ALIGN_CENTER = 2; + + /** + * 头信息数据 + * @var array + */ + protected $header = []; + + /** + * 头部对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER + * @var int + */ + protected $headerAlign = 1; + + /** + * 表格数据(二维数组) + * @var array + */ + protected $rows = []; + + /** + * 单元格对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER + * @var int + */ + protected $cellAlign = 1; + + /** + * 单元格宽度信息 + * @var array + */ + protected $colWidth = []; + + /** + * 表格输出样式 + * @var string + */ + protected $style = 'default'; + + /** + * 表格样式定义 + * @var array + */ + protected $format = [ + 'compact' => [], + 'default' => [ + 'top' => ['+', '-', '+', '+'], + 'cell' => ['|', ' ', '|', '|'], + 'middle' => ['+', '-', '+', '+'], + 'bottom' => ['+', '-', '+', '+'], + 'cross-top' => ['+', '-', '-', '+'], + 'cross-bottom' => ['+', '-', '-', '+'], + ], + 'markdown' => [ + 'top' => [' ', ' ', ' ', ' '], + 'cell' => ['|', ' ', '|', '|'], + 'middle' => ['|', '-', '|', '|'], + 'bottom' => [' ', ' ', ' ', ' '], + 'cross-top' => ['|', ' ', ' ', '|'], + 'cross-bottom' => ['|', ' ', ' ', '|'], + ], + 'borderless' => [ + 'top' => ['=', '=', ' ', '='], + 'cell' => [' ', ' ', ' ', ' '], + 'middle' => ['=', '=', ' ', '='], + 'bottom' => ['=', '=', ' ', '='], + 'cross-top' => ['=', '=', ' ', '='], + 'cross-bottom' => ['=', '=', ' ', '='], + ], + 'box' => [ + 'top' => ['┌', '─', '┬', '┐'], + 'cell' => ['│', ' ', '│', '│'], + 'middle' => ['├', '─', '┼', '┤'], + 'bottom' => ['└', '─', '┴', '┘'], + 'cross-top' => ['├', '─', '┴', '┤'], + 'cross-bottom' => ['├', '─', '┬', '┤'], + ], + 'box-double' => [ + 'top' => ['╔', '═', '╤', '╗'], + 'cell' => ['║', ' ', '│', '║'], + 'middle' => ['╠', '─', '╪', '╣'], + 'bottom' => ['╚', '═', '╧', '╝'], + 'cross-top' => ['╠', '═', '╧', '╣'], + 'cross-bottom' => ['╠', '═', '╤', '╣'], + ], + ]; + + /** + * 设置表格头信息 以及对齐方式 + * @access public + * @param array $header 要输出的Header信息 + * @param int $align 对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER + * @return void + */ + public function setHeader(array $header, $align = self::ALIGN_LEFT) + { + $this->header = $header; + $this->headerAlign = $align; + + $this->checkColWidth($header); + } + + /** + * 设置输出表格数据 及对齐方式 + * @access public + * @param array $rows 要输出的表格数据(二维数组) + * @param int $align 对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER + * @return void + */ + public function setRows(array $rows, $align = self::ALIGN_LEFT) + { + $this->rows = $rows; + $this->cellAlign = $align; + + foreach ($rows as $row) { + $this->checkColWidth($row); + } + } + + /** + * 检查列数据的显示宽度 + * @access public + * @param mixed $row 行数据 + * @return void + */ + protected function checkColWidth($row) + { + if (is_array($row)) { + foreach ($row as $key => $cell) { + if (!isset($this->colWidth[$key]) || strlen($cell) > $this->colWidth[$key]) { + $this->colWidth[$key] = strlen($cell); + } + } + } + } + + /** + * 增加一行表格数据 + * @access public + * @param mixed $row 行数据 + * @param bool $first 是否在开头插入 + * @return void + */ + public function addRow($row, $first = false) + { + if ($first) { + array_unshift($this->rows, $row); + } else { + $this->rows[] = $row; + } + + $this->checkColWidth($row); + } + + /** + * 设置输出表格的样式 + * @access public + * @param string $style 样式名 + * @return void + */ + public function setStyle($style) + { + $this->style = isset($this->format[$style]) ? $style : 'default'; + } + + /** + * 输出分隔行 + * @access public + * @param string $pos 位置 + * @return string + */ + protected function renderSeparator($pos) + { + $style = $this->getStyle($pos); + $array = []; + + foreach ($this->colWidth as $width) { + $array[] = str_repeat($style[1], $width + 2); + } + + return $style[0] . implode($style[2], $array) . $style[3] . PHP_EOL; + } + + /** + * 输出表格头部 + * @access public + * @return string + */ + protected function renderHeader() + { + $style = $this->getStyle('cell'); + $content = $this->renderSeparator('top'); + + foreach ($this->header as $key => $header) { + $array[] = ' ' . str_pad($header, $this->colWidth[$key], $style[1], $this->headerAlign); + } + + if (!empty($array)) { + $content .= $style[0] . implode(' ' . $style[2], $array) . ' ' . $style[3] . PHP_EOL; + + if ($this->rows) { + $content .= $this->renderSeparator('middle'); + } + } + + return $content; + } + + protected function getStyle($style) + { + if ($this->format[$this->style]) { + $style = $this->format[$this->style][$style]; + } else { + $style = [' ', ' ', ' ', ' ']; + } + + return $style; + } + + /** + * 输出表格 + * @access public + * @param array $dataList 表格数据 + * @return string + */ + public function render($dataList = []) + { + if ($dataList) { + $this->setRows($dataList); + } + + // 输出头部 + $content = $this->renderHeader(); + $style = $this->getStyle('cell'); + + if ($this->rows) { + foreach ($this->rows as $row) { + if (is_string($row) && '-' === $row) { + $content .= $this->renderSeparator('middle'); + } elseif (is_scalar($row)) { + $content .= $this->renderSeparator('cross-top'); + $array = str_pad($row, 3 * (count($this->colWidth) - 1) + array_reduce($this->colWidth, function ($a, $b) { + return $a + $b; + })); + + $content .= $style[0] . ' ' . $array . ' ' . $style[3] . PHP_EOL; + $content .= $this->renderSeparator('cross-bottom'); + } else { + $array = []; + + foreach ($row as $key => $val) { + $array[] = ' ' . str_pad($val, $this->colWidth[$key], ' ', $this->cellAlign); + } + + $content .= $style[0] . implode(' ' . $style[2], $array) . ' ' . $style[3] . PHP_EOL; + + } + } + } + + $content .= $this->renderSeparator('bottom'); + + return $content; + } +} diff --git a/thinkphp/library/think/console/bin/README.md b/thinkphp/library/think/console/bin/README.md new file mode 100644 index 0000000..9acc52f --- /dev/null +++ b/thinkphp/library/think/console/bin/README.md @@ -0,0 +1 @@ +console 工具使用 hiddeninput.exe 在 windows 上隐藏密码输入,该二进制文件由第三方提供,相关源码和其他细节可以在 [Hidden Input](https://github.com/Seldaek/hidden-input) 找到。 diff --git a/thinkphp/library/think/console/bin/hiddeninput.exe b/thinkphp/library/think/console/bin/hiddeninput.exe new file mode 100644 index 0000000..c8cf65e Binary files /dev/null and b/thinkphp/library/think/console/bin/hiddeninput.exe differ diff --git a/thinkphp/library/think/console/command/Build.php b/thinkphp/library/think/console/command/Build.php new file mode 100644 index 0000000..88a5bf8 --- /dev/null +++ b/thinkphp/library/think/console/command/Build.php @@ -0,0 +1,59 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; +use think\facade\App; +use think\facade\Build as AppBuild; + +class Build extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->setName('build') + ->setDefinition([ + new Option('config', null, Option::VALUE_OPTIONAL, "build.php path"), + new Option('module', null, Option::VALUE_OPTIONAL, "module name"), + ]) + ->setDescription('Build Application Dirs'); + } + + protected function execute(Input $input, Output $output) + { + if ($input->hasOption('module')) { + AppBuild::module($input->getOption('module')); + $output->writeln("Successed"); + return; + } + + if ($input->hasOption('config')) { + $build = include $input->getOption('config'); + } else { + $build = include App::getAppPath() . 'build.php'; + } + + if (empty($build)) { + $output->writeln("Build Config Is Empty"); + return; + } + + AppBuild::run($build); + $output->writeln("Successed"); + + } +} diff --git a/thinkphp/library/think/console/command/Clear.php b/thinkphp/library/think/console/command/Clear.php new file mode 100644 index 0000000..1442575 --- /dev/null +++ b/thinkphp/library/think/console/command/Clear.php @@ -0,0 +1,70 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; +use think\facade\App; +use think\facade\Cache; + +class Clear extends Command +{ + protected function configure() + { + // 指令配置 + $this + ->setName('clear') + ->addOption('path', 'd', Option::VALUE_OPTIONAL, 'path to clear', null) + ->addOption('cache', 'c', Option::VALUE_NONE, 'clear cache file') + ->addOption('route', 'u', Option::VALUE_NONE, 'clear route cache') + ->addOption('log', 'l', Option::VALUE_NONE, 'clear log file') + ->addOption('dir', 'r', Option::VALUE_NONE, 'clear empty dir') + ->setDescription('Clear runtime file'); + } + + protected function execute(Input $input, Output $output) + { + if ($input->getOption('route')) { + Cache::clear('route_cache'); + } else { + if ($input->getOption('cache')) { + $path = App::getRuntimePath() . 'cache'; + } elseif ($input->getOption('log')) { + $path = App::getRuntimePath() . 'log'; + } else { + $path = $input->getOption('path') ?: App::getRuntimePath(); + } + + $rmdir = $input->getOption('dir') ? true : false; + $this->clear(rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, $rmdir); + } + + $output->writeln("Clear Successed"); + } + + protected function clear($path, $rmdir) + { + $files = is_dir($path) ? scandir($path) : []; + + foreach ($files as $file) { + if ('.' != $file && '..' != $file && is_dir($path . $file)) { + array_map('unlink', glob($path . $file . DIRECTORY_SEPARATOR . '*.*')); + if ($rmdir) { + rmdir($path . $file); + } + } elseif ('.gitignore' != $file && is_file($path . $file)) { + unlink($path . $file); + } + } + } +} diff --git a/thinkphp/library/think/console/command/Help.php b/thinkphp/library/think/console/command/Help.php new file mode 100644 index 0000000..f1b63b4 --- /dev/null +++ b/thinkphp/library/think/console/command/Help.php @@ -0,0 +1,68 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Argument as InputArgument; +use think\console\input\Option as InputOption; +use think\console\Output; + +class Help extends Command +{ + private $command; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->ignoreValidationErrors(); + + $this->setName('help')->setDefinition([ + new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), + ])->setDescription('Displays help for a command')->setHelp(<<%command.name% command displays help for a given command: + + php %command.full_name% list + +To display the list of available commands, please use the list command. +EOF + ); + } + + /** + * Sets the command. + * @param Command $command The command to set + */ + public function setCommand(Command $command) + { + $this->command = $command; + } + + /** + * {@inheritdoc} + */ + protected function execute(Input $input, Output $output) + { + if (null === $this->command) { + $this->command = $this->getConsole()->find($input->getArgument('command_name')); + } + + $output->describe($this->command, [ + 'raw_text' => $input->getOption('raw'), + ]); + + $this->command = null; + } +} diff --git a/thinkphp/library/think/console/command/Lists.php b/thinkphp/library/think/console/command/Lists.php new file mode 100644 index 0000000..6eb856c --- /dev/null +++ b/thinkphp/library/think/console/command/Lists.php @@ -0,0 +1,73 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Argument as InputArgument; +use think\console\input\Definition as InputDefinition; +use think\console\input\Option as InputOption; +use think\console\Output; + +class Lists extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->setName('list')->setDefinition($this->createDefinition())->setDescription('Lists commands')->setHelp(<<%command.name% command lists all commands: + + php %command.full_name% + +You can also display the commands for a specific namespace: + + php %command.full_name% test + +It's also possible to get raw list of commands (useful for embedding command runner): + + php %command.full_name% --raw +EOF + ); + } + + /** + * {@inheritdoc} + */ + public function getNativeDefinition() + { + return $this->createDefinition(); + } + + /** + * {@inheritdoc} + */ + protected function execute(Input $input, Output $output) + { + $output->describe($this->getConsole(), [ + 'raw_text' => $input->getOption('raw'), + 'namespace' => $input->getArgument('namespace'), + ]); + } + + /** + * {@inheritdoc} + */ + private function createDefinition() + { + return new InputDefinition([ + new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), + ]); + } +} diff --git a/thinkphp/library/think/console/command/Make.php b/thinkphp/library/think/console/command/Make.php new file mode 100644 index 0000000..2f20954 --- /dev/null +++ b/thinkphp/library/think/console/command/Make.php @@ -0,0 +1,110 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Argument; +use think\console\Output; +use think\facade\App; +use think\facade\Config; +use think\facade\Env; + +abstract class Make extends Command +{ + protected $type; + + abstract protected function getStub(); + + protected function configure() + { + $this->addArgument('name', Argument::REQUIRED, "The name of the class"); + } + + protected function execute(Input $input, Output $output) + { + + $name = trim($input->getArgument('name')); + + $classname = $this->getClassName($name); + + $pathname = $this->getPathName($classname); + + if (is_file($pathname)) { + $output->writeln('' . $this->type . ' already exists!'); + return false; + } + + if (!is_dir(dirname($pathname))) { + mkdir(dirname($pathname), 0755, true); + } + + file_put_contents($pathname, $this->buildClass($classname)); + + $output->writeln('' . $this->type . ' created successfully.'); + + } + + protected function buildClass($name) + { + $stub = file_get_contents($this->getStub()); + + $namespace = trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\'); + + $class = str_replace($namespace . '\\', '', $name); + + return str_replace(['{%className%}', '{%actionSuffix%}', '{%namespace%}', '{%app_namespace%}'], [ + $class, + Config::get('action_suffix'), + $namespace, + App::getNamespace(), + ], $stub); + } + + protected function getPathName($name) + { + $name = str_replace(App::getNamespace() . '\\', '', $name); + + return Env::get('app_path') . ltrim(str_replace('\\', '/', $name), '/') . '.php'; + } + + protected function getClassName($name) + { + $appNamespace = App::getNamespace(); + + if (strpos($name, $appNamespace . '\\') !== false) { + return $name; + } + + if (Config::get('app_multi_module')) { + if (strpos($name, '/')) { + list($module, $name) = explode('/', $name, 2); + } else { + $module = 'common'; + } + } else { + $module = null; + } + + if (strpos($name, '/') !== false) { + $name = str_replace('/', '\\', $name); + } + + return $this->getNamespace($appNamespace, $module) . '\\' . $name; + } + + protected function getNamespace($appNamespace, $module) + { + return $module ? ($appNamespace . '\\' . $module) : $appNamespace; + } + +} diff --git a/thinkphp/library/think/console/command/RouteList.php b/thinkphp/library/think/console/command/RouteList.php new file mode 100644 index 0000000..0405c31 --- /dev/null +++ b/thinkphp/library/think/console/command/RouteList.php @@ -0,0 +1,130 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Argument; +use think\console\input\Option; +use think\console\Output; +use think\console\Table; +use think\Container; + +class RouteList extends Command +{ + protected $sortBy = [ + 'rule' => 0, + 'route' => 1, + 'method' => 2, + 'name' => 3, + 'domain' => 4, + ]; + + protected function configure() + { + $this->setName('route:list') + ->addArgument('style', Argument::OPTIONAL, "the style of the table.", 'default') + ->addOption('sort', 's', Option::VALUE_OPTIONAL, 'order by rule name.', 0) + ->addOption('more', 'm', Option::VALUE_NONE, 'show route options.') + ->setDescription('show route list.'); + } + + protected function execute(Input $input, Output $output) + { + $filename = Container::get('app')->getRuntimePath() . 'route_list.php'; + + if (is_file($filename)) { + unlink($filename); + } + + $content = $this->getRouteList(); + file_put_contents($filename, 'Route List' . PHP_EOL . $content); + } + + protected function getRouteList() + { + Container::get('route')->setTestMode(true); + // 路由检测 + $path = Container::get('app')->getRoutePath(); + + $files = is_dir($path) ? scandir($path) : []; + + foreach ($files as $file) { + if (strpos($file, '.php')) { + $filename = $path . DIRECTORY_SEPARATOR . $file; + // 导入路由配置 + $rules = include $filename; + + if (is_array($rules)) { + Container::get('route')->import($rules); + } + } + } + + if (Container::get('config')->get('route_annotation')) { + $suffix = Container::get('config')->get('controller_suffix') || Container::get('config')->get('class_suffix'); + + include Container::get('build')->buildRoute($suffix); + } + + $table = new Table(); + + if ($this->input->hasOption('more')) { + $header = ['Rule', 'Route', 'Method', 'Name', 'Domain', 'Option', 'Pattern']; + } else { + $header = ['Rule', 'Route', 'Method', 'Name', 'Domain']; + } + + $table->setHeader($header); + + $routeList = Container::get('route')->getRuleList(); + $rows = []; + + foreach ($routeList as $domain => $items) { + foreach ($items as $item) { + $item['route'] = $item['route'] instanceof \Closure ? '' : $item['route']; + + if ($this->input->hasOption('more')) { + $item = [$item['rule'], $item['route'], $item['method'], $item['name'], $domain, json_encode($item['option']), json_encode($item['pattern'])]; + } else { + $item = [$item['rule'], $item['route'], $item['method'], $item['name'], $domain]; + } + + $rows[] = $item; + } + } + + if ($this->input->getOption('sort')) { + $sort = $this->input->getOption('sort'); + + if (isset($this->sortBy[$sort])) { + $sort = $this->sortBy[$sort]; + } + + uasort($rows, function ($a, $b) use ($sort) { + $itemA = isset($a[$sort]) ? $a[$sort] : null; + $itemB = isset($b[$sort]) ? $b[$sort] : null; + + return strcasecmp($itemA, $itemB); + }); + } + + $table->setRows($rows); + + if ($this->input->getArgument('style')) { + $style = $this->input->getArgument('style'); + $table->setStyle($style); + } + + return $this->table($table); + } + +} diff --git a/thinkphp/library/think/console/command/RunServer.php b/thinkphp/library/think/console/command/RunServer.php new file mode 100644 index 0000000..2e028dc --- /dev/null +++ b/thinkphp/library/think/console/command/RunServer.php @@ -0,0 +1,53 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; +use think\facade\App; + +class RunServer extends Command +{ + public function configure() + { + $this->setName('run') + ->addOption('host', 'H', Option::VALUE_OPTIONAL, + 'The host to server the application on', '127.0.0.1') + ->addOption('port', 'p', Option::VALUE_OPTIONAL, + 'The port to server the application on', 8000) + ->addOption('root', 'r', Option::VALUE_OPTIONAL, + 'The document root of the application', App::getRootPath() . 'public') + ->setDescription('PHP Built-in Server for ThinkPHP'); + } + + public function execute(Input $input, Output $output) + { + $host = $input->getOption('host'); + $port = $input->getOption('port'); + $root = $input->getOption('root'); + + $command = sprintf( + 'php -S %s:%d -t %s %s', + $host, + $port, + escapeshellarg($root), + escapeshellarg($root . DIRECTORY_SEPARATOR . 'router.php') + ); + + $output->writeln(sprintf('ThinkPHP Development server is started On ', $host, $port)); + $output->writeln(sprintf('You can exit with `CTRL-C`')); + $output->writeln(sprintf('Document root is: %s', $root)); + passthru($command); + } + +} diff --git a/thinkphp/library/think/console/command/Version.php b/thinkphp/library/think/console/command/Version.php new file mode 100644 index 0000000..ee7eca9 --- /dev/null +++ b/thinkphp/library/think/console/command/Version.php @@ -0,0 +1,31 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\Output; +use think\facade\App; + +class Version extends Command +{ + protected function configure() + { + // 指令配置 + $this->setName('version') + ->setDescription('show thinkphp framework version'); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('v' . App::version()); + } +} diff --git a/thinkphp/library/think/console/command/make/Command.php b/thinkphp/library/think/console/command/make/Command.php new file mode 100644 index 0000000..b539eb2 --- /dev/null +++ b/thinkphp/library/think/console/command/make/Command.php @@ -0,0 +1,56 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\console\command\Make; +use think\console\input\Argument; +use think\facade\App; + +class Command extends Make +{ + protected $type = "Command"; + + protected function configure() + { + parent::configure(); + $this->setName('make:command') + ->addArgument('commandName', Argument::OPTIONAL, "The name of the command") + ->setDescription('Create a new command class'); + } + + protected function buildClass($name) + { + $commandName = $this->input->getArgument('commandName') ?: strtolower(basename($name)); + $namespace = trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\'); + + $class = str_replace($namespace . '\\', '', $name); + $stub = file_get_contents($this->getStub()); + + return str_replace(['{%commandName%}', '{%className%}', '{%namespace%}', '{%app_namespace%}'], [ + $commandName, + $class, + $namespace, + App::getNamespace(), + ], $stub); + } + + protected function getStub() + { + return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'command.stub'; + } + + protected function getNamespace($appNamespace, $module) + { + return $appNamespace . '\\command'; + } + +} diff --git a/thinkphp/library/think/console/command/make/Controller.php b/thinkphp/library/think/console/command/make/Controller.php new file mode 100644 index 0000000..2a6ab77 --- /dev/null +++ b/thinkphp/library/think/console/command/make/Controller.php @@ -0,0 +1,56 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\console\command\Make; +use think\console\input\Option; +use think\facade\Config; + +class Controller extends Make +{ + protected $type = "Controller"; + + protected function configure() + { + parent::configure(); + $this->setName('make:controller') + ->addOption('api', null, Option::VALUE_NONE, 'Generate an api controller class.') + ->addOption('plain', null, Option::VALUE_NONE, 'Generate an empty controller class.') + ->setDescription('Create a new resource controller class'); + } + + protected function getStub() + { + $stubPath = __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR; + + if ($this->input->getOption('api')) { + return $stubPath . 'controller.api.stub'; + } + + if ($this->input->getOption('plain')) { + return $stubPath . 'controller.plain.stub'; + } + + return $stubPath . 'controller.stub'; + } + + protected function getClassName($name) + { + return parent::getClassName($name) . (Config::get('controller_suffix') ? ucfirst(Config::get('url_controller_layer')) : ''); + } + + protected function getNamespace($appNamespace, $module) + { + return parent::getNamespace($appNamespace, $module) . '\controller'; + } + +} diff --git a/thinkphp/library/think/console/command/make/Middleware.php b/thinkphp/library/think/console/command/make/Middleware.php new file mode 100644 index 0000000..bfe821b --- /dev/null +++ b/thinkphp/library/think/console/command/make/Middleware.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\console\command\Make; + +class Middleware extends Make +{ + protected $type = "Middleware"; + + protected function configure() + { + parent::configure(); + $this->setName('make:middleware') + ->setDescription('Create a new middleware class'); + } + + protected function getStub() + { + return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'middleware.stub'; + } + + protected function getNamespace($appNamespace, $module) + { + return parent::getNamespace($appNamespace, 'http') . '\middleware'; + } +} diff --git a/thinkphp/library/think/console/command/make/Model.php b/thinkphp/library/think/console/command/make/Model.php new file mode 100644 index 0000000..03e6b3f --- /dev/null +++ b/thinkphp/library/think/console/command/make/Model.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\console\command\Make; + +class Model extends Make +{ + protected $type = "Model"; + + protected function configure() + { + parent::configure(); + $this->setName('make:model') + ->setDescription('Create a new model class'); + } + + protected function getStub() + { + return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'model.stub'; + } + + protected function getNamespace($appNamespace, $module) + { + return parent::getNamespace($appNamespace, $module) . '\model'; + } +} diff --git a/thinkphp/library/think/console/command/make/Validate.php b/thinkphp/library/think/console/command/make/Validate.php new file mode 100644 index 0000000..89830ad --- /dev/null +++ b/thinkphp/library/think/console/command/make/Validate.php @@ -0,0 +1,39 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\console\command\Make; + +class Validate extends Make +{ + protected $type = "Validate"; + + protected function configure() + { + parent::configure(); + $this->setName('make:validate') + ->setDescription('Create a validate class'); + } + + protected function getStub() + { + $stubPath = __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR; + + return $stubPath . 'validate.stub'; + } + + protected function getNamespace($appNamespace, $module) + { + return parent::getNamespace($appNamespace, $module) . '\validate'; + } + +} diff --git a/thinkphp/library/think/console/command/make/stubs/command.stub b/thinkphp/library/think/console/command/make/stubs/command.stub new file mode 100644 index 0000000..d2c7c1e --- /dev/null +++ b/thinkphp/library/think/console/command/make/stubs/command.stub @@ -0,0 +1,24 @@ +setName('{%commandName%}'); + // 设置参数 + + } + + protected function execute(Input $input, Output $output) + { + // 指令输出 + $output->writeln('{%commandName%}'); + } +} diff --git a/thinkphp/library/think/console/command/make/stubs/controller.api.stub b/thinkphp/library/think/console/command/make/stubs/controller.api.stub new file mode 100644 index 0000000..54ec059 --- /dev/null +++ b/thinkphp/library/think/console/command/make/stubs/controller.api.stub @@ -0,0 +1,64 @@ + ['规则1','规则2'...] + * + * @var array + */ + protected $rule = []; + + /** + * 定义错误信息 + * 格式:'字段名.规则名' => '错误信息' + * + * @var array + */ + protected $message = []; +} diff --git a/thinkphp/library/think/console/command/optimize/Autoload.php b/thinkphp/library/think/console/command/optimize/Autoload.php new file mode 100644 index 0000000..b51fd25 --- /dev/null +++ b/thinkphp/library/think/console/command/optimize/Autoload.php @@ -0,0 +1,279 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\console\Command; +use think\console\Input; +use think\console\Output; +use think\Container; + +class Autoload extends Command +{ + protected function configure() + { + $this->setName('optimize:autoload') + ->setDescription('Optimizes PSR0 and PSR4 packages to be loaded with classmaps too, good for production.'); + } + + protected function execute(Input $input, Output $output) + { + + $classmapFile = <<getNamespace() . '\\' => realpath(rtrim($app->getAppPath())), + 'think\\' => $app->getThinkPath() . 'library/think', + 'traits\\' => $app->getThinkPath() . 'library/traits', + '' => realpath(rtrim($app->getRootPath() . 'extend')), + ]; + + krsort($namespacesToScan); + $classMap = []; + foreach ($namespacesToScan as $namespace => $dir) { + + if (!is_dir($dir)) { + continue; + } + + $namespaceFilter = '' === $namespace ? null : $namespace; + $classMap = $this->addClassMapCode($dir, $namespaceFilter, $classMap); + } + + ksort($classMap); + foreach ($classMap as $class => $code) { + $classmapFile .= ' ' . var_export($class, true) . ' => ' . $code; + } + $classmapFile .= "];\n"; + $runtimePath = $app->getRuntimePath(); + if (!is_dir($runtimePath)) { + @mkdir($runtimePath, 0755, true); + } + + file_put_contents($runtimePath . 'classmap.php', $classmapFile); + + $output->writeln('Succeed!'); + } + + protected function addClassMapCode($dir, $namespace, $classMap) + { + foreach ($this->createMap($dir, $namespace) as $class => $path) { + + $pathCode = $this->getPathCode($path) . ",\n"; + + if (!isset($classMap[$class])) { + $classMap[$class] = $pathCode; + } elseif ($classMap[$class] !== $pathCode && !preg_match('{/(test|fixture|example|stub)s?/}i', strtr($classMap[$class] . ' ' . $path, '\\', '/'))) { + $this->output->writeln( + 'Warning: Ambiguous class resolution, "' . $class . '"' . + ' was found in both "' . str_replace(["',\n"], [ + '', + ], $classMap[$class]) . '" and "' . $path . '", the first will be used.' + ); + } + } + return $classMap; + } + + protected function getPathCode($path) + { + $baseDir = ''; + $app = Container::get('app'); + $appPath = $this->normalizePath(realpath($app->getAppPath())); + $libPath = $this->normalizePath(realpath($app->getThinkPath() . 'library')); + $extendPath = $this->normalizePath(realpath($app->getRootPath() . 'extend')); + $path = $this->normalizePath($path); + + if (strpos($path, $libPath . '/') === 0) { + $path = substr($path, strlen($app->getThinkPath() . 'library')); + $baseDir = "'" . $libPath . "/'"; + } elseif (strpos($path, $appPath . '/') === 0) { + $path = substr($path, strlen($appPath) + 1); + $baseDir = "'" . $appPath . "/'"; + } elseif (strpos($path, $extendPath . '/') === 0) { + $path = substr($path, strlen($extendPath) + 1); + $baseDir = "'" . $extendPath . "/'"; + } + + if (false !== $path) { + $baseDir .= " . "; + } + + return $baseDir . ((false !== $path) ? var_export($path, true) : ""); + } + + protected function normalizePath($path) + { + $parts = []; + $path = strtr($path, '\\', '/'); + $prefix = ''; + $absolute = false; + + if (preg_match('{^([0-9a-z]+:(?://(?:[a-z]:)?)?)}i', $path, $match)) { + $prefix = $match[1]; + $path = substr($path, strlen($prefix)); + } + + if (substr($path, 0, 1) === '/') { + $absolute = true; + $path = substr($path, 1); + } + + $up = false; + foreach (explode('/', $path) as $chunk) { + if ('..' === $chunk && ($absolute || $up)) { + array_pop($parts); + $up = !(empty($parts) || '..' === end($parts)); + } elseif ('.' !== $chunk && '' !== $chunk) { + $parts[] = $chunk; + $up = '..' !== $chunk; + } + } + + return $prefix . ($absolute ? '/' : '') . implode('/', $parts); + } + + protected function createMap($path, $namespace = null) + { + if (is_string($path)) { + if (is_file($path)) { + $path = [new \SplFileInfo($path)]; + } elseif (is_dir($path)) { + + $objects = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path), \RecursiveIteratorIterator::SELF_FIRST); + + $path = []; + + /** @var \SplFileInfo $object */ + foreach ($objects as $object) { + if ($object->isFile() && $object->getExtension() == 'php') { + $path[] = $object; + } + } + } else { + throw new \RuntimeException( + 'Could not scan for classes inside "' . $path . + '" which does not appear to be a file nor a folder' + ); + } + } + + $map = []; + + /** @var \SplFileInfo $file */ + foreach ($path as $file) { + $filePath = $file->getRealPath(); + + if (pathinfo($filePath, PATHINFO_EXTENSION) != 'php') { + continue; + } + + $classes = $this->findClasses($filePath); + + foreach ($classes as $class) { + if (null !== $namespace && 0 !== strpos($class, $namespace)) { + continue; + } + + if (!isset($map[$class])) { + $map[$class] = $filePath; + } elseif ($map[$class] !== $filePath && !preg_match('{/(test|fixture|example|stub)s?/}i', strtr($map[$class] . ' ' . $filePath, '\\', '/'))) { + $this->output->writeln( + 'Warning: Ambiguous class resolution, "' . $class . '"' . + ' was found in both "' . $map[$class] . '" and "' . $filePath . '", the first will be used.' + ); + } + } + } + + return $map; + } + + protected function findClasses($path) + { + $extraTypes = '|trait'; + + $contents = @php_strip_whitespace($path); + if (!$contents) { + if (!file_exists($path)) { + $message = 'File at "%s" does not exist, check your classmap definitions'; + } elseif (!is_readable($path)) { + $message = 'File at "%s" is not readable, check its permissions'; + } elseif ('' === trim(file_get_contents($path))) { + return []; + } else { + $message = 'File at "%s" could not be parsed as PHP, it may be binary or corrupted'; + } + $error = error_get_last(); + if (isset($error['message'])) { + $message .= PHP_EOL . 'The following message may be helpful:' . PHP_EOL . $error['message']; + } + throw new \RuntimeException(sprintf($message, $path)); + } + + if (!preg_match('{\b(?:class|interface' . $extraTypes . ')\s}i', $contents)) { + return []; + } + + // strip heredocs/nowdocs + $contents = preg_replace('{<<<\s*(\'?)(\w+)\\1(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)\\2(?=\r\n|\n|\r|;)}s', 'null', $contents); + // strip strings + $contents = preg_replace('{"[^"\\\\]*+(\\\\.[^"\\\\]*+)*+"|\'[^\'\\\\]*+(\\\\.[^\'\\\\]*+)*+\'}s', 'null', $contents); + // strip leading non-php code if needed + if (substr($contents, 0, 2) !== '.+<\?}s', '?>'); + if (false !== $pos && false === strpos(substr($contents, $pos), '])(?Pclass|interface' . $extraTypes . ') \s++ (?P[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+) + | \b(?])(?Pnamespace) (?P\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;] + ) + }ix', $contents, $matches); + + $classes = []; + $namespace = ''; + + for ($i = 0, $len = count($matches['type']); $i < $len; $i++) { + if (!empty($matches['ns'][$i])) { + $namespace = str_replace([' ', "\t", "\r", "\n"], '', $matches['nsname'][$i]) . '\\'; + } else { + $name = $matches['name'][$i]; + if (':' === $name[0]) { + $name = 'xhp' . substr(str_replace(['-', ':'], ['_', '__'], $name), 1); + } elseif ('enum' === $matches['type'][$i]) { + $name = rtrim($name, ':'); + } + $classes[] = ltrim($namespace . $name, '\\'); + } + } + + return $classes; + } + +} diff --git a/thinkphp/library/think/console/command/optimize/Config.php b/thinkphp/library/think/console/command/optimize/Config.php new file mode 100644 index 0000000..da95556 --- /dev/null +++ b/thinkphp/library/think/console/command/optimize/Config.php @@ -0,0 +1,107 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\console\Command; +use think\console\Input; +use think\console\input\Argument; +use think\console\Output; +use think\Container; +use think\facade\App; + +class Config extends Command +{ + protected function configure() + { + $this->setName('optimize:config') + ->addArgument('module', Argument::OPTIONAL, 'Build module config cache .') + ->setDescription('Build config and common file cache.'); + } + + protected function execute(Input $input, Output $output) + { + if ($input->getArgument('module')) { + $module = $input->getArgument('module') . DIRECTORY_SEPARATOR; + } else { + $module = ''; + } + + $content = 'buildCacheContent($module); + $runtimePath = App::getRuntimePath(); + if (!is_dir($runtimePath . $module)) { + @mkdir($runtimePath . $module, 0755, true); + } + + file_put_contents($runtimePath . $module . 'init.php', $content); + + $output->writeln('Succeed!'); + } + + protected function buildCacheContent($module) + { + $content = '// This cache file is automatically generated at:' . date('Y-m-d H:i:s') . PHP_EOL; + $path = realpath(App::getAppPath() . $module) . DIRECTORY_SEPARATOR; + if ($module) { + $configPath = is_dir($path . 'config') ? $path . 'config' : App::getConfigPath() . $module; + } else { + $configPath = App::getConfigPath(); + } + $ext = App::getConfigExt(); + $config = Container::get('config'); + + $files = is_dir($configPath) ? scandir($configPath) : []; + + foreach ($files as $file) { + if ('.' . pathinfo($file, PATHINFO_EXTENSION) === $ext) { + $filename = $configPath . DIRECTORY_SEPARATOR . $file; + $config->load($filename, pathinfo($file, PATHINFO_FILENAME)); + } + } + + // 加载行为扩展文件 + if (is_file($path . 'tags.php')) { + $tags = include $path . 'tags.php'; + if (is_array($tags)) { + $content .= PHP_EOL . '\think\facade\Hook::import(' . (var_export($tags, true)) . ');' . PHP_EOL; + } + } + + // 加载公共文件 + if (is_file($path . 'common.php')) { + $common = substr(php_strip_whitespace($path . 'common.php'), 6); + if ($common) { + $content .= PHP_EOL . $common . PHP_EOL; + } + } + + if ('' == $module) { + $content .= PHP_EOL . substr(php_strip_whitespace(App::getThinkPath() . 'helper.php'), 6) . PHP_EOL; + + if (is_file($path . 'middleware.php')) { + $middleware = include $path . 'middleware.php'; + if (is_array($middleware)) { + $content .= PHP_EOL . '\think\Container::get("middleware")->import(' . var_export($middleware, true) . ');' . PHP_EOL; + } + } + } + + if (is_file($path . 'provider.php')) { + $provider = include $path . 'provider.php'; + if (is_array($provider)) { + $content .= PHP_EOL . '\think\Container::getInstance()->bindTo(' . var_export($provider, true) . ');' . PHP_EOL; + } + } + + $content .= PHP_EOL . '\think\facade\Config::set(' . var_export($config->get(), true) . ');' . PHP_EOL; + + return $content; + } +} diff --git a/thinkphp/library/think/console/command/optimize/Route.php b/thinkphp/library/think/console/command/optimize/Route.php new file mode 100644 index 0000000..f6dc632 --- /dev/null +++ b/thinkphp/library/think/console/command/optimize/Route.php @@ -0,0 +1,66 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\console\Command; +use think\console\Input; +use think\console\Output; +use think\Container; + +class Route extends Command +{ + protected function configure() + { + $this->setName('optimize:route') + ->setDescription('Build route cache.'); + } + + protected function execute(Input $input, Output $output) + { + $filename = Container::get('app')->getRuntimePath() . 'route.php'; + if (is_file($filename)) { + unlink($filename); + } + file_put_contents($filename, $this->buildRouteCache()); + $output->writeln('Succeed!'); + } + + protected function buildRouteCache() + { + Container::get('route')->setName([]); + Container::get('route')->setTestMode(true); + // 路由检测 + $path = Container::get('app')->getRoutePath(); + + $files = is_dir($path) ? scandir($path) : []; + + foreach ($files as $file) { + if (strpos($file, '.php')) { + $filename = $path . DIRECTORY_SEPARATOR . $file; + // 导入路由配置 + $rules = include $filename; + if (is_array($rules)) { + Container::get('route')->import($rules); + } + } + } + + if (Container::get('config')->get('route_annotation')) { + $suffix = Container::get('config')->get('controller_suffix') || Container::get('config')->get('class_suffix'); + include Container::get('build')->buildRoute($suffix); + } + + $content = 'getName(), true) . ';'; + return $content; + } + +} diff --git a/thinkphp/library/think/console/command/optimize/Schema.php b/thinkphp/library/think/console/command/optimize/Schema.php new file mode 100644 index 0000000..16ac83d --- /dev/null +++ b/thinkphp/library/think/console/command/optimize/Schema.php @@ -0,0 +1,118 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; +use think\Db; +use think\facade\App; + +class Schema extends Command +{ + protected function configure() + { + $this->setName('optimize:schema') + ->addOption('db', null, Option::VALUE_REQUIRED, 'db name .') + ->addOption('table', null, Option::VALUE_REQUIRED, 'table name .') + ->addOption('module', null, Option::VALUE_REQUIRED, 'module name .') + ->setDescription('Build database schema cache.'); + } + + protected function execute(Input $input, Output $output) + { + if (!is_dir(App::getRuntimePath() . 'schema')) { + @mkdir(App::getRuntimePath() . 'schema', 0755, true); + } + + if ($input->hasOption('module')) { + $module = $input->getOption('module'); + // 读取模型 + $path = App::getAppPath() . $module . DIRECTORY_SEPARATOR . 'model'; + $list = is_dir($path) ? scandir($path) : []; + $namespace = App::getNamespace(); + + foreach ($list as $file) { + if (0 === strpos($file, '.')) { + continue; + } + $class = '\\' . $namespace . '\\' . $module . '\\model\\' . pathinfo($file, PATHINFO_FILENAME); + $this->buildModelSchema($class); + } + + $output->writeln('Succeed!'); + return; + } elseif ($input->hasOption('table')) { + $table = $input->getOption('table'); + if (false === strpos($table, '.')) { + $dbName = Db::getConfig('database'); + } + + $tables[] = $table; + } elseif ($input->hasOption('db')) { + $dbName = $input->getOption('db'); + $tables = Db::getConnection()->getTables($dbName); + } elseif (!\think\facade\Config::get('app_multi_module')) { + $namespace = App::getNamespace(); + $path = App::getAppPath() . 'model'; + $list = is_dir($path) ? scandir($path) : []; + + foreach ($list as $file) { + if (0 === strpos($file, '.')) { + continue; + } + $class = '\\' . $namespace . '\\model\\' . pathinfo($file, PATHINFO_FILENAME); + $this->buildModelSchema($class); + } + + $output->writeln('Succeed!'); + return; + } else { + $tables = Db::getConnection()->getTables(); + } + + $db = isset($dbName) ? $dbName . '.' : ''; + $this->buildDataBaseSchema($tables, $db); + + $output->writeln('Succeed!'); + } + + protected function buildModelSchema($class) + { + $reflect = new \ReflectionClass($class); + if (!$reflect->isAbstract() && $reflect->isSubclassOf('\think\Model')) { + $table = $class::getTable(); + $dbName = $class::getConfig('database'); + $content = 'getFields($table); + $content .= var_export($info, true) . ';'; + + file_put_contents(App::getRuntimePath() . 'schema' . DIRECTORY_SEPARATOR . $dbName . '.' . $table . '.php', $content); + } + } + + protected function buildDataBaseSchema($tables, $db) + { + if ('' == $db) { + $dbName = Db::getConfig('database') . '.'; + } else { + $dbName = $db; + } + + foreach ($tables as $table) { + $content = 'getFields($db . $table); + $content .= var_export($info, true) . ';'; + file_put_contents(App::getRuntimePath() . 'schema' . DIRECTORY_SEPARATOR . $dbName . $table . '.php', $content); + } + } +} diff --git a/thinkphp/library/think/console/input/Argument.php b/thinkphp/library/think/console/input/Argument.php new file mode 100644 index 0000000..16223bb --- /dev/null +++ b/thinkphp/library/think/console/input/Argument.php @@ -0,0 +1,115 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\input; + +class Argument +{ + + const REQUIRED = 1; + const OPTIONAL = 2; + const IS_ARRAY = 4; + + private $name; + private $mode; + private $default; + private $description; + + /** + * 构造方法 + * @param string $name 参数名 + * @param int $mode 参数类型: self::REQUIRED 或者 self::OPTIONAL + * @param string $description 描述 + * @param mixed $default 默认值 (仅 self::OPTIONAL 类型有效) + * @throws \InvalidArgumentException + */ + public function __construct($name, $mode = null, $description = '', $default = null) + { + if (null === $mode) { + $mode = self::OPTIONAL; + } elseif (!is_int($mode) || $mode > 7 || $mode < 1) { + throw new \InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode)); + } + + $this->name = $name; + $this->mode = $mode; + $this->description = $description; + + $this->setDefault($default); + } + + /** + * 获取参数名 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 是否必须 + * @return bool + */ + public function isRequired() + { + return self::REQUIRED === (self::REQUIRED & $this->mode); + } + + /** + * 该参数是否接受数组 + * @return bool + */ + public function isArray() + { + return self::IS_ARRAY === (self::IS_ARRAY & $this->mode); + } + + /** + * 设置默认值 + * @param mixed $default 默认值 + * @throws \LogicException + */ + public function setDefault($default = null) + { + if (self::REQUIRED === $this->mode && null !== $default) { + throw new \LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.'); + } + + if ($this->isArray()) { + if (null === $default) { + $default = []; + } elseif (!is_array($default)) { + throw new \LogicException('A default value for an array argument must be an array.'); + } + } + + $this->default = $default; + } + + /** + * 获取默认值 + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * 获取描述 + * @return string + */ + public function getDescription() + { + return $this->description; + } +} diff --git a/thinkphp/library/think/console/input/Definition.php b/thinkphp/library/think/console/input/Definition.php new file mode 100644 index 0000000..c71977e --- /dev/null +++ b/thinkphp/library/think/console/input/Definition.php @@ -0,0 +1,375 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\input; + +class Definition +{ + + /** + * @var Argument[] + */ + private $arguments; + + private $requiredCount; + private $hasAnArrayArgument = false; + private $hasOptional; + + /** + * @var Option[] + */ + private $options; + private $shortcuts; + + /** + * 构造方法 + * @param array $definition + * @api + */ + public function __construct(array $definition = []) + { + $this->setDefinition($definition); + } + + /** + * 设置指令的定义 + * @param array $definition 定义的数组 + */ + public function setDefinition(array $definition) + { + $arguments = []; + $options = []; + foreach ($definition as $item) { + if ($item instanceof Option) { + $options[] = $item; + } else { + $arguments[] = $item; + } + } + + $this->setArguments($arguments); + $this->setOptions($options); + } + + /** + * 设置参数 + * @param Argument[] $arguments 参数数组 + */ + public function setArguments($arguments = []) + { + $this->arguments = []; + $this->requiredCount = 0; + $this->hasOptional = false; + $this->hasAnArrayArgument = false; + $this->addArguments($arguments); + } + + /** + * 添加参数 + * @param Argument[] $arguments 参数数组 + * @api + */ + public function addArguments($arguments = []) + { + if (null !== $arguments) { + foreach ($arguments as $argument) { + $this->addArgument($argument); + } + } + } + + /** + * 添加一个参数 + * @param Argument $argument 参数 + * @throws \LogicException + */ + public function addArgument(Argument $argument) + { + if (isset($this->arguments[$argument->getName()])) { + throw new \LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName())); + } + + if ($this->hasAnArrayArgument) { + throw new \LogicException('Cannot add an argument after an array argument.'); + } + + if ($argument->isRequired() && $this->hasOptional) { + throw new \LogicException('Cannot add a required argument after an optional one.'); + } + + if ($argument->isArray()) { + $this->hasAnArrayArgument = true; + } + + if ($argument->isRequired()) { + ++$this->requiredCount; + } else { + $this->hasOptional = true; + } + + $this->arguments[$argument->getName()] = $argument; + } + + /** + * 根据名称或者位置获取参数 + * @param string|int $name 参数名或者位置 + * @return Argument 参数 + * @throws \InvalidArgumentException + */ + public function getArgument($name) + { + if (!$this->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments; + + return $arguments[$name]; + } + + /** + * 根据名称或位置检查是否具有某个参数 + * @param string|int $name 参数名或者位置 + * @return bool + * @api + */ + public function hasArgument($name) + { + $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments; + + return isset($arguments[$name]); + } + + /** + * 获取所有的参数 + * @return Argument[] 参数数组 + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * 获取参数数量 + * @return int + */ + public function getArgumentCount() + { + return $this->hasAnArrayArgument ? PHP_INT_MAX : count($this->arguments); + } + + /** + * 获取必填的参数的数量 + * @return int + */ + public function getArgumentRequiredCount() + { + return $this->requiredCount; + } + + /** + * 获取参数默认值 + * @return array + */ + public function getArgumentDefaults() + { + $values = []; + foreach ($this->arguments as $argument) { + $values[$argument->getName()] = $argument->getDefault(); + } + + return $values; + } + + /** + * 设置选项 + * @param Option[] $options 选项数组 + */ + public function setOptions($options = []) + { + $this->options = []; + $this->shortcuts = []; + $this->addOptions($options); + } + + /** + * 添加选项 + * @param Option[] $options 选项数组 + * @api + */ + public function addOptions($options = []) + { + foreach ($options as $option) { + $this->addOption($option); + } + } + + /** + * 添加一个选项 + * @param Option $option 选项 + * @throws \LogicException + * @api + */ + public function addOption(Option $option) + { + if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) { + throw new \LogicException(sprintf('An option named "%s" already exists.', $option->getName())); + } + + if ($option->getShortcut()) { + foreach (explode('|', $option->getShortcut()) as $shortcut) { + if (isset($this->shortcuts[$shortcut]) + && !$option->equals($this->options[$this->shortcuts[$shortcut]]) + ) { + throw new \LogicException(sprintf('An option with shortcut "%s" already exists.', $shortcut)); + } + } + } + + $this->options[$option->getName()] = $option; + if ($option->getShortcut()) { + foreach (explode('|', $option->getShortcut()) as $shortcut) { + $this->shortcuts[$shortcut] = $option->getName(); + } + } + } + + /** + * 根据名称获取选项 + * @param string $name 选项名 + * @return Option + * @throws \InvalidArgumentException + * @api + */ + public function getOption($name) + { + if (!$this->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name)); + } + + return $this->options[$name]; + } + + /** + * 根据名称检查是否有这个选项 + * @param string $name 选项名 + * @return bool + * @api + */ + public function hasOption($name) + { + return isset($this->options[$name]); + } + + /** + * 获取所有选项 + * @return Option[] + * @api + */ + public function getOptions() + { + return $this->options; + } + + /** + * 根据名称检查某个选项是否有短名称 + * @param string $name 短名称 + * @return bool + */ + public function hasShortcut($name) + { + return isset($this->shortcuts[$name]); + } + + /** + * 根据短名称获取选项 + * @param string $shortcut 短名称 + * @return Option + */ + public function getOptionForShortcut($shortcut) + { + return $this->getOption($this->shortcutToName($shortcut)); + } + + /** + * 获取所有选项的默认值 + * @return array + */ + public function getOptionDefaults() + { + $values = []; + foreach ($this->options as $option) { + $values[$option->getName()] = $option->getDefault(); + } + + return $values; + } + + /** + * 根据短名称获取选项名 + * @param string $shortcut 短名称 + * @return string + * @throws \InvalidArgumentException + */ + private function shortcutToName($shortcut) + { + if (!isset($this->shortcuts[$shortcut])) { + throw new \InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + return $this->shortcuts[$shortcut]; + } + + /** + * 获取该指令的介绍 + * @param bool $short 是否简洁介绍 + * @return string + */ + public function getSynopsis($short = false) + { + $elements = []; + + if ($short && $this->getOptions()) { + $elements[] = '[options]'; + } elseif (!$short) { + foreach ($this->getOptions() as $option) { + $value = ''; + if ($option->acceptValue()) { + $value = sprintf(' %s%s%s', $option->isValueOptional() ? '[' : '', strtoupper($option->getName()), $option->isValueOptional() ? ']' : ''); + } + + $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : ''; + $elements[] = sprintf('[%s--%s%s]', $shortcut, $option->getName(), $value); + } + } + + if (count($elements) && $this->getArguments()) { + $elements[] = '[--]'; + } + + foreach ($this->getArguments() as $argument) { + $element = '<' . $argument->getName() . '>'; + if (!$argument->isRequired()) { + $element = '[' . $element . ']'; + } elseif ($argument->isArray()) { + $element .= ' (' . $element . ')'; + } + + if ($argument->isArray()) { + $element .= '...'; + } + + $elements[] = $element; + } + + return implode(' ', $elements); + } +} diff --git a/thinkphp/library/think/console/input/Option.php b/thinkphp/library/think/console/input/Option.php new file mode 100644 index 0000000..e5707c9 --- /dev/null +++ b/thinkphp/library/think/console/input/Option.php @@ -0,0 +1,190 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\input; + +class Option +{ + + const VALUE_NONE = 1; + const VALUE_REQUIRED = 2; + const VALUE_OPTIONAL = 4; + const VALUE_IS_ARRAY = 8; + + private $name; + private $shortcut; + private $mode; + private $default; + private $description; + + /** + * 构造方法 + * @param string $name 选项名 + * @param string|array $shortcut 短名称,多个用|隔开或者使用数组 + * @param int $mode 选项类型(可选类型为 self::VALUE_*) + * @param string $description 描述 + * @param mixed $default 默认值 (类型为 self::VALUE_REQUIRED 或者 self::VALUE_NONE 的时候必须为null) + * @throws \InvalidArgumentException + */ + public function __construct($name, $shortcut = null, $mode = null, $description = '', $default = null) + { + if (0 === strpos($name, '--')) { + $name = substr($name, 2); + } + + if (empty($name)) { + throw new \InvalidArgumentException('An option name cannot be empty.'); + } + + if (empty($shortcut)) { + $shortcut = null; + } + + if (null !== $shortcut) { + if (is_array($shortcut)) { + $shortcut = implode('|', $shortcut); + } + $shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-')); + $shortcuts = array_filter($shortcuts); + $shortcut = implode('|', $shortcuts); + + if (empty($shortcut)) { + throw new \InvalidArgumentException('An option shortcut cannot be empty.'); + } + } + + if (null === $mode) { + $mode = self::VALUE_NONE; + } elseif (!is_int($mode) || $mode > 15 || $mode < 1) { + throw new \InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode)); + } + + $this->name = $name; + $this->shortcut = $shortcut; + $this->mode = $mode; + $this->description = $description; + + if ($this->isArray() && !$this->acceptValue()) { + throw new \InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.'); + } + + $this->setDefault($default); + } + + /** + * 获取短名称 + * @return string + */ + public function getShortcut() + { + return $this->shortcut; + } + + /** + * 获取选项名 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 是否可以设置值 + * @return bool 类型不是 self::VALUE_NONE 的时候返回true,其他均返回false + */ + public function acceptValue() + { + return $this->isValueRequired() || $this->isValueOptional(); + } + + /** + * 是否必须 + * @return bool 类型是 self::VALUE_REQUIRED 的时候返回true,其他均返回false + */ + public function isValueRequired() + { + return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode); + } + + /** + * 是否可选 + * @return bool 类型是 self::VALUE_OPTIONAL 的时候返回true,其他均返回false + */ + public function isValueOptional() + { + return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode); + } + + /** + * 选项值是否接受数组 + * @return bool 类型是 self::VALUE_IS_ARRAY 的时候返回true,其他均返回false + */ + public function isArray() + { + return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode); + } + + /** + * 设置默认值 + * @param mixed $default 默认值 + * @throws \LogicException + */ + public function setDefault($default = null) + { + if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) { + throw new \LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.'); + } + + if ($this->isArray()) { + if (null === $default) { + $default = []; + } elseif (!is_array($default)) { + throw new \LogicException('A default value for an array option must be an array.'); + } + } + + $this->default = $this->acceptValue() ? $default : false; + } + + /** + * 获取默认值 + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * 获取描述文字 + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * 检查所给选项是否是当前这个 + * @param Option $option + * @return bool + */ + public function equals(Option $option) + { + return $option->getName() === $this->getName() + && $option->getShortcut() === $this->getShortcut() + && $option->getDefault() === $this->getDefault() + && $option->isArray() === $this->isArray() + && $option->isValueRequired() === $this->isValueRequired() + && $option->isValueOptional() === $this->isValueOptional(); + } +} diff --git a/thinkphp/library/think/console/output/Ask.php b/thinkphp/library/think/console/output/Ask.php new file mode 100644 index 0000000..3933eb2 --- /dev/null +++ b/thinkphp/library/think/console/output/Ask.php @@ -0,0 +1,340 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output; + +use think\console\Input; +use think\console\Output; +use think\console\output\question\Choice; +use think\console\output\question\Confirmation; + +class Ask +{ + private static $stty; + + private static $shell; + + /** @var Input */ + protected $input; + + /** @var Output */ + protected $output; + + /** @var Question */ + protected $question; + + public function __construct(Input $input, Output $output, Question $question) + { + $this->input = $input; + $this->output = $output; + $this->question = $question; + } + + public function run() + { + if (!$this->input->isInteractive()) { + return $this->question->getDefault(); + } + + if (!$this->question->getValidator()) { + return $this->doAsk(); + } + + $that = $this; + + $interviewer = function () use ($that) { + return $that->doAsk(); + }; + + return $this->validateAttempts($interviewer); + } + + protected function doAsk() + { + $this->writePrompt(); + + $inputStream = STDIN; + $autocomplete = $this->question->getAutocompleterValues(); + + if (null === $autocomplete || !$this->hasSttyAvailable()) { + $ret = false; + if ($this->question->isHidden()) { + try { + $ret = trim($this->getHiddenResponse($inputStream)); + } catch (\RuntimeException $e) { + if (!$this->question->isHiddenFallback()) { + throw $e; + } + } + } + + if (false === $ret) { + $ret = fgets($inputStream, 4096); + if (false === $ret) { + throw new \RuntimeException('Aborted'); + } + $ret = trim($ret); + } + } else { + $ret = trim($this->autocomplete($inputStream)); + } + + $ret = strlen($ret) > 0 ? $ret : $this->question->getDefault(); + + if ($normalizer = $this->question->getNormalizer()) { + return $normalizer($ret); + } + + return $ret; + } + + private function autocomplete($inputStream) + { + $autocomplete = $this->question->getAutocompleterValues(); + $ret = ''; + + $i = 0; + $ofs = -1; + $matches = $autocomplete; + $numMatches = count($matches); + + $sttyMode = shell_exec('stty -g'); + + shell_exec('stty -icanon -echo'); + + while (!feof($inputStream)) { + $c = fread($inputStream, 1); + + if ("\177" === $c) { + if (0 === $numMatches && 0 !== $i) { + --$i; + $this->output->write("\033[1D"); + } + + if ($i === 0) { + $ofs = -1; + $matches = $autocomplete; + $numMatches = count($matches); + } else { + $numMatches = 0; + } + + $ret = substr($ret, 0, $i); + } elseif ("\033" === $c) { + $c .= fread($inputStream, 2); + + if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) { + if ('A' === $c[2] && -1 === $ofs) { + $ofs = 0; + } + + if (0 === $numMatches) { + continue; + } + + $ofs += ('A' === $c[2]) ? -1 : 1; + $ofs = ($numMatches + $ofs) % $numMatches; + } + } elseif (ord($c) < 32) { + if ("\t" === $c || "\n" === $c) { + if ($numMatches > 0 && -1 !== $ofs) { + $ret = $matches[$ofs]; + $this->output->write(substr($ret, $i)); + $i = strlen($ret); + } + + if ("\n" === $c) { + $this->output->write($c); + break; + } + + $numMatches = 0; + } + + continue; + } else { + $this->output->write($c); + $ret .= $c; + ++$i; + + $numMatches = 0; + $ofs = 0; + + foreach ($autocomplete as $value) { + if (0 === strpos($value, $ret) && $i !== strlen($value)) { + $matches[$numMatches++] = $value; + } + } + } + + $this->output->write("\033[K"); + + if ($numMatches > 0 && -1 !== $ofs) { + $this->output->write("\0337"); + $this->output->highlight(substr($matches[$ofs], $i)); + $this->output->write("\0338"); + } + } + + shell_exec(sprintf('stty %s', $sttyMode)); + + return $ret; + } + + protected function getHiddenResponse($inputStream) + { + if ('\\' === DIRECTORY_SEPARATOR) { + $exe = __DIR__ . '/../bin/hiddeninput.exe'; + + $value = rtrim(shell_exec($exe)); + $this->output->writeln(''); + + if (isset($tmpExe)) { + unlink($tmpExe); + } + + return $value; + } + + if ($this->hasSttyAvailable()) { + $sttyMode = shell_exec('stty -g'); + + shell_exec('stty -echo'); + $value = fgets($inputStream, 4096); + shell_exec(sprintf('stty %s', $sttyMode)); + + if (false === $value) { + throw new \RuntimeException('Aborted'); + } + + $value = trim($value); + $this->output->writeln(''); + + return $value; + } + + if (false !== $shell = $this->getShell()) { + $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword'; + $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd); + $value = rtrim(shell_exec($command)); + $this->output->writeln(''); + + return $value; + } + + throw new \RuntimeException('Unable to hide the response.'); + } + + protected function validateAttempts($interviewer) + { + /** @var \Exception $error */ + $error = null; + $attempts = $this->question->getMaxAttempts(); + while (null === $attempts || $attempts--) { + if (null !== $error) { + $this->output->error($error->getMessage()); + } + + try { + return call_user_func($this->question->getValidator(), $interviewer()); + } catch (\Exception $error) { + } + } + + throw $error; + } + + /** + * 显示问题的提示信息 + */ + protected function writePrompt() + { + $text = $this->question->getQuestion(); + $default = $this->question->getDefault(); + + switch (true) { + case null === $default: + $text = sprintf(' %s:', $text); + + break; + + case $this->question instanceof Confirmation: + $text = sprintf(' %s (yes/no) [%s]:', $text, $default ? 'yes' : 'no'); + + break; + + case $this->question instanceof Choice && $this->question->isMultiselect(): + $choices = $this->question->getChoices(); + $default = explode(',', $default); + + foreach ($default as $key => $value) { + $default[$key] = $choices[trim($value)]; + } + + $text = sprintf(' %s [%s]:', $text, implode(', ', $default)); + + break; + + case $this->question instanceof Choice: + $choices = $this->question->getChoices(); + $text = sprintf(' %s [%s]:', $text, $choices[$default]); + + break; + + default: + $text = sprintf(' %s [%s]:', $text, $default); + } + + $this->output->writeln($text); + + if ($this->question instanceof Choice) { + $width = max(array_map('strlen', array_keys($this->question->getChoices()))); + + foreach ($this->question->getChoices() as $key => $value) { + $this->output->writeln(sprintf(" [%-${width}s] %s", $key, $value)); + } + } + + $this->output->write(' > '); + } + + private function getShell() + { + if (null !== self::$shell) { + return self::$shell; + } + + self::$shell = false; + + if (file_exists('/usr/bin/env')) { + $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null"; + foreach (['bash', 'zsh', 'ksh', 'csh'] as $sh) { + if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) { + self::$shell = $sh; + break; + } + } + } + + return self::$shell; + } + + private function hasSttyAvailable() + { + if (null !== self::$stty) { + return self::$stty; + } + + exec('stty 2>&1', $output, $exitcode); + + return self::$stty = $exitcode === 0; + } +} diff --git a/thinkphp/library/think/console/output/Descriptor.php b/thinkphp/library/think/console/output/Descriptor.php new file mode 100644 index 0000000..6d98d53 --- /dev/null +++ b/thinkphp/library/think/console/output/Descriptor.php @@ -0,0 +1,319 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output; + +use think\Console; +use think\console\Command; +use think\console\input\Argument as InputArgument; +use think\console\input\Definition as InputDefinition; +use think\console\input\Option as InputOption; +use think\console\Output; +use think\console\output\descriptor\Console as ConsoleDescription; + +class Descriptor +{ + + /** + * @var Output + */ + protected $output; + + /** + * {@inheritdoc} + */ + public function describe(Output $output, $object, array $options = []) + { + $this->output = $output; + + switch (true) { + case $object instanceof InputArgument: + $this->describeInputArgument($object, $options); + break; + case $object instanceof InputOption: + $this->describeInputOption($object, $options); + break; + case $object instanceof InputDefinition: + $this->describeInputDefinition($object, $options); + break; + case $object instanceof Command: + $this->describeCommand($object, $options); + break; + case $object instanceof Console: + $this->describeConsole($object, $options); + break; + default: + throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object))); + } + } + + /** + * 输出内容 + * @param string $content + * @param bool $decorated + */ + protected function write($content, $decorated = false) + { + $this->output->write($content, false, $decorated ? Output::OUTPUT_NORMAL : Output::OUTPUT_RAW); + } + + /** + * 描述参数 + * @param InputArgument $argument + * @param array $options + * @return string|mixed + */ + protected function describeInputArgument(InputArgument $argument, array $options = []) + { + if (null !== $argument->getDefault() + && (!is_array($argument->getDefault()) + || count($argument->getDefault())) + ) { + $default = sprintf(' [default: %s]', $this->formatDefaultValue($argument->getDefault())); + } else { + $default = ''; + } + + $totalWidth = isset($options['total_width']) ? $options['total_width'] : strlen($argument->getName()); + $spacingWidth = $totalWidth - strlen($argument->getName()) + 2; + + $this->writeText(sprintf(" %s%s%s%s", $argument->getName(), str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + + + 2 spaces + preg_replace('/\s*\R\s*/', PHP_EOL . str_repeat(' ', $totalWidth + 17), $argument->getDescription()), $default), $options); + } + + /** + * 描述选项 + * @param InputOption $option + * @param array $options + * @return string|mixed + */ + protected function describeInputOption(InputOption $option, array $options = []) + { + if ($option->acceptValue() && null !== $option->getDefault() + && (!is_array($option->getDefault()) + || count($option->getDefault())) + ) { + $default = sprintf(' [default: %s]', $this->formatDefaultValue($option->getDefault())); + } else { + $default = ''; + } + + $value = ''; + if ($option->acceptValue()) { + $value = '=' . strtoupper($option->getName()); + + if ($option->isValueOptional()) { + $value = '[' . $value . ']'; + } + } + + $totalWidth = isset($options['total_width']) ? $options['total_width'] : $this->calculateTotalWidthForOptions([$option]); + $synopsis = sprintf('%s%s', $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ', sprintf('--%s%s', $option->getName(), $value)); + + $spacingWidth = $totalWidth - strlen($synopsis) + 2; + + $this->writeText(sprintf(" %s%s%s%s%s", $synopsis, str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + + + 2 spaces + preg_replace('/\s*\R\s*/', "\n" . str_repeat(' ', $totalWidth + 17), $option->getDescription()), $default, $option->isArray() ? ' (multiple values allowed)' : ''), $options); + } + + /** + * 描述输入 + * @param InputDefinition $definition + * @param array $options + * @return string|mixed + */ + protected function describeInputDefinition(InputDefinition $definition, array $options = []) + { + $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions()); + foreach ($definition->getArguments() as $argument) { + $totalWidth = max($totalWidth, strlen($argument->getName())); + } + + if ($definition->getArguments()) { + $this->writeText('Arguments:', $options); + $this->writeText("\n"); + foreach ($definition->getArguments() as $argument) { + $this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth])); + $this->writeText("\n"); + } + } + + if ($definition->getArguments() && $definition->getOptions()) { + $this->writeText("\n"); + } + + if ($definition->getOptions()) { + $laterOptions = []; + + $this->writeText('Options:', $options); + foreach ($definition->getOptions() as $option) { + if (strlen($option->getShortcut()) > 1) { + $laterOptions[] = $option; + continue; + } + $this->writeText("\n"); + $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); + } + foreach ($laterOptions as $option) { + $this->writeText("\n"); + $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); + } + } + } + + /** + * 描述指令 + * @param Command $command + * @param array $options + * @return string|mixed + */ + protected function describeCommand(Command $command, array $options = []) + { + $command->getSynopsis(true); + $command->getSynopsis(false); + $command->mergeConsoleDefinition(false); + + $this->writeText('Usage:', $options); + foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) { + $this->writeText("\n"); + $this->writeText(' ' . $usage, $options); + } + $this->writeText("\n"); + + $definition = $command->getNativeDefinition(); + if ($definition->getOptions() || $definition->getArguments()) { + $this->writeText("\n"); + $this->describeInputDefinition($definition, $options); + $this->writeText("\n"); + } + + if ($help = $command->getProcessedHelp()) { + $this->writeText("\n"); + $this->writeText('Help:', $options); + $this->writeText("\n"); + $this->writeText(' ' . str_replace("\n", "\n ", $help), $options); + $this->writeText("\n"); + } + } + + /** + * 描述控制台 + * @param Console $console + * @param array $options + * @return string|mixed + */ + protected function describeConsole(Console $console, array $options = []) + { + $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null; + $description = new ConsoleDescription($console, $describedNamespace); + + if (isset($options['raw_text']) && $options['raw_text']) { + $width = $this->getColumnWidth($description->getCommands()); + + foreach ($description->getCommands() as $command) { + $this->writeText(sprintf("%-${width}s %s", $command->getName(), $command->getDescription()), $options); + $this->writeText("\n"); + } + } else { + if ('' != $help = $console->getHelp()) { + $this->writeText("$help\n\n", $options); + } + + $this->writeText("Usage:\n", $options); + $this->writeText(" command [options] [arguments]\n\n", $options); + + $this->describeInputDefinition(new InputDefinition($console->getDefinition()->getOptions()), $options); + + $this->writeText("\n"); + $this->writeText("\n"); + + $width = $this->getColumnWidth($description->getCommands()); + + if ($describedNamespace) { + $this->writeText(sprintf('Available commands for the "%s" namespace:', $describedNamespace), $options); + } else { + $this->writeText('Available commands:', $options); + } + + // add commands by namespace + foreach ($description->getNamespaces() as $namespace) { + if (!$describedNamespace && ConsoleDescription::GLOBAL_NAMESPACE !== $namespace['id']) { + $this->writeText("\n"); + $this->writeText(' ' . $namespace['id'] . '', $options); + } + + foreach ($namespace['commands'] as $name) { + $this->writeText("\n"); + $spacingWidth = $width - strlen($name); + $this->writeText(sprintf(" %s%s%s", $name, str_repeat(' ', $spacingWidth), $description->getCommand($name) + ->getDescription()), $options); + } + } + + $this->writeText("\n"); + } + } + + /** + * {@inheritdoc} + */ + private function writeText($content, array $options = []) + { + $this->write(isset($options['raw_text']) + && $options['raw_text'] ? strip_tags($content) : $content, isset($options['raw_output']) ? !$options['raw_output'] : true); + } + + /** + * 格式化 + * @param mixed $default + * @return string + */ + private function formatDefaultValue($default) + { + return json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + + /** + * @param Command[] $commands + * @return int + */ + private function getColumnWidth(array $commands) + { + $width = 0; + foreach ($commands as $command) { + $width = strlen($command->getName()) > $width ? strlen($command->getName()) : $width; + } + + return $width + 2; + } + + /** + * @param InputOption[] $options + * @return int + */ + private function calculateTotalWidthForOptions($options) + { + $totalWidth = 0; + foreach ($options as $option) { + $nameLength = 4 + strlen($option->getName()) + 2; // - + shortcut + , + whitespace + name + -- + + if ($option->acceptValue()) { + $valueLength = 1 + strlen($option->getName()); // = + value + $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ] + + $nameLength += $valueLength; + } + $totalWidth = max($totalWidth, $nameLength); + } + + return $totalWidth; + } +} diff --git a/thinkphp/library/think/console/output/Formatter.php b/thinkphp/library/think/console/output/Formatter.php new file mode 100644 index 0000000..f8bee55 --- /dev/null +++ b/thinkphp/library/think/console/output/Formatter.php @@ -0,0 +1,198 @@ + +// +---------------------------------------------------------------------- +namespace think\console\output; + +use think\console\output\formatter\Stack as StyleStack; +use think\console\output\formatter\Style; + +class Formatter +{ + + private $decorated = false; + private $styles = []; + private $styleStack; + + /** + * 转义 + * @param string $text + * @return string + */ + public static function escape($text) + { + return preg_replace('/([^\\\\]?)setStyle('error', new Style('white', 'red')); + $this->setStyle('info', new Style('green')); + $this->setStyle('comment', new Style('yellow')); + $this->setStyle('question', new Style('black', 'cyan')); + $this->setStyle('highlight', new Style('red')); + $this->setStyle('warning', new Style('black', 'yellow')); + + $this->styleStack = new StyleStack(); + } + + /** + * 设置外观标识 + * @param bool $decorated 是否美化文字 + */ + public function setDecorated($decorated) + { + $this->decorated = (bool) $decorated; + } + + /** + * 获取外观标识 + * @return bool + */ + public function isDecorated() + { + return $this->decorated; + } + + /** + * 添加一个新样式 + * @param string $name 样式名 + * @param Style $style 样式实例 + */ + public function setStyle($name, Style $style) + { + $this->styles[strtolower($name)] = $style; + } + + /** + * 是否有这个样式 + * @param string $name + * @return bool + */ + public function hasStyle($name) + { + return isset($this->styles[strtolower($name)]); + } + + /** + * 获取样式 + * @param string $name + * @return Style + * @throws \InvalidArgumentException + */ + public function getStyle($name) + { + if (!$this->hasStyle($name)) { + throw new \InvalidArgumentException(sprintf('Undefined style: %s', $name)); + } + + return $this->styles[strtolower($name)]; + } + + /** + * 使用所给的样式格式化文字 + * @param string $message 文字 + * @return string + */ + public function format($message) + { + $offset = 0; + $output = ''; + $tagRegex = '[a-z][a-z0-9_=;-]*'; + preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#isx", $message, $matches, PREG_OFFSET_CAPTURE); + foreach ($matches[0] as $i => $match) { + $pos = $match[1]; + $text = $match[0]; + + if (0 != $pos && '\\' == $message[$pos - 1]) { + continue; + } + + $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset)); + $offset = $pos + strlen($text); + + if ($open = '/' != $text[1]) { + $tag = $matches[1][$i][0]; + } else { + $tag = isset($matches[3][$i][0]) ? $matches[3][$i][0] : ''; + } + + if (!$open && !$tag) { + // + $this->styleStack->pop(); + } elseif (false === $style = $this->createStyleFromString(strtolower($tag))) { + $output .= $this->applyCurrentStyle($text); + } elseif ($open) { + $this->styleStack->push($style); + } else { + $this->styleStack->pop($style); + } + } + + $output .= $this->applyCurrentStyle(substr($message, $offset)); + + return str_replace('\\<', '<', $output); + } + + /** + * @return StyleStack + */ + public function getStyleStack() + { + return $this->styleStack; + } + + /** + * 根据字符串创建新的样式实例 + * @param string $string + * @return Style|bool + */ + private function createStyleFromString($string) + { + if (isset($this->styles[$string])) { + return $this->styles[$string]; + } + + if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', strtolower($string), $matches, PREG_SET_ORDER)) { + return false; + } + + $style = new Style(); + foreach ($matches as $match) { + array_shift($match); + + if ('fg' == $match[0]) { + $style->setForeground($match[1]); + } elseif ('bg' == $match[0]) { + $style->setBackground($match[1]); + } else { + try { + $style->setOption($match[1]); + } catch (\InvalidArgumentException $e) { + return false; + } + } + } + + return $style; + } + + /** + * 从堆栈应用样式到文字 + * @param string $text 文字 + * @return string + */ + private function applyCurrentStyle($text) + { + return $this->isDecorated() && strlen($text) > 0 ? $this->styleStack->getCurrent()->apply($text) : $text; + } +} diff --git a/thinkphp/library/think/console/output/Question.php b/thinkphp/library/think/console/output/Question.php new file mode 100644 index 0000000..03975f2 --- /dev/null +++ b/thinkphp/library/think/console/output/Question.php @@ -0,0 +1,211 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output; + +class Question +{ + + private $question; + private $attempts; + private $hidden = false; + private $hiddenFallback = true; + private $autocompleterValues; + private $validator; + private $default; + private $normalizer; + + /** + * 构造方法 + * @param string $question 问题 + * @param mixed $default 默认答案 + */ + public function __construct($question, $default = null) + { + $this->question = $question; + $this->default = $default; + } + + /** + * 获取问题 + * @return string + */ + public function getQuestion() + { + return $this->question; + } + + /** + * 获取默认答案 + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * 是否隐藏答案 + * @return bool + */ + public function isHidden() + { + return $this->hidden; + } + + /** + * 隐藏答案 + * @param bool $hidden + * @return Question + */ + public function setHidden($hidden) + { + if ($this->autocompleterValues) { + throw new \LogicException('A hidden question cannot use the autocompleter.'); + } + + $this->hidden = (bool) $hidden; + + return $this; + } + + /** + * 不能被隐藏是否撤销 + * @return bool + */ + public function isHiddenFallback() + { + return $this->hiddenFallback; + } + + /** + * 设置不能被隐藏的时候的操作 + * @param bool $fallback + * @return Question + */ + public function setHiddenFallback($fallback) + { + $this->hiddenFallback = (bool) $fallback; + + return $this; + } + + /** + * 获取自动完成 + * @return null|array|\Traversable + */ + public function getAutocompleterValues() + { + return $this->autocompleterValues; + } + + /** + * 设置自动完成的值 + * @param null|array|\Traversable $values + * @return Question + * @throws \InvalidArgumentException + * @throws \LogicException + */ + public function setAutocompleterValues($values) + { + if (is_array($values) && $this->isAssoc($values)) { + $values = array_merge(array_keys($values), array_values($values)); + } + + if (null !== $values && !is_array($values)) { + if (!$values instanceof \Traversable || $values instanceof \Countable) { + throw new \InvalidArgumentException('Autocompleter values can be either an array, `null` or an object implementing both `Countable` and `Traversable` interfaces.'); + } + } + + if ($this->hidden) { + throw new \LogicException('A hidden question cannot use the autocompleter.'); + } + + $this->autocompleterValues = $values; + + return $this; + } + + /** + * 设置答案的验证器 + * @param null|callable $validator + * @return Question The current instance + */ + public function setValidator($validator) + { + $this->validator = $validator; + + return $this; + } + + /** + * 获取验证器 + * @return null|callable + */ + public function getValidator() + { + return $this->validator; + } + + /** + * 设置最大重试次数 + * @param null|int $attempts + * @return Question + * @throws \InvalidArgumentException + */ + public function setMaxAttempts($attempts) + { + if (null !== $attempts && $attempts < 1) { + throw new \InvalidArgumentException('Maximum number of attempts must be a positive value.'); + } + + $this->attempts = $attempts; + + return $this; + } + + /** + * 获取最大重试次数 + * @return null|int + */ + public function getMaxAttempts() + { + return $this->attempts; + } + + /** + * 设置响应的回调 + * @param string|\Closure $normalizer + * @return Question + */ + public function setNormalizer($normalizer) + { + $this->normalizer = $normalizer; + + return $this; + } + + /** + * 获取响应回调 + * The normalizer can ba a callable (a string), a closure or a class implementing __invoke. + * @return string|\Closure + */ + public function getNormalizer() + { + return $this->normalizer; + } + + protected function isAssoc($array) + { + return (bool) count(array_filter(array_keys($array), 'is_string')); + } +} diff --git a/thinkphp/library/think/console/output/descriptor/Console.php b/thinkphp/library/think/console/output/descriptor/Console.php new file mode 100644 index 0000000..8739c53 --- /dev/null +++ b/thinkphp/library/think/console/output/descriptor/Console.php @@ -0,0 +1,153 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\descriptor; + +use think\Console as ThinkConsole; +use think\console\Command; + +class Console +{ + + const GLOBAL_NAMESPACE = '_global'; + + /** + * @var ThinkConsole + */ + private $console; + + /** + * @var null|string + */ + private $namespace; + + /** + * @var array + */ + private $namespaces; + + /** + * @var Command[] + */ + private $commands; + + /** + * @var Command[] + */ + private $aliases; + + /** + * 构造方法 + * @param ThinkConsole $console + * @param string|null $namespace + */ + public function __construct(ThinkConsole $console, $namespace = null) + { + $this->console = $console; + $this->namespace = $namespace; + } + + /** + * @return array + */ + public function getNamespaces() + { + if (null === $this->namespaces) { + $this->inspectConsole(); + } + + return $this->namespaces; + } + + /** + * @return Command[] + */ + public function getCommands() + { + if (null === $this->commands) { + $this->inspectConsole(); + } + + return $this->commands; + } + + /** + * @param string $name + * @return Command + * @throws \InvalidArgumentException + */ + public function getCommand($name) + { + if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) { + throw new \InvalidArgumentException(sprintf('Command %s does not exist.', $name)); + } + + return isset($this->commands[$name]) ? $this->commands[$name] : $this->aliases[$name]; + } + + private function inspectConsole() + { + $this->commands = []; + $this->namespaces = []; + + $all = $this->console->all($this->namespace ? $this->console->findNamespace($this->namespace) : null); + foreach ($this->sortCommands($all) as $namespace => $commands) { + $names = []; + + /** @var Command $command */ + foreach ($commands as $name => $command) { + if (is_string($command)) { + $command = new $command(); + } + + if (!$command->getName()) { + continue; + } + + if ($command->getName() === $name) { + $this->commands[$name] = $command; + } else { + $this->aliases[$name] = $command; + } + + $names[] = $name; + } + + $this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names]; + } + } + + /** + * @param array $commands + * @return array + */ + private function sortCommands(array $commands) + { + $namespacedCommands = []; + foreach ($commands as $name => $command) { + $key = $this->console->extractNamespace($name, 1); + if (!$key) { + $key = self::GLOBAL_NAMESPACE; + } + + $namespacedCommands[$key][$name] = $command; + } + ksort($namespacedCommands); + + foreach ($namespacedCommands as &$commandsSet) { + ksort($commandsSet); + } + // unset reference to keep scope clear + unset($commandsSet); + + return $namespacedCommands; + } +} diff --git a/thinkphp/library/think/console/output/driver/Buffer.php b/thinkphp/library/think/console/output/driver/Buffer.php new file mode 100644 index 0000000..c77a2ec --- /dev/null +++ b/thinkphp/library/think/console/output/driver/Buffer.php @@ -0,0 +1,52 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\driver; + +use think\console\Output; + +class Buffer +{ + /** + * @var string + */ + private $buffer = ''; + + public function __construct(Output $output) + { + // do nothing + } + + public function fetch() + { + $content = $this->buffer; + $this->buffer = ''; + return $content; + } + + public function write($messages, $newline = false, $options = Output::OUTPUT_NORMAL) + { + $messages = (array) $messages; + + foreach ($messages as $message) { + $this->buffer .= $message; + } + if ($newline) { + $this->buffer .= "\n"; + } + } + + public function renderException(\Exception $e) + { + // do nothing + } + +} diff --git a/thinkphp/library/think/console/output/driver/Console.php b/thinkphp/library/think/console/output/driver/Console.php new file mode 100644 index 0000000..e041b52 --- /dev/null +++ b/thinkphp/library/think/console/output/driver/Console.php @@ -0,0 +1,368 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\driver; + +use think\console\Output; +use think\console\output\Formatter; + +class Console +{ + + /** @var Resource */ + private $stdout; + + /** @var Formatter */ + private $formatter; + + private $terminalDimensions; + + /** @var Output */ + private $output; + + public function __construct(Output $output) + { + $this->output = $output; + $this->formatter = new Formatter(); + $this->stdout = $this->openOutputStream(); + $decorated = $this->hasColorSupport($this->stdout); + $this->formatter->setDecorated($decorated); + } + + public function setDecorated($decorated) + { + $this->formatter->setDecorated($decorated); + } + + public function write($messages, $newline = false, $type = Output::OUTPUT_NORMAL, $stream = null) + { + if (Output::VERBOSITY_QUIET === $this->output->getVerbosity()) { + return; + } + + $messages = (array) $messages; + + foreach ($messages as $message) { + switch ($type) { + case Output::OUTPUT_NORMAL: + $message = $this->formatter->format($message); + break; + case Output::OUTPUT_RAW: + break; + case Output::OUTPUT_PLAIN: + $message = strip_tags($this->formatter->format($message)); + break; + default: + throw new \InvalidArgumentException(sprintf('Unknown output type given (%s)', $type)); + } + + $this->doWrite($message, $newline, $stream); + } + } + + public function renderException(\Exception $e) + { + $stderr = $this->openErrorStream(); + $decorated = $this->hasColorSupport($stderr); + $this->formatter->setDecorated($decorated); + + do { + $title = sprintf(' [%s] ', get_class($e)); + + $len = $this->stringWidth($title); + + $width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : PHP_INT_MAX; + + if (defined('HHVM_VERSION') && $width > 1 << 31) { + $width = 1 << 31; + } + $lines = []; + foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) { + foreach ($this->splitStringByWidth($line, $width - 4) as $line) { + + $lineLength = $this->stringWidth(preg_replace('/\[[^m]*m/', '', $line)) + 4; + $lines[] = [$line, $lineLength]; + + $len = max($lineLength, $len); + } + } + + $messages = ['', '']; + $messages[] = $emptyLine = sprintf('%s', str_repeat(' ', $len)); + $messages[] = sprintf('%s%s', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title)))); + foreach ($lines as $line) { + $messages[] = sprintf(' %s %s', $line[0], str_repeat(' ', $len - $line[1])); + } + $messages[] = $emptyLine; + $messages[] = ''; + $messages[] = ''; + + $this->write($messages, true, Output::OUTPUT_NORMAL, $stderr); + + if (Output::VERBOSITY_VERBOSE <= $this->output->getVerbosity()) { + $this->write('Exception trace:', true, Output::OUTPUT_NORMAL, $stderr); + + // exception related properties + $trace = $e->getTrace(); + array_unshift($trace, [ + 'function' => '', + 'file' => $e->getFile() !== null ? $e->getFile() : 'n/a', + 'line' => $e->getLine() !== null ? $e->getLine() : 'n/a', + 'args' => [], + ]); + + for ($i = 0, $count = count($trace); $i < $count; ++$i) { + $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : ''; + $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : ''; + $function = $trace[$i]['function']; + $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a'; + $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a'; + + $this->write(sprintf(' %s%s%s() at %s:%s', $class, $type, $function, $file, $line), true, Output::OUTPUT_NORMAL, $stderr); + } + + $this->write('', true, Output::OUTPUT_NORMAL, $stderr); + $this->write('', true, Output::OUTPUT_NORMAL, $stderr); + } + } while ($e = $e->getPrevious()); + + } + + /** + * 获取终端宽度 + * @return int|null + */ + protected function getTerminalWidth() + { + $dimensions = $this->getTerminalDimensions(); + + return $dimensions[0]; + } + + /** + * 获取终端高度 + * @return int|null + */ + protected function getTerminalHeight() + { + $dimensions = $this->getTerminalDimensions(); + + return $dimensions[1]; + } + + /** + * 获取当前终端的尺寸 + * @return array + */ + public function getTerminalDimensions() + { + if ($this->terminalDimensions) { + return $this->terminalDimensions; + } + + if ('\\' === DIRECTORY_SEPARATOR) { + if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) { + return [(int) $matches[1], (int) $matches[2]]; + } + if (preg_match('/^(\d+)x(\d+)$/', $this->getMode(), $matches)) { + return [(int) $matches[1], (int) $matches[2]]; + } + } + + if ($sttyString = $this->getSttyColumns()) { + if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) { + return [(int) $matches[2], (int) $matches[1]]; + } + if (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) { + return [(int) $matches[2], (int) $matches[1]]; + } + } + + return [null, null]; + } + + /** + * 获取stty列数 + * @return string + */ + private function getSttyColumns() + { + if (!function_exists('proc_open')) { + return; + } + + $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; + $process = proc_open('stty -a | grep columns', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]); + if (is_resource($process)) { + $info = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + return $info; + } + return; + } + + /** + * 获取终端模式 + * @return string x 或 null + */ + private function getMode() + { + if (!function_exists('proc_open')) { + return; + } + + $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; + $process = proc_open('mode CON', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]); + if (is_resource($process)) { + $info = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { + return $matches[2] . 'x' . $matches[1]; + } + } + return; + } + + private function stringWidth($string) + { + if (!function_exists('mb_strwidth')) { + return strlen($string); + } + + if (false === $encoding = mb_detect_encoding($string)) { + return strlen($string); + } + + return mb_strwidth($string, $encoding); + } + + private function splitStringByWidth($string, $width) + { + if (!function_exists('mb_strwidth')) { + return str_split($string, $width); + } + + if (false === $encoding = mb_detect_encoding($string)) { + return str_split($string, $width); + } + + $utf8String = mb_convert_encoding($string, 'utf8', $encoding); + $lines = []; + $line = ''; + foreach (preg_split('//u', $utf8String) as $char) { + if (mb_strwidth($line . $char, 'utf8') <= $width) { + $line .= $char; + continue; + } + $lines[] = str_pad($line, $width); + $line = $char; + } + if (strlen($line)) { + $lines[] = count($lines) ? str_pad($line, $width) : $line; + } + + mb_convert_variables($encoding, 'utf8', $lines); + + return $lines; + } + + private function isRunningOS400() + { + $checks = [ + function_exists('php_uname') ? php_uname('s') : '', + getenv('OSTYPE'), + PHP_OS, + ]; + return false !== stripos(implode(';', $checks), 'OS400'); + } + + /** + * 当前环境是否支持写入控制台输出到stdout. + * + * @return bool + */ + protected function hasStdoutSupport() + { + return false === $this->isRunningOS400(); + } + + /** + * 当前环境是否支持写入控制台输出到stderr. + * + * @return bool + */ + protected function hasStderrSupport() + { + return false === $this->isRunningOS400(); + } + + /** + * @return resource + */ + private function openOutputStream() + { + if (!$this->hasStdoutSupport()) { + return fopen('php://output', 'w'); + } + return @fopen('php://stdout', 'w') ?: fopen('php://output', 'w'); + } + + /** + * @return resource + */ + private function openErrorStream() + { + return fopen($this->hasStderrSupport() ? 'php://stderr' : 'php://output', 'w'); + } + + /** + * 将消息写入到输出。 + * @param string $message 消息 + * @param bool $newline 是否另起一行 + * @param null $stream + */ + protected function doWrite($message, $newline, $stream = null) + { + if (null === $stream) { + $stream = $this->stdout; + } + if (false === @fwrite($stream, $message . ($newline ? PHP_EOL : ''))) { + throw new \RuntimeException('Unable to write output.'); + } + + fflush($stream); + } + + /** + * 是否支持着色 + * @param $stream + * @return bool + */ + protected function hasColorSupport($stream) + { + if (DIRECTORY_SEPARATOR === '\\') { + return + '10.0.10586' === PHP_WINDOWS_VERSION_MAJOR . '.' . PHP_WINDOWS_VERSION_MINOR . '.' . PHP_WINDOWS_VERSION_BUILD + || false !== getenv('ANSICON') + || 'ON' === getenv('ConEmuANSI') + || 'xterm' === getenv('TERM'); + } + + return function_exists('posix_isatty') && @posix_isatty($stream); + } + +} diff --git a/thinkphp/library/think/console/output/driver/Nothing.php b/thinkphp/library/think/console/output/driver/Nothing.php new file mode 100644 index 0000000..9a55f77 --- /dev/null +++ b/thinkphp/library/think/console/output/driver/Nothing.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\driver; + +use think\console\Output; + +class Nothing +{ + + public function __construct(Output $output) + { + // do nothing + } + + public function write($messages, $newline = false, $options = Output::OUTPUT_NORMAL) + { + // do nothing + } + + public function renderException(\Exception $e) + { + // do nothing + } +} diff --git a/thinkphp/library/think/console/output/formatter/Stack.php b/thinkphp/library/think/console/output/formatter/Stack.php new file mode 100644 index 0000000..4864a3f --- /dev/null +++ b/thinkphp/library/think/console/output/formatter/Stack.php @@ -0,0 +1,116 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\formatter; + +class Stack +{ + + /** + * @var Style[] + */ + private $styles; + + /** + * @var Style + */ + private $emptyStyle; + + /** + * 构造方法 + * @param Style|null $emptyStyle + */ + public function __construct(Style $emptyStyle = null) + { + $this->emptyStyle = $emptyStyle ?: new Style(); + $this->reset(); + } + + /** + * 重置堆栈 + */ + public function reset() + { + $this->styles = []; + } + + /** + * 推一个样式进入堆栈 + * @param Style $style + */ + public function push(Style $style) + { + $this->styles[] = $style; + } + + /** + * 从堆栈中弹出一个样式 + * @param Style|null $style + * @return Style + * @throws \InvalidArgumentException + */ + public function pop(Style $style = null) + { + if (empty($this->styles)) { + return $this->emptyStyle; + } + + if (null === $style) { + return array_pop($this->styles); + } + + /** + * @var int $index + * @var Style $stackedStyle + */ + foreach (array_reverse($this->styles, true) as $index => $stackedStyle) { + if ($style->apply('') === $stackedStyle->apply('')) { + $this->styles = array_slice($this->styles, 0, $index); + + return $stackedStyle; + } + } + + throw new \InvalidArgumentException('Incorrectly nested style tag found.'); + } + + /** + * 计算堆栈的当前样式。 + * @return Style + */ + public function getCurrent() + { + if (empty($this->styles)) { + return $this->emptyStyle; + } + + return $this->styles[count($this->styles) - 1]; + } + + /** + * @param Style $emptyStyle + * @return Stack + */ + public function setEmptyStyle(Style $emptyStyle) + { + $this->emptyStyle = $emptyStyle; + + return $this; + } + + /** + * @return Style + */ + public function getEmptyStyle() + { + return $this->emptyStyle; + } +} diff --git a/thinkphp/library/think/console/output/formatter/Style.php b/thinkphp/library/think/console/output/formatter/Style.php new file mode 100644 index 0000000..d9b0999 --- /dev/null +++ b/thinkphp/library/think/console/output/formatter/Style.php @@ -0,0 +1,189 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\formatter; + +class Style +{ + + private static $availableForegroundColors = [ + 'black' => ['set' => 30, 'unset' => 39], + 'red' => ['set' => 31, 'unset' => 39], + 'green' => ['set' => 32, 'unset' => 39], + 'yellow' => ['set' => 33, 'unset' => 39], + 'blue' => ['set' => 34, 'unset' => 39], + 'magenta' => ['set' => 35, 'unset' => 39], + 'cyan' => ['set' => 36, 'unset' => 39], + 'white' => ['set' => 37, 'unset' => 39], + ]; + private static $availableBackgroundColors = [ + 'black' => ['set' => 40, 'unset' => 49], + 'red' => ['set' => 41, 'unset' => 49], + 'green' => ['set' => 42, 'unset' => 49], + 'yellow' => ['set' => 43, 'unset' => 49], + 'blue' => ['set' => 44, 'unset' => 49], + 'magenta' => ['set' => 45, 'unset' => 49], + 'cyan' => ['set' => 46, 'unset' => 49], + 'white' => ['set' => 47, 'unset' => 49], + ]; + private static $availableOptions = [ + 'bold' => ['set' => 1, 'unset' => 22], + 'underscore' => ['set' => 4, 'unset' => 24], + 'blink' => ['set' => 5, 'unset' => 25], + 'reverse' => ['set' => 7, 'unset' => 27], + 'conceal' => ['set' => 8, 'unset' => 28], + ]; + + private $foreground; + private $background; + private $options = []; + + /** + * 初始化输出的样式 + * @param string|null $foreground 字体颜色 + * @param string|null $background 背景色 + * @param array $options 格式 + * @api + */ + public function __construct($foreground = null, $background = null, array $options = []) + { + if (null !== $foreground) { + $this->setForeground($foreground); + } + if (null !== $background) { + $this->setBackground($background); + } + if (count($options)) { + $this->setOptions($options); + } + } + + /** + * 设置字体颜色 + * @param string|null $color 颜色名 + * @throws \InvalidArgumentException + * @api + */ + public function setForeground($color = null) + { + if (null === $color) { + $this->foreground = null; + + return; + } + + if (!isset(static::$availableForegroundColors[$color])) { + throw new \InvalidArgumentException(sprintf('Invalid foreground color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableForegroundColors)))); + } + + $this->foreground = static::$availableForegroundColors[$color]; + } + + /** + * 设置背景色 + * @param string|null $color 颜色名 + * @throws \InvalidArgumentException + * @api + */ + public function setBackground($color = null) + { + if (null === $color) { + $this->background = null; + + return; + } + + if (!isset(static::$availableBackgroundColors[$color])) { + throw new \InvalidArgumentException(sprintf('Invalid background color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableBackgroundColors)))); + } + + $this->background = static::$availableBackgroundColors[$color]; + } + + /** + * 设置字体格式 + * @param string $option 格式名 + * @throws \InvalidArgumentException When the option name isn't defined + * @api + */ + public function setOption($option) + { + if (!isset(static::$availableOptions[$option])) { + throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions)))); + } + + if (!in_array(static::$availableOptions[$option], $this->options)) { + $this->options[] = static::$availableOptions[$option]; + } + } + + /** + * 重置字体格式 + * @param string $option 格式名 + * @throws \InvalidArgumentException + */ + public function unsetOption($option) + { + if (!isset(static::$availableOptions[$option])) { + throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions)))); + } + + $pos = array_search(static::$availableOptions[$option], $this->options); + if (false !== $pos) { + unset($this->options[$pos]); + } + } + + /** + * 批量设置字体格式 + * @param array $options + */ + public function setOptions(array $options) + { + $this->options = []; + + foreach ($options as $option) { + $this->setOption($option); + } + } + + /** + * 应用样式到文字 + * @param string $text 文字 + * @return string + */ + public function apply($text) + { + $setCodes = []; + $unsetCodes = []; + + if (null !== $this->foreground) { + $setCodes[] = $this->foreground['set']; + $unsetCodes[] = $this->foreground['unset']; + } + if (null !== $this->background) { + $setCodes[] = $this->background['set']; + $unsetCodes[] = $this->background['unset']; + } + if (count($this->options)) { + foreach ($this->options as $option) { + $setCodes[] = $option['set']; + $unsetCodes[] = $option['unset']; + } + } + + if (0 === count($setCodes)) { + return $text; + } + + return sprintf("\033[%sm%s\033[%sm", implode(';', $setCodes), $text, implode(';', $unsetCodes)); + } +} diff --git a/thinkphp/library/think/console/output/question/Choice.php b/thinkphp/library/think/console/output/question/Choice.php new file mode 100644 index 0000000..cdc3b4e --- /dev/null +++ b/thinkphp/library/think/console/output/question/Choice.php @@ -0,0 +1,163 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\question; + +use think\console\output\Question; + +class Choice extends Question +{ + + private $choices; + private $multiselect = false; + private $prompt = ' > '; + private $errorMessage = 'Value "%s" is invalid'; + + /** + * 构造方法 + * @param string $question 问题 + * @param array $choices 选项 + * @param mixed $default 默认答案 + */ + public function __construct($question, array $choices, $default = null) + { + parent::__construct($question, $default); + + $this->choices = $choices; + $this->setValidator($this->getDefaultValidator()); + $this->setAutocompleterValues($choices); + } + + /** + * 可选项 + * @return array + */ + public function getChoices() + { + return $this->choices; + } + + /** + * 设置可否多选 + * @param bool $multiselect + * @return self + */ + public function setMultiselect($multiselect) + { + $this->multiselect = $multiselect; + $this->setValidator($this->getDefaultValidator()); + + return $this; + } + + public function isMultiselect() + { + return $this->multiselect; + } + + /** + * 获取提示 + * @return string + */ + public function getPrompt() + { + return $this->prompt; + } + + /** + * 设置提示 + * @param string $prompt + * @return self + */ + public function setPrompt($prompt) + { + $this->prompt = $prompt; + + return $this; + } + + /** + * 设置错误提示信息 + * @param string $errorMessage + * @return self + */ + public function setErrorMessage($errorMessage) + { + $this->errorMessage = $errorMessage; + $this->setValidator($this->getDefaultValidator()); + + return $this; + } + + /** + * 获取默认的验证方法 + * @return callable + */ + private function getDefaultValidator() + { + $choices = $this->choices; + $errorMessage = $this->errorMessage; + $multiselect = $this->multiselect; + $isAssoc = $this->isAssoc($choices); + + return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) { + // Collapse all spaces. + $selectedChoices = str_replace(' ', '', $selected); + + if ($multiselect) { + // Check for a separated comma values + if (!preg_match('/^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$/', $selectedChoices, $matches)) { + throw new \InvalidArgumentException(sprintf($errorMessage, $selected)); + } + $selectedChoices = explode(',', $selectedChoices); + } else { + $selectedChoices = [$selected]; + } + + $multiselectChoices = []; + foreach ($selectedChoices as $value) { + $results = []; + foreach ($choices as $key => $choice) { + if ($choice === $value) { + $results[] = $key; + } + } + + if (count($results) > 1) { + throw new \InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of %s.', implode(' or ', $results))); + } + + $result = array_search($value, $choices); + + if (!$isAssoc) { + if (!empty($result)) { + $result = $choices[$result]; + } elseif (isset($choices[$value])) { + $result = $choices[$value]; + } + } elseif (empty($result) && array_key_exists($value, $choices)) { + $result = $value; + } + + if (false === $result) { + throw new \InvalidArgumentException(sprintf($errorMessage, $value)); + } + array_push($multiselectChoices, $result); + } + + if ($multiselect) { + return $multiselectChoices; + } + + return current($multiselectChoices); + }; + } +} diff --git a/thinkphp/library/think/console/output/question/Confirmation.php b/thinkphp/library/think/console/output/question/Confirmation.php new file mode 100644 index 0000000..6598f9b --- /dev/null +++ b/thinkphp/library/think/console/output/question/Confirmation.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\question; + +use think\console\output\Question; + +class Confirmation extends Question +{ + + private $trueAnswerRegex; + + /** + * 构造方法 + * @param string $question 问题 + * @param bool $default 默认答案 + * @param string $trueAnswerRegex 验证正则 + */ + public function __construct($question, $default = true, $trueAnswerRegex = '/^y/i') + { + parent::__construct($question, (bool) $default); + + $this->trueAnswerRegex = $trueAnswerRegex; + $this->setNormalizer($this->getDefaultNormalizer()); + } + + /** + * 获取默认的答案回调 + * @return callable + */ + private function getDefaultNormalizer() + { + $default = $this->getDefault(); + $regex = $this->trueAnswerRegex; + + return function ($answer) use ($default, $regex) { + if (is_bool($answer)) { + return $answer; + } + + $answerIsTrue = (bool) preg_match($regex, $answer); + if (false === $default) { + return $answer && $answerIsTrue; + } + + return !$answer || $answerIsTrue; + }; + } +} diff --git a/thinkphp/library/think/db/Builder.php b/thinkphp/library/think/db/Builder.php new file mode 100644 index 0000000..60b470e --- /dev/null +++ b/thinkphp/library/think/db/Builder.php @@ -0,0 +1,1173 @@ + +// +---------------------------------------------------------------------- + +namespace think\db; + +use PDO; +use think\Exception; + +abstract class Builder +{ + // connection对象实例 + protected $connection; + + // 查询表达式映射 + protected $exp = ['EQ' => '=', 'NEQ' => '<>', 'GT' => '>', 'EGT' => '>=', 'LT' => '<', 'ELT' => '<=', 'NOTLIKE' => 'NOT LIKE', 'NOTIN' => 'NOT IN', 'NOTBETWEEN' => 'NOT BETWEEN', 'NOTEXISTS' => 'NOT EXISTS', 'NOTNULL' => 'NOT NULL', 'NOTBETWEEN TIME' => 'NOT BETWEEN TIME']; + + // 查询表达式解析 + protected $parser = [ + 'parseCompare' => ['=', '<>', '>', '>=', '<', '<='], + 'parseLike' => ['LIKE', 'NOT LIKE'], + 'parseBetween' => ['NOT BETWEEN', 'BETWEEN'], + 'parseIn' => ['NOT IN', 'IN'], + 'parseExp' => ['EXP'], + 'parseNull' => ['NOT NULL', 'NULL'], + 'parseBetweenTime' => ['BETWEEN TIME', 'NOT BETWEEN TIME'], + 'parseTime' => ['< TIME', '> TIME', '<= TIME', '>= TIME'], + 'parseExists' => ['NOT EXISTS', 'EXISTS'], + 'parseColumn' => ['COLUMN'], + ]; + + // SQL表达式 + 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%'; + + protected $deleteSql = 'DELETE FROM %TABLE%%USING%%JOIN%%WHERE%%ORDER%%LIMIT% %LOCK%%COMMENT%'; + + /** + * 架构函数 + * @access public + * @param Connection $connection 数据库连接对象实例 + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + /** + * 获取当前的连接对象实例 + * @access public + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * 注册查询表达式解析 + * @access public + * @param string $name 解析方法 + * @param array $parser 匹配表达式数据 + * @return $this + */ + public function bindParser($name, $parser) + { + $this->parser[$name] = $parser; + return $this; + } + + /** + * 数据分析 + * @access protected + * @param Query $query 查询对象 + * @param array $data 数据 + * @param array $fields 字段信息 + * @param array $bind 参数绑定 + * @return array + */ + protected function parseData(Query $query, $data = [], $fields = [], $bind = []) + { + if (empty($data)) { + return []; + } + + $options = $query->getOptions(); + + // 获取绑定信息 + if (empty($bind)) { + $bind = $this->connection->getFieldsBind($options['table']); + } + + if (empty($fields)) { + if ('*' == $options['field']) { + $fields = array_keys($bind); + } else { + $fields = $options['field']; + } + } + + $result = []; + + foreach ($data as $key => $val) { + if ('*' != $options['field'] && !in_array($key, $fields, true)) { + continue; + } + + $item = $this->parseKey($query, $key, true); + + if ($val instanceof Expression) { + $result[$item] = $val->getValue(); + continue; + } elseif (!is_scalar($val) && (in_array($key, (array) $query->getOptions('json')) || 'json' == $this->connection->getFieldsType($options['table'], $key))) { + $val = json_encode($val, JSON_UNESCAPED_UNICODE); + } elseif (is_object($val) && method_exists($val, '__toString')) { + // 对象数据写入 + $val = $val->__toString(); + } + + if (false !== strpos($key, '->')) { + list($key, $name) = explode('->', $key); + $item = $this->parseKey($query, $key); + $result[$item] = 'json_set(' . $item . ', \'$.' . $name . '\', ' . $this->parseDataBind($query, $key, $val, $bind) . ')'; + } elseif ('*' == $options['field'] && false === strpos($key, '.') && !in_array($key, $fields, true)) { + if ($options['strict']) { + throw new Exception('fields not exists:[' . $key . ']'); + } + } elseif (is_null($val)) { + $result[$item] = 'NULL'; + } elseif (is_array($val) && !empty($val)) { + switch (strtoupper($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)) { + // 过滤非标量数据 + $result[$item] = $this->parseDataBind($query, $key, $val, $bind); + } + } + + return $result; + } + + /** + * 数据绑定处理 + * @access protected + * @param Query $query 查询对象 + * @param string $key 字段名 + * @param mixed $data 数据 + * @param array $bind 绑定数据 + * @return string + */ + protected function parseDataBind(Query $query, $key, $data, $bind = []) + { + if ($data instanceof Expression) { + return $data->getValue(); + } + + $name = $query->bind($data, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR); + + return ':' . $name; + } + + /** + * 字段名分析 + * @access public + * @param Query $query 查询对象 + * @param mixed $key 字段名 + * @param bool $strict 严格检测 + * @return string + */ + public function parseKey(Query $query, $key, $strict = false) + { + return $key instanceof Expression ? $key->getValue() : $key; + } + + /** + * field分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $fields 字段名 + * @return string + */ + protected function parseField(Query $query, $fields) + { + if ('*' == $fields || empty($fields)) { + $fieldsStr = '*'; + } elseif (is_array($fields)) { + // 支持 'field1'=>'field2' 这样的字段别名定义 + $array = []; + + foreach ($fields as $key => $field) { + if (!is_numeric($key)) { + $array[] = $this->parseKey($query, $key) . ' AS ' . $this->parseKey($query, $field, true); + } else { + $array[] = $this->parseKey($query, $field); + } + } + + $fieldsStr = implode(',', $array); + } + + return $fieldsStr; + } + + /** + * table分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $tables 表名 + * @return string + */ + protected function parseTable(Query $query, $tables) + { + $item = []; + $options = $query->getOptions(); + + foreach ((array) $tables as $key => $table) { + if (!is_numeric($key)) { + $key = $this->connection->parseSqlTable($key); + $item[] = $this->parseKey($query, $key) . ' ' . $this->parseKey($query, $table); + } else { + $table = $this->connection->parseSqlTable($table); + + if (isset($options['alias'][$table])) { + $item[] = $this->parseKey($query, $table) . ' ' . $this->parseKey($query, $options['alias'][$table]); + } else { + $item[] = $this->parseKey($query, $table); + } + } + } + + return implode(',', $item); + } + + /** + * where分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $where 查询条件 + * @return string + */ + protected function parseWhere(Query $query, $where) + { + $options = $query->getOptions(); + $whereStr = $this->buildWhere($query, $where); + + if (!empty($options['soft_delete'])) { + // 附加软删除条件 + list($field, $condition) = $options['soft_delete']; + + $binds = $this->connection->getFieldsBind($options['table']); + $whereStr = $whereStr ? '( ' . $whereStr . ' ) AND ' : ''; + $whereStr = $whereStr . $this->parseWhereItem($query, $field, $condition, '', $binds); + } + + return empty($whereStr) ? '' : ' WHERE ' . $whereStr; + } + + /** + * 生成查询条件SQL + * @access public + * @param Query $query 查询对象 + * @param mixed $where 查询条件 + * @return string + */ + public function buildWhere(Query $query, $where) + { + if (empty($where)) { + $where = []; + } + + $whereStr = ''; + $binds = $this->connection->getFieldsBind($query->getOptions('table')); + + foreach ($where as $logic => $val) { + $str = []; + + foreach ($val as $value) { + if ($value instanceof Expression) { + $str[] = ' ' . $logic . ' ( ' . $value->getValue() . ' )'; + continue; + } + + if (is_array($value)) { + if (key($value) !== 0) { + throw new Exception('where express error:' . var_export($value, true)); + } + $field = array_shift($value); + } elseif (!($value instanceof \Closure)) { + throw new Exception('where express error:' . var_export($value, true)); + } + + if ($value instanceof \Closure) { + // 使用闭包查询 + $newQuery = $query->newQuery()->setConnection($this->connection); + $value($newQuery); + $whereClause = $this->buildWhere($newQuery, $newQuery->getOptions('where')); + + if (!empty($whereClause)) { + $query->bind($newQuery->getBind(false)); + $str[] = ' ' . $logic . ' ( ' . $whereClause . ' )'; + } + } elseif (is_array($field)) { + array_unshift($value, $field); + $str2 = []; + foreach ($value as $item) { + $str2[] = $this->parseWhereItem($query, array_shift($item), $item, $logic, $binds); + } + + $str[] = ' ' . $logic . ' ( ' . implode(' AND ', $str2) . ' )'; + } elseif (strpos($field, '|')) { + // 不同字段使用相同查询条件(OR) + $array = explode('|', $field); + $item = []; + + foreach ($array as $k) { + $item[] = $this->parseWhereItem($query, $k, $value, '', $binds); + } + + $str[] = ' ' . $logic . ' ( ' . implode(' OR ', $item) . ' )'; + } elseif (strpos($field, '&')) { + // 不同字段使用相同查询条件(AND) + $array = explode('&', $field); + $item = []; + + foreach ($array as $k) { + $item[] = $this->parseWhereItem($query, $k, $value, '', $binds); + } + + $str[] = ' ' . $logic . ' ( ' . implode(' AND ', $item) . ' )'; + } else { + // 对字段使用表达式查询 + $field = is_string($field) ? $field : ''; + $str[] = ' ' . $logic . ' ' . $this->parseWhereItem($query, $field, $value, $logic, $binds); + } + } + + $whereStr .= empty($whereStr) ? substr(implode(' ', $str), strlen($logic) + 1) : implode(' ', $str); + } + + return $whereStr; + } + + // where子单元分析 + protected function parseWhereItem(Query $query, $field, $val, $rule = '', $binds = []) + { + // 字段分析 + $key = $field ? $this->parseKey($query, $field, true) : ''; + + // 查询规则和条件 + if (!is_array($val)) { + $val = is_null($val) ? ['NULL', ''] : ['=', $val]; + } + + list($exp, $value) = $val; + + // 对一个字段使用多个查询条件 + if (is_array($exp)) { + $item = array_pop($val); + + // 传入 or 或者 and + if (is_string($item) && in_array($item, ['AND', 'and', 'OR', 'or'])) { + $rule = $item; + } else { + array_push($val, $item); + } + + foreach ($val as $k => $item) { + $str[] = $this->parseWhereItem($query, $field, $item, $rule, $binds); + } + + return '( ' . implode(' ' . $rule . ' ', $str) . ' )'; + } + + // 检测操作符 + $exp = strtoupper($exp); + if (isset($this->exp[$exp])) { + $exp = $this->exp[$exp]; + } + + if ($value instanceof Expression) { + + } elseif (is_object($value) && method_exists($value, '__toString')) { + // 对象数据写入 + $value = $value->__toString(); + } + + if (strpos($field, '->')) { + $jsonType = $query->getJsonFieldType($field); + $bindType = $this->connection->getFieldBindType($jsonType); + } else { + $bindType = isset($binds[$field]) && 'LIKE' != $exp ? $binds[$field] : PDO::PARAM_STR; + } + + if (is_scalar($value) && !in_array($exp, ['EXP', 'NOT NULL', 'NULL', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN']) && strpos($exp, 'TIME') === false) { + if (0 === strpos($value, ':') && $query->isBind(substr($value, 1))) { + } else { + $name = $query->bind($value, $bindType); + $value = ':' . $name; + } + } + + // 解析查询表达式 + foreach ($this->parser as $fun => $parse) { + if (in_array($exp, $parse)) { + $whereStr = $this->$fun($query, $key, $exp, $value, $field, $bindType, isset($val[2]) ? $val[2] : 'AND'); + break; + } + } + + if (!isset($whereStr)) { + throw new Exception('where express error:' . $exp); + } + + return $whereStr; + } + + /** + * 模糊查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @param integer $bindType + * @param string $logic + * @return string + */ + protected function parseLike(Query $query, $key, $exp, $value, $field, $bindType, $logic) + { + // 模糊匹配 + if (is_array($value)) { + foreach ($value as $item) { + $name = $query->bind($item, PDO::PARAM_STR); + $array[] = $key . ' ' . $exp . ' :' . $name; + } + + $whereStr = '(' . implode(' ' . strtoupper($logic) . ' ', $array) . ')'; + } else { + $whereStr = $key . ' ' . $exp . ' ' . $value; + } + + return $whereStr; + } + + /** + * 表达式查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param array $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseColumn(Query $query, $key, $exp, array $value, $field, $bindType) + { + // 字段比较查询 + list($op, $field2) = $value; + + if (!in_array($op, ['=', '<>', '>', '>=', '<', '<='])) { + throw new Exception('where express error:' . var_export($value, true)); + } + + return '( ' . $key . ' ' . $op . ' ' . $this->parseKey($query, $field2, true) . ' )'; + } + + /** + * 表达式查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param Expression $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseExp(Query $query, $key, $exp, Expression $value, $field, $bindType) + { + // 表达式查询 + return '( ' . $key . ' ' . $value->getValue() . ' )'; + } + + /** + * Null查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseNull(Query $query, $key, $exp, $value, $field, $bindType) + { + // NULL 查询 + return $key . ' IS ' . $exp; + } + + /** + * 范围查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseBetween(Query $query, $key, $exp, $value, $field, $bindType) + { + // BETWEEN 查询 + $data = is_array($value) ? $value : explode(',', $value); + + $min = $query->bind($data[0], $bindType); + $max = $query->bind($data[1], $bindType); + + return $key . ' ' . $exp . ' :' . $min . ' AND :' . $max . ' '; + } + + /** + * Exists查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseExists(Query $query, $key, $exp, $value, $field, $bindType) + { + // EXISTS 查询 + if ($value instanceof \Closure) { + $value = $this->parseClosure($query, $value, false); + } elseif ($value instanceof Expression) { + $value = $value->getValue(); + } else { + throw new Exception('where express error:' . $value); + } + + return $exp . ' (' . $value . ')'; + } + + /** + * 时间比较查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseTime(Query $query, $key, $exp, $value, $field, $bindType) + { + return $key . ' ' . substr($exp, 0, 2) . ' ' . $this->parseDateTime($query, $value, $field, $bindType); + } + + /** + * 大小比较查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseCompare(Query $query, $key, $exp, $value, $field, $bindType) + { + if (is_array($value)) { + throw new Exception('where express error:' . $exp . var_export($value, true)); + } + + // 比较运算 + if ($value instanceof \Closure) { + $value = $this->parseClosure($query, $value); + } + + if ('=' == $exp && is_null($value)) { + return $key . ' IS NULL'; + } + + return $key . ' ' . $exp . ' ' . $value; + } + + /** + * 时间范围查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseBetweenTime(Query $query, $key, $exp, $value, $field, $bindType) + { + if (is_string($value)) { + $value = explode(',', $value); + } + + return $key . ' ' . substr($exp, 0, -4) + . $this->parseDateTime($query, $value[0], $field, $bindType) + . ' AND ' + . $this->parseDateTime($query, $value[1], $field, $bindType); + + } + + /** + * IN查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseIn(Query $query, $key, $exp, $value, $field, $bindType) + { + // IN 查询 + if ($value instanceof \Closure) { + $value = $this->parseClosure($query, $value, false); + } elseif ($value instanceof Expression) { + $value = $value->getValue(); + } else { + $value = array_unique(is_array($value) ? $value : explode(',', $value)); + $array = []; + + foreach ($value as $k => $v) { + $name = $query->bind($v, $bindType); + $array[] = ':' . $name; + } + + if (count($array) == 1) { + return $key . ('IN' == $exp ? ' = ' : ' <> ') . $array[0]; + } else { + $zone = implode(',', $array); + $value = empty($zone) ? "''" : $zone; + } + } + + return $key . ' ' . $exp . ' (' . $value . ')'; + } + + /** + * 闭包子查询 + * @access protected + * @param Query $query 查询对象 + * @param \Closure $call + * @param bool $show + * @return string + */ + protected function parseClosure(Query $query, $call, $show = true) + { + $newQuery = $query->newQuery()->removeOption(); + $call($newQuery); + + return $newQuery->buildSql($show); + } + + /** + * 日期时间条件解析 + * @access protected + * @param Query $query 查询对象 + * @param string $value + * @param string $key + * @param integer $bindType + * @return string + */ + protected function parseDateTime(Query $query, $value, $key, $bindType = null) + { + $options = $query->getOptions(); + + // 获取时间字段类型 + if (strpos($key, '.')) { + list($table, $key) = explode('.', $key); + + if (isset($options['alias']) && $pos = array_search($table, $options['alias'])) { + $table = $pos; + } + } else { + $table = $options['table']; + } + + $type = $this->connection->getTableInfo($table, 'type'); + + if (isset($type[$key])) { + $info = $type[$key]; + } + + if (isset($info)) { + if (is_string($value)) { + $value = strtotime($value) ?: $value; + } + + if (preg_match('/(datetime|timestamp)/is', $info)) { + // 日期及时间戳类型 + $value = date('Y-m-d H:i:s', $value); + } elseif (preg_match('/(date)/is', $info)) { + // 日期及时间戳类型 + $value = date('Y-m-d', $value); + } + } + + $name = $query->bind($value, $bindType); + + return ':' . $name; + } + + /** + * limit分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $limit + * @return string + */ + protected function parseLimit(Query $query, $limit) + { + return (!empty($limit) && false === strpos($limit, '(')) ? ' LIMIT ' . $limit . ' ' : ''; + } + + /** + * join分析 + * @access protected + * @param Query $query 查询对象 + * @param array $join + * @return string + */ + protected function parseJoin(Query $query, $join) + { + $joinStr = ''; + + if (!empty($join)) { + foreach ($join as $item) { + list($table, $type, $on) = $item; + + $condition = []; + + foreach ((array) $on as $val) { + if ($val instanceof Expression) { + $condition[] = $val->getValue(); + } elseif (strpos($val, '=')) { + list($val1, $val2) = explode('=', $val, 2); + + $condition[] = $this->parseKey($query, $val1) . '=' . $this->parseKey($query, $val2); + } else { + $condition[] = $val; + } + } + + $table = $this->parseTable($query, $table); + + $joinStr .= ' ' . $type . ' JOIN ' . $table . ' ON ' . implode(' AND ', $condition); + } + } + + return $joinStr; + } + + /** + * order分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $order + * @return string + */ + protected function parseOrder(Query $query, $order) + { + foreach ($order as $key => $val) { + if ($val instanceof Expression) { + $array[] = $val->getValue(); + } elseif (is_array($val) && preg_match('/^[\w\.]+$/', $key)) { + $array[] = $this->parseOrderField($query, $key, $val); + } elseif ('[rand]' == $val) { + $array[] = $this->parseRand($query); + } elseif (is_string($val)) { + if (is_numeric($key)) { + list($key, $sort) = explode(' ', strpos($val, ' ') ? $val : $val . ' '); + } else { + $sort = $val; + } + + if (preg_match('/^[\w\.]+$/', $key)) { + $sort = strtoupper($sort); + $sort = in_array($sort, ['ASC', 'DESC'], true) ? ' ' . $sort : ''; + $array[] = $this->parseKey($query, $key, true) . $sort; + } else { + throw new Exception('order express error:' . $key); + } + } + } + + return empty($array) ? '' : ' ORDER BY ' . implode(',', $array); + } + + /** + * orderField分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $key + * @param array $val + * @return string + */ + protected function parseOrderField($query, $key, $val) + { + if (isset($val['sort'])) { + $sort = $val['sort']; + unset($val['sort']); + } else { + $sort = ''; + } + + $sort = strtoupper($sort); + $sort = in_array($sort, ['ASC', 'DESC'], true) ? ' ' . $sort : ''; + + $options = $query->getOptions(); + $bind = $this->connection->getFieldsBind($options['table']); + + foreach ($val as $k => $item) { + $val[$k] = $this->parseDataBind($query, $key, $item, $bind); + } + + return 'field(' . $this->parseKey($query, $key, true) . ',' . implode(',', $val) . ')' . $sort; + } + + /** + * group分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $group + * @return string + */ + protected function parseGroup(Query $query, $group) + { + if (empty($group)) { + return ''; + } + + if (is_string($group)) { + $group = explode(',', $group); + } + + foreach ($group as $key) { + $val[] = $this->parseKey($query, $key); + } + + return ' GROUP BY ' . implode(',', $val); + } + + /** + * having分析 + * @access protected + * @param Query $query 查询对象 + * @param string $having + * @return string + */ + protected function parseHaving(Query $query, $having) + { + return !empty($having) ? ' HAVING ' . $having : ''; + } + + /** + * comment分析 + * @access protected + * @param Query $query 查询对象 + * @param string $comment + * @return string + */ + protected function parseComment(Query $query, $comment) + { + if (false !== strpos($comment, '*/')) { + $comment = strstr($comment, '*/', true); + } + + return !empty($comment) ? ' /* ' . $comment . ' */' : ''; + } + + /** + * distinct分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $distinct + * @return string + */ + protected function parseDistinct(Query $query, $distinct) + { + return !empty($distinct) ? ' DISTINCT ' : ''; + } + + /** + * union分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $union + * @return string + */ + protected function parseUnion(Query $query, $union) + { + if (empty($union)) { + return ''; + } + + $type = $union['type']; + unset($union['type']); + + foreach ($union as $u) { + if ($u instanceof \Closure) { + $sql[] = $type . ' ' . $this->parseClosure($query, $u); + } elseif (is_string($u)) { + $sql[] = $type . ' ( ' . $this->connection->parseSqlTable($u) . ' )'; + } + } + + return ' ' . implode(' ', $sql); + } + + /** + * index分析,可在操作链中指定需要强制使用的索引 + * @access protected + * @param Query $query 查询对象 + * @param mixed $index + * @return string + */ + protected function parseForce(Query $query, $index) + { + if (empty($index)) { + return ''; + } + + return sprintf(" FORCE INDEX ( %s ) ", is_array($index) ? implode(',', $index) : $index); + } + + /** + * 设置锁机制 + * @access protected + * @param Query $query 查询对象 + * @param bool|string $lock + * @return string + */ + protected function parseLock(Query $query, $lock = false) + { + if (is_bool($lock)) { + return $lock ? ' FOR UPDATE ' : ''; + } elseif (is_string($lock) && !empty($lock)) { + return ' ' . trim($lock) . ' '; + } + } + + /** + * 生成查询SQL + * @access public + * @param Query $query 查询对象 + * @return string + */ + public function select(Query $query) + { + $options = $query->getOptions(); + + return str_replace( + ['%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'], + [ + $this->parseTable($query, $options['table']), + $this->parseDistinct($query, $options['distinct']), + $this->parseField($query, $options['field']), + $this->parseJoin($query, $options['join']), + $this->parseWhere($query, $options['where']), + $this->parseGroup($query, $options['group']), + $this->parseHaving($query, $options['having']), + $this->parseOrder($query, $options['order']), + $this->parseLimit($query, $options['limit']), + $this->parseUnion($query, $options['union']), + $this->parseLock($query, $options['lock']), + $this->parseComment($query, $options['comment']), + $this->parseForce($query, $options['force']), + ], + $this->selectSql); + } + + /** + * 生成Insert SQL + * @access public + * @param Query $query 查询对象 + * @param bool $replace 是否replace + * @return string + */ + public function insert(Query $query, $replace = false) + { + $options = $query->getOptions(); + + // 分析并处理数据 + $data = $this->parseData($query, $options['data']); + if (empty($data)) { + return ''; + } + + $fields = array_keys($data); + $values = array_values($data); + + return str_replace( + ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], + [ + $replace ? 'REPLACE' : 'INSERT', + $this->parseTable($query, $options['table']), + implode(' , ', $fields), + implode(' , ', $values), + $this->parseComment($query, $options['comment']), + ], + $this->insertSql); + } + + /** + * 生成insertall SQL + * @access public + * @param Query $query 查询对象 + * @param array $dataSet 数据集 + * @param bool $replace 是否replace + * @return string + */ + public function insertAll(Query $query, $dataSet, $replace = false) + { + $options = $query->getOptions(); + + // 获取合法的字段 + if ('*' == $options['field']) { + $allowFields = $this->connection->getTableFields($options['table']); + } else { + $allowFields = $options['field']; + } + + // 获取绑定信息 + $bind = $this->connection->getFieldsBind($options['table']); + + foreach ($dataSet as $data) { + $data = $this->parseData($query, $data, $allowFields, $bind); + + $values[] = 'SELECT ' . implode(',', array_values($data)); + + if (!isset($insertFields)) { + $insertFields = array_keys($data); + } + } + + $fields = []; + + foreach ($insertFields as $field) { + $fields[] = $this->parseKey($query, $field); + } + + return str_replace( + ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], + [ + $replace ? 'REPLACE' : 'INSERT', + $this->parseTable($query, $options['table']), + implode(' , ', $fields), + implode(' UNION ALL ', $values), + $this->parseComment($query, $options['comment']), + ], + $this->insertAllSql); + } + + /** + * 生成slect insert SQL + * @access public + * @param Query $query 查询对象 + * @param array $fields 数据 + * @param string $table 数据表 + * @return string + */ + public function selectInsert(Query $query, $fields, $table) + { + if (is_string($fields)) { + $fields = explode(',', $fields); + } + + foreach ($fields as &$field) { + $field = $this->parseKey($query, $field, true); + } + + return 'INSERT INTO ' . $this->parseTable($query, $table) . ' (' . implode(',', $fields) . ') ' . $this->select($query); + } + + /** + * 生成update SQL + * @access public + * @param Query $query 查询对象 + * @return string + */ + public function update(Query $query) + { + $options = $query->getOptions(); + + $data = $this->parseData($query, $options['data']); + + if (empty($data)) { + return ''; + } + + foreach ($data as $key => $val) { + $set[] = $key . ' = ' . $val; + } + + return str_replace( + ['%TABLE%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'], + [ + $this->parseTable($query, $options['table']), + implode(' , ', $set), + $this->parseJoin($query, $options['join']), + $this->parseWhere($query, $options['where']), + $this->parseOrder($query, $options['order']), + $this->parseLimit($query, $options['limit']), + $this->parseLock($query, $options['lock']), + $this->parseComment($query, $options['comment']), + ], + $this->updateSql); + } + + /** + * 生成delete SQL + * @access public + * @param Query $query 查询对象 + * @return string + */ + public function delete(Query $query) + { + $options = $query->getOptions(); + + return str_replace( + ['%TABLE%', '%USING%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'], + [ + $this->parseTable($query, $options['table']), + !empty($options['using']) ? ' USING ' . $this->parseTable($query, $options['using']) . ' ' : '', + $this->parseJoin($query, $options['join']), + $this->parseWhere($query, $options['where']), + $this->parseOrder($query, $options['order']), + $this->parseLimit($query, $options['limit']), + $this->parseLock($query, $options['lock']), + $this->parseComment($query, $options['comment']), + ], + $this->deleteSql); + } +} diff --git a/thinkphp/library/think/db/Connection.php b/thinkphp/library/think/db/Connection.php new file mode 100644 index 0000000..18b4885 --- /dev/null +++ b/thinkphp/library/think/db/Connection.php @@ -0,0 +1,2152 @@ + +// +---------------------------------------------------------------------- + +namespace think\db; + +use InvalidArgumentException; +use PDO; +use PDOStatement; +use think\Container; +use think\Db; +use think\db\exception\BindParamException; +use think\Debug; +use think\Exception; +use think\exception\PDOException; +use think\Loader; + +abstract class Connection +{ + const PARAM_FLOAT = 21; + protected static $instance = []; + /** @var PDOStatement PDO操作实例 */ + protected $PDOStatement; + + /** @var string 当前SQL指令 */ + protected $queryStr = ''; + // 返回或者影响记录数 + protected $numRows = 0; + // 事务指令数 + protected $transTimes = 0; + // 错误信息 + protected $error = ''; + + /** @var PDO[] 数据库连接ID 支持多个连接 */ + protected $links = []; + + /** @var PDO 当前连接ID */ + protected $linkID; + protected $linkRead; + protected $linkWrite; + + // 查询结果类型 + protected $fetchType = PDO::FETCH_ASSOC; + // 字段属性大小写 + protected $attrCase = PDO::CASE_LOWER; + // 监听回调 + protected static $event = []; + + // 数据表信息 + protected static $info = []; + + // 使用Builder类 + protected $builderClassName; + // Builder对象 + protected $builder; + // 数据库连接参数配置 + protected $config = [ + // 数据库类型 + 'type' => '', + // 服务器地址 + 'hostname' => '', + // 数据库名 + 'database' => '', + // 用户名 + 'username' => '', + // 密码 + 'password' => '', + // 端口 + 'hostport' => '', + // 连接dsn + 'dsn' => '', + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => '', + // 数据库调试模式 + 'debug' => false, + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 模型写入后自动读取主服务器 + 'read_master' => false, + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 数据集返回类型 + 'resultset_type' => '', + // 自动写入时间戳字段 + 'auto_timestamp' => false, + // 时间字段取出后的默认时间格式 + 'datetime_format' => 'Y-m-d H:i:s', + // 是否需要进行SQL性能分析 + 'sql_explain' => false, + // Builder类 + 'builder' => '', + // Query类 + 'query' => '\\think\\db\\Query', + // 是否需要断线重连 + 'break_reconnect' => false, + // 断线标识字符串 + 'break_match_str' => [], + ]; + + // PDO连接参数 + protected $params = [ + PDO::ATTR_CASE => PDO::CASE_NATURAL, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, + PDO::ATTR_STRINGIFY_FETCHES => false, + PDO::ATTR_EMULATE_PREPARES => false, + ]; + + // 服务器断线标识字符 + protected $breakMatchStr = [ + 'server has gone away', + 'no connection to the server', + 'Lost connection', + 'is dead or not enabled', + 'Error while sending', + 'decryption failed or bad record mac', + 'server closed the connection unexpectedly', + 'SSL connection has been closed unexpectedly', + 'Error writing data to the connection', + 'Resource deadlock avoided', + 'failed with errno', + ]; + + // 绑定参数 + protected $bind = []; + + /** + * 架构函数 读取数据库配置信息 + * @access public + * @param array $config 数据库配置数组 + */ + public function __construct(array $config = []) + { + if (!empty($config)) { + $this->config = array_merge($this->config, $config); + } + + // 创建Builder对象 + $class = $this->getBuilderClass(); + + $this->builder = new $class($this); + + // 执行初始化操作 + $this->initialize(); + } + + /** + * 初始化 + * @access protected + * @return void + */ + protected function initialize() + {} + + /** + * 取得数据库连接类实例 + * @access public + * @param mixed $config 连接配置 + * @param bool|string $name 连接标识 true 强制重新连接 + * @return Connection + * @throws Exception + */ + public static function instance($config = [], $name = false) + { + if (false === $name) { + $name = md5(serialize($config)); + } + + if (true === $name || !isset(self::$instance[$name])) { + if (empty($config['type'])) { + throw new InvalidArgumentException('Undefined db type'); + } + + // 记录初始化信息 + Container::get('app')->log('[ DB ] INIT ' . $config['type']); + + if (true === $name) { + $name = md5(serialize($config)); + } + + self::$instance[$name] = Loader::factory($config['type'], '\\think\\db\\connector\\', $config); + } + + return self::$instance[$name]; + } + + /** + * 获取当前连接器类对应的Builder类 + * @access public + * @return string + */ + public function getBuilderClass() + { + if (!empty($this->builderClassName)) { + return $this->builderClassName; + } + + return $this->getConfig('builder') ?: '\\think\\db\\builder\\' . ucfirst($this->getConfig('type')); + } + + /** + * 设置当前的数据库Builder对象 + * @access protected + * @param Builder $builder + * @return void + */ + protected function setBuilder(Builder $builder) + { + $this->builder = $builder; + + return $this; + } + + /** + * 获取当前的builder实例对象 + * @access public + * @return Builder + */ + public function getBuilder() + { + return $this->builder; + } + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + abstract protected function parseDsn($config); + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + abstract public function getFields($tableName); + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + abstract public function getTables($dbName); + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + abstract protected function getExplain($sql); + + /** + * 对返数据表字段信息进行大小写转换出来 + * @access public + * @param array $info 字段信息 + * @return array + */ + public function fieldCase($info) + { + // 字段大小写转换 + switch ($this->attrCase) { + case PDO::CASE_LOWER: + $info = array_change_key_case($info); + break; + case PDO::CASE_UPPER: + $info = array_change_key_case($info, CASE_UPPER); + break; + case PDO::CASE_NATURAL: + default: + // 不做转换 + } + + return $info; + } + + /** + * 获取字段绑定类型 + * @access public + * @param string $type 字段类型 + * @return integer + */ + public function getFieldBindType($type) + { + if (0 === strpos($type, 'set') || 0 === strpos($type, 'enum')) { + $bind = PDO::PARAM_STR; + } elseif (preg_match('/(double|float|decimal|real|numeric)/is', $type)) { + $bind = self::PARAM_FLOAT; + } elseif (preg_match('/(int|serial|bit)/is', $type)) { + $bind = PDO::PARAM_INT; + } elseif (preg_match('/bool/is', $type)) { + $bind = PDO::PARAM_BOOL; + } else { + $bind = PDO::PARAM_STR; + } + + return $bind; + } + + /** + * 将SQL语句中的__TABLE_NAME__字符串替换成带前缀的表名(小写) + * @access public + * @param string $sql sql语句 + * @return string + */ + public function parseSqlTable($sql) + { + if (false !== strpos($sql, '__')) { + $sql = preg_replace_callback("/__([A-Z0-9_-]+)__/sU", function ($match) { + return $this->getConfig('prefix') . strtolower($match[1]); + }, $sql); + } + + return $sql; + } + + /** + * 获取数据表信息 + * @access public + * @param mixed $tableName 数据表名 留空自动获取 + * @param string $fetch 获取信息类型 包括 fields type bind pk + * @return mixed + */ + public function getTableInfo($tableName, $fetch = '') + { + if (is_array($tableName)) { + $tableName = key($tableName) ?: current($tableName); + } + + if (strpos($tableName, ',')) { + // 多表不获取字段信息 + return false; + } else { + $tableName = $this->parseSqlTable($tableName); + } + + // 修正子查询作为表名的问题 + if (strpos($tableName, ')')) { + return []; + } + + list($tableName) = explode(' ', $tableName); + + if (false === strpos($tableName, '.')) { + $schema = $this->getConfig('database') . '.' . $tableName; + } else { + $schema = $tableName; + } + + if (!isset(self::$info[$schema])) { + // 读取缓存 + $cacheFile = Container::get('app')->getRuntimePath() . 'schema' . DIRECTORY_SEPARATOR . $schema . '.php'; + + if (!$this->config['debug'] && is_file($cacheFile)) { + $info = include $cacheFile; + } else { + $info = $this->getFields($tableName); + } + + $fields = array_keys($info); + $bind = $type = []; + + foreach ($info as $key => $val) { + // 记录字段类型 + $type[$key] = $val['type']; + $bind[$key] = $this->getFieldBindType($val['type']); + + if (!empty($val['primary'])) { + $pk[] = $key; + } + } + + if (isset($pk)) { + // 设置主键 + $pk = count($pk) > 1 ? $pk : $pk[0]; + } else { + $pk = null; + } + + self::$info[$schema] = ['fields' => $fields, 'type' => $type, 'bind' => $bind, 'pk' => $pk]; + } + + return $fetch ? self::$info[$schema][$fetch] : self::$info[$schema]; + } + + /** + * 获取数据表的主键 + * @access public + * @param string $tableName 数据表名 + * @return string|array + */ + public function getPk($tableName) + { + return $this->getTableInfo($tableName, 'pk'); + } + + /** + * 获取数据表字段信息 + * @access public + * @param string $tableName 数据表名 + * @return array + */ + public function getTableFields($tableName) + { + return $this->getTableInfo($tableName, 'fields'); + } + + /** + * 获取数据表字段类型 + * @access public + * @param string $tableName 数据表名 + * @param string $field 字段名 + * @return array|string + */ + public function getFieldsType($tableName, $field = null) + { + $result = $this->getTableInfo($tableName, 'type'); + + if ($field && isset($result[$field])) { + return $result[$field]; + } + + return $result; + } + + /** + * 获取数据表绑定信息 + * @access public + * @param string $tableName 数据表名 + * @return array + */ + public function getFieldsBind($tableName) + { + return $this->getTableInfo($tableName, 'bind'); + } + + /** + * 获取数据库的配置参数 + * @access public + * @param string $config 配置名称 + * @return mixed + */ + public function getConfig($config = '') + { + return $config ? $this->config[$config] : $this->config; + } + + /** + * 设置数据库的配置参数 + * @access public + * @param string|array $config 配置名称 + * @param mixed $value 配置值 + * @return void + */ + public function setConfig($config, $value = '') + { + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } else { + $this->config[$config] = $value; + } + } + + /** + * 连接数据库方法 + * @access public + * @param array $config 连接参数 + * @param integer $linkNum 连接序号 + * @param array|bool $autoConnection 是否自动连接主数据库(用于分布式) + * @return PDO + * @throws Exception + */ + public function connect(array $config = [], $linkNum = 0, $autoConnection = false) + { + if (isset($this->links[$linkNum])) { + return $this->links[$linkNum]; + } + + if (!$config) { + $config = $this->config; + } else { + $config = array_merge($this->config, $config); + } + + // 连接参数 + if (isset($config['params']) && is_array($config['params'])) { + $params = $config['params'] + $this->params; + } else { + $params = $this->params; + } + + // 记录当前字段属性大小写设置 + $this->attrCase = $params[PDO::ATTR_CASE]; + + if (!empty($config['break_match_str'])) { + $this->breakMatchStr = array_merge($this->breakMatchStr, (array) $config['break_match_str']); + } + + try { + if (empty($config['dsn'])) { + $config['dsn'] = $this->parseDsn($config); + } + + if ($config['debug']) { + $startTime = microtime(true); + } + + $this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $params); + + if ($config['debug']) { + // 记录数据库连接信息 + $this->log('[ DB ] CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn']); + } + + return $this->links[$linkNum]; + } catch (\PDOException $e) { + if ($autoConnection) { + $this->log($e->getMessage(), 'error'); + return $this->connect($autoConnection, $linkNum); + } else { + throw $e; + } + } + } + + /** + * 释放查询结果 + * @access public + */ + public function free() + { + $this->PDOStatement = null; + } + + /** + * 获取PDO对象 + * @access public + * @return \PDO|false + */ + public function getPdo() + { + if (!$this->linkID) { + return false; + } + + return $this->linkID; + } + + /** + * 执行查询 使用生成器返回数据 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param bool $master 是否在主服务器读操作 + * @param Model $model 模型对象实例 + * @param array $condition 查询条件 + * @param mixed $relation 关联查询 + * @return \Generator + */ + public function getCursor($sql, $bind = [], $master = false, $model = null, $condition = null, $relation = null) + { + $this->initConnect($master); + + // 记录SQL语句 + $this->queryStr = $sql; + + $this->bind = $bind; + + Db::$queryTimes++; + + // 调试开始 + $this->debug(true); + + // 预处理 + $this->PDOStatement = $this->linkID->prepare($sql); + + // 是否为存储过程调用 + $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']); + + // 参数绑定 + if ($procedure) { + $this->bindParam($bind); + } else { + $this->bindValue($bind); + } + + // 执行查询 + $this->PDOStatement->execute(); + + // 调试结束 + $this->debug(false, '', $master); + + // 返回结果集 + while ($result = $this->PDOStatement->fetch($this->fetchType)) { + if ($model) { + $instance = $model->newInstance($result, $condition); + + if ($relation) { + $instance->relationQuery($relation); + } + + yield $instance; + } else { + yield $result; + } + } + } + + /** + * 执行查询 返回数据集 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param bool $master 是否在主服务器读操作 + * @param bool $pdo 是否返回PDO对象 + * @return array + * @throws BindParamException + * @throws \PDOException + * @throws \Exception + * @throws \Throwable + */ + public function query($sql, $bind = [], $master = false, $pdo = false) + { + $this->initConnect($master); + + if (!$this->linkID) { + return false; + } + + // 记录SQL语句 + $this->queryStr = $sql; + + $this->bind = $bind; + + Db::$queryTimes++; + + try { + // 调试开始 + $this->debug(true); + + // 预处理 + $this->PDOStatement = $this->linkID->prepare($sql); + + // 是否为存储过程调用 + $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']); + + // 参数绑定 + if ($procedure) { + $this->bindParam($bind); + } else { + $this->bindValue($bind); + } + + // 执行查询 + $this->PDOStatement->execute(); + + // 调试结束 + $this->debug(false, '', $master); + + // 返回结果集 + return $this->getResult($pdo, $procedure); + } catch (\PDOException $e) { + if ($this->isBreak($e)) { + return $this->close()->query($sql, $bind, $master, $pdo); + } + + throw new PDOException($e, $this->config, $this->getLastsql()); + } 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); + } + + throw $e; + } + } + + /** + * 执行语句 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param Query $query 查询对象 + * @return int + * @throws BindParamException + * @throws \PDOException + * @throws \Exception + * @throws \Throwable + */ + public function execute($sql, $bind = [], Query $query = null) + { + $this->initConnect(true); + + if (!$this->linkID) { + return false; + } + + // 记录SQL语句 + $this->queryStr = $sql; + + $this->bind = $bind; + + Db::$executeTimes++; + try { + // 调试开始 + $this->debug(true); + + // 预处理 + $this->PDOStatement = $this->linkID->prepare($sql); + + // 是否为存储过程调用 + $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']); + + // 参数绑定 + if ($procedure) { + $this->bindParam($bind); + } else { + $this->bindValue($bind); + } + + // 执行语句 + $this->PDOStatement->execute(); + + // 调试结束 + $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, $query); + } + + throw new PDOException($e, $this->config, $this->getLastsql()); + } catch (\Throwable $e) { + if ($this->isBreak($e)) { + 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; + } + } + + /** + * 查找单条记录 + * @access public + * @param Query $query 查询对象 + * @return array|null|\PDOStatement|string + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function find(Query $query) + { + // 分析查询表达式 + $options = $query->getOptions(); + $pk = $query->getPk($options); + + $data = $options['data']; + $query->setOption('limit', 1); + + if (empty($options['fetch_sql']) && !empty($options['cache'])) { + // 判断查询缓存 + $cache = $options['cache']; + + if (is_string($cache['key'])) { + $key = $cache['key']; + } else { + $key = $this->getCacheKey($query, $data); + } + + $result = Container::get('cache')->get($key); + + if (false !== $result) { + return $result; + } + } + + if (is_string($pk) && !is_array($data)) { + if (isset($key) && strpos($key, '|')) { + list($a, $val) = explode('|', $key); + $item[$pk] = $val; + } else { + $item[$pk] = $data; + } + $data = $item; + } + + $query->setOption('data', $data); + + // 生成查询SQL + $sql = $this->builder->select($query); + + $query->removeOption('limit'); + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + // 事件回调 + $result = $query->trigger('before_find'); + + if (!$result) { + // 执行查询 + $resultSet = $this->query($sql, $bind, $options['master'], $options['fetch_pdo']); + + if ($resultSet instanceof \PDOStatement) { + // 返回PDOStatement对象 + return $resultSet; + } + + $result = isset($resultSet[0]) ? $resultSet[0] : null; + } + + if (isset($cache) && $result) { + // 缓存数据 + $this->cacheData($key, $result, $cache); + } + + return $result; + } + + /** + * 使用游标查询记录 + * @access public + * @param Query $query 查询对象 + * @return \Generator + */ + public function cursor(Query $query) + { + // 分析查询表达式 + $options = $query->getOptions(); + + // 生成查询SQL + $sql = $this->builder->select($query); + + $bind = $query->getBind(); + + $condition = isset($options['where']['AND']) ? $options['where']['AND'] : null; + $relation = isset($options['relaltion']) ? $options['relation'] : null; + + // 执行查询操作 + return $this->getCursor($sql, $bind, $options['master'], $query->getModel(), $condition, $relation); + } + + /** + * 查找记录 + * @access public + * @param Query $query 查询对象 + * @return array|\PDOStatement|string + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function select(Query $query) + { + // 分析查询表达式 + $options = $query->getOptions(); + + if (empty($options['fetch_sql']) && !empty($options['cache'])) { + $resultSet = $this->getCacheData($query, $options['cache'], null, $key); + + if (false !== $resultSet) { + return $resultSet; + } + } + + // 生成查询SQL + $sql = $this->builder->select($query); + + $query->removeOption('limit'); + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + $resultSet = $query->trigger('before_select'); + + if (!$resultSet) { + // 执行查询操作 + $resultSet = $this->query($sql, $bind, $options['master'], $options['fetch_pdo']); + + if ($resultSet instanceof \PDOStatement) { + // 返回PDOStatement对象 + return $resultSet; + } + } + + if (!empty($options['cache']) && false !== $resultSet) { + // 缓存数据集 + $this->cacheData($key, $resultSet, $options['cache']); + } + + return $resultSet; + } + + /** + * 插入记录 + * @access public + * @param Query $query 查询对象 + * @param boolean $replace 是否replace + * @param boolean $getLastInsID 返回自增主键 + * @param string $sequence 自增序列名 + * @return integer|string + */ + public function insert(Query $query, $replace = false, $getLastInsID = false, $sequence = null) + { + // 分析查询表达式 + $options = $query->getOptions(); + + // 生成SQL语句 + $sql = $this->builder->insert($query, $replace); + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + // 执行操作 + $result = '' == $sql ? 0 : $this->execute($sql, $bind, $query); + + if ($result) { + $sequence = $sequence ?: (isset($options['sequence']) ? $options['sequence'] : null); + $lastInsId = $this->getLastInsID($sequence); + + $data = $options['data']; + + if ($lastInsId) { + $pk = $query->getPk($options); + if (is_string($pk)) { + $data[$pk] = $lastInsId; + } + } + + $query->setOption('data', $data); + + $query->trigger('after_insert'); + + if ($getLastInsID) { + return $lastInsId; + } + } + + return $result; + } + + /** + * 批量插入记录 + * @access public + * @param Query $query 查询对象 + * @param mixed $dataSet 数据集 + * @param bool $replace 是否replace + * @param integer $limit 每次写入数据限制 + * @return integer|string + * @throws \Exception + * @throws \Throwable + */ + public function insertAll(Query $query, $dataSet = [], $replace = false, $limit = null) + { + if (!is_array(reset($dataSet))) { + return false; + } + + $options = $query->getOptions(); + + if ($limit) { + // 分批写入 自动启动事务支持 + $this->startTrans(); + + try { + $array = array_chunk($dataSet, $limit, true); + $count = 0; + + foreach ($array as $item) { + $sql = $this->builder->insertAll($query, $item, $replace); + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + $fetchSql[] = $this->getRealSql($sql, $bind); + } else { + $count += $this->execute($sql, $bind, $query); + } + } + + // 提交事务 + $this->commit(); + } catch (\Exception $e) { + $this->rollback(); + throw $e; + } catch (\Throwable $e) { + $this->rollback(); + throw $e; + } + + return isset($fetchSql) ? implode(';', $fetchSql) : $count; + } + + $sql = $this->builder->insertAll($query, $dataSet, $replace); + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + return $this->getRealSql($sql, $bind); + } + + return $this->execute($sql, $bind, $query); + } + + /** + * 通过Select方式插入记录 + * @access public + * @param Query $query 查询对象 + * @param string $fields 要插入的数据表字段名 + * @param string $table 要插入的数据表名 + * @return integer|string + * @throws PDOException + */ + public function selectInsert(Query $query, $fields, $table) + { + // 分析查询表达式 + $options = $query->getOptions(); + + $table = $this->parseSqlTable($table); + + $sql = $this->builder->selectInsert($query, $fields, $table); + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + return $this->getRealSql($sql, $bind); + } + + return $this->execute($sql, $bind, $query); + } + + /** + * 更新记录 + * @access public + * @param Query $query 查询对象 + * @return integer|string + * @throws Exception + * @throws PDOException + */ + public function update(Query $query) + { + $options = $query->getOptions(); + + if (isset($options['cache']) && is_string($options['cache']['key'])) { + $key = $options['cache']['key']; + } + + $pk = $query->getPk($options); + $data = $options['data']; + + if (empty($options['where'])) { + // 如果存在主键数据 则自动作为更新条件 + if (is_string($pk) && isset($data[$pk])) { + $where[$pk] = [$pk, '=', $data[$pk]]; + if (!isset($key)) { + $key = $this->getCacheKey($query, $data[$pk]); + } + unset($data[$pk]); + } elseif (is_array($pk)) { + // 增加复合主键支持 + foreach ($pk as $field) { + if (isset($data[$field])) { + $where[$field] = [$field, '=', $data[$field]]; + } else { + // 如果缺少复合主键数据则不执行 + throw new Exception('miss complex primary data'); + } + unset($data[$field]); + } + } + + if (!isset($where)) { + // 如果没有任何更新条件则不执行 + throw new Exception('miss update condition'); + } else { + $options['where']['AND'] = $where; + $query->setOption('where', ['AND' => $where]); + } + } elseif (!isset($key) && is_string($pk) && isset($options['where']['AND'])) { + foreach ($options['where']['AND'] as $val) { + if (is_array($val) && $val[0] == $pk) { + $key = $this->getCacheKey($query, $val); + } + } + } + + // 更新数据 + $query->setOption('data', $data); + + // 生成UPDATE SQL语句 + $sql = $this->builder->update($query); + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + // 检测缓存 + $cache = Container::get('cache'); + + if (isset($key) && $cache->get($key)) { + // 删除缓存 + $cache->rm($key); + } elseif (!empty($options['cache']['tag'])) { + $cache->clear($options['cache']['tag']); + } + + // 执行操作 + $result = '' == $sql ? 0 : $this->execute($sql, $bind, $query); + + if ($result) { + if (is_string($pk) && isset($where[$pk])) { + $data[$pk] = $where[$pk]; + } elseif (is_string($pk) && isset($key) && strpos($key, '|')) { + list($a, $val) = explode('|', $key); + $data[$pk] = $val; + } + + $query->setOption('data', $data); + $query->trigger('after_update'); + } + + return $result; + } + + /** + * 删除记录 + * @access public + * @param Query $query 查询对象 + * @return int + * @throws Exception + * @throws PDOException + */ + public function delete(Query $query) + { + // 分析查询表达式 + $options = $query->getOptions(); + $pk = $query->getPk($options); + $data = $options['data']; + + if (isset($options['cache']) && is_string($options['cache']['key'])) { + $key = $options['cache']['key']; + } elseif (!is_null($data) && true !== $data && !is_array($data)) { + $key = $this->getCacheKey($query, $data); + } elseif (is_string($pk) && isset($options['where']['AND'])) { + foreach ($options['where']['AND'] as $val) { + if (is_array($val) && $val[0] == $pk) { + $key = $this->getCacheKey($query, $val); + } + } + } + + if (true !== $data && empty($options['where'])) { + // 如果条件为空 不进行删除操作 除非设置 1=1 + throw new Exception('delete without condition'); + } + + // 生成删除SQL语句 + $sql = $this->builder->delete($query); + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + // 检测缓存 + $cache = Container::get('cache'); + + if (isset($key) && $cache->get($key)) { + // 删除缓存 + $cache->rm($key); + } elseif (!empty($options['cache']['tag'])) { + $cache->clear($options['cache']['tag']); + } + + // 执行操作 + $result = $this->execute($sql, $bind, $query); + + if ($result) { + if (!is_array($data) && is_string($pk) && isset($key) && strpos($key, '|')) { + list($a, $val) = explode('|', $key); + $item[$pk] = $val; + $data = $item; + } + + $options['data'] = $data; + + $query->trigger('after_delete'); + } + + return $result; + } + + /** + * 得到某个字段的值 + * @access public + * @param Query $query 查询对象 + * @param string $field 字段名 + * @param mixed $default 默认值 + * @param bool $one 是否返回一个值 + * @return mixed + */ + public function value(Query $query, $field, $default = null, $one = true) + { + $options = $query->getOptions(); + + if (isset($options['field'])) { + $query->removeOption('field'); + } + + if (is_string($field)) { + $field = array_map('trim', explode(',', $field)); + } + + $query->setOption('field', $field); + + if (empty($options['fetch_sql']) && !empty($options['cache'])) { + $cache = $options['cache']; + $result = $this->getCacheData($query, $cache, null, $key); + + if (false !== $result) { + return $result; + } + } + + if ($one) { + $query->setOption('limit', 1); + } + + // 生成查询SQL + $sql = $this->builder->select($query); + + if (isset($options['field'])) { + $query->setOption('field', $options['field']); + } else { + $query->removeOption('field'); + } + + $query->removeOption('limit'); + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + // 执行查询操作 + $pdo = $this->query($sql, $bind, $options['master'], true); + + $result = $pdo->fetchColumn(); + + if (isset($cache) && false !== $result) { + // 缓存数据 + $this->cacheData($key, $result, $cache); + } + + return false !== $result ? $result : $default; + } + + /** + * 得到某个字段的值 + * @access public + * @param Query $query 查询对象 + * @param string $aggregate 聚合方法 + * @param mixed $field 字段名 + * @return mixed + */ + public function aggregate(Query $query, $aggregate, $field) + { + if (is_string($field) && 0 === stripos($field, 'DISTINCT ')) { + list($distinct, $field) = explode(' ', $field); + } + + $field = $aggregate . '(' . (!empty($distinct) ? 'DISTINCT ' : '') . $this->builder->parseKey($query, $field, true) . ') AS tp_' . strtolower($aggregate); + + return $this->value($query, $field, 0, false); + } + + /** + * 得到某个列的数组 + * @access public + * @param Query $query 查询对象 + * @param string $field 字段名 多个字段用逗号分隔 + * @param string $key 索引 + * @return array + */ + public function column(Query $query, $field, $key = '') + { + $options = $query->getOptions(); + + if (isset($options['field'])) { + $query->removeOption('field'); + } + + if (is_null($field)) { + $field = ['*']; + } elseif (is_string($field)) { + $field = array_map('trim', explode(',', $field)); + } + + if ($key && ['*'] != $field) { + array_unshift($field, $key); + $field = array_unique($field); + } + + $query->setOption('field', $field); + + if (empty($options['fetch_sql']) && !empty($options['cache'])) { + // 判断查询缓存 + $cache = $options['cache']; + $result = $this->getCacheData($query, $cache, null, $guid); + + if (false !== $result) { + return $result; + } + } + + // 生成查询SQL + $sql = $this->builder->select($query); + + // 还原field参数 + if (isset($options['field'])) { + $query->setOption('field', $options['field']); + } else { + $query->removeOption('field'); + } + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + // 执行查询操作 + $pdo = $this->query($sql, $bind, $options['master'], true); + + if (1 == $pdo->columnCount()) { + $result = $pdo->fetchAll(PDO::FETCH_COLUMN); + } else { + $resultSet = $pdo->fetchAll(PDO::FETCH_ASSOC); + + if (['*'] == $field && $key) { + $result = array_column($resultSet, null, $key); + } elseif ($resultSet) { + $fields = array_keys($resultSet[0]); + $count = count($fields); + $key1 = array_shift($fields); + $key2 = $fields ? array_shift($fields) : ''; + $key = $key ?: $key1; + + if (strpos($key, '.')) { + list($alias, $key) = explode('.', $key); + } + + if (2 == $count) { + $column = $key2; + } elseif (1 == $count) { + $column = $key1; + } else { + $column = null; + } + + $result = array_column($resultSet, $column, $key); + } else { + $result = []; + } + } + + if (isset($cache) && isset($guid)) { + // 缓存数据 + $this->cacheData($guid, $result, $cache); + } + + return $result; + } + + /** + * 执行查询但只返回PDOStatement对象 + * @access public + * @return \PDOStatement|string + */ + public function pdo(Query $query) + { + // 分析查询表达式 + $options = $query->getOptions(); + + // 生成查询SQL + $sql = $this->builder->select($query); + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + // 执行查询操作 + return $this->query($sql, $bind, $options['master'], true); + } + + /** + * 根据参数绑定组装最终的SQL语句 便于调试 + * @access public + * @param string $sql 带参数绑定的sql语句 + * @param array $bind 参数绑定列表 + * @return string + */ + 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; + + if ((self::PARAM_FLOAT == $type || PDO::PARAM_STR == $type) && is_string($value)) { + $value = '\'' . addslashes($value) . '\''; + } elseif (PDO::PARAM_INT == $type && '' === $value) { + $value = 0; + } + + // 判断占位符 + $sql = is_numeric($key) ? + substr_replace($sql, $value, strpos($sql, '?'), 1) : + substr_replace($sql, $value, strpos($sql, ':' . $key), strlen(':' . $key)); + } + + return rtrim($sql); + } + + /** + * 参数绑定 + * 支持 ['name'=>'value','id'=>123] 对应命名占位符 + * 或者 ['value',123] 对应问号占位符 + * @access public + * @param array $bind 要绑定的参数列表 + * @return void + * @throws BindParamException + */ + protected function bindValue(array $bind = []) + { + foreach ($bind as $key => $val) { + // 占位符 + $param = is_int($key) ? $key + 1 : ':' . $key; + + if (is_array($val)) { + if (PDO::PARAM_INT == $val[1] && '' === $val[0]) { + $val[0] = 0; + } elseif (self::PARAM_FLOAT == $val[1]) { + $val[0] = is_string($val[0]) ? (float) $val[0] : $val[0]; + $val[1] = PDO::PARAM_STR; + } + + $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]); + } else { + $result = $this->PDOStatement->bindValue($param, $val); + } + + if (!$result) { + throw new BindParamException( + "Error occurred when binding parameters '{$param}'", + $this->config, + $this->getLastsql(), + $bind + ); + } + } + } + + /** + * 存储过程的输入输出参数绑定 + * @access public + * @param array $bind 要绑定的参数列表 + * @return void + * @throws BindParamException + */ + protected function bindParam($bind) + { + foreach ($bind as $key => $val) { + $param = is_int($key) ? $key + 1 : ':' . $key; + + if (is_array($val)) { + array_unshift($val, $param); + $result = call_user_func_array([$this->PDOStatement, 'bindParam'], $val); + } else { + $result = $this->PDOStatement->bindValue($param, $val); + } + + if (!$result) { + $param = array_shift($val); + + throw new BindParamException( + "Error occurred when binding parameters '{$param}'", + $this->config, + $this->getLastsql(), + $bind + ); + } + } + } + + /** + * 获得数据集数组 + * @access protected + * @param bool $pdo 是否返回PDOStatement + * @param bool $procedure 是否存储过程 + * @return array + */ + protected function getResult($pdo = false, $procedure = false) + { + if ($pdo) { + // 返回PDOStatement对象处理 + return $this->PDOStatement; + } + + if ($procedure) { + // 存储过程返回结果 + return $this->procedure(); + } + + $result = $this->PDOStatement->fetchAll($this->fetchType); + + $this->numRows = count($result); + + return $result; + } + + /** + * 获得存储过程数据集 + * @access protected + * @return array + */ + protected function procedure() + { + $item = []; + + do { + $result = $this->getResult(); + if ($result) { + $item[] = $result; + } + } while ($this->PDOStatement->nextRowset()); + + $this->numRows = count($item); + + return $item; + } + + /** + * 执行数据库事务 + * @access public + * @param callable $callback 数据操作方法回调 + * @return mixed + * @throws PDOException + * @throws \Exception + * @throws \Throwable + */ + public function transaction($callback) + { + $this->startTrans(); + + try { + $result = null; + if (is_callable($callback)) { + $result = call_user_func_array($callback, [$this]); + } + + $this->commit(); + return $result; + } catch (\Exception $e) { + $this->rollback(); + throw $e; + } catch (\Throwable $e) { + $this->rollback(); + throw $e; + } + } + + /** + * 启动XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function startTransXa($xid) + {} + + /** + * 预编译XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function prepareXa($xid) + {} + + /** + * 提交XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function commitXa($xid) + {} + + /** + * 回滚XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function rollbackXa($xid) + {} + + /** + * 启动事务 + * @access public + * @return void + * @throws \PDOException + * @throws \Exception + */ + public function startTrans() + { + $this->initConnect(true); + if (!$this->linkID) { + return false; + } + + ++$this->transTimes; + + try { + if (1 == $this->transTimes) { + $this->linkID->beginTransaction(); + } elseif ($this->transTimes > 1 && $this->supportSavepoint()) { + $this->linkID->exec( + $this->parseSavepoint('trans' . $this->transTimes) + ); + } + } catch (\Exception $e) { + if ($this->isBreak($e)) { + --$this->transTimes; + return $this->close()->startTrans(); + } + throw $e; + } + } + + /** + * 用于非自动提交状态下面的查询提交 + * @access public + * @return void + * @throws PDOException + */ + public function commit() + { + $this->initConnect(true); + + if (1 == $this->transTimes) { + $this->linkID->commit(); + } + + --$this->transTimes; + } + + /** + * 事务回滚 + * @access public + * @return void + * @throws PDOException + */ + public function rollback() + { + $this->initConnect(true); + + if (1 == $this->transTimes) { + $this->linkID->rollBack(); + } elseif ($this->transTimes > 1 && $this->supportSavepoint()) { + $this->linkID->exec( + $this->parseSavepointRollBack('trans' . $this->transTimes) + ); + } + + $this->transTimes = max(0, $this->transTimes - 1); + } + + /** + * 是否支持事务嵌套 + * @return bool + */ + protected function supportSavepoint() + { + return false; + } + + /** + * 生成定义保存点的SQL + * @access protected + * @param $name + * @return string + */ + protected function parseSavepoint($name) + { + return 'SAVEPOINT ' . $name; + } + + /** + * 生成回滚到保存点的SQL + * @access protected + * @param $name + * @return string + */ + protected function parseSavepointRollBack($name) + { + return 'ROLLBACK TO SAVEPOINT ' . $name; + } + + /** + * 批处理执行SQL语句 + * 批处理的指令都认为是execute操作 + * @access public + * @param array $sqlArray SQL批处理指令 + * @param array $bind 参数绑定 + * @return boolean + */ + public function batchQuery($sqlArray = [], $bind = []) + { + if (!is_array($sqlArray)) { + return false; + } + + // 自动启动事务支持 + $this->startTrans(); + + try { + foreach ($sqlArray as $sql) { + $this->execute($sql, $bind); + } + // 提交事务 + $this->commit(); + } catch (\Exception $e) { + $this->rollback(); + throw $e; + } + + return true; + } + + /** + * 获得查询次数 + * @access public + * @param boolean $execute 是否包含所有查询 + * @return integer + */ + public function getQueryTimes($execute = false) + { + return $execute ? Db::$queryTimes + Db::$executeTimes : Db::$queryTimes; + } + + /** + * 获得执行次数 + * @access public + * @return integer + */ + public function getExecuteTimes() + { + return Db::$executeTimes; + } + + /** + * 关闭数据库(或者重新连接) + * @access public + * @return $this + */ + public function close() + { + $this->linkID = null; + $this->linkWrite = null; + $this->linkRead = null; + $this->links = []; + + // 释放查询 + $this->free(); + + return $this; + } + + /** + * 是否断线 + * @access protected + * @param \PDOException|\Exception $e 异常对象 + * @return bool + */ + protected function isBreak($e) + { + if (!$this->config['break_reconnect']) { + return false; + } + + $error = $e->getMessage(); + + foreach ($this->breakMatchStr as $msg) { + if (false !== stripos($error, $msg)) { + return true; + } + } + return false; + } + + /** + * 获取最近一次查询的sql语句 + * @access public + * @return string + */ + public function getLastSql() + { + return $this->getRealSql($this->queryStr, $this->bind); + } + + /** + * 获取最近插入的ID + * @access public + * @param string $sequence 自增序列名 + * @return string + */ + public function getLastInsID($sequence = null) + { + return $this->linkID->lastInsertId($sequence); + } + + /** + * 获取返回或者影响的记录数 + * @access public + * @return integer + */ + public function getNumRows() + { + return $this->numRows; + } + + /** + * 获取最近的错误信息 + * @access public + * @return string + */ + public function getError() + { + if ($this->PDOStatement) { + $error = $this->PDOStatement->errorInfo(); + $error = $error[1] . ':' . $error[2]; + } else { + $error = ''; + } + + if ('' != $this->queryStr) { + $error .= "\n [ SQL语句 ] : " . $this->getLastsql(); + } + + return $error; + } + + /** + * 数据库调试 记录当前SQL及分析性能 + * @access protected + * @param boolean $start 调试开始标记 true 开始 false 结束 + * @param string $sql 执行的SQL语句 留空自动获取 + * @param bool $master 主从标记 + * @return void + */ + protected function debug($start, $sql = '', $master = false) + { + if (!empty($this->config['debug'])) { + // 开启数据库调试模式 + $debug = Container::get('debug'); + + if ($start) { + $debug->remark('queryStartTime', 'time'); + } else { + // 记录操作结束时间 + $debug->remark('queryEndTime', 'time'); + $runtime = $debug->getRangeTime('queryStartTime', 'queryEndTime'); + $sql = $sql ?: $this->getLastsql(); + $result = []; + + // SQL性能分析 + if ($this->config['sql_explain'] && 0 === stripos(trim($sql), 'select')) { + $result = $this->getExplain($sql); + } + + // SQL监听 + $this->triggerSql($sql, $runtime, $result, $master); + } + } + } + + /** + * 监听SQL执行 + * @access public + * @param callable $callback 回调方法 + * @return void + */ + public function listen($callback) + { + self::$event[] = $callback; + } + + /** + * 触发SQL事件 + * @access protected + * @param string $sql SQL语句 + * @param float $runtime SQL运行时间 + * @param mixed $explain SQL分析 + * @param bool $master 主从标记 + * @return void + */ + protected function triggerSql($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, $master]); + } + } + } else { + if ($this->config['deploy']) { + // 分布式记录当前操作的主从 + $master = $master ? 'master|' : 'slave|'; + } else { + $master = ''; + } + + // 未注册监听则记录到日志中 + $this->log('[ SQL ] ' . $sql . ' [ ' . $master . 'RunTime:' . $runtime . 's ]'); + + if (!empty($explain)) { + $this->log('[ EXPLAIN : ' . var_export($explain, true) . ' ]'); + } + } + } + + public function log($log, $type = 'sql') + { + $this->config['debug'] && Container::get('log')->record($log, $type); + } + + /** + * 初始化数据库连接 + * @access protected + * @param boolean $master 是否主服务器 + * @return void + */ + protected function initConnect($master = true) + { + if (!empty($this->config['deploy'])) { + // 采用分布式数据库 + if ($master || $this->transTimes) { + if (!$this->linkWrite) { + $this->linkWrite = $this->multiConnect(true); + } + + $this->linkID = $this->linkWrite; + } else { + if (!$this->linkRead) { + $this->linkRead = $this->multiConnect(false); + } + + $this->linkID = $this->linkRead; + } + } elseif (!$this->linkID) { + // 默认单数据库 + $this->linkID = $this->connect(); + } + } + + /** + * 连接分布式服务器 + * @access protected + * @param boolean $master 主服务器 + * @return PDO + */ + protected function multiConnect($master = false) + { + $_config = []; + + // 分布式数据库配置解析 + foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { + $_config[$name] = is_string($this->config[$name]) ? explode(',', $this->config[$name]) : $this->config[$name]; + } + + // 主服务器序号 + $m = floor(mt_rand(0, $this->config['master_num'] - 1)); + + if ($this->config['rw_separate']) { + // 主从式采用读写分离 + if ($master) // 主服务器写入 + { + $r = $m; + } elseif (is_numeric($this->config['slave_no'])) { + // 指定服务器读 + $r = $this->config['slave_no']; + } else { + // 读操作连接从服务器 每次随机连接的数据库 + $r = floor(mt_rand($this->config['master_num'], count($_config['hostname']) - 1)); + } + } else { + // 读写操作不区分服务器 每次随机连接的数据库 + $r = floor(mt_rand(0, count($_config['hostname']) - 1)); + } + $dbMaster = false; + + if ($m != $r) { + $dbMaster = []; + foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { + $dbMaster[$name] = isset($_config[$name][$m]) ? $_config[$name][$m] : $_config[$name][0]; + } + } + + $dbConfig = []; + + foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { + $dbConfig[$name] = isset($_config[$name][$r]) ? $_config[$name][$r] : $_config[$name][0]; + } + + return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster); + } + + /** + * 析构方法 + * @access public + */ + public function __destruct() + { + // 关闭连接 + $this->close(); + } + + /** + * 缓存数据 + * @access protected + * @param string $key 缓存标识 + * @param mixed $data 缓存数据 + * @param array $config 缓存参数 + */ + protected function cacheData($key, $data, $config = []) + { + $cache = Container::get('cache'); + + if (isset($config['tag'])) { + $cache->tag($config['tag'])->set($key, $data, $config['expire']); + } else { + $cache->set($key, $data, $config['expire']); + } + } + + /** + * 获取缓存数据 + * @access protected + * @param Query $query 查询对象 + * @param mixed $cache 缓存设置 + * @param array $options 缓存 + * @return mixed + */ + protected function getCacheData(Query $query, $cache, $data, &$key = null) + { + // 判断查询缓存 + $key = is_string($cache['key']) ? $cache['key'] : $this->getCacheKey($query, $data); + + return Container::get('cache')->get($key); + } + + /** + * 生成缓存标识 + * @access protected + * @param Query $query 查询对象 + * @param mixed $value 缓存数据 + * @return string + */ + protected function getCacheKey(Query $query, $value) + { + if (is_scalar($value)) { + $data = $value; + } elseif (is_array($value) && isset($value[1], $value[2]) && in_array($value[1], ['=', 'eq'], true) && is_scalar($value[2])) { + $data = $value[2]; + } + + $prefix = 'think:' . $this->getConfig('database') . '.'; + + if (isset($data)) { + return $prefix . $query->getTable() . '|' . $data; + } + + try { + return md5($prefix . serialize($query->getOptions()) . serialize($query->getBind(false))); + } catch (\Exception $e) { + throw new Exception('closure not support cache(true)'); + } + } + +} diff --git a/thinkphp/library/think/db/Expression.php b/thinkphp/library/think/db/Expression.php new file mode 100644 index 0000000..f1b92ab --- /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 new file mode 100644 index 0000000..ba08279 --- /dev/null +++ b/thinkphp/library/think/db/Query.php @@ -0,0 +1,3766 @@ + +// +---------------------------------------------------------------------- + +namespace think\db; + +use PDO; +use think\Collection; +use think\Container; +use think\Db; +use think\db\exception\BindParamException; +use think\db\exception\DataNotFoundException; +use think\db\exception\ModelNotFoundException; +use think\Exception; +use think\exception\DbException; +use think\exception\PDOException; +use think\Loader; +use think\Model; +use think\model\Collection as ModelCollection; +use think\model\Relation; +use think\model\relation\OneToOne; +use think\Paginator; + +class Query +{ + /** + * 当前数据库连接对象 + * @var Connection + */ + protected $connection; + + /** + * 当前模型对象 + * @var Model + */ + protected $model; + + /** + * 当前数据表名称(不含前缀) + * @var string + */ + protected $name = ''; + + /** + * 当前数据表主键 + * @var string|array + */ + protected $pk; + + /** + * 当前数据表前缀 + * @var string + */ + protected $prefix = ''; + + /** + * 当前查询参数 + * @var array + */ + protected $options = []; + + /** + * 当前参数绑定 + * @var array + */ + protected $bind = []; + + /** + * 事件回调 + * @var array + */ + private static $event = []; + + /** + * 扩展查询方法 + * @var array + */ + private static $extend = []; + + /** + * 读取主库的表 + * @var array + */ + protected static $readMaster = []; + + /** + * 日期查询表达式 + * @var array + */ + protected $timeRule = [ + 'today' => ['today', 'tomorrow -1second'], + 'yesterday' => ['yesterday', 'today -1second'], + 'week' => ['this week 00:00:00', 'next week 00:00:00 -1second'], + 'last week' => ['last week 00:00:00', 'this week 00:00:00 -1second'], + 'month' => ['first Day of this month 00:00:00', 'first Day of next month 00:00:00 -1second'], + 'last month' => ['first Day of last month 00:00:00', 'first Day of this month 00:00:00 -1second'], + 'year' => ['this year 1/1', 'next year 1/1 -1second'], + 'last year' => ['last year 1/1', 'this year 1/1 -1second'], + ]; + + /** + * 日期查询快捷定义 + * @var array + */ + protected $timeExp = ['d' => 'today', 'w' => 'week', 'm' => 'month', 'y' => 'year']; + + /** + * 架构函数 + * @access public + */ + public function __construct(Connection $connection = null) + { + if (is_null($connection)) { + $this->connection = Db::connect(); + } else { + $this->connection = $connection; + } + + $this->prefix = $this->connection->getConfig('prefix'); + } + + /** + * 创建一个新的查询对象 + * @access public + * @return Query + */ + public function newQuery() + { + $query = new static($this->connection); + + if ($this->model) { + $query->model($this->model); + } + + if (isset($this->options['table'])) { + $query->table($this->options['table']); + } else { + $query->name($this->name); + } + + if (isset($this->options['json'])) { + $query->json($this->options['json'], $this->options['json_assoc']); + } + + if (isset($this->options['field_type'])) { + $query->setJsonFieldType($this->options['field_type']); + } + + return $query; + } + + /** + * 利用__call方法实现一些特殊的Model方法 + * @access public + * @param string $method 方法名称 + * @param array $args 调用参数 + * @return mixed + * @throws DbException + * @throws Exception + */ + public function __call($method, $args) + { + if (isset(self::$extend[strtolower($method)])) { + // 调用扩展查询方法 + array_unshift($args, $this); + + return Container::getInstance() + ->invoke(self::$extend[strtolower($method)], $args); + } elseif (strtolower(substr($method, 0, 5)) == 'getby') { + // 根据某个字段获取记录 + $field = Loader::parseName(substr($method, 5)); + return $this->where($field, '=', $args[0])->find(); + } elseif (strtolower(substr($method, 0, 10)) == 'getfieldby') { + // 根据某个字段获取记录的某个值 + $name = Loader::parseName(substr($method, 10)); + return $this->where($name, '=', $args[0])->value($args[1]); + } elseif (strtolower(substr($method, 0, 7)) == 'whereor') { + $name = Loader::parseName(substr($method, 7)); + array_unshift($args, $name); + return call_user_func_array([$this, 'whereOr'], $args); + } elseif (strtolower(substr($method, 0, 5)) == 'where') { + $name = Loader::parseName(substr($method, 5)); + array_unshift($args, $name); + return call_user_func_array([$this, 'where'], $args); + } 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:' . ($this->model ? get_class($this->model) : static::class) . '->' . $method); + } + } + + /** + * 扩展查询方法 + * @access public + * @param string|array $method 查询方法名 + * @param callable $callback + * @return void + */ + public static function extend($method, $callback = null) + { + if (is_array($method)) { + foreach ($method as $key => $val) { + self::$extend[strtolower($key)] = $val; + } + } else { + self::$extend[strtolower($method)] = $callback; + } + } + + /** + * 设置当前的数据库Connection对象 + * @access public + * @param Connection $connection + * @return $this + */ + public function setConnection(Connection $connection) + { + $this->connection = $connection; + $this->prefix = $this->connection->getConfig('prefix'); + + return $this; + } + + /** + * 获取当前的数据库Connection对象 + * @access public + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * 指定模型 + * @access public + * @param Model $model 模型对象实例 + * @return $this + */ + public function model(Model $model) + { + $this->model = $model; + return $this; + } + + /** + * 获取当前的模型对象 + * @access public + * @return Model|null + */ + public function getModel() + { + return $this->model ? $this->model->setQuery($this) : null; + } + + /** + * 设置从主库读取数据 + * @access public + * @param bool $all 是否所有表有效 + * @return $this + */ + public function readMaster($all = false) + { + $table = $all ? '*' : $this->getTable(); + + static::$readMaster[$table] = true; + + return $this; + } + + /** + * 指定当前数据表名(不含前缀) + * @access public + * @param string $name + * @return $this + */ + public function name($name) + { + $this->name = $name; + return $this; + } + + /** + * 获取当前的数据表名称 + * @access public + * @return string + */ + public function getName() + { + return $this->name ?: $this->model->getName(); + } + + /** + * 得到当前或者指定名称的数据表 + * @access public + * @param string $name + * @return string + */ + public function getTable($name = '') + { + if (empty($name) && isset($this->options['table'])) { + return $this->options['table']; + } + + $name = $name ?: $this->name; + + return $this->prefix . Loader::parseName($name); + } + + /** + * 执行查询 返回数据集 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param boolean $master 是否在主服务器读操作 + * @param bool $pdo 是否返回PDO对象 + * @return mixed + * @throws BindParamException + * @throws PDOException + */ + public function query($sql, $bind = [], $master = false, $pdo = false) + { + return $this->connection->query($sql, $bind, $master, $pdo); + } + + /** + * 执行语句 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @return int + * @throws BindParamException + * @throws PDOException + */ + public function execute($sql, $bind = []) + { + return $this->connection->execute($sql, $bind, $this); + } + + /** + * 监听SQL执行 + * @access public + * @param callable $callback 回调方法 + * @return void + */ + public function listen($callback) + { + $this->connection->listen($callback); + } + + /** + * 获取最近插入的ID + * @access public + * @param string $sequence 自增序列名 + * @return string + */ + public function getLastInsID($sequence = null) + { + return $this->connection->getLastInsID($sequence); + } + + /** + * 获取返回或者影响的记录数 + * @access public + * @return integer + */ + public function getNumRows() + { + return $this->connection->getNumRows(); + } + + /** + * 获取最近一次查询的sql语句 + * @access public + * @return string + */ + public function getLastSql() + { + return $this->connection->getLastSql(); + } + + /** + * 执行数据库Xa事务 + * @access public + * @param callable $callback 数据操作方法回调 + * @param array $dbs 多个查询对象或者连接对象 + * @return mixed + * @throws PDOException + * @throws \Exception + * @throws \Throwable + */ + public function transactionXa($callback, array $dbs = []) + { + $xid = uniqid('xa'); + + if (empty($dbs)) { + $dbs[] = $this->getConnection(); + } + + foreach ($dbs as $key => $db) { + if ($db instanceof Query) { + $db = $db->getConnection(); + + $dbs[$key] = $db; + } + + $db->startTransXa($xid); + } + + try { + $result = null; + if (is_callable($callback)) { + $result = call_user_func_array($callback, [$this]); + } + + foreach ($dbs as $db) { + $db->prepareXa($xid); + } + + foreach ($dbs as $db) { + $db->commitXa($xid); + } + + return $result; + } catch (\Exception $e) { + foreach ($dbs as $db) { + $db->rollbackXa($xid); + } + throw $e; + } catch (\Throwable $e) { + foreach ($dbs as $db) { + $db->rollbackXa($xid); + } + throw $e; + } + } + + /** + * 执行数据库事务 + * @access public + * @param callable $callback 数据操作方法回调 + * @return mixed + */ + public function transaction($callback) + { + return $this->connection->transaction($callback); + } + + /** + * 启动事务 + * @access public + * @return void + */ + public function startTrans() + { + $this->connection->startTrans(); + } + + /** + * 用于非自动提交状态下面的查询提交 + * @access public + * @return void + * @throws PDOException + */ + public function commit() + { + $this->connection->commit(); + } + + /** + * 事务回滚 + * @access public + * @return void + * @throws PDOException + */ + public function rollback() + { + $this->connection->rollback(); + } + + /** + * 批处理执行SQL语句 + * 批处理的指令都认为是execute操作 + * @access public + * @param array $sql SQL批处理指令 + * @return boolean + */ + public function batchQuery($sql = []) + { + return $this->connection->batchQuery($sql); + } + + /** + * 获取数据库的配置参数 + * @access public + * @param string $name 参数名称 + * @return mixed + */ + public function getConfig($name = '') + { + return $this->connection->getConfig($name); + } + + /** + * 获取数据表字段信息 + * @access public + * @param string $tableName 数据表名 + * @return array + */ + public function getTableFields($tableName = '') + { + if ('' == $tableName) { + $tableName = isset($this->options['table']) ? $this->options['table'] : $this->getTable(); + } + + return $this->connection->getTableFields($tableName); + } + + /** + * 获取数据表字段类型 + * @access public + * @param string $tableName 数据表名 + * @param string $field 字段名 + * @return array|string + */ + public function getFieldsType($tableName = '', $field = null) + { + if ('' == $tableName) { + $tableName = isset($this->options['table']) ? $this->options['table'] : $this->getTable(); + } + + return $this->connection->getFieldsType($tableName, $field); + } + + /** + * 得到分表的的数据表名 + * @access public + * @param array $data 操作的数据 + * @param string $field 分表依据的字段 + * @param array $rule 分表规则 + * @return array + */ + public function getPartitionTableName($data, $field, $rule = []) + { + // 对数据表进行分区 + if ($field && isset($data[$field])) { + $value = $data[$field]; + $type = $rule['type']; + switch ($type) { + case 'id': + // 按照id范围分表 + $step = $rule['expr']; + $seq = floor($value / $step) + 1; + break; + case 'year': + // 按照年份分表 + if (!is_numeric($value)) { + $value = strtotime($value); + } + $seq = date('Y', $value) - $rule['expr'] + 1; + break; + case 'mod': + // 按照id的模数分表 + $seq = ($value % $rule['num']) + 1; + break; + case 'md5': + // 按照md5的序列分表 + $seq = (ord(substr(md5($value), 0, 1)) % $rule['num']) + 1; + break; + default: + if (function_exists($type)) { + // 支持指定函数哈希 + $value = $type($value); + } + + $seq = (ord(substr($value, 0, 1)) % $rule['num']) + 1; + } + + return $this->getTable() . '_' . $seq; + } + // 当设置的分表字段不在查询条件或者数据中 + // 进行联合查询,必须设定 partition['num'] + $tableName = []; + for ($i = 0; $i < $rule['num']; $i++) { + $tableName[] = 'SELECT * FROM ' . $this->getTable() . '_' . ($i + 1); + } + + return ['( ' . implode(" UNION ", $tableName) . ' )' => $this->name]; + } + + /** + * 得到某个字段的值 + * @access public + * @param string $field 字段名 + * @param mixed $default 默认值 + * @return mixed + */ + public function value($field, $default = null) + { + $this->parseOptions(); + + return $this->connection->value($this, $field, $default); + } + + /** + * 得到某个列的数组 + * @access public + * @param string $field 字段名 多个字段用逗号分隔 + * @param string $key 索引 + * @return array + */ + public function column($field, $key = '') + { + $this->parseOptions(); + + return $this->connection->column($this, $field, $key); + } + + /** + * 聚合查询 + * @access public + * @param string $aggregate 聚合方法 + * @param string|Expression $field 字段名 + * @param bool $force 强制转为数字类型 + * @return mixed + */ + public function aggregate($aggregate, $field, $force = false) + { + $this->parseOptions(); + + $result = $this->connection->aggregate($this, $aggregate, $field); + + if (!empty($this->options['fetch_sql'])) { + return $result; + } elseif ($force) { + $result = (float) $result; + } + + return $result; + } + + /** + * COUNT查询 + * @access public + * @param string|Expression $field 字段名 + * @return float|string + */ + public function count($field = '*') + { + if (!empty($this->options['group'])) { + // 支持GROUP + $options = $this->getOptions(); + $subSql = $this->options($options) + ->field('count(' . $field . ') AS think_count') + ->bind($this->bind) + ->buildSql(); + + $query = $this->newQuery()->table([$subSql => '_group_count_']); + + if (!empty($options['fetch_sql'])) { + $query->fetchSql(true); + } + + $count = $query->aggregate('COUNT', '*', true); + } else { + $count = $this->aggregate('COUNT', $field, true); + } + + return is_string($count) ? $count : (int) $count; + } + + /** + * SUM查询 + * @access public + * @param string|Expression $field 字段名 + * @return float + */ + public function sum($field) + { + return $this->aggregate('SUM', $field, true); + } + + /** + * MIN查询 + * @access public + * @param string|Expression $field 字段名 + * @param bool $force 强制转为数字类型 + * @return mixed + */ + public function min($field, $force = true) + { + return $this->aggregate('MIN', $field, $force); + } + + /** + * MAX查询 + * @access public + * @param string|Expression $field 字段名 + * @param bool $force 强制转为数字类型 + * @return mixed + */ + public function max($field, $force = true) + { + return $this->aggregate('MAX', $field, $force); + } + + /** + * AVG查询 + * @access public + * @param string|Expression $field 字段名 + * @return float + */ + public function avg($field) + { + return $this->aggregate('AVG', $field, true); + } + + /** + * 设置记录的某个字段值 + * 支持使用数据库字段和方法 + * @access public + * @param string|array $field 字段名 + * @param mixed $value 字段值 + * @return integer + */ + public function setField($field, $value = '') + { + if (is_array($field)) { + $data = $field; + } else { + $data[$field] = $value; + } + + return $this->update($data); + } + + /** + * 字段值(延迟)增长 + * @access public + * @param string $field 字段名 + * @param integer $step 增长值 + * @param integer $lazyTime 延时时间(s) + * @return integer|true + * @throws Exception + */ + public function setInc($field, $step = 1, $lazyTime = 0) + { + $condition = !empty($this->options['where']) ? $this->options['where'] : []; + + if (empty($condition)) { + // 没有条件不做任何更新 + throw new Exception('no data to update'); + } + + if ($lazyTime > 0) { + // 延迟写入 + $guid = md5($this->getTable() . '_' . $field . '_' . serialize($condition)); + $step = $this->lazyWrite('inc', $guid, $step, $lazyTime); + + if (false === $step) { + // 清空查询条件 + $this->options = []; + return true; + } + } + + return $this->setField($field, ['INC', $step]); + } + + /** + * 字段值(延迟)减少 + * @access public + * @param string $field 字段名 + * @param integer $step 减少值 + * @param integer $lazyTime 延时时间(s) + * @return integer|true + * @throws Exception + */ + public function setDec($field, $step = 1, $lazyTime = 0) + { + $condition = !empty($this->options['where']) ? $this->options['where'] : []; + + if (empty($condition)) { + // 没有条件不做任何更新 + throw new Exception('no data to update'); + } + + if ($lazyTime > 0) { + // 延迟写入 + $guid = md5($this->getTable() . '_' . $field . '_' . serialize($condition)); + $step = $this->lazyWrite('dec', $guid, $step, $lazyTime); + + if (false === $step) { + // 清空查询条件 + $this->options = []; + return true; + } + + $value = ['INC', $step]; + } else { + $value = ['DEC', $step]; + } + + return $this->setField($field, $value); + } + + /** + * 延时更新检查 返回false表示需要延时 + * 否则返回实际写入的数值 + * @access protected + * @param string $type 自增或者自减 + * @param string $guid 写入标识 + * @param integer $step 写入步进值 + * @param integer $lazyTime 延时时间(s) + * @return false|integer + */ + protected function lazyWrite($type, $guid, $step, $lazyTime) + { + $cache = Container::get('cache'); + + if (!$cache->has($guid . '_time')) { + // 计时开始 + $cache->set($guid . '_time', time(), 0); + $cache->$type($guid, $step); + } elseif (time() > $cache->get($guid . '_time') + $lazyTime) { + // 删除缓存 + $value = $cache->$type($guid, $step); + $cache->rm($guid); + $cache->rm($guid . '_time'); + return 0 === $value ? false : $value; + } else { + // 更新缓存 + $cache->$type($guid, $step); + } + + return false; + } + + /** + * 查询SQL组装 join + * @access public + * @param mixed $join 关联的表名 + * @param mixed $condition 条件 + * @param string $type JOIN类型 + * @param array $bind 参数绑定 + * @return $this + */ + public function join($join, $condition = null, $type = 'INNER', $bind = []) + { + if (empty($condition)) { + // 如果为组数,则循环调用join + foreach ($join as $key => $value) { + if (is_array($value) && 2 <= count($value)) { + $this->join($value[0], $value[1], isset($value[2]) ? $value[2] : $type); + } + } + } else { + $table = $this->getJoinTable($join); + if ($bind) { + $this->bindParams($condition, $bind); + } + $this->options['join'][] = [$table, strtoupper($type), $condition]; + } + + return $this; + } + + /** + * LEFT JOIN + * @access public + * @param mixed $join 关联的表名 + * @param mixed $condition 条件 + * @param array $bind 参数绑定 + * @return $this + */ + public function leftJoin($join, $condition = null, $bind = []) + { + return $this->join($join, $condition, 'LEFT'); + } + + /** + * RIGHT JOIN + * @access public + * @param mixed $join 关联的表名 + * @param mixed $condition 条件 + * @param array $bind 参数绑定 + * @return $this + */ + public function rightJoin($join, $condition = null, $bind = []) + { + return $this->join($join, $condition, 'RIGHT'); + } + + /** + * FULL JOIN + * @access public + * @param mixed $join 关联的表名 + * @param mixed $condition 条件 + * @param array $bind 参数绑定 + * @return $this + */ + public function fullJoin($join, $condition = null, $bind = []) + { + return $this->join($join, $condition, 'FULL'); + } + + /** + * 获取Join表名及别名 支持 + * ['prefix_table或者子查询'=>'alias'] 'table alias' + * @access protected + * @param array|string $join + * @param string $alias + * @return string + */ + protected function getJoinTable($join, &$alias = null) + { + if (is_array($join)) { + $table = $join; + $alias = array_shift($join); + } else { + $join = trim($join); + + if (false !== strpos($join, '(')) { + // 使用子查询 + $table = $join; + } else { + $prefix = $this->prefix; + if (strpos($join, ' ')) { + // 使用别名 + list($table, $alias) = explode(' ', $join); + } else { + $table = $join; + if (false === strpos($join, '.') && 0 !== strpos($join, '__')) { + $alias = $join; + } + } + + if ($prefix && false === strpos($table, '.') && 0 !== strpos($table, $prefix) && 0 !== strpos($table, '__')) { + $table = $this->getTable($table); + } + } + + if (isset($alias) && $table != $alias) { + $table = [$table => $alias]; + } + } + + return $table; + } + + /** + * 查询SQL组装 union + * @access public + * @param mixed $union + * @param boolean $all + * @return $this + */ + public function union($union, $all = false) + { + if (empty($union)) { + return $this; + } + + $this->options['union']['type'] = $all ? 'UNION ALL' : 'UNION'; + + if (is_array($union)) { + $this->options['union'] = array_merge($this->options['union'], $union); + } else { + $this->options['union'][] = $union; + } + + return $this; + } + + /** + * 查询SQL组装 union all + * @access public + * @param mixed $union + * @return $this + */ + public function unionAll($union) + { + return $this->union($union, true); + } + + /** + * 指定查询字段 支持字段排除和指定数据表 + * @access public + * @param mixed $field + * @param boolean $except 是否排除 + * @param string $tableName 数据表名 + * @param string $prefix 字段前缀 + * @param string $alias 别名前缀 + * @return $this + */ + public function field($field, $except = false, $tableName = '', $prefix = '', $alias = '') + { + 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) { + // 获取全部字段 + $fields = $this->getTableFields($tableName); + $field = $fields ?: ['*']; + } elseif ($except) { + // 字段排除 + $fields = $this->getTableFields($tableName); + $field = $fields ? array_diff($fields, $field) : $field; + } + + if ($tableName) { + // 添加统一的前缀 + $prefix = $prefix ?: $tableName; + foreach ($field as $key => &$val) { + if (is_numeric($key) && $alias) { + $field[$prefix . '.' . $val] = $alias . $val; + unset($field[$key]); + } elseif (is_numeric($key)) { + $val = $prefix . '.' . $val; + } + } + } + + if (isset($this->options['field'])) { + $field = array_merge((array) $this->options['field'], $field); + } + + $this->options['field'] = array_unique($field); + + return $this; + } + + /** + * 表达式方式指定查询字段 + * @access public + * @param string $field 字段名 + * @return $this + */ + public function fieldRaw($field) + { + $this->options['field'][] = $this->raw($field); + + return $this; + } + + /** + * 设置数据 + * @access public + * @param mixed $field 字段名或者数据 + * @param mixed $value 字段值 + * @return $this + */ + public function data($field, $value = null) + { + if (is_array($field)) { + $this->options['data'] = isset($this->options['data']) ? array_merge($this->options['data'], $field) : $field; + } else { + $this->options['data'][$field] = $value; + } + + return $this; + } + + /** + * 字段值增长 + * @access public + * @param string|array $field 字段名 + * @param integer $step 增长值 + * @return $this + */ + public function inc($field, $step = 1, $op = 'INC') + { + $fields = is_string($field) ? explode(',', $field) : $field; + + foreach ($fields as $field => $val) { + if (is_numeric($field)) { + $field = $val; + } else { + $step = $val; + } + + $this->data($field, [$op, $step]); + } + + return $this; + } + + /** + * 字段值减少 + * @access public + * @param string|array $field 字段名 + * @param integer $step 增长值 + * @return $this + */ + public function dec($field, $step = 1) + { + return $this->inc($field, $step, 'DEC'); + } + + /** + * 使用表达式设置数据 + * @access public + * @param string $field 字段名 + * @param string $value 字段值 + * @return $this + */ + public function exp($field, $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 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) { + foreach ($join as $key => $val) { + $this->view($val[0], $val[1], isset($val[2]) ? $val[2] : null, isset($val[3]) ? $val[3] : 'INNER'); + } + } else { + $fields = []; + $table = $this->getJoinTable($join, $alias); + + if (true === $field) { + $fields = $alias . '.*'; + } else { + if (is_string($field)) { + $field = explode(',', $field); + } + + foreach ($field as $key => $val) { + if (is_numeric($key)) { + $fields[] = $alias . '.' . $val; + + $this->options['map'][$val] = $alias . '.' . $val; + } else { + if (preg_match('/[,=\.\'\"\(\s]/', $key)) { + $name = $key; + } else { + $name = $alias . '.' . $key; + } + + $fields[] = $name . ' AS ' . $val; + + $this->options['map'][$val] = $name; + } + } + } + + $this->field($fields); + + if ($on) { + $this->join($table, $on, $type); + } else { + $this->table($table); + } + } + + return $this; + } + + /** + * 设置分表规则 + * @access public + * @param array $data 操作的数据 + * @param string $field 分表依据的字段 + * @param array $rule 分表规则 + * @return $this + */ + public function partition($data, $field, $rule = []) + { + $this->options['table'] = $this->getPartitionTableName($data, $field, $rule); + + return $this; + } + + /** + * 指定AND查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @return $this + */ + public function where($field, $op = null, $condition = null) + { + $param = func_get_args(); + array_shift($param); + return $this->parseWhereExp('AND', $field, $op, $condition, $param); + } + + /** + * 指定OR查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @return $this + */ + public function whereOr($field, $op = null, $condition = null) + { + $param = func_get_args(); + array_shift($param); + return $this->parseWhereExp('OR', $field, $op, $condition, $param); + } + + /** + * 指定XOR查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @return $this + */ + public function whereXor($field, $op = null, $condition = null) + { + $param = func_get_args(); + array_shift($param); + return $this->parseWhereExp('XOR', $field, $op, $condition, $param); + } + + /** + * 指定Null查询条件 + * @access public + * @param mixed $field 查询字段 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNull($field, $logic = 'AND') + { + return $this->parseWhereExp($logic, $field, 'NULL', null, [], true); + } + + /** + * 指定NotNull查询条件 + * @access public + * @param mixed $field 查询字段 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotNull($field, $logic = 'AND') + { + return $this->parseWhereExp($logic, $field, 'NOTNULL', null, [], true); + } + + /** + * 指定Exists查询条件 + * @access public + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereExists($condition, $logic = 'AND') + { + if (is_string($condition)) { + $condition = $this->raw($condition); + } + + $this->options['where'][strtoupper($logic)][] = ['', 'EXISTS', $condition]; + return $this; + } + + /** + * 指定NotExists查询条件 + * @access public + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotExists($condition, $logic = 'AND') + { + if (is_string($condition)) { + $condition = $this->raw($condition); + } + + $this->options['where'][strtoupper($logic)][] = ['', 'NOT EXISTS', $condition]; + return $this; + } + + /** + * 指定In查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereIn($field, $condition, $logic = 'AND') + { + return $this->parseWhereExp($logic, $field, 'IN', $condition, [], true); + } + + /** + * 指定NotIn查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotIn($field, $condition, $logic = 'AND') + { + return $this->parseWhereExp($logic, $field, 'NOT IN', $condition, [], true); + } + + /** + * 指定Like查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereLike($field, $condition, $logic = 'AND') + { + return $this->parseWhereExp($logic, $field, 'LIKE', $condition, [], true); + } + + /** + * 指定NotLike查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotLike($field, $condition, $logic = 'AND') + { + return $this->parseWhereExp($logic, $field, 'NOT LIKE', $condition, [], true); + } + + /** + * 指定Between查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereBetween($field, $condition, $logic = 'AND') + { + return $this->parseWhereExp($logic, $field, 'BETWEEN', $condition, [], true); + } + + /** + * 指定NotBetween查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotBetween($field, $condition, $logic = 'AND') + { + return $this->parseWhereExp($logic, $field, 'NOT BETWEEN', $condition, [], true); + } + + /** + * 比较两个字段 + * @access public + * @param string|array $field1 查询字段 + * @param string $operator 比较操作符 + * @param string $field2 比较字段 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereColumn($field1, $operator = null, $field2 = null, $logic = 'AND') + { + if (is_array($field1)) { + foreach ($field1 as $item) { + $this->whereColumn($item[0], $item[1], isset($item[2]) ? $item[2] : null); + } + return $this; + } + + if (is_null($field2)) { + $field2 = $operator; + $operator = '='; + } + + return $this->parseWhereExp($logic, $field1, 'COLUMN', [$operator, $field2], [], true); + } + + /** + * 设置软删除字段及条件 + * @access public + * @param false|string $field 查询字段 + * @param mixed $condition 查询条件 + * @return $this + */ + public function useSoftDelete($field, $condition = null) + { + if ($field) { + $this->options['soft_delete'] = [$field, $condition]; + } + + return $this; + } + + /** + * 指定Exp查询条件 + * @access public + * @param mixed $field 查询字段 + * @param string $where 查询条件 + * @param array $bind 参数绑定 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereExp($field, $where, $bind = [], $logic = 'AND') + { + if ($bind) { + $this->bindParams($where, $bind); + } + + $this->options['where'][$logic][] = [$field, 'EXP', $this->raw($where)]; + + 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') + { + if ($bind) { + $this->bindParams($where, $bind); + } + + $this->options['where'][$logic][] = $this->raw($where); + + return $this; + } + + /** + * 参数绑定 + * @access public + * @param string $sql 绑定的sql表达式 + * @param array $bind 参数绑定 + * @return void + */ + protected function bindParams(&$sql, array $bind = []) + { + foreach ($bind as $key => $value) { + if (is_array($value)) { + $name = $this->bind($value[0], $value[1], isset($value[2]) ? $value[2] : null); + } else { + $name = $this->bind($value); + } + + if (is_numeric($key)) { + $sql = substr_replace($sql, ':' . $name, strpos($sql, '?'), 1); + } else { + $sql = str_replace(':' . $key, ':' . $name, $sql); + } + } + } + + /** + * 指定表达式查询条件 OR + * @access public + * @param string $where 查询条件 + * @param array $bind 参数绑定 + * @return $this + */ + public function whereOrRaw($where, $bind = []) + { + return $this->whereRaw($where, $bind, 'OR'); + } + + /** + * 分析查询表达式 + * @access protected + * @param string $logic 查询逻辑 and or xor + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @param array $param 查询参数 + * @param bool $strict 严格模式 + * @return $this + */ + protected function parseWhereExp($logic, $field, $op, $condition, array $param = [], $strict = false) + { + if ($field instanceof $this) { + $this->options['where'] = $field->getOptions('where'); + $this->bind($field->getBind(false)); + return $this; + } + + $logic = strtoupper($logic); + + if ($field instanceof Where) { + $this->options['where'][$logic] = $field->parse(); + return $this; + } + + if (is_string($field) && !empty($this->options['via']) && false === strpos($field, '.')) { + $field = $this->options['via'] . '.' . $field; + } + + if ($field instanceof Expression) { + return $this->whereRaw($field, is_array($op) ? $op : [], $logic); + } elseif ($strict) { + // 使用严格模式查询 + $where = [$field, $op, $condition, $logic]; + } elseif (is_array($field)) { + // 解析数组批量查询 + return $this->parseArrayWhereItems($field, $logic); + } elseif ($field instanceof \Closure) { + $where = $field; + } elseif (is_string($field)) { + if (preg_match('/[,=\<\'\"\(\s]/', $field)) { + return $this->whereRaw($field, $op, $logic); + } elseif (is_string($op) && strtolower($op) == 'exp') { + $bind = isset($param[2]) && is_array($param[2]) ? $param[2] : null; + return $this->whereExp($field, $condition, $bind, $logic); + } + + $where = $this->parseWhereItem($logic, $field, $op, $condition, $param); + } + + if (!empty($where)) { + $this->options['where'][$logic][] = $where; + } + + return $this; + } + + /** + * 分析查询表达式 + * @access protected + * @param string $logic 查询逻辑 and or xor + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @param array $param 查询参数 + * @return mixed + */ + protected function parseWhereItem($logic, $field, $op, $condition, $param = []) + { + if (is_array($op)) { + // 同一字段多条件查询 + array_unshift($param, $field); + $where = $param; + } elseif ($field && is_null($condition)) { + if (in_array(strtoupper($op), ['NULL', 'NOTNULL', 'NOT NULL'], true)) { + // null查询 + $where = [$field, $op, '']; + } elseif (in_array($op, ['=', 'eq', 'EQ', null], true)) { + $where = [$field, 'NULL', '']; + } elseif (in_array($op, ['<>', 'neq', 'NEQ'], true)) { + $where = [$field, 'NOTNULL', '']; + } else { + // 字段相等查询 + $where = [$field, '=', $op]; + } + } elseif (in_array(strtoupper($op), ['EXISTS', 'NOT EXISTS', 'NOTEXISTS'], true)) { + $where = [$field, $op, is_string($condition) ? $this->raw($condition) : $condition]; + } else { + $where = $field ? [$field, $op, $condition, isset($param[2]) ? $param[2] : null] : null; + } + + return $where; + } + + /** + * 数组批量查询 + * @access protected + * @param array $field 批量查询 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + protected function parseArrayWhereItems($field, $logic) + { + if (key($field) !== 0) { + $where = []; + foreach ($field as $key => $val) { + if ($val instanceof Expression) { + $where[] = [$key, 'exp', $val]; + } elseif (is_null($val)) { + $where[] = [$key, 'NULL', '']; + } else { + $where[] = [$key, is_array($val) ? 'IN' : '=', $val]; + } + } + } else { + // 数组批量查询 + $where = $field; + } + + if (!empty($where)) { + $this->options['where'][$logic] = isset($this->options['where'][$logic]) ? array_merge($this->options['where'][$logic], $where) : $where; + } + + return $this; + } + + /** + * 去除某个查询条件 + * @access public + * @param string $field 查询字段 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function removeWhereField($field, $logic = 'AND') + { + $logic = strtoupper($logic); + + if (isset($this->options['where'][$logic])) { + foreach ($this->options['where'][$logic] as $key => $val) { + if (is_array($val) && $val[0] == $field) { + unset($this->options['where'][$logic][$key]); + } + } + } + + return $this; + } + + /** + * 去除查询参数 + * @access public + * @param string|bool $option 参数名 true 表示去除所有参数 + * @return $this + */ + public function removeOption($option = true) + { + if (true === $option) { + $this->options = []; + $this->bind = []; + } elseif (is_string($option) && isset($this->options[$option])) { + unset($this->options[$option]); + } + + return $this; + } + + /** + * 条件查询 + * @access public + * @param mixed $condition 满足条件(支持闭包) + * @param \Closure|array $query 满足条件后执行的查询表达式(闭包或数组) + * @param \Closure|array $otherwise 不满足条件后执行 + * @return $this + */ + public function when($condition, $query, $otherwise = null) + { + if ($condition instanceof \Closure) { + $condition = $condition($this); + } + + if ($condition) { + if ($query instanceof \Closure) { + $query($this, $condition); + } elseif (is_array($query)) { + $this->where($query); + } + } elseif ($otherwise) { + if ($otherwise instanceof \Closure) { + $otherwise($this, $condition); + } elseif (is_array($otherwise)) { + $this->where($otherwise); + } + } + + return $this; + } + + /** + * 指定查询数量 + * @access public + * @param mixed $offset 起始位置 + * @param mixed $length 查询数量 + * @return $this + */ + public function limit($offset, $length = null) + { + if (is_null($length) && strpos($offset, ',')) { + list($offset, $length) = explode(',', $offset); + } + + $this->options['limit'] = intval($offset) . ($length ? ',' . intval($length) : ''); + + return $this; + } + + /** + * 指定分页 + * @access public + * @param mixed $page 页数 + * @param mixed $listRows 每页数量 + * @return $this + */ + public function page($page, $listRows = null) + { + if (is_null($listRows) && strpos($page, ',')) { + list($page, $listRows) = explode(',', $page); + } + + $this->options['page'] = [intval($page), intval($listRows)]; + + return $this; + } + + /** + * 分页查询 + * @access public + * @param int|array $listRows 每页数量 数组表示配置参数 + * @param int|bool $simple 是否简洁模式或者总记录数 + * @param array $config 配置参数 + * page:当前页, + * path:url路径, + * query:url额外参数, + * fragment:url锚点, + * var_page:分页变量, + * list_rows:每页数量 + * type:分页类名 + * @return $this[]|\think\Paginator + * @throws DbException + */ + public function paginate($listRows = null, $simple = false, $config = []) + { + if (is_int($simple)) { + $total = $simple; + $simple = false; + } + + $paginate = Container::get('config')->pull('paginate'); + + if (is_array($listRows)) { + $config = array_merge($paginate, $listRows); + $listRows = $config['list_rows']; + } else { + $config = array_merge($paginate, $config); + $listRows = $listRows ?: $config['list_rows']; + } + + /** @var Paginator $class */ + $class = false !== strpos($config['type'], '\\') ? $config['type'] : '\\think\\paginator\\driver\\' . ucwords($config['type']); + $page = isset($config['page']) ? (int) $config['page'] : call_user_func([ + $class, + 'getCurrentPage', + ], $config['var_page']); + + $page = $page < 1 ? 1 : $page; + + $config['path'] = isset($config['path']) ? $config['path'] : call_user_func([$class, 'getCurrentPath']); + + if (!isset($total) && !$simple) { + $options = $this->getOptions(); + + unset($this->options['order'], $this->options['limit'], $this->options['page'], $this->options['field']); + + $bind = $this->bind; + $total = $this->count(); + $results = $this->options($options)->bind($bind)->page($page, $listRows)->select(); + } elseif ($simple) { + $results = $this->limit(($page - 1) * $listRows, $listRows + 1)->select(); + $total = null; + } else { + $results = $this->page($page, $listRows)->select(); + } + + $this->removeOption('limit'); + $this->removeOption('page'); + + return $class::make($results, $listRows, $page, $total, $simple, $config); + } + + /** + * 指定当前操作的数据表 + * @access public + * @param mixed $table 表名 + * @return $this + */ + public function table($table) + { + if (is_string($table)) { + if (strpos($table, ')')) { + // 子查询 + } elseif (strpos($table, ',')) { + $tables = explode(',', $table); + $table = []; + + foreach ($tables as $item) { + list($item, $alias) = explode(' ', trim($item)); + if ($alias) { + $this->alias([$item => $alias]); + $table[$item] = $alias; + } else { + $table[] = $item; + } + } + } elseif (strpos($table, ' ')) { + list($table, $alias) = explode(' ', $table); + + $table = [$table => $alias]; + $this->alias($table); + } + } else { + $tables = $table; + $table = []; + + foreach ($tables as $key => $val) { + if (is_numeric($key)) { + $table[] = $val; + } else { + $this->alias([$key => $val]); + $table[$key] = $val; + } + } + } + + $this->options['table'] = $table; + + return $this; + } + + /** + * USING支持 用于多表删除 + * @access public + * @param mixed $using + * @return $this + */ + public function using($using) + { + $this->options['using'] = $using; + return $this; + } + + /** + * 指定排序 order('id','desc') 或者 order(['id'=>'desc','create_time'=>'desc']) + * @access public + * @param string|array $field 排序字段 + * @param string $order 排序 + * @return $this + */ + public function order($field, $order = null) + { + 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 (strpos($field, ',')) { + $field = array_map('trim', explode(',', $field)); + } else { + $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, $bind = []) + { + if ($bind) { + $this->bindParams($field, $bind); + } + + $this->options['order'][] = $this->raw($field); + + return $this; + } + + /** + * 指定Field排序 order('id',[1,2,3],'desc') + * @access public + * @param string|array $field 排序字段 + * @param array $values 排序值 + * @param string $order + * @return $this + */ + public function orderField($field, array $values, $order = '') + { + if (!empty($values)) { + $values['sort'] = $order; + + $this->options['order'][$field] = $values; + } + + return $this; + } + + /** + * 随机排序 + * @access public + * @return $this + */ + public function orderRand() + { + $this->options['order'][] = '[rand]'; + return $this; + } + + /** + * 查询缓存 + * @access public + * @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 ($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]; + } + + return $this; + } + + /** + * 指定group查询 + * @access public + * @param string|array $group GROUP + * @return $this + */ + public function group($group) + { + $this->options['group'] = $group; + return $this; + } + + /** + * 指定having查询 + * @access public + * @param string $having having + * @return $this + */ + public function having($having) + { + $this->options['having'] = $having; + return $this; + } + + /** + * 指定查询lock + * @access public + * @param bool|string $lock 是否lock + * @return $this + */ + public function lock($lock = false) + { + $this->options['lock'] = $lock; + $this->options['master'] = true; + + return $this; + } + + /** + * 指定distinct查询 + * @access public + * @param string $distinct 是否唯一 + * @return $this + */ + public function distinct($distinct) + { + $this->options['distinct'] = $distinct; + return $this; + } + + /** + * 指定数据表别名 + * @access public + * @param array|string $alias 数据表别名 + * @return $this + */ + public function alias($alias) + { + if (is_array($alias)) { + foreach ($alias as $key => $val) { + if (false !== strpos($key, '__')) { + $table = $this->connection->parseSqlTable($key); + } else { + $table = $key; + } + $this->options['alias'][$table] = $val; + } + } else { + if (isset($this->options['table'])) { + $table = is_array($this->options['table']) ? key($this->options['table']) : $this->options['table']; + if (false !== strpos($table, '__')) { + $table = $this->connection->parseSqlTable($table); + } + } else { + $table = $this->getTable(); + } + + $this->options['alias'][$table] = $alias; + } + + return $this; + } + + /** + * 指定强制索引 + * @access public + * @param string $force 索引名称 + * @return $this + */ + public function force($force) + { + $this->options['force'] = $force; + return $this; + } + + /** + * 查询注释 + * @access public + * @param string $comment 注释 + * @return $this + */ + public function comment($comment) + { + $this->options['comment'] = $comment; + return $this; + } + + /** + * 获取执行的SQL语句 + * @access public + * @param boolean $fetch 是否返回sql + * @return $this + */ + public function fetchSql($fetch = true) + { + $this->options['fetch_sql'] = $fetch; + return $this; + } + + /** + * 不主动获取数据集 + * @access public + * @param bool $pdo 是否返回 PDOStatement 对象 + * @return $this + */ + public function fetchPdo($pdo = true) + { + $this->options['fetch_pdo'] = $pdo; + return $this; + } + + /** + * 设置是否返回数据集对象(支持设置数据集对象类名) + * @access public + * @param bool|string $collection 是否返回数据集对象 + * @return $this + */ + public function fetchCollection($collection = true) + { + $this->options['collection'] = $collection; + + return $this; + } + + /** + * 设置从主服务器读取数据 + * @access public + * @return $this + */ + public function master() + { + $this->options['master'] = true; + return $this; + } + + /** + * 设置是否严格检查字段名 + * @access public + * @param bool $strict 是否严格检查字段 + * @return $this + */ + public function strict($strict = true) + { + $this->options['strict'] = $strict; + return $this; + } + + /** + * 设置查询数据不存在是否抛出异常 + * @access public + * @param bool $fail 数据不存在是否抛出异常 + * @return $this + */ + public function failException($fail = true) + { + $this->options['fail'] = $fail; + return $this; + } + + /** + * 设置自增序列名 + * @access public + * @param string $sequence 自增序列名 + * @return $this + */ + public function sequence($sequence = null) + { + $this->options['sequence'] = $sequence; + return $this; + } + + /** + * 设置需要隐藏的输出属性 + * @access public + * @param mixed $hidden 需要隐藏的字段名 + * @return $this + */ + public function hidden($hidden) + { + if ($this->model) { + $this->options['hidden'] = $hidden; + return $this; + } + + return $this->field($hidden, true); + } + + /** + * 设置需要输出的属性 + * @access public + * @param array $visible 需要输出的属性 + * @return $this + */ + public function visible(array $visible) + { + $this->options['visible'] = $visible; + return $this; + } + + /** + * 设置需要附加的输出属性 + * @access public + * @param array $append 属性列表 + * @return $this + */ + public function append(array $append = []) + { + $this->options['append'] = $append; + return $this; + } + + /** + * 设置数据字段获取器 + * @access public + * @param string|array $name 字段名 + * @param callable $callback 闭包获取器 + * @return $this + */ + public function withAttr($name, $callback = null) + { + if (is_array($name)) { + $this->options['with_attr'] = $name; + } else { + $this->options['with_attr'][$name] = $callback; + } + + return $this; + } + + /** + * 设置JSON字段信息 + * @access public + * @param array $json JSON字段 + * @param bool $assoc 是否取出数组 + * @return $this + */ + public function json(array $json = [], $assoc = false) + { + $this->options['json'] = $json; + $this->options['json_assoc'] = $assoc; + return $this; + } + + /** + * 设置字段类型信息 + * @access public + * @param array $type 字段类型信息 + * @return $this + */ + public function setJsonFieldType(array $type) + { + $this->options['field_type'] = $type; + return $this; + } + + /** + * 获取字段类型信息 + * @access public + * @param string $field 字段名 + * @return string|null + */ + public function getJsonFieldType($field) + { + return isset($this->options['field_type'][$field]) ? $this->options['field_type'][$field] : null; + } + + /** + * 是否允许返回空数据(或空模型) + * @access public + * @param bool $allowEmpty 是否允许为空 + * @return $this + */ + public function allowEmpty($allowEmpty = true) + { + $this->options['allow_empty'] = $allowEmpty; + return $this; + } + + /** + * 添加查询范围 + * @access public + * @param array|string|\Closure $scope 查询范围定义 + * @param array $args 参数 + * @return $this + */ + public function scope($scope, ...$args) + { + // 查询范围的第一个参数始终是当前查询对象 + array_unshift($args, $this); + + if ($scope instanceof \Closure) { + call_user_func_array($scope, $args); + return $this; + } + + if (is_string($scope)) { + $scope = explode(',', $scope); + } + + if ($this->model) { + // 检查模型类的查询范围方法 + foreach ($scope as $name) { + $method = 'scope' . trim($name); + + if (method_exists($this->model, $method)) { + call_user_func_array([$this->model, $method], $args); + } + } + } + + return $this; + } + + /** + * 使用搜索器条件搜索字段 + * @access public + * @param array $fields 搜索字段 + * @param array $data 搜索数据 + * @param string $prefix 字段前缀标识 + * @return $this + */ + public function withSearch(array $fields, array $data = [], $prefix = '') + { + foreach ($fields as $key => $field) { + if ($field instanceof \Closure) { + $field($this, isset($data[$key]) ? $data[$key] : null, $data, $prefix); + } elseif ($this->model) { + // 检测搜索器 + $fieldName = is_numeric($key) ? $field : $key; + $method = 'search' . Loader::parseName($fieldName, 1) . 'Attr'; + + if (method_exists($this->model, $method)) { + $this->model->$method($this, isset($data[$field]) ? $data[$field] : null, $data, $prefix); + } + } + } + + return $this; + } + + /** + * 指定数据表主键 + * @access public + * @param string $pk 主键 + * @return $this + */ + public function pk($pk) + { + $this->pk = $pk; + return $this; + } + + /** + * 查询日期或者时间 + * @access public + * @param string $name 时间表达式 + * @param string|array $rule 时间范围 + * @return $this + */ + public function timeRule($name, $rule) + { + $this->timeRule[$name] = $rule; + return $this; + } + + /** + * 查询日期或者时间 + * @access public + * @param string $field 日期字段名 + * @param string|array $op 比较运算符或者表达式 + * @param string|array $range 比较范围 + * @param string $logic AND OR + * @return $this + */ + public function whereTime($field, $op, $range = null, $logic = 'AND') + { + if (is_null($range)) { + if (is_array($op)) { + $range = $op; + } else { + if (isset($this->timeExp[strtolower($op)])) { + $op = $this->timeExp[strtolower($op)]; + } + + if (isset($this->timeRule[strtolower($op)])) { + $range = $this->timeRule[strtolower($op)]; + } else { + $range = $op; + } + } + + $op = is_array($range) ? 'between' : '>='; + } + + return $this->parseWhereExp($logic, $field, strtolower($op) . ' time', $range, [], true); + } + + /** + * 查询当前时间在两个时间字段范围 + * @access public + * @param string $startField 开始时间字段 + * @param string $endField 结束时间字段 + * @return $this + */ + public function whereBetweenTimeField($startField, $endField) + { + return $this->whereTime($startField, '<=', time()) + ->whereTime($endField, '>=', time()); + } + + /** + * 查询当前时间不在两个时间字段范围 + * @access public + * @param string $startField 开始时间字段 + * @param string $endField 结束时间字段 + * @return $this + */ + public function whereNotBetweenTimeField($startField, $endField) + { + return $this->whereTime($startField, '>', time()) + ->whereTime($endField, '<', time(), 'OR'); + } + + /** + * 查询日期或者时间范围 + * @access public + * @param string $field 日期字段名 + * @param string $startTime 开始时间 + * @param string $endTime 结束时间 + * @param string $logic AND OR + * @return $this + */ + public function whereBetweenTime($field, $startTime, $endTime = null, $logic = 'AND') + { + if (is_null($endTime)) { + $time = is_string($startTime) ? strtotime($startTime) : $startTime; + $endTime = strtotime('+1 day', $time); + } + + return $this->parseWhereExp($logic, $field, 'between time', [$startTime, $endTime], [], true); + } + + /** + * 获取当前数据表的主键 + * @access public + * @param string|array $options 数据表名或者查询参数 + * @return string|array + */ + public function getPk($options = '') + { + if (!empty($this->pk)) { + $pk = $this->pk; + } else { + $pk = $this->connection->getPk(is_array($options) && isset($options['table']) ? $options['table'] : $this->getTable()); + } + + return $pk; + } + + /** + * 参数绑定 + * @access public + * @param mixed $value 绑定变量值 + * @param integer $type 绑定类型 + * @param string $name 绑定名称 + * @return $this|string + */ + public function bind($value, $type = PDO::PARAM_STR, $name = null) + { + if (is_array($value)) { + $this->bind = array_merge($this->bind, $value); + } else { + $name = $name ?: 'ThinkBind_' . (count($this->bind) + 1) . '_' . mt_rand() . '_'; + + $this->bind[$name] = [$value, $type]; + return $name; + } + + return $this; + } + + /** + * 检测参数是否已经绑定 + * @access public + * @param string $key 参数名 + * @return bool + */ + public function isBind($key) + { + return isset($this->bind[$key]); + } + + /** + * 查询参数赋值 + * @access public + * @param string $name 参数名 + * @param mixed $value 值 + * @return $this + */ + public function option($name, $value) + { + $this->options[$name] = $value; + return $this; + } + + /** + * 查询参数赋值 + * @access protected + * @param array $options 表达式参数 + * @return $this + */ + protected function options(array $options) + { + $this->options = $options; + return $this; + } + + /** + * 获取当前的查询参数 + * @access public + * @param string $name 参数名 + * @return mixed + */ + public function getOptions($name = '') + { + if ('' === $name) { + return $this->options; + } + return isset($this->options[$name]) ? $this->options[$name] : null; + } + + /** + * 设置当前的查询参数 + * @access public + * @param string $option 参数名 + * @param mixed $value 参数值 + * @return $this + */ + public function setOption($option, $value) + { + $this->options[$option] = $value; + return $this; + } + + /** + * 设置关联查询JOIN预查询 + * @access public + * @param string|array $with 关联方法名称 + * @return $this + */ + public function with($with) + { + if (empty($with)) { + return $this; + } + + if (is_string($with)) { + $with = explode(',', $with); + } + + $first = true; + + /** @var Model $class */ + $class = $this->model; + foreach ($with as $key => $relation) { + $closure = null; + + if ($relation instanceof \Closure) { + // 支持闭包查询过滤关联条件 + $closure = $relation; + $relation = $key; + } elseif (is_array($relation)) { + $relation = $key; + } elseif (is_string($relation) && strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation, 2); + } + + /** @var Relation $model */ + $relation = Loader::parseName($relation, 1, false); + $model = $class->$relation(); + + if ($model instanceof OneToOne && 0 == $model->getEagerlyType()) { + $table = $model->getTable(); + $model->removeOption() + ->table($table) + ->eagerly($this, $relation, true, '', $closure, $first); + $first = false; + } + } + + $this->via(); + + $this->options['with'] = $with; + + return $this; + } + + /** + * 关联预载入 JOIN方式(不支持嵌套) + * @access protected + * @param string|array $with 关联方法名 + * @param string $joinType JOIN方式 + * @return $this + */ + public function withJoin($with, $joinType = '') + { + if (empty($with)) { + return $this; + } + + if (is_string($with)) { + $with = explode(',', $with); + } + + $first = true; + + /** @var Model $class */ + $class = $this->model; + foreach ($with as $key => $relation) { + $closure = null; + $field = true; + + if ($relation instanceof \Closure) { + // 支持闭包查询过滤关联条件 + $closure = $relation; + $relation = $key; + } elseif (is_array($relation)) { + $field = $relation; + $relation = $key; + } elseif (is_string($relation) && strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation, 2); + } + + /** @var Relation $model */ + $relation = Loader::parseName($relation, 1, false); + $model = $class->$relation(); + + if ($model instanceof OneToOne) { + $model->eagerly($this, $relation, $field, $joinType, $closure, $first); + $first = false; + } else { + // 不支持其它关联 + unset($with[$key]); + } + } + + $this->via(); + + $this->options['with_join'] = $with; + + return $this; + } + + /** + * 关联统计 + * @access protected + * @param string|array $relation 关联方法名 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + protected function withAggregate($relation, $aggregate = 'count', $field = '*', $subQuery = true) + { + $relations = is_string($relation) ? explode(',', $relation) : $relation; + + if (!$subQuery) { + $this->options['with_count'][] = [$relations, $aggregate, $field]; + } else { + if (!isset($this->options['field'])) { + $this->field('*'); + } + + foreach ($relations as $key => $relation) { + $closure = $aggregateField = null; + + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } elseif (!is_int($key)) { + $aggregateField = $relation; + $relation = $key; + } + + $relation = Loader::parseName($relation, 1, false); + + $count = $this->model->$relation()->getRelationCountQuery($closure, $aggregate, $field, $aggregateField); + + if (empty($aggregateField)) { + $aggregateField = Loader::parseName($relation) . '_' . $aggregate; + } + + $this->field(['(' . $count . ')' => $aggregateField]); + } + } + + return $this; + } + + /** + * 关联统计 + * @access public + * @param string|array $relation 关联方法名 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + public function withCount($relation, $subQuery = true) + { + return $this->withAggregate($relation, 'count', '*', $subQuery); + } + + /** + * 关联统计Sum + * @access public + * @param string|array $relation 关联方法名 + * @param string $field 字段 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + public function withSum($relation, $field, $subQuery = true) + { + return $this->withAggregate($relation, 'sum', $field, $subQuery); + } + + /** + * 关联统计Max + * @access public + * @param string|array $relation 关联方法名 + * @param string $field 字段 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + public function withMax($relation, $field, $subQuery = true) + { + return $this->withAggregate($relation, 'max', $field, $subQuery); + } + + /** + * 关联统计Min + * @access public + * @param string|array $relation 关联方法名 + * @param string $field 字段 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + public function withMin($relation, $field, $subQuery = true) + { + return $this->withAggregate($relation, 'min', $field, $subQuery); + } + + /** + * 关联统计Avg + * @access public + * @param string|array $relation 关联方法名 + * @param string $field 字段 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + public function withAvg($relation, $field, $subQuery = true) + { + return $this->withAggregate($relation, 'avg', $field, $subQuery); + } + + /** + * 关联预加载中 获取关联指定字段值 + * example: + * Model::with(['relation' => function($query){ + * $query->withField("id,name"); + * }]) + * + * @access public + * @param string | array $field 指定获取的字段 + * @return $this + */ + public function withField($field) + { + $this->options['with_field'] = $field; + + return $this; + } + + /** + * 设置当前字段添加的表别名 + * @access public + * @param string $via + * @return $this + */ + public function via($via = '') + { + $this->options['via'] = $via; + + return $this; + } + + /** + * 设置关联查询 + * @access public + * @param string|array $relation 关联名称 + * @return $this + */ + public function relation($relation) + { + if (empty($relation)) { + return $this; + } + + if (is_string($relation)) { + $relation = explode(',', $relation); + } + + if (isset($this->options['relation'])) { + $this->options['relation'] = array_merge($this->options['relation'], $relation); + } else { + $this->options['relation'] = $relation; + } + + return $this; + } + + /** + * 插入记录 + * @access public + * @param array $data 数据 + * @param boolean $replace 是否replace + * @param boolean $getLastInsID 返回自增主键 + * @param string $sequence 自增序列名 + * @return integer|string + */ + public function insert(array $data = [], $replace = false, $getLastInsID = false, $sequence = null) + { + $this->parseOptions(); + + $this->options['data'] = array_merge($this->options['data'], $data); + + return $this->connection->insert($this, $replace, $getLastInsID, $sequence); + } + + /** + * 插入记录并获取自增ID + * @access public + * @param array $data 数据 + * @param boolean $replace 是否replace + * @param string $sequence 自增序列名 + * @return integer|string + */ + public function insertGetId(array $data, $replace = false, $sequence = null) + { + return $this->insert($data, $replace, true, $sequence); + } + + /** + * 批量插入记录 + * @access public + * @param array $dataSet 数据集 + * @param boolean $replace 是否replace + * @param integer $limit 每次写入数据限制 + * @return integer|string + */ + public function insertAll(array $dataSet = [], $replace = false, $limit = null) + { + $this->parseOptions(); + + if (empty($dataSet)) { + $dataSet = $this->options['data']; + } + + if (empty($limit) && !empty($this->options['limit'])) { + $limit = $this->options['limit']; + } + + return $this->connection->insertAll($this, $dataSet, $replace, $limit); + } + + /** + * 通过Select方式插入记录 + * @access public + * @param string $fields 要插入的数据表字段名 + * @param string $table 要插入的数据表名 + * @return integer|string + * @throws PDOException + */ + public function selectInsert($fields, $table) + { + $this->parseOptions(); + + return $this->connection->selectInsert($this, $fields, $table); + } + + /** + * 更新记录 + * @access public + * @param mixed $data 数据 + * @return integer|string + * @throws Exception + * @throws PDOException + */ + public function update(array $data = []) + { + $this->parseOptions(); + + $this->options['data'] = array_merge($this->options['data'], $data); + + return $this->connection->update($this); + } + + /** + * 删除记录 + * @access public + * @param mixed $data 表达式 true 表示强制删除 + * @return int + * @throws Exception + * @throws PDOException + */ + public function delete($data = null) + { + $this->parseOptions(); + + if (!is_null($data) && true !== $data) { + // AR模式分析主键条件 + $this->parsePkWhere($data); + } + + if (!empty($this->options['soft_delete'])) { + // 软删除 + list($field, $condition) = $this->options['soft_delete']; + if ($condition) { + unset($this->options['soft_delete']); + $this->options['data'] = [$field => $condition]; + + return $this->connection->update($this); + } + } + + $this->options['data'] = $data; + + return $this->connection->delete($this); + } + + /** + * 执行查询但只返回PDOStatement对象 + * @access public + * @return \PDOStatement|string + */ + public function getPdo() + { + $this->parseOptions(); + + return $this->connection->pdo($this); + } + + /** + * 使用游标查找记录 + * @access public + * @param array|string|Query|\Closure $data + * @return \Generator + */ + public function cursor($data = null) + { + if ($data instanceof \Closure) { + $data($this); + $data = null; + } + + $this->parseOptions(); + + if (!is_null($data)) { + // 主键条件分析 + $this->parsePkWhere($data); + } + + $this->options['data'] = $data; + + $connection = clone $this->connection; + + return $connection->cursor($this); + } + + /** + * 查找记录 + * @access public + * @param array|string|Query|\Closure $data + * @return Collection|array|\PDOStatement|string + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function select($data = null) + { + if ($data instanceof Query) { + return $data->select(); + } elseif ($data instanceof \Closure) { + $data($this); + $data = null; + } + + $this->parseOptions(); + + if (false === $data) { + // 用于子查询 不查询只返回SQL + $this->options['fetch_sql'] = true; + } elseif (!is_null($data)) { + // 主键条件分析 + $this->parsePkWhere($data); + } + + $this->options['data'] = $data; + + $resultSet = $this->connection->select($this); + + if ($this->options['fetch_sql']) { + return $resultSet; + } + + // 返回结果处理 + if (!empty($this->options['fail']) && count($resultSet) == 0) { + $this->throwNotFound($this->options); + } + + // 数据列表读取后的处理 + if (!empty($this->model)) { + // 生成模型对象 + $resultSet = $this->resultSetToModelCollection($resultSet); + } else { + $this->resultSet($resultSet); + } + + return $resultSet; + } + + /** + * 查询数据转换为模型数据集对象 + * @access protected + * @param array $resultSet 数据集 + * @return ModelCollection + */ + protected function resultSetToModelCollection(array $resultSet) + { + if (!empty($this->options['collection']) && is_string($this->options['collection'])) { + $collection = $this->options['collection']; + } + + if (empty($resultSet)) { + return $this->model->toCollection([], isset($collection) ? $collection : null); + } + + // 检查动态获取器 + if (!empty($this->options['with_attr'])) { + foreach ($this->options['with_attr'] as $name => $val) { + if (strpos($name, '.')) { + list($relation, $field) = explode('.', $name); + + $withRelationAttr[$relation][$field] = $val; + unset($this->options['with_attr'][$name]); + } + } + } + + $withRelationAttr = isset($withRelationAttr) ? $withRelationAttr : []; + + foreach ($resultSet as $key => &$result) { + // 数据转换为模型对象 + $this->resultToModel($result, $this->options, true, $withRelationAttr); + } + + if (!empty($this->options['with'])) { + // 预载入 + $result->eagerlyResultSet($resultSet, $this->options['with'], $withRelationAttr); + } + + if (!empty($this->options['with_join'])) { + // JOIN预载入 + $result->eagerlyResultSet($resultSet, $this->options['with_join'], $withRelationAttr, true); + } + + // 模型数据集转换 + return $result->toCollection($resultSet, isset($collection) ? $collection : null); + } + + /** + * 处理数据集 + * @access public + * @param array $resultSet + * @return void + */ + protected function resultSet(&$resultSet) + { + if (!empty($this->options['json'])) { + foreach ($resultSet as &$result) { + $this->jsonResult($result, $this->options['json'], true); + } + } + + if (!empty($this->options['with_attr'])) { + foreach ($resultSet as &$result) { + $this->getResultAttr($result, $this->options['with_attr']); + } + } + + if (!empty($this->options['collection']) || 'collection' == $this->connection->getConfig('resultset_type')) { + // 返回Collection对象 + $resultSet = new Collection($resultSet); + } + } + + /** + * 查找单条记录 + * @access public + * @param array|string|Query|\Closure $data + * @return array|null|\PDOStatement|string|Model + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function find($data = null) + { + if ($data instanceof Query) { + return $data->find(); + } elseif ($data instanceof \Closure) { + $data($this); + $data = null; + } + + $this->parseOptions(); + + if (!is_null($data)) { + // AR模式分析主键条件 + $this->parsePkWhere($data); + } + + $this->options['data'] = $data; + + $result = $this->connection->find($this); + + if ($this->options['fetch_sql']) { + return $result; + } + + // 数据处理 + if (empty($result)) { + return $this->resultToEmpty(); + } + + if (!empty($this->model)) { + // 返回模型对象 + $this->resultToModel($result, $this->options); + } else { + $this->result($result); + } + + return $result; + } + + /** + * 处理空数据 + * @access protected + * @return array|Model|null + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + protected function resultToEmpty() + { + if (!empty($this->options['allow_empty'])) { + return !empty($this->model) ? $this->model->newInstance([], $this->getModelUpdateCondition($this->options)) : []; + } elseif (!empty($this->options['fail'])) { + $this->throwNotFound($this->options); + } + } + + /** + * 查找单条记录 + * @access public + * @param mixed $data 主键值或者查询条件(闭包) + * @param mixed $with 关联预查询 + * @param bool $cache 是否缓存 + * @param bool $failException 是否抛出异常 + * @return static|null + * @throws exception\DbException + */ + public function get($data, $with = [], $cache = false, $failException = false) + { + if (is_null($data)) { + return; + } + + if (true === $with || is_int($with)) { + $cache = $with; + $with = []; + } + + return $this->parseQuery($data, $with, $cache) + ->failException($failException) + ->find($data); + } + + /** + * 查找单条记录 如果不存在直接抛出异常 + * @access public + * @param mixed $data 主键值或者查询条件(闭包) + * @param mixed $with 关联预查询 + * @param bool $cache 是否缓存 + * @return static|null + * @throws exception\DbException + */ + public function getOrFail($data, $with = [], $cache = false) + { + return $this->get($data, $with, $cache, true); + } + + /** + * 查找所有记录 + * @access public + * @param mixed $data 主键列表或者查询条件(闭包) + * @param array|string $with 关联预查询 + * @param bool $cache 是否缓存 + * @return static[]|false + * @throws exception\DbException + */ + public function all($data = null, $with = [], $cache = false) + { + if (true === $with || is_int($with)) { + $cache = $with; + $with = []; + } + + return $this->parseQuery($data, $with, $cache)->select($data); + } + + /** + * 分析查询表达式 + * @access public + * @param mixed $data 主键列表或者查询条件(闭包) + * @param string $with 关联预查询 + * @param bool $cache 是否缓存 + * @return Query + */ + protected function parseQuery(&$data, $with, $cache) + { + $result = $this->with($with)->cache($cache); + + if ((is_array($data) && key($data) !== 0) || $data instanceof Where) { + $result = $result->where($data); + $data = null; + } elseif ($data instanceof \Closure) { + $data($result); + $data = null; + } elseif ($data instanceof Query) { + $result = $data->with($with)->cache($cache); + $data = null; + } + + return $result; + } + + /** + * 处理数据 + * @access protected + * @param array $result 查询数据 + * @return void + */ + protected function result(&$result) + { + if (!empty($this->options['json'])) { + $this->jsonResult($result, $this->options['json'], true); + } + + if (!empty($this->options['with_attr'])) { + $this->getResultAttr($result, $this->options['with_attr']); + } + } + + /** + * 使用获取器处理数据 + * @access protected + * @param array $result 查询数据 + * @param array $withAttr 字段获取器 + * @return void + */ + protected function getResultAttr(&$result, $withAttr = []) + { + foreach ($withAttr as $name => $closure) { + $name = Loader::parseName($name); + + if (strpos($name, '.')) { + // 支持JSON字段 获取器定义 + list($key, $field) = explode('.', $name); + + if (isset($result[$key])) { + $result[$key][$field] = $closure(isset($result[$key][$field]) ? $result[$key][$field] : null, $result[$key]); + } + } else { + $result[$name] = $closure(isset($result[$name]) ? $result[$name] : null, $result); + } + } + } + + /** + * JSON字段数据转换 + * @access protected + * @param array $result 查询数据 + * @param array $json JSON字段 + * @param bool $assoc 是否转换为数组 + * @param array $withRelationAttr 关联获取器 + * @return void + */ + protected function jsonResult(&$result, $json = [], $assoc = false, $withRelationAttr = []) + { + foreach ($json as $name) { + if (isset($result[$name])) { + $result[$name] = json_decode($result[$name], $assoc); + + if (isset($withRelationAttr[$name])) { + foreach ($withRelationAttr[$name] as $key => $closure) { + $data = get_object_vars($result[$name]); + $result[$name]->$key = $closure(isset($result[$name]->$key) ? $result[$name]->$key : null, $data); + } + } + } + } + } + + /** + * 查询数据转换为模型对象 + * @access protected + * @param array $result 查询数据 + * @param array $options 查询参数 + * @param bool $resultSet 是否为数据集查询 + * @param array $withRelationAttr 关联字段获取器 + * @return void + */ + protected function resultToModel(&$result, $options = [], $resultSet = false, $withRelationAttr = []) + { + // 动态获取器 + if (!empty($options['with_attr']) && empty($withRelationAttr)) { + foreach ($options['with_attr'] as $name => $val) { + if (strpos($name, '.')) { + list($relation, $field) = explode('.', $name); + + $withRelationAttr[$relation][$field] = $val; + unset($options['with_attr'][$name]); + } + } + } + + // JSON 数据处理 + if (!empty($options['json'])) { + $this->jsonResult($result, $options['json'], $options['json_assoc'], $withRelationAttr); + } + + $result = $this->model->newInstance($result, $resultSet ? null : $this->getModelUpdateCondition($options)); + + // 动态获取器 + if (!empty($options['with_attr'])) { + $result->withAttribute($options['with_attr']); + } + + // 输出属性控制 + if (!empty($options['visible'])) { + $result->visible($options['visible'], true); + } elseif (!empty($options['hidden'])) { + $result->hidden($options['hidden'], true); + } + + if (!empty($options['append'])) { + $result->append($options['append'], true); + } + + // 关联查询 + if (!empty($options['relation'])) { + $result->relationQuery($options['relation'], $withRelationAttr); + } + + // 预载入查询 + if (!$resultSet && !empty($options['with'])) { + $result->eagerlyResult($result, $options['with'], $withRelationAttr); + } + + // JOIN预载入查询 + if (!$resultSet && !empty($options['with_join'])) { + $result->eagerlyResult($result, $options['with_join'], $withRelationAttr, true); + } + + // 关联统计 + if (!empty($options['with_count'])) { + foreach ($options['with_count'] as $val) { + $result->relationCount($result, $val[0], $val[1], $val[2]); + } + } + } + + /** + * 获取模型的更新条件 + * @access protected + * @param array $options 查询参数 + */ + protected function getModelUpdateCondition(array $options) + { + return isset($options['where']['AND']) ? $options['where']['AND'] : null; + } + + /** + * 查询失败 抛出异常 + * @access protected + * @param array $options 查询参数 + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + protected function throwNotFound($options = []) + { + if (!empty($this->model)) { + $class = get_class($this->model); + throw new ModelNotFoundException('model data Not Found:' . $class, $class, $options); + } + $table = is_array($options['table']) ? key($options['table']) : $options['table']; + throw new DataNotFoundException('table data not Found:' . $table, $table, $options); + } + + /** + * 查找多条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return array|\PDOStatement|string|Model + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function selectOrFail($data = null) + { + return $this->failException(true)->select($data); + } + + /** + * 查找单条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return array|\PDOStatement|string|Model + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function findOrFail($data = null) + { + return $this->failException(true)->find($data); + } + + /** + * 查找单条记录 不存在则返回空模型 + * @access public + * @param array|string|Query|\Closure $data + * @return array|\PDOStatement|string|Model + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function findOrEmpty($data = null) + { + return $this->allowEmpty(true)->find($data); + } + + /** + * 分批数据返回处理 + * @access public + * @param integer $count 每次处理的数据数量 + * @param callable $callback 处理回调方法 + * @param string|array $column 分批处理的字段名 + * @param string $order 字段排序 + * @return boolean + * @throws DbException + */ + public function chunk($count, $callback, $column = null, $order = 'asc') + { + $options = $this->getOptions(); + $column = $column ?: $this->getPk($options); + + if (isset($options['order'])) { + if (Container::get('app')->isDebug()) { + throw new DbException('chunk not support call order'); + } + unset($options['order']); + } + + $bind = $this->bind; + + if (is_array($column)) { + $times = 1; + $query = $this->options($options)->page($times, $count); + } else { + $query = $this->options($options)->limit($count); + + if (strpos($column, '.')) { + list($alias, $key) = explode('.', $column); + } else { + $key = $column; + } + } + + $resultSet = $query->order($column, $order)->select(); + + while (count($resultSet) > 0) { + if ($resultSet instanceof Collection) { + $resultSet = $resultSet->all(); + } + + if (false === call_user_func($callback, $resultSet)) { + return false; + } + + if (isset($times)) { + $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; + } + + /** + * 获取绑定的参数 并清空 + * @access public + * @param bool $clear + * @return array + */ + public function getBind($clear = true) + { + $bind = $this->bind; + if ($clear) { + $this->bind = []; + } + + return $bind; + } + + /** + * 创建子查询SQL + * @access public + * @param bool $sub + * @return string + * @throws DbException + */ + public function buildSql($sub = true) + { + return $sub ? '( ' . $this->select(false) . ' )' : $this->select(false); + } + + /** + * 视图查询处理 + * @access protected + * @param array $options 查询参数 + * @return void + */ + protected function parseView(&$options) + { + if (!isset($options['map'])) { + return; + } + + foreach (['AND', 'OR'] as $logic) { + if (isset($options['where'][$logic])) { + foreach ($options['where'][$logic] as $key => $val) { + if (array_key_exists($key, $options['map'])) { + array_shift($val); + array_unshift($val, $options['map'][$key]); + $options['where'][$logic][$options['map'][$key]] = $val; + unset($options['where'][$logic][$key]); + } + } + } + } + + if (isset($options['order'])) { + // 视图查询排序处理 + if (is_string($options['order'])) { + $options['order'] = explode(',', $options['order']); + } + foreach ($options['order'] as $key => $val) { + if (is_numeric($key) && is_string($val)) { + if (strpos($val, ' ')) { + list($field, $sort) = explode(' ', $val); + if (array_key_exists($field, $options['map'])) { + $options['order'][$options['map'][$field]] = $sort; + unset($options['order'][$key]); + } + } elseif (array_key_exists($val, $options['map'])) { + $options['order'][$options['map'][$val]] = 'asc'; + unset($options['order'][$key]); + } + } elseif (array_key_exists($key, $options['map'])) { + $options['order'][$options['map'][$key]] = $val; + unset($options['order'][$key]); + } + } + } + } + + /** + * 把主键值转换为查询条件 支持复合主键 + * @access public + * @param array|string $data 主键数据 + * @return void + * @throws Exception + */ + public function parsePkWhere($data) + { + $pk = $this->getPk($this->options); + // 获取当前数据表 + $table = is_array($this->options['table']) ? key($this->options['table']) : $this->options['table']; + + if (!empty($this->options['alias'][$table])) { + $alias = $this->options['alias'][$table]; + } + + if (is_string($pk)) { + $key = isset($alias) ? $alias . '.' . $pk : $pk; + // 根据主键查询 + if (is_array($data)) { + $where[$pk] = isset($data[$pk]) ? [$key, '=', $data[$pk]] : [$key, 'in', $data]; + } else { + $where[$pk] = strpos($data, ',') ? [$key, 'IN', $data] : [$key, '=', $data]; + } + } elseif (is_array($pk) && is_array($data) && !empty($data)) { + // 根据复合主键查询 + foreach ($pk as $key) { + if (isset($data[$key])) { + $attr = isset($alias) ? $alias . '.' . $key : $key; + $where[$key] = [$attr, '=', $data[$key]]; + } else { + throw new Exception('miss complex primary data'); + } + } + } + + if (!empty($where)) { + if (isset($this->options['where']['AND'])) { + $this->options['where']['AND'] = array_merge($this->options['where']['AND'], $where); + } else { + $this->options['where']['AND'] = $where; + } + } + + return; + } + + /** + * 分析表达式(可用于查询或者写入操作) + * @access protected + * @return array + */ + protected function parseOptions() + { + $options = $this->getOptions(); + + // 获取数据表 + if (empty($options['table'])) { + $options['table'] = $this->getTable(); + } + + if (!isset($options['where'])) { + $options['where'] = []; + } elseif (isset($options['view'])) { + // 视图查询条件处理 + $this->parseView($options); + } + + if (!isset($options['field'])) { + $options['field'] = '*'; + } + + foreach (['data', 'order', 'join', 'union'] as $name) { + if (!isset($options[$name])) { + $options[$name] = []; + } + } + + if (!isset($options['strict'])) { + $options['strict'] = $this->getConfig('fields_strict'); + } + + foreach (['master', 'lock', 'fetch_pdo', 'fetch_sql', 'distinct'] as $name) { + if (!isset($options[$name])) { + $options[$name] = false; + } + } + + if (isset(static::$readMaster['*']) || (is_string($options['table']) && isset(static::$readMaster[$options['table']]))) { + $options['master'] = true; + } + + foreach (['group', 'having', 'limit', 'force', 'comment'] as $name) { + if (!isset($options[$name])) { + $options[$name] = ''; + } + } + + if (isset($options['page'])) { + // 根据页数计算limit + list($page, $listRows) = $options['page']; + $page = $page > 0 ? $page : 1; + $listRows = $listRows > 0 ? $listRows : (is_numeric($options['limit']) ? $options['limit'] : 20); + $offset = $listRows * ($page - 1); + $options['limit'] = $offset . ',' . $listRows; + } + + $this->options = $options; + + return $options; + } + + /** + * 注册回调方法 + * @access public + * @param string $event 事件名 + * @param callable $callback 回调方法 + * @return void + */ + public static function event($event, $callback) + { + self::$event[$event] = $callback; + } + + /** + * 触发事件 + * @access public + * @param string $event 事件名 + * @return bool + */ + public function trigger($event) + { + $result = false; + + if (isset(self::$event[$event])) { + $result = Container::getInstance()->invoke(self::$event[$event], [$this]); + } + + return $result; + } + +} diff --git a/thinkphp/library/think/db/Where.php b/thinkphp/library/think/db/Where.php new file mode 100644 index 0000000..9132e54 --- /dev/null +++ b/thinkphp/library/think/db/Where.php @@ -0,0 +1,178 @@ + +// +---------------------------------------------------------------------- + +namespace think\db; + +use ArrayAccess; + +class Where implements ArrayAccess +{ + /** + * 查询表达式 + * @var array + */ + protected $where = []; + + /** + * 是否需要增加括号 + * @var bool + */ + protected $enclose = false; + + /** + * 创建一个查询表达式 + * + * @param array $where 查询条件数组 + * @param bool $enclose 是否增加括号 + */ + public function __construct(array $where = [], $enclose = false) + { + $this->where = $where; + $this->enclose = $enclose; + } + + /** + * 设置是否添加括号 + * @access public + * @param bool $enclose + * @return $this + */ + public function enclose($enclose = true) + { + $this->enclose = $enclose; + return $this; + } + + /** + * 解析为Query对象可识别的查询条件数组 + * @access public + * @return array + */ + public function parse() + { + $where = []; + + foreach ($this->where as $key => $val) { + if ($val instanceof Expression) { + $where[] = [$key, 'exp', $val]; + } elseif (is_null($val)) { + $where[] = [$key, 'NULL', '']; + } elseif (is_array($val)) { + $where[] = $this->parseItem($key, $val); + } else { + $where[] = [$key, '=', $val]; + } + } + + return $this->enclose ? [$where] : $where; + } + + /** + * 分析查询表达式 + * @access protected + * @param string $field 查询字段 + * @param array $where 查询条件 + * @return array + */ + protected function parseItem($field, $where = []) + { + $op = $where[0]; + $condition = isset($where[1]) ? $where[1] : null; + + if (is_array($op)) { + // 同一字段多条件查询 + array_unshift($where, $field); + } elseif (is_null($condition)) { + if (in_array(strtoupper($op), ['NULL', 'NOTNULL', 'NOT NULL'], true)) { + // null查询 + $where = [$field, $op, '']; + } elseif (in_array($op, ['=', 'eq', 'EQ', null], true)) { + $where = [$field, 'NULL', '']; + } elseif (in_array($op, ['<>', 'neq', 'NEQ'], true)) { + $where = [$field, 'NOTNULL', '']; + } else { + // 字段相等查询 + $where = [$field, '=', $op]; + } + } else { + $where = [$field, $op, $condition]; + } + + return $where; + } + + /** + * 修改器 设置数据对象的值 + * @access public + * @param string $name 名称 + * @param mixed $value 值 + * @return void + */ + public function __set($name, $value) + { + $this->where[$name] = $value; + } + + /** + * 获取器 获取数据对象的值 + * @access public + * @param string $name 名称 + * @return mixed + */ + public function __get($name) + { + return isset($this->where[$name]) ? $this->where[$name] : null; + } + + /** + * 检测数据对象的值 + * @access public + * @param string $name 名称 + * @return boolean + */ + public function __isset($name) + { + return isset($this->where[$name]); + } + + /** + * 销毁数据对象的值 + * @access public + * @param string $name 名称 + * @return void + */ + public function __unset($name) + { + unset($this->where[$name]); + } + + // ArrayAccess + public function offsetSet($name, $value) + { + $this->__set($name, $value); + } + + public function offsetExists($name) + { + return $this->__isset($name); + } + + public function offsetUnset($name) + { + $this->__unset($name); + } + + public function offsetGet($name) + { + return $this->__get($name); + } + +} diff --git a/thinkphp/library/think/db/builder/Mysql.php b/thinkphp/library/think/db/builder/Mysql.php new file mode 100644 index 0000000..f7384b3 --- /dev/null +++ b/thinkphp/library/think/db/builder/Mysql.php @@ -0,0 +1,184 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\builder; + +use think\db\Builder; +use think\db\Expression; +use think\db\Query; +use think\Exception; + +/** + * mysql数据库驱动 + */ +class Mysql extends Builder +{ + // 查询表达式解析 + protected $parser = [ + 'parseCompare' => ['=', '<>', '>', '>=', '<', '<='], + 'parseLike' => ['LIKE', 'NOT LIKE'], + 'parseBetween' => ['NOT BETWEEN', 'BETWEEN'], + 'parseIn' => ['NOT IN', 'IN'], + 'parseExp' => ['EXP'], + 'parseRegexp' => ['REGEXP', 'NOT REGEXP'], + 'parseNull' => ['NOT NULL', 'NULL'], + 'parseBetweenTime' => ['BETWEEN TIME', 'NOT BETWEEN TIME'], + 'parseTime' => ['< TIME', '> TIME', '<= TIME', '>= TIME'], + 'parseExists' => ['NOT EXISTS', 'EXISTS'], + 'parseColumn' => ['COLUMN'], + ]; + + 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 Query $query 查询对象 + * @param array $dataSet 数据集 + * @param bool $replace 是否replace + * @return string + */ + public function insertAll(Query $query, $dataSet, $replace = false) + { + $options = $query->getOptions(); + + // 获取合法的字段 + if ('*' == $options['field']) { + $allowFields = $this->connection->getTableFields($options['table']); + } else { + $allowFields = $options['field']; + } + + // 获取绑定信息 + $bind = $this->connection->getFieldsBind($options['table']); + + foreach ($dataSet as $k => $data) { + $data = $this->parseData($query, $data, $allowFields, $bind); + + $values[] = '( ' . implode(',', array_values($data)) . ' )'; + + if (!isset($insertFields)) { + $insertFields = array_keys($data); + } + } + + $fields = []; + foreach ($insertFields as $field) { + $fields[] = $this->parseKey($query, $field); + } + + return str_replace( + ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], + [ + $replace ? 'REPLACE' : 'INSERT', + $this->parseTable($query, $options['table']), + implode(' , ', $fields), + implode(' , ', $values), + $this->parseComment($query, $options['comment']), + ], + $this->insertAllSql); + } + + /** + * 正则查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @return string + */ + protected function parseRegexp(Query $query, $key, $exp, $value, $field) + { + if ($value instanceof Expression) { + $value = $value->getValue(); + } + + return $key . ' ' . $exp . ' ' . $value; + } + + /** + * 字段和表名处理 + * @access public + * @param Query $query 查询对象 + * @param mixed $key 字段名 + * @param bool $strict 严格检测 + * @return string + */ + public function parseKey(Query $query, $key, $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, 2); + + return $this->parseKey($query, $field, true) . '->>\'$' . (strpos($name, '[') === 0 ? '' : '.') . str_replace('->>', '.', $name) . '\''; + } + elseif (strpos($key, '->') && false === strpos($key, '(')) { + // JSON字段支持 + list($field, $name) = explode('->', $key, 2); + + return 'json_extract(' . $this->parseKey($query, $field, true) . ', \'$' . (strpos($name, '[') === 0 ? '' : '.') . str_replace('->', '.', $name) . '\')'; + } elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) { + list($table, $key) = explode('.', $key, 2); + + $alias = $query->getOptions('alias'); + + if ('__TABLE__' == $table) { + $table = $query->getOptions('table'); + $table = is_array($table) ? array_shift($table) : $table; + } + + if (isset($alias[$table])) { + $table = $alias[$table]; + } + } + + if ($strict && !preg_match('/^[\w\.\*]+$/', $key)) { + throw new Exception('not support data:' . $key); + } + + if ('*' != $key && !preg_match('/[,\'\"\*\(\)`.\s]/', $key)) { + $key = '`' . $key . '`'; + } + + if (isset($table)) { + if (strpos($table, '.')) { + $table = str_replace('.', '`.`', $table); + } + + $key = '`' . $table . '`.' . $key; + } + + return $key; + } + + /** + * 随机排序 + * @access protected + * @param Query $query 查询对象 + * @return string + */ + protected function parseRand(Query $query) + { + return 'rand()'; + } + +} diff --git a/thinkphp/library/think/db/builder/Pgsql.php b/thinkphp/library/think/db/builder/Pgsql.php new file mode 100644 index 0000000..742c7db --- /dev/null +++ b/thinkphp/library/think/db/builder/Pgsql.php @@ -0,0 +1,104 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\builder; + +use think\db\Builder; +use think\db\Query; + +/** + * Pgsql数据库驱动 + */ +class Pgsql extends Builder +{ + + protected $insertSql = 'INSERT INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%'; + protected $insertAllSql = 'INSERT INTO %TABLE% (%FIELD%) %DATA% %COMMENT%'; + + /** + * limit分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $limit + * @return string + */ + public function parseLimit(Query $query, $limit) + { + $limitStr = ''; + + if (!empty($limit)) { + $limit = explode(',', $limit); + if (count($limit) > 1) { + $limitStr .= ' LIMIT ' . $limit[1] . ' OFFSET ' . $limit[0] . ' '; + } else { + $limitStr .= ' LIMIT ' . $limit[0] . ' '; + } + } + + return $limitStr; + } + + /** + * 字段和表名处理 + * @access public + * @param Query $query 查询对象 + * @param mixed $key 字段名 + * @param bool $strict 严格检测 + * @return string + */ + public function parseKey(Query $query, $key, $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 = $field . '->>\'' . $name . '\''; + } elseif (strpos($key, '.')) { + list($table, $key) = explode('.', $key, 2); + + $alias = $query->getOptions('alias'); + + if ('__TABLE__' == $table) { + $table = $query->getOptions('table'); + $table = is_array($table) ? array_shift($table) : $table; + } + + if (isset($alias[$table])) { + $table = $alias[$table]; + } + } + + if (isset($table)) { + $key = $table . '.' . $key; + } + + return $key; + } + + /** + * 随机排序 + * @access protected + * @param Query $query 查询对象 + * @return string + */ + protected function parseRand(Query $query) + { + return 'RANDOM()'; + } + +} diff --git a/thinkphp/library/think/db/builder/Sqlite.php b/thinkphp/library/think/db/builder/Sqlite.php new file mode 100644 index 0000000..2b887ca --- /dev/null +++ b/thinkphp/library/think/db/builder/Sqlite.php @@ -0,0 +1,96 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\builder; + +use think\db\Builder; +use think\db\Query; + +/** + * Sqlite数据库驱动 + */ +class Sqlite extends Builder +{ + + /** + * limit + * @access public + * @param Query $query 查询对象 + * @param mixed $limit + * @return string + */ + public function parseLimit(Query $query, $limit) + { + $limitStr = ''; + + if (!empty($limit)) { + $limit = explode(',', $limit); + if (count($limit) > 1) { + $limitStr .= ' LIMIT ' . $limit[1] . ' OFFSET ' . $limit[0] . ' '; + } else { + $limitStr .= ' LIMIT ' . $limit[0] . ' '; + } + } + + return $limitStr; + } + + /** + * 随机排序 + * @access protected + * @param Query $query 查询对象 + * @return string + */ + protected function parseRand(Query $query) + { + return 'RANDOM()'; + } + + /** + * 字段和表名处理 + * @access public + * @param Query $query 查询对象 + * @param mixed $key 字段名 + * @param bool $strict 严格检测 + * @return string + */ + public function parseKey(Query $query, $key, $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); + + $alias = $query->getOptions('alias'); + + if ('__TABLE__' == $table) { + $table = $query->getOptions('table'); + $table = is_array($table) ? array_shift($table) : $table; + } + + if (isset($alias[$table])) { + $table = $alias[$table]; + } + } + + if (isset($table)) { + $key = $table . '.' . $key; + } + + return $key; + } +} diff --git a/thinkphp/library/think/db/builder/Sqlsrv.php b/thinkphp/library/think/db/builder/Sqlsrv.php new file mode 100644 index 0000000..ef27aaf --- /dev/null +++ b/thinkphp/library/think/db/builder/Sqlsrv.php @@ -0,0 +1,159 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\builder; + +use think\db\Builder; +use think\db\Expression; +use think\db\Query; +use think\Exception; + +/** + * Sqlsrv数据库驱动 + */ +class Sqlsrv extends Builder +{ + protected $selectSql = 'SELECT T1.* FROM (SELECT thinkphp.*, ROW_NUMBER() OVER (%ORDER%) AS ROW_NUMBER FROM (SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%) AS thinkphp) AS T1 %LIMIT%%COMMENT%'; + 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分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $order + * @return string + */ + protected function parseOrder(Query $query, $order) + { + if (empty($order)) { + return ' ORDER BY rand()'; + } + + foreach ($order as $key => $val) { + if ($val instanceof Expression) { + $array[] = $val->getValue(); + } elseif ('[rand]' == $val) { + $array[] = $this->parseRand($query); + } else { + if (is_numeric($key)) { + list($key, $sort) = explode(' ', strpos($val, ' ') ? $val : $val . ' '); + } else { + $sort = $val; + } + + if (preg_match('/^[\w\.]+$/', $key)) { + $sort = strtoupper($sort); + $sort = in_array($sort, ['ASC', 'DESC'], true) ? ' ' . $sort : ''; + $array[] = $this->parseKey($query, $key, true) . $sort; + } else { + throw new Exception('order express error:' . $key); + } + } + } + + return empty($array) ? '' : ' ORDER BY ' . implode(',', $array); + } + + /** + * 随机排序 + * @access protected + * @param Query $query 查询对象 + * @return string + */ + protected function parseRand(Query $query) + { + return 'rand()'; + } + + /** + * 字段和表名处理 + * @access public + * @param Query $query 查询对象 + * @param mixed $key 字段名 + * @param bool $strict 严格检测 + * @return string + */ + public function parseKey(Query $query, $key, $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); + + $alias = $query->getOptions('alias'); + + if ('__TABLE__' == $table) { + $table = $query->getOptions('table'); + $table = is_array($table) ? array_shift($table) : $table; + } + + if (isset($alias[$table])) { + $table = $alias[$table]; + } + } + + if ($strict && !preg_match('/^[\w\.\*]+$/', $key)) { + throw new Exception('not support data:' . $key); + } + + if ('*' != $key && !preg_match('/[,\'\"\*\(\)\[.\s]/', $key)) { + $key = '[' . $key . ']'; + } + + if (isset($table)) { + $key = '[' . $table . '].' . $key; + } + + return $key; + } + + /** + * limit + * @access protected + * @param Query $query 查询对象 + * @param mixed $limit + * @return string + */ + protected function parseLimit(Query $query, $limit) + { + if (empty($limit)) { + return ''; + } + + $limit = explode(',', $limit); + + if (count($limit) > 1) { + $limitStr = '(T1.ROW_NUMBER BETWEEN ' . $limit[0] . ' + 1 AND ' . $limit[0] . ' + ' . $limit[1] . ')'; + } else { + $limitStr = '(T1.ROW_NUMBER BETWEEN 1 AND ' . $limit[0] . ")"; + } + + return 'WHERE ' . $limitStr; + } + + public function selectInsert(Query $query, $fields, $table) + { + $this->selectSql = $this->selectInsertSql; + + return parent::selectInsert($query, $fields, $table); + } + +} diff --git a/thinkphp/library/think/db/connector/Mysql.php b/thinkphp/library/think/db/connector/Mysql.php new file mode 100644 index 0000000..cfd2ac7 --- /dev/null +++ b/thinkphp/library/think/db/connector/Mysql.php @@ -0,0 +1,229 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\Connection; +use think\db\Query; + +/** + * mysql数据库驱动 + */ +class Mysql extends Connection +{ + + protected $builder = '\\think\\db\\builder\\Mysql'; + + /** + * 初始化 + * @access protected + * @return void + */ + protected function initialize() + { + // Point类型支持 + Query::extend('point', function ($query, $field, $value = null, $fun = 'GeomFromText', $type = 'POINT') { + if (!is_null($value)) { + $query->data($field, ['point', $value, $fun, $type]); + } else { + if (is_string($field)) { + $field = explode(',', $field); + } + $query->setOption('point', $field); + } + + return $query; + }); + } + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn($config) + { + 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']; + } + + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields($tableName) + { + list($tableName) = explode(' ', $tableName); + + if (false === strpos($tableName, '`')) { + if (strpos($tableName, '.')) { + $tableName = str_replace('.', '`.`', $tableName); + } + $tableName = '`' . $tableName . '`'; + } + + $sql = 'SHOW COLUMNS FROM ' . $tableName; + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + if ($result) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + $info[$val['field']] = [ + 'name' => $val['field'], + 'type' => $val['type'], + 'notnull' => 'NO' == $val['null'], + 'default' => $val['default'], + 'primary' => strtolower($val['key']) == 'pri', + 'autoinc' => strtolower($val['extra']) == 'auto_increment', + ]; + } + } + + return $this->fieldCase($info); + } + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables($dbName = '') + { + $sql = !empty($dbName) ? 'SHOW TABLES FROM ' . $dbName : 'SHOW TABLES '; + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + + return $info; + } + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + protected function getExplain($sql) + { + $pdo = $this->linkID->prepare("EXPLAIN " . $this->queryStr); + + foreach ($this->bind as $key => $val) { + // 占位符 + $param = is_int($key) ? $key + 1 : ':' . $key; + + if (is_array($val)) { + if (PDO::PARAM_INT == $val[1] && '' === $val[0]) { + $val[0] = 0; + } elseif (self::PARAM_FLOAT == $val[1]) { + $val[0] = is_string($val[0]) ? (float) $val[0] : $val[0]; + $val[1] = PDO::PARAM_STR; + } + + $result = $pdo->bindValue($param, $val[0], $val[1]); + } else { + $result = $pdo->bindValue($param, $val); + } + } + + $pdo->execute(); + $result = $pdo->fetch(PDO::FETCH_ASSOC); + $result = array_change_key_case($result); + + if (isset($result['extra'])) { + if (strpos($result['extra'], 'filesort') || strpos($result['extra'], 'temporary')) { + $this->log('SQL:' . $this->queryStr . '[' . $result['extra'] . ']', 'warn'); + } + } + + return $result; + } + + protected function supportSavepoint() + { + return true; + } + + /** + * 启动XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function startTransXa($xid) + { + $this->initConnect(true); + if (!$this->linkID) { + return false; + } + + $this->linkID->exec("XA START '$xid'"); + } + + /** + * 预编译XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function prepareXa($xid) + { + $this->initConnect(true); + $this->linkID->exec("XA END '$xid'"); + $this->linkID->exec("XA PREPARE '$xid'"); + } + + /** + * 提交XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function commitXa($xid) + { + $this->initConnect(true); + $this->linkID->exec("XA COMMIT '$xid'"); + } + + /** + * 回滚XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function rollbackXa($xid) + { + $this->initConnect(true); + $this->linkID->exec("XA ROLLBACK '$xid'"); + } +} diff --git a/thinkphp/library/think/db/connector/Pgsql.php b/thinkphp/library/think/db/connector/Pgsql.php new file mode 100644 index 0000000..ee9fca0 --- /dev/null +++ b/thinkphp/library/think/db/connector/Pgsql.php @@ -0,0 +1,116 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\Connection; + +/** + * Pgsql数据库驱动 + */ +class Pgsql extends Connection +{ + protected $builder = '\\think\\db\\builder\\Pgsql'; + + // PDO连接参数 + protected $params = [ + PDO::ATTR_CASE => PDO::CASE_NATURAL, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, + PDO::ATTR_STRINGIFY_FETCHES => false, + ]; + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn($config) + { + $dsn = 'pgsql:dbname=' . $config['database'] . ';host=' . $config['hostname']; + + if (!empty($config['hostport'])) { + $dsn .= ';port=' . $config['hostport']; + } + + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields($tableName) + { + list($tableName) = explode(' ', $tableName); + $sql = 'select fields_name as "field",fields_type as "type",fields_not_null as "null",fields_key_name as "key",fields_default as "default",fields_default as "extra" from table_msg(\'' . $tableName . '\');'; + + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + if ($result) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + $info[$val['field']] = [ + 'name' => $val['field'], + 'type' => $val['type'], + 'notnull' => (bool) ('' !== $val['null']), + 'default' => $val['default'], + 'primary' => !empty($val['key']), + 'autoinc' => (0 === strpos($val['extra'], 'nextval(')), + ]; + } + } + + return $this->fieldCase($info); + } + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables($dbName = '') + { + $sql = "select tablename as Tables_in_test from pg_tables where schemaname ='public'"; + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + + return $info; + } + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + protected function getExplain($sql) + { + return []; + } + + protected function supportSavepoint() + { + return true; + } +} diff --git a/thinkphp/library/think/db/connector/Sqlite.php b/thinkphp/library/think/db/connector/Sqlite.php new file mode 100644 index 0000000..5b9b3fa --- /dev/null +++ b/thinkphp/library/think/db/connector/Sqlite.php @@ -0,0 +1,108 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\Connection; + +/** + * Sqlite数据库驱动 + */ +class Sqlite extends Connection +{ + + protected $builder = '\\think\\db\\builder\\Sqlite'; + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn($config) + { + $dsn = 'sqlite:' . $config['database']; + + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields($tableName) + { + list($tableName) = explode(' ', $tableName); + $sql = 'PRAGMA table_info( ' . $tableName . ' )'; + + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + if ($result) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + $info[$val['name']] = [ + 'name' => $val['name'], + 'type' => $val['type'], + 'notnull' => 1 === $val['notnull'], + 'default' => $val['dflt_value'], + 'primary' => '1' == $val['pk'], + 'autoinc' => '1' == $val['pk'], + ]; + } + } + + return $this->fieldCase($info); + } + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables($dbName = '') + { + $sql = "SELECT name FROM sqlite_master WHERE type='table' " + . "UNION ALL SELECT name FROM sqlite_temp_master " + . "WHERE type='table' ORDER BY name"; + + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + + return $info; + } + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + protected function getExplain($sql) + { + return []; + } + + protected function supportSavepoint() + { + return true; + } +} diff --git a/thinkphp/library/think/db/connector/Sqlsrv.php b/thinkphp/library/think/db/connector/Sqlsrv.php new file mode 100644 index 0000000..123affb --- /dev/null +++ b/thinkphp/library/think/db/connector/Sqlsrv.php @@ -0,0 +1,235 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\Connection; +use think\db\Query; + +/** + * Sqlsrv数据库驱动 + */ +class Sqlsrv extends Connection +{ + // PDO连接参数 + protected $params = [ + PDO::ATTR_CASE => PDO::CASE_NATURAL, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, + PDO::ATTR_STRINGIFY_FETCHES => false, + ]; + + protected $builder = '\\think\\db\\builder\\Sqlsrv'; + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn($config) + { + $dsn = 'sqlsrv:Database=' . $config['database'] . ';Server=' . $config['hostname']; + + if (!empty($config['hostport'])) { + $dsn .= ',' . $config['hostport']; + } + + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields($tableName) + { + list($tableName) = explode(' ', $tableName); + $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 + AND t.table_schema = c.table_schema + AND t.table_name = c.table_name + WHERE t.table_name = '$tableName'"; + + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + if ($result) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + $info[$val['column_name']] = [ + 'name' => $val['column_name'], + 'type' => $val['data_type'], + 'notnull' => (bool) ('' === $val['is_nullable']), // not null is empty, null is yes + 'default' => $val['column_default'], + 'primary' => false, + 'autoinc' => false, + ]; + } + } + + $sql = "SELECT column_name FROM information_schema.key_column_usage WHERE table_name='$tableName'"; + + // 调试开始 + $this->debug(true); + + $pdo = $this->linkID->query($sql); + + // 调试结束 + $this->debug(false, $sql); + + $result = $pdo->fetch(PDO::FETCH_ASSOC); + + if ($result) { + $info[$result['column_name']]['primary'] = true; + } + + return $this->fieldCase($info); + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables($dbName = '') + { + $sql = "SELECT TABLE_NAME + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_TYPE = 'BASE TABLE' + "; + + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + + return $info; + } + + /** + * 得到某个列的数组 + * @access public + * @param Query $query 查询对象 + * @param string $field 字段名 多个字段用逗号分隔 + * @param string $key 索引 + * @return array + */ + public function column(Query $query, $field, $key = '') + { + $options = $query->getOptions(); + + if (empty($options['fetch_sql']) && !empty($options['cache'])) { + // 判断查询缓存 + $cache = $options['cache']; + + $guid = is_string($cache['key']) ? $cache['key'] : $this->getCacheKey($query, $field); + + $result = Container::get('cache')->get($guid); + + if (false !== $result) { + return $result; + } + } + + if (isset($options['field'])) { + $query->removeOption('field'); + } + + if (is_null($field)) { + $field = '*'; + } elseif ($key && '*' != $field) { + $field = $key . ',' . $field; + } + + if (is_string($field)) { + $field = array_map('trim', explode(',', $field)); + } + + $query->setOption('field', $field); + + // 生成查询SQL + $sql = $this->builder->select($query); + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + // 执行查询操作 + $pdo = $this->query($sql, $bind, $options['master'], true); + + if (1 == $pdo->columnCount()) { + $result = $pdo->fetchAll(PDO::FETCH_COLUMN); + } else { + $resultSet = $pdo->fetchAll(PDO::FETCH_ASSOC); + + if ('*' == $field && $key) { + $result = array_column($resultSet, null, $key); + } elseif ($resultSet) { + $fields = array_keys($resultSet[0]); + $count = count($fields); + $key1 = array_shift($fields); + $key2 = $fields ? array_shift($fields) : ''; + $key = $key ?: $key1; + + if (strpos($key, '.')) { + list($alias, $key) = explode('.', $key); + } + + if (3 == $count) { + $column = $key2; + } elseif ($count < 3) { + $column = $key1; + } else { + $column = null; + } + + $result = array_column($resultSet, $column, $key); + } else { + $result = []; + } + } + + if (isset($cache) && isset($guid)) { + // 缓存数据 + $this->cacheData($guid, $result, $cache); + } + + return $result; + } + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + protected function getExplain($sql) + { + return []; + } +} diff --git a/thinkphp/library/think/db/connector/pgsql.sql b/thinkphp/library/think/db/connector/pgsql.sql new file mode 100644 index 0000000..5a4442d --- /dev/null +++ b/thinkphp/library/think/db/connector/pgsql.sql @@ -0,0 +1,153 @@ +CREATE OR REPLACE FUNCTION pgsql_type(a_type varchar) RETURNS varchar AS +$BODY$ +DECLARE + v_type varchar; +BEGIN + IF a_type='int8' THEN + v_type:='bigint'; + ELSIF a_type='int4' THEN + v_type:='integer'; + ELSIF a_type='int2' THEN + v_type:='smallint'; + ELSIF a_type='bpchar' THEN + v_type:='char'; + ELSE + v_type:=a_type; + END IF; + RETURN v_type; +END; +$BODY$ +LANGUAGE PLPGSQL; + +CREATE TYPE "public"."tablestruct" AS ( + "fields_key_name" varchar(100), + "fields_name" VARCHAR(200), + "fields_type" VARCHAR(20), + "fields_length" BIGINT, + "fields_not_null" VARCHAR(10), + "fields_default" VARCHAR(500), + "fields_comment" VARCHAR(1000) +); + +CREATE OR REPLACE FUNCTION "public"."table_msg" (a_schema_name varchar, a_table_name varchar) RETURNS SETOF "public"."tablestruct" AS +$body$ +DECLARE + v_ret tablestruct; + v_oid oid; + v_sql varchar; + v_rec RECORD; + v_key varchar; + v_conkey smallint[]; + v_pk varchar[]; + v_len smallint; + v_pos smallint := 1; +BEGIN + SELECT + pg_class.oid INTO v_oid + FROM + pg_class + INNER JOIN pg_namespace ON (pg_class.relnamespace = pg_namespace.oid AND lower(pg_namespace.nspname) = a_schema_name) + WHERE + pg_class.relname=a_table_name; + IF NOT FOUND THEN + RETURN; + END IF; + + SELECT + pg_constraint.conkey INTO v_conkey + FROM + pg_constraint + INNER JOIN pg_class ON pg_constraint.conrelid = pg_class.oid + INNER JOIN pg_attribute ON pg_attribute.attrelid = pg_class.oid + INNER JOIN pg_type ON pg_type.oid = pg_attribute.atttypid + WHERE + pg_class.relname = a_table_name + AND pg_constraint.contype = 'p'; + + v_len := array_length(v_conkey,1) + 1; + WHILE v_pos < v_len LOOP + SELECT + pg_attribute.attname INTO v_key + FROM pg_constraint + INNER JOIN pg_class ON pg_constraint.conrelid = pg_class.oid + INNER JOIN pg_attribute ON pg_attribute.attrelid = pg_class.oid AND pg_attribute.attnum = pg_constraint.conkey [ v_conkey[v_pos] ] + INNER JOIN pg_type ON pg_type.oid = pg_attribute.atttypid + WHERE pg_class.relname = a_table_name AND pg_constraint.contype = 'p'; + v_pk := array_append(v_pk,v_key); + + v_pos := v_pos + 1; + END LOOP; + + v_sql=' + SELECT + pg_attribute.attname AS fields_name, + pg_attribute.attnum AS fields_index, + pgsql_type(pg_type.typname::varchar) AS fields_type, + pg_attribute.atttypmod-4 as fields_length, + CASE WHEN pg_attribute.attnotnull THEN ''not null'' + ELSE '''' + END AS fields_not_null, + pg_attrdef.adsrc AS fields_default, + pg_description.description AS fields_comment + FROM + pg_attribute + INNER JOIN pg_class ON pg_attribute.attrelid = pg_class.oid + INNER JOIN pg_type ON pg_attribute.atttypid = pg_type.oid + LEFT OUTER JOIN pg_attrdef ON pg_attrdef.adrelid = pg_class.oid AND pg_attrdef.adnum = pg_attribute.attnum + LEFT OUTER JOIN pg_description ON pg_description.objoid = pg_class.oid AND pg_description.objsubid = pg_attribute.attnum + WHERE + pg_attribute.attnum > 0 + AND attisdropped <> ''t'' + AND pg_class.oid = ' || v_oid || ' + ORDER BY pg_attribute.attnum' ; + + FOR v_rec IN EXECUTE v_sql LOOP + v_ret.fields_name=v_rec.fields_name; + v_ret.fields_type=v_rec.fields_type; + IF v_rec.fields_length > 0 THEN + v_ret.fields_length:=v_rec.fields_length; + ELSE + v_ret.fields_length:=NULL; + END IF; + v_ret.fields_not_null=v_rec.fields_not_null; + v_ret.fields_default=v_rec.fields_default; + v_ret.fields_comment=v_rec.fields_comment; + + v_ret.fields_key_name=''; + + v_len := array_length(v_pk,1) + 1; + v_pos := 1; + WHILE v_pos < v_len LOOP + IF v_rec.fields_name = v_pk[v_pos] THEN + v_ret.fields_key_name=v_pk[v_pos]; + EXIT; + END IF; + v_pos := v_pos + 1; + END LOOP; + + RETURN NEXT v_ret; + END LOOP; + RETURN ; +END; +$body$ +LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER; + +COMMENT ON FUNCTION "public"."table_msg"(a_schema_name varchar, a_table_name varchar) +IS '获得表信息'; + +---重载一个函数 +CREATE OR REPLACE FUNCTION "public"."table_msg" (a_table_name varchar) RETURNS SETOF "public"."tablestruct" AS +$body$ +DECLARE + v_ret tablestruct; +BEGIN + FOR v_ret IN SELECT * FROM table_msg('public',a_table_name) LOOP + RETURN NEXT v_ret; + END LOOP; + RETURN; +END; +$body$ +LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER; + +COMMENT ON FUNCTION "public"."table_msg"(a_table_name varchar) +IS '获得表信息'; \ No newline at end of file diff --git a/thinkphp/library/think/db/exception/BindParamException.php b/thinkphp/library/think/db/exception/BindParamException.php new file mode 100644 index 0000000..dce0c7b --- /dev/null +++ b/thinkphp/library/think/db/exception/BindParamException.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\exception; + +use think\exception\DbException; + +/** + * PDO参数绑定异常 + */ +class BindParamException extends DbException +{ + + /** + * BindParamException constructor. + * @access public + * @param string $message + * @param array $config + * @param string $sql + * @param array $bind + * @param int $code + */ + public function __construct($message, $config, $sql, $bind, $code = 10502) + { + $this->setData('Bind Param', $bind); + parent::__construct($message, $config, $sql, $code); + } +} diff --git a/thinkphp/library/think/db/exception/DataNotFoundException.php b/thinkphp/library/think/db/exception/DataNotFoundException.php new file mode 100644 index 0000000..883e333 --- /dev/null +++ b/thinkphp/library/think/db/exception/DataNotFoundException.php @@ -0,0 +1,44 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\exception; + +use think\exception\DbException; + +class DataNotFoundException extends DbException +{ + protected $table; + + /** + * DbException constructor. + * @access public + * @param string $message + * @param string $table + * @param array $config + */ + public function __construct($message, $table = '', array $config = []) + { + $this->message = $message; + $this->table = $table; + + $this->setData('Database Config', $config); + } + + /** + * 获取数据表名 + * @access public + * @return string + */ + public function getTable() + { + return $this->table; + } +} diff --git a/thinkphp/library/think/db/exception/ModelNotFoundException.php b/thinkphp/library/think/db/exception/ModelNotFoundException.php new file mode 100644 index 0000000..ae52baf --- /dev/null +++ b/thinkphp/library/think/db/exception/ModelNotFoundException.php @@ -0,0 +1,45 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\exception; + +use think\exception\DbException; + +class ModelNotFoundException extends DbException +{ + protected $model; + + /** + * 构造方法 + * @access public + * @param string $message + * @param string $model + * @param array $config + */ + public function __construct($message, $model = '', array $config = []) + { + $this->message = $message; + $this->model = $model; + + $this->setData('Database Config', $config); + } + + /** + * 获取模型类名 + * @access public + * @return string + */ + public function getModel() + { + return $this->model; + } + +} diff --git a/thinkphp/library/think/debug/Console.php b/thinkphp/library/think/debug/Console.php new file mode 100644 index 0000000..5cbaa0f --- /dev/null +++ b/thinkphp/library/think/debug/Console.php @@ -0,0 +1,156 @@ + +// +---------------------------------------------------------------------- + +namespace think\debug; + +use think\Container; +use think\Db; +use think\Response; + +/** + * 浏览器调试输出 + */ +class Console +{ + protected $config = [ + 'tabs' => ['base' => '基本', 'file' => '文件', 'info' => '流程', 'notice|error' => '错误', 'sql' => 'SQL', 'debug|log' => '调试'], + ]; + + // 实例化并传入参数 + public function __construct($config = []) + { + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } + } + + /** + * 调试输出接口 + * @access public + * @param Response $response Response对象 + * @param array $log 日志信息 + * @return bool + */ + public function output(Response $response, array $log = []) + { + $request = Container::get('request'); + $contentType = $response->getHeader('Content-Type'); + $accept = $request->header('accept'); + if (strpos($accept, 'application/json') === 0 || $request->isAjax()) { + return false; + } elseif (!empty($contentType) && strpos($contentType, 'html') === false) { + return false; + } + // 获取基本信息 + $runtime = number_format(microtime(true) - Container::get('app')->getBeginTime(), 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + $mem = number_format((memory_get_usage() - Container::get('app')->getBeginMem()) / 1024, 2); + + if ($request->host()) { + $uri = $request->protocol() . ' ' . $request->method() . ' : ' . $request->url(true); + } else { + $uri = 'cmd:' . implode(' ', $_SERVER['argv']); + } + + // 页面Trace信息 + $base = [ + '请求信息' => date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']) . ' ' . $uri, + '运行时间' => number_format($runtime, 6) . 's [ 吞吐率:' . $reqs . 'req/s ] 内存消耗:' . $mem . 'kb 文件加载:' . count(get_included_files()), + '查询信息' => Db::$queryTimes . ' queries ' . Db::$executeTimes . ' writes ', + '缓存信息' => Container::get('cache')->getReadTimes() . ' reads,' . Container::get('cache')->getWriteTimes() . ' writes', + ]; + + if (session_id()) { + $base['会话信息'] = 'SESSION_ID=' . session_id(); + } + + $info = Container::get('debug')->getFile(true); + + // 页面Trace信息 + $trace = []; + foreach ($this->config['tabs'] as $name => $title) { + $name = strtolower($name); + switch ($name) { + case 'base': // 基本信息 + $trace[$title] = $base; + break; + case 'file': // 文件信息 + $trace[$title] = $info; + break; + default: // 调试信息 + if (strpos($name, '|')) { + // 多组信息 + $names = explode('|', $name); + $result = []; + foreach ($names as $name) { + $result = array_merge($result, isset($log[$name]) ? $log[$name] : []); + } + $trace[$title] = $result; + } else { + $trace[$title] = isset($log[$name]) ? $log[$name] : ''; + } + } + } + + //输出到控制台 + $lines = ''; + foreach ($trace as $type => $msg) { + $lines .= $this->console($type, $msg); + } + $js = << +{$lines} + +JS; + return $js; + } + + protected function console($type, $msg) + { + $type = strtolower($type); + $trace_tabs = array_values($this->config['tabs']); + $line[] = ($type == $trace_tabs[0] || '调试' == $type || '错误' == $type) + ? "console.group('{$type}');" + : "console.groupCollapsed('{$type}');"; + + foreach ((array) $msg as $key => $m) { + switch ($type) { + case '调试': + $var_type = gettype($m); + if (in_array($var_type, ['array', 'string'])) { + $line[] = "console.log(" . json_encode($m) . ");"; + } else { + $line[] = "console.log(" . json_encode(var_export($m, 1)) . ");"; + } + break; + case '错误': + $msg = str_replace("\n", '\n', addslashes(is_scalar($m) ? $m : json_encode($m))); + $style = 'color:#F4006B;font-size:14px;'; + $line[] = "console.error(\"%c{$msg}\", \"{$style}\");"; + break; + case 'sql': + $msg = str_replace("\n", '\n', addslashes($m)); + $style = "color:#009bb4;"; + $line[] = "console.log(\"%c{$msg}\", \"{$style}\");"; + break; + default: + $m = is_string($key) ? $key . ' ' . $m : $key + 1 . ' ' . $m; + $msg = json_encode($m); + $line[] = "console.log({$msg});"; + break; + } + } + $line[] = "console.groupEnd();"; + return implode(PHP_EOL, $line); + } + +} diff --git a/thinkphp/library/think/debug/Html.php b/thinkphp/library/think/debug/Html.php new file mode 100644 index 0000000..a123762 --- /dev/null +++ b/thinkphp/library/think/debug/Html.php @@ -0,0 +1,106 @@ + +// +---------------------------------------------------------------------- + +namespace think\debug; + +use think\Container; +use think\Db; +use think\Response; + +/** + * 页面Trace调试 + */ +class Html +{ + protected $config = [ + 'file' => '', + 'tabs' => ['base' => '基本', 'file' => '文件', 'info' => '流程', 'notice|error' => '错误', 'sql' => 'SQL', 'debug|log' => '调试'], + ]; + + // 实例化并传入参数 + public function __construct(array $config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 调试输出接口 + * @access public + * @param Response $response Response对象 + * @param array $log 日志信息 + * @return bool + */ + public function output(Response $response, array $log = []) + { + $request = Container::get('request'); + $contentType = $response->getHeader('Content-Type'); + $accept = $request->header('accept'); + if (strpos($accept, 'application/json') === 0 || $request->isAjax()) { + return false; + } elseif (!empty($contentType) && strpos($contentType, 'html') === false) { + return false; + } + // 获取基本信息 + $runtime = number_format(microtime(true) - Container::get('app')->getBeginTime(), 10, '.', ''); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + $mem = number_format((memory_get_usage() - Container::get('app')->getBeginMem()) / 1024, 2); + + // 页面Trace信息 + if ($request->host()) { + $uri = $request->protocol() . ' ' . $request->method() . ' : ' . $request->url(true); + } else { + $uri = 'cmd:' . implode(' ', $_SERVER['argv']); + } + $base = [ + '请求信息' => date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']) . ' ' . $uri, + '运行时间' => number_format($runtime, 6) . 's [ 吞吐率:' . $reqs . 'req/s ] 内存消耗:' . $mem . 'kb 文件加载:' . count(get_included_files()), + '查询信息' => Db::$queryTimes . ' queries ' . Db::$executeTimes . ' writes ', + '缓存信息' => Container::get('cache')->getReadTimes() . ' reads,' . Container::get('cache')->getWriteTimes() . ' writes', + ]; + + if (session_id()) { + $base['会话信息'] = 'SESSION_ID=' . session_id(); + } + + $info = Container::get('debug')->getFile(true); + + // 页面Trace信息 + $trace = []; + foreach ($this->config['tabs'] as $name => $title) { + $name = strtolower($name); + switch ($name) { + case 'base': // 基本信息 + $trace[$title] = $base; + break; + case 'file': // 文件信息 + $trace[$title] = $info; + break; + default: // 调试信息 + if (strpos($name, '|')) { + // 多组信息 + $names = explode('|', $name); + $result = []; + foreach ($names as $name) { + $result = array_merge($result, isset($log[$name]) ? $log[$name] : []); + } + $trace[$title] = $result; + } else { + $trace[$title] = isset($log[$name]) ? $log[$name] : ''; + } + } + } + // 调用Trace页面模板 + ob_start(); + include $this->config['file']; + return ob_get_clean(); + } + +} diff --git a/thinkphp/library/think/exception/ClassNotFoundException.php b/thinkphp/library/think/exception/ClassNotFoundException.php new file mode 100644 index 0000000..eb22e73 --- /dev/null +++ b/thinkphp/library/think/exception/ClassNotFoundException.php @@ -0,0 +1,32 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class ClassNotFoundException extends \RuntimeException +{ + protected $class; + public function __construct($message, $class = '') + { + $this->message = $message; + $this->class = $class; + } + + /** + * 获取类名 + * @access public + * @return string + */ + public function getClass() + { + return $this->class; + } +} diff --git a/thinkphp/library/think/exception/DbException.php b/thinkphp/library/think/exception/DbException.php new file mode 100644 index 0000000..6baafb5 --- /dev/null +++ b/thinkphp/library/think/exception/DbException.php @@ -0,0 +1,44 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use think\Exception; + +/** + * Database相关异常处理类 + */ +class DbException extends Exception +{ + /** + * DbException constructor. + * @access public + * @param string $message + * @param array $config + * @param string $sql + * @param int $code + */ + public function __construct($message, array $config = [], $sql = '', $code = 10500) + { + $this->message = $message; + $this->code = $code; + + $this->setData('Database Status', [ + 'Error Code' => $code, + 'Error Message' => $message, + '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 new file mode 100644 index 0000000..3143b8f --- /dev/null +++ b/thinkphp/library/think/exception/ErrorException.php @@ -0,0 +1,56 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use think\Exception; + +/** + * ThinkPHP错误异常 + * 主要用于封装 set_error_handler 和 register_shutdown_function 得到的错误 + * 除开从 think\Exception 继承的功能 + * 其他和PHP系统\ErrorException功能基本一样 + */ +class ErrorException extends Exception +{ + /** + * 用于保存错误级别 + * @var integer + */ + protected $severity; + + /** + * 错误异常构造函数 + * @access public + * @param integer $severity 错误级别 + * @param string $message 错误详细信息 + * @param string $file 出错文件路径 + * @param integer $line 出错行号 + */ + public function __construct($severity, $message, $file, $line) + { + $this->severity = $severity; + $this->message = $message; + $this->file = $file; + $this->line = $line; + $this->code = 0; + } + + /** + * 获取错误级别 + * @access public + * @return integer 错误级别 + */ + final public function getSeverity() + { + return $this->severity; + } +} diff --git a/thinkphp/library/think/exception/Handle.php b/thinkphp/library/think/exception/Handle.php new file mode 100644 index 0000000..02c85ec --- /dev/null +++ b/thinkphp/library/think/exception/Handle.php @@ -0,0 +1,306 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use Exception; +use think\console\Output; +use think\Container; +use think\Response; + +class Handle +{ + protected $render; + protected $ignoreReport = [ + '\\think\\exception\\HttpException', + ]; + + public function setRender($render) + { + $this->render = $render; + } + + /** + * Report or log an exception. + * + * @access public + * @param \Exception $exception + * @return void + */ + public function report(Exception $exception) + { + if (!$this->isIgnoreReport($exception)) { + // 收集异常数据 + if (Container::get('app')->isDebug()) { + $data = [ + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'message' => $this->getMessage($exception), + 'code' => $this->getCode($exception), + ]; + $log = "[{$data['code']}]{$data['message']}[{$data['file']}:{$data['line']}]"; + } else { + $data = [ + 'code' => $this->getCode($exception), + 'message' => $this->getMessage($exception), + ]; + $log = "[{$data['code']}]{$data['message']}"; + } + + if (Container::get('app')->config('log.record_trace')) { + $log .= "\r\n" . $exception->getTraceAsString(); + } + + Container::get('log')->record($log, 'error'); + } + } + + protected function isIgnoreReport(Exception $exception) + { + foreach ($this->ignoreReport as $class) { + if ($exception instanceof $class) { + return true; + } + } + + return false; + } + + /** + * Render an exception into an HTTP response. + * + * @access public + * @param \Exception $e + * @return Response + */ + 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 { + return $this->convertExceptionToResponse($e); + } + } + + /** + * @access public + * @param Output $output + * @param Exception $e + */ + public function renderForConsole(Output $output, Exception $e) + { + if (Container::get('app')->isDebug()) { + $output->setVerbosity(Output::VERBOSITY_DEBUG); + } + + $output->renderException($e); + } + + /** + * @access protected + * @param HttpException $e + * @return Response + */ + protected function renderHttpException(HttpException $e) + { + $status = $e->getStatusCode(); + $template = Container::get('app')->config('http_exception_template'); + + if (!Container::get('app')->isDebug() && !empty($template[$status])) { + return Response::create($template[$status], 'view', $status)->assign(['e' => $e]); + } else { + return $this->convertExceptionToResponse($e); + } + } + + /** + * @access protected + * @param Exception $exception + * @return Response + */ + protected function convertExceptionToResponse(Exception $exception) + { + // 收集异常数据 + if (Container::get('app')->isDebug()) { + // 调试模式,获取详细的错误信息 + $data = [ + 'name' => get_class($exception), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'message' => $this->getMessage($exception), + 'trace' => $exception->getTrace(), + 'code' => $this->getCode($exception), + 'source' => $this->getSourceCode($exception), + 'datas' => $this->getExtendData($exception), + 'tables' => [ + 'GET Data' => $_GET, + 'POST Data' => $_POST, + 'Files' => $_FILES, + 'Cookies' => $_COOKIE, + 'Session' => isset($_SESSION) ? $_SESSION : [], + 'Server/Request Data' => $_SERVER, + 'Environment Variables' => $_ENV, + 'ThinkPHP Constants' => $this->getConst(), + ], + ]; + } else { + // 部署模式仅显示 Code 和 Message + $data = [ + 'code' => $this->getCode($exception), + 'message' => $this->getMessage($exception), + ]; + + if (!Container::get('app')->config('show_error_msg')) { + // 不显示详细错误信息 + $data['message'] = Container::get('app')->config('error_message'); + } + } + + //保留一层 + while (ob_get_level() > 1) { + ob_end_clean(); + } + + $data['echo'] = ob_get_clean(); + + ob_start(); + extract($data); + include Container::get('app')->config('exception_tmpl'); + + // 获取并清空缓存 + $content = ob_get_clean(); + $response = Response::create($content, 'html'); + + if ($exception instanceof HttpException) { + $statusCode = $exception->getStatusCode(); + $response->header($exception->getHeaders()); + } + + if (!isset($statusCode)) { + $statusCode = 500; + } + $response->code($statusCode); + + return $response; + } + + /** + * 获取错误编码 + * ErrorException则使用错误级别作为错误编码 + * @access protected + * @param \Exception $exception + * @return integer 错误编码 + */ + protected function getCode(Exception $exception) + { + $code = $exception->getCode(); + + if (!$code && $exception instanceof ErrorException) { + $code = $exception->getSeverity(); + } + + return $code; + } + + /** + * 获取错误信息 + * ErrorException则使用错误级别作为错误编码 + * @access protected + * @param \Exception $exception + * @return string 错误信息 + */ + protected function getMessage(Exception $exception) + { + $message = $exception->getMessage(); + + if (PHP_SAPI == 'cli') { + return $message; + } + + $lang = Container::get('lang'); + + if (strpos($message, ':')) { + $name = strstr($message, ':', true); + $message = $lang->has($name) ? $lang->get($name) . strstr($message, ':') : $message; + } elseif (strpos($message, ',')) { + $name = strstr($message, ',', true); + $message = $lang->has($name) ? $lang->get($name) . ':' . substr(strstr($message, ','), 1) : $message; + } elseif ($lang->has($message)) { + $message = $lang->get($message); + } + + return $message; + } + + /** + * 获取出错文件内容 + * 获取错误的前9行和后9行 + * @access protected + * @param \Exception $exception + * @return array 错误文件内容 + */ + protected function getSourceCode(Exception $exception) + { + // 读取前9行和后9行 + $line = $exception->getLine(); + $first = ($line - 9 > 0) ? $line - 9 : 1; + + try { + $contents = file($exception->getFile()); + $source = [ + 'first' => $first, + 'source' => array_slice($contents, $first - 1, 19), + ]; + } catch (Exception $e) { + $source = []; + } + + return $source; + } + + /** + * 获取异常扩展信息 + * 用于非调试模式html返回类型显示 + * @access protected + * @param \Exception $exception + * @return array 异常类定义的扩展数据 + */ + protected function getExtendData(Exception $exception) + { + $data = []; + + if ($exception instanceof \think\Exception) { + $data = $exception->getData(); + } + + return $data; + } + + /** + * 获取常量列表 + * @access private + * @return array 常量列表 + */ + private static function getConst() + { + $const = get_defined_constants(true); + + return isset($const['user']) ? $const['user'] : []; + } +} diff --git a/thinkphp/library/think/exception/HttpException.php b/thinkphp/library/think/exception/HttpException.php new file mode 100644 index 0000000..01a27fc --- /dev/null +++ b/thinkphp/library/think/exception/HttpException.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class HttpException extends \RuntimeException +{ + private $statusCode; + private $headers; + + public function __construct($statusCode, $message = null, \Exception $previous = null, array $headers = [], $code = 0) + { + $this->statusCode = $statusCode; + $this->headers = $headers; + + parent::__construct($message, $code, $previous); + } + + public function getStatusCode() + { + return $this->statusCode; + } + + public function getHeaders() + { + return $this->headers; + } +} diff --git a/thinkphp/library/think/exception/HttpResponseException.php b/thinkphp/library/think/exception/HttpResponseException.php new file mode 100644 index 0000000..5297286 --- /dev/null +++ b/thinkphp/library/think/exception/HttpResponseException.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use think\Response; + +class HttpResponseException extends \RuntimeException +{ + /** + * @var Response + */ + protected $response; + + public function __construct(Response $response) + { + $this->response = $response; + } + + public function getResponse() + { + return $this->response; + } + +} diff --git a/thinkphp/library/think/exception/PDOException.php b/thinkphp/library/think/exception/PDOException.php new file mode 100644 index 0000000..25240b6 --- /dev/null +++ b/thinkphp/library/think/exception/PDOException.php @@ -0,0 +1,40 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +/** + * PDO异常处理类 + * 重新封装了系统的\PDOException类 + */ +class PDOException extends DbException +{ + /** + * PDOException constructor. + * @access public + * @param \PDOException $exception + * @param array $config + * @param string $sql + * @param int $code + */ + public function __construct(\PDOException $exception, array $config, $sql, $code = 10501) + { + $error = $exception->errorInfo; + + $this->setData('PDO Error Info', [ + 'SQLSTATE' => $error[0], + 'Driver Error Code' => isset($error[1]) ? $error[1] : 0, + 'Driver Error Message' => isset($error[2]) ? $error[2] : '', + ]); + + parent::__construct($exception->getMessage(), $config, $sql, $code); + } +} diff --git a/thinkphp/library/think/exception/RouteNotFoundException.php b/thinkphp/library/think/exception/RouteNotFoundException.php new file mode 100644 index 0000000..d22e3a6 --- /dev/null +++ b/thinkphp/library/think/exception/RouteNotFoundException.php @@ -0,0 +1,22 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class RouteNotFoundException extends HttpException +{ + + public function __construct() + { + parent::__construct(404, 'Route Not Found'); + } + +} diff --git a/thinkphp/library/think/exception/TemplateNotFoundException.php b/thinkphp/library/think/exception/TemplateNotFoundException.php new file mode 100644 index 0000000..4202069 --- /dev/null +++ b/thinkphp/library/think/exception/TemplateNotFoundException.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class TemplateNotFoundException extends \RuntimeException +{ + protected $template; + + public function __construct($message, $template = '') + { + $this->message = $message; + $this->template = $template; + } + + /** + * 获取模板文件 + * @access public + * @return string + */ + public function getTemplate() + { + return $this->template; + } +} diff --git a/thinkphp/library/think/exception/ThrowableError.php b/thinkphp/library/think/exception/ThrowableError.php new file mode 100644 index 0000000..87b6b9d --- /dev/null +++ b/thinkphp/library/think/exception/ThrowableError.php @@ -0,0 +1,47 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class ThrowableError extends \ErrorException +{ + public function __construct(\Throwable $e) + { + + if ($e instanceof \ParseError) { + $message = 'Parse error: ' . $e->getMessage(); + $severity = E_PARSE; + } elseif ($e instanceof \TypeError) { + $message = 'Type error: ' . $e->getMessage(); + $severity = E_RECOVERABLE_ERROR; + } else { + $message = 'Fatal error: ' . $e->getMessage(); + $severity = E_ERROR; + } + + parent::__construct( + $message, + $e->getCode(), + $severity, + $e->getFile(), + $e->getLine() + ); + + $this->setTrace($e->getTrace()); + } + + protected function setTrace($trace) + { + $traceReflector = new \ReflectionProperty('Exception', 'trace'); + $traceReflector->setAccessible(true); + $traceReflector->setValue($this, $trace); + } +} diff --git a/thinkphp/library/think/exception/ValidateException.php b/thinkphp/library/think/exception/ValidateException.php new file mode 100644 index 0000000..81ddfe2 --- /dev/null +++ b/thinkphp/library/think/exception/ValidateException.php @@ -0,0 +1,34 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class ValidateException extends \RuntimeException +{ + protected $error; + + public function __construct($error, $code = 0) + { + $this->error = $error; + $this->message = is_array($error) ? implode(PHP_EOL, $error) : $error; + $this->code = $code; + } + + /** + * 获取验证错误信息 + * @access public + * @return array|string + */ + public function getError() + { + return $this->error; + } +} diff --git a/thinkphp/library/think/facade/App.php b/thinkphp/library/think/facade/App.php new file mode 100644 index 0000000..b375aa0 --- /dev/null +++ b/thinkphp/library/think/facade/App.php @@ -0,0 +1,63 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\App + * @mixin \think\App + * @method \think\App bind(string $bind) static 绑定模块或者控制器 + * @method void initialize() static 初始化应用 + * @method void init(string $module='') static 初始化模块 + * @method \think\Response run() static 执行应用 + * @method \think\App dispatch(\think\route\Dispatch $dispatch) static 设置当前请求的调度信息 + * @method void log(mixed $log, string $type = 'info') static 记录调试信息 + * @method mixed config(string $name='') static 获取配置参数 + * @method \think\route\Dispatch routeCheck() static URL路由检测(根据PATH_INFO) + * @method \think\App routeMust(bool $must = false) static 设置应用的路由检测机制 + * @method \think\Model model(string $name = '', string $layer = 'model', bool $appendSuffix = false, string $common = 'common') static 实例化模型 + * @method object controller(string $name, string $layer = 'controller', bool $appendSuffix = false, string $empty = '') static 实例化控制器 + * @method \think\Validate validate(string $name = '', string $layer = 'validate', bool $appendSuffix = false, string $common = 'common') static 实例化验证器类 + * @method \think\db\Query db(mixed $config = [], mixed $name = false) static 数据库初始化 + * @method mixed action(string $url, $vars = [], $layer = 'controller', $appendSuffix = false) static 调用模块的操作方法 + * @method string parseClass(string $module, string $layer, string $name, bool $appendSuffix = false) static 解析应用类的类名 + * @method string version() static 获取框架版本 + * @method bool isDebug() static 是否为调试模式 + * @method string getModulePath() static 获取当前模块路径 + * @method void setModulePath(string $path) static 设置当前模块路径 + * @method string getRootPath() static 获取应用根目录 + * @method string getAppPath() static 获取应用类库目录 + * @method string getRuntimePath() static 获取应用运行时目录 + * @method string getThinkPath() static 获取核心框架目录 + * @method string getRoutePath() static 获取路由目录 + * @method string getConfigPath() static 获取应用配置目录 + * @method string getConfigExt() static 获取配置后缀 + * @method string setNamespace(string $namespace) static 设置应用类库命名空间 + * @method string getNamespace() static 获取应用类库命名空间 + * @method string getSuffix() static 是否启用类库后缀 + * @method float getBeginTime() static 获取应用开启时间 + * @method integer getBeginMem() static 获取应用初始内存占用 + * @method \think\Container container() static 获取容器实例 + */ +class App extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'app'; + } +} diff --git a/thinkphp/library/think/facade/Build.php b/thinkphp/library/think/facade/Build.php new file mode 100644 index 0000000..c051bea --- /dev/null +++ b/thinkphp/library/think/facade/Build.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Build + * @mixin \think\Build + * @method void run(array $build = [], string $namespace = 'app', bool $suffix = false) static 根据传入的build资料创建目录和文件 + * @method void module(string $module = '', array $list = [], string $namespace = 'app', bool $suffix = false) static 创建模块 + */ +class Build extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'build'; + } +} diff --git a/thinkphp/library/think/facade/Cache.php b/thinkphp/library/think/facade/Cache.php new file mode 100644 index 0000000..9743486 --- /dev/null +++ b/thinkphp/library/think/facade/Cache.php @@ -0,0 +1,45 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Cache + * @mixin \think\Cache + * @method \think\cache\Driver connect(array $options = [], mixed $name = false) static 连接缓存 + * @method \think\cache\Driver init(array $options = []) static 初始化缓存 + * @method \think\cache\Driver store(string $name = '') static 切换缓存类型 + * @method bool has(string $name) static 判断缓存是否存在 + * @method mixed get(string $name, mixed $default = false) static 读取缓存 + * @method mixed pull(string $name) static 读取缓存并删除 + * @method mixed set(string $name, mixed $value, int $expire = null) static 设置缓存 + * @method mixed remember(string $name, mixed $value, int $expire = null) static 如果不存在则写入缓存 + * @method mixed inc(string $name, int $step = 1) static 自增缓存(针对数值缓存) + * @method mixed dec(string $name, int $step = 1) static 自减缓存(针对数值缓存) + * @method bool rm(string $name) static 删除缓存 + * @method bool clear(string $tag = null) static 清除缓存 + * @method mixed tag(string $name, mixed $keys = null, bool $overlay = false) static 缓存标签 + * @method object handler() static 返回句柄对象,可执行其它高级方法 + */ +class Cache extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'cache'; + } +} diff --git a/thinkphp/library/think/facade/Config.php b/thinkphp/library/think/facade/Config.php new file mode 100644 index 0000000..824d2b6 --- /dev/null +++ b/thinkphp/library/think/facade/Config.php @@ -0,0 +1,39 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Config + * @mixin \think\Config + * @method array load(string $file, string $name = '') static 加载配置文件 + * @method bool has(string $name) static 检测配置是否存在 + * @method array pull(string $name) static 获取一级配置参数 + * @method mixed get(string $name,mixed $default = null) static 获取配置参数 + * @method array set(mixed $name, mixed $value = null) static 设置配置参数 + * @method array reset(string $name ='') static 重置配置参数 + * @method void remove(string $name = '') static 移除配置 + * @method void setYaconf(mixed $yaconf) static 设置开启Yaconf 或者指定配置文件名 + */ +class Config extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'config'; + } +} diff --git a/thinkphp/library/think/facade/Cookie.php b/thinkphp/library/think/facade/Cookie.php new file mode 100644 index 0000000..4d7cea2 --- /dev/null +++ b/thinkphp/library/think/facade/Cookie.php @@ -0,0 +1,39 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Cookie + * @mixin \think\Cookie + * @method void init(array $config = []) static 初始化 + * @method bool has(string $name,string $prefix = null) static 判断Cookie数据 + * @method mixed prefix(string $prefix = '') static 设置或者获取cookie作用域(前缀) + * @method mixed get(string $name,string $prefix = null) static Cookie获取 + * @method mixed set(string $name, mixed $value = null, mixed $option = null) static 设置Cookie + * @method void forever(string $name, mixed $value = null, mixed $option = null) static 永久保存Cookie数据 + * @method void delete(string $name, string $prefix = null) static Cookie删除 + * @method void clear($prefix = null) static Cookie清空 + */ +class Cookie extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'cookie'; + } +} diff --git a/thinkphp/library/think/facade/Debug.php b/thinkphp/library/think/facade/Debug.php new file mode 100644 index 0000000..df20086 --- /dev/null +++ b/thinkphp/library/think/facade/Debug.php @@ -0,0 +1,40 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Debug + * @mixin \think\Debug + * @method void remark(string $name, mixed $value = '') static 记录时间(微秒)和内存使用情况 + * @method int getRangeTime(string $start, string $end, mixed $dec = 6) static 统计某个区间的时间(微秒)使用情况 + * @method int getUseTime(int $dec = 6) static 统计从开始到统计时的时间(微秒)使用情况 + * @method string getThroughputRate(string $start, string $end, mixed $dec = 6) static 获取当前访问的吞吐率情况 + * @method string getRangeMem(string $start, string $end, mixed $dec = 2) static 记录区间的内存使用情况 + * @method int getUseMem(int $dec = 2) static 统计从开始到统计时的内存使用情况 + * @method string getMemPeak(string $start, string $end, mixed $dec = 2) static 统计区间的内存峰值情况 + * @method mixed getFile(bool $detail = false) static 获取文件加载信息 + * @method mixed dump(mixed $var, bool $echo = true, string $label = null, int $flags = ENT_SUBSTITUTE) static 浏览器友好的变量输出 + */ +class Debug extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'debug'; + } +} diff --git a/thinkphp/library/think/facade/Env.php b/thinkphp/library/think/facade/Env.php new file mode 100644 index 0000000..5d04724 --- /dev/null +++ b/thinkphp/library/think/facade/Env.php @@ -0,0 +1,34 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Env + * @mixin \think\Env + * @method void load(string $file) static 读取环境变量定义文件 + * @method mixed get(string $name = null, mixed $default = null) static 获取环境变量值 + * @method void set(mixed $env, string $value = null) static 设置环境变量值 + */ +class Env extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'env'; + } +} diff --git a/thinkphp/library/think/facade/Hook.php b/thinkphp/library/think/facade/Hook.php new file mode 100644 index 0000000..e9e1208 --- /dev/null +++ b/thinkphp/library/think/facade/Hook.php @@ -0,0 +1,37 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Hook + * @mixin \think\Hook + * @method \think\Hook alias(mixed $name, mixed $behavior = null) static 指定行为标识 + * @method void add(string $tag, mixed $behavior, bool $first = false) static 动态添加行为扩展到某个标签 + * @method void import(array $tags, bool $recursive = true) static 批量导入插件 + * @method array get(string $tag = '') static 获取插件信息 + * @method mixed listen(string $tag, mixed $params = null, bool $once = false) static 监听标签的行为 + * @method mixed exec(mixed $class, mixed $params = null) static 执行行为 + */ +class Hook extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'hook'; + } +} diff --git a/thinkphp/library/think/facade/Lang.php b/thinkphp/library/think/facade/Lang.php new file mode 100644 index 0000000..56c4777 --- /dev/null +++ b/thinkphp/library/think/facade/Lang.php @@ -0,0 +1,41 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Lang + * @mixin \think\Lang + * @method mixed range($range = '') static 设定当前的语言 + * @method mixed set(mixed $name, string $value = null, string $range = '') static 设置语言定义 + * @method array load(mixed $file, string $range = '') static 加载语言定义 + * @method mixed get(string $name = null, array $vars = [], string $range = '') static 获取语言定义 + * @method mixed has(string $name, string $range = '') static 获取语言定义 + * @method string detect() static 自动侦测设置获取语言选择 + * @method void saveToCookie(string $lang = null) static 设置当前语言到Cookie + * @method void setLangDetectVar(string $var) static 设置语言自动侦测的变量 + * @method void setLangCookieVar(string $var) static 设置语言的cookie保存变量 + * @method void setAllowLangList(array $list) static 设置允许的语言列表 + */ +class Lang extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'lang'; + } +} diff --git a/thinkphp/library/think/facade/Log.php b/thinkphp/library/think/facade/Log.php new file mode 100644 index 0000000..ae627a5 --- /dev/null +++ b/thinkphp/library/think/facade/Log.php @@ -0,0 +1,50 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Log + * @mixin \think\Log + * @method \think\Log init(array $config = []) static 日志初始化 + * @method mixed getLog(string $type = '') static 获取日志信息 + * @method \think\Log record(mixed $msg, string $type = 'info', array $context = []) static 记录日志信息 + * @method \think\Log clear() static 清空日志信息 + * @method \think\Log key(string $key) static 当前日志记录的授权key + * @method \think\Log close() static 关闭本次请求日志写入 + * @method bool check(array $config) static 检查日志写入权限 + * @method bool save() static 保存调试信息 + * @method void write(mixed $msg, string $type = 'info', bool $force = false) static 实时写入日志信息 + * @method void log(string $level,mixed $message, array $context = []) static 记录日志信息 + * @method void emergency(mixed $message, array $context = []) static 记录emergency信息 + * @method void alert(mixed $message, array $context = []) static 记录alert信息 + * @method void critical(mixed $message, array $context = []) static 记录critical信息 + * @method void error(mixed $message, array $context = []) static 记录error信息 + * @method void warning(mixed $message, array $context = []) static 记录warning信息 + * @method void notice(mixed $message, array $context = []) static 记录notice信息 + * @method void info(mixed $message, array $context = []) static 记录info信息 + * @method void debug(mixed $message, array $context = []) static 记录debug信息 + * @method void sql(mixed $message, array $context = []) static 记录sql信息 + */ +class Log extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'log'; + } +} diff --git a/thinkphp/library/think/facade/Middleware.php b/thinkphp/library/think/facade/Middleware.php new file mode 100644 index 0000000..5e4cac7 --- /dev/null +++ b/thinkphp/library/think/facade/Middleware.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Middleware + * @mixin \think\Middleware + * @method void import(array $middlewares = []) static 批量设置中间件 + * @method void add(mixed $middleware) static 添加中间件到队列 + * @method void unshift(mixed $middleware) static 添加中间件到队列开头 + * @method array all() static 获取中间件队列 + * @method \think\Response dispatch(\think\Request $request) static 执行中间件调度 + */ +class Middleware extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'middleware'; + } +} diff --git a/thinkphp/library/think/facade/Request.php b/thinkphp/library/think/facade/Request.php new file mode 100644 index 0000000..0989253 --- /dev/null +++ b/thinkphp/library/think/facade/Request.php @@ -0,0 +1,97 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Request + * @mixin \think\Request + * @method void hook(mixed $method, mixed $callback = null) static Hook 方法注入 + * @method \think\Request create(string $uri, string $method = 'GET', array $params = [], array $cookie = [], array $files = [], array $server = [], string $content = null) static 创建一个URL请求 + * @method mixed domain(bool $port = false) static 获取当前包含协议、端口的域名 + * @method mixed url(bool $domain = false) static 获取当前完整URL + * @method mixed baseUrl(bool $domain = false) static 获取当前URL + * @method mixed baseFile(bool $domain = false) static 获取当前执行的文件 + * @method mixed root(bool $domain = false) static 获取URL访问根地址 + * @method string rootUrl() static 获取URL访问根目录 + * @method string pathinfo() static 获取当前请求URL的pathinfo信息(含URL后缀) + * @method string path() static 获取当前请求URL的pathinfo信息(不含URL后缀) + * @method string ext() static 当前URL的访问后缀 + * @method float time(bool $float = false) static 获取当前请求的时间 + * @method mixed type() static 当前请求的资源类型 + * @method void mimeType(mixed $type, string $val = '') static 设置资源类型 + * @method string method(bool $method = false) static 当前的请求类型 + * @method bool isGet() static 是否为GET请求 + * @method bool isPost() static 是否为POST请求 + * @method bool isPut() static 是否为PUT请求 + * @method bool isDelete() static 是否为DELTE请求 + * @method bool isHead() static 是否为HEAD请求 + * @method bool isPatch() static 是否为PATCH请求 + * @method bool isOptions() static 是否为OPTIONS请求 + * @method bool isCli() static 是否为cli + * @method bool isCgi() static 是否为cgi + * @method mixed param(string $name = '', mixed $default = null, mixed $filter = '') static 获取当前请求的参数 + * @method mixed route(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取路由参数 + * @method mixed get(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取GET参数 + * @method mixed post(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取POST参数 + * @method mixed put(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取PUT参数 + * @method mixed delete(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取DELETE参数 + * @method mixed patch(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取PATCH参数 + * @method mixed request(string $name = '', mixed $default = null, mixed $filter = '') static 获取request变量 + * @method mixed session(string $name = '', mixed $default = null, mixed $filter = '') static 获取session数据 + * @method mixed cookie(string $name = '', mixed $default = null, mixed $filter = '') static 获取cookie参数 + * @method mixed server(string $name = '', mixed $default = null, mixed $filter = '') static 获取server参数 + * @method mixed env(string $name = '', mixed $default = null, mixed $filter = '') static 获取环境变量 + * @method mixed file(string $name = '') static 获取上传的文件信息 + * @method mixed header(string $name = '', mixed $default = null) static 设置或者获取当前的Header + * @method mixed input(array $data,mixed $name = '', mixed $default = null, mixed $filter = '') static 获取变量 支持过滤和默认值 + * @method mixed filter(mixed $filter = null) static 设置或获取当前的过滤规则 + * @method mixed has(string $name, string $type = 'param', bool $checkEmpty = false) static 是否存在某个请求参数 + * @method mixed only(mixed $name, string $type = 'param') static 获取指定的参数 + * @method mixed except(mixed $name, string $type = 'param') static 排除指定参数获取 + * @method bool isSsl() static 当前是否ssl + * @method bool isAjax(bool $ajax = false) static 当前是否Ajax请求 + * @method bool isPjax(bool $pjax = false) static 当前是否Pjax请求 + * @method mixed ip(int $type = 0, bool $adv = true) static 获取客户端IP地址 + * @method bool isMobile() static 检测是否使用手机访问 + * @method string scheme() static 当前URL地址中的scheme参数 + * @method string query() static 当前请求URL地址中的query参数 + * @method string host(bool $stric = false) static 当前请求的host + * @method string port() static 当前请求URL地址中的port参数 + * @method string protocol() static 当前请求 SERVER_PROTOCOL + * @method string remotePort() static 当前请求 REMOTE_PORT + * @method string contentType() static 当前请求 HTTP_CONTENT_TYPE + * @method array routeInfo() static 获取当前请求的路由信息 + * @method array dispatch() static 获取当前请求的调度信息 + * @method string module() static 获取当前的模块名 + * @method string controller(bool $convert = false) static 获取当前的控制器名 + * @method string action(bool $convert = false) static 获取当前的操作名 + * @method string langset() static 获取当前的语言 + * @method string getContent() static 设置或者获取当前请求的content + * @method string getInput() static 获取当前请求的php://input + * @method string token(string $name = '__token__', mixed $type = 'md5') static 生成请求令牌 + * @method string cache(string $key, mixed $expire = null, array $except = [], string $tag = null) static 设置当前地址的请求缓存 + * @method string getCache() static 读取请求缓存设置 + */ +class Request extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'request'; + } +} diff --git a/thinkphp/library/think/facade/Response.php b/thinkphp/library/think/facade/Response.php new file mode 100644 index 0000000..d7de142 --- /dev/null +++ b/thinkphp/library/think/facade/Response.php @@ -0,0 +1,47 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Response + * @mixin \think\Response + * @method \think\response create(mixed $data = '', string $type = '', int $code = 200, array $header = [], array $options = []) static 创建Response对象 + * @method void send() static 发送数据到客户端 + * @method \think\Response options(mixed $options = []) static 输出的参数 + * @method \think\Response data(mixed $data) static 输出数据设置 + * @method \think\Response header(mixed $name, string $value = null) static 设置响应头 + * @method \think\Response content(mixed $content) static 设置页面输出内容 + * @method \think\Response code(int $code) static 发送HTTP状态 + * @method \think\Response lastModified(string $time) static LastModified + * @method \think\Response expires(string $time) static expires + * @method \think\Response eTag(string $eTag) static eTag + * @method \think\Response cacheControl(string $cache) static 页面缓存控制 + * @method \think\Response contentType(string $contentType, string $charset = 'utf-8') static 页面输出类型 + * @method mixed getHeader(string $name) static 获取头部信息 + * @method mixed getData() static 获取原始数据 + * @method mixed getContent() static 获取输出数据 + * @method int getCode() static 获取状态码 + */ +class Response extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'response'; + } +} diff --git a/thinkphp/library/think/facade/Route.php b/thinkphp/library/think/facade/Route.php new file mode 100644 index 0000000..6457ba4 --- /dev/null +++ b/thinkphp/library/think/facade/Route.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Route + * @mixin \think\Route + * @method \think\route\Domain domain(mixed $name, mixed $rule = '', array $option = [], array $pattern = []) static 注册域名路由 + * @method \think\Route pattern(mixed $name, string $rule = '') static 注册变量规则 + * @method \think\Route option(mixed $name, mixed $value = '') static 注册路由参数 + * @method \think\Route bind(string $bind) static 设置路由绑定 + * @method mixed getBind(string $bind) static 读取路由绑定 + * @method \think\Route name(string $name) static 设置当前路由标识 + * @method mixed getName(string $name) static 读取路由标识 + * @method void setName(string $name) static 批量导入路由标识 + * @method void import(array $rules, string $type = '*') static 导入配置文件的路由规则 + * @method \think\route\RuleItem rule(string $rule, mixed $route, string $method = '*', array $option = [], array $pattern = []) static 注册路由规则 + * @method void rules(array $rules, string $method = '*', array $option = [], array $pattern = []) static 批量注册路由规则 + * @method \think\route\RuleGroup group(string|array $name, array|\Closure $route, array $method = '*', array $option = [], array $pattern = []) static 注册路由分组 + * @method \think\route\RuleItem any(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由 + * @method \think\route\RuleItem get(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由 + * @method \think\route\RuleItem post(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由 + * @method \think\route\RuleItem put(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由 + * @method \think\route\RuleItem delete(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由 + * @method \think\route\RuleItem patch(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由 + * @method \think\route\Resource resource(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册资源路由 + * @method \think\Route controller(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册控制器路由 + * @method \think\Route alias(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册别名路由 + * @method \think\Route setMethodPrefix(mixed $method, string $prefix = '') static 设置不同请求类型下面的方法前缀 + * @method \think\Route rest(string $name, array $resource = []) static rest方法定义和修改 + * @method \think\Route\RuleItem miss(string $route, string $method = '*', array $option = []) static 注册未匹配路由规则后的处理 + * @method \think\Route\RuleItem auto(string $route) static 注册一个自动解析的URL路由 + * @method \think\Route\Dispatch check(string $url, string $depr = '/', bool $must = false, bool $completeMatch = false) static 检测URL路由 + */ +class Route extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'route'; + } +} diff --git a/thinkphp/library/think/facade/Session.php b/thinkphp/library/think/facade/Session.php new file mode 100644 index 0000000..fb9206a --- /dev/null +++ b/thinkphp/library/think/facade/Session.php @@ -0,0 +1,46 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Session + * @mixin \think\Session + * @method void init(array $config = []) static session初始化 + * @method bool has(string $name,string $prefix = null) static 判断session数据 + * @method mixed prefix(string $prefix = '') static 设置或者获取session作用域(前缀) + * @method mixed get(string $name = '',string $prefix = null) static session获取 + * @method mixed pull(string $name,string $prefix = null) static session获取并删除 + * @method void push(string $key, mixed $value) static 添加数据到一个session数组 + * @method void set(string $name, mixed $value , string $prefix = null) static 设置session数据 + * @method void flash(string $name, mixed $value = null) static session设置 下一次请求有效 + * @method void flush() static 清空当前请求的session数据 + * @method void delete(string $name, string $prefix = null) static 删除session数据 + * @method void clear($prefix = null) static 清空session数据 + * @method void start() static 启动session + * @method void destroy() static 销毁session + * @method void pause() static 暂停session + * @method void regenerate(bool $delete = false) static 重新生成session_id + */ +class Session extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'session'; + } +} diff --git a/thinkphp/library/think/facade/Template.php b/thinkphp/library/think/facade/Template.php new file mode 100644 index 0000000..f91b118 --- /dev/null +++ b/thinkphp/library/think/facade/Template.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Template + * @mixin \think\Template + * @method void assign(mixed $name, mixed $value = '') static 模板变量赋值 + * @method mixed get(string $name = '') static 获取模板变量 + * @method void fetch(string $template, array $vars = [], array $config = []) static 渲染模板文件 + * @method void display(string $content, array $vars = [], array $config = []) static 渲染模板内容 + * @method mixed layout(string $name, string $replace = '') static 设置模板布局 + */ +class Template extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'template'; + } +} diff --git a/thinkphp/library/think/facade/Url.php b/thinkphp/library/think/facade/Url.php new file mode 100644 index 0000000..639591a --- /dev/null +++ b/thinkphp/library/think/facade/Url.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Url + * @mixin \think\Url + * @method string build(string $url = '', mixed $vars = '', mixed $suffix = true, mixed $domain = false) static URL生成 支持路由反射 + * @method void root(string $root) static 指定当前生成URL地址的root + */ +class Url extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'url'; + } +} diff --git a/thinkphp/library/think/facade/Validate.php b/thinkphp/library/think/facade/Validate.php new file mode 100644 index 0000000..a6eec23 --- /dev/null +++ b/thinkphp/library/think/facade/Validate.php @@ -0,0 +1,75 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Validate + * @mixin \think\Validate + * @method \think\Validate make(array $rules = [], array $message = [], array $field = []) static 创建一个验证器类 + * @method \think\Validate rule(mixed $name, mixed $rule = '') static 添加字段验证规则 + * @method void extend(string $type, mixed $callback = null) static 注册扩展验证(类型)规则 + * @method void setTypeMsg(mixed $type, string $msg = null) static 设置验证规则的默认提示信息 + * @method \think\Validate message(mixed $name, string $message = '') static 设置提示信息 + * @method \think\Validate scene(string $name) static 设置验证场景 + * @method bool hasScene(string $name) static 判断是否存在某个验证场景 + * @method \think\Validate batch(bool $batch = true) static 设置批量验证 + * @method \think\Validate only(array $fields) static 指定需要验证的字段列表 + * @method \think\Validate remove(mixed $field, mixed $rule = true) static 移除某个字段的验证规则 + * @method \think\Validate append(mixed $field, mixed $rule = null) static 追加某个字段的验证规则 + * @method bool confirm(mixed $value, mixed $rule, array $data = [], string $field = '') static 验证是否和某个字段的值一致 + * @method bool different(mixed $value, mixed $rule, array $data = []) static 验证是否和某个字段的值是否不同 + * @method bool egt(mixed $value, mixed $rule, array $data = []) static 验证是否大于等于某个值 + * @method bool gt(mixed $value, mixed $rule, array $data = []) static 验证是否大于某个值 + * @method bool elt(mixed $value, mixed $rule, array $data = []) static 验证是否小于等于某个值 + * @method bool lt(mixed $value, mixed $rule, array $data = []) static 验证是否小于某个值 + * @method bool eq(mixed $value, mixed $rule) static 验证是否等于某个值 + * @method bool must(mixed $value, mixed $rule) static 必须验证 + * @method bool is(mixed $value, mixed $rule, array $data = []) static 验证字段值是否为有效格式 + * @method bool ip(mixed $value, mixed $rule) static 验证是否有效IP + * @method bool requireIf(mixed $value, mixed $rule) static 验证某个字段等于某个值的时候必须 + * @method bool requireCallback(mixed $value, mixed $rule,array $data) static 通过回调方法验证某个字段是否必须 + * @method bool requireWith(mixed $value, mixed $rule, array $data) static 验证某个字段有值的情况下必须 + * @method bool filter(mixed $value, mixed $rule) static 使用filter_var方式验证 + * @method bool in(mixed $value, mixed $rule) static 验证是否在范围内 + * @method bool notIn(mixed $value, mixed $rule) static 验证是否不在范围内 + * @method bool between(mixed $value, mixed $rule) static between验证数据 + * @method bool notBetween(mixed $value, mixed $rule) static 使用notbetween验证数据 + * @method bool length(mixed $value, mixed $rule) static 验证数据长度 + * @method bool max(mixed $value, mixed $rule) static 验证数据最大长度 + * @method bool min(mixed $value, mixed $rule) static 验证数据最小长度 + * @method bool after(mixed $value, mixed $rule) static 验证日期 + * @method bool before(mixed $value, mixed $rule) static 验证日期 + * @method bool expire(mixed $value, mixed $rule) static 验证有效期 + * @method bool allowIp(mixed $value, mixed $rule) static 验证IP许可 + * @method bool denyIp(mixed $value, mixed $rule) static 验证IP禁用 + * @method bool regex(mixed $value, mixed $rule) static 使用正则验证数据 + * @method bool token(mixed $value, mixed $rule) static 验证表单令牌 + * @method bool dateFormat(mixed $value, mixed $rule) static 验证时间和日期是否符合指定格式 + * @method bool unique(mixed $value, mixed $rule, array $data = [], string $field = '') static 验证是否唯一 + * @method bool check(array $data, mixed $rules = [], string $scene = '') static 数据自动验证 + * @method mixed getError(mixed $value, mixed $rule) static 获取错误信息 + */ +class Validate extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'validate'; + } + +} diff --git a/thinkphp/library/think/facade/View.php b/thinkphp/library/think/facade/View.php new file mode 100644 index 0000000..0843391 --- /dev/null +++ b/thinkphp/library/think/facade/View.php @@ -0,0 +1,40 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\View + * @mixin \think\View + * @method \think\View init(mixed $engine = [], array $replace = []) static 初始化 + * @method \think\View share(mixed $name, mixed $value = '') static 模板变量静态赋值 + * @method \think\View assign(mixed $name, mixed $value = '') static 模板变量赋值 + * @method \think\View config(mixed $name, mixed $value = '') static 配置模板引擎 + * @method \think\View exists(mixed $name) static 检查模板是否存在 + * @method \think\View filter(Callable $filter) static 视图内容过滤 + * @method \think\View engine(mixed $engine = []) static 设置当前模板解析的引擎 + * @method string fetch(string $template = '', array $vars = [], array $config = [], bool $renderContent = false) static 解析和获取模板内容 + * @method string display(string $content = '', array $vars = [], array $config = []) static 渲染内容输出 + */ +class View extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'view'; + } +} diff --git a/thinkphp/library/think/log/driver/File.php b/thinkphp/library/think/log/driver/File.php new file mode 100644 index 0000000..3f6522d --- /dev/null +++ b/thinkphp/library/think/log/driver/File.php @@ -0,0 +1,287 @@ + +// +---------------------------------------------------------------------- + +namespace think\log\driver; + +use think\App; + +/** + * 本地化调试输出到文件 + */ +class File +{ + protected $config = [ + 'time_format' => 'c', + 'single' => false, + 'file_size' => 2097152, + 'path' => '', + 'apart_level' => [], + 'max_files' => 0, + 'json' => false, + ]; + + protected $app; + + // 实例化并传入参数 + public function __construct(App $app, $config = []) + { + $this->app = $app; + + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } + + if (empty($this->config['path'])) { + $this->config['path'] = $this->app->getRuntimePath() . 'log' . DIRECTORY_SEPARATOR; + } elseif (substr($this->config['path'], -1) != DIRECTORY_SEPARATOR) { + $this->config['path'] .= DIRECTORY_SEPARATOR; + } + } + + /** + * 日志写入接口 + * @access public + * @param array $log 日志信息 + * @param bool $append 是否追加请求信息 + * @return bool + */ + public function save(array $log = [], $append = false) + { + $destination = $this->getMasterLogFile(); + + $path = dirname($destination); + !is_dir($path) && mkdir($path, 0755, true); + + $info = []; + + foreach ($log as $type => $val) { + + foreach ($val as $msg) { + if (!is_string($msg)) { + $msg = var_export($msg, true); + } + + $info[$type][] = $this->config['json'] ? $msg : '[ ' . $type . ' ] ' . $msg; + } + + if (!$this->config['json'] && (true === $this->config['apart_level'] || in_array($type, $this->config['apart_level']))) { + // 独立记录的日志级别 + $filename = $this->getApartLevelFile($path, $type); + + $this->write($info[$type], $filename, true, $append); + + unset($info[$type]); + } + } + + if ($info) { + return $this->write($info, $destination, false, $append); + } + + return true; + } + + /** + * 日志写入 + * @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) { + $msg = is_array($msg) ? implode(PHP_EOL, $msg) : $msg; + if (PHP_SAPI == 'cli') { + $info['msg'] = $msg; + $info['type'] = $type; + } else { + $info[$type] = $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 public + * @return string + */ + protected function getMasterLogFile() + { + if ($this->config['max_files']) { + $files = glob($this->config['path'] . '*.log'); + + try { + if (count($files) > $this->config['max_files']) { + unlink($files[0]); + } + } catch (\Exception $e) { + } + } + + $cli = PHP_SAPI == 'cli' ? '_cli' : ''; + + if ($this->config['single']) { + $name = is_string($this->config['single']) ? $this->config['single'] : 'single'; + + $destination = $this->config['path'] . $name . $cli . '.log'; + } else { + if ($this->config['max_files']) { + $filename = date('Ymd') . $cli . '.log'; + } 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'; + } elseif ($this->config['max_files']) { + $name = date('Ymd'); + } else { + $name = date('d'); + } + + return $path . DIRECTORY_SEPARATOR . $name . '_' . $type . $cli . '.log'; + } + + /** + * 检查日志文件大小并自动生成备份文件 + * @access protected + * @param string $destination 日志文件 + * @return void + */ + protected function checkLogSize($destination) + { + if (is_file($destination) && floor($this->config['file_size']) <= filesize($destination)) { + try { + rename($destination, dirname($destination) . DIRECTORY_SEPARATOR . time() . '-' . basename($destination)); + } catch (\Exception $e) { + } + } + } + + /** + * 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) . PHP_EOL; + } else { + $now = $info['timestamp']; + unset($info['timestamp']); + + $message = implode(PHP_EOL, $info); + + $message = "[{$now}]" . $message . PHP_EOL; + } + + return $message; + } + + /** + * 解析日志 + * @access protected + * @param array $info 日志信息 + * @return string + */ + protected function parseLog($info) + { + $requestInfo = [ + 'ip' => $this->app['request']->ip(), + 'method' => $this->app['request']->method(), + 'host' => $this->app['request']->host(), + 'uri' => $this->app['request']->url(), + ]; + + if ($this->config['json']) { + $info = $requestInfo + $info; + return json_encode($info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL; + } + + array_unshift($info, "---------------------------------------------------------------" . PHP_EOL . "\r\n[{$info['timestamp']}] {$requestInfo['ip']} {$requestInfo['method']} {$requestInfo['host']}{$requestInfo['uri']}"); + unset($info['timestamp']); + + return implode(PHP_EOL, $info) . PHP_EOL; + } + + protected function getDebugLog(&$info, $append, $apart) + { + if ($this->app->isDebug() && $append) { + + if ($this->config['json']) { + // 获取基本信息 + $runtime = round(microtime(true) - $this->app->getBeginTime(), 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + + $memory_use = number_format((memory_get_usage() - $this->app->getBeginMem()) / 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) - $this->app->getBeginTime(), 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + + $memory_use = number_format((memory_get_usage() - $this->app->getBeginMem()) / 1024, 2); + + $time_str = '[运行时间:' . number_format($runtime, 6) . 's] [吞吐率:' . $reqs . 'req/s]'; + $memory_str = ' [内存消耗:' . $memory_use . 'kb]'; + $file_load = ' [文件加载:' . count(get_included_files()) . ']'; + + array_unshift($info, $time_str . $memory_str . $file_load); + } + } + } +} diff --git a/thinkphp/library/think/log/driver/Socket.php b/thinkphp/library/think/log/driver/Socket.php new file mode 100644 index 0000000..5e4f8bf --- /dev/null +++ b/thinkphp/library/think/log/driver/Socket.php @@ -0,0 +1,279 @@ + +// +---------------------------------------------------------------------- + +namespace think\log\driver; + +use think\App; + +/** + * github: https://github.com/luofei614/SocketLog + * @author luofei614 + */ +class Socket +{ + public $port = 1116; //SocketLog 服务的http的端口号 + + protected $config = [ + // socket服务器地址 + 'host' => 'localhost', + // 是否显示加载的文件列表 + 'show_included_files' => false, + // 日志强制记录到配置的client_id + 'force_client_ids' => [], + // 限制允许读取日志的client_id + 'allow_client_ids' => [], + //输出到浏览器默认展开的日志级别 + 'expand_level' => ['debug'], + ]; + + protected $css = [ + 'sql' => 'color:#009bb4;', + 'sql_warn' => 'color:#009bb4;font-size:14px;', + 'error' => 'color:#f4006b;font-size:14px;', + 'page' => 'color:#40e2ff;background:#171717;', + 'big' => 'font-size:20px;color:red;', + ]; + + protected $allowForceClientIds = []; //配置强制推送且被授权的client_id + protected $app; + + /** + * 架构函数 + * @access public + * @param array $config 缓存参数 + */ + public function __construct(App $app, array $config = []) + { + $this->app = $app; + + if (!empty($config)) { + $this->config = array_merge($this->config, $config); + } + } + + /** + * 调试输出接口 + * @access public + * @param array $log 日志信息 + * @return bool + */ + public function save(array $log = [], $append = false) + { + if (!$this->check()) { + return false; + } + + $trace = []; + + if ($this->app->isDebug()) { + $runtime = round(microtime(true) - $this->app->getBeginTime(), 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() - $this->app->getBeginMem()) / 1024, 2); + $memory_str = ' [内存消耗:' . $memory_use . 'kb]'; + $file_load = ' [文件加载:' . count(get_included_files()) . ']'; + + if (isset($_SERVER['HTTP_HOST'])) { + $current_uri = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + } else { + $current_uri = 'cmd:' . implode(' ', $_SERVER['argv']); + } + + // 基本信息 + $trace[] = [ + 'type' => 'group', + 'msg' => $current_uri . $time_str . $memory_str . $file_load, + 'css' => $this->css['page'], + ]; + } + + foreach ($log as $type => $val) { + $trace[] = [ + 'type' => in_array($type, $this->config['expand_level']) ? 'group' : 'groupCollapsed', + 'msg' => '[ ' . $type . ' ]', + 'css' => isset($this->css[$type]) ? $this->css[$type] : '', + ]; + + foreach ($val as $msg) { + if (!is_string($msg)) { + $msg = var_export($msg, true); + } + $trace[] = [ + 'type' => 'log', + 'msg' => $msg, + 'css' => '', + ]; + } + + $trace[] = [ + 'type' => 'groupEnd', + 'msg' => '', + 'css' => '', + ]; + } + + if ($this->config['show_included_files']) { + $trace[] = [ + 'type' => 'groupCollapsed', + 'msg' => '[ file ]', + 'css' => '', + ]; + + $trace[] = [ + 'type' => 'log', + 'msg' => implode("\n", get_included_files()), + 'css' => '', + ]; + + $trace[] = [ + 'type' => 'groupEnd', + 'msg' => '', + 'css' => '', + ]; + } + + $trace[] = [ + 'type' => 'groupEnd', + 'msg' => '', + 'css' => '', + ]; + + $tabid = $this->getClientArg('tabid'); + + if (!$client_id = $this->getClientArg('client_id')) { + $client_id = ''; + } + + if (!empty($this->allowForceClientIds)) { + //强制推送到多个client_id + foreach ($this->allowForceClientIds as $force_client_id) { + $client_id = $force_client_id; + $this->sendToClient($tabid, $client_id, $trace, $force_client_id); + } + } else { + $this->sendToClient($tabid, $client_id, $trace, ''); + } + + return true; + } + + /** + * 发送给指定客户端 + * @access protected + * @author Zjmainstay + * @param $tabid + * @param $client_id + * @param $logs + * @param $force_client_id + */ + protected function sendToClient($tabid, $client_id, $logs, $force_client_id) + { + $logs = [ + 'tabid' => $tabid, + 'client_id' => $client_id, + 'logs' => $logs, + 'force_client_id' => $force_client_id, + ]; + + $msg = @json_encode($logs); + $address = '/' . $client_id; //将client_id作为地址, server端通过地址判断将日志发布给谁 + + $this->send($this->config['host'], $msg, $address); + } + + protected function check() + { + $tabid = $this->getClientArg('tabid'); + + //是否记录日志的检查 + if (!$tabid && !$this->config['force_client_ids']) { + return false; + } + + //用户认证 + $allow_client_ids = $this->config['allow_client_ids']; + + if (!empty($allow_client_ids)) { + //通过数组交集得出授权强制推送的client_id + $this->allowForceClientIds = array_intersect($allow_client_ids, $this->config['force_client_ids']); + if (!$tabid && count($this->allowForceClientIds)) { + return true; + } + + $client_id = $this->getClientArg('client_id'); + if (!in_array($client_id, $allow_client_ids)) { + return false; + } + } else { + $this->allowForceClientIds = $this->config['force_client_ids']; + } + + return true; + } + + protected function getClientArg($name) + { + static $args = []; + + $key = 'HTTP_USER_AGENT'; + + if (isset($_SERVER['HTTP_SOCKETLOG'])) { + $key = 'HTTP_SOCKETLOG'; + } + + if (!isset($_SERVER[$key])) { + return; + } + + if (empty($args)) { + if (!preg_match('/SocketLog\((.*?)\)/', $_SERVER[$key], $match)) { + $args = ['tabid' => null]; + return; + } + parse_str($match[1], $args); + } + + if (isset($args[$name])) { + return $args[$name]; + } + + return; + } + + /** + * @access protected + * @param string $host - $host of socket server + * @param string $message - 发送的消息 + * @param string $address - 地址 + * @return bool + */ + protected function send($host, $message = '', $address = '/') + { + $url = 'http://' . $host . ':' . $this->port . $address; + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $message); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + + $headers = [ + "Content-Type: application/json;charset=UTF-8", + ]; + + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); //设置header + + return curl_exec($ch); + } + +} diff --git a/thinkphp/library/think/model/Collection.php b/thinkphp/library/think/model/Collection.php new file mode 100644 index 0000000..fc0967c --- /dev/null +++ b/thinkphp/library/think/model/Collection.php @@ -0,0 +1,118 @@ + +// +---------------------------------------------------------------------- + +namespace think\model; + +use think\Collection as BaseCollection; +use think\Model; + +class Collection extends BaseCollection +{ + /** + * 延迟预载入关联查询 + * @access public + * @param mixed $relation 关联 + * @return $this + */ + public function load($relation) + { + if (!$this->isEmpty()) { + $item = current($this->items); + $item->eagerlyResultSet($this->items, $relation); + } + + return $this; + } + + /** + * 绑定(一对一)关联属性到当前模型 + * @access protected + * @param string $relation 关联名称 + * @param array $attrs 绑定属性 + * @return $this + */ + public function bindAttr($relation, array $attrs = []) + { + $this->each(function (Model $model) use ($relation, $attrs) { + $model->bindAttr($relation, $attrs); + }); + + return $this; + } + + /** + * 设置需要隐藏的输出属性 + * @access public + * @param array $hidden 属性列表 + * @param bool $override 是否覆盖 + * @return $this + */ + public function hidden($hidden = [], $override = false) + { + $this->each(function ($model) use ($hidden, $override) { + /** @var Model $model */ + $model->hidden($hidden, $override); + }); + + return $this; + } + + /** + * 设置需要输出的属性 + * @access public + * @param array $visible + * @param bool $override 是否覆盖 + * @return $this + */ + public function visible($visible = [], $override = false) + { + $this->each(function ($model) use ($visible, $override) { + /** @var Model $model */ + $model->visible($visible, $override); + }); + + return $this; + } + + /** + * 设置需要追加的输出属性 + * @access public + * @param array $append 属性列表 + * @param bool $override 是否覆盖 + * @return $this + */ + public function append($append = [], $override = false) + { + $this->each(function ($model) use ($append, $override) { + /** @var Model $model */ + $model && $model->append($append, $override); + }); + + return $this; + } + + /** + * 设置数据字段获取器 + * @access public + * @param string|array $name 字段名 + * @param callable $callback 闭包获取器 + * @return $this + */ + public function withAttr($name, $callback = null) + { + $this->each(function ($model) use ($name, $callback) { + /** @var Model $model */ + $model && $model->withAttribute($name, $callback); + }); + + return $this; + } +} diff --git a/thinkphp/library/think/model/Pivot.php b/thinkphp/library/think/model/Pivot.php new file mode 100644 index 0000000..a3a395e --- /dev/null +++ b/thinkphp/library/think/model/Pivot.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- + +namespace think\model; + +use think\Model; + +class Pivot extends Model +{ + + /** @var Model */ + public $parent; + + protected $autoWriteTimestamp = false; + + /** + * 架构函数 + * @access public + * @param array|object $data 数据 + * @param Model $parent 上级模型 + * @param string $table 中间数据表名 + */ + public function __construct($data = [], Model $parent = null, $table = '') + { + $this->parent = $parent; + + if (is_null($this->name)) { + $this->name = $table; + } + + parent::__construct($data); + } + +} diff --git a/thinkphp/library/think/model/Relation.php b/thinkphp/library/think/model/Relation.php new file mode 100644 index 0000000..ac6dd4c --- /dev/null +++ b/thinkphp/library/think/model/Relation.php @@ -0,0 +1,187 @@ + +// +---------------------------------------------------------------------- + +namespace think\model; + +use think\db\Query; +use think\Exception; +use think\Model; + +/** + * Class Relation + * @package think\model + * + * @mixin Query + */ +abstract class Relation +{ + // 父模型对象 + protected $parent; + /** @var Model 当前关联的模型类 */ + protected $model; + /** @var Query 关联模型查询对象 */ + protected $query; + // 关联表外键 + protected $foreignKey; + // 关联表主键 + protected $localKey; + // 基础查询 + protected $baseQuery; + // 是否为自关联 + protected $selfRelation; + + /** + * 获取关联的所属模型 + * @access public + * @return Model + */ + public function getParent() + { + return $this->parent; + } + + /** + * 获取当前的关联模型类的实例 + * @access public + * @return Model + */ + public function getModel() + { + return $this->query->getModel(); + } + + /** + * 获取当前的关联模型类的实例 + * @access public + * @return Query + */ + public function getQuery() + { + 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 + * @param array $resultSet 数据集 + * @return mixed + */ + protected function resultSetBuild($resultSet) + { + return (new $this->model)->toCollection($resultSet); + } + + protected function getQueryFields($model) + { + $fields = $this->query->getOptions('field'); + return $this->getRelationQueryFields($fields, $model); + } + + protected function getRelationQueryFields($fields, $model) + { + if ($fields) { + + if (is_string($fields)) { + $fields = explode(',', $fields); + } + + foreach ($fields as &$field) { + if (false === strpos($field, '.')) { + $field = $model . '.' . $field; + } + } + } else { + $fields = $model . '.*'; + } + + return $fields; + } + + protected function getQueryWhere(&$where, $relation) + { + foreach ($where as $key => &$val) { + if (is_string($key)) { + $where[] = [false === strpos($key, '.') ? $relation . '.' . $key : $key, '=', $val]; + unset($where[$key]); + } elseif (isset($val[0]) && false === strpos($val[0], '.')) { + $val[0] = $relation . '.' . $val[0]; + } + } + } + + /** + * 更新数据 + * @access public + * @param array $data 更新数据 + * @return integer|string + */ + public function update(array $data = []) + { + return $this->query->update($data); + } + + /** + * 删除记录 + * @access public + * @param mixed $data 表达式 true 表示强制删除 + * @return int + * @throws Exception + * @throws PDOException + */ + public function delete($data = null) + { + return $this->query->delete($data); + } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + {} + + public function __call($method, $args) + { + if ($this->query) { + // 执行基础查询 + $this->baseQuery(); + + $result = call_user_func_array([$this->query->getModel(), $method], $args); + + return $result === $this->query && !in_array(strtolower($method), ['fetchsql', 'fetchpdo']) ? $this : $result; + } else { + throw new Exception('method not exists:' . __CLASS__ . '->' . $method); + } + } +} diff --git a/thinkphp/library/think/model/concern/Attribute.php b/thinkphp/library/think/model/concern/Attribute.php new file mode 100644 index 0000000..66627b3 --- /dev/null +++ b/thinkphp/library/think/model/concern/Attribute.php @@ -0,0 +1,656 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\concern; + +use InvalidArgumentException; +use think\db\Expression; +use think\Exception; +use think\Loader; +use think\model\Relation; + +trait Attribute +{ + /** + * 数据表主键 复合主键使用数组定义 + * @var string|array + */ + protected $pk = 'id'; + + /** + * 数据表字段信息 留空则自动获取 + * @var array + */ + protected $field = []; + + /** + * JSON数据表字段 + * @var array + */ + protected $json = []; + + /** + * JSON数据取出是否需要转换为数组 + * @var bool + */ + protected $jsonAssoc = false; + + /** + * JSON数据表字段类型 + * @var array + */ + protected $jsonType = []; + + /** + * 数据表废弃字段 + * @var array + */ + protected $disuse = []; + + /** + * 数据表只读字段 + * @var array + */ + protected $readonly = []; + + /** + * 数据表字段类型 + * @var array + */ + protected $type = []; + + /** + * 当前模型数据 + * @var array + */ + private $data = []; + + /** + * 修改器执行记录 + * @var array + */ + private $set = []; + + /** + * 原始数据 + * @var array + */ + private $origin = []; + + /** + * 动态获取器 + * @var array + */ + private $withAttr = []; + + /** + * 获取模型对象的主键 + * @access public + * @return string|array + */ + public function getPk() + { + return $this->pk; + } + + /** + * 判断一个字段名是否为主键字段 + * @access public + * @param string $key 名称 + * @return bool + */ + protected function isPk($key) + { + $pk = $this->getPk(); + if (is_string($pk) && $pk == $key) { + return true; + } elseif (is_array($pk) && in_array($key, $pk)) { + return true; + } + + return false; + } + + /** + * 获取模型对象的主键值 + * @access public + * @return integer + */ + public function getKey() + { + $pk = $this->getPk(); + if (is_string($pk) && array_key_exists($pk, $this->data)) { + return $this->data[$pk]; + } + + return; + } + + /** + * 设置允许写入的字段 + * @access public + * @param array|string|true $field 允许写入的字段 如果为true只允许写入数据表字段 + * @return $this + */ + public function allowField($field) + { + if (is_string($field)) { + $field = explode(',', $field); + } + + $this->field = $field; + + return $this; + } + + /** + * 设置只读字段 + * @access public + * @param array|string $field 只读字段 + * @return $this + */ + public function readonly($field) + { + if (is_string($field)) { + $field = explode(',', $field); + } + + $this->readonly = $field; + + return $this; + } + + /** + * 设置数据对象值 + * @access public + * @param mixed $data 数据或者属性名 + * @param mixed $value 值 + * @return $this + */ + public function data($data, $value = null) + { + if (is_string($data)) { + $this->data[$data] = $value; + return $this; + } + + // 清空数据 + $this->data = []; + + if (is_object($data)) { + $data = get_object_vars($data); + } + + if ($this->disuse) { + // 废弃字段 + foreach ((array) $this->disuse as $key) { + if (array_key_exists($key, $data)) { + unset($data[$key]); + } + } + } + + if (true === $value) { + // 数据对象赋值 + foreach ($data as $key => $value) { + $this->setAttr($key, $value, $data); + } + } elseif (is_array($value)) { + foreach ($value as $name) { + if (isset($data[$name])) { + $this->data[$name] = $data[$name]; + } + } + } else { + $this->data = $data; + } + + return $this; + } + + /** + * 批量设置数据对象值 + * @access public + * @param mixed $data 数据 + * @param bool $set 是否需要进行数据处理 + * @return $this + */ + public function appendData($data, $set = false) + { + if ($set) { + // 进行数据处理 + foreach ($data as $key => $value) { + $this->setAttr($key, $value, $data); + } + } else { + if (is_object($data)) { + $data = get_object_vars($data); + } + + $this->data = array_merge($this->data, $data); + } + + return $this; + } + + /** + * 获取对象原始数据 如果不存在指定字段返回null + * @access public + * @param string $name 字段名 留空获取全部 + * @return mixed + */ + public function getOrigin($name = null) + { + if (is_null($name)) { + return $this->origin; + } + return array_key_exists($name, $this->origin) ? $this->origin[$name] : null; + } + + /** + * 获取对象原始数据 如果不存在指定字段返回false + * @access public + * @param string $name 字段名 留空获取全部 + * @return mixed + * @throws InvalidArgumentException + */ + public function getData($name = null) + { + if (is_null($name)) { + return $this->data; + } elseif (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } elseif (array_key_exists($name, $this->relation)) { + return $this->relation[$name]; + } + throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name); + } + + /** + * 获取变化的数据 并排除只读数据 + * @access public + * @return array + */ + public function getChangedData() + { + 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)) { + // 只读字段不允许更新 + foreach ($this->readonly as $key => $field) { + if (isset($data[$field])) { + unset($data[$field]); + } + } + } + + return $data; + } + + /** + * 修改器 设置数据对象值 + * @access public + * @param string $name 属性名 + * @param mixed $value 属性值 + * @param array $data 数据 + * @return void + */ + public function setAttr($name, $value, $data = []) + { + if (isset($this->set[$name])) { + return; + } + + if (is_null($value) && $this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) { + // 自动写入的时间戳字段 + $value = $this->autoWriteTimestamp($name); + } else { + // 检测修改器 + $method = 'set' . Loader::parseName($name, 1) . 'Attr'; + + if (method_exists($this, $method)) { + $origin = $this->data; + $value = $this->$method($value, array_merge($this->data, $data)); + + $this->set[$name] = true; + if (is_null($value) && $origin !== $this->data) { + return; + } + } elseif (isset($this->type[$name])) { + // 类型转换 + $value = $this->writeTransform($value, $this->type[$name]); + } + } + + // 设置数据对象属性 + $this->data[$name] = $value; + } + + /** + * 是否需要自动写入时间字段 + * @access public + * @param bool $auto + * @return $this + */ + public function isAutoWriteTimestamp($auto) + { + $this->autoWriteTimestamp = $auto; + + return $this; + } + + /** + * 自动写入时间戳 + * @access protected + * @param string $name 时间戳字段 + * @return mixed + */ + protected function autoWriteTimestamp($name) + { + if (isset($this->type[$name])) { + $type = $this->type[$name]; + + if (strpos($type, ':')) { + list($type, $param) = explode(':', $type, 2); + } + + switch ($type) { + case 'datetime': + case 'date': + $value = $this->formatDateTime('Y-m-d H:i:s.u'); + break; + case 'timestamp': + case 'integer': + default: + $value = time(); + break; + } + } elseif (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [ + 'datetime', + 'date', + 'timestamp', + ])) { + $value = $this->formatDateTime('Y-m-d H:i:s.u'); + } else { + $value = time(); + } + + return $value; + } + + /** + * 数据写入 类型转换 + * @access protected + * @param mixed $value 值 + * @param string|array $type 要转换的类型 + * @return mixed + */ + protected function writeTransform($value, $type) + { + if (is_null($value)) { + return; + } + + if ($value instanceof Expression) { + return $value; + } + + if (is_array($type)) { + list($type, $param) = $type; + } elseif (strpos($type, ':')) { + list($type, $param) = explode(':', $type, 2); + } + + switch ($type) { + case 'integer': + $value = (int) $value; + break; + case 'float': + if (empty($param)) { + $value = (float) $value; + } else { + $value = (float) number_format($value, $param, '.', ''); + } + break; + case 'boolean': + $value = (bool) $value; + break; + case 'timestamp': + if (!is_numeric($value)) { + $value = strtotime($value); + } + break; + case 'datetime': + $value = is_numeric($value) ? $value : strtotime($value); + $value = $this->formatDateTime('Y-m-d H:i:s.u', $value); + break; + case 'object': + if (is_object($value)) { + $value = json_encode($value, JSON_FORCE_OBJECT); + } + break; + case 'array': + $value = (array) $value; + case 'json': + $option = !empty($param) ? (int) $param : JSON_UNESCAPED_UNICODE; + $value = json_encode($value, $option); + break; + case 'serialize': + $value = serialize($value); + break; + } + + return $value; + } + + /** + * 获取器 获取数据对象的值 + * @access public + * @param string $name 名称 + * @param array $item 数据 + * @return mixed + * @throws InvalidArgumentException + */ + public function getAttr($name, &$item = null) + { + try { + $notFound = false; + $value = $this->getData($name); + } catch (InvalidArgumentException $e) { + $notFound = true; + $value = null; + } + + // 检测属性获取器 + $fieldName = Loader::parseName($name); + $method = 'get' . Loader::parseName($name, 1) . 'Attr'; + + if (isset($this->withAttr[$fieldName])) { + if ($notFound && $relation = $this->isRelationAttr($name)) { + $modelRelation = $this->$relation(); + $value = $this->getRelationData($modelRelation); + } + + $closure = $this->withAttr[$fieldName]; + $value = $closure($value, $this->data); + } elseif (method_exists($this, $method)) { + if ($notFound && $relation = $this->isRelationAttr($name)) { + $modelRelation = $this->$relation(); + $value = $this->getRelationData($modelRelation); + } + + $value = $this->$method($value, $this->data); + } elseif (isset($this->type[$name])) { + // 类型转换 + $value = $this->readTransform($value, $this->type[$name]); + } elseif ($this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) { + if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [ + 'datetime', + 'date', + 'timestamp', + ])) { + $value = $this->formatDateTime($this->dateFormat, $value); + } else { + $value = $this->formatDateTime($this->dateFormat, $value, true); + } + } elseif ($notFound) { + $value = $this->getRelationAttribute($name, $item); + } + + return $value; + } + + /** + * 获取关联属性值 + * @access protected + * @param string $name 属性名 + * @param array $item 数据 + * @return mixed + */ + protected function getRelationAttribute($name, &$item) + { + $relation = $this->isRelationAttr($name); + + if ($relation) { + $modelRelation = $this->$relation(); + if ($modelRelation instanceof Relation) { + $value = $this->getRelationData($modelRelation); + + if ($item && method_exists($modelRelation, 'getBindAttr') && $bindAttr = $modelRelation->getBindAttr()) { + + foreach ($bindAttr as $key => $attr) { + $key = is_numeric($key) ? $attr : $key; + + if (isset($item[$key])) { + throw new Exception('bind attr has exists:' . $key); + } else { + $item[$key] = $value ? $value->getAttr($attr) : null; + } + } + + return false; + } + + // 保存关联对象值 + $this->relation[$name] = $value; + + return $value; + } + } + + throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name); + } + + /** + * 数据读取 类型转换 + * @access protected + * @param mixed $value 值 + * @param string|array $type 要转换的类型 + * @return mixed + */ + protected function readTransform($value, $type) + { + if (is_null($value)) { + return; + } + + if (is_array($type)) { + list($type, $param) = $type; + } elseif (strpos($type, ':')) { + list($type, $param) = explode(':', $type, 2); + } + + switch ($type) { + case 'integer': + $value = (int) $value; + break; + case 'float': + if (empty($param)) { + $value = (float) $value; + } else { + $value = (float) number_format($value, $param, '.', ''); + } + break; + case 'boolean': + $value = (bool) $value; + break; + case 'timestamp': + if (!is_null($value)) { + $format = !empty($param) ? $param : $this->dateFormat; + $value = $this->formatDateTime($format, $value, true); + } + break; + case 'datetime': + if (!is_null($value)) { + $format = !empty($param) ? $param : $this->dateFormat; + $value = $this->formatDateTime($format, $value); + } + break; + case 'json': + $value = json_decode($value, true); + break; + case 'array': + $value = empty($value) ? [] : json_decode($value, true); + break; + case 'object': + $value = empty($value) ? new \stdClass() : json_decode($value); + break; + case 'serialize': + try { + $value = unserialize($value); + } catch (\Exception $e) { + $value = null; + } + break; + default: + if (false !== strpos($type, '\\')) { + // 对象类型 + $value = new $type($value); + } + } + + return $value; + } + + /** + * 设置数据字段获取器 + * @access public + * @param string|array $name 字段名 + * @param callable $callback 闭包获取器 + * @return $this + */ + public function withAttribute($name, $callback = null) + { + if (is_array($name)) { + foreach ($name as $key => $val) { + $key = Loader::parseName($key); + + $this->withAttr[$key] = $val; + } + } else { + $name = Loader::parseName($name); + + $this->withAttr[$name] = $callback; + } + + return $this; + } +} diff --git a/thinkphp/library/think/model/concern/Conversion.php b/thinkphp/library/think/model/concern/Conversion.php new file mode 100644 index 0000000..de4db93 --- /dev/null +++ b/thinkphp/library/think/model/concern/Conversion.php @@ -0,0 +1,273 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\concern; + +use think\Collection; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Collection as ModelCollection; + +/** + * 模型数据转换处理 + */ +trait Conversion +{ + /** + * 数据输出显示的属性 + * @var array + */ + protected $visible = []; + + /** + * 数据输出隐藏的属性 + * @var array + */ + protected $hidden = []; + + /** + * 数据输出需要追加的属性 + * @var array + */ + protected $append = []; + + /** + * 数据集对象名 + * @var string + */ + protected $resultSetType; + + /** + * 设置需要附加的输出属性 + * @access public + * @param array $append 属性列表 + * @param bool $override 是否覆盖 + * @return $this + */ + public function append(array $append = [], $override = false) + { + $this->append = $override ? $append : array_merge($this->append, $append); + + return $this; + } + + /** + * 设置附加关联对象的属性 + * @access public + * @param string $attr 关联属性 + * @param string|array $append 追加属性名 + * @return $this + * @throws Exception + */ + public function appendRelationAttr($attr, $append) + { + if (is_string($append)) { + $append = explode(',', $append); + } + + $relation = Loader::parseName($attr, 1, false); + if (isset($this->relation[$relation])) { + $model = $this->relation[$relation]; + } else { + $model = $this->getRelationData($this->$relation()); + } + + if ($model instanceof Model) { + foreach ($append as $key => $attr) { + $key = is_numeric($key) ? $attr : $key; + if (isset($this->data[$key])) { + throw new Exception('bind attr has exists:' . $key); + } else { + $this->data[$key] = $model->getAttr($attr); + } + } + } + + return $this; + } + + /** + * 设置需要隐藏的输出属性 + * @access public + * @param array $hidden 属性列表 + * @param bool $override 是否覆盖 + * @return $this + */ + public function hidden(array $hidden = [], $override = false) + { + $this->hidden = $override ? $hidden : array_merge($this->hidden, $hidden); + + return $this; + } + + /** + * 设置需要输出的属性 + * @access public + * @param array $visible + * @param bool $override 是否覆盖 + * @return $this + */ + public function visible(array $visible = [], $override = false) + { + $this->visible = $override ? $visible : array_merge($this->visible, $visible); + + return $this; + } + + /** + * 转换当前模型对象为数组 + * @access public + * @return array + */ + public function toArray() + { + $item = []; + $hasVisible = false; + + foreach ($this->visible as $key => $val) { + if (is_string($val)) { + if (strpos($val, '.')) { + list($relation, $name) = explode('.', $val); + $this->visible[$relation][] = $name; + } else { + $this->visible[$val] = true; + $hasVisible = true; + } + unset($this->visible[$key]); + } + } + + foreach ($this->hidden as $key => $val) { + if (is_string($val)) { + if (strpos($val, '.')) { + list($relation, $name) = explode('.', $val); + $this->hidden[$relation][] = $name; + } else { + $this->hidden[$val] = true; + } + unset($this->hidden[$key]); + } + } + + // 合并关联数据 + $data = array_merge($this->data, $this->relation); + + foreach ($data as $key => $val) { + if ($val instanceof Model || $val instanceof ModelCollection) { + // 关联模型对象 + if (isset($this->visible[$key]) && is_array($this->visible[$key])) { + $val->visible($this->visible[$key]); + } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) { + $val->hidden($this->hidden[$key]); + } + // 关联模型对象 + if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) { + $item[$key] = $val->toArray(); + } + } elseif (isset($this->visible[$key])) { + $item[$key] = $this->getAttr($key); + } elseif (!isset($this->hidden[$key]) && !$hasVisible) { + $item[$key] = $this->getAttr($key); + } + } + + // 追加属性(必须定义获取器) + if (!empty($this->append)) { + foreach ($this->append as $key => $name) { + if (is_array($name)) { + // 追加关联对象属性 + $relation = $this->getRelation($key); + + if (!$relation) { + $relation = $this->getAttr($key); + if ($relation) { + $relation->visible($name); + } + } + + $item[$key] = $relation ? $relation->append($name)->toArray() : []; + } elseif (strpos($name, '.')) { + list($key, $attr) = explode('.', $name); + // 追加关联对象属性 + $relation = $this->getRelation($key); + + if (!$relation) { + $relation = $this->getAttr($key); + if ($relation) { + $relation->visible([$attr]); + } + } + + $item[$key] = $relation ? $relation->append([$attr])->toArray() : []; + } else { + $item[$name] = $this->getAttr($name, $item); + } + } + } + + return $item; + } + + /** + * 转换当前模型对象为JSON字符串 + * @access public + * @param integer $options json参数 + * @return string + */ + public function toJson($options = JSON_UNESCAPED_UNICODE) + { + return json_encode($this->toArray(), $options); + } + + /** + * 移除当前模型的关联属性 + * @access public + * @return $this + */ + public function removeRelation() + { + $this->relation = []; + return $this; + } + + public function __toString() + { + return $this->toJson(); + } + + // JsonSerializable + public function jsonSerialize() + { + return $this->toArray(); + } + + /** + * 转换数据集为数据集对象 + * @access public + * @param array|Collection $collection 数据集 + * @param string $resultSetType 数据集类 + * @return Collection + */ + public function toCollection($collection, $resultSetType = null) + { + $resultSetType = $resultSetType ?: $this->resultSetType; + + if ($resultSetType && false !== strpos($resultSetType, '\\')) { + $collection = new $resultSetType($collection); + } else { + $collection = new ModelCollection($collection); + } + + return $collection; + } + +} diff --git a/thinkphp/library/think/model/concern/ModelEvent.php b/thinkphp/library/think/model/concern/ModelEvent.php new file mode 100644 index 0000000..3a87484 --- /dev/null +++ b/thinkphp/library/think/model/concern/ModelEvent.php @@ -0,0 +1,238 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\concern; + +use think\Container; +use think\Loader; + +/** + * 模型事件处理 + */ +trait ModelEvent +{ + /** + * 模型回调 + * @var array + */ + private static $event = []; + + /** + * 模型事件观察 + * @var array + */ + protected static $observe = ['before_write', 'after_write', 'before_insert', 'after_insert', 'before_update', 'after_update', 'before_delete', 'after_delete', 'before_restore', 'after_restore']; + + /** + * 绑定模型事件观察者类 + * @var array + */ + protected $observerClass; + + /** + * 是否需要事件响应 + * @var bool + */ + private $withEvent = true; + + /** + * 注册回调方法 + * @access public + * @param string $event 事件名 + * @param callable $callback 回调方法 + * @param bool $override 是否覆盖 + * @return void + */ + public static function event($event, $callback, $override = false) + { + $class = static::class; + + if ($override) { + self::$event[$class][$event] = []; + } + + self::$event[$class][$event][] = $callback; + } + + /** + * 清除回调方法 + * @access public + * @return void + */ + public static function flushEvent() + { + self::$event[static::class] = []; + } + + /** + * 注册一个模型观察者 + * + * @param object|string $class + * @return void + */ + public static function observe($class) + { + self::flushEvent(); + + foreach (static::$observe as $event) { + $eventFuncName = Loader::parseName($event, 1, false); + + if (method_exists($class, $eventFuncName)) { + static::event($event, [$class, $eventFuncName]); + } + } + } + + /** + * 当前操作的事件响应 + * @access protected + * @param bool $event 是否需要事件响应 + * @return $this + */ + public function withEvent($event) + { + $this->withEvent = $event; + return $this; + } + + /** + * 触发事件 + * @access protected + * @param string $event 事件名 + * @return bool + */ + protected function trigger($event) + { + $class = static::class; + + if ($this->withEvent && isset(self::$event[$class][$event])) { + foreach (self::$event[$class][$event] as $callback) { + $result = Container::getInstance()->invoke($callback, [$this]); + + if (false === $result) { + return false; + } + } + } + + return true; + } + + /** + * 模型before_insert事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function beforeInsert($callback, $override = false) + { + self::event('before_insert', $callback, $override); + } + + /** + * 模型after_insert事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function afterInsert($callback, $override = false) + { + self::event('after_insert', $callback, $override); + } + + /** + * 模型before_update事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function beforeUpdate($callback, $override = false) + { + self::event('before_update', $callback, $override); + } + + /** + * 模型after_update事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function afterUpdate($callback, $override = false) + { + self::event('after_update', $callback, $override); + } + + /** + * 模型before_write事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function beforeWrite($callback, $override = false) + { + self::event('before_write', $callback, $override); + } + + /** + * 模型after_write事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function afterWrite($callback, $override = false) + { + self::event('after_write', $callback, $override); + } + + /** + * 模型before_delete事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function beforeDelete($callback, $override = false) + { + self::event('before_delete', $callback, $override); + } + + /** + * 模型after_delete事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function afterDelete($callback, $override = false) + { + self::event('after_delete', $callback, $override); + } + + /** + * 模型before_restore事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function beforeRestore($callback, $override = false) + { + self::event('before_restore', $callback, $override); + } + + /** + * 模型after_restore事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function afterRestore($callback, $override = false) + { + self::event('after_restore', $callback, $override); + } +} diff --git a/thinkphp/library/think/model/concern/RelationShip.php b/thinkphp/library/think/model/concern/RelationShip.php new file mode 100644 index 0000000..48579b7 --- /dev/null +++ b/thinkphp/library/think/model/concern/RelationShip.php @@ -0,0 +1,697 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\concern; + +use think\Collection; +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; +use think\model\relation\BelongsTo; +use think\model\relation\BelongsToMany; +use think\model\relation\HasMany; +use think\model\relation\HasManyThrough; +use think\model\relation\HasOne; +use think\model\relation\MorphMany; +use think\model\relation\MorphOne; +use think\model\relation\MorphTo; + +/** + * 模型关联处理 + */ +trait RelationShip +{ + /** + * 父关联模型对象 + * @var object + */ + private $parent; + + /** + * 模型关联数据 + * @var array + */ + private $relation = []; + + /** + * 关联写入定义信息 + * @var array + */ + private $together; + + /** + * 关联自动写入信息 + * @var array + */ + protected $relationWrite; + + /** + * 设置父关联对象 + * @access public + * @param Model $model 模型对象 + * @return $this + */ + public function setParent($model) + { + $this->parent = $model; + + return $this; + } + + /** + * 获取父关联对象 + * @access public + * @return Model + */ + public function getParent() + { + return $this->parent; + } + + /** + * 获取当前模型的关联模型数据 + * @access public + * @param string $name 关联方法名 + * @return mixed + */ + public function getRelation($name = null) + { + if (is_null($name)) { + return $this->relation; + } elseif (array_key_exists($name, $this->relation)) { + return $this->relation[$name]; + } + return; + } + + /** + * 设置关联数据对象值 + * @access public + * @param string $name 属性名 + * @param mixed $value 属性值 + * @param array $data 数据 + * @return $this + */ + public function setRelation($name, $value, $data = []) + { + // 检测修改器 + $method = 'set' . Loader::parseName($name, 1) . 'Attr'; + + if (method_exists($this, $method)) { + $value = $this->$method($value, array_merge($this->data, $data)); + } + + $this->relation[$name] = $value; + + return $this; + } + + /** + * 绑定(一对一)关联属性到当前模型 + * @access protected + * @param string $relation 关联名称 + * @param array $attrs 绑定属性 + * @return $this + * @throws Exception + */ + public function bindAttr($relation, array $attrs = []) + { + $relation = $this->getRelation($relation); + + foreach ($attrs as $key => $attr) { + $key = is_numeric($key) ? $attr : $key; + $value = $this->getOrigin($key); + + if (!is_null($value)) { + throw new Exception('bind attr has exists:' . $key); + } + + $this->setAttr($key, $relation ? $relation->getAttr($attr) : null); + } + + return $this; + } + + /** + * 关联数据写入 + * @access public + * @param array|string $relation 关联 + * @return $this + */ + public function together($relation) + { + if (is_string($relation)) { + $relation = explode(',', $relation); + } + + $this->together = $relation; + + $this->checkAutoRelationWrite(); + + return $this; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $relation 关联方法名 + * @param mixed $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public static function has($relation, $operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + $relation = (new static())->$relation(); + + if (is_array($operator) || $operator instanceof \Closure) { + return $relation->hasWhere($operator); + } + + return $relation->has($operator, $count, $id, $joinType); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $relation 关联方法名 + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + public static function hasWhere($relation, $where = [], $fields = '*') + { + return (new static())->$relation()->hasWhere($where, $fields); + } + + /** + * 查询当前模型的关联数据 + * @access public + * @param string|array $relations 关联名 + * @param array $withRelationAttr 关联获取器 + * @return $this + */ + public function relationQuery($relations, $withRelationAttr = []) + { + if (is_string($relations)) { + $relations = explode(',', $relations); + } + + foreach ($relations as $key => $relation) { + $subRelation = ''; + $closure = null; + + if ($relation instanceof \Closure) { + // 支持闭包查询过滤关联条件 + $closure = $relation; + $relation = $key; + } + + if (is_array($relation)) { + $subRelation = $relation; + $relation = $key; + } elseif (strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation, 2); + } + + $method = Loader::parseName($relation, 1, false); + $relationName = Loader::parseName($relation); + + $relationResult = $this->$method(); + + if (isset($withRelationAttr[$relationName])) { + $relationResult->getQuery()->withAttr($withRelationAttr[$relationName]); + } + + $this->relation[$relation] = $relationResult->getRelation($subRelation, $closure); + } + + return $this; + } + + /** + * 预载入关联查询 返回数据集 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 关联名 + * @param array $withRelationAttr 关联获取器 + * @param bool $join 是否为JOIN方式 + * @return array + */ + public function eagerlyResultSet(&$resultSet, $relation, $withRelationAttr = [], $join = false) + { + $relations = is_string($relation) ? explode(',', $relation) : $relation; + + foreach ($relations as $key => $relation) { + $subRelation = ''; + $closure = null; + + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } + + if (is_array($relation)) { + $subRelation = $relation; + $relation = $key; + } elseif (strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation, 2); + } + + $relation = Loader::parseName($relation, 1, false); + $relationName = Loader::parseName($relation); + + $relationResult = $this->$relation(); + + if (isset($withRelationAttr[$relationName])) { + $relationResult->getQuery()->withAttr($withRelationAttr[$relationName]); + } + + $relationResult->eagerlyResultSet($resultSet, $relation, $subRelation, $closure, $join); + } + } + + /** + * 预载入关联查询 返回模型对象 + * @access public + * @param Model $result 数据对象 + * @param string $relation 关联名 + * @param array $withRelationAttr 关联获取器 + * @param bool $join 是否为JOIN方式 + * @return Model + */ + public function eagerlyResult(&$result, $relation, $withRelationAttr = [], $join = false) + { + $relations = is_string($relation) ? explode(',', $relation) : $relation; + + foreach ($relations as $key => $relation) { + $subRelation = ''; + $closure = null; + + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } + + if (is_array($relation)) { + $subRelation = $relation; + $relation = $key; + } elseif (strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation, 2); + } + + $relation = Loader::parseName($relation, 1, false); + $relationName = Loader::parseName($relation); + + $relationResult = $this->$relation(); + + if (isset($withRelationAttr[$relationName])) { + $relationResult->getQuery()->withAttr($withRelationAttr[$relationName]); + } + + $relationResult->eagerlyResult($result, $relation, $subRelation, $closure, $join); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param array $relations 关联名 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @return void + */ + public function relationCount(&$result, $relations, $aggregate = 'sum', $field = '*') + { + foreach ($relations as $key => $relation) { + $closure = $name = null; + + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } elseif (is_string($key)) { + $name = $relation; + $relation = $key; + } + + $relation = Loader::parseName($relation, 1, false); + + $count = $this->$relation()->relationCount($result, $closure, $aggregate, $field, $name); + + if (empty($name)) { + $name = Loader::parseName($relation) . '_' . $aggregate; + } + + $result->setAttr($name, $count); + } + } + + /** + * HAS ONE 关联定义 + * @access public + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前主键 + * @return HasOne + */ + public function hasOne($model, $foreignKey = '', $localKey = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $localKey = $localKey ?: $this->getPk(); + $foreignKey = $foreignKey ?: $this->getForeignKey($this->name); + + return new HasOne($this, $model, $foreignKey, $localKey); + } + + /** + * BELONGS TO 关联定义 + * @access public + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 关联主键 + * @return BelongsTo + */ + public function belongsTo($model, $foreignKey = '', $localKey = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $foreignKey = $foreignKey ?: $this->getForeignKey((new $model)->getName()); + $localKey = $localKey ?: (new $model)->getPk(); + $trace = debug_backtrace(false, 2); + $relation = Loader::parseName($trace[1]['function']); + + return new BelongsTo($this, $model, $foreignKey, $localKey, $relation); + } + + /** + * HAS MANY 关联定义 + * @access public + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前主键 + * @return HasMany + */ + public function hasMany($model, $foreignKey = '', $localKey = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $localKey = $localKey ?: $this->getPk(); + $foreignKey = $foreignKey ?: $this->getForeignKey($this->name); + + return new HasMany($this, $model, $foreignKey, $localKey); + } + + /** + * HAS MANY 远程关联定义 + * @access public + * @param string $model 模型名 + * @param string $through 中间模型名 + * @param string $foreignKey 关联外键 + * @param string $throughKey 关联外键 + * @param string $localKey 当前主键 + * @return HasManyThrough + */ + public function hasManyThrough($model, $through, $foreignKey = '', $throughKey = '', $localKey = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $through = $this->parseModel($through); + $localKey = $localKey ?: $this->getPk(); + $foreignKey = $foreignKey ?: $this->getForeignKey($this->name); + $throughKey = $throughKey ?: $this->getForeignKey((new $through)->getName()); + + return new HasManyThrough($this, $model, $through, $foreignKey, $throughKey, $localKey); + } + + /** + * BELONGS TO MANY 关联定义 + * @access public + * @param string $model 模型名 + * @param string $table 中间表名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前模型关联键 + * @return BelongsToMany + */ + public function belongsToMany($model, $table = '', $foreignKey = '', $localKey = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $name = Loader::parseName(basename(str_replace('\\', '/', $model))); + $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); + } + + /** + * MORPH One 关联定义 + * @access public + * @param string $model 模型名 + * @param string|array $morph 多态字段信息 + * @param string $type 多态类型 + * @return MorphOne + */ + public function morphOne($model, $morph = null, $type = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + + if (is_null($morph)) { + $trace = debug_backtrace(false, 2); + $morph = Loader::parseName($trace[1]['function']); + } + + if (is_array($morph)) { + list($morphType, $foreignKey) = $morph; + } else { + $morphType = $morph . '_type'; + $foreignKey = $morph . '_id'; + } + + $type = $type ?: get_class($this); + + return new MorphOne($this, $model, $foreignKey, $morphType, $type); + } + + /** + * MORPH MANY 关联定义 + * @access public + * @param string $model 模型名 + * @param string|array $morph 多态字段信息 + * @param string $type 多态类型 + * @return MorphMany + */ + public function morphMany($model, $morph = null, $type = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + + if (is_null($morph)) { + $trace = debug_backtrace(false, 2); + $morph = Loader::parseName($trace[1]['function']); + } + + $type = $type ?: get_class($this); + + if (is_array($morph)) { + list($morphType, $foreignKey) = $morph; + } else { + $morphType = $morph . '_type'; + $foreignKey = $morph . '_id'; + } + + return new MorphMany($this, $model, $foreignKey, $morphType, $type); + } + + /** + * MORPH TO 关联定义 + * @access public + * @param string|array $morph 多态字段信息 + * @param array $alias 多态别名定义 + * @return MorphTo + */ + public function morphTo($morph = null, $alias = []) + { + $trace = debug_backtrace(false, 2); + $relation = Loader::parseName($trace[1]['function']); + + if (is_null($morph)) { + $morph = $relation; + } + + // 记录当前关联信息 + if (is_array($morph)) { + list($morphType, $foreignKey) = $morph; + } else { + $morphType = $morph . '_type'; + $foreignKey = $morph . '_id'; + } + + return new MorphTo($this, $morphType, $foreignKey, $alias, $relation); + } + + /** + * 解析模型的完整命名空间 + * @access protected + * @param string $model 模型名(或者完整类名) + * @return string + */ + protected function parseModel($model) + { + if (false === strpos($model, '\\')) { + $path = explode('\\', static::class); + array_pop($path); + array_push($path, Loader::parseName($model, 1)); + $model = implode('\\', $path); + } + + return $model; + } + + /** + * 获取模型的默认外键名 + * @access protected + * @param string $name 模型名 + * @return string + */ + protected function getForeignKey($name) + { + if (strpos($name, '\\')) { + $name = basename(str_replace('\\', '/', $name)); + } + + return Loader::parseName($name) . '_id'; + } + + /** + * 检查属性是否为关联属性 如果是则返回关联方法名 + * @access protected + * @param string $attr 关联属性名 + * @return string|false + */ + protected function isRelationAttr($attr) + { + $relation = Loader::parseName($attr, 1, false); + + if (method_exists($this, $relation) && !method_exists('think\Model', $relation)) { + return $relation; + } + + return false; + } + + /** + * 智能获取关联模型数据 + * @access protected + * @param Relation $modelRelation 模型关联对象 + * @return mixed + */ + protected function getRelationData(Relation $modelRelation) + { + if ($this->parent && !$modelRelation->isSelfRelation() && get_class($this->parent) == get_class($modelRelation->getModel())) { + $value = $this->parent; + } else { + // 获取关联数据 + $value = $modelRelation->getRelation(); + } + + return $value; + } + + /** + * 关联数据自动写入检查 + * @access protected + * @return void + */ + protected function checkAutoRelationWrite() + { + foreach ($this->together as $key => $name) { + if (is_array($name)) { + if (key($name) === 0) { + $this->relationWrite[$key] = []; + // 绑定关联属性 + foreach ((array) $name as $val) { + if (isset($this->data[$val])) { + $this->relationWrite[$key][$val] = $this->data[$val]; + } + } + } else { + // 直接传入关联数据 + $this->relationWrite[$key] = $name; + } + } elseif (isset($this->relation[$name])) { + $this->relationWrite[$name] = $this->relation[$name]; + } elseif (isset($this->data[$name])) { + $this->relationWrite[$name] = $this->data[$name]; + unset($this->data[$name]); + } + } + } + + /** + * 自动关联数据更新(针对一对一关联) + * @access protected + * @return void + */ + protected function autoRelationUpdate() + { + foreach ($this->relationWrite as $name => $val) { + if ($val instanceof Model) { + $val->isUpdate()->save(); + } else { + $model = $this->getRelation($name); + if ($model instanceof Model) { + $model->isUpdate()->save($val); + } + } + } + } + + /** + * 自动关联数据写入(针对一对一关联) + * @access protected + * @return void + */ + protected function autoRelationInsert() + { + foreach ($this->relationWrite as $name => $val) { + $method = Loader::parseName($name, 1, false); + $this->$method()->save($val); + } + } + + /** + * 自动关联数据删除(支持一对一及一对多关联) + * @access protected + * @return void + */ + protected function autoRelationDelete() + { + 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) { + foreach ($result as $model) { + $model->delete(); + } + } + } + } +} diff --git a/thinkphp/library/think/model/concern/SoftDelete.php b/thinkphp/library/think/model/concern/SoftDelete.php new file mode 100644 index 0000000..ec866ac --- /dev/null +++ b/thinkphp/library/think/model/concern/SoftDelete.php @@ -0,0 +1,246 @@ +getDeleteTimeField(); + + if ($field && !empty($this->getOrigin($field))) { + return true; + } + + return false; + } + + /** + * 查询软删除数据 + * @access public + * @return Query + */ + public static function withTrashed() + { + $model = new static(); + + return $model->withTrashedData(true)->db(false); + } + + /** + * 是否包含软删除数据 + * @access protected + * @param bool $withTrashed 是否包含软删除数据 + * @return $this + */ + protected function withTrashedData($withTrashed) + { + $this->withTrashed = $withTrashed; + return $this; + } + + /** + * 只查询软删除数据 + * @access public + * @return Query + */ + public static function onlyTrashed() + { + $model = new static(); + $field = $model->getDeleteTimeField(true); + + if ($field) { + return $model + ->db(false) + ->useSoftDelete($field, $model->getWithTrashedExp()); + } + + return $model->db(false); + } + + /** + * 获取软删除数据的查询条件 + * @access protected + * @return array + */ + protected function getWithTrashedExp() + { + return is_null($this->defaultSoftDelete) ? + ['notnull', ''] : ['<>', $this->defaultSoftDelete]; + } + + /** + * 删除当前的记录 + * @access public + * @return bool + */ + public function delete($force = false) + { + if (!$this->isExists() || false === $this->trigger('before_delete', $this)) { + return false; + } + + $force = $force ?: $this->isForce(); + $name = $this->getDeleteTimeField(); + + if ($name && !$force) { + // 软删除 + $this->data($name, $this->autoWriteTimestamp($name)); + + $result = $this->isUpdate()->withEvent(false)->save(); + + $this->withEvent(true); + } else { + // 读取更新条件 + $where = $this->getWhere(); + + // 删除当前模型数据 + $result = $this->db(false) + ->where($where) + ->removeOption('soft_delete') + ->delete(); + } + + // 关联删除 + if (!empty($this->relationWrite)) { + $this->autoRelationDelete(); + } + + $this->trigger('after_delete', $this); + + $this->exists(false); + + return true; + } + + /** + * 删除记录 + * @access public + * @param mixed $data 主键列表 支持闭包查询条件 + * @param bool $force 是否强制删除 + * @return bool + */ + public static function destroy($data, $force = false) + { + // 传入空不执行删除,但是0可以删除 + if (empty($data) && 0 !== $data) { + return false; + } + // 包含软删除数据 + $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 false; + } + + $resultSet = $query->select($data); + + if ($resultSet) { + foreach ($resultSet as $data) { + $data->force($force)->delete(); + } + } + + return true; + } + + /** + * 恢复被软删除的记录 + * @access public + * @param array $where 更新条件 + * @return bool + */ + public function restore($where = []) + { + $name = $this->getDeleteTimeField(); + + if ($name) { + if (false === $this->trigger('before_restore')) { + return false; + } + + if (empty($where)) { + $pk = $this->getPk(); + + $where[] = [$pk, '=', $this->getData($pk)]; + } + + // 恢复删除 + $this->db(false) + ->where($where) + ->useSoftDelete($name, $this->getWithTrashedExp()) + ->update([$name => $this->defaultSoftDelete]); + + $this->trigger('after_restore'); + + return true; + } + + return false; + } + + /** + * 获取软删除字段 + * @access protected + * @param bool $read 是否查询操作 写操作的时候会自动去掉表别名 + * @return string|false + */ + protected function getDeleteTimeField($read = false) + { + $field = property_exists($this, 'deleteTime') && isset($this->deleteTime) ? $this->deleteTime : 'delete_time'; + + if (false === $field) { + return false; + } + + if (false === strpos($field, '.')) { + $field = '__TABLE__.' . $field; + } + + if (!$read && strpos($field, '.')) { + $array = explode('.', $field); + $field = array_pop($array); + } + + return $field; + } + + /** + * 查询的时候默认排除软删除数据 + * @access protected + * @param Query $query + * @return void + */ + protected function withNoTrashed($query) + { + $field = $this->getDeleteTimeField(true); + + if ($field) { + $condition = is_null($this->defaultSoftDelete) ? ['null', ''] : ['=', $this->defaultSoftDelete]; + $query->useSoftDelete($field, $condition); + } + } +} diff --git a/thinkphp/library/think/model/concern/TimeStamp.php b/thinkphp/library/think/model/concern/TimeStamp.php new file mode 100644 index 0000000..99a31fa --- /dev/null +++ b/thinkphp/library/think/model/concern/TimeStamp.php @@ -0,0 +1,92 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\concern; + +use DateTime; + +/** + * 自动时间戳 + */ +trait TimeStamp +{ + /** + * 是否需要自动写入时间戳 如果设置为字符串 则表示时间字段的类型 + * @var bool|string + */ + protected $autoWriteTimestamp; + + /** + * 创建时间字段 false表示关闭 + * @var false|string + */ + protected $createTime = 'create_time'; + + /** + * 更新时间字段 false表示关闭 + * @var false|string + */ + protected $updateTime = 'update_time'; + + /** + * 时间字段显示格式 + * @var string + */ + protected $dateFormat; + + /** + * 时间日期字段格式化处理 + * @access protected + * @param mixed $format 日期格式 + * @param mixed $time 时间日期表达式 + * @param bool $timestamp 是否进行时间戳转换 + * @return mixed + */ + protected function formatDateTime($format, $time = 'now', $timestamp = false) + { + if (empty($time)) { + return; + } + + if (false === $format) { + return $time; + } elseif (false !== strpos($format, '\\')) { + return new $format($time); + } + + if ($timestamp) { + $dateTime = new DateTime(); + $dateTime->setTimestamp($time); + } else { + $dateTime = new DateTime($time); + } + + return $dateTime->format($format); + } + + /** + * 检查时间字段写入 + * @access protected + * @return void + */ + protected function checkTimeStampWrite() + { + // 自动写入创建时间和更新时间 + if ($this->autoWriteTimestamp) { + if ($this->createTime && !isset($this->data[$this->createTime])) { + $this->data[$this->createTime] = $this->autoWriteTimestamp($this->createTime); + } + if ($this->updateTime && !isset($this->data[$this->updateTime])) { + $this->data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime); + } + } + } +} diff --git a/thinkphp/library/think/model/relation/BelongsTo.php b/thinkphp/library/think/model/relation/BelongsTo.php new file mode 100644 index 0000000..056c7d7 --- /dev/null +++ b/thinkphp/library/think/model/relation/BelongsTo.php @@ -0,0 +1,323 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\Loader; +use think\Model; + +class BelongsTo extends OneToOne +{ + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 关联主键 + * @param string $relation 关联名 + */ + public function __construct(Model $parent, $model, $foreignKey, $localKey, $relation = null) + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + $this->joinType = 'INNER'; + $this->query = (new $model)->db(); + $this->relation = $relation; + + if (get_class($parent) == $model) { + $this->selfRelation = true; + } + } + + /** + * 延迟获取关联数据 + * @access public + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return Model + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure instanceof Closure) { + $closure($this->query); + } + + $foreignKey = $this->foreignKey; + + $relationModel = $this->query + ->removeWhereField($this->localKey) + ->where($this->localKey, $this->parent->$foreignKey) + ->relation($subRelation) + ->find(); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } + + return $relationModel; + } + + /** + * 创建关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $aggregateAlias 聚合字段别名 + * @return string + */ + public function getRelationCountQuery($closure, $aggregate = 'count', $field = '*', &$aggregateAlias = '') + { + if ($closure instanceof Closure) { + $return = $closure($this->query); + + if ($return && is_string($return)) { + $aggregateAlias = $return; + } + } + + return $this->query + ->whereExp($this->localKey, '=' . $this->parent->getTable() . '.' . $this->foreignKey) + ->fetchSql() + ->$aggregate($field); + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount($result, $closure, $aggregate = 'count', $field = '*', &$name = '') + { + $foreignKey = $this->foreignKey; + + if (!isset($result->$foreignKey)) { + return 0; + } + + if ($closure instanceof Closure) { + $return = $closure($this->query); + + if ($return && is_string($return)) { + $name = $return; + } + } + + return $this->query + ->where($this->localKey, '=', $result->$foreignKey) + ->$aggregate($field); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + $table = $this->query->getTable(); + $model = basename(str_replace('\\', '/', get_class($this->parent))); + $relation = basename(str_replace('\\', '/', $this->model)); + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + $softDelete = $this->query->getOptions('soft_delete'); + + return $this->parent->db() + ->alias($model) + ->whereExists(function ($query) use ($table, $model, $relation, $localKey, $foreignKey) { + $query->table([$table => $relation]) + ->field($relation . '.' . $localKey) + ->whereExp($model . '.' . $foreignKey, '=' . $relation . '.' . $localKey) + ->when($softDelete, function ($query) use ($softDelete, $relation) { + $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }); + }); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + 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)) { + $this->getQueryWhere($where, $relation); + } + + $fields = $this->getRelationQueryFields($fields, $model); + $softDelete = $this->query->getOptions('soft_delete'); + + return $this->parent->db() + ->alias($model) + ->field($fields) + ->join([$table => $relation], $model . '.' . $this->foreignKey . '=' . $relation . '.' . $this->localKey, $this->joinType) + ->when($softDelete, function ($query) use ($softDelete, $relation) { + $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }) + ->where($where); + } + + /** + * 预载入关联查询(数据集) + * @access protected + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$foreignKey)) { + $range[] = $result->$foreignKey; + } + } + + if (!empty($range)) { + $this->query->removeWhereField($localKey); + + $data = $this->eagerlyWhere([ + [$localKey, 'in', $range], + ], $localKey, $relation, $subRelation, $closure); + + // 关联属性名 + $attr = Loader::parseName($relation); + + // 关联数据封装 + foreach ($resultSet as $result) { + // 关联模型 + if (!isset($data[$result->$foreignKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$foreignKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($relationModel, $result); + } else { + // 设置关联属性 + $result->setRelation($attr, $relationModel); + } + } + } + } + + /** + * 预载入关联查询(数据) + * @access protected + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + protected function eagerlyOne(&$result, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $this->query->removeWhereField($localKey); + + $data = $this->eagerlyWhere([ + [$localKey, '=', $result->$foreignKey], + ], $localKey, $relation, $subRelation, $closure); + + // 关联模型 + if (!isset($data[$result->$foreignKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$foreignKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($relationModel, $result); + } else { + // 设置关联属性 + $result->setRelation(Loader::parseName($relation), $relationModel); + } + } + + /** + * 添加关联数据 + * @access public + * @param Model $model 关联模型对象 + * @return Model + */ + public function associate($model) + { + $this->parent->setAttr($this->foreignKey, $model->getKey()); + $this->parent->save(); + + return $this->parent->setRelation($this->relation, $model); + } + + /** + * 注销关联数据 + * @access public + * @return Model + */ + public function dissociate() + { + $this->parent->setAttr($this->foreignKey, null); + $this->parent->save(); + + 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 new file mode 100644 index 0000000..6105e23 --- /dev/null +++ b/thinkphp/library/think/model/relation/BelongsToMany.php @@ -0,0 +1,712 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\Collection; +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Pivot; +use think\model\Relation; +use think\Paginator; + +class BelongsToMany extends Relation +{ + // 中间表表名 + protected $middle; + // 中间表模型名称 + protected $pivotName; + // 中间表数据名称 + protected $pivotDataName = 'pivot'; + // 中间表模型对象 + protected $pivot; + + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $table 中间表名 + * @param string $foreignKey 关联模型外键 + * @param string $localKey 当前模型关联键 + */ + public function __construct(Model $parent, $model, $table, $foreignKey, $localKey) + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + + if (false !== strpos($table, '\\')) { + $this->pivotName = $table; + $this->middle = basename(str_replace('\\', '/', $table)); + } else { + $this->middle = $table; + } + + $this->query = (new $model)->db(); + $this->pivot = $this->newPivot(); + } + + /** + * 设置中间表模型 + * @access public + * @param $pivot + * @return $this + */ + public function pivot($pivot) + { + $this->pivotName = $pivot; + return $this; + } + + /** + * 设置中间表数据名称 + * @access public + * @param string $name + * @return $this + */ + public function pivotDataName($name) + { + $this->pivotDataName = $name; + return $this; + } + + /** + * 获取中间表更新条件 + * @param $data + * @return array + */ + protected function getUpdateWhere($data) + { + return [ + $this->localKey => $data[$this->localKey], + $this->foreignKey => $data[$this->foreignKey], + ]; + } + + /** + * 实例化中间表模型 + * @access public + * @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; + } + + throw new Exception('pivot model must extends: \think\model\Pivot'); + } + + /** + * 合成中间表模型 + * @access protected + * @param array|Collection|Paginator $models + */ + protected function hydratePivot($models) + { + foreach ($models as $model) { + $pivot = []; + + foreach ($model->getData() as $key => $val) { + if (strpos($key, '__')) { + list($name, $attr) = explode('__', $key, 2); + + if ('pivot' == $name) { + $pivot[$attr] = $val; + unset($model->$key); + } + } + } + + $model->setRelation($this->pivotDataName, $this->newPivot($pivot, true)); + } + } + + /** + * 创建关联查询Query对象 + * @access protected + * @return Query + */ + protected function buildQuery() + { + $foreignKey = $this->foreignKey; + $localKey = $this->localKey; + + // 关联查询 + $pk = $this->parent->getPk(); + + $condition[] = ['pivot.' . $localKey, '=', $this->parent->$pk]; + + return $this->belongsToManyQuery($foreignKey, $localKey, $condition); + } + + /** + * 延迟获取关联数据 + * @access public + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure instanceof Closure) { + $closure($this->query); + } + + $result = $this->buildQuery()->relation($subRelation)->select(); + $this->hydratePivot($result); + + return $result; + } + + /** + * 重载select方法 + * @access public + * @param mixed $data + * @return Collection + */ + public function select($data = null) + { + $result = $this->buildQuery()->select($data); + $this->hydratePivot($result); + + return $result; + } + + /** + * 重载paginate方法 + * @access public + * @param null $listRows + * @param bool $simple + * @param array $config + * @return Paginator + */ + public function paginate($listRows = null, $simple = false, $config = []) + { + $result = $this->buildQuery()->paginate($listRows, $simple, $config); + $this->hydratePivot($result); + + return $result; + } + + /** + * 重载find方法 + * @access public + * @param mixed $data + * @return Model + */ + public function find($data = null) + { + $result = $this->buildQuery()->find($data); + if ($result) { + $this->hydratePivot([$result]); + } + + return $result; + } + + /** + * 查找多条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return Collection + */ + public function selectOrFail($data = null) + { + return $this->failException(true)->select($data); + } + + /** + * 查找单条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return Model + */ + public function findOrFail($data = null) + { + return $this->failException(true)->find($data); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + * @throws Exception + */ + public function hasWhere($where = [], $fields = null) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 设置中间表的查询条件 + * @access public + * @param string $field + * @param string $op + * @param mixed $condition + * @return $this + */ + public function wherePivot($field, $op = null, $condition = null) + { + $this->query->where('pivot.' . $field, $op, $condition); + return $this; + } + + /** + * 预载入关联查询(数据集) + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $pk = $resultSet[0]->getPk(); + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$pk)) { + $range[] = $result->$pk; + } + } + + if (!empty($range)) { + // 查询关联数据 + $data = $this->eagerlyManyToMany([ + ['pivot.' . $localKey, 'in', $range], + ], $relation, $subRelation, $closure); + + // 关联属性名 + $attr = Loader::parseName($relation); + + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$pk])) { + $data[$result->$pk] = []; + } + + $result->setRelation($attr, $this->resultSetBuild($data[$result->$pk])); + } + } + } + + /** + * 预载入关联查询(单个数据) + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $pk = $result->getPk(); + + if (isset($result->$pk)) { + $pk = $result->$pk; + // 查询管理数据 + $data = $this->eagerlyManyToMany([ + ['pivot.' . $this->localKey, '=', $pk], + ], $relation, $subRelation, $closure); + + // 关联数据封装 + if (!isset($data[$pk])) { + $data[$pk] = []; + } + + $result->setRelation(Loader::parseName($relation), $this->resultSetBuild($data[$pk])); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount($result, $closure, $aggregate = 'count', $field = '*', &$name = '') + { + $pk = $result->getPk(); + + if (!isset($result->$pk)) { + return 0; + } + + $pk = $result->$pk; + + if ($closure instanceof Closure) { + $return = $closure($this->query); + + if ($return && is_string($return)) { + $name = $return; + } + } + + return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [ + ['pivot.' . $this->localKey, '=', $pk], + ])->$aggregate($field); + } + + /** + * 获取关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $aggregateAlias 聚合字段别名 + * @return array + */ + public function getRelationCountQuery($closure, $aggregate = 'count', $field = '*', &$aggregateAlias = '') + { + if ($closure instanceof Closure) { + $return = $closure($this->query); + + if ($return && is_string($return)) { + $aggregateAlias = $return; + } + } + + return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [ + [ + 'pivot.' . $this->localKey, 'exp', $this->query->raw('=' . $this->parent->getTable() . '.' . $this->parent->getPk()), + ], + ])->fetchSql()->$aggregate($field); + } + + /** + * 多对多 关联模型预查询 + * @access protected + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param \Closure $closure 闭包 + * @return array + */ + protected function eagerlyManyToMany($where, $relation, $subRelation = '', $closure = null) + { + // 预载入关联查询 支持嵌套预载入 + if ($closure instanceof Closure) { + $closure($this->query); + } + + $list = $this->belongsToManyQuery($this->foreignKey, $this->localKey, $where) + ->with($subRelation) + ->select(); + + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $pivot = []; + foreach ($set->getData() as $key => $val) { + if (strpos($key, '__')) { + list($name, $attr) = explode('__', $key, 2); + if ('pivot' == $name) { + $pivot[$attr] = $val; + unset($set->$key); + } + } + } + + $set->setRelation($this->pivotDataName, $this->newPivot($pivot, true)); + + $data[$pivot[$this->localKey]][] = $set; + } + + return $data; + } + + /** + * BELONGS TO MANY 关联查询 + * @access protected + * @param string $foreignKey 关联模型关联键 + * @param string $localKey 当前模型关联键 + * @param array $condition 关联查询条件 + * @return Query + */ + protected function belongsToManyQuery($foreignKey, $localKey, $condition = []) + { + // 关联查询封装 + $tableName = $this->query->getTable(); + $table = $this->pivot->getTable(); + $fields = $this->getQueryFields($tableName); + + $query = $this->query + ->field($fields) + ->field(true, false, $table, 'pivot', 'pivot__'); + + if (empty($this->baseQuery)) { + $relationFk = $this->query->getPk(); + $query->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk) + ->where($condition); + } + + return $query; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @param array $pivot 中间表额外数据 + * @return array|Pivot + */ + public function save($data, array $pivot = []) + { + // 保存关联表/中间表数据 + return $this->attach($data, $pivot); + } + + /** + * 批量保存当前关联数据对象 + * @access public + * @param array $dataSet 数据集 + * @param array $pivot 中间表额外数据 + * @param bool $samePivot 额外数据是否相同 + * @return array|false + */ + public function saveAll(array $dataSet, array $pivot = [], $samePivot = false) + { + $result = []; + + foreach ($dataSet as $key => $data) { + if (!$samePivot) { + $pivotData = isset($pivot[$key]) ? $pivot[$key] : []; + } else { + $pivotData = $pivot; + } + + $result[] = $this->attach($data, $pivotData); + } + + return empty($result) ? false : $result; + } + + /** + * 附加关联的一个中间表数据 + * @access public + * @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键 + * @param array $pivot 中间表额外数据 + * @return array|Pivot + * @throws Exception + */ + public function attach($data, $pivot = []) + { + if (is_array($data)) { + if (key($data) === 0) { + $id = $data; + } else { + // 保存关联表数据 + $model = new $this->model; + $id = $model->insertGetId($data); + } + } elseif (is_numeric($data) || is_string($data)) { + // 根据关联表主键直接写入中间表 + $id = $data; + } elseif ($data instanceof Model) { + // 根据关联表主键直接写入中间表 + $relationFk = $data->getPk(); + $id = $data->$relationFk; + } + + if ($id) { + // 保存中间表数据 + $pk = $this->parent->getPk(); + $pivot[$this->localKey] = $this->parent->$pk; + $ids = (array) $id; + + foreach ($ids as $id) { + $pivot[$this->foreignKey] = $id; + $this->pivot->replace() + ->exists(false) + ->data([]) + ->save($pivot); + $result[] = $this->newPivot($pivot, true); + } + + if (count($result) == 1) { + // 返回中间表模型对象 + $result = $result[0]; + } + + return $result; + } else { + throw new Exception('miss relation data'); + } + } + + /** + * 判断是否存在关联数据 + * @access public + * @param mixed $data 数据 可以使用关联模型对象 或者 关联对象的主键 + * @return Pivot|false + * @throws Exception + */ + public function attached($data) + { + if ($data instanceof Model) { + $id = $data->getKey(); + } else { + $id = $data; + } + + $pivot = $this->pivot + ->where($this->localKey, $this->parent->getKey()) + ->where($this->foreignKey, $id) + ->find(); + + return $pivot ?: false; + } + + /** + * 解除关联的一个中间表数据 + * @access public + * @param integer|array $data 数据 可以使用关联对象的主键 + * @param bool $relationDel 是否同时删除关联表数据 + * @return integer + */ + public function detach($data = null, $relationDel = false) + { + if (is_array($data)) { + $id = $data; + } elseif (is_numeric($data) || is_string($data)) { + // 根据关联表主键直接写入中间表 + $id = $data; + } elseif ($data instanceof Model) { + // 根据关联表主键直接写入中间表 + $relationFk = $data->getPk(); + $id = $data->$relationFk; + } + + // 删除中间表数据 + $pk = $this->parent->getPk(); + $pivot[] = [$this->localKey, '=', $this->parent->$pk]; + + if (isset($id)) { + $pivot[] = [$this->foreignKey, is_array($id) ? 'in' : '=', $id]; + } + + $result = $this->pivot->where($pivot)->delete(); + + // 删除关联表数据 + if (isset($id) && $relationDel) { + $model = $this->model; + $model::destroy($id); + } + + return $result; + } + + /** + * 数据同步 + * @access public + * @param array $ids + * @param bool $detaching + * @return array + */ + public function sync($ids, $detaching = true) + { + $changes = [ + 'attached' => [], + 'detached' => [], + 'updated' => [], + ]; + + $pk = $this->parent->getPk(); + + $current = $this->pivot + ->where($this->localKey, $this->parent->$pk) + ->column($this->foreignKey); + + $records = []; + + foreach ($ids as $key => $value) { + if (!is_array($value)) { + $records[$value] = []; + } else { + $records[$key] = $value; + } + } + + $detach = array_diff($current, array_keys($records)); + + if ($detaching && count($detach) > 0) { + $this->detach($detach); + $changes['detached'] = $detach; + } + + foreach ($records as $id => $attributes) { + if (!in_array($id, $current)) { + $this->attach($id, $attributes); + $changes['attached'][] = $id; + } elseif (count($attributes) > 0 && $this->attach($id, $attributes)) { + $changes['updated'][] = $id; + } + } + + return $changes; + } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + 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->baseQuery = true; + } + } + +} diff --git a/thinkphp/library/think/model/relation/HasMany.php b/thinkphp/library/think/model/relation/HasMany.php new file mode 100644 index 0000000..e4df5c4 --- /dev/null +++ b/thinkphp/library/think/model/relation/HasMany.php @@ -0,0 +1,360 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\db\Query; +use think\Loader; +use think\Model; +use think\model\Relation; + +class HasMany extends Relation +{ + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前模型主键 + */ + public function __construct(Model $parent, $model, $foreignKey, $localKey) + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + $this->query = (new $model)->db(); + + if (get_class($parent) == $model) { + $this->selfRelation = true; + } + } + + /** + * 延迟获取关联数据 + * @access public + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return \think\Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure instanceof Closure) { + $closure($this->query); + } + + $list = $this->query + ->where($this->foreignKey, $this->parent->{$this->localKey}) + ->relation($subRelation) + ->select(); + + $parent = clone $this->parent; + + foreach ($list as &$model) { + $model->setParent($parent); + } + + return $list; + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $range = []; + + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$localKey)) { + $range[] = $result->$localKey; + } + } + + if (!empty($range)) { + $where = [ + [$this->foreignKey, 'in', $range], + ]; + $data = $this->eagerlyOneToMany($where, $relation, $subRelation, $closure); + + // 关联属性名 + $attr = Loader::parseName($relation); + + // 关联数据封装 + foreach ($resultSet as $result) { + $pk = $result->$localKey; + if (!isset($data[$pk])) { + $data[$pk] = []; + } + + foreach ($data[$pk] as &$relationModel) { + $relationModel->setParent(clone $result); + } + + $result->setRelation($attr, $this->resultSetBuild($data[$pk])); + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + + if (isset($result->$localKey)) { + $pk = $result->$localKey; + $where = [ + [$this->foreignKey, '=', $pk], + ]; + $data = $this->eagerlyOneToMany($where, $relation, $subRelation, $closure); + + // 关联数据封装 + if (!isset($data[$pk])) { + $data[$pk] = []; + } + + foreach ($data[$pk] as &$relationModel) { + $relationModel->setParent(clone $result); + } + + $result->setRelation(Loader::parseName($relation), $this->resultSetBuild($data[$pk])); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount($result, $closure, $aggregate = 'count', $field = '*', &$name = '') + { + $localKey = $this->localKey; + + if (!isset($result->$localKey)) { + return 0; + } + + if ($closure instanceof Closure) { + $return = $closure($this->query); + if ($return && is_string($return)) { + $name = $return; + } + } + + return $this->query + ->where($this->foreignKey, '=', $result->$localKey) + ->$aggregate($field); + } + + /** + * 创建关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $aggregateAlias 聚合字段别名 + * @return string + */ + public function getRelationCountQuery($closure, $aggregate = 'count', $field = '*', &$aggregateAlias = '') + { + if ($closure instanceof Closure) { + $return = $closure($this->query); + + if ($return && is_string($return)) { + $aggregateAlias = $return; + } + } + + return $this->query->alias($aggregate . '_table') + ->whereExp($aggregate . '_table.' . $this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey) + ->fetchSql() + ->$aggregate($field); + } + + /** + * 一对多 关联模型预查询 + * @access public + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param \Closure $closure + * @return array + */ + protected function eagerlyOneToMany($where, $relation, $subRelation = '', $closure = null) + { + $foreignKey = $this->foreignKey; + + $this->query->removeWhereField($this->foreignKey); + + // 预载入关联查询 支持嵌套预载入 + if ($closure instanceof Closure) { + $closure($this->query); + } + + $list = $this->query->where($where)->with($subRelation)->select(); + + // 组装模型数据 + $data = []; + + foreach ($list as $set) { + $data[$set->$foreignKey][] = $set; + } + + return $data; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @param boolean $replace 是否自动识别更新和写入 + * @return Model|false + */ + public function save($data, $replace = true) + { + $model = $this->make(); + + return $model->replace($replace)->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 + * @param array|\think\Collection $dataSet 数据集 + * @param boolean $replace 是否自动识别更新和写入 + * @return array|false + */ + public function saveAll($dataSet, $replace = true) + { + $result = []; + + foreach ($dataSet as $key => $data) { + $result[] = $this->save($data, $replace); + } + + return empty($result) ? false : $result; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + $table = $this->query->getTable(); + $model = basename(str_replace('\\', '/', get_class($this->parent))); + $relation = basename(str_replace('\\', '/', $this->model)); + $softDelete = $this->query->getOptions('soft_delete'); + + return $this->parent->db() + ->alias($model) + ->field($model . '.*') + ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $joinType) + ->when($softDelete, function ($query) use ($softDelete, $relation) { + $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }) + ->group($relation . '.' . $this->foreignKey) + ->having('count(' . $id . ')' . $operator . $count); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + 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)) { + $this->getQueryWhere($where, $relation); + } + + $fields = $this->getRelationQueryFields($fields, $model); + $softDelete = $this->query->getOptions('soft_delete'); + + return $this->parent->db() + ->alias($model) + ->group($model . '.' . $this->localKey) + ->field($fields) + ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey) + ->when($softDelete, function ($query) use ($softDelete, $relation) { + $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }) + ->where($where); + } + + /** + * 执行基础查询(仅执行一次) + * @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/HasManyThrough.php b/thinkphp/library/think/model/relation/HasManyThrough.php new file mode 100644 index 0000000..be0b0cd --- /dev/null +++ b/thinkphp/library/think/model/relation/HasManyThrough.php @@ -0,0 +1,363 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\db\Query; +use think\Loader; +use think\Model; +use think\model\Relation; + +class HasManyThrough extends Relation +{ + // 中间关联表外键 + protected $throughKey; + // 中间表模型 + protected $through; + + /** + * 中间主键 + * @var string + */ + protected $throughPk; + + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $through 中间模型名 + * @param string $foreignKey 关联外键 + * @param string $throughKey 关联外键 + * @param string $localKey 当前主键 + */ + public function __construct(Model $parent, $model, $through, $foreignKey, $throughKey, $localKey) + { + $this->parent = $parent; + $this->model = $model; + $this->through = (new $through)->db(); + $this->foreignKey = $foreignKey; + $this->throughKey = $throughKey; + $this->throughPk = $this->through->getPk(); + $this->localKey = $localKey; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @access public + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return \think\Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure instanceof Closure) { + $closure($this->query); + } + + $this->baseQuery(); + + return $this->query->relation($subRelation)->select(); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + $model = Loader::parseName(basename(str_replace('\\', '/', get_class($this->parent)))); + $throughTable = $this->through->getTable(); + $pk = $this->throughPk; + $throughKey = $this->throughKey; + $relation = (new $this->model)->db(); + $relationTable = $relation->getTable(); + $softDelete = $this->query->getOptions('soft_delete'); + + if ('*' != $id) { + $id = $relationTable . '.' . $relation->getPk(); + } + + return $this->parent->db() + ->alias($model) + ->field($model . '.*') + ->join($throughTable, $throughTable . '.' . $this->foreignKey . '=' . $model . '.' . $this->localKey) + ->join($relationTable, $relationTable . '.' . $throughKey . '=' . $throughTable . '.' . $this->throughPk) + ->when($softDelete, function ($query) use ($softDelete, $relationTable) { + $query->where($relationTable . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }) + ->group($relationTable . '.' . $this->throughKey) + ->having('count(' . $id . ')' . $operator . $count); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + public function hasWhere($where = [], $fields = null) + { + $model = Loader::parseName(basename(str_replace('\\', '/', get_class($this->parent)))); + $throughTable = $this->through->getTable(); + $pk = $this->throughPk; + $throughKey = $this->throughKey; + $modelTable = (new $this->model)->db()->getTable(); + + if (is_array($where)) { + $this->getQueryWhere($where, $modelTable); + } + + $fields = $this->getRelationQueryFields($fields, $model); + $softDelete = $this->query->getOptions('soft_delete'); + + return $this->parent->db() + ->alias($model) + ->join($throughTable, $throughTable . '.' . $this->foreignKey . '=' . $model . '.' . $this->localKey) + ->join($modelTable, $modelTable . '.' . $throughKey . '=' . $throughTable . '.' . $this->throughPk) + ->when($softDelete, function ($query) use ($softDelete, $modelTable) { + $query->where($modelTable . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }) + ->group($modelTable . '.' . $this->throughKey) + ->where($where) + ->field($fields); + } + + /** + * 预载入关联查询(数据集) + * @access protected + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param mixed $subRelation 子关联名 + * @param Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(array &$resultSet, $relation, $subRelation = '', $closure = null) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$localKey)) { + $range[] = $result->$localKey; + } + } + + if (!empty($range)) { + $this->query->removeWhereField($foreignKey); + + $data = $this->eagerlyWhere([ + [$this->foreignKey, 'in', $range], + ], $foreignKey, $relation, $subRelation, $closure); + + // 关联属性名 + $attr = Loader::parseName($relation); + + // 关联数据封装 + foreach ($resultSet as $result) { + $pk = $result->$localKey; + if (!isset($data[$pk])) { + $data[$pk] = []; + } + + foreach ($data[$pk] as &$relationModel) { + $relationModel->setParent(clone $result); + } + + // 设置关联属性 + $result->setRelation($attr, $this->resultSetBuild($data[$pk])); + } + } + } + + /** + * 预载入关联查询(数据) + * @access protected + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param mixed $subRelation 子关联名 + * @param Closure $closure 闭包 + * @return void + */ + public function eagerlyResult($result, $relation, $subRelation = '', $closure = null) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + $pk = $result->$localKey; + + $this->query->removeWhereField($foreignKey); + + $data = $this->eagerlyWhere([ + [$foreignKey, '=', $pk], + ], $foreignKey, $relation, $subRelation, $closure); + + // 关联数据封装 + if (!isset($data[$pk])) { + $data[$pk] = []; + } + + foreach ($data[$pk] as &$relationModel) { + $relationModel->setParent(clone $result); + } + + $result->setRelation(Loader::parseName($relation), $this->resultSetBuild($data[$pk])); + } + + /** + * 关联模型预查询 + * @access public + * @param array $where 关联预查询条件 + * @param string $key 关联键名 + * @param string $relation 关联名 + * @param mixed $subRelation 子关联 + * @param Closure $closure + * @return array + */ + protected function eagerlyWhere(array $where, $key, $relation, $subRelation = '', $closure = null) + { + // 预载入关联查询 支持嵌套预载入 + $throughList = $this->through->where($where)->select(); + $keys = $throughList->column($this->throughPk, $this->throughPk); + + if ($closure instanceof Closure) { + $closure($this->query); + } + + $list = $this->query->where($this->throughKey, 'in', $keys)->select(); + + // 组装模型数据 + $data = []; + $keys = $throughList->column($this->foreignKey, $this->throughPk); + + foreach ($list as $set) { + $data[$keys[$set->{$this->throughKey}]][] = $set; + } + + return $data; + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount($result, $closure, $aggregate = 'count', $field = '*', &$name = null) + { + $localKey = $this->localKey; + + if (!isset($result->$localKey)) { + return 0; + } + + if ($closure instanceof Closure) { + $return = $closure($this->query); + if ($return && is_string($return)) { + $name = $return; + } + } + + $alias = Loader::parseName(basename(str_replace('\\', '/', $this->model))); + $throughTable = $this->through->getTable(); + $pk = $this->throughPk; + $throughKey = $this->throughKey; + $modelTable = $this->parent->getTable(); + + if (false === strpos($field, '.')) { + $field = $alias . '.' . $field; + } + + return $this->query + ->alias($alias) + ->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey) + ->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey) + ->where($throughTable . '.' . $this->foreignKey, $result->$localKey) + ->$aggregate($field); + } + + /** + * 创建关联统计子查询 + * @access public + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return string + */ + public function getRelationCountQuery($closure = null, $aggregate = 'count', $field = '*', &$name = null) + { + if ($closure instanceof Closure) { + $return = $closure($this->query); + if ($return && is_string($return)) { + $name = $return; + } + } + + $alias = Loader::parseName(basename(str_replace('\\', '/', $this->model))); + $throughTable = $this->through->getTable(); + $pk = $this->throughPk; + $throughKey = $this->throughKey; + $modelTable = $this->parent->getTable(); + + if (false === strpos($field, '.')) { + $field = $alias . '.' . $field; + } + + return $this->query + ->alias($alias) + ->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey) + ->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey) + ->whereExp($throughTable . '.' . $this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey) + ->fetchSql() + ->$aggregate($field); + } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $alias = Loader::parseName(basename(str_replace('\\', '/', $this->model))); + $throughTable = $this->through->getTable(); + $pk = $this->throughPk; + $throughKey = $this->throughKey; + $modelTable = $this->parent->getTable(); + $fields = $this->getQueryFields($alias); + + $this->query + ->field($fields) + ->alias($alias) + ->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey) + ->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey) + ->where($throughTable . '.' . $this->foreignKey, $this->parent->{$this->localKey}); + + $this->baseQuery = true; + } + } + +} diff --git a/thinkphp/library/think/model/relation/HasOne.php b/thinkphp/library/think/model/relation/HasOne.php new file mode 100644 index 0000000..fe09443 --- /dev/null +++ b/thinkphp/library/think/model/relation/HasOne.php @@ -0,0 +1,294 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\db\Query; +use think\Loader; +use think\Model; + +class HasOne extends OneToOne +{ + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前模型主键 + */ + public function __construct(Model $parent, $model, $foreignKey, $localKey) + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + $this->joinType = 'INNER'; + $this->query = (new $model)->db(); + + if (get_class($parent) == $model) { + $this->selfRelation = true; + } + } + + /** + * 延迟获取关联数据 + * @access public + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return Model + */ + public function getRelation($subRelation = '', $closure = null) + { + $localKey = $this->localKey; + + if ($closure instanceof Closure) { + $closure($this->query); + } + + // 判断关联类型执行查询 + $relationModel = $this->query + ->removeWhereField($this->foreignKey) + ->where($this->foreignKey, $this->parent->$localKey) + ->relation($subRelation) + ->find(); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } + + return $relationModel; + } + + /** + * 创建关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $aggregateAlias 聚合字段别名 + * @return string + */ + public function getRelationCountQuery($closure, $aggregate = 'count', $field = '*', &$aggregateAlias = '') + { + if ($closure instanceof Closure) { + $return = $closure($this->query); + + if ($return && is_string($return)) { + $aggregateAlias = $return; + } + } + + return $this->query + ->whereExp($this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey) + ->fetchSql() + ->$aggregate($field); + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount($result, $closure, $aggregate = 'count', $field = '*', &$name = '') + { + $localKey = $this->localKey; + + if (!isset($result->$localKey)) { + return 0; + } + + if ($closure instanceof Closure) { + $return = $closure($this->query); + if ($return && is_string($return)) { + $name = $return; + } + } + + return $this->query + ->where($this->foreignKey, '=', $result->$localKey) + ->$aggregate($field); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + $table = $this->query->getTable(); + $model = basename(str_replace('\\', '/', get_class($this->parent))); + $relation = basename(str_replace('\\', '/', $this->model)); + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + $softDelete = $this->query->getOptions('soft_delete'); + + return $this->parent->db() + ->alias($model) + ->whereExists(function ($query) use ($table, $model, $relation, $localKey, $foreignKey, $softDelete) { + $query->table([$table => $relation]) + ->field($relation . '.' . $foreignKey) + ->whereExp($model . '.' . $localKey, '=' . $relation . '.' . $foreignKey) + ->when($softDelete, function ($query) use ($softDelete, $relation) { + $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }); + }); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + 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)) { + $this->getQueryWhere($where, $relation); + } + + $fields = $this->getRelationQueryFields($fields, $model); + $softDelete = $this->query->getOptions('soft_delete'); + + return $this->parent->db() + ->alias($model) + ->field($fields) + ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $this->joinType) + ->when($softDelete, function ($query) use ($softDelete, $relation) { + $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }) + ->where($where); + } + + /** + * 预载入关联查询(数据集) + * @access protected + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$localKey)) { + $range[] = $result->$localKey; + } + } + + if (!empty($range)) { + $this->query->removeWhereField($foreignKey); + + $data = $this->eagerlyWhere([ + [$foreignKey, 'in', $range], + ], $foreignKey, $relation, $subRelation, $closure); + + // 关联属性名 + $attr = Loader::parseName($relation); + + // 关联数据封装 + foreach ($resultSet as $result) { + // 关联模型 + if (!isset($data[$result->$localKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$localKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($relationModel, $result, $this->bindAttr); + } else { + // 设置关联属性 + $result->setRelation($attr, $relationModel); + } + } + } + } + + /** + * 预载入关联查询(数据) + * @access protected + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + protected function eagerlyOne(&$result, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $this->query->removeWhereField($foreignKey); + + $data = $this->eagerlyWhere([ + [$foreignKey, '=', $result->$localKey], + ], $foreignKey, $relation, $subRelation, $closure); + + // 关联模型 + if (!isset($data[$result->$localKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$localKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + 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 new file mode 100644 index 0000000..d2af66e --- /dev/null +++ b/thinkphp/library/think/model/relation/MorphMany.php @@ -0,0 +1,342 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +class MorphMany extends Relation +{ + // 多态字段 + protected $morphKey; + protected $morphType; + // 多态类型 + protected $type; + + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $morphKey 关联外键 + * @param string $morphType 多态字段名 + * @param string $type 多态类型 + */ + public function __construct(Model $parent, $model, $morphKey, $morphType, $type) + { + $this->parent = $parent; + $this->model = $model; + $this->type = $type; + $this->morphKey = $morphKey; + $this->morphType = $morphType; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @access public + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return \think\Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure instanceof Closure) { + $closure($this->query); + } + + $this->baseQuery(); + + $list = $this->query->relation($subRelation)->select(); + $parent = clone $this->parent; + + foreach ($list as &$model) { + $model->setParent($parent); + } + + return $list; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + throw new Exception('relation not support: has'); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + public function hasWhere($where = [], $fields = null) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $morphType = $this->morphType; + $morphKey = $this->morphKey; + $type = $this->type; + $range = []; + + foreach ($resultSet as $result) { + $pk = $result->getPk(); + // 获取关联外键列表 + if (isset($result->$pk)) { + $range[] = $result->$pk; + } + } + + if (!empty($range)) { + $where = [ + [$morphKey, 'in', $range], + [$morphType, '=', $type], + ]; + $data = $this->eagerlyMorphToMany($where, $relation, $subRelation, $closure); + + // 关联属性名 + $attr = Loader::parseName($relation); + + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$pk])) { + $data[$result->$pk] = []; + } + + foreach ($data[$result->$pk] as &$relationModel) { + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + $result->setRelation($attr, $this->resultSetBuild($data[$result->$pk])); + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $pk = $result->getPk(); + + if (isset($result->$pk)) { + $key = $result->$pk; + $where = [ + [$this->morphKey, '=', $key], + [$this->morphType, '=', $this->type], + ]; + $data = $this->eagerlyMorphToMany($where, $relation, $subRelation, $closure); + + if (!isset($data[$key])) { + $data[$key] = []; + } + + foreach ($data[$key] as &$relationModel) { + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + $result->setRelation(Loader::parseName($relation), $this->resultSetBuild($data[$key])); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount($result, $closure, $aggregate = 'count', $field = '*', &$name = '') + { + $pk = $result->getPk(); + + if (!isset($result->$pk)) { + return 0; + } + + if ($closure instanceof Closure) { + $return = $closure($this->query); + + if ($return && is_string($return)) { + $name = $return; + } + } + + return $this->query + ->where([ + [$this->morphKey, '=', $result->$pk], + [$this->morphType, '=', $this->type], + ]) + ->$aggregate($field); + } + + /** + * 获取关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $aggregateAlias 聚合字段别名 + * @return string + */ + public function getRelationCountQuery($closure, $aggregate = 'count', $field = '*', &$aggregateAlias = '') + { + if ($closure instanceof Closure) { + $return = $closure($this->query); + + if ($return && is_string($return)) { + $aggregateAlias = $return; + } + } + + return $this->query + ->whereExp($this->morphKey, '=' . $this->parent->getTable() . '.' . $this->parent->getPk()) + ->where($this->morphType, '=', $this->type) + ->fetchSql() + ->$aggregate($field); + } + + /** + * 多态一对多 关联模型预查询 + * @access protected + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param \Closure $closure 闭包 + * @return array + */ + protected function eagerlyMorphToMany($where, $relation, $subRelation = '', $closure = null) + { + // 预载入关联查询 支持嵌套预载入 + $this->query->removeOption('where'); + + if ($closure instanceof Closure) { + $closure($this->query); + } + + $list = $this->query->where($where)->with($subRelation)->select(); + $morphKey = $this->morphKey; + + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $data[$set->$morphKey][] = $set; + } + + return $data; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 + * @return Model|false + */ + public function save($data) + { + $model = $this->make(); + + return $model->save($data) ? $model : false; + } + + /** + * 创建关联对象实例 + * @param array $data + * @return Model + */ + public function make($data = []) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + // 保存关联表数据 + $pk = $this->parent->getPk(); + + $data[$this->morphKey] = $this->parent->$pk; + $data[$this->morphType] = $this->type; + + return new $this->model($data); + } + + /** + * 批量保存当前关联数据对象 + * @access public + * @param array $dataSet 数据集 + * @return array|false + */ + public function saveAll(array $dataSet) + { + $result = []; + + foreach ($dataSet as $key => $data) { + $result[] = $this->save($data); + } + + return empty($result) ? false : $result; + } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $pk = $this->parent->getPk(); + + $this->query->where([ + [$this->morphKey, '=', $this->parent->$pk], + [$this->morphType, '=', $this->type], + ]); + + $this->baseQuery = true; + } + } + +} diff --git a/thinkphp/library/think/model/relation/MorphOne.php b/thinkphp/library/think/model/relation/MorphOne.php new file mode 100644 index 0000000..6bc205c --- /dev/null +++ b/thinkphp/library/think/model/relation/MorphOne.php @@ -0,0 +1,257 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +class MorphOne extends Relation +{ + // 多态字段 + protected $morphKey; + protected $morphType; + // 多态类型 + protected $type; + + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $morphKey 关联外键 + * @param string $morphType 多态字段名 + * @param string $type 多态类型 + */ + public function __construct(Model $parent, $model, $morphKey, $morphType, $type) + { + $this->parent = $parent; + $this->model = $model; + $this->type = $type; + $this->morphKey = $morphKey; + $this->morphType = $morphType; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @access public + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return Model + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure instanceof Closure) { + $closure($this->query); + } + + $this->baseQuery(); + + $relationModel = $this->query->relation($subRelation)->find(); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } + + return $relationModel; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + public function hasWhere($where = [], $fields = null) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $morphType = $this->morphType; + $morphKey = $this->morphKey; + $type = $this->type; + $range = []; + + foreach ($resultSet as $result) { + $pk = $result->getPk(); + // 获取关联外键列表 + if (isset($result->$pk)) { + $range[] = $result->$pk; + } + } + + if (!empty($range)) { + $data = $this->eagerlyMorphToOne([ + [$morphKey, 'in', $range], + [$morphType, '=', $type], + ], $relation, $subRelation, $closure); + + // 关联属性名 + $attr = Loader::parseName($relation); + + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$pk])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$pk]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + $result->setRelation($attr, $relationModel); + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $pk = $result->getPk(); + + if (isset($result->$pk)) { + $pk = $result->$pk; + $data = $this->eagerlyMorphToOne([ + [$this->morphKey, '=', $pk], + [$this->morphType, '=', $this->type], + ], $relation, $subRelation, $closure); + + if (isset($data[$pk])) { + $relationModel = $data[$pk]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } else { + $relationModel = null; + } + + $result->setRelation(Loader::parseName($relation), $relationModel); + } + } + + /** + * 多态一对一 关联模型预查询 + * @access protected + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param \Closure $closure 闭包 + * @return array + */ + protected function eagerlyMorphToOne($where, $relation, $subRelation = '', $closure = null) + { + // 预载入关联查询 支持嵌套预载入 + if ($closure instanceof Closure) { + $closure($this->query); + } + + $list = $this->query->where($where)->with($subRelation)->select(); + $morphKey = $this->morphKey; + + // 组装模型数据 + $data = []; + + foreach ($list as $set) { + $data[$set->$morphKey] = $set; + } + + return $data; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 + * @return Model|false + */ + public function save($data) + { + $model = $this->make(); + return $model->save($data) ? $model : false; + } + + /** + * 创建关联对象实例 + * @param array $data + * @return Model + */ + public function make($data = []) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + // 保存关联表数据 + $pk = $this->parent->getPk(); + + $data[$this->morphKey] = $this->parent->$pk; + $data[$this->morphType] = $this->type; + + return new $this->model($data); + } + + /** + * 执行基础查询(进执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $pk = $this->parent->getPk(); + + $this->query->where([ + [$this->morphKey, '=', $this->parent->$pk], + [$this->morphType, '=', $this->type], + ]); + $this->baseQuery = true; + } + } + +} diff --git a/thinkphp/library/think/model/relation/MorphTo.php b/thinkphp/library/think/model/relation/MorphTo.php new file mode 100644 index 0000000..0786c2f --- /dev/null +++ b/thinkphp/library/think/model/relation/MorphTo.php @@ -0,0 +1,316 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +class MorphTo extends Relation +{ + // 多态字段 + protected $morphKey; + protected $morphType; + // 多态别名 + protected $alias; + // 关联名 + protected $relation; + + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $morphType 多态字段名 + * @param string $morphKey 外键名 + * @param array $alias 多态别名定义 + * @param string $relation 关联名 + */ + public function __construct(Model $parent, $morphType, $morphKey, $alias = [], $relation = null) + { + $this->parent = $parent; + $this->morphType = $morphType; + $this->morphKey = $morphKey; + $this->alias = $alias; + $this->relation = $relation; + } + + /** + * 获取当前的关联模型类的实例 + * @access public + * @return Model + */ + public function getModel() + { + $morphType = $this->morphType; + $model = $this->parseModel($this->parent->$morphType); + + return (new $model); + } + + /** + * 延迟获取关联数据 + * @access public + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return Model + */ + public function getRelation($subRelation = '', $closure = null) + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + + // 多态模型 + $model = $this->parseModel($this->parent->$morphType); + + // 主键数据 + $pk = $this->parent->$morphKey; + + $relationModel = (new $model)->relation($subRelation)->find($pk); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } + + return $relationModel; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + public function hasWhere($where = [], $fields = null) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 解析模型的完整命名空间 + * @access protected + * @param string $model 模型名(或者完整类名) + * @return string + */ + protected function parseModel($model) + { + if (isset($this->alias[$model])) { + $model = $this->alias[$model]; + } + + if (false === strpos($model, '\\')) { + $path = explode('\\', get_class($this->parent)); + array_pop($path); + array_push($path, Loader::parseName($model, 1)); + $model = implode('\\', $path); + } + + return $model; + } + + /** + * 设置多态别名 + * @access public + * @param array $alias 别名定义 + * @return $this + */ + public function setAlias($alias) + { + $this->alias = $alias; + + return $this; + } + + /** + * 移除关联查询参数 + * @access public + * @return $this + */ + public function removeOption() + { + return $this; + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + * @throws Exception + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + $range = []; + + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (!empty($result->$morphKey)) { + $range[$result->$morphType][] = $result->$morphKey; + } + } + + if (!empty($range)) { + // 关联属性名 + $attr = Loader::parseName($relation); + + foreach ($range as $key => $val) { + // 多态类型映射 + $model = $this->parseModel($key); + $obj = (new $model)->db(); + $pk = $obj->getPk(); + // 预载入关联查询 支持嵌套预载入 + if ($closure instanceof \Closure) { + $closure($obj); + + if ($field = $obj->getOptions('with_field')) { + $obj->field($field)->removeOption('with_field'); + } + } + $list = $obj->all($val, $subRelation); + $data = []; + + foreach ($list as $k => $vo) { + $data[$vo->$pk] = $vo; + } + + foreach ($resultSet as $result) { + if ($key == $result->$morphType) { + // 关联模型 + if (!isset($data[$result->$morphKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$morphKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + $result->setRelation($attr, $relationModel); + } + } + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + // 多态类型映射 + $model = $this->parseModel($result->{$this->morphType}); + + $this->eagerlyMorphToOne($model, $relation, $result, $subRelation); + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount($result, $closure, $aggregate = 'count', $field = '*', &$name = '') + {} + + /** + * 多态MorphTo 关联模型预查询 + * @access protected + * @param string $model 关联模型对象 + * @param string $relation 关联名 + * @param Model $result + * @param string $subRelation 子关联 + * @return void + */ + protected function eagerlyMorphToOne($model, $relation, &$result, $subRelation = '') + { + // 预载入关联查询 支持嵌套预载入 + $pk = $this->parent->{$this->morphKey}; + $data = (new $model)->with($subRelation)->find($pk); + + if ($data) { + $data->setParent(clone $result); + $data->isUpdate(true); + } + + $result->setRelation(Loader::parseName($relation), $data ?: null); + } + + /** + * 添加关联数据 + * @access public + * @param Model $model 关联模型对象 + * @param string $type 多态类型 + * @return 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, $type ?: get_class($model)); + $this->parent->save(); + + return $this->parent->setRelation($this->relation, $model); + } + + /** + * 注销关联数据 + * @access public + * @return Model + */ + public function dissociate() + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + + $this->parent->setAttr($morphKey, null); + $this->parent->setAttr($morphType, null); + $this->parent->save(); + + return $this->parent->setRelation($this->relation, null); + } + +} diff --git a/thinkphp/library/think/model/relation/OneToOne.php b/thinkphp/library/think/model/relation/OneToOne.php new file mode 100644 index 0000000..5e22b80 --- /dev/null +++ b/thinkphp/library/think/model/relation/OneToOne.php @@ -0,0 +1,337 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +/** + * Class OneToOne + * @package think\model\relation + * + */ +abstract class OneToOne extends Relation +{ + // 预载入方式 0 -JOIN 1 -IN + protected $eagerlyType = 1; + // 当前关联的JOIN类型 + protected $joinType; + // 要绑定的属性 + protected $bindAttr = []; + // 关联名 + protected $relation; + + /** + * 设置join类型 + * @access public + * @param string $type JOIN类型 + * @return $this + */ + public function joinType($type) + { + $this->joinType = $type; + return $this; + } + + /** + * 预载入关联查询(JOIN方式) + * @access public + * @param Query $query 查询对象 + * @param string $relation 关联名 + * @param mixed $field 关联字段 + * @param string $joinType JOIN方式 + * @param \Closure $closure 闭包条件 + * @param bool $first + * @return void + */ + public function eagerly(Query $query, $relation, $field, $joinType, $closure, $first) + { + $name = Loader::parseName(basename(str_replace('\\', '/', get_class($this->parent)))); + + if ($first) { + $table = $query->getTable(); + $query->table([$table => $name]); + + if ($query->getOptions('field')) { + $masterField = $query->getOptions('field'); + $query->removeOption('field'); + } else { + $masterField = true; + } + + $query->field($masterField, false, $table, $name); + } + + // 预载入封装 + $joinTable = $this->query->getTable(); + $joinAlias = $relation; + $joinType = $joinType ?: $this->joinType; + + $query->via($joinAlias); + + if ($this instanceof BelongsTo) { + $joinOn = $name . '.' . $this->foreignKey . '=' . $joinAlias . '.' . $this->localKey; + } else { + $joinOn = $name . '.' . $this->localKey . '=' . $joinAlias . '.' . $this->foreignKey; + } + + if ($closure instanceof Closure) { + // 执行闭包查询 + $closure($query); + // 使用withField指定获取关联的字段,如 + // $query->where(['id'=>1])->withField('id,name'); + if ($query->getOptions('with_field')) { + $field = $query->getOptions('with_field'); + $query->removeOption('with_field'); + } + } + + $query->join([$joinTable => $joinAlias], $joinOn, $joinType) + ->field($field, false, $joinTable, $joinAlias, $relation . '__'); + } + + /** + * 预载入关联查询(数据集) + * @access protected + * @param array $resultSet + * @param string $relation + * @param string $subRelation + * @param \Closure $closure + * @return mixed + */ + abstract protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure); + + /** + * 预载入关联查询(数据) + * @access protected + * @param Model $result + * @param string $relation + * @param string $subRelation + * @param \Closure $closure + * @return mixed + */ + abstract protected function eagerlyOne(&$result, $relation, $subRelation, $closure); + + /** + * 预载入关联查询(数据集) + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @param bool $join 是否为JOIN方式 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure, $join = false) + { + if ($join || 0 == $this->eagerlyType) { + // 模型JOIN关联组装 + foreach ($resultSet as $result) { + $this->match($this->model, $relation, $result); + } + } else { + // IN查询 + $this->eagerlySet($resultSet, $relation, $subRelation, $closure); + } + } + + /** + * 预载入关联查询(数据) + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @param bool $join 是否为JOIN方式 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure, $join = false) + { + if (0 == $this->eagerlyType || $join) { + // 模型JOIN关联组装 + $this->match($this->model, $relation, $result); + } else { + // IN查询 + $this->eagerlyOne($result, $relation, $subRelation, $closure); + } + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @return Model|false + */ + public function save($data) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + $model = new $this->model; + // 保存关联表数据 + $data[$this->foreignKey] = $this->parent->{$this->localKey}; + + return $model->save($data) ? $model : false; + } + + /** + * 设置预载入方式 + * @access public + * @param integer $type 预载入方式 0 JOIN查询 1 IN查询 + * @return $this + */ + public function setEagerlyType($type) + { + $this->eagerlyType = $type; + + return $this; + } + + /** + * 获取预载入方式 + * @access public + * @return integer + */ + public function getEagerlyType() + { + return $this->eagerlyType; + } + + /** + * 绑定关联表的属性到父模型属性 + * @access public + * @param mixed $attr 要绑定的属性列表 + * @return $this + */ + public function bind($attr) + { + if (is_string($attr)) { + $attr = explode(',', $attr); + } + $this->bindAttr = $attr; + + return $this; + } + + /** + * 获取绑定属性 + * @access public + * @return array + */ + public function getBindAttr() + { + return $this->bindAttr; + } + + /** + * 一对一 关联模型预查询拼装 + * @access public + * @param string $model 模型名称 + * @param string $relation 关联名 + * @param Model $result 模型对象实例 + * @return void + */ + protected function match($model, $relation, &$result) + { + // 重新组装模型数据 + foreach ($result->getData() as $key => $val) { + if (strpos($key, '__')) { + list($name, $attr) = explode('__', $key, 2); + if ($name == $relation) { + $list[$name][$attr] = $val; + unset($result->$key); + } + } + } + + if (isset($list[$relation])) { + $array = array_unique($list[$relation]); + + if (count($array) == 1 && null === current($array)) { + $relationModel = null; + } else { + $relationModel = new $model($list[$relation]); + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + if (!empty($this->bindAttr)) { + $this->bindAttr($relationModel, $result, $this->bindAttr); + } + } else { + $relationModel = null; + } + + $result->setRelation(Loader::parseName($relation), $relationModel); + } + + /** + * 绑定关联属性到父模型 + * @access protected + * @param Model $result 关联模型对象 + * @param Model $model 父模型对象 + * @return void + * @throws Exception + */ + protected function bindAttr($model, &$result) + { + foreach ($this->bindAttr as $key => $attr) { + $key = is_numeric($key) ? $attr : $key; + $value = $result->getOrigin($key); + + if (!is_null($value)) { + throw new Exception('bind attr has exists:' . $key); + } + + $result->setAttr($key, $model ? $model->getAttr($attr) : null); + } + } + + /** + * 一对一 关联模型预查询(IN方式) + * @access public + * @param array $where 关联预查询条件 + * @param string $key 关联键名 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param \Closure $closure + * @return array + */ + protected function eagerlyWhere($where, $key, $relation, $subRelation = '', $closure = null) + { + // 预载入关联查询 支持嵌套预载入 + if ($closure instanceof Closure) { + $closure($this->query); + + if ($field = $this->query->getOptions('with_field')) { + $this->query->field($field)->removeOption('with_field'); + } + } + + $list = $this->query->where($where)->with($subRelation)->select(); + + // 组装模型数据 + $data = []; + + foreach ($list as $set) { + $data[$set->$key] = $set; + } + + return $data; + } + +} diff --git a/thinkphp/library/think/paginator/driver/Bootstrap.php b/thinkphp/library/think/paginator/driver/Bootstrap.php new file mode 100644 index 0000000..ab5315c --- /dev/null +++ b/thinkphp/library/think/paginator/driver/Bootstrap.php @@ -0,0 +1,206 @@ + +// +---------------------------------------------------------------------- + +namespace think\paginator\driver; + +use think\Paginator; + +class Bootstrap extends Paginator +{ + + /** + * 上一页按钮 + * @param string $text + * @return string + */ + protected function getPreviousButton($text = "«") + { + + if ($this->currentPage() <= 1) { + return $this->getDisabledTextWrapper($text); + } + + $url = $this->url( + $this->currentPage() - 1 + ); + + return $this->getPageLinkWrapper($url, $text); + } + + /** + * 下一页按钮 + * @param string $text + * @return string + */ + protected function getNextButton($text = '»') + { + if (!$this->hasMore) { + return $this->getDisabledTextWrapper($text); + } + + $url = $this->url($this->currentPage() + 1); + + return $this->getPageLinkWrapper($url, $text); + } + + /** + * 页码按钮 + * @return string + */ + protected function getLinks() + { + if ($this->simple) { + return ''; + } + + $block = [ + 'first' => null, + 'slider' => null, + 'last' => null, + ]; + + $side = 3; + $window = $side * 2; + + if ($this->lastPage < $window + 6) { + $block['first'] = $this->getUrlRange(1, $this->lastPage); + } elseif ($this->currentPage <= $window) { + $block['first'] = $this->getUrlRange(1, $window + 2); + $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage); + } elseif ($this->currentPage > ($this->lastPage - $window)) { + $block['first'] = $this->getUrlRange(1, 2); + $block['last'] = $this->getUrlRange($this->lastPage - ($window + 2), $this->lastPage); + } else { + $block['first'] = $this->getUrlRange(1, 2); + $block['slider'] = $this->getUrlRange($this->currentPage - $side, $this->currentPage + $side); + $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage); + } + + $html = ''; + + if (is_array($block['first'])) { + $html .= $this->getUrlLinks($block['first']); + } + + if (is_array($block['slider'])) { + $html .= $this->getDots(); + $html .= $this->getUrlLinks($block['slider']); + } + + if (is_array($block['last'])) { + $html .= $this->getDots(); + $html .= $this->getUrlLinks($block['last']); + } + + return $html; + } + + /** + * 渲染分页html + * @return mixed + */ + public function render() + { + if ($this->hasPages()) { + if ($this->simple) { + return sprintf( + '
                %s %s
              ', + $this->getPreviousButton(), + $this->getNextButton() + ); + } else { + return sprintf( + '
                %s %s %s
              ', + $this->getPreviousButton(), + $this->getLinks(), + $this->getNextButton() + ); + } + } + } + + /** + * 生成一个可点击的按钮 + * + * @param string $url + * @param int $page + * @return string + */ + protected function getAvailablePageWrapper($url, $page) + { + return '
            • ' . $page . '
            • '; + } + + /** + * 生成一个禁用的按钮 + * + * @param string $text + * @return string + */ + protected function getDisabledTextWrapper($text) + { + return '
            • ' . $text . '
            • '; + } + + /** + * 生成一个激活的按钮 + * + * @param string $text + * @return string + */ + protected function getActivePageWrapper($text) + { + return '
            • ' . $text . '
            • '; + } + + /** + * 生成省略号按钮 + * + * @return string + */ + protected function getDots() + { + return $this->getDisabledTextWrapper('...'); + } + + /** + * 批量生成页码按钮. + * + * @param array $urls + * @return string + */ + protected function getUrlLinks(array $urls) + { + $html = ''; + + foreach ($urls as $page => $url) { + $html .= $this->getPageLinkWrapper($url, $page); + } + + return $html; + } + + /** + * 生成普通页码按钮 + * + * @param string $url + * @param int $page + * @return string + */ + protected function getPageLinkWrapper($url, $page) + { + if ($this->currentPage() == $page) { + return $this->getActivePageWrapper($page); + } + + return $this->getAvailablePageWrapper($url, $page); + } +} diff --git a/thinkphp/library/think/process/Builder.php b/thinkphp/library/think/process/Builder.php new file mode 100644 index 0000000..da56163 --- /dev/null +++ b/thinkphp/library/think/process/Builder.php @@ -0,0 +1,233 @@ + +// +---------------------------------------------------------------------- + +namespace think\process; + +use think\Process; + +class Builder +{ + private $arguments; + private $cwd; + private $env = null; + private $input; + private $timeout = 60; + private $options = []; + private $inheritEnv = true; + private $prefix = []; + private $outputDisabled = false; + + /** + * 构造方法 + * @param string[] $arguments 参数 + */ + public function __construct(array $arguments = []) + { + $this->arguments = $arguments; + } + + /** + * 创建一个实例 + * @param string[] $arguments 参数 + * @return self + */ + public static function create(array $arguments = []) + { + return new static($arguments); + } + + /** + * 添加一个参数 + * @param string $argument 参数 + * @return self + */ + public function add($argument) + { + $this->arguments[] = $argument; + + return $this; + } + + /** + * 添加一个前缀 + * @param string|array $prefix + * @return self + */ + public function setPrefix($prefix) + { + $this->prefix = is_array($prefix) ? $prefix : [$prefix]; + + return $this; + } + + /** + * 设置参数 + * @param string[] $arguments + * @return self + */ + public function setArguments(array $arguments) + { + $this->arguments = $arguments; + + return $this; + } + + /** + * 设置工作目录 + * @param null|string $cwd + * @return self + */ + public function setWorkingDirectory($cwd) + { + $this->cwd = $cwd; + + return $this; + } + + /** + * 是否初始化环境变量 + * @param bool $inheritEnv + * @return self + */ + public function inheritEnvironmentVariables($inheritEnv = true) + { + $this->inheritEnv = $inheritEnv; + + return $this; + } + + /** + * 设置环境变量 + * @param string $name + * @param null|string $value + * @return self + */ + public function setEnv($name, $value) + { + $this->env[$name] = $value; + + return $this; + } + + /** + * 添加环境变量 + * @param array $variables + * @return self + */ + public function addEnvironmentVariables(array $variables) + { + $this->env = array_replace($this->env, $variables); + + return $this; + } + + /** + * 设置输入 + * @param mixed $input + * @return self + */ + public function setInput($input) + { + $this->input = Utils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input); + + return $this; + } + + /** + * 设置超时时间 + * @param float|null $timeout + * @return self + */ + public function setTimeout($timeout) + { + if (null === $timeout) { + $this->timeout = null; + + return $this; + } + + $timeout = (float) $timeout; + + if ($timeout < 0) { + throw new \InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); + } + + $this->timeout = $timeout; + + return $this; + } + + /** + * 设置proc_open选项 + * @param string $name + * @param string $value + * @return self + */ + public function setOption($name, $value) + { + $this->options[$name] = $value; + + return $this; + } + + /** + * 禁止输出 + * @return self + */ + public function disableOutput() + { + $this->outputDisabled = true; + + return $this; + } + + /** + * 开启输出 + * @return self + */ + public function enableOutput() + { + $this->outputDisabled = false; + + return $this; + } + + /** + * 创建一个Process实例 + * @return Process + */ + public function getProcess() + { + if (0 === count($this->prefix) && 0 === count($this->arguments)) { + throw new \LogicException('You must add() command arguments before calling getProcess().'); + } + + $options = $this->options; + + $arguments = array_merge($this->prefix, $this->arguments); + $script = implode(' ', array_map([__NAMESPACE__ . '\\Utils', 'escapeArgument'], $arguments)); + + if ($this->inheritEnv) { + // include $_ENV for BC purposes + $env = array_replace($_ENV, $_SERVER, $this->env); + } else { + $env = $this->env; + } + + $process = new Process($script, $this->cwd, $env, $this->input, $this->timeout, $options); + + if ($this->outputDisabled) { + $process->disableOutput(); + } + + return $process; + } +} diff --git a/thinkphp/library/think/process/Utils.php b/thinkphp/library/think/process/Utils.php new file mode 100644 index 0000000..f94c648 --- /dev/null +++ b/thinkphp/library/think/process/Utils.php @@ -0,0 +1,75 @@ + +// +---------------------------------------------------------------------- + +namespace think\process; + +class Utils +{ + + /** + * 转义字符串 + * @param string $argument + * @return string + */ + public static function escapeArgument($argument) + { + + if ('' === $argument) { + return escapeshellarg($argument); + } + $escapedArgument = ''; + $quote = false; + foreach (preg_split('/(")/i', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) { + if ('"' === $part) { + $escapedArgument .= '\\"'; + } elseif (self::isSurroundedBy($part, '%')) { + // Avoid environment variable expansion + $escapedArgument .= '^%"' . substr($part, 1, -1) . '"^%'; + } else { + // escape trailing backslash + if ('\\' === substr($part, -1)) { + $part .= '\\'; + } + $quote = true; + $escapedArgument .= $part; + } + } + if ($quote) { + $escapedArgument = '"' . $escapedArgument . '"'; + } + return $escapedArgument; + } + + /** + * 验证并进行规范化Process输入。 + * @param string $caller + * @param mixed $input + * @return string + * @throws \InvalidArgumentException + */ + public static function validateInput($caller, $input) + { + if (null !== $input) { + if (is_resource($input)) { + return $input; + } + if (is_scalar($input)) { + return (string) $input; + } + throw new \InvalidArgumentException(sprintf('%s only accepts strings or stream resources.', $caller)); + } + return $input; + } + + private static function isSurroundedBy($arg, $char) + { + return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1]; + } + +} diff --git a/thinkphp/library/think/process/exception/Faild.php b/thinkphp/library/think/process/exception/Faild.php new file mode 100644 index 0000000..38647bc --- /dev/null +++ b/thinkphp/library/think/process/exception/Faild.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\exception; + +use think\Process; + +class Faild extends \RuntimeException +{ + + private $process; + + public function __construct(Process $process) + { + if ($process->isSuccessful()) { + throw new \InvalidArgumentException('Expected a failed process, but the given process was successful.'); + } + + $error = sprintf('The command "%s" failed.' . "\nExit Code: %s(%s)", $process->getCommandLine(), $process->getExitCode(), $process->getExitCodeText()); + + if (!$process->isOutputDisabled()) { + $error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", $process->getOutput(), $process->getErrorOutput()); + } + + parent::__construct($error); + + $this->process = $process; + } + + public function getProcess() + { + return $this->process; + } +} diff --git a/thinkphp/library/think/process/exception/Failed.php b/thinkphp/library/think/process/exception/Failed.php new file mode 100644 index 0000000..5295082 --- /dev/null +++ b/thinkphp/library/think/process/exception/Failed.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\exception; + +use think\Process; + +class Failed extends \RuntimeException +{ + + private $process; + + public function __construct(Process $process) + { + if ($process->isSuccessful()) { + throw new \InvalidArgumentException('Expected a failed process, but the given process was successful.'); + } + + $error = sprintf('The command "%s" failed.' . "\nExit Code: %s(%s)", $process->getCommandLine(), $process->getExitCode(), $process->getExitCodeText()); + + if (!$process->isOutputDisabled()) { + $error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", $process->getOutput(), $process->getErrorOutput()); + } + + parent::__construct($error); + + $this->process = $process; + } + + public function getProcess() + { + return $this->process; + } +} diff --git a/thinkphp/library/think/process/exception/Timeout.php b/thinkphp/library/think/process/exception/Timeout.php new file mode 100644 index 0000000..d5f1162 --- /dev/null +++ b/thinkphp/library/think/process/exception/Timeout.php @@ -0,0 +1,61 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\exception; + +use think\Process; + +class Timeout extends \RuntimeException +{ + + const TYPE_GENERAL = 1; + const TYPE_IDLE = 2; + + private $process; + private $timeoutType; + + public function __construct(Process $process, $timeoutType) + { + $this->process = $process; + $this->timeoutType = $timeoutType; + + parent::__construct(sprintf('The process "%s" exceeded the timeout of %s seconds.', $process->getCommandLine(), $this->getExceededTimeout())); + } + + public function getProcess() + { + return $this->process; + } + + public function isGeneralTimeout() + { + return $this->timeoutType === self::TYPE_GENERAL; + } + + public function isIdleTimeout() + { + return $this->timeoutType === self::TYPE_IDLE; + } + + public function getExceededTimeout() + { + switch ($this->timeoutType) { + case self::TYPE_GENERAL: + return $this->process->getTimeout(); + + case self::TYPE_IDLE: + return $this->process->getIdleTimeout(); + + default: + throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)); + } + } +} diff --git a/thinkphp/library/think/process/pipes/Pipes.php b/thinkphp/library/think/process/pipes/Pipes.php new file mode 100644 index 0000000..82396b8 --- /dev/null +++ b/thinkphp/library/think/process/pipes/Pipes.php @@ -0,0 +1,93 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\pipes; + +abstract class Pipes +{ + + /** @var array */ + public $pipes = []; + + /** @var string */ + protected $inputBuffer = ''; + /** @var resource|null */ + protected $input; + + /** @var bool */ + private $blocked = true; + + const CHUNK_SIZE = 16384; + + /** + * 返回用于 proc_open 描述符的数组 + * @return array + */ + abstract public function getDescriptors(); + + /** + * 返回一个数组的索引由其相关的流,以防这些管道使用的临时文件的文件名。 + * @return string[] + */ + abstract public function getFiles(); + + /** + * 文件句柄和管道中读取数据。 + * @param bool $blocking 是否使用阻塞调用 + * @param bool $close 是否要关闭管道,如果他们已经到达 EOF。 + * @return string[] + */ + abstract public function readAndWrite($blocking, $close = false); + + /** + * 返回当前状态如果有打开的文件句柄或管道。 + * @return bool + */ + abstract public function areOpen(); + + /** + * {@inheritdoc} + */ + public function close() + { + foreach ($this->pipes as $pipe) { + fclose($pipe); + } + $this->pipes = []; + } + + /** + * 检查系统调用已被中断 + * @return bool + */ + protected function hasSystemCallBeenInterrupted() + { + $lastError = error_get_last(); + + return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call'); + } + + protected function unblock() + { + if (!$this->blocked) { + return; + } + + foreach ($this->pipes as $pipe) { + stream_set_blocking($pipe, 0); + } + if (null !== $this->input) { + stream_set_blocking($this->input, 0); + } + + $this->blocked = false; + } +} diff --git a/thinkphp/library/think/process/pipes/Unix.php b/thinkphp/library/think/process/pipes/Unix.php new file mode 100644 index 0000000..fd99a5d --- /dev/null +++ b/thinkphp/library/think/process/pipes/Unix.php @@ -0,0 +1,196 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\pipes; + +use think\Process; + +class Unix extends Pipes +{ + + /** @var bool */ + private $ttyMode; + /** @var bool */ + private $ptyMode; + /** @var bool */ + private $disableOutput; + + public function __construct($ttyMode, $ptyMode, $input, $disableOutput) + { + $this->ttyMode = (bool) $ttyMode; + $this->ptyMode = (bool) $ptyMode; + $this->disableOutput = (bool) $disableOutput; + + if (is_resource($input)) { + $this->input = $input; + } else { + $this->inputBuffer = (string) $input; + } + } + + public function __destruct() + { + $this->close(); + } + + /** + * {@inheritdoc} + */ + public function getDescriptors() + { + if ($this->disableOutput) { + $nullstream = fopen('/dev/null', 'c'); + + return [ + ['pipe', 'r'], + $nullstream, + $nullstream, + ]; + } + + if ($this->ttyMode) { + return [ + ['file', '/dev/tty', 'r'], + ['file', '/dev/tty', 'w'], + ['file', '/dev/tty', 'w'], + ]; + } + + if ($this->ptyMode && Process::isPtySupported()) { + return [ + ['pty'], + ['pty'], + ['pty'], + ]; + } + + return [ + ['pipe', 'r'], + ['pipe', 'w'], // stdout + ['pipe', 'w'], // stderr + ]; + } + + /** + * {@inheritdoc} + */ + public function getFiles() + { + return []; + } + + /** + * {@inheritdoc} + */ + public function readAndWrite($blocking, $close = false) + { + + if (1 === count($this->pipes) && [0] === array_keys($this->pipes)) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + + if (empty($this->pipes)) { + return []; + } + + $this->unblock(); + + $read = []; + + if (null !== $this->input) { + $r = array_merge($this->pipes, ['input' => $this->input]); + } else { + $r = $this->pipes; + } + + unset($r[0]); + + $w = isset($this->pipes[0]) ? [$this->pipes[0]] : null; + $e = null; + + if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = []; + } + + return $read; + } + + if (0 === $n) { + return $read; + } + + foreach ($r as $pipe) { + + $type = (false !== $found = array_search($pipe, $this->pipes)) ? $found : 'input'; + $data = ''; + while ('' !== $dataread = (string) fread($pipe, self::CHUNK_SIZE)) { + $data .= $dataread; + } + + if ('' !== $data) { + if ('input' === $type) { + $this->inputBuffer .= $data; + } else { + $read[$type] = $data; + } + } + + if (false === $data || (true === $close && feof($pipe) && '' === $data)) { + if ('input' === $type) { + $this->input = null; + } else { + fclose($this->pipes[$type]); + unset($this->pipes[$type]); + } + } + } + + if (null !== $w && 0 < count($w)) { + while (strlen($this->inputBuffer)) { + $written = fwrite($w[0], $this->inputBuffer, 2 << 18); // write 512k + if ($written > 0) { + $this->inputBuffer = (string) substr($this->inputBuffer, $written); + } else { + break; + } + } + } + + if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function areOpen() + { + return (bool) $this->pipes; + } + + /** + * 创建一个新的 UnixPipes 实例 + * @param Process $process + * @param string|resource $input + * @return self + */ + public static function create(Process $process, $input) + { + return new static($process->isTty(), $process->isPty(), $input, $process->isOutputDisabled()); + } +} diff --git a/thinkphp/library/think/process/pipes/Windows.php b/thinkphp/library/think/process/pipes/Windows.php new file mode 100644 index 0000000..1b8b0d4 --- /dev/null +++ b/thinkphp/library/think/process/pipes/Windows.php @@ -0,0 +1,228 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\pipes; + +use think\Process; + +class Windows extends Pipes +{ + + /** @var array */ + private $files = []; + /** @var array */ + private $fileHandles = []; + /** @var array */ + private $readBytes = [ + Process::STDOUT => 0, + Process::STDERR => 0, + ]; + /** @var bool */ + private $disableOutput; + + public function __construct($disableOutput, $input) + { + $this->disableOutput = (bool) $disableOutput; + + if (!$this->disableOutput) { + + $this->files = [ + Process::STDOUT => tempnam(sys_get_temp_dir(), 'sf_proc_stdout'), + Process::STDERR => tempnam(sys_get_temp_dir(), 'sf_proc_stderr'), + ]; + foreach ($this->files as $offset => $file) { + $this->fileHandles[$offset] = fopen($this->files[$offset], 'rb'); + if (false === $this->fileHandles[$offset]) { + throw new \RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable'); + } + } + } + + if (is_resource($input)) { + $this->input = $input; + } else { + $this->inputBuffer = $input; + } + } + + public function __destruct() + { + $this->close(); + $this->removeFiles(); + } + + /** + * {@inheritdoc} + */ + public function getDescriptors() + { + if ($this->disableOutput) { + $nullstream = fopen('NUL', 'c'); + + return [ + ['pipe', 'r'], + $nullstream, + $nullstream, + ]; + } + + return [ + ['pipe', 'r'], + ['file', 'NUL', 'w'], + ['file', 'NUL', 'w'], + ]; + } + + /** + * {@inheritdoc} + */ + public function getFiles() + { + return $this->files; + } + + /** + * {@inheritdoc} + */ + public function readAndWrite($blocking, $close = false) + { + $this->write($blocking, $close); + + $read = []; + $fh = $this->fileHandles; + foreach ($fh as $type => $fileHandle) { + if (0 !== fseek($fileHandle, $this->readBytes[$type])) { + continue; + } + $data = ''; + $dataread = null; + while (!feof($fileHandle)) { + if (false !== $dataread = fread($fileHandle, self::CHUNK_SIZE)) { + $data .= $dataread; + } + } + if (0 < $length = strlen($data)) { + $this->readBytes[$type] += $length; + $read[$type] = $data; + } + + if (false === $dataread || (true === $close && feof($fileHandle) && '' === $data)) { + fclose($this->fileHandles[$type]); + unset($this->fileHandles[$type]); + } + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function areOpen() + { + return (bool) $this->pipes && (bool) $this->fileHandles; + } + + /** + * {@inheritdoc} + */ + public function close() + { + parent::close(); + foreach ($this->fileHandles as $handle) { + fclose($handle); + } + $this->fileHandles = []; + } + + /** + * 创建一个新的 WindowsPipes 实例。 + * @param Process $process + * @param $input + * @return self + */ + public static function create(Process $process, $input) + { + return new static($process->isOutputDisabled(), $input); + } + + /** + * 删除临时文件 + */ + private function removeFiles() + { + foreach ($this->files as $filename) { + if (file_exists($filename)) { + @unlink($filename); + } + } + $this->files = []; + } + + /** + * 写入到 stdin 输入 + * @param bool $blocking + * @param bool $close + */ + private function write($blocking, $close) + { + if (empty($this->pipes)) { + return; + } + + $this->unblock(); + + $r = null !== $this->input ? ['input' => $this->input] : null; + $w = isset($this->pipes[0]) ? [$this->pipes[0]] : null; + $e = null; + + if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = []; + } + + return; + } + + if (0 === $n) { + return; + } + + if (null !== $r && 0 < count($r)) { + $data = ''; + while ($dataread = fread($r['input'], self::CHUNK_SIZE)) { + $data .= $dataread; + } + + $this->inputBuffer .= $data; + + if (false === $data || (true === $close && feof($r['input']) && '' === $data)) { + $this->input = null; + } + } + + if (null !== $w && 0 < count($w)) { + while (strlen($this->inputBuffer)) { + $written = fwrite($w[0], $this->inputBuffer, 2 << 18); + if ($written > 0) { + $this->inputBuffer = (string) substr($this->inputBuffer, $written); + } else { + break; + } + } + } + + if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + } +} diff --git a/thinkphp/library/think/response/Download.php b/thinkphp/library/think/response/Download.php new file mode 100644 index 0000000..5595f9a --- /dev/null +++ b/thinkphp/library/think/response/Download.php @@ -0,0 +1,148 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Exception; +use think\Response; + +class Download extends Response +{ + protected $expire = 360; + protected $name; + protected $mimeType; + protected $isContent = false; + protected $openinBrowser = false; + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + * @throws \Exception + */ + protected function output($data) + { + if (!$this->isContent && !is_file($data)) { + throw new Exception('file not exists:' . $data); + } + + ob_end_clean(); + + if (!empty($this->name)) { + $name = $this->name; + } else { + $name = !$this->isContent ? pathinfo($data, PATHINFO_BASENAME) : ''; + } + + if ($this->isContent) { + $mimeType = $this->mimeType; + $size = strlen($data); + } else { + $mimeType = $this->getMimeType($data); + $size = filesize($data); + } + + $this->header['Pragma'] = 'public'; + $this->header['Content-Type'] = $mimeType ?: 'application/octet-stream'; + $this->header['Cache-control'] = 'max-age=' . $this->expire; + $this->header['Content-Disposition'] = $this->openinBrowser ? 'inline' : 'attachment; filename="' . $name . '"'; + $this->header['Content-Length'] = $size; + $this->header['Content-Transfer-Encoding'] = 'binary'; + $this->header['Expires'] = gmdate("D, d M Y H:i:s", time() + $this->expire) . ' GMT'; + + $this->lastModified(gmdate('D, d M Y H:i:s', time()) . ' GMT'); + + $data = $this->isContent ? $data : file_get_contents($data); + return $data; + } + + /** + * 设置是否为内容 必须配合mimeType方法使用 + * @access public + * @param bool $content + * @return $this + */ + public function isContent($content = true) + { + $this->isContent = $content; + return $this; + } + + /** + * 设置有效期 + * @access public + * @param integer $expire 有效期 + * @return $this + */ + public function expire($expire) + { + $this->expire = $expire; + return $this; + } + + /** + * 设置文件类型 + * @access public + * @param string $filename 文件名 + * @return $this + */ + public function mimeType($mimeType) + { + $this->mimeType = $mimeType; + return $this; + } + + /** + * 获取文件类型信息 + * @access public + * @param string $filename 文件名 + * @return string + */ + protected function getMimeType($filename) + { + if (!empty($this->mimeType)) { + return $this->mimeType; + } + + $finfo = finfo_open(FILEINFO_MIME_TYPE); + + return finfo_file($finfo, $filename); + } + + /** + * 设置下载文件的显示名称 + * @access public + * @param string $filename 文件名 + * @param bool $extension 后缀自动识别 + * @return $this + */ + public function name($filename, $extension = true) + { + $this->name = $filename; + + if ($extension && false === strpos($filename, '.')) { + $this->name .= '.' . pathinfo($this->data, PATHINFO_EXTENSION); + } + + return $this; + } + + /** + * 设置是否在浏览器中显示文件 + * @access public + * @param bool $openinBrowser 是否在浏览器中显示文件 + * @return $this + */ + public function openinBrowser($openinBrowser) { + $this->openinBrowser = $openinBrowser; + return $this; + } +} diff --git a/thinkphp/library/think/response/Json.php b/thinkphp/library/think/response/Json.php new file mode 100644 index 0000000..aa5bbd6 --- /dev/null +++ b/thinkphp/library/think/response/Json.php @@ -0,0 +1,51 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Response; + +class Json extends Response +{ + // 输出参数 + protected $options = [ + 'json_encode_param' => JSON_UNESCAPED_UNICODE, + ]; + + protected $contentType = 'application/json'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + * @throws \Exception + */ + protected function output($data) + { + try { + // 返回JSON数据格式到客户端 包含状态信息 + $data = json_encode($data, $this->options['json_encode_param']); + + if (false === $data) { + throw new \InvalidArgumentException(json_last_error_msg()); + } + + return $data; + } catch (\Exception $e) { + if ($e->getPrevious()) { + throw $e->getPrevious(); + } + throw $e; + } + } + +} diff --git a/thinkphp/library/think/response/Jsonp.php b/thinkphp/library/think/response/Jsonp.php new file mode 100644 index 0000000..f69e88e --- /dev/null +++ b/thinkphp/library/think/response/Jsonp.php @@ -0,0 +1,58 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Response; + +class Jsonp extends Response +{ + // 输出参数 + protected $options = [ + 'var_jsonp_handler' => 'callback', + 'default_jsonp_handler' => 'jsonpReturn', + 'json_encode_param' => JSON_UNESCAPED_UNICODE, + ]; + + protected $contentType = 'application/javascript'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + * @throws \Exception + */ + protected function output($data) + { + try { + // 返回JSON数据格式到客户端 包含状态信息 [当url_common_param为false时是无法获取到$_GET的数据的,故使用Request来获取] + $var_jsonp_handler = $this->app['request']->param($this->options['var_jsonp_handler'], ""); + $handler = !empty($var_jsonp_handler) ? $var_jsonp_handler : $this->options['default_jsonp_handler']; + + $data = json_encode($data, $this->options['json_encode_param']); + + if (false === $data) { + throw new \InvalidArgumentException(json_last_error_msg()); + } + + $data = $handler . '(' . $data . ');'; + + return $data; + } catch (\Exception $e) { + if ($e->getPrevious()) { + throw $e->getPrevious(); + } + throw $e; + } + } + +} diff --git a/thinkphp/library/think/response/Jump.php b/thinkphp/library/think/response/Jump.php new file mode 100644 index 0000000..258448c --- /dev/null +++ b/thinkphp/library/think/response/Jump.php @@ -0,0 +1,32 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Response; + +class Jump extends Response +{ + protected $contentType = 'text/html'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + * @throws \Exception + */ + protected function output($data) + { + $data = $this->app['view']->fetch($this->options['jump_template'], $data); + return $data; + } +} diff --git a/thinkphp/library/think/response/Redirect.php b/thinkphp/library/think/response/Redirect.php new file mode 100644 index 0000000..6b4f118 --- /dev/null +++ b/thinkphp/library/think/response/Redirect.php @@ -0,0 +1,119 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Response; + +class Redirect extends Response +{ + + protected $options = []; + + // URL参数 + protected $params = []; + + public function __construct($data = '', $code = 302, array $header = [], array $options = []) + { + parent::__construct($data, $code, $header, $options); + + $this->cacheControl('no-cache,must-revalidate'); + } + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + $this->header['Location'] = $this->getTargetUrl(); + + return; + } + + /** + * 重定向传值(通过Session) + * @access protected + * @param string|array $name 变量名或者数组 + * @param mixed $value 值 + * @return $this + */ + public function with($name, $value = null) + { + $session = $this->app['session']; + + if (is_array($name)) { + foreach ($name as $key => $val) { + $session->flash($key, $val); + } + } else { + $session->flash($name, $value); + } + + return $this; + } + + /** + * 获取跳转地址 + * @access public + * @return string + */ + public function getTargetUrl() + { + if (strpos($this->data, '://') || (0 === strpos($this->data, '/') && empty($this->params))) { + return $this->data; + } else { + return $this->app['url']->build($this->data, $this->params); + } + } + + public function params($params = []) + { + $this->params = $params; + + return $this; + } + + /** + * 记住当前url后跳转 + * @access public + * @param string $url 指定记住的url + * @return $this + */ + public function remember($url = null) + { + $this->app['session']->set('redirect_url', $url ?: $this->app['request']->url()); + + return $this; + } + + /** + * 跳转到上次记住的url + * @access public + * @param string $url 闪存数据不存在时的跳转地址 + * @return $this + */ + public function restore($url = null) + { + $session = $this->app['session']; + + if ($session->has('redirect_url')) { + $this->data = $session->get('redirect_url'); + $session->delete('redirect_url'); + } elseif ($url) { + $this->data = $url; + } + + return $this; + } +} diff --git a/thinkphp/library/think/response/View.php b/thinkphp/library/think/response/View.php new file mode 100644 index 0000000..3d54c73 --- /dev/null +++ b/thinkphp/library/think/response/View.php @@ -0,0 +1,119 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Response; + +class View extends Response +{ + // 输出参数 + protected $options = []; + protected $vars = []; + protected $config = []; + protected $filter; + protected $contentType = 'text/html'; + + /** + * 是否内容渲染 + * @var bool + */ + protected $isContent = false; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + // 渲染模板输出 + return $this->app['view'] + ->filter($this->filter) + ->fetch($data, $this->vars, $this->config, $this->isContent); + } + + /** + * 设置是否为内容渲染 + * @access public + * @param bool $content + * @return $this + */ + public function isContent($content = true) + { + $this->isContent = $content; + return $this; + } + + /** + * 获取视图变量 + * @access public + * @param string $name 模板变量 + * @return mixed + */ + public function getVars($name = null) + { + if (is_null($name)) { + return $this->vars; + } else { + return isset($this->vars[$name]) ? $this->vars[$name] : null; + } + } + + /** + * 模板变量赋值 + * @access public + * @param mixed $name 变量名 + * @param mixed $value 变量值 + * @return $this + */ + public function assign($name, $value = '') + { + if (is_array($name)) { + $this->vars = array_merge($this->vars, $name); + } else { + $this->vars[$name] = $value; + } + + return $this; + } + + public function config($config) + { + $this->config = $config; + return $this; + } + + /** + * 视图内容过滤 + * @access public + * @param callable $filter + * @return $this + */ + public function filter($filter) + { + $this->filter = $filter; + return $this; + } + + /** + * 检查模板是否存在 + * @access private + * @param string|array $name 参数名 + * @return bool + */ + public function exists($name) + { + return $this->app['view']->exists($name); + } + +} diff --git a/thinkphp/library/think/response/Xml.php b/thinkphp/library/think/response/Xml.php new file mode 100644 index 0000000..9c1681a --- /dev/null +++ b/thinkphp/library/think/response/Xml.php @@ -0,0 +1,116 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Collection; +use think\Model; +use think\Response; + +class Xml extends Response +{ + // 输出参数 + protected $options = [ + // 根节点名 + 'root_node' => 'think', + // 根节点属性 + 'root_attr' => '', + //数字索引的子节点名 + 'item_node' => 'item', + // 数字索引子节点key转换的属性名 + 'item_key' => 'id', + // 数据编码 + 'encoding' => 'utf-8', + ]; + + protected $contentType = 'text/xml'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + if (is_string($data)) { + if (0 !== strpos($data, 'options['encoding']; + $xml = ""; + $data = $xml . $data; + } + return $data; + } + + // XML数据转换 + return $this->xmlEncode($data, $this->options['root_node'], $this->options['item_node'], $this->options['root_attr'], $this->options['item_key'], $this->options['encoding']); + } + + /** + * XML编码 + * @access protected + * @param mixed $data 数据 + * @param string $root 根节点名 + * @param string $item 数字索引的子节点名 + * @param string $attr 根节点属性 + * @param string $id 数字索引子节点key转换的属性名 + * @param string $encoding 数据编码 + * @return string + */ + protected function xmlEncode($data, $root, $item, $attr, $id, $encoding) + { + if (is_array($attr)) { + $array = []; + foreach ($attr as $key => $value) { + $array[] = "{$key}=\"{$value}\""; + } + $attr = implode(' ', $array); + } + + $attr = trim($attr); + $attr = empty($attr) ? '' : " {$attr}"; + $xml = ""; + $xml .= "<{$root}{$attr}>"; + $xml .= $this->dataToXml($data, $item, $id); + $xml .= ""; + + return $xml; + } + + /** + * 数据XML编码 + * @access protected + * @param mixed $data 数据 + * @param string $item 数字索引时的节点名称 + * @param string $id 数字索引key转换为的属性名 + * @return string + */ + 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}\""; + $key = $item; + } + $xml .= "<{$key}{$attr}>"; + $xml .= (is_array($val) || is_object($val)) ? $this->dataToXml($val, $item, $id) : $val; + $xml .= ""; + } + + return $xml; + } +} diff --git a/thinkphp/library/think/route/AliasRule.php b/thinkphp/library/think/route/AliasRule.php new file mode 100644 index 0000000..393cb31 --- /dev/null +++ b/thinkphp/library/think/route/AliasRule.php @@ -0,0 +1,119 @@ + +// +---------------------------------------------------------------------- + +namespace think\route; + +use think\Route; + +class AliasRule extends Domain +{ + /** + * 架构函数 + * @access public + * @param Route $router 路由实例 + * @param RuleGroup $parent 上级对象 + * @param string $name 路由别名 + * @param string $route 路由绑定 + * @param array $option 路由参数 + */ + public function __construct(Route $router, RuleGroup $parent, $name, $route, $option = []) + { + $this->router = $router; + $this->parent = $parent; + $this->name = $name; + $this->route = $route; + $this->option = $option; + } + + /** + * 检测路由别名 + * @access public + * @param Request $request 请求对象 + * @param string $url 访问地址 + * @param bool $completeMatch 路由是否完全匹配 + * @return Dispatch|false + */ + public function check($request, $url, $completeMatch = false) + { + if ($dispatch = $this->checkCrossDomain($request)) { + // 允许跨域 + return $dispatch; + } + + // 检查参数有效性 + if (!$this->checkOption($this->option, $request)) { + return false; + } + + list($action, $bind) = array_pad(explode('|', $url, 2), 2, ''); + + if (isset($this->option['allow']) && !in_array($action, $this->option['allow'])) { + // 允许操作 + return false; + } elseif (isset($this->option['except']) && in_array($action, $this->option['except'])) { + // 排除操作 + return false; + } + + if (isset($this->option['method'][$action])) { + $this->option['method'] = $this->option['method'][$action]; + } + + // 匹配后执行的行为 + $this->afterMatchGroup($request); + + if ($this->parent) { + // 合并分组参数 + $this->mergeGroupOptions(); + } + + if (isset($this->option['ext'])) { + // 路由ext参数 优先于系统配置的URL伪静态后缀参数 + $bind = preg_replace('/\.(' . $request->ext() . ')$/i', '', $bind); + } + + $this->parseBindAppendParam($this->route); + + if (0 === strpos($this->route, '\\')) { + // 路由到类 + return $this->bindToClass($request, $bind, substr($this->route, 1)); + } elseif (0 === strpos($this->route, '@')) { + // 路由到控制器类 + return $this->bindToController($request, $bind, substr($this->route, 1)); + } else { + // 路由到模块/控制器 + return $this->bindToModule($request, $bind, $this->route); + } + } + + /** + * 设置允许的操作方法 + * @access public + * @param array $action 操作方法 + * @return $this + */ + public function allow($action = []) + { + return $this->option('allow', $action); + } + + /** + * 设置排除的操作方法 + * @access public + * @param array $action 操作方法 + * @return $this + */ + public function except($action = []) + { + return $this->option('except', $action); + } + +} diff --git a/thinkphp/library/think/route/Dispatch.php b/thinkphp/library/think/route/Dispatch.php new file mode 100644 index 0000000..7323c98 --- /dev/null +++ b/thinkphp/library/think/route/Dispatch.php @@ -0,0 +1,366 @@ + +// +---------------------------------------------------------------------- + +namespace think\route; + +use think\App; +use think\Container; +use think\exception\ValidateException; +use think\Request; +use think\Response; + +abstract class Dispatch +{ + /** + * 应用对象 + * @var App + */ + protected $app; + + /** + * 请求对象 + * @var Request + */ + protected $request; + + /** + * 路由规则 + * @var Rule + */ + protected $rule; + + /** + * 调度信息 + * @var mixed + */ + protected $dispatch; + + /** + * 调度参数 + * @var array + */ + protected $param; + + /** + * 状态码 + * @var string + */ + protected $code; + + /** + * 是否进行大小写转换 + * @var bool + */ + protected $convert; + + public function __construct(Request $request, Rule $rule, $dispatch, $param = [], $code = null) + { + $this->request = $request; + $this->rule = $rule; + $this->app = Container::get('app'); + $this->dispatch = $dispatch; + $this->param = $param; + $this->code = $code; + + if (isset($param['convert'])) { + $this->convert = $param['convert']; + } + } + + public function init() + { + // 执行路由后置操作 + if ($this->rule->doAfter()) { + // 设置请求的路由信息 + + // 设置当前请求的参数 + $this->request->setRouteVars($this->rule->getVars()); + $this->request->routeInfo([ + 'rule' => $this->rule->getRule(), + 'route' => $this->rule->getRoute(), + 'option' => $this->rule->getOption(), + 'var' => $this->rule->getVars(), + ]); + + $this->doRouteAfter(); + } + + return $this; + } + + /** + * 检查路由后置操作 + * @access protected + * @return void + */ + protected function doRouteAfter() + { + // 记录匹配的路由信息 + $option = $this->rule->getOption(); + $matches = $this->rule->getVars(); + + // 添加中间件 + if (!empty($option['middleware'])) { + $this->app['middleware']->import($option['middleware']); + } + + // 绑定模型数据 + if (!empty($option['model'])) { + $this->createBindModel($option['model'], $matches); + } + + // 指定Header数据 + if (!empty($option['header'])) { + $header = $option['header']; + $this->app['hook']->add('response_send', function ($response) use ($header) { + $response->header($header); + }); + } + + // 指定Response响应数据 + if (!empty($option['response'])) { + foreach ($option['response'] as $response) { + $this->app['hook']->add('response_send', $response); + } + } + + // 开启请求缓存 + if (isset($option['cache']) && $this->request->isGet()) { + $this->parseRequestCache($option['cache']); + } + + if (!empty($option['append'])) { + $this->request->setRouteVars($option['append']); + } + } + + /** + * 执行路由调度 + * @access public + * @return mixed + */ + public function run() + { + $option = $this->rule->getOption(); + + // 检测路由after行为 + if (!empty($option['after'])) { + $dispatch = $this->checkAfter($option['after']); + + if ($dispatch instanceof Response) { + return $dispatch; + } + } + + // 数据自动验证 + if (isset($option['validate'])) { + $this->autoValidate($option['validate']); + } + + $data = $this->exec(); + + return $this->autoResponse($data); + } + + protected function autoResponse($data) + { + if ($data instanceof Response) { + $response = $data; + } elseif (!is_null($data)) { + // 默认自动识别响应输出类型 + $isAjax = $this->request->isAjax(); + $type = $isAjax ? $this->rule->getConfig('default_ajax_return') : $this->rule->getConfig('default_return_type'); + + $response = Response::create($data, $type); + } else { + $data = ob_get_clean(); + $content = false === $data ? '' : $data; + $status = '' === $content && $this->request->isJson() ? 204 : 200; + + $response = Response::create($content, '', $status); + } + + return $response; + } + + /** + * 检查路由后置行为 + * @access protected + * @param mixed $after 后置行为 + * @return mixed + */ + protected function checkAfter($after) + { + $this->app['log']->notice('路由后置行为建议使用中间件替代!'); + + $hook = $this->app['hook']; + + $result = null; + + foreach ((array) $after as $behavior) { + $result = $hook->exec($behavior); + + if (!is_null($result)) { + break; + } + } + + // 路由规则重定向 + if ($result instanceof Response) { + return $result; + } + + return false; + } + + /** + * 验证数据 + * @access protected + * @param array $option + * @return void + * @throws ValidateException + */ + protected function autoValidate($option) + { + list($validate, $scene, $message, $batch) = $option; + + if (is_array($validate)) { + // 指定验证规则 + $v = $this->app->validate(); + $v->rule($validate); + } else { + // 调用验证器 + $v = $this->app->validate($validate); + if (!empty($scene)) { + $v->scene($scene); + } + } + + if (!empty($message)) { + $v->message($message); + } + + // 批量验证 + if ($batch) { + $v->batch(true); + } + + if (!$v->check($this->request->param())) { + throw new ValidateException($v->getError()); + } + } + + /** + * 处理路由请求缓存 + * @access protected + * @param Request $request 请求对象 + * @param string|array $cache 路由缓存 + * @return void + */ + protected function parseRequestCache($cache) + { + if (is_array($cache)) { + list($key, $expire, $tag) = array_pad($cache, 3, null); + } else { + $key = str_replace('|', '/', $this->request->url()); + $expire = $cache; + $tag = null; + } + + $cache = $this->request->cache($key, $expire, $tag); + $this->app->setResponseCache($cache); + } + + /** + * 路由绑定模型实例 + * @access protected + * @param array|\Clousre $bindModel 绑定模型 + * @param array $matches 路由变量 + * @return void + */ + protected function createBindModel($bindModel, $matches) + { + foreach ($bindModel as $key => $val) { + if ($val instanceof \Closure) { + $result = $this->app->invokeFunction($val, $matches); + } else { + $fields = explode('&', $key); + + if (is_array($val)) { + list($model, $exception) = $val; + } else { + $model = $val; + $exception = true; + } + + $where = []; + $match = true; + + foreach ($fields as $field) { + if (!isset($matches[$field])) { + $match = false; + break; + } else { + $where[] = [$field, '=', $matches[$field]]; + } + } + + if ($match) { + $query = strpos($model, '\\') ? $model::where($where) : $this->app->model($model)->where($where); + $result = $query->failException($exception)->find(); + } + } + + if (!empty($result)) { + // 注入容器 + $this->app->instance(get_class($result), $result); + } + } + } + + public function convert($convert) + { + $this->convert = $convert; + + return $this; + } + + public function getDispatch() + { + return $this->dispatch; + } + + public function getParam() + { + return $this->param; + } + + abstract public function exec(); + + public function __sleep() + { + return ['rule', 'dispatch', 'convert', 'param', 'code', 'controller', 'actionName']; + } + + public function __wakeup() + { + $this->app = Container::get('app'); + $this->request = $this->app['request']; + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app'], $data['request'], $data['rule']); + + return $data; + } +} diff --git a/thinkphp/library/think/route/Domain.php b/thinkphp/library/think/route/Domain.php new file mode 100644 index 0000000..923d9b4 --- /dev/null +++ b/thinkphp/library/think/route/Domain.php @@ -0,0 +1,237 @@ + +// +---------------------------------------------------------------------- + +namespace think\route; + +use think\Container; +use think\Loader; +use think\Request; +use think\Route; +use think\route\dispatch\Callback as CallbackDispatch; +use think\route\dispatch\Controller as ControllerDispatch; +use think\route\dispatch\Module as ModuleDispatch; + +class Domain extends RuleGroup +{ + /** + * 架构函数 + * @access public + * @param Route $router 路由对象 + * @param string $name 路由域名 + * @param mixed $rule 域名路由 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + */ + public function __construct(Route $router, $name = '', $rule = null, $option = [], $pattern = []) + { + $this->router = $router; + $this->domain = $name; + $this->option = $option; + $this->rule = $rule; + $this->pattern = $pattern; + } + + /** + * 检测域名路由 + * @access public + * @param Request $request 请求对象 + * @param string $url 访问地址 + * @param bool $completeMatch 路由是否完全匹配 + * @return Dispatch|false + */ + public function check($request, $url, $completeMatch = false) + { + // 检测别名路由 + $result = $this->checkRouteAlias($request, $url); + + if (false !== $result) { + return $result; + } + + // 检测URL绑定 + $result = $this->checkUrlBind($request, $url); + + if (!empty($this->option['append'])) { + $request->setRouteVars($this->option['append']); + unset($this->option['append']); + } + + if (false !== $result) { + return $result; + } + + // 添加域名中间件 + if (!empty($this->option['middleware'])) { + Container::get('middleware')->import($this->option['middleware']); + unset($this->option['middleware']); + } + + return parent::check($request, $url, $completeMatch); + } + + /** + * 设置路由绑定 + * @access public + * @param string $bind 绑定信息 + * @return $this + */ + public function bind($bind) + { + $this->router->bind($bind, $this->domain); + return $this; + } + + /** + * 检测路由别名 + * @access private + * @param Request $request + * @param string $url URL地址 + * @return Dispatch|false + */ + private function checkRouteAlias($request, $url) + { + $alias = strpos($url, '|') ? strstr($url, '|', true) : $url; + + $item = $this->router->getAlias($alias); + + return $item ? $item->check($request, $url) : false; + } + + /** + * 检测URL绑定 + * @access private + * @param Request $request + * @param string $url URL地址 + * @return Dispatch|false + */ + private function checkUrlBind($request, $url) + { + $bind = $this->router->getBind($this->domain); + + if (!empty($bind)) { + $this->parseBindAppendParam($bind); + + // 记录绑定信息 + Container::get('app')->log('[ BIND ] ' . var_export($bind, true)); + + // 如果有URL绑定 则进行绑定检测 + $type = substr($bind, 0, 1); + $bind = substr($bind, 1); + + $bindTo = [ + '\\' => 'bindToClass', + '@' => 'bindToController', + ':' => 'bindToNamespace', + ]; + + if (isset($bindTo[$type])) { + return $this->{$bindTo[$type]}($request, $url, $bind); + } + } + + return false; + } + + protected function parseBindAppendParam(&$bind) + { + if (false !== strpos($bind, '?')) { + list($bind, $query) = explode('?', $bind); + parse_str($query, $vars); + $this->append($vars); + } + } + + /** + * 绑定到类 + * @access protected + * @param Request $request + * @param string $url URL地址 + * @param string $class 类名(带命名空间) + * @return CallbackDispatch + */ + protected function bindToClass($request, $url, $class) + { + $array = explode('|', $url, 2); + $action = !empty($array[0]) ? $array[0] : $this->router->config('default_action'); + $param = []; + + if (!empty($array[1])) { + $this->parseUrlParams($request, $array[1], $param); + } + + return new CallbackDispatch($request, $this, [$class, $action], $param); + } + + /** + * 绑定到命名空间 + * @access protected + * @param Request $request + * @param string $url URL地址 + * @param string $namespace 命名空间 + * @return CallbackDispatch + */ + protected function bindToNamespace($request, $url, $namespace) + { + $array = explode('|', $url, 3); + $class = !empty($array[0]) ? $array[0] : $this->router->config('default_controller'); + $method = !empty($array[1]) ? $array[1] : $this->router->config('default_action'); + $param = []; + + if (!empty($array[2])) { + $this->parseUrlParams($request, $array[2], $param); + } + + return new CallbackDispatch($request, $this, [$namespace . '\\' . Loader::parseName($class, 1), $method], $param); + } + + /** + * 绑定到控制器类 + * @access protected + * @param Request $request + * @param string $url URL地址 + * @param string $controller 控制器名 (支持带模块名 index/user ) + * @return ControllerDispatch + */ + protected function bindToController($request, $url, $controller) + { + $array = explode('|', $url, 2); + $action = !empty($array[0]) ? $array[0] : $this->router->config('default_action'); + $param = []; + + if (!empty($array[1])) { + $this->parseUrlParams($request, $array[1], $param); + } + + return new ControllerDispatch($request, $this, $controller . '/' . $action, $param); + } + + /** + * 绑定到模块/控制器 + * @access protected + * @param Request $request + * @param string $url URL地址 + * @param string $controller 控制器类名(带命名空间) + * @return ModuleDispatch + */ + protected function bindToModule($request, $url, $controller) + { + $array = explode('|', $url, 2); + $action = !empty($array[0]) ? $array[0] : $this->router->config('default_action'); + $param = []; + + if (!empty($array[1])) { + $this->parseUrlParams($request, $array[1], $param); + } + + return new ModuleDispatch($request, $this, $controller . '/' . $action, $param); + } + +} diff --git a/thinkphp/library/think/route/Resource.php b/thinkphp/library/think/route/Resource.php new file mode 100644 index 0000000..ff13928 --- /dev/null +++ b/thinkphp/library/think/route/Resource.php @@ -0,0 +1,126 @@ + +// +---------------------------------------------------------------------- + +namespace think\route; + +use think\Route; + +class Resource extends RuleGroup +{ + // 资源路由名称 + protected $resource; + + // REST路由方法定义 + protected $rest = []; + + /** + * 架构函数 + * @access public + * @param Route $router 路由对象 + * @param RuleGroup $parent 上级对象 + * @param string $name 资源名称 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @param array $rest 资源定义 + */ + public function __construct(Route $router, RuleGroup $parent = null, $name = '', $route = '', $option = [], $pattern = [], $rest = []) + { + $this->router = $router; + $this->parent = $parent; + $this->resource = $name; + $this->route = $route; + $this->name = strpos($name, '.') ? strstr($name, '.', true) : $name; + + $this->setFullName(); + + // 资源路由默认为完整匹配 + $option['complete_match'] = true; + + $this->pattern = $pattern; + $this->option = $option; + $this->rest = $rest; + + if ($this->parent) { + $this->domain = $this->parent->getDomain(); + $this->parent->addRuleItem($this); + } + + if ($router->isTest()) { + $this->buildResourceRule(); + } + } + + /** + * 生成资源路由规则 + * @access protected + * @return void + */ + protected function buildResourceRule() + { + $origin = $this->router->getGroup(); + $this->router->setGroup($this); + + $rule = $this->resource; + $option = $this->option; + + if (strpos($rule, '.')) { + // 注册嵌套资源路由 + $array = explode('.', $rule); + $last = array_pop($array); + $item = []; + + foreach ($array as $val) { + $item[] = $val . '/<' . (isset($option['var'][$val]) ? $option['var'][$val] : $val . '_id') . '>'; + } + + $rule = implode('/', $item) . '/' . $last; + } + + $prefix = substr($rule, strlen($this->name) + 1); + + // 注册资源路由 + foreach ($this->rest as $key => $val) { + if ((isset($option['only']) && !in_array($key, $option['only'])) + || (isset($option['except']) && in_array($key, $option['except']))) { + continue; + } + + if (isset($last) && strpos($val[1], '') && isset($option['var'][$last])) { + $val[1] = str_replace('', '<' . $option['var'][$last] . '>', $val[1]); + } elseif (strpos($val[1], '') && isset($option['var'][$rule])) { + $val[1] = str_replace('', '<' . $option['var'][$rule] . '>', $val[1]); + } + + $this->addRule(trim($prefix . $val[1], '/'), $this->route . '/' . $val[2], $val[0]); + } + + $this->router->setGroup($origin); + } + + /** + * rest方法定义和修改 + * @access public + * @param string $name 方法名称 + * @param array|bool $resource 资源 + * @return $this + */ + public function rest($name, $resource = []) + { + if (is_array($name)) { + $this->rest = $resource ? $name : array_merge($this->rest, $name); + } else { + $this->rest[$name] = $resource; + } + + return $this; + } +} diff --git a/thinkphp/library/think/route/Rule.php b/thinkphp/library/think/route/Rule.php new file mode 100644 index 0000000..996305f --- /dev/null +++ b/thinkphp/library/think/route/Rule.php @@ -0,0 +1,1130 @@ + +// +---------------------------------------------------------------------- + +namespace think\route; + +use think\Container; +use think\Request; +use think\Response; +use think\route\dispatch\Callback as CallbackDispatch; +use think\route\dispatch\Controller as ControllerDispatch; +use think\route\dispatch\Module as ModuleDispatch; +use think\route\dispatch\Redirect as RedirectDispatch; +use think\route\dispatch\Response as ResponseDispatch; +use think\route\dispatch\View as ViewDispatch; + +abstract class Rule +{ + /** + * 路由标识 + * @var string + */ + protected $name; + + /** + * 路由对象 + * @var Route + */ + protected $router; + + /** + * 路由所属分组 + * @var RuleGroup + */ + protected $parent; + + /** + * 路由规则 + * @var mixed + */ + protected $rule; + + /** + * 路由地址 + * @var string|\Closure + */ + protected $route; + + /** + * 请求类型 + * @var string + */ + protected $method; + + /** + * 路由变量 + * @var array + */ + protected $vars = []; + + /** + * 路由参数 + * @var array + */ + protected $option = []; + + /** + * 路由变量规则 + * @var array + */ + protected $pattern = []; + + /** + * 需要和分组合并的路由参数 + * @var array + */ + protected $mergeOptions = ['after', 'model', 'header', 'response', 'append', 'middleware']; + + /** + * 是否需要后置操作 + * @var bool + */ + protected $doAfter; + + /** + * 是否锁定参数 + * @var bool + */ + protected $lockOption = false; + + abstract public function check($request, $url, $completeMatch = false); + + /** + * 获取Name + * @access public + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 获取当前路由规则 + * @access public + * @return string + */ + public function getRule() + { + return $this->rule; + } + + /** + * 获取当前路由地址 + * @access public + * @return mixed + */ + public function getRoute() + { + return $this->route; + } + + /** + * 获取当前路由的请求类型 + * @access public + * @return string + */ + public function getMethod() + { + return strtolower($this->method); + } + + /** + * 获取当前路由的变量 + * @access public + * @return array + */ + public function getVars() + { + return $this->vars; + } + + /** + * 获取路由对象 + * @access public + * @return Route + */ + public function getRouter() + { + return $this->router; + } + + /** + * 路由是否有后置操作 + * @access public + * @return bool + */ + public function doAfter() + { + return $this->doAfter; + } + + /** + * 获取路由分组 + * @access public + * @return RuleGroup|null + */ + public function getParent() + { + return $this->parent; + } + + /** + * 获取路由所在域名 + * @access public + * @return string + */ + public function getDomain() + { + return $this->parent->getDomain(); + } + + /** + * 获取变量规则定义 + * @access public + * @param string $name 变量名 + * @return mixed + */ + public function getPattern($name = '') + { + if ('' === $name) { + return $this->pattern; + } + + return isset($this->pattern[$name]) ? $this->pattern[$name] : null; + } + + /** + * 获取路由参数 + * @access public + * @param string $name 变量名 + * @return mixed + */ + public function getConfig($name = '') + { + return $this->router->config($name); + } + + /** + * 获取路由参数定义 + * @access public + * @param string $name 参数名 + * @return mixed + */ + public function getOption($name = '') + { + if ('' === $name) { + return $this->option; + } + + return isset($this->option[$name]) ? $this->option[$name] : null; + } + + /** + * 注册路由参数 + * @access public + * @param string|array $name 参数名 + * @param mixed $value 值 + * @return $this + */ + public function option($name, $value = '') + { + if (is_array($name)) { + $this->option = array_merge($this->option, $name); + } else { + $this->option[$name] = $value; + } + + return $this; + } + + /** + * 注册变量规则 + * @access public + * @param string|array $name 变量名 + * @param string $rule 变量规则 + * @return $this + */ + public function pattern($name, $rule = '') + { + if (is_array($name)) { + $this->pattern = array_merge($this->pattern, $name); + } else { + $this->pattern[$name] = $rule; + } + + return $this; + } + + /** + * 设置标识 + * @access public + * @param string $name 标识名 + * @return $this + */ + public function name($name) + { + $this->name = $name; + + return $this; + } + + /** + * 设置变量 + * @access public + * @param array $vars 变量 + * @return $this + */ + public function vars($vars) + { + $this->vars = $vars; + + return $this; + } + + /** + * 设置路由请求类型 + * @access public + * @param string $method + * @return $this + */ + public function method($method) + { + return $this->option('method', strtolower($method)); + } + + /** + * 设置路由前置行为 + * @access public + * @param array|\Closure $before + * @return $this + */ + public function before($before) + { + return $this->option('before', $before); + } + + /** + * 设置路由后置行为 + * @access public + * @param array|\Closure $after + * @return $this + */ + public function after($after) + { + return $this->option('after', $after); + } + + /** + * 检查后缀 + * @access public + * @param string $ext + * @return $this + */ + public function ext($ext = '') + { + return $this->option('ext', $ext); + } + + /** + * 检查禁止后缀 + * @access public + * @param string $ext + * @return $this + */ + public function denyExt($ext = '') + { + return $this->option('deny_ext', $ext); + } + + /** + * 检查域名 + * @access public + * @param string $domain + * @return $this + */ + public function domain($domain) + { + return $this->option('domain', $domain); + } + + /** + * 设置参数过滤检查 + * @access public + * @param string|array $name + * @param mixed $value + * @return $this + */ + public function filter($name, $value = null) + { + if (is_array($name)) { + $this->option['filter'] = $name; + } else { + $this->option['filter'][$name] = $value; + } + + return $this; + } + + /** + * 绑定模型 + * @access public + * @param array|string $var 路由变量名 多个使用 & 分割 + * @param string|\Closure $model 绑定模型类 + * @param bool $exception 是否抛出异常 + * @return $this + */ + public function model($var, $model = null, $exception = true) + { + if ($var instanceof \Closure) { + $this->option['model'][] = $var; + } elseif (is_array($var)) { + $this->option['model'] = $var; + } elseif (is_null($model)) { + $this->option['model']['id'] = [$var, true]; + } else { + $this->option['model'][$var] = [$model, $exception]; + } + + return $this; + } + + /** + * 附加路由隐式参数 + * @access public + * @param array $append + * @return $this + */ + public function append(array $append = []) + { + if (isset($this->option['append'])) { + $this->option['append'] = array_merge($this->option['append'], $append); + } else { + $this->option['append'] = $append; + } + + return $this; + } + + /** + * 绑定验证 + * @access public + * @param mixed $validate 验证器类 + * @param string $scene 验证场景 + * @param array $message 验证提示 + * @param bool $batch 批量验证 + * @return $this + */ + public function validate($validate, $scene = null, $message = [], $batch = false) + { + $this->option['validate'] = [$validate, $scene, $message, $batch]; + + return $this; + } + + /** + * 绑定Response对象 + * @access public + * @param mixed $response + * @return $this + */ + public function response($response) + { + $this->option['response'][] = $response; + return $this; + } + + /** + * 设置Response Header信息 + * @access public + * @param string|array $name 参数名 + * @param string $value 参数值 + * @return $this + */ + public function header($header, $value = null) + { + if (is_array($header)) { + $this->option['header'] = $header; + } else { + $this->option['header'][$header] = $value; + } + + return $this; + } + + /** + * 指定路由中间件 + * @access public + * @param string|array|\Closure $middleware + * @param mixed $param + * @return $this + */ + public function middleware($middleware, $param = null) + { + if (is_null($param) && is_array($middleware)) { + $this->option['middleware'] = $middleware; + } else { + foreach ((array) $middleware as $item) { + $this->option['middleware'][] = [$item, $param]; + } + } + + return $this; + } + + /** + * 设置路由缓存 + * @access public + * @param array|string $cache + * @return $this + */ + public function cache($cache) + { + return $this->option('cache', $cache); + } + + /** + * 检查URL分隔符 + * @access public + * @param bool $depr + * @return $this + */ + public function depr($depr) + { + return $this->option('param_depr', $depr); + } + + /** + * 是否合并额外参数 + * @access public + * @param bool $merge + * @return $this + */ + public function mergeExtraVars($merge = true) + { + return $this->option('merge_extra_vars', $merge); + } + + /** + * 设置需要合并的路由参数 + * @access public + * @param array $option + * @return $this + */ + public function mergeOptions($option = []) + { + $this->mergeOptions = array_merge($this->mergeOptions, $option); + return $this; + } + + /** + * 检查是否为HTTPS请求 + * @access public + * @param bool $https + * @return $this + */ + public function https($https = true) + { + return $this->option('https', $https); + } + + /** + * 检查是否为AJAX请求 + * @access public + * @param bool $ajax + * @return $this + */ + public function ajax($ajax = true) + { + return $this->option('ajax', $ajax); + } + + /** + * 检查是否为PJAX请求 + * @access public + * @param bool $pjax + * @return $this + */ + public function pjax($pjax = true) + { + return $this->option('pjax', $pjax); + } + + /** + * 检查是否为手机访问 + * @access public + * @param bool $mobile + * @return $this + */ + public function mobile($mobile = true) + { + return $this->option('mobile', $mobile); + } + + /** + * 当前路由到一个模板地址 当使用数组的时候可以传入模板变量 + * @access public + * @param bool|array $view + * @return $this + */ + public function view($view = true) + { + return $this->option('view', $view); + } + + /** + * 当前路由为重定向 + * @access public + * @param bool $redirect 是否为重定向 + * @return $this + */ + public function redirect($redirect = true) + { + return $this->option('redirect', $redirect); + } + + /** + * 设置路由完整匹配 + * @access public + * @param bool $match + * @return $this + */ + public function completeMatch($match = true) + { + return $this->option('complete_match', $match); + } + + /** + * 是否去除URL最后的斜线 + * @access public + * @param bool $remove + * @return $this + */ + public function removeSlash($remove = true) + { + return $this->option('remove_slash', $remove); + } + + /** + * 设置是否允许跨域 + * @access public + * @param bool $allow + * @param array $header + * @return $this + */ + public function allowCrossDomain($allow = true, $header = []) + { + if (!empty($header)) { + $this->header($header); + } + + if ($allow && $this->parent) { + $this->parent->addRuleItem($this, 'options'); + } + + return $this->option('cross_domain', $allow); + } + + /** + * 检查OPTIONS请求 + * @access public + * @param Request $request + * @return Dispatch|void + */ + protected function checkCrossDomain($request) + { + if (!empty($this->option['cross_domain'])) { + $header = [ + 'Access-Control-Allow-Credentials' => 'true', + 'Access-Control-Allow-Methods' => 'GET, POST, PATCH, PUT, DELETE', + 'Access-Control-Allow-Headers' => 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With', + ]; + + if (!empty($this->option['header'])) { + $header = array_merge($header, $this->option['header']); + } + + if (!isset($header['Access-Control-Allow-Origin'])) { + $httpOrigin = $request->header('origin'); + + if ($httpOrigin && strpos(config('cookie.domain'), $httpOrigin)) { + $header['Access-Control-Allow-Origin'] = $httpOrigin; + } else { + $header['Access-Control-Allow-Origin'] = '*'; + } + } + + $this->option['header'] = $header; + + if ($request->method(true) == 'OPTIONS') { + return new ResponseDispatch($request, $this, Response::create()->code(204)->header($header)); + } + } + } + + /** + * 设置路由规则全局有效 + * @access public + * @return $this + */ + public function crossDomainRule() + { + if ($this instanceof RuleGroup) { + $method = '*'; + } else { + $method = $this->method; + } + + $this->router->setCrossDomainRule($this, $method); + + return $this; + } + + /** + * 合并分组参数 + * @access public + * @return array + */ + public function mergeGroupOptions() + { + if (!$this->lockOption) { + $parentOption = $this->parent->getOption(); + // 合并分组参数 + foreach ($this->mergeOptions as $item) { + if (isset($parentOption[$item]) && isset($this->option[$item])) { + $this->option[$item] = array_merge($parentOption[$item], $this->option[$item]); + } + } + + $this->option = array_merge($parentOption, $this->option); + $this->lockOption = true; + } + + return $this->option; + } + + /** + * 解析匹配到的规则路由 + * @access public + * @param Request $request 请求对象 + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $url URL地址 + * @param array $option 路由参数 + * @param array $matches 匹配的变量 + * @return Dispatch + */ + public function parseRule($request, $rule, $route, $url, $option = [], $matches = []) + { + if (is_string($route) && isset($option['prefix'])) { + // 路由地址前缀 + $route = $option['prefix'] . $route; + } + + // 替换路由地址中的变量 + if (is_string($route) && !empty($matches)) { + $search = $replace = []; + + foreach ($matches as $key => $value) { + $search[] = '<' . $key . '>'; + $replace[] = $value; + + $search[] = ':' . $key; + $replace[] = $value; + } + + $route = str_replace($search, $replace, $route); + } + + // 解析额外参数 + $count = substr_count($rule, '/'); + $url = array_slice(explode('|', $url), $count + 1); + $this->parseUrlParams($request, implode('|', $url), $matches); + + $this->vars = $matches; + $this->option = $option; + $this->doAfter = true; + + // 发起路由调度 + return $this->dispatch($request, $route, $option); + } + + /** + * 检查路由前置行为 + * @access protected + * @param mixed $before 前置行为 + * @return mixed + */ + protected function checkBefore($before) + { + $hook = Container::get('hook'); + + foreach ((array) $before as $behavior) { + $result = $hook->exec($behavior); + + if (false === $result) { + return false; + } + } + } + + /** + * 发起路由调度 + * @access protected + * @param Request $request Request对象 + * @param mixed $route 路由地址 + * @param array $option 路由参数 + * @return Dispatch + */ + protected function dispatch($request, $route, $option) + { + if ($route instanceof \Closure) { + // 执行闭包 + $result = new CallbackDispatch($request, $this, $route); + } elseif ($route instanceof Response) { + $result = new ResponseDispatch($request, $this, $route); + } elseif (isset($option['view']) && false !== $option['view']) { + $result = new ViewDispatch($request, $this, $route, is_array($option['view']) ? $option['view'] : []); + } elseif (!empty($option['redirect']) || 0 === strpos($route, '/') || strpos($route, '://')) { + // 路由到重定向地址 + $result = new RedirectDispatch($request, $this, $route, [], isset($option['status']) ? $option['status'] : 301); + } elseif (false !== strpos($route, '\\')) { + // 路由到方法 + $result = $this->dispatchMethod($request, $route); + } elseif (0 === strpos($route, '@')) { + // 路由到控制器 + $result = $this->dispatchController($request, substr($route, 1)); + } else { + // 路由到模块/控制器/操作 + $result = $this->dispatchModule($request, $route); + } + + return $result; + } + + /** + * 解析URL地址为 模块/控制器/操作 + * @access protected + * @param Request $request Request对象 + * @param string $route 路由地址 + * @return CallbackDispatch + */ + protected function dispatchMethod($request, $route) + { + list($path, $var) = $this->parseUrlPath($route); + + $route = str_replace('/', '@', implode('/', $path)); + $method = strpos($route, '@') ? explode('@', $route) : $route; + + return new CallbackDispatch($request, $this, $method, $var); + } + + /** + * 解析URL地址为 模块/控制器/操作 + * @access protected + * @param Request $request Request对象 + * @param string $route 路由地址 + * @return ControllerDispatch + */ + protected function dispatchController($request, $route) + { + list($route, $var) = $this->parseUrlPath($route); + + $result = new ControllerDispatch($request, $this, implode('/', $route), $var); + + $request->setAction(array_pop($route)); + $request->setController($route ? array_pop($route) : $this->getConfig('default_controller')); + $request->setModule($route ? array_pop($route) : $this->getConfig('default_module')); + + return $result; + } + + /** + * 解析URL地址为 模块/控制器/操作 + * @access protected + * @param Request $request Request对象 + * @param string $route 路由地址 + * @return ModuleDispatch + */ + protected function dispatchModule($request, $route) + { + list($path, $var) = $this->parseUrlPath($route); + + $action = array_pop($path); + $controller = !empty($path) ? array_pop($path) : null; + $module = $this->getConfig('app_multi_module') && !empty($path) ? array_pop($path) : null; + $method = $request->method(); + + if ($this->getConfig('use_action_prefix') && $this->router->getMethodPrefix($method)) { + $prefix = $this->router->getMethodPrefix($method); + // 操作方法前缀支持 + $action = 0 !== strpos($action, $prefix) ? $prefix . $action : $action; + } + + // 设置当前请求的路由变量 + $request->setRouteVars($var); + + // 路由到模块/控制器/操作 + return new ModuleDispatch($request, $this, [$module, $controller, $action], ['convert' => false]); + } + + /** + * 路由检查 + * @access protected + * @param array $option 路由参数 + * @param Request $request Request对象 + * @return bool + */ + protected function checkOption($option, Request $request) + { + // 请求类型检测 + if (!empty($option['method'])) { + if (is_string($option['method']) && false === stripos($option['method'], $request->method())) { + return false; + } + } + + // AJAX PJAX 请求检查 + foreach (['ajax', 'pjax', 'mobile'] as $item) { + if (isset($option[$item])) { + $call = 'is' . $item; + if ($option[$item] && !$request->$call() || !$option[$item] && $request->$call()) { + return false; + } + } + } + + // 伪静态后缀检测 + if ($request->url() != '/' && ((isset($option['ext']) && false === stripos('|' . $option['ext'] . '|', '|' . $request->ext() . '|')) + || (isset($option['deny_ext']) && false !== stripos('|' . $option['deny_ext'] . '|', '|' . $request->ext() . '|')))) { + return false; + } + + // 域名检查 + if ((isset($option['domain']) && !in_array($option['domain'], [$request->host(true), $request->subDomain()]))) { + return false; + } + + // HTTPS检查 + if ((isset($option['https']) && $option['https'] && !$request->isSsl()) + || (isset($option['https']) && !$option['https'] && $request->isSsl())) { + return false; + } + + // 请求参数检查 + if (isset($option['filter'])) { + foreach ($option['filter'] as $name => $value) { + if ($request->param($name, '', null) != $value) { + return false; + } + } + } + return true; + } + + /** + * 解析URL地址中的参数Request对象 + * @access protected + * @param Request $request + * @param string $rule 路由规则 + * @param array $var 变量 + * @return void + */ + protected function parseUrlParams($request, $url, &$var = []) + { + if ($url) { + if ($this->getConfig('url_param_type')) { + $var += explode('|', $url); + } else { + preg_replace_callback('/(\w+)\|([^\|]+)/', function ($match) use (&$var) { + $var[$match[1]] = strip_tags($match[2]); + }, $url); + } + } + } + + /** + * 解析URL的pathinfo参数和变量 + * @access public + * @param string $url URL地址 + * @return array + */ + public function parseUrlPath($url) + { + // 分隔符替换 确保路由定义使用统一的分隔符 + $url = str_replace('|', '/', $url); + $url = trim($url, '/'); + $var = []; + + if (false !== strpos($url, '?')) { + // [模块/控制器/操作?]参数1=值1&参数2=值2... + $info = parse_url($url); + $path = explode('/', $info['path']); + parse_str($info['query'], $var); + } elseif (strpos($url, '/')) { + // [模块/控制器/操作] + $path = explode('/', $url); + } elseif (false !== strpos($url, '=')) { + // 参数1=值1&参数2=值2... + $path = []; + parse_str($url, $var); + } else { + $path = [$url]; + } + + return [$path, $var]; + } + + /** + * 生成路由的正则规则 + * @access protected + * @param string $rule 路由规则 + * @param array $match 匹配的变量 + * @param array $pattern 路由变量规则 + * @param array $option 路由参数 + * @param bool $completeMatch 路由是否完全匹配 + * @param string $suffix 路由正则变量后缀 + * @return string + */ + protected function buildRuleRegex($rule, $match, $pattern = [], $option = [], $completeMatch = false, $suffix = '') + { + foreach ($match as $name) { + $replace[] = $this->buildNameRegex($name, $pattern, $suffix); + } + + // 是否区分 / 地址访问 + if ('/' != $rule) { + if (!empty($option['remove_slash'])) { + $rule = rtrim($rule, '/'); + } elseif (substr($rule, -1) == '/') { + $rule = rtrim($rule, '/'); + $hasSlash = true; + } + } + + $regex = str_replace(array_unique($match), array_unique($replace), $rule); + $regex = str_replace([')?/', ')/', ')?-', ')-', '\\\\/'], [')\/', ')\/', ')\-', ')\-', '\/'], $regex); + + if (isset($hasSlash)) { + $regex .= '\/'; + } + + return $regex . ($completeMatch ? '$' : ''); + } + + /** + * 生成路由变量的正则规则 + * @access protected + * @param string $name 路由变量 + * @param string $pattern 变量规则 + * @param string $suffix 路由正则变量后缀 + * @return string + */ + protected function buildNameRegex($name, $pattern, $suffix) + { + $optional = ''; + $slash = substr($name, 0, 1); + + if (in_array($slash, ['/', '-'])) { + $prefix = '\\' . $slash; + $name = substr($name, 1); + $slash = substr($name, 0, 1); + } else { + $prefix = ''; + } + + if ('<' != $slash) { + return $prefix . preg_quote($name, '/'); + } + + if (strpos($name, '?')) { + $name = substr($name, 1, -2); + $optional = '?'; + } elseif (strpos($name, '>')) { + $name = substr($name, 1, -1); + } + + if (isset($pattern[$name])) { + $nameRule = $pattern[$name]; + if (0 === strpos($nameRule, '/') && '/' == substr($nameRule, -1)) { + $nameRule = substr($nameRule, 1, -1); + } + } else { + $nameRule = $this->getConfig('default_route_pattern'); + } + + return '(' . $prefix . '(?<' . $name . $suffix . '>' . $nameRule . '))' . $optional; + } + + /** + * 分析路由规则中的变量 + * @access protected + * @param string $rule 路由规则 + * @return array + */ + protected function parseVar($rule) + { + // 提取路由规则中的变量 + $var = []; + + if (preg_match_all('/<\w+\??>/', $rule, $matches)) { + foreach ($matches[0] as $name) { + $optional = false; + + if (strpos($name, '?')) { + $name = substr($name, 1, -2); + $optional = true; + } else { + $name = substr($name, 1, -1); + } + + $var[$name] = $optional ? 2 : 1; + } + } + + return $var; + } + + /** + * 设置路由参数 + * @access public + * @param string $method 方法名 + * @param array $args 调用参数 + * @return $this + */ + public function __call($method, $args) + { + if (count($args) > 1) { + $args[0] = $args; + } + array_unshift($args, $method); + + return call_user_func_array([$this, 'option'], $args); + } + + public function __sleep() + { + return ['name', 'rule', 'route', 'method', 'vars', 'option', 'pattern', 'doAfter']; + } + + public function __wakeup() + { + $this->router = Container::get('route'); + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['parent'], $data['router'], $data['route']); + + return $data; + } +} diff --git a/thinkphp/library/think/route/RuleGroup.php b/thinkphp/library/think/route/RuleGroup.php new file mode 100644 index 0000000..5781d8c --- /dev/null +++ b/thinkphp/library/think/route/RuleGroup.php @@ -0,0 +1,601 @@ + +// +---------------------------------------------------------------------- + +namespace think\route; + +use think\Container; +use think\Exception; +use think\Request; +use think\Response; +use think\Route; +use think\route\dispatch\Response as ResponseDispatch; +use think\route\dispatch\Url as UrlDispatch; + +class RuleGroup extends Rule +{ + // 分组路由(包括子分组) + protected $rules = [ + '*' => [], + 'get' => [], + 'post' => [], + 'put' => [], + 'patch' => [], + 'delete' => [], + 'head' => [], + 'options' => [], + ]; + + // MISS路由 + protected $miss; + + // 自动路由 + protected $auto; + + // 完整名称 + protected $fullName; + + // 所在域名 + protected $domain; + + /** + * 架构函数 + * @access public + * @param Route $router 路由对象 + * @param RuleGroup $parent 上级对象 + * @param string $name 分组名称 + * @param mixed $rule 分组路由 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + */ + public function __construct(Route $router, RuleGroup $parent = null, $name = '', $rule = [], $option = [], $pattern = []) + { + $this->router = $router; + $this->parent = $parent; + $this->rule = $rule; + $this->name = trim($name, '/'); + $this->option = $option; + $this->pattern = $pattern; + + $this->setFullName(); + + if ($this->parent) { + $this->domain = $this->parent->getDomain(); + $this->parent->addRuleItem($this); + } + + if (!empty($option['cross_domain'])) { + $this->router->setCrossDomainRule($this); + } + + if ($router->isTest()) { + $this->lazy(false); + } + } + + /** + * 设置分组的路由规则 + * @access public + * @return void + */ + protected function setFullName() + { + if (false !== strpos($this->name, ':')) { + $this->name = preg_replace(['/\[\:(\w+)\]/', '/\:(\w+)/'], ['<\1?>', '<\1>'], $this->name); + } + + if ($this->parent && $this->parent->getFullName()) { + $this->fullName = $this->parent->getFullName() . ($this->name ? '/' . $this->name : ''); + } else { + $this->fullName = $this->name; + } + } + + /** + * 获取所属域名 + * @access public + * @return string + */ + public function getDomain() + { + return $this->domain; + } + + /** + * 检测分组路由 + * @access public + * @param Request $request 请求对象 + * @param string $url 访问地址 + * @param bool $completeMatch 路由是否完全匹配 + * @return Dispatch|false + */ + public function check($request, $url, $completeMatch = false) + { + // 跨域OPTIONS请求 + if ($dispatch = $this->checkCrossDomain($request)) { + return $dispatch; + } + + // 检查分组有效性 + if (!$this->checkOption($this->option, $request) || !$this->checkUrl($url)) { + return false; + } + + // 检查前置行为 + if (isset($this->option['before'])) { + if (false === $this->checkBefore($this->option['before'])) { + return false; + } + unset($this->option['before']); + } + + // 解析分组路由 + if ($this instanceof Resource) { + $this->buildResourceRule(); + } elseif ($this->rule) { + if ($this->rule instanceof Response) { + return new ResponseDispatch($request, $this, $this->rule); + } + + $this->parseGroupRule($this->rule); + } + + // 获取当前路由规则 + $method = strtolower($request->method()); + $rules = $this->getMethodRules($method); + + if ($this->parent) { + // 合并分组参数 + $this->mergeGroupOptions(); + // 合并分组变量规则 + $this->pattern = array_merge($this->parent->getPattern(), $this->pattern); + } + + if (isset($this->option['complete_match'])) { + $completeMatch = $this->option['complete_match']; + } + + if (!empty($this->option['merge_rule_regex'])) { + // 合并路由正则规则进行路由匹配检查 + $result = $this->checkMergeRuleRegex($request, $rules, $url, $completeMatch); + + if (false !== $result) { + return $result; + } + } + + // 检查分组路由 + foreach ($rules as $key => $item) { + $result = $item->check($request, $url, $completeMatch); + + if (false !== $result) { + return $result; + } + } + + if ($this->auto) { + // 自动解析URL地址 + $result = new UrlDispatch($request, $this, $this->auto . '/' . $url, ['auto_search' => false]); + } elseif ($this->miss && in_array($this->miss->getMethod(), ['*', $method])) { + // 未匹配所有路由的路由规则处理 + $result = $this->miss->parseRule($request, '', $this->miss->getRoute(), $url, $this->miss->mergeGroupOptions()); + } else { + $result = false; + } + + return $result; + } + + /** + * 获取当前请求的路由规则(包括子分组、资源路由) + * @access protected + * @param string $method + * @return array + */ + protected function getMethodRules($method) + { + return array_merge($this->rules[$method], $this->rules['*']); + } + + /** + * 分组URL匹配检查 + * @access protected + * @param string $url + * @return bool + */ + protected function checkUrl($url) + { + if ($this->fullName) { + $pos = strpos($this->fullName, '<'); + + if (false !== $pos) { + $str = substr($this->fullName, 0, $pos); + } else { + $str = $this->fullName; + } + + if ($str && 0 !== stripos(str_replace('|', '/', $url), $str)) { + return false; + } + } + + return true; + } + + /** + * 延迟解析分组的路由规则 + * @access public + * @param bool $lazy 路由是否延迟解析 + * @return $this + */ + public function lazy($lazy = true) + { + if (!$lazy) { + $this->parseGroupRule($this->rule); + $this->rule = null; + } + + return $this; + } + + /** + * 解析分组和域名的路由规则及绑定 + * @access public + * @param mixed $rule 路由规则 + * @return void + */ + public function parseGroupRule($rule) + { + $origin = $this->router->getGroup(); + $this->router->setGroup($this); + + if ($rule instanceof \Closure) { + Container::getInstance()->invokeFunction($rule); + } elseif (is_array($rule)) { + $this->addRules($rule); + } elseif (is_string($rule) && $rule) { + $this->router->bind($rule, $this->domain); + } + + $this->router->setGroup($origin); + } + + /** + * 检测分组路由 + * @access public + * @param Request $request 请求对象 + * @param array $rules 路由规则 + * @param string $url 访问地址 + * @param bool $completeMatch 路由是否完全匹配 + * @return Dispatch|false + */ + protected function checkMergeRuleRegex($request, &$rules, $url, $completeMatch) + { + $depr = $this->router->config('pathinfo_depr'); + $url = $depr . str_replace('|', $depr, $url); + + foreach ($rules as $key => $item) { + if ($item instanceof RuleItem) { + $rule = $depr . str_replace('/', $depr, $item->getRule()); + if ($depr == $rule && $depr != $url) { + unset($rules[$key]); + continue; + } + + $complete = null !== $item->getOption('complete_match') ? $item->getOption('complete_match') : $completeMatch; + + if (false === strpos($rule, '<')) { + if (0 === strcasecmp($rule, $url) || (!$complete && 0 === strncasecmp($rule, $url, strlen($rule)))) { + return $item->checkRule($request, $url, []); + } + + unset($rules[$key]); + continue; + } + + $slash = preg_quote('/-' . $depr, '/'); + + if ($matchRule = preg_split('/[' . $slash . ']<\w+\??>/', $rule, 2)) { + if ($matchRule[0] && 0 !== strncasecmp($rule, $url, strlen($matchRule[0]))) { + unset($rules[$key]); + continue; + } + } + + if (preg_match_all('/[' . $slash . ']??/', $rule, $matches)) { + unset($rules[$key]); + $pattern = array_merge($this->getPattern(), $item->getPattern()); + $option = array_merge($this->getOption(), $item->getOption()); + + $regex[$key] = $this->buildRuleRegex($rule, $matches[0], $pattern, $option, $complete, '_THINK_' . $key); + $items[$key] = $item; + } + } + } + + if (empty($regex)) { + return false; + } + + try { + $result = preg_match('/^(?:' . implode('|', $regex) . ')/u', $url, $match); + } catch (\Exception $e) { + throw new Exception('route pattern error'); + } + + if ($result) { + $var = []; + foreach ($match as $key => $val) { + if (is_string($key) && '' !== $val) { + list($name, $pos) = explode('_THINK_', $key); + + $var[$name] = $val; + } + } + + if (!isset($pos)) { + foreach ($regex as $key => $item) { + if (0 === strpos(str_replace(['\/', '\-', '\\' . $depr], ['/', '-', $depr], $item), $match[0])) { + $pos = $key; + break; + } + } + } + + $rule = $items[$pos]->getRule(); + $array = $this->router->getRule($rule); + + foreach ($array as $item) { + if (in_array($item->getMethod(), ['*', strtolower($request->method())])) { + $result = $item->checkRule($request, $url, $var); + + if (false !== $result) { + return $result; + } + } + } + } + + return false; + } + + /** + * 获取分组的MISS路由 + * @access public + * @return RuleItem|null + */ + public function getMissRule() + { + return $this->miss; + } + + /** + * 获取分组的自动路由 + * @access public + * @return string + */ + public function getAutoRule() + { + return $this->auto; + } + + /** + * 注册自动路由 + * @access public + * @param string $route 路由规则 + * @return void + */ + public function addAutoRule($route) + { + $this->auto = $route; + } + + /** + * 注册MISS路由 + * @access public + * @param string $route 路由地址 + * @param string $method 请求类型 + * @param array $option 路由参数 + * @return RuleItem + */ + public function addMissRule($route, $method = '*', $option = []) + { + // 创建路由规则实例 + $ruleItem = new RuleItem($this->router, $this, null, '', $route, strtolower($method), $option); + + $this->miss = $ruleItem; + + return $ruleItem; + } + + /** + * 添加分组下的路由规则或者子分组 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $method 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return $this + */ + public function addRule($rule, $route, $method = '*', $option = [], $pattern = []) + { + // 读取路由标识 + if (is_array($rule)) { + $name = $rule[0]; + $rule = $rule[1]; + } elseif (is_string($route)) { + $name = $route; + } else { + $name = null; + } + + $method = strtolower($method); + + if ('/' === $rule || '' === $rule) { + // 首页自动完整匹配 + $rule .= '$'; + } + + // 创建路由规则实例 + $ruleItem = new RuleItem($this->router, $this, $name, $rule, $route, $method, $option, $pattern); + + if (!empty($option['cross_domain'])) { + $this->router->setCrossDomainRule($ruleItem, $method); + } + + $this->addRuleItem($ruleItem, $method); + + return $ruleItem; + } + + /** + * 批量注册路由规则 + * @access public + * @param array $rules 路由规则 + * @param string $method 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public function addRules($rules, $method = '*', $option = [], $pattern = []) + { + foreach ($rules as $key => $val) { + if (is_numeric($key)) { + $key = array_shift($val); + } + + if (is_array($val)) { + $route = array_shift($val); + $option = $val ? array_shift($val) : []; + $pattern = $val ? array_shift($val) : []; + } else { + $route = $val; + } + + $this->addRule($key, $route, $method, $option, $pattern); + } + } + + public function addRuleItem($rule, $method = '*') + { + if (strpos($method, '|')) { + $rule->method($method); + $method = '*'; + } + + $this->rules[$method][] = $rule; + + return $this; + } + + /** + * 设置分组的路由前缀 + * @access public + * @param string $prefix + * @return $this + */ + public function prefix($prefix) + { + if ($this->parent && $this->parent->getOption('prefix')) { + $prefix = $this->parent->getOption('prefix') . $prefix; + } + + return $this->option('prefix', $prefix); + } + + /** + * 设置资源允许 + * @access public + * @param array $only + * @return $this + */ + public function only($only) + { + return $this->option('only', $only); + } + + /** + * 设置资源排除 + * @access public + * @param array $except + * @return $this + */ + public function except($except) + { + return $this->option('except', $except); + } + + /** + * 设置资源路由的变量 + * @access public + * @param array $vars + * @return $this + */ + public function vars($vars) + { + return $this->option('var', $vars); + } + + /** + * 合并分组的路由规则正则 + * @access public + * @param bool $merge + * @return $this + */ + public function mergeRuleRegex($merge = true) + { + return $this->option('merge_rule_regex', $merge); + } + + /** + * 获取完整分组Name + * @access public + * @return string + */ + public function getFullName() + { + return $this->fullName; + } + + /** + * 获取分组的路由规则 + * @access public + * @param string $method + * @return array + */ + public function getRules($method = '') + { + if ('' === $method) { + return $this->rules; + } + + return isset($this->rules[strtolower($method)]) ? $this->rules[strtolower($method)] : []; + } + + /** + * 清空分组下的路由规则 + * @access public + * @return void + */ + public function clear() + { + $this->rules = [ + '*' => [], + 'get' => [], + 'post' => [], + 'put' => [], + 'patch' => [], + 'delete' => [], + 'head' => [], + 'options' => [], + ]; + } +} diff --git a/thinkphp/library/think/route/RuleItem.php b/thinkphp/library/think/route/RuleItem.php new file mode 100644 index 0000000..a05d2de --- /dev/null +++ b/thinkphp/library/think/route/RuleItem.php @@ -0,0 +1,292 @@ + +// +---------------------------------------------------------------------- + +namespace think\route; + +use think\Container; +use think\Exception; +use think\Route; + +class RuleItem extends Rule +{ + protected $hasSetRule; + + /** + * 架构函数 + * @access public + * @param Route $router 路由实例 + * @param RuleGroup $parent 上级对象 + * @param string $name 路由标识 + * @param string|array $rule 路由规则 + * @param string|\Closure $route 路由地址 + * @param string $method 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + */ + public function __construct(Route $router, RuleGroup $parent, $name, $rule, $route, $method = '*', $option = [], $pattern = []) + { + $this->router = $router; + $this->parent = $parent; + $this->name = $name; + $this->route = $route; + $this->method = $method; + $this->option = $option; + $this->pattern = $pattern; + + $this->setRule($rule); + + if (!empty($option['cross_domain'])) { + $this->router->setCrossDomainRule($this, $method); + } + } + + /** + * 路由规则预处理 + * @access public + * @param string $rule 路由规则 + * @return void + */ + public function setRule($rule) + { + if ('$' == substr($rule, -1, 1)) { + // 是否完整匹配 + $rule = substr($rule, 0, -1); + + $this->option['complete_match'] = true; + } + + $rule = '/' != $rule ? ltrim($rule, '/') : ''; + + if ($this->parent && $prefix = $this->parent->getFullName()) { + $rule = $prefix . ($rule ? '/' . ltrim($rule, '/') : ''); + } + + if (false !== strpos($rule, ':')) { + $this->rule = preg_replace(['/\[\:(\w+)\]/', '/\:(\w+)/'], ['<\1?>', '<\1>'], $rule); + } else { + $this->rule = $rule; + } + + // 生成路由标识的快捷访问 + $this->setRuleName(); + } + + /** + * 检查后缀 + * @access public + * @param string $ext + * @return $this + */ + public function ext($ext = '') + { + $this->option('ext', $ext); + $this->setRuleName(true); + + return $this; + } + + /** + * 设置别名 + * @access public + * @param string $name + * @return $this + */ + public function name($name) + { + $this->name = $name; + $this->setRuleName(true); + + return $this; + } + + /** + * 设置路由标识 用于URL反解生成 + * @access protected + * @param bool $first 是否插入开头 + * @return void + */ + protected function setRuleName($first = false) + { + if ($this->name) { + $vars = $this->parseVar($this->rule); + $name = strtolower($this->name); + + if (isset($this->option['ext'])) { + $suffix = $this->option['ext']; + } elseif ($this->parent->getOption('ext')) { + $suffix = $this->parent->getOption('ext'); + } else { + $suffix = null; + } + + $value = [$this->rule, $vars, $this->parent->getDomain(), $suffix, $this->method]; + + Container::get('rule_name')->set($name, $value, $first); + } + + if (!$this->hasSetRule) { + Container::get('rule_name')->setRule($this->rule, $this); + $this->hasSetRule = true; + } + } + + /** + * 检测路由 + * @access public + * @param Request $request 请求对象 + * @param string $url 访问地址 + * @param array $match 匹配路由变量 + * @param bool $completeMatch 路由是否完全匹配 + * @return Dispatch|false + */ + public function checkRule($request, $url, $match = null, $completeMatch = false) + { + // 检查参数有效性 + if (!$this->checkOption($this->option, $request)) { + return false; + } + + // 合并分组参数 + $option = $this->mergeGroupOptions(); + + $url = $this->urlSuffixCheck($request, $url, $option); + + if (is_null($match)) { + $match = $this->match($url, $option, $completeMatch); + } + + if (false !== $match) { + if (!empty($option['cross_domain'])) { + if ($dispatch = $this->checkCrossDomain($request)) { + // 允许跨域 + return $dispatch; + } + + $option['header'] = $this->option['header']; + } + + // 检查前置行为 + if (isset($option['before']) && false === $this->checkBefore($option['before'])) { + return false; + } + + return $this->parseRule($request, $this->rule, $this->route, $url, $option, $match); + } + + return false; + } + + /** + * 检测路由(含路由匹配) + * @access public + * @param Request $request 请求对象 + * @param string $url 访问地址 + * @param string $depr 路径分隔符 + * @param bool $completeMatch 路由是否完全匹配 + * @return Dispatch|false + */ + public function check($request, $url, $completeMatch = false) + { + return $this->checkRule($request, $url, null, $completeMatch); + } + + /** + * URL后缀及Slash检查 + * @access protected + * @param Request $request 请求对象 + * @param string $url 访问地址 + * @param array $option 路由参数 + * @return string + */ + protected function urlSuffixCheck($request, $url, $option = []) + { + // 是否区分 / 地址访问 + if (!empty($option['remove_slash']) && '/' != $this->rule) { + $this->rule = rtrim($this->rule, '/'); + $url = rtrim($url, '|'); + } + + if (isset($option['ext'])) { + // 路由ext参数 优先于系统配置的URL伪静态后缀参数 + $url = preg_replace('/\.(' . $request->ext() . ')$/i', '', $url); + } + + return $url; + } + + /** + * 检测URL和规则路由是否匹配 + * @access private + * @param string $url URL地址 + * @param array $option 路由参数 + * @param bool $completeMatch 路由是否完全匹配 + * @return array|false + */ + private function match($url, $option, $completeMatch) + { + if (isset($option['complete_match'])) { + $completeMatch = $option['complete_match']; + } + + $pattern = array_merge($this->parent->getPattern(), $this->pattern); + $depr = $this->router->config('pathinfo_depr'); + + // 检查完整规则定义 + if (isset($pattern['__url__']) && !preg_match(0 === strpos($pattern['__url__'], '/') ? $pattern['__url__'] : '/^' . $pattern['__url__'] . '/', str_replace('|', $depr, $url))) { + return false; + } + + $var = []; + $url = $depr . str_replace('|', $depr, $url); + $rule = $depr . str_replace('/', $depr, $this->rule); + + if ($depr == $rule && $depr != $url) { + return false; + } + + if (false === strpos($rule, '<')) { + if (0 === strcasecmp($rule, $url) || (!$completeMatch && 0 === strncasecmp($rule . $depr, $url . $depr, strlen($rule . $depr)))) { + return $var; + } + return false; + } + + $slash = preg_quote('/-' . $depr, '/'); + + if ($matchRule = preg_split('/[' . $slash . ']?<\w+\??>/', $rule, 2)) { + if ($matchRule[0] && 0 !== strncasecmp($rule, $url, strlen($matchRule[0]))) { + return false; + } + } + + if (preg_match_all('/[' . $slash . ']??/', $rule, $matches)) { + $regex = $this->buildRuleRegex($rule, $matches[0], $pattern, $option, $completeMatch); + + try { + if (!preg_match('/^' . $regex . ($completeMatch ? '$' : '') . '/u', $url, $match)) { + return false; + } + } catch (\Exception $e) { + throw new Exception('route pattern error'); + } + + foreach ($match as $key => $val) { + if (is_string($key)) { + $var[$key] = $val; + } + } + } + + // 成功匹配后返回URL中的动态变量数组 + return $var; + } + +} diff --git a/thinkphp/library/think/route/RuleName.php b/thinkphp/library/think/route/RuleName.php new file mode 100644 index 0000000..202fb0e --- /dev/null +++ b/thinkphp/library/think/route/RuleName.php @@ -0,0 +1,147 @@ + +// +---------------------------------------------------------------------- + +namespace think\route; + +class RuleName +{ + protected $item = []; + protected $rule = []; + + /** + * 注册路由标识 + * @access public + * @param string $name 路由标识 + * @param array $value 路由规则 + * @param bool $first 是否置顶 + * @return void + */ + public function set($name, $value, $first = false) + { + if ($first && isset($this->item[$name])) { + array_unshift($this->item[$name], $value); + } else { + $this->item[$name][] = $value; + } + } + + /** + * 注册路由规则 + * @access public + * @param string $rule 路由规则 + * @param RuleItem $route 路由 + * @return void + */ + public function setRule($rule, $route) + { + $this->rule[$route->getDomain()][$rule][$route->getMethod()] = $route; + } + + /** + * 根据路由规则获取路由对象(列表) + * @access public + * @param string $name 路由标识 + * @param string $domain 域名 + * @return array + */ + public function getRule($rule, $domain = null) + { + return isset($this->rule[$domain][$rule]) ? $this->rule[$domain][$rule] : []; + } + + /** + * 获取全部路由列表 + * @access public + * @param string $domain 域名 + * @return array + */ + public function getRuleList($domain = null) + { + $list = []; + + foreach ($this->rule as $ruleDomain => $rules) { + foreach ($rules as $rule => $items) { + foreach ($items as $item) { + $val['domain'] = $ruleDomain; + + foreach (['method', 'rule', 'name', 'route', 'pattern', 'option'] as $param) { + $call = 'get' . $param; + $val[$param] = $item->$call(); + } + + $list[$ruleDomain][] = $val; + } + } + } + + if ($domain) { + return isset($list[$domain]) ? $list[$domain] : []; + } + + return $list; + } + + /** + * 导入路由标识 + * @access public + * @param array $name 路由标识 + * @return void + */ + public function import($item) + { + $this->item = $item; + } + + /** + * 根据路由标识获取路由信息(用于URL生成) + * @access public + * @param string $name 路由标识 + * @param string $domain 域名 + * @return array|null + */ + public function get($name = null, $domain = null, $method = '*') + { + if (is_null($name)) { + return $this->item; + } + + $name = strtolower($name); + $method = strtolower($method); + + if (isset($this->item[$name])) { + if (is_null($domain)) { + $result = $this->item[$name]; + } else { + $result = []; + foreach ($this->item[$name] as $item) { + if ($item[2] == $domain && ('*' == $item[4] || $method == $item[4])) { + $result[] = $item; + } + } + } + } else { + $result = null; + } + + return $result; + } + + /** + * 清空路由规则 + * @access public + * @return void + */ + public function clear() + { + $this->item = []; + $this->rule = []; + } +} diff --git a/thinkphp/library/think/route/dispatch/Callback.php b/thinkphp/library/think/route/dispatch/Callback.php new file mode 100644 index 0000000..ca76fc9 --- /dev/null +++ b/thinkphp/library/think/route/dispatch/Callback.php @@ -0,0 +1,26 @@ + +// +---------------------------------------------------------------------- + +namespace think\route\dispatch; + +use think\route\Dispatch; + +class Callback extends Dispatch +{ + public function exec() + { + // 执行回调方法 + $vars = array_merge($this->request->param(), $this->param); + + return $this->app->invoke($this->dispatch, $vars); + } + +} diff --git a/thinkphp/library/think/route/dispatch/Controller.php b/thinkphp/library/think/route/dispatch/Controller.php new file mode 100644 index 0000000..1de8299 --- /dev/null +++ b/thinkphp/library/think/route/dispatch/Controller.php @@ -0,0 +1,30 @@ + +// +---------------------------------------------------------------------- + +namespace think\route\dispatch; + +use think\route\Dispatch; + +class Controller extends Dispatch +{ + public function exec() + { + // 执行控制器的操作方法 + $vars = array_merge($this->request->param(), $this->param); + + return $this->app->action( + $this->dispatch, $vars, + $this->rule->getConfig('url_controller_layer'), + $this->rule->getConfig('controller_suffix') + ); + } + +} diff --git a/thinkphp/library/think/route/dispatch/Module.php b/thinkphp/library/think/route/dispatch/Module.php new file mode 100644 index 0000000..40bd775 --- /dev/null +++ b/thinkphp/library/think/route/dispatch/Module.php @@ -0,0 +1,138 @@ + +// +---------------------------------------------------------------------- + +namespace think\route\dispatch; + +use ReflectionMethod; +use think\exception\ClassNotFoundException; +use think\exception\HttpException; +use think\Loader; +use think\Request; +use think\route\Dispatch; + +class Module extends Dispatch +{ + protected $controller; + protected $actionName; + + public function init() + { + parent::init(); + + $result = $this->dispatch; + + if (is_string($result)) { + $result = explode('/', $result); + } + + if ($this->rule->getConfig('app_multi_module')) { + // 多模块部署 + $module = strip_tags(strtolower($result[0] ?: $this->rule->getConfig('default_module'))); + $bind = $this->rule->getRouter()->getBind(); + $available = false; + + if ($bind && preg_match('/^[a-z]/is', $bind)) { + // 绑定模块 + list($bindModule) = explode('/', $bind); + if (empty($result[0])) { + $module = $bindModule; + } + $available = true; + } elseif (!in_array($module, $this->rule->getConfig('deny_module_list')) && is_dir($this->app->getAppPath() . $module)) { + $available = true; + } elseif ($this->rule->getConfig('empty_module')) { + $module = $this->rule->getConfig('empty_module'); + $available = true; + } + + // 模块初始化 + if ($module && $available) { + // 初始化模块 + $this->request->setModule($module); + $this->app->init($module); + } else { + throw new HttpException(404, 'module not exists:' . $module); + } + } + + // 是否自动转换控制器和操作名 + $convert = is_bool($this->convert) ? $this->convert : $this->rule->getConfig('url_convert'); + // 获取控制器名 + $controller = strip_tags($result[1] ?: $this->rule->getConfig('default_controller')); + + $this->controller = $convert ? strtolower($controller) : $controller; + + // 获取操作名 + $this->actionName = strip_tags($result[2] ?: $this->rule->getConfig('default_action')); + + // 设置当前请求的控制器、操作 + $this->request + ->setController(Loader::parseName($this->controller, 1)) + ->setAction($this->actionName); + + return $this; + } + + public function exec() + { + // 监听module_init + $this->app['hook']->listen('module_init'); + + try { + // 实例化控制器 + $instance = $this->app->controller($this->controller, + $this->rule->getConfig('url_controller_layer'), + $this->rule->getConfig('controller_suffix'), + $this->rule->getConfig('empty_controller')); + } catch (ClassNotFoundException $e) { + throw new HttpException(404, 'controller not exists:' . $e->getClass()); + } + + $this->app['middleware']->controller(function (Request $request, $next) use ($instance) { + // 获取当前操作名 + $action = $this->actionName . $this->rule->getConfig('action_suffix'); + + if (is_callable([$instance, $action])) { + // 执行操作方法 + $call = [$instance, $action]; + + // 严格获取当前操作方法名 + $reflect = new ReflectionMethod($instance, $action); + $methodName = $reflect->getName(); + $suffix = $this->rule->getConfig('action_suffix'); + $actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName; + $this->request->setAction($actionName); + + // 自动获取请求变量 + $vars = $this->rule->getConfig('url_param_type') + ? $this->request->route() + : $this->request->param(); + $vars = array_merge($vars, $this->param); + } elseif (is_callable([$instance, '_empty'])) { + // 空操作 + $call = [$instance, '_empty']; + $vars = [$this->actionName]; + $reflect = new ReflectionMethod($instance, '_empty'); + } else { + // 操作不存在 + throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()'); + } + + $this->app['hook']->listen('action_begin', $call); + + $data = $this->app->invokeReflectMethod($instance, $reflect, $vars); + + return $this->autoResponse($data); + }); + + return $this->app['middleware']->dispatch($this->request, 'controller'); + } +} diff --git a/thinkphp/library/think/route/dispatch/Redirect.php b/thinkphp/library/think/route/dispatch/Redirect.php new file mode 100644 index 0000000..fae2c9a --- /dev/null +++ b/thinkphp/library/think/route/dispatch/Redirect.php @@ -0,0 +1,23 @@ + +// +---------------------------------------------------------------------- + +namespace think\route\dispatch; + +use think\Response; +use think\route\Dispatch; + +class Redirect extends Dispatch +{ + public function exec() + { + return Response::create($this->dispatch, 'redirect')->code($this->code); + } +} diff --git a/thinkphp/library/think/route/dispatch/Response.php b/thinkphp/library/think/route/dispatch/Response.php new file mode 100644 index 0000000..66f4e5a --- /dev/null +++ b/thinkphp/library/think/route/dispatch/Response.php @@ -0,0 +1,23 @@ + +// +---------------------------------------------------------------------- + +namespace think\route\dispatch; + +use think\route\Dispatch; + +class Response extends Dispatch +{ + public function exec() + { + return $this->dispatch; + } + +} diff --git a/thinkphp/library/think/route/dispatch/Url.php b/thinkphp/library/think/route/dispatch/Url.php new file mode 100644 index 0000000..acc524e --- /dev/null +++ b/thinkphp/library/think/route/dispatch/Url.php @@ -0,0 +1,169 @@ + +// +---------------------------------------------------------------------- + +namespace think\route\dispatch; + +use think\exception\HttpException; +use think\Loader; +use think\route\Dispatch; + +class Url extends Dispatch +{ + public function init() + { + // 解析默认的URL规则 + $result = $this->parseUrl($this->dispatch); + + return (new Module($this->request, $this->rule, $result))->init(); + } + + public function exec() + {} + + /** + * 解析URL地址 + * @access protected + * @param string $url URL + * @return array + */ + protected function parseUrl($url) + { + $depr = $this->rule->getConfig('pathinfo_depr'); + $bind = $this->rule->getRouter()->getBind(); + + if (!empty($bind) && preg_match('/^[a-z]/is', $bind)) { + $bind = str_replace('/', $depr, $bind); + // 如果有模块/控制器绑定 + $url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr); + } + + list($path, $var) = $this->rule->parseUrlPath($url); + if (empty($path)) { + return [null, null, null]; + } + + // 解析模块 + $module = $this->rule->getConfig('app_multi_module') ? array_shift($path) : null; + + if ($this->param['auto_search']) { + $controller = $this->autoFindController($module, $path); + } else { + // 解析控制器 + $controller = !empty($path) ? array_shift($path) : null; + } + + if ($controller && !preg_match('/^[A-Za-z0-9][\w|\.]*$/', $controller)) { + throw new HttpException(404, 'controller not exists:' . $controller); + } + + // 解析操作 + $action = !empty($path) ? array_shift($path) : null; + + // 解析额外参数 + if ($path) { + if ($this->rule->getConfig('url_param_type')) { + $var += $path; + } else { + preg_replace_callback('/(\w+)\|([^\|]+)/', function ($match) use (&$var) { + $var[$match[1]] = strip_tags($match[2]); + }, implode('|', $path)); + } + } + + $panDomain = $this->request->panDomain(); + + if ($panDomain && $key = array_search('*', $var)) { + // 泛域名赋值 + $var[$key] = $panDomain; + } + + // 设置当前请求的参数 + $this->request->setRouteVars($var); + + // 封装路由 + $route = [$module, $controller, $action]; + + if ($this->hasDefinedRoute($route, $bind)) { + throw new HttpException(404, 'invalid request:' . str_replace('|', $depr, $url)); + } + + return $route; + } + + /** + * 检查URL是否已经定义过路由 + * @access protected + * @param string $route 路由信息 + * @param string $bind 绑定信息 + * @return bool + */ + protected function hasDefinedRoute($route, $bind) + { + list($module, $controller, $action) = $route; + + // 检查地址是否被定义过路由 + $name = strtolower($module . '/' . Loader::parseName($controller, 1) . '/' . $action); + + $name2 = ''; + + if (empty($module) || $module == $bind) { + $name2 = strtolower(Loader::parseName($controller, 1) . '/' . $action); + } + + $host = $this->request->host(true); + + $method = $this->request->method(); + + if ($this->rule->getRouter()->getName($name, $host, $method) || $this->rule->getRouter()->getName($name2, $host, $method)) { + return true; + } + + return false; + } + + /** + * 自动定位控制器类 + * @access protected + * @param string $module 模块名 + * @param array $path URL + * @return string + */ + protected function autoFindController($module, &$path) + { + $dir = $this->app->getAppPath() . ($module ? $module . '/' : '') . $this->rule->getConfig('url_controller_layer'); + $suffix = $this->app->getSuffix() || $this->rule->getConfig('controller_suffix') ? ucfirst($this->rule->getConfig('url_controller_layer')) : ''; + + $item = []; + $find = false; + + foreach ($path as $val) { + $item[] = $val; + $file = $dir . '/' . str_replace('.', '/', $val) . $suffix . '.php'; + $file = pathinfo($file, PATHINFO_DIRNAME) . '/' . Loader::parseName(pathinfo($file, PATHINFO_FILENAME), 1) . '.php'; + if (is_file($file)) { + $find = true; + break; + } else { + $dir .= '/' . Loader::parseName($val); + } + } + + if ($find) { + $controller = implode('.', $item); + $path = array_slice($path, count($item)); + } else { + $controller = array_shift($path); + } + + return $controller; + } + +} diff --git a/thinkphp/library/think/route/dispatch/View.php b/thinkphp/library/think/route/dispatch/View.php new file mode 100644 index 0000000..ea3ef11 --- /dev/null +++ b/thinkphp/library/think/route/dispatch/View.php @@ -0,0 +1,26 @@ + +// +---------------------------------------------------------------------- + +namespace think\route\dispatch; + +use think\Response; +use think\route\Dispatch; + +class View extends Dispatch +{ + public function exec() + { + // 渲染模板输出 + $vars = array_merge($this->request->param(), $this->param); + + return Response::create($this->dispatch, 'view')->assign($vars); + } +} diff --git a/thinkphp/library/think/session/driver/Memcache.php b/thinkphp/library/think/session/driver/Memcache.php new file mode 100644 index 0000000..40d7bb8 --- /dev/null +++ b/thinkphp/library/think/session/driver/Memcache.php @@ -0,0 +1,124 @@ + +// +---------------------------------------------------------------------- + +namespace think\session\driver; + +use SessionHandlerInterface; +use think\Exception; + +class Memcache implements SessionHandlerInterface +{ + protected $handler = null; + protected $config = [ + 'host' => '127.0.0.1', // memcache主机 + 'port' => 11211, // memcache端口 + 'expire' => 3600, // session有效期 + 'timeout' => 0, // 连接超时时间(单位:毫秒) + 'persistent' => true, // 长连接 + 'session_name' => '', // memcache key前缀 + ]; + + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 打开Session + * @access public + * @param string $savePath + * @param mixed $sessName + */ + public function open($savePath, $sessName) + { + // 检测php环境 + if (!extension_loaded('memcache')) { + throw new Exception('not support:memcache'); + } + + $this->handler = new \Memcache; + + // 支持集群 + $hosts = explode(',', $this->config['host']); + $ports = explode(',', $this->config['port']); + + if (empty($ports[0])) { + $ports[0] = 11211; + } + + // 建立连接 + foreach ((array) $hosts as $i => $host) { + $port = isset($ports[$i]) ? $ports[$i] : $ports[0]; + $this->config['timeout'] > 0 ? + $this->handler->addServer($host, $port, $this->config['persistent'], 1, $this->config['timeout']) : + $this->handler->addServer($host, $port, $this->config['persistent'], 1); + } + + return true; + } + + /** + * 关闭Session + * @access public + */ + public function close() + { + $this->gc(ini_get('session.gc_maxlifetime')); + $this->handler->close(); + $this->handler = null; + + return true; + } + + /** + * 读取Session + * @access public + * @param string $sessID + */ + public function read($sessID) + { + return (string) $this->handler->get($this->config['session_name'] . $sessID); + } + + /** + * 写入Session + * @access public + * @param string $sessID + * @param string $sessData + * @return bool + */ + public function write($sessID, $sessData) + { + return $this->handler->set($this->config['session_name'] . $sessID, $sessData, 0, $this->config['expire']); + } + + /** + * 删除Session + * @access public + * @param string $sessID + * @return bool + */ + public function destroy($sessID) + { + return $this->handler->delete($this->config['session_name'] . $sessID); + } + + /** + * Session 垃圾回收 + * @access public + * @param string $sessMaxLifeTime + * @return true + */ + public function gc($sessMaxLifeTime) + { + return true; + } +} diff --git a/thinkphp/library/think/session/driver/Memcached.php b/thinkphp/library/think/session/driver/Memcached.php new file mode 100644 index 0000000..074b2ff --- /dev/null +++ b/thinkphp/library/think/session/driver/Memcached.php @@ -0,0 +1,135 @@ + +// +---------------------------------------------------------------------- + +namespace think\session\driver; + +use SessionHandlerInterface; +use think\Exception; + +class Memcached implements SessionHandlerInterface +{ + protected $handler = null; + protected $config = [ + 'host' => '127.0.0.1', // memcache主机 + 'port' => 11211, // memcache端口 + 'expire' => 3600, // session有效期 + 'timeout' => 0, // 连接超时时间(单位:毫秒) + 'session_name' => '', // memcache key前缀 + 'username' => '', //账号 + 'password' => '', //密码 + ]; + + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 打开Session + * @access public + * @param string $savePath + * @param mixed $sessName + */ + public function open($savePath, $sessName) + { + // 检测php环境 + if (!extension_loaded('memcached')) { + throw new Exception('not support:memcached'); + } + + $this->handler = new \Memcached; + + // 设置连接超时时间(单位:毫秒) + if ($this->config['timeout'] > 0) { + $this->handler->setOption(\Memcached::OPT_CONNECT_TIMEOUT, $this->config['timeout']); + } + + // 支持集群 + $hosts = explode(',', $this->config['host']); + $ports = explode(',', $this->config['port']); + + if (empty($ports[0])) { + $ports[0] = 11211; + } + + // 建立连接 + $servers = []; + foreach ((array) $hosts as $i => $host) { + $servers[] = [$host, (isset($ports[$i]) ? $ports[$i] : $ports[0]), 1]; + } + + $this->handler->addServers($servers); + + if ('' != $this->config['username']) { + $this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); + $this->handler->setSaslAuthData($this->config['username'], $this->config['password']); + } + + return true; + } + + /** + * 关闭Session + * @access public + */ + public function close() + { + $this->gc(ini_get('session.gc_maxlifetime')); + $this->handler->quit(); + $this->handler = null; + + return true; + } + + /** + * 读取Session + * @access public + * @param string $sessID + */ + public function read($sessID) + { + return (string) $this->handler->get($this->config['session_name'] . $sessID); + } + + /** + * 写入Session + * @access public + * @param string $sessID + * @param string $sessData + * @return bool + */ + public function write($sessID, $sessData) + { + return $this->handler->set($this->config['session_name'] . $sessID, $sessData, $this->config['expire']); + } + + /** + * 删除Session + * @access public + * @param string $sessID + * @return bool + */ + public function destroy($sessID) + { + return $this->handler->delete($this->config['session_name'] . $sessID); + } + + /** + * Session 垃圾回收 + * @access public + * @param string $sessMaxLifeTime + * @return true + */ + public function gc($sessMaxLifeTime) + { + return true; + } +} diff --git a/thinkphp/library/think/session/driver/Redis.php b/thinkphp/library/think/session/driver/Redis.php new file mode 100644 index 0000000..5a0e7bc --- /dev/null +++ b/thinkphp/library/think/session/driver/Redis.php @@ -0,0 +1,179 @@ + +// +---------------------------------------------------------------------- + +namespace think\session\driver; + +use SessionHandlerInterface; +use think\Exception; + +class Redis implements SessionHandlerInterface +{ + /** @var \Redis */ + protected $handler = null; + protected $config = [ + 'host' => '127.0.0.1', // redis主机 + 'port' => 6379, // redis端口 + 'password' => '', // 密码 + 'select' => 0, // 操作库 + 'expire' => 3600, // 有效期(秒) + 'timeout' => 0, // 超时时间(秒) + 'persistent' => true, // 是否长连接 + 'session_name' => '', // sessionkey前缀 + ]; + + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 打开Session + * @access public + * @param string $savePath + * @param mixed $sessName + * @return bool + * @throws Exception + */ + public function open($savePath, $sessName) + { + if (extension_loaded('redis')) { + $this->handler = new \Redis; + + // 建立连接 + $func = $this->config['persistent'] ? 'pconnect' : 'connect'; + $this->handler->$func($this->config['host'], $this->config['port'], $this->config['timeout']); + + if ('' != $this->config['password']) { + $this->handler->auth($this->config['password']); + } + + if (0 != $this->config['select']) { + $this->handler->select($this->config['select']); + } + } elseif (class_exists('\Predis\Client')) { + $params = []; + foreach ($this->config as $key => $val) { + if (in_array($key, ['aggregate', 'cluster', 'connections', 'exceptions', 'prefix', 'profile', 'replication'])) { + $params[$key] = $val; + unset($this->config[$key]); + } + } + $this->handler = new \Predis\Client($this->config, $params); + } else { + throw new \BadFunctionCallException('not support: redis'); + } + + return true; + } + + /** + * 关闭Session + * @access public + */ + public function close() + { + $this->gc(ini_get('session.gc_maxlifetime')); + $this->handler->close(); + $this->handler = null; + + return true; + } + + /** + * 读取Session + * @access public + * @param string $sessID + * @return string + */ + public function read($sessID) + { + return (string) $this->handler->get($this->config['session_name'] . $sessID); + } + + /** + * 写入Session + * @access public + * @param string $sessID + * @param string $sessData + * @return bool + */ + public function write($sessID, $sessData) + { + if ($this->config['expire'] > 0) { + $result = $this->handler->setex($this->config['session_name'] . $sessID, $this->config['expire'], $sessData); + } else { + $result = $this->handler->set($this->config['session_name'] . $sessID, $sessData); + } + + return $result ? true : false; + } + + /** + * 删除Session + * @access public + * @param string $sessID + * @return bool + */ + public function destroy($sessID) + { + return $this->handler->del($this->config['session_name'] . $sessID) > 0; + } + + /** + * Session 垃圾回收 + * @access public + * @param string $sessMaxLifeTime + * @return bool + */ + public function gc($sessMaxLifeTime) + { + return true; + } + + /** + * Redis Session 驱动的加锁机制 + * @access public + * @param string $sessID 用于加锁的sessID + * @param integer $timeout 默认过期时间 + * @return bool + */ + public function lock($sessID, $timeout = 10) + { + if (null == $this->handler) { + $this->open('', ''); + } + + $lockKey = 'LOCK_PREFIX_' . $sessID; + // 使用setnx操作加锁 + $isLock = $this->handler->setnx($lockKey, 1); + if ($isLock) { + // 设置过期时间,防止死任务的出现 + $this->handler->expire($lockKey, $timeout); + return true; + } + + return false; + } + + /** + * Redis Session 驱动的解锁机制 + * @access public + * @param string $sessID 用于解锁的sessID + */ + public function unlock($sessID) + { + if (null == $this->handler) { + $this->open('', ''); + } + + $this->handler->del('LOCK_PREFIX_' . $sessID); + } +} diff --git a/thinkphp/library/think/template/TagLib.php b/thinkphp/library/think/template/TagLib.php new file mode 100644 index 0000000..bbbb2c0 --- /dev/null +++ b/thinkphp/library/think/template/TagLib.php @@ -0,0 +1,351 @@ + +// +---------------------------------------------------------------------- + +namespace think\template; + +use think\Exception; + +/** + * ThinkPHP标签库TagLib解析基类 + * @category Think + * @package Think + * @subpackage Template + * @author liu21st + */ +class TagLib +{ + + /** + * 标签库定义XML文件 + * @var string + * @access protected + */ + protected $xml = ''; + protected $tags = []; // 标签定义 + /** + * 标签库名称 + * @var string + * @access protected + */ + protected $tagLib = ''; + + /** + * 标签库标签列表 + * @var array + * @access protected + */ + protected $tagList = []; + + /** + * 标签库分析数组 + * @var array + * @access protected + */ + protected $parse = []; + + /** + * 标签库是否有效 + * @var bool + * @access protected + */ + protected $valid = false; + + /** + * 当前模板对象 + * @var object + * @access protected + */ + protected $tpl; + + protected $comparison = [' nheq ' => ' !== ', ' heq ' => ' === ', ' neq ' => ' != ', ' eq ' => ' == ', ' egt ' => ' >= ', ' gt ' => ' > ', ' elt ' => ' <= ', ' lt ' => ' < ']; + + /** + * 架构函数 + * @access public + * @param \stdClass $template 模板引擎对象 + */ + public function __construct($template) + { + $this->tpl = $template; + } + + /** + * 按签标库替换页面中的标签 + * @access public + * @param string $content 模板内容 + * @param string $lib 标签库名 + * @return void + */ + public function parseTag(&$content, $lib = '') + { + $tags = []; + $lib = $lib ? strtolower($lib) . ':' : ''; + + foreach ($this->tags as $name => $val) { + $close = !isset($val['close']) || $val['close'] ? 1 : 0; + $tags[$close][$lib . $name] = $name; + if (isset($val['alias'])) { + // 别名设置 + $array = (array) $val['alias']; + foreach (explode(',', $array[0]) as $v) { + $tags[$close][$lib . $v] = $name; + } + } + } + + // 闭合标签 + if (!empty($tags[1])) { + $nodes = []; + $regex = $this->getRegex(array_keys($tags[1]), 1); + if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { + $right = []; + foreach ($matches as $match) { + if ('' == $match[1][0]) { + $name = strtolower($match[2][0]); + // 如果有没闭合的标签头则取出最后一个 + if (!empty($right[$name])) { + // $match[0][1]为标签结束符在模板中的位置 + $nodes[$match[0][1]] = [ + 'name' => $name, + 'begin' => array_pop($right[$name]), // 标签开始符 + 'end' => $match[0], // 标签结束符 + ]; + } + } else { + // 标签头压入栈 + $right[strtolower($match[1][0])][] = $match[0]; + } + } + unset($right, $matches); + // 按标签在模板中的位置从后向前排序 + krsort($nodes); + } + + $break = ''; + if ($nodes) { + $beginArray = []; + // 标签替换 从后向前 + foreach ($nodes as $pos => $node) { + // 对应的标签名 + $name = $tags[1][$node['name']]; + $alias = $lib . $name != $node['name'] ? ($lib ? strstr($node['name'], $lib) : $node['name']) : ''; + + // 解析标签属性 + $attrs = $this->parseAttr($node['begin'][0], $name, $alias); + $method = 'tag' . $name; + + // 读取标签库中对应的标签内容 replace[0]用来替换标签头,replace[1]用来替换标签尾 + $replace = explode($break, $this->$method($attrs, $break)); + + if (count($replace) > 1) { + while ($beginArray) { + $begin = end($beginArray); + // 判断当前标签尾的位置是否在栈中最后一个标签头的后面,是则为子标签 + if ($node['end'][1] > $begin['pos']) { + break; + } else { + // 不为子标签时,取出栈中最后一个标签头 + $begin = array_pop($beginArray); + // 替换标签头部 + $content = substr_replace($content, $begin['str'], $begin['pos'], $begin['len']); + } + } + // 替换标签尾部 + $content = substr_replace($content, $replace[1], $node['end'][1], strlen($node['end'][0])); + // 把标签头压入栈 + $beginArray[] = ['pos' => $node['begin'][1], 'len' => strlen($node['begin'][0]), 'str' => $replace[0]]; + } + } + + while ($beginArray) { + $begin = array_pop($beginArray); + // 替换标签头部 + $content = substr_replace($content, $begin['str'], $begin['pos'], $begin['len']); + } + } + } + // 自闭合标签 + if (!empty($tags[0])) { + $regex = $this->getRegex(array_keys($tags[0]), 0); + $content = preg_replace_callback($regex, function ($matches) use (&$tags, &$lib) { + // 对应的标签名 + $name = $tags[0][strtolower($matches[1])]; + $alias = $lib . $name != $matches[1] ? ($lib ? strstr($matches[1], $lib) : $matches[1]) : ''; + // 解析标签属性 + $attrs = $this->parseAttr($matches[0], $name, $alias); + $method = 'tag' . $name; + return $this->$method($attrs, ''); + }, $content); + } + + return; + } + + /** + * 按标签生成正则 + * @access public + * @param array|string $tags 标签名 + * @param boolean $close 是否为闭合标签 + * @return string + */ + public function getRegex($tags, $close) + { + $begin = $this->tpl->config('taglib_begin'); + $end = $this->tpl->config('taglib_end'); + $single = strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1 ? true : false; + $tagName = is_array($tags) ? implode('|', $tags) : $tags; + + if ($single) { + if ($close) { + // 如果是闭合标签 + $regex = $begin . '(?:(' . $tagName . ')\b(?>[^' . $end . ']*)|\/(' . $tagName . '))' . $end; + } else { + $regex = $begin . '(' . $tagName . ')\b(?>[^' . $end . ']*)' . $end; + } + } else { + if ($close) { + // 如果是闭合标签 + $regex = $begin . '(?:(' . $tagName . ')\b(?>(?:(?!' . $end . ').)*)|\/(' . $tagName . '))' . $end; + } else { + $regex = $begin . '(' . $tagName . ')\b(?>(?:(?!' . $end . ').)*)' . $end; + } + } + + return '/' . $regex . '/is'; + } + + /** + * 分析标签属性 正则方式 + * @access public + * @param string $str 标签属性字符串 + * @param string $name 标签名 + * @param string $alias 别名 + * @return array + */ + public function parseAttr($str, $name, $alias = '') + { + $regex = '/\s+(?>(?P[\w-]+)\s*)=(?>\s*)([\"\'])(?P(?:(?!\\2).)*)\\2/is'; + $result = []; + + if (preg_match_all($regex, $str, $matches)) { + foreach ($matches['name'] as $key => $val) { + $result[$val] = $matches['value'][$key]; + } + + if (!isset($this->tags[$name])) { + // 检测是否存在别名定义 + foreach ($this->tags as $key => $val) { + if (isset($val['alias'])) { + $array = (array) $val['alias']; + if (in_array($name, explode(',', $array[0]))) { + $tag = $val; + $type = !empty($array[1]) ? $array[1] : 'type'; + $result[$type] = $name; + break; + } + } + } + } else { + $tag = $this->tags[$name]; + // 设置了标签别名 + if (!empty($alias) && isset($tag['alias'])) { + $type = !empty($tag['alias'][1]) ? $tag['alias'][1] : 'type'; + $result[$type] = $alias; + } + } + + if (!empty($tag['must'])) { + $must = explode(',', $tag['must']); + foreach ($must as $name) { + if (!isset($result[$name])) { + throw new Exception('tag attr must:' . $name); + } + } + } + } else { + // 允许直接使用表达式的标签 + if (!empty($this->tags[$name]['expression'])) { + static $_taglibs; + if (!isset($_taglibs[$name])) { + $_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]); + // 清除自闭合标签尾部/ + $result['expression'] = rtrim($result['expression'], '/'); + $result['expression'] = trim($result['expression']); + } elseif (empty($this->tags[$name]) || !empty($this->tags[$name]['attr'])) { + throw new Exception('tag error:' . $name); + } + } + + return $result; + } + + /** + * 解析条件表达式 + * @access public + * @param string $condition 表达式标签内容 + * @return string + */ + public function parseCondition($condition) + { + if (!strpos($condition, '::') && strpos($condition, ':')) { + $condition = ' ' . substr(strstr($condition, ':'), 1); + } + + $condition = str_ireplace(array_keys($this->comparison), array_values($this->comparison), $condition); + $this->tpl->parseVar($condition); + + // $this->tpl->parseVarFunction($condition); // XXX: 此句能解析表达式中用|分隔的函数,但表达式中如果有|、||这样的逻辑运算就产生了歧异 + return $condition; + } + + /** + * 自动识别构建变量 + * @access public + * @param string $name 变量描述 + * @return string + */ + public function autoBuildVar(&$name) + { + $flag = substr($name, 0, 1); + + if (':' == $flag) { + // 以:开头为函数调用,解析前去掉: + $name = substr($name, 1); + } elseif ('$' != $flag && preg_match('/[a-zA-Z_]/', $flag)) { + // XXX: 这句的写法可能还需要改进 + // 常量不需要解析 + if (defined($name)) { + return $name; + } + + // 不以$开头并且也不是常量,自动补上$前缀 + $name = '$' . $name; + } + + $this->tpl->parseVar($name); + $this->tpl->parseVarFunction($name, false); + + return $name; + } + + /** + * 获取标签列表 + * @access public + * @return array + */ + public function getTags() + { + return $this->tags; + } +} diff --git a/thinkphp/library/think/template/driver/File.php b/thinkphp/library/think/template/driver/File.php new file mode 100644 index 0000000..3b96a0f --- /dev/null +++ b/thinkphp/library/think/template/driver/File.php @@ -0,0 +1,83 @@ + +// +---------------------------------------------------------------------- + +namespace think\template\driver; + +use think\Exception; + +class File +{ + protected $cacheFile; + + /** + * 写入编译缓存 + * @access public + * @param string $cacheFile 缓存的文件名 + * @param string $content 缓存的内容 + * @return void|array + */ + public function write($cacheFile, $content) + { + // 检测模板目录 + $dir = dirname($cacheFile); + + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + + // 生成模板缓存文件 + if (false === file_put_contents($cacheFile, $content)) { + throw new Exception('cache write error:' . $cacheFile, 11602); + } + } + + /** + * 读取编译编译 + * @access public + * @param string $cacheFile 缓存的文件名 + * @param array $vars 变量数组 + * @return void + */ + public function read($cacheFile, $vars = []) + { + $this->cacheFile = $cacheFile; + + if (!empty($vars) && is_array($vars)) { + // 模板阵列变量分解成为独立变量 + extract($vars, EXTR_OVERWRITE); + } + + //载入模版缓存文件 + include $this->cacheFile; + } + + /** + * 检查编译缓存是否有效 + * @access public + * @param string $cacheFile 缓存的文件名 + * @param int $cacheTime 缓存时间 + * @return boolean + */ + public function check($cacheFile, $cacheTime) + { + // 缓存文件不存在, 直接返回false + if (!file_exists($cacheFile)) { + return false; + } + + if (0 != $cacheTime && time() > filemtime($cacheFile) + $cacheTime) { + // 缓存是否在有效期 + return false; + } + + return true; + } +} diff --git a/thinkphp/library/think/template/taglib/Cx.php b/thinkphp/library/think/template/taglib/Cx.php new file mode 100644 index 0000000..ad741f2 --- /dev/null +++ b/thinkphp/library/think/template/taglib/Cx.php @@ -0,0 +1,724 @@ + +// +---------------------------------------------------------------------- + +namespace think\template\taglib; + +use think\template\TagLib; + +/** + * CX标签库解析类 + * @category Think + * @package Think + * @subpackage Driver.Taglib + * @author liu21st + */ +class Cx extends Taglib +{ + + // 标签定义 + protected $tags = [ + // 标签定义: attr 属性列表 close 是否闭合(0 或者1 默认1) alias 标签别名 level 嵌套层次 + 'php' => ['attr' => ''], + 'volist' => ['attr' => 'name,id,offset,length,key,mod', 'alias' => 'iterate'], + 'foreach' => ['attr' => 'name,id,item,key,offset,length,mod', 'expression' => true], + 'if' => ['attr' => 'condition', 'expression' => true], + 'elseif' => ['attr' => 'condition', 'close' => 0, 'expression' => true], + 'else' => ['attr' => '', 'close' => 0], + 'switch' => ['attr' => 'name', 'expression' => true], + 'case' => ['attr' => 'value,break', 'expression' => true], + 'default' => ['attr' => '', 'close' => 0], + 'compare' => ['attr' => 'name,value,type', 'alias' => ['eq,equal,notequal,neq,gt,lt,egt,elt,heq,nheq', 'type']], + 'range' => ['attr' => 'name,value,type', 'alias' => ['in,notin,between,notbetween', 'type']], + 'empty' => ['attr' => 'name'], + 'notempty' => ['attr' => 'name'], + 'present' => ['attr' => 'name'], + 'notpresent' => ['attr' => 'name'], + 'defined' => ['attr' => 'name'], + 'notdefined' => ['attr' => 'name'], + 'load' => ['attr' => 'file,href,type,value,basepath', 'close' => 0, 'alias' => ['import,css,js', 'type']], + 'assign' => ['attr' => 'name,value', 'close' => 0], + 'define' => ['attr' => 'name,value', 'close' => 0], + 'for' => ['attr' => 'start,end,name,comparison,step'], + 'url' => ['attr' => 'link,vars,suffix,domain', 'close' => 0, 'expression' => true], + 'function' => ['attr' => 'name,vars,use,call'], + ]; + + /** + * php标签解析 + * 格式: + * {php}echo $name{/php} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagPhp($tag, $content) + { + $parseStr = ''; + return $parseStr; + } + + /** + * volist标签解析 循环输出数据集 + * 格式: + * {volist name="userList" id="user" empty=""} + * {user.username} + * {user.email} + * {/volist} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string|void + */ + public function tagVolist($tag, $content) + { + $name = $tag['name']; + $id = $tag['id']; + $empty = isset($tag['empty']) ? $tag['empty'] : ''; + $key = !empty($tag['key']) ? $tag['key'] : 'i'; + $mod = isset($tag['mod']) ? $tag['mod'] : '2'; + $offset = !empty($tag['offset']) && is_numeric($tag['offset']) ? intval($tag['offset']) : 0; + $length = !empty($tag['length']) && is_numeric($tag['length']) ? intval($tag['length']) : 'null'; + // 允许使用函数设定数据集 {$vo.name} + $parseStr = 'autoBuildVar($name); + $parseStr .= '$_result=' . $name . ';'; + $name = '$_result'; + } else { + $name = $this->autoBuildVar($name); + } + + $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection || ' . $name . ' instanceof \think\Paginator): $' . $key . ' = 0;'; + + // 设置了输出数组长度 + if (0 != $offset || 'null' != $length) { + $parseStr .= '$__LIST__ = is_array(' . $name . ') ? array_slice(' . $name . ',' . $offset . ',' . $length . ', true) : ' . $name . '->slice(' . $offset . ',' . $length . ', true); '; + } else { + $parseStr .= ' $__LIST__ = ' . $name . ';'; + } + + $parseStr .= 'if( count($__LIST__)==0 ) : echo "' . $empty . '" ;'; + $parseStr .= 'else: '; + $parseStr .= 'foreach($__LIST__ as $key=>$' . $id . '): '; + $parseStr .= '$mod = ($' . $key . ' % ' . $mod . ' );'; + $parseStr .= '++$' . $key . ';?>'; + $parseStr .= $content; + $parseStr .= ''; + + if (!empty($parseStr)) { + return $parseStr; + } + + return; + } + + /** + * foreach标签解析 循环输出数据集 + * 格式: + * {foreach name="userList" id="user" key="key" index="i" mod="2" offset="3" length="5" empty=""} + * {user.username} + * {/foreach} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string|void + */ + public function tagForeach($tag, $content) + { + // 直接使用表达式 + if (!empty($tag['expression'])) { + $expression = ltrim(rtrim($tag['expression'], ')'), '('); + $expression = $this->autoBuildVar($expression); + $parseStr = ''; + $parseStr .= $content; + $parseStr .= ''; + return $parseStr; + } + + $name = $tag['name']; + $key = !empty($tag['key']) ? $tag['key'] : 'key'; + $item = !empty($tag['id']) ? $tag['id'] : $tag['item']; + $empty = isset($tag['empty']) ? $tag['empty'] : ''; + $offset = !empty($tag['offset']) && is_numeric($tag['offset']) ? intval($tag['offset']) : 0; + $length = !empty($tag['length']) && is_numeric($tag['length']) ? intval($tag['length']) : 'null'; + + $parseStr = 'autoBuildVar($name); + $parseStr .= $var . '=' . $name . '; '; + $name = $var; + } else { + $name = $this->autoBuildVar($name); + } + + $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection || ' . $name . ' instanceof \think\Paginator): '; + + // 设置了输出数组长度 + if (0 != $offset || 'null' != $length) { + if (!isset($var)) { + $var = '$_' . uniqid(); + } + $parseStr .= $var . ' = is_array(' . $name . ') ? array_slice(' . $name . ',' . $offset . ',' . $length . ', true) : ' . $name . '->slice(' . $offset . ',' . $length . ', true); '; + } else { + $var = &$name; + } + + $parseStr .= 'if( count(' . $var . ')==0 ) : echo "' . $empty . '" ;'; + $parseStr .= 'else: '; + + // 设置了索引项 + if (isset($tag['index'])) { + $index = $tag['index']; + $parseStr .= '$' . $index . '=0; '; + } + + $parseStr .= 'foreach(' . $var . ' as $' . $key . '=>$' . $item . '): '; + + // 设置了索引项 + if (isset($tag['index'])) { + $index = $tag['index']; + if (isset($tag['mod'])) { + $mod = (int) $tag['mod']; + $parseStr .= '$mod = ($' . $index . ' % ' . $mod . '); '; + } + $parseStr .= '++$' . $index . '; '; + } + + $parseStr .= '?>'; + // 循环体中的内容 + $parseStr .= $content; + $parseStr .= ''; + + if (!empty($parseStr)) { + return $parseStr; + } + + return; + } + + /** + * if标签解析 + * 格式: + * {if condition=" $a eq 1"} + * {elseif condition="$a eq 2" /} + * {else /} + * {/if} + * 表达式支持 eq neq gt egt lt elt == > >= < <= or and || && + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagIf($tag, $content) + { + $condition = !empty($tag['expression']) ? $tag['expression'] : $tag['condition']; + $condition = $this->parseCondition($condition); + $parseStr = '' . $content . ''; + + return $parseStr; + } + + /** + * elseif标签解析 + * 格式:见if标签 + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagElseif($tag, $content) + { + $condition = !empty($tag['expression']) ? $tag['expression'] : $tag['condition']; + $condition = $this->parseCondition($condition); + $parseStr = ''; + + return $parseStr; + } + + /** + * else标签解析 + * 格式:见if标签 + * @access public + * @param array $tag 标签属性 + * @return string + */ + public function tagElse($tag) + { + $parseStr = ''; + + return $parseStr; + } + + /** + * switch标签解析 + * 格式: + * {switch name="a.name"} + * {case value="1" break="false"}1{/case} + * {case value="2" }2{/case} + * {default /}other + * {/switch} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagSwitch($tag, $content) + { + $name = !empty($tag['expression']) ? $tag['expression'] : $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = '' . $content . ''; + + return $parseStr; + } + + /** + * case标签解析 需要配合switch才有效 + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagCase($tag, $content) + { + $value = isset($tag['expression']) ? $tag['expression'] : $tag['value']; + $flag = substr($value, 0, 1); + + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($value); + $value = 'case ' . $value . ':'; + } elseif (strpos($value, '|')) { + $values = explode('|', $value); + $value = ''; + foreach ($values as $val) { + $value .= 'case "' . addslashes($val) . '":'; + } + } else { + $value = 'case "' . $value . '":'; + } + + $parseStr = '' . $content; + $isBreak = isset($tag['break']) ? $tag['break'] : ''; + + if ('' == $isBreak || $isBreak) { + $parseStr .= ''; + } + + return $parseStr; + } + + /** + * default标签解析 需要配合switch才有效 + * 使用: {default /}ddfdf + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagDefault($tag) + { + $parseStr = ''; + + return $parseStr; + } + + /** + * compare标签解析 + * 用于值的比较 支持 eq neq gt lt egt elt heq nheq 默认是eq + * 格式: {compare name="" type="eq" value="" }content{/compare} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagCompare($tag, $content) + { + $name = $tag['name']; + $value = $tag['value']; + $type = isset($tag['type']) ? $tag['type'] : 'eq'; // 比较类型 + $name = $this->autoBuildVar($name); + $flag = substr($value, 0, 1); + + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($value); + } else { + $value = '\'' . $value . '\''; + } + + switch ($type) { + case 'equal': + $type = 'eq'; + break; + case 'notequal': + $type = 'neq'; + break; + } + $type = $this->parseCondition(' ' . $type . ' '); + $parseStr = '' . $content . ''; + + return $parseStr; + } + + /** + * range标签解析 + * 如果某个变量存在于某个范围 则输出内容 type= in 表示在范围内 否则表示在范围外 + * 格式: {range name="var|function" value="val" type='in|notin' }content{/range} + * example: {range name="a" value="1,2,3" type='in' }content{/range} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagRange($tag, $content) + { + $name = $tag['name']; + $value = $tag['value']; + $type = isset($tag['type']) ? $tag['type'] : 'in'; // 比较类型 + + $name = $this->autoBuildVar($name); + $flag = substr($value, 0, 1); + + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($value); + $str = 'is_array(' . $value . ')?' . $value . ':explode(\',\',' . $value . ')'; + } else { + $value = '"' . $value . '"'; + $str = 'explode(\',\',' . $value . ')'; + } + + if ('between' == $type) { + $parseStr = '= $_RANGE_VAR_[0] && ' . $name . '<= $_RANGE_VAR_[1]):?>' . $content . ''; + } elseif ('notbetween' == $type) { + $parseStr = '$_RANGE_VAR_[1]):?>' . $content . ''; + } else { + $fun = ('in' == $type) ? 'in_array' : '!in_array'; + $parseStr = '' . $content . ''; + } + + return $parseStr; + } + + /** + * present标签解析 + * 如果某个变量已经设置 则输出内容 + * 格式: {present name="" }content{/present} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagPresent($tag, $content) + { + $name = $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = '' . $content . ''; + + return $parseStr; + } + + /** + * notpresent标签解析 + * 如果某个变量没有设置,则输出内容 + * 格式: {notpresent name="" }content{/notpresent} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagNotpresent($tag, $content) + { + $name = $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = '' . $content . ''; + + return $parseStr; + } + + /** + * empty标签解析 + * 如果某个变量为empty 则输出内容 + * 格式: {empty name="" }content{/empty} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagEmpty($tag, $content) + { + $name = $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = 'isEmpty())): ?>' . $content . ''; + + return $parseStr; + } + + /** + * notempty标签解析 + * 如果某个变量不为empty 则输出内容 + * 格式: {notempty name="" }content{/notempty} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagNotempty($tag, $content) + { + $name = $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = 'isEmpty()))): ?>' . $content . ''; + + return $parseStr; + } + + /** + * 判断是否已经定义了该常量 + * {defined name='TXT'}已定义{/defined} + * @access public + * @param array $tag + * @param string $content + * @return string + */ + public function tagDefined($tag, $content) + { + $name = $tag['name']; + $parseStr = '' . $content . ''; + + return $parseStr; + } + + /** + * 判断是否没有定义了该常量 + * {notdefined name='TXT'}已定义{/notdefined} + * @access public + * @param array $tag + * @param string $content + * @return string + */ + public function tagNotdefined($tag, $content) + { + $name = $tag['name']; + $parseStr = '' . $content . ''; + + return $parseStr; + } + + /** + * load 标签解析 {load file="/static/js/base.js" /} + * 格式:{load file="/static/css/base.css" /} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagLoad($tag, $content) + { + $file = isset($tag['file']) ? $tag['file'] : $tag['href']; + $type = isset($tag['type']) ? strtolower($tag['type']) : ''; + + $parseStr = ''; + $endStr = ''; + + // 判断是否存在加载条件 允许使用函数判断(默认为isset) + if (isset($tag['value'])) { + $name = $tag['value']; + $name = $this->autoBuildVar($name); + $name = 'isset(' . $name . ')'; + $parseStr .= ''; + $endStr = ''; + } + + // 文件方式导入 + $array = explode(',', $file); + + foreach ($array as $val) { + $type = strtolower(substr(strrchr($val, '.'), 1)); + switch ($type) { + case 'js': + $parseStr .= ''; + break; + case 'css': + $parseStr .= ''; + break; + case 'php': + $parseStr .= ''; + break; + } + } + + return $parseStr . $endStr; + } + + /** + * assign标签解析 + * 在模板中给某个变量赋值 支持变量赋值 + * 格式: {assign name="" value="" /} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagAssign($tag, $content) + { + $name = $this->autoBuildVar($tag['name']); + $flag = substr($tag['value'], 0, 1); + + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($tag['value']); + } else { + $value = '\'' . $tag['value'] . '\''; + } + + $parseStr = ''; + + return $parseStr; + } + + /** + * define标签解析 + * 在模板中定义常量 支持变量赋值 + * 格式: {define name="" value="" /} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagDefine($tag, $content) + { + $name = '\'' . $tag['name'] . '\''; + $flag = substr($tag['value'], 0, 1); + + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($tag['value']); + } else { + $value = '\'' . $tag['value'] . '\''; + } + + $parseStr = ''; + + return $parseStr; + } + + /** + * for标签解析 + * 格式: + * {for start="" end="" comparison="" step="" name=""} + * content + * {/for} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagFor($tag, $content) + { + //设置默认值 + $start = 0; + $end = 0; + $step = 1; + $comparison = 'lt'; + $name = 'i'; + $rand = rand(); //添加随机数,防止嵌套变量冲突 + + //获取属性 + foreach ($tag as $key => $value) { + $value = trim($value); + $flag = substr($value, 0, 1); + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($value); + } + + switch ($key) { + case 'start': + $start = $value; + break; + case 'end': + $end = $value; + break; + case 'step': + $step = $value; + break; + case 'comparison': + $comparison = $value; + break; + case 'name': + $name = $value; + break; + } + } + + $parseStr = 'parseCondition('$' . $name . ' ' . $comparison . ' $__FOR_END_' . $rand . '__') . ';$' . $name . '+=' . $step . '){ ?>'; + $parseStr .= $content; + $parseStr .= ''; + + return $parseStr; + } + + /** + * url函数的tag标签 + * 格式:{url link="模块/控制器/方法" vars="参数" suffix="true或者false 是否带有后缀" domain="true或者false 是否携带域名" /} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagUrl($tag, $content) + { + $url = isset($tag['link']) ? $tag['link'] : ''; + $vars = isset($tag['vars']) ? $tag['vars'] : ''; + $suffix = isset($tag['suffix']) ? $tag['suffix'] : 'true'; + $domain = isset($tag['domain']) ? $tag['domain'] : 'false'; + + return ''; + } + + /** + * function标签解析 匿名函数,可实现递归 + * 使用: + * {function name="func" vars="$data" call="$list" use="&$a,&$b"} + * {if is_array($data)} + * {foreach $data as $val} + * {~func($val) /} + * {/foreach} + * {else /} + * {$data} + * {/if} + * {/function} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagFunction($tag, $content) + { + $name = !empty($tag['name']) ? $tag['name'] : 'func'; + $vars = !empty($tag['vars']) ? $tag['vars'] : ''; + $call = !empty($tag['call']) ? $tag['call'] : ''; + $use = ['&$' . $name]; + + if (!empty($tag['use'])) { + foreach (explode(',', $tag['use']) as $val) { + $use[] = '&' . ltrim(trim($val), '&'); + } + } + + $parseStr = '' . $content . '' : '?>'; + + return $parseStr; + } +} diff --git a/thinkphp/library/think/validate/ValidateRule.php b/thinkphp/library/think/validate/ValidateRule.php new file mode 100644 index 0000000..7cd7017 --- /dev/null +++ b/thinkphp/library/think/validate/ValidateRule.php @@ -0,0 +1,171 @@ + +// +---------------------------------------------------------------------- + +namespace think\validate; + +/** + * Class ValidateRule + * @package think\validate + * @method ValidateRule confirm(mixed $rule, string $msg = '') static 验证是否和某个字段的值一致 + * @method ValidateRule different(mixed $rule, string $msg = '') static 验证是否和某个字段的值是否不同 + * @method ValidateRule egt(mixed $rule, string $msg = '') static 验证是否大于等于某个值 + * @method ValidateRule gt(mixed $rule, string $msg = '') static 验证是否大于某个值 + * @method ValidateRule elt(mixed $rule, string $msg = '') static 验证是否小于等于某个值 + * @method ValidateRule lt(mixed $rule, string $msg = '') static 验证是否小于某个值 + * @method ValidateRule eg(mixed $rule, string $msg = '') static 验证是否等于某个值 + * @method ValidateRule in(mixed $rule, string $msg = '') static 验证是否在范围内 + * @method ValidateRule notIn(mixed $rule, string $msg = '') static 验证是否不在某个范围 + * @method ValidateRule between(mixed $rule, string $msg = '') static 验证是否在某个区间 + * @method ValidateRule notBetween(mixed $rule, string $msg = '') static 验证是否不在某个区间 + * @method ValidateRule length(mixed $rule, string $msg = '') static 验证数据长度 + * @method ValidateRule max(mixed $rule, string $msg = '') static 验证数据最大长度 + * @method ValidateRule min(mixed $rule, string $msg = '') static 验证数据最小长度 + * @method ValidateRule after(mixed $rule, string $msg = '') static 验证日期 + * @method ValidateRule before(mixed $rule, string $msg = '') static 验证日期 + * @method ValidateRule expire(mixed $rule, string $msg = '') static 验证有效期 + * @method ValidateRule allowIp(mixed $rule, string $msg = '') static 验证IP许可 + * @method ValidateRule denyIp(mixed $rule, string $msg = '') static 验证IP禁用 + * @method ValidateRule regex(mixed $rule, string $msg = '') static 使用正则验证数据 + * @method ValidateRule token(mixed $rule='__token__', string $msg = '') static 验证表单令牌 + * @method ValidateRule is(mixed $rule, string $msg = '') static 验证字段值是否为有效格式 + * @method ValidateRule isRequire(mixed $rule = null, string $msg = '') static 验证字段必须 + * @method ValidateRule isNumber(mixed $rule = null, string $msg = '') static 验证字段值是否为数字 + * @method ValidateRule isArray(mixed $rule = null, string $msg = '') static 验证字段值是否为数组 + * @method ValidateRule isInteger(mixed $rule = null, string $msg = '') static 验证字段值是否为整形 + * @method ValidateRule isFloat(mixed $rule = null, string $msg = '') static 验证字段值是否为浮点数 + * @method ValidateRule isMobile(mixed $rule = null, string $msg = '') static 验证字段值是否为手机 + * @method ValidateRule isIdCard(mixed $rule = null, string $msg = '') static 验证字段值是否为身份证号码 + * @method ValidateRule isChs(mixed $rule = null, string $msg = '') static 验证字段值是否为中文 + * @method ValidateRule isChsDash(mixed $rule = null, string $msg = '') static 验证字段值是否为中文字母及下划线 + * @method ValidateRule isChsAlpha(mixed $rule = null, string $msg = '') static 验证字段值是否为中文和字母 + * @method ValidateRule isChsAlphaNum(mixed $rule = null, string $msg = '') static 验证字段值是否为中文字母和数字 + * @method ValidateRule isDate(mixed $rule = null, string $msg = '') static 验证字段值是否为有效格式 + * @method ValidateRule isBool(mixed $rule = null, string $msg = '') static 验证字段值是否为布尔值 + * @method ValidateRule isAlpha(mixed $rule = null, string $msg = '') static 验证字段值是否为字母 + * @method ValidateRule isAlphaDash(mixed $rule = null, string $msg = '') static 验证字段值是否为字母和下划线 + * @method ValidateRule isAlphaNum(mixed $rule = null, string $msg = '') static 验证字段值是否为字母和数字 + * @method ValidateRule isAccepted(mixed $rule = null, string $msg = '') static 验证字段值是否为yes, on, 或是 1 + * @method ValidateRule isEmail(mixed $rule = null, string $msg = '') static 验证字段值是否为有效邮箱格式 + * @method ValidateRule isUrl(mixed $rule = null, string $msg = '') static 验证字段值是否为有效URL地址 + * @method ValidateRule activeUrl(mixed $rule, string $msg = '') static 验证是否为合格的域名或者IP + * @method ValidateRule ip(mixed $rule, string $msg = '') static 验证是否有效IP + * @method ValidateRule fileExt(mixed $rule, string $msg = '') static 验证文件后缀 + * @method ValidateRule fileMime(mixed $rule, string $msg = '') static 验证文件类型 + * @method ValidateRule fileSize(mixed $rule, string $msg = '') static 验证文件大小 + * @method ValidateRule image(mixed $rule, string $msg = '') static 验证图像文件 + * @method ValidateRule method(mixed $rule, string $msg = '') static 验证请求类型 + * @method ValidateRule dateFormat(mixed $rule, string $msg = '') static 验证时间和日期是否符合指定格式 + * @method ValidateRule unique(mixed $rule, string $msg = '') static 验证是否唯一 + * @method ValidateRule behavior(mixed $rule, string $msg = '') static 使用行为类验证 + * @method ValidateRule filter(mixed $rule, string $msg = '') static 使用filter_var方式验证 + * @method ValidateRule requireIf(mixed $rule, string $msg = '') static 验证某个字段等于某个值的时候必须 + * @method ValidateRule requireCallback(mixed $rule, string $msg = '') static 通过回调方法验证某个字段是否必须 + * @method ValidateRule requireWith(mixed $rule, string $msg = '') static 验证某个字段有值的情况下必须 + * @method ValidateRule must(mixed $rule = null, string $msg = '') static 必须验证 + */ +class ValidateRule +{ + // 验证字段的名称 + protected $title; + + // 当前验证规则 + protected $rule = []; + + // 验证提示信息 + protected $message = []; + + /** + * 添加验证因子 + * @access protected + * @param string $name 验证名称 + * @param mixed $rule 验证规则 + * @param string $msg 提示信息 + * @return $this + */ + protected function addItem($name, $rule = null, $msg = '') + { + if ($rule || 0 === $rule) { + $this->rule[$name] = $rule; + } else { + $this->rule[] = $name; + } + + $this->message[] = $msg; + + return $this; + } + + /** + * 获取验证规则 + * @access public + * @return array + */ + public function getRule() + { + return $this->rule; + } + + /** + * 获取验证字段名称 + * @access public + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * 获取验证提示 + * @access public + * @return array + */ + public function getMsg() + { + return $this->message; + } + + /** + * 设置验证字段名称 + * @access public + * @return $this + */ + public function title($title) + { + $this->title = $title; + + return $this; + } + + public function __call($method, $args) + { + if ('is' == strtolower(substr($method, 0, 2))) { + $method = substr($method, 2); + } + + array_unshift($args, lcfirst($method)); + + return call_user_func_array([$this, 'addItem'], $args); + } + + public static function __callStatic($method, $args) + { + $rule = new static(); + + if ('is' == strtolower(substr($method, 0, 2))) { + $method = substr($method, 2); + } + + array_unshift($args, lcfirst($method)); + + return call_user_func_array([$rule, 'addItem'], $args); + } +} diff --git a/thinkphp/library/think/view/driver/Php.php b/thinkphp/library/think/view/driver/Php.php new file mode 100644 index 0000000..7948dc0 --- /dev/null +++ b/thinkphp/library/think/view/driver/Php.php @@ -0,0 +1,183 @@ + +// +---------------------------------------------------------------------- + +namespace think\view\driver; + +use think\App; +use think\exception\TemplateNotFoundException; +use think\Loader; + +class Php +{ + // 模板引擎参数 + protected $config = [ + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 + 'auto_rule' => 1, + // 视图基础目录(集中式) + 'view_base' => '', + // 模板起始路径 + 'view_path' => '', + // 模板文件后缀 + 'view_suffix' => 'php', + // 模板文件名分隔符 + 'view_depr' => DIRECTORY_SEPARATOR, + ]; + + protected $template; + protected $app; + protected $content; + + public function __construct(App $app, $config = []) + { + $this->app = $app; + $this->config = array_merge($this->config, (array) $config); + } + + /** + * 检测是否存在模板文件 + * @access public + * @param string $template 模板文件或者模板规则 + * @return bool + */ + public function exists($template) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + + return is_file($template); + } + + /** + * 渲染模板文件 + * @access public + * @param string $template 模板文件 + * @param array $data 模板变量 + * @return void + */ + public function fetch($template, $data = []) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + + // 模板不存在 抛出异常 + if (!is_file($template)) { + throw new TemplateNotFoundException('template not exists:' . $template, $template); + } + + $this->template = $template; + + // 记录视图信息 + $this->app + ->log('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]'); + + extract($data, EXTR_OVERWRITE); + include $this->template; + } + + /** + * 渲染模板内容 + * @access public + * @param string $content 模板内容 + * @param array $data 模板变量 + * @return void + */ + public function display($content, $data = []) + { + $this->content = $content; + + extract($data, EXTR_OVERWRITE); + eval('?>' . $this->content); + } + + /** + * 自动定位模板文件 + * @access private + * @param string $template 模板文件规则 + * @return string + */ + private function parseTemplate($template) + { + if (empty($this->config['view_path'])) { + $this->config['view_path'] = $this->app->getModulePath() . 'view' . DIRECTORY_SEPARATOR; + } + + $request = $this->app['request']; + + // 获取视图根目录 + if (strpos($template, '@')) { + // 跨模块调用 + list($module, $template) = explode('@', $template); + } + + if ($this->config['view_base']) { + // 基础视图目录 + $module = isset($module) ? $module : $request->module(); + $path = $this->config['view_base'] . ($module ? $module . DIRECTORY_SEPARATOR : ''); + } else { + $path = isset($module) ? $this->app->getAppPath() . $module . DIRECTORY_SEPARATOR . 'view' . DIRECTORY_SEPARATOR : $this->config['view_path']; + } + + $depr = $this->config['view_depr']; + + if (0 !== strpos($template, '/')) { + $template = str_replace(['/', ':'], $depr, $template); + $controller = Loader::parseName($request->controller()); + + if ($controller) { + if ('' == $template) { + // 如果模板文件名为空 按照默认规则定位 + $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $this->getActionTemplate($request); + } elseif (false === strpos($template, $depr)) { + $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $template; + } + } + } else { + $template = str_replace(['/', ':'], $depr, substr($template, 1)); + } + + return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.'); + } + + protected function getActionTemplate($request) + { + $rule = [$request->action(true), Loader::parseName($request->action(true)), $request->action()]; + $type = $this->config['auto_rule']; + + return isset($rule[$type]) ? $rule[$type] : $rule[0]; + } + + /** + * 配置模板引擎 + * @access private + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @return void + */ + public function config($name, $value = null) + { + if (is_array($name)) { + $this->config = array_merge($this->config, $name); + } elseif (is_null($value)) { + return isset($this->config[$name]) ? $this->config[$name] : null; + } else { + $this->config[$name] = $value; + } + } + + public function __debugInfo() + { + return ['config' => $this->config]; + } +} diff --git a/thinkphp/library/think/view/driver/Think.php b/thinkphp/library/think/view/driver/Think.php new file mode 100644 index 0000000..877aee8 --- /dev/null +++ b/thinkphp/library/think/view/driver/Think.php @@ -0,0 +1,192 @@ + +// +---------------------------------------------------------------------- + +namespace think\view\driver; + +use think\App; +use think\exception\TemplateNotFoundException; +use think\Loader; +use think\Template; + +class Think +{ + // 模板引擎实例 + private $template; + private $app; + + // 模板引擎参数 + protected $config = [ + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 + 'auto_rule' => 1, + // 视图基础目录(集中式) + 'view_base' => '', + // 模板起始路径 + 'view_path' => '', + // 模板文件后缀 + 'view_suffix' => 'html', + // 模板文件名分隔符 + 'view_depr' => DIRECTORY_SEPARATOR, + // 是否开启模板编译缓存,设为false则每次都会重新编译 + 'tpl_cache' => true, + ]; + + public function __construct(App $app, $config = []) + { + $this->app = $app; + $this->config = array_merge($this->config, (array) $config); + + if (empty($this->config['view_path'])) { + $this->config['view_path'] = $app->getModulePath() . 'view' . DIRECTORY_SEPARATOR; + } + + $this->template = new Template($app, $this->config); + } + + /** + * 检测是否存在模板文件 + * @access public + * @param string $template 模板文件或者模板规则 + * @return bool + */ + public function exists($template) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + + return is_file($template); + } + + /** + * 渲染模板文件 + * @access public + * @param string $template 模板文件 + * @param array $data 模板变量 + * @param array $config 模板参数 + * @return void + */ + public function fetch($template, $data = [], $config = []) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + + // 模板不存在 抛出异常 + if (!is_file($template)) { + throw new TemplateNotFoundException('template not exists:' . $template, $template); + } + + // 记录视图信息 + $this->app + ->log('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]'); + + $this->template->fetch($template, $data, $config); + } + + /** + * 渲染模板内容 + * @access public + * @param string $template 模板内容 + * @param array $data 模板变量 + * @param array $config 模板参数 + * @return void + */ + public function display($template, $data = [], $config = []) + { + $this->template->display($template, $data, $config); + } + + /** + * 自动定位模板文件 + * @access private + * @param string $template 模板文件规则 + * @return string + */ + private function parseTemplate($template) + { + // 分析模板文件规则 + $request = $this->app['request']; + + // 获取视图根目录 + if (strpos($template, '@')) { + // 跨模块调用 + list($module, $template) = explode('@', $template); + } + + if ($this->config['view_base']) { + // 基础视图目录 + $module = isset($module) ? $module : $request->module(); + $path = $this->config['view_base'] . ($module ? $module . DIRECTORY_SEPARATOR : ''); + } else { + $path = isset($module) ? $this->app->getAppPath() . $module . DIRECTORY_SEPARATOR . 'view' . DIRECTORY_SEPARATOR : $this->config['view_path']; + } + + $depr = $this->config['view_depr']; + + if (0 !== strpos($template, '/')) { + $template = str_replace(['/', ':'], $depr, $template); + $controller = Loader::parseName($request->controller()); + + if ($controller) { + if ('' == $template) { + // 如果模板文件名为空 按照默认规则定位 + $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $this->getActionTemplate($request); + } elseif (false === strpos($template, $depr)) { + $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $template; + } + } + } else { + $template = str_replace(['/', ':'], $depr, substr($template, 1)); + } + + return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.'); + } + + protected function getActionTemplate($request) + { + $rule = [$request->action(true), Loader::parseName($request->action(true)), $request->action()]; + $type = $this->config['auto_rule']; + + return isset($rule[$type]) ? $rule[$type] : $rule[0]; + } + + /** + * 配置或者获取模板引擎参数 + * @access private + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @return mixed + */ + public function config($name, $value = null) + { + if (is_array($name)) { + $this->template->config($name); + $this->config = array_merge($this->config, $name); + } elseif (is_null($value)) { + return $this->template->config($name); + } else { + $this->template->$name = $value; + $this->config[$name] = $value; + } + } + + public function __call($method, $params) + { + return call_user_func_array([$this->template, $method], $params); + } + + public function __debugInfo() + { + return ['config' => $this->config]; + } +} diff --git a/thinkphp/library/traits/controller/Jump.php b/thinkphp/library/traits/controller/Jump.php new file mode 100644 index 0000000..41f7e93 --- /dev/null +++ b/thinkphp/library/traits/controller/Jump.php @@ -0,0 +1,168 @@ +error(); + * $this->redirect(); + * } + * } + */ +namespace traits\controller; + +use think\Container; +use think\exception\HttpResponseException; +use think\Response; +use think\response\Redirect; + +trait Jump +{ + /** + * 应用实例 + * @var \think\App + */ + protected $app; + + /** + * 操作成功跳转的快捷方法 + * @access protected + * @param mixed $msg 提示信息 + * @param string $url 跳转的URL地址 + * @param mixed $data 返回的数据 + * @param integer $wait 跳转等待时间 + * @param array $header 发送的Header信息 + * @return void + */ + protected function success($msg = '', $url = null, $data = '', $wait = 3, array $header = []) + { + if (is_null($url) && isset($_SERVER["HTTP_REFERER"])) { + $url = $_SERVER["HTTP_REFERER"]; + } elseif ('' !== $url) { + $url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : Container::get('url')->build($url); + } + + $result = [ + 'code' => 1, + 'msg' => $msg, + 'data' => $data, + 'url' => $url, + 'wait' => $wait, + ]; + + $type = $this->getResponseType(); + // 把跳转模板的渲染下沉,这样在 response_send 行为里通过getData()获得的数据是一致性的格式 + if ('html' == strtolower($type)) { + $type = 'jump'; + } + + $response = Response::create($result, $type)->header($header)->options(['jump_template' => $this->app['config']->get('dispatch_success_tmpl')]); + + throw new HttpResponseException($response); + } + + /** + * 操作错误跳转的快捷方法 + * @access protected + * @param mixed $msg 提示信息 + * @param string $url 跳转的URL地址 + * @param mixed $data 返回的数据 + * @param integer $wait 跳转等待时间 + * @param array $header 发送的Header信息 + * @return void + */ + protected function error($msg = '', $url = null, $data = '', $wait = 3, array $header = []) + { + $type = $this->getResponseType(); + if (is_null($url)) { + $url = $this->app['request']->isAjax() ? '' : 'javascript:history.back(-1);'; + } elseif ('' !== $url) { + $url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : $this->app['url']->build($url); + } + + $result = [ + 'code' => 0, + 'msg' => $msg, + 'data' => $data, + 'url' => $url, + 'wait' => $wait, + ]; + + if ('html' == strtolower($type)) { + $type = 'jump'; + } + + $response = Response::create($result, $type)->header($header)->options(['jump_template' => $this->app['config']->get('dispatch_error_tmpl')]); + + throw new HttpResponseException($response); + } + + /** + * 返回封装后的API数据到客户端 + * @access protected + * @param mixed $data 要返回的数据 + * @param integer $code 返回的code + * @param mixed $msg 提示信息 + * @param string $type 返回数据格式 + * @param array $header 发送的Header信息 + * @return void + */ + protected function result($data, $code = 0, $msg = '', $type = '', array $header = []) + { + $result = [ + 'code' => $code, + 'msg' => $msg, + 'time' => time(), + 'data' => $data, + ]; + + $type = $type ?: $this->getResponseType(); + $response = Response::create($result, $type)->header($header); + + throw new HttpResponseException($response); + } + + /** + * URL重定向 + * @access protected + * @param string $url 跳转的URL表达式 + * @param array|integer $params 其它URL参数 + * @param integer $code http code + * @param array $with 隐式传参 + * @return void + */ + protected function redirect($url, $params = [], $code = 302, $with = []) + { + $response = new Redirect($url); + + if (is_integer($params)) { + $code = $params; + $params = []; + } + + $response->code($code)->params($params)->with($with); + + throw new HttpResponseException($response); + } + + /** + * 获取当前的response 输出类型 + * @access protected + * @return string + */ + protected function getResponseType() + { + if (!$this->app) { + $this->app = Container::get('app'); + } + + $isAjax = $this->app['request']->isAjax(); + $config = $this->app['config']; + + return $isAjax + ? $config->get('default_ajax_return') + : $config->get('default_return_type'); + } +} diff --git a/thinkphp/logo.png b/thinkphp/logo.png new file mode 100644 index 0000000..25fd059 Binary files /dev/null and b/thinkphp/logo.png differ diff --git a/thinkphp/phpunit.xml.dist b/thinkphp/phpunit.xml.dist new file mode 100644 index 0000000..37c3d2b --- /dev/null +++ b/thinkphp/phpunit.xml.dist @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + ./library/think/*/tests/ + + + + + + ./library/ + + ./library/think/*/tests + ./library/think/*/assets + ./library/think/*/resources + ./library/think/*/vendor + + + + \ No newline at end of file diff --git a/thinkphp/tpl/default_index.tpl b/thinkphp/tpl/default_index.tpl new file mode 100644 index 0000000..e5c1363 --- /dev/null +++ b/thinkphp/tpl/default_index.tpl @@ -0,0 +1,10 @@ +*{ 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 }

              :)

              ThinkPHP V5.1
              12载初心不改(2006-2018) - 你值得信赖的PHP框架

              '; + } +} diff --git a/thinkphp/tpl/dispatch_jump.tpl b/thinkphp/tpl/dispatch_jump.tpl new file mode 100644 index 0000000..583376b --- /dev/null +++ b/thinkphp/tpl/dispatch_jump.tpl @@ -0,0 +1,49 @@ +{__NOLAYOUT__} + + + + + 跳转提示 + + + +
              + + +

              :)

              +

              + + +

              :(

              +

              + + +

              +

              + 页面自动 跳转 等待时间: +

              +
              + + + diff --git a/thinkphp/tpl/page_trace.tpl b/thinkphp/tpl/page_trace.tpl new file mode 100644 index 0000000..2e5afba --- /dev/null +++ b/thinkphp/tpl/page_trace.tpl @@ -0,0 +1,71 @@ +
              + + +
              +
              +
              getUseTime().'s ';?>
              + +
              + + diff --git a/thinkphp/tpl/think_exception.tpl b/thinkphp/tpl/think_exception.tpl new file mode 100644 index 0000000..1c316b9 --- /dev/null +++ b/thinkphp/tpl/think_exception.tpl @@ -0,0 +1,507 @@ +'.end($names).''; + } + } + + if(!function_exists('parse_file')){ + function parse_file($file, $line) + { + return ''.basename($file)." line {$line}".''; + } + } + + if(!function_exists('parse_args')){ + function parse_args($args) + { + $result = []; + + foreach ($args as $key => $item) { + switch (true) { + case is_object($item): + $value = sprintf('object(%s)', parse_class(get_class($item))); + break; + case is_array($item): + if(count($item) > 3){ + $value = sprintf('[%s, ...]', parse_args(array_slice($item, 0, 3))); + } else { + $value = sprintf('[%s]', parse_args($item)); + } + break; + case is_string($item): + if(strlen($item) > 20){ + $value = sprintf( + '\'%s...\'', + htmlentities($item), + htmlentities(substr($item, 0, 20)) + ); + } else { + $value = sprintf("'%s'", htmlentities($item)); + } + break; + case is_int($item): + case is_float($item): + $value = $item; + break; + case is_null($item): + $value = 'null'; + break; + case is_bool($item): + $value = '' . ($item ? 'true' : 'false') . ''; + break; + case is_resource($item): + $value = 'resource'; + break; + default: + $value = htmlentities(str_replace("\n", '', var_export(strval($item), true))); + break; + } + + $result[] = is_int($key) ? $value : sprintf('\'%s\' => %s', htmlentities($key), $value); + } + + return implode(', ', $result); + } + } +?> + + + + + 系统发生错误 + + + + +
              + +
              + +
              +
              + +
              +
              +

              [

              +
              +

              +
              + +
              + +
              +
                $value) { ?>
              +
              + +
              +

              Call Stack

              +
                +
              1. + +
              2. + +
              3. + +
              +
              +
              + +
              + +

              + +
              + + + +
              +

              Exception Datas

              + $value) { ?> + + + + + + + $val) { ?> + + + + + + + +
              empty
              + +
              + +
              + + + +
              +

              Environment Variables

              + $value) { ?> + + + + + + + $val) { ?> + + + + + + + +
              empty
              + +
              + +
              + + + + + + + + diff --git a/vendor/.gitignore b/vendor/.gitignore new file mode 100644 index 0000000..b722e9e --- /dev/null +++ b/vendor/.gitignore @@ -0,0 +1 @@ +!.gitignore \ No newline at end of file diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 0000000..0b6bfc4 --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,25 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ + private $vendorDir; + + // PSR-4 + /** + * @var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var list + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array>> + */ + private $prefixesPsr0 = array(); + /** + * @var list + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var array + */ + private $missingClasses = array(); + + /** @var string|null */ + private $apcuPrefix; + + /** + * @var array + */ + private static $registeredLoaders = array(); + + /** + * @param string|null $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return array> + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return list + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return list + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return array Array of classname => path + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + $paths + ); + } + } elseif (!isset($this->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."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + $includeFile = self::$includeFile; + $includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders keyed by their corresponding vendor directories. + * + * @return array + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } + + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } +} diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php new file mode 100644 index 0000000..51e734a --- /dev/null +++ b/vendor/composer/InstalledVersions.php @@ -0,0 +1,359 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + $installed[] = self::$installedByVendor[$vendorDir] = $required; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array()) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..f79184e --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,16 @@ + $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', + 'JsonException' => $vendorDir . '/symfony/polyfill-php73/Resources/stubs/JsonException.php', + 'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', + 'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', + 'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', + 'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', +); diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php new file mode 100644 index 0000000..1a898a8 --- /dev/null +++ b/vendor/composer/autoload_files.php @@ -0,0 +1,21 @@ + $vendorDir . '/symfony/deprecation-contracts/function.php', + 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php', + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', + '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', + 'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php', + '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', + '0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php', + '2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php', + 'f0e7e63bbb278a92db02393536748c5f' => $vendorDir . '/overtrue/wechat/src/Kernel/Support/Helpers.php', + '6747f579ad6817f318cc3a7e7a0abb93' => $vendorDir . '/overtrue/wechat/src/Kernel/Helpers.php', + '1cfd2761b63b0a29ed23657ea394cb2d' => $vendorDir . '/topthink/think-captcha/src/helper.php', + '9b552a3cc426e3287cc811caefa3cf53' => $vendorDir . '/topthink/think-helper/src/helper.php', +); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..be11dd8 --- /dev/null +++ b/vendor/composer/autoload_namespaces.php @@ -0,0 +1,11 @@ + array($vendorDir . '/pimple/pimple/src'), + 'HTMLPurifier' => array($vendorDir . '/ezyang/htmlpurifier/library'), +); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php new file mode 100644 index 0000000..c3900bc --- /dev/null +++ b/vendor/composer/autoload_psr4.php @@ -0,0 +1,39 @@ + array($vendorDir . '/topthink/think-helper/src'), + 'think\\composer\\' => array($vendorDir . '/topthink/think-installer/src'), + 'think\\captcha\\' => array($vendorDir . '/topthink/think-captcha/src'), + 'think\\' => array($vendorDir . '/topthink/think-image/src'), + 'app\\' => array($baseDir . '/application'), + 'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'), + 'Symfony\\Polyfill\\Php73\\' => array($vendorDir . '/symfony/polyfill-php73'), + 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), + 'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'), + 'Symfony\\Contracts\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher-contracts'), + 'Symfony\\Contracts\\Cache\\' => array($vendorDir . '/symfony/cache-contracts'), + 'Symfony\\Component\\VarExporter\\' => array($vendorDir . '/symfony/var-exporter'), + 'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'), + 'Symfony\\Component\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher'), + 'Symfony\\Component\\Cache\\' => array($vendorDir . '/symfony/cache'), + 'Symfony\\Bridge\\PsrHttpMessage\\' => array($vendorDir . '/symfony/psr-http-message-bridge'), + 'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'), + 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), + 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src', $vendorDir . '/psr/http-factory/src'), + 'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'), + 'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'), + 'Psr\\Container\\' => array($vendorDir . '/psr/container/src'), + 'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'), + 'Overtrue\\Socialite\\' => array($vendorDir . '/overtrue/socialite/src'), + 'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'), + 'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'), + 'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'), + 'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'), + 'EasyWeChat\\' => array($vendorDir . '/overtrue/wechat/src'), + 'EasyWeChatComposer\\' => array($vendorDir . '/easywechat-composer/easywechat-composer/src'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 0000000..0c3df12 --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,50 @@ +register(true); + + $filesToLoad = \Composer\Autoload\ComposerStaticInitea24a8d88377c9cd0962665edfe67ce8::$files; + $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + + require $file; + } + }, null, null); + foreach ($filesToLoad as $fileIdentifier => $file) { + $requireFile($fileIdentifier, $file); + } + + return $loader; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 0000000..42696f7 --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,242 @@ + __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', + 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', + '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', + 'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php', + '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php', + '0d59ee240a4cd96ddbb4ff164fccea4d' => __DIR__ . '/..' . '/symfony/polyfill-php73/bootstrap.php', + '2cffec82183ee1cea088009cef9a6fc3' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php', + 'f0e7e63bbb278a92db02393536748c5f' => __DIR__ . '/..' . '/overtrue/wechat/src/Kernel/Support/Helpers.php', + '6747f579ad6817f318cc3a7e7a0abb93' => __DIR__ . '/..' . '/overtrue/wechat/src/Kernel/Helpers.php', + '1cfd2761b63b0a29ed23657ea394cb2d' => __DIR__ . '/..' . '/topthink/think-captcha/src/helper.php', + '9b552a3cc426e3287cc811caefa3cf53' => __DIR__ . '/..' . '/topthink/think-helper/src/helper.php', + ); + + public static $prefixLengthsPsr4 = array ( + 't' => + array ( + 'think\\helper\\' => 13, + 'think\\composer\\' => 15, + 'think\\captcha\\' => 14, + 'think\\' => 6, + ), + 'a' => + array ( + 'app\\' => 4, + ), + 'S' => + array ( + 'Symfony\\Polyfill\\Php80\\' => 23, + 'Symfony\\Polyfill\\Php73\\' => 23, + 'Symfony\\Polyfill\\Mbstring\\' => 26, + 'Symfony\\Contracts\\Service\\' => 26, + 'Symfony\\Contracts\\EventDispatcher\\' => 34, + 'Symfony\\Contracts\\Cache\\' => 24, + 'Symfony\\Component\\VarExporter\\' => 30, + 'Symfony\\Component\\HttpFoundation\\' => 33, + 'Symfony\\Component\\EventDispatcher\\' => 34, + 'Symfony\\Component\\Cache\\' => 24, + 'Symfony\\Bridge\\PsrHttpMessage\\' => 30, + ), + 'P' => + array ( + 'Psr\\SimpleCache\\' => 16, + 'Psr\\Log\\' => 8, + 'Psr\\Http\\Message\\' => 17, + 'Psr\\Http\\Client\\' => 16, + 'Psr\\EventDispatcher\\' => 20, + 'Psr\\Container\\' => 14, + 'Psr\\Cache\\' => 10, + ), + 'O' => + array ( + 'Overtrue\\Socialite\\' => 19, + ), + 'M' => + array ( + 'Monolog\\' => 8, + ), + 'G' => + array ( + 'GuzzleHttp\\Psr7\\' => 16, + 'GuzzleHttp\\Promise\\' => 19, + 'GuzzleHttp\\' => 11, + ), + 'E' => + array ( + 'EasyWeChat\\' => 11, + 'EasyWeChatComposer\\' => 19, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'think\\helper\\' => + array ( + 0 => __DIR__ . '/..' . '/topthink/think-helper/src', + ), + 'think\\composer\\' => + array ( + 0 => __DIR__ . '/..' . '/topthink/think-installer/src', + ), + 'think\\captcha\\' => + array ( + 0 => __DIR__ . '/..' . '/topthink/think-captcha/src', + ), + 'think\\' => + array ( + 0 => __DIR__ . '/..' . '/topthink/think-image/src', + ), + 'app\\' => + array ( + 0 => __DIR__ . '/../..' . '/application', + ), + 'Symfony\\Polyfill\\Php80\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-php80', + ), + 'Symfony\\Polyfill\\Php73\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-php73', + ), + 'Symfony\\Polyfill\\Mbstring\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', + ), + 'Symfony\\Contracts\\Service\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/service-contracts', + ), + 'Symfony\\Contracts\\EventDispatcher\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/event-dispatcher-contracts', + ), + 'Symfony\\Contracts\\Cache\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/cache-contracts', + ), + 'Symfony\\Component\\VarExporter\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/var-exporter', + ), + 'Symfony\\Component\\HttpFoundation\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/http-foundation', + ), + 'Symfony\\Component\\EventDispatcher\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/event-dispatcher', + ), + 'Symfony\\Component\\Cache\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/cache', + ), + 'Symfony\\Bridge\\PsrHttpMessage\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/psr-http-message-bridge', + ), + 'Psr\\SimpleCache\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/simple-cache/src', + ), + 'Psr\\Log\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/log/Psr/Log', + ), + 'Psr\\Http\\Message\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/http-message/src', + 1 => __DIR__ . '/..' . '/psr/http-factory/src', + ), + 'Psr\\Http\\Client\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/http-client/src', + ), + 'Psr\\EventDispatcher\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/event-dispatcher/src', + ), + 'Psr\\Container\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/container/src', + ), + 'Psr\\Cache\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/cache/src', + ), + 'Overtrue\\Socialite\\' => + array ( + 0 => __DIR__ . '/..' . '/overtrue/socialite/src', + ), + 'Monolog\\' => + array ( + 0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog', + ), + 'GuzzleHttp\\Psr7\\' => + array ( + 0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src', + ), + 'GuzzleHttp\\Promise\\' => + array ( + 0 => __DIR__ . '/..' . '/guzzlehttp/promises/src', + ), + 'GuzzleHttp\\' => + array ( + 0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src', + ), + 'EasyWeChat\\' => + array ( + 0 => __DIR__ . '/..' . '/overtrue/wechat/src', + ), + 'EasyWeChatComposer\\' => + array ( + 0 => __DIR__ . '/..' . '/easywechat-composer/easywechat-composer/src', + ), + ); + + public static $prefixesPsr0 = array ( + 'P' => + array ( + 'Pimple' => + array ( + 0 => __DIR__ . '/..' . '/pimple/pimple/src', + ), + ), + 'H' => + array ( + 'HTMLPurifier' => + array ( + 0 => __DIR__ . '/..' . '/ezyang/htmlpurifier/library', + ), + ), + ); + + public static $classMap = array ( + 'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'JsonException' => __DIR__ . '/..' . '/symfony/polyfill-php73/Resources/stubs/JsonException.php', + 'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', + 'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', + 'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', + 'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitea24a8d88377c9cd0962665edfe67ce8::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitea24a8d88377c9cd0962665edfe67ce8::$prefixDirsPsr4; + $loader->prefixesPsr0 = ComposerStaticInitea24a8d88377c9cd0962665edfe67ce8::$prefixesPsr0; + $loader->classMap = ComposerStaticInitea24a8d88377c9cd0962665edfe67ce8::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 0000000..ff41100 --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,2569 @@ +{ + "packages": [ + { + "name": "easywechat-composer/easywechat-composer", + "version": "1.4.1", + "version_normalized": "1.4.1.0", + "source": { + "type": "git", + "url": "https://github.com/mingyoung/easywechat-composer.git", + "reference": "3fc6a7ab6d3853c0f4e2922539b56cc37ef361cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mingyoung/easywechat-composer/zipball/3fc6a7ab6d3853c0f4e2922539b56cc37ef361cd", + "reference": "3fc6a7ab6d3853c0f4e2922539b56cc37ef361cd", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=7.0" + }, + "require-dev": { + "composer/composer": "^1.0 || ^2.0", + "phpunit/phpunit": "^6.5 || ^7.0" + }, + "time": "2021-07-05T04:03:22+00:00", + "type": "composer-plugin", + "extra": { + "class": "EasyWeChatComposer\\Plugin" + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "EasyWeChatComposer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "张铭阳", + "email": "mingyoungcheung@gmail.com" + } + ], + "description": "The composer plugin for EasyWeChat", + "support": { + "issues": "https://github.com/mingyoung/easywechat-composer/issues", + "source": "https://github.com/mingyoung/easywechat-composer/tree/1.4.1" + }, + "install-path": "../easywechat-composer/easywechat-composer" + }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.10.0", + "version_normalized": "4.10.0.0", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/d85d39da4576a6934b72480be6978fb10c860021", + "reference": "d85d39da4576a6934b72480be6978fb10c860021", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "require-dev": { + "simpletest/simpletest": "^1.1" + }, + "time": "2018-02-23T01:58:20+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "HTMLPurifier": "library/" + }, + "files": [ + "library/HTMLPurifier.composer.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "install-path": "../ezyang/htmlpurifier" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.2", + "version_normalized": "7.9.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "time": "2024-07-24T11:22:20+00:00", + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "install-path": "../guzzlehttp/guzzle" + }, + { + "name": "guzzlehttp/promises", + "version": "1.5.3", + "version_normalized": "1.5.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e", + "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "time": "2023-05-21T12:31:43+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.5.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "install-path": "../guzzlehttp/promises" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.0", + "version_normalized": "2.7.0.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "time": "2024-07-18T11:15:46+00:00", + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "install-path": "../guzzlehttp/psr7" + }, + { + "name": "monolog/monolog", + "version": "2.9.3", + "version_normalized": "2.9.3.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "a30bfe2e142720dfa990d0a7e573997f5d884215" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/a30bfe2e142720dfa990d0a7e573997f5d884215", + "reference": "a30bfe2e142720dfa990d0a7e573997f5d884215", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2@dev", + "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpspec/prophecy": "^1.15", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.38 || ^9.6.19", + "predis/predis": "^1.1 || ^2.0", + "rollbar/rollbar": "^1.3 || ^2 || ^3", + "ruflin/elastica": "^7", + "swiftmailer/swiftmailer": "^5.3|^6.0", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "time": "2024-04-12T20:52:51+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/2.9.3" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "install-path": "../monolog/monolog" + }, + { + "name": "overtrue/socialite", + "version": "2.0.24", + "version_normalized": "2.0.24.0", + "source": { + "type": "git", + "url": "https://github.com/overtrue/socialite.git", + "reference": "ee7e7b000ec7d64f2b8aba1f6a2eec5cdf3f8bec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/overtrue/socialite/zipball/ee7e7b000ec7d64f2b8aba1f6a2eec5cdf3f8bec", + "reference": "ee7e7b000ec7d64f2b8aba1f6a2eec5cdf3f8bec", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^5.0|^6.0|^7.0", + "php": ">=5.6", + "symfony/http-foundation": "^2.7|^3.0|^4.0|^5.0" + }, + "require-dev": { + "mockery/mockery": "~1.2", + "phpunit/phpunit": "^6.0|^7.0|^8.0|^9.0" + }, + "time": "2021-05-13T16:04:48+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Overtrue\\Socialite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "overtrue", + "email": "anzhengchao@gmail.com" + } + ], + "description": "A collection of OAuth 2 packages that extracts from laravel/socialite.", + "keywords": [ + "login", + "oauth", + "qq", + "social", + "wechat", + "weibo" + ], + "support": { + "issues": "https://github.com/overtrue/socialite/issues", + "source": "https://github.com/overtrue/socialite/tree/2.0.24" + }, + "funding": [ + { + "url": "https://www.patreon.com/overtrue", + "type": "patreon" + } + ], + "install-path": "../overtrue/socialite" + }, + { + "name": "overtrue/wechat", + "version": "4.6.0", + "version_normalized": "4.6.0.0", + "source": { + "type": "git", + "url": "https://github.com/w7corp/easywechat.git", + "reference": "52af4cbe777cd4aea307beafa0a4518c347467b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/w7corp/easywechat/zipball/52af4cbe777cd4aea307beafa0a4518c347467b1", + "reference": "52af4cbe777cd4aea307beafa0a4518c347467b1", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "easywechat-composer/easywechat-composer": "^1.1", + "ext-fileinfo": "*", + "ext-openssl": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^6.2 || ^7.0", + "monolog/monolog": "^1.22 || ^2.0", + "overtrue/socialite": "~2.0", + "php": ">=7.2", + "pimple/pimple": "^3.0", + "psr/simple-cache": "^1.0", + "symfony/cache": "^3.3 || ^4.3 || ^5.0", + "symfony/event-dispatcher": "^4.3 || ^5.0", + "symfony/http-foundation": "^2.7 || ^3.0 || ^4.0 || ^5.0", + "symfony/psr-http-message-bridge": "^0.3 || ^1.0 || ^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.15", + "mikey179/vfsstream": "^1.6", + "mockery/mockery": "^1.2.3", + "phpstan/phpstan": "^0.12.0", + "phpunit/phpunit": "^7.5" + }, + "time": "2022-08-24T07:30:42+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "src/Kernel/Support/Helpers.php", + "src/Kernel/Helpers.php" + ], + "psr-4": { + "EasyWeChat\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "overtrue", + "email": "anzhengchao@gmail.com" + } + ], + "description": "微信SDK", + "keywords": [ + "easywechat", + "sdk", + "wechat", + "weixin", + "weixin-sdk" + ], + "support": { + "issues": "https://github.com/w7corp/easywechat/issues", + "source": "https://github.com/w7corp/easywechat/tree/4.6.0" + }, + "funding": [ + { + "url": "https://github.com/overtrue", + "type": "github" + } + ], + "abandoned": "w7corp/easywechat", + "install-path": "../overtrue/wechat" + }, + { + "name": "pimple/pimple", + "version": "v3.5.0", + "version_normalized": "3.5.0.0", + "source": { + "type": "git", + "url": "https://github.com/silexphp/Pimple.git", + "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a94b3a4db7fb774b3d78dad2315ddc07629e1bed", + "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1 || ^2.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^5.4@dev" + }, + "time": "2021-10-28T11:13:42+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-0": { + "Pimple": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Pimple, a simple Dependency Injection Container", + "homepage": "https://pimple.symfony.com", + "keywords": [ + "container", + "dependency injection" + ], + "support": { + "source": "https://github.com/silexphp/Pimple/tree/v3.5.0" + }, + "install-path": "../pimple/pimple" + }, + { + "name": "psr/cache", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2016-08-06T20:24:11+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/master" + }, + "install-path": "../psr/cache" + }, + { + "name": "psr/container", + "version": "2.0.1", + "version_normalized": "2.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "2ae37329ee82f91efadc282cc2d527fd6065a5ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/2ae37329ee82f91efadc282cc2d527fd6065a5ef", + "reference": "2ae37329ee82f91efadc282cc2d527fd6065a5ef", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.0" + }, + "time": "2021-03-24T13:40:57+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.1" + }, + "install-path": "../psr/container" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.0" + }, + "time": "2019-01-08T18:20:26+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "install-path": "../psr/event-dispatcher" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "version_normalized": "1.0.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "time": "2023-09-23T14:17:50+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "install-path": "../psr/http-client" + }, + { + "name": "psr/http-factory", + "version": "1.0.2", + "version_normalized": "1.0.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "e616d01114759c4c489f93b099585439f795fe35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "time": "2023-04-10T20:10:41+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + }, + "install-path": "../psr/http-factory" + }, + { + "name": "psr/http-message", + "version": "2.0", + "version_normalized": "2.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "time": "2023-04-04T09:54:51+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://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" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "install-path": "../psr/http-message" + }, + { + "name": "psr/log", + "version": "1.1.4", + "version_normalized": "1.1.4.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2021-05-03T11:20:27+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "install-path": "../psr/log" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2017-10-23T01:57:42+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/master" + }, + "install-path": "../psr/simple-cache" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "version_normalized": "3.0.3.0", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "time": "2019-03-08T08:55:37+00:00", + "type": "library", + "installation-source": "dist", + "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.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "install-path": "../ralouphie/getallheaders" + }, + { + "name": "symfony/cache", + "version": "v5.4.42", + "version_normalized": "5.4.42.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "6f5f750692bd5a212e01a4f1945fd856bceef89e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/6f5f750692bd5a212e01a4f1945fd856bceef89e", + "reference": "6f5f750692bd5a212e01a4f1945fd856bceef89e", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.5", + "psr/cache": "^1.0|^2.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^1.1.7|^2", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/var-exporter": "^4.4|^5.0|^6.0" + }, + "conflict": { + "doctrine/dbal": "<2.13.1", + "symfony/dependency-injection": "<4.4", + "symfony/http-kernel": "<4.4", + "symfony/var-dumper": "<4.4" + }, + "provide": { + "psr/cache-implementation": "1.0|2.0", + "psr/simple-cache-implementation": "1.0|2.0", + "symfony/cache-implementation": "1.0|2.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/cache": "^1.6|^2.0", + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/filesystem": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "time": "2024-07-10T06:02:18+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v5.4.42" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/cache" + }, + { + "name": "symfony/cache-contracts", + "version": "v2.5.2", + "version_normalized": "2.5.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "64be4a7acb83b6f2bf6de9a02cee6dad41277ebc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/64be4a7acb83b6f2bf6de9a02cee6dad41277ebc", + "reference": "64be4a7acb83b6f2bf6de9a02cee6dad41277ebc", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.5", + "psr/cache": "^1.0|^2.0|^3.0" + }, + "suggest": { + "symfony/cache-implementation": "" + }, + "time": "2022-01-02T09:53:40+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v2.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/cache-contracts" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.2", + "version_normalized": "2.5.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.1" + }, + "time": "2022-01-02T09:53:40+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/deprecation-contracts" + }, + { + "name": "symfony/event-dispatcher", + "version": "v5.4.40", + "version_normalized": "5.4.40.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "a54e2a8a114065f31020d6a89ede83e34c3b27a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a54e2a8a114065f31020d6a89ede83e34c3b27a4", + "reference": "a54e2a8a114065f31020d6a89ede83e34c3b27a4", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher-contracts": "^2|^3", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "symfony/dependency-injection": "<4.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/stopwatch": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "time": "2024-05-31T14:33:22+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.40" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/event-dispatcher" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v2.5.2", + "version_normalized": "2.5.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/f98b54df6ad059855739db6fcbc2d36995283fe1", + "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.5", + "psr/event-dispatcher": "^1" + }, + "suggest": { + "symfony/event-dispatcher-implementation": "" + }, + "time": "2022-01-02T09:53:40+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/event-dispatcher-contracts" + }, + { + "name": "symfony/http-foundation", + "version": "v5.4.44", + "version_normalized": "5.4.44.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "ae0d217e5932aa0b70ddb4cf7822cc76d48aee53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ae0d217e5932aa0b70ddb4cf7822cc76d48aee53", + "reference": "ae0d217e5932aa0b70ddb4cf7822cc76d48aee53", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "predis/predis": "^1.0|^2.0", + "symfony/cache": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", + "symfony/mime": "^4.4|^5.0|^6.0", + "symfony/rate-limiter": "^5.2|^6.0" + }, + "suggest": { + "symfony/mime": "To use the file extension guesser" + }, + "time": "2024-09-15T07:55:06+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v5.4.44" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/http-foundation" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.29.0", + "version_normalized": "1.29.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "time": "2024-01-29T20:11:03+00:00", + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-mbstring" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.31.0", + "version_normalized": "1.31.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2" + }, + "time": "2024-09-09T11:45:10+00:00", + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-php73" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.31.0", + "version_normalized": "1.31.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2" + }, + "time": "2024-09-09T11:45:10+00:00", + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-php80" + }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v2.3.1", + "version_normalized": "2.3.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/581ca6067eb62640de5ff08ee1ba6850a0ee472e", + "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.5", + "psr/http-message": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/http-foundation": "^5.4 || ^6.0" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "psr/log": "^1.1 || ^2 || ^3", + "symfony/browser-kit": "^5.4 || ^6.0", + "symfony/config": "^5.4 || ^6.0", + "symfony/event-dispatcher": "^5.4 || ^6.0", + "symfony/framework-bundle": "^5.4 || ^6.0", + "symfony/http-kernel": "^5.4 || ^6.0", + "symfony/phpunit-bridge": "^6.2" + }, + "suggest": { + "nyholm/psr7": "For a super lightweight PSR-7/17 implementation" + }, + "time": "2023-07-26T11:53:26+00:00", + "type": "symfony-bridge", + "extra": { + "branch-alias": { + "dev-main": "2.3-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "http://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/symfony/psr-http-message-bridge/issues", + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/psr-http-message-bridge" + }, + { + "name": "symfony/service-contracts", + "version": "v1.1.2", + "version_normalized": "1.1.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "191afdcb5804db960d26d8566b7e9a2843cab3a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/191afdcb5804db960d26d8566b7e9a2843cab3a0", + "reference": "191afdcb5804db960d26d8566b7e9a2843cab3a0", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.1.3" + }, + "suggest": { + "psr/container": "", + "symfony/service-implementation": "" + }, + "time": "2019-05-28T07:50:59+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v1.1.2" + }, + "install-path": "../symfony/service-contracts" + }, + { + "name": "symfony/var-exporter", + "version": "v5.4.40", + "version_normalized": "5.4.40.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "6a13d37336d512927986e09f19a4bed24178baa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/6a13d37336d512927986e09f19a4bed24178baa6", + "reference": "6a13d37336d512927986e09f19a4bed24178baa6", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "symfony/var-dumper": "^4.4.9|^5.0.9|^6.0" + }, + "time": "2024-05-31T14:33:22+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v5.4.40" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/var-exporter" + }, + { + "name": "topthink/framework", + "version": "v5.1.42", + "version_normalized": "5.1.42.0", + "dist": { + "type": "zip", + "url": "https://mirrors.tencent.com/repository/composer/topthink/framework/v5.1.42/topthink-framework-v5.1.42.zip", + "reference": "ecf1a90d397d821ce2df58f7d47e798c17eba3ad", + "shasum": "" + }, + "require": { + "php": ">=5.6.0", + "topthink/think-installer": "2.*" + }, + "require-dev": { + "johnkary/phpunit-speedtrap": "^1.0", + "mikey179/vfsstream": "~1.6", + "phpdocumentor/reflection-docblock": "^2.0", + "phploc/phploc": "2.*", + "phpunit/phpunit": "^5.0|^6.0", + "sebastian/phpcpd": "2.*", + "squizlabs/php_codesniffer": "2.*" + }, + "time": "2022-10-25T15:04:49+00:00", + "type": "think-framework", + "installation-source": "dist", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + }, + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "the new thinkphp framework", + "homepage": "http://thinkphp.cn/", + "keywords": [ + "framework", + "orm", + "thinkphp" + ], + "install-path": "../topthink/framework" + }, + { + "name": "topthink/think-captcha", + "version": "v2.0.2", + "version_normalized": "2.0.2.0", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-captcha/zipball/54c8a51552f99ff9ea89ea9c272383a8f738ceee", + "reference": "54c8a51552f99ff9ea89ea9c272383a8f738ceee", + "shasum": "" + }, + "require": { + "topthink/framework": "5.1.*" + }, + "time": "2017-12-31T16:37:49+00:00", + "type": "library", + "installation-source": "dist", + "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", + "install-path": "../topthink/think-captcha" + }, + { + "name": "topthink/think-helper", + "version": "v1.0.7", + "version_normalized": "1.0.7.0", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-helper/zipball/5f92178606c8ce131d36b37a57c58eb71e55f019", + "reference": "5f92178606c8ce131d36b37a57c58eb71e55f019", + "shasum": "" + }, + "time": "2018-10-05T00:43:21+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "think\\helper\\": "src" + }, + "files": [ + "src/helper.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "The ThinkPHP5 Helper Package", + "install-path": "../topthink/think-helper" + }, + { + "name": "topthink/think-image", + "version": "v1.0.7", + "version_normalized": "1.0.7.0", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-image/zipball/8586cf47f117481c6d415b20f7dedf62e79d5512", + "reference": "8586cf47f117481c6d415b20f7dedf62e79d5512", + "shasum": "" + }, + "require": { + "ext-gd": "*" + }, + "require-dev": { + "phpunit/phpunit": "4.8.*", + "topthink/framework": "^5.0" + }, + "time": "2016-09-29T06:05:43+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "The ThinkPHP5 Image Package", + "install-path": "../topthink/think-image" + }, + { + "name": "topthink/think-installer", + "version": "v2.0.5", + "version_normalized": "2.0.5.0", + "dist": { + "type": "zip", + "url": "https://mirrors.tencent.com/repository/composer/topthink/think-installer/v2.0.5/topthink-think-installer-v2.0.5.zip", + "reference": "38ba647706e35d6704b5d370c06f8a160b635f88", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0||^2.0" + }, + "require-dev": { + "composer/composer": "^1.0||^2.0" + }, + "time": "2021-01-14T12:12:14+00:00", + "type": "composer-plugin", + "extra": { + "class": "think\\composer\\Plugin" + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "think\\composer\\": "src" + } + }, + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "install-path": "../topthink/think-installer" + } + ], + "dev": true, + "dev-package-names": [] +} diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php new file mode 100644 index 0000000..b906219 --- /dev/null +++ b/vendor/composer/installed.php @@ -0,0 +1,392 @@ + array( + 'name' => 'topthink/think', + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => null, + 'type' => 'project', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev' => true, + ), + 'versions' => array( + 'easywechat-composer/easywechat-composer' => array( + 'pretty_version' => '1.4.1', + 'version' => '1.4.1.0', + 'reference' => '3fc6a7ab6d3853c0f4e2922539b56cc37ef361cd', + 'type' => 'composer-plugin', + 'install_path' => __DIR__ . '/../easywechat-composer/easywechat-composer', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'ezyang/htmlpurifier' => array( + 'pretty_version' => 'v4.10.0', + 'version' => '4.10.0.0', + 'reference' => 'd85d39da4576a6934b72480be6978fb10c860021', + 'type' => 'library', + 'install_path' => __DIR__ . '/../ezyang/htmlpurifier', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'guzzlehttp/guzzle' => array( + 'pretty_version' => '7.9.2', + 'version' => '7.9.2.0', + 'reference' => 'd281ed313b989f213357e3be1a179f02196ac99b', + 'type' => 'library', + 'install_path' => __DIR__ . '/../guzzlehttp/guzzle', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'guzzlehttp/promises' => array( + 'pretty_version' => '1.5.3', + 'version' => '1.5.3.0', + 'reference' => '67ab6e18aaa14d753cc148911d273f6e6cb6721e', + 'type' => 'library', + 'install_path' => __DIR__ . '/../guzzlehttp/promises', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'guzzlehttp/psr7' => array( + 'pretty_version' => '2.7.0', + 'version' => '2.7.0.0', + 'reference' => 'a70f5c95fb43bc83f07c9c948baa0dc1829bf201', + 'type' => 'library', + 'install_path' => __DIR__ . '/../guzzlehttp/psr7', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'monolog/monolog' => array( + 'pretty_version' => '2.9.3', + 'version' => '2.9.3.0', + 'reference' => 'a30bfe2e142720dfa990d0a7e573997f5d884215', + 'type' => 'library', + 'install_path' => __DIR__ . '/../monolog/monolog', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'overtrue/socialite' => array( + 'pretty_version' => '2.0.24', + 'version' => '2.0.24.0', + 'reference' => 'ee7e7b000ec7d64f2b8aba1f6a2eec5cdf3f8bec', + 'type' => 'library', + 'install_path' => __DIR__ . '/../overtrue/socialite', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'overtrue/wechat' => array( + 'pretty_version' => '4.6.0', + 'version' => '4.6.0.0', + 'reference' => '52af4cbe777cd4aea307beafa0a4518c347467b1', + 'type' => 'library', + 'install_path' => __DIR__ . '/../overtrue/wechat', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'pimple/pimple' => array( + 'pretty_version' => 'v3.5.0', + 'version' => '3.5.0.0', + 'reference' => 'a94b3a4db7fb774b3d78dad2315ddc07629e1bed', + 'type' => 'library', + 'install_path' => __DIR__ . '/../pimple/pimple', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/cache' => array( + 'pretty_version' => '1.0.1', + 'version' => '1.0.1.0', + 'reference' => 'd11b50ad223250cf17b86e38383413f5a6764bf8', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/cache', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/cache-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0|2.0', + ), + ), + 'psr/container' => array( + 'pretty_version' => '2.0.1', + 'version' => '2.0.1.0', + 'reference' => '2ae37329ee82f91efadc282cc2d527fd6065a5ef', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/container', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/event-dispatcher' => array( + 'pretty_version' => '1.0.0', + 'version' => '1.0.0.0', + 'reference' => 'dbefd12671e8a14ec7f180cab83036ed26714bb0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/event-dispatcher', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/event-dispatcher-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0', + ), + ), + 'psr/http-client' => array( + 'pretty_version' => '1.0.3', + 'version' => '1.0.3.0', + 'reference' => 'bb5906edc1c324c9a05aa0873d40117941e5fa90', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/http-client', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/http-client-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0', + ), + ), + 'psr/http-factory' => array( + 'pretty_version' => '1.0.2', + 'version' => '1.0.2.0', + 'reference' => 'e616d01114759c4c489f93b099585439f795fe35', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/http-factory', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/http-factory-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0', + ), + ), + 'psr/http-message' => array( + 'pretty_version' => '2.0', + 'version' => '2.0.0.0', + 'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/http-message', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/http-message-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0', + ), + ), + 'psr/log' => array( + 'pretty_version' => '1.1.4', + 'version' => '1.1.4.0', + 'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/log', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/log-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0.0 || 2.0.0 || 3.0.0', + ), + ), + 'psr/simple-cache' => array( + 'pretty_version' => '1.0.1', + 'version' => '1.0.1.0', + 'reference' => '408d5eafb83c57f6365a3ca330ff23aa4a5fa39b', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/simple-cache', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/simple-cache-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0|2.0', + ), + ), + 'ralouphie/getallheaders' => array( + 'pretty_version' => '3.0.3', + 'version' => '3.0.3.0', + 'reference' => '120b605dfeb996808c31b6477290a714d356e822', + 'type' => 'library', + 'install_path' => __DIR__ . '/../ralouphie/getallheaders', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/cache' => array( + 'pretty_version' => 'v5.4.42', + 'version' => '5.4.42.0', + 'reference' => '6f5f750692bd5a212e01a4f1945fd856bceef89e', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/cache', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/cache-contracts' => array( + 'pretty_version' => 'v2.5.2', + 'version' => '2.5.2.0', + 'reference' => '64be4a7acb83b6f2bf6de9a02cee6dad41277ebc', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/cache-contracts', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/cache-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0|2.0', + ), + ), + 'symfony/deprecation-contracts' => array( + 'pretty_version' => 'v2.5.2', + 'version' => '2.5.2.0', + 'reference' => 'e8b495ea28c1d97b5e0c121748d6f9b53d075c66', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/deprecation-contracts', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/event-dispatcher' => array( + 'pretty_version' => 'v5.4.40', + 'version' => '5.4.40.0', + 'reference' => 'a54e2a8a114065f31020d6a89ede83e34c3b27a4', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/event-dispatcher', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/event-dispatcher-contracts' => array( + 'pretty_version' => 'v2.5.2', + 'version' => '2.5.2.0', + 'reference' => 'f98b54df6ad059855739db6fcbc2d36995283fe1', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/event-dispatcher-contracts', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/event-dispatcher-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '2.0', + ), + ), + 'symfony/http-foundation' => array( + 'pretty_version' => 'v5.4.44', + 'version' => '5.4.44.0', + 'reference' => 'ae0d217e5932aa0b70ddb4cf7822cc76d48aee53', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/http-foundation', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-mbstring' => array( + 'pretty_version' => 'v1.29.0', + 'version' => '1.29.0.0', + 'reference' => '9773676c8a1bb1f8d4340a62efe641cf76eda7ec', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-mbstring', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-php73' => array( + 'pretty_version' => 'v1.31.0', + 'version' => '1.31.0.0', + 'reference' => '0f68c03565dcaaf25a890667542e8bd75fe7e5bb', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-php73', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-php80' => array( + 'pretty_version' => 'v1.31.0', + 'version' => '1.31.0.0', + 'reference' => '60328e362d4c2c802a54fcbf04f9d3fb892b4cf8', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-php80', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/psr-http-message-bridge' => array( + 'pretty_version' => 'v2.3.1', + 'version' => '2.3.1.0', + 'reference' => '581ca6067eb62640de5ff08ee1ba6850a0ee472e', + 'type' => 'symfony-bridge', + 'install_path' => __DIR__ . '/../symfony/psr-http-message-bridge', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/service-contracts' => array( + 'pretty_version' => 'v1.1.2', + 'version' => '1.1.2.0', + 'reference' => '191afdcb5804db960d26d8566b7e9a2843cab3a0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/service-contracts', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/var-exporter' => array( + 'pretty_version' => 'v5.4.40', + 'version' => '5.4.40.0', + 'reference' => '6a13d37336d512927986e09f19a4bed24178baa6', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/var-exporter', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'topthink/framework' => array( + 'pretty_version' => 'v5.1.42', + 'version' => '5.1.42.0', + 'reference' => 'ecf1a90d397d821ce2df58f7d47e798c17eba3ad', + 'type' => 'think-framework', + 'install_path' => __DIR__ . '/../topthink/framework', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'topthink/think' => array( + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => null, + 'type' => 'project', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'topthink/think-captcha' => array( + 'pretty_version' => 'v2.0.2', + 'version' => '2.0.2.0', + 'reference' => '54c8a51552f99ff9ea89ea9c272383a8f738ceee', + 'type' => 'library', + 'install_path' => __DIR__ . '/../topthink/think-captcha', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'topthink/think-helper' => array( + 'pretty_version' => 'v1.0.7', + 'version' => '1.0.7.0', + 'reference' => '5f92178606c8ce131d36b37a57c58eb71e55f019', + 'type' => 'library', + 'install_path' => __DIR__ . '/../topthink/think-helper', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'topthink/think-image' => array( + 'pretty_version' => 'v1.0.7', + 'version' => '1.0.7.0', + 'reference' => '8586cf47f117481c6d415b20f7dedf62e79d5512', + 'type' => 'library', + 'install_path' => __DIR__ . '/../topthink/think-image', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'topthink/think-installer' => array( + 'pretty_version' => 'v2.0.5', + 'version' => '2.0.5.0', + 'reference' => '38ba647706e35d6704b5d370c06f8a160b635f88', + 'type' => 'composer-plugin', + 'install_path' => __DIR__ . '/../topthink/think-installer', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php new file mode 100644 index 0000000..a8b98d5 --- /dev/null +++ b/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ += 70205)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.5". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} diff --git a/vendor/easywechat-composer/easywechat-composer/.gitignore b/vendor/easywechat-composer/easywechat-composer/.gitignore new file mode 100644 index 0000000..c7a0a65 --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/.gitignore @@ -0,0 +1,5 @@ +.idea/ +/vendor +composer.lock +extensions.php +.php_cs.cache diff --git a/vendor/easywechat-composer/easywechat-composer/.php_cs b/vendor/easywechat-composer/easywechat-composer/.php_cs new file mode 100644 index 0000000..d256932 --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/.php_cs @@ -0,0 +1,29 @@ + + +This source file is subject to the MIT license that is bundled +with this source code in the file LICENSE. +EOF; + +return PhpCsFixer\Config::create() + ->setRiskyAllowed(true) + ->setRules([ + '@Symfony' => true, + 'header_comment' => ['header' => $header], + 'declare_strict_types' => true, + 'ordered_imports' => true, + 'strict_comparison' => true, + 'no_empty_comment' => false, + 'yoda_style' => false, + ]) + ->setFinder( + PhpCsFixer\Finder::create() + ->exclude('vendor') + ->notPath('src/Laravel/config.php', 'src/Laravel/routes.php') + ->in(__DIR__) + ) +; diff --git a/vendor/easywechat-composer/easywechat-composer/.travis.yml b/vendor/easywechat-composer/easywechat-composer/.travis.yml new file mode 100644 index 0000000..e819807 --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/.travis.yml @@ -0,0 +1,12 @@ +language: php + +php: + - 7.0 + - 7.1 + - 7.2 + - 7.3 + +install: + - travis_retry composer install --no-interaction --no-suggest + +script: ./vendor/bin/phpunit diff --git a/vendor/easywechat-composer/easywechat-composer/LICENSE b/vendor/easywechat-composer/easywechat-composer/LICENSE new file mode 100644 index 0000000..3559904 --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 张铭阳 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/easywechat-composer/easywechat-composer/README.md b/vendor/easywechat-composer/easywechat-composer/README.md new file mode 100644 index 0000000..a08c1be --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/README.md @@ -0,0 +1,55 @@ +

              +

              EasyWeChat Composer Plugin

              +

              + +

              + Build Status + Scrutinizer Code Quality + Latest Stable Version + Total Downloads + License +

              + +Usage +--- + +Set the `type` to be `easywechat-extension` in your package composer.json file: + +```json +{ + "name": "your/package", + "type": "easywechat-extension" +} +``` + +Specify server observer classes in the extra section: + +```json +{ + "name": "your/package", + "type": "easywechat-extension", + "extra": { + "observers": [ + "Acme\\Observers\\Handler" + ] + } +} +``` + +Examples +--- +* [easywechat-composer/open-platform-testcase](https://github.com/mingyoung/open-platform-testcase) + +Server Delegation +--- + +> 目前仅支持 Laravel + +1. 在 `config/app.php` 中添加 `EasyWeChatComposer\Laravel\ServiceProvider::class` + +2. 在**本地项目**的 `.env` 文件中添加如下配置: + +``` +EASYWECHAT_DELEGATION=true # false 则不启用 +EASYWECHAT_DELEGATION_HOST=https://example.com # 线上域名 +``` diff --git a/vendor/easywechat-composer/easywechat-composer/composer.json b/vendor/easywechat-composer/easywechat-composer/composer.json new file mode 100644 index 0000000..32d7c94 --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/composer.json @@ -0,0 +1,35 @@ +{ + "name": "easywechat-composer/easywechat-composer", + "description": "The composer plugin for EasyWeChat", + "type": "composer-plugin", + "license": "MIT", + "authors": [ + { + "name": "张铭阳", + "email": "mingyoungcheung@gmail.com" + } + ], + "require": { + "php": ">=7.0", + "composer-plugin-api": "^1.0 || ^2.0" + }, + "require-dev": { + "composer/composer": "^1.0 || ^2.0", + "phpunit/phpunit": "^6.5 || ^7.0" + }, + "autoload": { + "psr-4": { + "EasyWeChatComposer\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "EasyWeChatComposer\\Tests\\": "tests/" + } + }, + "extra": { + "class": "EasyWeChatComposer\\Plugin" + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/vendor/easywechat-composer/easywechat-composer/phpunit.xml b/vendor/easywechat-composer/easywechat-composer/phpunit.xml new file mode 100644 index 0000000..361d92c --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/phpunit.xml @@ -0,0 +1,20 @@ + + + + tests + + + + + src + + + diff --git a/vendor/easywechat-composer/easywechat-composer/src/Commands/ExtensionsCommand.php b/vendor/easywechat-composer/easywechat-composer/src/Commands/ExtensionsCommand.php new file mode 100644 index 0000000..bc0155e --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Commands/ExtensionsCommand.php @@ -0,0 +1,63 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer\Commands; + +use Composer\Command\BaseCommand; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ExtensionsCommand extends BaseCommand +{ + /** + * Configures the current command. + */ + protected function configure() + { + $this->setName('easywechat:extensions') + ->setDescription('Lists all installed extensions.'); + } + + /** + * Executes the current command. + * + * @param InputInterface $input + * @param OutputInterface $output + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $extensions = require __DIR__.'/../../extensions.php'; + + if (empty($extensions) || !is_array($extensions)) { + return $output->writeln('No extension installed.'); + } + + $table = new Table($output); + $table->setHeaders(['Name', 'Observers']) + ->setRows( + array_map([$this, 'getRows'], array_keys($extensions), $extensions) + )->render(); + } + + /** + * @param string $name + * @param array $extension + * + * @return array + */ + protected function getRows($name, $extension) + { + return [$name, implode("\n", $extension['observers'] ?? [])]; + } +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Commands/Provider.php b/vendor/easywechat-composer/easywechat-composer/src/Commands/Provider.php new file mode 100644 index 0000000..928f096 --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Commands/Provider.php @@ -0,0 +1,31 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer\Commands; + +use Composer\Plugin\Capability\CommandProvider; + +class Provider implements CommandProvider +{ + /** + * Retrieves an array of commands. + * + * @return \Composer\Command\BaseCommand[] + */ + public function getCommands() + { + return [ + new ExtensionsCommand(), + ]; + } +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Contracts/Encrypter.php b/vendor/easywechat-composer/easywechat-composer/src/Contracts/Encrypter.php new file mode 100644 index 0000000..af8b8d1 --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Contracts/Encrypter.php @@ -0,0 +1,35 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer\Contracts; + +interface Encrypter +{ + /** + * Encrypt the given value. + * + * @param string $value + * + * @return string + */ + public function encrypt($value); + + /** + * Decrypt the given value. + * + * @param string $payload + * + * @return string + */ + public function decrypt($payload); +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Delegation/DelegationOptions.php b/vendor/easywechat-composer/easywechat-composer/src/Delegation/DelegationOptions.php new file mode 100644 index 0000000..a333261 --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Delegation/DelegationOptions.php @@ -0,0 +1,80 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer\Delegation; + +use EasyWeChatComposer\EasyWeChat; + +class DelegationOptions +{ + /** + * @var array + */ + protected $config = [ + 'enabled' => false, + ]; + + /** + * @return $this + */ + public function enable() + { + $this->config['enabled'] = true; + + return $this; + } + + /** + * @return $this + */ + public function disable() + { + $this->config['enabled'] = false; + + return $this; + } + + /** + * @param bool $ability + * + * @return $this + */ + public function ability($ability) + { + $this->config['enabled'] = (bool) $ability; + + return $this; + } + + /** + * @param string $host + * + * @return $this + */ + public function toHost($host) + { + $this->config['host'] = $host; + + return $this; + } + + /** + * Destructor. + */ + public function __destruct() + { + EasyWeChat::mergeConfig([ + 'delegation' => $this->config, + ]); + } +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Delegation/DelegationTo.php b/vendor/easywechat-composer/easywechat-composer/src/Delegation/DelegationTo.php new file mode 100644 index 0000000..2e9e6db --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Delegation/DelegationTo.php @@ -0,0 +1,83 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer\Delegation; + +use EasyWeChatComposer\Traits\MakesHttpRequests; + +class DelegationTo +{ + use MakesHttpRequests; + + /** + * @var \EasyWeChat\Kernel\ServiceContainer + */ + protected $app; + + /** + * @var array + */ + protected $identifiers = []; + + /** + * @param \EasyWeChat\Kernel\ServiceContainer $app + * @param string $identifier + */ + public function __construct($app, $identifier) + { + $this->app = $app; + + $this->push($identifier); + } + + /** + * @param string $identifier + */ + public function push($identifier) + { + $this->identifiers[] = $identifier; + } + + /** + * @param string $identifier + * + * @return $this + */ + public function __get($identifier) + { + $this->push($identifier); + + return $this; + } + + /** + * @param string $method + * @param array $arguments + * + * @return mixed + */ + public function __call($method, $arguments) + { + $config = array_intersect_key($this->app->getConfig(), array_flip(['app_id', 'secret', 'token', 'aes_key', 'response_type', 'component_app_id', 'refresh_token'])); + + $data = [ + 'config' => $config, + 'application' => get_class($this->app), + 'identifiers' => $this->identifiers, + 'method' => $method, + 'arguments' => $arguments, + ]; + + return $this->request('easywechat-composer/delegate', $data); + } +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Delegation/Hydrate.php b/vendor/easywechat-composer/easywechat-composer/src/Delegation/Hydrate.php new file mode 100644 index 0000000..b83bbe9 --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Delegation/Hydrate.php @@ -0,0 +1,83 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer\Delegation; + +use EasyWeChat; +use EasyWeChatComposer\Http\DelegationResponse; + +class Hydrate +{ + /** + * @var array + */ + protected $attributes; + + /** + * @param array $attributes + */ + public function __construct(array $attributes) + { + $this->attributes = $attributes; + } + + /** + * @return array + */ + public function handle() + { + $app = $this->createsApplication()->shouldntDelegate(); + + foreach ($this->attributes['identifiers'] as $identifier) { + $app = $app->$identifier; + } + + return call_user_func_array([$app, $this->attributes['method']], $this->attributes['arguments']); + } + + /** + * @return \EasyWeChat\Kernel\ServiceContainer + */ + protected function createsApplication() + { + $application = $this->attributes['application']; + + if ($application === EasyWeChat\OpenPlatform\Authorizer\OfficialAccount\Application::class) { + return $this->createsOpenPlatformApplication('officialAccount'); + } + + if ($application === EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Application::class) { + return $this->createsOpenPlatformApplication('miniProgram'); + } + + return new $application($this->buildConfig($this->attributes['config'])); + } + + protected function createsOpenPlatformApplication($type) + { + $config = $this->attributes['config']; + + $authorizerAppId = $config['app_id']; + + $config['app_id'] = $config['component_app_id']; + + return EasyWeChat\Factory::openPlatform($this->buildConfig($config))->$type($authorizerAppId, $config['refresh_token']); + } + + protected function buildConfig(array $config) + { + $config['response_type'] = DelegationResponse::class; + + return $config; + } +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/EasyWeChat.php b/vendor/easywechat-composer/easywechat-composer/src/EasyWeChat.php new file mode 100644 index 0000000..4ff3d9b --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/EasyWeChat.php @@ -0,0 +1,79 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer; + +use EasyWeChatComposer\Delegation\DelegationOptions; + +class EasyWeChat +{ + /** + * @var array + */ + protected static $config = []; + + /** + * Encryption key. + * + * @var string + */ + protected static $encryptionKey; + + /** + * @param array $config + */ + public static function mergeConfig(array $config) + { + static::$config = array_merge(static::$config, $config); + } + + /** + * @return array + */ + public static function config() + { + return static::$config; + } + + /** + * Set encryption key. + * + * @param string $key + * + * @return static + */ + public static function setEncryptionKey(string $key) + { + static::$encryptionKey = $key; + + return new static(); + } + + /** + * Get encryption key. + * + * @return string + */ + public static function getEncryptionKey(): string + { + return static::$encryptionKey; + } + + /** + * @return \EasyWeChatComposer\Delegation\DelegationOptions + */ + public static function withDelegation() + { + return new DelegationOptions(); + } +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Encryption/DefaultEncrypter.php b/vendor/easywechat-composer/easywechat-composer/src/Encryption/DefaultEncrypter.php new file mode 100644 index 0000000..2c4cd53 --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Encryption/DefaultEncrypter.php @@ -0,0 +1,89 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer\Encryption; + +use EasyWeChatComposer\Contracts\Encrypter; +use EasyWeChatComposer\Exceptions\DecryptException; +use EasyWeChatComposer\Exceptions\EncryptException; + +class DefaultEncrypter implements Encrypter +{ + /** + * @var string + */ + protected $key; + + /** + * @var string + */ + protected $cipher; + + /** + * @param string $key + * @param string $cipher + */ + public function __construct($key, $cipher = 'AES-256-CBC') + { + $this->key = $key; + $this->cipher = $cipher; + } + + /** + * Encrypt the given value. + * + * @param string $value + * + * @return string + * + * @throws \EasyWeChatComposer\Exceptions\EncryptException + */ + public function encrypt($value) + { + $iv = random_bytes(openssl_cipher_iv_length($this->cipher)); + + $value = openssl_encrypt($value, $this->cipher, $this->key, 0, $iv); + + if ($value === false) { + throw new EncryptException('Could not encrypt the data.'); + } + + $iv = base64_encode($iv); + + return base64_encode(json_encode(compact('iv', 'value'))); + } + + /** + * Decrypt the given value. + * + * @param string $payload + * + * @return string + * + * @throws \EasyWeChatComposer\Exceptions\DecryptException + */ + public function decrypt($payload) + { + $payload = json_decode(base64_decode($payload), true); + + $iv = base64_decode($payload['iv']); + + $decrypted = openssl_decrypt($payload['value'], $this->cipher, $this->key, 0, $iv); + + if ($decrypted === false) { + throw new DecryptException('Could not decrypt the data.'); + } + + return $decrypted; + } +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Exceptions/DecryptException.php b/vendor/easywechat-composer/easywechat-composer/src/Exceptions/DecryptException.php new file mode 100644 index 0000000..e210d1f --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Exceptions/DecryptException.php @@ -0,0 +1,21 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer\Exceptions; + +use Exception; + +class DecryptException extends Exception +{ + // +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Exceptions/DelegationException.php b/vendor/easywechat-composer/easywechat-composer/src/Exceptions/DelegationException.php new file mode 100644 index 0000000..0af9c2d --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Exceptions/DelegationException.php @@ -0,0 +1,42 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer\Exceptions; + +use Exception; + +class DelegationException extends Exception +{ + /** + * @var string + */ + protected $exception; + + /** + * @param string $exception + */ + public function setException($exception) + { + $this->exception = $exception; + + return $this; + } + + /** + * @return string + */ + public function getException() + { + return $this->exception; + } +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Exceptions/EncryptException.php b/vendor/easywechat-composer/easywechat-composer/src/Exceptions/EncryptException.php new file mode 100644 index 0000000..f88f8f4 --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Exceptions/EncryptException.php @@ -0,0 +1,21 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer\Exceptions; + +use Exception; + +class EncryptException extends Exception +{ + // +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Extension.php b/vendor/easywechat-composer/easywechat-composer/src/Extension.php new file mode 100644 index 0000000..6a521d1 --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Extension.php @@ -0,0 +1,143 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer; + +use EasyWeChat\Kernel\Contracts\EventHandlerInterface; +use Pimple\Container; +use ReflectionClass; + +class Extension +{ + /** + * @var \Pimple\Container + */ + protected $app; + + /** + * @var string + */ + protected $manifestPath; + + /** + * @var array|null + */ + protected $manifest; + + /** + * @param \Pimple\Container $app + */ + public function __construct(Container $app) + { + $this->app = $app; + $this->manifestPath = __DIR__.'/../extensions.php'; + } + + /** + * Get observers. + * + * @return array + */ + public function observers(): array + { + if ($this->shouldIgnore()) { + return []; + } + + $observers = []; + + foreach ($this->getManifest() as $name => $extra) { + $observers = array_merge($observers, $extra['observers'] ?? []); + } + + return array_map([$this, 'listObserver'], array_filter($observers, [$this, 'validateObserver'])); + } + + /** + * @param mixed $observer + * + * @return bool + */ + protected function isDisable($observer): bool + { + return in_array($observer, $this->app->config->get('disable_observers', [])); + } + + /** + * Get the observers should be ignore. + * + * @return bool + */ + protected function shouldIgnore(): bool + { + return !file_exists($this->manifestPath) || $this->isDisable('*'); + } + + /** + * Validate the given observer. + * + * @param mixed $observer + * + * @return bool + * + * @throws \ReflectionException + */ + protected function validateObserver($observer): bool + { + return !$this->isDisable($observer) + && (new ReflectionClass($observer))->implementsInterface(EventHandlerInterface::class) + && $this->accessible($observer); + } + + /** + * Determine whether the given observer is accessible. + * + * @param string $observer + * + * @return bool + */ + protected function accessible($observer): bool + { + if (!method_exists($observer, 'getAccessor')) { + return true; + } + + return in_array(get_class($this->app), (array) $observer::getAccessor()); + } + + /** + * @param mixed $observer + * + * @return array + */ + protected function listObserver($observer): array + { + $condition = method_exists($observer, 'onCondition') ? $observer::onCondition() : '*'; + + return [$observer, $condition]; + } + + /** + * Get the easywechat manifest. + * + * @return array + */ + protected function getManifest(): array + { + if (!is_null($this->manifest)) { + return $this->manifest; + } + + return $this->manifest = file_exists($this->manifestPath) ? require $this->manifestPath : []; + } +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Http/DelegationResponse.php b/vendor/easywechat-composer/easywechat-composer/src/Http/DelegationResponse.php new file mode 100644 index 0000000..329eb54 --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Http/DelegationResponse.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer\Http; + +class DelegationResponse extends Response +{ + /** + * @return string + */ + public function getBodyContents() + { + return $this->response->getBodyContents(); + } +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Http/Response.php b/vendor/easywechat-composer/easywechat-composer/src/Http/Response.php new file mode 100644 index 0000000..534bf54 --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Http/Response.php @@ -0,0 +1,104 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer\Http; + +use EasyWeChat\Kernel\Contracts\Arrayable; +use EasyWeChat\Kernel\Http\Response as HttpResponse; +use JsonSerializable; + +class Response implements Arrayable, JsonSerializable +{ + /** + * @var \EasyWeChat\Kernel\Http\Response + */ + protected $response; + + /** + * @var array + */ + protected $array; + + /** + * @param \EasyWeChat\Kernel\Http\Response $response + */ + public function __construct(HttpResponse $response) + { + $this->response = $response; + } + + /** + * @see \ArrayAccess::offsetExists + * + * @param string $offset + * + * @return bool + */ + public function offsetExists($offset) + { + return isset($this->toArray()[$offset]); + } + + /** + * @see \ArrayAccess::offsetGet + * + * @param string $offset + * + * @return mixed + */ + public function offsetGet($offset) + { + return $this->toArray()[$offset] ?? null; + } + + /** + * @see \ArrayAccess::offsetSet + * + * @param string $offset + * @param mixed $value + */ + public function offsetSet($offset, $value) + { + // + } + + /** + * @see \ArrayAccess::offsetUnset + * + * @param string $offset + */ + public function offsetUnset($offset) + { + // + } + + /** + * Get the instance as an array. + * + * @return array + */ + public function toArray() + { + return $this->array ?: $this->array = $this->response->toArray(); + } + + /** + * Convert the object into something JSON serializable. + * + * @return array + */ + public function jsonSerialize() + { + return $this->toArray(); + } +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Laravel/Http/Controllers/DelegatesController.php b/vendor/easywechat-composer/easywechat-composer/src/Laravel/Http/Controllers/DelegatesController.php new file mode 100644 index 0000000..ad014d8 --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Laravel/Http/Controllers/DelegatesController.php @@ -0,0 +1,49 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer\Laravel\Http\Controllers; + +use EasyWeChatComposer\Delegation\Hydrate; +use EasyWeChatComposer\Encryption\DefaultEncrypter; +use Illuminate\Http\Request; +use Throwable; + +class DelegatesController +{ + /** + * @param \Illuminate\Http\Request $request + * @param \EasyWeChatComposer\Encryption\DefaultEncrypter $encrypter + * + * @return \Illuminate\Http\Response + */ + public function __invoke(Request $request, DefaultEncrypter $encrypter) + { + try { + $data = json_decode($encrypter->decrypt($request->get('encrypted')), true); + + $hydrate = new Hydrate($data); + + $response = $hydrate->handle(); + + return response()->json([ + 'response_type' => get_class($response), + 'response' => $encrypter->encrypt($response->getBodyContents()), + ]); + } catch (Throwable $t) { + return [ + 'exception' => get_class($t), + 'message' => $t->getMessage(), + ]; + } + } +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Laravel/ServiceProvider.php b/vendor/easywechat-composer/easywechat-composer/src/Laravel/ServiceProvider.php new file mode 100644 index 0000000..4c43b04 --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Laravel/ServiceProvider.php @@ -0,0 +1,116 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer\Laravel; + +use EasyWeChatComposer\EasyWeChat; +use EasyWeChatComposer\Encryption\DefaultEncrypter; +use Illuminate\Foundation\Application; +use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Route; +use Illuminate\Support\ServiceProvider as LaravelServiceProvider; +use RuntimeException; + +class ServiceProvider extends LaravelServiceProvider +{ + /** + * Bootstrap any application services. + */ + public function boot() + { + $this->registerRoutes(); + $this->publishes([ + __DIR__.'/config.php' => config_path('easywechat-composer.php'), + ]); + + EasyWeChat::setEncryptionKey( + $defaultKey = $this->getKey() + ); + + EasyWeChat::withDelegation() + ->toHost($this->config('delegation.host')) + ->ability($this->config('delegation.enabled')); + + $this->app->when(DefaultEncrypter::class)->needs('$key')->give($defaultKey); + } + + /** + * Register routes. + */ + protected function registerRoutes() + { + Route::prefix('easywechat-composer')->namespace('EasyWeChatComposer\Laravel\Http\Controllers')->group(function () { + $this->loadRoutesFrom(__DIR__.'/routes.php'); + }); + } + + /** + * Register any application services. + */ + public function register() + { + $this->configure(); + } + + /** + * Register config. + */ + protected function configure() + { + $this->mergeConfigFrom( + __DIR__.'/config.php', 'easywechat-composer' + ); + } + + /** + * Get the specified configuration value. + * + * @param string|null $key + * @param mixed $default + * + * @return mixed + */ + protected function config($key = null, $default = null) + { + $config = $this->app['config']->get('easywechat-composer'); + + if (is_null($key)) { + return $config; + } + + return Arr::get($config, $key, $default); + } + + /** + * @return string + */ + protected function getKey() + { + return $this->config('encryption.key') ?: $this->getMd5Key(); + } + + /** + * @return string + */ + protected function getMd5Key() + { + $ttl = (version_compare(Application::VERSION, '5.8') === -1) ? 30 : 1800; + + return Cache::remember('easywechat-composer.encryption_key', $ttl, function () { + throw_unless(file_exists($path = base_path('composer.lock')), RuntimeException::class, 'No encryption key provided.'); + + return md5_file($path); + }); + } +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Laravel/config.php b/vendor/easywechat-composer/easywechat-composer/src/Laravel/config.php new file mode 100644 index 0000000..5deabef --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Laravel/config.php @@ -0,0 +1,29 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +return [ + + 'encryption' => [ + + 'key' => env('EASYWECHAT_KEY'), + + ], + + 'delegation' => [ + + 'enabled' => env('EASYWECHAT_DELEGATION', false), + + 'host' => env('EASYWECHAT_DELEGATION_HOST'), + ], + +]; diff --git a/vendor/easywechat-composer/easywechat-composer/src/Laravel/routes.php b/vendor/easywechat-composer/easywechat-composer/src/Laravel/routes.php new file mode 100644 index 0000000..6a3041a --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Laravel/routes.php @@ -0,0 +1,16 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +use Illuminate\Support\Facades\Route; + +Route::post('delegate', 'DelegatesController'); diff --git a/vendor/easywechat-composer/easywechat-composer/src/ManifestManager.php b/vendor/easywechat-composer/easywechat-composer/src/ManifestManager.php new file mode 100644 index 0000000..e1af69c --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/ManifestManager.php @@ -0,0 +1,127 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer; + +use Composer\Plugin\PluginInterface; + +class ManifestManager +{ + const PACKAGE_TYPE = 'easywechat-extension'; + + const EXTRA_OBSERVER = 'observers'; + + /** + * The vendor path. + * + * @var string + */ + protected $vendorPath; + + /** + * The manifest path. + * + * @var string + */ + protected $manifestPath; + + /** + * @param string $vendorPath + * @param string|null $manifestPath + */ + public function __construct(string $vendorPath, string $manifestPath = null) + { + $this->vendorPath = $vendorPath; + $this->manifestPath = $manifestPath ?: $vendorPath.'/easywechat-composer/easywechat-composer/extensions.php'; + } + + /** + * Remove manifest file. + * + * @return $this + */ + public function unlink() + { + if (file_exists($this->manifestPath)) { + @unlink($this->manifestPath); + } + + return $this; + } + + /** + * Build the manifest file. + */ + public function build() + { + $packages = []; + + if (file_exists($installed = $this->vendorPath.'/composer/installed.json')) { + $packages = json_decode(file_get_contents($installed), true); + if (version_compare(PluginInterface::PLUGIN_API_VERSION, '2.0.0', 'ge')) { + $packages = $packages['packages']; + } + } + + $this->write($this->map($packages)); + } + + /** + * @param array $packages + * + * @return array + */ + protected function map(array $packages): array + { + $manifest = []; + + $packages = array_filter($packages, function ($package) { + if(isset($package['type'])){ + return $package['type'] === self::PACKAGE_TYPE; + } + }); + + foreach ($packages as $package) { + $manifest[$package['name']] = [self::EXTRA_OBSERVER => $package['extra'][self::EXTRA_OBSERVER] ?? []]; + } + + return $manifest; + } + + /** + * Write the manifest array to a file. + * + * @param array $manifest + */ + protected function write(array $manifest) + { + file_put_contents( + $this->manifestPath, + 'invalidate($this->manifestPath); + } + + /** + * Invalidate the given file. + * + * @param string $file + */ + protected function invalidate($file) + { + if (function_exists('opcache_invalidate')) { + @opcache_invalidate($file, true); + } + } +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Plugin.php b/vendor/easywechat-composer/easywechat-composer/src/Plugin.php new file mode 100644 index 0000000..cd9e504 --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Plugin.php @@ -0,0 +1,107 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer; + +use Composer\Composer; +use Composer\EventDispatcher\EventSubscriberInterface; +use Composer\Installer\PackageEvent; +use Composer\Installer\PackageEvents; +use Composer\IO\IOInterface; +use Composer\Plugin\Capable; +use Composer\Plugin\PluginInterface; +use Composer\Script\Event; +use Composer\Script\ScriptEvents; + +class Plugin implements PluginInterface, EventSubscriberInterface, Capable +{ + /** + * @var bool + */ + protected $activated = true; + + /** + * Apply plugin modifications to Composer. + */ + public function activate(Composer $composer, IOInterface $io) + { + // + } + + /** + * Remove any hooks from Composer. + * + * This will be called when a plugin is deactivated before being + * uninstalled, but also before it gets upgraded to a new version + * so the old one can be deactivated and the new one activated. + */ + public function deactivate(Composer $composer, IOInterface $io) + { + // + } + + /** + * Prepare the plugin to be uninstalled. + * + * This will be called after deactivate. + */ + public function uninstall(Composer $composer, IOInterface $io) + { + } + + /** + * @return array + */ + public function getCapabilities() + { + return [ + 'Composer\Plugin\Capability\CommandProvider' => 'EasyWeChatComposer\Commands\Provider', + ]; + } + + /** + * Listen events. + * + * @return array + */ + public static function getSubscribedEvents() + { + return [ + PackageEvents::PRE_PACKAGE_UNINSTALL => 'prePackageUninstall', + ScriptEvents::POST_AUTOLOAD_DUMP => 'postAutoloadDump', + ]; + } + + /** + * @param \Composer\Installer\PackageEvent + */ + public function prePackageUninstall(PackageEvent $event) + { + if ($event->getOperation()->getPackage()->getName() === 'overtrue/wechat') { + $this->activated = false; + } + } + + public function postAutoloadDump(Event $event) + { + if (!$this->activated) { + return; + } + + $manifest = new ManifestManager( + rtrim($event->getComposer()->getConfig()->get('vendor-dir'), '/') + ); + + $manifest->unlink()->build(); + } +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Traits/MakesHttpRequests.php b/vendor/easywechat-composer/easywechat-composer/src/Traits/MakesHttpRequests.php new file mode 100644 index 0000000..091a75c --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Traits/MakesHttpRequests.php @@ -0,0 +1,110 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer\Traits; + +use EasyWeChat\Kernel\Http\StreamResponse; +use EasyWeChat\Kernel\Traits\ResponseCastable; +use EasyWeChatComposer\Contracts\Encrypter; +use EasyWeChatComposer\EasyWeChat; +use EasyWeChatComposer\Encryption\DefaultEncrypter; +use EasyWeChatComposer\Exceptions\DelegationException; +use GuzzleHttp\Client; +use GuzzleHttp\ClientInterface; + +trait MakesHttpRequests +{ + use ResponseCastable; + + /** + * @var \GuzzleHttp\ClientInterface + */ + protected $httpClient; + + /** + * @var \EasyWeChatComposer\Contracts\Encrypter + */ + protected $encrypter; + + /** + * @param string $endpoint + * @param array $payload + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function request($endpoint, array $payload) + { + $response = $this->getHttpClient()->request('POST', $endpoint, [ + 'form_params' => $this->buildFormParams($payload), + ]); + + $parsed = $this->parseResponse($response); + + return $this->detectAndCastResponseToType( + $this->getEncrypter()->decrypt($parsed['response']), + ($parsed['response_type'] === StreamResponse::class) ? 'raw' : $this->app['config']['response_type'] + ); + } + + /** + * @param array $payload + * + * @return array + */ + protected function buildFormParams($payload) + { + return [ + 'encrypted' => $this->getEncrypter()->encrypt(json_encode($payload)), + ]; + } + + /** + * @param \Psr\Http\Message\ResponseInterface $response + * + * @return array + */ + protected function parseResponse($response) + { + $result = json_decode((string) $response->getBody(), true); + + if (isset($result['exception'])) { + throw (new DelegationException($result['message']))->setException($result['exception']); + } + + return $result; + } + + /** + * @return \GuzzleHttp\ClientInterface + */ + protected function getHttpClient(): ClientInterface + { + return $this->httpClient ?: $this->httpClient = new Client([ + 'base_uri' => $this->app['config']['delegation']['host'], + ]); + } + + /** + * @return \EasyWeChatComposer\Contracts\Encrypter + */ + protected function getEncrypter(): Encrypter + { + return $this->encrypter ?: $this->encrypter = new DefaultEncrypter( + EasyWeChat::getEncryptionKey() + ); + } +} diff --git a/vendor/easywechat-composer/easywechat-composer/src/Traits/WithAggregator.php b/vendor/easywechat-composer/easywechat-composer/src/Traits/WithAggregator.php new file mode 100644 index 0000000..a3eb16e --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/src/Traits/WithAggregator.php @@ -0,0 +1,60 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer\Traits; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChatComposer\Delegation\DelegationTo; +use EasyWeChatComposer\EasyWeChat; + +trait WithAggregator +{ + /** + * Aggregate. + */ + protected function aggregate() + { + foreach (EasyWeChat::config() as $key => $value) { + $this['config']->set($key, $value); + } + } + + /** + * @return bool + */ + public function shouldDelegate($id) + { + return $this['config']->get('delegation.enabled') + && $this->offsetGet($id) instanceof BaseClient; + } + + /** + * @return $this + */ + public function shouldntDelegate() + { + $this['config']->set('delegation.enabled', false); + + return $this; + } + + /** + * @param string $id + * + * @return \EasyWeChatComposer\Delegation + */ + public function delegateTo($id) + { + return new DelegationTo($this, $id); + } +} diff --git a/vendor/easywechat-composer/easywechat-composer/tests/ManifestManagerTest.php b/vendor/easywechat-composer/easywechat-composer/tests/ManifestManagerTest.php new file mode 100644 index 0000000..23b8e2c --- /dev/null +++ b/vendor/easywechat-composer/easywechat-composer/tests/ManifestManagerTest.php @@ -0,0 +1,37 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChatComposer\Tests; + +use EasyWeChatComposer\ManifestManager; +use PHPUnit\Framework\TestCase; + +class ManifestManagerTest extends TestCase +{ + private $vendorPath; + private $manifestPath; + + protected function getManifestManager() + { + return new ManifestManager( + $this->vendorPath = __DIR__.'/__fixtures__/vendor/', + $this->manifestPath = __DIR__.'/__fixtures__/extensions.php' + ); + } + + public function testUnlink() + { + $this->assertInstanceOf(ManifestManager::class, $this->getManifestManager()->unlink()); + $this->assertFalse(file_exists($this->manifestPath)); + } +} diff --git a/vendor/ezyang/htmlpurifier/CREDITS b/vendor/ezyang/htmlpurifier/CREDITS new file mode 100644 index 0000000..7921b45 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/CREDITS @@ -0,0 +1,9 @@ + +CREDITS + +Almost everything written by Edward Z. Yang (Ambush Commander). Lots of thanks +to the DevNetwork Community for their help (see docs/ref-devnetwork.html for +more details), Feyd especially (namely IPv6 and optimization). Thanks to RSnake +for letting me package his fantastic XSS cheatsheet for a smoketest. + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/INSTALL b/vendor/ezyang/htmlpurifier/INSTALL new file mode 100644 index 0000000..e6dd02a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/INSTALL @@ -0,0 +1,373 @@ + +Install + How to install HTML Purifier + +HTML Purifier is designed to run out of the box, so actually using the +library is extremely easy. (Although... if you were looking for a +step-by-step installation GUI, you've downloaded the wrong software!) + +While the impatient can get going immediately with some of the sample +code at the bottom of this library, it's well worth reading this entire +document--most of the other documentation assumes that you are familiar +with these contents. + + +--------------------------------------------------------------------------- +1. Compatibility + +HTML Purifier is PHP 5 and PHP 7, and is actively tested from PHP 5.0.5 +and up. It has no core dependencies with other libraries. + +These optional extensions can enhance the capabilities of HTML Purifier: + + * iconv : Converts text to and from non-UTF-8 encodings + * bcmath : Used for unit conversion and imagecrash protection + * tidy : Used for pretty-printing HTML + +These optional libraries can enhance the capabilities of HTML Purifier: + + * CSSTidy : Clean CSS stylesheets using %Core.ExtractStyleBlocks + Note: You should use the modernized fork of CSSTidy available + at https://github.com/Cerdic/CSSTidy + * Net_IDNA2 (PEAR) : IRI support using %Core.EnableIDNA + Note: This is not necessary for PHP 5.3 or later + +--------------------------------------------------------------------------- +2. Reconnaissance + +A big plus of HTML Purifier is its inerrant support of standards, so +your web-pages should be standards-compliant. (They should also use +semantic markup, but that's another issue altogether, one HTML Purifier +cannot fix without reading your mind.) + +HTML Purifier can process these doctypes: + +* XHTML 1.0 Transitional (default) +* XHTML 1.0 Strict +* HTML 4.01 Transitional +* HTML 4.01 Strict +* XHTML 1.1 + +...and these character encodings: + +* UTF-8 (default) +* Any encoding iconv supports (with crippled internationalization support) + +These defaults reflect what my choices would be if I were authoring an +HTML document, however, what you choose depends on the nature of your +codebase. If you don't know what doctype you are using, you can determine +the doctype from this identifier at the top of your source code: + + + +...and the character encoding from this code: + + + +If the character encoding declaration is missing, STOP NOW, and +read 'docs/enduser-utf8.html' (web accessible at +http://htmlpurifier.org/docs/enduser-utf8.html). In fact, even if it is +present, read this document anyway, as many websites specify their +document's character encoding incorrectly. + + +--------------------------------------------------------------------------- +3. Including the library + +The procedure is quite simple: + + require_once '/path/to/library/HTMLPurifier.auto.php'; + +This will setup an autoloader, so the library's files are only included +when you use them. + +Only the contents in the library/ folder are necessary, so you can remove +everything else when using HTML Purifier in a production environment. + +If you installed HTML Purifier via PEAR, all you need to do is: + + require_once 'HTMLPurifier.auto.php'; + +Please note that the usual PEAR practice of including just the classes you +want will not work with HTML Purifier's autoloading scheme. + +Advanced users, read on; other users can skip to section 4. + +Autoload compatibility +---------------------- + + HTML Purifier attempts to be as smart as possible when registering an + autoloader, but there are some cases where you will need to change + your own code to accomodate HTML Purifier. These are those cases: + + PHP VERSION IS LESS THAN 5.1.2, AND YOU'VE DEFINED __autoload + Because spl_autoload_register() doesn't exist in early versions + of PHP 5, HTML Purifier has no way of adding itself to the autoload + stack. Modify your __autoload function to test + HTMLPurifier_Bootstrap::autoload($class) + + For example, suppose your autoload function looks like this: + + function __autoload($class) { + require str_replace('_', '/', $class) . '.php'; + return true; + } + + A modified version with HTML Purifier would look like this: + + function __autoload($class) { + if (HTMLPurifier_Bootstrap::autoload($class)) return true; + require str_replace('_', '/', $class) . '.php'; + return true; + } + + Note that there *is* some custom behavior in our autoloader; the + original autoloader in our example would work for 99% of the time, + but would fail when including language files. + + AN __autoload FUNCTION IS DECLARED AFTER OUR AUTOLOADER IS REGISTERED + spl_autoload_register() has the curious behavior of disabling + the existing __autoload() handler. Users need to explicitly + spl_autoload_register('__autoload'). Because we use SPL when it + is available, __autoload() will ALWAYS be disabled. If __autoload() + is declared before HTML Purifier is loaded, this is not a problem: + HTML Purifier will register the function for you. But if it is + declared afterwards, it will mysteriously not work. This + snippet of code (after your autoloader is defined) will fix it: + + spl_autoload_register('__autoload') + + Users should also be on guard if they use a version of PHP previous + to 5.1.2 without an autoloader--HTML Purifier will define __autoload() + for you, which can collide with an autoloader that was added by *you* + later. + + +For better performance +---------------------- + + Opcode caches, which greatly speed up PHP initialization for scripts + with large amounts of code (HTML Purifier included), don't like + autoloaders. We offer an include file that includes all of HTML Purifier's + files in one go in an opcode cache friendly manner: + + // If /path/to/library isn't already in your include path, uncomment + // the below line: + // require '/path/to/library/HTMLPurifier.path.php'; + + require 'HTMLPurifier.includes.php'; + + Optional components still need to be included--you'll know if you try to + use a feature and you get a class doesn't exists error! The autoloader + can be used in conjunction with this approach to catch classes that are + missing. Simply add this afterwards: + + require 'HTMLPurifier.autoload.php'; + +Standalone version +------------------ + + HTML Purifier has a standalone distribution; you can also generate + a standalone file from the full version by running the script + maintenance/generate-standalone.php . The standalone version has the + benefit of having most of its code in one file, so parsing is much + faster and the library is easier to manage. + + If HTMLPurifier.standalone.php exists in the library directory, you + can use it like this: + + require '/path/to/HTMLPurifier.standalone.php'; + + This is equivalent to including HTMLPurifier.includes.php, except that + the contents of standalone/ will be added to your path. To override this + behavior, specify a new HTMLPURIFIER_PREFIX where standalone files can + be found (usually, this will be one directory up, the "true" library + directory in full distributions). Don't forget to set your path too! + + The autoloader can be added to the end to ensure the classes are + loaded when necessary; otherwise you can manually include them. + To use the autoloader, use this: + + require 'HTMLPurifier.autoload.php'; + +For advanced users +------------------ + + HTMLPurifier.auto.php performs a number of operations that can be done + individually. These are: + + HTMLPurifier.path.php + Puts /path/to/library in the include path. For high performance, + this should be done in php.ini. + + HTMLPurifier.autoload.php + Registers our autoload handler HTMLPurifier_Bootstrap::autoload($class). + + You can do these operations by yourself--in fact, you must modify your own + autoload handler if you are using a version of PHP earlier than PHP 5.1.2 + (See "Autoload compatibility" above). + + +--------------------------------------------------------------------------- +4. Configuration + +HTML Purifier is designed to run out-of-the-box, but occasionally HTML +Purifier needs to be told what to do. If you answer no to any of these +questions, read on; otherwise, you can skip to the next section (or, if you're +into configuring things just for the heck of it, skip to 4.3). + +* Am I using UTF-8? +* Am I using XHTML 1.0 Transitional? + +If you answered no to any of these questions, instantiate a configuration +object and read on: + + $config = HTMLPurifier_Config::createDefault(); + + +4.1. Setting a different character encoding + +You really shouldn't use any other encoding except UTF-8, especially if you +plan to support multilingual websites (read section three for more details). +However, switching to UTF-8 is not always immediately feasible, so we can +adapt. + +HTML Purifier uses iconv to support other character encodings, as such, +any encoding that iconv supports +HTML Purifier supports with this code: + + $config->set('Core.Encoding', /* put your encoding here */); + +An example usage for Latin-1 websites (the most common encoding for English +websites): + + $config->set('Core.Encoding', 'ISO-8859-1'); + +Note that HTML Purifier's support for non-Unicode encodings is crippled by the +fact that any character not supported by that encoding will be silently +dropped, EVEN if it is ampersand escaped. If you want to work around +this, you are welcome to read docs/enduser-utf8.html for a fix, +but please be cognizant of the issues the "solution" creates (for this +reason, I do not include the solution in this document). + + +4.2. Setting a different doctype + +For those of you using HTML 4.01 Transitional, you can disable +XHTML output like this: + + $config->set('HTML.Doctype', 'HTML 4.01 Transitional'); + +Other supported doctypes include: + + * HTML 4.01 Strict + * HTML 4.01 Transitional + * XHTML 1.0 Strict + * XHTML 1.0 Transitional + * XHTML 1.1 + + +4.3. Other settings + +There are more configuration directives which can be read about +here: They're a bit boring, +but they can help out for those of you who like to exert maximum control over +your code. Some of the more interesting ones are configurable at the +demo and are well worth looking into +for your own system. + +For example, you can fine tune allowed elements and attributes, convert +relative URLs to absolute ones, and even autoparagraph input text! These +are, respectively, %HTML.Allowed, %URI.MakeAbsolute and %URI.Base, and +%AutoFormat.AutoParagraph. The %Namespace.Directive naming convention +translates to: + + $config->set('Namespace.Directive', $value); + +E.g. + + $config->set('HTML.Allowed', 'p,b,a[href],i'); + $config->set('URI.Base', 'http://www.example.com'); + $config->set('URI.MakeAbsolute', true); + $config->set('AutoFormat.AutoParagraph', true); + + +--------------------------------------------------------------------------- +5. Caching + +HTML Purifier generates some cache files (generally one or two) to speed up +its execution. For maximum performance, make sure that +library/HTMLPurifier/DefinitionCache/Serializer is writeable by the webserver. + +If you are in the library/ folder of HTML Purifier, you can set the +appropriate permissions using: + + chmod -R 0755 HTMLPurifier/DefinitionCache/Serializer + +If the above command doesn't work, you may need to assign write permissions +to group: + + chmod -R 0775 HTMLPurifier/DefinitionCache/Serializer + +You can also chmod files via your FTP client; this option +is usually accessible by right clicking the corresponding directory and +then selecting "chmod" or "file permissions". + +Starting with 2.0.1, HTML Purifier will generate friendly error messages +that will tell you exactly what you have to chmod the directory to, if in doubt, +follow its advice. + +If you are unable or unwilling to give write permissions to the cache +directory, you can either disable the cache (and suffer a performance +hit): + + $config->set('Core.DefinitionCache', null); + +Or move the cache directory somewhere else (no trailing slash): + + $config->set('Cache.SerializerPath', '/home/user/absolute/path'); + + +--------------------------------------------------------------------------- +6. Using the code + +The interface is mind-numbingly simple: + + $purifier = new HTMLPurifier($config); + $clean_html = $purifier->purify( $dirty_html ); + +That's it! For more examples, check out docs/examples/ (they aren't very +different though). Also, docs/enduser-slow.html gives advice on what to +do if HTML Purifier is slowing down your application. + + +--------------------------------------------------------------------------- +7. Quick install + +First, make sure library/HTMLPurifier/DefinitionCache/Serializer is +writable by the webserver (see Section 5: Caching above for details). +If your website is in UTF-8 and XHTML Transitional, use this code: + +purify($dirty_html); +?> + +If your website is in a different encoding or doctype, use this code: + +set('Core.Encoding', 'ISO-8859-1'); // replace with your encoding + $config->set('HTML.Doctype', 'HTML 4.01 Transitional'); // replace with your doctype + $purifier = new HTMLPurifier($config); + + $clean_html = $purifier->purify($dirty_html); +?> + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/INSTALL.fr.utf8 b/vendor/ezyang/htmlpurifier/INSTALL.fr.utf8 new file mode 100644 index 0000000..95164ab --- /dev/null +++ b/vendor/ezyang/htmlpurifier/INSTALL.fr.utf8 @@ -0,0 +1,60 @@ + +Installation + Comment installer HTML Purifier + +Attention : Ce document est encodé en UTF-8, si les lettres avec des accents +ne s'affichent pas, prenez un meilleur éditeur de texte. + +L'installation de HTML Purifier est très simple, parce qu'il n'a pas besoin +de configuration. Pour les utilisateurs impatients, le code se trouve dans le +pied de page, mais je recommande de lire le document. + +1. Compatibilité + +HTML Purifier fonctionne avec PHP 5. PHP 5.0.5 est la dernière version testée. +Il ne dépend pas d'autres librairies. + +Les extensions optionnelles sont iconv (généralement déjà installée) et tidy +(répendue aussi). Si vous utilisez UTF-8 et que vous ne voulez pas l'indentation, +vous pouvez utiliser HTML Purifier sans ces extensions. + + +2. Inclure la librairie + +Quand vous devez l'utilisez, incluez le : + + require_once('/path/to/library/HTMLPurifier.auto.php'); + +Ne pas l'inclure si ce n'est pas nécessaire, car HTML Purifier est lourd. + +HTML Purifier utilise "autoload". Si vous avez défini la fonction __autoload, +vous devez ajouter cette fonction : + + spl_autoload_register('__autoload') + +Plus d'informations dans le document "INSTALL". + +3. Installation rapide + +Si votre site Web est en UTF-8 et XHTML Transitional, utilisez : + +purify($html_a_purifier); +?> + +Sinon, utilisez : + +set('Core', 'Encoding', 'ISO-8859-1'); //Remplacez par votre + encodage + $config->set('Core', 'XHTML', true); //Remplacer par false si HTML 4.01 + $purificateur = new HTMLPurifier($config); + $html_propre = $purificateur->purify($html_a_purifier); +?> + + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/LICENSE b/vendor/ezyang/htmlpurifier/LICENSE new file mode 100644 index 0000000..8c88a20 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/LICENSE @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/NEWS b/vendor/ezyang/htmlpurifier/NEWS new file mode 100644 index 0000000..9b6e102 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/NEWS @@ -0,0 +1,1190 @@ +NEWS ( CHANGELOG and HISTORY ) HTMLPurifier +||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + += KEY ==================== + # Breaks back-compat + ! Feature + - Bugfix + + Sub-comment + . Internal change +========================== + +4.10.0, released 2018-02-22 +# PHP 5.3 is no longer officially supported by HTML Purifier + (we did not specifically break support, but we are no longer + testing on PHP 5.3) +! Relative CSS length units are now supported +- A few PHP 7.2 compatibility fixes, thanks John Flatness + +- Improve portability with old versions of libxml which don't + support accessing the data of a node +- IDNA2008 is now used for converting domains to ASCII, fixing + some rather strange bugs with international domains +- Fix race condition resulting in E_WARNING when creating + directories with Serializer + +4.9.3, released 2017-06-02 +- Workaround PHP 7.1 infinite loop when opcode cache is enabled. + Thanks @Xiphin (#134, #135) +- Don't use autoloader when testing for DOMDocument. Hypothetically, + this could cause your install to start using DirectLex if you had + previously been monkeypatching in a custom, autoloaded implementation + of DOMDocument. Don't do that. Thanks @Izumi-kun (#130) + +4.9.2, released 2017-03-12 +- Fixes PHP 5.3 compatibility +- Fix breakage when decoding decimal entities. Thanks @rybakit (#129) + +4.9.1, released 2017-03-08 +! %URI.DefaultScheme can now be set to null, in which case + all relative paths are removed. +! New CSS properties: min-width, max-width, min-height, max-height (#94) +! Transparency (rgba) and hsl/hsla supported where color CSS is present. + Thanks @fxbt for contributing the patch. (#118) +- When idn_to_ascii is defined, we might accept malformed + hostnames. Apply validation to the result in such cases. +- Close directory when done in Serializer DefinitionCache (#100) +- Deleted some asserts to avoid linters from choking (#97) +- Rework Serializer cache behavior to avoid chmod'ing if possible (#32) +- Embedded semicolons in strings in CSS are now handled correctly! +- We accidentally dropped certain Unicode characters if there was + one or more invalid characters. This has been fixed, thanks + to mpyw +- Fix for "Don't truncate upon encountering
              when using DOMLex" + caused a regression with HTML 4.01 Strict parsing with libxml 2.9.1 + (and maybe later versions, but known OK with libxml 2.9.4). The + fix is to go about handling truncation a bit more cleverly so that + we can wrap with divs (sidestepping the bug) but slurping out the + rest of the text in case it ran off the end. (#78) +- Fix PREG_BACKTRACK_LIMIT_ERROR in HTMLPurifier_Filter_ExtractStyle. + Thanks @breathbath for contributing the report and fix (#120) +- Fix entity decoding algorithm to be more conservative about + decoding entities that are missing trailing semicolon. + To get old behavior, set %Core.LegacyEntityDecoder to true. + (#119) +- Workaround libxml bug when HTML tags are embedded inside + script tags. To disable workaround set %Core.AggressivelyRemoveScript + to false. (#83) +# By default, when a link has a target attribute associated + with it, we now also add rel="noopener" in order to + prevent the new window from being able to overwrite + the original frame. To disable this protection, + set %HTML.TargetNoopener to FALSE. + +4.9.0 was cut on Git but never properly released; when we did the +real release we decided to skip this version number. + +4.8.0, released 2016-07-16 +# By default, when a link has a target attribute associated + with it, we now also add rel="noreferrer" in order to + prevent the new window from being able to overwrite + the original frame. To disable this protection, + set %HTML.TargetNoreferrer to FALSE. +! Full PHP 7 compatibility, the test suite is ALL GO. +! %CSS.AllowDuplicates permits duplicate CSS properties. +! Support for 'tel' URIs. +! Partial support for 'border-radius' properties when %CSS.AllowProprietary is true. + The slash syntax, i.e., 'border-radius: 2em 1em 4em / 0.5em 3em' is not + yet supported. +! %Attr.ID.HTML5 turns on HTML5-style ID handling. +- alt truncation could result in malformed UTF-8 sequence. Don't + truncate. Thanks Brandon Farber for reporting. +- Linkify regex is smarter, based off of Gruber's regex. +- IDNA supported natively on PHP 5.3 and later. +- Non all-numeric top-level names (e.g., foo.1f, 1f) are now + allowed. +- Minor bounds error fix to squash a PHP 7 notice. +- Support non-/tmp temporary directories for data:// validation +- Give a better error message when a user attempts to allow + ul/ol without allowing li. +- On some versions of PHP, the Serializer DefinitionCache could + infinite loop when the directory exists but is not listable. (#49) +- Don't match for inside comments with + %Core.ConvertDocumentToFragment. (#67) +- SafeObject is now less case sensitive. (#57) +- AutoFormat.RemoveEmpty.Predicate now correctly renders in + web form. (#85) + +4.7.0, released 2015-08-04 +# opacity is now considered a "tricky" CSS property rather than a + proprietary one. +! %AutoFormat.RemoveEmpty.Predicate for specifying exactly when + an element should be considered "empty" (maybe preserve if it + has attributes), and modify iframe support so that the iframe + is removed if it is missing a src attribute. Thanks meeva for + reporting. +- Don't truncate upon encountering when using DOMLex. Thanks + Myrto Christina for finally convincing me to fix this. +- Update YouTube filter for new code. +- Fix parsing of rgb() values with spaces in them for 'border' + attribute. +- Don't remove foo="" attributes if foo is a boolean attribute. Thanks + valME for reporting. + +4.6.0, released 2013-11-30 +# Secure URI munge hashing algorithm has changed to hash_hmac("sha256", $url, $secret). + Please update any verification scripts you may have. +# URI parsing algorithm was made more strict, so only prefixes which + looks like schemes will actually be schemes. Thanks + Michael Gusev for fixing. +# %Core.EscapeInvalidChildren is no longer supported, and no longer does + anything. +! New directive %Core.AllowHostnameUnderscore which allows underscores + in hostnames. +- Eliminate quadratic behavior in DOMLex by using a proper queue. + Thanks Ole Laursen for noticing this. +- Rewritten MakeWellFormed/FixNesting implementation eliminates quadratic + behavior in the rest of the purificaiton pipeline. Thanks Chedburn + Networks for sponsoring this work. +- Made Linkify URL parser a bit less permissive, so that non-breaking + spaces and commas are not included as part of URL. Thanks nAS for fixing. +- Fix some bad interactions with %HTML.Allowed and injectors. Thanks + David Hirtz for reporting. +- Fix infinite loop in DirectLex. Thanks Ashar Javed (@soaj1664ashar) + for reporting. + +4.5.0, released 2013-02-17 +# Fix bug where stacked attribute transforms clobber each other; + this also means it's no longer possible to override attribute + transforms in later modules. No internal code was using this + but this may break some clients. +# We now use SHA-1 to identify cached definitions, instead of MD5. +! Support display:inline-block +! Support for more white-space CSS values. +! Permit underscores in font families +! Support for page-break-* CSS3 properties when proprietary properties + are enabled. +! New directive %Core.DisableExcludes; can be set to 'true' to turn off + SGML excludes checking. If HTML Purifier is removing too much text + and you don't care about full standards compliance, try setting this to + 'true'. +- Use prepend for SPL autoloading on PHP 5.3 and later. +- Fix bug with nofollow transform when pre-existing rel exists. +- Fix bug where background:url() always gets lower-cased + (but not background-image:url()) +- Fix bug with non lower-case color names in HTML +- Fix bug where data URI validation doesn't remove temporary files. + Thanks Javier Marín Ros for reporting. +- Don't remove certain empty tags on RemoveEmpty. + +4.4.0, released 2012-01-18 +# Removed PEARSax3 handler. +# URI.Munge now munges URIs inside the same host that go from https + to http. Reported by Neike Taika-Tessaro. +# Core.EscapeNonASCIICharacters now always transforms entities to + entities, even if target encoding is UTF-8. +# Tighten up selector validation in ExtractStyleBlocks. + Non-syntactically valid selectors are now rejected, along with + some of the more obscure ones such as attribute selectors, the + :lang pseudoselector, and anything not in CSS2.1. Furthermore, + ID and class selectors now work properly with the relevant + configuration attributes. Also, mute errors when parsing CSS + with CSS Tidy. Reported by Mario Heiderich and Norman Hippert. +! Added support for 'scope' attribute on tables. +! Added %HTML.TargetBlank, which adds target="blank" to all outgoing links. +! Properly handle sub-lists directly nested inside of lists in + a standards compliant way, by moving them into the preceding
            • +! Added %HTML.AllowedComments and %HTML.AllowedCommentsRegexp for + limited allowed comments in untrusted situations. +! Implement iframes, and allow them to be used in untrusted mode with + %HTML.SafeIframe and %URI.SafeIframeRegexp. Thanks Bradley M. Froehle + for submitting an initial version of the patch. +! The Forms module now works properly for transitional doctypes. +! Added support for internationalized domain names. You need the PEAR + Net_IDNA2 module to be in your path; if it is installed, ensure the + class can be loaded and then set %Core.EnableIDNA to true. +- Color keywords are now case insensitive. Thanks Yzmir Ramirez + for reporting. +- Explicitly initialize anonModule variable to null. +- Do not duplicate nofollow if already present. Thanks 178 + for reporting. +- Do not add nofollow if hostname matches our current host. Thanks 178 + for reporting, and Neike Taika-Tessaro for helping diagnose. +- Do not unset parser variable; this fixes intermittent serialization + problems. Thanks Neike Taika-Tessaro for reporting, bill + <10010tiger@gmail.com> for diagnosing. +- Fix iconv truncation bug, where non-UTF-8 target encodings see + output truncated after around 8000 characters. Thanks Jörg Ludwig + for reporting. +- Fix broken table content model for XHTML1.1 (and also earlier + versions, although the W3C validator doesn't catch those violations). + Thanks GlitchMr for reporting. + +4.3.0, released 2011-03-27 +# Fixed broken caching of customized raw definitions, but requires an + API change. The old API still works but will emit a warning, + see http://htmlpurifier.org/docs/enduser-customize.html#optimized + for how to upgrade your code. +# Protect against Internet Explorer innerHTML behavior by specially + treating attributes with backticks but no angled brackets, quotes or + spaces. This constitutes a slight semantic change, which can be + reverted using %Output.FixInnerHTML. Reported by Neike Taika-Tessaro + and Mario Heiderich. +# Protect against cssText/innerHTML by restricting allowed characters + used in fonts further than mandated by the specification and encoding + some extra special characters in URLs. Reported by Neike + Taika-Tessaro and Mario Heiderich. +! Added %HTML.Nofollow to add rel="nofollow" to external links. +! More types of SPL autoloaders allowed on later versions of PHP. +! Implementations for position, top, left, right, bottom, z-index + when %CSS.Trusted is on. +! Add %Cache.SerializerPermissions option for custom serializer + directory/file permissions +! Fix longstanding bug in Flash support for non-IE browsers, and + allow more wmode attributes. +! Add %CSS.AllowedFonts to restrict permissible font names. +- Switch to an iterative traversal of the DOM, which prevents us + from running out of stack space for deeply nested documents. + Thanks Maxim Krizhanovsky for contributing a patch. +- Make removal of conditional IE comments ungreedy; thanks Bernd + for reporting. +- Escape CDATA before removing Internet Explorer comments. +- Fix removal of id attributes under certain conditions by ensuring + armor attributes are preserved when recreating tags. +- Check if schema.ser was corrupted. +- Check if zend.ze1_compatibility_mode is on, and error out if it is. + This safety check is only done for HTMLPurifier.auto.php; if you + are using standalone or the specialized includes files, you're + expected to know what you're doing. +- Stop repeatedly writing the cache file after I'm done customizing a + raw definition. Reported by ajh. +- Switch to using require_once in the Bootstrap to work around bad + interaction with Zend Debugger and APC. Reported by Antonio Parraga. +- Fix URI handling when hostname is missing but scheme is present. + Reported by Neike Taika-Tessaro. +- Fix missing numeric entities on DirectLex; thanks Neike Taika-Tessaro + for reporting. +- Fix harmless notice from indexing into empty string. Thanks Matthijs + Kooijman for reporting. +- Don't autoclose no parent elements are able to support the element + that triggered the autoclose. In particular fixes strange behavior + of stray
            • tags. Thanks pkuliga@gmail.com for reporting and + Neike Taika-Tessaro for debugging assistance. + +4.2.0, released 2010-09-15 +! Added %Core.RemoveProcessingInstructions, which lets you remove + statements. +! Added %URI.DisableResources functionality; the directive originally + did nothing. Thanks David Rothstein for reporting. +! Add documentation about configuration directive types. +! Add %CSS.ForbiddenProperties configuration directive. +! Add %HTML.FlashAllowFullScreen to permit embedded Flash objects + to utilize full-screen mode. +! Add optional support for the file URI scheme, enable + by explicitly setting %URI.AllowedSchemes. +! Add %Core.NormalizeNewlines options to allow turning off newline + normalization. +- Fix improper handling of Internet Explorer conditional comments + by parser. Thanks zmonteca for reporting. +- Fix missing attributes bug when running on Mac Snow Leopard and APC. + Thanks sidepodcast for the fix. +- Warn if an element is allowed, but an attribute it requires is + not allowed. + +4.1.1, released 2010-05-31 +- Fix undefined index warnings in maintenance scripts. +- Fix bug in DirectLex for parsing elements with a single attribute + with entities. +- Rewrite CSS output logic for font-family and url(). Thanks Mario + Heiderich for reporting and Takeshi + Terada for suggesting the fix. +- Emit an error for CollectErrors if a body is extracted +- Fix bug where in background-position for center keyword handling. +- Fix infinite loop when a wrapper element is inserted in a context + where it's not allowed. Thanks Lars for reporting. +- Remove +x bit and shebang from index.php; only supported mode is to + explicitly call it with php. +- Make test script less chatty when log_errors is on. + +4.1.0, released 2010-04-26 +! Support proprietary height attribute on table element +! Support YouTube slideshows that contain /cp/ in their URL. +! Support for data: URI scheme; not enabled by default, add it using + %URI.AllowedSchemes +! Support flashvars when using %HTML.SafeObject and %HTML.SafeEmbed. +! Support for Internet Explorer compatibility with %HTML.SafeObject + using %Output.FlashCompat. +! Handle
                  properly, by inserting the necessary
                1. tag. +- Always quote the insides of url(...) in CSS. + +4.0.0, released 2009-07-07 +# APIs for ConfigSchema subsystem have substantially changed. See + docs/dev-config-bcbreaks.txt for details; in essence, anything that + had both namespace and directive now have a single unified key. +# Some configuration directives were renamed, specifically: + %AutoFormatParam.PurifierLinkifyDocURL -> %AutoFormat.PurifierLinkify.DocURL + %FilterParam.ExtractStyleBlocksEscaping -> %Filter.ExtractStyleBlocks.Escaping + %FilterParam.ExtractStyleBlocksScope -> %Filter.ExtractStyleBlocks.Scope + %FilterParam.ExtractStyleBlocksTidyImpl -> %Filter.ExtractStyleBlocks.TidyImpl + As usual, the old directive names will still work, but will throw E_NOTICE + errors. +# The allowed values for class have been relaxed to allow all of CDATA for + doctypes that are not XHTML 1.1 or XHTML 2.0. For old behavior, set + %Attr.ClassUseCDATA to false. +# Instead of appending the content model to an old content model, a blank + element will replace the old content model. You can use #SUPER to get + the old content model. +! More robust support for name="" and id="" +! HTMLPurifier_Config::inherit($config) allows you to inherit one + configuration, and have changes to that configuration be propagated + to all of its children. +! Implement %HTML.Attr.Name.UseCDATA, which relaxes validation rules on + the name attribute when set. Use with care. Thanks Ian Cook for + sponsoring. +! Implement %AutoFormat.RemoveEmpty.RemoveNbsp, which removes empty + tags that contain non-breaking spaces as well other whitespace. You + can also modify which tags should have   maintained with + %AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions. +! Implement %Attr.AllowedClasses, which allows administrators to restrict + classes users can use to a specified finite set of classes, and + %Attr.ForbiddenClasses, which is the logical inverse. +! You can now maintain your own configuration schema directories by + creating a config-schema.php file or passing an extra argument. Check + docs/dev-config-schema.html for more details. +! Added HTMLPurifier_Config->serialize() method, which lets you save away + your configuration in a compact serial file, which you can unserialize + and use directly without having to go through the overhead of setup. +- Fix bug where URIDefinition would not get cleared if it's directives got + changed. +- Fix fatal error in HTMLPurifier_Encoder on certain platforms (probably NetBSD 5.0) +- Fix bug in Linkify autoformatter involving http://foo +- Make %URI.Munge not apply to links that have the same host as your host. +- Prevent stray tag from truncating output, if a second + is present. +. Created script maintenance/rename-config.php for renaming a configuration + directive while maintaining its alias. This script does not change source code. +. Implement namespace locking for definition construction, to prevent + bugs where a directive is used for definition construction but is not + used to construct the cache hash. + +3.3.0, released 2009-02-16 +! Implement CSS property 'overflow' when %CSS.AllowTricky is true. +! Implement generic property list classess +- Fix bug with testEncodingSupportsASCII() algorithm when iconv() implementation + does not do the "right thing" with characters not supported in the output + set. +- Spellcheck UTF-8: The Secret To Character Encoding +- Fix improper removal of the contents of elements with only whitespace. Thanks + Eric Wald for reporting. +- Fix broken test suite in versions of PHP without spl_autoload_register() +- Fix degenerate case with YouTube filter involving double hyphens. + Thanks Pierre Attar for reporting. +- Fix YouTube rendering problem on certain versions of Firefox. +- Fix CSSDefinition Printer problems with decorators +- Add text parameter to unit tests, forces text output +. Add verbose mode to command line test runner, use (--verbose) +. Turn on unit tests for UnitConverter +. Fix missing version number in configuration %Attr.DefaultImageAlt (added 3.2.0) +. Fix newline errors that caused spurious failures when CRLF HTML Purifier was + tested on Linux. +. Removed trailing whitespace from all text files, see + remote-trailing-whitespace.php maintenance script. +. Convert configuration to use property list backend. + +3.2.0, released 2008-10-31 +# Using %Core.CollectErrors forces line number/column tracking on, whereas + previously you could theoretically turn it off. +# HTMLPurifier_Injector->notifyEnd() is formally deprecated. Please + use handleEnd() instead. +! %Output.AttrSort for when you need your attributes in alphabetical order to + deal with a bug in FCKEditor. Requested by frank farmer. +! Enable HTML comments when %HTML.Trusted is on. Requested by Waldo Jaquith. +! Proper support for name attribute. It is now allowed and equivalent to the id + attribute in a and img tags, and is only converted to id when %HTML.TidyLevel + is heavy (for all doctypes). +! %AutoFormat.RemoveEmpty to remove some empty tags from documents. Please don't + use on hand-written HTML. +! Add error-cases for unsupported elements in MakeWellFormed. This enables + the strategy to be used, standalone, on untrusted input. +! %Core.AggressivelyFixLt is on by default. This causes more sensible + processing of left angled brackets in smileys and other whatnot. +! Test scripts now have a 'type' parameter, which lets you say 'htmlpurifier', + 'phpt', 'vtest', etc. in order to only execute those tests. This supercedes + the --only-phpt parameter, although for backwards-compatibility the flag + will still work. +! AutoParagraph auto-formatter will now preserve double-newlines upon output. + Users who are not performing inbound filtering, this may seem a little + useless, but as a bonus, the test suite and handling of edge cases is also + improved. +! Experimental implementation of forms for %HTML.Trusted +! Track column numbers when maintain line numbers is on +! Proprietary 'background' attribute on table-related elements converted into + corresponding CSS. Thanks Fusemail for sponsoring this feature! +! Add forward(), forwardUntilEndToken(), backward() and current() to Injector + supertype. +! HTMLPurifier_Injector->handleEnd() permits modification to end tokens. The + time of operation varies slightly from notifyEnd() as *all* end tokens are + processed by the injector before they are subject to the well-formedness rules. +! %Attr.DefaultImageAlt allows overriding default behavior of setting alt to + basename of image when not present. +! %AutoFormat.DisplayLinkURI neuters tags into plain text URLs. +- Fix two bugs in %URI.MakeAbsolute; one involving empty paths in base URLs, + the other involving an undefined $is_folder error. +- Throw error when %Core.Encoding is set to a spurious value. Previously, + this errored silently and returned false. +- Redirected stderr to stdout for flush error output. +- %URI.DisableExternal will now use the host in %URI.Base if %URI.Host is not + available. +- Do not re-munge URL if the output URL has the same host as the input URL. + Requested by Chris. +- Fix error in documentation regarding %Filter.ExtractStyleBlocks +- Prevent ]]> from triggering %Core.ConvertDocumentToFragment +- Fix bug with inline elements in blockquotes conflicting with strict doctype +- Detect if HTML support is disabled for DOM by checking for loadHTML() method. +- Fix bug where dots and double-dots in absolute URLs without hostname were + not collapsed by URIFilter_MakeAbsolute. +- Fix bug with anonymous modules operating on SafeEmbed or SafeObject elements + by reordering their addition. +- Will now throw exception on many error conditions during lexer creation; also + throw an exception when MaintainLineNumbers is true, but a non-tracksLineNumbers + is being used. +- Detect if domxml extension is loaded, and use DirectLEx accordingly. +- Improve handling of big numbers with floating point arithmetic in UnitConverter. + Reported by David Morton. +. Strategy_MakeWellFormed now operates in-place, saving memory and allowing + for more interesting filter-backtracking +. New HTMLPurifier_Injector->rewind() functionality, allows injectors to rewind + index to reprocess tokens. +. StringHashParser now allows for multiline sections with "empty" content; + previously the section would remain undefined. +. Added --quick option to multitest.php, which tests only the most recent + release for each series. +. Added --distro option to multitest.php, which accepts either 'normal' or + 'standalone'. This supercedes --exclude-normal and --exclude-standalone + +3.1.1, released 2008-06-19 +# %URI.Munge now, by default, does not munge resources (for example, ) + In order to enable this again, please set %URI.MungeResources to true. +! More robust imagecrash protection with height/width CSS with %CSS.MaxImgLength, + and height/width HTML with %HTML.MaxImgLength. +! %URI.MungeSecretKey for secure URI munging. Thanks Chris + for sponsoring this feature. Check out the corresponding documentation + for details. (Att Nightly testers: The API for this feature changed before + the general release. Namely, rename your directives %URI.SecureMungeSecretKey => + %URI.MungeSecretKey and and %URI.SecureMunge => %URI.Munge) +! Implemented post URI filtering. Set member variable $post to true to set + a URIFilter as such. +! Allow modules to define injectors via $info_injector. Injectors are + automatically disabled if injector's needed elements are not found. +! Support for "safe" objects added, use %HTML.SafeObject and %HTML.SafeEmbed. + Thanks Chris for sponsoring. If you've been using ad hoc code from the + forums, PLEASE use this instead. +! Added substitutions for %e, %n, %a and %p in %URI.Munge (in order, + embedded, tag name, attribute name, CSS property name). See %URI.Munge + for more details. Requested by Jochem Blok. +- Disable percent height/width attributes for img. +- AttrValidator operations are now atomic; updates to attributes are not + manifest in token until end of operations. This prevents naughty internal + code from directly modifying CurrentToken when they're not supposed to. + This semantics change was requested by frank farmer. +- Percent encoding checks enabled for URI query and fragment +- Fix stray backslashes in font-family; CSS Unicode character escapes are + now properly resolved (although *only* in font-family). Thanks Takeshi Terada + for reporting. +- Improve parseCDATA algorithm to take into account newline normalization +- Account for browser confusion between Yen character and backslash in + Shift_JIS encoding. This fix generalizes to any other encoding which is not + a strict superset of printable ASCII. Thanks Takeshi Terada for reporting. +- Fix missing configuration parameter in Generator calls. Thanks vs for the + partial patch. +- Improved adherence to Unicode by checking for non-character codepoints. + Thanks Geoffrey Sneddon for reporting. This may result in degraded + performance for extremely large inputs. +- Allow CSS property-value pair ''text-decoration: none''. Thanks Jochem Blok + for reporting. +. Added HTMLPurifier_UnitConverter and HTMLPurifier_Length for convenient + handling of CSS-style lengths. HTMLPurifier_AttrDef_CSS_Length now uses + this class. +. API of HTMLPurifier_AttrDef_CSS_Length changed from __construct($disable_negative) + to __construct($min, $max). __construct(true) is equivalent to + __construct('0'). +. Added HTMLPurifier_AttrDef_Switch class +. Rename HTMLPurifier_HTMLModule_Tidy->construct() to setup() and bubble method + up inheritance hierarchy to HTMLPurifier_HTMLModule. All HTMLModules + get this called with the configuration object. All modules now + use this rather than __construct(), although legacy code using constructors + will still work--the new format, however, lets modules access the + configuration object for HTML namespace dependant tweaks. +. AttrDef_HTML_Pixels now takes a single construction parameter, pixels. +. ConfigSchema data-structure heavily optimized; on average it uses a third + the memory it did previously. The interface has changed accordingly, + consult changes to HTMLPurifier_Config for details. +. Variable parsing types now are magic integers instead of strings +. Added benchmark for ConfigSchema +. HTMLPurifier_Generator requires $config and $context parameters. If you + don't know what they should be, use HTMLPurifier_Config::createDefault() + and new HTMLPurifier_Context(). +. Printers now properly distinguish between output configuration, and + target configuration. This is not applicable to scripts using + the Printers for HTML Purifier related tasks. +. HTML/CSS Printers must be primed with prepareGenerator($gen_config), otherwise + fatal errors will ensue. +. URIFilter->prepare can return false in order to abort loading of the filter +. Factory for AttrDef_URI implemented, URI#embedded to indicate URI that embeds + an external resource. +. %URI.Munge functionality factored out into a post-filter class. +. Added CurrentCSSProperty context variable during CSS validation + +3.1.0, released 2008-05-18 +# Unnecessary references to objects (vestiges of PHP4) removed from method + signatures. The following methods do not need references when assigning from + them and will result in E_STRICT errors if you try: + + HTMLPurifier_Config->get*Definition() [* = HTML, CSS] + + HTMLPurifier_ConfigSchema::instance() + + HTMLPurifier_DefinitionCacheFactory::instance() + + HTMLPurifier_DefinitionCacheFactory->create() + + HTMLPurifier_DoctypeRegistry->register() + + HTMLPurifier_DoctypeRegistry->get() + + HTMLPurifier_HTMLModule->addElement() + + HTMLPurifier_HTMLModule->addBlankElement() + + HTMLPurifier_LanguageFactory::instance() +# Printer_ConfigForm's get*() functions were static-ified +# %HTML.ForbiddenAttributes requires attribute declarations to be in the + form of tag@attr, NOT tag.attr (which will throw an error and won't do + anything). This is for forwards compatibility with XML; you'd do best + to migrate an %HTML.AllowedAttributes directives to this syntax too. +! Allow index to be false for config from form creation +! Added HTMLPurifier::VERSION constant +! Commas, not dashes, used for serializer IDs. This change is forwards-compatible + and allows for version numbers like "3.1.0-dev". +! %HTML.Allowed deals gracefully with whitespace anywhere, anytime! +! HTML Purifier's URI handling is a lot more robust, with much stricter + validation checks and better percent encoding handling. Thanks Gareth Heyes + for indicating security vulnerabilities from lax percent encoding. +! Bootstrap autoloader deals more robustly with classes that don't exist, + preventing class_exists($class, true) from barfing. +- InterchangeBuilder now alphabetizes its lists +- Validation error in configdoc output fixed +- Iconv and other encoding errors muted even with custom error handlers that + do not honor error_reporting +- Add protection against imagecrash attack with CSS height/width +- HTMLPurifier::instance() created for consistency, is equivalent to getInstance() +- Fixed and revamped broken ConfigForm smoketest +- Bug with bool/null fields in Printer_ConfigForm fixed +- Bug with global forbidden attributes fixed +- Improved error messages for allowed and forbidden HTML elements and attributes +- Missing (or null) in configdoc documentation restored +- If DOM throws and exception during parsing with PH5P (occurs in newer versions + of DOM), HTML Purifier punts to DirectLex +- Fatal error with unserialization of ScriptRequired +- Created directories are now chmod'ed properly +- Fixed bug with fallback languages in LanguageFactory +- Standalone testing setup properly with autoload +. Out-of-date documentation revised +. UTF-8 encoding check optimization as suggested by Diego +. HTMLPurifier_Error removed in favor of exceptions +. More copy() function removed; should use clone instead +. More extensive unit tests for HTMLDefinition +. assertPurification moved to central harness +. HTMLPurifier_Generator accepts $config and $context parameters during + instantiation, not runtime +. Double-quotes outside of attribute values are now unescaped + +3.1.0rc1, released 2008-04-22 +# Autoload support added. Internal require_once's removed in favor of an + explicit require list or autoloading. To use HTML Purifier, + you must now either use HTMLPurifier.auto.php + or HTMLPurifier.includes.php; setting the include path and including + HTMLPurifier.php is insufficient--in such cases include HTMLPurifier.autoload.php + as well to register our autoload handler (or modify your autoload function + to check HTMLPurifier_Bootstrap::getPath($class)). You can also use + HTMLPurifier.safe-includes.php for a less performance friendly but more + user-friendly library load. +# HTMLPurifier_ConfigSchema static functions are officially deprecated. Schema + information is stored in the ConfigSchema directory, and the + maintenance/generate-schema-cache.php generates the schema.ser file, which + is now instantiated. Support for userland schema changes coming soon! +# HTMLPurifier_Config will now throw E_USER_NOTICE when you use a directive + alias; to get rid of these errors just modify your configuration to use + the new directive name. +# HTMLPurifier->addFilter is deprecated; built-in filters can now be + enabled using %Filter.$filter_name or by setting your own filters using + %Filter.Custom +# Directive-level safety properties superceded in favor of module-level + safety. Internal method HTMLModule->addElement() has changed, although + the externally visible HTMLDefinition->addElement has *not* changed. +! Extra utility classes for testing and non-library operations can + be found in extras/. Specifically, these are FSTools and ConfigDoc. + You may find a use for these in your own project, but right now they + are highly experimental and volatile. +! Integration with PHPT allows for automated smoketests +! Limited support for proprietary HTML elements, namely , sponsored + by Chris. You can enable them with %HTML.Proprietary if your client + demands them. +! Support for !important CSS cascade modifier. By default, this will be stripped + from CSS, but you can enable it using %CSS.AllowImportant +! Support for display and visibility CSS properties added, set %CSS.AllowTricky + to true to use them. +! HTML Purifier now has its own Exception hierarchy under HTMLPurifier_Exception. + Developer error (not enduser error) can cause these to be triggered. +! Experimental kses() wrapper introduced with HTMLPurifier.kses.php +! Finally %CSS.AllowedProperties for tweaking allowed CSS properties without + mucking around with HTMLPurifier_CSSDefinition +! ConfigDoc output has been enhanced with version and deprecation info. +! %HTML.ForbiddenAttributes and %HTML.ForbiddenElements implemented. +- Autoclose now operates iteratively, i.e.
                  now has + both span tags closed. +- Various HTMLPurifier_Config convenience functions now accept another parameter + $schema which defines what HTMLPurifier_ConfigSchema to use besides the + global default. +- Fix bug with trusted script handling in libxml versions later than 2.6.28. +- Fix bug in ExtractStyleBlocks with comments in style tags +- Fix bug in comment parsing for DirectLex +- Flush output now displayed when in command line mode for unit tester +- Fix bug with rgb(0, 1, 2) color syntax with spaces inside shorthand syntax +- HTMLPurifier_HTMLDefinition->addAttribute can now be called multiple times + on the same element without emitting errors. +- Fixed fatal error in PH5P lexer with invalid tag names +. Plugins now get their own changelogs according to project conventions. +. Convert tokens to use instanceof, reducing memory footprint and + improving comparison speed. +. Dry runs now supported in SimpleTest; testing facilities improved +. Bootstrap class added for handling autoloading functionality +. Implemented recursive glob at FSTools->globr +. ConfigSchema now has instance methods for all corresponding define* + static methods. +. A couple of new historical maintenance scripts were added. +. HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php split into two files +. tests/index.php can now be run from any directory. +. HTMLPurifier_Token subclasses split into seperate files +. HTMLPURIFIER_PREFIX now is defined in Bootstrap.php, NOT HTMLPurifier.php +. HTMLPURIFIER_PREFIX can now be defined outside of HTML Purifier +. New --php=php flag added, allows PHP executable to be specified (command + line only!) +. htmlpurifier_add_test() preferred method to translate test files in to + classes, because it handles PHPT files too. +. Debugger class is deprecated and will be removed soon. +. Command line argument parsing for testing scripts revamped, now --opt value + format is supported. +. Smoketests now cleanup after magic quotes +. Generator now can output comments (however, comments are still stripped + from HTML Purifier output) +. HTMLPurifier_ConfigSchema->validate() deprecated in favor of + HTMLPurifier_VarParser->parse() +. Integers auto-cast into float type by VarParser. +. HTMLPURIFIER_STRICT removed; no validation is performed on runtime, only + during cache generation +. Reordered script calls in maintenance/flush.php +. Command line scripts now honor exit codes +. When --flush fails in unit testers, abort tests and print message +. Improved documentation in docs/dev-flush.html about the maintenance scripts +. copy() methods removed in favor of clone keyword + +3.0.0, released 2008-01-06 +# HTML Purifier is PHP 5 only! The 2.1.x branch will be maintained + until PHP 4 is completely deprecated, but no new features will be added + to it. + + Visibility declarations added + + Constructor methods renamed to __construct() + + PHP4 reference cruft removed (in progress) +! CSS properties are now case-insensitive +! DefinitionCacheFactory now can register new implementations +! New HTMLPurifier_Filter_ExtractStyleBlocks for extracting Some text'; + + $config = HTMLPurifier_Config::createDefault(); + $config->set('Filter', 'ExtractStyleBlocks', true); + $purifier = new HTMLPurifier($config); + + $html = $purifier->purify($dirty); + + // This implementation writes the stylesheets to the styles/ directory. + // You can also echo the styles inside the document, but it's a bit + // more difficult to make sure they get interpreted properly by + // browsers; try the usual CSS armoring techniques. + $styles = $purifier->context->get('StyleBlocks'); + $dir = 'styles/'; + if (!is_dir($dir)) mkdir($dir); + $hash = sha1($_GET['html']); + foreach ($styles as $i => $style) { + file_put_contents($name = $dir . $hash . "_$i"); + echo ''; + } +?> + + +
                  + +
                  + + +]]> +

                  + Warning: It is possible for a user to mount an + imagecrash attack using this CSS. Counter-measures are difficult; + it is not simply enough to limit the range of CSS lengths (using + relative lengths with many nesting levels allows for large values + to be attained without actually specifying them in the stylesheet), + and the flexible nature of selectors makes it difficult to selectively + disable lengths on image tags (HTML Purifier, however, does disable + CSS width and height in inline styling). There are probably two effective + counter measures: an explicit width and height set to auto in all + images in your document (unlikely) or the disabling of width and + height (somewhat reasonable). Whether or not these measures should be + used is left to the reader. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt new file mode 100644 index 0000000..321eaa2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt @@ -0,0 +1,16 @@ +Filter.YouTube +TYPE: bool +VERSION: 3.1.0 +DEFAULT: false +--DESCRIPTION-- +

                  + Warning: Deprecated in favor of %HTML.SafeObject and + %Output.FlashCompat (turn both on to allow YouTube videos and other + Flash content). +

                  +

                  + This directive enables YouTube video embedding in HTML Purifier. Check + this document + on embedding videos for more information on what this filter does. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt new file mode 100644 index 0000000..0b2c106 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt @@ -0,0 +1,25 @@ +HTML.Allowed +TYPE: itext/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

                  + This is a preferred convenience directive that combines + %HTML.AllowedElements and %HTML.AllowedAttributes. + Specify elements and attributes that are allowed using: + element1[attr1|attr2],element2.... For example, + if you would like to only allow paragraphs and links, specify + a[href],p. You can specify attributes that apply + to all elements using an asterisk, e.g. *[lang]. + You can also use newlines instead of commas to separate elements. +

                  +

                  + Warning: + All of the constraints on the component directives are still enforced. + The syntax is a subset of TinyMCE's valid_elements + whitelist: directly copy-pasting it here will probably result in + broken whitelists. If %HTML.AllowedElements or %HTML.AllowedAttributes + are set, this directive has no effect. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt new file mode 100644 index 0000000..fcf093f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt @@ -0,0 +1,19 @@ +HTML.AllowedAttributes +TYPE: lookup/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- + +

                  + If HTML Purifier's attribute set is unsatisfactory, overload it! + The syntax is "tag.attr" or "*.attr" for the global attributes + (style, id, class, dir, lang, xml:lang). +

                  +

                  + Warning: If another directive conflicts with the + elements here, that directive will win and override. For + example, %HTML.EnableAttrID will take precedence over *.id in this + directive. You must set that directive to true before you can use + IDs at all. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt new file mode 100644 index 0000000..140e214 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt @@ -0,0 +1,10 @@ +HTML.AllowedComments +TYPE: lookup +VERSION: 4.4.0 +DEFAULT: array() +--DESCRIPTION-- +A whitelist which indicates what explicit comment bodies should be +allowed, modulo leading and trailing whitespace. See also %HTML.AllowedCommentsRegexp +(these directives are union'ed together, so a comment is considered +valid if any directive deems it valid.) +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt new file mode 100644 index 0000000..f22e977 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt @@ -0,0 +1,15 @@ +HTML.AllowedCommentsRegexp +TYPE: string/null +VERSION: 4.4.0 +DEFAULT: NULL +--DESCRIPTION-- +A regexp, which if it matches the body of a comment, indicates that +it should be allowed. Trailing and leading spaces are removed prior +to running this regular expression. +Warning: Make sure you specify +correct anchor metacharacters ^regex$, otherwise you may accept +comments that you did not mean to! In particular, the regex /foo|bar/ +is probably not sufficiently strict, since it also allows foobar. +See also %HTML.AllowedComments (these directives are union'ed together, +so a comment is considered valid if any directive deems it valid.) +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt new file mode 100644 index 0000000..1d3fa79 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt @@ -0,0 +1,23 @@ +HTML.AllowedElements +TYPE: lookup/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- +

                  + If HTML Purifier's tag set is unsatisfactory for your needs, you can + overload it with your own list of tags to allow. If you change + this, you probably also want to change %HTML.AllowedAttributes; see + also %HTML.Allowed which lets you set allowed elements and + attributes at the same time. +

                  +

                  + If you attempt to allow an element that HTML Purifier does not know + about, HTML Purifier will raise an error. You will need to manually + tell HTML Purifier about this element by using the + advanced customization features. +

                  +

                  + Warning: If another directive conflicts with the + elements here, that directive will win and override. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt new file mode 100644 index 0000000..5a59a55 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt @@ -0,0 +1,20 @@ +HTML.AllowedModules +TYPE: lookup/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

                  + A doctype comes with a set of usual modules to use. Without having + to mucking about with the doctypes, you can quickly activate or + disable these modules by specifying which modules you wish to allow + with this directive. This is most useful for unit testing specific + modules, although end users may find it useful for their own ends. +

                  +

                  + If you specify a module that does not exist, the manager will silently + fail to use it, so be careful! User-defined modules are not affected + by this directive. Modules defined in %HTML.CoreModules are not + affected by this directive. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt new file mode 100644 index 0000000..151fb7b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt @@ -0,0 +1,11 @@ +HTML.Attr.Name.UseCDATA +TYPE: bool +DEFAULT: false +VERSION: 4.0.0 +--DESCRIPTION-- +The W3C specification DTD defines the name attribute to be CDATA, not ID, due +to limitations of DTD. In certain documents, this relaxed behavior is desired, +whether it is to specify duplicate names, or to specify names that would be +illegal IDs (for example, names that begin with a digit.) Set this configuration +directive to true to use the relaxed parsing rules. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt new file mode 100644 index 0000000..45ae469 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt @@ -0,0 +1,18 @@ +HTML.BlockWrapper +TYPE: string +VERSION: 1.3.0 +DEFAULT: 'p' +--DESCRIPTION-- + +

                  + String name of element to wrap inline elements that are inside a block + context. This only occurs in the children of blockquote in strict mode. +

                  +

                  + Example: by default value, + <blockquote>Foo</blockquote> would become + <blockquote><p>Foo</p></blockquote>. + The <p> tags can be replaced with whatever you desire, + as long as it is a block level element. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt new file mode 100644 index 0000000..5246188 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt @@ -0,0 +1,23 @@ +HTML.CoreModules +TYPE: lookup +VERSION: 2.0.0 +--DEFAULT-- +array ( + 'Structure' => true, + 'Text' => true, + 'Hypertext' => true, + 'List' => true, + 'NonXMLCommonAttributes' => true, + 'XMLCommonAttributes' => true, + 'CommonAttributes' => true, +) +--DESCRIPTION-- + +

                  + Certain modularized doctypes (XHTML, namely), have certain modules + that must be included for the doctype to be an conforming document + type: put those modules here. By default, XHTML's core modules + are used. You can set this to a blank array to disable core module + protection, but this is not recommended. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt new file mode 100644 index 0000000..6ed70b5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt @@ -0,0 +1,9 @@ +HTML.CustomDoctype +TYPE: string/null +VERSION: 2.0.1 +DEFAULT: NULL +--DESCRIPTION-- + +A custom doctype for power-users who defined their own document +type. This directive only applies when %HTML.Doctype is blank. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt new file mode 100644 index 0000000..103db75 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt @@ -0,0 +1,33 @@ +HTML.DefinitionID +TYPE: string/null +DEFAULT: NULL +VERSION: 2.0.0 +--DESCRIPTION-- + +

                  + Unique identifier for a custom-built HTML definition. If you edit + the raw version of the HTMLDefinition, introducing changes that the + configuration object does not reflect, you must specify this variable. + If you change your custom edits, you should change this directive, or + clear your cache. Example: +

                  +
                  +$config = HTMLPurifier_Config::createDefault();
                  +$config->set('HTML', 'DefinitionID', '1');
                  +$def = $config->getHTMLDefinition();
                  +$def->addAttribute('a', 'tabindex', 'Number');
                  +
                  +

                  + In the above example, the configuration is still at the defaults, but + using the advanced API, an extra attribute has been added. The + configuration object normally has no way of knowing that this change + has taken place, so it needs an extra directive: %HTML.DefinitionID. + If someone else attempts to use the default configuration, these two + pieces of code will not clobber each other in the cache, since one has + an extra directive attached to it. +

                  +

                  + You must specify a value to this directive to use the + advanced API features. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt new file mode 100644 index 0000000..229ae02 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt @@ -0,0 +1,16 @@ +HTML.DefinitionRev +TYPE: int +VERSION: 2.0.0 +DEFAULT: 1 +--DESCRIPTION-- + +

                  + Revision identifier for your custom definition specified in + %HTML.DefinitionID. This serves the same purpose: uniquely identifying + your custom definition, but this one does so in a chronological + context: revision 3 is more up-to-date then revision 2. Thus, when + this gets incremented, the cache handling is smart enough to clean + up any older revisions of your definition as well as flush the + cache. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt new file mode 100644 index 0000000..9dab497 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt @@ -0,0 +1,11 @@ +HTML.Doctype +TYPE: string/null +DEFAULT: NULL +--DESCRIPTION-- +Doctype to use during filtering. Technically speaking this is not actually +a doctype (as it does not identify a corresponding DTD), but we are using +this name for sake of simplicity. When non-blank, this will override any +older directives like %HTML.XHTML or %HTML.Strict. +--ALLOWED-- +'HTML 4.01 Transitional', 'HTML 4.01 Strict', 'XHTML 1.0 Transitional', 'XHTML 1.0 Strict', 'XHTML 1.1' +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt new file mode 100644 index 0000000..7878dc0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt @@ -0,0 +1,11 @@ +HTML.FlashAllowFullScreen +TYPE: bool +VERSION: 4.2.0 +DEFAULT: false +--DESCRIPTION-- +

                  + Whether or not to permit embedded Flash content from + %HTML.SafeObject to expand to the full screen. Corresponds to + the allowFullScreen parameter. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt new file mode 100644 index 0000000..57358f9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt @@ -0,0 +1,21 @@ +HTML.ForbiddenAttributes +TYPE: lookup +VERSION: 3.1.0 +DEFAULT: array() +--DESCRIPTION-- +

                  + While this directive is similar to %HTML.AllowedAttributes, for + forwards-compatibility with XML, this attribute has a different syntax. Instead of + tag.attr, use tag@attr. To disallow href + attributes in a tags, set this directive to + a@href. You can also disallow an attribute globally with + attr or *@attr (either syntax is fine; the latter + is provided for consistency with %HTML.AllowedAttributes). +

                  +

                  + Warning: This directive complements %HTML.ForbiddenElements, + accordingly, check + out that directive for a discussion of why you + should think twice before using this directive. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt new file mode 100644 index 0000000..93a53e1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt @@ -0,0 +1,20 @@ +HTML.ForbiddenElements +TYPE: lookup +VERSION: 3.1.0 +DEFAULT: array() +--DESCRIPTION-- +

                  + This was, perhaps, the most requested feature ever in HTML + Purifier. Please don't abuse it! This is the logical inverse of + %HTML.AllowedElements, and it will override that directive, or any + other directive. +

                  +

                  + If possible, %HTML.Allowed is recommended over this directive, because it + can sometimes be difficult to tell whether or not you've forbidden all of + the behavior you would like to disallow. If you forbid img + with the expectation of preventing images on your site, you'll be in for + a nasty surprise when people start using the background-image + CSS property. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt new file mode 100644 index 0000000..e424c38 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt @@ -0,0 +1,14 @@ +HTML.MaxImgLength +TYPE: int/null +DEFAULT: 1200 +VERSION: 3.1.1 +--DESCRIPTION-- +

                  + This directive controls the maximum number of pixels in the width and + height attributes in img tags. This is + in place to prevent imagecrash attacks, disable with null at your own risk. + This directive is similar to %CSS.MaxImgLength, and both should be + concurrently edited, although there are + subtle differences in the input format (the HTML max is an integer). +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt new file mode 100644 index 0000000..700b309 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt @@ -0,0 +1,7 @@ +HTML.Nofollow +TYPE: bool +VERSION: 4.3.0 +DEFAULT: FALSE +--DESCRIPTION-- +If enabled, nofollow rel attributes are added to all outgoing links. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt new file mode 100644 index 0000000..62e8e16 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt @@ -0,0 +1,12 @@ +HTML.Parent +TYPE: string +VERSION: 1.3.0 +DEFAULT: 'div' +--DESCRIPTION-- + +

                  + String name of element that HTML fragment passed to library will be + inserted in. An interesting variation would be using span as the + parent element, meaning that only inline tags would be allowed. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt new file mode 100644 index 0000000..dfb7204 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt @@ -0,0 +1,12 @@ +HTML.Proprietary +TYPE: bool +VERSION: 3.1.0 +DEFAULT: false +--DESCRIPTION-- +

                  + Whether or not to allow proprietary elements and attributes in your + documents, as per HTMLPurifier_HTMLModule_Proprietary. + Warning: This can cause your documents to stop + validating! +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt new file mode 100644 index 0000000..cdda09a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt @@ -0,0 +1,13 @@ +HTML.SafeEmbed +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- +

                  + Whether or not to permit embed tags in documents, with a number of extra + security features added to prevent script execution. This is similar to + what websites like MySpace do to embed tags. Embed is a proprietary + element and will cause your website to stop validating; you should + see if you can use %Output.FlashCompat with %HTML.SafeObject instead + first.

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt new file mode 100644 index 0000000..5eb6ec2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt @@ -0,0 +1,13 @@ +HTML.SafeIframe +TYPE: bool +VERSION: 4.4.0 +DEFAULT: false +--DESCRIPTION-- +

                  + Whether or not to permit iframe tags in untrusted documents. This + directive must be accompanied by a whitelist of permitted iframes, + such as %URI.SafeIframeRegexp, otherwise it will fatally error. + This directive has no effect on strict doctypes, as iframes are not + valid. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt new file mode 100644 index 0000000..ceb342e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt @@ -0,0 +1,13 @@ +HTML.SafeObject +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- +

                  + Whether or not to permit object tags in documents, with a number of extra + security features added to prevent script execution. This is similar to + what websites like MySpace do to object tags. You should also enable + %Output.FlashCompat in order to generate Internet Explorer + compatibility code for your object tags. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt new file mode 100644 index 0000000..5ebc7a1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt @@ -0,0 +1,10 @@ +HTML.SafeScripting +TYPE: lookup +VERSION: 4.5.0 +DEFAULT: array() +--DESCRIPTION-- +

                  + Whether or not to permit script tags to external scripts in documents. + Inline scripting is not allowed, and the script must match an explicit whitelist. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt new file mode 100644 index 0000000..a8b1de5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt @@ -0,0 +1,9 @@ +HTML.Strict +TYPE: bool +VERSION: 1.3.0 +DEFAULT: false +DEPRECATED-VERSION: 1.7.0 +DEPRECATED-USE: HTML.Doctype +--DESCRIPTION-- +Determines whether or not to use Transitional (loose) or Strict rulesets. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt new file mode 100644 index 0000000..587a167 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt @@ -0,0 +1,8 @@ +HTML.TargetBlank +TYPE: bool +VERSION: 4.4.0 +DEFAULT: FALSE +--DESCRIPTION-- +If enabled, target=blank attributes are added to all outgoing links. +(This includes links from an HTTPS version of a page to an HTTP version.) +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt new file mode 100644 index 0000000..dd514c0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt @@ -0,0 +1,10 @@ +--# vim: et sw=4 sts=4 +HTML.TargetNoopener +TYPE: bool +VERSION: 4.8.0 +DEFAULT: TRUE +--DESCRIPTION-- +If enabled, noopener rel attributes are added to links which have +a target attribute associated with them. This prevents malicious +destinations from overwriting the original window. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt new file mode 100644 index 0000000..cb5a0b0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt @@ -0,0 +1,9 @@ +HTML.TargetNoreferrer +TYPE: bool +VERSION: 4.8.0 +DEFAULT: TRUE +--DESCRIPTION-- +If enabled, noreferrer rel attributes are added to links which have +a target attribute associated with them. This prevents malicious +destinations from overwriting the original window. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt new file mode 100644 index 0000000..b4c271b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt @@ -0,0 +1,8 @@ +HTML.TidyAdd +TYPE: lookup +VERSION: 2.0.0 +DEFAULT: array() +--DESCRIPTION-- + +Fixes to add to the default set of Tidy fixes as per your level. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt new file mode 100644 index 0000000..4186ccd --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt @@ -0,0 +1,24 @@ +HTML.TidyLevel +TYPE: string +VERSION: 2.0.0 +DEFAULT: 'medium' +--DESCRIPTION-- + +

                  General level of cleanliness the Tidy module should enforce. +There are four allowed values:

                  +
                  +
                  none
                  +
                  No extra tidying should be done
                  +
                  light
                  +
                  Only fix elements that would be discarded otherwise due to + lack of support in doctype
                  +
                  medium
                  +
                  Enforce best practices
                  +
                  heavy
                  +
                  Transform all deprecated elements and attributes to standards + compliant equivalents
                  +
                  + +--ALLOWED-- +'none', 'light', 'medium', 'heavy' +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt new file mode 100644 index 0000000..996762b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt @@ -0,0 +1,8 @@ +HTML.TidyRemove +TYPE: lookup +VERSION: 2.0.0 +DEFAULT: array() +--DESCRIPTION-- + +Fixes to remove from the default set of Tidy fixes as per your level. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt new file mode 100644 index 0000000..1db9237 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt @@ -0,0 +1,9 @@ +HTML.Trusted +TYPE: bool +VERSION: 2.0.0 +DEFAULT: false +--DESCRIPTION-- +Indicates whether or not the user input is trusted or not. If the input is +trusted, a more expansive set of allowed tags and attributes will be used. +See also %CSS.Trusted. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt new file mode 100644 index 0000000..2a47e38 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt @@ -0,0 +1,11 @@ +HTML.XHTML +TYPE: bool +DEFAULT: true +VERSION: 1.1.0 +DEPRECATED-VERSION: 1.7.0 +DEPRECATED-USE: HTML.Doctype +--DESCRIPTION-- +Determines whether or not output is XHTML 1.0 or HTML 4.01 flavor. +--ALIASES-- +Core.XHTML +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt new file mode 100644 index 0000000..08921fd --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt @@ -0,0 +1,10 @@ +Output.CommentScriptContents +TYPE: bool +VERSION: 2.0.0 +DEFAULT: true +--DESCRIPTION-- +Determines whether or not HTML Purifier should attempt to fix up the +contents of script tags for legacy browsers with comments. +--ALIASES-- +Core.CommentScriptContents +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt new file mode 100644 index 0000000..d6f0d9f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt @@ -0,0 +1,15 @@ +Output.FixInnerHTML +TYPE: bool +VERSION: 4.3.0 +DEFAULT: true +--DESCRIPTION-- +

                  + If true, HTML Purifier will protect against Internet Explorer's + mishandling of the innerHTML attribute by appending + a space to any attribute that does not contain angled brackets, spaces + or quotes, but contains a backtick. This slightly changes the + semantics of any given attribute, so if this is unacceptable and + you do not use innerHTML on any of your pages, you can + turn this directive off. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt new file mode 100644 index 0000000..93398e8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt @@ -0,0 +1,11 @@ +Output.FlashCompat +TYPE: bool +VERSION: 4.1.0 +DEFAULT: false +--DESCRIPTION-- +

                  + If true, HTML Purifier will generate Internet Explorer compatibility + code for all object code. This is highly recommended if you enable + %HTML.SafeObject. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt new file mode 100644 index 0000000..79f8ad8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt @@ -0,0 +1,13 @@ +Output.Newline +TYPE: string/null +VERSION: 2.0.1 +DEFAULT: NULL +--DESCRIPTION-- + +

                  + Newline string to format final output with. If left null, HTML Purifier + will auto-detect the default newline type of the system and use that; + you can manually override it here. Remember, \r\n is Windows, \r + is Mac, and \n is Unix. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt new file mode 100644 index 0000000..232b023 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt @@ -0,0 +1,14 @@ +Output.SortAttr +TYPE: bool +VERSION: 3.2.0 +DEFAULT: false +--DESCRIPTION-- +

                  + If true, HTML Purifier will sort attributes by name before writing them back + to the document, converting a tag like: <el b="" a="" c="" /> + to <el a="" b="" c="" />. This is a workaround for + a bug in FCKeditor which causes it to swap attributes order, adding noise + to text diffs. If you're not seeing this bug, chances are, you don't need + this directive. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt new file mode 100644 index 0000000..06bab00 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt @@ -0,0 +1,25 @@ +Output.TidyFormat +TYPE: bool +VERSION: 1.1.1 +DEFAULT: false +--DESCRIPTION-- +

                  + Determines whether or not to run Tidy on the final output for pretty + formatting reasons, such as indentation and wrap. +

                  +

                  + This can greatly improve readability for editors who are hand-editing + the HTML, but is by no means necessary as HTML Purifier has already + fixed all major errors the HTML may have had. Tidy is a non-default + extension, and this directive will silently fail if Tidy is not + available. +

                  +

                  + If you are looking to make the overall look of your page's source + better, I recommend running Tidy on the entire page rather than just + user-content (after all, the indentation relative to the containing + blocks will be incorrect). +

                  +--ALIASES-- +Core.TidyFormat +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt new file mode 100644 index 0000000..071bc02 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt @@ -0,0 +1,7 @@ +Test.ForceNoIconv +TYPE: bool +DEFAULT: false +--DESCRIPTION-- +When set to true, HTMLPurifier_Encoder will act as if iconv does not exist +and use only pure PHP implementations. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt new file mode 100644 index 0000000..eb97307 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt @@ -0,0 +1,18 @@ +URI.AllowedSchemes +TYPE: lookup +--DEFAULT-- +array ( + 'http' => true, + 'https' => true, + 'mailto' => true, + 'ftp' => true, + 'nntp' => true, + 'news' => true, + 'tel' => true, +) +--DESCRIPTION-- +Whitelist that defines the schemes that a URI is allowed to have. This +prevents XSS attacks from using pseudo-schemes like javascript or mocha. +There is also support for the data and file +URI schemes, but they are not enabled by default. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Base.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Base.txt new file mode 100644 index 0000000..876f068 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Base.txt @@ -0,0 +1,17 @@ +URI.Base +TYPE: string/null +VERSION: 2.1.0 +DEFAULT: NULL +--DESCRIPTION-- + +

                  + The base URI is the URI of the document this purified HTML will be + inserted into. This information is important if HTML Purifier needs + to calculate absolute URIs from relative URIs, such as when %URI.MakeAbsolute + is on. You may use a non-absolute URI for this value, but behavior + may vary (%URI.MakeAbsolute deals nicely with both absolute and + relative paths, but forwards-compatibility is not guaranteed). + Warning: If set, the scheme on this URI + overrides the one specified by %URI.DefaultScheme. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt new file mode 100644 index 0000000..834bc08 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt @@ -0,0 +1,15 @@ +URI.DefaultScheme +TYPE: string/null +DEFAULT: 'http' +--DESCRIPTION-- + +

                  + Defines through what scheme the output will be served, in order to + select the proper object validator when no scheme information is present. +

                  + +

                  + Starting with HTML Purifier 4.9.0, the default scheme can be null, in + which case we reject all URIs which do not have explicit schemes. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt new file mode 100644 index 0000000..f05312b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt @@ -0,0 +1,11 @@ +URI.DefinitionID +TYPE: string/null +VERSION: 2.1.0 +DEFAULT: NULL +--DESCRIPTION-- + +

                  + Unique identifier for a custom-built URI definition. If you want + to add custom URIFilters, you must specify this value. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt new file mode 100644 index 0000000..80cfea9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt @@ -0,0 +1,11 @@ +URI.DefinitionRev +TYPE: int +VERSION: 2.1.0 +DEFAULT: 1 +--DESCRIPTION-- + +

                  + Revision identifier for your custom definition. See + %HTML.DefinitionRev for details. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt new file mode 100644 index 0000000..71ce025 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt @@ -0,0 +1,14 @@ +URI.Disable +TYPE: bool +VERSION: 1.3.0 +DEFAULT: false +--DESCRIPTION-- + +

                  + Disables all URIs in all forms. Not sure why you'd want to do that + (after all, the Internet's founded on the notion of a hyperlink). +

                  + +--ALIASES-- +Attr.DisableURI +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt new file mode 100644 index 0000000..13c122c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt @@ -0,0 +1,11 @@ +URI.DisableExternal +TYPE: bool +VERSION: 1.2.0 +DEFAULT: false +--DESCRIPTION-- +Disables links to external websites. This is a highly effective anti-spam +and anti-pagerank-leech measure, but comes at a hefty price: nolinks or +images outside of your domain will be allowed. Non-linkified URIs will +still be preserved. If you want to be able to link to subdomains or use +absolute URIs, specify %URI.Host for your website. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt new file mode 100644 index 0000000..abcc1ef --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt @@ -0,0 +1,13 @@ +URI.DisableExternalResources +TYPE: bool +VERSION: 1.3.0 +DEFAULT: false +--DESCRIPTION-- +Disables the embedding of external resources, preventing users from +embedding things like images from other hosts. This prevents access +tracking (good for email viewers), bandwidth leeching, cross-site request +forging, goatse.cx posting, and other nasties, but also results in a loss +of end-user functionality (they can't directly post a pic they posted from +Flickr anymore). Use it if you don't have a robust user-content moderation +team. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt new file mode 100644 index 0000000..f891de4 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt @@ -0,0 +1,15 @@ +URI.DisableResources +TYPE: bool +VERSION: 4.2.0 +DEFAULT: false +--DESCRIPTION-- +

                  + Disables embedding resources, essentially meaning no pictures. You can + still link to them though. See %URI.DisableExternalResources for why + this might be a good idea. +

                  +

                  + Note: While this directive has been available since 1.3.0, + it didn't actually start doing anything until 4.2.0. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Host.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Host.txt new file mode 100644 index 0000000..ee83b12 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Host.txt @@ -0,0 +1,19 @@ +URI.Host +TYPE: string/null +VERSION: 1.2.0 +DEFAULT: NULL +--DESCRIPTION-- + +

                  + Defines the domain name of the server, so we can determine whether or + an absolute URI is from your website or not. Not strictly necessary, + as users should be using relative URIs to reference resources on your + website. It will, however, let you use absolute URIs to link to + subdomains of the domain you post here: i.e. example.com will allow + sub.example.com. However, higher up domains will still be excluded: + if you set %URI.Host to sub.example.com, example.com will be blocked. + Note: This directive overrides %URI.Base because + a given page may be on a sub-domain, but you wish HTML Purifier to be + more relaxed and allow some of the parent domains too. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt new file mode 100644 index 0000000..0b6df76 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt @@ -0,0 +1,9 @@ +URI.HostBlacklist +TYPE: list +VERSION: 1.3.0 +DEFAULT: array() +--DESCRIPTION-- +List of strings that are forbidden in the host of any URI. Use it to kill +domain names of spam, etc. Note that it will catch anything in the domain, +so moo.com will catch moo.com.example.com. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt new file mode 100644 index 0000000..4214900 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt @@ -0,0 +1,13 @@ +URI.MakeAbsolute +TYPE: bool +VERSION: 2.1.0 +DEFAULT: false +--DESCRIPTION-- + +

                  + Converts all URIs into absolute forms. This is useful when the HTML + being filtered assumes a specific base path, but will actually be + viewed in a different context (and setting an alternate base URI is + not possible). %URI.Base must be set for this directive to work. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt new file mode 100644 index 0000000..58c81dc --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt @@ -0,0 +1,83 @@ +URI.Munge +TYPE: string/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- + +

                  + Munges all browsable (usually http, https and ftp) + absolute URIs into another URI, usually a URI redirection service. + This directive accepts a URI, formatted with a %s where + the url-encoded original URI should be inserted (sample: + http://www.google.com/url?q=%s). +

                  +

                  + Uses for this directive: +

                  +
                    +
                  • + Prevent PageRank leaks, while being fairly transparent + to users (you may also want to add some client side JavaScript to + override the text in the statusbar). Notice: + Many security experts believe that this form of protection does not deter spam-bots. +
                  • +
                  • + Redirect users to a splash page telling them they are leaving your + website. While this is poor usability practice, it is often mandated + in corporate environments. +
                  • +
                  +

                  + Prior to HTML Purifier 3.1.1, this directive also enabled the munging + of browsable external resources, which could break things if your redirection + script was a splash page or used meta tags. To revert to + previous behavior, please use %URI.MungeResources. +

                  +

                  + You may want to also use %URI.MungeSecretKey along with this directive + in order to enforce what URIs your redirector script allows. Open + redirector scripts can be a security risk and negatively affect the + reputation of your domain name. +

                  +

                  + Starting with HTML Purifier 3.1.1, there is also these substitutions: +

                  + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                  KeyDescriptionExample <a href="">
                  %r1 - The URI embeds a resource
                  (blank) - The URI is merely a link
                  %nThe name of the tag this URI came froma
                  %mThe name of the attribute this URI came fromhref
                  %pThe name of the CSS property this URI came from, or blank if irrelevant
                  +

                  + Admittedly, these letters are somewhat arbitrary; the only stipulation + was that they couldn't be a through f. r is for resource (I would have preferred + e, but you take what you can get), n is for name, m + was picked because it came after n (and I couldn't use a), p is for + property. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt new file mode 100644 index 0000000..6fce0fd --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt @@ -0,0 +1,17 @@ +URI.MungeResources +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- +

                  + If true, any URI munging directives like %URI.Munge + will also apply to embedded resources, such as <img src="">. + Be careful enabling this directive if you have a redirector script + that does not use the Location HTTP header; all of your images + and other embedded resources will break. +

                  +

                  + Warning: It is strongly advised you use this in conjunction + %URI.MungeSecretKey to mitigate the security risk of an open redirector. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt new file mode 100644 index 0000000..1e17c1d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt @@ -0,0 +1,30 @@ +URI.MungeSecretKey +TYPE: string/null +VERSION: 3.1.1 +DEFAULT: NULL +--DESCRIPTION-- +

                  + This directive enables secure checksum generation along with %URI.Munge. + It should be set to a secure key that is not shared with anyone else. + The checksum can be placed in the URI using %t. Use of this checksum + affords an additional level of protection by allowing a redirector + to check if a URI has passed through HTML Purifier with this line: +

                  + +
                  $checksum === hash_hmac("sha256", $url, $secret_key)
                  + +

                  + If the output is TRUE, the redirector script should accept the URI. +

                  + +

                  + Please note that it would still be possible for an attacker to procure + secure hashes en-mass by abusing your website's Preview feature or the + like, but this service affords an additional level of protection + that should be combined with website blacklisting. +

                  + +

                  + Remember this has no effect if %URI.Munge is not on. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt new file mode 100644 index 0000000..23331a4 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt @@ -0,0 +1,9 @@ +URI.OverrideAllowedSchemes +TYPE: bool +DEFAULT: true +--DESCRIPTION-- +If this is set to true (which it is by default), you can override +%URI.AllowedSchemes by simply registering a HTMLPurifier_URIScheme to the +registry. If false, you will also have to update that directive in order +to add more schemes. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt new file mode 100644 index 0000000..7908483 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt @@ -0,0 +1,22 @@ +URI.SafeIframeRegexp +TYPE: string/null +VERSION: 4.4.0 +DEFAULT: NULL +--DESCRIPTION-- +

                  + A PCRE regular expression that will be matched against an iframe URI. This is + a relatively inflexible scheme, but works well enough for the most common + use-case of iframes: embedded video. This directive only has an effect if + %HTML.SafeIframe is enabled. Here are some example values: +

                  +
                    +
                  • %^http://www.youtube.com/embed/% - Allow YouTube videos
                  • +
                  • %^http://player.vimeo.com/video/% - Allow Vimeo videos
                  • +
                  • %^http://(www.youtube.com/embed/|player.vimeo.com/video/)% - Allow both
                  • +
                  +

                  + Note that this directive does not give you enough granularity to, say, disable + all autoplay videos. Pipe up on the HTML Purifier forums if this + is a capability you want. +

                  +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/info.ini b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/info.ini new file mode 100644 index 0000000..5de4505 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/info.ini @@ -0,0 +1,3 @@ +name = "HTML Purifier" + +; vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ContentSets.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ContentSets.php new file mode 100644 index 0000000..543e3f8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ContentSets.php @@ -0,0 +1,170 @@ + true) indexed by name. + * @type array + * @note This is in HTMLPurifier_HTMLDefinition->info_content_sets + */ + public $lookup = array(); + + /** + * Synchronized list of defined content sets (keys of info). + * @type array + */ + protected $keys = array(); + /** + * Synchronized list of defined content values (values of info). + * @type array + */ + protected $values = array(); + + /** + * Merges in module's content sets, expands identifiers in the content + * sets and populates the keys, values and lookup member variables. + * @param HTMLPurifier_HTMLModule[] $modules List of HTMLPurifier_HTMLModule + */ + public function __construct($modules) + { + if (!is_array($modules)) { + $modules = array($modules); + } + // populate content_sets based on module hints + // sorry, no way of overloading + foreach ($modules as $module) { + foreach ($module->content_sets as $key => $value) { + $temp = $this->convertToLookup($value); + if (isset($this->lookup[$key])) { + // add it into the existing content set + $this->lookup[$key] = array_merge($this->lookup[$key], $temp); + } else { + $this->lookup[$key] = $temp; + } + } + } + $old_lookup = false; + while ($old_lookup !== $this->lookup) { + $old_lookup = $this->lookup; + foreach ($this->lookup as $i => $set) { + $add = array(); + foreach ($set as $element => $x) { + if (isset($this->lookup[$element])) { + $add += $this->lookup[$element]; + unset($this->lookup[$i][$element]); + } + } + $this->lookup[$i] += $add; + } + } + + foreach ($this->lookup as $key => $lookup) { + $this->info[$key] = implode(' | ', array_keys($lookup)); + } + $this->keys = array_keys($this->info); + $this->values = array_values($this->info); + } + + /** + * Accepts a definition; generates and assigns a ChildDef for it + * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef reference + * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef + */ + public function generateChildDef(&$def, $module) + { + if (!empty($def->child)) { // already done! + return; + } + $content_model = $def->content_model; + if (is_string($content_model)) { + // Assume that $this->keys is alphanumeric + $def->content_model = preg_replace_callback( + '/\b(' . implode('|', $this->keys) . ')\b/', + array($this, 'generateChildDefCallback'), + $content_model + ); + //$def->content_model = str_replace( + // $this->keys, $this->values, $content_model); + } + $def->child = $this->getChildDef($def, $module); + } + + public function generateChildDefCallback($matches) + { + return $this->info[$matches[0]]; + } + + /** + * Instantiates a ChildDef based on content_model and content_model_type + * member variables in HTMLPurifier_ElementDef + * @note This will also defer to modules for custom HTMLPurifier_ChildDef + * subclasses that need content set expansion + * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef to have ChildDef extracted + * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef + * @return HTMLPurifier_ChildDef corresponding to ElementDef + */ + public function getChildDef($def, $module) + { + $value = $def->content_model; + if (is_object($value)) { + trigger_error( + 'Literal object child definitions should be stored in '. + 'ElementDef->child not ElementDef->content_model', + E_USER_NOTICE + ); + return $value; + } + switch ($def->content_model_type) { + case 'required': + return new HTMLPurifier_ChildDef_Required($value); + case 'optional': + return new HTMLPurifier_ChildDef_Optional($value); + case 'empty': + return new HTMLPurifier_ChildDef_Empty(); + case 'custom': + return new HTMLPurifier_ChildDef_Custom($value); + } + // defer to its module + $return = false; + if ($module->defines_child_def) { // save a func call + $return = $module->getChildDef($def); + } + if ($return !== false) { + return $return; + } + // error-out + trigger_error( + 'Could not determine which ChildDef class to instantiate', + E_USER_ERROR + ); + return false; + } + + /** + * Converts a string list of elements separated by pipes into + * a lookup array. + * @param string $string List of elements + * @return array Lookup array of elements + */ + protected function convertToLookup($string) + { + $array = explode('|', str_replace(' ', '', $string)); + $ret = array(); + foreach ($array as $k) { + $ret[$k] = true; + } + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Context.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Context.php new file mode 100644 index 0000000..00e509c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Context.php @@ -0,0 +1,95 @@ +_storage)) { + trigger_error( + "Name $name produces collision, cannot re-register", + E_USER_ERROR + ); + return; + } + $this->_storage[$name] =& $ref; + } + + /** + * Retrieves a variable reference from the context. + * @param string $name String name + * @param bool $ignore_error Boolean whether or not to ignore error + * @return mixed + */ + public function &get($name, $ignore_error = false) + { + if (!array_key_exists($name, $this->_storage)) { + if (!$ignore_error) { + trigger_error( + "Attempted to retrieve non-existent variable $name", + E_USER_ERROR + ); + } + $var = null; // so we can return by reference + return $var; + } + return $this->_storage[$name]; + } + + /** + * Destroys a variable in the context. + * @param string $name String name + */ + public function destroy($name) + { + if (!array_key_exists($name, $this->_storage)) { + trigger_error( + "Attempted to destroy non-existent variable $name", + E_USER_ERROR + ); + return; + } + unset($this->_storage[$name]); + } + + /** + * Checks whether or not the variable exists. + * @param string $name String name + * @return bool + */ + public function exists($name) + { + return array_key_exists($name, $this->_storage); + } + + /** + * Loads a series of variables from an associative array + * @param array $context_array Assoc array of variables to load + */ + public function loadArray($context_array) + { + foreach ($context_array as $key => $discard) { + $this->register($key, $context_array[$key]); + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Definition.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Definition.php new file mode 100644 index 0000000..bc6d433 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Definition.php @@ -0,0 +1,55 @@ +setup) { + return; + } + $this->setup = true; + $this->doSetup($config); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache.php new file mode 100644 index 0000000..9aa8ff3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache.php @@ -0,0 +1,129 @@ +type = $type; + } + + /** + * Generates a unique identifier for a particular configuration + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config + * @return string + */ + public function generateKey($config) + { + return $config->version . ',' . // possibly replace with function calls + $config->getBatchSerial($this->type) . ',' . + $config->get($this->type . '.DefinitionRev'); + } + + /** + * Tests whether or not a key is old with respect to the configuration's + * version and revision number. + * @param string $key Key to test + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config to test against + * @return bool + */ + public function isOld($key, $config) + { + if (substr_count($key, ',') < 2) { + return true; + } + list($version, $hash, $revision) = explode(',', $key, 3); + $compare = version_compare($version, $config->version); + // version mismatch, is always old + if ($compare != 0) { + return true; + } + // versions match, ids match, check revision number + if ($hash == $config->getBatchSerial($this->type) && + $revision < $config->get($this->type . '.DefinitionRev')) { + return true; + } + return false; + } + + /** + * Checks if a definition's type jives with the cache's type + * @note Throws an error on failure + * @param HTMLPurifier_Definition $def Definition object to check + * @return bool true if good, false if not + */ + public function checkDefType($def) + { + if ($def->type !== $this->type) { + trigger_error("Cannot use definition of type {$def->type} in cache for {$this->type}"); + return false; + } + return true; + } + + /** + * Adds a definition object to the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function add($def, $config); + + /** + * Unconditionally saves a definition object to the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function set($def, $config); + + /** + * Replace an object in the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function replace($def, $config); + + /** + * Retrieves a definition object from the cache + * @param HTMLPurifier_Config $config + */ + abstract public function get($config); + + /** + * Removes a definition object to the cache + * @param HTMLPurifier_Config $config + */ + abstract public function remove($config); + + /** + * Clears all objects from cache + * @param HTMLPurifier_Config $config + */ + abstract public function flush($config); + + /** + * Clears all expired (older version or revision) objects from cache + * @note Be careful implementing this method as flush. Flush must + * not interfere with other Definition types, and cleanup() + * should not be repeatedly called by userland code. + * @param HTMLPurifier_Config $config + */ + abstract public function cleanup($config); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator.php new file mode 100644 index 0000000..b57a51b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator.php @@ -0,0 +1,112 @@ +copy(); + // reference is necessary for mocks in PHP 4 + $decorator->cache =& $cache; + $decorator->type = $cache->type; + return $decorator; + } + + /** + * Cross-compatible clone substitute + * @return HTMLPurifier_DefinitionCache_Decorator + */ + public function copy() + { + return new HTMLPurifier_DefinitionCache_Decorator(); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function add($def, $config) + { + return $this->cache->add($def, $config); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function set($def, $config) + { + return $this->cache->set($def, $config); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function replace($def, $config) + { + return $this->cache->replace($def, $config); + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function get($config) + { + return $this->cache->get($config); + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function remove($config) + { + return $this->cache->remove($config); + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function flush($config) + { + return $this->cache->flush($config); + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function cleanup($config) + { + return $this->cache->cleanup($config); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php new file mode 100644 index 0000000..4991777 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php @@ -0,0 +1,78 @@ +definitions[$this->generateKey($config)] = $def; + } + return $status; + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function set($def, $config) + { + $status = parent::set($def, $config); + if ($status) { + $this->definitions[$this->generateKey($config)] = $def; + } + return $status; + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function replace($def, $config) + { + $status = parent::replace($def, $config); + if ($status) { + $this->definitions[$this->generateKey($config)] = $def; + } + return $status; + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function get($config) + { + $key = $this->generateKey($config); + if (isset($this->definitions[$key])) { + return $this->definitions[$key]; + } + $this->definitions[$key] = parent::get($config); + return $this->definitions[$key]; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Template.php.in b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Template.php.in new file mode 100644 index 0000000..b1fec8d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Template.php.in @@ -0,0 +1,82 @@ +checkDefType($def)) { + return; + } + $file = $this->generateFilePath($config); + if (file_exists($file)) { + return false; + } + if (!$this->_prepareDir($config)) { + return false; + } + return $this->_write($file, serialize($def), $config); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return int|bool + */ + public function set($def, $config) + { + if (!$this->checkDefType($def)) { + return; + } + $file = $this->generateFilePath($config); + if (!$this->_prepareDir($config)) { + return false; + } + return $this->_write($file, serialize($def), $config); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return int|bool + */ + public function replace($def, $config) + { + if (!$this->checkDefType($def)) { + return; + } + $file = $this->generateFilePath($config); + if (!file_exists($file)) { + return false; + } + if (!$this->_prepareDir($config)) { + return false; + } + return $this->_write($file, serialize($def), $config); + } + + /** + * @param HTMLPurifier_Config $config + * @return bool|HTMLPurifier_Config + */ + public function get($config) + { + $file = $this->generateFilePath($config); + if (!file_exists($file)) { + return false; + } + return unserialize(file_get_contents($file)); + } + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function remove($config) + { + $file = $this->generateFilePath($config); + if (!file_exists($file)) { + return false; + } + return unlink($file); + } + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function flush($config) + { + if (!$this->_prepareDir($config)) { + return false; + } + $dir = $this->generateDirectoryPath($config); + $dh = opendir($dir); + // Apparently, on some versions of PHP, readdir will return + // an empty string if you pass an invalid argument to readdir. + // So you need this test. See #49. + if (false === $dh) { + return false; + } + while (false !== ($filename = readdir($dh))) { + if (empty($filename)) { + continue; + } + if ($filename[0] === '.') { + continue; + } + unlink($dir . '/' . $filename); + } + closedir($dh); + return true; + } + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function cleanup($config) + { + if (!$this->_prepareDir($config)) { + return false; + } + $dir = $this->generateDirectoryPath($config); + $dh = opendir($dir); + // See #49 (and above). + if (false === $dh) { + return false; + } + while (false !== ($filename = readdir($dh))) { + if (empty($filename)) { + continue; + } + if ($filename[0] === '.') { + continue; + } + $key = substr($filename, 0, strlen($filename) - 4); + if ($this->isOld($key, $config)) { + unlink($dir . '/' . $filename); + } + } + closedir($dh); + return true; + } + + /** + * Generates the file path to the serial file corresponding to + * the configuration and definition name + * @param HTMLPurifier_Config $config + * @return string + * @todo Make protected + */ + public function generateFilePath($config) + { + $key = $this->generateKey($config); + return $this->generateDirectoryPath($config) . '/' . $key . '.ser'; + } + + /** + * Generates the path to the directory contain this cache's serial files + * @param HTMLPurifier_Config $config + * @return string + * @note No trailing slash + * @todo Make protected + */ + public function generateDirectoryPath($config) + { + $base = $this->generateBaseDirectoryPath($config); + return $base . '/' . $this->type; + } + + /** + * Generates path to base directory that contains all definition type + * serials + * @param HTMLPurifier_Config $config + * @return mixed|string + * @todo Make protected + */ + public function generateBaseDirectoryPath($config) + { + $base = $config->get('Cache.SerializerPath'); + $base = is_null($base) ? HTMLPURIFIER_PREFIX . '/HTMLPurifier/DefinitionCache/Serializer' : $base; + return $base; + } + + /** + * Convenience wrapper function for file_put_contents + * @param string $file File name to write to + * @param string $data Data to write into file + * @param HTMLPurifier_Config $config + * @return int|bool Number of bytes written if success, or false if failure. + */ + private function _write($file, $data, $config) + { + $result = file_put_contents($file, $data); + if ($result !== false) { + // set permissions of the new file (no execute) + $chmod = $config->get('Cache.SerializerPermissions'); + if ($chmod !== null) { + chmod($file, $chmod & 0666); + } + } + return $result; + } + + /** + * Prepares the directory that this type stores the serials in + * @param HTMLPurifier_Config $config + * @return bool True if successful + */ + private function _prepareDir($config) + { + $directory = $this->generateDirectoryPath($config); + $chmod = $config->get('Cache.SerializerPermissions'); + if ($chmod === null) { + if (!@mkdir($directory) && !is_dir($directory)) { + trigger_error( + 'Could not create directory ' . $directory . '', + E_USER_WARNING + ); + return false; + } + return true; + } + if (!is_dir($directory)) { + $base = $this->generateBaseDirectoryPath($config); + if (!is_dir($base)) { + trigger_error( + 'Base directory ' . $base . ' does not exist, + please create or change using %Cache.SerializerPath', + E_USER_WARNING + ); + return false; + } elseif (!$this->_testPermissions($base, $chmod)) { + return false; + } + if (!@mkdir($directory, $chmod) && !is_dir($directory)) { + trigger_error( + 'Could not create directory ' . $directory . '', + E_USER_WARNING + ); + return false; + } + if (!$this->_testPermissions($directory, $chmod)) { + return false; + } + } elseif (!$this->_testPermissions($directory, $chmod)) { + return false; + } + return true; + } + + /** + * Tests permissions on a directory and throws out friendly + * error messages and attempts to chmod it itself if possible + * @param string $dir Directory path + * @param int $chmod Permissions + * @return bool True if directory is writable + */ + private function _testPermissions($dir, $chmod) + { + // early abort, if it is writable, everything is hunky-dory + if (is_writable($dir)) { + return true; + } + if (!is_dir($dir)) { + // generally, you'll want to handle this beforehand + // so a more specific error message can be given + trigger_error( + 'Directory ' . $dir . ' does not exist', + E_USER_WARNING + ); + return false; + } + if (function_exists('posix_getuid') && $chmod !== null) { + // POSIX system, we can give more specific advice + if (fileowner($dir) === posix_getuid()) { + // we can chmod it ourselves + $chmod = $chmod | 0700; + if (chmod($dir, $chmod)) { + return true; + } + } elseif (filegroup($dir) === posix_getgid()) { + $chmod = $chmod | 0070; + } else { + // PHP's probably running as nobody, so we'll + // need to give global permissions + $chmod = $chmod | 0777; + } + trigger_error( + 'Directory ' . $dir . ' not writable, ' . + 'please chmod to ' . decoct($chmod), + E_USER_WARNING + ); + } else { + // generic error message + trigger_error( + 'Directory ' . $dir . ' not writable, ' . + 'please alter file permissions', + E_USER_WARNING + ); + } + return false; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer/README b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer/README new file mode 100644 index 0000000..2e35c1c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer/README @@ -0,0 +1,3 @@ +This is a dummy file to prevent Git from ignoring this empty directory. + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCacheFactory.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCacheFactory.php new file mode 100644 index 0000000..fd1cc9b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCacheFactory.php @@ -0,0 +1,106 @@ + array()); + + /** + * @type array + */ + protected $implementations = array(); + + /** + * @type HTMLPurifier_DefinitionCache_Decorator[] + */ + protected $decorators = array(); + + /** + * Initialize default decorators + */ + public function setup() + { + $this->addDecorator('Cleanup'); + } + + /** + * Retrieves an instance of global definition cache factory. + * @param HTMLPurifier_DefinitionCacheFactory $prototype + * @return HTMLPurifier_DefinitionCacheFactory + */ + public static function instance($prototype = null) + { + static $instance; + if ($prototype !== null) { + $instance = $prototype; + } elseif ($instance === null || $prototype === true) { + $instance = new HTMLPurifier_DefinitionCacheFactory(); + $instance->setup(); + } + return $instance; + } + + /** + * Registers a new definition cache object + * @param string $short Short name of cache object, for reference + * @param string $long Full class name of cache object, for construction + */ + public function register($short, $long) + { + $this->implementations[$short] = $long; + } + + /** + * Factory method that creates a cache object based on configuration + * @param string $type Name of definitions handled by cache + * @param HTMLPurifier_Config $config Config instance + * @return mixed + */ + public function create($type, $config) + { + $method = $config->get('Cache.DefinitionImpl'); + if ($method === null) { + return new HTMLPurifier_DefinitionCache_Null($type); + } + if (!empty($this->caches[$method][$type])) { + return $this->caches[$method][$type]; + } + if (isset($this->implementations[$method]) && + class_exists($class = $this->implementations[$method], false)) { + $cache = new $class($type); + } else { + if ($method != 'Serializer') { + trigger_error("Unrecognized DefinitionCache $method, using Serializer instead", E_USER_WARNING); + } + $cache = new HTMLPurifier_DefinitionCache_Serializer($type); + } + foreach ($this->decorators as $decorator) { + $new_cache = $decorator->decorate($cache); + // prevent infinite recursion in PHP 4 + unset($cache); + $cache = $new_cache; + } + $this->caches[$method][$type] = $cache; + return $this->caches[$method][$type]; + } + + /** + * Registers a decorator to add to all new cache objects + * @param HTMLPurifier_DefinitionCache_Decorator|string $decorator An instance or the name of a decorator + */ + public function addDecorator($decorator) + { + if (is_string($decorator)) { + $class = "HTMLPurifier_DefinitionCache_Decorator_$decorator"; + $decorator = new $class; + } + $this->decorators[$decorator->name] = $decorator; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Doctype.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Doctype.php new file mode 100644 index 0000000..4acd06e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Doctype.php @@ -0,0 +1,73 @@ +renderDoctype. + * If structure changes, please update that function. + */ +class HTMLPurifier_Doctype +{ + /** + * Full name of doctype + * @type string + */ + public $name; + + /** + * List of standard modules (string identifiers or literal objects) + * that this doctype uses + * @type array + */ + public $modules = array(); + + /** + * List of modules to use for tidying up code + * @type array + */ + public $tidyModules = array(); + + /** + * Is the language derived from XML (i.e. XHTML)? + * @type bool + */ + public $xml = true; + + /** + * List of aliases for this doctype + * @type array + */ + public $aliases = array(); + + /** + * Public DTD identifier + * @type string + */ + public $dtdPublic; + + /** + * System DTD identifier + * @type string + */ + public $dtdSystem; + + public function __construct( + $name = null, + $xml = true, + $modules = array(), + $tidyModules = array(), + $aliases = array(), + $dtd_public = null, + $dtd_system = null + ) { + $this->name = $name; + $this->xml = $xml; + $this->modules = $modules; + $this->tidyModules = $tidyModules; + $this->aliases = $aliases; + $this->dtdPublic = $dtd_public; + $this->dtdSystem = $dtd_system; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DoctypeRegistry.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DoctypeRegistry.php new file mode 100644 index 0000000..acc1d64 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DoctypeRegistry.php @@ -0,0 +1,142 @@ +doctypes[$doctype->name] = $doctype; + $name = $doctype->name; + // hookup aliases + foreach ($doctype->aliases as $alias) { + if (isset($this->doctypes[$alias])) { + continue; + } + $this->aliases[$alias] = $name; + } + // remove old aliases + if (isset($this->aliases[$name])) { + unset($this->aliases[$name]); + } + return $doctype; + } + + /** + * Retrieves reference to a doctype of a certain name + * @note This function resolves aliases + * @note When possible, use the more fully-featured make() + * @param string $doctype Name of doctype + * @return HTMLPurifier_Doctype Editable doctype object + */ + public function get($doctype) + { + if (isset($this->aliases[$doctype])) { + $doctype = $this->aliases[$doctype]; + } + if (!isset($this->doctypes[$doctype])) { + trigger_error('Doctype ' . htmlspecialchars($doctype) . ' does not exist', E_USER_ERROR); + $anon = new HTMLPurifier_Doctype($doctype); + return $anon; + } + return $this->doctypes[$doctype]; + } + + /** + * Creates a doctype based on a configuration object, + * will perform initialization on the doctype + * @note Use this function to get a copy of doctype that config + * can hold on to (this is necessary in order to tell + * Generator whether or not the current document is XML + * based or not). + * @param HTMLPurifier_Config $config + * @return HTMLPurifier_Doctype + */ + public function make($config) + { + return clone $this->get($this->getDoctypeFromConfig($config)); + } + + /** + * Retrieves the doctype from the configuration object + * @param HTMLPurifier_Config $config + * @return string + */ + public function getDoctypeFromConfig($config) + { + // recommended test + $doctype = $config->get('HTML.Doctype'); + if (!empty($doctype)) { + return $doctype; + } + $doctype = $config->get('HTML.CustomDoctype'); + if (!empty($doctype)) { + return $doctype; + } + // backwards-compatibility + if ($config->get('HTML.XHTML')) { + $doctype = 'XHTML 1.0'; + } else { + $doctype = 'HTML 4.01'; + } + if ($config->get('HTML.Strict')) { + $doctype .= ' Strict'; + } else { + $doctype .= ' Transitional'; + } + return $doctype; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ElementDef.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ElementDef.php new file mode 100644 index 0000000..d5311ce --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ElementDef.php @@ -0,0 +1,216 @@ +setup(), this array may also + * contain an array at index 0 that indicates which attribute + * collections to load into the full array. It may also + * contain string indentifiers in lieu of HTMLPurifier_AttrDef, + * see HTMLPurifier_AttrTypes on how they are expanded during + * HTMLPurifier_HTMLDefinition->setup() processing. + */ + public $attr = array(); + + // XXX: Design note: currently, it's not possible to override + // previously defined AttrTransforms without messing around with + // the final generated config. This is by design; a previous version + // used an associated list of attr_transform, but it was extremely + // easy to accidentally override other attribute transforms by + // forgetting to specify an index (and just using 0.) While we + // could check this by checking the index number and complaining, + // there is a second problem which is that it is not at all easy to + // tell when something is getting overridden. Combine this with a + // codebase where this isn't really being used, and it's perfect for + // nuking. + + /** + * List of tags HTMLPurifier_AttrTransform to be done before validation. + * @type array + */ + public $attr_transform_pre = array(); + + /** + * List of tags HTMLPurifier_AttrTransform to be done after validation. + * @type array + */ + public $attr_transform_post = array(); + + /** + * HTMLPurifier_ChildDef of this tag. + * @type HTMLPurifier_ChildDef + */ + public $child; + + /** + * Abstract string representation of internal ChildDef rules. + * @see HTMLPurifier_ContentSets for how this is parsed and then transformed + * into an HTMLPurifier_ChildDef. + * @warning This is a temporary variable that is not available after + * being processed by HTMLDefinition + * @type string + */ + public $content_model; + + /** + * Value of $child->type, used to determine which ChildDef to use, + * used in combination with $content_model. + * @warning This must be lowercase + * @warning This is a temporary variable that is not available after + * being processed by HTMLDefinition + * @type string + */ + public $content_model_type; + + /** + * Does the element have a content model (#PCDATA | Inline)*? This + * is important for chameleon ins and del processing in + * HTMLPurifier_ChildDef_Chameleon. Dynamically set: modules don't + * have to worry about this one. + * @type bool + */ + public $descendants_are_inline = false; + + /** + * List of the names of required attributes this element has. + * Dynamically populated by HTMLPurifier_HTMLDefinition::getElement() + * @type array + */ + public $required_attr = array(); + + /** + * Lookup table of tags excluded from all descendants of this tag. + * @type array + * @note SGML permits exclusions for all descendants, but this is + * not possible with DTDs or XML Schemas. W3C has elected to + * use complicated compositions of content_models to simulate + * exclusion for children, but we go the simpler, SGML-style + * route of flat-out exclusions, which correctly apply to + * all descendants and not just children. Note that the XHTML + * Modularization Abstract Modules are blithely unaware of such + * distinctions. + */ + public $excludes = array(); + + /** + * This tag is explicitly auto-closed by the following tags. + * @type array + */ + public $autoclose = array(); + + /** + * If a foreign element is found in this element, test if it is + * allowed by this sub-element; if it is, instead of closing the + * current element, place it inside this element. + * @type string + */ + public $wrap; + + /** + * Whether or not this is a formatting element affected by the + * "Active Formatting Elements" algorithm. + * @type bool + */ + public $formatting; + + /** + * Low-level factory constructor for creating new standalone element defs + */ + public static function create($content_model, $content_model_type, $attr) + { + $def = new HTMLPurifier_ElementDef(); + $def->content_model = $content_model; + $def->content_model_type = $content_model_type; + $def->attr = $attr; + return $def; + } + + /** + * Merges the values of another element definition into this one. + * Values from the new element def take precedence if a value is + * not mergeable. + * @param HTMLPurifier_ElementDef $def + */ + public function mergeIn($def) + { + // later keys takes precedence + foreach ($def->attr as $k => $v) { + if ($k === 0) { + // merge in the includes + // sorry, no way to override an include + foreach ($v as $v2) { + $this->attr[0][] = $v2; + } + continue; + } + if ($v === false) { + if (isset($this->attr[$k])) { + unset($this->attr[$k]); + } + continue; + } + $this->attr[$k] = $v; + } + $this->_mergeAssocArray($this->excludes, $def->excludes); + $this->attr_transform_pre = array_merge($this->attr_transform_pre, $def->attr_transform_pre); + $this->attr_transform_post = array_merge($this->attr_transform_post, $def->attr_transform_post); + + if (!empty($def->content_model)) { + $this->content_model = + str_replace("#SUPER", $this->content_model, $def->content_model); + $this->child = false; + } + if (!empty($def->content_model_type)) { + $this->content_model_type = $def->content_model_type; + $this->child = false; + } + if (!is_null($def->child)) { + $this->child = $def->child; + } + if (!is_null($def->formatting)) { + $this->formatting = $def->formatting; + } + if ($def->descendants_are_inline) { + $this->descendants_are_inline = $def->descendants_are_inline; + } + } + + /** + * Merges one array into another, removes values which equal false + * @param $a1 Array by reference that is merged into + * @param $a2 Array that merges into $a1 + */ + private function _mergeAssocArray(&$a1, $a2) + { + foreach ($a2 as $k => $v) { + if ($v === false) { + if (isset($a1[$k])) { + unset($a1[$k]); + } + continue; + } + $a1[$k] = $v; + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php new file mode 100644 index 0000000..b94f175 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php @@ -0,0 +1,617 @@ += $c) { + $r .= self::unsafeIconv($in, $out, substr($text, $i)); + break; + } + // wibble the boundary + if (0x80 != (0xC0 & ord($text[$i + $max_chunk_size]))) { + $chunk_size = $max_chunk_size; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 1]))) { + $chunk_size = $max_chunk_size - 1; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 2]))) { + $chunk_size = $max_chunk_size - 2; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 3]))) { + $chunk_size = $max_chunk_size - 3; + } else { + return false; // rather confusing UTF-8... + } + $chunk = substr($text, $i, $chunk_size); // substr doesn't mind overlong lengths + $r .= self::unsafeIconv($in, $out, $chunk); + $i += $chunk_size; + } + return $r; + } else { + return false; + } + } else { + return false; + } + } + + /** + * Cleans a UTF-8 string for well-formedness and SGML validity + * + * It will parse according to UTF-8 and return a valid UTF8 string, with + * non-SGML codepoints excluded. + * + * Specifically, it will permit: + * \x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF} + * Source: https://www.w3.org/TR/REC-xml/#NT-Char + * Arguably this function should be modernized to the HTML5 set + * of allowed characters: + * https://www.w3.org/TR/html5/syntax.html#preprocessing-the-input-stream + * which simultaneously expand and restrict the set of allowed characters. + * + * @param string $str The string to clean + * @param bool $force_php + * @return string + * + * @note Just for reference, the non-SGML code points are 0 to 31 and + * 127 to 159, inclusive. However, we allow code points 9, 10 + * and 13, which are the tab, line feed and carriage return + * respectively. 128 and above the code points map to multibyte + * UTF-8 representations. + * + * @note Fallback code adapted from utf8ToUnicode by Henri Sivonen and + * hsivonen@iki.fi at under the + * LGPL license. Notes on what changed are inside, but in general, + * the original code transformed UTF-8 text into an array of integer + * Unicode codepoints. Understandably, transforming that back to + * a string would be somewhat expensive, so the function was modded to + * directly operate on the string. However, this discourages code + * reuse, and the logic enumerated here would be useful for any + * function that needs to be able to understand UTF-8 characters. + * As of right now, only smart lossless character encoding converters + * would need that, and I'm probably not going to implement them. + */ + public static function cleanUTF8($str, $force_php = false) + { + // UTF-8 validity is checked since PHP 4.3.5 + // This is an optimization: if the string is already valid UTF-8, no + // need to do PHP stuff. 99% of the time, this will be the case. + if (preg_match( + '/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du', + $str + )) { + return $str; + } + + $mState = 0; // cached expected number of octets after the current octet + // until the beginning of the next UTF8 character sequence + $mUcs4 = 0; // cached Unicode character + $mBytes = 1; // cached expected number of octets in the current sequence + + // original code involved an $out that was an array of Unicode + // codepoints. Instead of having to convert back into UTF-8, we've + // decided to directly append valid UTF-8 characters onto a string + // $out once they're done. $char accumulates raw bytes, while $mUcs4 + // turns into the Unicode code point, so there's some redundancy. + + $out = ''; + $char = ''; + + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + $in = ord($str{$i}); + $char .= $str[$i]; // append byte to char + if (0 == $mState) { + // When mState is zero we expect either a US-ASCII character + // or a multi-octet sequence. + if (0 == (0x80 & ($in))) { + // US-ASCII, pass straight through. + if (($in <= 31 || $in == 127) && + !($in == 9 || $in == 13 || $in == 10) // save \r\t\n + ) { + // control characters, remove + } else { + $out .= $char; + } + // reset + $char = ''; + $mBytes = 1; + } elseif (0xC0 == (0xE0 & ($in))) { + // First octet of 2 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x1F) << 6; + $mState = 1; + $mBytes = 2; + } elseif (0xE0 == (0xF0 & ($in))) { + // First octet of 3 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x0F) << 12; + $mState = 2; + $mBytes = 3; + } elseif (0xF0 == (0xF8 & ($in))) { + // First octet of 4 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x07) << 18; + $mState = 3; + $mBytes = 4; + } elseif (0xF8 == (0xFC & ($in))) { + // First octet of 5 octet sequence. + // + // This is illegal because the encoded codepoint must be + // either: + // (a) not the shortest form or + // (b) outside the Unicode range of 0-0x10FFFF. + // Rather than trying to resynchronize, we will carry on + // until the end of the sequence and let the later error + // handling code catch it. + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x03) << 24; + $mState = 4; + $mBytes = 5; + } elseif (0xFC == (0xFE & ($in))) { + // First octet of 6 octet sequence, see comments for 5 + // octet sequence. + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 1) << 30; + $mState = 5; + $mBytes = 6; + } else { + // Current octet is neither in the US-ASCII range nor a + // legal first octet of a multi-octet sequence. + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char = ''; + } + } else { + // When mState is non-zero, we expect a continuation of the + // multi-octet sequence + if (0x80 == (0xC0 & ($in))) { + // Legal continuation. + $shift = ($mState - 1) * 6; + $tmp = $in; + $tmp = ($tmp & 0x0000003F) << $shift; + $mUcs4 |= $tmp; + + if (0 == --$mState) { + // End of the multi-octet sequence. mUcs4 now contains + // the final Unicode codepoint to be output + + // Check for illegal sequences and codepoints. + + // From Unicode 3.1, non-shortest form is illegal + if (((2 == $mBytes) && ($mUcs4 < 0x0080)) || + ((3 == $mBytes) && ($mUcs4 < 0x0800)) || + ((4 == $mBytes) && ($mUcs4 < 0x10000)) || + (4 < $mBytes) || + // From Unicode 3.2, surrogate characters = illegal + (($mUcs4 & 0xFFFFF800) == 0xD800) || + // Codepoints outside the Unicode range are illegal + ($mUcs4 > 0x10FFFF) + ) { + + } elseif (0xFEFF != $mUcs4 && // omit BOM + // check for valid Char unicode codepoints + ( + 0x9 == $mUcs4 || + 0xA == $mUcs4 || + 0xD == $mUcs4 || + (0x20 <= $mUcs4 && 0x7E >= $mUcs4) || + // 7F-9F is not strictly prohibited by XML, + // but it is non-SGML, and thus we don't allow it + (0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) || + (0xE000 <= $mUcs4 && 0xFFFD >= $mUcs4) || + (0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4) + ) + ) { + $out .= $char; + } + // initialize UTF8 cache (reset) + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char = ''; + } + } else { + // ((0xC0 & (*in) != 0x80) && (mState != 0)) + // Incomplete multi-octet sequence. + // used to result in complete fail, but we'll reset + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char =''; + } + } + } + return $out; + } + + /** + * Translates a Unicode codepoint into its corresponding UTF-8 character. + * @note Based on Feyd's function at + * , + * which is in public domain. + * @note While we're going to do code point parsing anyway, a good + * optimization would be to refuse to translate code points that + * are non-SGML characters. However, this could lead to duplication. + * @note This is very similar to the unichr function in + * maintenance/generate-entity-file.php (although this is superior, + * due to its sanity checks). + */ + + // +----------+----------+----------+----------+ + // | 33222222 | 22221111 | 111111 | | + // | 10987654 | 32109876 | 54321098 | 76543210 | bit + // +----------+----------+----------+----------+ + // | | | | 0xxxxxxx | 1 byte 0x00000000..0x0000007F + // | | | 110yyyyy | 10xxxxxx | 2 byte 0x00000080..0x000007FF + // | | 1110zzzz | 10yyyyyy | 10xxxxxx | 3 byte 0x00000800..0x0000FFFF + // | 11110www | 10wwzzzz | 10yyyyyy | 10xxxxxx | 4 byte 0x00010000..0x0010FFFF + // +----------+----------+----------+----------+ + // | 00000000 | 00011111 | 11111111 | 11111111 | Theoretical upper limit of legal scalars: 2097151 (0x001FFFFF) + // | 00000000 | 00010000 | 11111111 | 11111111 | Defined upper limit of legal scalar codes + // +----------+----------+----------+----------+ + + public static function unichr($code) + { + if ($code > 1114111 or $code < 0 or + ($code >= 55296 and $code <= 57343) ) { + // bits are set outside the "valid" range as defined + // by UNICODE 4.1.0 + return ''; + } + + $x = $y = $z = $w = 0; + if ($code < 128) { + // regular ASCII character + $x = $code; + } else { + // set up bits for UTF-8 + $x = ($code & 63) | 128; + if ($code < 2048) { + $y = (($code & 2047) >> 6) | 192; + } else { + $y = (($code & 4032) >> 6) | 128; + if ($code < 65536) { + $z = (($code >> 12) & 15) | 224; + } else { + $z = (($code >> 12) & 63) | 128; + $w = (($code >> 18) & 7) | 240; + } + } + } + // set up the actual character + $ret = ''; + if ($w) { + $ret .= chr($w); + } + if ($z) { + $ret .= chr($z); + } + if ($y) { + $ret .= chr($y); + } + $ret .= chr($x); + + return $ret; + } + + /** + * @return bool + */ + public static function iconvAvailable() + { + static $iconv = null; + if ($iconv === null) { + $iconv = function_exists('iconv') && self::testIconvTruncateBug() != self::ICONV_UNUSABLE; + } + return $iconv; + } + + /** + * Convert a string to UTF-8 based on configuration. + * @param string $str The string to convert + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public static function convertToUTF8($str, $config, $context) + { + $encoding = $config->get('Core.Encoding'); + if ($encoding === 'utf-8') { + return $str; + } + static $iconv = null; + if ($iconv === null) { + $iconv = self::iconvAvailable(); + } + if ($iconv && !$config->get('Test.ForceNoIconv')) { + // unaffected by bugs, since UTF-8 support all characters + $str = self::unsafeIconv($encoding, 'utf-8//IGNORE', $str); + if ($str === false) { + // $encoding is not a valid encoding + trigger_error('Invalid encoding ' . $encoding, E_USER_ERROR); + return ''; + } + // If the string is bjorked by Shift_JIS or a similar encoding + // that doesn't support all of ASCII, convert the naughty + // characters to their true byte-wise ASCII/UTF-8 equivalents. + $str = strtr($str, self::testEncodingSupportsASCII($encoding)); + return $str; + } elseif ($encoding === 'iso-8859-1') { + $str = utf8_encode($str); + return $str; + } + $bug = HTMLPurifier_Encoder::testIconvTruncateBug(); + if ($bug == self::ICONV_OK) { + trigger_error('Encoding not supported, please install iconv', E_USER_ERROR); + } else { + trigger_error( + 'You have a buggy version of iconv, see https://bugs.php.net/bug.php?id=48147 ' . + 'and http://sourceware.org/bugzilla/show_bug.cgi?id=13541', + E_USER_ERROR + ); + } + } + + /** + * Converts a string from UTF-8 based on configuration. + * @param string $str The string to convert + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + * @note Currently, this is a lossy conversion, with unexpressable + * characters being omitted. + */ + public static function convertFromUTF8($str, $config, $context) + { + $encoding = $config->get('Core.Encoding'); + if ($escape = $config->get('Core.EscapeNonASCIICharacters')) { + $str = self::convertToASCIIDumbLossless($str); + } + if ($encoding === 'utf-8') { + return $str; + } + static $iconv = null; + if ($iconv === null) { + $iconv = self::iconvAvailable(); + } + if ($iconv && !$config->get('Test.ForceNoIconv')) { + // Undo our previous fix in convertToUTF8, otherwise iconv will barf + $ascii_fix = self::testEncodingSupportsASCII($encoding); + if (!$escape && !empty($ascii_fix)) { + $clear_fix = array(); + foreach ($ascii_fix as $utf8 => $native) { + $clear_fix[$utf8] = ''; + } + $str = strtr($str, $clear_fix); + } + $str = strtr($str, array_flip($ascii_fix)); + // Normal stuff + $str = self::iconv('utf-8', $encoding . '//IGNORE', $str); + return $str; + } elseif ($encoding === 'iso-8859-1') { + $str = utf8_decode($str); + return $str; + } + trigger_error('Encoding not supported', E_USER_ERROR); + // You might be tempted to assume that the ASCII representation + // might be OK, however, this is *not* universally true over all + // encodings. So we take the conservative route here, rather + // than forcibly turn on %Core.EscapeNonASCIICharacters + } + + /** + * Lossless (character-wise) conversion of HTML to ASCII + * @param string $str UTF-8 string to be converted to ASCII + * @return string ASCII encoded string with non-ASCII character entity-ized + * @warning Adapted from MediaWiki, claiming fair use: this is a common + * algorithm. If you disagree with this license fudgery, + * implement it yourself. + * @note Uses decimal numeric entities since they are best supported. + * @note This is a DUMB function: it has no concept of keeping + * character entities that the projected character encoding + * can allow. We could possibly implement a smart version + * but that would require it to also know which Unicode + * codepoints the charset supported (not an easy task). + * @note Sort of with cleanUTF8() but it assumes that $str is + * well-formed UTF-8 + */ + public static function convertToASCIIDumbLossless($str) + { + $bytesleft = 0; + $result = ''; + $working = 0; + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + $bytevalue = ord($str[$i]); + if ($bytevalue <= 0x7F) { //0xxx xxxx + $result .= chr($bytevalue); + $bytesleft = 0; + } elseif ($bytevalue <= 0xBF) { //10xx xxxx + $working = $working << 6; + $working += ($bytevalue & 0x3F); + $bytesleft--; + if ($bytesleft <= 0) { + $result .= "&#" . $working . ";"; + } + } elseif ($bytevalue <= 0xDF) { //110x xxxx + $working = $bytevalue & 0x1F; + $bytesleft = 1; + } elseif ($bytevalue <= 0xEF) { //1110 xxxx + $working = $bytevalue & 0x0F; + $bytesleft = 2; + } else { //1111 0xxx + $working = $bytevalue & 0x07; + $bytesleft = 3; + } + } + return $result; + } + + /** No bugs detected in iconv. */ + const ICONV_OK = 0; + + /** Iconv truncates output if converting from UTF-8 to another + * character set with //IGNORE, and a non-encodable character is found */ + const ICONV_TRUNCATES = 1; + + /** Iconv does not support //IGNORE, making it unusable for + * transcoding purposes */ + const ICONV_UNUSABLE = 2; + + /** + * glibc iconv has a known bug where it doesn't handle the magic + * //IGNORE stanza correctly. In particular, rather than ignore + * characters, it will return an EILSEQ after consuming some number + * of characters, and expect you to restart iconv as if it were + * an E2BIG. Old versions of PHP did not respect the errno, and + * returned the fragment, so as a result you would see iconv + * mysteriously truncating output. We can work around this by + * manually chopping our input into segments of about 8000 + * characters, as long as PHP ignores the error code. If PHP starts + * paying attention to the error code, iconv becomes unusable. + * + * @return int Error code indicating severity of bug. + */ + public static function testIconvTruncateBug() + { + static $code = null; + if ($code === null) { + // better not use iconv, otherwise infinite loop! + $r = self::unsafeIconv('utf-8', 'ascii//IGNORE', "\xCE\xB1" . str_repeat('a', 9000)); + if ($r === false) { + $code = self::ICONV_UNUSABLE; + } elseif (($c = strlen($r)) < 9000) { + $code = self::ICONV_TRUNCATES; + } elseif ($c > 9000) { + trigger_error( + 'Your copy of iconv is extremely buggy. Please notify HTML Purifier maintainers: ' . + 'include your iconv version as per phpversion()', + E_USER_ERROR + ); + } else { + $code = self::ICONV_OK; + } + } + return $code; + } + + /** + * This expensive function tests whether or not a given character + * encoding supports ASCII. 7/8-bit encodings like Shift_JIS will + * fail this test, and require special processing. Variable width + * encodings shouldn't ever fail. + * + * @param string $encoding Encoding name to test, as per iconv format + * @param bool $bypass Whether or not to bypass the precompiled arrays. + * @return Array of UTF-8 characters to their corresponding ASCII, + * which can be used to "undo" any overzealous iconv action. + */ + public static function testEncodingSupportsASCII($encoding, $bypass = false) + { + // All calls to iconv here are unsafe, proof by case analysis: + // If ICONV_OK, no difference. + // If ICONV_TRUNCATE, all calls involve one character inputs, + // so bug is not triggered. + // If ICONV_UNUSABLE, this call is irrelevant + static $encodings = array(); + if (!$bypass) { + if (isset($encodings[$encoding])) { + return $encodings[$encoding]; + } + $lenc = strtolower($encoding); + switch ($lenc) { + case 'shift_jis': + return array("\xC2\xA5" => '\\', "\xE2\x80\xBE" => '~'); + case 'johab': + return array("\xE2\x82\xA9" => '\\'); + } + if (strpos($lenc, 'iso-8859-') === 0) { + return array(); + } + } + $ret = array(); + if (self::unsafeIconv('UTF-8', $encoding, 'a') === false) { + return false; + } + for ($i = 0x20; $i <= 0x7E; $i++) { // all printable ASCII chars + $c = chr($i); // UTF-8 char + $r = self::unsafeIconv('UTF-8', "$encoding//IGNORE", $c); // initial conversion + if ($r === '' || + // This line is needed for iconv implementations that do not + // omit characters that do not exist in the target character set + ($r === $c && self::unsafeIconv($encoding, 'UTF-8//IGNORE', $r) !== $c) + ) { + // Reverse engineer: what's the UTF-8 equiv of this byte + // sequence? This assumes that there's no variable width + // encoding that doesn't support ASCII. + $ret[self::unsafeIconv($encoding, 'UTF-8//IGNORE', $c)] = $c; + } + } + $encodings[$encoding] = $ret; + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup.php new file mode 100644 index 0000000..f12ff13 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup.php @@ -0,0 +1,48 @@ +table = unserialize(file_get_contents($file)); + } + + /** + * Retrieves sole instance of the object. + * @param bool|HTMLPurifier_EntityLookup $prototype Optional prototype of custom lookup table to overload with. + * @return HTMLPurifier_EntityLookup + */ + public static function instance($prototype = false) + { + // no references, since PHP doesn't copy unless modified + static $instance = null; + if ($prototype) { + $instance = $prototype; + } elseif (!$instance) { + $instance = new HTMLPurifier_EntityLookup(); + $instance->setup(); + } + return $instance; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup/entities.ser b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup/entities.ser new file mode 100644 index 0000000..e8b0812 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup/entities.ser @@ -0,0 +1 @@ +a:253:{s:4:"fnof";s:2:"ƒ";s:5:"Alpha";s:2:"Α";s:4:"Beta";s:2:"Β";s:5:"Gamma";s:2:"Γ";s:5:"Delta";s:2:"Δ";s:7:"Epsilon";s:2:"Ε";s:4:"Zeta";s:2:"Ζ";s:3:"Eta";s:2:"Η";s:5:"Theta";s:2:"Θ";s:4:"Iota";s:2:"Ι";s:5:"Kappa";s:2:"Κ";s:6:"Lambda";s:2:"Λ";s:2:"Mu";s:2:"Μ";s:2:"Nu";s:2:"Ν";s:2:"Xi";s:2:"Ξ";s:7:"Omicron";s:2:"Ο";s:2:"Pi";s:2:"Π";s:3:"Rho";s:2:"Ρ";s:5:"Sigma";s:2:"Σ";s:3:"Tau";s:2:"Τ";s:7:"Upsilon";s:2:"Υ";s:3:"Phi";s:2:"Φ";s:3:"Chi";s:2:"Χ";s:3:"Psi";s:2:"Ψ";s:5:"Omega";s:2:"Ω";s:5:"alpha";s:2:"α";s:4:"beta";s:2:"β";s:5:"gamma";s:2:"γ";s:5:"delta";s:2:"δ";s:7:"epsilon";s:2:"ε";s:4:"zeta";s:2:"ζ";s:3:"eta";s:2:"η";s:5:"theta";s:2:"θ";s:4:"iota";s:2:"ι";s:5:"kappa";s:2:"κ";s:6:"lambda";s:2:"λ";s:2:"mu";s:2:"μ";s:2:"nu";s:2:"ν";s:2:"xi";s:2:"ξ";s:7:"omicron";s:2:"ο";s:2:"pi";s:2:"π";s:3:"rho";s:2:"ρ";s:6:"sigmaf";s:2:"ς";s:5:"sigma";s:2:"σ";s:3:"tau";s:2:"τ";s:7:"upsilon";s:2:"υ";s:3:"phi";s:2:"φ";s:3:"chi";s:2:"χ";s:3:"psi";s:2:"ψ";s:5:"omega";s:2:"ω";s:8:"thetasym";s:2:"ϑ";s:5:"upsih";s:2:"ϒ";s:3:"piv";s:2:"ϖ";s:4:"bull";s:3:"•";s:6:"hellip";s:3:"…";s:5:"prime";s:3:"′";s:5:"Prime";s:3:"″";s:5:"oline";s:3:"‾";s:5:"frasl";s:3:"⁄";s:6:"weierp";s:3:"℘";s:5:"image";s:3:"ℑ";s:4:"real";s:3:"ℜ";s:5:"trade";s:3:"™";s:7:"alefsym";s:3:"ℵ";s:4:"larr";s:3:"←";s:4:"uarr";s:3:"↑";s:4:"rarr";s:3:"→";s:4:"darr";s:3:"↓";s:4:"harr";s:3:"↔";s:5:"crarr";s:3:"↵";s:4:"lArr";s:3:"⇐";s:4:"uArr";s:3:"⇑";s:4:"rArr";s:3:"⇒";s:4:"dArr";s:3:"⇓";s:4:"hArr";s:3:"⇔";s:6:"forall";s:3:"∀";s:4:"part";s:3:"∂";s:5:"exist";s:3:"∃";s:5:"empty";s:3:"∅";s:5:"nabla";s:3:"∇";s:4:"isin";s:3:"∈";s:5:"notin";s:3:"∉";s:2:"ni";s:3:"∋";s:4:"prod";s:3:"∏";s:3:"sum";s:3:"∑";s:5:"minus";s:3:"−";s:6:"lowast";s:3:"∗";s:5:"radic";s:3:"√";s:4:"prop";s:3:"∝";s:5:"infin";s:3:"∞";s:3:"ang";s:3:"∠";s:3:"and";s:3:"∧";s:2:"or";s:3:"∨";s:3:"cap";s:3:"∩";s:3:"cup";s:3:"∪";s:3:"int";s:3:"∫";s:6:"there4";s:3:"∴";s:3:"sim";s:3:"∼";s:4:"cong";s:3:"≅";s:5:"asymp";s:3:"≈";s:2:"ne";s:3:"≠";s:5:"equiv";s:3:"≡";s:2:"le";s:3:"≤";s:2:"ge";s:3:"≥";s:3:"sub";s:3:"⊂";s:3:"sup";s:3:"⊃";s:4:"nsub";s:3:"⊄";s:4:"sube";s:3:"⊆";s:4:"supe";s:3:"⊇";s:5:"oplus";s:3:"⊕";s:6:"otimes";s:3:"⊗";s:4:"perp";s:3:"⊥";s:4:"sdot";s:3:"⋅";s:5:"lceil";s:3:"⌈";s:5:"rceil";s:3:"⌉";s:6:"lfloor";s:3:"⌊";s:6:"rfloor";s:3:"⌋";s:4:"lang";s:3:"〈";s:4:"rang";s:3:"〉";s:3:"loz";s:3:"◊";s:6:"spades";s:3:"♠";s:5:"clubs";s:3:"♣";s:6:"hearts";s:3:"♥";s:5:"diams";s:3:"♦";s:4:"quot";s:1:""";s:3:"amp";s:1:"&";s:2:"lt";s:1:"<";s:2:"gt";s:1:">";s:4:"apos";s:1:"'";s:5:"OElig";s:2:"Œ";s:5:"oelig";s:2:"œ";s:6:"Scaron";s:2:"Š";s:6:"scaron";s:2:"š";s:4:"Yuml";s:2:"Ÿ";s:4:"circ";s:2:"ˆ";s:5:"tilde";s:2:"˜";s:4:"ensp";s:3:" ";s:4:"emsp";s:3:" ";s:6:"thinsp";s:3:" ";s:4:"zwnj";s:3:"‌";s:3:"zwj";s:3:"‍";s:3:"lrm";s:3:"‎";s:3:"rlm";s:3:"‏";s:5:"ndash";s:3:"–";s:5:"mdash";s:3:"—";s:5:"lsquo";s:3:"‘";s:5:"rsquo";s:3:"’";s:5:"sbquo";s:3:"‚";s:5:"ldquo";s:3:"“";s:5:"rdquo";s:3:"”";s:5:"bdquo";s:3:"„";s:6:"dagger";s:3:"†";s:6:"Dagger";s:3:"‡";s:6:"permil";s:3:"‰";s:6:"lsaquo";s:3:"‹";s:6:"rsaquo";s:3:"›";s:4:"euro";s:3:"€";s:4:"nbsp";s:2:" ";s:5:"iexcl";s:2:"¡";s:4:"cent";s:2:"¢";s:5:"pound";s:2:"£";s:6:"curren";s:2:"¤";s:3:"yen";s:2:"¥";s:6:"brvbar";s:2:"¦";s:4:"sect";s:2:"§";s:3:"uml";s:2:"¨";s:4:"copy";s:2:"©";s:4:"ordf";s:2:"ª";s:5:"laquo";s:2:"«";s:3:"not";s:2:"¬";s:3:"shy";s:2:"­";s:3:"reg";s:2:"®";s:4:"macr";s:2:"¯";s:3:"deg";s:2:"°";s:6:"plusmn";s:2:"±";s:4:"sup2";s:2:"²";s:4:"sup3";s:2:"³";s:5:"acute";s:2:"´";s:5:"micro";s:2:"µ";s:4:"para";s:2:"¶";s:6:"middot";s:2:"·";s:5:"cedil";s:2:"¸";s:4:"sup1";s:2:"¹";s:4:"ordm";s:2:"º";s:5:"raquo";s:2:"»";s:6:"frac14";s:2:"¼";s:6:"frac12";s:2:"½";s:6:"frac34";s:2:"¾";s:6:"iquest";s:2:"¿";s:6:"Agrave";s:2:"À";s:6:"Aacute";s:2:"Á";s:5:"Acirc";s:2:"Â";s:6:"Atilde";s:2:"Ã";s:4:"Auml";s:2:"Ä";s:5:"Aring";s:2:"Å";s:5:"AElig";s:2:"Æ";s:6:"Ccedil";s:2:"Ç";s:6:"Egrave";s:2:"È";s:6:"Eacute";s:2:"É";s:5:"Ecirc";s:2:"Ê";s:4:"Euml";s:2:"Ë";s:6:"Igrave";s:2:"Ì";s:6:"Iacute";s:2:"Í";s:5:"Icirc";s:2:"Î";s:4:"Iuml";s:2:"Ï";s:3:"ETH";s:2:"Ð";s:6:"Ntilde";s:2:"Ñ";s:6:"Ograve";s:2:"Ò";s:6:"Oacute";s:2:"Ó";s:5:"Ocirc";s:2:"Ô";s:6:"Otilde";s:2:"Õ";s:4:"Ouml";s:2:"Ö";s:5:"times";s:2:"×";s:6:"Oslash";s:2:"Ø";s:6:"Ugrave";s:2:"Ù";s:6:"Uacute";s:2:"Ú";s:5:"Ucirc";s:2:"Û";s:4:"Uuml";s:2:"Ü";s:6:"Yacute";s:2:"Ý";s:5:"THORN";s:2:"Þ";s:5:"szlig";s:2:"ß";s:6:"agrave";s:2:"à";s:6:"aacute";s:2:"á";s:5:"acirc";s:2:"â";s:6:"atilde";s:2:"ã";s:4:"auml";s:2:"ä";s:5:"aring";s:2:"å";s:5:"aelig";s:2:"æ";s:6:"ccedil";s:2:"ç";s:6:"egrave";s:2:"è";s:6:"eacute";s:2:"é";s:5:"ecirc";s:2:"ê";s:4:"euml";s:2:"ë";s:6:"igrave";s:2:"ì";s:6:"iacute";s:2:"í";s:5:"icirc";s:2:"î";s:4:"iuml";s:2:"ï";s:3:"eth";s:2:"ð";s:6:"ntilde";s:2:"ñ";s:6:"ograve";s:2:"ò";s:6:"oacute";s:2:"ó";s:5:"ocirc";s:2:"ô";s:6:"otilde";s:2:"õ";s:4:"ouml";s:2:"ö";s:6:"divide";s:2:"÷";s:6:"oslash";s:2:"ø";s:6:"ugrave";s:2:"ù";s:6:"uacute";s:2:"ú";s:5:"ucirc";s:2:"û";s:4:"uuml";s:2:"ü";s:6:"yacute";s:2:"ý";s:5:"thorn";s:2:"þ";s:4:"yuml";s:2:"ÿ";} \ No newline at end of file diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityParser.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityParser.php new file mode 100644 index 0000000..c372b5a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityParser.php @@ -0,0 +1,285 @@ +_semiOptionalPrefixRegex = "/&()()()($semi_optional)/"; + + $this->_textEntitiesRegex = + '/&(?:'. + // hex + '[#]x([a-fA-F0-9]+);?|'. + // dec + '[#]0*(\d+);?|'. + // string (mandatory semicolon) + // NB: order matters: match semicolon preferentially + '([A-Za-z_:][A-Za-z0-9.\-_:]*);|'. + // string (optional semicolon) + "($semi_optional)". + ')/'; + + $this->_attrEntitiesRegex = + '/&(?:'. + // hex + '[#]x([a-fA-F0-9]+);?|'. + // dec + '[#]0*(\d+);?|'. + // string (mandatory semicolon) + // NB: order matters: match semicolon preferentially + '([A-Za-z_:][A-Za-z0-9.\-_:]*);|'. + // string (optional semicolon) + // don't match if trailing is equals or alphanumeric (URL + // like) + "($semi_optional)(?![=;A-Za-z0-9])". + ')/'; + + } + + /** + * Substitute entities with the parsed equivalents. Use this on + * textual data in an HTML document (as opposed to attributes.) + * + * @param string $string String to have entities parsed. + * @return string Parsed string. + */ + public function substituteTextEntities($string) + { + return preg_replace_callback( + $this->_textEntitiesRegex, + array($this, 'entityCallback'), + $string + ); + } + + /** + * Substitute entities with the parsed equivalents. Use this on + * attribute contents in documents. + * + * @param string $string String to have entities parsed. + * @return string Parsed string. + */ + public function substituteAttrEntities($string) + { + return preg_replace_callback( + $this->_attrEntitiesRegex, + array($this, 'entityCallback'), + $string + ); + } + + /** + * Callback function for substituteNonSpecialEntities() that does the work. + * + * @param array $matches PCRE matches array, with 0 the entire match, and + * either index 1, 2 or 3 set with a hex value, dec value, + * or string (respectively). + * @return string Replacement string. + */ + + protected function entityCallback($matches) + { + $entity = $matches[0]; + $hex_part = @$matches[1]; + $dec_part = @$matches[2]; + $named_part = empty($matches[3]) ? @$matches[4] : $matches[3]; + if ($hex_part !== NULL && $hex_part !== "") { + return HTMLPurifier_Encoder::unichr(hexdec($hex_part)); + } elseif ($dec_part !== NULL && $dec_part !== "") { + return HTMLPurifier_Encoder::unichr((int) $dec_part); + } else { + if (!$this->_entity_lookup) { + $this->_entity_lookup = HTMLPurifier_EntityLookup::instance(); + } + if (isset($this->_entity_lookup->table[$named_part])) { + return $this->_entity_lookup->table[$named_part]; + } else { + // exact match didn't match anything, so test if + // any of the semicolon optional match the prefix. + // Test that this is an EXACT match is important to + // prevent infinite loop + if (!empty($matches[3])) { + return preg_replace_callback( + $this->_semiOptionalPrefixRegex, + array($this, 'entityCallback'), + $entity + ); + } + return $entity; + } + } + } + + // LEGACY CODE BELOW + + /** + * Callback regex string for parsing entities. + * @type string + */ + protected $_substituteEntitiesRegex = + '/&(?:[#]x([a-fA-F0-9]+)|[#]0*(\d+)|([A-Za-z_:][A-Za-z0-9.\-_:]*));?/'; + // 1. hex 2. dec 3. string (XML style) + + /** + * Decimal to parsed string conversion table for special entities. + * @type array + */ + protected $_special_dec2str = + array( + 34 => '"', + 38 => '&', + 39 => "'", + 60 => '<', + 62 => '>' + ); + + /** + * Stripped entity names to decimal conversion table for special entities. + * @type array + */ + protected $_special_ent2dec = + array( + 'quot' => 34, + 'amp' => 38, + 'lt' => 60, + 'gt' => 62 + ); + + /** + * Substitutes non-special entities with their parsed equivalents. Since + * running this whenever you have parsed character is t3h 5uck, we run + * it before everything else. + * + * @param string $string String to have non-special entities parsed. + * @return string Parsed string. + */ + public function substituteNonSpecialEntities($string) + { + // it will try to detect missing semicolons, but don't rely on it + return preg_replace_callback( + $this->_substituteEntitiesRegex, + array($this, 'nonSpecialEntityCallback'), + $string + ); + } + + /** + * Callback function for substituteNonSpecialEntities() that does the work. + * + * @param array $matches PCRE matches array, with 0 the entire match, and + * either index 1, 2 or 3 set with a hex value, dec value, + * or string (respectively). + * @return string Replacement string. + */ + + protected function nonSpecialEntityCallback($matches) + { + // replaces all but big five + $entity = $matches[0]; + $is_num = (@$matches[0][1] === '#'); + if ($is_num) { + $is_hex = (@$entity[2] === 'x'); + $code = $is_hex ? hexdec($matches[1]) : (int) $matches[2]; + // abort for special characters + if (isset($this->_special_dec2str[$code])) { + return $entity; + } + return HTMLPurifier_Encoder::unichr($code); + } else { + if (isset($this->_special_ent2dec[$matches[3]])) { + return $entity; + } + if (!$this->_entity_lookup) { + $this->_entity_lookup = HTMLPurifier_EntityLookup::instance(); + } + if (isset($this->_entity_lookup->table[$matches[3]])) { + return $this->_entity_lookup->table[$matches[3]]; + } else { + return $entity; + } + } + } + + /** + * Substitutes only special entities with their parsed equivalents. + * + * @notice We try to avoid calling this function because otherwise, it + * would have to be called a lot (for every parsed section). + * + * @param string $string String to have non-special entities parsed. + * @return string Parsed string. + */ + public function substituteSpecialEntities($string) + { + return preg_replace_callback( + $this->_substituteEntitiesRegex, + array($this, 'specialEntityCallback'), + $string + ); + } + + /** + * Callback function for substituteSpecialEntities() that does the work. + * + * This callback has same syntax as nonSpecialEntityCallback(). + * + * @param array $matches PCRE-style matches array, with 0 the entire match, and + * either index 1, 2 or 3 set with a hex value, dec value, + * or string (respectively). + * @return string Replacement string. + */ + protected function specialEntityCallback($matches) + { + $entity = $matches[0]; + $is_num = (@$matches[0][1] === '#'); + if ($is_num) { + $is_hex = (@$entity[2] === 'x'); + $int = $is_hex ? hexdec($matches[1]) : (int) $matches[2]; + return isset($this->_special_dec2str[$int]) ? + $this->_special_dec2str[$int] : + $entity; + } else { + return isset($this->_special_ent2dec[$matches[3]]) ? + $this->_special_dec2str[$this->_special_ent2dec[$matches[3]]] : + $entity; + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorCollector.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorCollector.php new file mode 100644 index 0000000..d47e3f2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorCollector.php @@ -0,0 +1,244 @@ +locale =& $context->get('Locale'); + $this->context = $context; + $this->_current =& $this->_stacks[0]; + $this->errors =& $this->_stacks[0]; + } + + /** + * Sends an error message to the collector for later use + * @param int $severity Error severity, PHP error style (don't use E_USER_) + * @param string $msg Error message text + */ + public function send($severity, $msg) + { + $args = array(); + if (func_num_args() > 2) { + $args = func_get_args(); + array_shift($args); + unset($args[0]); + } + + $token = $this->context->get('CurrentToken', true); + $line = $token ? $token->line : $this->context->get('CurrentLine', true); + $col = $token ? $token->col : $this->context->get('CurrentCol', true); + $attr = $this->context->get('CurrentAttr', true); + + // perform special substitutions, also add custom parameters + $subst = array(); + if (!is_null($token)) { + $args['CurrentToken'] = $token; + } + if (!is_null($attr)) { + $subst['$CurrentAttr.Name'] = $attr; + if (isset($token->attr[$attr])) { + $subst['$CurrentAttr.Value'] = $token->attr[$attr]; + } + } + + if (empty($args)) { + $msg = $this->locale->getMessage($msg); + } else { + $msg = $this->locale->formatMessage($msg, $args); + } + + if (!empty($subst)) { + $msg = strtr($msg, $subst); + } + + // (numerically indexed) + $error = array( + self::LINENO => $line, + self::SEVERITY => $severity, + self::MESSAGE => $msg, + self::CHILDREN => array() + ); + $this->_current[] = $error; + + // NEW CODE BELOW ... + // Top-level errors are either: + // TOKEN type, if $value is set appropriately, or + // "syntax" type, if $value is null + $new_struct = new HTMLPurifier_ErrorStruct(); + $new_struct->type = HTMLPurifier_ErrorStruct::TOKEN; + if ($token) { + $new_struct->value = clone $token; + } + if (is_int($line) && is_int($col)) { + if (isset($this->lines[$line][$col])) { + $struct = $this->lines[$line][$col]; + } else { + $struct = $this->lines[$line][$col] = $new_struct; + } + // These ksorts may present a performance problem + ksort($this->lines[$line], SORT_NUMERIC); + } else { + if (isset($this->lines[-1])) { + $struct = $this->lines[-1]; + } else { + $struct = $this->lines[-1] = $new_struct; + } + } + ksort($this->lines, SORT_NUMERIC); + + // Now, check if we need to operate on a lower structure + if (!empty($attr)) { + $struct = $struct->getChild(HTMLPurifier_ErrorStruct::ATTR, $attr); + if (!$struct->value) { + $struct->value = array($attr, 'PUT VALUE HERE'); + } + } + if (!empty($cssprop)) { + $struct = $struct->getChild(HTMLPurifier_ErrorStruct::CSSPROP, $cssprop); + if (!$struct->value) { + // if we tokenize CSS this might be a little more difficult to do + $struct->value = array($cssprop, 'PUT VALUE HERE'); + } + } + + // Ok, structs are all setup, now time to register the error + $struct->addError($severity, $msg); + } + + /** + * Retrieves raw error data for custom formatter to use + */ + public function getRaw() + { + return $this->errors; + } + + /** + * Default HTML formatting implementation for error messages + * @param HTMLPurifier_Config $config Configuration, vital for HTML output nature + * @param array $errors Errors array to display; used for recursion. + * @return string + */ + public function getHTMLFormatted($config, $errors = null) + { + $ret = array(); + + $this->generator = new HTMLPurifier_Generator($config, $this->context); + if ($errors === null) { + $errors = $this->errors; + } + + // 'At line' message needs to be removed + + // generation code for new structure goes here. It needs to be recursive. + foreach ($this->lines as $line => $col_array) { + if ($line == -1) { + continue; + } + foreach ($col_array as $col => $struct) { + $this->_renderStruct($ret, $struct, $line, $col); + } + } + if (isset($this->lines[-1])) { + $this->_renderStruct($ret, $this->lines[-1]); + } + + if (empty($errors)) { + return '

                  ' . $this->locale->getMessage('ErrorCollector: No errors') . '

                  '; + } else { + return '
                  • ' . implode('
                  • ', $ret) . '
                  '; + } + + } + + private function _renderStruct(&$ret, $struct, $line = null, $col = null) + { + $stack = array($struct); + $context_stack = array(array()); + while ($current = array_pop($stack)) { + $context = array_pop($context_stack); + foreach ($current->errors as $error) { + list($severity, $msg) = $error; + $string = ''; + $string .= '
                  '; + // W3C uses an icon to indicate the severity of the error. + $error = $this->locale->getErrorName($severity); + $string .= "$error "; + if (!is_null($line) && !is_null($col)) { + $string .= "Line $line, Column $col: "; + } else { + $string .= 'End of Document: '; + } + $string .= '' . $this->generator->escape($msg) . ' '; + $string .= '
                  '; + // Here, have a marker for the character on the column appropriate. + // Be sure to clip extremely long lines. + //$string .= '
                  ';
                  +                //$string .= '';
                  +                //$string .= '
                  '; + $ret[] = $string; + } + foreach ($current->children as $array) { + $context[] = $current; + $stack = array_merge($stack, array_reverse($array, true)); + for ($i = count($array); $i > 0; $i--) { + $context_stack[] = $context; + } + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorStruct.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorStruct.php new file mode 100644 index 0000000..cf869d3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorStruct.php @@ -0,0 +1,74 @@ +children[$type][$id])) { + $this->children[$type][$id] = new HTMLPurifier_ErrorStruct(); + $this->children[$type][$id]->type = $type; + } + return $this->children[$type][$id]; + } + + /** + * @param int $severity + * @param string $message + */ + public function addError($severity, $message) + { + $this->errors[] = array($severity, $message); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Exception.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Exception.php new file mode 100644 index 0000000..be85b4c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Exception.php @@ -0,0 +1,12 @@ +preFilter, + * 2->preFilter, 3->preFilter, purify, 3->postFilter, 2->postFilter, + * 1->postFilter. + * + * @note Methods are not declared abstract as it is perfectly legitimate + * for an implementation not to want anything to happen on a step + */ + +class HTMLPurifier_Filter +{ + + /** + * Name of the filter for identification purposes. + * @type string + */ + public $name; + + /** + * Pre-processor function, handles HTML before HTML Purifier + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function preFilter($html, $config, $context) + { + return $html; + } + + /** + * Post-processor function, handles HTML after HTML Purifier + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function postFilter($html, $config, $context) + { + return $html; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php new file mode 100644 index 0000000..66f70b0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php @@ -0,0 +1,341 @@ + blocks from input HTML, cleans them up + * using CSSTidy, and then places them in $purifier->context->get('StyleBlocks') + * so they can be used elsewhere in the document. + * + * @note + * See tests/HTMLPurifier/Filter/ExtractStyleBlocksTest.php for + * sample usage. + * + * @note + * This filter can also be used on stylesheets not included in the + * document--something purists would probably prefer. Just directly + * call HTMLPurifier_Filter_ExtractStyleBlocks->cleanCSS() + */ +class HTMLPurifier_Filter_ExtractStyleBlocks extends HTMLPurifier_Filter +{ + /** + * @type string + */ + public $name = 'ExtractStyleBlocks'; + + /** + * @type array + */ + private $_styleMatches = array(); + + /** + * @type csstidy + */ + private $_tidy; + + /** + * @type HTMLPurifier_AttrDef_HTML_ID + */ + private $_id_attrdef; + + /** + * @type HTMLPurifier_AttrDef_CSS_Ident + */ + private $_class_attrdef; + + /** + * @type HTMLPurifier_AttrDef_Enum + */ + private $_enum_attrdef; + + public function __construct() + { + $this->_tidy = new csstidy(); + $this->_tidy->set_cfg('lowercase_s', false); + $this->_id_attrdef = new HTMLPurifier_AttrDef_HTML_ID(true); + $this->_class_attrdef = new HTMLPurifier_AttrDef_CSS_Ident(); + $this->_enum_attrdef = new HTMLPurifier_AttrDef_Enum( + array( + 'first-child', + 'link', + 'visited', + 'active', + 'hover', + 'focus' + ) + ); + } + + /** + * Save the contents of CSS blocks to style matches + * @param array $matches preg_replace style $matches array + */ + protected function styleCallback($matches) + { + $this->_styleMatches[] = $matches[1]; + } + + /** + * Removes inline + // we must not grab foo in a font-family prop). + if ($config->get('Filter.ExtractStyleBlocks.Escaping')) { + $css = str_replace( + array('<', '>', '&'), + array('\3C ', '\3E ', '\26 '), + $css + ); + } + return $css; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php new file mode 100644 index 0000000..276d836 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php @@ -0,0 +1,65 @@ +]+>.+?' . + '(?:http:)?//www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+).+?#s'; + $pre_replace = '\1'; + return preg_replace($pre_regex, $pre_replace, $html); + } + + /** + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function postFilter($html, $config, $context) + { + $post_regex = '#((?:v|cp)/[A-Za-z0-9\-_=]+)#'; + return preg_replace_callback($post_regex, array($this, 'postFilterCallback'), $html); + } + + /** + * @param $url + * @return string + */ + protected function armorUrl($url) + { + return str_replace('--', '--', $url); + } + + /** + * @param array $matches + * @return string + */ + protected function postFilterCallback($matches) + { + $url = $this->armorUrl($matches[1]); + return '' . + '' . + '' . + ''; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php new file mode 100644 index 0000000..eb56e2d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php @@ -0,0 +1,286 @@ + tags. + * @type bool + */ + private $_scriptFix = false; + + /** + * Cache of HTMLDefinition during HTML output to determine whether or + * not attributes should be minimized. + * @type HTMLPurifier_HTMLDefinition + */ + private $_def; + + /** + * Cache of %Output.SortAttr. + * @type bool + */ + private $_sortAttr; + + /** + * Cache of %Output.FlashCompat. + * @type bool + */ + private $_flashCompat; + + /** + * Cache of %Output.FixInnerHTML. + * @type bool + */ + private $_innerHTMLFix; + + /** + * Stack for keeping track of object information when outputting IE + * compatibility code. + * @type array + */ + private $_flashStack = array(); + + /** + * Configuration for the generator + * @type HTMLPurifier_Config + */ + protected $config; + + /** + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + */ + public function __construct($config, $context) + { + $this->config = $config; + $this->_scriptFix = $config->get('Output.CommentScriptContents'); + $this->_innerHTMLFix = $config->get('Output.FixInnerHTML'); + $this->_sortAttr = $config->get('Output.SortAttr'); + $this->_flashCompat = $config->get('Output.FlashCompat'); + $this->_def = $config->getHTMLDefinition(); + $this->_xhtml = $this->_def->doctype->xml; + } + + /** + * Generates HTML from an array of tokens. + * @param HTMLPurifier_Token[] $tokens Array of HTMLPurifier_Token + * @return string Generated HTML + */ + public function generateFromTokens($tokens) + { + if (!$tokens) { + return ''; + } + + // Basic algorithm + $html = ''; + for ($i = 0, $size = count($tokens); $i < $size; $i++) { + if ($this->_scriptFix && $tokens[$i]->name === 'script' + && $i + 2 < $size && $tokens[$i+2] instanceof HTMLPurifier_Token_End) { + // script special case + // the contents of the script block must be ONE token + // for this to work. + $html .= $this->generateFromToken($tokens[$i++]); + $html .= $this->generateScriptFromToken($tokens[$i++]); + } + $html .= $this->generateFromToken($tokens[$i]); + } + + // Tidy cleanup + if (extension_loaded('tidy') && $this->config->get('Output.TidyFormat')) { + $tidy = new Tidy; + $tidy->parseString( + $html, + array( + 'indent'=> true, + 'output-xhtml' => $this->_xhtml, + 'show-body-only' => true, + 'indent-spaces' => 2, + 'wrap' => 68, + ), + 'utf8' + ); + $tidy->cleanRepair(); + $html = (string) $tidy; // explicit cast necessary + } + + // Normalize newlines to system defined value + if ($this->config->get('Core.NormalizeNewlines')) { + $nl = $this->config->get('Output.Newline'); + if ($nl === null) { + $nl = PHP_EOL; + } + if ($nl !== "\n") { + $html = str_replace("\n", $nl, $html); + } + } + return $html; + } + + /** + * Generates HTML from a single token. + * @param HTMLPurifier_Token $token HTMLPurifier_Token object. + * @return string Generated HTML + */ + public function generateFromToken($token) + { + if (!$token instanceof HTMLPurifier_Token) { + trigger_error('Cannot generate HTML from non-HTMLPurifier_Token object', E_USER_WARNING); + return ''; + + } elseif ($token instanceof HTMLPurifier_Token_Start) { + $attr = $this->generateAttributes($token->attr, $token->name); + if ($this->_flashCompat) { + if ($token->name == "object") { + $flash = new stdClass(); + $flash->attr = $token->attr; + $flash->param = array(); + $this->_flashStack[] = $flash; + } + } + return '<' . $token->name . ($attr ? ' ' : '') . $attr . '>'; + + } elseif ($token instanceof HTMLPurifier_Token_End) { + $_extra = ''; + if ($this->_flashCompat) { + if ($token->name == "object" && !empty($this->_flashStack)) { + // doesn't do anything for now + } + } + return $_extra . 'name . '>'; + + } elseif ($token instanceof HTMLPurifier_Token_Empty) { + if ($this->_flashCompat && $token->name == "param" && !empty($this->_flashStack)) { + $this->_flashStack[count($this->_flashStack)-1]->param[$token->attr['name']] = $token->attr['value']; + } + $attr = $this->generateAttributes($token->attr, $token->name); + return '<' . $token->name . ($attr ? ' ' : '') . $attr . + ( $this->_xhtml ? ' /': '' ) //
                  v.
                  + . '>'; + + } elseif ($token instanceof HTMLPurifier_Token_Text) { + return $this->escape($token->data, ENT_NOQUOTES); + + } elseif ($token instanceof HTMLPurifier_Token_Comment) { + return ''; + } else { + return ''; + + } + } + + /** + * Special case processor for the contents of script tags + * @param HTMLPurifier_Token $token HTMLPurifier_Token object. + * @return string + * @warning This runs into problems if there's already a literal + * --> somewhere inside the script contents. + */ + public function generateScriptFromToken($token) + { + if (!$token instanceof HTMLPurifier_Token_Text) { + return $this->generateFromToken($token); + } + // Thanks + $data = preg_replace('#//\s*$#', '', $token->data); + return ''; + } + + /** + * Generates attribute declarations from attribute array. + * @note This does not include the leading or trailing space. + * @param array $assoc_array_of_attributes Attribute array + * @param string $element Name of element attributes are for, used to check + * attribute minimization. + * @return string Generated HTML fragment for insertion. + */ + public function generateAttributes($assoc_array_of_attributes, $element = '') + { + $html = ''; + if ($this->_sortAttr) { + ksort($assoc_array_of_attributes); + } + foreach ($assoc_array_of_attributes as $key => $value) { + if (!$this->_xhtml) { + // Remove namespaced attributes + if (strpos($key, ':') !== false) { + continue; + } + // Check if we should minimize the attribute: val="val" -> val + if ($element && !empty($this->_def->info[$element]->attr[$key]->minimized)) { + $html .= $key . ' '; + continue; + } + } + // Workaround for Internet Explorer innerHTML bug. + // Essentially, Internet Explorer, when calculating + // innerHTML, omits quotes if there are no instances of + // angled brackets, quotes or spaces. However, when parsing + // HTML (for example, when you assign to innerHTML), it + // treats backticks as quotes. Thus, + // `` + // becomes + // `` + // becomes + // + // Fortunately, all we need to do is trigger an appropriate + // quoting style, which we do by adding an extra space. + // This also is consistent with the W3C spec, which states + // that user agents may ignore leading or trailing + // whitespace (in fact, most don't, at least for attributes + // like alt, but an extra space at the end is barely + // noticeable). Still, we have a configuration knob for + // this, since this transformation is not necesary if you + // don't process user input with innerHTML or you don't plan + // on supporting Internet Explorer. + if ($this->_innerHTMLFix) { + if (strpos($value, '`') !== false) { + // check if correct quoting style would not already be + // triggered + if (strcspn($value, '"\' <>') === strlen($value)) { + // protect! + $value .= ' '; + } + } + } + $html .= $key.'="'.$this->escape($value).'" '; + } + return rtrim($html); + } + + /** + * Escapes raw text data. + * @todo This really ought to be protected, but until we have a facility + * for properly generating HTML here w/o using tokens, it stays + * public. + * @param string $string String data to escape for HTML. + * @param int $quote Quoting style, like htmlspecialchars. ENT_NOQUOTES is + * permissible for non-attribute output. + * @return string escaped data. + */ + public function escape($string, $quote = null) + { + // Workaround for APC bug on Mac Leopard reported by sidepodcast + // http://htmlpurifier.org/phorum/read.php?3,4823,4846 + if ($quote === null) { + $quote = ENT_COMPAT; + } + return htmlspecialchars($string, $quote, 'UTF-8'); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php new file mode 100644 index 0000000..9b7b334 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php @@ -0,0 +1,493 @@ +getAnonymousModule(); + if (!isset($module->info[$element_name])) { + $element = $module->addBlankElement($element_name); + } else { + $element = $module->info[$element_name]; + } + $element->attr[$attr_name] = $def; + } + + /** + * Adds a custom element to your HTML definition + * @see HTMLPurifier_HTMLModule::addElement() for detailed + * parameter and return value descriptions. + */ + public function addElement($element_name, $type, $contents, $attr_collections, $attributes = array()) + { + $module = $this->getAnonymousModule(); + // assume that if the user is calling this, the element + // is safe. This may not be a good idea + $element = $module->addElement($element_name, $type, $contents, $attr_collections, $attributes); + return $element; + } + + /** + * Adds a blank element to your HTML definition, for overriding + * existing behavior + * @param string $element_name + * @return HTMLPurifier_ElementDef + * @see HTMLPurifier_HTMLModule::addBlankElement() for detailed + * parameter and return value descriptions. + */ + public function addBlankElement($element_name) + { + $module = $this->getAnonymousModule(); + $element = $module->addBlankElement($element_name); + return $element; + } + + /** + * Retrieves a reference to the anonymous module, so you can + * bust out advanced features without having to make your own + * module. + * @return HTMLPurifier_HTMLModule + */ + public function getAnonymousModule() + { + if (!$this->_anonModule) { + $this->_anonModule = new HTMLPurifier_HTMLModule(); + $this->_anonModule->name = 'Anonymous'; + } + return $this->_anonModule; + } + + private $_anonModule = null; + + // PUBLIC BUT INTERNAL VARIABLES -------------------------------------- + + /** + * @type string + */ + public $type = 'HTML'; + + /** + * @type HTMLPurifier_HTMLModuleManager + */ + public $manager; + + /** + * Performs low-cost, preliminary initialization. + */ + public function __construct() + { + $this->manager = new HTMLPurifier_HTMLModuleManager(); + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetup($config) + { + $this->processModules($config); + $this->setupConfigStuff($config); + unset($this->manager); + + // cleanup some of the element definitions + foreach ($this->info as $k => $v) { + unset($this->info[$k]->content_model); + unset($this->info[$k]->content_model_type); + } + } + + /** + * Extract out the information from the manager + * @param HTMLPurifier_Config $config + */ + protected function processModules($config) + { + if ($this->_anonModule) { + // for user specific changes + // this is late-loaded so we don't have to deal with PHP4 + // reference wonky-ness + $this->manager->addModule($this->_anonModule); + unset($this->_anonModule); + } + + $this->manager->setup($config); + $this->doctype = $this->manager->doctype; + + foreach ($this->manager->modules as $module) { + foreach ($module->info_tag_transform as $k => $v) { + if ($v === false) { + unset($this->info_tag_transform[$k]); + } else { + $this->info_tag_transform[$k] = $v; + } + } + foreach ($module->info_attr_transform_pre as $k => $v) { + if ($v === false) { + unset($this->info_attr_transform_pre[$k]); + } else { + $this->info_attr_transform_pre[$k] = $v; + } + } + foreach ($module->info_attr_transform_post as $k => $v) { + if ($v === false) { + unset($this->info_attr_transform_post[$k]); + } else { + $this->info_attr_transform_post[$k] = $v; + } + } + foreach ($module->info_injector as $k => $v) { + if ($v === false) { + unset($this->info_injector[$k]); + } else { + $this->info_injector[$k] = $v; + } + } + } + $this->info = $this->manager->getElements(); + $this->info_content_sets = $this->manager->contentSets->lookup; + } + + /** + * Sets up stuff based on config. We need a better way of doing this. + * @param HTMLPurifier_Config $config + */ + protected function setupConfigStuff($config) + { + $block_wrapper = $config->get('HTML.BlockWrapper'); + if (isset($this->info_content_sets['Block'][$block_wrapper])) { + $this->info_block_wrapper = $block_wrapper; + } else { + trigger_error( + 'Cannot use non-block element as block wrapper', + E_USER_ERROR + ); + } + + $parent = $config->get('HTML.Parent'); + $def = $this->manager->getElement($parent, true); + if ($def) { + $this->info_parent = $parent; + $this->info_parent_def = $def; + } else { + trigger_error( + 'Cannot use unrecognized element as parent', + E_USER_ERROR + ); + $this->info_parent_def = $this->manager->getElement($this->info_parent, true); + } + + // support template text + $support = "(for information on implementing this, see the support forums) "; + + // setup allowed elements ----------------------------------------- + + $allowed_elements = $config->get('HTML.AllowedElements'); + $allowed_attributes = $config->get('HTML.AllowedAttributes'); // retrieve early + + if (!is_array($allowed_elements) && !is_array($allowed_attributes)) { + $allowed = $config->get('HTML.Allowed'); + if (is_string($allowed)) { + list($allowed_elements, $allowed_attributes) = $this->parseTinyMCEAllowedList($allowed); + } + } + + if (is_array($allowed_elements)) { + foreach ($this->info as $name => $d) { + if (!isset($allowed_elements[$name])) { + unset($this->info[$name]); + } + unset($allowed_elements[$name]); + } + // emit errors + foreach ($allowed_elements as $element => $d) { + $element = htmlspecialchars($element); // PHP doesn't escape errors, be careful! + trigger_error("Element '$element' is not supported $support", E_USER_WARNING); + } + } + + // setup allowed attributes --------------------------------------- + + $allowed_attributes_mutable = $allowed_attributes; // by copy! + if (is_array($allowed_attributes)) { + // This actually doesn't do anything, since we went away from + // global attributes. It's possible that userland code uses + // it, but HTMLModuleManager doesn't! + foreach ($this->info_global_attr as $attr => $x) { + $keys = array($attr, "*@$attr", "*.$attr"); + $delete = true; + foreach ($keys as $key) { + if ($delete && isset($allowed_attributes[$key])) { + $delete = false; + } + if (isset($allowed_attributes_mutable[$key])) { + unset($allowed_attributes_mutable[$key]); + } + } + if ($delete) { + unset($this->info_global_attr[$attr]); + } + } + + foreach ($this->info as $tag => $info) { + foreach ($info->attr as $attr => $x) { + $keys = array("$tag@$attr", $attr, "*@$attr", "$tag.$attr", "*.$attr"); + $delete = true; + foreach ($keys as $key) { + if ($delete && isset($allowed_attributes[$key])) { + $delete = false; + } + if (isset($allowed_attributes_mutable[$key])) { + unset($allowed_attributes_mutable[$key]); + } + } + if ($delete) { + if ($this->info[$tag]->attr[$attr]->required) { + trigger_error( + "Required attribute '$attr' in element '$tag' " . + "was not allowed, which means '$tag' will not be allowed either", + E_USER_WARNING + ); + } + unset($this->info[$tag]->attr[$attr]); + } + } + } + // emit errors + foreach ($allowed_attributes_mutable as $elattr => $d) { + $bits = preg_split('/[.@]/', $elattr, 2); + $c = count($bits); + switch ($c) { + case 2: + if ($bits[0] !== '*') { + $element = htmlspecialchars($bits[0]); + $attribute = htmlspecialchars($bits[1]); + if (!isset($this->info[$element])) { + trigger_error( + "Cannot allow attribute '$attribute' if element " . + "'$element' is not allowed/supported $support" + ); + } else { + trigger_error( + "Attribute '$attribute' in element '$element' not supported $support", + E_USER_WARNING + ); + } + break; + } + // otherwise fall through + case 1: + $attribute = htmlspecialchars($bits[0]); + trigger_error( + "Global attribute '$attribute' is not ". + "supported in any elements $support", + E_USER_WARNING + ); + break; + } + } + } + + // setup forbidden elements --------------------------------------- + + $forbidden_elements = $config->get('HTML.ForbiddenElements'); + $forbidden_attributes = $config->get('HTML.ForbiddenAttributes'); + + foreach ($this->info as $tag => $info) { + if (isset($forbidden_elements[$tag])) { + unset($this->info[$tag]); + continue; + } + foreach ($info->attr as $attr => $x) { + if (isset($forbidden_attributes["$tag@$attr"]) || + isset($forbidden_attributes["*@$attr"]) || + isset($forbidden_attributes[$attr]) + ) { + unset($this->info[$tag]->attr[$attr]); + continue; + } elseif (isset($forbidden_attributes["$tag.$attr"])) { // this segment might get removed eventually + // $tag.$attr are not user supplied, so no worries! + trigger_error( + "Error with $tag.$attr: tag.attr syntax not supported for " . + "HTML.ForbiddenAttributes; use tag@attr instead", + E_USER_WARNING + ); + } + } + } + foreach ($forbidden_attributes as $key => $v) { + if (strlen($key) < 2) { + continue; + } + if ($key[0] != '*') { + continue; + } + if ($key[1] == '.') { + trigger_error( + "Error with $key: *.attr syntax not supported for HTML.ForbiddenAttributes; use attr instead", + E_USER_WARNING + ); + } + } + + // setup injectors ----------------------------------------------------- + foreach ($this->info_injector as $i => $injector) { + if ($injector->checkNeeded($config) !== false) { + // remove injector that does not have it's required + // elements/attributes present, and is thus not needed. + unset($this->info_injector[$i]); + } + } + } + + /** + * Parses a TinyMCE-flavored Allowed Elements and Attributes list into + * separate lists for processing. Format is element[attr1|attr2],element2... + * @warning Although it's largely drawn from TinyMCE's implementation, + * it is different, and you'll probably have to modify your lists + * @param array $list String list to parse + * @return array + * @todo Give this its own class, probably static interface + */ + public function parseTinyMCEAllowedList($list) + { + $list = str_replace(array(' ', "\t"), '', $list); + + $elements = array(); + $attributes = array(); + + $chunks = preg_split('/(,|[\n\r]+)/', $list); + foreach ($chunks as $chunk) { + if (empty($chunk)) { + continue; + } + // remove TinyMCE element control characters + if (!strpos($chunk, '[')) { + $element = $chunk; + $attr = false; + } else { + list($element, $attr) = explode('[', $chunk); + } + if ($element !== '*') { + $elements[$element] = true; + } + if (!$attr) { + continue; + } + $attr = substr($attr, 0, strlen($attr) - 1); // remove trailing ] + $attr = explode('|', $attr); + foreach ($attr as $key) { + $attributes["$element.$key"] = true; + } + } + return array($elements, $attributes); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule.php new file mode 100644 index 0000000..bb3a923 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule.php @@ -0,0 +1,284 @@ +info, since the object's data is only info, + * with extra behavior associated with it. + * @type array + */ + public $attr_collections = array(); + + /** + * Associative array of deprecated tag name to HTMLPurifier_TagTransform. + * @type array + */ + public $info_tag_transform = array(); + + /** + * List of HTMLPurifier_AttrTransform to be performed before validation. + * @type array + */ + public $info_attr_transform_pre = array(); + + /** + * List of HTMLPurifier_AttrTransform to be performed after validation. + * @type array + */ + public $info_attr_transform_post = array(); + + /** + * List of HTMLPurifier_Injector to be performed during well-formedness fixing. + * An injector will only be invoked if all of it's pre-requisites are met; + * if an injector fails setup, there will be no error; it will simply be + * silently disabled. + * @type array + */ + public $info_injector = array(); + + /** + * Boolean flag that indicates whether or not getChildDef is implemented. + * For optimization reasons: may save a call to a function. Be sure + * to set it if you do implement getChildDef(), otherwise it will have + * no effect! + * @type bool + */ + public $defines_child_def = false; + + /** + * Boolean flag whether or not this module is safe. If it is not safe, all + * of its members are unsafe. Modules are safe by default (this might be + * slightly dangerous, but it doesn't make much sense to force HTML Purifier, + * which is based off of safe HTML, to explicitly say, "This is safe," even + * though there are modules which are "unsafe") + * + * @type bool + * @note Previously, safety could be applied at an element level granularity. + * We've removed this ability, so in order to add "unsafe" elements + * or attributes, a dedicated module with this property set to false + * must be used. + */ + public $safe = true; + + /** + * Retrieves a proper HTMLPurifier_ChildDef subclass based on + * content_model and content_model_type member variables of + * the HTMLPurifier_ElementDef class. There is a similar function + * in HTMLPurifier_HTMLDefinition. + * @param HTMLPurifier_ElementDef $def + * @return HTMLPurifier_ChildDef subclass + */ + public function getChildDef($def) + { + return false; + } + + // -- Convenience ----------------------------------------------------- + + /** + * Convenience function that sets up a new element + * @param string $element Name of element to add + * @param string|bool $type What content set should element be registered to? + * Set as false to skip this step. + * @param string $contents Allowed children in form of: + * "$content_model_type: $content_model" + * @param array $attr_includes What attribute collections to register to + * element? + * @param array $attr What unique attributes does the element define? + * @see HTMLPurifier_ElementDef:: for in-depth descriptions of these parameters. + * @return HTMLPurifier_ElementDef Created element definition object, so you + * can set advanced parameters + */ + public function addElement($element, $type, $contents, $attr_includes = array(), $attr = array()) + { + $this->elements[] = $element; + // parse content_model + list($content_model_type, $content_model) = $this->parseContents($contents); + // merge in attribute inclusions + $this->mergeInAttrIncludes($attr, $attr_includes); + // add element to content sets + if ($type) { + $this->addElementToContentSet($element, $type); + } + // create element + $this->info[$element] = HTMLPurifier_ElementDef::create( + $content_model, + $content_model_type, + $attr + ); + // literal object $contents means direct child manipulation + if (!is_string($contents)) { + $this->info[$element]->child = $contents; + } + return $this->info[$element]; + } + + /** + * Convenience function that creates a totally blank, non-standalone + * element. + * @param string $element Name of element to create + * @return HTMLPurifier_ElementDef Created element + */ + public function addBlankElement($element) + { + if (!isset($this->info[$element])) { + $this->elements[] = $element; + $this->info[$element] = new HTMLPurifier_ElementDef(); + $this->info[$element]->standalone = false; + } else { + trigger_error("Definition for $element already exists in module, cannot redefine"); + } + return $this->info[$element]; + } + + /** + * Convenience function that registers an element to a content set + * @param string $element Element to register + * @param string $type Name content set (warning: case sensitive, usually upper-case + * first letter) + */ + public function addElementToContentSet($element, $type) + { + if (!isset($this->content_sets[$type])) { + $this->content_sets[$type] = ''; + } else { + $this->content_sets[$type] .= ' | '; + } + $this->content_sets[$type] .= $element; + } + + /** + * Convenience function that transforms single-string contents + * into separate content model and content model type + * @param string $contents Allowed children in form of: + * "$content_model_type: $content_model" + * @return array + * @note If contents is an object, an array of two nulls will be + * returned, and the callee needs to take the original $contents + * and use it directly. + */ + public function parseContents($contents) + { + if (!is_string($contents)) { + return array(null, null); + } // defer + switch ($contents) { + // check for shorthand content model forms + case 'Empty': + return array('empty', ''); + case 'Inline': + return array('optional', 'Inline | #PCDATA'); + case 'Flow': + return array('optional', 'Flow | #PCDATA'); + } + list($content_model_type, $content_model) = explode(':', $contents); + $content_model_type = strtolower(trim($content_model_type)); + $content_model = trim($content_model); + return array($content_model_type, $content_model); + } + + /** + * Convenience function that merges a list of attribute includes into + * an attribute array. + * @param array $attr Reference to attr array to modify + * @param array $attr_includes Array of includes / string include to merge in + */ + public function mergeInAttrIncludes(&$attr, $attr_includes) + { + if (!is_array($attr_includes)) { + if (empty($attr_includes)) { + $attr_includes = array(); + } else { + $attr_includes = array($attr_includes); + } + } + $attr[0] = $attr_includes; + } + + /** + * Convenience function that generates a lookup table with boolean + * true as value. + * @param string $list List of values to turn into a lookup + * @note You can also pass an arbitrary number of arguments in + * place of the regular argument + * @return array array equivalent of list + */ + public function makeLookup($list) + { + if (is_string($list)) { + $list = func_get_args(); + } + $ret = array(); + foreach ($list as $value) { + if (is_null($value)) { + continue; + } + $ret[$value] = true; + } + return $ret; + } + + /** + * Lazy load construction of the module after determining whether + * or not it's needed, and also when a finalized configuration object + * is available. + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Bdo.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Bdo.php new file mode 100644 index 0000000..1e67c79 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Bdo.php @@ -0,0 +1,44 @@ + array('dir' => false) + ); + + /** + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + $bdo = $this->addElement( + 'bdo', + 'Inline', + 'Inline', + array('Core', 'Lang'), + array( + 'dir' => 'Enum#ltr,rtl', // required + // The Abstract Module specification has the attribute + // inclusions wrong for bdo: bdo allows Lang + ) + ); + $bdo->attr_transform_post[] = new HTMLPurifier_AttrTransform_BdoDir(); + + $this->attr_collections['I18N']['dir'] = 'Enum#ltr,rtl'; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/CommonAttributes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/CommonAttributes.php new file mode 100644 index 0000000..a96ab1b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/CommonAttributes.php @@ -0,0 +1,31 @@ + array( + 0 => array('Style'), + // 'xml:space' => false, + 'class' => 'Class', + 'id' => 'ID', + 'title' => 'CDATA', + ), + 'Lang' => array(), + 'I18N' => array( + 0 => array('Lang'), // proprietary, for xml:lang/lang + ), + 'Common' => array( + 0 => array('Core', 'I18N') + ) + ); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php new file mode 100644 index 0000000..a9042a3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php @@ -0,0 +1,55 @@ + 'URI', + // 'datetime' => 'Datetime', // not implemented + ); + $this->addElement('del', 'Inline', $contents, 'Common', $attr); + $this->addElement('ins', 'Inline', $contents, 'Common', $attr); + } + + // HTML 4.01 specifies that ins/del must not contain block + // elements when used in an inline context, chameleon is + // a complicated workaround to acheive this effect + + // Inline context ! Block context (exclamation mark is + // separator, see getChildDef for parsing) + + /** + * @type bool + */ + public $defines_child_def = true; + + /** + * @param HTMLPurifier_ElementDef $def + * @return HTMLPurifier_ChildDef_Chameleon + */ + public function getChildDef($def) + { + if ($def->content_model_type != 'chameleon') { + return false; + } + $value = explode('!', $def->content_model); + return new HTMLPurifier_ChildDef_Chameleon($value[0], $value[1]); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Forms.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Forms.php new file mode 100644 index 0000000..6f7ddbc --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Forms.php @@ -0,0 +1,190 @@ + 'Form', + 'Inline' => 'Formctrl', + ); + + /** + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + $form = $this->addElement( + 'form', + 'Form', + 'Required: Heading | List | Block | fieldset', + 'Common', + array( + 'accept' => 'ContentTypes', + 'accept-charset' => 'Charsets', + 'action*' => 'URI', + 'method' => 'Enum#get,post', + // really ContentType, but these two are the only ones used today + 'enctype' => 'Enum#application/x-www-form-urlencoded,multipart/form-data', + ) + ); + $form->excludes = array('form' => true); + + $input = $this->addElement( + 'input', + 'Formctrl', + 'Empty', + 'Common', + array( + 'accept' => 'ContentTypes', + 'accesskey' => 'Character', + 'alt' => 'Text', + 'checked' => 'Bool#checked', + 'disabled' => 'Bool#disabled', + 'maxlength' => 'Number', + 'name' => 'CDATA', + 'readonly' => 'Bool#readonly', + 'size' => 'Number', + 'src' => 'URI#embedded', + 'tabindex' => 'Number', + 'type' => 'Enum#text,password,checkbox,button,radio,submit,reset,file,hidden,image', + 'value' => 'CDATA', + ) + ); + $input->attr_transform_post[] = new HTMLPurifier_AttrTransform_Input(); + + $this->addElement( + 'select', + 'Formctrl', + 'Required: optgroup | option', + 'Common', + array( + 'disabled' => 'Bool#disabled', + 'multiple' => 'Bool#multiple', + 'name' => 'CDATA', + 'size' => 'Number', + 'tabindex' => 'Number', + ) + ); + + $this->addElement( + 'option', + false, + 'Optional: #PCDATA', + 'Common', + array( + 'disabled' => 'Bool#disabled', + 'label' => 'Text', + 'selected' => 'Bool#selected', + 'value' => 'CDATA', + ) + ); + // It's illegal for there to be more than one selected, but not + // be multiple. Also, no selected means undefined behavior. This might + // be difficult to implement; perhaps an injector, or a context variable. + + $textarea = $this->addElement( + 'textarea', + 'Formctrl', + 'Optional: #PCDATA', + 'Common', + array( + 'accesskey' => 'Character', + 'cols*' => 'Number', + 'disabled' => 'Bool#disabled', + 'name' => 'CDATA', + 'readonly' => 'Bool#readonly', + 'rows*' => 'Number', + 'tabindex' => 'Number', + ) + ); + $textarea->attr_transform_pre[] = new HTMLPurifier_AttrTransform_Textarea(); + + $button = $this->addElement( + 'button', + 'Formctrl', + 'Optional: #PCDATA | Heading | List | Block | Inline', + 'Common', + array( + 'accesskey' => 'Character', + 'disabled' => 'Bool#disabled', + 'name' => 'CDATA', + 'tabindex' => 'Number', + 'type' => 'Enum#button,submit,reset', + 'value' => 'CDATA', + ) + ); + + // For exclusions, ideally we'd specify content sets, not literal elements + $button->excludes = $this->makeLookup( + 'form', + 'fieldset', // Form + 'input', + 'select', + 'textarea', + 'label', + 'button', // Formctrl + 'a', // as per HTML 4.01 spec, this is omitted by modularization + 'isindex', + 'iframe' // legacy items + ); + + // Extra exclusion: img usemap="" is not permitted within this element. + // We'll omit this for now, since we don't have any good way of + // indicating it yet. + + // This is HIGHLY user-unfriendly; we need a custom child-def for this + $this->addElement('fieldset', 'Form', 'Custom: (#WS?,legend,(Flow|#PCDATA)*)', 'Common'); + + $label = $this->addElement( + 'label', + 'Formctrl', + 'Optional: #PCDATA | Inline', + 'Common', + array( + 'accesskey' => 'Character', + // 'for' => 'IDREF', // IDREF not implemented, cannot allow + ) + ); + $label->excludes = array('label' => true); + + $this->addElement( + 'legend', + false, + 'Optional: #PCDATA | Inline', + 'Common', + array( + 'accesskey' => 'Character', + ) + ); + + $this->addElement( + 'optgroup', + false, + 'Required: option', + 'Common', + array( + 'disabled' => 'Bool#disabled', + 'label*' => 'Text', + ) + ); + // Don't forget an injector for . This one's a little complex + // because it maps to multiple elements. + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Hypertext.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Hypertext.php new file mode 100644 index 0000000..72d7a31 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Hypertext.php @@ -0,0 +1,40 @@ +addElement( + 'a', + 'Inline', + 'Inline', + 'Common', + array( + // 'accesskey' => 'Character', + // 'charset' => 'Charset', + 'href' => 'URI', + // 'hreflang' => 'LanguageCode', + 'rel' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rel'), + 'rev' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rev'), + // 'tabindex' => 'Number', + // 'type' => 'ContentType', + ) + ); + $a->formatting = true; + $a->excludes = array('a' => true); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php new file mode 100644 index 0000000..f7e7c91 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php @@ -0,0 +1,51 @@ +get('HTML.SafeIframe')) { + $this->safe = true; + } + $this->addElement( + 'iframe', + 'Inline', + 'Flow', + 'Common', + array( + 'src' => 'URI#embedded', + 'width' => 'Length', + 'height' => 'Length', + 'name' => 'ID', + 'scrolling' => 'Enum#yes,no,auto', + 'frameborder' => 'Enum#0,1', + 'longdesc' => 'URI', + 'marginheight' => 'Pixels', + 'marginwidth' => 'Pixels', + ) + ); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Image.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Image.php new file mode 100644 index 0000000..0f5fdb3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Image.php @@ -0,0 +1,49 @@ +get('HTML.MaxImgLength'); + $img = $this->addElement( + 'img', + 'Inline', + 'Empty', + 'Common', + array( + 'alt*' => 'Text', + // According to the spec, it's Length, but percents can + // be abused, so we allow only Pixels. + 'height' => 'Pixels#' . $max, + 'width' => 'Pixels#' . $max, + 'longdesc' => 'URI', + 'src*' => new HTMLPurifier_AttrDef_URI(true), // embedded + ) + ); + if ($max === null || $config->get('HTML.Trusted')) { + $img->attr['height'] = + $img->attr['width'] = 'Length'; + } + + // kind of strange, but splitting things up would be inefficient + $img->attr_transform_pre[] = + $img->attr_transform_post[] = + new HTMLPurifier_AttrTransform_ImgRequired(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Legacy.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Legacy.php new file mode 100644 index 0000000..86b5299 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Legacy.php @@ -0,0 +1,186 @@ +addElement( + 'basefont', + 'Inline', + 'Empty', + null, + array( + 'color' => 'Color', + 'face' => 'Text', // extremely broad, we should + 'size' => 'Text', // tighten it + 'id' => 'ID' + ) + ); + $this->addElement('center', 'Block', 'Flow', 'Common'); + $this->addElement( + 'dir', + 'Block', + 'Required: li', + 'Common', + array( + 'compact' => 'Bool#compact' + ) + ); + $this->addElement( + 'font', + 'Inline', + 'Inline', + array('Core', 'I18N'), + array( + 'color' => 'Color', + 'face' => 'Text', // extremely broad, we should + 'size' => 'Text', // tighten it + ) + ); + $this->addElement( + 'menu', + 'Block', + 'Required: li', + 'Common', + array( + 'compact' => 'Bool#compact' + ) + ); + + $s = $this->addElement('s', 'Inline', 'Inline', 'Common'); + $s->formatting = true; + + $strike = $this->addElement('strike', 'Inline', 'Inline', 'Common'); + $strike->formatting = true; + + $u = $this->addElement('u', 'Inline', 'Inline', 'Common'); + $u->formatting = true; + + // setup modifications to old elements + + $align = 'Enum#left,right,center,justify'; + + $address = $this->addBlankElement('address'); + $address->content_model = 'Inline | #PCDATA | p'; + $address->content_model_type = 'optional'; + $address->child = false; + + $blockquote = $this->addBlankElement('blockquote'); + $blockquote->content_model = 'Flow | #PCDATA'; + $blockquote->content_model_type = 'optional'; + $blockquote->child = false; + + $br = $this->addBlankElement('br'); + $br->attr['clear'] = 'Enum#left,all,right,none'; + + $caption = $this->addBlankElement('caption'); + $caption->attr['align'] = 'Enum#top,bottom,left,right'; + + $div = $this->addBlankElement('div'); + $div->attr['align'] = $align; + + $dl = $this->addBlankElement('dl'); + $dl->attr['compact'] = 'Bool#compact'; + + for ($i = 1; $i <= 6; $i++) { + $h = $this->addBlankElement("h$i"); + $h->attr['align'] = $align; + } + + $hr = $this->addBlankElement('hr'); + $hr->attr['align'] = $align; + $hr->attr['noshade'] = 'Bool#noshade'; + $hr->attr['size'] = 'Pixels'; + $hr->attr['width'] = 'Length'; + + $img = $this->addBlankElement('img'); + $img->attr['align'] = 'IAlign'; + $img->attr['border'] = 'Pixels'; + $img->attr['hspace'] = 'Pixels'; + $img->attr['vspace'] = 'Pixels'; + + // figure out this integer business + + $li = $this->addBlankElement('li'); + $li->attr['value'] = new HTMLPurifier_AttrDef_Integer(); + $li->attr['type'] = 'Enum#s:1,i,I,a,A,disc,square,circle'; + + $ol = $this->addBlankElement('ol'); + $ol->attr['compact'] = 'Bool#compact'; + $ol->attr['start'] = new HTMLPurifier_AttrDef_Integer(); + $ol->attr['type'] = 'Enum#s:1,i,I,a,A'; + + $p = $this->addBlankElement('p'); + $p->attr['align'] = $align; + + $pre = $this->addBlankElement('pre'); + $pre->attr['width'] = 'Number'; + + // script omitted + + $table = $this->addBlankElement('table'); + $table->attr['align'] = 'Enum#left,center,right'; + $table->attr['bgcolor'] = 'Color'; + + $tr = $this->addBlankElement('tr'); + $tr->attr['bgcolor'] = 'Color'; + + $th = $this->addBlankElement('th'); + $th->attr['bgcolor'] = 'Color'; + $th->attr['height'] = 'Length'; + $th->attr['nowrap'] = 'Bool#nowrap'; + $th->attr['width'] = 'Length'; + + $td = $this->addBlankElement('td'); + $td->attr['bgcolor'] = 'Color'; + $td->attr['height'] = 'Length'; + $td->attr['nowrap'] = 'Bool#nowrap'; + $td->attr['width'] = 'Length'; + + $ul = $this->addBlankElement('ul'); + $ul->attr['compact'] = 'Bool#compact'; + $ul->attr['type'] = 'Enum#square,disc,circle'; + + // "safe" modifications to "unsafe" elements + // WARNING: If you want to add support for an unsafe, legacy + // attribute, make a new TrustedLegacy module with the trusted + // bit set appropriately + + $form = $this->addBlankElement('form'); + $form->content_model = 'Flow | #PCDATA'; + $form->content_model_type = 'optional'; + $form->attr['target'] = 'FrameTarget'; + + $input = $this->addBlankElement('input'); + $input->attr['align'] = 'IAlign'; + + $legend = $this->addBlankElement('legend'); + $legend->attr['align'] = 'LAlign'; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/List.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/List.php new file mode 100644 index 0000000..7a20ff7 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/List.php @@ -0,0 +1,51 @@ + 'List'); + + /** + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + $ol = $this->addElement('ol', 'List', new HTMLPurifier_ChildDef_List(), 'Common'); + $ul = $this->addElement('ul', 'List', new HTMLPurifier_ChildDef_List(), 'Common'); + // XXX The wrap attribute is handled by MakeWellFormed. This is all + // quite unsatisfactory, because we generated this + // *specifically* for lists, and now a big chunk of the handling + // is done properly by the List ChildDef. So actually, we just + // want enough information to make autoclosing work properly, + // and then hand off the tricky stuff to the ChildDef. + $ol->wrap = 'li'; + $ul->wrap = 'li'; + $this->addElement('dl', 'List', 'Required: dt | dd', 'Common'); + + $this->addElement('li', false, 'Flow', 'Common'); + + $this->addElement('dd', false, 'Flow', 'Common'); + $this->addElement('dt', false, 'Inline', 'Common'); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Name.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Name.php new file mode 100644 index 0000000..60c0545 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Name.php @@ -0,0 +1,26 @@ +addBlankElement($name); + $element->attr['name'] = 'CDATA'; + if (!$config->get('HTML.Attr.Name.UseCDATA')) { + $element->attr_transform_post[] = new HTMLPurifier_AttrTransform_NameSync(); + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Nofollow.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Nofollow.php new file mode 100644 index 0000000..dc9410a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Nofollow.php @@ -0,0 +1,25 @@ +addBlankElement('a'); + $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_Nofollow(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php new file mode 100644 index 0000000..da72225 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php @@ -0,0 +1,20 @@ + array( + 'lang' => 'LanguageCode', + ) + ); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Object.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Object.php new file mode 100644 index 0000000..2f9efc5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Object.php @@ -0,0 +1,62 @@ + to cater to legacy browsers: this + * module does not allow this sort of behavior + */ +class HTMLPurifier_HTMLModule_Object extends HTMLPurifier_HTMLModule +{ + /** + * @type string + */ + public $name = 'Object'; + + /** + * @type bool + */ + public $safe = false; + + /** + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + $this->addElement( + 'object', + 'Inline', + 'Optional: #PCDATA | Flow | param', + 'Common', + array( + 'archive' => 'URI', + 'classid' => 'URI', + 'codebase' => 'URI', + 'codetype' => 'Text', + 'data' => 'URI', + 'declare' => 'Bool#declare', + 'height' => 'Length', + 'name' => 'CDATA', + 'standby' => 'Text', + 'tabindex' => 'Number', + 'type' => 'ContentType', + 'width' => 'Length' + ) + ); + + $this->addElement( + 'param', + false, + 'Empty', + null, + array( + 'id' => 'ID', + 'name*' => 'Text', + 'type' => 'Text', + 'value' => 'Text', + 'valuetype' => 'Enum#data,ref,object' + ) + ); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Presentation.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Presentation.php new file mode 100644 index 0000000..6458ce9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Presentation.php @@ -0,0 +1,42 @@ +addElement('hr', 'Block', 'Empty', 'Common'); + $this->addElement('sub', 'Inline', 'Inline', 'Common'); + $this->addElement('sup', 'Inline', 'Inline', 'Common'); + $b = $this->addElement('b', 'Inline', 'Inline', 'Common'); + $b->formatting = true; + $big = $this->addElement('big', 'Inline', 'Inline', 'Common'); + $big->formatting = true; + $i = $this->addElement('i', 'Inline', 'Inline', 'Common'); + $i->formatting = true; + $small = $this->addElement('small', 'Inline', 'Inline', 'Common'); + $small->formatting = true; + $tt = $this->addElement('tt', 'Inline', 'Inline', 'Common'); + $tt->formatting = true; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Proprietary.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Proprietary.php new file mode 100644 index 0000000..5ee3c8e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Proprietary.php @@ -0,0 +1,40 @@ +addElement( + 'marquee', + 'Inline', + 'Flow', + 'Common', + array( + 'direction' => 'Enum#left,right,up,down', + 'behavior' => 'Enum#alternate', + 'width' => 'Length', + 'height' => 'Length', + 'scrolldelay' => 'Number', + 'scrollamount' => 'Number', + 'loop' => 'Number', + 'bgcolor' => 'Color', + 'hspace' => 'Pixels', + 'vspace' => 'Pixels', + ) + ); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php new file mode 100644 index 0000000..a0d4892 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php @@ -0,0 +1,36 @@ +addElement( + 'ruby', + 'Inline', + 'Custom: ((rb, (rt | (rp, rt, rp))) | (rbc, rtc, rtc?))', + 'Common' + ); + $this->addElement('rbc', false, 'Required: rb', 'Common'); + $this->addElement('rtc', false, 'Required: rt', 'Common'); + $rb = $this->addElement('rb', false, 'Inline', 'Common'); + $rb->excludes = array('ruby' => true); + $rt = $this->addElement('rt', false, 'Inline', 'Common', array('rbspan' => 'Number')); + $rt->excludes = array('ruby' => true); + $this->addElement('rp', false, 'Optional: #PCDATA', 'Common'); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeEmbed.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeEmbed.php new file mode 100644 index 0000000..04e6689 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeEmbed.php @@ -0,0 +1,40 @@ +get('HTML.MaxImgLength'); + $embed = $this->addElement( + 'embed', + 'Inline', + 'Empty', + 'Common', + array( + 'src*' => 'URI#embedded', + 'type' => 'Enum#application/x-shockwave-flash', + 'width' => 'Pixels#' . $max, + 'height' => 'Pixels#' . $max, + 'allowscriptaccess' => 'Enum#never', + 'allownetworking' => 'Enum#internal', + 'flashvars' => 'Text', + 'wmode' => 'Enum#window,transparent,opaque', + 'name' => 'ID', + ) + ); + $embed->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeEmbed(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeObject.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeObject.php new file mode 100644 index 0000000..1297f80 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeObject.php @@ -0,0 +1,62 @@ +get('HTML.MaxImgLength'); + $object = $this->addElement( + 'object', + 'Inline', + 'Optional: param | Flow | #PCDATA', + 'Common', + array( + // While technically not required by the spec, we're forcing + // it to this value. + 'type' => 'Enum#application/x-shockwave-flash', + 'width' => 'Pixels#' . $max, + 'height' => 'Pixels#' . $max, + 'data' => 'URI#embedded', + 'codebase' => new HTMLPurifier_AttrDef_Enum( + array( + 'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0' + ) + ), + ) + ); + $object->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeObject(); + + $param = $this->addElement( + 'param', + false, + 'Empty', + false, + array( + 'id' => 'ID', + 'name*' => 'Text', + 'value' => 'Text' + ) + ); + $param->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeParam(); + $this->info_injector[] = 'SafeObject'; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeScripting.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeScripting.php new file mode 100644 index 0000000..0330cd9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeScripting.php @@ -0,0 +1,40 @@ +get('HTML.SafeScripting'); + $script = $this->addElement( + 'script', + 'Inline', + 'Empty', + null, + array( + // While technically not required by the spec, we're forcing + // it to this value. + 'type' => 'Enum#text/javascript', + 'src*' => new HTMLPurifier_AttrDef_Enum(array_keys($allowed)) + ) + ); + $script->attr_transform_pre[] = + $script->attr_transform_post[] = new HTMLPurifier_AttrTransform_ScriptRequired(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Scripting.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Scripting.php new file mode 100644 index 0000000..8b28a7b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Scripting.php @@ -0,0 +1,73 @@ + 'script | noscript', 'Inline' => 'script | noscript'); + + /** + * @type bool + */ + public $safe = false; + + /** + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + // TODO: create custom child-definition for noscript that + // auto-wraps stray #PCDATA in a similar manner to + // blockquote's custom definition (we would use it but + // blockquote's contents are optional while noscript's contents + // are required) + + // TODO: convert this to new syntax, main problem is getting + // both content sets working + + // In theory, this could be safe, but I don't see any reason to + // allow it. + $this->info['noscript'] = new HTMLPurifier_ElementDef(); + $this->info['noscript']->attr = array(0 => array('Common')); + $this->info['noscript']->content_model = 'Heading | List | Block'; + $this->info['noscript']->content_model_type = 'required'; + + $this->info['script'] = new HTMLPurifier_ElementDef(); + $this->info['script']->attr = array( + 'defer' => new HTMLPurifier_AttrDef_Enum(array('defer')), + 'src' => new HTMLPurifier_AttrDef_URI(true), + 'type' => new HTMLPurifier_AttrDef_Enum(array('text/javascript')) + ); + $this->info['script']->content_model = '#PCDATA'; + $this->info['script']->content_model_type = 'optional'; + $this->info['script']->attr_transform_pre[] = + $this->info['script']->attr_transform_post[] = + new HTMLPurifier_AttrTransform_ScriptRequired(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/StyleAttribute.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/StyleAttribute.php new file mode 100644 index 0000000..497b832 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/StyleAttribute.php @@ -0,0 +1,33 @@ + array('style' => false), // see constructor + 'Core' => array(0 => array('Style')) + ); + + /** + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + $this->attr_collections['Style']['style'] = new HTMLPurifier_AttrDef_CSS(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tables.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tables.php new file mode 100644 index 0000000..8a0b3b4 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tables.php @@ -0,0 +1,75 @@ +addElement('caption', false, 'Inline', 'Common'); + + $this->addElement( + 'table', + 'Block', + new HTMLPurifier_ChildDef_Table(), + 'Common', + array( + 'border' => 'Pixels', + 'cellpadding' => 'Length', + 'cellspacing' => 'Length', + 'frame' => 'Enum#void,above,below,hsides,lhs,rhs,vsides,box,border', + 'rules' => 'Enum#none,groups,rows,cols,all', + 'summary' => 'Text', + 'width' => 'Length' + ) + ); + + // common attributes + $cell_align = array( + 'align' => 'Enum#left,center,right,justify,char', + 'charoff' => 'Length', + 'valign' => 'Enum#top,middle,bottom,baseline', + ); + + $cell_t = array_merge( + array( + 'abbr' => 'Text', + 'colspan' => 'Number', + 'rowspan' => 'Number', + // Apparently, as of HTML5 this attribute only applies + // to 'th' elements. + 'scope' => 'Enum#row,col,rowgroup,colgroup', + ), + $cell_align + ); + $this->addElement('td', false, 'Flow', 'Common', $cell_t); + $this->addElement('th', false, 'Flow', 'Common', $cell_t); + + $this->addElement('tr', false, 'Required: td | th', 'Common', $cell_align); + + $cell_col = array_merge( + array( + 'span' => 'Number', + 'width' => 'MultiLength', + ), + $cell_align + ); + $this->addElement('col', false, 'Empty', 'Common', $cell_col); + $this->addElement('colgroup', false, 'Optional: col', 'Common', $cell_col); + + $this->addElement('tbody', false, 'Required: tr', 'Common', $cell_align); + $this->addElement('thead', false, 'Required: tr', 'Common', $cell_align); + $this->addElement('tfoot', false, 'Required: tr', 'Common', $cell_align); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Target.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Target.php new file mode 100644 index 0000000..b188ac9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Target.php @@ -0,0 +1,28 @@ +addBlankElement($name); + $e->attr = array( + 'target' => new HTMLPurifier_AttrDef_HTML_FrameTarget() + ); + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetBlank.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetBlank.php new file mode 100644 index 0000000..58ccc68 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetBlank.php @@ -0,0 +1,24 @@ +addBlankElement('a'); + $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetBlank(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoopener.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoopener.php new file mode 100644 index 0000000..b967ff5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoopener.php @@ -0,0 +1,21 @@ +addBlankElement('a'); + $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetNoopener(); + } +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoreferrer.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoreferrer.php new file mode 100644 index 0000000..32484d6 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoreferrer.php @@ -0,0 +1,21 @@ +addBlankElement('a'); + $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetNoreferrer(); + } +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Text.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Text.php new file mode 100644 index 0000000..7a65e00 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Text.php @@ -0,0 +1,87 @@ + 'Heading | Block | Inline' + ); + + /** + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + // Inline Phrasal ------------------------------------------------- + $this->addElement('abbr', 'Inline', 'Inline', 'Common'); + $this->addElement('acronym', 'Inline', 'Inline', 'Common'); + $this->addElement('cite', 'Inline', 'Inline', 'Common'); + $this->addElement('dfn', 'Inline', 'Inline', 'Common'); + $this->addElement('kbd', 'Inline', 'Inline', 'Common'); + $this->addElement('q', 'Inline', 'Inline', 'Common', array('cite' => 'URI')); + $this->addElement('samp', 'Inline', 'Inline', 'Common'); + $this->addElement('var', 'Inline', 'Inline', 'Common'); + + $em = $this->addElement('em', 'Inline', 'Inline', 'Common'); + $em->formatting = true; + + $strong = $this->addElement('strong', 'Inline', 'Inline', 'Common'); + $strong->formatting = true; + + $code = $this->addElement('code', 'Inline', 'Inline', 'Common'); + $code->formatting = true; + + // Inline Structural ---------------------------------------------- + $this->addElement('span', 'Inline', 'Inline', 'Common'); + $this->addElement('br', 'Inline', 'Empty', 'Core'); + + // Block Phrasal -------------------------------------------------- + $this->addElement('address', 'Block', 'Inline', 'Common'); + $this->addElement('blockquote', 'Block', 'Optional: Heading | Block | List', 'Common', array('cite' => 'URI')); + $pre = $this->addElement('pre', 'Block', 'Inline', 'Common'); + $pre->excludes = $this->makeLookup( + 'img', + 'big', + 'small', + 'object', + 'applet', + 'font', + 'basefont' + ); + $this->addElement('h1', 'Heading', 'Inline', 'Common'); + $this->addElement('h2', 'Heading', 'Inline', 'Common'); + $this->addElement('h3', 'Heading', 'Inline', 'Common'); + $this->addElement('h4', 'Heading', 'Inline', 'Common'); + $this->addElement('h5', 'Heading', 'Inline', 'Common'); + $this->addElement('h6', 'Heading', 'Inline', 'Common'); + + // Block Structural ----------------------------------------------- + $p = $this->addElement('p', 'Block', 'Inline', 'Common'); + $p->autoclose = array_flip( + array("address", "blockquote", "center", "dir", "div", "dl", "fieldset", "ol", "p", "ul") + ); + + $this->addElement('div', 'Block', 'Flow', 'Common'); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy.php new file mode 100644 index 0000000..08aa232 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy.php @@ -0,0 +1,230 @@ + 'none', 'light', 'medium', 'heavy'); + + /** + * Default level to place all fixes in. + * Disabled by default. + * @type string + */ + public $defaultLevel = null; + + /** + * Lists of fixes used by getFixesForLevel(). + * Format is: + * HTMLModule_Tidy->fixesForLevel[$level] = array('fix-1', 'fix-2'); + * @type array + */ + public $fixesForLevel = array( + 'light' => array(), + 'medium' => array(), + 'heavy' => array() + ); + + /** + * Lazy load constructs the module by determining the necessary + * fixes to create and then delegating to the populate() function. + * @param HTMLPurifier_Config $config + * @todo Wildcard matching and error reporting when an added or + * subtracted fix has no effect. + */ + public function setup($config) + { + // create fixes, initialize fixesForLevel + $fixes = $this->makeFixes(); + $this->makeFixesForLevel($fixes); + + // figure out which fixes to use + $level = $config->get('HTML.TidyLevel'); + $fixes_lookup = $this->getFixesForLevel($level); + + // get custom fix declarations: these need namespace processing + $add_fixes = $config->get('HTML.TidyAdd'); + $remove_fixes = $config->get('HTML.TidyRemove'); + + foreach ($fixes as $name => $fix) { + // needs to be refactored a little to implement globbing + if (isset($remove_fixes[$name]) || + (!isset($add_fixes[$name]) && !isset($fixes_lookup[$name]))) { + unset($fixes[$name]); + } + } + + // populate this module with necessary fixes + $this->populate($fixes); + } + + /** + * Retrieves all fixes per a level, returning fixes for that specific + * level as well as all levels below it. + * @param string $level level identifier, see $levels for valid values + * @return array Lookup up table of fixes + */ + public function getFixesForLevel($level) + { + if ($level == $this->levels[0]) { + return array(); + } + $activated_levels = array(); + for ($i = 1, $c = count($this->levels); $i < $c; $i++) { + $activated_levels[] = $this->levels[$i]; + if ($this->levels[$i] == $level) { + break; + } + } + if ($i == $c) { + trigger_error( + 'Tidy level ' . htmlspecialchars($level) . ' not recognized', + E_USER_WARNING + ); + return array(); + } + $ret = array(); + foreach ($activated_levels as $level) { + foreach ($this->fixesForLevel[$level] as $fix) { + $ret[$fix] = true; + } + } + return $ret; + } + + /** + * Dynamically populates the $fixesForLevel member variable using + * the fixes array. It may be custom overloaded, used in conjunction + * with $defaultLevel, or not used at all. + * @param array $fixes + */ + public function makeFixesForLevel($fixes) + { + if (!isset($this->defaultLevel)) { + return; + } + if (!isset($this->fixesForLevel[$this->defaultLevel])) { + trigger_error( + 'Default level ' . $this->defaultLevel . ' does not exist', + E_USER_ERROR + ); + return; + } + $this->fixesForLevel[$this->defaultLevel] = array_keys($fixes); + } + + /** + * Populates the module with transforms and other special-case code + * based on a list of fixes passed to it + * @param array $fixes Lookup table of fixes to activate + */ + public function populate($fixes) + { + foreach ($fixes as $name => $fix) { + // determine what the fix is for + list($type, $params) = $this->getFixType($name); + switch ($type) { + case 'attr_transform_pre': + case 'attr_transform_post': + $attr = $params['attr']; + if (isset($params['element'])) { + $element = $params['element']; + if (empty($this->info[$element])) { + $e = $this->addBlankElement($element); + } else { + $e = $this->info[$element]; + } + } else { + $type = "info_$type"; + $e = $this; + } + // PHP does some weird parsing when I do + // $e->$type[$attr], so I have to assign a ref. + $f =& $e->$type; + $f[$attr] = $fix; + break; + case 'tag_transform': + $this->info_tag_transform[$params['element']] = $fix; + break; + case 'child': + case 'content_model_type': + $element = $params['element']; + if (empty($this->info[$element])) { + $e = $this->addBlankElement($element); + } else { + $e = $this->info[$element]; + } + $e->$type = $fix; + break; + default: + trigger_error("Fix type $type not supported", E_USER_ERROR); + break; + } + } + } + + /** + * Parses a fix name and determines what kind of fix it is, as well + * as other information defined by the fix + * @param $name String name of fix + * @return array(string $fix_type, array $fix_parameters) + * @note $fix_parameters is type dependant, see populate() for usage + * of these parameters + */ + public function getFixType($name) + { + // parse it + $property = $attr = null; + if (strpos($name, '#') !== false) { + list($name, $property) = explode('#', $name); + } + if (strpos($name, '@') !== false) { + list($name, $attr) = explode('@', $name); + } + + // figure out the parameters + $params = array(); + if ($name !== '') { + $params['element'] = $name; + } + if (!is_null($attr)) { + $params['attr'] = $attr; + } + + // special case: attribute transform + if (!is_null($attr)) { + if (is_null($property)) { + $property = 'pre'; + } + $type = 'attr_transform_' . $property; + return array($type, $params); + } + + // special case: tag transform + if (is_null($property)) { + return array('tag_transform', $params); + } + + return array($property, $params); + + } + + /** + * Defines all fixes the module will perform in a compact + * associative array of fix name to fix implementation. + * @return array + */ + public function makeFixes() + { + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Name.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Name.php new file mode 100644 index 0000000..a995161 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Name.php @@ -0,0 +1,33 @@ +content_model_type != 'strictblockquote') { + return parent::getChildDef($def); + } + return new HTMLPurifier_ChildDef_StrictBlockquote($def->content_model); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Transitional.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Transitional.php new file mode 100644 index 0000000..c095ad9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Transitional.php @@ -0,0 +1,16 @@ + 'text-align:left;', + 'right' => 'text-align:right;', + 'top' => 'caption-side:top;', + 'bottom' => 'caption-side:bottom;' // not supported by IE + ) + ); + + // @align for img ------------------------------------------------- + $r['img@align'] = + new HTMLPurifier_AttrTransform_EnumToCSS( + 'align', + array( + 'left' => 'float:left;', + 'right' => 'float:right;', + 'top' => 'vertical-align:top;', + 'middle' => 'vertical-align:middle;', + 'bottom' => 'vertical-align:baseline;', + ) + ); + + // @align for table ----------------------------------------------- + $r['table@align'] = + new HTMLPurifier_AttrTransform_EnumToCSS( + 'align', + array( + 'left' => 'float:left;', + 'center' => 'margin-left:auto;margin-right:auto;', + 'right' => 'float:right;' + ) + ); + + // @align for hr ----------------------------------------------- + $r['hr@align'] = + new HTMLPurifier_AttrTransform_EnumToCSS( + 'align', + array( + // we use both text-align and margin because these work + // for different browsers (IE and Firefox, respectively) + // and the melange makes for a pretty cross-compatible + // solution + 'left' => 'margin-left:0;margin-right:auto;text-align:left;', + 'center' => 'margin-left:auto;margin-right:auto;text-align:center;', + 'right' => 'margin-left:auto;margin-right:0;text-align:right;' + ) + ); + + // @align for h1, h2, h3, h4, h5, h6, p, div ---------------------- + // {{{ + $align_lookup = array(); + $align_values = array('left', 'right', 'center', 'justify'); + foreach ($align_values as $v) { + $align_lookup[$v] = "text-align:$v;"; + } + // }}} + $r['h1@align'] = + $r['h2@align'] = + $r['h3@align'] = + $r['h4@align'] = + $r['h5@align'] = + $r['h6@align'] = + $r['p@align'] = + $r['div@align'] = + new HTMLPurifier_AttrTransform_EnumToCSS('align', $align_lookup); + + // @bgcolor for table, tr, td, th --------------------------------- + $r['table@bgcolor'] = + $r['td@bgcolor'] = + $r['th@bgcolor'] = + new HTMLPurifier_AttrTransform_BgColor(); + + // @border for img ------------------------------------------------ + $r['img@border'] = new HTMLPurifier_AttrTransform_Border(); + + // @clear for br -------------------------------------------------- + $r['br@clear'] = + new HTMLPurifier_AttrTransform_EnumToCSS( + 'clear', + array( + 'left' => 'clear:left;', + 'right' => 'clear:right;', + 'all' => 'clear:both;', + 'none' => 'clear:none;', + ) + ); + + // @height for td, th --------------------------------------------- + $r['td@height'] = + $r['th@height'] = + new HTMLPurifier_AttrTransform_Length('height'); + + // @hspace for img ------------------------------------------------ + $r['img@hspace'] = new HTMLPurifier_AttrTransform_ImgSpace('hspace'); + + // @noshade for hr ------------------------------------------------ + // this transformation is not precise but often good enough. + // different browsers use different styles to designate noshade + $r['hr@noshade'] = + new HTMLPurifier_AttrTransform_BoolToCSS( + 'noshade', + 'color:#808080;background-color:#808080;border:0;' + ); + + // @nowrap for td, th --------------------------------------------- + $r['td@nowrap'] = + $r['th@nowrap'] = + new HTMLPurifier_AttrTransform_BoolToCSS( + 'nowrap', + 'white-space:nowrap;' + ); + + // @size for hr -------------------------------------------------- + $r['hr@size'] = new HTMLPurifier_AttrTransform_Length('size', 'height'); + + // @type for li, ol, ul ------------------------------------------- + // {{{ + $ul_types = array( + 'disc' => 'list-style-type:disc;', + 'square' => 'list-style-type:square;', + 'circle' => 'list-style-type:circle;' + ); + $ol_types = array( + '1' => 'list-style-type:decimal;', + 'i' => 'list-style-type:lower-roman;', + 'I' => 'list-style-type:upper-roman;', + 'a' => 'list-style-type:lower-alpha;', + 'A' => 'list-style-type:upper-alpha;' + ); + $li_types = $ul_types + $ol_types; + // }}} + + $r['ul@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $ul_types); + $r['ol@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $ol_types, true); + $r['li@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $li_types, true); + + // @vspace for img ------------------------------------------------ + $r['img@vspace'] = new HTMLPurifier_AttrTransform_ImgSpace('vspace'); + + // @width for hr, td, th ------------------------------------------ + $r['td@width'] = + $r['th@width'] = + $r['hr@width'] = new HTMLPurifier_AttrTransform_Length('width'); + + return $r; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/XMLCommonAttributes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/XMLCommonAttributes.php new file mode 100644 index 0000000..01dbe9d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/XMLCommonAttributes.php @@ -0,0 +1,20 @@ + array( + 'xml:lang' => 'LanguageCode', + ) + ); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModuleManager.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModuleManager.php new file mode 100644 index 0000000..38c058f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModuleManager.php @@ -0,0 +1,467 @@ +attrTypes = new HTMLPurifier_AttrTypes(); + $this->doctypes = new HTMLPurifier_DoctypeRegistry(); + + // setup basic modules + $common = array( + 'CommonAttributes', 'Text', 'Hypertext', 'List', + 'Presentation', 'Edit', 'Bdo', 'Tables', 'Image', + 'StyleAttribute', + // Unsafe: + 'Scripting', 'Object', 'Forms', + // Sorta legacy, but present in strict: + 'Name', + ); + $transitional = array('Legacy', 'Target', 'Iframe'); + $xml = array('XMLCommonAttributes'); + $non_xml = array('NonXMLCommonAttributes'); + + // setup basic doctypes + $this->doctypes->register( + 'HTML 4.01 Transitional', + false, + array_merge($common, $transitional, $non_xml), + array('Tidy_Transitional', 'Tidy_Proprietary'), + array(), + '-//W3C//DTD HTML 4.01 Transitional//EN', + 'http://www.w3.org/TR/html4/loose.dtd' + ); + + $this->doctypes->register( + 'HTML 4.01 Strict', + false, + array_merge($common, $non_xml), + array('Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'), + array(), + '-//W3C//DTD HTML 4.01//EN', + 'http://www.w3.org/TR/html4/strict.dtd' + ); + + $this->doctypes->register( + 'XHTML 1.0 Transitional', + true, + array_merge($common, $transitional, $xml, $non_xml), + array('Tidy_Transitional', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Name'), + array(), + '-//W3C//DTD XHTML 1.0 Transitional//EN', + 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd' + ); + + $this->doctypes->register( + 'XHTML 1.0 Strict', + true, + array_merge($common, $xml, $non_xml), + array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'), + array(), + '-//W3C//DTD XHTML 1.0 Strict//EN', + 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd' + ); + + $this->doctypes->register( + 'XHTML 1.1', + true, + // Iframe is a real XHTML 1.1 module, despite being + // "transitional"! + array_merge($common, $xml, array('Ruby', 'Iframe')), + array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Strict', 'Tidy_Name'), // Tidy_XHTML1_1 + array(), + '-//W3C//DTD XHTML 1.1//EN', + 'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd' + ); + + } + + /** + * Registers a module to the recognized module list, useful for + * overloading pre-existing modules. + * @param $module Mixed: string module name, with or without + * HTMLPurifier_HTMLModule prefix, or instance of + * subclass of HTMLPurifier_HTMLModule. + * @param $overload Boolean whether or not to overload previous modules. + * If this is not set, and you do overload a module, + * HTML Purifier will complain with a warning. + * @note This function will not call autoload, you must instantiate + * (and thus invoke) autoload outside the method. + * @note If a string is passed as a module name, different variants + * will be tested in this order: + * - Check for HTMLPurifier_HTMLModule_$name + * - Check all prefixes with $name in order they were added + * - Check for literal object name + * - Throw fatal error + * If your object name collides with an internal class, specify + * your module manually. All modules must have been included + * externally: registerModule will not perform inclusions for you! + */ + public function registerModule($module, $overload = false) + { + if (is_string($module)) { + // attempt to load the module + $original_module = $module; + $ok = false; + foreach ($this->prefixes as $prefix) { + $module = $prefix . $original_module; + if (class_exists($module)) { + $ok = true; + break; + } + } + if (!$ok) { + $module = $original_module; + if (!class_exists($module)) { + trigger_error( + $original_module . ' module does not exist', + E_USER_ERROR + ); + return; + } + } + $module = new $module(); + } + if (empty($module->name)) { + trigger_error('Module instance of ' . get_class($module) . ' must have name'); + return; + } + if (!$overload && isset($this->registeredModules[$module->name])) { + trigger_error('Overloading ' . $module->name . ' without explicit overload parameter', E_USER_WARNING); + } + $this->registeredModules[$module->name] = $module; + } + + /** + * Adds a module to the current doctype by first registering it, + * and then tacking it on to the active doctype + */ + public function addModule($module) + { + $this->registerModule($module); + if (is_object($module)) { + $module = $module->name; + } + $this->userModules[] = $module; + } + + /** + * Adds a class prefix that registerModule() will use to resolve a + * string name to a concrete class + */ + public function addPrefix($prefix) + { + $this->prefixes[] = $prefix; + } + + /** + * Performs processing on modules, after being called you may + * use getElement() and getElements() + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + $this->trusted = $config->get('HTML.Trusted'); + + // generate + $this->doctype = $this->doctypes->make($config); + $modules = $this->doctype->modules; + + // take out the default modules that aren't allowed + $lookup = $config->get('HTML.AllowedModules'); + $special_cases = $config->get('HTML.CoreModules'); + + if (is_array($lookup)) { + foreach ($modules as $k => $m) { + if (isset($special_cases[$m])) { + continue; + } + if (!isset($lookup[$m])) { + unset($modules[$k]); + } + } + } + + // custom modules + if ($config->get('HTML.Proprietary')) { + $modules[] = 'Proprietary'; + } + if ($config->get('HTML.SafeObject')) { + $modules[] = 'SafeObject'; + } + if ($config->get('HTML.SafeEmbed')) { + $modules[] = 'SafeEmbed'; + } + if ($config->get('HTML.SafeScripting') !== array()) { + $modules[] = 'SafeScripting'; + } + if ($config->get('HTML.Nofollow')) { + $modules[] = 'Nofollow'; + } + if ($config->get('HTML.TargetBlank')) { + $modules[] = 'TargetBlank'; + } + // NB: HTML.TargetNoreferrer and HTML.TargetNoopener must be AFTER HTML.TargetBlank + // so that its post-attr-transform gets run afterwards. + if ($config->get('HTML.TargetNoreferrer')) { + $modules[] = 'TargetNoreferrer'; + } + if ($config->get('HTML.TargetNoopener')) { + $modules[] = 'TargetNoopener'; + } + + // merge in custom modules + $modules = array_merge($modules, $this->userModules); + + foreach ($modules as $module) { + $this->processModule($module); + $this->modules[$module]->setup($config); + } + + foreach ($this->doctype->tidyModules as $module) { + $this->processModule($module); + $this->modules[$module]->setup($config); + } + + // prepare any injectors + foreach ($this->modules as $module) { + $n = array(); + foreach ($module->info_injector as $injector) { + if (!is_object($injector)) { + $class = "HTMLPurifier_Injector_$injector"; + $injector = new $class; + } + $n[$injector->name] = $injector; + } + $module->info_injector = $n; + } + + // setup lookup table based on all valid modules + foreach ($this->modules as $module) { + foreach ($module->info as $name => $def) { + if (!isset($this->elementLookup[$name])) { + $this->elementLookup[$name] = array(); + } + $this->elementLookup[$name][] = $module->name; + } + } + + // note the different choice + $this->contentSets = new HTMLPurifier_ContentSets( + // content set assembly deals with all possible modules, + // not just ones deemed to be "safe" + $this->modules + ); + $this->attrCollections = new HTMLPurifier_AttrCollections( + $this->attrTypes, + // there is no way to directly disable a global attribute, + // but using AllowedAttributes or simply not including + // the module in your custom doctype should be sufficient + $this->modules + ); + } + + /** + * Takes a module and adds it to the active module collection, + * registering it if necessary. + */ + public function processModule($module) + { + if (!isset($this->registeredModules[$module]) || is_object($module)) { + $this->registerModule($module); + } + $this->modules[$module] = $this->registeredModules[$module]; + } + + /** + * Retrieves merged element definitions. + * @return Array of HTMLPurifier_ElementDef + */ + public function getElements() + { + $elements = array(); + foreach ($this->modules as $module) { + if (!$this->trusted && !$module->safe) { + continue; + } + foreach ($module->info as $name => $v) { + if (isset($elements[$name])) { + continue; + } + $elements[$name] = $this->getElement($name); + } + } + + // remove dud elements, this happens when an element that + // appeared to be safe actually wasn't + foreach ($elements as $n => $v) { + if ($v === false) { + unset($elements[$n]); + } + } + + return $elements; + + } + + /** + * Retrieves a single merged element definition + * @param string $name Name of element + * @param bool $trusted Boolean trusted overriding parameter: set to true + * if you want the full version of an element + * @return HTMLPurifier_ElementDef Merged HTMLPurifier_ElementDef + * @note You may notice that modules are getting iterated over twice (once + * in getElements() and once here). This + * is because + */ + public function getElement($name, $trusted = null) + { + if (!isset($this->elementLookup[$name])) { + return false; + } + + // setup global state variables + $def = false; + if ($trusted === null) { + $trusted = $this->trusted; + } + + // iterate through each module that has registered itself to this + // element + foreach ($this->elementLookup[$name] as $module_name) { + $module = $this->modules[$module_name]; + + // refuse to create/merge from a module that is deemed unsafe-- + // pretend the module doesn't exist--when trusted mode is not on. + if (!$trusted && !$module->safe) { + continue; + } + + // clone is used because, ideally speaking, the original + // definition should not be modified. Usually, this will + // make no difference, but for consistency's sake + $new_def = clone $module->info[$name]; + + if (!$def && $new_def->standalone) { + $def = $new_def; + } elseif ($def) { + // This will occur even if $new_def is standalone. In practice, + // this will usually result in a full replacement. + $def->mergeIn($new_def); + } else { + // :TODO: + // non-standalone definitions that don't have a standalone + // to merge into could be deferred to the end + // HOWEVER, it is perfectly valid for a non-standalone + // definition to lack a standalone definition, even + // after all processing: this allows us to safely + // specify extra attributes for elements that may not be + // enabled all in one place. In particular, this might + // be the case for trusted elements. WARNING: care must + // be taken that the /extra/ definitions are all safe. + continue; + } + + // attribute value expansions + $this->attrCollections->performInclusions($def->attr); + $this->attrCollections->expandIdentifiers($def->attr, $this->attrTypes); + + // descendants_are_inline, for ChildDef_Chameleon + if (is_string($def->content_model) && + strpos($def->content_model, 'Inline') !== false) { + if ($name != 'del' && $name != 'ins') { + // this is for you, ins/del + $def->descendants_are_inline = true; + } + } + + $this->contentSets->generateChildDef($def, $module); + } + + // This can occur if there is a blank definition, but no base to + // mix it in with + if (!$def) { + return false; + } + + // add information on required attributes + foreach ($def->attr as $attr_name => $attr_def) { + if ($attr_def->required) { + $def->required_attr[] = $attr_name; + } + } + return $def; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/IDAccumulator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/IDAccumulator.php new file mode 100644 index 0000000..65c902c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/IDAccumulator.php @@ -0,0 +1,57 @@ +load($config->get('Attr.IDBlacklist')); + return $id_accumulator; + } + + /** + * Add an ID to the lookup table. + * @param string $id ID to be added. + * @return bool status, true if success, false if there's a dupe + */ + public function add($id) + { + if (isset($this->ids[$id])) { + return false; + } + return $this->ids[$id] = true; + } + + /** + * Load a list of IDs into the lookup table + * @param $array_of_ids Array of IDs to load + * @note This function doesn't care about duplicates + */ + public function load($array_of_ids) + { + foreach ($array_of_ids as $id) { + $this->ids[$id] = true; + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector.php new file mode 100644 index 0000000..116b470 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector.php @@ -0,0 +1,283 @@ +processToken() + * documentation. + * + * @todo Allow injectors to request a re-run on their output. This + * would help if an operation is recursive. + */ +abstract class HTMLPurifier_Injector +{ + + /** + * Advisory name of injector, this is for friendly error messages. + * @type string + */ + public $name; + + /** + * @type HTMLPurifier_HTMLDefinition + */ + protected $htmlDefinition; + + /** + * Reference to CurrentNesting variable in Context. This is an array + * list of tokens that we are currently "inside" + * @type array + */ + protected $currentNesting; + + /** + * Reference to current token. + * @type HTMLPurifier_Token + */ + protected $currentToken; + + /** + * Reference to InputZipper variable in Context. + * @type HTMLPurifier_Zipper + */ + protected $inputZipper; + + /** + * Array of elements and attributes this injector creates and therefore + * need to be allowed by the definition. Takes form of + * array('element' => array('attr', 'attr2'), 'element2') + * @type array + */ + public $needed = array(); + + /** + * Number of elements to rewind backwards (relative). + * @type bool|int + */ + protected $rewindOffset = false; + + /** + * Rewind to a spot to re-perform processing. This is useful if you + * deleted a node, and now need to see if this change affected any + * earlier nodes. Rewinding does not affect other injectors, and can + * result in infinite loops if not used carefully. + * @param bool|int $offset + * @warning HTML Purifier will prevent you from fast-forwarding with this + * function. + */ + public function rewindOffset($offset) + { + $this->rewindOffset = $offset; + } + + /** + * Retrieves rewind offset, and then unsets it. + * @return bool|int + */ + public function getRewindOffset() + { + $r = $this->rewindOffset; + $this->rewindOffset = false; + return $r; + } + + /** + * Prepares the injector by giving it the config and context objects: + * this allows references to important variables to be made within + * the injector. This function also checks if the HTML environment + * will work with the Injector (see checkNeeded()). + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string Boolean false if success, string of missing needed element/attribute if failure + */ + public function prepare($config, $context) + { + $this->htmlDefinition = $config->getHTMLDefinition(); + // Even though this might fail, some unit tests ignore this and + // still test checkNeeded, so be careful. Maybe get rid of that + // dependency. + $result = $this->checkNeeded($config); + if ($result !== false) { + return $result; + } + $this->currentNesting =& $context->get('CurrentNesting'); + $this->currentToken =& $context->get('CurrentToken'); + $this->inputZipper =& $context->get('InputZipper'); + return false; + } + + /** + * This function checks if the HTML environment + * will work with the Injector: if p tags are not allowed, the + * Auto-Paragraphing injector should not be enabled. + * @param HTMLPurifier_Config $config + * @return bool|string Boolean false if success, string of missing needed element/attribute if failure + */ + public function checkNeeded($config) + { + $def = $config->getHTMLDefinition(); + foreach ($this->needed as $element => $attributes) { + if (is_int($element)) { + $element = $attributes; + } + if (!isset($def->info[$element])) { + return $element; + } + if (!is_array($attributes)) { + continue; + } + foreach ($attributes as $name) { + if (!isset($def->info[$element]->attr[$name])) { + return "$element.$name"; + } + } + } + return false; + } + + /** + * Tests if the context node allows a certain element + * @param string $name Name of element to test for + * @return bool True if element is allowed, false if it is not + */ + public function allowsElement($name) + { + if (!empty($this->currentNesting)) { + $parent_token = array_pop($this->currentNesting); + $this->currentNesting[] = $parent_token; + $parent = $this->htmlDefinition->info[$parent_token->name]; + } else { + $parent = $this->htmlDefinition->info_parent_def; + } + if (!isset($parent->child->elements[$name]) || isset($parent->excludes[$name])) { + return false; + } + // check for exclusion + if (!empty($this->currentNesting)) { + for ($i = count($this->currentNesting) - 2; $i >= 0; $i--) { + $node = $this->currentNesting[$i]; + $def = $this->htmlDefinition->info[$node->name]; + if (isset($def->excludes[$name])) { + return false; + } + } + } + return true; + } + + /** + * Iterator function, which starts with the next token and continues until + * you reach the end of the input tokens. + * @warning Please prevent previous references from interfering with this + * functions by setting $i = null beforehand! + * @param int $i Current integer index variable for inputTokens + * @param HTMLPurifier_Token $current Current token variable. + * Do NOT use $token, as that variable is also a reference + * @return bool + */ + protected function forward(&$i, &$current) + { + if ($i === null) { + $i = count($this->inputZipper->back) - 1; + } else { + $i--; + } + if ($i < 0) { + return false; + } + $current = $this->inputZipper->back[$i]; + return true; + } + + /** + * Similar to _forward, but accepts a third parameter $nesting (which + * should be initialized at 0) and stops when we hit the end tag + * for the node $this->inputIndex starts in. + * @param int $i Current integer index variable for inputTokens + * @param HTMLPurifier_Token $current Current token variable. + * Do NOT use $token, as that variable is also a reference + * @param int $nesting + * @return bool + */ + protected function forwardUntilEndToken(&$i, &$current, &$nesting) + { + $result = $this->forward($i, $current); + if (!$result) { + return false; + } + if ($nesting === null) { + $nesting = 0; + } + if ($current instanceof HTMLPurifier_Token_Start) { + $nesting++; + } elseif ($current instanceof HTMLPurifier_Token_End) { + if ($nesting <= 0) { + return false; + } + $nesting--; + } + return true; + } + + /** + * Iterator function, starts with the previous token and continues until + * you reach the beginning of input tokens. + * @warning Please prevent previous references from interfering with this + * functions by setting $i = null beforehand! + * @param int $i Current integer index variable for inputTokens + * @param HTMLPurifier_Token $current Current token variable. + * Do NOT use $token, as that variable is also a reference + * @return bool + */ + protected function backward(&$i, &$current) + { + if ($i === null) { + $i = count($this->inputZipper->front) - 1; + } else { + $i--; + } + if ($i < 0) { + return false; + } + $current = $this->inputZipper->front[$i]; + return true; + } + + /** + * Handler that is called when a text token is processed + */ + public function handleText(&$token) + { + } + + /** + * Handler that is called when a start or empty token is processed + */ + public function handleElement(&$token) + { + } + + /** + * Handler that is called when an end token is processed + */ + public function handleEnd(&$token) + { + $this->notifyEnd($token); + } + + /** + * Notifier that is called when an end token is processed + * @param HTMLPurifier_Token $token Current token variable. + * @note This differs from handlers in that the token is read-only + * @deprecated + */ + public function notifyEnd($token) + { + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/AutoParagraph.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/AutoParagraph.php new file mode 100644 index 0000000..4afdd12 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/AutoParagraph.php @@ -0,0 +1,356 @@ +armor['MakeWellFormed_TagClosedError'] = true; + return $par; + } + + /** + * @param HTMLPurifier_Token_Text $token + */ + public function handleText(&$token) + { + $text = $token->data; + // Does the current parent allow

                  tags? + if ($this->allowsElement('p')) { + if (empty($this->currentNesting) || strpos($text, "\n\n") !== false) { + // Note that we have differing behavior when dealing with text + // in the anonymous root node, or a node inside the document. + // If the text as a double-newline, the treatment is the same; + // if it doesn't, see the next if-block if you're in the document. + + $i = $nesting = null; + if (!$this->forwardUntilEndToken($i, $current, $nesting) && $token->is_whitespace) { + // State 1.1: ... ^ (whitespace, then document end) + // ---- + // This is a degenerate case + } else { + if (!$token->is_whitespace || $this->_isInline($current)) { + // State 1.2: PAR1 + // ---- + + // State 1.3: PAR1\n\nPAR2 + // ------------ + + // State 1.4:

                  PAR1\n\nPAR2 (see State 2) + // ------------ + $token = array($this->_pStart()); + $this->_splitText($text, $token); + } else { + // State 1.5: \n
                  + // -- + } + } + } else { + // State 2:
                  PAR1... (similar to 1.4) + // ---- + + // We're in an element that allows paragraph tags, but we're not + // sure if we're going to need them. + if ($this->_pLookAhead()) { + // State 2.1:
                  PAR1PAR1\n\nPAR2 + // ---- + // Note: This will always be the first child, since any + // previous inline element would have triggered this very + // same routine, and found the double newline. One possible + // exception would be a comment. + $token = array($this->_pStart(), $token); + } else { + // State 2.2.1:
                  PAR1
                  + // ---- + + // State 2.2.2:
                  PAR1PAR1
                  + // ---- + } + } + // Is the current parent a

                  tag? + } elseif (!empty($this->currentNesting) && + $this->currentNesting[count($this->currentNesting) - 1]->name == 'p') { + // State 3.1: ...

                  PAR1 + // ---- + + // State 3.2: ...

                  PAR1\n\nPAR2 + // ------------ + $token = array(); + $this->_splitText($text, $token); + // Abort! + } else { + // State 4.1: ...PAR1 + // ---- + + // State 4.2: ...PAR1\n\nPAR2 + // ------------ + } + } + + /** + * @param HTMLPurifier_Token $token + */ + public function handleElement(&$token) + { + // We don't have to check if we're already in a

                  tag for block + // tokens, because the tag would have been autoclosed by MakeWellFormed. + if ($this->allowsElement('p')) { + if (!empty($this->currentNesting)) { + if ($this->_isInline($token)) { + // State 1:

                  ... + // --- + // Check if this token is adjacent to the parent token + // (seek backwards until token isn't whitespace) + $i = null; + $this->backward($i, $prev); + + if (!$prev instanceof HTMLPurifier_Token_Start) { + // Token wasn't adjacent + if ($prev instanceof HTMLPurifier_Token_Text && + substr($prev->data, -2) === "\n\n" + ) { + // State 1.1.4:

                  PAR1

                  \n\n + // --- + // Quite frankly, this should be handled by splitText + $token = array($this->_pStart(), $token); + } else { + // State 1.1.1:

                  PAR1

                  + // --- + // State 1.1.2:

                  + // --- + // State 1.1.3:
                  PAR + // --- + } + } else { + // State 1.2.1:
                  + // --- + // Lookahead to see if

                  is needed. + if ($this->_pLookAhead()) { + // State 1.3.1:

                  PAR1\n\nPAR2 + // --- + $token = array($this->_pStart(), $token); + } else { + // State 1.3.2:
                  PAR1
                  + // --- + + // State 1.3.3:
                  PAR1
                  \n\n
                  + // --- + } + } + } else { + // State 2.3: ...
                  + // ----- + } + } else { + if ($this->_isInline($token)) { + // State 3.1: + // --- + // This is where the {p} tag is inserted, not reflected in + // inputTokens yet, however. + $token = array($this->_pStart(), $token); + } else { + // State 3.2:
                  + // ----- + } + + $i = null; + if ($this->backward($i, $prev)) { + if (!$prev instanceof HTMLPurifier_Token_Text) { + // State 3.1.1: ...

                  {p} + // --- + // State 3.2.1: ...

                  + // ----- + if (!is_array($token)) { + $token = array($token); + } + array_unshift($token, new HTMLPurifier_Token_Text("\n\n")); + } else { + // State 3.1.2: ...

                  \n\n{p} + // --- + // State 3.2.2: ...

                  \n\n
                  + // ----- + // Note: PAR cannot occur because PAR would have been + // wrapped in

                  tags. + } + } + } + } else { + // State 2.2:

            • $th:".$td."
              '; + + $output .= $this->addRow('Message', (string) $record['message']); + $output .= $this->addRow('Time', $this->formatDate($record['datetime'])); + $output .= $this->addRow('Channel', $record['channel']); + if ($record['context']) { + $embeddedTable = '
              '; + foreach ($record['context'] as $key => $value) { + $embeddedTable .= $this->addRow((string) $key, $this->convertToString($value)); + } + $embeddedTable .= '
              '; + $output .= $this->addRow('Context', $embeddedTable, false); + } + if ($record['extra']) { + $embeddedTable = ''; + foreach ($record['extra'] as $key => $value) { + $embeddedTable .= $this->addRow((string) $key, $this->convertToString($value)); + } + $embeddedTable .= '
              '; + $output .= $this->addRow('Extra', $embeddedTable, false); + } + + return $output.''; + } + + /** + * Formats a set of log records. + * + * @return string The formatted set of records + */ + public function formatBatch(array $records): string + { + $message = ''; + foreach ($records as $record) { + $message .= $this->format($record); + } + + return $message; + } + + /** + * @param mixed $data + */ + protected function convertToString($data): string + { + if (null === $data || is_scalar($data)) { + return (string) $data; + } + + $data = $this->normalize($data); + + return Utils::jsonEncode($data, JSON_PRETTY_PRINT | Utils::DEFAULT_JSON_FLAGS, true); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Formatter/JsonFormatter.php b/vendor/monolog/monolog/src/Monolog/Formatter/JsonFormatter.php new file mode 100644 index 0000000..b737d82 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Formatter/JsonFormatter.php @@ -0,0 +1,224 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +use Throwable; + +/** + * Encodes whatever record data is passed to it as json + * + * This can be useful to log to databases or remote APIs + * + * @author Jordi Boggiano + * + * @phpstan-import-type Record from \Monolog\Logger + */ +class JsonFormatter extends NormalizerFormatter +{ + public const BATCH_MODE_JSON = 1; + public const BATCH_MODE_NEWLINES = 2; + + /** @var self::BATCH_MODE_* */ + protected $batchMode; + /** @var bool */ + protected $appendNewline; + /** @var bool */ + protected $ignoreEmptyContextAndExtra; + /** @var bool */ + protected $includeStacktraces = false; + + /** + * @param self::BATCH_MODE_* $batchMode + */ + public function __construct(int $batchMode = self::BATCH_MODE_JSON, bool $appendNewline = true, bool $ignoreEmptyContextAndExtra = false, bool $includeStacktraces = false) + { + $this->batchMode = $batchMode; + $this->appendNewline = $appendNewline; + $this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra; + $this->includeStacktraces = $includeStacktraces; + + parent::__construct(); + } + + /** + * The batch mode option configures the formatting style for + * multiple records. By default, multiple records will be + * formatted as a JSON-encoded array. However, for + * compatibility with some API endpoints, alternative styles + * are available. + */ + public function getBatchMode(): int + { + return $this->batchMode; + } + + /** + * True if newlines are appended to every formatted record + */ + public function isAppendingNewlines(): bool + { + return $this->appendNewline; + } + + /** + * {@inheritDoc} + */ + public function format(array $record): string + { + $normalized = $this->normalize($record); + + if (isset($normalized['context']) && $normalized['context'] === []) { + if ($this->ignoreEmptyContextAndExtra) { + unset($normalized['context']); + } else { + $normalized['context'] = new \stdClass; + } + } + if (isset($normalized['extra']) && $normalized['extra'] === []) { + if ($this->ignoreEmptyContextAndExtra) { + unset($normalized['extra']); + } else { + $normalized['extra'] = new \stdClass; + } + } + + return $this->toJson($normalized, true) . ($this->appendNewline ? "\n" : ''); + } + + /** + * {@inheritDoc} + */ + public function formatBatch(array $records): string + { + switch ($this->batchMode) { + case static::BATCH_MODE_NEWLINES: + return $this->formatBatchNewlines($records); + + case static::BATCH_MODE_JSON: + default: + return $this->formatBatchJson($records); + } + } + + /** + * @return self + */ + public function includeStacktraces(bool $include = true): self + { + $this->includeStacktraces = $include; + + return $this; + } + + /** + * Return a JSON-encoded array of records. + * + * @phpstan-param Record[] $records + */ + protected function formatBatchJson(array $records): string + { + return $this->toJson($this->normalize($records), true); + } + + /** + * Use new lines to separate records instead of a + * JSON-encoded array. + * + * @phpstan-param Record[] $records + */ + protected function formatBatchNewlines(array $records): string + { + $instance = $this; + + $oldNewline = $this->appendNewline; + $this->appendNewline = false; + array_walk($records, function (&$value, $key) use ($instance) { + $value = $instance->format($value); + }); + $this->appendNewline = $oldNewline; + + return implode("\n", $records); + } + + /** + * Normalizes given $data. + * + * @param mixed $data + * + * @return mixed + */ + protected function normalize($data, int $depth = 0) + { + if ($depth > $this->maxNormalizeDepth) { + return 'Over '.$this->maxNormalizeDepth.' levels deep, aborting normalization'; + } + + if (is_array($data)) { + $normalized = []; + + $count = 1; + foreach ($data as $key => $value) { + if ($count++ > $this->maxNormalizeItemCount) { + $normalized['...'] = 'Over '.$this->maxNormalizeItemCount.' items ('.count($data).' total), aborting normalization'; + break; + } + + $normalized[$key] = $this->normalize($value, $depth + 1); + } + + return $normalized; + } + + if (is_object($data)) { + if ($data instanceof \DateTimeInterface) { + return $this->formatDate($data); + } + + if ($data instanceof Throwable) { + return $this->normalizeException($data, $depth); + } + + // if the object has specific json serializability we want to make sure we skip the __toString treatment below + if ($data instanceof \JsonSerializable) { + return $data; + } + + if (method_exists($data, '__toString')) { + return $data->__toString(); + } + + return $data; + } + + if (is_resource($data)) { + return parent::normalize($data); + } + + return $data; + } + + /** + * Normalizes given exception with or without its own stack trace based on + * `includeStacktraces` property. + * + * {@inheritDoc} + */ + protected function normalizeException(Throwable $e, int $depth = 0): array + { + $data = parent::normalizeException($e, $depth); + if (!$this->includeStacktraces) { + unset($data['trace']); + } + + return $data; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php b/vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php new file mode 100644 index 0000000..e6e7898 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php @@ -0,0 +1,246 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +use Monolog\Utils; + +/** + * Formats incoming records into a one-line string + * + * This is especially useful for logging to files + * + * @author Jordi Boggiano + * @author Christophe Coevoet + */ +class LineFormatter extends NormalizerFormatter +{ + public const SIMPLE_FORMAT = "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n"; + + /** @var string */ + protected $format; + /** @var bool */ + protected $allowInlineLineBreaks; + /** @var bool */ + protected $ignoreEmptyContextAndExtra; + /** @var bool */ + protected $includeStacktraces; + /** @var ?callable */ + protected $stacktracesParser; + + /** + * @param string|null $format The format of the message + * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format + * @param bool $allowInlineLineBreaks Whether to allow inline line breaks in log entries + * @param bool $ignoreEmptyContextAndExtra + */ + public function __construct(?string $format = null, ?string $dateFormat = null, bool $allowInlineLineBreaks = false, bool $ignoreEmptyContextAndExtra = false, bool $includeStacktraces = false) + { + $this->format = $format === null ? static::SIMPLE_FORMAT : $format; + $this->allowInlineLineBreaks = $allowInlineLineBreaks; + $this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra; + $this->includeStacktraces($includeStacktraces); + parent::__construct($dateFormat); + } + + public function includeStacktraces(bool $include = true, ?callable $parser = null): self + { + $this->includeStacktraces = $include; + if ($this->includeStacktraces) { + $this->allowInlineLineBreaks = true; + $this->stacktracesParser = $parser; + } + + return $this; + } + + public function allowInlineLineBreaks(bool $allow = true): self + { + $this->allowInlineLineBreaks = $allow; + + return $this; + } + + public function ignoreEmptyContextAndExtra(bool $ignore = true): self + { + $this->ignoreEmptyContextAndExtra = $ignore; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function format(array $record): string + { + $vars = parent::format($record); + + $output = $this->format; + + foreach ($vars['extra'] as $var => $val) { + if (false !== strpos($output, '%extra.'.$var.'%')) { + $output = str_replace('%extra.'.$var.'%', $this->stringify($val), $output); + unset($vars['extra'][$var]); + } + } + + foreach ($vars['context'] as $var => $val) { + if (false !== strpos($output, '%context.'.$var.'%')) { + $output = str_replace('%context.'.$var.'%', $this->stringify($val), $output); + unset($vars['context'][$var]); + } + } + + if ($this->ignoreEmptyContextAndExtra) { + if (empty($vars['context'])) { + unset($vars['context']); + $output = str_replace('%context%', '', $output); + } + + if (empty($vars['extra'])) { + unset($vars['extra']); + $output = str_replace('%extra%', '', $output); + } + } + + foreach ($vars as $var => $val) { + if (false !== strpos($output, '%'.$var.'%')) { + $output = str_replace('%'.$var.'%', $this->stringify($val), $output); + } + } + + // remove leftover %extra.xxx% and %context.xxx% if any + if (false !== strpos($output, '%')) { + $output = preg_replace('/%(?:extra|context)\..+?%/', '', $output); + if (null === $output) { + $pcreErrorCode = preg_last_error(); + throw new \RuntimeException('Failed to run preg_replace: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode)); + } + } + + return $output; + } + + public function formatBatch(array $records): string + { + $message = ''; + foreach ($records as $record) { + $message .= $this->format($record); + } + + return $message; + } + + /** + * @param mixed $value + */ + public function stringify($value): string + { + return $this->replaceNewlines($this->convertToString($value)); + } + + protected function normalizeException(\Throwable $e, int $depth = 0): string + { + $str = $this->formatException($e); + + if ($previous = $e->getPrevious()) { + do { + $depth++; + if ($depth > $this->maxNormalizeDepth) { + $str .= "\n[previous exception] Over " . $this->maxNormalizeDepth . ' levels deep, aborting normalization'; + break; + } + + $str .= "\n[previous exception] " . $this->formatException($previous); + } while ($previous = $previous->getPrevious()); + } + + return $str; + } + + /** + * @param mixed $data + */ + protected function convertToString($data): string + { + if (null === $data || is_bool($data)) { + return var_export($data, true); + } + + if (is_scalar($data)) { + return (string) $data; + } + + return $this->toJson($data, true); + } + + protected function replaceNewlines(string $str): string + { + if ($this->allowInlineLineBreaks) { + if (0 === strpos($str, '{')) { + $str = preg_replace('/(?getCode(); + if ($e instanceof \SoapFault) { + if (isset($e->faultcode)) { + $str .= ' faultcode: ' . $e->faultcode; + } + + if (isset($e->faultactor)) { + $str .= ' faultactor: ' . $e->faultactor; + } + + if (isset($e->detail)) { + if (is_string($e->detail)) { + $str .= ' detail: ' . $e->detail; + } elseif (is_object($e->detail) || is_array($e->detail)) { + $str .= ' detail: ' . $this->toJson($e->detail, true); + } + } + } + $str .= '): ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine() . ')'; + + if ($this->includeStacktraces) { + $str .= $this->stacktracesParser($e); + } + + return $str; + } + + private function stacktracesParser(\Throwable $e): string + { + $trace = $e->getTraceAsString(); + + if ($this->stacktracesParser) { + $trace = $this->stacktracesParserCustom($trace); + } + + return "\n[stacktrace]\n" . $trace . "\n"; + } + + private function stacktracesParserCustom(string $trace): string + { + return implode("\n", array_filter(array_map($this->stacktracesParser, explode("\n", $trace)))); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Formatter/LogglyFormatter.php b/vendor/monolog/monolog/src/Monolog/Formatter/LogglyFormatter.php new file mode 100644 index 0000000..29841aa --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Formatter/LogglyFormatter.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +/** + * Encodes message information into JSON in a format compatible with Loggly. + * + * @author Adam Pancutt + */ +class LogglyFormatter extends JsonFormatter +{ + /** + * Overrides the default batch mode to new lines for compatibility with the + * Loggly bulk API. + */ + public function __construct(int $batchMode = self::BATCH_MODE_NEWLINES, bool $appendNewline = false) + { + parent::__construct($batchMode, $appendNewline); + } + + /** + * Appends the 'timestamp' parameter for indexing by Loggly. + * + * @see https://www.loggly.com/docs/automated-parsing/#json + * @see \Monolog\Formatter\JsonFormatter::format() + */ + public function format(array $record): string + { + if (isset($record["datetime"]) && ($record["datetime"] instanceof \DateTimeInterface)) { + $record["timestamp"] = $record["datetime"]->format("Y-m-d\TH:i:s.uO"); + unset($record["datetime"]); + } + + return parent::format($record); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Formatter/LogmaticFormatter.php b/vendor/monolog/monolog/src/Monolog/Formatter/LogmaticFormatter.php new file mode 100644 index 0000000..b0451ab --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Formatter/LogmaticFormatter.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +/** + * Encodes message information into JSON in a format compatible with Logmatic. + * + * @author Julien Breux + */ +class LogmaticFormatter extends JsonFormatter +{ + protected const MARKERS = ["sourcecode", "php"]; + + /** + * @var string + */ + protected $hostname = ''; + + /** + * @var string + */ + protected $appname = ''; + + public function setHostname(string $hostname): self + { + $this->hostname = $hostname; + + return $this; + } + + public function setAppname(string $appname): self + { + $this->appname = $appname; + + return $this; + } + + /** + * Appends the 'hostname' and 'appname' parameter for indexing by Logmatic. + * + * @see http://doc.logmatic.io/docs/basics-to-send-data + * @see \Monolog\Formatter\JsonFormatter::format() + */ + public function format(array $record): string + { + if (!empty($this->hostname)) { + $record["hostname"] = $this->hostname; + } + if (!empty($this->appname)) { + $record["appname"] = $this->appname; + } + + $record["@marker"] = static::MARKERS; + + return parent::format($record); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Formatter/LogstashFormatter.php b/vendor/monolog/monolog/src/Monolog/Formatter/LogstashFormatter.php new file mode 100644 index 0000000..f8de0d3 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Formatter/LogstashFormatter.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +/** + * Serializes a log message to Logstash Event Format + * + * @see https://www.elastic.co/products/logstash + * @see https://github.com/elastic/logstash/blob/master/logstash-core/src/main/java/org/logstash/Event.java + * + * @author Tim Mower + */ +class LogstashFormatter extends NormalizerFormatter +{ + /** + * @var string the name of the system for the Logstash log message, used to fill the @source field + */ + protected $systemName; + + /** + * @var string an application name for the Logstash log message, used to fill the @type field + */ + protected $applicationName; + + /** + * @var string the key for 'extra' fields from the Monolog record + */ + protected $extraKey; + + /** + * @var string the key for 'context' fields from the Monolog record + */ + protected $contextKey; + + /** + * @param string $applicationName The application that sends the data, used as the "type" field of logstash + * @param string|null $systemName The system/machine name, used as the "source" field of logstash, defaults to the hostname of the machine + * @param string $extraKey The key for extra keys inside logstash "fields", defaults to extra + * @param string $contextKey The key for context keys inside logstash "fields", defaults to context + */ + public function __construct(string $applicationName, ?string $systemName = null, string $extraKey = 'extra', string $contextKey = 'context') + { + // logstash requires a ISO 8601 format date with optional millisecond precision. + parent::__construct('Y-m-d\TH:i:s.uP'); + + $this->systemName = $systemName === null ? (string) gethostname() : $systemName; + $this->applicationName = $applicationName; + $this->extraKey = $extraKey; + $this->contextKey = $contextKey; + } + + /** + * {@inheritDoc} + */ + public function format(array $record): string + { + $record = parent::format($record); + + if (empty($record['datetime'])) { + $record['datetime'] = gmdate('c'); + } + $message = [ + '@timestamp' => $record['datetime'], + '@version' => 1, + 'host' => $this->systemName, + ]; + if (isset($record['message'])) { + $message['message'] = $record['message']; + } + if (isset($record['channel'])) { + $message['type'] = $record['channel']; + $message['channel'] = $record['channel']; + } + if (isset($record['level_name'])) { + $message['level'] = $record['level_name']; + } + if (isset($record['level'])) { + $message['monolog_level'] = $record['level']; + } + if ($this->applicationName) { + $message['type'] = $this->applicationName; + } + if (!empty($record['extra'])) { + $message[$this->extraKey] = $record['extra']; + } + if (!empty($record['context'])) { + $message[$this->contextKey] = $record['context']; + } + + return $this->toJson($message) . "\n"; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php b/vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php new file mode 100644 index 0000000..fca69a8 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +use MongoDB\BSON\Type; +use MongoDB\BSON\UTCDateTime; +use Monolog\Utils; + +/** + * Formats a record for use with the MongoDBHandler. + * + * @author Florian Plattner + */ +class MongoDBFormatter implements FormatterInterface +{ + /** @var bool */ + private $exceptionTraceAsString; + /** @var int */ + private $maxNestingLevel; + /** @var bool */ + private $isLegacyMongoExt; + + /** + * @param int $maxNestingLevel 0 means infinite nesting, the $record itself is level 1, $record['context'] is 2 + * @param bool $exceptionTraceAsString set to false to log exception traces as a sub documents instead of strings + */ + public function __construct(int $maxNestingLevel = 3, bool $exceptionTraceAsString = true) + { + $this->maxNestingLevel = max($maxNestingLevel, 0); + $this->exceptionTraceAsString = $exceptionTraceAsString; + + $this->isLegacyMongoExt = extension_loaded('mongodb') && version_compare((string) phpversion('mongodb'), '1.1.9', '<='); + } + + /** + * {@inheritDoc} + * + * @return mixed[] + */ + public function format(array $record): array + { + /** @var mixed[] $res */ + $res = $this->formatArray($record); + + return $res; + } + + /** + * {@inheritDoc} + * + * @return array + */ + public function formatBatch(array $records): array + { + $formatted = []; + foreach ($records as $key => $record) { + $formatted[$key] = $this->format($record); + } + + return $formatted; + } + + /** + * @param mixed[] $array + * @return mixed[]|string Array except when max nesting level is reached then a string "[...]" + */ + protected function formatArray(array $array, int $nestingLevel = 0) + { + if ($this->maxNestingLevel > 0 && $nestingLevel > $this->maxNestingLevel) { + return '[...]'; + } + + foreach ($array as $name => $value) { + if ($value instanceof \DateTimeInterface) { + $array[$name] = $this->formatDate($value, $nestingLevel + 1); + } elseif ($value instanceof \Throwable) { + $array[$name] = $this->formatException($value, $nestingLevel + 1); + } elseif (is_array($value)) { + $array[$name] = $this->formatArray($value, $nestingLevel + 1); + } elseif (is_object($value) && !$value instanceof Type) { + $array[$name] = $this->formatObject($value, $nestingLevel + 1); + } + } + + return $array; + } + + /** + * @param mixed $value + * @return mixed[]|string + */ + protected function formatObject($value, int $nestingLevel) + { + $objectVars = get_object_vars($value); + $objectVars['class'] = Utils::getClass($value); + + return $this->formatArray($objectVars, $nestingLevel); + } + + /** + * @return mixed[]|string + */ + protected function formatException(\Throwable $exception, int $nestingLevel) + { + $formattedException = [ + 'class' => Utils::getClass($exception), + 'message' => $exception->getMessage(), + 'code' => (int) $exception->getCode(), + 'file' => $exception->getFile() . ':' . $exception->getLine(), + ]; + + if ($this->exceptionTraceAsString === true) { + $formattedException['trace'] = $exception->getTraceAsString(); + } else { + $formattedException['trace'] = $exception->getTrace(); + } + + return $this->formatArray($formattedException, $nestingLevel); + } + + protected function formatDate(\DateTimeInterface $value, int $nestingLevel): UTCDateTime + { + if ($this->isLegacyMongoExt) { + return $this->legacyGetMongoDbDateTime($value); + } + + return $this->getMongoDbDateTime($value); + } + + private function getMongoDbDateTime(\DateTimeInterface $value): UTCDateTime + { + return new UTCDateTime((int) floor(((float) $value->format('U.u')) * 1000)); + } + + /** + * This is needed to support MongoDB Driver v1.19 and below + * + * See https://github.com/mongodb/mongo-php-driver/issues/426 + * + * It can probably be removed in 2.1 or later once MongoDB's 1.2 is released and widely adopted + */ + private function legacyGetMongoDbDateTime(\DateTimeInterface $value): UTCDateTime + { + $milliseconds = floor(((float) $value->format('U.u')) * 1000); + + $milliseconds = (PHP_INT_SIZE == 8) //64-bit OS? + ? (int) $milliseconds + : (string) $milliseconds; + + // @phpstan-ignore-next-line + return new UTCDateTime($milliseconds); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Formatter/NormalizerFormatter.php b/vendor/monolog/monolog/src/Monolog/Formatter/NormalizerFormatter.php new file mode 100644 index 0000000..f926a84 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Formatter/NormalizerFormatter.php @@ -0,0 +1,290 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +use Monolog\DateTimeImmutable; +use Monolog\Utils; +use Throwable; + +/** + * Normalizes incoming records to remove objects/resources so it's easier to dump to various targets + * + * @author Jordi Boggiano + */ +class NormalizerFormatter implements FormatterInterface +{ + public const SIMPLE_DATE = "Y-m-d\TH:i:sP"; + + /** @var string */ + protected $dateFormat; + /** @var int */ + protected $maxNormalizeDepth = 9; + /** @var int */ + protected $maxNormalizeItemCount = 1000; + + /** @var int */ + private $jsonEncodeOptions = Utils::DEFAULT_JSON_FLAGS; + + /** + * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format + */ + public function __construct(?string $dateFormat = null) + { + $this->dateFormat = null === $dateFormat ? static::SIMPLE_DATE : $dateFormat; + if (!function_exists('json_encode')) { + throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s NormalizerFormatter'); + } + } + + /** + * {@inheritDoc} + * + * @param mixed[] $record + */ + public function format(array $record) + { + return $this->normalize($record); + } + + /** + * {@inheritDoc} + */ + public function formatBatch(array $records) + { + foreach ($records as $key => $record) { + $records[$key] = $this->format($record); + } + + return $records; + } + + public function getDateFormat(): string + { + return $this->dateFormat; + } + + public function setDateFormat(string $dateFormat): self + { + $this->dateFormat = $dateFormat; + + return $this; + } + + /** + * The maximum number of normalization levels to go through + */ + public function getMaxNormalizeDepth(): int + { + return $this->maxNormalizeDepth; + } + + public function setMaxNormalizeDepth(int $maxNormalizeDepth): self + { + $this->maxNormalizeDepth = $maxNormalizeDepth; + + return $this; + } + + /** + * The maximum number of items to normalize per level + */ + public function getMaxNormalizeItemCount(): int + { + return $this->maxNormalizeItemCount; + } + + public function setMaxNormalizeItemCount(int $maxNormalizeItemCount): self + { + $this->maxNormalizeItemCount = $maxNormalizeItemCount; + + return $this; + } + + /** + * Enables `json_encode` pretty print. + */ + public function setJsonPrettyPrint(bool $enable): self + { + if ($enable) { + $this->jsonEncodeOptions |= JSON_PRETTY_PRINT; + } else { + $this->jsonEncodeOptions &= ~JSON_PRETTY_PRINT; + } + + return $this; + } + + /** + * @param mixed $data + * @return null|scalar|array + */ + protected function normalize($data, int $depth = 0) + { + if ($depth > $this->maxNormalizeDepth) { + return 'Over ' . $this->maxNormalizeDepth . ' levels deep, aborting normalization'; + } + + if (null === $data || is_scalar($data)) { + if (is_float($data)) { + if (is_infinite($data)) { + return ($data > 0 ? '' : '-') . 'INF'; + } + if (is_nan($data)) { + return 'NaN'; + } + } + + return $data; + } + + if (is_array($data)) { + $normalized = []; + + $count = 1; + foreach ($data as $key => $value) { + if ($count++ > $this->maxNormalizeItemCount) { + $normalized['...'] = 'Over ' . $this->maxNormalizeItemCount . ' items ('.count($data).' total), aborting normalization'; + break; + } + + $normalized[$key] = $this->normalize($value, $depth + 1); + } + + return $normalized; + } + + if ($data instanceof \DateTimeInterface) { + return $this->formatDate($data); + } + + if (is_object($data)) { + if ($data instanceof Throwable) { + return $this->normalizeException($data, $depth); + } + + if ($data instanceof \JsonSerializable) { + /** @var null|scalar|array $value */ + $value = $data->jsonSerialize(); + } elseif (\get_class($data) === '__PHP_Incomplete_Class') { + $accessor = new \ArrayObject($data); + $value = (string) $accessor['__PHP_Incomplete_Class_Name']; + } elseif (method_exists($data, '__toString')) { + /** @var string $value */ + $value = $data->__toString(); + } else { + // the rest is normalized by json encoding and decoding it + /** @var null|scalar|array $value */ + $value = json_decode($this->toJson($data, true), true); + } + + return [Utils::getClass($data) => $value]; + } + + if (is_resource($data)) { + return sprintf('[resource(%s)]', get_resource_type($data)); + } + + return '[unknown('.gettype($data).')]'; + } + + /** + * @return mixed[] + */ + protected function normalizeException(Throwable $e, int $depth = 0) + { + if ($depth > $this->maxNormalizeDepth) { + return ['Over ' . $this->maxNormalizeDepth . ' levels deep, aborting normalization']; + } + + if ($e instanceof \JsonSerializable) { + return (array) $e->jsonSerialize(); + } + + $data = [ + 'class' => Utils::getClass($e), + 'message' => $e->getMessage(), + 'code' => (int) $e->getCode(), + 'file' => $e->getFile().':'.$e->getLine(), + ]; + + if ($e instanceof \SoapFault) { + if (isset($e->faultcode)) { + $data['faultcode'] = $e->faultcode; + } + + if (isset($e->faultactor)) { + $data['faultactor'] = $e->faultactor; + } + + if (isset($e->detail)) { + if (is_string($e->detail)) { + $data['detail'] = $e->detail; + } elseif (is_object($e->detail) || is_array($e->detail)) { + $data['detail'] = $this->toJson($e->detail, true); + } + } + } + + $trace = $e->getTrace(); + foreach ($trace as $frame) { + if (isset($frame['file'])) { + $data['trace'][] = $frame['file'].':'.$frame['line']; + } + } + + if ($previous = $e->getPrevious()) { + $data['previous'] = $this->normalizeException($previous, $depth + 1); + } + + return $data; + } + + /** + * Return the JSON representation of a value + * + * @param mixed $data + * @throws \RuntimeException if encoding fails and errors are not ignored + * @return string if encoding fails and ignoreErrors is true 'null' is returned + */ + protected function toJson($data, bool $ignoreErrors = false): string + { + return Utils::jsonEncode($data, $this->jsonEncodeOptions, $ignoreErrors); + } + + /** + * @return string + */ + protected function formatDate(\DateTimeInterface $date) + { + // in case the date format isn't custom then we defer to the custom DateTimeImmutable + // formatting logic, which will pick the right format based on whether useMicroseconds is on + if ($this->dateFormat === self::SIMPLE_DATE && $date instanceof DateTimeImmutable) { + return (string) $date; + } + + return $date->format($this->dateFormat); + } + + public function addJsonEncodeOption(int $option): self + { + $this->jsonEncodeOptions |= $option; + + return $this; + } + + public function removeJsonEncodeOption(int $option): self + { + $this->jsonEncodeOptions &= ~$option; + + return $this; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Formatter/ScalarFormatter.php b/vendor/monolog/monolog/src/Monolog/Formatter/ScalarFormatter.php new file mode 100644 index 0000000..187bc55 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Formatter/ScalarFormatter.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +/** + * Formats data into an associative array of scalar values. + * Objects and arrays will be JSON encoded. + * + * @author Andrew Lawson + */ +class ScalarFormatter extends NormalizerFormatter +{ + /** + * {@inheritDoc} + * + * @phpstan-return array $record + */ + public function format(array $record): array + { + $result = []; + foreach ($record as $key => $value) { + $result[$key] = $this->normalizeValue($value); + } + + return $result; + } + + /** + * @param mixed $value + * @return scalar|null + */ + protected function normalizeValue($value) + { + $normalized = $this->normalize($value); + + if (is_array($normalized)) { + return $this->toJson($normalized, true); + } + + return $normalized; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Formatter/WildfireFormatter.php b/vendor/monolog/monolog/src/Monolog/Formatter/WildfireFormatter.php new file mode 100644 index 0000000..6539b34 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Formatter/WildfireFormatter.php @@ -0,0 +1,139 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +use Monolog\Logger; + +/** + * Serializes a log message according to Wildfire's header requirements + * + * @author Eric Clemmons (@ericclemmons) + * @author Christophe Coevoet + * @author Kirill chEbba Chebunin + * + * @phpstan-import-type Level from \Monolog\Logger + */ +class WildfireFormatter extends NormalizerFormatter +{ + /** + * Translates Monolog log levels to Wildfire levels. + * + * @var array + */ + private $logLevels = [ + Logger::DEBUG => 'LOG', + Logger::INFO => 'INFO', + Logger::NOTICE => 'INFO', + Logger::WARNING => 'WARN', + Logger::ERROR => 'ERROR', + Logger::CRITICAL => 'ERROR', + Logger::ALERT => 'ERROR', + Logger::EMERGENCY => 'ERROR', + ]; + + /** + * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format + */ + public function __construct(?string $dateFormat = null) + { + parent::__construct($dateFormat); + + // http headers do not like non-ISO-8559-1 characters + $this->removeJsonEncodeOption(JSON_UNESCAPED_UNICODE); + } + + /** + * {@inheritDoc} + * + * @return string + */ + public function format(array $record): string + { + // Retrieve the line and file if set and remove them from the formatted extra + $file = $line = ''; + if (isset($record['extra']['file'])) { + $file = $record['extra']['file']; + unset($record['extra']['file']); + } + if (isset($record['extra']['line'])) { + $line = $record['extra']['line']; + unset($record['extra']['line']); + } + + /** @var mixed[] $record */ + $record = $this->normalize($record); + $message = ['message' => $record['message']]; + $handleError = false; + if ($record['context']) { + $message['context'] = $record['context']; + $handleError = true; + } + if ($record['extra']) { + $message['extra'] = $record['extra']; + $handleError = true; + } + if (count($message) === 1) { + $message = reset($message); + } + + if (isset($record['context']['table'])) { + $type = 'TABLE'; + $label = $record['channel'] .': '. $record['message']; + $message = $record['context']['table']; + } else { + $type = $this->logLevels[$record['level']]; + $label = $record['channel']; + } + + // Create JSON object describing the appearance of the message in the console + $json = $this->toJson([ + [ + 'Type' => $type, + 'File' => $file, + 'Line' => $line, + 'Label' => $label, + ], + $message, + ], $handleError); + + // The message itself is a serialization of the above JSON object + it's length + return sprintf( + '%d|%s|', + strlen($json), + $json + ); + } + + /** + * {@inheritDoc} + * + * @phpstan-return never + */ + public function formatBatch(array $records) + { + throw new \BadMethodCallException('Batch formatting does not make sense for the WildfireFormatter'); + } + + /** + * {@inheritDoc} + * + * @return null|scalar|array|object + */ + protected function normalize($data, int $depth = 0) + { + if (is_object($data) && !$data instanceof \DateTimeInterface) { + return $data; + } + + return parent::normalize($data, $depth); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/AbstractHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/AbstractHandler.php new file mode 100644 index 0000000..a5cdaa7 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/AbstractHandler.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\ResettableInterface; +use Psr\Log\LogLevel; + +/** + * Base Handler class providing basic level/bubble support + * + * @author Jordi Boggiano + * + * @phpstan-import-type Level from \Monolog\Logger + * @phpstan-import-type LevelName from \Monolog\Logger + */ +abstract class AbstractHandler extends Handler implements ResettableInterface +{ + /** + * @var int + * @phpstan-var Level + */ + protected $level = Logger::DEBUG; + /** @var bool */ + protected $bubble = true; + + /** + * @param int|string $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * + * @phpstan-param Level|LevelName|LogLevel::* $level + */ + public function __construct($level = Logger::DEBUG, bool $bubble = true) + { + $this->setLevel($level); + $this->bubble = $bubble; + } + + /** + * {@inheritDoc} + */ + public function isHandling(array $record): bool + { + return $record['level'] >= $this->level; + } + + /** + * Sets minimum logging level at which this handler will be triggered. + * + * @param Level|LevelName|LogLevel::* $level Level or level name + * @return self + */ + public function setLevel($level): self + { + $this->level = Logger::toMonologLevel($level); + + return $this; + } + + /** + * Gets minimum logging level at which this handler will be triggered. + * + * @return int + * + * @phpstan-return Level + */ + public function getLevel(): int + { + return $this->level; + } + + /** + * Sets the bubbling behavior. + * + * @param bool $bubble true means that this handler allows bubbling. + * false means that bubbling is not permitted. + * @return self + */ + public function setBubble(bool $bubble): self + { + $this->bubble = $bubble; + + return $this; + } + + /** + * Gets the bubbling behavior. + * + * @return bool true means that this handler allows bubbling. + * false means that bubbling is not permitted. + */ + public function getBubble(): bool + { + return $this->bubble; + } + + /** + * {@inheritDoc} + */ + public function reset() + { + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/AbstractProcessingHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/AbstractProcessingHandler.php new file mode 100644 index 0000000..77e533f --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/AbstractProcessingHandler.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +/** + * Base Handler class providing the Handler structure, including processors and formatters + * + * Classes extending it should (in most cases) only implement write($record) + * + * @author Jordi Boggiano + * @author Christophe Coevoet + * + * @phpstan-import-type LevelName from \Monolog\Logger + * @phpstan-import-type Level from \Monolog\Logger + * @phpstan-import-type Record from \Monolog\Logger + * @phpstan-type FormattedRecord array{message: string, context: mixed[], level: Level, level_name: LevelName, channel: string, datetime: \DateTimeImmutable, extra: mixed[], formatted: mixed} + */ +abstract class AbstractProcessingHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface +{ + use ProcessableHandlerTrait; + use FormattableHandlerTrait; + + /** + * {@inheritDoc} + */ + public function handle(array $record): bool + { + if (!$this->isHandling($record)) { + return false; + } + + if ($this->processors) { + /** @var Record $record */ + $record = $this->processRecord($record); + } + + $record['formatted'] = $this->getFormatter()->format($record); + + $this->write($record); + + return false === $this->bubble; + } + + /** + * Writes the record down to the log of the implementing handler + * + * @phpstan-param FormattedRecord $record + */ + abstract protected function write(array $record): void; + + /** + * @return void + */ + public function reset() + { + parent::reset(); + + $this->resetProcessors(); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/AbstractSyslogHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/AbstractSyslogHandler.php new file mode 100644 index 0000000..5e5ad1c --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/AbstractSyslogHandler.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LineFormatter; + +/** + * Common syslog functionality + * + * @phpstan-import-type Level from \Monolog\Logger + */ +abstract class AbstractSyslogHandler extends AbstractProcessingHandler +{ + /** @var int */ + protected $facility; + + /** + * Translates Monolog log levels to syslog log priorities. + * @var array + * @phpstan-var array + */ + protected $logLevels = [ + Logger::DEBUG => LOG_DEBUG, + Logger::INFO => LOG_INFO, + Logger::NOTICE => LOG_NOTICE, + Logger::WARNING => LOG_WARNING, + Logger::ERROR => LOG_ERR, + Logger::CRITICAL => LOG_CRIT, + Logger::ALERT => LOG_ALERT, + Logger::EMERGENCY => LOG_EMERG, + ]; + + /** + * List of valid log facility names. + * @var array + */ + protected $facilities = [ + 'auth' => LOG_AUTH, + 'authpriv' => LOG_AUTHPRIV, + 'cron' => LOG_CRON, + 'daemon' => LOG_DAEMON, + 'kern' => LOG_KERN, + 'lpr' => LOG_LPR, + 'mail' => LOG_MAIL, + 'news' => LOG_NEWS, + 'syslog' => LOG_SYSLOG, + 'user' => LOG_USER, + 'uucp' => LOG_UUCP, + ]; + + /** + * @param string|int $facility Either one of the names of the keys in $this->facilities, or a LOG_* facility constant + */ + public function __construct($facility = LOG_USER, $level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + + if (!defined('PHP_WINDOWS_VERSION_BUILD')) { + $this->facilities['local0'] = LOG_LOCAL0; + $this->facilities['local1'] = LOG_LOCAL1; + $this->facilities['local2'] = LOG_LOCAL2; + $this->facilities['local3'] = LOG_LOCAL3; + $this->facilities['local4'] = LOG_LOCAL4; + $this->facilities['local5'] = LOG_LOCAL5; + $this->facilities['local6'] = LOG_LOCAL6; + $this->facilities['local7'] = LOG_LOCAL7; + } else { + $this->facilities['local0'] = 128; // LOG_LOCAL0 + $this->facilities['local1'] = 136; // LOG_LOCAL1 + $this->facilities['local2'] = 144; // LOG_LOCAL2 + $this->facilities['local3'] = 152; // LOG_LOCAL3 + $this->facilities['local4'] = 160; // LOG_LOCAL4 + $this->facilities['local5'] = 168; // LOG_LOCAL5 + $this->facilities['local6'] = 176; // LOG_LOCAL6 + $this->facilities['local7'] = 184; // LOG_LOCAL7 + } + + // convert textual description of facility to syslog constant + if (is_string($facility) && array_key_exists(strtolower($facility), $this->facilities)) { + $facility = $this->facilities[strtolower($facility)]; + } elseif (!in_array($facility, array_values($this->facilities), true)) { + throw new \UnexpectedValueException('Unknown facility value "'.$facility.'" given'); + } + + $this->facility = $facility; + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new LineFormatter('%channel%.%level_name%: %message% %context% %extra%'); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/AmqpHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/AmqpHandler.php new file mode 100644 index 0000000..994872c --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/AmqpHandler.php @@ -0,0 +1,171 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\JsonFormatter; +use PhpAmqpLib\Message\AMQPMessage; +use PhpAmqpLib\Channel\AMQPChannel; +use AMQPExchange; + +/** + * @phpstan-import-type Record from \Monolog\Logger + */ +class AmqpHandler extends AbstractProcessingHandler +{ + /** + * @var AMQPExchange|AMQPChannel $exchange + */ + protected $exchange; + /** @var array */ + private $extraAttributes = []; + + /** + * @return array + */ + public function getExtraAttributes(): array + { + return $this->extraAttributes; + } + + /** + * Configure extra attributes to pass to the AMQPExchange (if you are using the amqp extension) + * + * @param array $extraAttributes One of content_type, content_encoding, + * message_id, user_id, app_id, delivery_mode, + * priority, timestamp, expiration, type + * or reply_to, headers. + * @return AmqpHandler + */ + public function setExtraAttributes(array $extraAttributes): self + { + $this->extraAttributes = $extraAttributes; + return $this; + } + + /** + * @var string + */ + protected $exchangeName; + + /** + * @param AMQPExchange|AMQPChannel $exchange AMQPExchange (php AMQP ext) or PHP AMQP lib channel, ready for use + * @param string|null $exchangeName Optional exchange name, for AMQPChannel (PhpAmqpLib) only + */ + public function __construct($exchange, ?string $exchangeName = null, $level = Logger::DEBUG, bool $bubble = true) + { + if ($exchange instanceof AMQPChannel) { + $this->exchangeName = (string) $exchangeName; + } elseif (!$exchange instanceof AMQPExchange) { + throw new \InvalidArgumentException('PhpAmqpLib\Channel\AMQPChannel or AMQPExchange instance required'); + } elseif ($exchangeName) { + @trigger_error('The $exchangeName parameter can only be passed when using PhpAmqpLib, if using an AMQPExchange instance configure it beforehand', E_USER_DEPRECATED); + } + $this->exchange = $exchange; + + parent::__construct($level, $bubble); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $data = $record["formatted"]; + $routingKey = $this->getRoutingKey($record); + + if ($this->exchange instanceof AMQPExchange) { + $attributes = [ + 'delivery_mode' => 2, + 'content_type' => 'application/json', + ]; + if ($this->extraAttributes) { + $attributes = array_merge($attributes, $this->extraAttributes); + } + $this->exchange->publish( + $data, + $routingKey, + 0, + $attributes + ); + } else { + $this->exchange->basic_publish( + $this->createAmqpMessage($data), + $this->exchangeName, + $routingKey + ); + } + } + + /** + * {@inheritDoc} + */ + public function handleBatch(array $records): void + { + if ($this->exchange instanceof AMQPExchange) { + parent::handleBatch($records); + + return; + } + + foreach ($records as $record) { + if (!$this->isHandling($record)) { + continue; + } + + /** @var Record $record */ + $record = $this->processRecord($record); + $data = $this->getFormatter()->format($record); + + $this->exchange->batch_basic_publish( + $this->createAmqpMessage($data), + $this->exchangeName, + $this->getRoutingKey($record) + ); + } + + $this->exchange->publish_batch(); + } + + /** + * Gets the routing key for the AMQP exchange + * + * @phpstan-param Record $record + */ + protected function getRoutingKey(array $record): string + { + $routingKey = sprintf('%s.%s', $record['level_name'], $record['channel']); + + return strtolower($routingKey); + } + + private function createAmqpMessage(string $data): AMQPMessage + { + $attributes = [ + 'delivery_mode' => 2, + 'content_type' => 'application/json', + ]; + if ($this->extraAttributes) { + $attributes = array_merge($attributes, $this->extraAttributes); + } + return new AMQPMessage($data, $attributes); + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/BrowserConsoleHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/BrowserConsoleHandler.php new file mode 100644 index 0000000..95bbfed --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/BrowserConsoleHandler.php @@ -0,0 +1,308 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LineFormatter; +use Monolog\Utils; +use Monolog\Logger; + +use function count; +use function headers_list; +use function stripos; +use function trigger_error; + +use const E_USER_DEPRECATED; + +/** + * Handler sending logs to browser's javascript console with no browser extension required + * + * @author Olivier Poitrey + * + * @phpstan-import-type FormattedRecord from AbstractProcessingHandler + */ +class BrowserConsoleHandler extends AbstractProcessingHandler +{ + /** @var bool */ + protected static $initialized = false; + /** @var FormattedRecord[] */ + protected static $records = []; + + protected const FORMAT_HTML = 'html'; + protected const FORMAT_JS = 'js'; + protected const FORMAT_UNKNOWN = 'unknown'; + + /** + * {@inheritDoc} + * + * Formatted output may contain some formatting markers to be transferred to `console.log` using the %c format. + * + * Example of formatted string: + * + * You can do [[blue text]]{color: blue} or [[green background]]{background-color: green; color: white} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new LineFormatter('[[%channel%]]{macro: autolabel} [[%level_name%]]{font-weight: bold} %message%'); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + // Accumulate records + static::$records[] = $record; + + // Register shutdown handler if not already done + if (!static::$initialized) { + static::$initialized = true; + $this->registerShutdownFunction(); + } + } + + /** + * Convert records to javascript console commands and send it to the browser. + * This method is automatically called on PHP shutdown if output is HTML or Javascript. + */ + public static function send(): void + { + $format = static::getResponseFormat(); + if ($format === self::FORMAT_UNKNOWN) { + return; + } + + if (count(static::$records)) { + if ($format === self::FORMAT_HTML) { + static::writeOutput(''); + } elseif ($format === self::FORMAT_JS) { + static::writeOutput(static::generateScript()); + } + static::resetStatic(); + } + } + + public function close(): void + { + self::resetStatic(); + } + + public function reset() + { + parent::reset(); + + self::resetStatic(); + } + + /** + * Forget all logged records + */ + public static function resetStatic(): void + { + static::$records = []; + } + + /** + * Wrapper for register_shutdown_function to allow overriding + */ + protected function registerShutdownFunction(): void + { + if (PHP_SAPI !== 'cli') { + register_shutdown_function(['Monolog\Handler\BrowserConsoleHandler', 'send']); + } + } + + /** + * Wrapper for echo to allow overriding + */ + protected static function writeOutput(string $str): void + { + echo $str; + } + + /** + * Checks the format of the response + * + * If Content-Type is set to application/javascript or text/javascript -> js + * If Content-Type is set to text/html, or is unset -> html + * If Content-Type is anything else -> unknown + * + * @return string One of 'js', 'html' or 'unknown' + * @phpstan-return self::FORMAT_* + */ + protected static function getResponseFormat(): string + { + // Check content type + foreach (headers_list() as $header) { + if (stripos($header, 'content-type:') === 0) { + return static::getResponseFormatFromContentType($header); + } + } + + return self::FORMAT_HTML; + } + + /** + * @return string One of 'js', 'html' or 'unknown' + * @phpstan-return self::FORMAT_* + */ + protected static function getResponseFormatFromContentType(string $contentType): string + { + // This handler only works with HTML and javascript outputs + // text/javascript is obsolete in favour of application/javascript, but still used + if (stripos($contentType, 'application/javascript') !== false || stripos($contentType, 'text/javascript') !== false) { + return self::FORMAT_JS; + } + + if (stripos($contentType, 'text/html') !== false) { + return self::FORMAT_HTML; + } + + return self::FORMAT_UNKNOWN; + } + + private static function generateScript(): string + { + $script = []; + foreach (static::$records as $record) { + $context = static::dump('Context', $record['context']); + $extra = static::dump('Extra', $record['extra']); + + if (empty($context) && empty($extra)) { + $script[] = static::call_array(static::getConsoleMethodForLevel($record['level']), static::handleStyles($record['formatted'])); + } else { + $script = array_merge( + $script, + [static::call_array('groupCollapsed', static::handleStyles($record['formatted']))], + $context, + $extra, + [static::call('groupEnd')] + ); + } + } + + return "(function (c) {if (c && c.groupCollapsed) {\n" . implode("\n", $script) . "\n}})(console);"; + } + + private static function getConsoleMethodForLevel(int $level): string + { + return [ + Logger::DEBUG => 'debug', + Logger::INFO => 'info', + Logger::NOTICE => 'info', + Logger::WARNING => 'warn', + Logger::ERROR => 'error', + Logger::CRITICAL => 'error', + Logger::ALERT => 'error', + Logger::EMERGENCY => 'error', + ][$level] ?? 'log'; + } + + /** + * @return string[] + */ + private static function handleStyles(string $formatted): array + { + $args = []; + $format = '%c' . $formatted; + preg_match_all('/\[\[(.*?)\]\]\{([^}]*)\}/s', $format, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); + + foreach (array_reverse($matches) as $match) { + $args[] = '"font-weight: normal"'; + $args[] = static::quote(static::handleCustomStyles($match[2][0], $match[1][0])); + + $pos = $match[0][1]; + $format = Utils::substr($format, 0, $pos) . '%c' . $match[1][0] . '%c' . Utils::substr($format, $pos + strlen($match[0][0])); + } + + $args[] = static::quote('font-weight: normal'); + $args[] = static::quote($format); + + return array_reverse($args); + } + + private static function handleCustomStyles(string $style, string $string): string + { + static $colors = ['blue', 'green', 'red', 'magenta', 'orange', 'black', 'grey']; + static $labels = []; + + $style = preg_replace_callback('/macro\s*:(.*?)(?:;|$)/', function (array $m) use ($string, &$colors, &$labels) { + if (trim($m[1]) === 'autolabel') { + // Format the string as a label with consistent auto assigned background color + if (!isset($labels[$string])) { + $labels[$string] = $colors[count($labels) % count($colors)]; + } + $color = $labels[$string]; + + return "background-color: $color; color: white; border-radius: 3px; padding: 0 2px 0 2px"; + } + + return $m[1]; + }, $style); + + if (null === $style) { + $pcreErrorCode = preg_last_error(); + throw new \RuntimeException('Failed to run preg_replace_callback: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode)); + } + + return $style; + } + + /** + * @param mixed[] $dict + * @return mixed[] + */ + private static function dump(string $title, array $dict): array + { + $script = []; + $dict = array_filter($dict); + if (empty($dict)) { + return $script; + } + $script[] = static::call('log', static::quote('%c%s'), static::quote('font-weight: bold'), static::quote($title)); + foreach ($dict as $key => $value) { + $value = json_encode($value); + if (empty($value)) { + $value = static::quote(''); + } + $script[] = static::call('log', static::quote('%s: %o'), static::quote((string) $key), $value); + } + + return $script; + } + + private static function quote(string $arg): string + { + return '"' . addcslashes($arg, "\"\n\\") . '"'; + } + + /** + * @param mixed $args + */ + private static function call(...$args): string + { + $method = array_shift($args); + if (!is_string($method)) { + throw new \UnexpectedValueException('Expected the first arg to be a string, got: '.var_export($method, true)); + } + + return static::call_array($method, $args); + } + + /** + * @param mixed[] $args + */ + private static function call_array(string $method, array $args): string + { + return 'c.' . $method . '(' . implode(', ', $args) . ');'; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/BufferHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/BufferHandler.php new file mode 100644 index 0000000..fcce5d6 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/BufferHandler.php @@ -0,0 +1,167 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\ResettableInterface; +use Monolog\Formatter\FormatterInterface; + +/** + * Buffers all records until closing the handler and then pass them as batch. + * + * This is useful for a MailHandler to send only one mail per request instead of + * sending one per log message. + * + * @author Christophe Coevoet + * + * @phpstan-import-type Record from \Monolog\Logger + */ +class BufferHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface +{ + use ProcessableHandlerTrait; + + /** @var HandlerInterface */ + protected $handler; + /** @var int */ + protected $bufferSize = 0; + /** @var int */ + protected $bufferLimit; + /** @var bool */ + protected $flushOnOverflow; + /** @var Record[] */ + protected $buffer = []; + /** @var bool */ + protected $initialized = false; + + /** + * @param HandlerInterface $handler Handler. + * @param int $bufferLimit How many entries should be buffered at most, beyond that the oldest items are removed from the buffer. + * @param bool $flushOnOverflow If true, the buffer is flushed when the max size has been reached, by default oldest entries are discarded + */ + public function __construct(HandlerInterface $handler, int $bufferLimit = 0, $level = Logger::DEBUG, bool $bubble = true, bool $flushOnOverflow = false) + { + parent::__construct($level, $bubble); + $this->handler = $handler; + $this->bufferLimit = $bufferLimit; + $this->flushOnOverflow = $flushOnOverflow; + } + + /** + * {@inheritDoc} + */ + public function handle(array $record): bool + { + if ($record['level'] < $this->level) { + return false; + } + + if (!$this->initialized) { + // __destructor() doesn't get called on Fatal errors + register_shutdown_function([$this, 'close']); + $this->initialized = true; + } + + if ($this->bufferLimit > 0 && $this->bufferSize === $this->bufferLimit) { + if ($this->flushOnOverflow) { + $this->flush(); + } else { + array_shift($this->buffer); + $this->bufferSize--; + } + } + + if ($this->processors) { + /** @var Record $record */ + $record = $this->processRecord($record); + } + + $this->buffer[] = $record; + $this->bufferSize++; + + return false === $this->bubble; + } + + public function flush(): void + { + if ($this->bufferSize === 0) { + return; + } + + $this->handler->handleBatch($this->buffer); + $this->clear(); + } + + public function __destruct() + { + // suppress the parent behavior since we already have register_shutdown_function() + // to call close(), and the reference contained there will prevent this from being + // GC'd until the end of the request + } + + /** + * {@inheritDoc} + */ + public function close(): void + { + $this->flush(); + + $this->handler->close(); + } + + /** + * Clears the buffer without flushing any messages down to the wrapped handler. + */ + public function clear(): void + { + $this->bufferSize = 0; + $this->buffer = []; + } + + public function reset() + { + $this->flush(); + + parent::reset(); + + $this->resetProcessors(); + + if ($this->handler instanceof ResettableInterface) { + $this->handler->reset(); + } + } + + /** + * {@inheritDoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + if ($this->handler instanceof FormattableHandlerInterface) { + $this->handler->setFormatter($formatter); + + return $this; + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.'); + } + + /** + * {@inheritDoc} + */ + public function getFormatter(): FormatterInterface + { + if ($this->handler instanceof FormattableHandlerInterface) { + return $this->handler->getFormatter(); + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.'); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/ChromePHPHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/ChromePHPHandler.php new file mode 100644 index 0000000..234ecf6 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/ChromePHPHandler.php @@ -0,0 +1,196 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\ChromePHPFormatter; +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; +use Monolog\Utils; + +/** + * Handler sending logs to the ChromePHP extension (http://www.chromephp.com/) + * + * This also works out of the box with Firefox 43+ + * + * @author Christophe Coevoet + * + * @phpstan-import-type Record from \Monolog\Logger + */ +class ChromePHPHandler extends AbstractProcessingHandler +{ + use WebRequestRecognizerTrait; + + /** + * Version of the extension + */ + protected const VERSION = '4.0'; + + /** + * Header name + */ + protected const HEADER_NAME = 'X-ChromeLogger-Data'; + + /** + * Regular expression to detect supported browsers (matches any Chrome, or Firefox 43+) + */ + protected const USER_AGENT_REGEX = '{\b(?:Chrome/\d+(?:\.\d+)*|HeadlessChrome|Firefox/(?:4[3-9]|[5-9]\d|\d{3,})(?:\.\d)*)\b}'; + + /** @var bool */ + protected static $initialized = false; + + /** + * Tracks whether we sent too much data + * + * Chrome limits the headers to 4KB, so when we sent 3KB we stop sending + * + * @var bool + */ + protected static $overflowed = false; + + /** @var mixed[] */ + protected static $json = [ + 'version' => self::VERSION, + 'columns' => ['label', 'log', 'backtrace', 'type'], + 'rows' => [], + ]; + + /** @var bool */ + protected static $sendHeaders = true; + + public function __construct($level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + if (!function_exists('json_encode')) { + throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s ChromePHPHandler'); + } + } + + /** + * {@inheritDoc} + */ + public function handleBatch(array $records): void + { + if (!$this->isWebRequest()) { + return; + } + + $messages = []; + + foreach ($records as $record) { + if ($record['level'] < $this->level) { + continue; + } + /** @var Record $message */ + $message = $this->processRecord($record); + $messages[] = $message; + } + + if (!empty($messages)) { + $messages = $this->getFormatter()->formatBatch($messages); + self::$json['rows'] = array_merge(self::$json['rows'], $messages); + $this->send(); + } + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new ChromePHPFormatter(); + } + + /** + * Creates & sends header for a record + * + * @see sendHeader() + * @see send() + */ + protected function write(array $record): void + { + if (!$this->isWebRequest()) { + return; + } + + self::$json['rows'][] = $record['formatted']; + + $this->send(); + } + + /** + * Sends the log header + * + * @see sendHeader() + */ + protected function send(): void + { + if (self::$overflowed || !self::$sendHeaders) { + return; + } + + if (!self::$initialized) { + self::$initialized = true; + + self::$sendHeaders = $this->headersAccepted(); + if (!self::$sendHeaders) { + return; + } + + self::$json['request_uri'] = $_SERVER['REQUEST_URI'] ?? ''; + } + + $json = Utils::jsonEncode(self::$json, Utils::DEFAULT_JSON_FLAGS & ~JSON_UNESCAPED_UNICODE, true); + $data = base64_encode($json); + if (strlen($data) > 3 * 1024) { + self::$overflowed = true; + + $record = [ + 'message' => 'Incomplete logs, chrome header size limit reached', + 'context' => [], + 'level' => Logger::WARNING, + 'level_name' => Logger::getLevelName(Logger::WARNING), + 'channel' => 'monolog', + 'datetime' => new \DateTimeImmutable(), + 'extra' => [], + ]; + self::$json['rows'][count(self::$json['rows']) - 1] = $this->getFormatter()->format($record); + $json = Utils::jsonEncode(self::$json, Utils::DEFAULT_JSON_FLAGS & ~JSON_UNESCAPED_UNICODE, true); + $data = base64_encode($json); + } + + if (trim($data) !== '') { + $this->sendHeader(static::HEADER_NAME, $data); + } + } + + /** + * Send header string to the client + */ + protected function sendHeader(string $header, string $content): void + { + if (!headers_sent() && self::$sendHeaders) { + header(sprintf('%s: %s', $header, $content)); + } + } + + /** + * Verifies if the headers are accepted by the current user agent + */ + protected function headersAccepted(): bool + { + if (empty($_SERVER['HTTP_USER_AGENT'])) { + return false; + } + + return preg_match(static::USER_AGENT_REGEX, $_SERVER['HTTP_USER_AGENT']) === 1; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/CouchDBHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/CouchDBHandler.php new file mode 100644 index 0000000..5265761 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/CouchDBHandler.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\JsonFormatter; +use Monolog\Logger; + +/** + * CouchDB handler + * + * @author Markus Bachmann + */ +class CouchDBHandler extends AbstractProcessingHandler +{ + /** @var mixed[] */ + private $options; + + /** + * @param mixed[] $options + */ + public function __construct(array $options = [], $level = Logger::DEBUG, bool $bubble = true) + { + $this->options = array_merge([ + 'host' => 'localhost', + 'port' => 5984, + 'dbname' => 'logger', + 'username' => null, + 'password' => null, + ], $options); + + parent::__construct($level, $bubble); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $basicAuth = null; + if ($this->options['username']) { + $basicAuth = sprintf('%s:%s@', $this->options['username'], $this->options['password']); + } + + $url = 'http://'.$basicAuth.$this->options['host'].':'.$this->options['port'].'/'.$this->options['dbname']; + $context = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'content' => $record['formatted'], + 'ignore_errors' => true, + 'max_redirects' => 0, + 'header' => 'Content-type: application/json', + ], + ]); + + if (false === @file_get_contents($url, false, $context)) { + throw new \RuntimeException(sprintf('Could not connect to %s', $url)); + } + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/CubeHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/CubeHandler.php new file mode 100644 index 0000000..3535a4f --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/CubeHandler.php @@ -0,0 +1,167 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Utils; + +/** + * Logs to Cube. + * + * @link https://github.com/square/cube/wiki + * @author Wan Chen + * @deprecated Since 2.8.0 and 3.2.0, Cube appears abandoned and thus we will drop this handler in Monolog 4 + */ +class CubeHandler extends AbstractProcessingHandler +{ + /** @var resource|\Socket|null */ + private $udpConnection = null; + /** @var resource|\CurlHandle|null */ + private $httpConnection = null; + /** @var string */ + private $scheme; + /** @var string */ + private $host; + /** @var int */ + private $port; + /** @var string[] */ + private $acceptedSchemes = ['http', 'udp']; + + /** + * Create a Cube handler + * + * @throws \UnexpectedValueException when given url is not a valid url. + * A valid url must consist of three parts : protocol://host:port + * Only valid protocols used by Cube are http and udp + */ + public function __construct(string $url, $level = Logger::DEBUG, bool $bubble = true) + { + $urlInfo = parse_url($url); + + if ($urlInfo === false || !isset($urlInfo['scheme'], $urlInfo['host'], $urlInfo['port'])) { + throw new \UnexpectedValueException('URL "'.$url.'" is not valid'); + } + + if (!in_array($urlInfo['scheme'], $this->acceptedSchemes)) { + throw new \UnexpectedValueException( + 'Invalid protocol (' . $urlInfo['scheme'] . ').' + . ' Valid options are ' . implode(', ', $this->acceptedSchemes) + ); + } + + $this->scheme = $urlInfo['scheme']; + $this->host = $urlInfo['host']; + $this->port = (int) $urlInfo['port']; + + parent::__construct($level, $bubble); + } + + /** + * Establish a connection to an UDP socket + * + * @throws \LogicException when unable to connect to the socket + * @throws MissingExtensionException when there is no socket extension + */ + protected function connectUdp(): void + { + if (!extension_loaded('sockets')) { + throw new MissingExtensionException('The sockets extension is required to use udp URLs with the CubeHandler'); + } + + $udpConnection = socket_create(AF_INET, SOCK_DGRAM, 0); + if (false === $udpConnection) { + throw new \LogicException('Unable to create a socket'); + } + + $this->udpConnection = $udpConnection; + if (!socket_connect($this->udpConnection, $this->host, $this->port)) { + throw new \LogicException('Unable to connect to the socket at ' . $this->host . ':' . $this->port); + } + } + + /** + * Establish a connection to an http server + * + * @throws \LogicException when unable to connect to the socket + * @throws MissingExtensionException when no curl extension + */ + protected function connectHttp(): void + { + if (!extension_loaded('curl')) { + throw new MissingExtensionException('The curl extension is required to use http URLs with the CubeHandler'); + } + + $httpConnection = curl_init('http://'.$this->host.':'.$this->port.'/1.0/event/put'); + if (false === $httpConnection) { + throw new \LogicException('Unable to connect to ' . $this->host . ':' . $this->port); + } + + $this->httpConnection = $httpConnection; + curl_setopt($this->httpConnection, CURLOPT_CUSTOMREQUEST, "POST"); + curl_setopt($this->httpConnection, CURLOPT_RETURNTRANSFER, true); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $date = $record['datetime']; + + $data = ['time' => $date->format('Y-m-d\TH:i:s.uO')]; + unset($record['datetime']); + + if (isset($record['context']['type'])) { + $data['type'] = $record['context']['type']; + unset($record['context']['type']); + } else { + $data['type'] = $record['channel']; + } + + $data['data'] = $record['context']; + $data['data']['level'] = $record['level']; + + if ($this->scheme === 'http') { + $this->writeHttp(Utils::jsonEncode($data)); + } else { + $this->writeUdp(Utils::jsonEncode($data)); + } + } + + private function writeUdp(string $data): void + { + if (!$this->udpConnection) { + $this->connectUdp(); + } + + socket_send($this->udpConnection, $data, strlen($data), 0); + } + + private function writeHttp(string $data): void + { + if (!$this->httpConnection) { + $this->connectHttp(); + } + + if (null === $this->httpConnection) { + throw new \LogicException('No connection could be established'); + } + + curl_setopt($this->httpConnection, CURLOPT_POSTFIELDS, '['.$data.']'); + curl_setopt($this->httpConnection, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Content-Length: ' . strlen('['.$data.']'), + ]); + + Curl\Util::execute($this->httpConnection, 5, false); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/Curl/Util.php b/vendor/monolog/monolog/src/Monolog/Handler/Curl/Util.php new file mode 100644 index 0000000..7213e8e --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/Curl/Util.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler\Curl; + +use CurlHandle; + +/** + * This class is marked as internal and it is not under the BC promise of the package. + * + * @internal + */ +final class Util +{ + /** @var array */ + private static $retriableErrorCodes = [ + CURLE_COULDNT_RESOLVE_HOST, + CURLE_COULDNT_CONNECT, + CURLE_HTTP_NOT_FOUND, + CURLE_READ_ERROR, + CURLE_OPERATION_TIMEOUTED, + CURLE_HTTP_POST_ERROR, + CURLE_SSL_CONNECT_ERROR, + ]; + + /** + * Executes a CURL request with optional retries and exception on failure + * + * @param resource|CurlHandle $ch curl handler + * @param int $retries + * @param bool $closeAfterDone + * @return bool|string @see curl_exec + */ + public static function execute($ch, int $retries = 5, bool $closeAfterDone = true) + { + while ($retries--) { + $curlResponse = curl_exec($ch); + if ($curlResponse === false) { + $curlErrno = curl_errno($ch); + + if (false === in_array($curlErrno, self::$retriableErrorCodes, true) || !$retries) { + $curlError = curl_error($ch); + + if ($closeAfterDone) { + curl_close($ch); + } + + throw new \RuntimeException(sprintf('Curl error (code %d): %s', $curlErrno, $curlError)); + } + + continue; + } + + if ($closeAfterDone) { + curl_close($ch); + } + + return $curlResponse; + } + + return false; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php new file mode 100644 index 0000000..9b85ae7 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php @@ -0,0 +1,186 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Psr\Log\LogLevel; + +/** + * Simple handler wrapper that deduplicates log records across multiple requests + * + * It also includes the BufferHandler functionality and will buffer + * all messages until the end of the request or flush() is called. + * + * This works by storing all log records' messages above $deduplicationLevel + * to the file specified by $deduplicationStore. When further logs come in at the end of the + * request (or when flush() is called), all those above $deduplicationLevel are checked + * against the existing stored logs. If they match and the timestamps in the stored log is + * not older than $time seconds, the new log record is discarded. If no log record is new, the + * whole data set is discarded. + * + * This is mainly useful in combination with Mail handlers or things like Slack or HipChat handlers + * that send messages to people, to avoid spamming with the same message over and over in case of + * a major component failure like a database server being down which makes all requests fail in the + * same way. + * + * @author Jordi Boggiano + * + * @phpstan-import-type Record from \Monolog\Logger + * @phpstan-import-type LevelName from \Monolog\Logger + * @phpstan-import-type Level from \Monolog\Logger + */ +class DeduplicationHandler extends BufferHandler +{ + /** + * @var string + */ + protected $deduplicationStore; + + /** + * @var Level + */ + protected $deduplicationLevel; + + /** + * @var int + */ + protected $time; + + /** + * @var bool + */ + private $gc = false; + + /** + * @param HandlerInterface $handler Handler. + * @param string $deduplicationStore The file/path where the deduplication log should be kept + * @param string|int $deduplicationLevel The minimum logging level for log records to be looked at for deduplication purposes + * @param int $time The period (in seconds) during which duplicate entries should be suppressed after a given log is sent through + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * + * @phpstan-param Level|LevelName|LogLevel::* $deduplicationLevel + */ + public function __construct(HandlerInterface $handler, ?string $deduplicationStore = null, $deduplicationLevel = Logger::ERROR, int $time = 60, bool $bubble = true) + { + parent::__construct($handler, 0, Logger::DEBUG, $bubble, false); + + $this->deduplicationStore = $deduplicationStore === null ? sys_get_temp_dir() . '/monolog-dedup-' . substr(md5(__FILE__), 0, 20) .'.log' : $deduplicationStore; + $this->deduplicationLevel = Logger::toMonologLevel($deduplicationLevel); + $this->time = $time; + } + + public function flush(): void + { + if ($this->bufferSize === 0) { + return; + } + + $passthru = null; + + foreach ($this->buffer as $record) { + if ($record['level'] >= $this->deduplicationLevel) { + $passthru = $passthru || !$this->isDuplicate($record); + if ($passthru) { + $this->appendRecord($record); + } + } + } + + // default of null is valid as well as if no record matches duplicationLevel we just pass through + if ($passthru === true || $passthru === null) { + $this->handler->handleBatch($this->buffer); + } + + $this->clear(); + + if ($this->gc) { + $this->collectLogs(); + } + } + + /** + * @phpstan-param Record $record + */ + private function isDuplicate(array $record): bool + { + if (!file_exists($this->deduplicationStore)) { + return false; + } + + $store = file($this->deduplicationStore, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if (!is_array($store)) { + return false; + } + + $yesterday = time() - 86400; + $timestampValidity = $record['datetime']->getTimestamp() - $this->time; + $expectedMessage = preg_replace('{[\r\n].*}', '', $record['message']); + + for ($i = count($store) - 1; $i >= 0; $i--) { + list($timestamp, $level, $message) = explode(':', $store[$i], 3); + + if ($level === $record['level_name'] && $message === $expectedMessage && $timestamp > $timestampValidity) { + return true; + } + + if ($timestamp < $yesterday) { + $this->gc = true; + } + } + + return false; + } + + private function collectLogs(): void + { + if (!file_exists($this->deduplicationStore)) { + return; + } + + $handle = fopen($this->deduplicationStore, 'rw+'); + + if (!$handle) { + throw new \RuntimeException('Failed to open file for reading and writing: ' . $this->deduplicationStore); + } + + flock($handle, LOCK_EX); + $validLogs = []; + + $timestampValidity = time() - $this->time; + + while (!feof($handle)) { + $log = fgets($handle); + if ($log && substr($log, 0, 10) >= $timestampValidity) { + $validLogs[] = $log; + } + } + + ftruncate($handle, 0); + rewind($handle); + foreach ($validLogs as $log) { + fwrite($handle, $log); + } + + flock($handle, LOCK_UN); + fclose($handle); + + $this->gc = false; + } + + /** + * @phpstan-param Record $record + */ + private function appendRecord(array $record): void + { + file_put_contents($this->deduplicationStore, $record['datetime']->getTimestamp() . ':' . $record['level_name'] . ':' . preg_replace('{[\r\n].*}', '', $record['message']) . "\n", FILE_APPEND); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/DoctrineCouchDBHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/DoctrineCouchDBHandler.php new file mode 100644 index 0000000..ebd52c3 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/DoctrineCouchDBHandler.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Formatter\NormalizerFormatter; +use Monolog\Formatter\FormatterInterface; +use Doctrine\CouchDB\CouchDBClient; + +/** + * CouchDB handler for Doctrine CouchDB ODM + * + * @author Markus Bachmann + */ +class DoctrineCouchDBHandler extends AbstractProcessingHandler +{ + /** @var CouchDBClient */ + private $client; + + public function __construct(CouchDBClient $client, $level = Logger::DEBUG, bool $bubble = true) + { + $this->client = $client; + parent::__construct($level, $bubble); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $this->client->postDocument($record['formatted']); + } + + protected function getDefaultFormatter(): FormatterInterface + { + return new NormalizerFormatter; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/DynamoDbHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/DynamoDbHandler.php new file mode 100644 index 0000000..21840bf --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/DynamoDbHandler.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Aws\Sdk; +use Aws\DynamoDb\DynamoDbClient; +use Monolog\Formatter\FormatterInterface; +use Aws\DynamoDb\Marshaler; +use Monolog\Formatter\ScalarFormatter; +use Monolog\Logger; + +/** + * Amazon DynamoDB handler (http://aws.amazon.com/dynamodb/) + * + * @link https://github.com/aws/aws-sdk-php/ + * @author Andrew Lawson + */ +class DynamoDbHandler extends AbstractProcessingHandler +{ + public const DATE_FORMAT = 'Y-m-d\TH:i:s.uO'; + + /** + * @var DynamoDbClient + */ + protected $client; + + /** + * @var string + */ + protected $table; + + /** + * @var int + */ + protected $version; + + /** + * @var Marshaler + */ + protected $marshaler; + + public function __construct(DynamoDbClient $client, string $table, $level = Logger::DEBUG, bool $bubble = true) + { + /** @phpstan-ignore-next-line */ + if (defined('Aws\Sdk::VERSION') && version_compare(Sdk::VERSION, '3.0', '>=')) { + $this->version = 3; + $this->marshaler = new Marshaler; + } else { + $this->version = 2; + } + + $this->client = $client; + $this->table = $table; + + parent::__construct($level, $bubble); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $filtered = $this->filterEmptyFields($record['formatted']); + if ($this->version === 3) { + $formatted = $this->marshaler->marshalItem($filtered); + } else { + /** @phpstan-ignore-next-line */ + $formatted = $this->client->formatAttributes($filtered); + } + + $this->client->putItem([ + 'TableName' => $this->table, + 'Item' => $formatted, + ]); + } + + /** + * @param mixed[] $record + * @return mixed[] + */ + protected function filterEmptyFields(array $record): array + { + return array_filter($record, function ($value) { + return !empty($value) || false === $value || 0 === $value; + }); + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new ScalarFormatter(self::DATE_FORMAT); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/ElasticaHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/ElasticaHandler.php new file mode 100644 index 0000000..fc92ca4 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/ElasticaHandler.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Elastica\Document; +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\ElasticaFormatter; +use Monolog\Logger; +use Elastica\Client; +use Elastica\Exception\ExceptionInterface; + +/** + * Elastic Search handler + * + * Usage example: + * + * $client = new \Elastica\Client(); + * $options = array( + * 'index' => 'elastic_index_name', + * 'type' => 'elastic_doc_type', Types have been removed in Elastica 7 + * ); + * $handler = new ElasticaHandler($client, $options); + * $log = new Logger('application'); + * $log->pushHandler($handler); + * + * @author Jelle Vink + */ +class ElasticaHandler extends AbstractProcessingHandler +{ + /** + * @var Client + */ + protected $client; + + /** + * @var mixed[] Handler config options + */ + protected $options = []; + + /** + * @param Client $client Elastica Client object + * @param mixed[] $options Handler configuration + */ + public function __construct(Client $client, array $options = [], $level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + $this->client = $client; + $this->options = array_merge( + [ + 'index' => 'monolog', // Elastic index name + 'type' => 'record', // Elastic document type + 'ignore_error' => false, // Suppress Elastica exceptions + ], + $options + ); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $this->bulkSend([$record['formatted']]); + } + + /** + * {@inheritDoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + if ($formatter instanceof ElasticaFormatter) { + return parent::setFormatter($formatter); + } + + throw new \InvalidArgumentException('ElasticaHandler is only compatible with ElasticaFormatter'); + } + + /** + * @return mixed[] + */ + public function getOptions(): array + { + return $this->options; + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new ElasticaFormatter($this->options['index'], $this->options['type']); + } + + /** + * {@inheritDoc} + */ + public function handleBatch(array $records): void + { + $documents = $this->getFormatter()->formatBatch($records); + $this->bulkSend($documents); + } + + /** + * Use Elasticsearch bulk API to send list of documents + * + * @param Document[] $documents + * + * @throws \RuntimeException + */ + protected function bulkSend(array $documents): void + { + try { + $this->client->addDocuments($documents); + } catch (ExceptionInterface $e) { + if (!$this->options['ignore_error']) { + throw new \RuntimeException("Error sending messages to Elasticsearch", 0, $e); + } + } + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/ElasticsearchHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/ElasticsearchHandler.php new file mode 100644 index 0000000..e88375c --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/ElasticsearchHandler.php @@ -0,0 +1,218 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Elastic\Elasticsearch\Response\Elasticsearch; +use Throwable; +use RuntimeException; +use Monolog\Logger; +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\ElasticsearchFormatter; +use InvalidArgumentException; +use Elasticsearch\Common\Exceptions\RuntimeException as ElasticsearchRuntimeException; +use Elasticsearch\Client; +use Elastic\Elasticsearch\Exception\InvalidArgumentException as ElasticInvalidArgumentException; +use Elastic\Elasticsearch\Client as Client8; + +/** + * Elasticsearch handler + * + * @link https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/index.html + * + * Simple usage example: + * + * $client = \Elasticsearch\ClientBuilder::create() + * ->setHosts($hosts) + * ->build(); + * + * $options = array( + * 'index' => 'elastic_index_name', + * 'type' => 'elastic_doc_type', + * ); + * $handler = new ElasticsearchHandler($client, $options); + * $log = new Logger('application'); + * $log->pushHandler($handler); + * + * @author Avtandil Kikabidze + */ +class ElasticsearchHandler extends AbstractProcessingHandler +{ + /** + * @var Client|Client8 + */ + protected $client; + + /** + * @var mixed[] Handler config options + */ + protected $options = []; + + /** + * @var bool + */ + private $needsType; + + /** + * @param Client|Client8 $client Elasticsearch Client object + * @param mixed[] $options Handler configuration + */ + public function __construct($client, array $options = [], $level = Logger::DEBUG, bool $bubble = true) + { + if (!$client instanceof Client && !$client instanceof Client8) { + throw new \TypeError('Elasticsearch\Client or Elastic\Elasticsearch\Client instance required'); + } + + parent::__construct($level, $bubble); + $this->client = $client; + $this->options = array_merge( + [ + 'index' => 'monolog', // Elastic index name + 'type' => '_doc', // Elastic document type + 'ignore_error' => false, // Suppress Elasticsearch exceptions + ], + $options + ); + + if ($client instanceof Client8 || $client::VERSION[0] === '7') { + $this->needsType = false; + // force the type to _doc for ES8/ES7 + $this->options['type'] = '_doc'; + } else { + $this->needsType = true; + } + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $this->bulkSend([$record['formatted']]); + } + + /** + * {@inheritDoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + if ($formatter instanceof ElasticsearchFormatter) { + return parent::setFormatter($formatter); + } + + throw new InvalidArgumentException('ElasticsearchHandler is only compatible with ElasticsearchFormatter'); + } + + /** + * Getter options + * + * @return mixed[] + */ + public function getOptions(): array + { + return $this->options; + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new ElasticsearchFormatter($this->options['index'], $this->options['type']); + } + + /** + * {@inheritDoc} + */ + public function handleBatch(array $records): void + { + $documents = $this->getFormatter()->formatBatch($records); + $this->bulkSend($documents); + } + + /** + * Use Elasticsearch bulk API to send list of documents + * + * @param array[] $records Records + _index/_type keys + * @throws \RuntimeException + */ + protected function bulkSend(array $records): void + { + try { + $params = [ + 'body' => [], + ]; + + foreach ($records as $record) { + $params['body'][] = [ + 'index' => $this->needsType ? [ + '_index' => $record['_index'], + '_type' => $record['_type'], + ] : [ + '_index' => $record['_index'], + ], + ]; + unset($record['_index'], $record['_type']); + + $params['body'][] = $record; + } + + /** @var Elasticsearch */ + $responses = $this->client->bulk($params); + + if ($responses['errors'] === true) { + throw $this->createExceptionFromResponses($responses); + } + } catch (Throwable $e) { + if (! $this->options['ignore_error']) { + throw new RuntimeException('Error sending messages to Elasticsearch', 0, $e); + } + } + } + + /** + * Creates elasticsearch exception from responses array + * + * Only the first error is converted into an exception. + * + * @param mixed[]|Elasticsearch $responses returned by $this->client->bulk() + */ + protected function createExceptionFromResponses($responses): Throwable + { + foreach ($responses['items'] ?? [] as $item) { + if (isset($item['index']['error'])) { + return $this->createExceptionFromError($item['index']['error']); + } + } + + if (class_exists(ElasticInvalidArgumentException::class)) { + return new ElasticInvalidArgumentException('Elasticsearch failed to index one or more records.'); + } + + return new ElasticsearchRuntimeException('Elasticsearch failed to index one or more records.'); + } + + /** + * Creates elasticsearch exception from error array + * + * @param mixed[] $error + */ + protected function createExceptionFromError(array $error): Throwable + { + $previous = isset($error['caused_by']) ? $this->createExceptionFromError($error['caused_by']) : null; + + if (class_exists(ElasticInvalidArgumentException::class)) { + return new ElasticInvalidArgumentException($error['type'] . ': ' . $error['reason'], 0, $previous); + } + + return new ElasticsearchRuntimeException($error['type'] . ': ' . $error['reason'], 0, $previous); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/ErrorLogHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/ErrorLogHandler.php new file mode 100644 index 0000000..f2e2203 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/ErrorLogHandler.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\LineFormatter; +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; +use Monolog\Utils; + +/** + * Stores to PHP error_log() handler. + * + * @author Elan Ruusamäe + */ +class ErrorLogHandler extends AbstractProcessingHandler +{ + public const OPERATING_SYSTEM = 0; + public const SAPI = 4; + + /** @var int */ + protected $messageType; + /** @var bool */ + protected $expandNewlines; + + /** + * @param int $messageType Says where the error should go. + * @param bool $expandNewlines If set to true, newlines in the message will be expanded to be take multiple log entries + */ + public function __construct(int $messageType = self::OPERATING_SYSTEM, $level = Logger::DEBUG, bool $bubble = true, bool $expandNewlines = false) + { + parent::__construct($level, $bubble); + + if (false === in_array($messageType, self::getAvailableTypes(), true)) { + $message = sprintf('The given message type "%s" is not supported', print_r($messageType, true)); + + throw new \InvalidArgumentException($message); + } + + $this->messageType = $messageType; + $this->expandNewlines = $expandNewlines; + } + + /** + * @return int[] With all available types + */ + public static function getAvailableTypes(): array + { + return [ + self::OPERATING_SYSTEM, + self::SAPI, + ]; + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new LineFormatter('[%datetime%] %channel%.%level_name%: %message% %context% %extra%'); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + if (!$this->expandNewlines) { + error_log((string) $record['formatted'], $this->messageType); + + return; + } + + $lines = preg_split('{[\r\n]+}', (string) $record['formatted']); + if ($lines === false) { + $pcreErrorCode = preg_last_error(); + throw new \RuntimeException('Failed to preg_split formatted string: ' . $pcreErrorCode . ' / '. Utils::pcreLastErrorMessage($pcreErrorCode)); + } + foreach ($lines as $line) { + error_log($line, $this->messageType); + } + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/FallbackGroupHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/FallbackGroupHandler.php new file mode 100644 index 0000000..d4e234c --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/FallbackGroupHandler.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Throwable; + +/** + * Forwards records to at most one handler + * + * If a handler fails, the exception is suppressed and the record is forwarded to the next handler. + * + * As soon as one handler handles a record successfully, the handling stops there. + * + * @phpstan-import-type Record from \Monolog\Logger + */ +class FallbackGroupHandler extends GroupHandler +{ + /** + * {@inheritDoc} + */ + public function handle(array $record): bool + { + if ($this->processors) { + /** @var Record $record */ + $record = $this->processRecord($record); + } + foreach ($this->handlers as $handler) { + try { + $handler->handle($record); + break; + } catch (Throwable $e) { + // What throwable? + } + } + + return false === $this->bubble; + } + + /** + * {@inheritDoc} + */ + public function handleBatch(array $records): void + { + if ($this->processors) { + $processed = []; + foreach ($records as $record) { + $processed[] = $this->processRecord($record); + } + /** @var Record[] $records */ + $records = $processed; + } + + foreach ($this->handlers as $handler) { + try { + $handler->handleBatch($records); + break; + } catch (Throwable $e) { + // What throwable? + } + } + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/FilterHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/FilterHandler.php new file mode 100644 index 0000000..5e43e1d --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/FilterHandler.php @@ -0,0 +1,212 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\ResettableInterface; +use Monolog\Formatter\FormatterInterface; +use Psr\Log\LogLevel; + +/** + * Simple handler wrapper that filters records based on a list of levels + * + * It can be configured with an exact list of levels to allow, or a min/max level. + * + * @author Hennadiy Verkh + * @author Jordi Boggiano + * + * @phpstan-import-type Record from \Monolog\Logger + * @phpstan-import-type Level from \Monolog\Logger + * @phpstan-import-type LevelName from \Monolog\Logger + */ +class FilterHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface, FormattableHandlerInterface +{ + use ProcessableHandlerTrait; + + /** + * Handler or factory callable($record, $this) + * + * @var callable|HandlerInterface + * @phpstan-var callable(?Record, HandlerInterface): HandlerInterface|HandlerInterface + */ + protected $handler; + + /** + * Minimum level for logs that are passed to handler + * + * @var int[] + * @phpstan-var array + */ + protected $acceptedLevels; + + /** + * Whether the messages that are handled can bubble up the stack or not + * + * @var bool + */ + protected $bubble; + + /** + * @psalm-param HandlerInterface|callable(?Record, HandlerInterface): HandlerInterface $handler + * + * @param callable|HandlerInterface $handler Handler or factory callable($record|null, $filterHandler). + * @param int|array $minLevelOrList A list of levels to accept or a minimum level if maxLevel is provided + * @param int|string $maxLevel Maximum level to accept, only used if $minLevelOrList is not an array + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * + * @phpstan-param Level|LevelName|LogLevel::*|array $minLevelOrList + * @phpstan-param Level|LevelName|LogLevel::* $maxLevel + */ + public function __construct($handler, $minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY, bool $bubble = true) + { + $this->handler = $handler; + $this->bubble = $bubble; + $this->setAcceptedLevels($minLevelOrList, $maxLevel); + + if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) { + throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object"); + } + } + + /** + * @phpstan-return array + */ + public function getAcceptedLevels(): array + { + return array_flip($this->acceptedLevels); + } + + /** + * @param int|string|array $minLevelOrList A list of levels to accept or a minimum level or level name if maxLevel is provided + * @param int|string $maxLevel Maximum level or level name to accept, only used if $minLevelOrList is not an array + * + * @phpstan-param Level|LevelName|LogLevel::*|array $minLevelOrList + * @phpstan-param Level|LevelName|LogLevel::* $maxLevel + */ + public function setAcceptedLevels($minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY): self + { + if (is_array($minLevelOrList)) { + $acceptedLevels = array_map('Monolog\Logger::toMonologLevel', $minLevelOrList); + } else { + $minLevelOrList = Logger::toMonologLevel($minLevelOrList); + $maxLevel = Logger::toMonologLevel($maxLevel); + $acceptedLevels = array_values(array_filter(Logger::getLevels(), function ($level) use ($minLevelOrList, $maxLevel) { + return $level >= $minLevelOrList && $level <= $maxLevel; + })); + } + $this->acceptedLevels = array_flip($acceptedLevels); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function isHandling(array $record): bool + { + return isset($this->acceptedLevels[$record['level']]); + } + + /** + * {@inheritDoc} + */ + public function handle(array $record): bool + { + if (!$this->isHandling($record)) { + return false; + } + + if ($this->processors) { + /** @var Record $record */ + $record = $this->processRecord($record); + } + + $this->getHandler($record)->handle($record); + + return false === $this->bubble; + } + + /** + * {@inheritDoc} + */ + public function handleBatch(array $records): void + { + $filtered = []; + foreach ($records as $record) { + if ($this->isHandling($record)) { + $filtered[] = $record; + } + } + + if (count($filtered) > 0) { + $this->getHandler($filtered[count($filtered) - 1])->handleBatch($filtered); + } + } + + /** + * Return the nested handler + * + * If the handler was provided as a factory callable, this will trigger the handler's instantiation. + * + * @return HandlerInterface + * + * @phpstan-param Record $record + */ + public function getHandler(?array $record = null) + { + if (!$this->handler instanceof HandlerInterface) { + $this->handler = ($this->handler)($record, $this); + if (!$this->handler instanceof HandlerInterface) { + throw new \RuntimeException("The factory callable should return a HandlerInterface"); + } + } + + return $this->handler; + } + + /** + * {@inheritDoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + $handler->setFormatter($formatter); + + return $this; + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); + } + + /** + * {@inheritDoc} + */ + public function getFormatter(): FormatterInterface + { + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + return $handler->getFormatter(); + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); + } + + public function reset() + { + $this->resetProcessors(); + + if ($this->getHandler() instanceof ResettableInterface) { + $this->getHandler()->reset(); + } + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ActivationStrategyInterface.php b/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ActivationStrategyInterface.php new file mode 100644 index 0000000..0aa5607 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ActivationStrategyInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler\FingersCrossed; + +/** + * Interface for activation strategies for the FingersCrossedHandler. + * + * @author Johannes M. Schmitt + * + * @phpstan-import-type Record from \Monolog\Logger + */ +interface ActivationStrategyInterface +{ + /** + * Returns whether the given record activates the handler. + * + * @phpstan-param Record $record + */ + public function isHandlerActivated(array $record): bool; +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ChannelLevelActivationStrategy.php b/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ChannelLevelActivationStrategy.php new file mode 100644 index 0000000..7b9abb5 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ChannelLevelActivationStrategy.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler\FingersCrossed; + +use Monolog\Logger; +use Psr\Log\LogLevel; + +/** + * Channel and Error level based monolog activation strategy. Allows to trigger activation + * based on level per channel. e.g. trigger activation on level 'ERROR' by default, except + * for records of the 'sql' channel; those should trigger activation on level 'WARN'. + * + * Example: + * + * + * $activationStrategy = new ChannelLevelActivationStrategy( + * Logger::CRITICAL, + * array( + * 'request' => Logger::ALERT, + * 'sensitive' => Logger::ERROR, + * ) + * ); + * $handler = new FingersCrossedHandler(new StreamHandler('php://stderr'), $activationStrategy); + * + * + * @author Mike Meessen + * + * @phpstan-import-type Record from \Monolog\Logger + * @phpstan-import-type Level from \Monolog\Logger + * @phpstan-import-type LevelName from \Monolog\Logger + */ +class ChannelLevelActivationStrategy implements ActivationStrategyInterface +{ + /** + * @var Level + */ + private $defaultActionLevel; + + /** + * @var array + */ + private $channelToActionLevel; + + /** + * @param int|string $defaultActionLevel The default action level to be used if the record's category doesn't match any + * @param array $channelToActionLevel An array that maps channel names to action levels. + * + * @phpstan-param array $channelToActionLevel + * @phpstan-param Level|LevelName|LogLevel::* $defaultActionLevel + */ + public function __construct($defaultActionLevel, array $channelToActionLevel = []) + { + $this->defaultActionLevel = Logger::toMonologLevel($defaultActionLevel); + $this->channelToActionLevel = array_map('Monolog\Logger::toMonologLevel', $channelToActionLevel); + } + + /** + * @phpstan-param Record $record + */ + public function isHandlerActivated(array $record): bool + { + if (isset($this->channelToActionLevel[$record['channel']])) { + return $record['level'] >= $this->channelToActionLevel[$record['channel']]; + } + + return $record['level'] >= $this->defaultActionLevel; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ErrorLevelActivationStrategy.php b/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ErrorLevelActivationStrategy.php new file mode 100644 index 0000000..5ec88ea --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ErrorLevelActivationStrategy.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler\FingersCrossed; + +use Monolog\Logger; +use Psr\Log\LogLevel; + +/** + * Error level based activation strategy. + * + * @author Johannes M. Schmitt + * + * @phpstan-import-type Level from \Monolog\Logger + * @phpstan-import-type LevelName from \Monolog\Logger + */ +class ErrorLevelActivationStrategy implements ActivationStrategyInterface +{ + /** + * @var Level + */ + private $actionLevel; + + /** + * @param int|string $actionLevel Level or name or value + * + * @phpstan-param Level|LevelName|LogLevel::* $actionLevel + */ + public function __construct($actionLevel) + { + $this->actionLevel = Logger::toMonologLevel($actionLevel); + } + + public function isHandlerActivated(array $record): bool + { + return $record['level'] >= $this->actionLevel; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossedHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossedHandler.php new file mode 100644 index 0000000..dfcb3af --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossedHandler.php @@ -0,0 +1,252 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; +use Monolog\Handler\FingersCrossed\ActivationStrategyInterface; +use Monolog\Logger; +use Monolog\ResettableInterface; +use Monolog\Formatter\FormatterInterface; +use Psr\Log\LogLevel; + +/** + * Buffers all records until a certain level is reached + * + * The advantage of this approach is that you don't get any clutter in your log files. + * Only requests which actually trigger an error (or whatever your actionLevel is) will be + * in the logs, but they will contain all records, not only those above the level threshold. + * + * You can then have a passthruLevel as well which means that at the end of the request, + * even if it did not get activated, it will still send through log records of e.g. at least a + * warning level. + * + * You can find the various activation strategies in the + * Monolog\Handler\FingersCrossed\ namespace. + * + * @author Jordi Boggiano + * + * @phpstan-import-type Record from \Monolog\Logger + * @phpstan-import-type Level from \Monolog\Logger + * @phpstan-import-type LevelName from \Monolog\Logger + */ +class FingersCrossedHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface, FormattableHandlerInterface +{ + use ProcessableHandlerTrait; + + /** + * @var callable|HandlerInterface + * @phpstan-var callable(?Record, HandlerInterface): HandlerInterface|HandlerInterface + */ + protected $handler; + /** @var ActivationStrategyInterface */ + protected $activationStrategy; + /** @var bool */ + protected $buffering = true; + /** @var int */ + protected $bufferSize; + /** @var Record[] */ + protected $buffer = []; + /** @var bool */ + protected $stopBuffering; + /** + * @var ?int + * @phpstan-var ?Level + */ + protected $passthruLevel; + /** @var bool */ + protected $bubble; + + /** + * @psalm-param HandlerInterface|callable(?Record, HandlerInterface): HandlerInterface $handler + * + * @param callable|HandlerInterface $handler Handler or factory callable($record|null, $fingersCrossedHandler). + * @param int|string|ActivationStrategyInterface $activationStrategy Strategy which determines when this handler takes action, or a level name/value at which the handler is activated + * @param int $bufferSize How many entries should be buffered at most, beyond that the oldest items are removed from the buffer. + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param bool $stopBuffering Whether the handler should stop buffering after being triggered (default true) + * @param int|string $passthruLevel Minimum level to always flush to handler on close, even if strategy not triggered + * + * @phpstan-param Level|LevelName|LogLevel::* $passthruLevel + * @phpstan-param Level|LevelName|LogLevel::*|ActivationStrategyInterface $activationStrategy + */ + public function __construct($handler, $activationStrategy = null, int $bufferSize = 0, bool $bubble = true, bool $stopBuffering = true, $passthruLevel = null) + { + if (null === $activationStrategy) { + $activationStrategy = new ErrorLevelActivationStrategy(Logger::WARNING); + } + + // convert simple int activationStrategy to an object + if (!$activationStrategy instanceof ActivationStrategyInterface) { + $activationStrategy = new ErrorLevelActivationStrategy($activationStrategy); + } + + $this->handler = $handler; + $this->activationStrategy = $activationStrategy; + $this->bufferSize = $bufferSize; + $this->bubble = $bubble; + $this->stopBuffering = $stopBuffering; + + if ($passthruLevel !== null) { + $this->passthruLevel = Logger::toMonologLevel($passthruLevel); + } + + if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) { + throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object"); + } + } + + /** + * {@inheritDoc} + */ + public function isHandling(array $record): bool + { + return true; + } + + /** + * Manually activate this logger regardless of the activation strategy + */ + public function activate(): void + { + if ($this->stopBuffering) { + $this->buffering = false; + } + + $this->getHandler(end($this->buffer) ?: null)->handleBatch($this->buffer); + $this->buffer = []; + } + + /** + * {@inheritDoc} + */ + public function handle(array $record): bool + { + if ($this->processors) { + /** @var Record $record */ + $record = $this->processRecord($record); + } + + if ($this->buffering) { + $this->buffer[] = $record; + if ($this->bufferSize > 0 && count($this->buffer) > $this->bufferSize) { + array_shift($this->buffer); + } + if ($this->activationStrategy->isHandlerActivated($record)) { + $this->activate(); + } + } else { + $this->getHandler($record)->handle($record); + } + + return false === $this->bubble; + } + + /** + * {@inheritDoc} + */ + public function close(): void + { + $this->flushBuffer(); + + $this->getHandler()->close(); + } + + public function reset() + { + $this->flushBuffer(); + + $this->resetProcessors(); + + if ($this->getHandler() instanceof ResettableInterface) { + $this->getHandler()->reset(); + } + } + + /** + * Clears the buffer without flushing any messages down to the wrapped handler. + * + * It also resets the handler to its initial buffering state. + */ + public function clear(): void + { + $this->buffer = []; + $this->reset(); + } + + /** + * Resets the state of the handler. Stops forwarding records to the wrapped handler. + */ + private function flushBuffer(): void + { + if (null !== $this->passthruLevel) { + $level = $this->passthruLevel; + $this->buffer = array_filter($this->buffer, function ($record) use ($level) { + return $record['level'] >= $level; + }); + if (count($this->buffer) > 0) { + $this->getHandler(end($this->buffer))->handleBatch($this->buffer); + } + } + + $this->buffer = []; + $this->buffering = true; + } + + /** + * Return the nested handler + * + * If the handler was provided as a factory callable, this will trigger the handler's instantiation. + * + * @return HandlerInterface + * + * @phpstan-param Record $record + */ + public function getHandler(?array $record = null) + { + if (!$this->handler instanceof HandlerInterface) { + $this->handler = ($this->handler)($record, $this); + if (!$this->handler instanceof HandlerInterface) { + throw new \RuntimeException("The factory callable should return a HandlerInterface"); + } + } + + return $this->handler; + } + + /** + * {@inheritDoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + $handler->setFormatter($formatter); + + return $this; + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); + } + + /** + * {@inheritDoc} + */ + public function getFormatter(): FormatterInterface + { + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + return $handler->getFormatter(); + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/FirePHPHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/FirePHPHandler.php new file mode 100644 index 0000000..72718de --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/FirePHPHandler.php @@ -0,0 +1,180 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\WildfireFormatter; +use Monolog\Formatter\FormatterInterface; + +/** + * Simple FirePHP Handler (http://www.firephp.org/), which uses the Wildfire protocol. + * + * @author Eric Clemmons (@ericclemmons) + * + * @phpstan-import-type FormattedRecord from AbstractProcessingHandler + */ +class FirePHPHandler extends AbstractProcessingHandler +{ + use WebRequestRecognizerTrait; + + /** + * WildFire JSON header message format + */ + protected const PROTOCOL_URI = 'http://meta.wildfirehq.org/Protocol/JsonStream/0.2'; + + /** + * FirePHP structure for parsing messages & their presentation + */ + protected const STRUCTURE_URI = 'http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1'; + + /** + * Must reference a "known" plugin, otherwise headers won't display in FirePHP + */ + protected const PLUGIN_URI = 'http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/0.3'; + + /** + * Header prefix for Wildfire to recognize & parse headers + */ + protected const HEADER_PREFIX = 'X-Wf'; + + /** + * Whether or not Wildfire vendor-specific headers have been generated & sent yet + * @var bool + */ + protected static $initialized = false; + + /** + * Shared static message index between potentially multiple handlers + * @var int + */ + protected static $messageIndex = 1; + + /** @var bool */ + protected static $sendHeaders = true; + + /** + * Base header creation function used by init headers & record headers + * + * @param array $meta Wildfire Plugin, Protocol & Structure Indexes + * @param string $message Log message + * + * @return array Complete header string ready for the client as key and message as value + * + * @phpstan-return non-empty-array + */ + protected function createHeader(array $meta, string $message): array + { + $header = sprintf('%s-%s', static::HEADER_PREFIX, join('-', $meta)); + + return [$header => $message]; + } + + /** + * Creates message header from record + * + * @return array + * + * @phpstan-return non-empty-array + * + * @see createHeader() + * + * @phpstan-param FormattedRecord $record + */ + protected function createRecordHeader(array $record): array + { + // Wildfire is extensible to support multiple protocols & plugins in a single request, + // but we're not taking advantage of that (yet), so we're using "1" for simplicity's sake. + return $this->createHeader( + [1, 1, 1, self::$messageIndex++], + $record['formatted'] + ); + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new WildfireFormatter(); + } + + /** + * Wildfire initialization headers to enable message parsing + * + * @see createHeader() + * @see sendHeader() + * + * @return array + */ + protected function getInitHeaders(): array + { + // Initial payload consists of required headers for Wildfire + return array_merge( + $this->createHeader(['Protocol', 1], static::PROTOCOL_URI), + $this->createHeader([1, 'Structure', 1], static::STRUCTURE_URI), + $this->createHeader([1, 'Plugin', 1], static::PLUGIN_URI) + ); + } + + /** + * Send header string to the client + */ + protected function sendHeader(string $header, string $content): void + { + if (!headers_sent() && self::$sendHeaders) { + header(sprintf('%s: %s', $header, $content)); + } + } + + /** + * Creates & sends header for a record, ensuring init headers have been sent prior + * + * @see sendHeader() + * @see sendInitHeaders() + */ + protected function write(array $record): void + { + if (!self::$sendHeaders || !$this->isWebRequest()) { + return; + } + + // WildFire-specific headers must be sent prior to any messages + if (!self::$initialized) { + self::$initialized = true; + + self::$sendHeaders = $this->headersAccepted(); + if (!self::$sendHeaders) { + return; + } + + foreach ($this->getInitHeaders() as $header => $content) { + $this->sendHeader($header, $content); + } + } + + $header = $this->createRecordHeader($record); + if (trim(current($header)) !== '') { + $this->sendHeader(key($header), current($header)); + } + } + + /** + * Verifies if the headers are accepted by the current user agent + */ + protected function headersAccepted(): bool + { + if (!empty($_SERVER['HTTP_USER_AGENT']) && preg_match('{\bFirePHP/\d+\.\d+\b}', $_SERVER['HTTP_USER_AGENT'])) { + return true; + } + + return isset($_SERVER['HTTP_X_FIREPHP_VERSION']); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/FleepHookHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/FleepHookHandler.php new file mode 100644 index 0000000..85c95b9 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/FleepHookHandler.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LineFormatter; +use Monolog\Logger; + +/** + * Sends logs to Fleep.io using Webhook integrations + * + * You'll need a Fleep.io account to use this handler. + * + * @see https://fleep.io/integrations/webhooks/ Fleep Webhooks Documentation + * @author Ando Roots + * + * @phpstan-import-type FormattedRecord from AbstractProcessingHandler + */ +class FleepHookHandler extends SocketHandler +{ + protected const FLEEP_HOST = 'fleep.io'; + + protected const FLEEP_HOOK_URI = '/hook/'; + + /** + * @var string Webhook token (specifies the conversation where logs are sent) + */ + protected $token; + + /** + * Construct a new Fleep.io Handler. + * + * For instructions on how to create a new web hook in your conversations + * see https://fleep.io/integrations/webhooks/ + * + * @param string $token Webhook token + * @throws MissingExtensionException + */ + public function __construct( + string $token, + $level = Logger::DEBUG, + bool $bubble = true, + bool $persistent = false, + float $timeout = 0.0, + float $writingTimeout = 10.0, + ?float $connectionTimeout = null, + ?int $chunkSize = null + ) { + if (!extension_loaded('openssl')) { + throw new MissingExtensionException('The OpenSSL PHP extension is required to use the FleepHookHandler'); + } + + $this->token = $token; + + $connectionString = 'ssl://' . static::FLEEP_HOST . ':443'; + parent::__construct( + $connectionString, + $level, + $bubble, + $persistent, + $timeout, + $writingTimeout, + $connectionTimeout, + $chunkSize + ); + } + + /** + * Returns the default formatter to use with this handler + * + * Overloaded to remove empty context and extra arrays from the end of the log message. + * + * @return LineFormatter + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new LineFormatter(null, null, true, true); + } + + /** + * Handles a log record + */ + public function write(array $record): void + { + parent::write($record); + $this->closeSocket(); + } + + /** + * {@inheritDoc} + */ + protected function generateDataStream(array $record): string + { + $content = $this->buildContent($record); + + return $this->buildHeader($content) . $content; + } + + /** + * Builds the header of the API Call + */ + private function buildHeader(string $content): string + { + $header = "POST " . static::FLEEP_HOOK_URI . $this->token . " HTTP/1.1\r\n"; + $header .= "Host: " . static::FLEEP_HOST . "\r\n"; + $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; + $header .= "Content-Length: " . strlen($content) . "\r\n"; + $header .= "\r\n"; + + return $header; + } + + /** + * Builds the body of API call + * + * @phpstan-param FormattedRecord $record + */ + private function buildContent(array $record): string + { + $dataArray = [ + 'message' => $record['formatted'], + ]; + + return http_build_query($dataArray); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/FlowdockHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/FlowdockHandler.php new file mode 100644 index 0000000..5715d58 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/FlowdockHandler.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Utils; +use Monolog\Formatter\FlowdockFormatter; +use Monolog\Formatter\FormatterInterface; + +/** + * Sends notifications through the Flowdock push API + * + * This must be configured with a FlowdockFormatter instance via setFormatter() + * + * Notes: + * API token - Flowdock API token + * + * @author Dominik Liebler + * @see https://www.flowdock.com/api/push + * + * @phpstan-import-type FormattedRecord from AbstractProcessingHandler + * @deprecated Since 2.9.0 and 3.3.0, Flowdock was shutdown we will thus drop this handler in Monolog 4 + */ +class FlowdockHandler extends SocketHandler +{ + /** + * @var string + */ + protected $apiToken; + + /** + * @throws MissingExtensionException if OpenSSL is missing + */ + public function __construct( + string $apiToken, + $level = Logger::DEBUG, + bool $bubble = true, + bool $persistent = false, + float $timeout = 0.0, + float $writingTimeout = 10.0, + ?float $connectionTimeout = null, + ?int $chunkSize = null + ) { + if (!extension_loaded('openssl')) { + throw new MissingExtensionException('The OpenSSL PHP extension is required to use the FlowdockHandler'); + } + + parent::__construct( + 'ssl://api.flowdock.com:443', + $level, + $bubble, + $persistent, + $timeout, + $writingTimeout, + $connectionTimeout, + $chunkSize + ); + $this->apiToken = $apiToken; + } + + /** + * {@inheritDoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + if (!$formatter instanceof FlowdockFormatter) { + throw new \InvalidArgumentException('The FlowdockHandler requires an instance of Monolog\Formatter\FlowdockFormatter to function correctly'); + } + + return parent::setFormatter($formatter); + } + + /** + * Gets the default formatter. + */ + protected function getDefaultFormatter(): FormatterInterface + { + throw new \InvalidArgumentException('The FlowdockHandler must be configured (via setFormatter) with an instance of Monolog\Formatter\FlowdockFormatter to function correctly'); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + parent::write($record); + + $this->closeSocket(); + } + + /** + * {@inheritDoc} + */ + protected function generateDataStream(array $record): string + { + $content = $this->buildContent($record); + + return $this->buildHeader($content) . $content; + } + + /** + * Builds the body of API call + * + * @phpstan-param FormattedRecord $record + */ + private function buildContent(array $record): string + { + return Utils::jsonEncode($record['formatted']['flowdock']); + } + + /** + * Builds the header of the API Call + */ + private function buildHeader(string $content): string + { + $header = "POST /v1/messages/team_inbox/" . $this->apiToken . " HTTP/1.1\r\n"; + $header .= "Host: api.flowdock.com\r\n"; + $header .= "Content-Type: application/json\r\n"; + $header .= "Content-Length: " . strlen($content) . "\r\n"; + $header .= "\r\n"; + + return $header; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerInterface.php b/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerInterface.php new file mode 100644 index 0000000..fc1693c --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerInterface.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; + +/** + * Interface to describe loggers that have a formatter + * + * @author Jordi Boggiano + */ +interface FormattableHandlerInterface +{ + /** + * Sets the formatter. + * + * @param FormatterInterface $formatter + * @return HandlerInterface self + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface; + + /** + * Gets the formatter. + * + * @return FormatterInterface + */ + public function getFormatter(): FormatterInterface; +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerTrait.php b/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerTrait.php new file mode 100644 index 0000000..b60bdce --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerTrait.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LineFormatter; + +/** + * Helper trait for implementing FormattableInterface + * + * @author Jordi Boggiano + */ +trait FormattableHandlerTrait +{ + /** + * @var ?FormatterInterface + */ + protected $formatter; + + /** + * {@inheritDoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + $this->formatter = $formatter; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getFormatter(): FormatterInterface + { + if (!$this->formatter) { + $this->formatter = $this->getDefaultFormatter(); + } + + return $this->formatter; + } + + /** + * Gets the default formatter. + * + * Overwrite this if the LineFormatter is not a good default for your handler. + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new LineFormatter(); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/GelfHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/GelfHandler.php new file mode 100644 index 0000000..4ff26c4 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/GelfHandler.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Gelf\PublisherInterface; +use Monolog\Logger; +use Monolog\Formatter\GelfMessageFormatter; +use Monolog\Formatter\FormatterInterface; + +/** + * Handler to send messages to a Graylog2 (http://www.graylog2.org) server + * + * @author Matt Lehner + * @author Benjamin Zikarsky + */ +class GelfHandler extends AbstractProcessingHandler +{ + /** + * @var PublisherInterface the publisher object that sends the message to the server + */ + protected $publisher; + + /** + * @param PublisherInterface $publisher a gelf publisher object + */ + public function __construct(PublisherInterface $publisher, $level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + + $this->publisher = $publisher; + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $this->publisher->publish($record['formatted']); + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new GelfMessageFormatter(); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/GroupHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/GroupHandler.php new file mode 100644 index 0000000..3c9dc4b --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/GroupHandler.php @@ -0,0 +1,132 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\ResettableInterface; + +/** + * Forwards records to multiple handlers + * + * @author Lenar Lõhmus + * + * @phpstan-import-type Record from \Monolog\Logger + */ +class GroupHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface +{ + use ProcessableHandlerTrait; + + /** @var HandlerInterface[] */ + protected $handlers; + /** @var bool */ + protected $bubble; + + /** + * @param HandlerInterface[] $handlers Array of Handlers. + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct(array $handlers, bool $bubble = true) + { + foreach ($handlers as $handler) { + if (!$handler instanceof HandlerInterface) { + throw new \InvalidArgumentException('The first argument of the GroupHandler must be an array of HandlerInterface instances.'); + } + } + + $this->handlers = $handlers; + $this->bubble = $bubble; + } + + /** + * {@inheritDoc} + */ + public function isHandling(array $record): bool + { + foreach ($this->handlers as $handler) { + if ($handler->isHandling($record)) { + return true; + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function handle(array $record): bool + { + if ($this->processors) { + /** @var Record $record */ + $record = $this->processRecord($record); + } + + foreach ($this->handlers as $handler) { + $handler->handle($record); + } + + return false === $this->bubble; + } + + /** + * {@inheritDoc} + */ + public function handleBatch(array $records): void + { + if ($this->processors) { + $processed = []; + foreach ($records as $record) { + $processed[] = $this->processRecord($record); + } + /** @var Record[] $records */ + $records = $processed; + } + + foreach ($this->handlers as $handler) { + $handler->handleBatch($records); + } + } + + public function reset() + { + $this->resetProcessors(); + + foreach ($this->handlers as $handler) { + if ($handler instanceof ResettableInterface) { + $handler->reset(); + } + } + } + + public function close(): void + { + parent::close(); + + foreach ($this->handlers as $handler) { + $handler->close(); + } + } + + /** + * {@inheritDoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + foreach ($this->handlers as $handler) { + if ($handler instanceof FormattableHandlerInterface) { + $handler->setFormatter($formatter); + } + } + + return $this; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/Handler.php b/vendor/monolog/monolog/src/Monolog/Handler/Handler.php new file mode 100644 index 0000000..34b4935 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/Handler.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +/** + * Base Handler class providing basic close() support as well as handleBatch + * + * @author Jordi Boggiano + */ +abstract class Handler implements HandlerInterface +{ + /** + * {@inheritDoc} + */ + public function handleBatch(array $records): void + { + foreach ($records as $record) { + $this->handle($record); + } + } + + /** + * {@inheritDoc} + */ + public function close(): void + { + } + + public function __destruct() + { + try { + $this->close(); + } catch (\Throwable $e) { + // do nothing + } + } + + public function __sleep() + { + $this->close(); + + $reflClass = new \ReflectionClass($this); + + $keys = []; + foreach ($reflClass->getProperties() as $reflProp) { + if (!$reflProp->isStatic()) { + $keys[] = $reflProp->getName(); + } + } + + return $keys; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/HandlerInterface.php b/vendor/monolog/monolog/src/Monolog/Handler/HandlerInterface.php new file mode 100644 index 0000000..affcc51 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/HandlerInterface.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +/** + * Interface that all Monolog Handlers must implement + * + * @author Jordi Boggiano + * + * @phpstan-import-type Record from \Monolog\Logger + * @phpstan-import-type Level from \Monolog\Logger + */ +interface HandlerInterface +{ + /** + * Checks whether the given record will be handled by this handler. + * + * This is mostly done for performance reasons, to avoid calling processors for nothing. + * + * Handlers should still check the record levels within handle(), returning false in isHandling() + * is no guarantee that handle() will not be called, and isHandling() might not be called + * for a given record. + * + * @param array $record Partial log record containing only a level key + * + * @return bool + * + * @phpstan-param array{level: Level} $record + */ + public function isHandling(array $record): bool; + + /** + * Handles a record. + * + * All records may be passed to this method, and the handler should discard + * those that it does not want to handle. + * + * The return value of this function controls the bubbling process of the handler stack. + * Unless the bubbling is interrupted (by returning true), the Logger class will keep on + * calling further handlers in the stack with a given log record. + * + * @param array $record The record to handle + * @return bool true means that this handler handled the record, and that bubbling is not permitted. + * false means the record was either not processed or that this handler allows bubbling. + * + * @phpstan-param Record $record + */ + public function handle(array $record): bool; + + /** + * Handles a set of records at once. + * + * @param array $records The records to handle (an array of record arrays) + * + * @phpstan-param Record[] $records + */ + public function handleBatch(array $records): void; + + /** + * Closes the handler. + * + * Ends a log cycle and frees all resources used by the handler. + * + * Closing a Handler means flushing all buffers and freeing any open resources/handles. + * + * Implementations have to be idempotent (i.e. it should be possible to call close several times without breakage) + * and ideally handlers should be able to reopen themselves on handle() after they have been closed. + * + * This is useful at the end of a request and will be called automatically when the object + * is destroyed if you extend Monolog\Handler\Handler. + * + * If you are thinking of calling this method yourself, most likely you should be + * calling ResettableInterface::reset instead. Have a look. + */ + public function close(): void; +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/HandlerWrapper.php b/vendor/monolog/monolog/src/Monolog/Handler/HandlerWrapper.php new file mode 100644 index 0000000..d4351b9 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/HandlerWrapper.php @@ -0,0 +1,136 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\ResettableInterface; +use Monolog\Formatter\FormatterInterface; + +/** + * This simple wrapper class can be used to extend handlers functionality. + * + * Example: A custom filtering that can be applied to any handler. + * + * Inherit from this class and override handle() like this: + * + * public function handle(array $record) + * { + * if ($record meets certain conditions) { + * return false; + * } + * return $this->handler->handle($record); + * } + * + * @author Alexey Karapetov + */ +class HandlerWrapper implements HandlerInterface, ProcessableHandlerInterface, FormattableHandlerInterface, ResettableInterface +{ + /** + * @var HandlerInterface + */ + protected $handler; + + public function __construct(HandlerInterface $handler) + { + $this->handler = $handler; + } + + /** + * {@inheritDoc} + */ + public function isHandling(array $record): bool + { + return $this->handler->isHandling($record); + } + + /** + * {@inheritDoc} + */ + public function handle(array $record): bool + { + return $this->handler->handle($record); + } + + /** + * {@inheritDoc} + */ + public function handleBatch(array $records): void + { + $this->handler->handleBatch($records); + } + + /** + * {@inheritDoc} + */ + public function close(): void + { + $this->handler->close(); + } + + /** + * {@inheritDoc} + */ + public function pushProcessor(callable $callback): HandlerInterface + { + if ($this->handler instanceof ProcessableHandlerInterface) { + $this->handler->pushProcessor($callback); + + return $this; + } + + throw new \LogicException('The wrapped handler does not implement ' . ProcessableHandlerInterface::class); + } + + /** + * {@inheritDoc} + */ + public function popProcessor(): callable + { + if ($this->handler instanceof ProcessableHandlerInterface) { + return $this->handler->popProcessor(); + } + + throw new \LogicException('The wrapped handler does not implement ' . ProcessableHandlerInterface::class); + } + + /** + * {@inheritDoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + if ($this->handler instanceof FormattableHandlerInterface) { + $this->handler->setFormatter($formatter); + + return $this; + } + + throw new \LogicException('The wrapped handler does not implement ' . FormattableHandlerInterface::class); + } + + /** + * {@inheritDoc} + */ + public function getFormatter(): FormatterInterface + { + if ($this->handler instanceof FormattableHandlerInterface) { + return $this->handler->getFormatter(); + } + + throw new \LogicException('The wrapped handler does not implement ' . FormattableHandlerInterface::class); + } + + public function reset() + { + if ($this->handler instanceof ResettableInterface) { + $this->handler->reset(); + } + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/IFTTTHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/IFTTTHandler.php new file mode 100644 index 0000000..000ccea --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/IFTTTHandler.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Utils; + +/** + * IFTTTHandler uses cURL to trigger IFTTT Maker actions + * + * Register a secret key and trigger/event name at https://ifttt.com/maker + * + * value1 will be the channel from monolog's Logger constructor, + * value2 will be the level name (ERROR, WARNING, ..) + * value3 will be the log record's message + * + * @author Nehal Patel + */ +class IFTTTHandler extends AbstractProcessingHandler +{ + /** @var string */ + private $eventName; + /** @var string */ + private $secretKey; + + /** + * @param string $eventName The name of the IFTTT Maker event that should be triggered + * @param string $secretKey A valid IFTTT secret key + */ + public function __construct(string $eventName, string $secretKey, $level = Logger::ERROR, bool $bubble = true) + { + if (!extension_loaded('curl')) { + throw new MissingExtensionException('The curl extension is needed to use the IFTTTHandler'); + } + + $this->eventName = $eventName; + $this->secretKey = $secretKey; + + parent::__construct($level, $bubble); + } + + /** + * {@inheritDoc} + */ + public function write(array $record): void + { + $postData = [ + "value1" => $record["channel"], + "value2" => $record["level_name"], + "value3" => $record["message"], + ]; + $postString = Utils::jsonEncode($postData); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, "https://maker.ifttt.com/trigger/" . $this->eventName . "/with/key/" . $this->secretKey); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $postString); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Content-Type: application/json", + ]); + + Curl\Util::execute($ch); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/InsightOpsHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/InsightOpsHandler.php new file mode 100644 index 0000000..71f64a2 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/InsightOpsHandler.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; + +/** + * Inspired on LogEntriesHandler. + * + * @author Robert Kaufmann III + * @author Gabriel Machado + */ +class InsightOpsHandler extends SocketHandler +{ + /** + * @var string + */ + protected $logToken; + + /** + * @param string $token Log token supplied by InsightOps + * @param string $region Region where InsightOps account is hosted. Could be 'us' or 'eu'. + * @param bool $useSSL Whether or not SSL encryption should be used + * + * @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing + */ + public function __construct( + string $token, + string $region = 'us', + bool $useSSL = true, + $level = Logger::DEBUG, + bool $bubble = true, + bool $persistent = false, + float $timeout = 0.0, + float $writingTimeout = 10.0, + ?float $connectionTimeout = null, + ?int $chunkSize = null + ) { + if ($useSSL && !extension_loaded('openssl')) { + throw new MissingExtensionException('The OpenSSL PHP plugin is required to use SSL encrypted connection for InsightOpsHandler'); + } + + $endpoint = $useSSL + ? 'ssl://' . $region . '.data.logs.insight.rapid7.com:443' + : $region . '.data.logs.insight.rapid7.com:80'; + + parent::__construct( + $endpoint, + $level, + $bubble, + $persistent, + $timeout, + $writingTimeout, + $connectionTimeout, + $chunkSize + ); + $this->logToken = $token; + } + + /** + * {@inheritDoc} + */ + protected function generateDataStream(array $record): string + { + return $this->logToken . ' ' . $record['formatted']; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/LogEntriesHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/LogEntriesHandler.php new file mode 100644 index 0000000..25fcd15 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/LogEntriesHandler.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; + +/** + * @author Robert Kaufmann III + */ +class LogEntriesHandler extends SocketHandler +{ + /** + * @var string + */ + protected $logToken; + + /** + * @param string $token Log token supplied by LogEntries + * @param bool $useSSL Whether or not SSL encryption should be used. + * @param string $host Custom hostname to send the data to if needed + * + * @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing + */ + public function __construct( + string $token, + bool $useSSL = true, + $level = Logger::DEBUG, + bool $bubble = true, + string $host = 'data.logentries.com', + bool $persistent = false, + float $timeout = 0.0, + float $writingTimeout = 10.0, + ?float $connectionTimeout = null, + ?int $chunkSize = null + ) { + if ($useSSL && !extension_loaded('openssl')) { + throw new MissingExtensionException('The OpenSSL PHP plugin is required to use SSL encrypted connection for LogEntriesHandler'); + } + + $endpoint = $useSSL ? 'ssl://' . $host . ':443' : $host . ':80'; + parent::__construct( + $endpoint, + $level, + $bubble, + $persistent, + $timeout, + $writingTimeout, + $connectionTimeout, + $chunkSize + ); + $this->logToken = $token; + } + + /** + * {@inheritDoc} + */ + protected function generateDataStream(array $record): string + { + return $this->logToken . ' ' . $record['formatted']; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/LogglyHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/LogglyHandler.php new file mode 100644 index 0000000..6d13db3 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/LogglyHandler.php @@ -0,0 +1,160 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LogglyFormatter; +use function array_key_exists; +use CurlHandle; + +/** + * Sends errors to Loggly. + * + * @author Przemek Sobstel + * @author Adam Pancutt + * @author Gregory Barchard + */ +class LogglyHandler extends AbstractProcessingHandler +{ + protected const HOST = 'logs-01.loggly.com'; + protected const ENDPOINT_SINGLE = 'inputs'; + protected const ENDPOINT_BATCH = 'bulk'; + + /** + * Caches the curl handlers for every given endpoint. + * + * @var resource[]|CurlHandle[] + */ + protected $curlHandlers = []; + + /** @var string */ + protected $token; + + /** @var string[] */ + protected $tag = []; + + /** + * @param string $token API token supplied by Loggly + * + * @throws MissingExtensionException If the curl extension is missing + */ + public function __construct(string $token, $level = Logger::DEBUG, bool $bubble = true) + { + if (!extension_loaded('curl')) { + throw new MissingExtensionException('The curl extension is needed to use the LogglyHandler'); + } + + $this->token = $token; + + parent::__construct($level, $bubble); + } + + /** + * Loads and returns the shared curl handler for the given endpoint. + * + * @param string $endpoint + * + * @return resource|CurlHandle + */ + protected function getCurlHandler(string $endpoint) + { + if (!array_key_exists($endpoint, $this->curlHandlers)) { + $this->curlHandlers[$endpoint] = $this->loadCurlHandle($endpoint); + } + + return $this->curlHandlers[$endpoint]; + } + + /** + * Starts a fresh curl session for the given endpoint and returns its handler. + * + * @param string $endpoint + * + * @return resource|CurlHandle + */ + private function loadCurlHandle(string $endpoint) + { + $url = sprintf("https://%s/%s/%s/", static::HOST, $endpoint, $this->token); + + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + return $ch; + } + + /** + * @param string[]|string $tag + */ + public function setTag($tag): self + { + $tag = !empty($tag) ? $tag : []; + $this->tag = is_array($tag) ? $tag : [$tag]; + + return $this; + } + + /** + * @param string[]|string $tag + */ + public function addTag($tag): self + { + if (!empty($tag)) { + $tag = is_array($tag) ? $tag : [$tag]; + $this->tag = array_unique(array_merge($this->tag, $tag)); + } + + return $this; + } + + protected function write(array $record): void + { + $this->send($record["formatted"], static::ENDPOINT_SINGLE); + } + + public function handleBatch(array $records): void + { + $level = $this->level; + + $records = array_filter($records, function ($record) use ($level) { + return ($record['level'] >= $level); + }); + + if ($records) { + $this->send($this->getFormatter()->formatBatch($records), static::ENDPOINT_BATCH); + } + } + + protected function send(string $data, string $endpoint): void + { + $ch = $this->getCurlHandler($endpoint); + + $headers = ['Content-Type: application/json']; + + if (!empty($this->tag)) { + $headers[] = 'X-LOGGLY-TAG: '.implode(',', $this->tag); + } + + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + Curl\Util::execute($ch, 5, false); + } + + protected function getDefaultFormatter(): FormatterInterface + { + return new LogglyFormatter(); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/LogmaticHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/LogmaticHandler.php new file mode 100644 index 0000000..859a469 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/LogmaticHandler.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LogmaticFormatter; + +/** + * @author Julien Breux + */ +class LogmaticHandler extends SocketHandler +{ + /** + * @var string + */ + private $logToken; + + /** + * @var string + */ + private $hostname; + + /** + * @var string + */ + private $appname; + + /** + * @param string $token Log token supplied by Logmatic. + * @param string $hostname Host name supplied by Logmatic. + * @param string $appname Application name supplied by Logmatic. + * @param bool $useSSL Whether or not SSL encryption should be used. + * + * @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing + */ + public function __construct( + string $token, + string $hostname = '', + string $appname = '', + bool $useSSL = true, + $level = Logger::DEBUG, + bool $bubble = true, + bool $persistent = false, + float $timeout = 0.0, + float $writingTimeout = 10.0, + ?float $connectionTimeout = null, + ?int $chunkSize = null + ) { + if ($useSSL && !extension_loaded('openssl')) { + throw new MissingExtensionException('The OpenSSL PHP extension is required to use SSL encrypted connection for LogmaticHandler'); + } + + $endpoint = $useSSL ? 'ssl://api.logmatic.io:10515' : 'api.logmatic.io:10514'; + $endpoint .= '/v1/'; + + parent::__construct( + $endpoint, + $level, + $bubble, + $persistent, + $timeout, + $writingTimeout, + $connectionTimeout, + $chunkSize + ); + + $this->logToken = $token; + $this->hostname = $hostname; + $this->appname = $appname; + } + + /** + * {@inheritDoc} + */ + protected function generateDataStream(array $record): string + { + return $this->logToken . ' ' . $record['formatted']; + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + $formatter = new LogmaticFormatter(); + + if (!empty($this->hostname)) { + $formatter->setHostname($this->hostname); + } + if (!empty($this->appname)) { + $formatter->setAppname($this->appname); + } + + return $formatter; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/MailHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/MailHandler.php new file mode 100644 index 0000000..97f3432 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/MailHandler.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\HtmlFormatter; + +/** + * Base class for all mail handlers + * + * @author Gyula Sallai + * + * @phpstan-import-type Record from \Monolog\Logger + */ +abstract class MailHandler extends AbstractProcessingHandler +{ + /** + * {@inheritDoc} + */ + public function handleBatch(array $records): void + { + $messages = []; + + foreach ($records as $record) { + if ($record['level'] < $this->level) { + continue; + } + /** @var Record $message */ + $message = $this->processRecord($record); + $messages[] = $message; + } + + if (!empty($messages)) { + $this->send((string) $this->getFormatter()->formatBatch($messages), $messages); + } + } + + /** + * Send a mail with the given content + * + * @param string $content formatted email body to be sent + * @param array $records the array of log records that formed this content + * + * @phpstan-param Record[] $records + */ + abstract protected function send(string $content, array $records): void; + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $this->send((string) $record['formatted'], [$record]); + } + + /** + * @phpstan-param non-empty-array $records + * @phpstan-return Record + */ + protected function getHighestRecord(array $records): array + { + $highestRecord = null; + foreach ($records as $record) { + if ($highestRecord === null || $highestRecord['level'] < $record['level']) { + $highestRecord = $record; + } + } + + return $highestRecord; + } + + protected function isHtmlBody(string $body): bool + { + return ($body[0] ?? null) === '<'; + } + + /** + * Gets the default formatter. + * + * @return FormatterInterface + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new HtmlFormatter(); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/MandrillHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/MandrillHandler.php new file mode 100644 index 0000000..3003500 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/MandrillHandler.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Swift; +use Swift_Message; + +/** + * MandrillHandler uses cURL to send the emails to the Mandrill API + * + * @author Adam Nicholson + */ +class MandrillHandler extends MailHandler +{ + /** @var Swift_Message */ + protected $message; + /** @var string */ + protected $apiKey; + + /** + * @psalm-param Swift_Message|callable(): Swift_Message $message + * + * @param string $apiKey A valid Mandrill API key + * @param callable|Swift_Message $message An example message for real messages, only the body will be replaced + */ + public function __construct(string $apiKey, $message, $level = Logger::ERROR, bool $bubble = true) + { + parent::__construct($level, $bubble); + + if (!$message instanceof Swift_Message && is_callable($message)) { + $message = $message(); + } + if (!$message instanceof Swift_Message) { + throw new \InvalidArgumentException('You must provide either a Swift_Message instance or a callable returning it'); + } + $this->message = $message; + $this->apiKey = $apiKey; + } + + /** + * {@inheritDoc} + */ + protected function send(string $content, array $records): void + { + $mime = 'text/plain'; + if ($this->isHtmlBody($content)) { + $mime = 'text/html'; + } + + $message = clone $this->message; + $message->setBody($content, $mime); + /** @phpstan-ignore-next-line */ + if (version_compare(Swift::VERSION, '6.0.0', '>=')) { + $message->setDate(new \DateTimeImmutable()); + } else { + /** @phpstan-ignore-next-line */ + $message->setDate(time()); + } + + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, 'https://mandrillapp.com/api/1.0/messages/send-raw.json'); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([ + 'key' => $this->apiKey, + 'raw_message' => (string) $message, + 'async' => false, + ])); + + Curl\Util::execute($ch); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/MissingExtensionException.php b/vendor/monolog/monolog/src/Monolog/Handler/MissingExtensionException.php new file mode 100644 index 0000000..3965aee --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/MissingExtensionException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +/** + * Exception can be thrown if an extension for a handler is missing + * + * @author Christian Bergau + */ +class MissingExtensionException extends \Exception +{ +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/MongoDBHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/MongoDBHandler.php new file mode 100644 index 0000000..3063091 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/MongoDBHandler.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use MongoDB\Driver\BulkWrite; +use MongoDB\Driver\Manager; +use MongoDB\Client; +use Monolog\Logger; +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\MongoDBFormatter; + +/** + * Logs to a MongoDB database. + * + * Usage example: + * + * $log = new \Monolog\Logger('application'); + * $client = new \MongoDB\Client('mongodb://localhost:27017'); + * $mongodb = new \Monolog\Handler\MongoDBHandler($client, 'logs', 'prod'); + * $log->pushHandler($mongodb); + * + * The above examples uses the MongoDB PHP library's client class; however, the + * MongoDB\Driver\Manager class from ext-mongodb is also supported. + */ +class MongoDBHandler extends AbstractProcessingHandler +{ + /** @var \MongoDB\Collection */ + private $collection; + /** @var Client|Manager */ + private $manager; + /** @var string */ + private $namespace; + + /** + * Constructor. + * + * @param Client|Manager $mongodb MongoDB library or driver client + * @param string $database Database name + * @param string $collection Collection name + */ + public function __construct($mongodb, string $database, string $collection, $level = Logger::DEBUG, bool $bubble = true) + { + if (!($mongodb instanceof Client || $mongodb instanceof Manager)) { + throw new \InvalidArgumentException('MongoDB\Client or MongoDB\Driver\Manager instance required'); + } + + if ($mongodb instanceof Client) { + $this->collection = $mongodb->selectCollection($database, $collection); + } else { + $this->manager = $mongodb; + $this->namespace = $database . '.' . $collection; + } + + parent::__construct($level, $bubble); + } + + protected function write(array $record): void + { + if (isset($this->collection)) { + $this->collection->insertOne($record['formatted']); + } + + if (isset($this->manager, $this->namespace)) { + $bulk = new BulkWrite; + $bulk->insert($record["formatted"]); + $this->manager->executeBulkWrite($this->namespace, $bulk); + } + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new MongoDBFormatter; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/NativeMailerHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/NativeMailerHandler.php new file mode 100644 index 0000000..0c0a3bd --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/NativeMailerHandler.php @@ -0,0 +1,174 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Formatter\LineFormatter; + +/** + * NativeMailerHandler uses the mail() function to send the emails + * + * @author Christophe Coevoet + * @author Mark Garrett + */ +class NativeMailerHandler extends MailHandler +{ + /** + * The email addresses to which the message will be sent + * @var string[] + */ + protected $to; + + /** + * The subject of the email + * @var string + */ + protected $subject; + + /** + * Optional headers for the message + * @var string[] + */ + protected $headers = []; + + /** + * Optional parameters for the message + * @var string[] + */ + protected $parameters = []; + + /** + * The wordwrap length for the message + * @var int + */ + protected $maxColumnWidth; + + /** + * The Content-type for the message + * @var string|null + */ + protected $contentType; + + /** + * The encoding for the message + * @var string + */ + protected $encoding = 'utf-8'; + + /** + * @param string|string[] $to The receiver of the mail + * @param string $subject The subject of the mail + * @param string $from The sender of the mail + * @param int $maxColumnWidth The maximum column width that the message lines will have + */ + public function __construct($to, string $subject, string $from, $level = Logger::ERROR, bool $bubble = true, int $maxColumnWidth = 70) + { + parent::__construct($level, $bubble); + $this->to = (array) $to; + $this->subject = $subject; + $this->addHeader(sprintf('From: %s', $from)); + $this->maxColumnWidth = $maxColumnWidth; + } + + /** + * Add headers to the message + * + * @param string|string[] $headers Custom added headers + */ + public function addHeader($headers): self + { + foreach ((array) $headers as $header) { + if (strpos($header, "\n") !== false || strpos($header, "\r") !== false) { + throw new \InvalidArgumentException('Headers can not contain newline characters for security reasons'); + } + $this->headers[] = $header; + } + + return $this; + } + + /** + * Add parameters to the message + * + * @param string|string[] $parameters Custom added parameters + */ + public function addParameter($parameters): self + { + $this->parameters = array_merge($this->parameters, (array) $parameters); + + return $this; + } + + /** + * {@inheritDoc} + */ + protected function send(string $content, array $records): void + { + $contentType = $this->getContentType() ?: ($this->isHtmlBody($content) ? 'text/html' : 'text/plain'); + + if ($contentType !== 'text/html') { + $content = wordwrap($content, $this->maxColumnWidth); + } + + $headers = ltrim(implode("\r\n", $this->headers) . "\r\n", "\r\n"); + $headers .= 'Content-type: ' . $contentType . '; charset=' . $this->getEncoding() . "\r\n"; + if ($contentType === 'text/html' && false === strpos($headers, 'MIME-Version:')) { + $headers .= 'MIME-Version: 1.0' . "\r\n"; + } + + $subject = $this->subject; + if ($records) { + $subjectFormatter = new LineFormatter($this->subject); + $subject = $subjectFormatter->format($this->getHighestRecord($records)); + } + + $parameters = implode(' ', $this->parameters); + foreach ($this->to as $to) { + mail($to, $subject, $content, $headers, $parameters); + } + } + + public function getContentType(): ?string + { + return $this->contentType; + } + + public function getEncoding(): string + { + return $this->encoding; + } + + /** + * @param string $contentType The content type of the email - Defaults to text/plain. Use text/html for HTML messages. + */ + public function setContentType(string $contentType): self + { + if (strpos($contentType, "\n") !== false || strpos($contentType, "\r") !== false) { + throw new \InvalidArgumentException('The content type can not contain newline characters to prevent email header injection'); + } + + $this->contentType = $contentType; + + return $this; + } + + public function setEncoding(string $encoding): self + { + if (strpos($encoding, "\n") !== false || strpos($encoding, "\r") !== false) { + throw new \InvalidArgumentException('The encoding can not contain newline characters to prevent email header injection'); + } + + $this->encoding = $encoding; + + return $this; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/NewRelicHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/NewRelicHandler.php new file mode 100644 index 0000000..114d749 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/NewRelicHandler.php @@ -0,0 +1,199 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Utils; +use Monolog\Formatter\NormalizerFormatter; +use Monolog\Formatter\FormatterInterface; + +/** + * Class to record a log on a NewRelic application. + * Enabling New Relic High Security mode may prevent capture of useful information. + * + * This handler requires a NormalizerFormatter to function and expects an array in $record['formatted'] + * + * @see https://docs.newrelic.com/docs/agents/php-agent + * @see https://docs.newrelic.com/docs/accounts-partnerships/accounts/security/high-security + */ +class NewRelicHandler extends AbstractProcessingHandler +{ + /** + * Name of the New Relic application that will receive logs from this handler. + * + * @var ?string + */ + protected $appName; + + /** + * Name of the current transaction + * + * @var ?string + */ + protected $transactionName; + + /** + * Some context and extra data is passed into the handler as arrays of values. Do we send them as is + * (useful if we are using the API), or explode them for display on the NewRelic RPM website? + * + * @var bool + */ + protected $explodeArrays; + + /** + * {@inheritDoc} + * + * @param string|null $appName + * @param bool $explodeArrays + * @param string|null $transactionName + */ + public function __construct( + $level = Logger::ERROR, + bool $bubble = true, + ?string $appName = null, + bool $explodeArrays = false, + ?string $transactionName = null + ) { + parent::__construct($level, $bubble); + + $this->appName = $appName; + $this->explodeArrays = $explodeArrays; + $this->transactionName = $transactionName; + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + if (!$this->isNewRelicEnabled()) { + throw new MissingExtensionException('The newrelic PHP extension is required to use the NewRelicHandler'); + } + + if ($appName = $this->getAppName($record['context'])) { + $this->setNewRelicAppName($appName); + } + + if ($transactionName = $this->getTransactionName($record['context'])) { + $this->setNewRelicTransactionName($transactionName); + unset($record['formatted']['context']['transaction_name']); + } + + if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) { + newrelic_notice_error($record['message'], $record['context']['exception']); + unset($record['formatted']['context']['exception']); + } else { + newrelic_notice_error($record['message']); + } + + if (isset($record['formatted']['context']) && is_array($record['formatted']['context'])) { + foreach ($record['formatted']['context'] as $key => $parameter) { + if (is_array($parameter) && $this->explodeArrays) { + foreach ($parameter as $paramKey => $paramValue) { + $this->setNewRelicParameter('context_' . $key . '_' . $paramKey, $paramValue); + } + } else { + $this->setNewRelicParameter('context_' . $key, $parameter); + } + } + } + + if (isset($record['formatted']['extra']) && is_array($record['formatted']['extra'])) { + foreach ($record['formatted']['extra'] as $key => $parameter) { + if (is_array($parameter) && $this->explodeArrays) { + foreach ($parameter as $paramKey => $paramValue) { + $this->setNewRelicParameter('extra_' . $key . '_' . $paramKey, $paramValue); + } + } else { + $this->setNewRelicParameter('extra_' . $key, $parameter); + } + } + } + } + + /** + * Checks whether the NewRelic extension is enabled in the system. + * + * @return bool + */ + protected function isNewRelicEnabled(): bool + { + return extension_loaded('newrelic'); + } + + /** + * Returns the appname where this log should be sent. Each log can override the default appname, set in this + * handler's constructor, by providing the appname in it's context. + * + * @param mixed[] $context + */ + protected function getAppName(array $context): ?string + { + if (isset($context['appname'])) { + return $context['appname']; + } + + return $this->appName; + } + + /** + * Returns the name of the current transaction. Each log can override the default transaction name, set in this + * handler's constructor, by providing the transaction_name in it's context + * + * @param mixed[] $context + */ + protected function getTransactionName(array $context): ?string + { + if (isset($context['transaction_name'])) { + return $context['transaction_name']; + } + + return $this->transactionName; + } + + /** + * Sets the NewRelic application that should receive this log. + */ + protected function setNewRelicAppName(string $appName): void + { + newrelic_set_appname($appName); + } + + /** + * Overwrites the name of the current transaction + */ + protected function setNewRelicTransactionName(string $transactionName): void + { + newrelic_name_transaction($transactionName); + } + + /** + * @param string $key + * @param mixed $value + */ + protected function setNewRelicParameter(string $key, $value): void + { + if (null === $value || is_scalar($value)) { + newrelic_add_custom_parameter($key, $value); + } else { + newrelic_add_custom_parameter($key, Utils::jsonEncode($value, null, true)); + } + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new NormalizerFormatter(); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/NoopHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/NoopHandler.php new file mode 100644 index 0000000..1ddf0be --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/NoopHandler.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +/** + * No-op + * + * This handler handles anything, but does nothing, and does not stop bubbling to the rest of the stack. + * This can be used for testing, or to disable a handler when overriding a configuration without + * influencing the rest of the stack. + * + * @author Roel Harbers + */ +class NoopHandler extends Handler +{ + /** + * {@inheritDoc} + */ + public function isHandling(array $record): bool + { + return true; + } + + /** + * {@inheritDoc} + */ + public function handle(array $record): bool + { + return false; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/NullHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/NullHandler.php new file mode 100644 index 0000000..e75ee0c --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/NullHandler.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Psr\Log\LogLevel; + +/** + * Blackhole + * + * Any record it can handle will be thrown away. This can be used + * to put on top of an existing stack to override it temporarily. + * + * @author Jordi Boggiano + * + * @phpstan-import-type Level from \Monolog\Logger + * @phpstan-import-type LevelName from \Monolog\Logger + */ +class NullHandler extends Handler +{ + /** + * @var int + */ + private $level; + + /** + * @param string|int $level The minimum logging level at which this handler will be triggered + * + * @phpstan-param Level|LevelName|LogLevel::* $level + */ + public function __construct($level = Logger::DEBUG) + { + $this->level = Logger::toMonologLevel($level); + } + + /** + * {@inheritDoc} + */ + public function isHandling(array $record): bool + { + return $record['level'] >= $this->level; + } + + /** + * {@inheritDoc} + */ + public function handle(array $record): bool + { + return $record['level'] >= $this->level; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/OverflowHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/OverflowHandler.php new file mode 100644 index 0000000..22068c9 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/OverflowHandler.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Formatter\FormatterInterface; + +/** + * Handler to only pass log messages when a certain threshold of number of messages is reached. + * + * This can be useful in cases of processing a batch of data, but you're for example only interested + * in case it fails catastrophically instead of a warning for 1 or 2 events. Worse things can happen, right? + * + * Usage example: + * + * ``` + * $log = new Logger('application'); + * $handler = new SomeHandler(...) + * + * // Pass all warnings to the handler when more than 10 & all error messages when more then 5 + * $overflow = new OverflowHandler($handler, [Logger::WARNING => 10, Logger::ERROR => 5]); + * + * $log->pushHandler($overflow); + *``` + * + * @author Kris Buist + */ +class OverflowHandler extends AbstractHandler implements FormattableHandlerInterface +{ + /** @var HandlerInterface */ + private $handler; + + /** @var int[] */ + private $thresholdMap = [ + Logger::DEBUG => 0, + Logger::INFO => 0, + Logger::NOTICE => 0, + Logger::WARNING => 0, + Logger::ERROR => 0, + Logger::CRITICAL => 0, + Logger::ALERT => 0, + Logger::EMERGENCY => 0, + ]; + + /** + * Buffer of all messages passed to the handler before the threshold was reached + * + * @var mixed[][] + */ + private $buffer = []; + + /** + * @param HandlerInterface $handler + * @param int[] $thresholdMap Dictionary of logger level => threshold + */ + public function __construct( + HandlerInterface $handler, + array $thresholdMap = [], + $level = Logger::DEBUG, + bool $bubble = true + ) { + $this->handler = $handler; + foreach ($thresholdMap as $thresholdLevel => $threshold) { + $this->thresholdMap[$thresholdLevel] = $threshold; + } + parent::__construct($level, $bubble); + } + + /** + * Handles a record. + * + * All records may be passed to this method, and the handler should discard + * those that it does not want to handle. + * + * The return value of this function controls the bubbling process of the handler stack. + * Unless the bubbling is interrupted (by returning true), the Logger class will keep on + * calling further handlers in the stack with a given log record. + * + * {@inheritDoc} + */ + public function handle(array $record): bool + { + if ($record['level'] < $this->level) { + return false; + } + + $level = $record['level']; + + if (!isset($this->thresholdMap[$level])) { + $this->thresholdMap[$level] = 0; + } + + if ($this->thresholdMap[$level] > 0) { + // The overflow threshold is not yet reached, so we're buffering the record and lowering the threshold by 1 + $this->thresholdMap[$level]--; + $this->buffer[$level][] = $record; + + return false === $this->bubble; + } + + if ($this->thresholdMap[$level] == 0) { + // This current message is breaking the threshold. Flush the buffer and continue handling the current record + foreach ($this->buffer[$level] ?? [] as $buffered) { + $this->handler->handle($buffered); + } + $this->thresholdMap[$level]--; + unset($this->buffer[$level]); + } + + $this->handler->handle($record); + + return false === $this->bubble; + } + + /** + * {@inheritDoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + if ($this->handler instanceof FormattableHandlerInterface) { + $this->handler->setFormatter($formatter); + + return $this; + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.'); + } + + /** + * {@inheritDoc} + */ + public function getFormatter(): FormatterInterface + { + if ($this->handler instanceof FormattableHandlerInterface) { + return $this->handler->getFormatter(); + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.'); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/PHPConsoleHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/PHPConsoleHandler.php new file mode 100644 index 0000000..23a1d11 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/PHPConsoleHandler.php @@ -0,0 +1,263 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\LineFormatter; +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; +use Monolog\Utils; +use PhpConsole\Connector; +use PhpConsole\Handler as VendorPhpConsoleHandler; +use PhpConsole\Helper; + +/** + * Monolog handler for Google Chrome extension "PHP Console" + * + * Display PHP error/debug log messages in Google Chrome console and notification popups, executes PHP code remotely + * + * Usage: + * 1. Install Google Chrome extension [now dead and removed from the chrome store] + * 2. See overview https://github.com/barbushin/php-console#overview + * 3. Install PHP Console library https://github.com/barbushin/php-console#installation + * 4. Example (result will looks like http://i.hizliresim.com/vg3Pz4.png) + * + * $logger = new \Monolog\Logger('all', array(new \Monolog\Handler\PHPConsoleHandler())); + * \Monolog\ErrorHandler::register($logger); + * echo $undefinedVar; + * $logger->debug('SELECT * FROM users', array('db', 'time' => 0.012)); + * PC::debug($_SERVER); // PHP Console debugger for any type of vars + * + * @author Sergey Barbushin https://www.linkedin.com/in/barbushin + * + * @phpstan-import-type Record from \Monolog\Logger + * @deprecated Since 2.8.0 and 3.2.0, PHPConsole is abandoned and thus we will drop this handler in Monolog 4 + */ +class PHPConsoleHandler extends AbstractProcessingHandler +{ + /** @var array */ + private $options = [ + 'enabled' => true, // bool Is PHP Console server enabled + 'classesPartialsTraceIgnore' => ['Monolog\\'], // array Hide calls of classes started with... + 'debugTagsKeysInContext' => [0, 'tag'], // bool Is PHP Console server enabled + 'useOwnErrorsHandler' => false, // bool Enable errors handling + 'useOwnExceptionsHandler' => false, // bool Enable exceptions handling + 'sourcesBasePath' => null, // string Base path of all project sources to strip in errors source paths + 'registerHelper' => true, // bool Register PhpConsole\Helper that allows short debug calls like PC::debug($var, 'ta.g.s') + 'serverEncoding' => null, // string|null Server internal encoding + 'headersLimit' => null, // int|null Set headers size limit for your web-server + 'password' => null, // string|null Protect PHP Console connection by password + 'enableSslOnlyMode' => false, // bool Force connection by SSL for clients with PHP Console installed + 'ipMasks' => [], // array Set IP masks of clients that will be allowed to connect to PHP Console: array('192.168.*.*', '127.0.0.1') + 'enableEvalListener' => false, // bool Enable eval request to be handled by eval dispatcher(if enabled, 'password' option is also required) + 'dumperDetectCallbacks' => false, // bool Convert callback items in dumper vars to (callback SomeClass::someMethod) strings + 'dumperLevelLimit' => 5, // int Maximum dumped vars array or object nested dump level + 'dumperItemsCountLimit' => 100, // int Maximum dumped var same level array items or object properties number + 'dumperItemSizeLimit' => 5000, // int Maximum length of any string or dumped array item + 'dumperDumpSizeLimit' => 500000, // int Maximum approximate size of dumped vars result formatted in JSON + 'detectDumpTraceAndSource' => false, // bool Autodetect and append trace data to debug + 'dataStorage' => null, // \PhpConsole\Storage|null Fixes problem with custom $_SESSION handler(see http://goo.gl/Ne8juJ) + ]; + + /** @var Connector */ + private $connector; + + /** + * @param array $options See \Monolog\Handler\PHPConsoleHandler::$options for more details + * @param Connector|null $connector Instance of \PhpConsole\Connector class (optional) + * @throws \RuntimeException + */ + public function __construct(array $options = [], ?Connector $connector = null, $level = Logger::DEBUG, bool $bubble = true) + { + if (!class_exists('PhpConsole\Connector')) { + throw new \RuntimeException('PHP Console library not found. See https://github.com/barbushin/php-console#installation'); + } + parent::__construct($level, $bubble); + $this->options = $this->initOptions($options); + $this->connector = $this->initConnector($connector); + } + + /** + * @param array $options + * + * @return array + */ + private function initOptions(array $options): array + { + $wrongOptions = array_diff(array_keys($options), array_keys($this->options)); + if ($wrongOptions) { + throw new \RuntimeException('Unknown options: ' . implode(', ', $wrongOptions)); + } + + return array_replace($this->options, $options); + } + + private function initConnector(?Connector $connector = null): Connector + { + if (!$connector) { + if ($this->options['dataStorage']) { + Connector::setPostponeStorage($this->options['dataStorage']); + } + $connector = Connector::getInstance(); + } + + if ($this->options['registerHelper'] && !Helper::isRegistered()) { + Helper::register(); + } + + if ($this->options['enabled'] && $connector->isActiveClient()) { + if ($this->options['useOwnErrorsHandler'] || $this->options['useOwnExceptionsHandler']) { + $handler = VendorPhpConsoleHandler::getInstance(); + $handler->setHandleErrors($this->options['useOwnErrorsHandler']); + $handler->setHandleExceptions($this->options['useOwnExceptionsHandler']); + $handler->start(); + } + if ($this->options['sourcesBasePath']) { + $connector->setSourcesBasePath($this->options['sourcesBasePath']); + } + if ($this->options['serverEncoding']) { + $connector->setServerEncoding($this->options['serverEncoding']); + } + if ($this->options['password']) { + $connector->setPassword($this->options['password']); + } + if ($this->options['enableSslOnlyMode']) { + $connector->enableSslOnlyMode(); + } + if ($this->options['ipMasks']) { + $connector->setAllowedIpMasks($this->options['ipMasks']); + } + if ($this->options['headersLimit']) { + $connector->setHeadersLimit($this->options['headersLimit']); + } + if ($this->options['detectDumpTraceAndSource']) { + $connector->getDebugDispatcher()->detectTraceAndSource = true; + } + $dumper = $connector->getDumper(); + $dumper->levelLimit = $this->options['dumperLevelLimit']; + $dumper->itemsCountLimit = $this->options['dumperItemsCountLimit']; + $dumper->itemSizeLimit = $this->options['dumperItemSizeLimit']; + $dumper->dumpSizeLimit = $this->options['dumperDumpSizeLimit']; + $dumper->detectCallbacks = $this->options['dumperDetectCallbacks']; + if ($this->options['enableEvalListener']) { + $connector->startEvalRequestsListener(); + } + } + + return $connector; + } + + public function getConnector(): Connector + { + return $this->connector; + } + + /** + * @return array + */ + public function getOptions(): array + { + return $this->options; + } + + public function handle(array $record): bool + { + if ($this->options['enabled'] && $this->connector->isActiveClient()) { + return parent::handle($record); + } + + return !$this->bubble; + } + + /** + * Writes the record down to the log of the implementing handler + */ + protected function write(array $record): void + { + if ($record['level'] < Logger::NOTICE) { + $this->handleDebugRecord($record); + } elseif (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) { + $this->handleExceptionRecord($record); + } else { + $this->handleErrorRecord($record); + } + } + + /** + * @phpstan-param Record $record + */ + private function handleDebugRecord(array $record): void + { + $tags = $this->getRecordTags($record); + $message = $record['message']; + if ($record['context']) { + $message .= ' ' . Utils::jsonEncode($this->connector->getDumper()->dump(array_filter($record['context'])), null, true); + } + $this->connector->getDebugDispatcher()->dispatchDebug($message, $tags, $this->options['classesPartialsTraceIgnore']); + } + + /** + * @phpstan-param Record $record + */ + private function handleExceptionRecord(array $record): void + { + $this->connector->getErrorsDispatcher()->dispatchException($record['context']['exception']); + } + + /** + * @phpstan-param Record $record + */ + private function handleErrorRecord(array $record): void + { + $context = $record['context']; + + $this->connector->getErrorsDispatcher()->dispatchError( + $context['code'] ?? null, + $context['message'] ?? $record['message'], + $context['file'] ?? null, + $context['line'] ?? null, + $this->options['classesPartialsTraceIgnore'] + ); + } + + /** + * @phpstan-param Record $record + * @return string + */ + private function getRecordTags(array &$record) + { + $tags = null; + if (!empty($record['context'])) { + $context = & $record['context']; + foreach ($this->options['debugTagsKeysInContext'] as $key) { + if (!empty($context[$key])) { + $tags = $context[$key]; + if ($key === 0) { + array_shift($context); + } else { + unset($context[$key]); + } + break; + } + } + } + + return $tags ?: strtolower($record['level_name']); + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new LineFormatter('%message%'); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/ProcessHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/ProcessHandler.php new file mode 100644 index 0000000..8a8cf1b --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/ProcessHandler.php @@ -0,0 +1,191 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; + +/** + * Stores to STDIN of any process, specified by a command. + * + * Usage example: + *
              + * $log = new Logger('myLogger');
              + * $log->pushHandler(new ProcessHandler('/usr/bin/php /var/www/monolog/someScript.php'));
              + * 
              + * + * @author Kolja Zuelsdorf + */ +class ProcessHandler extends AbstractProcessingHandler +{ + /** + * Holds the process to receive data on its STDIN. + * + * @var resource|bool|null + */ + private $process; + + /** + * @var string + */ + private $command; + + /** + * @var string|null + */ + private $cwd; + + /** + * @var resource[] + */ + private $pipes = []; + + /** + * @var array + */ + protected const DESCRIPTOR_SPEC = [ + 0 => ['pipe', 'r'], // STDIN is a pipe that the child will read from + 1 => ['pipe', 'w'], // STDOUT is a pipe that the child will write to + 2 => ['pipe', 'w'], // STDERR is a pipe to catch the any errors + ]; + + /** + * @param string $command Command for the process to start. Absolute paths are recommended, + * especially if you do not use the $cwd parameter. + * @param string|null $cwd "Current working directory" (CWD) for the process to be executed in. + * @throws \InvalidArgumentException + */ + public function __construct(string $command, $level = Logger::DEBUG, bool $bubble = true, ?string $cwd = null) + { + if ($command === '') { + throw new \InvalidArgumentException('The command argument must be a non-empty string.'); + } + if ($cwd === '') { + throw new \InvalidArgumentException('The optional CWD argument must be a non-empty string or null.'); + } + + parent::__construct($level, $bubble); + + $this->command = $command; + $this->cwd = $cwd; + } + + /** + * Writes the record down to the log of the implementing handler + * + * @throws \UnexpectedValueException + */ + protected function write(array $record): void + { + $this->ensureProcessIsStarted(); + + $this->writeProcessInput($record['formatted']); + + $errors = $this->readProcessErrors(); + if (empty($errors) === false) { + throw new \UnexpectedValueException(sprintf('Errors while writing to process: %s', $errors)); + } + } + + /** + * Makes sure that the process is actually started, and if not, starts it, + * assigns the stream pipes, and handles startup errors, if any. + */ + private function ensureProcessIsStarted(): void + { + if (is_resource($this->process) === false) { + $this->startProcess(); + + $this->handleStartupErrors(); + } + } + + /** + * Starts the actual process and sets all streams to non-blocking. + */ + private function startProcess(): void + { + $this->process = proc_open($this->command, static::DESCRIPTOR_SPEC, $this->pipes, $this->cwd); + + foreach ($this->pipes as $pipe) { + stream_set_blocking($pipe, false); + } + } + + /** + * Selects the STDERR stream, handles upcoming startup errors, and throws an exception, if any. + * + * @throws \UnexpectedValueException + */ + private function handleStartupErrors(): void + { + $selected = $this->selectErrorStream(); + if (false === $selected) { + throw new \UnexpectedValueException('Something went wrong while selecting a stream.'); + } + + $errors = $this->readProcessErrors(); + + if (is_resource($this->process) === false || empty($errors) === false) { + throw new \UnexpectedValueException( + sprintf('The process "%s" could not be opened: ' . $errors, $this->command) + ); + } + } + + /** + * Selects the STDERR stream. + * + * @return int|bool + */ + protected function selectErrorStream() + { + $empty = []; + $errorPipes = [$this->pipes[2]]; + + return stream_select($errorPipes, $empty, $empty, 1); + } + + /** + * Reads the errors of the process, if there are any. + * + * @codeCoverageIgnore + * @return string Empty string if there are no errors. + */ + protected function readProcessErrors(): string + { + return (string) stream_get_contents($this->pipes[2]); + } + + /** + * Writes to the input stream of the opened process. + * + * @codeCoverageIgnore + */ + protected function writeProcessInput(string $string): void + { + fwrite($this->pipes[0], $string); + } + + /** + * {@inheritDoc} + */ + public function close(): void + { + if (is_resource($this->process)) { + foreach ($this->pipes as $pipe) { + fclose($pipe); + } + proc_close($this->process); + $this->process = null; + } + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerInterface.php b/vendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerInterface.php new file mode 100644 index 0000000..3adec7a --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerInterface.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Processor\ProcessorInterface; + +/** + * Interface to describe loggers that have processors + * + * @author Jordi Boggiano + * + * @phpstan-import-type Record from \Monolog\Logger + */ +interface ProcessableHandlerInterface +{ + /** + * Adds a processor in the stack. + * + * @psalm-param ProcessorInterface|callable(Record): Record $callback + * + * @param ProcessorInterface|callable $callback + * @return HandlerInterface self + */ + public function pushProcessor(callable $callback): HandlerInterface; + + /** + * Removes the processor on top of the stack and returns it. + * + * @psalm-return ProcessorInterface|callable(Record): Record $callback + * + * @throws \LogicException In case the processor stack is empty + * @return callable|ProcessorInterface + */ + public function popProcessor(): callable; +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerTrait.php b/vendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerTrait.php new file mode 100644 index 0000000..9ef6e30 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerTrait.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\ResettableInterface; +use Monolog\Processor\ProcessorInterface; + +/** + * Helper trait for implementing ProcessableInterface + * + * @author Jordi Boggiano + * + * @phpstan-import-type Record from \Monolog\Logger + */ +trait ProcessableHandlerTrait +{ + /** + * @var callable[] + * @phpstan-var array + */ + protected $processors = []; + + /** + * {@inheritDoc} + */ + public function pushProcessor(callable $callback): HandlerInterface + { + array_unshift($this->processors, $callback); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function popProcessor(): callable + { + if (!$this->processors) { + throw new \LogicException('You tried to pop from an empty processor stack.'); + } + + return array_shift($this->processors); + } + + /** + * Processes a record. + * + * @phpstan-param Record $record + * @phpstan-return Record + */ + protected function processRecord(array $record): array + { + foreach ($this->processors as $processor) { + $record = $processor($record); + } + + return $record; + } + + protected function resetProcessors(): void + { + foreach ($this->processors as $processor) { + if ($processor instanceof ResettableInterface) { + $processor->reset(); + } + } + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/PsrHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/PsrHandler.php new file mode 100644 index 0000000..36e19cc --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/PsrHandler.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Psr\Log\LoggerInterface; +use Monolog\Formatter\FormatterInterface; + +/** + * Proxies log messages to an existing PSR-3 compliant logger. + * + * If a formatter is configured, the formatter's output MUST be a string and the + * formatted message will be fed to the wrapped PSR logger instead of the original + * log record's message. + * + * @author Michael Moussa + */ +class PsrHandler extends AbstractHandler implements FormattableHandlerInterface +{ + /** + * PSR-3 compliant logger + * + * @var LoggerInterface + */ + protected $logger; + + /** + * @var FormatterInterface|null + */ + protected $formatter; + + /** + * @param LoggerInterface $logger The underlying PSR-3 compliant logger to which messages will be proxied + */ + public function __construct(LoggerInterface $logger, $level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + + $this->logger = $logger; + } + + /** + * {@inheritDoc} + */ + public function handle(array $record): bool + { + if (!$this->isHandling($record)) { + return false; + } + + if ($this->formatter) { + $formatted = $this->formatter->format($record); + $this->logger->log(strtolower($record['level_name']), (string) $formatted, $record['context']); + } else { + $this->logger->log(strtolower($record['level_name']), $record['message'], $record['context']); + } + + return false === $this->bubble; + } + + /** + * Sets the formatter. + * + * @param FormatterInterface $formatter + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + $this->formatter = $formatter; + + return $this; + } + + /** + * Gets the formatter. + * + * @return FormatterInterface + */ + public function getFormatter(): FormatterInterface + { + if (!$this->formatter) { + throw new \LogicException('No formatter has been set and this handler does not have a default formatter'); + } + + return $this->formatter; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/PushoverHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/PushoverHandler.php new file mode 100644 index 0000000..fed2303 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/PushoverHandler.php @@ -0,0 +1,246 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Utils; +use Psr\Log\LogLevel; + +/** + * Sends notifications through the pushover api to mobile phones + * + * @author Sebastian Göttschkes + * @see https://www.pushover.net/api + * + * @phpstan-import-type FormattedRecord from AbstractProcessingHandler + * @phpstan-import-type Level from \Monolog\Logger + * @phpstan-import-type LevelName from \Monolog\Logger + */ +class PushoverHandler extends SocketHandler +{ + /** @var string */ + private $token; + /** @var array */ + private $users; + /** @var string */ + private $title; + /** @var string|int|null */ + private $user = null; + /** @var int */ + private $retry; + /** @var int */ + private $expire; + + /** @var int */ + private $highPriorityLevel; + /** @var int */ + private $emergencyLevel; + /** @var bool */ + private $useFormattedMessage = false; + + /** + * All parameters that can be sent to Pushover + * @see https://pushover.net/api + * @var array + */ + private $parameterNames = [ + 'token' => true, + 'user' => true, + 'message' => true, + 'device' => true, + 'title' => true, + 'url' => true, + 'url_title' => true, + 'priority' => true, + 'timestamp' => true, + 'sound' => true, + 'retry' => true, + 'expire' => true, + 'callback' => true, + ]; + + /** + * Sounds the api supports by default + * @see https://pushover.net/api#sounds + * @var string[] + */ + private $sounds = [ + 'pushover', 'bike', 'bugle', 'cashregister', 'classical', 'cosmic', 'falling', 'gamelan', 'incoming', + 'intermission', 'magic', 'mechanical', 'pianobar', 'siren', 'spacealarm', 'tugboat', 'alien', 'climb', + 'persistent', 'echo', 'updown', 'none', + ]; + + /** + * @param string $token Pushover api token + * @param string|array $users Pushover user id or array of ids the message will be sent to + * @param string|null $title Title sent to the Pushover API + * @param bool $useSSL Whether to connect via SSL. Required when pushing messages to users that are not + * the pushover.net app owner. OpenSSL is required for this option. + * @param string|int $highPriorityLevel The minimum logging level at which this handler will start + * sending "high priority" requests to the Pushover API + * @param string|int $emergencyLevel The minimum logging level at which this handler will start + * sending "emergency" requests to the Pushover API + * @param int $retry The retry parameter specifies how often (in seconds) the Pushover servers will + * send the same notification to the user. + * @param int $expire The expire parameter specifies how many seconds your notification will continue + * to be retried for (every retry seconds). + * + * @phpstan-param string|array $users + * @phpstan-param Level|LevelName|LogLevel::* $highPriorityLevel + * @phpstan-param Level|LevelName|LogLevel::* $emergencyLevel + */ + public function __construct( + string $token, + $users, + ?string $title = null, + $level = Logger::CRITICAL, + bool $bubble = true, + bool $useSSL = true, + $highPriorityLevel = Logger::CRITICAL, + $emergencyLevel = Logger::EMERGENCY, + int $retry = 30, + int $expire = 25200, + bool $persistent = false, + float $timeout = 0.0, + float $writingTimeout = 10.0, + ?float $connectionTimeout = null, + ?int $chunkSize = null + ) { + $connectionString = $useSSL ? 'ssl://api.pushover.net:443' : 'api.pushover.net:80'; + parent::__construct( + $connectionString, + $level, + $bubble, + $persistent, + $timeout, + $writingTimeout, + $connectionTimeout, + $chunkSize + ); + + $this->token = $token; + $this->users = (array) $users; + $this->title = $title ?: (string) gethostname(); + $this->highPriorityLevel = Logger::toMonologLevel($highPriorityLevel); + $this->emergencyLevel = Logger::toMonologLevel($emergencyLevel); + $this->retry = $retry; + $this->expire = $expire; + } + + protected function generateDataStream(array $record): string + { + $content = $this->buildContent($record); + + return $this->buildHeader($content) . $content; + } + + /** + * @phpstan-param FormattedRecord $record + */ + private function buildContent(array $record): string + { + // Pushover has a limit of 512 characters on title and message combined. + $maxMessageLength = 512 - strlen($this->title); + + $message = ($this->useFormattedMessage) ? $record['formatted'] : $record['message']; + $message = Utils::substr($message, 0, $maxMessageLength); + + $timestamp = $record['datetime']->getTimestamp(); + + $dataArray = [ + 'token' => $this->token, + 'user' => $this->user, + 'message' => $message, + 'title' => $this->title, + 'timestamp' => $timestamp, + ]; + + if (isset($record['level']) && $record['level'] >= $this->emergencyLevel) { + $dataArray['priority'] = 2; + $dataArray['retry'] = $this->retry; + $dataArray['expire'] = $this->expire; + } elseif (isset($record['level']) && $record['level'] >= $this->highPriorityLevel) { + $dataArray['priority'] = 1; + } + + // First determine the available parameters + $context = array_intersect_key($record['context'], $this->parameterNames); + $extra = array_intersect_key($record['extra'], $this->parameterNames); + + // Least important info should be merged with subsequent info + $dataArray = array_merge($extra, $context, $dataArray); + + // Only pass sounds that are supported by the API + if (isset($dataArray['sound']) && !in_array($dataArray['sound'], $this->sounds)) { + unset($dataArray['sound']); + } + + return http_build_query($dataArray); + } + + private function buildHeader(string $content): string + { + $header = "POST /1/messages.json HTTP/1.1\r\n"; + $header .= "Host: api.pushover.net\r\n"; + $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; + $header .= "Content-Length: " . strlen($content) . "\r\n"; + $header .= "\r\n"; + + return $header; + } + + protected function write(array $record): void + { + foreach ($this->users as $user) { + $this->user = $user; + + parent::write($record); + $this->closeSocket(); + } + + $this->user = null; + } + + /** + * @param int|string $value + * + * @phpstan-param Level|LevelName|LogLevel::* $value + */ + public function setHighPriorityLevel($value): self + { + $this->highPriorityLevel = Logger::toMonologLevel($value); + + return $this; + } + + /** + * @param int|string $value + * + * @phpstan-param Level|LevelName|LogLevel::* $value + */ + public function setEmergencyLevel($value): self + { + $this->emergencyLevel = Logger::toMonologLevel($value); + + return $this; + } + + /** + * Use the formatted message? + */ + public function useFormattedMessage(bool $value): self + { + $this->useFormattedMessage = $value; + + return $this; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/RedisHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/RedisHandler.php new file mode 100644 index 0000000..91d16ea --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/RedisHandler.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\LineFormatter; +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; + +/** + * Logs to a Redis key using rpush + * + * usage example: + * + * $log = new Logger('application'); + * $redis = new RedisHandler(new Predis\Client("tcp://localhost:6379"), "logs", "prod"); + * $log->pushHandler($redis); + * + * @author Thomas Tourlourat + * + * @phpstan-import-type FormattedRecord from AbstractProcessingHandler + */ +class RedisHandler extends AbstractProcessingHandler +{ + /** @var \Predis\Client<\Predis\Client>|\Redis */ + private $redisClient; + /** @var string */ + private $redisKey; + /** @var int */ + protected $capSize; + + /** + * @param \Predis\Client<\Predis\Client>|\Redis $redis The redis instance + * @param string $key The key name to push records to + * @param int $capSize Number of entries to limit list size to, 0 = unlimited + */ + public function __construct($redis, string $key, $level = Logger::DEBUG, bool $bubble = true, int $capSize = 0) + { + if (!(($redis instanceof \Predis\Client) || ($redis instanceof \Redis))) { + throw new \InvalidArgumentException('Predis\Client or Redis instance required'); + } + + $this->redisClient = $redis; + $this->redisKey = $key; + $this->capSize = $capSize; + + parent::__construct($level, $bubble); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + if ($this->capSize) { + $this->writeCapped($record); + } else { + $this->redisClient->rpush($this->redisKey, $record["formatted"]); + } + } + + /** + * Write and cap the collection + * Writes the record to the redis list and caps its + * + * @phpstan-param FormattedRecord $record + */ + protected function writeCapped(array $record): void + { + if ($this->redisClient instanceof \Redis) { + $mode = defined('\Redis::MULTI') ? \Redis::MULTI : 1; + $this->redisClient->multi($mode) + ->rpush($this->redisKey, $record["formatted"]) + ->ltrim($this->redisKey, -$this->capSize, -1) + ->exec(); + } else { + $redisKey = $this->redisKey; + $capSize = $this->capSize; + $this->redisClient->transaction(function ($tx) use ($record, $redisKey, $capSize) { + $tx->rpush($redisKey, $record["formatted"]); + $tx->ltrim($redisKey, -$capSize, -1); + }); + } + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new LineFormatter(); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/RedisPubSubHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/RedisPubSubHandler.php new file mode 100644 index 0000000..7789309 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/RedisPubSubHandler.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\LineFormatter; +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; + +/** + * Sends the message to a Redis Pub/Sub channel using PUBLISH + * + * usage example: + * + * $log = new Logger('application'); + * $redis = new RedisPubSubHandler(new Predis\Client("tcp://localhost:6379"), "logs", Logger::WARNING); + * $log->pushHandler($redis); + * + * @author Gaëtan Faugère + */ +class RedisPubSubHandler extends AbstractProcessingHandler +{ + /** @var \Predis\Client<\Predis\Client>|\Redis */ + private $redisClient; + /** @var string */ + private $channelKey; + + /** + * @param \Predis\Client<\Predis\Client>|\Redis $redis The redis instance + * @param string $key The channel key to publish records to + */ + public function __construct($redis, string $key, $level = Logger::DEBUG, bool $bubble = true) + { + if (!(($redis instanceof \Predis\Client) || ($redis instanceof \Redis))) { + throw new \InvalidArgumentException('Predis\Client or Redis instance required'); + } + + $this->redisClient = $redis; + $this->channelKey = $key; + + parent::__construct($level, $bubble); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $this->redisClient->publish($this->channelKey, $record["formatted"]); + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new LineFormatter(); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/RollbarHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/RollbarHandler.php new file mode 100644 index 0000000..adcc939 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/RollbarHandler.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Rollbar\RollbarLogger; +use Throwable; +use Monolog\Logger; + +/** + * Sends errors to Rollbar + * + * If the context data contains a `payload` key, that is used as an array + * of payload options to RollbarLogger's log method. + * + * Rollbar's context info will contain the context + extra keys from the log record + * merged, and then on top of that a few keys: + * + * - level (rollbar level name) + * - monolog_level (monolog level name, raw level, as rollbar only has 5 but monolog 8) + * - channel + * - datetime (unix timestamp) + * + * @author Paul Statezny + */ +class RollbarHandler extends AbstractProcessingHandler +{ + /** + * @var RollbarLogger + */ + protected $rollbarLogger; + + /** @var string[] */ + protected $levelMap = [ + Logger::DEBUG => 'debug', + Logger::INFO => 'info', + Logger::NOTICE => 'info', + Logger::WARNING => 'warning', + Logger::ERROR => 'error', + Logger::CRITICAL => 'critical', + Logger::ALERT => 'critical', + Logger::EMERGENCY => 'critical', + ]; + + /** + * Records whether any log records have been added since the last flush of the rollbar notifier + * + * @var bool + */ + private $hasRecords = false; + + /** @var bool */ + protected $initialized = false; + + /** + * @param RollbarLogger $rollbarLogger RollbarLogger object constructed with valid token + */ + public function __construct(RollbarLogger $rollbarLogger, $level = Logger::ERROR, bool $bubble = true) + { + $this->rollbarLogger = $rollbarLogger; + + parent::__construct($level, $bubble); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + if (!$this->initialized) { + // __destructor() doesn't get called on Fatal errors + register_shutdown_function(array($this, 'close')); + $this->initialized = true; + } + + $context = $record['context']; + $context = array_merge($context, $record['extra'], [ + 'level' => $this->levelMap[$record['level']], + 'monolog_level' => $record['level_name'], + 'channel' => $record['channel'], + 'datetime' => $record['datetime']->format('U'), + ]); + + if (isset($context['exception']) && $context['exception'] instanceof Throwable) { + $exception = $context['exception']; + unset($context['exception']); + $toLog = $exception; + } else { + $toLog = $record['message']; + } + + // @phpstan-ignore-next-line + $this->rollbarLogger->log($context['level'], $toLog, $context); + + $this->hasRecords = true; + } + + public function flush(): void + { + if ($this->hasRecords) { + $this->rollbarLogger->flush(); + $this->hasRecords = false; + } + } + + /** + * {@inheritDoc} + */ + public function close(): void + { + $this->flush(); + } + + /** + * {@inheritDoc} + */ + public function reset() + { + $this->flush(); + + parent::reset(); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php new file mode 100644 index 0000000..17745d2 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php @@ -0,0 +1,207 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use InvalidArgumentException; +use Monolog\Logger; +use Monolog\Utils; + +/** + * Stores logs to files that are rotated every day and a limited number of files are kept. + * + * This rotation is only intended to be used as a workaround. Using logrotate to + * handle the rotation is strongly encouraged when you can use it. + * + * @author Christophe Coevoet + * @author Jordi Boggiano + */ +class RotatingFileHandler extends StreamHandler +{ + public const FILE_PER_DAY = 'Y-m-d'; + public const FILE_PER_MONTH = 'Y-m'; + public const FILE_PER_YEAR = 'Y'; + + /** @var string */ + protected $filename; + /** @var int */ + protected $maxFiles; + /** @var bool */ + protected $mustRotate; + /** @var \DateTimeImmutable */ + protected $nextRotation; + /** @var string */ + protected $filenameFormat; + /** @var string */ + protected $dateFormat; + + /** + * @param string $filename + * @param int $maxFiles The maximal amount of files to keep (0 means unlimited) + * @param int|null $filePermission Optional file permissions (default (0644) are only for owner read/write) + * @param bool $useLocking Try to lock log file before doing any writes + */ + public function __construct(string $filename, int $maxFiles = 0, $level = Logger::DEBUG, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false) + { + $this->filename = Utils::canonicalizePath($filename); + $this->maxFiles = $maxFiles; + $this->nextRotation = new \DateTimeImmutable('tomorrow'); + $this->filenameFormat = '{filename}-{date}'; + $this->dateFormat = static::FILE_PER_DAY; + + parent::__construct($this->getTimedFilename(), $level, $bubble, $filePermission, $useLocking); + } + + /** + * {@inheritDoc} + */ + public function close(): void + { + parent::close(); + + if (true === $this->mustRotate) { + $this->rotate(); + } + } + + /** + * {@inheritDoc} + */ + public function reset() + { + parent::reset(); + + if (true === $this->mustRotate) { + $this->rotate(); + } + } + + public function setFilenameFormat(string $filenameFormat, string $dateFormat): self + { + if (!preg_match('{^[Yy](([/_.-]?m)([/_.-]?d)?)?$}', $dateFormat)) { + throw new InvalidArgumentException( + 'Invalid date format - format must be one of '. + 'RotatingFileHandler::FILE_PER_DAY ("Y-m-d"), RotatingFileHandler::FILE_PER_MONTH ("Y-m") '. + 'or RotatingFileHandler::FILE_PER_YEAR ("Y"), or you can set one of the '. + 'date formats using slashes, underscores and/or dots instead of dashes.' + ); + } + if (substr_count($filenameFormat, '{date}') === 0) { + throw new InvalidArgumentException( + 'Invalid filename format - format must contain at least `{date}`, because otherwise rotating is impossible.' + ); + } + $this->filenameFormat = $filenameFormat; + $this->dateFormat = $dateFormat; + $this->url = $this->getTimedFilename(); + $this->close(); + + return $this; + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + // on the first record written, if the log is new, we should rotate (once per day) + if (null === $this->mustRotate) { + $this->mustRotate = null === $this->url || !file_exists($this->url); + } + + if ($this->nextRotation <= $record['datetime']) { + $this->mustRotate = true; + $this->close(); + } + + parent::write($record); + } + + /** + * Rotates the files. + */ + protected function rotate(): void + { + // update filename + $this->url = $this->getTimedFilename(); + $this->nextRotation = new \DateTimeImmutable('tomorrow'); + + // skip GC of old logs if files are unlimited + if (0 === $this->maxFiles) { + return; + } + + $logFiles = glob($this->getGlobPattern()); + if (false === $logFiles) { + // failed to glob + return; + } + + if ($this->maxFiles >= count($logFiles)) { + // no files to remove + return; + } + + // Sorting the files by name to remove the older ones + usort($logFiles, function ($a, $b) { + return strcmp($b, $a); + }); + + foreach (array_slice($logFiles, $this->maxFiles) as $file) { + if (is_writable($file)) { + // suppress errors here as unlink() might fail if two processes + // are cleaning up/rotating at the same time + set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline): bool { + return false; + }); + unlink($file); + restore_error_handler(); + } + } + + $this->mustRotate = false; + } + + protected function getTimedFilename(): string + { + $fileInfo = pathinfo($this->filename); + $timedFilename = str_replace( + ['{filename}', '{date}'], + [$fileInfo['filename'], date($this->dateFormat)], + $fileInfo['dirname'] . '/' . $this->filenameFormat + ); + + if (isset($fileInfo['extension'])) { + $timedFilename .= '.'.$fileInfo['extension']; + } + + return $timedFilename; + } + + protected function getGlobPattern(): string + { + $fileInfo = pathinfo($this->filename); + $glob = str_replace( + ['{filename}', '{date}'], + [$fileInfo['filename'], str_replace( + ['Y', 'y', 'm', 'd'], + ['[0-9][0-9][0-9][0-9]', '[0-9][0-9]', '[0-9][0-9]', '[0-9][0-9]'], + $this->dateFormat) + ], + $fileInfo['dirname'] . '/' . $this->filenameFormat + ); + if (isset($fileInfo['extension'])) { + $glob .= '.'.$fileInfo['extension']; + } + + return $glob; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/SamplingHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/SamplingHandler.php new file mode 100644 index 0000000..25cce07 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/SamplingHandler.php @@ -0,0 +1,132 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; + +/** + * Sampling handler + * + * A sampled event stream can be useful for logging high frequency events in + * a production environment where you only need an idea of what is happening + * and are not concerned with capturing every occurrence. Since the decision to + * handle or not handle a particular event is determined randomly, the + * resulting sampled log is not guaranteed to contain 1/N of the events that + * occurred in the application, but based on the Law of large numbers, it will + * tend to be close to this ratio with a large number of attempts. + * + * @author Bryan Davis + * @author Kunal Mehta + * + * @phpstan-import-type Record from \Monolog\Logger + * @phpstan-import-type Level from \Monolog\Logger + */ +class SamplingHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface +{ + use ProcessableHandlerTrait; + + /** + * @var HandlerInterface|callable + * @phpstan-var HandlerInterface|callable(Record|array{level: Level}|null, HandlerInterface): HandlerInterface + */ + protected $handler; + + /** + * @var int $factor + */ + protected $factor; + + /** + * @psalm-param HandlerInterface|callable(Record|array{level: Level}|null, HandlerInterface): HandlerInterface $handler + * + * @param callable|HandlerInterface $handler Handler or factory callable($record|null, $samplingHandler). + * @param int $factor Sample factor (e.g. 10 means every ~10th record is sampled) + */ + public function __construct($handler, int $factor) + { + parent::__construct(); + $this->handler = $handler; + $this->factor = $factor; + + if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) { + throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object"); + } + } + + public function isHandling(array $record): bool + { + return $this->getHandler($record)->isHandling($record); + } + + public function handle(array $record): bool + { + if ($this->isHandling($record) && mt_rand(1, $this->factor) === 1) { + if ($this->processors) { + /** @var Record $record */ + $record = $this->processRecord($record); + } + + $this->getHandler($record)->handle($record); + } + + return false === $this->bubble; + } + + /** + * Return the nested handler + * + * If the handler was provided as a factory callable, this will trigger the handler's instantiation. + * + * @phpstan-param Record|array{level: Level}|null $record + * + * @return HandlerInterface + */ + public function getHandler(?array $record = null) + { + if (!$this->handler instanceof HandlerInterface) { + $this->handler = ($this->handler)($record, $this); + if (!$this->handler instanceof HandlerInterface) { + throw new \RuntimeException("The factory callable should return a HandlerInterface"); + } + } + + return $this->handler; + } + + /** + * {@inheritDoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + $handler->setFormatter($formatter); + + return $this; + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); + } + + /** + * {@inheritDoc} + */ + public function getFormatter(): FormatterInterface + { + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + return $handler->getFormatter(); + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/SendGridHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/SendGridHandler.php new file mode 100644 index 0000000..1280ee7 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/SendGridHandler.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; + +/** + * SendGridrHandler uses the SendGrid API v2 function to send Log emails, more information in https://sendgrid.com/docs/API_Reference/Web_API/mail.html + * + * @author Ricardo Fontanelli + */ +class SendGridHandler extends MailHandler +{ + /** + * The SendGrid API User + * @var string + */ + protected $apiUser; + + /** + * The SendGrid API Key + * @var string + */ + protected $apiKey; + + /** + * The email addresses to which the message will be sent + * @var string + */ + protected $from; + + /** + * The email addresses to which the message will be sent + * @var string[] + */ + protected $to; + + /** + * The subject of the email + * @var string + */ + protected $subject; + + /** + * @param string $apiUser The SendGrid API User + * @param string $apiKey The SendGrid API Key + * @param string $from The sender of the email + * @param string|string[] $to The recipients of the email + * @param string $subject The subject of the mail + */ + public function __construct(string $apiUser, string $apiKey, string $from, $to, string $subject, $level = Logger::ERROR, bool $bubble = true) + { + if (!extension_loaded('curl')) { + throw new MissingExtensionException('The curl extension is needed to use the SendGridHandler'); + } + + parent::__construct($level, $bubble); + $this->apiUser = $apiUser; + $this->apiKey = $apiKey; + $this->from = $from; + $this->to = (array) $to; + $this->subject = $subject; + } + + /** + * {@inheritDoc} + */ + protected function send(string $content, array $records): void + { + $message = []; + $message['api_user'] = $this->apiUser; + $message['api_key'] = $this->apiKey; + $message['from'] = $this->from; + foreach ($this->to as $recipient) { + $message['to[]'] = $recipient; + } + $message['subject'] = $this->subject; + $message['date'] = date('r'); + + if ($this->isHtmlBody($content)) { + $message['html'] = $content; + } else { + $message['text'] = $content; + } + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, 'https://api.sendgrid.com/api/mail.send.json'); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($message)); + Curl\Util::execute($ch, 2); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/Slack/SlackRecord.php b/vendor/monolog/monolog/src/Monolog/Handler/Slack/SlackRecord.php new file mode 100644 index 0000000..9ae1003 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/Slack/SlackRecord.php @@ -0,0 +1,387 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler\Slack; + +use Monolog\Logger; +use Monolog\Utils; +use Monolog\Formatter\NormalizerFormatter; +use Monolog\Formatter\FormatterInterface; + +/** + * Slack record utility helping to log to Slack webhooks or API. + * + * @author Greg Kedzierski + * @author Haralan Dobrev + * @see https://api.slack.com/incoming-webhooks + * @see https://api.slack.com/docs/message-attachments + * + * @phpstan-import-type FormattedRecord from \Monolog\Handler\AbstractProcessingHandler + * @phpstan-import-type Record from \Monolog\Logger + */ +class SlackRecord +{ + public const COLOR_DANGER = 'danger'; + + public const COLOR_WARNING = 'warning'; + + public const COLOR_GOOD = 'good'; + + public const COLOR_DEFAULT = '#e3e4e6'; + + /** + * Slack channel (encoded ID or name) + * @var string|null + */ + private $channel; + + /** + * Name of a bot + * @var string|null + */ + private $username; + + /** + * User icon e.g. 'ghost', 'http://example.com/user.png' + * @var string|null + */ + private $userIcon; + + /** + * Whether the message should be added to Slack as attachment (plain text otherwise) + * @var bool + */ + private $useAttachment; + + /** + * Whether the the context/extra messages added to Slack as attachments are in a short style + * @var bool + */ + private $useShortAttachment; + + /** + * Whether the attachment should include context and extra data + * @var bool + */ + private $includeContextAndExtra; + + /** + * Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2'] + * @var string[] + */ + private $excludeFields; + + /** + * @var ?FormatterInterface + */ + private $formatter; + + /** + * @var NormalizerFormatter + */ + private $normalizerFormatter; + + /** + * @param string[] $excludeFields + */ + public function __construct( + ?string $channel = null, + ?string $username = null, + bool $useAttachment = true, + ?string $userIcon = null, + bool $useShortAttachment = false, + bool $includeContextAndExtra = false, + array $excludeFields = array(), + ?FormatterInterface $formatter = null + ) { + $this + ->setChannel($channel) + ->setUsername($username) + ->useAttachment($useAttachment) + ->setUserIcon($userIcon) + ->useShortAttachment($useShortAttachment) + ->includeContextAndExtra($includeContextAndExtra) + ->excludeFields($excludeFields) + ->setFormatter($formatter); + + if ($this->includeContextAndExtra) { + $this->normalizerFormatter = new NormalizerFormatter(); + } + } + + /** + * Returns required data in format that Slack + * is expecting. + * + * @phpstan-param FormattedRecord $record + * @phpstan-return mixed[] + */ + public function getSlackData(array $record): array + { + $dataArray = array(); + $record = $this->removeExcludedFields($record); + + if ($this->username) { + $dataArray['username'] = $this->username; + } + + if ($this->channel) { + $dataArray['channel'] = $this->channel; + } + + if ($this->formatter && !$this->useAttachment) { + /** @phpstan-ignore-next-line */ + $message = $this->formatter->format($record); + } else { + $message = $record['message']; + } + + if ($this->useAttachment) { + $attachment = array( + 'fallback' => $message, + 'text' => $message, + 'color' => $this->getAttachmentColor($record['level']), + 'fields' => array(), + 'mrkdwn_in' => array('fields'), + 'ts' => $record['datetime']->getTimestamp(), + 'footer' => $this->username, + 'footer_icon' => $this->userIcon, + ); + + if ($this->useShortAttachment) { + $attachment['title'] = $record['level_name']; + } else { + $attachment['title'] = 'Message'; + $attachment['fields'][] = $this->generateAttachmentField('Level', $record['level_name']); + } + + if ($this->includeContextAndExtra) { + foreach (array('extra', 'context') as $key) { + if (empty($record[$key])) { + continue; + } + + if ($this->useShortAttachment) { + $attachment['fields'][] = $this->generateAttachmentField( + (string) $key, + $record[$key] + ); + } else { + // Add all extra fields as individual fields in attachment + $attachment['fields'] = array_merge( + $attachment['fields'], + $this->generateAttachmentFields($record[$key]) + ); + } + } + } + + $dataArray['attachments'] = array($attachment); + } else { + $dataArray['text'] = $message; + } + + if ($this->userIcon) { + if (filter_var($this->userIcon, FILTER_VALIDATE_URL)) { + $dataArray['icon_url'] = $this->userIcon; + } else { + $dataArray['icon_emoji'] = ":{$this->userIcon}:"; + } + } + + return $dataArray; + } + + /** + * Returns a Slack message attachment color associated with + * provided level. + */ + public function getAttachmentColor(int $level): string + { + switch (true) { + case $level >= Logger::ERROR: + return static::COLOR_DANGER; + case $level >= Logger::WARNING: + return static::COLOR_WARNING; + case $level >= Logger::INFO: + return static::COLOR_GOOD; + default: + return static::COLOR_DEFAULT; + } + } + + /** + * Stringifies an array of key/value pairs to be used in attachment fields + * + * @param mixed[] $fields + */ + public function stringify(array $fields): string + { + /** @var Record $fields */ + $normalized = $this->normalizerFormatter->format($fields); + + $hasSecondDimension = count(array_filter($normalized, 'is_array')); + $hasNonNumericKeys = !count(array_filter(array_keys($normalized), 'is_numeric')); + + return $hasSecondDimension || $hasNonNumericKeys + ? Utils::jsonEncode($normalized, JSON_PRETTY_PRINT|Utils::DEFAULT_JSON_FLAGS) + : Utils::jsonEncode($normalized, Utils::DEFAULT_JSON_FLAGS); + } + + /** + * Channel used by the bot when posting + * + * @param ?string $channel + * + * @return static + */ + public function setChannel(?string $channel = null): self + { + $this->channel = $channel; + + return $this; + } + + /** + * Username used by the bot when posting + * + * @param ?string $username + * + * @return static + */ + public function setUsername(?string $username = null): self + { + $this->username = $username; + + return $this; + } + + public function useAttachment(bool $useAttachment = true): self + { + $this->useAttachment = $useAttachment; + + return $this; + } + + public function setUserIcon(?string $userIcon = null): self + { + $this->userIcon = $userIcon; + + if (\is_string($userIcon)) { + $this->userIcon = trim($userIcon, ':'); + } + + return $this; + } + + public function useShortAttachment(bool $useShortAttachment = false): self + { + $this->useShortAttachment = $useShortAttachment; + + return $this; + } + + public function includeContextAndExtra(bool $includeContextAndExtra = false): self + { + $this->includeContextAndExtra = $includeContextAndExtra; + + if ($this->includeContextAndExtra) { + $this->normalizerFormatter = new NormalizerFormatter(); + } + + return $this; + } + + /** + * @param string[] $excludeFields + */ + public function excludeFields(array $excludeFields = []): self + { + $this->excludeFields = $excludeFields; + + return $this; + } + + public function setFormatter(?FormatterInterface $formatter = null): self + { + $this->formatter = $formatter; + + return $this; + } + + /** + * Generates attachment field + * + * @param string|mixed[] $value + * + * @return array{title: string, value: string, short: false} + */ + private function generateAttachmentField(string $title, $value): array + { + $value = is_array($value) + ? sprintf('```%s```', substr($this->stringify($value), 0, 1990)) + : $value; + + return array( + 'title' => ucfirst($title), + 'value' => $value, + 'short' => false, + ); + } + + /** + * Generates a collection of attachment fields from array + * + * @param mixed[] $data + * + * @return array + */ + private function generateAttachmentFields(array $data): array + { + /** @var Record $data */ + $normalized = $this->normalizerFormatter->format($data); + + $fields = array(); + foreach ($normalized as $key => $value) { + $fields[] = $this->generateAttachmentField((string) $key, $value); + } + + return $fields; + } + + /** + * Get a copy of record with fields excluded according to $this->excludeFields + * + * @phpstan-param FormattedRecord $record + * + * @return mixed[] + */ + private function removeExcludedFields(array $record): array + { + foreach ($this->excludeFields as $field) { + $keys = explode('.', $field); + $node = &$record; + $lastKey = end($keys); + foreach ($keys as $key) { + if (!isset($node[$key])) { + break; + } + if ($lastKey === $key) { + unset($node[$key]); + break; + } + $node = &$node[$key]; + } + } + + return $record; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/SlackHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/SlackHandler.php new file mode 100644 index 0000000..a648513 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/SlackHandler.php @@ -0,0 +1,256 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; +use Monolog\Utils; +use Monolog\Handler\Slack\SlackRecord; + +/** + * Sends notifications through Slack API + * + * @author Greg Kedzierski + * @see https://api.slack.com/ + * + * @phpstan-import-type FormattedRecord from AbstractProcessingHandler + */ +class SlackHandler extends SocketHandler +{ + /** + * Slack API token + * @var string + */ + private $token; + + /** + * Instance of the SlackRecord util class preparing data for Slack API. + * @var SlackRecord + */ + private $slackRecord; + + /** + * @param string $token Slack API token + * @param string $channel Slack channel (encoded ID or name) + * @param string|null $username Name of a bot + * @param bool $useAttachment Whether the message should be added to Slack as attachment (plain text otherwise) + * @param string|null $iconEmoji The emoji name to use (or null) + * @param bool $useShortAttachment Whether the context/extra messages added to Slack as attachments are in a short style + * @param bool $includeContextAndExtra Whether the attachment should include context and extra data + * @param string[] $excludeFields Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2'] + * @throws MissingExtensionException If no OpenSSL PHP extension configured + */ + public function __construct( + string $token, + string $channel, + ?string $username = null, + bool $useAttachment = true, + ?string $iconEmoji = null, + $level = Logger::CRITICAL, + bool $bubble = true, + bool $useShortAttachment = false, + bool $includeContextAndExtra = false, + array $excludeFields = array(), + bool $persistent = false, + float $timeout = 0.0, + float $writingTimeout = 10.0, + ?float $connectionTimeout = null, + ?int $chunkSize = null + ) { + if (!extension_loaded('openssl')) { + throw new MissingExtensionException('The OpenSSL PHP extension is required to use the SlackHandler'); + } + + parent::__construct( + 'ssl://slack.com:443', + $level, + $bubble, + $persistent, + $timeout, + $writingTimeout, + $connectionTimeout, + $chunkSize + ); + + $this->slackRecord = new SlackRecord( + $channel, + $username, + $useAttachment, + $iconEmoji, + $useShortAttachment, + $includeContextAndExtra, + $excludeFields + ); + + $this->token = $token; + } + + public function getSlackRecord(): SlackRecord + { + return $this->slackRecord; + } + + public function getToken(): string + { + return $this->token; + } + + /** + * {@inheritDoc} + */ + protected function generateDataStream(array $record): string + { + $content = $this->buildContent($record); + + return $this->buildHeader($content) . $content; + } + + /** + * Builds the body of API call + * + * @phpstan-param FormattedRecord $record + */ + private function buildContent(array $record): string + { + $dataArray = $this->prepareContentData($record); + + return http_build_query($dataArray); + } + + /** + * @phpstan-param FormattedRecord $record + * @return string[] + */ + protected function prepareContentData(array $record): array + { + $dataArray = $this->slackRecord->getSlackData($record); + $dataArray['token'] = $this->token; + + if (!empty($dataArray['attachments'])) { + $dataArray['attachments'] = Utils::jsonEncode($dataArray['attachments']); + } + + return $dataArray; + } + + /** + * Builds the header of the API Call + */ + private function buildHeader(string $content): string + { + $header = "POST /api/chat.postMessage HTTP/1.1\r\n"; + $header .= "Host: slack.com\r\n"; + $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; + $header .= "Content-Length: " . strlen($content) . "\r\n"; + $header .= "\r\n"; + + return $header; + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + parent::write($record); + $this->finalizeWrite(); + } + + /** + * Finalizes the request by reading some bytes and then closing the socket + * + * If we do not read some but close the socket too early, slack sometimes + * drops the request entirely. + */ + protected function finalizeWrite(): void + { + $res = $this->getResource(); + if (is_resource($res)) { + @fread($res, 2048); + } + $this->closeSocket(); + } + + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + parent::setFormatter($formatter); + $this->slackRecord->setFormatter($formatter); + + return $this; + } + + public function getFormatter(): FormatterInterface + { + $formatter = parent::getFormatter(); + $this->slackRecord->setFormatter($formatter); + + return $formatter; + } + + /** + * Channel used by the bot when posting + */ + public function setChannel(string $channel): self + { + $this->slackRecord->setChannel($channel); + + return $this; + } + + /** + * Username used by the bot when posting + */ + public function setUsername(string $username): self + { + $this->slackRecord->setUsername($username); + + return $this; + } + + public function useAttachment(bool $useAttachment): self + { + $this->slackRecord->useAttachment($useAttachment); + + return $this; + } + + public function setIconEmoji(string $iconEmoji): self + { + $this->slackRecord->setUserIcon($iconEmoji); + + return $this; + } + + public function useShortAttachment(bool $useShortAttachment): self + { + $this->slackRecord->useShortAttachment($useShortAttachment); + + return $this; + } + + public function includeContextAndExtra(bool $includeContextAndExtra): self + { + $this->slackRecord->includeContextAndExtra($includeContextAndExtra); + + return $this; + } + + /** + * @param string[] $excludeFields + */ + public function excludeFields(array $excludeFields): self + { + $this->slackRecord->excludeFields($excludeFields); + + return $this; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/SlackWebhookHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/SlackWebhookHandler.php new file mode 100644 index 0000000..8ae3c78 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/SlackWebhookHandler.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; +use Monolog\Utils; +use Monolog\Handler\Slack\SlackRecord; + +/** + * Sends notifications through Slack Webhooks + * + * @author Haralan Dobrev + * @see https://api.slack.com/incoming-webhooks + */ +class SlackWebhookHandler extends AbstractProcessingHandler +{ + /** + * Slack Webhook token + * @var string + */ + private $webhookUrl; + + /** + * Instance of the SlackRecord util class preparing data for Slack API. + * @var SlackRecord + */ + private $slackRecord; + + /** + * @param string $webhookUrl Slack Webhook URL + * @param string|null $channel Slack channel (encoded ID or name) + * @param string|null $username Name of a bot + * @param bool $useAttachment Whether the message should be added to Slack as attachment (plain text otherwise) + * @param string|null $iconEmoji The emoji name to use (or null) + * @param bool $useShortAttachment Whether the the context/extra messages added to Slack as attachments are in a short style + * @param bool $includeContextAndExtra Whether the attachment should include context and extra data + * @param string[] $excludeFields Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2'] + */ + public function __construct( + string $webhookUrl, + ?string $channel = null, + ?string $username = null, + bool $useAttachment = true, + ?string $iconEmoji = null, + bool $useShortAttachment = false, + bool $includeContextAndExtra = false, + $level = Logger::CRITICAL, + bool $bubble = true, + array $excludeFields = array() + ) { + if (!extension_loaded('curl')) { + throw new MissingExtensionException('The curl extension is needed to use the SlackWebhookHandler'); + } + + parent::__construct($level, $bubble); + + $this->webhookUrl = $webhookUrl; + + $this->slackRecord = new SlackRecord( + $channel, + $username, + $useAttachment, + $iconEmoji, + $useShortAttachment, + $includeContextAndExtra, + $excludeFields + ); + } + + public function getSlackRecord(): SlackRecord + { + return $this->slackRecord; + } + + public function getWebhookUrl(): string + { + return $this->webhookUrl; + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $postData = $this->slackRecord->getSlackData($record); + $postString = Utils::jsonEncode($postData); + + $ch = curl_init(); + $options = array( + CURLOPT_URL => $this->webhookUrl, + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => array('Content-type: application/json'), + CURLOPT_POSTFIELDS => $postString, + ); + if (defined('CURLOPT_SAFE_UPLOAD')) { + $options[CURLOPT_SAFE_UPLOAD] = true; + } + + curl_setopt_array($ch, $options); + + Curl\Util::execute($ch); + } + + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + parent::setFormatter($formatter); + $this->slackRecord->setFormatter($formatter); + + return $this; + } + + public function getFormatter(): FormatterInterface + { + $formatter = parent::getFormatter(); + $this->slackRecord->setFormatter($formatter); + + return $formatter; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.php new file mode 100644 index 0000000..21701af --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.php @@ -0,0 +1,448 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; + +/** + * Stores to any socket - uses fsockopen() or pfsockopen(). + * + * @author Pablo de Leon Belloc + * @see http://php.net/manual/en/function.fsockopen.php + * + * @phpstan-import-type Record from \Monolog\Logger + * @phpstan-import-type FormattedRecord from AbstractProcessingHandler + */ +class SocketHandler extends AbstractProcessingHandler +{ + /** @var string */ + private $connectionString; + /** @var float */ + private $connectionTimeout; + /** @var resource|null */ + private $resource; + /** @var float */ + private $timeout; + /** @var float */ + private $writingTimeout; + /** @var ?int */ + private $lastSentBytes = null; + /** @var ?int */ + private $chunkSize; + /** @var bool */ + private $persistent; + /** @var ?int */ + private $errno = null; + /** @var ?string */ + private $errstr = null; + /** @var ?float */ + private $lastWritingAt = null; + + /** + * @param string $connectionString Socket connection string + * @param bool $persistent Flag to enable/disable persistent connections + * @param float $timeout Socket timeout to wait until the request is being aborted + * @param float $writingTimeout Socket timeout to wait until the request should've been sent/written + * @param float|null $connectionTimeout Socket connect timeout to wait until the connection should've been + * established + * @param int|null $chunkSize Sets the chunk size. Only has effect during connection in the writing cycle + * + * @throws \InvalidArgumentException If an invalid timeout value (less than 0) is passed. + */ + public function __construct( + string $connectionString, + $level = Logger::DEBUG, + bool $bubble = true, + bool $persistent = false, + float $timeout = 0.0, + float $writingTimeout = 10.0, + ?float $connectionTimeout = null, + ?int $chunkSize = null + ) { + parent::__construct($level, $bubble); + $this->connectionString = $connectionString; + + if ($connectionTimeout !== null) { + $this->validateTimeout($connectionTimeout); + } + + $this->connectionTimeout = $connectionTimeout ?? (float) ini_get('default_socket_timeout'); + $this->persistent = $persistent; + $this->validateTimeout($timeout); + $this->timeout = $timeout; + $this->validateTimeout($writingTimeout); + $this->writingTimeout = $writingTimeout; + $this->chunkSize = $chunkSize; + } + + /** + * Connect (if necessary) and write to the socket + * + * {@inheritDoc} + * + * @throws \UnexpectedValueException + * @throws \RuntimeException + */ + protected function write(array $record): void + { + $this->connectIfNotConnected(); + $data = $this->generateDataStream($record); + $this->writeToSocket($data); + } + + /** + * We will not close a PersistentSocket instance so it can be reused in other requests. + */ + public function close(): void + { + if (!$this->isPersistent()) { + $this->closeSocket(); + } + } + + /** + * Close socket, if open + */ + public function closeSocket(): void + { + if (is_resource($this->resource)) { + fclose($this->resource); + $this->resource = null; + } + } + + /** + * Set socket connection to be persistent. It only has effect before the connection is initiated. + */ + public function setPersistent(bool $persistent): self + { + $this->persistent = $persistent; + + return $this; + } + + /** + * Set connection timeout. Only has effect before we connect. + * + * @see http://php.net/manual/en/function.fsockopen.php + */ + public function setConnectionTimeout(float $seconds): self + { + $this->validateTimeout($seconds); + $this->connectionTimeout = $seconds; + + return $this; + } + + /** + * Set write timeout. Only has effect before we connect. + * + * @see http://php.net/manual/en/function.stream-set-timeout.php + */ + public function setTimeout(float $seconds): self + { + $this->validateTimeout($seconds); + $this->timeout = $seconds; + + return $this; + } + + /** + * Set writing timeout. Only has effect during connection in the writing cycle. + * + * @param float $seconds 0 for no timeout + */ + public function setWritingTimeout(float $seconds): self + { + $this->validateTimeout($seconds); + $this->writingTimeout = $seconds; + + return $this; + } + + /** + * Set chunk size. Only has effect during connection in the writing cycle. + */ + public function setChunkSize(int $bytes): self + { + $this->chunkSize = $bytes; + + return $this; + } + + /** + * Get current connection string + */ + public function getConnectionString(): string + { + return $this->connectionString; + } + + /** + * Get persistent setting + */ + public function isPersistent(): bool + { + return $this->persistent; + } + + /** + * Get current connection timeout setting + */ + public function getConnectionTimeout(): float + { + return $this->connectionTimeout; + } + + /** + * Get current in-transfer timeout + */ + public function getTimeout(): float + { + return $this->timeout; + } + + /** + * Get current local writing timeout + * + * @return float + */ + public function getWritingTimeout(): float + { + return $this->writingTimeout; + } + + /** + * Get current chunk size + */ + public function getChunkSize(): ?int + { + return $this->chunkSize; + } + + /** + * Check to see if the socket is currently available. + * + * UDP might appear to be connected but might fail when writing. See http://php.net/fsockopen for details. + */ + public function isConnected(): bool + { + return is_resource($this->resource) + && !feof($this->resource); // on TCP - other party can close connection. + } + + /** + * Wrapper to allow mocking + * + * @return resource|false + */ + protected function pfsockopen() + { + return @pfsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout); + } + + /** + * Wrapper to allow mocking + * + * @return resource|false + */ + protected function fsockopen() + { + return @fsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout); + } + + /** + * Wrapper to allow mocking + * + * @see http://php.net/manual/en/function.stream-set-timeout.php + * + * @return bool + */ + protected function streamSetTimeout() + { + $seconds = floor($this->timeout); + $microseconds = round(($this->timeout - $seconds) * 1e6); + + if (!is_resource($this->resource)) { + throw new \LogicException('streamSetTimeout called but $this->resource is not a resource'); + } + + return stream_set_timeout($this->resource, (int) $seconds, (int) $microseconds); + } + + /** + * Wrapper to allow mocking + * + * @see http://php.net/manual/en/function.stream-set-chunk-size.php + * + * @return int|bool + */ + protected function streamSetChunkSize() + { + if (!is_resource($this->resource)) { + throw new \LogicException('streamSetChunkSize called but $this->resource is not a resource'); + } + + if (null === $this->chunkSize) { + throw new \LogicException('streamSetChunkSize called but $this->chunkSize is not set'); + } + + return stream_set_chunk_size($this->resource, $this->chunkSize); + } + + /** + * Wrapper to allow mocking + * + * @return int|bool + */ + protected function fwrite(string $data) + { + if (!is_resource($this->resource)) { + throw new \LogicException('fwrite called but $this->resource is not a resource'); + } + + return @fwrite($this->resource, $data); + } + + /** + * Wrapper to allow mocking + * + * @return mixed[]|bool + */ + protected function streamGetMetadata() + { + if (!is_resource($this->resource)) { + throw new \LogicException('streamGetMetadata called but $this->resource is not a resource'); + } + + return stream_get_meta_data($this->resource); + } + + private function validateTimeout(float $value): void + { + if ($value < 0) { + throw new \InvalidArgumentException("Timeout must be 0 or a positive float (got $value)"); + } + } + + private function connectIfNotConnected(): void + { + if ($this->isConnected()) { + return; + } + $this->connect(); + } + + /** + * @phpstan-param FormattedRecord $record + */ + protected function generateDataStream(array $record): string + { + return (string) $record['formatted']; + } + + /** + * @return resource|null + */ + protected function getResource() + { + return $this->resource; + } + + private function connect(): void + { + $this->createSocketResource(); + $this->setSocketTimeout(); + $this->setStreamChunkSize(); + } + + private function createSocketResource(): void + { + if ($this->isPersistent()) { + $resource = $this->pfsockopen(); + } else { + $resource = $this->fsockopen(); + } + if (is_bool($resource)) { + throw new \UnexpectedValueException("Failed connecting to $this->connectionString ($this->errno: $this->errstr)"); + } + $this->resource = $resource; + } + + private function setSocketTimeout(): void + { + if (!$this->streamSetTimeout()) { + throw new \UnexpectedValueException("Failed setting timeout with stream_set_timeout()"); + } + } + + private function setStreamChunkSize(): void + { + if ($this->chunkSize && !$this->streamSetChunkSize()) { + throw new \UnexpectedValueException("Failed setting chunk size with stream_set_chunk_size()"); + } + } + + private function writeToSocket(string $data): void + { + $length = strlen($data); + $sent = 0; + $this->lastSentBytes = $sent; + while ($this->isConnected() && $sent < $length) { + if (0 == $sent) { + $chunk = $this->fwrite($data); + } else { + $chunk = $this->fwrite(substr($data, $sent)); + } + if ($chunk === false) { + throw new \RuntimeException("Could not write to socket"); + } + $sent += $chunk; + $socketInfo = $this->streamGetMetadata(); + if (is_array($socketInfo) && $socketInfo['timed_out']) { + throw new \RuntimeException("Write timed-out"); + } + + if ($this->writingIsTimedOut($sent)) { + throw new \RuntimeException("Write timed-out, no data sent for `{$this->writingTimeout}` seconds, probably we got disconnected (sent $sent of $length)"); + } + } + if (!$this->isConnected() && $sent < $length) { + throw new \RuntimeException("End-of-file reached, probably we got disconnected (sent $sent of $length)"); + } + } + + private function writingIsTimedOut(int $sent): bool + { + // convert to ms + if (0.0 == $this->writingTimeout) { + return false; + } + + if ($sent !== $this->lastSentBytes) { + $this->lastWritingAt = microtime(true); + $this->lastSentBytes = $sent; + + return false; + } else { + usleep(100); + } + + if ((microtime(true) - $this->lastWritingAt) >= $this->writingTimeout) { + $this->closeSocket(); + + return true; + } + + return false; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/SqsHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/SqsHandler.php new file mode 100644 index 0000000..dcf282b --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/SqsHandler.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Aws\Sqs\SqsClient; +use Monolog\Logger; +use Monolog\Utils; + +/** + * Writes to any sqs queue. + * + * @author Martijn van Calker + */ +class SqsHandler extends AbstractProcessingHandler +{ + /** 256 KB in bytes - maximum message size in SQS */ + protected const MAX_MESSAGE_SIZE = 262144; + /** 100 KB in bytes - head message size for new error log */ + protected const HEAD_MESSAGE_SIZE = 102400; + + /** @var SqsClient */ + private $client; + /** @var string */ + private $queueUrl; + + public function __construct(SqsClient $sqsClient, string $queueUrl, $level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + + $this->client = $sqsClient; + $this->queueUrl = $queueUrl; + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + if (!isset($record['formatted']) || 'string' !== gettype($record['formatted'])) { + throw new \InvalidArgumentException('SqsHandler accepts only formatted records as a string' . Utils::getRecordMessageForException($record)); + } + + $messageBody = $record['formatted']; + if (strlen($messageBody) >= static::MAX_MESSAGE_SIZE) { + $messageBody = Utils::substr($messageBody, 0, static::HEAD_MESSAGE_SIZE); + } + + $this->client->sendMessage([ + 'QueueUrl' => $this->queueUrl, + 'MessageBody' => $messageBody, + ]); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php new file mode 100644 index 0000000..82c048e --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php @@ -0,0 +1,224 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Utils; + +/** + * Stores to any stream resource + * + * Can be used to store into php://stderr, remote and local files, etc. + * + * @author Jordi Boggiano + * + * @phpstan-import-type FormattedRecord from AbstractProcessingHandler + */ +class StreamHandler extends AbstractProcessingHandler +{ + /** @const int */ + protected const MAX_CHUNK_SIZE = 2147483647; + /** @const int 10MB */ + protected const DEFAULT_CHUNK_SIZE = 10 * 1024 * 1024; + /** @var int */ + protected $streamChunkSize; + /** @var resource|null */ + protected $stream; + /** @var ?string */ + protected $url = null; + /** @var ?string */ + private $errorMessage = null; + /** @var ?int */ + protected $filePermission; + /** @var bool */ + protected $useLocking; + /** @var true|null */ + private $dirCreated = null; + + /** + * @param resource|string $stream If a missing path can't be created, an UnexpectedValueException will be thrown on first write + * @param int|null $filePermission Optional file permissions (default (0644) are only for owner read/write) + * @param bool $useLocking Try to lock log file before doing any writes + * + * @throws \InvalidArgumentException If stream is not a resource or string + */ + public function __construct($stream, $level = Logger::DEBUG, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false) + { + parent::__construct($level, $bubble); + + if (($phpMemoryLimit = Utils::expandIniShorthandBytes(ini_get('memory_limit'))) !== false) { + if ($phpMemoryLimit > 0) { + // use max 10% of allowed memory for the chunk size, and at least 100KB + $this->streamChunkSize = min(static::MAX_CHUNK_SIZE, max((int) ($phpMemoryLimit / 10), 100 * 1024)); + } else { + // memory is unlimited, set to the default 10MB + $this->streamChunkSize = static::DEFAULT_CHUNK_SIZE; + } + } else { + // no memory limit information, set to the default 10MB + $this->streamChunkSize = static::DEFAULT_CHUNK_SIZE; + } + + if (is_resource($stream)) { + $this->stream = $stream; + + stream_set_chunk_size($this->stream, $this->streamChunkSize); + } elseif (is_string($stream)) { + $this->url = Utils::canonicalizePath($stream); + } else { + throw new \InvalidArgumentException('A stream must either be a resource or a string.'); + } + + $this->filePermission = $filePermission; + $this->useLocking = $useLocking; + } + + /** + * {@inheritDoc} + */ + public function close(): void + { + if ($this->url && is_resource($this->stream)) { + fclose($this->stream); + } + $this->stream = null; + $this->dirCreated = null; + } + + /** + * Return the currently active stream if it is open + * + * @return resource|null + */ + public function getStream() + { + return $this->stream; + } + + /** + * Return the stream URL if it was configured with a URL and not an active resource + * + * @return string|null + */ + public function getUrl(): ?string + { + return $this->url; + } + + /** + * @return int + */ + public function getStreamChunkSize(): int + { + return $this->streamChunkSize; + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + if (!is_resource($this->stream)) { + $url = $this->url; + if (null === $url || '' === $url) { + throw new \LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().' . Utils::getRecordMessageForException($record)); + } + $this->createDir($url); + $this->errorMessage = null; + set_error_handler([$this, 'customErrorHandler']); + try { + $stream = fopen($url, 'a'); + if ($this->filePermission !== null) { + @chmod($url, $this->filePermission); + } + } finally { + restore_error_handler(); + } + if (!is_resource($stream)) { + $this->stream = null; + + throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened in append mode: '.$this->errorMessage, $url) . Utils::getRecordMessageForException($record)); + } + stream_set_chunk_size($stream, $this->streamChunkSize); + $this->stream = $stream; + } + + $stream = $this->stream; + if (!is_resource($stream)) { + throw new \LogicException('No stream was opened yet' . Utils::getRecordMessageForException($record)); + } + + if ($this->useLocking) { + // ignoring errors here, there's not much we can do about them + flock($stream, LOCK_EX); + } + + $this->streamWrite($stream, $record); + + if ($this->useLocking) { + flock($stream, LOCK_UN); + } + } + + /** + * Write to stream + * @param resource $stream + * @param array $record + * + * @phpstan-param FormattedRecord $record + */ + protected function streamWrite($stream, array $record): void + { + fwrite($stream, (string) $record['formatted']); + } + + private function customErrorHandler(int $code, string $msg): bool + { + $this->errorMessage = preg_replace('{^(fopen|mkdir)\(.*?\): }', '', $msg); + + return true; + } + + private function getDirFromStream(string $stream): ?string + { + $pos = strpos($stream, '://'); + if ($pos === false) { + return dirname($stream); + } + + if ('file://' === substr($stream, 0, 7)) { + return dirname(substr($stream, 7)); + } + + return null; + } + + private function createDir(string $url): void + { + // Do not try to create dir if it has already been tried. + if ($this->dirCreated) { + return; + } + + $dir = $this->getDirFromStream($url); + if (null !== $dir && !is_dir($dir)) { + $this->errorMessage = null; + set_error_handler([$this, 'customErrorHandler']); + $status = mkdir($dir, 0777, true); + restore_error_handler(); + if (false === $status && !is_dir($dir) && strpos((string) $this->errorMessage, 'File exists') === false) { + throw new \UnexpectedValueException(sprintf('There is no existing directory at "%s" and it could not be created: '.$this->errorMessage, $dir)); + } + } + $this->dirCreated = true; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/SwiftMailerHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/SwiftMailerHandler.php new file mode 100644 index 0000000..fae9251 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/SwiftMailerHandler.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Utils; +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LineFormatter; +use Swift_Message; +use Swift; + +/** + * SwiftMailerHandler uses Swift_Mailer to send the emails + * + * @author Gyula Sallai + * + * @phpstan-import-type Record from \Monolog\Logger + * @deprecated Since Monolog 2.6. Use SymfonyMailerHandler instead. + */ +class SwiftMailerHandler extends MailHandler +{ + /** @var \Swift_Mailer */ + protected $mailer; + /** @var Swift_Message|callable(string, Record[]): Swift_Message */ + private $messageTemplate; + + /** + * @psalm-param Swift_Message|callable(string, Record[]): Swift_Message $message + * + * @param \Swift_Mailer $mailer The mailer to use + * @param callable|Swift_Message $message An example message for real messages, only the body will be replaced + */ + public function __construct(\Swift_Mailer $mailer, $message, $level = Logger::ERROR, bool $bubble = true) + { + parent::__construct($level, $bubble); + + @trigger_error('The SwiftMailerHandler is deprecated since Monolog 2.6. Use SymfonyMailerHandler instead.', E_USER_DEPRECATED); + + $this->mailer = $mailer; + $this->messageTemplate = $message; + } + + /** + * {@inheritDoc} + */ + protected function send(string $content, array $records): void + { + $this->mailer->send($this->buildMessage($content, $records)); + } + + /** + * Gets the formatter for the Swift_Message subject. + * + * @param string|null $format The format of the subject + */ + protected function getSubjectFormatter(?string $format): FormatterInterface + { + return new LineFormatter($format); + } + + /** + * Creates instance of Swift_Message to be sent + * + * @param string $content formatted email body to be sent + * @param array $records Log records that formed the content + * @return Swift_Message + * + * @phpstan-param Record[] $records + */ + protected function buildMessage(string $content, array $records): Swift_Message + { + $message = null; + if ($this->messageTemplate instanceof Swift_Message) { + $message = clone $this->messageTemplate; + $message->generateId(); + } elseif (is_callable($this->messageTemplate)) { + $message = ($this->messageTemplate)($content, $records); + } + + if (!$message instanceof Swift_Message) { + $record = reset($records); + throw new \InvalidArgumentException('Could not resolve message as instance of Swift_Message or a callable returning it' . ($record ? Utils::getRecordMessageForException($record) : '')); + } + + if ($records) { + $subjectFormatter = $this->getSubjectFormatter($message->getSubject()); + $message->setSubject($subjectFormatter->format($this->getHighestRecord($records))); + } + + $mime = 'text/plain'; + if ($this->isHtmlBody($content)) { + $mime = 'text/html'; + } + + $message->setBody($content, $mime); + /** @phpstan-ignore-next-line */ + if (version_compare(Swift::VERSION, '6.0.0', '>=')) { + $message->setDate(new \DateTimeImmutable()); + } else { + /** @phpstan-ignore-next-line */ + $message->setDate(time()); + } + + return $message; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/SymfonyMailerHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/SymfonyMailerHandler.php new file mode 100644 index 0000000..130e6f1 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/SymfonyMailerHandler.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Utils; +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LineFormatter; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Mime\Email; + +/** + * SymfonyMailerHandler uses Symfony's Mailer component to send the emails + * + * @author Jordi Boggiano + * + * @phpstan-import-type Record from \Monolog\Logger + */ +class SymfonyMailerHandler extends MailHandler +{ + /** @var MailerInterface|TransportInterface */ + protected $mailer; + /** @var Email|callable(string, Record[]): Email */ + private $emailTemplate; + + /** + * @psalm-param Email|callable(string, Record[]): Email $email + * + * @param MailerInterface|TransportInterface $mailer The mailer to use + * @param callable|Email $email An email template, the subject/body will be replaced + */ + public function __construct($mailer, $email, $level = Logger::ERROR, bool $bubble = true) + { + parent::__construct($level, $bubble); + + $this->mailer = $mailer; + $this->emailTemplate = $email; + } + + /** + * {@inheritDoc} + */ + protected function send(string $content, array $records): void + { + $this->mailer->send($this->buildMessage($content, $records)); + } + + /** + * Gets the formatter for the Swift_Message subject. + * + * @param string|null $format The format of the subject + */ + protected function getSubjectFormatter(?string $format): FormatterInterface + { + return new LineFormatter($format); + } + + /** + * Creates instance of Email to be sent + * + * @param string $content formatted email body to be sent + * @param array $records Log records that formed the content + * + * @phpstan-param Record[] $records + */ + protected function buildMessage(string $content, array $records): Email + { + $message = null; + if ($this->emailTemplate instanceof Email) { + $message = clone $this->emailTemplate; + } elseif (is_callable($this->emailTemplate)) { + $message = ($this->emailTemplate)($content, $records); + } + + if (!$message instanceof Email) { + $record = reset($records); + throw new \InvalidArgumentException('Could not resolve message as instance of Email or a callable returning it' . ($record ? Utils::getRecordMessageForException($record) : '')); + } + + if ($records) { + $subjectFormatter = $this->getSubjectFormatter($message->getSubject()); + $message->subject($subjectFormatter->format($this->getHighestRecord($records))); + } + + if ($this->isHtmlBody($content)) { + if (null !== ($charset = $message->getHtmlCharset())) { + $message->html($content, $charset); + } else { + $message->html($content); + } + } else { + if (null !== ($charset = $message->getTextCharset())) { + $message->text($content, $charset); + } else { + $message->text($content); + } + } + + return $message->date(new \DateTimeImmutable()); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/SyslogHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/SyslogHandler.php new file mode 100644 index 0000000..1d543b7 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/SyslogHandler.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Utils; + +/** + * Logs to syslog service. + * + * usage example: + * + * $log = new Logger('application'); + * $syslog = new SyslogHandler('myfacility', 'local6'); + * $formatter = new LineFormatter("%channel%.%level_name%: %message% %extra%"); + * $syslog->setFormatter($formatter); + * $log->pushHandler($syslog); + * + * @author Sven Paulus + */ +class SyslogHandler extends AbstractSyslogHandler +{ + /** @var string */ + protected $ident; + /** @var int */ + protected $logopts; + + /** + * @param string $ident + * @param string|int $facility Either one of the names of the keys in $this->facilities, or a LOG_* facility constant + * @param int $logopts Option flags for the openlog() call, defaults to LOG_PID + */ + public function __construct(string $ident, $facility = LOG_USER, $level = Logger::DEBUG, bool $bubble = true, int $logopts = LOG_PID) + { + parent::__construct($facility, $level, $bubble); + + $this->ident = $ident; + $this->logopts = $logopts; + } + + /** + * {@inheritDoc} + */ + public function close(): void + { + closelog(); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + if (!openlog($this->ident, $this->logopts, $this->facility)) { + throw new \LogicException('Can\'t open syslog for ident "'.$this->ident.'" and facility "'.$this->facility.'"' . Utils::getRecordMessageForException($record)); + } + syslog($this->logLevels[$record['level']], (string) $record['formatted']); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/SyslogUdp/UdpSocket.php b/vendor/monolog/monolog/src/Monolog/Handler/SyslogUdp/UdpSocket.php new file mode 100644 index 0000000..dbd8ef6 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/SyslogUdp/UdpSocket.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler\SyslogUdp; + +use Monolog\Utils; +use Socket; + +class UdpSocket +{ + protected const DATAGRAM_MAX_LENGTH = 65023; + + /** @var string */ + protected $ip; + /** @var int */ + protected $port; + /** @var resource|Socket|null */ + protected $socket = null; + + public function __construct(string $ip, int $port = 514) + { + $this->ip = $ip; + $this->port = $port; + } + + /** + * @param string $line + * @param string $header + * @return void + */ + public function write($line, $header = "") + { + $this->send($this->assembleMessage($line, $header)); + } + + public function close(): void + { + if (is_resource($this->socket) || $this->socket instanceof Socket) { + socket_close($this->socket); + $this->socket = null; + } + } + + /** + * @return resource|Socket + */ + protected function getSocket() + { + if (null !== $this->socket) { + return $this->socket; + } + + $domain = AF_INET; + $protocol = SOL_UDP; + // Check if we are using unix sockets. + if ($this->port === 0) { + $domain = AF_UNIX; + $protocol = IPPROTO_IP; + } + + $this->socket = socket_create($domain, SOCK_DGRAM, $protocol) ?: null; + if (null === $this->socket) { + throw new \RuntimeException('The UdpSocket to '.$this->ip.':'.$this->port.' could not be opened via socket_create'); + } + + return $this->socket; + } + + protected function send(string $chunk): void + { + socket_sendto($this->getSocket(), $chunk, strlen($chunk), $flags = 0, $this->ip, $this->port); + } + + protected function assembleMessage(string $line, string $header): string + { + $chunkSize = static::DATAGRAM_MAX_LENGTH - strlen($header); + + return $header . Utils::substr($line, 0, $chunkSize); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/SyslogUdpHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/SyslogUdpHandler.php new file mode 100644 index 0000000..deaa19f --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/SyslogUdpHandler.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use DateTimeInterface; +use Monolog\Logger; +use Monolog\Handler\SyslogUdp\UdpSocket; +use Monolog\Utils; + +/** + * A Handler for logging to a remote syslogd server. + * + * @author Jesper Skovgaard Nielsen + * @author Dominik Kukacka + */ +class SyslogUdpHandler extends AbstractSyslogHandler +{ + const RFC3164 = 0; + const RFC5424 = 1; + const RFC5424e = 2; + + /** @var array */ + private $dateFormats = array( + self::RFC3164 => 'M d H:i:s', + self::RFC5424 => \DateTime::RFC3339, + self::RFC5424e => \DateTime::RFC3339_EXTENDED, + ); + + /** @var UdpSocket */ + protected $socket; + /** @var string */ + protected $ident; + /** @var self::RFC* */ + protected $rfc; + + /** + * @param string $host Either IP/hostname or a path to a unix socket (port must be 0 then) + * @param int $port Port number, or 0 if $host is a unix socket + * @param string|int $facility Either one of the names of the keys in $this->facilities, or a LOG_* facility constant + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param string $ident Program name or tag for each log message. + * @param int $rfc RFC to format the message for. + * @throws MissingExtensionException + * + * @phpstan-param self::RFC* $rfc + */ + public function __construct(string $host, int $port = 514, $facility = LOG_USER, $level = Logger::DEBUG, bool $bubble = true, string $ident = 'php', int $rfc = self::RFC5424) + { + if (!extension_loaded('sockets')) { + throw new MissingExtensionException('The sockets extension is required to use the SyslogUdpHandler'); + } + + parent::__construct($facility, $level, $bubble); + + $this->ident = $ident; + $this->rfc = $rfc; + + $this->socket = new UdpSocket($host, $port); + } + + protected function write(array $record): void + { + $lines = $this->splitMessageIntoLines($record['formatted']); + + $header = $this->makeCommonSyslogHeader($this->logLevels[$record['level']], $record['datetime']); + + foreach ($lines as $line) { + $this->socket->write($line, $header); + } + } + + public function close(): void + { + $this->socket->close(); + } + + /** + * @param string|string[] $message + * @return string[] + */ + private function splitMessageIntoLines($message): array + { + if (is_array($message)) { + $message = implode("\n", $message); + } + + $lines = preg_split('/$\R?^/m', (string) $message, -1, PREG_SPLIT_NO_EMPTY); + if (false === $lines) { + $pcreErrorCode = preg_last_error(); + throw new \RuntimeException('Could not preg_split: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode)); + } + + return $lines; + } + + /** + * Make common syslog header (see rfc5424 or rfc3164) + */ + protected function makeCommonSyslogHeader(int $severity, DateTimeInterface $datetime): string + { + $priority = $severity + $this->facility; + + if (!$pid = getmypid()) { + $pid = '-'; + } + + if (!$hostname = gethostname()) { + $hostname = '-'; + } + + if ($this->rfc === self::RFC3164) { + // see https://github.com/phpstan/phpstan/issues/5348 + // @phpstan-ignore-next-line + $dateNew = $datetime->setTimezone(new \DateTimeZone('UTC')); + $date = $dateNew->format($this->dateFormats[$this->rfc]); + + return "<$priority>" . + $date . " " . + $hostname . " " . + $this->ident . "[" . $pid . "]: "; + } + + $date = $datetime->format($this->dateFormats[$this->rfc]); + + return "<$priority>1 " . + $date . " " . + $hostname . " " . + $this->ident . " " . + $pid . " - - "; + } + + /** + * Inject your own socket, mainly used for testing + */ + public function setSocket(UdpSocket $socket): self + { + $this->socket = $socket; + + return $this; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/TelegramBotHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/TelegramBotHandler.php new file mode 100644 index 0000000..a6223b7 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/TelegramBotHandler.php @@ -0,0 +1,274 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use RuntimeException; +use Monolog\Logger; +use Monolog\Utils; + +/** + * Handler send logs to Telegram using Telegram Bot API. + * + * How to use: + * 1) Create telegram bot with https://telegram.me/BotFather + * 2) Create a telegram channel where logs will be recorded. + * 3) Add created bot from step 1 to the created channel from step 2. + * + * Use telegram bot API key from step 1 and channel name with '@' prefix from step 2 to create instance of TelegramBotHandler + * + * @link https://core.telegram.org/bots/api + * + * @author Mazur Alexandr + * + * @phpstan-import-type Record from \Monolog\Logger + */ +class TelegramBotHandler extends AbstractProcessingHandler +{ + private const BOT_API = 'https://api.telegram.org/bot'; + + /** + * The available values of parseMode according to the Telegram api documentation + */ + private const AVAILABLE_PARSE_MODES = [ + 'HTML', + 'MarkdownV2', + 'Markdown', // legacy mode without underline and strikethrough, use MarkdownV2 instead + ]; + + /** + * The maximum number of characters allowed in a message according to the Telegram api documentation + */ + private const MAX_MESSAGE_LENGTH = 4096; + + /** + * Telegram bot access token provided by BotFather. + * Create telegram bot with https://telegram.me/BotFather and use access token from it. + * @var string + */ + private $apiKey; + + /** + * Telegram channel name. + * Since to start with '@' symbol as prefix. + * @var string + */ + private $channel; + + /** + * The kind of formatting that is used for the message. + * See available options at https://core.telegram.org/bots/api#formatting-options + * or in AVAILABLE_PARSE_MODES + * @var ?string + */ + private $parseMode; + + /** + * Disables link previews for links in the message. + * @var ?bool + */ + private $disableWebPagePreview; + + /** + * Sends the message silently. Users will receive a notification with no sound. + * @var ?bool + */ + private $disableNotification; + + /** + * True - split a message longer than MAX_MESSAGE_LENGTH into parts and send in multiple messages. + * False - truncates a message that is too long. + * @var bool + */ + private $splitLongMessages; + + /** + * Adds 1-second delay between sending a split message (according to Telegram API to avoid 429 Too Many Requests). + * @var bool + */ + private $delayBetweenMessages; + + /** + * @param string $apiKey Telegram bot access token provided by BotFather + * @param string $channel Telegram channel name + * @param bool $splitLongMessages Split a message longer than MAX_MESSAGE_LENGTH into parts and send in multiple messages + * @param bool $delayBetweenMessages Adds delay between sending a split message according to Telegram API + * @throws MissingExtensionException + */ + public function __construct( + string $apiKey, + string $channel, + $level = Logger::DEBUG, + bool $bubble = true, + ?string $parseMode = null, + ?bool $disableWebPagePreview = null, + ?bool $disableNotification = null, + bool $splitLongMessages = false, + bool $delayBetweenMessages = false + ) + { + if (!extension_loaded('curl')) { + throw new MissingExtensionException('The curl extension is needed to use the TelegramBotHandler'); + } + + parent::__construct($level, $bubble); + + $this->apiKey = $apiKey; + $this->channel = $channel; + $this->setParseMode($parseMode); + $this->disableWebPagePreview($disableWebPagePreview); + $this->disableNotification($disableNotification); + $this->splitLongMessages($splitLongMessages); + $this->delayBetweenMessages($delayBetweenMessages); + } + + public function setParseMode(?string $parseMode = null): self + { + if ($parseMode !== null && !in_array($parseMode, self::AVAILABLE_PARSE_MODES)) { + throw new \InvalidArgumentException('Unknown parseMode, use one of these: ' . implode(', ', self::AVAILABLE_PARSE_MODES) . '.'); + } + + $this->parseMode = $parseMode; + + return $this; + } + + public function disableWebPagePreview(?bool $disableWebPagePreview = null): self + { + $this->disableWebPagePreview = $disableWebPagePreview; + + return $this; + } + + public function disableNotification(?bool $disableNotification = null): self + { + $this->disableNotification = $disableNotification; + + return $this; + } + + /** + * True - split a message longer than MAX_MESSAGE_LENGTH into parts and send in multiple messages. + * False - truncates a message that is too long. + * @param bool $splitLongMessages + * @return $this + */ + public function splitLongMessages(bool $splitLongMessages = false): self + { + $this->splitLongMessages = $splitLongMessages; + + return $this; + } + + /** + * Adds 1-second delay between sending a split message (according to Telegram API to avoid 429 Too Many Requests). + * @param bool $delayBetweenMessages + * @return $this + */ + public function delayBetweenMessages(bool $delayBetweenMessages = false): self + { + $this->delayBetweenMessages = $delayBetweenMessages; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function handleBatch(array $records): void + { + /** @var Record[] $messages */ + $messages = []; + + foreach ($records as $record) { + if (!$this->isHandling($record)) { + continue; + } + + if ($this->processors) { + /** @var Record $record */ + $record = $this->processRecord($record); + } + + $messages[] = $record; + } + + if (!empty($messages)) { + $this->send((string)$this->getFormatter()->formatBatch($messages)); + } + } + + /** + * @inheritDoc + */ + protected function write(array $record): void + { + $this->send($record['formatted']); + } + + /** + * Send request to @link https://api.telegram.org/bot on SendMessage action. + * @param string $message + */ + protected function send(string $message): void + { + $messages = $this->handleMessageLength($message); + + foreach ($messages as $key => $msg) { + if ($this->delayBetweenMessages && $key > 0) { + sleep(1); + } + + $this->sendCurl($msg); + } + } + + protected function sendCurl(string $message): void + { + $ch = curl_init(); + $url = self::BOT_API . $this->apiKey . '/SendMessage'; + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([ + 'text' => $message, + 'chat_id' => $this->channel, + 'parse_mode' => $this->parseMode, + 'disable_web_page_preview' => $this->disableWebPagePreview, + 'disable_notification' => $this->disableNotification, + ])); + + $result = Curl\Util::execute($ch); + if (!is_string($result)) { + throw new RuntimeException('Telegram API error. Description: No response'); + } + $result = json_decode($result, true); + + if ($result['ok'] === false) { + throw new RuntimeException('Telegram API error. Description: ' . $result['description']); + } + } + + /** + * Handle a message that is too long: truncates or splits into several + * @param string $message + * @return string[] + */ + private function handleMessageLength(string $message): array + { + $truncatedMarker = ' (...truncated)'; + if (!$this->splitLongMessages && strlen($message) > self::MAX_MESSAGE_LENGTH) { + return [Utils::substr($message, 0, self::MAX_MESSAGE_LENGTH - strlen($truncatedMarker)) . $truncatedMarker]; + } + + return str_split($message, self::MAX_MESSAGE_LENGTH); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/TestHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/TestHandler.php new file mode 100644 index 0000000..0986da2 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/TestHandler.php @@ -0,0 +1,231 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Psr\Log\LogLevel; + +/** + * Used for testing purposes. + * + * It records all records and gives you access to them for verification. + * + * @author Jordi Boggiano + * + * @method bool hasEmergency($record) + * @method bool hasAlert($record) + * @method bool hasCritical($record) + * @method bool hasError($record) + * @method bool hasWarning($record) + * @method bool hasNotice($record) + * @method bool hasInfo($record) + * @method bool hasDebug($record) + * + * @method bool hasEmergencyRecords() + * @method bool hasAlertRecords() + * @method bool hasCriticalRecords() + * @method bool hasErrorRecords() + * @method bool hasWarningRecords() + * @method bool hasNoticeRecords() + * @method bool hasInfoRecords() + * @method bool hasDebugRecords() + * + * @method bool hasEmergencyThatContains($message) + * @method bool hasAlertThatContains($message) + * @method bool hasCriticalThatContains($message) + * @method bool hasErrorThatContains($message) + * @method bool hasWarningThatContains($message) + * @method bool hasNoticeThatContains($message) + * @method bool hasInfoThatContains($message) + * @method bool hasDebugThatContains($message) + * + * @method bool hasEmergencyThatMatches($message) + * @method bool hasAlertThatMatches($message) + * @method bool hasCriticalThatMatches($message) + * @method bool hasErrorThatMatches($message) + * @method bool hasWarningThatMatches($message) + * @method bool hasNoticeThatMatches($message) + * @method bool hasInfoThatMatches($message) + * @method bool hasDebugThatMatches($message) + * + * @method bool hasEmergencyThatPasses($message) + * @method bool hasAlertThatPasses($message) + * @method bool hasCriticalThatPasses($message) + * @method bool hasErrorThatPasses($message) + * @method bool hasWarningThatPasses($message) + * @method bool hasNoticeThatPasses($message) + * @method bool hasInfoThatPasses($message) + * @method bool hasDebugThatPasses($message) + * + * @phpstan-import-type Record from \Monolog\Logger + * @phpstan-import-type Level from \Monolog\Logger + * @phpstan-import-type LevelName from \Monolog\Logger + */ +class TestHandler extends AbstractProcessingHandler +{ + /** @var Record[] */ + protected $records = []; + /** @var array */ + protected $recordsByLevel = []; + /** @var bool */ + private $skipReset = false; + + /** + * @return array + * + * @phpstan-return Record[] + */ + public function getRecords() + { + return $this->records; + } + + /** + * @return void + */ + public function clear() + { + $this->records = []; + $this->recordsByLevel = []; + } + + /** + * @return void + */ + public function reset() + { + if (!$this->skipReset) { + $this->clear(); + } + } + + /** + * @return void + */ + public function setSkipReset(bool $skipReset) + { + $this->skipReset = $skipReset; + } + + /** + * @param string|int $level Logging level value or name + * + * @phpstan-param Level|LevelName|LogLevel::* $level + */ + public function hasRecords($level): bool + { + return isset($this->recordsByLevel[Logger::toMonologLevel($level)]); + } + + /** + * @param string|array $record Either a message string or an array containing message and optionally context keys that will be checked against all records + * @param string|int $level Logging level value or name + * + * @phpstan-param array{message: string, context?: mixed[]}|string $record + * @phpstan-param Level|LevelName|LogLevel::* $level + */ + public function hasRecord($record, $level): bool + { + if (is_string($record)) { + $record = array('message' => $record); + } + + return $this->hasRecordThatPasses(function ($rec) use ($record) { + if ($rec['message'] !== $record['message']) { + return false; + } + if (isset($record['context']) && $rec['context'] !== $record['context']) { + return false; + } + + return true; + }, $level); + } + + /** + * @param string|int $level Logging level value or name + * + * @phpstan-param Level|LevelName|LogLevel::* $level + */ + public function hasRecordThatContains(string $message, $level): bool + { + return $this->hasRecordThatPasses(function ($rec) use ($message) { + return strpos($rec['message'], $message) !== false; + }, $level); + } + + /** + * @param string|int $level Logging level value or name + * + * @phpstan-param Level|LevelName|LogLevel::* $level + */ + public function hasRecordThatMatches(string $regex, $level): bool + { + return $this->hasRecordThatPasses(function (array $rec) use ($regex): bool { + return preg_match($regex, $rec['message']) > 0; + }, $level); + } + + /** + * @param string|int $level Logging level value or name + * @return bool + * + * @psalm-param callable(Record, int): mixed $predicate + * @phpstan-param Level|LevelName|LogLevel::* $level + */ + public function hasRecordThatPasses(callable $predicate, $level) + { + $level = Logger::toMonologLevel($level); + + if (!isset($this->recordsByLevel[$level])) { + return false; + } + + foreach ($this->recordsByLevel[$level] as $i => $rec) { + if ($predicate($rec, $i)) { + return true; + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $this->recordsByLevel[$record['level']][] = $record; + $this->records[] = $record; + } + + /** + * @param string $method + * @param mixed[] $args + * @return bool + */ + public function __call($method, $args) + { + if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) { + $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3]; + $level = constant('Monolog\Logger::' . strtoupper($matches[2])); + $callback = [$this, $genericMethod]; + if (is_callable($callback)) { + $args[] = $level; + + return call_user_func_array($callback, $args); + } + } + + throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()'); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/WebRequestRecognizerTrait.php b/vendor/monolog/monolog/src/Monolog/Handler/WebRequestRecognizerTrait.php new file mode 100644 index 0000000..c818352 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/WebRequestRecognizerTrait.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +trait WebRequestRecognizerTrait +{ + /** + * Checks if PHP's serving a web request + * @return bool + */ + protected function isWebRequest(): bool + { + return 'cli' !== \PHP_SAPI && 'phpdbg' !== \PHP_SAPI; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/WhatFailureGroupHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/WhatFailureGroupHandler.php new file mode 100644 index 0000000..b6d3d3b --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/WhatFailureGroupHandler.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +/** + * Forwards records to multiple handlers suppressing failures of each handler + * and continuing through to give every handler a chance to succeed. + * + * @author Craig D'Amelio + * + * @phpstan-import-type Record from \Monolog\Logger + */ +class WhatFailureGroupHandler extends GroupHandler +{ + /** + * {@inheritDoc} + */ + public function handle(array $record): bool + { + if ($this->processors) { + /** @var Record $record */ + $record = $this->processRecord($record); + } + + foreach ($this->handlers as $handler) { + try { + $handler->handle($record); + } catch (\Throwable $e) { + // What failure? + } + } + + return false === $this->bubble; + } + + /** + * {@inheritDoc} + */ + public function handleBatch(array $records): void + { + if ($this->processors) { + $processed = array(); + foreach ($records as $record) { + $processed[] = $this->processRecord($record); + } + /** @var Record[] $records */ + $records = $processed; + } + + foreach ($this->handlers as $handler) { + try { + $handler->handleBatch($records); + } catch (\Throwable $e) { + // What failure? + } + } + } + + /** + * {@inheritDoc} + */ + public function close(): void + { + foreach ($this->handlers as $handler) { + try { + $handler->close(); + } catch (\Throwable $e) { + // What failure? + } + } + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Handler/ZendMonitorHandler.php b/vendor/monolog/monolog/src/Monolog/Handler/ZendMonitorHandler.php new file mode 100644 index 0000000..ddd46d8 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Handler/ZendMonitorHandler.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\NormalizerFormatter; +use Monolog\Logger; + +/** + * Handler sending logs to Zend Monitor + * + * @author Christian Bergau + * @author Jason Davis + * + * @phpstan-import-type FormattedRecord from AbstractProcessingHandler + */ +class ZendMonitorHandler extends AbstractProcessingHandler +{ + /** + * Monolog level / ZendMonitor Custom Event priority map + * + * @var array + */ + protected $levelMap = []; + + /** + * @throws MissingExtensionException + */ + public function __construct($level = Logger::DEBUG, bool $bubble = true) + { + if (!function_exists('zend_monitor_custom_event')) { + throw new MissingExtensionException( + 'You must have Zend Server installed with Zend Monitor enabled in order to use this handler' + ); + } + //zend monitor constants are not defined if zend monitor is not enabled. + $this->levelMap = [ + Logger::DEBUG => \ZEND_MONITOR_EVENT_SEVERITY_INFO, + Logger::INFO => \ZEND_MONITOR_EVENT_SEVERITY_INFO, + Logger::NOTICE => \ZEND_MONITOR_EVENT_SEVERITY_INFO, + Logger::WARNING => \ZEND_MONITOR_EVENT_SEVERITY_WARNING, + Logger::ERROR => \ZEND_MONITOR_EVENT_SEVERITY_ERROR, + Logger::CRITICAL => \ZEND_MONITOR_EVENT_SEVERITY_ERROR, + Logger::ALERT => \ZEND_MONITOR_EVENT_SEVERITY_ERROR, + Logger::EMERGENCY => \ZEND_MONITOR_EVENT_SEVERITY_ERROR, + ]; + parent::__construct($level, $bubble); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $this->writeZendMonitorCustomEvent( + Logger::getLevelName($record['level']), + $record['message'], + $record['formatted'], + $this->levelMap[$record['level']] + ); + } + + /** + * Write to Zend Monitor Events + * @param string $type Text displayed in "Class Name (custom)" field + * @param string $message Text displayed in "Error String" + * @param array $formatted Displayed in Custom Variables tab + * @param int $severity Set the event severity level (-1,0,1) + * + * @phpstan-param FormattedRecord $formatted + */ + protected function writeZendMonitorCustomEvent(string $type, string $message, array $formatted, int $severity): void + { + zend_monitor_custom_event($type, $message, $formatted, $severity); + } + + /** + * {@inheritDoc} + */ + public function getDefaultFormatter(): FormatterInterface + { + return new NormalizerFormatter(); + } + + /** + * @return array + */ + public function getLevelMap(): array + { + return $this->levelMap; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/LogRecord.php b/vendor/monolog/monolog/src/Monolog/LogRecord.php new file mode 100644 index 0000000..702807d --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/LogRecord.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog; + +use ArrayAccess; + +/** + * Monolog log record interface for forward compatibility with Monolog 3.0 + * + * This is just present in Monolog 2.4+ to allow interoperable code to be written against + * both versions by type-hinting arguments as `array|\Monolog\LogRecord $record` + * + * Do not rely on this interface for other purposes, and do not implement it. + * + * @author Jordi Boggiano + * @template-extends \ArrayAccess<'message'|'level'|'context'|'level_name'|'channel'|'datetime'|'extra'|'formatted', mixed> + * @phpstan-import-type Record from Logger + */ +interface LogRecord extends \ArrayAccess +{ + /** + * @phpstan-return Record + */ + public function toArray(): array; +} diff --git a/vendor/monolog/monolog/src/Monolog/Logger.php b/vendor/monolog/monolog/src/Monolog/Logger.php new file mode 100644 index 0000000..3c588a7 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Logger.php @@ -0,0 +1,761 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog; + +use DateTimeZone; +use Monolog\Handler\HandlerInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\InvalidArgumentException; +use Psr\Log\LogLevel; +use Throwable; +use Stringable; + +/** + * Monolog log channel + * + * It contains a stack of Handlers and a stack of Processors, + * and uses them to store records that are added to it. + * + * @author Jordi Boggiano + * + * @phpstan-type Level Logger::DEBUG|Logger::INFO|Logger::NOTICE|Logger::WARNING|Logger::ERROR|Logger::CRITICAL|Logger::ALERT|Logger::EMERGENCY + * @phpstan-type LevelName 'DEBUG'|'INFO'|'NOTICE'|'WARNING'|'ERROR'|'CRITICAL'|'ALERT'|'EMERGENCY' + * @phpstan-type Record array{message: string, context: mixed[], level: Level, level_name: LevelName, channel: string, datetime: \DateTimeImmutable, extra: mixed[]} + */ +class Logger implements LoggerInterface, ResettableInterface +{ + /** + * Detailed debug information + */ + public const DEBUG = 100; + + /** + * Interesting events + * + * Examples: User logs in, SQL logs. + */ + public const INFO = 200; + + /** + * Uncommon events + */ + public const NOTICE = 250; + + /** + * Exceptional occurrences that are not errors + * + * Examples: Use of deprecated APIs, poor use of an API, + * undesirable things that are not necessarily wrong. + */ + public const WARNING = 300; + + /** + * Runtime errors + */ + public const ERROR = 400; + + /** + * Critical conditions + * + * Example: Application component unavailable, unexpected exception. + */ + public const CRITICAL = 500; + + /** + * Action must be taken immediately + * + * Example: Entire website down, database unavailable, etc. + * This should trigger the SMS alerts and wake you up. + */ + public const ALERT = 550; + + /** + * Urgent alert. + */ + public const EMERGENCY = 600; + + /** + * Monolog API version + * + * This is only bumped when API breaks are done and should + * follow the major version of the library + * + * @var int + */ + public const API = 2; + + /** + * This is a static variable and not a constant to serve as an extension point for custom levels + * + * @var array $levels Logging levels with the levels as key + * + * @phpstan-var array $levels Logging levels with the levels as key + */ + protected static $levels = [ + self::DEBUG => 'DEBUG', + self::INFO => 'INFO', + self::NOTICE => 'NOTICE', + self::WARNING => 'WARNING', + self::ERROR => 'ERROR', + self::CRITICAL => 'CRITICAL', + self::ALERT => 'ALERT', + self::EMERGENCY => 'EMERGENCY', + ]; + + /** + * Mapping between levels numbers defined in RFC 5424 and Monolog ones + * + * @phpstan-var array $rfc_5424_levels + */ + private const RFC_5424_LEVELS = [ + 7 => self::DEBUG, + 6 => self::INFO, + 5 => self::NOTICE, + 4 => self::WARNING, + 3 => self::ERROR, + 2 => self::CRITICAL, + 1 => self::ALERT, + 0 => self::EMERGENCY, + ]; + + /** + * @var string + */ + protected $name; + + /** + * The handler stack + * + * @var HandlerInterface[] + */ + protected $handlers; + + /** + * Processors that will process all log records + * + * To process records of a single handler instead, add the processor on that specific handler + * + * @var callable[] + */ + protected $processors; + + /** + * @var bool + */ + protected $microsecondTimestamps = true; + + /** + * @var DateTimeZone + */ + protected $timezone; + + /** + * @var callable|null + */ + protected $exceptionHandler; + + /** + * @var int Keeps track of depth to prevent infinite logging loops + */ + private $logDepth = 0; + + /** + * @var \WeakMap<\Fiber, int>|null Keeps track of depth inside fibers to prevent infinite logging loops + */ + private $fiberLogDepth; + + /** + * @var bool Whether to detect infinite logging loops + * + * This can be disabled via {@see useLoggingLoopDetection} if you have async handlers that do not play well with this + */ + private $detectCycles = true; + + /** + * @psalm-param array $processors + * + * @param string $name The logging channel, a simple descriptive name that is attached to all log records + * @param HandlerInterface[] $handlers Optional stack of handlers, the first one in the array is called first, etc. + * @param callable[] $processors Optional array of processors + * @param DateTimeZone|null $timezone Optional timezone, if not provided date_default_timezone_get() will be used + */ + public function __construct(string $name, array $handlers = [], array $processors = [], ?DateTimeZone $timezone = null) + { + $this->name = $name; + $this->setHandlers($handlers); + $this->processors = $processors; + $this->timezone = $timezone ?: new DateTimeZone(date_default_timezone_get() ?: 'UTC'); + + if (\PHP_VERSION_ID >= 80100) { + // Local variable for phpstan, see https://github.com/phpstan/phpstan/issues/6732#issuecomment-1111118412 + /** @var \WeakMap<\Fiber, int> $fiberLogDepth */ + $fiberLogDepth = new \WeakMap(); + $this->fiberLogDepth = $fiberLogDepth; + } + } + + public function getName(): string + { + return $this->name; + } + + /** + * Return a new cloned instance with the name changed + */ + public function withName(string $name): self + { + $new = clone $this; + $new->name = $name; + + return $new; + } + + /** + * Pushes a handler on to the stack. + */ + public function pushHandler(HandlerInterface $handler): self + { + array_unshift($this->handlers, $handler); + + return $this; + } + + /** + * Pops a handler from the stack + * + * @throws \LogicException If empty handler stack + */ + public function popHandler(): HandlerInterface + { + if (!$this->handlers) { + throw new \LogicException('You tried to pop from an empty handler stack.'); + } + + return array_shift($this->handlers); + } + + /** + * Set handlers, replacing all existing ones. + * + * If a map is passed, keys will be ignored. + * + * @param HandlerInterface[] $handlers + */ + public function setHandlers(array $handlers): self + { + $this->handlers = []; + foreach (array_reverse($handlers) as $handler) { + $this->pushHandler($handler); + } + + return $this; + } + + /** + * @return HandlerInterface[] + */ + public function getHandlers(): array + { + return $this->handlers; + } + + /** + * Adds a processor on to the stack. + */ + public function pushProcessor(callable $callback): self + { + array_unshift($this->processors, $callback); + + return $this; + } + + /** + * Removes the processor on top of the stack and returns it. + * + * @throws \LogicException If empty processor stack + * @return callable + */ + public function popProcessor(): callable + { + if (!$this->processors) { + throw new \LogicException('You tried to pop from an empty processor stack.'); + } + + return array_shift($this->processors); + } + + /** + * @return callable[] + */ + public function getProcessors(): array + { + return $this->processors; + } + + /** + * Control the use of microsecond resolution timestamps in the 'datetime' + * member of new records. + * + * As of PHP7.1 microseconds are always included by the engine, so + * there is no performance penalty and Monolog 2 enabled microseconds + * by default. This function lets you disable them though in case you want + * to suppress microseconds from the output. + * + * @param bool $micro True to use microtime() to create timestamps + */ + public function useMicrosecondTimestamps(bool $micro): self + { + $this->microsecondTimestamps = $micro; + + return $this; + } + + public function useLoggingLoopDetection(bool $detectCycles): self + { + $this->detectCycles = $detectCycles; + + return $this; + } + + /** + * Adds a log record. + * + * @param int $level The logging level (a Monolog or RFC 5424 level) + * @param string $message The log message + * @param mixed[] $context The log context + * @param DateTimeImmutable $datetime Optional log date to log into the past or future + * @return bool Whether the record has been processed + * + * @phpstan-param Level $level + */ + public function addRecord(int $level, string $message, array $context = [], ?DateTimeImmutable $datetime = null): bool + { + if (isset(self::RFC_5424_LEVELS[$level])) { + $level = self::RFC_5424_LEVELS[$level]; + } + + if ($this->detectCycles) { + if (\PHP_VERSION_ID >= 80100 && $fiber = \Fiber::getCurrent()) { + $this->fiberLogDepth[$fiber] = $this->fiberLogDepth[$fiber] ?? 0; + $logDepth = ++$this->fiberLogDepth[$fiber]; + } else { + $logDepth = ++$this->logDepth; + } + } else { + $logDepth = 0; + } + + if ($logDepth === 3) { + $this->warning('A possible infinite logging loop was detected and aborted. It appears some of your handler code is triggering logging, see the previous log record for a hint as to what may be the cause.'); + return false; + } elseif ($logDepth >= 5) { // log depth 4 is let through, so we can log the warning above + return false; + } + + try { + $record = null; + + foreach ($this->handlers as $handler) { + if (null === $record) { + // skip creating the record as long as no handler is going to handle it + if (!$handler->isHandling(['level' => $level])) { + continue; + } + + $levelName = static::getLevelName($level); + + $record = [ + 'message' => $message, + 'context' => $context, + 'level' => $level, + 'level_name' => $levelName, + 'channel' => $this->name, + 'datetime' => $datetime ?? new DateTimeImmutable($this->microsecondTimestamps, $this->timezone), + 'extra' => [], + ]; + + try { + foreach ($this->processors as $processor) { + $record = $processor($record); + } + } catch (Throwable $e) { + $this->handleException($e, $record); + + return true; + } + } + + // once the record exists, send it to all handlers as long as the bubbling chain is not interrupted + try { + if (true === $handler->handle($record)) { + break; + } + } catch (Throwable $e) { + $this->handleException($e, $record); + + return true; + } + } + } finally { + if ($this->detectCycles) { + if (isset($fiber)) { + $this->fiberLogDepth[$fiber]--; + } else { + $this->logDepth--; + } + } + } + + return null !== $record; + } + + /** + * Ends a log cycle and frees all resources used by handlers. + * + * Closing a Handler means flushing all buffers and freeing any open resources/handles. + * Handlers that have been closed should be able to accept log records again and re-open + * themselves on demand, but this may not always be possible depending on implementation. + * + * This is useful at the end of a request and will be called automatically on every handler + * when they get destructed. + */ + public function close(): void + { + foreach ($this->handlers as $handler) { + $handler->close(); + } + } + + /** + * Ends a log cycle and resets all handlers and processors to their initial state. + * + * Resetting a Handler or a Processor means flushing/cleaning all buffers, resetting internal + * state, and getting it back to a state in which it can receive log records again. + * + * This is useful in case you want to avoid logs leaking between two requests or jobs when you + * have a long running process like a worker or an application server serving multiple requests + * in one process. + */ + public function reset(): void + { + foreach ($this->handlers as $handler) { + if ($handler instanceof ResettableInterface) { + $handler->reset(); + } + } + + foreach ($this->processors as $processor) { + if ($processor instanceof ResettableInterface) { + $processor->reset(); + } + } + } + + /** + * Gets all supported logging levels. + * + * @return array Assoc array with human-readable level names => level codes. + * @phpstan-return array + */ + public static function getLevels(): array + { + return array_flip(static::$levels); + } + + /** + * Gets the name of the logging level. + * + * @throws \Psr\Log\InvalidArgumentException If level is not defined + * + * @phpstan-param Level $level + * @phpstan-return LevelName + */ + public static function getLevelName(int $level): string + { + if (!isset(static::$levels[$level])) { + throw new InvalidArgumentException('Level "'.$level.'" is not defined, use one of: '.implode(', ', array_keys(static::$levels))); + } + + return static::$levels[$level]; + } + + /** + * Converts PSR-3 levels to Monolog ones if necessary + * + * @param string|int $level Level number (monolog) or name (PSR-3) + * @throws \Psr\Log\InvalidArgumentException If level is not defined + * + * @phpstan-param Level|LevelName|LogLevel::* $level + * @phpstan-return Level + */ + public static function toMonologLevel($level): int + { + if (is_string($level)) { + if (is_numeric($level)) { + /** @phpstan-ignore-next-line */ + return intval($level); + } + + // Contains chars of all log levels and avoids using strtoupper() which may have + // strange results depending on locale (for example, "i" will become "İ" in Turkish locale) + $upper = strtr($level, 'abcdefgilmnortuwy', 'ABCDEFGILMNORTUWY'); + if (defined(__CLASS__.'::'.$upper)) { + return constant(__CLASS__ . '::' . $upper); + } + + throw new InvalidArgumentException('Level "'.$level.'" is not defined, use one of: '.implode(', ', array_keys(static::$levels) + static::$levels)); + } + + if (!is_int($level)) { + throw new InvalidArgumentException('Level "'.var_export($level, true).'" is not defined, use one of: '.implode(', ', array_keys(static::$levels) + static::$levels)); + } + + return $level; + } + + /** + * Checks whether the Logger has a handler that listens on the given level + * + * @phpstan-param Level $level + */ + public function isHandling(int $level): bool + { + $record = [ + 'level' => $level, + ]; + + foreach ($this->handlers as $handler) { + if ($handler->isHandling($record)) { + return true; + } + } + + return false; + } + + /** + * Set a custom exception handler that will be called if adding a new record fails + * + * The callable will receive an exception object and the record that failed to be logged + */ + public function setExceptionHandler(?callable $callback): self + { + $this->exceptionHandler = $callback; + + return $this; + } + + public function getExceptionHandler(): ?callable + { + return $this->exceptionHandler; + } + + /** + * Adds a log record at an arbitrary level. + * + * This method allows for compatibility with common interfaces. + * + * @param mixed $level The log level (a Monolog, PSR-3 or RFC 5424 level) + * @param string|Stringable $message The log message + * @param mixed[] $context The log context + * + * @phpstan-param Level|LevelName|LogLevel::* $level + */ + public function log($level, $message, array $context = []): void + { + if (!is_int($level) && !is_string($level)) { + throw new \InvalidArgumentException('$level is expected to be a string or int'); + } + + if (isset(self::RFC_5424_LEVELS[$level])) { + $level = self::RFC_5424_LEVELS[$level]; + } + + $level = static::toMonologLevel($level); + + $this->addRecord($level, (string) $message, $context); + } + + /** + * Adds a log record at the DEBUG level. + * + * This method allows for compatibility with common interfaces. + * + * @param string|Stringable $message The log message + * @param mixed[] $context The log context + */ + public function debug($message, array $context = []): void + { + $this->addRecord(static::DEBUG, (string) $message, $context); + } + + /** + * Adds a log record at the INFO level. + * + * This method allows for compatibility with common interfaces. + * + * @param string|Stringable $message The log message + * @param mixed[] $context The log context + */ + public function info($message, array $context = []): void + { + $this->addRecord(static::INFO, (string) $message, $context); + } + + /** + * Adds a log record at the NOTICE level. + * + * This method allows for compatibility with common interfaces. + * + * @param string|Stringable $message The log message + * @param mixed[] $context The log context + */ + public function notice($message, array $context = []): void + { + $this->addRecord(static::NOTICE, (string) $message, $context); + } + + /** + * Adds a log record at the WARNING level. + * + * This method allows for compatibility with common interfaces. + * + * @param string|Stringable $message The log message + * @param mixed[] $context The log context + */ + public function warning($message, array $context = []): void + { + $this->addRecord(static::WARNING, (string) $message, $context); + } + + /** + * Adds a log record at the ERROR level. + * + * This method allows for compatibility with common interfaces. + * + * @param string|Stringable $message The log message + * @param mixed[] $context The log context + */ + public function error($message, array $context = []): void + { + $this->addRecord(static::ERROR, (string) $message, $context); + } + + /** + * Adds a log record at the CRITICAL level. + * + * This method allows for compatibility with common interfaces. + * + * @param string|Stringable $message The log message + * @param mixed[] $context The log context + */ + public function critical($message, array $context = []): void + { + $this->addRecord(static::CRITICAL, (string) $message, $context); + } + + /** + * Adds a log record at the ALERT level. + * + * This method allows for compatibility with common interfaces. + * + * @param string|Stringable $message The log message + * @param mixed[] $context The log context + */ + public function alert($message, array $context = []): void + { + $this->addRecord(static::ALERT, (string) $message, $context); + } + + /** + * Adds a log record at the EMERGENCY level. + * + * This method allows for compatibility with common interfaces. + * + * @param string|Stringable $message The log message + * @param mixed[] $context The log context + */ + public function emergency($message, array $context = []): void + { + $this->addRecord(static::EMERGENCY, (string) $message, $context); + } + + /** + * Sets the timezone to be used for the timestamp of log records. + */ + public function setTimezone(DateTimeZone $tz): self + { + $this->timezone = $tz; + + return $this; + } + + /** + * Returns the timezone to be used for the timestamp of log records. + */ + public function getTimezone(): DateTimeZone + { + return $this->timezone; + } + + /** + * Delegates exception management to the custom exception handler, + * or throws the exception if no custom handler is set. + * + * @param array $record + * @phpstan-param Record $record + */ + protected function handleException(Throwable $e, array $record): void + { + if (!$this->exceptionHandler) { + throw $e; + } + + ($this->exceptionHandler)($e, $record); + } + + /** + * @return array + */ + public function __serialize(): array + { + return [ + 'name' => $this->name, + 'handlers' => $this->handlers, + 'processors' => $this->processors, + 'microsecondTimestamps' => $this->microsecondTimestamps, + 'timezone' => $this->timezone, + 'exceptionHandler' => $this->exceptionHandler, + 'logDepth' => $this->logDepth, + 'detectCycles' => $this->detectCycles, + ]; + } + + /** + * @param array $data + */ + public function __unserialize(array $data): void + { + foreach (['name', 'handlers', 'processors', 'microsecondTimestamps', 'timezone', 'exceptionHandler', 'logDepth', 'detectCycles'] as $property) { + if (isset($data[$property])) { + $this->$property = $data[$property]; + } + } + + if (\PHP_VERSION_ID >= 80100) { + // Local variable for phpstan, see https://github.com/phpstan/phpstan/issues/6732#issuecomment-1111118412 + /** @var \WeakMap<\Fiber, int> $fiberLogDepth */ + $fiberLogDepth = new \WeakMap(); + $this->fiberLogDepth = $fiberLogDepth; + } + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Processor/GitProcessor.php b/vendor/monolog/monolog/src/Monolog/Processor/GitProcessor.php new file mode 100644 index 0000000..8166bdc --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Processor/GitProcessor.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +use Monolog\Logger; +use Psr\Log\LogLevel; + +/** + * Injects Git branch and Git commit SHA in all records + * + * @author Nick Otter + * @author Jordi Boggiano + * + * @phpstan-import-type Level from \Monolog\Logger + * @phpstan-import-type LevelName from \Monolog\Logger + */ +class GitProcessor implements ProcessorInterface +{ + /** @var int */ + private $level; + /** @var array{branch: string, commit: string}|array|null */ + private static $cache = null; + + /** + * @param string|int $level The minimum logging level at which this Processor will be triggered + * + * @phpstan-param Level|LevelName|LogLevel::* $level + */ + public function __construct($level = Logger::DEBUG) + { + $this->level = Logger::toMonologLevel($level); + } + + /** + * {@inheritDoc} + */ + public function __invoke(array $record): array + { + // return if the level is not high enough + if ($record['level'] < $this->level) { + return $record; + } + + $record['extra']['git'] = self::getGitInfo(); + + return $record; + } + + /** + * @return array{branch: string, commit: string}|array + */ + private static function getGitInfo(): array + { + if (self::$cache) { + return self::$cache; + } + + $branches = `git branch -v --no-abbrev`; + if ($branches && preg_match('{^\* (.+?)\s+([a-f0-9]{40})(?:\s|$)}m', $branches, $matches)) { + return self::$cache = [ + 'branch' => $matches[1], + 'commit' => $matches[2], + ]; + } + + return self::$cache = []; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Processor/HostnameProcessor.php b/vendor/monolog/monolog/src/Monolog/Processor/HostnameProcessor.php new file mode 100644 index 0000000..91fda7d --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Processor/HostnameProcessor.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +/** + * Injects value of gethostname in all records + */ +class HostnameProcessor implements ProcessorInterface +{ + /** @var string */ + private static $host; + + public function __construct() + { + self::$host = (string) gethostname(); + } + + /** + * {@inheritDoc} + */ + public function __invoke(array $record): array + { + $record['extra']['hostname'] = self::$host; + + return $record; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Processor/IntrospectionProcessor.php b/vendor/monolog/monolog/src/Monolog/Processor/IntrospectionProcessor.php new file mode 100644 index 0000000..a32e76b --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Processor/IntrospectionProcessor.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +use Monolog\Logger; +use Psr\Log\LogLevel; + +/** + * Injects line/file:class/function where the log message came from + * + * Warning: This only works if the handler processes the logs directly. + * If you put the processor on a handler that is behind a FingersCrossedHandler + * for example, the processor will only be called once the trigger level is reached, + * and all the log records will have the same file/line/.. data from the call that + * triggered the FingersCrossedHandler. + * + * @author Jordi Boggiano + * + * @phpstan-import-type Level from \Monolog\Logger + * @phpstan-import-type LevelName from \Monolog\Logger + */ +class IntrospectionProcessor implements ProcessorInterface +{ + /** @var int */ + private $level; + /** @var string[] */ + private $skipClassesPartials; + /** @var int */ + private $skipStackFramesCount; + /** @var string[] */ + private $skipFunctions = [ + 'call_user_func', + 'call_user_func_array', + ]; + + /** + * @param string|int $level The minimum logging level at which this Processor will be triggered + * @param string[] $skipClassesPartials + * + * @phpstan-param Level|LevelName|LogLevel::* $level + */ + public function __construct($level = Logger::DEBUG, array $skipClassesPartials = [], int $skipStackFramesCount = 0) + { + $this->level = Logger::toMonologLevel($level); + $this->skipClassesPartials = array_merge(['Monolog\\'], $skipClassesPartials); + $this->skipStackFramesCount = $skipStackFramesCount; + } + + /** + * {@inheritDoc} + */ + public function __invoke(array $record): array + { + // return if the level is not high enough + if ($record['level'] < $this->level) { + return $record; + } + + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + + // skip first since it's always the current method + array_shift($trace); + // the call_user_func call is also skipped + array_shift($trace); + + $i = 0; + + while ($this->isTraceClassOrSkippedFunction($trace, $i)) { + if (isset($trace[$i]['class'])) { + foreach ($this->skipClassesPartials as $part) { + if (strpos($trace[$i]['class'], $part) !== false) { + $i++; + + continue 2; + } + } + } elseif (in_array($trace[$i]['function'], $this->skipFunctions)) { + $i++; + + continue; + } + + break; + } + + $i += $this->skipStackFramesCount; + + // we should have the call source now + $record['extra'] = array_merge( + $record['extra'], + [ + 'file' => isset($trace[$i - 1]['file']) ? $trace[$i - 1]['file'] : null, + 'line' => isset($trace[$i - 1]['line']) ? $trace[$i - 1]['line'] : null, + 'class' => isset($trace[$i]['class']) ? $trace[$i]['class'] : null, + 'callType' => isset($trace[$i]['type']) ? $trace[$i]['type'] : null, + 'function' => isset($trace[$i]['function']) ? $trace[$i]['function'] : null, + ] + ); + + return $record; + } + + /** + * @param array[] $trace + */ + private function isTraceClassOrSkippedFunction(array $trace, int $index): bool + { + if (!isset($trace[$index])) { + return false; + } + + return isset($trace[$index]['class']) || in_array($trace[$index]['function'], $this->skipFunctions); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Processor/MemoryPeakUsageProcessor.php b/vendor/monolog/monolog/src/Monolog/Processor/MemoryPeakUsageProcessor.php new file mode 100644 index 0000000..37c756f --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Processor/MemoryPeakUsageProcessor.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +/** + * Injects memory_get_peak_usage in all records + * + * @see Monolog\Processor\MemoryProcessor::__construct() for options + * @author Rob Jensen + */ +class MemoryPeakUsageProcessor extends MemoryProcessor +{ + /** + * {@inheritDoc} + */ + public function __invoke(array $record): array + { + $usage = memory_get_peak_usage($this->realUsage); + + if ($this->useFormatting) { + $usage = $this->formatBytes($usage); + } + + $record['extra']['memory_peak_usage'] = $usage; + + return $record; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Processor/MemoryProcessor.php b/vendor/monolog/monolog/src/Monolog/Processor/MemoryProcessor.php new file mode 100644 index 0000000..227deb7 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Processor/MemoryProcessor.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +/** + * Some methods that are common for all memory processors + * + * @author Rob Jensen + */ +abstract class MemoryProcessor implements ProcessorInterface +{ + /** + * @var bool If true, get the real size of memory allocated from system. Else, only the memory used by emalloc() is reported. + */ + protected $realUsage; + + /** + * @var bool If true, then format memory size to human readable string (MB, KB, B depending on size) + */ + protected $useFormatting; + + /** + * @param bool $realUsage Set this to true to get the real size of memory allocated from system. + * @param bool $useFormatting If true, then format memory size to human readable string (MB, KB, B depending on size) + */ + public function __construct(bool $realUsage = true, bool $useFormatting = true) + { + $this->realUsage = $realUsage; + $this->useFormatting = $useFormatting; + } + + /** + * Formats bytes into a human readable string if $this->useFormatting is true, otherwise return $bytes as is + * + * @param int $bytes + * @return string|int Formatted string if $this->useFormatting is true, otherwise return $bytes as int + */ + protected function formatBytes(int $bytes) + { + if (!$this->useFormatting) { + return $bytes; + } + + if ($bytes > 1024 * 1024) { + return round($bytes / 1024 / 1024, 2).' MB'; + } elseif ($bytes > 1024) { + return round($bytes / 1024, 2).' KB'; + } + + return $bytes . ' B'; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Processor/MemoryUsageProcessor.php b/vendor/monolog/monolog/src/Monolog/Processor/MemoryUsageProcessor.php new file mode 100644 index 0000000..e141921 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Processor/MemoryUsageProcessor.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +/** + * Injects memory_get_usage in all records + * + * @see Monolog\Processor\MemoryProcessor::__construct() for options + * @author Rob Jensen + */ +class MemoryUsageProcessor extends MemoryProcessor +{ + /** + * {@inheritDoc} + */ + public function __invoke(array $record): array + { + $usage = memory_get_usage($this->realUsage); + + if ($this->useFormatting) { + $usage = $this->formatBytes($usage); + } + + $record['extra']['memory_usage'] = $usage; + + return $record; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Processor/MercurialProcessor.php b/vendor/monolog/monolog/src/Monolog/Processor/MercurialProcessor.php new file mode 100644 index 0000000..d4a628f --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Processor/MercurialProcessor.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +use Monolog\Logger; +use Psr\Log\LogLevel; + +/** + * Injects Hg branch and Hg revision number in all records + * + * @author Jonathan A. Schweder + * + * @phpstan-import-type LevelName from \Monolog\Logger + * @phpstan-import-type Level from \Monolog\Logger + */ +class MercurialProcessor implements ProcessorInterface +{ + /** @var Level */ + private $level; + /** @var array{branch: string, revision: string}|array|null */ + private static $cache = null; + + /** + * @param int|string $level The minimum logging level at which this Processor will be triggered + * + * @phpstan-param Level|LevelName|LogLevel::* $level + */ + public function __construct($level = Logger::DEBUG) + { + $this->level = Logger::toMonologLevel($level); + } + + /** + * {@inheritDoc} + */ + public function __invoke(array $record): array + { + // return if the level is not high enough + if ($record['level'] < $this->level) { + return $record; + } + + $record['extra']['hg'] = self::getMercurialInfo(); + + return $record; + } + + /** + * @return array{branch: string, revision: string}|array + */ + private static function getMercurialInfo(): array + { + if (self::$cache) { + return self::$cache; + } + + $result = explode(' ', trim(`hg id -nb`)); + + if (count($result) >= 3) { + return self::$cache = [ + 'branch' => $result[1], + 'revision' => $result[2], + ]; + } + + return self::$cache = []; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Processor/ProcessIdProcessor.php b/vendor/monolog/monolog/src/Monolog/Processor/ProcessIdProcessor.php new file mode 100644 index 0000000..3b939a9 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Processor/ProcessIdProcessor.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +/** + * Adds value of getmypid into records + * + * @author Andreas Hörnicke + */ +class ProcessIdProcessor implements ProcessorInterface +{ + /** + * {@inheritDoc} + */ + public function __invoke(array $record): array + { + $record['extra']['process_id'] = getmypid(); + + return $record; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Processor/ProcessorInterface.php b/vendor/monolog/monolog/src/Monolog/Processor/ProcessorInterface.php new file mode 100644 index 0000000..5defb7e --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Processor/ProcessorInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +/** + * An optional interface to allow labelling Monolog processors. + * + * @author Nicolas Grekas + * + * @phpstan-import-type Record from \Monolog\Logger + */ +interface ProcessorInterface +{ + /** + * @return array The processed record + * + * @phpstan-param Record $record + * @phpstan-return Record + */ + public function __invoke(array $record); +} diff --git a/vendor/monolog/monolog/src/Monolog/Processor/PsrLogMessageProcessor.php b/vendor/monolog/monolog/src/Monolog/Processor/PsrLogMessageProcessor.php new file mode 100644 index 0000000..e7c1217 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Processor/PsrLogMessageProcessor.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +use Monolog\Utils; + +/** + * Processes a record's message according to PSR-3 rules + * + * It replaces {foo} with the value from $context['foo'] + * + * @author Jordi Boggiano + */ +class PsrLogMessageProcessor implements ProcessorInterface +{ + public const SIMPLE_DATE = "Y-m-d\TH:i:s.uP"; + + /** @var string|null */ + private $dateFormat; + + /** @var bool */ + private $removeUsedContextFields; + + /** + * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format + * @param bool $removeUsedContextFields If set to true the fields interpolated into message gets unset + */ + public function __construct(?string $dateFormat = null, bool $removeUsedContextFields = false) + { + $this->dateFormat = $dateFormat; + $this->removeUsedContextFields = $removeUsedContextFields; + } + + /** + * {@inheritDoc} + */ + public function __invoke(array $record): array + { + if (false === strpos($record['message'], '{')) { + return $record; + } + + $replacements = []; + foreach ($record['context'] as $key => $val) { + $placeholder = '{' . $key . '}'; + if (strpos($record['message'], $placeholder) === false) { + continue; + } + + if (is_null($val) || is_scalar($val) || (is_object($val) && method_exists($val, "__toString"))) { + $replacements[$placeholder] = $val; + } elseif ($val instanceof \DateTimeInterface) { + if (!$this->dateFormat && $val instanceof \Monolog\DateTimeImmutable) { + // handle monolog dates using __toString if no specific dateFormat was asked for + // so that it follows the useMicroseconds flag + $replacements[$placeholder] = (string) $val; + } else { + $replacements[$placeholder] = $val->format($this->dateFormat ?: static::SIMPLE_DATE); + } + } elseif ($val instanceof \UnitEnum) { + $replacements[$placeholder] = $val instanceof \BackedEnum ? $val->value : $val->name; + } elseif (is_object($val)) { + $replacements[$placeholder] = '[object '.Utils::getClass($val).']'; + } elseif (is_array($val)) { + $replacements[$placeholder] = 'array'.Utils::jsonEncode($val, null, true); + } else { + $replacements[$placeholder] = '['.gettype($val).']'; + } + + if ($this->removeUsedContextFields) { + unset($record['context'][$key]); + } + } + + $record['message'] = strtr($record['message'], $replacements); + + return $record; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Processor/TagProcessor.php b/vendor/monolog/monolog/src/Monolog/Processor/TagProcessor.php new file mode 100644 index 0000000..80f1874 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Processor/TagProcessor.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +/** + * Adds a tags array into record + * + * @author Martijn Riemers + */ +class TagProcessor implements ProcessorInterface +{ + /** @var string[] */ + private $tags; + + /** + * @param string[] $tags + */ + public function __construct(array $tags = []) + { + $this->setTags($tags); + } + + /** + * @param string[] $tags + */ + public function addTags(array $tags = []): self + { + $this->tags = array_merge($this->tags, $tags); + + return $this; + } + + /** + * @param string[] $tags + */ + public function setTags(array $tags = []): self + { + $this->tags = $tags; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function __invoke(array $record): array + { + $record['extra']['tags'] = $this->tags; + + return $record; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Processor/UidProcessor.php b/vendor/monolog/monolog/src/Monolog/Processor/UidProcessor.php new file mode 100644 index 0000000..a27b74d --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Processor/UidProcessor.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +use Monolog\ResettableInterface; + +/** + * Adds a unique identifier into records + * + * @author Simon Mönch + */ +class UidProcessor implements ProcessorInterface, ResettableInterface +{ + /** @var string */ + private $uid; + + public function __construct(int $length = 7) + { + if ($length > 32 || $length < 1) { + throw new \InvalidArgumentException('The uid length must be an integer between 1 and 32'); + } + + $this->uid = $this->generateUid($length); + } + + /** + * {@inheritDoc} + */ + public function __invoke(array $record): array + { + $record['extra']['uid'] = $this->uid; + + return $record; + } + + public function getUid(): string + { + return $this->uid; + } + + public function reset() + { + $this->uid = $this->generateUid(strlen($this->uid)); + } + + private function generateUid(int $length): string + { + return substr(bin2hex(random_bytes((int) ceil($length / 2))), 0, $length); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Processor/WebProcessor.php b/vendor/monolog/monolog/src/Monolog/Processor/WebProcessor.php new file mode 100644 index 0000000..887f4d3 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Processor/WebProcessor.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +/** + * Injects url/method and remote IP of the current web request in all records + * + * @author Jordi Boggiano + */ +class WebProcessor implements ProcessorInterface +{ + /** + * @var array|\ArrayAccess + */ + protected $serverData; + + /** + * Default fields + * + * Array is structured as [key in record.extra => key in $serverData] + * + * @var array + */ + protected $extraFields = [ + 'url' => 'REQUEST_URI', + 'ip' => 'REMOTE_ADDR', + 'http_method' => 'REQUEST_METHOD', + 'server' => 'SERVER_NAME', + 'referrer' => 'HTTP_REFERER', + 'user_agent' => 'HTTP_USER_AGENT', + ]; + + /** + * @param array|\ArrayAccess|null $serverData Array or object w/ ArrayAccess that provides access to the $_SERVER data + * @param array|array|null $extraFields Field names and the related key inside $serverData to be added (or just a list of field names to use the default configured $serverData mapping). If not provided it defaults to: [url, ip, http_method, server, referrer] + unique_id if present in server data + */ + public function __construct($serverData = null, ?array $extraFields = null) + { + if (null === $serverData) { + $this->serverData = &$_SERVER; + } elseif (is_array($serverData) || $serverData instanceof \ArrayAccess) { + $this->serverData = $serverData; + } else { + throw new \UnexpectedValueException('$serverData must be an array or object implementing ArrayAccess.'); + } + + $defaultEnabled = ['url', 'ip', 'http_method', 'server', 'referrer']; + if (isset($this->serverData['UNIQUE_ID'])) { + $this->extraFields['unique_id'] = 'UNIQUE_ID'; + $defaultEnabled[] = 'unique_id'; + } + + if (null === $extraFields) { + $extraFields = $defaultEnabled; + } + if (isset($extraFields[0])) { + foreach (array_keys($this->extraFields) as $fieldName) { + if (!in_array($fieldName, $extraFields)) { + unset($this->extraFields[$fieldName]); + } + } + } else { + $this->extraFields = $extraFields; + } + } + + /** + * {@inheritDoc} + */ + public function __invoke(array $record): array + { + // skip processing if for some reason request data + // is not present (CLI or wonky SAPIs) + if (!isset($this->serverData['REQUEST_URI'])) { + return $record; + } + + $record['extra'] = $this->appendExtraFields($record['extra']); + + return $record; + } + + public function addExtraField(string $extraName, string $serverName): self + { + $this->extraFields[$extraName] = $serverName; + + return $this; + } + + /** + * @param mixed[] $extra + * @return mixed[] + */ + private function appendExtraFields(array $extra): array + { + foreach ($this->extraFields as $extraName => $serverName) { + $extra[$extraName] = $this->serverData[$serverName] ?? null; + } + + return $extra; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Registry.php b/vendor/monolog/monolog/src/Monolog/Registry.php new file mode 100644 index 0000000..ae94ae6 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Registry.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog; + +use InvalidArgumentException; + +/** + * Monolog log registry + * + * Allows to get `Logger` instances in the global scope + * via static method calls on this class. + * + * + * $application = new Monolog\Logger('application'); + * $api = new Monolog\Logger('api'); + * + * Monolog\Registry::addLogger($application); + * Monolog\Registry::addLogger($api); + * + * function testLogger() + * { + * Monolog\Registry::api()->error('Sent to $api Logger instance'); + * Monolog\Registry::application()->error('Sent to $application Logger instance'); + * } + * + * + * @author Tomas Tatarko + */ +class Registry +{ + /** + * List of all loggers in the registry (by named indexes) + * + * @var Logger[] + */ + private static $loggers = []; + + /** + * Adds new logging channel to the registry + * + * @param Logger $logger Instance of the logging channel + * @param string|null $name Name of the logging channel ($logger->getName() by default) + * @param bool $overwrite Overwrite instance in the registry if the given name already exists? + * @throws \InvalidArgumentException If $overwrite set to false and named Logger instance already exists + * @return void + */ + public static function addLogger(Logger $logger, ?string $name = null, bool $overwrite = false) + { + $name = $name ?: $logger->getName(); + + if (isset(self::$loggers[$name]) && !$overwrite) { + throw new InvalidArgumentException('Logger with the given name already exists'); + } + + self::$loggers[$name] = $logger; + } + + /** + * Checks if such logging channel exists by name or instance + * + * @param string|Logger $logger Name or logger instance + */ + public static function hasLogger($logger): bool + { + if ($logger instanceof Logger) { + $index = array_search($logger, self::$loggers, true); + + return false !== $index; + } + + return isset(self::$loggers[$logger]); + } + + /** + * Removes instance from registry by name or instance + * + * @param string|Logger $logger Name or logger instance + */ + public static function removeLogger($logger): void + { + if ($logger instanceof Logger) { + if (false !== ($idx = array_search($logger, self::$loggers, true))) { + unset(self::$loggers[$idx]); + } + } else { + unset(self::$loggers[$logger]); + } + } + + /** + * Clears the registry + */ + public static function clear(): void + { + self::$loggers = []; + } + + /** + * Gets Logger instance from the registry + * + * @param string $name Name of the requested Logger instance + * @throws \InvalidArgumentException If named Logger instance is not in the registry + */ + public static function getInstance($name): Logger + { + if (!isset(self::$loggers[$name])) { + throw new InvalidArgumentException(sprintf('Requested "%s" logger instance is not in the registry', $name)); + } + + return self::$loggers[$name]; + } + + /** + * Gets Logger instance from the registry via static method call + * + * @param string $name Name of the requested Logger instance + * @param mixed[] $arguments Arguments passed to static method call + * @throws \InvalidArgumentException If named Logger instance is not in the registry + * @return Logger Requested instance of Logger + */ + public static function __callStatic($name, $arguments) + { + return self::getInstance($name); + } +} diff --git a/vendor/monolog/monolog/src/Monolog/ResettableInterface.php b/vendor/monolog/monolog/src/Monolog/ResettableInterface.php new file mode 100644 index 0000000..2c5fd78 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/ResettableInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog; + +/** + * Handler or Processor implementing this interface will be reset when Logger::reset() is called. + * + * Resetting ends a log cycle gets them back to their initial state. + * + * Resetting a Handler or a Processor means flushing/cleaning all buffers, resetting internal + * state, and getting it back to a state in which it can receive log records again. + * + * This is useful in case you want to avoid logs leaking between two requests or jobs when you + * have a long running process like a worker or an application server serving multiple requests + * in one process. + * + * @author Grégoire Pineau + */ +interface ResettableInterface +{ + /** + * @return void + */ + public function reset(); +} diff --git a/vendor/monolog/monolog/src/Monolog/SignalHandler.php b/vendor/monolog/monolog/src/Monolog/SignalHandler.php new file mode 100644 index 0000000..d730eea --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/SignalHandler.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog; + +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; +use ReflectionExtension; + +/** + * Monolog POSIX signal handler + * + * @author Robert Gust-Bardon + * + * @phpstan-import-type Level from \Monolog\Logger + * @phpstan-import-type LevelName from \Monolog\Logger + */ +class SignalHandler +{ + /** @var LoggerInterface */ + private $logger; + + /** @var array SIG_DFL, SIG_IGN or previous callable */ + private $previousSignalHandler = []; + /** @var array */ + private $signalLevelMap = []; + /** @var array */ + private $signalRestartSyscalls = []; + + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } + + /** + * @param int|string $level Level or level name + * @param bool $callPrevious + * @param bool $restartSyscalls + * @param bool|null $async + * @return $this + * + * @phpstan-param Level|LevelName|LogLevel::* $level + */ + public function registerSignalHandler(int $signo, $level = LogLevel::CRITICAL, bool $callPrevious = true, bool $restartSyscalls = true, ?bool $async = true): self + { + if (!extension_loaded('pcntl') || !function_exists('pcntl_signal')) { + return $this; + } + + $level = Logger::toMonologLevel($level); + + if ($callPrevious) { + $handler = pcntl_signal_get_handler($signo); + $this->previousSignalHandler[$signo] = $handler; + } else { + unset($this->previousSignalHandler[$signo]); + } + $this->signalLevelMap[$signo] = $level; + $this->signalRestartSyscalls[$signo] = $restartSyscalls; + + if ($async !== null) { + pcntl_async_signals($async); + } + + pcntl_signal($signo, [$this, 'handleSignal'], $restartSyscalls); + + return $this; + } + + /** + * @param mixed $siginfo + */ + public function handleSignal(int $signo, $siginfo = null): void + { + static $signals = []; + + if (!$signals && extension_loaded('pcntl')) { + $pcntl = new ReflectionExtension('pcntl'); + // HHVM 3.24.2 returns an empty array. + foreach ($pcntl->getConstants() ?: get_defined_constants(true)['Core'] as $name => $value) { + if (substr($name, 0, 3) === 'SIG' && $name[3] !== '_' && is_int($value)) { + $signals[$value] = $name; + } + } + } + + $level = $this->signalLevelMap[$signo] ?? LogLevel::CRITICAL; + $signal = $signals[$signo] ?? $signo; + $context = $siginfo ?? []; + $this->logger->log($level, sprintf('Program received signal %s', $signal), $context); + + if (!isset($this->previousSignalHandler[$signo])) { + return; + } + + if ($this->previousSignalHandler[$signo] === SIG_DFL) { + if (extension_loaded('pcntl') && function_exists('pcntl_signal') && function_exists('pcntl_sigprocmask') && function_exists('pcntl_signal_dispatch') + && extension_loaded('posix') && function_exists('posix_getpid') && function_exists('posix_kill') + ) { + $restartSyscalls = $this->signalRestartSyscalls[$signo] ?? true; + pcntl_signal($signo, SIG_DFL, $restartSyscalls); + pcntl_sigprocmask(SIG_UNBLOCK, [$signo], $oldset); + posix_kill(posix_getpid(), $signo); + pcntl_signal_dispatch(); + pcntl_sigprocmask(SIG_SETMASK, $oldset); + pcntl_signal($signo, [$this, 'handleSignal'], $restartSyscalls); + } + } elseif (is_callable($this->previousSignalHandler[$signo])) { + $this->previousSignalHandler[$signo]($signo, $siginfo); + } + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Test/TestCase.php b/vendor/monolog/monolog/src/Monolog/Test/TestCase.php new file mode 100644 index 0000000..bc0b425 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Test/TestCase.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Test; + +use Monolog\Logger; +use Monolog\DateTimeImmutable; +use Monolog\Formatter\FormatterInterface; + +/** + * Lets you easily generate log records and a dummy formatter for testing purposes + * + * @author Jordi Boggiano + * + * @phpstan-import-type Record from \Monolog\Logger + * @phpstan-import-type Level from \Monolog\Logger + * + * @internal feel free to reuse this to test your own handlers, this is marked internal to avoid issues with PHPStorm https://github.com/Seldaek/monolog/issues/1677 + */ +class TestCase extends \PHPUnit\Framework\TestCase +{ + public function tearDown(): void + { + parent::tearDown(); + + if (isset($this->handler)) { + unset($this->handler); + } + } + + /** + * @param mixed[] $context + * + * @return array Record + * + * @phpstan-param Level $level + * @phpstan-return Record + */ + protected function getRecord(int $level = Logger::WARNING, string $message = 'test', array $context = []): array + { + return [ + 'message' => (string) $message, + 'context' => $context, + 'level' => $level, + 'level_name' => Logger::getLevelName($level), + 'channel' => 'test', + 'datetime' => new DateTimeImmutable(true), + 'extra' => [], + ]; + } + + /** + * @phpstan-return Record[] + */ + protected function getMultipleRecords(): array + { + return [ + $this->getRecord(Logger::DEBUG, 'debug message 1'), + $this->getRecord(Logger::DEBUG, 'debug message 2'), + $this->getRecord(Logger::INFO, 'information'), + $this->getRecord(Logger::WARNING, 'warning'), + $this->getRecord(Logger::ERROR, 'error'), + ]; + } + + protected function getIdentityFormatter(): FormatterInterface + { + $formatter = $this->createMock(FormatterInterface::class); + $formatter->expects($this->any()) + ->method('format') + ->will($this->returnCallback(function ($record) { + return $record['message']; + })); + + return $formatter; + } +} diff --git a/vendor/monolog/monolog/src/Monolog/Utils.php b/vendor/monolog/monolog/src/Monolog/Utils.php new file mode 100644 index 0000000..360c421 --- /dev/null +++ b/vendor/monolog/monolog/src/Monolog/Utils.php @@ -0,0 +1,284 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog; + +final class Utils +{ + const DEFAULT_JSON_FLAGS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR; + + public static function getClass(object $object): string + { + $class = \get_class($object); + + if (false === ($pos = \strpos($class, "@anonymous\0"))) { + return $class; + } + + if (false === ($parent = \get_parent_class($class))) { + return \substr($class, 0, $pos + 10); + } + + return $parent . '@anonymous'; + } + + public static function substr(string $string, int $start, ?int $length = null): string + { + if (extension_loaded('mbstring')) { + return mb_strcut($string, $start, $length); + } + + return substr($string, $start, (null === $length) ? strlen($string) : $length); + } + + /** + * Makes sure if a relative path is passed in it is turned into an absolute path + * + * @param string $streamUrl stream URL or path without protocol + */ + public static function canonicalizePath(string $streamUrl): string + { + $prefix = ''; + if ('file://' === substr($streamUrl, 0, 7)) { + $streamUrl = substr($streamUrl, 7); + $prefix = 'file://'; + } + + // other type of stream, not supported + if (false !== strpos($streamUrl, '://')) { + return $streamUrl; + } + + // already absolute + if (substr($streamUrl, 0, 1) === '/' || substr($streamUrl, 1, 1) === ':' || substr($streamUrl, 0, 2) === '\\\\') { + return $prefix.$streamUrl; + } + + $streamUrl = getcwd() . '/' . $streamUrl; + + return $prefix.$streamUrl; + } + + /** + * Return the JSON representation of a value + * + * @param mixed $data + * @param int $encodeFlags flags to pass to json encode, defaults to DEFAULT_JSON_FLAGS + * @param bool $ignoreErrors whether to ignore encoding errors or to throw on error, when ignored and the encoding fails, "null" is returned which is valid json for null + * @throws \RuntimeException if encoding fails and errors are not ignored + * @return string when errors are ignored and the encoding fails, "null" is returned which is valid json for null + */ + public static function jsonEncode($data, ?int $encodeFlags = null, bool $ignoreErrors = false): string + { + if (null === $encodeFlags) { + $encodeFlags = self::DEFAULT_JSON_FLAGS; + } + + if ($ignoreErrors) { + $json = @json_encode($data, $encodeFlags); + if (false === $json) { + return 'null'; + } + + return $json; + } + + $json = json_encode($data, $encodeFlags); + if (false === $json) { + $json = self::handleJsonError(json_last_error(), $data); + } + + return $json; + } + + /** + * Handle a json_encode failure. + * + * If the failure is due to invalid string encoding, try to clean the + * input and encode again. If the second encoding attempt fails, the + * initial error is not encoding related or the input can't be cleaned then + * raise a descriptive exception. + * + * @param int $code return code of json_last_error function + * @param mixed $data data that was meant to be encoded + * @param int $encodeFlags flags to pass to json encode, defaults to JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION + * @throws \RuntimeException if failure can't be corrected + * @return string JSON encoded data after error correction + */ + public static function handleJsonError(int $code, $data, ?int $encodeFlags = null): string + { + if ($code !== JSON_ERROR_UTF8) { + self::throwEncodeError($code, $data); + } + + if (is_string($data)) { + self::detectAndCleanUtf8($data); + } elseif (is_array($data)) { + array_walk_recursive($data, array('Monolog\Utils', 'detectAndCleanUtf8')); + } else { + self::throwEncodeError($code, $data); + } + + if (null === $encodeFlags) { + $encodeFlags = self::DEFAULT_JSON_FLAGS; + } + + $json = json_encode($data, $encodeFlags); + + if ($json === false) { + self::throwEncodeError(json_last_error(), $data); + } + + return $json; + } + + /** + * @internal + */ + public static function pcreLastErrorMessage(int $code): string + { + if (PHP_VERSION_ID >= 80000) { + return preg_last_error_msg(); + } + + $constants = (get_defined_constants(true))['pcre']; + $constants = array_filter($constants, function ($key) { + return substr($key, -6) == '_ERROR'; + }, ARRAY_FILTER_USE_KEY); + + $constants = array_flip($constants); + + return $constants[$code] ?? 'UNDEFINED_ERROR'; + } + + /** + * Throws an exception according to a given code with a customized message + * + * @param int $code return code of json_last_error function + * @param mixed $data data that was meant to be encoded + * @throws \RuntimeException + * + * @return never + */ + private static function throwEncodeError(int $code, $data): void + { + switch ($code) { + case JSON_ERROR_DEPTH: + $msg = 'Maximum stack depth exceeded'; + break; + case JSON_ERROR_STATE_MISMATCH: + $msg = 'Underflow or the modes mismatch'; + break; + case JSON_ERROR_CTRL_CHAR: + $msg = 'Unexpected control character found'; + break; + case JSON_ERROR_UTF8: + $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded'; + break; + default: + $msg = 'Unknown error'; + } + + throw new \RuntimeException('JSON encoding failed: '.$msg.'. Encoding: '.var_export($data, true)); + } + + /** + * Detect invalid UTF-8 string characters and convert to valid UTF-8. + * + * Valid UTF-8 input will be left unmodified, but strings containing + * invalid UTF-8 codepoints will be reencoded as UTF-8 with an assumed + * original encoding of ISO-8859-15. This conversion may result in + * incorrect output if the actual encoding was not ISO-8859-15, but it + * will be clean UTF-8 output and will not rely on expensive and fragile + * detection algorithms. + * + * Function converts the input in place in the passed variable so that it + * can be used as a callback for array_walk_recursive. + * + * @param mixed $data Input to check and convert if needed, passed by ref + */ + private static function detectAndCleanUtf8(&$data): void + { + if (is_string($data) && !preg_match('//u', $data)) { + $data = preg_replace_callback( + '/[\x80-\xFF]+/', + function ($m) { + return function_exists('mb_convert_encoding') ? mb_convert_encoding($m[0], 'UTF-8', 'ISO-8859-1') : utf8_encode($m[0]); + }, + $data + ); + if (!is_string($data)) { + $pcreErrorCode = preg_last_error(); + throw new \RuntimeException('Failed to preg_replace_callback: ' . $pcreErrorCode . ' / ' . self::pcreLastErrorMessage($pcreErrorCode)); + } + $data = str_replace( + ['¤', '¦', '¨', '´', '¸', '¼', '½', '¾'], + ['€', 'Š', 'š', 'Ž', 'ž', 'Œ', 'œ', 'Ÿ'], + $data + ); + } + } + + /** + * Converts a string with a valid 'memory_limit' format, to bytes. + * + * @param string|false $val + * @return int|false Returns an integer representing bytes. Returns FALSE in case of error. + */ + public static function expandIniShorthandBytes($val) + { + if (!is_string($val)) { + return false; + } + + // support -1 + if ((int) $val < 0) { + return (int) $val; + } + + if (!preg_match('/^\s*(?\d+)(?:\.\d+)?\s*(?[gmk]?)\s*$/i', $val, $match)) { + return false; + } + + $val = (int) $match['val']; + switch (strtolower($match['unit'] ?? '')) { + case 'g': + $val *= 1024; + case 'm': + $val *= 1024; + case 'k': + $val *= 1024; + } + + return $val; + } + + /** + * @param array $record + */ + public static function getRecordMessageForException(array $record): string + { + $context = ''; + $extra = ''; + try { + if ($record['context']) { + $context = "\nContext: " . json_encode($record['context']); + } + if ($record['extra']) { + $extra = "\nExtra: " . json_encode($record['extra']); + } + } catch (\Throwable $e) { + // noop + } + + return "\nThe exception occurred while attempting to log: " . $record['message'] . $context . $extra; + } +} diff --git a/vendor/overtrue/socialite/.github/FUNDING.yml b/vendor/overtrue/socialite/.github/FUNDING.yml new file mode 100644 index 0000000..a3be7fa --- /dev/null +++ b/vendor/overtrue/socialite/.github/FUNDING.yml @@ -0,0 +1,9 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: overtrue +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +custom: # Replace with a single custom sponsorship URL diff --git a/vendor/overtrue/socialite/.gitignore b/vendor/overtrue/socialite/.gitignore new file mode 100644 index 0000000..d6eb268 --- /dev/null +++ b/vendor/overtrue/socialite/.gitignore @@ -0,0 +1,9 @@ +/vendor +composer.phar +composer.lock +.DS_Store +/.idea +Thumbs.db +/*.php +sftp-config.json +.php_cs.cache \ No newline at end of file diff --git a/vendor/overtrue/socialite/.php_cs b/vendor/overtrue/socialite/.php_cs new file mode 100644 index 0000000..bda3644 --- /dev/null +++ b/vendor/overtrue/socialite/.php_cs @@ -0,0 +1,28 @@ + + +This source file is subject to the MIT license that is bundled +with this source code in the file LICENSE. +EOF; + +return PhpCsFixer\Config::create() + ->setRiskyAllowed(true) + ->setRules(array( + '@Symfony' => true, + 'header_comment' => array('header' => $header), + 'array_syntax' => array('syntax' => 'short'), + 'ordered_imports' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'php_unit_construct' => true, + 'php_unit_strict' => true, + )) + ->setFinder( + PhpCsFixer\Finder::create() + ->exclude('vendor') + ->in(__DIR__) + ) +; \ No newline at end of file diff --git a/vendor/overtrue/socialite/.travis.yml b/vendor/overtrue/socialite/.travis.yml new file mode 100644 index 0000000..39912f9 --- /dev/null +++ b/vendor/overtrue/socialite/.travis.yml @@ -0,0 +1,13 @@ +language: php + +php: + - 7.0 + - 7.1 + - 7.2 + +sudo: false +dist: trusty + +install: travis_retry composer install --no-interaction --prefer-source + +script: vendor/bin/phpunit --verbose diff --git a/vendor/overtrue/socialite/LICENSE.txt b/vendor/overtrue/socialite/LICENSE.txt new file mode 100644 index 0000000..c5fe984 --- /dev/null +++ b/vendor/overtrue/socialite/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) overtrue + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/overtrue/socialite/README.md b/vendor/overtrue/socialite/README.md new file mode 100644 index 0000000..1309f31 --- /dev/null +++ b/vendor/overtrue/socialite/README.md @@ -0,0 +1,267 @@ +

              Socialite

              +

              +Build Status +Latest Stable Version +Latest Unstable Version +Build Status +Scrutinizer Code Quality +Code Coverage +Total Downloads +License +

              + + +

              Socialite is an OAuth2 Authentication tool. It is inspired by laravel/socialite, You can easily use it in any PHP project.

              + +# Requirement + +``` +PHP >= 5.6 +``` +# Installation + +```shell +$ composer require "overtrue/socialite" -vvv +``` + +# Usage + +For Laravel 5: [overtrue/laravel-socialite](https://github.com/overtrue/laravel-socialite) + +`authorize.php`: + +```php + [ + 'client_id' => 'your-app-id', + 'client_secret' => 'your-app-secret', + 'redirect' => 'http://localhost/socialite/callback.php', + ], +]; + +$socialite = new SocialiteManager($config); + +$response = $socialite->driver('github')->redirect(); + +echo $response;// or $response->send(); +``` + +`callback.php`: + +```php + [ + 'client_id' => 'your-app-id', + 'client_secret' => 'your-app-secret', + 'redirect' => 'http://localhost/socialite/callback.php', + ], +]; + +$socialite = new SocialiteManager($config); + +$user = $socialite->driver('github')->user(); + +$user->getId(); // 1472352 +$user->getNickname(); // "overtrue" +$user->getUsername(); // "overtrue" +$user->getName(); // "安正超" +$user->getEmail(); // "anzhengchao@gmail.com" +$user->getProviderName(); // GitHub +... +``` + +### Configuration + +Now we support the following sites: + +`facebook`, `github`, `google`, `linkedin`, `outlook`, `weibo`, `taobao`, `qq`, `wechat`, `douyin`, `baidu`, `feishu`, and `douban`. + +Each driver uses the same configuration keys: `client_id`, `client_secret`, `redirect`. + +Example: +``` +... + 'weibo' => [ + 'client_id' => 'your-app-id', + 'client_secret' => 'your-app-secret', + 'redirect' => 'http://localhost/socialite/callback.php', + ], +... +``` + +### Scope + +Before redirecting the user, you may also set "scopes" on the request using the scope method. This method will overwrite all existing scopes: + +```php +$response = $socialite->driver('github') + ->scopes(['scope1', 'scope2'])->redirect(); + +``` + +### Redirect URL + +You may also want to dynamicly set `redirect`,you can use the following methods to change the `redirect` URL: + +```php +$socialite->redirect($url); +// or +$socialite->withRedirectUrl($url)->redirect(); +// or +$socialite->setRedirectUrl($url)->redirect(); +``` + +> WeChat scopes: +- `snsapi_base`, `snsapi_userinfo` - Used to Media Platform Authentication. +- `snsapi_login` - Used to web Authentication. + +### Additional parameters + +To include any optional parameters in the request, call the with method with an associative array: + +```php +$response = $socialite->driver('google') + ->with(['hd' => 'example.com'])->redirect(); +``` + +### User interface + +#### Standard user api: + +```php + +$user = $socialite->driver('weibo')->user(); +``` + +```json +{ + "id": 1472352, + "nickname": "overtrue", + "name": "安正超", + "email": "anzhengchao@gmail.com", + "avatar": "https://avatars.githubusercontent.com/u/1472352?v=3", + "original": { + "login": "overtrue", + "id": 1472352, + "avatar_url": "https://avatars.githubusercontent.com/u/1472352?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/overtrue", + "html_url": "https://github.com/overtrue", + ... + }, + "token": { + "access_token": "5b1dc56d64fffbd052359f032716cc4e0a1cb9a0", + "token_type": "bearer", + "scope": "user:email" + } +} +``` + +You can fetch the user attribute as a array keys like these: + +```php +$user['id']; // 1472352 +$user['nickname']; // "overtrue" +$user['name']; // "安正超" +$user['email']; // "anzhengchao@gmail.com" +... +``` + +Or using the method: + +```php +$user->getId(); +$user->getNickname(); +$user->getName(); +$user->getEmail(); +$user->getAvatar(); +$user->getOriginal(); +$user->getToken();// or $user->getAccessToken() +$user->getProviderName(); // GitHub/Google/Facebook... +``` + +#### Get original response from OAuth API + +The `$user->getOriginal()` method will return an array of the API raw response. + +#### Get access token Object + +You can get the access token instance of current session by call `$user->getToken()` or `$user->getAccessToken()` or `$user['token']` . + + +### Get user with access token + +```php +$accessToken = new AccessToken(['access_token' => $accessToken]); +$user = $socialite->user($accessToken); +``` + + +### Custom Session or Request instance. + +You can set the request with your custom `Request` instance which instanceof `Symfony\Component\HttpFoundation\Request` before you call `driver` method. + + +```php + +$request = new Request(); // or use AnotherCustomRequest. + +$socialite = new SocialiteManager($config, $request); +``` + +Or set request to `SocialiteManager` instance: + +```php +$socialite->setRequest($request); +``` + +You can get the request from the `SocialiteManager` instance by `getRequest()`: + +```php +$request = $socialite->getRequest(); +``` + +#### Set custom session manager. + +By default, the `SocialiteManager` uses the `Symfony\Component\HttpFoundation\Session\Session` instance as session manager, you can change it as follows: + +```php +$session = new YourCustomSessionManager(); +$socialite->getRequest()->setSession($session); +``` + +> Your custom session manager must be implement the [`Symfony\Component\HttpFoundation\Session\SessionInterface`](http://api.symfony.com/3.0/Symfony/Component/HttpFoundation/Session/SessionInterface.html). + +Enjoy it! :heart: + +# Reference + +- [Google - OpenID Connect](https://developers.google.com/identity/protocols/OpenIDConnect) +- [Facebook - Graph API](https://developers.facebook.com/docs/graph-api) +- [Linkedin - Authenticating with OAuth 2.0](https://developer.linkedin.com/docs/oauth2) +- [微博 - OAuth 2.0 授权机制说明](http://open.weibo.com/wiki/%E6%8E%88%E6%9D%83%E6%9C%BA%E5%88%B6%E8%AF%B4%E6%98%8E) +- [QQ - OAuth 2.0 登录QQ](http://wiki.connect.qq.com/oauth2-0%E7%AE%80%E4%BB%8B) +- [微信公众平台 - OAuth文档](http://mp.weixin.qq.com/wiki/9/01f711493b5a02f24b04365ac5d8fd95.html) +- [微信开放平台 - 网站应用微信登录开发指南](https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN) +- [微信开放平台 - 代公众号发起网页授权](https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419318590&token=&lang=zh_CN) +- [豆瓣 - OAuth 2.0 授权机制说明](http://developers.douban.com/wiki/?title=oauth2) +- [抖音 - 网站应用开发指南](http://open.douyin.com/platform/doc) +- [飞书 - 授权说明](https://open.feishu.cn/document/ukTMukTMukTM/uMTNz4yM1MjLzUzM) + +## PHP 扩展包开发 + +> 想知道如何从零开始构建 PHP 扩展包? +> +> 请关注我的实战课程,我会在此课程中分享一些扩展开发经验 —— [《PHP 扩展包实战教程 - 从入门到发布》](https://learnku.com/courses/creating-package) + +# License + +MIT diff --git a/vendor/overtrue/socialite/composer.json b/vendor/overtrue/socialite/composer.json new file mode 100644 index 0000000..d5b4aef --- /dev/null +++ b/vendor/overtrue/socialite/composer.json @@ -0,0 +1,34 @@ +{ + "name": "overtrue/socialite", + "description": "A collection of OAuth 2 packages that extracts from laravel/socialite.", + "keywords": [ + "OAuth", + "social", + "login", + "Weibo", + "WeChat", + "QQ" + ], + "autoload": { + "psr-4": { + "Overtrue\\Socialite\\": "src/" + } + }, + "require": { + "php": ">=5.6", + "guzzlehttp/guzzle": "^5.0|^6.0|^7.0", + "symfony/http-foundation": "^2.7|^3.0|^4.0|^5.0", + "ext-json": "*" + }, + "require-dev": { + "mockery/mockery": "~1.2", + "phpunit/phpunit": "^6.0|^7.0|^8.0|^9.0" + }, + "license": "MIT", + "authors": [ + { + "name": "overtrue", + "email": "anzhengchao@gmail.com" + } + ] +} diff --git a/vendor/overtrue/socialite/phpunit.xml b/vendor/overtrue/socialite/phpunit.xml new file mode 100644 index 0000000..3347b75 --- /dev/null +++ b/vendor/overtrue/socialite/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./tests/ + + + diff --git a/vendor/overtrue/socialite/src/AccessToken.php b/vendor/overtrue/socialite/src/AccessToken.php new file mode 100644 index 0000000..d62dfe0 --- /dev/null +++ b/vendor/overtrue/socialite/src/AccessToken.php @@ -0,0 +1,84 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite; + +use ArrayAccess; +use InvalidArgumentException; +use JsonSerializable; + +/** + * Class AccessToken. + */ +class AccessToken implements AccessTokenInterface, ArrayAccess, JsonSerializable +{ + use HasAttributes; + + /** + * AccessToken constructor. + * + * @param array $attributes + */ + public function __construct(array $attributes) + { + if (empty($attributes['access_token'])) { + throw new InvalidArgumentException('The key "access_token" could not be empty.'); + } + + $this->attributes = $attributes; + } + + /** + * Return the access token string. + * + * @return string + */ + public function getToken() + { + return $this->getAttribute('access_token'); + } + + /** + * Return the refresh token string. + * + * @return string + */ + public function getRefreshToken() + { + return $this->getAttribute('refresh_token'); + } + + /** + * Set refresh token into this object. + * + * @param string $token + */ + public function setRefreshToken($token) + { + $this->setAttribute('refresh_token', $token); + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return strval($this->getAttribute('access_token', '')); + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() + { + return $this->getToken(); + } +} diff --git a/vendor/overtrue/socialite/src/AccessTokenInterface.php b/vendor/overtrue/socialite/src/AccessTokenInterface.php new file mode 100644 index 0000000..f6f54bc --- /dev/null +++ b/vendor/overtrue/socialite/src/AccessTokenInterface.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite; + +/** + * Interface AccessTokenInterface. + */ +interface AccessTokenInterface +{ + /** + * Return the access token string. + * + * @return string + */ + public function getToken(); +} diff --git a/vendor/overtrue/socialite/src/AuthorizeFailedException.php b/vendor/overtrue/socialite/src/AuthorizeFailedException.php new file mode 100644 index 0000000..cc2b128 --- /dev/null +++ b/vendor/overtrue/socialite/src/AuthorizeFailedException.php @@ -0,0 +1,35 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite; + +class AuthorizeFailedException extends \RuntimeException +{ + /** + * Response body. + * + * @var array + */ + public $body; + + /** + * Constructor. + * + * @param string $message + * @param array $body + */ + public function __construct($message, $body) + { + parent::__construct($message, -1); + + $this->body = $body; + } +} diff --git a/vendor/overtrue/socialite/src/Config.php b/vendor/overtrue/socialite/src/Config.php new file mode 100644 index 0000000..bbe0862 --- /dev/null +++ b/vendor/overtrue/socialite/src/Config.php @@ -0,0 +1,180 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite; + +use ArrayAccess; +use InvalidArgumentException; + +/** + * Class Config. + */ +class Config implements ArrayAccess +{ + /** + * @var array + */ + protected $config; + + /** + * Config constructor. + * + * @param array $config + */ + public function __construct(array $config) + { + $this->config = $config; + } + + /** + * Get an item from an array using "dot" notation. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function get($key, $default = null) + { + $config = $this->config; + + if (is_null($key)) { + return $config; + } + if (isset($config[$key])) { + return $config[$key]; + } + foreach (explode('.', $key) as $segment) { + if (!is_array($config) || !array_key_exists($segment, $config)) { + return $default; + } + $config = $config[$segment]; + } + + return $config; + } + + /** + * Set an array item to a given value using "dot" notation. + * + * @param string $key + * @param mixed $value + * + * @return array + */ + public function set($key, $value) + { + if (is_null($key)) { + throw new InvalidArgumentException('Invalid config key.'); + } + + $keys = explode('.', $key); + $config = &$this->config; + + while (count($keys) > 1) { + $key = array_shift($keys); + if (!isset($config[$key]) || !is_array($config[$key])) { + $config[$key] = []; + } + $config = &$config[$key]; + } + + $config[array_shift($keys)] = $value; + + return $config; + } + + /** + * Determine if the given configuration value exists. + * + * @param string $key + * + * @return bool + */ + public function has($key) + { + return (bool) $this->get($key); + } + + /** + * Whether a offset exists. + * + * @see http://php.net/manual/en/arrayaccess.offsetexists.php + * + * @param mixed $offset

              + * An offset to check for. + *

              + * + * @return bool true on success or false on failure. + *

              + *

              + * The return value will be casted to boolean if non-boolean was returned + * + * @since 5.0.0 + */ + public function offsetExists($offset) + { + return array_key_exists($offset, $this->config); + } + + /** + * Offset to retrieve. + * + * @see http://php.net/manual/en/arrayaccess.offsetget.php + * + * @param mixed $offset

              + * The offset to retrieve. + *

              + * + * @return mixed Can return all value types + * + * @since 5.0.0 + */ + public function offsetGet($offset) + { + return $this->get($offset); + } + + /** + * Offset to set. + * + * @see http://php.net/manual/en/arrayaccess.offsetset.php + * + * @param mixed $offset

              + * The offset to assign the value to. + *

              + * @param mixed $value

              + * The value to set. + *

              + * + * @since 5.0.0 + */ + public function offsetSet($offset, $value) + { + $this->set($offset, $value); + } + + /** + * Offset to unset. + * + * @see http://php.net/manual/en/arrayaccess.offsetunset.php + * + * @param mixed $offset

              + * The offset to unset. + *

              + * + * @since 5.0.0 + */ + public function offsetUnset($offset) + { + $this->set($offset, null); + } +} diff --git a/vendor/overtrue/socialite/src/FactoryInterface.php b/vendor/overtrue/socialite/src/FactoryInterface.php new file mode 100644 index 0000000..7a4959c --- /dev/null +++ b/vendor/overtrue/socialite/src/FactoryInterface.php @@ -0,0 +1,27 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite; + +/** + * Interface FactoryInterface. + */ +interface FactoryInterface +{ + /** + * Get an OAuth provider implementation. + * + * @param string $driver + * + * @return \Overtrue\Socialite\ProviderInterface + */ + public function driver($driver); +} diff --git a/vendor/overtrue/socialite/src/HasAttributes.php b/vendor/overtrue/socialite/src/HasAttributes.php new file mode 100644 index 0000000..eeff890 --- /dev/null +++ b/vendor/overtrue/socialite/src/HasAttributes.php @@ -0,0 +1,135 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite; + +/** + * Trait HasAttributes. + */ +trait HasAttributes +{ + /** + * @var array + */ + protected $attributes = []; + + /** + * Return the attributes. + * + * @return array + */ + public function getAttributes() + { + return $this->attributes; + } + + /** + * Return the extra attribute. + * + * @param string $name + * @param string $default + * + * @return mixed + */ + public function getAttribute($name, $default = null) + { + return isset($this->attributes[$name]) ? $this->attributes[$name] : $default; + } + + /** + * Set extra attributes. + * + * @param string $name + * @param mixed $value + * + * @return $this + */ + public function setAttribute($name, $value) + { + $this->attributes[$name] = $value; + + return $this; + } + + /** + * Map the given array onto the user's properties. + * + * @param array $attributes + * + * @return $this + */ + public function merge(array $attributes) + { + $this->attributes = array_merge($this->attributes, $attributes); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) + { + return array_key_exists($offset, $this->attributes); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset) + { + return $this->getAttribute($offset); + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value) + { + $this->setAttribute($offset, $value); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset) + { + unset($this->attributes[$offset]); + } + + /** + * {@inheritdoc} + */ + public function __get($property) + { + return $this->getAttribute($property); + } + + /** + * Return array. + * + * @return array + */ + public function toArray() + { + return $this->getAttributes(); + } + + /** + * Return JSON. + * + * @return string + */ + public function toJSON() + { + return json_encode($this->getAttributes(), JSON_UNESCAPED_UNICODE); + } +} diff --git a/vendor/overtrue/socialite/src/InvalidArgumentException.php b/vendor/overtrue/socialite/src/InvalidArgumentException.php new file mode 100644 index 0000000..c044e64 --- /dev/null +++ b/vendor/overtrue/socialite/src/InvalidArgumentException.php @@ -0,0 +1,16 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite; + +class InvalidArgumentException extends \InvalidArgumentException +{ +} diff --git a/vendor/overtrue/socialite/src/InvalidStateException.php b/vendor/overtrue/socialite/src/InvalidStateException.php new file mode 100644 index 0000000..96ac503 --- /dev/null +++ b/vendor/overtrue/socialite/src/InvalidStateException.php @@ -0,0 +1,16 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite; + +class InvalidStateException extends \InvalidArgumentException +{ +} diff --git a/vendor/overtrue/socialite/src/ProviderInterface.php b/vendor/overtrue/socialite/src/ProviderInterface.php new file mode 100644 index 0000000..e78d172 --- /dev/null +++ b/vendor/overtrue/socialite/src/ProviderInterface.php @@ -0,0 +1,31 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite; + +interface ProviderInterface +{ + /** + * Redirect the user to the authentication page for the provider. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + */ + public function redirect(); + + /** + * Get the User instance for the authenticated user. + * + * @param \Overtrue\Socialite\AccessTokenInterface $token + * + * @return \Overtrue\Socialite\User + */ + public function user(AccessTokenInterface $token = null); +} diff --git a/vendor/overtrue/socialite/src/Providers/AbstractProvider.php b/vendor/overtrue/socialite/src/Providers/AbstractProvider.php new file mode 100644 index 0000000..b33e6da --- /dev/null +++ b/vendor/overtrue/socialite/src/Providers/AbstractProvider.php @@ -0,0 +1,585 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite\Providers; + +use GuzzleHttp\Client; +use GuzzleHttp\ClientInterface; +use Overtrue\Socialite\AccessToken; +use Overtrue\Socialite\AccessTokenInterface; +use Overtrue\Socialite\AuthorizeFailedException; +use Overtrue\Socialite\Config; +use Overtrue\Socialite\InvalidStateException; +use Overtrue\Socialite\ProviderInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; + +/** + * Class AbstractProvider. + */ +abstract class AbstractProvider implements ProviderInterface +{ + /** + * Provider name. + * + * @var string + */ + protected $name; + + /** + * The HTTP request instance. + * + * @var \Symfony\Component\HttpFoundation\Request + */ + protected $request; + + /** + * Driver config. + * + * @var Config + */ + protected $config; + + /** + * The client ID. + * + * @var string + */ + protected $clientId; + + /** + * The client secret. + * + * @var string + */ + protected $clientSecret; + + /** + * @var \Overtrue\Socialite\AccessTokenInterface + */ + protected $accessToken; + + /** + * The redirect URL. + * + * @var string + */ + protected $redirectUrl; + + /** + * The custom parameters to be sent with the request. + * + * @var array + */ + protected $parameters = []; + + /** + * The scopes being requested. + * + * @var array + */ + protected $scopes = []; + + /** + * The separating character for the requested scopes. + * + * @var string + */ + protected $scopeSeparator = ','; + + /** + * The type of the encoding in the query. + * + * @var int Can be either PHP_QUERY_RFC3986 or PHP_QUERY_RFC1738 + */ + protected $encodingType = PHP_QUERY_RFC1738; + + /** + * Indicates if the session state should be utilized. + * + * @var bool + */ + protected $stateless = false; + + /** + * The options for guzzle\client. + * + * @var array + */ + protected static $guzzleOptions = ['http_errors' => false]; + + /** + * Create a new provider instance. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * @param array $config + */ + public function __construct(Request $request, $config) + { + // 兼容处理 + if (!\is_array($config)) { + $config = [ + 'client_id' => \func_get_arg(1), + 'client_secret' => \func_get_arg(2), + 'redirect' => \func_get_arg(3) ?: null, + ]; + } + $this->config = new Config($config); + $this->request = $request; + $this->clientId = $config['client_id']; + $this->clientSecret = $config['client_secret']; + $this->redirectUrl = isset($config['redirect']) ? $config['redirect'] : null; + } + + /** + * Get the authentication URL for the provider. + * + * @param string $state + * + * @return string + */ + abstract protected function getAuthUrl($state); + + /** + * Get the token URL for the provider. + * + * @return string + */ + abstract protected function getTokenUrl(); + + /** + * Get the raw user for the given access token. + * + * @param \Overtrue\Socialite\AccessTokenInterface $token + * + * @return array + */ + abstract protected function getUserByToken(AccessTokenInterface $token); + + /** + * Map the raw user array to a Socialite User instance. + * + * @param array $user + * + * @return \Overtrue\Socialite\User + */ + abstract protected function mapUserToObject(array $user); + + /** + * Redirect the user of the application to the provider's authentication screen. + * + * @param string $redirectUrl + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + */ + public function redirect($redirectUrl = null) + { + $state = null; + + if (!is_null($redirectUrl)) { + $this->redirectUrl = $redirectUrl; + } + + if ($this->usesState()) { + $state = $this->makeState(); + } + + return new RedirectResponse($this->getAuthUrl($state)); + } + + /** + * {@inheritdoc} + */ + public function user(AccessTokenInterface $token = null) + { + if (is_null($token) && $this->hasInvalidState()) { + throw new InvalidStateException(); + } + + $token = $token ?: $this->getAccessToken($this->getCode()); + + $user = $this->getUserByToken($token); + + $user = $this->mapUserToObject($user)->merge(['original' => $user]); + + return $user->setToken($token)->setProviderName($this->getName()); + } + + /** + * Set redirect url. + * + * @param string $redirectUrl + * + * @return $this + */ + public function setRedirectUrl($redirectUrl) + { + $this->redirectUrl = $redirectUrl; + + return $this; + } + + /** + * Set redirect url. + * + * @param string $redirectUrl + * + * @return $this + */ + public function withRedirectUrl($redirectUrl) + { + $this->redirectUrl = $redirectUrl; + + return $this; + } + + /** + * Return the redirect url. + * + * @return string + */ + public function getRedirectUrl() + { + return $this->redirectUrl; + } + + /** + * @param \Overtrue\Socialite\AccessTokenInterface $accessToken + * + * @return $this + */ + public function setAccessToken(AccessTokenInterface $accessToken) + { + $this->accessToken = $accessToken; + + return $this; + } + + /** + * Get the access token for the given code. + * + * @param string $code + * + * @return \Overtrue\Socialite\AccessTokenInterface + */ + public function getAccessToken($code) + { + if ($this->accessToken) { + return $this->accessToken; + } + + $guzzleVersion = \defined(ClientInterface::class.'::VERSION') ? \constant(ClientInterface::class.'::VERSION') : 7; + + $postKey = (1 === version_compare($guzzleVersion, '6')) ? 'form_params' : 'body'; + + $response = $this->getHttpClient()->post($this->getTokenUrl(), [ + 'headers' => ['Accept' => 'application/json'], + $postKey => $this->getTokenFields($code), + ]); + + return $this->parseAccessToken($response->getBody()); + } + + /** + * Set the scopes of the requested access. + * + * @param array $scopes + * + * @return $this + */ + public function scopes(array $scopes) + { + $this->scopes = $scopes; + + return $this; + } + + /** + * Set the request instance. + * + * @param Request $request + * + * @return $this + */ + public function setRequest(Request $request) + { + $this->request = $request; + + return $this; + } + + /** + * Get the request instance. + * + * @return \Symfony\Component\HttpFoundation\Request + */ + public function getRequest() + { + return $this->request; + } + + /** + * Indicates that the provider should operate as stateless. + * + * @return $this + */ + public function stateless() + { + $this->stateless = true; + + return $this; + } + + /** + * Set the custom parameters of the request. + * + * @param array $parameters + * + * @return $this + */ + public function with(array $parameters) + { + $this->parameters = $parameters; + + return $this; + } + + /** + * @throws \ReflectionException + * + * @return string + */ + public function getName() + { + if (empty($this->name)) { + $this->name = strstr((new \ReflectionClass(get_class($this)))->getShortName(), 'Provider', true); + } + + return $this->name; + } + + /** + * @return array + */ + public function getConfig() + { + return $this->config; + } + + /** + * Get the authentication URL for the provider. + * + * @param string $url + * @param string $state + * + * @return string + */ + protected function buildAuthUrlFromBase($url, $state) + { + return $url.'?'.http_build_query($this->getCodeFields($state), '', '&', $this->encodingType); + } + + /** + * Get the GET parameters for the code request. + * + * @param string|null $state + * + * @return array + */ + protected function getCodeFields($state = null) + { + $fields = array_merge([ + 'client_id' => $this->config['client_id'], + 'redirect_uri' => $this->redirectUrl, + 'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator), + 'response_type' => 'code', + ], $this->parameters); + + if ($this->usesState()) { + $fields['state'] = $state; + } + + return $fields; + } + + /** + * Format the given scopes. + * + * @param array $scopes + * @param string $scopeSeparator + * + * @return string + */ + protected function formatScopes(array $scopes, $scopeSeparator) + { + return implode($scopeSeparator, $scopes); + } + + /** + * Determine if the current request / session has a mismatching "state". + * + * @return bool + */ + protected function hasInvalidState() + { + if ($this->isStateless()) { + return false; + } + + $state = $this->request->getSession()->get('state'); + + return !(strlen($state) > 0 && $this->request->get('state') === $state); + } + + /** + * Get the POST fields for the token request. + * + * @param string $code + * + * @return array + */ + protected function getTokenFields($code) + { + return [ + 'client_id' => $this->getConfig()->get('client_id'), + 'client_secret' => $this->getConfig()->get('client_secret'), + 'code' => $code, + 'redirect_uri' => $this->redirectUrl, + ]; + } + + /** + * Get the access token from the token response body. + * + * @param \Psr\Http\Message\StreamInterface|array $body + * + * @return \Overtrue\Socialite\AccessTokenInterface + */ + protected function parseAccessToken($body) + { + if (!is_array($body)) { + $body = json_decode($body, true); + } + + if (empty($body['access_token'])) { + throw new AuthorizeFailedException('Authorize Failed: '.json_encode($body, JSON_UNESCAPED_UNICODE), $body); + } + + return new AccessToken($body); + } + + /** + * Get the code from the request. + * + * @return string + */ + protected function getCode() + { + return $this->request->get('code'); + } + + /** + * Get a fresh instance of the Guzzle HTTP client. + * + * @return \GuzzleHttp\Client + */ + protected function getHttpClient() + { + return new Client(self::$guzzleOptions); + } + + /** + * Set options for Guzzle HTTP client. + * + * @param array $config + * + * @return array + */ + public static function setGuzzleOptions($config = []) + { + return self::$guzzleOptions = $config; + } + + /** + * Determine if the provider is operating with state. + * + * @return bool + */ + protected function usesState() + { + return !$this->stateless; + } + + /** + * Determine if the provider is operating as stateless. + * + * @return bool + */ + protected function isStateless() + { + return !$this->request->hasSession() || $this->stateless; + } + + /** + * Return array item by key. + * + * @param array $array + * @param string $key + * @param mixed $default + * + * @return mixed + */ + protected function arrayItem(array $array, $key, $default = null) + { + if (is_null($key)) { + return $array; + } + + if (isset($array[$key])) { + return $array[$key]; + } + + foreach (explode('.', $key) as $segment) { + if (!is_array($array) || !array_key_exists($segment, $array)) { + return $default; + } + + $array = $array[$segment]; + } + + return $array; + } + + /** + * Put state to session storage and return it. + * + * @return string|bool + */ + protected function makeState() + { + if (!$this->request->hasSession()) { + return false; + } + + $state = sha1(uniqid(mt_rand(1, 1000000), true)); + $session = $this->request->getSession(); + + if (is_callable([$session, 'put'])) { + $session->put('state', $state); + } elseif (is_callable([$session, 'set'])) { + $session->set('state', $state); + } else { + return false; + } + + return $state; + } +} diff --git a/vendor/overtrue/socialite/src/Providers/BaiduProvider.php b/vendor/overtrue/socialite/src/Providers/BaiduProvider.php new file mode 100644 index 0000000..e06b890 --- /dev/null +++ b/vendor/overtrue/socialite/src/Providers/BaiduProvider.php @@ -0,0 +1,134 @@ +buildAuthUrlFromBase($this->baseUrl.'/oauth/'.$this->version.'/authorize', $state); + } + + /** + * {@inheritdoc}. + */ + protected function getCodeFields($state = null) + { + return array_merge([ + 'response_type' => 'code', + 'client_id' => $this->getConfig()->get('client_id'), + 'redirect_uri' => $this->redirectUrl, + 'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator), + 'display' => $this->display, + ], $this->parameters); + } + + /** + * Get the token URL for the provider. + * + * @return string + */ + protected function getTokenUrl() + { + return $this->baseUrl.'/oauth/'.$this->version.'/token'; + } + + /** + * Get the Post fields for the token request. + * + * @param string $code + * + * @return array + */ + protected function getTokenFields($code) + { + return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; + } + + /** + * Get the raw user for the given access token. + * + * @param \Overtrue\Socialite\AccessTokenInterface $token + * + * @return array + */ + protected function getUserByToken(AccessTokenInterface $token) + { + $response = $this->getHttpClient()->get($this->baseUrl.'/rest/'.$this->version.'/passport/users/getInfo', [ + 'query' => [ + 'access_token' => $token->getToken(), + ], + 'headers' => [ + 'Accept' => 'application/json', + ], + ]); + + return json_decode($response->getBody(), true); + } + + /** + * Map the raw user array to a Socialite User instance. + * + * @param array $user + * + * @return \Overtrue\Socialite\User + */ + protected function mapUserToObject(array $user) + { + $realname = $this->arrayItem($user, 'realname'); + + return new User([ + 'id' => $this->arrayItem($user, 'userid'), + 'nickname' => empty($realname) ? '' : $realname, + 'name' => $this->arrayItem($user, 'username'), + 'email' => '', + 'avatar' => $this->arrayItem($user, 'portrait'), + ]); + } +} diff --git a/vendor/overtrue/socialite/src/Providers/DouYinProvider.php b/vendor/overtrue/socialite/src/Providers/DouYinProvider.php new file mode 100644 index 0000000..f798a86 --- /dev/null +++ b/vendor/overtrue/socialite/src/Providers/DouYinProvider.php @@ -0,0 +1,169 @@ +buildAuthUrlFromBase($this->baseUrl.'/platform/oauth/connect', $state); + } + + /** + * 获取授权码接口参数. + * + * @param string|null $state + * + * @return array + */ + public function getCodeFields($state = null) + { + $fields = [ + 'client_key' => $this->getConfig()->get('client_id'), + 'redirect_uri' => $this->redirectUrl, + 'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator), + 'response_type' => 'code', + ]; + + if ($this->usesState()) { + $fields['state'] = $state; + } + + return $fields; + } + + /** + * 获取access_token地址. + * + * {@inheritdoc} + */ + protected function getTokenUrl() + { + return $this->baseUrl.'/oauth/access_token'; + } + + /** + * 通过code获取access_token. + * + * @param string $code + * + * @return \Overtrue\Socialite\AccessToken + */ + public function getAccessToken($code) + { + $response = $this->getHttpClient()->get($this->getTokenUrl(), [ + 'query' => $this->getTokenFields($code), + ]); + + return $this->parseAccessToken($response->getBody()->getContents()); + } + + /** + * 获取access_token接口参数. + * + * @param string $code + * + * @return array + */ + protected function getTokenFields($code) + { + return [ + 'client_key' => $this->getConfig()->get('client_id'), + 'client_secret' => $this->getConfig()->get('client_secret'), + 'code' => $code, + 'grant_type' => 'authorization_code', + ]; + } + + /** + * 格式化token. + * + * @param \Psr\Http\Message\StreamInterface|array $body + * + * @return \Overtrue\Socialite\AccessTokenInterface + */ + protected function parseAccessToken($body) + { + if (!is_array($body)) { + $body = json_decode($body, true); + } + + if (empty($body['data']['access_token'])) { + throw new AuthorizeFailedException('Authorize Failed: '.json_encode($body, JSON_UNESCAPED_UNICODE), $body); + } + + return new AccessToken($body['data']); + } + + /** + * 通过token 获取用户信息. + * + * @param AccessTokenInterface $token + * + * @return array|mixed + */ + protected function getUserByToken(AccessTokenInterface $token) + { + $userUrl = $this->baseUrl.'/oauth/userinfo/'; + + $response = $this->getHttpClient()->get( + $userUrl, + [ + 'query' => [ + 'access_token' => $token->getToken(), + 'open_id' => $token['open_id'], + ], + ] + ); + + return json_decode($response->getBody(), true); + } + + /** + * 格式化用户信息. + * + * @param array $user + * + * @return User + */ + protected function mapUserToObject(array $user) + { + return new User([ + 'id' => $this->arrayItem($user, 'open_id'), + 'username' => $this->arrayItem($user, 'nickname'), + 'nickname' => $this->arrayItem($user, 'nickname'), + 'avatar' => $this->arrayItem($user, 'avatar'), + ]); + } +} diff --git a/vendor/overtrue/socialite/src/Providers/DoubanProvider.php b/vendor/overtrue/socialite/src/Providers/DoubanProvider.php new file mode 100644 index 0000000..f3b5c93 --- /dev/null +++ b/vendor/overtrue/socialite/src/Providers/DoubanProvider.php @@ -0,0 +1,88 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite\Providers; + +use Overtrue\Socialite\AccessTokenInterface; +use Overtrue\Socialite\ProviderInterface; +use Overtrue\Socialite\User; + +/** + * Class DoubanProvider. + * + * @see http://developers.douban.com/wiki/?title=oauth2 [使用 OAuth 2.0 访问豆瓣 API] + */ +class DoubanProvider extends AbstractProvider implements ProviderInterface +{ + /** + * {@inheritdoc}. + */ + protected function getAuthUrl($state) + { + return $this->buildAuthUrlFromBase('https://www.douban.com/service/auth2/auth', $state); + } + + /** + * {@inheritdoc}. + */ + protected function getTokenUrl() + { + return 'https://www.douban.com/service/auth2/token'; + } + + /** + * {@inheritdoc}. + */ + protected function getUserByToken(AccessTokenInterface $token) + { + $response = $this->getHttpClient()->get('https://api.douban.com/v2/user/~me', [ + 'headers' => [ + 'Authorization' => 'Bearer '.$token->getToken(), + ], + ]); + + return json_decode($response->getBody()->getContents(), true); + } + + /** + * {@inheritdoc}. + */ + protected function mapUserToObject(array $user) + { + return new User([ + 'id' => $this->arrayItem($user, 'id'), + 'nickname' => $this->arrayItem($user, 'name'), + 'name' => $this->arrayItem($user, 'name'), + 'avatar' => $this->arrayItem($user, 'large_avatar'), + 'email' => null, + ]); + } + + /** + * {@inheritdoc}. + */ + protected function getTokenFields($code) + { + return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; + } + + /** + * {@inheritdoc}. + */ + public function getAccessToken($code) + { + $response = $this->getHttpClient()->post($this->getTokenUrl(), [ + 'form_params' => $this->getTokenFields($code), + ]); + + return $this->parseAccessToken($response->getBody()->getContents()); + } +} diff --git a/vendor/overtrue/socialite/src/Providers/FacebookProvider.php b/vendor/overtrue/socialite/src/Providers/FacebookProvider.php new file mode 100644 index 0000000..ce2cbec --- /dev/null +++ b/vendor/overtrue/socialite/src/Providers/FacebookProvider.php @@ -0,0 +1,168 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite\Providers; + +use Overtrue\Socialite\AccessTokenInterface; +use Overtrue\Socialite\ProviderInterface; +use Overtrue\Socialite\User; + +/** + * Class FacebookProvider. + * + * @see https://developers.facebook.com/docs/graph-api [Facebook - Graph API] + */ +class FacebookProvider extends AbstractProvider implements ProviderInterface +{ + /** + * The base Facebook Graph URL. + * + * @var string + */ + protected $graphUrl = 'https://graph.facebook.com'; + + /** + * The Graph API version for the request. + * + * @var string + */ + protected $version = 'v3.3'; + + /** + * The user fields being requested. + * + * @var array + */ + protected $fields = ['first_name', 'last_name', 'email', 'gender', 'verified']; + + /** + * The scopes being requested. + * + * @var array + */ + protected $scopes = ['email']; + + /** + * Display the dialog in a popup view. + * + * @var bool + */ + protected $popup = false; + + /** + * {@inheritdoc} + */ + protected function getAuthUrl($state) + { + return $this->buildAuthUrlFromBase('https://www.facebook.com/'.$this->version.'/dialog/oauth', $state); + } + + /** + * {@inheritdoc} + */ + protected function getTokenUrl() + { + return $this->graphUrl.'/oauth/access_token'; + } + + /** + * Get the access token for the given code. + * + * @param string $code + * + * @return \Overtrue\Socialite\AccessToken + */ + public function getAccessToken($code) + { + $response = $this->getHttpClient()->get($this->getTokenUrl(), [ + 'query' => $this->getTokenFields($code), + ]); + + return $this->parseAccessToken($response->getBody()); + } + + /** + * {@inheritdoc} + */ + protected function getUserByToken(AccessTokenInterface $token) + { + $appSecretProof = hash_hmac('sha256', $token->getToken(), $this->getConfig()->get('client_secret')); + + $response = $this->getHttpClient()->get($this->graphUrl.'/'.$this->version.'/me?access_token='.$token.'&appsecret_proof='.$appSecretProof.'&fields='.implode(',', $this->fields), [ + 'headers' => [ + 'Accept' => 'application/json', + ], + ]); + + return json_decode($response->getBody(), true); + } + + /** + * {@inheritdoc} + */ + protected function mapUserToObject(array $user) + { + $userId = $this->arrayItem($user, 'id'); + $avatarUrl = $this->graphUrl.'/'.$this->version.'/'.$userId.'/picture'; + + $firstName = $this->arrayItem($user, 'first_name'); + $lastName = $this->arrayItem($user, 'last_name'); + + return new User([ + 'id' => $this->arrayItem($user, 'id'), + 'nickname' => null, + 'name' => $firstName.' '.$lastName, + 'email' => $this->arrayItem($user, 'email'), + 'avatar' => $userId ? $avatarUrl.'?type=normal' : null, + 'avatar_original' => $userId ? $avatarUrl.'?width=1920' : null, + ]); + } + + /** + * {@inheritdoc} + */ + protected function getCodeFields($state = null) + { + $fields = parent::getCodeFields($state); + + if ($this->popup) { + $fields['display'] = 'popup'; + } + + return $fields; + } + + /** + * Set the user fields to request from Facebook. + * + * @param array $fields + * + * @return $this + */ + public function fields(array $fields) + { + $this->fields = $fields; + + return $this; + } + + /** + * Set the dialog to be displayed as a popup. + * + * @return $this + */ + public function asPopup() + { + $this->popup = true; + + return $this; + } +} diff --git a/vendor/overtrue/socialite/src/Providers/FeiShuProvider.php b/vendor/overtrue/socialite/src/Providers/FeiShuProvider.php new file mode 100644 index 0000000..2d61d07 --- /dev/null +++ b/vendor/overtrue/socialite/src/Providers/FeiShuProvider.php @@ -0,0 +1,192 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite\Providers; + +use Overtrue\Socialite\AccessToken; +use Overtrue\Socialite\AccessTokenInterface; +use Overtrue\Socialite\AuthorizeFailedException; +use Overtrue\Socialite\InvalidStateException; +use Overtrue\Socialite\ProviderInterface; +use Overtrue\Socialite\User; + +/** + * Class FeiShuProvider. + * + * @author qijian.song@show.world + * + * @see https://open.feishu.cn/ + */ +class FeiShuProvider extends AbstractProvider implements ProviderInterface +{ + /** + * 飞书接口域名. + * + * @var string + */ + protected $baseUrl = 'https://open.feishu.cn'; + + /** + * 应用授权作用域. + * + * @var array + */ + protected $scopes = ['user_info']; + + /** + * 获取登录页面地址. + * + * {@inheritdoc} + */ + protected function getAuthUrl($state) + { + return $this->buildAuthUrlFromBase($this->baseUrl.'/open-apis/authen/v1/index', $state); + } + + /** + * 获取授权码接口参数. + * + * @param string|null $state + * + * @return array + */ + protected function getCodeFields($state = null) + { + $fields = [ + 'redirect_uri' => $this->redirectUrl, + 'app_id' => $this->getConfig()->get('client_id'), + ]; + + if ($this->usesState()) { + $fields['state'] = $state; + } + + return $fields; + } + + /** + * 获取 app_access_token 地址. + * + * {@inheritdoc} + */ + protected function getTokenUrl() + { + return $this->baseUrl.'/open-apis/auth/v3/app_access_token/internal'; + } + + /** + * 获取 app_access_token. + * + * @return \Overtrue\Socialite\AccessToken + */ + public function getAccessToken($code = '') + { + $response = $this->getHttpClient()->post($this->getTokenUrl(), [ + 'headers' => ['Content-Type' => 'application/json'], + 'json' => $this->getTokenFields($code), + ]); + + return $this->parseAccessToken($response->getBody()->getContents()); + } + + /** + * 获取 app_access_token 接口参数. + * + * @return array + */ + protected function getTokenFields($code) + { + return [ + 'app_id' => $this->getConfig()->get('client_id'), + 'app_secret' => $this->getConfig()->get('client_secret'), + ]; + } + + /** + * 格式化 token. + * + * @param \Psr\Http\Message\StreamInterface|array $body + * + * @return \Overtrue\Socialite\AccessTokenInterface + */ + protected function parseAccessToken($body) + { + if (!is_array($body)) { + $body = json_decode($body, true); + } + + if (empty($body['app_access_token'])) { + throw new AuthorizeFailedException('Authorize Failed: '.json_encode($body, JSON_UNESCAPED_UNICODE), $body); + } + $data['access_token'] = $body['app_access_token']; + + return new AccessToken($data); + } + + /** + * 获取用户信息. + * + * @return array|mixed + */ + public function user(AccessTokenInterface $token = null) + { + if (is_null($token) && $this->hasInvalidState()) { + throw new InvalidStateException(); + } + + $token = $token ?: $this->getAccessToken(); + + $user = $this->getUserByToken($token, $this->getCode()); + $user = $this->mapUserToObject($user)->merge(['original' => $user]); + + return $user->setToken($token)->setProviderName($this->getName()); + } + + /** + * 通过 token 获取用户信息. + * + * @return array|mixed + */ + protected function getUserByToken(AccessTokenInterface $token) + { + $userUrl = $this->baseUrl.'/open-apis/authen/v1/access_token'; + + $response = $this->getHttpClient()->post( + $userUrl, + [ + 'json' => [ + 'app_access_token' => $token->getToken(), + 'code' => $this->getCode(), + 'grant_type' => 'authorization_code', + ], + ] + ); + + $result = json_decode($response->getBody(), true); + + return $result['data']; + } + + /** + * 格式化用户信息. + * + * @return User + */ + protected function mapUserToObject(array $user) + { + return new User([ + 'id' => $this->arrayItem($user, 'open_id'), + 'username' => $this->arrayItem($user, 'name'), + 'nickname' => $this->arrayItem($user, 'name'), + 'avatar' => $this->arrayItem($user, 'avatar_url'), + ]); + } +} diff --git a/vendor/overtrue/socialite/src/Providers/GitHubProvider.php b/vendor/overtrue/socialite/src/Providers/GitHubProvider.php new file mode 100644 index 0000000..955fa9d --- /dev/null +++ b/vendor/overtrue/socialite/src/Providers/GitHubProvider.php @@ -0,0 +1,126 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite\Providers; + +use Exception; +use Overtrue\Socialite\AccessTokenInterface; +use Overtrue\Socialite\ProviderInterface; +use Overtrue\Socialite\User; + +/** + * Class GitHubProvider. + */ +class GitHubProvider extends AbstractProvider implements ProviderInterface +{ + /** + * The scopes being requested. + * + * @var array + */ + protected $scopes = ['user:email']; + + /** + * {@inheritdoc} + */ + protected function getAuthUrl($state) + { + return $this->buildAuthUrlFromBase('https://github.com/login/oauth/authorize', $state); + } + + /** + * {@inheritdoc} + */ + protected function getTokenUrl() + { + return 'https://github.com/login/oauth/access_token'; + } + + /** + * {@inheritdoc} + */ + protected function getUserByToken(AccessTokenInterface $token) + { + $userUrl = 'https://api.github.com/user'; + + $response = $this->getHttpClient()->get( + $userUrl, + $this->createAuthorizationHeaders($token) + ); + + $user = json_decode($response->getBody(), true); + + if (in_array('user:email', $this->scopes)) { + $user['email'] = $this->getEmailByToken($token); + } + + return $user; + } + + /** + * Get the email for the given access token. + * + * @param string $token + * + * @return string|null + */ + protected function getEmailByToken($token) + { + $emailsUrl = 'https://api.github.com/user/emails'; + + try { + $response = $this->getHttpClient()->get( + $emailsUrl, + $this->createAuthorizationHeaders($token) + ); + } catch (Exception $e) { + return; + } + + foreach (json_decode($response->getBody(), true) as $email) { + if ($email['primary'] && $email['verified']) { + return $email['email']; + } + } + } + + /** + * {@inheritdoc} + */ + protected function mapUserToObject(array $user) + { + return new User([ + 'id' => $this->arrayItem($user, 'id'), + 'username' => $this->arrayItem($user, 'login'), + 'nickname' => $this->arrayItem($user, 'login'), + 'name' => $this->arrayItem($user, 'name'), + 'email' => $this->arrayItem($user, 'email'), + 'avatar' => $this->arrayItem($user, 'avatar_url'), + ]); + } + + /** + * Get the default options for an HTTP request. + * + * @param string $token + * + * @return array + */ + protected function createAuthorizationHeaders(string $token) + { + return [ + 'headers' => [ + 'Accept' => 'application/vnd.github.v3+json', + 'Authorization' => sprintf('token %s', $token), + ], + ]; + } +} diff --git a/vendor/overtrue/socialite/src/Providers/GoogleProvider.php b/vendor/overtrue/socialite/src/Providers/GoogleProvider.php new file mode 100644 index 0000000..c702aff --- /dev/null +++ b/vendor/overtrue/socialite/src/Providers/GoogleProvider.php @@ -0,0 +1,119 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite\Providers; + +use GuzzleHttp\ClientInterface; +use Overtrue\Socialite\AccessTokenInterface; +use Overtrue\Socialite\ProviderInterface; +use Overtrue\Socialite\User; + +/** + * Class GoogleProvider. + * + * @see https://developers.google.com/identity/protocols/OpenIDConnect [OpenID Connect] + */ +class GoogleProvider extends AbstractProvider implements ProviderInterface +{ + /** + * The separating character for the requested scopes. + * + * @var string + */ + protected $scopeSeparator = ' '; + + /** + * The scopes being requested. + * + * @var array + */ + protected $scopes = [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', + ]; + + /** + * {@inheritdoc} + */ + protected function getAuthUrl($state) + { + return $this->buildAuthUrlFromBase('https://accounts.google.com/o/oauth2/v2/auth', $state); + } + + /** + * {@inheritdoc} + */ + protected function getTokenUrl() + { + return 'https://www.googleapis.com/oauth2/v4/token'; + } + + /** + * Get the access token for the given code. + * + * @param string $code + * + * @return string + */ + public function getAccessToken($code) + { + $guzzleVersion = \defined(ClientInterface::class.'::VERSION') ? \constant(ClientInterface::class.'::VERSION') : 7; + $postKey = (1 === version_compare($guzzleVersion, '6')) ? 'form_params' : 'body'; + + $response = $this->getHttpClient()->post($this->getTokenUrl(), [ + $postKey => $this->getTokenFields($code), + ]); + + return $this->parseAccessToken($response->getBody()); + } + + /** + * Get the POST fields for the token request. + * + * @param string $code + * + * @return array + */ + protected function getTokenFields($code) + { + return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; + } + + /** + * {@inheritdoc} + */ + protected function getUserByToken(AccessTokenInterface $token) + { + $response = $this->getHttpClient()->get('https://www.googleapis.com/userinfo/v2/me', [ + 'headers' => [ + 'Accept' => 'application/json', + 'Authorization' => 'Bearer '.$token->getToken(), + ], + ]); + + return json_decode($response->getBody(), true); + } + + /** + * {@inheritdoc} + */ + protected function mapUserToObject(array $user) + { + return new User([ + 'id' => $this->arrayItem($user, 'id'), + 'username' => $this->arrayItem($user, 'email'), + 'nickname' => $this->arrayItem($user, 'name'), + 'name' => $this->arrayItem($user, 'name'), + 'email' => $this->arrayItem($user, 'email'), + 'avatar' => $this->arrayItem($user, 'picture'), + ]); + } +} diff --git a/vendor/overtrue/socialite/src/Providers/LinkedinProvider.php b/vendor/overtrue/socialite/src/Providers/LinkedinProvider.php new file mode 100644 index 0000000..019167a --- /dev/null +++ b/vendor/overtrue/socialite/src/Providers/LinkedinProvider.php @@ -0,0 +1,181 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite\Providers; + +use Overtrue\Socialite\AccessTokenInterface; +use Overtrue\Socialite\ProviderInterface; +use Overtrue\Socialite\User; + +/** + * Class LinkedinProvider. + * + * @see https://developer.linkedin.com/docs/oauth2 [Authenticating with OAuth 2.0] + */ +class LinkedinProvider extends AbstractProvider implements ProviderInterface +{ + /** + * The scopes being requested. + * + * @var array + */ + protected $scopes = ['r_liteprofile', 'r_emailaddress']; + + /** + * {@inheritdoc} + */ + protected function getAuthUrl($state) + { + return $this->buildAuthUrlFromBase('https://www.linkedin.com/oauth/v2/authorization', $state); + } + + /** + * Get the access token for the given code. + * + * @param string $code + * + * @return \Overtrue\Socialite\AccessToken + */ + public function getAccessToken($code) + { + $response = $this->getHttpClient() + ->post($this->getTokenUrl(), ['form_params' => $this->getTokenFields($code)]); + + return $this->parseAccessToken($response->getBody()); + } + + /** + * {@inheritdoc} + */ + protected function getTokenUrl() + { + return 'https://www.linkedin.com/oauth/v2/accessToken'; + } + + /** + * Get the POST fields for the token request. + * + * @param string $code + * + * @return array + */ + protected function getTokenFields($code) + { + return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; + } + + /** + * {@inheritdoc} + */ + protected function getUserByToken(AccessTokenInterface $token) + { + $basicProfile = $this->getBasicProfile($token); + $emailAddress = $this->getEmailAddress($token); + + return array_merge($basicProfile, $emailAddress); + } + + /** + * Get the basic profile fields for the user. + * + * @param string $token + * + * @return array + */ + protected function getBasicProfile($token) + { + $url = 'https://api.linkedin.com/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))'; + + $response = $this->getHttpClient()->get($url, [ + 'headers' => [ + 'Authorization' => 'Bearer '.$token, + 'X-RestLi-Protocol-Version' => '2.0.0', + ], + ]); + + return (array) json_decode($response->getBody(), true); + } + + /** + * Get the email address for the user. + * + * @param string $token + * + * @return array + */ + protected function getEmailAddress($token) + { + $url = 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))'; + + $response = $this->getHttpClient()->get($url, [ + 'headers' => [ + 'Authorization' => 'Bearer '.$token, + 'X-RestLi-Protocol-Version' => '2.0.0', + ], + ]); + + return (array) $this->arrayItem(json_decode($response->getBody(), true), 'elements.0.handle~'); + } + + /** + * {@inheritdoc} + */ + protected function mapUserToObject(array $user) + { + $preferredLocale = $this->arrayItem($user, 'firstName.preferredLocale.language').'_'.$this->arrayItem($user, 'firstName.preferredLocale.country'); + $firstName = $this->arrayItem($user, 'firstName.localized.'.$preferredLocale); + $lastName = $this->arrayItem($user, 'lastName.localized.'.$preferredLocale); + $name = $firstName.' '.$lastName; + + $images = (array) $this->arrayItem($user, 'profilePicture.displayImage~.elements', []); + $avatars = array_filter($images, function ($image) { + return $image['data']['com.linkedin.digitalmedia.mediaartifact.StillImage']['storageSize']['width'] === 100; + }); + $avatar = array_shift($avatars); + $originalAvatars = array_filter($images, function ($image) { + return $image['data']['com.linkedin.digitalmedia.mediaartifact.StillImage']['storageSize']['width'] === 800; + }); + $originalAvatar = array_shift($originalAvatars); + + return new User([ + 'id' => $this->arrayItem($user, 'id'), + 'nickname' => $name, + 'name' => $name, + 'email' => $this->arrayItem($user, 'emailAddress'), + 'avatar' => $avatar ? $this->arrayItem($avatar, 'identifiers.0.identifier') : null, + 'avatar_original' => $originalAvatar ? $this->arrayItem($originalAvatar, 'identifiers.0.identifier') : null, + ]); + } + + /** + * Set the user fields to request from LinkedIn. + * + * @param array $fields + * + * @return $this + */ + public function fields(array $fields) + { + $this->fields = $fields; + + return $this; + } + + /** + * Determine if the provider is operating as stateless. + * + * @return bool + */ + protected function isStateless() + { + return true; + } +} diff --git a/vendor/overtrue/socialite/src/Providers/OutlookProvider.php b/vendor/overtrue/socialite/src/Providers/OutlookProvider.php new file mode 100644 index 0000000..18d9fd7 --- /dev/null +++ b/vendor/overtrue/socialite/src/Providers/OutlookProvider.php @@ -0,0 +1,89 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite\Providers; + +use Overtrue\Socialite\AccessTokenInterface; +use Overtrue\Socialite\ProviderInterface; +use Overtrue\Socialite\User; + +/** + * Class OutlookProvider. + */ +class OutlookProvider extends AbstractProvider implements ProviderInterface +{ + /** + * {@inheritdoc} + */ + protected $scopes = ['User.Read']; + + /** + * {@inheritdoc} + */ + protected $scopeSeparator = ' '; + + /** + * {@inheritdoc} + */ + protected function getAuthUrl($state) + { + return $this->buildAuthUrlFromBase('https://login.microsoftonline.com/common/oauth2/v2.0/authorize', $state); + } + + /** + * {@inheritdoc} + */ + protected function getTokenUrl() + { + return 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; + } + + /** + * {@inheritdoc} + */ + protected function getUserByToken(AccessTokenInterface $token) + { + $response = $this->getHttpClient()->get( + 'https://graph.microsoft.com/v1.0/me', + ['headers' => [ + 'Accept' => 'application/json', + 'Authorization' => 'Bearer '.$token->getToken(), + ], + ] + ); + + return json_decode($response->getBody()->getContents(), true); + } + + /** + * {@inheritdoc} + */ + protected function mapUserToObject(array $user) + { + return new User([ + 'id' => $this->arrayItem($user, 'id'), + 'nickname' => null, + 'name' => $this->arrayItem($user, 'displayName'), + 'email' => $this->arrayItem($user, 'userPrincipalName'), + 'avatar' => null, + ]); + } + + /** + * {@inheritdoc} + */ + protected function getTokenFields($code) + { + return array_merge(parent::getTokenFields($code), [ + 'grant_type' => 'authorization_code', + ]); + } +} diff --git a/vendor/overtrue/socialite/src/Providers/QQProvider.php b/vendor/overtrue/socialite/src/Providers/QQProvider.php new file mode 100644 index 0000000..124357e --- /dev/null +++ b/vendor/overtrue/socialite/src/Providers/QQProvider.php @@ -0,0 +1,206 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite\Providers; + +use Overtrue\Socialite\AccessTokenInterface; +use Overtrue\Socialite\ProviderInterface; +use Overtrue\Socialite\User; + +/** + * Class QQProvider. + * + * @see http://wiki.connect.qq.com/oauth2-0%E7%AE%80%E4%BB%8B [QQ - OAuth 2.0 登录QQ] + */ +class QQProvider extends AbstractProvider implements ProviderInterface +{ + /** + * The base url of QQ API. + * + * @var string + */ + protected $baseUrl = 'https://graph.qq.com'; + + /** + * User openid. + * + * @var string + */ + protected $openId; + + /** + * get token(openid) with unionid. + * + * @var bool + */ + protected $withUnionId = false; + + /** + * User unionid. + * + * @var string + */ + protected $unionId; + + /** + * The scopes being requested. + * + * @var array + */ + protected $scopes = ['get_user_info']; + + /** + * The uid of user authorized. + * + * @var int + */ + protected $uid; + + /** + * Get the authentication URL for the provider. + * + * @param string $state + * + * @return string + */ + protected function getAuthUrl($state) + { + return $this->buildAuthUrlFromBase($this->baseUrl.'/oauth2.0/authorize', $state); + } + + /** + * Get the token URL for the provider. + * + * @return string + */ + protected function getTokenUrl() + { + return $this->baseUrl.'/oauth2.0/token'; + } + + /** + * Get the Post fields for the token request. + * + * @param string $code + * + * @return array + */ + protected function getTokenFields($code) + { + return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; + } + + /** + * Get the access token for the given code. + * + * @param string $code + * + * @return \Overtrue\Socialite\AccessToken + */ + public function getAccessToken($code) + { + $response = $this->getHttpClient()->get($this->getTokenUrl(), [ + 'query' => $this->getTokenFields($code), + ]); + + return $this->parseAccessToken($response->getBody()->getContents()); + } + + /** + * Get the access token from the token response body. + * + * @param string $body + * + * @return \Overtrue\Socialite\AccessToken + */ + public function parseAccessToken($body) + { + parse_str($body, $token); + + return parent::parseAccessToken($token); + } + + /** + * @return self + */ + public function withUnionId() + { + $this->withUnionId = true; + + return $this; + } + + /** + * Get the raw user for the given access token. + * + * @param \Overtrue\Socialite\AccessTokenInterface $token + * + * @return array + */ + protected function getUserByToken(AccessTokenInterface $token) + { + $url = $this->baseUrl.'/oauth2.0/me?access_token='.$token->getToken(); + $this->withUnionId && $url .= '&unionid=1'; + + $response = $this->getHttpClient()->get($url); + + $me = json_decode($this->removeCallback($response->getBody()->getContents()), true); + $this->openId = $me['openid']; + $this->unionId = isset($me['unionid']) ? $me['unionid'] : ''; + + $queries = [ + 'access_token' => $token->getToken(), + 'openid' => $this->openId, + 'oauth_consumer_key' => $this->getConfig()->get('client_id'), + ]; + + $response = $this->getHttpClient()->get($this->baseUrl.'/user/get_user_info?'.http_build_query($queries)); + + return json_decode($this->removeCallback($response->getBody()->getContents()), true); + } + + /** + * Map the raw user array to a Socialite User instance. + * + * @param array $user + * + * @return \Overtrue\Socialite\User + */ + protected function mapUserToObject(array $user) + { + return new User([ + 'id' => $this->openId, + 'unionid' => $this->unionId, + 'nickname' => $this->arrayItem($user, 'nickname'), + 'name' => $this->arrayItem($user, 'nickname'), + 'email' => $this->arrayItem($user, 'email'), + 'avatar' => $this->arrayItem($user, 'figureurl_qq_2'), + ]); + } + + /** + * Remove the fucking callback parentheses. + * + * @param string $response + * + * @return string + */ + protected function removeCallback($response) + { + if (false !== strpos($response, 'callback')) { + $lpos = strpos($response, '('); + $rpos = strrpos($response, ')'); + $response = substr($response, $lpos + 1, $rpos - $lpos - 1); + } + + return $response; + } +} diff --git a/vendor/overtrue/socialite/src/Providers/TaobaoProvider.php b/vendor/overtrue/socialite/src/Providers/TaobaoProvider.php new file mode 100644 index 0000000..4daacd9 --- /dev/null +++ b/vendor/overtrue/socialite/src/Providers/TaobaoProvider.php @@ -0,0 +1,242 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite\Providers; + +use Overtrue\Socialite\AccessTokenInterface; +use Overtrue\Socialite\ProviderInterface; +use Overtrue\Socialite\User; + +/** + * Class TaobaoProvider. + * + * @author mechono + * + * @see https://open.taobao.com/doc.htm?docId=102635&docType=1&source=search [Taobao - OAuth 2.0 授权登录] + */ +class TaobaoProvider extends AbstractProvider implements ProviderInterface +{ + /** + * The base url of Taobao API. + * + * @var string + */ + protected $baseUrl = 'https://oauth.taobao.com'; + + /** + * Taobao API service URL address. + * + * @var string + */ + protected $gatewayUrl = 'https://eco.taobao.com/router/rest'; + + /** + * The API version for the request. + * + * @var string + */ + protected $version = '2.0'; + + /** + * @var string + */ + protected $format = 'json'; + + /** + * @var string + */ + protected $signMethod = 'md5'; + + /** + * Web 对应 PC 端(淘宝 logo )浏览器页面样式;Tmall 对应天猫的浏览器页面样式;Wap 对应无线端的浏览器页面样式。 + */ + protected $view = 'web'; + + /** + * The scopes being requested. + * + * @var array + */ + protected $scopes = ['user_info']; + + /** + * Get the authentication URL for the provider. + * + * @param string $state + * + * @return string + */ + protected function getAuthUrl($state) + { + return $this->buildAuthUrlFromBase($this->baseUrl.'/authorize', $state); + } + + /** + * 获取授权码接口参数. + * + * @param string|null $state + * + * @return array + */ + public function getCodeFields($state = null) + { + $fields = [ + 'client_id' => $this->getConfig()->get('client_id'), + 'redirect_uri' => $this->redirectUrl, + 'view' => $this->view, + 'response_type' => 'code', + ]; + + if ($this->usesState()) { + $fields['state'] = $state; + } + + return $fields; + } + + /** + * Get the token URL for the provider. + * + * @return string + */ + protected function getTokenUrl() + { + return $this->baseUrl.'/token'; + } + + /** + * Get the Post fields for the token request. + * + * @param string $code + * + * @return array + */ + protected function getTokenFields($code) + { + return parent::getTokenFields($code) + ['grant_type' => 'authorization_code', 'view' => $this->view]; + } + + /** + * Get the access token for the given code. + * + * @param string $code + * + * @return \Overtrue\Socialite\AccessToken + */ + public function getAccessToken($code) + { + $response = $this->getHttpClient()->post($this->getTokenUrl(), [ + 'query' => $this->getTokenFields($code), + ]); + + return $this->parseAccessToken($response->getBody()->getContents()); + } + + /** + * Get the access token from the token response body. + * + * @param string $body + * + * @return \Overtrue\Socialite\AccessToken + */ + public function parseAccessToken($body) + { + return parent::parseAccessToken($body); + } + + /** + * Get the raw user for the given access token. + * + * @param \Overtrue\Socialite\AccessTokenInterface $token + * + * @return array + */ + protected function getUserByToken(AccessTokenInterface $token) + { + $response = $this->getHttpClient()->post($this->getUserInfoUrl($this->gatewayUrl, $token)); + + return json_decode($response->getBody(), true); + } + + /** + * Map the raw user array to a Socialite User instance. + * + * @param array $user + * + * @return \Overtrue\Socialite\User + */ + protected function mapUserToObject(array $user) + { + return new User([ + 'id' => $this->arrayItem($user, 'open_id'), + 'nickname' => $this->arrayItem($user, 'nick'), + 'name' => $this->arrayItem($user, 'nick'), + 'avatar' => $this->arrayItem($user, 'avatar'), + ]); + } + + /** + * @param $params + * + * @return string + */ + protected function generateSign($params) + { + ksort($params); + + $stringToBeSigned = $this->getConfig()->get('client_secret'); + + foreach ($params as $k => $v) { + if (!is_array($v) && '@' != substr($v, 0, 1)) { + $stringToBeSigned .= "$k$v"; + } + } + + $stringToBeSigned .= $this->getConfig()->get('client_secret'); + + return strtoupper(md5($stringToBeSigned)); + } + + /** + * @param \Overtrue\Socialite\AccessTokenInterface $token + * @param array $apiFields + * + * @return array + */ + protected function getPublicFields(AccessTokenInterface $token, array $apiFields = []) + { + $fields = [ + 'app_key' => $this->getConfig()->get('client_id'), + 'sign_method' => $this->signMethod, + 'session' => $token->getToken(), + 'timestamp' => date('Y-m-d H:i:s'), + 'v' => $this->version, + 'format' => $this->format, + ]; + + $fields = array_merge($apiFields, $fields); + $fields['sign'] = $this->generateSign($fields); + + return $fields; + } + + /** + * {@inheritdoc}. + */ + protected function getUserInfoUrl($url, AccessTokenInterface $token) + { + $apiFields = ['method' => 'taobao.miniapp.userInfo.get']; + + $query = http_build_query($this->getPublicFields($token, $apiFields), '', '&', $this->encodingType); + + return $url.'?'.$query; + } +} diff --git a/vendor/overtrue/socialite/src/Providers/WeChatProvider.php b/vendor/overtrue/socialite/src/Providers/WeChatProvider.php new file mode 100644 index 0000000..0b76532 --- /dev/null +++ b/vendor/overtrue/socialite/src/Providers/WeChatProvider.php @@ -0,0 +1,234 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite\Providers; + +use Overtrue\Socialite\AccessTokenInterface; +use Overtrue\Socialite\InvalidArgumentException; +use Overtrue\Socialite\ProviderInterface; +use Overtrue\Socialite\User; +use Overtrue\Socialite\WeChatComponentInterface; + +/** + * Class WeChatProvider. + * + * @see http://mp.weixin.qq.com/wiki/9/01f711493b5a02f24b04365ac5d8fd95.html [WeChat - 公众平台OAuth文档] + * @see https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN [网站应用微信登录开发指南] + */ +class WeChatProvider extends AbstractProvider implements ProviderInterface +{ + /** + * The base url of WeChat API. + * + * @var string + */ + protected $baseUrl = 'https://api.weixin.qq.com/sns'; + + /** + * {@inheritdoc}. + */ + protected $openId; + + /** + * {@inheritdoc}. + */ + protected $scopes = ['snsapi_login']; + + /** + * Indicates if the session state should be utilized. + * + * @var bool + */ + protected $stateless = true; + + /** + * Return country code instead of country name. + * + * @var bool + */ + protected $withCountryCode = false; + + /** + * @var WeChatComponentInterface + */ + protected $component; + + /** + * Return country code instead of country name. + * + * @return $this + */ + public function withCountryCode() + { + $this->withCountryCode = true; + + return $this; + } + + /** + * WeChat OpenPlatform 3rd component. + * + * @param WeChatComponentInterface $component + * + * @return $this + */ + public function component(WeChatComponentInterface $component) + { + $this->scopes = ['snsapi_base']; + + $this->component = $component; + + return $this; + } + + /** + * {@inheritdoc}. + */ + public function getAccessToken($code) + { + $response = $this->getHttpClient()->get($this->getTokenUrl(), [ + 'headers' => ['Accept' => 'application/json'], + 'query' => $this->getTokenFields($code), + ]); + + return $this->parseAccessToken($response->getBody()); + } + + /** + * {@inheritdoc}. + */ + protected function getAuthUrl($state) + { + $path = 'oauth2/authorize'; + + if (in_array('snsapi_login', $this->scopes)) { + $path = 'qrconnect'; + } + + return $this->buildAuthUrlFromBase("https://open.weixin.qq.com/connect/{$path}", $state); + } + + /** + * {@inheritdoc}. + */ + protected function buildAuthUrlFromBase($url, $state) + { + $query = http_build_query($this->getCodeFields($state), '', '&', $this->encodingType); + + return $url.'?'.$query.'#wechat_redirect'; + } + + /** + * {@inheritdoc}. + */ + protected function getCodeFields($state = null) + { + if ($this->component) { + $this->with(array_merge($this->parameters, ['component_appid' => $this->component->getAppId()])); + } + + return array_merge([ + 'appid' => $this->getConfig()->get('client_id'), + 'redirect_uri' => $this->redirectUrl, + 'response_type' => 'code', + 'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator), + 'state' => $state ?: md5(time()), + 'connect_redirect' => 1, + ], $this->parameters); + } + + /** + * {@inheritdoc}. + */ + protected function getTokenUrl() + { + if ($this->component) { + return $this->baseUrl.'/oauth2/component/access_token'; + } + + return $this->baseUrl.'/oauth2/access_token'; + } + + /** + * {@inheritdoc}. + */ + protected function getUserByToken(AccessTokenInterface $token) + { + $scopes = explode(',', $token->getAttribute('scope', '')); + + if (in_array('snsapi_base', $scopes)) { + return $token->toArray(); + } + + if (empty($token['openid'])) { + throw new InvalidArgumentException('openid of AccessToken is required.'); + } + + $language = $this->withCountryCode ? null : (isset($this->parameters['lang']) ? $this->parameters['lang'] : 'zh_CN'); + + $response = $this->getHttpClient()->get($this->baseUrl.'/userinfo', [ + 'query' => array_filter([ + 'access_token' => $token->getToken(), + 'openid' => $token['openid'], + 'lang' => $language, + ]), + ]); + + return json_decode($response->getBody(), true); + } + + /** + * {@inheritdoc}. + */ + protected function mapUserToObject(array $user) + { + return new User([ + 'id' => $this->arrayItem($user, 'openid'), + 'name' => $this->arrayItem($user, 'nickname'), + 'nickname' => $this->arrayItem($user, 'nickname'), + 'avatar' => $this->arrayItem($user, 'headimgurl'), + 'email' => null, + ]); + } + + /** + * {@inheritdoc}. + */ + protected function getTokenFields($code) + { + return array_filter([ + 'appid' => $this->getConfig()->get('client_id'), + 'secret' => $this->getConfig()->get('client_secret'), + 'component_appid' => $this->component ? $this->component->getAppId() : null, + 'component_access_token' => $this->component ? $this->component->getToken() : null, + 'code' => $code, + 'grant_type' => 'authorization_code', + ]); + } + + /** + * Remove the fucking callback parentheses. + * + * @param mixed $response + * + * @return string + */ + protected function removeCallback($response) + { + if (false !== strpos($response, 'callback')) { + $lpos = strpos($response, '('); + $rpos = strrpos($response, ')'); + $response = substr($response, $lpos + 1, $rpos - $lpos - 1); + } + + return $response; + } +} diff --git a/vendor/overtrue/socialite/src/Providers/WeWorkProvider.php b/vendor/overtrue/socialite/src/Providers/WeWorkProvider.php new file mode 100644 index 0000000..7efde33 --- /dev/null +++ b/vendor/overtrue/socialite/src/Providers/WeWorkProvider.php @@ -0,0 +1,214 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite\Providers; + +use Overtrue\Socialite\AccessTokenInterface; +use Overtrue\Socialite\ProviderInterface; +use Overtrue\Socialite\User; + +/** + * Class WeWorkProvider. + * + * @author mingyoung + */ +class WeWorkProvider extends AbstractProvider implements ProviderInterface +{ + /** + * @var string + */ + protected $agentId; + + /** + * @var bool + */ + protected $detailed = false; + + /** + * Set agent id. + * + * @param string $agentId + * + * @return $this + */ + public function setAgentId($agentId) + { + $this->agentId = $agentId; + + return $this; + } + + /** + * @param string $agentId + * + * @return $this + */ + public function agent($agentId) + { + return $this->setAgentId($agentId); + } + + /** + * Return user details. + * + * @return $this + */ + public function detailed() + { + $this->detailed = true; + + return $this; + } + + /** + * @param string $state + * + * @return string + */ + protected function getAuthUrl($state) + { + // 网页授权登录 + if (!empty($this->scopes)) { + return $this->getOAuthUrl($state); + } + + // 第三方网页应用登录(扫码登录) + return $this->getQrConnectUrl($state); + } + + /** + * OAuth url. + * + * @param string $state + * + * @return string + */ + protected function getOAuthUrl($state) + { + $queries = [ + 'appid' => $this->getConfig()->get('client_id'), + 'redirect_uri' => $this->redirectUrl, + 'response_type' => 'code', + 'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator), + 'agentid' => $this->agentId, + 'state' => $state, + ]; + + return sprintf('https://open.weixin.qq.com/connect/oauth2/authorize?%s#wechat_redirect', http_build_query($queries)); + } + + /** + * Qr connect url. + * + * @param string $state + * + * @return string + */ + protected function getQrConnectUrl($state) + { + $queries = [ + 'appid' => $this->getConfig()->get('client_id'), + 'agentid' => $this->agentId, + 'redirect_uri' => $this->redirectUrl, + 'state' => $state, + ]; + + return 'https://open.work.weixin.qq.com/wwopen/sso/qrConnect?'.http_build_query($queries); + } + + protected function getTokenUrl() + { + return null; + } + + /** + * @param \Overtrue\Socialite\AccessTokenInterface $token + * + * @return mixed + */ + protected function getUserByToken(AccessTokenInterface $token) + { + $userInfo = $this->getUserInfo($token); + + if ($this->detailed && isset($userInfo['user_ticket'])) { + return $this->getUserDetail($token, $userInfo['user_ticket']); + } + + $this->detailed = false; + + return $userInfo; + } + + /** + * Get user base info. + * + * @param \Overtrue\Socialite\AccessTokenInterface $token + * + * @return mixed + */ + protected function getUserInfo(AccessTokenInterface $token) + { + $response = $this->getHttpClient()->get('https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo', [ + 'query' => array_filter([ + 'access_token' => $token->getToken(), + 'code' => $this->getCode(), + ]), + ]); + + return json_decode($response->getBody(), true); + } + + /** + * Get user detail info. + * + * @param \Overtrue\Socialite\AccessTokenInterface $token + * @param $ticket + * + * @return mixed + */ + protected function getUserDetail(AccessTokenInterface $token, $ticket) + { + $response = $this->getHttpClient()->post('https://qyapi.weixin.qq.com/cgi-bin/user/getuserdetail', [ + 'query' => [ + 'access_token' => $token->getToken(), + ], + 'json' => [ + 'user_ticket' => $ticket, + ], + ]); + + return json_decode($response->getBody(), true); + } + + /** + * @param array $user + * + * @return \Overtrue\Socialite\User + */ + protected function mapUserToObject(array $user) + { + if ($this->detailed) { + return new User([ + 'id' => $this->arrayItem($user, 'userid'), + 'name' => $this->arrayItem($user, 'name'), + 'avatar' => $this->arrayItem($user, 'avatar'), + 'email' => $this->arrayItem($user, 'email'), + ]); + } + + return new User(array_filter([ + 'id' => $this->arrayItem($user, 'UserId') ?: $this->arrayItem($user, 'OpenId'), + 'userId' => $this->arrayItem($user, 'UserId'), + 'openid' => $this->arrayItem($user, 'OpenId'), + 'deviceId' => $this->arrayItem($user, 'DeviceId'), + ])); + } +} diff --git a/vendor/overtrue/socialite/src/Providers/WeiboProvider.php b/vendor/overtrue/socialite/src/Providers/WeiboProvider.php new file mode 100644 index 0000000..47ec56d --- /dev/null +++ b/vendor/overtrue/socialite/src/Providers/WeiboProvider.php @@ -0,0 +1,126 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite\Providers; + +use Overtrue\Socialite\AccessTokenInterface; +use Overtrue\Socialite\ProviderInterface; +use Overtrue\Socialite\User; + +/** + * Class WeiboProvider. + * + * @see http://open.weibo.com/wiki/%E6%8E%88%E6%9D%83%E6%9C%BA%E5%88%B6%E8%AF%B4%E6%98%8E [OAuth 2.0 授权机制说明] + */ +class WeiboProvider extends AbstractProvider implements ProviderInterface +{ + /** + * The base url of Weibo API. + * + * @var string + */ + protected $baseUrl = 'https://api.weibo.com'; + + /** + * The API version for the request. + * + * @var string + */ + protected $version = '2'; + + /** + * The scopes being requested. + * + * @var array + */ + protected $scopes = ['email']; + + /** + * The uid of user authorized. + * + * @var int + */ + protected $uid; + + /** + * Get the authentication URL for the provider. + * + * @param string $state + * + * @return string + */ + protected function getAuthUrl($state) + { + return $this->buildAuthUrlFromBase($this->baseUrl.'/oauth2/authorize', $state); + } + + /** + * Get the token URL for the provider. + * + * @return string + */ + protected function getTokenUrl() + { + return $this->baseUrl.'/'.$this->version.'/oauth2/access_token'; + } + + /** + * Get the Post fields for the token request. + * + * @param string $code + * + * @return array + */ + protected function getTokenFields($code) + { + return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; + } + + /** + * Get the raw user for the given access token. + * + * @param \Overtrue\Socialite\AccessTokenInterface $token + * + * @return array + */ + protected function getUserByToken(AccessTokenInterface $token) + { + $response = $this->getHttpClient()->get($this->baseUrl.'/'.$this->version.'/users/show.json', [ + 'query' => [ + 'uid' => $token['uid'], + 'access_token' => $token->getToken(), + ], + 'headers' => [ + 'Accept' => 'application/json', + ], + ]); + + return json_decode($response->getBody(), true); + } + + /** + * Map the raw user array to a Socialite User instance. + * + * @param array $user + * + * @return \Overtrue\Socialite\User + */ + protected function mapUserToObject(array $user) + { + return new User([ + 'id' => $this->arrayItem($user, 'id'), + 'nickname' => $this->arrayItem($user, 'screen_name'), + 'name' => $this->arrayItem($user, 'name'), + 'email' => $this->arrayItem($user, 'email'), + 'avatar' => $this->arrayItem($user, 'avatar_large'), + ]); + } +} diff --git a/vendor/overtrue/socialite/src/SocialiteManager.php b/vendor/overtrue/socialite/src/SocialiteManager.php new file mode 100644 index 0000000..cc109ab --- /dev/null +++ b/vendor/overtrue/socialite/src/SocialiteManager.php @@ -0,0 +1,251 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite; + +use Closure; +use InvalidArgumentException; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Session\Session; + +/** + * Class SocialiteManager. + */ +class SocialiteManager implements FactoryInterface +{ + /** + * The configuration. + * + * @var \Overtrue\Socialite\Config + */ + protected $config; + + /** + * The request instance. + * + * @var Request + */ + protected $request; + + /** + * The registered custom driver creators. + * + * @var array + */ + protected $customCreators = []; + + /** + * The initial drivers. + * + * @var array + */ + protected $initialDrivers = [ + 'facebook' => 'Facebook', + 'github' => 'GitHub', + 'google' => 'Google', + 'linkedin' => 'Linkedin', + 'weibo' => 'Weibo', + 'qq' => 'QQ', + 'wechat' => 'WeChat', + 'douban' => 'Douban', + 'wework' => 'WeWork', + 'outlook' => 'Outlook', + 'douyin' => 'DouYin', + 'taobao' => 'Taobao', + 'feishu' => 'FeiShu', + ]; + + /** + * The array of created "drivers". + * + * @var ProviderInterface[] + */ + protected $drivers = []; + + /** + * SocialiteManager constructor. + * + * @param array $config + * @param Request|null $request + */ + public function __construct(array $config, Request $request = null) + { + $this->config = new Config($config); + + if ($this->config->has('guzzle')) { + Providers\AbstractProvider::setGuzzleOptions($this->config->get('guzzle')); + } + + if ($request) { + $this->setRequest($request); + } + } + + /** + * Set config instance. + * + * @param \Overtrue\Socialite\Config $config + * + * @return $this + */ + public function config(Config $config) + { + $this->config = $config; + + return $this; + } + + /** + * Get a driver instance. + * + * @param string $driver + * + * @return ProviderInterface + */ + public function driver($driver) + { + $driver = strtolower($driver); + + if (!isset($this->drivers[$driver])) { + $this->drivers[$driver] = $this->createDriver($driver); + } + + return $this->drivers[$driver]; + } + + /** + * @param \Symfony\Component\HttpFoundation\Request $request + * + * @return $this + */ + public function setRequest(Request $request) + { + $this->request = $request; + + return $this; + } + + /** + * @return \Symfony\Component\HttpFoundation\Request + */ + public function getRequest() + { + return $this->request ?: $this->createDefaultRequest(); + } + + /** + * Create a new driver instance. + * + * @param string $driver + * + * @throws \InvalidArgumentException + * + * @return ProviderInterface + */ + protected function createDriver($driver) + { + if (isset($this->customCreators[$driver])) { + return $this->callCustomCreator($driver); + } + + if (isset($this->initialDrivers[$driver])) { + $provider = $this->initialDrivers[$driver]; + $provider = __NAMESPACE__.'\\Providers\\'.$provider.'Provider'; + + return $this->buildProvider($provider, $this->formatConfig($this->config->get($driver))); + } + + throw new InvalidArgumentException("Driver [$driver] not supported."); + } + + /** + * Call a custom driver creator. + * + * @param string $driver + * + * @return ProviderInterface + */ + protected function callCustomCreator($driver) + { + return $this->customCreators[$driver]($this->config); + } + + /** + * Create default request instance. + * + * @return Request + */ + protected function createDefaultRequest() + { + $request = Request::createFromGlobals(); + $session = new Session(); + + $request->setSession($session); + + return $request; + } + + /** + * Register a custom driver creator Closure. + * + * @param string $driver + * @param \Closure $callback + * + * @return $this + */ + public function extend($driver, Closure $callback) + { + $driver = strtolower($driver); + + $this->customCreators[$driver] = $callback; + + return $this; + } + + /** + * Get all of the created "drivers". + * + * @return ProviderInterface[] + */ + public function getDrivers() + { + return $this->drivers; + } + + /** + * Build an OAuth 2 provider instance. + * + * @param string $provider + * @param array $config + * + * @return ProviderInterface + */ + public function buildProvider($provider, $config) + { + return new $provider($this->getRequest(), $config); + } + + /** + * Format the server configuration. + * + * @param array $config + * + * @return array + */ + public function formatConfig(array $config) + { + return array_merge([ + 'identifier' => $config['client_id'], + 'secret' => $config['client_secret'], + 'callback_uri' => $config['redirect'], + ], $config); + } +} diff --git a/vendor/overtrue/socialite/src/User.php b/vendor/overtrue/socialite/src/User.php new file mode 100644 index 0000000..761ac8f --- /dev/null +++ b/vendor/overtrue/socialite/src/User.php @@ -0,0 +1,204 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite; + +use ArrayAccess; +use JsonSerializable; + +/** + * Class User. + */ +class User implements ArrayAccess, UserInterface, JsonSerializable, \Serializable +{ + use HasAttributes; + + /** + * User constructor. + * + * @param array $attributes + */ + public function __construct(array $attributes) + { + $this->attributes = $attributes; + } + + /** + * Get the unique identifier for the user. + * + * @return string + */ + public function getId() + { + return $this->getAttribute('id'); + } + + /** + * Get the username for the user. + * + * @return string + */ + public function getUsername() + { + return $this->getAttribute('username', $this->getId()); + } + + /** + * Get the nickname / username for the user. + * + * @return string + */ + public function getNickname() + { + return $this->getAttribute('nickname'); + } + + /** + * Get the full name of the user. + * + * @return string + */ + public function getName() + { + return $this->getAttribute('name'); + } + + /** + * Get the e-mail address of the user. + * + * @return string + */ + public function getEmail() + { + return $this->getAttribute('email'); + } + + /** + * Get the avatar / image URL for the user. + * + * @return string + */ + public function getAvatar() + { + return $this->getAttribute('avatar'); + } + + /** + * Set the token on the user. + * + * @param \Overtrue\Socialite\AccessTokenInterface $token + * + * @return $this + */ + public function setToken(AccessTokenInterface $token) + { + $this->setAttribute('token', $token->getToken()); + $this->setAttribute('access_token', $token->getToken()); + + if (\is_callable([$token, 'getRefreshToken'])) { + $this->setAttribute('refresh_token', $token->getRefreshToken()); + } + + return $this; + } + + /** + * @param string $provider + * + * @return $this + */ + public function setProviderName($provider) + { + $this->setAttribute('provider', $provider); + + return $this; + } + + /** + * @return string + */ + public function getProviderName() + { + return $this->getAttribute('provider'); + } + + /** + * Get the authorized token. + * + * @return \Overtrue\Socialite\AccessToken + */ + public function getToken() + { + return new AccessToken([ + 'access_token' => $this->getAccessToken(), + 'refresh_token' => $this->getAttribute('refresh_token') + ]); + } + + /** + * Get user access token. + * + * @return string + */ + public function getAccessToken() + { + return $this->getAttribute('token') ?: $this->getAttribute('access_token'); + } + + /** + * Get user refresh token. + * + * @return string + */ + public function getRefreshToken() + { + return $this->getAttribute('refresh_token'); + } + + /** + * Get the original attributes. + * + * @return array + */ + public function getOriginal() + { + return $this->getAttribute('original'); + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() + { + return $this->attributes; + } + + public function serialize() + { + return serialize($this->attributes); + } + + /** + * Constructs the object. + * + * @see https://php.net/manual/en/serializable.unserialize.php + * + * @param string $serialized

              + * The string representation of the object. + *

              + * + * @since 5.1.0 + */ + public function unserialize($serialized) + { + $this->attributes = unserialize($serialized) ?: []; + } +} diff --git a/vendor/overtrue/socialite/src/UserInterface.php b/vendor/overtrue/socialite/src/UserInterface.php new file mode 100644 index 0000000..1403339 --- /dev/null +++ b/vendor/overtrue/socialite/src/UserInterface.php @@ -0,0 +1,53 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite; + +/** + * Interface UserInterface. + */ +interface UserInterface +{ + /** + * Get the unique identifier for the user. + * + * @return string + */ + public function getId(); + + /** + * Get the nickname / username for the user. + * + * @return string + */ + public function getNickname(); + + /** + * Get the full name of the user. + * + * @return string + */ + public function getName(); + + /** + * Get the e-mail address of the user. + * + * @return string + */ + public function getEmail(); + + /** + * Get the avatar / image URL for the user. + * + * @return string + */ + public function getAvatar(); +} diff --git a/vendor/overtrue/socialite/src/WeChatComponentInterface.php b/vendor/overtrue/socialite/src/WeChatComponentInterface.php new file mode 100644 index 0000000..1754521 --- /dev/null +++ b/vendor/overtrue/socialite/src/WeChatComponentInterface.php @@ -0,0 +1,32 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\Socialite; + +/** + * Interface WeChatComponentInterface. + */ +interface WeChatComponentInterface +{ + /** + * Return the open-platform component app id. + * + * @return string + */ + public function getAppId(); + + /** + * Return the open-platform component access token string. + * + * @return string + */ + public function getToken(); +} diff --git a/vendor/overtrue/socialite/tests/OAuthTest.php b/vendor/overtrue/socialite/tests/OAuthTest.php new file mode 100644 index 0000000..64d360b --- /dev/null +++ b/vendor/overtrue/socialite/tests/OAuthTest.php @@ -0,0 +1,243 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +use Mockery as m; +use Overtrue\Socialite\AccessTokenInterface; +use Overtrue\Socialite\Providers\AbstractProvider; +use Overtrue\Socialite\User; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; + +class OAuthTest extends TestCase +{ + public function tearDown() + { + m::close(); + } + + public function testAbstractProviderBackwardCompatible() + { + $request = Request::create('foo'); + $request->setSession($session = m::mock('Symfony\Component\HttpFoundation\Session\SessionInterface')); + $session->shouldReceive('put')->once(); + $provider = new OAuthTwoTestProviderStub($request, 'client_id', 'client_secret', 'redirect'); + + $this->assertSame('client_id', $provider->getConfig()['client_id']); + $this->assertSame('client_secret', $provider->getConfig()['client_secret']); + $this->assertSame('redirect', $provider->getConfig()['redirect']); + + $response = $provider->redirect(); + + $this->assertInstanceOf('Symfony\Component\HttpFoundation\RedirectResponse', $response); + $this->assertSame('http://auth.url', $response->getTargetUrl()); + } + + public function testRedirectGeneratesTheProperSymfonyRedirectResponse() + { + $request = Request::create('foo'); + $request->setSession($session = m::mock('Symfony\Component\HttpFoundation\Session\SessionInterface')); + $session->shouldReceive('put')->once(); + $provider = new OAuthTwoTestProviderStub( + $request, [ + 'client_id' => 'client_id', + 'client_secret' => 'client_secret', + 'redirect' => 'redirect', + ] + ); + $response = $provider->redirect(); + + $this->assertInstanceOf('Symfony\Component\HttpFoundation\RedirectResponse', $response); + $this->assertSame('http://auth.url', $response->getTargetUrl()); + } + + public function testRedirectUrl() + { + $request = Request::create('foo', 'GET', ['state' => str_repeat('A', 40), 'code' => 'code']); + $request->setSession($session = m::mock('Symfony\Component\HttpFoundation\Session\SessionInterface')); + + $provider = new OAuthTwoTestProviderStub( + $request, [ + 'client_id' => 'client_id', + 'client_secret' => 'client_secret', + ] + ); + $this->assertNull($provider->getRedirectUrl()); + + $provider = new OAuthTwoTestProviderStub( + $request, [ + 'client_id' => 'client_id', + 'client_secret' => 'client_secret', + 'redirect' => 'redirect_uri', + ] + ); + $this->assertSame('redirect_uri', $provider->getRedirectUrl()); + $provider->setRedirectUrl('overtrue.me'); + $this->assertSame('overtrue.me', $provider->getRedirectUrl()); + + $provider->withRedirectUrl('http://overtrue.me'); + $this->assertSame('http://overtrue.me', $provider->getRedirectUrl()); + } + + public function testUserReturnsAUserInstanceForTheAuthenticatedRequest() + { + $request = Request::create('foo', 'GET', ['state' => str_repeat('A', 40), 'code' => 'code']); + $request->setSession($session = m::mock('Symfony\Component\HttpFoundation\Session\SessionInterface')); + + $session->shouldReceive('get')->once()->with('state')->andReturn(str_repeat('A', 40)); + $provider = new OAuthTwoTestProviderStub( + $request, [ + 'client_id' => 'client_id', + 'client_secret' => 'client_secret', + 'redirect' => 'redirect_uri', + ] + ); + $provider->http = m::mock('StdClass'); + $provider->http->shouldReceive('post')->once()->with( + 'http://token.url', + [ + 'headers' => ['Accept' => 'application/json'], + 'form_params' => [ + 'client_id' => 'client_id', + 'client_secret' => 'client_secret', + 'code' => 'code', + 'redirect_uri' => 'redirect_uri', + ], + ] + )->andReturn($response = m::mock('StdClass')); + $response->shouldReceive('getBody')->once()->andReturn('{"access_token":"access_token"}'); + $user = $provider->user(); + + $this->assertInstanceOf('Overtrue\Socialite\User', $user); + $this->assertSame('foo', $user->getId()); + } + + /** + * @expectedException \Overtrue\Socialite\InvalidStateException + */ + public function testExceptionIsThrownIfStateIsInvalid() + { + $request = Request::create('foo', 'GET', ['state' => str_repeat('B', 40), 'code' => 'code']); + $request->setSession($session = m::mock('Symfony\Component\HttpFoundation\Session\SessionInterface')); + $session->shouldReceive('get')->once()->with('state')->andReturn(str_repeat('A', 40)); + $provider = new OAuthTwoTestProviderStub( + $request, [ + 'client_id' => 'client_id', + 'client_secret' => 'client_secret', + 'redirect' => 'redirect', + ] + ); + $user = $provider->user(); + } + + /** + * @expectedException \Overtrue\Socialite\AuthorizeFailedException + * @expectedExceptionMessage Authorize Failed: {"error":"scope is invalid"} + */ + public function testExceptionisThrownIfAuthorizeFailed() + { + $request = Request::create('foo', 'GET', ['state' => str_repeat('A', 40), 'code' => 'code']); + $request->setSession($session = m::mock('Symfony\Component\HttpFoundation\Session\SessionInterface')); + $session->shouldReceive('get')->once()->with('state')->andReturn(str_repeat('A', 40)); + $provider = new OAuthTwoTestProviderStub( + $request, [ + 'client_id' => 'client_id', + 'client_secret' => 'client_secret', + 'redirect' => 'redirect_uri', + ] + ); + $provider->http = m::mock('StdClass'); + $provider->http->shouldReceive('post')->once()->with( + 'http://token.url', + [ + 'headers' => ['Accept' => 'application/json'], + 'form_params' => [ + 'client_id' => 'client_id', + 'client_secret' => 'client_secret', + 'code' => 'code', + 'redirect_uri' => 'redirect_uri', + ], + ] + )->andReturn($response = m::mock('StdClass')); + $response->shouldReceive('getBody')->once()->andReturn('{"error":"scope is invalid"}'); + $user = $provider->user(); + } + + /** + * @expectedException \Overtrue\Socialite\InvalidStateException + */ + public function testExceptionIsThrownIfStateIsNotSet() + { + $request = Request::create('foo', 'GET', ['state' => 'state', 'code' => 'code']); + $request->setSession($session = m::mock('Symfony\Component\HttpFoundation\Session\SessionInterface')); + $session->shouldReceive('get')->once()->with('state'); + $provider = new OAuthTwoTestProviderStub( + $request, [ + 'client_id' => 'client_id', + 'client_secret' => 'client_secret', + 'redirect' => 'redirect', + ] + ); + $user = $provider->user(); + } + + public function testDriverName() + { + $request = Request::create('foo', 'GET', ['state' => 'state', 'code' => 'code']); + $provider = new OAuthTwoTestProviderStub( + $request, [ + 'client_id' => 'client_id', + 'client_secret' => 'client_secret', + 'redirect' => 'redirect', + ] + ); + + $this->assertSame('OAuthTwoTest', $provider->getName()); + } +} + +class OAuthTwoTestProviderStub extends AbstractProvider +{ + public $http; + + protected function getAuthUrl($state) + { + return 'http://auth.url'; + } + + protected function getTokenUrl() + { + return 'http://token.url'; + } + + protected function getUserByToken(AccessTokenInterface $token) + { + return ['id' => 'foo']; + } + + protected function mapUserToObject(array $user) + { + return new User(['id' => $user['id']]); + } + + /** + * Get a fresh instance of the Guzzle HTTP client. + * + * @return \GuzzleHttp\Client + */ + protected function getHttpClient() + { + if ($this->http) { + return $this->http; + } + + return $this->http = m::mock('StdClass'); + } +} diff --git a/vendor/overtrue/socialite/tests/Providers/WeWorkProviderTest.php b/vendor/overtrue/socialite/tests/Providers/WeWorkProviderTest.php new file mode 100644 index 0000000..9780930 --- /dev/null +++ b/vendor/overtrue/socialite/tests/Providers/WeWorkProviderTest.php @@ -0,0 +1,60 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +use Overtrue\Socialite\Providers\WeWorkProvider; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; + +class WeWorkProviderTest extends TestCase +{ + public function testQrConnect() + { + $response = (new WeWorkProvider(Request::create('foo'), [ + 'client_id' => 'ww100000a5f2191', + 'client_secret' => 'client_secret', + 'redirect' => 'http://www.oa.com', + ])) + ->setAgentId('1000000') + ->stateless() + ->redirect(); + + $this->assertSame('https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=ww100000a5f2191&agentid=1000000&redirect_uri=http%3A%2F%2Fwww.oa.com', $response->getTargetUrl()); + } + + public function testOAuthWithAgentId() + { + $response = (new WeWorkProvider(Request::create('foo'), [ + 'client_id' => 'CORPID', + 'client_secret' => 'client_secret', + 'redirect' => 'REDIRECT_URI', + ])) + ->scopes(['snsapi_base']) + ->setAgentId('1000000') + ->stateless() + ->redirect(); + + $this->assertSame('https://open.weixin.qq.com/connect/oauth2/authorize?appid=CORPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_base&agentid=1000000#wechat_redirect', $response->getTargetUrl()); + } + + public function testOAuthWithoutAgentId() + { + $response = (new WeWorkProvider(Request::create('foo'), [ + 'client_id' => 'CORPID', + 'client_secret' => 'client_secret', + 'redirect' => 'REDIRECT_URI', + ])) + ->scopes(['snsapi_base']) + ->stateless() + ->redirect(); + + $this->assertSame('https://open.weixin.qq.com/connect/oauth2/authorize?appid=CORPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_base#wechat_redirect', $response->getTargetUrl()); + } +} diff --git a/vendor/overtrue/socialite/tests/UserTest.php b/vendor/overtrue/socialite/tests/UserTest.php new file mode 100644 index 0000000..49adf43 --- /dev/null +++ b/vendor/overtrue/socialite/tests/UserTest.php @@ -0,0 +1,45 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +use Overtrue\Socialite\AccessToken; +use Overtrue\Socialite\User; +use PHPUnit\Framework\TestCase; + +class UserTest extends TestCase +{ + public function testJsonserialize() + { + $this->assertSame('[]', json_encode(new User([]))); + + $this->assertSame('{"token":"mock-token"}', json_encode(new User(['token' => new AccessToken(['access_token' => 'mock-token'])]))); + } + + public function test_it_can_get_refresh_token() + { + $user = new User([ + 'access_token' => 'mock-token', + 'refresh_token' => 'fake_refresh', + ]); + + // 能通过用 User 对象获取 refresh token + $this->assertSame('fake_refresh', $user->getRefreshToken()); + // json 序列化只有 token 字段 + $this->assertSame('{"access_token":"mock-token","refresh_token":"fake_refresh"}', json_encode($user)); + + $user = new User([]); + $user->setToken(new AccessToken([ + 'access_token' => 'mock-token', + 'refresh_token' => 'fake_refresh', + ])); + $this->assertSame('fake_refresh', $user->getRefreshToken()); + $this->assertSame('{"token":"mock-token","access_token":"mock-token","refresh_token":"fake_refresh"}', json_encode($user)); + } +} diff --git a/vendor/overtrue/socialite/tests/WechatProviderTest.php b/vendor/overtrue/socialite/tests/WechatProviderTest.php new file mode 100644 index 0000000..74621df --- /dev/null +++ b/vendor/overtrue/socialite/tests/WechatProviderTest.php @@ -0,0 +1,137 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +use Overtrue\Socialite\Providers\WeChatProvider as RealWeChatProvider; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; + +class WechatProviderTest extends TestCase +{ + public function testWeChatProviderHasCorrectlyRedirectResponse() + { + $response = (new WeChatProvider(Request::create('foo'), [ + 'client_id' => 'client_id', + 'client_secret' => 'client_secret', + 'redirect' => 'http://localhost/socialite/callback.php', + ]))->redirect(); + + $this->assertInstanceOf('Symfony\Component\HttpFoundation\RedirectResponse', $response); + $this->assertStringStartsWith('https://open.weixin.qq.com/connect/qrconnect', $response->getTargetUrl()); + $this->assertRegExp('/redirect_uri=http%3A%2F%2Flocalhost%2Fsocialite%2Fcallback.php/', $response->getTargetUrl()); + } + + public function testWeChatProviderTokenUrlAndRequestFields() + { + $provider = new WeChatProvider(Request::create('foo'), [ + 'client_id' => 'client_id', + 'client_secret' => 'client_secret', + 'redirect' => 'http://localhost/socialite/callback.php', + ]); + + $this->assertSame('https://api.weixin.qq.com/sns/oauth2/access_token', $provider->tokenUrl()); + $this->assertSame([ + 'appid' => 'client_id', + 'secret' => 'client_secret', + 'code' => 'iloveyou', + 'grant_type' => 'authorization_code', + ], $provider->tokenFields('iloveyou')); + + $this->assertSame([ + 'appid' => 'client_id', + 'redirect_uri' => 'http://localhost/socialite/callback.php', + 'response_type' => 'code', + 'scope' => 'snsapi_login', + 'state' => 'wechat-state', + 'connect_redirect' => 1, + ], $provider->codeFields('wechat-state')); + } + + public function testOpenPlatformComponent() + { + $provider = new WeChatProvider(Request::create('foo'), [ + 'client_id' => 'client_id', + 'client_secret' => null, + 'redirect' => 'redirect-url', + ]); + $provider->component(new WeChatComponent()); + $this->assertSame([ + 'appid' => 'client_id', + 'redirect_uri' => 'redirect-url', + 'response_type' => 'code', + 'scope' => 'snsapi_base', + 'state' => 'state', + 'connect_redirect' => 1, + 'component_appid' => 'component-app-id', + ], $provider->codeFields('state')); + + $this->assertSame([ + 'appid' => 'client_id', + 'component_appid' => 'component-app-id', + 'component_access_token' => 'token', + 'code' => 'simcode', + 'grant_type' => 'authorization_code', + ], $provider->tokenFields('simcode')); + + $this->assertSame('https://api.weixin.qq.com/sns/oauth2/component/access_token', $provider->tokenUrl()); + } + + public function testOpenPlatformComponentWithCustomParameters() + { + $provider = new WeChatProvider(Request::create('foo'), [ + 'client_id' => 'client_id', + 'client_secret' => null, + 'redirect' => 'redirect-url', + ]); + $provider->component(new WeChatComponent()); + $provider->with(['foo' => 'bar']); + + $fields = $provider->codeFields('wechat-state'); + + $this->assertArrayHasKey('foo', $fields); + $this->assertSame('bar', $fields['foo']); + } +} + +trait ProviderTrait +{ + public function tokenUrl() + { + return $this->getTokenUrl(); + } + + public function tokenFields($code) + { + return $this->getTokenFields($code); + } + + public function codeFields($state = null) + { + return $this->getCodeFields($state); + } +} + +class WeChatProvider extends RealWeChatProvider +{ + use ProviderTrait; +} + +class WeChatComponent implements \Overtrue\Socialite\WeChatComponentInterface +{ + public function getAppId() + { + return 'component-app-id'; + } + + public function getToken() + { + return 'token'; + } +} diff --git a/vendor/overtrue/wechat/CHANGELOG.md b/vendor/overtrue/wechat/CHANGELOG.md new file mode 100644 index 0000000..df42aa6 --- /dev/null +++ b/vendor/overtrue/wechat/CHANGELOG.md @@ -0,0 +1,1401 @@ +# Change Log + +## [Unreleased](https://github.com/overtrue/wechat/tree/HEAD) + +[Full Changelog](https://github.com/overtrue/wechat/compare/4.0.0...HEAD) + +**Closed issues:** + +- 能否增加对symfony4的支持 [\#1044](https://github.com/overtrue/wechat/issues/1044) + +## [4.0.0](https://github.com/overtrue/wechat/tree/4.0.0) (2017-12-11) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.21...4.0.0) + +**Closed issues:** + +- filecache.php 文件 createPathIfNeeded\(string $path\) : bool [\#1046](https://github.com/overtrue/wechat/issues/1046) +- 沙箱模式的Notify总是出错:Invalid request payloads. [\#1045](https://github.com/overtrue/wechat/issues/1045) +- 你好我是SwooleDistributed框架的作者 [\#1040](https://github.com/overtrue/wechat/issues/1040) + +## [3.3.21](https://github.com/overtrue/wechat/tree/3.3.21) (2017-12-10) +[Full Changelog](https://github.com/overtrue/wechat/compare/4.0.0-beta.4...3.3.21) + +**Closed issues:** + +- 开启开放平台自动路由后报错 [\#1042](https://github.com/overtrue/wechat/issues/1042) +- 关于3.x升级4.x的问题 [\#1041](https://github.com/overtrue/wechat/issues/1041) +- 获取不了unionid [\#1038](https://github.com/overtrue/wechat/issues/1038) +- authorizer\_refresh\_token刷新问题 [\#1033](https://github.com/overtrue/wechat/issues/1033) +- lumen+swoole无法获取request信息。 [\#1032](https://github.com/overtrue/wechat/issues/1032) +- 上传素材报错, empty post data hint [\#1031](https://github.com/overtrue/wechat/issues/1031) +- 开放平台不支持全网发布接入检测(第三方) [\#1029](https://github.com/overtrue/wechat/issues/1029) +- 公众号模板消息不兼容跳转小程序 [\#1025](https://github.com/overtrue/wechat/issues/1025) +- swoole下无法使用 [\#1017](https://github.com/overtrue/wechat/issues/1017) +- 请教有没有高清素材下载方法? [\#997](https://github.com/overtrue/wechat/issues/997) +- 自动回复多图文素材,错误 [\#996](https://github.com/overtrue/wechat/issues/996) +- xml解释失败 [\#989](https://github.com/overtrue/wechat/issues/989) +- Curl error 77 [\#982](https://github.com/overtrue/wechat/issues/982) +- 3.1.10 H5支付不晓得算不算BUG的BUG [\#968](https://github.com/overtrue/wechat/issues/968) +- 请问是否有遇到微信扫码或内部打开外部网站出现请求2次的情况 [\#963](https://github.com/overtrue/wechat/issues/963) +- 请问4.0何时正式发布? [\#962](https://github.com/overtrue/wechat/issues/962) +- dev-master 不能用于laravel5.1 [\#952](https://github.com/overtrue/wechat/issues/952) +- 请教小程序的模板消息是否支持 [\#920](https://github.com/overtrue/wechat/issues/920) +- 模板消息的颜色设置问题 [\#914](https://github.com/overtrue/wechat/issues/914) +- 英文文档跳转问题 [\#854](https://github.com/overtrue/wechat/issues/854) +- \[4.0\] 功能测试 [\#849](https://github.com/overtrue/wechat/issues/849) +- \[4.0\] 命名变更 [\#743](https://github.com/overtrue/wechat/issues/743) + +**Merged pull requests:** + +- Scrutinizer Auto-Fixes [\#1043](https://github.com/overtrue/wechat/pull/1043) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- 修复解密小程序转发信息数据\(wx.getShareInfo\)失败的问题 [\#1037](https://github.com/overtrue/wechat/pull/1037) ([yyqqing](https://github.com/yyqqing)) +- 修復微信支付沙盒模式的通知結果本地校驗失敗錯誤。 [\#1036](https://github.com/overtrue/wechat/pull/1036) ([amyuki](https://github.com/amyuki)) +- 修复 verifyTicket 使用不了自定义缓存的问题 [\#1034](https://github.com/overtrue/wechat/pull/1034) ([mingyoung](https://github.com/mingyoung)) +- 🚧 Auto discover extensions. [\#1027](https://github.com/overtrue/wechat/pull/1027) ([mingyoung](https://github.com/mingyoung)) + +## [4.0.0-beta.4](https://github.com/overtrue/wechat/tree/4.0.0-beta.4) (2017-11-21) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.20...4.0.0-beta.4) + +**Closed issues:** + +- Order对象的$attributes中不能传入device\_info参数 [\#1030](https://github.com/overtrue/wechat/issues/1030) +- 默认文件缓存的路径是否可以简单修改? [\#1023](https://github.com/overtrue/wechat/issues/1023) +- 3.3.17 版本获取 token 的问题 [\#1022](https://github.com/overtrue/wechat/issues/1022) +- \[V3\] AccessToken.php:243 [\#1021](https://github.com/overtrue/wechat/issues/1021) + +**Merged pull requests:** + +- more detailed cache key. [\#1028](https://github.com/overtrue/wechat/pull/1028) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#1026](https://github.com/overtrue/wechat/pull/1026) ([mingyoung](https://github.com/mingyoung)) +- Specify the request instance. [\#1024](https://github.com/overtrue/wechat/pull/1024) ([mingyoung](https://github.com/mingyoung)) + +## [3.3.20](https://github.com/overtrue/wechat/tree/3.3.20) (2017-11-13) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.18...3.3.20) + +## [3.3.18](https://github.com/overtrue/wechat/tree/3.3.18) (2017-11-11) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.17...3.3.18) + +**Closed issues:** + +- 临时二维码接口无法生成以字符串为参数的二维码 [\#1020](https://github.com/overtrue/wechat/issues/1020) +- 现金红包出现了500错误,跟进显示http\_error:true [\#1016](https://github.com/overtrue/wechat/issues/1016) +- 4.0 企业微信agent OA的错误 [\#1015](https://github.com/overtrue/wechat/issues/1015) +- 求thinkphp框架demo [\#1010](https://github.com/overtrue/wechat/issues/1010) +- 沙箱模式获取验签 key 时产生无限循环 , 无法正常获取 [\#1009](https://github.com/overtrue/wechat/issues/1009) +- JSSDK里面url导致的invalid signature错误 [\#1002](https://github.com/overtrue/wechat/issues/1002) +- 微信支付沙箱模式下,回调验签错误 [\#998](https://github.com/overtrue/wechat/issues/998) +- 有微信退款回调接口吗? [\#985](https://github.com/overtrue/wechat/issues/985) +- 希望兼容新出的微信H5支付 [\#966](https://github.com/overtrue/wechat/issues/966) +- 小程序生成无限量二维码接口缺少参数 [\#965](https://github.com/overtrue/wechat/issues/965) + +**Merged pull requests:** + +- 查询企业付款接口参数调整,加入企业付款到银行卡接口(RSA 参数加密待完成) [\#1019](https://github.com/overtrue/wechat/pull/1019) ([tianyong90](https://github.com/tianyong90)) +- Token AESKey can be null. [\#1013](https://github.com/overtrue/wechat/pull/1013) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#1012](https://github.com/overtrue/wechat/pull/1012) ([mingyoung](https://github.com/mingyoung)) +- Add Mini-program tester's binding/unbinding feature [\#1011](https://github.com/overtrue/wechat/pull/1011) ([caikeal](https://github.com/caikeal)) +- Apply fixes from StyleCI [\#1008](https://github.com/overtrue/wechat/pull/1008) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#1007](https://github.com/overtrue/wechat/pull/1007) ([overtrue](https://github.com/overtrue)) +- Added open-platform's mini-program code management [\#1003](https://github.com/overtrue/wechat/pull/1003) ([caikeal](https://github.com/caikeal)) +- Cleanup payment [\#1001](https://github.com/overtrue/wechat/pull/1001) ([mingyoung](https://github.com/mingyoung)) +- Unify get stream. [\#995](https://github.com/overtrue/wechat/pull/995) ([mingyoung](https://github.com/mingyoung)) +- Add appCode `page` param. [\#991](https://github.com/overtrue/wechat/pull/991) ([mingyoung](https://github.com/mingyoung)) + +## [3.3.17](https://github.com/overtrue/wechat/tree/3.3.17) (2017-10-27) +[Full Changelog](https://github.com/overtrue/wechat/compare/4.0.0-beta.3...3.3.17) + +**Closed issues:** + +- open platform component\_verify\_ticket 错误 [\#984](https://github.com/overtrue/wechat/issues/984) +- 请教下载语音后的文件不完整怎么处理? [\#980](https://github.com/overtrue/wechat/issues/980) +- 微信支付 API 调用下单解析缓缓 [\#977](https://github.com/overtrue/wechat/issues/977) +- 是否可以加入微信收款(个人转账版)服务接口 [\#970](https://github.com/overtrue/wechat/issues/970) +- 微信公众号消息加解密方式‘兼容模式’也需要填写‘aes\_key’参数,不能为空 [\#967](https://github.com/overtrue/wechat/issues/967) +- 第三方平台 接收消息一直报错 但是能回复消息 也会提示错误 [\#961](https://github.com/overtrue/wechat/issues/961) +- 中文官网无法访问 [\#960](https://github.com/overtrue/wechat/issues/960) +- laravel队列中使用了SDK报Component verify ticket does not exists. [\#958](https://github.com/overtrue/wechat/issues/958) +- 接口调用次数每日限额清零方法没有? [\#953](https://github.com/overtrue/wechat/issues/953) +- 获取access\_toekn失败之后抛出异常的地方,能够与其他地方统一使用下述这个 resolveResponse 返回数据 [\#951](https://github.com/overtrue/wechat/issues/951) +- 官网挂了 [\#950](https://github.com/overtrue/wechat/issues/950) +- 无法接收到菜单点击事件推送的消息 [\#949](https://github.com/overtrue/wechat/issues/949) +- 请教这个sdk是否可用于android 或者ios 登录? [\#948](https://github.com/overtrue/wechat/issues/948) +- 关于access token 后端分布式部署的中控服务器的问题 [\#947](https://github.com/overtrue/wechat/issues/947) +- 4.0 不支持laravel 5.2? [\#946](https://github.com/overtrue/wechat/issues/946) +- log不能打印出来 [\#945](https://github.com/overtrue/wechat/issues/945) +- EasyWeChat.org域名挂了?? [\#940](https://github.com/overtrue/wechat/issues/940) +- 微信静默授权的时候,页面上老是会显示一段很长的英文Redirecting to http://xxxx,很影响用户体验,有没有什么方法可以去掉,保留空白页,或者允许自定义显示内容 [\#939](https://github.com/overtrue/wechat/issues/939) +- 微信小程序生成二维码(接口B)微信扫描不出来结果 [\#938](https://github.com/overtrue/wechat/issues/938) +- 官网可否支持看老版本的文档? [\#937](https://github.com/overtrue/wechat/issues/937) +- 客服发送消息 收到的中文信息被unicode 编码 [\#935](https://github.com/overtrue/wechat/issues/935) +- 有多个商户时,订单通知的 $payment 怎么创建 [\#934](https://github.com/overtrue/wechat/issues/934) +- console中使用$app-\>user-\>get报错 [\#932](https://github.com/overtrue/wechat/issues/932) +- PC端扫描登录的问题 [\#930](https://github.com/overtrue/wechat/issues/930) +- 关于小程序支付的疑问 [\#912](https://github.com/overtrue/wechat/issues/912) +- 服务商api模式使用可以更加详细吗 [\#653](https://github.com/overtrue/wechat/issues/653) + +**Merged pull requests:** + +- 修正 微信公众号要求 所有接口使用 HTTPS 方式访问 [\#988](https://github.com/overtrue/wechat/pull/988) ([drogjh](https://github.com/drogjh)) +- Apply fixes from StyleCI [\#987](https://github.com/overtrue/wechat/pull/987) ([mingyoung](https://github.com/mingyoung)) +- 修复微信收款(个人转账版)商户添加、查询含有多余字段导致签名失败的问题 [\#986](https://github.com/overtrue/wechat/pull/986) ([chenhaizano](https://github.com/chenhaizano)) +- Add merchant client. [\#983](https://github.com/overtrue/wechat/pull/983) ([mingyoung](https://github.com/mingyoung)) +- Fix PKCS7 unpad issue. [\#981](https://github.com/overtrue/wechat/pull/981) ([mingyoung](https://github.com/mingyoung)) +- 💯 Add unit tests. [\#979](https://github.com/overtrue/wechat/pull/979) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#978](https://github.com/overtrue/wechat/pull/978) ([overtrue](https://github.com/overtrue)) +- Add sub-merchant support. [\#976](https://github.com/overtrue/wechat/pull/976) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#974](https://github.com/overtrue/wechat/pull/974) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#973](https://github.com/overtrue/wechat/pull/973) ([mingyoung](https://github.com/mingyoung)) +- Refactoring payment [\#972](https://github.com/overtrue/wechat/pull/972) ([mingyoung](https://github.com/mingyoung)) +- Fix request method. [\#964](https://github.com/overtrue/wechat/pull/964) ([mingyoung](https://github.com/mingyoung)) +- MiniProgram template. [\#959](https://github.com/overtrue/wechat/pull/959) ([mingyoung](https://github.com/mingyoung)) +- 企业微信 jssdk ticket [\#954](https://github.com/overtrue/wechat/pull/954) ([mingyoung](https://github.com/mingyoung)) +- Scrutinizer Auto-Fixes [\#944](https://github.com/overtrue/wechat/pull/944) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- 简化子商户js config [\#943](https://github.com/overtrue/wechat/pull/943) ([HanSon](https://github.com/HanSon)) +- Apply fixes from StyleCI [\#942](https://github.com/overtrue/wechat/pull/942) ([overtrue](https://github.com/overtrue)) +- 支持子商户JS CONFIG生成 [\#941](https://github.com/overtrue/wechat/pull/941) ([HanSon](https://github.com/HanSon)) + +## [4.0.0-beta.3](https://github.com/overtrue/wechat/tree/4.0.0-beta.3) (2017-09-23) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.16...4.0.0-beta.3) + +**Closed issues:** + +- 退款结果通知 [\#858](https://github.com/overtrue/wechat/issues/858) + +**Merged pull requests:** + +- Update Application.php [\#936](https://github.com/overtrue/wechat/pull/936) ([HanSon](https://github.com/HanSon)) + +## [3.3.16](https://github.com/overtrue/wechat/tree/3.3.16) (2017-09-20) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.15...3.3.16) + +**Closed issues:** + +- 希望能增加获取回复数据的方法 [\#929](https://github.com/overtrue/wechat/issues/929) +- 3.3 版本 数据类型不对导致无法运行 [\#928](https://github.com/overtrue/wechat/issues/928) + +**Merged pull requests:** + +- 增加退款回调处理 [\#931](https://github.com/overtrue/wechat/pull/931) ([leo108](https://github.com/leo108)) + +## [3.3.15](https://github.com/overtrue/wechat/tree/3.3.15) (2017-09-13) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.14...3.3.15) + +**Closed issues:** + +- 微信 for windows 发送文件的时候报错 [\#927](https://github.com/overtrue/wechat/issues/927) + +## [3.3.14](https://github.com/overtrue/wechat/tree/3.3.14) (2017-09-13) +[Full Changelog](https://github.com/overtrue/wechat/compare/4.0.0-beta.2...3.3.14) + +**Closed issues:** + +- 请教授权的时候什么方法拿到用户是否关注了本公众号? [\#926](https://github.com/overtrue/wechat/issues/926) + +## [4.0.0-beta.2](https://github.com/overtrue/wechat/tree/4.0.0-beta.2) (2017-09-12) +[Full Changelog](https://github.com/overtrue/wechat/compare/4.0.0-beta.1...4.0.0-beta.2) + +**Closed issues:** + +- readme.md写错了? [\#923](https://github.com/overtrue/wechat/issues/923) +- token验证成功,但还是回复暂时不可用,困扰1个星期多了,真心求助!!!有偿都可以!! [\#922](https://github.com/overtrue/wechat/issues/922) +- 条件判断错了,stripos返回的是“返回在字符串 haystack 中 needle 首次出现的数字位置。”,所以不能直接作为条件判断 [\#915](https://github.com/overtrue/wechat/issues/915) +- README中的链接是否错误 [\#913](https://github.com/overtrue/wechat/issues/913) +- 测试公众号无法接受用户信息 [\#911](https://github.com/overtrue/wechat/issues/911) +- ReadMe文件过期 [\#910](https://github.com/overtrue/wechat/issues/910) +- 开放平台服务,取消授权会有哪些参数过来? [\#909](https://github.com/overtrue/wechat/issues/909) +- token无法验证 [\#908](https://github.com/overtrue/wechat/issues/908) +- laravel 5.4 composer 失败 [\#907](https://github.com/overtrue/wechat/issues/907) +- 开放平台:组件ticket无法通过 [\#904](https://github.com/overtrue/wechat/issues/904) +- 官方网站一直登陆不了,浙江丽水地区 [\#903](https://github.com/overtrue/wechat/issues/903) +- \[4.0\] Pimple\Exception\UnknownIdentifierException [\#901](https://github.com/overtrue/wechat/issues/901) +- 4.0 报错“Your requirements could not be resolved to an installable set of packages.” [\#898](https://github.com/overtrue/wechat/issues/898) + +**Merged pull requests:** + +- 修改通过ticket换取二维码图片地址的逻辑 [\#925](https://github.com/overtrue/wechat/pull/925) ([Gwill](https://github.com/Gwill)) +- make domain more flexible [\#924](https://github.com/overtrue/wechat/pull/924) ([HanSon](https://github.com/HanSon)) +- add code & domain comment [\#921](https://github.com/overtrue/wechat/pull/921) ([HanSon](https://github.com/HanSon)) +- Apply fixes from StyleCI [\#919](https://github.com/overtrue/wechat/pull/919) ([overtrue](https://github.com/overtrue)) +- \[3.1\] Custom PreAuthCode Support [\#918](https://github.com/overtrue/wechat/pull/918) ([freyo](https://github.com/freyo)) +- 修改acess\_token无效时微信返回错误码的判断 [\#916](https://github.com/overtrue/wechat/pull/916) ([blackjune](https://github.com/blackjune)) +- \[4.0\] Add optional 'request' parameter to notify handler methods [\#905](https://github.com/overtrue/wechat/pull/905) ([edwardaa](https://github.com/edwardaa)) +- Apply fixes from StyleCI [\#902](https://github.com/overtrue/wechat/pull/902) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#897](https://github.com/overtrue/wechat/pull/897) ([overtrue](https://github.com/overtrue)) +- 增加OAuth中Guzzle\Client的配置项的设置 [\#893](https://github.com/overtrue/wechat/pull/893) ([khsing](https://github.com/khsing)) +- Apply fixes from StyleCI [\#887](https://github.com/overtrue/wechat/pull/887) ([overtrue](https://github.com/overtrue)) +- Scrutinizer Auto-Fixes [\#884](https://github.com/overtrue/wechat/pull/884) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) + +## [4.0.0-beta.1](https://github.com/overtrue/wechat/tree/4.0.0-beta.1) (2017-08-31) +[Full Changelog](https://github.com/overtrue/wechat/compare/4.0.0-alpha.2...4.0.0-beta.1) + +**Closed issues:** + +- http://easywechat.org/ 网站访问不了了? [\#896](https://github.com/overtrue/wechat/issues/896) +- 关于缓存,请问为什么key中包含appId \* 2,有什么讲究吗? [\#892](https://github.com/overtrue/wechat/issues/892) +- 小程序调用解密程序报-41003错误 [\#891](https://github.com/overtrue/wechat/issues/891) +- 小程序调用加密数据解密时报错,不存在方法 [\#890](https://github.com/overtrue/wechat/issues/890) +- 有关4.0使用文档的问题 [\#883](https://github.com/overtrue/wechat/issues/883) +- \[4.0\] PHP最低版本能否降到7.0 [\#880](https://github.com/overtrue/wechat/issues/880) + +**Merged pull requests:** + +- \[4.0\] Pass proper arguments to the Response constructor [\#895](https://github.com/overtrue/wechat/pull/895) ([edwardaa](https://github.com/edwardaa)) +- Fix baseUrl and json issues. [\#894](https://github.com/overtrue/wechat/pull/894) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#889](https://github.com/overtrue/wechat/pull/889) ([overtrue](https://github.com/overtrue)) +- Scrutinizer Auto-Fixes [\#885](https://github.com/overtrue/wechat/pull/885) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- Apply fixes from StyleCI [\#882](https://github.com/overtrue/wechat/pull/882) ([overtrue](https://github.com/overtrue)) +- 补充通用卡接口 [\#881](https://github.com/overtrue/wechat/pull/881) ([XiaoLer](https://github.com/XiaoLer)) +- Apply fixes from StyleCI [\#879](https://github.com/overtrue/wechat/pull/879) ([overtrue](https://github.com/overtrue)) +- \[3.1\] Payment/API 没有使用全局的 cache [\#878](https://github.com/overtrue/wechat/pull/878) ([edwardaa](https://github.com/edwardaa)) +- Add JSON\_UNESCAPED\_UNICODE option. [\#874](https://github.com/overtrue/wechat/pull/874) ([mingyoung](https://github.com/mingyoung)) +- update \_\_set\_state magic method to static [\#872](https://github.com/overtrue/wechat/pull/872) ([8090Lambert](https://github.com/8090Lambert)) + +## [4.0.0-alpha.2](https://github.com/overtrue/wechat/tree/4.0.0-alpha.2) (2017-08-20) +[Full Changelog](https://github.com/overtrue/wechat/compare/4.0.0-alpha.1...4.0.0-alpha.2) + +**Closed issues:** + +- 你好,怎么用的 [\#869](https://github.com/overtrue/wechat/issues/869) + +**Merged pull requests:** + +- Tweak dir [\#871](https://github.com/overtrue/wechat/pull/871) ([mingyoung](https://github.com/mingyoung)) +- Fix mini-program guard. [\#870](https://github.com/overtrue/wechat/pull/870) ([mingyoung](https://github.com/mingyoung)) + +## [4.0.0-alpha.1](https://github.com/overtrue/wechat/tree/4.0.0-alpha.1) (2017-08-14) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.13...4.0.0-alpha.1) + +**Closed issues:** + +- 对doctrine/cache依赖的版本锁定 [\#867](https://github.com/overtrue/wechat/issues/867) + +## [3.3.13](https://github.com/overtrue/wechat/tree/3.3.13) (2017-08-13) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.12...3.3.13) + +**Closed issues:** + +- 文档中网页授权实例写的不明确 [\#850](https://github.com/overtrue/wechat/issues/850) +- \[意见\]作者能否提供getTokenFromServer方法扩展从外部第三方获取access\_token [\#837](https://github.com/overtrue/wechat/issues/837) +- invalid credential, access\_token is invalid or not latest [\#808](https://github.com/overtrue/wechat/issues/808) +- \[4.0\] 重构卡券 [\#806](https://github.com/overtrue/wechat/issues/806) +- \[4.0\] 重构 Broadcasting [\#805](https://github.com/overtrue/wechat/issues/805) +- \[4.0\] 变更日志 [\#746](https://github.com/overtrue/wechat/issues/746) + +**Merged pull requests:** + +- Fixed open-platform authorizer server token. [\#866](https://github.com/overtrue/wechat/pull/866) ([mingyoung](https://github.com/mingyoung)) +- payment\ClientTest 优化 [\#865](https://github.com/overtrue/wechat/pull/865) ([tianyong90](https://github.com/tianyong90)) +- Apply fixes from StyleCI [\#864](https://github.com/overtrue/wechat/pull/864) ([overtrue](https://github.com/overtrue)) +- 退款通知处理及相关单元测试 [\#863](https://github.com/overtrue/wechat/pull/863) ([tianyong90](https://github.com/tianyong90)) +- Apply fixes from StyleCI [\#862](https://github.com/overtrue/wechat/pull/862) ([overtrue](https://github.com/overtrue)) +- Update dependence version. [\#861](https://github.com/overtrue/wechat/pull/861) ([mingyoung](https://github.com/mingyoung)) +- Add tests. [\#859](https://github.com/overtrue/wechat/pull/859) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#857](https://github.com/overtrue/wechat/pull/857) ([overtrue](https://github.com/overtrue)) +- Payment 单元测试优化 [\#856](https://github.com/overtrue/wechat/pull/856) ([tianyong90](https://github.com/tianyong90)) +- Apply fixes from StyleCI [\#855](https://github.com/overtrue/wechat/pull/855) ([overtrue](https://github.com/overtrue)) +- lists 方法重命名为 list,相关单元测试调整 [\#853](https://github.com/overtrue/wechat/pull/853) ([tianyong90](https://github.com/tianyong90)) +- Apply fixes from StyleCI [\#852](https://github.com/overtrue/wechat/pull/852) ([overtrue](https://github.com/overtrue)) +- Payment 单元测试及部分问题修复 [\#851](https://github.com/overtrue/wechat/pull/851) ([tianyong90](https://github.com/tianyong90)) +- Apply fixes from StyleCI [\#848](https://github.com/overtrue/wechat/pull/848) ([overtrue](https://github.com/overtrue)) +- 调整 Payment\BaseClient 注入的 $app 类型 [\#847](https://github.com/overtrue/wechat/pull/847) ([tianyong90](https://github.com/tianyong90)) +- array\_merge 方法参数类型转换, type hints [\#846](https://github.com/overtrue/wechat/pull/846) ([tianyong90](https://github.com/tianyong90)) +- Fix oauth. [\#845](https://github.com/overtrue/wechat/pull/845) ([mingyoung](https://github.com/mingyoung)) +- Text message. [\#844](https://github.com/overtrue/wechat/pull/844) ([mingyoung](https://github.com/mingyoung)) +- Rename BaseService -\> BasicService. [\#843](https://github.com/overtrue/wechat/pull/843) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#842](https://github.com/overtrue/wechat/pull/842) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#841](https://github.com/overtrue/wechat/pull/841) ([overtrue](https://github.com/overtrue)) +- phpdoc types。 [\#840](https://github.com/overtrue/wechat/pull/840) ([tianyong90](https://github.com/tianyong90)) +- Apply fixes from StyleCI [\#839](https://github.com/overtrue/wechat/pull/839) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#836](https://github.com/overtrue/wechat/pull/836) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#835](https://github.com/overtrue/wechat/pull/835) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#833](https://github.com/overtrue/wechat/pull/833) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#831](https://github.com/overtrue/wechat/pull/831) ([overtrue](https://github.com/overtrue)) + +## [3.3.12](https://github.com/overtrue/wechat/tree/3.3.12) (2017-08-01) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.11...3.3.12) + +**Closed issues:** + +- 能否整合微信开放平台在给出一套demo [\#816](https://github.com/overtrue/wechat/issues/816) +- 请教这个项目的支付部分,尤其是签名和结果回调,是否支持小程序? [\#814](https://github.com/overtrue/wechat/issues/814) +- 微信意图识别接口返回invalid param [\#804](https://github.com/overtrue/wechat/issues/804) +- 返回param invalid [\#803](https://github.com/overtrue/wechat/issues/803) + +**Merged pull requests:** + +- change comment word [\#830](https://github.com/overtrue/wechat/pull/830) ([tianyong90](https://github.com/tianyong90)) +- Fix getTicket. [\#829](https://github.com/overtrue/wechat/pull/829) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#827](https://github.com/overtrue/wechat/pull/827) ([overtrue](https://github.com/overtrue)) +- 修正 HasAttributes Trait 引用错误 [\#825](https://github.com/overtrue/wechat/pull/825) ([tianyong90](https://github.com/tianyong90)) +- Apply fixes from StyleCI [\#824](https://github.com/overtrue/wechat/pull/824) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#822](https://github.com/overtrue/wechat/pull/822) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#820](https://github.com/overtrue/wechat/pull/820) ([mingyoung](https://github.com/mingyoung)) +- Add subscribe message. [\#819](https://github.com/overtrue/wechat/pull/819) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#818](https://github.com/overtrue/wechat/pull/818) ([mingyoung](https://github.com/mingyoung)) +- 微信开放平台帐号管理 [\#817](https://github.com/overtrue/wechat/pull/817) ([XiaoLer](https://github.com/XiaoLer)) +- add method in comment [\#813](https://github.com/overtrue/wechat/pull/813) ([HanSon](https://github.com/HanSon)) +- fixed guzzle version [\#812](https://github.com/overtrue/wechat/pull/812) ([HanSon](https://github.com/HanSon)) +- Apply fixes from StyleCI [\#811](https://github.com/overtrue/wechat/pull/811) ([mingyoung](https://github.com/mingyoung)) +- Downgrade to php 7.0 [\#809](https://github.com/overtrue/wechat/pull/809) ([HanSon](https://github.com/HanSon)) + +## [3.3.11](https://github.com/overtrue/wechat/tree/3.3.11) (2017-07-17) +[Full Changelog](https://github.com/overtrue/wechat/compare/4.0.0-alpha1...3.3.11) + +**Closed issues:** + +- 请添加 「退款原因」 参数 [\#802](https://github.com/overtrue/wechat/issues/802) + +## [4.0.0-alpha1](https://github.com/overtrue/wechat/tree/4.0.0-alpha1) (2017-07-17) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.10...4.0.0-alpha1) + +**Closed issues:** + +- Overtrue\Wechat\Media not found [\#801](https://github.com/overtrue/wechat/issues/801) +- 在微信的接口配置时Token 无效,可任意输入 [\#800](https://github.com/overtrue/wechat/issues/800) + +## [3.3.10](https://github.com/overtrue/wechat/tree/3.3.10) (2017-07-13) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.9...3.3.10) + +**Closed issues:** + +- 第三方平台refresh\_token的保存问题 [\#798](https://github.com/overtrue/wechat/issues/798) +- 网页授权共享session已晚 [\#792](https://github.com/overtrue/wechat/issues/792) + +**Merged pull requests:** + +- 临时二维码也是支持scene\_str的,这里补充上 [\#797](https://github.com/overtrue/wechat/pull/797) ([lornewang](https://github.com/lornewang)) +- Apply fixes from StyleCI [\#795](https://github.com/overtrue/wechat/pull/795) ([overtrue](https://github.com/overtrue)) +- add card message type [\#794](https://github.com/overtrue/wechat/pull/794) ([IanGely](https://github.com/IanGely)) +- add staff message type wxcard [\#793](https://github.com/overtrue/wechat/pull/793) ([IanGely](https://github.com/IanGely)) + +## [3.3.9](https://github.com/overtrue/wechat/tree/3.3.9) (2017-07-07) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.8...3.3.9) + +**Closed issues:** + +- \[4.0\] Http 模块 [\#678](https://github.com/overtrue/wechat/issues/678) +- \[4.0\] Http 请求类 [\#582](https://github.com/overtrue/wechat/issues/582) + +**Merged pull requests:** + +- Apply fixes from StyleCI [\#791](https://github.com/overtrue/wechat/pull/791) ([overtrue](https://github.com/overtrue)) +- Add get user portrait method. [\#790](https://github.com/overtrue/wechat/pull/790) ([getive](https://github.com/getive)) +- \[Feature\] Move directories [\#789](https://github.com/overtrue/wechat/pull/789) ([overtrue](https://github.com/overtrue)) +- \[Feature\] Move traits to kernel. [\#788](https://github.com/overtrue/wechat/pull/788) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#787](https://github.com/overtrue/wechat/pull/787) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#786](https://github.com/overtrue/wechat/pull/786) ([overtrue](https://github.com/overtrue)) + +## [3.3.8](https://github.com/overtrue/wechat/tree/3.3.8) (2017-07-07) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.7...3.3.8) + +**Closed issues:** + +- $temporary-\>getStream\($media\_id\) 与 file\_get\_contents\(\) 有区别??? [\#742](https://github.com/overtrue/wechat/issues/742) + +## [3.3.7](https://github.com/overtrue/wechat/tree/3.3.7) (2017-07-06) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.6...3.3.7) + +**Closed issues:** + +- 多添加一个$option [\#772](https://github.com/overtrue/wechat/issues/772) +- 消息群发,指定openid群发视频时,微信报错invalid message type hint: \[JUs0Oa0779ge25\] [\#757](https://github.com/overtrue/wechat/issues/757) + +## [3.3.6](https://github.com/overtrue/wechat/tree/3.3.6) (2017-07-06) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.5...3.3.6) + +**Fixed bugs:** + +- 素材管理,如果media\_id不存在会保存网页返回的错误代码 [\#592](https://github.com/overtrue/wechat/issues/592) + +**Closed issues:** + +- https://easywechat.org网站证书刚过期了,知会作者一声 [\#781](https://github.com/overtrue/wechat/issues/781) +- access\_token 是否能不内部主动请求微信 [\#778](https://github.com/overtrue/wechat/issues/778) +- 门店创建API \($poi-\>create\) 建议返回 poi\_id / exception [\#774](https://github.com/overtrue/wechat/issues/774) +- 扩展门店小程序错误 [\#762](https://github.com/overtrue/wechat/issues/762) +- \[4.0\] jssdk 抽出独立模块 [\#754](https://github.com/overtrue/wechat/issues/754) +- \[4.0\] 消息加密解密模块提取到 Kernel [\#753](https://github.com/overtrue/wechat/issues/753) +- 网页能授权但无法获取用户信息,代码跟官方文档一样。 [\#713](https://github.com/overtrue/wechat/issues/713) + +**Merged pull requests:** + +- Feature: BaseService. [\#785](https://github.com/overtrue/wechat/pull/785) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#784](https://github.com/overtrue/wechat/pull/784) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#783](https://github.com/overtrue/wechat/pull/783) ([mingyoung](https://github.com/mingyoung)) + +## [3.3.5](https://github.com/overtrue/wechat/tree/3.3.5) (2017-07-04) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.4...3.3.5) + +**Implemented enhancements:** + +- 并发下access\_token存在脏写隐患 [\#696](https://github.com/overtrue/wechat/issues/696) + +**Merged pull requests:** + +- Apply fixes from StyleCI [\#780](https://github.com/overtrue/wechat/pull/780) ([overtrue](https://github.com/overtrue)) + +## [3.3.4](https://github.com/overtrue/wechat/tree/3.3.4) (2017-07-04) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.3...3.3.4) + +**Closed issues:** + +- 网页授权获取用户信息无法打开授权页面 [\#773](https://github.com/overtrue/wechat/issues/773) +- Class 'EasyWechat\Foundation\Application' not found [\#769](https://github.com/overtrue/wechat/issues/769) +- 获取小程序二维码报错 [\#766](https://github.com/overtrue/wechat/issues/766) +- Call to undefined method EasyWeChat\Server\Guard::setRequest\(\) [\#765](https://github.com/overtrue/wechat/issues/765) +- 网页授权问题,提示scopes类型错误 [\#764](https://github.com/overtrue/wechat/issues/764) +- 门店小程序扩展错误问题 [\#763](https://github.com/overtrue/wechat/issues/763) +- 微信开发者平台,全网发布怎么通过 [\#761](https://github.com/overtrue/wechat/issues/761) +- 微信网页授权重复请求报code无效 [\#714](https://github.com/overtrue/wechat/issues/714) + +**Merged pull requests:** + +- 新版客服功能-获取聊天记录 [\#775](https://github.com/overtrue/wechat/pull/775) ([wuwenbao](https://github.com/wuwenbao)) +- Fix mini-program qrcode. [\#768](https://github.com/overtrue/wechat/pull/768) ([mingyoung](https://github.com/mingyoung)) +- Add code comments [\#756](https://github.com/overtrue/wechat/pull/756) ([daxiong123](https://github.com/daxiong123)) + +## [3.3.3](https://github.com/overtrue/wechat/tree/3.3.3) (2017-06-22) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.2...3.3.3) + +**Implemented enhancements:** + +- \[4.0\] Trait HasHttpRequests [\#671](https://github.com/overtrue/wechat/issues/671) +- \[4.0\] 缓存抽象成 trait: InteractsWithCache [\#670](https://github.com/overtrue/wechat/issues/670) +- \[4.0\] 返回值类型可配置 [\#661](https://github.com/overtrue/wechat/issues/661) +- \[4.0\] 报错信息可选 [\#596](https://github.com/overtrue/wechat/issues/596) +- \[4.0\] 简化并完善开发者配置项 [\#584](https://github.com/overtrue/wechat/issues/584) + +**Fixed bugs:** + +- open\_platform.oauth 过早的获取 access token [\#701](https://github.com/overtrue/wechat/issues/701) + +**Closed issues:** + +- 微信网页支付配置生成 [\#751](https://github.com/overtrue/wechat/issues/751) +- configForJSSDKPayment [\#744](https://github.com/overtrue/wechat/issues/744) +- 发现微信上有管理公众号留言的接口,不知道是不是新出的 [\#721](https://github.com/overtrue/wechat/issues/721) +- oauth能获取用户信息,再通过access\_token与用户openid去获取信息,部分用户的信息为空 [\#720](https://github.com/overtrue/wechat/issues/720) +- 接入多个公众号 [\#718](https://github.com/overtrue/wechat/issues/718) +- guzzle curl error28 - 去哪设置默认timeout ? [\#715](https://github.com/overtrue/wechat/issues/715) +- 使用$server-\>getMessage\(\);报错 [\#712](https://github.com/overtrue/wechat/issues/712) +- 怎样从数据库中调取配置 [\#711](https://github.com/overtrue/wechat/issues/711) +- \[4.0\] 支持企业微信 [\#707](https://github.com/overtrue/wechat/issues/707) +- defaultColor does not work. [\#703](https://github.com/overtrue/wechat/issues/703) +- 是否支持H5支付 [\#694](https://github.com/overtrue/wechat/issues/694) +- 生成AccessToken时,似乎没有调用自定义缓存的delete方法 [\#693](https://github.com/overtrue/wechat/issues/693) +- \[4.0\] PSR-6 缓存接口 [\#692](https://github.com/overtrue/wechat/issues/692) +- 微信支付沙盒模式支持配置文件配置 [\#690](https://github.com/overtrue/wechat/issues/690) +- \[4.0\] 优化服务提供器结构 [\#689](https://github.com/overtrue/wechat/issues/689) +- 强制项目不要自动获取AccessToken [\#688](https://github.com/overtrue/wechat/issues/688) +- 小程序解密$encryptedData数据 [\#687](https://github.com/overtrue/wechat/issues/687) +- 微信坑爹timestamp已经解决不需要configForJSSDKPayment改变timestamp中s大小写 [\#686](https://github.com/overtrue/wechat/issues/686) +- \[4.0\] 所有 API 改名为 Client. [\#677](https://github.com/overtrue/wechat/issues/677) +- sandbox\_signkey 过期 [\#675](https://github.com/overtrue/wechat/issues/675) +- 接口配置失败 [\#672](https://github.com/overtrue/wechat/issues/672) +- 下载语音文件偶尔报错:ErrorException: is\_readable\(\) expects parameter 1 to be a valid path [\#667](https://github.com/overtrue/wechat/issues/667) +- 微信支付沙箱地址混乱 [\#665](https://github.com/overtrue/wechat/issues/665) +- 开放平台自动回复出错,提示“该服务号暂时无法提供服务” [\#654](https://github.com/overtrue/wechat/issues/654) +- \[4.0\]自定义微信API的区域接入点 [\#636](https://github.com/overtrue/wechat/issues/636) +- 在命令行使用easywechat如何关闭日志 [\#601](https://github.com/overtrue/wechat/issues/601) +- \[4.0\] PHP 版本最低要求 7.1 [\#586](https://github.com/overtrue/wechat/issues/586) +- \[4.0\] 简化微信 API 请求 [\#583](https://github.com/overtrue/wechat/issues/583) +- \[4.0\] 自定义 endpoint [\#521](https://github.com/overtrue/wechat/issues/521) + +**Merged pull requests:** + +- Apply fixes from StyleCI [\#750](https://github.com/overtrue/wechat/pull/750) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#749](https://github.com/overtrue/wechat/pull/749) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#747](https://github.com/overtrue/wechat/pull/747) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#745](https://github.com/overtrue/wechat/pull/745) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#740](https://github.com/overtrue/wechat/pull/740) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#737](https://github.com/overtrue/wechat/pull/737) ([mingyoung](https://github.com/mingyoung)) +- 分模块静态调用 [\#734](https://github.com/overtrue/wechat/pull/734) ([mingyoung](https://github.com/mingyoung)) +- Revert "Apply fixes from StyleCI" [\#731](https://github.com/overtrue/wechat/pull/731) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#730](https://github.com/overtrue/wechat/pull/730) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#729](https://github.com/overtrue/wechat/pull/729) ([overtrue](https://github.com/overtrue)) +- Revert "Apply fixes from StyleCI" [\#728](https://github.com/overtrue/wechat/pull/728) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#727](https://github.com/overtrue/wechat/pull/727) ([overtrue](https://github.com/overtrue)) +- 修复Https 请求判断不准 [\#726](https://github.com/overtrue/wechat/pull/726) ([xutl](https://github.com/xutl)) +- Apply fixes from StyleCI [\#725](https://github.com/overtrue/wechat/pull/725) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#724](https://github.com/overtrue/wechat/pull/724) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#723](https://github.com/overtrue/wechat/pull/723) ([mingyoung](https://github.com/mingyoung)) +- Correction notes [\#722](https://github.com/overtrue/wechat/pull/722) ([PersiLiao](https://github.com/PersiLiao)) +- Apply fixes from StyleCI [\#717](https://github.com/overtrue/wechat/pull/717) ([mingyoung](https://github.com/mingyoung)) +- 新增图文消息留言管理接口 [\#716](https://github.com/overtrue/wechat/pull/716) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#710](https://github.com/overtrue/wechat/pull/710) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#709](https://github.com/overtrue/wechat/pull/709) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#708](https://github.com/overtrue/wechat/pull/708) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#706](https://github.com/overtrue/wechat/pull/706) ([overtrue](https://github.com/overtrue)) +- 命令行下不打印日志 [\#705](https://github.com/overtrue/wechat/pull/705) ([mingyoung](https://github.com/mingyoung)) +- add defaultColor [\#704](https://github.com/overtrue/wechat/pull/704) ([damonto](https://github.com/damonto)) +- Fix [\#702](https://github.com/overtrue/wechat/pull/702) ([mingyoung](https://github.com/mingyoung)) +- Add api. [\#700](https://github.com/overtrue/wechat/pull/700) ([mingyoung](https://github.com/mingyoung)) +- Rename method. [\#699](https://github.com/overtrue/wechat/pull/699) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#698](https://github.com/overtrue/wechat/pull/698) ([mingyoung](https://github.com/mingyoung)) +- 修正素材管理中的返回值文档注释,正确的类型应该是集合,而不是字符串。 [\#695](https://github.com/overtrue/wechat/pull/695) ([starlight36](https://github.com/starlight36)) +- Payment sandbox config. [\#691](https://github.com/overtrue/wechat/pull/691) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#684](https://github.com/overtrue/wechat/pull/684) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#683](https://github.com/overtrue/wechat/pull/683) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#682](https://github.com/overtrue/wechat/pull/682) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#681](https://github.com/overtrue/wechat/pull/681) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#680](https://github.com/overtrue/wechat/pull/680) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#679](https://github.com/overtrue/wechat/pull/679) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#676](https://github.com/overtrue/wechat/pull/676) ([mingyoung](https://github.com/mingyoung)) +- checks via composer. [\#673](https://github.com/overtrue/wechat/pull/673) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#668](https://github.com/overtrue/wechat/pull/668) ([overtrue](https://github.com/overtrue)) +- Correct payment sandbox endpoint and add a method to get sandbox sign key [\#666](https://github.com/overtrue/wechat/pull/666) ([skyred](https://github.com/skyred)) + +## [3.3.2](https://github.com/overtrue/wechat/tree/3.3.2) (2017-04-27) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.1...3.3.2) + +**Implemented enhancements:** + +- \[4.0\] Open Platform 模块 [\#587](https://github.com/overtrue/wechat/issues/587) +- \[4.0\] 微信支付 sandbox模式 [\#507](https://github.com/overtrue/wechat/issues/507) + +**Closed issues:** + +- \[4.0\] staff 模块改名为 customer service [\#585](https://github.com/overtrue/wechat/issues/585) + +**Merged pull requests:** + +- Module rename. [\#664](https://github.com/overtrue/wechat/pull/664) ([mingyoung](https://github.com/mingyoung)) +- Merge branch master into branch develop. [\#663](https://github.com/overtrue/wechat/pull/663) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#662](https://github.com/overtrue/wechat/pull/662) ([mingyoung](https://github.com/mingyoung)) +- Fix payment tools API [\#660](https://github.com/overtrue/wechat/pull/660) ([mingyoung](https://github.com/mingyoung)) +- Avoid ambiguity [\#659](https://github.com/overtrue/wechat/pull/659) ([mingyoung](https://github.com/mingyoung)) +- Support Payment Sandbox mode [\#658](https://github.com/overtrue/wechat/pull/658) ([skyred](https://github.com/skyred)) +- Apply fixes from StyleCI [\#656](https://github.com/overtrue/wechat/pull/656) ([overtrue](https://github.com/overtrue)) +- Mini program datacube. [\#655](https://github.com/overtrue/wechat/pull/655) ([mingyoung](https://github.com/mingyoung)) + +## [3.3.1](https://github.com/overtrue/wechat/tree/3.3.1) (2017-04-16) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.3.0...3.3.1) + +**Closed issues:** + +- 微信第三方平台缓存位置,是否可以在配置文件中自定义 [\#648](https://github.com/overtrue/wechat/issues/648) +- 微信开放平台authorizer token缓存问题 [\#644](https://github.com/overtrue/wechat/issues/644) +- 微信开放平台发起网页授权bug [\#638](https://github.com/overtrue/wechat/issues/638) +- 微信公众号不能回复接收到的消息,日志无报错 [\#637](https://github.com/overtrue/wechat/issues/637) +- \[4.0\]黑名单管理 [\#538](https://github.com/overtrue/wechat/issues/538) + +**Merged pull requests:** + +- optimizes [\#652](https://github.com/overtrue/wechat/pull/652) ([mingyoung](https://github.com/mingyoung)) + +## [3.3.0](https://github.com/overtrue/wechat/tree/3.3.0) (2017-04-13) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.2.7...3.3.0) + +**Closed issues:** + +- 微信接口获取openid是怎么排序的? [\#650](https://github.com/overtrue/wechat/issues/650) +- 缺少网页扫码支付接口 [\#647](https://github.com/overtrue/wechat/issues/647) +- 微信下的单的默认过期时间是多少啊 [\#645](https://github.com/overtrue/wechat/issues/645) +- 在获取用户信息是出错 [\#643](https://github.com/overtrue/wechat/issues/643) +- 调用$app =app\('wechat'\);时报错Use of undefined constant CURLOPT\_IPRESOLVE - assumed 'CURLOPT\_IPRESOLVE' [\#633](https://github.com/overtrue/wechat/issues/633) +- 提示找不到EasyWeChat\Server\Guard::setRequest\(\)方法 [\#626](https://github.com/overtrue/wechat/issues/626) +- 开放平台接收ComponentVerifyTicket,会出现Undefined index: FromUserName [\#623](https://github.com/overtrue/wechat/issues/623) +- 美国移动网络获取不到accessToken [\#610](https://github.com/overtrue/wechat/issues/610) +- 开放平台 APP 微信登录 [\#604](https://github.com/overtrue/wechat/issues/604) + +**Merged pull requests:** + +- Merge from open-platform branch. [\#651](https://github.com/overtrue/wechat/pull/651) ([mingyoung](https://github.com/mingyoung)) +- Update code for open-platform [\#649](https://github.com/overtrue/wechat/pull/649) ([mingyoung](https://github.com/mingyoung)) +- Code cleanup & refactoring. [\#646](https://github.com/overtrue/wechat/pull/646) ([mingyoung](https://github.com/mingyoung)) +- support cash coupon [\#642](https://github.com/overtrue/wechat/pull/642) ([HanSon](https://github.com/HanSon)) +- ♻️ All tests have been namespaced. [\#641](https://github.com/overtrue/wechat/pull/641) ([mingyoung](https://github.com/mingyoung)) +- tweak code. [\#640](https://github.com/overtrue/wechat/pull/640) ([mingyoung](https://github.com/mingyoung)) +- modify oauth property [\#639](https://github.com/overtrue/wechat/pull/639) ([jekst](https://github.com/jekst)) +- Apply fixes from StyleCI [\#635](https://github.com/overtrue/wechat/pull/635) ([overtrue](https://github.com/overtrue)) +- ✨ Blacklist. [\#634](https://github.com/overtrue/wechat/pull/634) ([mingyoung](https://github.com/mingyoung)) +- 🔨 Refactoring for mini-program. [\#632](https://github.com/overtrue/wechat/pull/632) ([mingyoung](https://github.com/mingyoung)) + +## [3.2.7](https://github.com/overtrue/wechat/tree/3.2.7) (2017-03-31) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.2.6...3.2.7) + +**Closed issues:** + +- 不管哪个公众号,只要填写 这个接口地址,都能配置或应用成功,实际上是不成功的,不到怎么找错。。 [\#611](https://github.com/overtrue/wechat/issues/611) + +**Merged pull requests:** + +- 修复一个创建卡券时的 bug, 添加获取微信门店类目表的api [\#631](https://github.com/overtrue/wechat/pull/631) ([Hexor](https://github.com/Hexor)) + +## [3.2.6](https://github.com/overtrue/wechat/tree/3.2.6) (2017-03-31) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.2.5...3.2.6) + +**Closed issues:** + +- 我想大量发模板消息,但send每次都等待返回太慢,有啥解决办法吗? [\#630](https://github.com/overtrue/wechat/issues/630) +- 3.2开放平台缺少authorizer\_token和authorization [\#629](https://github.com/overtrue/wechat/issues/629) +- 微信开发平台接受消息报Invalid request signature bug [\#625](https://github.com/overtrue/wechat/issues/625) +- 图文上传thumb\_media\_id 返回 {"errcode":40007,"errmsg":"invalid media\_id hint: \[\]"} [\#622](https://github.com/overtrue/wechat/issues/622) +- Encryptor基类hack导致小程序的sessionKey base64\_decode失败 [\#614](https://github.com/overtrue/wechat/issues/614) +- 是否有 2.1 升级到最新版的方案? [\#609](https://github.com/overtrue/wechat/issues/609) +- laravel5.3 安装 "overtrue/wechat:~3.1 失败 [\#607](https://github.com/overtrue/wechat/issues/607) +- overtrue/wechat和phpdoc包依赖冲突。 [\#605](https://github.com/overtrue/wechat/issues/605) +- \[bug\]2个问题 [\#597](https://github.com/overtrue/wechat/issues/597) +- 微信第三方平台开发是否只做了一部分? [\#594](https://github.com/overtrue/wechat/issues/594) +- \[4.0\] ServiceProvider 移动到各自模块里 [\#588](https://github.com/overtrue/wechat/issues/588) +- Cannot use EasyWeChat\OpenPlatform\Traits\VerifyTicket as VerifyTicket because the name is already in use [\#579](https://github.com/overtrue/wechat/issues/579) +- 授权state值怎么设置 [\#573](https://github.com/overtrue/wechat/issues/573) +- mini\_app get jscode problem, report appid & secret value is null [\#569](https://github.com/overtrue/wechat/issues/569) +- 小程序生成二维码问题 [\#568](https://github.com/overtrue/wechat/issues/568) + +**Merged pull requests:** + +- Update OpenPlatform AppId [\#624](https://github.com/overtrue/wechat/pull/624) ([jeftom](https://github.com/jeftom)) +- Apply fixes from StyleCI [\#621](https://github.com/overtrue/wechat/pull/621) ([overtrue](https://github.com/overtrue)) +- Apply fixes from StyleCI [\#618](https://github.com/overtrue/wechat/pull/618) ([overtrue](https://github.com/overtrue)) +- Compatible with php5.5 [\#617](https://github.com/overtrue/wechat/pull/617) ([mingyoung](https://github.com/mingyoung)) +- Make the testcase works. [\#616](https://github.com/overtrue/wechat/pull/616) ([mingyoung](https://github.com/mingyoung)) +- Fix mini-program decryptor [\#615](https://github.com/overtrue/wechat/pull/615) ([mingyoung](https://github.com/mingyoung)) +- Missing message handling [\#613](https://github.com/overtrue/wechat/pull/613) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#612](https://github.com/overtrue/wechat/pull/612) ([overtrue](https://github.com/overtrue)) +- 添加卡券创建二维码接口 [\#608](https://github.com/overtrue/wechat/pull/608) ([forecho](https://github.com/forecho)) +- 开放平台大幅重构并且添加测试 [\#606](https://github.com/overtrue/wechat/pull/606) ([tsunamilx](https://github.com/tsunamilx)) +- Update MessageBuilder.php [\#603](https://github.com/overtrue/wechat/pull/603) ([U2Fsd](https://github.com/U2Fsd)) +- 生成 js添加到卡包接口 增加fixed\_begintimestamp、outer\_str字段 [\#602](https://github.com/overtrue/wechat/pull/602) ([gychg](https://github.com/gychg)) +- tests for speed [\#600](https://github.com/overtrue/wechat/pull/600) ([mingyoung](https://github.com/mingyoung)) +- Update test files [\#599](https://github.com/overtrue/wechat/pull/599) ([mingyoung](https://github.com/mingyoung)) +- 允许自定义ticket缓存key [\#598](https://github.com/overtrue/wechat/pull/598) ([XiaoLer](https://github.com/XiaoLer)) +- delete top color [\#595](https://github.com/overtrue/wechat/pull/595) ([HanSon](https://github.com/HanSon)) +- Add payment scan notify handler [\#593](https://github.com/overtrue/wechat/pull/593) ([acgrid](https://github.com/acgrid)) +- Apply fixes from StyleCI [\#591](https://github.com/overtrue/wechat/pull/591) ([overtrue](https://github.com/overtrue)) +- Upgrade packages version to 4.0 [\#590](https://github.com/overtrue/wechat/pull/590) ([reatang](https://github.com/reatang)) +- Move providers to module dir. \#588 [\#589](https://github.com/overtrue/wechat/pull/589) ([overtrue](https://github.com/overtrue)) +- 把OpenPlatform中的组件依赖解耦 [\#581](https://github.com/overtrue/wechat/pull/581) ([reatang](https://github.com/reatang)) + +## [3.2.5](https://github.com/overtrue/wechat/tree/3.2.5) (2017-02-04) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.2.4...3.2.5) + +**Merged pull requests:** + +- fix naming [\#580](https://github.com/overtrue/wechat/pull/580) ([mingyoung](https://github.com/mingyoung)) +- Allow client code configure its own GuzzleHTTP handler [\#578](https://github.com/overtrue/wechat/pull/578) ([acgrid](https://github.com/acgrid)) + +## [3.2.4](https://github.com/overtrue/wechat/tree/3.2.4) (2017-01-24) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.2.3...3.2.4) + +**Closed issues:** + +- 如何在其他框架下使用$app-\>payment-\>handleNotify [\#574](https://github.com/overtrue/wechat/issues/574) +- 前后端分离单页下获取的config,认证失败 [\#565](https://github.com/overtrue/wechat/issues/565) +- 支付签名错误 [\#563](https://github.com/overtrue/wechat/issues/563) + +**Merged pull requests:** + +- Update Authorizer.php [\#577](https://github.com/overtrue/wechat/pull/577) ([ww380459000](https://github.com/ww380459000)) +- 补全通用卡接口 [\#575](https://github.com/overtrue/wechat/pull/575) ([XiaoLer](https://github.com/XiaoLer)) +- require ext-SimpleXML [\#572](https://github.com/overtrue/wechat/pull/572) ([garveen](https://github.com/garveen)) +- fix README Contribution link [\#571](https://github.com/overtrue/wechat/pull/571) ([zhwei](https://github.com/zhwei)) +- Add user data decryption. [\#570](https://github.com/overtrue/wechat/pull/570) ([mingyoung](https://github.com/mingyoung)) +- change request parameter [\#567](https://github.com/overtrue/wechat/pull/567) ([cloudsthere](https://github.com/cloudsthere)) +- 完善小程序代码 [\#566](https://github.com/overtrue/wechat/pull/566) ([mingyoung](https://github.com/mingyoung)) +- 添加小程序支持 [\#564](https://github.com/overtrue/wechat/pull/564) ([mingyoung](https://github.com/mingyoung)) + +## [3.2.3](https://github.com/overtrue/wechat/tree/3.2.3) (2017-01-04) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.2.2...3.2.3) + +**Closed issues:** + +- 文档里的自定义菜单中,group\_id是否为tag\_id的误写? [\#561](https://github.com/overtrue/wechat/issues/561) +- Open Platform有简明的使用文档吗?3ks [\#560](https://github.com/overtrue/wechat/issues/560) +- 刷新access\_token有效期,未发现有相关的封装 [\#540](https://github.com/overtrue/wechat/issues/540) + +**Merged pull requests:** + +- Update Card.php [\#562](https://github.com/overtrue/wechat/pull/562) ([XiaoLer](https://github.com/XiaoLer)) +- Apply fixes from StyleCI [\#559](https://github.com/overtrue/wechat/pull/559) ([overtrue](https://github.com/overtrue)) +- Update API.php [\#558](https://github.com/overtrue/wechat/pull/558) ([drogjh](https://github.com/drogjh)) +- optimized code [\#557](https://github.com/overtrue/wechat/pull/557) ([mingyoung](https://github.com/mingyoung)) + +## [3.2.2](https://github.com/overtrue/wechat/tree/3.2.2) (2016-12-27) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.2.1...3.2.2) + +**Closed issues:** + +- How to get authorize url? [\#555](https://github.com/overtrue/wechat/issues/555) + +**Merged pull requests:** + +- fixed downloadBill method result [\#556](https://github.com/overtrue/wechat/pull/556) ([hidehalo](https://github.com/hidehalo)) +- add config:log.permission for monolog [\#554](https://github.com/overtrue/wechat/pull/554) ([woshizoufeng](https://github.com/woshizoufeng)) +- Improve open platform support. [\#553](https://github.com/overtrue/wechat/pull/553) ([mingyoung](https://github.com/mingyoung)) +- Improve. [\#552](https://github.com/overtrue/wechat/pull/552) ([mingyoung](https://github.com/mingyoung)) +- add $forceRefresh param to js-\>ticket\(\) method [\#551](https://github.com/overtrue/wechat/pull/551) ([leo108](https://github.com/leo108)) + +## [3.2.1](https://github.com/overtrue/wechat/tree/3.2.1) (2016-12-20) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.2.0...3.2.1) + +**Merged pull requests:** + +- 增加小程序用jscode获取用户信息的接口 [\#550](https://github.com/overtrue/wechat/pull/550) ([soone](https://github.com/soone)) + +## [3.2.0](https://github.com/overtrue/wechat/tree/3.2.0) (2016-12-19) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.1.9...3.2.0) + +**Closed issues:** + +- 喵喵喵 [\#545](https://github.com/overtrue/wechat/issues/545) +- HttpException with uploadArticle API [\#544](https://github.com/overtrue/wechat/issues/544) +- 是否有接入小程序的计划 [\#543](https://github.com/overtrue/wechat/issues/543) +- "Call to undefined method Overtrue\Socialite\Providers\WeChat Provider::driver\(\) [\#536](https://github.com/overtrue/wechat/issues/536) +- 服务端Server模块回复音乐消息出错 [\#533](https://github.com/overtrue/wechat/issues/533) +- 用户授权出现The key "access\_token" could not be empty [\#527](https://github.com/overtrue/wechat/issues/527) + +**Merged pull requests:** + +- Apply fixes from StyleCI [\#549](https://github.com/overtrue/wechat/pull/549) ([overtrue](https://github.com/overtrue)) +- 添加摇一摇周边模块 [\#548](https://github.com/overtrue/wechat/pull/548) ([allen05ren](https://github.com/allen05ren)) +- Make some compatible. [\#542](https://github.com/overtrue/wechat/pull/542) ([mingyoung](https://github.com/mingyoung)) +- Apply fixes from StyleCI [\#541](https://github.com/overtrue/wechat/pull/541) ([overtrue](https://github.com/overtrue)) +- 改变了http 中 json 方法的接口, 从而支持 添加 添加 query参数 [\#539](https://github.com/overtrue/wechat/pull/539) ([shoaly](https://github.com/shoaly)) +- 提交 [\#537](https://github.com/overtrue/wechat/pull/537) ([shoaly](https://github.com/shoaly)) +- Apply fixes from StyleCI [\#535](https://github.com/overtrue/wechat/pull/535) ([overtrue](https://github.com/overtrue)) + +## [3.1.9](https://github.com/overtrue/wechat/tree/3.1.9) (2016-12-01) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.1.8...3.1.9) + +**Closed issues:** + +- 还是不懂怎么获取unionid [\#531](https://github.com/overtrue/wechat/issues/531) +- Scope 参数错误或没有 Scope 权限 [\#528](https://github.com/overtrue/wechat/issues/528) +- $\_SERVER\['SERVER\_ADDR'\] 在mac php7中获取不到 [\#520](https://github.com/overtrue/wechat/issues/520) +- 能否永久素材其他类型封装个download方法,跟临时一样 [\#505](https://github.com/overtrue/wechat/issues/505) +- V3.1 JSSDK使用疑惑 [\#503](https://github.com/overtrue/wechat/issues/503) +- 如何加入QQ群 [\#501](https://github.com/overtrue/wechat/issues/501) +- 能否在下一个版本把企业的相关接口整合集成进去 [\#496](https://github.com/overtrue/wechat/issues/496) +- 既然使用了monolog,那么在Application::initializeLogger只使用了文件流的特定形式来记录日志是否合理? [\#494](https://github.com/overtrue/wechat/issues/494) +- configForShareAddress [\#482](https://github.com/overtrue/wechat/issues/482) +- 更新微信文章的时候MatialEasyWeChat\Material,如果设置了show\_pic\_cover和content\_source\_url不会生效 [\#470](https://github.com/overtrue/wechat/issues/470) +- 请问 SDK 是否支持授权接入的公众号接口调用? [\#438](https://github.com/overtrue/wechat/issues/438) +- 通过unionid发送信息。 [\#411](https://github.com/overtrue/wechat/issues/411) +- 【新增】设备管理 [\#77](https://github.com/overtrue/wechat/issues/77) + +**Merged pull requests:** + +- Add support wechat open platform. [\#532](https://github.com/overtrue/wechat/pull/532) ([mingyoung](https://github.com/mingyoung)) +- Applied fixes from StyleCI [\#530](https://github.com/overtrue/wechat/pull/530) ([overtrue](https://github.com/overtrue)) +- 新增硬件设备api [\#529](https://github.com/overtrue/wechat/pull/529) ([soone](https://github.com/soone)) + +## [3.1.8](https://github.com/overtrue/wechat/tree/3.1.8) (2016-11-23) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.1.7...3.1.8) + +**Closed issues:** + +- SCAN 事件会出现无法提供服务 [\#525](https://github.com/overtrue/wechat/issues/525) + +## [3.1.7](https://github.com/overtrue/wechat/tree/3.1.7) (2016-10-26) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.1.6...3.1.7) + +**Closed issues:** + +- preg\_replace unicode 的兼容问题 [\#515](https://github.com/overtrue/wechat/issues/515) + +**Merged pull requests:** + +- support psr-http-message-bridge 1.0 [\#524](https://github.com/overtrue/wechat/pull/524) ([wppd](https://github.com/wppd)) +- Applied fixes from StyleCI [\#523](https://github.com/overtrue/wechat/pull/523) ([overtrue](https://github.com/overtrue)) +- for \#520 [\#522](https://github.com/overtrue/wechat/pull/522) ([jinchun](https://github.com/jinchun)) + +## [3.1.6](https://github.com/overtrue/wechat/tree/3.1.6) (2016-10-19) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.1.5...3.1.6) + +**Closed issues:** + +- PHP Fatal error: Uncaught HttpException [\#517](https://github.com/overtrue/wechat/issues/517) +- 微信支付回调出错 [\#514](https://github.com/overtrue/wechat/issues/514) + +**Merged pull requests:** + +- Fix xml preg replace [\#519](https://github.com/overtrue/wechat/pull/519) ([springjk](https://github.com/springjk)) +- fix the DOC [\#518](https://github.com/overtrue/wechat/pull/518) ([ac1982](https://github.com/ac1982)) + +## [3.1.5](https://github.com/overtrue/wechat/tree/3.1.5) (2016-10-13) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.1.4...3.1.5) + +**Closed issues:** + +- wechat 在 larave l5.3 使用 passport 包下无法安装 [\#513](https://github.com/overtrue/wechat/issues/513) + +**Merged pull requests:** + +- Applied fixes from StyleCI [\#512](https://github.com/overtrue/wechat/pull/512) ([overtrue](https://github.com/overtrue)) + +## [3.1.4](https://github.com/overtrue/wechat/tree/3.1.4) (2016-10-12) +[Full Changelog](https://github.com/overtrue/wechat/compare/2.1.39...3.1.4) + +**Closed issues:** + +- 微信卡券特殊票券创建之后为什么无法更新卡券信息一致提示code非法。 [\#511](https://github.com/overtrue/wechat/issues/511) +- 请添加 「退款方式」 参数 [\#509](https://github.com/overtrue/wechat/issues/509) +- 2.1.40命名空间巨变引发的重大问题\(疑似提错版本了\) [\#508](https://github.com/overtrue/wechat/issues/508) +- 卡券核销、查询建议 [\#506](https://github.com/overtrue/wechat/issues/506) +- 支付重复回调问题 [\#504](https://github.com/overtrue/wechat/issues/504) + +**Merged pull requests:** + +- Changed method doc to the right accepted param type [\#510](https://github.com/overtrue/wechat/pull/510) ([marianoasselborn](https://github.com/marianoasselborn)) +- 增加判断是否有人工客服帐号,避免出现无账号时候,头像为默认头像的情况 [\#502](https://github.com/overtrue/wechat/pull/502) ([hello2t](https://github.com/hello2t)) +- Applied fixes from StyleCI [\#500](https://github.com/overtrue/wechat/pull/500) ([overtrue](https://github.com/overtrue)) +- 为initializeLogger日志初始话函数添加判断分支 [\#499](https://github.com/overtrue/wechat/pull/499) ([403studio](https://github.com/403studio)) + +## [2.1.39](https://github.com/overtrue/wechat/tree/2.1.39) (2016-09-05) +[Full Changelog](https://github.com/overtrue/wechat/compare/2.1.41...2.1.39) + +## [2.1.41](https://github.com/overtrue/wechat/tree/2.1.41) (2016-09-05) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.1.3...2.1.41) + +**Closed issues:** + +- 调用接口次数超过最大限制问题 [\#493](https://github.com/overtrue/wechat/issues/493) +- 微信退款证书报错 Unable to set private key file [\#492](https://github.com/overtrue/wechat/issues/492) +- 微信支付存在问题 [\#489](https://github.com/overtrue/wechat/issues/489) +- 预支付下单 response body 为空 [\#488](https://github.com/overtrue/wechat/issues/488) +- https check issue [\#486](https://github.com/overtrue/wechat/issues/486) + +**Merged pull requests:** + +- update composer.json [\#498](https://github.com/overtrue/wechat/pull/498) ([ac1982](https://github.com/ac1982)) +- use openssl instead of mcrypt [\#497](https://github.com/overtrue/wechat/pull/497) ([ac1982](https://github.com/ac1982)) +- 修复 with 方法带数据的问题 [\#491](https://github.com/overtrue/wechat/pull/491) ([XiaoLer](https://github.com/XiaoLer)) + +## [3.1.3](https://github.com/overtrue/wechat/tree/3.1.3) (2016-08-08) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.1.2...3.1.3) + +**Closed issues:** + +- Laravel中写的最简单的例子在phpunit出错。 [\#485](https://github.com/overtrue/wechat/issues/485) +- 微信的消息回复的FromUserName和ToUserName是不是对调了 [\#484](https://github.com/overtrue/wechat/issues/484) +- 微信红包不能发给别的公众号的用户吗 [\#483](https://github.com/overtrue/wechat/issues/483) +- 用户授权登录问题 [\#481](https://github.com/overtrue/wechat/issues/481) +- cURL error 56: SSLRead\(\) return error -9806 [\#473](https://github.com/overtrue/wechat/issues/473) +- 会员卡开卡字段文档有错误 [\#471](https://github.com/overtrue/wechat/issues/471) +- Getting more done in GitHub with ZenHub [\#439](https://github.com/overtrue/wechat/issues/439) +- 微信支付下单错误 [\#376](https://github.com/overtrue/wechat/issues/376) + +**Merged pull requests:** + +- update the File class to recognize pdf file. [\#480](https://github.com/overtrue/wechat/pull/480) ([ac1982](https://github.com/ac1982)) +- update testActivateUserForm [\#478](https://github.com/overtrue/wechat/pull/478) ([wangniuniu](https://github.com/wangniuniu)) +- Scrutinizer Auto-Fixes [\#477](https://github.com/overtrue/wechat/pull/477) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- Applied fixes from StyleCI [\#476](https://github.com/overtrue/wechat/pull/476) ([overtrue](https://github.com/overtrue)) +- Scrutinizer Auto-Fixes [\#475](https://github.com/overtrue/wechat/pull/475) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- 开放自定义prefix和缓存键值方法 [\#474](https://github.com/overtrue/wechat/pull/474) ([XiaoLer](https://github.com/XiaoLer)) +- Applied fixes from StyleCI [\#469](https://github.com/overtrue/wechat/pull/469) ([overtrue](https://github.com/overtrue)) +- modify stats [\#468](https://github.com/overtrue/wechat/pull/468) ([wangniuniu](https://github.com/wangniuniu)) + +## [3.1.2](https://github.com/overtrue/wechat/tree/3.1.2) (2016-07-21) +[Full Changelog](https://github.com/overtrue/wechat/compare/2.1.38...3.1.2) + +**Closed issues:** + +- 素材管理中,上传图文下的上传图片,关于返回内容的差异 [\#466](https://github.com/overtrue/wechat/issues/466) +- spbill\_create\_ip参数设置 [\#461](https://github.com/overtrue/wechat/issues/461) + +**Merged pull requests:** + +- 更新获取标签下粉丝列表方法 [\#467](https://github.com/overtrue/wechat/pull/467) ([dingdayu](https://github.com/dingdayu)) +- Applied fixes from StyleCI [\#465](https://github.com/overtrue/wechat/pull/465) ([overtrue](https://github.com/overtrue)) +- card module. [\#464](https://github.com/overtrue/wechat/pull/464) ([wangniuniu](https://github.com/wangniuniu)) +- Applied fixes from StyleCI [\#463](https://github.com/overtrue/wechat/pull/463) ([overtrue](https://github.com/overtrue)) +- Scrutinizer Auto-Fixes [\#462](https://github.com/overtrue/wechat/pull/462) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) + +## [2.1.38](https://github.com/overtrue/wechat/tree/2.1.38) (2016-07-16) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.1.1...2.1.38) + +**Closed issues:** + +- 请问卡券管理功能整合上日程表了吗 [\#454](https://github.com/overtrue/wechat/issues/454) + +**Merged pull requests:** + +- Typo. [\#460](https://github.com/overtrue/wechat/pull/460) ([tianyong90](https://github.com/tianyong90)) +- Applied fixes from StyleCI [\#459](https://github.com/overtrue/wechat/pull/459) ([overtrue](https://github.com/overtrue)) +- add voice recognition [\#458](https://github.com/overtrue/wechat/pull/458) ([leniy](https://github.com/leniy)) +- Applied fixes from StyleCI [\#457](https://github.com/overtrue/wechat/pull/457) ([overtrue](https://github.com/overtrue)) +- Update API.php [\#456](https://github.com/overtrue/wechat/pull/456) ([marvin8212](https://github.com/marvin8212)) +- Update XML.php [\#455](https://github.com/overtrue/wechat/pull/455) ([canon4ever](https://github.com/canon4ever)) + +## [3.1.1](https://github.com/overtrue/wechat/tree/3.1.1) (2016-07-12) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.1.0...3.1.1) + +**Closed issues:** + +- 拿到code=CODE&state=STATE之后怎么拿到openid? [\#452](https://github.com/overtrue/wechat/issues/452) +- 安装出错 [\#450](https://github.com/overtrue/wechat/issues/450) +- 自定义菜单接口\(新版\)出错 [\#448](https://github.com/overtrue/wechat/issues/448) +- h5上没法打开微信app授权界面 [\#447](https://github.com/overtrue/wechat/issues/447) +- 重构卡券 [\#76](https://github.com/overtrue/wechat/issues/76) + +**Merged pull requests:** + +- typos. [\#453](https://github.com/overtrue/wechat/pull/453) ([tianye](https://github.com/tianye)) +- edit readme.md [\#451](https://github.com/overtrue/wechat/pull/451) ([tianyong90](https://github.com/tianyong90)) +- Add cache driver config. [\#449](https://github.com/overtrue/wechat/pull/449) ([dingdayu](https://github.com/dingdayu)) + +## [3.1.0](https://github.com/overtrue/wechat/tree/3.1.0) (2016-06-28) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.0.21...3.1.0) + +**Merged pull requests:** + +- Applied fixes from StyleCI [\#446](https://github.com/overtrue/wechat/pull/446) ([overtrue](https://github.com/overtrue)) +- New Staff API. [\#445](https://github.com/overtrue/wechat/pull/445) ([overtrue](https://github.com/overtrue)) +- 2.1 [\#444](https://github.com/overtrue/wechat/pull/444) ([dongnanyanhai](https://github.com/dongnanyanhai)) +- Fix path. [\#443](https://github.com/overtrue/wechat/pull/443) ([overtrue](https://github.com/overtrue)) + +## [3.0.21](https://github.com/overtrue/wechat/tree/3.0.21) (2016-06-17) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.0.1...3.0.21) + +**Closed issues:** + +- scan出现公众号暂时无法服务的消息 [\#436](https://github.com/overtrue/wechat/issues/436) +- scan出现公众号暂时无法服务的消息 [\#435](https://github.com/overtrue/wechat/issues/435) +- 用户标签接口无法使用 [\#433](https://github.com/overtrue/wechat/issues/433) +- WeChatProvider下的getAuthUrl个人觉得应该暴露出来 [\#432](https://github.com/overtrue/wechat/issues/432) +- 支持二维码扫描进入公众号推送的SCAN事件 [\#431](https://github.com/overtrue/wechat/issues/431) +- \[3.0\] EasyWeChat\Support\XML::parse方法会将空节点解析为空数组,而不是空字符串 [\#426](https://github.com/overtrue/wechat/issues/426) +- 下载二维码, $qrcode-\>download\($ticket,$paths\); 目录参数不可加入 中文 [\#420](https://github.com/overtrue/wechat/issues/420) +- \[help want\]Is hard to change default configuration of GuzzleHttp [\#415](https://github.com/overtrue/wechat/issues/415) +- PHP7.0 curl\_setopt 设置问题 [\#413](https://github.com/overtrue/wechat/issues/413) +- 无法通知微信支付完成 [\#412](https://github.com/overtrue/wechat/issues/412) +- 如何获取用户的unionid? [\#407](https://github.com/overtrue/wechat/issues/407) +- 是否支持多框架 [\#406](https://github.com/overtrue/wechat/issues/406) +- fuckTheWeChatInvalidJSON [\#405](https://github.com/overtrue/wechat/issues/405) +- Class 'GuzzleHttp\Middleware' not found [\#404](https://github.com/overtrue/wechat/issues/404) +- 支付统一下单接口签名错误 [\#402](https://github.com/overtrue/wechat/issues/402) +- payment里没有configForJSSDKPayment方法 [\#401](https://github.com/overtrue/wechat/issues/401) +- 查询支付的地址多了一个空格,导致查询失败,去掉最后的那个空格后就好了 [\#393](https://github.com/overtrue/wechat/issues/393) +- 网页授权过不了 [\#392](https://github.com/overtrue/wechat/issues/392) +- 微信AccessToken被动更新可能会有并发更新的情况出现 [\#390](https://github.com/overtrue/wechat/issues/390) +- 临时素材下载,文件名和扩展名之间会有2个\[.\] [\#389](https://github.com/overtrue/wechat/issues/389) +- 有一个地方变量名对不上 [\#380](https://github.com/overtrue/wechat/issues/380) +- 自定义缓存 [\#379](https://github.com/overtrue/wechat/issues/379) +- https://easywechat.org/ 底部 “开始使用” url拼错 [\#378](https://github.com/overtrue/wechat/issues/378) +- 在server.php里面调用yii的model,一直报错 [\#375](https://github.com/overtrue/wechat/issues/375) +- overture/wechat 2.1.36\(客服消息转发错误\) [\#374](https://github.com/overtrue/wechat/issues/374) +- 建议支持开发模式下禁用验证 [\#373](https://github.com/overtrue/wechat/issues/373) +- https://easywechat.org/ 导航 首页 about:blank [\#370](https://github.com/overtrue/wechat/issues/370) +- laravel 下session问题 [\#369](https://github.com/overtrue/wechat/issues/369) +- 关于Access——toekn [\#368](https://github.com/overtrue/wechat/issues/368) +- 返回支付页面时报错:"access\_token" could not be empty [\#367](https://github.com/overtrue/wechat/issues/367) +- xampp下js-\>config报错 [\#366](https://github.com/overtrue/wechat/issues/366) +- 官方文档有误 [\#360](https://github.com/overtrue/wechat/issues/360) +- \[BUG\] 微信收货地址无法成功 [\#359](https://github.com/overtrue/wechat/issues/359) +- 无法获取 $message-\>ScanCodeInfo-\>ScanType 对象 [\#358](https://github.com/overtrue/wechat/issues/358) +- \[Bugs\] 项目文档首页跳转问题 [\#357](https://github.com/overtrue/wechat/issues/357) +- Business和UnifiedOrder没有定义 [\#356](https://github.com/overtrue/wechat/issues/356) +- 你的网站访问不了。。。。https://easywechat.org/ [\#352](https://github.com/overtrue/wechat/issues/352) +- 连续多次执行微信支付退款报错 [\#348](https://github.com/overtrue/wechat/issues/348) +- 客服操作 都是 -1 错误 [\#344](https://github.com/overtrue/wechat/issues/344) +- 请使用openssl 而不是不安全的mcrypt来加密 [\#342](https://github.com/overtrue/wechat/issues/342) +- 文本类型的通知消息 [\#341](https://github.com/overtrue/wechat/issues/341) +- 服务器配置https 并且 通过阿里云 https cdn之后, 会出现 https 判断语句失效 [\#338](https://github.com/overtrue/wechat/issues/338) +- 作者请问者个sdk支持企业号吗? [\#336](https://github.com/overtrue/wechat/issues/336) +- laravel 5.1引入包报错 [\#331](https://github.com/overtrue/wechat/issues/331) +- 申请退款有问题 [\#328](https://github.com/overtrue/wechat/issues/328) +- 订单相关接口bug [\#327](https://github.com/overtrue/wechat/issues/327) +- 临时素材接口无法使用 [\#319](https://github.com/overtrue/wechat/issues/319) +- 使用sendNormal\(\),sendGroup\(\)发送红包时,报Undefined index: HTTP\_CLIENT\_IP [\#316](https://github.com/overtrue/wechat/issues/316) +- v3中微信卡券功能缺失? [\#307](https://github.com/overtrue/wechat/issues/307) +- 测试 [\#305](https://github.com/overtrue/wechat/issues/305) +- \[3.0\] 永久素材上传视频无法上传问题 [\#304](https://github.com/overtrue/wechat/issues/304) +- Cannot destroy active lambda function [\#296](https://github.com/overtrue/wechat/issues/296) +- 微信支付-》企业付款也可以增加个类上去,跟企业红包类似 [\#232](https://github.com/overtrue/wechat/issues/232) + +**Merged pull requests:** + +- Applied fixes from StyleCI [\#442](https://github.com/overtrue/wechat/pull/442) ([overtrue](https://github.com/overtrue)) +- NGINX HTTPS无法签名 [\#441](https://github.com/overtrue/wechat/pull/441) ([ares333](https://github.com/ares333)) +- Develop [\#440](https://github.com/overtrue/wechat/pull/440) ([overtrue](https://github.com/overtrue)) +- Develop [\#437](https://github.com/overtrue/wechat/pull/437) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#434](https://github.com/overtrue/wechat/pull/434) ([overtrue](https://github.com/overtrue)) +- 修改错误提示信息,方便跟踪错误 [\#430](https://github.com/overtrue/wechat/pull/430) ([zerozh](https://github.com/zerozh)) +- Develop [\#429](https://github.com/overtrue/wechat/pull/429) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#428](https://github.com/overtrue/wechat/pull/428) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#427](https://github.com/overtrue/wechat/pull/427) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#425](https://github.com/overtrue/wechat/pull/425) ([overtrue](https://github.com/overtrue)) +- update annotation [\#424](https://github.com/overtrue/wechat/pull/424) ([lilocon](https://github.com/lilocon)) +- Develop [\#421](https://github.com/overtrue/wechat/pull/421) ([overtrue](https://github.com/overtrue)) +- Set default timeout. [\#419](https://github.com/overtrue/wechat/pull/419) ([overtrue](https://github.com/overtrue)) +- Develop [\#418](https://github.com/overtrue/wechat/pull/418) ([overtrue](https://github.com/overtrue)) +- Develop [\#416](https://github.com/overtrue/wechat/pull/416) ([overtrue](https://github.com/overtrue)) +- better implementation for prepare oauth callback url [\#414](https://github.com/overtrue/wechat/pull/414) ([lichunqiang](https://github.com/lichunqiang)) +- Develop [\#410](https://github.com/overtrue/wechat/pull/410) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#409](https://github.com/overtrue/wechat/pull/409) ([overtrue](https://github.com/overtrue)) +- 增加微信支付服务商支持 [\#408](https://github.com/overtrue/wechat/pull/408) ([takatost](https://github.com/takatost)) +- Develop [\#403](https://github.com/overtrue/wechat/pull/403) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#400](https://github.com/overtrue/wechat/pull/400) ([overtrue](https://github.com/overtrue)) +- Scrutinizer Auto-Fixes [\#399](https://github.com/overtrue/wechat/pull/399) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- Develop [\#398](https://github.com/overtrue/wechat/pull/398) ([overtrue](https://github.com/overtrue)) +- Develop [\#397](https://github.com/overtrue/wechat/pull/397) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#396](https://github.com/overtrue/wechat/pull/396) ([overtrue](https://github.com/overtrue)) +- Typo & Improve code. [\#395](https://github.com/overtrue/wechat/pull/395) ([jinchun](https://github.com/jinchun)) +- Develop [\#394](https://github.com/overtrue/wechat/pull/394) ([overtrue](https://github.com/overtrue)) +- Bugfix close \#389 [\#391](https://github.com/overtrue/wechat/pull/391) ([overtrue](https://github.com/overtrue)) +- Update NoticeNoticeTest.php [\#388](https://github.com/overtrue/wechat/pull/388) ([xiabeifeng](https://github.com/xiabeifeng)) +- Update Notice.php [\#387](https://github.com/overtrue/wechat/pull/387) ([xiabeifeng](https://github.com/xiabeifeng)) +- Tests for \#384 [\#386](https://github.com/overtrue/wechat/pull/386) ([xiabeifeng](https://github.com/xiabeifeng)) +- Improve Notice API. [\#384](https://github.com/overtrue/wechat/pull/384) ([xiabeifeng](https://github.com/xiabeifeng)) +- 对应根 版本依赖 [\#382](https://github.com/overtrue/wechat/pull/382) ([parkshinhye](https://github.com/parkshinhye)) +- Develop [\#381](https://github.com/overtrue/wechat/pull/381) ([overtrue](https://github.com/overtrue)) +- Develop [\#377](https://github.com/overtrue/wechat/pull/377) ([overtrue](https://github.com/overtrue)) +- Fix test for \#371 [\#372](https://github.com/overtrue/wechat/pull/372) ([overtrue](https://github.com/overtrue)) +- 刷卡支付不需要notify\_url参数 [\#371](https://github.com/overtrue/wechat/pull/371) ([lilocon](https://github.com/lilocon)) +- Applied fixes from StyleCI [\#365](https://github.com/overtrue/wechat/pull/365) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#364](https://github.com/overtrue/wechat/pull/364) ([overtrue](https://github.com/overtrue)) +- Merge Develop [\#363](https://github.com/overtrue/wechat/pull/363) ([overtrue](https://github.com/overtrue)) +- Update composer.json [\#361](https://github.com/overtrue/wechat/pull/361) ([jaychan](https://github.com/jaychan)) +- Applied fixes from StyleCI [\#355](https://github.com/overtrue/wechat/pull/355) ([overtrue](https://github.com/overtrue)) +- \[ci skip\]fix document typo [\#354](https://github.com/overtrue/wechat/pull/354) ([lichunqiang](https://github.com/lichunqiang)) +- 自定义Logger [\#353](https://github.com/overtrue/wechat/pull/353) ([lilocon](https://github.com/lilocon)) +- Update Refund.php [\#351](https://github.com/overtrue/wechat/pull/351) ([jaring](https://github.com/jaring)) +- Applied fixes from StyleCI [\#350](https://github.com/overtrue/wechat/pull/350) ([overtrue](https://github.com/overtrue)) +- OpenSSL bugfix. [\#349](https://github.com/overtrue/wechat/pull/349) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#347](https://github.com/overtrue/wechat/pull/347) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#346](https://github.com/overtrue/wechat/pull/346) ([overtrue](https://github.com/overtrue)) +- Merge Develop [\#345](https://github.com/overtrue/wechat/pull/345) ([overtrue](https://github.com/overtrue)) +- 添加代码提示 [\#343](https://github.com/overtrue/wechat/pull/343) ([lilocon](https://github.com/lilocon)) +- Applied fixes from StyleCI [\#340](https://github.com/overtrue/wechat/pull/340) ([overtrue](https://github.com/overtrue)) +- Fix bug: Payment::downloadBill\(\) response error. [\#339](https://github.com/overtrue/wechat/pull/339) ([overtrue](https://github.com/overtrue)) +- change get\_client\_ip to get\_server\_ip [\#335](https://github.com/overtrue/wechat/pull/335) ([tianyong90](https://github.com/tianyong90)) +- Payment SSL. [\#334](https://github.com/overtrue/wechat/pull/334) ([overtrue](https://github.com/overtrue)) +- Add a helper to get correct client ip address. fixed \#316 [\#333](https://github.com/overtrue/wechat/pull/333) ([tianyong90](https://github.com/tianyong90)) +- Dependency Bugfix. overtrue/laravel-wechat\#24 [\#332](https://github.com/overtrue/wechat/pull/332) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#330](https://github.com/overtrue/wechat/pull/330) ([overtrue](https://github.com/overtrue)) +- Merge Develop [\#329](https://github.com/overtrue/wechat/pull/329) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#326](https://github.com/overtrue/wechat/pull/326) ([overtrue](https://github.com/overtrue)) +- Add order default notify\_url. [\#325](https://github.com/overtrue/wechat/pull/325) ([foreverglory](https://github.com/foreverglory)) +- Revert "Applied fixes from StyleCI" [\#323](https://github.com/overtrue/wechat/pull/323) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#322](https://github.com/overtrue/wechat/pull/322) ([overtrue](https://github.com/overtrue)) +- Develop [\#321](https://github.com/overtrue/wechat/pull/321) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#320](https://github.com/overtrue/wechat/pull/320) ([overtrue](https://github.com/overtrue)) +- 模板消息添加【 获取模板列表】和【 删除模板】接口 [\#318](https://github.com/overtrue/wechat/pull/318) ([forecho](https://github.com/forecho)) +- Applied fixes from StyleCI [\#314](https://github.com/overtrue/wechat/pull/314) ([overtrue](https://github.com/overtrue)) +- fix Temporary upload bug [\#313](https://github.com/overtrue/wechat/pull/313) ([mani95lisa](https://github.com/mani95lisa)) +- Applied fixes from StyleCI [\#312](https://github.com/overtrue/wechat/pull/312) ([overtrue](https://github.com/overtrue)) +- MerchantPay Class [\#311](https://github.com/overtrue/wechat/pull/311) ([ac1982](https://github.com/ac1982)) +- Applied fixes from StyleCI [\#309](https://github.com/overtrue/wechat/pull/309) ([overtrue](https://github.com/overtrue)) +- Merge Develop [\#308](https://github.com/overtrue/wechat/pull/308) ([overtrue](https://github.com/overtrue)) +- 删除裂变红包接口中的ip参数 [\#306](https://github.com/overtrue/wechat/pull/306) ([xjchengo](https://github.com/xjchengo)) +- fix code style and some spelling mistakes [\#303](https://github.com/overtrue/wechat/pull/303) ([jinchun](https://github.com/jinchun)) +- Merge Develop [\#302](https://github.com/overtrue/wechat/pull/302) ([overtrue](https://github.com/overtrue)) +- Add method for app payment [\#301](https://github.com/overtrue/wechat/pull/301) ([lichunqiang](https://github.com/lichunqiang)) +- Removed the return syntax [\#300](https://github.com/overtrue/wechat/pull/300) ([lichunqiang](https://github.com/lichunqiang)) +- add return tag [\#299](https://github.com/overtrue/wechat/pull/299) ([lichunqiang](https://github.com/lichunqiang)) +- Merge Develop [\#298](https://github.com/overtrue/wechat/pull/298) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#297](https://github.com/overtrue/wechat/pull/297) ([overtrue](https://github.com/overtrue)) +- \[ci skip\]Update .gitattributes [\#295](https://github.com/overtrue/wechat/pull/295) ([lichunqiang](https://github.com/lichunqiang)) +- Merge Develop [\#294](https://github.com/overtrue/wechat/pull/294) ([overtrue](https://github.com/overtrue)) + +## [3.0.1](https://github.com/overtrue/wechat/tree/3.0.1) (2016-02-19) +[Full Changelog](https://github.com/overtrue/wechat/compare/3.0...3.0.1) + +**Closed issues:** + +- composer 安装 3.0版本,报错如下: [\#291](https://github.com/overtrue/wechat/issues/291) +- \[3.0\] 下载永久素材时,微信返回的Content-Type不正确,导致出错。 [\#290](https://github.com/overtrue/wechat/issues/290) +- 挖个坑,自己跳 [\#147](https://github.com/overtrue/wechat/issues/147) + +**Merged pull requests:** + +- Applied fixes from StyleCI [\#293](https://github.com/overtrue/wechat/pull/293) ([overtrue](https://github.com/overtrue)) +- Merge Develop [\#292](https://github.com/overtrue/wechat/pull/292) ([overtrue](https://github.com/overtrue)) + +## [3.0](https://github.com/overtrue/wechat/tree/3.0) (2016-02-17) +[Full Changelog](https://github.com/overtrue/wechat/compare/2.1.0...3.0) + +**Implemented enhancements:** + +- MIME json 格式检查优化 [\#49](https://github.com/overtrue/wechat/issues/49) +- 获取 refresh\_token,access\_token [\#43](https://github.com/overtrue/wechat/issues/43) +- 关于API\_TOKEN\_REFRESH [\#20](https://github.com/overtrue/wechat/issues/20) + +**Closed issues:** + +- \[3.0\] 无法获取用户分组信息 [\#285](https://github.com/overtrue/wechat/issues/285) +- 新的laravel 5.2 不能兼容了 [\#284](https://github.com/overtrue/wechat/issues/284) +- \[3.0\]Message/Article类的$properties内的source\_url没有正常转换为content\_source\_url. [\#281](https://github.com/overtrue/wechat/issues/281) +- 3.0删除个性菜单失败 [\#280](https://github.com/overtrue/wechat/issues/280) +- 也许你该给一个代码贡献规范 [\#277](https://github.com/overtrue/wechat/issues/277) +- 3.0网页授权时scope为snsapi\_base得不到openid [\#276](https://github.com/overtrue/wechat/issues/276) +- wechat3.0中 有2个地方的js调用参数不一样,超哥没有提供 [\#272](https://github.com/overtrue/wechat/issues/272) +- 我想知道2.X和3.0有什么大的区别! [\#270](https://github.com/overtrue/wechat/issues/270) +- 2.1: Link 消息类型没有实现 [\#269](https://github.com/overtrue/wechat/issues/269) +- 关于模板消息换行的问题 [\#266](https://github.com/overtrue/wechat/issues/266) +- easywechat Invalid request [\#265](https://github.com/overtrue/wechat/issues/265) +- 40029不合法的oauth\_code [\#264](https://github.com/overtrue/wechat/issues/264) +- 下载素材的一个小问题 [\#263](https://github.com/overtrue/wechat/issues/263) +- \[2.1\] 微信自定义菜单结构变更导致`Menu::get\(\)` 无法读取个性化菜单 [\#262](https://github.com/overtrue/wechat/issues/262) +- payment中是不是不包含H5和JS的生成配置文件的方法了? [\#261](https://github.com/overtrue/wechat/issues/261) +- payment下prepare方法bug [\#260](https://github.com/overtrue/wechat/issues/260) +- UserServiceProvider中似乎忘记注册user.group了 [\#256](https://github.com/overtrue/wechat/issues/256) +- 2.1.X版媒体下载没有扩展名 [\#252](https://github.com/overtrue/wechat/issues/252) +- 为什么所有的子模块在自己的库都是develop分支 [\#247](https://github.com/overtrue/wechat/issues/247) +- 网页授权使用跳转的bug [\#246](https://github.com/overtrue/wechat/issues/246) +- typo of variable [\#245](https://github.com/overtrue/wechat/issues/245) +- The implementation class of ServerServiceProvider missing an important [\#244](https://github.com/overtrue/wechat/issues/244) +- \[3.0\]\[payment\] 两个可能的bug [\#235](https://github.com/overtrue/wechat/issues/235) +- 发送多图文 [\#233](https://github.com/overtrue/wechat/issues/233) +- 自定义菜单返回应该把个性化自定义菜单也一起返回 [\#231](https://github.com/overtrue/wechat/issues/231) +- 发送模板消息 CRUL 错误 [\#223](https://github.com/overtrue/wechat/issues/223) +- 客服接口暂时测到有3个bug,麻烦修复 [\#222](https://github.com/overtrue/wechat/issues/222) +- JSSDK access\_token missing [\#211](https://github.com/overtrue/wechat/issues/211) +- Js.php/ticket [\#210](https://github.com/overtrue/wechat/issues/210) +- 微信支付里有一个收货地址共享 ,超哥你这里没有,可以加一下不? [\#204](https://github.com/overtrue/wechat/issues/204) +- 小问题 [\#203](https://github.com/overtrue/wechat/issues/203) +- 网页授权 跳转 [\#202](https://github.com/overtrue/wechat/issues/202) +- access token 重复添加的问题 [\#201](https://github.com/overtrue/wechat/issues/201) +- authorize snsapi\_base 下可以获取unionid [\#198](https://github.com/overtrue/wechat/issues/198) +- 网页授权 [\#189](https://github.com/overtrue/wechat/issues/189) +- 一点建议 [\#188](https://github.com/overtrue/wechat/issues/188) +- 接口更新-新增临时素材接口变动 [\#186](https://github.com/overtrue/wechat/issues/186) +- 接入多个公众号不用id [\#185](https://github.com/overtrue/wechat/issues/185) +- \[Insight\] Files should not be executable [\#184](https://github.com/overtrue/wechat/issues/184) +- 建议不要写死Http [\#183](https://github.com/overtrue/wechat/issues/183) +- laravel4.2安装不成功 [\#182](https://github.com/overtrue/wechat/issues/182) +- 是否支持laravel4.2 [\#181](https://github.com/overtrue/wechat/issues/181) +- 微信出个性化菜单了,希望支持 [\#180](https://github.com/overtrue/wechat/issues/180) +- 3.0 composer依赖Symfony2.7。能不能支持Symfony3.0? [\#179](https://github.com/overtrue/wechat/issues/179) +- 发送链接类消息错误 [\#175](https://github.com/overtrue/wechat/issues/175) +- Throw Exception的时候 Intel server status 设置为200是不是好一些 [\#174](https://github.com/overtrue/wechat/issues/174) +- 生成临时二维码时,返回EventKey不是传递的值 [\#173](https://github.com/overtrue/wechat/issues/173) +- 关于素材获取的一个建议 [\#172](https://github.com/overtrue/wechat/issues/172) +- 能否增加微信APP支付相关方法 [\#171](https://github.com/overtrue/wechat/issues/171) +- 微信回调URL回调不到 [\#170](https://github.com/overtrue/wechat/issues/170) +- 素材管理添加永久素材返回JSON/XML内容错误 [\#169](https://github.com/overtrue/wechat/issues/169) +- \[消息的使用\] 中 \[上传素材文件\] 的文档示例貌似有误 [\#168](https://github.com/overtrue/wechat/issues/168) +- 素材管理里的download方法不是很符合sdk一站式的解决. [\#165](https://github.com/overtrue/wechat/issues/165) +- \[Wechat\]不合法的oauth\_code' in /src/Wechat/Http.php:124 [\#164](https://github.com/overtrue/wechat/issues/164) +- AccessToken Expired Error Code [\#163](https://github.com/overtrue/wechat/issues/163) +- 素材管理接口出错 [\#162](https://github.com/overtrue/wechat/issues/162) +- 两处代码php5.4才能运行 [\#158](https://github.com/overtrue/wechat/issues/158) +- extension is null when calling `download video` in wechat.media [\#157](https://github.com/overtrue/wechat/issues/157) +- Payment/UnifiedOrder does not support serialize or create by array [\#155](https://github.com/overtrue/wechat/issues/155) +- 没有找到"微信支付-\>查询订单"相关功能 [\#150](https://github.com/overtrue/wechat/issues/150) +- 请教,Cache::setter中your\_custom\_set\_cache怎么使用 [\#149](https://github.com/overtrue/wechat/issues/149) +- 发生异常时, 希望能把发送和接收的原始数据记录下来. [\#148](https://github.com/overtrue/wechat/issues/148) +- 发送红包,证书错误 [\#144](https://github.com/overtrue/wechat/issues/144) +- 发视频消息总返回 -1 [\#143](https://github.com/overtrue/wechat/issues/143) +- 关于PHP版本 [\#141](https://github.com/overtrue/wechat/issues/141) +- Server消息回复必须以事件方式吗? [\#140](https://github.com/overtrue/wechat/issues/140) +- 微信支付相关文档细化 [\#138](https://github.com/overtrue/wechat/issues/138) +- 好奇地问个问题,这项目的测试用例放在哪? [\#135](https://github.com/overtrue/wechat/issues/135) +- 试了两次,真的不会用 [\#134](https://github.com/overtrue/wechat/issues/134) +- 不知道这算不算是个BUG [\#133](https://github.com/overtrue/wechat/issues/133) +- 微信小店 [\#130](https://github.com/overtrue/wechat/issues/130) +- 多次遇到 accesstoken 无效的问题 [\#129](https://github.com/overtrue/wechat/issues/129) +- MCH\_KEY 微信支付 [\#128](https://github.com/overtrue/wechat/issues/128) +- 使用flightphp框架,验证URL的时候,在Apache下接入成功,在Nginx接入失败 [\#126](https://github.com/overtrue/wechat/issues/126) +- 好东西!可惜没有我需要的微信红包 [\#125](https://github.com/overtrue/wechat/issues/125) +- Cache存储部件可定制 [\#120](https://github.com/overtrue/wechat/issues/120) +- 关于Bag [\#119](https://github.com/overtrue/wechat/issues/119) +- 将代码部署到负载均衡上如何管理access token [\#118](https://github.com/overtrue/wechat/issues/118) +- 消息接受和回复时,如果不对消息做回复,该如何做? [\#117](https://github.com/overtrue/wechat/issues/117) +- 请教一个问题 [\#116](https://github.com/overtrue/wechat/issues/116) +- 关于 Cache [\#115](https://github.com/overtrue/wechat/issues/115) +- 如何才能获取普通的access\_token [\#113](https://github.com/overtrue/wechat/issues/113) +- $HTTP\_RAW\_POST\_DATA DEPRECATED [\#111](https://github.com/overtrue/wechat/issues/111) +- App支付缺少错误码 [\#109](https://github.com/overtrue/wechat/issues/109) +- 当用户信息有 " 字符时系统出错 \(用户与用户组管理接口\) [\#107](https://github.com/overtrue/wechat/issues/107) +- 提示错误 [\#106](https://github.com/overtrue/wechat/issues/106) +- 使用企业号的时候 接入失败啊,在验证url的时候 [\#104](https://github.com/overtrue/wechat/issues/104) +- 支付签名错误 [\#101](https://github.com/overtrue/wechat/issues/101) +- 微信支付.$payment-\>getConfig\(\)调用时候\[Wechat\]系统繁忙,此时请开发者稍候再试. [\#96](https://github.com/overtrue/wechat/issues/96) +- wechat/src/Wechat/Payment/UnifiedOrder.php 小问题 [\#94](https://github.com/overtrue/wechat/issues/94) +- 请教laravel中如何在微信支付中 catch UnifiedOrder 抛出的异常? [\#93](https://github.com/overtrue/wechat/issues/93) +- 是否可以增加一个第三方接口融合功能 [\#91](https://github.com/overtrue/wechat/issues/91) +- 订单查询 [\#90](https://github.com/overtrue/wechat/issues/90) +- 如何不下载图片,通过mediaId获取图片存储的URL [\#89](https://github.com/overtrue/wechat/issues/89) +- 'Undefined index: HTTP\_HOST' [\#88](https://github.com/overtrue/wechat/issues/88) +- Undefined index: HTTP\_HOST [\#87](https://github.com/overtrue/wechat/issues/87) +- 不能上传gif格式的图片素材 [\#84](https://github.com/overtrue/wechat/issues/84) +- OAuth重构 [\#74](https://github.com/overtrue/wechat/issues/74) +- \[3.0\] Tasks [\#50](https://github.com/overtrue/wechat/issues/50) +- appId 和 appSecret不要作为各个类的构造参数 [\#114](https://github.com/overtrue/wechat/issues/114) +- 增加debug相关的选项 [\#112](https://github.com/overtrue/wechat/issues/112) +- 好像没有获取自动回复数据接口 [\#108](https://github.com/overtrue/wechat/issues/108) +- js端查看微信卡券接口 chooseCard [\#79](https://github.com/overtrue/wechat/issues/79) +- 【新增】支付 [\#78](https://github.com/overtrue/wechat/issues/78) +- 模板消息重构 [\#75](https://github.com/overtrue/wechat/issues/75) +- 素材下载自动识别MIME生成后缀 [\#54](https://github.com/overtrue/wechat/issues/54) +- \[建议\] 深度结合微信多图文与素材管理 [\#46](https://github.com/overtrue/wechat/issues/46) +- 群发功能 [\#18](https://github.com/overtrue/wechat/issues/18) + +**Merged pull requests:** + +- 3.0 [\#289](https://github.com/overtrue/wechat/pull/289) ([overtrue](https://github.com/overtrue)) +- Merge Develop [\#288](https://github.com/overtrue/wechat/pull/288) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#287](https://github.com/overtrue/wechat/pull/287) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#286](https://github.com/overtrue/wechat/pull/286) ([overtrue](https://github.com/overtrue)) +- Fix bug in batchGet method. [\#283](https://github.com/overtrue/wechat/pull/283) ([tianyong90](https://github.com/tianyong90)) +- Typo. [\#279](https://github.com/overtrue/wechat/pull/279) ([overtrue](https://github.com/overtrue)) +- Add contribution guide. resolves \#277 [\#278](https://github.com/overtrue/wechat/pull/278) ([overtrue](https://github.com/overtrue)) +- Develop [\#274](https://github.com/overtrue/wechat/pull/274) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#273](https://github.com/overtrue/wechat/pull/273) ([overtrue](https://github.com/overtrue)) +- Develop [\#271](https://github.com/overtrue/wechat/pull/271) ([overtrue](https://github.com/overtrue)) +- Merge Develop [\#268](https://github.com/overtrue/wechat/pull/268) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#267](https://github.com/overtrue/wechat/pull/267) ([overtrue](https://github.com/overtrue)) +- Update QRCode.php [\#258](https://github.com/overtrue/wechat/pull/258) ([webshiyue](https://github.com/webshiyue)) +- Add tests for LuckyMoney. [\#255](https://github.com/overtrue/wechat/pull/255) ([tianyong90](https://github.com/tianyong90)) +- CS. [\#254](https://github.com/overtrue/wechat/pull/254) ([overtrue](https://github.com/overtrue)) +- Scrutinizer Auto-Fixes [\#253](https://github.com/overtrue/wechat/pull/253) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- Applied fixes from StyleCI [\#251](https://github.com/overtrue/wechat/pull/251) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#250](https://github.com/overtrue/wechat/pull/250) ([overtrue](https://github.com/overtrue)) +- Merge Develop [\#249](https://github.com/overtrue/wechat/pull/249) ([overtrue](https://github.com/overtrue)) +- Merge Develop [\#248](https://github.com/overtrue/wechat/pull/248) ([overtrue](https://github.com/overtrue)) +- Merge from Develop [\#243](https://github.com/overtrue/wechat/pull/243) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#242](https://github.com/overtrue/wechat/pull/242) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#241](https://github.com/overtrue/wechat/pull/241) ([overtrue](https://github.com/overtrue)) +- Add Luckymoney. [\#240](https://github.com/overtrue/wechat/pull/240) ([tianyong90](https://github.com/tianyong90)) +- Applied fixes from StyleCI [\#237](https://github.com/overtrue/wechat/pull/237) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#234](https://github.com/overtrue/wechat/pull/234) ([overtrue](https://github.com/overtrue)) +- Multiple News Items Support [\#230](https://github.com/overtrue/wechat/pull/230) ([fanglinks](https://github.com/fanglinks)) +- Applied fixes from StyleCI [\#221](https://github.com/overtrue/wechat/pull/221) ([overtrue](https://github.com/overtrue)) +- \[3.0\]\[Bugfix\]发送图文消息缺少type [\#217](https://github.com/overtrue/wechat/pull/217) ([sunbiao0526](https://github.com/sunbiao0526)) +- fix Js.php 获取自定义cache对象 [\#215](https://github.com/overtrue/wechat/pull/215) ([sunbiao0526](https://github.com/sunbiao0526)) +- Applied fixes from StyleCI [\#197](https://github.com/overtrue/wechat/pull/197) ([overtrue](https://github.com/overtrue)) +- Add alias [\#196](https://github.com/overtrue/wechat/pull/196) ([ruchengtang](https://github.com/ruchengtang)) +- Applied fixes from StyleCI [\#195](https://github.com/overtrue/wechat/pull/195) ([overtrue](https://github.com/overtrue)) +- Applied fixes from StyleCI [\#194](https://github.com/overtrue/wechat/pull/194) ([overtrue](https://github.com/overtrue)) +- Add Broadcast. [\#193](https://github.com/overtrue/wechat/pull/193) ([ruchengtang](https://github.com/ruchengtang)) +- 微信红包类优化 [\#190](https://github.com/overtrue/wechat/pull/190) ([tianyong90](https://github.com/tianyong90)) +- Update ServerServiceProvider.php [\#187](https://github.com/overtrue/wechat/pull/187) ([ghost](https://github.com/ghost)) +- Update README\_EN.md [\#178](https://github.com/overtrue/wechat/pull/178) ([spekulatius](https://github.com/spekulatius)) +- 添加群发消息文档 [\#177](https://github.com/overtrue/wechat/pull/177) ([ruchengtang](https://github.com/ruchengtang)) +- 群发消息 [\#176](https://github.com/overtrue/wechat/pull/176) ([ruchengtang](https://github.com/ruchengtang)) +- Master [\#167](https://github.com/overtrue/wechat/pull/167) ([xiaohome](https://github.com/xiaohome)) +- 微信小店 [\#166](https://github.com/overtrue/wechat/pull/166) ([xiaohome](https://github.com/xiaohome)) +- 红包类更新 [\#161](https://github.com/overtrue/wechat/pull/161) ([overtrue](https://github.com/overtrue)) +- 加入摇一摇红包类,红包类提升至Overtrue命名空间 [\#160](https://github.com/overtrue/wechat/pull/160) ([tianyong90](https://github.com/tianyong90)) +- 2.1 [\#159](https://github.com/overtrue/wechat/pull/159) ([overtrue](https://github.com/overtrue)) +- Update QRCode.php [\#156](https://github.com/overtrue/wechat/pull/156) ([ruchengtang](https://github.com/ruchengtang)) +- 修复使用!=,来判断0 != null 的时候的一个bug [\#154](https://github.com/overtrue/wechat/pull/154) ([Liv1020](https://github.com/Liv1020)) +- 调整多客服类删除客服方法 [\#151](https://github.com/overtrue/wechat/pull/151) ([tianyong90](https://github.com/tianyong90)) +- 修复个bug [\#146](https://github.com/overtrue/wechat/pull/146) ([xiaohome](https://github.com/xiaohome)) +- Update README.md [\#142](https://github.com/overtrue/wechat/pull/142) ([parkshinhye](https://github.com/parkshinhye)) +- Fix code style to PSR-2 [\#139](https://github.com/overtrue/wechat/pull/139) ([tianyong90](https://github.com/tianyong90)) +- 加入红包工具类,支持现金和裂变红包的发送及查询 [\#137](https://github.com/overtrue/wechat/pull/137) ([tianyong90](https://github.com/tianyong90)) +- 卡券类批量获取卡券ID方法支持仅获取指定状态卡券 [\#132](https://github.com/overtrue/wechat/pull/132) ([tianyong90](https://github.com/tianyong90)) +- 添加客服 卡券回复!!! [\#124](https://github.com/overtrue/wechat/pull/124) ([parkshinhye](https://github.com/parkshinhye)) +- 调整退款类中一处异常抛出逻辑并修正单词拼写错误 [\#122](https://github.com/overtrue/wechat/pull/122) ([tianyong90](https://github.com/tianyong90)) +- 加入创建卡券货架接口 [\#121](https://github.com/overtrue/wechat/pull/121) ([tianyong90](https://github.com/tianyong90)) +- 增加退款类 [\#105](https://github.com/overtrue/wechat/pull/105) ([jaring](https://github.com/jaring)) +- 增加获取用户已领取卡券方法 [\#103](https://github.com/overtrue/wechat/pull/103) ([tenstone](https://github.com/tenstone)) +- Scrutinizer Auto-Fixes [\#100](https://github.com/overtrue/wechat/pull/100) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- 修正二维码类中生成卡券二维码方法 [\#99](https://github.com/overtrue/wechat/pull/99) ([tianyong90](https://github.com/tianyong90)) +- 卡券接口加入添加测试白名单方法 [\#98](https://github.com/overtrue/wechat/pull/98) ([tianyong90](https://github.com/tianyong90)) +- 依样画葫芦写了一个查询订单,更改了UnifiedOrder中Http初始化 [\#95](https://github.com/overtrue/wechat/pull/95) ([jaring](https://github.com/jaring)) +- accessToken根据appId变化 [\#92](https://github.com/overtrue/wechat/pull/92) ([keepeye](https://github.com/keepeye)) +- Fix payment sign bug. [\#82](https://github.com/overtrue/wechat/pull/82) ([0i](https://github.com/0i)) +- \[wiki\] wechat payment [\#81](https://github.com/overtrue/wechat/pull/81) ([0i](https://github.com/0i)) + +## [2.1.0](https://github.com/overtrue/wechat/tree/2.1.0) (2015-08-18) +[Full Changelog](https://github.com/overtrue/wechat/compare/2.0.35...2.1.0) + +**Merged pull requests:** + +- Wechat Payment [\#80](https://github.com/overtrue/wechat/pull/80) ([0i](https://github.com/0i)) + +## [2.0.35](https://github.com/overtrue/wechat/tree/2.0.35) (2015-08-11) +[Full Changelog](https://github.com/overtrue/wechat/compare/2.0.1...2.0.35) + +**Implemented enhancements:** + +- Overtrue\Wechat\Http识别JSON的问题 [\#47](https://github.com/overtrue/wechat/issues/47) + +**Fixed bugs:** + +- 模板消息简单格式无效 [\#34](https://github.com/overtrue/wechat/issues/34) + +**Closed issues:** + +- $data是数组,title输出不了内容 [\#73](https://github.com/overtrue/wechat/issues/73) +- 回调是如何传递外部参数的? [\#72](https://github.com/overtrue/wechat/issues/72) +- 【建议】可以添加微信js的功能吗? [\#71](https://github.com/overtrue/wechat/issues/71) +- Message::make\('link'\) 无效 [\#70](https://github.com/overtrue/wechat/issues/70) +- 监听消息 返回Bad Request [\#65](https://github.com/overtrue/wechat/issues/65) +- 微信素材管理小改版,求跟上~ [\#64](https://github.com/overtrue/wechat/issues/64) +- 在新浪SAE平台上的部署问题 [\#63](https://github.com/overtrue/wechat/issues/63) +- $xmlInput = file\_get\_contents\('php://input'\);貌似在某些版本的PHP有问题还是怎的 [\#57](https://github.com/overtrue/wechat/issues/57) +- 卡券的 attachExtension 方法 [\#56](https://github.com/overtrue/wechat/issues/56) +- 网页授权$auth-\>authorize\(\) 后还需要保存access\_token吗? [\#53](https://github.com/overtrue/wechat/issues/53) +- php 5.6版本下出现错误(5.6以下版本正常) [\#51](https://github.com/overtrue/wechat/issues/51) +- 消息发送后服务器无法正确返回响应 [\#48](https://github.com/overtrue/wechat/issues/48) +- token验证失败 [\#45](https://github.com/overtrue/wechat/issues/45) +- 微信关注自动回复问题 [\#44](https://github.com/overtrue/wechat/issues/44) +- js sdk config 建议增加 beta 字段 [\#35](https://github.com/overtrue/wechat/issues/35) +- 关于Util\HTTP::encode\(\)中的urlencode\(\)/urldecode\(\)成组操作的疑问 [\#31](https://github.com/overtrue/wechat/issues/31) +- Media::updateNews\(\) 方法与微信API不一致 [\#29](https://github.com/overtrue/wechat/issues/29) +- 希望能有一个ThinkPHP的使用示例 [\#28](https://github.com/overtrue/wechat/issues/28) +- 事件消息 [\#22](https://github.com/overtrue/wechat/issues/22) +- 模板消息notice [\#21](https://github.com/overtrue/wechat/issues/21) +- 关于获取(接收)用户发送消息 [\#19](https://github.com/overtrue/wechat/issues/19) +- 微信公众号绑定的一点问题,请教。 [\#16](https://github.com/overtrue/wechat/issues/16) +- 获取素材列表错误 [\#15](https://github.com/overtrue/wechat/issues/15) + +**Merged pull requests:** + +- Scrutinizer Auto-Fixes [\#69](https://github.com/overtrue/wechat/pull/69) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- Scrutinizer Auto-Fixes [\#68](https://github.com/overtrue/wechat/pull/68) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- Scrutinizer Auto-Fixes [\#67](https://github.com/overtrue/wechat/pull/67) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- Fixed StyleCI config [\#66](https://github.com/overtrue/wechat/pull/66) ([GrahamCampbell](https://github.com/GrahamCampbell)) +- 洁癖爆发了。。。 [\#62](https://github.com/overtrue/wechat/pull/62) ([TheNorthMemory](https://github.com/TheNorthMemory)) +- fix: js getUrl use Url::current\(\) [\#61](https://github.com/overtrue/wechat/pull/61) ([wdjwxh](https://github.com/wdjwxh)) +- bug-fix: add x-forwarded-host for Url::current [\#60](https://github.com/overtrue/wechat/pull/60) ([wdjwxh](https://github.com/wdjwxh)) +- Fix request method for User::batchGet\(\), should be POST with JSON. [\#59](https://github.com/overtrue/wechat/pull/59) ([acgrid](https://github.com/acgrid)) +- optimize some code [\#58](https://github.com/overtrue/wechat/pull/58) ([tabalt](https://github.com/tabalt)) +- 增加使用media id发送图文消息的功能 [\#52](https://github.com/overtrue/wechat/pull/52) ([zengohm](https://github.com/zengohm)) +- fix Staff::delete, let it works [\#42](https://github.com/overtrue/wechat/pull/42) ([TheNorthMemory](https://github.com/TheNorthMemory)) +- 支持自定义菜单类型:下发消息media\_id、跳转图文消息view\_limited [\#40](https://github.com/overtrue/wechat/pull/40) ([acgrid](https://github.com/acgrid)) +- docline comments & fix AccessToken parameter typos [\#39](https://github.com/overtrue/wechat/pull/39) ([TheNorthMemory](https://github.com/TheNorthMemory)) +- Merge from master [\#38](https://github.com/overtrue/wechat/pull/38) ([overtrue](https://github.com/overtrue)) +- 客服接口Bugfix [\#37](https://github.com/overtrue/wechat/pull/37) ([overtrue](https://github.com/overtrue)) +- fix Staff and AccessToken typos [\#36](https://github.com/overtrue/wechat/pull/36) ([TheNorthMemory](https://github.com/TheNorthMemory)) +- Update QRCode.php [\#33](https://github.com/overtrue/wechat/pull/33) ([refear99](https://github.com/refear99)) +- English Readme [\#32](https://github.com/overtrue/wechat/pull/32) ([hareluya](https://github.com/hareluya)) +- 更新图文消息方法Media::updateNews\(\)与微信API不一致 [\#30](https://github.com/overtrue/wechat/pull/30) ([acgrid](https://github.com/acgrid)) +- 代码之美在于不断修正 :\) [\#27](https://github.com/overtrue/wechat/pull/27) ([TheNorthMemory](https://github.com/TheNorthMemory)) +- the json\_encode $depth parameter was added@5.5.0 [\#26](https://github.com/overtrue/wechat/pull/26) ([TheNorthMemory](https://github.com/TheNorthMemory)) +- fix \#4 for PHP5.3 [\#25](https://github.com/overtrue/wechat/pull/25) ([TheNorthMemory](https://github.com/TheNorthMemory)) +- fix \#4 for PHP5.3 [\#23](https://github.com/overtrue/wechat/pull/23) ([TheNorthMemory](https://github.com/TheNorthMemory)) +- Update QRCode.php [\#17](https://github.com/overtrue/wechat/pull/17) ([gundanx10](https://github.com/gundanx10)) + +## [2.0.1](https://github.com/overtrue/wechat/tree/2.0.1) (2015-05-08) +[Full Changelog](https://github.com/overtrue/wechat/compare/2.0.0...2.0.1) + +**Closed issues:** + +- 2.0版本使用问题 [\#14](https://github.com/overtrue/wechat/issues/14) + +## [2.0.0](https://github.com/overtrue/wechat/tree/2.0.0) (2015-05-07) +[Full Changelog](https://github.com/overtrue/wechat/compare/1.0.1...2.0.0) + +**Closed issues:** + +- 素材管理 -- 部分图片下载失败 [\#13](https://github.com/overtrue/wechat/issues/13) +- 素材管理 -- 图片下载失败 [\#12](https://github.com/overtrue/wechat/issues/12) +- 请问这样判断Mcrypt到底准不准? [\#11](https://github.com/overtrue/wechat/issues/11) +- 好奇怪啊,开发者中心的服务器配置已经提交并验证成功了,可是message不起作用 [\#10](https://github.com/overtrue/wechat/issues/10) +- 网页授权一刷新页面就出现40029 不合法的oauth\_code [\#8](https://github.com/overtrue/wechat/issues/8) +- mcrypt\_module\_open error [\#7](https://github.com/overtrue/wechat/issues/7) +- composer update 之后报错 [\#6](https://github.com/overtrue/wechat/issues/6) +- 今天开始,授权时候一直报40029,invalid code的错误 [\#5](https://github.com/overtrue/wechat/issues/5) +- Using $this when not in object context [\#4](https://github.com/overtrue/wechat/issues/4) +- 监听事件时不区分 $target(监听所有event和message) [\#3](https://github.com/overtrue/wechat/issues/3) +- Does this support Oauth already? [\#1](https://github.com/overtrue/wechat/issues/1) + +**Merged pull requests:** + +- Fix wiki url error [\#9](https://github.com/overtrue/wechat/pull/9) ([sinoon](https://github.com/sinoon)) +- Update Bag.php [\#2](https://github.com/overtrue/wechat/pull/2) ([zerozh](https://github.com/zerozh)) + +## [1.0.1](https://github.com/overtrue/wechat/tree/1.0.1) (2015-03-19) +[Full Changelog](https://github.com/overtrue/wechat/compare/1.0...1.0.1) + +## [1.0](https://github.com/overtrue/wechat/tree/1.0) (2015-03-13) + + +\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* \ No newline at end of file diff --git a/vendor/overtrue/wechat/CONTRIBUTING.md b/vendor/overtrue/wechat/CONTRIBUTING.md new file mode 100644 index 0000000..134204f --- /dev/null +++ b/vendor/overtrue/wechat/CONTRIBUTING.md @@ -0,0 +1,67 @@ +# Contribute + +## Introduction + +First, thank you for considering contributing to wechat! It's people like you that make the open source community such a great community! 😊 + +We welcome any type of contribution, not only code. You can help with +- **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open) +- **Marketing**: writing blog posts, howto's, printing stickers, ... +- **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ... +- **Code**: take a look at the [open issues](issues). Even if you can't write code, commenting on them, showing that you care about a given issue matters. It helps us triage them. +- **Money**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/wechat). + +## Your First Contribution + +Working on your first Pull Request? You can learn how from this *free* series, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). + +## Submitting code + +Any code change should be submitted as a pull request. The description should explain what the code does and give steps to execute it. The pull request should also contain tests. + +## Code review process + +The bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge. +It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you? + +## Financial contributions + +We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/wechat). +Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed. + +## Questions + +If you have any questions, create an [issue](issue) (protip: do a quick search first to see if someone else didn't ask the same question before!). +You can also reach us at hello@wechat.opencollective.com. + +## Credits + +### Contributors + +Thank you to all the people who have already contributed to wechat! + + + +### Backers + +Thank you to all our backers! [[Become a backer](https://opencollective.com/wechat#backer)] + + + + +### Sponsors + +Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/wechat#sponsor)) + + + + + + + + + + + + + \ No newline at end of file diff --git a/vendor/overtrue/wechat/LICENSE b/vendor/overtrue/wechat/LICENSE new file mode 100644 index 0000000..f80ba02 --- /dev/null +++ b/vendor/overtrue/wechat/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) overtrue + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/overtrue/wechat/README.md b/vendor/overtrue/wechat/README.md new file mode 100644 index 0000000..6d1747f --- /dev/null +++ b/vendor/overtrue/wechat/README.md @@ -0,0 +1,92 @@ +EasyWeChat Logo + +

              EasyWeChat

              + +📦 It is probably the best SDK in the world for developing Wechat App. + +[![Test Status](https://github.com/overtrue/wechat/workflows/Test/badge.svg)](https://github.com/overtrue/wechat/actions) +[![Lint Status](https://github.com/overtrue/wechat/workflows/Lint/badge.svg)](https://github.com/overtrue/wechat/actions) +[![Latest Stable Version](https://poser.pugx.org/overtrue/wechat/v/stable.svg)](https://packagist.org/packages/overtrue/wechat) +[![Latest Unstable Version](https://poser.pugx.org/overtrue/wechat/v/unstable.svg)](https://packagist.org/packages/overtrue/wechat) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/overtrue/wechat/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/overtrue/wechat/?branch=master) +[![Code Coverage](https://scrutinizer-ci.com/g/overtrue/wechat/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/overtrue/wechat/?branch=master) +[![Total Downloads](https://poser.pugx.org/overtrue/wechat/downloads)](https://packagist.org/packages/overtrue/wechat) +[![License](https://poser.pugx.org/overtrue/wechat/license)](https://packagist.org/packages/overtrue/wechat) + + +## Requirement + +1. PHP >= 7.2 +2. **[Composer](https://getcomposer.org/)** +3. openssl 拓展 +4. fileinfo 拓展(素材管理模块需要用到) + +## Installation + +```shell +$ composer require "overtrue/wechat:^4.2" -vvv +``` + +## Usage + +基本使用(以服务端为例): + +```php + 'wx3cf0f39249eb0exxx', + 'secret' => 'f1c242f4f28f735d4687abb469072xxx', + 'token' => 'easywechat', + 'log' => [ + 'level' => 'debug', + 'file' => '/tmp/easywechat.log', + ], + // ... +]; + +$app = Factory::officialAccount($options); + +$server = $app->server; +$user = $app->user; + +$server->push(function($message) use ($user) { + $fromUser = $user->get($message['FromUserName']); + + return "{$fromUser->nickname} 您好!欢迎关注 overtrue!"; +}); + +$server->serve()->send(); +``` + +更多请参考 [https://www.easywechat.com/](https://www.easywechat.com/)。 + +## Documentation + +[官网](https://www.easywechat.com) · [教程](https://www.easywechat.com/tutorials) · [讨论](https://yike.io/) · [微信公众平台](https://mp.weixin.qq.com/wiki) · [WeChat Official](http://admin.wechat.com/wiki) + +## Integration + +[Laravel 5 拓展包: overtrue/laravel-wechat](https://github.com/overtrue/laravel-wechat) + +## Contributors + +This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. + + + +## PHP 扩展包开发 + +> 想知道如何从零开始构建 PHP 扩展包? +> +> 请关注我的实战课程,我会在此课程中分享一些扩展开发经验 —— [《PHP 扩展包实战教程 - 从入门到发布》](https://learnku.com/courses/creating-package) + + +## License + +MIT + + +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fovertrue%2Fwechat.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fovertrue%2Fwechat?ref=badge_large) diff --git a/vendor/overtrue/wechat/composer.json b/vendor/overtrue/wechat/composer.json new file mode 100644 index 0000000..ad21285 --- /dev/null +++ b/vendor/overtrue/wechat/composer.json @@ -0,0 +1,62 @@ +{ + "name": "overtrue/wechat", + "description": "微信SDK", + "keywords": [ + "easywechat", + "wechat", + "weixin", + "weixin-sdk", + "sdk" + ], + "license": "MIT", + "authors": [ + { + "name": "overtrue", + "email": "anzhengchao@gmail.com" + } + ], + "require": { + "php": ">=7.2", + "ext-fileinfo": "*", + "ext-openssl": "*", + "ext-simplexml": "*", + "easywechat-composer/easywechat-composer": "^1.1", + "guzzlehttp/guzzle": "^6.2 || ^7.0", + "monolog/monolog": "^1.22 || ^2.0", + "overtrue/socialite": "~2.0", + "pimple/pimple": "^3.0", + "psr/simple-cache": "^1.0", + "symfony/cache": "^3.3 || ^4.3 || ^5.0", + "symfony/event-dispatcher": "^4.3 || ^5.0", + "symfony/http-foundation": "^2.7 || ^3.0 || ^4.0 || ^5.0", + "symfony/psr-http-message-bridge": "^0.3 || ^1.0 || ^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.15", + "mikey179/vfsstream": "^1.6", + "mockery/mockery": "^1.2.3", + "phpstan/phpstan": "^0.12.0", + "phpunit/phpunit": "^7.5" + }, + "autoload": { + "psr-4": { + "EasyWeChat\\": "src/" + }, + "files": [ + "src/Kernel/Support/Helpers.php", + "src/Kernel/Helpers.php" + ] + }, + "autoload-dev": { + "psr-4": { + "EasyWeChat\\Tests\\": "tests/" + } + }, + "scripts": { + "phpcs": "vendor/bin/php-cs-fixer fix", + "phpstan": "vendor/bin/phpstan analyse", + "check-style": "php-cs-fixer fix --using-cache=no --diff --config=.php_cs --dry-run --ansi", + "fix-style": "php-cs-fixer fix --using-cache=no --config=.php_cs --ansi", + "test": "vendor/bin/phpunit --colors=always" + } +} diff --git a/vendor/overtrue/wechat/src/BasicService/Application.php b/vendor/overtrue/wechat/src/BasicService/Application.php new file mode 100644 index 0000000..e4b1e8a --- /dev/null +++ b/vendor/overtrue/wechat/src/BasicService/Application.php @@ -0,0 +1,39 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\BasicService; + +use EasyWeChat\Kernel\ServiceContainer; + +/** + * Class Application. + * + * @author overtrue + * + * @property \EasyWeChat\BasicService\Jssdk\Client $jssdk + * @property \EasyWeChat\BasicService\Media\Client $media + * @property \EasyWeChat\BasicService\QrCode\Client $qrcode + * @property \EasyWeChat\BasicService\Url\Client $url + * @property \EasyWeChat\BasicService\ContentSecurity\Client $content_security + */ +class Application extends ServiceContainer +{ + /** + * @var array + */ + protected $providers = [ + Jssdk\ServiceProvider::class, + QrCode\ServiceProvider::class, + Media\ServiceProvider::class, + Url\ServiceProvider::class, + ContentSecurity\ServiceProvider::class, + ]; +} diff --git a/vendor/overtrue/wechat/src/BasicService/ContentSecurity/Client.php b/vendor/overtrue/wechat/src/BasicService/ContentSecurity/Client.php new file mode 100644 index 0000000..9f4b142 --- /dev/null +++ b/vendor/overtrue/wechat/src/BasicService/ContentSecurity/Client.php @@ -0,0 +1,107 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\BasicService\ContentSecurity; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; + +/** + * Class Client. + * + * @author tianyong90 <412039588@qq.com> + */ +class Client extends BaseClient +{ + /** + * Text content security check. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function checkText(string $text) + { + $params = [ + 'content' => $text, + ]; + + return $this->httpPostJson('wxa/msg_sec_check', $params); + } + + /** + * Image security check. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function checkImage(string $path) + { + return $this->httpUpload('wxa/img_sec_check', ['media' => $path]); + } + + /** + * Media security check. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function checkMediaAsync(string $mediaUrl, int $mediaType) + { + /* + * 1:音频;2:图片 + */ + $mediaTypes = [1, 2]; + + if (!in_array($mediaType, $mediaTypes, true)) { + throw new InvalidArgumentException('media type must be 1 or 2'); + } + + $params = [ + 'media_url' => $mediaUrl, + 'media_type' => $mediaType, + ]; + + return $this->httpPostJson('wxa/media_check_async', $params); + } + + /** + * Image security check async. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function checkImageAsync(string $mediaUrl) + { + return $this->checkMediaAsync($mediaUrl, 2); + } + + /** + * Audio security check async. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function checkAudioAsync(string $mediaUrl) + { + return $this->checkMediaAsync($mediaUrl, 1); + } +} diff --git a/vendor/overtrue/wechat/src/BasicService/ContentSecurity/ServiceProvider.php b/vendor/overtrue/wechat/src/BasicService/ContentSecurity/ServiceProvider.php new file mode 100644 index 0000000..3645de9 --- /dev/null +++ b/vendor/overtrue/wechat/src/BasicService/ContentSecurity/ServiceProvider.php @@ -0,0 +1,31 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\BasicService\ContentSecurity; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['content_security'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/BasicService/Jssdk/Client.php b/vendor/overtrue/wechat/src/BasicService/Jssdk/Client.php new file mode 100644 index 0000000..f6dbb5e --- /dev/null +++ b/vendor/overtrue/wechat/src/BasicService/Jssdk/Client.php @@ -0,0 +1,198 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\BasicService\Jssdk; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Exceptions\RuntimeException; +use EasyWeChat\Kernel\Support; +use EasyWeChat\Kernel\Traits\InteractsWithCache; + +/** + * Class Client. + * + * @author overtrue + */ +class Client extends BaseClient +{ + use InteractsWithCache; + + /** + * @var string + */ + protected $ticketEndpoint = 'cgi-bin/ticket/getticket'; + + /** + * Current URI. + * + * @var string + */ + protected $url; + + /** + * Get config json for jsapi. + * + * @param array $jsApiList + * @param bool $debug + * @param bool $beta + * @param bool $json + * @param array $openTagList + * @param string $url + * + * @return array|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function buildConfig(array $jsApiList, bool $debug = false, bool $beta = false, bool $json = true, array $openTagList = [], string $url = null) + { + $config = array_merge(compact('debug', 'beta', 'jsApiList', 'openTagList'), $this->configSignature($url)); + + return $json ? json_encode($config) : $config; + } + + /** + * Return jsapi config as a PHP array. + * + * @param array $apis + * @param bool $debug + * @param bool $beta + * @param array $openTagList + * @param string $url + * + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function getConfigArray(array $apis, bool $debug = false, bool $beta = false, array $openTagList = [], string $url = null) + { + return $this->buildConfig($apis, $debug, $beta, false, $openTagList, $url); + } + + /** + * Get js ticket. + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function getTicket(bool $refresh = false, string $type = 'jsapi'): array + { + $cacheKey = sprintf('easywechat.basic_service.jssdk.ticket.%s.%s', $type, $this->getAppId()); + + if (!$refresh && $this->getCache()->has($cacheKey)) { + return $this->getCache()->get($cacheKey); + } + + /** @var array $result */ + $result = $this->castResponseToType( + $this->requestRaw($this->ticketEndpoint, 'GET', ['query' => ['type' => $type]]), + 'array' + ); + + $this->getCache()->set($cacheKey, $result, $result['expires_in'] - 500); + + if (!$this->getCache()->has($cacheKey)) { + throw new RuntimeException('Failed to cache jssdk ticket.'); + } + + return $result; + } + + /** + * Build signature. + * + * @param int|null $timestamp + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + protected function configSignature(string $url = null, string $nonce = null, $timestamp = null): array + { + $url = $url ?: $this->getUrl(); + $nonce = $nonce ?: Support\Str::quickRandom(10); + $timestamp = $timestamp ?: time(); + + return [ + 'appId' => $this->getAppId(), + 'nonceStr' => $nonce, + 'timestamp' => $timestamp, + 'url' => $url, + 'signature' => $this->getTicketSignature($this->getTicket()['ticket'], $nonce, $timestamp, $url), + ]; + } + + /** + * Sign the params. + * + * @param string $ticket + * @param string $nonce + * @param int $timestamp + * @param string $url + */ + public function getTicketSignature($ticket, $nonce, $timestamp, $url): string + { + return sha1(sprintf('jsapi_ticket=%s&noncestr=%s×tamp=%s&url=%s', $ticket, $nonce, $timestamp, $url)); + } + + /** + * @return string + */ + public function dictionaryOrderSignature() + { + $params = func_get_args(); + + sort($params, SORT_STRING); + + return sha1(implode('', $params)); + } + + /** + * Set current url. + * + * @return $this + */ + public function setUrl(string $url) + { + $this->url = $url; + + return $this; + } + + /** + * Get current url. + */ + public function getUrl(): string + { + if ($this->url) { + return $this->url; + } + + return Support\current_url(); + } + + /** + * @return string + */ + protected function getAppId() + { + return $this->app['config']->get('app_id'); + } +} diff --git a/vendor/overtrue/wechat/src/BasicService/Jssdk/ServiceProvider.php b/vendor/overtrue/wechat/src/BasicService/Jssdk/ServiceProvider.php new file mode 100644 index 0000000..5581a1e --- /dev/null +++ b/vendor/overtrue/wechat/src/BasicService/Jssdk/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\BasicService\Jssdk; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['jssdk'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/BasicService/Media/Client.php b/vendor/overtrue/wechat/src/BasicService/Media/Client.php new file mode 100644 index 0000000..70654ba --- /dev/null +++ b/vendor/overtrue/wechat/src/BasicService/Media/Client.php @@ -0,0 +1,192 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\BasicService\Media; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\Http\StreamResponse; + +/** + * Class Client. + * + * @author overtrue + */ +class Client extends BaseClient +{ + /** + * Allow media type. + * + * @var array + */ + protected $allowTypes = ['image', 'voice', 'video', 'thumb']; + + /** + * Upload image. + * + * @param string $path + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function uploadImage($path) + { + return $this->upload('image', $path); + } + + /** + * Upload video. + * + * @param string $path + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function uploadVideo($path) + { + return $this->upload('video', $path); + } + + /** + * @param string $path + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function uploadVoice($path) + { + return $this->upload('voice', $path); + } + + /** + * @param string $path + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function uploadThumb($path) + { + return $this->upload('thumb', $path); + } + + /** + * Upload temporary material. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function upload(string $type, string $path) + { + if (!file_exists($path) || !is_readable($path)) { + throw new InvalidArgumentException(sprintf("File does not exist, or the file is unreadable: '%s'", $path)); + } + + if (!in_array($type, $this->allowTypes, true)) { + throw new InvalidArgumentException(sprintf("Unsupported media type: '%s'", $type)); + } + + return $this->httpUpload('cgi-bin/media/upload', ['media' => $path], ['type' => $type]); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function uploadVideoForBroadcasting(string $path, string $title, string $description) + { + $response = $this->uploadVideo($path); + /** @var array $arrayResponse */ + $arrayResponse = $this->detectAndCastResponseToType($response, 'array'); + + if (!empty($arrayResponse['media_id'])) { + return $this->createVideoForBroadcasting($arrayResponse['media_id'], $title, $description); + } + + return $response; + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function createVideoForBroadcasting(string $mediaId, string $title, string $description) + { + return $this->httpPostJson('cgi-bin/media/uploadvideo', [ + 'media_id' => $mediaId, + 'title' => $title, + 'description' => $description, + ]); + } + + /** + * Fetch item from WeChat server. + * + * @return \EasyWeChat\Kernel\Http\StreamResponse|\Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(string $mediaId) + { + $response = $this->requestRaw('cgi-bin/media/get', 'GET', [ + 'query' => [ + 'media_id' => $mediaId, + ], + ]); + + if (false !== stripos($response->getHeaderLine('Content-disposition'), 'attachment')) { + return StreamResponse::buildFromPsrResponse($response); + } + + return $this->castResponseToType($response, $this->app['config']->get('response_type')); + } + + /** + * @return array|\EasyWeChat\Kernel\Http\Response|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getJssdkMedia(string $mediaId) + { + $response = $this->requestRaw('cgi-bin/media/get/jssdk', 'GET', [ + 'query' => [ + 'media_id' => $mediaId, + ], + ]); + + if (false !== stripos($response->getHeaderLine('Content-disposition'), 'attachment')) { + return StreamResponse::buildFromPsrResponse($response); + } + + return $this->castResponseToType($response, $this->app['config']->get('response_type')); + } +} diff --git a/vendor/overtrue/wechat/src/BasicService/Media/ServiceProvider.php b/vendor/overtrue/wechat/src/BasicService/Media/ServiceProvider.php new file mode 100644 index 0000000..45de142 --- /dev/null +++ b/vendor/overtrue/wechat/src/BasicService/Media/ServiceProvider.php @@ -0,0 +1,44 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * ServiceProvider.php. + * + * This file is part of the wechat. + * + * (c) overtrue + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\BasicService\Media; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['media'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/BasicService/QrCode/Client.php b/vendor/overtrue/wechat/src/BasicService/QrCode/Client.php new file mode 100644 index 0000000..92caeeb --- /dev/null +++ b/vendor/overtrue/wechat/src/BasicService/QrCode/Client.php @@ -0,0 +1,115 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\BasicService\QrCode; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author overtrue + */ +class Client extends BaseClient +{ + public const DAY = 86400; + public const SCENE_MAX_VALUE = 100000; + public const SCENE_QR_CARD = 'QR_CARD'; + public const SCENE_QR_TEMPORARY = 'QR_SCENE'; + public const SCENE_QR_TEMPORARY_STR = 'QR_STR_SCENE'; + public const SCENE_QR_FOREVER = 'QR_LIMIT_SCENE'; + public const SCENE_QR_FOREVER_STR = 'QR_LIMIT_STR_SCENE'; + + /** + * Create forever QR code. + * + * @param string|int $sceneValue + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + */ + public function forever($sceneValue) + { + if (is_int($sceneValue) && $sceneValue > 0 && $sceneValue < self::SCENE_MAX_VALUE) { + $type = self::SCENE_QR_FOREVER; + $sceneKey = 'scene_id'; + } else { + $type = self::SCENE_QR_FOREVER_STR; + $sceneKey = 'scene_str'; + } + $scene = [$sceneKey => $sceneValue]; + + return $this->create($type, $scene, false); + } + + /** + * Create temporary QR code. + * + * @param string|int $sceneValue + * @param int|null $expireSeconds + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + */ + public function temporary($sceneValue, $expireSeconds = null) + { + if (is_int($sceneValue) && $sceneValue > 0) { + $type = self::SCENE_QR_TEMPORARY; + $sceneKey = 'scene_id'; + } else { + $type = self::SCENE_QR_TEMPORARY_STR; + $sceneKey = 'scene_str'; + } + $scene = [$sceneKey => $sceneValue]; + + return $this->create($type, $scene, true, $expireSeconds); + } + + /** + * Return url for ticket. + * Detail: https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1443433542 . + * + * @param string $ticket + * + * @return string + */ + public function url($ticket) + { + return sprintf('https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=%s', urlencode($ticket)); + } + + /** + * Create a QrCode. + * + * @param string $actionName + * @param array $actionInfo + * @param bool $temporary + * @param int $expireSeconds + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function create($actionName, $actionInfo, $temporary = true, $expireSeconds = null) + { + null !== $expireSeconds || $expireSeconds = 7 * self::DAY; + + $params = [ + 'action_name' => $actionName, + 'action_info' => ['scene' => $actionInfo], + ]; + + if ($temporary) { + $params['expire_seconds'] = min($expireSeconds, 30 * self::DAY); + } + + return $this->httpPostJson('cgi-bin/qrcode/create', $params); + } +} diff --git a/vendor/overtrue/wechat/src/BasicService/QrCode/ServiceProvider.php b/vendor/overtrue/wechat/src/BasicService/QrCode/ServiceProvider.php new file mode 100644 index 0000000..1638c66 --- /dev/null +++ b/vendor/overtrue/wechat/src/BasicService/QrCode/ServiceProvider.php @@ -0,0 +1,31 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\BasicService\QrCode; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['qrcode'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/BasicService/Url/Client.php b/vendor/overtrue/wechat/src/BasicService/Url/Client.php new file mode 100644 index 0000000..b4b0a4c --- /dev/null +++ b/vendor/overtrue/wechat/src/BasicService/Url/Client.php @@ -0,0 +1,40 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\BasicService\Url; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author overtrue + */ +class Client extends BaseClient +{ + /** + * Shorten the url. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function shorten(string $url) + { + $params = [ + 'action' => 'long2short', + 'long_url' => $url, + ]; + + return $this->httpPostJson('cgi-bin/shorturl', $params); + } +} diff --git a/vendor/overtrue/wechat/src/BasicService/Url/ServiceProvider.php b/vendor/overtrue/wechat/src/BasicService/Url/ServiceProvider.php new file mode 100644 index 0000000..6740bb8 --- /dev/null +++ b/vendor/overtrue/wechat/src/BasicService/Url/ServiceProvider.php @@ -0,0 +1,31 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\BasicService\Url; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['url'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Factory.php b/vendor/overtrue/wechat/src/Factory.php new file mode 100644 index 0000000..a004b05 --- /dev/null +++ b/vendor/overtrue/wechat/src/Factory.php @@ -0,0 +1,53 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat; + +/** + * Class Factory. + * + * @method static \EasyWeChat\Payment\Application payment(array $config) + * @method static \EasyWeChat\MiniProgram\Application miniProgram(array $config) + * @method static \EasyWeChat\OpenPlatform\Application openPlatform(array $config) + * @method static \EasyWeChat\OfficialAccount\Application officialAccount(array $config) + * @method static \EasyWeChat\BasicService\Application basicService(array $config) + * @method static \EasyWeChat\Work\Application work(array $config) + * @method static \EasyWeChat\OpenWork\Application openWork(array $config) + * @method static \EasyWeChat\MicroMerchant\Application microMerchant(array $config) + */ +class Factory +{ + /** + * @param string $name + * + * @return \EasyWeChat\Kernel\ServiceContainer + */ + public static function make($name, array $config) + { + $namespace = Kernel\Support\Str::studly($name); + $application = "\\EasyWeChat\\{$namespace}\\Application"; + + return new $application($config); + } + + /** + * Dynamically pass methods to the application. + * + * @param string $name + * @param array $arguments + * + * @return mixed + */ + public static function __callStatic($name, $arguments) + { + return self::make($name, ...$arguments); + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/AccessToken.php b/vendor/overtrue/wechat/src/Kernel/AccessToken.php new file mode 100644 index 0000000..6edc1d4 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/AccessToken.php @@ -0,0 +1,255 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel; + +use EasyWeChat\Kernel\Contracts\AccessTokenInterface; +use EasyWeChat\Kernel\Exceptions\HttpException; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\Exceptions\RuntimeException; +use EasyWeChat\Kernel\Traits\HasHttpRequests; +use EasyWeChat\Kernel\Traits\InteractsWithCache; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; + +/** + * Class AccessToken. + * + * @author overtrue + */ +abstract class AccessToken implements AccessTokenInterface +{ + use HasHttpRequests; + use InteractsWithCache; + + /** + * @var \EasyWeChat\Kernel\ServiceContainer + */ + protected $app; + + /** + * @var string + */ + protected $requestMethod = 'GET'; + + /** + * @var string + */ + protected $endpointToGetToken; + + /** + * @var string + */ + protected $queryName; + + /** + * @var array + */ + protected $token; + + /** + * @var string + */ + protected $tokenKey = 'access_token'; + + /** + * @var string + */ + protected $cachePrefix = 'easywechat.kernel.access_token.'; + + /** + * AccessToken constructor. + * + * @param \EasyWeChat\Kernel\ServiceContainer $app + */ + public function __construct(ServiceContainer $app) + { + $this->app = $app; + } + + public function getLastToken(): array + { + return $this->token; + } + + /** + * @throws \EasyWeChat\Kernel\Exceptions\HttpException + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function getRefreshedToken(): array + { + return $this->getToken(true); + } + + /** + * @throws \EasyWeChat\Kernel\Exceptions\HttpException + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function getToken(bool $refresh = false): array + { + $cacheKey = $this->getCacheKey(); + $cache = $this->getCache(); + + if (!$refresh && $cache->has($cacheKey) && $result = $cache->get($cacheKey)) { + return $result; + } + + /** @var array $token */ + $token = $this->requestToken($this->getCredentials(), true); + + $this->setToken($token[$this->tokenKey], $token['expires_in'] ?? 7200); + + $this->token = $token; + + $this->app->events->dispatch(new Events\AccessTokenRefreshed($this)); + + return $token; + } + + /** + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function setToken(string $token, int $lifetime = 7200): AccessTokenInterface + { + $this->getCache()->set($this->getCacheKey(), [ + $this->tokenKey => $token, + 'expires_in' => $lifetime, + ], $lifetime); + + if (!$this->getCache()->has($this->getCacheKey())) { + throw new RuntimeException('Failed to cache access token.'); + } + + return $this; + } + + /** + * @throws \EasyWeChat\Kernel\Exceptions\HttpException + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function refresh(): AccessTokenInterface + { + $this->getToken(true); + + return $this; + } + + /** + * @param bool $toArray + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\HttpException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function requestToken(array $credentials, $toArray = false) + { + $response = $this->sendRequest($credentials); + $result = json_decode($response->getBody()->getContents(), true); + $formatted = $this->castResponseToType($response, $this->app['config']->get('response_type')); + + if (empty($result[$this->tokenKey])) { + throw new HttpException('Request access_token fail: '.json_encode($result, JSON_UNESCAPED_UNICODE), $response, $formatted); + } + + return $toArray ? $result : $formatted; + } + + /** + * @throws \EasyWeChat\Kernel\Exceptions\HttpException + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function applyToRequest(RequestInterface $request, array $requestOptions = []): RequestInterface + { + parse_str($request->getUri()->getQuery(), $query); + + $query = http_build_query(array_merge($this->getQuery(), $query)); + + return $request->withUri($request->getUri()->withQuery($query)); + } + + /** + * Send http request. + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function sendRequest(array $credentials): ResponseInterface + { + $options = [ + ('GET' === $this->requestMethod) ? 'query' : 'json' => $credentials, + ]; + + return $this->setHttpClient($this->app['http_client'])->request($this->getEndpoint(), $this->requestMethod, $options); + } + + /** + * @return string + */ + protected function getCacheKey() + { + return $this->cachePrefix.md5(json_encode($this->getCredentials())); + } + + /** + * The request query will be used to add to the request. + * + * @throws \EasyWeChat\Kernel\Exceptions\HttpException + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + protected function getQuery(): array + { + return [$this->queryName ?? $this->tokenKey => $this->getToken()[$this->tokenKey]]; + } + + /** + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function getEndpoint(): string + { + if (empty($this->endpointToGetToken)) { + throw new InvalidArgumentException('No endpoint for access token request.'); + } + + return $this->endpointToGetToken; + } + + /** + * @return string + */ + public function getTokenKey() + { + return $this->tokenKey; + } + + /** + * Credential for get token. + */ + abstract protected function getCredentials(): array; +} diff --git a/vendor/overtrue/wechat/src/Kernel/BaseClient.php b/vendor/overtrue/wechat/src/Kernel/BaseClient.php new file mode 100644 index 0000000..389a0f9 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/BaseClient.php @@ -0,0 +1,243 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel; + +use EasyWeChat\Kernel\Contracts\AccessTokenInterface; +use EasyWeChat\Kernel\Http\Response; +use EasyWeChat\Kernel\Traits\HasHttpRequests; +use GuzzleHttp\MessageFormatter; +use GuzzleHttp\Middleware; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Log\LogLevel; + +/** + * Class BaseClient. + * + * @author overtrue + */ +class BaseClient +{ + use HasHttpRequests { request as performRequest; } + + /** + * @var \EasyWeChat\Kernel\ServiceContainer + */ + protected $app; + + /** + * @var \EasyWeChat\Kernel\Contracts\AccessTokenInterface + */ + protected $accessToken; + + /** + * @var string + */ + protected $baseUri; + + /** + * BaseClient constructor. + * + * @param \EasyWeChat\Kernel\ServiceContainer $app + */ + public function __construct(ServiceContainer $app, AccessTokenInterface $accessToken = null) + { + $this->app = $app; + $this->accessToken = $accessToken ?? $this->app['access_token']; + } + + /** + * GET request. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function httpGet(string $url, array $query = []) + { + return $this->request($url, 'GET', ['query' => $query]); + } + + /** + * POST request. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function httpPost(string $url, array $data = []) + { + return $this->request($url, 'POST', ['form_params' => $data]); + } + + /** + * JSON request. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function httpPostJson(string $url, array $data = [], array $query = []) + { + return $this->request($url, 'POST', ['query' => $query, 'json' => $data]); + } + + /** + * Upload file. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function httpUpload(string $url, array $files = [], array $form = [], array $query = []) + { + $multipart = []; + + foreach ($files as $name => $path) { + $multipart[] = [ + 'name' => $name, + 'contents' => fopen($path, 'r'), + ]; + } + + foreach ($form as $name => $contents) { + $multipart[] = compact('name', 'contents'); + } + + return $this->request($url, 'POST', ['query' => $query, 'multipart' => $multipart, 'connect_timeout' => 30, 'timeout' => 30, 'read_timeout' => 30]); + } + + public function getAccessToken(): AccessTokenInterface + { + return $this->accessToken; + } + + /** + * @return $this + */ + public function setAccessToken(AccessTokenInterface $accessToken) + { + $this->accessToken = $accessToken; + + return $this; + } + + /** + * @param bool $returnRaw + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function request(string $url, string $method = 'GET', array $options = [], $returnRaw = false) + { + if (empty($this->middlewares)) { + $this->registerHttpMiddlewares(); + } + + $response = $this->performRequest($url, $method, $options); + + $this->app->events->dispatch(new Events\HttpResponseCreated($response)); + + return $returnRaw ? $response : $this->castResponseToType($response, $this->app->config->get('response_type')); + } + + /** + * @return \EasyWeChat\Kernel\Http\Response + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function requestRaw(string $url, string $method = 'GET', array $options = []) + { + return Response::buildFromPsrResponse($this->request($url, $method, $options, true)); + } + + /** + * Register Guzzle middlewares. + */ + protected function registerHttpMiddlewares() + { + // retry + $this->pushMiddleware($this->retryMiddleware(), 'retry'); + // access token + $this->pushMiddleware($this->accessTokenMiddleware(), 'access_token'); + // log + $this->pushMiddleware($this->logMiddleware(), 'log'); + } + + /** + * Attache access token to request query. + * + * @return \Closure + */ + protected function accessTokenMiddleware() + { + return function (callable $handler) { + return function (RequestInterface $request, array $options) use ($handler) { + if ($this->accessToken) { + $request = $this->accessToken->applyToRequest($request, $options); + } + + return $handler($request, $options); + }; + }; + } + + /** + * Log the request. + * + * @return \Closure + */ + protected function logMiddleware() + { + $formatter = new MessageFormatter($this->app['config']['http.log_template'] ?? MessageFormatter::DEBUG); + + return Middleware::log($this->app['logger'], $formatter, LogLevel::DEBUG); + } + + /** + * Return retry middleware. + * + * @return \Closure + */ + protected function retryMiddleware() + { + return Middleware::retry(function ( + $retries, + RequestInterface $request, + ResponseInterface $response = null + ) { + // Limit the number of retries to 2 + if ($retries < $this->app->config->get('http.max_retries', 1) && $response && $body = $response->getBody()) { + // Retry on server errors + $response = json_decode($body, true); + + if (!empty($response['errcode']) && in_array(abs($response['errcode']), [40001, 40014, 42001], true)) { + $this->accessToken->refresh(); + $this->app['logger']->debug('Retrying with refreshed access token.'); + + return true; + } + } + + return false; + }, function () { + return abs($this->app->config->get('http.retry_delay', 500)); + }); + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Clauses/Clause.php b/vendor/overtrue/wechat/src/Kernel/Clauses/Clause.php new file mode 100644 index 0000000..6991e39 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Clauses/Clause.php @@ -0,0 +1,73 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Clauses; + +/** + * Class Clause. + * + * @author mingyoung + */ +class Clause +{ + /** + * @var array + */ + protected $clauses = [ + 'where' => [], + ]; + + /** + * @param mixed ...$args + * + * @return $this + */ + public function where(...$args) + { + array_push($this->clauses['where'], $args); + + return $this; + } + + /** + * @param mixed $payload + * + * @return bool + */ + public function intercepted($payload) + { + return (bool) $this->interceptWhereClause($payload); + } + + /** + * @param mixed $payload + * + * @return bool + */ + protected function interceptWhereClause($payload) + { + foreach ($this->clauses['where'] as $item) { + list($key, $value) = $item; + if (!isset($payload[$key])) { + continue; + } + + if (is_array($value) && !in_array($payload[$key], $value)) { + return true; + } + + if (!is_array($value) && $payload[$key] !== $value) { + return true; + } + } + return false; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Config.php b/vendor/overtrue/wechat/src/Kernel/Config.php new file mode 100644 index 0000000..081f6fd --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Config.php @@ -0,0 +1,23 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel; + +use EasyWeChat\Kernel\Support\Collection; + +/** + * Class Config. + * + * @author overtrue + */ +class Config extends Collection +{ +} diff --git a/vendor/overtrue/wechat/src/Kernel/Contracts/AccessTokenInterface.php b/vendor/overtrue/wechat/src/Kernel/Contracts/AccessTokenInterface.php new file mode 100644 index 0000000..69268ff --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Contracts/AccessTokenInterface.php @@ -0,0 +1,31 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Contracts; + +use Psr\Http\Message\RequestInterface; + +/** + * Interface AuthorizerAccessToken. + * + * @author overtrue + */ +interface AccessTokenInterface +{ + public function getToken(): array; + + /** + * @return \EasyWeChat\Kernel\Contracts\AccessTokenInterface + */ + public function refresh(): self; + + public function applyToRequest(RequestInterface $request, array $requestOptions = []): RequestInterface; +} diff --git a/vendor/overtrue/wechat/src/Kernel/Contracts/Arrayable.php b/vendor/overtrue/wechat/src/Kernel/Contracts/Arrayable.php new file mode 100644 index 0000000..d947f8f --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Contracts/Arrayable.php @@ -0,0 +1,29 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Contracts; + +use ArrayAccess; + +/** + * Interface Arrayable. + * + * @author overtrue + */ +interface Arrayable extends ArrayAccess +{ + /** + * Get the instance as an array. + * + * @return array + */ + public function toArray(); +} diff --git a/vendor/overtrue/wechat/src/Kernel/Contracts/EventHandlerInterface.php b/vendor/overtrue/wechat/src/Kernel/Contracts/EventHandlerInterface.php new file mode 100644 index 0000000..c9d116c --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Contracts/EventHandlerInterface.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Contracts; + +/** + * Interface EventHandlerInterface. + * + * @author mingyoung + */ +interface EventHandlerInterface +{ + /** + * @param mixed $payload + */ + public function handle($payload = null); +} diff --git a/vendor/overtrue/wechat/src/Kernel/Contracts/MediaInterface.php b/vendor/overtrue/wechat/src/Kernel/Contracts/MediaInterface.php new file mode 100644 index 0000000..a8ded09 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Contracts/MediaInterface.php @@ -0,0 +1,22 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Contracts; + +/** + * Interface MediaInterface. + * + * @author overtrue + */ +interface MediaInterface extends MessageInterface +{ + public function getMediaId(): string; +} diff --git a/vendor/overtrue/wechat/src/Kernel/Contracts/MessageInterface.php b/vendor/overtrue/wechat/src/Kernel/Contracts/MessageInterface.php new file mode 100644 index 0000000..32aec4a --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Contracts/MessageInterface.php @@ -0,0 +1,26 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Contracts; + +/** + * Interface MessageInterface. + * + * @author overtrue + */ +interface MessageInterface +{ + public function getType(): string; + + public function transformForJsonRequest(): array; + + public function transformToXml(): string; +} diff --git a/vendor/overtrue/wechat/src/Kernel/Decorators/FinallyResult.php b/vendor/overtrue/wechat/src/Kernel/Decorators/FinallyResult.php new file mode 100644 index 0000000..e698bbf --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Decorators/FinallyResult.php @@ -0,0 +1,35 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Decorators; + +/** + * Class FinallyResult. + * + * @author overtrue + */ +class FinallyResult +{ + /** + * @var mixed + */ + public $content; + + /** + * FinallyResult constructor. + * + * @param mixed $content + */ + public function __construct($content) + { + $this->content = $content; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Decorators/TerminateResult.php b/vendor/overtrue/wechat/src/Kernel/Decorators/TerminateResult.php new file mode 100644 index 0000000..cf1042d --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Decorators/TerminateResult.php @@ -0,0 +1,35 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Decorators; + +/** + * Class TerminateResult. + * + * @author overtrue + */ +class TerminateResult +{ + /** + * @var mixed + */ + public $content; + + /** + * FinallyResult constructor. + * + * @param mixed $content + */ + public function __construct($content) + { + $this->content = $content; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Encryptor.php b/vendor/overtrue/wechat/src/Kernel/Encryptor.php new file mode 100644 index 0000000..bebb2a5 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Encryptor.php @@ -0,0 +1,198 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel; + +use EasyWeChat\Kernel\Exceptions\RuntimeException; +use EasyWeChat\Kernel\Support\AES; +use EasyWeChat\Kernel\Support\XML; +use Throwable; +use function EasyWeChat\Kernel\Support\str_random; + +/** + * Class Encryptor. + * + * @author overtrue + */ +class Encryptor +{ + public const ERROR_INVALID_SIGNATURE = -40001; // Signature verification failed + public const ERROR_PARSE_XML = -40002; // Parse XML failed + public const ERROR_CALC_SIGNATURE = -40003; // Calculating the signature failed + public const ERROR_INVALID_AES_KEY = -40004; // Invalid AESKey + public const ERROR_INVALID_APP_ID = -40005; // Check AppID failed + public const ERROR_ENCRYPT_AES = -40006; // AES EncryptionInterface failed + public const ERROR_DECRYPT_AES = -40007; // AES decryption failed + public const ERROR_INVALID_XML = -40008; // Invalid XML + public const ERROR_BASE64_ENCODE = -40009; // Base64 encoding failed + public const ERROR_BASE64_DECODE = -40010; // Base64 decoding failed + public const ERROR_XML_BUILD = -40011; // XML build failed + public const ILLEGAL_BUFFER = -41003; // Illegal buffer + + /** + * App id. + * + * @var string + */ + protected $appId; + + /** + * App token. + * + * @var string + */ + protected $token; + + /** + * @var string + */ + protected $aesKey; + + /** + * Block size. + * + * @var int + */ + protected $blockSize = 32; + + /** + * Constructor. + */ + public function __construct(string $appId, string $token = null, string $aesKey = null) + { + $this->appId = $appId; + $this->token = $token; + $this->aesKey = base64_decode($aesKey.'=', true); + } + + /** + * Get the app token. + */ + public function getToken(): string + { + return $this->token; + } + + /** + * Encrypt the message and return XML. + * + * @param string $xml + * @param string $nonce + * @param int $timestamp + * + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function encrypt($xml, $nonce = null, $timestamp = null): string + { + try { + $xml = $this->pkcs7Pad(str_random(16).pack('N', strlen($xml)).$xml.$this->appId, $this->blockSize); + + $encrypted = base64_encode(AES::encrypt( + $xml, + $this->aesKey, + substr($this->aesKey, 0, 16), + OPENSSL_NO_PADDING + )); + // @codeCoverageIgnoreStart + } catch (Throwable $e) { + throw new RuntimeException($e->getMessage(), self::ERROR_ENCRYPT_AES); + } + // @codeCoverageIgnoreEnd + + !is_null($nonce) || $nonce = substr($this->appId, 0, 10); + !is_null($timestamp) || $timestamp = time(); + + $response = [ + 'Encrypt' => $encrypted, + 'MsgSignature' => $this->signature($this->token, $timestamp, $nonce, $encrypted), + 'TimeStamp' => $timestamp, + 'Nonce' => $nonce, + ]; + + //生成响应xml + return XML::build($response); + } + + /** + * Decrypt message. + * + * @param string $content + * @param string $msgSignature + * @param string $nonce + * @param string $timestamp + * + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function decrypt($content, $msgSignature, $nonce, $timestamp): string + { + $signature = $this->signature($this->token, $timestamp, $nonce, $content); + + if ($signature !== $msgSignature) { + throw new RuntimeException('Invalid Signature.', self::ERROR_INVALID_SIGNATURE); + } + + $decrypted = AES::decrypt( + base64_decode($content, true), + $this->aesKey, + substr($this->aesKey, 0, 16), + OPENSSL_NO_PADDING + ); + $result = $this->pkcs7Unpad($decrypted); + $content = substr($result, 16, strlen($result)); + $contentLen = unpack('N', substr($content, 0, 4))[1]; + + if (trim(substr($content, $contentLen + 4)) !== $this->appId) { + throw new RuntimeException('Invalid appId.', self::ERROR_INVALID_APP_ID); + } + + return substr($content, 4, $contentLen); + } + + /** + * Get SHA1. + */ + public function signature(): string + { + $array = func_get_args(); + sort($array, SORT_STRING); + + return sha1(implode($array)); + } + + /** + * PKCS#7 pad. + * + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function pkcs7Pad(string $text, int $blockSize): string + { + if ($blockSize > 256) { + throw new RuntimeException('$blockSize may not be more than 256'); + } + $padding = $blockSize - (strlen($text) % $blockSize); + $pattern = chr($padding); + + return $text.str_repeat($pattern, $padding); + } + + /** + * PKCS#7 unpad. + */ + public function pkcs7Unpad(string $text): string + { + $pad = ord(substr($text, -1)); + if ($pad < 1 || $pad > $this->blockSize) { + $pad = 0; + } + + return substr($text, 0, (strlen($text) - $pad)); + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Events/AccessTokenRefreshed.php b/vendor/overtrue/wechat/src/Kernel/Events/AccessTokenRefreshed.php new file mode 100644 index 0000000..6fd0aca --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Events/AccessTokenRefreshed.php @@ -0,0 +1,32 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Events; + +use EasyWeChat\Kernel\AccessToken; + +/** + * Class AccessTokenRefreshed. + * + * @author mingyoung + */ +class AccessTokenRefreshed +{ + /** + * @var \EasyWeChat\Kernel\AccessToken + */ + public $accessToken; + + public function __construct(AccessToken $accessToken) + { + $this->accessToken = $accessToken; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Events/ApplicationInitialized.php b/vendor/overtrue/wechat/src/Kernel/Events/ApplicationInitialized.php new file mode 100644 index 0000000..c83df65 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Events/ApplicationInitialized.php @@ -0,0 +1,32 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Events; + +use EasyWeChat\Kernel\ServiceContainer; + +/** + * Class ApplicationInitialized. + * + * @author mingyoung + */ +class ApplicationInitialized +{ + /** + * @var \EasyWeChat\Kernel\ServiceContainer + */ + public $app; + + public function __construct(ServiceContainer $app) + { + $this->app = $app; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Events/HttpResponseCreated.php b/vendor/overtrue/wechat/src/Kernel/Events/HttpResponseCreated.php new file mode 100644 index 0000000..809de20 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Events/HttpResponseCreated.php @@ -0,0 +1,32 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Events; + +use Psr\Http\Message\ResponseInterface; + +/** + * Class HttpResponseCreated. + * + * @author mingyoung + */ +class HttpResponseCreated +{ + /** + * @var \Psr\Http\Message\ResponseInterface + */ + public $response; + + public function __construct(ResponseInterface $response) + { + $this->response = $response; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Events/ServerGuardResponseCreated.php b/vendor/overtrue/wechat/src/Kernel/Events/ServerGuardResponseCreated.php new file mode 100644 index 0000000..46e221d --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Events/ServerGuardResponseCreated.php @@ -0,0 +1,32 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Events; + +use Symfony\Component\HttpFoundation\Response; + +/** + * Class ServerGuardResponseCreated. + * + * @author mingyoung + */ +class ServerGuardResponseCreated +{ + /** + * @var \Symfony\Component\HttpFoundation\Response + */ + public $response; + + public function __construct(Response $response) + { + $this->response = $response; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Exceptions/BadRequestException.php b/vendor/overtrue/wechat/src/Kernel/Exceptions/BadRequestException.php new file mode 100644 index 0000000..a0b4f91 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Exceptions/BadRequestException.php @@ -0,0 +1,21 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Exceptions; + +/** + * Class BadRequestException. + * + * @author overtrue + */ +class BadRequestException extends Exception +{ +} diff --git a/vendor/overtrue/wechat/src/Kernel/Exceptions/DecryptException.php b/vendor/overtrue/wechat/src/Kernel/Exceptions/DecryptException.php new file mode 100644 index 0000000..f29acca --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Exceptions/DecryptException.php @@ -0,0 +1,16 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Exceptions; + +class DecryptException extends Exception +{ +} diff --git a/vendor/overtrue/wechat/src/Kernel/Exceptions/Exception.php b/vendor/overtrue/wechat/src/Kernel/Exceptions/Exception.php new file mode 100644 index 0000000..9eba298 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Exceptions/Exception.php @@ -0,0 +1,23 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Exceptions; + +use Exception as BaseException; + +/** + * Class Exception. + * + * @author overtrue + */ +class Exception extends BaseException +{ +} diff --git a/vendor/overtrue/wechat/src/Kernel/Exceptions/HttpException.php b/vendor/overtrue/wechat/src/Kernel/Exceptions/HttpException.php new file mode 100644 index 0000000..5f64064 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Exceptions/HttpException.php @@ -0,0 +1,51 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Exceptions; + +use Psr\Http\Message\ResponseInterface; + +/** + * Class HttpException. + * + * @author overtrue + */ +class HttpException extends Exception +{ + /** + * @var \Psr\Http\Message\ResponseInterface|null + */ + public $response; + + /** + * @var \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string|null + */ + public $formattedResponse; + + /** + * HttpException constructor. + * + * @param string $message + * @param null $formattedResponse + * @param int|null $code + */ + public function __construct($message, ResponseInterface $response = null, $formattedResponse = null, $code = null) + { + parent::__construct($message, $code); + + $this->response = $response; + $this->formattedResponse = $formattedResponse; + + if ($response) { + $response->getBody()->rewind(); + } + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Exceptions/InvalidArgumentException.php b/vendor/overtrue/wechat/src/Kernel/Exceptions/InvalidArgumentException.php new file mode 100644 index 0000000..386a144 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Exceptions/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Exceptions; + +/** + * Class InvalidArgumentException. + * + * @author overtrue + */ +class InvalidArgumentException extends Exception +{ +} diff --git a/vendor/overtrue/wechat/src/Kernel/Exceptions/InvalidConfigException.php b/vendor/overtrue/wechat/src/Kernel/Exceptions/InvalidConfigException.php new file mode 100644 index 0000000..1e4ae2a --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Exceptions/InvalidConfigException.php @@ -0,0 +1,21 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Exceptions; + +/** + * Class InvalidConfigException. + * + * @author overtrue + */ +class InvalidConfigException extends Exception +{ +} diff --git a/vendor/overtrue/wechat/src/Kernel/Exceptions/RuntimeException.php b/vendor/overtrue/wechat/src/Kernel/Exceptions/RuntimeException.php new file mode 100644 index 0000000..25f2f5b --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Exceptions/RuntimeException.php @@ -0,0 +1,21 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Exceptions; + +/** + * Class RuntimeException. + * + * @author overtrue + */ +class RuntimeException extends Exception +{ +} diff --git a/vendor/overtrue/wechat/src/Kernel/Exceptions/UnboundServiceException.php b/vendor/overtrue/wechat/src/Kernel/Exceptions/UnboundServiceException.php new file mode 100644 index 0000000..78ea85c --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Exceptions/UnboundServiceException.php @@ -0,0 +1,21 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Exceptions; + +/** + * Class InvalidConfigException. + * + * @author overtrue + */ +class UnboundServiceException extends Exception +{ +} diff --git a/vendor/overtrue/wechat/src/Kernel/Helpers.php b/vendor/overtrue/wechat/src/Kernel/Helpers.php new file mode 100644 index 0000000..3ea48a4 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Helpers.php @@ -0,0 +1,57 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel; + +use EasyWeChat\Kernel\Contracts\Arrayable; +use EasyWeChat\Kernel\Exceptions\RuntimeException; +use EasyWeChat\Kernel\Support\Arr; +use EasyWeChat\Kernel\Support\Collection; + +function data_get($data, $key, $default = null) +{ + switch (true) { + case is_array($data): + return Arr::get($data, $key, $default); + case $data instanceof Collection: + return $data->get($key, $default); + case $data instanceof Arrayable: + return Arr::get($data->toArray(), $key, $default); + case $data instanceof \ArrayIterator: + return $data->getArrayCopy()[$key] ?? $default; + case $data instanceof \ArrayAccess: + return $data[$key] ?? $default; + case $data instanceof \IteratorAggregate && $data->getIterator() instanceof \ArrayIterator: + return $data->getIterator()->getArrayCopy()[$key] ?? $default; + case is_object($data): + return $data->{$key} ?? $default; + default: + throw new RuntimeException(sprintf('Can\'t access data with key "%s"', $key)); + } +} + +function data_to_array($data) +{ + switch (true) { + case is_array($data): + return $data; + case $data instanceof Collection: + return $data->all(); + case $data instanceof Arrayable: + return $data->toArray(); + case $data instanceof \IteratorAggregate && $data->getIterator() instanceof \ArrayIterator: + return $data->getIterator()->getArrayCopy(); + case $data instanceof \ArrayIterator: + return $data->getArrayCopy(); + default: + throw new RuntimeException(sprintf('Can\'t transform data to array')); + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Http/Response.php b/vendor/overtrue/wechat/src/Kernel/Http/Response.php new file mode 100644 index 0000000..4ff2f76 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Http/Response.php @@ -0,0 +1,117 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Http; + +use EasyWeChat\Kernel\Support\Collection; +use EasyWeChat\Kernel\Support\XML; +use GuzzleHttp\Psr7\Response as GuzzleResponse; +use Psr\Http\Message\ResponseInterface; + +/** + * Class Response. + * + * @author overtrue + */ +class Response extends GuzzleResponse +{ + /** + * @return string + */ + public function getBodyContents() + { + $this->getBody()->rewind(); + $contents = $this->getBody()->getContents(); + $this->getBody()->rewind(); + + return $contents; + } + + /** + * @return \EasyWeChat\Kernel\Http\Response + */ + public static function buildFromPsrResponse(ResponseInterface $response) + { + return new static( + $response->getStatusCode(), + $response->getHeaders(), + $response->getBody(), + $response->getProtocolVersion(), + $response->getReasonPhrase() + ); + } + + /** + * Build to json. + * + * @return string + */ + public function toJson() + { + return json_encode($this->toArray()); + } + + /** + * Build to array. + * + * @return array + */ + public function toArray() + { + $content = $this->removeControlCharacters($this->getBodyContents()); + + if (false !== stripos($this->getHeaderLine('Content-Type'), 'xml') || 0 === stripos($content, 'toArray()); + } + + /** + * @return object + */ + public function toObject() + { + return json_decode($this->toJson()); + } + + /** + * @return bool|string + */ + public function __toString() + { + return $this->getBodyContents(); + } + + /** + * @return string + */ + protected function removeControlCharacters(string $content) + { + return \preg_replace('/[\x00-\x1F\x80-\x9F]/u', '', $content); + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Http/StreamResponse.php b/vendor/overtrue/wechat/src/Kernel/Http/StreamResponse.php new file mode 100644 index 0000000..91c1346 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Http/StreamResponse.php @@ -0,0 +1,78 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Http; + +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\Exceptions\RuntimeException; +use EasyWeChat\Kernel\Support\File; + +/** + * Class StreamResponse. + * + * @author overtrue + */ +class StreamResponse extends Response +{ + /** + * @return bool|int + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function save(string $directory, string $filename = '', bool $appendSuffix = true) + { + $this->getBody()->rewind(); + + $directory = rtrim($directory, '/'); + + if (!is_dir($directory)) { + mkdir($directory, 0755, true); // @codeCoverageIgnore + } + + if (!is_writable($directory)) { + throw new InvalidArgumentException(sprintf("'%s' is not writable.", $directory)); + } + + $contents = $this->getBody()->getContents(); + + if (empty($contents) || '{' === $contents[0]) { + throw new RuntimeException('Invalid media response content.'); + } + + if (empty($filename)) { + if (preg_match('/filename="(?.*?)"/', $this->getHeaderLine('Content-Disposition'), $match)) { + $filename = $match['filename']; + } else { + $filename = md5($contents); + } + } + + if ($appendSuffix && empty(pathinfo($filename, PATHINFO_EXTENSION))) { + $filename .= File::getStreamExt($contents); + } + + file_put_contents($directory.'/'.$filename, $contents); + + return $filename; + } + + /** + * @return bool|int + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function saveAs(string $directory, string $filename, bool $appendSuffix = true) + { + return $this->save($directory, $filename, $appendSuffix); + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Log/LogManager.php b/vendor/overtrue/wechat/src/Kernel/Log/LogManager.php new file mode 100644 index 0000000..0d46e48 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Log/LogManager.php @@ -0,0 +1,580 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Log; + +use EasyWeChat\Kernel\ServiceContainer; +use InvalidArgumentException; +use Monolog\Formatter\LineFormatter; +use Monolog\Handler\ErrorLogHandler; +use Monolog\Handler\FormattableHandlerInterface; +use Monolog\Handler\HandlerInterface; +use Monolog\Handler\NullHandler; +use Monolog\Handler\RotatingFileHandler; +use Monolog\Handler\SlackWebhookHandler; +use Monolog\Handler\StreamHandler; +use Monolog\Handler\SyslogHandler; +use Monolog\Handler\WhatFailureGroupHandler; +use Monolog\Logger as Monolog; +use Psr\Log\LoggerInterface; + +/** + * Class LogManager. + * + * @author overtrue + */ +class LogManager implements LoggerInterface +{ + /** + * @var \EasyWeChat\Kernel\ServiceContainer + */ + protected $app; + + /** + * The array of resolved channels. + * + * @var array + */ + protected $channels = []; + + /** + * The registered custom driver creators. + * + * @var array + */ + protected $customCreators = []; + + /** + * The Log levels. + * + * @var array + */ + protected $levels = [ + 'debug' => Monolog::DEBUG, + 'info' => Monolog::INFO, + 'notice' => Monolog::NOTICE, + 'warning' => Monolog::WARNING, + 'error' => Monolog::ERROR, + 'critical' => Monolog::CRITICAL, + 'alert' => Monolog::ALERT, + 'emergency' => Monolog::EMERGENCY, + ]; + + /** + * LogManager constructor. + */ + public function __construct(ServiceContainer $app) + { + $this->app = $app; + } + + /** + * Create a new, on-demand aggregate logger instance. + * + * @param array $channels + * @param string|null $channel + * + * @return \Psr\Log\LoggerInterface + * + * @throws \Exception + */ + public function stack(array $channels, $channel = null) + { + return $this->createStackDriver(compact('channels', 'channel')); + } + + /** + * Get a log channel instance. + * + * @param string|null $channel + * + * @return mixed + * + * @throws \Exception + */ + public function channel($channel = null) + { + return $this->driver($channel); + } + + /** + * Get a log driver instance. + * + * @param string|null $driver + * + * @return mixed + * + * @throws \Exception + */ + public function driver($driver = null) + { + return $this->get($driver ?? $this->getDefaultDriver()); + } + + /** + * Attempt to get the log from the local cache. + * + * @param string $name + * + * @return \Psr\Log\LoggerInterface + * + * @throws \Exception + */ + protected function get($name) + { + try { + return $this->channels[$name] ?? ($this->channels[$name] = $this->resolve($name)); + } catch (\Throwable $e) { + $logger = $this->createEmergencyLogger(); + + $logger->emergency('Unable to create configured logger. Using emergency logger.', [ + 'exception' => $e, + ]); + + return $logger; + } + } + + /** + * Resolve the given log instance by name. + * + * @param string $name + * + * @return \Psr\Log\LoggerInterface + * + * @throws InvalidArgumentException + */ + protected function resolve($name) + { + $config = $this->app['config']->get(\sprintf('log.channels.%s', $name)); + + if (is_null($config)) { + throw new InvalidArgumentException(\sprintf('Log [%s] is not defined.', $name)); + } + + if (isset($this->customCreators[$config['driver']])) { + return $this->callCustomCreator($config); + } + + $driverMethod = 'create'.ucfirst($config['driver']).'Driver'; + + if (method_exists($this, $driverMethod)) { + return $this->{$driverMethod}($config); + } + + throw new InvalidArgumentException(\sprintf('Driver [%s] is not supported.', $config['driver'])); + } + + /** + * Call a custom driver creator. + * + * @return mixed + */ + protected function callCustomCreator(array $config) + { + return $this->customCreators[$config['driver']]($this->app, $config); + } + + /** + * Create an emergency log handler to avoid white screens of death. + * + * @return \Monolog\Logger + * + * @throws \Exception + */ + protected function createEmergencyLogger() + { + return new Monolog('EasyWeChat', $this->prepareHandlers([new StreamHandler( + \sys_get_temp_dir().'/easywechat/easywechat.log', + $this->level(['level' => 'debug']) + )])); + } + + /** + * Create an aggregate log driver instance. + * + * @return \Monolog\Logger + * + * @throws \Exception + */ + protected function createStackDriver(array $config) + { + $handlers = []; + + foreach ($config['channels'] ?? [] as $channel) { + $handlers = \array_merge($handlers, $this->channel($channel)->getHandlers()); + } + + if ($config['ignore_exceptions'] ?? false) { + $handlers = [new WhatFailureGroupHandler($handlers)]; + } + + return new Monolog($this->parseChannel($config), $handlers); + } + + /** + * Create an instance of the single file log driver. + * + * @return \Psr\Log\LoggerInterface + * + * @throws \Exception + */ + protected function createSingleDriver(array $config) + { + return new Monolog($this->parseChannel($config), [ + $this->prepareHandler(new StreamHandler( + $config['path'], + $this->level($config), + $config['bubble'] ?? true, + $config['permission'] ?? null, + $config['locking'] ?? false + ), $config), + ]); + } + + /** + * Create an instance of the daily file log driver. + * + * @return \Psr\Log\LoggerInterface + */ + protected function createDailyDriver(array $config) + { + return new Monolog($this->parseChannel($config), [ + $this->prepareHandler(new RotatingFileHandler( + $config['path'], + $config['days'] ?? 7, + $this->level($config), + $config['bubble'] ?? true, + $config['permission'] ?? null, + $config['locking'] ?? false + ), $config), + ]); + } + + /** + * Create an instance of the Slack log driver. + * + * @return \Psr\Log\LoggerInterface + */ + protected function createSlackDriver(array $config) + { + return new Monolog($this->parseChannel($config), [ + $this->prepareHandler(new SlackWebhookHandler( + $config['url'], + $config['channel'] ?? null, + $config['username'] ?? 'EasyWeChat', + $config['attachment'] ?? true, + $config['emoji'] ?? ':boom:', + $config['short'] ?? false, + $config['context'] ?? true, + $this->level($config), + $config['bubble'] ?? true, + $config['exclude_fields'] ?? [] + ), $config), + ]); + } + + /** + * Create an instance of the syslog log driver. + * + * @return \Psr\Log\LoggerInterface + */ + protected function createSyslogDriver(array $config) + { + return new Monolog($this->parseChannel($config), [ + $this->prepareHandler(new SyslogHandler( + 'EasyWeChat', + $config['facility'] ?? LOG_USER, + $this->level($config) + ), $config), + ]); + } + + /** + * Create an instance of the "error log" log driver. + * + * @return \Psr\Log\LoggerInterface + */ + protected function createErrorlogDriver(array $config) + { + return new Monolog($this->parseChannel($config), [ + $this->prepareHandler( + new ErrorLogHandler( + $config['type'] ?? ErrorLogHandler::OPERATING_SYSTEM, + $this->level($config) + ) + ), + ]); + } + + protected function createNullDriver() + { + return new Monolog('EasyWeChat', [new NullHandler()]); + } + + /** + * Prepare the handlers for usage by Monolog. + * + * @return array + */ + protected function prepareHandlers(array $handlers) + { + foreach ($handlers as $key => $handler) { + $handlers[$key] = $this->prepareHandler($handler); + } + + return $handlers; + } + + /** + * Prepare the handler for usage by Monolog. + * + * @return \Monolog\Handler\HandlerInterface + */ + protected function prepareHandler(HandlerInterface $handler, array $config = []) + { + if (!isset($config['formatter'])) { + if ($handler instanceof FormattableHandlerInterface) { + $handler->setFormatter($this->formatter()); + } + } + + return $handler; + } + + /** + * Get a Monolog formatter instance. + * + * @return \Monolog\Formatter\FormatterInterface + */ + protected function formatter() + { + $formatter = new LineFormatter(null, null, true, true); + $formatter->includeStacktraces(); + + return $formatter; + } + + /** + * Extract the log channel from the given configuration. + * + * @return string + */ + protected function parseChannel(array $config) + { + return $config['name'] ?? 'EasyWeChat'; + } + + /** + * Parse the string level into a Monolog constant. + * + * @return int + * + * @throws InvalidArgumentException + */ + protected function level(array $config) + { + $level = $config['level'] ?? 'debug'; + + if (isset($this->levels[$level])) { + return $this->levels[$level]; + } + + throw new InvalidArgumentException('Invalid log level.'); + } + + /** + * Get the default log driver name. + * + * @return string + */ + public function getDefaultDriver() + { + return $this->app['config']['log.default']; + } + + /** + * Set the default log driver name. + * + * @param string $name + */ + public function setDefaultDriver($name) + { + $this->app['config']['log.default'] = $name; + } + + /** + * Register a custom driver creator Closure. + * + * @param string $driver + * + * @return $this + */ + public function extend($driver, \Closure $callback) + { + $this->customCreators[$driver] = $callback->bindTo($this, $this); + + return $this; + } + + /** + * System is unusable. + * + * @param string $message + * + * @return mixed + * + * @throws \Exception + */ + public function emergency($message, array $context = []) + { + return $this->driver()->emergency($message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * + * @return mixed + * + * @throws \Exception + */ + public function alert($message, array $context = []) + { + return $this->driver()->alert($message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * + * @return mixed + * + * @throws \Exception + */ + public function critical($message, array $context = []) + { + return $this->driver()->critical($message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * + * @return mixed + * + * @throws \Exception + */ + public function error($message, array $context = []) + { + return $this->driver()->error($message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * + * @return mixed + * + * @throws \Exception + */ + public function warning($message, array $context = []) + { + return $this->driver()->warning($message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * + * @return mixed + * + * @throws \Exception + */ + public function notice($message, array $context = []) + { + return $this->driver()->notice($message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * + * @return mixed + * + * @throws \Exception + */ + public function info($message, array $context = []) + { + return $this->driver()->info($message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * + * @return mixed + * + * @throws \Exception + */ + public function debug($message, array $context = []) + { + return $this->driver()->debug($message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * + * @return mixed + * + * @throws \Exception + */ + public function log($level, $message, array $context = []) + { + return $this->driver()->log($level, $message, $context); + } + + /** + * Dynamically call the default driver instance. + * + * @param string $method + * @param array $parameters + * + * @return mixed + * + * @throws \Exception + */ + public function __call($method, $parameters) + { + return $this->driver()->$method(...$parameters); + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/Article.php b/vendor/overtrue/wechat/src/Kernel/Messages/Article.php new file mode 100644 index 0000000..4792a1f --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/Article.php @@ -0,0 +1,58 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class Article. + */ +class Article extends Message +{ + /** + * @var string + */ + protected $type = 'mpnews'; + + /** + * Properties. + * + * @var array + */ + protected $properties = [ + 'thumb_media_id', + 'author', + 'title', + 'content', + 'digest', + 'source_url', + 'show_cover', + ]; + + /** + * Aliases of attribute. + * + * @var array + */ + protected $jsonAliases = [ + 'content_source_url' => 'source_url', + 'show_cover_pic' => 'show_cover', + ]; + + /** + * @var array + */ + protected $required = [ + 'thumb_media_id', + 'title', + 'content', + 'show_cover', + ]; +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/Card.php b/vendor/overtrue/wechat/src/Kernel/Messages/Card.php new file mode 100644 index 0000000..d5b2590 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/Card.php @@ -0,0 +1,50 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Card.php. + * + * @author overtrue + * @copyright 2015 overtrue + * + * @see https://github.com/overtrue + * @see http://overtrue.me + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class Card. + */ +class Card extends Message +{ + /** + * Message type. + * + * @var string + */ + protected $type = 'wxcard'; + + /** + * Properties. + * + * @var array + */ + protected $properties = ['card_id']; + + /** + * Media constructor. + */ + public function __construct(string $cardId) + { + parent::__construct(['card_id' => $cardId]); + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/DeviceEvent.php b/vendor/overtrue/wechat/src/Kernel/Messages/DeviceEvent.php new file mode 100644 index 0000000..ae57910 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/DeviceEvent.php @@ -0,0 +1,40 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class DeviceEvent. + * + * @property string $media_id + */ +class DeviceEvent extends Message +{ + /** + * Messages type. + * + * @var string + */ + protected $type = 'device_event'; + + /** + * Properties. + * + * @var array + */ + protected $properties = [ + 'device_type', + 'device_id', + 'content', + 'session_id', + 'open_id', + ]; +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/DeviceText.php b/vendor/overtrue/wechat/src/Kernel/Messages/DeviceText.php new file mode 100644 index 0000000..87e627a --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/DeviceText.php @@ -0,0 +1,50 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class DeviceText. + * + * @property string $content + */ +class DeviceText extends Message +{ + /** + * Messages type. + * + * @var string + */ + protected $type = 'device_text'; + + /** + * Properties. + * + * @var array + */ + protected $properties = [ + 'device_type', + 'device_id', + 'content', + 'session_id', + 'open_id', + ]; + + public function toXmlArray() + { + return [ + 'DeviceType' => $this->get('device_type'), + 'DeviceID' => $this->get('device_id'), + 'SessionID' => $this->get('session_id'), + 'Content' => base64_encode($this->get('content')), + ]; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/File.php b/vendor/overtrue/wechat/src/Kernel/Messages/File.php new file mode 100644 index 0000000..a14c42f --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/File.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class Image. + * + * @property string $media_id + */ +class File extends Media +{ + /** + * @var string + */ + protected $type = 'file'; +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/Image.php b/vendor/overtrue/wechat/src/Kernel/Messages/Image.php new file mode 100644 index 0000000..982033b --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/Image.php @@ -0,0 +1,27 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class Image. + * + * @property string $media_id + */ +class Image extends Media +{ + /** + * Messages type. + * + * @var string + */ + protected $type = 'image'; +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/Link.php b/vendor/overtrue/wechat/src/Kernel/Messages/Link.php new file mode 100644 index 0000000..9c126b5 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/Link.php @@ -0,0 +1,36 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class Link. + */ +class Link extends Message +{ + /** + * Messages type. + * + * @var string + */ + protected $type = 'link'; + + /** + * Properties. + * + * @var array + */ + protected $properties = [ + 'title', + 'description', + 'url', + ]; +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/Location.php b/vendor/overtrue/wechat/src/Kernel/Messages/Location.php new file mode 100644 index 0000000..f10351b --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/Location.php @@ -0,0 +1,38 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class Location. + */ +class Location extends Message +{ + /** + * Messages type. + * + * @var string + */ + protected $type = 'location'; + + /** + * Properties. + * + * @var array + */ + protected $properties = [ + 'latitude', + 'longitude', + 'scale', + 'label', + 'precision', + ]; +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/Media.php b/vendor/overtrue/wechat/src/Kernel/Messages/Media.php new file mode 100644 index 0000000..ab44477 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/Media.php @@ -0,0 +1,66 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +use EasyWeChat\Kernel\Contracts\MediaInterface; +use EasyWeChat\Kernel\Support\Str; + +/** + * Class Media. + */ +class Media extends Message implements MediaInterface +{ + /** + * Properties. + * + * @var array + */ + protected $properties = ['media_id']; + + /** + * @var array + */ + protected $required = [ + 'media_id', + ]; + + /** + * MaterialClient constructor. + * + * @param string $type + */ + public function __construct(string $mediaId, $type = null, array $attributes = []) + { + parent::__construct(array_merge(['media_id' => $mediaId], $attributes)); + + !empty($type) && $this->setType($type); + } + + /** + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function getMediaId(): string + { + $this->checkRequiredAttributes(); + + return $this->get('media_id'); + } + + public function toXmlArray() + { + return [ + Str::studly($this->getType()) => [ + 'MediaId' => $this->get('media_id'), + ], + ]; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/Message.php b/vendor/overtrue/wechat/src/Kernel/Messages/Message.php new file mode 100644 index 0000000..2c9d72f --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/Message.php @@ -0,0 +1,187 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +use EasyWeChat\Kernel\Contracts\MessageInterface; +use EasyWeChat\Kernel\Exceptions\RuntimeException; +use EasyWeChat\Kernel\Support\XML; +use EasyWeChat\Kernel\Traits\HasAttributes; + +/** + * Class Messages. + */ +abstract class Message implements MessageInterface +{ + use HasAttributes; + + public const TEXT = 2; + public const IMAGE = 4; + public const VOICE = 8; + public const VIDEO = 16; + public const SHORT_VIDEO = 32; + public const LOCATION = 64; + public const LINK = 128; + public const DEVICE_EVENT = 256; + public const DEVICE_TEXT = 512; + public const FILE = 1024; + public const TEXT_CARD = 2048; + public const TRANSFER = 4096; + public const EVENT = 1048576; + public const MINIPROGRAM_PAGE = 2097152; + public const MINIPROGRAM_NOTICE = 4194304; + public const ALL = self::TEXT | self::IMAGE | self::VOICE | self::VIDEO | self::SHORT_VIDEO | self::LOCATION | self::LINK + | self::DEVICE_EVENT | self::DEVICE_TEXT | self::FILE | self::TEXT_CARD | self::TRANSFER | self::EVENT + | self::MINIPROGRAM_PAGE | self::MINIPROGRAM_NOTICE; + + /** + * @var string + */ + protected $type; + + /** + * @var int + */ + protected $id; + + /** + * @var string + */ + protected $to; + + /** + * @var string + */ + protected $from; + + /** + * @var array + */ + protected $properties = []; + + /** + * @var array + */ + protected $jsonAliases = []; + + /** + * Message constructor. + */ + public function __construct(array $attributes = []) + { + $this->setAttributes($attributes); + } + + /** + * Return type name message. + */ + public function getType(): string + { + return $this->type; + } + + public function setType(string $type) + { + $this->type = $type; + } + + /** + * Magic getter. + * + * @param string $property + * + * @return mixed + */ + public function __get($property) + { + if (property_exists($this, $property)) { + return $this->$property; + } + + return $this->getAttribute($property); + } + + /** + * Magic setter. + * + * @param string $property + * @param mixed $value + * + * @return Message + */ + public function __set($property, $value) + { + if (property_exists($this, $property)) { + $this->$property = $value; + } else { + $this->setAttribute($property, $value); + } + + return $this; + } + + /** + * @return array + */ + public function transformForJsonRequestWithoutType(array $appends = []) + { + return $this->transformForJsonRequest($appends, false); + } + + /** + * @param bool $withType + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function transformForJsonRequest(array $appends = [], $withType = true): array + { + if (!$withType) { + return $this->propertiesToArray([], $this->jsonAliases); + } + $messageType = $this->getType(); + $data = array_merge(['msgtype' => $messageType], $appends); + + $data[$messageType] = array_merge($data[$messageType] ?? [], $this->propertiesToArray([], $this->jsonAliases)); + + return $data; + } + + public function transformToXml(array $appends = [], bool $returnAsArray = false): string + { + $data = array_merge(['MsgType' => $this->getType()], $this->toXmlArray(), $appends); + + return $returnAsArray ? $data : XML::build($data); + } + + /** + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + protected function propertiesToArray(array $data, array $aliases = []): array + { + $this->checkRequiredAttributes(); + + foreach ($this->attributes as $property => $value) { + if (is_null($value) && !$this->isRequired($property)) { + continue; + } + $alias = array_search($property, $aliases, true); + + $data[$alias ?: $property] = $this->get($property); + } + + return $data; + } + + public function toXmlArray() + { + throw new RuntimeException(sprintf('Class "%s" cannot support transform to XML message.', __CLASS__)); + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/MiniProgramPage.php b/vendor/overtrue/wechat/src/Kernel/Messages/MiniProgramPage.php new file mode 100644 index 0000000..e4973b1 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/MiniProgramPage.php @@ -0,0 +1,31 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class MiniProgramPage. + */ +class MiniProgramPage extends Message +{ + protected $type = 'miniprogrampage'; + + protected $properties = [ + 'title', + 'appid', + 'pagepath', + 'thumb_media_id', + ]; + + protected $required = [ + 'thumb_media_id', 'appid', 'pagepath', + ]; +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/MiniprogramNotice.php b/vendor/overtrue/wechat/src/Kernel/Messages/MiniprogramNotice.php new file mode 100644 index 0000000..10589c5 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/MiniprogramNotice.php @@ -0,0 +1,22 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +class MiniprogramNotice extends Message +{ + protected $type = 'miniprogram_notice'; + + protected $properties = [ + 'appid', + 'title', + ]; +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/Music.php b/vendor/overtrue/wechat/src/Kernel/Messages/Music.php new file mode 100644 index 0000000..85feb76 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/Music.php @@ -0,0 +1,73 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class Music. + * + * @property string $url + * @property string $hq_url + * @property string $title + * @property string $description + * @property string $thumb_media_id + * @property string $format + */ +class Music extends Message +{ + /** + * Messages type. + * + * @var string + */ + protected $type = 'music'; + + /** + * Properties. + * + * @var array + */ + protected $properties = [ + 'title', + 'description', + 'url', + 'hq_url', + 'thumb_media_id', + 'format', + ]; + + /** + * Aliases of attribute. + * + * @var array + */ + protected $jsonAliases = [ + 'musicurl' => 'url', + 'hqmusicurl' => 'hq_url', + ]; + + public function toXmlArray() + { + $music = [ + 'Music' => [ + 'Title' => $this->get('title'), + 'Description' => $this->get('description'), + 'MusicUrl' => $this->get('url'), + 'HQMusicUrl' => $this->get('hq_url'), + ], + ]; + if ($thumbMediaId = $this->get('thumb_media_id')) { + $music['Music']['ThumbMediaId'] = $thumbMediaId; + } + + return $music; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/News.php b/vendor/overtrue/wechat/src/Kernel/Messages/News.php new file mode 100644 index 0000000..33703d2 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/News.php @@ -0,0 +1,65 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class News. + * + * @author overtrue + */ +class News extends Message +{ + /** + * @var string + */ + protected $type = 'news'; + + /** + * @var array + */ + protected $properties = [ + 'items', + ]; + + /** + * News constructor. + */ + public function __construct(array $items = []) + { + parent::__construct(compact('items')); + } + + public function propertiesToArray(array $data, array $aliases = []): array + { + return ['articles' => array_map(function ($item) { + if ($item instanceof NewsItem) { + return $item->toJsonArray(); + } + }, $this->get('items'))]; + } + + public function toXmlArray() + { + $items = []; + + foreach ($this->get('items') as $item) { + if ($item instanceof NewsItem) { + $items[] = $item->toXmlArray(); + } + } + + return [ + 'ArticleCount' => count($items), + 'Articles' => $items, + ]; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/NewsItem.php b/vendor/overtrue/wechat/src/Kernel/Messages/NewsItem.php new file mode 100644 index 0000000..50bf336 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/NewsItem.php @@ -0,0 +1,57 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class NewsItem. + */ +class NewsItem extends Message +{ + /** + * Messages type. + * + * @var string + */ + protected $type = 'news'; + + /** + * Properties. + * + * @var array + */ + protected $properties = [ + 'title', + 'description', + 'url', + 'image', + ]; + + public function toJsonArray() + { + return [ + 'title' => $this->get('title'), + 'description' => $this->get('description'), + 'url' => $this->get('url'), + 'picurl' => $this->get('image'), + ]; + } + + public function toXmlArray() + { + return [ + 'Title' => $this->get('title'), + 'Description' => $this->get('description'), + 'Url' => $this->get('url'), + 'PicUrl' => $this->get('image'), + ]; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/Raw.php b/vendor/overtrue/wechat/src/Kernel/Messages/Raw.php new file mode 100644 index 0000000..a3dd609 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/Raw.php @@ -0,0 +1,51 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class Raw. + */ +class Raw extends Message +{ + /** + * @var string + */ + protected $type = 'raw'; + + /** + * Properties. + * + * @var array + */ + protected $properties = ['content']; + + /** + * Constructor. + */ + public function __construct(string $content) + { + parent::__construct(['content' => strval($content)]); + } + + /** + * @param bool $withType + */ + public function transformForJsonRequest(array $appends = [], $withType = true): array + { + return json_decode($this->content, true) ?? []; + } + + public function __toString() + { + return $this->get('content') ?? ''; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/ShortVideo.php b/vendor/overtrue/wechat/src/Kernel/Messages/ShortVideo.php new file mode 100644 index 0000000..1dc1db9 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/ShortVideo.php @@ -0,0 +1,30 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class ShortVideo. + * + * @property string $title + * @property string $media_id + * @property string $description + * @property string $thumb_media_id + */ +class ShortVideo extends Video +{ + /** + * Messages type. + * + * @var string + */ + protected $type = 'shortvideo'; +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/TaskCard.php b/vendor/overtrue/wechat/src/Kernel/Messages/TaskCard.php new file mode 100644 index 0000000..d02c411 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/TaskCard.php @@ -0,0 +1,44 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class TaskCard. + * + * @property string $title + * @property string $description + * @property string $url + * @property string $task_id + * @property array $btn + */ +class TaskCard extends Message +{ + /** + * Messages type. + * + * @var string + */ + protected $type = 'taskcard'; + + /** + * Properties. + * + * @var array + */ + protected $properties = [ + 'title', + 'description', + 'url', + 'task_id', + 'btn', + ]; +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/Text.php b/vendor/overtrue/wechat/src/Kernel/Messages/Text.php new file mode 100644 index 0000000..7ce9df5 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/Text.php @@ -0,0 +1,52 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class Text. + * + * @property string $content + */ +class Text extends Message +{ + /** + * Message type. + * + * @var string + */ + protected $type = 'text'; + + /** + * Properties. + * + * @var array + */ + protected $properties = ['content']; + + /** + * Text constructor. + */ + public function __construct(string $content) + { + parent::__construct(compact('content')); + } + + /** + * @return array + */ + public function toXmlArray() + { + return [ + 'Content' => $this->get('content'), + ]; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/TextCard.php b/vendor/overtrue/wechat/src/Kernel/Messages/TextCard.php new file mode 100644 index 0000000..edfb7c5 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/TextCard.php @@ -0,0 +1,40 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class Text. + * + * @property string $title + * @property string $description + * @property string $url + */ +class TextCard extends Message +{ + /** + * Messages type. + * + * @var string + */ + protected $type = 'textcard'; + + /** + * Properties. + * + * @var array + */ + protected $properties = [ + 'title', + 'description', + 'url', + ]; +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/Transfer.php b/vendor/overtrue/wechat/src/Kernel/Messages/Transfer.php new file mode 100644 index 0000000..fe7918b --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/Transfer.php @@ -0,0 +1,54 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class Transfer. + * + * @property string $to + * @property string $account + */ +class Transfer extends Message +{ + /** + * Messages type. + * + * @var string + */ + protected $type = 'transfer_customer_service'; + + /** + * Properties. + * + * @var array + */ + protected $properties = [ + 'account', + ]; + + /** + * Transfer constructor. + */ + public function __construct(string $account = null) + { + parent::__construct(compact('account')); + } + + public function toXmlArray() + { + return empty($this->get('account')) ? [] : [ + 'TransInfo' => [ + 'KfAccount' => $this->get('account'), + ], + ]; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/Video.php b/vendor/overtrue/wechat/src/Kernel/Messages/Video.php new file mode 100644 index 0000000..3ae4860 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/Video.php @@ -0,0 +1,62 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class Video. + * + * @property string $video + * @property string $title + * @property string $media_id + * @property string $description + * @property string $thumb_media_id + */ +class Video extends Media +{ + /** + * Messages type. + * + * @var string + */ + protected $type = 'video'; + + /** + * Properties. + * + * @var array + */ + protected $properties = [ + 'title', + 'description', + 'media_id', + 'thumb_media_id', + ]; + + /** + * Video constructor. + */ + public function __construct(string $mediaId, array $attributes = []) + { + parent::__construct($mediaId, 'video', $attributes); + } + + public function toXmlArray() + { + return [ + 'Video' => [ + 'MediaId' => $this->get('media_id'), + 'Title' => $this->get('title'), + 'Description' => $this->get('description'), + ], + ]; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Messages/Voice.php b/vendor/overtrue/wechat/src/Kernel/Messages/Voice.php new file mode 100644 index 0000000..ff723ea --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Messages/Voice.php @@ -0,0 +1,37 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Messages; + +/** + * Class Voice. + * + * @property string $media_id + */ +class Voice extends Media +{ + /** + * Messages type. + * + * @var string + */ + protected $type = 'voice'; + + /** + * Properties. + * + * @var array + */ + protected $properties = [ + 'media_id', + 'recognition', + ]; +} diff --git a/vendor/overtrue/wechat/src/Kernel/Providers/ConfigServiceProvider.php b/vendor/overtrue/wechat/src/Kernel/Providers/ConfigServiceProvider.php new file mode 100644 index 0000000..24ff919 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Providers/ConfigServiceProvider.php @@ -0,0 +1,39 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Providers; + +use EasyWeChat\Kernel\Config; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ConfigServiceProvider. + * + * @author overtrue + */ +class ConfigServiceProvider implements ServiceProviderInterface +{ + /** + * Registers services on the given container. + * + * This method should only be used to configure services and parameters. + * It should not get services. + * + * @param Container $pimple A container instance + */ + public function register(Container $pimple) + { + !isset($pimple['config']) && $pimple['config'] = function ($app) { + return new Config($app->getConfig()); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Providers/EventDispatcherServiceProvider.php b/vendor/overtrue/wechat/src/Kernel/Providers/EventDispatcherServiceProvider.php new file mode 100644 index 0000000..97b2b99 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Providers/EventDispatcherServiceProvider.php @@ -0,0 +1,47 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Providers; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Symfony\Component\EventDispatcher\EventDispatcher; + +/** + * Class EventDispatcherServiceProvider. + * + * @author mingyoung + */ +class EventDispatcherServiceProvider implements ServiceProviderInterface +{ + /** + * Registers services on the given container. + * + * This method should only be used to configure services and parameters. + * It should not get services. + * + * @param Container $pimple A container instance + */ + public function register(Container $pimple) + { + !isset($pimple['events']) && $pimple['events'] = function ($app) { + $dispatcher = new EventDispatcher(); + + foreach ($app->config->get('events.listen', []) as $event => $listeners) { + foreach ($listeners as $listener) { + $dispatcher->addListener($event, $listener); + } + } + + return $dispatcher; + }; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Providers/ExtensionServiceProvider.php b/vendor/overtrue/wechat/src/Kernel/Providers/ExtensionServiceProvider.php new file mode 100644 index 0000000..1d8badd --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Providers/ExtensionServiceProvider.php @@ -0,0 +1,39 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Providers; + +use EasyWeChatComposer\Extension; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ExtensionServiceProvider. + * + * @author overtrue + */ +class ExtensionServiceProvider implements ServiceProviderInterface +{ + /** + * Registers services on the given container. + * + * This method should only be used to configure services and parameters. + * It should not get services. + * + * @param Container $pimple A container instance + */ + public function register(Container $pimple) + { + !isset($pimple['extension']) && $pimple['extension'] = function ($app) { + return new Extension($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Providers/HttpClientServiceProvider.php b/vendor/overtrue/wechat/src/Kernel/Providers/HttpClientServiceProvider.php new file mode 100644 index 0000000..e21edfa --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Providers/HttpClientServiceProvider.php @@ -0,0 +1,39 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Providers; + +use GuzzleHttp\Client; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class HttpClientServiceProvider. + * + * @author overtrue + */ +class HttpClientServiceProvider implements ServiceProviderInterface +{ + /** + * Registers services on the given container. + * + * This method should only be used to configure services and parameters. + * It should not get services. + * + * @param Container $pimple A container instance + */ + public function register(Container $pimple) + { + !isset($pimple['http_client']) && $pimple['http_client'] = function ($app) { + return new Client($app['config']->get('http', [])); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Providers/LogServiceProvider.php b/vendor/overtrue/wechat/src/Kernel/Providers/LogServiceProvider.php new file mode 100644 index 0000000..fa15e6b --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Providers/LogServiceProvider.php @@ -0,0 +1,80 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Providers; + +use EasyWeChat\Kernel\Log\LogManager; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class LoggingServiceProvider. + * + * @author overtrue + */ +class LogServiceProvider implements ServiceProviderInterface +{ + /** + * Registers services on the given container. + * + * This method should only be used to configure services and parameters. + * It should not get services. + * + * @param Container $pimple A container instance + */ + public function register(Container $pimple) + { + !isset($pimple['log']) && $pimple['log'] = function ($app) { + $config = $this->formatLogConfig($app); + + if (!empty($config)) { + $app->rebind('config', $app['config']->merge($config)); + } + + return new LogManager($app); + }; + + !isset($pimple['logger']) && $pimple['logger'] = $pimple['log']; + } + + public function formatLogConfig($app) + { + if (!empty($app['config']->get('log.channels'))) { + return $app['config']->get('log'); + } + + if (empty($app['config']->get('log'))) { + return [ + 'log' => [ + 'default' => 'null', + 'channels' => [ + 'null' => [ + 'driver' => 'null', + ], + ], + ], + ]; + } + + return [ + 'log' => [ + 'default' => 'single', + 'channels' => [ + 'single' => [ + 'driver' => 'single', + 'path' => $app['config']->get('log.file') ?: \sys_get_temp_dir().'/logs/easywechat.log', + 'level' => $app['config']->get('log.level', 'debug'), + ], + ], + ], + ]; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Providers/RequestServiceProvider.php b/vendor/overtrue/wechat/src/Kernel/Providers/RequestServiceProvider.php new file mode 100644 index 0000000..d38522e --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Providers/RequestServiceProvider.php @@ -0,0 +1,39 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Providers; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * Class RequestServiceProvider. + * + * @author overtrue + */ +class RequestServiceProvider implements ServiceProviderInterface +{ + /** + * Registers services on the given container. + * + * This method should only be used to configure services and parameters. + * It should not get services. + * + * @param Container $pimple A container instance + */ + public function register(Container $pimple) + { + !isset($pimple['request']) && $pimple['request'] = function () { + return Request::createFromGlobals(); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/ServerGuard.php b/vendor/overtrue/wechat/src/Kernel/ServerGuard.php new file mode 100644 index 0000000..11d5399 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/ServerGuard.php @@ -0,0 +1,352 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel; + +use EasyWeChat\Kernel\Contracts\MessageInterface; +use EasyWeChat\Kernel\Exceptions\BadRequestException; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\Messages\Message; +use EasyWeChat\Kernel\Messages\News; +use EasyWeChat\Kernel\Messages\NewsItem; +use EasyWeChat\Kernel\Messages\Raw as RawMessage; +use EasyWeChat\Kernel\Messages\Text; +use EasyWeChat\Kernel\Support\XML; +use EasyWeChat\Kernel\Traits\Observable; +use EasyWeChat\Kernel\Traits\ResponseCastable; +use Symfony\Component\HttpFoundation\Response; + +/** + * Class ServerGuard. + * + * 1. url 里的 signature 只是将 token+nonce+timestamp 得到的签名,只是用于验证当前请求的,在公众号环境下一直有 + * 2. 企业号消息发送时是没有的,因为固定为完全模式,所以 url 里不会存在 signature, 只有 msg_signature 用于解密消息的 + * + * @author overtrue + */ +class ServerGuard +{ + use Observable; + use ResponseCastable; + + /** + * @var bool + */ + protected $alwaysValidate = false; + + /** + * Empty string. + */ + public const SUCCESS_EMPTY_RESPONSE = 'success'; + + /** + * @var array + */ + public const MESSAGE_TYPE_MAPPING = [ + 'text' => Message::TEXT, + 'image' => Message::IMAGE, + 'voice' => Message::VOICE, + 'video' => Message::VIDEO, + 'shortvideo' => Message::SHORT_VIDEO, + 'location' => Message::LOCATION, + 'link' => Message::LINK, + 'device_event' => Message::DEVICE_EVENT, + 'device_text' => Message::DEVICE_TEXT, + 'event' => Message::EVENT, + 'file' => Message::FILE, + 'miniprogrampage' => Message::MINIPROGRAM_PAGE, + ]; + + /** + * @var \EasyWeChat\Kernel\ServiceContainer + */ + protected $app; + + /** + * Constructor. + * + * @codeCoverageIgnore + * + * @param \EasyWeChat\Kernel\ServiceContainer $app + */ + public function __construct(ServiceContainer $app) + { + $this->app = $app; + + foreach ($this->app->extension->observers() as $observer) { + call_user_func_array([$this, 'push'], $observer); + } + } + + /** + * Handle and return response. + * + * @throws BadRequestException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function serve(): Response + { + $this->app['logger']->debug('Request received:', [ + 'method' => $this->app['request']->getMethod(), + 'uri' => $this->app['request']->getUri(), + 'content-type' => $this->app['request']->getContentType(), + 'content' => $this->app['request']->getContent(), + ]); + + $response = $this->validate()->resolve(); + + $this->app['logger']->debug('Server response created:', ['content' => $response->getContent()]); + + return $response; + } + + /** + * @return $this + * + * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException + */ + public function validate() + { + if (!$this->alwaysValidate && !$this->isSafeMode()) { + return $this; + } + + if ($this->app['request']->get('signature') !== $this->signature([ + $this->getToken(), + $this->app['request']->get('timestamp'), + $this->app['request']->get('nonce'), + ])) { + throw new BadRequestException('Invalid request signature.', 400); + } + + return $this; + } + + /** + * Force validate request. + * + * @return $this + */ + public function forceValidate() + { + $this->alwaysValidate = true; + + return $this; + } + + /** + * Get request message. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|string + * + * @throws BadRequestException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getMessage() + { + $message = $this->parseMessage($this->app['request']->getContent(false)); + + if (!is_array($message) || empty($message)) { + throw new BadRequestException('No message received.'); + } + + if ($this->isSafeMode() && !empty($message['Encrypt'])) { + $message = $this->decryptMessage($message); + + // Handle JSON format. + $dataSet = json_decode($message, true); + + if ($dataSet && (JSON_ERROR_NONE === json_last_error())) { + return $dataSet; + } + + $message = XML::parse($message); + } + + return $this->detectAndCastResponseToType($message, $this->app->config->get('response_type')); + } + + /** + * Resolve server request and return the response. + * + * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + protected function resolve(): Response + { + $result = $this->handleRequest(); + + if ($this->shouldReturnRawResponse()) { + $response = new Response($result['response']); + } else { + $response = new Response( + $this->buildResponse($result['to'], $result['from'], $result['response']), + 200, + ['Content-Type' => 'application/xml'] + ); + } + + $this->app->events->dispatch(new Events\ServerGuardResponseCreated($response)); + + return $response; + } + + /** + * @return string|null + */ + protected function getToken() + { + return $this->app['config']['token']; + } + + /** + * @param \EasyWeChat\Kernel\Contracts\MessageInterface|string|int $message + * + * @return string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function buildResponse(string $to, string $from, $message) + { + if (empty($message) || self::SUCCESS_EMPTY_RESPONSE === $message) { + return self::SUCCESS_EMPTY_RESPONSE; + } + + if ($message instanceof RawMessage) { + return $message->get('content', self::SUCCESS_EMPTY_RESPONSE); + } + + if (is_string($message) || is_numeric($message)) { + $message = new Text((string) $message); + } + + if (is_array($message) && reset($message) instanceof NewsItem) { + $message = new News($message); + } + + if (!($message instanceof Message)) { + throw new InvalidArgumentException(sprintf('Invalid Messages type "%s".', gettype($message))); + } + + return $this->buildReply($to, $from, $message); + } + + /** + * Handle request. + * + * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + protected function handleRequest(): array + { + $castedMessage = $this->getMessage(); + + $messageArray = $this->detectAndCastResponseToType($castedMessage, 'array'); + + $response = $this->dispatch(self::MESSAGE_TYPE_MAPPING[$messageArray['MsgType'] ?? $messageArray['msg_type'] ?? 'text'], $castedMessage); + + return [ + 'to' => $messageArray['FromUserName'] ?? '', + 'from' => $messageArray['ToUserName'] ?? '', + 'response' => $response, + ]; + } + + /** + * Build reply XML. + */ + protected function buildReply(string $to, string $from, MessageInterface $message): string + { + $prepends = [ + 'ToUserName' => $to, + 'FromUserName' => $from, + 'CreateTime' => time(), + 'MsgType' => $message->getType(), + ]; + + $response = $message->transformToXml($prepends); + + if ($this->isSafeMode()) { + $this->app['logger']->debug('Messages safe mode is enabled.'); + $response = $this->app['encryptor']->encrypt($response); + } + + return $response; + } + + /** + * @return string + */ + protected function signature(array $params) + { + sort($params, SORT_STRING); + + return sha1(implode($params)); + } + + /** + * Parse message array from raw php input. + * + * @param string $content + * + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException + */ + protected function parseMessage($content) + { + try { + if (0 === stripos($content, '<')) { + $content = XML::parse($content); + } else { + // Handle JSON format. + $dataSet = json_decode($content, true); + if ($dataSet && (JSON_ERROR_NONE === json_last_error())) { + $content = $dataSet; + } + } + + return (array) $content; + } catch (\Exception $e) { + throw new BadRequestException(sprintf('Invalid message content:(%s) %s', $e->getCode(), $e->getMessage()), $e->getCode()); + } + } + + /** + * Check the request message safe mode. + */ + protected function isSafeMode(): bool + { + return $this->app['request']->get('signature') && 'aes' === $this->app['request']->get('encrypt_type'); + } + + protected function shouldReturnRawResponse(): bool + { + return false; + } + + /** + * @return mixed + */ + protected function decryptMessage(array $message) + { + return $message = $this->app['encryptor']->decrypt( + $message['Encrypt'], + $this->app['request']->get('msg_signature'), + $this->app['request']->get('nonce'), + $this->app['request']->get('timestamp') + ); + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/ServiceContainer.php b/vendor/overtrue/wechat/src/Kernel/ServiceContainer.php new file mode 100644 index 0000000..135093c --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/ServiceContainer.php @@ -0,0 +1,160 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel; + +use EasyWeChat\Kernel\Providers\ConfigServiceProvider; +use EasyWeChat\Kernel\Providers\EventDispatcherServiceProvider; +use EasyWeChat\Kernel\Providers\ExtensionServiceProvider; +use EasyWeChat\Kernel\Providers\HttpClientServiceProvider; +use EasyWeChat\Kernel\Providers\LogServiceProvider; +use EasyWeChat\Kernel\Providers\RequestServiceProvider; +use EasyWeChatComposer\Traits\WithAggregator; +use Pimple\Container; + +/** + * Class ServiceContainer. + * + * @author overtrue + * + * @property \EasyWeChat\Kernel\Config $config + * @property \Symfony\Component\HttpFoundation\Request $request + * @property \GuzzleHttp\Client $http_client + * @property \Monolog\Logger $logger + * @property \Symfony\Component\EventDispatcher\EventDispatcher $events + */ +class ServiceContainer extends Container +{ + use WithAggregator; + + /** + * @var string + */ + protected $id; + + /** + * @var array + */ + protected $providers = []; + + /** + * @var array + */ + protected $defaultConfig = []; + + /** + * @var array + */ + protected $userConfig = []; + + /** + * Constructor. + */ + public function __construct(array $config = [], array $prepends = [], string $id = null) + { + $this->userConfig = $config; + + parent::__construct($prepends); + + $this->id = $id; + + $this->registerProviders($this->getProviders()); + + $this->aggregate(); + + $this->events->dispatch(new Events\ApplicationInitialized($this)); + } + + /** + * @return string + */ + public function getId() + { + return $this->id ?? $this->id = md5(json_encode($this->userConfig)); + } + + /** + * @return array + */ + public function getConfig() + { + $base = [ + // http://docs.guzzlephp.org/en/stable/request-options.html + 'http' => [ + 'timeout' => 30.0, + 'base_uri' => 'https://api.weixin.qq.com/', + ], + ]; + + return array_replace_recursive($base, $this->defaultConfig, $this->userConfig); + } + + /** + * Return all providers. + * + * @return array + */ + public function getProviders() + { + return array_merge([ + ConfigServiceProvider::class, + LogServiceProvider::class, + RequestServiceProvider::class, + HttpClientServiceProvider::class, + ExtensionServiceProvider::class, + EventDispatcherServiceProvider::class, + ], $this->providers); + } + + /** + * @param string $id + * @param mixed $value + */ + public function rebind($id, $value) + { + $this->offsetUnset($id); + $this->offsetSet($id, $value); + } + + /** + * Magic get access. + * + * @param string $id + * + * @return mixed + */ + public function __get($id) + { + if ($this->shouldDelegate($id)) { + return $this->delegateTo($id); + } + + return $this->offsetGet($id); + } + + /** + * Magic set access. + * + * @param string $id + * @param mixed $value + */ + public function __set($id, $value) + { + $this->offsetSet($id, $value); + } + + public function registerProviders(array $providers) + { + foreach ($providers as $provider) { + parent::register(new $provider()); + } + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Support/AES.php b/vendor/overtrue/wechat/src/Kernel/Support/AES.php new file mode 100644 index 0000000..8aa1694 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Support/AES.php @@ -0,0 +1,66 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Support; + +/** + * Class AES. + * + * @author overtrue + */ +class AES +{ + public static function encrypt(string $text, string $key, string $iv, int $option = OPENSSL_RAW_DATA): string + { + self::validateKey($key); + self::validateIv($iv); + + return openssl_encrypt($text, self::getMode($key), $key, $option, $iv); + } + + /** + * @param string|null $method + */ + public static function decrypt(string $cipherText, string $key, string $iv, int $option = OPENSSL_RAW_DATA, $method = null): string + { + self::validateKey($key); + self::validateIv($iv); + + return openssl_decrypt($cipherText, $method ?: self::getMode($key), $key, $option, $iv); + } + + /** + * @param string $key + * + * @return string + */ + public static function getMode($key) + { + return 'aes-'.(8 * strlen($key)).'-cbc'; + } + + public static function validateKey(string $key) + { + if (!in_array(strlen($key), [16, 24, 32], true)) { + throw new \InvalidArgumentException(sprintf('Key length must be 16, 24, or 32 bytes; got key len (%s).', strlen($key))); + } + } + + /** + * @throws \InvalidArgumentException + */ + public static function validateIv(string $iv) + { + if (!empty($iv) && 16 !== strlen($iv)) { + throw new \InvalidArgumentException('IV length must be 16 bytes.'); + } + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Support/Arr.php b/vendor/overtrue/wechat/src/Kernel/Support/Arr.php new file mode 100644 index 0000000..00fd654 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Support/Arr.php @@ -0,0 +1,439 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Support; + +/** + * Array helper from Illuminate\Support\Arr. + */ +class Arr +{ + /** + * Add an element to an array using "dot" notation if it doesn't exist. + * + * @param string $key + * @param mixed $value + * + * @return array + */ + public static function add(array $array, $key, $value) + { + if (is_null(static::get($array, $key))) { + static::set($array, $key, $value); + } + + return $array; + } + + /** + * Cross join the given arrays, returning all possible permutations. + * + * @param array ...$arrays + * + * @return array + */ + public static function crossJoin(...$arrays) + { + $results = [[]]; + + foreach ($arrays as $index => $array) { + $append = []; + + foreach ($results as $product) { + foreach ($array as $item) { + $product[$index] = $item; + + $append[] = $product; + } + } + + $results = $append; + } + + return $results; + } + + /** + * Divide an array into two arrays. One with keys and the other with values. + * + * @return array + */ + public static function divide(array $array) + { + return [array_keys($array), array_values($array)]; + } + + /** + * Flatten a multi-dimensional associative array with dots. + * + * @param string $prepend + * + * @return array + */ + public static function dot(array $array, $prepend = '') + { + $results = []; + + foreach ($array as $key => $value) { + if (is_array($value) && !empty($value)) { + $results = array_merge($results, static::dot($value, $prepend.$key.'.')); + } else { + $results[$prepend.$key] = $value; + } + } + + return $results; + } + + /** + * Get all of the given array except for a specified array of items. + * + * @param array|string $keys + * + * @return array + */ + public static function except(array $array, $keys) + { + static::forget($array, $keys); + + return $array; + } + + /** + * Determine if the given key exists in the provided array. + * + * @param string|int $key + * + * @return bool + */ + public static function exists(array $array, $key) + { + return array_key_exists($key, $array); + } + + /** + * Return the first element in an array passing a given truth test. + * + * @param mixed $default + * + * @return mixed + */ + public static function first(array $array, callable $callback = null, $default = null) + { + if (is_null($callback)) { + if (empty($array)) { + return $default; + } + + foreach ($array as $item) { + return $item; + } + } + + foreach ($array as $key => $value) { + if (call_user_func($callback, $value, $key)) { + return $value; + } + } + + return $default; + } + + /** + * Return the last element in an array passing a given truth test. + * + * @param mixed $default + * + * @return mixed + */ + public static function last(array $array, callable $callback = null, $default = null) + { + if (is_null($callback)) { + return empty($array) ? $default : end($array); + } + + return static::first(array_reverse($array, true), $callback, $default); + } + + /** + * Flatten a multi-dimensional array into a single level. + * + * @param int $depth + * + * @return array + */ + public static function flatten(array $array, $depth = INF) + { + return array_reduce($array, function ($result, $item) use ($depth) { + $item = $item instanceof Collection ? $item->all() : $item; + + if (!is_array($item)) { + return array_merge($result, [$item]); + } elseif (1 === $depth) { + return array_merge($result, array_values($item)); + } + + return array_merge($result, static::flatten($item, $depth - 1)); + }, []); + } + + /** + * Remove one or many array items from a given array using "dot" notation. + * + * @param array|string $keys + */ + public static function forget(array &$array, $keys) + { + $original = &$array; + + $keys = (array) $keys; + + if (0 === count($keys)) { + return; + } + + foreach ($keys as $key) { + // if the exact key exists in the top-level, remove it + if (static::exists($array, $key)) { + unset($array[$key]); + + continue; + } + + $parts = explode('.', $key); + + // clean up before each pass + $array = &$original; + + while (count($parts) > 1) { + $part = array_shift($parts); + + if (isset($array[$part]) && is_array($array[$part])) { + $array = &$array[$part]; + } else { + continue 2; + } + } + + unset($array[array_shift($parts)]); + } + } + + /** + * Get an item from an array using "dot" notation. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public static function get(array $array, $key, $default = null) + { + if (is_null($key)) { + return $array; + } + + if (static::exists($array, $key)) { + return $array[$key]; + } + + foreach (explode('.', $key) as $segment) { + if (static::exists($array, $segment)) { + $array = $array[$segment]; + } else { + return $default; + } + } + + return $array; + } + + /** + * Check if an item or items exist in an array using "dot" notation. + * + * @param string|array $keys + * + * @return bool + */ + public static function has(array $array, $keys) + { + if (is_null($keys)) { + return false; + } + + $keys = (array) $keys; + + if (empty($array)) { + return false; + } + + if ($keys === []) { + return false; + } + + foreach ($keys as $key) { + $subKeyArray = $array; + + if (static::exists($array, $key)) { + continue; + } + + foreach (explode('.', $key) as $segment) { + if (static::exists($subKeyArray, $segment)) { + $subKeyArray = $subKeyArray[$segment]; + } else { + return false; + } + } + } + + return true; + } + + /** + * Determines if an array is associative. + * + * An array is "associative" if it doesn't have sequential numerical keys beginning with zero. + * + * @return bool + */ + public static function isAssoc(array $array) + { + $keys = array_keys($array); + + return array_keys($keys) !== $keys; + } + + /** + * Get a subset of the items from the given array. + * + * @param array|string $keys + * + * @return array + */ + public static function only(array $array, $keys) + { + return array_intersect_key($array, array_flip((array) $keys)); + } + + /** + * Push an item onto the beginning of an array. + * + * @param mixed $value + * @param mixed $key + * + * @return array + */ + public static function prepend(array $array, $value, $key = null) + { + if (is_null($key)) { + array_unshift($array, $value); + } else { + $array = [$key => $value] + $array; + } + + return $array; + } + + /** + * Get a value from the array, and remove it. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public static function pull(array &$array, $key, $default = null) + { + $value = static::get($array, $key, $default); + + static::forget($array, $key); + + return $value; + } + + /** + * Get a 1 value from an array. + * + * @return mixed + * + * @throws \InvalidArgumentException + */ + public static function random(array $array, int $amount = null) + { + if (is_null($amount)) { + return $array[array_rand($array)]; + } + + $keys = array_rand($array, $amount); + + $results = []; + + foreach ((array) $keys as $key) { + $results[] = $array[$key]; + } + + return $results; + } + + /** + * Set an array item to a given value using "dot" notation. + * + * If no key is given to the method, the entire array will be replaced. + * + * @param mixed $value + * + * @return array + */ + public static function set(array &$array, string $key, $value) + { + $keys = explode('.', $key); + + while (count($keys) > 1) { + $key = array_shift($keys); + + // If the key doesn't exist at this depth, we will just create an empty array + // to hold the next value, allowing us to create the arrays to hold final + // values at the correct depth. Then we'll keep digging into the array. + if (!isset($array[$key]) || !is_array($array[$key])) { + $array[$key] = []; + } + + $array = &$array[$key]; + } + + $array[array_shift($keys)] = $value; + + return $array; + } + + /** + * Filter the array using the given callback. + * + * @return array + */ + public static function where(array $array, callable $callback) + { + return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH); + } + + /** + * If the given value is not an array, wrap it in one. + * + * @param mixed $value + * + * @return array + */ + public static function wrap($value) + { + return !is_array($value) ? [$value] : $value; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Support/ArrayAccessible.php b/vendor/overtrue/wechat/src/Kernel/Support/ArrayAccessible.php new file mode 100644 index 0000000..72e0b04 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Support/ArrayAccessible.php @@ -0,0 +1,66 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Support; + +use ArrayAccess; +use ArrayIterator; +use EasyWeChat\Kernel\Contracts\Arrayable; +use IteratorAggregate; + +/** + * Class ArrayAccessible. + * + * @author overtrue + */ +class ArrayAccessible implements ArrayAccess, IteratorAggregate, Arrayable +{ + private $array; + + public function __construct(array $array = []) + { + $this->array = $array; + } + + public function offsetExists($offset) + { + return array_key_exists($offset, $this->array); + } + + public function offsetGet($offset) + { + return $this->array[$offset]; + } + + public function offsetSet($offset, $value) + { + if (null === $offset) { + $this->array[] = $value; + } else { + $this->array[$offset] = $value; + } + } + + public function offsetUnset($offset) + { + unset($this->array[$offset]); + } + + public function getIterator() + { + return new ArrayIterator($this->array); + } + + public function toArray() + { + return $this->array; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Support/Collection.php b/vendor/overtrue/wechat/src/Kernel/Support/Collection.php new file mode 100644 index 0000000..f750e0b --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Support/Collection.php @@ -0,0 +1,417 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Support; + +use ArrayAccess; +use ArrayIterator; +use Countable; +use EasyWeChat\Kernel\Contracts\Arrayable; +use IteratorAggregate; +use JsonSerializable; +use Serializable; + +/** + * Class Collection. + */ +class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable, Serializable, Arrayable +{ + /** + * The collection data. + * + * @var array + */ + protected $items = []; + + /** + * set data. + */ + public function __construct(array $items = []) + { + foreach ($items as $key => $value) { + $this->set($key, $value); + } + } + + /** + * Return all items. + * + * @return array + */ + public function all() + { + return $this->items; + } + + /** + * Return specific items. + * + * @return \EasyWeChat\Kernel\Support\Collection + */ + public function only(array $keys) + { + $return = []; + + foreach ($keys as $key) { + $value = $this->get($key); + + if (!is_null($value)) { + $return[$key] = $value; + } + } + + return new static($return); + } + + /** + * Get all items except for those with the specified keys. + * + * @param mixed $keys + * + * @return static + */ + public function except($keys) + { + $keys = is_array($keys) ? $keys : func_get_args(); + + return new static(Arr::except($this->items, $keys)); + } + + /** + * Merge data. + * + * @param Collection|array $items + * + * @return \EasyWeChat\Kernel\Support\Collection + */ + public function merge($items) + { + $clone = new static($this->all()); + + foreach ($items as $key => $value) { + $clone->set($key, $value); + } + + return $clone; + } + + /** + * To determine Whether the specified element exists. + * + * @param string $key + * + * @return bool + */ + public function has($key) + { + return !is_null(Arr::get($this->items, $key)); + } + + /** + * Retrieve the first item. + * + * @return mixed + */ + public function first() + { + return reset($this->items); + } + + /** + * Retrieve the last item. + * + * @return bool + */ + public function last() + { + $end = end($this->items); + + reset($this->items); + + return $end; + } + + /** + * add the item value. + * + * @param string $key + * @param mixed $value + */ + public function add($key, $value) + { + Arr::set($this->items, $key, $value); + } + + /** + * Set the item value. + * + * @param string $key + * @param mixed $value + */ + public function set($key, $value) + { + Arr::set($this->items, $key, $value); + } + + /** + * Retrieve item from Collection. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function get($key, $default = null) + { + return Arr::get($this->items, $key, $default); + } + + /** + * Remove item form Collection. + * + * @param string $key + */ + public function forget($key) + { + Arr::forget($this->items, $key); + } + + /** + * Build to array. + * + * @return array + */ + public function toArray() + { + return $this->all(); + } + + /** + * Build to json. + * + * @param int $option + * + * @return string + */ + public function toJson($option = JSON_UNESCAPED_UNICODE) + { + return json_encode($this->all(), $option); + } + + /** + * To string. + * + * @return string + */ + public function __toString() + { + return $this->toJson(); + } + + /** + * (PHP 5 >= 5.4.0)
              + * Specify data which should be serialized to JSON. + * + * @see http://php.net/manual/en/jsonserializable.jsonserialize.php + * + * @return mixed data which can be serialized by json_encode, + * which is a value of any type other than a resource + */ + public function jsonSerialize() + { + return $this->items; + } + + /** + * (PHP 5 >= 5.1.0)
              + * String representation of object. + * + * @see http://php.net/manual/en/serializable.serialize.php + * + * @return string the string representation of the object or null + */ + public function serialize() + { + return serialize($this->items); + } + + /** + * (PHP 5 >= 5.0.0)
              + * Retrieve an external iterator. + * + * @see http://php.net/manual/en/iteratoraggregate.getiterator.php + * + * @return \ArrayIterator An instance of an object implementing Iterator or + * Traversable + */ + public function getIterator() + { + return new ArrayIterator($this->items); + } + + /** + * (PHP 5 >= 5.1.0)
              + * Count elements of an object. + * + * @see http://php.net/manual/en/countable.count.php + * + * @return int the custom count as an integer. + *

              + *

              + * The return value is cast to an integer + */ + public function count() + { + return count($this->items); + } + + /** + * (PHP 5 >= 5.1.0)
              + * Constructs the object. + * + * @see http://php.net/manual/en/serializable.unserialize.php + * + * @param string $serialized

              + * The string representation of the object. + *

              + * + * @return mixed|void + */ + public function unserialize($serialized) + { + return $this->items = unserialize($serialized); + } + + /** + * Get a data by key. + * + * @param string $key + * + * @return mixed + */ + public function __get($key) + { + return $this->get($key); + } + + /** + * Assigns a value to the specified data. + * + * @param string $key + * @param mixed $value + */ + public function __set($key, $value) + { + $this->set($key, $value); + } + + /** + * Whether or not an data exists by key. + * + * @param string $key + * + * @return bool + */ + public function __isset($key) + { + return $this->has($key); + } + + /** + * Unset an data by key. + * + * @param string $key + */ + public function __unset($key) + { + $this->forget($key); + } + + /** + * var_export. + * + * @return array + */ + public static function __set_state(array $properties) + { + return (new static($properties))->all(); + } + + /** + * (PHP 5 >= 5.0.0)
              + * Whether a offset exists. + * + * @see http://php.net/manual/en/arrayaccess.offsetexists.php + * + * @param mixed $offset

              + * An offset to check for. + *

              + * + * @return bool true on success or false on failure. + * The return value will be casted to boolean if non-boolean was returned + */ + public function offsetExists($offset) + { + return $this->has($offset); + } + + /** + * (PHP 5 >= 5.0.0)
              + * Offset to unset. + * + * @see http://php.net/manual/en/arrayaccess.offsetunset.php + * + * @param mixed $offset

              + * The offset to unset. + *

              + */ + public function offsetUnset($offset) + { + if ($this->offsetExists($offset)) { + $this->forget($offset); + } + } + + /** + * (PHP 5 >= 5.0.0)
              + * Offset to retrieve. + * + * @see http://php.net/manual/en/arrayaccess.offsetget.php + * + * @param mixed $offset

              + * The offset to retrieve. + *

              + * + * @return mixed Can return all value types + */ + public function offsetGet($offset) + { + return $this->offsetExists($offset) ? $this->get($offset) : null; + } + + /** + * (PHP 5 >= 5.0.0)
              + * Offset to set. + * + * @see http://php.net/manual/en/arrayaccess.offsetset.php + * + * @param mixed $offset

              + * The offset to assign the value to. + *

              + * @param mixed $value

              + * The value to set. + *

              + */ + public function offsetSet($offset, $value) + { + $this->set($offset, $value); + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Support/File.php b/vendor/overtrue/wechat/src/Kernel/Support/File.php new file mode 100644 index 0000000..b51b41d --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Support/File.php @@ -0,0 +1,135 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Support; + +use finfo; + +/** + * Class File. + */ +class File +{ + /** + * MIME mapping. + * + * @var array + */ + protected static $extensionMap = [ + 'audio/wav' => '.wav', + 'audio/x-ms-wma' => '.wma', + 'video/x-ms-wmv' => '.wmv', + 'video/mp4' => '.mp4', + 'audio/mpeg' => '.mp3', + 'audio/amr' => '.amr', + 'application/vnd.rn-realmedia' => '.rm', + 'audio/mid' => '.mid', + 'image/bmp' => '.bmp', + 'image/gif' => '.gif', + 'image/png' => '.png', + 'image/tiff' => '.tiff', + 'image/jpeg' => '.jpg', + 'application/pdf' => '.pdf', + + // 列举更多的文件 mime, 企业号是支持的,公众平台这边之后万一也更新了呢 + 'application/msword' => '.doc', + + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => '.docx', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => '.dotx', + 'application/vnd.ms-word.document.macroEnabled.12' => '.docm', + 'application/vnd.ms-word.template.macroEnabled.12' => '.dotm', + + 'application/vnd.ms-excel' => '.xls', + + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => '.xlsx', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => '.xltx', + 'application/vnd.ms-excel.sheet.macroEnabled.12' => '.xlsm', + 'application/vnd.ms-excel.template.macroEnabled.12' => '.xltm', + 'application/vnd.ms-excel.addin.macroEnabled.12' => '.xlam', + 'application/vnd.ms-excel.sheet.binary.macroEnabled.12' => '.xlsb', + + 'application/vnd.ms-powerpoint' => '.ppt', + + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => '.pptx', + 'application/vnd.openxmlformats-officedocument.presentationml.template' => '.potx', + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => '.ppsx', + 'application/vnd.ms-powerpoint.addin.macroEnabled.12' => '.ppam', + ]; + + /** + * File header signatures. + * + * @var array + */ + protected static $signatures = [ + 'ffd8ff' => '.jpg', + '424d' => '.bmp', + '47494638' => '.gif', + '2f55736572732f6f7665' => '.png', + '89504e47' => '.png', + '494433' => '.mp3', + 'fffb' => '.mp3', + 'fff3' => '.mp3', + '3026b2758e66cf11' => '.wma', + '52494646' => '.wav', + '57415645' => '.wav', + '41564920' => '.avi', + '000001ba' => '.mpg', + '000001b3' => '.mpg', + '2321414d52' => '.amr', + '25504446' => '.pdf', + ]; + + /** + * Return steam extension. + * + * @param string $stream + * + * @return string|false + */ + public static function getStreamExt($stream) + { + $ext = self::getExtBySignature($stream); + + try { + if (empty($ext) && is_readable($stream)) { + $stream = file_get_contents($stream); + } + } catch (\Exception $e) { + } + + $fileInfo = new finfo(FILEINFO_MIME); + + $mime = strstr($fileInfo->buffer($stream), ';', true); + + return isset(self::$extensionMap[$mime]) ? self::$extensionMap[$mime] : $ext; + } + + /** + * Get file extension by file header signature. + * + * @param string $stream + * + * @return string + */ + public static function getExtBySignature($stream) + { + $prefix = strval(bin2hex(mb_strcut($stream, 0, 10))); + + foreach (self::$signatures as $signature => $extension) { + if (0 === strpos($prefix, strval($signature))) { + return $extension; + } + } + + return ''; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Support/Helpers.php b/vendor/overtrue/wechat/src/Kernel/Support/Helpers.php new file mode 100644 index 0000000..6f1db32 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Support/Helpers.php @@ -0,0 +1,127 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Support; + +/* + * helpers. + * + * @author overtrue + */ + +/** + * Generate a signature. + * + * @param string $key + * @param string $encryptMethod + * + * @return string + */ +function generate_sign(array $attributes, $key, $encryptMethod = 'md5') +{ + ksort($attributes); + + $attributes['key'] = $key; + + return strtoupper(call_user_func_array($encryptMethod, [urldecode(http_build_query($attributes))])); +} + +/** + * @return \Closure|string + */ +function get_encrypt_method(string $signType, string $secretKey = '') +{ + if ('HMAC-SHA256' === $signType) { + return function ($str) use ($secretKey) { + return hash_hmac('sha256', $str, $secretKey); + }; + } + + return 'md5'; +} + +/** + * Get client ip. + * + * @return string + */ +function get_client_ip() +{ + if (!empty($_SERVER['REMOTE_ADDR'])) { + $ip = $_SERVER['REMOTE_ADDR']; + } else { + // for php-cli(phpunit etc.) + $ip = defined('PHPUNIT_RUNNING') ? '127.0.0.1' : gethostbyname(gethostname()); + } + + return filter_var($ip, FILTER_VALIDATE_IP) ?: '127.0.0.1'; +} + +/** + * Get current server ip. + * + * @return string + */ +function get_server_ip() +{ + if (!empty($_SERVER['SERVER_ADDR'])) { + $ip = $_SERVER['SERVER_ADDR']; + } elseif (!empty($_SERVER['SERVER_NAME'])) { + $ip = gethostbyname($_SERVER['SERVER_NAME']); + } else { + // for php-cli(phpunit etc.) + $ip = defined('PHPUNIT_RUNNING') ? '127.0.0.1' : gethostbyname(gethostname()); + } + + return filter_var($ip, FILTER_VALIDATE_IP) ?: '127.0.0.1'; +} + +/** + * Return current url. + * + * @return string + */ +function current_url() +{ + $protocol = 'http://'; + + if ((!empty($_SERVER['HTTPS']) && 'off' !== $_SERVER['HTTPS']) || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? 'http') === 'https') { + $protocol = 'https://'; + } + + return $protocol.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']; +} + +/** + * Return random string. + * + * @param string $length + * + * @return string + */ +function str_random($length) +{ + return Str::random($length); +} + +/** + * @param string $content + * @param string $publicKey + * + * @return string + */ +function rsa_public_encrypt($content, $publicKey) +{ + $encrypted = ''; + openssl_public_encrypt($content, $encrypted, openssl_pkey_get_public($publicKey), OPENSSL_PKCS1_OAEP_PADDING); + + return base64_encode($encrypted); +} diff --git a/vendor/overtrue/wechat/src/Kernel/Support/Str.php b/vendor/overtrue/wechat/src/Kernel/Support/Str.php new file mode 100644 index 0000000..3a51968 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Support/Str.php @@ -0,0 +1,193 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Support; + +use EasyWeChat\Kernel\Exceptions\RuntimeException; + +/** + * Class Str. + */ +class Str +{ + /** + * The cache of snake-cased words. + * + * @var array + */ + protected static $snakeCache = []; + + /** + * The cache of camel-cased words. + * + * @var array + */ + protected static $camelCache = []; + + /** + * The cache of studly-cased words. + * + * @var array + */ + protected static $studlyCache = []; + + /** + * Convert a value to camel case. + * + * @param string $value + * + * @return string + */ + public static function camel($value) + { + if (isset(static::$camelCache[$value])) { + return static::$camelCache[$value]; + } + + return static::$camelCache[$value] = lcfirst(static::studly($value)); + } + + /** + * Generate a more truly "random" alpha-numeric string. + * + * @param int $length + * + * @return string + * + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public static function random($length = 16) + { + $string = ''; + + while (($len = strlen($string)) < $length) { + $size = $length - $len; + + $bytes = static::randomBytes($size); + + $string .= substr(str_replace(['/', '+', '='], '', base64_encode($bytes)), 0, $size); + } + + return $string; + } + + /** + * Generate a more truly "random" bytes. + * + * @param int $length + * + * @return string + * + * @throws RuntimeException + * + * @codeCoverageIgnore + * + * @throws \Exception + */ + public static function randomBytes($length = 16) + { + if (function_exists('random_bytes')) { + $bytes = random_bytes($length); + } elseif (function_exists('openssl_random_pseudo_bytes')) { + $bytes = openssl_random_pseudo_bytes($length, $strong); + if (false === $bytes || false === $strong) { + throw new RuntimeException('Unable to generate random string.'); + } + } else { + throw new RuntimeException('OpenSSL extension is required for PHP 5 users.'); + } + + return $bytes; + } + + /** + * Generate a "random" alpha-numeric string. + * + * Should not be considered sufficient for cryptography, etc. + * + * @param int $length + * + * @return string + */ + public static function quickRandom($length = 16) + { + $pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + + return substr(str_shuffle(str_repeat($pool, $length)), 0, $length); + } + + /** + * Convert the given string to upper-case. + * + * @param string $value + * + * @return string + */ + public static function upper($value) + { + return mb_strtoupper($value); + } + + /** + * Convert the given string to title case. + * + * @param string $value + * + * @return string + */ + public static function title($value) + { + return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8'); + } + + /** + * Convert a string to snake case. + * + * @param string $value + * @param string $delimiter + * + * @return string + */ + public static function snake($value, $delimiter = '_') + { + $key = $value.$delimiter; + + if (isset(static::$snakeCache[$key])) { + return static::$snakeCache[$key]; + } + + if (!ctype_lower($value)) { + $value = strtolower(preg_replace('/(.)(?=[A-Z])/', '$1'.$delimiter, $value)); + } + + return static::$snakeCache[$key] = trim($value, '_'); + } + + /** + * Convert a value to studly caps case. + * + * @param string $value + * + * @return string + */ + public static function studly($value) + { + $key = $value; + + if (isset(static::$studlyCache[$key])) { + return static::$studlyCache[$key]; + } + + $value = ucwords(str_replace(['-', '_'], ' ', $value)); + + return static::$studlyCache[$key] = str_replace(' ', '', $value); + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Support/XML.php b/vendor/overtrue/wechat/src/Kernel/Support/XML.php new file mode 100644 index 0000000..b131892 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Support/XML.php @@ -0,0 +1,166 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Support; + +use SimpleXMLElement; + +/** + * Class XML. + */ +class XML +{ + /** + * XML to array. + * + * @param string $xml XML string + * + * @return array + */ + public static function parse($xml) + { + $backup = libxml_disable_entity_loader(true); + + $result = self::normalize(simplexml_load_string(self::sanitize($xml), 'SimpleXMLElement', LIBXML_COMPACT | LIBXML_NOCDATA | LIBXML_NOBLANKS)); + + libxml_disable_entity_loader($backup); + + return $result; + } + + /** + * XML encode. + * + * @param mixed $data + * @param string $root + * @param string $item + * @param string $attr + * @param string $id + * + * @return string + */ + public static function build( + $data, + $root = 'xml', + $item = 'item', + $attr = '', + $id = 'id' + ) { + if (is_array($attr)) { + $_attr = []; + + foreach ($attr as $key => $value) { + $_attr[] = "{$key}=\"{$value}\""; + } + + $attr = implode(' ', $_attr); + } + + $attr = trim($attr); + $attr = empty($attr) ? '' : " {$attr}"; + $xml = "<{$root}{$attr}>"; + $xml .= self::data2Xml($data, $item, $id); + $xml .= ""; + + return $xml; + } + + /** + * Build CDATA. + * + * @param string $string + * + * @return string + */ + public static function cdata($string) + { + return sprintf('', $string); + } + + /** + * Object to array. + * + * @param SimpleXMLElement $obj + * + * @return array + */ + protected static function normalize($obj) + { + $result = null; + + if (is_object($obj)) { + $obj = (array) $obj; + } + + if (is_array($obj)) { + foreach ($obj as $key => $value) { + $res = self::normalize($value); + if (('@attributes' === $key) && ($key)) { + $result = $res; // @codeCoverageIgnore + } else { + $result[$key] = $res; + } + } + } else { + $result = $obj; + } + + return $result; + } + + /** + * Array to XML. + * + * @param array $data + * @param string $item + * @param string $id + * + * @return string + */ + protected static function data2Xml($data, $item = 'item', $id = 'id') + { + $xml = $attr = ''; + + foreach ($data as $key => $val) { + if (is_numeric($key)) { + $id && $attr = " {$id}=\"{$key}\""; + $key = $item; + } + + $xml .= "<{$key}{$attr}>"; + + if ((is_array($val) || is_object($val))) { + $xml .= self::data2Xml((array) $val, $item, $id); + } else { + $xml .= is_numeric($val) ? $val : self::cdata($val); + } + + $xml .= ""; + } + + return $xml; + } + + /** + * Delete invalid characters in XML. + * + * @see https://www.w3.org/TR/2008/REC-xml-20081126/#charsets - XML charset range + * @see http://php.net/manual/en/regexp.reference.escape.php - escape in UTF-8 mode + * + * @param string $xml + * + * @return string + */ + public static function sanitize($xml) + { + return preg_replace('/[^\x{9}\x{A}\x{D}\x{20}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]+/u', '', $xml); + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Traits/HasAttributes.php b/vendor/overtrue/wechat/src/Kernel/Traits/HasAttributes.php new file mode 100644 index 0000000..28dcf7a --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Traits/HasAttributes.php @@ -0,0 +1,245 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Traits; + +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\Support\Arr; +use EasyWeChat\Kernel\Support\Str; + +/** + * Trait Attributes. + */ +trait HasAttributes +{ + /** + * @var array + */ + protected $attributes = []; + + /** + * @var bool + */ + protected $snakeable = true; + + /** + * Set Attributes. + * + * @return $this + */ + public function setAttributes(array $attributes = []) + { + $this->attributes = $attributes; + + return $this; + } + + /** + * Set attribute. + * + * @param string $attribute + * @param string $value + * + * @return $this + */ + public function setAttribute($attribute, $value) + { + Arr::set($this->attributes, $attribute, $value); + + return $this; + } + + /** + * Get attribute. + * + * @param string $attribute + * @param mixed $default + * + * @return mixed + */ + public function getAttribute($attribute, $default = null) + { + return Arr::get($this->attributes, $attribute, $default); + } + + /** + * @param string $attribute + * + * @return bool + */ + public function isRequired($attribute) + { + return in_array($attribute, $this->getRequired(), true); + } + + /** + * @return array|mixed + */ + public function getRequired() + { + return property_exists($this, 'required') ? $this->required : []; + } + + /** + * Set attribute. + * + * @param string $attribute + * @param mixed $value + * + * @return $this + */ + public function with($attribute, $value) + { + $this->snakeable && $attribute = Str::snake($attribute); + + $this->setAttribute($attribute, $value); + + return $this; + } + + /** + * Override parent set() method. + * + * @param string $attribute + * @param mixed $value + * + * @return $this + */ + public function set($attribute, $value) + { + $this->setAttribute($attribute, $value); + + return $this; + } + + /** + * Override parent get() method. + * + * @param string $attribute + * @param mixed $default + * + * @return mixed + */ + public function get($attribute, $default = null) + { + return $this->getAttribute($attribute, $default); + } + + /** + * @return bool + */ + public function has(string $key) + { + return Arr::has($this->attributes, $key); + } + + /** + * @return $this + */ + public function merge(array $attributes) + { + $this->attributes = array_merge($this->attributes, $attributes); + + return $this; + } + + /** + * @param array|string $keys + * + * @return array + */ + public function only($keys) + { + return Arr::only($this->attributes, $keys); + } + + /** + * Return all items. + * + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function all() + { + $this->checkRequiredAttributes(); + + return $this->attributes; + } + + /** + * Magic call. + * + * @param string $method + * @param array $args + * + * @return $this + */ + public function __call($method, $args) + { + if (0 === stripos($method, 'with')) { + return $this->with(substr($method, 4), array_shift($args)); + } + + throw new \BadMethodCallException(sprintf('Method "%s" does not exists.', $method)); + } + + /** + * Magic get. + * + * @param string $property + * + * @return mixed + */ + public function __get($property) + { + return $this->get($property); + } + + /** + * Magic set. + * + * @param string $property + * @param mixed $value + * + * @return $this + */ + public function __set($property, $value) + { + return $this->with($property, $value); + } + + /** + * Whether or not an data exists by key. + * + * @param string $key + * + * @return bool + */ + public function __isset($key) + { + return isset($this->attributes[$key]); + } + + /** + * Check required attributes. + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + protected function checkRequiredAttributes() + { + foreach ($this->getRequired() as $attribute) { + if (is_null($this->get($attribute))) { + throw new InvalidArgumentException(sprintf('"%s" cannot be empty.', $attribute)); + } + } + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Traits/HasHttpRequests.php b/vendor/overtrue/wechat/src/Kernel/Traits/HasHttpRequests.php new file mode 100644 index 0000000..1d05e2a --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Traits/HasHttpRequests.php @@ -0,0 +1,211 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Traits; + +use GuzzleHttp\Client; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\HandlerStack; +use Psr\Http\Message\ResponseInterface; + +/** + * Trait HasHttpRequests. + * + * @author overtrue + */ +trait HasHttpRequests +{ + use ResponseCastable; + + /** + * @var \GuzzleHttp\ClientInterface + */ + protected $httpClient; + + /** + * @var array + */ + protected $middlewares = []; + + /** + * @var \GuzzleHttp\HandlerStack + */ + protected $handlerStack; + + /** + * @var array + */ + protected static $defaults = [ + 'curl' => [ + CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, + ], + ]; + + /** + * Set guzzle default settings. + * + * @param array $defaults + */ + public static function setDefaultOptions($defaults = []) + { + self::$defaults = $defaults; + } + + /** + * Return current guzzle default settings. + */ + public static function getDefaultOptions(): array + { + return self::$defaults; + } + + /** + * Set GuzzleHttp\Client. + * + * @return $this + */ + public function setHttpClient(ClientInterface $httpClient) + { + $this->httpClient = $httpClient; + + return $this; + } + + /** + * Return GuzzleHttp\ClientInterface instance. + */ + public function getHttpClient(): ClientInterface + { + if (!($this->httpClient instanceof ClientInterface)) { + if (property_exists($this, 'app') && $this->app['http_client']) { + $this->httpClient = $this->app['http_client']; + } else { + $this->httpClient = new Client(['handler' => HandlerStack::create($this->getGuzzleHandler())]); + } + } + + return $this->httpClient; + } + + /** + * Add a middleware. + * + * @param string $name + * + * @return $this + */ + public function pushMiddleware(callable $middleware, string $name = null) + { + if (!is_null($name)) { + $this->middlewares[$name] = $middleware; + } else { + array_push($this->middlewares, $middleware); + } + + return $this; + } + + /** + * Return all middlewares. + */ + public function getMiddlewares(): array + { + return $this->middlewares; + } + + /** + * Make a request. + * + * @param string $url + * @param string $method + * @param array $options + * + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function request($url, $method = 'GET', $options = []): ResponseInterface + { + $method = strtoupper($method); + + $options = array_merge(self::$defaults, $options, ['handler' => $this->getHandlerStack()]); + + $options = $this->fixJsonIssue($options); + + if (property_exists($this, 'baseUri') && !is_null($this->baseUri)) { + $options['base_uri'] = $this->baseUri; + } + + $response = $this->getHttpClient()->request($method, $url, $options); + $response->getBody()->rewind(); + + return $response; + } + + /** + * @return $this + */ + public function setHandlerStack(HandlerStack $handlerStack) + { + $this->handlerStack = $handlerStack; + + return $this; + } + + /** + * Build a handler stack. + */ + public function getHandlerStack(): HandlerStack + { + if ($this->handlerStack) { + return $this->handlerStack; + } + + $this->handlerStack = HandlerStack::create($this->getGuzzleHandler()); + + foreach ($this->middlewares as $name => $middleware) { + $this->handlerStack->push($middleware, $name); + } + + return $this->handlerStack; + } + + protected function fixJsonIssue(array $options): array + { + if (isset($options['json']) && is_array($options['json'])) { + $options['headers'] = array_merge($options['headers'] ?? [], ['Content-Type' => 'application/json']); + + if (empty($options['json'])) { + $options['body'] = \GuzzleHttp\json_encode($options['json'], JSON_FORCE_OBJECT); + } else { + $options['body'] = \GuzzleHttp\json_encode($options['json'], JSON_UNESCAPED_UNICODE); + } + + unset($options['json']); + } + + return $options; + } + + /** + * Get guzzle handler. + * + * @return callable + */ + protected function getGuzzleHandler() + { + if (property_exists($this, 'app') && isset($this->app['guzzle_handler'])) { + return is_string($handler = $this->app->raw('guzzle_handler')) + ? new $handler() + : $handler; + } + + return \GuzzleHttp\choose_handler(); + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Traits/InteractsWithCache.php b/vendor/overtrue/wechat/src/Kernel/Traits/InteractsWithCache.php new file mode 100644 index 0000000..ab26fe6 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Traits/InteractsWithCache.php @@ -0,0 +1,102 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Traits; + +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\ServiceContainer; +use Psr\Cache\CacheItemPoolInterface; +use Psr\SimpleCache\CacheInterface as SimpleCacheInterface; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Psr16Cache; +use Symfony\Component\Cache\Simple\FilesystemCache; + +/** + * Trait InteractsWithCache. + * + * @author overtrue + */ +trait InteractsWithCache +{ + /** + * @var \Psr\SimpleCache\CacheInterface + */ + protected $cache; + + /** + * Get cache instance. + * + * @return \Psr\SimpleCache\CacheInterface + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function getCache() + { + if ($this->cache) { + return $this->cache; + } + + if (property_exists($this, 'app') && $this->app instanceof ServiceContainer && isset($this->app['cache'])) { + $this->setCache($this->app['cache']); + + // Fix PHPStan error + assert($this->cache instanceof \Psr\SimpleCache\CacheInterface); + + return $this->cache; + } + + return $this->cache = $this->createDefaultCache(); + } + + /** + * Set cache instance. + * + * @param \Psr\SimpleCache\CacheInterface|\Psr\Cache\CacheItemPoolInterface $cache + * + * @return $this + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function setCache($cache) + { + if (empty(\array_intersect([SimpleCacheInterface::class, CacheItemPoolInterface::class], \class_implements($cache)))) { + throw new InvalidArgumentException(\sprintf('The cache instance must implements %s or %s interface.', SimpleCacheInterface::class, CacheItemPoolInterface::class)); + } + + if ($cache instanceof CacheItemPoolInterface) { + if (!$this->isSymfony43OrHigher()) { + throw new InvalidArgumentException(sprintf('The cache instance must implements %s', SimpleCacheInterface::class)); + } + $cache = new Psr16Cache($cache); + } + + $this->cache = $cache; + + return $this; + } + + /** + * @return \Psr\SimpleCache\CacheInterface + */ + protected function createDefaultCache() + { + if ($this->isSymfony43OrHigher()) { + return new Psr16Cache(new FilesystemAdapter('easywechat', 1500)); + } + + return new FilesystemCache(); + } + + protected function isSymfony43OrHigher(): bool + { + return \class_exists('Symfony\Component\Cache\Psr16Cache'); + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Traits/Observable.php b/vendor/overtrue/wechat/src/Kernel/Traits/Observable.php new file mode 100644 index 0000000..668d490 --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Traits/Observable.php @@ -0,0 +1,278 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Traits; + +use EasyWeChat\Kernel\Clauses\Clause; +use EasyWeChat\Kernel\Contracts\EventHandlerInterface; +use EasyWeChat\Kernel\Decorators\FinallyResult; +use EasyWeChat\Kernel\Decorators\TerminateResult; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\ServiceContainer; + +/** + * Trait Observable. + * + * @author overtrue + */ +trait Observable +{ + /** + * @var array + */ + protected $handlers = []; + + /** + * @var array + */ + protected $clauses = []; + + /** + * @param \Closure|EventHandlerInterface|callable|string $handler + * @param \Closure|EventHandlerInterface|callable|string $condition + * + * @return \EasyWeChat\Kernel\Clauses\Clause + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \ReflectionException + */ + public function push($handler, $condition = '*') + { + list($handler, $condition) = $this->resolveHandlerAndCondition($handler, $condition); + + if (!isset($this->handlers[$condition])) { + $this->handlers[$condition] = []; + } + + array_push($this->handlers[$condition], $handler); + + return $this->newClause($handler); + } + + /** + * @return $this + */ + public function setHandlers(array $handlers = []) + { + $this->handlers = $handlers; + + return $this; + } + + /** + * @param \Closure|EventHandlerInterface|string $handler + * @param \Closure|EventHandlerInterface|string $condition + * + * @return \EasyWeChat\Kernel\Clauses\Clause + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \ReflectionException + */ + public function unshift($handler, $condition = '*') + { + list($handler, $condition) = $this->resolveHandlerAndCondition($handler, $condition); + + if (!isset($this->handlers[$condition])) { + $this->handlers[$condition] = []; + } + + array_unshift($this->handlers[$condition], $handler); + + return $this->newClause($handler); + } + + /** + * @param string $condition + * @param \Closure|EventHandlerInterface|string $handler + * + * @return \EasyWeChat\Kernel\Clauses\Clause + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \ReflectionException + */ + public function observe($condition, $handler) + { + return $this->push($handler, $condition); + } + + /** + * @param string $condition + * @param \Closure|EventHandlerInterface|string $handler + * + * @return \EasyWeChat\Kernel\Clauses\Clause + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \ReflectionException + */ + public function on($condition, $handler) + { + return $this->push($handler, $condition); + } + + /** + * @param string|int $event + * @param mixed ...$payload + * + * @return mixed|null + */ + public function dispatch($event, $payload) + { + return $this->notify($event, $payload); + } + + /** + * @param string|int $event + * @param mixed ...$payload + * + * @return mixed|null + */ + public function notify($event, $payload) + { + $result = null; + + foreach ($this->handlers as $condition => $handlers) { + if ('*' === $condition || ($condition & $event) === $event) { + foreach ($handlers as $handler) { + if ($clause = $this->clauses[$this->getHandlerHash($handler)] ?? null) { + if ($clause->intercepted($payload)) { + continue; + } + } + + $response = $this->callHandler($handler, $payload); + + switch (true) { + case $response instanceof TerminateResult: + return $response->content; + case true === $response: + continue 2; + case false === $response: + break 2; + case !empty($response) && !($result instanceof FinallyResult): + $result = $response; + } + } + } + } + + return $result instanceof FinallyResult ? $result->content : $result; + } + + /** + * @return array + */ + public function getHandlers() + { + return $this->handlers; + } + + /** + * @param mixed $handler + */ + protected function newClause($handler): Clause + { + return $this->clauses[$this->getHandlerHash($handler)] = new Clause(); + } + + /** + * @param mixed $handler + * + * @return string + */ + protected function getHandlerHash($handler) + { + if (is_string($handler)) { + return $handler; + } + + if (is_array($handler)) { + return is_string($handler[0]) + ? $handler[0].'::'.$handler[1] + : get_class($handler[0]).$handler[1]; + } + + return spl_object_hash($handler); + } + + /** + * @param mixed $payload + * + * @return mixed + */ + protected function callHandler(callable $handler, $payload) + { + try { + return call_user_func_array($handler, [$payload]); + } catch (\Exception $e) { + if (property_exists($this, 'app') && $this->app instanceof ServiceContainer) { + $this->app['logger']->error($e->getCode().': '.$e->getMessage(), [ + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + ]); + } + } + } + + /** + * @param mixed $handler + * + * @return \Closure + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \ReflectionException + */ + protected function makeClosure($handler) + { + if (is_callable($handler)) { + return $handler; + } + + if (is_string($handler) && '*' !== $handler) { + if (!class_exists($handler)) { + throw new InvalidArgumentException(sprintf('Class "%s" not exists.', $handler)); + } + + if (!in_array(EventHandlerInterface::class, (new \ReflectionClass($handler))->getInterfaceNames(), true)) { + throw new InvalidArgumentException(sprintf('Class "%s" not an instance of "%s".', $handler, EventHandlerInterface::class)); + } + + return function ($payload) use ($handler) { + return (new $handler($this->app ?? null))->handle($payload); + }; + } + + if ($handler instanceof EventHandlerInterface) { + return function () use ($handler) { + return $handler->handle(...func_get_args()); + }; + } + + throw new InvalidArgumentException('No valid handler is found in arguments.'); + } + + /** + * @param mixed $handler + * @param mixed $condition + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \ReflectionException + */ + protected function resolveHandlerAndCondition($handler, $condition): array + { + if (is_int($handler) || (is_string($handler) && !class_exists($handler))) { + list($handler, $condition) = [$condition, $handler]; + } + + return [$this->makeClosure($handler), $condition]; + } +} diff --git a/vendor/overtrue/wechat/src/Kernel/Traits/ResponseCastable.php b/vendor/overtrue/wechat/src/Kernel/Traits/ResponseCastable.php new file mode 100644 index 0000000..504b5ea --- /dev/null +++ b/vendor/overtrue/wechat/src/Kernel/Traits/ResponseCastable.php @@ -0,0 +1,92 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Kernel\Traits; + +use EasyWeChat\Kernel\Contracts\Arrayable; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\Exceptions\InvalidConfigException; +use EasyWeChat\Kernel\Http\Response; +use EasyWeChat\Kernel\Support\Collection; +use Psr\Http\Message\ResponseInterface; + +/** + * Trait ResponseCastable. + * + * @author overtrue + */ +trait ResponseCastable +{ + /** + * @param string|null $type + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + protected function castResponseToType(ResponseInterface $response, $type = null) + { + $response = Response::buildFromPsrResponse($response); + $response->getBody()->rewind(); + + switch ($type ?? 'array') { + case 'collection': + return $response->toCollection(); + case 'array': + return $response->toArray(); + case 'object': + return $response->toObject(); + case 'raw': + return $response; + default: + if (!is_subclass_of($type, Arrayable::class)) { + throw new InvalidConfigException(sprintf('Config key "response_type" classname must be an instanceof %s', Arrayable::class)); + } + + return new $type($response); + } + } + + /** + * @param mixed $response + * @param string|null $type + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + protected function detectAndCastResponseToType($response, $type = null) + { + switch (true) { + case $response instanceof ResponseInterface: + $response = Response::buildFromPsrResponse($response); + + break; + case $response instanceof Arrayable: + $response = new Response(200, [], json_encode($response->toArray())); + + break; + case ($response instanceof Collection) || is_array($response) || is_object($response): + $response = new Response(200, [], json_encode($response)); + + break; + case is_scalar($response): + $response = new Response(200, [], (string) $response); + + break; + default: + throw new InvalidArgumentException(sprintf('Unsupported response type "%s"', gettype($response))); + } + + return $this->castResponseToType($response, $type); + } +} diff --git a/vendor/overtrue/wechat/src/MicroMerchant/Application.php b/vendor/overtrue/wechat/src/MicroMerchant/Application.php new file mode 100644 index 0000000..2d5976c --- /dev/null +++ b/vendor/overtrue/wechat/src/MicroMerchant/Application.php @@ -0,0 +1,168 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MicroMerchant; + +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\ServiceContainer; +use EasyWeChat\Kernel\Support; +use EasyWeChat\MicroMerchant\Kernel\Exceptions\InvalidSignException; + +/** + * Class Application. + * + * @author liuml + * + * @property \EasyWeChat\MicroMerchant\Certficates\Client $certficates + * @property \EasyWeChat\MicroMerchant\Material\Client $material + * @property \EasyWeChat\MicroMerchant\MerchantConfig\Client $merchantConfig + * @property \EasyWeChat\MicroMerchant\Withdraw\Client $withdraw + * @property \EasyWeChat\MicroMerchant\Media\Client $media + * + * @method mixed submitApplication(array $params) + * @method mixed getStatus(string $applymentId, string $businessCode = '') + * @method mixed upgrade(array $params) + * @method mixed getUpgradeStatus(string $subMchId = '') + */ +class Application extends ServiceContainer +{ + /** + * @var array + */ + protected $providers = [ + // Base services + Base\ServiceProvider::class, + Certficates\ServiceProvider::class, + MerchantConfig\ServiceProvider::class, + Material\ServiceProvider::class, + Withdraw\ServiceProvider::class, + Media\ServiceProvider::class, + ]; + + /** + * @var array + */ + protected $defaultConfig = [ + 'http' => [ + 'base_uri' => 'https://api.mch.weixin.qq.com/', + ], + 'log' => [ + 'default' => 'dev', // 默认使用的 channel,生产环境可以改为下面的 prod + 'channels' => [ + // 测试环境 + 'dev' => [ + 'driver' => 'single', + 'path' => '/tmp/easywechat.log', + 'level' => 'debug', + ], + // 生产环境 + 'prod' => [ + 'driver' => 'daily', + 'path' => '/tmp/easywechat.log', + 'level' => 'info', + ], + ], + ], + ]; + + /** + * @return string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function getKey() + { + $key = $this['config']->key; + + if (empty($key)) { + throw new InvalidArgumentException('config key connot be empty.'); + } + + if (32 !== strlen($key)) { + throw new InvalidArgumentException(sprintf("'%s' should be 32 chars length.", $key)); + } + + return $key; + } + + /** + * set sub-mch-id and appid. + * + * @param string $subMchId Identification Number of Small and Micro Businessmen Reported by Service Providers + * @param string $appId Public Account ID of Service Provider + * + * @return $this + */ + public function setSubMchId(string $subMchId, string $appId = '') + { + $this['config']->set('sub_mch_id', $subMchId); + if ($appId) { + $this['config']->set('appid', $appId); + } + + return $this; + } + + /** + * setCertificate. + * + * @return $this + */ + public function setCertificate(string $certificate, string $serialNo) + { + $this['config']->set('certificate', $certificate); + $this['config']->set('serial_no', $serialNo); + + return $this; + } + + /** + * Returning true indicates that the verification is successful, + * returning false indicates that the signature field does not exist or is empty, + * and if the signature verification is wrong, the InvalidSignException will be thrown directly. + * + * @return bool + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\InvalidSignException + */ + public function verifySignature(array $data) + { + if (!isset($data['sign']) || empty($data['sign'])) { + return false; + } + + $sign = $data['sign']; + unset($data['sign']); + + $signType = strlen($sign) > 32 ? 'HMAC-SHA256' : 'MD5'; + $secretKey = $this->getKey(); + + $encryptMethod = Support\get_encrypt_method($signType, $secretKey); + + if (Support\generate_sign($data, $secretKey, $encryptMethod) === $sign) { + return true; + } + + throw new InvalidSignException('return value signature verification error'); + } + + /** + * @param string $name + * @param array $arguments + * + * @return mixed + */ + public function __call($name, $arguments) + { + return call_user_func_array([$this['base'], $name], $arguments); + } +} diff --git a/vendor/overtrue/wechat/src/MicroMerchant/Base/Client.php b/vendor/overtrue/wechat/src/MicroMerchant/Base/Client.php new file mode 100644 index 0000000..3493649 --- /dev/null +++ b/vendor/overtrue/wechat/src/MicroMerchant/Base/Client.php @@ -0,0 +1,117 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MicroMerchant\Base; + +use EasyWeChat\MicroMerchant\Kernel\BaseClient; + +/** + * Class Client. + * + * @author liuml + * @DateTime 2019-05-30 14:19 + */ +class Client extends BaseClient +{ + /** + * apply to settle in to become a small micro merchant. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\EncryptException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function submitApplication(array $params) + { + $params = $this->processParams(array_merge($params, [ + 'version' => '3.0', + 'cert_sn' => '', + 'sign_type' => 'HMAC-SHA256', + 'nonce_str' => uniqid('micro'), + ])); + + return $this->safeRequest('applyment/micro/submit', $params); + } + + /** + * query application status. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getStatus(string $applymentId, string $businessCode = '') + { + if (!empty($applymentId)) { + $params = [ + 'applyment_id' => $applymentId, + ]; + } else { + $params = [ + 'business_code' => $businessCode, + ]; + } + + $params = array_merge($params, [ + 'version' => '1.0', + 'sign_type' => 'HMAC-SHA256', + 'nonce_str' => uniqid('micro'), + ]); + + return $this->safeRequest('applyment/micro/getstate', $params); + } + + /** + * merchant upgrade api. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\EncryptException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function upgrade(array $params) + { + $params['sub_mch_id'] = $params['sub_mch_id'] ?? $this->app['config']->sub_mch_id; + $params = $this->processParams(array_merge($params, [ + 'version' => '1.0', + 'cert_sn' => '', + 'sign_type' => 'HMAC-SHA256', + 'nonce_str' => uniqid('micro'), + ])); + + return $this->safeRequest('applyment/micro/submitupgrade', $params); + } + + /** + * get upgrade status. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getUpgradeStatus(string $subMchId = '') + { + return $this->safeRequest('applyment/micro/getupgradestate', [ + 'version' => '1.0', + 'sign_type' => 'HMAC-SHA256', + 'sub_mch_id' => $subMchId ?: $this->app['config']->sub_mch_id, + 'nonce_str' => uniqid('micro'), + ]); + } +} diff --git a/vendor/overtrue/wechat/src/MicroMerchant/Base/ServiceProvider.php b/vendor/overtrue/wechat/src/MicroMerchant/Base/ServiceProvider.php new file mode 100644 index 0000000..db2f056 --- /dev/null +++ b/vendor/overtrue/wechat/src/MicroMerchant/Base/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MicroMerchant\Base; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['base'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MicroMerchant/Certficates/Client.php b/vendor/overtrue/wechat/src/MicroMerchant/Certficates/Client.php new file mode 100644 index 0000000..2b09145 --- /dev/null +++ b/vendor/overtrue/wechat/src/MicroMerchant/Certficates/Client.php @@ -0,0 +1,89 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MicroMerchant\Certficates; + +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\MicroMerchant\Kernel\BaseClient; +use EasyWeChat\MicroMerchant\Kernel\Exceptions\InvalidExtensionException; + +/** + * Class Client. + * + * @author liuml + * @DateTime 2019-05-30 14:19 + */ +class Client extends BaseClient +{ + /** + * get certficates. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\InvalidExtensionException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(bool $returnRaw = false) + { + $params = [ + 'sign_type' => 'HMAC-SHA256', + 'nonce_str' => uniqid('micro'), + ]; + + if (true === $returnRaw) { + return $this->requestRaw('risk/getcertficates', $params); + } + /** @var array $response */ + $response = $this->requestArray('risk/getcertficates', $params); + + if ('SUCCESS' !== $response['return_code']) { + throw new InvalidArgumentException(sprintf('Failed to get certificate. return_code_msg: "%s" .', $response['return_code'].'('.$response['return_msg'].')')); + } + if ('SUCCESS' !== $response['result_code']) { + throw new InvalidArgumentException(sprintf('Failed to get certificate. result_err_code_desc: "%s" .', $response['result_code'].'('.$response['err_code'].'['.$response['err_code_desc'].'])')); + } + $certificates = \GuzzleHttp\json_decode($response['certificates'], true)['data'][0]; + $ciphertext = $this->decrypt($certificates['encrypt_certificate']); + unset($certificates['encrypt_certificate']); + $certificates['certificates'] = $ciphertext; + + return $certificates; + } + + /** + * decrypt ciphertext. + * + * @return string + * + * @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\InvalidExtensionException + */ + public function decrypt(array $encryptCertificate) + { + if (false === extension_loaded('sodium')) { + throw new InvalidExtensionException('sodium extension is not installed,Reference link https://php.net/manual/zh/book.sodium.php'); + } + + if (false === sodium_crypto_aead_aes256gcm_is_available()) { + throw new InvalidExtensionException('aes256gcm is not currently supported'); + } + + // sodium_crypto_aead_aes256gcm_decrypt function needs to open libsodium extension. + // https://www.php.net/manual/zh/function.sodium-crypto-aead-aes256gcm-decrypt.php + return sodium_crypto_aead_aes256gcm_decrypt( + base64_decode($encryptCertificate['ciphertext'], true), + $encryptCertificate['associated_data'], + $encryptCertificate['nonce'], + $this->app['config']->apiv3_key + ); + } +} diff --git a/vendor/overtrue/wechat/src/MicroMerchant/Certficates/ServiceProvider.php b/vendor/overtrue/wechat/src/MicroMerchant/Certficates/ServiceProvider.php new file mode 100644 index 0000000..2e88b7e --- /dev/null +++ b/vendor/overtrue/wechat/src/MicroMerchant/Certficates/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MicroMerchant\Certficates; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['certficates'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MicroMerchant/Kernel/BaseClient.php b/vendor/overtrue/wechat/src/MicroMerchant/Kernel/BaseClient.php new file mode 100644 index 0000000..fa1e796 --- /dev/null +++ b/vendor/overtrue/wechat/src/MicroMerchant/Kernel/BaseClient.php @@ -0,0 +1,241 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MicroMerchant\Kernel; + +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\Support; +use EasyWeChat\MicroMerchant\Application; +use EasyWeChat\MicroMerchant\Kernel\Exceptions\EncryptException; +use EasyWeChat\Payment\Kernel\BaseClient as PaymentBaseClient; + +/** + * Class BaseClient. + * + * @author liuml + * @DateTime 2019-07-10 12:06 + */ +class BaseClient extends PaymentBaseClient +{ + /** + * @var string + */ + protected $certificates; + + /** + * BaseClient constructor. + */ + public function __construct(Application $app) + { + $this->app = $app; + + $this->setHttpClient($this->app['http_client']); + } + + /** + * Extra request params. + * + * @return array + */ + protected function prepends() + { + return []; + } + + /** + * httpUpload. + * + * @param bool $returnResponse + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\InvalidSignException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function httpUpload(string $url, array $files = [], array $form = [], array $query = [], $returnResponse = false) + { + $multipart = []; + + foreach ($files as $name => $path) { + $multipart[] = [ + 'name' => $name, + 'contents' => fopen($path, 'r'), + ]; + } + + $base = [ + 'mch_id' => $this->app['config']['mch_id'], + ]; + + $form = array_merge($base, $form); + + $form['sign'] = $this->getSign($form); + + foreach ($form as $name => $contents) { + $multipart[] = compact('name', 'contents'); + } + + $options = [ + 'query' => $query, + 'multipart' => $multipart, + 'connect_timeout' => 30, + 'timeout' => 30, + 'read_timeout' => 30, + 'cert' => $this->app['config']->get('cert_path'), + 'ssl_key' => $this->app['config']->get('key_path'), + ]; + + $this->pushMiddleware($this->logMiddleware(), 'log'); + + $response = $this->performRequest($url, 'POST', $options); + + $result = $returnResponse ? $response : $this->castResponseToType($response, $this->app->config->get('response_type')); + // auto verify signature + if ($returnResponse || 'array' !== ($this->app->config->get('response_type') ?? 'array')) { + $this->app->verifySignature($this->castResponseToType($response, 'array')); + } else { + $this->app->verifySignature($result); + } + + return $result; + } + + /** + * request. + * + * @param string $method + * @param bool $returnResponse + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\InvalidSignException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function request(string $endpoint, array $params = [], $method = 'post', array $options = [], $returnResponse = false) + { + $base = [ + 'mch_id' => $this->app['config']['mch_id'], + ]; + + $params = array_merge($base, $this->prepends(), $params); + $params['sign'] = $this->getSign($params); + $options = array_merge([ + 'body' => Support\XML::build($params), + ], $options); + + $this->pushMiddleware($this->logMiddleware(), 'log'); + $response = $this->performRequest($endpoint, $method, $options); + $result = $returnResponse ? $response : $this->castResponseToType($response, $this->app->config->get('response_type')); + // auto verify signature + if ($returnResponse || 'array' !== ($this->app->config->get('response_type') ?? 'array')) { + $this->app->verifySignature($this->castResponseToType($response, 'array')); + } else { + $this->app->verifySignature($result); + } + + return $result; + } + + /** + * processing parameters contain fields that require sensitive information encryption. + * + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\EncryptException + */ + protected function processParams(array $params) + { + $serial_no = $this->app['config']->get('serial_no'); + if (null === $serial_no) { + throw new InvalidArgumentException('config serial_no connot be empty.'); + } + + $params['cert_sn'] = $serial_no; + $sensitive_fields = $this->getSensitiveFieldsName(); + foreach ($params as $k => $v) { + if (in_array($k, $sensitive_fields, true)) { + $params[$k] = $this->encryptSensitiveInformation($v); + } + } + + return $params; + } + + /** + * To id card, mobile phone number and other fields sensitive information encryption. + * + * @return string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\EncryptException + */ + protected function encryptSensitiveInformation(string $string) + { + $certificates = $this->app['config']->get('certificate'); + if (null === $certificates) { + throw new InvalidArgumentException('config certificate connot be empty.'); + } + + $encrypted = ''; + $publicKeyResource = openssl_get_publickey($certificates); + $f = openssl_public_encrypt($string, $encrypted, $publicKeyResource); + openssl_free_key($publicKeyResource); + if ($f) { + return base64_encode($encrypted); + } + + throw new EncryptException('Encryption of sensitive information failed'); + } + + /** + * get sensitive fields name. + * + * @return array + */ + protected function getSensitiveFieldsName() + { + return [ + 'id_card_name', + 'id_card_number', + 'account_name', + 'account_number', + 'contact', + 'contact_phone', + 'contact_email', + 'legal_person', + 'mobile_phone', + 'email', + ]; + } + + /** + * getSign. + * + * @return string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + protected function getSign(array $params) + { + $params = array_filter($params); + + $key = $this->app->getKey(); + + $encryptMethod = Support\get_encrypt_method(Support\Arr::get($params, 'sign_type', 'MD5'), $key); + + return Support\generate_sign($params, $key, $encryptMethod); + } +} diff --git a/vendor/overtrue/wechat/src/MicroMerchant/Kernel/Exceptions/EncryptException.php b/vendor/overtrue/wechat/src/MicroMerchant/Kernel/Exceptions/EncryptException.php new file mode 100644 index 0000000..dd874ae --- /dev/null +++ b/vendor/overtrue/wechat/src/MicroMerchant/Kernel/Exceptions/EncryptException.php @@ -0,0 +1,23 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MicroMerchant\Kernel\Exceptions; + +use EasyWeChat\Kernel\Exceptions\Exception; + +/** + * Class EncryptException. + * + * @author liuml + */ +class EncryptException extends Exception +{ +} diff --git a/vendor/overtrue/wechat/src/MicroMerchant/Kernel/Exceptions/InvalidExtensionException.php b/vendor/overtrue/wechat/src/MicroMerchant/Kernel/Exceptions/InvalidExtensionException.php new file mode 100644 index 0000000..41e21cf --- /dev/null +++ b/vendor/overtrue/wechat/src/MicroMerchant/Kernel/Exceptions/InvalidExtensionException.php @@ -0,0 +1,23 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MicroMerchant\Kernel\Exceptions; + +use EasyWeChat\Kernel\Exceptions\Exception; + +/** + * Class InvalidExtensionException. + * + * @author liuml + */ +class InvalidExtensionException extends Exception +{ +} diff --git a/vendor/overtrue/wechat/src/MicroMerchant/Kernel/Exceptions/InvalidSignException.php b/vendor/overtrue/wechat/src/MicroMerchant/Kernel/Exceptions/InvalidSignException.php new file mode 100644 index 0000000..d09d92b --- /dev/null +++ b/vendor/overtrue/wechat/src/MicroMerchant/Kernel/Exceptions/InvalidSignException.php @@ -0,0 +1,23 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MicroMerchant\Kernel\Exceptions; + +use EasyWeChat\Kernel\Exceptions\Exception; + +/** + * Class InvalidSignException. + * + * @author liuml + */ +class InvalidSignException extends Exception +{ +} diff --git a/vendor/overtrue/wechat/src/MicroMerchant/Material/Client.php b/vendor/overtrue/wechat/src/MicroMerchant/Material/Client.php new file mode 100644 index 0000000..eb7a0d8 --- /dev/null +++ b/vendor/overtrue/wechat/src/MicroMerchant/Material/Client.php @@ -0,0 +1,69 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MicroMerchant\Material; + +use EasyWeChat\MicroMerchant\Kernel\BaseClient; + +/** + * Class Client. + * + * @author liuml + * @DateTime 2019-05-30 14:19 + */ +class Client extends BaseClient +{ + /** + * update settlement card. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\EncryptException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setSettlementCard(array $params) + { + $params['sub_mch_id'] = $params['sub_mch_id'] ?? $this->app['config']->sub_mch_id; + $params = $this->processParams(array_merge($params, [ + 'version' => '1.0', + 'cert_sn' => '', + 'sign_type' => 'HMAC-SHA256', + 'nonce_str' => uniqid('micro'), + ])); + + return $this->safeRequest('applyment/micro/modifyarchives', $params); + } + + /** + * update contact info. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\EncryptException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function updateContact(array $params) + { + $params['sub_mch_id'] = $params['sub_mch_id'] ?? $this->app['config']->sub_mch_id; + $params = $this->processParams(array_merge($params, [ + 'version' => '1.0', + 'cert_sn' => '', + 'sign_type' => 'HMAC-SHA256', + 'nonce_str' => uniqid('micro'), + ])); + + return $this->safeRequest('applyment/micro/modifycontactinfo', $params); + } +} diff --git a/vendor/overtrue/wechat/src/MicroMerchant/Material/ServiceProvider.php b/vendor/overtrue/wechat/src/MicroMerchant/Material/ServiceProvider.php new file mode 100644 index 0000000..dec5af7 --- /dev/null +++ b/vendor/overtrue/wechat/src/MicroMerchant/Material/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MicroMerchant\Material; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['material'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MicroMerchant/Media/Client.php b/vendor/overtrue/wechat/src/MicroMerchant/Media/Client.php new file mode 100644 index 0000000..7d8e83c --- /dev/null +++ b/vendor/overtrue/wechat/src/MicroMerchant/Media/Client.php @@ -0,0 +1,47 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MicroMerchant\Media; + +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\MicroMerchant\Kernel\BaseClient; + +/** + * Class Client. + * + * @author liuml + * @DateTime 2019-06-10 14:50 + */ +class Client extends BaseClient +{ + /** + * Upload material. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\InvalidSignException + */ + public function upload(string $path) + { + if (!file_exists($path) || !is_readable($path)) { + throw new InvalidArgumentException(sprintf("File does not exist, or the file is unreadable: '%s'", $path)); + } + + $form = [ + 'media_hash' => strtolower(md5_file($path)), + 'sign_type' => 'HMAC-SHA256', + ]; + + return $this->httpUpload('secapi/mch/uploadmedia', ['media' => $path], $form); + } +} diff --git a/vendor/overtrue/wechat/src/MicroMerchant/Media/ServiceProvider.php b/vendor/overtrue/wechat/src/MicroMerchant/Media/ServiceProvider.php new file mode 100644 index 0000000..9d46a84 --- /dev/null +++ b/vendor/overtrue/wechat/src/MicroMerchant/Media/ServiceProvider.php @@ -0,0 +1,44 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * ServiceProvider.php. + * + * This file is part of the wechat. + * + * (c) overtrue + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MicroMerchant\Media; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['media'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MicroMerchant/MerchantConfig/Client.php b/vendor/overtrue/wechat/src/MicroMerchant/MerchantConfig/Client.php new file mode 100644 index 0000000..e72cc91 --- /dev/null +++ b/vendor/overtrue/wechat/src/MicroMerchant/MerchantConfig/Client.php @@ -0,0 +1,116 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MicroMerchant\MerchantConfig; + +use EasyWeChat\MicroMerchant\Kernel\BaseClient; + +/** + * Class Client. + * + * @author liuml + * @DateTime 2019-05-30 14:19 + */ +class Client extends BaseClient +{ + /** + * Service providers configure recommendation functions for small and micro businesses. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setFollowConfig(string $subAppId, string $subscribeAppId, string $receiptAppId = '', string $subMchId = '') + { + $params = [ + 'sub_appid' => $subAppId, + 'sub_mch_id' => $subMchId ?: $this->app['config']->sub_mch_id, + ]; + + if (!empty($subscribeAppId)) { + $params['subscribe_appid'] = $subscribeAppId; + } else { + $params['receipt_appid'] = $receiptAppId; + } + + return $this->safeRequest('secapi/mkt/addrecommendconf', array_merge($params, [ + 'sign_type' => 'HMAC-SHA256', + 'nonce_str' => uniqid('micro'), + ])); + } + + /** + * Configure the new payment directory. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function addPath(string $jsapiPath, string $appId = '', string $subMchId = '') + { + return $this->addConfig([ + 'appid' => $appId ?: $this->app['config']->appid, + 'sub_mch_id' => $subMchId ?: $this->app['config']->sub_mch_id, + 'jsapi_path' => $jsapiPath, + ]); + } + + /** + * bind appid. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function bindAppId(string $subAppId, string $appId = '', string $subMchId = '') + { + return $this->addConfig([ + 'appid' => $appId ?: $this->app['config']->appid, + 'sub_mch_id' => $subMchId ?: $this->app['config']->sub_mch_id, + 'sub_appid' => $subAppId, + ]); + } + + /** + * add sub dev config. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + private function addConfig(array $params) + { + return $this->safeRequest('secapi/mch/addsubdevconfig', $params); + } + + /** + * query Sub Dev Config. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getConfig(string $subMchId = '', string $appId = '') + { + return $this->safeRequest('secapi/mch/querysubdevconfig', [ + 'sub_mch_id' => $subMchId ?: $this->app['config']->sub_mch_id, + 'appid' => $appId ?: $this->app['config']->appid, + ]); + } +} diff --git a/vendor/overtrue/wechat/src/MicroMerchant/MerchantConfig/ServiceProvider.php b/vendor/overtrue/wechat/src/MicroMerchant/MerchantConfig/ServiceProvider.php new file mode 100644 index 0000000..5b78710 --- /dev/null +++ b/vendor/overtrue/wechat/src/MicroMerchant/MerchantConfig/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MicroMerchant\MerchantConfig; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['merchantConfig'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MicroMerchant/Withdraw/Client.php b/vendor/overtrue/wechat/src/MicroMerchant/Withdraw/Client.php new file mode 100644 index 0000000..c96c363 --- /dev/null +++ b/vendor/overtrue/wechat/src/MicroMerchant/Withdraw/Client.php @@ -0,0 +1,67 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MicroMerchant\Withdraw; + +use EasyWeChat\MicroMerchant\Kernel\BaseClient; + +/** + * Class Client. + * + * @author liuml + * @DateTime 2019-05-30 14:19 + */ +class Client extends BaseClient +{ + /** + * Query withdrawal status. + * + * @param string $date + * @param string $subMchId + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function queryWithdrawalStatus($date, $subMchId = '') + { + return $this->safeRequest('fund/queryautowithdrawbydate', [ + 'date' => $date, + 'sign_type' => 'HMAC-SHA256', + 'nonce_str' => uniqid('micro'), + 'sub_mch_id' => $subMchId ?: $this->app['config']->sub_mch_id, + ]); + } + + /** + * Re-initiation of withdrawal. + * + * @param string $date + * @param string $subMchId + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function requestWithdraw($date, $subMchId = '') + { + return $this->safeRequest('fund/reautowithdrawbydate', [ + 'date' => $date, + 'sign_type' => 'HMAC-SHA256', + 'nonce_str' => uniqid('micro'), + 'sub_mch_id' => $subMchId ?: $this->app['config']->sub_mch_id, + ]); + } +} diff --git a/vendor/overtrue/wechat/src/MicroMerchant/Withdraw/ServiceProvider.php b/vendor/overtrue/wechat/src/MicroMerchant/Withdraw/ServiceProvider.php new file mode 100644 index 0000000..b9c0141 --- /dev/null +++ b/vendor/overtrue/wechat/src/MicroMerchant/Withdraw/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MicroMerchant\Withdraw; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['withdraw'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/ActivityMessage/Client.php b/vendor/overtrue/wechat/src/MiniProgram/ActivityMessage/Client.php new file mode 100644 index 0000000..2cbfa08 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/ActivityMessage/Client.php @@ -0,0 +1,79 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\ActivityMessage; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; + +class Client extends BaseClient +{ + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function createActivityId() + { + return $this->httpGet('cgi-bin/message/wxopen/activityid/create'); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function updateMessage(string $activityId, int $state = 0, array $params = []) + { + if (!in_array($state, [0, 1], true)) { + throw new InvalidArgumentException('"state" should be "0" or "1".'); + } + + $params = $this->formatParameters($params); + + $params = [ + 'activity_id' => $activityId, + 'target_state' => $state, + 'template_info' => ['parameter_list' => $params], + ]; + + return $this->httpPostJson('cgi-bin/message/wxopen/updatablemsg/send', $params); + } + + /** + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + protected function formatParameters(array $params) + { + $formatted = []; + + foreach ($params as $name => $value) { + if (!in_array($name, ['member_count', 'room_limit', 'path', 'version_type'], true)) { + continue; + } + + if ('version_type' === $name && !in_array($value, ['develop', 'trial', 'release'], true)) { + throw new InvalidArgumentException('Invalid value of attribute "version_type".'); + } + + $formatted[] = [ + 'name' => $name, + 'value' => strval($value), + ]; + } + + return $formatted; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/ActivityMessage/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/ActivityMessage/ServiceProvider.php new file mode 100644 index 0000000..fa7d3cf --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/ActivityMessage/ServiceProvider.php @@ -0,0 +1,28 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\ActivityMessage; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['activity_message'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/AppCode/Client.php b/vendor/overtrue/wechat/src/MiniProgram/AppCode/Client.php new file mode 100644 index 0000000..b9df3bc --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/AppCode/Client.php @@ -0,0 +1,80 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\AppCode; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Http\StreamResponse; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * Get AppCode. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + */ + public function get(string $path, array $optional = []) + { + $params = array_merge([ + 'path' => $path, + ], $optional); + + return $this->getStream('wxa/getwxacode', $params); + } + + /** + * Get AppCode unlimit. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + */ + public function getUnlimit(string $scene, array $optional = []) + { + $params = array_merge([ + 'scene' => $scene, + ], $optional); + + return $this->getStream('wxa/getwxacodeunlimit', $params); + } + + /** + * Create QrCode. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + */ + public function getQrCode(string $path, int $width = null) + { + return $this->getStream('cgi-bin/wxaapp/createwxaqrcode', compact('path', 'width')); + } + + /** + * Get stream. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function getStream(string $endpoint, array $params) + { + $response = $this->requestRaw($endpoint, 'POST', ['json' => $params]); + + if (false !== stripos($response->getHeaderLine('Content-disposition'), 'attachment')) { + return StreamResponse::buildFromPsrResponse($response); + } + + return $this->castResponseToType($response, $this->app['config']->get('response_type')); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/AppCode/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/AppCode/ServiceProvider.php new file mode 100644 index 0000000..fefdc22 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/AppCode/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\AppCode; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['app_code'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Application.php b/vendor/overtrue/wechat/src/MiniProgram/Application.php new file mode 100644 index 0000000..ee655a7 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Application.php @@ -0,0 +1,91 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram; + +use EasyWeChat\BasicService; +use EasyWeChat\Kernel\ServiceContainer; + +/** + * Class Application. + * + * @author mingyoung + * + * @property \EasyWeChat\MiniProgram\Auth\AccessToken $access_token + * @property \EasyWeChat\MiniProgram\DataCube\Client $data_cube + * @property \EasyWeChat\MiniProgram\AppCode\Client $app_code + * @property \EasyWeChat\MiniProgram\Auth\Client $auth + * @property \EasyWeChat\OfficialAccount\Server\Guard $server + * @property \EasyWeChat\MiniProgram\Encryptor $encryptor + * @property \EasyWeChat\MiniProgram\TemplateMessage\Client $template_message + * @property \EasyWeChat\OfficialAccount\CustomerService\Client $customer_service + * @property \EasyWeChat\MiniProgram\Plugin\Client $plugin + * @property \EasyWeChat\MiniProgram\Plugin\DevClient $plugin_dev + * @property \EasyWeChat\MiniProgram\UniformMessage\Client $uniform_message + * @property \EasyWeChat\MiniProgram\ActivityMessage\Client $activity_message + * @property \EasyWeChat\MiniProgram\Express\Client $logistics + * @property \EasyWeChat\MiniProgram\NearbyPoi\Client $nearby_poi + * @property \EasyWeChat\MiniProgram\OCR\Client $ocr + * @property \EasyWeChat\MiniProgram\Soter\Client $soter + * @property \EasyWeChat\BasicService\Media\Client $media + * @property \EasyWeChat\BasicService\ContentSecurity\Client $content_security + * @property \EasyWeChat\MiniProgram\Mall\ForwardsMall $mall + * @property \EasyWeChat\MiniProgram\SubscribeMessage\Client $subscribe_message + * @property \EasyWeChat\MiniProgram\RealtimeLog\Client $realtime_log + * @property \EasyWeChat\MiniProgram\Search\Client $search + * @property \EasyWeChat\MiniProgram\Live\Client $live + * @property \EasyWeChat\MiniProgram\Broadcast\Client $broadcast + */ +class Application extends ServiceContainer +{ + /** + * @var array + */ + protected $providers = [ + Auth\ServiceProvider::class, + DataCube\ServiceProvider::class, + AppCode\ServiceProvider::class, + Server\ServiceProvider::class, + TemplateMessage\ServiceProvider::class, + CustomerService\ServiceProvider::class, + UniformMessage\ServiceProvider::class, + ActivityMessage\ServiceProvider::class, + OpenData\ServiceProvider::class, + Plugin\ServiceProvider::class, + Base\ServiceProvider::class, + Express\ServiceProvider::class, + NearbyPoi\ServiceProvider::class, + OCR\ServiceProvider::class, + Soter\ServiceProvider::class, + Mall\ServiceProvider::class, + SubscribeMessage\ServiceProvider::class, + RealtimeLog\ServiceProvider::class, + Search\ServiceProvider::class, + Live\ServiceProvider::class, + Broadcast\ServiceProvider::class, + // Base services + BasicService\Media\ServiceProvider::class, + BasicService\ContentSecurity\ServiceProvider::class, + ]; + + /** + * Handle dynamic calls. + * + * @param string $method + * @param array $args + * + * @return mixed + */ + public function __call($method, $args) + { + return $this->base->$method(...$args); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Auth/AccessToken.php b/vendor/overtrue/wechat/src/MiniProgram/Auth/AccessToken.php new file mode 100644 index 0000000..af08921 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Auth/AccessToken.php @@ -0,0 +1,39 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Auth; + +use EasyWeChat\Kernel\AccessToken as BaseAccessToken; + +/** + * Class AccessToken. + * + * @author mingyoung + */ +class AccessToken extends BaseAccessToken +{ + /** + * @var string + */ + protected $endpointToGetToken = 'cgi-bin/token'; + + /** + * {@inheritdoc} + */ + protected function getCredentials(): array + { + return [ + 'grant_type' => 'client_credential', + 'appid' => $this->app['config']['app_id'], + 'secret' => $this->app['config']['secret'], + ]; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Auth/Client.php b/vendor/overtrue/wechat/src/MiniProgram/Auth/Client.php new file mode 100644 index 0000000..3131208 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Auth/Client.php @@ -0,0 +1,41 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Auth; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Auth. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * Get session info by code. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function session(string $code) + { + $params = [ + 'appid' => $this->app['config']['app_id'], + 'secret' => $this->app['config']['secret'], + 'js_code' => $code, + 'grant_type' => 'authorization_code', + ]; + + return $this->httpGet('sns/jscode2session', $params); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Auth/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/Auth/ServiceProvider.php new file mode 100644 index 0000000..fcb687b --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Auth/ServiceProvider.php @@ -0,0 +1,32 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Auth; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + !isset($app['access_token']) && $app['access_token'] = function ($app) { + return new AccessToken($app); + }; + + !isset($app['auth']) && $app['auth'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Base/Client.php b/vendor/overtrue/wechat/src/MiniProgram/Base/Client.php new file mode 100644 index 0000000..b4f5bd8 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Base/Client.php @@ -0,0 +1,53 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Base; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * Get paid unionid. + * + * @param string $openid + * @param array $options + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getPaidUnionid($openid, $options = []) + { + return $this->httpGet('wxa/getpaidunionid', compact('openid') + $options); + } + + /** + * Get user phone number by code + * + * @param string $code + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getPhoneNumber($code) + { + return $this->httpPostJson('wxa/business/getuserphonenumber', ['code' => $code]); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Base/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/Base/ServiceProvider.php new file mode 100644 index 0000000..d9aa41b --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Base/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Base; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['base'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Broadcast/Client.php b/vendor/overtrue/wechat/src/MiniProgram/Broadcast/Client.php new file mode 100644 index 0000000..8ab659e --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Broadcast/Client.php @@ -0,0 +1,206 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Broadcast; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author Abbotton + */ +class Client extends BaseClient +{ + /** + * Add broadcast goods. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create(array $goodsInfo) + { + $params = [ + 'goodsInfo' => $goodsInfo, + ]; + + return $this->httpPostJson('wxaapi/broadcast/goods/add', $params); + } + + /** + * Reset audit. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function resetAudit(int $auditId, int $goodsId) + { + $params = [ + 'auditId' => $auditId, + 'goodsId' => $goodsId, + ]; + + return $this->httpPostJson('wxaapi/broadcast/goods/resetaudit', $params); + } + + /** + * Resubmit audit goods. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function resubmitAudit(int $goodsId) + { + $params = [ + 'goodsId' => $goodsId, + ]; + + return $this->httpPostJson('wxaapi/broadcast/goods/audit', $params); + } + + /** + * Delete broadcast goods. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(int $goodsId) + { + $params = [ + 'goodsId' => $goodsId, + ]; + + return $this->httpPostJson('wxaapi/broadcast/goods/delete', $params); + } + + /** + * Update goods info. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(array $goodsInfo) + { + $params = [ + 'goodsInfo' => $goodsInfo, + ]; + + return $this->httpPostJson('wxaapi/broadcast/goods/update', $params); + } + + /** + * Get goods information and review status. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getGoodsWarehouse(array $goodsIdArray) + { + $params = [ + 'goods_ids' => $goodsIdArray, + ]; + + return $this->httpPostJson('wxa/business/getgoodswarehouse', $params); + } + + /** + * Get goods list based on status. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getApproved(array $params) + { + return $this->httpGet('wxaapi/broadcast/goods/getapproved', $params); + } + + /** + * Add goods to the designated live room. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function addGoods(array $params) + { + return $this->httpPost('wxaapi/broadcast/room/addgoods', $params); + } + + /** + * Get Room List. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + * + * @author onekb <1@1kb.ren> + */ + public function getRooms(int $start = 0, int $limit = 10) + { + $params = [ + 'start' => $start, + 'limit' => $limit, + ]; + + return $this->httpPostJson('wxa/business/getliveinfo', $params); + } + + /** + * Get Playback List. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + * + * @author onekb <1@1kb.ren> + */ + public function getPlaybacks(int $roomId, int $start = 0, int $limit = 10) + { + $params = [ + 'action' => 'get_replay', + 'room_id' => $roomId, + 'start' => $start, + 'limit' => $limit, + ]; + + return $this->httpPostJson('wxa/business/getliveinfo', $params); + } + + /** + * Create a live room. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function createLiveRoom(array $params) + { + return $this->httpPost('wxaapi/broadcast/room/create', $params); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Broadcast/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/Broadcast/ServiceProvider.php new file mode 100644 index 0000000..f33f29f --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Broadcast/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Broadcast; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author Abbotton + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['broadcast'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/CustomerService/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/CustomerService/ServiceProvider.php new file mode 100644 index 0000000..cfd3039 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/CustomerService/ServiceProvider.php @@ -0,0 +1,34 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\CustomerService; + +use EasyWeChat\OfficialAccount\CustomerService\Client; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['customer_service'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/DataCube/Client.php b/vendor/overtrue/wechat/src/MiniProgram/DataCube/Client.php new file mode 100644 index 0000000..0430d71 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/DataCube/Client.php @@ -0,0 +1,140 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\DataCube; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * Get summary trend. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + */ + public function summaryTrend(string $from, string $to) + { + return $this->query('datacube/getweanalysisappiddailysummarytrend', $from, $to); + } + + /** + * Get daily visit trend. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + */ + public function dailyVisitTrend(string $from, string $to) + { + return $this->query('datacube/getweanalysisappiddailyvisittrend', $from, $to); + } + + /** + * Get weekly visit trend. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + */ + public function weeklyVisitTrend(string $from, string $to) + { + return $this->query('datacube/getweanalysisappidweeklyvisittrend', $from, $to); + } + + /** + * Get monthly visit trend. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + */ + public function monthlyVisitTrend(string $from, string $to) + { + return $this->query('datacube/getweanalysisappidmonthlyvisittrend', $from, $to); + } + + /** + * Get visit distribution. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + */ + public function visitDistribution(string $from, string $to) + { + return $this->query('datacube/getweanalysisappidvisitdistribution', $from, $to); + } + + /** + * Get daily retain info. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + */ + public function dailyRetainInfo(string $from, string $to) + { + return $this->query('datacube/getweanalysisappiddailyretaininfo', $from, $to); + } + + /** + * Get weekly retain info. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + */ + public function weeklyRetainInfo(string $from, string $to) + { + return $this->query('datacube/getweanalysisappidweeklyretaininfo', $from, $to); + } + + /** + * Get monthly retain info. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + */ + public function monthlyRetainInfo(string $from, string $to) + { + return $this->query('datacube/getweanalysisappidmonthlyretaininfo', $from, $to); + } + + /** + * Get visit page. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + */ + public function visitPage(string $from, string $to) + { + return $this->query('datacube/getweanalysisappidvisitpage', $from, $to); + } + + /** + * Get user portrait. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + */ + public function userPortrait(string $from, string $to) + { + return $this->query('datacube/getweanalysisappiduserportrait', $from, $to); + } + + /** + * Unify query. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function query(string $api, string $from, string $to) + { + $params = [ + 'begin_date' => $from, + 'end_date' => $to, + ]; + + return $this->httpPostJson($api, $params); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/DataCube/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/DataCube/ServiceProvider.php new file mode 100644 index 0000000..258d7c4 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/DataCube/ServiceProvider.php @@ -0,0 +1,28 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\DataCube; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['data_cube'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Encryptor.php b/vendor/overtrue/wechat/src/MiniProgram/Encryptor.php new file mode 100644 index 0000000..28a2dd8 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Encryptor.php @@ -0,0 +1,46 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram; + +use EasyWeChat\Kernel\Encryptor as BaseEncryptor; +use EasyWeChat\Kernel\Exceptions\DecryptException; +use EasyWeChat\Kernel\Support\AES; + +/** + * Class Encryptor. + * + * @author mingyoung + */ +class Encryptor extends BaseEncryptor +{ + /** + * Decrypt data. + * + * @throws \EasyWeChat\Kernel\Exceptions\DecryptException + */ + public function decryptData(string $sessionKey, string $iv, string $encrypted): array + { + $decrypted = AES::decrypt( + base64_decode($encrypted, false), + base64_decode($sessionKey, false), + base64_decode($iv, false) + ); + + $decrypted = json_decode($decrypted, true); + + if (!$decrypted) { + throw new DecryptException('The given payload is invalid.'); + } + + return $decrypted; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Express/Client.php b/vendor/overtrue/wechat/src/MiniProgram/Express/Client.php new file mode 100644 index 0000000..342ebd5 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Express/Client.php @@ -0,0 +1,129 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Express; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author kehuanhuan <1152018701@qq.com> + */ +class Client extends BaseClient +{ + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function listProviders() + { + return $this->httpGet('cgi-bin/express/business/delivery/getall'); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function createWaybill(array $params = []) + { + return $this->httpPostJson('cgi-bin/express/business/order/add', $params); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function deleteWaybill(array $params = []) + { + return $this->httpPostJson('cgi-bin/express/business/order/cancel', $params); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getWaybill(array $params = []) + { + return $this->httpPostJson('cgi-bin/express/business/order/get', $params); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getWaybillTrack(array $params = []) + { + return $this->httpPostJson('cgi-bin/express/business/path/get', $params); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getBalance(string $deliveryId, string $bizId) + { + return $this->httpPostJson('cgi-bin/express/business/quota/get', [ + 'delivery_id' => $deliveryId, + 'biz_id' => $bizId, + ]); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getPrinter() + { + return $this->httpPostJson('cgi-bin/express/business/printer/getall'); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function bindPrinter(string $openid) + { + return $this->httpPostJson('cgi-bin/express/business/printer/update', [ + 'update_type' => 'bind', + 'openid' => $openid, + ]); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function unbindPrinter(string $openid) + { + return $this->httpPostJson('cgi-bin/express/business/printer/update', [ + 'update_type' => 'unbind', + 'openid' => $openid, + ]); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Express/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/Express/ServiceProvider.php new file mode 100644 index 0000000..4794094 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Express/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Express; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author kehuanhuan <1152018701@qq.com> + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['express'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Live/Client.php b/vendor/overtrue/wechat/src/MiniProgram/Live/Client.php new file mode 100644 index 0000000..de0b034 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Live/Client.php @@ -0,0 +1,58 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Live; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author onekb <1@1kb.ren> + */ +class Client extends BaseClient +{ + /** + * Get Room List. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @deprecated This method has been merged into `\EasyWeChat\MiniProgram\Broadcast` + */ + public function getRooms(int $start = 0, int $limit = 10) + { + $params = [ + 'start' => $start, + 'limit' => $limit, + ]; + + return $this->httpPostJson('wxa/business/getliveinfo', $params); + } + + /** + * Get Playback List. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @deprecated This method has been merged into `\EasyWeChat\MiniProgram\Broadcast` + */ + public function getPlaybacks(int $roomId, int $start = 0, int $limit = 10) + { + $params = [ + 'action' => 'get_replay', + 'room_id' => $roomId, + 'start' => $start, + 'limit' => $limit, + ]; + + return $this->httpPostJson('wxa/business/getliveinfo', $params); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Live/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/Live/ServiceProvider.php new file mode 100644 index 0000000..6d6dff6 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Live/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Live; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author onekb <1@1kb.ren> + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['live'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Mall/CartClient.php b/vendor/overtrue/wechat/src/MiniProgram/Mall/CartClient.php new file mode 100644 index 0000000..ddf0587 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Mall/CartClient.php @@ -0,0 +1,87 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Mall; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author mingyoung + */ +class CartClient extends BaseClient +{ + /** + * 导入收藏. + * + * @param array $params + * @param bool $isTest + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function add($params, $isTest = false) + { + return $this->httpPostJson('mall/addshoppinglist', $params, ['is_test' => (int) $isTest]); + } + + /** + * 查询用户收藏信息. + * + * @param array $params + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function query($params) + { + return $this->httpPostJson('mall/queryshoppinglist', $params, ['type' => 'batchquery']); + } + + /** + * 查询用户收藏信息. + * + * @param array $params + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function queryByPage($params) + { + return $this->httpPostJson('mall/queryshoppinglist', $params, ['type' => 'getbypage']); + } + + /** + * 删除收藏. + * + * @param string $openid + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete($openid, array $products = []) + { + if (empty($products)) { + return $this->httpPostJson('mall/deletebizallshoppinglist', ['user_open_id' => $openid]); + } + + return $this->httpPostJson('mall/deleteshoppinglist', ['user_open_id' => $openid, 'sku_product_list' => $products]); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Mall/ForwardsMall.php b/vendor/overtrue/wechat/src/MiniProgram/Mall/ForwardsMall.php new file mode 100644 index 0000000..f727b17 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Mall/ForwardsMall.php @@ -0,0 +1,48 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Mall; + +/** + * Class Application. + * + * @author mingyoung + * + * @property \EasyWeChat\MiniProgram\Mall\OrderClient $order + * @property \EasyWeChat\MiniProgram\Mall\CartClient $cart + * @property \EasyWeChat\MiniProgram\Mall\ProductClient $product + * @property \EasyWeChat\MiniProgram\Mall\MediaClient $media + */ +class ForwardsMall +{ + /** + * @var \EasyWeChat\Kernel\ServiceContainer + */ + protected $app; + + /** + * @param \EasyWeChat\Kernel\ServiceContainer $app + */ + public function __construct($app) + { + $this->app = $app; + } + + /** + * @param string $property + * + * @return mixed + */ + public function __get($property) + { + return $this->app["mall.{$property}"]; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Mall/MediaClient.php b/vendor/overtrue/wechat/src/MiniProgram/Mall/MediaClient.php new file mode 100644 index 0000000..a3827bc --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Mall/MediaClient.php @@ -0,0 +1,37 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Mall; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author mingyoung + */ +class MediaClient extends BaseClient +{ + /** + * 更新或导入媒体信息. + * + * @param array $params + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function import($params) + { + return $this->httpPostJson('mall/importmedia', $params); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Mall/OrderClient.php b/vendor/overtrue/wechat/src/MiniProgram/Mall/OrderClient.php new file mode 100644 index 0000000..9879695 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Mall/OrderClient.php @@ -0,0 +1,75 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Mall; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author mingyoung + */ +class OrderClient extends BaseClient +{ + /** + * 导入订单. + * + * @param array $params + * @param bool $isHistory + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function add($params, $isHistory = false) + { + return $this->httpPostJson('mall/importorder', $params, ['action' => 'add-order', 'is_history' => (int) $isHistory]); + } + + /** + * 导入订单. + * + * @param array $params + * @param bool $isHistory + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update($params, $isHistory = false) + { + return $this->httpPostJson('mall/importorder', $params, ['action' => 'update-order', 'is_history' => (int) $isHistory]); + } + + /** + * 删除订单. + * + * @param string $openid + * @param string $orderId + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete($openid, $orderId) + { + $params = [ + 'user_open_id' => $openid, + 'order_id' => $orderId, + ]; + + return $this->httpPostJson('mall/deleteorder', $params); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Mall/ProductClient.php b/vendor/overtrue/wechat/src/MiniProgram/Mall/ProductClient.php new file mode 100644 index 0000000..f7fbfd0 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Mall/ProductClient.php @@ -0,0 +1,68 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Mall; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author mingyoung + */ +class ProductClient extends BaseClient +{ + /** + * 更新或导入物品信息. + * + * @param array $params + * @param bool $isTest + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function import($params, $isTest = false) + { + return $this->httpPostJson('mall/importproduct', $params, ['is_test' => (int) $isTest]); + } + + /** + * 查询物品信息. + * + * @param array $params + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function query($params) + { + return $this->httpPostJson('mall/queryproduct', $params, ['type' => 'batchquery']); + } + + /** + * 小程序的物品是否可被搜索. + * + * @param bool $value + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setSearchable($value) + { + return $this->httpPostJson('mall/brandmanage', ['can_be_search' => $value], ['action' => 'set_biz_can_be_search']); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Mall/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/Mall/ServiceProvider.php new file mode 100644 index 0000000..964395b --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Mall/ServiceProvider.php @@ -0,0 +1,44 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Mall; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['mall'] = function ($app) { + return new ForwardsMall($app); + }; + + $app['mall.order'] = function ($app) { + return new OrderClient($app); + }; + + $app['mall.cart'] = function ($app) { + return new CartClient($app); + }; + + $app['mall.product'] = function ($app) { + return new ProductClient($app); + }; + + $app['mall.media'] = function ($app) { + return new MediaClient($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/NearbyPoi/Client.php b/vendor/overtrue/wechat/src/MiniProgram/NearbyPoi/Client.php new file mode 100644 index 0000000..b5e17de --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/NearbyPoi/Client.php @@ -0,0 +1,110 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\NearbyPoi; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; + +/** + * Class Client. + * + * @author joyeekk + */ +class Client extends BaseClient +{ + /** + * Add nearby poi. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function add(array $params) + { + $params = array_merge([ + 'is_comm_nearby' => '1', + 'poi_id' => '', + ], $params); + + return $this->httpPostJson('wxa/addnearbypoi', $params); + } + + /** + * Update nearby poi. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(string $poiId, array $params) + { + $params = array_merge([ + 'is_comm_nearby' => '1', + 'poi_id' => $poiId, + ], $params); + + return $this->httpPostJson('wxa/addnearbypoi', $params); + } + + /** + * Delete nearby poi. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(string $poiId) + { + return $this->httpPostJson('wxa/delnearbypoi', [ + 'poi_id' => $poiId, + ]); + } + + /** + * Get nearby poi list. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function list(int $page, int $pageRows) + { + return $this->httpGet('wxa/getnearbypoilist', [ + 'page' => $page, + 'page_rows' => $pageRows, + ]); + } + + /** + * Set nearby poi show status. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setVisibility(string $poiId, int $status) + { + if (!in_array($status, [0, 1], true)) { + throw new InvalidArgumentException('status should be 0 or 1.'); + } + + return $this->httpPostJson('wxa/setnearbypoishowstatus', [ + 'poi_id' => $poiId, + 'status' => $status, + ]); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/NearbyPoi/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/NearbyPoi/ServiceProvider.php new file mode 100644 index 0000000..cb15bb3 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/NearbyPoi/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\NearbyPoi; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author joyeekk + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['nearby_poi'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/OCR/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/OCR/ServiceProvider.php new file mode 100644 index 0000000..80e0001 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/OCR/ServiceProvider.php @@ -0,0 +1,34 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\OCR; + +use EasyWeChat\OfficialAccount\OCR\Client; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author joyeekk + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['ocr'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/OpenData/Client.php b/vendor/overtrue/wechat/src/MiniProgram/OpenData/Client.php new file mode 100644 index 0000000..c945f8c --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/OpenData/Client.php @@ -0,0 +1,81 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\OpenData; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author tianyong90 <412039588@qq.com> + */ +class Client extends BaseClient +{ + /** + * removeUserStorage. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function removeUserStorage(string $openid, string $sessionKey, array $key) + { + $data = ['key' => $key]; + $query = [ + 'openid' => $openid, + 'sig_method' => 'hmac_sha256', + 'signature' => hash_hmac('sha256', json_encode($data), $sessionKey), + ]; + + return $this->httpPostJson('wxa/remove_user_storage', $data, $query); + } + + /** + * setUserStorage. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setUserStorage(string $openid, string $sessionKey, array $kvList) + { + $kvList = $this->formatKVLists($kvList); + + $data = ['kv_list' => $kvList]; + $query = [ + 'openid' => $openid, + 'sig_method' => 'hmac_sha256', + 'signature' => hash_hmac('sha256', json_encode($data), $sessionKey), + ]; + + return $this->httpPostJson('wxa/set_user_storage', $data, $query); + } + + /** + * @return array + */ + protected function formatKVLists(array $params) + { + $formatted = []; + + foreach ($params as $name => $value) { + $formatted[] = [ + 'key' => $name, + 'value' => strval($value), + ]; + } + + return $formatted; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/OpenData/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/OpenData/ServiceProvider.php new file mode 100644 index 0000000..3ea1287 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/OpenData/ServiceProvider.php @@ -0,0 +1,28 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\OpenData; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['open_data'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Plugin/Client.php b/vendor/overtrue/wechat/src/MiniProgram/Plugin/Client.php new file mode 100644 index 0000000..cd885cc --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Plugin/Client.php @@ -0,0 +1,67 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Plugin; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * @param string $appId + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function apply($appId) + { + return $this->httpPostJson('wxa/plugin', [ + 'action' => 'apply', + 'plugin_appid' => $appId, + ]); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function list() + { + return $this->httpPostJson('wxa/plugin', [ + 'action' => 'list', + ]); + } + + /** + * @param string $appId + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function unbind($appId) + { + return $this->httpPostJson('wxa/plugin', [ + 'action' => 'unbind', + 'plugin_appid' => $appId, + ]); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Plugin/DevClient.php b/vendor/overtrue/wechat/src/MiniProgram/Plugin/DevClient.php new file mode 100644 index 0000000..663501b --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Plugin/DevClient.php @@ -0,0 +1,86 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Plugin; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class DevClient. + * + * @author her-cat + */ +class DevClient extends BaseClient +{ + /** + * Get users. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getUsers(int $page = 1, int $size = 10) + { + return $this->httpPostJson('wxa/devplugin', [ + 'action' => 'dev_apply_list', + 'page' => $page, + 'num' => $size, + ]); + } + + /** + * Agree to use plugin. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function agree(string $appId) + { + return $this->httpPostJson('wxa/devplugin', [ + 'action' => 'dev_agree', + 'appid' => $appId, + ]); + } + + /** + * Refuse to use plugin. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function refuse(string $reason) + { + return $this->httpPostJson('wxa/devplugin', [ + 'action' => 'dev_refuse', + 'reason' => $reason, + ]); + } + + /** + * Delete rejected applications. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete() + { + return $this->httpPostJson('wxa/devplugin', [ + 'action' => 'dev_delete', + ]); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Plugin/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/Plugin/ServiceProvider.php new file mode 100644 index 0000000..d2c6837 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Plugin/ServiceProvider.php @@ -0,0 +1,40 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Plugin; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * Registers services on the given container. + * + * This method should only be used to configure services and parameters. + * It should not get services. + */ + public function register(Container $app) + { + $app['plugin'] = function ($app) { + return new Client($app); + }; + + $app['plugin_dev'] = function ($app) { + return new DevClient($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/RealtimeLog/Client.php b/vendor/overtrue/wechat/src/MiniProgram/RealtimeLog/Client.php new file mode 100644 index 0000000..eda114f --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/RealtimeLog/Client.php @@ -0,0 +1,41 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\RealtimeLog; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author her-cat + */ +class Client extends BaseClient +{ + /** + * Real time log query. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function search(string $date, int $beginTime, int $endTime, array $options = []) + { + $params = [ + 'date' => $date, + 'begintime' => $beginTime, + 'endtime' => $endTime, + ]; + + return $this->httpGet('wxaapi/userlog/userlog_search', $params + $options); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/RealtimeLog/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/RealtimeLog/ServiceProvider.php new file mode 100644 index 0000000..9a28141 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/RealtimeLog/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\RealtimeLog; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author her-cat + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc} + */ + public function register(Container $app) + { + $app['realtime_log'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Search/Client.php b/vendor/overtrue/wechat/src/MiniProgram/Search/Client.php new file mode 100644 index 0000000..81bc42d --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Search/Client.php @@ -0,0 +1,35 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Search; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author her-cat + */ +class Client extends BaseClient +{ + /** + * Submit applet page URL and parameter information. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function submitPage(array $pages) + { + return $this->httpPostJson('wxa/search/wxaapi_submitpages', compact('pages')); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Search/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/Search/ServiceProvider.php new file mode 100644 index 0000000..85cb569 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Search/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Search; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author her-cat + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['search'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Server/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/Server/ServiceProvider.php new file mode 100644 index 0000000..0586bb9 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Server/ServiceProvider.php @@ -0,0 +1,42 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Server; + +use EasyWeChat\MiniProgram\Encryptor; +use EasyWeChat\OfficialAccount\Server\Guard; +use EasyWeChat\OfficialAccount\Server\Handlers\EchoStrHandler; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + !isset($app['encryptor']) && $app['encryptor'] = function ($app) { + return new Encryptor( + $app['config']['app_id'], + $app['config']['token'], + $app['config']['aes_key'] + ); + }; + + !isset($app['server']) && $app['server'] = function ($app) { + $guard = new Guard($app); + $guard->push(new EchoStrHandler($app)); + + return $guard; + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Soter/Client.php b/vendor/overtrue/wechat/src/MiniProgram/Soter/Client.php new file mode 100644 index 0000000..287fd22 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Soter/Client.php @@ -0,0 +1,37 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Soter; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author her-cat + */ +class Client extends BaseClient +{ + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function verifySignature(string $openid, string $json, string $signature) + { + return $this->httpPostJson('cgi-bin/soter/verify_signature', [ + 'openid' => $openid, + 'json_string' => $json, + 'json_signature' => $signature, + ]); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/Soter/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/Soter/ServiceProvider.php new file mode 100644 index 0000000..c8520db --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/Soter/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\Soter; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author her-cat + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc} + */ + public function register(Container $app) + { + $app['soter'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/SubscribeMessage/Client.php b/vendor/overtrue/wechat/src/MiniProgram/SubscribeMessage/Client.php new file mode 100644 index 0000000..02b3de9 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/SubscribeMessage/Client.php @@ -0,0 +1,192 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\SubscribeMessage; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use ReflectionClass; + +/** + * Class Client. + * + * @author hugo + */ +class Client extends BaseClient +{ + /** + * {@inheritdoc}. + */ + protected $message = [ + 'touser' => '', + 'template_id' => '', + 'page' => '', + 'data' => [], + ]; + + /** + * {@inheritdoc}. + */ + protected $required = ['touser', 'template_id', 'data']; + + /** + * Send a template message. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function send(array $data = []) + { + $params = $this->formatMessage($data); + + $this->restoreMessage(); + + return $this->httpPostJson('cgi-bin/message/subscribe/send', $params); + } + + /** + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + protected function formatMessage(array $data = []) + { + $params = array_merge($this->message, $data); + + foreach ($params as $key => $value) { + if (in_array($key, $this->required, true) && empty($value) && empty($this->message[$key])) { + throw new InvalidArgumentException(sprintf('Attribute "%s" can not be empty!', $key)); + } + + $params[$key] = empty($value) ? $this->message[$key] : $value; + } + + foreach ($params['data'] as $key => $value) { + if (is_array($value)) { + if (\array_key_exists('value', $value)) { + $params['data'][$key] = ['value' => $value['value']]; + + continue; + } + + if (count($value) >= 1) { + $value = [ + 'value' => $value[0], +// 'color' => $value[1],// color unsupported + ]; + } + } else { + $value = [ + 'value' => strval($value), + ]; + } + + $params['data'][$key] = $value; + } + + return $params; + } + + /** + * Restore message. + */ + protected function restoreMessage() + { + $this->message = (new ReflectionClass(static::class))->getDefaultProperties()['message']; + } + + /** + * Combine templates and add them to your personal template library under your account. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function addTemplate(string $tid, array $kidList, string $sceneDesc = null) + { + $sceneDesc = $sceneDesc ?? ''; + $data = \compact('tid', 'kidList', 'sceneDesc'); + + return $this->httpPost('wxaapi/newtmpl/addtemplate', $data); + } + + /** + * Delete personal template under account. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function deleteTemplate(string $id) + { + return $this->httpPost('wxaapi/newtmpl/deltemplate', ['priTmplId' => $id]); + } + + /** + * Get keyword list under template title. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getTemplateKeywords(string $tid) + { + return $this->httpGet('wxaapi/newtmpl/getpubtemplatekeywords', compact('tid')); + } + + /** + * Get the title of the public template under the category to which the account belongs. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getTemplateTitles(array $ids, int $start = 0, int $limit = 30) + { + $ids = \implode(',', $ids); + $query = \compact('ids', 'start', 'limit'); + + return $this->httpGet('wxaapi/newtmpl/getpubtemplatetitles', $query); + } + + /** + * Get list of personal templates under the current account. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getTemplates() + { + return $this->httpGet('wxaapi/newtmpl/gettemplate'); + } + + /** + * Get the category of the applet account. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getCategory() + { + return $this->httpGet('wxaapi/newtmpl/getcategory'); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/SubscribeMessage/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/SubscribeMessage/ServiceProvider.php new file mode 100644 index 0000000..726b3ed --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/SubscribeMessage/ServiceProvider.php @@ -0,0 +1,28 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\SubscribeMessage; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['subscribe_message'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/TemplateMessage/Client.php b/vendor/overtrue/wechat/src/MiniProgram/TemplateMessage/Client.php new file mode 100644 index 0000000..6c6da31 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/TemplateMessage/Client.php @@ -0,0 +1,101 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\TemplateMessage; + +use EasyWeChat\OfficialAccount\TemplateMessage\Client as BaseClient; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + public const API_SEND = 'cgi-bin/message/wxopen/template/send'; + + /** + * {@inheritdoc}. + */ + protected $message = [ + 'touser' => '', + 'template_id' => '', + 'page' => '', + 'form_id' => '', + 'data' => [], + 'emphasis_keyword' => '', + ]; + + /** + * {@inheritdoc}. + */ + protected $required = ['touser', 'template_id', 'form_id']; + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function list(int $offset, int $count) + { + return $this->httpPostJson('cgi-bin/wxopen/template/library/list', compact('offset', 'count')); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(string $id) + { + return $this->httpPostJson('cgi-bin/wxopen/template/library/get', compact('id')); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function add(string $id, array $keyword) + { + return $this->httpPostJson('cgi-bin/wxopen/template/add', [ + 'id' => $id, + 'keyword_id_list' => $keyword, + ]); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(string $templateId) + { + return $this->httpPostJson('cgi-bin/wxopen/template/del', [ + 'template_id' => $templateId, + ]); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getTemplates(int $offset, int $count) + { + return $this->httpPostJson('cgi-bin/wxopen/template/list', compact('offset', 'count')); + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/TemplateMessage/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/TemplateMessage/ServiceProvider.php new file mode 100644 index 0000000..776a15e --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/TemplateMessage/ServiceProvider.php @@ -0,0 +1,28 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\TemplateMessage; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['template_message'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/UniformMessage/Client.php b/vendor/overtrue/wechat/src/MiniProgram/UniformMessage/Client.php new file mode 100644 index 0000000..56a9132 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/UniformMessage/Client.php @@ -0,0 +1,140 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\UniformMessage; + +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\OfficialAccount\TemplateMessage\Client as BaseClient; + +class Client extends BaseClient +{ + public const API_SEND = 'cgi-bin/message/wxopen/template/uniform_send'; + + /** + * {@inheritdoc}. + * + * @var array + */ + protected $message = [ + 'touser' => '', + ]; + + /** + * Weapp Attributes. + * + * @var array + */ + protected $weappMessage = [ + 'template_id' => '', + 'page' => '', + 'form_id' => '', + 'data' => [], + 'emphasis_keyword' => '', + ]; + + /** + * Official account attributes. + * + * @var array + */ + protected $mpMessage = [ + 'appid' => '', + 'template_id' => '', + 'url' => '', + 'miniprogram' => [], + 'data' => [], + ]; + + /** + * Required attributes. + * + * @var array + */ + protected $required = ['touser', 'template_id', 'form_id', 'miniprogram', 'appid']; + + /** + * @return array + * + * @throws InvalidArgumentException + */ + protected function formatMessage(array $data = []) + { + $params = array_merge($this->message, $data); + + if (empty($params['touser'])) { + throw new InvalidArgumentException(sprintf('Attribute "touser" can not be empty!')); + } + + if (!empty($params['weapp_template_msg'])) { + $params['weapp_template_msg'] = $this->formatWeappMessage($params['weapp_template_msg']); + } + + if (!empty($params['mp_template_msg'])) { + $params['mp_template_msg'] = $this->formatMpMessage($params['mp_template_msg']); + } + + return $params; + } + + /** + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + protected function formatWeappMessage(array $data = []) + { + $params = $this->baseFormat($data, $this->weappMessage); + + $params['data'] = $this->formatData($params['data'] ?? []); + + return $params; + } + + /** + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + protected function formatMpMessage(array $data = []) + { + $params = $this->baseFormat($data, $this->mpMessage); + + if (empty($params['miniprogram']['appid'])) { + $params['miniprogram']['appid'] = $this->app['config']['app_id']; + } + + $params['data'] = $this->formatData($params['data'] ?? []); + + return $params; + } + + /** + * @param array $data + * @param array $default + * + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + protected function baseFormat($data = [], $default = []) + { + $params = array_merge($default, $data); + foreach ($params as $key => $value) { + if (in_array($key, $this->required, true) && empty($value) && empty($default[$key])) { + throw new InvalidArgumentException(sprintf('Attribute "%s" can not be empty!', $key)); + } + + $params[$key] = empty($value) ? $default[$key] : $value; + } + + return $params; + } +} diff --git a/vendor/overtrue/wechat/src/MiniProgram/UniformMessage/ServiceProvider.php b/vendor/overtrue/wechat/src/MiniProgram/UniformMessage/ServiceProvider.php new file mode 100644 index 0000000..0e86db3 --- /dev/null +++ b/vendor/overtrue/wechat/src/MiniProgram/UniformMessage/ServiceProvider.php @@ -0,0 +1,28 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\MiniProgram\UniformMessage; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['uniform_message'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Application.php b/vendor/overtrue/wechat/src/OfficialAccount/Application.php new file mode 100644 index 0000000..75cb1dd --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Application.php @@ -0,0 +1,93 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount; + +use EasyWeChat\BasicService; +use EasyWeChat\Kernel\ServiceContainer; + +/** + * Class Application. + * + * @author overtrue + * + * @property \EasyWeChat\BasicService\Media\Client $media + * @property \EasyWeChat\BasicService\Url\Client $url + * @property \EasyWeChat\BasicService\QrCode\Client $qrcode + * @property \EasyWeChat\BasicService\Jssdk\Client $jssdk + * @property \EasyWeChat\OfficialAccount\Auth\AccessToken $access_token + * @property \EasyWeChat\OfficialAccount\Server\Guard $server + * @property \EasyWeChat\OfficialAccount\User\UserClient $user + * @property \EasyWeChat\OfficialAccount\User\TagClient $user_tag + * @property \EasyWeChat\OfficialAccount\Menu\Client $menu + * @property \EasyWeChat\OfficialAccount\TemplateMessage\Client $template_message + * @property \EasyWeChat\OfficialAccount\SubscribeMessage\Client $subscribe_message + * @property \EasyWeChat\OfficialAccount\Material\Client $material + * @property \EasyWeChat\OfficialAccount\CustomerService\Client $customer_service + * @property \EasyWeChat\OfficialAccount\CustomerService\SessionClient $customer_service_session + * @property \EasyWeChat\OfficialAccount\Semantic\Client $semantic + * @property \EasyWeChat\OfficialAccount\DataCube\Client $data_cube + * @property \EasyWeChat\OfficialAccount\AutoReply\Client $auto_reply + * @property \EasyWeChat\OfficialAccount\Broadcasting\Client $broadcasting + * @property \EasyWeChat\OfficialAccount\Card\Card $card + * @property \EasyWeChat\OfficialAccount\Device\Client $device + * @property \EasyWeChat\OfficialAccount\ShakeAround\ShakeAround $shake_around + * @property \EasyWeChat\OfficialAccount\POI\Client $poi + * @property \EasyWeChat\OfficialAccount\Store\Client $store + * @property \EasyWeChat\OfficialAccount\Base\Client $base + * @property \EasyWeChat\OfficialAccount\Comment\Client $comment + * @property \EasyWeChat\OfficialAccount\OCR\Client $ocr + * @property \EasyWeChat\OfficialAccount\Goods\Client $goods + * @property \Overtrue\Socialite\Providers\WeChatProvider $oauth + * @property \EasyWeChat\OfficialAccount\WiFi\Client $wifi + * @property \EasyWeChat\OfficialAccount\WiFi\CardClient $wifi_card + * @property \EasyWeChat\OfficialAccount\WiFi\DeviceClient $wifi_device + * @property \EasyWeChat\OfficialAccount\WiFi\ShopClient $wifi_shop + * @property \EasyWeChat\OfficialAccount\Guide\Client $guide + */ +class Application extends ServiceContainer +{ + /** + * @var array + */ + protected $providers = [ + Auth\ServiceProvider::class, + Server\ServiceProvider::class, + User\ServiceProvider::class, + OAuth\ServiceProvider::class, + Menu\ServiceProvider::class, + TemplateMessage\ServiceProvider::class, + SubscribeMessage\ServiceProvider::class, + Material\ServiceProvider::class, + CustomerService\ServiceProvider::class, + Semantic\ServiceProvider::class, + DataCube\ServiceProvider::class, + POI\ServiceProvider::class, + AutoReply\ServiceProvider::class, + Broadcasting\ServiceProvider::class, + Card\ServiceProvider::class, + Device\ServiceProvider::class, + ShakeAround\ServiceProvider::class, + Store\ServiceProvider::class, + Comment\ServiceProvider::class, + Base\ServiceProvider::class, + OCR\ServiceProvider::class, + Goods\ServiceProvider::class, + WiFi\ServiceProvider::class, + // Base services + BasicService\QrCode\ServiceProvider::class, + BasicService\Media\ServiceProvider::class, + BasicService\Url\ServiceProvider::class, + BasicService\Jssdk\ServiceProvider::class, + // Append Guide Interface + Guide\ServiceProvider::class, + ]; +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Auth/AccessToken.php b/vendor/overtrue/wechat/src/OfficialAccount/Auth/AccessToken.php new file mode 100644 index 0000000..55409ba --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Auth/AccessToken.php @@ -0,0 +1,36 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Auth; + +use EasyWeChat\Kernel\AccessToken as BaseAccessToken; + +/** + * Class AuthorizerAccessToken. + * + * @author overtrue + */ +class AccessToken extends BaseAccessToken +{ + /** + * @var string + */ + protected $endpointToGetToken = 'cgi-bin/token'; + + protected function getCredentials(): array + { + return [ + 'grant_type' => 'client_credential', + 'appid' => $this->app['config']['app_id'], + 'secret' => $this->app['config']['secret'], + ]; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Auth/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/Auth/ServiceProvider.php new file mode 100644 index 0000000..e748730 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Auth/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Auth; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + !isset($app['access_token']) && $app['access_token'] = function ($app) { + return new AccessToken($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/AutoReply/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/AutoReply/Client.php new file mode 100644 index 0000000..6d2e4d6 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/AutoReply/Client.php @@ -0,0 +1,34 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\AutoReply; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author overtrue + */ +class Client extends BaseClient +{ + /** + * Get current auto reply settings. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function current() + { + return $this->httpGet('cgi-bin/get_current_autoreply_info'); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/AutoReply/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/AutoReply/ServiceProvider.php new file mode 100644 index 0000000..4377550 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/AutoReply/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\AutoReply; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['auto_reply'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Base/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/Base/Client.php new file mode 100644 index 0000000..3d2a3e9 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Base/Client.php @@ -0,0 +1,81 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Base; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * Clear quota. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function clearQuota() + { + $params = [ + 'appid' => $this->app['config']['app_id'], + ]; + + return $this->httpPostJson('cgi-bin/clear_quota', $params); + } + + /** + * Get wechat callback ip. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getValidIps() + { + return $this->httpGet('cgi-bin/getcallbackip'); + } + + /** + * Check the callback address network. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function checkCallbackUrl(string $action = 'all', string $operator = 'DEFAULT') + { + if (!in_array($action, ['dns', 'ping', 'all'], true)) { + throw new InvalidArgumentException('The action must be dns, ping, all.'); + } + + $operator = strtoupper($operator); + + if (!in_array($operator, ['CHINANET', 'UNICOM', 'CAP', 'DEFAULT'], true)) { + throw new InvalidArgumentException('The operator must be CHINANET, UNICOM, CAP, DEFAULT.'); + } + + $params = [ + 'action' => $action, + 'check_operator' => $operator, + ]; + + return $this->httpPostJson('cgi-bin/callback/check', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Base/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/Base/ServiceProvider.php new file mode 100644 index 0000000..409593d --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Base/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Base; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['base'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Broadcasting/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/Broadcasting/Client.php new file mode 100644 index 0000000..27515c3 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Broadcasting/Client.php @@ -0,0 +1,359 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Broadcasting; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Contracts\MessageInterface; +use EasyWeChat\Kernel\Exceptions\RuntimeException; +use EasyWeChat\Kernel\Messages\Card; +use EasyWeChat\Kernel\Messages\Image; +use EasyWeChat\Kernel\Messages\Media; +use EasyWeChat\Kernel\Messages\Text; +use EasyWeChat\Kernel\Support\Arr; + +/** + * Class Client. + * + * @method \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string previewTextByName($text, $name); + * @method \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string previewNewsByName($mediaId, $name); + * @method \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string previewVoiceByName($mediaId, $name); + * @method \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string previewImageByName($mediaId, $name); + * @method \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string previewVideoByName($message, $name); + * @method \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string previewCardByName($cardId, $name); + * + * @author overtrue + */ +class Client extends BaseClient +{ + public const PREVIEW_BY_OPENID = 'touser'; + public const PREVIEW_BY_NAME = 'towxname'; + + /** + * Send a message. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function send(array $message) + { + if (empty($message['filter']) && empty($message['touser'])) { + throw new RuntimeException('The message reception object is not specified'); + } + + $api = Arr::get($message, 'touser') ? 'cgi-bin/message/mass/send' : 'cgi-bin/message/mass/sendall'; + + return $this->httpPostJson($api, $message); + } + + /** + * Preview a message. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function preview(array $message) + { + return $this->httpPostJson('cgi-bin/message/mass/preview', $message); + } + + /** + * Delete a broadcast. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(string $msgId, int $index = 0) + { + $options = [ + 'msg_id' => $msgId, + 'article_idx' => $index, + ]; + + return $this->httpPostJson('cgi-bin/message/mass/delete', $options); + } + + /** + * Get a broadcast status. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function status(string $msgId) + { + $options = [ + 'msg_id' => $msgId, + ]; + + return $this->httpPostJson('cgi-bin/message/mass/get', $options); + } + + /** + * Send a text message. + * + * @param mixed $reception + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function sendText(string $message, $reception = null, array $attributes = []) + { + return $this->sendMessage(new Text($message), $reception, $attributes); + } + + /** + * Send a news message. + * + * @param mixed $reception + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function sendNews(string $mediaId, $reception = null, array $attributes = []) + { + return $this->sendMessage(new Media($mediaId, 'mpnews'), $reception, $attributes); + } + + /** + * Send a voice message. + * + * @param mixed $reception + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function sendVoice(string $mediaId, $reception = null, array $attributes = []) + { + return $this->sendMessage(new Media($mediaId, 'voice'), $reception, $attributes); + } + + /** + * Send a image message. + * + * @param mixed $reception + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function sendImage(string $mediaId, $reception = null, array $attributes = []) + { + return $this->sendMessage(new Image($mediaId), $reception, $attributes); + } + + /** + * Send a video message. + * + * @param mixed $reception + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function sendVideo(string $mediaId, $reception = null, array $attributes = []) + { + return $this->sendMessage(new Media($mediaId, 'mpvideo'), $reception, $attributes); + } + + /** + * Send a card message. + * + * @param mixed $reception + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function sendCard(string $cardId, $reception = null, array $attributes = []) + { + return $this->sendMessage(new Card($cardId), $reception, $attributes); + } + + /** + * Preview a text message. + * + * @param string $message message + * @param string $reception + * @param string $method + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function previewText(string $message, $reception, $method = self::PREVIEW_BY_OPENID) + { + return $this->previewMessage(new Text($message), $reception, $method); + } + + /** + * Preview a news message. + * + * @param string $mediaId message + * @param string $reception + * @param string $method + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function previewNews(string $mediaId, $reception, $method = self::PREVIEW_BY_OPENID) + { + return $this->previewMessage(new Media($mediaId, 'mpnews'), $reception, $method); + } + + /** + * Preview a voice message. + * + * @param string $mediaId message + * @param string $reception + * @param string $method + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function previewVoice(string $mediaId, $reception, $method = self::PREVIEW_BY_OPENID) + { + return $this->previewMessage(new Media($mediaId, 'voice'), $reception, $method); + } + + /** + * Preview a image message. + * + * @param string $mediaId message + * @param string $reception + * @param string $method + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function previewImage(string $mediaId, $reception, $method = self::PREVIEW_BY_OPENID) + { + return $this->previewMessage(new Image($mediaId), $reception, $method); + } + + /** + * Preview a video message. + * + * @param string $mediaId message + * @param string $reception + * @param string $method + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function previewVideo(string $mediaId, $reception, $method = self::PREVIEW_BY_OPENID) + { + return $this->previewMessage(new Media($mediaId, 'mpvideo'), $reception, $method); + } + + /** + * Preview a card message. + * + * @param string $cardId message + * @param string $reception + * @param string $method + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function previewCard(string $cardId, $reception, $method = self::PREVIEW_BY_OPENID) + { + return $this->previewMessage(new Card($cardId), $reception, $method); + } + + /** + * @param string $method + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function previewMessage(MessageInterface $message, string $reception, $method = self::PREVIEW_BY_OPENID) + { + $message = (new MessageBuilder())->message($message)->buildForPreview($method, $reception); + + return $this->preview($message); + } + + /** + * @param mixed $reception + * @param array $attributes + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function sendMessage(MessageInterface $message, $reception = null, $attributes = []) + { + $message = (new MessageBuilder())->message($message)->with($attributes)->toAll(); + + if (\is_int($reception)) { + $message->toTag($reception); + } elseif (\is_array($reception)) { + $message->toUsers($reception); + } + + return $this->send($message->build()); + } + + /** + * @codeCoverageIgnore + * + * @param string $method + * @param array $args + * + * @return mixed + */ + public function __call($method, $args) + { + if (strpos($method, 'ByName') > 0) { + $method = strstr($method, 'ByName', true); + + if (method_exists($this, $method)) { + array_push($args, self::PREVIEW_BY_NAME); + + return $this->$method(...$args); + } + } + + throw new \BadMethodCallException(sprintf('Method %s not exists.', $method)); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Broadcasting/MessageBuilder.php b/vendor/overtrue/wechat/src/OfficialAccount/Broadcasting/MessageBuilder.php new file mode 100644 index 0000000..9bf80f1 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Broadcasting/MessageBuilder.php @@ -0,0 +1,143 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Broadcasting; + +use EasyWeChat\Kernel\Contracts\MessageInterface; +use EasyWeChat\Kernel\Exceptions\RuntimeException; + +/** + * Class MessageBuilder. + * + * @author overtrue + */ +class MessageBuilder +{ + /** + * @var array + */ + protected $to = []; + + /** + * @var \EasyWeChat\Kernel\Contracts\MessageInterface + */ + protected $message; + + /** + * @var array + */ + protected $attributes = []; + + /** + * Set message. + * + * @return $this + */ + public function message(MessageInterface $message) + { + $this->message = $message; + + return $this; + } + + /** + * Set target user or group. + * + * @return $this + */ + public function to(array $to) + { + $this->to = $to; + + return $this; + } + + /** + * @return \EasyWeChat\OfficialAccount\Broadcasting\MessageBuilder + */ + public function toTag(int $tagId) + { + $this->to([ + 'filter' => [ + 'is_to_all' => false, + 'tag_id' => $tagId, + ], + ]); + + return $this; + } + + /** + * @return \EasyWeChat\OfficialAccount\Broadcasting\MessageBuilder + */ + public function toUsers(array $openids) + { + $this->to([ + 'touser' => $openids, + ]); + + return $this; + } + + /** + * @return $this + */ + public function toAll() + { + $this->to([ + 'filter' => ['is_to_all' => true], + ]); + + return $this; + } + + /** + * @return \EasyWeChat\OfficialAccount\Broadcasting\MessageBuilder + */ + public function with(array $attributes) + { + $this->attributes = $attributes; + + return $this; + } + + /** + * Build message. + * + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function build(array $prepends = []): array + { + if (empty($this->message)) { + throw new RuntimeException('No message content to send.'); + } + + $content = $this->message->transformForJsonRequest(); + + if (empty($prepends)) { + $prepends = $this->to; + } + + $message = array_merge($prepends, $content, $this->attributes); + + return $message; + } + + /** + * Build preview message. + * + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function buildForPreview(string $by, string $user): array + { + return $this->build([$by => $user]); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Broadcasting/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/Broadcasting/ServiceProvider.php new file mode 100644 index 0000000..1f956cf --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Broadcasting/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Broadcasting; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['broadcasting'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Card/BoardingPassClient.php b/vendor/overtrue/wechat/src/OfficialAccount/Card/BoardingPassClient.php new file mode 100644 index 0000000..de33298 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Card/BoardingPassClient.php @@ -0,0 +1,31 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Card; + +/** + * Class BoardingPassClient. + * + * @author overtrue + */ +class BoardingPassClient extends Client +{ + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function checkin(array $params) + { + return $this->httpPostJson('card/boardingpass/checkin', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Card/Card.php b/vendor/overtrue/wechat/src/OfficialAccount/Card/Card.php new file mode 100644 index 0000000..14e817b --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Card/Card.php @@ -0,0 +1,52 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Card; + +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; + +/** + * Class Card. + * + * @author overtrue + * + * @property \EasyWeChat\OfficialAccount\Card\CodeClient $code + * @property \EasyWeChat\OfficialAccount\Card\MeetingTicketClient $meeting_ticket + * @property \EasyWeChat\OfficialAccount\Card\MemberCardClient $member_card + * @property \EasyWeChat\OfficialAccount\Card\GeneralCardClient $general_card + * @property \EasyWeChat\OfficialAccount\Card\MovieTicketClient $movie_ticket + * @property \EasyWeChat\OfficialAccount\Card\CoinClient $coin + * @property \EasyWeChat\OfficialAccount\Card\SubMerchantClient $sub_merchant + * @property \EasyWeChat\OfficialAccount\Card\BoardingPassClient $boarding_pass + * @property \EasyWeChat\OfficialAccount\Card\JssdkClient $jssdk + * @property \EasyWeChat\OfficialAccount\Card\GiftCardClient $gift_card + * @property \EasyWeChat\OfficialAccount\Card\GiftCardOrderClient $gift_card_order + * @property \EasyWeChat\OfficialAccount\Card\GiftCardPageClient $gift_card_page + * @property \EasyWeChat\OfficialAccount\Card\InvoiceClient $invoice + */ +class Card extends Client +{ + /** + * @param string $property + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function __get($property) + { + if (isset($this->app["card.{$property}"])) { + return $this->app["card.{$property}"]; + } + + throw new InvalidArgumentException(sprintf('No card service named "%s".', $property)); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Card/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/Card/Client.php new file mode 100644 index 0000000..9db4e1b --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Card/Client.php @@ -0,0 +1,422 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Card; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Traits\InteractsWithCache; + +/** + * Class Client. + * + * @author overtrue + */ +class Client extends BaseClient +{ + use InteractsWithCache; + + /** + * @var string + */ + protected $url; + + /** + * Ticket cache key. + * + * @var string + */ + protected $ticketCacheKey; + + /** + * Ticket cache prefix. + * + * @var string + */ + protected $ticketCachePrefix = 'easywechat.official_account.card.api_ticket.'; + + /** + * 获取卡券颜色. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function colors() + { + return $this->httpGet('card/getcolors'); + } + + /** + * 卡券开放类目查询接口. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function categories() + { + return $this->httpGet('card/getapplyprotocol'); + } + + /** + * 创建卡券. + * + * @param string $cardType + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create($cardType = 'member_card', array $attributes) + { + $params = [ + 'card' => [ + 'card_type' => strtoupper($cardType), + strtolower($cardType) => $attributes, + ], + ]; + + return $this->httpPostJson('card/create', $params); + } + + /** + * 查看卡券详情. + * + * @param string $cardId + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get($cardId) + { + $params = [ + 'card_id' => $cardId, + ]; + + return $this->httpPostJson('card/get', $params); + } + + /** + * 批量查询卡列表. + * + * @param int $offset + * @param int $count + * @param string $statusList + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function list($offset = 0, $count = 10, $statusList = 'CARD_STATUS_VERIFY_OK') + { + $params = [ + 'offset' => $offset, + 'count' => $count, + 'status_list' => $statusList, + ]; + + return $this->httpPostJson('card/batchget', $params); + } + + /** + * 更改卡券信息接口 and 设置跟随推荐接口. + * + * @param string $cardId + * @param string $type + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update($cardId, $type, array $attributes = []) + { + $card = []; + $card['card_id'] = $cardId; + $card[strtolower($type)] = $attributes; + + return $this->httpPostJson('card/update', $card); + } + + /** + * 删除卡券接口. + * + * @param string $cardId + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete($cardId) + { + $params = [ + 'card_id' => $cardId, + ]; + + return $this->httpPostJson('card/delete', $params); + } + + /** + * 创建二维码. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function createQrCode(array $cards) + { + return $this->httpPostJson('card/qrcode/create', $cards); + } + + /** + * ticket 换取二维码图片. + * + * @param string $ticket + * + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getQrCode($ticket) + { + $baseUri = 'https://mp.weixin.qq.com/cgi-bin/showqrcode'; + $params = [ + 'ticket' => $ticket, + ]; + + $response = $this->requestRaw($baseUri, 'GET', $params); + + return [ + 'status' => $response->getStatusCode(), + 'reason' => $response->getReasonPhrase(), + 'headers' => $response->getHeaders(), + 'body' => strval($response->getBody()), + 'url' => $baseUri.'?'.http_build_query($params), + ]; + } + + /** + * 通过ticket换取二维码 链接. + * + * @param string $ticket + * + * @return string + */ + public function getQrCodeUrl($ticket) + { + return sprintf('https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=%s', $ticket); + } + + /** + * 创建货架接口. + * + * @param string $banner + * @param string $pageTitle + * @param bool $canShare + * @param string $scene [SCENE_NEAR_BY 附近,SCENE_MENU 自定义菜单,SCENE_QRCODE 二维码,SCENE_ARTICLE 公众号文章, + * SCENE_H5 h5页面,SCENE_IVR 自动回复,SCENE_CARD_CUSTOM_CELL 卡券自定义cell] + * @param array $cardList + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function createLandingPage($banner, $pageTitle, $canShare, $scene, $cardList) + { + $params = [ + 'banner' => $banner, + 'page_title' => $pageTitle, + 'can_share' => $canShare, + 'scene' => $scene, + 'card_list' => $cardList, + ]; + + return $this->httpPostJson('card/landingpage/create', $params); + } + + /** + * 图文消息群发卡券. + * + * @param string $cardId + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getHtml($cardId) + { + $params = [ + 'card_id' => $cardId, + ]; + + return $this->httpPostJson('card/mpnews/gethtml', $params); + } + + /** + * 设置测试白名单. + * + * @param array $openids + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setTestWhitelist($openids) + { + $params = [ + 'openid' => $openids, + ]; + + return $this->httpPostJson('card/testwhitelist/set', $params); + } + + /** + * 设置测试白名单(by username). + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setTestWhitelistByName(array $usernames) + { + $params = [ + 'username' => $usernames, + ]; + + return $this->httpPostJson('card/testwhitelist/set', $params); + } + + /** + * 获取用户已领取卡券接口. + * + * @param string $openid + * @param string $cardId + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getUserCards($openid, $cardId = '') + { + $params = [ + 'openid' => $openid, + 'card_id' => $cardId, + ]; + + return $this->httpPostJson('card/user/getcardlist', $params); + } + + /** + * 设置微信买单接口. + * 设置买单的 card_id 必须已经配置了门店,否则会报错. + * + * @param string $cardId + * @param bool $isOpen + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setPayCell($cardId, $isOpen = true) + { + $params = [ + 'card_id' => $cardId, + 'is_open' => $isOpen, + ]; + + return $this->httpPostJson('card/paycell/set', $params); + } + + /** + * 设置自助核销接口 + * 设置买单的 card_id 必须已经配置了门店,否则会报错. + * + * @param string $cardId + * @param bool $isOpen + * @param bool $verifyCod + * @param bool $remarkAmount + * + * @return mixed + */ + public function setPayConsumeCell($cardId, $isOpen = true, $verifyCod = false, $remarkAmount = false) + { + $params = [ + 'card_id' => $cardId, + 'is_open' => $isOpen, + 'need_verify_cod' => $verifyCod, + 'need_remark_amount' => $remarkAmount, + ]; + + return $this->httpPostJson('card/selfconsumecell/set', $params); + } + + /** + * 增加库存. + * + * @param string $cardId + * @param int $amount + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + */ + public function increaseStock($cardId, $amount) + { + return $this->updateStock($cardId, $amount, 'increase'); + } + + /** + * 减少库存. + * + * @param string $cardId + * @param int $amount + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + */ + public function reduceStock($cardId, $amount) + { + return $this->updateStock($cardId, $amount, 'reduce'); + } + + /** + * 修改库存接口. + * + * @param string $cardId + * @param int $amount + * @param string $action + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function updateStock($cardId, $amount, $action = 'increase') + { + $key = 'increase' === $action ? 'increase_stock_value' : 'reduce_stock_value'; + $params = [ + 'card_id' => $cardId, + $key => abs($amount), + ]; + + return $this->httpPostJson('card/modifystock', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Card/CodeClient.php b/vendor/overtrue/wechat/src/OfficialAccount/Card/CodeClient.php new file mode 100644 index 0000000..8a000a3 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Card/CodeClient.php @@ -0,0 +1,169 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Card; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class CodeClient. + * + * @author overtrue + */ +class CodeClient extends BaseClient +{ + /** + * 导入code接口. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function deposit(string $cardId, array $codes) + { + $params = [ + 'card_id' => $cardId, + 'code' => $codes, + ]; + + return $this->httpPostJson('card/code/deposit', $params); + } + + /** + * 查询导入code数目. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getDepositedCount(string $cardId) + { + $params = [ + 'card_id' => $cardId, + ]; + + return $this->httpPostJson('card/code/getdepositcount', $params); + } + + /** + * 核查code接口. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function check(string $cardId, array $codes) + { + $params = [ + 'card_id' => $cardId, + 'code' => $codes, + ]; + + return $this->httpPostJson('card/code/checkcode', $params); + } + + /** + * 查询 Code 接口. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(string $code, string $cardId = '', bool $checkConsume = true) + { + $params = [ + 'code' => $code, + 'check_consume' => $checkConsume, + 'card_id' => $cardId, + ]; + + return $this->httpPostJson('card/code/get', $params); + } + + /** + * 更改Code接口. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(string $code, string $newCode, string $cardId = '') + { + $params = [ + 'code' => $code, + 'new_code' => $newCode, + 'card_id' => $cardId, + ]; + + return $this->httpPostJson('card/code/update', $params); + } + + /** + * 设置卡券失效. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function disable(string $code, string $cardId = '') + { + $params = [ + 'code' => $code, + 'card_id' => $cardId, + ]; + + return $this->httpPostJson('card/code/unavailable', $params); + } + + /** + * 核销 Code 接口. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function consume(string $code, string $cardId = null) + { + $params = [ + 'code' => $code, + ]; + + if (!is_null($cardId)) { + $params['card_id'] = $cardId; + } + + return $this->httpPostJson('card/code/consume', $params); + } + + /** + * Code解码接口. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function decrypt(string $encryptedCode) + { + $params = [ + 'encrypt_code' => $encryptedCode, + ]; + + return $this->httpPostJson('card/code/decrypt', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Card/CoinClient.php b/vendor/overtrue/wechat/src/OfficialAccount/Card/CoinClient.php new file mode 100644 index 0000000..ceb93c5 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Card/CoinClient.php @@ -0,0 +1,106 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Card; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class CoinClient. + * + * @author overtrue + */ +class CoinClient extends BaseClient +{ + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function activate() + { + return $this->httpGet('card/pay/activate'); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getPrice(string $cardId, int $quantity) + { + return $this->httpPostJson('card/pay/getpayprice', [ + 'card_id' => $cardId, + 'quantity' => $quantity, + ]); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function summary() + { + return $this->httpGet('card/pay/getcoinsinfo'); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function recharge(int $count) + { + return $this->httpPostJson('card/pay/recharge', [ + 'coin_count' => $count, + ]); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function order(string $orderId) + { + return $this->httpPostJson('card/pay/getorder', ['order_id' => $orderId]); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function orders(array $filters) + { + return $this->httpPostJson('card/pay/getorderlist', $filters); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function confirm(string $cardId, string $orderId, int $quantity) + { + return $this->httpPostJson('card/pay/confirm', [ + 'card_id' => $cardId, + 'order_id' => $orderId, + 'quantity' => $quantity, + ]); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Card/GeneralCardClient.php b/vendor/overtrue/wechat/src/OfficialAccount/Card/GeneralCardClient.php new file mode 100644 index 0000000..5f52e17 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Card/GeneralCardClient.php @@ -0,0 +1,64 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Card; + +/** + * Class GeneralCardClient. + * + * @author overtrue + */ +class GeneralCardClient extends Client +{ + /** + * 通用卡接口激活. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function activate(array $info = []) + { + return $this->httpPostJson('card/generalcard/activate', $info); + } + + /** + * 通用卡撤销激活. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function deactivate(string $cardId, string $code) + { + $params = [ + 'card_id' => $cardId, + 'code' => $code, + ]; + + return $this->httpPostJson('card/generalcard/unactivate', $params); + } + + /** + * 更新会员信息. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function updateUser(array $params = []) + { + return $this->httpPostJson('card/generalcard/updateuser', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Card/GiftCardClient.php b/vendor/overtrue/wechat/src/OfficialAccount/Card/GiftCardClient.php new file mode 100644 index 0000000..938f8c9 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Card/GiftCardClient.php @@ -0,0 +1,66 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Card; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class GiftCardClient. + * + * @author overtrue + */ +class GiftCardClient extends BaseClient +{ + /** + * 申请微信支付礼品卡权限接口. + * + * @return mixed + */ + public function add(string $subMchId) + { + $params = [ + 'sub_mch_id' => $subMchId, + ]; + + return $this->httpPostJson('card/giftcard/pay/whitelist/add', $params); + } + + /** + * 绑定商户号到礼品卡小程序接口(商户号必须为公众号申请的商户号,否则报错). + * + * @return mixed + */ + public function bind(string $subMchId, string $wxaAppid) + { + $params = [ + 'sub_mch_id' => $subMchId, + 'wxa_appid' => $wxaAppid, + ]; + + return $this->httpPostJson('card/giftcard/pay/submch/bind', $params); + } + + /** + * 上传小程序代码. + * + * @return mixed + */ + public function set(string $wxaAppid, string $pageId) + { + $params = [ + 'wxa_appid' => $wxaAppid, + 'page_id' => $pageId, + ]; + + return $this->httpPostJson('card/giftcard/wxa/set', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Card/GiftCardOrderClient.php b/vendor/overtrue/wechat/src/OfficialAccount/Card/GiftCardOrderClient.php new file mode 100644 index 0000000..9f3be73 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Card/GiftCardOrderClient.php @@ -0,0 +1,68 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Card; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class GiftCardOrderClient. + * + * @author overtrue + */ +class GiftCardOrderClient extends BaseClient +{ + /** + * 查询-单个礼品卡订单信息接口. + * + * @return mixed + */ + public function get(string $orderId) + { + $params = [ + 'order_id' => $orderId, + ]; + + return $this->httpPostJson('card/giftcard/order/get', $params); + } + + /** + * 查询-批量查询礼品卡订单信息接口. + * + * @return mixed + */ + public function list(int $beginTime, int $endTime, int $offset = 0, int $count = 10, string $sortType = 'ASC') + { + $params = [ + 'begin_time' => $beginTime, + 'end_time' => $endTime, + 'sort_type' => $sortType, + 'offset' => $offset, + 'count' => $count, + ]; + + return $this->httpPostJson('card/giftcard/order/batchget', $params); + } + + /** + * 退款接口. + * + * @return mixed + */ + public function refund(string $orderId) + { + $params = [ + 'order_id' => $orderId, + ]; + + return $this->httpPostJson('card/giftcard/order/refund', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Card/GiftCardPageClient.php b/vendor/overtrue/wechat/src/OfficialAccount/Card/GiftCardPageClient.php new file mode 100644 index 0000000..c525ec6 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Card/GiftCardPageClient.php @@ -0,0 +1,92 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Card; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class GiftCardPageClient. + * + * @author overtrue + */ +class GiftCardPageClient extends BaseClient +{ + /** + * 创建-礼品卡货架接口. + * + * @return mixed + */ + public function add(array $attributes) + { + $params = [ + 'page' => $attributes, + ]; + + return $this->httpPostJson('card/giftcard/page/add', $params); + } + + /** + * 查询-礼品卡货架信息接口. + * + * @return mixed + */ + public function get(string $pageId) + { + $params = [ + 'page_id' => $pageId, + ]; + + return $this->httpPostJson('card/giftcard/page/get', $params); + } + + /** + * 修改-礼品卡货架信息接口. + * + * @return mixed + */ + public function update(string $pageId, string $bannerPicUrl, array $themeList) + { + $params = [ + 'page' => [ + 'page_id' => $pageId, + 'banner_pic_url' => $bannerPicUrl, + 'theme_list' => $themeList, + ], + ]; + + return $this->httpPostJson('card/giftcard/page/update', $params); + } + + /** + * 查询-礼品卡货架列表接口. + * + * @return mixed + */ + public function list() + { + return $this->httpPostJson('card/giftcard/page/batchget'); + } + + /** + * 下架-礼品卡货架接口(下架某一个货架或者全部货架). + * + * @return mixed + */ + public function setMaintain(string $pageId = '') + { + $params = ($pageId ? ['page_id' => $pageId] : ['all' => true]) + [ + 'maintain' => true, + ]; + + return $this->httpPostJson('card/giftcard/maintain/set', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Card/InvoiceClient.php b/vendor/overtrue/wechat/src/OfficialAccount/Card/InvoiceClient.php new file mode 100644 index 0000000..d80d3bb --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Card/InvoiceClient.php @@ -0,0 +1,101 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Card; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class InvoiceClient. + * + * @author overtrue + */ +class InvoiceClient extends BaseClient +{ + /** + * 设置支付后开票信息接口. + * + * @return mixed + */ + public function set(string $mchid, string $sPappid) + { + $params = [ + 'paymch_info' => [ + 'mchid' => $mchid, + 's_pappid' => $sPappid, + ], + ]; + + return $this->setBizAttr('set_pay_mch', $params); + } + + /** + * 查询支付后开票信息接口. + * + * @return mixed + */ + public function get() + { + return $this->setBizAttr('get_pay_mch'); + } + + /** + * 设置授权页字段信息接口. + * + * @return mixed + */ + public function setAuthField(array $userData, array $bizData) + { + $params = [ + 'auth_field' => [ + 'user_field' => $userData, + 'biz_field' => $bizData, + ], + ]; + + return $this->setBizAttr('set_auth_field', $params); + } + + /** + * 查询授权页字段信息接口. + * + * @return mixed + */ + public function getAuthField() + { + return $this->setBizAttr('get_auth_field'); + } + + /** + * 查询开票信息. + * + * @return mixed + */ + public function getAuthData(string $appId, string $orderId) + { + $params = [ + 'order_id' => $orderId, + 's_appid' => $appId, + ]; + + return $this->httpPost('card/invoice/getauthdata', $params); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + private function setBizAttr(string $action, array $params = []) + { + return $this->httpPostJson('card/invoice/setbizattr', $params, ['action' => $action]); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Card/JssdkClient.php b/vendor/overtrue/wechat/src/OfficialAccount/Card/JssdkClient.php new file mode 100644 index 0000000..7630724 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Card/JssdkClient.php @@ -0,0 +1,77 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Card; + +use EasyWeChat\BasicService\Jssdk\Client as Jssdk; +use EasyWeChat\Kernel\Support\Arr; +use function EasyWeChat\Kernel\Support\str_random; + +/** + * Class Jssdk. + * + * @author overtrue + */ +class JssdkClient extends Jssdk +{ + /** + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function getTicket(bool $refresh = false, string $type = 'wx_card'): array + { + return parent::getTicket($refresh, $type); + } + + /** + * 微信卡券:JSAPI 卡券发放. + * + * @return string + */ + public function assign(array $cards) + { + return json_encode(array_map(function ($card) { + return $this->attachExtension($card['card_id'], $card); + }, $cards)); + } + + /** + * 生成 js添加到卡包 需要的 card_list 项. + * + * @param string $cardId + * + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function attachExtension($cardId, array $extension = []) + { + $timestamp = time(); + $nonce = str_random(6); + $ticket = $this->getTicket()['ticket']; + + $ext = array_merge(['timestamp' => $timestamp, 'nonce_str' => $nonce], Arr::only( + $extension, + ['code', 'openid', 'outer_id', 'balance', 'fixed_begintimestamp', 'outer_str'] + )); + + $ext['signature'] = $this->dictionaryOrderSignature($ticket, $timestamp, $cardId, $ext['code'] ?? '', $ext['openid'] ?? '', $nonce); + + return [ + 'cardId' => $cardId, + 'cardExt' => json_encode($ext), + ]; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Card/MeetingTicketClient.php b/vendor/overtrue/wechat/src/OfficialAccount/Card/MeetingTicketClient.php new file mode 100644 index 0000000..ae0cff7 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Card/MeetingTicketClient.php @@ -0,0 +1,31 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Card; + +/** + * Class MeetingTicketClient. + * + * @author overtrue + */ +class MeetingTicketClient extends Client +{ + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function updateUser(array $params) + { + return $this->httpPostJson('card/meetingticket/updateuser', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Card/MemberCardClient.php b/vendor/overtrue/wechat/src/OfficialAccount/Card/MemberCardClient.php new file mode 100644 index 0000000..f2a9758 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Card/MemberCardClient.php @@ -0,0 +1,113 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Card; + +/** + * Class MemberCardClient. + * + * @author overtrue + */ +class MemberCardClient extends Client +{ + /** + * 会员卡接口激活. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function activate(array $info = []) + { + return $this->httpPostJson('card/membercard/activate', $info); + } + + /** + * 设置开卡字段接口. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setActivationForm(string $cardId, array $settings) + { + $params = array_merge(['card_id' => $cardId], $settings); + + return $this->httpPostJson('card/membercard/activateuserform/set', $params); + } + + /** + * 拉取会员信息接口. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getUser(string $cardId, string $code) + { + $params = [ + 'card_id' => $cardId, + 'code' => $code, + ]; + + return $this->httpPostJson('card/membercard/userinfo/get', $params); + } + + /** + * 更新会员信息. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function updateUser(array $params = []) + { + return $this->httpPostJson('card/membercard/updateuser', $params); + } + + /** + * 获取用户提交资料. + * + * @param string $activateTicket + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getActivationForm($activateTicket) + { + $params = [ + 'activate_ticket' => $activateTicket, + ]; + + return $this->httpPostJson('card/membercard/activatetempinfo/get', $params); + } + + /** + * 获取开卡组件链接接口. + * + * @param array $params 包含会员卡ID和随机字符串 + * + * @return string 开卡组件链接 + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getActivateUrl(array $params = []) + { + return $this->httpPostJson('card/membercard/activate/geturl', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Card/MovieTicketClient.php b/vendor/overtrue/wechat/src/OfficialAccount/Card/MovieTicketClient.php new file mode 100644 index 0000000..9540a74 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Card/MovieTicketClient.php @@ -0,0 +1,31 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Card; + +/** + * Class MovieTicketClient. + * + * @author overtrue + */ +class MovieTicketClient extends Client +{ + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function updateUser(array $params) + { + return $this->httpPostJson('card/movieticket/updateuser', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Card/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/Card/ServiceProvider.php new file mode 100644 index 0000000..3807fa3 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Card/ServiceProvider.php @@ -0,0 +1,89 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Card; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['card'] = function ($app) { + return new Card($app); + }; + + $app['card.client'] = function ($app) { + return new Client($app); + }; + + $app['card.coin'] = function ($app) { + return new CoinClient($app); + }; + + $app['card.sub_merchant'] = function ($app) { + return new SubMerchantClient($app); + }; + + $app['card.code'] = function ($app) { + return new CodeClient($app); + }; + + $app['card.movie_ticket'] = function ($app) { + return new MovieTicketClient($app); + }; + + $app['card.member_card'] = function ($app) { + return new MemberCardClient($app); + }; + + $app['card.general_card'] = function ($app) { + return new GeneralCardClient($app); + }; + + $app['card.boarding_pass'] = function ($app) { + return new BoardingPassClient($app); + }; + + $app['card.meeting_ticket'] = function ($app) { + return new MeetingTicketClient($app); + }; + + $app['card.jssdk'] = function ($app) { + return new JssdkClient($app); + }; + + $app['card.gift_card'] = function ($app) { + return new GiftCardClient($app); + }; + + $app['card.gift_card_order'] = function ($app) { + return new GiftCardOrderClient($app); + }; + + $app['card.gift_card_page'] = function ($app) { + return new GiftCardPageClient($app); + }; + + $app['card.invoice'] = function ($app) { + return new InvoiceClient($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Card/SubMerchantClient.php b/vendor/overtrue/wechat/src/OfficialAccount/Card/SubMerchantClient.php new file mode 100644 index 0000000..cf09c81 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Card/SubMerchantClient.php @@ -0,0 +1,112 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Card; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Support\Arr; + +/** + * Class SubMerchantClient. + * + * @author overtrue + */ +class SubMerchantClient extends BaseClient +{ + /** + * 添加子商户. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create(array $info = []) + { + $params = [ + 'info' => Arr::only($info, [ + 'brand_name', + 'logo_url', + 'protocol', + 'end_time', + 'primary_category_id', + 'secondary_category_id', + 'agreement_media_id', + 'operator_media_id', + 'app_id', + ]), + ]; + + return $this->httpPostJson('card/submerchant/submit', $params); + } + + /** + * 更新子商户. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(int $merchantId, array $info = []) + { + $params = [ + 'info' => array_merge( + ['merchant_id' => $merchantId], + Arr::only($info, [ + 'brand_name', + 'logo_url', + 'protocol', + 'end_time', + 'primary_category_id', + 'secondary_category_id', + 'agreement_media_id', + 'operator_media_id', + 'app_id', + ]) + ), + ]; + + return $this->httpPostJson('card/submerchant/update', $params); + } + + /** + * 获取子商户信息. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(int $merchantId) + { + return $this->httpPostJson('card/submerchant/get', ['merchant_id' => $merchantId]); + } + + /** + * 批量获取子商户信息. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function list(int $beginId = 0, int $limit = 50, string $status = 'CHECKING') + { + $params = [ + 'begin_id' => $beginId, + 'limit' => $limit, + 'status' => $status, + ]; + + return $this->httpPostJson('card/submerchant/batchget', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Comment/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/Comment/Client.php new file mode 100644 index 0000000..6a5da10 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Comment/Client.php @@ -0,0 +1,175 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Comment; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author overtrue + */ +class Client extends BaseClient +{ + /** + * Open article comment. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function open(string $msgId, int $index = null) + { + $params = [ + 'msg_data_id' => $msgId, + 'index' => $index, + ]; + + return $this->httpPostJson('cgi-bin/comment/open', $params); + } + + /** + * Close comment. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function close(string $msgId, int $index = null) + { + $params = [ + 'msg_data_id' => $msgId, + 'index' => $index, + ]; + + return $this->httpPostJson('cgi-bin/comment/close', $params); + } + + /** + * Get article comments. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function list(string $msgId, int $index, int $begin, int $count, int $type = 0) + { + $params = [ + 'msg_data_id' => $msgId, + 'index' => $index, + 'begin' => $begin, + 'count' => $count, + 'type' => $type, + ]; + + return $this->httpPostJson('cgi-bin/comment/list', $params); + } + + /** + * Mark elect comment. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function markElect(string $msgId, int $index, int $commentId) + { + $params = [ + 'msg_data_id' => $msgId, + 'index' => $index, + 'user_comment_id' => $commentId, + ]; + + return $this->httpPostJson('cgi-bin/comment/markelect', $params); + } + + /** + * Unmark elect comment. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function unmarkElect(string $msgId, int $index, int $commentId) + { + $params = [ + 'msg_data_id' => $msgId, + 'index' => $index, + 'user_comment_id' => $commentId, + ]; + + return $this->httpPostJson('cgi-bin/comment/unmarkelect', $params); + } + + /** + * Delete comment. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(string $msgId, int $index, int $commentId) + { + $params = [ + 'msg_data_id' => $msgId, + 'index' => $index, + 'user_comment_id' => $commentId, + ]; + + return $this->httpPostJson('cgi-bin/comment/delete', $params); + } + + /** + * Reply to a comment. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function reply(string $msgId, int $index, int $commentId, string $content) + { + $params = [ + 'msg_data_id' => $msgId, + 'index' => $index, + 'user_comment_id' => $commentId, + 'content' => $content, + ]; + + return $this->httpPostJson('cgi-bin/comment/reply/add', $params); + } + + /** + * Delete a reply. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function deleteReply(string $msgId, int $index, int $commentId) + { + $params = [ + 'msg_data_id' => $msgId, + 'index' => $index, + 'user_comment_id' => $commentId, + ]; + + return $this->httpPostJson('cgi-bin/comment/reply/delete', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Comment/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/Comment/ServiceProvider.php new file mode 100644 index 0000000..8d6806c --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Comment/ServiceProvider.php @@ -0,0 +1,44 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * ServiceProvider.php. + * + * This file is part of the wechat. + * + * (c) overtrue + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Comment; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['comment'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/CustomerService/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/CustomerService/Client.php new file mode 100644 index 0000000..7726910 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/CustomerService/Client.php @@ -0,0 +1,208 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\CustomerService; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author overtrue + */ +class Client extends BaseClient +{ + /** + * List all staffs. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function list() + { + return $this->httpGet('cgi-bin/customservice/getkflist'); + } + + /** + * List all online staffs. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function online() + { + return $this->httpGet('cgi-bin/customservice/getonlinekflist'); + } + + /** + * Create a staff. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create(string $account, string $nickname) + { + $params = [ + 'kf_account' => $account, + 'nickname' => $nickname, + ]; + + return $this->httpPostJson('customservice/kfaccount/add', $params); + } + + /** + * Update a staff. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(string $account, string $nickname) + { + $params = [ + 'kf_account' => $account, + 'nickname' => $nickname, + ]; + + return $this->httpPostJson('customservice/kfaccount/update', $params); + } + + /** + * Delete a staff. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(string $account) + { + return $this->httpPostJson('customservice/kfaccount/del', [], ['kf_account' => $account]); + } + + /** + * Invite a staff. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function invite(string $account, string $wechatId) + { + $params = [ + 'kf_account' => $account, + 'invite_wx' => $wechatId, + ]; + + return $this->httpPostJson('customservice/kfaccount/inviteworker', $params); + } + + /** + * Set staff avatar. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setAvatar(string $account, string $path) + { + return $this->httpUpload('customservice/kfaccount/uploadheadimg', ['media' => $path], [], ['kf_account' => $account]); + } + + /** + * Get message builder. + * + * @param \EasyWeChat\Kernel\Messages\Message|string $message + * + * @return \EasyWeChat\OfficialAccount\CustomerService\Messenger + */ + public function message($message) + { + $messageBuilder = new Messenger($this); + + return $messageBuilder->message($message); + } + + /** + * Send a message. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function send(array $message) + { + return $this->httpPostJson('cgi-bin/message/custom/send', $message); + } + + /** + * Show typing status. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function showTypingStatusToUser(string $openid) + { + return $this->httpPostJson('cgi-bin/message/custom/typing', [ + 'touser' => $openid, + 'command' => 'Typing', + ]); + } + + /** + * Hide typing status. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function hideTypingStatusToUser(string $openid) + { + return $this->httpPostJson('cgi-bin/message/custom/typing', [ + 'touser' => $openid, + 'command' => 'CancelTyping', + ]); + } + + /** + * Get messages history. + * + * @param int $startTime + * @param int $endTime + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function messages($startTime, $endTime, int $msgId = 1, int $number = 10000) + { + $params = [ + 'starttime' => is_numeric($startTime) ? $startTime : strtotime($startTime), + 'endtime' => is_numeric($endTime) ? $endTime : strtotime($endTime), + 'msgid' => $msgId, + 'number' => $number, + ]; + + return $this->httpPostJson('customservice/msgrecord/getmsglist', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/CustomerService/Messenger.php b/vendor/overtrue/wechat/src/OfficialAccount/CustomerService/Messenger.php new file mode 100644 index 0000000..4b66a9e --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/CustomerService/Messenger.php @@ -0,0 +1,159 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\CustomerService; + +use EasyWeChat\Kernel\Exceptions\RuntimeException; +use EasyWeChat\Kernel\Messages\Message; +use EasyWeChat\Kernel\Messages\Raw as RawMessage; +use EasyWeChat\Kernel\Messages\Text; + +/** + * Class MessageBuilder. + * + * @author overtrue + */ +class Messenger +{ + /** + * Messages to send. + * + * @var \EasyWeChat\Kernel\Messages\Message; + */ + protected $message; + + /** + * Messages target user open id. + * + * @var string + */ + protected $to; + + /** + * Messages sender staff id. + * + * @var string + */ + protected $account; + + /** + * Customer service instance. + * + * @var \EasyWeChat\OfficialAccount\CustomerService\Client + */ + protected $client; + + /** + * MessageBuilder constructor. + * + * @param \EasyWeChat\OfficialAccount\CustomerService\Client $client + */ + public function __construct(Client $client) + { + $this->client = $client; + } + + /** + * Set message to send. + * + * @param string|Message $message + * + * @return Messenger + */ + public function message($message) + { + if (is_string($message)) { + $message = new Text($message); + } + + $this->message = $message; + + return $this; + } + + /** + * Set staff account to send message. + * + * @return Messenger + */ + public function by(string $account) + { + $this->account = $account; + + return $this; + } + + /** + * @return Messenger + */ + public function from(string $account) + { + return $this->by($account); + } + + /** + * Set target user open id. + * + * @param string $openid + * + * @return Messenger + */ + public function to($openid) + { + $this->to = $openid; + + return $this; + } + + /** + * Send the message. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function send() + { + if (empty($this->message)) { + throw new RuntimeException('No message to send.'); + } + + if ($this->message instanceof RawMessage) { + $message = json_decode($this->message->get('content'), true); + } else { + $prepends = [ + 'touser' => $this->to, + ]; + if ($this->account) { + $prepends['customservice'] = ['kf_account' => $this->account]; + } + $message = $this->message->transformForJsonRequest($prepends); + } + + return $this->client->send($message); + } + + /** + * Return property. + * + * @return mixed + */ + public function __get(string $property) + { + if (property_exists($this, $property)) { + return $this->$property; + } + + return null; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/CustomerService/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/CustomerService/ServiceProvider.php new file mode 100644 index 0000000..a879ce8 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/CustomerService/ServiceProvider.php @@ -0,0 +1,37 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\CustomerService; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['customer_service'] = function ($app) { + return new Client($app); + }; + + $app['customer_service_session'] = function ($app) { + return new SessionClient($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/CustomerService/SessionClient.php b/vendor/overtrue/wechat/src/OfficialAccount/CustomerService/SessionClient.php new file mode 100644 index 0000000..0802b32 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/CustomerService/SessionClient.php @@ -0,0 +1,94 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\CustomerService; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class SessionClient. + * + * @author overtrue + */ +class SessionClient extends BaseClient +{ + /** + * List all sessions of $account. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function list(string $account) + { + return $this->httpGet('customservice/kfsession/getsessionlist', ['kf_account' => $account]); + } + + /** + * List all the people waiting. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function waiting() + { + return $this->httpGet('customservice/kfsession/getwaitcase'); + } + + /** + * Create a session. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create(string $account, string $openid) + { + $params = [ + 'kf_account' => $account, + 'openid' => $openid, + ]; + + return $this->httpPostJson('customservice/kfsession/create', $params); + } + + /** + * Close a session. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function close(string $account, string $openid) + { + $params = [ + 'kf_account' => $account, + 'openid' => $openid, + ]; + + return $this->httpPostJson('customservice/kfsession/close', $params); + } + + /** + * Get a session. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function get(string $openid) + { + return $this->httpGet('customservice/kfsession/getsession', ['openid' => $openid]); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/DataCube/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/DataCube/Client.php new file mode 100644 index 0000000..ca53024 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/DataCube/Client.php @@ -0,0 +1,271 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\DataCube; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author overtrue + */ +class Client extends BaseClient +{ + /** + * 获取用户增减数据. + * + * @return mixed + */ + public function userSummary(string $from, string $to) + { + return $this->query('datacube/getusersummary', $from, $to); + } + + /** + * 获取累计用户数据. + * + * @return mixed + */ + public function userCumulate(string $from, string $to) + { + return $this->query('datacube/getusercumulate', $from, $to); + } + + /** + * 获取图文群发每日数据. + * + * @return mixed + */ + public function articleSummary(string $from, string $to) + { + return $this->query('datacube/getarticlesummary', $from, $to); + } + + /** + * 获取图文群发总数据. + * + * @return mixed + */ + public function articleTotal(string $from, string $to) + { + return $this->query('datacube/getarticletotal', $from, $to); + } + + /** + * 获取图文统计数据. + * + * @return mixed + */ + public function userReadSummary(string $from, string $to) + { + return $this->query('datacube/getuserread', $from, $to); + } + + /** + * 获取图文统计分时数据. + * + * @return mixed + */ + public function userReadHourly(string $from, string $to) + { + return $this->query('datacube/getuserreadhour', $from, $to); + } + + /** + * 获取图文分享转发数据. + * + * @return mixed + */ + public function userShareSummary(string $from, string $to) + { + return $this->query('datacube/getusershare', $from, $to); + } + + /** + * 获取图文分享转发分时数据. + * + * @return mixed + */ + public function userShareHourly(string $from, string $to) + { + return $this->query('datacube/getusersharehour', $from, $to); + } + + /** + * 获取消息发送概况数据. + * + * @return mixed + */ + public function upstreamMessageSummary(string $from, string $to) + { + return $this->query('datacube/getupstreammsg', $from, $to); + } + + /** + * 获取消息分送分时数据. + * + * @return mixed + */ + public function upstreamMessageHourly(string $from, string $to) + { + return $this->query('datacube/getupstreammsghour', $from, $to); + } + + /** + * 获取消息发送周数据. + * + * @return mixed + */ + public function upstreamMessageWeekly(string $from, string $to) + { + return $this->query('datacube/getupstreammsgweek', $from, $to); + } + + /** + * 获取消息发送月数据. + * + * @return mixed + */ + public function upstreamMessageMonthly(string $from, string $to) + { + return $this->query('datacube/getupstreammsgmonth', $from, $to); + } + + /** + * 获取消息发送分布数据. + * + * @return mixed + */ + public function upstreamMessageDistSummary(string $from, string $to) + { + return $this->query('datacube/getupstreammsgdist', $from, $to); + } + + /** + * 获取消息发送分布周数据. + * + * @return mixed + */ + public function upstreamMessageDistWeekly(string $from, string $to) + { + return $this->query('datacube/getupstreammsgdistweek', $from, $to); + } + + /** + * 获取消息发送分布月数据. + * + * @return mixed + */ + public function upstreamMessageDistMonthly(string $from, string $to) + { + return $this->query('datacube/getupstreammsgdistmonth', $from, $to); + } + + /** + * 获取接口分析数据. + * + * @return mixed + */ + public function interfaceSummary(string $from, string $to) + { + return $this->query('datacube/getinterfacesummary', $from, $to); + } + + /** + * 获取接口分析分时数据. + * + * @return mixed + */ + public function interfaceSummaryHourly(string $from, string $to) + { + return $this->query('datacube/getinterfacesummaryhour', $from, $to); + } + + /** + * 拉取卡券概况数据接口. + * + * @param int $condSource + * + * @return mixed + */ + public function cardSummary(string $from, string $to, $condSource = 0) + { + $ext = [ + 'cond_source' => intval($condSource), + ]; + + return $this->query('datacube/getcardbizuininfo', $from, $to, $ext); + } + + /** + * 获取免费券数据接口. + * + * @return mixed + */ + public function freeCardSummary(string $from, string $to, int $condSource = 0, string $cardId = '') + { + $ext = [ + 'cond_source' => intval($condSource), + 'card_id' => $cardId, + ]; + + return $this->query('datacube/getcardcardinfo', $from, $to, $ext); + } + + /** + * 拉取会员卡数据接口. + * + * @param int $condSource + * + * @return mixed + */ + public function memberCardSummary(string $from, string $to, $condSource = 0) + { + $ext = [ + 'cond_source' => intval($condSource), + ]; + + return $this->query('datacube/getcardmembercardinfo', $from, $to, $ext); + } + + /** + * 拉取单张会员卡数据接口. + * + * @return mixed + */ + public function memberCardSummaryById(string $from, string $to, string $cardId) + { + $ext = [ + 'card_id' => $cardId, + ]; + + return $this->query('datacube/getcardmembercarddetail', $from, $to, $ext); + } + + /** + * 查询数据. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function query(string $api, string $from, string $to, array $ext = []) + { + $params = array_merge([ + 'begin_date' => $from, + 'end_date' => $to, + ], $ext); + + return $this->httpPostJson($api, $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/DataCube/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/DataCube/ServiceProvider.php new file mode 100644 index 0000000..bfec89a --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/DataCube/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\DataCube; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['data_cube'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Device/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/Device/Client.php new file mode 100644 index 0000000..7d2bbfc --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Device/Client.php @@ -0,0 +1,217 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Device; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @see http://iot.weixin.qq.com/wiki/new/index.html + * + * @author soone <66812590@qq.com> + */ +class Client extends BaseClient +{ + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function message(string $deviceId, string $openid, string $content) + { + $params = [ + 'device_type' => $this->app['config']['device_type'], + 'device_id' => $deviceId, + 'open_id' => $openid, + 'content' => base64_encode($content), + ]; + + return $this->httpPostJson('device/transmsg', $params); + } + + /** + * Get device qrcode. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function qrCode(array $deviceIds) + { + $params = [ + 'device_num' => count($deviceIds), + 'device_id_list' => $deviceIds, + ]; + + return $this->httpPostJson('device/create_qrcode', $params); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function authorize(array $devices, string $productId, int $opType = 0) + { + $params = [ + 'device_num' => count($devices), + 'device_list' => $devices, + 'op_type' => $opType, + 'product_id' => $productId, + ]; + + return $this->httpPostJson('device/authorize_device', $params); + } + + /** + * 获取 device id 和二维码 + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function createId(string $productId) + { + $params = [ + 'product_id' => $productId, + ]; + + return $this->httpGet('device/getqrcode', $params); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function bind(string $openid, string $deviceId, string $ticket) + { + $params = [ + 'ticket' => $ticket, + 'device_id' => $deviceId, + 'openid' => $openid, + ]; + + return $this->httpPostJson('device/bind', $params); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function unbind(string $openid, string $deviceId, string $ticket) + { + $params = [ + 'ticket' => $ticket, + 'device_id' => $deviceId, + 'openid' => $openid, + ]; + + return $this->httpPostJson('device/unbind', $params); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function forceBind(string $openid, string $deviceId) + { + $params = [ + 'device_id' => $deviceId, + 'openid' => $openid, + ]; + + return $this->httpPostJson('device/compel_bind', $params); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function forceUnbind(string $openid, string $deviceId) + { + $params = [ + 'device_id' => $deviceId, + 'openid' => $openid, + ]; + + return $this->httpPostJson('device/compel_unbind', $params); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function status(string $deviceId) + { + $params = [ + 'device_id' => $deviceId, + ]; + + return $this->httpGet('device/get_stat', $params); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function verify(string $ticket) + { + $params = [ + 'ticket' => $ticket, + ]; + + return $this->httpPost('device/verify_qrcode', $params); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function openid(string $deviceId) + { + $params = [ + 'device_type' => $this->app['config']['device_type'], + 'device_id' => $deviceId, + ]; + + return $this->httpGet('device/get_openid', $params); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function listByOpenid(string $openid) + { + $params = [ + 'openid' => $openid, + ]; + + return $this->httpGet('device/get_bind_device', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Device/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/Device/ServiceProvider.php new file mode 100644 index 0000000..e3dce8e --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Device/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Device; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author soone <66812590@qq.com + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['device'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Goods/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/Goods/Client.php new file mode 100644 index 0000000..49c7ec2 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Goods/Client.php @@ -0,0 +1,101 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Goods; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author her-cat + */ +class Client extends BaseClient +{ + /** + * Add the goods. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function add(array $data) + { + return $this->httpPostJson('scan/product/v2/add', [ + 'product' => $data, + ]); + } + + /** + * Update the goods. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(array $data) + { + return $this->httpPostJson('scan/product/v2/add', [ + 'product' => $data, + ]); + } + + /** + * Get add or update goods results. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function status(string $ticket) + { + return $this->httpPostJson('scan/product/v2/status', [ + 'status_ticket' => $ticket, + ]); + } + + /** + * Get goods information. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(string $pid) + { + return $this->httpPostJson('scan/product/v2/getinfo', [ + 'product' => [ + 'pid' => $pid, + ], + ]); + } + + /** + * Get a list of goods. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function list(string $context = '', int $page = 1, int $size = 10) + { + return $this->httpPostJson('scan/product/v2/getinfobypage', [ + 'page_context' => $context, + 'page_num' => $page, + 'page_size' => $size, + ]); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Goods/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/Goods/ServiceProvider.php new file mode 100644 index 0000000..38a0902 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Goods/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Goods; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author her-cat + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['goods'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Guide/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/Guide/Client.php new file mode 100644 index 0000000..9c771dc --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Guide/Client.php @@ -0,0 +1,991 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Guide; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Exceptions\InvalidConfigException; +use EasyWeChat\Kernel\Support\Collection; +use Psr\Http\Message\ResponseInterface; + +/** + * Class Client. + * + * @author MillsGuo + */ +class Client extends BaseClient +{ + /** + * 添加顾问 + * @param string $guideAccount + * @param string $guideOpenid + * @param string $guideHeadImgUrl + * @param string $guideNickname + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function createAdviser($guideAccount = '', $guideOpenid = '', $guideHeadImgUrl = '', $guideNickname = '') + { + $params = $this->selectAccountAndOpenid(array(), $guideAccount, $guideOpenid); + if (!empty($guideHeadImgUrl)) { + $params['guide_headimgurl'] = $guideHeadImgUrl; + } + if (!empty($guideNickname)) { + $params['guide_nickname'] = $guideNickname; + } + + return $this->httpPostJson('cgi-bin/guide/addguideacct', $params); + } + + /** + * 获取顾问信息 + * @param string $guideAccount + * @param string $guideOpenid + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getAdviser($guideAccount = '', $guideOpenid = '') + { + $params = $this->selectAccountAndOpenid(array(), $guideAccount, $guideOpenid); + + return $this->httpPostJson('cgi-bin/guide/getguideacct', $params); + } + + /** + * 修改顾问的昵称或头像 + * @param string $guideAccount + * @param string $guideOpenid + * @param string $guideHeadImgUrl + * @param string $guideNickname + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function updateAdviser($guideAccount = '', $guideOpenid = '', $guideHeadImgUrl = '', $guideNickname = '') + { + $params = $this->selectAccountAndOpenid(array(), $guideAccount, $guideOpenid); + if (!empty($guideHeadImgUrl)) { + $params['guide_headimgurl'] = $guideHeadImgUrl; + } + if (!empty($guideNickname)) { + $params['guide_nickname'] = $guideNickname; + } + + return $this->httpPostJson('cgi-bin/guide/updateguideacct', $params); + } + + /** + * 删除顾问 + * @param string $guideAccount + * @param string $guideOpenid + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function deleteAdviser($guideAccount = '', $guideOpenid = '') + { + $params = $this->selectAccountAndOpenid(array(), $guideAccount, $guideOpenid); + + return $this->httpPostJson('cgi-bin/guide/delguideacct', $params); + } + + /** + * 获取服务号顾问列表 + * + * @return mixed + * + * @throws InvalidConfigException + */ + public function getAdvisers($count, $page) + { + $params = [ + 'page' => $page, + 'num' => $count + ]; + + return $this->httpPostJson('cgi-bin/guide/getguideacctlist', $params); + } + + /** + * 生成顾问二维码 + * @param string $guideAccount + * @param string $guideOpenid + * @param string $qrCodeInfo + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function createQrCode($guideAccount = '', $guideOpenid = '', $qrCodeInfo = '') + { + $params = $this->selectAccountAndOpenid(array(), $guideAccount, $guideOpenid); + if (!empty($qrCodeInfo)) { + $params['qrcode_info'] = $qrCodeInfo; + } + + return $this->httpPostJson('cgi-bin/guide/guidecreateqrcode', $params); + } + + /** + * 获取顾问聊天记录 + * @param string $guideAccount + * @param string $guideOpenid + * @param string $openid + * @param int $beginTime + * @param int $endTime + * @param int $page + * @param int $count + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getBuyerChatRecords($guideAccount = '', $guideOpenid = '', $openid = '', $beginTime = 0, $endTime = 0, $page = 1, $count = 100) + { + $params = [ + 'page' => $page, + 'num' => $count + ]; + $params = $this->selectAccountAndOpenid($params, $guideAccount, $guideOpenid); + if (!empty($openid)) { + $params['openid'] = $openid; + } + if (!empty($beginTime)) { + $params['begin_time'] = $beginTime; + } + if (!empty($endTime)) { + $params['end_time'] = $endTime; + } + + return $this->httpPostJson('cgi-bin/guide/getguidebuyerchatrecord', $params); + } + + /** + * 设置快捷回复与关注自动回复 + * @param string $guideAccount + * @param string $guideOpenid + * @param bool $isDelete + * @param array $fastReplyListArray + * @param array $guideAutoReply + * @param array $guideAutoReplyPlus + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function setConfig($guideAccount = '', $guideOpenid = '', $isDelete = false, $fastReplyListArray = array(), $guideAutoReply = array(), $guideAutoReplyPlus = array()) + { + $params = [ + 'is_delete' => $isDelete + ]; + $params = $this->selectAccountAndOpenid($params, $guideAccount, $guideOpenid); + if (!empty($fastReplyListArray)) { + $params['guide_fast_reply_list'] = $fastReplyListArray; + } + if (!empty($guideAutoReply)) { + $params['guide_auto_reply'] = $guideAutoReply; + } + if (!empty($guideAutoReplyPlus)) { + $params['guide_auto_reply_plus'] = $guideAutoReplyPlus; + } + + return $this->httpPostJson('cgi-bin/guide/setguideconfig', $params); + } + + /** + * 获取快捷回复与关注自动回复 + * @param string $guideAccount + * @param string $guideOpenid + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getConfig($guideAccount = '', $guideOpenid = '') + { + try { + $params = $this->selectAccountAndOpenid(array(), $guideAccount, $guideOpenid); + } catch (InvalidConfigException $e) { + $params = array(); + } + + return $this->httpPostJson('cgi-bin/guide/getguideconfig', $params); + } + + /** + * 设置离线自动回复与敏感词 + * @param bool $isDelete + * @param array $blackKeyword + * @param array $guideAutoReply + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function setAdviserConfig(bool $isDelete, array $blackKeyword = [], array $guideAutoReply = []) + { + $params = [ + 'is_delete' => $isDelete + ]; + if (!empty($blackKeyword)) { + $params['black_keyword'] = $blackKeyword; + } + if (!empty($guideAutoReply)) { + $params['guide_auto_reply'] = $guideAutoReply; + } + + return $this->httpPostJson('cgi-bin/guide/setguideacctconfig', $params); + } + + /** + * 获取离线自动回复与敏感词 + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getAdviserConfig() + { + return $this->httpPostJson('cgi-bin/guide/getguideacctconfig', array()); + } + + /** + * 允许微信用户复制小程序页面路径 + * @param string $wxaAppid 小程序APPID + * @param string $wxUsername 微信用户的微信号 + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function allowCopyMiniAppPath(string $wxaAppid, string $wxUsername) + { + $params = [ + 'wxa_appid' => $wxaAppid, + 'wx_username' => $wxUsername + ]; + + return $this->httpPostJson('cgi-bin/guide/pushshowwxapathmenu', $params); + } + + /** + * 传入微信号或OPENID二选一 + * @param array $params + * @param string $guideAccount + * @param string $guideOpenid + * @return array + * @throws InvalidConfigException + */ + protected function selectAccountAndOpenid($params, $guideAccount = '', $guideOpenid = '') + { + if (!is_array($params)) { + throw new InvalidConfigException("传入配置参数必须为数组"); + } + if (!empty($guideOpenid)) { + $params['guide_openid'] = $guideOpenid; + } elseif (!empty($guideAccount)) { + $params['guide_account'] = $guideAccount; + } else { + throw new InvalidConfigException("微信号和OPENID不能同时为空"); + } + + return $params; + } + + /** + * 新建顾问分组 + * @param string $groupName + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function createGroup(string $groupName) + { + $params = [ + 'group_name' => $groupName + ]; + + return $this->httpPostJson('cgi-bin/guide/newguidegroup', $params); + } + + /** + * 获取顾问分组列表 + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getGuideGroups() + { + return $this->httpPostJson('cgi-bin/guide/getguidegrouplist', array()); + } + + /** + * 获取指定顾问分组信息 + * @param int $groupId + * @param int $page + * @param int $num + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getGroups(int $groupId, int $page, int $num) + { + $params = [ + 'group_id' => $groupId, + 'page' => $page, + 'num' => $num + ]; + + return $this->httpPostJson('cgi-bin/guide/getgroupinfo', $params); + } + + /** + * 分组内添加顾问 + * @param int $groupId + * @param string $guideAccount + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function addGroupGuide(int $groupId, string $guideAccount) + { + $params = [ + 'group_id' => $groupId, + 'gruide_account' => $guideAccount + ]; + + return $this->httpPostJson('cgi-bin/guide/addguide2guidegroup', $params); + } + + /** + * 分组内删除顾问 + * @param int $groupId + * @param string $guideAccount + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function deleteGroupGuide(int $groupId, string $guideAccount) + { + $params = [ + 'group_id' => $groupId, + 'guide_account' => $guideAccount + ]; + + return $this->httpPostJson('cgi-bin/guide/delguide2guidegroup', $params); + } + + /** + * 获取顾问所在分组 + * @param string $guideAccount + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getGuideGroup(string $guideAccount) + { + $params = [ + 'guide_account' => $guideAccount + ]; + + return $this->httpPostJson('cgi-bin/guide/getgroupbyguide', $params); + } + + /** + * 删除指定顾问分组 + * @param int $groupId + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function deleteGroup(int $groupId) + { + $params = [ + 'group_id' => $groupId + ]; + + return $this->httpPostJson('cgi-bin/guide/delguidegroup', $params); + } + + /** + * 为顾问分配客户 + * @param string $guideAccount + * @param string $guideOpenid + * @param array $buyerList + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function createBuyerRelation(string $guideAccount, string $guideOpenid, array $buyerList) + { + $params = [ + 'buyer_list' => $buyerList + ]; + $params = $this->selectAccountAndOpenid($params, $guideAccount, $guideOpenid); + + return $this->httpPostJson('cgi-bin/guide/addguidebuyerrelation', $params); + } + + /** + * 为顾问移除客户 + * @param string $guideAccount + * @param string $guideOpenid + * @param array $openidList + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function deleteBuyerRelation(string $guideAccount, string $guideOpenid, array $openidList) + { + $params = [ + 'openid_list' => $openidList + ]; + $params = $this->selectAccountAndOpenid($params, $guideAccount, $guideOpenid); + + return $this->httpPostJson('cgi-bin/guide/delguidebuyerrelation', $params); + } + + /** + * 获取顾问的客户列表 + * @param string $guideAccount + * @param string $guideOpenid + * @param int $page + * @param int $num + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getBuyerRelations(string $guideAccount, string $guideOpenid, int $page, int $num) + { + $params = [ + 'page' => $page, + 'num' => $num + ]; + $params = $this->selectAccountAndOpenid($params, $guideAccount, $guideOpenid); + + return $this->httpPostJson('cgi-bin/guide/getguidebuyerrelationlist', $params); + } + + /** + * 为客户更换顾问 + * @param string $oldGuideTarget + * @param string $newGuideTarget + * @param array $openidList + * @param bool $useTargetOpenid true使用OPENID,false使用微信号 + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function rebindBuyerGuide(string $oldGuideTarget, string $newGuideTarget, array $openidList, bool $useTargetOpenid = true) + { + $params = [ + 'openid_list' => $openidList + ]; + if ($useTargetOpenid) { + $params['old_guide_openid'] = $oldGuideTarget; + $params['new_guide_openid'] = $newGuideTarget; + } else { + $params['old_guide_account'] = $oldGuideTarget; + $params['new_guide_account'] = $newGuideTarget; + } + + return $this->httpPostJson('cgi-bin/guide/rebindguideacctforbuyer', $params); + } + + /** + * 修改客户昵称 + * @param string $guideAccount + * @param string $guideOpenid + * @param string $openid + * @param string $nickname + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function updateBuyerRelation(string $guideAccount, string $guideOpenid, string $openid, string $nickname) + { + $params = [ + 'openid' => $openid, + 'buyer_nickname' => $nickname + ]; + $params = $this->selectAccountAndOpenid($params, $guideAccount, $guideOpenid); + + return $this->httpPostJson('cgi-bin/guide/updateguidebuyerrelation', $params); + } + + /** + * 查询客户所属顾问 + * @param string $openid + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getBuyerRelation(string $openid) + { + $params = [ + 'openid' => $openid + ]; + + return $this->httpPostJson('cgi-bin/guide/getguidebuyerrelationbybuyer', $params); + } + + /** + * 查询指定顾问和客户的关系 + * @param string $guideAccount + * @param string $guideOpenid + * @param string $openid + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getBuyerRelationByGuide(string $guideAccount, string $guideOpenid, string $openid) + { + $params = [ + 'openid' => $openid + ]; + $params = $this->selectAccountAndOpenid($params, $guideAccount, $guideOpenid); + + return $this->httpPostJson('cgi-bin/guide/getguidebuyerrelation', $params); + } + + /** + * 新建可查询的标签类型 + * @param string $tagName + * @param array $tagValues + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function newTagOption(string $tagName, array $tagValues) + { + $params = [ + 'tag_name' => $tagName, + 'tag_values' => $tagValues + ]; + + return $this->httpPostJson('cgi-bin/guide/newguidetagoption', $params); + } + + /** + * 删除指定标签类型 + * @param string $tagName + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function deleteTagOption(string $tagName) + { + $params = [ + 'tag_name' => $tagName + ]; + + return $this->httpPostJson('cgi-bin/guide/delguidetagoption', $params); + } + + /** + * 为标签添加可选值 + * @param string $tagName + * @param array $tagValues + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function createTagOption(string $tagName, array $tagValues) + { + $params = [ + 'tag_name' => $tagName, + 'tag_values' => $tagValues + ]; + + return $this->httpPostJson('cgi-bin/guide/addguidetagoption', $params); + } + + /** + * 获取标签和可选值 + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getTagOption() + { + return $this->httpPostJson('cgi-bin/guide/getguidetagoption', array()); + } + + /** + * 为客户设置标签 + * @param string $guideAccount + * @param string $guideOpenid + * @param array $openidList + * @param string $tagValue + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function setBuyersTag(string $guideAccount, string $guideOpenid, array $openidList, string $tagValue) + { + $params = [ + 'tag_value' => $tagValue, + 'openid_list' => $openidList + ]; + $params = $this->selectAccountAndOpenid($params, $guideAccount, $guideOpenid); + + return $this->httpPostJson('cgi-bin/guide/addguidebuyertag', $params); + } + + /** + * 查询客户标签 + * @param string $guideAccount + * @param string $guideOpenid + * @param string $openid + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getBuyerTags(string $guideAccount, string $guideOpenid, string $openid) + { + $params = [ + 'openid' => $openid + ]; + $params = $this->selectAccountAndOpenid($params, $guideAccount, $guideOpenid); + + return $this->httpPostJson('cgi-bin/guide/getguidebuyertag', $params); + } + + /** + * 根据标签值筛选粉丝 + * @param string $guideAccount + * @param string $guideOpenid + * @param int $pushCount + * @param array $tagValues + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getBuyerByTag(string $guideAccount, string $guideOpenid, int $pushCount = 0, array $tagValues = array()) + { + $params = $this->selectAccountAndOpenid(array(), $guideAccount, $guideOpenid); + if ($pushCount > 0) { + $params['push_count'] = $pushCount; + } + if (count($tagValues) > 0) { + $params['tag_values'] = $tagValues; + } + + return $this->httpPostJson('cgi-bin/guide/queryguidebuyerbytag', $params); + } + + /** + * 删除客户标签 + * @param string $guideAccount + * @param string $guideOpenid + * @param string $tagValue + * @param array $openidList + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function deleteBuyerTag(string $guideAccount, string $guideOpenid, string $tagValue, array $openidList) + { + $params = [ + 'tag_value' => $tagValue + ]; + $params = $this->selectAccountAndOpenid($params, $guideAccount, $guideOpenid); + if (count($openidList) > 0) { + $params['openid_list'] = $openidList; + } + + return $this->httpPostJson('cgi-bin/guide/delguidebuyertag', $params); + } + + /** + * 设置自定义客户信息 + * @param string $guideAccount + * @param string $guideOpenid + * @param string $openid + * @param array $displayTagList + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function setBuyerDisplayTags(string $guideAccount, string $guideOpenid, string $openid, array $displayTagList) + { + $params = [ + 'openid' => $openid, + 'display_tag_list' => $displayTagList + ]; + $params = $this->selectAccountAndOpenid($params, $guideAccount, $guideOpenid); + + return $this->httpPostJson('cgi-bin/guide/addguidebuyerdisplaytag', $params); + } + + /** + * 获取自定义客户信息 + * @param string $guideAccount + * @param string $guideOpenid + * @param string $openid + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getBuyerDisplayTags(string $guideAccount, string $guideOpenid, string $openid) + { + $params = [ + 'openid' => $openid + ]; + $params = $this->selectAccountAndOpenid($params, $guideAccount, $guideOpenid); + + return $this->httpPostJson('cgi-bin/guide/getguidebuyerdisplaytag', $params); + } + + /** + * 添加小程序卡片素材 + * @param string $mediaId + * @param string $title + * @param string $path + * @param string $appid + * @param int $type + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function createCardMaterial(string $mediaId, string $title, string $path, string $appid, int $type = 0) + { + $params = [ + 'media_id' => $mediaId, + 'type' => $type, + 'title' => $title, + 'path' => $path, + 'appid' => $appid + ]; + + return $this->httpPostJson('cgi-bin/guide/setguidecardmaterial', $params); + } + + /** + * 查询小程序卡片素材 + * @param int $type + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getCardMaterial(int $type = 0) + { + $params = [ + 'type' => $type + ]; + + return $this->httpPostJson('cgi-bin/guide/getguidecardmaterial', $params); + } + + /** + * 删除小程序卡片素材 + * @param string $title + * @param string $path + * @param string $appid + * @param int $type + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function deleteCardMaterial(string $title, string $path, string $appid, int $type = 0) + { + $params = [ + 'type' => $type, + 'title' => $title, + 'path' => $path, + 'appid' => $appid + ]; + + return $this->httpPostJson('cgi-bin/guide/delguidecardmaterial', $params); + } + + /** + * 添加图片素材 + * @param string $mediaId + * @param int $type + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function createImageMaterial(string $mediaId, int $type = 0) + { + $params = [ + 'media_id' => $mediaId, + 'type' => $type + ]; + + return $this->httpPostJson('cgi-bin/guide/setguideimagematerial', $params); + } + + /** + * 查询图片素材 + * @param int $type + * @param int $start + * @param int $num + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getImageMaterial(int $type, int $start, int $num) + { + $params = [ + 'type' => $type, + 'start' => $start, + 'num' => $num + ]; + + return $this->httpPostJson('cgi-bin/guide/getguideimagematerial', $params); + } + + /** + * 删除图片素材 + * @param int $type + * @param string $picUrl + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function deleteImageMaterial(int $type, string $picUrl) + { + $params = [ + 'type' => $type, + 'picurl' => $picUrl + ]; + + return $this->httpPostJson('cgi-bin/guide/delguideimagematerial', $params); + } + + /** + * 添加文字素材 + * @param int $type + * @param string $word + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function createWordMaterial(int $type, string $word) + { + $params = [ + 'type' => $type, + 'word' => $word + ]; + + return $this->httpPostJson('cgi-bin/guide/setguidewordmaterial', $params); + } + + /** + * 查询文字素材 + * @param int $type + * @param int $start + * @param int $num + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getWordMaterial(int $type, int $start, int $num) + { + $params = [ + 'type' => $type, + 'start' => $start, + 'num' => $num + ]; + + return $this->httpPostJson('cgi-bin/guide/getguidewordmaterial', $params); + } + + /** + * 删除文字素材 + * @param int $type + * @param string $word + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function deleteWordMaterial(int $type, string $word) + { + $params = [ + 'type' => $type, + 'word' => $word + ]; + + return $this->httpPostJson('cgi-bin/guide/delguidewordmaterial', $params); + } + + /** + * 添加群发任务,为指定顾问添加群发任务 + * @param string $guideAccount + * @param string $guideOpenid + * @param string $taskName + * @param string $taskRemark + * @param int $pushTime + * @param array $openidArray + * @param array $materialArray + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function createMasSendJob(string $guideAccount, string $guideOpenid, string $taskName, string $taskRemark, int $pushTime, array $openidArray, array $materialArray) + { + $params = [ + 'task_name' => $taskName, + 'push_time' => $pushTime, + 'openid' => $openidArray, + 'material' => $materialArray + ]; + if (!empty($taskRemark)) { + $params['task_remark'] = $taskRemark; + } + $params = $this->selectAccountAndOpenid($params, $guideAccount, $guideOpenid); + + return $this->httpPostJson('cgi-bin/guide/addguidemassendjob', $params); + } + + /** + * 获取群发任务列表 + * @param string $guideAccount + * @param string $guideOpenid + * @param array $taskStatus + * @param int $offset + * @param int $limit + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getMasSendJobs(string $guideAccount, string $guideOpenid, array $taskStatus = [], int $offset = 0, int $limit = 50) + { + $params = $this->selectAccountAndOpenid(array(), $guideAccount, $guideOpenid); + if (!empty($taskStatus)) { + $params['task_status'] = $taskStatus; + } + if ($offset > 0) { + $params['offset'] = $offset; + } + if ($limit != 50) { + $params['limit'] = $limit; + } + + return $this->httpPostJson('cgi-bin/guide/getguidemassendjoblist', $params); + } + + /** + * 获取指定群发任务信息 + * @param string $taskId + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function getMasSendJob(string $taskId) + { + $params = [ + 'task_id' => $taskId + ]; + + return $this->httpPostJson('cgi-bin/guide/getguidemassendjob', $params); + } + + /** + * 修改群发任务 + * @param string $taskId + * @param string $taskName + * @param string $taskRemark + * @param int $pushTime + * @param array $openidArray + * @param array $materialArray + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function updateMasSendJob(string $taskId, string $taskName, string $taskRemark, int $pushTime, array $openidArray, array $materialArray) + { + $params = [ + 'task_id' => $taskId + ]; + if (!empty($taskName)) { + $params['task_name'] = $taskName; + } + if (!empty($taskRemark)) { + $params['task_remark'] = $taskRemark; + } + if (!empty($pushTime)) { + $params['push_time'] = $pushTime; + } + if (!empty($openidArray)) { + $params['openid'] = $openidArray; + } + if (!empty($materialArray)) { + $params['material'] = $materialArray; + } + + return $this->httpPostJson('cgi-bin/guide/updateguidemassendjob', $params); + } + + /** + * 取消群发任务 + * @param string $taskId + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function cancelMasSendJob(string $taskId) + { + $params = [ + 'task_id' => $taskId + ]; + + return $this->httpPostJson('cgi-bin/guide/cancelguidemassendjob', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Guide/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/Guide/ServiceProvider.php new file mode 100644 index 0000000..46ebbca --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Guide/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Guide; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author millsguo + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['guide'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Material/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/Material/Client.php new file mode 100644 index 0000000..ef39ab6 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Material/Client.php @@ -0,0 +1,275 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Material; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\Http\StreamResponse; +use EasyWeChat\Kernel\Messages\Article; + +/** + * Class Client. + * + * @author overtrue + */ +class Client extends BaseClient +{ + /** + * Allow media type. + * + * @var array + */ + protected $allowTypes = ['image', 'voice', 'video', 'thumb', 'news_image']; + + /** + * Upload image. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function uploadImage(string $path) + { + return $this->upload('image', $path); + } + + /** + * Upload voice. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function uploadVoice(string $path) + { + return $this->upload('voice', $path); + } + + /** + * Upload thumb. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function uploadThumb(string $path) + { + return $this->upload('thumb', $path); + } + + /** + * Upload video. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function uploadVideo(string $path, string $title, string $description) + { + $params = [ + 'description' => json_encode( + [ + 'title' => $title, + 'introduction' => $description, + ], + JSON_UNESCAPED_UNICODE + ), + ]; + + return $this->upload('video', $path, $params); + } + + /** + * Upload articles. + * + * @param array|Article $articles + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function uploadArticle($articles) + { + if ($articles instanceof Article || !empty($articles['title'])) { + $articles = [$articles]; + } + + $params = ['articles' => array_map(function ($article) { + if ($article instanceof Article) { + return $article->transformForJsonRequestWithoutType(); + } + + return $article; + }, $articles)]; + + return $this->httpPostJson('cgi-bin/material/add_news', $params); + } + + /** + * Update article. + * + * @param array|Article $article + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function updateArticle(string $mediaId, $article, int $index = 0) + { + if ($article instanceof Article) { + $article = $article->transformForJsonRequestWithoutType(); + } + + $params = [ + 'media_id' => $mediaId, + 'index' => $index, + 'articles' => isset($article['title']) ? $article : (isset($article[$index]) ? $article[$index] : []), + ]; + + return $this->httpPostJson('cgi-bin/material/update_news', $params); + } + + /** + * Upload image for article. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function uploadArticleImage(string $path) + { + return $this->upload('news_image', $path); + } + + /** + * Fetch material. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(string $mediaId) + { + $response = $this->requestRaw('cgi-bin/material/get_material', 'POST', ['json' => ['media_id' => $mediaId]]); + + if (false !== stripos($response->getHeaderLine('Content-disposition'), 'attachment')) { + return StreamResponse::buildFromPsrResponse($response); + } + + return $this->castResponseToType($response, $this->app['config']->get('response_type')); + } + + /** + * Delete material by media ID. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(string $mediaId) + { + return $this->httpPostJson('cgi-bin/material/del_material', ['media_id' => $mediaId]); + } + + /** + * List materials. + * + * example: + * + * { + * "total_count": TOTAL_COUNT, + * "item_count": ITEM_COUNT, + * "item": [{ + * "media_id": MEDIA_ID, + * "name": NAME, + * "update_time": UPDATE_TIME + * }, + * // more... + * ] + * } + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function list(string $type, int $offset = 0, int $count = 20) + { + $params = [ + 'type' => $type, + 'offset' => $offset, + 'count' => $count, + ]; + + return $this->httpPostJson('cgi-bin/material/batchget_material', $params); + } + + /** + * Get stats of materials. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function stats() + { + return $this->httpGet('cgi-bin/material/get_materialcount'); + } + + /** + * Upload material. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function upload(string $type, string $path, array $form = []) + { + if (!file_exists($path) || !is_readable($path)) { + throw new InvalidArgumentException(sprintf('File does not exist, or the file is unreadable: "%s"', $path)); + } + + $form['type'] = $type; + + return $this->httpUpload($this->getApiByType($type), ['media' => $path], $form); + } + + /** + * Get API by type. + * + * @return string + */ + public function getApiByType(string $type) + { + switch ($type) { + case 'news_image': + return 'cgi-bin/media/uploadimg'; + default: + return 'cgi-bin/material/add_material'; + } + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Material/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/Material/ServiceProvider.php new file mode 100644 index 0000000..089a8f8 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Material/ServiceProvider.php @@ -0,0 +1,44 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * ServiceProvider.php. + * + * This file is part of the wechat. + * + * (c) overtrue + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Material; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['material'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Menu/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/Menu/Client.php new file mode 100644 index 0000000..c8b3b88 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Menu/Client.php @@ -0,0 +1,98 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Menu; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author overtrue + */ +class Client extends BaseClient +{ + /** + * Get all menus. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function list() + { + return $this->httpGet('cgi-bin/menu/get'); + } + + /** + * Get current menus. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function current() + { + return $this->httpGet('cgi-bin/get_current_selfmenu_info'); + } + + /** + * Add menu. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create(array $buttons, array $matchRule = []) + { + if (!empty($matchRule)) { + return $this->httpPostJson('cgi-bin/menu/addconditional', [ + 'button' => $buttons, + 'matchrule' => $matchRule, + ]); + } + + return $this->httpPostJson('cgi-bin/menu/create', ['button' => $buttons]); + } + + /** + * Destroy menu. + * + * @param int $menuId + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(int $menuId = null) + { + if (is_null($menuId)) { + return $this->httpGet('cgi-bin/menu/delete'); + } + + return $this->httpPostJson('cgi-bin/menu/delconditional', ['menuid' => $menuId]); + } + + /** + * Test conditional menu. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function match(string $userId) + { + return $this->httpPostJson('cgi-bin/menu/trymatch', ['user_id' => $userId]); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Menu/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/Menu/ServiceProvider.php new file mode 100644 index 0000000..e79b105 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Menu/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Menu; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['menu'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/OAuth/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/OAuth/ServiceProvider.php new file mode 100644 index 0000000..ba176dc --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/OAuth/ServiceProvider.php @@ -0,0 +1,66 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\OAuth; + +use Overtrue\Socialite\SocialiteManager as Socialite; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['oauth'] = function ($app) { + $socialite = (new Socialite([ + 'wechat' => [ + 'client_id' => $app['config']['app_id'], + 'client_secret' => $app['config']['secret'], + 'redirect' => $this->prepareCallbackUrl($app), + ], + ], $app['request']))->driver('wechat'); + + $scopes = (array) $app['config']->get('oauth.scopes', ['snsapi_userinfo']); + + if (!empty($scopes)) { + $socialite->scopes($scopes); + } + + return $socialite; + }; + } + + /** + * Prepare the OAuth callback url for wechat. + * + * @param Container $app + * + * @return string + */ + private function prepareCallbackUrl($app) + { + $callback = $app['config']->get('oauth.callback'); + if (0 === stripos($callback, 'http')) { + return $callback; + } + $baseUrl = $app['request']->getSchemeAndHttpHost(); + + return $baseUrl.'/'.ltrim($callback, '/'); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/OCR/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/OCR/Client.php new file mode 100644 index 0000000..80b7a95 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/OCR/Client.php @@ -0,0 +1,78 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\OCR; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; + +/** + * Class Client. + * + * @author joyeekk + */ +class Client extends BaseClient +{ + /** + * Allow image parameter type. + * + * @var array + */ + protected $allowTypes = ['photo', 'scan']; + + /** + * ID card OCR. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function idCard(string $path, string $type = 'photo') + { + if (!\in_array($type, $this->allowTypes, true)) { + throw new InvalidArgumentException(sprintf("Unsupported type: '%s'", $type)); + } + + return $this->httpPost('cv/ocr/idcard', [ + 'type' => $type, + 'img_url' => $path, + ]); + } + + /** + * Bank card OCR. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function bankCard(string $path) + { + return $this->httpPost('cv/ocr/bankcard', [ + 'img_url' => $path, + ]); + } + + /** + * Vehicle license OCR. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function vehicleLicense(string $path) + { + return $this->httpPost('cv/ocr/drivinglicense', [ + 'img_url' => $path, + ]); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/OCR/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/OCR/ServiceProvider.php new file mode 100644 index 0000000..1773079 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/OCR/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\OCR; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author joyeekk + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['ocr'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/POI/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/POI/Client.php new file mode 100644 index 0000000..f71d5d2 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/POI/Client.php @@ -0,0 +1,131 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\POI; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author overtrue + */ +class Client extends BaseClient +{ + /** + * Get POI supported categories. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function categories() + { + return $this->httpGet('cgi-bin/poi/getwxcategory'); + } + + /** + * Get POI by ID. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(int $poiId) + { + return $this->httpPostJson('cgi-bin/poi/getpoi', ['poi_id' => $poiId]); + } + + /** + * List POI. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function list(int $offset = 0, int $limit = 10) + { + $params = [ + 'begin' => $offset, + 'limit' => $limit, + ]; + + return $this->httpPostJson('cgi-bin/poi/getpoilist', $params); + } + + /** + * Create a POI. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create(array $baseInfo) + { + $params = [ + 'business' => [ + 'base_info' => $baseInfo, + ], + ]; + + return $this->httpPostJson('cgi-bin/poi/addpoi', $params); + } + + /** + * @return int + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function createAndGetId(array $databaseInfo) + { + /** @var array $response */ + $response = $this->detectAndCastResponseToType($this->create($databaseInfo), 'array'); + + return $response['poi_id']; + } + + /** + * Update a POI. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(int $poiId, array $baseInfo) + { + $params = [ + 'business' => [ + 'base_info' => array_merge($baseInfo, ['poi_id' => $poiId]), + ], + ]; + + return $this->httpPostJson('cgi-bin/poi/updatepoi', $params); + } + + /** + * Delete a POI. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(int $poiId) + { + return $this->httpPostJson('cgi-bin/poi/delpoi', ['poi_id' => $poiId]); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/POI/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/POI/ServiceProvider.php new file mode 100644 index 0000000..156440b --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/POI/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\POI; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['poi'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Semantic/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/Semantic/Client.php new file mode 100644 index 0000000..d0e2a51 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Semantic/Client.php @@ -0,0 +1,41 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Semantic; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author overtrue + */ +class Client extends BaseClient +{ + /** + * Get the semantic content of giving string. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function query(string $keyword, string $categories, array $optional = []) + { + $params = [ + 'query' => $keyword, + 'category' => $categories, + 'appid' => $this->app['config']['app_id'], + ]; + + return $this->httpPostJson('semantic/semproxy/search', array_merge($params, $optional)); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Semantic/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/Semantic/ServiceProvider.php new file mode 100644 index 0000000..835b7fc --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Semantic/ServiceProvider.php @@ -0,0 +1,31 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Semantic; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['semantic'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Server/Guard.php b/vendor/overtrue/wechat/src/OfficialAccount/Server/Guard.php new file mode 100644 index 0000000..a42205f --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Server/Guard.php @@ -0,0 +1,27 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Server; + +use EasyWeChat\Kernel\ServerGuard; + +/** + * Class Guard. + * + * @author overtrue + */ +class Guard extends ServerGuard +{ + protected function shouldReturnRawResponse(): bool + { + return !is_null($this->app['request']->get('echostr')); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Server/Handlers/EchoStrHandler.php b/vendor/overtrue/wechat/src/OfficialAccount/Server/Handlers/EchoStrHandler.php new file mode 100644 index 0000000..b999375 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Server/Handlers/EchoStrHandler.php @@ -0,0 +1,49 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Server\Handlers; + +use EasyWeChat\Kernel\Contracts\EventHandlerInterface; +use EasyWeChat\Kernel\Decorators\FinallyResult; +use EasyWeChat\Kernel\ServiceContainer; + +/** + * Class EchoStrHandler. + * + * @author overtrue + */ +class EchoStrHandler implements EventHandlerInterface +{ + /** + * @var ServiceContainer + */ + protected $app; + + /** + * EchoStrHandler constructor. + */ + public function __construct(ServiceContainer $app) + { + $this->app = $app; + } + + /** + * @param mixed $payload + * + * @return FinallyResult|null + */ + public function handle($payload = null) + { + if ($str = $this->app['request']->get('echostr')) { + return new FinallyResult($str); + } + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Server/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/Server/ServiceProvider.php new file mode 100644 index 0000000..0c6716b --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Server/ServiceProvider.php @@ -0,0 +1,46 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Server; + +use EasyWeChat\Kernel\Encryptor; +use EasyWeChat\OfficialAccount\Server\Handlers\EchoStrHandler; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + !isset($app['encryptor']) && $app['encryptor'] = function ($app) { + return new Encryptor( + $app['config']['app_id'], + $app['config']['token'], + $app['config']['aes_key'] + ); + }; + + !isset($app['server']) && $app['server'] = function ($app) { + $guard = new Guard($app); + $guard->push(new EchoStrHandler($app)); + + return $guard; + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/Client.php new file mode 100644 index 0000000..bcc2a0d --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/Client.php @@ -0,0 +1,76 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\ShakeAround; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author overtrue + */ +class Client extends BaseClient +{ + /** + * @param array $data + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function register($data) + { + return $this->httpPostJson('shakearound/account/register', $data); + } + + /** + * Get audit status. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function status() + { + return $this->httpGet('shakearound/account/auditstatus'); + } + + /** + * Get shake info. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function user(string $ticket, bool $needPoi = false) + { + $params = [ + 'ticket' => $ticket, + ]; + + if ($needPoi) { + $params['need_poi'] = 1; + } + + return $this->httpPostJson('shakearound/user/getshakeinfo', $params); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + */ + public function userWithPoi(string $ticket) + { + return $this->user($ticket, true); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/DeviceClient.php b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/DeviceClient.php new file mode 100644 index 0000000..0e0f7d7 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/DeviceClient.php @@ -0,0 +1,165 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\ShakeAround; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class DeviceClient. + * + * @author allen05ren + */ +class DeviceClient extends BaseClient +{ + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function apply(array $data) + { + return $this->httpPostJson('shakearound/device/applyid', $data); + } + + /** + * Get audit status. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function status(int $applyId) + { + $params = [ + 'apply_id' => $applyId, + ]; + + return $this->httpPostJson('shakearound/device/applystatus', $params); + } + + /** + * Update a device comment. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(array $deviceIdentifier, string $comment) + { + $params = [ + 'device_identifier' => $deviceIdentifier, + 'comment' => $comment, + ]; + + return $this->httpPostJson('shakearound/device/update', $params); + } + + /** + * Bind location for device. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function bindPoi(array $deviceIdentifier, int $poiId) + { + $params = [ + 'device_identifier' => $deviceIdentifier, + 'poi_id' => $poiId, + ]; + + return $this->httpPostJson('shakearound/device/bindlocation', $params); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function bindThirdPoi(array $deviceIdentifier, int $poiId, string $appId) + { + $params = [ + 'device_identifier' => $deviceIdentifier, + 'poi_id' => $poiId, + 'type' => 2, + 'poi_appid' => $appId, + ]; + + return $this->httpPostJson('shakearound/device/bindlocation', $params); + } + + /** + * Fetch batch of devices by deviceIds. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + */ + public function listByIds(array $deviceIdentifiers) + { + $params = [ + 'type' => 1, + 'device_identifiers' => $deviceIdentifiers, + ]; + + return $this->search($params); + } + + /** + * Pagination to get batch of devices. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + */ + public function list(int $lastId, int $count) + { + $params = [ + 'type' => 2, + 'last_seen' => $lastId, + 'count' => $count, + ]; + + return $this->search($params); + } + + /** + * Fetch batch of devices by applyId. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + */ + public function listByApplyId(int $applyId, int $lastId, int $count) + { + $params = [ + 'type' => 3, + 'apply_id' => $applyId, + 'last_seen' => $lastId, + 'count' => $count, + ]; + + return $this->search($params); + } + + /** + * Fetch batch of devices. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function search(array $params) + { + return $this->httpPostJson('shakearound/device/search', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/GroupClient.php b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/GroupClient.php new file mode 100644 index 0000000..1907c2d --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/GroupClient.php @@ -0,0 +1,147 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\ShakeAround; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class GroupClient. + * + * @author allen05ren + */ +class GroupClient extends BaseClient +{ + /** + * Add device group. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create(string $name) + { + $params = [ + 'group_name' => $name, + ]; + + return $this->httpPostJson('shakearound/device/group/add', $params); + } + + /** + * Update a device group name. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(int $groupId, string $name) + { + $params = [ + 'group_id' => $groupId, + 'group_name' => $name, + ]; + + return $this->httpPostJson('shakearound/device/group/update', $params); + } + + /** + * Delete device group. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(int $groupId) + { + $params = [ + 'group_id' => $groupId, + ]; + + return $this->httpPostJson('shakearound/device/group/delete', $params); + } + + /** + * List all device groups. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function list(int $begin, int $count) + { + $params = [ + 'begin' => $begin, + 'count' => $count, + ]; + + return $this->httpPostJson('shakearound/device/group/getlist', $params); + } + + /** + * Get detail of a device group. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(int $groupId, int $begin, int $count) + { + $params = [ + 'group_id' => $groupId, + 'begin' => $begin, + 'count' => $count, + ]; + + return $this->httpPostJson('shakearound/device/group/getdetail', $params); + } + + /** + * Add one or more devices to a device group. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function addDevices(int $groupId, array $deviceIdentifiers) + { + $params = [ + 'group_id' => $groupId, + 'device_identifiers' => $deviceIdentifiers, + ]; + + return $this->httpPostJson('shakearound/device/group/adddevice', $params); + } + + /** + * Remove one or more devices from a device group. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function removeDevices(int $groupId, array $deviceIdentifiers) + { + $params = [ + 'group_id' => $groupId, + 'device_identifiers' => $deviceIdentifiers, + ]; + + return $this->httpPostJson('shakearound/device/group/deletedevice', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/MaterialClient.php b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/MaterialClient.php new file mode 100644 index 0000000..e7a4d06 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/MaterialClient.php @@ -0,0 +1,41 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\ShakeAround; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; + +/** + * Class MaterialClient. + * + * @author allen05ren + */ +class MaterialClient extends BaseClient +{ + /** + * Upload image material. + * + * @return string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function uploadImage(string $path, string $type = 'icon') + { + if (!file_exists($path) || !is_readable($path)) { + throw new InvalidArgumentException(sprintf('File does not exist, or the file is unreadable: "%s"', $path)); + } + + return $this->httpUpload('shakearound/material/add', ['media' => $path], [], ['type' => strtolower($type)]); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/PageClient.php b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/PageClient.php new file mode 100644 index 0000000..e230bb3 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/PageClient.php @@ -0,0 +1,98 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\ShakeAround; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class PageClient. + * + * @author allen05ren + */ +class PageClient extends BaseClient +{ + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create(array $data) + { + return $this->httpPostJson('shakearound/page/add', $data); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(int $pageId, array $data) + { + return $this->httpPostJson('shakearound/page/update', array_merge(['page_id' => $pageId], $data)); + } + + /** + * Fetch batch of pages by pageIds. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function listByIds(array $pageIds) + { + $params = [ + 'type' => 1, + 'page_ids' => $pageIds, + ]; + + return $this->httpPostJson('shakearound/page/search', $params); + } + + /** + * Pagination to get batch of pages. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function list(int $begin, int $count) + { + $params = [ + 'type' => 2, + 'begin' => $begin, + 'count' => $count, + ]; + + return $this->httpPostJson('shakearound/page/search', $params); + } + + /** + * delete a page. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(int $pageId) + { + $params = [ + 'page_id' => $pageId, + ]; + + return $this->httpPostJson('shakearound/page/delete', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/RelationClient.php b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/RelationClient.php new file mode 100644 index 0000000..774199b --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/RelationClient.php @@ -0,0 +1,78 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\ShakeAround; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class RelationClient. + * + * @author allen05ren + */ +class RelationClient extends BaseClient +{ + /** + * Bind pages for device. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function bindPages(array $deviceIdentifier, array $pageIds) + { + $params = [ + 'device_identifier' => $deviceIdentifier, + 'page_ids' => $pageIds, + ]; + + return $this->httpPostJson('shakearound/device/bindpage', $params); + } + + /** + * Get pageIds by deviceId. + * + * @return array|\EasyWeChat\Kernel\Support\Collection + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function listByDeviceId(array $deviceIdentifier) + { + $params = [ + 'type' => 1, + 'device_identifier' => $deviceIdentifier, + ]; + + return $this->httpPostJson('shakearound/relation/search', $params); + } + + /** + * Get devices by pageId. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function listByPageId(int $pageId, int $begin, int $count) + { + $params = [ + 'type' => 2, + 'page_id' => $pageId, + 'begin' => $begin, + 'count' => $count, + ]; + + return $this->httpPostJson('shakearound/relation/search', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/ServiceProvider.php new file mode 100644 index 0000000..a13d2b0 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/ServiceProvider.php @@ -0,0 +1,57 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\ShakeAround; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author allen05ren + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['shake_around'] = function ($app) { + return new ShakeAround($app); + }; + + $app['shake_around.device'] = function ($app) { + return new DeviceClient($app); + }; + + $app['shake_around.page'] = function ($app) { + return new PageClient($app); + }; + + $app['shake_around.material'] = function ($app) { + return new MaterialClient($app); + }; + + $app['shake_around.group'] = function ($app) { + return new GroupClient($app); + }; + + $app['shake_around.relation'] = function ($app) { + return new RelationClient($app); + }; + + $app['shake_around.stats'] = function ($app) { + return new StatsClient($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/ShakeAround.php b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/ShakeAround.php new file mode 100644 index 0000000..666a38a --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/ShakeAround.php @@ -0,0 +1,44 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\ShakeAround; + +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; + +/** + * Class Card. + * + * @author overtrue + * + * @property \EasyWeChat\OfficialAccount\ShakeAround\DeviceClient $device + * @property \EasyWeChat\OfficialAccount\ShakeAround\GroupClient $group + * @property \EasyWeChat\OfficialAccount\ShakeAround\MaterialClient $material + * @property \EasyWeChat\OfficialAccount\ShakeAround\RelationClient $relation + * @property \EasyWeChat\OfficialAccount\ShakeAround\StatsClient $stats + */ +class ShakeAround extends Client +{ + /** + * @param string $property + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function __get($property) + { + if (isset($this->app["shake_around.{$property}"])) { + return $this->app["shake_around.{$property}"]; + } + + throw new InvalidArgumentException(sprintf('No shake_around service named "%s".', $property)); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/StatsClient.php b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/StatsClient.php new file mode 100644 index 0000000..1b52ef7 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/ShakeAround/StatsClient.php @@ -0,0 +1,102 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\ShakeAround; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class StatsClient. + * + * @author allen05ren + */ +class StatsClient extends BaseClient +{ + /** + * Fetch statistics data by deviceId. + * + * @param int $beginTime (Unix timestamp) + * @param int $endTime (Unix timestamp) + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function deviceSummary(array $deviceIdentifier, int $beginTime, int $endTime) + { + $params = [ + 'device_identifier' => $deviceIdentifier, + 'begin_date' => $beginTime, + 'end_date' => $endTime, + ]; + + return $this->httpPostJson('shakearound/statistics/device', $params); + } + + /** + * Fetch all devices statistics data by date. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function devicesSummary(int $timestamp, int $pageIndex) + { + $params = [ + 'date' => $timestamp, + 'page_index' => $pageIndex, + ]; + + return $this->httpPostJson('shakearound/statistics/devicelist', $params); + } + + /** + * Fetch statistics data by pageId. + * + * @param int $beginTime (Unix timestamp) + * @param int $endTime (Unix timestamp) + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function pageSummary(int $pageId, int $beginTime, int $endTime) + { + $params = [ + 'page_id' => $pageId, + 'begin_date' => $beginTime, + 'end_date' => $endTime, + ]; + + return $this->httpPostJson('shakearound/statistics/page', $params); + } + + /** + * Fetch all pages statistics data by date. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function pagesSummary(int $timestamp, int $pageIndex) + { + $params = [ + 'date' => $timestamp, + 'page_index' => $pageIndex, + ]; + + return $this->httpPostJson('shakearound/statistics/pagelist', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Store/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/Store/Client.php new file mode 100644 index 0000000..5b2bab7 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Store/Client.php @@ -0,0 +1,188 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Store; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author bigface + */ +class Client extends BaseClient +{ + /** + * Get WXA supported categories. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function categories() + { + return $this->httpGet('wxa/get_merchant_category'); + } + + /** + * Get district from tencent map . + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function districts() + { + return $this->httpGet('wxa/get_district'); + } + + /** + * Search store from tencent map. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function searchFromMap(int $districtId, string $keyword) + { + $params = [ + 'districtid' => $districtId, + 'keyword' => $keyword, + ]; + + return $this->httpPostJson('wxa/search_map_poi', $params); + } + + /** + * Get store check status. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getStatus() + { + return $this->httpPostJson('wxa/get_merchant_audit_info'); + } + + /** + * Create a merchant. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function createMerchant(array $baseInfo) + { + return $this->httpPostJson('wxa/apply_merchant', $baseInfo); + } + + /** + * Update a merchant. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function updateMerchant(array $params) + { + return $this->httpPostJson('wxa/modify_merchant', $params); + } + + /** + * Create a store from tencent map. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function createFromMap(array $baseInfo) + { + return $this->httpPostJson('wxa/create_map_poi', $baseInfo); + } + + /** + * Create a store. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create(array $baseInfo) + { + return $this->httpPostJson('wxa/add_store', $baseInfo); + } + + /** + * Update a store. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(int $poiId, array $baseInfo) + { + $params = array_merge($baseInfo, ['poi_id' => $poiId]); + + return $this->httpPostJson('wxa/update_store', $params); + } + + /** + * Get store by ID. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(int $poiId) + { + return $this->httpPostJson('wxa/get_store_info', ['poi_id' => $poiId]); + } + + /** + * List store. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function list(int $offset = 0, int $limit = 10) + { + $params = [ + 'offset' => $offset, + 'limit' => $limit, + ]; + + return $this->httpPostJson('wxa/get_store_list', $params); + } + + /** + * Delete a store. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(int $poiId) + { + return $this->httpPostJson('wxa/del_store', ['poi_id' => $poiId]); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/Store/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/Store/ServiceProvider.php new file mode 100644 index 0000000..f5c48d7 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/Store/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\Store; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author bigface + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['store'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/SubscribeMessage/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/SubscribeMessage/Client.php new file mode 100644 index 0000000..9dbb57b --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/SubscribeMessage/Client.php @@ -0,0 +1,193 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\SubscribeMessage; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use ReflectionClass; + +/** + * Class Client. + * + * @author hugo + */ +class Client extends BaseClient +{ + /** + * {@inheritdoc}. + */ + protected $message = [ + 'touser' => '', + 'template_id' => '', + 'page' => '', + 'miniprogram' => '', + 'data' => [], + ]; + + /** + * {@inheritdoc}. + */ + protected $required = ['touser', 'template_id', 'data']; + + /** + * Combine templates and add them to your personal template library under your account. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function addTemplate(string $tid, array $kidList, string $sceneDesc = null) + { + $sceneDesc = $sceneDesc ?? ''; + $data = \compact('tid', 'kidList', 'sceneDesc'); + + return $this->httpPost('wxaapi/newtmpl/addtemplate', $data); + } + + /** + * Delete personal template under account. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function deleteTemplate(string $id) + { + return $this->httpPost('wxaapi/newtmpl/deltemplate', ['priTmplId' => $id]); + } + + /** + * Get the category of the applet account. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getCategory() + { + return $this->httpGet('wxaapi/newtmpl/getcategory'); + } + + /** + * Get keyword list under template title. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getTemplateKeywords(string $tid) + { + return $this->httpGet('wxaapi/newtmpl/getpubtemplatekeywords', compact('tid')); + } + + /** + * Get the title of the public template under the category to which the account belongs. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getTemplateTitles(array $ids, int $start = 0, int $limit = 30) + { + $ids = \implode(',', $ids); + $query = \compact('ids', 'start', 'limit'); + + return $this->httpGet('wxaapi/newtmpl/getpubtemplatetitles', $query); + } + + /** + * Get list of personal templates under the current account. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getTemplates() + { + return $this->httpGet('wxaapi/newtmpl/gettemplate'); + } + + /** + * Send a subscribe message. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function send(array $data = []) + { + $params = $this->formatMessage($data); + + $this->restoreMessage(); + + return $this->httpPostJson('cgi-bin/message/subscribe/bizsend', $params); + } + + /** + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + protected function formatMessage(array $data = []) + { + $params = array_merge($this->message, $data); + + foreach ($params as $key => $value) { + if (in_array($key, $this->required, true) && empty($value) && empty($this->message[$key])) { + throw new InvalidArgumentException(sprintf('Attribute "%s" can not be empty!', $key)); + } + + $params[$key] = empty($value) ? $this->message[$key] : $value; + } + + foreach ($params['data'] as $key => $value) { + if (is_array($value)) { + if (\array_key_exists('value', $value)) { + $params['data'][$key] = ['value' => $value['value']]; + + continue; + } + + if (count($value) >= 1) { + $value = [ + 'value' => $value[0], +// 'color' => $value[1],// color unsupported + ]; + } + } else { + $value = [ + 'value' => strval($value), + ]; + } + + $params['data'][$key] = $value; + } + + return $params; + } + + /** + * Restore message. + */ + protected function restoreMessage() + { + $this->message = (new ReflectionClass(static::class))->getDefaultProperties()['message']; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/SubscribeMessage/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/SubscribeMessage/ServiceProvider.php new file mode 100644 index 0000000..babe11f --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/SubscribeMessage/ServiceProvider.php @@ -0,0 +1,28 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\SubscribeMessage; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['subscribe_message'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/TemplateMessage/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/TemplateMessage/Client.php new file mode 100644 index 0000000..11f592d --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/TemplateMessage/Client.php @@ -0,0 +1,226 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\TemplateMessage; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use ReflectionClass; + +/** + * Class Client. + * + * @author overtrue + */ +class Client extends BaseClient +{ + public const API_SEND = 'cgi-bin/message/template/send'; + + /** + * Attributes. + * + * @var array + */ + protected $message = [ + 'touser' => '', + 'template_id' => '', + 'url' => '', + 'data' => [], + 'miniprogram' => '', + ]; + + /** + * Required attributes. + * + * @var array + */ + protected $required = ['touser', 'template_id']; + + /** + * Set industry. + * + * @param int $industryOne + * @param int $industryTwo + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setIndustry($industryOne, $industryTwo) + { + $params = [ + 'industry_id1' => $industryOne, + 'industry_id2' => $industryTwo, + ]; + + return $this->httpPostJson('cgi-bin/template/api_set_industry', $params); + } + + /** + * Get industry. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getIndustry() + { + return $this->httpPostJson('cgi-bin/template/get_industry'); + } + + /** + * Add a template and get template ID. + * + * @param string $shortId + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function addTemplate($shortId) + { + $params = ['template_id_short' => $shortId]; + + return $this->httpPostJson('cgi-bin/template/api_add_template', $params); + } + + /** + * Get private templates. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getPrivateTemplates() + { + return $this->httpPostJson('cgi-bin/template/get_all_private_template'); + } + + /** + * Delete private template. + * + * @param string $templateId + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function deletePrivateTemplate($templateId) + { + $params = ['template_id' => $templateId]; + + return $this->httpPostJson('cgi-bin/template/del_private_template', $params); + } + + /** + * Send a template message. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function send(array $data = []) + { + $params = $this->formatMessage($data); + + $this->restoreMessage(); + + return $this->httpPostJson(static::API_SEND, $params); + } + + /** + * Send template-message for subscription. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function sendSubscription(array $data = []) + { + $params = $this->formatMessage($data); + + $this->restoreMessage(); + + return $this->httpPostJson('cgi-bin/message/template/subscribe', $params); + } + + /** + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + protected function formatMessage(array $data = []) + { + $params = array_merge($this->message, $data); + + foreach ($params as $key => $value) { + if (in_array($key, $this->required, true) && empty($value) && empty($this->message[$key])) { + throw new InvalidArgumentException(sprintf('Attribute "%s" can not be empty!', $key)); + } + + $params[$key] = empty($value) ? $this->message[$key] : $value; + } + + $params['data'] = $this->formatData($params['data'] ?? []); + + return $params; + } + + /** + * @return array + */ + protected function formatData(array $data) + { + $formatted = []; + + foreach ($data as $key => $value) { + if (is_array($value)) { + if (\array_key_exists('value', $value)) { + $formatted[$key] = $value; + + continue; + } + + if (count($value) >= 2) { + $value = [ + 'value' => $value[0], + 'color' => $value[1], + ]; + } + } else { + $value = [ + 'value' => strval($value), + ]; + } + + $formatted[$key] = $value; + } + + return $formatted; + } + + /** + * Restore message. + */ + protected function restoreMessage() + { + $this->message = (new ReflectionClass(static::class))->getDefaultProperties()['message']; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/TemplateMessage/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/TemplateMessage/ServiceProvider.php new file mode 100644 index 0000000..98476fc --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/TemplateMessage/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\TemplateMessage; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['template_message'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/User/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/User/ServiceProvider.php new file mode 100644 index 0000000..c11d8b3 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/User/ServiceProvider.php @@ -0,0 +1,35 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\User; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['user'] = function ($app) { + return new UserClient($app); + }; + + $app['user_tag'] = function ($app) { + return new TagClient($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/User/TagClient.php b/vendor/overtrue/wechat/src/OfficialAccount/User/TagClient.php new file mode 100644 index 0000000..9c145c8 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/User/TagClient.php @@ -0,0 +1,157 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\User; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class TagClient. + * + * @author overtrue + */ +class TagClient extends BaseClient +{ + /** + * Create tag. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create(string $name) + { + $params = [ + 'tag' => ['name' => $name], + ]; + + return $this->httpPostJson('cgi-bin/tags/create', $params); + } + + /** + * List all tags. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function list() + { + return $this->httpGet('cgi-bin/tags/get'); + } + + /** + * Update a tag name. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(int $tagId, string $name) + { + $params = [ + 'tag' => [ + 'id' => $tagId, + 'name' => $name, + ], + ]; + + return $this->httpPostJson('cgi-bin/tags/update', $params); + } + + /** + * Delete tag. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(int $tagId) + { + $params = [ + 'tag' => ['id' => $tagId], + ]; + + return $this->httpPostJson('cgi-bin/tags/delete', $params); + } + + /** + * Get user tags. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function userTags(string $openid) + { + $params = ['openid' => $openid]; + + return $this->httpPostJson('cgi-bin/tags/getidlist', $params); + } + + /** + * Get users from a tag. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function usersOfTag(int $tagId, string $nextOpenId = '') + { + $params = [ + 'tagid' => $tagId, + 'next_openid' => $nextOpenId, + ]; + + return $this->httpPostJson('cgi-bin/user/tag/get', $params); + } + + /** + * Batch tag users. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function tagUsers(array $openids, int $tagId) + { + $params = [ + 'openid_list' => $openids, + 'tagid' => $tagId, + ]; + + return $this->httpPostJson('cgi-bin/tags/members/batchtagging', $params); + } + + /** + * Untag users from a tag. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function untagUsers(array $openids, int $tagId) + { + $params = [ + 'openid_list' => $openids, + 'tagid' => $tagId, + ]; + + return $this->httpPostJson('cgi-bin/tags/members/batchuntagging', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/User/UserClient.php b/vendor/overtrue/wechat/src/OfficialAccount/User/UserClient.php new file mode 100644 index 0000000..625ae0f --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/User/UserClient.php @@ -0,0 +1,158 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\User; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class UserClient. + * + * @author overtrue + */ +class UserClient extends BaseClient +{ + /** + * Fetch a user by open id. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function get(string $openid, string $lang = 'zh_CN') + { + $params = [ + 'openid' => $openid, + 'lang' => $lang, + ]; + + return $this->httpGet('cgi-bin/user/info', $params); + } + + /** + * Batch get users. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function select(array $openids, string $lang = 'zh_CN') + { + return $this->httpPostJson('cgi-bin/user/info/batchget', [ + 'user_list' => array_map(function ($openid) use ($lang) { + return [ + 'openid' => $openid, + 'lang' => $lang, + ]; + }, $openids), + ]); + } + + /** + * List users. + * + * @param string $nextOpenId + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function list(string $nextOpenId = null) + { + $params = ['next_openid' => $nextOpenId]; + + return $this->httpGet('cgi-bin/user/get', $params); + } + + /** + * Set user remark. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function remark(string $openid, string $remark) + { + $params = [ + 'openid' => $openid, + 'remark' => $remark, + ]; + + return $this->httpPostJson('cgi-bin/user/info/updateremark', $params); + } + + /** + * Get black list. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function blacklist(string $beginOpenid = null) + { + $params = ['begin_openid' => $beginOpenid]; + + return $this->httpPostJson('cgi-bin/tags/members/getblacklist', $params); + } + + /** + * Batch block user. + * + * @param array|string $openidList + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function block($openidList) + { + $params = ['openid_list' => (array) $openidList]; + + return $this->httpPostJson('cgi-bin/tags/members/batchblacklist', $params); + } + + /** + * Batch unblock user. + * + * @param array $openidList + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function unblock($openidList) + { + $params = ['openid_list' => (array) $openidList]; + + return $this->httpPostJson('cgi-bin/tags/members/batchunblacklist', $params); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function changeOpenid(string $oldAppId, array $openidList) + { + $params = [ + 'from_appid' => $oldAppId, + 'openid_list' => $openidList, + ]; + + return $this->httpPostJson('cgi-bin/changeopenid', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/WiFi/CardClient.php b/vendor/overtrue/wechat/src/OfficialAccount/WiFi/CardClient.php new file mode 100644 index 0000000..9e58b0d --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/WiFi/CardClient.php @@ -0,0 +1,48 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\WiFi; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class CardClient. + * + * @author her-cat + */ +class CardClient extends BaseClient +{ + /** + * Set shop card coupon delivery information. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function set(array $data) + { + return $this->httpPostJson('bizwifi/couponput/set', $data); + } + + /** + * Get shop card coupon delivery information. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(int $shopId = 0) + { + return $this->httpPostJson('bizwifi/couponput/get', ['shop_id' => $shopId]); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/WiFi/Client.php b/vendor/overtrue/wechat/src/OfficialAccount/WiFi/Client.php new file mode 100644 index 0000000..638a8c9 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/WiFi/Client.php @@ -0,0 +1,86 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\WiFi; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author her-cat + */ +class Client extends BaseClient +{ + /** + * Get Wi-Fi statistics. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function summary(string $beginDate, string $endDate, int $shopId = -1) + { + $data = [ + 'begin_date' => $beginDate, + 'end_date' => $endDate, + 'shop_id' => $shopId, + ]; + + return $this->httpPostJson('bizwifi/statistics/list', $data); + } + + /** + * Get the material QR code. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getQrCodeUrl(int $shopId, string $ssid, int $type = 0) + { + $data = [ + 'shop_id' => $shopId, + 'ssid' => $ssid, + 'img_id' => $type, + ]; + + return $this->httpPostJson('bizwifi/qrcode/get', $data); + } + + /** + * Wi-Fi completion page jump applet. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setFinishPage(array $data) + { + return $this->httpPostJson('bizwifi/finishpage/set', $data); + } + + /** + * Set the top banner jump applet. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setHomePage(array $data) + { + return $this->httpPostJson('bizwifi/homepage/set', $data); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/WiFi/DeviceClient.php b/vendor/overtrue/wechat/src/OfficialAccount/WiFi/DeviceClient.php new file mode 100644 index 0000000..99ff8fd --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/WiFi/DeviceClient.php @@ -0,0 +1,110 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\WiFi; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class DeviceClient. + * + * @author her-cat + */ +class DeviceClient extends BaseClient +{ + /** + * Add a password device. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function addPasswordDevice(int $shopId, string $ssid, string $password) + { + $data = [ + 'shop_id' => $shopId, + 'ssid' => $ssid, + 'password' => $password, + ]; + + return $this->httpPostJson('bizwifi/device/add', $data); + } + + /** + * Add a portal device. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function addPortalDevice(int $shopId, string $ssid, bool $reset = false) + { + $data = [ + 'shop_id' => $shopId, + 'ssid' => $ssid, + 'reset' => $reset, + ]; + + return $this->httpPostJson('bizwifi/apportal/register', $data); + } + + /** + * Delete device by MAC address. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(string $macAddress) + { + return $this->httpPostJson('bizwifi/device/delete', ['bssid' => $macAddress]); + } + + /** + * Get a list of devices. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function list(int $page = 1, int $size = 10) + { + $data = [ + 'pageindex' => $page, + 'pagesize' => $size, + ]; + + return $this->httpPostJson('bizwifi/device/list', $data); + } + + /** + * Get a list of devices by shop ID. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function listByShopId(int $shopId, int $page = 1, int $size = 10) + { + $data = [ + 'shop_id' => $shopId, + 'pageindex' => $page, + 'pagesize' => $size, + ]; + + return $this->httpPostJson('bizwifi/device/list', $data); + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/WiFi/ServiceProvider.php b/vendor/overtrue/wechat/src/OfficialAccount/WiFi/ServiceProvider.php new file mode 100644 index 0000000..7a28b51 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/WiFi/ServiceProvider.php @@ -0,0 +1,45 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\WiFi; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author her-cat + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc} + */ + public function register(Container $app) + { + $app['wifi'] = function ($app) { + return new Client($app); + }; + + $app['wifi_card'] = function ($app) { + return new CardClient($app); + }; + + $app['wifi_device'] = function ($app) { + return new DeviceClient($app); + }; + + $app['wifi_shop'] = function ($app) { + return new ShopClient($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OfficialAccount/WiFi/ShopClient.php b/vendor/overtrue/wechat/src/OfficialAccount/WiFi/ShopClient.php new file mode 100644 index 0000000..30cac21 --- /dev/null +++ b/vendor/overtrue/wechat/src/OfficialAccount/WiFi/ShopClient.php @@ -0,0 +1,89 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OfficialAccount\WiFi; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class ShopClient. + * + * @author her-cat + */ +class ShopClient extends BaseClient +{ + /** + * Get shop Wi-Fi information. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(int $shopId) + { + return $this->httpPostJson('bizwifi/shop/get', ['shop_id' => $shopId]); + } + + /** + * Get a list of Wi-Fi shops. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function list(int $page = 1, int $size = 10) + { + $data = [ + 'pageindex' => $page, + 'pagesize' => $size, + ]; + + return $this->httpPostJson('bizwifi/shop/list', $data); + } + + /** + * Update shop Wi-Fi information. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(int $shopId, array $data) + { + $data = array_merge(['shop_id' => $shopId], $data); + + return $this->httpPostJson('bizwifi/shop/update', $data); + } + + /** + * Clear shop network and equipment. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function clearDevice(int $shopId, string $ssid = null) + { + $data = [ + 'shop_id' => $shopId, + ]; + + if (!is_null($ssid)) { + $data['ssid'] = $ssid; + } + + return $this->httpPostJson('bizwifi/shop/clean', $data); + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Application.php b/vendor/overtrue/wechat/src/OpenPlatform/Application.php new file mode 100644 index 0000000..79faa4d --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Application.php @@ -0,0 +1,199 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform; + +use EasyWeChat\Kernel\ServiceContainer; +use EasyWeChat\Kernel\Traits\ResponseCastable; +use EasyWeChat\MiniProgram\Encryptor; +use EasyWeChat\OpenPlatform\Authorizer\Auth\AccessToken; +use EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Application as MiniProgram; +use EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Auth\Client; +use EasyWeChat\OpenPlatform\Authorizer\OfficialAccount\Account\Client as AccountClient; +use EasyWeChat\OpenPlatform\Authorizer\OfficialAccount\Application as OfficialAccount; +use EasyWeChat\OpenPlatform\Authorizer\OfficialAccount\OAuth\ComponentDelegate; +use EasyWeChat\OpenPlatform\Authorizer\Server\Guard; + +use function EasyWeChat\Kernel\data_get; + +/** + * Class Application. + * + * @property \EasyWeChat\OpenPlatform\Server\Guard $server + * @property \EasyWeChat\OpenPlatform\Auth\AccessToken $access_token + * @property \EasyWeChat\OpenPlatform\CodeTemplate\Client $code_template + * @property \EasyWeChat\OpenPlatform\Component\Client $component + * + * @method mixed handleAuthorize(string $authCode = null) + * @method mixed getAuthorizer(string $appId) + * @method mixed getAuthorizerOption(string $appId, string $name) + * @method mixed setAuthorizerOption(string $appId, string $name, string $value) + * @method mixed getAuthorizers(int $offset = 0, int $count = 500) + * @method mixed createPreAuthorizationCode() + */ +class Application extends ServiceContainer +{ + use ResponseCastable; + + /** + * @var array + */ + protected $providers = [ + Auth\ServiceProvider::class, + Base\ServiceProvider::class, + Server\ServiceProvider::class, + CodeTemplate\ServiceProvider::class, + Component\ServiceProvider::class, + ]; + + /** + * @var array + */ + protected $defaultConfig = [ + 'http' => [ + 'timeout' => 5.0, + 'base_uri' => 'https://api.weixin.qq.com/', + ], + ]; + + /** + * Creates the officialAccount application. + */ + public function officialAccount(string $appId, string $refreshToken = null, AccessToken $accessToken = null): OfficialAccount + { + $application = new OfficialAccount($this->getAuthorizerConfig($appId, $refreshToken), $this->getReplaceServices($accessToken) + [ + 'encryptor' => $this['encryptor'], + + 'account' => function ($app) { + return new AccountClient($app, $this); + }, + ]); + + $application->extend('oauth', function ($socialite) { + /* @var \Overtrue\Socialite\Providers\WeChatProvider $socialite */ + return $socialite->component(new ComponentDelegate($this)); + }); + + return $application; + } + + /** + * Creates the miniProgram application. + */ + public function miniProgram(string $appId, string $refreshToken = null, AccessToken $accessToken = null): MiniProgram + { + return new MiniProgram($this->getAuthorizerConfig($appId, $refreshToken), $this->getReplaceServices($accessToken) + [ + 'encryptor' => function () { + return new Encryptor($this['config']['app_id'], $this['config']['token'], $this['config']['aes_key']); + }, + + 'auth' => function ($app) { + return new Client($app, $this); + }, + ]); + } + + /** + * Return the pre-authorization login page url. + * + * @param string|array|null $optional + */ + public function getPreAuthorizationUrl(string $callbackUrl, $optional = []): string + { + // 兼容旧版 API 设计 + if (\is_string($optional)) { + $optional = [ + 'pre_auth_code' => $optional, + ]; + } else { + $optional['pre_auth_code'] = data_get($this->createPreAuthorizationCode(), 'pre_auth_code'); + } + + $queries = \array_merge($optional, [ + 'component_appid' => $this['config']['app_id'], + 'redirect_uri' => $callbackUrl, + ]); + + return 'https://mp.weixin.qq.com/cgi-bin/componentloginpage?'.http_build_query($queries); + } + + /** + * Return the pre-authorization login page url (mobile). + * + * @param string|array|null $optional + * + * @return string + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + public function getMobilePreAuthorizationUrl(string $callbackUrl, $optional = []): string + { + // 兼容旧版 API 设计 + if (\is_string($optional)) { + $optional = [ + 'pre_auth_code' => $optional, + ]; + } else { + $optional['pre_auth_code'] = data_get($this->createPreAuthorizationCode(), 'pre_auth_code'); + } + + $queries = \array_merge($optional, [ + 'component_appid' => $this['config']['app_id'], + 'redirect_uri' => $callbackUrl, + 'action' => 'bindcomponent', + 'no_scan' => 1, + ]); + + return 'https://mp.weixin.qq.com/safe/bindcomponent?'.http_build_query($queries).'#wechat_redirect'; + } + + protected function getAuthorizerConfig(string $appId, string $refreshToken = null): array + { + return $this['config']->merge([ + 'component_app_id' => $this['config']['app_id'], + 'app_id' => $appId, + 'refresh_token' => $refreshToken, + ])->toArray(); + } + + protected function getReplaceServices(AccessToken $accessToken = null): array + { + $services = [ + 'access_token' => $accessToken ?: function ($app) { + return new AccessToken($app, $this); + }, + + 'server' => function ($app) { + return new Guard($app); + }, + ]; + + foreach (['cache', 'http_client', 'log', 'logger', 'request'] as $reuse) { + if (isset($this[$reuse])) { + $services[$reuse] = $this[$reuse]; + } + } + + return $services; + } + + /** + * Handle dynamic calls. + * + * @param string $method + * @param array $args + * + * @return mixed + */ + public function __call($method, $args) + { + return $this->base->$method(...$args); + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Auth/AccessToken.php b/vendor/overtrue/wechat/src/OpenPlatform/Auth/AccessToken.php new file mode 100644 index 0000000..a8cf5e4 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Auth/AccessToken.php @@ -0,0 +1,46 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Auth; + +use EasyWeChat\Kernel\AccessToken as BaseAccessToken; + +/** + * Class AccessToken. + * + * @author mingyoung + */ +class AccessToken extends BaseAccessToken +{ + /** + * @var string + */ + protected $requestMethod = 'POST'; + + /** + * @var string + */ + protected $tokenKey = 'component_access_token'; + + /** + * @var string + */ + protected $endpointToGetToken = 'cgi-bin/component/api_component_token'; + + protected function getCredentials(): array + { + return [ + 'component_appid' => $this->app['config']['app_id'], + 'component_appsecret' => $this->app['config']['secret'], + 'component_verify_ticket' => $this->app['verify_ticket']->getTicket(), + ]; + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Auth/ServiceProvider.php b/vendor/overtrue/wechat/src/OpenPlatform/Auth/ServiceProvider.php new file mode 100644 index 0000000..c60784b --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Auth/ServiceProvider.php @@ -0,0 +1,37 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Auth; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['verify_ticket'] = function ($app) { + return new VerifyTicket($app); + }; + + $app['access_token'] = function ($app) { + return new AccessToken($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Auth/VerifyTicket.php b/vendor/overtrue/wechat/src/OpenPlatform/Auth/VerifyTicket.php new file mode 100644 index 0000000..d2c495a --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Auth/VerifyTicket.php @@ -0,0 +1,83 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Auth; + +use EasyWeChat\Kernel\Exceptions\RuntimeException; +use EasyWeChat\Kernel\Traits\InteractsWithCache; +use EasyWeChat\OpenPlatform\Application; + +/** + * Class VerifyTicket. + * + * @author mingyoung + */ +class VerifyTicket +{ + use InteractsWithCache; + + /** + * @var \EasyWeChat\OpenPlatform\Application + */ + protected $app; + + /** + * Constructor. + */ + public function __construct(Application $app) + { + $this->app = $app; + } + + /** + * Put the credential `component_verify_ticket` in cache. + * + * @return $this + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function setTicket(string $ticket) + { + $this->getCache()->set($this->getCacheKey(), $ticket, 3600); + + if (!$this->getCache()->has($this->getCacheKey())) { + throw new RuntimeException('Failed to cache verify ticket.'); + } + + return $this; + } + + /** + * Get the credential `component_verify_ticket` from cache. + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function getTicket(): string + { + if ($cached = $this->getCache()->get($this->getCacheKey())) { + return $cached; + } + + throw new RuntimeException('Credential "component_verify_ticket" does not exist in cache.'); + } + + /** + * Get cache key. + */ + protected function getCacheKey(): string + { + return 'easywechat.open_platform.verify_ticket.'.$this->app['config']['app_id']; + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/Aggregate/Account/Client.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/Aggregate/Account/Client.php new file mode 100644 index 0000000..b062e3a --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/Aggregate/Account/Client.php @@ -0,0 +1,96 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\Aggregate\Account; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author Scholer + */ +class Client extends BaseClient +{ + /** + * 创建开放平台帐号并绑定公众号/小程序. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create() + { + $params = [ + 'appid' => $this->app['config']['app_id'], + ]; + + return $this->httpPostJson('cgi-bin/open/create', $params); + } + + /** + * 将公众号/小程序绑定到开放平台帐号下. + * + * @param string $openAppId 开放平台帐号appid + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function bindTo(string $openAppId) + { + $params = [ + 'appid' => $this->app['config']['app_id'], + 'open_appid' => $openAppId, + ]; + + return $this->httpPostJson('cgi-bin/open/bind', $params); + } + + /** + * 将公众号/小程序从开放平台帐号下解绑. + * + * @param string $openAppId 开放平台帐号appid + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function unbindFrom(string $openAppId) + { + $params = [ + 'appid' => $this->app['config']['app_id'], + 'open_appid' => $openAppId, + ]; + + return $this->httpPostJson('cgi-bin/open/unbind', $params); + } + + /** + * 获取公众号/小程序所绑定的开放平台帐号. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getBinding() + { + $params = [ + 'appid' => $this->app['config']['app_id'], + ]; + + return $this->httpPostJson('cgi-bin/open/get', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/Aggregate/AggregateServiceProvider.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/Aggregate/AggregateServiceProvider.php new file mode 100644 index 0000000..d93293d --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/Aggregate/AggregateServiceProvider.php @@ -0,0 +1,22 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\Aggregate; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class AggregateServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/Auth/AccessToken.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/Auth/AccessToken.php new file mode 100644 index 0000000..2c9d53c --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/Auth/AccessToken.php @@ -0,0 +1,73 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\Auth; + +use EasyWeChat\Kernel\AccessToken as BaseAccessToken; +use EasyWeChat\OpenPlatform\Application; +use Pimple\Container; + +/** + * Class AccessToken. + * + * @author mingyoung + */ +class AccessToken extends BaseAccessToken +{ + /** + * @var string + */ + protected $requestMethod = 'POST'; + + /** + * @var string + */ + protected $queryName = 'access_token'; + + /** + * {@inheritdoc}. + */ + protected $tokenKey = 'authorizer_access_token'; + + /** + * @var \EasyWeChat\OpenPlatform\Application + */ + protected $component; + + /** + * AuthorizerAccessToken constructor. + */ + public function __construct(Container $app, Application $component) + { + parent::__construct($app); + + $this->component = $component; + } + + /** + * {@inheritdoc}. + */ + protected function getCredentials(): array + { + return [ + 'component_appid' => $this->component['config']['app_id'], + 'authorizer_appid' => $this->app['config']['app_id'], + 'authorizer_refresh_token' => $this->app['config']['refresh_token'], + ]; + } + + public function getEndpoint(): string + { + return 'cgi-bin/component/api_authorizer_token?'.http_build_query([ + 'component_access_token' => $this->component['access_token']->getToken()['component_access_token'], + ]); + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Account/Client.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Account/Client.php new file mode 100644 index 0000000..f1b7afb --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Account/Client.php @@ -0,0 +1,79 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Account; + +use EasyWeChat\OpenPlatform\Authorizer\Aggregate\Account\Client as BaseClient; + +/** + * Class Client. + * + * @author ClouderSky + */ +class Client extends BaseClient +{ + /** + * 获取账号基本信息. + */ + public function getBasicInfo() + { + return $this->httpPostJson('cgi-bin/account/getaccountbasicinfo'); + } + + /** + * 修改头像. + * + * @param string $mediaId 头像素材mediaId + * @param string $left 剪裁框左上角x坐标(取值范围:[0, 1]) + * @param string $top 剪裁框左上角y坐标(取值范围:[0, 1]) + * @param string $right 剪裁框右下角x坐标(取值范围:[0, 1]) + * @param string $bottom 剪裁框右下角y坐标(取值范围:[0, 1]) + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function updateAvatar( + string $mediaId, + $left = '0.0', + $top = '0.0', + $right = '1.0', + $bottom = '1.0' + ) { + $params = [ + 'head_img_media_id' => $mediaId, + 'x1' => \strval($left), + 'y1' => \strval($top), + 'x2' => \strval($right), + 'y2' => \strval($bottom), + ]; + + return $this->httpPostJson('cgi-bin/account/modifyheadimage', $params); + } + + /** + * 修改功能介绍. + * + * @param string $signature 功能介绍(简介) + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function updateSignature(string $signature) + { + $params = ['signature' => $signature]; + + return $this->httpPostJson('cgi-bin/account/modifysignature', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Account/ServiceProvider.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Account/ServiceProvider.php new file mode 100644 index 0000000..f062954 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Account/ServiceProvider.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Account; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app['account'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Application.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Application.php new file mode 100644 index 0000000..85bb557 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Application.php @@ -0,0 +1,50 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\MiniProgram; + +use EasyWeChat\MiniProgram\Application as MiniProgram; +use EasyWeChat\OpenPlatform\Authorizer\Aggregate\AggregateServiceProvider; + +/** + * Class Application. + * + * @author mingyoung + * + * @property \EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Account\Client $account + * @property \EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Code\Client $code + * @property \EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Domain\Client $domain + * @property \EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Setting\Client $setting + * @property \EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Tester\Client $tester + */ +class Application extends MiniProgram +{ + /** + * Application constructor. + */ + public function __construct(array $config = [], array $prepends = []) + { + parent::__construct($config, $prepends); + + $providers = [ + AggregateServiceProvider::class, + Code\ServiceProvider::class, + Domain\ServiceProvider::class, + Account\ServiceProvider::class, + Setting\ServiceProvider::class, + Tester\ServiceProvider::class, + ]; + + foreach ($providers as $provider) { + $this->register(new $provider()); + } + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Auth/Client.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Auth/Client.php new file mode 100644 index 0000000..caf87d7 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Auth/Client.php @@ -0,0 +1,59 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Auth; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\ServiceContainer; +use EasyWeChat\OpenPlatform\Application; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * @var \EasyWeChat\OpenPlatform\Application + */ + protected $component; + + /** + * Client constructor. + */ + public function __construct(ServiceContainer $app, Application $component) + { + parent::__construct($app); + + $this->component = $component; + } + + /** + * Get session info by code. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function session(string $code) + { + $params = [ + 'appid' => $this->app['config']['app_id'], + 'js_code' => $code, + 'grant_type' => 'authorization_code', + 'component_appid' => $this->component['config']['app_id'], + 'component_access_token' => $this->component['access_token']->getToken()['component_access_token'], + ]; + + return $this->httpGet('sns/component/jscode2session', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Code/Client.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Code/Client.php new file mode 100644 index 0000000..c733f6a --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Code/Client.php @@ -0,0 +1,252 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Code; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function commit(int $templateId, string $extJson, string $version, string $description) + { + return $this->httpPostJson('wxa/commit', [ + 'template_id' => $templateId, + 'ext_json' => $extJson, + 'user_version' => $version, + 'user_desc' => $description, + ]); + } + + /** + * @return \EasyWeChat\Kernel\Http\Response + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getQrCode(string $path = null) + { + return $this->requestRaw('wxa/get_qrcode', 'GET', [ + 'query' => ['path' => $path], + ]); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getCategory() + { + return $this->httpGet('wxa/get_category'); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getPage() + { + return $this->httpGet('wxa/get_page'); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function submitAudit(array $data, string $feedbackInfo = null, string $feedbackStuff = null) + { + if (isset($data['item_list'])) { + return $this->httpPostJson('wxa/submit_audit', $data); + } + + return $this->httpPostJson('wxa/submit_audit', [ + 'item_list' => $data, + 'feedback_info' => $feedbackInfo, + 'feedback_stuff' => $feedbackStuff, + ]); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getAuditStatus(int $auditId) + { + return $this->httpPostJson('wxa/get_auditstatus', [ + 'auditid' => $auditId, + ]); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getLatestAuditStatus() + { + return $this->httpGet('wxa/get_latest_auditstatus'); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function release() + { + return $this->httpPostJson('wxa/release'); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function withdrawAudit() + { + return $this->httpGet('wxa/undocodeaudit'); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function rollbackRelease() + { + return $this->httpGet('wxa/revertcoderelease'); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function changeVisitStatus(string $action) + { + return $this->httpPostJson('wxa/change_visitstatus', [ + 'action' => $action, + ]); + } + + /** + * 分阶段发布. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function grayRelease(int $grayPercentage) + { + return $this->httpPostJson('wxa/grayrelease', [ + 'gray_percentage' => $grayPercentage, + ]); + } + + /** + * 取消分阶段发布. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function revertGrayRelease() + { + return $this->httpGet('wxa/revertgrayrelease'); + } + + /** + * 查询当前分阶段发布详情. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getGrayRelease() + { + return $this->httpGet('wxa/getgrayreleaseplan'); + } + + /** + * 查询当前设置的最低基础库版本及各版本用户占比. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getSupportVersion() + { + return $this->httpPostJson('cgi-bin/wxopen/getweappsupportversion'); + } + + /** + * 设置最低基础库版本. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setSupportVersion(string $version) + { + return $this->httpPostJson('cgi-bin/wxopen/setweappsupportversion', [ + 'version' => $version, + ]); + } + + /** + * 查询服务商的当月提审限额(quota)和加急次数. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function queryQuota() + { + return $this->httpGet('wxa/queryquota'); + } + + /** + * 加急审核申请. + * + * @param int $auditId 审核单ID + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function speedupAudit(int $auditId) + { + return $this->httpPostJson('wxa/speedupaudit', [ + 'auditid' => $auditId, + ]); + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Code/ServiceProvider.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Code/ServiceProvider.php new file mode 100644 index 0000000..bce8611 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Code/ServiceProvider.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Code; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app['code'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Domain/Client.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Domain/Client.php new file mode 100644 index 0000000..c065802 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Domain/Client.php @@ -0,0 +1,51 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Domain; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function modify(array $params) + { + return $this->httpPostJson('wxa/modify_domain', $params); + } + + /** + * 设置小程序业务域名. + * + * @param string $action + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setWebviewDomain(array $domains, $action = 'add') + { + return $this->httpPostJson('wxa/setwebviewdomain', [ + 'action' => $action, + 'webviewdomain' => $domains, + ]); + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Domain/ServiceProvider.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Domain/ServiceProvider.php new file mode 100644 index 0000000..4eaef13 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Domain/ServiceProvider.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Domain; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app['domain'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Setting/Client.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Setting/Client.php new file mode 100644 index 0000000..3186fda --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Setting/Client.php @@ -0,0 +1,247 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Setting; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author ClouderSky + */ +class Client extends BaseClient +{ + /** + * 获取账号可以设置的所有类目. + */ + public function getAllCategories() + { + return $this->httpPostJson('cgi-bin/wxopen/getallcategories'); + } + + /** + * 添加类目. + * + * @param array $categories 类目数组 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function addCategories(array $categories) + { + $params = ['categories' => $categories]; + + return $this->httpPostJson('cgi-bin/wxopen/addcategory', $params); + } + + /** + * 删除类目. + * + * @param int $firstId 一级类目ID + * @param int $secondId 二级类目ID + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function deleteCategories(int $firstId, int $secondId) + { + $params = ['first' => $firstId, 'second' => $secondId]; + + return $this->httpPostJson('cgi-bin/wxopen/deletecategory', $params); + } + + /** + * 获取账号已经设置的所有类目. + */ + public function getCategories() + { + return $this->httpPostJson('cgi-bin/wxopen/getcategory'); + } + + /** + * 修改类目. + * + * @param array $category 单个类目 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function updateCategory(array $category) + { + return $this->httpPostJson('cgi-bin/wxopen/modifycategory', $category); + } + + /** + * 小程序名称设置及改名. + * + * @param string $nickname 昵称 + * @param string $idCardMediaId 身份证照片素材ID + * @param string $licenseMediaId 组织机构代码证或营业执照素材ID + * @param array $otherStuffs 其他证明材料素材ID + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setNickname( + string $nickname, + string $idCardMediaId = '', + string $licenseMediaId = '', + array $otherStuffs = [] + ) { + $params = [ + 'nick_name' => $nickname, + 'id_card' => $idCardMediaId, + 'license' => $licenseMediaId, + ]; + + for ($i = \count($otherStuffs) - 1; $i >= 0; --$i) { + $params['naming_other_stuff_'.($i + 1)] = $otherStuffs[$i]; + } + + return $this->httpPostJson('wxa/setnickname', $params); + } + + /** + * 小程序改名审核状态查询. + * + * @param int $auditId 审核单id + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getNicknameAuditStatus($auditId) + { + $params = ['audit_id' => $auditId]; + + return $this->httpPostJson('wxa/api_wxa_querynickname', $params); + } + + /** + * 微信认证名称检测. + * + * @param string $nickname 名称(昵称) + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function isAvailableNickname($nickname) + { + $params = ['nick_name' => $nickname]; + + return $this->httpPostJson( + 'cgi-bin/wxverify/checkwxverifynickname', + $params + ); + } + + /** + * 查询小程序是否可被搜索. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getSearchStatus() + { + return $this->httpGet('wxa/getwxasearchstatus'); + } + + /** + * 设置小程序可被搜素. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setSearchable() + { + return $this->httpPostJson('wxa/changewxasearchstatus', [ + 'status' => 0, + ]); + } + + /** + * 设置小程序不可被搜素. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setUnsearchable() + { + return $this->httpPostJson('wxa/changewxasearchstatus', [ + 'status' => 1, + ]); + } + + /** + * 获取展示的公众号信息. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getDisplayedOfficialAccount() + { + return $this->httpGet('wxa/getshowwxaitem'); + } + + /** + * 设置展示的公众号. + * + * @param string|bool $appid + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setDisplayedOfficialAccount($appid) + { + return $this->httpPostJson('wxa/updateshowwxaitem', [ + 'appid' => $appid ?: null, + 'wxa_subscribe_biz_flag' => $appid ? 1 : 0, + ]); + } + + /** + * 获取可以用来设置的公众号列表. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getDisplayableOfficialAccounts(int $page, int $num) + { + return $this->httpGet('wxa/getwxamplinkforshow', [ + 'page' => $page, + 'num' => $num, + ]); + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Setting/ServiceProvider.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Setting/ServiceProvider.php new file mode 100644 index 0000000..917aec5 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Setting/ServiceProvider.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Setting; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app['setting'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Tester/Client.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Tester/Client.php new file mode 100644 index 0000000..6ff0901 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Tester/Client.php @@ -0,0 +1,67 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Tester; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author caikeal + */ +class Client extends BaseClient +{ + /** + * 绑定小程序体验者. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function bind(string $wechatId) + { + return $this->httpPostJson('wxa/bind_tester', [ + 'wechatid' => $wechatId, + ]); + } + + /** + * 解绑小程序体验者. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function unbind(string $wechatId) + { + return $this->httpPostJson('wxa/unbind_tester', [ + 'wechatid' => $wechatId, + ]); + } + + /** + * 获取体验者列表. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function list() + { + return $this->httpPostJson('wxa/memberauth', [ + 'action' => 'get_experiencer', + ]); + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Tester/ServiceProvider.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Tester/ServiceProvider.php new file mode 100644 index 0000000..ff1ffb3 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/MiniProgram/Tester/ServiceProvider.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Tester; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app['tester'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/OfficialAccount/Account/Client.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/OfficialAccount/Account/Client.php new file mode 100644 index 0000000..603bc3d --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/OfficialAccount/Account/Client.php @@ -0,0 +1,71 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\OfficialAccount\Account; + +use EasyWeChat\Kernel\ServiceContainer; +use EasyWeChat\OpenPlatform\Application; +use EasyWeChat\OpenPlatform\Authorizer\Aggregate\Account\Client as BaseClient; + +/** + * Class Client. + * + * @author Keal + */ +class Client extends BaseClient +{ + /** + * @var \EasyWeChat\OpenPlatform\Application + */ + protected $component; + + /** + * Client constructor. + */ + public function __construct(ServiceContainer $app, Application $component) + { + parent::__construct($app); + + $this->component = $component; + } + + /** + * 从第三方平台跳转至微信公众平台授权注册页面, 授权注册小程序. + */ + public function getFastRegistrationUrl(string $callbackUrl, bool $copyWxVerify = true): string + { + $queries = [ + 'copy_wx_verify' => $copyWxVerify, + 'component_appid' => $this->component['config']['app_id'], + 'appid' => $this->app['config']['app_id'], + 'redirect_uri' => $callbackUrl, + ]; + + return 'https://mp.weixin.qq.com/cgi-bin/fastregisterauth?'.http_build_query($queries); + } + + /** + * 小程序快速注册. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function register(string $ticket) + { + $params = [ + 'ticket' => $ticket, + ]; + + return $this->httpPostJson('cgi-bin/account/fastregister', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/OfficialAccount/Application.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/OfficialAccount/Application.php new file mode 100644 index 0000000..9e23778 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/OfficialAccount/Application.php @@ -0,0 +1,43 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\OfficialAccount; + +use EasyWeChat\OfficialAccount\Application as OfficialAccount; +use EasyWeChat\OpenPlatform\Authorizer\Aggregate\AggregateServiceProvider; + +/** + * Class Application. + * + * @author mingyoung + * + * @property \EasyWeChat\OpenPlatform\Authorizer\OfficialAccount\Account\Client $account + * @property \EasyWeChat\OpenPlatform\Authorizer\OfficialAccount\MiniProgram\Client $mini_program + */ +class Application extends OfficialAccount +{ + /** + * Application constructor. + */ + public function __construct(array $config = [], array $prepends = []) + { + parent::__construct($config, $prepends); + + $providers = [ + AggregateServiceProvider::class, + MiniProgram\ServiceProvider::class, + ]; + + foreach ($providers as $provider) { + $this->register(new $provider()); + } + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/OfficialAccount/MiniProgram/Client.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/OfficialAccount/MiniProgram/Client.php new file mode 100644 index 0000000..e5d4e1f --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/OfficialAccount/MiniProgram/Client.php @@ -0,0 +1,71 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\OfficialAccount\MiniProgram; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author Keal + */ +class Client extends BaseClient +{ + /** + * 获取公众号关联的小程序. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function list() + { + return $this->httpPostJson('cgi-bin/wxopen/wxamplinkget'); + } + + /** + * 关联小程序. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function link(string $appId, bool $notifyUsers = true, bool $showProfile = false) + { + $params = [ + 'appid' => $appId, + 'notify_users' => (string) $notifyUsers, + 'show_profile' => (string) $showProfile, + ]; + + return $this->httpPostJson('cgi-bin/wxopen/wxamplink', $params); + } + + /** + * 解除已关联的小程序. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function unlink(string $appId) + { + $params = [ + 'appid' => $appId, + ]; + + return $this->httpPostJson('cgi-bin/wxopen/wxampunlink', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/OfficialAccount/MiniProgram/ServiceProvider.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/OfficialAccount/MiniProgram/ServiceProvider.php new file mode 100644 index 0000000..31ce10b --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/OfficialAccount/MiniProgram/ServiceProvider.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\OfficialAccount\MiniProgram; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app['mini_program'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/OfficialAccount/OAuth/ComponentDelegate.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/OfficialAccount/OAuth/ComponentDelegate.php new file mode 100644 index 0000000..4a1cab2 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/OfficialAccount/OAuth/ComponentDelegate.php @@ -0,0 +1,52 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\OfficialAccount\OAuth; + +use EasyWeChat\OpenPlatform\Application; +use Overtrue\Socialite\WeChatComponentInterface; + +/** + * Class ComponentDelegate. + * + * @author mingyoung + */ +class ComponentDelegate implements WeChatComponentInterface +{ + /** + * @var \EasyWeChat\OpenPlatform\Application + */ + protected $app; + + /** + * ComponentDelegate Constructor. + */ + public function __construct(Application $app) + { + $this->app = $app; + } + + /** + * @return string + */ + public function getAppId() + { + return $this->app['config']['app_id']; + } + + /** + * @return string + */ + public function getToken() + { + return $this->app['access_token']->getToken()['component_access_token']; + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/Server/Guard.php b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/Server/Guard.php new file mode 100644 index 0000000..d7efbd3 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Authorizer/Server/Guard.php @@ -0,0 +1,32 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Authorizer\Server; + +use EasyWeChat\Kernel\ServerGuard; + +/** + * Class Guard. + * + * @author mingyoung + */ +class Guard extends ServerGuard +{ + /** + * Get token from OpenPlatform encryptor. + * + * @return string + */ + protected function getToken() + { + return $this->app['encryptor']->getToken(); + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Base/Client.php b/vendor/overtrue/wechat/src/OpenPlatform/Base/Client.php new file mode 100644 index 0000000..57baa65 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Base/Client.php @@ -0,0 +1,155 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Base; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * Get authorization info. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function handleAuthorize(string $authCode = null) + { + $params = [ + 'component_appid' => $this->app['config']['app_id'], + 'authorization_code' => $authCode ?? $this->app['request']->get('auth_code'), + ]; + + return $this->httpPostJson('cgi-bin/component/api_query_auth', $params); + } + + /** + * Get authorizer info. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getAuthorizer(string $appId) + { + $params = [ + 'component_appid' => $this->app['config']['app_id'], + 'authorizer_appid' => $appId, + ]; + + return $this->httpPostJson('cgi-bin/component/api_get_authorizer_info', $params); + } + + /** + * Get options. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getAuthorizerOption(string $appId, string $name) + { + $params = [ + 'component_appid' => $this->app['config']['app_id'], + 'authorizer_appid' => $appId, + 'option_name' => $name, + ]; + + return $this->httpPostJson('cgi-bin/component/api_get_authorizer_option', $params); + } + + /** + * Set authorizer option. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setAuthorizerOption(string $appId, string $name, string $value) + { + $params = [ + 'component_appid' => $this->app['config']['app_id'], + 'authorizer_appid' => $appId, + 'option_name' => $name, + 'option_value' => $value, + ]; + + return $this->httpPostJson('cgi-bin/component/api_set_authorizer_option', $params); + } + + /** + * Get authorizer list. + * + * @param int $offset + * @param int $count + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getAuthorizers($offset = 0, $count = 500) + { + $params = [ + 'component_appid' => $this->app['config']['app_id'], + 'offset' => $offset, + 'count' => $count, + ]; + + return $this->httpPostJson('cgi-bin/component/api_get_authorizer_list', $params); + } + + /** + * Create pre-authorization code. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function createPreAuthorizationCode() + { + $params = [ + 'component_appid' => $this->app['config']['app_id'], + ]; + + return $this->httpPostJson('cgi-bin/component/api_create_preauthcode', $params); + } + + /** + * OpenPlatform Clear quota. + * + * @see https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419318587 + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function clearQuota() + { + $params = [ + 'component_appid' => $this->app['config']['app_id'], + ]; + + return $this->httpPostJson('cgi-bin/component/clear_quota', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Base/ServiceProvider.php b/vendor/overtrue/wechat/src/OpenPlatform/Base/ServiceProvider.php new file mode 100644 index 0000000..e647c41 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Base/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Base; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['base'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/CodeTemplate/Client.php b/vendor/overtrue/wechat/src/OpenPlatform/CodeTemplate/Client.php new file mode 100644 index 0000000..5949eb0 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/CodeTemplate/Client.php @@ -0,0 +1,84 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\CodeTemplate; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author caikeal + */ +class Client extends BaseClient +{ + /** + * 获取草稿箱内的所有临时代码草稿 + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getDrafts() + { + return $this->httpGet('wxa/gettemplatedraftlist'); + } + + /** + * 将草稿箱的草稿选为小程序代码模版. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function createFromDraft(int $draftId) + { + $params = [ + 'draft_id' => $draftId, + ]; + + return $this->httpPostJson('wxa/addtotemplate', $params); + } + + /** + * 获取代码模版库中的所有小程序代码模版. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function list() + { + return $this->httpGet('wxa/gettemplatelist'); + } + + /** + * 删除指定小程序代码模版. + * + * @param string $templateId + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete($templateId) + { + $params = [ + 'template_id' => $templateId, + ]; + + return $this->httpPostJson('wxa/deletetemplate', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/CodeTemplate/ServiceProvider.php b/vendor/overtrue/wechat/src/OpenPlatform/CodeTemplate/ServiceProvider.php new file mode 100644 index 0000000..ab543a7 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/CodeTemplate/ServiceProvider.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\CodeTemplate; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app['code_template'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Component/Client.php b/vendor/overtrue/wechat/src/OpenPlatform/Component/Client.php new file mode 100644 index 0000000..b7d1402 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Component/Client.php @@ -0,0 +1,54 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Component; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author dudashuang + */ +class Client extends BaseClient +{ + /** + * 通过法人微信快速创建小程序. + * + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function registerMiniProgram(array $params) + { + return $this->httpPostJson('cgi-bin/component/fastregisterweapp', $params, ['action' => 'create']); + } + + /** + * 查询创建任务状态. + * + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getRegistrationStatus(string $companyName, string $legalPersonaWechat, string $legalPersonaName) + { + $params = [ + 'name' => $companyName, + 'legal_persona_wechat' => $legalPersonaWechat, + 'legal_persona_name' => $legalPersonaName, + ]; + + return $this->httpPostJson('cgi-bin/component/fastregisterweapp', $params, ['action' => 'search']); + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Component/ServiceProvider.php b/vendor/overtrue/wechat/src/OpenPlatform/Component/ServiceProvider.php new file mode 100644 index 0000000..83e309f --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Component/ServiceProvider.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Component; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app['component'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Server/Guard.php b/vendor/overtrue/wechat/src/OpenPlatform/Server/Guard.php new file mode 100644 index 0000000..ca0f1d2 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Server/Guard.php @@ -0,0 +1,63 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Server; + +use EasyWeChat\Kernel\ServerGuard; +use EasyWeChat\OpenPlatform\Server\Handlers\Authorized; +use EasyWeChat\OpenPlatform\Server\Handlers\Unauthorized; +use EasyWeChat\OpenPlatform\Server\Handlers\UpdateAuthorized; +use EasyWeChat\OpenPlatform\Server\Handlers\VerifyTicketRefreshed; +use Symfony\Component\HttpFoundation\Response; +use function EasyWeChat\Kernel\data_get; + +/** + * Class Guard. + * + * @author mingyoung + */ +class Guard extends ServerGuard +{ + public const EVENT_AUTHORIZED = 'authorized'; + public const EVENT_UNAUTHORIZED = 'unauthorized'; + public const EVENT_UPDATE_AUTHORIZED = 'updateauthorized'; + public const EVENT_COMPONENT_VERIFY_TICKET = 'component_verify_ticket'; + public const EVENT_THIRD_FAST_REGISTERED = 'notify_third_fasteregister'; + + /** + * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + protected function resolve(): Response + { + $this->registerHandlers(); + + $message = $this->getMessage(); + + if ($infoType = data_get($message, 'InfoType')) { + $this->dispatch($infoType, $message); + } + + return new Response(static::SUCCESS_EMPTY_RESPONSE); + } + + /** + * Register event handlers. + */ + protected function registerHandlers() + { + $this->on(self::EVENT_AUTHORIZED, Authorized::class); + $this->on(self::EVENT_UNAUTHORIZED, Unauthorized::class); + $this->on(self::EVENT_UPDATE_AUTHORIZED, UpdateAuthorized::class); + $this->on(self::EVENT_COMPONENT_VERIFY_TICKET, VerifyTicketRefreshed::class); + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Server/Handlers/Authorized.php b/vendor/overtrue/wechat/src/OpenPlatform/Server/Handlers/Authorized.php new file mode 100644 index 0000000..fe7f9c6 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Server/Handlers/Authorized.php @@ -0,0 +1,30 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Server\Handlers; + +use EasyWeChat\Kernel\Contracts\EventHandlerInterface; + +/** + * Class Authorized. + * + * @author mingyoung + */ +class Authorized implements EventHandlerInterface +{ + /** + * {@inheritdoc}. + */ + public function handle($payload = null) + { + // Do nothing for the time being. + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Server/Handlers/Unauthorized.php b/vendor/overtrue/wechat/src/OpenPlatform/Server/Handlers/Unauthorized.php new file mode 100644 index 0000000..158228b --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Server/Handlers/Unauthorized.php @@ -0,0 +1,30 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Server\Handlers; + +use EasyWeChat\Kernel\Contracts\EventHandlerInterface; + +/** + * Class Unauthorized. + * + * @author mingyoung + */ +class Unauthorized implements EventHandlerInterface +{ + /** + * {@inheritdoc}. + */ + public function handle($payload = null) + { + // Do nothing for the time being. + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Server/Handlers/UpdateAuthorized.php b/vendor/overtrue/wechat/src/OpenPlatform/Server/Handlers/UpdateAuthorized.php new file mode 100644 index 0000000..e73caa5 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Server/Handlers/UpdateAuthorized.php @@ -0,0 +1,30 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Server\Handlers; + +use EasyWeChat\Kernel\Contracts\EventHandlerInterface; + +/** + * Class UpdateAuthorized. + * + * @author mingyoung + */ +class UpdateAuthorized implements EventHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function handle($payload = null) + { + // Do nothing for the time being. + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Server/Handlers/VerifyTicketRefreshed.php b/vendor/overtrue/wechat/src/OpenPlatform/Server/Handlers/VerifyTicketRefreshed.php new file mode 100644 index 0000000..67fe661 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Server/Handlers/VerifyTicketRefreshed.php @@ -0,0 +1,50 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Server\Handlers; + +use EasyWeChat\Kernel\Contracts\EventHandlerInterface; +use EasyWeChat\OpenPlatform\Application; + +use function EasyWeChat\Kernel\data_get; + +/** + * Class VerifyTicketRefreshed. + * + * @author mingyoung + */ +class VerifyTicketRefreshed implements EventHandlerInterface +{ + /** + * @var \EasyWeChat\OpenPlatform\Application + */ + protected $app; + + /** + * Constructor. + */ + public function __construct(Application $app) + { + $this->app = $app; + } + + /** + * {@inheritdoc}. + */ + public function handle($payload = null) + { + $ticket = data_get($payload, 'ComponentVerifyTicket'); + + if (!empty($ticket)) { + $this->app['verify_ticket']->setTicket($ticket); + } + } +} diff --git a/vendor/overtrue/wechat/src/OpenPlatform/Server/ServiceProvider.php b/vendor/overtrue/wechat/src/OpenPlatform/Server/ServiceProvider.php new file mode 100644 index 0000000..7747061 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenPlatform/Server/ServiceProvider.php @@ -0,0 +1,34 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenPlatform\Server; + +use EasyWeChat\Kernel\Encryptor; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app['encryptor'] = function ($app) { + return new Encryptor( + $app['config']['app_id'], + $app['config']['token'], + $app['config']['aes_key'] + ); + }; + + $app['server'] = function ($app) { + return new Guard($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OpenWork/Application.php b/vendor/overtrue/wechat/src/OpenWork/Application.php new file mode 100644 index 0000000..47db6da --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenWork/Application.php @@ -0,0 +1,81 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenWork; + +use EasyWeChat\Kernel\ServiceContainer; +use EasyWeChat\OpenWork\Work\Application as Work; + +/** + * Application. + * + * @author xiaomin + * + * @property \EasyWeChat\OpenWork\Server\Guard $server + * @property \EasyWeChat\OpenWork\Corp\Client $corp + * @property \EasyWeChat\OpenWork\Provider\Client $provider + * @property \EasyWeChat\OpenWork\SuiteAuth\AccessToken $suite_access_token + * @property \EasyWeChat\OpenWork\Auth\AccessToken $provider_access_token + * @property \EasyWeChat\OpenWork\SuiteAuth\SuiteTicket $suite_ticket + * @property \EasyWeChat\OpenWork\MiniProgram\Auth\Client $mini_program + */ +class Application extends ServiceContainer +{ + /** + * @var array + */ + protected $providers = [ + Auth\ServiceProvider::class, + SuiteAuth\ServiceProvider::class, + Server\ServiceProvider::class, + Corp\ServiceProvider::class, + Provider\ServiceProvider::class, + MiniProgram\ServiceProvider::class, + ]; + + /** + * @var array + */ + protected $defaultConfig = [ + // http://docs.guzzlephp.org/en/stable/request-options.html + 'http' => [ + 'base_uri' => 'https://qyapi.weixin.qq.com/', + ], + ]; + + /** + * Creates the miniProgram application. + */ + public function miniProgram(): \EasyWeChat\Work\MiniProgram\Application + { + return new \EasyWeChat\Work\MiniProgram\Application($this->getConfig()); + } + + /** + * @param string $authCorpId 企业 corp_id + * @param string $permanentCode 企业永久授权码 + */ + public function work(string $authCorpId, string $permanentCode): Work + { + return new Work($authCorpId, $permanentCode, $this); + } + + /** + * @param string $method + * @param array $arguments + * + * @return mixed + */ + public function __call($method, $arguments) + { + return $this['base']->$method(...$arguments); + } +} diff --git a/vendor/overtrue/wechat/src/OpenWork/Auth/AccessToken.php b/vendor/overtrue/wechat/src/OpenWork/Auth/AccessToken.php new file mode 100644 index 0000000..a90ab32 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenWork/Auth/AccessToken.php @@ -0,0 +1,50 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenWork\Auth; + +use EasyWeChat\Kernel\AccessToken as BaseAccessToken; + +/** + * AccessToken. + * + * @author xiaomin + */ +class AccessToken extends BaseAccessToken +{ + protected $requestMethod = 'POST'; + + /** + * @var string + */ + protected $endpointToGetToken = 'cgi-bin/service/get_provider_token'; + + /** + * @var string + */ + protected $tokenKey = 'provider_access_token'; + + /** + * @var string + */ + protected $cachePrefix = 'easywechat.kernel.provider_access_token.'; + + /** + * Credential for get token. + */ + protected function getCredentials(): array + { + return [ + 'corpid' => $this->app['config']['corp_id'], //服务商的corpid + 'provider_secret' => $this->app['config']['secret'], + ]; + } +} diff --git a/vendor/overtrue/wechat/src/OpenWork/Auth/ServiceProvider.php b/vendor/overtrue/wechat/src/OpenWork/Auth/ServiceProvider.php new file mode 100644 index 0000000..78aba05 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenWork/Auth/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenWork\Auth; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * ServiceProvider. + * + * @author xiaomin + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + isset($app['provider_access_token']) || $app['provider_access_token'] = function ($app) { + return new AccessToken($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OpenWork/Corp/Client.php b/vendor/overtrue/wechat/src/OpenWork/Corp/Client.php new file mode 100644 index 0000000..1220779 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenWork/Corp/Client.php @@ -0,0 +1,197 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenWork\Corp; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\ServiceContainer; + +/** + * Client. + * + * @author xiaomin + */ +class Client extends BaseClient +{ + /** + * Client constructor. + * 三方接口有三个access_token,这里用的是suite_access_token. + */ + public function __construct(ServiceContainer $app) + { + parent::__construct($app, $app['suite_access_token']); + } + + /** + * 企业微信安装应用授权 url. + * + * @param string $preAuthCode 预授权码 + * @param string $redirectUri 回调地址 + * + * @return string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getPreAuthorizationUrl(string $preAuthCode = '', string $redirectUri = '', string $state = '') + { + $redirectUri || $redirectUri = $this->app->config['redirect_uri_install']; + $preAuthCode || $preAuthCode = $this->getPreAuthCode()['pre_auth_code']; + $state || $state = rand(); + + $params = [ + 'suite_id' => $this->app['config']['suite_id'], + 'redirect_uri' => $redirectUri, + 'pre_auth_code' => $preAuthCode, + 'state' => $state, + ]; + + return 'https://open.work.weixin.qq.com/3rdapp/install?'.http_build_query($params); + } + + /** + * 获取预授权码. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getPreAuthCode() + { + return $this->httpGet('cgi-bin/service/get_pre_auth_code'); + } + + /** + * 设置授权配置. + * 该接口可对某次授权进行配置. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function setSession(string $preAuthCode, array $sessionInfo) + { + $params = [ + 'pre_auth_code' => $preAuthCode, + 'session_info' => $sessionInfo, + ]; + + return $this->httpPostJson('cgi-bin/service/set_session_info', $params); + } + + /** + * 获取企业永久授权码. + * + * @param string $authCode 临时授权码,会在授权成功时附加在redirect_uri中跳转回第三方服务商网站,或通过回调推送给服务商。长度为64至512个字节 + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getPermanentByCode(string $authCode) + { + $params = [ + 'auth_code' => $authCode, + ]; + + return $this->httpPostJson('cgi-bin/service/get_permanent_code', $params); + } + + /** + * 获取企业授权信息. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getAuthorization(string $authCorpId, string $permanentCode) + { + $params = [ + 'auth_corpid' => $authCorpId, + 'permanent_code' => $permanentCode, + ]; + + return $this->httpPostJson('cgi-bin/service/get_auth_info', $params); + } + + /** + * 获取应用的管理员列表. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getManagers(string $authCorpId, string $agentId) + { + $params = [ + 'auth_corpid' => $authCorpId, + 'agentid' => $agentId, + ]; + + return $this->httpPostJson('cgi-bin/service/get_admin_list', $params); + } + + /** + * 获取登录url. + * + * @return string + */ + public function getOAuthRedirectUrl(string $redirectUri = '', string $scope = 'snsapi_userinfo', string $state = null) + { + $redirectUri || $redirectUri = $this->app->config['redirect_uri_oauth']; + $state || $state = rand(); + $params = [ + 'appid' => $this->app['config']['suite_id'], + 'redirect_uri' => $redirectUri, + 'response_type' => 'code', + 'scope' => $scope, + 'state' => $state, + ]; + + return 'https://open.weixin.qq.com/connect/oauth2/authorize?'.http_build_query($params).'#wechat_redirect'; + } + + /** + * 第三方根据code获取企业成员信息. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getUserByCode(string $code) + { + $params = [ + 'code' => $code, + ]; + + return $this->httpGet('cgi-bin/service/getuserinfo3rd', $params); + } + + /** + * 第三方使用user_ticket获取成员详情. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getUserByTicket(string $userTicket) + { + $params = [ + 'user_ticket' => $userTicket, + ]; + + return $this->httpPostJson('cgi-bin/service/getuserdetail3rd', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OpenWork/Corp/ServiceProvider.php b/vendor/overtrue/wechat/src/OpenWork/Corp/ServiceProvider.php new file mode 100644 index 0000000..f467e18 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenWork/Corp/ServiceProvider.php @@ -0,0 +1,36 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenWork\Corp; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * ServiceProvider. + * + * @author xiaomin + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * Registers services on the given container. + * + * This method should only be used to configure services and parameters. + * It should not get services. + */ + public function register(Container $app) + { + isset($app['corp']) || $app['corp'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OpenWork/MiniProgram/Client.php b/vendor/overtrue/wechat/src/OpenWork/MiniProgram/Client.php new file mode 100644 index 0000000..a054aae --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenWork/MiniProgram/Client.php @@ -0,0 +1,46 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenWork\MiniProgram; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\ServiceContainer; + +/** + * Class Client. + */ +class Client extends BaseClient +{ + /** + * Client constructor. + */ + public function __construct(ServiceContainer $app) + { + parent::__construct($app, $app['suite_access_token']); + } + + /** + * Get session info by code. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function session(string $code) + { + $params = [ + 'js_code' => $code, + 'grant_type' => 'authorization_code', + ]; + + return $this->httpGet('cgi-bin/service/miniprogram/jscode2session', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OpenWork/MiniProgram/ServiceProvider.php b/vendor/overtrue/wechat/src/OpenWork/MiniProgram/ServiceProvider.php new file mode 100644 index 0000000..8e7dba2 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenWork/MiniProgram/ServiceProvider.php @@ -0,0 +1,31 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenWork\MiniProgram; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * ServiceProvider. + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + !isset($app['mini_program']) && $app['mini_program'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OpenWork/Provider/Client.php b/vendor/overtrue/wechat/src/OpenWork/Provider/Client.php new file mode 100644 index 0000000..047ac0e --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenWork/Provider/Client.php @@ -0,0 +1,214 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenWork\Provider; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\ServiceContainer; + +/** + * Client. + * + * @author xiaomin + */ +class Client extends BaseClient +{ + /** + * Client constructor. + */ + public function __construct(ServiceContainer $app) + { + parent::__construct($app, $app['provider_access_token']); + } + + /** + * 单点登录 - 获取登录的地址. + * + * @return string + */ + public function getLoginUrl(string $redirectUri = '', string $userType = 'admin', string $state = '') + { + $redirectUri || $redirectUri = $this->app->config['redirect_uri_single']; + $state || $state = rand(); + $params = [ + 'appid' => $this->app['config']['corp_id'], + 'redirect_uri' => $redirectUri, + 'usertype' => $userType, + 'state' => $state, + ]; + + return 'https://open.work.weixin.qq.com/wwopen/sso/3rd_qrConnect?'.http_build_query($params); + } + + /** + * 单点登录 - 获取登录用户信息. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getLoginInfo(string $authCode) + { + $params = [ + 'auth_code' => $authCode, + ]; + + return $this->httpPostJson('cgi-bin/service/get_login_info', $params); + } + + /** + * 获取注册定制化URL. + * + * @return string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function getRegisterUri(string $registerCode = '') + { + if (!$registerCode) { + /** @var array $response */ + $response = $this->detectAndCastResponseToType($this->getRegisterCode(), 'array'); + + $registerCode = $response['register_code']; + } + + $params = ['register_code' => $registerCode]; + + return 'https://open.work.weixin.qq.com/3rdservice/wework/register?'.http_build_query($params); + } + + /** + * 获取注册码. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getRegisterCode( + string $corpName = '', + string $adminName = '', + string $adminMobile = '', + string $state = '' + ) { + $params = []; + $params['template_id'] = $this->app['config']['reg_template_id']; + !empty($corpName) && $params['corp_name'] = $corpName; + !empty($adminName) && $params['admin_name'] = $adminName; + !empty($adminMobile) && $params['admin_mobile'] = $adminMobile; + !empty($state) && $params['state'] = $state; + + return $this->httpPostJson('cgi-bin/service/get_register_code', $params); + } + + /** + * 查询注册状态. + * + * Desc:该API用于查询企业注册状态,企业注册成功返回注册信息. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getRegisterInfo(string $registerCode) + { + $params = [ + 'register_code' => $registerCode, + ]; + + return $this->httpPostJson('cgi-bin/service/get_register_info', $params); + } + + /** + * 设置授权应用可见范围. + * + * Desc:调用该接口前提是开启通讯录迁移,收到授权成功通知后可调用。 + * 企业注册初始化安装应用后,应用默认可见范围为根部门。 + * 如需修改应用可见范围,服务商可以调用该接口设置授权应用的可见范围。 + * 该接口只能使用注册完成回调事件或者查询注册状态返回的access_token。 + * 调用设置通讯录同步完成后或者access_token超过30分钟失效(即解除通讯录锁定状态)则不能继续调用该接口。 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function setAgentScope( + string $accessToken, + string $agentId, + array $allowUser = [], + array $allowParty = [], + array $allowTag = [] + ) { + $params = [ + 'agentid' => $agentId, + 'allow_user' => $allowUser, + 'allow_party' => $allowParty, + 'allow_tag' => $allowTag, + 'access_token' => $accessToken, + ]; + + return $this->httpGet('cgi-bin/agent/set_scope', $params); + } + + /** + * 设置通讯录同步完成. + * + * Desc:该API用于设置通讯录同步完成,解除通讯录锁定状态,同时使通讯录迁移access_token失效。 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function contactSyncSuccess(string $accessToken) + { + $params = ['access_token' => $accessToken]; + + return $this->httpGet('cgi-bin/sync/contact_sync_success', $params); + } + + /** + * 通讯录单个搜索 + * + * @param string $queryWord + * @param $agentId + * @param int $offset + * @param int $limit + * @param int $queryType + * @param null $fullMatchField + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function searchContact( + string $queryWord, + $agentId, + int $offset = 0, + int $limit = 50, + int $queryType = 0, + $fullMatchField = null + ) { + $params = []; + $params['auth_corpid'] = $this->app['config']['corp_id']; + $params['query_word'] = $queryWord; + $params['query_type'] = $queryType; + $params['agentid'] = $agentId; + $params['offset'] = $offset; + $params['limit'] = $limit; + !empty($fullMatchField) && $params['full_match_field'] = $fullMatchField; + + return $this->httpPostJson('cgi-bin/service/contact/search', $params); + } +} diff --git a/vendor/overtrue/wechat/src/OpenWork/Provider/ServiceProvider.php b/vendor/overtrue/wechat/src/OpenWork/Provider/ServiceProvider.php new file mode 100644 index 0000000..09c4faa --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenWork/Provider/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenWork\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * ServiceProvider. + * + * @author xiaomin + */ +class ServiceProvider implements ServiceProviderInterface +{ + protected $app; + + public function register(Container $app) + { + $this->app = $app; + isset($app['provider']) || $app['provider'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OpenWork/Server/Guard.php b/vendor/overtrue/wechat/src/OpenWork/Server/Guard.php new file mode 100644 index 0000000..1e16794 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenWork/Server/Guard.php @@ -0,0 +1,63 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenWork\Server; + +use EasyWeChat\Kernel\Encryptor; +use EasyWeChat\Kernel\ServerGuard; + +/** + * Guard. + * + * @author xiaomin + */ +class Guard extends ServerGuard +{ + /** + * @var bool + */ + protected $alwaysValidate = true; + + /** + * @return $this + */ + public function validate() + { + return $this; + } + + protected function shouldReturnRawResponse(): bool + { + return !is_null($this->app['request']->get('echostr')); + } + + protected function isSafeMode(): bool + { + return true; + } + + /** + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + */ + protected function decryptMessage(array $message) + { + $encryptor = new Encryptor($message['ToUserName'], $this->app['config']->get('token'), $this->app['config']->get('aes_key')); + + return $message = $encryptor->decrypt( + $message['Encrypt'], + $this->app['request']->get('msg_signature'), + $this->app['request']->get('nonce'), + $this->app['request']->get('timestamp') + ); + } +} diff --git a/vendor/overtrue/wechat/src/OpenWork/Server/Handlers/EchoStrHandler.php b/vendor/overtrue/wechat/src/OpenWork/Server/Handlers/EchoStrHandler.php new file mode 100644 index 0000000..b3aaf37 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenWork/Server/Handlers/EchoStrHandler.php @@ -0,0 +1,60 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenWork\Server\Handlers; + +use EasyWeChat\Kernel\Contracts\EventHandlerInterface; +use EasyWeChat\Kernel\Decorators\FinallyResult; +use EasyWeChat\Kernel\ServiceContainer; + +/** + * EchoStrHandler. + * + * @author xiaomin + */ +class EchoStrHandler implements EventHandlerInterface +{ + /** + * @var ServiceContainer + */ + protected $app; + + /** + * EchoStrHandler constructor. + */ + public function __construct(ServiceContainer $app) + { + $this->app = $app; + } + + /** + * @param mixed $payload + * + * @return FinallyResult|null + */ + public function handle($payload = null) + { + if ($decrypted = $this->app['request']->get('echostr')) { + $str = $this->app['encryptor_corp']->decrypt( + $decrypted, + $this->app['request']->get('msg_signature'), + $this->app['request']->get('nonce'), + $this->app['request']->get('timestamp') + ); + + return new FinallyResult($str); + } + //把SuiteTicket缓存起来 + if (!empty($payload['SuiteTicket']) && !empty($payload['SuiteId']) && $this->app['config']['suite_id'] == $payload['SuiteId']) { + $this->app['suite_ticket']->setTicket($payload['SuiteTicket']); + } + } +} diff --git a/vendor/overtrue/wechat/src/OpenWork/Server/ServiceProvider.php b/vendor/overtrue/wechat/src/OpenWork/Server/ServiceProvider.php new file mode 100644 index 0000000..d33d5c8 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenWork/Server/ServiceProvider.php @@ -0,0 +1,56 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenWork\Server; + +use EasyWeChat\Kernel\Encryptor; +use EasyWeChat\OpenWork\Server\Handlers\EchoStrHandler; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * ServiceProvider. + * + * @author xiaomin + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + //微信第三方在校验url是使用的是GET方式请求和corp_id进行加密 + !isset($app['encryptor_corp']) && $app['encryptor_corp'] = function ($app) { + return new Encryptor( + $app['config']['corp_id'], + $app['config']['token'], + $app['config']['aes_key'] + ); + }; + + //微信第三方推送数据时使用的是suite_id进行加密 + !isset($app['encryptor']) && $app['encryptor'] = function ($app) { + return new Encryptor( + $app['config']['suite_id'], + $app['config']['token'], + $app['config']['aes_key'] + ); + }; + + !isset($app['server']) && $app['server'] = function ($app) { + $guard = new Guard($app); + $guard->push(new EchoStrHandler($app)); + + return $guard; + }; + } +} diff --git a/vendor/overtrue/wechat/src/OpenWork/SuiteAuth/AccessToken.php b/vendor/overtrue/wechat/src/OpenWork/SuiteAuth/AccessToken.php new file mode 100644 index 0000000..3c5a611 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenWork/SuiteAuth/AccessToken.php @@ -0,0 +1,54 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenWork\SuiteAuth; + +use EasyWeChat\Kernel\AccessToken as BaseAccessToken; + +/** + * AccessToken. + * + * @author xiaomin + */ +class AccessToken extends BaseAccessToken +{ + /** + * @var string + */ + protected $requestMethod = 'POST'; + + /** + * @var string + */ + protected $endpointToGetToken = 'cgi-bin/service/get_suite_token'; + + /** + * @var string + */ + protected $tokenKey = 'suite_access_token'; + + /** + * @var string + */ + protected $cachePrefix = 'easywechat.kernel.suite_access_token.'; + + /** + * Credential for get token. + */ + protected function getCredentials(): array + { + return [ + 'suite_id' => $this->app['config']['suite_id'], + 'suite_secret' => $this->app['config']['suite_secret'], + 'suite_ticket' => $this->app['suite_ticket']->getTicket(), + ]; + } +} diff --git a/vendor/overtrue/wechat/src/OpenWork/SuiteAuth/ServiceProvider.php b/vendor/overtrue/wechat/src/OpenWork/SuiteAuth/ServiceProvider.php new file mode 100644 index 0000000..a4f5386 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenWork/SuiteAuth/ServiceProvider.php @@ -0,0 +1,37 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenWork\SuiteAuth; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * ServiceProvider. + * + * @author xiaomin + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['suite_ticket'] = function ($app) { + return new SuiteTicket($app); + }; + + isset($app['suite_access_token']) || $app['suite_access_token'] = function ($app) { + return new AccessToken($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/OpenWork/SuiteAuth/SuiteTicket.php b/vendor/overtrue/wechat/src/OpenWork/SuiteAuth/SuiteTicket.php new file mode 100644 index 0000000..c9acb7d --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenWork/SuiteAuth/SuiteTicket.php @@ -0,0 +1,76 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenWork\SuiteAuth; + +use EasyWeChat\Kernel\Exceptions\RuntimeException; +use EasyWeChat\Kernel\Traits\InteractsWithCache; +use EasyWeChat\OpenWork\Application; + +/** + * SuiteTicket. + * + * @author xiaomin + */ +class SuiteTicket +{ + use InteractsWithCache; + + /** + * @var Application + */ + protected $app; + + /** + * SuiteTicket constructor. + */ + public function __construct(Application $app) + { + $this->app = $app; + } + + /** + * @return $this + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function setTicket(string $ticket) + { + $this->getCache()->set($this->getCacheKey(), $ticket, 1800); + + if (!$this->getCache()->has($this->getCacheKey())) { + throw new RuntimeException('Failed to cache suite ticket.'); + } + + return $this; + } + + /** + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function getTicket(): string + { + if ($cached = $this->getCache()->get($this->getCacheKey())) { + return $cached; + } + + throw new RuntimeException('Credential "suite_ticket" does not exist in cache.'); + } + + protected function getCacheKey(): string + { + return 'easywechat.open_work.suite_ticket.'.$this->app['config']['suite_id']; + } +} diff --git a/vendor/overtrue/wechat/src/OpenWork/Work/Application.php b/vendor/overtrue/wechat/src/OpenWork/Work/Application.php new file mode 100644 index 0000000..fbf9cc2 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenWork/Work/Application.php @@ -0,0 +1,36 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenWork\Work; + +use EasyWeChat\OpenWork\Application as OpenWork; +use EasyWeChat\OpenWork\Work\Auth\AccessToken; +use EasyWeChat\Work\Application as Work; + +/** + * Application. + * + * @author xiaomin + */ +class Application extends Work +{ + /** + * Application constructor. + */ + public function __construct(string $authCorpId, string $permanentCode, OpenWork $component, array $prepends = []) + { + parent::__construct($component->getConfig(), $prepends + [ + 'access_token' => function ($app) use ($authCorpId, $permanentCode, $component) { + return new AccessToken($app, $authCorpId, $permanentCode, $component); + }, + ]); + } +} diff --git a/vendor/overtrue/wechat/src/OpenWork/Work/Auth/AccessToken.php b/vendor/overtrue/wechat/src/OpenWork/Work/Auth/AccessToken.php new file mode 100644 index 0000000..6a895a7 --- /dev/null +++ b/vendor/overtrue/wechat/src/OpenWork/Work/Auth/AccessToken.php @@ -0,0 +1,70 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\OpenWork\Work\Auth; + +use EasyWeChat\Kernel\AccessToken as BaseAccessToken; +use EasyWeChat\OpenWork\Application; +use Pimple\Container; + +/** + * AccessToken. + * + * @author xiaomin + */ +class AccessToken extends BaseAccessToken +{ + /** + * @var string + */ + protected $requestMethod = 'POST'; + + /** + * @var string 授权方企业ID + */ + protected $authCorpid; + + /** + * @var string 授权方企业永久授权码,通过get_permanent_code获取 + */ + protected $permanentCode; + + protected $component; + + /** + * AccessToken constructor. + */ + public function __construct(Container $app, string $authCorpId, string $permanentCode, Application $component) + { + $this->authCorpid = $authCorpId; + $this->permanentCode = $permanentCode; + $this->component = $component; + parent::__construct($app); + } + + /** + * Credential for get token. + */ + protected function getCredentials(): array + { + return [ + 'auth_corpid' => $this->authCorpid, + 'permanent_code' => $this->permanentCode, + ]; + } + + public function getEndpoint(): string + { + return 'cgi-bin/service/get_corp_token?'.http_build_query([ + 'suite_access_token' => $this->component['suite_access_token']->getToken()['suite_access_token'], + ]); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Application.php b/vendor/overtrue/wechat/src/Payment/Application.php new file mode 100644 index 0000000..5f5b4c6 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Application.php @@ -0,0 +1,190 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment; + +use Closure; +use EasyWeChat\BasicService; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\ServiceContainer; +use EasyWeChat\Kernel\Support; +use EasyWeChat\OfficialAccount; + +/** + * Class Application. + * + * @property \EasyWeChat\Payment\Bill\Client $bill + * @property \EasyWeChat\Payment\Fundflow\Client $fundflow + * @property \EasyWeChat\Payment\Jssdk\Client $jssdk + * @property \EasyWeChat\Payment\Order\Client $order + * @property \EasyWeChat\Payment\Refund\Client $refund + * @property \EasyWeChat\Payment\Coupon\Client $coupon + * @property \EasyWeChat\Payment\Reverse\Client $reverse + * @property \EasyWeChat\Payment\Redpack\Client $redpack + * @property \EasyWeChat\BasicService\Url\Client $url + * @property \EasyWeChat\Payment\Transfer\Client $transfer + * @property \EasyWeChat\Payment\Security\Client $security + * @property \EasyWeChat\Payment\ProfitSharing\Client $profit_sharing + * @property \EasyWeChat\Payment\Contract\Client $contract + * @property \EasyWeChat\OfficialAccount\Auth\AccessToken $access_token + * + * @method mixed pay(array $attributes) + * @method mixed authCodeToOpenid(string $authCode) + */ +class Application extends ServiceContainer +{ + /** + * @var array + */ + protected $providers = [ + OfficialAccount\Auth\ServiceProvider::class, + BasicService\Url\ServiceProvider::class, + Base\ServiceProvider::class, + Bill\ServiceProvider::class, + Fundflow\ServiceProvider::class, + Coupon\ServiceProvider::class, + Jssdk\ServiceProvider::class, + Merchant\ServiceProvider::class, + Order\ServiceProvider::class, + Redpack\ServiceProvider::class, + Refund\ServiceProvider::class, + Reverse\ServiceProvider::class, + Sandbox\ServiceProvider::class, + Transfer\ServiceProvider::class, + Security\ServiceProvider::class, + ProfitSharing\ServiceProvider::class, + Contract\ServiceProvider::class, + ]; + + /** + * @var array + */ + protected $defaultConfig = [ + 'http' => [ + 'base_uri' => 'https://api.mch.weixin.qq.com/', + ], + ]; + + /** + * Build payment scheme for product. + */ + public function scheme(string $productId): string + { + $params = [ + 'appid' => $this['config']->app_id, + 'mch_id' => $this['config']->mch_id, + 'time_stamp' => time(), + 'nonce_str' => uniqid(), + 'product_id' => $productId, + ]; + + $params['sign'] = Support\generate_sign($params, $this['config']->key); + + return 'weixin://wxpay/bizpayurl?'.http_build_query($params); + } + + /** + * @return string + */ + public function codeUrlScheme(string $codeUrl) + { + return \sprintf('weixin://wxpay/bizpayurl?sr=%s', $codeUrl); + } + + /** + * @return \Symfony\Component\HttpFoundation\Response + * + * @codeCoverageIgnore + * + * @throws \EasyWeChat\Kernel\Exceptions\Exception + */ + public function handlePaidNotify(Closure $closure) + { + return (new Notify\Paid($this))->handle($closure); + } + + /** + * @return \Symfony\Component\HttpFoundation\Response + * + * @codeCoverageIgnore + * + * @throws \EasyWeChat\Kernel\Exceptions\Exception + */ + public function handleRefundedNotify(Closure $closure) + { + return (new Notify\Refunded($this))->handle($closure); + } + + /** + * @return \Symfony\Component\HttpFoundation\Response + * + * @codeCoverageIgnore + * + * @throws \EasyWeChat\Kernel\Exceptions\Exception + */ + public function handleScannedNotify(Closure $closure) + { + return (new Notify\Scanned($this))->handle($closure); + } + + /** + * Set sub-merchant. + * + * @return $this + */ + public function setSubMerchant(string $mchId, string $appId = null) + { + $this['config']->set('sub_mch_id', $mchId); + $this['config']->set('sub_appid', $appId); + + return $this; + } + + public function inSandbox(): bool + { + return (bool) $this['config']->get('sandbox'); + } + + /** + * @return string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function getKey(string $endpoint = null) + { + if ('sandboxnew/pay/getsignkey' === $endpoint) { + return $this['config']->key; + } + + $key = $this->inSandbox() ? $this['sandbox']->getKey() : $this['config']->key; + + if (empty($key)) { + throw new InvalidArgumentException('config key should not empty.'); + } + + if (32 !== strlen($key)) { + throw new InvalidArgumentException(sprintf("'%s' should be 32 chars length.", $key)); + } + + return $key; + } + + /** + * @param string $name + * @param array $arguments + * + * @return mixed + */ + public function __call($name, $arguments) + { + return call_user_func_array([$this['base'], $name], $arguments); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Base/Client.php b/vendor/overtrue/wechat/src/Payment/Base/Client.php new file mode 100644 index 0000000..1fbb607 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Base/Client.php @@ -0,0 +1,50 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Base; + +use EasyWeChat\Payment\Kernel\BaseClient; + +class Client extends BaseClient +{ + /** + * Pay the order. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function pay(array $params) + { + $params['appid'] = $this->app['config']->app_id; + + return $this->request($this->wrap('pay/micropay'), $params); + } + + /** + * Get openid by auth code. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function authCodeToOpenid(string $authCode) + { + return $this->request('tools/authcodetoopenid', [ + 'appid' => $this->app['config']->app_id, + 'auth_code' => $authCode, + ]); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Base/ServiceProvider.php b/vendor/overtrue/wechat/src/Payment/Base/ServiceProvider.php new file mode 100644 index 0000000..71aebd9 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Base/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Base; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['base'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Bill/Client.php b/vendor/overtrue/wechat/src/Payment/Bill/Client.php new file mode 100644 index 0000000..e8df7f9 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Bill/Client.php @@ -0,0 +1,44 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Bill; + +use EasyWeChat\Kernel\Http\StreamResponse; +use EasyWeChat\Payment\Kernel\BaseClient; + +class Client extends BaseClient +{ + /** + * Download bill history as a table file. + * + * @return \EasyWeChat\Kernel\Http\StreamResponse|\Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(string $date, string $type = 'ALL', array $optional = []) + { + $params = [ + 'appid' => $this->app['config']->app_id, + 'bill_date' => $date, + 'bill_type' => $type, + ] + $optional; + + $response = $this->requestRaw($this->wrap('pay/downloadbill'), $params); + + if (0 === strpos($response->getBody()->getContents(), '')) { + return $this->castResponseToType($response, $this->app['config']->get('response_type')); + } + + return StreamResponse::buildFromPsrResponse($response); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Bill/ServiceProvider.php b/vendor/overtrue/wechat/src/Payment/Bill/ServiceProvider.php new file mode 100644 index 0000000..3fd98d4 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Bill/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Bill; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['bill'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Contract/Client.php b/vendor/overtrue/wechat/src/Payment/Contract/Client.php new file mode 100644 index 0000000..141d22c --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Contract/Client.php @@ -0,0 +1,102 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Contract; + +use EasyWeChat\Payment\Kernel\BaseClient; + +/** + * Class Client. + * + * @author tianyong90 <412039588@qq.com> + */ +class Client extends BaseClient +{ + /** + * entrust official account. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function web(array $params) + { + $params['appid'] = $this->app['config']->app_id; + + return $this->safeRequest('papay/entrustweb', $params); + } + + /** + * entrust app. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function app(array $params) + { + $params['appid'] = $this->app['config']->app_id; + + return $this->safeRequest('papay/preentrustweb', $params); + } + + /** + * entrust html 5. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function h5(array $params) + { + $params['appid'] = $this->app['config']->app_id; + + return $this->safeRequest('papay/h5entrustweb', $params); + } + + /** + * apply papay. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function apply(array $params) + { + $params['appid'] = $this->app['config']->app_id; + + return $this->safeRequest('pay/pappayapply', $params); + } + + /** + * delete papay contrace. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(array $params) + { + $params['appid'] = $this->app['config']->app_id; + + return $this->safeRequest('papay/deletecontract', $params); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Contract/ServiceProvider.php b/vendor/overtrue/wechat/src/Payment/Contract/ServiceProvider.php new file mode 100644 index 0000000..945ea68 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Contract/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Contract; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['contract'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Coupon/Client.php b/vendor/overtrue/wechat/src/Payment/Coupon/Client.php new file mode 100644 index 0000000..b81831b --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Coupon/Client.php @@ -0,0 +1,71 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Coupon; + +use EasyWeChat\Payment\Kernel\BaseClient; + +/** + * Class Client. + * + * @author tianyong90 <412039588@qq.com> + */ +class Client extends BaseClient +{ + /** + * send a cash coupon. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function send(array $params) + { + $params['appid'] = $this->app['config']->app_id; + $params['openid_count'] = 1; + + return $this->safeRequest('mmpaymkttransfers/send_coupon', $params); + } + + /** + * query a coupon stock. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function stock(array $params) + { + $params['appid'] = $this->app['config']->app_id; + + return $this->request('mmpaymkttransfers/query_coupon_stock', $params); + } + + /** + * query a info of coupon. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function info(array $params) + { + $params['appid'] = $this->app['config']->app_id; + + return $this->request('mmpaymkttransfers/querycouponsinfo', $params); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Coupon/ServiceProvider.php b/vendor/overtrue/wechat/src/Payment/Coupon/ServiceProvider.php new file mode 100644 index 0000000..513734b --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Coupon/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Coupon; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['coupon'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Fundflow/Client.php b/vendor/overtrue/wechat/src/Payment/Fundflow/Client.php new file mode 100644 index 0000000..b69c7eb --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Fundflow/Client.php @@ -0,0 +1,54 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Fundflow; + +use EasyWeChat\Kernel\Http\StreamResponse; +use EasyWeChat\Payment\Kernel\BaseClient; + +class Client extends BaseClient +{ + /** + * Download fundflow history as a table file. + * + * @param array $options + * + * @return array|\EasyWeChat\Kernel\Http\Response|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(string $date, string $type = 'Basic', $options = []) + { + $params = [ + 'appid' => $this->app['config']->app_id, + 'bill_date' => $date, + 'account_type' => $type, + 'sign_type' => 'HMAC-SHA256', + 'nonce_str' => uniqid('micro'), + ]; + $options = array_merge( + [ + 'cert' => $this->app['config']->get('cert_path'), + 'ssl_key' => $this->app['config']->get('key_path'), + ], + $options + ); + $response = $this->requestRaw('pay/downloadfundflow', $params, 'post', $options); + + if (0 === strpos($response->getBody()->getContents(), '')) { + return $this->castResponseToType($response, $this->app['config']->get('response_type')); + } + + return StreamResponse::buildFromPsrResponse($response); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Fundflow/ServiceProvider.php b/vendor/overtrue/wechat/src/Payment/Fundflow/ServiceProvider.php new file mode 100644 index 0000000..9901124 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Fundflow/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Fundflow; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['fundflow'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Jssdk/Client.php b/vendor/overtrue/wechat/src/Payment/Jssdk/Client.php new file mode 100644 index 0000000..52cf7f7 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Jssdk/Client.php @@ -0,0 +1,136 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Jssdk; + +use EasyWeChat\BasicService\Jssdk\Client as JssdkClient; +use EasyWeChat\Kernel\Support; +use Overtrue\Socialite\AccessTokenInterface; + +/** + * Class Client. + * + * @author overtrue + */ +class Client extends JssdkClient +{ + /** + * [WeixinJSBridge] Generate js config for payment. + * + *
              +     * WeixinJSBridge.invoke(
              +     *  'getBrandWCPayRequest',
              +     *  ...
              +     * );
              +     * 
              + * + * @return string|array + */ + public function bridgeConfig(string $prepayId, bool $json = true) + { + $params = [ + 'appId' => $this->app['config']->sub_appid ?: $this->app['config']->app_id, + 'timeStamp' => strval(time()), + 'nonceStr' => uniqid(), + 'package' => "prepay_id=$prepayId", + 'signType' => 'MD5', + ]; + + $params['paySign'] = Support\generate_sign($params, $this->app['config']->key, 'md5'); + + return $json ? json_encode($params) : $params; + } + + /** + * [JSSDK] Generate js config for payment. + * + *
              +     * wx.chooseWXPay({...});
              +     * 
              + */ + public function sdkConfig(string $prepayId): array + { + $config = $this->bridgeConfig($prepayId, false); + + $config['timestamp'] = $config['timeStamp']; + unset($config['timeStamp']); + + return $config; + } + + /** + * Generate app payment parameters. + */ + public function appConfig(string $prepayId): array + { + $params = [ + 'appid' => $this->app['config']->app_id, + 'partnerid' => $this->app['config']->mch_id, + 'prepayid' => $prepayId, + 'noncestr' => uniqid(), + 'timestamp' => time(), + 'package' => 'Sign=WXPay', + ]; + + $params['sign'] = Support\generate_sign($params, $this->app['config']->key); + + return $params; + } + + /** + * Generate js config for share user address. + * + * @param string|\Overtrue\Socialite\AccessTokenInterface $accessToken + * + * @return string|array + */ + public function shareAddressConfig($accessToken, bool $json = true) + { + if ($accessToken instanceof AccessTokenInterface) { + $accessToken = $accessToken->getToken(); + } + + $params = [ + 'appId' => $this->app['config']->app_id, + 'scope' => 'jsapi_address', + 'timeStamp' => strval(time()), + 'nonceStr' => uniqid(), + 'signType' => 'SHA1', + ]; + + $signParams = [ + 'appid' => $params['appId'], + 'url' => $this->getUrl(), + 'timestamp' => $params['timeStamp'], + 'noncestr' => $params['nonceStr'], + 'accesstoken' => strval($accessToken), + ]; + + ksort($signParams); + + $params['addrSign'] = sha1(urldecode(http_build_query($signParams))); + + return $json ? json_encode($params) : $params; + } + + /** + * Generate js config for contract of mini program. + */ + public function contractConfig(array $params): array + { + $params['appid'] = $this->app['config']->app_id; + $params['timestamp'] = time(); + + $params['sign'] = Support\generate_sign($params, $this->app['config']->key); + + return $params; + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Jssdk/ServiceProvider.php b/vendor/overtrue/wechat/src/Payment/Jssdk/ServiceProvider.php new file mode 100644 index 0000000..24f2a76 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Jssdk/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Jssdk; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['jssdk'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Kernel/BaseClient.php b/vendor/overtrue/wechat/src/Payment/Kernel/BaseClient.php new file mode 100644 index 0000000..c765f40 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Kernel/BaseClient.php @@ -0,0 +1,171 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Kernel; + +use EasyWeChat\Kernel\Support; +use EasyWeChat\Kernel\Traits\HasHttpRequests; +use EasyWeChat\Payment\Application; +use GuzzleHttp\MessageFormatter; +use GuzzleHttp\Middleware; +use Psr\Http\Message\ResponseInterface; + +/** + * Class BaseClient. + * + * @author overtrue + */ +class BaseClient +{ + use HasHttpRequests { request as performRequest; } + + /** + * @var \EasyWeChat\Payment\Application + */ + protected $app; + + /** + * Constructor. + */ + public function __construct(Application $app) + { + $this->app = $app; + + $this->setHttpClient($this->app['http_client']); + } + + /** + * Extra request params. + * + * @return array + */ + protected function prepends() + { + return []; + } + + /** + * Make a API request. + * + * @param string $method + * @param bool $returnResponse + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function request(string $endpoint, array $params = [], $method = 'post', array $options = [], $returnResponse = false) + { + $base = [ + 'mch_id' => $this->app['config']['mch_id'], + 'nonce_str' => uniqid(), + 'sub_mch_id' => $this->app['config']['sub_mch_id'], + 'sub_appid' => $this->app['config']['sub_appid'], + ]; + + $params = array_filter(array_merge($base, $this->prepends(), $params), 'strlen'); + + $secretKey = $this->app->getKey($endpoint); + + $encryptMethod = Support\get_encrypt_method(Support\Arr::get($params, 'sign_type', 'MD5'), $secretKey); + + $params['sign'] = Support\generate_sign($params, $secretKey, $encryptMethod); + + $options = array_merge([ + 'body' => Support\XML::build($params), + ], $options); + + $this->pushMiddleware($this->logMiddleware(), 'log'); + + $response = $this->performRequest($endpoint, $method, $options); + + return $returnResponse ? $response : $this->castResponseToType($response, $this->app->config->get('response_type')); + } + + /** + * Log the request. + * + * @return \Closure + */ + protected function logMiddleware() + { + $formatter = new MessageFormatter($this->app['config']['http.log_template'] ?? MessageFormatter::DEBUG); + + return Middleware::log($this->app['logger'], $formatter); + } + + /** + * Make a request and return raw response. + * + * @param string $method + * + * @return ResponseInterface + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function requestRaw(string $endpoint, array $params = [], $method = 'post', array $options = []) + { + /** @var ResponseInterface $response */ + $response = $this->request($endpoint, $params, $method, $options, true); + + return $response; + } + + /** + * Make a request and return an array. + * + * @param string $method + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function requestArray(string $endpoint, array $params = [], $method = 'post', array $options = []): array + { + $response = $this->requestRaw($endpoint, $params, $method, $options); + + return $this->castResponseToType($response, 'array'); + } + + /** + * Request with SSL. + * + * @param string $endpoint + * @param string $method + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function safeRequest($endpoint, array $params, $method = 'post', array $options = []) + { + $options = array_merge([ + 'cert' => $this->app['config']->get('cert_path'), + 'ssl_key' => $this->app['config']->get('key_path'), + ], $options); + + return $this->request($endpoint, $params, $method, $options); + } + + /** + * Wrapping an API endpoint. + */ + protected function wrap(string $endpoint): string + { + return $this->app->inSandbox() ? "sandboxnew/{$endpoint}" : $endpoint; + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Kernel/Exceptions/InvalidSignException.php b/vendor/overtrue/wechat/src/Payment/Kernel/Exceptions/InvalidSignException.php new file mode 100644 index 0000000..cdd25ba --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Kernel/Exceptions/InvalidSignException.php @@ -0,0 +1,18 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Kernel\Exceptions; + +use EasyWeChat\Kernel\Exceptions\Exception; + +class InvalidSignException extends Exception +{ +} diff --git a/vendor/overtrue/wechat/src/Payment/Kernel/Exceptions/SandboxException.php b/vendor/overtrue/wechat/src/Payment/Kernel/Exceptions/SandboxException.php new file mode 100644 index 0000000..01f9dd5 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Kernel/Exceptions/SandboxException.php @@ -0,0 +1,18 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Kernel\Exceptions; + +use EasyWeChat\Kernel\Exceptions\Exception; + +class SandboxException extends Exception +{ +} diff --git a/vendor/overtrue/wechat/src/Payment/Merchant/Client.php b/vendor/overtrue/wechat/src/Payment/Merchant/Client.php new file mode 100644 index 0000000..493bce1 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Merchant/Client.php @@ -0,0 +1,85 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Merchant; + +use EasyWeChat\Payment\Kernel\BaseClient; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * Add sub-merchant. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function addSubMerchant(array $params) + { + return $this->manage($params, ['action' => 'add']); + } + + /** + * Query sub-merchant by merchant id. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function querySubMerchantByMerchantId(string $id) + { + $params = [ + 'micro_mch_id' => $id, + ]; + + return $this->manage($params, ['action' => 'query']); + } + + /** + * Query sub-merchant by wechat id. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function querySubMerchantByWeChatId(string $id) + { + $params = [ + 'recipient_wechatid' => $id, + ]; + + return $this->manage($params, ['action' => 'query']); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function manage(array $params, array $query) + { + $params = array_merge($params, [ + 'appid' => $this->app['config']->app_id, + 'nonce_str' => '', + 'sub_mch_id' => '', + 'sub_appid' => '', + ]); + + return $this->safeRequest('secapi/mch/submchmanage', $params, 'post', compact('query')); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Merchant/ServiceProvider.php b/vendor/overtrue/wechat/src/Payment/Merchant/ServiceProvider.php new file mode 100644 index 0000000..5d05c95 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Merchant/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Merchant; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['merchant'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Notify/Handler.php b/vendor/overtrue/wechat/src/Payment/Notify/Handler.php new file mode 100644 index 0000000..886e55f --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Notify/Handler.php @@ -0,0 +1,189 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Notify; + +use Closure; +use EasyWeChat\Kernel\Exceptions\Exception; +use EasyWeChat\Kernel\Support; +use EasyWeChat\Kernel\Support\XML; +use EasyWeChat\Payment\Kernel\Exceptions\InvalidSignException; +use Symfony\Component\HttpFoundation\Response; + +abstract class Handler +{ + public const SUCCESS = 'SUCCESS'; + public const FAIL = 'FAIL'; + + /** + * @var \EasyWeChat\Payment\Application + */ + protected $app; + + /** + * @var array + */ + protected $message; + + /** + * @var string|null + */ + protected $fail; + + /** + * @var array + */ + protected $attributes = []; + + /** + * Check sign. + * If failed, throws an exception. + * + * @var bool + */ + protected $check = true; + + /** + * Respond with sign. + * + * @var bool + */ + protected $sign = false; + + /** + * @param \EasyWeChat\Payment\Application $app + */ + public function __construct($app) + { + $this->app = $app; + } + + /** + * Handle incoming notify. + * + * @return \Symfony\Component\HttpFoundation\Response + */ + abstract public function handle(Closure $closure); + + public function fail(string $message) + { + $this->fail = $message; + } + + /** + * @return $this + */ + public function respondWith(array $attributes, bool $sign = false) + { + $this->attributes = $attributes; + $this->sign = $sign; + + return $this; + } + + /** + * Build xml and return the response to WeChat. + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function toResponse(): Response + { + $base = [ + 'return_code' => is_null($this->fail) ? static::SUCCESS : static::FAIL, + 'return_msg' => $this->fail, + ]; + + $attributes = array_merge($base, $this->attributes); + + if ($this->sign) { + $attributes['sign'] = Support\generate_sign($attributes, $this->app->getKey()); + } + + return new Response(XML::build($attributes)); + } + + /** + * Return the notify message from request. + * + * @throws \EasyWeChat\Kernel\Exceptions\Exception + */ + public function getMessage(): array + { + if (!empty($this->message)) { + return $this->message; + } + + try { + $message = XML::parse(strval($this->app['request']->getContent())); + } catch (\Throwable $e) { + throw new Exception('Invalid request XML: '.$e->getMessage(), 400); + } + + if (!is_array($message) || empty($message)) { + throw new Exception('Invalid request XML.', 400); + } + + if ($this->check) { + $this->validate($message); + } + + return $this->message = $message; + } + + /** + * Decrypt message. + * + * @return string|null + * + * @throws \EasyWeChat\Kernel\Exceptions\Exception + */ + public function decryptMessage(string $key) + { + $message = $this->getMessage(); + if (empty($message[$key])) { + return null; + } + + return Support\AES::decrypt( + base64_decode($message[$key], true), + md5($this->app['config']->key), + '', + OPENSSL_RAW_DATA, + 'AES-256-ECB' + ); + } + + /** + * Validate the request params. + * + * @throws \EasyWeChat\Payment\Kernel\Exceptions\InvalidSignException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + protected function validate(array $message) + { + $sign = $message['sign']; + unset($message['sign']); + + if (Support\generate_sign($message, $this->app->getKey()) !== $sign) { + throw new InvalidSignException(); + } + } + + /** + * @param mixed $result + */ + protected function strict($result) + { + if (true !== $result && is_null($this->fail)) { + $this->fail(strval($result)); + } + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Notify/Paid.php b/vendor/overtrue/wechat/src/Payment/Notify/Paid.php new file mode 100644 index 0000000..8e107bd --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Notify/Paid.php @@ -0,0 +1,31 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Notify; + +use Closure; + +class Paid extends Handler +{ + /** + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \EasyWeChat\Kernel\Exceptions\Exception + */ + public function handle(Closure $closure) + { + $this->strict( + \call_user_func($closure, $this->getMessage(), [$this, 'fail']) + ); + + return $this->toResponse(); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Notify/Refunded.php b/vendor/overtrue/wechat/src/Payment/Notify/Refunded.php new file mode 100644 index 0000000..2ff7edd --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Notify/Refunded.php @@ -0,0 +1,46 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Notify; + +use Closure; +use EasyWeChat\Kernel\Support\XML; + +class Refunded extends Handler +{ + protected $check = false; + + /** + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \EasyWeChat\Kernel\Exceptions\Exception + */ + public function handle(Closure $closure) + { + $this->strict( + \call_user_func($closure, $this->getMessage(), $this->reqInfo(), [$this, 'fail']) + ); + + return $this->toResponse(); + } + + /** + * Decrypt the `req_info` from request message. + * + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\Exception + */ + public function reqInfo() + { + return XML::parse($this->decryptMessage('req_info')); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Notify/Scanned.php b/vendor/overtrue/wechat/src/Payment/Notify/Scanned.php new file mode 100644 index 0000000..dfa9543 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Notify/Scanned.php @@ -0,0 +1,55 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Notify; + +use Closure; + +class Scanned extends Handler +{ + protected $check = false; + + /** + * @var string|null + */ + protected $alert; + + public function alert(string $message) + { + $this->alert = $message; + } + + /** + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \EasyWeChat\Kernel\Exceptions\Exception + */ + public function handle(Closure $closure) + { + $result = \call_user_func($closure, $this->getMessage(), [$this, 'fail'], [$this, 'alert']); + + $attributes = [ + 'result_code' => is_null($this->alert) && is_null($this->fail) ? static::SUCCESS : static::FAIL, + 'err_code_des' => $this->alert, + ]; + + if (is_null($this->alert) && is_string($result)) { + $attributes += [ + 'appid' => $this->app['config']->app_id, + 'mch_id' => $this->app['config']->mch_id, + 'nonce_str' => uniqid(), + 'prepay_id' => $result, + ]; + } + + return $this->respondWith($attributes, true)->toResponse(); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Order/Client.php b/vendor/overtrue/wechat/src/Payment/Order/Client.php new file mode 100644 index 0000000..0be54ed --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Order/Client.php @@ -0,0 +1,117 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Order; + +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\Exceptions\InvalidConfigException; +use EasyWeChat\Kernel\Support; +use EasyWeChat\Kernel\Support\Collection; +use EasyWeChat\Payment\Kernel\BaseClient; +use Psr\Http\Message\ResponseInterface; + +class Client extends BaseClient +{ + /** + * Unify order. + * + * @param bool $isContract + * + * @return ResponseInterface|Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function unify(array $params, $isContract = false) + { + if (empty($params['spbill_create_ip'])) { + $params['spbill_create_ip'] = ('NATIVE' === $params['trade_type']) ? Support\get_server_ip() : Support\get_client_ip(); + } + + $params['appid'] = $this->app['config']->app_id; + $params['notify_url'] = $params['notify_url'] ?? $this->app['config']['notify_url']; + + if ($isContract) { + $params['contract_appid'] = $this->app['config']['app_id']; + $params['contract_mchid'] = $this->app['config']['mch_id']; + $params['request_serial'] = $params['request_serial'] ?? time(); + $params['contract_notify_url'] = $params['contract_notify_url'] ?? $this->app['config']['contract_notify_url']; + + return $this->request($this->wrap('pay/contractorder'), $params); + } + + return $this->request($this->wrap('pay/unifiedorder'), $params); + } + + /** + * Query order by out trade number. + * + * @return ResponseInterface|Collection|array|object|string + * + * @throws InvalidArgumentException + * @throws InvalidConfigException + */ + public function queryByOutTradeNumber(string $number) + { + return $this->query([ + 'out_trade_no' => $number, + ]); + } + + /** + * Query order by transaction id. + * + * @return ResponseInterface|Collection|array|object|string + * + * @throws InvalidArgumentException + * @throws InvalidConfigException + */ + public function queryByTransactionId(string $transactionId) + { + return $this->query([ + 'transaction_id' => $transactionId, + ]); + } + + /** + * @return ResponseInterface|Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function query(array $params) + { + $params['appid'] = $this->app['config']->app_id; + + return $this->request($this->wrap('pay/orderquery'), $params); + } + + /** + * Close order by out_trade_no. + * + * @return ResponseInterface|Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function close(string $tradeNo) + { + $params = [ + 'appid' => $this->app['config']->app_id, + 'out_trade_no' => $tradeNo, + ]; + + return $this->request($this->wrap('pay/closeorder'), $params); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Order/ServiceProvider.php b/vendor/overtrue/wechat/src/Payment/Order/ServiceProvider.php new file mode 100644 index 0000000..9e781c0 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Order/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Order; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['order'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Payment/ProfitSharing/Client.php b/vendor/overtrue/wechat/src/Payment/ProfitSharing/Client.php new file mode 100644 index 0000000..8b75b32 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/ProfitSharing/Client.php @@ -0,0 +1,249 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\ProfitSharing; + +use EasyWeChat\Payment\Kernel\BaseClient; + +/** + * Class Client. + * + * @author ClouderSky + */ +class Client extends BaseClient +{ + /** + * {@inheritdoc}. + */ + protected function prepends() + { + return [ + 'sign_type' => 'HMAC-SHA256', + ]; + } + + /** + * Add profit sharing receiver. + * 服务商代子商户发起添加分账接收方请求. + * 后续可通过发起分账请求将结算后的钱分到该分账接收方. + * + * @param array $receiver 分账接收方对象,json格式 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function addReceiver(array $receiver) + { + $params = [ + 'appid' => $this->app['config']->app_id, + 'receiver' => json_encode( + $receiver, + JSON_UNESCAPED_UNICODE + ), + ]; + + return $this->request( + 'pay/profitsharingaddreceiver', + $params + ); + } + + /** + * Delete profit sharing receiver. + * 服务商代子商户发起删除分账接收方请求. + * 删除后不支持将结算后的钱分到该分账接收方. + * + * @param array $receiver 分账接收方对象,json格式 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function deleteReceiver(array $receiver) + { + $params = [ + 'appid' => $this->app['config']->app_id, + 'receiver' => json_encode( + $receiver, + JSON_UNESCAPED_UNICODE + ), + ]; + + return $this->request( + 'pay/profitsharingremovereceiver', + $params + ); + } + + /** + * Single profit sharing. + * 请求单次分账. + * + * @param string $transactionId 微信支付订单号 + * @param string $outOrderNo 商户系统内部的分账单号 + * @param array $receivers 分账接收方列表 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function share( + string $transactionId, + string $outOrderNo, + array $receivers + ) { + $params = [ + 'appid' => $this->app['config']->app_id, + 'transaction_id' => $transactionId, + 'out_order_no' => $outOrderNo, + 'receivers' => json_encode( + $receivers, + JSON_UNESCAPED_UNICODE + ), + ]; + + return $this->safeRequest( + 'secapi/pay/profitsharing', + $params + ); + } + + /** + * Multi profit sharing. + * 请求多次分账. + * + * @param string $transactionId 微信支付订单号 + * @param string $outOrderNo 商户系统内部的分账单号 + * @param array $receivers 分账接收方列表 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function multiShare( + string $transactionId, + string $outOrderNo, + array $receivers + ) { + $params = [ + 'appid' => $this->app['config']->app_id, + 'transaction_id' => $transactionId, + 'out_order_no' => $outOrderNo, + 'receivers' => json_encode( + $receivers, + JSON_UNESCAPED_UNICODE + ), + ]; + + return $this->safeRequest( + 'secapi/pay/multiprofitsharing', + $params + ); + } + + /** + * Finish profit sharing. + * 完结分账. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function markOrderAsFinished(array $params) + { + $params['appid'] = $this->app['config']->app_id; + $params['sub_appid'] = null; + + return $this->safeRequest( + 'secapi/pay/profitsharingfinish', + $params + ); + } + + /** + * Query profit sharing result. + * 查询分账结果. + * + * @param string $transactionId 微信支付订单号 + * @param string $outOrderNo 商户系统内部的分账单号 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function query( + string $transactionId, + string $outOrderNo + ) { + $params = [ + 'sub_appid' => null, + 'transaction_id' => $transactionId, + 'out_order_no' => $outOrderNo, + ]; + + return $this->request( + 'pay/profitsharingquery', + $params + ); + } + + /** + * Profit sharing return. + * 分账回退. + * + * @param string $outOrderNo 商户系统内部的分账单号 + * @param string $outReturnNo 商户系统内部分账回退单号 + * @param int $returnAmount 回退金额 + * @param string $returnAccount 回退方账号 + * @param string $description 回退描述 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function returnShare( + string $outOrderNo, + string $outReturnNo, + int $returnAmount, + string $returnAccount, + string $description + ) { + $params = [ + 'appid' => $this->app['config']->app_id, + 'out_order_no' => $outOrderNo, + 'out_return_no' => $outReturnNo, + 'return_account_type' => 'MERCHANT_ID', + 'return_account' => $returnAccount, + 'return_amount' => $returnAmount, + 'description' => $description, + ]; + + return $this->safeRequest( + 'secapi/pay/profitsharingreturn', + $params + ); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/ProfitSharing/ServiceProvider.php b/vendor/overtrue/wechat/src/Payment/ProfitSharing/ServiceProvider.php new file mode 100644 index 0000000..d247d53 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/ProfitSharing/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\ProfitSharing; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author ClouderSky + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['profit_sharing'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Redpack/Client.php b/vendor/overtrue/wechat/src/Payment/Redpack/Client.php new file mode 100644 index 0000000..1f16fb0 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Redpack/Client.php @@ -0,0 +1,102 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Redpack; + +use EasyWeChat\Kernel\Support; +use EasyWeChat\Payment\Kernel\BaseClient; + +/** + * Class Client. + * + * @author tianyong90 <412039588@qq.com> + */ +class Client extends BaseClient +{ + /** + * Query redpack. + * + * @param mixed $mchBillno + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function info($mchBillno) + { + $params = is_array($mchBillno) ? $mchBillno : ['mch_billno' => $mchBillno]; + $base = [ + 'appid' => $this->app['config']->app_id, + 'bill_type' => 'MCHT', + ]; + + return $this->safeRequest('mmpaymkttransfers/gethbinfo', array_merge($base, $params)); + } + + /** + * Send miniprogram normal redpack. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function sendMiniprogramNormal(array $params) + { + $base = [ + 'total_num' => 1, + 'client_ip' => $params['client_ip'] ?? Support\get_server_ip(), + 'wxappid' => $this->app['config']->app_id, + ]; + + return $this->safeRequest('mmpaymkttransfers/sendminiprogramhb', array_merge($base, $params)); + } + + /** + * Send normal redpack. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function sendNormal(array $params) + { + $base = [ + 'total_num' => 1, + 'client_ip' => $params['client_ip'] ?? Support\get_server_ip(), + 'wxappid' => $this->app['config']->app_id, + ]; + + return $this->safeRequest('mmpaymkttransfers/sendredpack', array_merge($base, $params)); + } + + /** + * Send group redpack. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function sendGroup(array $params) + { + $base = [ + 'amt_type' => 'ALL_RAND', + 'wxappid' => $this->app['config']->app_id, + ]; + + return $this->safeRequest('mmpaymkttransfers/sendgroupredpack', array_merge($base, $params)); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Redpack/ServiceProvider.php b/vendor/overtrue/wechat/src/Payment/Redpack/ServiceProvider.php new file mode 100644 index 0000000..af36f35 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Redpack/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Redpack; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['redpack'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Refund/Client.php b/vendor/overtrue/wechat/src/Payment/Refund/Client.php new file mode 100644 index 0000000..0036bde --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Refund/Client.php @@ -0,0 +1,133 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Refund; + +use EasyWeChat\Payment\Kernel\BaseClient; + +class Client extends BaseClient +{ + /** + * Refund by out trade number. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function byOutTradeNumber(string $number, string $refundNumber, int $totalFee, int $refundFee, array $optional = []) + { + return $this->refund($refundNumber, $totalFee, $refundFee, array_merge($optional, ['out_trade_no' => $number])); + } + + /** + * Refund by transaction id. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function byTransactionId(string $transactionId, string $refundNumber, int $totalFee, int $refundFee, array $optional = []) + { + return $this->refund($refundNumber, $totalFee, $refundFee, array_merge($optional, ['transaction_id' => $transactionId])); + } + + /** + * Query refund by transaction id. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function queryByTransactionId(string $transactionId) + { + return $this->query($transactionId, 'transaction_id'); + } + + /** + * Query refund by out trade number. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function queryByOutTradeNumber(string $outTradeNumber) + { + return $this->query($outTradeNumber, 'out_trade_no'); + } + + /** + * Query refund by out refund number. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function queryByOutRefundNumber(string $outRefundNumber) + { + return $this->query($outRefundNumber, 'out_refund_no'); + } + + /** + * Query refund by refund id. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function queryByRefundId(string $refundId) + { + return $this->query($refundId, 'refund_id'); + } + + /** + * Refund. + * + * @param array $optional + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function refund(string $refundNumber, int $totalFee, int $refundFee, $optional = []) + { + $params = array_merge([ + 'out_refund_no' => $refundNumber, + 'total_fee' => $totalFee, + 'refund_fee' => $refundFee, + 'appid' => $this->app['config']->app_id, + ], $optional); + + return $this->safeRequest($this->wrap( + $this->app->inSandbox() ? 'pay/refund' : 'secapi/pay/refund' + ), $params); + } + + /** + * Query refund. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function query(string $number, string $type) + { + $params = [ + 'appid' => $this->app['config']->app_id, + $type => $number, + ]; + + return $this->request($this->wrap('pay/refundquery'), $params); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Refund/ServiceProvider.php b/vendor/overtrue/wechat/src/Payment/Refund/ServiceProvider.php new file mode 100644 index 0000000..faa4e89 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Refund/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Refund; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['refund'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Reverse/Client.php b/vendor/overtrue/wechat/src/Payment/Reverse/Client.php new file mode 100644 index 0000000..ee0b245 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Reverse/Client.php @@ -0,0 +1,60 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Reverse; + +use EasyWeChat\Payment\Kernel\BaseClient; + +class Client extends BaseClient +{ + /** + * Reverse order by out trade number. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function byOutTradeNumber(string $outTradeNumber) + { + return $this->reverse($outTradeNumber, 'out_trade_no'); + } + + /** + * Reverse order by transaction_id. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function byTransactionId(string $transactionId) + { + return $this->reverse($transactionId, 'transaction_id'); + } + + /** + * Reverse order. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function reverse(string $number, string $type) + { + $params = [ + 'appid' => $this->app['config']->app_id, + $type => $number, + ]; + + return $this->safeRequest($this->wrap('secapi/pay/reverse'), $params); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Reverse/ServiceProvider.php b/vendor/overtrue/wechat/src/Payment/Reverse/ServiceProvider.php new file mode 100644 index 0000000..2417874 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Reverse/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Reverse; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['reverse'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Sandbox/Client.php b/vendor/overtrue/wechat/src/Payment/Sandbox/Client.php new file mode 100644 index 0000000..e5a6b44 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Sandbox/Client.php @@ -0,0 +1,55 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Sandbox; + +use EasyWeChat\Kernel\Traits\InteractsWithCache; +use EasyWeChat\Payment\Kernel\BaseClient; +use EasyWeChat\Payment\Kernel\Exceptions\SandboxException; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + use InteractsWithCache; + + /** + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Payment\Kernel\Exceptions\SandboxException + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getKey(): string + { + if ($cache = $this->getCache()->get($this->getCacheKey())) { + return $cache; + } + + $response = $this->requestArray('sandboxnew/pay/getsignkey'); + + if ('SUCCESS' === $response['return_code']) { + $this->getCache()->set($this->getCacheKey(), $key = $response['sandbox_signkey'], 24 * 3600); + + return $key; + } + + throw new SandboxException($response['retmsg'] ?? $response['return_msg']); + } + + protected function getCacheKey(): string + { + return 'easywechat.payment.sandbox.'.md5($this->app['config']->app_id.$this->app['config']['mch_id']); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Sandbox/ServiceProvider.php b/vendor/overtrue/wechat/src/Payment/Sandbox/ServiceProvider.php new file mode 100644 index 0000000..05fb7dd --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Sandbox/ServiceProvider.php @@ -0,0 +1,30 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Sandbox; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app['sandbox'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Security/Client.php b/vendor/overtrue/wechat/src/Payment/Security/Client.php new file mode 100644 index 0000000..01aa752 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Security/Client.php @@ -0,0 +1,38 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Security; + +use EasyWeChat\Payment\Kernel\BaseClient; + +/** + * Class Client. + * + * @author overtrue + */ +class Client extends BaseClient +{ + /** + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getPublicKey() + { + $params = [ + 'sign_type' => 'MD5', + ]; + + return $this->safeRequest('https://fraud.mch.weixin.qq.com/risk/getpublickey', $params); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Security/ServiceProvider.php b/vendor/overtrue/wechat/src/Payment/Security/ServiceProvider.php new file mode 100644 index 0000000..e0e31f8 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Security/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Security; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['security'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Transfer/Client.php b/vendor/overtrue/wechat/src/Payment/Transfer/Client.php new file mode 100644 index 0000000..1e80c68 --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Transfer/Client.php @@ -0,0 +1,114 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Transfer; + +use EasyWeChat\Kernel\Exceptions\RuntimeException; +use EasyWeChat\Payment\Kernel\BaseClient; +use function EasyWeChat\Kernel\Support\get_server_ip; +use function EasyWeChat\Kernel\Support\rsa_public_encrypt; + +/** + * Class Client. + * + * @author AC + */ +class Client extends BaseClient +{ + /** + * Query MerchantPay to balance. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function queryBalanceOrder(string $partnerTradeNo) + { + $params = [ + 'appid' => $this->app['config']->app_id, + 'mch_id' => $this->app['config']->mch_id, + 'partner_trade_no' => $partnerTradeNo, + ]; + + return $this->safeRequest('mmpaymkttransfers/gettransferinfo', $params); + } + + /** + * Send MerchantPay to balance. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function toBalance(array $params) + { + $base = [ + 'mch_id' => null, + 'mchid' => $this->app['config']->mch_id, + 'mch_appid' => $this->app['config']->app_id, + ]; + + if (empty($params['spbill_create_ip'])) { + $params['spbill_create_ip'] = get_server_ip(); + } + + return $this->safeRequest('mmpaymkttransfers/promotion/transfers', array_merge($base, $params)); + } + + /** + * Query MerchantPay order to BankCard. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function queryBankCardOrder(string $partnerTradeNo) + { + $params = [ + 'mch_id' => $this->app['config']->mch_id, + 'partner_trade_no' => $partnerTradeNo, + ]; + + return $this->safeRequest('mmpaysptrans/query_bank', $params); + } + + /** + * Send MerchantPay to BankCard. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function toBankCard(array $params) + { + foreach (['bank_code', 'partner_trade_no', 'enc_bank_no', 'enc_true_name', 'amount'] as $key) { + if (empty($params[$key])) { + throw new RuntimeException(\sprintf('"%s" is required.', $key)); + } + } + + $publicKey = file_get_contents($this->app['config']->get('rsa_public_key_path')); + + $params['enc_bank_no'] = rsa_public_encrypt($params['enc_bank_no'], $publicKey); + $params['enc_true_name'] = rsa_public_encrypt($params['enc_true_name'], $publicKey); + + return $this->safeRequest('mmpaysptrans/pay_bank', $params); + } +} diff --git a/vendor/overtrue/wechat/src/Payment/Transfer/ServiceProvider.php b/vendor/overtrue/wechat/src/Payment/Transfer/ServiceProvider.php new file mode 100644 index 0000000..0e27aad --- /dev/null +++ b/vendor/overtrue/wechat/src/Payment/Transfer/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Payment\Transfer; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['transfer'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/Agent/Client.php b/vendor/overtrue/wechat/src/Work/Agent/Client.php new file mode 100644 index 0000000..f049883 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Agent/Client.php @@ -0,0 +1,63 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Agent; + +use EasyWeChat\Kernel\BaseClient; + +/** + * This is WeWork Agent Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * Get agent. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function get(int $agentId) + { + $params = [ + 'agentid' => $agentId, + ]; + + return $this->httpGet('cgi-bin/agent/get', $params); + } + + /** + * Set agent. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function set(int $agentId, array $attributes) + { + return $this->httpPostJson('cgi-bin/agent/set', array_merge(['agentid' => $agentId], $attributes)); + } + + /** + * Get agent list. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function list() + { + return $this->httpGet('cgi-bin/agent/list'); + } +} diff --git a/vendor/overtrue/wechat/src/Work/Agent/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/Agent/ServiceProvider.php new file mode 100644 index 0000000..b85a6f5 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Agent/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Agent; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['agent'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/Application.php b/vendor/overtrue/wechat/src/Work/Application.php new file mode 100644 index 0000000..30d4241 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Application.php @@ -0,0 +1,105 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work; + +use EasyWeChat\Kernel\ServiceContainer; +use EasyWeChat\Work\MiniProgram\Application as MiniProgram; + +/** + * Application. + * + * @author mingyoung + * + * @property \EasyWeChat\Work\OA\Client $oa + * @property \EasyWeChat\Work\Auth\AccessToken $access_token + * @property \EasyWeChat\Work\Agent\Client $agent + * @property \EasyWeChat\Work\Department\Client $department + * @property \EasyWeChat\Work\Media\Client $media + * @property \EasyWeChat\Work\Menu\Client $menu + * @property \EasyWeChat\Work\Message\Client $message + * @property \EasyWeChat\Work\Message\Messenger $messenger + * @property \EasyWeChat\Work\User\Client $user + * @property \EasyWeChat\Work\User\TagClient $tag + * @property \EasyWeChat\Work\Server\Guard $server + * @property \EasyWeChat\Work\Jssdk\Client $jssdk + * @property \Overtrue\Socialite\Providers\WeWorkProvider $oauth + * @property \EasyWeChat\Work\Invoice\Client $invoice + * @property \EasyWeChat\Work\Chat\Client $chat + * @property \EasyWeChat\Work\ExternalContact\Client $external_contact + * @property \EasyWeChat\Work\ExternalContact\ContactWayClient $contact_way + * @property \EasyWeChat\Work\ExternalContact\StatisticsClient $external_contact_statistics + * @property \EasyWeChat\Work\ExternalContact\MessageClient $external_contact_message + * @property \EasyWeChat\Work\GroupRobot\Client $group_robot + * @property \EasyWeChat\Work\GroupRobot\Messenger $group_robot_messenger + * @property \EasyWeChat\Work\Calendar\Client $calendar + * @property \EasyWeChat\Work\Schedule\Client $schedule + * @property \EasyWeChat\Work\MsgAudit\Client $msg_audit + * @property \EasyWeChat\Work\ExternalContact\SchoolClient $school + * + * @method mixed getCallbackIp() + */ +class Application extends ServiceContainer +{ + /** + * @var array + */ + protected $providers = [ + OA\ServiceProvider::class, + Auth\ServiceProvider::class, + Base\ServiceProvider::class, + Menu\ServiceProvider::class, + OAuth\ServiceProvider::class, + User\ServiceProvider::class, + Agent\ServiceProvider::class, + Media\ServiceProvider::class, + Message\ServiceProvider::class, + Department\ServiceProvider::class, + Server\ServiceProvider::class, + Jssdk\ServiceProvider::class, + Invoice\ServiceProvider::class, + Chat\ServiceProvider::class, + ExternalContact\ServiceProvider::class, + GroupRobot\ServiceProvider::class, + Calendar\ServiceProvider::class, + Schedule\ServiceProvider::class, + MsgAudit\ServiceProvider::class, + ]; + + /** + * @var array + */ + protected $defaultConfig = [ + // http://docs.guzzlephp.org/en/stable/request-options.html + 'http' => [ + 'base_uri' => 'https://qyapi.weixin.qq.com/', + ], + ]; + + /** + * Creates the miniProgram application. + */ + public function miniProgram(): MiniProgram + { + return new MiniProgram($this->getConfig()); + } + + /** + * @param string $method + * @param array $arguments + * + * @return mixed + */ + public function __call($method, $arguments) + { + return $this['base']->$method(...$arguments); + } +} diff --git a/vendor/overtrue/wechat/src/Work/Auth/AccessToken.php b/vendor/overtrue/wechat/src/Work/Auth/AccessToken.php new file mode 100644 index 0000000..2ad59d4 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Auth/AccessToken.php @@ -0,0 +1,43 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Auth; + +use EasyWeChat\Kernel\AccessToken as BaseAccessToken; + +/** + * Class AccessToken. + * + * @author mingyoung + */ +class AccessToken extends BaseAccessToken +{ + /** + * @var string + */ + protected $endpointToGetToken = 'cgi-bin/gettoken'; + + /** + * @var int + */ + protected $safeSeconds = 0; + + /** + * Credential for get token. + */ + protected function getCredentials(): array + { + return [ + 'corpid' => $this->app['config']['corp_id'], + 'corpsecret' => $this->app['config']['secret'], + ]; + } +} diff --git a/vendor/overtrue/wechat/src/Work/Auth/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/Auth/ServiceProvider.php new file mode 100644 index 0000000..5f0dba9 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Auth/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Auth; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + isset($app['access_token']) || $app['access_token'] = function ($app) { + return new AccessToken($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/Base/Client.php b/vendor/overtrue/wechat/src/Work/Base/Client.php new file mode 100644 index 0000000..97b5445 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Base/Client.php @@ -0,0 +1,34 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Base; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * Get callback ip. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getCallbackIp() + { + return $this->httpGet('cgi-bin/getcallbackip'); + } +} diff --git a/vendor/overtrue/wechat/src/Work/Base/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/Base/ServiceProvider.php new file mode 100644 index 0000000..375eb76 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Base/ServiceProvider.php @@ -0,0 +1,30 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Base; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app['base'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/Calendar/Client.php b/vendor/overtrue/wechat/src/Work/Calendar/Client.php new file mode 100644 index 0000000..ed6e99d --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Calendar/Client.php @@ -0,0 +1,78 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Calendar; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author her-cat + */ +class Client extends BaseClient +{ + /** + * Add a calendar. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function add(array $calendar) + { + return $this->httpPostJson('cgi-bin/oa/calendar/add', compact('calendar')); + } + + /** + * Update the calendar. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(string $id, array $calendar) + { + $calendar += ['cal_id' => $id]; + + return $this->httpPostJson('cgi-bin/oa/calendar/update', compact('calendar')); + } + + /** + * Get one or more calendars. + * + * @param string|array $ids + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get($ids) + { + return $this->httpPostJson('cgi-bin/oa/calendar/get', ['cal_id_list' => (array) $ids]); + } + + /** + * Delete a calendar. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(string $id) + { + return $this->httpPostJson('cgi-bin/oa/calendar/del', ['cal_id' => $id]); + } +} diff --git a/vendor/overtrue/wechat/src/Work/Calendar/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/Calendar/ServiceProvider.php new file mode 100644 index 0000000..1819e4f --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Calendar/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Calendar; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author her-cat + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc} + */ + public function register(Container $app) + { + $app['calendar'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/Chat/Client.php b/vendor/overtrue/wechat/src/Work/Chat/Client.php new file mode 100644 index 0000000..8881989 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Chat/Client.php @@ -0,0 +1,73 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Chat; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author XiaolonY + */ +class Client extends BaseClient +{ + /** + * Get chat. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function get(string $chatId) + { + return $this->httpGet('cgi-bin/appchat/get', ['chatid' => $chatId]); + } + + /** + * Create chat. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create(array $data) + { + return $this->httpPostJson('cgi-bin/appchat/create', $data); + } + + /** + * Update chat. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(string $chatId, array $data) + { + return $this->httpPostJson('cgi-bin/appchat/update', array_merge(['chatid' => $chatId], $data)); + } + + /** + * Send a message. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function send(array $message) + { + return $this->httpPostJson('cgi-bin/appchat/send', $message); + } +} diff --git a/vendor/overtrue/wechat/src/Work/Chat/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/Chat/ServiceProvider.php new file mode 100644 index 0000000..5bf9b2c --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Chat/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Chat; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author XiaolonY + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['chat'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/Department/Client.php b/vendor/overtrue/wechat/src/Work/Department/Client.php new file mode 100644 index 0000000..3b23042 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Department/Client.php @@ -0,0 +1,76 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Department; + +use EasyWeChat\Kernel\BaseClient; + +/** + * This is WeWork Department Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * Create a department. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create(array $data) + { + return $this->httpPostJson('cgi-bin/department/create', $data); + } + + /** + * Update a department. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(int $id, array $data) + { + return $this->httpPostJson('cgi-bin/department/update', array_merge(compact('id'), $data)); + } + + /** + * Delete a department. + * + * @param int $id + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function delete($id) + { + return $this->httpGet('cgi-bin/department/delete', compact('id')); + } + + /** + * Get department lists. + * + * @param int|null $id + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function list($id = null) + { + return $this->httpGet('cgi-bin/department/list', compact('id')); + } +} diff --git a/vendor/overtrue/wechat/src/Work/Department/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/Department/ServiceProvider.php new file mode 100644 index 0000000..2fe5bc6 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Department/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Department; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['department'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/ExternalContact/Client.php b/vendor/overtrue/wechat/src/Work/ExternalContact/Client.php new file mode 100644 index 0000000..21ece98 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/ExternalContact/Client.php @@ -0,0 +1,266 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\ExternalContact; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * 获取配置了客户联系功能的成员列表. + * + * @see https://work.weixin.qq.com/api/doc#90000/90135/91554 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getFollowUsers() + { + return $this->httpGet('cgi-bin/externalcontact/get_follow_user_list'); + } + + /** + * 获取外部联系人列表. + * + * @see https://work.weixin.qq.com/api/doc#90000/90135/91555 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function list(string $userId) + { + return $this->httpGet('cgi-bin/externalcontact/list', [ + 'userid' => $userId, + ]); + } + + /** + * 获取外部联系人详情. + * + * @see https://work.weixin.qq.com/api/doc#90000/90135/91556 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function get(string $externalUserId) + { + return $this->httpGet('cgi-bin/externalcontact/get', [ + 'external_userid' => $externalUserId, + ]); + } + + /** + * 批量获取外部联系人详情. + * + * @see https://work.weixin.qq.com/api/doc/90000/90135/92994 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function batchGetUsers(array $data) + { + return $this->httpPostJson( + 'cgi-bin/externalcontact/batch/get_by_user', + $data + ); + } + + /** + * 修改客户备注信息. + * + * @see https://work.weixin.qq.com/api/doc/90000/90135/92115 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function remark(array $data) + { + return $this->httpPostJson( + 'cgi-bin/externalcontact/remark', + $data + ); + } + + /** + * 获取离职成员的客户列表. + * + * @see https://work.weixin.qq.com/api/doc#90000/90135/91563 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getUnassigned(int $pageId = 0, int $pageSize = 1000) + { + return $this->httpPostJson('cgi-bin/externalcontact/get_unassigned_list', [ + 'page_id' => $pageId, + 'page_size' => $pageSize, + ]); + } + + /** + * 离职成员的外部联系人再分配. + * + * @see https://work.weixin.qq.com/api/doc#90000/90135/91564 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function transfer(string $externalUserId, string $handoverUserId, string $takeoverUserId) + { + $params = [ + 'external_userid' => $externalUserId, + 'handover_userid' => $handoverUserId, + 'takeover_userid' => $takeoverUserId, + ]; + + return $this->httpPostJson('cgi-bin/externalcontact/transfer', $params); + } + + /** + * 获取客户群列表. + * + * @see https://work.weixin.qq.com/api/doc/90000/90135/92120 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getGroupChats(array $params) + { + return $this->httpPostJson('cgi-bin/externalcontact/groupchat/list', $params); + } + + /** + * 获取客户群详情. + * + * @see https://work.weixin.qq.com/api/doc/90000/90135/92122 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getGroupChat(string $chatId, int $needName = 0) + { + $params = [ + 'chat_id' => $chatId, + 'need_name' => $needName, + ]; + + return $this->httpPostJson('cgi-bin/externalcontact/groupchat/get', $params); + } + + /** + * 获取企业标签库. + * + * @see https://work.weixin.qq.com/api/doc/90000/90135/92117#获取企业标签库 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getCorpTags(array $tagIds = []) + { + $params = [ + 'tag_id' => $tagIds, + ]; + + return $this->httpPostJson('cgi-bin/externalcontact/get_corp_tag_list', $params); + } + + /** + * 添加企业客户标签. + * + * @see https://work.weixin.qq.com/api/doc/90000/90135/92117#添加企业客户标签 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function addCorpTag(array $params) + { + return $this->httpPostJson('cgi-bin/externalcontact/add_corp_tag', $params); + } + + /** + * 编辑企业客户标签. + * + * @see https://work.weixin.qq.com/api/doc/90000/90135/92117#编辑企业客户标签 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function updateCorpTag(string $id, string $name, int $order = 1) + { + $params = [ + 'id' => $id, + 'name' => $name, + 'order' => $order, + ]; + + return $this->httpPostJson('cgi-bin/externalcontact/edit_corp_tag', $params); + } + + /** + * 删除企业客户标签. + * + * @see https://work.weixin.qq.com/api/doc/90000/90135/92117#删除企业客户标签 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function deleteCorpTag(array $tagId, array $groupId) + { + $params = [ + 'tag_id' => $tagId, + 'group_id' => $groupId, + ]; + + return $this->httpPostJson('cgi-bin/externalcontact/del_corp_tag', $params); + } + + /** + * 编辑客户企业标签. + * + * @see https://work.weixin.qq.com/api/doc/90000/90135/92118 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function markTags(array $params) + { + return $this->httpPostJson('cgi-bin/externalcontact/mark_tag', $params); + } +} diff --git a/vendor/overtrue/wechat/src/Work/ExternalContact/ContactWayClient.php b/vendor/overtrue/wechat/src/Work/ExternalContact/ContactWayClient.php new file mode 100644 index 0000000..e373f94 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/ExternalContact/ContactWayClient.php @@ -0,0 +1,87 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\ExternalContact; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class ContactWayClient. + * + * @author milkmeowo + */ +class ContactWayClient extends BaseClient +{ + /** + * 配置客户联系「联系我」方式. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create(int $type, int $scene, array $config = []) + { + $params = array_merge([ + 'type' => $type, + 'scene' => $scene, + ], $config); + + return $this->httpPostJson('cgi-bin/externalcontact/add_contact_way', $params); + } + + /** + * 获取企业已配置的「联系我」方式. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(string $configId) + { + return $this->httpPostJson('cgi-bin/externalcontact/get_contact_way', [ + 'config_id' => $configId, + ]); + } + + /** + * 更新企业已配置的「联系我」方式. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(string $configId, array $config = []) + { + $params = array_merge([ + 'config_id' => $configId, + ], $config); + + return $this->httpPostJson('cgi-bin/externalcontact/update_contact_way', $params); + } + + /** + * 删除企业已配置的「联系我」方式. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(string $configId) + { + return $this->httpPostJson('cgi-bin/externalcontact/del_contact_way', [ + 'config_id' => $configId, + ]); + } +} diff --git a/vendor/overtrue/wechat/src/Work/ExternalContact/MessageClient.php b/vendor/overtrue/wechat/src/Work/ExternalContact/MessageClient.php new file mode 100644 index 0000000..0b47e3a --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/ExternalContact/MessageClient.php @@ -0,0 +1,156 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\ExternalContact; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; + +/** + * Class MessageClient. + * + * @author milkmeowo + */ +class MessageClient extends BaseClient +{ + /** + * Required attributes. + * + * @var array + */ + protected $required = ['media_id', 'title', 'url', 'pic_media_id', 'appid', 'page']; + + protected $textMessage = [ + 'content' => '', + ]; + + protected $imageMessage = [ + 'media_id' => '', + ]; + + protected $linkMessage = [ + 'title' => '', + 'picurl' => '', + 'desc' => '', + 'url' => '', + ]; + + protected $miniprogramMessage = [ + 'title' => '', + 'pic_media_id' => '', + 'appid' => '', + 'page' => '', + ]; + + /** + * 添加企业群发消息模板 + * + * @see https://work.weixin.qq.com/api/doc#90000/90135/91560 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function submit(array $msg) + { + $params = $this->formatMessage($msg); + + return $this->httpPostJson('cgi-bin/externalcontact/add_msg_template', $params); + } + + /** + * 获取企业群发消息发送结果. + * + * @see https://work.weixin.qq.com/api/doc#90000/90135/91561 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(string $msgId) + { + return $this->httpPostJson('cgi-bin/externalcontact/get_group_msg_result', [ + 'msgid' => $msgId, + ]); + } + + /** + * 发送新客户欢迎语. + * + * @see https://work.weixin.qq.com/api/doc#90000/90135/91688 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function sendWelcome(string $welcomeCode, array $msg) + { + $formattedMsg = $this->formatMessage($msg); + + $params = array_merge($formattedMsg, [ + 'welcome_code' => $welcomeCode, + ]); + + return $this->httpPostJson('cgi-bin/externalcontact/send_welcome_msg', $params); + } + + /** + * @return array + * + * @throws InvalidArgumentException + */ + protected function formatMessage(array $data = []) + { + $params = $data; + + if (!empty($params['text'])) { + $params['text'] = $this->formatFields($params['text'], $this->textMessage); + } + + if (!empty($params['image'])) { + $params['image'] = $this->formatFields($params['image'], $this->imageMessage); + } + + if (!empty($params['link'])) { + $params['link'] = $this->formatFields($params['link'], $this->linkMessage); + } + + if (!empty($params['miniprogram'])) { + $params['miniprogram'] = $this->formatFields($params['miniprogram'], $this->miniprogramMessage); + } + + return $params; + } + + /** + * @return array + * + * @throws InvalidArgumentException + */ + protected function formatFields(array $data = [], array $default = []) + { + $params = array_merge($default, $data); + foreach ($params as $key => $value) { + if (in_array($key, $this->required, true) && empty($value) && empty($default[$key])) { + throw new InvalidArgumentException(sprintf('Attribute "%s" can not be empty!', $key)); + } + + $params[$key] = empty($value) ? $default[$key] : $value; + } + + return $params; + } +} diff --git a/vendor/overtrue/wechat/src/Work/ExternalContact/SchoolClient.php b/vendor/overtrue/wechat/src/Work/ExternalContact/SchoolClient.php new file mode 100644 index 0000000..1e86328 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/ExternalContact/SchoolClient.php @@ -0,0 +1,454 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\ExternalContact; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author MillsGuo + */ +class SchoolClient extends BaseClient +{ + /** + * 创建部门 + * @see https://work.weixin.qq.com/api/doc/90000/90135/92340 + * @param string $name + * @param int $parentId + * @param int $type + * @param int $standardGrade + * @param int $registerYear + * @param int $order + * @param array $departmentAdmins [['userid':'139','type':1],['userid':'1399','type':2]] + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function createDepartment(string $name, int $parentId, int $type, int $standardGrade, int $registerYear, int $order, array $departmentAdmins) + { + $params = [ + 'name' => $name, + 'parentid' => $parentId, + 'type' => $type, + 'standard_grade' => $standardGrade, + 'register_year' => $registerYear, + 'order' => $order, + 'department_admins' => $departmentAdmins + ]; + + return $this->httpPostJson('cgi-bin/school/department/create', $params); + } + + /** + * 更新部门 + * @see https://work.weixin.qq.com/api/doc/90000/90135/92341 + * @param int $id + * @param string $name + * @param int $parentId + * @param int $type + * @param int $standardGrade + * @param int $registerYear + * @param int $order + * @param array $departmentAdmins [['op':0,'userid':'139','type':1],['op':1,'userid':'1399','type':2]] OP=0表示新增或更新,OP=1表示删除管理员 + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function updateDepartment(int $id, string $name, int $parentId, int $type, int $standardGrade, int $registerYear, int $order, array $departmentAdmins) + { + $params = [ + 'id' => $id, + 'name' => $name, + 'parentid' => $parentId, + 'type' => $type, + 'standard_grade' => $standardGrade, + 'register_year' => $registerYear, + 'order' => $order, + 'department_admins' => $departmentAdmins + ]; + $params = $this->filterNullValue($params); + + return $this->httpPostJson('cgi-bin/school/department/update', $params); + } + + /** + * 删除部门 + * @see https://work.weixin.qq.com/api/doc/90000/90135/92342 + * @param int $id + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function deleteDepartment(int $id) + { + return $this->httpGet('cgi-bin/school/department/delete', [ + 'id' => $id + ]); + } + + /** + * 获取部门列表 + * @see https://work.weixin.qq.com/api/doc/90000/90135/92343 + * @param int $id 如果ID为0,则获取全量组织架构 + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getDepartments(int $id) + { + if ($id > 0) { + $params = [ + 'id' => $id + ]; + } else { + $params = []; + } + + return $this->httpGet('cgi-bin/school/department/list', $params); + } + + /** + * 创建学生 + * @see https://work.weixin.qq.com/api/doc/90000/90135/92325 + * @param string $userId + * @param string $name + * @param array $department + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function createStudent(string $userId, string $name, array $department) + { + $params = [ + 'student_userid' => $userId, + 'name' => $name, + 'department' => $department + ]; + + return $this->httpPostJson('cgi-bin/school/user/create_student', $params); + } + + /** + * 删除学生 + * @see https://work.weixin.qq.com/api/doc/90000/90135/92326 + * @param string $userId + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function deleteStudent(string $userId) + { + return $this->httpGet('cgi-bin/school/user/delete_student', [ + 'userid' => $userId + ]); + } + + /** + * 更新学生 + * @see https://work.weixin.qq.com/api/doc/90000/90135/92327 + * @param string $userId + * @param string $name + * @param string $newUserId + * @param array $department + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function updateStudent(string $userId, string $name, string $newUserId, array $department) + { + $params = [ + 'student_userid' => $userId + ]; + if (!empty($name)) { + $params['name'] = $name; + } + if (!empty($newUserId)) { + $params['new_student_userid'] = $newUserId; + } + if (!empty($department)) { + $params['department'] = $department; + } + + return $this->httpPostJson('cgi-bin/school/user/update_student', $params); + } + + /** + * 批量创建学生,学生最多100个 + * @see https://work.weixin.qq.com/api/doc/90000/90135/92328 + * @param array $students 学生格式:[[student_userid:'','name':'','department':[1,2]],['student_userid':'','name':'','department':[1,2]]] + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function batchCreateStudents(array $students) + { + $params = [ + 'students' => $students + ]; + + return $this->httpPostJson('cgi-bin/school/user/batch_create_student', $params); + } + + /** + * 批量删除学生,每次最多100个学生 + * @see https://work.weixin.qq.com/api/doc/90000/90135/92329 + * @param array $useridList 学生USERID,格式:['zhangsan','lisi'] + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function batchDeleteStudents(array $useridList) + { + return $this->httpPostJson('cgi-bin/school/user/batch_delete_student', [ + 'useridlist' => $useridList + ]); + } + + /** + * 批量更新学生,每次最多100个 + * @see https://open.work.weixin.qq.com/api/doc/90001/90143/92042 + * @param array $students 格式:[['student_userid':'lisi','new_student_userid':'lisi2','name':'','department':[1,2]],.....] + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function batchUpdateStudents(array $students) + { + return $this->httpPostJson('cgi-bin/school/user/batch_update_student', [ + 'students' => $students + ]); + } + + /** + * 创建家长 + * @see https://open.work.weixin.qq.com/api/doc/90001/90143/92077 + * @param string $userId + * @param string $mobile + * @param bool $toInvite + * @param array $children + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function createParent(string $userId, string $mobile, bool $toInvite, array $children) + { + $params = [ + 'parent_userid' => $userId, + 'mobile' => $mobile, + 'to_invite' => $toInvite, + 'children' => $children + ]; + + return $this->httpPostJson('cgi-bin/school/user/create_parent', $params); + } + + /** + * 删除家长 + * @see https://open.work.weixin.qq.com/api/doc/90001/90143/92079 + * @param string $userId + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function deleteParent(string $userId) + { + return $this->httpPostJson('cgi-bin/school/user/delete_parent', [ + 'userid' => $userId + ]); + } + + /** + * 更新家长 + * @see https://open.work.weixin.qq.com/api/doc/90001/90143/92081 + * @param string $userId + * @param string $mobile + * @param string $newUserId + * @param array $children 格式:[['student_userid':'','relation':''],[]] + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function updateParent(string $userId, string $mobile, string $newUserId, array $children) + { + $params = [ + 'parent_userid' => $userId + ]; + if (!empty($newUserId)) { + $params['new_parent_userid'] = $newUserId; + } + if (!empty($mobile)) { + $params['mobile'] = $mobile; + } + if (!empty($children)) { + $params['children'] = $children; + } + + return $this->httpPostJson('cgi-bin/school/user/update_parent', $params); + } + + /** + * 批量创建家长 每次最多100个 + * @see https://open.work.weixin.qq.com/api/doc/90001/90143/92078 + * @param array $parents [['parent_userid':'','mobile':'','to_invite':true,'children':['student_userid':'','relation':'']],.....] + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function batchCreateParents(array $parents) + { + return $this->httpPostJson('cgi-bin/school/user/batch_create_parent', [ + 'parents' => $parents + ]); + } + + /** + * 批量删除家长,每次最多100个 + * @see https://open.work.weixin.qq.com/api/doc/90001/90143/92080 + * @param array $userIdList 格式:['chang','lisi'] + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function batchDeleteParents(array $userIdList) + { + return $this->httpPostJson('cgi-bin/school/user/batch_delete_parent', [ + 'useridlist' => $userIdList + ]); + } + + /** + * 批量更新家长,每次最多100个 + * @see https://open.work.weixin.qq.com/api/doc/90001/90143/92082 + * @param array $parents 格式:[['parent_userid':'','new_parent_userid':'','mobile':'','children':[['student_userid':'','relation':''],...]],.....] + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function batchUpdateParents(array $parents) + { + return $this->httpPostJson('cgi-bin/school/user/batch_update_parent', [ + 'parents' => $parents + ]); + } + + /** + * 读取学生或家长 + * @see https://open.work.weixin.qq.com/api/doc/90001/90143/92038 + * @param string $userId 学生或家长的userid + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getUser(string $userId) + { + return $this->httpGet('cgi-bin/school/user/get', [ + 'userid' => $userId + ]); + } + + /** + * 获取部门成员详情 + * @see https://open.work.weixin.qq.com/api/doc/90001/90143/92038 + * @param int $departmentId + * @param bool $fetchChild + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getStudents(int $departmentId, bool $fetchChild) + { + $params = [ + 'department_id' => $departmentId + ]; + if ($fetchChild) { + $params['fetch_child'] = 1; + } else { + $params['fetch_child'] = 0; + } + + return $this->httpGet('cgi-bin/school/user/list', $params); + } + + /** + * 获取学校通知二维码 + * @see https://open.work.weixin.qq.com/api/doc/90001/90143/92197 + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getSubscribeQrCode() + { + return $this->httpGet('cgi-bin/externalcontact/get_subscribe_qr_code'); + } + + /** + * 设置关注学校通知的模式 + * @see https://open.work.weixin.qq.com/api/doc/90001/90143/92290 + * @param int $mode 关注模式,1可扫码填写资料加入,2禁止扫码填写资料加入 + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function setSubscribeMode(int $mode) + { + return $this->httpPostJson('cgi-bin/externalcontact/set_subscribe_mode', [ + 'subscribe_mode' => $mode + ]); + } + + /** + * 获取关注学校通知的模式 + * @see https://open.work.weixin.qq.com/api/doc/90001/90143/92290 + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getSubscribeMode() + { + return $this->httpGet('cgi-bin/externalcontact/get_subscribe_mode'); + } + + /** + * 设置【老师可查看班级】的模式 + * @see https://open.work.weixin.qq.com/api/doc/90001/90143/92652 + * @param int $mode + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function setTeacherViewMode(int $mode) + { + return $this->httpPostJson('cgi-bin/school/set_teacher_view_mode', [ + 'view_mode' => $mode + ]); + } + + /** + * 获取【老师可查看班级】的模式 + * @see https://open.work.weixin.qq.com/api/doc/90001/90143/92652 + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getTeacherViewMode() + { + return $this->httpGet('cgi-bin/school/get_teacher_view_mode'); + } + + /** + * 外部联系人OPENID转换 + * @param string $userId + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function convertOpenid(string $userId) + { + return $this->httpGet('cgi-bin/externalcontact/convert_to_openid', [ + 'external_userid' => $userId + ]); + } + + /** + * 过滤数组中值为NULL的键 + * @param array $data + * @return array + */ + protected function filterNullValue(array $data) + { + $returnData = []; + foreach ($data as $key => $value) { + if ($value !== null) { + $returnData[$key] = trim($value); + } + } + + return $returnData; + } +} diff --git a/vendor/overtrue/wechat/src/Work/ExternalContact/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/ExternalContact/ServiceProvider.php new file mode 100644 index 0000000..25d2f8e --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/ExternalContact/ServiceProvider.php @@ -0,0 +1,49 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\ExternalContact; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['external_contact'] = function ($app) { + return new Client($app); + }; + + $app['contact_way'] = function ($app) { + return new ContactWayClient($app); + }; + + $app['external_contact_statistics'] = function ($app) { + return new StatisticsClient($app); + }; + + $app['external_contact_message'] = function ($app) { + return new MessageClient($app); + }; + + $app['school'] = function ($app) { + return new SchoolClient($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/ExternalContact/StatisticsClient.php b/vendor/overtrue/wechat/src/Work/ExternalContact/StatisticsClient.php new file mode 100644 index 0000000..cb19da7 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/ExternalContact/StatisticsClient.php @@ -0,0 +1,43 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\ExternalContact; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class StatisticsClient. + * + * @author milkmeowo + */ +class StatisticsClient extends BaseClient +{ + /** + * 获取员工行为数据. + * + * @see https://work.weixin.qq.com/api/doc#90000/90135/91580 + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function userBehavior(array $userIds, string $from, string $to) + { + $params = [ + 'userid' => $userIds, + 'start_time' => $from, + 'end_time' => $to, + ]; + + return $this->httpPostJson('cgi-bin/externalcontact/get_user_behavior_data', $params); + } +} diff --git a/vendor/overtrue/wechat/src/Work/GroupRobot/Client.php b/vendor/overtrue/wechat/src/Work/GroupRobot/Client.php new file mode 100644 index 0000000..3b3df8a --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/GroupRobot/Client.php @@ -0,0 +1,48 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\GroupRobot; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Work\GroupRobot\Messages\Message; + +/** + * Class Client. + * + * @author her-cat + */ +class Client extends BaseClient +{ + /** + * @param string|Message $message + * + * @return Messenger + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function message($message) + { + return (new Messenger($this))->message($message); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function send(string $key, array $message) + { + $this->accessToken = null; + + return $this->httpPostJson('cgi-bin/webhook/send', $message, ['key' => $key]); + } +} diff --git a/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/Image.php b/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/Image.php new file mode 100644 index 0000000..8ba69b0 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/Image.php @@ -0,0 +1,38 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\GroupRobot\Messages; + +/** + * Class Image. + * + * @author her-cat + */ +class Image extends Message +{ + /** + * @var string + */ + protected $type = 'image'; + + /** + * @var array + */ + protected $properties = ['base64', 'md5']; + + /** + * Image constructor. + */ + public function __construct(string $base64, string $md5) + { + parent::__construct(compact('base64', 'md5')); + } +} diff --git a/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/Markdown.php b/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/Markdown.php new file mode 100644 index 0000000..f26ac6f --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/Markdown.php @@ -0,0 +1,38 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\GroupRobot\Messages; + +/** + * Class Markdown. + * + * @author her-cat + */ +class Markdown extends Message +{ + /** + * @var string + */ + protected $type = 'markdown'; + + /** + * @var array + */ + protected $properties = ['content']; + + /** + * Markdown constructor. + */ + public function __construct(string $content) + { + parent::__construct(compact('content')); + } +} diff --git a/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/Message.php b/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/Message.php new file mode 100644 index 0000000..50201b9 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/Message.php @@ -0,0 +1,23 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\GroupRobot\Messages; + +use EasyWeChat\Kernel\Messages\Message as BaseMessage; + +/** + * Class Message. + * + * @author her-cat + */ +class Message extends BaseMessage +{ +} diff --git a/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/News.php b/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/News.php new file mode 100644 index 0000000..eeb37f0 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/News.php @@ -0,0 +1,47 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\GroupRobot\Messages; + +/** + * Class News. + * + * @author her-cat + */ +class News extends Message +{ + /** + * @var string + */ + protected $type = 'news'; + + /** + * @var array + */ + protected $properties = ['items']; + + /** + * News constructor. + */ + public function __construct(array $items = []) + { + parent::__construct(compact('items')); + } + + public function propertiesToArray(array $data, array $aliases = []): array + { + return ['articles' => array_map(function ($item) { + if ($item instanceof NewsItem) { + return $item->toJsonArray(); + } + }, $this->get('items'))]; + } +} diff --git a/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/NewsItem.php b/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/NewsItem.php new file mode 100644 index 0000000..2d206de --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/NewsItem.php @@ -0,0 +1,45 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\GroupRobot\Messages; + +/** + * Class NewsItem. + * + * @author her-cat + */ +class NewsItem extends Message +{ + /** + * @var string + */ + protected $type = 'news'; + + /** + * @var array + */ + protected $properties = [ + 'title', + 'description', + 'url', + 'image', + ]; + + public function toJsonArray() + { + return [ + 'title' => $this->get('title'), + 'description' => $this->get('description'), + 'url' => $this->get('url'), + 'picurl' => $this->get('image'), + ]; + } +} diff --git a/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/Text.php b/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/Text.php new file mode 100644 index 0000000..db7c2f8 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/GroupRobot/Messages/Text.php @@ -0,0 +1,69 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\GroupRobot\Messages; + +/** + * Class Text. + * + * @author her-cat + */ +class Text extends Message +{ + /** + * @var string + */ + protected $type = 'text'; + + /** + * @var array + */ + protected $properties = ['content', 'mentioned_list', 'mentioned_mobile_list']; + + /** + * Text constructor. + * + * @param string|array $userIds + * @param string|array $mobiles + */ + public function __construct(string $content, $userIds = [], $mobiles = []) + { + parent::__construct([ + 'content' => $content, + 'mentioned_list' => (array) $userIds, + 'mentioned_mobile_list' => (array) $mobiles, + ]); + } + + /** + * @param array $userIds + * + * @return Text + */ + public function mention($userIds) + { + $this->set('mentioned_list', (array) $userIds); + + return $this; + } + + /** + * @param array $mobiles + * + * @return Text + */ + public function mentionByMobile($mobiles) + { + $this->set('mentioned_mobile_list', (array) $mobiles); + + return $this; + } +} diff --git a/vendor/overtrue/wechat/src/Work/GroupRobot/Messenger.php b/vendor/overtrue/wechat/src/Work/GroupRobot/Messenger.php new file mode 100644 index 0000000..5a4e9c5 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/GroupRobot/Messenger.php @@ -0,0 +1,125 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\GroupRobot; + +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\Exceptions\InvalidConfigException; +use EasyWeChat\Kernel\Exceptions\RuntimeException; +use EasyWeChat\Work\GroupRobot\Messages\Message; +use EasyWeChat\Work\GroupRobot\Messages\Text; + +/** + * Class Messenger. + * + * @author her-cat + */ +class Messenger +{ + /** + * @var Client + */ + protected $client; + + /** + * @var Message|null + */ + protected $message; + + /** + * @var string|null + */ + protected $groupKey; + + /** + * Messenger constructor. + */ + public function __construct(Client $client) + { + $this->client = $client; + } + + /** + * @param string|Message $message + * + * @return Messenger + * + * @throws InvalidArgumentException + */ + public function message($message) + { + if (is_string($message) || is_numeric($message)) { + $message = new Text($message); + } + + if (!($message instanceof Message)) { + throw new InvalidArgumentException('Invalid message.'); + } + + $this->message = $message; + + return $this; + } + + /** + * @return Messenger + */ + public function toGroup(string $groupKey) + { + $this->groupKey = $groupKey; + + return $this; + } + + /** + * @param string|Message|null $message + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws RuntimeException + * @throws InvalidArgumentException + * @throws InvalidConfigException + */ + public function send($message = null) + { + if ($message) { + $this->message($message); + } + + if (empty($this->message)) { + throw new RuntimeException('No message to send.'); + } + + if (is_null($this->groupKey)) { + throw new RuntimeException('No group key specified.'); + } + + $message = $this->message->transformForJsonRequest(); + + return $this->client->send($this->groupKey, $message); + } + + /** + * @param string $property + * + * @return mixed + * + * @throws InvalidArgumentException + */ + public function __get($property) + { + if (property_exists($this, $property)) { + return $this->$property; + } + + throw new InvalidArgumentException(sprintf('No property named "%s"', $property)); + } +} diff --git a/vendor/overtrue/wechat/src/Work/GroupRobot/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/GroupRobot/ServiceProvider.php new file mode 100644 index 0000000..d72d630 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/GroupRobot/ServiceProvider.php @@ -0,0 +1,37 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\GroupRobot; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author her-cat + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc} + */ + public function register(Container $app) + { + $app['group_robot'] = function ($app) { + return new Client($app); + }; + + $app['group_robot_messenger'] = function ($app) { + return new Messenger($app['group_robot']); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/Invoice/Client.php b/vendor/overtrue/wechat/src/Work/Invoice/Client.php new file mode 100644 index 0000000..8dbc8b9 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Invoice/Client.php @@ -0,0 +1,87 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Invoice; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(string $cardId, string $encryptCode) + { + $params = [ + 'card_id' => $cardId, + 'encrypt_code' => $encryptCode, + ]; + + return $this->httpPostJson('cgi-bin/card/invoice/reimburse/getinvoiceinfo', $params); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function select(array $invoices) + { + $params = [ + 'item_list' => $invoices, + ]; + + return $this->httpPostJson('cgi-bin/card/invoice/reimburse/getinvoiceinfobatch', $params); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(string $cardId, string $encryptCode, string $status) + { + $params = [ + 'card_id' => $cardId, + 'encrypt_code' => $encryptCode, + 'reimburse_status' => $status, + ]; + + return $this->httpPostJson('cgi-bin/card/invoice/reimburse/updateinvoicestatus', $params); + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function batchUpdate(array $invoices, string $openid, string $status) + { + $params = [ + 'openid' => $openid, + 'reimburse_status' => $status, + 'invoice_list' => $invoices, + ]; + + return $this->httpPostJson('cgi-bin/card/invoice/reimburse/updatestatusbatch', $params); + } +} diff --git a/vendor/overtrue/wechat/src/Work/Invoice/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/Invoice/ServiceProvider.php new file mode 100644 index 0000000..3259d80 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Invoice/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Invoice; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['invoice'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/Jssdk/Client.php b/vendor/overtrue/wechat/src/Work/Jssdk/Client.php new file mode 100644 index 0000000..d65bbe2 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Jssdk/Client.php @@ -0,0 +1,186 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Jssdk; + +use EasyWeChat\BasicService\Jssdk\Client as BaseClient; +use EasyWeChat\Kernel\Exceptions\RuntimeException; +use EasyWeChat\Kernel\Support; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + protected $ticketEndpoint = 'cgi-bin/get_jsapi_ticket'; + + /** + * @return string + */ + protected function getAppId() + { + return $this->app['config']->get('corp_id'); + } + + /** + * @return string + */ + protected function getAgentId() + { + return $this->app['config']->get('agent_id'); + } + + /** + * @param array $apis + * @param $agentId + * @param bool $debug + * @param bool $beta + * @param array $openTagList + * @param string|null $url + * + * @return array|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function getAgentConfigArray(array $apis, $agentId, bool $debug = false, bool $beta = false, array $openTagList = [], string $url = null) + { + return $this->buildAgentConfig($apis, $agentId, $debug, $beta, false, $openTagList, $url); + } + + /** + * @param array $jsApiList + * @param $agentId + * @param bool $debug + * @param bool $beta + * @param bool $json + * @param array $openTagList + * @param string|null $url + * + * @return array|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function buildAgentConfig(array $jsApiList, $agentId, bool $debug = false, bool $beta = false, bool $json = true, array $openTagList = [], string $url = null) + { + $config = array_merge(compact('debug', 'beta', 'jsApiList', 'openTagList'), $this->agentConfigSignature($agentId, $url)); + + return $json ? json_encode($config) : $config; + } + + /** + * @param $agentId + * @param string|null $url + * @param string|null $nonce + * @param null $timestamp + * + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + protected function agentConfigSignature($agentId, string $url = null, string $nonce = null, $timestamp = null): array + { + $url = $url ?: $this->getUrl(); + $nonce = $nonce ?: Support\Str::quickRandom(10); + $timestamp = $timestamp ?: time(); + + return [ + 'corpid' => $this->getAppId(), + 'agentid' => $agentId, + 'nonceStr' => $nonce, + 'timestamp' => $timestamp, + 'url' => $url, + 'signature' => $this->getTicketSignature($this->getAgentTicket()['ticket'], $nonce, $timestamp, $url), + ]; + } + + /** + * Get js ticket. + * + * @param bool $refresh + * @param string $type + * + * @return array + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function getTicket(bool $refresh = false, string $type = 'config'): array + { + $cacheKey = sprintf('easywechat.work.jssdk.ticket.%s.%s', $type, $this->getAppId()); + + if (!$refresh && $this->getCache()->has($cacheKey)) { + return $this->getCache()->get($cacheKey); + } + + /** @var array $result */ + $result = $this->castResponseToType( + $this->requestRaw($this->ticketEndpoint, 'GET'), + 'array' + ); + + $this->getCache()->set($cacheKey, $result, $result['expires_in'] - 500); + + if (!$this->getCache()->has($cacheKey)) { + throw new RuntimeException('Failed to cache jssdk ticket.'); + } + + return $result; + } + + /** + * @return array|\EasyWeChat\Kernel\Support\Collection|mixed|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws RuntimeException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function getAgentTicket(bool $refresh = false, string $type = 'agent_config') + { + $cacheKey = sprintf('easywechat.work.jssdk.ticket.%s.%s.%s', $type, $this->getAppId(), $this->getAgentId()); + + if (!$refresh && $this->getCache()->has($cacheKey)) { + return $this->getCache()->get($cacheKey); + } + + /** @var array $result */ + $result = $this->castResponseToType( + $this->requestRaw('cgi-bin/ticket/get', 'GET', ['query' => ['type' => $type]]), + 'array' + ); + + $this->getCache()->set($cacheKey, $result, $result['expires_in'] - 500); + + if (!$this->getCache()->has($cacheKey)) { + throw new RuntimeException('Failed to cache jssdk ticket.'); + } + + return $result; + } +} diff --git a/vendor/overtrue/wechat/src/Work/Jssdk/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/Jssdk/ServiceProvider.php new file mode 100644 index 0000000..efb509c --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Jssdk/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Jssdk; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['jssdk'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/Media/Client.php b/vendor/overtrue/wechat/src/Work/Media/Client.php new file mode 100644 index 0000000..285a11a --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Media/Client.php @@ -0,0 +1,103 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Media; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Http\StreamResponse; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * Get media. + * + * @return array|\EasyWeChat\Kernel\Http\Response|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get(string $mediaId) + { + $response = $this->requestRaw('cgi-bin/media/get', 'GET', [ + 'query' => [ + 'media_id' => $mediaId, + ], + ]); + + if (false !== stripos($response->getHeaderLine('Content-Type'), 'text/plain')) { + return $this->castResponseToType($response, $this->app['config']->get('response_type')); + } + + return StreamResponse::buildFromPsrResponse($response); + } + + /** + * Upload Image. + * + * @return mixed + */ + public function uploadImage(string $path) + { + return $this->upload('image', $path); + } + + /** + * Upload Voice. + * + * @return mixed + */ + public function uploadVoice(string $path) + { + return $this->upload('voice', $path); + } + + /** + * Upload Video. + * + * @return mixed + */ + public function uploadVideo(string $path) + { + return $this->upload('video', $path); + } + + /** + * Upload File. + * + * @return mixed + */ + public function uploadFile(string $path) + { + return $this->upload('file', $path); + } + + /** + * Upload media. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function upload(string $type, string $path) + { + $files = [ + 'media' => $path, + ]; + + return $this->httpUpload('cgi-bin/media/upload', $files, [], compact('type')); + } +} diff --git a/vendor/overtrue/wechat/src/Work/Media/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/Media/ServiceProvider.php new file mode 100644 index 0000000..450d156 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Media/ServiceProvider.php @@ -0,0 +1,28 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Media; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['media'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/Menu/Client.php b/vendor/overtrue/wechat/src/Work/Menu/Client.php new file mode 100644 index 0000000..bdb8850 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Menu/Client.php @@ -0,0 +1,59 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Menu; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * Get menu. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function get() + { + return $this->httpGet('cgi-bin/menu/get', ['agentid' => $this->app['config']['agent_id']]); + } + + /** + * Create menu for the given agent. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create(array $data) + { + return $this->httpPostJson('cgi-bin/menu/create', $data, ['agentid' => $this->app['config']['agent_id']]); + } + + /** + * Delete menu. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function delete() + { + return $this->httpGet('cgi-bin/menu/delete', ['agentid' => $this->app['config']['agent_id']]); + } +} diff --git a/vendor/overtrue/wechat/src/Work/Menu/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/Menu/ServiceProvider.php new file mode 100644 index 0000000..02c4290 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Menu/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Menu; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['menu'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/Message/Client.php b/vendor/overtrue/wechat/src/Work/Message/Client.php new file mode 100644 index 0000000..8de4f3d --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Message/Client.php @@ -0,0 +1,59 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Message; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Messages\Message; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * @param string|\EasyWeChat\Kernel\Messages\Message $message + * + * @return \EasyWeChat\Work\Message\Messenger + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function message($message) + { + return (new Messenger($this))->message($message); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function send(array $message) + { + return $this->httpPostJson('cgi-bin/message/send', $message); + } + + /** + * @param string $msgid + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function recall(string $msgid) + { + return $this->httpPostJson('cgi-bin/message/recall', ['msgid' => $msgid]); + } +} diff --git a/vendor/overtrue/wechat/src/Work/Message/Messenger.php b/vendor/overtrue/wechat/src/Work/Message/Messenger.php new file mode 100644 index 0000000..aa2d215 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Message/Messenger.php @@ -0,0 +1,217 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Message; + +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\Exceptions\RuntimeException; +use EasyWeChat\Kernel\Messages\Message; +use EasyWeChat\Kernel\Messages\Text; + +/** + * Class MessageBuilder. + * + * @author overtrue + */ +class Messenger +{ + /** + * @var \EasyWeChat\Kernel\Messages\Message; + */ + protected $message; + + /** + * @var array + */ + protected $to = ['touser' => '@all']; + + /** + * @var int + */ + protected $agentId; + + /** + * @var bool + */ + protected $secretive = false; + + /** + * @var \EasyWeChat\Work\Message\Client + */ + protected $client; + + /** + * MessageBuilder constructor. + * + * @param \EasyWeChat\Work\Message\Client $client + */ + public function __construct(Client $client) + { + $this->client = $client; + } + + /** + * Set message to send. + * + * @param string|Message $message + * + * @return \EasyWeChat\Work\Message\Messenger + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function message($message) + { + if (is_string($message) || is_numeric($message)) { + $message = new Text($message); + } + + if (!($message instanceof Message)) { + throw new InvalidArgumentException('Invalid message.'); + } + + $this->message = $message; + + return $this; + } + + /** + * @return \EasyWeChat\Work\Message\Messenger + */ + public function ofAgent(int $agentId) + { + $this->agentId = $agentId; + + return $this; + } + + /** + * @param array|string $userIds + * + * @return \EasyWeChat\Work\Message\Messenger + */ + public function toUser($userIds) + { + return $this->setRecipients($userIds, 'touser'); + } + + /** + * @param array|string $partyIds + * + * @return \EasyWeChat\Work\Message\Messenger + */ + public function toParty($partyIds) + { + return $this->setRecipients($partyIds, 'toparty'); + } + + /** + * @param array|string $tagIds + * + * @return \EasyWeChat\Work\Message\Messenger + */ + public function toTag($tagIds) + { + return $this->setRecipients($tagIds, 'totag'); + } + + /** + * Keep secret. + * + * @return \EasyWeChat\Work\Message\Messenger + */ + public function secretive() + { + $this->secretive = true; + + return $this; + } + + /** + * @param array|string $ids + * + * @return \EasyWeChat\Work\Message\Messenger + */ + protected function setRecipients($ids, string $key): self + { + if (is_array($ids)) { + $ids = implode('|', $ids); + } + + $this->to = [$key => $ids]; + + return $this; + } + + /** + * @param \EasyWeChat\Kernel\Messages\Message|string|null $message + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + */ + public function send($message = null) + { + if ($message) { + $this->message($message); + } + + if (empty($this->message)) { + throw new RuntimeException('No message to send.'); + } + + if (is_null($this->agentId)) { + throw new RuntimeException('No agentid specified.'); + } + + $message = $this->message->transformForJsonRequest(array_merge([ + 'agentid' => $this->agentId, + 'safe' => intval($this->secretive), + ], $this->to)); + + $this->secretive = false; + + return $this->client->send($message); + } + + /** + * @param string $msgid + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws RuntimeException + */ + public function recall(string $msgid) + { + if (empty($msgid)) { + throw new RuntimeException('No msgid specified.'); + } + return $this->client->recall($msgid); + } + + /** + * Return property. + * + * @param string $property + * + * @return mixed + * + * @throws InvalidArgumentException + */ + public function __get($property) + { + if (property_exists($this, $property)) { + return $this->$property; + } + + throw new InvalidArgumentException(sprintf('No property named "%s"', $property)); + } +} diff --git a/vendor/overtrue/wechat/src/Work/Message/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/Message/ServiceProvider.php new file mode 100644 index 0000000..ea14f9e --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Message/ServiceProvider.php @@ -0,0 +1,43 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Message; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['message'] = function ($app) { + return new Client($app); + }; + + $app['messenger'] = function ($app) { + $messenger = new Messenger($app['message']); + + if (is_numeric($app['config']['agent_id'])) { + $messenger->ofAgent($app['config']['agent_id']); + } + + return $messenger; + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/MiniProgram/Application.php b/vendor/overtrue/wechat/src/Work/MiniProgram/Application.php new file mode 100644 index 0000000..abc425f --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/MiniProgram/Application.php @@ -0,0 +1,41 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\MiniProgram; + +use EasyWeChat\MiniProgram\Application as MiniProgram; +use EasyWeChat\Work\Auth\AccessToken; +use EasyWeChat\Work\MiniProgram\Auth\Client; + +/** + * Class Application. + * + * @author Caikeal + * + * @property \EasyWeChat\Work\MiniProgram\Auth\Client $auth + */ +class Application extends MiniProgram +{ + /** + * Application constructor. + */ + public function __construct(array $config = [], array $prepends = []) + { + parent::__construct($config, $prepends + [ + 'access_token' => function ($app) { + return new AccessToken($app); + }, + 'auth' => function ($app) { + return new Client($app); + }, + ]); + } +} diff --git a/vendor/overtrue/wechat/src/Work/MiniProgram/Auth/Client.php b/vendor/overtrue/wechat/src/Work/MiniProgram/Auth/Client.php new file mode 100644 index 0000000..71f79c4 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/MiniProgram/Auth/Client.php @@ -0,0 +1,37 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\MiniProgram\Auth; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + */ +class Client extends BaseClient +{ + /** + * Get session info by code. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function session(string $code) + { + $params = [ + 'js_code' => $code, + 'grant_type' => 'authorization_code', + ]; + + return $this->httpGet('cgi-bin/miniprogram/jscode2session', $params); + } +} diff --git a/vendor/overtrue/wechat/src/Work/MsgAudit/Client.php b/vendor/overtrue/wechat/src/Work/MsgAudit/Client.php new file mode 100644 index 0000000..4f4acca --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/MsgAudit/Client.php @@ -0,0 +1,82 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\MsgAudit; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author ZengJJ + */ +class Client extends BaseClient +{ + /** + * @param string|null $type + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getPermitUsers(string $type = null) + { + return $this->httpPostJson('cgi-bin/msgaudit/get_permit_user_list', (empty($type) ? [] : ['type' => $type])); + } + + /** + * @param array $info 数组,格式: [[userid, exteranalopenid], [userid, exteranalopenid]] + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getSingleAgreeStatus(array $info) + { + $params = [ + 'info' => $info + ]; + + return $this->httpPostJson('cgi-bin/msgaudit/check_single_agree', $params); + } + + /** + * @param string $roomid + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getRoomAgreeStatus(string $roomId) + { + $params = [ + 'roomid' => $roomId + ]; + + return $this->httpPostJson('cgi-bin/msgaudit/check_room_agree', $params); + } + + /** + * @param string $roomid + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getRoom(string $roomId) + { + $params = [ + 'roomid' => $roomId + ]; + + return $this->httpPostJson('cgi-bin/msgaudit/groupchat/get', $params); + } +} diff --git a/vendor/overtrue/wechat/src/Work/MsgAudit/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/MsgAudit/ServiceProvider.php new file mode 100644 index 0000000..e24c8b3 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/MsgAudit/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\MsgAudit; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author ZengJJ + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['msg_audit'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/OA/Client.php b/vendor/overtrue/wechat/src/Work/OA/Client.php new file mode 100644 index 0000000..c89f463 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/OA/Client.php @@ -0,0 +1,149 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\OA; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * Get the checkin data. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function checkinRecords(int $startTime, int $endTime, array $userList, int $type = 3) + { + $params = [ + 'opencheckindatatype' => $type, + 'starttime' => $startTime, + 'endtime' => $endTime, + 'useridlist' => $userList, + ]; + + return $this->httpPostJson('cgi-bin/checkin/getcheckindata', $params); + } + + /** + * Get the checkin rules. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function checkinRules(int $datetime, array $userList) + { + $params = [ + 'datetime' => $datetime, + 'useridlist' => $userList, + ]; + + return $this->httpPostJson('cgi-bin/checkin/getcheckinoption', $params); + } + + /** + * Get approval template details. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function approvalTemplate(string $templateId) + { + $params = [ + 'template_id' => $templateId, + ]; + + return $this->httpPostJson('cgi-bin/oa/gettemplatedetail', $params); + } + + /** + * Submit an application for approval. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function createApproval(array $data) + { + return $this->httpPostJson('cgi-bin/oa/applyevent', $data); + } + + /** + * Get Approval number. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function approvalNumbers(int $startTime, int $endTime, int $nextCursor = 0, int $size = 100, array $filters = []) + { + $params = [ + 'starttime' => $startTime, + 'endtime' => $endTime, + 'cursor' => $nextCursor, + 'size' => $size > 100 ? 100 : $size, + 'filters' => $filters, + ]; + + return $this->httpPostJson('cgi-bin/oa/getapprovalinfo', $params); + } + + /** + * Get approval detail. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function approvalDetail(int $number) + { + $params = [ + 'sp_no' => $number, + ]; + + return $this->httpPostJson('cgi-bin/oa/getapprovaldetail', $params); + } + + /** + * Get Approval Data. + * + * @param int $nextNumber + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function approvalRecords(int $startTime, int $endTime, int $nextNumber = null) + { + $params = [ + 'starttime' => $startTime, + 'endtime' => $endTime, + 'next_spnum' => $nextNumber, + ]; + + return $this->httpPostJson('cgi-bin/corp/getapprovaldata', $params); + } +} diff --git a/vendor/overtrue/wechat/src/Work/OA/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/OA/ServiceProvider.php new file mode 100644 index 0000000..5d389c2 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/OA/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\OA; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['oa'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/OAuth/AccessTokenDelegate.php b/vendor/overtrue/wechat/src/Work/OAuth/AccessTokenDelegate.php new file mode 100644 index 0000000..dc41b32 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/OAuth/AccessTokenDelegate.php @@ -0,0 +1,43 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\OAuth; + +use EasyWeChat\Work\Application; +use Overtrue\Socialite\AccessTokenInterface; + +/** + * Class AccessTokenDelegate. + * + * @author mingyoung + */ +class AccessTokenDelegate implements AccessTokenInterface +{ + /** + * @var \EasyWeChat\Work\Application + */ + protected $app; + + public function __construct(Application $app) + { + $this->app = $app; + } + + /** + * Return the access token string. + * + * @return string + */ + public function getToken() + { + return $this->app['access_token']->getToken()['access_token']; + } +} diff --git a/vendor/overtrue/wechat/src/Work/OAuth/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/OAuth/ServiceProvider.php new file mode 100644 index 0000000..b1fdfce --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/OAuth/ServiceProvider.php @@ -0,0 +1,62 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\OAuth; + +use Overtrue\Socialite\SocialiteManager; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ServiceProvider implements ServiceProviderInterface +{ + public function register(Container $app) + { + $app['oauth'] = function ($app) { + $socialite = (new SocialiteManager([ + 'wework' => [ + 'client_id' => $app['config']['corp_id'], + 'client_secret' => null, + 'redirect' => $this->prepareCallbackUrl($app), + ], + ], $app['request']))->driver('wework'); + + $scopes = (array) $app['config']->get('oauth.scopes', ['snsapi_base']); + + if (!empty($scopes)) { + $socialite->scopes($scopes); + } else { + $socialite->setAgentId($app['config']['agent_id']); + } + + return $socialite->setAccessToken(new AccessTokenDelegate($app)); + }; + } + + /** + * Prepare the OAuth callback url for wechat. + * + * @param Container $app + * + * @return string + */ + private function prepareCallbackUrl($app) + { + $callback = $app['config']->get('oauth.callback'); + + if (0 === stripos($callback, 'http')) { + return $callback; + } + + $baseUrl = $app['request']->getSchemeAndHttpHost(); + + return $baseUrl.'/'.ltrim($callback, '/'); + } +} diff --git a/vendor/overtrue/wechat/src/Work/Schedule/Client.php b/vendor/overtrue/wechat/src/Work/Schedule/Client.php new file mode 100644 index 0000000..b873a01 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Schedule/Client.php @@ -0,0 +1,93 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Schedule; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class Client. + * + * @author her-cat + */ +class Client extends BaseClient +{ + /** + * Add a schedule. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function add(array $schedule) + { + return $this->httpPostJson('cgi-bin/oa/schedule/add', compact('schedule')); + } + + /** + * Update the schedule. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(string $id, array $schedule) + { + $schedule += ['schedule_id' => $id]; + + return $this->httpPostJson('cgi-bin/oa/schedule/update', compact('schedule')); + } + + /** + * Get one or more schedules. + * + * @param string|array $ids + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function get($ids) + { + return $this->httpPostJson('cgi-bin/oa/schedule/get', ['schedule_id_list' => (array) $ids]); + } + + /** + * Get the list of schedules under a calendar. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getByCalendar(string $calendarId, int $offset = 0, int $limit = 500) + { + $data = compact('offset', 'limit') + ['cal_id' => $calendarId]; + + return $this->httpPostJson('cgi-bin/oa/schedule/get_by_calendar', $data); + } + + /** + * Delete a schedule. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete(string $id) + { + return $this->httpPostJson('cgi-bin/oa/schedule/del', ['schedule_id' => $id]); + } +} diff --git a/vendor/overtrue/wechat/src/Work/Schedule/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/Schedule/ServiceProvider.php new file mode 100644 index 0000000..9bdd17b --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Schedule/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Schedule; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author her-cat + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc} + */ + public function register(Container $app) + { + $app['schedule'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/Server/Guard.php b/vendor/overtrue/wechat/src/Work/Server/Guard.php new file mode 100644 index 0000000..aac3d8f --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Server/Guard.php @@ -0,0 +1,43 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Server; + +use EasyWeChat\Kernel\ServerGuard; + +/** + * Class Guard. + * + * @author overtrue + */ +class Guard extends ServerGuard +{ + /** + * @return $this + */ + public function validate() + { + return $this; + } + + /** + * Check the request message safe mode. + */ + protected function isSafeMode(): bool + { + return true; + } + + protected function shouldReturnRawResponse(): bool + { + return !is_null($this->app['request']->get('echostr')); + } +} diff --git a/vendor/overtrue/wechat/src/Work/Server/Handlers/EchoStrHandler.php b/vendor/overtrue/wechat/src/Work/Server/Handlers/EchoStrHandler.php new file mode 100644 index 0000000..6427ba5 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Server/Handlers/EchoStrHandler.php @@ -0,0 +1,56 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Server\Handlers; + +use EasyWeChat\Kernel\Contracts\EventHandlerInterface; +use EasyWeChat\Kernel\Decorators\FinallyResult; +use EasyWeChat\Kernel\ServiceContainer; + +/** + * Class EchoStrHandler. + * + * @author overtrue + */ +class EchoStrHandler implements EventHandlerInterface +{ + /** + * @var ServiceContainer + */ + protected $app; + + /** + * EchoStrHandler constructor. + */ + public function __construct(ServiceContainer $app) + { + $this->app = $app; + } + + /** + * @param mixed $payload + * + * @return FinallyResult|null + */ + public function handle($payload = null) + { + if ($decrypted = $this->app['request']->get('echostr')) { + $str = $this->app['encryptor']->decrypt( + $decrypted, + $this->app['request']->get('msg_signature'), + $this->app['request']->get('nonce'), + $this->app['request']->get('timestamp') + ); + + return new FinallyResult($str); + } + } +} diff --git a/vendor/overtrue/wechat/src/Work/Server/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/Server/ServiceProvider.php new file mode 100644 index 0000000..8a2fc3e --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/Server/ServiceProvider.php @@ -0,0 +1,46 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\Server; + +use EasyWeChat\Kernel\Encryptor; +use EasyWeChat\Work\Server\Handlers\EchoStrHandler; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author overtrue + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + !isset($app['encryptor']) && $app['encryptor'] = function ($app) { + return new Encryptor( + $app['config']['corp_id'], + $app['config']['token'], + $app['config']['aes_key'] + ); + }; + + !isset($app['server']) && $app['server'] = function ($app) { + $guard = new Guard($app); + $guard->push(new EchoStrHandler($app)); + + return $guard; + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/User/Client.php b/vendor/overtrue/wechat/src/Work/User/Client.php new file mode 100644 index 0000000..9141fc7 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/User/Client.php @@ -0,0 +1,223 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\User; + +use EasyWeChat\Kernel\BaseClient; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; + +/** + * Class Client. + * + * @author mingyoung + */ +class Client extends BaseClient +{ + /** + * Create a user. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create(array $data) + { + return $this->httpPostJson('cgi-bin/user/create', $data); + } + + /** + * Update an exist user. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(string $id, array $data) + { + return $this->httpPostJson('cgi-bin/user/update', array_merge(['userid' => $id], $data)); + } + + /** + * Delete a user. + * + * @param string|array $userId + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function delete($userId) + { + if (is_array($userId)) { + return $this->batchDelete($userId); + } + + return $this->httpGet('cgi-bin/user/delete', ['userid' => $userId]); + } + + /** + * Batch delete users. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function batchDelete(array $userIds) + { + return $this->httpPostJson('cgi-bin/user/batchdelete', ['useridlist' => $userIds]); + } + + /** + * Get user. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function get(string $userId) + { + return $this->httpGet('cgi-bin/user/get', ['userid' => $userId]); + } + + /** + * Get simple user list. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getDepartmentUsers(int $departmentId, bool $fetchChild = false) + { + $params = [ + 'department_id' => $departmentId, + 'fetch_child' => (int) $fetchChild, + ]; + + return $this->httpGet('cgi-bin/user/simplelist', $params); + } + + /** + * Get user list. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getDetailedDepartmentUsers(int $departmentId, bool $fetchChild = false) + { + $params = [ + 'department_id' => $departmentId, + 'fetch_child' => (int) $fetchChild, + ]; + + return $this->httpGet('cgi-bin/user/list', $params); + } + + /** + * Convert userId to openid. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function userIdToOpenid(string $userId, int $agentId = null) + { + $params = [ + 'userid' => $userId, + 'agentid' => $agentId, + ]; + + return $this->httpPostJson('cgi-bin/user/convert_to_openid', $params); + } + + /** + * Convert openid to userId. + * + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function openidToUserId(string $openid) + { + $params = [ + 'openid' => $openid, + ]; + + return $this->httpPostJson('cgi-bin/user/convert_to_userid', $params); + } + + /** + * Convert mobile to userId. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function mobileToUserId(string $mobile) + { + $params = [ + 'mobile' => $mobile, + ]; + + return $this->httpPostJson('cgi-bin/user/getuserid', $params); + } + + /** + * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function accept(string $userId) + { + $params = [ + 'userid' => $userId, + ]; + + return $this->httpGet('cgi-bin/user/authsucc', $params); + } + + /** + * Batch invite users. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function invite(array $params) + { + return $this->httpPostJson('cgi-bin/batch/invite', $params); + } + + /** + * Get invitation QR code. + * + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function getInvitationQrCode(int $sizeType = 1) + { + if (!\in_array($sizeType, [1, 2, 3, 4], true)) { + throw new InvalidArgumentException('The sizeType must be 1, 2, 3, 4.'); + } + + return $this->httpGet('cgi-bin/corp/get_join_qrcode', ['size_type' => $sizeType]); + } +} diff --git a/vendor/overtrue/wechat/src/Work/User/ServiceProvider.php b/vendor/overtrue/wechat/src/Work/User/ServiceProvider.php new file mode 100644 index 0000000..fcb5f10 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/User/ServiceProvider.php @@ -0,0 +1,37 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\User; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['user'] = function ($app) { + return new Client($app); + }; + + $app['tag'] = function ($app) { + return new TagClient($app); + }; + } +} diff --git a/vendor/overtrue/wechat/src/Work/User/TagClient.php b/vendor/overtrue/wechat/src/Work/User/TagClient.php new file mode 100644 index 0000000..c8d31f9 --- /dev/null +++ b/vendor/overtrue/wechat/src/Work/User/TagClient.php @@ -0,0 +1,151 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyWeChat\Work\User; + +use EasyWeChat\Kernel\BaseClient; + +/** + * Class TagClient. + * + * @author mingyoung + */ +class TagClient extends BaseClient +{ + /** + * Create tag. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function create(string $tagName, int $tagId = null) + { + $params = [ + 'tagname' => $tagName, + 'tagid' => $tagId, + ]; + + return $this->httpPostJson('cgi-bin/tag/create', $params); + } + + /** + * Update tag. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function update(int $tagId, string $tagName) + { + $params = [ + 'tagid' => $tagId, + 'tagname' => $tagName, + ]; + + return $this->httpPostJson('cgi-bin/tag/update', $params); + } + + /** + * Delete tag. + * + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function delete(int $tagId) + { + return $this->httpGet('cgi-bin/tag/delete', ['tagid' => $tagId]); + } + + /** + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function get(int $tagId) + { + return $this->httpGet('cgi-bin/tag/get', ['tagid' => $tagId]); + } + + /** + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function tagUsers(int $tagId, array $userList = []) + { + return $this->tagOrUntagUsers('cgi-bin/tag/addtagusers', $tagId, $userList); + } + + /** + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function tagDepartments(int $tagId, array $partyList = []) + { + return $this->tagOrUntagUsers('cgi-bin/tag/addtagusers', $tagId, [], $partyList); + } + + /** + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function untagUsers(int $tagId, array $userList = []) + { + return $this->tagOrUntagUsers('cgi-bin/tag/deltagusers', $tagId, $userList); + } + + /** + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function untagDepartments(int $tagId, array $partyList = []) + { + return $this->tagOrUntagUsers('cgi-bin/tag/deltagusers', $tagId, [], $partyList); + } + + /** + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function tagOrUntagUsers(string $endpoint, int $tagId, array $userList = [], array $partyList = []) + { + $data = [ + 'tagid' => $tagId, + 'userlist' => $userList, + 'partylist' => $partyList, + ]; + + return $this->httpPostJson($endpoint, $data); + } + + /** + * @return mixed + * + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + */ + public function list() + { + return $this->httpGet('cgi-bin/tag/list'); + } +} diff --git a/vendor/pimple/pimple/.github/workflows/tests.yml b/vendor/pimple/pimple/.github/workflows/tests.yml new file mode 100644 index 0000000..09b6760 --- /dev/null +++ b/vendor/pimple/pimple/.github/workflows/tests.yml @@ -0,0 +1,47 @@ +name: "Tests" + +on: + - pull_request + - push + +jobs: + test: + name: PHP ${{ matrix.php }} - ${{ matrix.dependencies }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: + - "7.2" + - "7.3" + - "7.4" + - "8.0" + - "8.1" + dependencies: + - "psr/container:^1.1" + - "psr/container:^2.0" + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + ini-values: display_errors=off, log_errors=on + extensions: :xdebug + env: + # https://github.com/shivammathur/setup-php/issues/407#issuecomment-773675741 + fail-fast: true + + - name: Validate composer.json + run: composer validate --strict --no-check-lock + + - name: Install dependencies +${{ matrix.dependencies }} + run: | + composer require --no-update ${{ matrix.dependencies }} + composer update --prefer-dist --no-progress + + - name: Run PHPUnit tests + run: vendor/bin/simple-phpunit --verbose diff --git a/vendor/pimple/pimple/.gitignore b/vendor/pimple/pimple/.gitignore new file mode 100644 index 0000000..a5c7ed6 --- /dev/null +++ b/vendor/pimple/pimple/.gitignore @@ -0,0 +1,4 @@ +phpunit.xml +.phpunit.result.cache +composer.lock +/vendor/ diff --git a/vendor/pimple/pimple/.php_cs.dist b/vendor/pimple/pimple/.php_cs.dist new file mode 100644 index 0000000..2787bb4 --- /dev/null +++ b/vendor/pimple/pimple/.php_cs.dist @@ -0,0 +1,20 @@ +setRules([ + '@Symfony' => true, + '@Symfony:risky' => true, + '@PHPUnit75Migration:risky' => true, + 'php_unit_dedicate_assert' => true, + 'array_syntax' => ['syntax' => 'short'], + 'php_unit_fqcn_annotation' => true, + 'no_unreachable_default_argument_value' => false, + 'braces' => ['allow_single_line_closure' => true], + 'heredoc_to_nowdoc' => false, + 'ordered_imports' => true, + 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], + 'native_function_invocation' => ['include' => ['@compiler_optimized'], 'scope' => 'all'], + ]) + ->setRiskyAllowed(true) + ->setFinder(PhpCsFixer\Finder::create()->in(__DIR__.'/src')) +; diff --git a/vendor/pimple/pimple/CHANGELOG b/vendor/pimple/pimple/CHANGELOG new file mode 100644 index 0000000..08059e5 --- /dev/null +++ b/vendor/pimple/pimple/CHANGELOG @@ -0,0 +1,72 @@ +* 3.4.0 (2021-03-06) + + * Implement version 1.1 of PSR-11 + +* 3.3.1 (2020-11-24) + + * Add support for PHP 8 + +* 3.3.0 (2020-03-03) + + * Drop PHP extension + * Bump min PHP version to 7.2.5 + +* 3.2.3 (2018-01-21) + + * prefixed all function calls with \ for extra speed + +* 3.2.2 (2017-07-23) + + * reverted extending a protected closure throws an exception (deprecated it instead) + +* 3.2.1 (2017-07-17) + + * fixed PHP error + +* 3.2.0 (2017-07-17) + + * added a PSR-11 service locator + * added a PSR-11 wrapper + * added ServiceIterator + * fixed extending a protected closure (now throws InvalidServiceIdentifierException) + +* 3.1.0 (2017-07-03) + + * deprecated the C extension + * added support for PSR-11 exceptions + +* 3.0.2 (2015-09-11) + + * refactored the C extension + * minor non-significant changes + +* 3.0.1 (2015-07-30) + + * simplified some code + * fixed a segfault in the C extension + +* 3.0.0 (2014-07-24) + + * removed the Pimple class alias (use Pimple\Container instead) + +* 2.1.1 (2014-07-24) + + * fixed compiler warnings for the C extension + * fixed code when dealing with circular references + +* 2.1.0 (2014-06-24) + + * moved the Pimple to Pimple\Container (with a BC layer -- Pimple is now a + deprecated alias which will be removed in Pimple 3.0) + * added Pimple\ServiceProviderInterface (and Pimple::register()) + +* 2.0.0 (2014-02-10) + + * changed extend to automatically re-assign the extended service and keep it as shared or factory + (to keep BC, extend still returns the extended service) + * changed services to be shared by default (use factory() for factory + services) + +* 1.0.0 + + * initial version diff --git a/vendor/pimple/pimple/LICENSE b/vendor/pimple/pimple/LICENSE new file mode 100644 index 0000000..3e2a9e1 --- /dev/null +++ b/vendor/pimple/pimple/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2009-2020 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/pimple/pimple/README.rst b/vendor/pimple/pimple/README.rst new file mode 100644 index 0000000..7081839 --- /dev/null +++ b/vendor/pimple/pimple/README.rst @@ -0,0 +1,332 @@ +Pimple +====== + +.. caution:: + + Pimple is now closed for changes. No new features will be added and no + cosmetic changes will be accepted either. The only accepted changes are + compatibility with newer PHP versions and security issue fixes. + +.. caution:: + + This is the documentation for Pimple 3.x. If you are using Pimple 1.x, read + the `Pimple 1.x documentation`_. Reading the Pimple 1.x code is also a good + way to learn more about how to create a simple Dependency Injection + Container (recent versions of Pimple are more focused on performance). + +Pimple is a small Dependency Injection Container for PHP. + +Installation +------------ + +Before using Pimple in your project, add it to your ``composer.json`` file: + +.. code-block:: bash + + $ ./composer.phar require pimple/pimple "^3.0" + +Usage +----- + +Creating a container is a matter of creating a ``Container`` instance: + +.. code-block:: php + + use Pimple\Container; + + $container = new Container(); + +As many other dependency injection containers, Pimple manages two different +kind of data: **services** and **parameters**. + +Defining Services +~~~~~~~~~~~~~~~~~ + +A service is an object that does something as part of a larger system. Examples +of services: a database connection, a templating engine, or a mailer. Almost +any **global** object can be a service. + +Services are defined by **anonymous functions** that return an instance of an +object: + +.. code-block:: php + + // define some services + $container['session_storage'] = function ($c) { + return new SessionStorage('SESSION_ID'); + }; + + $container['session'] = function ($c) { + return new Session($c['session_storage']); + }; + +Notice that the anonymous function has access to the current container +instance, allowing references to other services or parameters. + +As objects are only created when you get them, the order of the definitions +does not matter. + +Using the defined services is also very easy: + +.. code-block:: php + + // get the session object + $session = $container['session']; + + // the above call is roughly equivalent to the following code: + // $storage = new SessionStorage('SESSION_ID'); + // $session = new Session($storage); + +Defining Factory Services +~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, each time you get a service, Pimple returns the **same instance** +of it. If you want a different instance to be returned for all calls, wrap your +anonymous function with the ``factory()`` method + +.. code-block:: php + + $container['session'] = $container->factory(function ($c) { + return new Session($c['session_storage']); + }); + +Now, each call to ``$container['session']`` returns a new instance of the +session. + +Defining Parameters +~~~~~~~~~~~~~~~~~~~ + +Defining a parameter allows to ease the configuration of your container from +the outside and to store global values: + +.. code-block:: php + + // define some parameters + $container['cookie_name'] = 'SESSION_ID'; + $container['session_storage_class'] = 'SessionStorage'; + +If you change the ``session_storage`` service definition like below: + +.. code-block:: php + + $container['session_storage'] = function ($c) { + return new $c['session_storage_class']($c['cookie_name']); + }; + +You can now easily change the cookie name by overriding the +``cookie_name`` parameter instead of redefining the service +definition. + +Protecting Parameters +~~~~~~~~~~~~~~~~~~~~~ + +Because Pimple sees anonymous functions as service definitions, you need to +wrap anonymous functions with the ``protect()`` method to store them as +parameters: + +.. code-block:: php + + $container['random_func'] = $container->protect(function () { + return rand(); + }); + +Modifying Services after Definition +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In some cases you may want to modify a service definition after it has been +defined. You can use the ``extend()`` method to define additional code to be +run on your service just after it is created: + +.. code-block:: php + + $container['session_storage'] = function ($c) { + return new $c['session_storage_class']($c['cookie_name']); + }; + + $container->extend('session_storage', function ($storage, $c) { + $storage->...(); + + return $storage; + }); + +The first argument is the name of the service to extend, the second a function +that gets access to the object instance and the container. + +Extending a Container +~~~~~~~~~~~~~~~~~~~~~ + +If you use the same libraries over and over, you might want to reuse some +services from one project to the next one; package your services into a +**provider** by implementing ``Pimple\ServiceProviderInterface``: + +.. code-block:: php + + use Pimple\Container; + + class FooProvider implements Pimple\ServiceProviderInterface + { + public function register(Container $pimple) + { + // register some services and parameters + // on $pimple + } + } + +Then, register the provider on a Container: + +.. code-block:: php + + $pimple->register(new FooProvider()); + +Fetching the Service Creation Function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When you access an object, Pimple automatically calls the anonymous function +that you defined, which creates the service object for you. If you want to get +raw access to this function, you can use the ``raw()`` method: + +.. code-block:: php + + $container['session'] = function ($c) { + return new Session($c['session_storage']); + }; + + $sessionFunction = $container->raw('session'); + +PSR-11 compatibility +-------------------- + +For historical reasons, the ``Container`` class does not implement the PSR-11 +``ContainerInterface``. However, Pimple provides a helper class that will let +you decouple your code from the Pimple container class. + +The PSR-11 container class +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``Pimple\Psr11\Container`` class lets you access the content of an +underlying Pimple container using ``Psr\Container\ContainerInterface`` +methods: + +.. code-block:: php + + use Pimple\Container; + use Pimple\Psr11\Container as PsrContainer; + + $container = new Container(); + $container['service'] = function ($c) { + return new Service(); + }; + $psr11 = new PsrContainer($container); + + $controller = function (PsrContainer $container) { + $service = $container->get('service'); + }; + $controller($psr11); + +Using the PSR-11 ServiceLocator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes, a service needs access to several other services without being sure +that all of them will actually be used. In those cases, you may want the +instantiation of the services to be lazy. + +The traditional solution is to inject the entire service container to get only +the services really needed. However, this is not recommended because it gives +services a too broad access to the rest of the application and it hides their +actual dependencies. + +The ``ServiceLocator`` is intended to solve this problem by giving access to a +set of predefined services while instantiating them only when actually needed. + +It also allows you to make your services available under a different name than +the one used to register them. For instance, you may want to use an object +that expects an instance of ``EventDispatcherInterface`` to be available under +the name ``event_dispatcher`` while your event dispatcher has been +registered under the name ``dispatcher``: + +.. code-block:: php + + use Monolog\Logger; + use Pimple\Psr11\ServiceLocator; + use Psr\Container\ContainerInterface; + use Symfony\Component\EventDispatcher\EventDispatcher; + + class MyService + { + /** + * "logger" must be an instance of Psr\Log\LoggerInterface + * "event_dispatcher" must be an instance of Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + private $services; + + public function __construct(ContainerInterface $services) + { + $this->services = $services; + } + } + + $container['logger'] = function ($c) { + return new Monolog\Logger(); + }; + $container['dispatcher'] = function () { + return new EventDispatcher(); + }; + + $container['service'] = function ($c) { + $locator = new ServiceLocator($c, array('logger', 'event_dispatcher' => 'dispatcher')); + + return new MyService($locator); + }; + +Referencing a Collection of Services Lazily +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Passing a collection of services instances in an array may prove inefficient +if the class that consumes the collection only needs to iterate over it at a +later stage, when one of its method is called. It can also lead to problems +if there is a circular dependency between one of the services stored in the +collection and the class that consumes it. + +The ``ServiceIterator`` class helps you solve these issues. It receives a +list of service names during instantiation and will retrieve the services +when iterated over: + +.. code-block:: php + + use Pimple\Container; + use Pimple\ServiceIterator; + + class AuthorizationService + { + private $voters; + + public function __construct($voters) + { + $this->voters = $voters; + } + + public function canAccess($resource) + { + foreach ($this->voters as $voter) { + if (true === $voter->canAccess($resource)) { + return true; + } + } + + return false; + } + } + + $container = new Container(); + + $container['voter1'] = function ($c) { + return new SomeVoter(); + } + $container['voter2'] = function ($c) { + return new SomeOtherVoter($c['auth']); + } + $container['auth'] = function ($c) { + return new AuthorizationService(new ServiceIterator($c, array('voter1', 'voter2')); + } + +.. _Pimple 1.x documentation: https://github.com/silexphp/Pimple/tree/1.1 diff --git a/vendor/pimple/pimple/composer.json b/vendor/pimple/pimple/composer.json new file mode 100644 index 0000000..ca6a581 --- /dev/null +++ b/vendor/pimple/pimple/composer.json @@ -0,0 +1,29 @@ +{ + "name": "pimple/pimple", + "type": "library", + "description": "Pimple, a simple Dependency Injection Container", + "keywords": ["dependency injection", "container"], + "homepage": "https://pimple.symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1 || ^2.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^5.4@dev" + }, + "autoload": { + "psr-0": { "Pimple": "src/" } + }, + "extra": { + "branch-alias": { + "dev-master": "3.4.x-dev" + } + } +} diff --git a/vendor/pimple/pimple/phpunit.xml.dist b/vendor/pimple/pimple/phpunit.xml.dist new file mode 100644 index 0000000..8990202 --- /dev/null +++ b/vendor/pimple/pimple/phpunit.xml.dist @@ -0,0 +1,18 @@ + + + + + + ./src/Pimple/Tests + + + + + + + diff --git a/vendor/pimple/pimple/src/Pimple/Container.php b/vendor/pimple/pimple/src/Pimple/Container.php new file mode 100644 index 0000000..586a0b7 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Container.php @@ -0,0 +1,305 @@ +factories = new \SplObjectStorage(); + $this->protected = new \SplObjectStorage(); + + foreach ($values as $key => $value) { + $this->offsetSet($key, $value); + } + } + + /** + * Sets a parameter or an object. + * + * Objects must be defined as Closures. + * + * Allowing any PHP callable leads to difficult to debug problems + * as function names (strings) are callable (creating a function with + * the same name as an existing parameter would break your container). + * + * @param string $id The unique identifier for the parameter or object + * @param mixed $value The value of the parameter or a closure to define an object + * + * @return void + * + * @throws FrozenServiceException Prevent override of a frozen service + */ + #[\ReturnTypeWillChange] + public function offsetSet($id, $value) + { + if (isset($this->frozen[$id])) { + throw new FrozenServiceException($id); + } + + $this->values[$id] = $value; + $this->keys[$id] = true; + } + + /** + * Gets a parameter or an object. + * + * @param string $id The unique identifier for the parameter or object + * + * @return mixed The value of the parameter or an object + * + * @throws UnknownIdentifierException If the identifier is not defined + */ + #[\ReturnTypeWillChange] + public function offsetGet($id) + { + if (!isset($this->keys[$id])) { + throw new UnknownIdentifierException($id); + } + + if ( + isset($this->raw[$id]) + || !\is_object($this->values[$id]) + || isset($this->protected[$this->values[$id]]) + || !\method_exists($this->values[$id], '__invoke') + ) { + return $this->values[$id]; + } + + if (isset($this->factories[$this->values[$id]])) { + return $this->values[$id]($this); + } + + $raw = $this->values[$id]; + $val = $this->values[$id] = $raw($this); + $this->raw[$id] = $raw; + + $this->frozen[$id] = true; + + return $val; + } + + /** + * Checks if a parameter or an object is set. + * + * @param string $id The unique identifier for the parameter or object + * + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($id) + { + return isset($this->keys[$id]); + } + + /** + * Unsets a parameter or an object. + * + * @param string $id The unique identifier for the parameter or object + * + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetUnset($id) + { + if (isset($this->keys[$id])) { + if (\is_object($this->values[$id])) { + unset($this->factories[$this->values[$id]], $this->protected[$this->values[$id]]); + } + + unset($this->values[$id], $this->frozen[$id], $this->raw[$id], $this->keys[$id]); + } + } + + /** + * Marks a callable as being a factory service. + * + * @param callable $callable A service definition to be used as a factory + * + * @return callable The passed callable + * + * @throws ExpectedInvokableException Service definition has to be a closure or an invokable object + */ + public function factory($callable) + { + if (!\is_object($callable) || !\method_exists($callable, '__invoke')) { + throw new ExpectedInvokableException('Service definition is not a Closure or invokable object.'); + } + + $this->factories->attach($callable); + + return $callable; + } + + /** + * Protects a callable from being interpreted as a service. + * + * This is useful when you want to store a callable as a parameter. + * + * @param callable $callable A callable to protect from being evaluated + * + * @return callable The passed callable + * + * @throws ExpectedInvokableException Service definition has to be a closure or an invokable object + */ + public function protect($callable) + { + if (!\is_object($callable) || !\method_exists($callable, '__invoke')) { + throw new ExpectedInvokableException('Callable is not a Closure or invokable object.'); + } + + $this->protected->attach($callable); + + return $callable; + } + + /** + * Gets a parameter or the closure defining an object. + * + * @param string $id The unique identifier for the parameter or object + * + * @return mixed The value of the parameter or the closure defining an object + * + * @throws UnknownIdentifierException If the identifier is not defined + */ + public function raw($id) + { + if (!isset($this->keys[$id])) { + throw new UnknownIdentifierException($id); + } + + if (isset($this->raw[$id])) { + return $this->raw[$id]; + } + + return $this->values[$id]; + } + + /** + * Extends an object definition. + * + * Useful when you want to extend an existing object definition, + * without necessarily loading that object. + * + * @param string $id The unique identifier for the object + * @param callable $callable A service definition to extend the original + * + * @return callable The wrapped callable + * + * @throws UnknownIdentifierException If the identifier is not defined + * @throws FrozenServiceException If the service is frozen + * @throws InvalidServiceIdentifierException If the identifier belongs to a parameter + * @throws ExpectedInvokableException If the extension callable is not a closure or an invokable object + */ + public function extend($id, $callable) + { + if (!isset($this->keys[$id])) { + throw new UnknownIdentifierException($id); + } + + if (isset($this->frozen[$id])) { + throw new FrozenServiceException($id); + } + + if (!\is_object($this->values[$id]) || !\method_exists($this->values[$id], '__invoke')) { + throw new InvalidServiceIdentifierException($id); + } + + if (isset($this->protected[$this->values[$id]])) { + @\trigger_error(\sprintf('How Pimple behaves when extending protected closures will be fixed in Pimple 4. Are you sure "%s" should be protected?', $id), E_USER_DEPRECATED); + } + + if (!\is_object($callable) || !\method_exists($callable, '__invoke')) { + throw new ExpectedInvokableException('Extension service definition is not a Closure or invokable object.'); + } + + $factory = $this->values[$id]; + + $extended = function ($c) use ($callable, $factory) { + return $callable($factory($c), $c); + }; + + if (isset($this->factories[$factory])) { + $this->factories->detach($factory); + $this->factories->attach($extended); + } + + return $this[$id] = $extended; + } + + /** + * Returns all defined value names. + * + * @return array An array of value names + */ + public function keys() + { + return \array_keys($this->values); + } + + /** + * Registers a service provider. + * + * @param array $values An array of values that customizes the provider + * + * @return static + */ + public function register(ServiceProviderInterface $provider, array $values = []) + { + $provider->register($this); + + foreach ($values as $key => $value) { + $this[$key] = $value; + } + + return $this; + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Exception/ExpectedInvokableException.php b/vendor/pimple/pimple/src/Pimple/Exception/ExpectedInvokableException.php new file mode 100644 index 0000000..7228421 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Exception/ExpectedInvokableException.php @@ -0,0 +1,38 @@ + + */ +class ExpectedInvokableException extends \InvalidArgumentException implements ContainerExceptionInterface +{ +} diff --git a/vendor/pimple/pimple/src/Pimple/Exception/FrozenServiceException.php b/vendor/pimple/pimple/src/Pimple/Exception/FrozenServiceException.php new file mode 100644 index 0000000..e4d2f6d --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Exception/FrozenServiceException.php @@ -0,0 +1,45 @@ + + */ +class FrozenServiceException extends \RuntimeException implements ContainerExceptionInterface +{ + /** + * @param string $id Identifier of the frozen service + */ + public function __construct($id) + { + parent::__construct(\sprintf('Cannot override frozen service "%s".', $id)); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Exception/InvalidServiceIdentifierException.php b/vendor/pimple/pimple/src/Pimple/Exception/InvalidServiceIdentifierException.php new file mode 100644 index 0000000..91e82f9 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Exception/InvalidServiceIdentifierException.php @@ -0,0 +1,45 @@ + + */ +class InvalidServiceIdentifierException extends \InvalidArgumentException implements NotFoundExceptionInterface +{ + /** + * @param string $id The invalid identifier + */ + public function __construct($id) + { + parent::__construct(\sprintf('Identifier "%s" does not contain an object definition.', $id)); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Exception/UnknownIdentifierException.php b/vendor/pimple/pimple/src/Pimple/Exception/UnknownIdentifierException.php new file mode 100644 index 0000000..fb6b626 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Exception/UnknownIdentifierException.php @@ -0,0 +1,45 @@ + + */ +class UnknownIdentifierException extends \InvalidArgumentException implements NotFoundExceptionInterface +{ + /** + * @param string $id The unknown identifier + */ + public function __construct($id) + { + parent::__construct(\sprintf('Identifier "%s" is not defined.', $id)); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Psr11/Container.php b/vendor/pimple/pimple/src/Pimple/Psr11/Container.php new file mode 100644 index 0000000..e18592e --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Psr11/Container.php @@ -0,0 +1,55 @@ + + */ +final class Container implements ContainerInterface +{ + private $pimple; + + public function __construct(PimpleContainer $pimple) + { + $this->pimple = $pimple; + } + + public function get(string $id) + { + return $this->pimple[$id]; + } + + public function has(string $id): bool + { + return isset($this->pimple[$id]); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Psr11/ServiceLocator.php b/vendor/pimple/pimple/src/Pimple/Psr11/ServiceLocator.php new file mode 100644 index 0000000..714b882 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Psr11/ServiceLocator.php @@ -0,0 +1,75 @@ + + */ +class ServiceLocator implements ContainerInterface +{ + private $container; + private $aliases = []; + + /** + * @param PimpleContainer $container The Container instance used to locate services + * @param array $ids Array of service ids that can be located. String keys can be used to define aliases + */ + public function __construct(PimpleContainer $container, array $ids) + { + $this->container = $container; + + foreach ($ids as $key => $id) { + $this->aliases[\is_int($key) ? $id : $key] = $id; + } + } + + /** + * {@inheritdoc} + */ + public function get(string $id) + { + if (!isset($this->aliases[$id])) { + throw new UnknownIdentifierException($id); + } + + return $this->container[$this->aliases[$id]]; + } + + /** + * {@inheritdoc} + */ + public function has(string $id): bool + { + return isset($this->aliases[$id]) && isset($this->container[$this->aliases[$id]]); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/ServiceIterator.php b/vendor/pimple/pimple/src/Pimple/ServiceIterator.php new file mode 100644 index 0000000..ebafac1 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/ServiceIterator.php @@ -0,0 +1,89 @@ + + */ +final class ServiceIterator implements \Iterator +{ + private $container; + private $ids; + + public function __construct(Container $container, array $ids) + { + $this->container = $container; + $this->ids = $ids; + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function rewind() + { + \reset($this->ids); + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function current() + { + return $this->container[\current($this->ids)]; + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function key() + { + return \current($this->ids); + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function next() + { + \next($this->ids); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function valid() + { + return null !== \key($this->ids); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/ServiceProviderInterface.php b/vendor/pimple/pimple/src/Pimple/ServiceProviderInterface.php new file mode 100644 index 0000000..abf90d8 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/ServiceProviderInterface.php @@ -0,0 +1,44 @@ +value = $value; + + return $service; + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/NonInvokable.php b/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/NonInvokable.php new file mode 100644 index 0000000..33cd4e5 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/NonInvokable.php @@ -0,0 +1,34 @@ +factory(function () { + return new Service(); + }); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/Service.php b/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/Service.php new file mode 100644 index 0000000..d71b184 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/Service.php @@ -0,0 +1,35 @@ + + */ +class Service +{ + public $value; +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/PimpleServiceProviderInterfaceTest.php b/vendor/pimple/pimple/src/Pimple/Tests/PimpleServiceProviderInterfaceTest.php new file mode 100644 index 0000000..097a7fd --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/PimpleServiceProviderInterfaceTest.php @@ -0,0 +1,77 @@ + + */ +class PimpleServiceProviderInterfaceTest extends TestCase +{ + public function testProvider() + { + $pimple = new Container(); + + $pimpleServiceProvider = new Fixtures\PimpleServiceProvider(); + $pimpleServiceProvider->register($pimple); + + $this->assertEquals('value', $pimple['param']); + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $pimple['service']); + + $serviceOne = $pimple['factory']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + + $serviceTwo = $pimple['factory']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + + $this->assertNotSame($serviceOne, $serviceTwo); + } + + public function testProviderWithRegisterMethod() + { + $pimple = new Container(); + + $pimple->register(new Fixtures\PimpleServiceProvider(), [ + 'anotherParameter' => 'anotherValue', + ]); + + $this->assertEquals('value', $pimple['param']); + $this->assertEquals('anotherValue', $pimple['anotherParameter']); + + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $pimple['service']); + + $serviceOne = $pimple['factory']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + + $serviceTwo = $pimple['factory']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + + $this->assertNotSame($serviceOne, $serviceTwo); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/PimpleTest.php b/vendor/pimple/pimple/src/Pimple/Tests/PimpleTest.php new file mode 100644 index 0000000..ffa50a6 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/PimpleTest.php @@ -0,0 +1,610 @@ + + */ +class PimpleTest extends TestCase +{ + public function testWithString() + { + $pimple = new Container(); + $pimple['param'] = 'value'; + + $this->assertEquals('value', $pimple['param']); + } + + public function testWithClosure() + { + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $pimple['service']); + } + + public function testServicesShouldBeDifferent() + { + $pimple = new Container(); + $pimple['service'] = $pimple->factory(function () { + return new Fixtures\Service(); + }); + + $serviceOne = $pimple['service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + + $serviceTwo = $pimple['service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + + $this->assertNotSame($serviceOne, $serviceTwo); + } + + public function testShouldPassContainerAsParameter() + { + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + $pimple['container'] = function ($container) { + return $container; + }; + + $this->assertNotSame($pimple, $pimple['service']); + $this->assertSame($pimple, $pimple['container']); + } + + public function testIsset() + { + $pimple = new Container(); + $pimple['param'] = 'value'; + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + + $pimple['null'] = null; + + $this->assertTrue(isset($pimple['param'])); + $this->assertTrue(isset($pimple['service'])); + $this->assertTrue(isset($pimple['null'])); + $this->assertFalse(isset($pimple['non_existent'])); + } + + public function testConstructorInjection() + { + $params = ['param' => 'value']; + $pimple = new Container($params); + + $this->assertSame($params['param'], $pimple['param']); + } + + public function testOffsetGetValidatesKeyIsPresent() + { + $this->expectException(\Pimple\Exception\UnknownIdentifierException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $pimple = new Container(); + echo $pimple['foo']; + } + + /** + * @group legacy + */ + public function testLegacyOffsetGetValidatesKeyIsPresent() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $pimple = new Container(); + echo $pimple['foo']; + } + + public function testOffsetGetHonorsNullValues() + { + $pimple = new Container(); + $pimple['foo'] = null; + $this->assertNull($pimple['foo']); + } + + public function testUnset() + { + $pimple = new Container(); + $pimple['param'] = 'value'; + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + + unset($pimple['param'], $pimple['service']); + $this->assertFalse(isset($pimple['param'])); + $this->assertFalse(isset($pimple['service'])); + } + + /** + * @dataProvider serviceDefinitionProvider + */ + public function testShare($service) + { + $pimple = new Container(); + $pimple['shared_service'] = $service; + + $serviceOne = $pimple['shared_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + + $serviceTwo = $pimple['shared_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + + $this->assertSame($serviceOne, $serviceTwo); + } + + /** + * @dataProvider serviceDefinitionProvider + */ + public function testProtect($service) + { + $pimple = new Container(); + $pimple['protected'] = $pimple->protect($service); + + $this->assertSame($service, $pimple['protected']); + } + + public function testGlobalFunctionNameAsParameterValue() + { + $pimple = new Container(); + $pimple['global_function'] = 'strlen'; + $this->assertSame('strlen', $pimple['global_function']); + } + + public function testRaw() + { + $pimple = new Container(); + $pimple['service'] = $definition = $pimple->factory(function () { + return 'foo'; + }); + $this->assertSame($definition, $pimple->raw('service')); + } + + public function testRawHonorsNullValues() + { + $pimple = new Container(); + $pimple['foo'] = null; + $this->assertNull($pimple->raw('foo')); + } + + public function testFluentRegister() + { + $pimple = new Container(); + $this->assertSame($pimple, $pimple->register($this->getMockBuilder('Pimple\ServiceProviderInterface')->getMock())); + } + + public function testRawValidatesKeyIsPresent() + { + $this->expectException(\Pimple\Exception\UnknownIdentifierException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $pimple = new Container(); + $pimple->raw('foo'); + } + + /** + * @group legacy + */ + public function testLegacyRawValidatesKeyIsPresent() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $pimple = new Container(); + $pimple->raw('foo'); + } + + /** + * @dataProvider serviceDefinitionProvider + */ + public function testExtend($service) + { + $pimple = new Container(); + $pimple['shared_service'] = function () { + return new Fixtures\Service(); + }; + $pimple['factory_service'] = $pimple->factory(function () { + return new Fixtures\Service(); + }); + + $pimple->extend('shared_service', $service); + $serviceOne = $pimple['shared_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + $serviceTwo = $pimple['shared_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + $this->assertSame($serviceOne, $serviceTwo); + $this->assertSame($serviceOne->value, $serviceTwo->value); + + $pimple->extend('factory_service', $service); + $serviceOne = $pimple['factory_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + $serviceTwo = $pimple['factory_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + $this->assertNotSame($serviceOne, $serviceTwo); + $this->assertNotSame($serviceOne->value, $serviceTwo->value); + } + + public function testExtendDoesNotLeakWithFactories() + { + if (\extension_loaded('pimple')) { + $this->markTestSkipped('Pimple extension does not support this test'); + } + $pimple = new Container(); + + $pimple['foo'] = $pimple->factory(function () { + return; + }); + $pimple['foo'] = $pimple->extend('foo', function ($foo, $pimple) { + return; + }); + unset($pimple['foo']); + + $p = new \ReflectionProperty($pimple, 'values'); + $p->setAccessible(true); + $this->assertEmpty($p->getValue($pimple)); + + $p = new \ReflectionProperty($pimple, 'factories'); + $p->setAccessible(true); + $this->assertCount(0, $p->getValue($pimple)); + } + + public function testExtendValidatesKeyIsPresent() + { + $this->expectException(\Pimple\Exception\UnknownIdentifierException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $pimple = new Container(); + $pimple->extend('foo', function () { + }); + } + + /** + * @group legacy + */ + public function testLegacyExtendValidatesKeyIsPresent() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $pimple = new Container(); + $pimple->extend('foo', function () { + }); + } + + public function testKeys() + { + $pimple = new Container(); + $pimple['foo'] = 123; + $pimple['bar'] = 123; + + $this->assertEquals(['foo', 'bar'], $pimple->keys()); + } + + /** @test */ + public function settingAnInvokableObjectShouldTreatItAsFactory() + { + $pimple = new Container(); + $pimple['invokable'] = new Fixtures\Invokable(); + + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $pimple['invokable']); + } + + /** @test */ + public function settingNonInvokableObjectShouldTreatItAsParameter() + { + $pimple = new Container(); + $pimple['non_invokable'] = new Fixtures\NonInvokable(); + + $this->assertInstanceOf('Pimple\Tests\Fixtures\NonInvokable', $pimple['non_invokable']); + } + + /** + * @dataProvider badServiceDefinitionProvider + */ + public function testFactoryFailsForInvalidServiceDefinitions($service) + { + $this->expectException(\Pimple\Exception\ExpectedInvokableException::class); + $this->expectExceptionMessage('Service definition is not a Closure or invokable object.'); + + $pimple = new Container(); + $pimple->factory($service); + } + + /** + * @group legacy + * @dataProvider badServiceDefinitionProvider + */ + public function testLegacyFactoryFailsForInvalidServiceDefinitions($service) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Service definition is not a Closure or invokable object.'); + + $pimple = new Container(); + $pimple->factory($service); + } + + /** + * @dataProvider badServiceDefinitionProvider + */ + public function testProtectFailsForInvalidServiceDefinitions($service) + { + $this->expectException(\Pimple\Exception\ExpectedInvokableException::class); + $this->expectExceptionMessage('Callable is not a Closure or invokable object.'); + + $pimple = new Container(); + $pimple->protect($service); + } + + /** + * @group legacy + * @dataProvider badServiceDefinitionProvider + */ + public function testLegacyProtectFailsForInvalidServiceDefinitions($service) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Callable is not a Closure or invokable object.'); + + $pimple = new Container(); + $pimple->protect($service); + } + + /** + * @dataProvider badServiceDefinitionProvider + */ + public function testExtendFailsForKeysNotContainingServiceDefinitions($service) + { + $this->expectException(\Pimple\Exception\InvalidServiceIdentifierException::class); + $this->expectExceptionMessage('Identifier "foo" does not contain an object definition.'); + + $pimple = new Container(); + $pimple['foo'] = $service; + $pimple->extend('foo', function () { + }); + } + + /** + * @group legacy + * @dataProvider badServiceDefinitionProvider + */ + public function testLegacyExtendFailsForKeysNotContainingServiceDefinitions($service) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Identifier "foo" does not contain an object definition.'); + + $pimple = new Container(); + $pimple['foo'] = $service; + $pimple->extend('foo', function () { + }); + } + + /** + * @group legacy + * @expectedDeprecation How Pimple behaves when extending protected closures will be fixed in Pimple 4. Are you sure "foo" should be protected? + */ + public function testExtendingProtectedClosureDeprecation() + { + $pimple = new Container(); + $pimple['foo'] = $pimple->protect(function () { + return 'bar'; + }); + + $pimple->extend('foo', function ($value) { + return $value.'-baz'; + }); + + $this->assertSame('bar-baz', $pimple['foo']); + } + + /** + * @dataProvider badServiceDefinitionProvider + */ + public function testExtendFailsForInvalidServiceDefinitions($service) + { + $this->expectException(\Pimple\Exception\ExpectedInvokableException::class); + $this->expectExceptionMessage('Extension service definition is not a Closure or invokable object.'); + + $pimple = new Container(); + $pimple['foo'] = function () { + }; + $pimple->extend('foo', $service); + } + + /** + * @group legacy + * @dataProvider badServiceDefinitionProvider + */ + public function testLegacyExtendFailsForInvalidServiceDefinitions($service) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Extension service definition is not a Closure or invokable object.'); + + $pimple = new Container(); + $pimple['foo'] = function () { + }; + $pimple->extend('foo', $service); + } + + public function testExtendFailsIfFrozenServiceIsNonInvokable() + { + $this->expectException(\Pimple\Exception\FrozenServiceException::class); + $this->expectExceptionMessage('Cannot override frozen service "foo".'); + + $pimple = new Container(); + $pimple['foo'] = function () { + return new Fixtures\NonInvokable(); + }; + $foo = $pimple['foo']; + + $pimple->extend('foo', function () { + }); + } + + public function testExtendFailsIfFrozenServiceIsInvokable() + { + $this->expectException(\Pimple\Exception\FrozenServiceException::class); + $this->expectExceptionMessage('Cannot override frozen service "foo".'); + + $pimple = new Container(); + $pimple['foo'] = function () { + return new Fixtures\Invokable(); + }; + $foo = $pimple['foo']; + + $pimple->extend('foo', function () { + }); + } + + /** + * Provider for invalid service definitions. + */ + public function badServiceDefinitionProvider() + { + return [ + [123], + [new Fixtures\NonInvokable()], + ]; + } + + /** + * Provider for service definitions. + */ + public function serviceDefinitionProvider() + { + return [ + [function ($value) { + $service = new Fixtures\Service(); + $service->value = $value; + + return $service; + }], + [new Fixtures\Invokable()], + ]; + } + + public function testDefiningNewServiceAfterFreeze() + { + $pimple = new Container(); + $pimple['foo'] = function () { + return 'foo'; + }; + $foo = $pimple['foo']; + + $pimple['bar'] = function () { + return 'bar'; + }; + $this->assertSame('bar', $pimple['bar']); + } + + public function testOverridingServiceAfterFreeze() + { + $this->expectException(\Pimple\Exception\FrozenServiceException::class); + $this->expectExceptionMessage('Cannot override frozen service "foo".'); + + $pimple = new Container(); + $pimple['foo'] = function () { + return 'foo'; + }; + $foo = $pimple['foo']; + + $pimple['foo'] = function () { + return 'bar'; + }; + } + + /** + * @group legacy + */ + public function testLegacyOverridingServiceAfterFreeze() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Cannot override frozen service "foo".'); + + $pimple = new Container(); + $pimple['foo'] = function () { + return 'foo'; + }; + $foo = $pimple['foo']; + + $pimple['foo'] = function () { + return 'bar'; + }; + } + + public function testRemovingServiceAfterFreeze() + { + $pimple = new Container(); + $pimple['foo'] = function () { + return 'foo'; + }; + $foo = $pimple['foo']; + + unset($pimple['foo']); + $pimple['foo'] = function () { + return 'bar'; + }; + $this->assertSame('bar', $pimple['foo']); + } + + public function testExtendingService() + { + $pimple = new Container(); + $pimple['foo'] = function () { + return 'foo'; + }; + $pimple['foo'] = $pimple->extend('foo', function ($foo, $app) { + return "$foo.bar"; + }); + $pimple['foo'] = $pimple->extend('foo', function ($foo, $app) { + return "$foo.baz"; + }); + $this->assertSame('foo.bar.baz', $pimple['foo']); + } + + public function testExtendingServiceAfterOtherServiceFreeze() + { + $pimple = new Container(); + $pimple['foo'] = function () { + return 'foo'; + }; + $pimple['bar'] = function () { + return 'bar'; + }; + $foo = $pimple['foo']; + + $pimple['bar'] = $pimple->extend('bar', function ($bar, $app) { + return "$bar.baz"; + }); + $this->assertSame('bar.baz', $pimple['bar']); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/Psr11/ContainerTest.php b/vendor/pimple/pimple/src/Pimple/Tests/Psr11/ContainerTest.php new file mode 100644 index 0000000..d47b9c3 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/Psr11/ContainerTest.php @@ -0,0 +1,76 @@ +assertSame($pimple['service'], $psr->get('service')); + } + + public function testGetThrowsExceptionIfServiceIsNotFound() + { + $this->expectException(\Psr\Container\NotFoundExceptionInterface::class); + $this->expectExceptionMessage('Identifier "service" is not defined.'); + + $pimple = new Container(); + $psr = new PsrContainer($pimple); + + $psr->get('service'); + } + + public function testHasReturnsTrueIfServiceExists() + { + $pimple = new Container(); + $pimple['service'] = function () { + return new Service(); + }; + $psr = new PsrContainer($pimple); + + $this->assertTrue($psr->has('service')); + } + + public function testHasReturnsFalseIfServiceDoesNotExist() + { + $pimple = new Container(); + $psr = new PsrContainer($pimple); + + $this->assertFalse($psr->has('service')); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/Psr11/ServiceLocatorTest.php b/vendor/pimple/pimple/src/Pimple/Tests/Psr11/ServiceLocatorTest.php new file mode 100644 index 0000000..bd2d335 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/Psr11/ServiceLocatorTest.php @@ -0,0 +1,131 @@ + + */ +class ServiceLocatorTest extends TestCase +{ + public function testCanAccessServices() + { + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($pimple, ['service']); + + $this->assertSame($pimple['service'], $locator->get('service')); + } + + public function testCanAccessAliasedServices() + { + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($pimple, ['alias' => 'service']); + + $this->assertSame($pimple['service'], $locator->get('alias')); + } + + public function testCannotAccessAliasedServiceUsingRealIdentifier() + { + $this->expectException(\Pimple\Exception\UnknownIdentifierException::class); + $this->expectExceptionMessage('Identifier "service" is not defined.'); + + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($pimple, ['alias' => 'service']); + + $service = $locator->get('service'); + } + + public function testGetValidatesServiceCanBeLocated() + { + $this->expectException(\Pimple\Exception\UnknownIdentifierException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($pimple, ['alias' => 'service']); + + $service = $locator->get('foo'); + } + + public function testGetValidatesTargetServiceExists() + { + $this->expectException(\Pimple\Exception\UnknownIdentifierException::class); + $this->expectExceptionMessage('Identifier "invalid" is not defined.'); + + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($pimple, ['alias' => 'invalid']); + + $service = $locator->get('alias'); + } + + public function testHasValidatesServiceCanBeLocated() + { + $pimple = new Container(); + $pimple['service1'] = function () { + return new Fixtures\Service(); + }; + $pimple['service2'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($pimple, ['service1']); + + $this->assertTrue($locator->has('service1')); + $this->assertFalse($locator->has('service2')); + } + + public function testHasChecksIfTargetServiceExists() + { + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($pimple, ['foo' => 'service', 'bar' => 'invalid']); + + $this->assertTrue($locator->has('foo')); + $this->assertFalse($locator->has('bar')); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/ServiceIteratorTest.php b/vendor/pimple/pimple/src/Pimple/Tests/ServiceIteratorTest.php new file mode 100644 index 0000000..2bb935f --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/ServiceIteratorTest.php @@ -0,0 +1,52 @@ +assertSame(['service1' => $pimple['service1'], 'service2' => $pimple['service2']], iterator_to_array($iterator)); + } +} diff --git a/vendor/psr/cache/CHANGELOG.md b/vendor/psr/cache/CHANGELOG.md new file mode 100644 index 0000000..58ddab0 --- /dev/null +++ b/vendor/psr/cache/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to this project will be documented in this file, in reverse chronological order by release. + +## 1.0.1 - 2016-08-06 + +### Fixed + +- Make spacing consistent in phpdoc annotations php-fig/cache#9 - chalasr +- Fix grammar in phpdoc annotations php-fig/cache#10 - chalasr +- Be more specific in docblocks that `getItems()` and `deleteItems()` take an array of strings (`string[]`) compared to just `array` php-fig/cache#8 - GrahamCampbell +- For `expiresAt()` and `expiresAfter()` in CacheItemInterface fix docblock to specify null as a valid parameters as well as an implementation of DateTimeInterface php-fig/cache#7 - GrahamCampbell + +## 1.0.0 - 2015-12-11 + +Initial stable release; reflects accepted PSR-6 specification diff --git a/vendor/psr/cache/LICENSE.txt b/vendor/psr/cache/LICENSE.txt new file mode 100644 index 0000000..b1c2c97 --- /dev/null +++ b/vendor/psr/cache/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2015 PHP Framework Interoperability Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/psr/cache/README.md b/vendor/psr/cache/README.md new file mode 100644 index 0000000..c8706ce --- /dev/null +++ b/vendor/psr/cache/README.md @@ -0,0 +1,9 @@ +PSR Cache +========= + +This repository holds all interfaces defined by +[PSR-6](http://www.php-fig.org/psr/psr-6/). + +Note that this is not a Cache implementation of its own. It is merely an +interface that describes a Cache implementation. See the specification for more +details. diff --git a/vendor/psr/cache/composer.json b/vendor/psr/cache/composer.json new file mode 100644 index 0000000..e828fec --- /dev/null +++ b/vendor/psr/cache/composer.json @@ -0,0 +1,25 @@ +{ + "name": "psr/cache", + "description": "Common interface for caching libraries", + "keywords": ["psr", "psr-6", "cache"], + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/vendor/psr/cache/src/CacheException.php b/vendor/psr/cache/src/CacheException.php new file mode 100644 index 0000000..e27f22f --- /dev/null +++ b/vendor/psr/cache/src/CacheException.php @@ -0,0 +1,10 @@ +=7.2.0" + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + } +} diff --git a/vendor/psr/container/src/ContainerExceptionInterface.php b/vendor/psr/container/src/ContainerExceptionInterface.php new file mode 100644 index 0000000..cf10b8b --- /dev/null +++ b/vendor/psr/container/src/ContainerExceptionInterface.php @@ -0,0 +1,10 @@ +=7.2.0" + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/vendor/psr/event-dispatcher/src/EventDispatcherInterface.php b/vendor/psr/event-dispatcher/src/EventDispatcherInterface.php new file mode 100644 index 0000000..4306fa9 --- /dev/null +++ b/vendor/psr/event-dispatcher/src/EventDispatcherInterface.php @@ -0,0 +1,21 @@ +=7.0.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/vendor/psr/http-factory/src/RequestFactoryInterface.php b/vendor/psr/http-factory/src/RequestFactoryInterface.php new file mode 100644 index 0000000..cb39a08 --- /dev/null +++ b/vendor/psr/http-factory/src/RequestFactoryInterface.php @@ -0,0 +1,18 @@ + `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`. +> When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered. + diff --git a/vendor/psr/http-message/docs/PSR7-Usage.md b/vendor/psr/http-message/docs/PSR7-Usage.md new file mode 100644 index 0000000..b6d048a --- /dev/null +++ b/vendor/psr/http-message/docs/PSR7-Usage.md @@ -0,0 +1,159 @@ +### PSR-7 Usage + +All PSR-7 applications comply with these interfaces +They were created to establish a standard between middleware implementations. + +> `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`. +> When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered. + + +The following examples will illustrate how basic operations are done in PSR-7. + +##### Examples + + +For this examples to work (at least) a PSR-7 implementation package is required. (eg: zendframework/zend-diactoros, guzzlehttp/psr7, slim/slim, etc) +All PSR-7 implementations should have the same behaviour. + +The following will be assumed: +`$request` is an object of `Psr\Http\Message\RequestInterface` and + +`$response` is an object implementing `Psr\Http\Message\RequestInterface` + + +### Working with HTTP Headers + +#### Adding headers to response: + +```php +$response->withHeader('My-Custom-Header', 'My Custom Message'); +``` + +#### Appending values to headers + +```php +$response->withAddedHeader('My-Custom-Header', 'The second message'); +``` + +#### Checking if header exists: + +```php +$request->hasHeader('My-Custom-Header'); // will return false +$response->hasHeader('My-Custom-Header'); // will return true +``` + +> Note: My-Custom-Header was only added in the Response + +#### Getting comma-separated values from a header (also applies to request) + +```php +// getting value from request headers +$request->getHeaderLine('Content-Type'); // will return: "text/html; charset=UTF-8" +// getting value from response headers +$response->getHeaderLine('My-Custom-Header'); // will return: "My Custom Message; The second message" +``` + +#### Getting array of value from a header (also applies to request) +```php +// getting value from request headers +$request->getHeader('Content-Type'); // will return: ["text/html", "charset=UTF-8"] +// getting value from response headers +$response->getHeader('My-Custom-Header'); // will return: ["My Custom Message", "The second message"] +``` + +#### Removing headers from HTTP Messages +```php +// removing a header from Request, removing deprecated "Content-MD5" header +$request->withoutHeader('Content-MD5'); + +// removing a header from Response +// effect: the browser won't know the size of the stream +// the browser will download the stream till it ends +$response->withoutHeader('Content-Length'); +``` + +### Working with HTTP Message Body + +When working with the PSR-7 there are two methods of implementation: +#### 1. Getting the body separately + +> This method makes the body handling easier to understand and is useful when repeatedly calling body methods. (You only call `getBody()` once). Using this method mistakes like `$response->write()` are also prevented. + +```php +$body = $response->getBody(); +// operations on body, eg. read, write, seek +// ... +// replacing the old body +$response->withBody($body); +// this last statement is optional as we working with objects +// in this case the "new" body is same with the "old" one +// the $body variable has the same value as the one in $request, only the reference is passed +``` + +#### 2. Working directly on response + +> This method is useful when only performing few operations as the `$request->getBody()` statement fragment is required + +```php +$response->getBody()->write('hello'); +``` + +### Getting the body contents + +The following snippet gets the contents of a stream contents. +> Note: Streams must be rewinded, if content was written into streams, it will be ignored when calling `getContents()` because the stream pointer is set to the last character, which is `\0` - meaning end of stream. +```php +$body = $response->getBody(); +$body->rewind(); // or $body->seek(0); +$bodyText = $body->getContents(); +``` +> Note: If `$body->seek(1)` is called before `$body->getContents()`, the first character will be ommited as the starting pointer is set to `1`, not `0`. This is why using `$body->rewind()` is recommended. + +### Append to body + +```php +$response->getBody()->write('Hello'); // writing directly +$body = $request->getBody(); // which is a `StreamInterface` +$body->write('xxxxx'); +``` + +### Prepend to body +Prepending is different when it comes to streams. The content must be copied before writing the content to be prepended. +The following example will explain the behaviour of streams. + +```php +// assuming our response is initially empty +$body = $repsonse->getBody(); +// writing the string "abcd" +$body->write('abcd'); + +// seeking to start of stream +$body->seek(0); +// writing 'ef' +$body->write('ef'); // at this point the stream contains "efcd" +``` + +#### Prepending by rewriting separately + +```php +// assuming our response body stream only contains: "abcd" +$body = $response->getBody(); +$body->rewind(); +$contents = $body->getContents(); // abcd +// seeking the stream to beginning +$body->rewind(); +$body->write('ef'); // stream contains "efcd" +$body->write($contents); // stream contains "efabcd" +``` + +> Note: `getContents()` seeks the stream while reading it, therefore if the second `rewind()` method call was not present the stream would have resulted in `abcdefabcd` because the `write()` method appends to stream if not preceeded by `rewind()` or `seek(0)`. + +#### Prepending by using contents as a string +```php +$body = $response->getBody(); +$body->rewind(); +$contents = $body->getContents(); // efabcd +$contents = 'ef'.$contents; +$body->rewind(); +$body->write($contents); +``` diff --git a/vendor/psr/http-message/src/MessageInterface.php b/vendor/psr/http-message/src/MessageInterface.php new file mode 100644 index 0000000..a83c985 --- /dev/null +++ b/vendor/psr/http-message/src/MessageInterface.php @@ -0,0 +1,187 @@ +getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->getHeaders() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return string[][] Returns an associative array of the message's headers. Each + * key MUST be a header name, and each value MUST be an array of strings + * for that header. + */ + public function getHeaders(): array; + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader(string $name): bool; + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method MUST return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function getHeader(string $name): array; + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name Case-insensitive header field name. + * @return string A string of values as provided for the given header + * concatenated together using a comma. If the header does not appear in + * the message, this method MUST return an empty string. + */ + public function getHeaderLine(string $name): string; + + /** + * Return an instance with the provided value replacing the specified header. + * + * While header names are case-insensitive, the casing of the header will + * be preserved by this function, and returned from getHeaders(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new and/or updated header and value. + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withHeader(string $name, $value): MessageInterface; + + /** + * Return an instance with the specified header appended with the given value. + * + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new header and/or value. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withAddedHeader(string $name, $value): MessageInterface; + + /** + * Return an instance without the specified header. + * + * Header resolution MUST be done without case-sensitivity. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the named header. + * + * @param string $name Case-insensitive header field name to remove. + * @return static + */ + public function withoutHeader(string $name): MessageInterface; + + /** + * Gets the body of the message. + * + * @return StreamInterface Returns the body as a stream. + */ + public function getBody(): StreamInterface; + + /** + * Return an instance with the specified message body. + * + * The body MUST be a StreamInterface object. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new body stream. + * + * @param StreamInterface $body Body. + * @return static + * @throws \InvalidArgumentException When the body is not valid. + */ + public function withBody(StreamInterface $body): MessageInterface; +} diff --git a/vendor/psr/http-message/src/RequestInterface.php b/vendor/psr/http-message/src/RequestInterface.php new file mode 100644 index 0000000..33f85e5 --- /dev/null +++ b/vendor/psr/http-message/src/RequestInterface.php @@ -0,0 +1,130 @@ +getQuery()` + * or from the `QUERY_STRING` server param. + * + * @return array + */ + public function getQueryParams(): array; + + /** + * Return an instance with the specified query string arguments. + * + * These values SHOULD remain immutable over the course of the incoming + * request. They MAY be injected during instantiation, such as from PHP's + * $_GET superglobal, or MAY be derived from some other value such as the + * URI. In cases where the arguments are parsed from the URI, the data + * MUST be compatible with what PHP's parse_str() would return for + * purposes of how duplicate query parameters are handled, and how nested + * sets are handled. + * + * Setting query string arguments MUST NOT change the URI stored by the + * request, nor the values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated query string arguments. + * + * @param array $query Array of query string arguments, typically from + * $_GET. + * @return static + */ + public function withQueryParams(array $query): ServerRequestInterface; + + /** + * Retrieve normalized file upload data. + * + * This method returns upload metadata in a normalized tree, with each leaf + * an instance of Psr\Http\Message\UploadedFileInterface. + * + * These values MAY be prepared from $_FILES or the message body during + * instantiation, or MAY be injected via withUploadedFiles(). + * + * @return array An array tree of UploadedFileInterface instances; an empty + * array MUST be returned if no data is present. + */ + public function getUploadedFiles(): array; + + /** + * Create a new instance with the specified uploaded files. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param array $uploadedFiles An array tree of UploadedFileInterface instances. + * @return static + * @throws \InvalidArgumentException if an invalid structure is provided. + */ + public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface; + + /** + * Retrieve any parameters provided in the request body. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, this method MUST + * return the contents of $_POST. + * + * Otherwise, this method may return any results of deserializing + * the request body content; as parsing returns structured content, the + * potential types MUST be arrays or objects only. A null value indicates + * the absence of body content. + * + * @return null|array|object The deserialized body parameters, if any. + * These will typically be an array or object. + */ + public function getParsedBody(); + + /** + * Return an instance with the specified body parameters. + * + * These MAY be injected during instantiation. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, use this method + * ONLY to inject the contents of $_POST. + * + * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of + * deserializing the request body content. Deserialization/parsing returns + * structured data, and, as such, this method ONLY accepts arrays or objects, + * or a null value if nothing was available to parse. + * + * As an example, if content negotiation determines that the request data + * is a JSON payload, this method could be used to create a request + * instance with the deserialized parameters. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param null|array|object $data The deserialized body data. This will + * typically be in an array or object. + * @return static + * @throws \InvalidArgumentException if an unsupported argument type is + * provided. + */ + public function withParsedBody($data): ServerRequestInterface; + + /** + * Retrieve attributes derived from the request. + * + * The request "attributes" may be used to allow injection of any + * parameters derived from the request: e.g., the results of path + * match operations; the results of decrypting cookies; the results of + * deserializing non-form-encoded message bodies; etc. Attributes + * will be application and request specific, and CAN be mutable. + * + * @return array Attributes derived from the request. + */ + public function getAttributes(): array; + + /** + * Retrieve a single derived request attribute. + * + * Retrieves a single derived request attribute as described in + * getAttributes(). If the attribute has not been previously set, returns + * the default value as provided. + * + * This method obviates the need for a hasAttribute() method, as it allows + * specifying a default value to return if the attribute is not found. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + * @return mixed + */ + public function getAttribute(string $name, $default = null); + + /** + * Return an instance with the specified derived request attribute. + * + * This method allows setting a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $value The value of the attribute. + * @return static + */ + public function withAttribute(string $name, $value): ServerRequestInterface; + + /** + * Return an instance that removes the specified derived request attribute. + * + * This method allows removing a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @return static + */ + public function withoutAttribute(string $name): ServerRequestInterface; +} diff --git a/vendor/psr/http-message/src/StreamInterface.php b/vendor/psr/http-message/src/StreamInterface.php new file mode 100644 index 0000000..a62aabb --- /dev/null +++ b/vendor/psr/http-message/src/StreamInterface.php @@ -0,0 +1,158 @@ + + * [user-info@]host[:port] + * + * + * If the port component is not set or is the standard port for the current + * scheme, it SHOULD NOT be included. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.2 + * @return string The URI authority, in "[user-info@]host[:port]" format. + */ + public function getAuthority(): string; + + /** + * Retrieve the user information component of the URI. + * + * If no user information is present, this method MUST return an empty + * string. + * + * If a user is present in the URI, this will return that value; + * additionally, if the password is also present, it will be appended to the + * user value, with a colon (":") separating the values. + * + * The trailing "@" character is not part of the user information and MUST + * NOT be added. + * + * @return string The URI user information, in "username[:password]" format. + */ + public function getUserInfo(): string; + + /** + * Retrieve the host component of the URI. + * + * If no host is present, this method MUST return an empty string. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.2.2. + * + * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 + * @return string The URI host. + */ + public function getHost(): string; + + /** + * Retrieve the port component of the URI. + * + * If a port is present, and it is non-standard for the current scheme, + * this method MUST return it as an integer. If the port is the standard port + * used with the current scheme, this method SHOULD return null. + * + * If no port is present, and no scheme is present, this method MUST return + * a null value. + * + * If no port is present, but a scheme is present, this method MAY return + * the standard port for that scheme, but SHOULD return null. + * + * @return null|int The URI port. + */ + public function getPort(): ?int; + + /** + * Retrieve the path component of the URI. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * Normally, the empty path "" and absolute path "/" are considered equal as + * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically + * do this normalization because in contexts with a trimmed base path, e.g. + * the front controller, this difference becomes significant. It's the task + * of the user to handle both "" and "/". + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.3. + * + * As an example, if the value should include a slash ("/") not intended as + * delimiter between path segments, that value MUST be passed in encoded + * form (e.g., "%2F") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.3 + * @return string The URI path. + */ + public function getPath(): string; + + /** + * Retrieve the query string of the URI. + * + * If no query string is present, this method MUST return an empty string. + * + * The leading "?" character is not part of the query and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.4. + * + * As an example, if a value in a key/value pair of the query string should + * include an ampersand ("&") not intended as a delimiter between values, + * that value MUST be passed in encoded form (e.g., "%26") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.4 + * @return string The URI query string. + */ + public function getQuery(): string; + + /** + * Retrieve the fragment component of the URI. + * + * If no fragment is present, this method MUST return an empty string. + * + * The leading "#" character is not part of the fragment and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.5. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.5 + * @return string The URI fragment. + */ + public function getFragment(): string; + + /** + * Return an instance with the specified scheme. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified scheme. + * + * Implementations MUST support the schemes "http" and "https" case + * insensitively, and MAY accommodate other schemes if required. + * + * An empty scheme is equivalent to removing the scheme. + * + * @param string $scheme The scheme to use with the new instance. + * @return static A new instance with the specified scheme. + * @throws \InvalidArgumentException for invalid or unsupported schemes. + */ + public function withScheme(string $scheme): UriInterface; + + /** + * Return an instance with the specified user information. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified user information. + * + * Password is optional, but the user information MUST include the + * user; an empty string for the user is equivalent to removing user + * information. + * + * @param string $user The user name to use for authority. + * @param null|string $password The password associated with $user. + * @return static A new instance with the specified user information. + */ + public function withUserInfo(string $user, ?string $password = null): UriInterface; + + /** + * Return an instance with the specified host. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified host. + * + * An empty host value is equivalent to removing the host. + * + * @param string $host The hostname to use with the new instance. + * @return static A new instance with the specified host. + * @throws \InvalidArgumentException for invalid hostnames. + */ + public function withHost(string $host): UriInterface; + + /** + * Return an instance with the specified port. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified port. + * + * Implementations MUST raise an exception for ports outside the + * established TCP and UDP port ranges. + * + * A null value provided for the port is equivalent to removing the port + * information. + * + * @param null|int $port The port to use with the new instance; a null value + * removes the port information. + * @return static A new instance with the specified port. + * @throws \InvalidArgumentException for invalid ports. + */ + public function withPort(?int $port): UriInterface; + + /** + * Return an instance with the specified path. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified path. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * If the path is intended to be domain-relative rather than path relative then + * it must begin with a slash ("/"). Paths not starting with a slash ("/") + * are assumed to be relative to some base path known to the application or + * consumer. + * + * Users can provide both encoded and decoded path characters. + * Implementations ensure the correct encoding as outlined in getPath(). + * + * @param string $path The path to use with the new instance. + * @return static A new instance with the specified path. + * @throws \InvalidArgumentException for invalid paths. + */ + public function withPath(string $path): UriInterface; + + /** + * Return an instance with the specified query string. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified query string. + * + * Users can provide both encoded and decoded query characters. + * Implementations ensure the correct encoding as outlined in getQuery(). + * + * An empty query string value is equivalent to removing the query string. + * + * @param string $query The query string to use with the new instance. + * @return static A new instance with the specified query string. + * @throws \InvalidArgumentException for invalid query strings. + */ + public function withQuery(string $query): UriInterface; + + /** + * Return an instance with the specified URI fragment. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified URI fragment. + * + * Users can provide both encoded and decoded fragment characters. + * Implementations ensure the correct encoding as outlined in getFragment(). + * + * An empty fragment value is equivalent to removing the fragment. + * + * @param string $fragment The fragment to use with the new instance. + * @return static A new instance with the specified fragment. + */ + public function withFragment(string $fragment): UriInterface; + + /** + * Return the string representation as a URI reference. + * + * Depending on which components of the URI are present, the resulting + * string is either a full URI or relative reference according to RFC 3986, + * Section 4.1. The method concatenates the various components of the URI, + * using the appropriate delimiters: + * + * - If a scheme is present, it MUST be suffixed by ":". + * - If an authority is present, it MUST be prefixed by "//". + * - The path can be concatenated without delimiters. But there are two + * cases where the path has to be adjusted to make the URI reference + * valid as PHP does not allow to throw an exception in __toString(): + * - If the path is rootless and an authority is present, the path MUST + * be prefixed by "/". + * - If the path is starting with more than one "/" and no authority is + * present, the starting slashes MUST be reduced to one. + * - If a query is present, it MUST be prefixed by "?". + * - If a fragment is present, it MUST be prefixed by "#". + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + * @return string + */ + public function __toString(): string; +} diff --git a/vendor/psr/log/LICENSE b/vendor/psr/log/LICENSE new file mode 100644 index 0000000..474c952 --- /dev/null +++ b/vendor/psr/log/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 PHP Framework Interoperability Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/psr/log/Psr/Log/AbstractLogger.php b/vendor/psr/log/Psr/Log/AbstractLogger.php new file mode 100644 index 0000000..e02f9da --- /dev/null +++ b/vendor/psr/log/Psr/Log/AbstractLogger.php @@ -0,0 +1,128 @@ +log(LogLevel::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function alert($message, array $context = array()) + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function critical($message, array $context = array()) + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function error($message, array $context = array()) + { + $this->log(LogLevel::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function warning($message, array $context = array()) + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function notice($message, array $context = array()) + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function info($message, array $context = array()) + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function debug($message, array $context = array()) + { + $this->log(LogLevel::DEBUG, $message, $context); + } +} diff --git a/vendor/psr/log/Psr/Log/InvalidArgumentException.php b/vendor/psr/log/Psr/Log/InvalidArgumentException.php new file mode 100644 index 0000000..67f852d --- /dev/null +++ b/vendor/psr/log/Psr/Log/InvalidArgumentException.php @@ -0,0 +1,7 @@ +logger = $logger; + } +} diff --git a/vendor/psr/log/Psr/Log/LoggerInterface.php b/vendor/psr/log/Psr/Log/LoggerInterface.php new file mode 100644 index 0000000..2206cfd --- /dev/null +++ b/vendor/psr/log/Psr/Log/LoggerInterface.php @@ -0,0 +1,125 @@ +log(LogLevel::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function alert($message, array $context = array()) + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function critical($message, array $context = array()) + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function error($message, array $context = array()) + { + $this->log(LogLevel::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function warning($message, array $context = array()) + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function notice($message, array $context = array()) + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function info($message, array $context = array()) + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function debug($message, array $context = array()) + { + $this->log(LogLevel::DEBUG, $message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + * + * @throws \Psr\Log\InvalidArgumentException + */ + abstract public function log($level, $message, array $context = array()); +} diff --git a/vendor/psr/log/Psr/Log/NullLogger.php b/vendor/psr/log/Psr/Log/NullLogger.php new file mode 100644 index 0000000..c8f7293 --- /dev/null +++ b/vendor/psr/log/Psr/Log/NullLogger.php @@ -0,0 +1,30 @@ +logger) { }` + * blocks. + */ +class NullLogger extends AbstractLogger +{ + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + * + * @throws \Psr\Log\InvalidArgumentException + */ + public function log($level, $message, array $context = array()) + { + // noop + } +} diff --git a/vendor/psr/log/Psr/Log/Test/DummyTest.php b/vendor/psr/log/Psr/Log/Test/DummyTest.php new file mode 100644 index 0000000..9638c11 --- /dev/null +++ b/vendor/psr/log/Psr/Log/Test/DummyTest.php @@ -0,0 +1,18 @@ + ". + * + * Example ->error('Foo') would yield "error Foo". + * + * @return string[] + */ + abstract public function getLogs(); + + public function testImplements() + { + $this->assertInstanceOf('Psr\Log\LoggerInterface', $this->getLogger()); + } + + /** + * @dataProvider provideLevelsAndMessages + */ + public function testLogsAtAllLevels($level, $message) + { + $logger = $this->getLogger(); + $logger->{$level}($message, array('user' => 'Bob')); + $logger->log($level, $message, array('user' => 'Bob')); + + $expected = array( + $level.' message of level '.$level.' with context: Bob', + $level.' message of level '.$level.' with context: Bob', + ); + $this->assertEquals($expected, $this->getLogs()); + } + + public function provideLevelsAndMessages() + { + return array( + LogLevel::EMERGENCY => array(LogLevel::EMERGENCY, 'message of level emergency with context: {user}'), + LogLevel::ALERT => array(LogLevel::ALERT, 'message of level alert with context: {user}'), + LogLevel::CRITICAL => array(LogLevel::CRITICAL, 'message of level critical with context: {user}'), + LogLevel::ERROR => array(LogLevel::ERROR, 'message of level error with context: {user}'), + LogLevel::WARNING => array(LogLevel::WARNING, 'message of level warning with context: {user}'), + LogLevel::NOTICE => array(LogLevel::NOTICE, 'message of level notice with context: {user}'), + LogLevel::INFO => array(LogLevel::INFO, 'message of level info with context: {user}'), + LogLevel::DEBUG => array(LogLevel::DEBUG, 'message of level debug with context: {user}'), + ); + } + + /** + * @expectedException \Psr\Log\InvalidArgumentException + */ + public function testThrowsOnInvalidLevel() + { + $logger = $this->getLogger(); + $logger->log('invalid level', 'Foo'); + } + + public function testContextReplacement() + { + $logger = $this->getLogger(); + $logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar')); + + $expected = array('info {Message {nothing} Bob Bar a}'); + $this->assertEquals($expected, $this->getLogs()); + } + + public function testObjectCastToString() + { + if (method_exists($this, 'createPartialMock')) { + $dummy = $this->createPartialMock('Psr\Log\Test\DummyTest', array('__toString')); + } else { + $dummy = $this->getMock('Psr\Log\Test\DummyTest', array('__toString')); + } + $dummy->expects($this->once()) + ->method('__toString') + ->will($this->returnValue('DUMMY')); + + $this->getLogger()->warning($dummy); + + $expected = array('warning DUMMY'); + $this->assertEquals($expected, $this->getLogs()); + } + + public function testContextCanContainAnything() + { + $closed = fopen('php://memory', 'r'); + fclose($closed); + + $context = array( + 'bool' => true, + 'null' => null, + 'string' => 'Foo', + 'int' => 0, + 'float' => 0.5, + 'nested' => array('with object' => new DummyTest), + 'object' => new \DateTime, + 'resource' => fopen('php://memory', 'r'), + 'closed' => $closed, + ); + + $this->getLogger()->warning('Crazy context data', $context); + + $expected = array('warning Crazy context data'); + $this->assertEquals($expected, $this->getLogs()); + } + + public function testContextExceptionKeyCanBeExceptionOrOtherValues() + { + $logger = $this->getLogger(); + $logger->warning('Random message', array('exception' => 'oops')); + $logger->critical('Uncaught Exception!', array('exception' => new \LogicException('Fail'))); + + $expected = array( + 'warning Random message', + 'critical Uncaught Exception!' + ); + $this->assertEquals($expected, $this->getLogs()); + } +} diff --git a/vendor/psr/log/Psr/Log/Test/TestLogger.php b/vendor/psr/log/Psr/Log/Test/TestLogger.php new file mode 100644 index 0000000..1be3230 --- /dev/null +++ b/vendor/psr/log/Psr/Log/Test/TestLogger.php @@ -0,0 +1,147 @@ + $level, + 'message' => $message, + 'context' => $context, + ]; + + $this->recordsByLevel[$record['level']][] = $record; + $this->records[] = $record; + } + + public function hasRecords($level) + { + return isset($this->recordsByLevel[$level]); + } + + public function hasRecord($record, $level) + { + if (is_string($record)) { + $record = ['message' => $record]; + } + return $this->hasRecordThatPasses(function ($rec) use ($record) { + if ($rec['message'] !== $record['message']) { + return false; + } + if (isset($record['context']) && $rec['context'] !== $record['context']) { + return false; + } + return true; + }, $level); + } + + public function hasRecordThatContains($message, $level) + { + return $this->hasRecordThatPasses(function ($rec) use ($message) { + return strpos($rec['message'], $message) !== false; + }, $level); + } + + public function hasRecordThatMatches($regex, $level) + { + return $this->hasRecordThatPasses(function ($rec) use ($regex) { + return preg_match($regex, $rec['message']) > 0; + }, $level); + } + + public function hasRecordThatPasses(callable $predicate, $level) + { + if (!isset($this->recordsByLevel[$level])) { + return false; + } + foreach ($this->recordsByLevel[$level] as $i => $rec) { + if (call_user_func($predicate, $rec, $i)) { + return true; + } + } + return false; + } + + public function __call($method, $args) + { + if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) { + $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3]; + $level = strtolower($matches[2]); + if (method_exists($this, $genericMethod)) { + $args[] = $level; + return call_user_func_array([$this, $genericMethod], $args); + } + } + throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()'); + } + + public function reset() + { + $this->records = []; + $this->recordsByLevel = []; + } +} diff --git a/vendor/psr/log/README.md b/vendor/psr/log/README.md new file mode 100644 index 0000000..a9f20c4 --- /dev/null +++ b/vendor/psr/log/README.md @@ -0,0 +1,58 @@ +PSR Log +======= + +This repository holds all interfaces/classes/traits related to +[PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). + +Note that this is not a logger of its own. It is merely an interface that +describes a logger. See the specification for more details. + +Installation +------------ + +```bash +composer require psr/log +``` + +Usage +----- + +If you need a logger, you can use the interface like this: + +```php +logger = $logger; + } + + public function doSomething() + { + if ($this->logger) { + $this->logger->info('Doing work'); + } + + try { + $this->doSomethingElse(); + } catch (Exception $exception) { + $this->logger->error('Oh no!', array('exception' => $exception)); + } + + // do something useful + } +} +``` + +You can then pick one of the implementations of the interface to get a logger. + +If you want to implement the interface, you can require this package and +implement `Psr\Log\LoggerInterface` in your code. Please read the +[specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) +for details. diff --git a/vendor/psr/log/composer.json b/vendor/psr/log/composer.json new file mode 100644 index 0000000..ca05695 --- /dev/null +++ b/vendor/psr/log/composer.json @@ -0,0 +1,26 @@ +{ + "name": "psr/log", + "description": "Common interface for logging libraries", + "keywords": ["psr", "psr-3", "log"], + "homepage": "https://github.com/php-fig/log", + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + } +} diff --git a/vendor/psr/simple-cache/.editorconfig b/vendor/psr/simple-cache/.editorconfig new file mode 100644 index 0000000..48542cb --- /dev/null +++ b/vendor/psr/simple-cache/.editorconfig @@ -0,0 +1,12 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/vendor/psr/simple-cache/LICENSE.md b/vendor/psr/simple-cache/LICENSE.md new file mode 100644 index 0000000..e49a7c8 --- /dev/null +++ b/vendor/psr/simple-cache/LICENSE.md @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright (c) 2016 PHP Framework Interoperability Group + +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +> THE SOFTWARE. diff --git a/vendor/psr/simple-cache/README.md b/vendor/psr/simple-cache/README.md new file mode 100644 index 0000000..43641d1 --- /dev/null +++ b/vendor/psr/simple-cache/README.md @@ -0,0 +1,8 @@ +PHP FIG Simple Cache PSR +======================== + +This repository holds all interfaces related to PSR-16. + +Note that this is not a cache implementation of its own. It is merely an interface that describes a cache implementation. See [the specification](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-16-simple-cache.md) for more details. + +You can find implementations of the specification by looking for packages providing the [psr/simple-cache-implementation](https://packagist.org/providers/psr/simple-cache-implementation) virtual package. diff --git a/vendor/psr/simple-cache/composer.json b/vendor/psr/simple-cache/composer.json new file mode 100644 index 0000000..2978fa5 --- /dev/null +++ b/vendor/psr/simple-cache/composer.json @@ -0,0 +1,25 @@ +{ + "name": "psr/simple-cache", + "description": "Common interfaces for simple caching", + "keywords": ["psr", "psr-16", "cache", "simple-cache", "caching"], + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/vendor/psr/simple-cache/src/CacheException.php b/vendor/psr/simple-cache/src/CacheException.php new file mode 100644 index 0000000..eba5381 --- /dev/null +++ b/vendor/psr/simple-cache/src/CacheException.php @@ -0,0 +1,10 @@ + value pairs. Cache keys that do not exist or are stale will have $default as value. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if $keys is neither an array nor a Traversable, + * or if any of the $keys are not a legal value. + */ + public function getMultiple($keys, $default = null); + + /** + * Persists a set of key => value pairs in the cache, with an optional TTL. + * + * @param iterable $values A list of key => value pairs for a multiple-set operation. + * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and + * the driver supports TTL then the library may set a default value + * for it or let the driver take care of that. + * + * @return bool True on success and false on failure. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if $values is neither an array nor a Traversable, + * or if any of the $values are not a legal value. + */ + public function setMultiple($values, $ttl = null); + + /** + * Deletes multiple cache items in a single operation. + * + * @param iterable $keys A list of string-based keys to be deleted. + * + * @return bool True if the items were successfully removed. False if there was an error. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if $keys is neither an array nor a Traversable, + * or if any of the $keys are not a legal value. + */ + public function deleteMultiple($keys); + + /** + * Determines whether an item is present in the cache. + * + * NOTE: It is recommended that has() is only to be used for cache warming type purposes + * and not to be used within your live applications operations for get/set, as this method + * is subject to a race condition where your has() will return true and immediately after, + * another script can remove it making the state of your app out of date. + * + * @param string $key The cache item key. + * + * @return bool + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if the $key string is not a legal value. + */ + public function has($key); +} diff --git a/vendor/psr/simple-cache/src/InvalidArgumentException.php b/vendor/psr/simple-cache/src/InvalidArgumentException.php new file mode 100644 index 0000000..6a9524a --- /dev/null +++ b/vendor/psr/simple-cache/src/InvalidArgumentException.php @@ -0,0 +1,13 @@ += 5.3. + +[![Build Status](https://travis-ci.org/ralouphie/getallheaders.svg?branch=master)](https://travis-ci.org/ralouphie/getallheaders) +[![Coverage Status](https://coveralls.io/repos/ralouphie/getallheaders/badge.png?branch=master)](https://coveralls.io/r/ralouphie/getallheaders?branch=master) +[![Latest Stable Version](https://poser.pugx.org/ralouphie/getallheaders/v/stable.png)](https://packagist.org/packages/ralouphie/getallheaders) +[![Latest Unstable Version](https://poser.pugx.org/ralouphie/getallheaders/v/unstable.png)](https://packagist.org/packages/ralouphie/getallheaders) +[![License](https://poser.pugx.org/ralouphie/getallheaders/license.png)](https://packagist.org/packages/ralouphie/getallheaders) + + +This is a simple polyfill for [`getallheaders()`](http://www.php.net/manual/en/function.getallheaders.php). + +## Install + +For PHP version **`>= 5.6`**: + +``` +composer require ralouphie/getallheaders +``` + +For PHP version **`< 5.6`**: + +``` +composer require ralouphie/getallheaders "^2" +``` diff --git a/vendor/ralouphie/getallheaders/composer.json b/vendor/ralouphie/getallheaders/composer.json new file mode 100644 index 0000000..de8ce62 --- /dev/null +++ b/vendor/ralouphie/getallheaders/composer.json @@ -0,0 +1,26 @@ +{ + "name": "ralouphie/getallheaders", + "description": "A polyfill for getallheaders.", + "license": "MIT", + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "require": { + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "^5 || ^6.5", + "php-coveralls/php-coveralls": "^2.1" + }, + "autoload": { + "files": ["src/getallheaders.php"] + }, + "autoload-dev": { + "psr-4": { + "getallheaders\\Tests\\": "tests/" + } + } +} diff --git a/vendor/ralouphie/getallheaders/src/getallheaders.php b/vendor/ralouphie/getallheaders/src/getallheaders.php new file mode 100644 index 0000000..c7285a5 --- /dev/null +++ b/vendor/ralouphie/getallheaders/src/getallheaders.php @@ -0,0 +1,46 @@ + 'Content-Type', + 'CONTENT_LENGTH' => 'Content-Length', + 'CONTENT_MD5' => 'Content-Md5', + ); + + foreach ($_SERVER as $key => $value) { + if (substr($key, 0, 5) === 'HTTP_') { + $key = substr($key, 5); + if (!isset($copy_server[$key]) || !isset($_SERVER[$key])) { + $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $key)))); + $headers[$key] = $value; + } + } elseif (isset($copy_server[$key])) { + $headers[$copy_server[$key]] = $value; + } + } + + if (!isset($headers['Authorization'])) { + if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { + $headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; + } elseif (isset($_SERVER['PHP_AUTH_USER'])) { + $basic_pass = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : ''; + $headers['Authorization'] = 'Basic ' . base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $basic_pass); + } elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) { + $headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST']; + } + } + + return $headers; + } + +} diff --git a/vendor/symfony/cache-contracts/.gitignore b/vendor/symfony/cache-contracts/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/vendor/symfony/cache-contracts/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/vendor/symfony/cache-contracts/CHANGELOG.md b/vendor/symfony/cache-contracts/CHANGELOG.md new file mode 100644 index 0000000..7932e26 --- /dev/null +++ b/vendor/symfony/cache-contracts/CHANGELOG.md @@ -0,0 +1,5 @@ +CHANGELOG +========= + +The changelog is maintained for all Symfony contracts at the following URL: +https://github.com/symfony/contracts/blob/main/CHANGELOG.md diff --git a/vendor/symfony/cache-contracts/CacheInterface.php b/vendor/symfony/cache-contracts/CacheInterface.php new file mode 100644 index 0000000..67e4dfd --- /dev/null +++ b/vendor/symfony/cache-contracts/CacheInterface.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Cache; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\InvalidArgumentException; + +/** + * Covers most simple to advanced caching needs. + * + * @author Nicolas Grekas + */ +interface CacheInterface +{ + /** + * Fetches a value from the pool or computes it if not found. + * + * On cache misses, a callback is called that should return the missing value. + * This callback is given a PSR-6 CacheItemInterface instance corresponding to the + * requested key, that could be used e.g. for expiration control. It could also + * be an ItemInterface instance when its additional features are needed. + * + * @param string $key The key of the item to retrieve from the cache + * @param callable|CallbackInterface $callback Should return the computed value for the given key/item + * @param float|null $beta A float that, as it grows, controls the likeliness of triggering + * early expiration. 0 disables it, INF forces immediate expiration. + * The default (or providing null) is implementation dependent but should + * typically be 1.0, which should provide optimal stampede protection. + * See https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration + * @param array &$metadata The metadata of the cached item {@see ItemInterface::getMetadata()} + * + * @return mixed + * + * @throws InvalidArgumentException When $key is not valid or when $beta is negative + */ + public function get(string $key, callable $callback, float $beta = null, array &$metadata = null); + + /** + * Removes an item from the pool. + * + * @param string $key The key to delete + * + * @throws InvalidArgumentException When $key is not valid + * + * @return bool True if the item was successfully removed, false if there was any error + */ + public function delete(string $key): bool; +} diff --git a/vendor/symfony/cache-contracts/CacheTrait.php b/vendor/symfony/cache-contracts/CacheTrait.php new file mode 100644 index 0000000..d340e06 --- /dev/null +++ b/vendor/symfony/cache-contracts/CacheTrait.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Cache; + +use Psr\Cache\CacheItemPoolInterface; +use Psr\Cache\InvalidArgumentException; +use Psr\Log\LoggerInterface; + +// Help opcache.preload discover always-needed symbols +class_exists(InvalidArgumentException::class); + +/** + * An implementation of CacheInterface for PSR-6 CacheItemPoolInterface classes. + * + * @author Nicolas Grekas + */ +trait CacheTrait +{ + /** + * {@inheritdoc} + * + * @return mixed + */ + public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) + { + return $this->doGet($this, $key, $callback, $beta, $metadata); + } + + /** + * {@inheritdoc} + */ + public function delete(string $key): bool + { + return $this->deleteItem($key); + } + + private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback, ?float $beta, array &$metadata = null, LoggerInterface $logger = null) + { + if (0 > $beta = $beta ?? 1.0) { + throw new class(sprintf('Argument "$beta" provided to "%s::get()" must be a positive number, %f given.', static::class, $beta)) extends \InvalidArgumentException implements InvalidArgumentException { }; + } + + $item = $pool->getItem($key); + $recompute = !$item->isHit() || \INF === $beta; + $metadata = $item instanceof ItemInterface ? $item->getMetadata() : []; + + if (!$recompute && $metadata) { + $expiry = $metadata[ItemInterface::METADATA_EXPIRY] ?? false; + $ctime = $metadata[ItemInterface::METADATA_CTIME] ?? false; + + if ($recompute = $ctime && $expiry && $expiry <= ($now = microtime(true)) - $ctime / 1000 * $beta * log(random_int(1, \PHP_INT_MAX) / \PHP_INT_MAX)) { + // force applying defaultLifetime to expiry + $item->expiresAt(null); + $logger && $logger->info('Item "{key}" elected for early recomputation {delta}s before its expiration', [ + 'key' => $key, + 'delta' => sprintf('%.1f', $expiry - $now), + ]); + } + } + + if ($recompute) { + $save = true; + $item->set($callback($item, $save)); + if ($save) { + $pool->save($item); + } + } + + return $item->get(); + } +} diff --git a/vendor/symfony/cache-contracts/CallbackInterface.php b/vendor/symfony/cache-contracts/CallbackInterface.php new file mode 100644 index 0000000..7dae2aa --- /dev/null +++ b/vendor/symfony/cache-contracts/CallbackInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Cache; + +use Psr\Cache\CacheItemInterface; + +/** + * Computes and returns the cached value of an item. + * + * @author Nicolas Grekas + */ +interface CallbackInterface +{ + /** + * @param CacheItemInterface|ItemInterface $item The item to compute the value for + * @param bool &$save Should be set to false when the value should not be saved in the pool + * + * @return mixed The computed value for the passed item + */ + public function __invoke(CacheItemInterface $item, bool &$save); +} diff --git a/vendor/symfony/cache-contracts/ItemInterface.php b/vendor/symfony/cache-contracts/ItemInterface.php new file mode 100644 index 0000000..10c0488 --- /dev/null +++ b/vendor/symfony/cache-contracts/ItemInterface.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Cache; + +use Psr\Cache\CacheException; +use Psr\Cache\CacheItemInterface; +use Psr\Cache\InvalidArgumentException; + +/** + * Augments PSR-6's CacheItemInterface with support for tags and metadata. + * + * @author Nicolas Grekas + */ +interface ItemInterface extends CacheItemInterface +{ + /** + * References the Unix timestamp stating when the item will expire. + */ + public const METADATA_EXPIRY = 'expiry'; + + /** + * References the time the item took to be created, in milliseconds. + */ + public const METADATA_CTIME = 'ctime'; + + /** + * References the list of tags that were assigned to the item, as string[]. + */ + public const METADATA_TAGS = 'tags'; + + /** + * Reserved characters that cannot be used in a key or tag. + */ + public const RESERVED_CHARACTERS = '{}()/\@:'; + + /** + * Adds a tag to a cache item. + * + * Tags are strings that follow the same validation rules as keys. + * + * @param string|string[] $tags A tag or array of tags + * + * @return $this + * + * @throws InvalidArgumentException When $tag is not valid + * @throws CacheException When the item comes from a pool that is not tag-aware + */ + public function tag($tags): self; + + /** + * Returns a list of metadata info that were saved alongside with the cached value. + * + * See ItemInterface::METADATA_* consts for keys potentially found in the returned array. + */ + public function getMetadata(): array; +} diff --git a/vendor/symfony/cache-contracts/LICENSE b/vendor/symfony/cache-contracts/LICENSE new file mode 100644 index 0000000..74cdc2d --- /dev/null +++ b/vendor/symfony/cache-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2022 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/cache-contracts/README.md b/vendor/symfony/cache-contracts/README.md new file mode 100644 index 0000000..7085a69 --- /dev/null +++ b/vendor/symfony/cache-contracts/README.md @@ -0,0 +1,9 @@ +Symfony Cache Contracts +======================= + +A set of abstractions extracted out of the Symfony components. + +Can be used to build on semantics that the Symfony components proved useful - and +that already have battle tested implementations. + +See https://github.com/symfony/contracts/blob/main/README.md for more information. diff --git a/vendor/symfony/cache-contracts/TagAwareCacheInterface.php b/vendor/symfony/cache-contracts/TagAwareCacheInterface.php new file mode 100644 index 0000000..7c4cf11 --- /dev/null +++ b/vendor/symfony/cache-contracts/TagAwareCacheInterface.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Cache; + +use Psr\Cache\InvalidArgumentException; + +/** + * Allows invalidating cached items using tags. + * + * @author Nicolas Grekas + */ +interface TagAwareCacheInterface extends CacheInterface +{ + /** + * Invalidates cached items using tags. + * + * When implemented on a PSR-6 pool, invalidation should not apply + * to deferred items. Instead, they should be committed as usual. + * This allows replacing old tagged values by new ones without + * race conditions. + * + * @param string[] $tags An array of tags to invalidate + * + * @return bool True on success + * + * @throws InvalidArgumentException When $tags is not valid + */ + public function invalidateTags(array $tags); +} diff --git a/vendor/symfony/cache-contracts/composer.json b/vendor/symfony/cache-contracts/composer.json new file mode 100644 index 0000000..9f45e17 --- /dev/null +++ b/vendor/symfony/cache-contracts/composer.json @@ -0,0 +1,38 @@ +{ + "name": "symfony/cache-contracts", + "type": "library", + "description": "Generic abstractions related to caching", + "keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "psr/cache": "^1.0|^2.0|^3.0" + }, + "suggest": { + "symfony/cache-implementation": "" + }, + "autoload": { + "psr-4": { "Symfony\\Contracts\\Cache\\": "" } + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + } +} diff --git a/vendor/symfony/cache/Adapter/AbstractAdapter.php b/vendor/symfony/cache/Adapter/AbstractAdapter.php new file mode 100644 index 0000000..de5af17 --- /dev/null +++ b/vendor/symfony/cache/Adapter/AbstractAdapter.php @@ -0,0 +1,208 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\AbstractAdapterTrait; +use Symfony\Component\Cache\Traits\ContractsTrait; +use Symfony\Contracts\Cache\CacheInterface; + +/** + * @author Nicolas Grekas + */ +abstract class AbstractAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface +{ + use AbstractAdapterTrait; + use ContractsTrait; + + /** + * @internal + */ + protected const NS_SEPARATOR = ':'; + + private static $apcuSupported; + private static $phpFilesSupported; + + protected function __construct(string $namespace = '', int $defaultLifetime = 0) + { + $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).static::NS_SEPARATOR; + $this->defaultLifetime = $defaultLifetime; + if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) { + throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace)); + } + self::$createCacheItem ?? self::$createCacheItem = \Closure::bind( + static function ($key, $value, $isHit) { + $item = new CacheItem(); + $item->key = $key; + $item->value = $v = $value; + $item->isHit = $isHit; + // Detect wrapped values that encode for their expiry and creation duration + // For compactness, these values are packed in the key of an array using + // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F + if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = (string) array_key_first($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) { + $item->value = $v[$k]; + $v = unpack('Ve/Nc', substr($k, 1, -1)); + $item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET; + $item->metadata[CacheItem::METADATA_CTIME] = $v['c']; + } + + return $item; + }, + null, + CacheItem::class + ); + self::$mergeByLifetime ?? self::$mergeByLifetime = \Closure::bind( + static function ($deferred, $namespace, &$expiredIds, $getId, $defaultLifetime) { + $byLifetime = []; + $now = microtime(true); + $expiredIds = []; + + foreach ($deferred as $key => $item) { + $key = (string) $key; + if (null === $item->expiry) { + $ttl = 0 < $defaultLifetime ? $defaultLifetime : 0; + } elseif (!$item->expiry) { + $ttl = 0; + } elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) { + $expiredIds[] = $getId($key); + continue; + } + if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) { + unset($metadata[CacheItem::METADATA_TAGS]); + } + // For compactness, expiry and creation duration are packed in the key of an array, using magic numbers as separators + $byLifetime[$ttl][$getId($key)] = $metadata ? ["\x9D".pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME])."\x5F" => $item->value] : $item->value; + } + + return $byLifetime; + }, + null, + CacheItem::class + ); + } + + /** + * Returns the best possible adapter that your runtime supports. + * + * Using ApcuAdapter makes system caches compatible with read-only filesystems. + * + * @return AdapterInterface + */ + public static function createSystemCache(string $namespace, int $defaultLifetime, string $version, string $directory, ?LoggerInterface $logger = null) + { + $opcache = new PhpFilesAdapter($namespace, $defaultLifetime, $directory, true); + if (null !== $logger) { + $opcache->setLogger($logger); + } + + if (!self::$apcuSupported = self::$apcuSupported ?? ApcuAdapter::isSupported()) { + return $opcache; + } + + if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN)) { + return $opcache; + } + + $apcu = new ApcuAdapter($namespace, intdiv($defaultLifetime, 5), $version); + if (null !== $logger) { + $apcu->setLogger($logger); + } + + return new ChainAdapter([$apcu, $opcache]); + } + + public static function createConnection(string $dsn, array $options = []) + { + if (str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:')) { + return RedisAdapter::createConnection($dsn, $options); + } + if (str_starts_with($dsn, 'memcached:')) { + return MemcachedAdapter::createConnection($dsn, $options); + } + if (0 === strpos($dsn, 'couchbase:')) { + if (CouchbaseBucketAdapter::isSupported()) { + return CouchbaseBucketAdapter::createConnection($dsn, $options); + } + + return CouchbaseCollectionAdapter::createConnection($dsn, $options); + } + + throw new InvalidArgumentException('Unsupported DSN: it does not start with "redis[s]:", "memcached:" nor "couchbase:".'); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function commit() + { + $ok = true; + $byLifetime = (self::$mergeByLifetime)($this->deferred, $this->namespace, $expiredIds, \Closure::fromCallable([$this, 'getId']), $this->defaultLifetime); + $retry = $this->deferred = []; + + if ($expiredIds) { + try { + $this->doDelete($expiredIds); + } catch (\Exception $e) { + $ok = false; + CacheItem::log($this->logger, 'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]); + } + } + foreach ($byLifetime as $lifetime => $values) { + try { + $e = $this->doSave($values, $lifetime); + } catch (\Exception $e) { + } + if (true === $e || [] === $e) { + continue; + } + if (\is_array($e) || 1 === \count($values)) { + foreach (\is_array($e) ? $e : array_keys($values) as $id) { + $ok = false; + $v = $values[$id]; + $type = get_debug_type($v); + $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.'); + CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]); + } + } else { + foreach ($values as $id => $v) { + $retry[$lifetime][] = $id; + } + } + } + + // When bulk-save failed, retry each item individually + foreach ($retry as $lifetime => $ids) { + foreach ($ids as $id) { + try { + $v = $byLifetime[$lifetime][$id]; + $e = $this->doSave([$id => $v], $lifetime); + } catch (\Exception $e) { + } + if (true === $e || [] === $e) { + continue; + } + $ok = false; + $type = get_debug_type($v); + $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.'); + CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]); + } + } + + return $ok; + } +} diff --git a/vendor/symfony/cache/Adapter/AbstractTagAwareAdapter.php b/vendor/symfony/cache/Adapter/AbstractTagAwareAdapter.php new file mode 100644 index 0000000..a384b16 --- /dev/null +++ b/vendor/symfony/cache/Adapter/AbstractTagAwareAdapter.php @@ -0,0 +1,330 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Log\LoggerAwareInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\AbstractAdapterTrait; +use Symfony\Component\Cache\Traits\ContractsTrait; +use Symfony\Contracts\Cache\TagAwareCacheInterface; + +/** + * Abstract for native TagAware adapters. + * + * To keep info on tags, the tags are both serialized as part of cache value and provided as tag ids + * to Adapters on operations when needed for storage to doSave(), doDelete() & doInvalidate(). + * + * @author Nicolas Grekas + * @author André Rømcke + * + * @internal + */ +abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, LoggerAwareInterface, ResettableInterface +{ + use AbstractAdapterTrait; + use ContractsTrait; + + private const TAGS_PREFIX = "\0tags\0"; + + protected function __construct(string $namespace = '', int $defaultLifetime = 0) + { + $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':'; + $this->defaultLifetime = $defaultLifetime; + if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) { + throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace)); + } + self::$createCacheItem ?? self::$createCacheItem = \Closure::bind( + static function ($key, $value, $isHit) { + $item = new CacheItem(); + $item->key = $key; + $item->isTaggable = true; + // If structure does not match what we expect return item as is (no value and not a hit) + if (!\is_array($value) || !\array_key_exists('value', $value)) { + return $item; + } + $item->isHit = $isHit; + // Extract value, tags and meta data from the cache value + $item->value = $value['value']; + $item->metadata[CacheItem::METADATA_TAGS] = $value['tags'] ?? []; + if (isset($value['meta'])) { + // For compactness these values are packed, & expiry is offset to reduce size + $v = unpack('Ve/Nc', $value['meta']); + $item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET; + $item->metadata[CacheItem::METADATA_CTIME] = $v['c']; + } + + return $item; + }, + null, + CacheItem::class + ); + self::$mergeByLifetime ?? self::$mergeByLifetime = \Closure::bind( + static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime) { + $byLifetime = []; + $now = microtime(true); + $expiredIds = []; + + foreach ($deferred as $key => $item) { + $key = (string) $key; + if (null === $item->expiry) { + $ttl = 0 < $defaultLifetime ? $defaultLifetime : 0; + } elseif (!$item->expiry) { + $ttl = 0; + } elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) { + $expiredIds[] = $getId($key); + continue; + } + // Store Value and Tags on the cache value + if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) { + $value = ['value' => $item->value, 'tags' => $metadata[CacheItem::METADATA_TAGS]]; + unset($metadata[CacheItem::METADATA_TAGS]); + } else { + $value = ['value' => $item->value, 'tags' => []]; + } + + if ($metadata) { + // For compactness, expiry and creation duration are packed, using magic numbers as separators + $value['meta'] = pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME]); + } + + // Extract tag changes, these should be removed from values in doSave() + $value['tag-operations'] = ['add' => [], 'remove' => []]; + $oldTags = $item->metadata[CacheItem::METADATA_TAGS] ?? []; + foreach (array_diff($value['tags'], $oldTags) as $addedTag) { + $value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag); + } + foreach (array_diff($oldTags, $value['tags']) as $removedTag) { + $value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag); + } + + $byLifetime[$ttl][$getId($key)] = $value; + $item->metadata = $item->newMetadata; + } + + return $byLifetime; + }, + null, + CacheItem::class + ); + } + + /** + * Persists several cache items immediately. + * + * @param array $values The values to cache, indexed by their cache identifier + * @param int $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning + * @param array[] $addTagData Hash where key is tag id, and array value is list of cache id's to add to tag + * @param array[] $removeTagData Hash where key is tag id, and array value is list of cache id's to remove to tag + * + * @return array The identifiers that failed to be cached or a boolean stating if caching succeeded or not + */ + abstract protected function doSave(array $values, int $lifetime, array $addTagData = [], array $removeTagData = []): array; + + /** + * Removes multiple items from the pool and their corresponding tags. + * + * @param array $ids An array of identifiers that should be removed from the pool + * + * @return bool + */ + abstract protected function doDelete(array $ids); + + /** + * Removes relations between tags and deleted items. + * + * @param array $tagData Array of tag => key identifiers that should be removed from the pool + */ + abstract protected function doDeleteTagRelations(array $tagData): bool; + + /** + * Invalidates cached items using tags. + * + * @param string[] $tagIds An array of tags to invalidate, key is tag and value is tag id + */ + abstract protected function doInvalidate(array $tagIds): bool; + + /** + * Delete items and yields the tags they were bound to. + */ + protected function doDeleteYieldTags(array $ids): iterable + { + foreach ($this->doFetch($ids) as $id => $value) { + yield $id => \is_array($value) && \is_array($value['tags'] ?? null) ? $value['tags'] : []; + } + + $this->doDelete($ids); + } + + /** + * {@inheritdoc} + */ + public function commit(): bool + { + $ok = true; + $byLifetime = (self::$mergeByLifetime)($this->deferred, $expiredIds, \Closure::fromCallable([$this, 'getId']), self::TAGS_PREFIX, $this->defaultLifetime); + $retry = $this->deferred = []; + + if ($expiredIds) { + // Tags are not cleaned up in this case, however that is done on invalidateTags(). + try { + $this->doDelete($expiredIds); + } catch (\Exception $e) { + $ok = false; + CacheItem::log($this->logger, 'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]); + } + } + foreach ($byLifetime as $lifetime => $values) { + try { + $values = $this->extractTagData($values, $addTagData, $removeTagData); + $e = $this->doSave($values, $lifetime, $addTagData, $removeTagData); + } catch (\Exception $e) { + } + if (true === $e || [] === $e) { + continue; + } + if (\is_array($e) || 1 === \count($values)) { + foreach (\is_array($e) ? $e : array_keys($values) as $id) { + $ok = false; + $v = $values[$id]; + $type = get_debug_type($v); + $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.'); + CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]); + } + } else { + foreach ($values as $id => $v) { + $retry[$lifetime][] = $id; + } + } + } + + // When bulk-save failed, retry each item individually + foreach ($retry as $lifetime => $ids) { + foreach ($ids as $id) { + try { + $v = $byLifetime[$lifetime][$id]; + $values = $this->extractTagData([$id => $v], $addTagData, $removeTagData); + $e = $this->doSave($values, $lifetime, $addTagData, $removeTagData); + } catch (\Exception $e) { + } + if (true === $e || [] === $e) { + continue; + } + $ok = false; + $type = get_debug_type($v); + $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.'); + CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]); + } + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + public function deleteItems(array $keys): bool + { + if (!$keys) { + return true; + } + + $ok = true; + $ids = []; + $tagData = []; + + foreach ($keys as $key) { + $ids[$key] = $this->getId($key); + unset($this->deferred[$key]); + } + + try { + foreach ($this->doDeleteYieldTags(array_values($ids)) as $id => $tags) { + foreach ($tags as $tag) { + $tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id; + } + } + } catch (\Exception $e) { + $ok = false; + } + + try { + if ((!$tagData || $this->doDeleteTagRelations($tagData)) && $ok) { + return true; + } + } catch (\Exception $e) { + } + + // When bulk-delete failed, retry each item individually + foreach ($ids as $key => $id) { + try { + $e = null; + if ($this->doDelete([$id])) { + continue; + } + } catch (\Exception $e) { + } + $message = 'Failed to delete key "{key}"'.($e instanceof \Exception ? ': '.$e->getMessage() : '.'); + CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]); + $ok = false; + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + public function invalidateTags(array $tags) + { + if (empty($tags)) { + return false; + } + + $tagIds = []; + foreach (array_unique($tags) as $tag) { + $tagIds[] = $this->getId(self::TAGS_PREFIX.$tag); + } + + try { + if ($this->doInvalidate($tagIds)) { + return true; + } + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to invalidate tags: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]); + } + + return false; + } + + /** + * Extracts tags operation data from $values set in mergeByLifetime, and returns values without it. + */ + private function extractTagData(array $values, ?array &$addTagData, ?array &$removeTagData): array + { + $addTagData = $removeTagData = []; + foreach ($values as $id => $value) { + foreach ($value['tag-operations']['add'] as $tag => $tagId) { + $addTagData[$tagId][] = $id; + } + + foreach ($value['tag-operations']['remove'] as $tag => $tagId) { + $removeTagData[$tagId][] = $id; + } + + unset($values[$id]['tag-operations']); + } + + return $values; + } +} diff --git a/vendor/symfony/cache/Adapter/AdapterInterface.php b/vendor/symfony/cache/Adapter/AdapterInterface.php new file mode 100644 index 0000000..f8dce86 --- /dev/null +++ b/vendor/symfony/cache/Adapter/AdapterInterface.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; + +// Help opcache.preload discover always-needed symbols +class_exists(CacheItem::class); + +/** + * Interface for adapters managing instances of Symfony's CacheItem. + * + * @author Kévin Dunglas + */ +interface AdapterInterface extends CacheItemPoolInterface +{ + /** + * {@inheritdoc} + * + * @return CacheItem + */ + public function getItem($key); + + /** + * {@inheritdoc} + * + * @return \Traversable + */ + public function getItems(array $keys = []); + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = ''); +} diff --git a/vendor/symfony/cache/Adapter/ApcuAdapter.php b/vendor/symfony/cache/Adapter/ApcuAdapter.php new file mode 100644 index 0000000..639e314 --- /dev/null +++ b/vendor/symfony/cache/Adapter/ApcuAdapter.php @@ -0,0 +1,138 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Nicolas Grekas + */ +class ApcuAdapter extends AbstractAdapter +{ + private $marshaller; + + /** + * @throws CacheException if APCu is not enabled + */ + public function __construct(string $namespace = '', int $defaultLifetime = 0, ?string $version = null, ?MarshallerInterface $marshaller = null) + { + if (!static::isSupported()) { + throw new CacheException('APCu is not enabled.'); + } + if ('cli' === \PHP_SAPI) { + ini_set('apc.use_request_time', 0); + } + parent::__construct($namespace, $defaultLifetime); + + if (null !== $version) { + CacheItem::validateKey($version); + + if (!apcu_exists($version.'@'.$namespace)) { + $this->doClear($namespace); + apcu_add($version.'@'.$namespace, null); + } + } + + $this->marshaller = $marshaller; + } + + public static function isSupported() + { + return \function_exists('apcu_fetch') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOLEAN); + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback'); + try { + $values = []; + $ids = array_flip($ids); + foreach (apcu_fetch(array_keys($ids), $ok) ?: [] as $k => $v) { + if (!isset($ids[$k])) { + // work around https://github.com/krakjoe/apcu/issues/247 + $k = key($ids); + } + unset($ids[$k]); + + if (null !== $v || $ok) { + $values[$k] = null !== $this->marshaller ? $this->marshaller->unmarshall($v) : $v; + } + } + + return $values; + } catch (\Error $e) { + throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine()); + } finally { + ini_set('unserialize_callback_func', $unserializeCallbackHandler); + } + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id) + { + return apcu_exists($id); + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace) + { + return isset($namespace[0]) && class_exists(\APCUIterator::class, false) && ('cli' !== \PHP_SAPI || filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN)) + ? apcu_delete(new \APCUIterator(sprintf('/^%s/', preg_quote($namespace, '/')), \APC_ITER_KEY)) + : apcu_clear_cache(); + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + foreach ($ids as $id) { + apcu_delete($id); + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + if (null !== $this->marshaller && (!$values = $this->marshaller->marshall($values, $failed))) { + return $failed; + } + + try { + if (false === $failures = apcu_store($values, null, $lifetime)) { + $failures = $values; + } + + return array_keys($failures); + } catch (\Throwable $e) { + if (1 === \count($values)) { + // Workaround https://github.com/krakjoe/apcu/issues/170 + apcu_delete(array_key_first($values)); + } + + throw $e; + } + } +} diff --git a/vendor/symfony/cache/Adapter/ArrayAdapter.php b/vendor/symfony/cache/Adapter/ArrayAdapter.php new file mode 100644 index 0000000..b251814 --- /dev/null +++ b/vendor/symfony/cache/Adapter/ArrayAdapter.php @@ -0,0 +1,407 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Contracts\Cache\CacheInterface; + +/** + * An in-memory cache storage. + * + * Acts as a least-recently-used (LRU) storage when configured with a maximum number of items. + * + * @author Nicolas Grekas + */ +class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface +{ + use LoggerAwareTrait; + + private $storeSerialized; + private $values = []; + private $expiries = []; + private $defaultLifetime; + private $maxLifetime; + private $maxItems; + + private static $createCacheItem; + + /** + * @param bool $storeSerialized Disabling serialization can lead to cache corruptions when storing mutable values but increases performance otherwise + */ + public function __construct(int $defaultLifetime = 0, bool $storeSerialized = true, float $maxLifetime = 0, int $maxItems = 0) + { + if (0 > $maxLifetime) { + throw new InvalidArgumentException(sprintf('Argument $maxLifetime must be positive, %F passed.', $maxLifetime)); + } + + if (0 > $maxItems) { + throw new InvalidArgumentException(sprintf('Argument $maxItems must be a positive integer, %d passed.', $maxItems)); + } + + $this->defaultLifetime = $defaultLifetime; + $this->storeSerialized = $storeSerialized; + $this->maxLifetime = $maxLifetime; + $this->maxItems = $maxItems; + self::$createCacheItem ?? self::$createCacheItem = \Closure::bind( + static function ($key, $value, $isHit) { + $item = new CacheItem(); + $item->key = $key; + $item->value = $value; + $item->isHit = $isHit; + + return $item; + }, + null, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null) + { + $item = $this->getItem($key); + $metadata = $item->getMetadata(); + + // ArrayAdapter works in memory, we don't care about stampede protection + if (\INF === $beta || !$item->isHit()) { + $save = true; + $item->set($callback($item, $save)); + if ($save) { + $this->save($item); + } + } + + return $item->get(); + } + + /** + * {@inheritdoc} + */ + public function delete(string $key): bool + { + return $this->deleteItem($key); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function hasItem($key) + { + if (\is_string($key) && isset($this->expiries[$key]) && $this->expiries[$key] > microtime(true)) { + if ($this->maxItems) { + // Move the item last in the storage + $value = $this->values[$key]; + unset($this->values[$key]); + $this->values[$key] = $value; + } + + return true; + } + \assert('' !== CacheItem::validateKey($key)); + + return isset($this->expiries[$key]) && !$this->deleteItem($key); + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + if (!$isHit = $this->hasItem($key)) { + $value = null; + + if (!$this->maxItems) { + // Track misses in non-LRU mode only + $this->values[$key] = null; + } + } else { + $value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key]; + } + + return (self::$createCacheItem)($key, $value, $isHit); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = []) + { + \assert(self::validateKeys($keys)); + + return $this->generateItems($keys, microtime(true), self::$createCacheItem); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItem($key) + { + \assert('' !== CacheItem::validateKey($key)); + unset($this->values[$key], $this->expiries[$key]); + + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItems(array $keys) + { + foreach ($keys as $key) { + $this->deleteItem($key); + } + + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function save(CacheItemInterface $item) + { + if (!$item instanceof CacheItem) { + return false; + } + $item = (array) $item; + $key = $item["\0*\0key"]; + $value = $item["\0*\0value"]; + $expiry = $item["\0*\0expiry"]; + + $now = microtime(true); + + if (null !== $expiry) { + if (!$expiry) { + $expiry = \PHP_INT_MAX; + } elseif ($expiry <= $now) { + $this->deleteItem($key); + + return true; + } + } + if ($this->storeSerialized && null === $value = $this->freeze($value, $key)) { + return false; + } + if (null === $expiry && 0 < $this->defaultLifetime) { + $expiry = $this->defaultLifetime; + $expiry = $now + ($expiry > ($this->maxLifetime ?: $expiry) ? $this->maxLifetime : $expiry); + } elseif ($this->maxLifetime && (null === $expiry || $expiry > $now + $this->maxLifetime)) { + $expiry = $now + $this->maxLifetime; + } + + if ($this->maxItems) { + unset($this->values[$key]); + + // Iterate items and vacuum expired ones while we are at it + foreach ($this->values as $k => $v) { + if ($this->expiries[$k] > $now && \count($this->values) < $this->maxItems) { + break; + } + + unset($this->values[$k], $this->expiries[$k]); + } + } + + $this->values[$key] = $value; + $this->expiries[$key] = $expiry ?? \PHP_INT_MAX; + + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function saveDeferred(CacheItemInterface $item) + { + return $this->save($item); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function commit() + { + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = '') + { + if ('' !== $prefix) { + $now = microtime(true); + + foreach ($this->values as $key => $value) { + if (!isset($this->expiries[$key]) || $this->expiries[$key] <= $now || 0 === strpos($key, $prefix)) { + unset($this->values[$key], $this->expiries[$key]); + } + } + + if ($this->values) { + return true; + } + } + + $this->values = $this->expiries = []; + + return true; + } + + /** + * Returns all cached values, with cache miss as null. + * + * @return array + */ + public function getValues() + { + if (!$this->storeSerialized) { + return $this->values; + } + + $values = $this->values; + foreach ($values as $k => $v) { + if (null === $v || 'N;' === $v) { + continue; + } + if (!\is_string($v) || !isset($v[2]) || ':' !== $v[1]) { + $values[$k] = serialize($v); + } + } + + return $values; + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->clear(); + } + + private function generateItems(array $keys, float $now, \Closure $f): \Generator + { + foreach ($keys as $i => $key) { + if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] > $now || !$this->deleteItem($key))) { + $value = null; + + if (!$this->maxItems) { + // Track misses in non-LRU mode only + $this->values[$key] = null; + } + } else { + if ($this->maxItems) { + // Move the item last in the storage + $value = $this->values[$key]; + unset($this->values[$key]); + $this->values[$key] = $value; + } + + $value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key]; + } + unset($keys[$i]); + + yield $key => $f($key, $value, $isHit); + } + + foreach ($keys as $key) { + yield $key => $f($key, null, false); + } + } + + private function freeze($value, string $key) + { + if (null === $value) { + return 'N;'; + } + if (\is_string($value)) { + // Serialize strings if they could be confused with serialized objects or arrays + if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) { + return serialize($value); + } + } elseif (!\is_scalar($value)) { + try { + $serialized = serialize($value); + } catch (\Exception $e) { + unset($this->values[$key]); + $type = get_debug_type($value); + $message = sprintf('Failed to save key "{key}" of type %s: %s', $type, $e->getMessage()); + CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]); + + return; + } + // Keep value serialized if it contains any objects or any internal references + if ('C' === $serialized[0] || 'O' === $serialized[0] || preg_match('/;[OCRr]:[1-9]/', $serialized)) { + return $serialized; + } + } + + return $value; + } + + private function unfreeze(string $key, bool &$isHit) + { + if ('N;' === $value = $this->values[$key]) { + return null; + } + if (\is_string($value) && isset($value[2]) && ':' === $value[1]) { + try { + $value = unserialize($value); + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to unserialize key "{key}": '.$e->getMessage(), ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]); + $value = false; + } + if (false === $value) { + $value = null; + $isHit = false; + + if (!$this->maxItems) { + $this->values[$key] = null; + } + } + } + + return $value; + } + + private function validateKeys(array $keys): bool + { + foreach ($keys as $key) { + if (!\is_string($key) || !isset($this->expiries[$key])) { + CacheItem::validateKey($key); + } + } + + return true; + } +} diff --git a/vendor/symfony/cache/Adapter/ChainAdapter.php b/vendor/symfony/cache/Adapter/ChainAdapter.php new file mode 100644 index 0000000..7d95528 --- /dev/null +++ b/vendor/symfony/cache/Adapter/ChainAdapter.php @@ -0,0 +1,342 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\ContractsTrait; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Service\ResetInterface; + +/** + * Chains several adapters together. + * + * Cached items are fetched from the first adapter having them in its data store. + * They are saved and deleted in all adapters at once. + * + * @author Kévin Dunglas + */ +class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface +{ + use ContractsTrait; + + private $adapters = []; + private $adapterCount; + private $defaultLifetime; + + private static $syncItem; + + /** + * @param CacheItemPoolInterface[] $adapters The ordered list of adapters used to fetch cached items + * @param int $defaultLifetime The default lifetime of items propagated from lower adapters to upper ones + */ + public function __construct(array $adapters, int $defaultLifetime = 0) + { + if (!$adapters) { + throw new InvalidArgumentException('At least one adapter must be specified.'); + } + + foreach ($adapters as $adapter) { + if (!$adapter instanceof CacheItemPoolInterface) { + throw new InvalidArgumentException(sprintf('The class "%s" does not implement the "%s" interface.', get_debug_type($adapter), CacheItemPoolInterface::class)); + } + if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && $adapter instanceof ApcuAdapter && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN)) { + continue; // skip putting APCu in the chain when the backend is disabled + } + + if ($adapter instanceof AdapterInterface) { + $this->adapters[] = $adapter; + } else { + $this->adapters[] = new ProxyAdapter($adapter); + } + } + $this->adapterCount = \count($this->adapters); + $this->defaultLifetime = $defaultLifetime; + + self::$syncItem ?? self::$syncItem = \Closure::bind( + static function ($sourceItem, $item, $defaultLifetime, $sourceMetadata = null) { + $sourceItem->isTaggable = false; + $sourceMetadata = $sourceMetadata ?? $sourceItem->metadata; + unset($sourceMetadata[CacheItem::METADATA_TAGS]); + + $item->value = $sourceItem->value; + $item->isHit = $sourceItem->isHit; + $item->metadata = $item->newMetadata = $sourceItem->metadata = $sourceMetadata; + + if (isset($item->metadata[CacheItem::METADATA_EXPIRY])) { + $item->expiresAt(\DateTime::createFromFormat('U.u', sprintf('%.6F', $item->metadata[CacheItem::METADATA_EXPIRY]))); + } elseif (0 < $defaultLifetime) { + $item->expiresAfter($defaultLifetime); + } + + return $item; + }, + null, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null) + { + $doSave = true; + $callback = static function (CacheItem $item, bool &$save) use ($callback, &$doSave) { + $value = $callback($item, $save); + $doSave = $save; + + return $value; + }; + + $lastItem = null; + $i = 0; + $wrap = function (?CacheItem $item = null, bool &$save = true) use ($key, $callback, $beta, &$wrap, &$i, &$doSave, &$lastItem, &$metadata) { + $adapter = $this->adapters[$i]; + if (isset($this->adapters[++$i])) { + $callback = $wrap; + $beta = \INF === $beta ? \INF : 0; + } + if ($adapter instanceof CacheInterface) { + $value = $adapter->get($key, $callback, $beta, $metadata); + } else { + $value = $this->doGet($adapter, $key, $callback, $beta, $metadata); + } + if (null !== $item) { + (self::$syncItem)($lastItem = $lastItem ?? $item, $item, $this->defaultLifetime, $metadata); + } + $save = $doSave; + + return $value; + }; + + return $wrap(); + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + $syncItem = self::$syncItem; + $misses = []; + + foreach ($this->adapters as $i => $adapter) { + $item = $adapter->getItem($key); + + if ($item->isHit()) { + while (0 <= --$i) { + $this->adapters[$i]->save($syncItem($item, $misses[$i], $this->defaultLifetime)); + } + + return $item; + } + + $misses[$i] = $item; + } + + return $item; + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = []) + { + return $this->generateItems($this->adapters[0]->getItems($keys), 0); + } + + private function generateItems(iterable $items, int $adapterIndex): \Generator + { + $missing = []; + $misses = []; + $nextAdapterIndex = $adapterIndex + 1; + $nextAdapter = $this->adapters[$nextAdapterIndex] ?? null; + + foreach ($items as $k => $item) { + if (!$nextAdapter || $item->isHit()) { + yield $k => $item; + } else { + $missing[] = $k; + $misses[$k] = $item; + } + } + + if ($missing) { + $syncItem = self::$syncItem; + $adapter = $this->adapters[$adapterIndex]; + $items = $this->generateItems($nextAdapter->getItems($missing), $nextAdapterIndex); + + foreach ($items as $k => $item) { + if ($item->isHit()) { + $adapter->save($syncItem($item, $misses[$k], $this->defaultLifetime)); + } + + yield $k => $item; + } + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function hasItem($key) + { + foreach ($this->adapters as $adapter) { + if ($adapter->hasItem($key)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = '') + { + $cleared = true; + $i = $this->adapterCount; + + while ($i--) { + if ($this->adapters[$i] instanceof AdapterInterface) { + $cleared = $this->adapters[$i]->clear($prefix) && $cleared; + } else { + $cleared = $this->adapters[$i]->clear() && $cleared; + } + } + + return $cleared; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItem($key) + { + $deleted = true; + $i = $this->adapterCount; + + while ($i--) { + $deleted = $this->adapters[$i]->deleteItem($key) && $deleted; + } + + return $deleted; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItems(array $keys) + { + $deleted = true; + $i = $this->adapterCount; + + while ($i--) { + $deleted = $this->adapters[$i]->deleteItems($keys) && $deleted; + } + + return $deleted; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function save(CacheItemInterface $item) + { + $saved = true; + $i = $this->adapterCount; + + while ($i--) { + $saved = $this->adapters[$i]->save($item) && $saved; + } + + return $saved; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function saveDeferred(CacheItemInterface $item) + { + $saved = true; + $i = $this->adapterCount; + + while ($i--) { + $saved = $this->adapters[$i]->saveDeferred($item) && $saved; + } + + return $saved; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function commit() + { + $committed = true; + $i = $this->adapterCount; + + while ($i--) { + $committed = $this->adapters[$i]->commit() && $committed; + } + + return $committed; + } + + /** + * {@inheritdoc} + */ + public function prune() + { + $pruned = true; + + foreach ($this->adapters as $adapter) { + if ($adapter instanceof PruneableInterface) { + $pruned = $adapter->prune() && $pruned; + } + } + + return $pruned; + } + + /** + * {@inheritdoc} + */ + public function reset() + { + foreach ($this->adapters as $adapter) { + if ($adapter instanceof ResetInterface) { + $adapter->reset(); + } + } + } +} diff --git a/vendor/symfony/cache/Adapter/CouchbaseBucketAdapter.php b/vendor/symfony/cache/Adapter/CouchbaseBucketAdapter.php new file mode 100644 index 0000000..84ab281 --- /dev/null +++ b/vendor/symfony/cache/Adapter/CouchbaseBucketAdapter.php @@ -0,0 +1,252 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Antonio Jose Cerezo Aranda + */ +class CouchbaseBucketAdapter extends AbstractAdapter +{ + private const THIRTY_DAYS_IN_SECONDS = 2592000; + private const MAX_KEY_LENGTH = 250; + private const KEY_NOT_FOUND = 13; + private const VALID_DSN_OPTIONS = [ + 'operationTimeout', + 'configTimeout', + 'configNodeTimeout', + 'n1qlTimeout', + 'httpTimeout', + 'configDelay', + 'htconfigIdleTimeout', + 'durabilityInterval', + 'durabilityTimeout', + ]; + + private $bucket; + private $marshaller; + + public function __construct(\CouchbaseBucket $bucket, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null) + { + if (!static::isSupported()) { + throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.'); + } + + $this->maxIdLength = static::MAX_KEY_LENGTH; + + $this->bucket = $bucket; + + parent::__construct($namespace, $defaultLifetime); + $this->enableVersioning(); + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + } + + /** + * @param array|string $servers + */ + public static function createConnection($servers, array $options = []): \CouchbaseBucket + { + if (\is_string($servers)) { + $servers = [$servers]; + } elseif (!\is_array($servers)) { + throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be array or string, "%s" given.', __METHOD__, get_debug_type($servers))); + } + + if (!static::isSupported()) { + throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.'); + } + + set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); }); + + $dsnPattern = '/^(?couchbase(?:s)?)\:\/\/(?:(?[^\:]+)\:(?[^\@]{6,})@)?' + .'(?[^\:]+(?:\:\d+)?)(?:\/(?[^\?]+))(?:\?(?.*))?$/i'; + + $newServers = []; + $protocol = 'couchbase'; + try { + $options = self::initOptions($options); + $username = $options['username']; + $password = $options['password']; + + foreach ($servers as $dsn) { + if (0 !== strpos($dsn, 'couchbase:')) { + throw new InvalidArgumentException('Invalid Couchbase DSN: it does not start with "couchbase:".'); + } + + preg_match($dsnPattern, $dsn, $matches); + + $username = $matches['username'] ?: $username; + $password = $matches['password'] ?: $password; + $protocol = $matches['protocol'] ?: $protocol; + + if (isset($matches['options'])) { + $optionsInDsn = self::getOptions($matches['options']); + + foreach ($optionsInDsn as $parameter => $value) { + $options[$parameter] = $value; + } + } + + $newServers[] = $matches['host']; + } + + $connectionString = $protocol.'://'.implode(',', $newServers); + + $client = new \CouchbaseCluster($connectionString); + $client->authenticateAs($username, $password); + + $bucket = $client->openBucket($matches['bucketName']); + + unset($options['username'], $options['password']); + foreach ($options as $option => $value) { + if (!empty($value)) { + $bucket->$option = $value; + } + } + + return $bucket; + } finally { + restore_error_handler(); + } + } + + public static function isSupported(): bool + { + return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '2.6.0', '>=') && version_compare(phpversion('couchbase'), '3.0', '<'); + } + + private static function getOptions(string $options): array + { + $results = []; + $optionsInArray = explode('&', $options); + + foreach ($optionsInArray as $option) { + [$key, $value] = explode('=', $option); + + if (\in_array($key, static::VALID_DSN_OPTIONS, true)) { + $results[$key] = $value; + } + } + + return $results; + } + + private static function initOptions(array $options): array + { + $options['username'] = $options['username'] ?? ''; + $options['password'] = $options['password'] ?? ''; + $options['operationTimeout'] = $options['operationTimeout'] ?? 0; + $options['configTimeout'] = $options['configTimeout'] ?? 0; + $options['configNodeTimeout'] = $options['configNodeTimeout'] ?? 0; + $options['n1qlTimeout'] = $options['n1qlTimeout'] ?? 0; + $options['httpTimeout'] = $options['httpTimeout'] ?? 0; + $options['configDelay'] = $options['configDelay'] ?? 0; + $options['htconfigIdleTimeout'] = $options['htconfigIdleTimeout'] ?? 0; + $options['durabilityInterval'] = $options['durabilityInterval'] ?? 0; + $options['durabilityTimeout'] = $options['durabilityTimeout'] ?? 0; + + return $options; + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + $resultsCouchbase = $this->bucket->get($ids); + + $results = []; + foreach ($resultsCouchbase as $key => $value) { + if (null !== $value->error) { + continue; + } + $results[$key] = $this->marshaller->unmarshall($value->value); + } + + return $results; + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id): bool + { + return false !== $this->bucket->get($id); + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace): bool + { + if ('' === $namespace) { + $this->bucket->manager()->flush(); + + return true; + } + + return false; + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids): bool + { + $results = $this->bucket->remove(array_values($ids)); + + foreach ($results as $key => $result) { + if (null !== $result->error && static::KEY_NOT_FOUND !== $result->error->getCode()) { + continue; + } + unset($results[$key]); + } + + return 0 === \count($results); + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + if (!$values = $this->marshaller->marshall($values, $failed)) { + return $failed; + } + + $lifetime = $this->normalizeExpiry($lifetime); + + $ko = []; + foreach ($values as $key => $value) { + $result = $this->bucket->upsert($key, $value, ['expiry' => $lifetime]); + + if (null !== $result->error) { + $ko[$key] = $result; + } + } + + return [] === $ko ? true : $ko; + } + + private function normalizeExpiry(int $expiry): int + { + if ($expiry && $expiry > static::THIRTY_DAYS_IN_SECONDS) { + $expiry += time(); + } + + return $expiry; + } +} diff --git a/vendor/symfony/cache/Adapter/CouchbaseCollectionAdapter.php b/vendor/symfony/cache/Adapter/CouchbaseCollectionAdapter.php new file mode 100644 index 0000000..c0a1317 --- /dev/null +++ b/vendor/symfony/cache/Adapter/CouchbaseCollectionAdapter.php @@ -0,0 +1,222 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Couchbase\Bucket; +use Couchbase\Cluster; +use Couchbase\ClusterOptions; +use Couchbase\Collection; +use Couchbase\DocumentNotFoundException; +use Couchbase\UpsertOptions; +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Antonio Jose Cerezo Aranda + */ +class CouchbaseCollectionAdapter extends AbstractAdapter +{ + private const MAX_KEY_LENGTH = 250; + + /** @var Collection */ + private $connection; + private $marshaller; + + public function __construct(Collection $connection, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null) + { + if (!static::isSupported()) { + throw new CacheException('Couchbase >= 3.0.0 < 4.0.0 is required.'); + } + + $this->maxIdLength = static::MAX_KEY_LENGTH; + + $this->connection = $connection; + + parent::__construct($namespace, $defaultLifetime); + $this->enableVersioning(); + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + } + + /** + * @param array|string $dsn + * + * @return Bucket|Collection + */ + public static function createConnection($dsn, array $options = []) + { + if (\is_string($dsn)) { + $dsn = [$dsn]; + } elseif (!\is_array($dsn)) { + throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be array or string, "%s" given.', __METHOD__, get_debug_type($dsn))); + } + + if (!static::isSupported()) { + throw new CacheException('Couchbase >= 3.0.0 < 4.0.0 is required.'); + } + + set_error_handler(function ($type, $msg, $file, $line): bool { throw new \ErrorException($msg, 0, $type, $file, $line); }); + + $dsnPattern = '/^(?couchbase(?:s)?)\:\/\/(?:(?[^\:]+)\:(?[^\@]{6,})@)?' + .'(?[^\:]+(?:\:\d+)?)(?:\/(?[^\/\?]+))(?:(?:\/(?[^\/]+))' + .'(?:\/(?[^\/\?]+)))?(?:\/)?(?:\?(?.*))?$/i'; + + $newServers = []; + $protocol = 'couchbase'; + try { + $username = $options['username'] ?? ''; + $password = $options['password'] ?? ''; + + foreach ($dsn as $server) { + if (0 !== strpos($server, 'couchbase:')) { + throw new InvalidArgumentException('Invalid Couchbase DSN: it does not start with "couchbase:".'); + } + + preg_match($dsnPattern, $server, $matches); + + $username = $matches['username'] ?: $username; + $password = $matches['password'] ?: $password; + $protocol = $matches['protocol'] ?: $protocol; + + if (isset($matches['options'])) { + $optionsInDsn = self::getOptions($matches['options']); + + foreach ($optionsInDsn as $parameter => $value) { + $options[$parameter] = $value; + } + } + + $newServers[] = $matches['host']; + } + + $option = isset($matches['options']) ? '?'.$matches['options'] : ''; + $connectionString = $protocol.'://'.implode(',', $newServers).$option; + + $clusterOptions = new ClusterOptions(); + $clusterOptions->credentials($username, $password); + + $client = new Cluster($connectionString, $clusterOptions); + + $bucket = $client->bucket($matches['bucketName']); + $collection = $bucket->defaultCollection(); + if (!empty($matches['scopeName'])) { + $scope = $bucket->scope($matches['scopeName']); + $collection = $scope->collection($matches['collectionName']); + } + + return $collection; + } finally { + restore_error_handler(); + } + } + + public static function isSupported(): bool + { + return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '3.0.5', '>=') && version_compare(phpversion('couchbase'), '4.0', '<'); + } + + private static function getOptions(string $options): array + { + $results = []; + $optionsInArray = explode('&', $options); + + foreach ($optionsInArray as $option) { + [$key, $value] = explode('=', $option); + + $results[$key] = $value; + } + + return $results; + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids): array + { + $results = []; + foreach ($ids as $id) { + try { + $resultCouchbase = $this->connection->get($id); + } catch (DocumentNotFoundException $exception) { + continue; + } + + $content = $resultCouchbase->value ?? $resultCouchbase->content(); + + $results[$id] = $this->marshaller->unmarshall($content); + } + + return $results; + } + + /** + * {@inheritdoc} + */ + protected function doHave($id): bool + { + return $this->connection->exists($id)->exists(); + } + + /** + * {@inheritdoc} + */ + protected function doClear($namespace): bool + { + return false; + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids): bool + { + $idsErrors = []; + foreach ($ids as $id) { + try { + $result = $this->connection->remove($id); + + if (null === $result->mutationToken()) { + $idsErrors[] = $id; + } + } catch (DocumentNotFoundException $exception) { + } + } + + return 0 === \count($idsErrors); + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, $lifetime) + { + if (!$values = $this->marshaller->marshall($values, $failed)) { + return $failed; + } + + $upsertOptions = new UpsertOptions(); + $upsertOptions->expiry($lifetime); + + $ko = []; + foreach ($values as $key => $value) { + try { + $this->connection->upsert($key, $value, $upsertOptions); + } catch (\Exception $exception) { + $ko[$key] = ''; + } + } + + return [] === $ko ? true : $ko; + } +} diff --git a/vendor/symfony/cache/Adapter/DoctrineAdapter.php b/vendor/symfony/cache/Adapter/DoctrineAdapter.php new file mode 100644 index 0000000..efa30c8 --- /dev/null +++ b/vendor/symfony/cache/Adapter/DoctrineAdapter.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Doctrine\Common\Cache\CacheProvider; +use Doctrine\Common\Cache\Psr6\CacheAdapter; + +/** + * @author Nicolas Grekas + * + * @deprecated Since Symfony 5.4, use Doctrine\Common\Cache\Psr6\CacheAdapter instead + */ +class DoctrineAdapter extends AbstractAdapter +{ + private $provider; + + public function __construct(CacheProvider $provider, string $namespace = '', int $defaultLifetime = 0) + { + trigger_deprecation('symfony/cache', '5.4', '"%s" is deprecated, use "%s" instead.', __CLASS__, CacheAdapter::class); + + parent::__construct('', $defaultLifetime); + $this->provider = $provider; + $provider->setNamespace($namespace); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + parent::reset(); + $this->provider->setNamespace($this->provider->getNamespace()); + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + $unserializeCallbackHandler = ini_set('unserialize_callback_func', parent::class.'::handleUnserializeCallback'); + try { + return $this->provider->fetchMultiple($ids); + } catch (\Error $e) { + $trace = $e->getTrace(); + + if (isset($trace[0]['function']) && !isset($trace[0]['class'])) { + switch ($trace[0]['function']) { + case 'unserialize': + case 'apcu_fetch': + case 'apc_fetch': + throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine()); + } + } + + throw $e; + } finally { + ini_set('unserialize_callback_func', $unserializeCallbackHandler); + } + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id) + { + return $this->provider->contains($id); + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace) + { + $namespace = $this->provider->getNamespace(); + + return isset($namespace[0]) + ? $this->provider->deleteAll() + : $this->provider->flushAll(); + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + $ok = true; + foreach ($ids as $id) { + $ok = $this->provider->delete($id) && $ok; + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + return $this->provider->saveMultiple($values, $lifetime); + } +} diff --git a/vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php b/vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php new file mode 100644 index 0000000..c126824 --- /dev/null +++ b/vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php @@ -0,0 +1,448 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Doctrine\DBAL\ArrayParameterType; +use Doctrine\DBAL\Configuration; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Driver\ServerInfoAwareConnection; +use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Exception as DBALException; +use Doctrine\DBAL\Exception\TableNotFoundException; +use Doctrine\DBAL\ParameterType; +use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\ServerVersionProvider; +use Doctrine\DBAL\Tools\DsnParser; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; +use Symfony\Component\Cache\PruneableInterface; + +class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface +{ + protected $maxIdLength = 255; + + private $marshaller; + private $conn; + private $platformName; + private $serverVersion; + private $table = 'cache_items'; + private $idCol = 'item_id'; + private $dataCol = 'item_data'; + private $lifetimeCol = 'item_lifetime'; + private $timeCol = 'item_time'; + private $namespace; + + /** + * You can either pass an existing database Doctrine DBAL Connection or + * a DSN string that will be used to connect to the database. + * + * The cache table is created automatically when possible. + * Otherwise, use the createTable() method. + * + * List of available options: + * * db_table: The name of the table [default: cache_items] + * * db_id_col: The column where to store the cache id [default: item_id] + * * db_data_col: The column where to store the cache data [default: item_data] + * * db_lifetime_col: The column where to store the lifetime [default: item_lifetime] + * * db_time_col: The column where to store the timestamp [default: item_time] + * + * @param Connection|string $connOrDsn + * + * @throws InvalidArgumentException When namespace contains invalid characters + */ + public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], ?MarshallerInterface $marshaller = null) + { + if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) { + throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0])); + } + + if ($connOrDsn instanceof Connection) { + $this->conn = $connOrDsn; + } elseif (\is_string($connOrDsn)) { + if (!class_exists(DriverManager::class)) { + throw new InvalidArgumentException('Failed to parse DSN. Try running "composer require doctrine/dbal".'); + } + if (class_exists(DsnParser::class)) { + $params = (new DsnParser([ + 'db2' => 'ibm_db2', + 'mssql' => 'pdo_sqlsrv', + 'mysql' => 'pdo_mysql', + 'mysql2' => 'pdo_mysql', + 'postgres' => 'pdo_pgsql', + 'postgresql' => 'pdo_pgsql', + 'pgsql' => 'pdo_pgsql', + 'sqlite' => 'pdo_sqlite', + 'sqlite3' => 'pdo_sqlite', + ]))->parse($connOrDsn); + } else { + $params = ['url' => $connOrDsn]; + } + + $config = new Configuration(); + if (class_exists(DefaultSchemaManagerFactory::class)) { + $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); + } + + $this->conn = DriverManager::getConnection($params, $config); + } else { + throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be "%s" or string, "%s" given.', __METHOD__, Connection::class, get_debug_type($connOrDsn))); + } + + $this->table = $options['db_table'] ?? $this->table; + $this->idCol = $options['db_id_col'] ?? $this->idCol; + $this->dataCol = $options['db_data_col'] ?? $this->dataCol; + $this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol; + $this->timeCol = $options['db_time_col'] ?? $this->timeCol; + $this->namespace = $namespace; + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + + parent::__construct($namespace, $defaultLifetime); + } + + /** + * Creates the table to store cache items which can be called once for setup. + * + * Cache ID are saved in a column of maximum length 255. Cache data is + * saved in a BLOB. + * + * @throws DBALException When the table already exists + */ + public function createTable() + { + $schema = new Schema(); + $this->addTableToSchema($schema); + + foreach ($schema->toSql($this->conn->getDatabasePlatform()) as $sql) { + $this->conn->executeStatement($sql); + } + } + + /** + * {@inheritdoc} + */ + public function configureSchema(Schema $schema, Connection $forConnection): void + { + // only update the schema for this connection + if ($forConnection !== $this->conn) { + return; + } + + if ($schema->hasTable($this->table)) { + return; + } + + $this->addTableToSchema($schema); + } + + /** + * {@inheritdoc} + */ + public function prune(): bool + { + $deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ?"; + $params = [time()]; + $paramTypes = [ParameterType::INTEGER]; + + if ('' !== $this->namespace) { + $deleteSql .= " AND $this->idCol LIKE ?"; + $params[] = sprintf('%s%%', $this->namespace); + $paramTypes[] = ParameterType::STRING; + } + + try { + $this->conn->executeStatement($deleteSql, $params, $paramTypes); + } catch (TableNotFoundException $e) { + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids): iterable + { + $now = time(); + $expired = []; + + $sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN (?)"; + $result = $this->conn->executeQuery($sql, [ + $now, + $ids, + ], [ + ParameterType::INTEGER, + class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY, + ])->iterateNumeric(); + + foreach ($result as $row) { + if (null === $row[1]) { + $expired[] = $row[0]; + } else { + yield $row[0] => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]); + } + } + + if ($expired) { + $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN (?)"; + $this->conn->executeStatement($sql, [ + $now, + $expired, + ], [ + ParameterType::INTEGER, + class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY, + ]); + } + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id): bool + { + $sql = "SELECT 1 FROM $this->table WHERE $this->idCol = ? AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ?)"; + $result = $this->conn->executeQuery($sql, [ + $id, + time(), + ], [ + ParameterType::STRING, + ParameterType::INTEGER, + ]); + + return (bool) $result->fetchOne(); + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace): bool + { + if ('' === $namespace) { + if ('sqlite' === $this->getPlatformName()) { + $sql = "DELETE FROM $this->table"; + } else { + $sql = "TRUNCATE TABLE $this->table"; + } + } else { + $sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'"; + } + + try { + $this->conn->executeStatement($sql); + } catch (TableNotFoundException $e) { + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids): bool + { + $sql = "DELETE FROM $this->table WHERE $this->idCol IN (?)"; + try { + $this->conn->executeStatement($sql, [array_values($ids)], [class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY]); + } catch (TableNotFoundException $e) { + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + if (!$values = $this->marshaller->marshall($values, $failed)) { + return $failed; + } + + $platformName = $this->getPlatformName(); + $insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?)"; + + switch (true) { + case 'mysql' === $platformName: + $sql = $insertSql." ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)"; + break; + case 'oci' === $platformName: + // DUAL is Oracle specific dummy table + $sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ". + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". + "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?"; + break; + case 'sqlsrv' === $platformName && version_compare($this->getServerVersion(), '10', '>='): + // MERGE is only available since SQL Server 2008 and must be terminated by semicolon + // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx + $sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ". + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". + "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;"; + break; + case 'sqlite' === $platformName: + $sql = 'INSERT OR REPLACE'.substr($insertSql, 6); + break; + case 'pgsql' === $platformName && version_compare($this->getServerVersion(), '9.5', '>='): + $sql = $insertSql." ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)"; + break; + default: + $platformName = null; + $sql = "UPDATE $this->table SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ? WHERE $this->idCol = ?"; + break; + } + + $now = time(); + $lifetime = $lifetime ?: null; + try { + $stmt = $this->conn->prepare($sql); + } catch (TableNotFoundException $e) { + if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql', 'sqlite', 'sqlsrv'], true)) { + $this->createTable(); + } + $stmt = $this->conn->prepare($sql); + } + + if ('sqlsrv' === $platformName || 'oci' === $platformName) { + $bind = static function ($id, $data) use ($stmt) { + $stmt->bindValue(1, $id); + $stmt->bindValue(2, $id); + $stmt->bindValue(3, $data, ParameterType::LARGE_OBJECT); + $stmt->bindValue(6, $data, ParameterType::LARGE_OBJECT); + }; + $stmt->bindValue(4, $lifetime, ParameterType::INTEGER); + $stmt->bindValue(5, $now, ParameterType::INTEGER); + $stmt->bindValue(7, $lifetime, ParameterType::INTEGER); + $stmt->bindValue(8, $now, ParameterType::INTEGER); + } elseif (null !== $platformName) { + $bind = static function ($id, $data) use ($stmt) { + $stmt->bindValue(1, $id); + $stmt->bindValue(2, $data, ParameterType::LARGE_OBJECT); + }; + $stmt->bindValue(3, $lifetime, ParameterType::INTEGER); + $stmt->bindValue(4, $now, ParameterType::INTEGER); + } else { + $stmt->bindValue(2, $lifetime, ParameterType::INTEGER); + $stmt->bindValue(3, $now, ParameterType::INTEGER); + + $insertStmt = $this->conn->prepare($insertSql); + $insertStmt->bindValue(3, $lifetime, ParameterType::INTEGER); + $insertStmt->bindValue(4, $now, ParameterType::INTEGER); + + $bind = static function ($id, $data) use ($stmt, $insertStmt) { + $stmt->bindValue(1, $data, ParameterType::LARGE_OBJECT); + $stmt->bindValue(4, $id); + $insertStmt->bindValue(1, $id); + $insertStmt->bindValue(2, $data, ParameterType::LARGE_OBJECT); + }; + } + + foreach ($values as $id => $data) { + $bind($id, $data); + try { + $rowCount = $stmt->executeStatement(); + } catch (TableNotFoundException $e) { + if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql', 'sqlite', 'sqlsrv'], true)) { + $this->createTable(); + } + $rowCount = $stmt->executeStatement(); + } + if (null === $platformName && 0 === $rowCount) { + try { + $insertStmt->executeStatement(); + } catch (DBALException $e) { + // A concurrent write won, let it be + } + } + } + + return $failed; + } + + /** + * @internal + */ + protected function getId($key) + { + if ('pgsql' !== $this->getPlatformName()) { + return parent::getId($key); + } + + if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) { + $key = rawurlencode($key); + } + + return parent::getId($key); + } + + private function getPlatformName(): string + { + if (isset($this->platformName)) { + return $this->platformName; + } + + $platform = $this->conn->getDatabasePlatform(); + + switch (true) { + case $platform instanceof \Doctrine\DBAL\Platforms\MySQLPlatform: + case $platform instanceof \Doctrine\DBAL\Platforms\MySQL57Platform: + return $this->platformName = 'mysql'; + + case $platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform: + return $this->platformName = 'sqlite'; + + case $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform: + case $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQL94Platform: + return $this->platformName = 'pgsql'; + + case $platform instanceof \Doctrine\DBAL\Platforms\OraclePlatform: + return $this->platformName = 'oci'; + + case $platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform: + case $platform instanceof \Doctrine\DBAL\Platforms\SQLServer2012Platform: + return $this->platformName = 'sqlsrv'; + + default: + return $this->platformName = \get_class($platform); + } + } + + private function getServerVersion(): string + { + if (isset($this->serverVersion)) { + return $this->serverVersion; + } + + if ($this->conn instanceof ServerVersionProvider || $this->conn instanceof ServerInfoAwareConnection) { + return $this->serverVersion = $this->conn->getServerVersion(); + } + + // The condition should be removed once support for DBAL <3.3 is dropped + $conn = method_exists($this->conn, 'getNativeConnection') ? $this->conn->getNativeConnection() : $this->conn->getWrappedConnection(); + + return $this->serverVersion = $conn->getAttribute(\PDO::ATTR_SERVER_VERSION); + } + + private function addTableToSchema(Schema $schema): void + { + $types = [ + 'mysql' => 'binary', + 'sqlite' => 'text', + ]; + + $table = $schema->createTable($this->table); + $table->addColumn($this->idCol, $types[$this->getPlatformName()] ?? 'string', ['length' => 255]); + $table->addColumn($this->dataCol, 'blob', ['length' => 16777215]); + $table->addColumn($this->lifetimeCol, 'integer', ['unsigned' => true, 'notnull' => false]); + $table->addColumn($this->timeCol, 'integer', ['unsigned' => true]); + $table->setPrimaryKey([$this->idCol]); + } +} diff --git a/vendor/symfony/cache/Adapter/FilesystemAdapter.php b/vendor/symfony/cache/Adapter/FilesystemAdapter.php new file mode 100644 index 0000000..13daa56 --- /dev/null +++ b/vendor/symfony/cache/Adapter/FilesystemAdapter.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\Traits\FilesystemTrait; + +class FilesystemAdapter extends AbstractAdapter implements PruneableInterface +{ + use FilesystemTrait; + + public function __construct(string $namespace = '', int $defaultLifetime = 0, ?string $directory = null, ?MarshallerInterface $marshaller = null) + { + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + parent::__construct('', $defaultLifetime); + $this->init($namespace, $directory); + } +} diff --git a/vendor/symfony/cache/Adapter/FilesystemTagAwareAdapter.php b/vendor/symfony/cache/Adapter/FilesystemTagAwareAdapter.php new file mode 100644 index 0000000..440a37a --- /dev/null +++ b/vendor/symfony/cache/Adapter/FilesystemTagAwareAdapter.php @@ -0,0 +1,239 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\Marshaller\MarshallerInterface; +use Symfony\Component\Cache\Marshaller\TagAwareMarshaller; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\Traits\FilesystemTrait; + +/** + * Stores tag id <> cache id relationship as a symlink, and lookup on invalidation calls. + * + * @author Nicolas Grekas + * @author André Rømcke + */ +class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements PruneableInterface +{ + use FilesystemTrait { + doClear as private doClearCache; + doSave as private doSaveCache; + } + + /** + * Folder used for tag symlinks. + */ + private const TAG_FOLDER = 'tags'; + + public function __construct(string $namespace = '', int $defaultLifetime = 0, ?string $directory = null, ?MarshallerInterface $marshaller = null) + { + $this->marshaller = new TagAwareMarshaller($marshaller); + parent::__construct('', $defaultLifetime); + $this->init($namespace, $directory); + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace) + { + $ok = $this->doClearCache($namespace); + + if ('' !== $namespace) { + return $ok; + } + + set_error_handler(static function () {}); + $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + + try { + foreach ($this->scanHashDir($this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR) as $dir) { + if (rename($dir, $renamed = substr_replace($dir, bin2hex(random_bytes(4)), -8))) { + $dir = $renamed.\DIRECTORY_SEPARATOR; + } else { + $dir .= \DIRECTORY_SEPARATOR; + $renamed = null; + } + + for ($i = 0; $i < 38; ++$i) { + if (!is_dir($dir.$chars[$i])) { + continue; + } + for ($j = 0; $j < 38; ++$j) { + if (!is_dir($d = $dir.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j])) { + continue; + } + foreach (scandir($d, \SCANDIR_SORT_NONE) ?: [] as $link) { + if ('.' !== $link && '..' !== $link && (null !== $renamed || !realpath($d.\DIRECTORY_SEPARATOR.$link))) { + unlink($d.\DIRECTORY_SEPARATOR.$link); + } + } + null === $renamed ?: rmdir($d); + } + null === $renamed ?: rmdir($dir.$chars[$i]); + } + null === $renamed ?: rmdir($renamed); + } + } finally { + restore_error_handler(); + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime, array $addTagData = [], array $removeTagData = []): array + { + $failed = $this->doSaveCache($values, $lifetime); + + // Add Tags as symlinks + foreach ($addTagData as $tagId => $ids) { + $tagFolder = $this->getTagFolder($tagId); + foreach ($ids as $id) { + if ($failed && \in_array($id, $failed, true)) { + continue; + } + + $file = $this->getFile($id); + + if (!@symlink($file, $tagLink = $this->getFile($id, true, $tagFolder)) && !is_link($tagLink)) { + @unlink($file); + $failed[] = $id; + } + } + } + + // Unlink removed Tags + foreach ($removeTagData as $tagId => $ids) { + $tagFolder = $this->getTagFolder($tagId); + foreach ($ids as $id) { + if ($failed && \in_array($id, $failed, true)) { + continue; + } + + @unlink($this->getFile($id, false, $tagFolder)); + } + } + + return $failed; + } + + /** + * {@inheritdoc} + */ + protected function doDeleteYieldTags(array $ids): iterable + { + foreach ($ids as $id) { + $file = $this->getFile($id); + if (!is_file($file) || !$h = @fopen($file, 'r')) { + continue; + } + + if ((\PHP_VERSION_ID >= 70300 || '\\' !== \DIRECTORY_SEPARATOR) && !@unlink($file)) { + fclose($h); + continue; + } + + $meta = explode("\n", fread($h, 4096), 3)[2] ?? ''; + + // detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F + if (13 < \strlen($meta) && "\x9D" === $meta[0] && "\0" === $meta[5] && "\x5F" === $meta[9]) { + $meta[9] = "\0"; + $tagLen = unpack('Nlen', $meta, 9)['len']; + $meta = substr($meta, 13, $tagLen); + + if (0 < $tagLen -= \strlen($meta)) { + $meta .= fread($h, $tagLen); + } + + try { + yield $id => '' === $meta ? [] : $this->marshaller->unmarshall($meta); + } catch (\Exception $e) { + yield $id => []; + } + } + + fclose($h); + + if (\PHP_VERSION_ID < 70300 && '\\' === \DIRECTORY_SEPARATOR) { + @unlink($file); + } + } + } + + /** + * {@inheritdoc} + */ + protected function doDeleteTagRelations(array $tagData): bool + { + foreach ($tagData as $tagId => $idList) { + $tagFolder = $this->getTagFolder($tagId); + foreach ($idList as $id) { + @unlink($this->getFile($id, false, $tagFolder)); + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doInvalidate(array $tagIds): bool + { + foreach ($tagIds as $tagId) { + if (!is_dir($tagFolder = $this->getTagFolder($tagId))) { + continue; + } + + set_error_handler(static function () {}); + + try { + if (rename($tagFolder, $renamed = substr_replace($tagFolder, bin2hex(random_bytes(4)), -9))) { + $tagFolder = $renamed.\DIRECTORY_SEPARATOR; + } else { + $renamed = null; + } + + foreach ($this->scanHashDir($tagFolder) as $itemLink) { + unlink(realpath($itemLink) ?: $itemLink); + unlink($itemLink); + } + + if (null === $renamed) { + continue; + } + + $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + + for ($i = 0; $i < 38; ++$i) { + for ($j = 0; $j < 38; ++$j) { + rmdir($tagFolder.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j]); + } + rmdir($tagFolder.$chars[$i]); + } + rmdir($renamed); + } finally { + restore_error_handler(); + } + } + + return true; + } + + private function getTagFolder(string $tagId): string + { + return $this->getFile($tagId, false, $this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR; + } +} diff --git a/vendor/symfony/cache/Adapter/MemcachedAdapter.php b/vendor/symfony/cache/Adapter/MemcachedAdapter.php new file mode 100644 index 0000000..0bc20d4 --- /dev/null +++ b/vendor/symfony/cache/Adapter/MemcachedAdapter.php @@ -0,0 +1,347 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Rob Frawley 2nd + * @author Nicolas Grekas + */ +class MemcachedAdapter extends AbstractAdapter +{ + /** + * We are replacing characters that are illegal in Memcached keys with reserved characters from + * {@see \Symfony\Contracts\Cache\ItemInterface::RESERVED_CHARACTERS} that are legal in Memcached. + * Note: don’t use {@see \Symfony\Component\Cache\Adapter\AbstractAdapter::NS_SEPARATOR}. + */ + private const RESERVED_MEMCACHED = " \n\r\t\v\f\0"; + private const RESERVED_PSR6 = '@()\{}/'; + + protected $maxIdLength = 250; + + private $marshaller; + private $client; + private $lazyClient; + + /** + * Using a MemcachedAdapter with a TagAwareAdapter for storing tags is discouraged. + * Using a RedisAdapter is recommended instead. If you cannot do otherwise, be aware that: + * - the Memcached::OPT_BINARY_PROTOCOL must be enabled + * (that's the default when using MemcachedAdapter::createConnection()); + * - tags eviction by Memcached's LRU algorithm will break by-tags invalidation; + * your Memcached memory should be large enough to never trigger LRU. + * + * Using a MemcachedAdapter as a pure items store is fine. + */ + public function __construct(\Memcached $client, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null) + { + if (!static::isSupported()) { + throw new CacheException('Memcached '.(\PHP_VERSION_ID >= 80100 ? '> 3.1.5' : '>= 2.2.0').' is required.'); + } + if ('Memcached' === \get_class($client)) { + $opt = $client->getOption(\Memcached::OPT_SERIALIZER); + if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) { + throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".'); + } + $this->maxIdLength -= \strlen($client->getOption(\Memcached::OPT_PREFIX_KEY)); + $this->client = $client; + } else { + $this->lazyClient = $client; + } + + parent::__construct($namespace, $defaultLifetime); + $this->enableVersioning(); + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + } + + public static function isSupported() + { + return \extension_loaded('memcached') && version_compare(phpversion('memcached'), \PHP_VERSION_ID >= 80100 ? '3.1.6' : '2.2.0', '>='); + } + + /** + * Creates a Memcached instance. + * + * By default, the binary protocol, no block, and libketama compatible options are enabled. + * + * Examples for servers: + * - 'memcached://user:pass@localhost?weight=33' + * - [['localhost', 11211, 33]] + * + * @param array[]|string|string[] $servers An array of servers, a DSN, or an array of DSNs + * + * @return \Memcached + * + * @throws \ErrorException When invalid options or servers are provided + */ + public static function createConnection($servers, array $options = []) + { + if (\is_string($servers)) { + $servers = [$servers]; + } elseif (!\is_array($servers)) { + throw new InvalidArgumentException(sprintf('MemcachedAdapter::createClient() expects array or string as first argument, "%s" given.', get_debug_type($servers))); + } + if (!static::isSupported()) { + throw new CacheException('Memcached '.(\PHP_VERSION_ID >= 80100 ? '> 3.1.5' : '>= 2.2.0').' is required.'); + } + set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); }); + try { + $client = new \Memcached($options['persistent_id'] ?? null); + $username = $options['username'] ?? null; + $password = $options['password'] ?? null; + + // parse any DSN in $servers + foreach ($servers as $i => $dsn) { + if (\is_array($dsn)) { + continue; + } + if (!str_starts_with($dsn, 'memcached:')) { + throw new InvalidArgumentException('Invalid Memcached DSN: it does not start with "memcached:".'); + } + $params = preg_replace_callback('#^memcached:(//)?(?:([^@]*+)@)?#', function ($m) use (&$username, &$password) { + if (!empty($m[2])) { + [$username, $password] = explode(':', $m[2], 2) + [1 => null]; + $username = rawurldecode($username); + $password = null !== $password ? rawurldecode($password) : null; + } + + return 'file:'.($m[1] ?? ''); + }, $dsn); + if (false === $params = parse_url($params)) { + throw new InvalidArgumentException('Invalid Memcached DSN.'); + } + $query = $hosts = []; + if (isset($params['query'])) { + parse_str($params['query'], $query); + + if (isset($query['host'])) { + if (!\is_array($hosts = $query['host'])) { + throw new InvalidArgumentException('Invalid Memcached DSN: query parameter "host" must be an array.'); + } + foreach ($hosts as $host => $weight) { + if (false === $port = strrpos($host, ':')) { + $hosts[$host] = [$host, 11211, (int) $weight]; + } else { + $hosts[$host] = [substr($host, 0, $port), (int) substr($host, 1 + $port), (int) $weight]; + } + } + $hosts = array_values($hosts); + unset($query['host']); + } + if ($hosts && !isset($params['host']) && !isset($params['path'])) { + unset($servers[$i]); + $servers = array_merge($servers, $hosts); + continue; + } + } + if (!isset($params['host']) && !isset($params['path'])) { + throw new InvalidArgumentException('Invalid Memcached DSN: missing host or path.'); + } + if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) { + $params['weight'] = $m[1]; + $params['path'] = substr($params['path'], 0, -\strlen($m[0])); + } + $params += [ + 'host' => $params['host'] ?? $params['path'], + 'port' => isset($params['host']) ? 11211 : null, + 'weight' => 0, + ]; + if ($query) { + $params += $query; + $options = $query + $options; + } + + $servers[$i] = [$params['host'], $params['port'], $params['weight']]; + + if ($hosts) { + $servers = array_merge($servers, $hosts); + } + } + + // set client's options + unset($options['persistent_id'], $options['username'], $options['password'], $options['weight'], $options['lazy']); + $options = array_change_key_case($options, \CASE_UPPER); + $client->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); + $client->setOption(\Memcached::OPT_NO_BLOCK, true); + $client->setOption(\Memcached::OPT_TCP_NODELAY, true); + if (!\array_key_exists('LIBKETAMA_COMPATIBLE', $options) && !\array_key_exists(\Memcached::OPT_LIBKETAMA_COMPATIBLE, $options)) { + $client->setOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE, true); + } + foreach ($options as $name => $value) { + if (\is_int($name)) { + continue; + } + if ('HASH' === $name || 'SERIALIZER' === $name || 'DISTRIBUTION' === $name) { + $value = \constant('Memcached::'.$name.'_'.strtoupper($value)); + } + unset($options[$name]); + + if (\defined('Memcached::OPT_'.$name)) { + $options[\constant('Memcached::OPT_'.$name)] = $value; + } + } + $client->setOptions($options + [\Memcached::OPT_SERIALIZER => \Memcached::SERIALIZER_PHP]); + + // set client's servers, taking care of persistent connections + if (!$client->isPristine()) { + $oldServers = []; + foreach ($client->getServerList() as $server) { + $oldServers[] = [$server['host'], $server['port']]; + } + + $newServers = []; + foreach ($servers as $server) { + if (1 < \count($server)) { + $server = array_values($server); + unset($server[2]); + $server[1] = (int) $server[1]; + } + $newServers[] = $server; + } + + if ($oldServers !== $newServers) { + $client->resetServerList(); + $client->addServers($servers); + } + } else { + $client->addServers($servers); + } + + if (null !== $username || null !== $password) { + if (!method_exists($client, 'setSaslAuthData')) { + trigger_error('Missing SASL support: the memcached extension must be compiled with --enable-memcached-sasl.'); + } + $client->setSaslAuthData($username, $password); + } + + return $client; + } finally { + restore_error_handler(); + } + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + if (!$values = $this->marshaller->marshall($values, $failed)) { + return $failed; + } + + if ($lifetime && $lifetime > 30 * 86400) { + $lifetime += time(); + } + + $encodedValues = []; + foreach ($values as $key => $value) { + $encodedValues[self::encodeKey($key)] = $value; + } + + return $this->checkResultCode($this->getClient()->setMulti($encodedValues, $lifetime)) ? $failed : false; + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + try { + $encodedIds = array_map([__CLASS__, 'encodeKey'], $ids); + + $encodedResult = $this->checkResultCode($this->getClient()->getMulti($encodedIds)); + + $result = []; + foreach ($encodedResult as $key => $value) { + $result[self::decodeKey($key)] = $this->marshaller->unmarshall($value); + } + + return $result; + } catch (\Error $e) { + throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine()); + } + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id) + { + return false !== $this->getClient()->get(self::encodeKey($id)) || $this->checkResultCode(\Memcached::RES_SUCCESS === $this->client->getResultCode()); + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + $ok = true; + $encodedIds = array_map([__CLASS__, 'encodeKey'], $ids); + foreach ($this->checkResultCode($this->getClient()->deleteMulti($encodedIds)) as $result) { + if (\Memcached::RES_SUCCESS !== $result && \Memcached::RES_NOTFOUND !== $result) { + $ok = false; + } + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace) + { + return '' === $namespace && $this->getClient()->flush(); + } + + private function checkResultCode($result) + { + $code = $this->client->getResultCode(); + + if (\Memcached::RES_SUCCESS === $code || \Memcached::RES_NOTFOUND === $code) { + return $result; + } + + throw new CacheException('MemcachedAdapter client error: '.strtolower($this->client->getResultMessage())); + } + + private function getClient(): \Memcached + { + if ($this->client) { + return $this->client; + } + + $opt = $this->lazyClient->getOption(\Memcached::OPT_SERIALIZER); + if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) { + throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".'); + } + if ('' !== $prefix = (string) $this->lazyClient->getOption(\Memcached::OPT_PREFIX_KEY)) { + throw new CacheException(sprintf('MemcachedAdapter: "prefix_key" option must be empty when using proxified connections, "%s" given.', $prefix)); + } + + return $this->client = $this->lazyClient; + } + + private static function encodeKey(string $key): string + { + return strtr($key, self::RESERVED_MEMCACHED, self::RESERVED_PSR6); + } + + private static function decodeKey(string $key): string + { + return strtr($key, self::RESERVED_PSR6, self::RESERVED_MEMCACHED); + } +} diff --git a/vendor/symfony/cache/Adapter/NullAdapter.php b/vendor/symfony/cache/Adapter/NullAdapter.php new file mode 100644 index 0000000..bf5382f --- /dev/null +++ b/vendor/symfony/cache/Adapter/NullAdapter.php @@ -0,0 +1,152 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Contracts\Cache\CacheInterface; + +/** + * @author Titouan Galopin + */ +class NullAdapter implements AdapterInterface, CacheInterface +{ + private static $createCacheItem; + + public function __construct() + { + self::$createCacheItem ?? self::$createCacheItem = \Closure::bind( + static function ($key) { + $item = new CacheItem(); + $item->key = $key; + $item->isHit = false; + + return $item; + }, + null, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null) + { + $save = true; + + return $callback((self::$createCacheItem)($key), $save); + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + return (self::$createCacheItem)($key); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = []) + { + return $this->generateItems($keys); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function hasItem($key) + { + return false; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = '') + { + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItem($key) + { + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItems(array $keys) + { + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function save(CacheItemInterface $item) + { + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function saveDeferred(CacheItemInterface $item) + { + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function commit() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function delete(string $key): bool + { + return $this->deleteItem($key); + } + + private function generateItems(array $keys): \Generator + { + $f = self::$createCacheItem; + + foreach ($keys as $key) { + yield $key => $f($key); + } + } +} diff --git a/vendor/symfony/cache/Adapter/ParameterNormalizer.php b/vendor/symfony/cache/Adapter/ParameterNormalizer.php new file mode 100644 index 0000000..e33ae9f --- /dev/null +++ b/vendor/symfony/cache/Adapter/ParameterNormalizer.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +/** + * @author Lars Strojny + */ +final class ParameterNormalizer +{ + public static function normalizeDuration(string $duration): int + { + if (is_numeric($duration)) { + return $duration; + } + + if (false !== $time = strtotime($duration, 0)) { + return $time; + } + + try { + return \DateTime::createFromFormat('U', 0)->add(new \DateInterval($duration))->getTimestamp(); + } catch (\Exception $e) { + throw new \InvalidArgumentException(sprintf('Cannot parse date interval "%s".', $duration), 0, $e); + } + } +} diff --git a/vendor/symfony/cache/Adapter/PdoAdapter.php b/vendor/symfony/cache/Adapter/PdoAdapter.php new file mode 100644 index 0000000..a2a275b --- /dev/null +++ b/vendor/symfony/cache/Adapter/PdoAdapter.php @@ -0,0 +1,616 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Schema\Schema; +use Psr\Cache\CacheItemInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; +use Symfony\Component\Cache\PruneableInterface; + +class PdoAdapter extends AbstractAdapter implements PruneableInterface +{ + protected $maxIdLength = 255; + + private $marshaller; + private $conn; + private $dsn; + private $driver; + private $serverVersion; + private $table = 'cache_items'; + private $idCol = 'item_id'; + private $dataCol = 'item_data'; + private $lifetimeCol = 'item_lifetime'; + private $timeCol = 'item_time'; + private $username = null; + private $password = null; + private $connectionOptions = []; + private $namespace; + + private $dbalAdapter; + + /** + * You can either pass an existing database connection as PDO instance or + * a DSN string that will be used to lazy-connect to the database when the + * cache is actually used. + * + * List of available options: + * * db_table: The name of the table [default: cache_items] + * * db_id_col: The column where to store the cache id [default: item_id] + * * db_data_col: The column where to store the cache data [default: item_data] + * * db_lifetime_col: The column where to store the lifetime [default: item_lifetime] + * * db_time_col: The column where to store the timestamp [default: item_time] + * * db_username: The username when lazy-connect [default: ''] + * * db_password: The password when lazy-connect [default: ''] + * * db_connection_options: An array of driver-specific connection options [default: []] + * + * @param \PDO|string $connOrDsn + * + * @throws InvalidArgumentException When first argument is not PDO nor Connection nor string + * @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION + * @throws InvalidArgumentException When namespace contains invalid characters + */ + public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], ?MarshallerInterface $marshaller = null) + { + if ($connOrDsn instanceof Connection || (\is_string($connOrDsn) && str_contains($connOrDsn, '://'))) { + trigger_deprecation('symfony/cache', '5.4', 'Usage of a DBAL Connection with "%s" is deprecated and will be removed in symfony 6.0. Use "%s" instead.', __CLASS__, DoctrineDbalAdapter::class); + $this->dbalAdapter = new DoctrineDbalAdapter($connOrDsn, $namespace, $defaultLifetime, $options, $marshaller); + + return; + } + + if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) { + throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0])); + } + + if ($connOrDsn instanceof \PDO) { + if (\PDO::ERRMODE_EXCEPTION !== $connOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) { + throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)).', __CLASS__)); + } + + $this->conn = $connOrDsn; + } elseif (\is_string($connOrDsn)) { + $this->dsn = $connOrDsn; + } else { + throw new InvalidArgumentException(sprintf('"%s" requires PDO or Doctrine\DBAL\Connection instance or DSN string as first argument, "%s" given.', __CLASS__, get_debug_type($connOrDsn))); + } + + $this->table = $options['db_table'] ?? $this->table; + $this->idCol = $options['db_id_col'] ?? $this->idCol; + $this->dataCol = $options['db_data_col'] ?? $this->dataCol; + $this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol; + $this->timeCol = $options['db_time_col'] ?? $this->timeCol; + $this->username = $options['db_username'] ?? $this->username; + $this->password = $options['db_password'] ?? $this->password; + $this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions; + $this->namespace = $namespace; + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + + parent::__construct($namespace, $defaultLifetime); + } + + /** + * {@inheritDoc} + */ + public function getItem($key) + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->getItem($key); + } + + return parent::getItem($key); + } + + /** + * {@inheritDoc} + */ + public function getItems(array $keys = []) + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->getItems($keys); + } + + return parent::getItems($keys); + } + + /** + * {@inheritDoc} + */ + public function hasItem($key) + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->hasItem($key); + } + + return parent::hasItem($key); + } + + /** + * {@inheritDoc} + */ + public function deleteItem($key) + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->deleteItem($key); + } + + return parent::deleteItem($key); + } + + /** + * {@inheritDoc} + */ + public function deleteItems(array $keys) + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->deleteItems($keys); + } + + return parent::deleteItems($keys); + } + + /** + * {@inheritDoc} + */ + public function clear(string $prefix = '') + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->clear($prefix); + } + + return parent::clear($prefix); + } + + /** + * {@inheritDoc} + */ + public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null) + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->get($key, $callback, $beta, $metadata); + } + + return parent::get($key, $callback, $beta, $metadata); + } + + /** + * {@inheritDoc} + */ + public function delete(string $key): bool + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->delete($key); + } + + return parent::delete($key); + } + + /** + * {@inheritDoc} + */ + public function save(CacheItemInterface $item) + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->save($item); + } + + return parent::save($item); + } + + /** + * {@inheritDoc} + */ + public function saveDeferred(CacheItemInterface $item) + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->saveDeferred($item); + } + + return parent::saveDeferred($item); + } + + /** + * {@inheritDoc} + */ + public function setLogger(LoggerInterface $logger): void + { + if (isset($this->dbalAdapter)) { + $this->dbalAdapter->setLogger($logger); + + return; + } + + parent::setLogger($logger); + } + + /** + * {@inheritDoc} + */ + public function commit() + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->commit(); + } + + return parent::commit(); + } + + /** + * {@inheritDoc} + */ + public function reset() + { + if (isset($this->dbalAdapter)) { + $this->dbalAdapter->reset(); + + return; + } + + parent::reset(); + } + + /** + * Creates the table to store cache items which can be called once for setup. + * + * Cache ID are saved in a column of maximum length 255. Cache data is + * saved in a BLOB. + * + * @throws \PDOException When the table already exists + * @throws \DomainException When an unsupported PDO driver is used + */ + public function createTable() + { + if (isset($this->dbalAdapter)) { + $this->dbalAdapter->createTable(); + + return; + } + + // connect if we are not yet + $conn = $this->getConnection(); + + switch ($this->driver) { + case 'mysql': + // We use varbinary for the ID column because it prevents unwanted conversions: + // - character set conversions between server and client + // - trailing space removal + // - case-insensitivity + // - language processing like é == e + $sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(255) NOT NULL PRIMARY KEY, $this->dataCol MEDIUMBLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB"; + break; + case 'sqlite': + $sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)"; + break; + case 'pgsql': + $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)"; + break; + case 'oci': + $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(255) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)"; + break; + case 'sqlsrv': + $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)"; + break; + default: + throw new \DomainException(sprintf('Creating the cache table is currently not implemented for PDO driver "%s".', $this->driver)); + } + + $conn->exec($sql); + } + + /** + * Adds the Table to the Schema if the adapter uses this Connection. + * + * @deprecated since symfony/cache 5.4 use DoctrineDbalAdapter instead + */ + public function configureSchema(Schema $schema, Connection $forConnection): void + { + if (isset($this->dbalAdapter)) { + $this->dbalAdapter->configureSchema($schema, $forConnection); + } + } + + /** + * {@inheritdoc} + */ + public function prune() + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->prune(); + } + + $deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= :time"; + + if ('' !== $this->namespace) { + $deleteSql .= " AND $this->idCol LIKE :namespace"; + } + + $connection = $this->getConnection(); + + try { + $delete = $connection->prepare($deleteSql); + } catch (\PDOException $e) { + return true; + } + $delete->bindValue(':time', time(), \PDO::PARAM_INT); + + if ('' !== $this->namespace) { + $delete->bindValue(':namespace', sprintf('%s%%', $this->namespace), \PDO::PARAM_STR); + } + try { + return $delete->execute(); + } catch (\PDOException $e) { + return true; + } + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + $connection = $this->getConnection(); + + $now = time(); + $expired = []; + + $sql = str_pad('', (\count($ids) << 1) - 1, '?,'); + $sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN ($sql)"; + $stmt = $connection->prepare($sql); + $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT); + foreach ($ids as $id) { + $stmt->bindValue(++$i, $id); + } + $result = $stmt->execute(); + + if (\is_object($result)) { + $result = $result->iterateNumeric(); + } else { + $stmt->setFetchMode(\PDO::FETCH_NUM); + $result = $stmt; + } + + foreach ($result as $row) { + if (null === $row[1]) { + $expired[] = $row[0]; + } else { + yield $row[0] => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]); + } + } + + if ($expired) { + $sql = str_pad('', (\count($expired) << 1) - 1, '?,'); + $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN ($sql)"; + $stmt = $connection->prepare($sql); + $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT); + foreach ($expired as $id) { + $stmt->bindValue(++$i, $id); + } + $stmt->execute(); + } + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id) + { + $connection = $this->getConnection(); + + $sql = "SELECT 1 FROM $this->table WHERE $this->idCol = :id AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > :time)"; + $stmt = $connection->prepare($sql); + + $stmt->bindValue(':id', $id); + $stmt->bindValue(':time', time(), \PDO::PARAM_INT); + $stmt->execute(); + + return (bool) $stmt->fetchColumn(); + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace) + { + $conn = $this->getConnection(); + + if ('' === $namespace) { + if ('sqlite' === $this->driver) { + $sql = "DELETE FROM $this->table"; + } else { + $sql = "TRUNCATE TABLE $this->table"; + } + } else { + $sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'"; + } + + try { + $conn->exec($sql); + } catch (\PDOException $e) { + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + $sql = str_pad('', (\count($ids) << 1) - 1, '?,'); + $sql = "DELETE FROM $this->table WHERE $this->idCol IN ($sql)"; + try { + $stmt = $this->getConnection()->prepare($sql); + $stmt->execute(array_values($ids)); + } catch (\PDOException $e) { + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + if (!$values = $this->marshaller->marshall($values, $failed)) { + return $failed; + } + + $conn = $this->getConnection(); + + $driver = $this->driver; + $insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"; + + switch (true) { + case 'mysql' === $driver: + $sql = $insertSql." ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)"; + break; + case 'oci' === $driver: + // DUAL is Oracle specific dummy table + $sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ". + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". + "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?"; + break; + case 'sqlsrv' === $driver && version_compare($this->getServerVersion(), '10', '>='): + // MERGE is only available since SQL Server 2008 and must be terminated by semicolon + // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx + $sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ". + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". + "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;"; + break; + case 'sqlite' === $driver: + $sql = 'INSERT OR REPLACE'.substr($insertSql, 6); + break; + case 'pgsql' === $driver && version_compare($this->getServerVersion(), '9.5', '>='): + $sql = $insertSql." ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)"; + break; + default: + $driver = null; + $sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id"; + break; + } + + $now = time(); + $lifetime = $lifetime ?: null; + try { + $stmt = $conn->prepare($sql); + } catch (\PDOException $e) { + if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) { + $this->createTable(); + } + $stmt = $conn->prepare($sql); + } + + // $id and $data are defined later in the loop. Binding is done by reference, values are read on execution. + if ('sqlsrv' === $driver || 'oci' === $driver) { + $stmt->bindParam(1, $id); + $stmt->bindParam(2, $id); + $stmt->bindParam(3, $data, \PDO::PARAM_LOB); + $stmt->bindValue(4, $lifetime, \PDO::PARAM_INT); + $stmt->bindValue(5, $now, \PDO::PARAM_INT); + $stmt->bindParam(6, $data, \PDO::PARAM_LOB); + $stmt->bindValue(7, $lifetime, \PDO::PARAM_INT); + $stmt->bindValue(8, $now, \PDO::PARAM_INT); + } else { + $stmt->bindParam(':id', $id); + $stmt->bindParam(':data', $data, \PDO::PARAM_LOB); + $stmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT); + $stmt->bindValue(':time', $now, \PDO::PARAM_INT); + } + if (null === $driver) { + $insertStmt = $conn->prepare($insertSql); + + $insertStmt->bindParam(':id', $id); + $insertStmt->bindParam(':data', $data, \PDO::PARAM_LOB); + $insertStmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT); + $insertStmt->bindValue(':time', $now, \PDO::PARAM_INT); + } + + foreach ($values as $id => $data) { + try { + $stmt->execute(); + } catch (\PDOException $e) { + if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) { + $this->createTable(); + } + $stmt->execute(); + } + if (null === $driver && !$stmt->rowCount()) { + try { + $insertStmt->execute(); + } catch (\PDOException $e) { + // A concurrent write won, let it be + } + } + } + + return $failed; + } + + /** + * @internal + */ + protected function getId($key) + { + if ('pgsql' !== $this->driver ?? ($this->getConnection() ? $this->driver : null)) { + return parent::getId($key); + } + + if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) { + $key = rawurlencode($key); + } + + return parent::getId($key); + } + + private function getConnection(): \PDO + { + if (null === $this->conn) { + $this->conn = new \PDO($this->dsn, $this->username, $this->password, $this->connectionOptions); + $this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + } + if (null === $this->driver) { + $this->driver = $this->conn->getAttribute(\PDO::ATTR_DRIVER_NAME); + } + + return $this->conn; + } + + private function getServerVersion(): string + { + if (null === $this->serverVersion) { + $this->serverVersion = $this->conn->getAttribute(\PDO::ATTR_SERVER_VERSION); + } + + return $this->serverVersion; + } + + private function isTableMissing(\PDOException $exception): bool + { + $driver = $this->driver; + [$sqlState, $code] = $exception->errorInfo ?? [null, $exception->getCode()]; + + switch (true) { + case 'pgsql' === $driver && '42P01' === $sqlState: + case 'sqlite' === $driver && str_contains($exception->getMessage(), 'no such table:'): + case 'oci' === $driver && 942 === $code: + case 'sqlsrv' === $driver && 208 === $code: + case 'mysql' === $driver && 1146 === $code: + return true; + default: + return false; + } + } +} diff --git a/vendor/symfony/cache/Adapter/PhpArrayAdapter.php b/vendor/symfony/cache/Adapter/PhpArrayAdapter.php new file mode 100644 index 0000000..43e000a --- /dev/null +++ b/vendor/symfony/cache/Adapter/PhpArrayAdapter.php @@ -0,0 +1,435 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\ContractsTrait; +use Symfony\Component\Cache\Traits\ProxyTrait; +use Symfony\Component\VarExporter\VarExporter; +use Symfony\Contracts\Cache\CacheInterface; + +/** + * Caches items at warm up time using a PHP array that is stored in shared memory by OPCache since PHP 7.0. + * Warmed up items are read-only and run-time discovered items are cached using a fallback adapter. + * + * @author Titouan Galopin + * @author Nicolas Grekas + */ +class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface +{ + use ContractsTrait; + use ProxyTrait; + + private $file; + private $keys; + private $values; + + private static $createCacheItem; + private static $valuesCache = []; + + /** + * @param string $file The PHP file were values are cached + * @param AdapterInterface $fallbackPool A pool to fallback on when an item is not hit + */ + public function __construct(string $file, AdapterInterface $fallbackPool) + { + $this->file = $file; + $this->pool = $fallbackPool; + self::$createCacheItem ?? self::$createCacheItem = \Closure::bind( + static function ($key, $value, $isHit) { + $item = new CacheItem(); + $item->key = $key; + $item->value = $value; + $item->isHit = $isHit; + + return $item; + }, + null, + CacheItem::class + ); + } + + /** + * This adapter takes advantage of how PHP stores arrays in its latest versions. + * + * @param string $file The PHP file were values are cached + * @param CacheItemPoolInterface $fallbackPool A pool to fallback on when an item is not hit + * + * @return CacheItemPoolInterface + */ + public static function create(string $file, CacheItemPoolInterface $fallbackPool) + { + if (!$fallbackPool instanceof AdapterInterface) { + $fallbackPool = new ProxyAdapter($fallbackPool); + } + + return new static($file, $fallbackPool); + } + + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null) + { + if (null === $this->values) { + $this->initialize(); + } + if (!isset($this->keys[$key])) { + get_from_pool: + if ($this->pool instanceof CacheInterface) { + return $this->pool->get($key, $callback, $beta, $metadata); + } + + return $this->doGet($this->pool, $key, $callback, $beta, $metadata); + } + $value = $this->values[$this->keys[$key]]; + + if ('N;' === $value) { + return null; + } + try { + if ($value instanceof \Closure) { + return $value(); + } + } catch (\Throwable $e) { + unset($this->keys[$key]); + goto get_from_pool; + } + + return $value; + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + if (!\is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key))); + } + if (null === $this->values) { + $this->initialize(); + } + if (!isset($this->keys[$key])) { + return $this->pool->getItem($key); + } + + $value = $this->values[$this->keys[$key]]; + $isHit = true; + + if ('N;' === $value) { + $value = null; + } elseif ($value instanceof \Closure) { + try { + $value = $value(); + } catch (\Throwable $e) { + $value = null; + $isHit = false; + } + } + + return (self::$createCacheItem)($key, $value, $isHit); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = []) + { + foreach ($keys as $key) { + if (!\is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key))); + } + } + if (null === $this->values) { + $this->initialize(); + } + + return $this->generateItems($keys); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function hasItem($key) + { + if (!\is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key))); + } + if (null === $this->values) { + $this->initialize(); + } + + return isset($this->keys[$key]) || $this->pool->hasItem($key); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItem($key) + { + if (!\is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key))); + } + if (null === $this->values) { + $this->initialize(); + } + + return !isset($this->keys[$key]) && $this->pool->deleteItem($key); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItems(array $keys) + { + $deleted = true; + $fallbackKeys = []; + + foreach ($keys as $key) { + if (!\is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key))); + } + + if (isset($this->keys[$key])) { + $deleted = false; + } else { + $fallbackKeys[] = $key; + } + } + if (null === $this->values) { + $this->initialize(); + } + + if ($fallbackKeys) { + $deleted = $this->pool->deleteItems($fallbackKeys) && $deleted; + } + + return $deleted; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function save(CacheItemInterface $item) + { + if (null === $this->values) { + $this->initialize(); + } + + return !isset($this->keys[$item->getKey()]) && $this->pool->save($item); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function saveDeferred(CacheItemInterface $item) + { + if (null === $this->values) { + $this->initialize(); + } + + return !isset($this->keys[$item->getKey()]) && $this->pool->saveDeferred($item); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function commit() + { + return $this->pool->commit(); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = '') + { + $this->keys = $this->values = []; + + $cleared = @unlink($this->file) || !file_exists($this->file); + unset(self::$valuesCache[$this->file]); + + if ($this->pool instanceof AdapterInterface) { + return $this->pool->clear($prefix) && $cleared; + } + + return $this->pool->clear() && $cleared; + } + + /** + * Store an array of cached values. + * + * @param array $values The cached values + * + * @return string[] A list of classes to preload on PHP 7.4+ + */ + public function warmUp(array $values) + { + if (file_exists($this->file)) { + if (!is_file($this->file)) { + throw new InvalidArgumentException(sprintf('Cache path exists and is not a file: "%s".', $this->file)); + } + + if (!is_writable($this->file)) { + throw new InvalidArgumentException(sprintf('Cache file is not writable: "%s".', $this->file)); + } + } else { + $directory = \dirname($this->file); + + if (!is_dir($directory) && !@mkdir($directory, 0777, true)) { + throw new InvalidArgumentException(sprintf('Cache directory does not exist and cannot be created: "%s".', $directory)); + } + + if (!is_writable($directory)) { + throw new InvalidArgumentException(sprintf('Cache directory is not writable: "%s".', $directory)); + } + } + + $preload = []; + $dumpedValues = ''; + $dumpedMap = []; + $dump = <<<'EOF' + $value) { + CacheItem::validateKey(\is_int($key) ? (string) $key : $key); + $isStaticValue = true; + + if (null === $value) { + $value = "'N;'"; + } elseif (\is_object($value) || \is_array($value)) { + try { + $value = VarExporter::export($value, $isStaticValue, $preload); + } catch (\Exception $e) { + throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e); + } + } elseif (\is_string($value)) { + // Wrap "N;" in a closure to not confuse it with an encoded `null` + if ('N;' === $value) { + $isStaticValue = false; + } + $value = var_export($value, true); + } elseif (!\is_scalar($value)) { + throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value))); + } else { + $value = var_export($value, true); + } + + if (!$isStaticValue) { + $value = str_replace("\n", "\n ", $value); + $value = "static function () {\n return {$value};\n}"; + } + $hash = hash('md5', $value); + + if (null === $id = $dumpedMap[$hash] ?? null) { + $id = $dumpedMap[$hash] = \count($dumpedMap); + $dumpedValues .= "{$id} => {$value},\n"; + } + + $dump .= var_export($key, true)." => {$id},\n"; + } + + $dump .= "\n], [\n\n{$dumpedValues}\n]];\n"; + + $tmpFile = uniqid($this->file, true); + + file_put_contents($tmpFile, $dump); + @chmod($tmpFile, 0666 & ~umask()); + unset($serialized, $value, $dump); + + @rename($tmpFile, $this->file); + unset(self::$valuesCache[$this->file]); + + $this->initialize(); + + return $preload; + } + + /** + * Load the cache file. + */ + private function initialize() + { + if (isset(self::$valuesCache[$this->file])) { + $values = self::$valuesCache[$this->file]; + } elseif (!is_file($this->file)) { + $this->keys = $this->values = []; + + return; + } else { + $values = self::$valuesCache[$this->file] = (include $this->file) ?: [[], []]; + } + + if (2 !== \count($values) || !isset($values[0], $values[1])) { + $this->keys = $this->values = []; + } else { + [$this->keys, $this->values] = $values; + } + } + + private function generateItems(array $keys): \Generator + { + $f = self::$createCacheItem; + $fallbackKeys = []; + + foreach ($keys as $key) { + if (isset($this->keys[$key])) { + $value = $this->values[$this->keys[$key]]; + + if ('N;' === $value) { + yield $key => $f($key, null, true); + } elseif ($value instanceof \Closure) { + try { + yield $key => $f($key, $value(), true); + } catch (\Throwable $e) { + yield $key => $f($key, null, false); + } + } else { + yield $key => $f($key, $value, true); + } + } else { + $fallbackKeys[] = $key; + } + } + + if ($fallbackKeys) { + yield from $this->pool->getItems($fallbackKeys); + } + } +} diff --git a/vendor/symfony/cache/Adapter/PhpFilesAdapter.php b/vendor/symfony/cache/Adapter/PhpFilesAdapter.php new file mode 100644 index 0000000..8dcd79c --- /dev/null +++ b/vendor/symfony/cache/Adapter/PhpFilesAdapter.php @@ -0,0 +1,330 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\Traits\FilesystemCommonTrait; +use Symfony\Component\VarExporter\VarExporter; + +/** + * @author Piotr Stankowski + * @author Nicolas Grekas + * @author Rob Frawley 2nd + */ +class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface +{ + use FilesystemCommonTrait { + doClear as private doCommonClear; + doDelete as private doCommonDelete; + } + + private $includeHandler; + private $appendOnly; + private $values = []; + private $files = []; + + private static $startTime; + private static $valuesCache = []; + + /** + * @param $appendOnly Set to `true` to gain extra performance when the items stored in this pool never expire. + * Doing so is encouraged because it fits perfectly OPcache's memory model. + * + * @throws CacheException if OPcache is not enabled + */ + public function __construct(string $namespace = '', int $defaultLifetime = 0, ?string $directory = null, bool $appendOnly = false) + { + $this->appendOnly = $appendOnly; + self::$startTime = self::$startTime ?? $_SERVER['REQUEST_TIME'] ?? time(); + parent::__construct('', $defaultLifetime); + $this->init($namespace, $directory); + $this->includeHandler = static function ($type, $msg, $file, $line) { + throw new \ErrorException($msg, 0, $type, $file, $line); + }; + } + + public static function isSupported() + { + self::$startTime = self::$startTime ?? $_SERVER['REQUEST_TIME'] ?? time(); + + return \function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOLEAN)); + } + + /** + * @return bool + */ + public function prune() + { + $time = time(); + $pruned = true; + $getExpiry = true; + + set_error_handler($this->includeHandler); + try { + foreach ($this->scanHashDir($this->directory) as $file) { + try { + if (\is_array($expiresAt = include $file)) { + $expiresAt = $expiresAt[0]; + } + } catch (\ErrorException $e) { + $expiresAt = $time; + } + + if ($time >= $expiresAt) { + $pruned = ($this->doUnlink($file) || !file_exists($file)) && $pruned; + } + } + } finally { + restore_error_handler(); + } + + return $pruned; + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + if ($this->appendOnly) { + $now = 0; + $missingIds = []; + } else { + $now = time(); + $missingIds = $ids; + $ids = []; + } + $values = []; + + begin: + $getExpiry = false; + + foreach ($ids as $id) { + if (null === $value = $this->values[$id] ?? null) { + $missingIds[] = $id; + } elseif ('N;' === $value) { + $values[$id] = null; + } elseif (!\is_object($value)) { + $values[$id] = $value; + } elseif (!$value instanceof LazyValue) { + $values[$id] = $value(); + } elseif (false === $values[$id] = include $value->file) { + unset($values[$id], $this->values[$id]); + $missingIds[] = $id; + } + if (!$this->appendOnly) { + unset($this->values[$id]); + } + } + + if (!$missingIds) { + return $values; + } + + set_error_handler($this->includeHandler); + try { + $getExpiry = true; + + foreach ($missingIds as $k => $id) { + try { + $file = $this->files[$id] ?? $this->files[$id] = $this->getFile($id); + + if (isset(self::$valuesCache[$file])) { + [$expiresAt, $this->values[$id]] = self::$valuesCache[$file]; + } elseif (\is_array($expiresAt = include $file)) { + if ($this->appendOnly) { + self::$valuesCache[$file] = $expiresAt; + } + + [$expiresAt, $this->values[$id]] = $expiresAt; + } elseif ($now < $expiresAt) { + $this->values[$id] = new LazyValue($file); + } + + if ($now >= $expiresAt) { + unset($this->values[$id], $missingIds[$k], self::$valuesCache[$file]); + } + } catch (\ErrorException $e) { + unset($missingIds[$k]); + } + } + } finally { + restore_error_handler(); + } + + $ids = $missingIds; + $missingIds = []; + goto begin; + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id) + { + if ($this->appendOnly && isset($this->values[$id])) { + return true; + } + + set_error_handler($this->includeHandler); + try { + $file = $this->files[$id] ?? $this->files[$id] = $this->getFile($id); + $getExpiry = true; + + if (isset(self::$valuesCache[$file])) { + [$expiresAt, $value] = self::$valuesCache[$file]; + } elseif (\is_array($expiresAt = include $file)) { + if ($this->appendOnly) { + self::$valuesCache[$file] = $expiresAt; + } + + [$expiresAt, $value] = $expiresAt; + } elseif ($this->appendOnly) { + $value = new LazyValue($file); + } + } catch (\ErrorException $e) { + return false; + } finally { + restore_error_handler(); + } + if ($this->appendOnly) { + $now = 0; + $this->values[$id] = $value; + } else { + $now = time(); + } + + return $now < $expiresAt; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + $ok = true; + $expiry = $lifetime ? time() + $lifetime : 'PHP_INT_MAX'; + $allowCompile = self::isSupported(); + + foreach ($values as $key => $value) { + unset($this->values[$key]); + $isStaticValue = true; + if (null === $value) { + $value = "'N;'"; + } elseif (\is_object($value) || \is_array($value)) { + try { + $value = VarExporter::export($value, $isStaticValue); + } catch (\Exception $e) { + throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e); + } + } elseif (\is_string($value)) { + // Wrap "N;" in a closure to not confuse it with an encoded `null` + if ('N;' === $value) { + $isStaticValue = false; + } + $value = var_export($value, true); + } elseif (!\is_scalar($value)) { + throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value))); + } else { + $value = var_export($value, true); + } + + $encodedKey = rawurlencode($key); + + if ($isStaticValue) { + $value = "return [{$expiry}, {$value}];"; + } elseif ($this->appendOnly) { + $value = "return [{$expiry}, static function () { return {$value}; }];"; + } else { + // We cannot use a closure here because of https://bugs.php.net/76982 + $value = str_replace('\Symfony\Component\VarExporter\Internal\\', '', $value); + $value = "namespace Symfony\Component\VarExporter\Internal;\n\nreturn \$getExpiry ? {$expiry} : {$value};"; + } + + $file = $this->files[$key] = $this->getFile($key, true); + // Since OPcache only compiles files older than the script execution start, set the file's mtime in the past + $ok = $this->write($file, "directory)) { + throw new CacheException(sprintf('Cache directory is not writable (%s).', $this->directory)); + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace) + { + $this->values = []; + + return $this->doCommonClear($namespace); + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + foreach ($ids as $id) { + unset($this->values[$id]); + } + + return $this->doCommonDelete($ids); + } + + protected function doUnlink(string $file) + { + unset(self::$valuesCache[$file]); + + if (self::isSupported()) { + @opcache_invalidate($file, true); + } + + return @unlink($file); + } + + private function getFileKey(string $file): string + { + if (!$h = @fopen($file, 'r')) { + return ''; + } + + $encodedKey = substr(fgets($h), 8); + fclose($h); + + return rawurldecode(rtrim($encodedKey)); + } +} + +/** + * @internal + */ +class LazyValue +{ + public $file; + + public function __construct(string $file) + { + $this->file = $file; + } +} diff --git a/vendor/symfony/cache/Adapter/ProxyAdapter.php b/vendor/symfony/cache/Adapter/ProxyAdapter.php new file mode 100644 index 0000000..317018e --- /dev/null +++ b/vendor/symfony/cache/Adapter/ProxyAdapter.php @@ -0,0 +1,268 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\ContractsTrait; +use Symfony\Component\Cache\Traits\ProxyTrait; +use Symfony\Contracts\Cache\CacheInterface; + +/** + * @author Nicolas Grekas + */ +class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface +{ + use ContractsTrait; + use ProxyTrait; + + private $namespace = ''; + private $namespaceLen; + private $poolHash; + private $defaultLifetime; + + private static $createCacheItem; + private static $setInnerItem; + + public function __construct(CacheItemPoolInterface $pool, string $namespace = '', int $defaultLifetime = 0) + { + $this->pool = $pool; + $this->poolHash = $poolHash = spl_object_hash($pool); + if ('' !== $namespace) { + \assert('' !== CacheItem::validateKey($namespace)); + $this->namespace = $namespace; + } + $this->namespaceLen = \strlen($namespace); + $this->defaultLifetime = $defaultLifetime; + self::$createCacheItem ?? self::$createCacheItem = \Closure::bind( + static function ($key, $innerItem, $poolHash) { + $item = new CacheItem(); + $item->key = $key; + + if (null === $innerItem) { + return $item; + } + + $item->value = $v = $innerItem->get(); + $item->isHit = $innerItem->isHit(); + $item->innerItem = $innerItem; + $item->poolHash = $poolHash; + + // Detect wrapped values that encode for their expiry and creation duration + // For compactness, these values are packed in the key of an array using + // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F + if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = (string) array_key_first($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) { + $item->value = $v[$k]; + $v = unpack('Ve/Nc', substr($k, 1, -1)); + $item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET; + $item->metadata[CacheItem::METADATA_CTIME] = $v['c']; + } elseif ($innerItem instanceof CacheItem) { + $item->metadata = $innerItem->metadata; + } + $innerItem->set(null); + + return $item; + }, + null, + CacheItem::class + ); + self::$setInnerItem ?? self::$setInnerItem = \Closure::bind( + /** + * @param array $item A CacheItem cast to (array); accessing protected properties requires adding the "\0*\0" PHP prefix + */ + static function (CacheItemInterface $innerItem, array $item) { + // Tags are stored separately, no need to account for them when considering this item's newly set metadata + if (isset(($metadata = $item["\0*\0newMetadata"])[CacheItem::METADATA_TAGS])) { + unset($metadata[CacheItem::METADATA_TAGS]); + } + if ($metadata) { + // For compactness, expiry and creation duration are packed in the key of an array, using magic numbers as separators + $item["\0*\0value"] = ["\x9D".pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME])."\x5F" => $item["\0*\0value"]]; + } + $innerItem->set($item["\0*\0value"]); + $innerItem->expiresAt(null !== $item["\0*\0expiry"] ? \DateTime::createFromFormat('U.u', sprintf('%.6F', $item["\0*\0expiry"])) : null); + }, + null, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null) + { + if (!$this->pool instanceof CacheInterface) { + return $this->doGet($this, $key, $callback, $beta, $metadata); + } + + return $this->pool->get($this->getId($key), function ($innerItem, bool &$save) use ($key, $callback) { + $item = (self::$createCacheItem)($key, $innerItem, $this->poolHash); + $item->set($value = $callback($item, $save)); + (self::$setInnerItem)($innerItem, (array) $item); + + return $value; + }, $beta, $metadata); + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + $item = $this->pool->getItem($this->getId($key)); + + return (self::$createCacheItem)($key, $item, $this->poolHash); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = []) + { + if ($this->namespaceLen) { + foreach ($keys as $i => $key) { + $keys[$i] = $this->getId($key); + } + } + + return $this->generateItems($this->pool->getItems($keys)); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function hasItem($key) + { + return $this->pool->hasItem($this->getId($key)); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = '') + { + if ($this->pool instanceof AdapterInterface) { + return $this->pool->clear($this->namespace.$prefix); + } + + return $this->pool->clear(); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItem($key) + { + return $this->pool->deleteItem($this->getId($key)); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItems(array $keys) + { + if ($this->namespaceLen) { + foreach ($keys as $i => $key) { + $keys[$i] = $this->getId($key); + } + } + + return $this->pool->deleteItems($keys); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function save(CacheItemInterface $item) + { + return $this->doSave($item, __FUNCTION__); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function saveDeferred(CacheItemInterface $item) + { + return $this->doSave($item, __FUNCTION__); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function commit() + { + return $this->pool->commit(); + } + + private function doSave(CacheItemInterface $item, string $method) + { + if (!$item instanceof CacheItem) { + return false; + } + $item = (array) $item; + if (null === $item["\0*\0expiry"] && 0 < $this->defaultLifetime) { + $item["\0*\0expiry"] = microtime(true) + $this->defaultLifetime; + } + + if ($item["\0*\0poolHash"] === $this->poolHash && $item["\0*\0innerItem"]) { + $innerItem = $item["\0*\0innerItem"]; + } elseif ($this->pool instanceof AdapterInterface) { + // this is an optimization specific for AdapterInterface implementations + // so we can save a round-trip to the backend by just creating a new item + $innerItem = (self::$createCacheItem)($this->namespace.$item["\0*\0key"], null, $this->poolHash); + } else { + $innerItem = $this->pool->getItem($this->namespace.$item["\0*\0key"]); + } + + (self::$setInnerItem)($innerItem, $item); + + return $this->pool->$method($innerItem); + } + + private function generateItems(iterable $items): \Generator + { + $f = self::$createCacheItem; + + foreach ($items as $key => $item) { + if ($this->namespaceLen) { + $key = substr($key, $this->namespaceLen); + } + + yield $key => $f($key, $item, $this->poolHash); + } + } + + private function getId($key): string + { + \assert('' !== CacheItem::validateKey($key)); + + return $this->namespace.$key; + } +} diff --git a/vendor/symfony/cache/Adapter/Psr16Adapter.php b/vendor/symfony/cache/Adapter/Psr16Adapter.php new file mode 100644 index 0000000..a56aa39 --- /dev/null +++ b/vendor/symfony/cache/Adapter/Psr16Adapter.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\SimpleCache\CacheInterface; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\ProxyTrait; + +/** + * Turns a PSR-16 cache into a PSR-6 one. + * + * @author Nicolas Grekas + */ +class Psr16Adapter extends AbstractAdapter implements PruneableInterface, ResettableInterface +{ + use ProxyTrait; + + /** + * @internal + */ + protected const NS_SEPARATOR = '_'; + + private $miss; + + public function __construct(CacheInterface $pool, string $namespace = '', int $defaultLifetime = 0) + { + parent::__construct($namespace, $defaultLifetime); + + $this->pool = $pool; + $this->miss = new \stdClass(); + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + foreach ($this->pool->getMultiple($ids, $this->miss) as $key => $value) { + if ($this->miss !== $value) { + yield $key => $value; + } + } + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id) + { + return $this->pool->has($id); + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace) + { + return $this->pool->clear(); + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + return $this->pool->deleteMultiple($ids); + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + return $this->pool->setMultiple($values, 0 === $lifetime ? null : $lifetime); + } +} diff --git a/vendor/symfony/cache/Adapter/RedisAdapter.php b/vendor/symfony/cache/Adapter/RedisAdapter.php new file mode 100644 index 0000000..86714ae --- /dev/null +++ b/vendor/symfony/cache/Adapter/RedisAdapter.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\Marshaller\MarshallerInterface; +use Symfony\Component\Cache\Traits\RedisClusterProxy; +use Symfony\Component\Cache\Traits\RedisProxy; +use Symfony\Component\Cache\Traits\RedisTrait; + +class RedisAdapter extends AbstractAdapter +{ + use RedisTrait; + + /** + * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis The redis client + * @param string $namespace The default namespace + * @param int $defaultLifetime The default lifetime + */ + public function __construct($redis, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null) + { + $this->init($redis, $namespace, $defaultLifetime, $marshaller); + } +} diff --git a/vendor/symfony/cache/Adapter/RedisTagAwareAdapter.php b/vendor/symfony/cache/Adapter/RedisTagAwareAdapter.php new file mode 100644 index 0000000..958486e --- /dev/null +++ b/vendor/symfony/cache/Adapter/RedisTagAwareAdapter.php @@ -0,0 +1,325 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Predis\Connection\Aggregate\ClusterInterface; +use Predis\Connection\Aggregate\PredisCluster; +use Predis\Connection\Aggregate\ReplicationInterface; +use Predis\Response\ErrorInterface; +use Predis\Response\Status; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Exception\LogicException; +use Symfony\Component\Cache\Marshaller\DeflateMarshaller; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; +use Symfony\Component\Cache\Marshaller\TagAwareMarshaller; +use Symfony\Component\Cache\Traits\RedisClusterProxy; +use Symfony\Component\Cache\Traits\RedisProxy; +use Symfony\Component\Cache\Traits\RedisTrait; + +/** + * Stores tag id <> cache id relationship as a Redis Set. + * + * Set (tag relation info) is stored without expiry (non-volatile), while cache always gets an expiry (volatile) even + * if not set by caller. Thus if you configure redis with the right eviction policy you can be safe this tag <> cache + * relationship survives eviction (cache cleanup when Redis runs out of memory). + * + * Redis server 2.8+ with any `volatile-*` eviction policy, OR `noeviction` if you're sure memory will NEVER fill up + * + * Design limitations: + * - Max 4 billion cache keys per cache tag as limited by Redis Set datatype. + * E.g. If you use a "all" items tag for expiry instead of clear(), that limits you to 4 billion cache items also. + * + * @see https://redis.io/topics/lru-cache#eviction-policies Documentation for Redis eviction policies. + * @see https://redis.io/topics/data-types#sets Documentation for Redis Set datatype. + * + * @author Nicolas Grekas + * @author André Rømcke + */ +class RedisTagAwareAdapter extends AbstractTagAwareAdapter +{ + use RedisTrait; + + /** + * On cache items without a lifetime set, we set it to 100 days. This is to make sure cache items are + * preferred to be evicted over tag Sets, if eviction policy is configured according to requirements. + */ + private const DEFAULT_CACHE_TTL = 8640000; + + /** + * @var string|null detected eviction policy used on Redis server + */ + private $redisEvictionPolicy; + private $namespace; + + /** + * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis The redis client + * @param string $namespace The default namespace + * @param int $defaultLifetime The default lifetime + */ + public function __construct($redis, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null) + { + if ($redis instanceof \Predis\ClientInterface && $redis->getConnection() instanceof ClusterInterface && !$redis->getConnection() instanceof PredisCluster) { + throw new InvalidArgumentException(sprintf('Unsupported Predis cluster connection: only "%s" is, "%s" given.', PredisCluster::class, get_debug_type($redis->getConnection()))); + } + + if (\defined('Redis::OPT_COMPRESSION') && ($redis instanceof \Redis || $redis instanceof \RedisArray || $redis instanceof \RedisCluster)) { + $compression = $redis->getOption(\Redis::OPT_COMPRESSION); + + foreach (\is_array($compression) ? $compression : [$compression] as $c) { + if (\Redis::COMPRESSION_NONE !== $c) { + throw new InvalidArgumentException(sprintf('phpredis compression must be disabled when using "%s", use "%s" instead.', static::class, DeflateMarshaller::class)); + } + } + } + + $this->init($redis, $namespace, $defaultLifetime, new TagAwareMarshaller($marshaller)); + $this->namespace = $namespace; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime, array $addTagData = [], array $delTagData = []): array + { + $eviction = $this->getRedisEvictionPolicy(); + if ('noeviction' !== $eviction && !str_starts_with($eviction, 'volatile-')) { + throw new LogicException(sprintf('Redis maxmemory-policy setting "%s" is *not* supported by RedisTagAwareAdapter, use "noeviction" or "volatile-*" eviction policies.', $eviction)); + } + + // serialize values + if (!$serialized = $this->marshaller->marshall($values, $failed)) { + return $failed; + } + + // While pipeline isn't supported on RedisCluster, other setups will at least benefit from doing this in one op + $results = $this->pipeline(static function () use ($serialized, $lifetime, $addTagData, $delTagData, $failed) { + // Store cache items, force a ttl if none is set, as there is no MSETEX we need to set each one + foreach ($serialized as $id => $value) { + yield 'setEx' => [ + $id, + 0 >= $lifetime ? self::DEFAULT_CACHE_TTL : $lifetime, + $value, + ]; + } + + // Add and Remove Tags + foreach ($addTagData as $tagId => $ids) { + if (!$failed || $ids = array_diff($ids, $failed)) { + yield 'sAdd' => array_merge([$tagId], $ids); + } + } + + foreach ($delTagData as $tagId => $ids) { + if (!$failed || $ids = array_diff($ids, $failed)) { + yield 'sRem' => array_merge([$tagId], $ids); + } + } + }); + + foreach ($results as $id => $result) { + // Skip results of SADD/SREM operations, they'll be 1 or 0 depending on if set value already existed or not + if (is_numeric($result)) { + continue; + } + // setEx results + if (true !== $result && (!$result instanceof Status || Status::get('OK') !== $result)) { + $failed[] = $id; + } + } + + return $failed; + } + + /** + * {@inheritdoc} + */ + protected function doDeleteYieldTags(array $ids): iterable + { + $lua = <<<'EOLUA' + local v = redis.call('GET', KEYS[1]) + local e = redis.pcall('UNLINK', KEYS[1]) + + if type(e) ~= 'number' then + redis.call('DEL', KEYS[1]) + end + + if not v or v:len() <= 13 or v:byte(1) ~= 0x9D or v:byte(6) ~= 0 or v:byte(10) ~= 0x5F then + return '' + end + + return v:sub(14, 13 + v:byte(13) + v:byte(12) * 256 + v:byte(11) * 65536) +EOLUA; + + $results = $this->pipeline(function () use ($ids, $lua) { + foreach ($ids as $id) { + yield 'eval' => $this->redis instanceof \Predis\ClientInterface ? [$lua, 1, $id] : [$lua, [$id], 1]; + } + }); + + foreach ($results as $id => $result) { + if ($result instanceof \RedisException || $result instanceof ErrorInterface) { + CacheItem::log($this->logger, 'Failed to delete key "{key}": '.$result->getMessage(), ['key' => substr($id, \strlen($this->namespace)), 'exception' => $result]); + + continue; + } + + try { + yield $id => !\is_string($result) || '' === $result ? [] : $this->marshaller->unmarshall($result); + } catch (\Exception $e) { + yield $id => []; + } + } + } + + /** + * {@inheritdoc} + */ + protected function doDeleteTagRelations(array $tagData): bool + { + $results = $this->pipeline(static function () use ($tagData) { + foreach ($tagData as $tagId => $idList) { + array_unshift($idList, $tagId); + yield 'sRem' => $idList; + } + }); + foreach ($results as $result) { + // no-op + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doInvalidate(array $tagIds): bool + { + // This script scans the set of items linked to tag: it empties the set + // and removes the linked items. When the set is still not empty after + // the scan, it means we're in cluster mode and that the linked items + // are on other nodes: we move the links to a temporary set and we + // garbage collect that set from the client side. + + $lua = <<<'EOLUA' + redis.replicate_commands() + + local cursor = '0' + local id = KEYS[1] + repeat + local result = redis.call('SSCAN', id, cursor, 'COUNT', 5000); + cursor = result[1]; + local rems = {} + + for _, v in ipairs(result[2]) do + local ok, _ = pcall(redis.call, 'DEL', ARGV[1]..v) + if ok then + table.insert(rems, v) + end + end + if 0 < #rems then + redis.call('SREM', id, unpack(rems)) + end + until '0' == cursor; + + redis.call('SUNIONSTORE', '{'..id..'}'..id, id) + redis.call('DEL', id) + + return redis.call('SSCAN', '{'..id..'}'..id, '0', 'COUNT', 5000) +EOLUA; + + $results = $this->pipeline(function () use ($tagIds, $lua) { + if ($this->redis instanceof \Predis\ClientInterface) { + $prefix = $this->redis->getOptions()->prefix ? $this->redis->getOptions()->prefix->getPrefix() : ''; + } elseif (\is_array($prefix = $this->redis->getOption(\Redis::OPT_PREFIX) ?? '')) { + $prefix = current($prefix); + } + + foreach ($tagIds as $id) { + yield 'eval' => $this->redis instanceof \Predis\ClientInterface ? [$lua, 1, $id, $prefix] : [$lua, [$id, $prefix], 1]; + } + }); + + $lua = <<<'EOLUA' + redis.replicate_commands() + + local id = KEYS[1] + local cursor = table.remove(ARGV) + redis.call('SREM', '{'..id..'}'..id, unpack(ARGV)) + + return redis.call('SSCAN', '{'..id..'}'..id, cursor, 'COUNT', 5000) +EOLUA; + + $success = true; + foreach ($results as $id => $values) { + if ($values instanceof \RedisException || $values instanceof ErrorInterface) { + CacheItem::log($this->logger, 'Failed to invalidate key "{key}": '.$values->getMessage(), ['key' => substr($id, \strlen($this->namespace)), 'exception' => $values]); + $success = false; + + continue; + } + + [$cursor, $ids] = $values; + + while ($ids || '0' !== $cursor) { + $this->doDelete($ids); + + $evalArgs = [$id, $cursor]; + array_splice($evalArgs, 1, 0, $ids); + + if ($this->redis instanceof \Predis\ClientInterface) { + array_unshift($evalArgs, $lua, 1); + } else { + $evalArgs = [$lua, $evalArgs, 1]; + } + + $results = $this->pipeline(function () use ($evalArgs) { + yield 'eval' => $evalArgs; + }); + + foreach ($results as [$cursor, $ids]) { + // no-op + } + } + } + + return $success; + } + + private function getRedisEvictionPolicy(): string + { + if (null !== $this->redisEvictionPolicy) { + return $this->redisEvictionPolicy; + } + + $hosts = $this->getHosts(); + $host = reset($hosts); + if ($host instanceof \Predis\Client && $host->getConnection() instanceof ReplicationInterface) { + // Predis supports info command only on the master in replication environments + $hosts = [$host->getClientFor('master')]; + } + + foreach ($hosts as $host) { + $info = $host->info('Memory'); + + if ($info instanceof ErrorInterface) { + continue; + } + + $info = $info['Memory'] ?? $info; + + return $this->redisEvictionPolicy = $info['maxmemory_policy']; + } + + return $this->redisEvictionPolicy = ''; + } +} diff --git a/vendor/symfony/cache/Adapter/TagAwareAdapter.php b/vendor/symfony/cache/Adapter/TagAwareAdapter.php new file mode 100644 index 0000000..fb59599 --- /dev/null +++ b/vendor/symfony/cache/Adapter/TagAwareAdapter.php @@ -0,0 +1,428 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\InvalidArgumentException; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\ContractsTrait; +use Symfony\Component\Cache\Traits\ProxyTrait; +use Symfony\Contracts\Cache\TagAwareCacheInterface; + +/** + * @author Nicolas Grekas + */ +class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, PruneableInterface, ResettableInterface, LoggerAwareInterface +{ + use ContractsTrait; + use LoggerAwareTrait; + use ProxyTrait; + + public const TAGS_PREFIX = "\0tags\0"; + + private $deferred = []; + private $tags; + private $knownTagVersions = []; + private $knownTagVersionsTtl; + + private static $createCacheItem; + private static $setCacheItemTags; + private static $getTagsByKey; + private static $saveTags; + + public function __construct(AdapterInterface $itemsPool, ?AdapterInterface $tagsPool = null, float $knownTagVersionsTtl = 0.15) + { + $this->pool = $itemsPool; + $this->tags = $tagsPool ?: $itemsPool; + $this->knownTagVersionsTtl = $knownTagVersionsTtl; + self::$createCacheItem ?? self::$createCacheItem = \Closure::bind( + static function ($key, $value, CacheItem $protoItem) { + $item = new CacheItem(); + $item->key = $key; + $item->value = $value; + $item->expiry = $protoItem->expiry; + $item->poolHash = $protoItem->poolHash; + + return $item; + }, + null, + CacheItem::class + ); + self::$setCacheItemTags ?? self::$setCacheItemTags = \Closure::bind( + static function (CacheItem $item, $key, array &$itemTags) { + $item->isTaggable = true; + if (!$item->isHit) { + return $item; + } + if (isset($itemTags[$key])) { + foreach ($itemTags[$key] as $tag => $version) { + $item->metadata[CacheItem::METADATA_TAGS][$tag] = $tag; + } + unset($itemTags[$key]); + } else { + $item->value = null; + $item->isHit = false; + } + + return $item; + }, + null, + CacheItem::class + ); + self::$getTagsByKey ?? self::$getTagsByKey = \Closure::bind( + static function ($deferred) { + $tagsByKey = []; + foreach ($deferred as $key => $item) { + $tagsByKey[$key] = $item->newMetadata[CacheItem::METADATA_TAGS] ?? []; + $item->metadata = $item->newMetadata; + } + + return $tagsByKey; + }, + null, + CacheItem::class + ); + self::$saveTags ?? self::$saveTags = \Closure::bind( + static function (AdapterInterface $tagsAdapter, array $tags) { + ksort($tags); + + foreach ($tags as $v) { + $v->expiry = 0; + $tagsAdapter->saveDeferred($v); + } + + return $tagsAdapter->commit(); + }, + null, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function invalidateTags(array $tags) + { + $ids = []; + foreach ($tags as $tag) { + \assert('' !== CacheItem::validateKey($tag)); + unset($this->knownTagVersions[$tag]); + $ids[] = $tag.static::TAGS_PREFIX; + } + + return !$tags || $this->tags->deleteItems($ids); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function hasItem($key) + { + if (\is_string($key) && isset($this->deferred[$key])) { + $this->commit(); + } + + if (!$this->pool->hasItem($key)) { + return false; + } + + $itemTags = $this->pool->getItem(static::TAGS_PREFIX.$key); + + if (!$itemTags->isHit()) { + return false; + } + + if (!$itemTags = $itemTags->get()) { + return true; + } + + foreach ($this->getTagVersions([$itemTags]) as $tag => $version) { + if ($itemTags[$tag] !== $version) { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + foreach ($this->getItems([$key]) as $item) { + return $item; + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = []) + { + $tagKeys = []; + $commit = false; + + foreach ($keys as $key) { + if ('' !== $key && \is_string($key)) { + $commit = $commit || isset($this->deferred[$key]); + $key = static::TAGS_PREFIX.$key; + $tagKeys[$key] = $key; + } + } + + if ($commit) { + $this->commit(); + } + + try { + $items = $this->pool->getItems($tagKeys + $keys); + } catch (InvalidArgumentException $e) { + $this->pool->getItems($keys); // Should throw an exception + + throw $e; + } + + return $this->generateItems($items, $tagKeys); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = '') + { + if ('' !== $prefix) { + foreach ($this->deferred as $key => $item) { + if (str_starts_with($key, $prefix)) { + unset($this->deferred[$key]); + } + } + } else { + $this->deferred = []; + } + + if ($this->pool instanceof AdapterInterface) { + return $this->pool->clear($prefix); + } + + return $this->pool->clear(); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItem($key) + { + return $this->deleteItems([$key]); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItems(array $keys) + { + foreach ($keys as $key) { + if ('' !== $key && \is_string($key)) { + $keys[] = static::TAGS_PREFIX.$key; + } + } + + return $this->pool->deleteItems($keys); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function save(CacheItemInterface $item) + { + if (!$item instanceof CacheItem) { + return false; + } + $this->deferred[$item->getKey()] = $item; + + return $this->commit(); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function saveDeferred(CacheItemInterface $item) + { + if (!$item instanceof CacheItem) { + return false; + } + $this->deferred[$item->getKey()] = $item; + + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function commit() + { + if (!$this->deferred) { + return true; + } + + $ok = true; + foreach ($this->deferred as $key => $item) { + if (!$this->pool->saveDeferred($item)) { + unset($this->deferred[$key]); + $ok = false; + } + } + + $items = $this->deferred; + $tagsByKey = (self::$getTagsByKey)($items); + $this->deferred = []; + + $tagVersions = $this->getTagVersions($tagsByKey); + $f = self::$createCacheItem; + + foreach ($tagsByKey as $key => $tags) { + $this->pool->saveDeferred($f(static::TAGS_PREFIX.$key, array_intersect_key($tagVersions, $tags), $items[$key])); + } + + return $this->pool->commit() && $ok; + } + + /** + * @return array + */ + public function __sleep() + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + $this->commit(); + } + + private function generateItems(iterable $items, array $tagKeys): \Generator + { + $bufferedItems = $itemTags = []; + $f = self::$setCacheItemTags; + + foreach ($items as $key => $item) { + if (!$tagKeys) { + yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags); + continue; + } + if (!isset($tagKeys[$key])) { + $bufferedItems[$key] = $item; + continue; + } + + unset($tagKeys[$key]); + + if ($item->isHit()) { + $itemTags[$key] = $item->get() ?: []; + } + + if (!$tagKeys) { + $tagVersions = $this->getTagVersions($itemTags); + + foreach ($itemTags as $key => $tags) { + foreach ($tags as $tag => $version) { + if ($tagVersions[$tag] !== $version) { + unset($itemTags[$key]); + continue 2; + } + } + } + $tagVersions = $tagKeys = null; + + foreach ($bufferedItems as $key => $item) { + yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags); + } + $bufferedItems = null; + } + } + } + + private function getTagVersions(array $tagsByKey) + { + $tagVersions = []; + $fetchTagVersions = false; + + foreach ($tagsByKey as $tags) { + $tagVersions += $tags; + + foreach ($tags as $tag => $version) { + if ($tagVersions[$tag] !== $version) { + unset($this->knownTagVersions[$tag]); + } + } + } + + if (!$tagVersions) { + return []; + } + + $now = microtime(true); + $tags = []; + foreach ($tagVersions as $tag => $version) { + $tags[$tag.static::TAGS_PREFIX] = $tag; + if ($fetchTagVersions || ($this->knownTagVersions[$tag][1] ?? null) !== $version || $now - $this->knownTagVersions[$tag][0] >= $this->knownTagVersionsTtl) { + // reuse previously fetched tag versions up to the ttl + $fetchTagVersions = true; + } + } + + if (!$fetchTagVersions) { + return $tagVersions; + } + + $newTags = []; + $newVersion = null; + foreach ($this->tags->getItems(array_keys($tags)) as $tag => $version) { + if (!$version->isHit()) { + $newTags[$tag] = $version->set($newVersion ?? $newVersion = random_int(\PHP_INT_MIN, \PHP_INT_MAX)); + } + $tagVersions[$tag = $tags[$tag]] = $version->get(); + $this->knownTagVersions[$tag] = [$now, $tagVersions[$tag]]; + } + + if ($newTags) { + (self::$saveTags)($this->tags, $newTags); + } + + return $tagVersions; + } +} diff --git a/vendor/symfony/cache/Adapter/TagAwareAdapterInterface.php b/vendor/symfony/cache/Adapter/TagAwareAdapterInterface.php new file mode 100644 index 0000000..afa18d3 --- /dev/null +++ b/vendor/symfony/cache/Adapter/TagAwareAdapterInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\InvalidArgumentException; + +/** + * Interface for invalidating cached items using tags. + * + * @author Nicolas Grekas + */ +interface TagAwareAdapterInterface extends AdapterInterface +{ + /** + * Invalidates cached items using tags. + * + * @param string[] $tags An array of tags to invalidate + * + * @return bool + * + * @throws InvalidArgumentException When $tags is not valid + */ + public function invalidateTags(array $tags); +} diff --git a/vendor/symfony/cache/Adapter/TraceableAdapter.php b/vendor/symfony/cache/Adapter/TraceableAdapter.php new file mode 100644 index 0000000..06951db --- /dev/null +++ b/vendor/symfony/cache/Adapter/TraceableAdapter.php @@ -0,0 +1,295 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Service\ResetInterface; + +/** + * An adapter that collects data about all cache calls. + * + * @author Aaron Scherer + * @author Tobias Nyholm + * @author Nicolas Grekas + */ +class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface +{ + protected $pool; + private $calls = []; + + public function __construct(AdapterInterface $pool) + { + $this->pool = $pool; + } + + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null) + { + if (!$this->pool instanceof CacheInterface) { + throw new \BadMethodCallException(sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', get_debug_type($this->pool), CacheInterface::class)); + } + + $isHit = true; + $callback = function (CacheItem $item, bool &$save) use ($callback, &$isHit) { + $isHit = $item->isHit(); + + return $callback($item, $save); + }; + + $event = $this->start(__FUNCTION__); + try { + $value = $this->pool->get($key, $callback, $beta, $metadata); + $event->result[$key] = get_debug_type($value); + } finally { + $event->end = microtime(true); + } + if ($isHit) { + ++$event->hits; + } else { + ++$event->misses; + } + + return $value; + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + $event = $this->start(__FUNCTION__); + try { + $item = $this->pool->getItem($key); + } finally { + $event->end = microtime(true); + } + if ($event->result[$key] = $item->isHit()) { + ++$event->hits; + } else { + ++$event->misses; + } + + return $item; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function hasItem($key) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result[$key] = $this->pool->hasItem($key); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItem($key) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result[$key] = $this->pool->deleteItem($key); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function save(CacheItemInterface $item) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result[$item->getKey()] = $this->pool->save($item); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function saveDeferred(CacheItemInterface $item) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result[$item->getKey()] = $this->pool->saveDeferred($item); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = []) + { + $event = $this->start(__FUNCTION__); + try { + $result = $this->pool->getItems($keys); + } finally { + $event->end = microtime(true); + } + $f = function () use ($result, $event) { + $event->result = []; + foreach ($result as $key => $item) { + if ($event->result[$key] = $item->isHit()) { + ++$event->hits; + } else { + ++$event->misses; + } + yield $key => $item; + } + }; + + return $f(); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = '') + { + $event = $this->start(__FUNCTION__); + try { + if ($this->pool instanceof AdapterInterface) { + return $event->result = $this->pool->clear($prefix); + } + + return $event->result = $this->pool->clear(); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItems(array $keys) + { + $event = $this->start(__FUNCTION__); + $event->result['keys'] = $keys; + try { + return $event->result['result'] = $this->pool->deleteItems($keys); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function commit() + { + $event = $this->start(__FUNCTION__); + try { + return $event->result = $this->pool->commit(); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function prune() + { + if (!$this->pool instanceof PruneableInterface) { + return false; + } + $event = $this->start(__FUNCTION__); + try { + return $event->result = $this->pool->prune(); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function reset() + { + if ($this->pool instanceof ResetInterface) { + $this->pool->reset(); + } + + $this->clearCalls(); + } + + /** + * {@inheritdoc} + */ + public function delete(string $key): bool + { + $event = $this->start(__FUNCTION__); + try { + return $event->result[$key] = $this->pool->deleteItem($key); + } finally { + $event->end = microtime(true); + } + } + + public function getCalls() + { + return $this->calls; + } + + public function clearCalls() + { + $this->calls = []; + } + + protected function start(string $name) + { + $this->calls[] = $event = new TraceableAdapterEvent(); + $event->name = $name; + $event->start = microtime(true); + + return $event; + } +} + +class TraceableAdapterEvent +{ + public $name; + public $start; + public $end; + public $result; + public $hits = 0; + public $misses = 0; +} diff --git a/vendor/symfony/cache/Adapter/TraceableTagAwareAdapter.php b/vendor/symfony/cache/Adapter/TraceableTagAwareAdapter.php new file mode 100644 index 0000000..69461b8 --- /dev/null +++ b/vendor/symfony/cache/Adapter/TraceableTagAwareAdapter.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Contracts\Cache\TagAwareCacheInterface; + +/** + * @author Robin Chalas + */ +class TraceableTagAwareAdapter extends TraceableAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface +{ + public function __construct(TagAwareAdapterInterface $pool) + { + parent::__construct($pool); + } + + /** + * {@inheritdoc} + */ + public function invalidateTags(array $tags) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result = $this->pool->invalidateTags($tags); + } finally { + $event->end = microtime(true); + } + } +} diff --git a/vendor/symfony/cache/CHANGELOG.md b/vendor/symfony/cache/CHANGELOG.md new file mode 100644 index 0000000..60a8627 --- /dev/null +++ b/vendor/symfony/cache/CHANGELOG.md @@ -0,0 +1,108 @@ +CHANGELOG +========= + +5.4 +--- + + * Deprecate `DoctrineProvider` and `DoctrineAdapter` because these classes have been added to the `doctrine/cache` package + * Add `DoctrineDbalAdapter` identical to `PdoAdapter` for `Doctrine\DBAL\Connection` or DBAL URL + * Deprecate usage of `PdoAdapter` with `Doctrine\DBAL\Connection` or DBAL URL + +5.3 +--- + + * added support for connecting to Redis Sentinel clusters when using the Redis PHP extension + * add support for a custom serializer to the `ApcuAdapter` class + +5.2.0 +----- + + * added integration with Messenger to allow computing cached values in a worker + * allow ISO 8601 time intervals to specify default lifetime + +5.1.0 +----- + + * added max-items + LRU + max-lifetime capabilities to `ArrayCache` + * added `CouchbaseBucketAdapter` + * added context `cache-adapter` to log messages + +5.0.0 +----- + + * removed all PSR-16 implementations in the `Simple` namespace + * removed `SimpleCacheAdapter` + * removed `AbstractAdapter::unserialize()` + * removed `CacheItem::getPreviousTags()` + +4.4.0 +----- + + * added support for connecting to Redis Sentinel clusters + * added argument `$prefix` to `AdapterInterface::clear()` + * improved `RedisTagAwareAdapter` to support Redis server >= 2.8 and up to 4B items per tag + * added `TagAwareMarshaller` for optimized data storage when using `AbstractTagAwareAdapter` + * added `DeflateMarshaller` to compress serialized values + * removed support for phpredis 4 `compression` + * [BC BREAK] `RedisTagAwareAdapter` is not compatible with `RedisCluster` from `Predis` anymore, use `phpredis` instead + * Marked the `CacheDataCollector` class as `@final`. + * added `SodiumMarshaller` to encrypt/decrypt values using libsodium + +4.3.0 +----- + + * removed `psr/simple-cache` dependency, run `composer require psr/simple-cache` if you need it + * deprecated all PSR-16 adapters, use `Psr16Cache` or `Symfony\Contracts\Cache\CacheInterface` implementations instead + * deprecated `SimpleCacheAdapter`, use `Psr16Adapter` instead + +4.2.0 +----- + + * added support for connecting to Redis clusters via DSN + * added support for configuring multiple Memcached servers via DSN + * added `MarshallerInterface` and `DefaultMarshaller` to allow changing the serializer and provide one that automatically uses igbinary when available + * implemented `CacheInterface`, which provides stampede protection via probabilistic early expiration and should become the preferred way to use a cache + * added sub-second expiry accuracy for backends that support it + * added support for phpredis 4 `compression` and `tcp_keepalive` options + * added automatic table creation when using Doctrine DBAL with PDO-based backends + * throw `LogicException` when `CacheItem::tag()` is called on an item coming from a non tag-aware pool + * deprecated `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead + * deprecated the `AbstractAdapter::unserialize()` and `AbstractCache::unserialize()` methods + * added `CacheCollectorPass` (originally in `FrameworkBundle`) + * added `CachePoolClearerPass` (originally in `FrameworkBundle`) + * added `CachePoolPass` (originally in `FrameworkBundle`) + * added `CachePoolPrunerPass` (originally in `FrameworkBundle`) + +3.4.0 +----- + + * added using options from Memcached DSN + * added PruneableInterface so PSR-6 or PSR-16 cache implementations can declare support for manual stale cache pruning + * added prune logic to FilesystemTrait, PhpFilesTrait, PdoTrait, TagAwareAdapter and ChainTrait + * now FilesystemAdapter, PhpFilesAdapter, FilesystemCache, PhpFilesCache, PdoAdapter, PdoCache, ChainAdapter, and + ChainCache implement PruneableInterface and support manual stale cache pruning + +3.3.0 +----- + + * added CacheItem::getPreviousTags() to get bound tags coming from the pool storage if any + * added PSR-16 "Simple Cache" implementations for all existing PSR-6 adapters + * added Psr6Cache and SimpleCacheAdapter for bidirectional interoperability between PSR-6 and PSR-16 + * added MemcachedAdapter (PSR-6) and MemcachedCache (PSR-16) + * added TraceableAdapter (PSR-6) and TraceableCache (PSR-16) + +3.2.0 +----- + + * added TagAwareAdapter for tags-based invalidation + * added PdoAdapter with PDO and Doctrine DBAL support + * added PhpArrayAdapter and PhpFilesAdapter for OPcache-backed shared memory storage (PHP 7+ only) + * added NullAdapter + +3.1.0 +----- + + * added the component with strict PSR-6 implementations + * added ApcuAdapter, ArrayAdapter, FilesystemAdapter and RedisAdapter + * added AbstractAdapter, ChainAdapter and ProxyAdapter + * added DoctrineAdapter and DoctrineProvider for bidirectional interoperability with Doctrine Cache diff --git a/vendor/symfony/cache/CacheItem.php b/vendor/symfony/cache/CacheItem.php new file mode 100644 index 0000000..091d9e9 --- /dev/null +++ b/vendor/symfony/cache/CacheItem.php @@ -0,0 +1,192 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Exception\LogicException; +use Symfony\Contracts\Cache\ItemInterface; + +/** + * @author Nicolas Grekas + */ +final class CacheItem implements ItemInterface +{ + private const METADATA_EXPIRY_OFFSET = 1527506807; + + protected $key; + protected $value; + protected $isHit = false; + protected $expiry; + protected $metadata = []; + protected $newMetadata = []; + protected $innerItem; + protected $poolHash; + protected $isTaggable = false; + + /** + * {@inheritdoc} + */ + public function getKey(): string + { + return $this->key; + } + + /** + * {@inheritdoc} + * + * @return mixed + */ + public function get() + { + return $this->value; + } + + /** + * {@inheritdoc} + */ + public function isHit(): bool + { + return $this->isHit; + } + + /** + * {@inheritdoc} + * + * @return $this + */ + public function set($value): self + { + $this->value = $value; + + return $this; + } + + /** + * {@inheritdoc} + * + * @return $this + */ + public function expiresAt($expiration): self + { + if (null === $expiration) { + $this->expiry = null; + } elseif ($expiration instanceof \DateTimeInterface) { + $this->expiry = (float) $expiration->format('U.u'); + } else { + throw new InvalidArgumentException(sprintf('Expiration date must implement DateTimeInterface or be null, "%s" given.', get_debug_type($expiration))); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @return $this + */ + public function expiresAfter($time): self + { + if (null === $time) { + $this->expiry = null; + } elseif ($time instanceof \DateInterval) { + $this->expiry = microtime(true) + \DateTime::createFromFormat('U', 0)->add($time)->format('U.u'); + } elseif (\is_int($time)) { + $this->expiry = $time + microtime(true); + } else { + throw new InvalidArgumentException(sprintf('Expiration date must be an integer, a DateInterval or null, "%s" given.', get_debug_type($time))); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function tag($tags): ItemInterface + { + if (!$this->isTaggable) { + throw new LogicException(sprintf('Cache item "%s" comes from a non tag-aware pool: you cannot tag it.', $this->key)); + } + if (!is_iterable($tags)) { + $tags = [$tags]; + } + foreach ($tags as $tag) { + if (!\is_string($tag) && !(\is_object($tag) && method_exists($tag, '__toString'))) { + throw new InvalidArgumentException(sprintf('Cache tag must be string or object that implements __toString(), "%s" given.', \is_object($tag) ? \get_class($tag) : \gettype($tag))); + } + $tag = (string) $tag; + if (isset($this->newMetadata[self::METADATA_TAGS][$tag])) { + continue; + } + if ('' === $tag) { + throw new InvalidArgumentException('Cache tag length must be greater than zero.'); + } + if (false !== strpbrk($tag, self::RESERVED_CHARACTERS)) { + throw new InvalidArgumentException(sprintf('Cache tag "%s" contains reserved characters "%s".', $tag, self::RESERVED_CHARACTERS)); + } + $this->newMetadata[self::METADATA_TAGS][$tag] = $tag; + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getMetadata(): array + { + return $this->metadata; + } + + /** + * Validates a cache key according to PSR-6. + * + * @param mixed $key The key to validate + * + * @throws InvalidArgumentException When $key is not valid + */ + public static function validateKey($key): string + { + if (!\is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key))); + } + if ('' === $key) { + throw new InvalidArgumentException('Cache key length must be greater than zero.'); + } + if (false !== strpbrk($key, self::RESERVED_CHARACTERS)) { + throw new InvalidArgumentException(sprintf('Cache key "%s" contains reserved characters "%s".', $key, self::RESERVED_CHARACTERS)); + } + + return $key; + } + + /** + * Internal logging helper. + * + * @internal + */ + public static function log(?LoggerInterface $logger, string $message, array $context = []) + { + if ($logger) { + $logger->warning($message, $context); + } else { + $replace = []; + foreach ($context as $k => $v) { + if (\is_scalar($v)) { + $replace['{'.$k.'}'] = $v; + } + } + @trigger_error(strtr($message, $replace), \E_USER_WARNING); + } + } +} diff --git a/vendor/symfony/cache/DataCollector/CacheDataCollector.php b/vendor/symfony/cache/DataCollector/CacheDataCollector.php new file mode 100644 index 0000000..0479580 --- /dev/null +++ b/vendor/symfony/cache/DataCollector/CacheDataCollector.php @@ -0,0 +1,183 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\DataCollector; + +use Symfony\Component\Cache\Adapter\TraceableAdapter; +use Symfony\Component\Cache\Adapter\TraceableAdapterEvent; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; + +/** + * @author Aaron Scherer + * @author Tobias Nyholm + * + * @final + */ +class CacheDataCollector extends DataCollector implements LateDataCollectorInterface +{ + /** + * @var TraceableAdapter[] + */ + private $instances = []; + + public function addInstance(string $name, TraceableAdapter $instance) + { + $this->instances[$name] = $instance; + } + + /** + * {@inheritdoc} + */ + public function collect(Request $request, Response $response, ?\Throwable $exception = null) + { + $empty = ['calls' => [], 'config' => [], 'options' => [], 'statistics' => []]; + $this->data = ['instances' => $empty, 'total' => $empty]; + foreach ($this->instances as $name => $instance) { + $this->data['instances']['calls'][$name] = $instance->getCalls(); + } + + $this->data['instances']['statistics'] = $this->calculateStatistics(); + $this->data['total']['statistics'] = $this->calculateTotalStatistics(); + } + + public function reset() + { + $this->data = []; + foreach ($this->instances as $instance) { + $instance->clearCalls(); + } + } + + public function lateCollect() + { + $this->data['instances']['calls'] = $this->cloneVar($this->data['instances']['calls']); + } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return 'cache'; + } + + /** + * Method returns amount of logged Cache reads: "get" calls. + */ + public function getStatistics(): array + { + return $this->data['instances']['statistics']; + } + + /** + * Method returns the statistic totals. + */ + public function getTotals(): array + { + return $this->data['total']['statistics']; + } + + /** + * Method returns all logged Cache call objects. + * + * @return mixed + */ + public function getCalls() + { + return $this->data['instances']['calls']; + } + + private function calculateStatistics(): array + { + $statistics = []; + foreach ($this->data['instances']['calls'] as $name => $calls) { + $statistics[$name] = [ + 'calls' => 0, + 'time' => 0, + 'reads' => 0, + 'writes' => 0, + 'deletes' => 0, + 'hits' => 0, + 'misses' => 0, + ]; + /** @var TraceableAdapterEvent $call */ + foreach ($calls as $call) { + ++$statistics[$name]['calls']; + $statistics[$name]['time'] += ($call->end ?? microtime(true)) - $call->start; + if ('get' === $call->name) { + ++$statistics[$name]['reads']; + if ($call->hits) { + ++$statistics[$name]['hits']; + } else { + ++$statistics[$name]['misses']; + ++$statistics[$name]['writes']; + } + } elseif ('getItem' === $call->name) { + ++$statistics[$name]['reads']; + if ($call->hits) { + ++$statistics[$name]['hits']; + } else { + ++$statistics[$name]['misses']; + } + } elseif ('getItems' === $call->name) { + $statistics[$name]['reads'] += $call->hits + $call->misses; + $statistics[$name]['hits'] += $call->hits; + $statistics[$name]['misses'] += $call->misses; + } elseif ('hasItem' === $call->name) { + ++$statistics[$name]['reads']; + foreach ($call->result ?? [] as $result) { + ++$statistics[$name][$result ? 'hits' : 'misses']; + } + } elseif ('save' === $call->name) { + ++$statistics[$name]['writes']; + } elseif ('deleteItem' === $call->name) { + ++$statistics[$name]['deletes']; + } + } + if ($statistics[$name]['reads']) { + $statistics[$name]['hit_read_ratio'] = round(100 * $statistics[$name]['hits'] / $statistics[$name]['reads'], 2); + } else { + $statistics[$name]['hit_read_ratio'] = null; + } + } + + return $statistics; + } + + private function calculateTotalStatistics(): array + { + $statistics = $this->getStatistics(); + $totals = [ + 'calls' => 0, + 'time' => 0, + 'reads' => 0, + 'writes' => 0, + 'deletes' => 0, + 'hits' => 0, + 'misses' => 0, + ]; + foreach ($statistics as $name => $values) { + foreach ($totals as $key => $value) { + $totals[$key] += $statistics[$name][$key]; + } + } + if ($totals['reads']) { + $totals['hit_read_ratio'] = round(100 * $totals['hits'] / $totals['reads'], 2); + } else { + $totals['hit_read_ratio'] = null; + } + + return $totals; + } +} diff --git a/vendor/symfony/cache/DependencyInjection/CacheCollectorPass.php b/vendor/symfony/cache/DependencyInjection/CacheCollectorPass.php new file mode 100644 index 0000000..843232e --- /dev/null +++ b/vendor/symfony/cache/DependencyInjection/CacheCollectorPass.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\DependencyInjection; + +use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface; +use Symfony\Component\Cache\Adapter\TraceableAdapter; +use Symfony\Component\Cache\Adapter\TraceableTagAwareAdapter; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Inject a data collector to all the cache services to be able to get detailed statistics. + * + * @author Tobias Nyholm + */ +class CacheCollectorPass implements CompilerPassInterface +{ + private $dataCollectorCacheId; + private $cachePoolTag; + private $cachePoolRecorderInnerSuffix; + + public function __construct(string $dataCollectorCacheId = 'data_collector.cache', string $cachePoolTag = 'cache.pool', string $cachePoolRecorderInnerSuffix = '.recorder_inner') + { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/cache', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + + $this->dataCollectorCacheId = $dataCollectorCacheId; + $this->cachePoolTag = $cachePoolTag; + $this->cachePoolRecorderInnerSuffix = $cachePoolRecorderInnerSuffix; + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition($this->dataCollectorCacheId)) { + return; + } + + foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $attributes) { + $poolName = $attributes[0]['name'] ?? $id; + + $this->addToCollector($id, $poolName, $container); + } + } + + private function addToCollector(string $id, string $name, ContainerBuilder $container) + { + $definition = $container->getDefinition($id); + if ($definition->isAbstract()) { + return; + } + + $collectorDefinition = $container->getDefinition($this->dataCollectorCacheId); + $recorder = new Definition(is_subclass_of($definition->getClass(), TagAwareAdapterInterface::class) ? TraceableTagAwareAdapter::class : TraceableAdapter::class); + $recorder->setTags($definition->getTags()); + if (!$definition->isPublic() || !$definition->isPrivate()) { + $recorder->setPublic($definition->isPublic()); + } + $recorder->setArguments([new Reference($innerId = $id.$this->cachePoolRecorderInnerSuffix)]); + + foreach ($definition->getMethodCalls() as [$method, $args]) { + if ('setCallbackWrapper' !== $method || !$args[0] instanceof Definition || !($args[0]->getArguments()[2] ?? null) instanceof Definition) { + continue; + } + if ([new Reference($id), 'setCallbackWrapper'] == $args[0]->getArguments()[2]->getFactory()) { + $args[0]->getArguments()[2]->setFactory([new Reference($innerId), 'setCallbackWrapper']); + } + } + + $definition->setTags([]); + $definition->setPublic(false); + + $container->setDefinition($innerId, $definition); + $container->setDefinition($id, $recorder); + + // Tell the collector to add the new instance + $collectorDefinition->addMethodCall('addInstance', [$name, new Reference($id)]); + $collectorDefinition->setPublic(false); + } +} diff --git a/vendor/symfony/cache/DependencyInjection/CachePoolClearerPass.php b/vendor/symfony/cache/DependencyInjection/CachePoolClearerPass.php new file mode 100644 index 0000000..c9b04ad --- /dev/null +++ b/vendor/symfony/cache/DependencyInjection/CachePoolClearerPass.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Nicolas Grekas + */ +class CachePoolClearerPass implements CompilerPassInterface +{ + private $cachePoolClearerTag; + + public function __construct(string $cachePoolClearerTag = 'cache.pool.clearer') + { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/cache', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + + $this->cachePoolClearerTag = $cachePoolClearerTag; + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + $container->getParameterBag()->remove('cache.prefix.seed'); + + foreach ($container->findTaggedServiceIds($this->cachePoolClearerTag) as $id => $attr) { + $clearer = $container->getDefinition($id); + $pools = []; + foreach ($clearer->getArgument(0) as $name => $ref) { + if ($container->hasDefinition($ref)) { + $pools[$name] = new Reference($ref); + } + } + $clearer->replaceArgument(0, $pools); + } + } +} diff --git a/vendor/symfony/cache/DependencyInjection/CachePoolPass.php b/vendor/symfony/cache/DependencyInjection/CachePoolPass.php new file mode 100644 index 0000000..ee539af --- /dev/null +++ b/vendor/symfony/cache/DependencyInjection/CachePoolPass.php @@ -0,0 +1,274 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\DependencyInjection; + +use Symfony\Component\Cache\Adapter\AbstractAdapter; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\ChainAdapter; +use Symfony\Component\Cache\Adapter\NullAdapter; +use Symfony\Component\Cache\Adapter\ParameterNormalizer; +use Symfony\Component\Cache\Messenger\EarlyExpirationDispatcher; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Nicolas Grekas + */ +class CachePoolPass implements CompilerPassInterface +{ + private $cachePoolTag; + private $kernelResetTag; + private $cacheClearerId; + private $cachePoolClearerTag; + private $cacheSystemClearerId; + private $cacheSystemClearerTag; + private $reverseContainerId; + private $reversibleTag; + private $messageHandlerId; + + public function __construct(string $cachePoolTag = 'cache.pool', string $kernelResetTag = 'kernel.reset', string $cacheClearerId = 'cache.global_clearer', string $cachePoolClearerTag = 'cache.pool.clearer', string $cacheSystemClearerId = 'cache.system_clearer', string $cacheSystemClearerTag = 'kernel.cache_clearer', string $reverseContainerId = 'reverse_container', string $reversibleTag = 'container.reversible', string $messageHandlerId = 'cache.early_expiration_handler') + { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/cache', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + + $this->cachePoolTag = $cachePoolTag; + $this->kernelResetTag = $kernelResetTag; + $this->cacheClearerId = $cacheClearerId; + $this->cachePoolClearerTag = $cachePoolClearerTag; + $this->cacheSystemClearerId = $cacheSystemClearerId; + $this->cacheSystemClearerTag = $cacheSystemClearerTag; + $this->reverseContainerId = $reverseContainerId; + $this->reversibleTag = $reversibleTag; + $this->messageHandlerId = $messageHandlerId; + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if ($container->hasParameter('cache.prefix.seed')) { + $seed = $container->getParameterBag()->resolveValue($container->getParameter('cache.prefix.seed')); + } else { + $seed = '_'.$container->getParameter('kernel.project_dir'); + $seed .= '.'.$container->getParameter('kernel.container_class'); + } + + $needsMessageHandler = false; + $allPools = []; + $clearers = []; + $attributes = [ + 'provider', + 'name', + 'namespace', + 'default_lifetime', + 'early_expiration_message_bus', + 'reset', + ]; + foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $tags) { + $adapter = $pool = $container->getDefinition($id); + if ($pool->isAbstract()) { + continue; + } + $class = $adapter->getClass(); + while ($adapter instanceof ChildDefinition) { + $adapter = $container->findDefinition($adapter->getParent()); + $class = $class ?: $adapter->getClass(); + if ($t = $adapter->getTag($this->cachePoolTag)) { + $tags[0] += $t[0]; + } + } + $name = $tags[0]['name'] ?? $id; + if (!isset($tags[0]['namespace'])) { + $namespaceSeed = $seed; + if (null !== $class) { + $namespaceSeed .= '.'.$class; + } + + $tags[0]['namespace'] = $this->getNamespace($namespaceSeed, $name); + } + if (isset($tags[0]['clearer'])) { + $clearer = $tags[0]['clearer']; + while ($container->hasAlias($clearer)) { + $clearer = (string) $container->getAlias($clearer); + } + } else { + $clearer = null; + } + unset($tags[0]['clearer'], $tags[0]['name']); + + if (isset($tags[0]['provider'])) { + $tags[0]['provider'] = new Reference(static::getServiceProvider($container, $tags[0]['provider'])); + } + + if (ChainAdapter::class === $class) { + $adapters = []; + foreach ($adapter->getArgument(0) as $provider => $adapter) { + if ($adapter instanceof ChildDefinition) { + $chainedPool = $adapter; + } else { + $chainedPool = $adapter = new ChildDefinition($adapter); + } + + $chainedTags = [\is_int($provider) ? [] : ['provider' => $provider]]; + $chainedClass = ''; + + while ($adapter instanceof ChildDefinition) { + $adapter = $container->findDefinition($adapter->getParent()); + $chainedClass = $chainedClass ?: $adapter->getClass(); + if ($t = $adapter->getTag($this->cachePoolTag)) { + $chainedTags[0] += $t[0]; + } + } + + if (ChainAdapter::class === $chainedClass) { + throw new InvalidArgumentException(sprintf('Invalid service "%s": chain of adapters cannot reference another chain, found "%s".', $id, $chainedPool->getParent())); + } + + $i = 0; + + if (isset($chainedTags[0]['provider'])) { + $chainedPool->replaceArgument($i++, new Reference(static::getServiceProvider($container, $chainedTags[0]['provider']))); + } + + if (isset($tags[0]['namespace']) && !\in_array($adapter->getClass(), [ArrayAdapter::class, NullAdapter::class], true)) { + $chainedPool->replaceArgument($i++, $tags[0]['namespace']); + } + + if (isset($tags[0]['default_lifetime'])) { + $chainedPool->replaceArgument($i++, $tags[0]['default_lifetime']); + } + + $adapters[] = $chainedPool; + } + + $pool->replaceArgument(0, $adapters); + unset($tags[0]['provider'], $tags[0]['namespace']); + $i = 1; + } else { + $i = 0; + } + + foreach ($attributes as $attr) { + if (!isset($tags[0][$attr])) { + // no-op + } elseif ('reset' === $attr) { + if ($tags[0][$attr]) { + $pool->addTag($this->kernelResetTag, ['method' => $tags[0][$attr]]); + } + } elseif ('early_expiration_message_bus' === $attr) { + $needsMessageHandler = true; + $pool->addMethodCall('setCallbackWrapper', [(new Definition(EarlyExpirationDispatcher::class)) + ->addArgument(new Reference($tags[0]['early_expiration_message_bus'])) + ->addArgument(new Reference($this->reverseContainerId)) + ->addArgument((new Definition('callable')) + ->setFactory([new Reference($id), 'setCallbackWrapper']) + ->addArgument(null) + ), + ]); + $pool->addTag($this->reversibleTag); + } elseif ('namespace' !== $attr || !\in_array($class, [ArrayAdapter::class, NullAdapter::class], true)) { + $argument = $tags[0][$attr]; + + if ('default_lifetime' === $attr && !is_numeric($argument)) { + $argument = (new Definition('int', [$argument])) + ->setFactory([ParameterNormalizer::class, 'normalizeDuration']); + } + + $pool->replaceArgument($i++, $argument); + } + unset($tags[0][$attr]); + } + if (!empty($tags[0])) { + throw new InvalidArgumentException(sprintf('Invalid "%s" tag for service "%s": accepted attributes are "clearer", "provider", "name", "namespace", "default_lifetime", "early_expiration_message_bus" and "reset", found "%s".', $this->cachePoolTag, $id, implode('", "', array_keys($tags[0])))); + } + + if (null !== $clearer) { + $clearers[$clearer][$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE); + } + + $allPools[$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE); + } + + if (!$needsMessageHandler) { + $container->removeDefinition($this->messageHandlerId); + } + + $notAliasedCacheClearerId = $this->cacheClearerId; + while ($container->hasAlias($notAliasedCacheClearerId)) { + $notAliasedCacheClearerId = (string) $container->getAlias($notAliasedCacheClearerId); + } + if ($container->hasDefinition($notAliasedCacheClearerId)) { + $clearers[$notAliasedCacheClearerId] = $allPools; + } + + foreach ($clearers as $id => $pools) { + $clearer = $container->getDefinition($id); + if ($clearer instanceof ChildDefinition) { + $clearer->replaceArgument(0, $pools); + } else { + $clearer->setArgument(0, $pools); + } + $clearer->addTag($this->cachePoolClearerTag); + + if ($this->cacheSystemClearerId === $id) { + $clearer->addTag($this->cacheSystemClearerTag); + } + } + + $allPoolsKeys = array_keys($allPools); + + if ($container->hasDefinition('console.command.cache_pool_list')) { + $container->getDefinition('console.command.cache_pool_list')->replaceArgument(0, $allPoolsKeys); + } + + if ($container->hasDefinition('console.command.cache_pool_clear')) { + $container->getDefinition('console.command.cache_pool_clear')->addArgument($allPoolsKeys); + } + + if ($container->hasDefinition('console.command.cache_pool_delete')) { + $container->getDefinition('console.command.cache_pool_delete')->addArgument($allPoolsKeys); + } + } + + private function getNamespace(string $seed, string $id) + { + return substr(str_replace('/', '-', base64_encode(hash('sha256', $id.$seed, true))), 0, 10); + } + + /** + * @internal + */ + public static function getServiceProvider(ContainerBuilder $container, string $name) + { + $container->resolveEnvPlaceholders($name, null, $usedEnvs); + + if ($usedEnvs || preg_match('#^[a-z]++:#', $name)) { + $dsn = $name; + + if (!$container->hasDefinition($name = '.cache_connection.'.ContainerBuilder::hash($dsn))) { + $definition = new Definition(AbstractAdapter::class); + $definition->setPublic(false); + $definition->setFactory([AbstractAdapter::class, 'createConnection']); + $definition->setArguments([$dsn, ['lazy' => true]]); + $container->setDefinition($name, $definition); + } + } + + return $name; + } +} diff --git a/vendor/symfony/cache/DependencyInjection/CachePoolPrunerPass.php b/vendor/symfony/cache/DependencyInjection/CachePoolPrunerPass.php new file mode 100644 index 0000000..86a1906 --- /dev/null +++ b/vendor/symfony/cache/DependencyInjection/CachePoolPrunerPass.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\DependencyInjection; + +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Rob Frawley 2nd + */ +class CachePoolPrunerPass implements CompilerPassInterface +{ + private $cacheCommandServiceId; + private $cachePoolTag; + + public function __construct(string $cacheCommandServiceId = 'console.command.cache_pool_prune', string $cachePoolTag = 'cache.pool') + { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/cache', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + + $this->cacheCommandServiceId = $cacheCommandServiceId; + $this->cachePoolTag = $cachePoolTag; + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition($this->cacheCommandServiceId)) { + return; + } + + $services = []; + + foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $tags) { + $class = $container->getParameterBag()->resolveValue($container->getDefinition($id)->getClass()); + + if (!$reflection = $container->getReflectionClass($class)) { + throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); + } + + if ($reflection->implementsInterface(PruneableInterface::class)) { + $services[$id] = new Reference($id); + } + } + + $container->getDefinition($this->cacheCommandServiceId)->replaceArgument(0, new IteratorArgument($services)); + } +} diff --git a/vendor/symfony/cache/DoctrineProvider.php b/vendor/symfony/cache/DoctrineProvider.php new file mode 100644 index 0000000..7b55aae --- /dev/null +++ b/vendor/symfony/cache/DoctrineProvider.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +use Doctrine\Common\Cache\CacheProvider; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Contracts\Service\ResetInterface; + +if (!class_exists(CacheProvider::class)) { + return; +} + +/** + * @author Nicolas Grekas + * + * @deprecated Use Doctrine\Common\Cache\Psr6\DoctrineProvider instead + */ +class DoctrineProvider extends CacheProvider implements PruneableInterface, ResettableInterface +{ + private $pool; + + public function __construct(CacheItemPoolInterface $pool) + { + trigger_deprecation('symfony/cache', '5.4', '"%s" is deprecated, use "Doctrine\Common\Cache\Psr6\DoctrineProvider" instead.', __CLASS__); + + $this->pool = $pool; + } + + /** + * {@inheritdoc} + */ + public function prune() + { + return $this->pool instanceof PruneableInterface && $this->pool->prune(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + if ($this->pool instanceof ResetInterface) { + $this->pool->reset(); + } + $this->setNamespace($this->getNamespace()); + } + + /** + * {@inheritdoc} + * + * @return mixed + */ + protected function doFetch($id) + { + $item = $this->pool->getItem(rawurlencode($id)); + + return $item->isHit() ? $item->get() : false; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + protected function doContains($id) + { + return $this->pool->hasItem(rawurlencode($id)); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + protected function doSave($id, $data, $lifeTime = 0) + { + $item = $this->pool->getItem(rawurlencode($id)); + + if (0 < $lifeTime) { + $item->expiresAfter($lifeTime); + } + + return $this->pool->save($item->set($data)); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + protected function doDelete($id) + { + return $this->pool->deleteItem(rawurlencode($id)); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + protected function doFlush() + { + return $this->pool->clear(); + } + + /** + * {@inheritdoc} + * + * @return array|null + */ + protected function doGetStats() + { + return null; + } +} diff --git a/vendor/symfony/cache/Exception/CacheException.php b/vendor/symfony/cache/Exception/CacheException.php new file mode 100644 index 0000000..d2e975b --- /dev/null +++ b/vendor/symfony/cache/Exception/CacheException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Exception; + +use Psr\Cache\CacheException as Psr6CacheInterface; +use Psr\SimpleCache\CacheException as SimpleCacheInterface; + +if (interface_exists(SimpleCacheInterface::class)) { + class CacheException extends \Exception implements Psr6CacheInterface, SimpleCacheInterface + { + } +} else { + class CacheException extends \Exception implements Psr6CacheInterface + { + } +} diff --git a/vendor/symfony/cache/Exception/InvalidArgumentException.php b/vendor/symfony/cache/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..7f9584a --- /dev/null +++ b/vendor/symfony/cache/Exception/InvalidArgumentException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Exception; + +use Psr\Cache\InvalidArgumentException as Psr6CacheInterface; +use Psr\SimpleCache\InvalidArgumentException as SimpleCacheInterface; + +if (interface_exists(SimpleCacheInterface::class)) { + class InvalidArgumentException extends \InvalidArgumentException implements Psr6CacheInterface, SimpleCacheInterface + { + } +} else { + class InvalidArgumentException extends \InvalidArgumentException implements Psr6CacheInterface + { + } +} diff --git a/vendor/symfony/cache/Exception/LogicException.php b/vendor/symfony/cache/Exception/LogicException.php new file mode 100644 index 0000000..9ffa7ed --- /dev/null +++ b/vendor/symfony/cache/Exception/LogicException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Exception; + +use Psr\Cache\CacheException as Psr6CacheInterface; +use Psr\SimpleCache\CacheException as SimpleCacheInterface; + +if (interface_exists(SimpleCacheInterface::class)) { + class LogicException extends \LogicException implements Psr6CacheInterface, SimpleCacheInterface + { + } +} else { + class LogicException extends \LogicException implements Psr6CacheInterface + { + } +} diff --git a/vendor/symfony/cache/LICENSE b/vendor/symfony/cache/LICENSE new file mode 100644 index 0000000..0223acd --- /dev/null +++ b/vendor/symfony/cache/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2016-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/cache/LockRegistry.php b/vendor/symfony/cache/LockRegistry.php new file mode 100644 index 0000000..d0c5fc5 --- /dev/null +++ b/vendor/symfony/cache/LockRegistry.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +use Psr\Log\LoggerInterface; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; + +/** + * LockRegistry is used internally by existing adapters to protect against cache stampede. + * + * It does so by wrapping the computation of items in a pool of locks. + * Foreach each apps, there can be at most 20 concurrent processes that + * compute items at the same time and only one per cache-key. + * + * @author Nicolas Grekas + */ +final class LockRegistry +{ + private static $openedFiles = []; + private static $lockedFiles; + private static $signalingException; + private static $signalingCallback; + + /** + * The number of items in this list controls the max number of concurrent processes. + */ + private static $files = [ + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractTagAwareAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AdapterInterface.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ApcuAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ArrayAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ChainAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'CouchbaseBucketAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'CouchbaseCollectionAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'DoctrineAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'DoctrineDbalAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemTagAwareAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'MemcachedAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'NullAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ParameterNormalizer.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PdoAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PhpArrayAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PhpFilesAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ProxyAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'Psr16Adapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisTagAwareAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapterInterface.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TraceableAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TraceableTagAwareAdapter.php', + ]; + + /** + * Defines a set of existing files that will be used as keys to acquire locks. + * + * @return array The previously defined set of files + */ + public static function setFiles(array $files): array + { + $previousFiles = self::$files; + self::$files = $files; + + foreach (self::$openedFiles as $file) { + if ($file) { + flock($file, \LOCK_UN); + fclose($file); + } + } + self::$openedFiles = self::$lockedFiles = []; + + return $previousFiles; + } + + public static function compute(callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, ?\Closure $setMetadata = null, ?LoggerInterface $logger = null) + { + if ('\\' === \DIRECTORY_SEPARATOR && null === self::$lockedFiles) { + // disable locking on Windows by default + self::$files = self::$lockedFiles = []; + } + + $key = self::$files ? abs(crc32($item->getKey())) % \count(self::$files) : -1; + + if ($key < 0 || self::$lockedFiles || !$lock = self::open($key)) { + return $callback($item, $save); + } + + self::$signalingException ?? self::$signalingException = unserialize("O:9:\"Exception\":1:{s:16:\"\0Exception\0trace\";a:0:{}}"); + self::$signalingCallback ?? self::$signalingCallback = function () { throw self::$signalingException; }; + + while (true) { + try { + $locked = false; + // race to get the lock in non-blocking mode + $locked = flock($lock, \LOCK_EX | \LOCK_NB, $wouldBlock); + + if ($locked || !$wouldBlock) { + $logger && $logger->info(sprintf('Lock %s, now computing item "{key}"', $locked ? 'acquired' : 'not supported'), ['key' => $item->getKey()]); + self::$lockedFiles[$key] = true; + + $value = $callback($item, $save); + + if ($save) { + if ($setMetadata) { + $setMetadata($item); + } + + $pool->save($item->set($value)); + $save = false; + } + + return $value; + } + // if we failed the race, retry locking in blocking mode to wait for the winner + $logger && $logger->info('Item "{key}" is locked, waiting for it to be released', ['key' => $item->getKey()]); + flock($lock, \LOCK_SH); + } finally { + flock($lock, \LOCK_UN); + unset(self::$lockedFiles[$key]); + } + + try { + $value = $pool->get($item->getKey(), self::$signalingCallback, 0); + $logger && $logger->info('Item "{key}" retrieved after lock was released', ['key' => $item->getKey()]); + $save = false; + + return $value; + } catch (\Exception $e) { + if (self::$signalingException !== $e) { + throw $e; + } + $logger && $logger->info('Item "{key}" not found while lock was released, now retrying', ['key' => $item->getKey()]); + } + } + + return null; + } + + private static function open(int $key) + { + if (null !== $h = self::$openedFiles[$key] ?? null) { + return $h; + } + set_error_handler(function () {}); + try { + $h = fopen(self::$files[$key], 'r+'); + } finally { + restore_error_handler(); + } + + return self::$openedFiles[$key] = $h ?: @fopen(self::$files[$key], 'r'); + } +} diff --git a/vendor/symfony/cache/Marshaller/DefaultMarshaller.php b/vendor/symfony/cache/Marshaller/DefaultMarshaller.php new file mode 100644 index 0000000..43f7e7e --- /dev/null +++ b/vendor/symfony/cache/Marshaller/DefaultMarshaller.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Marshaller; + +use Symfony\Component\Cache\Exception\CacheException; + +/** + * Serializes/unserializes values using igbinary_serialize() if available, serialize() otherwise. + * + * @author Nicolas Grekas + */ +class DefaultMarshaller implements MarshallerInterface +{ + private $useIgbinarySerialize = true; + private $throwOnSerializationFailure; + + public function __construct(?bool $useIgbinarySerialize = null, bool $throwOnSerializationFailure = false) + { + if (null === $useIgbinarySerialize) { + $useIgbinarySerialize = \extension_loaded('igbinary') && (\PHP_VERSION_ID < 70400 || version_compare('3.1.6', phpversion('igbinary'), '<=')); + } elseif ($useIgbinarySerialize && (!\extension_loaded('igbinary') || (\PHP_VERSION_ID >= 70400 && version_compare('3.1.6', phpversion('igbinary'), '>')))) { + throw new CacheException(\extension_loaded('igbinary') && \PHP_VERSION_ID >= 70400 ? 'Please upgrade the "igbinary" PHP extension to v3.1.6 or higher.' : 'The "igbinary" PHP extension is not loaded.'); + } + $this->useIgbinarySerialize = $useIgbinarySerialize; + $this->throwOnSerializationFailure = $throwOnSerializationFailure; + } + + /** + * {@inheritdoc} + */ + public function marshall(array $values, ?array &$failed): array + { + $serialized = $failed = []; + + foreach ($values as $id => $value) { + try { + if ($this->useIgbinarySerialize) { + $serialized[$id] = igbinary_serialize($value); + } else { + $serialized[$id] = serialize($value); + } + } catch (\Exception $e) { + if ($this->throwOnSerializationFailure) { + throw new \ValueError($e->getMessage(), 0, $e); + } + $failed[] = $id; + } + } + + return $serialized; + } + + /** + * {@inheritdoc} + */ + public function unmarshall(string $value) + { + if ('b:0;' === $value) { + return false; + } + if ('N;' === $value) { + return null; + } + static $igbinaryNull; + if ($value === ($igbinaryNull ?? $igbinaryNull = \extension_loaded('igbinary') ? igbinary_serialize(null) : false)) { + return null; + } + $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback'); + try { + if (':' === ($value[1] ?? ':')) { + if (false !== $value = unserialize($value)) { + return $value; + } + } elseif (false === $igbinaryNull) { + throw new \RuntimeException('Failed to unserialize values, did you forget to install the "igbinary" extension?'); + } elseif (null !== $value = igbinary_unserialize($value)) { + return $value; + } + + throw new \DomainException(error_get_last() ? error_get_last()['message'] : 'Failed to unserialize values.'); + } catch (\Error $e) { + throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine()); + } finally { + ini_set('unserialize_callback_func', $unserializeCallbackHandler); + } + } + + /** + * @internal + */ + public static function handleUnserializeCallback(string $class) + { + throw new \DomainException('Class not found: '.$class); + } +} diff --git a/vendor/symfony/cache/Marshaller/DeflateMarshaller.php b/vendor/symfony/cache/Marshaller/DeflateMarshaller.php new file mode 100644 index 0000000..5544806 --- /dev/null +++ b/vendor/symfony/cache/Marshaller/DeflateMarshaller.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Marshaller; + +use Symfony\Component\Cache\Exception\CacheException; + +/** + * Compresses values using gzdeflate(). + * + * @author Nicolas Grekas + */ +class DeflateMarshaller implements MarshallerInterface +{ + private $marshaller; + + public function __construct(MarshallerInterface $marshaller) + { + if (!\function_exists('gzdeflate')) { + throw new CacheException('The "zlib" PHP extension is not loaded.'); + } + + $this->marshaller = $marshaller; + } + + /** + * {@inheritdoc} + */ + public function marshall(array $values, ?array &$failed): array + { + return array_map('gzdeflate', $this->marshaller->marshall($values, $failed)); + } + + /** + * {@inheritdoc} + */ + public function unmarshall(string $value) + { + if (false !== $inflatedValue = @gzinflate($value)) { + $value = $inflatedValue; + } + + return $this->marshaller->unmarshall($value); + } +} diff --git a/vendor/symfony/cache/Marshaller/MarshallerInterface.php b/vendor/symfony/cache/Marshaller/MarshallerInterface.php new file mode 100644 index 0000000..cdd6c40 --- /dev/null +++ b/vendor/symfony/cache/Marshaller/MarshallerInterface.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Marshaller; + +/** + * Serializes/unserializes PHP values. + * + * Implementations of this interface MUST deal with errors carefully. They MUST + * also deal with forward and backward compatibility at the storage format level. + * + * @author Nicolas Grekas + */ +interface MarshallerInterface +{ + /** + * Serializes a list of values. + * + * When serialization fails for a specific value, no exception should be + * thrown. Instead, its key should be listed in $failed. + */ + public function marshall(array $values, ?array &$failed): array; + + /** + * Unserializes a single value and throws an exception if anything goes wrong. + * + * @return mixed + * + * @throws \Exception Whenever unserialization fails + */ + public function unmarshall(string $value); +} diff --git a/vendor/symfony/cache/Marshaller/SodiumMarshaller.php b/vendor/symfony/cache/Marshaller/SodiumMarshaller.php new file mode 100644 index 0000000..7895ef5 --- /dev/null +++ b/vendor/symfony/cache/Marshaller/SodiumMarshaller.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Marshaller; + +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * Encrypt/decrypt values using Libsodium. + * + * @author Ahmed TAILOULOUTE + */ +class SodiumMarshaller implements MarshallerInterface +{ + private $marshaller; + private $decryptionKeys; + + /** + * @param string[] $decryptionKeys The key at index "0" is required and is used to decrypt and encrypt values; + * more rotating keys can be provided to decrypt values; + * each key must be generated using sodium_crypto_box_keypair() + */ + public function __construct(array $decryptionKeys, ?MarshallerInterface $marshaller = null) + { + if (!self::isSupported()) { + throw new CacheException('The "sodium" PHP extension is not loaded.'); + } + + if (!isset($decryptionKeys[0])) { + throw new InvalidArgumentException('At least one decryption key must be provided at index "0".'); + } + + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + $this->decryptionKeys = $decryptionKeys; + } + + public static function isSupported(): bool + { + return \function_exists('sodium_crypto_box_seal'); + } + + /** + * {@inheritdoc} + */ + public function marshall(array $values, ?array &$failed): array + { + $encryptionKey = sodium_crypto_box_publickey($this->decryptionKeys[0]); + + $encryptedValues = []; + foreach ($this->marshaller->marshall($values, $failed) as $k => $v) { + $encryptedValues[$k] = sodium_crypto_box_seal($v, $encryptionKey); + } + + return $encryptedValues; + } + + /** + * {@inheritdoc} + */ + public function unmarshall(string $value) + { + foreach ($this->decryptionKeys as $k) { + if (false !== $decryptedValue = @sodium_crypto_box_seal_open($value, $k)) { + $value = $decryptedValue; + break; + } + } + + return $this->marshaller->unmarshall($value); + } +} diff --git a/vendor/symfony/cache/Marshaller/TagAwareMarshaller.php b/vendor/symfony/cache/Marshaller/TagAwareMarshaller.php new file mode 100644 index 0000000..f2f26ab --- /dev/null +++ b/vendor/symfony/cache/Marshaller/TagAwareMarshaller.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Marshaller; + +/** + * A marshaller optimized for data structures generated by AbstractTagAwareAdapter. + * + * @author Nicolas Grekas + */ +class TagAwareMarshaller implements MarshallerInterface +{ + private $marshaller; + + public function __construct(?MarshallerInterface $marshaller = null) + { + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + } + + /** + * {@inheritdoc} + */ + public function marshall(array $values, ?array &$failed): array + { + $failed = $notSerialized = $serialized = []; + + foreach ($values as $id => $value) { + if (\is_array($value) && \is_array($value['tags'] ?? null) && \array_key_exists('value', $value) && \count($value) === 2 + (\is_string($value['meta'] ?? null) && 8 === \strlen($value['meta']))) { + // if the value is an array with keys "tags", "value" and "meta", use a compact serialization format + // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F allow detecting this format quickly in unmarshall() + + $v = $this->marshaller->marshall($value, $f); + + if ($f) { + $f = []; + $failed[] = $id; + } else { + if ([] === $value['tags']) { + $v['tags'] = ''; + } + + $serialized[$id] = "\x9D".($value['meta'] ?? "\0\0\0\0\0\0\0\0").pack('N', \strlen($v['tags'])).$v['tags'].$v['value']; + $serialized[$id][9] = "\x5F"; + } + } else { + // other arbitrary values are serialized using the decorated marshaller below + $notSerialized[$id] = $value; + } + } + + if ($notSerialized) { + $serialized += $this->marshaller->marshall($notSerialized, $f); + $failed = array_merge($failed, $f); + } + + return $serialized; + } + + /** + * {@inheritdoc} + */ + public function unmarshall(string $value) + { + // detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F + if (13 >= \strlen($value) || "\x9D" !== $value[0] || "\0" !== $value[5] || "\x5F" !== $value[9]) { + return $this->marshaller->unmarshall($value); + } + + // data consists of value, tags and metadata which we need to unpack + $meta = substr($value, 1, 12); + $meta[8] = "\0"; + $tagLen = unpack('Nlen', $meta, 8)['len']; + $meta = substr($meta, 0, 8); + + return [ + 'value' => $this->marshaller->unmarshall(substr($value, 13 + $tagLen)), + 'tags' => $tagLen ? $this->marshaller->unmarshall(substr($value, 13, $tagLen)) : [], + 'meta' => "\0\0\0\0\0\0\0\0" === $meta ? null : $meta, + ]; + } +} diff --git a/vendor/symfony/cache/Messenger/EarlyExpirationDispatcher.php b/vendor/symfony/cache/Messenger/EarlyExpirationDispatcher.php new file mode 100644 index 0000000..e09e282 --- /dev/null +++ b/vendor/symfony/cache/Messenger/EarlyExpirationDispatcher.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Messenger; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\DependencyInjection\ReverseContainer; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Stamp\HandledStamp; + +/** + * Sends the computation of cached values to a message bus. + */ +class EarlyExpirationDispatcher +{ + private $bus; + private $reverseContainer; + private $callbackWrapper; + + public function __construct(MessageBusInterface $bus, ReverseContainer $reverseContainer, ?callable $callbackWrapper = null) + { + $this->bus = $bus; + $this->reverseContainer = $reverseContainer; + $this->callbackWrapper = $callbackWrapper; + } + + public function __invoke(callable $callback, CacheItem $item, bool &$save, AdapterInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger = null) + { + if (!$item->isHit() || null === $message = EarlyExpirationMessage::create($this->reverseContainer, $callback, $item, $pool)) { + // The item is stale or the callback cannot be reversed: we must compute the value now + $logger && $logger->info('Computing item "{key}" online: '.($item->isHit() ? 'callback cannot be reversed' : 'item is stale'), ['key' => $item->getKey()]); + + return null !== $this->callbackWrapper ? ($this->callbackWrapper)($callback, $item, $save, $pool, $setMetadata, $logger) : $callback($item, $save); + } + + $envelope = $this->bus->dispatch($message); + + if ($logger) { + if ($envelope->last(HandledStamp::class)) { + $logger->info('Item "{key}" was computed online', ['key' => $item->getKey()]); + } else { + $logger->info('Item "{key}" sent for recomputation', ['key' => $item->getKey()]); + } + } + + // The item's value is not stale, no need to write it to the backend + $save = false; + + return $message->getItem()->get() ?? $item->get(); + } +} diff --git a/vendor/symfony/cache/Messenger/EarlyExpirationHandler.php b/vendor/symfony/cache/Messenger/EarlyExpirationHandler.php new file mode 100644 index 0000000..9e53f5d --- /dev/null +++ b/vendor/symfony/cache/Messenger/EarlyExpirationHandler.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Messenger; + +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\DependencyInjection\ReverseContainer; +use Symfony\Component\Messenger\Handler\MessageHandlerInterface; + +/** + * Computes cached values sent to a message bus. + */ +class EarlyExpirationHandler implements MessageHandlerInterface +{ + private $reverseContainer; + private $processedNonces = []; + + public function __construct(ReverseContainer $reverseContainer) + { + $this->reverseContainer = $reverseContainer; + } + + public function __invoke(EarlyExpirationMessage $message) + { + $item = $message->getItem(); + $metadata = $item->getMetadata(); + $expiry = $metadata[CacheItem::METADATA_EXPIRY] ?? 0; + $ctime = $metadata[CacheItem::METADATA_CTIME] ?? 0; + + if ($expiry && $ctime) { + // skip duplicate or expired messages + + $processingNonce = [$expiry, $ctime]; + $pool = $message->getPool(); + $key = $item->getKey(); + + if (($this->processedNonces[$pool][$key] ?? null) === $processingNonce) { + return; + } + + if (microtime(true) >= $expiry) { + return; + } + + $this->processedNonces[$pool] = [$key => $processingNonce] + ($this->processedNonces[$pool] ?? []); + + if (\count($this->processedNonces[$pool]) > 100) { + array_pop($this->processedNonces[$pool]); + } + } + + static $setMetadata; + + $setMetadata ?? $setMetadata = \Closure::bind( + function (CacheItem $item, float $startTime) { + if ($item->expiry > $endTime = microtime(true)) { + $item->newMetadata[CacheItem::METADATA_EXPIRY] = $item->expiry; + $item->newMetadata[CacheItem::METADATA_CTIME] = (int) ceil(1000 * ($endTime - $startTime)); + } + }, + null, + CacheItem::class + ); + + $startTime = microtime(true); + $pool = $message->findPool($this->reverseContainer); + $callback = $message->findCallback($this->reverseContainer); + $save = true; + $value = $callback($item, $save); + $setMetadata($item, $startTime); + $pool->save($item->set($value)); + } +} diff --git a/vendor/symfony/cache/Messenger/EarlyExpirationMessage.php b/vendor/symfony/cache/Messenger/EarlyExpirationMessage.php new file mode 100644 index 0000000..e25c07e --- /dev/null +++ b/vendor/symfony/cache/Messenger/EarlyExpirationMessage.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Messenger; + +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\DependencyInjection\ReverseContainer; + +/** + * Conveys a cached value that needs to be computed. + */ +final class EarlyExpirationMessage +{ + private $item; + private $pool; + private $callback; + + public static function create(ReverseContainer $reverseContainer, callable $callback, CacheItem $item, AdapterInterface $pool): ?self + { + try { + $item = clone $item; + $item->set(null); + } catch (\Exception $e) { + return null; + } + + $pool = $reverseContainer->getId($pool); + + if (\is_object($callback)) { + if (null === $id = $reverseContainer->getId($callback)) { + return null; + } + + $callback = '@'.$id; + } elseif (!\is_array($callback)) { + $callback = (string) $callback; + } elseif (!\is_object($callback[0])) { + $callback = [(string) $callback[0], (string) $callback[1]]; + } else { + if (null === $id = $reverseContainer->getId($callback[0])) { + return null; + } + + $callback = ['@'.$id, (string) $callback[1]]; + } + + return new self($item, $pool, $callback); + } + + public function getItem(): CacheItem + { + return $this->item; + } + + public function getPool(): string + { + return $this->pool; + } + + public function getCallback() + { + return $this->callback; + } + + public function findPool(ReverseContainer $reverseContainer): AdapterInterface + { + return $reverseContainer->getService($this->pool); + } + + public function findCallback(ReverseContainer $reverseContainer): callable + { + if (\is_string($callback = $this->callback)) { + return '@' === $callback[0] ? $reverseContainer->getService(substr($callback, 1)) : $callback; + } + if ('@' === $callback[0][0]) { + $callback[0] = $reverseContainer->getService(substr($callback[0], 1)); + } + + return $callback; + } + + private function __construct(CacheItem $item, string $pool, $callback) + { + $this->item = $item; + $this->pool = $pool; + $this->callback = $callback; + } +} diff --git a/vendor/symfony/cache/PruneableInterface.php b/vendor/symfony/cache/PruneableInterface.php new file mode 100644 index 0000000..4261525 --- /dev/null +++ b/vendor/symfony/cache/PruneableInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +/** + * Interface extends psr-6 and psr-16 caches to allow for pruning (deletion) of all expired cache items. + */ +interface PruneableInterface +{ + /** + * @return bool + */ + public function prune(); +} diff --git a/vendor/symfony/cache/Psr16Cache.php b/vendor/symfony/cache/Psr16Cache.php new file mode 100644 index 0000000..28c7de6 --- /dev/null +++ b/vendor/symfony/cache/Psr16Cache.php @@ -0,0 +1,289 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +use Psr\Cache\CacheException as Psr6CacheException; +use Psr\Cache\CacheItemPoolInterface; +use Psr\SimpleCache\CacheException as SimpleCacheException; +use Psr\SimpleCache\CacheInterface; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Traits\ProxyTrait; + +if (null !== (new \ReflectionMethod(CacheInterface::class, 'get'))->getReturnType()) { + throw new \LogicException('psr/simple-cache 3.0+ is not compatible with this version of symfony/cache. Please upgrade symfony/cache to 6.0+ or downgrade psr/simple-cache to 1.x or 2.x.'); +} + +/** + * Turns a PSR-6 cache into a PSR-16 one. + * + * @author Nicolas Grekas + */ +class Psr16Cache implements CacheInterface, PruneableInterface, ResettableInterface +{ + use ProxyTrait; + + private const METADATA_EXPIRY_OFFSET = 1527506807; + + private $createCacheItem; + private $cacheItemPrototype; + + public function __construct(CacheItemPoolInterface $pool) + { + $this->pool = $pool; + + if (!$pool instanceof AdapterInterface) { + return; + } + $cacheItemPrototype = &$this->cacheItemPrototype; + $createCacheItem = \Closure::bind( + static function ($key, $value, $allowInt = false) use (&$cacheItemPrototype) { + $item = clone $cacheItemPrototype; + $item->poolHash = $item->innerItem = null; + if ($allowInt && \is_int($key)) { + $item->key = (string) $key; + } else { + \assert('' !== CacheItem::validateKey($key)); + $item->key = $key; + } + $item->value = $value; + $item->isHit = false; + + return $item; + }, + null, + CacheItem::class + ); + $this->createCacheItem = function ($key, $value, $allowInt = false) use ($createCacheItem) { + if (null === $this->cacheItemPrototype) { + $this->get($allowInt && \is_int($key) ? (string) $key : $key); + } + $this->createCacheItem = $createCacheItem; + + return $createCacheItem($key, null, $allowInt)->set($value); + }; + } + + /** + * {@inheritdoc} + * + * @return mixed + */ + public function get($key, $default = null) + { + try { + $item = $this->pool->getItem($key); + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + if (null === $this->cacheItemPrototype) { + $this->cacheItemPrototype = clone $item; + $this->cacheItemPrototype->set(null); + } + + return $item->isHit() ? $item->get() : $default; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function set($key, $value, $ttl = null) + { + try { + if (null !== $f = $this->createCacheItem) { + $item = $f($key, $value); + } else { + $item = $this->pool->getItem($key)->set($value); + } + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + if (null !== $ttl) { + $item->expiresAfter($ttl); + } + + return $this->pool->save($item); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function delete($key) + { + try { + return $this->pool->deleteItem($key); + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear() + { + return $this->pool->clear(); + } + + /** + * {@inheritdoc} + * + * @return iterable + */ + public function getMultiple($keys, $default = null) + { + if ($keys instanceof \Traversable) { + $keys = iterator_to_array($keys, false); + } elseif (!\is_array($keys)) { + throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given.', get_debug_type($keys))); + } + + try { + $items = $this->pool->getItems($keys); + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + $values = []; + + if (!$this->pool instanceof AdapterInterface) { + foreach ($items as $key => $item) { + $values[$key] = $item->isHit() ? $item->get() : $default; + } + + return $values; + } + + foreach ($items as $key => $item) { + if (!$item->isHit()) { + $values[$key] = $default; + continue; + } + $values[$key] = $item->get(); + + if (!$metadata = $item->getMetadata()) { + continue; + } + unset($metadata[CacheItem::METADATA_TAGS]); + + if ($metadata) { + $values[$key] = ["\x9D".pack('VN', (int) (0.1 + $metadata[CacheItem::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[CacheItem::METADATA_CTIME])."\x5F" => $values[$key]]; + } + } + + return $values; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function setMultiple($values, $ttl = null) + { + $valuesIsArray = \is_array($values); + if (!$valuesIsArray && !$values instanceof \Traversable) { + throw new InvalidArgumentException(sprintf('Cache values must be array or Traversable, "%s" given.', get_debug_type($values))); + } + $items = []; + + try { + if (null !== $f = $this->createCacheItem) { + $valuesIsArray = false; + foreach ($values as $key => $value) { + $items[$key] = $f($key, $value, true); + } + } elseif ($valuesIsArray) { + $items = []; + foreach ($values as $key => $value) { + $items[] = (string) $key; + } + $items = $this->pool->getItems($items); + } else { + foreach ($values as $key => $value) { + if (\is_int($key)) { + $key = (string) $key; + } + $items[$key] = $this->pool->getItem($key)->set($value); + } + } + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + $ok = true; + + foreach ($items as $key => $item) { + if ($valuesIsArray) { + $item->set($values[$key]); + } + if (null !== $ttl) { + $item->expiresAfter($ttl); + } + $ok = $this->pool->saveDeferred($item) && $ok; + } + + return $this->pool->commit() && $ok; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteMultiple($keys) + { + if ($keys instanceof \Traversable) { + $keys = iterator_to_array($keys, false); + } elseif (!\is_array($keys)) { + throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given.', get_debug_type($keys))); + } + + try { + return $this->pool->deleteItems($keys); + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function has($key) + { + try { + return $this->pool->hasItem($key); + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + } +} diff --git a/vendor/symfony/cache/README.md b/vendor/symfony/cache/README.md new file mode 100644 index 0000000..c466d57 --- /dev/null +++ b/vendor/symfony/cache/README.md @@ -0,0 +1,19 @@ +Symfony PSR-6 implementation for caching +======================================== + +The Cache component provides extended +[PSR-6](https://www.php-fig.org/psr/psr-6/) implementations for adding cache to +your applications. It is designed to have a low overhead so that caching is +fastest. It ships with adapters for the most widespread caching backends. +It also provides a [PSR-16](https://www.php-fig.org/psr/psr-16/) adapter, +and implementations for [symfony/cache-contracts](https://github.com/symfony/cache-contracts)' +`CacheInterface` and `TagAwareCacheInterface`. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/cache.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/vendor/symfony/cache/ResettableInterface.php b/vendor/symfony/cache/ResettableInterface.php new file mode 100644 index 0000000..7b0a853 --- /dev/null +++ b/vendor/symfony/cache/ResettableInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +use Symfony\Contracts\Service\ResetInterface; + +/** + * Resets a pool's local state. + */ +interface ResettableInterface extends ResetInterface +{ +} diff --git a/vendor/symfony/cache/Traits/AbstractAdapterTrait.php b/vendor/symfony/cache/Traits/AbstractAdapterTrait.php new file mode 100644 index 0000000..32d78b1 --- /dev/null +++ b/vendor/symfony/cache/Traits/AbstractAdapterTrait.php @@ -0,0 +1,427 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Psr\Cache\CacheItemInterface; +use Psr\Log\LoggerAwareTrait; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * @author Nicolas Grekas + * + * @internal + */ +trait AbstractAdapterTrait +{ + use LoggerAwareTrait; + + /** + * @var \Closure needs to be set by class, signature is function(string , mixed , bool ) + */ + private static $createCacheItem; + + /** + * @var \Closure needs to be set by class, signature is function(array , string , array <&expiredIds>) + */ + private static $mergeByLifetime; + + private $namespace = ''; + private $defaultLifetime; + private $namespaceVersion = ''; + private $versioningIsEnabled = false; + private $deferred = []; + private $ids = []; + + /** + * @var int|null The maximum length to enforce for identifiers or null when no limit applies + */ + protected $maxIdLength; + + /** + * Fetches several cache items. + * + * @param array $ids The cache identifiers to fetch + * + * @return array|\Traversable + */ + abstract protected function doFetch(array $ids); + + /** + * Confirms if the cache contains specified cache item. + * + * @param string $id The identifier for which to check existence + * + * @return bool + */ + abstract protected function doHave(string $id); + + /** + * Deletes all items in the pool. + * + * @param string $namespace The prefix used for all identifiers managed by this pool + * + * @return bool + */ + abstract protected function doClear(string $namespace); + + /** + * Removes multiple items from the pool. + * + * @param array $ids An array of identifiers that should be removed from the pool + * + * @return bool + */ + abstract protected function doDelete(array $ids); + + /** + * Persists several cache items immediately. + * + * @param array $values The values to cache, indexed by their cache identifier + * @param int $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning + * + * @return array|bool The identifiers that failed to be cached or a boolean stating if caching succeeded or not + */ + abstract protected function doSave(array $values, int $lifetime); + + /** + * {@inheritdoc} + * + * @return bool + */ + public function hasItem($key) + { + $id = $this->getId($key); + + if (isset($this->deferred[$key])) { + $this->commit(); + } + + try { + return $this->doHave($id); + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to check if key "{key}" is cached: '.$e->getMessage(), ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]); + + return false; + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = '') + { + $this->deferred = []; + if ($cleared = $this->versioningIsEnabled) { + if ('' === $namespaceVersionToClear = $this->namespaceVersion) { + foreach ($this->doFetch([static::NS_SEPARATOR.$this->namespace]) as $v) { + $namespaceVersionToClear = $v; + } + } + $namespaceToClear = $this->namespace.$namespaceVersionToClear; + $namespaceVersion = self::formatNamespaceVersion(mt_rand()); + try { + $e = $this->doSave([static::NS_SEPARATOR.$this->namespace => $namespaceVersion], 0); + } catch (\Exception $e) { + } + if (true !== $e && [] !== $e) { + $cleared = false; + $message = 'Failed to save the new namespace'.($e instanceof \Exception ? ': '.$e->getMessage() : '.'); + CacheItem::log($this->logger, $message, ['exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]); + } else { + $this->namespaceVersion = $namespaceVersion; + $this->ids = []; + } + } else { + $namespaceToClear = $this->namespace.$prefix; + } + + try { + return $this->doClear($namespaceToClear) || $cleared; + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to clear the cache: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]); + + return false; + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItem($key) + { + return $this->deleteItems([$key]); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItems(array $keys) + { + $ids = []; + + foreach ($keys as $key) { + $ids[$key] = $this->getId($key); + unset($this->deferred[$key]); + } + + try { + if ($this->doDelete($ids)) { + return true; + } + } catch (\Exception $e) { + } + + $ok = true; + + // When bulk-delete failed, retry each item individually + foreach ($ids as $key => $id) { + try { + $e = null; + if ($this->doDelete([$id])) { + continue; + } + } catch (\Exception $e) { + } + $message = 'Failed to delete key "{key}"'.($e instanceof \Exception ? ': '.$e->getMessage() : '.'); + CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]); + $ok = false; + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + $id = $this->getId($key); + + if (isset($this->deferred[$key])) { + $this->commit(); + } + + $isHit = false; + $value = null; + + try { + foreach ($this->doFetch([$id]) as $value) { + $isHit = true; + } + + return (self::$createCacheItem)($key, $value, $isHit); + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to fetch key "{key}": '.$e->getMessage(), ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]); + } + + return (self::$createCacheItem)($key, null, false); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = []) + { + $ids = []; + $commit = false; + + foreach ($keys as $key) { + $ids[] = $this->getId($key); + $commit = $commit || isset($this->deferred[$key]); + } + + if ($commit) { + $this->commit(); + } + + try { + $items = $this->doFetch($ids); + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to fetch items: '.$e->getMessage(), ['keys' => $keys, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]); + $items = []; + } + $ids = array_combine($ids, $keys); + + return $this->generateItems($items, $ids); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function save(CacheItemInterface $item) + { + if (!$item instanceof CacheItem) { + return false; + } + $this->deferred[$item->getKey()] = $item; + + return $this->commit(); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function saveDeferred(CacheItemInterface $item) + { + if (!$item instanceof CacheItem) { + return false; + } + $this->deferred[$item->getKey()] = $item; + + return true; + } + + /** + * Enables/disables versioning of items. + * + * When versioning is enabled, clearing the cache is atomic and doesn't require listing existing keys to proceed, + * but old keys may need garbage collection and extra round-trips to the back-end are required. + * + * Calling this method also clears the memoized namespace version and thus forces a resynchronization of it. + * + * @return bool the previous state of versioning + */ + public function enableVersioning(bool $enable = true) + { + $wasEnabled = $this->versioningIsEnabled; + $this->versioningIsEnabled = $enable; + $this->namespaceVersion = ''; + $this->ids = []; + + return $wasEnabled; + } + + /** + * {@inheritdoc} + */ + public function reset() + { + if ($this->deferred) { + $this->commit(); + } + $this->namespaceVersion = ''; + $this->ids = []; + } + + /** + * @return array + */ + public function __sleep() + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + if ($this->deferred) { + $this->commit(); + } + } + + private function generateItems(iterable $items, array &$keys): \Generator + { + $f = self::$createCacheItem; + + try { + foreach ($items as $id => $value) { + if (!isset($keys[$id])) { + throw new InvalidArgumentException(sprintf('Could not match value id "%s" to keys "%s".', $id, implode('", "', $keys))); + } + $key = $keys[$id]; + unset($keys[$id]); + yield $key => $f($key, $value, true); + } + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to fetch items: '.$e->getMessage(), ['keys' => array_values($keys), 'exception' => $e, 'cache-adapter' => get_debug_type($this)]); + } + + foreach ($keys as $key) { + yield $key => $f($key, null, false); + } + } + + /** + * @internal + */ + protected function getId($key) + { + if ($this->versioningIsEnabled && '' === $this->namespaceVersion) { + $this->ids = []; + $this->namespaceVersion = '1'.static::NS_SEPARATOR; + try { + foreach ($this->doFetch([static::NS_SEPARATOR.$this->namespace]) as $v) { + $this->namespaceVersion = $v; + } + $e = true; + if ('1'.static::NS_SEPARATOR === $this->namespaceVersion) { + $this->namespaceVersion = self::formatNamespaceVersion(time()); + $e = $this->doSave([static::NS_SEPARATOR.$this->namespace => $this->namespaceVersion], 0); + } + } catch (\Exception $e) { + } + if (true !== $e && [] !== $e) { + $message = 'Failed to save the new namespace'.($e instanceof \Exception ? ': '.$e->getMessage() : '.'); + CacheItem::log($this->logger, $message, ['exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]); + } + } + + if (\is_string($key) && isset($this->ids[$key])) { + return $this->namespace.$this->namespaceVersion.$this->ids[$key]; + } + \assert('' !== CacheItem::validateKey($key)); + $this->ids[$key] = $key; + + if (\count($this->ids) > 1000) { + $this->ids = \array_slice($this->ids, 500, null, true); // stop memory leak if there are many keys + } + + if (null === $this->maxIdLength) { + return $this->namespace.$this->namespaceVersion.$key; + } + if (\strlen($id = $this->namespace.$this->namespaceVersion.$key) > $this->maxIdLength) { + // Use MD5 to favor speed over security, which is not an issue here + $this->ids[$key] = $id = substr_replace(base64_encode(hash('md5', $key, true)), static::NS_SEPARATOR, -(\strlen($this->namespaceVersion) + 2)); + $id = $this->namespace.$this->namespaceVersion.$id; + } + + return $id; + } + + /** + * @internal + */ + public static function handleUnserializeCallback(string $class) + { + throw new \DomainException('Class not found: '.$class); + } + + private static function formatNamespaceVersion(int $value): string + { + return strtr(substr_replace(base64_encode(pack('V', $value)), static::NS_SEPARATOR, 5), '/', '_'); + } +} diff --git a/vendor/symfony/cache/Traits/ContractsTrait.php b/vendor/symfony/cache/Traits/ContractsTrait.php new file mode 100644 index 0000000..c22e75f --- /dev/null +++ b/vendor/symfony/cache/Traits/ContractsTrait.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\LockRegistry; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\CacheTrait; +use Symfony\Contracts\Cache\ItemInterface; + +/** + * @author Nicolas Grekas + * + * @internal + */ +trait ContractsTrait +{ + use CacheTrait { + doGet as private contractsGet; + } + + private $callbackWrapper; + private $computing = []; + + /** + * Wraps the callback passed to ->get() in a callable. + * + * @return callable the previous callback wrapper + */ + public function setCallbackWrapper(?callable $callbackWrapper): callable + { + if (!isset($this->callbackWrapper)) { + $this->callbackWrapper = \Closure::fromCallable([LockRegistry::class, 'compute']); + + if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { + $this->setCallbackWrapper(null); + } + } + + $previousWrapper = $this->callbackWrapper; + $this->callbackWrapper = $callbackWrapper ?? static function (callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger) { + return $callback($item, $save); + }; + + return $previousWrapper; + } + + private function doGet(AdapterInterface $pool, string $key, callable $callback, ?float $beta, ?array &$metadata = null) + { + if (0 > $beta = $beta ?? 1.0) { + throw new InvalidArgumentException(sprintf('Argument "$beta" provided to "%s::get()" must be a positive number, %f given.', static::class, $beta)); + } + + static $setMetadata; + + $setMetadata ?? $setMetadata = \Closure::bind( + static function (CacheItem $item, float $startTime, ?array &$metadata) { + if ($item->expiry > $endTime = microtime(true)) { + $item->newMetadata[CacheItem::METADATA_EXPIRY] = $metadata[CacheItem::METADATA_EXPIRY] = $item->expiry; + $item->newMetadata[CacheItem::METADATA_CTIME] = $metadata[CacheItem::METADATA_CTIME] = (int) ceil(1000 * ($endTime - $startTime)); + } else { + unset($metadata[CacheItem::METADATA_EXPIRY], $metadata[CacheItem::METADATA_CTIME]); + } + }, + null, + CacheItem::class + ); + + return $this->contractsGet($pool, $key, function (CacheItem $item, bool &$save) use ($pool, $callback, $setMetadata, &$metadata, $key) { + // don't wrap nor save recursive calls + if (isset($this->computing[$key])) { + $value = $callback($item, $save); + $save = false; + + return $value; + } + + $this->computing[$key] = $key; + $startTime = microtime(true); + + if (!isset($this->callbackWrapper)) { + $this->setCallbackWrapper($this->setCallbackWrapper(null)); + } + + try { + $value = ($this->callbackWrapper)($callback, $item, $save, $pool, function (CacheItem $item) use ($setMetadata, $startTime, &$metadata) { + $setMetadata($item, $startTime, $metadata); + }, $this->logger ?? null); + $setMetadata($item, $startTime, $metadata); + + return $value; + } finally { + unset($this->computing[$key]); + } + }, $beta, $metadata, $this->logger ?? null); + } +} diff --git a/vendor/symfony/cache/Traits/FilesystemCommonTrait.php b/vendor/symfony/cache/Traits/FilesystemCommonTrait.php new file mode 100644 index 0000000..ab7e7dd --- /dev/null +++ b/vendor/symfony/cache/Traits/FilesystemCommonTrait.php @@ -0,0 +1,210 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * @author Nicolas Grekas + * + * @internal + */ +trait FilesystemCommonTrait +{ + private $directory; + private $tmp; + + private function init(string $namespace, ?string $directory) + { + if (!isset($directory[0])) { + $directory = sys_get_temp_dir().\DIRECTORY_SEPARATOR.'symfony-cache'; + } else { + $directory = realpath($directory) ?: $directory; + } + if (isset($namespace[0])) { + if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) { + throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0])); + } + $directory .= \DIRECTORY_SEPARATOR.$namespace; + } else { + $directory .= \DIRECTORY_SEPARATOR.'@'; + } + if (!is_dir($directory)) { + @mkdir($directory, 0777, true); + } + $directory .= \DIRECTORY_SEPARATOR; + // On Windows the whole path is limited to 258 chars + if ('\\' === \DIRECTORY_SEPARATOR && \strlen($directory) > 234) { + throw new InvalidArgumentException(sprintf('Cache directory too long (%s).', $directory)); + } + + $this->directory = $directory; + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace) + { + $ok = true; + + foreach ($this->scanHashDir($this->directory) as $file) { + if ('' !== $namespace && !str_starts_with($this->getFileKey($file), $namespace)) { + continue; + } + + $ok = ($this->doUnlink($file) || !file_exists($file)) && $ok; + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + $ok = true; + + foreach ($ids as $id) { + $file = $this->getFile($id); + $ok = (!is_file($file) || $this->doUnlink($file) || !file_exists($file)) && $ok; + } + + return $ok; + } + + protected function doUnlink(string $file) + { + return @unlink($file); + } + + private function write(string $file, string $data, ?int $expiresAt = null) + { + $unlink = false; + set_error_handler(__CLASS__.'::throwError'); + try { + if (null === $this->tmp) { + $this->tmp = $this->directory.bin2hex(random_bytes(6)); + } + try { + $h = fopen($this->tmp, 'x'); + } catch (\ErrorException $e) { + if (!str_contains($e->getMessage(), 'File exists')) { + throw $e; + } + + $this->tmp = $this->directory.bin2hex(random_bytes(6)); + $h = fopen($this->tmp, 'x'); + } + fwrite($h, $data); + fclose($h); + $unlink = true; + + if (null !== $expiresAt) { + touch($this->tmp, $expiresAt ?: time() + 31556952); // 1 year in seconds + } + + if ('\\' === \DIRECTORY_SEPARATOR) { + $success = copy($this->tmp, $file); + $unlink = true; + } else { + $success = rename($this->tmp, $file); + $unlink = !$success; + } + + return $success; + } finally { + restore_error_handler(); + + if ($unlink) { + @unlink($this->tmp); + } + } + } + + private function getFile(string $id, bool $mkdir = false, ?string $directory = null) + { + // Use MD5 to favor speed over security, which is not an issue here + $hash = str_replace('/', '-', base64_encode(hash('md5', static::class.$id, true))); + $dir = ($directory ?? $this->directory).strtoupper($hash[0].\DIRECTORY_SEPARATOR.$hash[1].\DIRECTORY_SEPARATOR); + + if ($mkdir && !is_dir($dir)) { + @mkdir($dir, 0777, true); + } + + return $dir.substr($hash, 2, 20); + } + + private function getFileKey(string $file): string + { + return ''; + } + + private function scanHashDir(string $directory): \Generator + { + if (!is_dir($directory)) { + return; + } + + $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + + for ($i = 0; $i < 38; ++$i) { + if (!is_dir($directory.$chars[$i])) { + continue; + } + + for ($j = 0; $j < 38; ++$j) { + if (!is_dir($dir = $directory.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j])) { + continue; + } + + foreach (@scandir($dir, \SCANDIR_SORT_NONE) ?: [] as $file) { + if ('.' !== $file && '..' !== $file) { + yield $dir.\DIRECTORY_SEPARATOR.$file; + } + } + } + } + } + + /** + * @internal + */ + public static function throwError(int $type, string $message, string $file, int $line) + { + throw new \ErrorException($message, 0, $type, $file, $line); + } + + /** + * @return array + */ + public function __sleep() + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + if (method_exists(parent::class, '__destruct')) { + parent::__destruct(); + } + if (null !== $this->tmp && is_file($this->tmp)) { + unlink($this->tmp); + } + } +} diff --git a/vendor/symfony/cache/Traits/FilesystemTrait.php b/vendor/symfony/cache/Traits/FilesystemTrait.php new file mode 100644 index 0000000..f2873d9 --- /dev/null +++ b/vendor/symfony/cache/Traits/FilesystemTrait.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Symfony\Component\Cache\Exception\CacheException; + +/** + * @author Nicolas Grekas + * @author Rob Frawley 2nd + * + * @internal + */ +trait FilesystemTrait +{ + use FilesystemCommonTrait; + + private $marshaller; + + /** + * @return bool + */ + public function prune() + { + $time = time(); + $pruned = true; + + foreach ($this->scanHashDir($this->directory) as $file) { + if (!$h = @fopen($file, 'r')) { + continue; + } + + if (($expiresAt = (int) fgets($h)) && $time >= $expiresAt) { + fclose($h); + $pruned = (@unlink($file) || !file_exists($file)) && $pruned; + } else { + fclose($h); + } + } + + return $pruned; + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + $values = []; + $now = time(); + + foreach ($ids as $id) { + $file = $this->getFile($id); + if (!is_file($file) || !$h = @fopen($file, 'r')) { + continue; + } + if (($expiresAt = (int) fgets($h)) && $now >= $expiresAt) { + fclose($h); + @unlink($file); + } else { + $i = rawurldecode(rtrim(fgets($h))); + $value = stream_get_contents($h); + fclose($h); + if ($i === $id) { + $values[$id] = $this->marshaller->unmarshall($value); + } + } + } + + return $values; + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id) + { + $file = $this->getFile($id); + + return is_file($file) && (@filemtime($file) > time() || $this->doFetch([$id])); + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + $expiresAt = $lifetime ? (time() + $lifetime) : 0; + $values = $this->marshaller->marshall($values, $failed); + + foreach ($values as $id => $value) { + if (!$this->write($this->getFile($id, true), $expiresAt."\n".rawurlencode($id)."\n".$value, $expiresAt)) { + $failed[] = $id; + } + } + + if ($failed && !is_writable($this->directory)) { + throw new CacheException(sprintf('Cache directory is not writable (%s).', $this->directory)); + } + + return $failed; + } + + private function getFileKey(string $file): string + { + if (!$h = @fopen($file, 'r')) { + return ''; + } + + fgets($h); // expiry + $encodedKey = fgets($h); + fclose($h); + + return rawurldecode(rtrim($encodedKey)); + } +} diff --git a/vendor/symfony/cache/Traits/ProxyTrait.php b/vendor/symfony/cache/Traits/ProxyTrait.php new file mode 100644 index 0000000..c86f360 --- /dev/null +++ b/vendor/symfony/cache/Traits/ProxyTrait.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Contracts\Service\ResetInterface; + +/** + * @author Nicolas Grekas + * + * @internal + */ +trait ProxyTrait +{ + private $pool; + + /** + * {@inheritdoc} + */ + public function prune() + { + return $this->pool instanceof PruneableInterface && $this->pool->prune(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + if ($this->pool instanceof ResetInterface) { + $this->pool->reset(); + } + } +} diff --git a/vendor/symfony/cache/Traits/RedisClusterNodeProxy.php b/vendor/symfony/cache/Traits/RedisClusterNodeProxy.php new file mode 100644 index 0000000..deba74f --- /dev/null +++ b/vendor/symfony/cache/Traits/RedisClusterNodeProxy.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +/** + * This file acts as a wrapper to the \RedisCluster implementation so it can accept the same type of calls as + * individual \Redis objects. + * + * Calls are made to individual nodes via: RedisCluster->{method}($host, ...args)' + * according to https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#directed-node-commands + * + * @author Jack Thomas + * + * @internal + */ +class RedisClusterNodeProxy +{ + private $host; + private $redis; + + /** + * @param \RedisCluster|RedisClusterProxy $redis + */ + public function __construct(array $host, $redis) + { + $this->host = $host; + $this->redis = $redis; + } + + public function __call(string $method, array $args) + { + return $this->redis->{$method}($this->host, ...$args); + } + + public function scan(&$iIterator, $strPattern = null, $iCount = null) + { + return $this->redis->scan($iIterator, $this->host, $strPattern, $iCount); + } + + public function getOption($name) + { + return $this->redis->getOption($name); + } +} diff --git a/vendor/symfony/cache/Traits/RedisClusterProxy.php b/vendor/symfony/cache/Traits/RedisClusterProxy.php new file mode 100644 index 0000000..73c6a4f --- /dev/null +++ b/vendor/symfony/cache/Traits/RedisClusterProxy.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +/** + * @author Alessandro Chitolina + * + * @internal + */ +class RedisClusterProxy +{ + private $redis; + private $initializer; + + public function __construct(\Closure $initializer) + { + $this->initializer = $initializer; + } + + public function __call(string $method, array $args) + { + $this->redis ?: $this->redis = $this->initializer->__invoke(); + + return $this->redis->{$method}(...$args); + } + + public function hscan($strKey, &$iIterator, $strPattern = null, $iCount = null) + { + $this->redis ?: $this->redis = $this->initializer->__invoke(); + + return $this->redis->hscan($strKey, $iIterator, $strPattern, $iCount); + } + + public function scan(&$iIterator, $strPattern = null, $iCount = null) + { + $this->redis ?: $this->redis = $this->initializer->__invoke(); + + return $this->redis->scan($iIterator, $strPattern, $iCount); + } + + public function sscan($strKey, &$iIterator, $strPattern = null, $iCount = null) + { + $this->redis ?: $this->redis = $this->initializer->__invoke(); + + return $this->redis->sscan($strKey, $iIterator, $strPattern, $iCount); + } + + public function zscan($strKey, &$iIterator, $strPattern = null, $iCount = null) + { + $this->redis ?: $this->redis = $this->initializer->__invoke(); + + return $this->redis->zscan($strKey, $iIterator, $strPattern, $iCount); + } +} diff --git a/vendor/symfony/cache/Traits/RedisProxy.php b/vendor/symfony/cache/Traits/RedisProxy.php new file mode 100644 index 0000000..ec5cfab --- /dev/null +++ b/vendor/symfony/cache/Traits/RedisProxy.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class RedisProxy +{ + private $redis; + private $initializer; + private $ready = false; + + public function __construct(\Redis $redis, \Closure $initializer) + { + $this->redis = $redis; + $this->initializer = $initializer; + } + + public function __call(string $method, array $args) + { + $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis); + + return $this->redis->{$method}(...$args); + } + + public function hscan($strKey, &$iIterator, $strPattern = null, $iCount = null) + { + $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis); + + return $this->redis->hscan($strKey, $iIterator, $strPattern, $iCount); + } + + public function scan(&$iIterator, $strPattern = null, $iCount = null) + { + $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis); + + return $this->redis->scan($iIterator, $strPattern, $iCount); + } + + public function sscan($strKey, &$iIterator, $strPattern = null, $iCount = null) + { + $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis); + + return $this->redis->sscan($strKey, $iIterator, $strPattern, $iCount); + } + + public function zscan($strKey, &$iIterator, $strPattern = null, $iCount = null) + { + $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis); + + return $this->redis->zscan($strKey, $iIterator, $strPattern, $iCount); + } +} diff --git a/vendor/symfony/cache/Traits/RedisTrait.php b/vendor/symfony/cache/Traits/RedisTrait.php new file mode 100644 index 0000000..129254b --- /dev/null +++ b/vendor/symfony/cache/Traits/RedisTrait.php @@ -0,0 +1,660 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Predis\Command\Redis\UNLINK; +use Predis\Connection\Aggregate\ClusterInterface; +use Predis\Connection\Aggregate\RedisCluster; +use Predis\Connection\Aggregate\ReplicationInterface; +use Predis\Connection\Cluster\ClusterInterface as Predis2ClusterInterface; +use Predis\Connection\Cluster\RedisCluster as Predis2RedisCluster; +use Predis\Response\ErrorInterface; +use Predis\Response\Status; +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Aurimas Niekis + * @author Nicolas Grekas + * + * @internal + */ +trait RedisTrait +{ + private static $defaultConnectionOptions = [ + 'class' => null, + 'persistent' => 0, + 'persistent_id' => null, + 'timeout' => 30, + 'read_timeout' => 0, + 'retry_interval' => 0, + 'tcp_keepalive' => 0, + 'lazy' => null, + 'redis_cluster' => false, + 'redis_sentinel' => null, + 'dbindex' => 0, + 'failover' => 'none', + 'ssl' => null, // see https://php.net/context.ssl + ]; + private $redis; + private $marshaller; + + /** + * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis + */ + private function init($redis, string $namespace, int $defaultLifetime, ?MarshallerInterface $marshaller) + { + parent::__construct($namespace, $defaultLifetime); + + if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) { + throw new InvalidArgumentException(sprintf('RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0])); + } + + if (!$redis instanceof \Redis && !$redis instanceof \RedisArray && !$redis instanceof \RedisCluster && !$redis instanceof \Predis\ClientInterface && !$redis instanceof RedisProxy && !$redis instanceof RedisClusterProxy) { + throw new InvalidArgumentException(sprintf('"%s()" expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\ClientInterface, "%s" given.', __METHOD__, get_debug_type($redis))); + } + + if ($redis instanceof \Predis\ClientInterface && $redis->getOptions()->exceptions) { + $options = clone $redis->getOptions(); + \Closure::bind(function () { $this->options['exceptions'] = false; }, $options, $options)(); + $redis = new $redis($redis->getConnection(), $options); + } + + $this->redis = $redis; + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + } + + /** + * Creates a Redis connection using a DSN configuration. + * + * Example DSN: + * - redis://localhost + * - redis://example.com:1234 + * - redis://secret@example.com/13 + * - redis:///var/run/redis.sock + * - redis://secret@/var/run/redis.sock/13 + * + * @param array $options See self::$defaultConnectionOptions + * + * @return \Redis|\RedisArray|\RedisCluster|RedisClusterProxy|RedisProxy|\Predis\ClientInterface According to the "class" option + * + * @throws InvalidArgumentException when the DSN is invalid + */ + public static function createConnection(string $dsn, array $options = []) + { + if (str_starts_with($dsn, 'redis:')) { + $scheme = 'redis'; + } elseif (str_starts_with($dsn, 'rediss:')) { + $scheme = 'rediss'; + } else { + throw new InvalidArgumentException('Invalid Redis DSN: it does not start with "redis[s]:".'); + } + + if (!\extension_loaded('redis') && !class_exists(\Predis\Client::class)) { + throw new CacheException('Cannot find the "redis" extension nor the "predis/predis" package.'); + } + + $params = preg_replace_callback('#^'.$scheme.':(//)?(?:(?:[^:@]*+:)?([^@]*+)@)?#', function ($m) use (&$auth) { + if (isset($m[2])) { + $auth = rawurldecode($m[2]); + + if ('' === $auth) { + $auth = null; + } + } + + return 'file:'.($m[1] ?? ''); + }, $dsn); + + if (false === $params = parse_url($params)) { + throw new InvalidArgumentException('Invalid Redis DSN.'); + } + + $query = $hosts = []; + + $tls = 'rediss' === $scheme; + $tcpScheme = $tls ? 'tls' : 'tcp'; + + if (isset($params['query'])) { + parse_str($params['query'], $query); + + if (isset($query['host'])) { + if (!\is_array($hosts = $query['host'])) { + throw new InvalidArgumentException('Invalid Redis DSN: query parameter "host" must be an array.'); + } + foreach ($hosts as $host => $parameters) { + if (\is_string($parameters)) { + parse_str($parameters, $parameters); + } + if (false === $i = strrpos($host, ':')) { + $hosts[$host] = ['scheme' => $tcpScheme, 'host' => $host, 'port' => 6379] + $parameters; + } elseif ($port = (int) substr($host, 1 + $i)) { + $hosts[$host] = ['scheme' => $tcpScheme, 'host' => substr($host, 0, $i), 'port' => $port] + $parameters; + } else { + $hosts[$host] = ['scheme' => 'unix', 'path' => substr($host, 0, $i)] + $parameters; + } + } + $hosts = array_values($hosts); + } + } + + if (isset($params['host']) || isset($params['path'])) { + if (!isset($params['dbindex']) && isset($params['path'])) { + if (preg_match('#/(\d+)?$#', $params['path'], $m)) { + $params['dbindex'] = $m[1] ?? $query['dbindex'] ?? '0'; + $params['path'] = substr($params['path'], 0, -\strlen($m[0])); + } elseif (isset($params['host'])) { + throw new InvalidArgumentException('Invalid Redis DSN: parameter "dbindex" must be a number.'); + } + } + + if (isset($params['host'])) { + array_unshift($hosts, ['scheme' => $tcpScheme, 'host' => $params['host'], 'port' => $params['port'] ?? 6379]); + } else { + array_unshift($hosts, ['scheme' => 'unix', 'path' => $params['path']]); + } + } + + if (!$hosts) { + throw new InvalidArgumentException('Invalid Redis DSN: missing host.'); + } + + if (isset($params['dbindex'], $query['dbindex']) && $params['dbindex'] !== $query['dbindex']) { + throw new InvalidArgumentException('Invalid Redis DSN: path and query "dbindex" parameters mismatch.'); + } + + $params += $query + $options + self::$defaultConnectionOptions; + + if (isset($params['redis_sentinel']) && !class_exists(\Predis\Client::class) && !class_exists(\RedisSentinel::class)) { + throw new CacheException('Redis Sentinel support requires the "predis/predis" package or the "redis" extension v5.2 or higher.'); + } + + if (isset($params['lazy'])) { + $params['lazy'] = filter_var($params['lazy'], \FILTER_VALIDATE_BOOLEAN); + } + $params['redis_cluster'] = filter_var($params['redis_cluster'], \FILTER_VALIDATE_BOOLEAN); + + if ($params['redis_cluster'] && isset($params['redis_sentinel'])) { + throw new InvalidArgumentException('Cannot use both "redis_cluster" and "redis_sentinel" at the same time.'); + } + + if (null === $params['class'] && \extension_loaded('redis')) { + $class = $params['redis_cluster'] ? \RedisCluster::class : (1 < \count($hosts) && !isset($params['redis_sentinel']) ? \RedisArray::class : \Redis::class); + } else { + $class = $params['class'] ?? \Predis\Client::class; + + if (isset($params['redis_sentinel']) && !is_a($class, \Predis\Client::class, true) && !class_exists(\RedisSentinel::class)) { + throw new CacheException(sprintf('Cannot use Redis Sentinel: class "%s" does not extend "Predis\Client" and ext-redis >= 5.2 not found.', $class)); + } + } + + if (is_a($class, \Redis::class, true)) { + $connect = $params['persistent'] || $params['persistent_id'] ? 'pconnect' : 'connect'; + $redis = new $class(); + + $initializer = static function ($redis) use ($connect, $params, $auth, $hosts, $tls) { + $hostIndex = 0; + do { + $host = $hosts[$hostIndex]['host'] ?? $hosts[$hostIndex]['path']; + $port = $hosts[$hostIndex]['port'] ?? 0; + $passAuth = \defined('Redis::OPT_NULL_MULTIBULK_AS_NULL') && isset($params['auth']); + $address = false; + + if (isset($hosts[$hostIndex]['host']) && $tls) { + $host = 'tls://'.$host; + } + + if (!isset($params['redis_sentinel'])) { + break; + } + + if (version_compare(phpversion('redis'), '6.0.0', '>=')) { + $options = [ + 'host' => $host, + 'port' => $port, + 'connectTimeout' => $params['timeout'], + 'persistent' => $params['persistent_id'], + 'retryInterval' => $params['retry_interval'], + 'readTimeout' => $params['read_timeout'], + ]; + + if ($passAuth) { + $options['auth'] = $params['auth']; + } + + $sentinel = new \RedisSentinel($options); + } else { + $extra = $passAuth ? [$params['auth']] : []; + + $sentinel = new \RedisSentinel($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...$extra); + } + + try { + if ($address = $sentinel->getMasterAddrByName($params['redis_sentinel'])) { + [$host, $port] = $address; + } + } catch (\RedisException $e) { + } + } while (++$hostIndex < \count($hosts) && !$address); + + if (isset($params['redis_sentinel']) && !$address) { + throw new InvalidArgumentException(sprintf('Failed to retrieve master information from sentinel "%s".', $params['redis_sentinel'])); + } + + try { + $extra = [ + 'stream' => $params['ssl'] ?? null, + ]; + $booleanStreamOptions = [ + 'allow_self_signed', + 'capture_peer_cert', + 'capture_peer_cert_chain', + 'disable_compression', + 'SNI_enabled', + 'verify_peer', + 'verify_peer_name', + ]; + + foreach ($extra['stream'] ?? [] as $streamOption => $value) { + if (\in_array($streamOption, $booleanStreamOptions, true) && \is_string($value)) { + $extra['stream'][$streamOption] = filter_var($value, \FILTER_VALIDATE_BOOL); + } + } + + if (isset($params['auth'])) { + $extra['auth'] = $params['auth']; + } + @$redis->{$connect}($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...\defined('Redis::SCAN_PREFIX') ? [$extra] : []); + + set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); + try { + $isConnected = $redis->isConnected(); + } finally { + restore_error_handler(); + } + if (!$isConnected) { + $error = preg_match('/^Redis::p?connect\(\): (.*)/', $error ?? $redis->getLastError() ?? '', $error) ? sprintf(' (%s)', $error[1]) : ''; + throw new InvalidArgumentException('Redis connection failed: '.$error.'.'); + } + + if ((null !== $auth && !$redis->auth($auth)) + // Due to a bug in phpredis we must always select the dbindex if persistent pooling is enabled + // @see https://github.com/phpredis/phpredis/issues/1920 + // @see https://github.com/symfony/symfony/issues/51578 + || (($params['dbindex'] || ('pconnect' === $connect && '0' !== \ini_get('redis.pconnect.pooling_enabled'))) && !$redis->select($params['dbindex'])) + ) { + $e = preg_replace('/^ERR /', '', $redis->getLastError()); + throw new InvalidArgumentException('Redis connection failed: '.$e.'.'); + } + + if (0 < $params['tcp_keepalive'] && \defined('Redis::OPT_TCP_KEEPALIVE')) { + $redis->setOption(\Redis::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']); + } + } catch (\RedisException $e) { + throw new InvalidArgumentException('Redis connection failed: '.$e->getMessage()); + } + + return true; + }; + + if ($params['lazy']) { + $redis = new RedisProxy($redis, $initializer); + } else { + $initializer($redis); + } + } elseif (is_a($class, \RedisArray::class, true)) { + foreach ($hosts as $i => $host) { + switch ($host['scheme']) { + case 'tcp': $hosts[$i] = $host['host'].':'.$host['port']; break; + case 'tls': $hosts[$i] = 'tls://'.$host['host'].':'.$host['port']; break; + default: $hosts[$i] = $host['path']; + } + } + $params['lazy_connect'] = $params['lazy'] ?? true; + $params['connect_timeout'] = $params['timeout']; + + try { + $redis = new $class($hosts, $params); + } catch (\RedisClusterException $e) { + throw new InvalidArgumentException('Redis connection failed: '.$e->getMessage()); + } + + if (0 < $params['tcp_keepalive'] && \defined('Redis::OPT_TCP_KEEPALIVE')) { + $redis->setOption(\Redis::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']); + } + } elseif (is_a($class, \RedisCluster::class, true)) { + $initializer = static function () use ($class, $params, $hosts) { + foreach ($hosts as $i => $host) { + switch ($host['scheme']) { + case 'tcp': $hosts[$i] = $host['host'].':'.$host['port']; break; + case 'tls': $hosts[$i] = 'tls://'.$host['host'].':'.$host['port']; break; + default: $hosts[$i] = $host['path']; + } + } + + try { + $redis = new $class(null, $hosts, $params['timeout'], $params['read_timeout'], (bool) $params['persistent'], $params['auth'] ?? '', ...\defined('Redis::SCAN_PREFIX') ? [$params['ssl'] ?? null] : []); + } catch (\RedisClusterException $e) { + throw new InvalidArgumentException('Redis connection failed: '.$e->getMessage()); + } + + if (0 < $params['tcp_keepalive'] && \defined('Redis::OPT_TCP_KEEPALIVE')) { + $redis->setOption(\Redis::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']); + } + switch ($params['failover']) { + case 'error': $redis->setOption(\RedisCluster::OPT_SLAVE_FAILOVER, \RedisCluster::FAILOVER_ERROR); break; + case 'distribute': $redis->setOption(\RedisCluster::OPT_SLAVE_FAILOVER, \RedisCluster::FAILOVER_DISTRIBUTE); break; + case 'slaves': $redis->setOption(\RedisCluster::OPT_SLAVE_FAILOVER, \RedisCluster::FAILOVER_DISTRIBUTE_SLAVES); break; + } + + return $redis; + }; + + $redis = $params['lazy'] ? new RedisClusterProxy($initializer) : $initializer(); + } elseif (is_a($class, \Predis\ClientInterface::class, true)) { + if ($params['redis_cluster']) { + $params['cluster'] = 'redis'; + } elseif (isset($params['redis_sentinel'])) { + $params['replication'] = 'sentinel'; + $params['service'] = $params['redis_sentinel']; + } + $params += ['parameters' => []]; + $params['parameters'] += [ + 'persistent' => $params['persistent'], + 'timeout' => $params['timeout'], + 'read_write_timeout' => $params['read_timeout'], + 'tcp_nodelay' => true, + ]; + if ($params['dbindex']) { + $params['parameters']['database'] = $params['dbindex']; + } + if (null !== $auth) { + $params['parameters']['password'] = $auth; + } + + if (isset($params['ssl'])) { + foreach ($hosts as $i => $host) { + if (!isset($host['ssl'])) { + $hosts[$i]['ssl'] = $params['ssl']; + } + } + } + + if (1 === \count($hosts) && !($params['redis_cluster'] || $params['redis_sentinel'])) { + $hosts = $hosts[0]; + } elseif (\in_array($params['failover'], ['slaves', 'distribute'], true) && !isset($params['replication'])) { + $params['replication'] = true; + $hosts[0] += ['alias' => 'master']; + } + $params['exceptions'] = false; + + $redis = new $class($hosts, array_diff_key($params, self::$defaultConnectionOptions)); + if (isset($params['redis_sentinel'])) { + $redis->getConnection()->setSentinelTimeout($params['timeout']); + } + } elseif (class_exists($class, false)) { + throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Redis", "RedisArray", "RedisCluster" nor "Predis\ClientInterface".', $class)); + } else { + throw new InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); + } + + return $redis; + } + + protected function doFetch(array $ids) + { + if (!$ids) { + return []; + } + + $result = []; + + if ($this->redis instanceof \Predis\ClientInterface && ($this->redis->getConnection() instanceof ClusterInterface || $this->redis->getConnection() instanceof Predis2ClusterInterface)) { + $values = $this->pipeline(function () use ($ids) { + foreach ($ids as $id) { + yield 'get' => [$id]; + } + }); + } else { + $values = $this->redis->mget($ids); + + if (!\is_array($values) || \count($values) !== \count($ids)) { + return []; + } + + $values = array_combine($ids, $values); + } + + foreach ($values as $id => $v) { + if ($v) { + $result[$id] = $this->marshaller->unmarshall($v); + } + } + + return $result; + } + + protected function doHave(string $id) + { + return (bool) $this->redis->exists($id); + } + + protected function doClear(string $namespace) + { + if ($this->redis instanceof \Predis\ClientInterface) { + $prefix = $this->redis->getOptions()->prefix ? $this->redis->getOptions()->prefix->getPrefix() : ''; + $prefixLen = \strlen($prefix ?? ''); + } + + $cleared = true; + $hosts = $this->getHosts(); + $host = reset($hosts); + if ($host instanceof \Predis\Client && $host->getConnection() instanceof ReplicationInterface) { + // Predis supports info command only on the master in replication environments + $hosts = [$host->getClientFor('master')]; + } + + foreach ($hosts as $host) { + if (!isset($namespace[0])) { + $cleared = $host->flushDb() && $cleared; + continue; + } + + $info = $host->info('Server'); + $info = !$info instanceof ErrorInterface ? $info['Server'] ?? $info : ['redis_version' => '2.0']; + + if (!$host instanceof \Predis\ClientInterface) { + $prefix = \defined('Redis::SCAN_PREFIX') && (\Redis::SCAN_PREFIX & $host->getOption(\Redis::OPT_SCAN)) ? '' : $host->getOption(\Redis::OPT_PREFIX); + $prefixLen = \strlen($host->getOption(\Redis::OPT_PREFIX) ?? ''); + } + $pattern = $prefix.$namespace.'*'; + + if (!version_compare($info['redis_version'], '2.8', '>=')) { + // As documented in Redis documentation (http://redis.io/commands/keys) using KEYS + // can hang your server when it is executed against large databases (millions of items). + // Whenever you hit this scale, you should really consider upgrading to Redis 2.8 or above. + $unlink = version_compare($info['redis_version'], '4.0', '>=') ? 'UNLINK' : 'DEL'; + $args = $this->redis instanceof \Predis\ClientInterface ? [0, $pattern] : [[$pattern], 0]; + $cleared = $host->eval("local keys=redis.call('KEYS',ARGV[1]) for i=1,#keys,5000 do redis.call('$unlink',unpack(keys,i,math.min(i+4999,#keys))) end return 1", $args[0], $args[1]) && $cleared; + continue; + } + + $cursor = null; + do { + $keys = $host instanceof \Predis\ClientInterface ? $host->scan($cursor, 'MATCH', $pattern, 'COUNT', 1000) : $host->scan($cursor, $pattern, 1000); + if (isset($keys[1]) && \is_array($keys[1])) { + $cursor = $keys[0]; + $keys = $keys[1]; + } + if ($keys) { + if ($prefixLen) { + foreach ($keys as $i => $key) { + $keys[$i] = substr($key, $prefixLen); + } + } + $this->doDelete($keys); + } + } while ($cursor); + } + + return $cleared; + } + + protected function doDelete(array $ids) + { + if (!$ids) { + return true; + } + + if ($this->redis instanceof \Predis\ClientInterface && ($this->redis->getConnection() instanceof ClusterInterface || $this->redis->getConnection() instanceof Predis2ClusterInterface)) { + static $del; + $del = $del ?? (class_exists(UNLINK::class) ? 'unlink' : 'del'); + + $this->pipeline(function () use ($ids, $del) { + foreach ($ids as $id) { + yield $del => [$id]; + } + })->rewind(); + } else { + static $unlink = true; + + if ($unlink) { + try { + $unlink = false !== $this->redis->unlink($ids); + } catch (\Throwable $e) { + $unlink = false; + } + } + + if (!$unlink) { + $this->redis->del($ids); + } + } + + return true; + } + + protected function doSave(array $values, int $lifetime) + { + if (!$values = $this->marshaller->marshall($values, $failed)) { + return $failed; + } + + $results = $this->pipeline(function () use ($values, $lifetime) { + foreach ($values as $id => $value) { + if (0 >= $lifetime) { + yield 'set' => [$id, $value]; + } else { + yield 'setEx' => [$id, $lifetime, $value]; + } + } + }); + + foreach ($results as $id => $result) { + if (true !== $result && (!$result instanceof Status || Status::get('OK') !== $result)) { + $failed[] = $id; + } + } + + return $failed; + } + + private function pipeline(\Closure $generator, ?object $redis = null): \Generator + { + $ids = []; + $redis = $redis ?? $this->redis; + + if ($redis instanceof RedisClusterProxy || $redis instanceof \RedisCluster || ($redis instanceof \Predis\ClientInterface && ($redis->getConnection() instanceof RedisCluster || $redis->getConnection() instanceof Predis2RedisCluster))) { + // phpredis & predis don't support pipelining with RedisCluster + // see https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#pipelining + // see https://github.com/nrk/predis/issues/267#issuecomment-123781423 + $results = []; + foreach ($generator() as $command => $args) { + $results[] = $redis->{$command}(...$args); + $ids[] = 'eval' === $command ? ($redis instanceof \Predis\ClientInterface ? $args[2] : $args[1][0]) : $args[0]; + } + } elseif ($redis instanceof \Predis\ClientInterface) { + $results = $redis->pipeline(static function ($redis) use ($generator, &$ids) { + foreach ($generator() as $command => $args) { + $redis->{$command}(...$args); + $ids[] = 'eval' === $command ? $args[2] : $args[0]; + } + }); + } elseif ($redis instanceof \RedisArray) { + $connections = $results = $ids = []; + foreach ($generator() as $command => $args) { + $id = 'eval' === $command ? $args[1][0] : $args[0]; + if (!isset($connections[$h = $redis->_target($id)])) { + $connections[$h] = [$redis->_instance($h), -1]; + $connections[$h][0]->multi(\Redis::PIPELINE); + } + $connections[$h][0]->{$command}(...$args); + $results[] = [$h, ++$connections[$h][1]]; + $ids[] = $id; + } + foreach ($connections as $h => $c) { + $connections[$h] = $c[0]->exec(); + } + foreach ($results as $k => [$h, $c]) { + $results[$k] = $connections[$h][$c]; + } + } else { + $redis->multi(\Redis::PIPELINE); + foreach ($generator() as $command => $args) { + $redis->{$command}(...$args); + $ids[] = 'eval' === $command ? $args[1][0] : $args[0]; + } + $results = $redis->exec(); + } + + if (!$redis instanceof \Predis\ClientInterface && 'eval' === $command && $redis->getLastError()) { + $e = new \RedisException($redis->getLastError()); + $results = array_map(function ($v) use ($e) { return false === $v ? $e : $v; }, (array) $results); + } + + if (\is_bool($results)) { + return; + } + + foreach ($ids as $k => $id) { + yield $id => $results[$k]; + } + } + + private function getHosts(): array + { + $hosts = [$this->redis]; + if ($this->redis instanceof \Predis\ClientInterface) { + $connection = $this->redis->getConnection(); + if (($connection instanceof ClusterInterface || $connection instanceof Predis2ClusterInterface) && $connection instanceof \Traversable) { + $hosts = []; + foreach ($connection as $c) { + $hosts[] = new \Predis\Client($c); + } + } + } elseif ($this->redis instanceof \RedisArray) { + $hosts = []; + foreach ($this->redis->_hosts() as $host) { + $hosts[] = $this->redis->_instance($host); + } + } elseif ($this->redis instanceof RedisClusterProxy || $this->redis instanceof \RedisCluster) { + $hosts = []; + foreach ($this->redis->_masters() as $host) { + $hosts[] = new RedisClusterNodeProxy($host, $this->redis); + } + } + + return $hosts; + } +} diff --git a/vendor/symfony/cache/composer.json b/vendor/symfony/cache/composer.json new file mode 100644 index 0000000..fdf794f --- /dev/null +++ b/vendor/symfony/cache/composer.json @@ -0,0 +1,60 @@ +{ + "name": "symfony/cache", + "type": "library", + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "keywords": ["caching", "psr6"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "provide": { + "psr/cache-implementation": "1.0|2.0", + "psr/simple-cache-implementation": "1.0|2.0", + "symfony/cache-implementation": "1.0|2.0" + }, + "require": { + "php": ">=7.2.5", + "psr/cache": "^1.0|^2.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^1.1.7|^2", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/var-exporter": "^4.4|^5.0|^6.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/cache": "^1.6|^2.0", + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/filesystem": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "conflict": { + "doctrine/dbal": "<2.13.1", + "symfony/dependency-injection": "<4.4", + "symfony/http-kernel": "<4.4", + "symfony/var-dumper": "<4.4" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Cache\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/vendor/symfony/deprecation-contracts/.gitignore b/vendor/symfony/deprecation-contracts/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/vendor/symfony/deprecation-contracts/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/vendor/symfony/deprecation-contracts/CHANGELOG.md b/vendor/symfony/deprecation-contracts/CHANGELOG.md new file mode 100644 index 0000000..7932e26 --- /dev/null +++ b/vendor/symfony/deprecation-contracts/CHANGELOG.md @@ -0,0 +1,5 @@ +CHANGELOG +========= + +The changelog is maintained for all Symfony contracts at the following URL: +https://github.com/symfony/contracts/blob/main/CHANGELOG.md diff --git a/vendor/symfony/deprecation-contracts/LICENSE b/vendor/symfony/deprecation-contracts/LICENSE new file mode 100644 index 0000000..406242f --- /dev/null +++ b/vendor/symfony/deprecation-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020-2022 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/deprecation-contracts/README.md b/vendor/symfony/deprecation-contracts/README.md new file mode 100644 index 0000000..4957933 --- /dev/null +++ b/vendor/symfony/deprecation-contracts/README.md @@ -0,0 +1,26 @@ +Symfony Deprecation Contracts +============================= + +A generic function and convention to trigger deprecation notices. + +This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices. + +By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component, +the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments. + +The function requires at least 3 arguments: + - the name of the Composer package that is triggering the deprecation + - the version of the package that introduced the deprecation + - the message of the deprecation + - more arguments can be provided: they will be inserted in the message using `printf()` formatting + +Example: +```php +trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin'); +``` + +This will generate the following message: +`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.` + +While not necessarily recommended, the deprecation notices can be completely ignored by declaring an empty +`function trigger_deprecation() {}` in your application. diff --git a/vendor/symfony/deprecation-contracts/composer.json b/vendor/symfony/deprecation-contracts/composer.json new file mode 100644 index 0000000..cc7cc12 --- /dev/null +++ b/vendor/symfony/deprecation-contracts/composer.json @@ -0,0 +1,35 @@ +{ + "name": "symfony/deprecation-contracts", + "type": "library", + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1" + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + } +} diff --git a/vendor/symfony/deprecation-contracts/function.php b/vendor/symfony/deprecation-contracts/function.php new file mode 100644 index 0000000..d437150 --- /dev/null +++ b/vendor/symfony/deprecation-contracts/function.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (!function_exists('trigger_deprecation')) { + /** + * Triggers a silenced deprecation notice. + * + * @param string $package The name of the Composer package that is triggering the deprecation + * @param string $version The version of the package that introduced the deprecation + * @param string $message The message of the deprecation + * @param mixed ...$args Values to insert in the message using printf() formatting + * + * @author Nicolas Grekas + */ + function trigger_deprecation(string $package, string $version, string $message, ...$args): void + { + @trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED); + } +} diff --git a/vendor/symfony/event-dispatcher-contracts/.gitignore b/vendor/symfony/event-dispatcher-contracts/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/vendor/symfony/event-dispatcher-contracts/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/vendor/symfony/event-dispatcher-contracts/CHANGELOG.md b/vendor/symfony/event-dispatcher-contracts/CHANGELOG.md new file mode 100644 index 0000000..7932e26 --- /dev/null +++ b/vendor/symfony/event-dispatcher-contracts/CHANGELOG.md @@ -0,0 +1,5 @@ +CHANGELOG +========= + +The changelog is maintained for all Symfony contracts at the following URL: +https://github.com/symfony/contracts/blob/main/CHANGELOG.md diff --git a/vendor/symfony/event-dispatcher-contracts/Event.php b/vendor/symfony/event-dispatcher-contracts/Event.php new file mode 100644 index 0000000..46dcb2b --- /dev/null +++ b/vendor/symfony/event-dispatcher-contracts/Event.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\EventDispatcher; + +use Psr\EventDispatcher\StoppableEventInterface; + +/** + * Event is the base class for classes containing event data. + * + * This class contains no event data. It is used by events that do not pass + * state information to an event handler when an event is raised. + * + * You can call the method stopPropagation() to abort the execution of + * further listeners in your event listener. + * + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Bernhard Schussek + * @author Nicolas Grekas + */ +class Event implements StoppableEventInterface +{ + private $propagationStopped = false; + + /** + * {@inheritdoc} + */ + public function isPropagationStopped(): bool + { + return $this->propagationStopped; + } + + /** + * Stops the propagation of the event to further event listeners. + * + * If multiple event listeners are connected to the same event, no + * further event listener will be triggered once any trigger calls + * stopPropagation(). + */ + public function stopPropagation(): void + { + $this->propagationStopped = true; + } +} diff --git a/vendor/symfony/event-dispatcher-contracts/EventDispatcherInterface.php b/vendor/symfony/event-dispatcher-contracts/EventDispatcherInterface.php new file mode 100644 index 0000000..351dc51 --- /dev/null +++ b/vendor/symfony/event-dispatcher-contracts/EventDispatcherInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\EventDispatcher; + +use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface; + +/** + * Allows providing hooks on domain-specific lifecycles by dispatching events. + */ +interface EventDispatcherInterface extends PsrEventDispatcherInterface +{ + /** + * Dispatches an event to all registered listeners. + * + * @param object $event The event to pass to the event handlers/listeners + * @param string|null $eventName The name of the event to dispatch. If not supplied, + * the class of $event should be used instead. + * + * @return object The passed $event MUST be returned + */ + public function dispatch(object $event, string $eventName = null): object; +} diff --git a/vendor/symfony/event-dispatcher-contracts/LICENSE b/vendor/symfony/event-dispatcher-contracts/LICENSE new file mode 100644 index 0000000..74cdc2d --- /dev/null +++ b/vendor/symfony/event-dispatcher-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2022 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/event-dispatcher-contracts/README.md b/vendor/symfony/event-dispatcher-contracts/README.md new file mode 100644 index 0000000..b1ab4c0 --- /dev/null +++ b/vendor/symfony/event-dispatcher-contracts/README.md @@ -0,0 +1,9 @@ +Symfony EventDispatcher Contracts +================================= + +A set of abstractions extracted out of the Symfony components. + +Can be used to build on semantics that the Symfony components proved useful - and +that already have battle tested implementations. + +See https://github.com/symfony/contracts/blob/main/README.md for more information. diff --git a/vendor/symfony/event-dispatcher-contracts/composer.json b/vendor/symfony/event-dispatcher-contracts/composer.json new file mode 100644 index 0000000..660df81 --- /dev/null +++ b/vendor/symfony/event-dispatcher-contracts/composer.json @@ -0,0 +1,38 @@ +{ + "name": "symfony/event-dispatcher-contracts", + "type": "library", + "description": "Generic abstractions related to dispatching event", + "keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "psr/event-dispatcher": "^1" + }, + "suggest": { + "symfony/event-dispatcher-implementation": "" + }, + "autoload": { + "psr-4": { "Symfony\\Contracts\\EventDispatcher\\": "" } + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + } +} diff --git a/vendor/symfony/event-dispatcher/Attribute/AsEventListener.php b/vendor/symfony/event-dispatcher/Attribute/AsEventListener.php new file mode 100644 index 0000000..bb931b8 --- /dev/null +++ b/vendor/symfony/event-dispatcher/Attribute/AsEventListener.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Attribute; + +/** + * Service tag to autoconfigure event listeners. + * + * @author Alexander M. Turek + */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class AsEventListener +{ + public function __construct( + public ?string $event = null, + public ?string $method = null, + public int $priority = 0, + public ?string $dispatcher = null, + ) { + } +} diff --git a/vendor/symfony/event-dispatcher/CHANGELOG.md b/vendor/symfony/event-dispatcher/CHANGELOG.md new file mode 100644 index 0000000..0f98598 --- /dev/null +++ b/vendor/symfony/event-dispatcher/CHANGELOG.md @@ -0,0 +1,91 @@ +CHANGELOG +========= + +5.4 +--- + + * Allow `#[AsEventListener]` attribute on methods + +5.3 +--- + + * Add `#[AsEventListener]` attribute for declaring listeners on PHP 8 + +5.1.0 +----- + + * The `LegacyEventDispatcherProxy` class has been deprecated. + * Added an optional `dispatcher` attribute to the listener and subscriber tags in `RegisterListenerPass`. + +5.0.0 +----- + + * The signature of the `EventDispatcherInterface::dispatch()` method has been changed to `dispatch($event, string $eventName = null): object`. + * The `Event` class has been removed in favor of `Symfony\Contracts\EventDispatcher\Event`. + * The `TraceableEventDispatcherInterface` has been removed. + * The `WrappedListener` class is now final. + +4.4.0 +----- + + * `AddEventAliasesPass` has been added, allowing applications and bundles to extend the event alias mapping used by `RegisterListenersPass`. + * Made the `event` attribute of the `kernel.event_listener` tag optional for FQCN events. + +4.3.0 +----- + + * The signature of the `EventDispatcherInterface::dispatch()` method should be updated to `dispatch($event, string $eventName = null)`, not doing so is deprecated + * deprecated the `Event` class, use `Symfony\Contracts\EventDispatcher\Event` instead + +4.1.0 +----- + + * added support for invokable event listeners tagged with `kernel.event_listener` by default + * The `TraceableEventDispatcher::getOrphanedEvents()` method has been added. + * The `TraceableEventDispatcherInterface` has been deprecated. + +4.0.0 +----- + + * removed the `ContainerAwareEventDispatcher` class + * added the `reset()` method to the `TraceableEventDispatcherInterface` + +3.4.0 +----- + + * Implementing `TraceableEventDispatcherInterface` without the `reset()` method has been deprecated. + +3.3.0 +----- + + * The ContainerAwareEventDispatcher class has been deprecated. Use EventDispatcher with closure factories instead. + +3.0.0 +----- + + * The method `getListenerPriority($eventName, $listener)` has been added to the + `EventDispatcherInterface`. + * The methods `Event::setDispatcher()`, `Event::getDispatcher()`, `Event::setName()` + and `Event::getName()` have been removed. + The event dispatcher and the event name are passed to the listener call. + +2.5.0 +----- + + * added Debug\TraceableEventDispatcher (originally in HttpKernel) + * changed Debug\TraceableEventDispatcherInterface to extend EventDispatcherInterface + * added RegisterListenersPass (originally in HttpKernel) + +2.1.0 +----- + + * added TraceableEventDispatcherInterface + * added ContainerAwareEventDispatcher + * added a reference to the EventDispatcher on the Event + * added a reference to the Event name on the event + * added fluid interface to the dispatch() method which now returns the Event + object + * added GenericEvent event class + * added the possibility for subscribers to subscribe several times for the + same event + * added ImmutableEventDispatcher diff --git a/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php b/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php new file mode 100644 index 0000000..84d6a08 --- /dev/null +++ b/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php @@ -0,0 +1,366 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Debug; + +use Psr\EventDispatcher\StoppableEventInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Contracts\Service\ResetInterface; + +/** + * Collects some data about event listeners. + * + * This event dispatcher delegates the dispatching to another one. + * + * @author Fabien Potencier + */ +class TraceableEventDispatcher implements EventDispatcherInterface, ResetInterface +{ + protected $logger; + protected $stopwatch; + + /** + * @var \SplObjectStorage + */ + private $callStack; + private $dispatcher; + private $wrappedListeners; + private $orphanedEvents; + private $requestStack; + private $currentRequestHash = ''; + + public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $stopwatch, ?LoggerInterface $logger = null, ?RequestStack $requestStack = null) + { + $this->dispatcher = $dispatcher; + $this->stopwatch = $stopwatch; + $this->logger = $logger; + $this->wrappedListeners = []; + $this->orphanedEvents = []; + $this->requestStack = $requestStack; + } + + /** + * {@inheritdoc} + */ + public function addListener(string $eventName, $listener, int $priority = 0) + { + $this->dispatcher->addListener($eventName, $listener, $priority); + } + + /** + * {@inheritdoc} + */ + public function addSubscriber(EventSubscriberInterface $subscriber) + { + $this->dispatcher->addSubscriber($subscriber); + } + + /** + * {@inheritdoc} + */ + public function removeListener(string $eventName, $listener) + { + if (isset($this->wrappedListeners[$eventName])) { + foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) { + if ($wrappedListener->getWrappedListener() === $listener || ($listener instanceof \Closure && $wrappedListener->getWrappedListener() == $listener)) { + $listener = $wrappedListener; + unset($this->wrappedListeners[$eventName][$index]); + break; + } + } + } + + return $this->dispatcher->removeListener($eventName, $listener); + } + + /** + * {@inheritdoc} + */ + public function removeSubscriber(EventSubscriberInterface $subscriber) + { + return $this->dispatcher->removeSubscriber($subscriber); + } + + /** + * {@inheritdoc} + */ + public function getListeners(?string $eventName = null) + { + return $this->dispatcher->getListeners($eventName); + } + + /** + * {@inheritdoc} + */ + public function getListenerPriority(string $eventName, $listener) + { + // we might have wrapped listeners for the event (if called while dispatching) + // in that case get the priority by wrapper + if (isset($this->wrappedListeners[$eventName])) { + foreach ($this->wrappedListeners[$eventName] as $wrappedListener) { + if ($wrappedListener->getWrappedListener() === $listener || ($listener instanceof \Closure && $wrappedListener->getWrappedListener() == $listener)) { + return $this->dispatcher->getListenerPriority($eventName, $wrappedListener); + } + } + } + + return $this->dispatcher->getListenerPriority($eventName, $listener); + } + + /** + * {@inheritdoc} + */ + public function hasListeners(?string $eventName = null) + { + return $this->dispatcher->hasListeners($eventName); + } + + /** + * {@inheritdoc} + */ + public function dispatch(object $event, ?string $eventName = null): object + { + $eventName = $eventName ?? \get_class($event); + + if (null === $this->callStack) { + $this->callStack = new \SplObjectStorage(); + } + + $currentRequestHash = $this->currentRequestHash = $this->requestStack && ($request = $this->requestStack->getCurrentRequest()) ? spl_object_hash($request) : ''; + + if (null !== $this->logger && $event instanceof StoppableEventInterface && $event->isPropagationStopped()) { + $this->logger->debug(sprintf('The "%s" event is already stopped. No listeners have been called.', $eventName)); + } + + $this->preProcess($eventName); + try { + $this->beforeDispatch($eventName, $event); + try { + $e = $this->stopwatch->start($eventName, 'section'); + try { + $this->dispatcher->dispatch($event, $eventName); + } finally { + if ($e->isStarted()) { + $e->stop(); + } + } + } finally { + $this->afterDispatch($eventName, $event); + } + } finally { + $this->currentRequestHash = $currentRequestHash; + $this->postProcess($eventName); + } + + return $event; + } + + /** + * @return array + */ + public function getCalledListeners(?Request $request = null) + { + if (null === $this->callStack) { + return []; + } + + $hash = $request ? spl_object_hash($request) : null; + $called = []; + foreach ($this->callStack as $listener) { + [$eventName, $requestHash] = $this->callStack->getInfo(); + if (null === $hash || $hash === $requestHash) { + $called[] = $listener->getInfo($eventName); + } + } + + return $called; + } + + /** + * @return array + */ + public function getNotCalledListeners(?Request $request = null) + { + try { + $allListeners = $this->getListeners(); + } catch (\Exception $e) { + if (null !== $this->logger) { + $this->logger->info('An exception was thrown while getting the uncalled listeners.', ['exception' => $e]); + } + + // unable to retrieve the uncalled listeners + return []; + } + + $hash = $request ? spl_object_hash($request) : null; + $calledListeners = []; + + if (null !== $this->callStack) { + foreach ($this->callStack as $calledListener) { + [, $requestHash] = $this->callStack->getInfo(); + + if (null === $hash || $hash === $requestHash) { + $calledListeners[] = $calledListener->getWrappedListener(); + } + } + } + + $notCalled = []; + foreach ($allListeners as $eventName => $listeners) { + foreach ($listeners as $listener) { + if (!\in_array($listener, $calledListeners, true)) { + if (!$listener instanceof WrappedListener) { + $listener = new WrappedListener($listener, null, $this->stopwatch, $this); + } + $notCalled[] = $listener->getInfo($eventName); + } + } + } + + uasort($notCalled, [$this, 'sortNotCalledListeners']); + + return $notCalled; + } + + public function getOrphanedEvents(?Request $request = null): array + { + if ($request) { + return $this->orphanedEvents[spl_object_hash($request)] ?? []; + } + + if (!$this->orphanedEvents) { + return []; + } + + return array_merge(...array_values($this->orphanedEvents)); + } + + public function reset() + { + $this->callStack = null; + $this->orphanedEvents = []; + $this->currentRequestHash = ''; + } + + /** + * Proxies all method calls to the original event dispatcher. + * + * @param string $method The method name + * @param array $arguments The method arguments + * + * @return mixed + */ + public function __call(string $method, array $arguments) + { + return $this->dispatcher->{$method}(...$arguments); + } + + /** + * Called before dispatching the event. + */ + protected function beforeDispatch(string $eventName, object $event) + { + } + + /** + * Called after dispatching the event. + */ + protected function afterDispatch(string $eventName, object $event) + { + } + + private function preProcess(string $eventName): void + { + if (!$this->dispatcher->hasListeners($eventName)) { + $this->orphanedEvents[$this->currentRequestHash][] = $eventName; + + return; + } + + foreach ($this->dispatcher->getListeners($eventName) as $listener) { + $priority = $this->getListenerPriority($eventName, $listener); + $wrappedListener = new WrappedListener($listener instanceof WrappedListener ? $listener->getWrappedListener() : $listener, null, $this->stopwatch, $this); + $this->wrappedListeners[$eventName][] = $wrappedListener; + $this->dispatcher->removeListener($eventName, $listener); + $this->dispatcher->addListener($eventName, $wrappedListener, $priority); + $this->callStack->attach($wrappedListener, [$eventName, $this->currentRequestHash]); + } + } + + private function postProcess(string $eventName): void + { + unset($this->wrappedListeners[$eventName]); + $skipped = false; + foreach ($this->dispatcher->getListeners($eventName) as $listener) { + if (!$listener instanceof WrappedListener) { // #12845: a new listener was added during dispatch. + continue; + } + // Unwrap listener + $priority = $this->getListenerPriority($eventName, $listener); + $this->dispatcher->removeListener($eventName, $listener); + $this->dispatcher->addListener($eventName, $listener->getWrappedListener(), $priority); + + if (null !== $this->logger) { + $context = ['event' => $eventName, 'listener' => $listener->getPretty()]; + } + + if ($listener->wasCalled()) { + if (null !== $this->logger) { + $this->logger->debug('Notified event "{event}" to listener "{listener}".', $context); + } + } else { + $this->callStack->detach($listener); + } + + if (null !== $this->logger && $skipped) { + $this->logger->debug('Listener "{listener}" was not called for event "{event}".', $context); + } + + if ($listener->stoppedPropagation()) { + if (null !== $this->logger) { + $this->logger->debug('Listener "{listener}" stopped propagation of the event "{event}".', $context); + } + + $skipped = true; + } + } + } + + private function sortNotCalledListeners(array $a, array $b) + { + if (0 !== $cmp = strcmp($a['event'], $b['event'])) { + return $cmp; + } + + if (\is_int($a['priority']) && !\is_int($b['priority'])) { + return 1; + } + + if (!\is_int($a['priority']) && \is_int($b['priority'])) { + return -1; + } + + if ($a['priority'] === $b['priority']) { + return 0; + } + + if ($a['priority'] > $b['priority']) { + return -1; + } + + return 1; + } +} diff --git a/vendor/symfony/event-dispatcher/Debug/WrappedListener.php b/vendor/symfony/event-dispatcher/Debug/WrappedListener.php new file mode 100644 index 0000000..792c175 --- /dev/null +++ b/vendor/symfony/event-dispatcher/Debug/WrappedListener.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Debug; + +use Psr\EventDispatcher\StoppableEventInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\VarDumper\Caster\ClassStub; + +/** + * @author Fabien Potencier + */ +final class WrappedListener +{ + private $listener; + private $optimizedListener; + private $name; + private $called; + private $stoppedPropagation; + private $stopwatch; + private $dispatcher; + private $pretty; + private $stub; + private $priority; + private static $hasClassStub; + + public function __construct($listener, ?string $name, Stopwatch $stopwatch, ?EventDispatcherInterface $dispatcher = null) + { + $this->listener = $listener; + $this->optimizedListener = $listener instanceof \Closure ? $listener : (\is_callable($listener) ? \Closure::fromCallable($listener) : null); + $this->stopwatch = $stopwatch; + $this->dispatcher = $dispatcher; + $this->called = false; + $this->stoppedPropagation = false; + + if (\is_array($listener)) { + $this->name = \is_object($listener[0]) ? get_debug_type($listener[0]) : $listener[0]; + $this->pretty = $this->name.'::'.$listener[1]; + } elseif ($listener instanceof \Closure) { + $r = new \ReflectionFunction($listener); + if (str_contains($r->name, '{closure')) { + $this->pretty = $this->name = 'closure'; + } elseif ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { + $this->name = $class->name; + $this->pretty = $this->name.'::'.$r->name; + } else { + $this->pretty = $this->name = $r->name; + } + } elseif (\is_string($listener)) { + $this->pretty = $this->name = $listener; + } else { + $this->name = get_debug_type($listener); + $this->pretty = $this->name.'::__invoke'; + } + + if (null !== $name) { + $this->name = $name; + } + + if (null === self::$hasClassStub) { + self::$hasClassStub = class_exists(ClassStub::class); + } + } + + public function getWrappedListener() + { + return $this->listener; + } + + public function wasCalled(): bool + { + return $this->called; + } + + public function stoppedPropagation(): bool + { + return $this->stoppedPropagation; + } + + public function getPretty(): string + { + return $this->pretty; + } + + public function getInfo(string $eventName): array + { + if (null === $this->stub) { + $this->stub = self::$hasClassStub ? new ClassStub($this->pretty.'()', $this->listener) : $this->pretty.'()'; + } + + return [ + 'event' => $eventName, + 'priority' => null !== $this->priority ? $this->priority : (null !== $this->dispatcher ? $this->dispatcher->getListenerPriority($eventName, $this->listener) : null), + 'pretty' => $this->pretty, + 'stub' => $this->stub, + ]; + } + + public function __invoke(object $event, string $eventName, EventDispatcherInterface $dispatcher): void + { + $dispatcher = $this->dispatcher ?: $dispatcher; + + $this->called = true; + $this->priority = $dispatcher->getListenerPriority($eventName, $this->listener); + + $e = $this->stopwatch->start($this->name, 'event_listener'); + + try { + ($this->optimizedListener ?? $this->listener)($event, $eventName, $dispatcher); + } finally { + if ($e->isStarted()) { + $e->stop(); + } + } + + if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { + $this->stoppedPropagation = true; + } + } +} diff --git a/vendor/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php b/vendor/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php new file mode 100644 index 0000000..6e7292b --- /dev/null +++ b/vendor/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * This pass allows bundles to extend the list of event aliases. + * + * @author Alexander M. Turek + */ +class AddEventAliasesPass implements CompilerPassInterface +{ + private $eventAliases; + private $eventAliasesParameter; + + public function __construct(array $eventAliases, string $eventAliasesParameter = 'event_dispatcher.event_aliases') + { + if (1 < \func_num_args()) { + trigger_deprecation('symfony/event-dispatcher', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + + $this->eventAliases = $eventAliases; + $this->eventAliasesParameter = $eventAliasesParameter; + } + + public function process(ContainerBuilder $container): void + { + $eventAliases = $container->hasParameter($this->eventAliasesParameter) ? $container->getParameter($this->eventAliasesParameter) : []; + + $container->setParameter( + $this->eventAliasesParameter, + array_merge($eventAliases, $this->eventAliases) + ); + } +} diff --git a/vendor/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php b/vendor/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php new file mode 100644 index 0000000..5f44ff0 --- /dev/null +++ b/vendor/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php @@ -0,0 +1,242 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\DependencyInjection; + +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Contracts\EventDispatcher\Event; + +/** + * Compiler pass to register tagged services for an event dispatcher. + */ +class RegisterListenersPass implements CompilerPassInterface +{ + protected $dispatcherService; + protected $listenerTag; + protected $subscriberTag; + protected $eventAliasesParameter; + + private $hotPathEvents = []; + private $hotPathTagName = 'container.hot_path'; + private $noPreloadEvents = []; + private $noPreloadTagName = 'container.no_preload'; + + public function __construct(string $dispatcherService = 'event_dispatcher', string $listenerTag = 'kernel.event_listener', string $subscriberTag = 'kernel.event_subscriber', string $eventAliasesParameter = 'event_dispatcher.event_aliases') + { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/event-dispatcher', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + + $this->dispatcherService = $dispatcherService; + $this->listenerTag = $listenerTag; + $this->subscriberTag = $subscriberTag; + $this->eventAliasesParameter = $eventAliasesParameter; + } + + /** + * @return $this + */ + public function setHotPathEvents(array $hotPathEvents) + { + $this->hotPathEvents = array_flip($hotPathEvents); + + if (1 < \func_num_args()) { + trigger_deprecation('symfony/event-dispatcher', '5.4', 'Configuring "$tagName" in "%s" is deprecated.', __METHOD__); + $this->hotPathTagName = func_get_arg(1); + } + + return $this; + } + + /** + * @return $this + */ + public function setNoPreloadEvents(array $noPreloadEvents): self + { + $this->noPreloadEvents = array_flip($noPreloadEvents); + + if (1 < \func_num_args()) { + trigger_deprecation('symfony/event-dispatcher', '5.4', 'Configuring "$tagName" in "%s" is deprecated.', __METHOD__); + $this->noPreloadTagName = func_get_arg(1); + } + + return $this; + } + + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition($this->dispatcherService) && !$container->hasAlias($this->dispatcherService)) { + return; + } + + $aliases = []; + + if ($container->hasParameter($this->eventAliasesParameter)) { + $aliases = $container->getParameter($this->eventAliasesParameter); + } + + $globalDispatcherDefinition = $container->findDefinition($this->dispatcherService); + + foreach ($container->findTaggedServiceIds($this->listenerTag, true) as $id => $events) { + $noPreload = 0; + + foreach ($events as $event) { + $priority = $event['priority'] ?? 0; + + if (!isset($event['event'])) { + if ($container->getDefinition($id)->hasTag($this->subscriberTag)) { + continue; + } + + $event['method'] = $event['method'] ?? '__invoke'; + $event['event'] = $this->getEventFromTypeDeclaration($container, $id, $event['method']); + } + + $event['event'] = $aliases[$event['event']] ?? $event['event']; + + if (!isset($event['method'])) { + $event['method'] = 'on'.preg_replace_callback([ + '/(?<=\b|_)[a-z]/i', + '/[^a-z0-9]/i', + ], function ($matches) { return strtoupper($matches[0]); }, $event['event']); + $event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']); + + if (null !== ($class = $container->getDefinition($id)->getClass()) && ($r = $container->getReflectionClass($class, false)) && !$r->hasMethod($event['method'])) { + if (!$r->hasMethod('__invoke')) { + throw new InvalidArgumentException(sprintf('None of the "%s" or "__invoke" methods exist for the service "%s". Please define the "method" attribute on "%s" tags.', $event['method'], $id, $this->listenerTag)); + } + + $event['method'] = '__invoke'; + } + } + + $dispatcherDefinition = $globalDispatcherDefinition; + if (isset($event['dispatcher'])) { + $dispatcherDefinition = $container->findDefinition($event['dispatcher']); + } + + $dispatcherDefinition->addMethodCall('addListener', [$event['event'], [new ServiceClosureArgument(new Reference($id)), $event['method']], $priority]); + + if (isset($this->hotPathEvents[$event['event']])) { + $container->getDefinition($id)->addTag($this->hotPathTagName); + } elseif (isset($this->noPreloadEvents[$event['event']])) { + ++$noPreload; + } + } + + if ($noPreload && \count($events) === $noPreload) { + $container->getDefinition($id)->addTag($this->noPreloadTagName); + } + } + + $extractingDispatcher = new ExtractingEventDispatcher(); + + foreach ($container->findTaggedServiceIds($this->subscriberTag, true) as $id => $tags) { + $def = $container->getDefinition($id); + + // We must assume that the class value has been correctly filled, even if the service is created by a factory + $class = $def->getClass(); + + if (!$r = $container->getReflectionClass($class)) { + throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); + } + if (!$r->isSubclassOf(EventSubscriberInterface::class)) { + throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, EventSubscriberInterface::class)); + } + $class = $r->name; + + $dispatcherDefinitions = []; + foreach ($tags as $attributes) { + if (!isset($attributes['dispatcher']) || isset($dispatcherDefinitions[$attributes['dispatcher']])) { + continue; + } + + $dispatcherDefinitions[$attributes['dispatcher']] = $container->findDefinition($attributes['dispatcher']); + } + + if (!$dispatcherDefinitions) { + $dispatcherDefinitions = [$globalDispatcherDefinition]; + } + + $noPreload = 0; + ExtractingEventDispatcher::$aliases = $aliases; + ExtractingEventDispatcher::$subscriber = $class; + $extractingDispatcher->addSubscriber($extractingDispatcher); + foreach ($extractingDispatcher->listeners as $args) { + $args[1] = [new ServiceClosureArgument(new Reference($id)), $args[1]]; + foreach ($dispatcherDefinitions as $dispatcherDefinition) { + $dispatcherDefinition->addMethodCall('addListener', $args); + } + + if (isset($this->hotPathEvents[$args[0]])) { + $container->getDefinition($id)->addTag($this->hotPathTagName); + } elseif (isset($this->noPreloadEvents[$args[0]])) { + ++$noPreload; + } + } + if ($noPreload && \count($extractingDispatcher->listeners) === $noPreload) { + $container->getDefinition($id)->addTag($this->noPreloadTagName); + } + $extractingDispatcher->listeners = []; + ExtractingEventDispatcher::$aliases = []; + } + } + + private function getEventFromTypeDeclaration(ContainerBuilder $container, string $id, string $method): string + { + if ( + null === ($class = $container->getDefinition($id)->getClass()) + || !($r = $container->getReflectionClass($class, false)) + || !$r->hasMethod($method) + || 1 > ($m = $r->getMethod($method))->getNumberOfParameters() + || !($type = $m->getParameters()[0]->getType()) instanceof \ReflectionNamedType + || $type->isBuiltin() + || Event::class === ($name = $type->getName()) + ) { + throw new InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "%s" tags.', $id, $this->listenerTag)); + } + + return $name; + } +} + +/** + * @internal + */ +class ExtractingEventDispatcher extends EventDispatcher implements EventSubscriberInterface +{ + public $listeners = []; + + public static $aliases = []; + public static $subscriber; + + public function addListener(string $eventName, $listener, int $priority = 0) + { + $this->listeners[] = [$eventName, $listener[1], $priority]; + } + + public static function getSubscribedEvents(): array + { + $events = []; + + foreach ([self::$subscriber, 'getSubscribedEvents']() as $eventName => $params) { + $events[self::$aliases[$eventName] ?? $eventName] = $params; + } + + return $events; + } +} diff --git a/vendor/symfony/event-dispatcher/EventDispatcher.php b/vendor/symfony/event-dispatcher/EventDispatcher.php new file mode 100644 index 0000000..9c86bd9 --- /dev/null +++ b/vendor/symfony/event-dispatcher/EventDispatcher.php @@ -0,0 +1,280 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +use Psr\EventDispatcher\StoppableEventInterface; +use Symfony\Component\EventDispatcher\Debug\WrappedListener; + +/** + * The EventDispatcherInterface is the central point of Symfony's event listener system. + * + * Listeners are registered on the manager and events are dispatched through the + * manager. + * + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Bernhard Schussek + * @author Fabien Potencier + * @author Jordi Boggiano + * @author Jordan Alliot + * @author Nicolas Grekas + */ +class EventDispatcher implements EventDispatcherInterface +{ + private $listeners = []; + private $sorted = []; + private $optimized; + + public function __construct() + { + if (__CLASS__ === static::class) { + $this->optimized = []; + } + } + + /** + * {@inheritdoc} + */ + public function dispatch(object $event, ?string $eventName = null): object + { + $eventName = $eventName ?? \get_class($event); + + if (null !== $this->optimized) { + $listeners = $this->optimized[$eventName] ?? (empty($this->listeners[$eventName]) ? [] : $this->optimizeListeners($eventName)); + } else { + $listeners = $this->getListeners($eventName); + } + + if ($listeners) { + $this->callListeners($listeners, $eventName, $event); + } + + return $event; + } + + /** + * {@inheritdoc} + */ + public function getListeners(?string $eventName = null) + { + if (null !== $eventName) { + if (empty($this->listeners[$eventName])) { + return []; + } + + if (!isset($this->sorted[$eventName])) { + $this->sortListeners($eventName); + } + + return $this->sorted[$eventName]; + } + + foreach ($this->listeners as $eventName => $eventListeners) { + if (!isset($this->sorted[$eventName])) { + $this->sortListeners($eventName); + } + } + + return array_filter($this->sorted); + } + + /** + * {@inheritdoc} + */ + public function getListenerPriority(string $eventName, $listener) + { + if (empty($this->listeners[$eventName])) { + return null; + } + + if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { + $listener[0] = $listener[0](); + $listener[1] = $listener[1] ?? '__invoke'; + } + + foreach ($this->listeners[$eventName] as $priority => &$listeners) { + foreach ($listeners as &$v) { + if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) { + $v[0] = $v[0](); + $v[1] = $v[1] ?? '__invoke'; + } + if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) { + return $priority; + } + } + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function hasListeners(?string $eventName = null) + { + if (null !== $eventName) { + return !empty($this->listeners[$eventName]); + } + + foreach ($this->listeners as $eventListeners) { + if ($eventListeners) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function addListener(string $eventName, $listener, int $priority = 0) + { + $this->listeners[$eventName][$priority][] = $listener; + unset($this->sorted[$eventName], $this->optimized[$eventName]); + } + + /** + * {@inheritdoc} + */ + public function removeListener(string $eventName, $listener) + { + if (empty($this->listeners[$eventName])) { + return; + } + + if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { + $listener[0] = $listener[0](); + $listener[1] = $listener[1] ?? '__invoke'; + } + + foreach ($this->listeners[$eventName] as $priority => &$listeners) { + foreach ($listeners as $k => &$v) { + if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) { + $v[0] = $v[0](); + $v[1] = $v[1] ?? '__invoke'; + } + if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) { + unset($listeners[$k], $this->sorted[$eventName], $this->optimized[$eventName]); + } + } + + if (!$listeners) { + unset($this->listeners[$eventName][$priority]); + } + } + } + + /** + * {@inheritdoc} + */ + public function addSubscriber(EventSubscriberInterface $subscriber) + { + foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { + if (\is_string($params)) { + $this->addListener($eventName, [$subscriber, $params]); + } elseif (\is_string($params[0])) { + $this->addListener($eventName, [$subscriber, $params[0]], $params[1] ?? 0); + } else { + foreach ($params as $listener) { + $this->addListener($eventName, [$subscriber, $listener[0]], $listener[1] ?? 0); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function removeSubscriber(EventSubscriberInterface $subscriber) + { + foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { + if (\is_array($params) && \is_array($params[0])) { + foreach ($params as $listener) { + $this->removeListener($eventName, [$subscriber, $listener[0]]); + } + } else { + $this->removeListener($eventName, [$subscriber, \is_string($params) ? $params : $params[0]]); + } + } + } + + /** + * Triggers the listeners of an event. + * + * This method can be overridden to add functionality that is executed + * for each listener. + * + * @param callable[] $listeners The event listeners + * @param string $eventName The name of the event to dispatch + * @param object $event The event object to pass to the event handlers/listeners + */ + protected function callListeners(iterable $listeners, string $eventName, object $event) + { + $stoppable = $event instanceof StoppableEventInterface; + + foreach ($listeners as $listener) { + if ($stoppable && $event->isPropagationStopped()) { + break; + } + $listener($event, $eventName, $this); + } + } + + /** + * Sorts the internal list of listeners for the given event by priority. + */ + private function sortListeners(string $eventName) + { + krsort($this->listeners[$eventName]); + $this->sorted[$eventName] = []; + + foreach ($this->listeners[$eventName] as &$listeners) { + foreach ($listeners as $k => &$listener) { + if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { + $listener[0] = $listener[0](); + $listener[1] = $listener[1] ?? '__invoke'; + } + $this->sorted[$eventName][] = $listener; + } + } + } + + /** + * Optimizes the internal list of listeners for the given event by priority. + */ + private function optimizeListeners(string $eventName): array + { + krsort($this->listeners[$eventName]); + $this->optimized[$eventName] = []; + + foreach ($this->listeners[$eventName] as &$listeners) { + foreach ($listeners as &$listener) { + $closure = &$this->optimized[$eventName][]; + if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { + $closure = static function (...$args) use (&$listener, &$closure) { + if ($listener[0] instanceof \Closure) { + $listener[0] = $listener[0](); + $listener[1] = $listener[1] ?? '__invoke'; + } + ($closure = \Closure::fromCallable($listener))(...$args); + }; + } else { + $closure = $listener instanceof \Closure || $listener instanceof WrappedListener ? $listener : \Closure::fromCallable($listener); + } + } + } + + return $this->optimized[$eventName]; + } +} diff --git a/vendor/symfony/event-dispatcher/EventDispatcherInterface.php b/vendor/symfony/event-dispatcher/EventDispatcherInterface.php new file mode 100644 index 0000000..4b65e5a --- /dev/null +++ b/vendor/symfony/event-dispatcher/EventDispatcherInterface.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface; + +/** + * The EventDispatcherInterface is the central point of Symfony's event listener system. + * Listeners are registered on the manager and events are dispatched through the + * manager. + * + * @author Bernhard Schussek + */ +interface EventDispatcherInterface extends ContractsEventDispatcherInterface +{ + /** + * Adds an event listener that listens on the specified events. + * + * @param int $priority The higher this value, the earlier an event + * listener will be triggered in the chain (defaults to 0) + */ + public function addListener(string $eventName, callable $listener, int $priority = 0); + + /** + * Adds an event subscriber. + * + * The subscriber is asked for all the events it is + * interested in and added as a listener for these events. + */ + public function addSubscriber(EventSubscriberInterface $subscriber); + + /** + * Removes an event listener from the specified events. + */ + public function removeListener(string $eventName, callable $listener); + + public function removeSubscriber(EventSubscriberInterface $subscriber); + + /** + * Gets the listeners of a specific event or all listeners sorted by descending priority. + * + * @return array + */ + public function getListeners(?string $eventName = null); + + /** + * Gets the listener priority for a specific event. + * + * Returns null if the event or the listener does not exist. + * + * @return int|null + */ + public function getListenerPriority(string $eventName, callable $listener); + + /** + * Checks whether an event has any registered listeners. + * + * @return bool + */ + public function hasListeners(?string $eventName = null); +} diff --git a/vendor/symfony/event-dispatcher/EventSubscriberInterface.php b/vendor/symfony/event-dispatcher/EventSubscriberInterface.php new file mode 100644 index 0000000..2085e42 --- /dev/null +++ b/vendor/symfony/event-dispatcher/EventSubscriberInterface.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +/** + * An EventSubscriber knows itself what events it is interested in. + * If an EventSubscriber is added to an EventDispatcherInterface, the manager invokes + * {@link getSubscribedEvents} and registers the subscriber as a listener for all + * returned events. + * + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Bernhard Schussek + */ +interface EventSubscriberInterface +{ + /** + * Returns an array of event names this subscriber wants to listen to. + * + * The array keys are event names and the value can be: + * + * * The method name to call (priority defaults to 0) + * * An array composed of the method name to call and the priority + * * An array of arrays composed of the method names to call and respective + * priorities, or 0 if unset + * + * For instance: + * + * * ['eventName' => 'methodName'] + * * ['eventName' => ['methodName', $priority]] + * * ['eventName' => [['methodName1', $priority], ['methodName2']]] + * + * The code must not depend on runtime state as it will only be called at compile time. + * All logic depending on runtime state must be put into the individual methods handling the events. + * + * @return array> + */ + public static function getSubscribedEvents(); +} diff --git a/vendor/symfony/event-dispatcher/GenericEvent.php b/vendor/symfony/event-dispatcher/GenericEvent.php new file mode 100644 index 0000000..4ecd29e --- /dev/null +++ b/vendor/symfony/event-dispatcher/GenericEvent.php @@ -0,0 +1,182 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +use Symfony\Contracts\EventDispatcher\Event; + +/** + * Event encapsulation class. + * + * Encapsulates events thus decoupling the observer from the subject they encapsulate. + * + * @author Drak + * + * @implements \ArrayAccess + * @implements \IteratorAggregate + */ +class GenericEvent extends Event implements \ArrayAccess, \IteratorAggregate +{ + protected $subject; + protected $arguments; + + /** + * Encapsulate an event with $subject and $arguments. + * + * @param mixed $subject The subject of the event, usually an object or a callable + * @param array $arguments Arguments to store in the event + */ + public function __construct($subject = null, array $arguments = []) + { + $this->subject = $subject; + $this->arguments = $arguments; + } + + /** + * Getter for subject property. + * + * @return mixed + */ + public function getSubject() + { + return $this->subject; + } + + /** + * Get argument by key. + * + * @return mixed + * + * @throws \InvalidArgumentException if key is not found + */ + public function getArgument(string $key) + { + if ($this->hasArgument($key)) { + return $this->arguments[$key]; + } + + throw new \InvalidArgumentException(sprintf('Argument "%s" not found.', $key)); + } + + /** + * Add argument to event. + * + * @param mixed $value Value + * + * @return $this + */ + public function setArgument(string $key, $value) + { + $this->arguments[$key] = $value; + + return $this; + } + + /** + * Getter for all arguments. + * + * @return array + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * Set args property. + * + * @return $this + */ + public function setArguments(array $args = []) + { + $this->arguments = $args; + + return $this; + } + + /** + * Has argument. + * + * @return bool + */ + public function hasArgument(string $key) + { + return \array_key_exists($key, $this->arguments); + } + + /** + * ArrayAccess for argument getter. + * + * @param string $key Array key + * + * @return mixed + * + * @throws \InvalidArgumentException if key does not exist in $this->args + */ + #[\ReturnTypeWillChange] + public function offsetGet($key) + { + return $this->getArgument($key); + } + + /** + * ArrayAccess for argument setter. + * + * @param string $key Array key to set + * @param mixed $value Value + * + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetSet($key, $value) + { + $this->setArgument($key, $value); + } + + /** + * ArrayAccess for unset argument. + * + * @param string $key Array key + * + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetUnset($key) + { + if ($this->hasArgument($key)) { + unset($this->arguments[$key]); + } + } + + /** + * ArrayAccess has argument. + * + * @param string $key Array key + * + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($key) + { + return $this->hasArgument($key); + } + + /** + * IteratorAggregate for iterating over the object like an array. + * + * @return \ArrayIterator + */ + #[\ReturnTypeWillChange] + public function getIterator() + { + return new \ArrayIterator($this->arguments); + } +} diff --git a/vendor/symfony/event-dispatcher/ImmutableEventDispatcher.php b/vendor/symfony/event-dispatcher/ImmutableEventDispatcher.php new file mode 100644 index 0000000..4e00bfa --- /dev/null +++ b/vendor/symfony/event-dispatcher/ImmutableEventDispatcher.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +/** + * A read-only proxy for an event dispatcher. + * + * @author Bernhard Schussek + */ +class ImmutableEventDispatcher implements EventDispatcherInterface +{ + private $dispatcher; + + public function __construct(EventDispatcherInterface $dispatcher) + { + $this->dispatcher = $dispatcher; + } + + /** + * {@inheritdoc} + */ + public function dispatch(object $event, ?string $eventName = null): object + { + return $this->dispatcher->dispatch($event, $eventName); + } + + /** + * {@inheritdoc} + */ + public function addListener(string $eventName, $listener, int $priority = 0) + { + throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); + } + + /** + * {@inheritdoc} + */ + public function addSubscriber(EventSubscriberInterface $subscriber) + { + throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); + } + + /** + * {@inheritdoc} + */ + public function removeListener(string $eventName, $listener) + { + throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); + } + + /** + * {@inheritdoc} + */ + public function removeSubscriber(EventSubscriberInterface $subscriber) + { + throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); + } + + /** + * {@inheritdoc} + */ + public function getListeners(?string $eventName = null) + { + return $this->dispatcher->getListeners($eventName); + } + + /** + * {@inheritdoc} + */ + public function getListenerPriority(string $eventName, $listener) + { + return $this->dispatcher->getListenerPriority($eventName, $listener); + } + + /** + * {@inheritdoc} + */ + public function hasListeners(?string $eventName = null) + { + return $this->dispatcher->hasListeners($eventName); + } +} diff --git a/vendor/symfony/event-dispatcher/LICENSE b/vendor/symfony/event-dispatcher/LICENSE new file mode 100644 index 0000000..0138f8f --- /dev/null +++ b/vendor/symfony/event-dispatcher/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/event-dispatcher/LegacyEventDispatcherProxy.php b/vendor/symfony/event-dispatcher/LegacyEventDispatcherProxy.php new file mode 100644 index 0000000..6e17c8f --- /dev/null +++ b/vendor/symfony/event-dispatcher/LegacyEventDispatcherProxy.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +trigger_deprecation('symfony/event-dispatcher', '5.1', '%s is deprecated, use the event dispatcher without the proxy.', LegacyEventDispatcherProxy::class); + +/** + * A helper class to provide BC/FC with the legacy signature of EventDispatcherInterface::dispatch(). + * + * @author Nicolas Grekas + * + * @deprecated since Symfony 5.1 + */ +final class LegacyEventDispatcherProxy +{ + public static function decorate(?EventDispatcherInterface $dispatcher): ?EventDispatcherInterface + { + return $dispatcher; + } +} diff --git a/vendor/symfony/event-dispatcher/README.md b/vendor/symfony/event-dispatcher/README.md new file mode 100644 index 0000000..dcdb68d --- /dev/null +++ b/vendor/symfony/event-dispatcher/README.md @@ -0,0 +1,15 @@ +EventDispatcher Component +========================= + +The EventDispatcher component provides tools that allow your application +components to communicate with each other by dispatching events and listening to +them. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/event_dispatcher.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/vendor/symfony/event-dispatcher/composer.json b/vendor/symfony/event-dispatcher/composer.json new file mode 100644 index 0000000..32b42e4 --- /dev/null +++ b/vendor/symfony/event-dispatcher/composer.json @@ -0,0 +1,52 @@ +{ + "name": "symfony/event-dispatcher", + "type": "library", + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher-contracts": "^2|^3", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/stopwatch": "^4.4|^5.0|^6.0", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "symfony/dependency-injection": "<4.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\EventDispatcher\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/vendor/symfony/http-foundation/AcceptHeader.php b/vendor/symfony/http-foundation/AcceptHeader.php new file mode 100644 index 0000000..057c6b5 --- /dev/null +++ b/vendor/symfony/http-foundation/AcceptHeader.php @@ -0,0 +1,168 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +// Help opcache.preload discover always-needed symbols +class_exists(AcceptHeaderItem::class); + +/** + * Represents an Accept-* header. + * + * An accept header is compound with a list of items, + * sorted by descending quality. + * + * @author Jean-François Simon + */ +class AcceptHeader +{ + /** + * @var AcceptHeaderItem[] + */ + private $items = []; + + /** + * @var bool + */ + private $sorted = true; + + /** + * @param AcceptHeaderItem[] $items + */ + public function __construct(array $items) + { + foreach ($items as $item) { + $this->add($item); + } + } + + /** + * Builds an AcceptHeader instance from a string. + * + * @return self + */ + public static function fromString(?string $headerValue) + { + $index = 0; + + $parts = HeaderUtils::split($headerValue ?? '', ',;='); + + return new self(array_map(function ($subParts) use (&$index) { + $part = array_shift($subParts); + $attributes = HeaderUtils::combine($subParts); + + $item = new AcceptHeaderItem($part[0], $attributes); + $item->setIndex($index++); + + return $item; + }, $parts)); + } + + /** + * Returns header value's string representation. + * + * @return string + */ + public function __toString() + { + return implode(',', $this->items); + } + + /** + * Tests if header has given value. + * + * @return bool + */ + public function has(string $value) + { + return isset($this->items[$value]); + } + + /** + * Returns given value's item, if exists. + * + * @return AcceptHeaderItem|null + */ + public function get(string $value) + { + return $this->items[$value] ?? $this->items[explode('/', $value)[0].'/*'] ?? $this->items['*/*'] ?? $this->items['*'] ?? null; + } + + /** + * Adds an item. + * + * @return $this + */ + public function add(AcceptHeaderItem $item) + { + $this->items[$item->getValue()] = $item; + $this->sorted = false; + + return $this; + } + + /** + * Returns all items. + * + * @return AcceptHeaderItem[] + */ + public function all() + { + $this->sort(); + + return $this->items; + } + + /** + * Filters items on their value using given regex. + * + * @return self + */ + public function filter(string $pattern) + { + return new self(array_filter($this->items, function (AcceptHeaderItem $item) use ($pattern) { + return preg_match($pattern, $item->getValue()); + })); + } + + /** + * Returns first item. + * + * @return AcceptHeaderItem|null + */ + public function first() + { + $this->sort(); + + return !empty($this->items) ? reset($this->items) : null; + } + + /** + * Sorts items by descending quality. + */ + private function sort(): void + { + if (!$this->sorted) { + uasort($this->items, function (AcceptHeaderItem $a, AcceptHeaderItem $b) { + $qA = $a->getQuality(); + $qB = $b->getQuality(); + + if ($qA === $qB) { + return $a->getIndex() > $b->getIndex() ? 1 : -1; + } + + return $qA > $qB ? -1 : 1; + }); + + $this->sorted = true; + } + } +} diff --git a/vendor/symfony/http-foundation/AcceptHeaderItem.php b/vendor/symfony/http-foundation/AcceptHeaderItem.php new file mode 100644 index 0000000..8b86eee --- /dev/null +++ b/vendor/symfony/http-foundation/AcceptHeaderItem.php @@ -0,0 +1,177 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * Represents an Accept-* header item. + * + * @author Jean-François Simon + */ +class AcceptHeaderItem +{ + private $value; + private $quality = 1.0; + private $index = 0; + private $attributes = []; + + public function __construct(string $value, array $attributes = []) + { + $this->value = $value; + foreach ($attributes as $name => $value) { + $this->setAttribute($name, $value); + } + } + + /** + * Builds an AcceptHeaderInstance instance from a string. + * + * @return self + */ + public static function fromString(?string $itemValue) + { + $parts = HeaderUtils::split($itemValue ?? '', ';='); + + $part = array_shift($parts); + $attributes = HeaderUtils::combine($parts); + + return new self($part[0], $attributes); + } + + /** + * Returns header value's string representation. + * + * @return string + */ + public function __toString() + { + $string = $this->value.($this->quality < 1 ? ';q='.$this->quality : ''); + if (\count($this->attributes) > 0) { + $string .= '; '.HeaderUtils::toString($this->attributes, ';'); + } + + return $string; + } + + /** + * Set the item value. + * + * @return $this + */ + public function setValue(string $value) + { + $this->value = $value; + + return $this; + } + + /** + * Returns the item value. + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * Set the item quality. + * + * @return $this + */ + public function setQuality(float $quality) + { + $this->quality = $quality; + + return $this; + } + + /** + * Returns the item quality. + * + * @return float + */ + public function getQuality() + { + return $this->quality; + } + + /** + * Set the item index. + * + * @return $this + */ + public function setIndex(int $index) + { + $this->index = $index; + + return $this; + } + + /** + * Returns the item index. + * + * @return int + */ + public function getIndex() + { + return $this->index; + } + + /** + * Tests if an attribute exists. + * + * @return bool + */ + public function hasAttribute(string $name) + { + return isset($this->attributes[$name]); + } + + /** + * Returns an attribute by its name. + * + * @param mixed $default + * + * @return mixed + */ + public function getAttribute(string $name, $default = null) + { + return $this->attributes[$name] ?? $default; + } + + /** + * Returns all attributes. + * + * @return array + */ + public function getAttributes() + { + return $this->attributes; + } + + /** + * Set an attribute. + * + * @return $this + */ + public function setAttribute(string $name, string $value) + { + if ('q' === $name) { + $this->quality = (float) $value; + } else { + $this->attributes[$name] = $value; + } + + return $this; + } +} diff --git a/vendor/symfony/http-foundation/BinaryFileResponse.php b/vendor/symfony/http-foundation/BinaryFileResponse.php new file mode 100644 index 0000000..ccfd638 --- /dev/null +++ b/vendor/symfony/http-foundation/BinaryFileResponse.php @@ -0,0 +1,418 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\File\Exception\FileException; +use Symfony\Component\HttpFoundation\File\File; + +/** + * BinaryFileResponse represents an HTTP response delivering a file. + * + * @author Niklas Fiekas + * @author stealth35 + * @author Igor Wiedler + * @author Jordan Alliot + * @author Sergey Linnik + */ +class BinaryFileResponse extends Response +{ + protected static $trustXSendfileTypeHeader = false; + + /** + * @var File + */ + protected $file; + protected $offset = 0; + protected $maxlen = -1; + protected $deleteFileAfterSend = false; + protected $chunkSize = 16 * 1024; + + /** + * @param \SplFileInfo|string $file The file to stream + * @param int $status The response status code + * @param array $headers An array of response headers + * @param bool $public Files are public by default + * @param string|null $contentDisposition The type of Content-Disposition to set automatically with the filename + * @param bool $autoEtag Whether the ETag header should be automatically set + * @param bool $autoLastModified Whether the Last-Modified header should be automatically set + */ + public function __construct($file, int $status = 200, array $headers = [], bool $public = true, ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true) + { + parent::__construct(null, $status, $headers); + + $this->setFile($file, $contentDisposition, $autoEtag, $autoLastModified); + + if ($public) { + $this->setPublic(); + } + } + + /** + * @param \SplFileInfo|string $file The file to stream + * @param int $status The response status code + * @param array $headers An array of response headers + * @param bool $public Files are public by default + * @param string|null $contentDisposition The type of Content-Disposition to set automatically with the filename + * @param bool $autoEtag Whether the ETag header should be automatically set + * @param bool $autoLastModified Whether the Last-Modified header should be automatically set + * + * @return static + * + * @deprecated since Symfony 5.2, use __construct() instead. + */ + public static function create($file = null, int $status = 200, array $headers = [], bool $public = true, ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true) + { + trigger_deprecation('symfony/http-foundation', '5.2', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class); + + return new static($file, $status, $headers, $public, $contentDisposition, $autoEtag, $autoLastModified); + } + + /** + * Sets the file to stream. + * + * @param \SplFileInfo|string $file The file to stream + * + * @return $this + * + * @throws FileException + */ + public function setFile($file, ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true) + { + if (!$file instanceof File) { + if ($file instanceof \SplFileInfo) { + $file = new File($file->getPathname()); + } else { + $file = new File((string) $file); + } + } + + if (!$file->isReadable()) { + throw new FileException('File must be readable.'); + } + + $this->file = $file; + + if ($autoEtag) { + $this->setAutoEtag(); + } + + if ($autoLastModified) { + $this->setAutoLastModified(); + } + + if ($contentDisposition) { + $this->setContentDisposition($contentDisposition); + } + + return $this; + } + + /** + * Gets the file. + * + * @return File + */ + public function getFile() + { + return $this->file; + } + + /** + * Sets the response stream chunk size. + * + * @return $this + */ + public function setChunkSize(int $chunkSize): self + { + if ($chunkSize < 1 || $chunkSize > \PHP_INT_MAX) { + throw new \LogicException('The chunk size of a BinaryFileResponse cannot be less than 1 or greater than PHP_INT_MAX.'); + } + + $this->chunkSize = $chunkSize; + + return $this; + } + + /** + * Automatically sets the Last-Modified header according the file modification date. + * + * @return $this + */ + public function setAutoLastModified() + { + $this->setLastModified(\DateTime::createFromFormat('U', $this->file->getMTime())); + + return $this; + } + + /** + * Automatically sets the ETag header according to the checksum of the file. + * + * @return $this + */ + public function setAutoEtag() + { + $this->setEtag(base64_encode(hash_file('sha256', $this->file->getPathname(), true))); + + return $this; + } + + /** + * Sets the Content-Disposition header with the given filename. + * + * @param string $disposition ResponseHeaderBag::DISPOSITION_INLINE or ResponseHeaderBag::DISPOSITION_ATTACHMENT + * @param string $filename Optionally use this UTF-8 encoded filename instead of the real name of the file + * @param string $filenameFallback A fallback filename, containing only ASCII characters. Defaults to an automatically encoded filename + * + * @return $this + */ + public function setContentDisposition(string $disposition, string $filename = '', string $filenameFallback = '') + { + if ('' === $filename) { + $filename = $this->file->getFilename(); + } + + if ('' === $filenameFallback && (!preg_match('/^[\x20-\x7e]*$/', $filename) || str_contains($filename, '%'))) { + $encoding = mb_detect_encoding($filename, null, true) ?: '8bit'; + + for ($i = 0, $filenameLength = mb_strlen($filename, $encoding); $i < $filenameLength; ++$i) { + $char = mb_substr($filename, $i, 1, $encoding); + + if ('%' === $char || \ord($char) < 32 || \ord($char) > 126) { + $filenameFallback .= '_'; + } else { + $filenameFallback .= $char; + } + } + } + + $dispositionHeader = $this->headers->makeDisposition($disposition, $filename, $filenameFallback); + $this->headers->set('Content-Disposition', $dispositionHeader); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function prepare(Request $request) + { + if ($this->isInformational() || $this->isEmpty()) { + parent::prepare($request); + + $this->maxlen = 0; + + return $this; + } + + if (!$this->headers->has('Content-Type')) { + $this->headers->set('Content-Type', $this->file->getMimeType() ?: 'application/octet-stream'); + } + + parent::prepare($request); + + $this->offset = 0; + $this->maxlen = -1; + + if (false === $fileSize = $this->file->getSize()) { + return $this; + } + $this->headers->remove('Transfer-Encoding'); + $this->headers->set('Content-Length', $fileSize); + + if (!$this->headers->has('Accept-Ranges')) { + // Only accept ranges on safe HTTP methods + $this->headers->set('Accept-Ranges', $request->isMethodSafe() ? 'bytes' : 'none'); + } + + if (self::$trustXSendfileTypeHeader && $request->headers->has('X-Sendfile-Type')) { + // Use X-Sendfile, do not send any content. + $type = $request->headers->get('X-Sendfile-Type'); + $path = $this->file->getRealPath(); + // Fall back to scheme://path for stream wrapped locations. + if (false === $path) { + $path = $this->file->getPathname(); + } + if ('x-accel-redirect' === strtolower($type)) { + // Do X-Accel-Mapping substitutions. + // @link https://github.com/rack/rack/blob/main/lib/rack/sendfile.rb + // @link https://mattbrictson.com/blog/accelerated-rails-downloads + if (!$request->headers->has('X-Accel-Mapping')) { + throw new \LogicException('The "X-Accel-Mapping" header must be set when "X-Sendfile-Type" is set to "X-Accel-Redirect".'); + } + $parts = HeaderUtils::split($request->headers->get('X-Accel-Mapping'), ',='); + foreach ($parts as $part) { + [$pathPrefix, $location] = $part; + if (substr($path, 0, \strlen($pathPrefix)) === $pathPrefix) { + $path = $location.substr($path, \strlen($pathPrefix)); + // Only set X-Accel-Redirect header if a valid URI can be produced + // as nginx does not serve arbitrary file paths. + $this->headers->set($type, $path); + $this->maxlen = 0; + break; + } + } + } else { + $this->headers->set($type, $path); + $this->maxlen = 0; + } + } elseif ($request->headers->has('Range') && $request->isMethod('GET')) { + // Process the range headers. + if (!$request->headers->has('If-Range') || $this->hasValidIfRangeHeader($request->headers->get('If-Range'))) { + $range = $request->headers->get('Range'); + + if (str_starts_with($range, 'bytes=')) { + [$start, $end] = explode('-', substr($range, 6), 2) + [1 => 0]; + + $end = ('' === $end) ? $fileSize - 1 : (int) $end; + + if ('' === $start) { + $start = $fileSize - $end; + $end = $fileSize - 1; + } else { + $start = (int) $start; + } + + if ($start <= $end) { + $end = min($end, $fileSize - 1); + if ($start < 0 || $start > $end) { + $this->setStatusCode(416); + $this->headers->set('Content-Range', sprintf('bytes */%s', $fileSize)); + } elseif ($end - $start < $fileSize - 1) { + $this->maxlen = $end < $fileSize ? $end - $start + 1 : -1; + $this->offset = $start; + + $this->setStatusCode(206); + $this->headers->set('Content-Range', sprintf('bytes %s-%s/%s', $start, $end, $fileSize)); + $this->headers->set('Content-Length', $end - $start + 1); + } + } + } + } + } + + if ($request->isMethod('HEAD')) { + $this->maxlen = 0; + } + + return $this; + } + + private function hasValidIfRangeHeader(?string $header): bool + { + if ($this->getEtag() === $header) { + return true; + } + + if (null === $lastModified = $this->getLastModified()) { + return false; + } + + return $lastModified->format('D, d M Y H:i:s').' GMT' === $header; + } + + /** + * {@inheritdoc} + */ + public function sendContent() + { + try { + if (!$this->isSuccessful()) { + return parent::sendContent(); + } + + if (0 === $this->maxlen) { + return $this; + } + + $out = fopen('php://output', 'w'); + $file = fopen($this->file->getPathname(), 'r'); + + ignore_user_abort(true); + + if (0 !== $this->offset) { + fseek($file, $this->offset); + } + + $length = $this->maxlen; + while ($length && !feof($file)) { + $read = $length > $this->chunkSize || 0 > $length ? $this->chunkSize : $length; + + if (false === $data = fread($file, $read)) { + break; + } + while ('' !== $data) { + $read = fwrite($out, $data); + if (false === $read || connection_aborted()) { + break 2; + } + if (0 < $length) { + $length -= $read; + } + $data = substr($data, $read); + } + } + + fclose($out); + fclose($file); + } finally { + if ($this->deleteFileAfterSend && is_file($this->file->getPathname())) { + unlink($this->file->getPathname()); + } + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @throws \LogicException when the content is not null + */ + public function setContent(?string $content) + { + if (null !== $content) { + throw new \LogicException('The content cannot be set on a BinaryFileResponse instance.'); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getContent() + { + return false; + } + + /** + * Trust X-Sendfile-Type header. + */ + public static function trustXSendfileTypeHeader() + { + self::$trustXSendfileTypeHeader = true; + } + + /** + * If this is set to true, the file will be unlinked after the request is sent + * Note: If the X-Sendfile header is used, the deleteFileAfterSend setting will not be used. + * + * @return $this + */ + public function deleteFileAfterSend(bool $shouldDelete = true) + { + $this->deleteFileAfterSend = $shouldDelete; + + return $this; + } +} diff --git a/vendor/symfony/http-foundation/CHANGELOG.md b/vendor/symfony/http-foundation/CHANGELOG.md new file mode 100644 index 0000000..ad7607a --- /dev/null +++ b/vendor/symfony/http-foundation/CHANGELOG.md @@ -0,0 +1,296 @@ +CHANGELOG +========= + +5.4 +--- + + * Deprecate passing `null` as `$requestIp` to `IpUtils::__checkIp()`, `IpUtils::__checkIp4()` or `IpUtils::__checkIp6()`, pass an empty string instead. + * Add the `litespeed_finish_request` method to work with Litespeed + * Deprecate `upload_progress.*` and `url_rewriter.tags` session options + * Allow setting session options via DSN + +5.3 +--- + + * Add the `SessionFactory`, `NativeSessionStorageFactory`, `PhpBridgeSessionStorageFactory` and `MockFileSessionStorageFactory` classes + * Calling `Request::getSession()` when there is no available session throws a `SessionNotFoundException` + * Add the `RequestStack::getSession` method + * Deprecate the `NamespacedAttributeBag` class + * Add `ResponseFormatSame` PHPUnit constraint + * Deprecate the `RequestStack::getMasterRequest()` method and add `getMainRequest()` as replacement + +5.2.0 +----- + + * added support for `X-Forwarded-Prefix` header + * added `HeaderUtils::parseQuery()`: it does the same as `parse_str()` but preserves dots in variable names + * added `File::getContent()` + * added ability to use comma separated ip addresses for `RequestMatcher::matchIps()` + * added `Request::toArray()` to parse a JSON request body to an array + * added `RateLimiter\RequestRateLimiterInterface` and `RateLimiter\AbstractRequestRateLimiter` + * deprecated not passing a `Closure` together with `FILTER_CALLBACK` to `ParameterBag::filter()`; wrap your filter in a closure instead. + * Deprecated the `Request::HEADER_X_FORWARDED_ALL` constant, use either `HEADER_X_FORWARDED_FOR | HEADER_X_FORWARDED_HOST | HEADER_X_FORWARDED_PORT | HEADER_X_FORWARDED_PROTO` or `HEADER_X_FORWARDED_AWS_ELB` or `HEADER_X_FORWARDED_TRAEFIK` constants instead. + * Deprecated `BinaryFileResponse::create()`, use `__construct()` instead + +5.1.0 +----- + + * added `Cookie::withValue`, `Cookie::withDomain`, `Cookie::withExpires`, + `Cookie::withPath`, `Cookie::withSecure`, `Cookie::withHttpOnly`, + `Cookie::withRaw`, `Cookie::withSameSite` + * Deprecate `Response::create()`, `JsonResponse::create()`, + `RedirectResponse::create()`, and `StreamedResponse::create()` methods (use + `__construct()` instead) + * added `Request::preferSafeContent()` and `Response::setContentSafe()` to handle "safe" HTTP preference + according to [RFC 8674](https://tools.ietf.org/html/rfc8674) + * made the Mime component an optional dependency + * added `MarshallingSessionHandler`, `IdentityMarshaller` + * made `Session` accept a callback to report when the session is being used + * Add support for all core cache control directives + * Added `Symfony\Component\HttpFoundation\InputBag` + * Deprecated retrieving non-string values using `InputBag::get()`, use `InputBag::all()` if you need access to the collection of values + +5.0.0 +----- + + * made `Cookie` auto-secure and lax by default + * removed classes in the `MimeType` namespace, use the Symfony Mime component instead + * removed method `UploadedFile::getClientSize()` and the related constructor argument + * made `Request::getSession()` throw if the session has not been set before + * removed `Response::HTTP_RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL` + * passing a null url when instantiating a `RedirectResponse` is not allowed + +4.4.0 +----- + + * passing arguments to `Request::isMethodSafe()` is deprecated. + * `ApacheRequest` is deprecated, use the `Request` class instead. + * passing a third argument to `HeaderBag::get()` is deprecated, use method `all()` instead + * [BC BREAK] `PdoSessionHandler` with MySQL changed the type of the lifetime column, + make sure to run `ALTER TABLE sessions MODIFY sess_lifetime INTEGER UNSIGNED NOT NULL` to + update your database. + * `PdoSessionHandler` now precalculates the expiry timestamp in the lifetime column, + make sure to run `CREATE INDEX EXPIRY ON sessions (sess_lifetime)` to update your database + to speed up garbage collection of expired sessions. + * added `SessionHandlerFactory` to create session handlers with a DSN + * added `IpUtils::anonymize()` to help with GDPR compliance. + +4.3.0 +----- + + * added PHPUnit constraints: `RequestAttributeValueSame`, `ResponseCookieValueSame`, `ResponseHasCookie`, + `ResponseHasHeader`, `ResponseHeaderSame`, `ResponseIsRedirected`, `ResponseIsSuccessful`, and `ResponseStatusCodeSame` + * deprecated `MimeTypeGuesserInterface` and `ExtensionGuesserInterface` in favor of `Symfony\Component\Mime\MimeTypesInterface`. + * deprecated `MimeType` and `MimeTypeExtensionGuesser` in favor of `Symfony\Component\Mime\MimeTypes`. + * deprecated `FileBinaryMimeTypeGuesser` in favor of `Symfony\Component\Mime\FileBinaryMimeTypeGuesser`. + * deprecated `FileinfoMimeTypeGuesser` in favor of `Symfony\Component\Mime\FileinfoMimeTypeGuesser`. + * added `UrlHelper` that allows to get an absolute URL and a relative path for a given path + +4.2.0 +----- + + * the default value of the "$secure" and "$samesite" arguments of Cookie's constructor + will respectively change from "false" to "null" and from "null" to "lax" in Symfony + 5.0, you should define their values explicitly or use "Cookie::create()" instead. + * added `matchPort()` in RequestMatcher + +4.1.3 +----- + + * [BC BREAK] Support for the IIS-only `X_ORIGINAL_URL` and `X_REWRITE_URL` + HTTP headers has been dropped for security reasons. + +4.1.0 +----- + + * Query string normalization uses `parse_str()` instead of custom parsing logic. + * Passing the file size to the constructor of the `UploadedFile` class is deprecated. + * The `getClientSize()` method of the `UploadedFile` class is deprecated. Use `getSize()` instead. + * added `RedisSessionHandler` to use Redis as a session storage + * The `get()` method of the `AcceptHeader` class now takes into account the + `*` and `*/*` default values (if they are present in the Accept HTTP header) + when looking for items. + * deprecated `Request::getSession()` when no session has been set. Use `Request::hasSession()` instead. + * added `CannotWriteFileException`, `ExtensionFileException`, `FormSizeFileException`, + `IniSizeFileException`, `NoFileException`, `NoTmpDirFileException`, `PartialFileException` to + handle failed `UploadedFile`. + * added `MigratingSessionHandler` for migrating between two session handlers without losing sessions + * added `HeaderUtils`. + +4.0.0 +----- + + * the `Request::setTrustedHeaderName()` and `Request::getTrustedHeaderName()` + methods have been removed + * the `Request::HEADER_CLIENT_IP` constant has been removed, use + `Request::HEADER_X_FORWARDED_FOR` instead + * the `Request::HEADER_CLIENT_HOST` constant has been removed, use + `Request::HEADER_X_FORWARDED_HOST` instead + * the `Request::HEADER_CLIENT_PROTO` constant has been removed, use + `Request::HEADER_X_FORWARDED_PROTO` instead + * the `Request::HEADER_CLIENT_PORT` constant has been removed, use + `Request::HEADER_X_FORWARDED_PORT` instead + * checking for cacheable HTTP methods using the `Request::isMethodSafe()` + method (by not passing `false` as its argument) is not supported anymore and + throws a `\BadMethodCallException` + * the `WriteCheckSessionHandler`, `NativeSessionHandler` and `NativeProxy` classes have been removed + * setting session save handlers that do not implement `\SessionHandlerInterface` in + `NativeSessionStorage::setSaveHandler()` is not supported anymore and throws a + `\TypeError` + +3.4.0 +----- + + * implemented PHP 7.0's `SessionUpdateTimestampHandlerInterface` with a new + `AbstractSessionHandler` base class and a new `StrictSessionHandler` wrapper + * deprecated the `WriteCheckSessionHandler`, `NativeSessionHandler` and `NativeProxy` classes + * deprecated setting session save handlers that do not implement `\SessionHandlerInterface` in `NativeSessionStorage::setSaveHandler()` + * deprecated using `MongoDbSessionHandler` with the legacy mongo extension; use it with the mongodb/mongodb package and ext-mongodb instead + * deprecated `MemcacheSessionHandler`; use `MemcachedSessionHandler` instead + +3.3.0 +----- + + * the `Request::setTrustedProxies()` method takes a new `$trustedHeaderSet` argument, + see https://symfony.com/doc/current/deployment/proxies.html for more info, + * deprecated the `Request::setTrustedHeaderName()` and `Request::getTrustedHeaderName()` methods, + * added `File\Stream`, to be passed to `BinaryFileResponse` when the size of the served file is unknown, + disabling `Range` and `Content-Length` handling, switching to chunked encoding instead + * added the `Cookie::fromString()` method that allows to create a cookie from a + raw header string + +3.1.0 +----- + + * Added support for creating `JsonResponse` with a string of JSON data + +3.0.0 +----- + + * The precedence of parameters returned from `Request::get()` changed from "GET, PATH, BODY" to "PATH, GET, BODY" + +2.8.0 +----- + + * Finding deep items in `ParameterBag::get()` is deprecated since version 2.8 and + will be removed in 3.0. + +2.6.0 +----- + + * PdoSessionHandler changes + - implemented different session locking strategies to prevent loss of data by concurrent access to the same session + - [BC BREAK] save session data in a binary column without base64_encode + - [BC BREAK] added lifetime column to the session table which allows to have different lifetimes for each session + - implemented lazy connections that are only opened when a session is used by either passing a dsn string + explicitly or falling back to session.save_path ini setting + - added a createTable method that initializes a correctly defined table depending on the database vendor + +2.5.0 +----- + + * added `JsonResponse::setEncodingOptions()` & `JsonResponse::getEncodingOptions()` for easier manipulation + of the options used while encoding data to JSON format. + +2.4.0 +----- + + * added RequestStack + * added Request::getEncodings() + * added accessors methods to session handlers + +2.3.0 +----- + + * added support for ranges of IPs in trusted proxies + * `UploadedFile::isValid` now returns false if the file was not uploaded via HTTP (in a non-test mode) + * Improved error-handling of `\Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler` + to ensure the supplied PDO handler throws Exceptions on error (as the class expects). Added related test cases + to verify that Exceptions are properly thrown when the PDO queries fail. + +2.2.0 +----- + + * fixed the Request::create() precedence (URI information always take precedence now) + * added Request::getTrustedProxies() + * deprecated Request::isProxyTrusted() + * [BC BREAK] JsonResponse does not turn a top level empty array to an object anymore, use an ArrayObject to enforce objects + * added a IpUtils class to check if an IP belongs to a CIDR + * added Request::getRealMethod() to get the "real" HTTP method (getMethod() returns the "intended" HTTP method) + * disabled _method request parameter support by default (call Request::enableHttpMethodParameterOverride() to + enable it, and Request::getHttpMethodParameterOverride() to check if it is supported) + * Request::splitHttpAcceptHeader() method is deprecated and will be removed in 2.3 + * Deprecated Flashbag::count() and \Countable interface, will be removed in 2.3 + +2.1.0 +----- + + * added Request::getSchemeAndHttpHost() and Request::getUserInfo() + * added a fluent interface to the Response class + * added Request::isProxyTrusted() + * added JsonResponse + * added a getTargetUrl method to RedirectResponse + * added support for streamed responses + * made Response::prepare() method the place to enforce HTTP specification + * [BC BREAK] moved management of the locale from the Session class to the Request class + * added a generic access to the PHP built-in filter mechanism: ParameterBag::filter() + * made FileBinaryMimeTypeGuesser command configurable + * added Request::getUser() and Request::getPassword() + * added support for the PATCH method in Request + * removed the ContentTypeMimeTypeGuesser class as it is deprecated and never used on PHP 5.3 + * added ResponseHeaderBag::makeDisposition() (implements RFC 6266) + * made mimetype to extension conversion configurable + * [BC BREAK] Moved all session related classes and interfaces into own namespace, as + `Symfony\Component\HttpFoundation\Session` and renamed classes accordingly. + Session handlers are located in the subnamespace `Symfony\Component\HttpFoundation\Session\Handler`. + * SessionHandlers must implement `\SessionHandlerInterface` or extend from the + `Symfony\Component\HttpFoundation\Storage\Handler\NativeSessionHandler` base class. + * Added internal storage driver proxy mechanism for forward compatibility with + PHP 5.4 `\SessionHandler` class. + * Added session handlers for custom Memcache, Memcached and Null session save handlers. + * [BC BREAK] Removed `NativeSessionStorage` and replaced with `NativeFileSessionHandler`. + * [BC BREAK] `SessionStorageInterface` methods removed: `write()`, `read()` and + `remove()`. Added `getBag()`, `registerBag()`. The `NativeSessionStorage` class + is a mediator for the session storage internals including the session handlers + which do the real work of participating in the internal PHP session workflow. + * [BC BREAK] Introduced mock implementations of `SessionStorage` to enable unit + and functional testing without starting real PHP sessions. Removed + `ArraySessionStorage`, and replaced with `MockArraySessionStorage` for unit + tests; removed `FilesystemSessionStorage`, and replaced with`MockFileSessionStorage` + for functional tests. These do not interact with global session ini + configuration values, session functions or `$_SESSION` superglobal. This means + they can be configured directly allowing multiple instances to work without + conflicting in the same PHP process. + * [BC BREAK] Removed the `close()` method from the `Session` class, as this is + now redundant. + * Deprecated the following methods from the Session class: `setFlash()`, `setFlashes()` + `getFlash()`, `hasFlash()`, and `removeFlash()`. Use `getFlashBag()` instead + which returns a `FlashBagInterface`. + * `Session->clear()` now only clears session attributes as before it cleared + flash messages and attributes. `Session->getFlashBag()->all()` clears flashes now. + * Session data is now managed by `SessionBagInterface` to better encapsulate + session data. + * Refactored session attribute and flash messages system to their own + `SessionBagInterface` implementations. + * Added `FlashBag`. Flashes expire when retrieved by `get()` or `all()`. This + implementation is ESI compatible. + * Added `AutoExpireFlashBag` (default) to replicate Symfony 2.0.x auto expire + behavior of messages auto expiring after one page page load. Messages must + be retrieved by `get()` or `all()`. + * Added `Symfony\Component\HttpFoundation\Attribute\AttributeBag` to replicate + attributes storage behavior from 2.0.x (default). + * Added `Symfony\Component\HttpFoundation\Attribute\NamespacedAttributeBag` for + namespace session attributes. + * Flash API can stores messages in an array so there may be multiple messages + per flash type. The old `Session` class API remains without BC break as it + will allow single messages as before. + * Added basic session meta-data to the session to record session create time, + last updated time, and the lifetime of the session cookie that was provided + to the client. + * Request::getClientIp() method doesn't take a parameter anymore but bases + itself on the trustProxy parameter. + * Added isMethod() to Request object. + * [BC BREAK] The methods `getPathInfo()`, `getBaseUrl()` and `getBasePath()` of + a `Request` now all return a raw value (vs a urldecoded value before). Any call + to one of these methods must be checked and wrapped in a `rawurldecode()` if + needed. diff --git a/vendor/symfony/http-foundation/Cookie.php b/vendor/symfony/http-foundation/Cookie.php new file mode 100644 index 0000000..3ff93b9 --- /dev/null +++ b/vendor/symfony/http-foundation/Cookie.php @@ -0,0 +1,422 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * Represents a cookie. + * + * @author Johannes M. Schmitt + */ +class Cookie +{ + public const SAMESITE_NONE = 'none'; + public const SAMESITE_LAX = 'lax'; + public const SAMESITE_STRICT = 'strict'; + + protected $name; + protected $value; + protected $domain; + protected $expire; + protected $path; + protected $secure; + protected $httpOnly; + + private $raw; + private $sameSite; + private $secureDefault = false; + + private const RESERVED_CHARS_LIST = "=,; \t\r\n\v\f"; + private const RESERVED_CHARS_FROM = ['=', ',', ';', ' ', "\t", "\r", "\n", "\v", "\f"]; + private const RESERVED_CHARS_TO = ['%3D', '%2C', '%3B', '%20', '%09', '%0D', '%0A', '%0B', '%0C']; + + /** + * Creates cookie from raw header string. + * + * @return static + */ + public static function fromString(string $cookie, bool $decode = false) + { + $data = [ + 'expires' => 0, + 'path' => '/', + 'domain' => null, + 'secure' => false, + 'httponly' => false, + 'raw' => !$decode, + 'samesite' => null, + ]; + + $parts = HeaderUtils::split($cookie, ';='); + $part = array_shift($parts); + + $name = $decode ? urldecode($part[0]) : $part[0]; + $value = isset($part[1]) ? ($decode ? urldecode($part[1]) : $part[1]) : null; + + $data = HeaderUtils::combine($parts) + $data; + $data['expires'] = self::expiresTimestamp($data['expires']); + + if (isset($data['max-age']) && ($data['max-age'] > 0 || $data['expires'] > time())) { + $data['expires'] = time() + (int) $data['max-age']; + } + + return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']); + } + + public static function create(string $name, ?string $value = null, $expire = 0, ?string $path = '/', ?string $domain = null, ?bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX): self + { + return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite); + } + + /** + * @param string $name The name of the cookie + * @param string|null $value The value of the cookie + * @param int|string|\DateTimeInterface $expire The time the cookie expires + * @param string|null $path The path on the server in which the cookie will be available on + * @param string|null $domain The domain that the cookie is available to + * @param bool|null $secure Whether the client should send back the cookie only over HTTPS or null to auto-enable this when the request is already using HTTPS + * @param bool $httpOnly Whether the cookie will be made accessible only through the HTTP protocol + * @param bool $raw Whether the cookie value should be sent with no url encoding + * @param string|null $sameSite Whether the cookie will be available for cross-site requests + * + * @throws \InvalidArgumentException + */ + public function __construct(string $name, ?string $value = null, $expire = 0, ?string $path = '/', ?string $domain = null, ?bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = 'lax') + { + // from PHP source code + if ($raw && false !== strpbrk($name, self::RESERVED_CHARS_LIST)) { + throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name)); + } + + if (empty($name)) { + throw new \InvalidArgumentException('The cookie name cannot be empty.'); + } + + $this->name = $name; + $this->value = $value; + $this->domain = $domain; + $this->expire = self::expiresTimestamp($expire); + $this->path = empty($path) ? '/' : $path; + $this->secure = $secure; + $this->httpOnly = $httpOnly; + $this->raw = $raw; + $this->sameSite = $this->withSameSite($sameSite)->sameSite; + } + + /** + * Creates a cookie copy with a new value. + * + * @return static + */ + public function withValue(?string $value): self + { + $cookie = clone $this; + $cookie->value = $value; + + return $cookie; + } + + /** + * Creates a cookie copy with a new domain that the cookie is available to. + * + * @return static + */ + public function withDomain(?string $domain): self + { + $cookie = clone $this; + $cookie->domain = $domain; + + return $cookie; + } + + /** + * Creates a cookie copy with a new time the cookie expires. + * + * @param int|string|\DateTimeInterface $expire + * + * @return static + */ + public function withExpires($expire = 0): self + { + $cookie = clone $this; + $cookie->expire = self::expiresTimestamp($expire); + + return $cookie; + } + + /** + * Converts expires formats to a unix timestamp. + * + * @param int|string|\DateTimeInterface $expire + */ + private static function expiresTimestamp($expire = 0): int + { + // convert expiration time to a Unix timestamp + if ($expire instanceof \DateTimeInterface) { + $expire = $expire->format('U'); + } elseif (!is_numeric($expire)) { + $expire = strtotime($expire); + + if (false === $expire) { + throw new \InvalidArgumentException('The cookie expiration time is not valid.'); + } + } + + return 0 < $expire ? (int) $expire : 0; + } + + /** + * Creates a cookie copy with a new path on the server in which the cookie will be available on. + * + * @return static + */ + public function withPath(string $path): self + { + $cookie = clone $this; + $cookie->path = '' === $path ? '/' : $path; + + return $cookie; + } + + /** + * Creates a cookie copy that only be transmitted over a secure HTTPS connection from the client. + * + * @return static + */ + public function withSecure(bool $secure = true): self + { + $cookie = clone $this; + $cookie->secure = $secure; + + return $cookie; + } + + /** + * Creates a cookie copy that be accessible only through the HTTP protocol. + * + * @return static + */ + public function withHttpOnly(bool $httpOnly = true): self + { + $cookie = clone $this; + $cookie->httpOnly = $httpOnly; + + return $cookie; + } + + /** + * Creates a cookie copy that uses no url encoding. + * + * @return static + */ + public function withRaw(bool $raw = true): self + { + if ($raw && false !== strpbrk($this->name, self::RESERVED_CHARS_LIST)) { + throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $this->name)); + } + + $cookie = clone $this; + $cookie->raw = $raw; + + return $cookie; + } + + /** + * Creates a cookie copy with SameSite attribute. + * + * @return static + */ + public function withSameSite(?string $sameSite): self + { + if ('' === $sameSite) { + $sameSite = null; + } elseif (null !== $sameSite) { + $sameSite = strtolower($sameSite); + } + + if (!\in_array($sameSite, [self::SAMESITE_LAX, self::SAMESITE_STRICT, self::SAMESITE_NONE, null], true)) { + throw new \InvalidArgumentException('The "sameSite" parameter value is not valid.'); + } + + $cookie = clone $this; + $cookie->sameSite = $sameSite; + + return $cookie; + } + + /** + * Returns the cookie as a string. + * + * @return string + */ + public function __toString() + { + if ($this->isRaw()) { + $str = $this->getName(); + } else { + $str = str_replace(self::RESERVED_CHARS_FROM, self::RESERVED_CHARS_TO, $this->getName()); + } + + $str .= '='; + + if ('' === (string) $this->getValue()) { + $str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0'; + } else { + $str .= $this->isRaw() ? $this->getValue() : rawurlencode($this->getValue()); + + if (0 !== $this->getExpiresTime()) { + $str .= '; expires='.gmdate('D, d-M-Y H:i:s T', $this->getExpiresTime()).'; Max-Age='.$this->getMaxAge(); + } + } + + if ($this->getPath()) { + $str .= '; path='.$this->getPath(); + } + + if ($this->getDomain()) { + $str .= '; domain='.$this->getDomain(); + } + + if (true === $this->isSecure()) { + $str .= '; secure'; + } + + if (true === $this->isHttpOnly()) { + $str .= '; httponly'; + } + + if (null !== $this->getSameSite()) { + $str .= '; samesite='.$this->getSameSite(); + } + + return $str; + } + + /** + * Gets the name of the cookie. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Gets the value of the cookie. + * + * @return string|null + */ + public function getValue() + { + return $this->value; + } + + /** + * Gets the domain that the cookie is available to. + * + * @return string|null + */ + public function getDomain() + { + return $this->domain; + } + + /** + * Gets the time the cookie expires. + * + * @return int + */ + public function getExpiresTime() + { + return $this->expire; + } + + /** + * Gets the max-age attribute. + * + * @return int + */ + public function getMaxAge() + { + $maxAge = $this->expire - time(); + + return 0 >= $maxAge ? 0 : $maxAge; + } + + /** + * Gets the path on the server in which the cookie will be available on. + * + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * Checks whether the cookie should only be transmitted over a secure HTTPS connection from the client. + * + * @return bool + */ + public function isSecure() + { + return $this->secure ?? $this->secureDefault; + } + + /** + * Checks whether the cookie will be made accessible only through the HTTP protocol. + * + * @return bool + */ + public function isHttpOnly() + { + return $this->httpOnly; + } + + /** + * Whether this cookie is about to be cleared. + * + * @return bool + */ + public function isCleared() + { + return 0 !== $this->expire && $this->expire < time(); + } + + /** + * Checks if the cookie value should be sent with no url encoding. + * + * @return bool + */ + public function isRaw() + { + return $this->raw; + } + + /** + * Gets the SameSite attribute. + * + * @return string|null + */ + public function getSameSite() + { + return $this->sameSite; + } + + /** + * @param bool $default The default value of the "secure" flag when it is set to null + */ + public function setSecureDefault(bool $default): void + { + $this->secureDefault = $default; + } +} diff --git a/vendor/symfony/http-foundation/Exception/BadRequestException.php b/vendor/symfony/http-foundation/Exception/BadRequestException.php new file mode 100644 index 0000000..e4bb309 --- /dev/null +++ b/vendor/symfony/http-foundation/Exception/BadRequestException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * Raised when a user sends a malformed request. + */ +class BadRequestException extends \UnexpectedValueException implements RequestExceptionInterface +{ +} diff --git a/vendor/symfony/http-foundation/Exception/ConflictingHeadersException.php b/vendor/symfony/http-foundation/Exception/ConflictingHeadersException.php new file mode 100644 index 0000000..5fcf5b4 --- /dev/null +++ b/vendor/symfony/http-foundation/Exception/ConflictingHeadersException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * The HTTP request contains headers with conflicting information. + * + * @author Magnus Nordlander + */ +class ConflictingHeadersException extends \UnexpectedValueException implements RequestExceptionInterface +{ +} diff --git a/vendor/symfony/http-foundation/Exception/JsonException.php b/vendor/symfony/http-foundation/Exception/JsonException.php new file mode 100644 index 0000000..5990e76 --- /dev/null +++ b/vendor/symfony/http-foundation/Exception/JsonException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * Thrown by Request::toArray() when the content cannot be JSON-decoded. + * + * @author Tobias Nyholm + */ +final class JsonException extends \UnexpectedValueException implements RequestExceptionInterface +{ +} diff --git a/vendor/symfony/http-foundation/Exception/RequestExceptionInterface.php b/vendor/symfony/http-foundation/Exception/RequestExceptionInterface.php new file mode 100644 index 0000000..478d0dc --- /dev/null +++ b/vendor/symfony/http-foundation/Exception/RequestExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * Interface for Request exceptions. + * + * Exceptions implementing this interface should trigger an HTTP 400 response in the application code. + */ +interface RequestExceptionInterface +{ +} diff --git a/vendor/symfony/http-foundation/Exception/SessionNotFoundException.php b/vendor/symfony/http-foundation/Exception/SessionNotFoundException.php new file mode 100644 index 0000000..80a21bf --- /dev/null +++ b/vendor/symfony/http-foundation/Exception/SessionNotFoundException.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * Raised when a session does not exist. This happens in the following cases: + * - the session is not enabled + * - attempt to read a session outside a request context (ie. cli script). + * + * @author Jérémy Derussé + */ +class SessionNotFoundException extends \LogicException implements RequestExceptionInterface +{ + public function __construct(string $message = 'There is currently no session available.', int $code = 0, ?\Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } +} diff --git a/vendor/symfony/http-foundation/Exception/SuspiciousOperationException.php b/vendor/symfony/http-foundation/Exception/SuspiciousOperationException.php new file mode 100644 index 0000000..ae7a5f1 --- /dev/null +++ b/vendor/symfony/http-foundation/Exception/SuspiciousOperationException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * Raised when a user has performed an operation that should be considered + * suspicious from a security perspective. + */ +class SuspiciousOperationException extends \UnexpectedValueException implements RequestExceptionInterface +{ +} diff --git a/vendor/symfony/http-foundation/ExpressionRequestMatcher.php b/vendor/symfony/http-foundation/ExpressionRequestMatcher.php new file mode 100644 index 0000000..26bed7d --- /dev/null +++ b/vendor/symfony/http-foundation/ExpressionRequestMatcher.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + +/** + * ExpressionRequestMatcher uses an expression to match a Request. + * + * @author Fabien Potencier + */ +class ExpressionRequestMatcher extends RequestMatcher +{ + private $language; + private $expression; + + public function setExpression(ExpressionLanguage $language, $expression) + { + $this->language = $language; + $this->expression = $expression; + } + + public function matches(Request $request) + { + if (!$this->language) { + throw new \LogicException('Unable to match the request as the expression language is not available.'); + } + + return $this->language->evaluate($this->expression, [ + 'request' => $request, + 'method' => $request->getMethod(), + 'path' => rawurldecode($request->getPathInfo()), + 'host' => $request->getHost(), + 'ip' => $request->getClientIp(), + 'attributes' => $request->attributes->all(), + ]) && parent::matches($request); + } +} diff --git a/vendor/symfony/http-foundation/File/Exception/AccessDeniedException.php b/vendor/symfony/http-foundation/File/Exception/AccessDeniedException.php new file mode 100644 index 0000000..136d2a9 --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/AccessDeniedException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when the access on a file was denied. + * + * @author Bernhard Schussek + */ +class AccessDeniedException extends FileException +{ + public function __construct(string $path) + { + parent::__construct(sprintf('The file %s could not be accessed', $path)); + } +} diff --git a/vendor/symfony/http-foundation/File/Exception/CannotWriteFileException.php b/vendor/symfony/http-foundation/File/Exception/CannotWriteFileException.php new file mode 100644 index 0000000..c49f53a --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/CannotWriteFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_CANT_WRITE error occurred with UploadedFile. + * + * @author Florent Mata + */ +class CannotWriteFileException extends FileException +{ +} diff --git a/vendor/symfony/http-foundation/File/Exception/ExtensionFileException.php b/vendor/symfony/http-foundation/File/Exception/ExtensionFileException.php new file mode 100644 index 0000000..ed83499 --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/ExtensionFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_EXTENSION error occurred with UploadedFile. + * + * @author Florent Mata + */ +class ExtensionFileException extends FileException +{ +} diff --git a/vendor/symfony/http-foundation/File/Exception/FileException.php b/vendor/symfony/http-foundation/File/Exception/FileException.php new file mode 100644 index 0000000..fad5133 --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/FileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an error occurred in the component File. + * + * @author Bernhard Schussek + */ +class FileException extends \RuntimeException +{ +} diff --git a/vendor/symfony/http-foundation/File/Exception/FileNotFoundException.php b/vendor/symfony/http-foundation/File/Exception/FileNotFoundException.php new file mode 100644 index 0000000..31bdf68 --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/FileNotFoundException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when a file was not found. + * + * @author Bernhard Schussek + */ +class FileNotFoundException extends FileException +{ + public function __construct(string $path) + { + parent::__construct(sprintf('The file "%s" does not exist', $path)); + } +} diff --git a/vendor/symfony/http-foundation/File/Exception/FormSizeFileException.php b/vendor/symfony/http-foundation/File/Exception/FormSizeFileException.php new file mode 100644 index 0000000..8741be0 --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/FormSizeFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_FORM_SIZE error occurred with UploadedFile. + * + * @author Florent Mata + */ +class FormSizeFileException extends FileException +{ +} diff --git a/vendor/symfony/http-foundation/File/Exception/IniSizeFileException.php b/vendor/symfony/http-foundation/File/Exception/IniSizeFileException.php new file mode 100644 index 0000000..c8fde61 --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/IniSizeFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_INI_SIZE error occurred with UploadedFile. + * + * @author Florent Mata + */ +class IniSizeFileException extends FileException +{ +} diff --git a/vendor/symfony/http-foundation/File/Exception/NoFileException.php b/vendor/symfony/http-foundation/File/Exception/NoFileException.php new file mode 100644 index 0000000..4b48cc7 --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/NoFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_NO_FILE error occurred with UploadedFile. + * + * @author Florent Mata + */ +class NoFileException extends FileException +{ +} diff --git a/vendor/symfony/http-foundation/File/Exception/NoTmpDirFileException.php b/vendor/symfony/http-foundation/File/Exception/NoTmpDirFileException.php new file mode 100644 index 0000000..bdead2d --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/NoTmpDirFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_NO_TMP_DIR error occurred with UploadedFile. + * + * @author Florent Mata + */ +class NoTmpDirFileException extends FileException +{ +} diff --git a/vendor/symfony/http-foundation/File/Exception/PartialFileException.php b/vendor/symfony/http-foundation/File/Exception/PartialFileException.php new file mode 100644 index 0000000..4641efb --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/PartialFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_PARTIAL error occurred with UploadedFile. + * + * @author Florent Mata + */ +class PartialFileException extends FileException +{ +} diff --git a/vendor/symfony/http-foundation/File/Exception/UnexpectedTypeException.php b/vendor/symfony/http-foundation/File/Exception/UnexpectedTypeException.php new file mode 100644 index 0000000..8533f99 --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/UnexpectedTypeException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +class UnexpectedTypeException extends FileException +{ + public function __construct($value, string $expectedType) + { + parent::__construct(sprintf('Expected argument of type %s, %s given', $expectedType, get_debug_type($value))); + } +} diff --git a/vendor/symfony/http-foundation/File/Exception/UploadException.php b/vendor/symfony/http-foundation/File/Exception/UploadException.php new file mode 100644 index 0000000..7074e76 --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/UploadException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an error occurred during file upload. + * + * @author Bernhard Schussek + */ +class UploadException extends FileException +{ +} diff --git a/vendor/symfony/http-foundation/File/File.php b/vendor/symfony/http-foundation/File/File.php new file mode 100644 index 0000000..2deb53d --- /dev/null +++ b/vendor/symfony/http-foundation/File/File.php @@ -0,0 +1,152 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File; + +use Symfony\Component\HttpFoundation\File\Exception\FileException; +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; +use Symfony\Component\Mime\MimeTypes; + +/** + * A file in the file system. + * + * @author Bernhard Schussek + */ +class File extends \SplFileInfo +{ + /** + * Constructs a new file from the given path. + * + * @param string $path The path to the file + * @param bool $checkPath Whether to check the path or not + * + * @throws FileNotFoundException If the given path is not a file + */ + public function __construct(string $path, bool $checkPath = true) + { + if ($checkPath && !is_file($path)) { + throw new FileNotFoundException($path); + } + + parent::__construct($path); + } + + /** + * Returns the extension based on the mime type. + * + * If the mime type is unknown, returns null. + * + * This method uses the mime type as guessed by getMimeType() + * to guess the file extension. + * + * @return string|null + * + * @see MimeTypes + * @see getMimeType() + */ + public function guessExtension() + { + if (!class_exists(MimeTypes::class)) { + throw new \LogicException('You cannot guess the extension as the Mime component is not installed. Try running "composer require symfony/mime".'); + } + + return MimeTypes::getDefault()->getExtensions($this->getMimeType())[0] ?? null; + } + + /** + * Returns the mime type of the file. + * + * The mime type is guessed using a MimeTypeGuesserInterface instance, + * which uses finfo_file() then the "file" system binary, + * depending on which of those are available. + * + * @return string|null + * + * @see MimeTypes + */ + public function getMimeType() + { + if (!class_exists(MimeTypes::class)) { + throw new \LogicException('You cannot guess the mime type as the Mime component is not installed. Try running "composer require symfony/mime".'); + } + + return MimeTypes::getDefault()->guessMimeType($this->getPathname()); + } + + /** + * Moves the file to a new location. + * + * @return self + * + * @throws FileException if the target file could not be created + */ + public function move(string $directory, ?string $name = null) + { + $target = $this->getTargetFile($directory, $name); + + set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); + try { + $renamed = rename($this->getPathname(), $target); + } finally { + restore_error_handler(); + } + if (!$renamed) { + throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error))); + } + + @chmod($target, 0666 & ~umask()); + + return $target; + } + + public function getContent(): string + { + $content = file_get_contents($this->getPathname()); + + if (false === $content) { + throw new FileException(sprintf('Could not get the content of the file "%s".', $this->getPathname())); + } + + return $content; + } + + /** + * @return self + */ + protected function getTargetFile(string $directory, ?string $name = null) + { + if (!is_dir($directory)) { + if (false === @mkdir($directory, 0777, true) && !is_dir($directory)) { + throw new FileException(sprintf('Unable to create the "%s" directory.', $directory)); + } + } elseif (!is_writable($directory)) { + throw new FileException(sprintf('Unable to write in the "%s" directory.', $directory)); + } + + $target = rtrim($directory, '/\\').\DIRECTORY_SEPARATOR.(null === $name ? $this->getBasename() : $this->getName($name)); + + return new self($target, false); + } + + /** + * Returns locale independent base name of the given path. + * + * @return string + */ + protected function getName(string $name) + { + $originalName = str_replace('\\', '/', $name); + $pos = strrpos($originalName, '/'); + $originalName = false === $pos ? $originalName : substr($originalName, $pos + 1); + + return $originalName; + } +} diff --git a/vendor/symfony/http-foundation/File/Stream.php b/vendor/symfony/http-foundation/File/Stream.php new file mode 100644 index 0000000..cef3e03 --- /dev/null +++ b/vendor/symfony/http-foundation/File/Stream.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File; + +/** + * A PHP stream of unknown size. + * + * @author Nicolas Grekas + */ +class Stream extends File +{ + /** + * {@inheritdoc} + * + * @return int|false + */ + #[\ReturnTypeWillChange] + public function getSize() + { + return false; + } +} diff --git a/vendor/symfony/http-foundation/File/UploadedFile.php b/vendor/symfony/http-foundation/File/UploadedFile.php new file mode 100644 index 0000000..6ff6e51 --- /dev/null +++ b/vendor/symfony/http-foundation/File/UploadedFile.php @@ -0,0 +1,290 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File; + +use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException; +use Symfony\Component\HttpFoundation\File\Exception\ExtensionFileException; +use Symfony\Component\HttpFoundation\File\Exception\FileException; +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; +use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException; +use Symfony\Component\HttpFoundation\File\Exception\IniSizeFileException; +use Symfony\Component\HttpFoundation\File\Exception\NoFileException; +use Symfony\Component\HttpFoundation\File\Exception\NoTmpDirFileException; +use Symfony\Component\HttpFoundation\File\Exception\PartialFileException; +use Symfony\Component\Mime\MimeTypes; + +/** + * A file uploaded through a form. + * + * @author Bernhard Schussek + * @author Florian Eckerstorfer + * @author Fabien Potencier + */ +class UploadedFile extends File +{ + private $test; + private $originalName; + private $mimeType; + private $error; + + /** + * Accepts the information of the uploaded file as provided by the PHP global $_FILES. + * + * The file object is only created when the uploaded file is valid (i.e. when the + * isValid() method returns true). Otherwise the only methods that could be called + * on an UploadedFile instance are: + * + * * getClientOriginalName, + * * getClientMimeType, + * * isValid, + * * getError. + * + * Calling any other method on an non-valid instance will cause an unpredictable result. + * + * @param string $path The full temporary path to the file + * @param string $originalName The original file name of the uploaded file + * @param string|null $mimeType The type of the file as provided by PHP; null defaults to application/octet-stream + * @param int|null $error The error constant of the upload (one of PHP's UPLOAD_ERR_XXX constants); null defaults to UPLOAD_ERR_OK + * @param bool $test Whether the test mode is active + * Local files are used in test mode hence the code should not enforce HTTP uploads + * + * @throws FileException If file_uploads is disabled + * @throws FileNotFoundException If the file does not exist + */ + public function __construct(string $path, string $originalName, ?string $mimeType = null, ?int $error = null, bool $test = false) + { + $this->originalName = $this->getName($originalName); + $this->mimeType = $mimeType ?: 'application/octet-stream'; + $this->error = $error ?: \UPLOAD_ERR_OK; + $this->test = $test; + + parent::__construct($path, \UPLOAD_ERR_OK === $this->error); + } + + /** + * Returns the original file name. + * + * It is extracted from the request from which the file has been uploaded. + * This should not be considered as a safe value to use for a file name on your servers. + * + * @return string + */ + public function getClientOriginalName() + { + return $this->originalName; + } + + /** + * Returns the original file extension. + * + * It is extracted from the original file name that was uploaded. + * This should not be considered as a safe value to use for a file name on your servers. + * + * @return string + */ + public function getClientOriginalExtension() + { + return pathinfo($this->originalName, \PATHINFO_EXTENSION); + } + + /** + * Returns the file mime type. + * + * The client mime type is extracted from the request from which the file + * was uploaded, so it should not be considered as a safe value. + * + * For a trusted mime type, use getMimeType() instead (which guesses the mime + * type based on the file content). + * + * @return string + * + * @see getMimeType() + */ + public function getClientMimeType() + { + return $this->mimeType; + } + + /** + * Returns the extension based on the client mime type. + * + * If the mime type is unknown, returns null. + * + * This method uses the mime type as guessed by getClientMimeType() + * to guess the file extension. As such, the extension returned + * by this method cannot be trusted. + * + * For a trusted extension, use guessExtension() instead (which guesses + * the extension based on the guessed mime type for the file). + * + * @return string|null + * + * @see guessExtension() + * @see getClientMimeType() + */ + public function guessClientExtension() + { + if (!class_exists(MimeTypes::class)) { + throw new \LogicException('You cannot guess the extension as the Mime component is not installed. Try running "composer require symfony/mime".'); + } + + return MimeTypes::getDefault()->getExtensions($this->getClientMimeType())[0] ?? null; + } + + /** + * Returns the upload error. + * + * If the upload was successful, the constant UPLOAD_ERR_OK is returned. + * Otherwise one of the other UPLOAD_ERR_XXX constants is returned. + * + * @return int + */ + public function getError() + { + return $this->error; + } + + /** + * Returns whether the file has been uploaded with HTTP and no error occurred. + * + * @return bool + */ + public function isValid() + { + $isOk = \UPLOAD_ERR_OK === $this->error; + + return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname()); + } + + /** + * Moves the file to a new location. + * + * @return File + * + * @throws FileException if, for any reason, the file could not have been moved + */ + public function move(string $directory, ?string $name = null) + { + if ($this->isValid()) { + if ($this->test) { + return parent::move($directory, $name); + } + + $target = $this->getTargetFile($directory, $name); + + set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); + try { + $moved = move_uploaded_file($this->getPathname(), $target); + } finally { + restore_error_handler(); + } + if (!$moved) { + throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error))); + } + + @chmod($target, 0666 & ~umask()); + + return $target; + } + + switch ($this->error) { + case \UPLOAD_ERR_INI_SIZE: + throw new IniSizeFileException($this->getErrorMessage()); + case \UPLOAD_ERR_FORM_SIZE: + throw new FormSizeFileException($this->getErrorMessage()); + case \UPLOAD_ERR_PARTIAL: + throw new PartialFileException($this->getErrorMessage()); + case \UPLOAD_ERR_NO_FILE: + throw new NoFileException($this->getErrorMessage()); + case \UPLOAD_ERR_CANT_WRITE: + throw new CannotWriteFileException($this->getErrorMessage()); + case \UPLOAD_ERR_NO_TMP_DIR: + throw new NoTmpDirFileException($this->getErrorMessage()); + case \UPLOAD_ERR_EXTENSION: + throw new ExtensionFileException($this->getErrorMessage()); + } + + throw new FileException($this->getErrorMessage()); + } + + /** + * Returns the maximum size of an uploaded file as configured in php.ini. + * + * @return int|float The maximum size of an uploaded file in bytes (returns float if size > PHP_INT_MAX) + */ + public static function getMaxFilesize() + { + $sizePostMax = self::parseFilesize(\ini_get('post_max_size')); + $sizeUploadMax = self::parseFilesize(\ini_get('upload_max_filesize')); + + return min($sizePostMax ?: \PHP_INT_MAX, $sizeUploadMax ?: \PHP_INT_MAX); + } + + /** + * Returns the given size from an ini value in bytes. + * + * @return int|float Returns float if size > PHP_INT_MAX + */ + private static function parseFilesize(string $size) + { + if ('' === $size) { + return 0; + } + + $size = strtolower($size); + + $max = ltrim($size, '+'); + if (str_starts_with($max, '0x')) { + $max = \intval($max, 16); + } elseif (str_starts_with($max, '0')) { + $max = \intval($max, 8); + } else { + $max = (int) $max; + } + + switch (substr($size, -1)) { + case 't': $max *= 1024; + // no break + case 'g': $max *= 1024; + // no break + case 'm': $max *= 1024; + // no break + case 'k': $max *= 1024; + } + + return $max; + } + + /** + * Returns an informative upload error message. + * + * @return string + */ + public function getErrorMessage() + { + static $errors = [ + \UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d KiB).', + \UPLOAD_ERR_FORM_SIZE => 'The file "%s" exceeds the upload limit defined in your form.', + \UPLOAD_ERR_PARTIAL => 'The file "%s" was only partially uploaded.', + \UPLOAD_ERR_NO_FILE => 'No file was uploaded.', + \UPLOAD_ERR_CANT_WRITE => 'The file "%s" could not be written on disk.', + \UPLOAD_ERR_NO_TMP_DIR => 'File could not be uploaded: missing temporary directory.', + \UPLOAD_ERR_EXTENSION => 'File upload was stopped by a PHP extension.', + ]; + + $errorCode = $this->error; + $maxFilesize = \UPLOAD_ERR_INI_SIZE === $errorCode ? self::getMaxFilesize() / 1024 : 0; + $message = $errors[$errorCode] ?? 'The file "%s" was not uploaded due to an unknown error.'; + + return sprintf($message, $this->getClientOriginalName(), $maxFilesize); + } +} diff --git a/vendor/symfony/http-foundation/FileBag.php b/vendor/symfony/http-foundation/FileBag.php new file mode 100644 index 0000000..ff5ab77 --- /dev/null +++ b/vendor/symfony/http-foundation/FileBag.php @@ -0,0 +1,140 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\File\UploadedFile; + +/** + * FileBag is a container for uploaded files. + * + * @author Fabien Potencier + * @author Bulat Shakirzyanov + */ +class FileBag extends ParameterBag +{ + private const FILE_KEYS = ['error', 'name', 'size', 'tmp_name', 'type']; + + /** + * @param array|UploadedFile[] $parameters An array of HTTP files + */ + public function __construct(array $parameters = []) + { + $this->replace($parameters); + } + + /** + * {@inheritdoc} + */ + public function replace(array $files = []) + { + $this->parameters = []; + $this->add($files); + } + + /** + * {@inheritdoc} + */ + public function set(string $key, $value) + { + if (!\is_array($value) && !$value instanceof UploadedFile) { + throw new \InvalidArgumentException('An uploaded file must be an array or an instance of UploadedFile.'); + } + + parent::set($key, $this->convertFileInformation($value)); + } + + /** + * {@inheritdoc} + */ + public function add(array $files = []) + { + foreach ($files as $key => $file) { + $this->set($key, $file); + } + } + + /** + * Converts uploaded files to UploadedFile instances. + * + * @param array|UploadedFile $file A (multi-dimensional) array of uploaded file information + * + * @return UploadedFile[]|UploadedFile|null + */ + protected function convertFileInformation($file) + { + if ($file instanceof UploadedFile) { + return $file; + } + + $file = $this->fixPhpFilesArray($file); + $keys = array_keys($file); + sort($keys); + + if (self::FILE_KEYS == $keys) { + if (\UPLOAD_ERR_NO_FILE == $file['error']) { + $file = null; + } else { + $file = new UploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['error'], false); + } + } else { + $file = array_map(function ($v) { return $v instanceof UploadedFile || \is_array($v) ? $this->convertFileInformation($v) : $v; }, $file); + if (array_keys($keys) === $keys) { + $file = array_filter($file); + } + } + + return $file; + } + + /** + * Fixes a malformed PHP $_FILES array. + * + * PHP has a bug that the format of the $_FILES array differs, depending on + * whether the uploaded file fields had normal field names or array-like + * field names ("normal" vs. "parent[child]"). + * + * This method fixes the array to look like the "normal" $_FILES array. + * + * It's safe to pass an already converted array, in which case this method + * just returns the original array unmodified. + * + * @return array + */ + protected function fixPhpFilesArray(array $data) + { + // Remove extra key added by PHP 8.1. + unset($data['full_path']); + $keys = array_keys($data); + sort($keys); + + if (self::FILE_KEYS != $keys || !isset($data['name']) || !\is_array($data['name'])) { + return $data; + } + + $files = $data; + foreach (self::FILE_KEYS as $k) { + unset($files[$k]); + } + + foreach ($data['name'] as $key => $name) { + $files[$key] = $this->fixPhpFilesArray([ + 'error' => $data['error'][$key], + 'name' => $name, + 'type' => $data['type'][$key], + 'tmp_name' => $data['tmp_name'][$key], + 'size' => $data['size'][$key], + ]); + } + + return $files; + } +} diff --git a/vendor/symfony/http-foundation/HeaderBag.php b/vendor/symfony/http-foundation/HeaderBag.php new file mode 100644 index 0000000..43d5f63 --- /dev/null +++ b/vendor/symfony/http-foundation/HeaderBag.php @@ -0,0 +1,295 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * HeaderBag is a container for HTTP headers. + * + * @author Fabien Potencier + * + * @implements \IteratorAggregate> + */ +class HeaderBag implements \IteratorAggregate, \Countable +{ + protected const UPPER = '_ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + protected const LOWER = '-abcdefghijklmnopqrstuvwxyz'; + + /** + * @var array> + */ + protected $headers = []; + protected $cacheControl = []; + + public function __construct(array $headers = []) + { + foreach ($headers as $key => $values) { + $this->set($key, $values); + } + } + + /** + * Returns the headers as a string. + * + * @return string + */ + public function __toString() + { + if (!$headers = $this->all()) { + return ''; + } + + ksort($headers); + $max = max(array_map('strlen', array_keys($headers))) + 1; + $content = ''; + foreach ($headers as $name => $values) { + $name = ucwords($name, '-'); + foreach ($values as $value) { + $content .= sprintf("%-{$max}s %s\r\n", $name.':', $value); + } + } + + return $content; + } + + /** + * Returns the headers. + * + * @param string|null $key The name of the headers to return or null to get them all + * + * @return array>|array + */ + public function all(?string $key = null) + { + if (null !== $key) { + return $this->headers[strtr($key, self::UPPER, self::LOWER)] ?? []; + } + + return $this->headers; + } + + /** + * Returns the parameter keys. + * + * @return string[] + */ + public function keys() + { + return array_keys($this->all()); + } + + /** + * Replaces the current HTTP headers by a new set. + */ + public function replace(array $headers = []) + { + $this->headers = []; + $this->add($headers); + } + + /** + * Adds new headers the current HTTP headers set. + */ + public function add(array $headers) + { + foreach ($headers as $key => $values) { + $this->set($key, $values); + } + } + + /** + * Returns the first header by name or the default one. + * + * @return string|null + */ + public function get(string $key, ?string $default = null) + { + $headers = $this->all($key); + + if (!$headers) { + return $default; + } + + if (null === $headers[0]) { + return null; + } + + return (string) $headers[0]; + } + + /** + * Sets a header by name. + * + * @param string|string[]|null $values The value or an array of values + * @param bool $replace Whether to replace the actual value or not (true by default) + */ + public function set(string $key, $values, bool $replace = true) + { + $key = strtr($key, self::UPPER, self::LOWER); + + if (\is_array($values)) { + $values = array_values($values); + + if (true === $replace || !isset($this->headers[$key])) { + $this->headers[$key] = $values; + } else { + $this->headers[$key] = array_merge($this->headers[$key], $values); + } + } else { + if (true === $replace || !isset($this->headers[$key])) { + $this->headers[$key] = [$values]; + } else { + $this->headers[$key][] = $values; + } + } + + if ('cache-control' === $key) { + $this->cacheControl = $this->parseCacheControl(implode(', ', $this->headers[$key])); + } + } + + /** + * Returns true if the HTTP header is defined. + * + * @return bool + */ + public function has(string $key) + { + return \array_key_exists(strtr($key, self::UPPER, self::LOWER), $this->all()); + } + + /** + * Returns true if the given HTTP header contains the given value. + * + * @return bool + */ + public function contains(string $key, string $value) + { + return \in_array($value, $this->all($key)); + } + + /** + * Removes a header. + */ + public function remove(string $key) + { + $key = strtr($key, self::UPPER, self::LOWER); + + unset($this->headers[$key]); + + if ('cache-control' === $key) { + $this->cacheControl = []; + } + } + + /** + * Returns the HTTP header value converted to a date. + * + * @return \DateTimeInterface|null + * + * @throws \RuntimeException When the HTTP header is not parseable + */ + public function getDate(string $key, ?\DateTime $default = null) + { + if (null === $value = $this->get($key)) { + return $default; + } + + if (false === $date = \DateTime::createFromFormat(\DATE_RFC2822, $value)) { + throw new \RuntimeException(sprintf('The "%s" HTTP header is not parseable (%s).', $key, $value)); + } + + return $date; + } + + /** + * Adds a custom Cache-Control directive. + * + * @param bool|string $value The Cache-Control directive value + */ + public function addCacheControlDirective(string $key, $value = true) + { + $this->cacheControl[$key] = $value; + + $this->set('Cache-Control', $this->getCacheControlHeader()); + } + + /** + * Returns true if the Cache-Control directive is defined. + * + * @return bool + */ + public function hasCacheControlDirective(string $key) + { + return \array_key_exists($key, $this->cacheControl); + } + + /** + * Returns a Cache-Control directive value by name. + * + * @return bool|string|null + */ + public function getCacheControlDirective(string $key) + { + return $this->cacheControl[$key] ?? null; + } + + /** + * Removes a Cache-Control directive. + */ + public function removeCacheControlDirective(string $key) + { + unset($this->cacheControl[$key]); + + $this->set('Cache-Control', $this->getCacheControlHeader()); + } + + /** + * Returns an iterator for headers. + * + * @return \ArrayIterator> + */ + #[\ReturnTypeWillChange] + public function getIterator() + { + return new \ArrayIterator($this->headers); + } + + /** + * Returns the number of headers. + * + * @return int + */ + #[\ReturnTypeWillChange] + public function count() + { + return \count($this->headers); + } + + protected function getCacheControlHeader() + { + ksort($this->cacheControl); + + return HeaderUtils::toString($this->cacheControl, ','); + } + + /** + * Parses a Cache-Control HTTP header. + * + * @return array + */ + protected function parseCacheControl(string $header) + { + $parts = HeaderUtils::split($header, ',='); + + return HeaderUtils::combine($parts); + } +} diff --git a/vendor/symfony/http-foundation/HeaderUtils.php b/vendor/symfony/http-foundation/HeaderUtils.php new file mode 100644 index 0000000..110896e --- /dev/null +++ b/vendor/symfony/http-foundation/HeaderUtils.php @@ -0,0 +1,298 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * HTTP header utility functions. + * + * @author Christian Schmidt + */ +class HeaderUtils +{ + public const DISPOSITION_ATTACHMENT = 'attachment'; + public const DISPOSITION_INLINE = 'inline'; + + /** + * This class should not be instantiated. + */ + private function __construct() + { + } + + /** + * Splits an HTTP header by one or more separators. + * + * Example: + * + * HeaderUtils::split('da, en-gb;q=0.8', ',;') + * // => ['da'], ['en-gb', 'q=0.8']] + * + * @param string $separators List of characters to split on, ordered by + * precedence, e.g. ',', ';=', or ',;=' + * + * @return array Nested array with as many levels as there are characters in + * $separators + */ + public static function split(string $header, string $separators): array + { + if ('' === $separators) { + throw new \InvalidArgumentException('At least one separator must be specified.'); + } + + $quotedSeparators = preg_quote($separators, '/'); + + preg_match_all(' + / + (?!\s) + (?: + # quoted-string + "(?:[^"\\\\]|\\\\.)*(?:"|\\\\|$) + | + # token + [^"'.$quotedSeparators.']+ + )+ + (?['.$quotedSeparators.']) + \s* + /x', trim($header), $matches, \PREG_SET_ORDER); + + return self::groupParts($matches, $separators); + } + + /** + * Combines an array of arrays into one associative array. + * + * Each of the nested arrays should have one or two elements. The first + * value will be used as the keys in the associative array, and the second + * will be used as the values, or true if the nested array only contains one + * element. Array keys are lowercased. + * + * Example: + * + * HeaderUtils::combine([['foo', 'abc'], ['bar']]) + * // => ['foo' => 'abc', 'bar' => true] + */ + public static function combine(array $parts): array + { + $assoc = []; + foreach ($parts as $part) { + $name = strtolower($part[0]); + $value = $part[1] ?? true; + $assoc[$name] = $value; + } + + return $assoc; + } + + /** + * Joins an associative array into a string for use in an HTTP header. + * + * The key and value of each entry are joined with '=', and all entries + * are joined with the specified separator and an additional space (for + * readability). Values are quoted if necessary. + * + * Example: + * + * HeaderUtils::toString(['foo' => 'abc', 'bar' => true, 'baz' => 'a b c'], ',') + * // => 'foo=abc, bar, baz="a b c"' + */ + public static function toString(array $assoc, string $separator): string + { + $parts = []; + foreach ($assoc as $name => $value) { + if (true === $value) { + $parts[] = $name; + } else { + $parts[] = $name.'='.self::quote($value); + } + } + + return implode($separator.' ', $parts); + } + + /** + * Encodes a string as a quoted string, if necessary. + * + * If a string contains characters not allowed by the "token" construct in + * the HTTP specification, it is backslash-escaped and enclosed in quotes + * to match the "quoted-string" construct. + */ + public static function quote(string $s): string + { + if (preg_match('/^[a-z0-9!#$%&\'*.^_`|~-]+$/i', $s)) { + return $s; + } + + return '"'.addcslashes($s, '"\\"').'"'; + } + + /** + * Decodes a quoted string. + * + * If passed an unquoted string that matches the "token" construct (as + * defined in the HTTP specification), it is passed through verbatim. + */ + public static function unquote(string $s): string + { + return preg_replace('/\\\\(.)|"/', '$1', $s); + } + + /** + * Generates an HTTP Content-Disposition field-value. + * + * @param string $disposition One of "inline" or "attachment" + * @param string $filename A unicode string + * @param string $filenameFallback A string containing only ASCII characters that + * is semantically equivalent to $filename. If the filename is already ASCII, + * it can be omitted, or just copied from $filename + * + * @throws \InvalidArgumentException + * + * @see RFC 6266 + */ + public static function makeDisposition(string $disposition, string $filename, string $filenameFallback = ''): string + { + if (!\in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE])) { + throw new \InvalidArgumentException(sprintf('The disposition must be either "%s" or "%s".', self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE)); + } + + if ('' === $filenameFallback) { + $filenameFallback = $filename; + } + + // filenameFallback is not ASCII. + if (!preg_match('/^[\x20-\x7e]*$/', $filenameFallback)) { + throw new \InvalidArgumentException('The filename fallback must only contain ASCII characters.'); + } + + // percent characters aren't safe in fallback. + if (str_contains($filenameFallback, '%')) { + throw new \InvalidArgumentException('The filename fallback cannot contain the "%" character.'); + } + + // path separators aren't allowed in either. + if (str_contains($filename, '/') || str_contains($filename, '\\') || str_contains($filenameFallback, '/') || str_contains($filenameFallback, '\\')) { + throw new \InvalidArgumentException('The filename and the fallback cannot contain the "/" and "\\" characters.'); + } + + $params = ['filename' => $filenameFallback]; + if ($filename !== $filenameFallback) { + $params['filename*'] = "utf-8''".rawurlencode($filename); + } + + return $disposition.'; '.self::toString($params, ';'); + } + + /** + * Like parse_str(), but preserves dots in variable names. + */ + public static function parseQuery(string $query, bool $ignoreBrackets = false, string $separator = '&'): array + { + $q = []; + + foreach (explode($separator, $query) as $v) { + if (false !== $i = strpos($v, "\0")) { + $v = substr($v, 0, $i); + } + + if (false === $i = strpos($v, '=')) { + $k = urldecode($v); + $v = ''; + } else { + $k = urldecode(substr($v, 0, $i)); + $v = substr($v, $i); + } + + if (false !== $i = strpos($k, "\0")) { + $k = substr($k, 0, $i); + } + + $k = ltrim($k, ' '); + + if ($ignoreBrackets) { + $q[$k][] = urldecode(substr($v, 1)); + + continue; + } + + if (false === $i = strpos($k, '[')) { + $q[] = bin2hex($k).$v; + } else { + $q[] = bin2hex(substr($k, 0, $i)).rawurlencode(substr($k, $i)).$v; + } + } + + if ($ignoreBrackets) { + return $q; + } + + parse_str(implode('&', $q), $q); + + $query = []; + + foreach ($q as $k => $v) { + if (false !== $i = strpos($k, '_')) { + $query[substr_replace($k, hex2bin(substr($k, 0, $i)).'[', 0, 1 + $i)] = $v; + } else { + $query[hex2bin($k)] = $v; + } + } + + return $query; + } + + private static function groupParts(array $matches, string $separators, bool $first = true): array + { + $separator = $separators[0]; + $separators = substr($separators, 1) ?: ''; + $i = 0; + + if ('' === $separators && !$first) { + $parts = ['']; + + foreach ($matches as $match) { + if (!$i && isset($match['separator'])) { + $i = 1; + $parts[1] = ''; + } else { + $parts[$i] .= self::unquote($match[0]); + } + } + + return $parts; + } + + $parts = []; + $partMatches = []; + + foreach ($matches as $match) { + if (($match['separator'] ?? null) === $separator) { + ++$i; + } else { + $partMatches[$i][] = $match; + } + } + + foreach ($partMatches as $matches) { + if ('' === $separators && '' !== $unquoted = self::unquote($matches[0][0])) { + $parts[] = $unquoted; + } elseif ($groupedParts = self::groupParts($matches, $separators, false)) { + $parts[] = $groupedParts; + } + } + + return $parts; + } +} diff --git a/vendor/symfony/http-foundation/InputBag.php b/vendor/symfony/http-foundation/InputBag.php new file mode 100644 index 0000000..356fbbc --- /dev/null +++ b/vendor/symfony/http-foundation/InputBag.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\Exception\BadRequestException; + +/** + * InputBag is a container for user input values such as $_GET, $_POST, $_REQUEST, and $_COOKIE. + * + * @author Saif Eddin Gmati + */ +final class InputBag extends ParameterBag +{ + /** + * Returns a scalar input value by name. + * + * @param string|int|float|bool|null $default The default value if the input key does not exist + * + * @return string|int|float|bool|null + */ + public function get(string $key, $default = null) + { + if (null !== $default && !\is_scalar($default) && !(\is_object($default) && method_exists($default, '__toString'))) { + trigger_deprecation('symfony/http-foundation', '5.1', 'Passing a non-scalar value as 2nd argument to "%s()" is deprecated, pass a scalar or null instead.', __METHOD__); + } + + $value = parent::get($key, $this); + + if (null !== $value && $this !== $value && !\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { + trigger_deprecation('symfony/http-foundation', '5.1', 'Retrieving a non-scalar value from "%s()" is deprecated, and will throw a "%s" exception in Symfony 6.0, use "%s::all($key)" instead.', __METHOD__, BadRequestException::class, __CLASS__); + } + + return $this === $value ? $default : $value; + } + + /** + * {@inheritdoc} + */ + public function all(?string $key = null): array + { + return parent::all($key); + } + + /** + * Replaces the current input values by a new set. + */ + public function replace(array $inputs = []) + { + $this->parameters = []; + $this->add($inputs); + } + + /** + * Adds input values. + */ + public function add(array $inputs = []) + { + foreach ($inputs as $input => $value) { + $this->set($input, $value); + } + } + + /** + * Sets an input by name. + * + * @param string|int|float|bool|array|null $value + */ + public function set(string $key, $value) + { + if (null !== $value && !\is_scalar($value) && !\is_array($value) && !method_exists($value, '__toString')) { + trigger_deprecation('symfony/http-foundation', '5.1', 'Passing "%s" as a 2nd Argument to "%s()" is deprecated, pass a scalar, array, or null instead.', get_debug_type($value), __METHOD__); + } + + $this->parameters[$key] = $value; + } + + /** + * {@inheritdoc} + */ + public function filter(string $key, $default = null, int $filter = \FILTER_DEFAULT, $options = []) + { + $value = $this->has($key) ? $this->all()[$key] : $default; + + // Always turn $options into an array - this allows filter_var option shortcuts. + if (!\is_array($options) && $options) { + $options = ['flags' => $options]; + } + + if (\is_array($value) && !(($options['flags'] ?? 0) & (\FILTER_REQUIRE_ARRAY | \FILTER_FORCE_ARRAY))) { + trigger_deprecation('symfony/http-foundation', '5.1', 'Filtering an array value with "%s()" without passing the FILTER_REQUIRE_ARRAY or FILTER_FORCE_ARRAY flag is deprecated', __METHOD__); + + if (!isset($options['flags'])) { + $options['flags'] = \FILTER_REQUIRE_ARRAY; + } + } + + if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) { + trigger_deprecation('symfony/http-foundation', '5.2', 'Not passing a Closure together with FILTER_CALLBACK to "%s()" is deprecated. Wrap your filter in a closure instead.', __METHOD__); + // throw new \InvalidArgumentException(sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null))); + } + + return filter_var($value, $filter, $options); + } +} diff --git a/vendor/symfony/http-foundation/IpUtils.php b/vendor/symfony/http-foundation/IpUtils.php new file mode 100644 index 0000000..49d9a9d --- /dev/null +++ b/vendor/symfony/http-foundation/IpUtils.php @@ -0,0 +1,216 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * Http utility functions. + * + * @author Fabien Potencier + */ +class IpUtils +{ + private static $checkedIps = []; + + /** + * This class should not be instantiated. + */ + private function __construct() + { + } + + /** + * Checks if an IPv4 or IPv6 address is contained in the list of given IPs or subnets. + * + * @param string|array $ips List of IPs or subnets (can be a string if only a single one) + * + * @return bool + */ + public static function checkIp(?string $requestIp, $ips) + { + if (null === $requestIp) { + trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + return false; + } + + if (!\is_array($ips)) { + $ips = [$ips]; + } + + $method = substr_count($requestIp, ':') > 1 ? 'checkIp6' : 'checkIp4'; + + foreach ($ips as $ip) { + if (self::$method($requestIp, $ip)) { + return true; + } + } + + return false; + } + + /** + * Compares two IPv4 addresses. + * In case a subnet is given, it checks if it contains the request IP. + * + * @param string $ip IPv4 address or subnet in CIDR notation + * + * @return bool Whether the request IP matches the IP, or whether the request IP is within the CIDR subnet + */ + public static function checkIp4(?string $requestIp, string $ip) + { + if (null === $requestIp) { + trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + return false; + } + + $cacheKey = $requestIp.'-'.$ip.'-v4'; + if (isset(self::$checkedIps[$cacheKey])) { + return self::$checkedIps[$cacheKey]; + } + + if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { + return self::$checkedIps[$cacheKey] = false; + } + + if (str_contains($ip, '/')) { + [$address, $netmask] = explode('/', $ip, 2); + + if ('0' === $netmask) { + return self::$checkedIps[$cacheKey] = false !== filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4); + } + + if ($netmask < 0 || $netmask > 32) { + return self::$checkedIps[$cacheKey] = false; + } + } else { + $address = $ip; + $netmask = 32; + } + + if (false === ip2long($address)) { + return self::$checkedIps[$cacheKey] = false; + } + + return self::$checkedIps[$cacheKey] = 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask); + } + + /** + * Compares two IPv6 addresses. + * In case a subnet is given, it checks if it contains the request IP. + * + * @author David Soria Parra + * + * @see https://github.com/dsp/v6tools + * + * @param string $ip IPv6 address or subnet in CIDR notation + * + * @return bool + * + * @throws \RuntimeException When IPV6 support is not enabled + */ + public static function checkIp6(?string $requestIp, string $ip) + { + if (null === $requestIp) { + trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + return false; + } + + $cacheKey = $requestIp.'-'.$ip.'-v6'; + if (isset(self::$checkedIps[$cacheKey])) { + return self::$checkedIps[$cacheKey]; + } + + if (!((\extension_loaded('sockets') && \defined('AF_INET6')) || @inet_pton('::1'))) { + throw new \RuntimeException('Unable to check Ipv6. Check that PHP was not compiled with option "disable-ipv6".'); + } + + // Check to see if we were given a IP4 $requestIp or $ip by mistake + if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { + return self::$checkedIps[$cacheKey] = false; + } + + if (str_contains($ip, '/')) { + [$address, $netmask] = explode('/', $ip, 2); + + if (!filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { + return self::$checkedIps[$cacheKey] = false; + } + + if ('0' === $netmask) { + return (bool) unpack('n*', @inet_pton($address)); + } + + if ($netmask < 1 || $netmask > 128) { + return self::$checkedIps[$cacheKey] = false; + } + } else { + if (!filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { + return self::$checkedIps[$cacheKey] = false; + } + + $address = $ip; + $netmask = 128; + } + + $bytesAddr = unpack('n*', @inet_pton($address)); + $bytesTest = unpack('n*', @inet_pton($requestIp)); + + if (!$bytesAddr || !$bytesTest) { + return self::$checkedIps[$cacheKey] = false; + } + + for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) { + $left = $netmask - 16 * ($i - 1); + $left = ($left <= 16) ? $left : 16; + $mask = ~(0xFFFF >> $left) & 0xFFFF; + if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) { + return self::$checkedIps[$cacheKey] = false; + } + } + + return self::$checkedIps[$cacheKey] = true; + } + + /** + * Anonymizes an IP/IPv6. + * + * Removes the last byte for v4 and the last 8 bytes for v6 IPs + */ + public static function anonymize(string $ip): string + { + $wrappedIPv6 = false; + if ('[' === substr($ip, 0, 1) && ']' === substr($ip, -1, 1)) { + $wrappedIPv6 = true; + $ip = substr($ip, 1, -1); + } + + $packedAddress = inet_pton($ip); + if (4 === \strlen($packedAddress)) { + $mask = '255.255.255.0'; + } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff:ffff'))) { + $mask = '::ffff:ffff:ff00'; + } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff'))) { + $mask = '::ffff:ff00'; + } else { + $mask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000'; + } + $ip = inet_ntop($packedAddress & inet_pton($mask)); + + if ($wrappedIPv6) { + $ip = '['.$ip.']'; + } + + return $ip; + } +} diff --git a/vendor/symfony/http-foundation/JsonResponse.php b/vendor/symfony/http-foundation/JsonResponse.php new file mode 100644 index 0000000..51bdf19 --- /dev/null +++ b/vendor/symfony/http-foundation/JsonResponse.php @@ -0,0 +1,221 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * Response represents an HTTP response in JSON format. + * + * Note that this class does not force the returned JSON content to be an + * object. It is however recommended that you do return an object as it + * protects yourself against XSSI and JSON-JavaScript Hijacking. + * + * @see https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/AJAX_Security_Cheat_Sheet.md#always-return-json-with-an-object-on-the-outside + * + * @author Igor Wiedler + */ +class JsonResponse extends Response +{ + protected $data; + protected $callback; + + // Encode <, >, ', &, and " characters in the JSON, making it also safe to be embedded into HTML. + // 15 === JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT + public const DEFAULT_ENCODING_OPTIONS = 15; + + protected $encodingOptions = self::DEFAULT_ENCODING_OPTIONS; + + /** + * @param mixed $data The response data + * @param int $status The response status code + * @param array $headers An array of response headers + * @param bool $json If the data is already a JSON string + */ + public function __construct($data = null, int $status = 200, array $headers = [], bool $json = false) + { + parent::__construct('', $status, $headers); + + if ($json && !\is_string($data) && !is_numeric($data) && !\is_callable([$data, '__toString'])) { + throw new \TypeError(sprintf('"%s": If $json is set to true, argument $data must be a string or object implementing __toString(), "%s" given.', __METHOD__, get_debug_type($data))); + } + + if (null === $data) { + $data = new \ArrayObject(); + } + + $json ? $this->setJson($data) : $this->setData($data); + } + + /** + * Factory method for chainability. + * + * Example: + * + * return JsonResponse::create(['key' => 'value']) + * ->setSharedMaxAge(300); + * + * @param mixed $data The JSON response data + * @param int $status The response status code + * @param array $headers An array of response headers + * + * @return static + * + * @deprecated since Symfony 5.1, use __construct() instead. + */ + public static function create($data = null, int $status = 200, array $headers = []) + { + trigger_deprecation('symfony/http-foundation', '5.1', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class); + + return new static($data, $status, $headers); + } + + /** + * Factory method for chainability. + * + * Example: + * + * return JsonResponse::fromJsonString('{"key": "value"}') + * ->setSharedMaxAge(300); + * + * @param string $data The JSON response string + * @param int $status The response status code + * @param array $headers An array of response headers + * + * @return static + */ + public static function fromJsonString(string $data, int $status = 200, array $headers = []) + { + return new static($data, $status, $headers, true); + } + + /** + * Sets the JSONP callback. + * + * @param string|null $callback The JSONP callback or null to use none + * + * @return $this + * + * @throws \InvalidArgumentException When the callback name is not valid + */ + public function setCallback(?string $callback = null) + { + if (null !== $callback) { + // partially taken from https://geekality.net/2011/08/03/valid-javascript-identifier/ + // partially taken from https://github.com/willdurand/JsonpCallbackValidator + // JsonpCallbackValidator is released under the MIT License. See https://github.com/willdurand/JsonpCallbackValidator/blob/v1.1.0/LICENSE for details. + // (c) William Durand + $pattern = '/^[$_\p{L}][$_\p{L}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\x{200C}\x{200D}]*(?:\[(?:"(?:\\\.|[^"\\\])*"|\'(?:\\\.|[^\'\\\])*\'|\d+)\])*?$/u'; + $reserved = [ + 'break', 'do', 'instanceof', 'typeof', 'case', 'else', 'new', 'var', 'catch', 'finally', 'return', 'void', 'continue', 'for', 'switch', 'while', + 'debugger', 'function', 'this', 'with', 'default', 'if', 'throw', 'delete', 'in', 'try', 'class', 'enum', 'extends', 'super', 'const', 'export', + 'import', 'implements', 'let', 'private', 'public', 'yield', 'interface', 'package', 'protected', 'static', 'null', 'true', 'false', + ]; + $parts = explode('.', $callback); + foreach ($parts as $part) { + if (!preg_match($pattern, $part) || \in_array($part, $reserved, true)) { + throw new \InvalidArgumentException('The callback name is not valid.'); + } + } + } + + $this->callback = $callback; + + return $this->update(); + } + + /** + * Sets a raw string containing a JSON document to be sent. + * + * @return $this + */ + public function setJson(string $json) + { + $this->data = $json; + + return $this->update(); + } + + /** + * Sets the data to be sent as JSON. + * + * @param mixed $data + * + * @return $this + * + * @throws \InvalidArgumentException + */ + public function setData($data = []) + { + try { + $data = json_encode($data, $this->encodingOptions); + } catch (\Exception $e) { + if ('Exception' === \get_class($e) && str_starts_with($e->getMessage(), 'Failed calling ')) { + throw $e->getPrevious() ?: $e; + } + throw $e; + } + + if (\PHP_VERSION_ID >= 70300 && (\JSON_THROW_ON_ERROR & $this->encodingOptions)) { + return $this->setJson($data); + } + + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(json_last_error_msg()); + } + + return $this->setJson($data); + } + + /** + * Returns options used while encoding data to JSON. + * + * @return int + */ + public function getEncodingOptions() + { + return $this->encodingOptions; + } + + /** + * Sets options used while encoding data to JSON. + * + * @return $this + */ + public function setEncodingOptions(int $encodingOptions) + { + $this->encodingOptions = $encodingOptions; + + return $this->setData(json_decode($this->data)); + } + + /** + * Updates the content and headers according to the JSON data and callback. + * + * @return $this + */ + protected function update() + { + if (null !== $this->callback) { + // Not using application/javascript for compatibility reasons with older browsers. + $this->headers->set('Content-Type', 'text/javascript'); + + return $this->setContent(sprintf('/**/%s(%s);', $this->callback, $this->data)); + } + + // Only set the header when there is none or when it equals 'text/javascript' (from a previous update with callback) + // in order to not overwrite a custom definition. + if (!$this->headers->has('Content-Type') || 'text/javascript' === $this->headers->get('Content-Type')) { + $this->headers->set('Content-Type', 'application/json'); + } + + return $this->setContent($this->data); + } +} diff --git a/vendor/symfony/http-foundation/LICENSE b/vendor/symfony/http-foundation/LICENSE new file mode 100644 index 0000000..0138f8f --- /dev/null +++ b/vendor/symfony/http-foundation/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/http-foundation/ParameterBag.php b/vendor/symfony/http-foundation/ParameterBag.php new file mode 100644 index 0000000..b542292 --- /dev/null +++ b/vendor/symfony/http-foundation/ParameterBag.php @@ -0,0 +1,228 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\Exception\BadRequestException; + +/** + * ParameterBag is a container for key/value pairs. + * + * @author Fabien Potencier + * + * @implements \IteratorAggregate + */ +class ParameterBag implements \IteratorAggregate, \Countable +{ + /** + * Parameter storage. + */ + protected $parameters; + + public function __construct(array $parameters = []) + { + $this->parameters = $parameters; + } + + /** + * Returns the parameters. + * + * @param string|null $key The name of the parameter to return or null to get them all + * + * @return array + */ + public function all(/* ?string $key = null */) + { + $key = \func_num_args() > 0 ? func_get_arg(0) : null; + + if (null === $key) { + return $this->parameters; + } + + if (!\is_array($value = $this->parameters[$key] ?? [])) { + throw new BadRequestException(sprintf('Unexpected value for parameter "%s": expecting "array", got "%s".', $key, get_debug_type($value))); + } + + return $value; + } + + /** + * Returns the parameter keys. + * + * @return array + */ + public function keys() + { + return array_keys($this->parameters); + } + + /** + * Replaces the current parameters by a new set. + */ + public function replace(array $parameters = []) + { + $this->parameters = $parameters; + } + + /** + * Adds parameters. + */ + public function add(array $parameters = []) + { + $this->parameters = array_replace($this->parameters, $parameters); + } + + /** + * Returns a parameter by name. + * + * @param mixed $default The default value if the parameter key does not exist + * + * @return mixed + */ + public function get(string $key, $default = null) + { + return \array_key_exists($key, $this->parameters) ? $this->parameters[$key] : $default; + } + + /** + * Sets a parameter by name. + * + * @param mixed $value The value + */ + public function set(string $key, $value) + { + $this->parameters[$key] = $value; + } + + /** + * Returns true if the parameter is defined. + * + * @return bool + */ + public function has(string $key) + { + return \array_key_exists($key, $this->parameters); + } + + /** + * Removes a parameter. + */ + public function remove(string $key) + { + unset($this->parameters[$key]); + } + + /** + * Returns the alphabetic characters of the parameter value. + * + * @return string + */ + public function getAlpha(string $key, string $default = '') + { + return preg_replace('/[^[:alpha:]]/', '', $this->get($key, $default)); + } + + /** + * Returns the alphabetic characters and digits of the parameter value. + * + * @return string + */ + public function getAlnum(string $key, string $default = '') + { + return preg_replace('/[^[:alnum:]]/', '', $this->get($key, $default)); + } + + /** + * Returns the digits of the parameter value. + * + * @return string + */ + public function getDigits(string $key, string $default = '') + { + // we need to remove - and + because they're allowed in the filter + return str_replace(['-', '+'], '', $this->filter($key, $default, \FILTER_SANITIZE_NUMBER_INT)); + } + + /** + * Returns the parameter value converted to integer. + * + * @return int + */ + public function getInt(string $key, int $default = 0) + { + return (int) $this->get($key, $default); + } + + /** + * Returns the parameter value converted to boolean. + * + * @return bool + */ + public function getBoolean(string $key, bool $default = false) + { + return $this->filter($key, $default, \FILTER_VALIDATE_BOOLEAN); + } + + /** + * Filter key. + * + * @param mixed $default Default = null + * @param int $filter FILTER_* constant + * @param mixed $options Filter options + * + * @see https://php.net/filter-var + * + * @return mixed + */ + public function filter(string $key, $default = null, int $filter = \FILTER_DEFAULT, $options = []) + { + $value = $this->get($key, $default); + + // Always turn $options into an array - this allows filter_var option shortcuts. + if (!\is_array($options) && $options) { + $options = ['flags' => $options]; + } + + // Add a convenience check for arrays. + if (\is_array($value) && !isset($options['flags'])) { + $options['flags'] = \FILTER_REQUIRE_ARRAY; + } + + if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) { + trigger_deprecation('symfony/http-foundation', '5.2', 'Not passing a Closure together with FILTER_CALLBACK to "%s()" is deprecated. Wrap your filter in a closure instead.', __METHOD__); + // throw new \InvalidArgumentException(sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null))); + } + + return filter_var($value, $filter, $options); + } + + /** + * Returns an iterator for parameters. + * + * @return \ArrayIterator + */ + #[\ReturnTypeWillChange] + public function getIterator() + { + return new \ArrayIterator($this->parameters); + } + + /** + * Returns the number of parameters. + * + * @return int + */ + #[\ReturnTypeWillChange] + public function count() + { + return \count($this->parameters); + } +} diff --git a/vendor/symfony/http-foundation/README.md b/vendor/symfony/http-foundation/README.md new file mode 100644 index 0000000..424f2c4 --- /dev/null +++ b/vendor/symfony/http-foundation/README.md @@ -0,0 +1,28 @@ +HttpFoundation Component +======================== + +The HttpFoundation component defines an object-oriented layer for the HTTP +specification. + +Sponsor +------- + +The HttpFoundation component for Symfony 5.4/6.0 is [backed][1] by [Laravel][2]. + +Laravel is a PHP web development framework that is passionate about maximum developer +happiness. Laravel is built using a variety of bespoke and Symfony based components. + +Help Symfony by [sponsoring][3] its development! + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/http_foundation.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) + +[1]: https://symfony.com/backers +[2]: https://laravel.com/ +[3]: https://symfony.com/sponsor diff --git a/vendor/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php b/vendor/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php new file mode 100644 index 0000000..a6dd993 --- /dev/null +++ b/vendor/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RateLimiter; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\RateLimiter\LimiterInterface; +use Symfony\Component\RateLimiter\Policy\NoLimiter; +use Symfony\Component\RateLimiter\RateLimit; + +/** + * An implementation of RequestRateLimiterInterface that + * fits most use-cases. + * + * @author Wouter de Jong + */ +abstract class AbstractRequestRateLimiter implements RequestRateLimiterInterface +{ + public function consume(Request $request): RateLimit + { + $limiters = $this->getLimiters($request); + if (0 === \count($limiters)) { + $limiters = [new NoLimiter()]; + } + + $minimalRateLimit = null; + foreach ($limiters as $limiter) { + $rateLimit = $limiter->consume(1); + + $minimalRateLimit = $minimalRateLimit ? self::getMinimalRateLimit($minimalRateLimit, $rateLimit) : $rateLimit; + } + + return $minimalRateLimit; + } + + public function reset(Request $request): void + { + foreach ($this->getLimiters($request) as $limiter) { + $limiter->reset(); + } + } + + /** + * @return LimiterInterface[] a set of limiters using keys extracted from the request + */ + abstract protected function getLimiters(Request $request): array; + + private static function getMinimalRateLimit(RateLimit $first, RateLimit $second): RateLimit + { + if ($first->isAccepted() !== $second->isAccepted()) { + return $first->isAccepted() ? $second : $first; + } + + $firstRemainingTokens = $first->getRemainingTokens(); + $secondRemainingTokens = $second->getRemainingTokens(); + + if ($firstRemainingTokens === $secondRemainingTokens) { + return $first->getRetryAfter() < $second->getRetryAfter() ? $second : $first; + } + + return $firstRemainingTokens > $secondRemainingTokens ? $second : $first; + } +} diff --git a/vendor/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php b/vendor/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php new file mode 100644 index 0000000..4c87a40 --- /dev/null +++ b/vendor/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RateLimiter; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\RateLimiter\RateLimit; + +/** + * A special type of limiter that deals with requests. + * + * This allows to limit on different types of information + * from the requests. + * + * @author Wouter de Jong + */ +interface RequestRateLimiterInterface +{ + public function consume(Request $request): RateLimit; + + public function reset(Request $request): void; +} diff --git a/vendor/symfony/http-foundation/RedirectResponse.php b/vendor/symfony/http-foundation/RedirectResponse.php new file mode 100644 index 0000000..7b89f0f --- /dev/null +++ b/vendor/symfony/http-foundation/RedirectResponse.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * RedirectResponse represents an HTTP response doing a redirect. + * + * @author Fabien Potencier + */ +class RedirectResponse extends Response +{ + protected $targetUrl; + + /** + * Creates a redirect response so that it conforms to the rules defined for a redirect status code. + * + * @param string $url The URL to redirect to. The URL should be a full URL, with schema etc., + * but practically every browser redirects on paths only as well + * @param int $status The status code (302 by default) + * @param array $headers The headers (Location is always set to the given URL) + * + * @throws \InvalidArgumentException + * + * @see https://tools.ietf.org/html/rfc2616#section-10.3 + */ + public function __construct(string $url, int $status = 302, array $headers = []) + { + parent::__construct('', $status, $headers); + + $this->setTargetUrl($url); + + if (!$this->isRedirect()) { + throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $status)); + } + + if (301 == $status && !\array_key_exists('cache-control', array_change_key_case($headers, \CASE_LOWER))) { + $this->headers->remove('cache-control'); + } + } + + /** + * Factory method for chainability. + * + * @param string $url The URL to redirect to + * + * @return static + * + * @deprecated since Symfony 5.1, use __construct() instead. + */ + public static function create($url = '', int $status = 302, array $headers = []) + { + trigger_deprecation('symfony/http-foundation', '5.1', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class); + + return new static($url, $status, $headers); + } + + /** + * Returns the target URL. + * + * @return string + */ + public function getTargetUrl() + { + return $this->targetUrl; + } + + /** + * Sets the redirect target of this response. + * + * @return $this + * + * @throws \InvalidArgumentException + */ + public function setTargetUrl(string $url) + { + if ('' === $url) { + throw new \InvalidArgumentException('Cannot redirect to an empty URL.'); + } + + $this->targetUrl = $url; + + $this->setContent( + sprintf(' + + + + + + Redirecting to %1$s + + + Redirecting to %1$s. + +', htmlspecialchars($url, \ENT_QUOTES, 'UTF-8'))); + + $this->headers->set('Location', $url); + $this->headers->set('Content-Type', 'text/html; charset=utf-8'); + + return $this; + } +} diff --git a/vendor/symfony/http-foundation/Request.php b/vendor/symfony/http-foundation/Request.php new file mode 100644 index 0000000..561cb88 --- /dev/null +++ b/vendor/symfony/http-foundation/Request.php @@ -0,0 +1,2176 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException; +use Symfony\Component\HttpFoundation\Exception\JsonException; +use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; +use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; +use Symfony\Component\HttpFoundation\Session\SessionInterface; + +// Help opcache.preload discover always-needed symbols +class_exists(AcceptHeader::class); +class_exists(FileBag::class); +class_exists(HeaderBag::class); +class_exists(HeaderUtils::class); +class_exists(InputBag::class); +class_exists(ParameterBag::class); +class_exists(ServerBag::class); + +/** + * Request represents an HTTP request. + * + * The methods dealing with URL accept / return a raw path (% encoded): + * * getBasePath + * * getBaseUrl + * * getPathInfo + * * getRequestUri + * * getUri + * * getUriForPath + * + * @author Fabien Potencier + */ +class Request +{ + public const HEADER_FORWARDED = 0b000001; // When using RFC 7239 + public const HEADER_X_FORWARDED_FOR = 0b000010; + public const HEADER_X_FORWARDED_HOST = 0b000100; + public const HEADER_X_FORWARDED_PROTO = 0b001000; + public const HEADER_X_FORWARDED_PORT = 0b010000; + public const HEADER_X_FORWARDED_PREFIX = 0b100000; + + /** @deprecated since Symfony 5.2, use either "HEADER_X_FORWARDED_FOR | HEADER_X_FORWARDED_HOST | HEADER_X_FORWARDED_PORT | HEADER_X_FORWARDED_PROTO" or "HEADER_X_FORWARDED_AWS_ELB" or "HEADER_X_FORWARDED_TRAEFIK" constants instead. */ + public const HEADER_X_FORWARDED_ALL = 0b1011110; // All "X-Forwarded-*" headers sent by "usual" reverse proxy + public const HEADER_X_FORWARDED_AWS_ELB = 0b0011010; // AWS ELB doesn't send X-Forwarded-Host + public const HEADER_X_FORWARDED_TRAEFIK = 0b0111110; // All "X-Forwarded-*" headers sent by Traefik reverse proxy + + public const METHOD_HEAD = 'HEAD'; + public const METHOD_GET = 'GET'; + public const METHOD_POST = 'POST'; + public const METHOD_PUT = 'PUT'; + public const METHOD_PATCH = 'PATCH'; + public const METHOD_DELETE = 'DELETE'; + public const METHOD_PURGE = 'PURGE'; + public const METHOD_OPTIONS = 'OPTIONS'; + public const METHOD_TRACE = 'TRACE'; + public const METHOD_CONNECT = 'CONNECT'; + + /** + * @var string[] + */ + protected static $trustedProxies = []; + + /** + * @var string[] + */ + protected static $trustedHostPatterns = []; + + /** + * @var string[] + */ + protected static $trustedHosts = []; + + protected static $httpMethodParameterOverride = false; + + /** + * Custom parameters. + * + * @var ParameterBag + */ + public $attributes; + + /** + * Request body parameters ($_POST). + * + * @var InputBag + */ + public $request; + + /** + * Query string parameters ($_GET). + * + * @var InputBag + */ + public $query; + + /** + * Server and execution environment parameters ($_SERVER). + * + * @var ServerBag + */ + public $server; + + /** + * Uploaded files ($_FILES). + * + * @var FileBag + */ + public $files; + + /** + * Cookies ($_COOKIE). + * + * @var InputBag + */ + public $cookies; + + /** + * Headers (taken from the $_SERVER). + * + * @var HeaderBag + */ + public $headers; + + /** + * @var string|resource|false|null + */ + protected $content; + + /** + * @var array + */ + protected $languages; + + /** + * @var array + */ + protected $charsets; + + /** + * @var array + */ + protected $encodings; + + /** + * @var array + */ + protected $acceptableContentTypes; + + /** + * @var string + */ + protected $pathInfo; + + /** + * @var string + */ + protected $requestUri; + + /** + * @var string + */ + protected $baseUrl; + + /** + * @var string + */ + protected $basePath; + + /** + * @var string + */ + protected $method; + + /** + * @var string + */ + protected $format; + + /** + * @var SessionInterface|callable(): SessionInterface + */ + protected $session; + + /** + * @var string|null + */ + protected $locale; + + /** + * @var string + */ + protected $defaultLocale = 'en'; + + /** + * @var array + */ + protected static $formats; + + protected static $requestFactory; + + /** + * @var string|null + */ + private $preferredFormat; + private $isHostValid = true; + private $isForwardedValid = true; + + /** + * @var bool|null + */ + private $isSafeContentPreferred; + + private static $trustedHeaderSet = -1; + + private const FORWARDED_PARAMS = [ + self::HEADER_X_FORWARDED_FOR => 'for', + self::HEADER_X_FORWARDED_HOST => 'host', + self::HEADER_X_FORWARDED_PROTO => 'proto', + self::HEADER_X_FORWARDED_PORT => 'host', + ]; + + /** + * Names for headers that can be trusted when + * using trusted proxies. + * + * The FORWARDED header is the standard as of rfc7239. + * + * The other headers are non-standard, but widely used + * by popular reverse proxies (like Apache mod_proxy or Amazon EC2). + */ + private const TRUSTED_HEADERS = [ + self::HEADER_FORWARDED => 'FORWARDED', + self::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR', + self::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST', + self::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO', + self::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT', + self::HEADER_X_FORWARDED_PREFIX => 'X_FORWARDED_PREFIX', + ]; + + /** @var bool */ + private $isIisRewrite = false; + + /** + * @param array $query The GET parameters + * @param array $request The POST parameters + * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies The COOKIE parameters + * @param array $files The FILES parameters + * @param array $server The SERVER parameters + * @param string|resource|null $content The raw body data + */ + public function __construct(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) + { + $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content); + } + + /** + * Sets the parameters for this request. + * + * This method also re-initializes all properties. + * + * @param array $query The GET parameters + * @param array $request The POST parameters + * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies The COOKIE parameters + * @param array $files The FILES parameters + * @param array $server The SERVER parameters + * @param string|resource|null $content The raw body data + */ + public function initialize(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) + { + $this->request = new InputBag($request); + $this->query = new InputBag($query); + $this->attributes = new ParameterBag($attributes); + $this->cookies = new InputBag($cookies); + $this->files = new FileBag($files); + $this->server = new ServerBag($server); + $this->headers = new HeaderBag($this->server->getHeaders()); + + $this->content = $content; + $this->languages = null; + $this->charsets = null; + $this->encodings = null; + $this->acceptableContentTypes = null; + $this->pathInfo = null; + $this->requestUri = null; + $this->baseUrl = null; + $this->basePath = null; + $this->method = null; + $this->format = null; + } + + /** + * Creates a new request with values from PHP's super globals. + * + * @return static + */ + public static function createFromGlobals() + { + $request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER); + + if (str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded') + && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH']) + ) { + parse_str($request->getContent(), $data); + $request->request = new InputBag($data); + } + + return $request; + } + + /** + * Creates a Request based on a given URI and configuration. + * + * The information contained in the URI always take precedence + * over the other information (server and parameters). + * + * @param string $uri The URI + * @param string $method The HTTP method + * @param array $parameters The query (GET) or request (POST) parameters + * @param array $cookies The request cookies ($_COOKIE) + * @param array $files The request files ($_FILES) + * @param array $server The server parameters ($_SERVER) + * @param string|resource|null $content The raw body data + * + * @return static + */ + public static function create(string $uri, string $method = 'GET', array $parameters = [], array $cookies = [], array $files = [], array $server = [], $content = null) + { + $server = array_replace([ + 'SERVER_NAME' => 'localhost', + 'SERVER_PORT' => 80, + 'HTTP_HOST' => 'localhost', + 'HTTP_USER_AGENT' => 'Symfony', + 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'HTTP_ACCEPT_LANGUAGE' => 'en-us,en;q=0.5', + 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', + 'REMOTE_ADDR' => '127.0.0.1', + 'SCRIPT_NAME' => '', + 'SCRIPT_FILENAME' => '', + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'REQUEST_TIME' => time(), + 'REQUEST_TIME_FLOAT' => microtime(true), + ], $server); + + $server['PATH_INFO'] = ''; + $server['REQUEST_METHOD'] = strtoupper($method); + + if (false === ($components = parse_url($uri)) && '/' === ($uri[0] ?? '')) { + $components = parse_url($uri.'#'); + unset($components['fragment']); + } + + if (isset($components['host'])) { + $server['SERVER_NAME'] = $components['host']; + $server['HTTP_HOST'] = $components['host']; + } + + if (isset($components['scheme'])) { + if ('https' === $components['scheme']) { + $server['HTTPS'] = 'on'; + $server['SERVER_PORT'] = 443; + } else { + unset($server['HTTPS']); + $server['SERVER_PORT'] = 80; + } + } + + if (isset($components['port'])) { + $server['SERVER_PORT'] = $components['port']; + $server['HTTP_HOST'] .= ':'.$components['port']; + } + + if (isset($components['user'])) { + $server['PHP_AUTH_USER'] = $components['user']; + } + + if (isset($components['pass'])) { + $server['PHP_AUTH_PW'] = $components['pass']; + } + + if (!isset($components['path'])) { + $components['path'] = '/'; + } + + switch (strtoupper($method)) { + case 'POST': + case 'PUT': + case 'DELETE': + if (!isset($server['CONTENT_TYPE'])) { + $server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; + } + // no break + case 'PATCH': + $request = $parameters; + $query = []; + break; + default: + $request = []; + $query = $parameters; + break; + } + + $queryString = ''; + if (isset($components['query'])) { + parse_str(html_entity_decode($components['query']), $qs); + + if ($query) { + $query = array_replace($qs, $query); + $queryString = http_build_query($query, '', '&'); + } else { + $query = $qs; + $queryString = $components['query']; + } + } elseif ($query) { + $queryString = http_build_query($query, '', '&'); + } + + $server['REQUEST_URI'] = $components['path'].('' !== $queryString ? '?'.$queryString : ''); + $server['QUERY_STRING'] = $queryString; + + return self::createRequestFromFactory($query, $request, [], $cookies, $files, $server, $content); + } + + /** + * Sets a callable able to create a Request instance. + * + * This is mainly useful when you need to override the Request class + * to keep BC with an existing system. It should not be used for any + * other purpose. + */ + public static function setFactory(?callable $callable) + { + self::$requestFactory = $callable; + } + + /** + * Clones a request and overrides some of its parameters. + * + * @param array|null $query The GET parameters + * @param array|null $request The POST parameters + * @param array|null $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array|null $cookies The COOKIE parameters + * @param array|null $files The FILES parameters + * @param array|null $server The SERVER parameters + * + * @return static + */ + public function duplicate(?array $query = null, ?array $request = null, ?array $attributes = null, ?array $cookies = null, ?array $files = null, ?array $server = null) + { + $dup = clone $this; + if (null !== $query) { + $dup->query = new InputBag($query); + } + if (null !== $request) { + $dup->request = new InputBag($request); + } + if (null !== $attributes) { + $dup->attributes = new ParameterBag($attributes); + } + if (null !== $cookies) { + $dup->cookies = new InputBag($cookies); + } + if (null !== $files) { + $dup->files = new FileBag($files); + } + if (null !== $server) { + $dup->server = new ServerBag($server); + $dup->headers = new HeaderBag($dup->server->getHeaders()); + } + $dup->languages = null; + $dup->charsets = null; + $dup->encodings = null; + $dup->acceptableContentTypes = null; + $dup->pathInfo = null; + $dup->requestUri = null; + $dup->baseUrl = null; + $dup->basePath = null; + $dup->method = null; + $dup->format = null; + + if (!$dup->get('_format') && $this->get('_format')) { + $dup->attributes->set('_format', $this->get('_format')); + } + + if (!$dup->getRequestFormat(null)) { + $dup->setRequestFormat($this->getRequestFormat(null)); + } + + return $dup; + } + + /** + * Clones the current request. + * + * Note that the session is not cloned as duplicated requests + * are most of the time sub-requests of the main one. + */ + public function __clone() + { + $this->query = clone $this->query; + $this->request = clone $this->request; + $this->attributes = clone $this->attributes; + $this->cookies = clone $this->cookies; + $this->files = clone $this->files; + $this->server = clone $this->server; + $this->headers = clone $this->headers; + } + + /** + * Returns the request as a string. + * + * @return string + */ + public function __toString() + { + $content = $this->getContent(); + + $cookieHeader = ''; + $cookies = []; + + foreach ($this->cookies as $k => $v) { + $cookies[] = \is_array($v) ? http_build_query([$k => $v], '', '; ', \PHP_QUERY_RFC3986) : "$k=$v"; + } + + if ($cookies) { + $cookieHeader = 'Cookie: '.implode('; ', $cookies)."\r\n"; + } + + return + sprintf('%s %s %s', $this->getMethod(), $this->getRequestUri(), $this->server->get('SERVER_PROTOCOL'))."\r\n". + $this->headers. + $cookieHeader."\r\n". + $content; + } + + /** + * Overrides the PHP global variables according to this request instance. + * + * It overrides $_GET, $_POST, $_REQUEST, $_SERVER, $_COOKIE. + * $_FILES is never overridden, see rfc1867 + */ + public function overrideGlobals() + { + $this->server->set('QUERY_STRING', static::normalizeQueryString(http_build_query($this->query->all(), '', '&'))); + + $_GET = $this->query->all(); + $_POST = $this->request->all(); + $_SERVER = $this->server->all(); + $_COOKIE = $this->cookies->all(); + + foreach ($this->headers->all() as $key => $value) { + $key = strtoupper(str_replace('-', '_', $key)); + if (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true)) { + $_SERVER[$key] = implode(', ', $value); + } else { + $_SERVER['HTTP_'.$key] = implode(', ', $value); + } + } + + $request = ['g' => $_GET, 'p' => $_POST, 'c' => $_COOKIE]; + + $requestOrder = \ini_get('request_order') ?: \ini_get('variables_order'); + $requestOrder = preg_replace('#[^cgp]#', '', strtolower($requestOrder)) ?: 'gp'; + + $_REQUEST = [[]]; + + foreach (str_split($requestOrder) as $order) { + $_REQUEST[] = $request[$order]; + } + + $_REQUEST = array_merge(...$_REQUEST); + } + + /** + * Sets a list of trusted proxies. + * + * You should only list the reverse proxies that you manage directly. + * + * @param array $proxies A list of trusted proxies, the string 'REMOTE_ADDR' will be replaced with $_SERVER['REMOTE_ADDR'] + * @param int $trustedHeaderSet A bit field of Request::HEADER_*, to set which headers to trust from your proxies + */ + public static function setTrustedProxies(array $proxies, int $trustedHeaderSet) + { + if (self::HEADER_X_FORWARDED_ALL === $trustedHeaderSet) { + trigger_deprecation('symfony/http-foundation', '5.2', 'The "HEADER_X_FORWARDED_ALL" constant is deprecated, use either "HEADER_X_FORWARDED_FOR | HEADER_X_FORWARDED_HOST | HEADER_X_FORWARDED_PORT | HEADER_X_FORWARDED_PROTO" or "HEADER_X_FORWARDED_AWS_ELB" or "HEADER_X_FORWARDED_TRAEFIK" constants instead.'); + } + self::$trustedProxies = array_reduce($proxies, function ($proxies, $proxy) { + if ('REMOTE_ADDR' !== $proxy) { + $proxies[] = $proxy; + } elseif (isset($_SERVER['REMOTE_ADDR'])) { + $proxies[] = $_SERVER['REMOTE_ADDR']; + } + + return $proxies; + }, []); + self::$trustedHeaderSet = $trustedHeaderSet; + } + + /** + * Gets the list of trusted proxies. + * + * @return array + */ + public static function getTrustedProxies() + { + return self::$trustedProxies; + } + + /** + * Gets the set of trusted headers from trusted proxies. + * + * @return int A bit field of Request::HEADER_* that defines which headers are trusted from your proxies + */ + public static function getTrustedHeaderSet() + { + return self::$trustedHeaderSet; + } + + /** + * Sets a list of trusted host patterns. + * + * You should only list the hosts you manage using regexs. + * + * @param array $hostPatterns A list of trusted host patterns + */ + public static function setTrustedHosts(array $hostPatterns) + { + self::$trustedHostPatterns = array_map(function ($hostPattern) { + return sprintf('{%s}i', $hostPattern); + }, $hostPatterns); + // we need to reset trusted hosts on trusted host patterns change + self::$trustedHosts = []; + } + + /** + * Gets the list of trusted host patterns. + * + * @return array + */ + public static function getTrustedHosts() + { + return self::$trustedHostPatterns; + } + + /** + * Normalizes a query string. + * + * It builds a normalized query string, where keys/value pairs are alphabetized, + * have consistent escaping and unneeded delimiters are removed. + * + * @return string + */ + public static function normalizeQueryString(?string $qs) + { + if ('' === ($qs ?? '')) { + return ''; + } + + $qs = HeaderUtils::parseQuery($qs); + ksort($qs); + + return http_build_query($qs, '', '&', \PHP_QUERY_RFC3986); + } + + /** + * Enables support for the _method request parameter to determine the intended HTTP method. + * + * Be warned that enabling this feature might lead to CSRF issues in your code. + * Check that you are using CSRF tokens when required. + * If the HTTP method parameter override is enabled, an html-form with method "POST" can be altered + * and used to send a "PUT" or "DELETE" request via the _method request parameter. + * If these methods are not protected against CSRF, this presents a possible vulnerability. + * + * The HTTP method can only be overridden when the real HTTP method is POST. + */ + public static function enableHttpMethodParameterOverride() + { + self::$httpMethodParameterOverride = true; + } + + /** + * Checks whether support for the _method request parameter is enabled. + * + * @return bool + */ + public static function getHttpMethodParameterOverride() + { + return self::$httpMethodParameterOverride; + } + + /** + * Gets a "parameter" value from any bag. + * + * This method is mainly useful for libraries that want to provide some flexibility. If you don't need the + * flexibility in controllers, it is better to explicitly get request parameters from the appropriate + * public property instead (attributes, query, request). + * + * Order of precedence: PATH (routing placeholders or custom attributes), GET, POST + * + * @param mixed $default The default value if the parameter key does not exist + * + * @return mixed + * + * @internal since Symfony 5.4, use explicit input sources instead + */ + public function get(string $key, $default = null) + { + if ($this !== $result = $this->attributes->get($key, $this)) { + return $result; + } + + if ($this->query->has($key)) { + return $this->query->all()[$key]; + } + + if ($this->request->has($key)) { + return $this->request->all()[$key]; + } + + return $default; + } + + /** + * Gets the Session. + * + * @return SessionInterface + */ + public function getSession() + { + $session = $this->session; + if (!$session instanceof SessionInterface && null !== $session) { + $this->setSession($session = $session()); + } + + if (null === $session) { + throw new SessionNotFoundException('Session has not been set.'); + } + + return $session; + } + + /** + * Whether the request contains a Session which was started in one of the + * previous requests. + * + * @return bool + */ + public function hasPreviousSession() + { + // the check for $this->session avoids malicious users trying to fake a session cookie with proper name + return $this->hasSession() && $this->cookies->has($this->getSession()->getName()); + } + + /** + * Whether the request contains a Session object. + * + * This method does not give any information about the state of the session object, + * like whether the session is started or not. It is just a way to check if this Request + * is associated with a Session instance. + * + * @param bool $skipIfUninitialized When true, ignores factories injected by `setSessionFactory` + * + * @return bool + */ + public function hasSession(/* bool $skipIfUninitialized = false */) + { + $skipIfUninitialized = \func_num_args() > 0 ? func_get_arg(0) : false; + + return null !== $this->session && (!$skipIfUninitialized || $this->session instanceof SessionInterface); + } + + public function setSession(SessionInterface $session) + { + $this->session = $session; + } + + /** + * @internal + * + * @param callable(): SessionInterface $factory + */ + public function setSessionFactory(callable $factory) + { + $this->session = $factory; + } + + /** + * Returns the client IP addresses. + * + * In the returned array the most trusted IP address is first, and the + * least trusted one last. The "real" client IP address is the last one, + * but this is also the least trusted one. Trusted proxies are stripped. + * + * Use this method carefully; you should use getClientIp() instead. + * + * @return array + * + * @see getClientIp() + */ + public function getClientIps() + { + $ip = $this->server->get('REMOTE_ADDR'); + + if (!$this->isFromTrustedProxy()) { + return [$ip]; + } + + return $this->getTrustedValues(self::HEADER_X_FORWARDED_FOR, $ip) ?: [$ip]; + } + + /** + * Returns the client IP address. + * + * This method can read the client IP address from the "X-Forwarded-For" header + * when trusted proxies were set via "setTrustedProxies()". The "X-Forwarded-For" + * header value is a comma+space separated list of IP addresses, the left-most + * being the original client, and each successive proxy that passed the request + * adding the IP address where it received the request from. + * + * If your reverse proxy uses a different header name than "X-Forwarded-For", + * ("Client-Ip" for instance), configure it via the $trustedHeaderSet + * argument of the Request::setTrustedProxies() method instead. + * + * @return string|null + * + * @see getClientIps() + * @see https://wikipedia.org/wiki/X-Forwarded-For + */ + public function getClientIp() + { + $ipAddresses = $this->getClientIps(); + + return $ipAddresses[0]; + } + + /** + * Returns current script name. + * + * @return string + */ + public function getScriptName() + { + return $this->server->get('SCRIPT_NAME', $this->server->get('ORIG_SCRIPT_NAME', '')); + } + + /** + * Returns the path being requested relative to the executed script. + * + * The path info always starts with a /. + * + * Suppose this request is instantiated from /mysite on localhost: + * + * * http://localhost/mysite returns an empty string + * * http://localhost/mysite/about returns '/about' + * * http://localhost/mysite/enco%20ded returns '/enco%20ded' + * * http://localhost/mysite/about?var=1 returns '/about' + * + * @return string The raw path (i.e. not urldecoded) + */ + public function getPathInfo() + { + if (null === $this->pathInfo) { + $this->pathInfo = $this->preparePathInfo(); + } + + return $this->pathInfo; + } + + /** + * Returns the root path from which this request is executed. + * + * Suppose that an index.php file instantiates this request object: + * + * * http://localhost/index.php returns an empty string + * * http://localhost/index.php/page returns an empty string + * * http://localhost/web/index.php returns '/web' + * * http://localhost/we%20b/index.php returns '/we%20b' + * + * @return string The raw path (i.e. not urldecoded) + */ + public function getBasePath() + { + if (null === $this->basePath) { + $this->basePath = $this->prepareBasePath(); + } + + return $this->basePath; + } + + /** + * Returns the root URL from which this request is executed. + * + * The base URL never ends with a /. + * + * This is similar to getBasePath(), except that it also includes the + * script filename (e.g. index.php) if one exists. + * + * @return string The raw URL (i.e. not urldecoded) + */ + public function getBaseUrl() + { + $trustedPrefix = ''; + + // the proxy prefix must be prepended to any prefix being needed at the webserver level + if ($this->isFromTrustedProxy() && $trustedPrefixValues = $this->getTrustedValues(self::HEADER_X_FORWARDED_PREFIX)) { + $trustedPrefix = rtrim($trustedPrefixValues[0], '/'); + } + + return $trustedPrefix.$this->getBaseUrlReal(); + } + + /** + * Returns the real base URL received by the webserver from which this request is executed. + * The URL does not include trusted reverse proxy prefix. + * + * @return string The raw URL (i.e. not urldecoded) + */ + private function getBaseUrlReal(): string + { + if (null === $this->baseUrl) { + $this->baseUrl = $this->prepareBaseUrl(); + } + + return $this->baseUrl; + } + + /** + * Gets the request's scheme. + * + * @return string + */ + public function getScheme() + { + return $this->isSecure() ? 'https' : 'http'; + } + + /** + * Returns the port on which the request is made. + * + * This method can read the client port from the "X-Forwarded-Port" header + * when trusted proxies were set via "setTrustedProxies()". + * + * The "X-Forwarded-Port" header must contain the client port. + * + * @return int|string|null Can be a string if fetched from the server bag + */ + public function getPort() + { + if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_PORT)) { + $host = $host[0]; + } elseif ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_HOST)) { + $host = $host[0]; + } elseif (!$host = $this->headers->get('HOST')) { + return $this->server->get('SERVER_PORT'); + } + + if ('[' === $host[0]) { + $pos = strpos($host, ':', strrpos($host, ']')); + } else { + $pos = strrpos($host, ':'); + } + + if (false !== $pos && $port = substr($host, $pos + 1)) { + return (int) $port; + } + + return 'https' === $this->getScheme() ? 443 : 80; + } + + /** + * Returns the user. + * + * @return string|null + */ + public function getUser() + { + return $this->headers->get('PHP_AUTH_USER'); + } + + /** + * Returns the password. + * + * @return string|null + */ + public function getPassword() + { + return $this->headers->get('PHP_AUTH_PW'); + } + + /** + * Gets the user info. + * + * @return string|null A user name if any and, optionally, scheme-specific information about how to gain authorization to access the server + */ + public function getUserInfo() + { + $userinfo = $this->getUser(); + + $pass = $this->getPassword(); + if ('' != $pass) { + $userinfo .= ":$pass"; + } + + return $userinfo; + } + + /** + * Returns the HTTP host being requested. + * + * The port name will be appended to the host if it's non-standard. + * + * @return string + */ + public function getHttpHost() + { + $scheme = $this->getScheme(); + $port = $this->getPort(); + + if (('http' == $scheme && 80 == $port) || ('https' == $scheme && 443 == $port)) { + return $this->getHost(); + } + + return $this->getHost().':'.$port; + } + + /** + * Returns the requested URI (path and query string). + * + * @return string The raw URI (i.e. not URI decoded) + */ + public function getRequestUri() + { + if (null === $this->requestUri) { + $this->requestUri = $this->prepareRequestUri(); + } + + return $this->requestUri; + } + + /** + * Gets the scheme and HTTP host. + * + * If the URL was called with basic authentication, the user + * and the password are not added to the generated string. + * + * @return string + */ + public function getSchemeAndHttpHost() + { + return $this->getScheme().'://'.$this->getHttpHost(); + } + + /** + * Generates a normalized URI (URL) for the Request. + * + * @return string + * + * @see getQueryString() + */ + public function getUri() + { + if (null !== $qs = $this->getQueryString()) { + $qs = '?'.$qs; + } + + return $this->getSchemeAndHttpHost().$this->getBaseUrl().$this->getPathInfo().$qs; + } + + /** + * Generates a normalized URI for the given path. + * + * @param string $path A path to use instead of the current one + * + * @return string + */ + public function getUriForPath(string $path) + { + return $this->getSchemeAndHttpHost().$this->getBaseUrl().$path; + } + + /** + * Returns the path as relative reference from the current Request path. + * + * Only the URIs path component (no schema, host etc.) is relevant and must be given. + * Both paths must be absolute and not contain relative parts. + * Relative URLs from one resource to another are useful when generating self-contained downloadable document archives. + * Furthermore, they can be used to reduce the link size in documents. + * + * Example target paths, given a base path of "/a/b/c/d": + * - "/a/b/c/d" -> "" + * - "/a/b/c/" -> "./" + * - "/a/b/" -> "../" + * - "/a/b/c/other" -> "other" + * - "/a/x/y" -> "../../x/y" + * + * @return string + */ + public function getRelativeUriForPath(string $path) + { + // be sure that we are dealing with an absolute path + if (!isset($path[0]) || '/' !== $path[0]) { + return $path; + } + + if ($path === $basePath = $this->getPathInfo()) { + return ''; + } + + $sourceDirs = explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath, 1) : $basePath); + $targetDirs = explode('/', substr($path, 1)); + array_pop($sourceDirs); + $targetFile = array_pop($targetDirs); + + foreach ($sourceDirs as $i => $dir) { + if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) { + unset($sourceDirs[$i], $targetDirs[$i]); + } else { + break; + } + } + + $targetDirs[] = $targetFile; + $path = str_repeat('../', \count($sourceDirs)).implode('/', $targetDirs); + + // A reference to the same base directory or an empty subdirectory must be prefixed with "./". + // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used + // as the first segment of a relative-path reference, as it would be mistaken for a scheme name + // (see https://tools.ietf.org/html/rfc3986#section-4.2). + return !isset($path[0]) || '/' === $path[0] + || false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos) + ? "./$path" : $path; + } + + /** + * Generates the normalized query string for the Request. + * + * It builds a normalized query string, where keys/value pairs are alphabetized + * and have consistent escaping. + * + * @return string|null + */ + public function getQueryString() + { + $qs = static::normalizeQueryString($this->server->get('QUERY_STRING')); + + return '' === $qs ? null : $qs; + } + + /** + * Checks whether the request is secure or not. + * + * This method can read the client protocol from the "X-Forwarded-Proto" header + * when trusted proxies were set via "setTrustedProxies()". + * + * The "X-Forwarded-Proto" header must contain the protocol: "https" or "http". + * + * @return bool + */ + public function isSecure() + { + if ($this->isFromTrustedProxy() && $proto = $this->getTrustedValues(self::HEADER_X_FORWARDED_PROTO)) { + return \in_array(strtolower($proto[0]), ['https', 'on', 'ssl', '1'], true); + } + + $https = $this->server->get('HTTPS'); + + return !empty($https) && 'off' !== strtolower($https); + } + + /** + * Returns the host name. + * + * This method can read the client host name from the "X-Forwarded-Host" header + * when trusted proxies were set via "setTrustedProxies()". + * + * The "X-Forwarded-Host" header must contain the client host name. + * + * @return string + * + * @throws SuspiciousOperationException when the host name is invalid or not trusted + */ + public function getHost() + { + if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_HOST)) { + $host = $host[0]; + } elseif (!$host = $this->headers->get('HOST')) { + if (!$host = $this->server->get('SERVER_NAME')) { + $host = $this->server->get('SERVER_ADDR', ''); + } + } + + // trim and remove port number from host + // host is lowercase as per RFC 952/2181 + $host = strtolower(preg_replace('/:\d+$/', '', trim($host))); + + // as the host can come from the user (HTTP_HOST and depending on the configuration, SERVER_NAME too can come from the user) + // check that it does not contain forbidden characters (see RFC 952 and RFC 2181) + // use preg_replace() instead of preg_match() to prevent DoS attacks with long host names + if ($host && '' !== preg_replace('/(?:^\[)?[a-zA-Z0-9-:\]_]+\.?/', '', $host)) { + if (!$this->isHostValid) { + return ''; + } + $this->isHostValid = false; + + throw new SuspiciousOperationException(sprintf('Invalid Host "%s".', $host)); + } + + if (\count(self::$trustedHostPatterns) > 0) { + // to avoid host header injection attacks, you should provide a list of trusted host patterns + + if (\in_array($host, self::$trustedHosts)) { + return $host; + } + + foreach (self::$trustedHostPatterns as $pattern) { + if (preg_match($pattern, $host)) { + self::$trustedHosts[] = $host; + + return $host; + } + } + + if (!$this->isHostValid) { + return ''; + } + $this->isHostValid = false; + + throw new SuspiciousOperationException(sprintf('Untrusted Host "%s".', $host)); + } + + return $host; + } + + /** + * Sets the request method. + */ + public function setMethod(string $method) + { + $this->method = null; + $this->server->set('REQUEST_METHOD', $method); + } + + /** + * Gets the request "intended" method. + * + * If the X-HTTP-Method-Override header is set, and if the method is a POST, + * then it is used to determine the "real" intended HTTP method. + * + * The _method request parameter can also be used to determine the HTTP method, + * but only if enableHttpMethodParameterOverride() has been called. + * + * The method is always an uppercased string. + * + * @return string + * + * @see getRealMethod() + */ + public function getMethod() + { + if (null !== $this->method) { + return $this->method; + } + + $this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET')); + + if ('POST' !== $this->method) { + return $this->method; + } + + $method = $this->headers->get('X-HTTP-METHOD-OVERRIDE'); + + if (!$method && self::$httpMethodParameterOverride) { + $method = $this->request->get('_method', $this->query->get('_method', 'POST')); + } + + if (!\is_string($method)) { + return $this->method; + } + + $method = strtoupper($method); + + if (\in_array($method, ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'PATCH', 'PURGE', 'TRACE'], true)) { + return $this->method = $method; + } + + if (!preg_match('/^[A-Z]++$/D', $method)) { + throw new SuspiciousOperationException(sprintf('Invalid method override "%s".', $method)); + } + + return $this->method = $method; + } + + /** + * Gets the "real" request method. + * + * @return string + * + * @see getMethod() + */ + public function getRealMethod() + { + return strtoupper($this->server->get('REQUEST_METHOD', 'GET')); + } + + /** + * Gets the mime type associated with the format. + * + * @return string|null + */ + public function getMimeType(string $format) + { + if (null === static::$formats) { + static::initializeFormats(); + } + + return isset(static::$formats[$format]) ? static::$formats[$format][0] : null; + } + + /** + * Gets the mime types associated with the format. + * + * @return array + */ + public static function getMimeTypes(string $format) + { + if (null === static::$formats) { + static::initializeFormats(); + } + + return static::$formats[$format] ?? []; + } + + /** + * Gets the format associated with the mime type. + * + * @return string|null + */ + public function getFormat(?string $mimeType) + { + $canonicalMimeType = null; + if ($mimeType && false !== $pos = strpos($mimeType, ';')) { + $canonicalMimeType = trim(substr($mimeType, 0, $pos)); + } + + if (null === static::$formats) { + static::initializeFormats(); + } + + foreach (static::$formats as $format => $mimeTypes) { + if (\in_array($mimeType, (array) $mimeTypes)) { + return $format; + } + if (null !== $canonicalMimeType && \in_array($canonicalMimeType, (array) $mimeTypes)) { + return $format; + } + } + + return null; + } + + /** + * Associates a format with mime types. + * + * @param string|array $mimeTypes The associated mime types (the preferred one must be the first as it will be used as the content type) + */ + public function setFormat(?string $format, $mimeTypes) + { + if (null === static::$formats) { + static::initializeFormats(); + } + + static::$formats[$format] = \is_array($mimeTypes) ? $mimeTypes : [$mimeTypes]; + } + + /** + * Gets the request format. + * + * Here is the process to determine the format: + * + * * format defined by the user (with setRequestFormat()) + * * _format request attribute + * * $default + * + * @see getPreferredFormat + * + * @return string|null + */ + public function getRequestFormat(?string $default = 'html') + { + if (null === $this->format) { + $this->format = $this->attributes->get('_format'); + } + + return $this->format ?? $default; + } + + /** + * Sets the request format. + */ + public function setRequestFormat(?string $format) + { + $this->format = $format; + } + + /** + * Gets the format associated with the request. + * + * @return string|null + */ + public function getContentType() + { + return $this->getFormat($this->headers->get('CONTENT_TYPE', '')); + } + + /** + * Sets the default locale. + */ + public function setDefaultLocale(string $locale) + { + $this->defaultLocale = $locale; + + if (null === $this->locale) { + $this->setPhpDefaultLocale($locale); + } + } + + /** + * Get the default locale. + * + * @return string + */ + public function getDefaultLocale() + { + return $this->defaultLocale; + } + + /** + * Sets the locale. + */ + public function setLocale(string $locale) + { + $this->setPhpDefaultLocale($this->locale = $locale); + } + + /** + * Get the locale. + * + * @return string + */ + public function getLocale() + { + return $this->locale ?? $this->defaultLocale; + } + + /** + * Checks if the request method is of specified type. + * + * @param string $method Uppercase request method (GET, POST etc) + * + * @return bool + */ + public function isMethod(string $method) + { + return $this->getMethod() === strtoupper($method); + } + + /** + * Checks whether or not the method is safe. + * + * @see https://tools.ietf.org/html/rfc7231#section-4.2.1 + * + * @return bool + */ + public function isMethodSafe() + { + return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE']); + } + + /** + * Checks whether or not the method is idempotent. + * + * @return bool + */ + public function isMethodIdempotent() + { + return \in_array($this->getMethod(), ['HEAD', 'GET', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'PURGE']); + } + + /** + * Checks whether the method is cacheable or not. + * + * @see https://tools.ietf.org/html/rfc7231#section-4.2.3 + * + * @return bool + */ + public function isMethodCacheable() + { + return \in_array($this->getMethod(), ['GET', 'HEAD']); + } + + /** + * Returns the protocol version. + * + * If the application is behind a proxy, the protocol version used in the + * requests between the client and the proxy and between the proxy and the + * server might be different. This returns the former (from the "Via" header) + * if the proxy is trusted (see "setTrustedProxies()"), otherwise it returns + * the latter (from the "SERVER_PROTOCOL" server parameter). + * + * @return string|null + */ + public function getProtocolVersion() + { + if ($this->isFromTrustedProxy()) { + preg_match('~^(HTTP/)?([1-9]\.[0-9]) ~', $this->headers->get('Via') ?? '', $matches); + + if ($matches) { + return 'HTTP/'.$matches[2]; + } + } + + return $this->server->get('SERVER_PROTOCOL'); + } + + /** + * Returns the request body content. + * + * @param bool $asResource If true, a resource will be returned + * + * @return string|resource + */ + public function getContent(bool $asResource = false) + { + $currentContentIsResource = \is_resource($this->content); + + if (true === $asResource) { + if ($currentContentIsResource) { + rewind($this->content); + + return $this->content; + } + + // Content passed in parameter (test) + if (\is_string($this->content)) { + $resource = fopen('php://temp', 'r+'); + fwrite($resource, $this->content); + rewind($resource); + + return $resource; + } + + $this->content = false; + + return fopen('php://input', 'r'); + } + + if ($currentContentIsResource) { + rewind($this->content); + + return stream_get_contents($this->content); + } + + if (null === $this->content || false === $this->content) { + $this->content = file_get_contents('php://input'); + } + + return $this->content; + } + + /** + * Gets the request body decoded as array, typically from a JSON payload. + * + * @return array + * + * @throws JsonException When the body cannot be decoded to an array + */ + public function toArray() + { + if ('' === $content = $this->getContent()) { + throw new JsonException('Request body is empty.'); + } + + try { + $content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0)); + } catch (\JsonException $e) { + throw new JsonException('Could not decode request body.', $e->getCode(), $e); + } + + if (\PHP_VERSION_ID < 70300 && \JSON_ERROR_NONE !== json_last_error()) { + throw new JsonException('Could not decode request body: '.json_last_error_msg(), json_last_error()); + } + + if (!\is_array($content)) { + throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned.', get_debug_type($content))); + } + + return $content; + } + + /** + * Gets the Etags. + * + * @return array + */ + public function getETags() + { + return preg_split('/\s*,\s*/', $this->headers->get('If-None-Match', ''), -1, \PREG_SPLIT_NO_EMPTY); + } + + /** + * @return bool + */ + public function isNoCache() + { + return $this->headers->hasCacheControlDirective('no-cache') || 'no-cache' == $this->headers->get('Pragma'); + } + + /** + * Gets the preferred format for the response by inspecting, in the following order: + * * the request format set using setRequestFormat; + * * the values of the Accept HTTP header. + * + * Note that if you use this method, you should send the "Vary: Accept" header + * in the response to prevent any issues with intermediary HTTP caches. + */ + public function getPreferredFormat(?string $default = 'html'): ?string + { + if (null !== $this->preferredFormat || null !== $this->preferredFormat = $this->getRequestFormat(null)) { + return $this->preferredFormat; + } + + foreach ($this->getAcceptableContentTypes() as $mimeType) { + if ($this->preferredFormat = $this->getFormat($mimeType)) { + return $this->preferredFormat; + } + } + + return $default; + } + + /** + * Returns the preferred language. + * + * @param string[] $locales An array of ordered available locales + * + * @return string|null + */ + public function getPreferredLanguage(?array $locales = null) + { + $preferredLanguages = $this->getLanguages(); + + if (empty($locales)) { + return $preferredLanguages[0] ?? null; + } + + if (!$preferredLanguages) { + return $locales[0]; + } + + $extendedPreferredLanguages = []; + foreach ($preferredLanguages as $language) { + $extendedPreferredLanguages[] = $language; + if (false !== $position = strpos($language, '_')) { + $superLanguage = substr($language, 0, $position); + if (!\in_array($superLanguage, $preferredLanguages)) { + $extendedPreferredLanguages[] = $superLanguage; + } + } + } + + $preferredLanguages = array_values(array_intersect($extendedPreferredLanguages, $locales)); + + return $preferredLanguages[0] ?? $locales[0]; + } + + /** + * Gets a list of languages acceptable by the client browser ordered in the user browser preferences. + * + * @return array + */ + public function getLanguages() + { + if (null !== $this->languages) { + return $this->languages; + } + + $languages = AcceptHeader::fromString($this->headers->get('Accept-Language'))->all(); + $this->languages = []; + foreach ($languages as $acceptHeaderItem) { + $lang = $acceptHeaderItem->getValue(); + if (str_contains($lang, '-')) { + $codes = explode('-', $lang); + if ('i' === $codes[0]) { + // Language not listed in ISO 639 that are not variants + // of any listed language, which can be registered with the + // i-prefix, such as i-cherokee + if (\count($codes) > 1) { + $lang = $codes[1]; + } + } else { + for ($i = 0, $max = \count($codes); $i < $max; ++$i) { + if (0 === $i) { + $lang = strtolower($codes[0]); + } else { + $lang .= '_'.strtoupper($codes[$i]); + } + } + } + } + + $this->languages[] = $lang; + } + + return $this->languages; + } + + /** + * Gets a list of charsets acceptable by the client browser in preferable order. + * + * @return array + */ + public function getCharsets() + { + if (null !== $this->charsets) { + return $this->charsets; + } + + return $this->charsets = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Charset'))->all())); + } + + /** + * Gets a list of encodings acceptable by the client browser in preferable order. + * + * @return array + */ + public function getEncodings() + { + if (null !== $this->encodings) { + return $this->encodings; + } + + return $this->encodings = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Encoding'))->all())); + } + + /** + * Gets a list of content types acceptable by the client browser in preferable order. + * + * @return array + */ + public function getAcceptableContentTypes() + { + if (null !== $this->acceptableContentTypes) { + return $this->acceptableContentTypes; + } + + return $this->acceptableContentTypes = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept'))->all())); + } + + /** + * Returns true if the request is an XMLHttpRequest. + * + * It works if your JavaScript library sets an X-Requested-With HTTP header. + * It is known to work with common JavaScript frameworks: + * + * @see https://wikipedia.org/wiki/List_of_Ajax_frameworks#JavaScript + * + * @return bool + */ + public function isXmlHttpRequest() + { + return 'XMLHttpRequest' == $this->headers->get('X-Requested-With'); + } + + /** + * Checks whether the client browser prefers safe content or not according to RFC8674. + * + * @see https://tools.ietf.org/html/rfc8674 + */ + public function preferSafeContent(): bool + { + if (null !== $this->isSafeContentPreferred) { + return $this->isSafeContentPreferred; + } + + if (!$this->isSecure()) { + // see https://tools.ietf.org/html/rfc8674#section-3 + return $this->isSafeContentPreferred = false; + } + + return $this->isSafeContentPreferred = AcceptHeader::fromString($this->headers->get('Prefer'))->has('safe'); + } + + /* + * The following methods are derived from code of the Zend Framework (1.10dev - 2010-01-24) + * + * Code subject to the new BSD license (https://framework.zend.com/license). + * + * Copyright (c) 2005-2010 Zend Technologies USA Inc. (https://www.zend.com/) + */ + + protected function prepareRequestUri() + { + $requestUri = ''; + + if ($this->isIisRewrite() && '' != $this->server->get('UNENCODED_URL')) { + // IIS7 with URL Rewrite: make sure we get the unencoded URL (double slash problem) + $requestUri = $this->server->get('UNENCODED_URL'); + $this->server->remove('UNENCODED_URL'); + } elseif ($this->server->has('REQUEST_URI')) { + $requestUri = $this->server->get('REQUEST_URI'); + + if ('' !== $requestUri && '/' === $requestUri[0]) { + // To only use path and query remove the fragment. + if (false !== $pos = strpos($requestUri, '#')) { + $requestUri = substr($requestUri, 0, $pos); + } + } else { + // HTTP proxy reqs setup request URI with scheme and host [and port] + the URL path, + // only use URL path. + $uriComponents = parse_url($requestUri); + + if (isset($uriComponents['path'])) { + $requestUri = $uriComponents['path']; + } + + if (isset($uriComponents['query'])) { + $requestUri .= '?'.$uriComponents['query']; + } + } + } elseif ($this->server->has('ORIG_PATH_INFO')) { + // IIS 5.0, PHP as CGI + $requestUri = $this->server->get('ORIG_PATH_INFO'); + if ('' != $this->server->get('QUERY_STRING')) { + $requestUri .= '?'.$this->server->get('QUERY_STRING'); + } + $this->server->remove('ORIG_PATH_INFO'); + } + + // normalize the request URI to ease creating sub-requests from this request + $this->server->set('REQUEST_URI', $requestUri); + + return $requestUri; + } + + /** + * Prepares the base URL. + * + * @return string + */ + protected function prepareBaseUrl() + { + $filename = basename($this->server->get('SCRIPT_FILENAME', '')); + + if (basename($this->server->get('SCRIPT_NAME', '')) === $filename) { + $baseUrl = $this->server->get('SCRIPT_NAME'); + } elseif (basename($this->server->get('PHP_SELF', '')) === $filename) { + $baseUrl = $this->server->get('PHP_SELF'); + } elseif (basename($this->server->get('ORIG_SCRIPT_NAME', '')) === $filename) { + $baseUrl = $this->server->get('ORIG_SCRIPT_NAME'); // 1and1 shared hosting compatibility + } else { + // Backtrack up the script_filename to find the portion matching + // php_self + $path = $this->server->get('PHP_SELF', ''); + $file = $this->server->get('SCRIPT_FILENAME', ''); + $segs = explode('/', trim($file, '/')); + $segs = array_reverse($segs); + $index = 0; + $last = \count($segs); + $baseUrl = ''; + do { + $seg = $segs[$index]; + $baseUrl = '/'.$seg.$baseUrl; + ++$index; + } while ($last > $index && (false !== $pos = strpos($path, $baseUrl)) && 0 != $pos); + } + + // Does the baseUrl have anything in common with the request_uri? + $requestUri = $this->getRequestUri(); + if ('' !== $requestUri && '/' !== $requestUri[0]) { + $requestUri = '/'.$requestUri; + } + + if ($baseUrl && null !== $prefix = $this->getUrlencodedPrefix($requestUri, $baseUrl)) { + // full $baseUrl matches + return $prefix; + } + + if ($baseUrl && null !== $prefix = $this->getUrlencodedPrefix($requestUri, rtrim(\dirname($baseUrl), '/'.\DIRECTORY_SEPARATOR).'/')) { + // directory portion of $baseUrl matches + return rtrim($prefix, '/'.\DIRECTORY_SEPARATOR); + } + + $truncatedRequestUri = $requestUri; + if (false !== $pos = strpos($requestUri, '?')) { + $truncatedRequestUri = substr($requestUri, 0, $pos); + } + + $basename = basename($baseUrl ?? ''); + if (empty($basename) || !strpos(rawurldecode($truncatedRequestUri), $basename)) { + // no match whatsoever; set it blank + return ''; + } + + // If using mod_rewrite or ISAPI_Rewrite strip the script filename + // out of baseUrl. $pos !== 0 makes sure it is not matching a value + // from PATH_INFO or QUERY_STRING + if (\strlen($requestUri) >= \strlen($baseUrl) && (false !== $pos = strpos($requestUri, $baseUrl)) && 0 !== $pos) { + $baseUrl = substr($requestUri, 0, $pos + \strlen($baseUrl)); + } + + return rtrim($baseUrl, '/'.\DIRECTORY_SEPARATOR); + } + + /** + * Prepares the base path. + * + * @return string + */ + protected function prepareBasePath() + { + $baseUrl = $this->getBaseUrl(); + if (empty($baseUrl)) { + return ''; + } + + $filename = basename($this->server->get('SCRIPT_FILENAME')); + if (basename($baseUrl) === $filename) { + $basePath = \dirname($baseUrl); + } else { + $basePath = $baseUrl; + } + + if ('\\' === \DIRECTORY_SEPARATOR) { + $basePath = str_replace('\\', '/', $basePath); + } + + return rtrim($basePath, '/'); + } + + /** + * Prepares the path info. + * + * @return string + */ + protected function preparePathInfo() + { + if (null === ($requestUri = $this->getRequestUri())) { + return '/'; + } + + // Remove the query string from REQUEST_URI + if (false !== $pos = strpos($requestUri, '?')) { + $requestUri = substr($requestUri, 0, $pos); + } + if ('' !== $requestUri && '/' !== $requestUri[0]) { + $requestUri = '/'.$requestUri; + } + + if (null === ($baseUrl = $this->getBaseUrlReal())) { + return $requestUri; + } + + $pathInfo = substr($requestUri, \strlen($baseUrl)); + if (false === $pathInfo || '' === $pathInfo) { + // If substr() returns false then PATH_INFO is set to an empty string + return '/'; + } + + return $pathInfo; + } + + /** + * Initializes HTTP request formats. + */ + protected static function initializeFormats() + { + static::$formats = [ + 'html' => ['text/html', 'application/xhtml+xml'], + 'txt' => ['text/plain'], + 'js' => ['application/javascript', 'application/x-javascript', 'text/javascript'], + 'css' => ['text/css'], + 'json' => ['application/json', 'application/x-json'], + 'jsonld' => ['application/ld+json'], + 'xml' => ['text/xml', 'application/xml', 'application/x-xml'], + 'rdf' => ['application/rdf+xml'], + 'atom' => ['application/atom+xml'], + 'rss' => ['application/rss+xml'], + 'form' => ['application/x-www-form-urlencoded', 'multipart/form-data'], + ]; + } + + private function setPhpDefaultLocale(string $locale): void + { + // if either the class Locale doesn't exist, or an exception is thrown when + // setting the default locale, the intl module is not installed, and + // the call can be ignored: + try { + if (class_exists(\Locale::class, false)) { + \Locale::setDefault($locale); + } + } catch (\Exception $e) { + } + } + + /** + * Returns the prefix as encoded in the string when the string starts with + * the given prefix, null otherwise. + */ + private function getUrlencodedPrefix(string $string, string $prefix): ?string + { + if ($this->isIisRewrite()) { + // ISS with UrlRewriteModule might report SCRIPT_NAME/PHP_SELF with wrong case + // see https://github.com/php/php-src/issues/11981 + if (0 !== stripos(rawurldecode($string), $prefix)) { + return null; + } + } elseif (!str_starts_with(rawurldecode($string), $prefix)) { + return null; + } + + $len = \strlen($prefix); + + if (preg_match(sprintf('#^(%%[[:xdigit:]]{2}|.){%d}#', $len), $string, $match)) { + return $match[0]; + } + + return null; + } + + private static function createRequestFromFactory(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null): self + { + if (self::$requestFactory) { + $request = (self::$requestFactory)($query, $request, $attributes, $cookies, $files, $server, $content); + + if (!$request instanceof self) { + throw new \LogicException('The Request factory must return an instance of Symfony\Component\HttpFoundation\Request.'); + } + + return $request; + } + + return new static($query, $request, $attributes, $cookies, $files, $server, $content); + } + + /** + * Indicates whether this request originated from a trusted proxy. + * + * This can be useful to determine whether or not to trust the + * contents of a proxy-specific header. + * + * @return bool + */ + public function isFromTrustedProxy() + { + return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR', ''), self::$trustedProxies); + } + + private function getTrustedValues(int $type, ?string $ip = null): array + { + $clientValues = []; + $forwardedValues = []; + + if ((self::$trustedHeaderSet & $type) && $this->headers->has(self::TRUSTED_HEADERS[$type])) { + foreach (explode(',', $this->headers->get(self::TRUSTED_HEADERS[$type])) as $v) { + $clientValues[] = (self::HEADER_X_FORWARDED_PORT === $type ? '0.0.0.0:' : '').trim($v); + } + } + + if ((self::$trustedHeaderSet & self::HEADER_FORWARDED) && (isset(self::FORWARDED_PARAMS[$type])) && $this->headers->has(self::TRUSTED_HEADERS[self::HEADER_FORWARDED])) { + $forwarded = $this->headers->get(self::TRUSTED_HEADERS[self::HEADER_FORWARDED]); + $parts = HeaderUtils::split($forwarded, ',;='); + $forwardedValues = []; + $param = self::FORWARDED_PARAMS[$type]; + foreach ($parts as $subParts) { + if (null === $v = HeaderUtils::combine($subParts)[$param] ?? null) { + continue; + } + if (self::HEADER_X_FORWARDED_PORT === $type) { + if (str_ends_with($v, ']') || false === $v = strrchr($v, ':')) { + $v = $this->isSecure() ? ':443' : ':80'; + } + $v = '0.0.0.0'.$v; + } + $forwardedValues[] = $v; + } + } + + if (null !== $ip) { + $clientValues = $this->normalizeAndFilterClientIps($clientValues, $ip); + $forwardedValues = $this->normalizeAndFilterClientIps($forwardedValues, $ip); + } + + if ($forwardedValues === $clientValues || !$clientValues) { + return $forwardedValues; + } + + if (!$forwardedValues) { + return $clientValues; + } + + if (!$this->isForwardedValid) { + return null !== $ip ? ['0.0.0.0', $ip] : []; + } + $this->isForwardedValid = false; + + throw new ConflictingHeadersException(sprintf('The request has both a trusted "%s" header and a trusted "%s" header, conflicting with each other. You should either configure your proxy to remove one of them, or configure your project to distrust the offending one.', self::TRUSTED_HEADERS[self::HEADER_FORWARDED], self::TRUSTED_HEADERS[$type])); + } + + private function normalizeAndFilterClientIps(array $clientIps, string $ip): array + { + if (!$clientIps) { + return []; + } + $clientIps[] = $ip; // Complete the IP chain with the IP the request actually came from + $firstTrustedIp = null; + + foreach ($clientIps as $key => $clientIp) { + if (strpos($clientIp, '.')) { + // Strip :port from IPv4 addresses. This is allowed in Forwarded + // and may occur in X-Forwarded-For. + $i = strpos($clientIp, ':'); + if ($i) { + $clientIps[$key] = $clientIp = substr($clientIp, 0, $i); + } + } elseif (str_starts_with($clientIp, '[')) { + // Strip brackets and :port from IPv6 addresses. + $i = strpos($clientIp, ']', 1); + $clientIps[$key] = $clientIp = substr($clientIp, 1, $i - 1); + } + + if (!filter_var($clientIp, \FILTER_VALIDATE_IP)) { + unset($clientIps[$key]); + + continue; + } + + if (IpUtils::checkIp($clientIp, self::$trustedProxies)) { + unset($clientIps[$key]); + + // Fallback to this when the client IP falls into the range of trusted proxies + if (null === $firstTrustedIp) { + $firstTrustedIp = $clientIp; + } + } + } + + // Now the IP chain contains only untrusted proxies and the client IP + return $clientIps ? array_reverse($clientIps) : [$firstTrustedIp]; + } + + /** + * Is this IIS with UrlRewriteModule? + * + * This method consumes, caches and removed the IIS_WasUrlRewritten env var, + * so we don't inherit it to sub-requests. + */ + private function isIisRewrite(): bool + { + if (1 === $this->server->getInt('IIS_WasUrlRewritten')) { + $this->isIisRewrite = true; + $this->server->remove('IIS_WasUrlRewritten'); + } + + return $this->isIisRewrite; + } +} diff --git a/vendor/symfony/http-foundation/RequestMatcher.php b/vendor/symfony/http-foundation/RequestMatcher.php new file mode 100644 index 0000000..03ccee9 --- /dev/null +++ b/vendor/symfony/http-foundation/RequestMatcher.php @@ -0,0 +1,196 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * RequestMatcher compares a pre-defined set of checks against a Request instance. + * + * @author Fabien Potencier + */ +class RequestMatcher implements RequestMatcherInterface +{ + /** + * @var string|null + */ + private $path; + + /** + * @var string|null + */ + private $host; + + /** + * @var int|null + */ + private $port; + + /** + * @var string[] + */ + private $methods = []; + + /** + * @var string[] + */ + private $ips = []; + + /** + * @var array + */ + private $attributes = []; + + /** + * @var string[] + */ + private $schemes = []; + + /** + * @param string|string[]|null $methods + * @param string|string[]|null $ips + * @param string|string[]|null $schemes + */ + public function __construct(?string $path = null, ?string $host = null, $methods = null, $ips = null, array $attributes = [], $schemes = null, ?int $port = null) + { + $this->matchPath($path); + $this->matchHost($host); + $this->matchMethod($methods); + $this->matchIps($ips); + $this->matchScheme($schemes); + $this->matchPort($port); + + foreach ($attributes as $k => $v) { + $this->matchAttribute($k, $v); + } + } + + /** + * Adds a check for the HTTP scheme. + * + * @param string|string[]|null $scheme An HTTP scheme or an array of HTTP schemes + */ + public function matchScheme($scheme) + { + $this->schemes = null !== $scheme ? array_map('strtolower', (array) $scheme) : []; + } + + /** + * Adds a check for the URL host name. + */ + public function matchHost(?string $regexp) + { + $this->host = $regexp; + } + + /** + * Adds a check for the URL port. + * + * @param int|null $port The port number to connect to + */ + public function matchPort(?int $port) + { + $this->port = $port; + } + + /** + * Adds a check for the URL path info. + */ + public function matchPath(?string $regexp) + { + $this->path = $regexp; + } + + /** + * Adds a check for the client IP. + * + * @param string $ip A specific IP address or a range specified using IP/netmask like 192.168.1.0/24 + */ + public function matchIp(string $ip) + { + $this->matchIps($ip); + } + + /** + * Adds a check for the client IP. + * + * @param string|string[]|null $ips A specific IP address or a range specified using IP/netmask like 192.168.1.0/24 + */ + public function matchIps($ips) + { + $ips = null !== $ips ? (array) $ips : []; + + $this->ips = array_reduce($ips, static function (array $ips, string $ip) { + return array_merge($ips, preg_split('/\s*,\s*/', $ip)); + }, []); + } + + /** + * Adds a check for the HTTP method. + * + * @param string|string[]|null $method An HTTP method or an array of HTTP methods + */ + public function matchMethod($method) + { + $this->methods = null !== $method ? array_map('strtoupper', (array) $method) : []; + } + + /** + * Adds a check for request attribute. + */ + public function matchAttribute(string $key, string $regexp) + { + $this->attributes[$key] = $regexp; + } + + /** + * {@inheritdoc} + */ + public function matches(Request $request) + { + if ($this->schemes && !\in_array($request->getScheme(), $this->schemes, true)) { + return false; + } + + if ($this->methods && !\in_array($request->getMethod(), $this->methods, true)) { + return false; + } + + foreach ($this->attributes as $key => $pattern) { + $requestAttribute = $request->attributes->get($key); + if (!\is_string($requestAttribute)) { + return false; + } + if (!preg_match('{'.$pattern.'}', $requestAttribute)) { + return false; + } + } + + if (null !== $this->path && !preg_match('{'.$this->path.'}', rawurldecode($request->getPathInfo()))) { + return false; + } + + if (null !== $this->host && !preg_match('{'.$this->host.'}i', $request->getHost())) { + return false; + } + + if (null !== $this->port && 0 < $this->port && $request->getPort() !== $this->port) { + return false; + } + + if (IpUtils::checkIp($request->getClientIp() ?? '', $this->ips)) { + return true; + } + + // Note to future implementors: add additional checks above the + // foreach above or else your check might not be run! + return 0 === \count($this->ips); + } +} diff --git a/vendor/symfony/http-foundation/RequestMatcherInterface.php b/vendor/symfony/http-foundation/RequestMatcherInterface.php new file mode 100644 index 0000000..c2e1478 --- /dev/null +++ b/vendor/symfony/http-foundation/RequestMatcherInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * RequestMatcherInterface is an interface for strategies to match a Request. + * + * @author Fabien Potencier + */ +interface RequestMatcherInterface +{ + /** + * Decides whether the rule(s) implemented by the strategy matches the supplied request. + * + * @return bool + */ + public function matches(Request $request); +} diff --git a/vendor/symfony/http-foundation/RequestStack.php b/vendor/symfony/http-foundation/RequestStack.php new file mode 100644 index 0000000..855b518 --- /dev/null +++ b/vendor/symfony/http-foundation/RequestStack.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; +use Symfony\Component\HttpFoundation\Session\SessionInterface; + +/** + * Request stack that controls the lifecycle of requests. + * + * @author Benjamin Eberlei + */ +class RequestStack +{ + /** + * @var Request[] + */ + private $requests = []; + + /** + * Pushes a Request on the stack. + * + * This method should generally not be called directly as the stack + * management should be taken care of by the application itself. + */ + public function push(Request $request) + { + $this->requests[] = $request; + } + + /** + * Pops the current request from the stack. + * + * This operation lets the current request go out of scope. + * + * This method should generally not be called directly as the stack + * management should be taken care of by the application itself. + * + * @return Request|null + */ + public function pop() + { + if (!$this->requests) { + return null; + } + + return array_pop($this->requests); + } + + /** + * @return Request|null + */ + public function getCurrentRequest() + { + return end($this->requests) ?: null; + } + + /** + * Gets the main request. + * + * Be warned that making your code aware of the main request + * might make it un-compatible with other features of your framework + * like ESI support. + */ + public function getMainRequest(): ?Request + { + if (!$this->requests) { + return null; + } + + return $this->requests[0]; + } + + /** + * Gets the master request. + * + * @return Request|null + * + * @deprecated since symfony/http-foundation 5.3, use getMainRequest() instead + */ + public function getMasterRequest() + { + trigger_deprecation('symfony/http-foundation', '5.3', '"%s()" is deprecated, use "getMainRequest()" instead.', __METHOD__); + + return $this->getMainRequest(); + } + + /** + * Returns the parent request of the current. + * + * Be warned that making your code aware of the parent request + * might make it un-compatible with other features of your framework + * like ESI support. + * + * If current Request is the main request, it returns null. + * + * @return Request|null + */ + public function getParentRequest() + { + $pos = \count($this->requests) - 2; + + return $this->requests[$pos] ?? null; + } + + /** + * Gets the current session. + * + * @throws SessionNotFoundException + */ + public function getSession(): SessionInterface + { + if ((null !== $request = end($this->requests) ?: null) && $request->hasSession()) { + return $request->getSession(); + } + + throw new SessionNotFoundException(); + } +} diff --git a/vendor/symfony/http-foundation/Response.php b/vendor/symfony/http-foundation/Response.php new file mode 100644 index 0000000..6798a04 --- /dev/null +++ b/vendor/symfony/http-foundation/Response.php @@ -0,0 +1,1288 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +// Help opcache.preload discover always-needed symbols +class_exists(ResponseHeaderBag::class); + +/** + * Response represents an HTTP response. + * + * @author Fabien Potencier + */ +class Response +{ + public const HTTP_CONTINUE = 100; + public const HTTP_SWITCHING_PROTOCOLS = 101; + public const HTTP_PROCESSING = 102; // RFC2518 + public const HTTP_EARLY_HINTS = 103; // RFC8297 + public const HTTP_OK = 200; + public const HTTP_CREATED = 201; + public const HTTP_ACCEPTED = 202; + public const HTTP_NON_AUTHORITATIVE_INFORMATION = 203; + public const HTTP_NO_CONTENT = 204; + public const HTTP_RESET_CONTENT = 205; + public const HTTP_PARTIAL_CONTENT = 206; + public const HTTP_MULTI_STATUS = 207; // RFC4918 + public const HTTP_ALREADY_REPORTED = 208; // RFC5842 + public const HTTP_IM_USED = 226; // RFC3229 + public const HTTP_MULTIPLE_CHOICES = 300; + public const HTTP_MOVED_PERMANENTLY = 301; + public const HTTP_FOUND = 302; + public const HTTP_SEE_OTHER = 303; + public const HTTP_NOT_MODIFIED = 304; + public const HTTP_USE_PROXY = 305; + public const HTTP_RESERVED = 306; + public const HTTP_TEMPORARY_REDIRECT = 307; + public const HTTP_PERMANENTLY_REDIRECT = 308; // RFC7238 + public const HTTP_BAD_REQUEST = 400; + public const HTTP_UNAUTHORIZED = 401; + public const HTTP_PAYMENT_REQUIRED = 402; + public const HTTP_FORBIDDEN = 403; + public const HTTP_NOT_FOUND = 404; + public const HTTP_METHOD_NOT_ALLOWED = 405; + public const HTTP_NOT_ACCEPTABLE = 406; + public const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407; + public const HTTP_REQUEST_TIMEOUT = 408; + public const HTTP_CONFLICT = 409; + public const HTTP_GONE = 410; + public const HTTP_LENGTH_REQUIRED = 411; + public const HTTP_PRECONDITION_FAILED = 412; + public const HTTP_REQUEST_ENTITY_TOO_LARGE = 413; + public const HTTP_REQUEST_URI_TOO_LONG = 414; + public const HTTP_UNSUPPORTED_MEDIA_TYPE = 415; + public const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; + public const HTTP_EXPECTATION_FAILED = 417; + public const HTTP_I_AM_A_TEAPOT = 418; // RFC2324 + public const HTTP_MISDIRECTED_REQUEST = 421; // RFC7540 + public const HTTP_UNPROCESSABLE_ENTITY = 422; // RFC4918 + public const HTTP_LOCKED = 423; // RFC4918 + public const HTTP_FAILED_DEPENDENCY = 424; // RFC4918 + public const HTTP_TOO_EARLY = 425; // RFC-ietf-httpbis-replay-04 + public const HTTP_UPGRADE_REQUIRED = 426; // RFC2817 + public const HTTP_PRECONDITION_REQUIRED = 428; // RFC6585 + public const HTTP_TOO_MANY_REQUESTS = 429; // RFC6585 + public const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431; // RFC6585 + public const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451; // RFC7725 + public const HTTP_INTERNAL_SERVER_ERROR = 500; + public const HTTP_NOT_IMPLEMENTED = 501; + public const HTTP_BAD_GATEWAY = 502; + public const HTTP_SERVICE_UNAVAILABLE = 503; + public const HTTP_GATEWAY_TIMEOUT = 504; + public const HTTP_VERSION_NOT_SUPPORTED = 505; + public const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506; // RFC2295 + public const HTTP_INSUFFICIENT_STORAGE = 507; // RFC4918 + public const HTTP_LOOP_DETECTED = 508; // RFC5842 + public const HTTP_NOT_EXTENDED = 510; // RFC2774 + public const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511; // RFC6585 + + /** + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control + */ + private const HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES = [ + 'must_revalidate' => false, + 'no_cache' => false, + 'no_store' => false, + 'no_transform' => false, + 'public' => false, + 'private' => false, + 'proxy_revalidate' => false, + 'max_age' => true, + 's_maxage' => true, + 'immutable' => false, + 'last_modified' => true, + 'etag' => true, + ]; + + /** + * @var ResponseHeaderBag + */ + public $headers; + + /** + * @var string + */ + protected $content; + + /** + * @var string + */ + protected $version; + + /** + * @var int + */ + protected $statusCode; + + /** + * @var string + */ + protected $statusText; + + /** + * @var string + */ + protected $charset; + + /** + * Status codes translation table. + * + * The list of codes is complete according to the + * {@link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml Hypertext Transfer Protocol (HTTP) Status Code Registry} + * (last updated 2021-10-01). + * + * Unless otherwise noted, the status code is defined in RFC2616. + * + * @var array + */ + public static $statusTexts = [ + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', // RFC2518 + 103 => 'Early Hints', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', // RFC4918 + 208 => 'Already Reported', // RFC5842 + 226 => 'IM Used', // RFC3229 + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', // RFC7238 + 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 => 'Content Too Large', // RFC-ietf-httpbis-semantics + 414 => 'URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Range Not Satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', // RFC2324 + 421 => 'Misdirected Request', // RFC7540 + 422 => 'Unprocessable Content', // RFC-ietf-httpbis-semantics + 423 => 'Locked', // RFC4918 + 424 => 'Failed Dependency', // RFC4918 + 425 => 'Too Early', // RFC-ietf-httpbis-replay-04 + 426 => 'Upgrade Required', // RFC2817 + 428 => 'Precondition Required', // RFC6585 + 429 => 'Too Many Requests', // RFC6585 + 431 => 'Request Header Fields Too Large', // RFC6585 + 451 => 'Unavailable For Legal Reasons', // RFC7725 + 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', // RFC2295 + 507 => 'Insufficient Storage', // RFC4918 + 508 => 'Loop Detected', // RFC5842 + 510 => 'Not Extended', // RFC2774 + 511 => 'Network Authentication Required', // RFC6585 + ]; + + /** + * @throws \InvalidArgumentException When the HTTP status code is not valid + */ + public function __construct(?string $content = '', int $status = 200, array $headers = []) + { + $this->headers = new ResponseHeaderBag($headers); + $this->setContent($content); + $this->setStatusCode($status); + $this->setProtocolVersion('1.0'); + } + + /** + * Factory method for chainability. + * + * Example: + * + * return Response::create($body, 200) + * ->setSharedMaxAge(300); + * + * @return static + * + * @deprecated since Symfony 5.1, use __construct() instead. + */ + public static function create(?string $content = '', int $status = 200, array $headers = []) + { + trigger_deprecation('symfony/http-foundation', '5.1', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class); + + return new static($content, $status, $headers); + } + + /** + * Returns the Response as an HTTP string. + * + * The string representation of the Response is the same as the + * one that will be sent to the client only if the prepare() method + * has been called before. + * + * @return string + * + * @see prepare() + */ + public function __toString() + { + return + sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n". + $this->headers."\r\n". + $this->getContent(); + } + + /** + * Clones the current Response instance. + */ + public function __clone() + { + $this->headers = clone $this->headers; + } + + /** + * Prepares the Response before it is sent to the client. + * + * This method tweaks the Response to ensure that it is + * compliant with RFC 2616. Most of the changes are based on + * the Request that is "associated" with this Response. + * + * @return $this + */ + public function prepare(Request $request) + { + $headers = $this->headers; + + if ($this->isInformational() || $this->isEmpty()) { + $this->setContent(null); + $headers->remove('Content-Type'); + $headers->remove('Content-Length'); + // prevent PHP from sending the Content-Type header based on default_mimetype + ini_set('default_mimetype', ''); + } else { + // Content-type based on the Request + if (!$headers->has('Content-Type')) { + $format = $request->getRequestFormat(null); + if (null !== $format && $mimeType = $request->getMimeType($format)) { + $headers->set('Content-Type', $mimeType); + } + } + + // Fix Content-Type + $charset = $this->charset ?: 'UTF-8'; + if (!$headers->has('Content-Type')) { + $headers->set('Content-Type', 'text/html; charset='.$charset); + } elseif (0 === stripos($headers->get('Content-Type') ?? '', 'text/') && false === stripos($headers->get('Content-Type') ?? '', 'charset')) { + // add the charset + $headers->set('Content-Type', $headers->get('Content-Type').'; charset='.$charset); + } + + // Fix Content-Length + if ($headers->has('Transfer-Encoding')) { + $headers->remove('Content-Length'); + } + + if ($request->isMethod('HEAD')) { + // cf. RFC2616 14.13 + $length = $headers->get('Content-Length'); + $this->setContent(null); + if ($length) { + $headers->set('Content-Length', $length); + } + } + } + + // Fix protocol + if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) { + $this->setProtocolVersion('1.1'); + } + + // Check if we need to send extra expire info headers + if ('1.0' == $this->getProtocolVersion() && str_contains($headers->get('Cache-Control', ''), 'no-cache')) { + $headers->set('pragma', 'no-cache'); + $headers->set('expires', -1); + } + + $this->ensureIEOverSSLCompatibility($request); + + if ($request->isSecure()) { + foreach ($headers->getCookies() as $cookie) { + $cookie->setSecureDefault(true); + } + } + + return $this; + } + + /** + * Sends HTTP headers. + * + * @return $this + */ + public function sendHeaders() + { + // headers have already been sent by the developer + if (headers_sent()) { + return $this; + } + + // headers + foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) { + $replace = 0 === strcasecmp($name, 'Content-Type'); + foreach ($values as $value) { + header($name.': '.$value, $replace, $this->statusCode); + } + } + + // cookies + foreach ($this->headers->getCookies() as $cookie) { + header('Set-Cookie: '.$cookie, false, $this->statusCode); + } + + // status + header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode); + + return $this; + } + + /** + * Sends content for the current web response. + * + * @return $this + */ + public function sendContent() + { + echo $this->content; + + return $this; + } + + /** + * Sends HTTP headers and content. + * + * @return $this + */ + public function send() + { + $this->sendHeaders(); + $this->sendContent(); + + if (\function_exists('fastcgi_finish_request')) { + fastcgi_finish_request(); + } elseif (\function_exists('litespeed_finish_request')) { + litespeed_finish_request(); + } elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { + static::closeOutputBuffers(0, true); + flush(); + } + + return $this; + } + + /** + * Sets the response content. + * + * @return $this + */ + public function setContent(?string $content) + { + $this->content = $content ?? ''; + + return $this; + } + + /** + * Gets the current response content. + * + * @return string|false + */ + public function getContent() + { + return $this->content; + } + + /** + * Sets the HTTP protocol version (1.0 or 1.1). + * + * @return $this + * + * @final + */ + public function setProtocolVersion(string $version): object + { + $this->version = $version; + + return $this; + } + + /** + * Gets the HTTP protocol version. + * + * @final + */ + public function getProtocolVersion(): string + { + return $this->version; + } + + /** + * Sets the response status code. + * + * If the status text is null it will be automatically populated for the known + * status codes and left empty otherwise. + * + * @return $this + * + * @throws \InvalidArgumentException When the HTTP status code is not valid + * + * @final + */ + public function setStatusCode(int $code, ?string $text = null): object + { + $this->statusCode = $code; + if ($this->isInvalid()) { + throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $code)); + } + + if (null === $text) { + $this->statusText = self::$statusTexts[$code] ?? 'unknown status'; + + return $this; + } + + if (false === $text) { + $this->statusText = ''; + + return $this; + } + + $this->statusText = $text; + + return $this; + } + + /** + * Retrieves the status code for the current web response. + * + * @final + */ + public function getStatusCode(): int + { + return $this->statusCode; + } + + /** + * Sets the response charset. + * + * @return $this + * + * @final + */ + public function setCharset(string $charset): object + { + $this->charset = $charset; + + return $this; + } + + /** + * Retrieves the response charset. + * + * @final + */ + public function getCharset(): ?string + { + return $this->charset; + } + + /** + * Returns true if the response may safely be kept in a shared (surrogate) cache. + * + * Responses marked "private" with an explicit Cache-Control directive are + * considered uncacheable. + * + * Responses with neither a freshness lifetime (Expires, max-age) nor cache + * validator (Last-Modified, ETag) are considered uncacheable because there is + * no way to tell when or how to remove them from the cache. + * + * Note that RFC 7231 and RFC 7234 possibly allow for a more permissive implementation, + * for example "status codes that are defined as cacheable by default [...] + * can be reused by a cache with heuristic expiration unless otherwise indicated" + * (https://tools.ietf.org/html/rfc7231#section-6.1) + * + * @final + */ + public function isCacheable(): bool + { + if (!\in_array($this->statusCode, [200, 203, 300, 301, 302, 404, 410])) { + return false; + } + + if ($this->headers->hasCacheControlDirective('no-store') || $this->headers->getCacheControlDirective('private')) { + return false; + } + + return $this->isValidateable() || $this->isFresh(); + } + + /** + * Returns true if the response is "fresh". + * + * Fresh responses may be served from cache without any interaction with the + * origin. A response is considered fresh when it includes a Cache-Control/max-age + * indicator or Expires header and the calculated age is less than the freshness lifetime. + * + * @final + */ + public function isFresh(): bool + { + return $this->getTtl() > 0; + } + + /** + * Returns true if the response includes headers that can be used to validate + * the response with the origin server using a conditional GET request. + * + * @final + */ + public function isValidateable(): bool + { + return $this->headers->has('Last-Modified') || $this->headers->has('ETag'); + } + + /** + * Marks the response as "private". + * + * It makes the response ineligible for serving other clients. + * + * @return $this + * + * @final + */ + public function setPrivate(): object + { + $this->headers->removeCacheControlDirective('public'); + $this->headers->addCacheControlDirective('private'); + + return $this; + } + + /** + * Marks the response as "public". + * + * It makes the response eligible for serving other clients. + * + * @return $this + * + * @final + */ + public function setPublic(): object + { + $this->headers->addCacheControlDirective('public'); + $this->headers->removeCacheControlDirective('private'); + + return $this; + } + + /** + * Marks the response as "immutable". + * + * @return $this + * + * @final + */ + public function setImmutable(bool $immutable = true): object + { + if ($immutable) { + $this->headers->addCacheControlDirective('immutable'); + } else { + $this->headers->removeCacheControlDirective('immutable'); + } + + return $this; + } + + /** + * Returns true if the response is marked as "immutable". + * + * @final + */ + public function isImmutable(): bool + { + return $this->headers->hasCacheControlDirective('immutable'); + } + + /** + * Returns true if the response must be revalidated by shared caches once it has become stale. + * + * This method indicates that the response must not be served stale by a + * cache in any circumstance without first revalidating with the origin. + * When present, the TTL of the response should not be overridden to be + * greater than the value provided by the origin. + * + * @final + */ + public function mustRevalidate(): bool + { + return $this->headers->hasCacheControlDirective('must-revalidate') || $this->headers->hasCacheControlDirective('proxy-revalidate'); + } + + /** + * Returns the Date header as a DateTime instance. + * + * @throws \RuntimeException When the header is not parseable + * + * @final + */ + public function getDate(): ?\DateTimeInterface + { + return $this->headers->getDate('Date'); + } + + /** + * Sets the Date header. + * + * @return $this + * + * @final + */ + public function setDate(\DateTimeInterface $date): object + { + if ($date instanceof \DateTime) { + $date = \DateTimeImmutable::createFromMutable($date); + } + + $date = $date->setTimezone(new \DateTimeZone('UTC')); + $this->headers->set('Date', $date->format('D, d M Y H:i:s').' GMT'); + + return $this; + } + + /** + * Returns the age of the response in seconds. + * + * @final + */ + public function getAge(): int + { + if (null !== $age = $this->headers->get('Age')) { + return (int) $age; + } + + return max(time() - (int) $this->getDate()->format('U'), 0); + } + + /** + * Marks the response stale by setting the Age header to be equal to the maximum age of the response. + * + * @return $this + */ + public function expire() + { + if ($this->isFresh()) { + $this->headers->set('Age', $this->getMaxAge()); + $this->headers->remove('Expires'); + } + + return $this; + } + + /** + * Returns the value of the Expires header as a DateTime instance. + * + * @final + */ + public function getExpires(): ?\DateTimeInterface + { + try { + return $this->headers->getDate('Expires'); + } catch (\RuntimeException $e) { + // according to RFC 2616 invalid date formats (e.g. "0" and "-1") must be treated as in the past + return \DateTime::createFromFormat('U', time() - 172800); + } + } + + /** + * Sets the Expires HTTP header with a DateTime instance. + * + * Passing null as value will remove the header. + * + * @return $this + * + * @final + */ + public function setExpires(?\DateTimeInterface $date = null): object + { + if (null === $date) { + $this->headers->remove('Expires'); + + return $this; + } + + if ($date instanceof \DateTime) { + $date = \DateTimeImmutable::createFromMutable($date); + } + + $date = $date->setTimezone(new \DateTimeZone('UTC')); + $this->headers->set('Expires', $date->format('D, d M Y H:i:s').' GMT'); + + return $this; + } + + /** + * Returns the number of seconds after the time specified in the response's Date + * header when the response should no longer be considered fresh. + * + * First, it checks for a s-maxage directive, then a max-age directive, and then it falls + * back on an expires header. It returns null when no maximum age can be established. + * + * @final + */ + public function getMaxAge(): ?int + { + if ($this->headers->hasCacheControlDirective('s-maxage')) { + return (int) $this->headers->getCacheControlDirective('s-maxage'); + } + + if ($this->headers->hasCacheControlDirective('max-age')) { + return (int) $this->headers->getCacheControlDirective('max-age'); + } + + if (null !== $expires = $this->getExpires()) { + $maxAge = (int) $expires->format('U') - (int) $this->getDate()->format('U'); + + return max($maxAge, 0); + } + + return null; + } + + /** + * Sets the number of seconds after which the response should no longer be considered fresh. + * + * This methods sets the Cache-Control max-age directive. + * + * @return $this + * + * @final + */ + public function setMaxAge(int $value): object + { + $this->headers->addCacheControlDirective('max-age', $value); + + return $this; + } + + /** + * Sets the number of seconds after which the response should no longer be considered fresh by shared caches. + * + * This methods sets the Cache-Control s-maxage directive. + * + * @return $this + * + * @final + */ + public function setSharedMaxAge(int $value): object + { + $this->setPublic(); + $this->headers->addCacheControlDirective('s-maxage', $value); + + return $this; + } + + /** + * Returns the response's time-to-live in seconds. + * + * It returns null when no freshness information is present in the response. + * + * When the response's TTL is 0, the response may not be served from cache without first + * revalidating with the origin. + * + * @final + */ + public function getTtl(): ?int + { + $maxAge = $this->getMaxAge(); + + return null !== $maxAge ? max($maxAge - $this->getAge(), 0) : null; + } + + /** + * Sets the response's time-to-live for shared caches in seconds. + * + * This method adjusts the Cache-Control/s-maxage directive. + * + * @return $this + * + * @final + */ + public function setTtl(int $seconds): object + { + $this->setSharedMaxAge($this->getAge() + $seconds); + + return $this; + } + + /** + * Sets the response's time-to-live for private/client caches in seconds. + * + * This method adjusts the Cache-Control/max-age directive. + * + * @return $this + * + * @final + */ + public function setClientTtl(int $seconds): object + { + $this->setMaxAge($this->getAge() + $seconds); + + return $this; + } + + /** + * Returns the Last-Modified HTTP header as a DateTime instance. + * + * @throws \RuntimeException When the HTTP header is not parseable + * + * @final + */ + public function getLastModified(): ?\DateTimeInterface + { + return $this->headers->getDate('Last-Modified'); + } + + /** + * Sets the Last-Modified HTTP header with a DateTime instance. + * + * Passing null as value will remove the header. + * + * @return $this + * + * @final + */ + public function setLastModified(?\DateTimeInterface $date = null): object + { + if (null === $date) { + $this->headers->remove('Last-Modified'); + + return $this; + } + + if ($date instanceof \DateTime) { + $date = \DateTimeImmutable::createFromMutable($date); + } + + $date = $date->setTimezone(new \DateTimeZone('UTC')); + $this->headers->set('Last-Modified', $date->format('D, d M Y H:i:s').' GMT'); + + return $this; + } + + /** + * Returns the literal value of the ETag HTTP header. + * + * @final + */ + public function getEtag(): ?string + { + return $this->headers->get('ETag'); + } + + /** + * Sets the ETag value. + * + * @param string|null $etag The ETag unique identifier or null to remove the header + * @param bool $weak Whether you want a weak ETag or not + * + * @return $this + * + * @final + */ + public function setEtag(?string $etag = null, bool $weak = false): object + { + if (null === $etag) { + $this->headers->remove('Etag'); + } else { + if (!str_starts_with($etag, '"')) { + $etag = '"'.$etag.'"'; + } + + $this->headers->set('ETag', (true === $weak ? 'W/' : '').$etag); + } + + return $this; + } + + /** + * Sets the response's cache headers (validation and/or expiration). + * + * Available options are: must_revalidate, no_cache, no_store, no_transform, public, private, proxy_revalidate, max_age, s_maxage, immutable, last_modified and etag. + * + * @return $this + * + * @throws \InvalidArgumentException + * + * @final + */ + public function setCache(array $options): object + { + if ($diff = array_diff(array_keys($options), array_keys(self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES))) { + throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', $diff))); + } + + if (isset($options['etag'])) { + $this->setEtag($options['etag']); + } + + if (isset($options['last_modified'])) { + $this->setLastModified($options['last_modified']); + } + + if (isset($options['max_age'])) { + $this->setMaxAge($options['max_age']); + } + + if (isset($options['s_maxage'])) { + $this->setSharedMaxAge($options['s_maxage']); + } + + foreach (self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES as $directive => $hasValue) { + if (!$hasValue && isset($options[$directive])) { + if ($options[$directive]) { + $this->headers->addCacheControlDirective(str_replace('_', '-', $directive)); + } else { + $this->headers->removeCacheControlDirective(str_replace('_', '-', $directive)); + } + } + } + + if (isset($options['public'])) { + if ($options['public']) { + $this->setPublic(); + } else { + $this->setPrivate(); + } + } + + if (isset($options['private'])) { + if ($options['private']) { + $this->setPrivate(); + } else { + $this->setPublic(); + } + } + + return $this; + } + + /** + * Modifies the response so that it conforms to the rules defined for a 304 status code. + * + * This sets the status, removes the body, and discards any headers + * that MUST NOT be included in 304 responses. + * + * @return $this + * + * @see https://tools.ietf.org/html/rfc2616#section-10.3.5 + * + * @final + */ + public function setNotModified(): object + { + $this->setStatusCode(304); + $this->setContent(null); + + // remove headers that MUST NOT be included with 304 Not Modified responses + foreach (['Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified'] as $header) { + $this->headers->remove($header); + } + + return $this; + } + + /** + * Returns true if the response includes a Vary header. + * + * @final + */ + public function hasVary(): bool + { + return null !== $this->headers->get('Vary'); + } + + /** + * Returns an array of header names given in the Vary header. + * + * @final + */ + public function getVary(): array + { + if (!$vary = $this->headers->all('Vary')) { + return []; + } + + $ret = []; + foreach ($vary as $item) { + $ret[] = preg_split('/[\s,]+/', $item); + } + + return array_merge([], ...$ret); + } + + /** + * Sets the Vary header. + * + * @param string|array $headers + * @param bool $replace Whether to replace the actual value or not (true by default) + * + * @return $this + * + * @final + */ + public function setVary($headers, bool $replace = true): object + { + $this->headers->set('Vary', $headers, $replace); + + return $this; + } + + /** + * Determines if the Response validators (ETag, Last-Modified) match + * a conditional value specified in the Request. + * + * If the Response is not modified, it sets the status code to 304 and + * removes the actual content by calling the setNotModified() method. + * + * @final + */ + public function isNotModified(Request $request): bool + { + if (!$request->isMethodCacheable()) { + return false; + } + + $notModified = false; + $lastModified = $this->headers->get('Last-Modified'); + $modifiedSince = $request->headers->get('If-Modified-Since'); + + if (($ifNoneMatchEtags = $request->getETags()) && (null !== $etag = $this->getEtag())) { + if (0 == strncmp($etag, 'W/', 2)) { + $etag = substr($etag, 2); + } + + // Use weak comparison as per https://tools.ietf.org/html/rfc7232#section-3.2. + foreach ($ifNoneMatchEtags as $ifNoneMatchEtag) { + if (0 == strncmp($ifNoneMatchEtag, 'W/', 2)) { + $ifNoneMatchEtag = substr($ifNoneMatchEtag, 2); + } + + if ($ifNoneMatchEtag === $etag || '*' === $ifNoneMatchEtag) { + $notModified = true; + break; + } + } + } + // Only do If-Modified-Since date comparison when If-None-Match is not present as per https://tools.ietf.org/html/rfc7232#section-3.3. + elseif ($modifiedSince && $lastModified) { + $notModified = strtotime($modifiedSince) >= strtotime($lastModified); + } + + if ($notModified) { + $this->setNotModified(); + } + + return $notModified; + } + + /** + * Is response invalid? + * + * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + * + * @final + */ + public function isInvalid(): bool + { + return $this->statusCode < 100 || $this->statusCode >= 600; + } + + /** + * Is response informative? + * + * @final + */ + public function isInformational(): bool + { + return $this->statusCode >= 100 && $this->statusCode < 200; + } + + /** + * Is response successful? + * + * @final + */ + public function isSuccessful(): bool + { + return $this->statusCode >= 200 && $this->statusCode < 300; + } + + /** + * Is the response a redirect? + * + * @final + */ + public function isRedirection(): bool + { + return $this->statusCode >= 300 && $this->statusCode < 400; + } + + /** + * Is there a client error? + * + * @final + */ + public function isClientError(): bool + { + return $this->statusCode >= 400 && $this->statusCode < 500; + } + + /** + * Was there a server side error? + * + * @final + */ + public function isServerError(): bool + { + return $this->statusCode >= 500 && $this->statusCode < 600; + } + + /** + * Is the response OK? + * + * @final + */ + public function isOk(): bool + { + return 200 === $this->statusCode; + } + + /** + * Is the response forbidden? + * + * @final + */ + public function isForbidden(): bool + { + return 403 === $this->statusCode; + } + + /** + * Is the response a not found error? + * + * @final + */ + public function isNotFound(): bool + { + return 404 === $this->statusCode; + } + + /** + * Is the response a redirect of some form? + * + * @final + */ + public function isRedirect(?string $location = null): bool + { + return \in_array($this->statusCode, [201, 301, 302, 303, 307, 308]) && (null === $location ?: $location == $this->headers->get('Location')); + } + + /** + * Is the response empty? + * + * @final + */ + public function isEmpty(): bool + { + return \in_array($this->statusCode, [204, 304]); + } + + /** + * Cleans or flushes output buffers up to target level. + * + * Resulting level can be greater than target level if a non-removable buffer has been encountered. + * + * @final + */ + public static function closeOutputBuffers(int $targetLevel, bool $flush): void + { + $status = ob_get_status(true); + $level = \count($status); + $flags = \PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? \PHP_OUTPUT_HANDLER_FLUSHABLE : \PHP_OUTPUT_HANDLER_CLEANABLE); + + while ($level-- > $targetLevel && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) { + if ($flush) { + ob_end_flush(); + } else { + ob_end_clean(); + } + } + } + + /** + * Marks a response as safe according to RFC8674. + * + * @see https://tools.ietf.org/html/rfc8674 + */ + public function setContentSafe(bool $safe = true): void + { + if ($safe) { + $this->headers->set('Preference-Applied', 'safe'); + } elseif ('safe' === $this->headers->get('Preference-Applied')) { + $this->headers->remove('Preference-Applied'); + } + + $this->setVary('Prefer', false); + } + + /** + * Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9. + * + * @see http://support.microsoft.com/kb/323308 + * + * @final + */ + protected function ensureIEOverSSLCompatibility(Request $request): void + { + if (false !== stripos($this->headers->get('Content-Disposition') ?? '', 'attachment') && 1 == preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT') ?? '', $match) && true === $request->isSecure()) { + if ((int) preg_replace('/(MSIE )(.*?);/', '$2', $match[0]) < 9) { + $this->headers->remove('Cache-Control'); + } + } + } +} diff --git a/vendor/symfony/http-foundation/ResponseHeaderBag.php b/vendor/symfony/http-foundation/ResponseHeaderBag.php new file mode 100644 index 0000000..d4c4f39 --- /dev/null +++ b/vendor/symfony/http-foundation/ResponseHeaderBag.php @@ -0,0 +1,291 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * ResponseHeaderBag is a container for Response HTTP headers. + * + * @author Fabien Potencier + */ +class ResponseHeaderBag extends HeaderBag +{ + public const COOKIES_FLAT = 'flat'; + public const COOKIES_ARRAY = 'array'; + + public const DISPOSITION_ATTACHMENT = 'attachment'; + public const DISPOSITION_INLINE = 'inline'; + + protected $computedCacheControl = []; + protected $cookies = []; + protected $headerNames = []; + + public function __construct(array $headers = []) + { + parent::__construct($headers); + + if (!isset($this->headers['cache-control'])) { + $this->set('Cache-Control', ''); + } + + /* RFC2616 - 14.18 says all Responses need to have a Date */ + if (!isset($this->headers['date'])) { + $this->initDate(); + } + } + + /** + * Returns the headers, with original capitalizations. + * + * @return array + */ + public function allPreserveCase() + { + $headers = []; + foreach ($this->all() as $name => $value) { + $headers[$this->headerNames[$name] ?? $name] = $value; + } + + return $headers; + } + + public function allPreserveCaseWithoutCookies() + { + $headers = $this->allPreserveCase(); + if (isset($this->headerNames['set-cookie'])) { + unset($headers[$this->headerNames['set-cookie']]); + } + + return $headers; + } + + /** + * {@inheritdoc} + */ + public function replace(array $headers = []) + { + $this->headerNames = []; + + parent::replace($headers); + + if (!isset($this->headers['cache-control'])) { + $this->set('Cache-Control', ''); + } + + if (!isset($this->headers['date'])) { + $this->initDate(); + } + } + + /** + * {@inheritdoc} + */ + public function all(?string $key = null) + { + $headers = parent::all(); + + if (null !== $key) { + $key = strtr($key, self::UPPER, self::LOWER); + + return 'set-cookie' !== $key ? $headers[$key] ?? [] : array_map('strval', $this->getCookies()); + } + + foreach ($this->getCookies() as $cookie) { + $headers['set-cookie'][] = (string) $cookie; + } + + return $headers; + } + + /** + * {@inheritdoc} + */ + public function set(string $key, $values, bool $replace = true) + { + $uniqueKey = strtr($key, self::UPPER, self::LOWER); + + if ('set-cookie' === $uniqueKey) { + if ($replace) { + $this->cookies = []; + } + foreach ((array) $values as $cookie) { + $this->setCookie(Cookie::fromString($cookie)); + } + $this->headerNames[$uniqueKey] = $key; + + return; + } + + $this->headerNames[$uniqueKey] = $key; + + parent::set($key, $values, $replace); + + // ensure the cache-control header has sensible defaults + if (\in_array($uniqueKey, ['cache-control', 'etag', 'last-modified', 'expires'], true) && '' !== $computed = $this->computeCacheControlValue()) { + $this->headers['cache-control'] = [$computed]; + $this->headerNames['cache-control'] = 'Cache-Control'; + $this->computedCacheControl = $this->parseCacheControl($computed); + } + } + + /** + * {@inheritdoc} + */ + public function remove(string $key) + { + $uniqueKey = strtr($key, self::UPPER, self::LOWER); + unset($this->headerNames[$uniqueKey]); + + if ('set-cookie' === $uniqueKey) { + $this->cookies = []; + + return; + } + + parent::remove($key); + + if ('cache-control' === $uniqueKey) { + $this->computedCacheControl = []; + } + + if ('date' === $uniqueKey) { + $this->initDate(); + } + } + + /** + * {@inheritdoc} + */ + public function hasCacheControlDirective(string $key) + { + return \array_key_exists($key, $this->computedCacheControl); + } + + /** + * {@inheritdoc} + */ + public function getCacheControlDirective(string $key) + { + return $this->computedCacheControl[$key] ?? null; + } + + public function setCookie(Cookie $cookie) + { + $this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie; + $this->headerNames['set-cookie'] = 'Set-Cookie'; + } + + /** + * Removes a cookie from the array, but does not unset it in the browser. + */ + public function removeCookie(string $name, ?string $path = '/', ?string $domain = null) + { + if (null === $path) { + $path = '/'; + } + + unset($this->cookies[$domain][$path][$name]); + + if (empty($this->cookies[$domain][$path])) { + unset($this->cookies[$domain][$path]); + + if (empty($this->cookies[$domain])) { + unset($this->cookies[$domain]); + } + } + + if (empty($this->cookies)) { + unset($this->headerNames['set-cookie']); + } + } + + /** + * Returns an array with all cookies. + * + * @return Cookie[] + * + * @throws \InvalidArgumentException When the $format is invalid + */ + public function getCookies(string $format = self::COOKIES_FLAT) + { + if (!\in_array($format, [self::COOKIES_FLAT, self::COOKIES_ARRAY])) { + throw new \InvalidArgumentException(sprintf('Format "%s" invalid (%s).', $format, implode(', ', [self::COOKIES_FLAT, self::COOKIES_ARRAY]))); + } + + if (self::COOKIES_ARRAY === $format) { + return $this->cookies; + } + + $flattenedCookies = []; + foreach ($this->cookies as $path) { + foreach ($path as $cookies) { + foreach ($cookies as $cookie) { + $flattenedCookies[] = $cookie; + } + } + } + + return $flattenedCookies; + } + + /** + * Clears a cookie in the browser. + */ + public function clearCookie(string $name, ?string $path = '/', ?string $domain = null, bool $secure = false, bool $httpOnly = true, ?string $sameSite = null) + { + $this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite)); + } + + /** + * @see HeaderUtils::makeDisposition() + */ + public function makeDisposition(string $disposition, string $filename, string $filenameFallback = '') + { + return HeaderUtils::makeDisposition($disposition, $filename, $filenameFallback); + } + + /** + * Returns the calculated value of the cache-control header. + * + * This considers several other headers and calculates or modifies the + * cache-control header to a sensible, conservative value. + * + * @return string + */ + protected function computeCacheControlValue() + { + if (!$this->cacheControl) { + if ($this->has('Last-Modified') || $this->has('Expires')) { + return 'private, must-revalidate'; // allows for heuristic expiration (RFC 7234 Section 4.2.2) in the case of "Last-Modified" + } + + // conservative by default + return 'no-cache, private'; + } + + $header = $this->getCacheControlHeader(); + if (isset($this->cacheControl['public']) || isset($this->cacheControl['private'])) { + return $header; + } + + // public if s-maxage is defined, private otherwise + if (!isset($this->cacheControl['s-maxage'])) { + return $header.', private'; + } + + return $header; + } + + private function initDate(): void + { + $this->set('Date', gmdate('D, d M Y H:i:s').' GMT'); + } +} diff --git a/vendor/symfony/http-foundation/ServerBag.php b/vendor/symfony/http-foundation/ServerBag.php new file mode 100644 index 0000000..831caa6 --- /dev/null +++ b/vendor/symfony/http-foundation/ServerBag.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * ServerBag is a container for HTTP headers from the $_SERVER variable. + * + * @author Fabien Potencier + * @author Bulat Shakirzyanov + * @author Robert Kiss + */ +class ServerBag extends ParameterBag +{ + /** + * Gets the HTTP headers. + * + * @return array + */ + public function getHeaders() + { + $headers = []; + foreach ($this->parameters as $key => $value) { + if (str_starts_with($key, 'HTTP_')) { + $headers[substr($key, 5)] = $value; + } elseif (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true) && '' !== $value) { + $headers[$key] = $value; + } + } + + if (isset($this->parameters['PHP_AUTH_USER'])) { + $headers['PHP_AUTH_USER'] = $this->parameters['PHP_AUTH_USER']; + $headers['PHP_AUTH_PW'] = $this->parameters['PHP_AUTH_PW'] ?? ''; + } else { + /* + * php-cgi under Apache does not pass HTTP Basic user/pass to PHP by default + * For this workaround to work, add these lines to your .htaccess file: + * RewriteCond %{HTTP:Authorization} .+ + * RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0] + * + * A sample .htaccess file: + * RewriteEngine On + * RewriteCond %{HTTP:Authorization} .+ + * RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0] + * RewriteCond %{REQUEST_FILENAME} !-f + * RewriteRule ^(.*)$ index.php [QSA,L] + */ + + $authorizationHeader = null; + if (isset($this->parameters['HTTP_AUTHORIZATION'])) { + $authorizationHeader = $this->parameters['HTTP_AUTHORIZATION']; + } elseif (isset($this->parameters['REDIRECT_HTTP_AUTHORIZATION'])) { + $authorizationHeader = $this->parameters['REDIRECT_HTTP_AUTHORIZATION']; + } + + if (null !== $authorizationHeader) { + if (0 === stripos($authorizationHeader, 'basic ')) { + // Decode AUTHORIZATION header into PHP_AUTH_USER and PHP_AUTH_PW when authorization header is basic + $exploded = explode(':', base64_decode(substr($authorizationHeader, 6)), 2); + if (2 == \count($exploded)) { + [$headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']] = $exploded; + } + } elseif (empty($this->parameters['PHP_AUTH_DIGEST']) && (0 === stripos($authorizationHeader, 'digest '))) { + // In some circumstances PHP_AUTH_DIGEST needs to be set + $headers['PHP_AUTH_DIGEST'] = $authorizationHeader; + $this->parameters['PHP_AUTH_DIGEST'] = $authorizationHeader; + } elseif (0 === stripos($authorizationHeader, 'bearer ')) { + /* + * XXX: Since there is no PHP_AUTH_BEARER in PHP predefined variables, + * I'll just set $headers['AUTHORIZATION'] here. + * https://php.net/reserved.variables.server + */ + $headers['AUTHORIZATION'] = $authorizationHeader; + } + } + } + + if (isset($headers['AUTHORIZATION'])) { + return $headers; + } + + // PHP_AUTH_USER/PHP_AUTH_PW + if (isset($headers['PHP_AUTH_USER'])) { + $headers['AUTHORIZATION'] = 'Basic '.base64_encode($headers['PHP_AUTH_USER'].':'.($headers['PHP_AUTH_PW'] ?? '')); + } elseif (isset($headers['PHP_AUTH_DIGEST'])) { + $headers['AUTHORIZATION'] = $headers['PHP_AUTH_DIGEST']; + } + + return $headers; + } +} diff --git a/vendor/symfony/http-foundation/Session/Attribute/AttributeBag.php b/vendor/symfony/http-foundation/Session/Attribute/AttributeBag.php new file mode 100644 index 0000000..f4f051c --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Attribute/AttributeBag.php @@ -0,0 +1,152 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Attribute; + +/** + * This class relates to session attribute storage. + * + * @implements \IteratorAggregate + */ +class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Countable +{ + private $name = 'attributes'; + private $storageKey; + + protected $attributes = []; + + /** + * @param string $storageKey The key used to store attributes in the session + */ + public function __construct(string $storageKey = '_sf2_attributes') + { + $this->storageKey = $storageKey; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->name; + } + + public function setName(string $name) + { + $this->name = $name; + } + + /** + * {@inheritdoc} + */ + public function initialize(array &$attributes) + { + $this->attributes = &$attributes; + } + + /** + * {@inheritdoc} + */ + public function getStorageKey() + { + return $this->storageKey; + } + + /** + * {@inheritdoc} + */ + public function has(string $name) + { + return \array_key_exists($name, $this->attributes); + } + + /** + * {@inheritdoc} + */ + public function get(string $name, $default = null) + { + return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; + } + + /** + * {@inheritdoc} + */ + public function set(string $name, $value) + { + $this->attributes[$name] = $value; + } + + /** + * {@inheritdoc} + */ + public function all() + { + return $this->attributes; + } + + /** + * {@inheritdoc} + */ + public function replace(array $attributes) + { + $this->attributes = []; + foreach ($attributes as $key => $value) { + $this->set($key, $value); + } + } + + /** + * {@inheritdoc} + */ + public function remove(string $name) + { + $retval = null; + if (\array_key_exists($name, $this->attributes)) { + $retval = $this->attributes[$name]; + unset($this->attributes[$name]); + } + + return $retval; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $return = $this->attributes; + $this->attributes = []; + + return $return; + } + + /** + * Returns an iterator for attributes. + * + * @return \ArrayIterator + */ + #[\ReturnTypeWillChange] + public function getIterator() + { + return new \ArrayIterator($this->attributes); + } + + /** + * Returns the number of attributes. + * + * @return int + */ + #[\ReturnTypeWillChange] + public function count() + { + return \count($this->attributes); + } +} diff --git a/vendor/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php b/vendor/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php new file mode 100644 index 0000000..cb50696 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Attribute; + +use Symfony\Component\HttpFoundation\Session\SessionBagInterface; + +/** + * Attributes store. + * + * @author Drak + */ +interface AttributeBagInterface extends SessionBagInterface +{ + /** + * Checks if an attribute is defined. + * + * @return bool + */ + public function has(string $name); + + /** + * Returns an attribute. + * + * @param mixed $default The default value if not found + * + * @return mixed + */ + public function get(string $name, $default = null); + + /** + * Sets an attribute. + * + * @param mixed $value + */ + public function set(string $name, $value); + + /** + * Returns attributes. + * + * @return array + */ + public function all(); + + public function replace(array $attributes); + + /** + * Removes an attribute. + * + * @return mixed The removed value or null when it does not exist + */ + public function remove(string $name); +} diff --git a/vendor/symfony/http-foundation/Session/Attribute/NamespacedAttributeBag.php b/vendor/symfony/http-foundation/Session/Attribute/NamespacedAttributeBag.php new file mode 100644 index 0000000..864b35f --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Attribute/NamespacedAttributeBag.php @@ -0,0 +1,161 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Attribute; + +trigger_deprecation('symfony/http-foundation', '5.3', 'The "%s" class is deprecated.', NamespacedAttributeBag::class); + +/** + * This class provides structured storage of session attributes using + * a name spacing character in the key. + * + * @author Drak + * + * @deprecated since Symfony 5.3 + */ +class NamespacedAttributeBag extends AttributeBag +{ + private $namespaceCharacter; + + /** + * @param string $storageKey Session storage key + * @param string $namespaceCharacter Namespace character to use in keys + */ + public function __construct(string $storageKey = '_sf2_attributes', string $namespaceCharacter = '/') + { + $this->namespaceCharacter = $namespaceCharacter; + parent::__construct($storageKey); + } + + /** + * {@inheritdoc} + */ + public function has(string $name) + { + // reference mismatch: if fixed, re-introduced in array_key_exists; keep as it is + $attributes = $this->resolveAttributePath($name); + $name = $this->resolveKey($name); + + if (null === $attributes) { + return false; + } + + return \array_key_exists($name, $attributes); + } + + /** + * {@inheritdoc} + */ + public function get(string $name, $default = null) + { + // reference mismatch: if fixed, re-introduced in array_key_exists; keep as it is + $attributes = $this->resolveAttributePath($name); + $name = $this->resolveKey($name); + + if (null === $attributes) { + return $default; + } + + return \array_key_exists($name, $attributes) ? $attributes[$name] : $default; + } + + /** + * {@inheritdoc} + */ + public function set(string $name, $value) + { + $attributes = &$this->resolveAttributePath($name, true); + $name = $this->resolveKey($name); + $attributes[$name] = $value; + } + + /** + * {@inheritdoc} + */ + public function remove(string $name) + { + $retval = null; + $attributes = &$this->resolveAttributePath($name); + $name = $this->resolveKey($name); + if (null !== $attributes && \array_key_exists($name, $attributes)) { + $retval = $attributes[$name]; + unset($attributes[$name]); + } + + return $retval; + } + + /** + * Resolves a path in attributes property and returns it as a reference. + * + * This method allows structured namespacing of session attributes. + * + * @param string $name Key name + * @param bool $writeContext Write context, default false + * + * @return array|null + */ + protected function &resolveAttributePath(string $name, bool $writeContext = false) + { + $array = &$this->attributes; + $name = (str_starts_with($name, $this->namespaceCharacter)) ? substr($name, 1) : $name; + + // Check if there is anything to do, else return + if (!$name) { + return $array; + } + + $parts = explode($this->namespaceCharacter, $name); + if (\count($parts) < 2) { + if (!$writeContext) { + return $array; + } + + $array[$parts[0]] = []; + + return $array; + } + + unset($parts[\count($parts) - 1]); + + foreach ($parts as $part) { + if (null !== $array && !\array_key_exists($part, $array)) { + if (!$writeContext) { + $null = null; + + return $null; + } + + $array[$part] = []; + } + + $array = &$array[$part]; + } + + return $array; + } + + /** + * Resolves the key from the name. + * + * This is the last part in a dot separated string. + * + * @return string + */ + protected function resolveKey(string $name) + { + if (false !== $pos = strrpos($name, $this->namespaceCharacter)) { + $name = substr($name, $pos + 1); + } + + return $name; + } +} diff --git a/vendor/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php b/vendor/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php new file mode 100644 index 0000000..8aab3a1 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php @@ -0,0 +1,161 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Flash; + +/** + * AutoExpireFlashBag flash message container. + * + * @author Drak + */ +class AutoExpireFlashBag implements FlashBagInterface +{ + private $name = 'flashes'; + private $flashes = ['display' => [], 'new' => []]; + private $storageKey; + + /** + * @param string $storageKey The key used to store flashes in the session + */ + public function __construct(string $storageKey = '_symfony_flashes') + { + $this->storageKey = $storageKey; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->name; + } + + public function setName(string $name) + { + $this->name = $name; + } + + /** + * {@inheritdoc} + */ + public function initialize(array &$flashes) + { + $this->flashes = &$flashes; + + // The logic: messages from the last request will be stored in new, so we move them to previous + // This request we will show what is in 'display'. What is placed into 'new' this time round will + // be moved to display next time round. + $this->flashes['display'] = \array_key_exists('new', $this->flashes) ? $this->flashes['new'] : []; + $this->flashes['new'] = []; + } + + /** + * {@inheritdoc} + */ + public function add(string $type, $message) + { + $this->flashes['new'][$type][] = $message; + } + + /** + * {@inheritdoc} + */ + public function peek(string $type, array $default = []) + { + return $this->has($type) ? $this->flashes['display'][$type] : $default; + } + + /** + * {@inheritdoc} + */ + public function peekAll() + { + return \array_key_exists('display', $this->flashes) ? $this->flashes['display'] : []; + } + + /** + * {@inheritdoc} + */ + public function get(string $type, array $default = []) + { + $return = $default; + + if (!$this->has($type)) { + return $return; + } + + if (isset($this->flashes['display'][$type])) { + $return = $this->flashes['display'][$type]; + unset($this->flashes['display'][$type]); + } + + return $return; + } + + /** + * {@inheritdoc} + */ + public function all() + { + $return = $this->flashes['display']; + $this->flashes['display'] = []; + + return $return; + } + + /** + * {@inheritdoc} + */ + public function setAll(array $messages) + { + $this->flashes['new'] = $messages; + } + + /** + * {@inheritdoc} + */ + public function set(string $type, $messages) + { + $this->flashes['new'][$type] = (array) $messages; + } + + /** + * {@inheritdoc} + */ + public function has(string $type) + { + return \array_key_exists($type, $this->flashes['display']) && $this->flashes['display'][$type]; + } + + /** + * {@inheritdoc} + */ + public function keys() + { + return array_keys($this->flashes['display']); + } + + /** + * {@inheritdoc} + */ + public function getStorageKey() + { + return $this->storageKey; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + return $this->all(); + } +} diff --git a/vendor/symfony/http-foundation/Session/Flash/FlashBag.php b/vendor/symfony/http-foundation/Session/Flash/FlashBag.php new file mode 100644 index 0000000..88df750 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Flash/FlashBag.php @@ -0,0 +1,152 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Flash; + +/** + * FlashBag flash message container. + * + * @author Drak + */ +class FlashBag implements FlashBagInterface +{ + private $name = 'flashes'; + private $flashes = []; + private $storageKey; + + /** + * @param string $storageKey The key used to store flashes in the session + */ + public function __construct(string $storageKey = '_symfony_flashes') + { + $this->storageKey = $storageKey; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->name; + } + + public function setName(string $name) + { + $this->name = $name; + } + + /** + * {@inheritdoc} + */ + public function initialize(array &$flashes) + { + $this->flashes = &$flashes; + } + + /** + * {@inheritdoc} + */ + public function add(string $type, $message) + { + $this->flashes[$type][] = $message; + } + + /** + * {@inheritdoc} + */ + public function peek(string $type, array $default = []) + { + return $this->has($type) ? $this->flashes[$type] : $default; + } + + /** + * {@inheritdoc} + */ + public function peekAll() + { + return $this->flashes; + } + + /** + * {@inheritdoc} + */ + public function get(string $type, array $default = []) + { + if (!$this->has($type)) { + return $default; + } + + $return = $this->flashes[$type]; + + unset($this->flashes[$type]); + + return $return; + } + + /** + * {@inheritdoc} + */ + public function all() + { + $return = $this->peekAll(); + $this->flashes = []; + + return $return; + } + + /** + * {@inheritdoc} + */ + public function set(string $type, $messages) + { + $this->flashes[$type] = (array) $messages; + } + + /** + * {@inheritdoc} + */ + public function setAll(array $messages) + { + $this->flashes = $messages; + } + + /** + * {@inheritdoc} + */ + public function has(string $type) + { + return \array_key_exists($type, $this->flashes) && $this->flashes[$type]; + } + + /** + * {@inheritdoc} + */ + public function keys() + { + return array_keys($this->flashes); + } + + /** + * {@inheritdoc} + */ + public function getStorageKey() + { + return $this->storageKey; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + return $this->all(); + } +} diff --git a/vendor/symfony/http-foundation/Session/Flash/FlashBagInterface.php b/vendor/symfony/http-foundation/Session/Flash/FlashBagInterface.php new file mode 100644 index 0000000..8713e71 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Flash/FlashBagInterface.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Flash; + +use Symfony\Component\HttpFoundation\Session\SessionBagInterface; + +/** + * FlashBagInterface. + * + * @author Drak + */ +interface FlashBagInterface extends SessionBagInterface +{ + /** + * Adds a flash message for the given type. + * + * @param mixed $message + */ + public function add(string $type, $message); + + /** + * Registers one or more messages for a given type. + * + * @param string|array $messages + */ + public function set(string $type, $messages); + + /** + * Gets flash messages for a given type. + * + * @param string $type Message category type + * @param array $default Default value if $type does not exist + * + * @return array + */ + public function peek(string $type, array $default = []); + + /** + * Gets all flash messages. + * + * @return array + */ + public function peekAll(); + + /** + * Gets and clears flash from the stack. + * + * @param array $default Default value if $type does not exist + * + * @return array + */ + public function get(string $type, array $default = []); + + /** + * Gets and clears flashes from the stack. + * + * @return array + */ + public function all(); + + /** + * Sets all flash messages. + */ + public function setAll(array $messages); + + /** + * Has flash messages for a given type? + * + * @return bool + */ + public function has(string $type); + + /** + * Returns a list of all defined types. + * + * @return array + */ + public function keys(); +} diff --git a/vendor/symfony/http-foundation/Session/Session.php b/vendor/symfony/http-foundation/Session/Session.php new file mode 100644 index 0000000..917920a --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Session.php @@ -0,0 +1,285 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; +use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface; +use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; +use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; +use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; +use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface; + +// Help opcache.preload discover always-needed symbols +class_exists(AttributeBag::class); +class_exists(FlashBag::class); +class_exists(SessionBagProxy::class); + +/** + * @author Fabien Potencier + * @author Drak + * + * @implements \IteratorAggregate + */ +class Session implements SessionInterface, \IteratorAggregate, \Countable +{ + protected $storage; + + private $flashName; + private $attributeName; + private $data = []; + private $usageIndex = 0; + private $usageReporter; + + public function __construct(?SessionStorageInterface $storage = null, ?AttributeBagInterface $attributes = null, ?FlashBagInterface $flashes = null, ?callable $usageReporter = null) + { + $this->storage = $storage ?? new NativeSessionStorage(); + $this->usageReporter = $usageReporter; + + $attributes = $attributes ?? new AttributeBag(); + $this->attributeName = $attributes->getName(); + $this->registerBag($attributes); + + $flashes = $flashes ?? new FlashBag(); + $this->flashName = $flashes->getName(); + $this->registerBag($flashes); + } + + /** + * {@inheritdoc} + */ + public function start() + { + return $this->storage->start(); + } + + /** + * {@inheritdoc} + */ + public function has(string $name) + { + return $this->getAttributeBag()->has($name); + } + + /** + * {@inheritdoc} + */ + public function get(string $name, $default = null) + { + return $this->getAttributeBag()->get($name, $default); + } + + /** + * {@inheritdoc} + */ + public function set(string $name, $value) + { + $this->getAttributeBag()->set($name, $value); + } + + /** + * {@inheritdoc} + */ + public function all() + { + return $this->getAttributeBag()->all(); + } + + /** + * {@inheritdoc} + */ + public function replace(array $attributes) + { + $this->getAttributeBag()->replace($attributes); + } + + /** + * {@inheritdoc} + */ + public function remove(string $name) + { + return $this->getAttributeBag()->remove($name); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $this->getAttributeBag()->clear(); + } + + /** + * {@inheritdoc} + */ + public function isStarted() + { + return $this->storage->isStarted(); + } + + /** + * Returns an iterator for attributes. + * + * @return \ArrayIterator + */ + #[\ReturnTypeWillChange] + public function getIterator() + { + return new \ArrayIterator($this->getAttributeBag()->all()); + } + + /** + * Returns the number of attributes. + * + * @return int + */ + #[\ReturnTypeWillChange] + public function count() + { + return \count($this->getAttributeBag()->all()); + } + + public function &getUsageIndex(): int + { + return $this->usageIndex; + } + + /** + * @internal + */ + public function isEmpty(): bool + { + if ($this->isStarted()) { + ++$this->usageIndex; + if ($this->usageReporter && 0 <= $this->usageIndex) { + ($this->usageReporter)(); + } + } + foreach ($this->data as &$data) { + if (!empty($data)) { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function invalidate(?int $lifetime = null) + { + $this->storage->clear(); + + return $this->migrate(true, $lifetime); + } + + /** + * {@inheritdoc} + */ + public function migrate(bool $destroy = false, ?int $lifetime = null) + { + return $this->storage->regenerate($destroy, $lifetime); + } + + /** + * {@inheritdoc} + */ + public function save() + { + $this->storage->save(); + } + + /** + * {@inheritdoc} + */ + public function getId() + { + return $this->storage->getId(); + } + + /** + * {@inheritdoc} + */ + public function setId(string $id) + { + if ($this->storage->getId() !== $id) { + $this->storage->setId($id); + } + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->storage->getName(); + } + + /** + * {@inheritdoc} + */ + public function setName(string $name) + { + $this->storage->setName($name); + } + + /** + * {@inheritdoc} + */ + public function getMetadataBag() + { + ++$this->usageIndex; + if ($this->usageReporter && 0 <= $this->usageIndex) { + ($this->usageReporter)(); + } + + return $this->storage->getMetadataBag(); + } + + /** + * {@inheritdoc} + */ + public function registerBag(SessionBagInterface $bag) + { + $this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex, $this->usageReporter)); + } + + /** + * {@inheritdoc} + */ + public function getBag(string $name) + { + $bag = $this->storage->getBag($name); + + return method_exists($bag, 'getBag') ? $bag->getBag() : $bag; + } + + /** + * Gets the flashbag interface. + * + * @return FlashBagInterface + */ + public function getFlashBag() + { + return $this->getBag($this->flashName); + } + + /** + * Gets the attributebag interface. + * + * Note that this method was added to help with IDE autocompletion. + */ + private function getAttributeBag(): AttributeBagInterface + { + return $this->getBag($this->attributeName); + } +} diff --git a/vendor/symfony/http-foundation/Session/SessionBagInterface.php b/vendor/symfony/http-foundation/Session/SessionBagInterface.php new file mode 100644 index 0000000..8e37d06 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/SessionBagInterface.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +/** + * Session Bag store. + * + * @author Drak + */ +interface SessionBagInterface +{ + /** + * Gets this bag's name. + * + * @return string + */ + public function getName(); + + /** + * Initializes the Bag. + */ + public function initialize(array &$array); + + /** + * Gets the storage key for this bag. + * + * @return string + */ + public function getStorageKey(); + + /** + * Clears out data from bag. + * + * @return mixed Whatever data was contained + */ + public function clear(); +} diff --git a/vendor/symfony/http-foundation/Session/SessionBagProxy.php b/vendor/symfony/http-foundation/Session/SessionBagProxy.php new file mode 100644 index 0000000..90aa010 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/SessionBagProxy.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +/** + * @author Nicolas Grekas + * + * @internal + */ +final class SessionBagProxy implements SessionBagInterface +{ + private $bag; + private $data; + private $usageIndex; + private $usageReporter; + + public function __construct(SessionBagInterface $bag, array &$data, ?int &$usageIndex, ?callable $usageReporter) + { + $this->bag = $bag; + $this->data = &$data; + $this->usageIndex = &$usageIndex; + $this->usageReporter = $usageReporter; + } + + public function getBag(): SessionBagInterface + { + ++$this->usageIndex; + if ($this->usageReporter && 0 <= $this->usageIndex) { + ($this->usageReporter)(); + } + + return $this->bag; + } + + public function isEmpty(): bool + { + if (!isset($this->data[$this->bag->getStorageKey()])) { + return true; + } + ++$this->usageIndex; + if ($this->usageReporter && 0 <= $this->usageIndex) { + ($this->usageReporter)(); + } + + return empty($this->data[$this->bag->getStorageKey()]); + } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return $this->bag->getName(); + } + + /** + * {@inheritdoc} + */ + public function initialize(array &$array): void + { + ++$this->usageIndex; + if ($this->usageReporter && 0 <= $this->usageIndex) { + ($this->usageReporter)(); + } + + $this->data[$this->bag->getStorageKey()] = &$array; + + $this->bag->initialize($array); + } + + /** + * {@inheritdoc} + */ + public function getStorageKey(): string + { + return $this->bag->getStorageKey(); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + return $this->bag->clear(); + } +} diff --git a/vendor/symfony/http-foundation/Session/SessionFactory.php b/vendor/symfony/http-foundation/Session/SessionFactory.php new file mode 100644 index 0000000..bd79282 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/SessionFactory.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageFactoryInterface; + +// Help opcache.preload discover always-needed symbols +class_exists(Session::class); + +/** + * @author Jérémy Derussé + */ +class SessionFactory implements SessionFactoryInterface +{ + private $requestStack; + private $storageFactory; + private $usageReporter; + + public function __construct(RequestStack $requestStack, SessionStorageFactoryInterface $storageFactory, ?callable $usageReporter = null) + { + $this->requestStack = $requestStack; + $this->storageFactory = $storageFactory; + $this->usageReporter = $usageReporter; + } + + public function createSession(): SessionInterface + { + return new Session($this->storageFactory->createStorage($this->requestStack->getMainRequest()), null, null, $this->usageReporter); + } +} diff --git a/vendor/symfony/http-foundation/Session/SessionFactoryInterface.php b/vendor/symfony/http-foundation/Session/SessionFactoryInterface.php new file mode 100644 index 0000000..b24fdc4 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/SessionFactoryInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +/** + * @author Kevin Bond + */ +interface SessionFactoryInterface +{ + public function createSession(): SessionInterface; +} diff --git a/vendor/symfony/http-foundation/Session/SessionInterface.php b/vendor/symfony/http-foundation/Session/SessionInterface.php new file mode 100644 index 0000000..b73dfd0 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/SessionInterface.php @@ -0,0 +1,166 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag; + +/** + * Interface for the session. + * + * @author Drak + */ +interface SessionInterface +{ + /** + * Starts the session storage. + * + * @return bool + * + * @throws \RuntimeException if session fails to start + */ + public function start(); + + /** + * Returns the session ID. + * + * @return string + */ + public function getId(); + + /** + * Sets the session ID. + */ + public function setId(string $id); + + /** + * Returns the session name. + * + * @return string + */ + public function getName(); + + /** + * Sets the session name. + */ + public function setName(string $name); + + /** + * Invalidates the current session. + * + * Clears all session attributes and flashes and regenerates the + * session and deletes the old session from persistence. + * + * @param int|null $lifetime Sets the cookie lifetime for the session cookie. A null value + * will leave the system settings unchanged, 0 sets the cookie + * to expire with browser session. Time is in seconds, and is + * not a Unix timestamp. + * + * @return bool + */ + public function invalidate(?int $lifetime = null); + + /** + * Migrates the current session to a new session id while maintaining all + * session attributes. + * + * @param bool $destroy Whether to delete the old session or leave it to garbage collection + * @param int|null $lifetime Sets the cookie lifetime for the session cookie. A null value + * will leave the system settings unchanged, 0 sets the cookie + * to expire with browser session. Time is in seconds, and is + * not a Unix timestamp. + * + * @return bool + */ + public function migrate(bool $destroy = false, ?int $lifetime = null); + + /** + * Force the session to be saved and closed. + * + * This method is generally not required for real sessions as + * the session will be automatically saved at the end of + * code execution. + */ + public function save(); + + /** + * Checks if an attribute is defined. + * + * @return bool + */ + public function has(string $name); + + /** + * Returns an attribute. + * + * @param mixed $default The default value if not found + * + * @return mixed + */ + public function get(string $name, $default = null); + + /** + * Sets an attribute. + * + * @param mixed $value + */ + public function set(string $name, $value); + + /** + * Returns attributes. + * + * @return array + */ + public function all(); + + /** + * Sets attributes. + */ + public function replace(array $attributes); + + /** + * Removes an attribute. + * + * @return mixed The removed value or null when it does not exist + */ + public function remove(string $name); + + /** + * Clears all attributes. + */ + public function clear(); + + /** + * Checks if the session was started. + * + * @return bool + */ + public function isStarted(); + + /** + * Registers a SessionBagInterface with the session. + */ + public function registerBag(SessionBagInterface $bag); + + /** + * Gets a bag instance by name. + * + * @return SessionBagInterface + */ + public function getBag(string $name); + + /** + * Gets session meta. + * + * @return MetadataBag + */ + public function getMetadataBag(); +} diff --git a/vendor/symfony/http-foundation/Session/SessionUtils.php b/vendor/symfony/http-foundation/Session/SessionUtils.php new file mode 100644 index 0000000..b5bce4a --- /dev/null +++ b/vendor/symfony/http-foundation/Session/SessionUtils.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +/** + * Session utility functions. + * + * @author Nicolas Grekas + * @author Rémon van de Kamp + * + * @internal + */ +final class SessionUtils +{ + /** + * Finds the session header amongst the headers that are to be sent, removes it, and returns + * it so the caller can process it further. + */ + public static function popSessionCookie(string $sessionName, string $sessionId): ?string + { + $sessionCookie = null; + $sessionCookiePrefix = sprintf(' %s=', urlencode($sessionName)); + $sessionCookieWithId = sprintf('%s%s;', $sessionCookiePrefix, urlencode($sessionId)); + $otherCookies = []; + foreach (headers_list() as $h) { + if (0 !== stripos($h, 'Set-Cookie:')) { + continue; + } + if (11 === strpos($h, $sessionCookiePrefix, 11)) { + $sessionCookie = $h; + + if (11 !== strpos($h, $sessionCookieWithId, 11)) { + $otherCookies[] = $h; + } + } else { + $otherCookies[] = $h; + } + } + if (null === $sessionCookie) { + return null; + } + + header_remove('Set-Cookie'); + foreach ($otherCookies as $h) { + header($h, false); + } + + return $sessionCookie; + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php new file mode 100644 index 0000000..35d7b4b --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php @@ -0,0 +1,155 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Symfony\Component\HttpFoundation\Session\SessionUtils; + +/** + * This abstract session handler provides a generic implementation + * of the PHP 7.0 SessionUpdateTimestampHandlerInterface, + * enabling strict and lazy session handling. + * + * @author Nicolas Grekas + */ +abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface +{ + private $sessionName; + private $prefetchId; + private $prefetchData; + private $newSessionId; + private $igbinaryEmptyData; + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function open($savePath, $sessionName) + { + $this->sessionName = $sessionName; + if (!headers_sent() && !\ini_get('session.cache_limiter') && '0' !== \ini_get('session.cache_limiter')) { + header(sprintf('Cache-Control: max-age=%d, private, must-revalidate', 60 * (int) \ini_get('session.cache_expire'))); + } + + return true; + } + + /** + * @return string + */ + abstract protected function doRead(string $sessionId); + + /** + * @return bool + */ + abstract protected function doWrite(string $sessionId, string $data); + + /** + * @return bool + */ + abstract protected function doDestroy(string $sessionId); + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function validateId($sessionId) + { + $this->prefetchData = $this->read($sessionId); + $this->prefetchId = $sessionId; + + if (\PHP_VERSION_ID < 70317 || (70400 <= \PHP_VERSION_ID && \PHP_VERSION_ID < 70405)) { + // work around https://bugs.php.net/79413 + foreach (debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { + if (!isset($frame['class']) && isset($frame['function']) && \in_array($frame['function'], ['session_regenerate_id', 'session_create_id'], true)) { + return '' === $this->prefetchData; + } + } + } + + return '' !== $this->prefetchData; + } + + /** + * @return string + */ + #[\ReturnTypeWillChange] + public function read($sessionId) + { + if (null !== $this->prefetchId) { + $prefetchId = $this->prefetchId; + $prefetchData = $this->prefetchData; + $this->prefetchId = $this->prefetchData = null; + + if ($prefetchId === $sessionId || '' === $prefetchData) { + $this->newSessionId = '' === $prefetchData ? $sessionId : null; + + return $prefetchData; + } + } + + $data = $this->doRead($sessionId); + $this->newSessionId = '' === $data ? $sessionId : null; + + return $data; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function write($sessionId, $data) + { + if (null === $this->igbinaryEmptyData) { + // see https://github.com/igbinary/igbinary/issues/146 + $this->igbinaryEmptyData = \function_exists('igbinary_serialize') ? igbinary_serialize([]) : ''; + } + if ('' === $data || $this->igbinaryEmptyData === $data) { + return $this->destroy($sessionId); + } + $this->newSessionId = null; + + return $this->doWrite($sessionId, $data); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function destroy($sessionId) + { + if (!headers_sent() && filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOLEAN)) { + if (!$this->sessionName) { + throw new \LogicException(sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', static::class)); + } + $cookie = SessionUtils::popSessionCookie($this->sessionName, $sessionId); + + /* + * We send an invalidation Set-Cookie header (zero lifetime) + * when either the session was started or a cookie with + * the session name was sent by the client (in which case + * we know it's invalid as a valid session cookie would've + * started the session). + */ + if (null === $cookie || isset($_COOKIE[$this->sessionName])) { + if (\PHP_VERSION_ID < 70300) { + setcookie($this->sessionName, '', 0, \ini_get('session.cookie_path'), \ini_get('session.cookie_domain'), filter_var(\ini_get('session.cookie_secure'), \FILTER_VALIDATE_BOOLEAN), filter_var(\ini_get('session.cookie_httponly'), \FILTER_VALIDATE_BOOLEAN)); + } else { + $params = session_get_cookie_params(); + unset($params['lifetime']); + setcookie($this->sessionName, '', $params); + } + } + } + + return $this->newSessionId === $sessionId || $this->doDestroy($sessionId); + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php b/vendor/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php new file mode 100644 index 0000000..bea3a32 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Ahmed TAILOULOUTE + */ +class IdentityMarshaller implements MarshallerInterface +{ + /** + * {@inheritdoc} + */ + public function marshall(array $values, ?array &$failed): array + { + foreach ($values as $key => $value) { + if (!\is_string($value)) { + throw new \LogicException(sprintf('%s accepts only string as data.', __METHOD__)); + } + } + + return $values; + } + + /** + * {@inheritdoc} + */ + public function unmarshall(string $value): string + { + return $value; + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php new file mode 100644 index 0000000..c321c8c --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Ahmed TAILOULOUTE + */ +class MarshallingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface +{ + private $handler; + private $marshaller; + + public function __construct(AbstractSessionHandler $handler, MarshallerInterface $marshaller) + { + $this->handler = $handler; + $this->marshaller = $marshaller; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function open($savePath, $name) + { + return $this->handler->open($savePath, $name); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function close() + { + return $this->handler->close(); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function destroy($sessionId) + { + return $this->handler->destroy($sessionId); + } + + /** + * @return int|false + */ + #[\ReturnTypeWillChange] + public function gc($maxlifetime) + { + return $this->handler->gc($maxlifetime); + } + + /** + * @return string + */ + #[\ReturnTypeWillChange] + public function read($sessionId) + { + return $this->marshaller->unmarshall($this->handler->read($sessionId)); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function write($sessionId, $data) + { + $failed = []; + $marshalledData = $this->marshaller->marshall(['data' => $data], $failed); + + if (isset($failed['data'])) { + return false; + } + + return $this->handler->write($sessionId, $marshalledData['data']); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function validateId($sessionId) + { + return $this->handler->validateId($sessionId); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function updateTimestamp($sessionId, $data) + { + return $this->handler->updateTimestamp($sessionId, $data); + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php new file mode 100644 index 0000000..e0ec4d2 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +/** + * Memcached based session storage handler based on the Memcached class + * provided by the PHP memcached extension. + * + * @see https://php.net/memcached + * + * @author Drak + */ +class MemcachedSessionHandler extends AbstractSessionHandler +{ + private $memcached; + + /** + * @var int Time to live in seconds + */ + private $ttl; + + /** + * @var string Key prefix for shared environments + */ + private $prefix; + + /** + * Constructor. + * + * List of available options: + * * prefix: The prefix to use for the memcached keys in order to avoid collision + * * ttl: The time to live in seconds. + * + * @throws \InvalidArgumentException When unsupported options are passed + */ + public function __construct(\Memcached $memcached, array $options = []) + { + $this->memcached = $memcached; + + if ($diff = array_diff(array_keys($options), ['prefix', 'expiretime', 'ttl'])) { + throw new \InvalidArgumentException(sprintf('The following options are not supported "%s".', implode(', ', $diff))); + } + + $this->ttl = $options['expiretime'] ?? $options['ttl'] ?? null; + $this->prefix = $options['prefix'] ?? 'sf2s'; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function close() + { + return $this->memcached->quit(); + } + + /** + * {@inheritdoc} + */ + protected function doRead(string $sessionId) + { + return $this->memcached->get($this->prefix.$sessionId) ?: ''; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function updateTimestamp($sessionId, $data) + { + $this->memcached->touch($this->prefix.$sessionId, $this->getCompatibleTtl()); + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doWrite(string $sessionId, string $data) + { + return $this->memcached->set($this->prefix.$sessionId, $data, $this->getCompatibleTtl()); + } + + private function getCompatibleTtl(): int + { + $ttl = (int) ($this->ttl ?? \ini_get('session.gc_maxlifetime')); + + // If the relative TTL that is used exceeds 30 days, memcached will treat the value as Unix time. + // We have to convert it to an absolute Unix time at this point, to make sure the TTL is correct. + if ($ttl > 60 * 60 * 24 * 30) { + $ttl += time(); + } + + return $ttl; + } + + /** + * {@inheritdoc} + */ + protected function doDestroy(string $sessionId) + { + $result = $this->memcached->delete($this->prefix.$sessionId); + + return $result || \Memcached::RES_NOTFOUND == $this->memcached->getResultCode(); + } + + /** + * @return int|false + */ + #[\ReturnTypeWillChange] + public function gc($maxlifetime) + { + // not required here because memcached will auto expire the records anyhow. + return 0; + } + + /** + * Return a Memcached instance. + * + * @return \Memcached + */ + protected function getMemcached() + { + return $this->memcached; + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php new file mode 100644 index 0000000..bf27ca6 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php @@ -0,0 +1,139 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +/** + * Migrating session handler for migrating from one handler to another. It reads + * from the current handler and writes both the current and new ones. + * + * It ignores errors from the new handler. + * + * @author Ross Motley + * @author Oliver Radwell + */ +class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface +{ + /** + * @var \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface + */ + private $currentHandler; + + /** + * @var \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface + */ + private $writeOnlyHandler; + + public function __construct(\SessionHandlerInterface $currentHandler, \SessionHandlerInterface $writeOnlyHandler) + { + if (!$currentHandler instanceof \SessionUpdateTimestampHandlerInterface) { + $currentHandler = new StrictSessionHandler($currentHandler); + } + if (!$writeOnlyHandler instanceof \SessionUpdateTimestampHandlerInterface) { + $writeOnlyHandler = new StrictSessionHandler($writeOnlyHandler); + } + + $this->currentHandler = $currentHandler; + $this->writeOnlyHandler = $writeOnlyHandler; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function close() + { + $result = $this->currentHandler->close(); + $this->writeOnlyHandler->close(); + + return $result; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function destroy($sessionId) + { + $result = $this->currentHandler->destroy($sessionId); + $this->writeOnlyHandler->destroy($sessionId); + + return $result; + } + + /** + * @return int|false + */ + #[\ReturnTypeWillChange] + public function gc($maxlifetime) + { + $result = $this->currentHandler->gc($maxlifetime); + $this->writeOnlyHandler->gc($maxlifetime); + + return $result; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function open($savePath, $sessionName) + { + $result = $this->currentHandler->open($savePath, $sessionName); + $this->writeOnlyHandler->open($savePath, $sessionName); + + return $result; + } + + /** + * @return string + */ + #[\ReturnTypeWillChange] + public function read($sessionId) + { + // No reading from new handler until switch-over + return $this->currentHandler->read($sessionId); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function write($sessionId, $sessionData) + { + $result = $this->currentHandler->write($sessionId, $sessionData); + $this->writeOnlyHandler->write($sessionId, $sessionData); + + return $result; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function validateId($sessionId) + { + // No reading from new handler until switch-over + return $this->currentHandler->validateId($sessionId); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function updateTimestamp($sessionId, $sessionData) + { + $result = $this->currentHandler->updateTimestamp($sessionId, $sessionData); + $this->writeOnlyHandler->updateTimestamp($sessionId, $sessionData); + + return $result; + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php new file mode 100644 index 0000000..ef8f719 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php @@ -0,0 +1,193 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use MongoDB\BSON\Binary; +use MongoDB\BSON\UTCDateTime; +use MongoDB\Client; +use MongoDB\Collection; + +/** + * Session handler using the mongodb/mongodb package and MongoDB driver extension. + * + * @author Markus Bachmann + * + * @see https://packagist.org/packages/mongodb/mongodb + * @see https://php.net/mongodb + */ +class MongoDbSessionHandler extends AbstractSessionHandler +{ + private $mongo; + + /** + * @var Collection + */ + private $collection; + + /** + * @var array + */ + private $options; + + /** + * Constructor. + * + * List of available options: + * * database: The name of the database [required] + * * collection: The name of the collection [required] + * * id_field: The field name for storing the session id [default: _id] + * * data_field: The field name for storing the session data [default: data] + * * time_field: The field name for storing the timestamp [default: time] + * * expiry_field: The field name for storing the expiry-timestamp [default: expires_at]. + * + * It is strongly recommended to put an index on the `expiry_field` for + * garbage-collection. Alternatively it's possible to automatically expire + * the sessions in the database as described below: + * + * A TTL collections can be used on MongoDB 2.2+ to cleanup expired sessions + * automatically. Such an index can for example look like this: + * + * db..createIndex( + * { "": 1 }, + * { "expireAfterSeconds": 0 } + * ) + * + * More details on: https://docs.mongodb.org/manual/tutorial/expire-data/ + * + * If you use such an index, you can drop `gc_probability` to 0 since + * no garbage-collection is required. + * + * @throws \InvalidArgumentException When "database" or "collection" not provided + */ + public function __construct(Client $mongo, array $options) + { + if (!isset($options['database']) || !isset($options['collection'])) { + throw new \InvalidArgumentException('You must provide the "database" and "collection" option for MongoDBSessionHandler.'); + } + + $this->mongo = $mongo; + + $this->options = array_merge([ + 'id_field' => '_id', + 'data_field' => 'data', + 'time_field' => 'time', + 'expiry_field' => 'expires_at', + ], $options); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function close() + { + return true; + } + + /** + * {@inheritdoc} + */ + protected function doDestroy(string $sessionId) + { + $this->getCollection()->deleteOne([ + $this->options['id_field'] => $sessionId, + ]); + + return true; + } + + /** + * @return int|false + */ + #[\ReturnTypeWillChange] + public function gc($maxlifetime) + { + return $this->getCollection()->deleteMany([ + $this->options['expiry_field'] => ['$lt' => new UTCDateTime()], + ])->getDeletedCount(); + } + + /** + * {@inheritdoc} + */ + protected function doWrite(string $sessionId, string $data) + { + $expiry = new UTCDateTime((time() + (int) \ini_get('session.gc_maxlifetime')) * 1000); + + $fields = [ + $this->options['time_field'] => new UTCDateTime(), + $this->options['expiry_field'] => $expiry, + $this->options['data_field'] => new Binary($data, Binary::TYPE_OLD_BINARY), + ]; + + $this->getCollection()->updateOne( + [$this->options['id_field'] => $sessionId], + ['$set' => $fields], + ['upsert' => true] + ); + + return true; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function updateTimestamp($sessionId, $data) + { + $expiry = new UTCDateTime((time() + (int) \ini_get('session.gc_maxlifetime')) * 1000); + + $this->getCollection()->updateOne( + [$this->options['id_field'] => $sessionId], + ['$set' => [ + $this->options['time_field'] => new UTCDateTime(), + $this->options['expiry_field'] => $expiry, + ]] + ); + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doRead(string $sessionId) + { + $dbData = $this->getCollection()->findOne([ + $this->options['id_field'] => $sessionId, + $this->options['expiry_field'] => ['$gte' => new UTCDateTime()], + ]); + + if (null === $dbData) { + return ''; + } + + return $dbData[$this->options['data_field']]->getData(); + } + + private function getCollection(): Collection + { + if (null === $this->collection) { + $this->collection = $this->mongo->selectCollection($this->options['database'], $this->options['collection']); + } + + return $this->collection; + } + + /** + * @return Client + */ + protected function getMongo() + { + return $this->mongo; + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php new file mode 100644 index 0000000..570d4f4 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +/** + * Native session handler using PHP's built in file storage. + * + * @author Drak + */ +class NativeFileSessionHandler extends \SessionHandler +{ + /** + * @param string|null $savePath Path of directory to save session files + * Default null will leave setting as defined by PHP. + * '/path', 'N;/path', or 'N;octal-mode;/path + * + * @see https://php.net/session.configuration#ini.session.save-path for further details. + * + * @throws \InvalidArgumentException On invalid $savePath + * @throws \RuntimeException When failing to create the save directory + */ + public function __construct(?string $savePath = null) + { + if (null === $savePath) { + $savePath = \ini_get('session.save_path'); + } + + $baseDir = $savePath; + + if ($count = substr_count($savePath, ';')) { + if ($count > 2) { + throw new \InvalidArgumentException(sprintf('Invalid argument $savePath \'%s\'.', $savePath)); + } + + // characters after last ';' are the path + $baseDir = ltrim(strrchr($savePath, ';'), ';'); + } + + if ($baseDir && !is_dir($baseDir) && !@mkdir($baseDir, 0777, true) && !is_dir($baseDir)) { + throw new \RuntimeException(sprintf('Session Storage was not able to create directory "%s".', $baseDir)); + } + + if ($savePath !== \ini_get('session.save_path')) { + ini_set('session.save_path', $savePath); + } + if ('files' !== \ini_get('session.save_handler')) { + ini_set('session.save_handler', 'files'); + } + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php new file mode 100644 index 0000000..4331dbe --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +/** + * Can be used in unit testing or in a situations where persisted sessions are not desired. + * + * @author Drak + */ +class NullSessionHandler extends AbstractSessionHandler +{ + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function close() + { + return true; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function validateId($sessionId) + { + return true; + } + + /** + * {@inheritdoc} + */ + protected function doRead(string $sessionId) + { + return ''; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function updateTimestamp($sessionId, $data) + { + return true; + } + + /** + * {@inheritdoc} + */ + protected function doWrite(string $sessionId, string $data) + { + return true; + } + + /** + * {@inheritdoc} + */ + protected function doDestroy(string $sessionId) + { + return true; + } + + /** + * @return int|false + */ + #[\ReturnTypeWillChange] + public function gc($maxlifetime) + { + return 0; + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php new file mode 100644 index 0000000..f9c5d9b --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php @@ -0,0 +1,943 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +/** + * Session handler using a PDO connection to read and write data. + * + * It works with MySQL, PostgreSQL, Oracle, SQL Server and SQLite and implements + * different locking strategies to handle concurrent access to the same session. + * Locking is necessary to prevent loss of data due to race conditions and to keep + * the session data consistent between read() and write(). With locking, requests + * for the same session will wait until the other one finished writing. For this + * reason it's best practice to close a session as early as possible to improve + * concurrency. PHPs internal files session handler also implements locking. + * + * Attention: Since SQLite does not support row level locks but locks the whole database, + * it means only one session can be accessed at a time. Even different sessions would wait + * for another to finish. So saving session in SQLite should only be considered for + * development or prototypes. + * + * Session data is a binary string that can contain non-printable characters like the null byte. + * For this reason it must be saved in a binary column in the database like BLOB in MySQL. + * Saving it in a character column could corrupt the data. You can use createTable() + * to initialize a correctly defined table. + * + * @see https://php.net/sessionhandlerinterface + * + * @author Fabien Potencier + * @author Michael Williams + * @author Tobias Schultze + */ +class PdoSessionHandler extends AbstractSessionHandler +{ + /** + * No locking is done. This means sessions are prone to loss of data due to + * race conditions of concurrent requests to the same session. The last session + * write will win in this case. It might be useful when you implement your own + * logic to deal with this like an optimistic approach. + */ + public const LOCK_NONE = 0; + + /** + * Creates an application-level lock on a session. The disadvantage is that the + * lock is not enforced by the database and thus other, unaware parts of the + * application could still concurrently modify the session. The advantage is it + * does not require a transaction. + * This mode is not available for SQLite and not yet implemented for oci and sqlsrv. + */ + public const LOCK_ADVISORY = 1; + + /** + * Issues a real row lock. Since it uses a transaction between opening and + * closing a session, you have to be careful when you use same database connection + * that you also use for your application logic. This mode is the default because + * it's the only reliable solution across DBMSs. + */ + public const LOCK_TRANSACTIONAL = 2; + + private const MAX_LIFETIME = 315576000; + + /** + * @var \PDO|null PDO instance or null when not connected yet + */ + private $pdo; + + /** + * DSN string or null for session.save_path or false when lazy connection disabled. + * + * @var string|false|null + */ + private $dsn = false; + + /** + * @var string|null + */ + private $driver; + + /** + * @var string + */ + private $table = 'sessions'; + + /** + * @var string + */ + private $idCol = 'sess_id'; + + /** + * @var string + */ + private $dataCol = 'sess_data'; + + /** + * @var string + */ + private $lifetimeCol = 'sess_lifetime'; + + /** + * @var string + */ + private $timeCol = 'sess_time'; + + /** + * Username when lazy-connect. + * + * @var string|null + */ + private $username = null; + + /** + * Password when lazy-connect. + * + * @var string|null + */ + private $password = null; + + /** + * Connection options when lazy-connect. + * + * @var array + */ + private $connectionOptions = []; + + /** + * The strategy for locking, see constants. + * + * @var int + */ + private $lockMode = self::LOCK_TRANSACTIONAL; + + /** + * It's an array to support multiple reads before closing which is manual, non-standard usage. + * + * @var \PDOStatement[] An array of statements to release advisory locks + */ + private $unlockStatements = []; + + /** + * True when the current session exists but expired according to session.gc_maxlifetime. + * + * @var bool + */ + private $sessionExpired = false; + + /** + * Whether a transaction is active. + * + * @var bool + */ + private $inTransaction = false; + + /** + * Whether gc() has been called. + * + * @var bool + */ + private $gcCalled = false; + + /** + * You can either pass an existing database connection as PDO instance or + * pass a DSN string that will be used to lazy-connect to the database + * when the session is actually used. Furthermore it's possible to pass null + * which will then use the session.save_path ini setting as PDO DSN parameter. + * + * List of available options: + * * db_table: The name of the table [default: sessions] + * * db_id_col: The column where to store the session id [default: sess_id] + * * db_data_col: The column where to store the session data [default: sess_data] + * * db_lifetime_col: The column where to store the lifetime [default: sess_lifetime] + * * db_time_col: The column where to store the timestamp [default: sess_time] + * * db_username: The username when lazy-connect [default: ''] + * * db_password: The password when lazy-connect [default: ''] + * * db_connection_options: An array of driver-specific connection options [default: []] + * * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL] + * + * @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or URL string or null + * + * @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION + */ + public function __construct($pdoOrDsn = null, array $options = []) + { + if ($pdoOrDsn instanceof \PDO) { + if (\PDO::ERRMODE_EXCEPTION !== $pdoOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) { + throw new \InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)).', __CLASS__)); + } + + $this->pdo = $pdoOrDsn; + $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); + } elseif (\is_string($pdoOrDsn) && str_contains($pdoOrDsn, '://')) { + $this->dsn = $this->buildDsnFromUrl($pdoOrDsn); + } else { + $this->dsn = $pdoOrDsn; + } + + $this->table = $options['db_table'] ?? $this->table; + $this->idCol = $options['db_id_col'] ?? $this->idCol; + $this->dataCol = $options['db_data_col'] ?? $this->dataCol; + $this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol; + $this->timeCol = $options['db_time_col'] ?? $this->timeCol; + $this->username = $options['db_username'] ?? $this->username; + $this->password = $options['db_password'] ?? $this->password; + $this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions; + $this->lockMode = $options['lock_mode'] ?? $this->lockMode; + } + + /** + * Creates the table to store sessions which can be called once for setup. + * + * Session ID is saved in a column of maximum length 128 because that is enough even + * for a 512 bit configured session.hash_function like Whirlpool. Session data is + * saved in a BLOB. One could also use a shorter inlined varbinary column + * if one was sure the data fits into it. + * + * @throws \PDOException When the table already exists + * @throws \DomainException When an unsupported PDO driver is used + */ + public function createTable() + { + // connect if we are not yet + $this->getConnection(); + + switch ($this->driver) { + case 'mysql': + // We use varbinary for the ID column because it prevents unwanted conversions: + // - character set conversions between server and client + // - trailing space removal + // - case-insensitivity + // - language processing like é == e + $sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB"; + break; + case 'sqlite': + $sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; + break; + case 'pgsql': + $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; + break; + case 'oci': + $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; + break; + case 'sqlsrv': + $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; + break; + default: + throw new \DomainException(sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver)); + } + + try { + $this->pdo->exec($sql); + $this->pdo->exec("CREATE INDEX EXPIRY ON $this->table ($this->lifetimeCol)"); + } catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + } + + /** + * Returns true when the current session exists but expired according to session.gc_maxlifetime. + * + * Can be used to distinguish between a new session and one that expired due to inactivity. + * + * @return bool + */ + public function isSessionExpired() + { + return $this->sessionExpired; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function open($savePath, $sessionName) + { + $this->sessionExpired = false; + + if (null === $this->pdo) { + $this->connect($this->dsn ?: $savePath); + } + + return parent::open($savePath, $sessionName); + } + + /** + * @return string + */ + #[\ReturnTypeWillChange] + public function read($sessionId) + { + try { + return parent::read($sessionId); + } catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + } + + /** + * @return int|false + */ + #[\ReturnTypeWillChange] + public function gc($maxlifetime) + { + // We delay gc() to close() so that it is executed outside the transactional and blocking read-write process. + // This way, pruning expired sessions does not block them from being started while the current session is used. + $this->gcCalled = true; + + return 0; + } + + /** + * {@inheritdoc} + */ + protected function doDestroy(string $sessionId) + { + // delete the record associated with this id + $sql = "DELETE FROM $this->table WHERE $this->idCol = :id"; + + try { + $stmt = $this->pdo->prepare($sql); + $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $stmt->execute(); + } catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doWrite(string $sessionId, string $data) + { + $maxlifetime = (int) \ini_get('session.gc_maxlifetime'); + + try { + // We use a single MERGE SQL query when supported by the database. + $mergeStmt = $this->getMergeStatement($sessionId, $data, $maxlifetime); + if (null !== $mergeStmt) { + $mergeStmt->execute(); + + return true; + } + + $updateStmt = $this->getUpdateStatement($sessionId, $data, $maxlifetime); + $updateStmt->execute(); + + // When MERGE is not supported, like in Postgres < 9.5, we have to use this approach that can result in + // duplicate key errors when the same session is written simultaneously (given the LOCK_NONE behavior). + // We can just catch such an error and re-execute the update. This is similar to a serializable + // transaction with retry logic on serialization failures but without the overhead and without possible + // false positives due to longer gap locking. + if (!$updateStmt->rowCount()) { + try { + $insertStmt = $this->getInsertStatement($sessionId, $data, $maxlifetime); + $insertStmt->execute(); + } catch (\PDOException $e) { + // Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys + if (str_starts_with($e->getCode(), '23')) { + $updateStmt->execute(); + } else { + throw $e; + } + } + } + } catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + + return true; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function updateTimestamp($sessionId, $data) + { + $expiry = time() + (int) \ini_get('session.gc_maxlifetime'); + + try { + $updateStmt = $this->pdo->prepare( + "UPDATE $this->table SET $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id" + ); + $updateStmt->bindValue(':id', $sessionId, \PDO::PARAM_STR); + $updateStmt->bindValue(':expiry', $expiry, \PDO::PARAM_INT); + $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); + $updateStmt->execute(); + } catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + + return true; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function close() + { + $this->commit(); + + while ($unlockStmt = array_shift($this->unlockStatements)) { + $unlockStmt->execute(); + } + + if ($this->gcCalled) { + $this->gcCalled = false; + + // delete the session records that have expired + $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol < :time AND $this->lifetimeCol > :min"; + $stmt = $this->pdo->prepare($sql); + $stmt->bindValue(':time', time(), \PDO::PARAM_INT); + $stmt->bindValue(':min', self::MAX_LIFETIME, \PDO::PARAM_INT); + $stmt->execute(); + // to be removed in 6.0 + if ('mysql' === $this->driver) { + $legacySql = "DELETE FROM $this->table WHERE $this->lifetimeCol <= :min AND $this->lifetimeCol + $this->timeCol < :time"; + } else { + $legacySql = "DELETE FROM $this->table WHERE $this->lifetimeCol <= :min AND $this->lifetimeCol < :time - $this->timeCol"; + } + + $stmt = $this->pdo->prepare($legacySql); + $stmt->bindValue(':time', time(), \PDO::PARAM_INT); + $stmt->bindValue(':min', self::MAX_LIFETIME, \PDO::PARAM_INT); + $stmt->execute(); + } + + if (false !== $this->dsn) { + $this->pdo = null; // only close lazy-connection + $this->driver = null; + } + + return true; + } + + /** + * Lazy-connects to the database. + */ + private function connect(string $dsn): void + { + $this->pdo = new \PDO($dsn, $this->username, $this->password, $this->connectionOptions); + $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); + } + + /** + * Builds a PDO DSN from a URL-like connection string. + * + * @todo implement missing support for oci DSN (which look totally different from other PDO ones) + */ + private function buildDsnFromUrl(string $dsnOrUrl): string + { + // (pdo_)?sqlite3?:///... => (pdo_)?sqlite3?://localhost/... or else the URL will be invalid + $url = preg_replace('#^((?:pdo_)?sqlite3?):///#', '$1://localhost/', $dsnOrUrl); + + $params = parse_url($url); + + if (false === $params) { + return $dsnOrUrl; // If the URL is not valid, let's assume it might be a DSN already. + } + + $params = array_map('rawurldecode', $params); + + // Override the default username and password. Values passed through options will still win over these in the constructor. + if (isset($params['user'])) { + $this->username = $params['user']; + } + + if (isset($params['pass'])) { + $this->password = $params['pass']; + } + + if (!isset($params['scheme'])) { + throw new \InvalidArgumentException('URLs without scheme are not supported to configure the PdoSessionHandler.'); + } + + $driverAliasMap = [ + 'mssql' => 'sqlsrv', + 'mysql2' => 'mysql', // Amazon RDS, for some weird reason + 'postgres' => 'pgsql', + 'postgresql' => 'pgsql', + 'sqlite3' => 'sqlite', + ]; + + $driver = $driverAliasMap[$params['scheme']] ?? $params['scheme']; + + // Doctrine DBAL supports passing its internal pdo_* driver names directly too (allowing both dashes and underscores). This allows supporting the same here. + if (str_starts_with($driver, 'pdo_') || str_starts_with($driver, 'pdo-')) { + $driver = substr($driver, 4); + } + + $dsn = null; + switch ($driver) { + case 'mysql': + $dsn = 'mysql:'; + if ('' !== ($params['query'] ?? '')) { + $queryParams = []; + parse_str($params['query'], $queryParams); + if ('' !== ($queryParams['charset'] ?? '')) { + $dsn .= 'charset='.$queryParams['charset'].';'; + } + + if ('' !== ($queryParams['unix_socket'] ?? '')) { + $dsn .= 'unix_socket='.$queryParams['unix_socket'].';'; + + if (isset($params['path'])) { + $dbName = substr($params['path'], 1); // Remove the leading slash + $dsn .= 'dbname='.$dbName.';'; + } + + return $dsn; + } + } + // If "unix_socket" is not in the query, we continue with the same process as pgsql + // no break + case 'pgsql': + $dsn ?? $dsn = 'pgsql:'; + + if (isset($params['host']) && '' !== $params['host']) { + $dsn .= 'host='.$params['host'].';'; + } + + if (isset($params['port']) && '' !== $params['port']) { + $dsn .= 'port='.$params['port'].';'; + } + + if (isset($params['path'])) { + $dbName = substr($params['path'], 1); // Remove the leading slash + $dsn .= 'dbname='.$dbName.';'; + } + + return $dsn; + + case 'sqlite': + return 'sqlite:'.substr($params['path'], 1); + + case 'sqlsrv': + $dsn = 'sqlsrv:server='; + + if (isset($params['host'])) { + $dsn .= $params['host']; + } + + if (isset($params['port']) && '' !== $params['port']) { + $dsn .= ','.$params['port']; + } + + if (isset($params['path'])) { + $dbName = substr($params['path'], 1); // Remove the leading slash + $dsn .= ';Database='.$dbName; + } + + return $dsn; + + default: + throw new \InvalidArgumentException(sprintf('The scheme "%s" is not supported by the PdoSessionHandler URL configuration. Pass a PDO DSN directly.', $params['scheme'])); + } + } + + /** + * Helper method to begin a transaction. + * + * Since SQLite does not support row level locks, we have to acquire a reserved lock + * on the database immediately. Because of https://bugs.php.net/42766 we have to create + * such a transaction manually which also means we cannot use PDO::commit or + * PDO::rollback or PDO::inTransaction for SQLite. + * + * Also MySQLs default isolation, REPEATABLE READ, causes deadlock for different sessions + * due to https://percona.com/blog/2013/12/12/one-more-innodb-gap-lock-to-avoid/ . + * So we change it to READ COMMITTED. + */ + private function beginTransaction(): void + { + if (!$this->inTransaction) { + if ('sqlite' === $this->driver) { + $this->pdo->exec('BEGIN IMMEDIATE TRANSACTION'); + } else { + if ('mysql' === $this->driver) { + $this->pdo->exec('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'); + } + $this->pdo->beginTransaction(); + } + $this->inTransaction = true; + } + } + + /** + * Helper method to commit a transaction. + */ + private function commit(): void + { + if ($this->inTransaction) { + try { + // commit read-write transaction which also releases the lock + if ('sqlite' === $this->driver) { + $this->pdo->exec('COMMIT'); + } else { + $this->pdo->commit(); + } + $this->inTransaction = false; + } catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + } + } + + /** + * Helper method to rollback a transaction. + */ + private function rollback(): void + { + // We only need to rollback if we are in a transaction. Otherwise the resulting + // error would hide the real problem why rollback was called. We might not be + // in a transaction when not using the transactional locking behavior or when + // two callbacks (e.g. destroy and write) are invoked that both fail. + if ($this->inTransaction) { + if ('sqlite' === $this->driver) { + $this->pdo->exec('ROLLBACK'); + } else { + $this->pdo->rollBack(); + } + $this->inTransaction = false; + } + } + + /** + * Reads the session data in respect to the different locking strategies. + * + * We need to make sure we do not return session data that is already considered garbage according + * to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes. + * + * @return string + */ + protected function doRead(string $sessionId) + { + if (self::LOCK_ADVISORY === $this->lockMode) { + $this->unlockStatements[] = $this->doAdvisoryLock($sessionId); + } + + $selectSql = $this->getSelectSql(); + $selectStmt = $this->pdo->prepare($selectSql); + $selectStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $insertStmt = null; + + while (true) { + $selectStmt->execute(); + $sessionRows = $selectStmt->fetchAll(\PDO::FETCH_NUM); + + if ($sessionRows) { + $expiry = (int) $sessionRows[0][1]; + if ($expiry <= self::MAX_LIFETIME) { + $expiry += $sessionRows[0][2]; + } + + if ($expiry < time()) { + $this->sessionExpired = true; + + return ''; + } + + return \is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0]; + } + + if (null !== $insertStmt) { + $this->rollback(); + throw new \RuntimeException('Failed to read session: INSERT reported a duplicate id but next SELECT did not return any data.'); + } + + if (!filter_var(\ini_get('session.use_strict_mode'), \FILTER_VALIDATE_BOOLEAN) && self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) { + // In strict mode, session fixation is not possible: new sessions always start with a unique + // random id, so that concurrency is not possible and this code path can be skipped. + // Exclusive-reading of non-existent rows does not block, so we need to do an insert to block + // until other connections to the session are committed. + try { + $insertStmt = $this->getInsertStatement($sessionId, '', 0); + $insertStmt->execute(); + } catch (\PDOException $e) { + // Catch duplicate key error because other connection created the session already. + // It would only not be the case when the other connection destroyed the session. + if (str_starts_with($e->getCode(), '23')) { + // Retrieve finished session data written by concurrent connection by restarting the loop. + // We have to start a new transaction as a failed query will mark the current transaction as + // aborted in PostgreSQL and disallow further queries within it. + $this->rollback(); + $this->beginTransaction(); + continue; + } + + throw $e; + } + } + + return ''; + } + } + + /** + * Executes an application-level lock on the database. + * + * @return \PDOStatement The statement that needs to be executed later to release the lock + * + * @throws \DomainException When an unsupported PDO driver is used + * + * @todo implement missing advisory locks + * - for oci using DBMS_LOCK.REQUEST + * - for sqlsrv using sp_getapplock with LockOwner = Session + */ + private function doAdvisoryLock(string $sessionId): \PDOStatement + { + switch ($this->driver) { + case 'mysql': + // MySQL 5.7.5 and later enforces a maximum length on lock names of 64 characters. Previously, no limit was enforced. + $lockId = substr($sessionId, 0, 64); + // should we handle the return value? 0 on timeout, null on error + // we use a timeout of 50 seconds which is also the default for innodb_lock_wait_timeout + $stmt = $this->pdo->prepare('SELECT GET_LOCK(:key, 50)'); + $stmt->bindValue(':key', $lockId, \PDO::PARAM_STR); + $stmt->execute(); + + $releaseStmt = $this->pdo->prepare('DO RELEASE_LOCK(:key)'); + $releaseStmt->bindValue(':key', $lockId, \PDO::PARAM_STR); + + return $releaseStmt; + case 'pgsql': + // Obtaining an exclusive session level advisory lock requires an integer key. + // When session.sid_bits_per_character > 4, the session id can contain non-hex-characters. + // So we cannot just use hexdec(). + if (4 === \PHP_INT_SIZE) { + $sessionInt1 = $this->convertStringToInt($sessionId); + $sessionInt2 = $this->convertStringToInt(substr($sessionId, 4, 4)); + + $stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key1, :key2)'); + $stmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT); + $stmt->bindValue(':key2', $sessionInt2, \PDO::PARAM_INT); + $stmt->execute(); + + $releaseStmt = $this->pdo->prepare('SELECT pg_advisory_unlock(:key1, :key2)'); + $releaseStmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT); + $releaseStmt->bindValue(':key2', $sessionInt2, \PDO::PARAM_INT); + } else { + $sessionBigInt = $this->convertStringToInt($sessionId); + + $stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key)'); + $stmt->bindValue(':key', $sessionBigInt, \PDO::PARAM_INT); + $stmt->execute(); + + $releaseStmt = $this->pdo->prepare('SELECT pg_advisory_unlock(:key)'); + $releaseStmt->bindValue(':key', $sessionBigInt, \PDO::PARAM_INT); + } + + return $releaseStmt; + case 'sqlite': + throw new \DomainException('SQLite does not support advisory locks.'); + default: + throw new \DomainException(sprintf('Advisory locks are currently not implemented for PDO driver "%s".', $this->driver)); + } + } + + /** + * Encodes the first 4 (when PHP_INT_SIZE == 4) or 8 characters of the string as an integer. + * + * Keep in mind, PHP integers are signed. + */ + private function convertStringToInt(string $string): int + { + if (4 === \PHP_INT_SIZE) { + return (\ord($string[3]) << 24) + (\ord($string[2]) << 16) + (\ord($string[1]) << 8) + \ord($string[0]); + } + + $int1 = (\ord($string[7]) << 24) + (\ord($string[6]) << 16) + (\ord($string[5]) << 8) + \ord($string[4]); + $int2 = (\ord($string[3]) << 24) + (\ord($string[2]) << 16) + (\ord($string[1]) << 8) + \ord($string[0]); + + return $int2 + ($int1 << 32); + } + + /** + * Return a locking or nonlocking SQL query to read session information. + * + * @throws \DomainException When an unsupported PDO driver is used + */ + private function getSelectSql(): string + { + if (self::LOCK_TRANSACTIONAL === $this->lockMode) { + $this->beginTransaction(); + + // selecting the time column should be removed in 6.0 + switch ($this->driver) { + case 'mysql': + case 'oci': + case 'pgsql': + return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id FOR UPDATE"; + case 'sqlsrv': + return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WITH (UPDLOCK, ROWLOCK) WHERE $this->idCol = :id"; + case 'sqlite': + // we already locked when starting transaction + break; + default: + throw new \DomainException(sprintf('Transactional locks are currently not implemented for PDO driver "%s".', $this->driver)); + } + } + + return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id"; + } + + /** + * Returns an insert statement supported by the database for writing session data. + */ + private function getInsertStatement(string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement + { + switch ($this->driver) { + case 'oci': + $data = fopen('php://memory', 'r+'); + fwrite($data, $sessionData); + rewind($data); + $sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, EMPTY_BLOB(), :expiry, :time) RETURNING $this->dataCol into :data"; + break; + default: + $data = $sessionData; + $sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)"; + break; + } + + $stmt = $this->pdo->prepare($sql); + $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $stmt->bindParam(':data', $data, \PDO::PARAM_LOB); + $stmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT); + $stmt->bindValue(':time', time(), \PDO::PARAM_INT); + + return $stmt; + } + + /** + * Returns an update statement supported by the database for writing session data. + */ + private function getUpdateStatement(string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement + { + switch ($this->driver) { + case 'oci': + $data = fopen('php://memory', 'r+'); + fwrite($data, $sessionData); + rewind($data); + $sql = "UPDATE $this->table SET $this->dataCol = EMPTY_BLOB(), $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id RETURNING $this->dataCol into :data"; + break; + default: + $data = $sessionData; + $sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id"; + break; + } + + $stmt = $this->pdo->prepare($sql); + $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $stmt->bindParam(':data', $data, \PDO::PARAM_LOB); + $stmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT); + $stmt->bindValue(':time', time(), \PDO::PARAM_INT); + + return $stmt; + } + + /** + * Returns a merge/upsert (i.e. insert or update) statement when supported by the database for writing session data. + */ + private function getMergeStatement(string $sessionId, string $data, int $maxlifetime): ?\PDOStatement + { + switch (true) { + case 'mysql' === $this->driver: + $mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time) ". + "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)"; + break; + case 'sqlsrv' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='): + // MERGE is only available since SQL Server 2008 and must be terminated by semicolon + // It also requires HOLDLOCK according to https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/ + $mergeSql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ". + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". + "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;"; + break; + case 'sqlite' === $this->driver: + $mergeSql = "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)"; + break; + case 'pgsql' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '9.5', '>='): + $mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time) ". + "ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)"; + break; + default: + // MERGE is not supported with LOBs: https://oracle.com/technetwork/articles/fuecks-lobs-095315.html + return null; + } + + $mergeStmt = $this->pdo->prepare($mergeSql); + + if ('sqlsrv' === $this->driver) { + $mergeStmt->bindParam(1, $sessionId, \PDO::PARAM_STR); + $mergeStmt->bindParam(2, $sessionId, \PDO::PARAM_STR); + $mergeStmt->bindParam(3, $data, \PDO::PARAM_LOB); + $mergeStmt->bindValue(4, time() + $maxlifetime, \PDO::PARAM_INT); + $mergeStmt->bindValue(5, time(), \PDO::PARAM_INT); + $mergeStmt->bindParam(6, $data, \PDO::PARAM_LOB); + $mergeStmt->bindValue(7, time() + $maxlifetime, \PDO::PARAM_INT); + $mergeStmt->bindValue(8, time(), \PDO::PARAM_INT); + } else { + $mergeStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $mergeStmt->bindParam(':data', $data, \PDO::PARAM_LOB); + $mergeStmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT); + $mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT); + } + + return $mergeStmt; + } + + /** + * Return a PDO instance. + * + * @return \PDO + */ + protected function getConnection() + { + if (null === $this->pdo) { + $this->connect($this->dsn ?: \ini_get('session.save_path')); + } + + return $this->pdo; + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php new file mode 100644 index 0000000..31954e6 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Predis\Response\ErrorInterface; +use Symfony\Component\Cache\Traits\RedisClusterProxy; +use Symfony\Component\Cache\Traits\RedisProxy; + +/** + * Redis based session storage handler based on the Redis class + * provided by the PHP redis extension. + * + * @author Dalibor Karlović + */ +class RedisSessionHandler extends AbstractSessionHandler +{ + private $redis; + + /** + * @var string Key prefix for shared environments + */ + private $prefix; + + /** + * @var int Time to live in seconds + */ + private $ttl; + + /** + * List of available options: + * * prefix: The prefix to use for the keys in order to avoid collision on the Redis server + * * ttl: The time to live in seconds. + * + * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis + * + * @throws \InvalidArgumentException When unsupported client or options are passed + */ + public function __construct($redis, array $options = []) + { + if ( + !$redis instanceof \Redis && + !$redis instanceof \RedisArray && + !$redis instanceof \RedisCluster && + !$redis instanceof \Predis\ClientInterface && + !$redis instanceof RedisProxy && + !$redis instanceof RedisClusterProxy + ) { + throw new \InvalidArgumentException(sprintf('"%s()" expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\ClientInterface, "%s" given.', __METHOD__, get_debug_type($redis))); + } + + if ($diff = array_diff(array_keys($options), ['prefix', 'ttl'])) { + throw new \InvalidArgumentException(sprintf('The following options are not supported "%s".', implode(', ', $diff))); + } + + $this->redis = $redis; + $this->prefix = $options['prefix'] ?? 'sf_s'; + $this->ttl = $options['ttl'] ?? null; + } + + /** + * {@inheritdoc} + */ + protected function doRead(string $sessionId): string + { + return $this->redis->get($this->prefix.$sessionId) ?: ''; + } + + /** + * {@inheritdoc} + */ + protected function doWrite(string $sessionId, string $data): bool + { + $result = $this->redis->setEx($this->prefix.$sessionId, (int) ($this->ttl ?? \ini_get('session.gc_maxlifetime')), $data); + + return $result && !$result instanceof ErrorInterface; + } + + /** + * {@inheritdoc} + */ + protected function doDestroy(string $sessionId): bool + { + static $unlink = true; + + if ($unlink) { + try { + $unlink = false !== $this->redis->unlink($this->prefix.$sessionId); + } catch (\Throwable $e) { + $unlink = false; + } + } + + if (!$unlink) { + $this->redis->del($this->prefix.$sessionId); + } + + return true; + } + + /** + * {@inheritdoc} + */ + #[\ReturnTypeWillChange] + public function close(): bool + { + return true; + } + + /** + * {@inheritdoc} + * + * @return int|false + */ + #[\ReturnTypeWillChange] + public function gc($maxlifetime) + { + return 0; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function updateTimestamp($sessionId, $data) + { + return (bool) $this->redis->expire($this->prefix.$sessionId, (int) ($this->ttl ?? \ini_get('session.gc_maxlifetime'))); + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php b/vendor/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php new file mode 100644 index 0000000..76e4373 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Doctrine\DBAL\Configuration; +use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; +use Doctrine\DBAL\Tools\DsnParser; +use Symfony\Component\Cache\Adapter\AbstractAdapter; +use Symfony\Component\Cache\Traits\RedisClusterProxy; +use Symfony\Component\Cache\Traits\RedisProxy; + +/** + * @author Nicolas Grekas + */ +class SessionHandlerFactory +{ + /** + * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy|\Memcached|\PDO|string $connection Connection or DSN + */ + public static function createHandler($connection): AbstractSessionHandler + { + if (!\is_string($connection) && !\is_object($connection)) { + throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a string or a connection object, "%s" given.', __METHOD__, get_debug_type($connection))); + } + + if ($options = \is_string($connection) ? parse_url($connection) : false) { + parse_str($options['query'] ?? '', $options); + } + + switch (true) { + case $connection instanceof \Redis: + case $connection instanceof \RedisArray: + case $connection instanceof \RedisCluster: + case $connection instanceof \Predis\ClientInterface: + case $connection instanceof RedisProxy: + case $connection instanceof RedisClusterProxy: + return new RedisSessionHandler($connection); + + case $connection instanceof \Memcached: + return new MemcachedSessionHandler($connection); + + case $connection instanceof \PDO: + return new PdoSessionHandler($connection); + + case !\is_string($connection): + throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', get_debug_type($connection))); + case str_starts_with($connection, 'file://'): + $savePath = substr($connection, 7); + + return new StrictSessionHandler(new NativeFileSessionHandler('' === $savePath ? null : $savePath)); + + case str_starts_with($connection, 'redis:'): + case str_starts_with($connection, 'rediss:'): + case str_starts_with($connection, 'memcached:'): + if (!class_exists(AbstractAdapter::class)) { + throw new \InvalidArgumentException('Unsupported Redis or Memcached DSN. Try running "composer require symfony/cache".'); + } + $handlerClass = str_starts_with($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class; + $connection = AbstractAdapter::createConnection($connection, ['lazy' => true]); + + return new $handlerClass($connection, array_intersect_key($options ?: [], ['prefix' => 1, 'ttl' => 1])); + + case str_starts_with($connection, 'pdo_oci://'): + if (!class_exists(DriverManager::class)) { + throw new \InvalidArgumentException('Unsupported PDO OCI DSN. Try running "composer require doctrine/dbal".'); + } + $connection[3] = '-'; + $params = class_exists(DsnParser::class) ? (new DsnParser())->parse($connection) : ['url' => $connection]; + $config = new Configuration(); + if (class_exists(DefaultSchemaManagerFactory::class)) { + $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); + } + + $connection = DriverManager::getConnection($params, $config); + // The condition should be removed once support for DBAL <3.3 is dropped + $connection = method_exists($connection, 'getNativeConnection') ? $connection->getNativeConnection() : $connection->getWrappedConnection(); + // no break; + + case str_starts_with($connection, 'mssql://'): + case str_starts_with($connection, 'mysql://'): + case str_starts_with($connection, 'mysql2://'): + case str_starts_with($connection, 'pgsql://'): + case str_starts_with($connection, 'postgres://'): + case str_starts_with($connection, 'postgresql://'): + case str_starts_with($connection, 'sqlsrv://'): + case str_starts_with($connection, 'sqlite://'): + case str_starts_with($connection, 'sqlite3://'): + return new PdoSessionHandler($connection, $options ?: []); + } + + throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', $connection)); + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php new file mode 100644 index 0000000..f7c385f --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +/** + * Adds basic `SessionUpdateTimestampHandlerInterface` behaviors to another `SessionHandlerInterface`. + * + * @author Nicolas Grekas + */ +class StrictSessionHandler extends AbstractSessionHandler +{ + private $handler; + private $doDestroy; + + public function __construct(\SessionHandlerInterface $handler) + { + if ($handler instanceof \SessionUpdateTimestampHandlerInterface) { + throw new \LogicException(sprintf('"%s" is already an instance of "SessionUpdateTimestampHandlerInterface", you cannot wrap it with "%s".', get_debug_type($handler), self::class)); + } + + $this->handler = $handler; + } + + /** + * Returns true if this handler wraps an internal PHP session save handler using \SessionHandler. + * + * @internal + */ + public function isWrapper(): bool + { + return $this->handler instanceof \SessionHandler; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function open($savePath, $sessionName) + { + parent::open($savePath, $sessionName); + + return $this->handler->open($savePath, $sessionName); + } + + /** + * {@inheritdoc} + */ + protected function doRead(string $sessionId) + { + return $this->handler->read($sessionId); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function updateTimestamp($sessionId, $data) + { + return $this->write($sessionId, $data); + } + + /** + * {@inheritdoc} + */ + protected function doWrite(string $sessionId, string $data) + { + return $this->handler->write($sessionId, $data); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function destroy($sessionId) + { + $this->doDestroy = true; + $destroyed = parent::destroy($sessionId); + + return $this->doDestroy ? $this->doDestroy($sessionId) : $destroyed; + } + + /** + * {@inheritdoc} + */ + protected function doDestroy(string $sessionId) + { + $this->doDestroy = false; + + return $this->handler->destroy($sessionId); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function close() + { + return $this->handler->close(); + } + + /** + * @return int|false + */ + #[\ReturnTypeWillChange] + public function gc($maxlifetime) + { + return $this->handler->gc($maxlifetime); + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/MetadataBag.php b/vendor/symfony/http-foundation/Session/Storage/MetadataBag.php new file mode 100644 index 0000000..3e10f6d --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/MetadataBag.php @@ -0,0 +1,167 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Session\SessionBagInterface; + +/** + * Metadata container. + * + * Adds metadata to the session. + * + * @author Drak + */ +class MetadataBag implements SessionBagInterface +{ + public const CREATED = 'c'; + public const UPDATED = 'u'; + public const LIFETIME = 'l'; + + /** + * @var string + */ + private $name = '__metadata'; + + /** + * @var string + */ + private $storageKey; + + /** + * @var array + */ + protected $meta = [self::CREATED => 0, self::UPDATED => 0, self::LIFETIME => 0]; + + /** + * Unix timestamp. + * + * @var int + */ + private $lastUsed; + + /** + * @var int + */ + private $updateThreshold; + + /** + * @param string $storageKey The key used to store bag in the session + * @param int $updateThreshold The time to wait between two UPDATED updates + */ + public function __construct(string $storageKey = '_sf2_meta', int $updateThreshold = 0) + { + $this->storageKey = $storageKey; + $this->updateThreshold = $updateThreshold; + } + + /** + * {@inheritdoc} + */ + public function initialize(array &$array) + { + $this->meta = &$array; + + if (isset($array[self::CREATED])) { + $this->lastUsed = $this->meta[self::UPDATED]; + + $timeStamp = time(); + if ($timeStamp - $array[self::UPDATED] >= $this->updateThreshold) { + $this->meta[self::UPDATED] = $timeStamp; + } + } else { + $this->stampCreated(); + } + } + + /** + * Gets the lifetime that the session cookie was set with. + * + * @return int + */ + public function getLifetime() + { + return $this->meta[self::LIFETIME]; + } + + /** + * Stamps a new session's metadata. + * + * @param int|null $lifetime Sets the cookie lifetime for the session cookie. A null value + * will leave the system settings unchanged, 0 sets the cookie + * to expire with browser session. Time is in seconds, and is + * not a Unix timestamp. + */ + public function stampNew(?int $lifetime = null) + { + $this->stampCreated($lifetime); + } + + /** + * {@inheritdoc} + */ + public function getStorageKey() + { + return $this->storageKey; + } + + /** + * Gets the created timestamp metadata. + * + * @return int Unix timestamp + */ + public function getCreated() + { + return $this->meta[self::CREATED]; + } + + /** + * Gets the last used metadata. + * + * @return int Unix timestamp + */ + public function getLastUsed() + { + return $this->lastUsed; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + // nothing to do + return null; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->name; + } + + /** + * Sets name. + */ + public function setName(string $name) + { + $this->name = $name; + } + + private function stampCreated(?int $lifetime = null): void + { + $timeStamp = time(); + $this->meta[self::CREATED] = $this->meta[self::UPDATED] = $this->lastUsed = $timeStamp; + $this->meta[self::LIFETIME] = $lifetime ?? (int) \ini_get('session.cookie_lifetime'); + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php b/vendor/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php new file mode 100644 index 0000000..c6a28b1 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php @@ -0,0 +1,249 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Session\SessionBagInterface; + +/** + * MockArraySessionStorage mocks the session for unit tests. + * + * No PHP session is actually started since a session can be initialized + * and shutdown only once per PHP execution cycle. + * + * When doing functional testing, you should use MockFileSessionStorage instead. + * + * @author Fabien Potencier + * @author Bulat Shakirzyanov + * @author Drak + */ +class MockArraySessionStorage implements SessionStorageInterface +{ + /** + * @var string + */ + protected $id = ''; + + /** + * @var string + */ + protected $name; + + /** + * @var bool + */ + protected $started = false; + + /** + * @var bool + */ + protected $closed = false; + + /** + * @var array + */ + protected $data = []; + + /** + * @var MetadataBag + */ + protected $metadataBag; + + /** + * @var array|SessionBagInterface[] + */ + protected $bags = []; + + public function __construct(string $name = 'MOCKSESSID', ?MetadataBag $metaBag = null) + { + $this->name = $name; + $this->setMetadataBag($metaBag); + } + + public function setSessionData(array $array) + { + $this->data = $array; + } + + /** + * {@inheritdoc} + */ + public function start() + { + if ($this->started) { + return true; + } + + if (empty($this->id)) { + $this->id = $this->generateId(); + } + + $this->loadSession(); + + return true; + } + + /** + * {@inheritdoc} + */ + public function regenerate(bool $destroy = false, ?int $lifetime = null) + { + if (!$this->started) { + $this->start(); + } + + $this->metadataBag->stampNew($lifetime); + $this->id = $this->generateId(); + + return true; + } + + /** + * {@inheritdoc} + */ + public function getId() + { + return $this->id; + } + + /** + * {@inheritdoc} + */ + public function setId(string $id) + { + if ($this->started) { + throw new \LogicException('Cannot set session ID after the session has started.'); + } + + $this->id = $id; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * {@inheritdoc} + */ + public function save() + { + if (!$this->started || $this->closed) { + throw new \RuntimeException('Trying to save a session that was not started yet or was already closed.'); + } + // nothing to do since we don't persist the session data + $this->closed = false; + $this->started = false; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + // clear out the bags + foreach ($this->bags as $bag) { + $bag->clear(); + } + + // clear out the session + $this->data = []; + + // reconnect the bags to the session + $this->loadSession(); + } + + /** + * {@inheritdoc} + */ + public function registerBag(SessionBagInterface $bag) + { + $this->bags[$bag->getName()] = $bag; + } + + /** + * {@inheritdoc} + */ + public function getBag(string $name) + { + if (!isset($this->bags[$name])) { + throw new \InvalidArgumentException(sprintf('The SessionBagInterface "%s" is not registered.', $name)); + } + + if (!$this->started) { + $this->start(); + } + + return $this->bags[$name]; + } + + /** + * {@inheritdoc} + */ + public function isStarted() + { + return $this->started; + } + + public function setMetadataBag(?MetadataBag $bag = null) + { + if (null === $bag) { + $bag = new MetadataBag(); + } + + $this->metadataBag = $bag; + } + + /** + * Gets the MetadataBag. + * + * @return MetadataBag + */ + public function getMetadataBag() + { + return $this->metadataBag; + } + + /** + * Generates a session ID. + * + * @return string + */ + protected function generateId() + { + return bin2hex(random_bytes(16)); + } + + protected function loadSession() + { + $bags = array_merge($this->bags, [$this->metadataBag]); + + foreach ($bags as $bag) { + $key = $bag->getStorageKey(); + $this->data[$key] = $this->data[$key] ?? []; + $bag->initialize($this->data[$key]); + } + + $this->started = true; + $this->closed = false; + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php b/vendor/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php new file mode 100644 index 0000000..8aeb972 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php @@ -0,0 +1,160 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +/** + * MockFileSessionStorage is used to mock sessions for + * functional testing where you may need to persist session data + * across separate PHP processes. + * + * No PHP session is actually started since a session can be initialized + * and shutdown only once per PHP execution cycle and this class does + * not pollute any session related globals, including session_*() functions + * or session.* PHP ini directives. + * + * @author Drak + */ +class MockFileSessionStorage extends MockArraySessionStorage +{ + private $savePath; + + /** + * @param string|null $savePath Path of directory to save session files + */ + public function __construct(?string $savePath = null, string $name = 'MOCKSESSID', ?MetadataBag $metaBag = null) + { + if (null === $savePath) { + $savePath = sys_get_temp_dir(); + } + + if (!is_dir($savePath) && !@mkdir($savePath, 0777, true) && !is_dir($savePath)) { + throw new \RuntimeException(sprintf('Session Storage was not able to create directory "%s".', $savePath)); + } + + $this->savePath = $savePath; + + parent::__construct($name, $metaBag); + } + + /** + * {@inheritdoc} + */ + public function start() + { + if ($this->started) { + return true; + } + + if (!$this->id) { + $this->id = $this->generateId(); + } + + $this->read(); + + $this->started = true; + + return true; + } + + /** + * {@inheritdoc} + */ + public function regenerate(bool $destroy = false, ?int $lifetime = null) + { + if (!$this->started) { + $this->start(); + } + + if ($destroy) { + $this->destroy(); + } + + return parent::regenerate($destroy, $lifetime); + } + + /** + * {@inheritdoc} + */ + public function save() + { + if (!$this->started) { + throw new \RuntimeException('Trying to save a session that was not started yet or was already closed.'); + } + + $data = $this->data; + + foreach ($this->bags as $bag) { + if (empty($data[$key = $bag->getStorageKey()])) { + unset($data[$key]); + } + } + if ([$key = $this->metadataBag->getStorageKey()] === array_keys($data)) { + unset($data[$key]); + } + + try { + if ($data) { + $path = $this->getFilePath(); + $tmp = $path.bin2hex(random_bytes(6)); + file_put_contents($tmp, serialize($data)); + rename($tmp, $path); + } else { + $this->destroy(); + } + } finally { + $this->data = $data; + } + + // this is needed when the session object is re-used across multiple requests + // in functional tests. + $this->started = false; + } + + /** + * Deletes a session from persistent storage. + * Deliberately leaves session data in memory intact. + */ + private function destroy(): void + { + set_error_handler(static function () {}); + try { + unlink($this->getFilePath()); + } finally { + restore_error_handler(); + } + } + + /** + * Calculate path to file. + */ + private function getFilePath(): string + { + return $this->savePath.'/'.$this->id.'.mocksess'; + } + + /** + * Reads session from storage and loads session. + */ + private function read(): void + { + set_error_handler(static function () {}); + try { + $data = file_get_contents($this->getFilePath()); + } finally { + restore_error_handler(); + } + + $this->data = $data ? unserialize($data) : []; + + $this->loadSession(); + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php b/vendor/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php new file mode 100644 index 0000000..900fa7c --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Request; + +// Help opcache.preload discover always-needed symbols +class_exists(MockFileSessionStorage::class); + +/** + * @author Jérémy Derussé + */ +class MockFileSessionStorageFactory implements SessionStorageFactoryInterface +{ + private $savePath; + private $name; + private $metaBag; + + /** + * @see MockFileSessionStorage constructor. + */ + public function __construct(?string $savePath = null, string $name = 'MOCKSESSID', ?MetadataBag $metaBag = null) + { + $this->savePath = $savePath; + $this->name = $name; + $this->metaBag = $metaBag; + } + + public function createStorage(?Request $request): SessionStorageInterface + { + return new MockFileSessionStorage($this->savePath, $this->name, $this->metaBag); + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php b/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php new file mode 100644 index 0000000..e7b42ed --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php @@ -0,0 +1,507 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Session\SessionBagInterface; +use Symfony\Component\HttpFoundation\Session\SessionUtils; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; +use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; + +// Help opcache.preload discover always-needed symbols +class_exists(MetadataBag::class); +class_exists(StrictSessionHandler::class); +class_exists(SessionHandlerProxy::class); + +/** + * This provides a base class for session attribute storage. + * + * @author Drak + */ +class NativeSessionStorage implements SessionStorageInterface +{ + /** + * @var SessionBagInterface[] + */ + protected $bags = []; + + /** + * @var bool + */ + protected $started = false; + + /** + * @var bool + */ + protected $closed = false; + + /** + * @var AbstractProxy|\SessionHandlerInterface + */ + protected $saveHandler; + + /** + * @var MetadataBag + */ + protected $metadataBag; + + /** + * @var string|null + */ + private $emulateSameSite; + + /** + * Depending on how you want the storage driver to behave you probably + * want to override this constructor entirely. + * + * List of options for $options array with their defaults. + * + * @see https://php.net/session.configuration for options + * but we omit 'session.' from the beginning of the keys for convenience. + * + * ("auto_start", is not supported as it tells PHP to start a session before + * PHP starts to execute user-land code. Setting during runtime has no effect). + * + * cache_limiter, "" (use "0" to prevent headers from being sent entirely). + * cache_expire, "0" + * cookie_domain, "" + * cookie_httponly, "" + * cookie_lifetime, "0" + * cookie_path, "/" + * cookie_secure, "" + * cookie_samesite, null + * gc_divisor, "100" + * gc_maxlifetime, "1440" + * gc_probability, "1" + * lazy_write, "1" + * name, "PHPSESSID" + * referer_check, "" + * serialize_handler, "php" + * use_strict_mode, "1" + * use_cookies, "1" + * use_only_cookies, "1" + * use_trans_sid, "0" + * sid_length, "32" + * sid_bits_per_character, "5" + * trans_sid_hosts, $_SERVER['HTTP_HOST'] + * trans_sid_tags, "a=href,area=href,frame=src,form=" + * + * @param AbstractProxy|\SessionHandlerInterface|null $handler + */ + public function __construct(array $options = [], $handler = null, ?MetadataBag $metaBag = null) + { + if (!\extension_loaded('session')) { + throw new \LogicException('PHP extension "session" is required.'); + } + + $options += [ + 'cache_limiter' => '', + 'cache_expire' => 0, + 'use_cookies' => 1, + 'lazy_write' => 1, + 'use_strict_mode' => 1, + ]; + + session_register_shutdown(); + + $this->setMetadataBag($metaBag); + $this->setOptions($options); + $this->setSaveHandler($handler); + } + + /** + * Gets the save handler instance. + * + * @return AbstractProxy|\SessionHandlerInterface + */ + public function getSaveHandler() + { + return $this->saveHandler; + } + + /** + * {@inheritdoc} + */ + public function start() + { + if ($this->started) { + return true; + } + + if (\PHP_SESSION_ACTIVE === session_status()) { + throw new \RuntimeException('Failed to start the session: already started by PHP.'); + } + + if (filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOLEAN) && headers_sent($file, $line)) { + throw new \RuntimeException(sprintf('Failed to start the session because headers have already been sent by "%s" at line %d.', $file, $line)); + } + + $sessionId = $_COOKIE[session_name()] ?? null; + /* + * Explanation of the session ID regular expression: `/^[a-zA-Z0-9,-]{22,250}$/`. + * + * ---------- Part 1 + * + * The part `[a-zA-Z0-9,-]` is related to the PHP ini directive `session.sid_bits_per_character` defined as 6. + * See https://www.php.net/manual/en/session.configuration.php#ini.session.sid-bits-per-character. + * Allowed values are integers such as: + * - 4 for range `a-f0-9` + * - 5 for range `a-v0-9` + * - 6 for range `a-zA-Z0-9,-` + * + * ---------- Part 2 + * + * The part `{22,250}` is related to the PHP ini directive `session.sid_length`. + * See https://www.php.net/manual/en/session.configuration.php#ini.session.sid-length. + * Allowed values are integers between 22 and 256, but we use 250 for the max. + * + * Where does the 250 come from? + * - The length of Windows and Linux filenames is limited to 255 bytes. Then the max must not exceed 255. + * - The session filename prefix is `sess_`, a 5 bytes string. Then the max must not exceed 255 - 5 = 250. + * + * ---------- Conclusion + * + * The parts 1 and 2 prevent the warning below: + * `PHP Warning: SessionHandler::read(): Session ID is too long or contains illegal characters. Only the A-Z, a-z, 0-9, "-", and "," characters are allowed.` + * + * The part 2 prevents the warning below: + * `PHP Warning: SessionHandler::read(): open(filepath, O_RDWR) failed: No such file or directory (2).` + */ + if ($sessionId && $this->saveHandler instanceof AbstractProxy && 'files' === $this->saveHandler->getSaveHandlerName() && !preg_match('/^[a-zA-Z0-9,-]{22,250}$/', $sessionId)) { + // the session ID in the header is invalid, create a new one + session_id(session_create_id()); + } + + // ok to try and start the session + if (!session_start()) { + throw new \RuntimeException('Failed to start the session.'); + } + + if (null !== $this->emulateSameSite) { + $originalCookie = SessionUtils::popSessionCookie(session_name(), session_id()); + if (null !== $originalCookie) { + header(sprintf('%s; SameSite=%s', $originalCookie, $this->emulateSameSite), false); + } + } + + $this->loadSession(); + + return true; + } + + /** + * {@inheritdoc} + */ + public function getId() + { + return $this->saveHandler->getId(); + } + + /** + * {@inheritdoc} + */ + public function setId(string $id) + { + $this->saveHandler->setId($id); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->saveHandler->getName(); + } + + /** + * {@inheritdoc} + */ + public function setName(string $name) + { + $this->saveHandler->setName($name); + } + + /** + * {@inheritdoc} + */ + public function regenerate(bool $destroy = false, ?int $lifetime = null) + { + // Cannot regenerate the session ID for non-active sessions. + if (\PHP_SESSION_ACTIVE !== session_status()) { + return false; + } + + if (headers_sent()) { + return false; + } + + if (null !== $lifetime && $lifetime != \ini_get('session.cookie_lifetime')) { + $this->save(); + ini_set('session.cookie_lifetime', $lifetime); + $this->start(); + } + + if ($destroy) { + $this->metadataBag->stampNew(); + } + + $isRegenerated = session_regenerate_id($destroy); + + if (null !== $this->emulateSameSite) { + $originalCookie = SessionUtils::popSessionCookie(session_name(), session_id()); + if (null !== $originalCookie) { + header(sprintf('%s; SameSite=%s', $originalCookie, $this->emulateSameSite), false); + } + } + + return $isRegenerated; + } + + /** + * {@inheritdoc} + */ + public function save() + { + // Store a copy so we can restore the bags in case the session was not left empty + $session = $_SESSION; + + foreach ($this->bags as $bag) { + if (empty($_SESSION[$key = $bag->getStorageKey()])) { + unset($_SESSION[$key]); + } + } + if ($_SESSION && [$key = $this->metadataBag->getStorageKey()] === array_keys($_SESSION)) { + unset($_SESSION[$key]); + } + + // Register error handler to add information about the current save handler + $previousHandler = set_error_handler(function ($type, $msg, $file, $line) use (&$previousHandler) { + if (\E_WARNING === $type && str_starts_with($msg, 'session_write_close():')) { + $handler = $this->saveHandler instanceof SessionHandlerProxy ? $this->saveHandler->getHandler() : $this->saveHandler; + $msg = sprintf('session_write_close(): Failed to write session data with "%s" handler', \get_class($handler)); + } + + return $previousHandler ? $previousHandler($type, $msg, $file, $line) : false; + }); + + try { + session_write_close(); + } finally { + restore_error_handler(); + + // Restore only if not empty + if ($_SESSION) { + $_SESSION = $session; + } + } + + $this->closed = true; + $this->started = false; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + // clear out the bags + foreach ($this->bags as $bag) { + $bag->clear(); + } + + // clear out the session + $_SESSION = []; + + // reconnect the bags to the session + $this->loadSession(); + } + + /** + * {@inheritdoc} + */ + public function registerBag(SessionBagInterface $bag) + { + if ($this->started) { + throw new \LogicException('Cannot register a bag when the session is already started.'); + } + + $this->bags[$bag->getName()] = $bag; + } + + /** + * {@inheritdoc} + */ + public function getBag(string $name) + { + if (!isset($this->bags[$name])) { + throw new \InvalidArgumentException(sprintf('The SessionBagInterface "%s" is not registered.', $name)); + } + + if (!$this->started && $this->saveHandler->isActive()) { + $this->loadSession(); + } elseif (!$this->started) { + $this->start(); + } + + return $this->bags[$name]; + } + + public function setMetadataBag(?MetadataBag $metaBag = null) + { + if (null === $metaBag) { + $metaBag = new MetadataBag(); + } + + $this->metadataBag = $metaBag; + } + + /** + * Gets the MetadataBag. + * + * @return MetadataBag + */ + public function getMetadataBag() + { + return $this->metadataBag; + } + + /** + * {@inheritdoc} + */ + public function isStarted() + { + return $this->started; + } + + /** + * Sets session.* ini variables. + * + * For convenience we omit 'session.' from the beginning of the keys. + * Explicitly ignores other ini keys. + * + * @param array $options Session ini directives [key => value] + * + * @see https://php.net/session.configuration + */ + public function setOptions(array $options) + { + if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { + return; + } + + $validOptions = array_flip([ + 'cache_expire', 'cache_limiter', 'cookie_domain', 'cookie_httponly', + 'cookie_lifetime', 'cookie_path', 'cookie_secure', 'cookie_samesite', + 'gc_divisor', 'gc_maxlifetime', 'gc_probability', + 'lazy_write', 'name', 'referer_check', + 'serialize_handler', 'use_strict_mode', 'use_cookies', + 'use_only_cookies', 'use_trans_sid', 'upload_progress.enabled', + 'upload_progress.cleanup', 'upload_progress.prefix', 'upload_progress.name', + 'upload_progress.freq', 'upload_progress.min_freq', 'url_rewriter.tags', + 'sid_length', 'sid_bits_per_character', 'trans_sid_hosts', 'trans_sid_tags', + ]); + + foreach ($options as $key => $value) { + if (isset($validOptions[$key])) { + if (str_starts_with($key, 'upload_progress.')) { + trigger_deprecation('symfony/http-foundation', '5.4', 'Support for the "%s" session option is deprecated. The settings prefixed with "session.upload_progress." can not be changed at runtime.', $key); + continue; + } + if ('url_rewriter.tags' === $key) { + trigger_deprecation('symfony/http-foundation', '5.4', 'Support for the "%s" session option is deprecated. Use "trans_sid_tags" instead.', $key); + } + if ('cookie_samesite' === $key && \PHP_VERSION_ID < 70300) { + // PHP < 7.3 does not support same_site cookies. We will emulate it in + // the start() method instead. + $this->emulateSameSite = $value; + continue; + } + if ('cookie_secure' === $key && 'auto' === $value) { + continue; + } + ini_set('url_rewriter.tags' !== $key ? 'session.'.$key : $key, $value); + } + } + } + + /** + * Registers session save handler as a PHP session handler. + * + * To use internal PHP session save handlers, override this method using ini_set with + * session.save_handler and session.save_path e.g. + * + * ini_set('session.save_handler', 'files'); + * ini_set('session.save_path', '/tmp'); + * + * or pass in a \SessionHandler instance which configures session.save_handler in the + * constructor, for a template see NativeFileSessionHandler. + * + * @see https://php.net/session-set-save-handler + * @see https://php.net/sessionhandlerinterface + * @see https://php.net/sessionhandler + * + * @param AbstractProxy|\SessionHandlerInterface|null $saveHandler + * + * @throws \InvalidArgumentException + */ + public function setSaveHandler($saveHandler = null) + { + if (!$saveHandler instanceof AbstractProxy + && !$saveHandler instanceof \SessionHandlerInterface + && null !== $saveHandler + ) { + throw new \InvalidArgumentException('Must be instance of AbstractProxy; implement \SessionHandlerInterface; or be null.'); + } + + // Wrap $saveHandler in proxy and prevent double wrapping of proxy + if (!$saveHandler instanceof AbstractProxy && $saveHandler instanceof \SessionHandlerInterface) { + $saveHandler = new SessionHandlerProxy($saveHandler); + } elseif (!$saveHandler instanceof AbstractProxy) { + $saveHandler = new SessionHandlerProxy(new StrictSessionHandler(new \SessionHandler())); + } + $this->saveHandler = $saveHandler; + + if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { + return; + } + + if ($this->saveHandler instanceof SessionHandlerProxy) { + session_set_save_handler($this->saveHandler, false); + } + } + + /** + * Load the session with attributes. + * + * After starting the session, PHP retrieves the session from whatever handlers + * are set to (either PHP's internal, or a custom save handler set with session_set_save_handler()). + * PHP takes the return value from the read() handler, unserializes it + * and populates $_SESSION with the result automatically. + */ + protected function loadSession(?array &$session = null) + { + if (null === $session) { + $session = &$_SESSION; + } + + $bags = array_merge($this->bags, [$this->metadataBag]); + + foreach ($bags as $bag) { + $key = $bag->getStorageKey(); + $session[$key] = isset($session[$key]) && \is_array($session[$key]) ? $session[$key] : []; + $bag->initialize($session[$key]); + } + + $this->started = true; + $this->closed = false; + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php b/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php new file mode 100644 index 0000000..48e6526 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Request; + +// Help opcache.preload discover always-needed symbols +class_exists(NativeSessionStorage::class); + +/** + * @author Jérémy Derussé + */ +class NativeSessionStorageFactory implements SessionStorageFactoryInterface +{ + private $options; + private $handler; + private $metaBag; + private $secure; + + /** + * @see NativeSessionStorage constructor. + */ + public function __construct(array $options = [], $handler = null, ?MetadataBag $metaBag = null, bool $secure = false) + { + $this->options = $options; + $this->handler = $handler; + $this->metaBag = $metaBag; + $this->secure = $secure; + } + + public function createStorage(?Request $request): SessionStorageInterface + { + $storage = new NativeSessionStorage($this->options, $this->handler, $this->metaBag); + if ($this->secure && $request && $request->isSecure()) { + $storage->setOptions(['cookie_secure' => true]); + } + + return $storage; + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php b/vendor/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php new file mode 100644 index 0000000..855d5e1 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; + +/** + * Allows session to be started by PHP and managed by Symfony. + * + * @author Drak + */ +class PhpBridgeSessionStorage extends NativeSessionStorage +{ + /** + * @param AbstractProxy|\SessionHandlerInterface|null $handler + */ + public function __construct($handler = null, ?MetadataBag $metaBag = null) + { + if (!\extension_loaded('session')) { + throw new \LogicException('PHP extension "session" is required.'); + } + + $this->setMetadataBag($metaBag); + $this->setSaveHandler($handler); + } + + /** + * {@inheritdoc} + */ + public function start() + { + if ($this->started) { + return true; + } + + $this->loadSession(); + + return true; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + // clear out the bags and nothing else that may be set + // since the purpose of this driver is to share a handler + foreach ($this->bags as $bag) { + $bag->clear(); + } + + // reconnect the bags to the session + $this->loadSession(); + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php b/vendor/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php new file mode 100644 index 0000000..aa93263 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Request; + +// Help opcache.preload discover always-needed symbols +class_exists(PhpBridgeSessionStorage::class); + +/** + * @author Jérémy Derussé + */ +class PhpBridgeSessionStorageFactory implements SessionStorageFactoryInterface +{ + private $handler; + private $metaBag; + private $secure; + + /** + * @see PhpBridgeSessionStorage constructor. + */ + public function __construct($handler = null, ?MetadataBag $metaBag = null, bool $secure = false) + { + $this->handler = $handler; + $this->metaBag = $metaBag; + $this->secure = $secure; + } + + public function createStorage(?Request $request): SessionStorageInterface + { + $storage = new PhpBridgeSessionStorage($this->handler, $this->metaBag); + if ($this->secure && $request && $request->isSecure()) { + $storage->setOptions(['cookie_secure' => true]); + } + + return $storage; + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php b/vendor/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php new file mode 100644 index 0000000..edd04df --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy; + +/** + * @author Drak + */ +abstract class AbstractProxy +{ + /** + * Flag if handler wraps an internal PHP session handler (using \SessionHandler). + * + * @var bool + */ + protected $wrapper = false; + + /** + * @var string + */ + protected $saveHandlerName; + + /** + * Gets the session.save_handler name. + * + * @return string|null + */ + public function getSaveHandlerName() + { + return $this->saveHandlerName; + } + + /** + * Is this proxy handler and instance of \SessionHandlerInterface. + * + * @return bool + */ + public function isSessionHandlerInterface() + { + return $this instanceof \SessionHandlerInterface; + } + + /** + * Returns true if this handler wraps an internal PHP session save handler using \SessionHandler. + * + * @return bool + */ + public function isWrapper() + { + return $this->wrapper; + } + + /** + * Has a session started? + * + * @return bool + */ + public function isActive() + { + return \PHP_SESSION_ACTIVE === session_status(); + } + + /** + * Gets the session ID. + * + * @return string + */ + public function getId() + { + return session_id(); + } + + /** + * Sets the session ID. + * + * @throws \LogicException + */ + public function setId(string $id) + { + if ($this->isActive()) { + throw new \LogicException('Cannot change the ID of an active session.'); + } + + session_id($id); + } + + /** + * Gets the session name. + * + * @return string + */ + public function getName() + { + return session_name(); + } + + /** + * Sets the session name. + * + * @throws \LogicException + */ + public function setName(string $name) + { + if ($this->isActive()) { + throw new \LogicException('Cannot change the name of an active session.'); + } + + session_name($name); + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php b/vendor/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php new file mode 100644 index 0000000..0defa4a --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy; + +use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; + +/** + * @author Drak + */ +class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface +{ + protected $handler; + + public function __construct(\SessionHandlerInterface $handler) + { + $this->handler = $handler; + $this->wrapper = $handler instanceof \SessionHandler; + $this->saveHandlerName = $this->wrapper || ($handler instanceof StrictSessionHandler && $handler->isWrapper()) ? \ini_get('session.save_handler') : 'user'; + } + + /** + * @return \SessionHandlerInterface + */ + public function getHandler() + { + return $this->handler; + } + + // \SessionHandlerInterface + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function open($savePath, $sessionName) + { + return $this->handler->open($savePath, $sessionName); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function close() + { + return $this->handler->close(); + } + + /** + * @return string|false + */ + #[\ReturnTypeWillChange] + public function read($sessionId) + { + return $this->handler->read($sessionId); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function write($sessionId, $data) + { + return $this->handler->write($sessionId, $data); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function destroy($sessionId) + { + return $this->handler->destroy($sessionId); + } + + /** + * @return int|false + */ + #[\ReturnTypeWillChange] + public function gc($maxlifetime) + { + return $this->handler->gc($maxlifetime); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function validateId($sessionId) + { + return !$this->handler instanceof \SessionUpdateTimestampHandlerInterface || $this->handler->validateId($sessionId); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function updateTimestamp($sessionId, $data) + { + return $this->handler instanceof \SessionUpdateTimestampHandlerInterface ? $this->handler->updateTimestamp($sessionId, $data) : $this->write($sessionId, $data); + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/ServiceSessionFactory.php b/vendor/symfony/http-foundation/Session/Storage/ServiceSessionFactory.php new file mode 100644 index 0000000..d17c60a --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/ServiceSessionFactory.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Request; + +/** + * @author Jérémy Derussé + * + * @internal to be removed in Symfony 6 + */ +final class ServiceSessionFactory implements SessionStorageFactoryInterface +{ + private $storage; + + public function __construct(SessionStorageInterface $storage) + { + $this->storage = $storage; + } + + public function createStorage(?Request $request): SessionStorageInterface + { + if ($this->storage instanceof NativeSessionStorage && $request && $request->isSecure()) { + $this->storage->setOptions(['cookie_secure' => true]); + } + + return $this->storage; + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php b/vendor/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php new file mode 100644 index 0000000..d03f0da --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Request; + +/** + * @author Jérémy Derussé + */ +interface SessionStorageFactoryInterface +{ + /** + * Creates a new instance of SessionStorageInterface. + */ + public function createStorage(?Request $request): SessionStorageInterface; +} diff --git a/vendor/symfony/http-foundation/Session/Storage/SessionStorageInterface.php b/vendor/symfony/http-foundation/Session/Storage/SessionStorageInterface.php new file mode 100644 index 0000000..70b7c6a --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/SessionStorageInterface.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Session\SessionBagInterface; + +/** + * StorageInterface. + * + * @author Fabien Potencier + * @author Drak + */ +interface SessionStorageInterface +{ + /** + * Starts the session. + * + * @return bool + * + * @throws \RuntimeException if something goes wrong starting the session + */ + public function start(); + + /** + * Checks if the session is started. + * + * @return bool + */ + public function isStarted(); + + /** + * Returns the session ID. + * + * @return string + */ + public function getId(); + + /** + * Sets the session ID. + */ + public function setId(string $id); + + /** + * Returns the session name. + * + * @return string + */ + public function getName(); + + /** + * Sets the session name. + */ + public function setName(string $name); + + /** + * Regenerates id that represents this storage. + * + * This method must invoke session_regenerate_id($destroy) unless + * this interface is used for a storage object designed for unit + * or functional testing where a real PHP session would interfere + * with testing. + * + * Note regenerate+destroy should not clear the session data in memory + * only delete the session data from persistent storage. + * + * Care: When regenerating the session ID no locking is involved in PHP's + * session design. See https://bugs.php.net/61470 for a discussion. + * So you must make sure the regenerated session is saved BEFORE sending the + * headers with the new ID. Symfony's HttpKernel offers a listener for this. + * See Symfony\Component\HttpKernel\EventListener\SaveSessionListener. + * Otherwise session data could get lost again for concurrent requests with the + * new ID. One result could be that you get logged out after just logging in. + * + * @param bool $destroy Destroy session when regenerating? + * @param int|null $lifetime Sets the cookie lifetime for the session cookie. A null value + * will leave the system settings unchanged, 0 sets the cookie + * to expire with browser session. Time is in seconds, and is + * not a Unix timestamp. + * + * @return bool + * + * @throws \RuntimeException If an error occurs while regenerating this storage + */ + public function regenerate(bool $destroy = false, ?int $lifetime = null); + + /** + * Force the session to be saved and closed. + * + * This method must invoke session_write_close() unless this interface is + * used for a storage object design for unit or functional testing where + * a real PHP session would interfere with testing, in which case + * it should actually persist the session data if required. + * + * @throws \RuntimeException if the session is saved without being started, or if the session + * is already closed + */ + public function save(); + + /** + * Clear all session data in memory. + */ + public function clear(); + + /** + * Gets a SessionBagInterface by name. + * + * @return SessionBagInterface + * + * @throws \InvalidArgumentException If the bag does not exist + */ + public function getBag(string $name); + + /** + * Registers a SessionBagInterface for use. + */ + public function registerBag(SessionBagInterface $bag); + + /** + * @return MetadataBag + */ + public function getMetadataBag(); +} diff --git a/vendor/symfony/http-foundation/StreamedResponse.php b/vendor/symfony/http-foundation/StreamedResponse.php new file mode 100644 index 0000000..b42330d --- /dev/null +++ b/vendor/symfony/http-foundation/StreamedResponse.php @@ -0,0 +1,139 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * StreamedResponse represents a streamed HTTP response. + * + * A StreamedResponse uses a callback for its content. + * + * The callback should use the standard PHP functions like echo + * to stream the response back to the client. The flush() function + * can also be used if needed. + * + * @see flush() + * + * @author Fabien Potencier + */ +class StreamedResponse extends Response +{ + protected $callback; + protected $streamed; + private $headersSent; + + public function __construct(?callable $callback = null, int $status = 200, array $headers = []) + { + parent::__construct(null, $status, $headers); + + if (null !== $callback) { + $this->setCallback($callback); + } + $this->streamed = false; + $this->headersSent = false; + } + + /** + * Factory method for chainability. + * + * @param callable|null $callback A valid PHP callback or null to set it later + * + * @return static + * + * @deprecated since Symfony 5.1, use __construct() instead. + */ + public static function create($callback = null, int $status = 200, array $headers = []) + { + trigger_deprecation('symfony/http-foundation', '5.1', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class); + + return new static($callback, $status, $headers); + } + + /** + * Sets the PHP callback associated with this Response. + * + * @return $this + */ + public function setCallback(callable $callback) + { + $this->callback = $callback; + + return $this; + } + + /** + * {@inheritdoc} + * + * This method only sends the headers once. + * + * @return $this + */ + public function sendHeaders() + { + if ($this->headersSent) { + return $this; + } + + $this->headersSent = true; + + return parent::sendHeaders(); + } + + /** + * {@inheritdoc} + * + * This method only sends the content once. + * + * @return $this + */ + public function sendContent() + { + if ($this->streamed) { + return $this; + } + + $this->streamed = true; + + if (null === $this->callback) { + throw new \LogicException('The Response callback must not be null.'); + } + + ($this->callback)(); + + return $this; + } + + /** + * {@inheritdoc} + * + * @return $this + * + * @throws \LogicException when the content is not null + */ + public function setContent(?string $content) + { + if (null !== $content) { + throw new \LogicException('The content cannot be set on a StreamedResponse instance.'); + } + + $this->streamed = true; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getContent() + { + return false; + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/RequestAttributeValueSame.php b/vendor/symfony/http-foundation/Test/Constraint/RequestAttributeValueSame.php new file mode 100644 index 0000000..cb216ea --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/RequestAttributeValueSame.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Request; + +final class RequestAttributeValueSame extends Constraint +{ + private $name; + private $value; + + public function __construct(string $name, string $value) + { + $this->name = $name; + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('has attribute "%s" with value "%s"', $this->name, $this->value); + } + + /** + * @param Request $request + * + * {@inheritdoc} + */ + protected function matches($request): bool + { + return $this->value === $request->attributes->get($this->name); + } + + /** + * @param Request $request + * + * {@inheritdoc} + */ + protected function failureDescription($request): string + { + return 'the Request '.$this->toString(); + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseCookieValueSame.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseCookieValueSame.php new file mode 100644 index 0000000..939925b --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseCookieValueSame.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\HttpFoundation\Response; + +final class ResponseCookieValueSame extends Constraint +{ + private $name; + private $value; + private $path; + private $domain; + + public function __construct(string $name, string $value, string $path = '/', ?string $domain = null) + { + $this->name = $name; + $this->value = $value; + $this->path = $path; + $this->domain = $domain; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + $str = sprintf('has cookie "%s"', $this->name); + if ('/' !== $this->path) { + $str .= sprintf(' with path "%s"', $this->path); + } + if ($this->domain) { + $str .= sprintf(' for domain "%s"', $this->domain); + } + $str .= sprintf(' with value "%s"', $this->value); + + return $str; + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function matches($response): bool + { + $cookie = $this->getCookie($response); + if (!$cookie) { + return false; + } + + return $this->value === (string) $cookie->getValue(); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function failureDescription($response): string + { + return 'the Response '.$this->toString(); + } + + protected function getCookie(Response $response): ?Cookie + { + $cookies = $response->headers->getCookies(); + + $filteredCookies = array_filter($cookies, function (Cookie $cookie) { + return $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain; + }); + + return reset($filteredCookies) ?: null; + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseFormatSame.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseFormatSame.php new file mode 100644 index 0000000..f73aedf --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseFormatSame.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Asserts that the response is in the given format. + * + * @author Kévin Dunglas + */ +final class ResponseFormatSame extends Constraint +{ + private $request; + private $format; + + public function __construct(Request $request, ?string $format) + { + $this->request = $request; + $this->format = $format; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return 'format is '.($this->format ?? 'null'); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function matches($response): bool + { + return $this->format === $this->request->getFormat($response->headers->get('Content-Type')); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function failureDescription($response): string + { + return 'the Response '.$this->toString(); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function additionalFailureDescription($response): string + { + return (string) $response; + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseHasCookie.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseHasCookie.php new file mode 100644 index 0000000..9d6e58c --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseHasCookie.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\HttpFoundation\Response; + +final class ResponseHasCookie extends Constraint +{ + private $name; + private $path; + private $domain; + + public function __construct(string $name, string $path = '/', ?string $domain = null) + { + $this->name = $name; + $this->path = $path; + $this->domain = $domain; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + $str = sprintf('has cookie "%s"', $this->name); + if ('/' !== $this->path) { + $str .= sprintf(' with path "%s"', $this->path); + } + if ($this->domain) { + $str .= sprintf(' for domain "%s"', $this->domain); + } + + return $str; + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function matches($response): bool + { + return null !== $this->getCookie($response); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function failureDescription($response): string + { + return 'the Response '.$this->toString(); + } + + private function getCookie(Response $response): ?Cookie + { + $cookies = $response->headers->getCookies(); + + $filteredCookies = array_filter($cookies, function (Cookie $cookie) { + return $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain; + }); + + return reset($filteredCookies) ?: null; + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseHasHeader.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseHasHeader.php new file mode 100644 index 0000000..68ad827 --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseHasHeader.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Response; + +final class ResponseHasHeader extends Constraint +{ + private $headerName; + + public function __construct(string $headerName) + { + $this->headerName = $headerName; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('has header "%s"', $this->headerName); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function matches($response): bool + { + return $response->headers->has($this->headerName); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function failureDescription($response): string + { + return 'the Response '.$this->toString(); + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseHeaderSame.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseHeaderSame.php new file mode 100644 index 0000000..a27d0c7 --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseHeaderSame.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Response; + +final class ResponseHeaderSame extends Constraint +{ + private $headerName; + private $expectedValue; + + public function __construct(string $headerName, string $expectedValue) + { + $this->headerName = $headerName; + $this->expectedValue = $expectedValue; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('has header "%s" with value "%s"', $this->headerName, $this->expectedValue); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function matches($response): bool + { + return $this->expectedValue === $response->headers->get($this->headerName, null); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function failureDescription($response): string + { + return 'the Response '.$this->toString(); + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseIsRedirected.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsRedirected.php new file mode 100644 index 0000000..8c4b883 --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsRedirected.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Response; + +final class ResponseIsRedirected extends Constraint +{ + /** + * {@inheritdoc} + */ + public function toString(): string + { + return 'is redirected'; + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function matches($response): bool + { + return $response->isRedirect(); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function failureDescription($response): string + { + return 'the Response '.$this->toString(); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function additionalFailureDescription($response): string + { + return (string) $response; + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseIsSuccessful.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsSuccessful.php new file mode 100644 index 0000000..9c66558 --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsSuccessful.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Response; + +final class ResponseIsSuccessful extends Constraint +{ + /** + * {@inheritdoc} + */ + public function toString(): string + { + return 'is successful'; + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function matches($response): bool + { + return $response->isSuccessful(); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function failureDescription($response): string + { + return 'the Response '.$this->toString(); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function additionalFailureDescription($response): string + { + return (string) $response; + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseIsUnprocessable.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsUnprocessable.php new file mode 100644 index 0000000..880c781 --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsUnprocessable.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Response; + +final class ResponseIsUnprocessable extends Constraint +{ + /** + * {@inheritdoc} + */ + public function toString(): string + { + return 'is unprocessable'; + } + + /** + * @param Response $other + * + * {@inheritdoc} + */ + protected function matches($other): bool + { + return Response::HTTP_UNPROCESSABLE_ENTITY === $other->getStatusCode(); + } + + /** + * @param Response $other + * + * {@inheritdoc} + */ + protected function failureDescription($other): string + { + return 'the Response '.$this->toString(); + } + + /** + * @param Response $other + * + * {@inheritdoc} + */ + protected function additionalFailureDescription($other): string + { + return (string) $other; + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseStatusCodeSame.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseStatusCodeSame.php new file mode 100644 index 0000000..72bb000 --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseStatusCodeSame.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Response; + +final class ResponseStatusCodeSame extends Constraint +{ + private $statusCode; + + public function __construct(int $statusCode) + { + $this->statusCode = $statusCode; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return 'status code is '.$this->statusCode; + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function matches($response): bool + { + return $this->statusCode === $response->getStatusCode(); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function failureDescription($response): string + { + return 'the Response '.$this->toString(); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function additionalFailureDescription($response): string + { + return (string) $response; + } +} diff --git a/vendor/symfony/http-foundation/UrlHelper.php b/vendor/symfony/http-foundation/UrlHelper.php new file mode 100644 index 0000000..9065994 --- /dev/null +++ b/vendor/symfony/http-foundation/UrlHelper.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RequestContextAwareInterface; + +/** + * A helper service for manipulating URLs within and outside the request scope. + * + * @author Valentin Udaltsov + */ +final class UrlHelper +{ + private $requestStack; + private $requestContext; + + /** + * @param RequestContextAwareInterface|RequestContext|null $requestContext + */ + public function __construct(RequestStack $requestStack, $requestContext = null) + { + if (null !== $requestContext && !$requestContext instanceof RequestContext && !$requestContext instanceof RequestContextAwareInterface) { + throw new \TypeError(__METHOD__.': Argument #2 ($requestContext) must of type Symfony\Component\Routing\RequestContextAwareInterface|Symfony\Component\Routing\RequestContext|null, '.get_debug_type($requestContext).' given.'); + } + + $this->requestStack = $requestStack; + $this->requestContext = $requestContext; + } + + public function getAbsoluteUrl(string $path): string + { + if (str_contains($path, '://') || '//' === substr($path, 0, 2)) { + return $path; + } + + if (null === $request = $this->requestStack->getMainRequest()) { + return $this->getAbsoluteUrlFromContext($path); + } + + if ('#' === $path[0]) { + $path = $request->getRequestUri().$path; + } elseif ('?' === $path[0]) { + $path = $request->getPathInfo().$path; + } + + if (!$path || '/' !== $path[0]) { + $prefix = $request->getPathInfo(); + $last = \strlen($prefix) - 1; + if ($last !== $pos = strrpos($prefix, '/')) { + $prefix = substr($prefix, 0, $pos).'/'; + } + + return $request->getUriForPath($prefix.$path); + } + + return $request->getSchemeAndHttpHost().$path; + } + + public function getRelativePath(string $path): string + { + if (str_contains($path, '://') || '//' === substr($path, 0, 2)) { + return $path; + } + + if (null === $request = $this->requestStack->getMainRequest()) { + return $path; + } + + return $request->getRelativeUriForPath($path); + } + + private function getAbsoluteUrlFromContext(string $path): string + { + if (null === $context = $this->requestContext) { + return $path; + } + + if ($context instanceof RequestContextAwareInterface) { + $context = $context->getContext(); + } + + if ('' === $host = $context->getHost()) { + return $path; + } + + $scheme = $context->getScheme(); + $port = ''; + + if ('http' === $scheme && 80 !== $context->getHttpPort()) { + $port = ':'.$context->getHttpPort(); + } elseif ('https' === $scheme && 443 !== $context->getHttpsPort()) { + $port = ':'.$context->getHttpsPort(); + } + + if ('#' === $path[0]) { + $queryString = $context->getQueryString(); + $path = $context->getPathInfo().($queryString ? '?'.$queryString : '').$path; + } elseif ('?' === $path[0]) { + $path = $context->getPathInfo().$path; + } + + if ('/' !== $path[0]) { + $path = rtrim($context->getBaseUrl(), '/').'/'.$path; + } + + return $scheme.'://'.$host.$port.$path; + } +} diff --git a/vendor/symfony/http-foundation/composer.json b/vendor/symfony/http-foundation/composer.json new file mode 100644 index 0000000..a2e43a9 --- /dev/null +++ b/vendor/symfony/http-foundation/composer.json @@ -0,0 +1,43 @@ +{ + "name": "symfony/http-foundation", + "type": "library", + "description": "Defines an object-oriented layer for the HTTP specification", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "predis/predis": "^1.0|^2.0", + "symfony/cache": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", + "symfony/mime": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/rate-limiter": "^5.2|^6.0" + }, + "suggest" : { + "symfony/mime": "To use the file extension guesser" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\HttpFoundation\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/vendor/symfony/polyfill-mbstring/LICENSE b/vendor/symfony/polyfill-mbstring/LICENSE new file mode 100644 index 0000000..6e3afce --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/polyfill-mbstring/Mbstring.php b/vendor/symfony/polyfill-mbstring/Mbstring.php new file mode 100644 index 0000000..2e0b969 --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/Mbstring.php @@ -0,0 +1,947 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Mbstring; + +/** + * Partial mbstring implementation in PHP, iconv based, UTF-8 centric. + * + * Implemented: + * - mb_chr - Returns a specific character from its Unicode code point + * - mb_convert_encoding - Convert character encoding + * - mb_convert_variables - Convert character code in variable(s) + * - mb_decode_mimeheader - Decode string in MIME header field + * - mb_encode_mimeheader - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED + * - mb_decode_numericentity - Decode HTML numeric string reference to character + * - mb_encode_numericentity - Encode character to HTML numeric string reference + * - mb_convert_case - Perform case folding on a string + * - mb_detect_encoding - Detect character encoding + * - mb_get_info - Get internal settings of mbstring + * - mb_http_input - Detect HTTP input character encoding + * - mb_http_output - Set/Get HTTP output character encoding + * - mb_internal_encoding - Set/Get internal character encoding + * - mb_list_encodings - Returns an array of all supported encodings + * - mb_ord - Returns the Unicode code point of a character + * - mb_output_handler - Callback function converts character encoding in output buffer + * - mb_scrub - Replaces ill-formed byte sequences with substitute characters + * - mb_strlen - Get string length + * - mb_strpos - Find position of first occurrence of string in a string + * - mb_strrpos - Find position of last occurrence of a string in a string + * - mb_str_split - Convert a string to an array + * - mb_strtolower - Make a string lowercase + * - mb_strtoupper - Make a string uppercase + * - mb_substitute_character - Set/Get substitution character + * - mb_substr - Get part of string + * - mb_stripos - Finds position of first occurrence of a string within another, case insensitive + * - mb_stristr - Finds first occurrence of a string within another, case insensitive + * - mb_strrchr - Finds the last occurrence of a character in a string within another + * - mb_strrichr - Finds the last occurrence of a character in a string within another, case insensitive + * - mb_strripos - Finds position of last occurrence of a string within another, case insensitive + * - mb_strstr - Finds first occurrence of a string within another + * - mb_strwidth - Return width of string + * - mb_substr_count - Count the number of substring occurrences + * + * Not implemented: + * - mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more) + * - mb_ereg_* - Regular expression with multibyte support + * - mb_parse_str - Parse GET/POST/COOKIE data and set global variable + * - mb_preferred_mime_name - Get MIME charset string + * - mb_regex_encoding - Returns current encoding for multibyte regex as string + * - mb_regex_set_options - Set/Get the default options for mbregex functions + * - mb_send_mail - Send encoded mail + * - mb_split - Split multibyte string using regular expression + * - mb_strcut - Get part of string + * - mb_strimwidth - Get truncated string with specified width + * + * @author Nicolas Grekas + * + * @internal + */ +final class Mbstring +{ + public const MB_CASE_FOLD = \PHP_INT_MAX; + + private const SIMPLE_CASE_FOLD = [ + ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"], + ['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'], + ]; + + private static $encodingList = ['ASCII', 'UTF-8']; + private static $language = 'neutral'; + private static $internalEncoding = 'UTF-8'; + + public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null) + { + if (\is_array($fromEncoding) || (null !== $fromEncoding && false !== strpos($fromEncoding, ','))) { + $fromEncoding = self::mb_detect_encoding($s, $fromEncoding); + } else { + $fromEncoding = self::getEncoding($fromEncoding); + } + + $toEncoding = self::getEncoding($toEncoding); + + if ('BASE64' === $fromEncoding) { + $s = base64_decode($s); + $fromEncoding = $toEncoding; + } + + if ('BASE64' === $toEncoding) { + return base64_encode($s); + } + + if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) { + if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) { + $fromEncoding = 'Windows-1252'; + } + if ('UTF-8' !== $fromEncoding) { + $s = iconv($fromEncoding, 'UTF-8//IGNORE', $s); + } + + return preg_replace_callback('/[\x80-\xFF]+/', [__CLASS__, 'html_encoding_callback'], $s); + } + + if ('HTML-ENTITIES' === $fromEncoding) { + $s = html_entity_decode($s, \ENT_COMPAT, 'UTF-8'); + $fromEncoding = 'UTF-8'; + } + + return iconv($fromEncoding, $toEncoding.'//IGNORE', $s); + } + + public static function mb_convert_variables($toEncoding, $fromEncoding, &...$vars) + { + $ok = true; + array_walk_recursive($vars, function (&$v) use (&$ok, $toEncoding, $fromEncoding) { + if (false === $v = self::mb_convert_encoding($v, $toEncoding, $fromEncoding)) { + $ok = false; + } + }); + + return $ok ? $fromEncoding : false; + } + + public static function mb_decode_mimeheader($s) + { + return iconv_mime_decode($s, 2, self::$internalEncoding); + } + + public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null) + { + trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', \E_USER_WARNING); + } + + public static function mb_decode_numericentity($s, $convmap, $encoding = null) + { + if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) { + trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING); + + return null; + } + + if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) { + return false; + } + + if (null !== $encoding && !\is_scalar($encoding)) { + trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING); + + return ''; // Instead of null (cf. mb_encode_numericentity). + } + + $s = (string) $s; + if ('' === $s) { + return ''; + } + + $encoding = self::getEncoding($encoding); + + if ('UTF-8' === $encoding) { + $encoding = null; + if (!preg_match('//u', $s)) { + $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); + } + } else { + $s = iconv($encoding, 'UTF-8//IGNORE', $s); + } + + $cnt = floor(\count($convmap) / 4) * 4; + + for ($i = 0; $i < $cnt; $i += 4) { + // collector_decode_htmlnumericentity ignores $convmap[$i + 3] + $convmap[$i] += $convmap[$i + 2]; + $convmap[$i + 1] += $convmap[$i + 2]; + } + + $s = preg_replace_callback('/&#(?:0*([0-9]+)|x0*([0-9a-fA-F]+))(?!&);?/', function (array $m) use ($cnt, $convmap) { + $c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1]; + for ($i = 0; $i < $cnt; $i += 4) { + if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) { + return self::mb_chr($c - $convmap[$i + 2]); + } + } + + return $m[0]; + }, $s); + + if (null === $encoding) { + return $s; + } + + return iconv('UTF-8', $encoding.'//IGNORE', $s); + } + + public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false) + { + if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) { + trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING); + + return null; + } + + if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) { + return false; + } + + if (null !== $encoding && !\is_scalar($encoding)) { + trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING); + + return null; // Instead of '' (cf. mb_decode_numericentity). + } + + if (null !== $is_hex && !\is_scalar($is_hex)) { + trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.\gettype($s).' given', \E_USER_WARNING); + + return null; + } + + $s = (string) $s; + if ('' === $s) { + return ''; + } + + $encoding = self::getEncoding($encoding); + + if ('UTF-8' === $encoding) { + $encoding = null; + if (!preg_match('//u', $s)) { + $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); + } + } else { + $s = iconv($encoding, 'UTF-8//IGNORE', $s); + } + + static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; + + $cnt = floor(\count($convmap) / 4) * 4; + $i = 0; + $len = \strlen($s); + $result = ''; + + while ($i < $len) { + $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; + $uchr = substr($s, $i, $ulen); + $i += $ulen; + $c = self::mb_ord($uchr); + + for ($j = 0; $j < $cnt; $j += 4) { + if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) { + $cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3]; + $result .= $is_hex ? sprintf('&#x%X;', $cOffset) : '&#'.$cOffset.';'; + continue 2; + } + } + $result .= $uchr; + } + + if (null === $encoding) { + return $result; + } + + return iconv('UTF-8', $encoding.'//IGNORE', $result); + } + + public static function mb_convert_case($s, $mode, $encoding = null) + { + $s = (string) $s; + if ('' === $s) { + return ''; + } + + $encoding = self::getEncoding($encoding); + + if ('UTF-8' === $encoding) { + $encoding = null; + if (!preg_match('//u', $s)) { + $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); + } + } else { + $s = iconv($encoding, 'UTF-8//IGNORE', $s); + } + + if (\MB_CASE_TITLE == $mode) { + static $titleRegexp = null; + if (null === $titleRegexp) { + $titleRegexp = self::getData('titleCaseRegexp'); + } + $s = preg_replace_callback($titleRegexp, [__CLASS__, 'title_case'], $s); + } else { + if (\MB_CASE_UPPER == $mode) { + static $upper = null; + if (null === $upper) { + $upper = self::getData('upperCase'); + } + $map = $upper; + } else { + if (self::MB_CASE_FOLD === $mode) { + static $caseFolding = null; + if (null === $caseFolding) { + $caseFolding = self::getData('caseFolding'); + } + $s = strtr($s, $caseFolding); + } + + static $lower = null; + if (null === $lower) { + $lower = self::getData('lowerCase'); + } + $map = $lower; + } + + static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; + + $i = 0; + $len = \strlen($s); + + while ($i < $len) { + $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; + $uchr = substr($s, $i, $ulen); + $i += $ulen; + + if (isset($map[$uchr])) { + $uchr = $map[$uchr]; + $nlen = \strlen($uchr); + + if ($nlen == $ulen) { + $nlen = $i; + do { + $s[--$nlen] = $uchr[--$ulen]; + } while ($ulen); + } else { + $s = substr_replace($s, $uchr, $i - $ulen, $ulen); + $len += $nlen - $ulen; + $i += $nlen - $ulen; + } + } + } + } + + if (null === $encoding) { + return $s; + } + + return iconv('UTF-8', $encoding.'//IGNORE', $s); + } + + public static function mb_internal_encoding($encoding = null) + { + if (null === $encoding) { + return self::$internalEncoding; + } + + $normalizedEncoding = self::getEncoding($encoding); + + if ('UTF-8' === $normalizedEncoding || false !== @iconv($normalizedEncoding, $normalizedEncoding, ' ')) { + self::$internalEncoding = $normalizedEncoding; + + return true; + } + + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError(sprintf('Argument #1 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + public static function mb_language($lang = null) + { + if (null === $lang) { + return self::$language; + } + + switch ($normalizedLang = strtolower($lang)) { + case 'uni': + case 'neutral': + self::$language = $normalizedLang; + + return true; + } + + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError(sprintf('Argument #1 ($language) must be a valid language, "%s" given', $lang)); + } + + public static function mb_list_encodings() + { + return ['UTF-8']; + } + + public static function mb_encoding_aliases($encoding) + { + switch (strtoupper($encoding)) { + case 'UTF8': + case 'UTF-8': + return ['utf8']; + } + + return false; + } + + public static function mb_check_encoding($var = null, $encoding = null) + { + if (PHP_VERSION_ID < 70200 && \is_array($var)) { + trigger_error('mb_check_encoding() expects parameter 1 to be string, array given', \E_USER_WARNING); + + return null; + } + + if (null === $encoding) { + if (null === $var) { + return false; + } + $encoding = self::$internalEncoding; + } + + if (!\is_array($var)) { + return self::mb_detect_encoding($var, [$encoding]) || false !== @iconv($encoding, $encoding, $var); + } + + foreach ($var as $key => $value) { + if (!self::mb_check_encoding($key, $encoding)) { + return false; + } + if (!self::mb_check_encoding($value, $encoding)) { + return false; + } + } + + return true; + + } + + public static function mb_detect_encoding($str, $encodingList = null, $strict = false) + { + if (null === $encodingList) { + $encodingList = self::$encodingList; + } else { + if (!\is_array($encodingList)) { + $encodingList = array_map('trim', explode(',', $encodingList)); + } + $encodingList = array_map('strtoupper', $encodingList); + } + + foreach ($encodingList as $enc) { + switch ($enc) { + case 'ASCII': + if (!preg_match('/[\x80-\xFF]/', $str)) { + return $enc; + } + break; + + case 'UTF8': + case 'UTF-8': + if (preg_match('//u', $str)) { + return 'UTF-8'; + } + break; + + default: + if (0 === strncmp($enc, 'ISO-8859-', 9)) { + return $enc; + } + } + } + + return false; + } + + public static function mb_detect_order($encodingList = null) + { + if (null === $encodingList) { + return self::$encodingList; + } + + if (!\is_array($encodingList)) { + $encodingList = array_map('trim', explode(',', $encodingList)); + } + $encodingList = array_map('strtoupper', $encodingList); + + foreach ($encodingList as $enc) { + switch ($enc) { + default: + if (strncmp($enc, 'ISO-8859-', 9)) { + return false; + } + // no break + case 'ASCII': + case 'UTF8': + case 'UTF-8': + } + } + + self::$encodingList = $encodingList; + + return true; + } + + public static function mb_strlen($s, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return \strlen($s); + } + + return @iconv_strlen($s, $encoding); + } + + public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return strpos($haystack, $needle, $offset); + } + + $needle = (string) $needle; + if ('' === $needle) { + if (80000 > \PHP_VERSION_ID) { + trigger_error(__METHOD__.': Empty delimiter', \E_USER_WARNING); + + return false; + } + + return 0; + } + + return iconv_strpos($haystack, $needle, $offset, $encoding); + } + + public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return strrpos($haystack, $needle, $offset); + } + + if ($offset != (int) $offset) { + $offset = 0; + } elseif ($offset = (int) $offset) { + if ($offset < 0) { + if (0 > $offset += self::mb_strlen($needle)) { + $haystack = self::mb_substr($haystack, 0, $offset, $encoding); + } + $offset = 0; + } else { + $haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding); + } + } + + $pos = '' !== $needle || 80000 > \PHP_VERSION_ID + ? iconv_strrpos($haystack, $needle, $encoding) + : self::mb_strlen($haystack, $encoding); + + return false !== $pos ? $offset + $pos : false; + } + + public static function mb_str_split($string, $split_length = 1, $encoding = null) + { + if (null !== $string && !\is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) { + trigger_error('mb_str_split() expects parameter 1 to be string, '.\gettype($string).' given', \E_USER_WARNING); + + return null; + } + + if (1 > $split_length = (int) $split_length) { + if (80000 > \PHP_VERSION_ID) { + trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING); + + return false; + } + + throw new \ValueError('Argument #2 ($length) must be greater than 0'); + } + + if (null === $encoding) { + $encoding = mb_internal_encoding(); + } + + if ('UTF-8' === $encoding = self::getEncoding($encoding)) { + $rx = '/('; + while (65535 < $split_length) { + $rx .= '.{65535}'; + $split_length -= 65535; + } + $rx .= '.{'.$split_length.'})/us'; + + return preg_split($rx, $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY); + } + + $result = []; + $length = mb_strlen($string, $encoding); + + for ($i = 0; $i < $length; $i += $split_length) { + $result[] = mb_substr($string, $i, $split_length, $encoding); + } + + return $result; + } + + public static function mb_strtolower($s, $encoding = null) + { + return self::mb_convert_case($s, \MB_CASE_LOWER, $encoding); + } + + public static function mb_strtoupper($s, $encoding = null) + { + return self::mb_convert_case($s, \MB_CASE_UPPER, $encoding); + } + + public static function mb_substitute_character($c = null) + { + if (null === $c) { + return 'none'; + } + if (0 === strcasecmp($c, 'none')) { + return true; + } + if (80000 > \PHP_VERSION_ID) { + return false; + } + if (\is_int($c) || 'long' === $c || 'entity' === $c) { + return false; + } + + throw new \ValueError('Argument #1 ($substitute_character) must be "none", "long", "entity" or a valid codepoint'); + } + + public static function mb_substr($s, $start, $length = null, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return (string) substr($s, $start, null === $length ? 2147483647 : $length); + } + + if ($start < 0) { + $start = iconv_strlen($s, $encoding) + $start; + if ($start < 0) { + $start = 0; + } + } + + if (null === $length) { + $length = 2147483647; + } elseif ($length < 0) { + $length = iconv_strlen($s, $encoding) + $length - $start; + if ($length < 0) { + return ''; + } + } + + return (string) iconv_substr($s, $start, $length, $encoding); + } + + public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) + { + [$haystack, $needle] = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], [ + self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding), + self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding), + ]); + + return self::mb_strpos($haystack, $needle, $offset, $encoding); + } + + public static function mb_stristr($haystack, $needle, $part = false, $encoding = null) + { + $pos = self::mb_stripos($haystack, $needle, 0, $encoding); + + return self::getSubpart($pos, $part, $haystack, $encoding); + } + + public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + $pos = strrpos($haystack, $needle); + } else { + $needle = self::mb_substr($needle, 0, 1, $encoding); + $pos = iconv_strrpos($haystack, $needle, $encoding); + } + + return self::getSubpart($pos, $part, $haystack, $encoding); + } + + public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null) + { + $needle = self::mb_substr($needle, 0, 1, $encoding); + $pos = self::mb_strripos($haystack, $needle, $encoding); + + return self::getSubpart($pos, $part, $haystack, $encoding); + } + + public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) + { + $haystack = self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding); + $needle = self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding); + + $haystack = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $haystack); + $needle = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $needle); + + return self::mb_strrpos($haystack, $needle, $offset, $encoding); + } + + public static function mb_strstr($haystack, $needle, $part = false, $encoding = null) + { + $pos = strpos($haystack, $needle); + if (false === $pos) { + return false; + } + if ($part) { + return substr($haystack, 0, $pos); + } + + return substr($haystack, $pos); + } + + public static function mb_get_info($type = 'all') + { + $info = [ + 'internal_encoding' => self::$internalEncoding, + 'http_output' => 'pass', + 'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)', + 'func_overload' => 0, + 'func_overload_list' => 'no overload', + 'mail_charset' => 'UTF-8', + 'mail_header_encoding' => 'BASE64', + 'mail_body_encoding' => 'BASE64', + 'illegal_chars' => 0, + 'encoding_translation' => 'Off', + 'language' => self::$language, + 'detect_order' => self::$encodingList, + 'substitute_character' => 'none', + 'strict_detection' => 'Off', + ]; + + if ('all' === $type) { + return $info; + } + if (isset($info[$type])) { + return $info[$type]; + } + + return false; + } + + public static function mb_http_input($type = '') + { + return false; + } + + public static function mb_http_output($encoding = null) + { + return null !== $encoding ? 'pass' === $encoding : 'pass'; + } + + public static function mb_strwidth($s, $encoding = null) + { + $encoding = self::getEncoding($encoding); + + if ('UTF-8' !== $encoding) { + $s = iconv($encoding, 'UTF-8//IGNORE', $s); + } + + $s = preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $s, -1, $wide); + + return ($wide << 1) + iconv_strlen($s, 'UTF-8'); + } + + public static function mb_substr_count($haystack, $needle, $encoding = null) + { + return substr_count($haystack, $needle); + } + + public static function mb_output_handler($contents, $status) + { + return $contents; + } + + public static function mb_chr($code, $encoding = null) + { + if (0x80 > $code %= 0x200000) { + $s = \chr($code); + } elseif (0x800 > $code) { + $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); + } elseif (0x10000 > $code) { + $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); + } else { + $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); + } + + if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { + $s = mb_convert_encoding($s, $encoding, 'UTF-8'); + } + + return $s; + } + + public static function mb_ord($s, $encoding = null) + { + if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { + $s = mb_convert_encoding($s, 'UTF-8', $encoding); + } + + if (1 === \strlen($s)) { + return \ord($s); + } + + $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0; + if (0xF0 <= $code) { + return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80; + } + if (0xE0 <= $code) { + return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80; + } + if (0xC0 <= $code) { + return (($code - 0xC0) << 6) + $s[2] - 0x80; + } + + return $code; + } + + public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, string $encoding = null): string + { + if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) { + throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH'); + } + + if (null === $encoding) { + $encoding = self::mb_internal_encoding(); + } + + try { + $validEncoding = @self::mb_check_encoding('', $encoding); + } catch (\ValueError $e) { + throw new \ValueError(sprintf('mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + // BC for PHP 7.3 and lower + if (!$validEncoding) { + throw new \ValueError(sprintf('mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + if (self::mb_strlen($pad_string, $encoding) <= 0) { + throw new \ValueError('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string'); + } + + $paddingRequired = $length - self::mb_strlen($string, $encoding); + + if ($paddingRequired < 1) { + return $string; + } + + switch ($pad_type) { + case \STR_PAD_LEFT: + return self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding).$string; + case \STR_PAD_RIGHT: + return $string.self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding); + default: + $leftPaddingLength = floor($paddingRequired / 2); + $rightPaddingLength = $paddingRequired - $leftPaddingLength; + + return self::mb_substr(str_repeat($pad_string, $leftPaddingLength), 0, $leftPaddingLength, $encoding).$string.self::mb_substr(str_repeat($pad_string, $rightPaddingLength), 0, $rightPaddingLength, $encoding); + } + } + + private static function getSubpart($pos, $part, $haystack, $encoding) + { + if (false === $pos) { + return false; + } + if ($part) { + return self::mb_substr($haystack, 0, $pos, $encoding); + } + + return self::mb_substr($haystack, $pos, null, $encoding); + } + + private static function html_encoding_callback(array $m) + { + $i = 1; + $entities = ''; + $m = unpack('C*', htmlentities($m[0], \ENT_COMPAT, 'UTF-8')); + + while (isset($m[$i])) { + if (0x80 > $m[$i]) { + $entities .= \chr($m[$i++]); + continue; + } + if (0xF0 <= $m[$i]) { + $c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; + } elseif (0xE0 <= $m[$i]) { + $c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; + } else { + $c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80; + } + + $entities .= '&#'.$c.';'; + } + + return $entities; + } + + private static function title_case(array $s) + { + return self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8'); + } + + private static function getData($file) + { + if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { + return require $file; + } + + return false; + } + + private static function getEncoding($encoding) + { + if (null === $encoding) { + return self::$internalEncoding; + } + + if ('UTF-8' === $encoding) { + return 'UTF-8'; + } + + $encoding = strtoupper($encoding); + + if ('8BIT' === $encoding || 'BINARY' === $encoding) { + return 'CP850'; + } + + if ('UTF8' === $encoding) { + return 'UTF-8'; + } + + return $encoding; + } +} diff --git a/vendor/symfony/polyfill-mbstring/README.md b/vendor/symfony/polyfill-mbstring/README.md new file mode 100644 index 0000000..478b40d --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/README.md @@ -0,0 +1,13 @@ +Symfony Polyfill / Mbstring +=========================== + +This component provides a partial, native PHP implementation for the +[Mbstring](https://php.net/mbstring) extension. + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php new file mode 100644 index 0000000..512bba0 --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php @@ -0,0 +1,119 @@ + 'i̇', + 'µ' => 'μ', + 'ſ' => 's', + 'ͅ' => 'ι', + 'ς' => 'σ', + 'ϐ' => 'β', + 'ϑ' => 'θ', + 'ϕ' => 'φ', + 'ϖ' => 'π', + 'ϰ' => 'κ', + 'ϱ' => 'ρ', + 'ϵ' => 'ε', + 'ẛ' => 'ṡ', + 'ι' => 'ι', + 'ß' => 'ss', + 'ʼn' => 'ʼn', + 'ǰ' => 'ǰ', + 'ΐ' => 'ΐ', + 'ΰ' => 'ΰ', + 'և' => 'եւ', + 'ẖ' => 'ẖ', + 'ẗ' => 'ẗ', + 'ẘ' => 'ẘ', + 'ẙ' => 'ẙ', + 'ẚ' => 'aʾ', + 'ẞ' => 'ss', + 'ὐ' => 'ὐ', + 'ὒ' => 'ὒ', + 'ὔ' => 'ὔ', + 'ὖ' => 'ὖ', + 'ᾀ' => 'ἀι', + 'ᾁ' => 'ἁι', + 'ᾂ' => 'ἂι', + 'ᾃ' => 'ἃι', + 'ᾄ' => 'ἄι', + 'ᾅ' => 'ἅι', + 'ᾆ' => 'ἆι', + 'ᾇ' => 'ἇι', + 'ᾈ' => 'ἀι', + 'ᾉ' => 'ἁι', + 'ᾊ' => 'ἂι', + 'ᾋ' => 'ἃι', + 'ᾌ' => 'ἄι', + 'ᾍ' => 'ἅι', + 'ᾎ' => 'ἆι', + 'ᾏ' => 'ἇι', + 'ᾐ' => 'ἠι', + 'ᾑ' => 'ἡι', + 'ᾒ' => 'ἢι', + 'ᾓ' => 'ἣι', + 'ᾔ' => 'ἤι', + 'ᾕ' => 'ἥι', + 'ᾖ' => 'ἦι', + 'ᾗ' => 'ἧι', + 'ᾘ' => 'ἠι', + 'ᾙ' => 'ἡι', + 'ᾚ' => 'ἢι', + 'ᾛ' => 'ἣι', + 'ᾜ' => 'ἤι', + 'ᾝ' => 'ἥι', + 'ᾞ' => 'ἦι', + 'ᾟ' => 'ἧι', + 'ᾠ' => 'ὠι', + 'ᾡ' => 'ὡι', + 'ᾢ' => 'ὢι', + 'ᾣ' => 'ὣι', + 'ᾤ' => 'ὤι', + 'ᾥ' => 'ὥι', + 'ᾦ' => 'ὦι', + 'ᾧ' => 'ὧι', + 'ᾨ' => 'ὠι', + 'ᾩ' => 'ὡι', + 'ᾪ' => 'ὢι', + 'ᾫ' => 'ὣι', + 'ᾬ' => 'ὤι', + 'ᾭ' => 'ὥι', + 'ᾮ' => 'ὦι', + 'ᾯ' => 'ὧι', + 'ᾲ' => 'ὰι', + 'ᾳ' => 'αι', + 'ᾴ' => 'άι', + 'ᾶ' => 'ᾶ', + 'ᾷ' => 'ᾶι', + 'ᾼ' => 'αι', + 'ῂ' => 'ὴι', + 'ῃ' => 'ηι', + 'ῄ' => 'ήι', + 'ῆ' => 'ῆ', + 'ῇ' => 'ῆι', + 'ῌ' => 'ηι', + 'ῒ' => 'ῒ', + 'ῖ' => 'ῖ', + 'ῗ' => 'ῗ', + 'ῢ' => 'ῢ', + 'ῤ' => 'ῤ', + 'ῦ' => 'ῦ', + 'ῧ' => 'ῧ', + 'ῲ' => 'ὼι', + 'ῳ' => 'ωι', + 'ῴ' => 'ώι', + 'ῶ' => 'ῶ', + 'ῷ' => 'ῶι', + 'ῼ' => 'ωι', + 'ff' => 'ff', + 'fi' => 'fi', + 'fl' => 'fl', + 'ffi' => 'ffi', + 'ffl' => 'ffl', + 'ſt' => 'st', + 'st' => 'st', + 'ﬓ' => 'մն', + 'ﬔ' => 'մե', + 'ﬕ' => 'մի', + 'ﬖ' => 'վն', + 'ﬗ' => 'մխ', +]; diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php new file mode 100644 index 0000000..fac60b0 --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php @@ -0,0 +1,1397 @@ + 'a', + 'B' => 'b', + 'C' => 'c', + 'D' => 'd', + 'E' => 'e', + 'F' => 'f', + 'G' => 'g', + 'H' => 'h', + 'I' => 'i', + 'J' => 'j', + 'K' => 'k', + 'L' => 'l', + 'M' => 'm', + 'N' => 'n', + 'O' => 'o', + 'P' => 'p', + 'Q' => 'q', + 'R' => 'r', + 'S' => 's', + 'T' => 't', + 'U' => 'u', + 'V' => 'v', + 'W' => 'w', + 'X' => 'x', + 'Y' => 'y', + 'Z' => 'z', + 'À' => 'à', + 'Á' => 'á', + 'Â' => 'â', + 'Ã' => 'ã', + 'Ä' => 'ä', + 'Å' => 'å', + 'Æ' => 'æ', + 'Ç' => 'ç', + 'È' => 'è', + 'É' => 'é', + 'Ê' => 'ê', + 'Ë' => 'ë', + 'Ì' => 'ì', + 'Í' => 'í', + 'Î' => 'î', + 'Ï' => 'ï', + 'Ð' => 'ð', + 'Ñ' => 'ñ', + 'Ò' => 'ò', + 'Ó' => 'ó', + 'Ô' => 'ô', + 'Õ' => 'õ', + 'Ö' => 'ö', + 'Ø' => 'ø', + 'Ù' => 'ù', + 'Ú' => 'ú', + 'Û' => 'û', + 'Ü' => 'ü', + 'Ý' => 'ý', + 'Þ' => 'þ', + 'Ā' => 'ā', + 'Ă' => 'ă', + 'Ą' => 'ą', + 'Ć' => 'ć', + 'Ĉ' => 'ĉ', + 'Ċ' => 'ċ', + 'Č' => 'č', + 'Ď' => 'ď', + 'Đ' => 'đ', + 'Ē' => 'ē', + 'Ĕ' => 'ĕ', + 'Ė' => 'ė', + 'Ę' => 'ę', + 'Ě' => 'ě', + 'Ĝ' => 'ĝ', + 'Ğ' => 'ğ', + 'Ġ' => 'ġ', + 'Ģ' => 'ģ', + 'Ĥ' => 'ĥ', + 'Ħ' => 'ħ', + 'Ĩ' => 'ĩ', + 'Ī' => 'ī', + 'Ĭ' => 'ĭ', + 'Į' => 'į', + 'İ' => 'i̇', + 'IJ' => 'ij', + 'Ĵ' => 'ĵ', + 'Ķ' => 'ķ', + 'Ĺ' => 'ĺ', + 'Ļ' => 'ļ', + 'Ľ' => 'ľ', + 'Ŀ' => 'ŀ', + 'Ł' => 'ł', + 'Ń' => 'ń', + 'Ņ' => 'ņ', + 'Ň' => 'ň', + 'Ŋ' => 'ŋ', + 'Ō' => 'ō', + 'Ŏ' => 'ŏ', + 'Ő' => 'ő', + 'Œ' => 'œ', + 'Ŕ' => 'ŕ', + 'Ŗ' => 'ŗ', + 'Ř' => 'ř', + 'Ś' => 'ś', + 'Ŝ' => 'ŝ', + 'Ş' => 'ş', + 'Š' => 'š', + 'Ţ' => 'ţ', + 'Ť' => 'ť', + 'Ŧ' => 'ŧ', + 'Ũ' => 'ũ', + 'Ū' => 'ū', + 'Ŭ' => 'ŭ', + 'Ů' => 'ů', + 'Ű' => 'ű', + 'Ų' => 'ų', + 'Ŵ' => 'ŵ', + 'Ŷ' => 'ŷ', + 'Ÿ' => 'ÿ', + 'Ź' => 'ź', + 'Ż' => 'ż', + 'Ž' => 'ž', + 'Ɓ' => 'ɓ', + 'Ƃ' => 'ƃ', + 'Ƅ' => 'ƅ', + 'Ɔ' => 'ɔ', + 'Ƈ' => 'ƈ', + 'Ɖ' => 'ɖ', + 'Ɗ' => 'ɗ', + 'Ƌ' => 'ƌ', + 'Ǝ' => 'ǝ', + 'Ə' => 'ə', + 'Ɛ' => 'ɛ', + 'Ƒ' => 'ƒ', + 'Ɠ' => 'ɠ', + 'Ɣ' => 'ɣ', + 'Ɩ' => 'ɩ', + 'Ɨ' => 'ɨ', + 'Ƙ' => 'ƙ', + 'Ɯ' => 'ɯ', + 'Ɲ' => 'ɲ', + 'Ɵ' => 'ɵ', + 'Ơ' => 'ơ', + 'Ƣ' => 'ƣ', + 'Ƥ' => 'ƥ', + 'Ʀ' => 'ʀ', + 'Ƨ' => 'ƨ', + 'Ʃ' => 'ʃ', + 'Ƭ' => 'ƭ', + 'Ʈ' => 'ʈ', + 'Ư' => 'ư', + 'Ʊ' => 'ʊ', + 'Ʋ' => 'ʋ', + 'Ƴ' => 'ƴ', + 'Ƶ' => 'ƶ', + 'Ʒ' => 'ʒ', + 'Ƹ' => 'ƹ', + 'Ƽ' => 'ƽ', + 'DŽ' => 'dž', + 'Dž' => 'dž', + 'LJ' => 'lj', + 'Lj' => 'lj', + 'NJ' => 'nj', + 'Nj' => 'nj', + 'Ǎ' => 'ǎ', + 'Ǐ' => 'ǐ', + 'Ǒ' => 'ǒ', + 'Ǔ' => 'ǔ', + 'Ǖ' => 'ǖ', + 'Ǘ' => 'ǘ', + 'Ǚ' => 'ǚ', + 'Ǜ' => 'ǜ', + 'Ǟ' => 'ǟ', + 'Ǡ' => 'ǡ', + 'Ǣ' => 'ǣ', + 'Ǥ' => 'ǥ', + 'Ǧ' => 'ǧ', + 'Ǩ' => 'ǩ', + 'Ǫ' => 'ǫ', + 'Ǭ' => 'ǭ', + 'Ǯ' => 'ǯ', + 'DZ' => 'dz', + 'Dz' => 'dz', + 'Ǵ' => 'ǵ', + 'Ƕ' => 'ƕ', + 'Ƿ' => 'ƿ', + 'Ǹ' => 'ǹ', + 'Ǻ' => 'ǻ', + 'Ǽ' => 'ǽ', + 'Ǿ' => 'ǿ', + 'Ȁ' => 'ȁ', + 'Ȃ' => 'ȃ', + 'Ȅ' => 'ȅ', + 'Ȇ' => 'ȇ', + 'Ȉ' => 'ȉ', + 'Ȋ' => 'ȋ', + 'Ȍ' => 'ȍ', + 'Ȏ' => 'ȏ', + 'Ȑ' => 'ȑ', + 'Ȓ' => 'ȓ', + 'Ȕ' => 'ȕ', + 'Ȗ' => 'ȗ', + 'Ș' => 'ș', + 'Ț' => 'ț', + 'Ȝ' => 'ȝ', + 'Ȟ' => 'ȟ', + 'Ƞ' => 'ƞ', + 'Ȣ' => 'ȣ', + 'Ȥ' => 'ȥ', + 'Ȧ' => 'ȧ', + 'Ȩ' => 'ȩ', + 'Ȫ' => 'ȫ', + 'Ȭ' => 'ȭ', + 'Ȯ' => 'ȯ', + 'Ȱ' => 'ȱ', + 'Ȳ' => 'ȳ', + 'Ⱥ' => 'ⱥ', + 'Ȼ' => 'ȼ', + 'Ƚ' => 'ƚ', + 'Ⱦ' => 'ⱦ', + 'Ɂ' => 'ɂ', + 'Ƀ' => 'ƀ', + 'Ʉ' => 'ʉ', + 'Ʌ' => 'ʌ', + 'Ɇ' => 'ɇ', + 'Ɉ' => 'ɉ', + 'Ɋ' => 'ɋ', + 'Ɍ' => 'ɍ', + 'Ɏ' => 'ɏ', + 'Ͱ' => 'ͱ', + 'Ͳ' => 'ͳ', + 'Ͷ' => 'ͷ', + 'Ϳ' => 'ϳ', + 'Ά' => 'ά', + 'Έ' => 'έ', + 'Ή' => 'ή', + 'Ί' => 'ί', + 'Ό' => 'ό', + 'Ύ' => 'ύ', + 'Ώ' => 'ώ', + 'Α' => 'α', + 'Β' => 'β', + 'Γ' => 'γ', + 'Δ' => 'δ', + 'Ε' => 'ε', + 'Ζ' => 'ζ', + 'Η' => 'η', + 'Θ' => 'θ', + 'Ι' => 'ι', + 'Κ' => 'κ', + 'Λ' => 'λ', + 'Μ' => 'μ', + 'Ν' => 'ν', + 'Ξ' => 'ξ', + 'Ο' => 'ο', + 'Π' => 'π', + 'Ρ' => 'ρ', + 'Σ' => 'σ', + 'Τ' => 'τ', + 'Υ' => 'υ', + 'Φ' => 'φ', + 'Χ' => 'χ', + 'Ψ' => 'ψ', + 'Ω' => 'ω', + 'Ϊ' => 'ϊ', + 'Ϋ' => 'ϋ', + 'Ϗ' => 'ϗ', + 'Ϙ' => 'ϙ', + 'Ϛ' => 'ϛ', + 'Ϝ' => 'ϝ', + 'Ϟ' => 'ϟ', + 'Ϡ' => 'ϡ', + 'Ϣ' => 'ϣ', + 'Ϥ' => 'ϥ', + 'Ϧ' => 'ϧ', + 'Ϩ' => 'ϩ', + 'Ϫ' => 'ϫ', + 'Ϭ' => 'ϭ', + 'Ϯ' => 'ϯ', + 'ϴ' => 'θ', + 'Ϸ' => 'ϸ', + 'Ϲ' => 'ϲ', + 'Ϻ' => 'ϻ', + 'Ͻ' => 'ͻ', + 'Ͼ' => 'ͼ', + 'Ͽ' => 'ͽ', + 'Ѐ' => 'ѐ', + 'Ё' => 'ё', + 'Ђ' => 'ђ', + 'Ѓ' => 'ѓ', + 'Є' => 'є', + 'Ѕ' => 'ѕ', + 'І' => 'і', + 'Ї' => 'ї', + 'Ј' => 'ј', + 'Љ' => 'љ', + 'Њ' => 'њ', + 'Ћ' => 'ћ', + 'Ќ' => 'ќ', + 'Ѝ' => 'ѝ', + 'Ў' => 'ў', + 'Џ' => 'џ', + 'А' => 'а', + 'Б' => 'б', + 'В' => 'в', + 'Г' => 'г', + 'Д' => 'д', + 'Е' => 'е', + 'Ж' => 'ж', + 'З' => 'з', + 'И' => 'и', + 'Й' => 'й', + 'К' => 'к', + 'Л' => 'л', + 'М' => 'м', + 'Н' => 'н', + 'О' => 'о', + 'П' => 'п', + 'Р' => 'р', + 'С' => 'с', + 'Т' => 'т', + 'У' => 'у', + 'Ф' => 'ф', + 'Х' => 'х', + 'Ц' => 'ц', + 'Ч' => 'ч', + 'Ш' => 'ш', + 'Щ' => 'щ', + 'Ъ' => 'ъ', + 'Ы' => 'ы', + 'Ь' => 'ь', + 'Э' => 'э', + 'Ю' => 'ю', + 'Я' => 'я', + 'Ѡ' => 'ѡ', + 'Ѣ' => 'ѣ', + 'Ѥ' => 'ѥ', + 'Ѧ' => 'ѧ', + 'Ѩ' => 'ѩ', + 'Ѫ' => 'ѫ', + 'Ѭ' => 'ѭ', + 'Ѯ' => 'ѯ', + 'Ѱ' => 'ѱ', + 'Ѳ' => 'ѳ', + 'Ѵ' => 'ѵ', + 'Ѷ' => 'ѷ', + 'Ѹ' => 'ѹ', + 'Ѻ' => 'ѻ', + 'Ѽ' => 'ѽ', + 'Ѿ' => 'ѿ', + 'Ҁ' => 'ҁ', + 'Ҋ' => 'ҋ', + 'Ҍ' => 'ҍ', + 'Ҏ' => 'ҏ', + 'Ґ' => 'ґ', + 'Ғ' => 'ғ', + 'Ҕ' => 'ҕ', + 'Җ' => 'җ', + 'Ҙ' => 'ҙ', + 'Қ' => 'қ', + 'Ҝ' => 'ҝ', + 'Ҟ' => 'ҟ', + 'Ҡ' => 'ҡ', + 'Ң' => 'ң', + 'Ҥ' => 'ҥ', + 'Ҧ' => 'ҧ', + 'Ҩ' => 'ҩ', + 'Ҫ' => 'ҫ', + 'Ҭ' => 'ҭ', + 'Ү' => 'ү', + 'Ұ' => 'ұ', + 'Ҳ' => 'ҳ', + 'Ҵ' => 'ҵ', + 'Ҷ' => 'ҷ', + 'Ҹ' => 'ҹ', + 'Һ' => 'һ', + 'Ҽ' => 'ҽ', + 'Ҿ' => 'ҿ', + 'Ӏ' => 'ӏ', + 'Ӂ' => 'ӂ', + 'Ӄ' => 'ӄ', + 'Ӆ' => 'ӆ', + 'Ӈ' => 'ӈ', + 'Ӊ' => 'ӊ', + 'Ӌ' => 'ӌ', + 'Ӎ' => 'ӎ', + 'Ӑ' => 'ӑ', + 'Ӓ' => 'ӓ', + 'Ӕ' => 'ӕ', + 'Ӗ' => 'ӗ', + 'Ә' => 'ә', + 'Ӛ' => 'ӛ', + 'Ӝ' => 'ӝ', + 'Ӟ' => 'ӟ', + 'Ӡ' => 'ӡ', + 'Ӣ' => 'ӣ', + 'Ӥ' => 'ӥ', + 'Ӧ' => 'ӧ', + 'Ө' => 'ө', + 'Ӫ' => 'ӫ', + 'Ӭ' => 'ӭ', + 'Ӯ' => 'ӯ', + 'Ӱ' => 'ӱ', + 'Ӳ' => 'ӳ', + 'Ӵ' => 'ӵ', + 'Ӷ' => 'ӷ', + 'Ӹ' => 'ӹ', + 'Ӻ' => 'ӻ', + 'Ӽ' => 'ӽ', + 'Ӿ' => 'ӿ', + 'Ԁ' => 'ԁ', + 'Ԃ' => 'ԃ', + 'Ԅ' => 'ԅ', + 'Ԇ' => 'ԇ', + 'Ԉ' => 'ԉ', + 'Ԋ' => 'ԋ', + 'Ԍ' => 'ԍ', + 'Ԏ' => 'ԏ', + 'Ԑ' => 'ԑ', + 'Ԓ' => 'ԓ', + 'Ԕ' => 'ԕ', + 'Ԗ' => 'ԗ', + 'Ԙ' => 'ԙ', + 'Ԛ' => 'ԛ', + 'Ԝ' => 'ԝ', + 'Ԟ' => 'ԟ', + 'Ԡ' => 'ԡ', + 'Ԣ' => 'ԣ', + 'Ԥ' => 'ԥ', + 'Ԧ' => 'ԧ', + 'Ԩ' => 'ԩ', + 'Ԫ' => 'ԫ', + 'Ԭ' => 'ԭ', + 'Ԯ' => 'ԯ', + 'Ա' => 'ա', + 'Բ' => 'բ', + 'Գ' => 'գ', + 'Դ' => 'դ', + 'Ե' => 'ե', + 'Զ' => 'զ', + 'Է' => 'է', + 'Ը' => 'ը', + 'Թ' => 'թ', + 'Ժ' => 'ժ', + 'Ի' => 'ի', + 'Լ' => 'լ', + 'Խ' => 'խ', + 'Ծ' => 'ծ', + 'Կ' => 'կ', + 'Հ' => 'հ', + 'Ձ' => 'ձ', + 'Ղ' => 'ղ', + 'Ճ' => 'ճ', + 'Մ' => 'մ', + 'Յ' => 'յ', + 'Ն' => 'ն', + 'Շ' => 'շ', + 'Ո' => 'ո', + 'Չ' => 'չ', + 'Պ' => 'պ', + 'Ջ' => 'ջ', + 'Ռ' => 'ռ', + 'Ս' => 'ս', + 'Վ' => 'վ', + 'Տ' => 'տ', + 'Ր' => 'ր', + 'Ց' => 'ց', + 'Ւ' => 'ւ', + 'Փ' => 'փ', + 'Ք' => 'ք', + 'Օ' => 'օ', + 'Ֆ' => 'ֆ', + 'Ⴀ' => 'ⴀ', + 'Ⴁ' => 'ⴁ', + 'Ⴂ' => 'ⴂ', + 'Ⴃ' => 'ⴃ', + 'Ⴄ' => 'ⴄ', + 'Ⴅ' => 'ⴅ', + 'Ⴆ' => 'ⴆ', + 'Ⴇ' => 'ⴇ', + 'Ⴈ' => 'ⴈ', + 'Ⴉ' => 'ⴉ', + 'Ⴊ' => 'ⴊ', + 'Ⴋ' => 'ⴋ', + 'Ⴌ' => 'ⴌ', + 'Ⴍ' => 'ⴍ', + 'Ⴎ' => 'ⴎ', + 'Ⴏ' => 'ⴏ', + 'Ⴐ' => 'ⴐ', + 'Ⴑ' => 'ⴑ', + 'Ⴒ' => 'ⴒ', + 'Ⴓ' => 'ⴓ', + 'Ⴔ' => 'ⴔ', + 'Ⴕ' => 'ⴕ', + 'Ⴖ' => 'ⴖ', + 'Ⴗ' => 'ⴗ', + 'Ⴘ' => 'ⴘ', + 'Ⴙ' => 'ⴙ', + 'Ⴚ' => 'ⴚ', + 'Ⴛ' => 'ⴛ', + 'Ⴜ' => 'ⴜ', + 'Ⴝ' => 'ⴝ', + 'Ⴞ' => 'ⴞ', + 'Ⴟ' => 'ⴟ', + 'Ⴠ' => 'ⴠ', + 'Ⴡ' => 'ⴡ', + 'Ⴢ' => 'ⴢ', + 'Ⴣ' => 'ⴣ', + 'Ⴤ' => 'ⴤ', + 'Ⴥ' => 'ⴥ', + 'Ⴧ' => 'ⴧ', + 'Ⴭ' => 'ⴭ', + 'Ꭰ' => 'ꭰ', + 'Ꭱ' => 'ꭱ', + 'Ꭲ' => 'ꭲ', + 'Ꭳ' => 'ꭳ', + 'Ꭴ' => 'ꭴ', + 'Ꭵ' => 'ꭵ', + 'Ꭶ' => 'ꭶ', + 'Ꭷ' => 'ꭷ', + 'Ꭸ' => 'ꭸ', + 'Ꭹ' => 'ꭹ', + 'Ꭺ' => 'ꭺ', + 'Ꭻ' => 'ꭻ', + 'Ꭼ' => 'ꭼ', + 'Ꭽ' => 'ꭽ', + 'Ꭾ' => 'ꭾ', + 'Ꭿ' => 'ꭿ', + 'Ꮀ' => 'ꮀ', + 'Ꮁ' => 'ꮁ', + 'Ꮂ' => 'ꮂ', + 'Ꮃ' => 'ꮃ', + 'Ꮄ' => 'ꮄ', + 'Ꮅ' => 'ꮅ', + 'Ꮆ' => 'ꮆ', + 'Ꮇ' => 'ꮇ', + 'Ꮈ' => 'ꮈ', + 'Ꮉ' => 'ꮉ', + 'Ꮊ' => 'ꮊ', + 'Ꮋ' => 'ꮋ', + 'Ꮌ' => 'ꮌ', + 'Ꮍ' => 'ꮍ', + 'Ꮎ' => 'ꮎ', + 'Ꮏ' => 'ꮏ', + 'Ꮐ' => 'ꮐ', + 'Ꮑ' => 'ꮑ', + 'Ꮒ' => 'ꮒ', + 'Ꮓ' => 'ꮓ', + 'Ꮔ' => 'ꮔ', + 'Ꮕ' => 'ꮕ', + 'Ꮖ' => 'ꮖ', + 'Ꮗ' => 'ꮗ', + 'Ꮘ' => 'ꮘ', + 'Ꮙ' => 'ꮙ', + 'Ꮚ' => 'ꮚ', + 'Ꮛ' => 'ꮛ', + 'Ꮜ' => 'ꮜ', + 'Ꮝ' => 'ꮝ', + 'Ꮞ' => 'ꮞ', + 'Ꮟ' => 'ꮟ', + 'Ꮠ' => 'ꮠ', + 'Ꮡ' => 'ꮡ', + 'Ꮢ' => 'ꮢ', + 'Ꮣ' => 'ꮣ', + 'Ꮤ' => 'ꮤ', + 'Ꮥ' => 'ꮥ', + 'Ꮦ' => 'ꮦ', + 'Ꮧ' => 'ꮧ', + 'Ꮨ' => 'ꮨ', + 'Ꮩ' => 'ꮩ', + 'Ꮪ' => 'ꮪ', + 'Ꮫ' => 'ꮫ', + 'Ꮬ' => 'ꮬ', + 'Ꮭ' => 'ꮭ', + 'Ꮮ' => 'ꮮ', + 'Ꮯ' => 'ꮯ', + 'Ꮰ' => 'ꮰ', + 'Ꮱ' => 'ꮱ', + 'Ꮲ' => 'ꮲ', + 'Ꮳ' => 'ꮳ', + 'Ꮴ' => 'ꮴ', + 'Ꮵ' => 'ꮵ', + 'Ꮶ' => 'ꮶ', + 'Ꮷ' => 'ꮷ', + 'Ꮸ' => 'ꮸ', + 'Ꮹ' => 'ꮹ', + 'Ꮺ' => 'ꮺ', + 'Ꮻ' => 'ꮻ', + 'Ꮼ' => 'ꮼ', + 'Ꮽ' => 'ꮽ', + 'Ꮾ' => 'ꮾ', + 'Ꮿ' => 'ꮿ', + 'Ᏸ' => 'ᏸ', + 'Ᏹ' => 'ᏹ', + 'Ᏺ' => 'ᏺ', + 'Ᏻ' => 'ᏻ', + 'Ᏼ' => 'ᏼ', + 'Ᏽ' => 'ᏽ', + 'Ა' => 'ა', + 'Ბ' => 'ბ', + 'Გ' => 'გ', + 'Დ' => 'დ', + 'Ე' => 'ე', + 'Ვ' => 'ვ', + 'Ზ' => 'ზ', + 'Თ' => 'თ', + 'Ი' => 'ი', + 'Კ' => 'კ', + 'Ლ' => 'ლ', + 'Მ' => 'მ', + 'Ნ' => 'ნ', + 'Ო' => 'ო', + 'Პ' => 'პ', + 'Ჟ' => 'ჟ', + 'Რ' => 'რ', + 'Ს' => 'ს', + 'Ტ' => 'ტ', + 'Უ' => 'უ', + 'Ფ' => 'ფ', + 'Ქ' => 'ქ', + 'Ღ' => 'ღ', + 'Ყ' => 'ყ', + 'Შ' => 'შ', + 'Ჩ' => 'ჩ', + 'Ც' => 'ც', + 'Ძ' => 'ძ', + 'Წ' => 'წ', + 'Ჭ' => 'ჭ', + 'Ხ' => 'ხ', + 'Ჯ' => 'ჯ', + 'Ჰ' => 'ჰ', + 'Ჱ' => 'ჱ', + 'Ჲ' => 'ჲ', + 'Ჳ' => 'ჳ', + 'Ჴ' => 'ჴ', + 'Ჵ' => 'ჵ', + 'Ჶ' => 'ჶ', + 'Ჷ' => 'ჷ', + 'Ჸ' => 'ჸ', + 'Ჹ' => 'ჹ', + 'Ჺ' => 'ჺ', + 'Ჽ' => 'ჽ', + 'Ჾ' => 'ჾ', + 'Ჿ' => 'ჿ', + 'Ḁ' => 'ḁ', + 'Ḃ' => 'ḃ', + 'Ḅ' => 'ḅ', + 'Ḇ' => 'ḇ', + 'Ḉ' => 'ḉ', + 'Ḋ' => 'ḋ', + 'Ḍ' => 'ḍ', + 'Ḏ' => 'ḏ', + 'Ḑ' => 'ḑ', + 'Ḓ' => 'ḓ', + 'Ḕ' => 'ḕ', + 'Ḗ' => 'ḗ', + 'Ḙ' => 'ḙ', + 'Ḛ' => 'ḛ', + 'Ḝ' => 'ḝ', + 'Ḟ' => 'ḟ', + 'Ḡ' => 'ḡ', + 'Ḣ' => 'ḣ', + 'Ḥ' => 'ḥ', + 'Ḧ' => 'ḧ', + 'Ḩ' => 'ḩ', + 'Ḫ' => 'ḫ', + 'Ḭ' => 'ḭ', + 'Ḯ' => 'ḯ', + 'Ḱ' => 'ḱ', + 'Ḳ' => 'ḳ', + 'Ḵ' => 'ḵ', + 'Ḷ' => 'ḷ', + 'Ḹ' => 'ḹ', + 'Ḻ' => 'ḻ', + 'Ḽ' => 'ḽ', + 'Ḿ' => 'ḿ', + 'Ṁ' => 'ṁ', + 'Ṃ' => 'ṃ', + 'Ṅ' => 'ṅ', + 'Ṇ' => 'ṇ', + 'Ṉ' => 'ṉ', + 'Ṋ' => 'ṋ', + 'Ṍ' => 'ṍ', + 'Ṏ' => 'ṏ', + 'Ṑ' => 'ṑ', + 'Ṓ' => 'ṓ', + 'Ṕ' => 'ṕ', + 'Ṗ' => 'ṗ', + 'Ṙ' => 'ṙ', + 'Ṛ' => 'ṛ', + 'Ṝ' => 'ṝ', + 'Ṟ' => 'ṟ', + 'Ṡ' => 'ṡ', + 'Ṣ' => 'ṣ', + 'Ṥ' => 'ṥ', + 'Ṧ' => 'ṧ', + 'Ṩ' => 'ṩ', + 'Ṫ' => 'ṫ', + 'Ṭ' => 'ṭ', + 'Ṯ' => 'ṯ', + 'Ṱ' => 'ṱ', + 'Ṳ' => 'ṳ', + 'Ṵ' => 'ṵ', + 'Ṷ' => 'ṷ', + 'Ṹ' => 'ṹ', + 'Ṻ' => 'ṻ', + 'Ṽ' => 'ṽ', + 'Ṿ' => 'ṿ', + 'Ẁ' => 'ẁ', + 'Ẃ' => 'ẃ', + 'Ẅ' => 'ẅ', + 'Ẇ' => 'ẇ', + 'Ẉ' => 'ẉ', + 'Ẋ' => 'ẋ', + 'Ẍ' => 'ẍ', + 'Ẏ' => 'ẏ', + 'Ẑ' => 'ẑ', + 'Ẓ' => 'ẓ', + 'Ẕ' => 'ẕ', + 'ẞ' => 'ß', + 'Ạ' => 'ạ', + 'Ả' => 'ả', + 'Ấ' => 'ấ', + 'Ầ' => 'ầ', + 'Ẩ' => 'ẩ', + 'Ẫ' => 'ẫ', + 'Ậ' => 'ậ', + 'Ắ' => 'ắ', + 'Ằ' => 'ằ', + 'Ẳ' => 'ẳ', + 'Ẵ' => 'ẵ', + 'Ặ' => 'ặ', + 'Ẹ' => 'ẹ', + 'Ẻ' => 'ẻ', + 'Ẽ' => 'ẽ', + 'Ế' => 'ế', + 'Ề' => 'ề', + 'Ể' => 'ể', + 'Ễ' => 'ễ', + 'Ệ' => 'ệ', + 'Ỉ' => 'ỉ', + 'Ị' => 'ị', + 'Ọ' => 'ọ', + 'Ỏ' => 'ỏ', + 'Ố' => 'ố', + 'Ồ' => 'ồ', + 'Ổ' => 'ổ', + 'Ỗ' => 'ỗ', + 'Ộ' => 'ộ', + 'Ớ' => 'ớ', + 'Ờ' => 'ờ', + 'Ở' => 'ở', + 'Ỡ' => 'ỡ', + 'Ợ' => 'ợ', + 'Ụ' => 'ụ', + 'Ủ' => 'ủ', + 'Ứ' => 'ứ', + 'Ừ' => 'ừ', + 'Ử' => 'ử', + 'Ữ' => 'ữ', + 'Ự' => 'ự', + 'Ỳ' => 'ỳ', + 'Ỵ' => 'ỵ', + 'Ỷ' => 'ỷ', + 'Ỹ' => 'ỹ', + 'Ỻ' => 'ỻ', + 'Ỽ' => 'ỽ', + 'Ỿ' => 'ỿ', + 'Ἀ' => 'ἀ', + 'Ἁ' => 'ἁ', + 'Ἂ' => 'ἂ', + 'Ἃ' => 'ἃ', + 'Ἄ' => 'ἄ', + 'Ἅ' => 'ἅ', + 'Ἆ' => 'ἆ', + 'Ἇ' => 'ἇ', + 'Ἐ' => 'ἐ', + 'Ἑ' => 'ἑ', + 'Ἒ' => 'ἒ', + 'Ἓ' => 'ἓ', + 'Ἔ' => 'ἔ', + 'Ἕ' => 'ἕ', + 'Ἠ' => 'ἠ', + 'Ἡ' => 'ἡ', + 'Ἢ' => 'ἢ', + 'Ἣ' => 'ἣ', + 'Ἤ' => 'ἤ', + 'Ἥ' => 'ἥ', + 'Ἦ' => 'ἦ', + 'Ἧ' => 'ἧ', + 'Ἰ' => 'ἰ', + 'Ἱ' => 'ἱ', + 'Ἲ' => 'ἲ', + 'Ἳ' => 'ἳ', + 'Ἴ' => 'ἴ', + 'Ἵ' => 'ἵ', + 'Ἶ' => 'ἶ', + 'Ἷ' => 'ἷ', + 'Ὀ' => 'ὀ', + 'Ὁ' => 'ὁ', + 'Ὂ' => 'ὂ', + 'Ὃ' => 'ὃ', + 'Ὄ' => 'ὄ', + 'Ὅ' => 'ὅ', + 'Ὑ' => 'ὑ', + 'Ὓ' => 'ὓ', + 'Ὕ' => 'ὕ', + 'Ὗ' => 'ὗ', + 'Ὠ' => 'ὠ', + 'Ὡ' => 'ὡ', + 'Ὢ' => 'ὢ', + 'Ὣ' => 'ὣ', + 'Ὤ' => 'ὤ', + 'Ὥ' => 'ὥ', + 'Ὦ' => 'ὦ', + 'Ὧ' => 'ὧ', + 'ᾈ' => 'ᾀ', + 'ᾉ' => 'ᾁ', + 'ᾊ' => 'ᾂ', + 'ᾋ' => 'ᾃ', + 'ᾌ' => 'ᾄ', + 'ᾍ' => 'ᾅ', + 'ᾎ' => 'ᾆ', + 'ᾏ' => 'ᾇ', + 'ᾘ' => 'ᾐ', + 'ᾙ' => 'ᾑ', + 'ᾚ' => 'ᾒ', + 'ᾛ' => 'ᾓ', + 'ᾜ' => 'ᾔ', + 'ᾝ' => 'ᾕ', + 'ᾞ' => 'ᾖ', + 'ᾟ' => 'ᾗ', + 'ᾨ' => 'ᾠ', + 'ᾩ' => 'ᾡ', + 'ᾪ' => 'ᾢ', + 'ᾫ' => 'ᾣ', + 'ᾬ' => 'ᾤ', + 'ᾭ' => 'ᾥ', + 'ᾮ' => 'ᾦ', + 'ᾯ' => 'ᾧ', + 'Ᾰ' => 'ᾰ', + 'Ᾱ' => 'ᾱ', + 'Ὰ' => 'ὰ', + 'Ά' => 'ά', + 'ᾼ' => 'ᾳ', + 'Ὲ' => 'ὲ', + 'Έ' => 'έ', + 'Ὴ' => 'ὴ', + 'Ή' => 'ή', + 'ῌ' => 'ῃ', + 'Ῐ' => 'ῐ', + 'Ῑ' => 'ῑ', + 'Ὶ' => 'ὶ', + 'Ί' => 'ί', + 'Ῠ' => 'ῠ', + 'Ῡ' => 'ῡ', + 'Ὺ' => 'ὺ', + 'Ύ' => 'ύ', + 'Ῥ' => 'ῥ', + 'Ὸ' => 'ὸ', + 'Ό' => 'ό', + 'Ὼ' => 'ὼ', + 'Ώ' => 'ώ', + 'ῼ' => 'ῳ', + 'Ω' => 'ω', + 'K' => 'k', + 'Å' => 'å', + 'Ⅎ' => 'ⅎ', + 'Ⅰ' => 'ⅰ', + 'Ⅱ' => 'ⅱ', + 'Ⅲ' => 'ⅲ', + 'Ⅳ' => 'ⅳ', + 'Ⅴ' => 'ⅴ', + 'Ⅵ' => 'ⅵ', + 'Ⅶ' => 'ⅶ', + 'Ⅷ' => 'ⅷ', + 'Ⅸ' => 'ⅸ', + 'Ⅹ' => 'ⅹ', + 'Ⅺ' => 'ⅺ', + 'Ⅻ' => 'ⅻ', + 'Ⅼ' => 'ⅼ', + 'Ⅽ' => 'ⅽ', + 'Ⅾ' => 'ⅾ', + 'Ⅿ' => 'ⅿ', + 'Ↄ' => 'ↄ', + 'Ⓐ' => 'ⓐ', + 'Ⓑ' => 'ⓑ', + 'Ⓒ' => 'ⓒ', + 'Ⓓ' => 'ⓓ', + 'Ⓔ' => 'ⓔ', + 'Ⓕ' => 'ⓕ', + 'Ⓖ' => 'ⓖ', + 'Ⓗ' => 'ⓗ', + 'Ⓘ' => 'ⓘ', + 'Ⓙ' => 'ⓙ', + 'Ⓚ' => 'ⓚ', + 'Ⓛ' => 'ⓛ', + 'Ⓜ' => 'ⓜ', + 'Ⓝ' => 'ⓝ', + 'Ⓞ' => 'ⓞ', + 'Ⓟ' => 'ⓟ', + 'Ⓠ' => 'ⓠ', + 'Ⓡ' => 'ⓡ', + 'Ⓢ' => 'ⓢ', + 'Ⓣ' => 'ⓣ', + 'Ⓤ' => 'ⓤ', + 'Ⓥ' => 'ⓥ', + 'Ⓦ' => 'ⓦ', + 'Ⓧ' => 'ⓧ', + 'Ⓨ' => 'ⓨ', + 'Ⓩ' => 'ⓩ', + 'Ⰰ' => 'ⰰ', + 'Ⰱ' => 'ⰱ', + 'Ⰲ' => 'ⰲ', + 'Ⰳ' => 'ⰳ', + 'Ⰴ' => 'ⰴ', + 'Ⰵ' => 'ⰵ', + 'Ⰶ' => 'ⰶ', + 'Ⰷ' => 'ⰷ', + 'Ⰸ' => 'ⰸ', + 'Ⰹ' => 'ⰹ', + 'Ⰺ' => 'ⰺ', + 'Ⰻ' => 'ⰻ', + 'Ⰼ' => 'ⰼ', + 'Ⰽ' => 'ⰽ', + 'Ⰾ' => 'ⰾ', + 'Ⰿ' => 'ⰿ', + 'Ⱀ' => 'ⱀ', + 'Ⱁ' => 'ⱁ', + 'Ⱂ' => 'ⱂ', + 'Ⱃ' => 'ⱃ', + 'Ⱄ' => 'ⱄ', + 'Ⱅ' => 'ⱅ', + 'Ⱆ' => 'ⱆ', + 'Ⱇ' => 'ⱇ', + 'Ⱈ' => 'ⱈ', + 'Ⱉ' => 'ⱉ', + 'Ⱊ' => 'ⱊ', + 'Ⱋ' => 'ⱋ', + 'Ⱌ' => 'ⱌ', + 'Ⱍ' => 'ⱍ', + 'Ⱎ' => 'ⱎ', + 'Ⱏ' => 'ⱏ', + 'Ⱐ' => 'ⱐ', + 'Ⱑ' => 'ⱑ', + 'Ⱒ' => 'ⱒ', + 'Ⱓ' => 'ⱓ', + 'Ⱔ' => 'ⱔ', + 'Ⱕ' => 'ⱕ', + 'Ⱖ' => 'ⱖ', + 'Ⱗ' => 'ⱗ', + 'Ⱘ' => 'ⱘ', + 'Ⱙ' => 'ⱙ', + 'Ⱚ' => 'ⱚ', + 'Ⱛ' => 'ⱛ', + 'Ⱜ' => 'ⱜ', + 'Ⱝ' => 'ⱝ', + 'Ⱞ' => 'ⱞ', + 'Ⱡ' => 'ⱡ', + 'Ɫ' => 'ɫ', + 'Ᵽ' => 'ᵽ', + 'Ɽ' => 'ɽ', + 'Ⱨ' => 'ⱨ', + 'Ⱪ' => 'ⱪ', + 'Ⱬ' => 'ⱬ', + 'Ɑ' => 'ɑ', + 'Ɱ' => 'ɱ', + 'Ɐ' => 'ɐ', + 'Ɒ' => 'ɒ', + 'Ⱳ' => 'ⱳ', + 'Ⱶ' => 'ⱶ', + 'Ȿ' => 'ȿ', + 'Ɀ' => 'ɀ', + 'Ⲁ' => 'ⲁ', + 'Ⲃ' => 'ⲃ', + 'Ⲅ' => 'ⲅ', + 'Ⲇ' => 'ⲇ', + 'Ⲉ' => 'ⲉ', + 'Ⲋ' => 'ⲋ', + 'Ⲍ' => 'ⲍ', + 'Ⲏ' => 'ⲏ', + 'Ⲑ' => 'ⲑ', + 'Ⲓ' => 'ⲓ', + 'Ⲕ' => 'ⲕ', + 'Ⲗ' => 'ⲗ', + 'Ⲙ' => 'ⲙ', + 'Ⲛ' => 'ⲛ', + 'Ⲝ' => 'ⲝ', + 'Ⲟ' => 'ⲟ', + 'Ⲡ' => 'ⲡ', + 'Ⲣ' => 'ⲣ', + 'Ⲥ' => 'ⲥ', + 'Ⲧ' => 'ⲧ', + 'Ⲩ' => 'ⲩ', + 'Ⲫ' => 'ⲫ', + 'Ⲭ' => 'ⲭ', + 'Ⲯ' => 'ⲯ', + 'Ⲱ' => 'ⲱ', + 'Ⲳ' => 'ⲳ', + 'Ⲵ' => 'ⲵ', + 'Ⲷ' => 'ⲷ', + 'Ⲹ' => 'ⲹ', + 'Ⲻ' => 'ⲻ', + 'Ⲽ' => 'ⲽ', + 'Ⲿ' => 'ⲿ', + 'Ⳁ' => 'ⳁ', + 'Ⳃ' => 'ⳃ', + 'Ⳅ' => 'ⳅ', + 'Ⳇ' => 'ⳇ', + 'Ⳉ' => 'ⳉ', + 'Ⳋ' => 'ⳋ', + 'Ⳍ' => 'ⳍ', + 'Ⳏ' => 'ⳏ', + 'Ⳑ' => 'ⳑ', + 'Ⳓ' => 'ⳓ', + 'Ⳕ' => 'ⳕ', + 'Ⳗ' => 'ⳗ', + 'Ⳙ' => 'ⳙ', + 'Ⳛ' => 'ⳛ', + 'Ⳝ' => 'ⳝ', + 'Ⳟ' => 'ⳟ', + 'Ⳡ' => 'ⳡ', + 'Ⳣ' => 'ⳣ', + 'Ⳬ' => 'ⳬ', + 'Ⳮ' => 'ⳮ', + 'Ⳳ' => 'ⳳ', + 'Ꙁ' => 'ꙁ', + 'Ꙃ' => 'ꙃ', + 'Ꙅ' => 'ꙅ', + 'Ꙇ' => 'ꙇ', + 'Ꙉ' => 'ꙉ', + 'Ꙋ' => 'ꙋ', + 'Ꙍ' => 'ꙍ', + 'Ꙏ' => 'ꙏ', + 'Ꙑ' => 'ꙑ', + 'Ꙓ' => 'ꙓ', + 'Ꙕ' => 'ꙕ', + 'Ꙗ' => 'ꙗ', + 'Ꙙ' => 'ꙙ', + 'Ꙛ' => 'ꙛ', + 'Ꙝ' => 'ꙝ', + 'Ꙟ' => 'ꙟ', + 'Ꙡ' => 'ꙡ', + 'Ꙣ' => 'ꙣ', + 'Ꙥ' => 'ꙥ', + 'Ꙧ' => 'ꙧ', + 'Ꙩ' => 'ꙩ', + 'Ꙫ' => 'ꙫ', + 'Ꙭ' => 'ꙭ', + 'Ꚁ' => 'ꚁ', + 'Ꚃ' => 'ꚃ', + 'Ꚅ' => 'ꚅ', + 'Ꚇ' => 'ꚇ', + 'Ꚉ' => 'ꚉ', + 'Ꚋ' => 'ꚋ', + 'Ꚍ' => 'ꚍ', + 'Ꚏ' => 'ꚏ', + 'Ꚑ' => 'ꚑ', + 'Ꚓ' => 'ꚓ', + 'Ꚕ' => 'ꚕ', + 'Ꚗ' => 'ꚗ', + 'Ꚙ' => 'ꚙ', + 'Ꚛ' => 'ꚛ', + 'Ꜣ' => 'ꜣ', + 'Ꜥ' => 'ꜥ', + 'Ꜧ' => 'ꜧ', + 'Ꜩ' => 'ꜩ', + 'Ꜫ' => 'ꜫ', + 'Ꜭ' => 'ꜭ', + 'Ꜯ' => 'ꜯ', + 'Ꜳ' => 'ꜳ', + 'Ꜵ' => 'ꜵ', + 'Ꜷ' => 'ꜷ', + 'Ꜹ' => 'ꜹ', + 'Ꜻ' => 'ꜻ', + 'Ꜽ' => 'ꜽ', + 'Ꜿ' => 'ꜿ', + 'Ꝁ' => 'ꝁ', + 'Ꝃ' => 'ꝃ', + 'Ꝅ' => 'ꝅ', + 'Ꝇ' => 'ꝇ', + 'Ꝉ' => 'ꝉ', + 'Ꝋ' => 'ꝋ', + 'Ꝍ' => 'ꝍ', + 'Ꝏ' => 'ꝏ', + 'Ꝑ' => 'ꝑ', + 'Ꝓ' => 'ꝓ', + 'Ꝕ' => 'ꝕ', + 'Ꝗ' => 'ꝗ', + 'Ꝙ' => 'ꝙ', + 'Ꝛ' => 'ꝛ', + 'Ꝝ' => 'ꝝ', + 'Ꝟ' => 'ꝟ', + 'Ꝡ' => 'ꝡ', + 'Ꝣ' => 'ꝣ', + 'Ꝥ' => 'ꝥ', + 'Ꝧ' => 'ꝧ', + 'Ꝩ' => 'ꝩ', + 'Ꝫ' => 'ꝫ', + 'Ꝭ' => 'ꝭ', + 'Ꝯ' => 'ꝯ', + 'Ꝺ' => 'ꝺ', + 'Ꝼ' => 'ꝼ', + 'Ᵹ' => 'ᵹ', + 'Ꝿ' => 'ꝿ', + 'Ꞁ' => 'ꞁ', + 'Ꞃ' => 'ꞃ', + 'Ꞅ' => 'ꞅ', + 'Ꞇ' => 'ꞇ', + 'Ꞌ' => 'ꞌ', + 'Ɥ' => 'ɥ', + 'Ꞑ' => 'ꞑ', + 'Ꞓ' => 'ꞓ', + 'Ꞗ' => 'ꞗ', + 'Ꞙ' => 'ꞙ', + 'Ꞛ' => 'ꞛ', + 'Ꞝ' => 'ꞝ', + 'Ꞟ' => 'ꞟ', + 'Ꞡ' => 'ꞡ', + 'Ꞣ' => 'ꞣ', + 'Ꞥ' => 'ꞥ', + 'Ꞧ' => 'ꞧ', + 'Ꞩ' => 'ꞩ', + 'Ɦ' => 'ɦ', + 'Ɜ' => 'ɜ', + 'Ɡ' => 'ɡ', + 'Ɬ' => 'ɬ', + 'Ɪ' => 'ɪ', + 'Ʞ' => 'ʞ', + 'Ʇ' => 'ʇ', + 'Ʝ' => 'ʝ', + 'Ꭓ' => 'ꭓ', + 'Ꞵ' => 'ꞵ', + 'Ꞷ' => 'ꞷ', + 'Ꞹ' => 'ꞹ', + 'Ꞻ' => 'ꞻ', + 'Ꞽ' => 'ꞽ', + 'Ꞿ' => 'ꞿ', + 'Ꟃ' => 'ꟃ', + 'Ꞔ' => 'ꞔ', + 'Ʂ' => 'ʂ', + 'Ᶎ' => 'ᶎ', + 'Ꟈ' => 'ꟈ', + 'Ꟊ' => 'ꟊ', + 'Ꟶ' => 'ꟶ', + 'A' => 'a', + 'B' => 'b', + 'C' => 'c', + 'D' => 'd', + 'E' => 'e', + 'F' => 'f', + 'G' => 'g', + 'H' => 'h', + 'I' => 'i', + 'J' => 'j', + 'K' => 'k', + 'L' => 'l', + 'M' => 'm', + 'N' => 'n', + 'O' => 'o', + 'P' => 'p', + 'Q' => 'q', + 'R' => 'r', + 'S' => 's', + 'T' => 't', + 'U' => 'u', + 'V' => 'v', + 'W' => 'w', + 'X' => 'x', + 'Y' => 'y', + 'Z' => 'z', + '𐐀' => '𐐨', + '𐐁' => '𐐩', + '𐐂' => '𐐪', + '𐐃' => '𐐫', + '𐐄' => '𐐬', + '𐐅' => '𐐭', + '𐐆' => '𐐮', + '𐐇' => '𐐯', + '𐐈' => '𐐰', + '𐐉' => '𐐱', + '𐐊' => '𐐲', + '𐐋' => '𐐳', + '𐐌' => '𐐴', + '𐐍' => '𐐵', + '𐐎' => '𐐶', + '𐐏' => '𐐷', + '𐐐' => '𐐸', + '𐐑' => '𐐹', + '𐐒' => '𐐺', + '𐐓' => '𐐻', + '𐐔' => '𐐼', + '𐐕' => '𐐽', + '𐐖' => '𐐾', + '𐐗' => '𐐿', + '𐐘' => '𐑀', + '𐐙' => '𐑁', + '𐐚' => '𐑂', + '𐐛' => '𐑃', + '𐐜' => '𐑄', + '𐐝' => '𐑅', + '𐐞' => '𐑆', + '𐐟' => '𐑇', + '𐐠' => '𐑈', + '𐐡' => '𐑉', + '𐐢' => '𐑊', + '𐐣' => '𐑋', + '𐐤' => '𐑌', + '𐐥' => '𐑍', + '𐐦' => '𐑎', + '𐐧' => '𐑏', + '𐒰' => '𐓘', + '𐒱' => '𐓙', + '𐒲' => '𐓚', + '𐒳' => '𐓛', + '𐒴' => '𐓜', + '𐒵' => '𐓝', + '𐒶' => '𐓞', + '𐒷' => '𐓟', + '𐒸' => '𐓠', + '𐒹' => '𐓡', + '𐒺' => '𐓢', + '𐒻' => '𐓣', + '𐒼' => '𐓤', + '𐒽' => '𐓥', + '𐒾' => '𐓦', + '𐒿' => '𐓧', + '𐓀' => '𐓨', + '𐓁' => '𐓩', + '𐓂' => '𐓪', + '𐓃' => '𐓫', + '𐓄' => '𐓬', + '𐓅' => '𐓭', + '𐓆' => '𐓮', + '𐓇' => '𐓯', + '𐓈' => '𐓰', + '𐓉' => '𐓱', + '𐓊' => '𐓲', + '𐓋' => '𐓳', + '𐓌' => '𐓴', + '𐓍' => '𐓵', + '𐓎' => '𐓶', + '𐓏' => '𐓷', + '𐓐' => '𐓸', + '𐓑' => '𐓹', + '𐓒' => '𐓺', + '𐓓' => '𐓻', + '𐲀' => '𐳀', + '𐲁' => '𐳁', + '𐲂' => '𐳂', + '𐲃' => '𐳃', + '𐲄' => '𐳄', + '𐲅' => '𐳅', + '𐲆' => '𐳆', + '𐲇' => '𐳇', + '𐲈' => '𐳈', + '𐲉' => '𐳉', + '𐲊' => '𐳊', + '𐲋' => '𐳋', + '𐲌' => '𐳌', + '𐲍' => '𐳍', + '𐲎' => '𐳎', + '𐲏' => '𐳏', + '𐲐' => '𐳐', + '𐲑' => '𐳑', + '𐲒' => '𐳒', + '𐲓' => '𐳓', + '𐲔' => '𐳔', + '𐲕' => '𐳕', + '𐲖' => '𐳖', + '𐲗' => '𐳗', + '𐲘' => '𐳘', + '𐲙' => '𐳙', + '𐲚' => '𐳚', + '𐲛' => '𐳛', + '𐲜' => '𐳜', + '𐲝' => '𐳝', + '𐲞' => '𐳞', + '𐲟' => '𐳟', + '𐲠' => '𐳠', + '𐲡' => '𐳡', + '𐲢' => '𐳢', + '𐲣' => '𐳣', + '𐲤' => '𐳤', + '𐲥' => '𐳥', + '𐲦' => '𐳦', + '𐲧' => '𐳧', + '𐲨' => '𐳨', + '𐲩' => '𐳩', + '𐲪' => '𐳪', + '𐲫' => '𐳫', + '𐲬' => '𐳬', + '𐲭' => '𐳭', + '𐲮' => '𐳮', + '𐲯' => '𐳯', + '𐲰' => '𐳰', + '𐲱' => '𐳱', + '𐲲' => '𐳲', + '𑢠' => '𑣀', + '𑢡' => '𑣁', + '𑢢' => '𑣂', + '𑢣' => '𑣃', + '𑢤' => '𑣄', + '𑢥' => '𑣅', + '𑢦' => '𑣆', + '𑢧' => '𑣇', + '𑢨' => '𑣈', + '𑢩' => '𑣉', + '𑢪' => '𑣊', + '𑢫' => '𑣋', + '𑢬' => '𑣌', + '𑢭' => '𑣍', + '𑢮' => '𑣎', + '𑢯' => '𑣏', + '𑢰' => '𑣐', + '𑢱' => '𑣑', + '𑢲' => '𑣒', + '𑢳' => '𑣓', + '𑢴' => '𑣔', + '𑢵' => '𑣕', + '𑢶' => '𑣖', + '𑢷' => '𑣗', + '𑢸' => '𑣘', + '𑢹' => '𑣙', + '𑢺' => '𑣚', + '𑢻' => '𑣛', + '𑢼' => '𑣜', + '𑢽' => '𑣝', + '𑢾' => '𑣞', + '𑢿' => '𑣟', + '𖹀' => '𖹠', + '𖹁' => '𖹡', + '𖹂' => '𖹢', + '𖹃' => '𖹣', + '𖹄' => '𖹤', + '𖹅' => '𖹥', + '𖹆' => '𖹦', + '𖹇' => '𖹧', + '𖹈' => '𖹨', + '𖹉' => '𖹩', + '𖹊' => '𖹪', + '𖹋' => '𖹫', + '𖹌' => '𖹬', + '𖹍' => '𖹭', + '𖹎' => '𖹮', + '𖹏' => '𖹯', + '𖹐' => '𖹰', + '𖹑' => '𖹱', + '𖹒' => '𖹲', + '𖹓' => '𖹳', + '𖹔' => '𖹴', + '𖹕' => '𖹵', + '𖹖' => '𖹶', + '𖹗' => '𖹷', + '𖹘' => '𖹸', + '𖹙' => '𖹹', + '𖹚' => '𖹺', + '𖹛' => '𖹻', + '𖹜' => '𖹼', + '𖹝' => '𖹽', + '𖹞' => '𖹾', + '𖹟' => '𖹿', + '𞤀' => '𞤢', + '𞤁' => '𞤣', + '𞤂' => '𞤤', + '𞤃' => '𞤥', + '𞤄' => '𞤦', + '𞤅' => '𞤧', + '𞤆' => '𞤨', + '𞤇' => '𞤩', + '𞤈' => '𞤪', + '𞤉' => '𞤫', + '𞤊' => '𞤬', + '𞤋' => '𞤭', + '𞤌' => '𞤮', + '𞤍' => '𞤯', + '𞤎' => '𞤰', + '𞤏' => '𞤱', + '𞤐' => '𞤲', + '𞤑' => '𞤳', + '𞤒' => '𞤴', + '𞤓' => '𞤵', + '𞤔' => '𞤶', + '𞤕' => '𞤷', + '𞤖' => '𞤸', + '𞤗' => '𞤹', + '𞤘' => '𞤺', + '𞤙' => '𞤻', + '𞤚' => '𞤼', + '𞤛' => '𞤽', + '𞤜' => '𞤾', + '𞤝' => '𞤿', + '𞤞' => '𞥀', + '𞤟' => '𞥁', + '𞤠' => '𞥂', + '𞤡' => '𞥃', +); diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php new file mode 100644 index 0000000..2a8f6e7 --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php @@ -0,0 +1,5 @@ + 'A', + 'b' => 'B', + 'c' => 'C', + 'd' => 'D', + 'e' => 'E', + 'f' => 'F', + 'g' => 'G', + 'h' => 'H', + 'i' => 'I', + 'j' => 'J', + 'k' => 'K', + 'l' => 'L', + 'm' => 'M', + 'n' => 'N', + 'o' => 'O', + 'p' => 'P', + 'q' => 'Q', + 'r' => 'R', + 's' => 'S', + 't' => 'T', + 'u' => 'U', + 'v' => 'V', + 'w' => 'W', + 'x' => 'X', + 'y' => 'Y', + 'z' => 'Z', + 'µ' => 'Μ', + 'à' => 'À', + 'á' => 'Á', + 'â' => 'Â', + 'ã' => 'Ã', + 'ä' => 'Ä', + 'å' => 'Å', + 'æ' => 'Æ', + 'ç' => 'Ç', + 'è' => 'È', + 'é' => 'É', + 'ê' => 'Ê', + 'ë' => 'Ë', + 'ì' => 'Ì', + 'í' => 'Í', + 'î' => 'Î', + 'ï' => 'Ï', + 'ð' => 'Ð', + 'ñ' => 'Ñ', + 'ò' => 'Ò', + 'ó' => 'Ó', + 'ô' => 'Ô', + 'õ' => 'Õ', + 'ö' => 'Ö', + 'ø' => 'Ø', + 'ù' => 'Ù', + 'ú' => 'Ú', + 'û' => 'Û', + 'ü' => 'Ü', + 'ý' => 'Ý', + 'þ' => 'Þ', + 'ÿ' => 'Ÿ', + 'ā' => 'Ā', + 'ă' => 'Ă', + 'ą' => 'Ą', + 'ć' => 'Ć', + 'ĉ' => 'Ĉ', + 'ċ' => 'Ċ', + 'č' => 'Č', + 'ď' => 'Ď', + 'đ' => 'Đ', + 'ē' => 'Ē', + 'ĕ' => 'Ĕ', + 'ė' => 'Ė', + 'ę' => 'Ę', + 'ě' => 'Ě', + 'ĝ' => 'Ĝ', + 'ğ' => 'Ğ', + 'ġ' => 'Ġ', + 'ģ' => 'Ģ', + 'ĥ' => 'Ĥ', + 'ħ' => 'Ħ', + 'ĩ' => 'Ĩ', + 'ī' => 'Ī', + 'ĭ' => 'Ĭ', + 'į' => 'Į', + 'ı' => 'I', + 'ij' => 'IJ', + 'ĵ' => 'Ĵ', + 'ķ' => 'Ķ', + 'ĺ' => 'Ĺ', + 'ļ' => 'Ļ', + 'ľ' => 'Ľ', + 'ŀ' => 'Ŀ', + 'ł' => 'Ł', + 'ń' => 'Ń', + 'ņ' => 'Ņ', + 'ň' => 'Ň', + 'ŋ' => 'Ŋ', + 'ō' => 'Ō', + 'ŏ' => 'Ŏ', + 'ő' => 'Ő', + 'œ' => 'Œ', + 'ŕ' => 'Ŕ', + 'ŗ' => 'Ŗ', + 'ř' => 'Ř', + 'ś' => 'Ś', + 'ŝ' => 'Ŝ', + 'ş' => 'Ş', + 'š' => 'Š', + 'ţ' => 'Ţ', + 'ť' => 'Ť', + 'ŧ' => 'Ŧ', + 'ũ' => 'Ũ', + 'ū' => 'Ū', + 'ŭ' => 'Ŭ', + 'ů' => 'Ů', + 'ű' => 'Ű', + 'ų' => 'Ų', + 'ŵ' => 'Ŵ', + 'ŷ' => 'Ŷ', + 'ź' => 'Ź', + 'ż' => 'Ż', + 'ž' => 'Ž', + 'ſ' => 'S', + 'ƀ' => 'Ƀ', + 'ƃ' => 'Ƃ', + 'ƅ' => 'Ƅ', + 'ƈ' => 'Ƈ', + 'ƌ' => 'Ƌ', + 'ƒ' => 'Ƒ', + 'ƕ' => 'Ƕ', + 'ƙ' => 'Ƙ', + 'ƚ' => 'Ƚ', + 'ƞ' => 'Ƞ', + 'ơ' => 'Ơ', + 'ƣ' => 'Ƣ', + 'ƥ' => 'Ƥ', + 'ƨ' => 'Ƨ', + 'ƭ' => 'Ƭ', + 'ư' => 'Ư', + 'ƴ' => 'Ƴ', + 'ƶ' => 'Ƶ', + 'ƹ' => 'Ƹ', + 'ƽ' => 'Ƽ', + 'ƿ' => 'Ƿ', + 'Dž' => 'DŽ', + 'dž' => 'DŽ', + 'Lj' => 'LJ', + 'lj' => 'LJ', + 'Nj' => 'NJ', + 'nj' => 'NJ', + 'ǎ' => 'Ǎ', + 'ǐ' => 'Ǐ', + 'ǒ' => 'Ǒ', + 'ǔ' => 'Ǔ', + 'ǖ' => 'Ǖ', + 'ǘ' => 'Ǘ', + 'ǚ' => 'Ǚ', + 'ǜ' => 'Ǜ', + 'ǝ' => 'Ǝ', + 'ǟ' => 'Ǟ', + 'ǡ' => 'Ǡ', + 'ǣ' => 'Ǣ', + 'ǥ' => 'Ǥ', + 'ǧ' => 'Ǧ', + 'ǩ' => 'Ǩ', + 'ǫ' => 'Ǫ', + 'ǭ' => 'Ǭ', + 'ǯ' => 'Ǯ', + 'Dz' => 'DZ', + 'dz' => 'DZ', + 'ǵ' => 'Ǵ', + 'ǹ' => 'Ǹ', + 'ǻ' => 'Ǻ', + 'ǽ' => 'Ǽ', + 'ǿ' => 'Ǿ', + 'ȁ' => 'Ȁ', + 'ȃ' => 'Ȃ', + 'ȅ' => 'Ȅ', + 'ȇ' => 'Ȇ', + 'ȉ' => 'Ȉ', + 'ȋ' => 'Ȋ', + 'ȍ' => 'Ȍ', + 'ȏ' => 'Ȏ', + 'ȑ' => 'Ȑ', + 'ȓ' => 'Ȓ', + 'ȕ' => 'Ȕ', + 'ȗ' => 'Ȗ', + 'ș' => 'Ș', + 'ț' => 'Ț', + 'ȝ' => 'Ȝ', + 'ȟ' => 'Ȟ', + 'ȣ' => 'Ȣ', + 'ȥ' => 'Ȥ', + 'ȧ' => 'Ȧ', + 'ȩ' => 'Ȩ', + 'ȫ' => 'Ȫ', + 'ȭ' => 'Ȭ', + 'ȯ' => 'Ȯ', + 'ȱ' => 'Ȱ', + 'ȳ' => 'Ȳ', + 'ȼ' => 'Ȼ', + 'ȿ' => 'Ȿ', + 'ɀ' => 'Ɀ', + 'ɂ' => 'Ɂ', + 'ɇ' => 'Ɇ', + 'ɉ' => 'Ɉ', + 'ɋ' => 'Ɋ', + 'ɍ' => 'Ɍ', + 'ɏ' => 'Ɏ', + 'ɐ' => 'Ɐ', + 'ɑ' => 'Ɑ', + 'ɒ' => 'Ɒ', + 'ɓ' => 'Ɓ', + 'ɔ' => 'Ɔ', + 'ɖ' => 'Ɖ', + 'ɗ' => 'Ɗ', + 'ə' => 'Ə', + 'ɛ' => 'Ɛ', + 'ɜ' => 'Ɜ', + 'ɠ' => 'Ɠ', + 'ɡ' => 'Ɡ', + 'ɣ' => 'Ɣ', + 'ɥ' => 'Ɥ', + 'ɦ' => 'Ɦ', + 'ɨ' => 'Ɨ', + 'ɩ' => 'Ɩ', + 'ɪ' => 'Ɪ', + 'ɫ' => 'Ɫ', + 'ɬ' => 'Ɬ', + 'ɯ' => 'Ɯ', + 'ɱ' => 'Ɱ', + 'ɲ' => 'Ɲ', + 'ɵ' => 'Ɵ', + 'ɽ' => 'Ɽ', + 'ʀ' => 'Ʀ', + 'ʂ' => 'Ʂ', + 'ʃ' => 'Ʃ', + 'ʇ' => 'Ʇ', + 'ʈ' => 'Ʈ', + 'ʉ' => 'Ʉ', + 'ʊ' => 'Ʊ', + 'ʋ' => 'Ʋ', + 'ʌ' => 'Ʌ', + 'ʒ' => 'Ʒ', + 'ʝ' => 'Ʝ', + 'ʞ' => 'Ʞ', + 'ͅ' => 'Ι', + 'ͱ' => 'Ͱ', + 'ͳ' => 'Ͳ', + 'ͷ' => 'Ͷ', + 'ͻ' => 'Ͻ', + 'ͼ' => 'Ͼ', + 'ͽ' => 'Ͽ', + 'ά' => 'Ά', + 'έ' => 'Έ', + 'ή' => 'Ή', + 'ί' => 'Ί', + 'α' => 'Α', + 'β' => 'Β', + 'γ' => 'Γ', + 'δ' => 'Δ', + 'ε' => 'Ε', + 'ζ' => 'Ζ', + 'η' => 'Η', + 'θ' => 'Θ', + 'ι' => 'Ι', + 'κ' => 'Κ', + 'λ' => 'Λ', + 'μ' => 'Μ', + 'ν' => 'Ν', + 'ξ' => 'Ξ', + 'ο' => 'Ο', + 'π' => 'Π', + 'ρ' => 'Ρ', + 'ς' => 'Σ', + 'σ' => 'Σ', + 'τ' => 'Τ', + 'υ' => 'Υ', + 'φ' => 'Φ', + 'χ' => 'Χ', + 'ψ' => 'Ψ', + 'ω' => 'Ω', + 'ϊ' => 'Ϊ', + 'ϋ' => 'Ϋ', + 'ό' => 'Ό', + 'ύ' => 'Ύ', + 'ώ' => 'Ώ', + 'ϐ' => 'Β', + 'ϑ' => 'Θ', + 'ϕ' => 'Φ', + 'ϖ' => 'Π', + 'ϗ' => 'Ϗ', + 'ϙ' => 'Ϙ', + 'ϛ' => 'Ϛ', + 'ϝ' => 'Ϝ', + 'ϟ' => 'Ϟ', + 'ϡ' => 'Ϡ', + 'ϣ' => 'Ϣ', + 'ϥ' => 'Ϥ', + 'ϧ' => 'Ϧ', + 'ϩ' => 'Ϩ', + 'ϫ' => 'Ϫ', + 'ϭ' => 'Ϭ', + 'ϯ' => 'Ϯ', + 'ϰ' => 'Κ', + 'ϱ' => 'Ρ', + 'ϲ' => 'Ϲ', + 'ϳ' => 'Ϳ', + 'ϵ' => 'Ε', + 'ϸ' => 'Ϸ', + 'ϻ' => 'Ϻ', + 'а' => 'А', + 'б' => 'Б', + 'в' => 'В', + 'г' => 'Г', + 'д' => 'Д', + 'е' => 'Е', + 'ж' => 'Ж', + 'з' => 'З', + 'и' => 'И', + 'й' => 'Й', + 'к' => 'К', + 'л' => 'Л', + 'м' => 'М', + 'н' => 'Н', + 'о' => 'О', + 'п' => 'П', + 'р' => 'Р', + 'с' => 'С', + 'т' => 'Т', + 'у' => 'У', + 'ф' => 'Ф', + 'х' => 'Х', + 'ц' => 'Ц', + 'ч' => 'Ч', + 'ш' => 'Ш', + 'щ' => 'Щ', + 'ъ' => 'Ъ', + 'ы' => 'Ы', + 'ь' => 'Ь', + 'э' => 'Э', + 'ю' => 'Ю', + 'я' => 'Я', + 'ѐ' => 'Ѐ', + 'ё' => 'Ё', + 'ђ' => 'Ђ', + 'ѓ' => 'Ѓ', + 'є' => 'Є', + 'ѕ' => 'Ѕ', + 'і' => 'І', + 'ї' => 'Ї', + 'ј' => 'Ј', + 'љ' => 'Љ', + 'њ' => 'Њ', + 'ћ' => 'Ћ', + 'ќ' => 'Ќ', + 'ѝ' => 'Ѝ', + 'ў' => 'Ў', + 'џ' => 'Џ', + 'ѡ' => 'Ѡ', + 'ѣ' => 'Ѣ', + 'ѥ' => 'Ѥ', + 'ѧ' => 'Ѧ', + 'ѩ' => 'Ѩ', + 'ѫ' => 'Ѫ', + 'ѭ' => 'Ѭ', + 'ѯ' => 'Ѯ', + 'ѱ' => 'Ѱ', + 'ѳ' => 'Ѳ', + 'ѵ' => 'Ѵ', + 'ѷ' => 'Ѷ', + 'ѹ' => 'Ѹ', + 'ѻ' => 'Ѻ', + 'ѽ' => 'Ѽ', + 'ѿ' => 'Ѿ', + 'ҁ' => 'Ҁ', + 'ҋ' => 'Ҋ', + 'ҍ' => 'Ҍ', + 'ҏ' => 'Ҏ', + 'ґ' => 'Ґ', + 'ғ' => 'Ғ', + 'ҕ' => 'Ҕ', + 'җ' => 'Җ', + 'ҙ' => 'Ҙ', + 'қ' => 'Қ', + 'ҝ' => 'Ҝ', + 'ҟ' => 'Ҟ', + 'ҡ' => 'Ҡ', + 'ң' => 'Ң', + 'ҥ' => 'Ҥ', + 'ҧ' => 'Ҧ', + 'ҩ' => 'Ҩ', + 'ҫ' => 'Ҫ', + 'ҭ' => 'Ҭ', + 'ү' => 'Ү', + 'ұ' => 'Ұ', + 'ҳ' => 'Ҳ', + 'ҵ' => 'Ҵ', + 'ҷ' => 'Ҷ', + 'ҹ' => 'Ҹ', + 'һ' => 'Һ', + 'ҽ' => 'Ҽ', + 'ҿ' => 'Ҿ', + 'ӂ' => 'Ӂ', + 'ӄ' => 'Ӄ', + 'ӆ' => 'Ӆ', + 'ӈ' => 'Ӈ', + 'ӊ' => 'Ӊ', + 'ӌ' => 'Ӌ', + 'ӎ' => 'Ӎ', + 'ӏ' => 'Ӏ', + 'ӑ' => 'Ӑ', + 'ӓ' => 'Ӓ', + 'ӕ' => 'Ӕ', + 'ӗ' => 'Ӗ', + 'ә' => 'Ә', + 'ӛ' => 'Ӛ', + 'ӝ' => 'Ӝ', + 'ӟ' => 'Ӟ', + 'ӡ' => 'Ӡ', + 'ӣ' => 'Ӣ', + 'ӥ' => 'Ӥ', + 'ӧ' => 'Ӧ', + 'ө' => 'Ө', + 'ӫ' => 'Ӫ', + 'ӭ' => 'Ӭ', + 'ӯ' => 'Ӯ', + 'ӱ' => 'Ӱ', + 'ӳ' => 'Ӳ', + 'ӵ' => 'Ӵ', + 'ӷ' => 'Ӷ', + 'ӹ' => 'Ӹ', + 'ӻ' => 'Ӻ', + 'ӽ' => 'Ӽ', + 'ӿ' => 'Ӿ', + 'ԁ' => 'Ԁ', + 'ԃ' => 'Ԃ', + 'ԅ' => 'Ԅ', + 'ԇ' => 'Ԇ', + 'ԉ' => 'Ԉ', + 'ԋ' => 'Ԋ', + 'ԍ' => 'Ԍ', + 'ԏ' => 'Ԏ', + 'ԑ' => 'Ԑ', + 'ԓ' => 'Ԓ', + 'ԕ' => 'Ԕ', + 'ԗ' => 'Ԗ', + 'ԙ' => 'Ԙ', + 'ԛ' => 'Ԛ', + 'ԝ' => 'Ԝ', + 'ԟ' => 'Ԟ', + 'ԡ' => 'Ԡ', + 'ԣ' => 'Ԣ', + 'ԥ' => 'Ԥ', + 'ԧ' => 'Ԧ', + 'ԩ' => 'Ԩ', + 'ԫ' => 'Ԫ', + 'ԭ' => 'Ԭ', + 'ԯ' => 'Ԯ', + 'ա' => 'Ա', + 'բ' => 'Բ', + 'գ' => 'Գ', + 'դ' => 'Դ', + 'ե' => 'Ե', + 'զ' => 'Զ', + 'է' => 'Է', + 'ը' => 'Ը', + 'թ' => 'Թ', + 'ժ' => 'Ժ', + 'ի' => 'Ի', + 'լ' => 'Լ', + 'խ' => 'Խ', + 'ծ' => 'Ծ', + 'կ' => 'Կ', + 'հ' => 'Հ', + 'ձ' => 'Ձ', + 'ղ' => 'Ղ', + 'ճ' => 'Ճ', + 'մ' => 'Մ', + 'յ' => 'Յ', + 'ն' => 'Ն', + 'շ' => 'Շ', + 'ո' => 'Ո', + 'չ' => 'Չ', + 'պ' => 'Պ', + 'ջ' => 'Ջ', + 'ռ' => 'Ռ', + 'ս' => 'Ս', + 'վ' => 'Վ', + 'տ' => 'Տ', + 'ր' => 'Ր', + 'ց' => 'Ց', + 'ւ' => 'Ւ', + 'փ' => 'Փ', + 'ք' => 'Ք', + 'օ' => 'Օ', + 'ֆ' => 'Ֆ', + 'ა' => 'Ა', + 'ბ' => 'Ბ', + 'გ' => 'Გ', + 'დ' => 'Დ', + 'ე' => 'Ე', + 'ვ' => 'Ვ', + 'ზ' => 'Ზ', + 'თ' => 'Თ', + 'ი' => 'Ი', + 'კ' => 'Კ', + 'ლ' => 'Ლ', + 'მ' => 'Მ', + 'ნ' => 'Ნ', + 'ო' => 'Ო', + 'პ' => 'Პ', + 'ჟ' => 'Ჟ', + 'რ' => 'Რ', + 'ს' => 'Ს', + 'ტ' => 'Ტ', + 'უ' => 'Უ', + 'ფ' => 'Ფ', + 'ქ' => 'Ქ', + 'ღ' => 'Ღ', + 'ყ' => 'Ყ', + 'შ' => 'Შ', + 'ჩ' => 'Ჩ', + 'ც' => 'Ც', + 'ძ' => 'Ძ', + 'წ' => 'Წ', + 'ჭ' => 'Ჭ', + 'ხ' => 'Ხ', + 'ჯ' => 'Ჯ', + 'ჰ' => 'Ჰ', + 'ჱ' => 'Ჱ', + 'ჲ' => 'Ჲ', + 'ჳ' => 'Ჳ', + 'ჴ' => 'Ჴ', + 'ჵ' => 'Ჵ', + 'ჶ' => 'Ჶ', + 'ჷ' => 'Ჷ', + 'ჸ' => 'Ჸ', + 'ჹ' => 'Ჹ', + 'ჺ' => 'Ჺ', + 'ჽ' => 'Ჽ', + 'ჾ' => 'Ჾ', + 'ჿ' => 'Ჿ', + 'ᏸ' => 'Ᏸ', + 'ᏹ' => 'Ᏹ', + 'ᏺ' => 'Ᏺ', + 'ᏻ' => 'Ᏻ', + 'ᏼ' => 'Ᏼ', + 'ᏽ' => 'Ᏽ', + 'ᲀ' => 'В', + 'ᲁ' => 'Д', + 'ᲂ' => 'О', + 'ᲃ' => 'С', + 'ᲄ' => 'Т', + 'ᲅ' => 'Т', + 'ᲆ' => 'Ъ', + 'ᲇ' => 'Ѣ', + 'ᲈ' => 'Ꙋ', + 'ᵹ' => 'Ᵹ', + 'ᵽ' => 'Ᵽ', + 'ᶎ' => 'Ᶎ', + 'ḁ' => 'Ḁ', + 'ḃ' => 'Ḃ', + 'ḅ' => 'Ḅ', + 'ḇ' => 'Ḇ', + 'ḉ' => 'Ḉ', + 'ḋ' => 'Ḋ', + 'ḍ' => 'Ḍ', + 'ḏ' => 'Ḏ', + 'ḑ' => 'Ḑ', + 'ḓ' => 'Ḓ', + 'ḕ' => 'Ḕ', + 'ḗ' => 'Ḗ', + 'ḙ' => 'Ḙ', + 'ḛ' => 'Ḛ', + 'ḝ' => 'Ḝ', + 'ḟ' => 'Ḟ', + 'ḡ' => 'Ḡ', + 'ḣ' => 'Ḣ', + 'ḥ' => 'Ḥ', + 'ḧ' => 'Ḧ', + 'ḩ' => 'Ḩ', + 'ḫ' => 'Ḫ', + 'ḭ' => 'Ḭ', + 'ḯ' => 'Ḯ', + 'ḱ' => 'Ḱ', + 'ḳ' => 'Ḳ', + 'ḵ' => 'Ḵ', + 'ḷ' => 'Ḷ', + 'ḹ' => 'Ḹ', + 'ḻ' => 'Ḻ', + 'ḽ' => 'Ḽ', + 'ḿ' => 'Ḿ', + 'ṁ' => 'Ṁ', + 'ṃ' => 'Ṃ', + 'ṅ' => 'Ṅ', + 'ṇ' => 'Ṇ', + 'ṉ' => 'Ṉ', + 'ṋ' => 'Ṋ', + 'ṍ' => 'Ṍ', + 'ṏ' => 'Ṏ', + 'ṑ' => 'Ṑ', + 'ṓ' => 'Ṓ', + 'ṕ' => 'Ṕ', + 'ṗ' => 'Ṗ', + 'ṙ' => 'Ṙ', + 'ṛ' => 'Ṛ', + 'ṝ' => 'Ṝ', + 'ṟ' => 'Ṟ', + 'ṡ' => 'Ṡ', + 'ṣ' => 'Ṣ', + 'ṥ' => 'Ṥ', + 'ṧ' => 'Ṧ', + 'ṩ' => 'Ṩ', + 'ṫ' => 'Ṫ', + 'ṭ' => 'Ṭ', + 'ṯ' => 'Ṯ', + 'ṱ' => 'Ṱ', + 'ṳ' => 'Ṳ', + 'ṵ' => 'Ṵ', + 'ṷ' => 'Ṷ', + 'ṹ' => 'Ṹ', + 'ṻ' => 'Ṻ', + 'ṽ' => 'Ṽ', + 'ṿ' => 'Ṿ', + 'ẁ' => 'Ẁ', + 'ẃ' => 'Ẃ', + 'ẅ' => 'Ẅ', + 'ẇ' => 'Ẇ', + 'ẉ' => 'Ẉ', + 'ẋ' => 'Ẋ', + 'ẍ' => 'Ẍ', + 'ẏ' => 'Ẏ', + 'ẑ' => 'Ẑ', + 'ẓ' => 'Ẓ', + 'ẕ' => 'Ẕ', + 'ẛ' => 'Ṡ', + 'ạ' => 'Ạ', + 'ả' => 'Ả', + 'ấ' => 'Ấ', + 'ầ' => 'Ầ', + 'ẩ' => 'Ẩ', + 'ẫ' => 'Ẫ', + 'ậ' => 'Ậ', + 'ắ' => 'Ắ', + 'ằ' => 'Ằ', + 'ẳ' => 'Ẳ', + 'ẵ' => 'Ẵ', + 'ặ' => 'Ặ', + 'ẹ' => 'Ẹ', + 'ẻ' => 'Ẻ', + 'ẽ' => 'Ẽ', + 'ế' => 'Ế', + 'ề' => 'Ề', + 'ể' => 'Ể', + 'ễ' => 'Ễ', + 'ệ' => 'Ệ', + 'ỉ' => 'Ỉ', + 'ị' => 'Ị', + 'ọ' => 'Ọ', + 'ỏ' => 'Ỏ', + 'ố' => 'Ố', + 'ồ' => 'Ồ', + 'ổ' => 'Ổ', + 'ỗ' => 'Ỗ', + 'ộ' => 'Ộ', + 'ớ' => 'Ớ', + 'ờ' => 'Ờ', + 'ở' => 'Ở', + 'ỡ' => 'Ỡ', + 'ợ' => 'Ợ', + 'ụ' => 'Ụ', + 'ủ' => 'Ủ', + 'ứ' => 'Ứ', + 'ừ' => 'Ừ', + 'ử' => 'Ử', + 'ữ' => 'Ữ', + 'ự' => 'Ự', + 'ỳ' => 'Ỳ', + 'ỵ' => 'Ỵ', + 'ỷ' => 'Ỷ', + 'ỹ' => 'Ỹ', + 'ỻ' => 'Ỻ', + 'ỽ' => 'Ỽ', + 'ỿ' => 'Ỿ', + 'ἀ' => 'Ἀ', + 'ἁ' => 'Ἁ', + 'ἂ' => 'Ἂ', + 'ἃ' => 'Ἃ', + 'ἄ' => 'Ἄ', + 'ἅ' => 'Ἅ', + 'ἆ' => 'Ἆ', + 'ἇ' => 'Ἇ', + 'ἐ' => 'Ἐ', + 'ἑ' => 'Ἑ', + 'ἒ' => 'Ἒ', + 'ἓ' => 'Ἓ', + 'ἔ' => 'Ἔ', + 'ἕ' => 'Ἕ', + 'ἠ' => 'Ἠ', + 'ἡ' => 'Ἡ', + 'ἢ' => 'Ἢ', + 'ἣ' => 'Ἣ', + 'ἤ' => 'Ἤ', + 'ἥ' => 'Ἥ', + 'ἦ' => 'Ἦ', + 'ἧ' => 'Ἧ', + 'ἰ' => 'Ἰ', + 'ἱ' => 'Ἱ', + 'ἲ' => 'Ἲ', + 'ἳ' => 'Ἳ', + 'ἴ' => 'Ἴ', + 'ἵ' => 'Ἵ', + 'ἶ' => 'Ἶ', + 'ἷ' => 'Ἷ', + 'ὀ' => 'Ὀ', + 'ὁ' => 'Ὁ', + 'ὂ' => 'Ὂ', + 'ὃ' => 'Ὃ', + 'ὄ' => 'Ὄ', + 'ὅ' => 'Ὅ', + 'ὑ' => 'Ὑ', + 'ὓ' => 'Ὓ', + 'ὕ' => 'Ὕ', + 'ὗ' => 'Ὗ', + 'ὠ' => 'Ὠ', + 'ὡ' => 'Ὡ', + 'ὢ' => 'Ὢ', + 'ὣ' => 'Ὣ', + 'ὤ' => 'Ὤ', + 'ὥ' => 'Ὥ', + 'ὦ' => 'Ὦ', + 'ὧ' => 'Ὧ', + 'ὰ' => 'Ὰ', + 'ά' => 'Ά', + 'ὲ' => 'Ὲ', + 'έ' => 'Έ', + 'ὴ' => 'Ὴ', + 'ή' => 'Ή', + 'ὶ' => 'Ὶ', + 'ί' => 'Ί', + 'ὸ' => 'Ὸ', + 'ό' => 'Ό', + 'ὺ' => 'Ὺ', + 'ύ' => 'Ύ', + 'ὼ' => 'Ὼ', + 'ώ' => 'Ώ', + 'ᾀ' => 'ἈΙ', + 'ᾁ' => 'ἉΙ', + 'ᾂ' => 'ἊΙ', + 'ᾃ' => 'ἋΙ', + 'ᾄ' => 'ἌΙ', + 'ᾅ' => 'ἍΙ', + 'ᾆ' => 'ἎΙ', + 'ᾇ' => 'ἏΙ', + 'ᾐ' => 'ἨΙ', + 'ᾑ' => 'ἩΙ', + 'ᾒ' => 'ἪΙ', + 'ᾓ' => 'ἫΙ', + 'ᾔ' => 'ἬΙ', + 'ᾕ' => 'ἭΙ', + 'ᾖ' => 'ἮΙ', + 'ᾗ' => 'ἯΙ', + 'ᾠ' => 'ὨΙ', + 'ᾡ' => 'ὩΙ', + 'ᾢ' => 'ὪΙ', + 'ᾣ' => 'ὫΙ', + 'ᾤ' => 'ὬΙ', + 'ᾥ' => 'ὭΙ', + 'ᾦ' => 'ὮΙ', + 'ᾧ' => 'ὯΙ', + 'ᾰ' => 'Ᾰ', + 'ᾱ' => 'Ᾱ', + 'ᾳ' => 'ΑΙ', + 'ι' => 'Ι', + 'ῃ' => 'ΗΙ', + 'ῐ' => 'Ῐ', + 'ῑ' => 'Ῑ', + 'ῠ' => 'Ῠ', + 'ῡ' => 'Ῡ', + 'ῥ' => 'Ῥ', + 'ῳ' => 'ΩΙ', + 'ⅎ' => 'Ⅎ', + 'ⅰ' => 'Ⅰ', + 'ⅱ' => 'Ⅱ', + 'ⅲ' => 'Ⅲ', + 'ⅳ' => 'Ⅳ', + 'ⅴ' => 'Ⅴ', + 'ⅵ' => 'Ⅵ', + 'ⅶ' => 'Ⅶ', + 'ⅷ' => 'Ⅷ', + 'ⅸ' => 'Ⅸ', + 'ⅹ' => 'Ⅹ', + 'ⅺ' => 'Ⅺ', + 'ⅻ' => 'Ⅻ', + 'ⅼ' => 'Ⅼ', + 'ⅽ' => 'Ⅽ', + 'ⅾ' => 'Ⅾ', + 'ⅿ' => 'Ⅿ', + 'ↄ' => 'Ↄ', + 'ⓐ' => 'Ⓐ', + 'ⓑ' => 'Ⓑ', + 'ⓒ' => 'Ⓒ', + 'ⓓ' => 'Ⓓ', + 'ⓔ' => 'Ⓔ', + 'ⓕ' => 'Ⓕ', + 'ⓖ' => 'Ⓖ', + 'ⓗ' => 'Ⓗ', + 'ⓘ' => 'Ⓘ', + 'ⓙ' => 'Ⓙ', + 'ⓚ' => 'Ⓚ', + 'ⓛ' => 'Ⓛ', + 'ⓜ' => 'Ⓜ', + 'ⓝ' => 'Ⓝ', + 'ⓞ' => 'Ⓞ', + 'ⓟ' => 'Ⓟ', + 'ⓠ' => 'Ⓠ', + 'ⓡ' => 'Ⓡ', + 'ⓢ' => 'Ⓢ', + 'ⓣ' => 'Ⓣ', + 'ⓤ' => 'Ⓤ', + 'ⓥ' => 'Ⓥ', + 'ⓦ' => 'Ⓦ', + 'ⓧ' => 'Ⓧ', + 'ⓨ' => 'Ⓨ', + 'ⓩ' => 'Ⓩ', + 'ⰰ' => 'Ⰰ', + 'ⰱ' => 'Ⰱ', + 'ⰲ' => 'Ⰲ', + 'ⰳ' => 'Ⰳ', + 'ⰴ' => 'Ⰴ', + 'ⰵ' => 'Ⰵ', + 'ⰶ' => 'Ⰶ', + 'ⰷ' => 'Ⰷ', + 'ⰸ' => 'Ⰸ', + 'ⰹ' => 'Ⰹ', + 'ⰺ' => 'Ⰺ', + 'ⰻ' => 'Ⰻ', + 'ⰼ' => 'Ⰼ', + 'ⰽ' => 'Ⰽ', + 'ⰾ' => 'Ⰾ', + 'ⰿ' => 'Ⰿ', + 'ⱀ' => 'Ⱀ', + 'ⱁ' => 'Ⱁ', + 'ⱂ' => 'Ⱂ', + 'ⱃ' => 'Ⱃ', + 'ⱄ' => 'Ⱄ', + 'ⱅ' => 'Ⱅ', + 'ⱆ' => 'Ⱆ', + 'ⱇ' => 'Ⱇ', + 'ⱈ' => 'Ⱈ', + 'ⱉ' => 'Ⱉ', + 'ⱊ' => 'Ⱊ', + 'ⱋ' => 'Ⱋ', + 'ⱌ' => 'Ⱌ', + 'ⱍ' => 'Ⱍ', + 'ⱎ' => 'Ⱎ', + 'ⱏ' => 'Ⱏ', + 'ⱐ' => 'Ⱐ', + 'ⱑ' => 'Ⱑ', + 'ⱒ' => 'Ⱒ', + 'ⱓ' => 'Ⱓ', + 'ⱔ' => 'Ⱔ', + 'ⱕ' => 'Ⱕ', + 'ⱖ' => 'Ⱖ', + 'ⱗ' => 'Ⱗ', + 'ⱘ' => 'Ⱘ', + 'ⱙ' => 'Ⱙ', + 'ⱚ' => 'Ⱚ', + 'ⱛ' => 'Ⱛ', + 'ⱜ' => 'Ⱜ', + 'ⱝ' => 'Ⱝ', + 'ⱞ' => 'Ⱞ', + 'ⱡ' => 'Ⱡ', + 'ⱥ' => 'Ⱥ', + 'ⱦ' => 'Ⱦ', + 'ⱨ' => 'Ⱨ', + 'ⱪ' => 'Ⱪ', + 'ⱬ' => 'Ⱬ', + 'ⱳ' => 'Ⱳ', + 'ⱶ' => 'Ⱶ', + 'ⲁ' => 'Ⲁ', + 'ⲃ' => 'Ⲃ', + 'ⲅ' => 'Ⲅ', + 'ⲇ' => 'Ⲇ', + 'ⲉ' => 'Ⲉ', + 'ⲋ' => 'Ⲋ', + 'ⲍ' => 'Ⲍ', + 'ⲏ' => 'Ⲏ', + 'ⲑ' => 'Ⲑ', + 'ⲓ' => 'Ⲓ', + 'ⲕ' => 'Ⲕ', + 'ⲗ' => 'Ⲗ', + 'ⲙ' => 'Ⲙ', + 'ⲛ' => 'Ⲛ', + 'ⲝ' => 'Ⲝ', + 'ⲟ' => 'Ⲟ', + 'ⲡ' => 'Ⲡ', + 'ⲣ' => 'Ⲣ', + 'ⲥ' => 'Ⲥ', + 'ⲧ' => 'Ⲧ', + 'ⲩ' => 'Ⲩ', + 'ⲫ' => 'Ⲫ', + 'ⲭ' => 'Ⲭ', + 'ⲯ' => 'Ⲯ', + 'ⲱ' => 'Ⲱ', + 'ⲳ' => 'Ⲳ', + 'ⲵ' => 'Ⲵ', + 'ⲷ' => 'Ⲷ', + 'ⲹ' => 'Ⲹ', + 'ⲻ' => 'Ⲻ', + 'ⲽ' => 'Ⲽ', + 'ⲿ' => 'Ⲿ', + 'ⳁ' => 'Ⳁ', + 'ⳃ' => 'Ⳃ', + 'ⳅ' => 'Ⳅ', + 'ⳇ' => 'Ⳇ', + 'ⳉ' => 'Ⳉ', + 'ⳋ' => 'Ⳋ', + 'ⳍ' => 'Ⳍ', + 'ⳏ' => 'Ⳏ', + 'ⳑ' => 'Ⳑ', + 'ⳓ' => 'Ⳓ', + 'ⳕ' => 'Ⳕ', + 'ⳗ' => 'Ⳗ', + 'ⳙ' => 'Ⳙ', + 'ⳛ' => 'Ⳛ', + 'ⳝ' => 'Ⳝ', + 'ⳟ' => 'Ⳟ', + 'ⳡ' => 'Ⳡ', + 'ⳣ' => 'Ⳣ', + 'ⳬ' => 'Ⳬ', + 'ⳮ' => 'Ⳮ', + 'ⳳ' => 'Ⳳ', + 'ⴀ' => 'Ⴀ', + 'ⴁ' => 'Ⴁ', + 'ⴂ' => 'Ⴂ', + 'ⴃ' => 'Ⴃ', + 'ⴄ' => 'Ⴄ', + 'ⴅ' => 'Ⴅ', + 'ⴆ' => 'Ⴆ', + 'ⴇ' => 'Ⴇ', + 'ⴈ' => 'Ⴈ', + 'ⴉ' => 'Ⴉ', + 'ⴊ' => 'Ⴊ', + 'ⴋ' => 'Ⴋ', + 'ⴌ' => 'Ⴌ', + 'ⴍ' => 'Ⴍ', + 'ⴎ' => 'Ⴎ', + 'ⴏ' => 'Ⴏ', + 'ⴐ' => 'Ⴐ', + 'ⴑ' => 'Ⴑ', + 'ⴒ' => 'Ⴒ', + 'ⴓ' => 'Ⴓ', + 'ⴔ' => 'Ⴔ', + 'ⴕ' => 'Ⴕ', + 'ⴖ' => 'Ⴖ', + 'ⴗ' => 'Ⴗ', + 'ⴘ' => 'Ⴘ', + 'ⴙ' => 'Ⴙ', + 'ⴚ' => 'Ⴚ', + 'ⴛ' => 'Ⴛ', + 'ⴜ' => 'Ⴜ', + 'ⴝ' => 'Ⴝ', + 'ⴞ' => 'Ⴞ', + 'ⴟ' => 'Ⴟ', + 'ⴠ' => 'Ⴠ', + 'ⴡ' => 'Ⴡ', + 'ⴢ' => 'Ⴢ', + 'ⴣ' => 'Ⴣ', + 'ⴤ' => 'Ⴤ', + 'ⴥ' => 'Ⴥ', + 'ⴧ' => 'Ⴧ', + 'ⴭ' => 'Ⴭ', + 'ꙁ' => 'Ꙁ', + 'ꙃ' => 'Ꙃ', + 'ꙅ' => 'Ꙅ', + 'ꙇ' => 'Ꙇ', + 'ꙉ' => 'Ꙉ', + 'ꙋ' => 'Ꙋ', + 'ꙍ' => 'Ꙍ', + 'ꙏ' => 'Ꙏ', + 'ꙑ' => 'Ꙑ', + 'ꙓ' => 'Ꙓ', + 'ꙕ' => 'Ꙕ', + 'ꙗ' => 'Ꙗ', + 'ꙙ' => 'Ꙙ', + 'ꙛ' => 'Ꙛ', + 'ꙝ' => 'Ꙝ', + 'ꙟ' => 'Ꙟ', + 'ꙡ' => 'Ꙡ', + 'ꙣ' => 'Ꙣ', + 'ꙥ' => 'Ꙥ', + 'ꙧ' => 'Ꙧ', + 'ꙩ' => 'Ꙩ', + 'ꙫ' => 'Ꙫ', + 'ꙭ' => 'Ꙭ', + 'ꚁ' => 'Ꚁ', + 'ꚃ' => 'Ꚃ', + 'ꚅ' => 'Ꚅ', + 'ꚇ' => 'Ꚇ', + 'ꚉ' => 'Ꚉ', + 'ꚋ' => 'Ꚋ', + 'ꚍ' => 'Ꚍ', + 'ꚏ' => 'Ꚏ', + 'ꚑ' => 'Ꚑ', + 'ꚓ' => 'Ꚓ', + 'ꚕ' => 'Ꚕ', + 'ꚗ' => 'Ꚗ', + 'ꚙ' => 'Ꚙ', + 'ꚛ' => 'Ꚛ', + 'ꜣ' => 'Ꜣ', + 'ꜥ' => 'Ꜥ', + 'ꜧ' => 'Ꜧ', + 'ꜩ' => 'Ꜩ', + 'ꜫ' => 'Ꜫ', + 'ꜭ' => 'Ꜭ', + 'ꜯ' => 'Ꜯ', + 'ꜳ' => 'Ꜳ', + 'ꜵ' => 'Ꜵ', + 'ꜷ' => 'Ꜷ', + 'ꜹ' => 'Ꜹ', + 'ꜻ' => 'Ꜻ', + 'ꜽ' => 'Ꜽ', + 'ꜿ' => 'Ꜿ', + 'ꝁ' => 'Ꝁ', + 'ꝃ' => 'Ꝃ', + 'ꝅ' => 'Ꝅ', + 'ꝇ' => 'Ꝇ', + 'ꝉ' => 'Ꝉ', + 'ꝋ' => 'Ꝋ', + 'ꝍ' => 'Ꝍ', + 'ꝏ' => 'Ꝏ', + 'ꝑ' => 'Ꝑ', + 'ꝓ' => 'Ꝓ', + 'ꝕ' => 'Ꝕ', + 'ꝗ' => 'Ꝗ', + 'ꝙ' => 'Ꝙ', + 'ꝛ' => 'Ꝛ', + 'ꝝ' => 'Ꝝ', + 'ꝟ' => 'Ꝟ', + 'ꝡ' => 'Ꝡ', + 'ꝣ' => 'Ꝣ', + 'ꝥ' => 'Ꝥ', + 'ꝧ' => 'Ꝧ', + 'ꝩ' => 'Ꝩ', + 'ꝫ' => 'Ꝫ', + 'ꝭ' => 'Ꝭ', + 'ꝯ' => 'Ꝯ', + 'ꝺ' => 'Ꝺ', + 'ꝼ' => 'Ꝼ', + 'ꝿ' => 'Ꝿ', + 'ꞁ' => 'Ꞁ', + 'ꞃ' => 'Ꞃ', + 'ꞅ' => 'Ꞅ', + 'ꞇ' => 'Ꞇ', + 'ꞌ' => 'Ꞌ', + 'ꞑ' => 'Ꞑ', + 'ꞓ' => 'Ꞓ', + 'ꞔ' => 'Ꞔ', + 'ꞗ' => 'Ꞗ', + 'ꞙ' => 'Ꞙ', + 'ꞛ' => 'Ꞛ', + 'ꞝ' => 'Ꞝ', + 'ꞟ' => 'Ꞟ', + 'ꞡ' => 'Ꞡ', + 'ꞣ' => 'Ꞣ', + 'ꞥ' => 'Ꞥ', + 'ꞧ' => 'Ꞧ', + 'ꞩ' => 'Ꞩ', + 'ꞵ' => 'Ꞵ', + 'ꞷ' => 'Ꞷ', + 'ꞹ' => 'Ꞹ', + 'ꞻ' => 'Ꞻ', + 'ꞽ' => 'Ꞽ', + 'ꞿ' => 'Ꞿ', + 'ꟃ' => 'Ꟃ', + 'ꟈ' => 'Ꟈ', + 'ꟊ' => 'Ꟊ', + 'ꟶ' => 'Ꟶ', + 'ꭓ' => 'Ꭓ', + 'ꭰ' => 'Ꭰ', + 'ꭱ' => 'Ꭱ', + 'ꭲ' => 'Ꭲ', + 'ꭳ' => 'Ꭳ', + 'ꭴ' => 'Ꭴ', + 'ꭵ' => 'Ꭵ', + 'ꭶ' => 'Ꭶ', + 'ꭷ' => 'Ꭷ', + 'ꭸ' => 'Ꭸ', + 'ꭹ' => 'Ꭹ', + 'ꭺ' => 'Ꭺ', + 'ꭻ' => 'Ꭻ', + 'ꭼ' => 'Ꭼ', + 'ꭽ' => 'Ꭽ', + 'ꭾ' => 'Ꭾ', + 'ꭿ' => 'Ꭿ', + 'ꮀ' => 'Ꮀ', + 'ꮁ' => 'Ꮁ', + 'ꮂ' => 'Ꮂ', + 'ꮃ' => 'Ꮃ', + 'ꮄ' => 'Ꮄ', + 'ꮅ' => 'Ꮅ', + 'ꮆ' => 'Ꮆ', + 'ꮇ' => 'Ꮇ', + 'ꮈ' => 'Ꮈ', + 'ꮉ' => 'Ꮉ', + 'ꮊ' => 'Ꮊ', + 'ꮋ' => 'Ꮋ', + 'ꮌ' => 'Ꮌ', + 'ꮍ' => 'Ꮍ', + 'ꮎ' => 'Ꮎ', + 'ꮏ' => 'Ꮏ', + 'ꮐ' => 'Ꮐ', + 'ꮑ' => 'Ꮑ', + 'ꮒ' => 'Ꮒ', + 'ꮓ' => 'Ꮓ', + 'ꮔ' => 'Ꮔ', + 'ꮕ' => 'Ꮕ', + 'ꮖ' => 'Ꮖ', + 'ꮗ' => 'Ꮗ', + 'ꮘ' => 'Ꮘ', + 'ꮙ' => 'Ꮙ', + 'ꮚ' => 'Ꮚ', + 'ꮛ' => 'Ꮛ', + 'ꮜ' => 'Ꮜ', + 'ꮝ' => 'Ꮝ', + 'ꮞ' => 'Ꮞ', + 'ꮟ' => 'Ꮟ', + 'ꮠ' => 'Ꮠ', + 'ꮡ' => 'Ꮡ', + 'ꮢ' => 'Ꮢ', + 'ꮣ' => 'Ꮣ', + 'ꮤ' => 'Ꮤ', + 'ꮥ' => 'Ꮥ', + 'ꮦ' => 'Ꮦ', + 'ꮧ' => 'Ꮧ', + 'ꮨ' => 'Ꮨ', + 'ꮩ' => 'Ꮩ', + 'ꮪ' => 'Ꮪ', + 'ꮫ' => 'Ꮫ', + 'ꮬ' => 'Ꮬ', + 'ꮭ' => 'Ꮭ', + 'ꮮ' => 'Ꮮ', + 'ꮯ' => 'Ꮯ', + 'ꮰ' => 'Ꮰ', + 'ꮱ' => 'Ꮱ', + 'ꮲ' => 'Ꮲ', + 'ꮳ' => 'Ꮳ', + 'ꮴ' => 'Ꮴ', + 'ꮵ' => 'Ꮵ', + 'ꮶ' => 'Ꮶ', + 'ꮷ' => 'Ꮷ', + 'ꮸ' => 'Ꮸ', + 'ꮹ' => 'Ꮹ', + 'ꮺ' => 'Ꮺ', + 'ꮻ' => 'Ꮻ', + 'ꮼ' => 'Ꮼ', + 'ꮽ' => 'Ꮽ', + 'ꮾ' => 'Ꮾ', + 'ꮿ' => 'Ꮿ', + 'a' => 'A', + 'b' => 'B', + 'c' => 'C', + 'd' => 'D', + 'e' => 'E', + 'f' => 'F', + 'g' => 'G', + 'h' => 'H', + 'i' => 'I', + 'j' => 'J', + 'k' => 'K', + 'l' => 'L', + 'm' => 'M', + 'n' => 'N', + 'o' => 'O', + 'p' => 'P', + 'q' => 'Q', + 'r' => 'R', + 's' => 'S', + 't' => 'T', + 'u' => 'U', + 'v' => 'V', + 'w' => 'W', + 'x' => 'X', + 'y' => 'Y', + 'z' => 'Z', + '𐐨' => '𐐀', + '𐐩' => '𐐁', + '𐐪' => '𐐂', + '𐐫' => '𐐃', + '𐐬' => '𐐄', + '𐐭' => '𐐅', + '𐐮' => '𐐆', + '𐐯' => '𐐇', + '𐐰' => '𐐈', + '𐐱' => '𐐉', + '𐐲' => '𐐊', + '𐐳' => '𐐋', + '𐐴' => '𐐌', + '𐐵' => '𐐍', + '𐐶' => '𐐎', + '𐐷' => '𐐏', + '𐐸' => '𐐐', + '𐐹' => '𐐑', + '𐐺' => '𐐒', + '𐐻' => '𐐓', + '𐐼' => '𐐔', + '𐐽' => '𐐕', + '𐐾' => '𐐖', + '𐐿' => '𐐗', + '𐑀' => '𐐘', + '𐑁' => '𐐙', + '𐑂' => '𐐚', + '𐑃' => '𐐛', + '𐑄' => '𐐜', + '𐑅' => '𐐝', + '𐑆' => '𐐞', + '𐑇' => '𐐟', + '𐑈' => '𐐠', + '𐑉' => '𐐡', + '𐑊' => '𐐢', + '𐑋' => '𐐣', + '𐑌' => '𐐤', + '𐑍' => '𐐥', + '𐑎' => '𐐦', + '𐑏' => '𐐧', + '𐓘' => '𐒰', + '𐓙' => '𐒱', + '𐓚' => '𐒲', + '𐓛' => '𐒳', + '𐓜' => '𐒴', + '𐓝' => '𐒵', + '𐓞' => '𐒶', + '𐓟' => '𐒷', + '𐓠' => '𐒸', + '𐓡' => '𐒹', + '𐓢' => '𐒺', + '𐓣' => '𐒻', + '𐓤' => '𐒼', + '𐓥' => '𐒽', + '𐓦' => '𐒾', + '𐓧' => '𐒿', + '𐓨' => '𐓀', + '𐓩' => '𐓁', + '𐓪' => '𐓂', + '𐓫' => '𐓃', + '𐓬' => '𐓄', + '𐓭' => '𐓅', + '𐓮' => '𐓆', + '𐓯' => '𐓇', + '𐓰' => '𐓈', + '𐓱' => '𐓉', + '𐓲' => '𐓊', + '𐓳' => '𐓋', + '𐓴' => '𐓌', + '𐓵' => '𐓍', + '𐓶' => '𐓎', + '𐓷' => '𐓏', + '𐓸' => '𐓐', + '𐓹' => '𐓑', + '𐓺' => '𐓒', + '𐓻' => '𐓓', + '𐳀' => '𐲀', + '𐳁' => '𐲁', + '𐳂' => '𐲂', + '𐳃' => '𐲃', + '𐳄' => '𐲄', + '𐳅' => '𐲅', + '𐳆' => '𐲆', + '𐳇' => '𐲇', + '𐳈' => '𐲈', + '𐳉' => '𐲉', + '𐳊' => '𐲊', + '𐳋' => '𐲋', + '𐳌' => '𐲌', + '𐳍' => '𐲍', + '𐳎' => '𐲎', + '𐳏' => '𐲏', + '𐳐' => '𐲐', + '𐳑' => '𐲑', + '𐳒' => '𐲒', + '𐳓' => '𐲓', + '𐳔' => '𐲔', + '𐳕' => '𐲕', + '𐳖' => '𐲖', + '𐳗' => '𐲗', + '𐳘' => '𐲘', + '𐳙' => '𐲙', + '𐳚' => '𐲚', + '𐳛' => '𐲛', + '𐳜' => '𐲜', + '𐳝' => '𐲝', + '𐳞' => '𐲞', + '𐳟' => '𐲟', + '𐳠' => '𐲠', + '𐳡' => '𐲡', + '𐳢' => '𐲢', + '𐳣' => '𐲣', + '𐳤' => '𐲤', + '𐳥' => '𐲥', + '𐳦' => '𐲦', + '𐳧' => '𐲧', + '𐳨' => '𐲨', + '𐳩' => '𐲩', + '𐳪' => '𐲪', + '𐳫' => '𐲫', + '𐳬' => '𐲬', + '𐳭' => '𐲭', + '𐳮' => '𐲮', + '𐳯' => '𐲯', + '𐳰' => '𐲰', + '𐳱' => '𐲱', + '𐳲' => '𐲲', + '𑣀' => '𑢠', + '𑣁' => '𑢡', + '𑣂' => '𑢢', + '𑣃' => '𑢣', + '𑣄' => '𑢤', + '𑣅' => '𑢥', + '𑣆' => '𑢦', + '𑣇' => '𑢧', + '𑣈' => '𑢨', + '𑣉' => '𑢩', + '𑣊' => '𑢪', + '𑣋' => '𑢫', + '𑣌' => '𑢬', + '𑣍' => '𑢭', + '𑣎' => '𑢮', + '𑣏' => '𑢯', + '𑣐' => '𑢰', + '𑣑' => '𑢱', + '𑣒' => '𑢲', + '𑣓' => '𑢳', + '𑣔' => '𑢴', + '𑣕' => '𑢵', + '𑣖' => '𑢶', + '𑣗' => '𑢷', + '𑣘' => '𑢸', + '𑣙' => '𑢹', + '𑣚' => '𑢺', + '𑣛' => '𑢻', + '𑣜' => '𑢼', + '𑣝' => '𑢽', + '𑣞' => '𑢾', + '𑣟' => '𑢿', + '𖹠' => '𖹀', + '𖹡' => '𖹁', + '𖹢' => '𖹂', + '𖹣' => '𖹃', + '𖹤' => '𖹄', + '𖹥' => '𖹅', + '𖹦' => '𖹆', + '𖹧' => '𖹇', + '𖹨' => '𖹈', + '𖹩' => '𖹉', + '𖹪' => '𖹊', + '𖹫' => '𖹋', + '𖹬' => '𖹌', + '𖹭' => '𖹍', + '𖹮' => '𖹎', + '𖹯' => '𖹏', + '𖹰' => '𖹐', + '𖹱' => '𖹑', + '𖹲' => '𖹒', + '𖹳' => '𖹓', + '𖹴' => '𖹔', + '𖹵' => '𖹕', + '𖹶' => '𖹖', + '𖹷' => '𖹗', + '𖹸' => '𖹘', + '𖹹' => '𖹙', + '𖹺' => '𖹚', + '𖹻' => '𖹛', + '𖹼' => '𖹜', + '𖹽' => '𖹝', + '𖹾' => '𖹞', + '𖹿' => '𖹟', + '𞤢' => '𞤀', + '𞤣' => '𞤁', + '𞤤' => '𞤂', + '𞤥' => '𞤃', + '𞤦' => '𞤄', + '𞤧' => '𞤅', + '𞤨' => '𞤆', + '𞤩' => '𞤇', + '𞤪' => '𞤈', + '𞤫' => '𞤉', + '𞤬' => '𞤊', + '𞤭' => '𞤋', + '𞤮' => '𞤌', + '𞤯' => '𞤍', + '𞤰' => '𞤎', + '𞤱' => '𞤏', + '𞤲' => '𞤐', + '𞤳' => '𞤑', + '𞤴' => '𞤒', + '𞤵' => '𞤓', + '𞤶' => '𞤔', + '𞤷' => '𞤕', + '𞤸' => '𞤖', + '𞤹' => '𞤗', + '𞤺' => '𞤘', + '𞤻' => '𞤙', + '𞤼' => '𞤚', + '𞤽' => '𞤛', + '𞤾' => '𞤜', + '𞤿' => '𞤝', + '𞥀' => '𞤞', + '𞥁' => '𞤟', + '𞥂' => '𞤠', + '𞥃' => '𞤡', + 'ß' => 'SS', + 'ff' => 'FF', + 'fi' => 'FI', + 'fl' => 'FL', + 'ffi' => 'FFI', + 'ffl' => 'FFL', + 'ſt' => 'ST', + 'st' => 'ST', + 'և' => 'ԵՒ', + 'ﬓ' => 'ՄՆ', + 'ﬔ' => 'ՄԵ', + 'ﬕ' => 'ՄԻ', + 'ﬖ' => 'ՎՆ', + 'ﬗ' => 'ՄԽ', + 'ʼn' => 'ʼN', + 'ΐ' => 'Ϊ́', + 'ΰ' => 'Ϋ́', + 'ǰ' => 'J̌', + 'ẖ' => 'H̱', + 'ẗ' => 'T̈', + 'ẘ' => 'W̊', + 'ẙ' => 'Y̊', + 'ẚ' => 'Aʾ', + 'ὐ' => 'Υ̓', + 'ὒ' => 'Υ̓̀', + 'ὔ' => 'Υ̓́', + 'ὖ' => 'Υ̓͂', + 'ᾶ' => 'Α͂', + 'ῆ' => 'Η͂', + 'ῒ' => 'Ϊ̀', + 'ΐ' => 'Ϊ́', + 'ῖ' => 'Ι͂', + 'ῗ' => 'Ϊ͂', + 'ῢ' => 'Ϋ̀', + 'ΰ' => 'Ϋ́', + 'ῤ' => 'Ρ̓', + 'ῦ' => 'Υ͂', + 'ῧ' => 'Ϋ͂', + 'ῶ' => 'Ω͂', + 'ᾈ' => 'ἈΙ', + 'ᾉ' => 'ἉΙ', + 'ᾊ' => 'ἊΙ', + 'ᾋ' => 'ἋΙ', + 'ᾌ' => 'ἌΙ', + 'ᾍ' => 'ἍΙ', + 'ᾎ' => 'ἎΙ', + 'ᾏ' => 'ἏΙ', + 'ᾘ' => 'ἨΙ', + 'ᾙ' => 'ἩΙ', + 'ᾚ' => 'ἪΙ', + 'ᾛ' => 'ἫΙ', + 'ᾜ' => 'ἬΙ', + 'ᾝ' => 'ἭΙ', + 'ᾞ' => 'ἮΙ', + 'ᾟ' => 'ἯΙ', + 'ᾨ' => 'ὨΙ', + 'ᾩ' => 'ὩΙ', + 'ᾪ' => 'ὪΙ', + 'ᾫ' => 'ὫΙ', + 'ᾬ' => 'ὬΙ', + 'ᾭ' => 'ὭΙ', + 'ᾮ' => 'ὮΙ', + 'ᾯ' => 'ὯΙ', + 'ᾼ' => 'ΑΙ', + 'ῌ' => 'ΗΙ', + 'ῼ' => 'ΩΙ', + 'ᾲ' => 'ᾺΙ', + 'ᾴ' => 'ΆΙ', + 'ῂ' => 'ῊΙ', + 'ῄ' => 'ΉΙ', + 'ῲ' => 'ῺΙ', + 'ῴ' => 'ΏΙ', + 'ᾷ' => 'Α͂Ι', + 'ῇ' => 'Η͂Ι', + 'ῷ' => 'Ω͂Ι', +); diff --git a/vendor/symfony/polyfill-mbstring/bootstrap.php b/vendor/symfony/polyfill-mbstring/bootstrap.php new file mode 100644 index 0000000..ecf1a03 --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/bootstrap.php @@ -0,0 +1,151 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Mbstring as p; + +if (\PHP_VERSION_ID >= 80000) { + return require __DIR__.'/bootstrap80.php'; +} + +if (!function_exists('mb_convert_encoding')) { + function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); } +} +if (!function_exists('mb_decode_mimeheader')) { + function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); } +} +if (!function_exists('mb_encode_mimeheader')) { + function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = "\r\n", $indent = 0) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); } +} +if (!function_exists('mb_decode_numericentity')) { + function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); } +} +if (!function_exists('mb_encode_numericentity')) { + function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); } +} +if (!function_exists('mb_convert_case')) { + function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); } +} +if (!function_exists('mb_internal_encoding')) { + function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); } +} +if (!function_exists('mb_language')) { + function mb_language($language = null) { return p\Mbstring::mb_language($language); } +} +if (!function_exists('mb_list_encodings')) { + function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); } +} +if (!function_exists('mb_encoding_aliases')) { + function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); } +} +if (!function_exists('mb_check_encoding')) { + function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); } +} +if (!function_exists('mb_detect_encoding')) { + function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); } +} +if (!function_exists('mb_detect_order')) { + function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); } +} +if (!function_exists('mb_parse_str')) { + function mb_parse_str($string, &$result = []) { parse_str($string, $result); return (bool) $result; } +} +if (!function_exists('mb_strlen')) { + function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); } +} +if (!function_exists('mb_strpos')) { + function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); } +} +if (!function_exists('mb_strtolower')) { + function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); } +} +if (!function_exists('mb_strtoupper')) { + function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); } +} +if (!function_exists('mb_substitute_character')) { + function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); } +} +if (!function_exists('mb_substr')) { + function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); } +} +if (!function_exists('mb_stripos')) { + function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); } +} +if (!function_exists('mb_stristr')) { + function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); } +} +if (!function_exists('mb_strrchr')) { + function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); } +} +if (!function_exists('mb_strrichr')) { + function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); } +} +if (!function_exists('mb_strripos')) { + function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); } +} +if (!function_exists('mb_strrpos')) { + function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); } +} +if (!function_exists('mb_strstr')) { + function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); } +} +if (!function_exists('mb_get_info')) { + function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); } +} +if (!function_exists('mb_http_output')) { + function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); } +} +if (!function_exists('mb_strwidth')) { + function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); } +} +if (!function_exists('mb_substr_count')) { + function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); } +} +if (!function_exists('mb_output_handler')) { + function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); } +} +if (!function_exists('mb_http_input')) { + function mb_http_input($type = null) { return p\Mbstring::mb_http_input($type); } +} + +if (!function_exists('mb_convert_variables')) { + function mb_convert_variables($to_encoding, $from_encoding, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, ...$vars); } +} + +if (!function_exists('mb_ord')) { + function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); } +} +if (!function_exists('mb_chr')) { + function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); } +} +if (!function_exists('mb_scrub')) { + function mb_scrub($string, $encoding = null) { $encoding = null === $encoding ? mb_internal_encoding() : $encoding; return mb_convert_encoding($string, $encoding, $encoding); } +} +if (!function_exists('mb_str_split')) { + function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); } +} + +if (!function_exists('mb_str_pad')) { + function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); } +} + +if (extension_loaded('mbstring')) { + return; +} + +if (!defined('MB_CASE_UPPER')) { + define('MB_CASE_UPPER', 0); +} +if (!defined('MB_CASE_LOWER')) { + define('MB_CASE_LOWER', 1); +} +if (!defined('MB_CASE_TITLE')) { + define('MB_CASE_TITLE', 2); +} diff --git a/vendor/symfony/polyfill-mbstring/bootstrap80.php b/vendor/symfony/polyfill-mbstring/bootstrap80.php new file mode 100644 index 0000000..2f9fb5b --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/bootstrap80.php @@ -0,0 +1,147 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Mbstring as p; + +if (!function_exists('mb_convert_encoding')) { + function mb_convert_encoding(array|string|null $string, ?string $to_encoding, array|string|null $from_encoding = null): array|string|false { return p\Mbstring::mb_convert_encoding($string ?? '', (string) $to_encoding, $from_encoding); } +} +if (!function_exists('mb_decode_mimeheader')) { + function mb_decode_mimeheader(?string $string): string { return p\Mbstring::mb_decode_mimeheader((string) $string); } +} +if (!function_exists('mb_encode_mimeheader')) { + function mb_encode_mimeheader(?string $string, ?string $charset = null, ?string $transfer_encoding = null, ?string $newline = "\r\n", ?int $indent = 0): string { return p\Mbstring::mb_encode_mimeheader((string) $string, $charset, $transfer_encoding, (string) $newline, (int) $indent); } +} +if (!function_exists('mb_decode_numericentity')) { + function mb_decode_numericentity(?string $string, array $map, ?string $encoding = null): string { return p\Mbstring::mb_decode_numericentity((string) $string, $map, $encoding); } +} +if (!function_exists('mb_encode_numericentity')) { + function mb_encode_numericentity(?string $string, array $map, ?string $encoding = null, ?bool $hex = false): string { return p\Mbstring::mb_encode_numericentity((string) $string, $map, $encoding, (bool) $hex); } +} +if (!function_exists('mb_convert_case')) { + function mb_convert_case(?string $string, ?int $mode, ?string $encoding = null): string { return p\Mbstring::mb_convert_case((string) $string, (int) $mode, $encoding); } +} +if (!function_exists('mb_internal_encoding')) { + function mb_internal_encoding(?string $encoding = null): string|bool { return p\Mbstring::mb_internal_encoding($encoding); } +} +if (!function_exists('mb_language')) { + function mb_language(?string $language = null): string|bool { return p\Mbstring::mb_language($language); } +} +if (!function_exists('mb_list_encodings')) { + function mb_list_encodings(): array { return p\Mbstring::mb_list_encodings(); } +} +if (!function_exists('mb_encoding_aliases')) { + function mb_encoding_aliases(?string $encoding): array { return p\Mbstring::mb_encoding_aliases((string) $encoding); } +} +if (!function_exists('mb_check_encoding')) { + function mb_check_encoding(array|string|null $value = null, ?string $encoding = null): bool { return p\Mbstring::mb_check_encoding($value, $encoding); } +} +if (!function_exists('mb_detect_encoding')) { + function mb_detect_encoding(?string $string, array|string|null $encodings = null, ?bool $strict = false): string|false { return p\Mbstring::mb_detect_encoding((string) $string, $encodings, (bool) $strict); } +} +if (!function_exists('mb_detect_order')) { + function mb_detect_order(array|string|null $encoding = null): array|bool { return p\Mbstring::mb_detect_order($encoding); } +} +if (!function_exists('mb_parse_str')) { + function mb_parse_str(?string $string, &$result = []): bool { parse_str((string) $string, $result); return (bool) $result; } +} +if (!function_exists('mb_strlen')) { + function mb_strlen(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strlen((string) $string, $encoding); } +} +if (!function_exists('mb_strpos')) { + function mb_strpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strpos((string) $haystack, (string) $needle, (int) $offset, $encoding); } +} +if (!function_exists('mb_strtolower')) { + function mb_strtolower(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtolower((string) $string, $encoding); } +} +if (!function_exists('mb_strtoupper')) { + function mb_strtoupper(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtoupper((string) $string, $encoding); } +} +if (!function_exists('mb_substitute_character')) { + function mb_substitute_character(string|int|null $substitute_character = null): string|int|bool { return p\Mbstring::mb_substitute_character($substitute_character); } +} +if (!function_exists('mb_substr')) { + function mb_substr(?string $string, ?int $start, ?int $length = null, ?string $encoding = null): string { return p\Mbstring::mb_substr((string) $string, (int) $start, $length, $encoding); } +} +if (!function_exists('mb_stripos')) { + function mb_stripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_stripos((string) $haystack, (string) $needle, (int) $offset, $encoding); } +} +if (!function_exists('mb_stristr')) { + function mb_stristr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_stristr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } +} +if (!function_exists('mb_strrchr')) { + function mb_strrchr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrchr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } +} +if (!function_exists('mb_strrichr')) { + function mb_strrichr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrichr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } +} +if (!function_exists('mb_strripos')) { + function mb_strripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strripos((string) $haystack, (string) $needle, (int) $offset, $encoding); } +} +if (!function_exists('mb_strrpos')) { + function mb_strrpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strrpos((string) $haystack, (string) $needle, (int) $offset, $encoding); } +} +if (!function_exists('mb_strstr')) { + function mb_strstr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strstr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } +} +if (!function_exists('mb_get_info')) { + function mb_get_info(?string $type = 'all'): array|string|int|false { return p\Mbstring::mb_get_info((string) $type); } +} +if (!function_exists('mb_http_output')) { + function mb_http_output(?string $encoding = null): string|bool { return p\Mbstring::mb_http_output($encoding); } +} +if (!function_exists('mb_strwidth')) { + function mb_strwidth(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strwidth((string) $string, $encoding); } +} +if (!function_exists('mb_substr_count')) { + function mb_substr_count(?string $haystack, ?string $needle, ?string $encoding = null): int { return p\Mbstring::mb_substr_count((string) $haystack, (string) $needle, $encoding); } +} +if (!function_exists('mb_output_handler')) { + function mb_output_handler(?string $string, ?int $status): string { return p\Mbstring::mb_output_handler((string) $string, (int) $status); } +} +if (!function_exists('mb_http_input')) { + function mb_http_input(?string $type = null): array|string|false { return p\Mbstring::mb_http_input($type); } +} + +if (!function_exists('mb_convert_variables')) { + function mb_convert_variables(?string $to_encoding, array|string|null $from_encoding, mixed &$var, mixed &...$vars): string|false { return p\Mbstring::mb_convert_variables((string) $to_encoding, $from_encoding ?? '', $var, ...$vars); } +} + +if (!function_exists('mb_ord')) { + function mb_ord(?string $string, ?string $encoding = null): int|false { return p\Mbstring::mb_ord((string) $string, $encoding); } +} +if (!function_exists('mb_chr')) { + function mb_chr(?int $codepoint, ?string $encoding = null): string|false { return p\Mbstring::mb_chr((int) $codepoint, $encoding); } +} +if (!function_exists('mb_scrub')) { + function mb_scrub(?string $string, ?string $encoding = null): string { $encoding ??= mb_internal_encoding(); return mb_convert_encoding((string) $string, $encoding, $encoding); } +} +if (!function_exists('mb_str_split')) { + function mb_str_split(?string $string, ?int $length = 1, ?string $encoding = null): array { return p\Mbstring::mb_str_split((string) $string, (int) $length, $encoding); } +} + +if (!function_exists('mb_str_pad')) { + function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); } +} + +if (extension_loaded('mbstring')) { + return; +} + +if (!defined('MB_CASE_UPPER')) { + define('MB_CASE_UPPER', 0); +} +if (!defined('MB_CASE_LOWER')) { + define('MB_CASE_LOWER', 1); +} +if (!defined('MB_CASE_TITLE')) { + define('MB_CASE_TITLE', 2); +} diff --git a/vendor/symfony/polyfill-mbstring/composer.json b/vendor/symfony/polyfill-mbstring/composer.json new file mode 100644 index 0000000..bd99d4b --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/composer.json @@ -0,0 +1,38 @@ +{ + "name": "symfony/polyfill-mbstring", + "type": "library", + "description": "Symfony polyfill for the Mbstring extension", + "keywords": ["polyfill", "shim", "compatibility", "portable", "mbstring"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Mbstring\\": "" }, + "files": [ "bootstrap.php" ] + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "minimum-stability": "dev", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/vendor/symfony/polyfill-php73/LICENSE b/vendor/symfony/polyfill-php73/LICENSE new file mode 100644 index 0000000..7536cae --- /dev/null +++ b/vendor/symfony/polyfill-php73/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/polyfill-php73/Php73.php b/vendor/symfony/polyfill-php73/Php73.php new file mode 100644 index 0000000..65c35a6 --- /dev/null +++ b/vendor/symfony/polyfill-php73/Php73.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php73; + +/** + * @author Gabriel Caruso + * @author Ion Bazan + * + * @internal + */ +final class Php73 +{ + public static $startAt = 1533462603; + + /** + * @param bool $asNum + * + * @return array|float|int + */ + public static function hrtime($asNum = false) + { + $ns = microtime(false); + $s = substr($ns, 11) - self::$startAt; + $ns = 1E9 * (float) $ns; + + if ($asNum) { + $ns += $s * 1E9; + + return \PHP_INT_SIZE === 4 ? $ns : (int) $ns; + } + + return [$s, (int) $ns]; + } +} diff --git a/vendor/symfony/polyfill-php73/README.md b/vendor/symfony/polyfill-php73/README.md new file mode 100644 index 0000000..032fafb --- /dev/null +++ b/vendor/symfony/polyfill-php73/README.md @@ -0,0 +1,18 @@ +Symfony Polyfill / Php73 +======================== + +This component provides functions added to PHP 7.3 core: + +- [`array_key_first`](https://php.net/array_key_first) +- [`array_key_last`](https://php.net/array_key_last) +- [`hrtime`](https://php.net/function.hrtime) +- [`is_countable`](https://php.net/is_countable) +- [`JsonException`](https://php.net/JsonException) + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/vendor/symfony/polyfill-php73/Resources/stubs/JsonException.php b/vendor/symfony/polyfill-php73/Resources/stubs/JsonException.php new file mode 100644 index 0000000..f06d6c2 --- /dev/null +++ b/vendor/symfony/polyfill-php73/Resources/stubs/JsonException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 70300) { + class JsonException extends Exception + { + } +} diff --git a/vendor/symfony/polyfill-php73/bootstrap.php b/vendor/symfony/polyfill-php73/bootstrap.php new file mode 100644 index 0000000..d6b2153 --- /dev/null +++ b/vendor/symfony/polyfill-php73/bootstrap.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php73 as p; + +if (\PHP_VERSION_ID >= 70300) { + return; +} + +if (!function_exists('is_countable')) { + function is_countable($value) { return is_array($value) || $value instanceof Countable || $value instanceof ResourceBundle || $value instanceof SimpleXmlElement; } +} +if (!function_exists('hrtime')) { + require_once __DIR__.'/Php73.php'; + p\Php73::$startAt = (int) microtime(true); + function hrtime($as_number = false) { return p\Php73::hrtime($as_number); } +} +if (!function_exists('array_key_first')) { + function array_key_first(array $array) { foreach ($array as $key => $value) { return $key; } } +} +if (!function_exists('array_key_last')) { + function array_key_last(array $array) { return key(array_slice($array, -1, 1, true)); } +} diff --git a/vendor/symfony/polyfill-php73/composer.json b/vendor/symfony/polyfill-php73/composer.json new file mode 100644 index 0000000..09d98cb --- /dev/null +++ b/vendor/symfony/polyfill-php73/composer.json @@ -0,0 +1,33 @@ +{ + "name": "symfony/polyfill-php73", + "type": "library", + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "keywords": ["polyfill", "shim", "compatibility", "portable"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Php73\\": "" }, + "files": [ "bootstrap.php" ], + "classmap": [ "Resources/stubs" ] + }, + "minimum-stability": "dev", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/vendor/symfony/polyfill-php80/LICENSE b/vendor/symfony/polyfill-php80/LICENSE new file mode 100644 index 0000000..0ed3a24 --- /dev/null +++ b/vendor/symfony/polyfill-php80/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/polyfill-php80/Php80.php b/vendor/symfony/polyfill-php80/Php80.php new file mode 100644 index 0000000..362dd1a --- /dev/null +++ b/vendor/symfony/polyfill-php80/Php80.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php80; + +/** + * @author Ion Bazan + * @author Nico Oelgart + * @author Nicolas Grekas + * + * @internal + */ +final class Php80 +{ + public static function fdiv(float $dividend, float $divisor): float + { + return @($dividend / $divisor); + } + + public static function get_debug_type($value): string + { + switch (true) { + case null === $value: return 'null'; + case \is_bool($value): return 'bool'; + case \is_string($value): return 'string'; + case \is_array($value): return 'array'; + case \is_int($value): return 'int'; + case \is_float($value): return 'float'; + case \is_object($value): break; + case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class'; + default: + if (null === $type = @get_resource_type($value)) { + return 'unknown'; + } + + if ('Unknown' === $type) { + $type = 'closed'; + } + + return "resource ($type)"; + } + + $class = \get_class($value); + + if (false === strpos($class, '@')) { + return $class; + } + + return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous'; + } + + public static function get_resource_id($res): int + { + if (!\is_resource($res) && null === @get_resource_type($res)) { + throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res))); + } + + return (int) $res; + } + + public static function preg_last_error_msg(): string + { + switch (preg_last_error()) { + case \PREG_INTERNAL_ERROR: + return 'Internal error'; + case \PREG_BAD_UTF8_ERROR: + return 'Malformed UTF-8 characters, possibly incorrectly encoded'; + case \PREG_BAD_UTF8_OFFSET_ERROR: + return 'The offset did not correspond to the beginning of a valid UTF-8 code point'; + case \PREG_BACKTRACK_LIMIT_ERROR: + return 'Backtrack limit exhausted'; + case \PREG_RECURSION_LIMIT_ERROR: + return 'Recursion limit exhausted'; + case \PREG_JIT_STACKLIMIT_ERROR: + return 'JIT stack limit exhausted'; + case \PREG_NO_ERROR: + return 'No error'; + default: + return 'Unknown error'; + } + } + + public static function str_contains(string $haystack, string $needle): bool + { + return '' === $needle || false !== strpos($haystack, $needle); + } + + public static function str_starts_with(string $haystack, string $needle): bool + { + return 0 === strncmp($haystack, $needle, \strlen($needle)); + } + + public static function str_ends_with(string $haystack, string $needle): bool + { + if ('' === $needle || $needle === $haystack) { + return true; + } + + if ('' === $haystack) { + return false; + } + + $needleLength = \strlen($needle); + + return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength); + } +} diff --git a/vendor/symfony/polyfill-php80/PhpToken.php b/vendor/symfony/polyfill-php80/PhpToken.php new file mode 100644 index 0000000..fe6e691 --- /dev/null +++ b/vendor/symfony/polyfill-php80/PhpToken.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php80; + +/** + * @author Fedonyuk Anton + * + * @internal + */ +class PhpToken implements \Stringable +{ + /** + * @var int + */ + public $id; + + /** + * @var string + */ + public $text; + + /** + * @var int + */ + public $line; + + /** + * @var int + */ + public $pos; + + public function __construct(int $id, string $text, int $line = -1, int $position = -1) + { + $this->id = $id; + $this->text = $text; + $this->line = $line; + $this->pos = $position; + } + + public function getTokenName(): ?string + { + if ('UNKNOWN' === $name = token_name($this->id)) { + $name = \strlen($this->text) > 1 || \ord($this->text) < 32 ? null : $this->text; + } + + return $name; + } + + /** + * @param int|string|array $kind + */ + public function is($kind): bool + { + foreach ((array) $kind as $value) { + if (\in_array($value, [$this->id, $this->text], true)) { + return true; + } + } + + return false; + } + + public function isIgnorable(): bool + { + return \in_array($this->id, [\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT, \T_OPEN_TAG], true); + } + + public function __toString(): string + { + return (string) $this->text; + } + + /** + * @return static[] + */ + public static function tokenize(string $code, int $flags = 0): array + { + $line = 1; + $position = 0; + $tokens = token_get_all($code, $flags); + foreach ($tokens as $index => $token) { + if (\is_string($token)) { + $id = \ord($token); + $text = $token; + } else { + [$id, $text, $line] = $token; + } + $tokens[$index] = new static($id, $text, $line, $position); + $position += \strlen($text); + } + + return $tokens; + } +} diff --git a/vendor/symfony/polyfill-php80/README.md b/vendor/symfony/polyfill-php80/README.md new file mode 100644 index 0000000..3816c55 --- /dev/null +++ b/vendor/symfony/polyfill-php80/README.md @@ -0,0 +1,25 @@ +Symfony Polyfill / Php80 +======================== + +This component provides features added to PHP 8.0 core: + +- [`Stringable`](https://php.net/stringable) interface +- [`fdiv`](https://php.net/fdiv) +- [`ValueError`](https://php.net/valueerror) class +- [`UnhandledMatchError`](https://php.net/unhandledmatcherror) class +- `FILTER_VALIDATE_BOOL` constant +- [`get_debug_type`](https://php.net/get_debug_type) +- [`PhpToken`](https://php.net/phptoken) class +- [`preg_last_error_msg`](https://php.net/preg_last_error_msg) +- [`str_contains`](https://php.net/str_contains) +- [`str_starts_with`](https://php.net/str_starts_with) +- [`str_ends_with`](https://php.net/str_ends_with) +- [`get_resource_id`](https://php.net/get_resource_id) + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php b/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php new file mode 100644 index 0000000..2b95542 --- /dev/null +++ b/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#[Attribute(Attribute::TARGET_CLASS)] +final class Attribute +{ + public const TARGET_CLASS = 1; + public const TARGET_FUNCTION = 2; + public const TARGET_METHOD = 4; + public const TARGET_PROPERTY = 8; + public const TARGET_CLASS_CONSTANT = 16; + public const TARGET_PARAMETER = 32; + public const TARGET_ALL = 63; + public const IS_REPEATABLE = 64; + + /** @var int */ + public $flags; + + public function __construct(int $flags = self::TARGET_ALL) + { + $this->flags = $flags; + } +} diff --git a/vendor/symfony/polyfill-php80/Resources/stubs/PhpToken.php b/vendor/symfony/polyfill-php80/Resources/stubs/PhpToken.php new file mode 100644 index 0000000..bd1212f --- /dev/null +++ b/vendor/symfony/polyfill-php80/Resources/stubs/PhpToken.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80000 && extension_loaded('tokenizer')) { + class PhpToken extends Symfony\Polyfill\Php80\PhpToken + { + } +} diff --git a/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php b/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php new file mode 100644 index 0000000..7c62d75 --- /dev/null +++ b/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80000) { + interface Stringable + { + /** + * @return string + */ + public function __toString(); + } +} diff --git a/vendor/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php b/vendor/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php new file mode 100644 index 0000000..01c6c6c --- /dev/null +++ b/vendor/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80000) { + class UnhandledMatchError extends Error + { + } +} diff --git a/vendor/symfony/polyfill-php80/Resources/stubs/ValueError.php b/vendor/symfony/polyfill-php80/Resources/stubs/ValueError.php new file mode 100644 index 0000000..783dbc2 --- /dev/null +++ b/vendor/symfony/polyfill-php80/Resources/stubs/ValueError.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80000) { + class ValueError extends Error + { + } +} diff --git a/vendor/symfony/polyfill-php80/bootstrap.php b/vendor/symfony/polyfill-php80/bootstrap.php new file mode 100644 index 0000000..e5f7dbc --- /dev/null +++ b/vendor/symfony/polyfill-php80/bootstrap.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php80 as p; + +if (\PHP_VERSION_ID >= 80000) { + return; +} + +if (!defined('FILTER_VALIDATE_BOOL') && defined('FILTER_VALIDATE_BOOLEAN')) { + define('FILTER_VALIDATE_BOOL', \FILTER_VALIDATE_BOOLEAN); +} + +if (!function_exists('fdiv')) { + function fdiv(float $num1, float $num2): float { return p\Php80::fdiv($num1, $num2); } +} +if (!function_exists('preg_last_error_msg')) { + function preg_last_error_msg(): string { return p\Php80::preg_last_error_msg(); } +} +if (!function_exists('str_contains')) { + function str_contains(?string $haystack, ?string $needle): bool { return p\Php80::str_contains($haystack ?? '', $needle ?? ''); } +} +if (!function_exists('str_starts_with')) { + function str_starts_with(?string $haystack, ?string $needle): bool { return p\Php80::str_starts_with($haystack ?? '', $needle ?? ''); } +} +if (!function_exists('str_ends_with')) { + function str_ends_with(?string $haystack, ?string $needle): bool { return p\Php80::str_ends_with($haystack ?? '', $needle ?? ''); } +} +if (!function_exists('get_debug_type')) { + function get_debug_type($value): string { return p\Php80::get_debug_type($value); } +} +if (!function_exists('get_resource_id')) { + function get_resource_id($resource): int { return p\Php80::get_resource_id($resource); } +} diff --git a/vendor/symfony/polyfill-php80/composer.json b/vendor/symfony/polyfill-php80/composer.json new file mode 100644 index 0000000..a503b03 --- /dev/null +++ b/vendor/symfony/polyfill-php80/composer.json @@ -0,0 +1,37 @@ +{ + "name": "symfony/polyfill-php80", + "type": "library", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "keywords": ["polyfill", "shim", "compatibility", "portable"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Php80\\": "" }, + "files": [ "bootstrap.php" ], + "classmap": [ "Resources/stubs" ] + }, + "minimum-stability": "dev", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/vendor/symfony/psr-http-message-bridge/.php-cs-fixer.dist.php b/vendor/symfony/psr-http-message-bridge/.php-cs-fixer.dist.php new file mode 100644 index 0000000..e9b256a --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/.php-cs-fixer.dist.php @@ -0,0 +1,25 @@ +setRules([ + '@Symfony' => true, + '@Symfony:risky' => true, + '@PHPUnit48Migration:risky' => true, + 'php_unit_no_expectation_annotation' => false, // part of `PHPUnitXYMigration:risky` ruleset, to be enabled when PHPUnit 4.x support will be dropped, as we don't want to rewrite exceptions handling twice + 'array_syntax' => ['syntax' => 'short'], + 'fopen_flags' => false, + 'ordered_imports' => true, + 'protected_to_private' => false, + // Part of @Symfony:risky in PHP-CS-Fixer 2.13.0. To be removed from the config file once upgrading + 'native_function_invocation' => ['include' => ['@compiler_optimized'], 'scope' => 'namespaced'], + // Part of future @Symfony ruleset in PHP-CS-Fixer To be removed from the config file once upgrading + 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], + ]) + ->setRiskyAllowed(true) + ->setFinder( + (new PhpCsFixer\Finder()) + ->in(__DIR__) + ->exclude('vendor') + ->name('*.php') + ) +; diff --git a/vendor/symfony/psr-http-message-bridge/ArgumentValueResolver/PsrServerRequestResolver.php b/vendor/symfony/psr-http-message-bridge/ArgumentValueResolver/PsrServerRequestResolver.php new file mode 100644 index 0000000..61cd8c5 --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/ArgumentValueResolver/PsrServerRequestResolver.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PsrHttpMessage\ArgumentValueResolver; + +use Psr\Http\Message\MessageInterface; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; +use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface as BaseValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; + +/** + * Injects the RequestInterface, MessageInterface or ServerRequestInterface when requested. + * + * @author Iltar van der Berg + * @author Alexander M. Turek + */ +final class PsrServerRequestResolver implements ArgumentValueResolverInterface, ValueResolverInterface +{ + private const SUPPORTED_TYPES = [ + ServerRequestInterface::class => true, + RequestInterface::class => true, + MessageInterface::class => true, + ]; + + private $httpMessageFactory; + + public function __construct(HttpMessageFactoryInterface $httpMessageFactory) + { + $this->httpMessageFactory = $httpMessageFactory; + } + + /** + * {@inheritdoc} + */ + public function supports(Request $request, ArgumentMetadata $argument): bool + { + if ($this instanceof BaseValueResolverInterface) { + trigger_deprecation('symfony/psr-http-message-bridge', '2.3', 'Method "%s" is deprecated, call "resolve()" without calling "supports()" first.', __METHOD__); + } + + return self::SUPPORTED_TYPES[$argument->getType()] ?? false; + } + + /** + * {@inheritdoc} + */ + public function resolve(Request $request, ArgumentMetadata $argument): \Traversable + { + if (!isset(self::SUPPORTED_TYPES[$argument->getType()])) { + return; + } + + yield $this->httpMessageFactory->createRequest($request); + } +} diff --git a/vendor/symfony/psr-http-message-bridge/ArgumentValueResolver/ValueResolverInterface.php b/vendor/symfony/psr-http-message-bridge/ArgumentValueResolver/ValueResolverInterface.php new file mode 100644 index 0000000..83a321a --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/ArgumentValueResolver/ValueResolverInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PsrHttpMessage\ArgumentValueResolver; + +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface as BaseValueResolverInterface; + +if (interface_exists(BaseValueResolverInterface::class)) { + /** @internal */ + interface ValueResolverInterface extends BaseValueResolverInterface + { + } +} else { + /** @internal */ + interface ValueResolverInterface + { + } +} diff --git a/vendor/symfony/psr-http-message-bridge/CHANGELOG.md b/vendor/symfony/psr-http-message-bridge/CHANGELOG.md new file mode 100644 index 0000000..f32c06f --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/CHANGELOG.md @@ -0,0 +1,85 @@ +CHANGELOG +========= + +# 2.3.1 (2023-07-26) + +* Don't rely on `Request::getPayload()` to populate the parsed body + +# 2.3.0 (2023-07-25) + +* Leverage `Request::getPayload()` to populate the parsed body of PSR-7 requests +* Implement `ValueResolverInterface` introduced with Symfony 6.2 + +# 2.2.0 (2023-04-21) + +* Drop support for Symfony 4 +* Bump minimum version of PHP to 7.2 +* Support version 2 of the psr/http-message contracts + +# 2.1.3 (2022-09-05) + +* Ignore invalid HTTP headers when creating PSR7 objects +* Fix for wrong type passed to `moveTo()` + +# 2.1.2 (2021-11-05) + +* Allow Symfony 6 + +# 2.1.0 (2021-02-17) + + * Added a `PsrResponseListener` to automatically convert PSR-7 responses returned by controllers + * Added a `PsrServerRequestResolver` that allows injecting PSR-7 request objects into controllers + +# 2.0.2 (2020-09-29) + + * Fix populating server params from URI in HttpFoundationFactory + * Create cookies as raw in HttpFoundationFactory + * Fix BinaryFileResponse with Content-Range PsrHttpFactory + +# 2.0.1 (2020-06-25) + + * Don't normalize query string in PsrHttpFactory + * Fix conversion for HTTPS requests + * Fix populating default port and headers in HttpFoundationFactory + +# 2.0.0 (2020-01-02) + + * Remove DiactorosFactory + +# 1.3.0 (2019-11-25) + + * Added support for streamed requests + * Added support for Symfony 5.0+ + * Fixed bridging UploadedFile objects + * Bumped minimum version of Symfony to 4.4 + +# 1.2.0 (2019-03-11) + + * Added new documentation links + * Bumped minimum version of PHP to 7.1 + * Added support for streamed responses + +# 1.1.2 (2019-04-03) + + * Fixed createResponse + +# 1.1.1 (2019-03-11) + + * Deprecated DiactorosFactory, use PsrHttpFactory instead + * Removed triggering of deprecation + +# 1.1.0 (2018-08-30) + + * Added support for creating PSR-7 messages using PSR-17 factories + +# 1.0.2 (2017-12-19) + + * Fixed request target in PSR7 Request (mtibben) + +# 1.0.1 (2017-12-04) + + * Added support for Symfony 4 (dunglas) + +# 1.0.0 (2016-09-14) + + * Initial release diff --git a/vendor/symfony/psr-http-message-bridge/EventListener/PsrResponseListener.php b/vendor/symfony/psr-http-message-bridge/EventListener/PsrResponseListener.php new file mode 100644 index 0000000..ee0e047 --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/EventListener/PsrResponseListener.php @@ -0,0 +1,50 @@ + + * @author Alexander M. Turek + */ +final class PsrResponseListener implements EventSubscriberInterface +{ + private $httpFoundationFactory; + + public function __construct(HttpFoundationFactoryInterface $httpFoundationFactory = null) + { + $this->httpFoundationFactory = $httpFoundationFactory ?? new HttpFoundationFactory(); + } + + /** + * Do the conversion if applicable and update the response of the event. + */ + public function onKernelView(ViewEvent $event): void + { + $controllerResult = $event->getControllerResult(); + + if (!$controllerResult instanceof ResponseInterface) { + return; + } + + $event->setResponse($this->httpFoundationFactory->createResponse($controllerResult)); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::VIEW => 'onKernelView', + ]; + } +} diff --git a/vendor/symfony/psr-http-message-bridge/Factory/HttpFoundationFactory.php b/vendor/symfony/psr-http-message-bridge/Factory/HttpFoundationFactory.php new file mode 100644 index 0000000..a69e8ff --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/Factory/HttpFoundationFactory.php @@ -0,0 +1,252 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PsrHttpMessage\Factory; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UploadedFileInterface; +use Psr\Http\Message\UriInterface; +use Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface; +use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\StreamedResponse; + +/** + * {@inheritdoc} + * + * @author Kévin Dunglas + */ +class HttpFoundationFactory implements HttpFoundationFactoryInterface +{ + /** + * @var int The maximum output buffering size for each iteration when sending the response + */ + private $responseBufferMaxLength; + + public function __construct(int $responseBufferMaxLength = 16372) + { + $this->responseBufferMaxLength = $responseBufferMaxLength; + } + + /** + * {@inheritdoc} + * + * @return Request + */ + public function createRequest(ServerRequestInterface $psrRequest, bool $streamed = false) + { + $server = []; + $uri = $psrRequest->getUri(); + + if ($uri instanceof UriInterface) { + $server['SERVER_NAME'] = $uri->getHost(); + $server['SERVER_PORT'] = $uri->getPort() ?: ('https' === $uri->getScheme() ? 443 : 80); + $server['REQUEST_URI'] = $uri->getPath(); + $server['QUERY_STRING'] = $uri->getQuery(); + + if ('' !== $server['QUERY_STRING']) { + $server['REQUEST_URI'] .= '?'.$server['QUERY_STRING']; + } + + if ('https' === $uri->getScheme()) { + $server['HTTPS'] = 'on'; + } + } + + $server['REQUEST_METHOD'] = $psrRequest->getMethod(); + + $server = array_replace($psrRequest->getServerParams(), $server); + + $parsedBody = $psrRequest->getParsedBody(); + $parsedBody = \is_array($parsedBody) ? $parsedBody : []; + + $request = new Request( + $psrRequest->getQueryParams(), + $parsedBody, + $psrRequest->getAttributes(), + $psrRequest->getCookieParams(), + $this->getFiles($psrRequest->getUploadedFiles()), + $server, + $streamed ? $psrRequest->getBody()->detach() : $psrRequest->getBody()->__toString() + ); + $request->headers->add($psrRequest->getHeaders()); + + return $request; + } + + /** + * Converts to the input array to $_FILES structure. + */ + private function getFiles(array $uploadedFiles): array + { + $files = []; + + foreach ($uploadedFiles as $key => $value) { + if ($value instanceof UploadedFileInterface) { + $files[$key] = $this->createUploadedFile($value); + } else { + $files[$key] = $this->getFiles($value); + } + } + + return $files; + } + + /** + * Creates Symfony UploadedFile instance from PSR-7 ones. + */ + private function createUploadedFile(UploadedFileInterface $psrUploadedFile): UploadedFile + { + return new UploadedFile($psrUploadedFile, function () { return $this->getTemporaryPath(); }); + } + + /** + * Gets a temporary file path. + * + * @return string + */ + protected function getTemporaryPath() + { + return tempnam(sys_get_temp_dir(), uniqid('symfony', true)); + } + + /** + * {@inheritdoc} + * + * @return Response + */ + public function createResponse(ResponseInterface $psrResponse, bool $streamed = false) + { + $cookies = $psrResponse->getHeader('Set-Cookie'); + $psrResponse = $psrResponse->withoutHeader('Set-Cookie'); + + if ($streamed) { + $response = new StreamedResponse( + $this->createStreamedResponseCallback($psrResponse->getBody()), + $psrResponse->getStatusCode(), + $psrResponse->getHeaders() + ); + } else { + $response = new Response( + $psrResponse->getBody()->__toString(), + $psrResponse->getStatusCode(), + $psrResponse->getHeaders() + ); + } + + $response->setProtocolVersion($psrResponse->getProtocolVersion()); + + foreach ($cookies as $cookie) { + $response->headers->setCookie($this->createCookie($cookie)); + } + + return $response; + } + + /** + * Creates a Cookie instance from a cookie string. + * + * Some snippets have been taken from the Guzzle project: https://github.com/guzzle/guzzle/blob/5.3/src/Cookie/SetCookie.php#L34 + * + * @throws \InvalidArgumentException + */ + private function createCookie(string $cookie): Cookie + { + foreach (explode(';', $cookie) as $part) { + $part = trim($part); + + $data = explode('=', $part, 2); + $name = $data[0]; + $value = isset($data[1]) ? trim($data[1], " \n\r\t\0\x0B\"") : null; + + if (!isset($cookieName)) { + $cookieName = $name; + $cookieValue = $value; + + continue; + } + + if ('expires' === strtolower($name) && null !== $value) { + $cookieExpire = new \DateTime($value); + + continue; + } + + if ('path' === strtolower($name) && null !== $value) { + $cookiePath = $value; + + continue; + } + + if ('domain' === strtolower($name) && null !== $value) { + $cookieDomain = $value; + + continue; + } + + if ('secure' === strtolower($name)) { + $cookieSecure = true; + + continue; + } + + if ('httponly' === strtolower($name)) { + $cookieHttpOnly = true; + + continue; + } + + if ('samesite' === strtolower($name) && null !== $value) { + $samesite = $value; + + continue; + } + } + + if (!isset($cookieName)) { + throw new \InvalidArgumentException('The value of the Set-Cookie header is malformed.'); + } + + return new Cookie( + $cookieName, + $cookieValue, + $cookieExpire ?? 0, + $cookiePath ?? '/', + $cookieDomain ?? null, + isset($cookieSecure), + isset($cookieHttpOnly), + true, + $samesite ?? null + ); + } + + private function createStreamedResponseCallback(StreamInterface $body): callable + { + return function () use ($body) { + if ($body->isSeekable()) { + $body->rewind(); + } + + if (!$body->isReadable()) { + echo $body; + + return; + } + + while (!$body->eof()) { + echo $body->read($this->responseBufferMaxLength); + } + }; + } +} diff --git a/vendor/symfony/psr-http-message-bridge/Factory/PsrHttpFactory.php b/vendor/symfony/psr-http-message-bridge/Factory/PsrHttpFactory.php new file mode 100644 index 0000000..09c4360 --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/Factory/PsrHttpFactory.php @@ -0,0 +1,198 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PsrHttpMessage\Factory; + +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestFactoryInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\UploadedFileFactoryInterface; +use Psr\Http\Message\UploadedFileInterface; +use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface; +use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\StreamedResponse; + +/** + * Builds Psr\HttpMessage instances using a PSR-17 implementation. + * + * @author Antonio J. García Lagar + * @author Aurélien Pillevesse + */ +class PsrHttpFactory implements HttpMessageFactoryInterface +{ + private $serverRequestFactory; + private $streamFactory; + private $uploadedFileFactory; + private $responseFactory; + + public function __construct(ServerRequestFactoryInterface $serverRequestFactory, StreamFactoryInterface $streamFactory, UploadedFileFactoryInterface $uploadedFileFactory, ResponseFactoryInterface $responseFactory) + { + $this->serverRequestFactory = $serverRequestFactory; + $this->streamFactory = $streamFactory; + $this->uploadedFileFactory = $uploadedFileFactory; + $this->responseFactory = $responseFactory; + } + + /** + * {@inheritdoc} + * + * @return ServerRequestInterface + */ + public function createRequest(Request $symfonyRequest) + { + $uri = $symfonyRequest->server->get('QUERY_STRING', ''); + $uri = $symfonyRequest->getSchemeAndHttpHost().$symfonyRequest->getBaseUrl().$symfonyRequest->getPathInfo().('' !== $uri ? '?'.$uri : ''); + + $request = $this->serverRequestFactory->createServerRequest( + $symfonyRequest->getMethod(), + $uri, + $symfonyRequest->server->all() + ); + + foreach ($symfonyRequest->headers->all() as $name => $value) { + try { + $request = $request->withHeader($name, $value); + } catch (\InvalidArgumentException $e) { + // ignore invalid header + } + } + + $body = $this->streamFactory->createStreamFromResource($symfonyRequest->getContent(true)); + + if (method_exists(Request::class, 'getContentTypeFormat')) { + $format = $symfonyRequest->getContentTypeFormat(); + } else { + $format = $symfonyRequest->getContentType(); + } + + if ('json' === $format) { + $parsedBody = json_decode($symfonyRequest->getContent(), true, 512, \JSON_BIGINT_AS_STRING); + + if (!\is_array($parsedBody)) { + $parsedBody = null; + } + } else { + $parsedBody = $symfonyRequest->request->all(); + } + + $request = $request + ->withBody($body) + ->withUploadedFiles($this->getFiles($symfonyRequest->files->all())) + ->withCookieParams($symfonyRequest->cookies->all()) + ->withQueryParams($symfonyRequest->query->all()) + ->withParsedBody($parsedBody) + ; + + foreach ($symfonyRequest->attributes->all() as $key => $value) { + $request = $request->withAttribute($key, $value); + } + + return $request; + } + + /** + * Converts Symfony uploaded files array to the PSR one. + */ + private function getFiles(array $uploadedFiles): array + { + $files = []; + + foreach ($uploadedFiles as $key => $value) { + if (null === $value) { + $files[$key] = $this->uploadedFileFactory->createUploadedFile($this->streamFactory->createStream(), 0, \UPLOAD_ERR_NO_FILE); + continue; + } + if ($value instanceof UploadedFile) { + $files[$key] = $this->createUploadedFile($value); + } else { + $files[$key] = $this->getFiles($value); + } + } + + return $files; + } + + /** + * Creates a PSR-7 UploadedFile instance from a Symfony one. + */ + private function createUploadedFile(UploadedFile $symfonyUploadedFile): UploadedFileInterface + { + return $this->uploadedFileFactory->createUploadedFile( + $this->streamFactory->createStreamFromFile( + $symfonyUploadedFile->getRealPath() + ), + (int) $symfonyUploadedFile->getSize(), + $symfonyUploadedFile->getError(), + $symfonyUploadedFile->getClientOriginalName(), + $symfonyUploadedFile->getClientMimeType() + ); + } + + /** + * {@inheritdoc} + * + * @return ResponseInterface + */ + public function createResponse(Response $symfonyResponse) + { + $response = $this->responseFactory->createResponse($symfonyResponse->getStatusCode(), Response::$statusTexts[$symfonyResponse->getStatusCode()] ?? ''); + + if ($symfonyResponse instanceof BinaryFileResponse && !$symfonyResponse->headers->has('Content-Range')) { + $stream = $this->streamFactory->createStreamFromFile( + $symfonyResponse->getFile()->getPathname() + ); + } else { + $stream = $this->streamFactory->createStreamFromFile('php://temp', 'wb+'); + if ($symfonyResponse instanceof StreamedResponse || $symfonyResponse instanceof BinaryFileResponse) { + ob_start(function ($buffer) use ($stream) { + $stream->write($buffer); + + return ''; + }, 1); + + $symfonyResponse->sendContent(); + ob_end_clean(); + } else { + $stream->write($symfonyResponse->getContent()); + } + } + + $response = $response->withBody($stream); + + $headers = $symfonyResponse->headers->all(); + $cookies = $symfonyResponse->headers->getCookies(); + if (!empty($cookies)) { + $headers['Set-Cookie'] = []; + + foreach ($cookies as $cookie) { + $headers['Set-Cookie'][] = $cookie->__toString(); + } + } + + foreach ($headers as $name => $value) { + try { + $response = $response->withHeader($name, $value); + } catch (\InvalidArgumentException $e) { + // ignore invalid header + } + } + + $protocolVersion = $symfonyResponse->getProtocolVersion(); + $response = $response->withProtocolVersion($protocolVersion); + + return $response; + } +} diff --git a/vendor/symfony/psr-http-message-bridge/Factory/UploadedFile.php b/vendor/symfony/psr-http-message-bridge/Factory/UploadedFile.php new file mode 100644 index 0000000..4d38a1f --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/Factory/UploadedFile.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PsrHttpMessage\Factory; + +use Psr\Http\Message\UploadedFileInterface; +use Symfony\Component\HttpFoundation\File\Exception\FileException; +use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\HttpFoundation\File\UploadedFile as BaseUploadedFile; + +/** + * @author Nicolas Grekas + */ +class UploadedFile extends BaseUploadedFile +{ + private $psrUploadedFile; + private $test = false; + + public function __construct(UploadedFileInterface $psrUploadedFile, callable $getTemporaryPath) + { + $error = $psrUploadedFile->getError(); + $path = ''; + + if (\UPLOAD_ERR_NO_FILE !== $error) { + $path = $psrUploadedFile->getStream()->getMetadata('uri') ?? ''; + + if ($this->test = !\is_string($path) || !is_uploaded_file($path)) { + $path = $getTemporaryPath(); + $psrUploadedFile->moveTo($path); + } + } + + parent::__construct( + $path, + (string) $psrUploadedFile->getClientFilename(), + $psrUploadedFile->getClientMediaType(), + $psrUploadedFile->getError(), + $this->test + ); + + $this->psrUploadedFile = $psrUploadedFile; + } + + /** + * {@inheritdoc} + */ + public function move(string $directory, string $name = null): File + { + if (!$this->isValid() || $this->test) { + return parent::move($directory, $name); + } + + $target = $this->getTargetFile($directory, $name); + + try { + $this->psrUploadedFile->moveTo((string) $target); + } catch (\RuntimeException $e) { + throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, $e->getMessage()), 0, $e); + } + + @chmod($target, 0666 & ~umask()); + + return $target; + } +} diff --git a/vendor/symfony/psr-http-message-bridge/HttpFoundationFactoryInterface.php b/vendor/symfony/psr-http-message-bridge/HttpFoundationFactoryInterface.php new file mode 100644 index 0000000..a3f9043 --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/HttpFoundationFactoryInterface.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PsrHttpMessage; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Creates Symfony Request and Response instances from PSR-7 ones. + * + * @author Kévin Dunglas + */ +interface HttpFoundationFactoryInterface +{ + /** + * Creates a Symfony Request instance from a PSR-7 one. + * + * @return Request + */ + public function createRequest(ServerRequestInterface $psrRequest, bool $streamed = false); + + /** + * Creates a Symfony Response instance from a PSR-7 one. + * + * @return Response + */ + public function createResponse(ResponseInterface $psrResponse, bool $streamed = false); +} diff --git a/vendor/symfony/psr-http-message-bridge/HttpMessageFactoryInterface.php b/vendor/symfony/psr-http-message-bridge/HttpMessageFactoryInterface.php new file mode 100644 index 0000000..f7b964e --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/HttpMessageFactoryInterface.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PsrHttpMessage; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Creates PSR HTTP Request and Response instances from Symfony ones. + * + * @author Kévin Dunglas + */ +interface HttpMessageFactoryInterface +{ + /** + * Creates a PSR-7 Request instance from a Symfony one. + * + * @return ServerRequestInterface + */ + public function createRequest(Request $symfonyRequest); + + /** + * Creates a PSR-7 Response instance from a Symfony one. + * + * @return ResponseInterface + */ + public function createResponse(Response $symfonyResponse); +} diff --git a/vendor/symfony/psr-http-message-bridge/LICENSE b/vendor/symfony/psr-http-message-bridge/LICENSE new file mode 100644 index 0000000..9ff2d0d --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2021 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/psr-http-message-bridge/README.md b/vendor/symfony/psr-http-message-bridge/README.md new file mode 100644 index 0000000..dcbc09a --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/README.md @@ -0,0 +1,19 @@ +PSR-7 Bridge +============ + +Provides integration for PSR7. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/psr7.html) + +Running the tests +----------------- + +If you want to run the unit tests, install dev dependencies before +running PHPUnit: + + $ cd path/to/Symfony/Bridge/PsrHttpMessage/ + $ composer.phar install + $ phpunit diff --git a/vendor/symfony/psr-http-message-bridge/composer.json b/vendor/symfony/psr-http-message-bridge/composer.json new file mode 100644 index 0000000..b705eb2 --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/composer.json @@ -0,0 +1,48 @@ +{ + "name": "symfony/psr-http-message-bridge", + "type": "symfony-bridge", + "description": "PSR HTTP message bridge", + "keywords": ["http", "psr-7", "psr-17", "http-message"], + "homepage": "http://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "psr/http-message": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/http-foundation": "^5.4 || ^6.0" + }, + "require-dev": { + "symfony/browser-kit": "^5.4 || ^6.0", + "symfony/config": "^5.4 || ^6.0", + "symfony/event-dispatcher": "^5.4 || ^6.0", + "symfony/framework-bundle": "^5.4 || ^6.0", + "symfony/http-kernel": "^5.4 || ^6.0", + "symfony/phpunit-bridge": "^6.2", + "nyholm/psr7": "^1.1", + "psr/log": "^1.1 || ^2 || ^3" + }, + "suggest": { + "nyholm/psr7": "For a super lightweight PSR-7/17 implementation" + }, + "autoload": { + "psr-4": { "Symfony\\Bridge\\PsrHttpMessage\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "extra": { + "branch-alias": { + "dev-main": "2.3-dev" + } + } +} diff --git a/vendor/symfony/service-contracts/LICENSE b/vendor/symfony/service-contracts/LICENSE new file mode 100644 index 0000000..3f853aa --- /dev/null +++ b/vendor/symfony/service-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2019 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/service-contracts/README.md b/vendor/symfony/service-contracts/README.md new file mode 100644 index 0000000..d033a43 --- /dev/null +++ b/vendor/symfony/service-contracts/README.md @@ -0,0 +1,9 @@ +Symfony Service Contracts +========================= + +A set of abstractions extracted out of the Symfony components. + +Can be used to build on semantics that the Symfony components proved useful - and +that already have battle tested implementations. + +See https://github.com/symfony/contracts/blob/master/README.md for more information. diff --git a/vendor/symfony/service-contracts/ResetInterface.php b/vendor/symfony/service-contracts/ResetInterface.php new file mode 100644 index 0000000..1af1075 --- /dev/null +++ b/vendor/symfony/service-contracts/ResetInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +/** + * Provides a way to reset an object to its initial state. + * + * When calling the "reset()" method on an object, it should be put back to its + * initial state. This usually means clearing any internal buffers and forwarding + * the call to internal dependencies. All properties of the object should be put + * back to the same state it had when it was first ready to use. + * + * This method could be called, for example, to recycle objects that are used as + * services, so that they can be used to handle several requests in the same + * process loop (note that we advise making your services stateless instead of + * implementing this interface when possible.) + */ +interface ResetInterface +{ + public function reset(); +} diff --git a/vendor/symfony/service-contracts/ServiceLocatorTrait.php b/vendor/symfony/service-contracts/ServiceLocatorTrait.php new file mode 100644 index 0000000..71b1b74 --- /dev/null +++ b/vendor/symfony/service-contracts/ServiceLocatorTrait.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; + +/** + * A trait to help implement ServiceProviderInterface. + * + * @author Robin Chalas + * @author Nicolas Grekas + */ +trait ServiceLocatorTrait +{ + private $factories; + private $loading = []; + private $providedTypes; + + /** + * @param callable[] $factories + */ + public function __construct(array $factories) + { + $this->factories = $factories; + } + + /** + * {@inheritdoc} + */ + public function has($id) + { + return isset($this->factories[$id]); + } + + /** + * {@inheritdoc} + */ + public function get($id) + { + if (!isset($this->factories[$id])) { + throw $this->createNotFoundException($id); + } + + if (isset($this->loading[$id])) { + $ids = array_values($this->loading); + $ids = \array_slice($this->loading, array_search($id, $ids)); + $ids[] = $id; + + throw $this->createCircularReferenceException($id, $ids); + } + + $this->loading[$id] = $id; + try { + return $this->factories[$id]($this); + } finally { + unset($this->loading[$id]); + } + } + + /** + * {@inheritdoc} + */ + public function getProvidedServices(): array + { + if (null === $this->providedTypes) { + $this->providedTypes = []; + + foreach ($this->factories as $name => $factory) { + if (!\is_callable($factory)) { + $this->providedTypes[$name] = '?'; + } else { + $type = (new \ReflectionFunction($factory))->getReturnType(); + + $this->providedTypes[$name] = $type ? ($type->allowsNull() ? '?' : '').$type->getName() : '?'; + } + } + } + + return $this->providedTypes; + } + + private function createNotFoundException(string $id): NotFoundExceptionInterface + { + if (!$alternatives = array_keys($this->factories)) { + $message = 'is empty...'; + } else { + $last = array_pop($alternatives); + if ($alternatives) { + $message = sprintf('only knows about the "%s" and "%s" services.', implode('", "', $alternatives), $last); + } else { + $message = sprintf('only knows about the "%s" service.', $last); + } + } + + if ($this->loading) { + $message = sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $message); + } else { + $message = sprintf('Service "%s" not found: the current service locator %s', $id, $message); + } + + return new class($message) extends \InvalidArgumentException implements NotFoundExceptionInterface { + }; + } + + private function createCircularReferenceException(string $id, array $path): ContainerExceptionInterface + { + return new class(sprintf('Circular reference detected for service "%s", path: "%s".', $id, implode(' -> ', $path))) extends \RuntimeException implements ContainerExceptionInterface { + }; + } +} diff --git a/vendor/symfony/service-contracts/ServiceProviderInterface.php b/vendor/symfony/service-contracts/ServiceProviderInterface.php new file mode 100644 index 0000000..c60ad0b --- /dev/null +++ b/vendor/symfony/service-contracts/ServiceProviderInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Psr\Container\ContainerInterface; + +/** + * A ServiceProviderInterface exposes the identifiers and the types of services provided by a container. + * + * @author Nicolas Grekas + * @author Mateusz Sip + */ +interface ServiceProviderInterface extends ContainerInterface +{ + /** + * Returns an associative array of service types keyed by the identifiers provided by the current container. + * + * Examples: + * + * * ['logger' => 'Psr\Log\LoggerInterface'] means the object provides a service named "logger" that implements Psr\Log\LoggerInterface + * * ['foo' => '?'] means the container provides service name "foo" of unspecified type + * * ['bar' => '?Bar\Baz'] means the container provides a service "bar" of type Bar\Baz|null + * + * @return string[] The provided service types, keyed by service names + */ + public function getProvidedServices(): array; +} diff --git a/vendor/symfony/service-contracts/ServiceSubscriberInterface.php b/vendor/symfony/service-contracts/ServiceSubscriberInterface.php new file mode 100644 index 0000000..8bb320f --- /dev/null +++ b/vendor/symfony/service-contracts/ServiceSubscriberInterface.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +/** + * A ServiceSubscriber exposes its dependencies via the static {@link getSubscribedServices} method. + * + * The getSubscribedServices method returns an array of service types required by such instances, + * optionally keyed by the service names used internally. Service types that start with an interrogation + * mark "?" are optional, while the other ones are mandatory service dependencies. + * + * The injected service locators SHOULD NOT allow access to any other services not specified by the method. + * + * It is expected that ServiceSubscriber instances consume PSR-11-based service locators internally. + * This interface does not dictate any injection method for these service locators, although constructor + * injection is recommended. + * + * @author Nicolas Grekas + */ +interface ServiceSubscriberInterface +{ + /** + * Returns an array of service types required by such instances, optionally keyed by the service names used internally. + * + * For mandatory dependencies: + * + * * ['logger' => 'Psr\Log\LoggerInterface'] means the objects use the "logger" name + * internally to fetch a service which must implement Psr\Log\LoggerInterface. + * * ['loggers' => 'Psr\Log\LoggerInterface[]'] means the objects use the "loggers" name + * internally to fetch an iterable of Psr\Log\LoggerInterface instances. + * * ['Psr\Log\LoggerInterface'] is a shortcut for + * * ['Psr\Log\LoggerInterface' => 'Psr\Log\LoggerInterface'] + * + * otherwise: + * + * * ['logger' => '?Psr\Log\LoggerInterface'] denotes an optional dependency + * * ['loggers' => '?Psr\Log\LoggerInterface[]'] denotes an optional iterable dependency + * * ['?Psr\Log\LoggerInterface'] is a shortcut for + * * ['Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface'] + * + * @return array The required service types, optionally keyed by service names + */ + public static function getSubscribedServices(); +} diff --git a/vendor/symfony/service-contracts/ServiceSubscriberTrait.php b/vendor/symfony/service-contracts/ServiceSubscriberTrait.php new file mode 100644 index 0000000..ceaef6f --- /dev/null +++ b/vendor/symfony/service-contracts/ServiceSubscriberTrait.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Psr\Container\ContainerInterface; + +/** + * Implementation of ServiceSubscriberInterface that determines subscribed services from + * private method return types. Service ids are available as "ClassName::methodName". + * + * @author Kevin Bond + */ +trait ServiceSubscriberTrait +{ + /** @var ContainerInterface */ + private $container; + + public static function getSubscribedServices(): array + { + static $services; + + if (null !== $services) { + return $services; + } + + $services = \is_callable(['parent', __FUNCTION__]) ? parent::getSubscribedServices() : []; + + foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { + if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { + continue; + } + + if (self::class === $method->getDeclaringClass()->name && ($returnType = $method->getReturnType()) && !$returnType->isBuiltin()) { + $services[self::class.'::'.$method->name] = '?'.$returnType->getName(); + } + } + + return $services; + } + + /** + * @required + */ + public function setContainer(ContainerInterface $container) + { + $this->container = $container; + + if (\is_callable(['parent', __FUNCTION__])) { + return parent::setContainer($container); + } + } +} diff --git a/vendor/symfony/service-contracts/Test/ServiceLocatorTest.php b/vendor/symfony/service-contracts/Test/ServiceLocatorTest.php new file mode 100644 index 0000000..6959458 --- /dev/null +++ b/vendor/symfony/service-contracts/Test/ServiceLocatorTest.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service\Test; + +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Symfony\Contracts\Service\ServiceLocatorTrait; + +class ServiceLocatorTest extends TestCase +{ + public function getServiceLocator(array $factories) + { + return new class($factories) implements ContainerInterface { + use ServiceLocatorTrait; + }; + } + + public function testHas() + { + $locator = $this->getServiceLocator([ + 'foo' => function () { return 'bar'; }, + 'bar' => function () { return 'baz'; }, + function () { return 'dummy'; }, + ]); + + $this->assertTrue($locator->has('foo')); + $this->assertTrue($locator->has('bar')); + $this->assertFalse($locator->has('dummy')); + } + + public function testGet() + { + $locator = $this->getServiceLocator([ + 'foo' => function () { return 'bar'; }, + 'bar' => function () { return 'baz'; }, + ]); + + $this->assertSame('bar', $locator->get('foo')); + $this->assertSame('baz', $locator->get('bar')); + } + + public function testGetDoesNotMemoize() + { + $i = 0; + $locator = $this->getServiceLocator([ + 'foo' => function () use (&$i) { + ++$i; + + return 'bar'; + }, + ]); + + $this->assertSame('bar', $locator->get('foo')); + $this->assertSame('bar', $locator->get('foo')); + $this->assertSame(2, $i); + } + + /** + * @expectedException \Psr\Container\NotFoundExceptionInterface + * @expectedExceptionMessage The service "foo" has a dependency on a non-existent service "bar". This locator only knows about the "foo" service. + */ + public function testThrowsOnUndefinedInternalService() + { + $locator = $this->getServiceLocator([ + 'foo' => function () use (&$locator) { return $locator->get('bar'); }, + ]); + + $locator->get('foo'); + } + + /** + * @expectedException \Psr\Container\ContainerExceptionInterface + * @expectedExceptionMessage Circular reference detected for service "bar", path: "bar -> baz -> bar". + */ + public function testThrowsOnCircularReference() + { + $locator = $this->getServiceLocator([ + 'foo' => function () use (&$locator) { return $locator->get('bar'); }, + 'bar' => function () use (&$locator) { return $locator->get('baz'); }, + 'baz' => function () use (&$locator) { return $locator->get('bar'); }, + ]); + + $locator->get('foo'); + } +} diff --git a/vendor/symfony/service-contracts/composer.json b/vendor/symfony/service-contracts/composer.json new file mode 100644 index 0000000..5434117 --- /dev/null +++ b/vendor/symfony/service-contracts/composer.json @@ -0,0 +1,34 @@ +{ + "name": "symfony/service-contracts", + "type": "library", + "description": "Generic abstractions related to writing services", + "keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": "^7.1.3" + }, + "suggest": { + "psr/container": "", + "symfony/service-implementation": "" + }, + "autoload": { + "psr-4": { "Symfony\\Contracts\\Service\\": "" } + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + } +} diff --git a/vendor/symfony/var-exporter/CHANGELOG.md b/vendor/symfony/var-exporter/CHANGELOG.md new file mode 100644 index 0000000..3406c30 --- /dev/null +++ b/vendor/symfony/var-exporter/CHANGELOG.md @@ -0,0 +1,12 @@ +CHANGELOG +========= + +5.1.0 +----- + + * added argument `array &$foundClasses` to `VarExporter::export()` to ease with preloading exported values + +4.2.0 +----- + + * added the component diff --git a/vendor/symfony/var-exporter/Exception/ClassNotFoundException.php b/vendor/symfony/var-exporter/Exception/ClassNotFoundException.php new file mode 100644 index 0000000..379a765 --- /dev/null +++ b/vendor/symfony/var-exporter/Exception/ClassNotFoundException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Exception; + +class ClassNotFoundException extends \Exception implements ExceptionInterface +{ + public function __construct(string $class, ?\Throwable $previous = null) + { + parent::__construct(sprintf('Class "%s" not found.', $class), 0, $previous); + } +} diff --git a/vendor/symfony/var-exporter/Exception/ExceptionInterface.php b/vendor/symfony/var-exporter/Exception/ExceptionInterface.php new file mode 100644 index 0000000..adfaed4 --- /dev/null +++ b/vendor/symfony/var-exporter/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Exception; + +interface ExceptionInterface extends \Throwable +{ +} diff --git a/vendor/symfony/var-exporter/Exception/NotInstantiableTypeException.php b/vendor/symfony/var-exporter/Exception/NotInstantiableTypeException.php new file mode 100644 index 0000000..b9ba225 --- /dev/null +++ b/vendor/symfony/var-exporter/Exception/NotInstantiableTypeException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Exception; + +class NotInstantiableTypeException extends \Exception implements ExceptionInterface +{ + public function __construct(string $type, ?\Throwable $previous = null) + { + parent::__construct(sprintf('Type "%s" is not instantiable.', $type), 0, $previous); + } +} diff --git a/vendor/symfony/var-exporter/Instantiator.php b/vendor/symfony/var-exporter/Instantiator.php new file mode 100644 index 0000000..368c769 --- /dev/null +++ b/vendor/symfony/var-exporter/Instantiator.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter; + +use Symfony\Component\VarExporter\Exception\ExceptionInterface; +use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException; +use Symfony\Component\VarExporter\Internal\Hydrator; +use Symfony\Component\VarExporter\Internal\Registry; + +/** + * A utility class to create objects without calling their constructor. + * + * @author Nicolas Grekas + */ +final class Instantiator +{ + /** + * Creates an object and sets its properties without calling its constructor nor any other methods. + * + * For example: + * + * // creates an empty instance of Foo + * Instantiator::instantiate(Foo::class); + * + * // creates a Foo instance and sets one of its properties + * Instantiator::instantiate(Foo::class, ['propertyName' => $propertyValue]); + * + * // creates a Foo instance and sets a private property defined on its parent Bar class + * Instantiator::instantiate(Foo::class, [], [ + * Bar::class => ['privateBarProperty' => $propertyValue], + * ]); + * + * Instances of ArrayObject, ArrayIterator and SplObjectStorage can be created + * by using the special "\0" property name to define their internal value: + * + * // creates an SplObjectStorage where $info1 is attached to $obj1, etc. + * Instantiator::instantiate(SplObjectStorage::class, ["\0" => [$obj1, $info1, $obj2, $info2...]]); + * + * // creates an ArrayObject populated with $inputArray + * Instantiator::instantiate(ArrayObject::class, ["\0" => [$inputArray]]); + * + * @param string $class The class of the instance to create + * @param array $properties The properties to set on the instance + * @param array $privateProperties The private properties to set on the instance, + * keyed by their declaring class + * + * @throws ExceptionInterface When the instance cannot be created + */ + public static function instantiate(string $class, array $properties = [], array $privateProperties = []): object + { + $reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class); + + if (Registry::$cloneable[$class]) { + $wrappedInstance = [clone Registry::$prototypes[$class]]; + } elseif (Registry::$instantiableWithoutConstructor[$class]) { + $wrappedInstance = [$reflector->newInstanceWithoutConstructor()]; + } elseif (null === Registry::$prototypes[$class]) { + throw new NotInstantiableTypeException($class); + } elseif ($reflector->implementsInterface('Serializable') && (\PHP_VERSION_ID < 70400 || !method_exists($class, '__unserialize'))) { + $wrappedInstance = [unserialize('C:'.\strlen($class).':"'.$class.'":0:{}')]; + } else { + $wrappedInstance = [unserialize('O:'.\strlen($class).':"'.$class.'":0:{}')]; + } + + if ($properties) { + $privateProperties[$class] = isset($privateProperties[$class]) ? $properties + $privateProperties[$class] : $properties; + } + + foreach ($privateProperties as $class => $properties) { + if (!$properties) { + continue; + } + foreach ($properties as $name => $value) { + // because they're also used for "unserialization", hydrators + // deal with array of instances, so we need to wrap values + $properties[$name] = [$value]; + } + (Hydrator::$hydrators[$class] ?? Hydrator::getHydrator($class))($properties, $wrappedInstance); + } + + return $wrappedInstance[0]; + } +} diff --git a/vendor/symfony/var-exporter/Internal/Exporter.php b/vendor/symfony/var-exporter/Internal/Exporter.php new file mode 100644 index 0000000..51c29e4 --- /dev/null +++ b/vendor/symfony/var-exporter/Internal/Exporter.php @@ -0,0 +1,417 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Internal; + +use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class Exporter +{ + /** + * Prepares an array of values for VarExporter. + * + * For performance this method is public and has no type-hints. + * + * @param array &$values + * @param \SplObjectStorage $objectsPool + * @param array &$refsPool + * @param int &$objectsCount + * @param bool &$valuesAreStatic + * + * @throws NotInstantiableTypeException When a value cannot be serialized + */ + public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount, &$valuesAreStatic): array + { + $refs = $values; + foreach ($values as $k => $value) { + if (\is_resource($value)) { + throw new NotInstantiableTypeException(get_resource_type($value).' resource'); + } + $refs[$k] = $objectsPool; + + if ($isRef = !$valueIsStatic = $values[$k] !== $objectsPool) { + $values[$k] = &$value; // Break hard references to make $values completely + unset($value); // independent from the original structure + $refs[$k] = $value = $values[$k]; + if ($value instanceof Reference && 0 > $value->id) { + $valuesAreStatic = false; + ++$value->count; + continue; + } + $refsPool[] = [&$refs[$k], $value, &$value]; + $refs[$k] = $values[$k] = new Reference(-\count($refsPool), $value); + } + + if (\is_array($value)) { + if ($value) { + $value = self::prepare($value, $objectsPool, $refsPool, $objectsCount, $valueIsStatic); + } + goto handle_value; + } elseif (!\is_object($value) || $value instanceof \UnitEnum) { + goto handle_value; + } + + $valueIsStatic = false; + if (isset($objectsPool[$value])) { + ++$objectsCount; + $value = new Reference($objectsPool[$value][0]); + goto handle_value; + } + + $class = \get_class($value); + $reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class); + $properties = []; + + if ($reflector->hasMethod('__serialize')) { + if (!$reflector->getMethod('__serialize')->isPublic()) { + throw new \Error(sprintf('Call to %s method "%s::__serialize()".', $reflector->getMethod('__serialize')->isProtected() ? 'protected' : 'private', $class)); + } + + if (!\is_array($serializeProperties = $value->__serialize())) { + throw new \TypeError($class.'::__serialize() must return an array'); + } + + if ($reflector->hasMethod('__unserialize')) { + $properties = $serializeProperties; + } else { + foreach ($serializeProperties as $n => $v) { + $c = \PHP_VERSION_ID >= 80100 && $reflector->hasProperty($n) && ($p = $reflector->getProperty($n))->isReadOnly() ? $p->class : 'stdClass'; + $properties[$c][$n] = $v; + } + } + + goto prepare_value; + } + + $sleep = null; + $proto = Registry::$prototypes[$class]; + + if (($value instanceof \ArrayIterator || $value instanceof \ArrayObject) && null !== $proto) { + // ArrayIterator and ArrayObject need special care because their "flags" + // option changes the behavior of the (array) casting operator. + [$arrayValue, $properties] = self::getArrayObjectProperties($value, $proto); + + // populates Registry::$prototypes[$class] with a new instance + Registry::getClassReflector($class, Registry::$instantiableWithoutConstructor[$class], Registry::$cloneable[$class]); + } elseif ($value instanceof \SplObjectStorage && Registry::$cloneable[$class] && null !== $proto) { + // By implementing Serializable, SplObjectStorage breaks + // internal references; let's deal with it on our own. + foreach (clone $value as $v) { + $properties[] = $v; + $properties[] = $value[$v]; + } + $properties = ['SplObjectStorage' => ["\0" => $properties]]; + $arrayValue = (array) $value; + } elseif ($value instanceof \Serializable + || $value instanceof \__PHP_Incomplete_Class + || \PHP_VERSION_ID < 80200 && $value instanceof \DatePeriod + ) { + ++$objectsCount; + $objectsPool[$value] = [$id = \count($objectsPool), serialize($value), [], 0]; + $value = new Reference($id); + goto handle_value; + } else { + if (method_exists($class, '__sleep')) { + if (!\is_array($sleep = $value->__sleep())) { + trigger_error('serialize(): __sleep should return an array only containing the names of instance-variables to serialize', \E_USER_NOTICE); + $value = null; + goto handle_value; + } + $sleep = array_flip($sleep); + } + + $arrayValue = (array) $value; + } + + $proto = (array) $proto; + + foreach ($arrayValue as $name => $v) { + $i = 0; + $n = (string) $name; + if ('' === $n || "\0" !== $n[0]) { + $c = \PHP_VERSION_ID >= 80100 && $reflector->hasProperty($n) && ($p = $reflector->getProperty($n))->isReadOnly() ? $p->class : 'stdClass'; + } elseif ('*' === $n[1]) { + $n = substr($n, 3); + $c = $reflector->getProperty($n)->class; + if ('Error' === $c) { + $c = 'TypeError'; + } elseif ('Exception' === $c) { + $c = 'ErrorException'; + } + } else { + $i = strpos($n, "\0", 2); + $c = substr($n, 1, $i - 1); + $n = substr($n, 1 + $i); + } + if (null !== $sleep) { + if (!isset($sleep[$name]) && (!isset($sleep[$n]) || ($i && $c !== $class))) { + unset($arrayValue[$name]); + continue; + } + unset($sleep[$name], $sleep[$n]); + } + if (!\array_key_exists($name, $proto) || $proto[$name] !== $v || "\x00Error\x00trace" === $name || "\x00Exception\x00trace" === $name) { + $properties[$c][$n] = $v; + } + } + if ($sleep) { + foreach ($sleep as $n => $v) { + trigger_error(sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $n), \E_USER_NOTICE); + } + } + if (method_exists($class, '__unserialize')) { + $properties = $arrayValue; + } + + prepare_value: + $objectsPool[$value] = [$id = \count($objectsPool)]; + $properties = self::prepare($properties, $objectsPool, $refsPool, $objectsCount, $valueIsStatic); + ++$objectsCount; + $objectsPool[$value] = [$id, $class, $properties, method_exists($class, '__unserialize') ? -$objectsCount : (method_exists($class, '__wakeup') ? $objectsCount : 0)]; + + $value = new Reference($id); + + handle_value: + if ($isRef) { + unset($value); // Break the hard reference created above + } elseif (!$valueIsStatic) { + $values[$k] = $value; + } + $valuesAreStatic = $valueIsStatic && $valuesAreStatic; + } + + return $values; + } + + public static function export($value, string $indent = '') + { + switch (true) { + case \is_int($value) || \is_float($value): return var_export($value, true); + case [] === $value: return '[]'; + case false === $value: return 'false'; + case true === $value: return 'true'; + case null === $value: return 'null'; + case '' === $value: return "''"; + case $value instanceof \UnitEnum: return '\\'.ltrim(var_export($value, true), '\\'); + } + + if ($value instanceof Reference) { + if (0 <= $value->id) { + return '$o['.$value->id.']'; + } + if (!$value->count) { + return self::export($value->value, $indent); + } + $value = -$value->id; + + return '&$r['.$value.']'; + } + $subIndent = $indent.' '; + + if (\is_string($value)) { + $code = sprintf("'%s'", addcslashes($value, "'\\")); + + $code = preg_replace_callback("/((?:[\\0\\r\\n]|\u{202A}|\u{202B}|\u{202D}|\u{202E}|\u{2066}|\u{2067}|\u{2068}|\u{202C}|\u{2069})++)(.)/", function ($m) use ($subIndent) { + $m[1] = sprintf('\'."%s".\'', str_replace( + ["\0", "\r", "\n", "\u{202A}", "\u{202B}", "\u{202D}", "\u{202E}", "\u{2066}", "\u{2067}", "\u{2068}", "\u{202C}", "\u{2069}", '\n\\'], + ['\0', '\r', '\n', '\u{202A}', '\u{202B}', '\u{202D}', '\u{202E}', '\u{2066}', '\u{2067}', '\u{2068}', '\u{202C}', '\u{2069}', '\n"'."\n".$subIndent.'."\\'], + $m[1] + )); + + if ("'" === $m[2]) { + return substr($m[1], 0, -2); + } + + if ('n".\'' === substr($m[1], -4)) { + return substr_replace($m[1], "\n".$subIndent.".'".$m[2], -2); + } + + return $m[1].$m[2]; + }, $code, -1, $count); + + if ($count && str_starts_with($code, "''.")) { + $code = substr($code, 3); + } + + return $code; + } + + if (\is_array($value)) { + $j = -1; + $code = ''; + foreach ($value as $k => $v) { + $code .= $subIndent; + if (!\is_int($k) || 1 !== $k - $j) { + $code .= self::export($k, $subIndent).' => '; + } + if (\is_int($k) && $k > $j) { + $j = $k; + } + $code .= self::export($v, $subIndent).",\n"; + } + + return "[\n".$code.$indent.']'; + } + + if ($value instanceof Values) { + $code = $subIndent."\$r = [],\n"; + foreach ($value->values as $k => $v) { + $code .= $subIndent.'$r['.$k.'] = '.self::export($v, $subIndent).",\n"; + } + + return "[\n".$code.$indent.']'; + } + + if ($value instanceof Registry) { + return self::exportRegistry($value, $indent, $subIndent); + } + + if ($value instanceof Hydrator) { + return self::exportHydrator($value, $indent, $subIndent); + } + + throw new \UnexpectedValueException(sprintf('Cannot export value of type "%s".', get_debug_type($value))); + } + + private static function exportRegistry(Registry $value, string $indent, string $subIndent): string + { + $code = ''; + $serializables = []; + $seen = []; + $prototypesAccess = 0; + $factoriesAccess = 0; + $r = '\\'.Registry::class; + $j = -1; + + foreach ($value->classes as $k => $class) { + if (':' === ($class[1] ?? null)) { + $serializables[$k] = $class; + continue; + } + if (!Registry::$instantiableWithoutConstructor[$class]) { + if (is_subclass_of($class, 'Serializable') && !method_exists($class, '__unserialize')) { + $serializables[$k] = 'C:'.\strlen($class).':"'.$class.'":0:{}'; + } else { + $serializables[$k] = 'O:'.\strlen($class).':"'.$class.'":0:{}'; + } + if (is_subclass_of($class, 'Throwable')) { + $eol = is_subclass_of($class, 'Error') ? "\0Error\0" : "\0Exception\0"; + $serializables[$k] = substr_replace($serializables[$k], '1:{s:'.(5 + \strlen($eol)).':"'.$eol.'trace";a:0:{}}', -4); + } + continue; + } + $code .= $subIndent.(1 !== $k - $j ? $k.' => ' : ''); + $j = $k; + $eol = ",\n"; + $c = '['.self::export($class).']'; + + if ($seen[$class] ?? false) { + if (Registry::$cloneable[$class]) { + ++$prototypesAccess; + $code .= 'clone $p'.$c; + } else { + ++$factoriesAccess; + $code .= '$f'.$c.'()'; + } + } else { + $seen[$class] = true; + if (Registry::$cloneable[$class]) { + $code .= 'clone ('.($prototypesAccess++ ? '$p' : '($p = &'.$r.'::$prototypes)').$c.' ?? '.$r.'::p'; + } else { + $code .= '('.($factoriesAccess++ ? '$f' : '($f = &'.$r.'::$factories)').$c.' ?? '.$r.'::f'; + $eol = '()'.$eol; + } + $code .= '('.substr($c, 1, -1).'))'; + } + $code .= $eol; + } + + if (1 === $prototypesAccess) { + $code = str_replace('($p = &'.$r.'::$prototypes)', $r.'::$prototypes', $code); + } + if (1 === $factoriesAccess) { + $code = str_replace('($f = &'.$r.'::$factories)', $r.'::$factories', $code); + } + if ('' !== $code) { + $code = "\n".$code.$indent; + } + + if ($serializables) { + $code = $r.'::unserialize(['.$code.'], '.self::export($serializables, $indent).')'; + } else { + $code = '['.$code.']'; + } + + return '$o = '.$code; + } + + private static function exportHydrator(Hydrator $value, string $indent, string $subIndent): string + { + $code = ''; + foreach ($value->properties as $class => $properties) { + $code .= $subIndent.' '.self::export($class).' => '.self::export($properties, $subIndent.' ').",\n"; + } + + $code = [ + self::export($value->registry, $subIndent), + self::export($value->values, $subIndent), + '' !== $code ? "[\n".$code.$subIndent.']' : '[]', + self::export($value->value, $subIndent), + self::export($value->wakeups, $subIndent), + ]; + + return '\\'.\get_class($value)."::hydrate(\n".$subIndent.implode(",\n".$subIndent, $code)."\n".$indent.')'; + } + + /** + * @param \ArrayIterator|\ArrayObject $value + * @param \ArrayIterator|\ArrayObject $proto + */ + private static function getArrayObjectProperties($value, $proto): array + { + $reflector = $value instanceof \ArrayIterator ? 'ArrayIterator' : 'ArrayObject'; + $reflector = Registry::$reflectors[$reflector] ?? Registry::getClassReflector($reflector); + + $properties = [ + $arrayValue = (array) $value, + $reflector->getMethod('getFlags')->invoke($value), + $value instanceof \ArrayObject ? $reflector->getMethod('getIteratorClass')->invoke($value) : 'ArrayIterator', + ]; + + $reflector = $reflector->getMethod('setFlags'); + $reflector->invoke($proto, \ArrayObject::STD_PROP_LIST); + + if ($properties[1] & \ArrayObject::STD_PROP_LIST) { + $reflector->invoke($value, 0); + $properties[0] = (array) $value; + } else { + $reflector->invoke($value, \ArrayObject::STD_PROP_LIST); + $arrayValue = (array) $value; + } + $reflector->invoke($value, $properties[1]); + + if ([[], 0, 'ArrayIterator'] === $properties) { + $properties = []; + } else { + if ('ArrayIterator' === $properties[2]) { + unset($properties[2]); + } + $properties = [$reflector->class => ["\0" => $properties]]; + } + + return [$arrayValue, $properties]; + } +} diff --git a/vendor/symfony/var-exporter/Internal/Hydrator.php b/vendor/symfony/var-exporter/Internal/Hydrator.php new file mode 100644 index 0000000..5ed6bdc --- /dev/null +++ b/vendor/symfony/var-exporter/Internal/Hydrator.php @@ -0,0 +1,152 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Internal; + +use Symfony\Component\VarExporter\Exception\ClassNotFoundException; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class Hydrator +{ + public static $hydrators = []; + + public $registry; + public $values; + public $properties; + public $value; + public $wakeups; + + public function __construct(?Registry $registry, ?Values $values, array $properties, $value, array $wakeups) + { + $this->registry = $registry; + $this->values = $values; + $this->properties = $properties; + $this->value = $value; + $this->wakeups = $wakeups; + } + + public static function hydrate($objects, $values, $properties, $value, $wakeups) + { + foreach ($properties as $class => $vars) { + (self::$hydrators[$class] ?? self::getHydrator($class))($vars, $objects); + } + foreach ($wakeups as $k => $v) { + if (\is_array($v)) { + $objects[-$k]->__unserialize($v); + } else { + $objects[$v]->__wakeup(); + } + } + + return $value; + } + + public static function getHydrator($class) + { + switch ($class) { + case 'stdClass': + return self::$hydrators[$class] = static function ($properties, $objects) { + foreach ($properties as $name => $values) { + foreach ($values as $i => $v) { + $objects[$i]->$name = $v; + } + } + }; + + case 'ErrorException': + return self::$hydrators[$class] = (self::$hydrators['stdClass'] ?? self::getHydrator('stdClass'))->bindTo(null, new class() extends \ErrorException { + }); + + case 'TypeError': + return self::$hydrators[$class] = (self::$hydrators['stdClass'] ?? self::getHydrator('stdClass'))->bindTo(null, new class() extends \Error { + }); + + case 'SplObjectStorage': + return self::$hydrators[$class] = static function ($properties, $objects) { + foreach ($properties as $name => $values) { + if ("\0" === $name) { + foreach ($values as $i => $v) { + for ($j = 0; $j < \count($v); ++$j) { + $objects[$i]->attach($v[$j], $v[++$j]); + } + } + continue; + } + foreach ($values as $i => $v) { + $objects[$i]->$name = $v; + } + } + }; + } + + if (!class_exists($class) && !interface_exists($class, false) && !trait_exists($class, false)) { + throw new ClassNotFoundException($class); + } + $classReflector = new \ReflectionClass($class); + + switch ($class) { + case 'ArrayIterator': + case 'ArrayObject': + $constructor = \Closure::fromCallable([$classReflector->getConstructor(), 'invokeArgs']); + + return self::$hydrators[$class] = static function ($properties, $objects) use ($constructor) { + foreach ($properties as $name => $values) { + if ("\0" !== $name) { + foreach ($values as $i => $v) { + $objects[$i]->$name = $v; + } + } + } + foreach ($properties["\0"] ?? [] as $i => $v) { + $constructor($objects[$i], $v); + } + }; + } + + if (!$classReflector->isInternal()) { + return self::$hydrators[$class] = (self::$hydrators['stdClass'] ?? self::getHydrator('stdClass'))->bindTo(null, $class); + } + + if ($classReflector->name !== $class) { + return self::$hydrators[$classReflector->name] ?? self::getHydrator($classReflector->name); + } + + $propertySetters = []; + foreach ($classReflector->getProperties() as $propertyReflector) { + if (!$propertyReflector->isStatic()) { + $propertyReflector->setAccessible(true); + $propertySetters[$propertyReflector->name] = \Closure::fromCallable([$propertyReflector, 'setValue']); + } + } + + if (!$propertySetters) { + return self::$hydrators[$class] = self::$hydrators['stdClass'] ?? self::getHydrator('stdClass'); + } + + return self::$hydrators[$class] = static function ($properties, $objects) use ($propertySetters) { + foreach ($properties as $name => $values) { + if ($setValue = $propertySetters[$name] ?? null) { + foreach ($values as $i => $v) { + $setValue($objects[$i], $v); + } + continue; + } + foreach ($values as $i => $v) { + $objects[$i]->$name = $v; + } + } + }; + } +} diff --git a/vendor/symfony/var-exporter/Internal/Reference.php b/vendor/symfony/var-exporter/Internal/Reference.php new file mode 100644 index 0000000..e371c07 --- /dev/null +++ b/vendor/symfony/var-exporter/Internal/Reference.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Internal; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class Reference +{ + public $id; + public $value; + public $count = 0; + + public function __construct(int $id, $value = null) + { + $this->id = $id; + $this->value = $value; + } +} diff --git a/vendor/symfony/var-exporter/Internal/Registry.php b/vendor/symfony/var-exporter/Internal/Registry.php new file mode 100644 index 0000000..24b77b9 --- /dev/null +++ b/vendor/symfony/var-exporter/Internal/Registry.php @@ -0,0 +1,146 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Internal; + +use Symfony\Component\VarExporter\Exception\ClassNotFoundException; +use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class Registry +{ + public static $reflectors = []; + public static $prototypes = []; + public static $factories = []; + public static $cloneable = []; + public static $instantiableWithoutConstructor = []; + + public $classes = []; + + public function __construct(array $classes) + { + $this->classes = $classes; + } + + public static function unserialize($objects, $serializables) + { + $unserializeCallback = ini_set('unserialize_callback_func', __CLASS__.'::getClassReflector'); + + try { + foreach ($serializables as $k => $v) { + $objects[$k] = unserialize($v); + } + } finally { + ini_set('unserialize_callback_func', $unserializeCallback); + } + + return $objects; + } + + public static function p($class) + { + self::getClassReflector($class, true, true); + + return self::$prototypes[$class]; + } + + public static function f($class) + { + $reflector = self::$reflectors[$class] ?? self::getClassReflector($class, true, false); + + return self::$factories[$class] = \Closure::fromCallable([$reflector, 'newInstanceWithoutConstructor']); + } + + public static function getClassReflector($class, $instantiableWithoutConstructor = false, $cloneable = null) + { + if (!($isClass = class_exists($class)) && !interface_exists($class, false) && !trait_exists($class, false)) { + throw new ClassNotFoundException($class); + } + $reflector = new \ReflectionClass($class); + + if ($instantiableWithoutConstructor) { + $proto = $reflector->newInstanceWithoutConstructor(); + } elseif (!$isClass || $reflector->isAbstract()) { + throw new NotInstantiableTypeException($class); + } elseif ($reflector->name !== $class) { + $reflector = self::$reflectors[$name = $reflector->name] ?? self::getClassReflector($name, false, $cloneable); + self::$cloneable[$class] = self::$cloneable[$name]; + self::$instantiableWithoutConstructor[$class] = self::$instantiableWithoutConstructor[$name]; + self::$prototypes[$class] = self::$prototypes[$name]; + + return self::$reflectors[$class] = $reflector; + } else { + try { + $proto = $reflector->newInstanceWithoutConstructor(); + $instantiableWithoutConstructor = true; + } catch (\ReflectionException $e) { + $proto = $reflector->implementsInterface('Serializable') && !method_exists($class, '__unserialize') ? 'C:' : 'O:'; + if ('C:' === $proto && !$reflector->getMethod('unserialize')->isInternal()) { + $proto = null; + } else { + try { + $proto = @unserialize($proto.\strlen($class).':"'.$class.'":0:{}'); + } catch (\Exception $e) { + if (__FILE__ !== $e->getFile()) { + throw $e; + } + throw new NotInstantiableTypeException($class, $e); + } + if (false === $proto) { + throw new NotInstantiableTypeException($class); + } + } + } + if (null !== $proto && !$proto instanceof \Throwable && !$proto instanceof \Serializable && !method_exists($class, '__sleep') && (\PHP_VERSION_ID < 70400 || !method_exists($class, '__serialize'))) { + try { + serialize($proto); + } catch (\Exception $e) { + throw new NotInstantiableTypeException($class, $e); + } + } + } + + if (null === $cloneable) { + if (($proto instanceof \Reflector || $proto instanceof \ReflectionGenerator || $proto instanceof \ReflectionType || $proto instanceof \IteratorIterator || $proto instanceof \RecursiveIteratorIterator) && (!$proto instanceof \Serializable && !method_exists($proto, '__wakeup') && (\PHP_VERSION_ID < 70400 || !method_exists($class, '__unserialize')))) { + throw new NotInstantiableTypeException($class); + } + + $cloneable = $reflector->isCloneable() && !$reflector->hasMethod('__clone'); + } + + self::$cloneable[$class] = $cloneable; + self::$instantiableWithoutConstructor[$class] = $instantiableWithoutConstructor; + self::$prototypes[$class] = $proto; + + if ($proto instanceof \Throwable) { + static $setTrace; + + if (null === $setTrace) { + $setTrace = [ + new \ReflectionProperty(\Error::class, 'trace'), + new \ReflectionProperty(\Exception::class, 'trace'), + ]; + $setTrace[0]->setAccessible(true); + $setTrace[1]->setAccessible(true); + $setTrace[0] = \Closure::fromCallable([$setTrace[0], 'setValue']); + $setTrace[1] = \Closure::fromCallable([$setTrace[1], 'setValue']); + } + + $setTrace[$proto instanceof \Exception]($proto, []); + } + + return self::$reflectors[$class] = $reflector; + } +} diff --git a/vendor/symfony/var-exporter/Internal/Values.php b/vendor/symfony/var-exporter/Internal/Values.php new file mode 100644 index 0000000..21ae04e --- /dev/null +++ b/vendor/symfony/var-exporter/Internal/Values.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Internal; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class Values +{ + public $values; + + public function __construct(array $values) + { + $this->values = $values; + } +} diff --git a/vendor/symfony/var-exporter/LICENSE b/vendor/symfony/var-exporter/LICENSE new file mode 100644 index 0000000..7536cae --- /dev/null +++ b/vendor/symfony/var-exporter/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/var-exporter/README.md b/vendor/symfony/var-exporter/README.md new file mode 100644 index 0000000..a34e4c2 --- /dev/null +++ b/vendor/symfony/var-exporter/README.md @@ -0,0 +1,38 @@ +VarExporter Component +===================== + +The VarExporter component allows exporting any serializable PHP data structure to +plain PHP code. While doing so, it preserves all the semantics associated with +the serialization mechanism of PHP (`__wakeup`, `__sleep`, `Serializable`, +`__serialize`, `__unserialize`). + +It also provides an instantiator that allows creating and populating objects +without calling their constructor nor any other methods. + +The reason to use this component *vs* `serialize()` or +[igbinary](https://github.com/igbinary/igbinary) is performance: thanks to +OPcache, the resulting code is significantly faster and more memory efficient +than using `unserialize()` or `igbinary_unserialize()`. + +Unlike `var_export()`, this works on any serializable PHP value. + +It also provides a few improvements over `var_export()`/`serialize()`: + + * the output is PSR-2 compatible; + * the output can be re-indented without messing up with `\r` or `\n` in the data + * missing classes throw a `ClassNotFoundException` instead of being unserialized to + `PHP_Incomplete_Class` objects; + * references involving `SplObjectStorage`, `ArrayObject` or `ArrayIterator` + instances are preserved; + * `Reflection*`, `IteratorIterator` and `RecursiveIteratorIterator` classes + throw an exception when being serialized (their unserialized version is broken + anyway, see https://bugs.php.net/76737). + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/var_exporter.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/vendor/symfony/var-exporter/VarExporter.php b/vendor/symfony/var-exporter/VarExporter.php new file mode 100644 index 0000000..d4c0809 --- /dev/null +++ b/vendor/symfony/var-exporter/VarExporter.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter; + +use Symfony\Component\VarExporter\Exception\ExceptionInterface; +use Symfony\Component\VarExporter\Internal\Exporter; +use Symfony\Component\VarExporter\Internal\Hydrator; +use Symfony\Component\VarExporter\Internal\Registry; +use Symfony\Component\VarExporter\Internal\Values; + +/** + * Exports serializable PHP values to PHP code. + * + * VarExporter allows serializing PHP data structures to plain PHP code (like var_export()) + * while preserving all the semantics associated with serialize() (unlike var_export()). + * + * By leveraging OPcache, the generated PHP code is faster than doing the same with unserialize(). + * + * @author Nicolas Grekas + */ +final class VarExporter +{ + /** + * Exports a serializable PHP value to PHP code. + * + * @param mixed $value The value to export + * @param bool &$isStaticValue Set to true after execution if the provided value is static, false otherwise + * @param array &$foundClasses Classes found in the value are added to this list as both keys and values + * + * @throws ExceptionInterface When the provided value cannot be serialized + */ + public static function export($value, ?bool &$isStaticValue = null, array &$foundClasses = []): string + { + $isStaticValue = true; + + if (!\is_object($value) && !(\is_array($value) && $value) && !\is_resource($value) || $value instanceof \UnitEnum) { + return Exporter::export($value); + } + + $objectsPool = new \SplObjectStorage(); + $refsPool = []; + $objectsCount = 0; + + try { + $value = Exporter::prepare([$value], $objectsPool, $refsPool, $objectsCount, $isStaticValue)[0]; + } finally { + $references = []; + foreach ($refsPool as $i => $v) { + if ($v[0]->count) { + $references[1 + $i] = $v[2]; + } + $v[0] = $v[1]; + } + } + + if ($isStaticValue) { + return Exporter::export($value); + } + + $classes = []; + $values = []; + $states = []; + foreach ($objectsPool as $i => $v) { + [, $class, $values[], $wakeup] = $objectsPool[$v]; + $foundClasses[$class] = $classes[] = $class; + + if (0 < $wakeup) { + $states[$wakeup] = $i; + } elseif (0 > $wakeup) { + $states[-$wakeup] = [$i, array_pop($values)]; + $values[] = []; + } + } + ksort($states); + + $wakeups = [null]; + foreach ($states as $v) { + if (\is_array($v)) { + $wakeups[-$v[0]] = $v[1]; + } else { + $wakeups[] = $v; + } + } + + if (null === $wakeups[0]) { + unset($wakeups[0]); + } + + $properties = []; + foreach ($values as $i => $vars) { + foreach ($vars as $class => $values) { + foreach ($values as $name => $v) { + $properties[$class][$name][$i] = $v; + } + } + } + + if ($classes || $references) { + $value = new Hydrator(new Registry($classes), $references ? new Values($references) : null, $properties, $value, $wakeups); + } else { + $isStaticValue = true; + } + + return Exporter::export($value); + } +} diff --git a/vendor/symfony/var-exporter/composer.json b/vendor/symfony/var-exporter/composer.json new file mode 100644 index 0000000..29d4901 --- /dev/null +++ b/vendor/symfony/var-exporter/composer.json @@ -0,0 +1,32 @@ +{ + "name": "symfony/var-exporter", + "type": "library", + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "keywords": ["export", "serialize", "instantiate", "hydrate", "construct", "clone"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "symfony/var-dumper": "^4.4.9|^5.0.9|^6.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\VarExporter\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/vendor/topthink/framework/.gitignore b/vendor/topthink/framework/.gitignore new file mode 100644 index 0000000..f7775ba --- /dev/null +++ b/vendor/topthink/framework/.gitignore @@ -0,0 +1,8 @@ +/vendor +composer.phar +composer.lock +.DS_Store +Thumbs.db +/phpunit.xml +/.idea +/.vscode \ No newline at end of file diff --git a/vendor/topthink/framework/.htaccess b/vendor/topthink/framework/.htaccess new file mode 100644 index 0000000..3418e55 --- /dev/null +++ b/vendor/topthink/framework/.htaccess @@ -0,0 +1 @@ +deny from all \ No newline at end of file diff --git a/vendor/topthink/framework/CONTRIBUTING.md b/vendor/topthink/framework/CONTRIBUTING.md new file mode 100644 index 0000000..6cefcb3 --- /dev/null +++ b/vendor/topthink/framework/CONTRIBUTING.md @@ -0,0 +1,119 @@ +如何贡献我的源代码 +=== + +此文档介绍了 ThinkPHP 团队的组成以及运转机制,您提交的代码将给 ThinkPHP 项目带来什么好处,以及如何才能加入我们的行列。 + +## 通过 Github 贡献代码 + +ThinkPHP 目前使用 Git 来控制程序版本,如果你想为 ThinkPHP 贡献源代码,请先大致了解 Git 的使用方法。我们目前把项目托管在 GitHub 上,任何 GitHub 用户都可以向我们贡献代码。 + +参与的方式很简单,`fork`一份 ThinkPHP 的代码到你的仓库中,修改后提交,并向我们发起`pull request`申请,我们会及时对代码进行审查并处理你的申请并。审查通过后,你的代码将被`merge`进我们的仓库中,这样你就会自动出现在贡献者名单里了,非常方便。 + +我们希望你贡献的代码符合: + +* ThinkPHP 的编码规范 +* 适当的注释,能让其他人读懂 +* 遵循 Apache2 开源协议 + +**如果想要了解更多细节或有任何疑问,请继续阅读下面的内容** + +### 注意事项 + +* 本项目代码格式化标准选用 [**PSR-2**](http://www.kancloud.cn/thinkphp/php-fig-psr/3141); +* 类名和类文件名遵循 [**PSR-4**](http://www.kancloud.cn/thinkphp/php-fig-psr/3144); +* 对于 Issues 的处理,请使用诸如 `fix #xxx(Issue ID)` 的 commit title 直接关闭 issue。 +* 系统会自动在 PHP 5.4 5.5 5.6 7.0 和 HHVM 上测试修改,其中 HHVM 下的测试容许报错,请确保你的修改符合 PHP 5.4 ~ 5.6 和 PHP 7.0 的语法规范; +* 管理员不会合并造成 CI faild 的修改,若出现 CI faild 请检查自己的源代码或修改相应的[单元测试文件](tests); + +## GitHub Issue + +GitHub 提供了 Issue 功能,该功能可以用于: + +* 提出 bug +* 提出功能改进 +* 反馈使用体验 + +该功能不应该用于: + + * 提出修改意见(涉及代码署名和修订追溯问题) + * 不友善的言论 + +## 快速修改 + +**GitHub 提供了快速编辑文件的功能** + +1. 登录 GitHub 帐号; +2. 浏览项目文件,找到要进行修改的文件; +3. 点击右上角铅笔图标进行修改; +4. 填写 `Commit changes` 相关内容(Title 必填); +5. 提交修改,等待 CI 验证和管理员合并。 + +**若您需要一次提交大量修改,请继续阅读下面的内容** + +## 完整流程 + +1. `fork`本项目; +2. 克隆(`clone`)你 `fork` 的项目到本地; +3. 新建分支(`branch`)并检出(`checkout`)新分支; +4. 添加本项目到你的本地 git 仓库作为上游(`upstream`); +5. 进行修改,若你的修改包含方法或函数的增减,请记得修改[单元测试文件](tests); +6. 变基(衍合 `rebase`)你的分支到上游 master 分支; +7. `push` 你的本地仓库到 GitHub; +8. 提交 `pull request`; +9. 等待 CI 验证(若不通过则重复 5~7,GitHub 会自动更新你的 `pull request`); +10. 等待管理员处理,并及时 `rebase` 你的分支到上游 master 分支(若上游 master 分支有修改)。 + +*若有必要,可以 `git push -f` 强行推送 rebase 后的分支到自己的 `fork`* + +*绝对不可以使用 `git push -f` 强行推送修改到上游* + +### 注意事项 + +* 若对上述流程有任何不清楚的地方,请查阅 GIT 教程,如 [这个](http://backlogtool.com/git-guide/cn/); +* 对于代码**不同方面**的修改,请在自己 `fork` 的项目中**创建不同的分支**(原因参见`完整流程`第9条备注部分); +* 变基及交互式变基操作参见 [Git 交互式变基](http://pakchoi.me/2015/03/17/git-interactive-rebase/) + +## 推荐资源 + +### 开发环境 + +* XAMPP for Windows 5.5.x +* WampServer (for Windows) +* upupw Apache PHP5.4 ( for Windows) + +或自行安装 + +- Apache / Nginx +- PHP 5.4 ~ 5.6 +- MySQL / MariaDB + +*Windows 用户推荐添加 PHP bin 目录到 PATH,方便使用 composer* + +*Linux 用户自行配置环境, Mac 用户推荐使用内置 Apache 配合 Homebrew 安装 PHP 和 MariaDB* + +### 编辑器 + +Sublime Text 3 + phpfmt 插件 + +phpfmt 插件参数 + +```json +{ + "autocomplete": true, + "enable_auto_align": true, + "format_on_save": true, + "indent_with_space": true, + "psr1_naming": false, + "psr2": true, + "version": 4 +} +``` + +或其他 编辑器 / IDE 配合 PSR2 自动格式化工具 + +### Git GUI + +* SourceTree +* GitHub Desktop + +或其他 Git 图形界面客户端 diff --git a/vendor/topthink/framework/LICENSE.txt b/vendor/topthink/framework/LICENSE.txt new file mode 100644 index 0000000..774fa76 --- /dev/null +++ b/vendor/topthink/framework/LICENSE.txt @@ -0,0 +1,32 @@ + +ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 +版权所有Copyright © 2006-2018 by ThinkPHP (http://thinkphp.cn) +All rights reserved。 +ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 + +Apache Licence是著名的非盈利开源组织Apache采用的协议。 +该协议和BSD类似,鼓励代码共享和尊重原作者的著作权, +允许代码修改,再作为开源或商业软件发布。需要满足 +的条件: +1. 需要给代码的用户一份Apache Licence ; +2. 如果你修改了代码,需要在被修改的文件中说明; +3. 在延伸的代码中(修改和有源代码衍生的代码中)需要 +带有原来代码中的协议,商标,专利声明和其他原来作者规 +定需要包含的说明; +4. 如果再发布的产品中包含一个Notice文件,则在Notice文 +件中需要带有本协议内容。你可以在Notice中增加自己的 +许可,但不可以表现为对Apache Licence构成更改。 +具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0 + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/topthink/framework/README.md b/vendor/topthink/framework/README.md new file mode 100644 index 0000000..1339e6c --- /dev/null +++ b/vendor/topthink/framework/README.md @@ -0,0 +1,99 @@ +![](https://box.kancloud.cn/5a0aaa69a5ff42657b5c4715f3d49221) + +ThinkPHP 5.1(LTS) —— 12载初心,你值得信赖的PHP框架 +=============== + +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/top-think/framework/badges/quality-score.png?b=5.1)](https://scrutinizer-ci.com/g/top-think/framework/?branch=5.1) +[![Build Status](https://travis-ci.org/top-think/framework.svg?branch=master)](https://travis-ci.org/top-think/framework) +[![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) +[![PHP Version](https://img.shields.io/badge/php-%3E%3D5.6-8892BF.svg)](http://www.php.net/) +[![License](https://poser.pugx.org/topthink/framework/license)](https://packagist.org/packages/topthink/framework) + +ThinkPHP5.1对底层架构做了进一步的改进,减少依赖,其主要特性包括: + + + 采用容器统一管理对象 + + 支持Facade + + 更易用的路由 + + 注解路由支持 + + 路由跨域请求支持 + + 验证类增强 + + 配置和路由目录独立 + + 取消系统常量 + + 类库别名机制 + + 模型和数据库增强 + + 依赖注入完善 + + 支持PSR-3日志规范 + + 中间件支持(`V5.1.6+`) + + 支持`Swoole`/`Workerman`运行(`V5.1.18+`) + +官方已经正式宣布`5.1.27`版本为LTS版本。 + +### 废除的功能: + + + 聚合模型 + + 内置控制器扩展类 + + 模型自动验证 + +> ThinkPHP5.1的运行环境要求PHP5.6+ 兼容PHP8.0。 + + +## 安装 + +使用composer安装 + +~~~ +composer create-project topthink/think tp +~~~ + +启动服务 + +~~~ +cd tp +php think run +~~~ + +然后就可以在浏览器中访问 + +~~~ +http://localhost:8000 +~~~ + +更新框架 +~~~ +composer update topthink/framework +~~~ + + +## 在线手册 + ++ [完全开发手册](https://www.kancloud.cn/manual/thinkphp5_1/content) ++ [升级指导](https://www.kancloud.cn/manual/thinkphp5_1/354155) + + +## 官方服务 + ++ [应用服务市场](https://market.topthink.com/) ++ [ThinkAPI——统一API服务](https://docs.topthink.com/think-api) + +## 命名规范 + +`ThinkPHP5.1`遵循PSR-2命名规范和PSR-4自动加载规范。 + +## 参与开发 + +请参阅 [ThinkPHP5 核心框架包](https://github.com/top-think/framework)。 + +## 版权信息 + +ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 + +本项目包含的第三方源码和二进制文件之版权信息另行标注。 + +版权所有Copyright © 2006-2018 by ThinkPHP (http://thinkphp.cn) + +All rights reserved。 + +ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 + +更多细节参阅 [LICENSE.txt](LICENSE.txt) diff --git a/vendor/topthink/framework/base.php b/vendor/topthink/framework/base.php new file mode 100644 index 0000000..d7238cc --- /dev/null +++ b/vendor/topthink/framework/base.php @@ -0,0 +1,52 @@ + +// +---------------------------------------------------------------------- +namespace think; + +// 载入Loader类 +require __DIR__ . '/library/think/Loader.php'; + +// 注册自动加载 +Loader::register(); + +// 注册错误和异常处理机制 +Error::register(); + +// 实现日志接口 +if (interface_exists('Psr\Log\LoggerInterface')) { + interface LoggerInterface extends \Psr\Log\LoggerInterface + {} +} else { + interface LoggerInterface + {} +} + +// 注册类库别名 +Loader::addClassAlias([ + 'App' => facade\App::class, + 'Build' => facade\Build::class, + 'Cache' => facade\Cache::class, + 'Config' => facade\Config::class, + 'Cookie' => facade\Cookie::class, + 'Db' => Db::class, + 'Debug' => facade\Debug::class, + 'Env' => facade\Env::class, + 'Facade' => Facade::class, + 'Hook' => facade\Hook::class, + 'Lang' => facade\Lang::class, + 'Log' => facade\Log::class, + 'Request' => facade\Request::class, + 'Response' => facade\Response::class, + 'Route' => facade\Route::class, + 'Session' => facade\Session::class, + 'Url' => facade\Url::class, + 'Validate' => facade\Validate::class, + 'View' => facade\View::class, +]); diff --git a/vendor/topthink/framework/composer.json b/vendor/topthink/framework/composer.json new file mode 100644 index 0000000..33477b1 --- /dev/null +++ b/vendor/topthink/framework/composer.json @@ -0,0 +1,35 @@ +{ + "name": "topthink/framework", + "description": "the new thinkphp framework", + "type": "think-framework", + "keywords": [ + "framework", + "thinkphp", + "ORM" + ], + "homepage": "http://thinkphp.cn/", + "license": "Apache-2.0", + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + }, + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "require": { + "php": ">=5.6.0", + "topthink/think-installer": "2.*" + }, + "require-dev": { + "phpunit/phpunit": "^5.0|^6.0", + "johnkary/phpunit-speedtrap": "^1.0", + "mikey179/vfsstream": "~1.6", + "phploc/phploc": "2.*", + "sebastian/phpcpd": "2.*", + "squizlabs/php_codesniffer": "2.*", + "phpdocumentor/reflection-docblock": "^2.0" + } +} diff --git a/vendor/topthink/framework/convention.php b/vendor/topthink/framework/convention.php new file mode 100644 index 0000000..1d85e56 --- /dev/null +++ b/vendor/topthink/framework/convention.php @@ -0,0 +1,327 @@ + [ + // 应用名称 + 'app_name' => '', + // 应用地址 + 'app_host' => '', + // 应用调试模式 + 'app_debug' => false, + // 应用Trace + 'app_trace' => false, + // 应用模式状态 + 'app_status' => '', + // 是否HTTPS + 'is_https' => false, + // 入口自动绑定模块 + 'auto_bind_module' => false, + // 注册的根命名空间 + 'root_namespace' => [], + // 默认输出类型 + 'default_return_type' => 'html', + // 默认AJAX 数据返回格式,可选json xml ... + 'default_ajax_return' => 'json', + // 默认JSONP格式返回的处理方法 + 'default_jsonp_handler' => 'jsonpReturn', + // 默认JSONP处理方法 + 'var_jsonp_handler' => 'callback', + // 默认时区 + 'default_timezone' => 'Asia/Shanghai', + // 是否开启多语言 + 'lang_switch_on' => false, + // 默认验证器 + 'default_validate' => '', + // 默认语言 + 'default_lang' => 'zh-cn', + + // +---------------------------------------------------------------------- + // | 模块设置 + // +---------------------------------------------------------------------- + + // 自动搜索控制器 + 'controller_auto_search' => false, + // 操作方法前缀 + 'use_action_prefix' => false, + // 操作方法后缀 + 'action_suffix' => '', + // 默认的空控制器名 + 'empty_controller' => 'Error', + // 默认的空模块名 + 'empty_module' => '', + // 默认模块名 + 'default_module' => 'index', + // 是否支持多模块 + 'app_multi_module' => true, + // 禁止访问模块 + 'deny_module_list' => ['common'], + // 默认控制器名 + 'default_controller' => 'Index', + // 默认操作名 + 'default_action' => 'index', + // 是否自动转换URL中的控制器和操作名 + 'url_convert' => true, + // 默认的访问控制器层 + 'url_controller_layer' => 'controller', + // 应用类库后缀 + 'class_suffix' => false, + // 控制器类后缀 + 'controller_suffix' => false, + + // +---------------------------------------------------------------------- + // | URL请求设置 + // +---------------------------------------------------------------------- + + // 默认全局过滤方法 用逗号分隔多个 + 'default_filter' => '', + // PATHINFO变量名 用于兼容模式 + 'var_pathinfo' => 's', + // 兼容PATH_INFO获取 + 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'], + // HTTPS代理标识 + 'https_agent_name' => '', + // IP代理获取标识 + 'http_agent_ip' => 'HTTP_X_REAL_IP', + // URL伪静态后缀 + 'url_html_suffix' => 'html', + // 域名根,如thinkphp.cn + 'url_domain_root' => '', + // 表单请求类型伪装变量 + 'var_method' => '_method', + // 表单ajax伪装变量 + 'var_ajax' => '_ajax', + // 表单pjax伪装变量 + 'var_pjax' => '_pjax', + // 是否开启请求缓存 true自动缓存 支持设置请求缓存规则 + 'request_cache' => false, + // 请求缓存有效期 + 'request_cache_expire' => null, + // 全局请求缓存排除规则 + 'request_cache_except' => [], + + // +---------------------------------------------------------------------- + // | 路由设置 + // +---------------------------------------------------------------------- + + // pathinfo分隔符 + 'pathinfo_depr' => '/', + // URL普通方式参数 用于自动生成 + 'url_common_param' => false, + // URL参数方式 0 按名称成对解析 1 按顺序解析 + 'url_param_type' => 0, + // 是否开启路由延迟解析 + 'url_lazy_route' => false, + // 是否强制使用路由 + 'url_route_must' => false, + // 合并路由规则 + 'route_rule_merge' => false, + // 路由是否完全匹配 + 'route_complete_match' => false, + // 使用注解路由 + 'route_annotation' => false, + // 默认的路由变量规则 + 'default_route_pattern' => '\w+', + // 是否开启路由缓存 + 'route_check_cache' => false, + // 路由缓存的Key自定义设置(闭包),默认为当前URL和请求类型的md5 + 'route_check_cache_key' => '', + // 路由缓存的设置 + 'route_cache_option' => [], + + // +---------------------------------------------------------------------- + // | 异常及错误设置 + // +---------------------------------------------------------------------- + + // 默认跳转页面对应的模板文件 + 'dispatch_success_tmpl' => __DIR__ . '/tpl/dispatch_jump.tpl', + 'dispatch_error_tmpl' => __DIR__ . '/tpl/dispatch_jump.tpl', + // 异常页面的模板文件 + 'exception_tmpl' => __DIR__ . '/tpl/think_exception.tpl', + // 错误显示信息,非调试模式有效 + 'error_message' => '页面错误!请稍后再试~', + // 显示错误信息 + 'show_error_msg' => false, + // 异常处理handle类 留空使用 \think\exception\Handle + 'exception_handle' => '', + ], + + // +---------------------------------------------------------------------- + // | 模板设置 + // +---------------------------------------------------------------------- + + 'template' => [ + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 + 'auto_rule' => 1, + // 模板引擎类型 支持 php think 支持扩展 + 'type' => 'Think', + // 视图基础目录,配置目录为所有模块的视图起始目录 + 'view_base' => '', + // 当前模板的视图目录 留空为自动获取 + 'view_path' => '', + // 模板后缀 + 'view_suffix' => 'html', + // 模板文件名分隔符 + 'view_depr' => DIRECTORY_SEPARATOR, + // 模板引擎普通标签开始标记 + 'tpl_begin' => '{', + // 模板引擎普通标签结束标记 + 'tpl_end' => '}', + // 标签库标签开始标记 + 'taglib_begin' => '{', + // 标签库标签结束标记 + 'taglib_end' => '}', + ], + + // +---------------------------------------------------------------------- + // | 日志设置 + // +---------------------------------------------------------------------- + + 'log' => [ + // 日志记录方式,内置 file socket 支持扩展 + 'type' => 'File', + // 日志保存目录 + //'path' => LOG_PATH, + // 日志记录级别 + 'level' => [], + // 是否记录trace信息到日志 + 'record_trace' => false, + // 是否JSON格式记录 + 'json' => false, + ], + + // +---------------------------------------------------------------------- + // | Trace设置 开启 app_trace 后 有效 + // +---------------------------------------------------------------------- + + 'trace' => [ + // 内置Html Console 支持扩展 + 'type' => 'Html', + 'file' => __DIR__ . '/tpl/page_trace.tpl', + ], + + // +---------------------------------------------------------------------- + // | 缓存设置 + // +---------------------------------------------------------------------- + + 'cache' => [ + // 驱动方式 + 'type' => 'File', + // 缓存保存目录 + //'path' => CACHE_PATH, + // 缓存前缀 + 'prefix' => '', + // 缓存有效期 0表示永久缓存 + 'expire' => 0, + ], + + // +---------------------------------------------------------------------- + // | 会话设置 + // +---------------------------------------------------------------------- + + 'session' => [ + 'id' => '', + // SESSION_ID的提交变量,解决flash上传跨域 + 'var_session_id' => '', + // SESSION 前缀 + 'prefix' => 'think', + // 驱动方式 支持redis memcache memcached + 'type' => '', + // 是否自动开启 SESSION + 'auto_start' => true, + 'httponly' => true, + 'secure' => false, + ], + + // +---------------------------------------------------------------------- + // | Cookie设置 + // +---------------------------------------------------------------------- + + 'cookie' => [ + // cookie 名称前缀 + 'prefix' => '', + // cookie 保存时间 + 'expire' => 0, + // cookie 保存路径 + 'path' => '/', + // cookie 有效域名 + 'domain' => '', + // cookie 启用安全传输 + 'secure' => false, + // httponly设置 + 'httponly' => '', + // 是否使用 setcookie + 'setcookie' => true, + ], + + // +---------------------------------------------------------------------- + // | 数据库设置 + // +---------------------------------------------------------------------- + + 'database' => [ + // 数据库类型 + 'type' => 'mysql', + // 数据库连接DSN配置 + 'dsn' => '', + // 服务器地址 + 'hostname' => '127.0.0.1', + // 数据库名 + 'database' => '', + // 数据库用户名 + 'username' => 'root', + // 数据库密码 + 'password' => '', + // 数据库连接端口 + 'hostport' => '', + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => '', + // 数据库调试模式 + 'debug' => false, + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 数据集返回类型 + 'resultset_type' => 'array', + // 自动写入时间戳字段 + 'auto_timestamp' => false, + // 时间字段取出后的默认时间格式 + 'datetime_format' => 'Y-m-d H:i:s', + // 是否需要进行SQL性能分析 + 'sql_explain' => false, + // 查询对象 + 'query' => '\\think\\db\\Query', + ], + + //分页配置 + 'paginate' => [ + 'type' => 'bootstrap', + 'var_page' => 'page', + 'list_rows' => 15, + ], + + //控制台配置 + 'console' => [ + 'name' => 'Think Console', + 'version' => '0.1', + 'user' => null, + 'auto_path' => '', + ], + + // 中间件配置 + 'middleware' => [ + 'default_namespace' => 'app\\http\\middleware\\', + ], +]; diff --git a/vendor/topthink/framework/helper.php b/vendor/topthink/framework/helper.php new file mode 100644 index 0000000..72b9e9f --- /dev/null +++ b/vendor/topthink/framework/helper.php @@ -0,0 +1,726 @@ + +// +---------------------------------------------------------------------- + +//------------------------ +// ThinkPHP 助手函数 +//------------------------- + +use think\Container; +use think\Db; +use think\exception\HttpException; +use think\exception\HttpResponseException; +use think\facade\Cache; +use think\facade\Config; +use think\facade\Cookie; +use think\facade\Debug; +use think\facade\Env; +use think\facade\Hook; +use think\facade\Lang; +use think\facade\Log; +use think\facade\Request; +use think\facade\Route; +use think\facade\Session; +use think\facade\Url; +use think\Response; +use think\route\RuleItem; + +if (!function_exists('abort')) { + /** + * 抛出HTTP异常 + * @param integer|Response $code 状态码 或者 Response对象实例 + * @param string $message 错误信息 + * @param array $header 参数 + */ + function abort($code, $message = null, $header = []) + { + if ($code instanceof Response) { + throw new HttpResponseException($code); + } else { + throw new HttpException($code, $message, null, $header); + } + } +} + +if (!function_exists('action')) { + /** + * 调用模块的操作方法 参数格式 [模块/控制器/]操作 + * @param string $url 调用地址 + * @param string|array $vars 调用参数 支持字符串和数组 + * @param string $layer 要调用的控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return mixed + */ + function action($url, $vars = [], $layer = 'controller', $appendSuffix = false) + { + return app()->action($url, $vars, $layer, $appendSuffix); + } +} + +if (!function_exists('app')) { + /** + * 快速获取容器中的实例 支持依赖注入 + * @param string $name 类名或标识 默认获取当前应用实例 + * @param array $args 参数 + * @param bool $newInstance 是否每次创建新的实例 + * @return mixed|\think\App + */ + function app($name = 'think\App', $args = [], $newInstance = false) + { + return Container::get($name, $args, $newInstance); + } +} + +if (!function_exists('behavior')) { + /** + * 执行某个行为(run方法) 支持依赖注入 + * @param mixed $behavior 行为类名或者别名 + * @param mixed $args 参数 + * @return mixed + */ + function behavior($behavior, $args = null) + { + return Hook::exec($behavior, $args); + } +} + +if (!function_exists('bind')) { + /** + * 绑定一个类到容器 + * @access public + * @param string $abstract 类标识、接口 + * @param mixed $concrete 要绑定的类、闭包或者实例 + * @return Container + */ + function bind($abstract, $concrete = null) + { + return Container::getInstance()->bindTo($abstract, $concrete); + } +} + +if (!function_exists('cache')) { + /** + * 缓存管理 + * @param mixed $name 缓存名称,如果为数组表示进行缓存设置 + * @param mixed $value 缓存值 + * @param mixed $options 缓存参数 + * @param string $tag 缓存标签 + * @return mixed + */ + function cache($name, $value = '', $options = null, $tag = null) + { + if (is_array($options)) { + // 缓存操作的同时初始化 + Cache::connect($options); + } elseif (is_array($name)) { + // 缓存初始化 + return Cache::connect($name); + } + + if ('' === $value) { + // 获取缓存 + return 0 === strpos($name, '?') ? Cache::has(substr($name, 1)) : Cache::get($name); + } elseif (is_null($value)) { + // 删除缓存 + return Cache::rm($name); + } + + // 缓存数据 + if (is_array($options)) { + $expire = isset($options['expire']) ? $options['expire'] : null; //修复查询缓存无法设置过期时间 + } else { + $expire = is_numeric($options) ? $options : null; //默认快捷缓存设置过期时间 + } + + if (is_null($tag)) { + return Cache::set($name, $value, $expire); + } else { + return Cache::tag($tag)->set($name, $value, $expire); + } + } +} + +if (!function_exists('call')) { + /** + * 调用反射执行callable 支持依赖注入 + * @param mixed $callable 支持闭包等callable写法 + * @param array $args 参数 + * @return mixed + */ + function call($callable, $args = []) + { + return Container::getInstance()->invoke($callable, $args); + } +} + +if (!function_exists('class_basename')) { + /** + * 获取类名(不包含命名空间) + * + * @param string|object $class + * @return string + */ + function class_basename($class) + { + $class = is_object($class) ? get_class($class) : $class; + return basename(str_replace('\\', '/', $class)); + } +} + +if (!function_exists('class_uses_recursive')) { + /** + *获取一个类里所有用到的trait,包括父类的 + * + * @param $class + * @return array + */ + function class_uses_recursive($class) + { + if (is_object($class)) { + $class = get_class($class); + } + + $results = []; + $classes = array_merge([$class => $class], class_parents($class)); + foreach ($classes as $class) { + $results += trait_uses_recursive($class); + } + + return array_unique($results); + } +} + +if (!function_exists('config')) { + /** + * 获取和设置配置参数 + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @return mixed + */ + function config($name = '', $value = null) + { + if (is_null($value) && is_string($name)) { + if ('.' == substr($name, -1)) { + return Config::pull(substr($name, 0, -1)); + } + + return 0 === strpos($name, '?') ? Config::has(substr($name, 1)) : Config::get($name); + } else { + return Config::set($name, $value); + } + } +} + +if (!function_exists('container')) { + /** + * 获取容器对象实例 + * @return Container + */ + function container() + { + return Container::getInstance(); + } +} + +if (!function_exists('controller')) { + /** + * 实例化控制器 格式:[模块/]控制器 + * @param string $name 资源地址 + * @param string $layer 控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return \think\Controller + */ + function controller($name, $layer = 'controller', $appendSuffix = false) + { + return app()->controller($name, $layer, $appendSuffix); + } +} + +if (!function_exists('cookie')) { + /** + * Cookie管理 + * @param string|array $name cookie名称,如果为数组表示进行cookie设置 + * @param mixed $value cookie值 + * @param mixed $option 参数 + * @return mixed + */ + function cookie($name, $value = '', $option = null) + { + if (is_array($name)) { + // 初始化 + Cookie::init($name); + } elseif (is_null($name)) { + // 清除 + Cookie::clear($value); + } elseif ('' === $value) { + // 获取 + return 0 === strpos($name, '?') ? Cookie::has(substr($name, 1), $option) : Cookie::get($name); + } elseif (is_null($value)) { + // 删除 + return Cookie::delete($name); + } else { + // 设置 + return Cookie::set($name, $value, $option); + } + } +} + +if (!function_exists('db')) { + /** + * 实例化数据库类 + * @param string $name 操作的数据表名称(不含前缀) + * @param array|string $config 数据库配置参数 + * @param bool $force 是否强制重新连接 + * @return \think\db\Query + */ + function db($name = '', $config = [], $force = true) + { + return Db::connect($config, $force)->name($name); + } +} + +if (!function_exists('debug')) { + /** + * 记录时间(微秒)和内存使用情况 + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 如果是m 表示统计内存占用 + * @return mixed + */ + function debug($start, $end = '', $dec = 6) + { + if ('' == $end) { + Debug::remark($start); + } else { + return 'm' == $dec ? Debug::getRangeMem($start, $end) : Debug::getRangeTime($start, $end, $dec); + } + } +} + +if (!function_exists('download')) { + /** + * 获取\think\response\Download对象实例 + * @param string $filename 要下载的文件 + * @param string $name 显示文件名 + * @param bool $content 是否为内容 + * @param integer $expire 有效期(秒) + * @return \think\response\Download + */ + function download($filename, $name = '', $content = false, $expire = 360, $openinBrowser = false) + { + return Response::create($filename, 'download')->name($name)->isContent($content)->expire($expire)->openinBrowser($openinBrowser); + } +} + +if (!function_exists('dump')) { + /** + * 浏览器友好的变量输出 + * @param mixed $var 变量 + * @param boolean $echo 是否输出 默认为true 如果为false 则返回输出字符串 + * @param string $label 标签 默认为空 + * @return void|string + */ + function dump($var, $echo = true, $label = null) + { + return Debug::dump($var, $echo, $label); + } +} + +if (!function_exists('env')) { + /** + * 获取环境变量值 + * @access public + * @param string $name 环境变量名(支持二级 .号分割) + * @param string $default 默认值 + * @return mixed + */ + function env($name = null, $default = null) + { + return Env::get($name, $default); + } +} + +if (!function_exists('exception')) { + /** + * 抛出异常处理 + * + * @param string $msg 异常消息 + * @param integer $code 异常代码 默认为0 + * @param string $exception 异常类 + * + * @throws Exception + */ + function exception($msg, $code = 0, $exception = '') + { + $e = $exception ?: '\think\Exception'; + throw new $e($msg, $code); + } +} + +if (!function_exists('halt')) { + /** + * 调试变量并且中断输出 + * @param mixed $var 调试变量或者信息 + */ + function halt($var) + { + dump($var); + + throw new HttpResponseException(new Response); + } +} + +if (!function_exists('input')) { + /** + * 获取输入数据 支持默认值和过滤 + * @param string $key 获取的变量名 + * @param mixed $default 默认值 + * @param string $filter 过滤方法 + * @return mixed + */ + function input($key = '', $default = null, $filter = '') + { + if (0 === strpos($key, '?')) { + $key = substr($key, 1); + $has = true; + } + + if ($pos = strpos($key, '.')) { + // 指定参数来源 + $method = substr($key, 0, $pos); + if (in_array($method, ['get', 'post', 'put', 'patch', 'delete', 'route', 'param', 'request', 'session', 'cookie', 'server', 'env', 'path', 'file'])) { + $key = substr($key, $pos + 1); + } else { + $method = 'param'; + } + } else { + // 默认为自动判断 + $method = 'param'; + } + + if (isset($has)) { + return request()->has($key, $method, $default); + } else { + return request()->$method($key, $default, $filter); + } + } +} + +if (!function_exists('json')) { + /** + * 获取\think\response\Json对象实例 + * @param mixed $data 返回的数据 + * @param integer $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Json + */ + function json($data = [], $code = 200, $header = [], $options = []) + { + return Response::create($data, 'json', $code, $header, $options); + } +} + +if (!function_exists('jsonp')) { + /** + * 获取\think\response\Jsonp对象实例 + * @param mixed $data 返回的数据 + * @param integer $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Jsonp + */ + function jsonp($data = [], $code = 200, $header = [], $options = []) + { + return Response::create($data, 'jsonp', $code, $header, $options); + } +} + +if (!function_exists('lang')) { + /** + * 获取语言变量值 + * @param string $name 语言变量名 + * @param array $vars 动态变量值 + * @param string $lang 语言 + * @return mixed + */ + function lang($name, $vars = [], $lang = '') + { + return Lang::get($name, $vars, $lang); + } +} + +if (!function_exists('model')) { + /** + * 实例化Model + * @param string $name Model名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return \think\Model + */ + function model($name = '', $layer = 'model', $appendSuffix = false) + { + return app()->model($name, $layer, $appendSuffix); + } +} + +if (!function_exists('parse_name')) { + /** + * 字符串命名风格转换 + * type 0 将Java风格转换为C的风格 1 将C风格转换为Java的风格 + * @param string $name 字符串 + * @param integer $type 转换类型 + * @param bool $ucfirst 首字母是否大写(驼峰规则) + * @return string + */ + function parse_name($name, $type = 0, $ucfirst = true) + { + if ($type) { + $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), "_")); + } + } +} + +if (!function_exists('redirect')) { + /** + * 获取\think\response\Redirect对象实例 + * @param mixed $url 重定向地址 支持Url::build方法的地址 + * @param array|integer $params 额外参数 + * @param integer $code 状态码 + * @return \think\response\Redirect + */ + function redirect($url = [], $params = [], $code = 302) + { + if (is_integer($params)) { + $code = $params; + $params = []; + } + + return Response::create($url, 'redirect', $code)->params($params); + } +} + +if (!function_exists('request')) { + /** + * 获取当前Request对象实例 + * @return Request + */ + function request() + { + return app('request'); + } +} + +if (!function_exists('response')) { + /** + * 创建普通 Response 对象实例 + * @param mixed $data 输出数据 + * @param int|string $code 状态码 + * @param array $header 头信息 + * @param string $type + * @return Response + */ + function response($data = '', $code = 200, $header = [], $type = 'html') + { + return Response::create($data, $type, $code, $header); + } +} + +if (!function_exists('route')) { + /** + * 路由注册 + * @param string $rule 路由规则 + * @param mixed $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + function route($rule, $route, $option = [], $pattern = []) + { + return Route::rule($rule, $route, '*', $option, $pattern); + } +} + +if (!function_exists('session')) { + /** + * Session管理 + * @param string|array $name session名称,如果为数组表示进行session设置 + * @param mixed $value session值 + * @param string $prefix 前缀 + * @return mixed + */ + function session($name, $value = '', $prefix = null) + { + if (is_array($name)) { + // 初始化 + Session::init($name); + } elseif (is_null($name)) { + // 清除 + Session::clear($value); + } elseif ('' === $value) { + // 判断或获取 + return 0 === strpos($name, '?') ? Session::has(substr($name, 1), $prefix) : Session::get($name, $prefix); + } elseif (is_null($value)) { + // 删除 + return Session::delete($name, $prefix); + } else { + // 设置 + return Session::set($name, $value, $prefix); + } + } +} + +if (!function_exists('token')) { + /** + * 生成表单令牌 + * @param string $name 令牌名称 + * @param mixed $type 令牌生成方法 + * @return string + */ + function token($name = '__token__', $type = 'md5') + { + $token = Request::token($name, $type); + + return ''; + } +} + +if (!function_exists('trace')) { + /** + * 记录日志信息 + * @param mixed $log log信息 支持字符串和数组 + * @param string $level 日志级别 + * @return array|void + */ + function trace($log = '[think]', $level = 'log') + { + if ('[think]' === $log) { + return Log::getLog(); + } else { + Log::record($log, $level); + } + } +} + +if (!function_exists('trait_uses_recursive')) { + /** + * 获取一个trait里所有引用到的trait + * + * @param string $trait + * @return array + */ + function trait_uses_recursive($trait) + { + $traits = class_uses($trait); + foreach ($traits as $trait) { + $traits += trait_uses_recursive($trait); + } + + return $traits; + } +} + +if (!function_exists('url')) { + /** + * Url生成 + * @param string $url 路由地址 + * @param string|array $vars 变量 + * @param bool|string $suffix 生成的URL后缀 + * @param bool|string $domain 域名 + * @return string + */ + function url($url = '', $vars = '', $suffix = true, $domain = false) + { + return Url::build($url, $vars, $suffix, $domain); + } +} + +if (!function_exists('validate')) { + /** + * 实例化验证器 + * @param string $name 验证器名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return \think\Validate + */ + function validate($name = '', $layer = 'validate', $appendSuffix = false) + { + return app()->validate($name, $layer, $appendSuffix); + } +} + +if (!function_exists('view')) { + /** + * 渲染模板输出 + * @param string $template 模板文件 + * @param array $vars 模板变量 + * @param integer $code 状态码 + * @param callable $filter 内容过滤 + * @return \think\response\View + */ + function view($template = '', $vars = [], $code = 200, $filter = null) + { + return Response::create($template, 'view', $code)->assign($vars)->filter($filter); + } +} + +if (!function_exists('widget')) { + /** + * 渲染输出Widget + * @param string $name Widget名称 + * @param array $data 传入的参数 + * @return mixed + */ + function widget($name, $data = []) + { + $result = app()->action($name, $data, 'widget'); + + if (is_object($result)) { + $result = $result->getContent(); + } + + return $result; + } +} + +if (!function_exists('xml')) { + /** + * 获取\think\response\Xml对象实例 + * @param mixed $data 返回的数据 + * @param integer $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Xml + */ + function xml($data = [], $code = 200, $header = [], $options = []) + { + return Response::create($data, 'xml', $code, $header, $options); + } +} + +if (!function_exists('yaconf')) { + /** + * 获取yaconf配置 + * + * @param string $name 配置参数名 + * @param mixed $default 默认值 + * @return mixed + */ + function yaconf($name, $default = null) + { + return Config::yaconf($name, $default); + } +} diff --git a/vendor/topthink/framework/lang/zh-cn.php b/vendor/topthink/framework/lang/zh-cn.php new file mode 100644 index 0000000..1e05082 --- /dev/null +++ b/vendor/topthink/framework/lang/zh-cn.php @@ -0,0 +1,144 @@ + +// +---------------------------------------------------------------------- + +// 核心中文语言包 +return [ + // 系统错误提示 + 'Undefined variable' => '未定义变量', + 'Undefined index' => '未定义数组索引', + 'Undefined offset' => '未定义数组下标', + 'Parse error' => '语法解析错误', + 'Type error' => '类型错误', + 'Fatal error' => '致命错误', + 'syntax error' => '语法错误', + + // 框架核心错误提示 + 'dispatch type not support' => '不支持的调度类型', + 'method param miss' => '方法参数错误', + 'method not exists' => '方法不存在', + 'function not exists' => '函数不存在', + 'file 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' => '查询表达式错误', + 'order express error' => '排序表达式错误', + 'no data to update' => '没有任何数据需要更新', + 'miss data to insert' => '缺少需要写入的数据', + 'not support data' => '不支持的数据表达式', + '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方法', + 'route pattern error' => '路由变量规则定义错误', + 'route behavior will not support' => '路由行为废弃(使用中间件替代)', + '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} 创建失败!', + + 'The middleware must return Response instance' => '中间件方法必须返回Response对象实例', + 'The queue was exhausted, with no response returned' => '中间件队列为空', + // Validate Error Message + ':attribute require' => ':attribute不能为空', + ':attribute must' => ':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/vendor/topthink/framework/library/think/App.php b/vendor/topthink/framework/library/think/App.php new file mode 100644 index 0000000..692b422 --- /dev/null +++ b/vendor/topthink/framework/library/think/App.php @@ -0,0 +1,979 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ClassNotFoundException; +use think\exception\HttpResponseException; +use think\route\Dispatch; + +/** + * App 应用管理 + */ +class App extends Container +{ + const VERSION = '5.1.41 LTS'; + + /** + * 当前模块路径 + * @var string + */ + protected $modulePath; + + /** + * 应用调试模式 + * @var bool + */ + protected $appDebug = true; + + /** + * 应用开始时间 + * @var float + */ + protected $beginTime; + + /** + * 应用内存初始占用 + * @var integer + */ + protected $beginMem; + + /** + * 应用类库命名空间 + * @var string + */ + protected $namespace = 'app'; + + /** + * 应用类库后缀 + * @var bool + */ + protected $suffix = false; + + /** + * 严格路由检测 + * @var bool + */ + protected $routeMust; + + /** + * 应用类库目录 + * @var string + */ + protected $appPath; + + /** + * 框架目录 + * @var string + */ + protected $thinkPath; + + /** + * 应用根目录 + * @var string + */ + protected $rootPath; + + /** + * 运行时目录 + * @var string + */ + protected $runtimePath; + + /** + * 配置目录 + * @var string + */ + protected $configPath; + + /** + * 路由目录 + * @var string + */ + protected $routePath; + + /** + * 配置后缀 + * @var string + */ + protected $configExt; + + /** + * 应用调度实例 + * @var Dispatch + */ + protected $dispatch; + + /** + * 绑定模块(控制器) + * @var string + */ + protected $bindModule; + + /** + * 初始化 + * @var bool + */ + protected $initialized = false; + + public function __construct($appPath = '') + { + $this->thinkPath = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR; + $this->path($appPath); + } + + /** + * 绑定模块或者控制器 + * @access public + * @param string $bind + * @return $this + */ + public function bind($bind) + { + $this->bindModule = $bind; + return $this; + } + + /** + * 设置应用类库目录 + * @access public + * @param string $path 路径 + * @return $this + */ + public function path($path) + { + $this->appPath = $path ? realpath($path) . DIRECTORY_SEPARATOR : $this->getAppPath(); + + return $this; + } + + /** + * 初始化应用 + * @access public + * @return void + */ + public function initialize() + { + if ($this->initialized) { + return; + } + + $this->initialized = true; + $this->beginTime = microtime(true); + $this->beginMem = memory_get_usage(); + + $this->rootPath = dirname($this->appPath) . DIRECTORY_SEPARATOR; + $this->runtimePath = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR; + $this->routePath = $this->rootPath . 'route' . DIRECTORY_SEPARATOR; + $this->configPath = $this->rootPath . 'config' . DIRECTORY_SEPARATOR; + + static::setInstance($this); + + $this->instance('app', $this); + + // 加载环境变量配置文件 + if (is_file($this->rootPath . '.env')) { + $this->env->load($this->rootPath . '.env'); + } + + $this->configExt = $this->env->get('config_ext', '.php'); + + // 加载惯例配置文件 + $this->config->set(include $this->thinkPath . 'convention.php'); + + // 设置路径环境变量 + $this->env->set([ + 'think_path' => $this->thinkPath, + 'root_path' => $this->rootPath, + 'app_path' => $this->appPath, + 'config_path' => $this->configPath, + 'route_path' => $this->routePath, + 'runtime_path' => $this->runtimePath, + 'extend_path' => $this->rootPath . 'extend' . DIRECTORY_SEPARATOR, + 'vendor_path' => $this->rootPath . 'vendor' . DIRECTORY_SEPARATOR, + ]); + + $this->namespace = $this->env->get('app_namespace', $this->namespace); + $this->env->set('app_namespace', $this->namespace); + + // 注册应用命名空间 + Loader::addNamespace($this->namespace, $this->appPath); + + // 初始化应用 + $this->init(); + + // 开启类名后缀 + $this->suffix = $this->config('app.class_suffix'); + + // 应用调试模式 + $this->appDebug = $this->env->get('app_debug', $this->config('app.app_debug')); + $this->env->set('app_debug', $this->appDebug); + + if (!$this->appDebug) { + ini_set('display_errors', 'Off'); + } elseif (PHP_SAPI != 'cli') { + //重新申请一块比较大的buffer + if (ob_get_level() > 0) { + $output = ob_get_clean(); + } + ob_start(); + if (!empty($output)) { + echo $output; + } + } + + // 注册异常处理类 + if ($this->config('app.exception_handle')) { + Error::setExceptionHandler($this->config('app.exception_handle')); + } + + // 注册根命名空间 + if (!empty($this->config('app.root_namespace'))) { + Loader::addNamespace($this->config('app.root_namespace')); + } + + // 加载composer autofile文件 + Loader::loadComposerAutoloadFiles(); + + // 注册类库别名 + Loader::addClassAlias($this->config->pull('alias')); + + // 数据库配置初始化 + Db::init($this->config->pull('database')); + + // 设置系统时区 + date_default_timezone_set($this->config('app.default_timezone')); + + // 读取语言包 + $this->loadLangPack(); + + // 路由初始化 + $this->routeInit(); + } + + /** + * 初始化应用或模块 + * @access public + * @param string $module 模块名 + * @return void + */ + public function init($module = '') + { + // 定位模块目录 + $module = $module ? $module . DIRECTORY_SEPARATOR : ''; + $path = $this->appPath . $module; + + // 加载初始化文件 + if (is_file($path . 'init.php')) { + include $path . 'init.php'; + } elseif (is_file($this->runtimePath . $module . 'init.php')) { + include $this->runtimePath . $module . 'init.php'; + } else { + // 加载行为扩展文件 + if (is_file($path . 'tags.php')) { + $tags = include $path . 'tags.php'; + if (is_array($tags)) { + $this->hook->import($tags); + } + } + + // 加载公共文件 + if (is_file($path . 'common.php')) { + include_once $path . 'common.php'; + } + + if ('' == $module) { + // 加载系统助手函数 + include $this->thinkPath . 'helper.php'; + } + + // 加载中间件 + if (is_file($path . 'middleware.php')) { + $middleware = include $path . 'middleware.php'; + if (is_array($middleware)) { + $this->middleware->import($middleware); + } + } + + // 注册服务的容器对象实例 + if (is_file($path . 'provider.php')) { + $provider = include $path . 'provider.php'; + if (is_array($provider)) { + $this->bindTo($provider); + } + } + + // 自动读取配置文件 + if (is_dir($path . 'config')) { + $dir = $path . 'config' . DIRECTORY_SEPARATOR; + } elseif (is_dir($this->configPath . $module)) { + $dir = $this->configPath . $module; + } + + $files = isset($dir) ? scandir($dir) : []; + + foreach ($files as $file) { + if ('.' . pathinfo($file, PATHINFO_EXTENSION) === $this->configExt) { + $this->config->load($dir . $file, pathinfo($file, PATHINFO_FILENAME)); + } + } + } + + $this->setModulePath($path); + + if ($module) { + // 对容器中的对象实例进行配置更新 + $this->containerConfigUpdate($module); + } + } + + protected function containerConfigUpdate($module) + { + $config = $this->config->get(); + + // 注册异常处理类 + if ($config['app']['exception_handle']) { + Error::setExceptionHandler($config['app']['exception_handle']); + } + + Db::init($config['database']); + $this->middleware->setConfig($config['middleware']); + $this->route->setConfig($config['app']); + $this->request->init($config['app']); + $this->cookie->init($config['cookie']); + $this->view->init($config['template']); + $this->log->init($config['log']); + $this->session->setConfig($config['session']); + $this->debug->setConfig($config['trace']); + $this->cache->init($config['cache'], true); + + // 加载当前模块语言包 + $this->lang->load($this->appPath . $module . DIRECTORY_SEPARATOR . 'lang' . DIRECTORY_SEPARATOR . $this->request->langset() . '.php'); + + // 模块请求缓存检查 + $this->checkRequestCache( + $config['app']['request_cache'], + $config['app']['request_cache_expire'], + $config['app']['request_cache_except'] + ); + } + + /** + * 执行应用程序 + * @access public + * @return Response + * @throws Exception + */ + public function run() + { + try { + // 初始化应用 + $this->initialize(); + + // 监听app_init + $this->hook->listen('app_init'); + + if ($this->bindModule) { + // 模块/控制器绑定 + $this->route->bind($this->bindModule); + } elseif ($this->config('app.auto_bind_module')) { + // 入口自动绑定 + $name = pathinfo($this->request->baseFile(), PATHINFO_FILENAME); + if ($name && 'index' != $name && is_dir($this->appPath . $name)) { + $this->route->bind($name); + } + } + + // 监听app_dispatch + $this->hook->listen('app_dispatch'); + + $dispatch = $this->dispatch; + + if (empty($dispatch)) { + // 路由检测 + $dispatch = $this->routeCheck()->init(); + } + + // 记录当前调度信息 + $this->request->dispatch($dispatch); + + // 记录路由和请求信息 + if ($this->appDebug) { + $this->log('[ ROUTE ] ' . var_export($this->request->routeInfo(), true)); + $this->log('[ HEADER ] ' . var_export($this->request->header(), true)); + $this->log('[ PARAM ] ' . var_export($this->request->param(), true)); + } + + // 监听app_begin + $this->hook->listen('app_begin'); + + // 请求缓存检查 + $this->checkRequestCache( + $this->config('request_cache'), + $this->config('request_cache_expire'), + $this->config('request_cache_except') + ); + + $data = null; + } catch (HttpResponseException $exception) { + $dispatch = null; + $data = $exception->getResponse(); + } + + $this->middleware->add(function (Request $request, $next) use ($dispatch, $data) { + return is_null($data) ? $dispatch->run() : $data; + }); + + $response = $this->middleware->dispatch($this->request); + + // 监听app_end + $this->hook->listen('app_end', $response); + + return $response; + } + + protected function getRouteCacheKey() + { + if ($this->config->get('route_check_cache_key')) { + $closure = $this->config->get('route_check_cache_key'); + $routeKey = $closure($this->request); + } else { + $routeKey = md5($this->request->baseUrl(true) . ':' . $this->request->method()); + } + + return $routeKey; + } + + protected function loadLangPack() + { + // 读取默认语言 + $this->lang->range($this->config('app.default_lang')); + + if ($this->config('app.lang_switch_on')) { + // 开启多语言机制 检测当前语言 + $this->lang->detect(); + } + + $this->request->setLangset($this->lang->range()); + + // 加载系统语言包 + $this->lang->load([ + $this->thinkPath . 'lang' . DIRECTORY_SEPARATOR . $this->request->langset() . '.php', + $this->appPath . 'lang' . DIRECTORY_SEPARATOR . $this->request->langset() . '.php', + ]); + } + + /** + * 设置当前地址的请求缓存 + * @access public + * @param string $key 缓存标识,支持变量规则 ,例如 item/:name/:id + * @param mixed $expire 缓存有效期 + * @param array $except 缓存排除 + * @param string $tag 缓存标签 + * @return void + */ + public function checkRequestCache($key, $expire = null, $except = [], $tag = null) + { + $cache = $this->request->cache($key, $expire, $except, $tag); + + if ($cache) { + $this->setResponseCache($cache); + } + } + + public function setResponseCache($cache) + { + list($key, $expire, $tag) = $cache; + + if (strtotime($this->request->server('HTTP_IF_MODIFIED_SINCE')) + $expire > $this->request->server('REQUEST_TIME')) { + // 读取缓存 + $response = Response::create()->code(304); + throw new HttpResponseException($response); + } elseif ($this->cache->has($key)) { + list($content, $header) = $this->cache->get($key); + + $response = Response::create($content)->header($header); + throw new HttpResponseException($response); + } + } + + /** + * 设置当前请求的调度信息 + * @access public + * @param Dispatch $dispatch 调度信息 + * @return $this + */ + public function dispatch(Dispatch $dispatch) + { + $this->dispatch = $dispatch; + return $this; + } + + /** + * 记录调试信息 + * @access public + * @param mixed $msg 调试信息 + * @param string $type 信息类型 + * @return void + */ + public function log($msg, $type = 'info') + { + $this->appDebug && $this->log->record($msg, $type); + } + + /** + * 获取配置参数 为空则获取所有配置 + * @access public + * @param string $name 配置参数名(支持二级配置 .号分割) + * @return mixed + */ + public function config($name = '') + { + return $this->config->get($name); + } + + /** + * 路由初始化 导入路由定义规则 + * @access public + * @return void + */ + public function routeInit() + { + // 路由检测 + if (is_dir($this->routePath)) { + $files = glob($this->routePath . '*.php'); + foreach ($files as $file) { + $rules = include $file; + if (is_array($rules)) { + $this->route->import($rules); + } + } + } + + if ($this->route->config('route_annotation')) { + // 自动生成路由定义 + if ($this->appDebug) { + $suffix = $this->route->config('controller_suffix') || $this->route->config('class_suffix'); + $this->build->buildRoute($suffix); + } + + $filename = $this->runtimePath . 'build_route.php'; + + if (is_file($filename)) { + include $filename; + } + } + } + + /** + * URL路由检测(根据PATH_INFO) + * @access public + * @return Dispatch + */ + public function routeCheck() + { + // 检测路由缓存 + if (!$this->appDebug && $this->config->get('route_check_cache')) { + $routeKey = $this->getRouteCacheKey(); + $option = $this->config->get('route_cache_option'); + + if ($option && $this->cache->connect($option)->has($routeKey)) { + return $this->cache->connect($option)->get($routeKey); + } elseif ($this->cache->has($routeKey)) { + return $this->cache->get($routeKey); + } + } + + // 获取应用调度信息 + $path = $this->request->path(); + + // 是否强制路由模式 + $must = !is_null($this->routeMust) ? $this->routeMust : $this->route->config('url_route_must'); + + // 路由检测 返回一个Dispatch对象 + $dispatch = $this->route->check($path, $must); + + if (!empty($routeKey)) { + try { + if ($option) { + $this->cache->connect($option)->tag('route_cache')->set($routeKey, $dispatch); + } else { + $this->cache->tag('route_cache')->set($routeKey, $dispatch); + } + } catch (\Exception $e) { + // 存在闭包的时候缓存无效 + } + } + + return $dispatch; + } + + /** + * 设置应用的路由检测机制 + * @access public + * @param bool $must 是否强制检测路由 + * @return $this + */ + public function routeMust($must = false) + { + $this->routeMust = $must; + return $this; + } + + /** + * 解析模块和类名 + * @access protected + * @param string $name 资源地址 + * @param string $layer 验证层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return array + */ + protected function parseModuleAndClass($name, $layer, $appendSuffix) + { + if (false !== strpos($name, '\\')) { + $class = $name; + $module = $this->request->module(); + } else { + if (strpos($name, '/')) { + list($module, $name) = explode('/', $name, 2); + } else { + $module = $this->request->module(); + } + + $class = $this->parseClass($module, $layer, $name, $appendSuffix); + } + + return [$module, $class]; + } + + /** + * 实例化应用类库 + * @access public + * @param string $name 类名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $common 公共模块名 + * @return object + * @throws ClassNotFoundException + */ + public function create($name, $layer, $appendSuffix = false, $common = 'common') + { + $guid = $name . $layer; + + if ($this->__isset($guid)) { + return $this->__get($guid); + } + + list($module, $class) = $this->parseModuleAndClass($name, $layer, $appendSuffix); + + if (class_exists($class)) { + $object = $this->__get($class); + } else { + $class = str_replace('\\' . $module . '\\', '\\' . $common . '\\', $class); + if (class_exists($class)) { + $object = $this->__get($class); + } else { + throw new ClassNotFoundException('class not exists:' . $class, $class); + } + } + + $this->__set($guid, $class); + + return $object; + } + + /** + * 实例化(分层)模型 + * @access public + * @param string $name Model名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $common 公共模块名 + * @return Model + * @throws ClassNotFoundException + */ + public function model($name = '', $layer = 'model', $appendSuffix = false, $common = 'common') + { + return $this->create($name, $layer, $appendSuffix, $common); + } + + /** + * 实例化(分层)控制器 格式:[模块名/]控制器名 + * @access public + * @param string $name 资源地址 + * @param string $layer 控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $empty 空控制器名称 + * @return object + * @throws ClassNotFoundException + */ + public function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '') + { + list($module, $class) = $this->parseModuleAndClass($name, $layer, $appendSuffix); + + if (class_exists($class)) { + return $this->make($class, true); + } elseif ($empty && class_exists($emptyClass = $this->parseClass($module, $layer, $empty, $appendSuffix))) { + return $this->make($emptyClass, true); + } + + throw new ClassNotFoundException('class not exists:' . $class, $class); + } + + /** + * 实例化验证类 格式:[模块名/]验证器名 + * @access public + * @param string $name 资源地址 + * @param string $layer 验证层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $common 公共模块名 + * @return Validate + * @throws ClassNotFoundException + */ + public function validate($name = '', $layer = 'validate', $appendSuffix = false, $common = 'common') + { + $name = $name ?: $this->config('default_validate'); + + if (empty($name)) { + return new Validate; + } + + return $this->create($name, $layer, $appendSuffix, $common); + } + + /** + * 数据库初始化 + * @access public + * @param mixed $config 数据库配置 + * @param bool|string $name 连接标识 true 强制重新连接 + * @return \think\db\Query + */ + public function db($config = [], $name = false) + { + return Db::connect($config, $name); + } + + /** + * 远程调用模块的操作方法 参数格式 [模块/控制器/]操作 + * @access public + * @param string $url 调用地址 + * @param string|array $vars 调用参数 支持字符串和数组 + * @param string $layer 要调用的控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return mixed + * @throws ClassNotFoundException + */ + public function action($url, $vars = [], $layer = 'controller', $appendSuffix = false) + { + $info = pathinfo($url); + $action = $info['basename']; + $module = '.' != $info['dirname'] ? $info['dirname'] : $this->request->controller(); + $class = $this->controller($module, $layer, $appendSuffix); + + if (is_scalar($vars)) { + if (strpos($vars, '=')) { + parse_str($vars, $vars); + } else { + $vars = [$vars]; + } + } + + return $this->invokeMethod([$class, $action . $this->config('action_suffix')], $vars); + } + + /** + * 解析应用类的类名 + * @access public + * @param string $module 模块名 + * @param string $layer 层名 controller model ... + * @param string $name 类名 + * @param bool $appendSuffix + * @return string + */ + public function parseClass($module, $layer, $name, $appendSuffix = false) + { + $name = str_replace(['/', '.'], '\\', $name); + $array = explode('\\', $name); + $class = Loader::parseName(array_pop($array), 1) . ($this->suffix || $appendSuffix ? ucfirst($layer) : ''); + $path = $array ? implode('\\', $array) . '\\' : ''; + + return $this->namespace . '\\' . ($module ? $module . '\\' : '') . $layer . '\\' . $path . $class; + } + + /** + * 获取框架版本 + * @access public + * @return string + */ + public function version() + { + return static::VERSION; + } + + /** + * 是否为调试模式 + * @access public + * @return bool + */ + public function isDebug() + { + return $this->appDebug; + } + + /** + * 获取模块路径 + * @access public + * @return string + */ + public function getModulePath() + { + return $this->modulePath; + } + + /** + * 设置模块路径 + * @access public + * @param string $path 路径 + * @return void + */ + public function setModulePath($path) + { + $this->modulePath = $path; + $this->env->set('module_path', $path); + } + + /** + * 获取应用根目录 + * @access public + * @return string + */ + public function getRootPath() + { + return $this->rootPath; + } + + /** + * 获取应用类库目录 + * @access public + * @return string + */ + public function getAppPath() + { + if (is_null($this->appPath)) { + $this->appPath = Loader::getRootPath() . 'application' . DIRECTORY_SEPARATOR; + } + + return $this->appPath; + } + + /** + * 获取应用运行时目录 + * @access public + * @return string + */ + public function getRuntimePath() + { + return $this->runtimePath; + } + + /** + * 获取核心框架目录 + * @access public + * @return string + */ + public function getThinkPath() + { + return $this->thinkPath; + } + + /** + * 获取路由目录 + * @access public + * @return string + */ + public function getRoutePath() + { + return $this->routePath; + } + + /** + * 获取应用配置目录 + * @access public + * @return string + */ + public function getConfigPath() + { + return $this->configPath; + } + + /** + * 获取配置后缀 + * @access public + * @return string + */ + public function getConfigExt() + { + return $this->configExt; + } + + /** + * 获取应用类库命名空间 + * @access public + * @return string + */ + public function getNamespace() + { + return $this->namespace; + } + + /** + * 设置应用类库命名空间 + * @access public + * @param string $namespace 命名空间名称 + * @return $this + */ + public function setNamespace($namespace) + { + $this->namespace = $namespace; + return $this; + } + + /** + * 是否启用类库后缀 + * @access public + * @return bool + */ + public function getSuffix() + { + return $this->suffix; + } + + /** + * 获取应用开启时间 + * @access public + * @return float + */ + public function getBeginTime() + { + return $this->beginTime; + } + + /** + * 获取应用初始内存占用 + * @access public + * @return integer + */ + public function getBeginMem() + { + return $this->beginMem; + } + +} diff --git a/vendor/topthink/framework/library/think/Build.php b/vendor/topthink/framework/library/think/Build.php new file mode 100644 index 0000000..7a531d7 --- /dev/null +++ b/vendor/topthink/framework/library/think/Build.php @@ -0,0 +1,415 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Build +{ + /** + * 应用对象 + * @var App + */ + protected $app; + + /** + * 应用目录 + * @var string + */ + protected $basePath; + + public function __construct(App $app) + { + $this->app = $app; + $this->basePath = $this->app->getAppPath(); + } + + /** + * 根据传入的build资料创建目录和文件 + * @access public + * @param array $build build列表 + * @param string $namespace 应用类库命名空间 + * @param bool $suffix 类库后缀 + * @return void + */ + public function run(array $build = [], $namespace = 'app', $suffix = false) + { + // 锁定 + $lockfile = $this->basePath . 'build.lock'; + + if (is_writable($lockfile)) { + return; + } elseif (!touch($lockfile)) { + throw new Exception('应用目录[' . $this->basePath . ']不可写,目录无法自动生成!
              请手动生成项目目录~', 10006); + } + + foreach ($build as $module => $list) { + if ('__dir__' == $module) { + // 创建目录列表 + $this->buildDir($list); + } elseif ('__file__' == $module) { + // 创建文件列表 + $this->buildFile($list); + } else { + // 创建模块 + $this->module($module, $list, $namespace, $suffix); + } + } + + // 解除锁定 + unlink($lockfile); + } + + /** + * 创建目录 + * @access protected + * @param array $list 目录列表 + * @return void + */ + protected function buildDir($list) + { + foreach ($list as $dir) { + $this->checkDirBuild($this->basePath . $dir); + } + } + + /** + * 创建文件 + * @access protected + * @param array $list 文件列表 + * @return void + */ + protected function buildFile($list) + { + foreach ($list as $file) { + if (!is_dir($this->basePath . dirname($file))) { + // 创建目录 + mkdir($this->basePath . dirname($file), 0755, true); + } + + if (!is_file($this->basePath . $file)) { + file_put_contents($this->basePath . $file, 'php' == pathinfo($file, PATHINFO_EXTENSION) ? "basePath . $module)) { + // 创建模块目录 + mkdir($this->basePath . $module); + } + + if (basename($this->app->getRuntimePath()) != $module) { + // 创建配置文件和公共文件 + $this->buildCommon($module); + // 创建模块的默认页面 + $this->buildHello($module, $namespace, $suffix); + } + + if (empty($list)) { + // 创建默认的模块目录和文件 + $list = [ + '__file__' => ['common.php'], + '__dir__' => ['controller', 'model', 'view', 'config'], + ]; + } + + // 创建子目录和文件 + foreach ($list as $path => $file) { + $modulePath = $this->basePath . $module . DIRECTORY_SEPARATOR; + if ('__dir__' == $path) { + // 生成子目录 + foreach ($file as $dir) { + $this->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) ? "checkDirBuild(dirname($filename)); + $content = ''; + break; + default: + // 其他文件 + $content = "app->getNameSpace(); + $content = 'app->config('app.url_controller_layer'); + } + + if ($this->app->config('app.app_multi_module')) { + $modules = glob($this->basePath . '*', GLOB_ONLYDIR); + + foreach ($modules as $module) { + $module = basename($module); + + if (in_array($module, $this->app->config('app.deny_module_list'))) { + continue; + } + + $path = $this->basePath . $module . DIRECTORY_SEPARATOR . $layer . DIRECTORY_SEPARATOR; + $content .= $this->buildDirRoute($path, $namespace, $module, $suffix, $layer); + } + } else { + $path = $this->basePath . $layer . DIRECTORY_SEPARATOR; + $content .= $this->buildDirRoute($path, $namespace, '', $suffix, $layer); + } + + $filename = $this->app->getRuntimePath() . 'build_route.php'; + file_put_contents($filename, $content); + + return $filename; + } + + /** + * 生成子目录控制器类的路由规则 + * @access protected + * @param string $path 控制器目录 + * @param string $namespace 应用命名空间 + * @param string $module 模块 + * @param bool $suffix 类库后缀 + * @param string $layer 控制器层目录名 + * @return string + */ + protected function buildDirRoute($path, $namespace, $module, $suffix, $layer) + { + $content = ''; + $controllers = glob($path . '*.php'); + + foreach ($controllers as $controller) { + $controller = basename($controller, '.php'); + + $class = new \ReflectionClass($namespace . '\\' . ($module ? $module . '\\' : '') . $layer . '\\' . $controller); + + if (strpos($layer, '\\')) { + // 多级控制器 + $level = str_replace(DIRECTORY_SEPARATOR, '.', substr($layer, 11)); + $controller = $level . '.' . $controller; + $length = strlen(strstr($layer, '\\', true)); + } else { + $length = strlen($layer); + } + + if ($suffix) { + $controller = substr($controller, 0, -$length); + } + + $content .= $this->getControllerRoute($class, $module, $controller); + } + + $subDir = glob($path . '*', GLOB_ONLYDIR); + + foreach ($subDir as $dir) { + $content .= $this->buildDirRoute($dir . DIRECTORY_SEPARATOR, $namespace, $module, $suffix, $layer . '\\' . basename($dir)); + } + + return $content; + } + + /** + * 生成控制器类的路由规则 + * @access protected + * @param string $class 控制器完整类名 + * @param string $module 模块名 + * @param string $controller 控制器名 + * @return string + */ + protected function getControllerRoute($class, $module, $controller) + { + $content = ''; + $comment = $class->getDocComment(); + + if (false !== strpos($comment, '@route(')) { + $comment = $this->parseRouteComment($comment); + $route = ($module ? $module . '/' : '') . $controller; + $comment = preg_replace('/route\(\s?([\'\"][\-\_\/\:\<\>\?\$\[\]\w]+[\'\"])\s?\)/is', 'Route::resource(\1,\'' . $route . '\')', $comment); + $content .= PHP_EOL . $comment; + } elseif (false !== strpos($comment, '@alias(')) { + $comment = $this->parseRouteComment($comment, '@alias('); + $route = ($module ? $module . '/' : '') . $controller; + $comment = preg_replace('/alias\(\s?([\'\"][\-\_\/\w]+[\'\"])\s?\)/is', 'Route::alias(\1,\'' . $route . '\')', $comment); + $content .= PHP_EOL . $comment; + } + + $methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC); + + foreach ($methods as $method) { + $comment = $this->getMethodRouteComment($module, $controller, $method); + if ($comment) { + $content .= PHP_EOL . $comment; + } + } + + return $content; + } + + /** + * 解析路由注释 + * @access protected + * @param string $comment + * @param string $tag + * @return string + */ + protected function parseRouteComment($comment, $tag = '@route(') + { + $comment = substr($comment, 3, -2); + $comment = explode(PHP_EOL, substr(strstr(trim($comment), $tag), 1)); + $comment = array_map(function ($item) {return trim(trim($item), ' \t*');}, $comment); + + if (count($comment) > 1) { + $key = array_search('', $comment); + $comment = array_slice($comment, 0, false === $key ? 1 : $key); + } + + $comment = implode(PHP_EOL . "\t", $comment) . ';'; + + if (strpos($comment, '{')) { + $comment = preg_replace_callback('/\{\s?.*?\s?\}/s', function ($matches) { + return false !== strpos($matches[0], '"') ? '[' . substr(var_export(json_decode($matches[0], true), true), 7, -1) . ']' : $matches[0]; + }, $comment); + } + return $comment; + } + + /** + * 获取方法的路由注释 + * @access protected + * @param string $module 模块 + * @param string $controller 控制器名 + * @param \ReflectMethod $reflectMethod + * @return string|void + */ + protected function getMethodRouteComment($module, $controller, $reflectMethod) + { + $comment = $reflectMethod->getDocComment(); + + if (false !== strpos($comment, '@route(')) { + $comment = $this->parseRouteComment($comment); + $action = $reflectMethod->getName(); + + if ($suffix = $this->app->config('app.action_suffix')) { + $action = substr($action, 0, -strlen($suffix)); + } + + $route = ($module ? $module . '/' : '') . $controller . '/' . $action; + $comment = preg_replace('/route\s?\(\s?([\'\"][\-\_\/\:\<\>\?\$\[\]\w]+[\'\"])\s?\,?\s?[\'\"]?(\w+?)[\'\"]?\s?\)/is', 'Route::\2(\1,\'' . $route . '\')', $comment); + $comment = preg_replace('/route\s?\(\s?([\'\"][\-\_\/\:\<\>\?\$\[\]\w]+[\'\"])\s?\)/is', 'Route::rule(\1,\'' . $route . '\')', $comment); + + return $comment; + } + } + + /** + * 创建模块的欢迎页面 + * @access protected + * @param string $module 模块名 + * @param string $namespace 应用类库命名空间 + * @param bool $suffix 类库后缀 + * @return void + */ + protected function buildHello($module, $namespace, $suffix = false) + { + $filename = $this->basePath . ($module ? $module . DIRECTORY_SEPARATOR : '') . 'controller' . DIRECTORY_SEPARATOR . 'Index' . ($suffix ? 'Controller' : '') . '.php'; + if (!is_file($filename)) { + $content = file_get_contents($this->app->getThinkPath() . 'tpl' . DIRECTORY_SEPARATOR . 'default_index.tpl'); + $content = str_replace(['{$app}', '{$module}', '{layer}', '{$suffix}'], [$namespace, $module ? $module . '\\' : '', 'controller', $suffix ? 'Controller' : ''], $content); + $this->checkDirBuild(dirname($filename)); + + file_put_contents($filename, $content); + } + } + + /** + * 创建模块的公共文件 + * @access protected + * @param string $module 模块名 + * @return void + */ + protected function buildCommon($module) + { + $filename = $this->app->getConfigPath() . ($module ? $module . DIRECTORY_SEPARATOR : '') . 'app.php'; + $this->checkDirBuild(dirname($filename)); + + if (!is_file($filename)) { + file_put_contents($filename, "basePath . ($module ? $module . DIRECTORY_SEPARATOR : '') . 'common.php'; + + if (!is_file($filename)) { + file_put_contents($filename, " +// +---------------------------------------------------------------------- + +namespace think; + +use think\cache\Driver; + +/** + * Class Cache + * + * @package think + * + * @mixin Driver + * @mixin \think\cache\driver\File + */ +class Cache +{ + /** + * 缓存实例 + * @var array + */ + protected $instance = []; + + /** + * 缓存配置 + * @var array + */ + protected $config = []; + + /** + * 操作句柄 + * @var object + */ + protected $handler; + + public function __construct(array $config = []) + { + $this->config = $config; + $this->init($config); + } + + /** + * 连接缓存 + * @access public + * @param array $options 配置数组 + * @param bool|string $name 缓存连接标识 true 强制重新连接 + * @return Driver + */ + public function connect(array $options = [], $name = false) + { + if (false === $name) { + $name = md5(serialize($options)); + } + + if (true === $name || !isset($this->instance[$name])) { + $type = !empty($options['type']) ? $options['type'] : 'File'; + + if (true === $name) { + $name = md5(serialize($options)); + } + + $this->instance[$name] = Loader::factory($type, '\\think\\cache\\driver\\', $options); + } + + return $this->instance[$name]; + } + + /** + * 自动初始化缓存 + * @access public + * @param array $options 配置数组 + * @param bool $force 强制更新 + * @return Driver + */ + public function init(array $options = [], $force = false) + { + if (is_null($this->handler) || $force) { + + if ('complex' == $options['type']) { + $default = $options['default']; + $options = isset($options[$default['type']]) ? $options[$default['type']] : $default; + } + + $this->handler = $this->connect($options); + } + + return $this->handler; + } + + public static function __make(Config $config) + { + return new static($config->pull('cache')); + } + + public function getConfig() + { + return $this->config; + } + + public function setConfig(array $config) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 切换缓存类型 需要配置 cache.type 为 complex + * @access public + * @param string $name 缓存标识 + * @return Driver + */ + public function store($name = '') + { + if ('' !== $name && 'complex' == $this->config['type']) { + return $this->connect($this->config[$name], strtolower($name)); + } + + return $this->init(); + } + + public function __call($method, $args) + { + return call_user_func_array([$this->init(), $method], $args); + } + +} diff --git a/vendor/topthink/framework/library/think/Collection.php b/vendor/topthink/framework/library/think/Collection.php new file mode 100644 index 0000000..d7454ec --- /dev/null +++ b/vendor/topthink/framework/library/think/Collection.php @@ -0,0 +1,552 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use ArrayAccess; +use ArrayIterator; +use Countable; +use IteratorAggregate; +use JsonSerializable; + +class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable +{ + /** + * 数据集数据 + * @var array + */ + protected $items = []; + + public function __construct($items = []) + { + $this->items = $this->convertToArray($items); + } + + public static function make($items = []) + { + return new static($items); + } + + /** + * 是否为空 + * @access public + * @return bool + */ + public function isEmpty() + { + return empty($this->items); + } + + public function toArray() + { + return array_map(function ($value) { + return ($value instanceof Model || $value instanceof self) ? $value->toArray() : $value; + }, $this->items); + } + + public function all() + { + return $this->items; + } + + /** + * 合并数组 + * + * @access public + * @param mixed $items + * @return static + */ + public function merge($items) + { + return new static(array_merge($this->items, $this->convertToArray($items))); + } + + /** + * 交换数组中的键和值 + * + * @access public + * @return static + */ + public function flip() + { + return new static(array_flip($this->items)); + } + + /** + * 按指定键整理数据 + * + * @access public + * @param mixed $items 数据 + * @param string $indexKey 键名 + * @return array + */ + public function dictionary($items = null, &$indexKey = null) + { + if ($items instanceof self || $items instanceof Paginator) { + $items = $items->all(); + } + + $items = is_null($items) ? $this->items : $items; + + if ($items && empty($indexKey)) { + $indexKey = is_array($items[0]) ? 'id' : $items[0]->getPk(); + } + + if (isset($indexKey) && is_string($indexKey)) { + return array_column($items, null, $indexKey); + } + + return $items; + } + + /** + * 比较数组,返回差集 + * + * @access public + * @param mixed $items 数据 + * @param string $indexKey 指定比较的键名 + * @return static + */ + public function diff($items, $indexKey = null) + { + if ($this->isEmpty() || is_scalar($this->items[0])) { + return new static(array_diff($this->items, $this->convertToArray($items))); + } + + $diff = []; + $dictionary = $this->dictionary($items, $indexKey); + + if (is_string($indexKey)) { + foreach ($this->items as $item) { + if (!isset($dictionary[$item[$indexKey]])) { + $diff[] = $item; + } + } + } + + return new static($diff); + } + + /** + * 比较数组,返回交集 + * + * @access public + * @param mixed $items 数据 + * @param string $indexKey 指定比较的键名 + * @return static + */ + public function intersect($items, $indexKey = null) + { + if ($this->isEmpty() || is_scalar($this->items[0])) { + return new static(array_diff($this->items, $this->convertToArray($items))); + } + + $intersect = []; + $dictionary = $this->dictionary($items, $indexKey); + + if (is_string($indexKey)) { + foreach ($this->items as $item) { + if (isset($dictionary[$item[$indexKey]])) { + $intersect[] = $item; + } + } + } + + return new static($intersect); + } + + /** + * 返回数组中所有的键名 + * + * @access public + * @return array + */ + public function keys() + { + $current = current($this->items); + + if (is_scalar($current)) { + $array = $this->items; + } elseif (is_array($current)) { + $array = $current; + } else { + $array = $current->toArray(); + } + + return array_keys($array); + } + + /** + * 删除数组的最后一个元素(出栈) + * + * @access public + * @return mixed + */ + public function pop() + { + return array_pop($this->items); + } + + /** + * 通过使用用户自定义函数,以字符串返回数组 + * + * @access public + * @param callable $callback + * @param mixed $initial + * @return mixed + */ + public function reduce(callable $callback, $initial = null) + { + return array_reduce($this->items, $callback, $initial); + } + + /** + * 以相反的顺序返回数组。 + * + * @access public + * @return static + */ + public function reverse() + { + return new static(array_reverse($this->items)); + } + + /** + * 删除数组中首个元素,并返回被删除元素的值 + * + * @access public + * @return mixed + */ + public function shift() + { + return array_shift($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 int $size + * @param bool $preserveKeys + * @return static + */ + public function chunk($size, $preserveKeys = false) + { + $chunks = []; + + foreach (array_chunk($this->items, $size, $preserveKeys) as $chunk) { + $chunks[] = new static($chunk); + } + + return new static($chunks); + } + + /** + * 在数组开头插入一个元素 + * @access public + * @param mixed $value + * @param mixed $key + * @return void + */ + public function unshift($value, $key = null) + { + if (is_null($key)) { + array_unshift($this->items, $value); + } else { + $this->items = [$key => $value] + $this->items; + } + } + + /** + * 给每个元素执行个回调 + * + * @access public + * @param callable $callback + * @return $this + */ + public function each(callable $callback) + { + foreach ($this->items as $key => $item) { + $result = $callback($item, $key); + + if (false === $result) { + break; + } elseif (!is_object($item)) { + $this->items[$key] = $result; + } + } + + return $this; + } + + /** + * 用回调函数处理数组中的元素 + * @access public + * @param callable|null $callback + * @return static + */ + public function map(callable $callback) + { + return new static(array_map($callback, $this->items)); + } + + /** + * 用回调函数过滤数组中的元素 + * @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)); + } + + /** + * 根据字段条件过滤数组中的元素 + * @access public + * @param string $field 字段名 + * @param mixed $operator 操作符 + * @param mixed $value 数据 + * @return static + */ + public function where($field, $operator, $value = null) + { + if (is_null($value)) { + $value = $operator; + $operator = '='; + } + + return $this->filter(function ($data) use ($field, $operator, $value) { + if (strpos($field, '.')) { + list($field, $relation) = explode('.', $field); + + $result = isset($data[$field][$relation]) ? $data[$field][$relation] : null; + } else { + $result = isset($data[$field]) ? $data[$field] : null; + } + + switch (strtolower($operator)) { + case '===': + return $result === $value; + case '!==': + return $result !== $value; + case '!=': + case '<>': + return $result != $value; + case '>': + return $result > $value; + case '>=': + return $result >= $value; + case '<': + return $result < $value; + case '<=': + return $result <= $value; + case 'like': + return is_string($result) && false !== strpos($result, $value); + case 'not like': + return is_string($result) && false === strpos($result, $value); + case 'in': + return is_scalar($result) && in_array($result, $value, true); + case 'not in': + return is_scalar($result) && !in_array($result, $value, true); + case 'between': + list($min, $max) = is_string($value) ? explode(',', $value) : $value; + return is_scalar($result) && $result >= $min && $result <= $max; + case 'not between': + list($min, $max) = is_string($value) ? explode(',', $value) : $value; + return is_scalar($result) && $result > $max || $result < $min; + case '==': + case '=': + default: + return $result == $value; + } + }); + } + + /** + * 返回数据中指定的一列 + * @access public + * @param mixed $columnKey 键名 + * @param mixed $indexKey 作为索引值的列 + * @return array + */ + public function column($columnKey, $indexKey = null) + { + return array_column($this->toArray(), $columnKey, $indexKey); + } + + /** + * 对数组排序 + * + * @access public + * @param callable|null $callback + * @return static + */ + public function sort(callable $callback = null) + { + $items = $this->items; + + $callback = $callback ?: function ($a, $b) { + return $a == $b ? 0 : (($a < $b) ? -1 : 1); + + }; + + uasort($items, $callback); + + return new static($items); + } + + /** + * 指定字段排序 + * @access public + * @param string $field 排序字段 + * @param string $order 排序 + * @param bool $intSort 是否为数字排序 + * @return $this + */ + public function order($field, $order = null, $intSort = true) + { + return $this->sort(function ($a, $b) use ($field, $order, $intSort) { + $fieldA = isset($a[$field]) ? $a[$field] : null; + $fieldB = isset($b[$field]) ? $b[$field] : null; + + if ($intSort) { + return 'desc' == strtolower($order) ? $fieldB >= $fieldA : $fieldA >= $fieldB; + } else { + return 'desc' == strtolower($order) ? strcmp($fieldB, $fieldA) : strcmp($fieldA, $fieldB); + } + }); + } + + /** + * 将数组打乱 + * + * @access public + * @return static + */ + public function shuffle() + { + $items = $this->items; + + shuffle($items); + + return new static($items); + } + + /** + * 截取数组 + * + * @access public + * @param int $offset + * @param int $length + * @param bool $preserveKeys + * @return static + */ + public function slice($offset, $length = null, $preserveKeys = false) + { + return new static(array_slice($this->items, $offset, $length, $preserveKeys)); + } + + // ArrayAccess + public function offsetExists($offset) + { + return array_key_exists($offset, $this->items); + } + + public function offsetGet($offset) + { + return $this->items[$offset]; + } + + public function offsetSet($offset, $value) + { + if (is_null($offset)) { + $this->items[] = $value; + } else { + $this->items[$offset] = $value; + } + } + + public function offsetUnset($offset) + { + unset($this->items[$offset]); + } + + //Countable + public function count() + { + return count($this->items); + } + + //IteratorAggregate + public function getIterator() + { + return new ArrayIterator($this->items); + } + + //JsonSerializable + public function jsonSerialize() + { + return $this->toArray(); + } + + /** + * 转换当前数据集为JSON字符串 + * @access public + * @param integer $options json参数 + * @return string + */ + public function toJson($options = JSON_UNESCAPED_UNICODE) + { + return json_encode($this->toArray(), $options); + } + + public function __toString() + { + return $this->toJson(); + } + + /** + * 转换成数组 + * + * @access public + * @param mixed $items + * @return array + */ + protected function convertToArray($items) + { + if ($items instanceof self) { + return $items->all(); + } + + return (array) $items; + } +} diff --git a/vendor/topthink/framework/library/think/Config.php b/vendor/topthink/framework/library/think/Config.php new file mode 100644 index 0000000..bec6222 --- /dev/null +++ b/vendor/topthink/framework/library/think/Config.php @@ -0,0 +1,398 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use Yaconf; + +class Config implements \ArrayAccess +{ + /** + * 配置参数 + * @var array + */ + protected $config = []; + + /** + * 配置前缀 + * @var string + */ + protected $prefix = 'app'; + + /** + * 配置文件目录 + * @var string + */ + protected $path; + + /** + * 配置文件后缀 + * @var string + */ + protected $ext; + + /** + * 是否支持Yaconf + * @var bool + */ + protected $yaconf; + + /** + * 构造方法 + * @access public + */ + public function __construct($path = '', $ext = '.php') + { + $this->path = $path; + $this->ext = $ext; + $this->yaconf = class_exists('Yaconf'); + } + + public static function __make(App $app) + { + $path = $app->getConfigPath(); + $ext = $app->getConfigExt(); + return new static($path, $ext); + } + + /** + * 设置开启Yaconf + * @access public + * @param bool|string $yaconf 是否使用Yaconf + * @return void + */ + public function setYaconf($yaconf) + { + if ($this->yaconf) { + $this->yaconf = $yaconf; + } + } + + /** + * 设置配置参数默认前缀 + * @access public + * @param string $prefix 前缀 + * @return void + */ + public function setDefaultPrefix($prefix) + { + $this->prefix = $prefix; + } + + /** + * 解析配置文件或内容 + * @access public + * @param string $config 配置文件路径或内容 + * @param string $type 配置解析类型 + * @param string $name 配置名(如设置即表示二级配置) + * @return mixed + */ + public function parse($config, $type = '', $name = '') + { + if (empty($type)) { + $type = pathinfo($config, PATHINFO_EXTENSION); + } + + $object = Loader::factory($type, '\\think\\config\\driver\\', $config); + + return $this->set($object->parse(), $name); + } + + /** + * 加载配置文件(多种格式) + * @access public + * @param string $file 配置文件名 + * @param string $name 一级配置名 + * @return mixed + */ + public function load($file, $name = '') + { + if (is_file($file)) { + $filename = $file; + } elseif (is_file($this->path . $file . $this->ext)) { + $filename = $this->path . $file . $this->ext; + } + + if (isset($filename)) { + return $this->loadFile($filename, $name); + } elseif ($this->yaconf && Yaconf::has($file)) { + return $this->set(Yaconf::get($file), $name); + } + + return $this->config; + } + + /** + * 获取实际的yaconf配置参数 + * @access protected + * @param string $name 配置参数名 + * @return string + */ + protected function getYaconfName($name) + { + if ($this->yaconf && is_string($this->yaconf)) { + return $this->yaconf . '.' . $name; + } + + return $name; + } + + /** + * 获取yaconf配置 + * @access public + * @param string $name 配置参数名 + * @param mixed $default 默认值 + * @return mixed + */ + public function yaconf($name, $default = null) + { + if ($this->yaconf) { + $yaconfName = $this->getYaconfName($name); + + if (Yaconf::has($yaconfName)) { + return Yaconf::get($yaconfName); + } + } + + return $default; + } + + protected function loadFile($file, $name) + { + $name = strtolower($name); + $type = pathinfo($file, PATHINFO_EXTENSION); + + if ('php' == $type) { + return $this->set(include $file, $name); + } elseif ('yaml' == $type && function_exists('yaml_parse_file')) { + return $this->set(yaml_parse_file($file), $name); + } + + return $this->parse($file, $type, $name); + } + + /** + * 检测配置是否存在 + * @access public + * @param string $name 配置参数名(支持多级配置 .号分割) + * @return bool + */ + public function has($name) + { + if (false === strpos($name, '.')) { + $name = $this->prefix . '.' . $name; + } + + return !is_null($this->get($name)); + } + + /** + * 获取一级配置 + * @access public + * @param string $name 一级配置名 + * @return array + */ + public function pull($name) + { + $name = strtolower($name); + + if ($this->yaconf) { + $yaconfName = $this->getYaconfName($name); + + if (Yaconf::has($yaconfName)) { + $config = Yaconf::get($yaconfName); + return isset($this->config[$name]) ? array_merge($this->config[$name], $config) : $config; + } + } + + return isset($this->config[$name]) ? $this->config[$name] : []; + } + + /** + * 获取配置参数 为空则获取所有配置 + * @access public + * @param string $name 配置参数名(支持多级配置 .号分割) + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name = null, $default = null) + { + if ($name && false === strpos($name, '.')) { + $name = $this->prefix . '.' . $name; + } + + // 无参数时获取所有 + if (empty($name)) { + return $this->config; + } + + if ('.' == substr($name, -1)) { + return $this->pull(substr($name, 0, -1)); + } + + if ($this->yaconf) { + $yaconfName = $this->getYaconfName($name); + + if (Yaconf::has($yaconfName)) { + return Yaconf::get($yaconfName); + } + } + + $name = explode('.', $name); + $name[0] = strtolower($name[0]); + $config = $this->config; + + // 按.拆分成多维数组进行判断 + foreach ($name as $val) { + if (isset($config[$val])) { + $config = $config[$val]; + } else { + return $default; + } + } + + return $config; + } + + /** + * 设置配置参数 name为数组则为批量设置 + * @access public + * @param string|array $name 配置参数名(支持三级配置 .号分割) + * @param mixed $value 配置值 + * @return mixed + */ + public function set($name, $value = null) + { + if (is_string($name)) { + if (false === strpos($name, '.')) { + $name = $this->prefix . '.' . $name; + } + + $name = explode('.', $name, 3); + + if (count($name) == 2) { + $this->config[strtolower($name[0])][$name[1]] = $value; + } else { + $this->config[strtolower($name[0])][$name[1]][$name[2]] = $value; + } + + return $value; + } elseif (is_array($name)) { + // 批量设置 + if (!empty($value)) { + if (isset($this->config[$value])) { + $result = array_merge($this->config[$value], $name); + } else { + $result = $name; + } + + $this->config[$value] = $result; + } else { + $result = $this->config = array_merge($this->config, $name); + } + } else { + // 为空直接返回 已有配置 + $result = $this->config; + } + + return $result; + } + + /** + * 移除配置 + * @access public + * @param string $name 配置参数名(支持三级配置 .号分割) + * @return void + */ + public function remove($name) + { + if (false === strpos($name, '.')) { + $name = $this->prefix . '.' . $name; + } + + $name = explode('.', $name, 3); + + if (count($name) == 2) { + unset($this->config[strtolower($name[0])][$name[1]]); + } else { + unset($this->config[strtolower($name[0])][$name[1]][$name[2]]); + } + } + + /** + * 重置配置参数 + * @access public + * @param string $prefix 配置前缀名 + * @return void + */ + public function reset($prefix = '') + { + if ('' === $prefix) { + $this->config = []; + } else { + $this->config[$prefix] = []; + } + } + + /** + * 设置配置 + * @access public + * @param string $name 参数名 + * @param mixed $value 值 + */ + public function __set($name, $value) + { + return $this->set($name, $value); + } + + /** + * 获取配置参数 + * @access public + * @param string $name 参数名 + * @return mixed + */ + public function __get($name) + { + return $this->get($name); + } + + /** + * 检测是否存在参数 + * @access public + * @param string $name 参数名 + * @return bool + */ + public function __isset($name) + { + return $this->has($name); + } + + // ArrayAccess + public function offsetSet($name, $value) + { + $this->set($name, $value); + } + + public function offsetExists($name) + { + return $this->has($name); + } + + public function offsetUnset($name) + { + $this->remove($name); + } + + public function offsetGet($name) + { + return $this->get($name); + } +} diff --git a/vendor/topthink/framework/library/think/Console.php b/vendor/topthink/framework/library/think/Console.php new file mode 100644 index 0000000..22f3e2c --- /dev/null +++ b/vendor/topthink/framework/library/think/Console.php @@ -0,0 +1,829 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\console\Command; +use think\console\command\Help as HelpCommand; +use think\console\Input; +use think\console\input\Argument as InputArgument; +use think\console\input\Definition as InputDefinition; +use think\console\input\Option as InputOption; +use think\console\Output; +use think\console\output\driver\Buffer; + +class Console +{ + + private $name; + private $version; + + /** @var Command[] */ + private $commands = []; + + private $wantHelps = false; + + private $catchExceptions = true; + private $autoExit = true; + private $definition; + private $defaultCommand; + + private static $defaultCommands = [ + 'help' => "think\\console\\command\\Help", + 'list' => "think\\console\\command\\Lists", + 'build' => "think\\console\\command\\Build", + 'clear' => "think\\console\\command\\Clear", + 'make:command' => "think\\console\\command\\make\\Command", + 'make:controller' => "think\\console\\command\\make\\Controller", + 'make:model' => "think\\console\\command\\make\\Model", + 'make:middleware' => "think\\console\\command\\make\\Middleware", + 'make:validate' => "think\\console\\command\\make\\Validate", + 'optimize:autoload' => "think\\console\\command\\optimize\\Autoload", + 'optimize:config' => "think\\console\\command\\optimize\\Config", + 'optimize:schema' => "think\\console\\command\\optimize\\Schema", + 'optimize:route' => "think\\console\\command\\optimize\\Route", + 'run' => "think\\console\\command\\RunServer", + 'version' => "think\\console\\command\\Version", + 'route:list' => "think\\console\\command\\RouteList", + ]; + + /** + * 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(); + } + + /** + * 设置执行用户 + * @param $user + */ + public function setUser($user) + { + if (DIRECTORY_SEPARATOR == '\\') { + return; + } + + $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) { + $config = Container::get('config')->pull('console'); + $console = new self($config['name'], $config['version'], $config['user']); + + $commands = $console->getDefinedCommands($config); + + // 添加指令集 + $console->addCommands($commands); + } + + if ($run) { + // 运行 + return $console->run(); + } else { + return $console; + } + } + + /** + * @access public + * @param array $config + * @return array + */ + public function getDefinedCommands(array $config = []) + { + $commands = self::$defaultCommands; + + if (!empty($config['auto_path']) && is_dir($config['auto_path'])) { + // 自动加载指令类 + $files = scandir($config['auto_path']); + + if (count($files) > 2) { + $beforeClass = get_declared_classes(); + + foreach ($files as $file) { + if (pathinfo($file, PATHINFO_EXTENSION) == 'php') { + include $config['auto_path'] . $file; + } + } + + $afterClass = get_declared_classes(); + $commands = array_merge($commands, array_diff($afterClass, $beforeClass)); + } + } + + $file = Container::get('env')->get('app_path') . 'command.php'; + + if (is_file($file)) { + $appCommands = include $file; + + if (is_array($appCommands)) { + $commands = array_merge($commands, $appCommands); + } + } + + return $commands; + } + + /** + * @access public + * @param string $command + * @param array $parameters + * @param string $driver + * @return Output|Buffer + */ + public static function call($command, array $parameters = [], $driver = 'buffer') + { + $console = self::init(false); + + array_unshift($parameters, $command); + + $input = new Input($parameters); + $output = new Output($driver); + + $console->setCatchExceptions(false); + $console->find($command)->run($input, $output); + + return $output; + } + + /** + * 执行当前的指令 + * @access public + * @return int + * @throws \Exception + * @api + */ + public function run() + { + $input = new Input(); + $output = new Output(); + + $this->configureIO($input, $output); + + try { + $exitCode = $this->doRun($input, $output); + } catch (\Exception $e) { + if (!$this->catchExceptions) { + throw $e; + } + + $output->renderException($e); + + $exitCode = $e->getCode(); + if (is_numeric($exitCode)) { + $exitCode = (int) $exitCode; + if (0 === $exitCode) { + $exitCode = 1; + } + } else { + $exitCode = 1; + } + } + + if ($this->autoExit) { + if ($exitCode > 255) { + $exitCode = 255; + } + + exit($exitCode); + } + + return $exitCode; + } + + /** + * 执行指令 + * @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()); + + return 0; + } + + $name = $this->getCommandName($input); + + if (true === $input->hasParameterOption(['--help', '-h'])) { + if (!$name) { + $name = 'help'; + $input = new Input(['help']); + } else { + $this->wantHelps = true; + } + } + + if (!$name) { + $name = $this->defaultCommand; + $input = new Input([$this->defaultCommand]); + } + + $command = $this->find($name); + + $exitCode = $this->doRunCommand($command, $input, $output); + + return $exitCode; + } + + /** + * 设置输入参数定义 + * @access public + * @param InputDefinition $definition + */ + public function setDefinition(InputDefinition $definition) + { + $this->definition = $definition; + } + + /** + * 获取输入参数定义 + * @access public + * @return InputDefinition The InputDefinition instance + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * Gets the help message. + * @access public + * @return string A help message. + */ + public function getHelp() + { + return $this->getLongVersion(); + } + + /** + * 是否捕获异常 + * @access public + * @param bool $boolean + * @api + */ + public function setCatchExceptions($boolean) + { + $this->catchExceptions = (bool) $boolean; + } + + /** + * 是否自动退出 + * @access public + * @param bool $boolean + * @api + */ + public function setAutoExit($boolean) + { + $this->autoExit = (bool) $boolean; + } + + /** + * 获取名称 + * @access public + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 设置名称 + * @access public + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * 获取版本 + * @access public + * @return string + * @api + */ + public function getVersion() + { + return $this->version; + } + + /** + * 设置版本 + * @access public + * @param string $version + */ + public function setVersion($version) + { + $this->version = $version; + } + + /** + * 获取完整的版本号 + * @access public + * @return string + */ + public function getLongVersion() + { + if ('UNKNOWN' !== $this->getName() && 'UNKNOWN' !== $this->getVersion()) { + return sprintf('%s version %s', $this->getName(), $this->getVersion()); + } + + return 'Console Tool'; + } + + /** + * 注册一个指令 (便于动态创建指令) + * @access public + * @param string $name 指令名 + * @return Command + */ + public function register($name) + { + return $this->add(new Command($name)); + } + + /** + * 添加指令集 + * @access public + * @param array $commands + */ + public function addCommands(array $commands) + { + foreach ($commands as $key => $command) { + if (is_subclass_of($command, "\\think\\console\\Command")) { + // 注册指令 + $this->add($command, is_numeric($key) ? '' : $key); + } + } + } + + /** + * 注册一个指令(对象) + * @access public + * @param mixed $command 指令对象或者指令类名 + * @param string $name 指令名 留空则自动获取 + * @return mixed + */ + public function add($command, $name) + { + if ($name) { + $this->commands[$name] = $command; + return; + } + + if (is_string($command)) { + $command = new $command(); + } + + $command->setConsole($this); + + if (!$command->isEnabled()) { + $command->setConsole(null); + return; + } + + 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))); + } + + $this->commands[$command->getName()] = $command; + + foreach ($command->getAliases() as $alias) { + $this->commands[$alias] = $command; + } + + return $command; + } + + /** + * 获取指令 + * @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)); + } + + $command = $this->commands[$name]; + + if (is_string($command)) { + $command = new $command(); + } + + $command->setConsole($this); + + if ($this->wantHelps) { + $this->wantHelps = false; + + /** @var HelpCommand $helpCommand */ + $helpCommand = $this->get('help'); + $helpCommand->setCommand($command); + + return $helpCommand; + } + + return $command; + } + + /** + * 某个指令是否存在 + * @access public + * @param string $name 指令名称 + * @return bool + */ + public function has($name) + { + return isset($this->commands[$name]); + } + + /** + * 获取所有的命名空间 + * @access public + * @return array + */ + public function getNamespaces() + { + $namespaces = []; + foreach ($this->commands as $name => $command) { + if (is_string($command)) { + $namespaces = array_merge($namespaces, $this->extractAllNamespaces($name)); + } else { + $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName())); + + foreach ($command->getAliases() as $alias) { + $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias)); + } + } + + } + + return array_values(array_unique(array_filter($namespaces))); + } + + /** + * 查找注册命名空间中的名称或缩写。 + * @access public + * @param string $namespace + * @return string + * @throws \InvalidArgumentException + */ + public function findNamespace($namespace) + { + $allNamespaces = $this->getNamespaces(); + $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { + return preg_quote($matches[1]) . '[^:]*'; + }, $namespace); + $namespaces = preg_grep('{^' . $expr . '}', $allNamespaces); + + if (empty($namespaces)) { + $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); + + if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) { + if (1 == count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + + $message .= implode("\n ", $alternatives); + } + + throw new \InvalidArgumentException($message); + } + + $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)))); + } + + return $exact ? $namespace : reset($namespaces); + } + + /** + * 查找指令 + * @access public + * @param string $name 名称或者别名 + * @return Command + * @throws \InvalidArgumentException + */ + public function find($name) + { + $allCommands = array_keys($this->commands); + + $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { + return preg_quote($matches[1]) . '[^:]*'; + }, $name); + + $commands = preg_grep('{^' . $expr . '}', $allCommands); + + if (empty($commands) || count(preg_grep('{^' . $expr . '$}', $commands)) < 1) { + if (false !== $pos = strrpos($name, ':')) { + $this->findNamespace(substr($name, 0, $pos)); + } + + $message = sprintf('Command "%s" is not defined.', $name); + + if ($alternatives = $this->findAlternatives($name, $allCommands)) { + if (1 == count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + $message .= implode("\n ", $alternatives); + } + + throw new \InvalidArgumentException($message); + } + + $exact = in_array($name, $commands, true); + if (count($commands) > 1 && !$exact) { + $suggestions = $this->getAbbreviationSuggestions(array_values($commands)); + + throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions)); + } + + return $this->get($exact ? $name : reset($commands)); + } + + /** + * 获取所有的指令 + * @access public + * @param string $namespace 命名空间 + * @return Command[] + * @api + */ + public function all($namespace = null) + { + 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; + } + } + + return $commands; + } + + /** + * 获取可能的指令名 + * @access public + * @param array $names + * @return array + */ + public static function getAbbreviations($names) + { + $abbrevs = []; + foreach ($names as $name) { + for ($len = strlen($name); $len > 0; --$len) { + $abbrev = substr($name, 0, $len); + $abbrevs[$abbrev][] = $name; + } + } + + return $abbrevs; + } + + /** + * 配置基于用户的参数和选项的输入和输出实例。 + * @access protected + * @param Input $input 输入实例 + * @param Output $output 输出实例 + */ + protected function configureIO(Input $input, Output $output) + { + if (true === $input->hasParameterOption(['--ansi'])) { + $output->setDecorated(true); + } elseif (true === $input->hasParameterOption(['--no-ansi'])) { + $output->setDecorated(false); + } + + if (true === $input->hasParameterOption(['--no-interaction', '-n'])) { + $input->setInteractive(false); + } + + if (true === $input->hasParameterOption(['--quiet', '-q'])) { + $output->setVerbosity(Output::VERBOSITY_QUIET); + } else { + if ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3') || $input->getParameterOption('--verbose') === 3) { + $output->setVerbosity(Output::VERBOSITY_DEBUG); + } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2') || $input->getParameterOption('--verbose') === 2) { + $output->setVerbosity(Output::VERBOSITY_VERY_VERBOSE); + } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1') || $input->hasParameterOption('--verbose') || $input->getParameterOption('--verbose')) { + $output->setVerbosity(Output::VERBOSITY_VERBOSE); + } + } + } + + /** + * 执行指令 + * @access protected + * @param Command $command 指令实例 + * @param Input $input 输入实例 + * @param Output $output 输出实例 + * @return int + * @throws \Exception + */ + protected function doRunCommand(Command $command, Input $input, Output $output) + { + return $command->run($input, $output); + } + + /** + * 获取指令的基础名称 + * @access protected + * @param Input $input + * @return string + */ + protected function getCommandName(Input $input) + { + return $input->getFirstArgument(); + } + + /** + * 获取默认输入定义 + * @access protected + * @return InputDefinition + */ + protected function getDefaultInputDefinition() + { + return new InputDefinition([ + new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), + new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'), + new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this console version'), + new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'), + new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'), + new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'), + new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'), + new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'), + ]); + } + + public static function addDefaultCommands(array $classnames) + { + self::$defaultCommands = array_merge(self::$defaultCommands, $classnames); + } + + /** + * 获取可能的建议 + * @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) : ''); + } + + /** + * 返回命名空间部分 + * @access public + * @param string $name 指令 + * @param string $limit 部分的命名空间的最大数量 + * @return string + */ + public function extractNamespace($name, $limit = null) + { + $parts = explode(':', $name); + array_pop($parts); + + return implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit)); + } + + /** + * 查找可替代的建议 + * @access private + * @param string $name + * @param array|\Traversable $collection + * @return array + */ + private function findAlternatives($name, $collection) + { + $threshold = 1e3; + $alternatives = []; + + $collectionParts = []; + foreach ($collection as $item) { + $collectionParts[$item] = explode(':', $item); + } + + foreach (explode(':', $name) as $i => $subname) { + foreach ($collectionParts as $collectionName => $parts) { + $exists = isset($alternatives[$collectionName]); + if (!isset($parts[$i]) && $exists) { + $alternatives[$collectionName] += $threshold; + continue; + } elseif (!isset($parts[$i])) { + continue; + } + + $lev = levenshtein($subname, $parts[$i]); + if ($lev <= strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) { + $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev; + } elseif ($exists) { + $alternatives[$collectionName] += $threshold; + } + } + } + + 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 = array_filter($alternatives, function ($lev) use ($threshold) { + return $lev < 2 * $threshold; + }); + asort($alternatives); + + return array_keys($alternatives); + } + + /** + * 设置默认的指令 + * @access public + * @param string $commandName The Command name + */ + public function setDefaultCommand($commandName) + { + $this->defaultCommand = $commandName; + } + + /** + * 返回所有的命名空间 + * @access private + * @param string $name + * @return array + */ + private function extractAllNamespaces($name) + { + $parts = explode(':', $name, -1); + $namespaces = []; + + foreach ($parts as $part) { + if (count($namespaces)) { + $namespaces[] = end($namespaces) . ':' . $part; + } else { + $namespaces[] = $part; + } + } + + return $namespaces; + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['commands'], $data['definition']); + + return $data; + } +} diff --git a/vendor/topthink/framework/library/think/Container.php b/vendor/topthink/framework/library/think/Container.php new file mode 100644 index 0000000..91b32aa --- /dev/null +++ b/vendor/topthink/framework/library/think/Container.php @@ -0,0 +1,618 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use ArrayAccess; +use ArrayIterator; +use Closure; +use Countable; +use InvalidArgumentException; +use IteratorAggregate; +use ReflectionClass; +use ReflectionException; +use ReflectionFunction; +use ReflectionMethod; +use think\exception\ClassNotFoundException; + +/** + * @package think + * @property Build $build + * @property Cache $cache + * @property Config $config + * @property Cookie $cookie + * @property Debug $debug + * @property Env $env + * @property Hook $hook + * @property Lang $lang + * @property Middleware $middleware + * @property Request $request + * @property Response $response + * @property Route $route + * @property Session $session + * @property Template $template + * @property Url $url + * @property Validate $validate + * @property View $view + * @property route\RuleName $rule_name + * @property Log $log + */ +class Container implements ArrayAccess, IteratorAggregate, Countable +{ + /** + * 容器对象实例 + * @var Container + */ + protected static $instance; + + /** + * 容器中的对象实例 + * @var array + */ + protected $instances = []; + + /** + * 容器绑定标识 + * @var array + */ + protected $bind = [ + 'app' => App::class, + 'build' => Build::class, + 'cache' => Cache::class, + 'config' => Config::class, + 'cookie' => Cookie::class, + 'debug' => Debug::class, + 'env' => Env::class, + 'hook' => Hook::class, + 'lang' => Lang::class, + 'log' => Log::class, + 'middleware' => Middleware::class, + 'request' => Request::class, + 'response' => Response::class, + 'route' => Route::class, + 'session' => Session::class, + 'template' => Template::class, + 'url' => Url::class, + 'validate' => Validate::class, + 'view' => View::class, + 'rule_name' => route\RuleName::class, + // 接口依赖注入 + 'think\LoggerInterface' => Log::class, + ]; + + /** + * 容器标识别名 + * @var array + */ + protected $name = []; + + /** + * 获取当前容器的实例(单例) + * @access public + * @return static + */ + public static function getInstance() + { + if (is_null(static::$instance)) { + static::$instance = new static; + } + + return static::$instance; + } + + /** + * 设置当前容器的实例 + * @access public + * @param object $instance + * @return void + */ + public static function setInstance($instance) + { + static::$instance = $instance; + } + + /** + * 获取容器中的对象实例 + * @access public + * @param string $abstract 类名或者标识 + * @param array|true $vars 变量 + * @param bool $newInstance 是否每次创建新的实例 + * @return object + */ + public static function get($abstract, $vars = [], $newInstance = false) + { + return static::getInstance()->make($abstract, $vars, $newInstance); + } + + /** + * 绑定一个类、闭包、实例、接口实现到容器 + * @access public + * @param string $abstract 类标识、接口 + * @param mixed $concrete 要绑定的类、闭包或者实例 + * @return Container + */ + public static function set($abstract, $concrete = null) + { + return static::getInstance()->bindTo($abstract, $concrete); + } + + /** + * 移除容器中的对象实例 + * @access public + * @param string $abstract 类标识、接口 + * @return void + */ + public static function remove($abstract) + { + return static::getInstance()->delete($abstract); + } + + /** + * 清除容器中的对象实例 + * @access public + * @return void + */ + public static function clear() + { + return static::getInstance()->flush(); + } + + /** + * 绑定一个类、闭包、实例、接口实现到容器 + * @access public + * @param string|array $abstract 类标识、接口 + * @param mixed $concrete 要绑定的类、闭包或者实例 + * @return $this + */ + public function bindTo($abstract, $concrete = null) + { + if (is_array($abstract)) { + $this->bind = array_merge($this->bind, $abstract); + } elseif ($concrete instanceof Closure) { + $this->bind[$abstract] = $concrete; + } elseif (is_object($concrete)) { + if (isset($this->bind[$abstract])) { + $abstract = $this->bind[$abstract]; + } + $this->instances[$abstract] = $concrete; + } else { + $this->bind[$abstract] = $concrete; + } + + return $this; + } + + /** + * 绑定一个类实例当容器 + * @access public + * @param string $abstract 类名或者标识 + * @param object|\Closure $instance 类的实例 + * @return $this + */ + public function instance($abstract, $instance) + { + if ($instance instanceof \Closure) { + $this->bind[$abstract] = $instance; + } else { + if (isset($this->bind[$abstract])) { + $abstract = $this->bind[$abstract]; + } + + $this->instances[$abstract] = $instance; + } + + return $this; + } + + /** + * 判断容器中是否存在类及标识 + * @access public + * @param string $abstract 类名或者标识 + * @return bool + */ + public function bound($abstract) + { + return isset($this->bind[$abstract]) || isset($this->instances[$abstract]); + } + + /** + * 判断容器中是否存在对象实例 + * @access public + * @param string $abstract 类名或者标识 + * @return bool + */ + public function exists($abstract) + { + if (isset($this->bind[$abstract])) { + $abstract = $this->bind[$abstract]; + } + + return isset($this->instances[$abstract]); + } + + /** + * 判断容器中是否存在类及标识 + * @access public + * @param string $name 类名或者标识 + * @return bool + */ + public function has($name) + { + return $this->bound($name); + } + + /** + * 创建类的实例 + * @access public + * @param string $abstract 类名或者标识 + * @param array|true $vars 变量 + * @param bool $newInstance 是否每次创建新的实例 + * @return object + */ + public function make($abstract, $vars = [], $newInstance = false) + { + if (true === $vars) { + // 总是创建新的实例化对象 + $newInstance = true; + $vars = []; + } + + $abstract = isset($this->name[$abstract]) ? $this->name[$abstract] : $abstract; + + if (isset($this->instances[$abstract]) && !$newInstance) { + return $this->instances[$abstract]; + } + + if (isset($this->bind[$abstract])) { + $concrete = $this->bind[$abstract]; + + if ($concrete instanceof Closure) { + $object = $this->invokeFunction($concrete, $vars); + } else { + $this->name[$abstract] = $concrete; + return $this->make($concrete, $vars, $newInstance); + } + } else { + $object = $this->invokeClass($abstract, $vars); + } + + if (!$newInstance) { + $this->instances[$abstract] = $object; + } + + return $object; + } + + /** + * 删除容器中的对象实例 + * @access public + * @param string|array $abstract 类名或者标识 + * @return void + */ + public function delete($abstract) + { + foreach ((array) $abstract as $name) { + $name = isset($this->name[$name]) ? $this->name[$name] : $name; + + if (isset($this->instances[$name])) { + unset($this->instances[$name]); + } + } + } + + /** + * 获取容器中的对象实例 + * @access public + * @return array + */ + public function all() + { + return $this->instances; + } + + /** + * 清除容器中的对象实例 + * @access public + * @return void + */ + public function flush() + { + $this->instances = []; + $this->bind = []; + $this->name = []; + } + + /** + * 执行函数或者闭包方法 支持参数调用 + * @access public + * @param mixed $function 函数或者闭包 + * @param array $vars 参数 + * @return mixed + */ + public function invokeFunction($function, $vars = []) + { + try { + $reflect = new ReflectionFunction($function); + + $args = $this->bindParams($reflect, $vars); + + return call_user_func_array($function, $args); + } catch (ReflectionException $e) { + throw new Exception('function not exists: ' . $function . '()'); + } + } + + /** + * 调用反射执行类的方法 支持参数绑定 + * @access public + * @param mixed $method 方法 + * @param array $vars 参数 + * @return mixed + */ + public function invokeMethod($method, $vars = []) + { + try { + if (is_array($method)) { + $class = is_object($method[0]) ? $method[0] : $this->invokeClass($method[0]); + $reflect = new ReflectionMethod($class, $method[1]); + } else { + // 静态方法 + $reflect = new ReflectionMethod($method); + } + + $args = $this->bindParams($reflect, $vars); + + return $reflect->invokeArgs(isset($class) ? $class : null, $args); + } catch (ReflectionException $e) { + if (is_array($method) && is_object($method[0])) { + $method[0] = get_class($method[0]); + } + + throw new Exception('method not exists: ' . (is_array($method) ? $method[0] . '::' . $method[1] : $method) . '()'); + } + } + + /** + * 调用反射执行类的方法 支持参数绑定 + * @access public + * @param object $instance 对象实例 + * @param mixed $reflect 反射类 + * @param array $vars 参数 + * @return mixed + */ + public function invokeReflectMethod($instance, $reflect, $vars = []) + { + $args = $this->bindParams($reflect, $vars); + + return $reflect->invokeArgs($instance, $args); + } + + /** + * 调用反射执行callable 支持参数绑定 + * @access public + * @param mixed $callable + * @param array $vars 参数 + * @return mixed + */ + public function invoke($callable, $vars = []) + { + if ($callable instanceof Closure) { + return $this->invokeFunction($callable, $vars); + } + + return $this->invokeMethod($callable, $vars); + } + + /** + * 调用反射执行类的实例化 支持依赖注入 + * @access public + * @param string $class 类名 + * @param array $vars 参数 + * @return mixed + */ + public function invokeClass($class, $vars = []) + { + try { + $reflect = new ReflectionClass($class); + + if ($reflect->hasMethod('__make')) { + $method = new ReflectionMethod($class, '__make'); + + if ($method->isPublic() && $method->isStatic()) { + $args = $this->bindParams($method, $vars); + return $method->invokeArgs(null, $args); + } + } + + $constructor = $reflect->getConstructor(); + + $args = $constructor ? $this->bindParams($constructor, $vars) : []; + + return $reflect->newInstanceArgs($args); + + } catch (ReflectionException $e) { + throw new ClassNotFoundException('class not exists: ' . $class, $class); + } + } + + /** + * 绑定参数 + * @access protected + * @param \ReflectionMethod|\ReflectionFunction $reflect 反射类 + * @param array $vars 参数 + * @return array + */ + protected function bindParams($reflect, $vars = []) + { + if ($reflect->getNumberOfParameters() == 0) { + return []; + } + + // 判断数组类型 数字数组时按顺序绑定参数 + reset($vars); + $type = key($vars) === 0 ? 1 : 0; + $params = $reflect->getParameters(); + + if (PHP_VERSION > 8.0) { + $args = $this->parseParamsForPHP8($params, $vars, $type); + } else { + $args = $this->parseParams($params, $vars, $type); + } + + return $args; + } + + /** + * 解析参数 + * @access protected + * @param array $params 参数列表 + * @param array $vars 参数数据 + * @param int $type 参数类别 + * @return array + */ + protected function parseParams($params, $vars, $type) + { + foreach ($params as $param) { + $name = $param->getName(); + $lowerName = Loader::parseName($name); + $class = $param->getClass(); + + if ($class) { + $args[] = $this->getObjectParam($class->getName(), $vars); + } elseif (1 == $type && !empty($vars)) { + $args[] = array_shift($vars); + } elseif (0 == $type && isset($vars[$name])) { + $args[] = $vars[$name]; + } elseif (0 == $type && isset($vars[$lowerName])) { + $args[] = $vars[$lowerName]; + } elseif ($param->isDefaultValueAvailable()) { + $args[] = $param->getDefaultValue(); + } else { + throw new InvalidArgumentException('method param miss:' . $name); + } + } + return $args; + } + + /** + * 解析参数 + * @access protected + * @param array $params 参数列表 + * @param array $vars 参数数据 + * @param int $type 参数类别 + * @return array + */ + protected function parseParamsForPHP8($params, $vars, $type) + { + foreach ($params as $param) { + $name = $param->getName(); + $lowerName = Loader::parseName($name); + $reflectionType = $param->getType(); + + if ($reflectionType && $reflectionType->isBuiltin() === false) { + $args[] = $this->getObjectParam($reflectionType->getName(), $vars); + } elseif (1 == $type && !empty($vars)) { + $args[] = array_shift($vars); + } elseif (0 == $type && array_key_exists($name, $vars)) { + $args[] = $vars[$name]; + } elseif (0 == $type && array_key_exists($lowerName, $vars)) { + $args[] = $vars[$lowerName]; + } elseif ($param->isDefaultValueAvailable()) { + $args[] = $param->getDefaultValue(); + } else { + throw new InvalidArgumentException('method param miss:' . $name); + } + } + return $args; + } + + /** + * 获取对象类型的参数值 + * @access protected + * @param string $className 类名 + * @param array $vars 参数 + * @return mixed + */ + protected function getObjectParam($className, &$vars) + { + $array = $vars; + $value = array_shift($array); + + if ($value instanceof $className) { + $result = $value; + array_shift($vars); + } else { + $result = $this->make($className); + } + + return $result; + } + + public function __set($name, $value) + { + $this->bindTo($name, $value); + } + + public function __get($name) + { + return $this->make($name); + } + + public function __isset($name) + { + return $this->bound($name); + } + + public function __unset($name) + { + $this->delete($name); + } + + public function offsetExists($key) + { + return $this->__isset($key); + } + + public function offsetGet($key) + { + return $this->__get($key); + } + + public function offsetSet($key, $value) + { + $this->__set($key, $value); + } + + public function offsetUnset($key) + { + $this->__unset($key); + } + + //Countable + public function count() + { + return count($this->instances); + } + + //IteratorAggregate + public function getIterator() + { + return new ArrayIterator($this->instances); + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['instances'], $data['instance']); + + return $data; + } +} diff --git a/vendor/topthink/framework/library/think/Controller.php b/vendor/topthink/framework/library/think/Controller.php new file mode 100644 index 0000000..966eaaa --- /dev/null +++ b/vendor/topthink/framework/library/think/Controller.php @@ -0,0 +1,287 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ValidateException; +use traits\controller\Jump; + +class Controller +{ + use Jump; + + /** + * 视图类实例 + * @var \think\View + */ + protected $view; + + /** + * Request实例 + * @var \think\Request + */ + protected $request; + + /** + * 验证失败是否抛出异常 + * @var bool + */ + protected $failException = false; + + /** + * 是否批量验证 + * @var bool + */ + protected $batchValidate = false; + + /** + * 前置操作方法列表(即将废弃) + * @var array $beforeActionList + */ + protected $beforeActionList = []; + + /** + * 控制器中间件 + * @var array + */ + protected $middleware = []; + + /** + * 构造方法 + * @access public + */ + public function __construct(App $app = null) + { + $this->app = $app ?: Container::get('app'); + $this->request = $this->app['request']; + $this->view = $this->app['view']; + + // 控制器初始化 + $this->initialize(); + + $this->registerMiddleware(); + + // 前置操作方法 即将废弃 + foreach ((array) $this->beforeActionList as $method => $options) { + is_numeric($method) ? + $this->beforeAction($options) : + $this->beforeAction($method, $options); + } + } + + // 初始化 + protected function initialize() + {} + + // 注册控制器中间件 + public function registerMiddleware() + { + foreach ($this->middleware as $key => $val) { + if (!is_int($key)) { + $only = $except = null; + + if (isset($val['only'])) { + $only = array_map(function ($item) { + return strtolower($item); + }, $val['only']); + } elseif (isset($val['except'])) { + $except = array_map(function ($item) { + return strtolower($item); + }, $val['except']); + } + + if (isset($only) && !in_array($this->request->action(), $only)) { + continue; + } elseif (isset($except) && in_array($this->request->action(), $except)) { + continue; + } else { + $val = $key; + } + } + + $this->app['middleware']->controller($val); + } + } + + /** + * 前置操作 + * @access protected + * @param string $method 前置操作方法名 + * @param array $options 调用参数 ['only'=>[...]] 或者['except'=>[...]] + */ + protected function beforeAction($method, $options = []) + { + if (isset($options['only'])) { + if (is_string($options['only'])) { + $options['only'] = explode(',', $options['only']); + } + + $only = array_map(function ($val) { + return strtolower($val); + }, $options['only']); + + if (!in_array($this->request->action(), $only)) { + return; + } + } elseif (isset($options['except'])) { + if (is_string($options['except'])) { + $options['except'] = explode(',', $options['except']); + } + + $except = array_map(function ($val) { + return strtolower($val); + }, $options['except']); + + if (in_array($this->request->action(), $except)) { + return; + } + } + + call_user_func([$this, $method]); + } + + /** + * 加载模板输出 + * @access protected + * @param string $template 模板文件名 + * @param array $vars 模板输出变量 + * @param array $config 模板参数 + * @return mixed + */ + protected function fetch($template = '', $vars = [], $config = []) + { + return Response::create($template, 'view')->assign($vars)->config($config); + } + + /** + * 渲染内容输出 + * @access protected + * @param string $content 模板内容 + * @param array $vars 模板输出变量 + * @param array $config 模板参数 + * @return mixed + */ + protected function display($content = '', $vars = [], $config = []) + { + return Response::create($content, 'view')->assign($vars)->config($config)->isContent(true); + } + + /** + * 模板变量赋值 + * @access protected + * @param mixed $name 要显示的模板变量 + * @param mixed $value 变量的值 + * @return $this + */ + protected function assign($name, $value = '') + { + $this->view->assign($name, $value); + + return $this; + } + + /** + * 视图过滤 + * @access protected + * @param Callable $filter 过滤方法或闭包 + * @return $this + */ + protected function filter($filter) + { + $this->view->filter($filter); + + return $this; + } + + /** + * 初始化模板引擎 + * @access protected + * @param array|string $engine 引擎参数 + * @return $this + */ + protected function engine($engine) + { + $this->view->engine($engine); + + return $this; + } + + /** + * 设置验证失败后是否抛出异常 + * @access protected + * @param bool $fail 是否抛出异常 + * @return $this + */ + 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 回调方法(闭包) + * @return array|string|true + * @throws ValidateException + */ + protected function validate($data, $validate, $message = [], $batch = false, $callback = null) + { + if (is_array($validate)) { + $v = $this->app->validate(); + $v->rule($validate); + } else { + if (strpos($validate, '.')) { + // 支持场景 + list($validate, $scene) = explode('.', $validate); + } + $v = $this->app->validate($validate); + if (!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]); + } + + if (!$v->check($data)) { + if ($this->failException) { + throw new ValidateException($v->getError()); + } + return $v->getError(); + } + + return true; + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app'], $data['request']); + + return $data; + } +} diff --git a/vendor/topthink/framework/library/think/Cookie.php b/vendor/topthink/framework/library/think/Cookie.php new file mode 100644 index 0000000..6a9fb1e --- /dev/null +++ b/vendor/topthink/framework/library/think/Cookie.php @@ -0,0 +1,268 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Cookie +{ + /** + * 配置参数 + * @var array + */ + protected $config = [ + // cookie 名称前缀 + 'prefix' => '', + // cookie 保存时间 + 'expire' => 0, + // cookie 保存路径 + 'path' => '/', + // cookie 有效域名 + 'domain' => '', + // cookie 启用安全传输 + 'secure' => false, + // httponly设置 + 'httponly' => false, + // 是否使用 setcookie + 'setcookie' => true, + ]; + + /** + * 构造方法 + * @access public + */ + public function __construct(array $config = []) + { + $this->init($config); + } + + /** + * Cookie初始化 + * @access public + * @param array $config + * @return void + */ + public function init(array $config = []) + { + $this->config = array_merge($this->config, array_change_key_case($config)); + + if (!empty($this->config['httponly']) && PHP_SESSION_ACTIVE != session_status()) { + ini_set('session.cookie_httponly', 1); + } + } + + public static function __make(Config $config) + { + return new static($config->pull('cookie')); + } + + /** + * 设置或者获取cookie作用域(前缀) + * @access public + * @param string $prefix + * @return string|void + */ + public function prefix($prefix = '') + { + if (empty($prefix)) { + return $this->config['prefix']; + } + + $this->config['prefix'] = $prefix; + } + + /** + * Cookie 设置、获取、删除 + * + * @access public + * @param string $name cookie名称 + * @param mixed $value cookie值 + * @param mixed $option 可选参数 可能会是 null|integer|string + * @return void + */ + public function set($name, $value = '', $option = null) + { + // 参数设置(会覆盖黙认设置) + if (!is_null($option)) { + if (is_numeric($option)) { + $option = ['expire' => $option]; + } elseif (is_string($option)) { + parse_str($option, $option); + } + + $config = array_merge($this->config, array_change_key_case($option)); + } else { + $config = $this->config; + } + + $name = $config['prefix'] . $name; + + // 设置cookie + if (is_array($value)) { + array_walk_recursive($value, [$this, 'jsonFormatProtect'], 'encode'); + $value = 'think:' . json_encode($value); + } + + $expire = !empty($config['expire']) ? $_SERVER['REQUEST_TIME'] + intval($config['expire']) : 0; + + if ($config['setcookie']) { + $this->setCookie($name, $value, $expire, $config); + } + + $_COOKIE[$name] = $value; + } + + /** + * Cookie 设置保存 + * + * @access public + * @param string $name cookie名称 + * @param mixed $value cookie值 + * @param array $option 可选参数 + * @return void + */ + protected function setCookie($name, $value, $expire, $option = []) + { + setcookie($name, $value, $expire, $option['path'], $option['domain'], $option['secure'], $option['httponly']); + } + + /** + * 永久保存Cookie数据 + * @access public + * @param string $name cookie名称 + * @param mixed $value cookie值 + * @param mixed $option 可选参数 可能会是 null|integer|string + * @return void + */ + public function forever($name, $value = '', $option = null) + { + if (is_null($option) || is_numeric($option)) { + $option = []; + } + + $option['expire'] = 315360000; + + $this->set($name, $value, $option); + } + + /** + * 判断Cookie数据 + * @access public + * @param string $name cookie名称 + * @param string|null $prefix cookie前缀 + * @return bool + */ + public function has($name, $prefix = null) + { + $prefix = !is_null($prefix) ? $prefix : $this->config['prefix']; + $name = $prefix . $name; + + return isset($_COOKIE[$name]); + } + + /** + * Cookie获取 + * @access public + * @param string $name cookie名称 留空获取全部 + * @param string|null $prefix cookie前缀 + * @return mixed + */ + public function get($name = '', $prefix = null) + { + $prefix = !is_null($prefix) ? $prefix : $this->config['prefix']; + $key = $prefix . $name; + + if ('' == $name) { + 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); + array_walk_recursive($value, [$this, 'jsonFormatProtect'], 'decode'); + } + } else { + $value = null; + } + + return $value; + } + + /** + * Cookie删除 + * @access public + * @param string $name cookie名称 + * @param string|null $prefix cookie前缀 + * @return void + */ + public function delete($name, $prefix = null) + { + $config = $this->config; + $prefix = !is_null($prefix) ? $prefix : $config['prefix']; + $name = $prefix . $name; + + if ($config['setcookie']) { + $this->setcookie($name, '', $_SERVER['REQUEST_TIME'] - 3600, $config); + } + + // 删除指定cookie + unset($_COOKIE[$name]); + } + + /** + * Cookie清空 + * @access public + * @param string|null $prefix cookie前缀 + * @return void + */ + public function clear($prefix = null) + { + // 清除指定前缀的所有cookie + if (empty($_COOKIE)) { + return; + } + + // 要删除的cookie前缀,不指定则删除config设置的指定前缀 + $config = $this->config; + $prefix = !is_null($prefix) ? $prefix : $config['prefix']; + + if ($prefix) { + // 如果前缀为空字符串将不作处理直接返回 + foreach ($_COOKIE as $key => $val) { + if (0 === strpos($key, $prefix)) { + if ($config['setcookie']) { + $this->setcookie($key, '', $_SERVER['REQUEST_TIME'] - 3600, $config); + } + unset($_COOKIE[$key]); + } + } + } + + return; + } + + private function jsonFormatProtect(&$val, $key, $type = 'encode') + { + if (!empty($val) && true !== $val) { + $val = 'decode' == $type ? urldecode($val) : urlencode($val); + } + } + +} diff --git a/vendor/topthink/framework/library/think/Db.php b/vendor/topthink/framework/library/think/Db.php new file mode 100644 index 0000000..9280eac --- /dev/null +++ b/vendor/topthink/framework/library/think/Db.php @@ -0,0 +1,197 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\db\Connection; + +/** + * Class Db + * @package think + * @method \think\db\Query master() static 从主服务器读取数据 + * @method \think\db\Query readMaster(bool $all = false) static 后续从主服务器读取数据 + * @method \think\db\Query table(string $table) static 指定数据表(含前缀) + * @method \think\db\Query name(string $name) static 指定数据表(不含前缀) + * @method \think\db\Expression raw(string $value) static 使用表达式设置数据 + * @method \think\db\Query where(mixed $field, string $op = null, mixed $condition = null) static 查询条件 + * @method \think\db\Query whereRaw(string $where, array $bind = []) static 表达式查询 + * @method \think\db\Query whereExp(string $field, string $condition, array $bind = []) static 字段表达式查询 + * @method \think\db\Query when(mixed $condition, mixed $query, mixed $otherwise = null) static 条件查询 + * @method \think\db\Query join(mixed $join, mixed $condition = null, string $type = 'INNER') static JOIN查询 + * @method \think\db\Query view(mixed $join, mixed $field = null, mixed $on = null, string $type = 'INNER') static 视图查询 + * @method \think\db\Query field(mixed $field, boolean $except = false) static 指定查询字段 + * @method \think\db\Query fieldRaw(string $field, array $bind = []) static 指定查询字段 + * @method \think\db\Query union(mixed $union, boolean $all = false) static UNION查询 + * @method \think\db\Query limit(mixed $offset, integer $length = null) static 查询LIMIT + * @method \think\db\Query order(mixed $field, string $order = null) static 查询ORDER + * @method \think\db\Query orderRaw(string $field, array $bind = []) static 查询ORDER + * @method \think\db\Query cache(mixed $key = null , integer $expire = null) static 设置查询缓存 + * @method \think\db\Query withAttr(string $name,callable $callback = null) static 使用获取器获取数据 + * @method mixed value(string $field) static 获取某个字段的值 + * @method array column(string $field, string $key = '') static 获取某个列的值 + * @method mixed find(mixed $data = null) static 查询单个记录 + * @method mixed select(mixed $data = null) static 查询多个记录 + * @method integer insert(array $data, boolean $replace = false, boolean $getLastInsID = false, string $sequence = null) static 插入一条记录 + * @method integer insertGetId(array $data, boolean $replace = false, string $sequence = null) static 插入一条记录并返回自增ID + * @method integer insertAll(array $dataSet) static 插入多条记录 + * @method integer update(array $data) static 更新记录 + * @method integer delete(mixed $data = null) static 删除记录 + * @method boolean chunk(integer $count, callable $callback, string $column = null) static 分块获取数据 + * @method \Generator cursor(mixed $data = null) static 使用游标查找记录 + * @method mixed query(string $sql, array $bind = [], boolean $master = false, bool $pdo = false) static SQL查询 + * @method integer execute(string $sql, array $bind = [], boolean $fetch = false, boolean $getLastInsID = false, string $sequence = null) static SQL执行 + * @method \think\Paginator paginate(integer $listRows = 15, mixed $simple = null, array $config = []) static 分页查询 + * @method mixed transaction(callable $callback) static 执行数据库事务 + * @method void startTrans() static 启动事务 + * @method void commit() static 用于非自动提交状态下面的查询提交 + * @method void rollback() static 事务回滚 + * @method boolean batchQuery(array $sqlArray) static 批处理执行SQL语句 + * @method string getLastInsID(string $sequence = null) static 获取最近插入的ID + */ +class Db +{ + /** + * 当前数据库连接对象 + * @var Connection + */ + protected static $connection; + + /** + * 数据库配置 + * @var array + */ + protected static $config = []; + + /** + * 查询次数 + * @var integer + */ + public static $queryTimes = 0; + + /** + * 执行次数 + * @var integer + */ + public static $executeTimes = 0; + + /** + * 配置 + * @access public + * @param mixed $config + * @return void + */ + public static function init($config = []) + { + self::$config = $config; + + if (empty($config['query'])) { + self::$config['query'] = '\\think\\db\\Query'; + } + } + + /** + * 获取数据库配置 + * @access public + * @param string $config 配置名称 + * @return mixed + */ + public static function getConfig($name = '') + { + if ('' === $name) { + return self::$config; + } + + return isset(self::$config[$name]) ? self::$config[$name] : null; + } + + /** + * 切换数据库连接 + * @access public + * @param mixed $config 连接配置 + * @param bool|string $name 连接标识 true 强制重新连接 + * @param string $query 查询对象类名 + * @return mixed 返回查询对象实例 + * @throws Exception + */ + public static function connect($config = [], $name = false, $query = '') + { + // 解析配置参数 + $options = self::parseConfig($config ?: self::$config); + + $query = $query ?: $options['query']; + + // 创建数据库连接对象实例 + self::$connection = Connection::instance($options, $name); + + return new $query(self::$connection); + } + + /** + * 数据库连接参数解析 + * @access private + * @param mixed $config + * @return array + */ + private static function parseConfig($config) + { + if (is_string($config) && false === strpos($config, '/')) { + // 支持读取配置参数 + $config = isset(self::$config[$config]) ? self::$config[$config] : self::$config; + } + + $result = is_string($config) ? self::parseDsnConfig($config) : $config; + + if (empty($result['query'])) { + $result['query'] = self::$config['query']; + } + + return $result; + } + + /** + * DSN解析 + * 格式: mysql://username:passwd@localhost:3306/DbName?param1=val1¶m2=val2#utf8 + * @access private + * @param string $dsnStr + * @return array + */ + private static function parseDsnConfig($dsnStr) + { + $info = parse_url($dsnStr); + + if (!$info) { + return []; + } + + $dsn = [ + 'type' => $info['scheme'], + 'username' => isset($info['user']) ? $info['user'] : '', + 'password' => isset($info['pass']) ? $info['pass'] : '', + 'hostname' => isset($info['host']) ? $info['host'] : '', + 'hostport' => isset($info['port']) ? $info['port'] : '', + 'database' => !empty($info['path']) ? ltrim($info['path'], '/') : '', + 'charset' => isset($info['fragment']) ? $info['fragment'] : 'utf8', + ]; + + if (isset($info['query'])) { + parse_str($info['query'], $dsn['params']); + } else { + $dsn['params'] = []; + } + + return $dsn; + } + + public static function __callStatic($method, $args) + { + return call_user_func_array([static::connect(), $method], $args); + } +} diff --git a/vendor/topthink/framework/library/think/Debug.php b/vendor/topthink/framework/library/think/Debug.php new file mode 100644 index 0000000..776e178 --- /dev/null +++ b/vendor/topthink/framework/library/think/Debug.php @@ -0,0 +1,278 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\model\Collection as ModelCollection; +use think\response\Redirect; + +class Debug +{ + /** + * 配置参数 + * @var array + */ + protected $config = []; + + /** + * 区间时间信息 + * @var array + */ + protected $info = []; + + /** + * 区间内存信息 + * @var array + */ + protected $mem = []; + + /** + * 应用对象 + * @var App + */ + protected $app; + + public function __construct(App $app, array $config = []) + { + $this->app = $app; + $this->config = $config; + } + + public static function __make(App $app, Config $config) + { + return new static($app, $config->pull('trace')); + } + + public function setConfig(array $config) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 记录时间(微秒)和内存使用情况 + * @access public + * @param string $name 标记位置 + * @param mixed $value 标记值 留空则取当前 time 表示仅记录时间 否则同时记录时间和内存 + * @return void + */ + public function remark($name, $value = '') + { + // 记录时间和内存使用 + $this->info[$name] = is_float($value) ? $value : microtime(true); + + if ('time' != $value) { + $this->mem['mem'][$name] = is_float($value) ? $value : memory_get_usage(); + $this->mem['peak'][$name] = memory_get_peak_usage(); + } + } + + /** + * 统计某个区间的时间(微秒)使用情况 + * @access public + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 + * @return integer + */ + public function getRangeTime($start, $end, $dec = 6) + { + if (!isset($this->info[$end])) { + $this->info[$end] = microtime(true); + } + + return number_format(($this->info[$end] - $this->info[$start]), $dec); + } + + /** + * 统计从开始到统计时的时间(微秒)使用情况 + * @access public + * @param integer|string $dec 小数位 + * @return integer + */ + public function getUseTime($dec = 6) + { + return number_format((microtime(true) - $this->app->getBeginTime()), $dec); + } + + /** + * 获取当前访问的吞吐率情况 + * @access public + * @return string + */ + public function getThroughputRate() + { + return number_format(1 / $this->getUseTime(), 2) . 'req/s'; + } + + /** + * 记录区间的内存使用情况 + * @access public + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 + * @return string + */ + public function getRangeMem($start, $end, $dec = 2) + { + if (!isset($this->mem['mem'][$end])) { + $this->mem['mem'][$end] = memory_get_usage(); + } + + $size = $this->mem['mem'][$end] - $this->mem['mem'][$start]; + $a = ['B', 'KB', 'MB', 'GB', 'TB']; + $pos = 0; + + while ($size >= 1024) { + $size /= 1024; + $pos++; + } + + return round($size, $dec) . " " . $a[$pos]; + } + + /** + * 统计从开始到统计时的内存使用情况 + * @access public + * @param integer|string $dec 小数位 + * @return string + */ + public function getUseMem($dec = 2) + { + $size = memory_get_usage() - $this->app->getBeginMem(); + $a = ['B', 'KB', 'MB', 'GB', 'TB']; + $pos = 0; + + while ($size >= 1024) { + $size /= 1024; + $pos++; + } + + return round($size, $dec) . " " . $a[$pos]; + } + + /** + * 统计区间的内存峰值情况 + * @access public + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 + * @return string + */ + public function getMemPeak($start, $end, $dec = 2) + { + if (!isset($this->mem['peak'][$end])) { + $this->mem['peak'][$end] = memory_get_peak_usage(); + } + + $size = $this->mem['peak'][$end] - $this->mem['peak'][$start]; + $a = ['B', 'KB', 'MB', 'GB', 'TB']; + $pos = 0; + + while ($size >= 1024) { + $size /= 1024; + $pos++; + } + + return round($size, $dec) . " " . $a[$pos]; + } + + /** + * 获取文件加载信息 + * @access public + * @param bool $detail 是否显示详细 + * @return integer|array + */ + public function getFile($detail = false) + { + if ($detail) { + $files = get_included_files(); + $info = []; + + foreach ($files as $key => $file) { + $info[] = $file . ' ( ' . number_format(filesize($file) / 1024, 2) . ' KB )'; + } + + return $info; + } + + return count(get_included_files()); + } + + /** + * 浏览器友好的变量输出 + * @access public + * @param mixed $var 变量 + * @param boolean $echo 是否输出 默认为true 如果为false 则返回输出字符串 + * @param string $label 标签 默认为空 + * @param integer $flags htmlspecialchars flags + * @return void|string + */ + public function dump($var, $echo = true, $label = null, $flags = ENT_SUBSTITUTE) + { + $label = (null === $label) ? '' : rtrim($label) . ':'; + if ($var instanceof Model || $var instanceof ModelCollection) { + $var = $var->toArray(); + } + + ob_start(); + var_dump($var); + + $output = ob_get_clean(); + $output = preg_replace('/\]\=\>\n(\s+)/m', '] => ', $output); + + if (PHP_SAPI == '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; + } + return $output; + } + + public function inject(Response $response, &$content) + { + $config = $this->config; + $type = isset($config['type']) ? $config['type'] : 'Html'; + + unset($config['type']); + + $trace = Loader::factory($type, '\\think\\debug\\', $config); + + if ($response instanceof Redirect) { + //TODO 记录 + } else { + $output = $trace->output($response, $this->app['log']->getLog()); + if (is_string($output)) { + // trace调试信息注入 + $pos = strripos($content, ''); + if (false !== $pos) { + $content = substr($content, 0, $pos) . $output . substr($content, $pos); + } else { + $content = $content . $output; + } + } + } + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app']); + + return $data; + } +} diff --git a/vendor/topthink/framework/library/think/Env.php b/vendor/topthink/framework/library/think/Env.php new file mode 100644 index 0000000..eaeee94 --- /dev/null +++ b/vendor/topthink/framework/library/think/Env.php @@ -0,0 +1,113 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Env +{ + /** + * 环境变量数据 + * @var array + */ + protected $data = []; + + public function __construct() + { + $this->data = $_ENV; + } + + /** + * 读取环境变量定义文件 + * @access public + * @param string $file 环境变量定义文件 + * @return void + */ + public function load($file) + { + $env = parse_ini_file($file, true); + $this->set($env); + } + + /** + * 获取环境变量值 + * @access public + * @param string $name 环境变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name = null, $default = null, $php_prefix = true) + { + if (is_null($name)) { + return $this->data; + } + + $name = strtoupper(str_replace('.', '_', $name)); + + if (isset($this->data[$name])) { + return $this->data[$name]; + } + + return $this->getEnv($name, $default, $php_prefix); + } + + protected function getEnv($name, $default = null, $php_prefix = true) + { + if ($php_prefix) { + $name = 'PHP_' . $name; + } + + $result = getenv($name); + + if (false === $result) { + return $default; + } + + if ('false' === $result) { + $result = false; + } elseif ('true' === $result) { + $result = true; + } + + if (!isset($this->data[$name])) { + $this->data[$name] = $result; + } + + return $result; + } + + /** + * 设置环境变量值 + * @access public + * @param string|array $env 环境变量 + * @param mixed $value 值 + * @return void + */ + public function set($env, $value = null) + { + if (is_array($env)) { + $env = array_change_key_case($env, CASE_UPPER); + + foreach ($env as $key => $val) { + if (is_array($val)) { + foreach ($val as $k => $v) { + $this->data[$key . '_' . strtoupper($k)] = $v; + } + } else { + $this->data[$key] = $val; + } + } + } else { + $name = strtoupper(str_replace('.', '_', $env)); + + $this->data[$name] = $value; + } + } +} diff --git a/vendor/topthink/framework/library/think/Error.php b/vendor/topthink/framework/library/think/Error.php new file mode 100644 index 0000000..ea3328e --- /dev/null +++ b/vendor/topthink/framework/library/think/Error.php @@ -0,0 +1,147 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\console\Output as ConsoleOutput; +use think\exception\ErrorException; +use think\exception\Handle; +use think\exception\ThrowableError; + +class Error +{ + /** + * 配置参数 + * @var array + */ + protected static $exceptionHandler; + + /** + * 注册异常处理 + * @access public + * @return void + */ + public static function register() + { + error_reporting(E_ALL); + set_error_handler([__CLASS__, 'appError']); + set_exception_handler([__CLASS__, 'appException']); + register_shutdown_function([__CLASS__, 'appShutdown']); + } + + /** + * Exception Handler + * @access public + * @param \Exception|\Throwable $e + */ + public static function appException($e) + { + if (!$e instanceof \Exception) { + $e = new ThrowableError($e); + } + + self::getExceptionHandler()->report($e); + + if (PHP_SAPI == 'cli') { + self::getExceptionHandler()->renderForConsole(new ConsoleOutput, $e); + } else { + self::getExceptionHandler()->render($e)->send(); + } + } + + /** + * Error Handler + * @access public + * @param integer $errno 错误编号 + * @param integer $errstr 详细错误信息 + * @param string $errfile 出错的文件 + * @param integer $errline 出错行号 + * @throws ErrorException + */ + public static function appError($errno, $errstr, $errfile = '', $errline = 0) + { + $exception = new ErrorException($errno, $errstr, $errfile, $errline); + if (error_reporting() & $errno) { + // 将错误信息托管至 think\exception\ErrorException + throw $exception; + } + + self::getExceptionHandler()->report($exception); + } + + /** + * Shutdown Handler + * @access public + */ + public static function appShutdown() + { + 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); + } + + // 写入日志 + Container::get('log')->save(); + } + + /** + * 确定错误类型是否致命 + * + * @access protected + * @param int $type + * @return bool + */ + protected static function isFatal($type) + { + return in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE]); + } + + /** + * 设置异常处理类 + * + * @access public + * @param mixed $handle + * @return void + */ + public static function setExceptionHandler($handle) + { + self::$exceptionHandler = $handle; + } + + /** + * Get an instance of the exception handler. + * + * @access public + * @return Handle + */ + public static function getExceptionHandler() + { + static $handle; + + if (!$handle) { + // 异常处理handle + $class = self::$exceptionHandler; + + 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/vendor/topthink/framework/library/think/Exception.php b/vendor/topthink/framework/library/think/Exception.php new file mode 100644 index 0000000..414a090 --- /dev/null +++ b/vendor/topthink/framework/library/think/Exception.php @@ -0,0 +1,56 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Exception extends \Exception +{ + + /** + * 保存异常页面显示的额外Debug数据 + * @var array + */ + protected $data = []; + + /** + * 设置异常额外的Debug数据 + * 数据将会显示为下面的格式 + * + * Exception Data + * -------------------------------------------------- + * Label 1 + * key1 value1 + * key2 value2 + * Label 2 + * key1 value1 + * key2 value2 + * + * @access protected + * @param string $label 数据分类,用于异常页面显示 + * @param array $data 需要显示的数据,必须为关联数组 + */ + final protected function setData($label, array $data) + { + $this->data[$label] = $data; + } + + /** + * 获取异常额外Debug数据 + * 主要用于输出到异常页面便于调试 + * @access public + * @return array 由setData设置的Debug数据 + */ + final public function getData() + { + return $this->data; + } + +} diff --git a/vendor/topthink/framework/library/think/Facade.php b/vendor/topthink/framework/library/think/Facade.php new file mode 100644 index 0000000..ac5ae28 --- /dev/null +++ b/vendor/topthink/framework/library/think/Facade.php @@ -0,0 +1,125 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Facade +{ + /** + * 绑定对象 + * @var array + */ + protected static $bind = []; + + /** + * 始终创建新的对象实例 + * @var bool + */ + protected static $alwaysNewInstance; + + /** + * 绑定类的静态代理 + * @static + * @access public + * @param string|array $name 类标识 + * @param string $class 类名 + * @return object + */ + public static function bind($name, $class = null) + { + if (__CLASS__ != static::class) { + return self::__callStatic('bind', func_get_args()); + } + + if (is_array($name)) { + self::$bind = array_merge(self::$bind, $name); + } else { + self::$bind[$name] = $class; + } + } + + /** + * 创建Facade实例 + * @static + * @access protected + * @param string $class 类名或标识 + * @param array $args 变量 + * @param bool $newInstance 是否每次创建新的实例 + * @return object + */ + protected static function createFacade($class = '', $args = [], $newInstance = false) + { + $class = $class ?: static::class; + + $facadeClass = static::getFacadeClass(); + + if ($facadeClass) { + $class = $facadeClass; + } elseif (isset(self::$bind[$class])) { + $class = self::$bind[$class]; + } + + if (static::$alwaysNewInstance) { + $newInstance = true; + } + + return Container::getInstance()->make($class, $args, $newInstance); + } + + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + {} + + /** + * 带参数实例化当前Facade类 + * @access public + * @return mixed + */ + public static function instance(...$args) + { + if (__CLASS__ != static::class) { + return self::createFacade('', $args); + } + } + + /** + * 调用类的实例 + * @access public + * @param string $class 类名或者标识 + * @param array|true $args 变量 + * @param bool $newInstance 是否每次创建新的实例 + * @return mixed + */ + public static function make($class, $args = [], $newInstance = false) + { + if (__CLASS__ != static::class) { + return self::__callStatic('make', func_get_args()); + } + + if (true === $args) { + // 总是创建新的实例化对象 + $newInstance = true; + $args = []; + } + + return self::createFacade($class, $args, $newInstance); + } + + // 调用实际类的方法 + public static function __callStatic($method, $params) + { + return call_user_func_array([static::createFacade(), $method], $params); + } +} diff --git a/vendor/topthink/framework/library/think/File.php b/vendor/topthink/framework/library/think/File.php new file mode 100644 index 0000000..b24b777 --- /dev/null +++ b/vendor/topthink/framework/library/think/File.php @@ -0,0 +1,496 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use SplFileObject; + +class File extends SplFileObject +{ + /** + * 错误信息 + * @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 + */ + protected $hash = []; + + public function __construct($filename, $mode = 'r') + { + parent::__construct($filename, $mode); + + $this->filename = $this->getRealPath() ?: $this->getPathname(); + } + + /** + * 是否测试 + * @access public + * @param bool $test 是否测试 + * @return $this + */ + public function isTest($test = false) + { + $this->isTest = $test; + + return $this; + } + + /** + * 设置上传信息 + * @access public + * @param array $info 上传文件信息 + * @return $this + */ + public function setUploadInfo($info) + { + $this->info = $info; + + return $this; + } + + /** + * 获取上传文件的信息 + * @access public + * @param string $name + * @return array|string + */ + public function getInfo($name = '') + { + return isset($this->info[$name]) ? $this->info[$name] : $this->info; + } + + /** + * 获取上传文件的文件名 + * @access public + * @return string + */ + public function getSaveName() + { + return $this->saveName; + } + + /** + * 设置上传文件的保存文件名 + * @access public + * @param string $saveName + * @return $this + */ + public function setSaveName($saveName) + { + $this->saveName = $saveName; + + return $this; + } + + /** + * 获取文件的哈希散列值 + * @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]; + } + + /** + * 检查目录是否可写 + * @access protected + * @param string $path 目录 + * @return boolean + */ + protected function checkPath($path) + { + if (is_dir($path)) { + return true; + } + + if (mkdir($path, 0755, true)) { + return true; + } + + $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); + } + + /** + * 设置文件的命名规则 + * @access public + * @param string $rule 文件命名规则 + * @return $this + */ + public function rule($rule) + { + $this->rule = $rule; + + return $this; + } + + /** + * 设置上传文件的验证规则 + * @access public + * @param array $rule 验证规则 + * @return $this + */ + public function validate($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); + } + + /** + * 检测上传文件 + * @access public + * @param array $rule 验证规则 + * @return bool + */ + public function check($rule = []) + { + $rule = $rule ?: $this->validate; + + if ((isset($rule['size']) && !$this->checkSize($rule['size'])) + || (isset($rule['type']) && !$this->checkMime($rule['type'])) + || (isset($rule['ext']) && !$this->checkExt($rule['ext'])) + || !$this->checkImg()) { + return false; + } + + return true; + } + + /** + * 检测上传文件后缀 + * @access public + * @param array|string $ext 允许后缀 + * @return bool + */ + public function checkExt($ext) + { + if (is_string($ext)) { + $ext = explode(',', $ext); + } + + $extension = strtolower(pathinfo($this->getInfo('name'), PATHINFO_EXTENSION)); + + if (!in_array($extension, $ext)) { + $this->error = 'extensions to upload is not allowed'; + return false; + } + + return true; + } + + /** + * 检测图像文件 + * @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, 13])) { + $this->error = 'illegal image files'; + return false; + } + + return true; + } + + // 判断图像类型 + protected function getImageType($image) + { + if (function_exists('exif_imagetype')) { + return exif_imagetype($image); + } + + try { + $info = getimagesize($image); + return $info ? $info[2] : false; + } catch (\Exception $e) { + return false; + } + } + + /** + * 检测上传文件大小 + * @access public + * @param integer $size 最大大小 + * @return bool + */ + public function checkSize($size) + { + if ($this->getSize() > (int) $size) { + $this->error = 'filesize not match'; + return false; + } + + return true; + } + + /** + * 检测上传文件类型 + * @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)) { + $this->error = 'mimetype to upload is not allowed'; + return false; + } + + return true; + } + + /** + * 移动文件 + * @access public + * @param string $path 保存路径 + * @param string|bool $savename 保存的文件名 默认自动生成 + * @param boolean $replace 同名文件是否覆盖 + * @param bool $autoAppendExt 自动补充扩展名 + * @return false|File false-失败 否则返回File实例 + */ + public function move($path, $savename = true, $replace = true, $autoAppendExt = true) + { + // 文件上传失败,捕获错误代码 + if (!empty($this->info['error'])) { + $this->error($this->info['error']); + return false; + } + + // 检测合法性 + if (!$this->isValid()) { + $this->error = 'upload illegal files'; + return false; + } + + // 验证上传 + if (!$this->check()) { + return false; + } + + $path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + // 文件保存命名规则 + $saveName = $this->buildSaveName($savename, $autoAppendExt); + $filename = $path . $saveName; + + // 检测目录 + if (false === $this->checkPath(dirname($filename))) { + return false; + } + + /* 不覆盖同名文件 */ + if (!$replace && is_file($filename)) { + $this->error = ['has the same filename: {:filename}', ['filename' => $filename]]; + return false; + } + + /* 移动文件 */ + if ($this->isTest) { + rename($this->filename, $filename); + } elseif (!move_uploaded_file($this->filename, $filename)) { + $this->error = 'upload write error'; + return false; + } + + // 返回 File对象实例 + $file = new self($filename); + $file->setSaveName($saveName); + $file->setUploadInfo($this->info); + + return $file; + } + + /** + * 获取保存文件名 + * @access protected + * @param string|bool $savename 保存的文件名 默认自动生成 + * @param bool $autoAppendExt 自动补充扩展名 + * @return string + */ + protected function buildSaveName($savename, $autoAppendExt = true) + { + if (true === $savename) { + // 自动生成文件名 + $savename = $this->autoBuildName(); + } elseif ('' === $savename || false === $savename) { + // 保留原文件名 + $savename = $this->getInfo('name'); + } + + if ($autoAppendExt && false === strpos($savename, '.')) { + $savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION); + } + + return $savename; + } + + /** + * 自动生成文件名 + * @access protected + * @return string + */ + protected function autoBuildName() + { + if ($this->rule instanceof \Closure) { + $savename = call_user_func_array($this->rule, [$this]); + } else { + switch ($this->rule) { + case 'date': + $savename = date('Ymd') . DIRECTORY_SEPARATOR . md5(microtime(true)); + break; + default: + if (in_array($this->rule, hash_algos())) { + $hash = $this->hash($this->rule); + $savename = substr($hash, 0, 2) . DIRECTORY_SEPARATOR . substr($hash, 2); + } elseif (is_callable($this->rule)) { + $savename = call_user_func($this->rule); + } else { + $savename = date('Ymd') . DIRECTORY_SEPARATOR . md5(microtime(true)); + } + } + } + + return $savename; + } + + /** + * 获取错误代码信息 + * @access private + * @param int $errorNo 错误号 + */ + private function error($errorNo) + { + switch ($errorNo) { + case 1: + case 2: + $this->error = 'upload File size exceeds the maximum value'; + break; + case 3: + $this->error = 'only the portion of file is uploaded'; + break; + case 4: + $this->error = 'no file to uploaded'; + break; + case 6: + $this->error = 'upload temp dir not found'; + break; + case 7: + $this->error = 'file write error'; + break; + default: + $this->error = 'unknown upload error'; + } + } + + /** + * 获取错误信息(支持多语言) + * @access public + * @return string + */ + public function getError() + { + $lang = Container::get('lang'); + + if (is_array($this->error)) { + list($msg, $vars) = $this->error; + } else { + $msg = $this->error; + $vars = []; + } + + return $lang->has($msg) ? $lang->get($msg, $vars) : $msg; + } + + public function __call($method, $args) + { + return $this->hash($method); + } +} diff --git a/vendor/topthink/framework/library/think/Hook.php b/vendor/topthink/framework/library/think/Hook.php new file mode 100644 index 0000000..1d01141 --- /dev/null +++ b/vendor/topthink/framework/library/think/Hook.php @@ -0,0 +1,220 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Hook +{ + /** + * 钩子行为定义 + * @var array + */ + private $tags = []; + + /** + * 绑定行为列表 + * @var array + */ + protected $bind = []; + + /** + * 入口方法名称 + * @var string + */ + private static $portal = 'run'; + + /** + * 应用对象 + * @var App + */ + protected $app; + + public function __construct(App $app) + { + $this->app = $app; + } + + /** + * 指定入口方法名称 + * @access public + * @param string $name 方法名 + * @return $this + */ + public function portal($name) + { + self::$portal = $name; + return $this; + } + + /** + * 指定行为标识 便于调用 + * @access public + * @param string|array $name 行为标识 + * @param mixed $behavior 行为 + * @return $this + */ + public function alias($name, $behavior = null) + { + if (is_array($name)) { + $this->bind = array_merge($this->bind, $name); + } else { + $this->bind[$name] = $behavior; + } + + return $this; + } + + /** + * 动态添加行为扩展到某个标签 + * @access public + * @param string $tag 标签名称 + * @param mixed $behavior 行为名称 + * @param bool $first 是否放到开头执行 + * @return void + */ + public function add($tag, $behavior, $first = false) + { + isset($this->tags[$tag]) || $this->tags[$tag] = []; + + if (is_array($behavior) && !is_callable($behavior)) { + if (!array_key_exists('_overlay', $behavior)) { + $this->tags[$tag] = array_merge($this->tags[$tag], $behavior); + } else { + unset($behavior['_overlay']); + $this->tags[$tag] = $behavior; + } + } elseif ($first) { + array_unshift($this->tags[$tag], $behavior); + } else { + $this->tags[$tag][] = $behavior; + } + } + + /** + * 批量导入插件 + * @access public + * @param array $tags 插件信息 + * @param bool $recursive 是否递归合并 + * @return void + */ + public function import(array $tags, $recursive = true) + { + if ($recursive) { + foreach ($tags as $tag => $behavior) { + $this->add($tag, $behavior); + } + } else { + $this->tags = $tags + $this->tags; + } + } + + /** + * 获取插件信息 + * @access public + * @param string $tag 插件位置 留空获取全部 + * @return array + */ + public function get($tag = '') + { + if (empty($tag)) { + //获取全部的插件信息 + return $this->tags; + } + + return array_key_exists($tag, $this->tags) ? $this->tags[$tag] : []; + } + + /** + * 监听标签的行为 + * @access public + * @param string $tag 标签名称 + * @param mixed $params 传入参数 + * @param bool $once 只获取一个有效返回值 + * @return mixed + */ + public function listen($tag, $params = null, $once = false) + { + $results = []; + $tags = $this->get($tag); + + foreach ($tags as $key => $name) { + $results[$key] = $this->execTag($name, $tag, $params); + + if (false === $results[$key] || (!is_null($results[$key]) && $once)) { + break; + } + } + + return $once ? end($results) : $results; + } + + /** + * 执行行为 + * @access public + * @param mixed $class 行为 + * @param mixed $params 参数 + * @return mixed + */ + public function exec($class, $params = null) + { + if ($class instanceof \Closure || is_array($class)) { + $method = $class; + } else { + if (isset($this->bind[$class])) { + $class = $this->bind[$class]; + } + $method = [$class, self::$portal]; + } + + return $this->app->invoke($method, [$params]); + } + + /** + * 执行某个标签的行为 + * @access protected + * @param mixed $class 要执行的行为 + * @param string $tag 方法名(标签名) + * @param mixed $params 参数 + * @return mixed + */ + protected function execTag($class, $tag = '', $params = null) + { + $method = Loader::parseName($tag, 1, false); + + if ($class instanceof \Closure) { + $call = $class; + $class = 'Closure'; + } elseif (is_array($class) || strpos($class, '::')) { + $call = $class; + } else { + $obj = Container::get($class); + + if (!is_callable([$obj, $method])) { + $method = self::$portal; + } + + $call = [$class, $method]; + $class = $class . '->' . $method; + } + + $result = $this->app->invoke($call, [$params]); + + return $result; + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app']); + + return $data; + } +} diff --git a/vendor/topthink/framework/library/think/Lang.php b/vendor/topthink/framework/library/think/Lang.php new file mode 100644 index 0000000..ed36dd8 --- /dev/null +++ b/vendor/topthink/framework/library/think/Lang.php @@ -0,0 +1,290 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Lang +{ + /** + * 多语言信息 + * @var array + */ + private $lang = []; + + /** + * 当前语言 + * @var string + */ + private $range = 'zh-cn'; + + /** + * 多语言自动侦测变量名 + * @var string + */ + protected $langDetectVar = 'lang'; + + /** + * 多语言cookie变量 + * @var string + */ + protected $langCookieVar = 'think_var'; + + /** + * 允许的多语言列表 + * @var array + */ + protected $allowLangList = []; + + /** + * Accept-Language转义为对应语言包名称 系统默认配置 + * @var string + */ + protected $acceptLanguage = [ + 'zh-hans-cn' => 'zh-cn', + ]; + + /** + * 应用对象 + * @var App + */ + protected $app; + + public function __construct(App $app) + { + $this->app = $app; + } + + // 设定当前的语言 + public function range($range = '') + { + if ('' == $range) { + return $this->range; + } else { + $this->range = $range; + } + } + + /** + * 设置语言定义(不区分大小写) + * @access public + * @param string|array $name 语言变量 + * @param string $value 语言值 + * @param string $range 语言作用域 + * @return mixed + */ + public function set($name, $value = null, $range = '') + { + $range = $range ?: $this->range; + // 批量定义 + if (!isset($this->lang[$range])) { + $this->lang[$range] = []; + } + + if (is_array($name)) { + return $this->lang[$range] = array_change_key_case($name) + $this->lang[$range]; + } + + return $this->lang[$range][strtolower($name)] = $value; + } + + /** + * 加载语言定义(不区分大小写) + * @access public + * @param string|array $file 语言文件 + * @param string $range 语言作用域 + * @return array + */ + public function load($file, $range = '') + { + $range = $range ?: $this->range; + if (!isset($this->lang[$range])) { + $this->lang[$range] = []; + } + + // 批量定义 + if (is_string($file)) { + $file = [$file]; + } + + $lang = []; + + foreach ($file as $_file) { + if (is_file($_file)) { + // 记录加载信息 + $this->app->log('[ LANG ] ' . $_file); + $_lang = include $_file; + if (is_array($_lang)) { + $lang = array_change_key_case($_lang) + $lang; + } + } + } + + if (!empty($lang)) { + $this->lang[$range] = $lang + $this->lang[$range]; + } + + return $this->lang[$range]; + } + + /** + * 获取语言定义(不区分大小写) + * @access public + * @param string|null $name 语言变量 + * @param string $range 语言作用域 + * @return bool + */ + public function has($name, $range = '') + { + $range = $range ?: $this->range; + + return isset($this->lang[$range][strtolower($name)]); + } + + /** + * 获取语言定义(不区分大小写) + * @access public + * @param string|null $name 语言变量 + * @param array $vars 变量替换 + * @param string $range 语言作用域 + * @return mixed + */ + public function get($name = null, $vars = [], $range = '') + { + $range = $range ?: $this->range; + + // 空参数返回所有定义 + if (is_null($name)) { + return $this->lang[$range]; + } + + $key = strtolower($name); + $value = isset($this->lang[$range][$key]) ? $this->lang[$range][$key] : $name; + + // 变量解析 + if (!empty($vars) && is_array($vars)) { + /** + * Notes: + * 为了检测的方便,数字索引的判断仅仅是参数数组的第一个元素的key为数字0 + * 数字索引采用的是系统的 sprintf 函数替换,用法请参考 sprintf 函数 + */ + if (key($vars) === 0) { + // 数字索引解析 + array_unshift($vars, $value); + $value = call_user_func_array('sprintf', $vars); + } else { + // 关联索引解析 + $replace = array_keys($vars); + foreach ($replace as &$v) { + $v = "{:{$v}}"; + } + $value = str_replace($replace, $vars, $value); + } + } + + return $value; + } + + /** + * 自动侦测设置获取语言选择 + * @access public + * @return string + */ + public function detect() + { + // 自动侦测设置获取语言选择 + $langSet = ''; + + if (isset($_GET[$this->langDetectVar])) { + // url中设置了语言变量 + $langSet = strtolower($_GET[$this->langDetectVar]); + } elseif (isset($_COOKIE[$this->langCookieVar])) { + // Cookie中设置了语言变量 + $langSet = strtolower($_COOKIE[$this->langCookieVar]); + } elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + // 自动侦测浏览器语言 + preg_match('/^([a-z\d\-]+)/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $matches); + $langSet = strtolower($matches[1]); + if (isset($this->acceptLanguage[$langSet])) { + $langSet = $this->acceptLanguage[$langSet]; + } + } + + if (preg_match('/^([a-z\d\-]+)/i', $langSet, $matches)) { + $langSet = strtolower($matches[1]); + } else { + $langSet = $this->range; + } + + if (empty($this->allowLangList) || in_array($langSet, $this->allowLangList)) { + // 合法的语言 + $this->range = $langSet ?: $this->range; + } + + return $this->range; + } + + /** + * 设置当前语言到Cookie + * @access public + * @param string $lang 语言 + * @return void + */ + public function saveToCookie($lang = null) + { + $range = $lang ?: $this->range; + + $_COOKIE[$this->langCookieVar] = $range; + } + + /** + * 设置语言自动侦测的变量 + * @access public + * @param string $var 变量名称 + * @return void + */ + public function setLangDetectVar($var) + { + $this->langDetectVar = $var; + } + + /** + * 设置语言的cookie保存变量 + * @access public + * @param string $var 变量名称 + * @return void + */ + public function setLangCookieVar($var) + { + $this->langCookieVar = $var; + } + + /** + * 设置允许的语言列表 + * @access public + * @param array $list 语言列表 + * @return void + */ + public function setAllowLangList(array $list) + { + $this->allowLangList = $list; + } + + /** + * 设置转义的语言列表 + * @access public + * @param array $list 语言列表 + * @return void + */ + public function setAcceptLanguage(array $list) + { + $this->acceptLanguage = array_merge($this->acceptLanguage, $list); + } +} diff --git a/vendor/topthink/framework/library/think/Loader.php b/vendor/topthink/framework/library/think/Loader.php new file mode 100644 index 0000000..d807db6 --- /dev/null +++ b/vendor/topthink/framework/library/think/Loader.php @@ -0,0 +1,417 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ClassNotFoundException; + +class Loader +{ + /** + * 类名映射信息 + * @var array + */ + protected static $classMap = []; + + /** + * 类库别名 + * @var array + */ + protected static $classAlias = []; + + /** + * PSR-4 + * @var array + */ + private static $prefixLengthsPsr4 = []; + private static $prefixDirsPsr4 = []; + private static $fallbackDirsPsr4 = []; + + /** + * PSR-0 + * @var array + */ + private static $prefixesPsr0 = []; + private static $fallbackDirsPsr0 = []; + + /** + * 需要加载的文件 + * @var array + */ + private static $files = []; + + /** + * Composer安装路径 + * @var string + */ + private static $composerPath; + + // 获取应用根目录 + public static function getRootPath() + { + if ('cli' == PHP_SAPI) { + $scriptName = realpath($_SERVER['argv'][0]); + } else { + $scriptName = $_SERVER['SCRIPT_FILENAME']; + } + + $path = realpath(dirname($scriptName)); + + if (!is_file($path . DIRECTORY_SEPARATOR . 'think')) { + $path = dirname($path); + } + + return $path . DIRECTORY_SEPARATOR; + } + + // 注册自动加载机制 + public static function register($autoload = '') + { + // 注册系统自动加载 + spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true); + + $rootPath = self::getRootPath(); + + self::$composerPath = $rootPath . 'vendor' . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR; + + // Composer自动加载支持 + if (is_dir(self::$composerPath)) { + if (is_file(self::$composerPath . 'autoload_static.php')) { + require self::$composerPath . '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::$composerPath); + } + } + + // 注册命名空间定义 + self::addNamespace([ + 'think' => __DIR__, + 'traits' => dirname(__DIR__) . DIRECTORY_SEPARATOR . 'traits', + ]); + + // 加载类库映射文件 + if (is_file($rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'classmap.php')) { + self::addClassMap(__include_file($rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'classmap.php')); + } + + // 自动加载extend目录 + self::addAutoLoadDir($rootPath . 'extend'); + } + + // 自动加载 + public static function autoload($class) + { + if (isset(self::$classAlias[$class])) { + return class_alias(self::$classAlias[$class], $class); + } + + if ($file = self::findFile($class)) { + + // Win环境严格区分大小写 + if (strpos(PHP_OS, 'WIN') !== false && pathinfo($file, PATHINFO_FILENAME) != pathinfo(realpath($file), PATHINFO_FILENAME)) { + return false; + } + + __include_file($file); + return true; + } + } + + /** + * 查找文件 + * @access private + * @param string $class + * @return string|false + */ + private static function findFile($class) + { + if (!empty(self::$classMap[$class])) { + // 类库映射 + return self::$classMap[$class]; + } + + // 查找 PSR-4 + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . '.php'; + + $first = $class[0]; + if (isset(self::$prefixLengthsPsr4[$first])) { + foreach (self::$prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach (self::$prefixDirsPsr4[$prefix] as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // 查找 PSR-4 fallback dirs + foreach (self::$fallbackDirsPsr4 as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // 查找 PSR-0 + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . '.php'; + } + + if (isset(self::$prefixesPsr0[$first])) { + foreach (self::$prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // 查找 PSR-0 fallback dirs + foreach (self::$fallbackDirsPsr0 as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + return self::$classMap[$class] = false; + } + + // 注册classmap + public static function addClassMap($class, $map = '') + { + if (is_array($class)) { + self::$classMap = array_merge(self::$classMap, $class); + } else { + self::$classMap[$class] = $map; + } + } + + // 注册命名空间 + public static function addNamespace($namespace, $path = '') + { + if (is_array($namespace)) { + foreach ($namespace as $prefix => $paths) { + self::addPsr4($prefix . '\\', rtrim($paths, DIRECTORY_SEPARATOR), true); + } + } else { + self::addPsr4($namespace . '\\', rtrim($path, DIRECTORY_SEPARATOR), true); + } + } + + // 添加Ps0空间 + private static function addPsr0($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + self::$fallbackDirsPsr0 = array_merge( + (array) $paths, + self::$fallbackDirsPsr0 + ); + } else { + self::$fallbackDirsPsr0 = array_merge( + self::$fallbackDirsPsr0, + (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空间 + 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 + ); + } + } 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."); + } + + 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 { + // Append directories for an already registered namespace. + self::$prefixDirsPsr4[$prefix] = array_merge( + self::$prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + // 注册自动加载类库目录 + public static function addAutoLoadDir($path) + { + self::$fallbackDirsPsr4[] = $path; + } + + // 注册类别名 + public static function addClassAlias($alias, $class = null) + { + if (is_array($alias)) { + self::$classAlias = array_merge(self::$classAlias, $alias); + } else { + self::$classAlias[$alias] = $class; + } + } + + // 注册composer自动加载 + public static function registerComposerLoader($composerPath) + { + if (is_file($composerPath . 'autoload_namespaces.php')) { + $map = require $composerPath . 'autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + self::addPsr0($namespace, $path); + } + } + + if (is_file($composerPath . 'autoload_psr4.php')) { + $map = require $composerPath . 'autoload_psr4.php'; + foreach ($map as $namespace => $path) { + self::addPsr4($namespace, $path); + } + } + + if (is_file($composerPath . 'autoload_classmap.php')) { + $classMap = require $composerPath . 'autoload_classmap.php'; + if ($classMap) { + self::addClassMap($classMap); + } + } + + if (is_file($composerPath . 'autoload_files.php')) { + self::$files = require $composerPath . '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; + } + } + } + + /** + * 字符串命名风格转换 + * 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) + { + if ($type) { + $name = preg_replace_callback('/_([a-zA-Z])/', function ($match) { + return strtoupper($match[1]); + }, $name); + return $ucfirst ? ucfirst($name) : lcfirst($name); + } + + return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_")); + } + + /** + * 创建工厂对象实例 + * @access public + * @param string $name 工厂类名 + * @param string $namespace 默认命名空间 + * @return mixed + */ + public static function factory($name, $namespace = '', ...$args) + { + $class = false !== strpos($name, '\\') ? $name : $namespace . ucwords($name); + + if (class_exists($class)) { + return Container::getInstance()->invokeClass($class, $args); + } else { + throw new ClassNotFoundException('class not exists:' . $class, $class); + } + } +} + +/** + * 作用范围隔离 + * + * @param $file + * @return mixed + */ +function __include_file($file) +{ + return include $file; +} + +function __require_file($file) +{ + return require $file; +} diff --git a/vendor/topthink/framework/library/think/Log.php b/vendor/topthink/framework/library/think/Log.php new file mode 100644 index 0000000..8902e97 --- /dev/null +++ b/vendor/topthink/framework/library/think/Log.php @@ -0,0 +1,389 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Log implements LoggerInterface +{ + const EMERGENCY = 'emergency'; + const ALERT = 'alert'; + const CRITICAL = 'critical'; + const ERROR = 'error'; + const WARNING = 'warning'; + const NOTICE = 'notice'; + const INFO = 'info'; + const DEBUG = 'debug'; + const SQL = 'sql'; + + /** + * 日志信息 + * @var array + */ + protected $log = []; + + /** + * 配置参数 + * @var array + */ + protected $config = []; + + /** + * 日志写入驱动 + * @var object + */ + protected $driver; + + /** + * 日志授权key + * @var string + */ + protected $key; + + /** + * 是否允许日志写入 + * @var bool + */ + protected $allowWrite = true; + + /** + * 应用对象 + * @var App + */ + protected $app; + + public function __construct(App $app) + { + $this->app = $app; + } + + public static function __make(App $app, Config $config) + { + return (new static($app))->init($config->pull('log')); + } + + /** + * 日志初始化 + * @access public + * @param array $config + * @return $this + */ + public function init($config = []) + { + $type = isset($config['type']) ? $config['type'] : 'File'; + + $this->config = $config; + + unset($config['type']); + + if (!empty($config['close'])) { + $this->allowWrite = false; + } + + $this->driver = Loader::factory($type, '\\think\\log\\driver\\', $config); + + return $this; + } + + /** + * 获取日志信息 + * @access public + * @param string $type 信息类型 + * @return array + */ + public function getLog($type = '') + { + return $type ? $this->log[$type] : $this->log; + } + + /** + * 记录日志信息 + * @access public + * @param mixed $msg 日志信息 + * @param string $type 日志级别 + * @param array $context 替换内容 + * @return $this + */ + public function record($msg, $type = 'info', array $context = []) + { + if (!$this->allowWrite) { + return; + } + + if (is_string($msg) && !empty($context)) { + $replace = []; + foreach ($context as $key => $val) { + $replace['{' . $key . '}'] = $val; + } + + $msg = strtr($msg, $replace); + } + + if (PHP_SAPI == 'cli') { + if (empty($this->config['level']) || in_array($type, $this->config['level'])) { + // 命令行日志实时写入 + $this->write($msg, $type, true); + } + } else { + $this->log[$type][] = $msg; + } + + return $this; + } + + /** + * 清空日志信息 + * @access public + * @return $this + */ + public function clear() + { + $this->log = []; + + return $this; + } + + /** + * 当前日志记录的授权key + * @access public + * @param string $key 授权key + * @return $this + */ + public function key($key) + { + $this->key = $key; + + return $this; + } + + /** + * 检查日志写入权限 + * @access public + * @param array $config 当前日志配置参数 + * @return bool + */ + public function check($config) + { + if ($this->key && !empty($config['allow_key']) && !in_array($this->key, $config['allow_key'])) { + return false; + } + + return true; + } + + /** + * 关闭本次请求日志写入 + * @access public + * @return $this + */ + public function close() + { + $this->allowWrite = false; + $this->log = []; + + return $this; + } + + /** + * 保存调试信息 + * @access public + * @return bool + */ + public function save() + { + if (empty($this->log) || !$this->allowWrite) { + return true; + } + + if (!$this->check($this->config)) { + // 检测日志写入权限 + return false; + } + + $log = []; + + foreach ($this->log as $level => $info) { + if (!$this->app->isDebug() && 'debug' == $level) { + continue; + } + + if (empty($this->config['level']) || in_array($level, $this->config['level'])) { + $log[$level] = $info; + + $this->app['hook']->listen('log_level', [$level, $info]); + } + } + + $result = $this->driver->save($log, true); + + if ($result) { + $this->log = []; + } + + return $result; + } + + /** + * 实时写入日志信息 并支持行为 + * @access public + * @param mixed $msg 调试信息 + * @param string $type 日志级别 + * @param bool $force 是否强制写入 + * @return bool + */ + public function write($msg, $type = 'info', $force = false) + { + // 封装日志信息 + if (empty($this->config['level'])) { + $force = true; + } + + if (true === $force || in_array($type, $this->config['level'])) { + $log[$type][] = $msg; + } else { + return false; + } + + // 监听log_write + $this->app['hook']->listen('log_write', $log); + + // 写入日志 + return $this->driver->save($log, false); + } + + /** + * 记录日志信息 + * @access public + * @param string $level 日志级别 + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function log($level, $message, array $context = []) + { + $this->record($message, $level, $context); + } + + /** + * 记录emergency信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function emergency($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录警报信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function alert($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录紧急情况 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function critical($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录错误信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function error($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录warning信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function warning($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录notice信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function notice($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录一般信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function info($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录调试信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function debug($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录sql信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function sql($message, array $context = []) + { + $this->log(__FUNCTION__, $message, $context); + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app']); + + return $data; + } +} diff --git a/vendor/topthink/framework/library/think/Middleware.php b/vendor/topthink/framework/library/think/Middleware.php new file mode 100644 index 0000000..d3f4360 --- /dev/null +++ b/vendor/topthink/framework/library/think/Middleware.php @@ -0,0 +1,205 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use InvalidArgumentException; +use LogicException; +use think\exception\HttpResponseException; + +class Middleware +{ + protected $queue = []; + protected $app; + protected $config = [ + 'default_namespace' => 'app\\http\\middleware\\', + ]; + + public function __construct(App $app, array $config = []) + { + $this->app = $app; + $this->config = array_merge($this->config, $config); + } + + public static function __make(App $app, Config $config) + { + return new static($app, $config->pull('middleware')); + } + + public function setConfig(array $config) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 导入中间件 + * @access public + * @param array $middlewares + * @param string $type 中间件类型 + */ + public function import(array $middlewares = [], $type = 'route') + { + foreach ($middlewares as $middleware) { + $this->add($middleware, $type); + } + } + + /** + * 注册中间件 + * @access public + * @param mixed $middleware + * @param string $type 中间件类型 + */ + public function add($middleware, $type = 'route') + { + if (is_null($middleware)) { + return; + } + + $middleware = $this->buildMiddleware($middleware, $type); + + if ($middleware) { + $this->queue[$type][] = $middleware; + } + } + + /** + * 注册控制器中间件 + * @access public + * @param mixed $middleware + */ + public function controller($middleware) + { + return $this->add($middleware, 'controller'); + } + + /** + * 移除中间件 + * @access public + * @param mixed $middleware + * @param string $type 中间件类型 + */ + public function unshift($middleware, $type = 'route') + { + if (is_null($middleware)) { + return; + } + + $middleware = $this->buildMiddleware($middleware, $type); + + if ($middleware) { + array_unshift($this->queue[$type], $middleware); + } + } + + /** + * 获取注册的中间件 + * @access public + * @param string $type 中间件类型 + */ + public function all($type = 'route') + { + return $this->queue[$type] ?: []; + } + + /** + * 清除中间件 + * @access public + */ + public function clear() + { + $this->queue = []; + } + + /** + * 中间件调度 + * @access public + * @param Request $request + * @param string $type 中间件类型 + */ + public function dispatch(Request $request, $type = 'route') + { + return call_user_func($this->resolve($type), $request); + } + + /** + * 解析中间件 + * @access protected + * @param mixed $middleware + * @param string $type 中间件类型 + */ + protected function buildMiddleware($middleware, $type = 'route') + { + if (is_array($middleware)) { + list($middleware, $param) = $middleware; + } + + if ($middleware instanceof \Closure) { + return [$middleware, isset($param) ? $param : null]; + } + + if (!is_string($middleware)) { + throw new InvalidArgumentException('The middleware is invalid'); + } + + if (false === strpos($middleware, '\\')) { + if (isset($this->config[$middleware])) { + $middleware = $this->config[$middleware]; + } else { + $middleware = $this->config['default_namespace'] . $middleware; + } + } + + if (is_array($middleware)) { + return $this->import($middleware, $type); + } + + if (strpos($middleware, ':')) { + list($middleware, $param) = explode(':', $middleware, 2); + } + + return [[$this->app->make($middleware), 'handle'], isset($param) ? $param : null]; + } + + protected function resolve($type = 'route') + { + return function (Request $request) use ($type) { + + $middleware = array_shift($this->queue[$type]); + + if (null === $middleware) { + throw new InvalidArgumentException('The queue was exhausted, with no response returned'); + } + + list($call, $param) = $middleware; + + try { + $response = call_user_func_array($call, [$request, $this->resolve($type), $param]); + } catch (HttpResponseException $exception) { + $response = $exception->getResponse(); + } + + if (!$response instanceof Response) { + throw new LogicException('The middleware must return Response instance'); + } + + return $response; + }; + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app']); + + return $data; + } +} diff --git a/vendor/topthink/framework/library/think/Model.php b/vendor/topthink/framework/library/think/Model.php new file mode 100644 index 0000000..50f2ca1 --- /dev/null +++ b/vendor/topthink/framework/library/think/Model.php @@ -0,0 +1,1125 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use InvalidArgumentException; +use think\db\Query; + +/** + * Class Model + * @package think + * @mixin Query + * @method $this scope(string|array $scope) static 查询范围 + * @method $this where(mixed $field, string $op = null, mixed $condition = null) static 查询条件 + * @method $this whereRaw(string $where, array $bind = [], string $logic = 'AND') static 表达式查询 + * @method $this whereExp(string $field, string $condition, array $bind = [], string $logic = 'AND') static 字段表达式查询 + * @method $this when(mixed $condition, mixed $query, mixed $otherwise = null) static 条件查询 + * @method $this join(mixed $join, mixed $condition = null, string $type = 'INNER', array $bind = []) static JOIN查询 + * @method $this view(mixed $join, mixed $field = null, mixed $on = null, string $type = 'INNER') static 视图查询 + * @method $this with(mixed $with, callable $callback = null) static 关联预载入 + * @method $this count(string $field = '*') static Count统计查询 + * @method $this min(string $field, bool $force = true) static Min统计查询 + * @method $this max(string $field, bool $force = true) static Max统计查询 + * @method $this sum(string $field) static SUM统计查询 + * @method $this avg(string $field) static Avg统计查询 + * @method $this field(mixed $field, boolean $except = false, string $tableName = '', string $prefix = '', string $alias = '') static 指定查询字段 + * @method $this fieldRaw(string $field) static 指定查询字段 + * @method $this union(mixed $union, boolean $all = false) static UNION查询 + * @method $this limit(mixed $offset, integer $length = null) static 查询LIMIT + * @method $this order(mixed $field, string $order = null) static 查询ORDER + * @method $this orderRaw(string $field, array $bind = []) static 查询ORDER + * @method $this cache(mixed $key = null, integer|\DateTime $expire = null, string $tag = null) static 设置查询缓存 + * @method mixed value(string $field, mixed $default = null) static 获取某个字段的值 + * @method array column(string $field, string $key = '') static 获取某个列的值 + * @method $this find(mixed $data = null) static 查询单个记录 + * @method $this findOrFail(mixed $data = null) 查询单个记录 + * @method Collection|$this[] select(mixed $data = null) static 查询多个记录 + * @method $this get(mixed $data = null, mixed $with = [], bool $cache = false, bool $failException = false) static 查询单个记录 支持关联预载入 + * @method $this getOrFail(mixed $data = null, mixed $with = [], bool $cache = false) static 查询单个记录 不存在则抛出异常 + * @method $this findOrEmpty(mixed $data = null) static 查询单个记录 不存在则返回空模型 + * @method Collection|$this[] all(mixed $data = null, mixed $with = [], bool $cache = false) static 查询多个记录 支持关联预载入 + * @method $this withAttr(array $name, \Closure $closure = null) static 动态定义获取器 + * @method $this withJoin(string|array $with, string $joinType = '') static + * @method $this withCount(string|array $relation, bool $subQuery = true) static 关联统计 + * @method $this withSum(string|array $relation, string $field, bool $subQuery = true) static 关联SUM统计 + * @method $this withMax(string|array $relation, string $field, bool $subQuery = true) static 关联MAX统计 + * @method $this withMin(string|array $relation, string $field, bool $subQuery = true) static 关联Min统计 + * @method $this withAvg(string|array $relation, string $field, bool $subQuery = true) static 关联Avg统计 + * @method Paginator|$this paginate(int|array $listRows = null, int|bool $simple = false, array $config = []) static 分页 + */ +abstract class Model implements \JsonSerializable, \ArrayAccess +{ + use model\concern\Attribute; + use model\concern\RelationShip; + use model\concern\ModelEvent; + use model\concern\TimeStamp; + use model\concern\Conversion; + + /** + * 是否存在数据 + * @var bool + */ + private $exists = false; + + /** + * 是否Replace + * @var bool + */ + private $replace = false; + + /** + * 是否强制更新所有数据 + * @var bool + */ + private $force = false; + + /** + * 更新条件 + * @var array + */ + private $updateWhere; + + /** + * 数据库配置信息 + * @var array|string + */ + protected $connection = []; + + /** + * 数据库查询对象类名 + * @var string + */ + protected $query; + + /** + * 模型名称 + * @var string + */ + protected $name; + + /** + * 数据表名称 + * @var string + */ + protected $table; + + /** + * 写入自动完成定义 + * @var array + */ + protected $auto = []; + + /** + * 新增自动完成定义 + * @var array + */ + protected $insert = []; + + /** + * 更新自动完成定义 + * @var array + */ + protected $update = []; + + /** + * 初始化过的模型. + * @var array + */ + protected static $initialized = []; + + /** + * 是否从主库读取(主从分布式有效) + * @var array + */ + protected static $readMaster; + + /** + * 查询对象实例 + * @var Query + */ + protected $queryInstance; + + /** + * 错误信息 + * @var mixed + */ + protected $error; + + /** + * 软删除字段默认值 + * @var mixed + */ + protected $defaultSoftDelete; + + /** + * 全局查询范围 + * @var array + */ + protected $globalScope = []; + + /** + * 架构函数 + * @access public + * @param array|object $data 数据 + */ + public function __construct($data = []) + { + if (is_object($data)) { + $this->data = get_object_vars($data); + } 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; + + $config = Db::getConfig(); + + if (empty($this->name)) { + // 当前模型名 + $name = str_replace('\\', '/', static::class); + $this->name = basename($name); + if (Container::get('config')->get('class_suffix')) { + $suffix = basename(dirname($name)); + $this->name = substr($this->name, 0, -strlen($suffix)); + } + } + + if (is_null($this->autoWriteTimestamp)) { + // 自动写入时间戳 + $this->autoWriteTimestamp = $config['auto_timestamp']; + } + + if (is_null($this->dateFormat)) { + // 设置时间戳格式 + $this->dateFormat = $config['datetime_format']; + } + + if (is_null($this->resultSetType)) { + $this->resultSetType = $config['resultset_type']; + } + + if (!empty($this->connection) && is_array($this->connection)) { + // 设置模型的数据库连接 + $this->connection = array_merge($config, $this->connection); + } + + if ($this->observerClass) { + // 注册模型观察者 + static::observe($this->observerClass); + } + + // 执行初始化操作 + $this->initialize(); + } + + /** + * 获取当前模型名称 + * @access public + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 是否从主库读取数据(主从分布有效) + * @access public + * @param bool $all 是否所有模型有效 + * @return $this + */ + public function readMaster($all = false) + { + $model = $all ? '*' : static::class; + + static::$readMaster[$model] = true; + + return $this; + } + + /** + * 创建新的模型实例 + * @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 protected + * @return Query + */ + protected function buildQuery() + { + // 设置当前模型 确保查询返回模型对象 + $query = Db::connect($this->connection, false, $this->query); + $query->model($this) + ->name($this->name) + ->json($this->json, $this->jsonAssoc) + ->setJsonFieldType($this->jsonType); + + if (isset(static::$readMaster['*']) || isset(static::$readMaster[static::class])) { + $query->master(true); + } + + // 设置当前数据表和模型名 + if (!empty($this->table)) { + $query->table($this->table); + } + + if (!empty($this->pk)) { + $query->pk($this->pk); + } + + return $query; + } + + /** + * 获取当前模型的数据库查询对象 + * @access public + * @param Query $query 查询对象实例 + * @return $this + */ + public function setQuery($query) + { + $this->queryInstance = $query; + return $this; + } + + /** + * 获取当前模型的数据库查询对象 + * @access public + * @param bool|array $useBaseQuery 是否调用全局查询范围(或者指定查询范围名称) + * @return Query + */ + public function db($useBaseQuery = true) + { + if ($this->queryInstance) { + return $this->queryInstance; + } + + $query = $this->buildQuery(); + + // 软删除 + if (property_exists($this, 'withTrashed') && !$this->withTrashed) { + $this->withNoTrashed($query); + } + + // 全局作用域 + if (true === $useBaseQuery && method_exists($this, 'base')) { + call_user_func_array([$this, 'base'], [ & $query]); + } + + $globalScope = is_array($useBaseQuery) && $useBaseQuery ? $useBaseQuery : $this->globalScope; + + if ($globalScope && false !== $useBaseQuery) { + $query->scope($globalScope); + } + + // 返回当前模型的数据库查询对象 + return $query; + } + + /** + * 初始化模型 + * @access protected + * @return void + */ + protected function initialize() + { + if (!isset(static::$initialized[static::class])) { + static::$initialized[static::class] = true; + static::init(); + } + } + + /** + * 初始化处理 + * @access protected + * @return void + */ + protected static function init() + {} + + /** + * 数据自动完成 + * @access protected + * @param array $auto 要自动更新的字段列表 + * @return void + */ + protected function autoCompleteData($auto = []) + { + foreach ($auto as $field => $value) { + if (is_integer($field)) { + $field = $value; + $value = null; + } + + if (!isset($this->data[$field])) { + $default = null; + } else { + $default = $this->data[$field]; + } + + $this->setAttr($field, !is_null($value) ? $value : $default); + } + } + + /** + * 更新是否强制写入数据 而不做比较 + * @access public + * @param bool $force + * @return $this + */ + public function force($force = true) + { + $this->force = $force; + return $this; + } + + /** + * 判断force + * @access public + * @return bool + */ + public function isForce() + { + return $this->force; + } + + /** + * 新增数据是否使用Replace + * @access public + * @param bool $replace + * @return $this + */ + public function replace($replace = true) + { + $this->replace = $replace; + return $this; + } + + /** + * 设置数据是否存在 + * @access public + * @param bool $exists + * @return $this + */ + public function exists($exists) + { + $this->exists = $exists; + return $this; + } + + /** + * 判断数据是否存在数据库 + * @access public + * @return bool + */ + public function isExists() + { + return $this->exists; + } + + /** + * 判断模型是否为空 + * @access public + * @return bool + */ + public function isEmpty() + { + return empty($this->data); + } + + /** + * 保存当前数据对象 + * @access public + * @param array $data 数据 + * @param array $where 更新条件 + * @param string $sequence 自增序列名 + * @return bool + */ + public function save($data = [], $where = [], $sequence = null) + { + if (is_string($data)) { + $sequence = $data; + $data = []; + } + + if (!$this->checkBeforeSave($data, $where)) { + return false; + } + + $result = $this->exists ? $this->updateData($where) : $this->insertData($sequence); + + if (false === $result) { + return false; + } + + // 写入回调 + $this->trigger('after_write'); + + // 重新记录原始数据 + $this->origin = $this->data; + $this->set = []; + + return true; + } + + /** + * 写入之前检查数据 + * @access protected + * @param array $data 数据 + * @param array $where 保存条件 + * @return bool + */ + protected function checkBeforeSave($data, $where) + { + if (!empty($data)) { + // 数据对象赋值 + foreach ($data as $key => $value) { + $this->setAttr($key, $value, $data); + } + + if (!empty($where)) { + $this->exists = true; + $this->updateWhere = $where; + } + } + + // 数据自动完成 + $this->autoCompleteData($this->auto); + + // 事件回调 + if (false === $this->trigger('before_write')) { + return false; + } + + return true; + } + + /** + * 检查数据是否允许写入 + * @access protected + * @param array $append 自动完成的字段列表 + * @return array + */ + protected function checkAllowFields(array $append = []) + { + // 检测字段 + if (empty($this->field) || true === $this->field) { + $query = $this->db(false); + $table = $this->table ?: $query->getTable(); + + $this->field = $query->getConnection()->getTableFields($table); + + $field = $this->field; + } else { + $field = array_merge($this->field, $append); + + if ($this->autoWriteTimestamp) { + array_push($field, $this->createTime, $this->updateTime); + } + } + + if ($this->disuse) { + // 废弃字段 + $field = array_diff($field, (array) $this->disuse); + } + + return $field; + } + + /** + * 更新写入数据 + * @access protected + * @param mixed $where 更新条件 + * @return bool + */ + protected function updateData($where) + { + // 自动更新 + $this->autoCompleteData($this->update); + + // 事件回调 + if (false === $this->trigger('before_update')) { + return false; + } + + // 获取有更新的数据 + $data = $this->getChangedData(); + + if (empty($data)) { + // 关联更新 + if (!empty($this->relationWrite)) { + $this->autoRelationUpdate(); + } + + return true; + } elseif ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) { + // 自动写入更新时间 + $data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime); + + $this->data[$this->updateTime] = $data[$this->updateTime]; + } + + if (empty($where) && !empty($this->updateWhere)) { + $where = $this->updateWhere; + } + + // 检查允许字段 + $allowFields = $this->checkAllowFields(array_merge($this->auto, $this->update)); + + // 保留主键数据 + foreach ($this->data as $key => $val) { + if ($this->isPk($key)) { + $data[$key] = $val; + } + } + + $pk = $this->getPk(); + $array = []; + + foreach ((array) $pk as $key) { + if (isset($data[$key])) { + $array[] = [$key, '=', $data[$key]]; + unset($data[$key]); + } + } + + if (!empty($array)) { + $where = $array; + } + + foreach ((array) $this->relationWrite as $name => $val) { + if (is_array($val)) { + foreach ($val as $key) { + if (isset($data[$key])) { + unset($data[$key]); + } + } + } + } + + // 模型更新 + $db = $this->db(false); + $db->startTrans(); + + try { + $db->where($where) + ->strict(false) + ->field($allowFields) + ->update($data); + + // 关联更新 + if (!empty($this->relationWrite)) { + $this->autoRelationUpdate(); + } + + $db->commit(); + + // 更新回调 + $this->trigger('after_update'); + + return true; + } catch (\Exception $e) { + $db->rollback(); + throw $e; + } + } + + /** + * 新增写入数据 + * @access protected + * @param string $sequence 自增序列名 + * @return bool + */ + protected function insertData($sequence) + { + // 自动写入 + $this->autoCompleteData($this->insert); + + // 时间戳自动写入 + $this->checkTimeStampWrite(); + + if (false === $this->trigger('before_insert')) { + return false; + } + + // 检查允许字段 + $allowFields = $this->checkAllowFields(array_merge($this->auto, $this->insert)); + + $db = $this->db(false); + $db->startTrans(); + + try { + $result = $db->strict(false) + ->field($allowFields) + ->insert($this->data, $this->replace, false, $sequence); + + // 获取自动增长主键 + if ($result && $insertId = $db->getLastInsID($sequence)) { + $pk = $this->getPk(); + + foreach ((array) $pk as $key) { + if (!isset($this->data[$key]) || '' == $this->data[$key]) { + $this->data[$key] = $insertId; + } + } + } + + // 关联写入 + if (!empty($this->relationWrite)) { + $this->autoRelationInsert(); + } + + $db->commit(); + + // 标记为更新 + $this->exists = true; + + // 新增回调 + $this->trigger('after_insert'); + + return true; + } catch (\Exception $e) { + $db->rollback(); + throw $e; + } + } + + /** + * 字段值(延迟)增长 + * @access public + * @param string $field 字段名 + * @param integer $step 增长值 + * @param integer $lazyTime 延时时间(s) + * @return bool + * @throws Exception + */ + public function setInc($field, $step = 1, $lazyTime = 0) + { + // 读取更新条件 + $where = $this->getWhere(); + + // 事件回调 + if (false === $this->trigger('before_update')) { + return false; + } + + $result = $this->db(false) + ->where($where) + ->setInc($field, $step, $lazyTime); + + if (true !== $result) { + $this->data[$field] += $step; + } + + // 更新回调 + $this->trigger('after_update'); + + return true; + } + + /** + * 字段值(延迟)减少 + * @access public + * @param string $field 字段名 + * @param integer $step 减少值 + * @param integer $lazyTime 延时时间(s) + * @return bool + * @throws Exception + */ + public function setDec($field, $step = 1, $lazyTime = 0) + { + // 读取更新条件 + $where = $this->getWhere(); + + // 事件回调 + if (false === $this->trigger('before_update')) { + return false; + } + + $result = $this->db(false) + ->where($where) + ->setDec($field, $step, $lazyTime); + + if (true !== $result) { + $this->data[$field] -= $step; + } + + // 更新回调 + $this->trigger('after_update'); + + return true; + } + + /** + * 获取当前的更新条件 + * @access protected + * @return mixed + */ + protected function getWhere() + { + // 删除条件 + $pk = $this->getPk(); + + $where = []; + if (is_string($pk) && isset($this->data[$pk])) { + $where[] = [$pk, '=', $this->data[$pk]]; + } elseif (is_array($pk)) { + foreach ($pk as $field) { + if (isset($this->data[$field])) { + $where[] = [$field, '=', $this->data[$field]]; + } + } + } + + if (empty($where)) { + $where = empty($this->updateWhere) ? null : $this->updateWhere; + } + + return $where; + } + + /** + * 保存多个数据到当前数据对象 + * @access public + * @param array $dataSet 数据 + * @param boolean $replace 是否自动识别更新和写入 + * @return Collection + * @throws \Exception + */ + public function saveAll($dataSet, $replace = true) + { + $db = $this->db(false); + $db->startTrans(); + + try { + $pk = $this->getPk(); + + if (is_string($pk) && $replace) { + $auto = true; + } + + $result = []; + + foreach ($dataSet as $key => $data) { + if ($this->exists || (!empty($auto) && isset($data[$pk]))) { + $result[$key] = self::update($data, [], $this->field); + } else { + $result[$key] = self::create($data, $this->field, $this->replace); + } + } + + $db->commit(); + + return $this->toCollection($result); + } catch (\Exception $e) { + $db->rollback(); + throw $e; + } + } + + /** + * 是否为更新数据 + * @access public + * @param mixed $update + * @param mixed $where + * @return $this + */ + public function isUpdate($update = true, $where = null) + { + if (is_bool($update)) { + $this->exists = $update; + + if (!empty($where)) { + $this->updateWhere = $where; + } + } else { + $this->exists = true; + $this->updateWhere = $update; + } + + return $this; + } + + /** + * 删除当前的记录 + * @access public + * @return bool + */ + public function delete() + { + if (!$this->exists || false === $this->trigger('before_delete')) { + return false; + } + + // 读取更新条件 + $where = $this->getWhere(); + + $db = $this->db(false); + $db->startTrans(); + + try { + // 删除当前模型数据 + $db->where($where)->delete(); + + // 关联删除 + if (!empty($this->relationWrite)) { + $this->autoRelationDelete(); + } + + $db->commit(); + + $this->trigger('after_delete'); + + $this->exists = false; + + return true; + } catch (\Exception $e) { + $db->rollback(); + throw $e; + } + } + + /** + * 设置自动完成的字段( 规则通过修改器定义) + * @access public + * @param array $fields 需要自动完成的字段 + * @return $this + */ + public function auto($fields) + { + $this->auto = $fields; + + return $this; + } + + /** + * 写入数据 + * @access public + * @param array $data 数据数组 + * @param array|true $field 允许字段 + * @param bool $replace 使用Replace + * @return static + */ + public static function create($data = [], $field = null, $replace = false) + { + $model = new static(); + + if (!empty($field)) { + $model->allowField($field); + } + + $model->isUpdate(false)->replace($replace)->save($data, []); + + return $model; + } + + /** + * 更新数据 + * @access public + * @param array $data 数据数组 + * @param array $where 更新条件 + * @param array|true $field 允许字段 + * @return static + */ + public static function update($data = [], $where = [], $field = null) + { + $model = new static(); + + if (!empty($field)) { + $model->allowField($field); + } + + $model->isUpdate(true)->save($data, $where); + + return $model; + } + + /** + * 删除记录 + * @access public + * @param mixed $data 主键列表 支持闭包查询条件 + * @return bool + */ + public static function destroy($data) + { + if (empty($data) && 0 !== $data) { + return false; + } + + $model = new static(); + + $query = $model->db(); + + if (is_array($data) && key($data) !== 0) { + $query->where($data); + $data = null; + } elseif ($data instanceof \Closure) { + $data($query); + $data = null; + } + + $resultSet = $query->select($data); + + if ($resultSet) { + foreach ($resultSet as $data) { + $data->delete(); + } + } + + return true; + } + + /** + * 获取错误信息 + * @access public + * @return mixed + */ + public function getError() + { + return $this->error; + } + + /** + * 解序列化后处理 + */ + public function __wakeup() + { + $this->initialize(); + } + + public function __debugInfo() + { + return [ + 'data' => $this->data, + 'relation' => $this->relation, + ]; + } + + /** + * 修改器 设置数据对象的值 + * @access public + * @param string $name 名称 + * @param mixed $value 值 + * @return void + */ + public function __set($name, $value) + { + $this->setAttr($name, $value); + } + + /** + * 获取器 获取数据对象的值 + * @access public + * @param string $name 名称 + * @return mixed + */ + public function __get($name) + { + return $this->getAttr($name); + } + + /** + * 检测数据对象的值 + * @access public + * @param string $name 名称 + * @return boolean + */ + public function __isset($name) + { + try { + return !is_null($this->getAttr($name)); + } catch (InvalidArgumentException $e) { + return false; + } + } + + /** + * 销毁数据对象的值 + * @access public + * @param string $name 名称 + * @return void + */ + public function __unset($name) + { + unset($this->data[$name], $this->relation[$name]); + } + + // ArrayAccess + public function offsetSet($name, $value) + { + $this->setAttr($name, $value); + } + + public function offsetExists($name) + { + return $this->__isset($name); + } + + public function offsetUnset($name) + { + $this->__unset($name); + } + + public function offsetGet($name) + { + return $this->getAttr($name); + } + + /** + * 设置是否使用全局查询范围 + * @access public + * @param bool|array $use 是否启用全局查询范围(或者用数组指定查询范围名称) + * @return Query + */ + public static function useGlobalScope($use) + { + $model = new static(); + + return $model->db($use); + } + + public function __call($method, $args) + { + if ('withattr' == strtolower($method)) { + return call_user_func_array([$this, 'withAttribute'], $args); + } + + return call_user_func_array([$this->db(), $method], $args); + } + + public static function __callStatic($method, $args) + { + $model = new static(); + + return call_user_func_array([$model->db(), $method], $args); + } +} diff --git a/vendor/topthink/framework/library/think/Paginator.php b/vendor/topthink/framework/library/think/Paginator.php new file mode 100644 index 0000000..bbe63e2 --- /dev/null +++ b/vendor/topthink/framework/library/think/Paginator.php @@ -0,0 +1,445 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use ArrayAccess; +use ArrayIterator; +use Countable; +use IteratorAggregate; +use JsonSerializable; +use Traversable; + +abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable +{ + /** + * 是否简洁模式 + * @var bool + */ + protected $simple = false; + + /** + * 数据集 + * @var Collection + */ + protected $items; + + /** + * 当前页 + * @var integer + */ + protected $currentPage; + + /** + * 最后一页 + * @var integer + */ + protected $lastPage; + + /** + * 数据总数 + * @var integer|null + */ + protected $total; + + /** + * 每页数量 + * @var integer + */ + protected $listRows; + + /** + * 是否有下一页 + * @var bool + */ + protected $hasMore; + + /** + * 分页配置 + * @var array + */ + protected $options = [ + 'var_page' => 'page', + 'path' => '/', + 'query' => [], + 'fragment' => '', + ]; + + public function __construct($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = []) + { + $this->options = array_merge($this->options, $options); + + $this->options['path'] = '/' != $this->options['path'] ? rtrim($this->options['path'], '/') : $this->options['path']; + + $this->simple = $simple; + $this->listRows = $listRows; + + if (!$items instanceof Collection) { + $items = Collection::make($items); + } + + if ($simple) { + $this->currentPage = $this->setCurrentPage($currentPage); + $this->hasMore = count($items) > ($this->listRows); + $items = $items->slice(0, $this->listRows); + } else { + $this->total = $total; + $this->lastPage = (int) ceil($total / $listRows); + $this->currentPage = $this->setCurrentPage($currentPage); + $this->hasMore = $this->currentPage < $this->lastPage; + } + $this->items = $items; + } + + /** + * @access public + * @param $items + * @param $listRows + * @param null $currentPage + * @param null $total + * @param bool $simple + * @param array $options + * @return Paginator + */ + public static function make($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = []) + { + return new static($items, $listRows, $currentPage, $total, $simple, $options); + } + + protected function setCurrentPage($currentPage) + { + if (!$this->simple && $currentPage > $this->lastPage) { + return $this->lastPage > 0 ? $this->lastPage : 1; + } + + return $currentPage; + } + + /** + * 获取页码对应的链接 + * + * @access protected + * @param $page + * @return string + */ + protected function url($page) + { + if ($page <= 0) { + $page = 1; + } + + if (strpos($this->options['path'], '[PAGE]') === false) { + $parameters = [$this->options['var_page'] => $page]; + $path = $this->options['path']; + } else { + $parameters = []; + $path = str_replace('[PAGE]', $page, $this->options['path']); + } + + if (count($this->options['query']) > 0) { + $parameters = array_merge($this->options['query'], $parameters); + } + + $url = $path; + if (!empty($parameters)) { + $url .= '?' . http_build_query($parameters, null, '&'); + } + + return $url . $this->buildFragment(); + } + + /** + * 自动获取当前页码 + * @access public + * @param string $varPage + * @param int $default + * @return int + */ + public static function getCurrentPage($varPage = 'page', $default = 1) + { + $page = Container::get('request')->param($varPage); + + if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) { + return $page; + } + + return $default; + } + + /** + * 自动获取当前的path + * @access public + * @return string + */ + public static function getCurrentPath() + { + return Container::get('request')->baseUrl(); + } + + public function total() + { + if ($this->simple) { + throw new \DomainException('not support total'); + } + + return $this->total; + } + + public function listRows() + { + return $this->listRows; + } + + public function currentPage() + { + return $this->currentPage; + } + + public function lastPage() + { + if ($this->simple) { + throw new \DomainException('not support last'); + } + + return $this->lastPage; + } + + /** + * 数据是否足够分页 + * @access public + * @return boolean + */ + public function hasPages() + { + return !(1 == $this->currentPage && !$this->hasMore); + } + + /** + * 创建一组分页链接 + * + * @access public + * @param int $start + * @param int $end + * @return array + */ + public function getUrlRange($start, $end) + { + $urls = []; + + for ($page = $start; $page <= $end; $page++) { + $urls[$page] = $this->url($page); + } + + return $urls; + } + + /** + * 设置URL锚点 + * + * @access public + * @param string|null $fragment + * @return $this + */ + public function fragment($fragment) + { + $this->options['fragment'] = $fragment; + + return $this; + } + + /** + * 添加URL参数 + * + * @access public + * @param array|string $key + * @param string|null $value + * @return $this + */ + public function appends($key, $value = null) + { + if (!is_array($key)) { + $queries = [$key => $value]; + } else { + $queries = $key; + } + + foreach ($queries as $k => $v) { + if ($k !== $this->options['var_page']) { + $this->options['query'][$k] = $v; + } + } + + return $this; + } + + /** + * 构造锚点字符串 + * + * @access public + * @return string + */ + protected function buildFragment() + { + return $this->options['fragment'] ? '#' . $this->options['fragment'] : ''; + } + + /** + * 渲染分页html + * @access public + * @return mixed + */ + abstract public function render(); + + public function items() + { + return $this->items->all(); + } + + public function getCollection() + { + return $this->items; + } + + public function isEmpty() + { + return $this->items->isEmpty(); + } + + /** + * 给每个元素执行个回调 + * + * @access public + * @param callable $callback + * @return $this + */ + public function each(callable $callback) + { + foreach ($this->items as $key => $item) { + $result = $callback($item, $key); + + if (false === $result) { + break; + } elseif (!is_object($item)) { + $this->items[$key] = $result; + } + } + + return $this; + } + + /** + * Retrieve an external iterator + * @access public + * @return Traversable An instance of an object implementing Iterator or + * Traversable + */ + public function getIterator() + { + return new ArrayIterator($this->items->all()); + } + + /** + * Whether a offset exists + * @access public + * @param mixed $offset + * @return bool + */ + public function offsetExists($offset) + { + return $this->items->offsetExists($offset); + } + + /** + * Offset to retrieve + * @access public + * @param mixed $offset + * @return mixed + */ + public function offsetGet($offset) + { + return $this->items->offsetGet($offset); + } + + /** + * Offset to set + * @access public + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet($offset, $value) + { + $this->items->offsetSet($offset, $value); + } + + /** + * Offset to unset + * @access public + * @param mixed $offset + * @return void + * @since 5.0.0 + */ + public function offsetUnset($offset) + { + $this->items->offsetUnset($offset); + } + + /** + * Count elements of an object + */ + public function count() + { + return $this->items->count(); + } + + public function __toString() + { + return (string) $this->render(); + } + + public function toArray() + { + try { + $total = $this->total(); + } catch (\DomainException $e) { + $total = null; + } + + return [ + 'total' => $total, + 'per_page' => $this->listRows(), + 'current_page' => $this->currentPage(), + 'last_page' => $this->lastPage, + 'data' => $this->items->toArray(), + ]; + } + + /** + * Specify data which should be serialized to JSON + */ + public function jsonSerialize() + { + return $this->toArray(); + } + + public function __call($name, $arguments) + { + $collection = $this->getCollection(); + + $result = call_user_func_array([$collection, $name], $arguments); + + if ($result === $collection) { + return $this; + } + + return $result; + } + +} diff --git a/vendor/topthink/framework/library/think/Process.php b/vendor/topthink/framework/library/think/Process.php new file mode 100644 index 0000000..3b574db --- /dev/null +++ b/vendor/topthink/framework/library/think/Process.php @@ -0,0 +1,1268 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\process\exception\Failed as ProcessFailedException; +use think\process\exception\Timeout as ProcessTimeoutException; +use think\process\pipes\Pipes; +use think\process\pipes\Unix as UnixPipes; +use think\process\pipes\Windows as WindowsPipes; +use think\process\Utils; + +class Process +{ + + const ERR = 'err'; + const OUT = 'out'; + + const STATUS_READY = 'ready'; + const STATUS_STARTED = 'started'; + const STATUS_TERMINATED = 'terminated'; + + const STDIN = 0; + const STDOUT = 1; + const STDERR = 2; + + const TIMEOUT_PRECISION = 0.2; + + private $callback; + private $commandline; + private $cwd; + private $env; + private $input; + private $starttime; + private $lastOutputTime; + private $timeout; + private $idleTimeout; + private $options; + private $exitcode; + private $fallbackExitcode; + private $processInformation; + private $outputDisabled = false; + private $stdout; + private $stderr; + private $enhanceWindowsCompatibility = true; + private $enhanceSigchildCompatibility; + private $process; + private $status = self::STATUS_READY; + private $incrementalOutputOffset = 0; + private $incrementalErrorOutputOffset = 0; + private $tty; + private $pty; + + private $useFileHandles = false; + + /** @var Pipes */ + private $processPipes; + + private $latestSignal; + + private static $sigchild; + + /** + * @var array + */ + public static $exitCodes = [ + 0 => 'OK', + 1 => 'General error', + 2 => 'Misuse of shell builtins', + 126 => 'Invoked command cannot execute', + 127 => 'Command not found', + 128 => 'Invalid exit argument', + // signals + 129 => 'Hangup', + 130 => 'Interrupt', + 131 => 'Quit and dump core', + 132 => 'Illegal instruction', + 133 => 'Trace/breakpoint trap', + 134 => 'Process aborted', + 135 => 'Bus error: "access to undefined portion of memory object"', + 136 => 'Floating point exception: "erroneous arithmetic operation"', + 137 => 'Kill (terminate immediately)', + 138 => 'User-defined 1', + 139 => 'Segmentation violation', + 140 => 'User-defined 2', + 141 => 'Write to pipe with no one reading', + 142 => 'Signal raised by alarm', + 143 => 'Termination (request to terminate)', + // 144 - not defined + 145 => 'Child process terminated, stopped (or continued*)', + 146 => 'Continue if stopped', + 147 => 'Stop executing temporarily', + 148 => 'Terminal stop signal', + 149 => 'Background process attempting to read from tty ("in")', + 150 => 'Background process attempting to write to tty ("out")', + 151 => 'Urgent data available on socket', + 152 => 'CPU time limit exceeded', + 153 => 'File size limit exceeded', + 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', + 155 => 'Profiling timer expired', + // 156 - not defined + 157 => 'Pollable event', + // 158 - not defined + 159 => 'Bad syscall', + ]; + + /** + * 构造方法 + * @access public + * @param string $commandline 指令 + * @param string|null $cwd 工作目录 + * @param array|null $env 环境变量 + * @param string|null $input 输入 + * @param int|float|null $timeout 超时时间 + * @param array $options proc_open的选项 + * @throws \RuntimeException + * @api + */ + public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = []) + { + if (!function_exists('proc_open')) { + throw new \RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.'); + } + + $this->commandline = $commandline; + $this->cwd = $cwd; + + if (null === $this->cwd && (defined('ZEND_THREAD_SAFE') || '\\' === DIRECTORY_SEPARATOR)) { + $this->cwd = getcwd(); + } + if (null !== $env) { + $this->setEnv($env); + } + + $this->input = $input; + $this->setTimeout($timeout); + $this->useFileHandles = '\\' === DIRECTORY_SEPARATOR; + $this->pty = false; + $this->enhanceWindowsCompatibility = true; + $this->enhanceSigchildCompatibility = '\\' !== DIRECTORY_SEPARATOR && $this->isSigchildEnabled(); + $this->options = array_replace([ + 'suppress_errors' => true, + 'binary_pipes' => true, + ], $options); + } + + public function __destruct() + { + $this->stop(); + } + + public function __clone() + { + $this->resetProcessData(); + } + + /** + * 运行指令 + * @access public + * @param callback|null $callback + * @return int + */ + public function run($callback = null) + { + $this->start($callback); + + return $this->wait(); + } + + /** + * 运行指令 + * @access public + * @param callable|null $callback + * @return self + * @throws \RuntimeException + * @throws ProcessFailedException + */ + public function mustRun($callback = null) + { + if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); + } + + if (0 !== $this->run($callback)) { + throw new ProcessFailedException($this); + } + + return $this; + } + + /** + * 启动进程并写到 STDIN 输入后返回。 + * @access public + * @param callable|null $callback + * @throws \RuntimeException + * @throws \RuntimeException + * @throws \LogicException + */ + public function start($callback = null) + { + if ($this->isRunning()) { + throw new \RuntimeException('Process is already running'); + } + if ($this->outputDisabled && null !== $callback) { + throw new \LogicException('Output has been disabled, enable it to allow the use of a callback.'); + } + + $this->resetProcessData(); + $this->starttime = $this->lastOutputTime = microtime(true); + $this->callback = $this->buildCallback($callback); + $descriptors = $this->getDescriptors(); + + $commandline = $this->commandline; + + if ('\\' === DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) { + $commandline = 'cmd /V:ON /E:ON /C "(' . $commandline . ')'; + foreach ($this->processPipes->getFiles() as $offset => $filename) { + $commandline .= ' ' . $offset . '>' . Utils::escapeArgument($filename); + } + $commandline .= '"'; + + if (!isset($this->options['bypass_shell'])) { + $this->options['bypass_shell'] = true; + } + } + + $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options); + + if (!is_resource($this->process)) { + throw new \RuntimeException('Unable to launch a new process.'); + } + $this->status = self::STATUS_STARTED; + + if ($this->tty) { + return; + } + + $this->updateStatus(false); + $this->checkTimeout(); + } + + /** + * 重启进程 + * @access public + * @param callable|null $callback + * @return Process + * @throws \RuntimeException + * @throws \RuntimeException + */ + public function restart($callback = null) + { + if ($this->isRunning()) { + throw new \RuntimeException('Process is already running'); + } + + $process = clone $this; + $process->start($callback); + + return $process; + } + + /** + * 等待要终止的进程 + * @access public + * @param callable|null $callback + * @return int + */ + public function wait($callback = null) + { + $this->requireProcessIsStarted(__FUNCTION__); + + $this->updateStatus(false); + if (null !== $callback) { + $this->callback = $this->buildCallback($callback); + } + + do { + $this->checkTimeout(); + $running = '\\' === DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); + $close = '\\' !== DIRECTORY_SEPARATOR || !$running; + $this->readPipes(true, $close); + } while ($running); + + while ($this->isRunning()) { + usleep(1000); + } + + if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) { + throw new \RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig'])); + } + + return $this->exitcode; + } + + /** + * 获取PID + * @access public + * @return int|null + * @throws \RuntimeException + */ + public function getPid() + { + if ($this->isSigchildEnabled()) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->isRunning() ? $this->processInformation['pid'] : null; + } + + /** + * 将一个 POSIX 信号发送到进程中 + * @access public + * @param int $signal + * @return Process + */ + public function signal($signal) + { + $this->doSignal($signal, true); + + return $this; + } + + /** + * 禁用从底层过程获取输出和错误输出。 + * @access public + * @return Process + */ + public function disableOutput() + { + if ($this->isRunning()) { + throw new \RuntimeException('Disabling output while the process is running is not possible.'); + } + if (null !== $this->idleTimeout) { + throw new \LogicException('Output can not be disabled while an idle timeout is set.'); + } + + $this->outputDisabled = true; + + return $this; + } + + /** + * 开启从底层过程获取输出和错误输出。 + * @access public + * @return Process + * @throws \RuntimeException + */ + public function enableOutput() + { + if ($this->isRunning()) { + throw new \RuntimeException('Enabling output while the process is running is not possible.'); + } + + $this->outputDisabled = false; + + return $this; + } + + /** + * 输出是否禁用 + * @access public + * @return bool + */ + public function isOutputDisabled() + { + return $this->outputDisabled; + } + + /** + * 获取当前的输出管道 + * @access public + * @return string + * @throws \LogicException + * @api + */ + public function getOutput() + { + if ($this->outputDisabled) { + throw new \LogicException('Output has been disabled.'); + } + + $this->requireProcessIsStarted(__FUNCTION__); + + $this->readPipes(false, '\\' === DIRECTORY_SEPARATOR ? !$this->processInformation['running'] : true); + + return $this->stdout; + } + + /** + * 以增量方式返回的输出结果。 + * @access public + * @return string + */ + public function getIncrementalOutput() + { + $this->requireProcessIsStarted(__FUNCTION__); + + $data = $this->getOutput(); + + $latest = substr($data, $this->incrementalOutputOffset); + + if (false === $latest) { + return ''; + } + + $this->incrementalOutputOffset = strlen($data); + + return $latest; + } + + /** + * 清空输出 + * @access public + * @return Process + */ + public function clearOutput() + { + $this->stdout = ''; + $this->incrementalOutputOffset = 0; + + return $this; + } + + /** + * 返回当前的错误输出的过程 (STDERR)。 + * @access public + * @return string + */ + public function getErrorOutput() + { + if ($this->outputDisabled) { + throw new \LogicException('Output has been disabled.'); + } + + $this->requireProcessIsStarted(__FUNCTION__); + + $this->readPipes(false, '\\' === DIRECTORY_SEPARATOR ? !$this->processInformation['running'] : true); + + return $this->stderr; + } + + /** + * 以增量方式返回 errorOutput + * @access public + * @return string + */ + public function getIncrementalErrorOutput() + { + $this->requireProcessIsStarted(__FUNCTION__); + + $data = $this->getErrorOutput(); + + $latest = substr($data, $this->incrementalErrorOutputOffset); + + if (false === $latest) { + return ''; + } + + $this->incrementalErrorOutputOffset = strlen($data); + + return $latest; + } + + /** + * 清空 errorOutput + * @access public + * @return Process + */ + public function clearErrorOutput() + { + $this->stderr = ''; + $this->incrementalErrorOutputOffset = 0; + + return $this; + } + + /** + * 获取退出码 + * @access public + * @return null|int + */ + public function getExitCode() + { + if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); + } + + $this->updateStatus(false); + + return $this->exitcode; + } + + /** + * 获取退出文本 + * @access public + * @return null|string + */ + public function getExitCodeText() + { + if (null === $exitcode = $this->getExitCode()) { + return; + } + + return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error'; + } + + /** + * 检查是否成功 + * @access public + * @return bool + */ + public function isSuccessful() + { + return 0 === $this->getExitCode(); + } + + /** + * 是否未捕获的信号已被终止子进程 + * @access public + * @return bool + */ + public function hasBeenSignaled() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + if ($this->isSigchildEnabled()) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->processInformation['signaled']; + } + + /** + * 返回导致子进程终止其执行的数。 + * @access public + * @return int + */ + public function getTermSignal() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + if ($this->isSigchildEnabled()) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->processInformation['termsig']; + } + + /** + * 检查子进程信号是否已停止 + * @access public + * @return bool + */ + public function hasBeenStopped() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + $this->updateStatus(false); + + return $this->processInformation['stopped']; + } + + /** + * 返回导致子进程停止其执行的数。 + * @access public + * @return int + */ + public function getStopSignal() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + $this->updateStatus(false); + + return $this->processInformation['stopsig']; + } + + /** + * 检查是否正在运行 + * @access public + * @return bool + */ + public function isRunning() + { + if (self::STATUS_STARTED !== $this->status) { + return false; + } + + $this->updateStatus(false); + + return $this->processInformation['running']; + } + + /** + * 检查是否已开始 + * @access public + * @return bool + */ + public function isStarted() + { + return self::STATUS_READY != $this->status; + } + + /** + * 检查是否已终止 + * @access public + * @return bool + */ + public function isTerminated() + { + $this->updateStatus(false); + + return self::STATUS_TERMINATED == $this->status; + } + + /** + * 获取当前的状态 + * @access public + * @return string + */ + public function getStatus() + { + $this->updateStatus(false); + + return $this->status; + } + + /** + * 终止进程 + * @access public + */ + public function stop() + { + if ($this->isRunning()) { + if ('\\' === DIRECTORY_SEPARATOR && !$this->isSigchildEnabled()) { + exec(sprintf('taskkill /F /T /PID %d 2>&1', $this->getPid()), $output, $exitCode); + if ($exitCode > 0) { + throw new \RuntimeException('Unable to kill the process'); + } + } else { + $pids = preg_split('/\s+/', `ps -o pid --no-heading --ppid {$this->getPid()}`); + foreach ($pids as $pid) { + if (is_numeric($pid)) { + posix_kill($pid, 9); + } + } + } + } + + $this->updateStatus(false); + if ($this->processInformation['running']) { + $this->close(); + } + + return $this->exitcode; + } + + /** + * 添加一行输出 + * @access public + * @param string $line + */ + public function addOutput($line) +{ + $this->lastOutputTime = microtime(true); + $this->stdout .= $line; + } + + /** + * 添加一行错误输出 + * @access public + * @param string $line + */ + public function addErrorOutput($line) +{ + $this->lastOutputTime = microtime(true); + $this->stderr .= $line; + } + + /** + * 获取被执行的指令 + * @access public + * @return string + */ + public function getCommandLine() +{ + return $this->commandline; + } + + /** + * 设置指令 + * @access public + * @param string $commandline + * @return self + */ + public function setCommandLine($commandline) +{ + $this->commandline = $commandline; + + return $this; + } + + /** + * 获取超时时间 + * @access public + * @return float|null + */ + public function getTimeout() +{ + return $this->timeout; + } + + /** + * 获取idle超时时间 + * @access public + * @return float|null + */ + public function getIdleTimeout() +{ + return $this->idleTimeout; + } + + /** + * 设置超时时间 + * @access public + * @param int|float|null $timeout + * @return self + */ + public function setTimeout($timeout) +{ + $this->timeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * 设置idle超时时间 + * @access public + * @param int|float|null $timeout + * @return self + */ + public function setIdleTimeout($timeout) +{ + if (null !== $timeout && $this->outputDisabled) { + throw new \LogicException('Idle timeout can not be set while the output is disabled.'); + } + + $this->idleTimeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * 设置TTY + * @access public + * @param bool $tty + * @return self + */ + public function setTty($tty) +{ + if ('\\' === DIRECTORY_SEPARATOR && $tty) { + throw new \RuntimeException('TTY mode is not supported on Windows platform.'); + } + if ($tty && (!file_exists('/dev/tty') || !is_readable('/dev/tty'))) { + throw new \RuntimeException('TTY mode requires /dev/tty to be readable.'); + } + + $this->tty = (bool) $tty; + + return $this; + } + + /** + * 检查是否是tty模式 + * @access public + * @return bool + */ + public function isTty() +{ + return $this->tty; + } + + /** + * 设置pty模式 + * @access public + * @param bool $bool + * @return self + */ + public function setPty($bool) +{ + $this->pty = (bool) $bool; + + return $this; + } + + /** + * 是否是pty模式 + * @access public + * @return bool + */ + public function isPty() +{ + return $this->pty; + } + + /** + * 获取工作目录 + * @access public + * @return string|null + */ + public function getWorkingDirectory() +{ + if (null === $this->cwd) { + return getcwd() ?: null; + } + + return $this->cwd; + } + + /** + * 设置工作目录 + * @access public + * @param string $cwd + * @return self + */ + public function setWorkingDirectory($cwd) +{ + $this->cwd = $cwd; + + return $this; + } + + /** + * 获取环境变量 + * @access public + * @return array + */ + public function getEnv() +{ + return $this->env; + } + + /** + * 设置环境变量 + * @access public + * @param array $env + * @return self + */ + public function setEnv(array $env) +{ + $env = array_filter($env, function ($value) { + return !is_array($value); + }); + + $this->env = []; + foreach ($env as $key => $value) { + $this->env[(binary) $key] = (binary) $value; + } + + return $this; + } + + /** + * 获取输入 + * @access public + * @return null|string + */ + public function getInput() +{ + return $this->input; + } + + /** + * 设置输入 + * @access public + * @param mixed $input + * @return self + */ + public function setInput($input) +{ + if ($this->isRunning()) { + throw new \LogicException('Input can not be set while the process is running.'); + } + + $this->input = Utils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input); + + return $this; + } + + /** + * 获取proc_open的选项 + * @access public + * @return array + */ + public function getOptions() +{ + return $this->options; + } + + /** + * 设置proc_open的选项 + * @access public + * @param array $options + * @return self + */ + public function setOptions(array $options) +{ + $this->options = $options; + + return $this; + } + + /** + * 是否兼容windows + * @access public + * @return bool + */ + public function getEnhanceWindowsCompatibility() +{ + return $this->enhanceWindowsCompatibility; + } + + /** + * 设置是否兼容windows + * @access public + * @param bool $enhance + * @return self + */ + public function setEnhanceWindowsCompatibility($enhance) +{ + $this->enhanceWindowsCompatibility = (bool) $enhance; + + return $this; + } + + /** + * 返回是否 sigchild 兼容模式激活 + * @access public + * @return bool + */ + public function getEnhanceSigchildCompatibility() +{ + return $this->enhanceSigchildCompatibility; + } + + /** + * 激活 sigchild 兼容性模式。 + * @access public + * @param bool $enhance + * @return self + */ + public function setEnhanceSigchildCompatibility($enhance) +{ + $this->enhanceSigchildCompatibility = (bool) $enhance; + + return $this; + } + + /** + * 是否超时 + */ + public function checkTimeout() +{ + if (self::STATUS_STARTED !== $this->status) { + return; + } + + if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { + $this->stop(); + + throw new ProcessTimeoutException($this, ProcessTimeoutException::TYPE_GENERAL); + } + + if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { + $this->stop(); + + throw new ProcessTimeoutException($this, ProcessTimeoutException::TYPE_IDLE); + } + } + + /** + * 是否支持pty + * @access public + * @return bool + */ + public static function isPtySupported() +{ + static $result; + + if (null !== $result) { + return $result; + } + + if ('\\' === DIRECTORY_SEPARATOR) { + return $result = false; + } + + $proc = @proc_open('echo 1', [['pty'], ['pty'], ['pty']], $pipes); + if (is_resource($proc)) { + proc_close($proc); + + return $result = true; + } + + return $result = false; + } + + /** + * 创建所需的 proc_open 的描述符 + * @access private + * @return array + */ + private function getDescriptors() +{ + if ('\\' === DIRECTORY_SEPARATOR) { + $this->processPipes = WindowsPipes::create($this, $this->input); + } else { + $this->processPipes = UnixPipes::create($this, $this->input); + } + $descriptors = $this->processPipes->getDescriptors($this->outputDisabled); + + if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { + + $descriptors = array_merge($descriptors, [['pipe', 'w']]); + + $this->commandline = '(' . $this->commandline . ') 3>/dev/null; code=$?; echo $code >&3; exit $code'; + } + + return $descriptors; + } + + /** + * 建立 wait () 使用的回调。 + * @access protected + * @param callable|null $callback + * @return callable + */ + protected function buildCallback($callback) +{ + $out = self::OUT; + $callback = function ($type, $data) use ($callback, $out) { + if ($out == $type) { + $this->addOutput($data); + } else { + $this->addErrorOutput($data); + } + + if (null !== $callback) { + call_user_func($callback, $type, $data); + } + }; + + return $callback; + } + + /** + * 更新状态 + * @access protected + * @param bool $blocking + */ + protected function updateStatus($blocking) +{ + if (self::STATUS_STARTED !== $this->status) { + return; + } + + $this->processInformation = proc_get_status($this->process); + $this->captureExitCode(); + + $this->readPipes($blocking, '\\' === DIRECTORY_SEPARATOR ? !$this->processInformation['running'] : true); + + if (!$this->processInformation['running']) { + $this->close(); + } + } + + /** + * 是否开启 '--enable-sigchild' + * @access protected + * @return bool + */ + protected function isSigchildEnabled() +{ + if (null !== self::$sigchild) { + return self::$sigchild; + } + + if (!function_exists('phpinfo')) { + return self::$sigchild = false; + } + + ob_start(); + phpinfo(INFO_GENERAL); + + return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); + } + + /** + * 验证是否超时 + * @access private + * @param int|float|null $timeout + * @return float|null + */ + private function validateTimeout($timeout) +{ + $timeout = (float) $timeout; + + if (0.0 === $timeout) { + $timeout = null; + } elseif ($timeout < 0) { + throw new \InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); + } + + return $timeout; + } + + /** + * 读取pipes + * @access private + * @param bool $blocking + * @param bool $close + */ + private function readPipes($blocking, $close) +{ + $result = $this->processPipes->readAndWrite($blocking, $close); + + $callback = $this->callback; + foreach ($result as $type => $data) { + if (3 == $type) { + $this->fallbackExitcode = (int) $data; + } else { + $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data); + } + } + } + + /** + * 捕获退出码 + */ + private function captureExitCode() +{ + if (isset($this->processInformation['exitcode']) && -1 != $this->processInformation['exitcode']) { + $this->exitcode = $this->processInformation['exitcode']; + } + } + + /** + * 关闭资源 + * @access private + * @return int 退出码 + */ + private function close() +{ + $this->processPipes->close(); + if (is_resource($this->process)) { + $exitcode = proc_close($this->process); + } else { + $exitcode = -1; + } + + $this->exitcode = -1 !== $exitcode ? $exitcode : (null !== $this->exitcode ? $this->exitcode : -1); + $this->status = self::STATUS_TERMINATED; + + if (-1 === $this->exitcode && null !== $this->fallbackExitcode) { + $this->exitcode = $this->fallbackExitcode; + } elseif (-1 === $this->exitcode && $this->processInformation['signaled'] + && 0 < $this->processInformation['termsig'] + ) { + $this->exitcode = 128 + $this->processInformation['termsig']; + } + + return $this->exitcode; + } + + /** + * 重置数据 + */ + private function resetProcessData() +{ + $this->starttime = null; + $this->callback = null; + $this->exitcode = null; + $this->fallbackExitcode = null; + $this->processInformation = null; + $this->stdout = null; + $this->stderr = null; + $this->process = null; + $this->latestSignal = null; + $this->status = self::STATUS_READY; + $this->incrementalOutputOffset = 0; + $this->incrementalErrorOutputOffset = 0; + } + + /** + * 将一个 POSIX 信号发送到进程中。 + * @access private + * @param int $signal + * @param bool $throwException + * @return bool + */ + private function doSignal($signal, $throwException) +{ + if (!$this->isRunning()) { + if ($throwException) { + throw new \LogicException('Can not send signal on a non running process.'); + } + + return false; + } + + if ($this->isSigchildEnabled()) { + if ($throwException) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + } + + return false; + } + + if (true !== @proc_terminate($this->process, $signal)) { + if ($throwException) { + throw new \RuntimeException(sprintf('Error while sending signal `%s`.', $signal)); + } + + return false; + } + + $this->latestSignal = $signal; + + return true; + } + + /** + * 确保进程已经开启 + * @access private + * @param string $functionName + */ + private function requireProcessIsStarted($functionName) +{ + if (!$this->isStarted()) { + throw new \LogicException(sprintf('Process must be started before calling %s.', $functionName)); + } + } + + /** + * 确保进程已经终止 + * @access private + * @param string $functionName + */ + private function requireProcessIsTerminated($functionName) +{ + if (!$this->isTerminated()) { + throw new \LogicException(sprintf('Process must be terminated before calling %s.', $functionName)); + } + } +} diff --git a/vendor/topthink/framework/library/think/Request.php b/vendor/topthink/framework/library/think/Request.php new file mode 100644 index 0000000..6b6dd4b --- /dev/null +++ b/vendor/topthink/framework/library/think/Request.php @@ -0,0 +1,2267 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\facade\Cookie; +use think\facade\Session; + +class Request +{ + /** + * 配置参数 + * @var array + */ + protected $config = [ + // 表单请求类型伪装变量 + 'var_method' => '_method', + // 表单ajax伪装变量 + 'var_ajax' => '_ajax', + // 表单pjax伪装变量 + 'var_pjax' => '_pjax', + // PATHINFO变量名 用于兼容模式 + 'var_pathinfo' => 's', + // 兼容PATH_INFO获取 + 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'], + // 默认全局过滤方法 用逗号分隔多个 + 'default_filter' => '', + // 域名根,如thinkphp.cn + 'url_domain_root' => '', + // HTTPS代理标识 + 'https_agent_name' => '', + // IP代理获取标识 + 'http_agent_ip' => 'HTTP_X_REAL_IP', + // URL伪静态后缀 + 'url_html_suffix' => 'html', + ]; + + /** + * 请求类型 + * @var string + */ + protected $method; + + /** + * 主机名(含端口) + * @var string + */ + protected $host; + + /** + * 域名(含协议及端口) + * @var string + */ + protected $domain; + + /** + * 子域名 + * @var string + */ + protected $subDomain; + + /** + * 泛域名 + * @var string + */ + protected $panDomain; + + /** + * 当前URL地址 + * @var string + */ + protected $url; + + /** + * 基础URL + * @var string + */ + protected $baseUrl; + + /** + * 当前执行的文件 + * @var string + */ + protected $baseFile; + + /** + * 访问的ROOT地址 + * @var string + */ + protected $root; + + /** + * pathinfo + * @var string + */ + protected $pathinfo; + + /** + * pathinfo(不含后缀) + * @var string + */ + protected $path; + + /** + * 当前路由信息 + * @var array + */ + protected $routeInfo = []; + + /** + * 当前调度信息 + * @var \think\route\Dispatch + */ + protected $dispatch; + + /** + * 当前模块名 + * @var string + */ + protected $module; + + /** + * 当前控制器名 + * @var string + */ + protected $controller; + + /** + * 当前操作名 + * @var string + */ + protected $action; + + /** + * 当前语言集 + * @var string + */ + protected $langset; + + /** + * 当前请求参数 + * @var array + */ + protected $param = []; + + /** + * 当前GET参数 + * @var array + */ + protected $get = []; + + /** + * 当前POST参数 + * @var array + */ + protected $post = []; + + /** + * 当前REQUEST参数 + * @var array + */ + protected $request = []; + + /** + * 当前ROUTE参数 + * @var array + */ + protected $route = []; + + /** + * 当前PUT参数 + * @var array + */ + protected $put; + + /** + * 当前SESSION参数 + * @var array + */ + protected $session = []; + + /** + * 当前FILE参数 + * @var array + */ + protected $file = []; + + /** + * 当前COOKIE参数 + * @var array + */ + protected $cookie = []; + + /** + * 当前SERVER参数 + * @var array + */ + protected $server = []; + + /** + * 当前ENV参数 + * @var array + */ + protected $env = []; + + /** + * 当前HEADER参数 + * @var array + */ + protected $header = []; + + /** + * 资源类型定义 + * @var array + */ + protected $mimeType = [ + 'xml' => 'application/xml,text/xml,application/x-xml', + 'json' => 'application/json,text/x-json,application/jsonrequest,text/json', + 'js' => 'text/javascript,application/javascript,application/x-javascript', + 'css' => 'text/css', + 'rss' => 'application/rss+xml', + 'yaml' => 'application/x-yaml,text/yaml', + 'atom' => 'application/atom+xml', + 'pdf' => 'application/pdf', + 'text' => 'text/plain', + 'image' => 'image/png,image/jpg,image/jpeg,image/pjpeg,image/gif,image/webp,image/*', + 'csv' => 'text/csv', + 'html' => 'text/html,application/xhtml+xml,*/*', + ]; + + /** + * 当前请求内容 + * @var string + */ + protected $content; + + /** + * 全局过滤规则 + * @var array + */ + protected $filter; + + /** + * 扩展方法 + * @var array + */ + protected $hook = []; + + /** + * php://input内容 + * @var string + */ + protected $input; + + /** + * 请求缓存 + * @var array + */ + protected $cache; + + /** + * 缓存是否检查 + * @var bool + */ + protected $isCheckCache; + + /** + * 请求安全Key + * @var string + */ + protected $secureKey; + + /** + * 是否合并Param + * @var bool + */ + protected $mergeParam = false; + + /** + * 架构函数 + * @access public + * @param array $options 参数 + */ + public function __construct(array $options = []) + { + $this->init($options); + + // 保存 php://input + $this->input = file_get_contents('php://input'); + } + + public function init(array $options = []) + { + $this->config = array_merge($this->config, $options); + + if (is_null($this->filter) && !empty($this->config['default_filter'])) { + $this->filter = $this->config['default_filter']; + } + } + + public function config($name = null) + { + if (is_null($name)) { + return $this->config; + } + return isset($this->config[$name]) ? $this->config[$name] : null; + } + + public static function __make(App $app, Config $config) + { + $request = new static($config->pull('app')); + + $request->server = $_SERVER; + $request->env = $app['env']->get(); + + return $request; + } + + public function __call($method, $args) + { + if (array_key_exists($method, $this->hook)) { + array_unshift($args, $this); + return call_user_func_array($this->hook[$method], $args); + } + + throw new Exception('method not exists:' . static::class . '->' . $method); + } + + /** + * Hook 方法注入 + * @access public + * @param string|array $method 方法名 + * @param mixed $callback callable + * @return void + */ + public function hook($method, $callback = null) + { + if (is_array($method)) { + $this->hook = array_merge($this->hook, $method); + } else { + $this->hook[$method] = $callback; + } + } + + /** + * 创建一个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 + * @return \think\Request + */ + public function create($uri, $method = 'GET', $params = [], $cookie = [], $files = [], $server = [], $content = null) + { + $server['PATH_INFO'] = ''; + $server['REQUEST_METHOD'] = strtoupper($method); + $info = parse_url($uri); + + if (isset($info['host'])) { + $server['SERVER_NAME'] = $info['host']; + $server['HTTP_HOST'] = $info['host']; + } + + if (isset($info['scheme'])) { + if ('https' === $info['scheme']) { + $server['HTTPS'] = 'on'; + $server['SERVER_PORT'] = 443; + } else { + unset($server['HTTPS']); + $server['SERVER_PORT'] = 80; + } + } + + if (isset($info['port'])) { + $server['SERVER_PORT'] = $info['port']; + $server['HTTP_HOST'] = $server['HTTP_HOST'] . ':' . $info['port']; + } + + if (isset($info['user'])) { + $server['PHP_AUTH_USER'] = $info['user']; + } + + if (isset($info['pass'])) { + $server['PHP_AUTH_PW'] = $info['pass']; + } + + if (!isset($info['path'])) { + $info['path'] = '/'; + } + + $options = []; + $queryString = ''; + + $options[strtolower($method)] = $params; + + if (isset($info['query'])) { + parse_str(html_entity_decode($info['query']), $query); + if (!empty($params)) { + $params = array_replace($query, $params); + $queryString = http_build_query($params, '', '&'); + } else { + $params = $query; + $queryString = $info['query']; + } + } elseif (!empty($params)) { + $queryString = http_build_query($params, '', '&'); + } + + if ($queryString) { + parse_str($queryString, $get); + $options['get'] = isset($options['get']) ? array_merge($get, $options['get']) : $get; + } + + $server['REQUEST_URI'] = $info['path'] . ('' !== $queryString ? '?' . $queryString : ''); + $server['QUERY_STRING'] = $queryString; + $options['cookie'] = $cookie; + $options['param'] = $params; + $options['file'] = $files; + $options['server'] = $server; + $options['url'] = $server['REQUEST_URI']; + $options['baseUrl'] = $info['path']; + $options['pathinfo'] = '/' == $info['path'] ? '/' : ltrim($info['path'], '/'); + $options['method'] = $server['REQUEST_METHOD']; + $options['domain'] = isset($info['scheme']) ? $info['scheme'] . '://' . $server['HTTP_HOST'] : ''; + $options['content'] = $content; + + $request = new static(); + foreach ($options as $name => $item) { + if (property_exists($request, $name)) { + $request->$name = $item; + } + } + + return $request; + } + + /** + * 获取当前包含协议、端口的域名 + * @access public + * @param bool $port 是否需要去除端口号 + * @return string + */ + public function domain($port = false) + { + return $this->scheme() . '://' . $this->host($port); + } + + /** + * 获取当前根域名 + * @access public + * @return string + */ + public function rootDomain() + { + $root = $this->config['url_domain_root']; + + if (!$root) { + $item = explode('.', $this->host(true)); + $count = count($item); + $root = $count > 1 ? $item[$count - 2] . '.' . $item[$count - 1] : $item[0]; + } + + return $root; + } + + /** + * 获取当前子域名 + * @access public + * @return string + */ + public function subDomain() + { + if (is_null($this->subDomain)) { + // 获取当前主域名 + $rootDomain = $this->config['url_domain_root']; + + if ($rootDomain) { + // 配置域名根 例如 thinkphp.cn 163.com.cn 如果是国家级域名 com.cn net.cn 之类的域名需要配置 + $domain = explode('.', rtrim(stristr($this->host(true), $rootDomain, true), '.')); + } else { + $domain = explode('.', $this->host(true), -2); + } + + $this->subDomain = implode('.', $domain); + } + + return $this->subDomain; + } + + /** + * 设置当前泛域名的值 + * @access public + * @param string $domain 域名 + * @return $this + */ + public function setPanDomain($domain) + { + $this->panDomain = $domain; + return $this; + } + + /** + * 获取当前泛域名的值 + * @access public + * @return string + */ + public function panDomain() + { + return $this->panDomain; + } + + /** + * 设置当前完整URL 包括QUERY_STRING + * @access public + * @param string $url URL + * @return $this + */ + public function setUrl($url) + { + $this->url = $url; + return $this; + } + + /** + * 获取当前完整URL 包括QUERY_STRING + * @access public + * @param bool $complete 是否包含域名 + * @return string + */ + public function url($complete = false) + { + if (!$this->url) { + if ($this->isCli()) { + $this->url = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : ''; + } elseif ($this->server('HTTP_X_REWRITE_URL')) { + $this->url = $this->server('HTTP_X_REWRITE_URL'); + } elseif ($this->server('REQUEST_URI')) { + $this->url = $this->server('REQUEST_URI'); + } elseif ($this->server('ORIG_PATH_INFO')) { + $this->url = $this->server('ORIG_PATH_INFO') . (!empty($this->server('QUERY_STRING')) ? '?' . $this->server('QUERY_STRING') : ''); + } else { + $this->url = ''; + } + } + + return $complete ? $this->domain() . $this->url : $this->url; + } + + /** + * 设置当前完整URL 不包括QUERY_STRING + * @access public + * @param string $url URL + * @return $this + */ + public function setBaseUrl($url) + { + $this->baseUrl = $url; + return $this; + } + + /** + * 获取当前URL 不含QUERY_STRING + * @access public + * @param bool $domain 是否包含域名 + * @return string|$this + */ + public function baseUrl($domain = false) + { + if (!$this->baseUrl) { + $str = $this->url(); + $this->baseUrl = strpos($str, '?') ? strstr($str, '?', true) : $str; + } + + return $domain ? $this->domain() . $this->baseUrl : $this->baseUrl; + } + + /** + * 设置或获取当前执行的文件 SCRIPT_NAME + * @access public + * @param bool $domain 是否包含域名 + * @return string|$this + */ + public function baseFile($domain = false) + { + if (!$this->baseFile) { + $url = ''; + if (!$this->isCli()) { + $script_name = basename($this->server('SCRIPT_FILENAME')); + if (basename($this->server('SCRIPT_NAME')) === $script_name) { + $url = $this->server('SCRIPT_NAME'); + } elseif (basename($this->server('PHP_SELF')) === $script_name) { + $url = $this->server('PHP_SELF'); + } elseif (basename($this->server('ORIG_SCRIPT_NAME')) === $script_name) { + $url = $this->server('ORIG_SCRIPT_NAME'); + } elseif (($pos = strpos($this->server('PHP_SELF'), '/' . $script_name)) !== false) { + $url = substr($this->server('SCRIPT_NAME'), 0, $pos) . '/' . $script_name; + } elseif ($this->server('DOCUMENT_ROOT') && strpos($this->server('SCRIPT_FILENAME'), $this->server('DOCUMENT_ROOT')) === 0) { + $url = str_replace('\\', '/', str_replace($this->server('DOCUMENT_ROOT'), '', $this->server('SCRIPT_FILENAME'))); + } + } + $this->baseFile = $url; + } + + return $domain ? $this->domain() . $this->baseFile : $this->baseFile; + } + + /** + * 设置URL访问根地址 + * @access public + * @param string $url URL地址 + * @return string|$this + */ + public function setRoot($url = null) + { + $this->root = $url; + return $this; + } + + /** + * 获取URL访问根地址 + * @access public + * @param bool $domain 是否包含域名 + * @return string|$this + */ + public function root($domain = false) + { + if (!$this->root) { + $file = $this->baseFile(); + if ($file && 0 !== strpos($this->url(), $file)) { + $file = str_replace('\\', '/', dirname($file)); + } + $this->root = rtrim($file, '/'); + } + + return $domain ? $this->domain() . $this->root : $this->root; + } + + /** + * 获取URL访问根目录 + * @access public + * @return string + */ + public function rootUrl() + { + $base = $this->root(); + $root = strpos($base, '.') ? ltrim(dirname($base), DIRECTORY_SEPARATOR) : $base; + + if ('' != $root) { + $root = '/' . ltrim($root, '/'); + } + + return $root; + } + + public function setPathinfo($pathinfo) + { + $this->pathinfo = $pathinfo; + return $this; + } + + /** + * 获取当前请求URL的pathinfo信息(含URL后缀) + * @access public + * @return string + */ + public function pathinfo() + { + if (is_null($this->pathinfo)) { + if (isset($_GET[$this->config['var_pathinfo']])) { + // 判断URL里面是否有兼容模式参数 + $pathinfo = $_GET[$this->config['var_pathinfo']]; + unset($_GET[$this->config['var_pathinfo']]); + unset($this->get[$this->config['var_pathinfo']]); + } elseif ($this->isCli()) { + // CLI模式下 index.php module/controller/action/params/... + $pathinfo = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : ''; + } elseif ('cli-server' == PHP_SAPI) { + $pathinfo = strpos($this->server('REQUEST_URI'), '?') ? strstr($this->server('REQUEST_URI'), '?', true) : $this->server('REQUEST_URI'); + } elseif ($this->server('PATH_INFO')) { + $pathinfo = $this->server('PATH_INFO'); + } + + // 分析PATHINFO信息 + if (!isset($pathinfo)) { + foreach ($this->config['pathinfo_fetch'] as $type) { + if ($this->server($type)) { + $pathinfo = (0 === strpos($this->server($type), $this->server('SCRIPT_NAME'))) ? + substr($this->server($type), strlen($this->server('SCRIPT_NAME'))) : $this->server($type); + break; + } + } + } + + if (!empty($pathinfo)) { + unset($this->get[$pathinfo], $this->request[$pathinfo]); + } + + $this->pathinfo = empty($pathinfo) || '/' == $pathinfo ? '' : ltrim($pathinfo, '/'); + } + + return $this->pathinfo; + } + + /** + * 获取当前请求URL的pathinfo信息(不含URL后缀) + * @access public + * @return string + */ + public function path() + { + if (is_null($this->path)) { + $suffix = $this->config['url_html_suffix']; + $pathinfo = $this->pathinfo(); + + if (false === $suffix) { + // 禁止伪静态访问 + $this->path = $pathinfo; + } elseif ($suffix) { + // 去除正常的URL后缀 + $this->path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo); + } else { + // 允许任何后缀访问 + $this->path = preg_replace('/\.' . $this->ext() . '$/i', '', $pathinfo); + } + } + + return $this->path; + } + + /** + * 当前URL的访问后缀 + * @access public + * @return string + */ + public function ext() + { + return pathinfo($this->pathinfo(), PATHINFO_EXTENSION); + } + + /** + * 获取当前请求的时间 + * @access public + * @param bool $float 是否使用浮点类型 + * @return integer|float + */ + public function time($float = false) + { + return $float ? $this->server('REQUEST_TIME_FLOAT') : $this->server('REQUEST_TIME'); + } + + /** + * 当前请求的资源类型 + * @access public + * @return false|string + */ + public function type() + { + $accept = $this->server('HTTP_ACCEPT'); + + if (empty($accept)) { + return false; + } + + foreach ($this->mimeType as $key => $val) { + $array = explode(',', $val); + foreach ($array as $k => $v) { + if (stristr($accept, $v)) { + return $key; + } + } + } + + return false; + } + + /** + * 设置资源类型 + * @access public + * @param string|array $type 资源类型名 + * @param string $val 资源类型 + * @return void + */ + public function mimeType($type, $val = '') + { + if (is_array($type)) { + $this->mimeType = array_merge($this->mimeType, $type); + } else { + $this->mimeType[$type] = $val; + } + } + + /** + * 当前的请求类型 + * @access public + * @param bool $origin 是否获取原始请求类型 + * @return string + */ + public function method($origin = false) + { + if ($origin) { + // 获取原始请求类型 + return $this->server('REQUEST_METHOD') ?: 'GET'; + } elseif (!$this->method) { + if (isset($_POST[$this->config['var_method']])) { + $method = strtolower($_POST[$this->config['var_method']]); + if (in_array($method, ['get', 'post', 'put', 'patch', 'delete'])) { + $this->method = strtoupper($method); + $this->{$method} = $_POST; + } else { + $this->method = 'POST'; + } + unset($_POST[$this->config['var_method']]); + } elseif ($this->server('HTTP_X_HTTP_METHOD_OVERRIDE')) { + $this->method = strtoupper($this->server('HTTP_X_HTTP_METHOD_OVERRIDE')); + } else { + $this->method = $this->server('REQUEST_METHOD') ?: 'GET'; + } + } + + return $this->method; + } + + /** + * 是否为GET请求 + * @access public + * @return bool + */ + public function isGet() + { + return $this->method() == 'GET'; + } + + /** + * 是否为POST请求 + * @access public + * @return bool + */ + public function isPost() + { + return $this->method() == 'POST'; + } + + /** + * 是否为PUT请求 + * @access public + * @return bool + */ + public function isPut() + { + return $this->method() == 'PUT'; + } + + /** + * 是否为DELTE请求 + * @access public + * @return bool + */ + public function isDelete() + { + return $this->method() == 'DELETE'; + } + + /** + * 是否为HEAD请求 + * @access public + * @return bool + */ + public function isHead() + { + return $this->method() == 'HEAD'; + } + + /** + * 是否为PATCH请求 + * @access public + * @return bool + */ + public function isPatch() + { + return $this->method() == 'PATCH'; + } + + /** + * 是否为OPTIONS请求 + * @access public + * @return bool + */ + public function isOptions() + { + return $this->method() == 'OPTIONS'; + } + + /** + * 是否为cli + * @access public + * @return bool + */ + public function isCli() + { + return PHP_SAPI == 'cli'; + } + + /** + * 是否为cgi + * @access public + * @return bool + */ + public function isCgi() + { + return strpos(PHP_SAPI, 'cgi') === 0; + } + + /** + * 获取当前请求的参数 + * @access public + * @param mixed $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function param($name = '', $default = null, $filter = '') + { + if (!$this->mergeParam) { + $method = $this->method(true); + + // 自动获取请求变量 + switch ($method) { + case 'POST': + $vars = $this->post(false); + break; + case 'PUT': + case 'DELETE': + case 'PATCH': + $vars = $this->put(false); + break; + default: + $vars = []; + } + + // 当前请求参数和URL地址中的参数合并 + $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false)); + + $this->mergeParam = true; + } + + if (true === $name) { + // 获取包含文件上传信息的数组 + $file = $this->file(); + $data = is_array($file) ? array_merge($this->param, $file) : $this->param; + + return $this->input($data, '', $default, $filter); + } + + return $this->input($this->param, $name, $default, $filter); + } + + /** + * 设置路由变量 + * @access public + * @param array $route 路由变量 + * @return $this + */ + public function setRouteVars(array $route) + { + $this->route = array_merge($this->route, $route); + return $this; + } + + /** + * 获取路由参数 + * @access public + * @param string|false $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function route($name = '', $default = null, $filter = '') + { + return $this->input($this->route, $name, $default, $filter); + } + + /** + * 获取GET参数 + * @access public + * @param string|false $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function get($name = '', $default = null, $filter = '') + { + if (empty($this->get)) { + $this->get = $_GET; + } + + return $this->input($this->get, $name, $default, $filter); + } + + /** + * 获取POST参数 + * @access public + * @param string|false $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function post($name = '', $default = null, $filter = '') + { + if (empty($this->post)) { + $this->post = !empty($_POST) ? $_POST : $this->getInputData($this->input); + } + + return $this->input($this->post, $name, $default, $filter); + } + + /** + * 获取PUT参数 + * @access public + * @param string|false $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function put($name = '', $default = null, $filter = '') + { + if (is_null($this->put)) { + $this->put = $this->getInputData($this->input); + } + + return $this->input($this->put, $name, $default, $filter); + } + + protected function getInputData($content) + { + if (false !== strpos($this->contentType(), 'json')) { + return (array) json_decode($content, true); + } elseif (strpos($content, '=')) { + parse_str($content, $data); + return $data; + } + + return []; + } + + /** + * 获取DELETE参数 + * @access public + * @param string|false $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function delete($name = '', $default = null, $filter = '') + { + return $this->put($name, $default, $filter); + } + + /** + * 获取PATCH参数 + * @access public + * @param string|false $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function patch($name = '', $default = null, $filter = '') + { + return $this->put($name, $default, $filter); + } + + /** + * 获取request变量 + * @access public + * @param string|false $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function request($name = '', $default = null, $filter = '') + { + if (empty($this->request)) { + $this->request = $_REQUEST; + } + + return $this->input($this->request, $name, $default, $filter); + } + + /** + * 获取session数据 + * @access public + * @param string $name 数据名称 + * @param string $default 默认值 + * @return mixed + */ + public function session($name = '', $default = null) + { + if (empty($this->session)) { + $this->session = Session::get(); + } + + if ('' === $name) { + return $this->session; + } + + $data = $this->getData($this->session, $name); + + return is_null($data) ? $default : $data; + } + + /** + * 获取cookie参数 + * @access public + * @param string $name 变量名 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function cookie($name = '', $default = null, $filter = '') + { + if (empty($this->cookie)) { + $this->cookie = Cookie::get(); + } + + if (!empty($name)) { + $data = Cookie::has($name) ? Cookie::get($name) : $default; + } else { + $data = $this->cookie; + } + + // 解析过滤器 + $filter = $this->getFilter($filter, $default); + + if (is_array($data)) { + array_walk_recursive($data, [$this, 'filterValue'], $filter); + reset($data); + } else { + $this->filterValue($data, $name, $filter); + } + + return $data; + } + + /** + * 获取server参数 + * @access public + * @param string $name 数据名称 + * @param string $default 默认值 + * @return mixed + */ + public function server($name = '', $default = null) + { + if (empty($name)) { + return $this->server; + } else { + $name = strtoupper($name); + } + + return isset($this->server[$name]) ? $this->server[$name] : $default; + } + + /** + * 获取上传的文件信息 + * @access public + * @param string $name 名称 + * @return null|array|\think\File + */ + public function file($name = '') + { + if (empty($this->file)) { + $this->file = isset($_FILES) ? $_FILES : []; + } + + $files = $this->file; + if (!empty($files)) { + if (strpos($name, '.')) { + list($name, $sub) = explode('.', $name); + } + + // 处理上传文件 + $array = $this->dealUploadFile($files, $name); + + if ('' === $name) { + // 获取全部文件 + return $array; + } elseif (isset($sub) && isset($array[$name][$sub])) { + return $array[$name][$sub]; + } elseif (isset($array[$name])) { + return $array[$name]; + } + } + + return; + } + + protected function dealUploadFile($files, $name) + { + $array = []; + foreach ($files as $key => $file) { + if ($file instanceof File) { + $array[$key] = $file; + } elseif (is_array($file['name'])) { + $item = []; + $keys = array_keys($file); + $count = count($file['name']); + + for ($i = 0; $i < $count; $i++) { + if ($file['error'][$i] > 0) { + if ($name == $key) { + $this->throwUploadFileError($file['error'][$i]); + } else { + continue; + } + } + + $temp['key'] = $key; + + foreach ($keys as $_key) { + $temp[$_key] = $file[$_key][$i]; + } + + $item[] = (new File($temp['tmp_name']))->setUploadInfo($temp); + } + + $array[$key] = $item; + } else { + if ($file['error'] > 0) { + if ($key == $name) { + $this->throwUploadFileError($file['error']); + } else { + continue; + } + } + + $array[$key] = (new File($file['tmp_name']))->setUploadInfo($file); + } + } + + return $array; + } + + protected function throwUploadFileError($error) + { + static $fileUploadErrors = [ + 1 => 'upload File size exceeds the maximum value', + 2 => 'upload File size exceeds the maximum value', + 3 => 'only the portion of file is uploaded', + 4 => 'no file to uploaded', + 6 => 'upload temp dir not found', + 7 => 'file write error', + ]; + + $msg = $fileUploadErrors[$error]; + + throw new Exception($msg); + } + + /** + * 获取环境变量 + * @access public + * @param string $name 数据名称 + * @param string $default 默认值 + * @return mixed + */ + public function env($name = '', $default = null) + { + if (empty($name)) { + return $this->env; + } else { + $name = strtoupper($name); + } + + return isset($this->env[$name]) ? $this->env[$name] : $default; + } + + /** + * 获取当前的Header + * @access public + * @param string $name header名称 + * @param string $default 默认值 + * @return string|array + */ + public function header($name = '', $default = null) + { + if (empty($this->header)) { + $header = []; + if (function_exists('apache_request_headers') && $result = apache_request_headers()) { + $header = $result; + } else { + $server = $this->server; + foreach ($server as $key => $val) { + if (0 === strpos($key, 'HTTP_')) { + $key = str_replace('_', '-', strtolower(substr($key, 5))); + $header[$key] = $val; + } + } + if (isset($server['CONTENT_TYPE'])) { + $header['content-type'] = $server['CONTENT_TYPE']; + } + if (isset($server['CONTENT_LENGTH'])) { + $header['content-length'] = $server['CONTENT_LENGTH']; + } + } + $this->header = array_change_key_case($header); + } + + if ('' === $name) { + return $this->header; + } + + $name = str_replace('_', '-', strtolower($name)); + + return isset($this->header[$name]) ? $this->header[$name] : $default; + } + + /** + * 递归重置数组指针 + * @access public + * @param array $data 数据源 + * @return void + */ + public function arrayReset(array &$data) + { + foreach ($data as &$value) { + if (is_array($value)) { + $this->arrayReset($value); + } + } + reset($data); + } + + /** + * 获取变量 支持过滤和默认值 + * @access public + * @param array $data 数据源 + * @param string|false $name 字段名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤函数 + * @return mixed + */ + public function input($data = [], $name = '', $default = null, $filter = '') + { + if (false === $name) { + // 获取原始数据 + return $data; + } + + $name = (string) $name; + if ('' != $name) { + // 解析name + if (strpos($name, '/')) { + list($name, $type) = explode('/', $name); + } + + $data = $this->getData($data, $name); + + if (is_null($data)) { + return $default; + } + + if (is_object($data)) { + return $data; + } + } + + // 解析过滤器 + $filter = $this->getFilter($filter, $default); + + if (is_array($data)) { + array_walk_recursive($data, [$this, 'filterValue'], $filter); + if (version_compare(PHP_VERSION, '7.1.0', '<')) { + // 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针 + $this->arrayReset($data); + } + } else { + $this->filterValue($data, $name, $filter); + } + + if (isset($type) && $data !== $default) { + // 强制类型转换 + $this->typeCast($data, $type); + } + + return $data; + } + + /** + * 获取数据 + * @access public + * @param array $data 数据源 + * @param string|false $name 字段名 + * @return mixed + */ + protected function getData(array $data, $name) + { + foreach (explode('.', $name) as $val) { + if (isset($data[$val])) { + $data = $data[$val]; + } else { + return; + } + } + + return $data; + } + + /** + * 设置或获取当前的过滤规则 + * @access public + * @param mixed $filter 过滤规则 + * @return mixed + */ + public function filter($filter = null) + { + if (is_null($filter)) { + return $this->filter; + } + + $this->filter = $filter; + } + + protected function getFilter($filter, $default) + { + if (is_null($filter)) { + $filter = []; + } else { + $filter = $filter ?: $this->filter; + if (is_string($filter) && false === strpos($filter, '/')) { + $filter = explode(',', $filter); + } else { + $filter = (array) $filter; + } + } + + $filter[] = $default; + + return $filter; + } + + /** + * 递归过滤给定的值 + * @access public + * @param mixed $value 键值 + * @param mixed $key 键名 + * @param array $filters 过滤方法+默认值 + * @return mixed + */ + private function filterValue(&$value, $key, $filters) + { + $default = array_pop($filters); + + foreach ($filters as $filter) { + if (is_callable($filter)) { + // 调用函数或者方法过滤 + $value = call_user_func($filter, $value); + } elseif (is_scalar($value)) { + if (false !== strpos($filter, '/')) { + // 正则过滤 + if (!preg_match($filter, $value)) { + // 匹配不成功返回默认值 + $value = $default; + break; + } + } elseif (!empty($filter)) { + // filter函数不存在时, 则使用filter_var进行过滤 + // filter为非整形值时, 调用filter_id取得过滤id + $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter)); + if (false === $value) { + $value = $default; + break; + } + } + } + } + + return $value; + } + + /** + * 强制类型转换 + * @access public + * @param string $data + * @param string $type + * @return mixed + */ + private function typeCast(&$data, $type) + { + switch (strtolower($type)) { + // 数组 + case 'a': + $data = (array) $data; + break; + // 数字 + case 'd': + $data = (int) $data; + break; + // 浮点 + case 'f': + $data = (float) $data; + break; + // 布尔 + case 'b': + $data = (boolean) $data; + break; + // 字符串 + case 's': + if (is_scalar($data)) { + $data = (string) $data; + } else { + throw new \InvalidArgumentException('variable type error:' . gettype($data)); + } + break; + } + } + + /** + * 是否存在某个请求参数 + * @access public + * @param string $name 变量名 + * @param string $type 变量类型 + * @param bool $checkEmpty 是否检测空值 + * @return mixed + */ + public function has($name, $type = 'param', $checkEmpty = false) + { + if (!in_array($type, ['param', 'get', 'post', 'request', 'put', 'patch', 'file', 'session', 'cookie', 'env', 'header', 'route'])) { + return false; + } + + if (empty($this->$type)) { + $param = $this->$type(); + } else { + $param = $this->$type; + } + + // 按.拆分成多维数组进行判断 + foreach (explode('.', $name) as $val) { + if (isset($param[$val])) { + $param = $param[$val]; + } else { + return false; + } + } + + return ($checkEmpty && '' === $param) ? false : true; + } + + /** + * 获取指定的参数 + * @access public + * @param string|array $name 变量名 + * @param string $type 变量类型 + * @return mixed + */ + public function only($name, $type = 'param') + { + $param = $this->$type(); + + if (is_string($name)) { + $name = explode(',', $name); + } + + $item = []; + foreach ($name as $key => $val) { + + if (is_int($key)) { + $default = null; + $key = $val; + } else { + $default = $val; + } + + if (isset($param[$key])) { + $item[$key] = $param[$key]; + } elseif (isset($default)) { + $item[$key] = $default; + } + } + + return $item; + } + + /** + * 排除指定参数获取 + * @access public + * @param string|array $name 变量名 + * @param string $type 变量类型 + * @return mixed + */ + public function except($name, $type = 'param') + { + $param = $this->$type(); + if (is_string($name)) { + $name = explode(',', $name); + } + + foreach ($name as $key) { + if (isset($param[$key])) { + unset($param[$key]); + } + } + + return $param; + } + + /** + * 当前是否ssl + * @access public + * @return bool + */ + public function isSsl() + { + if ($this->server('HTTPS') && ('1' == $this->server('HTTPS') || 'on' == strtolower($this->server('HTTPS')))) { + return true; + } elseif ('https' == $this->server('REQUEST_SCHEME')) { + return true; + } elseif ('443' == $this->server('SERVER_PORT')) { + return true; + } elseif ('https' == $this->server('HTTP_X_FORWARDED_PROTO')) { + return true; + } elseif ($this->config['https_agent_name'] && $this->server($this->config['https_agent_name'])) { + return true; + } + + return false; + } + + /** + * 当前是否JSON请求 + * @access public + * @return bool + */ + public function isJson() + { + return false !== strpos($this->type(), 'json'); + } + + /** + * 当前是否Ajax请求 + * @access public + * @param bool $ajax true 获取原始ajax请求 + * @return bool + */ + public function isAjax($ajax = false) + { + $value = $this->server('HTTP_X_REQUESTED_WITH'); + $result = 'xmlhttprequest' == strtolower($value) ? true : false; + + if (true === $ajax) { + return $result; + } + + $result = $this->param($this->config['var_ajax']) ? true : $result; + $this->mergeParam = false; + return $result; + } + + /** + * 当前是否Pjax请求 + * @access public + * @param bool $pjax true 获取原始pjax请求 + * @return bool + */ + public function isPjax($pjax = false) + { + $result = !is_null($this->server('HTTP_X_PJAX')) ? true : false; + + if (true === $pjax) { + return $result; + } + + $result = $this->param($this->config['var_pjax']) ? true : $result; + $this->mergeParam = false; + return $result; + } + + /** + * 获取客户端IP地址 + * @access public + * @param integer $type 返回类型 0 返回IP地址 1 返回IPV4地址数字 + * @param boolean $adv 是否进行高级模式获取(有可能被伪装) + * @return mixed + */ + public function ip($type = 0, $adv = true) + { + $type = $type ? 1 : 0; + static $ip = null; + + if (null !== $ip) { + return $ip[$type]; + } + + $httpAgentIp = $this->config['http_agent_ip']; + + if ($httpAgentIp && $this->server($httpAgentIp)) { + $ip = $this->server($httpAgentIp); + } elseif ($adv) { + if ($this->server('HTTP_X_FORWARDED_FOR')) { + $arr = explode(',', $this->server('HTTP_X_FORWARDED_FOR')); + $pos = array_search('unknown', $arr); + if (false !== $pos) { + unset($arr[$pos]); + } + $ip = trim(current($arr)); + } elseif ($this->server('HTTP_CLIENT_IP')) { + $ip = $this->server('HTTP_CLIENT_IP'); + } elseif ($this->server('REMOTE_ADDR')) { + $ip = $this->server('REMOTE_ADDR'); + } + } elseif ($this->server('REMOTE_ADDR')) { + $ip = $this->server('REMOTE_ADDR'); + } + + // IP地址类型 + $ip_mode = (strpos($ip, ':') === false) ? 'ipv4' : 'ipv6'; + + // IP地址合法验证 + if (filter_var($ip, FILTER_VALIDATE_IP) !== $ip) { + $ip = ('ipv4' === $ip_mode) ? '0.0.0.0' : '::'; + } + + // 如果是ipv4地址,则直接使用ip2long返回int类型ip;如果是ipv6地址,暂时不支持,直接返回0 + $long_ip = ('ipv4' === $ip_mode) ? sprintf("%u", ip2long($ip)) : 0; + + $ip = [$ip, $long_ip]; + + return $ip[$type]; + } + + /** + * 检测是否使用手机访问 + * @access public + * @return bool + */ + public function isMobile() + { + if ($this->server('HTTP_VIA') && stristr($this->server('HTTP_VIA'), "wap")) { + return true; + } elseif ($this->server('HTTP_ACCEPT') && strpos(strtoupper($this->server('HTTP_ACCEPT')), "VND.WAP.WML")) { + return true; + } elseif ($this->server('HTTP_X_WAP_PROFILE') || $this->server('HTTP_PROFILE')) { + return true; + } elseif ($this->server('HTTP_USER_AGENT') && preg_match('/(blackberry|configuration\/cldc|hp |hp-|htc |htc_|htc-|iemobile|kindle|midp|mmp|motorola|mobile|nokia|opera mini|opera |Googlebot-Mobile|YahooSeeker\/M1A1-R2D2|android|iphone|ipod|mobi|palm|palmos|pocket|portalmmm|ppc;|smartphone|sonyericsson|sqh|spv|symbian|treo|up.browser|up.link|vodafone|windows ce|xda |xda_)/i', $this->server('HTTP_USER_AGENT'))) { + return true; + } + + return false; + } + + /** + * 当前URL地址中的scheme参数 + * @access public + * @return string + */ + public function scheme() + { + return $this->isSsl() ? 'https' : 'http'; + } + + /** + * 当前请求URL地址中的query参数 + * @access public + * @return string + */ + public function query() + { + return $this->server('QUERY_STRING'); + } + + /** + * 设置当前请求的host(包含端口) + * @access public + * @param string $host 主机名(含端口) + * @return $this + */ + public function setHost($host) + { + $this->host = $host; + + return $this; + } + + /** + * 当前请求的host + * @access public + * @param bool $strict true 仅仅获取HOST + * @return string + */ + public function host($strict = false) + { + if (!$this->host) { + $this->host = $this->server('HTTP_X_REAL_HOST') ?: $this->server('HTTP_X_FORWARDED_HOST') ?: $this->server('HTTP_HOST'); + } + + return true === $strict && strpos($this->host, ':') ? strstr($this->host, ':', true) : $this->host; + } + + /** + * 当前请求URL地址中的port参数 + * @access public + * @return integer + */ + public function port() + { + return $this->server('SERVER_PORT'); + } + + /** + * 当前请求 SERVER_PROTOCOL + * @access public + * @return string + */ + public function protocol() + { + return $this->server('SERVER_PROTOCOL'); + } + + /** + * 当前请求 REMOTE_PORT + * @access public + * @return integer + */ + public function remotePort() + { + return $this->server('REMOTE_PORT'); + } + + /** + * 当前请求 HTTP_CONTENT_TYPE + * @access public + * @return string + */ + public function contentType() + { + $contentType = $this->server('CONTENT_TYPE'); + + if ($contentType) { + if (strpos($contentType, ';')) { + list($type) = explode(';', $contentType); + } else { + $type = $contentType; + } + return trim($type); + } + + return ''; + } + + /** + * 获取当前请求的路由信息 + * @access public + * @param array $route 路由名称 + * @return array + */ + public function routeInfo(array $route = []) + { + if (!empty($route)) { + $this->routeInfo = $route; + } + + return $this->routeInfo; + } + + /** + * 设置或者获取当前请求的调度信息 + * @access public + * @param \think\route\Dispatch $dispatch 调度信息 + * @return \think\route\Dispatch + */ + public function dispatch($dispatch = null) + { + if (!is_null($dispatch)) { + $this->dispatch = $dispatch; + } + + return $this->dispatch; + } + + /** + * 获取当前请求的安全Key + * @access public + * @return string + */ + public function secureKey() + { + if (is_null($this->secureKey)) { + $this->secureKey = uniqid('', true); + } + + return $this->secureKey; + } + + /** + * 设置当前的模块名 + * @access public + * @param string $module 模块名 + * @return $this + */ + public function setModule($module) + { + $this->module = $module; + return $this; + } + + /** + * 设置当前的控制器名 + * @access public + * @param string $controller 控制器名 + * @return $this + */ + public function setController($controller) + { + $this->controller = $controller; + return $this; + } + + /** + * 设置当前的操作名 + * @access public + * @param string $action 操作名 + * @return $this + */ + public function setAction($action) + { + $this->action = $action; + return $this; + } + + /** + * 获取当前的模块名 + * @access public + * @return string + */ + public function module() + { + return $this->module ?: ''; + } + + /** + * 获取当前的控制器名 + * @access public + * @param bool $convert 转换为小写 + * @return string + */ + public function controller($convert = false) + { + $name = $this->controller ?: ''; + return $convert ? strtolower($name) : $name; + } + + /** + * 获取当前的操作名 + * @access public + * @param bool $convert 转换为驼峰 + * @return string + */ + public function action($convert = false) + { + $name = $this->action ?: ''; + return $convert ? $name : strtolower($name); + } + + /** + * 设置当前的语言 + * @access public + * @param string $lang 语言名 + * @return $this + */ + public function setLangset($lang) + { + $this->langset = $lang; + return $this; + } + + /** + * 获取当前的语言 + * @access public + * @return string + */ + public function langset() + { + return $this->langset ?: ''; + } + + /** + * 设置或者获取当前请求的content + * @access public + * @return string + */ + public function getContent() + { + if (is_null($this->content)) { + $this->content = $this->input; + } + + return $this->content; + } + + /** + * 获取当前请求的php://input + * @access public + * @return string + */ + public function getInput() + { + return $this->input; + } + + /** + * 生成请求令牌 + * @access public + * @param string $name 令牌名称 + * @param mixed $type 令牌生成方法 + * @return string + */ + public function token($name = '__token__', $type = null) + { + $type = is_callable($type) ? $type : 'md5'; + $token = call_user_func($type, $this->server('REQUEST_TIME_FLOAT')); + + if ($this->isAjax()) { + header($name . ': ' . $token); + } + + facade\Session::set($name, $token); + + return $token; + } + + /** + * 设置当前地址的请求缓存 + * @access public + * @param string $key 缓存标识,支持变量规则 ,例如 item/:name/:id + * @param mixed $expire 缓存有效期 + * @param array $except 缓存排除 + * @param string $tag 缓存标签 + * @return mixed + */ + public function cache($key, $expire = null, $except = [], $tag = null) + { + if (!is_array($except)) { + $tag = $except; + $except = []; + } + + if (false === $key || !$this->isGet() || $this->isCheckCache || false === $expire) { + // 关闭当前缓存 + return; + } + + // 标记请求缓存检查 + $this->isCheckCache = true; + + foreach ($except as $rule) { + if (0 === stripos($this->url(), $rule)) { + return; + } + } + + if ($key instanceof \Closure) { + $key = call_user_func_array($key, [$this]); + } elseif (true === $key) { + // 自动缓存功能 + $key = '__URL__'; + } elseif (strpos($key, '|')) { + list($key, $fun) = explode('|', $key); + } + + // 特殊规则替换 + if (false !== strpos($key, '__')) { + $key = str_replace(['__MODULE__', '__CONTROLLER__', '__ACTION__', '__URL__'], [$this->module, $this->controller, $this->action, md5($this->url(true))], $key); + } + + if (false !== strpos($key, ':')) { + $param = $this->param(); + foreach ($param as $item => $val) { + if (is_string($val) && false !== strpos($key, ':' . $item)) { + $key = str_replace(':' . $item, $val, $key); + } + } + } elseif (strpos($key, ']')) { + if ('[' . $this->ext() . ']' == $key) { + // 缓存某个后缀的请求 + $key = md5($this->url()); + } else { + return; + } + } + + if (isset($fun)) { + $key = $fun($key); + } + + $this->cache = [$key, $expire, $tag]; + return $this->cache; + } + + /** + * 读取请求缓存设置 + * @access public + * @return array + */ + public function getCache() + { + return $this->cache; + } + + /** + * 设置GET数据 + * @access public + * @param array $get 数据 + * @return $this + */ + public function withGet(array $get) + { + $this->get = $get; + return $this; + } + + /** + * 设置POST数据 + * @access public + * @param array $post 数据 + * @return $this + */ + public function withPost(array $post) + { + $this->post = $post; + return $this; + } + + /** + * 设置php://input数据 + * @access public + * @param string $input RAW数据 + * @return $this + */ + public function withInput($input) + { + $this->input = $input; + return $this; + } + + /** + * 设置文件上传数据 + * @access public + * @param array $files 上传信息 + * @return $this + */ + public function withFiles(array $files) + { + $this->file = $files; + return $this; + } + + /** + * 设置COOKIE数据 + * @access public + * @param array $cookie 数据 + * @return $this + */ + public function withCookie(array $cookie) + { + $this->cookie = $cookie; + return $this; + } + + /** + * 设置SERVER数据 + * @access public + * @param array $server 数据 + * @return $this + */ + public function withServer(array $server) + { + $this->server = array_change_key_case($server, CASE_UPPER); + return $this; + } + + /** + * 设置HEADER数据 + * @access public + * @param array $header 数据 + * @return $this + */ + public function withHeader(array $header) + { + $this->header = array_change_key_case($header); + return $this; + } + + /** + * 设置ENV数据 + * @access public + * @param array $env 数据 + * @return $this + */ + public function withEnv(array $env) + { + $this->env = $env; + return $this; + } + + /** + * 设置ROUTE变量 + * @access public + * @param array $route 数据 + * @return $this + */ + public function withRoute(array $route) + { + $this->route = $route; + return $this; + } + + /** + * 设置请求数据 + * @access public + * @param string $name 参数名 + * @param mixed $value 值 + */ + public function __set($name, $value) + { + return $this->param[$name] = $value; + } + + /** + * 获取请求数据的值 + * @access public + * @param string $name 参数名 + * @return mixed + */ + public function __get($name) + { + return $this->param($name); + } + + /** + * 检测请求数据的值 + * @access public + * @param string $name 名称 + * @return boolean + */ + public function __isset($name) + { + return isset($this->param[$name]); + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['dispatch'], $data['config']); + + return $data; + } +} diff --git a/vendor/topthink/framework/library/think/Response.php b/vendor/topthink/framework/library/think/Response.php new file mode 100644 index 0000000..5fa5402 --- /dev/null +++ b/vendor/topthink/framework/library/think/Response.php @@ -0,0 +1,429 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\response\Redirect as RedirectResponse; + +class Response +{ + /** + * 原始数据 + * @var mixed + */ + protected $data; + + /** + * 应用对象实例 + * @var App + */ + protected $app; + + /** + * 当前contentType + * @var string + */ + protected $contentType = 'text/html'; + + /** + * 字符集 + * @var string + */ + protected $charset = 'utf-8'; + + /** + * 状态码 + * @var integer + */ + protected $code = 200; + + /** + * 是否允许请求缓存 + * @var bool + */ + protected $allowCache = true; + + /** + * 输出参数 + * @var array + */ + protected $options = []; + + /** + * header参数 + * @var array + */ + protected $header = []; + + /** + * 输出内容 + * @var string + */ + protected $content = null; + + /** + * 架构函数 + * @access public + * @param mixed $data 输出数据 + * @param int $code + * @param array $header + * @param array $options 输出参数 + */ + public function __construct($data = '', $code = 200, array $header = [], $options = []) + { + $this->data($data); + + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + $this->contentType($this->contentType, $this->charset); + + $this->code = $code; + $this->app = Container::get('app'); + $this->header = array_merge($this->header, $header); + } + + /** + * 创建Response对象 + * @access public + * @param mixed $data 输出数据 + * @param string $type 输出类型 + * @param int $code + * @param array $header + * @param array $options 输出参数 + * @return Response + */ + public static function create($data = '', $type = '', $code = 200, array $header = [], $options = []) + { + $class = false !== strpos($type, '\\') ? $type : '\\think\\response\\' . ucfirst(strtolower($type)); + + if (class_exists($class)) { + return new $class($data, $code, $header, $options); + } + + return new static($data, $code, $header, $options); + } + + /** + * 发送数据到客户端 + * @access public + * @return void + * @throws \InvalidArgumentException + */ + public function send() + { + // 监听response_send + $this->app['hook']->listen('response_send', $this); + + // 处理输出数据 + $data = $this->getContent(); + + // Trace调试注入 + if ('cli' != PHP_SAPI && $this->app['env']->get('app_trace', $this->app->config('app.app_trace'))) { + $this->app['debug']->inject($this, $data); + } + + if (200 == $this->code && $this->allowCache) { + $cache = $this->app['request']->getCache(); + if ($cache) { + $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'; + + $this->app['cache']->tag($cache[2])->set($cache[0], [$data, $this->header], $cache[1]); + } + } + + if (!headers_sent() && !empty($this->header)) { + // 发送状态码 + http_response_code($this->code); + // 发送头部信息 + foreach ($this->header as $name => $val) { + header($name . (!is_null($val) ? ':' . $val : '')); + } + } + + $this->sendData($data); + + if (function_exists('fastcgi_finish_request')) { + // 提高页面响应 + fastcgi_finish_request(); + } + + // 监听response_end + $this->app['hook']->listen('response_end', $this); + + // 清空当次请求有效的数据 + if (!($this instanceof RedirectResponse)) { + $this->app['session']->flush(); + } + } + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + return $data; + } + + /** + * 输出数据 + * @access protected + * @param string $data 要处理的数据 + * @return void + */ + protected function sendData($data) + { + echo $data; + } + + /** + * 输出的参数 + * @access public + * @param mixed $options 输出参数 + * @return $this + */ + public function options($options = []) + { + $this->options = array_merge($this->options, $options); + + return $this; + } + + /** + * 输出数据设置 + * @access public + * @param mixed $data 输出数据 + * @return $this + */ + public function data($data) + { + $this->data = $data; + + return $this; + } + + /** + * 是否允许请求缓存 + * @access public + * @param bool $cache 允许请求缓存 + * @return $this + */ + public function allowCache($cache) + { + $this->allowCache = $cache; + + return $this; + } + + /** + * 设置响应头 + * @access public + * @param string|array $name 参数名 + * @param string $value 参数值 + * @return $this + */ + public function header($name, $value = null) + { + if (is_array($name)) { + $this->header = array_merge($this->header, $name); + } else { + $this->header[$name] = $value; + } + + return $this; + } + + /** + * 设置页面输出内容 + * @access public + * @param mixed $content + * @return $this + */ + public function content($content) + { + if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([ + $content, + '__toString', + ]) + ) { + throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content))); + } + + $this->content = (string) $content; + + return $this; + } + + /** + * 发送HTTP状态 + * @access public + * @param integer $code 状态码 + * @return $this + */ + public function code($code) + { + $this->code = $code; + + return $this; + } + + /** + * LastModified + * @access public + * @param string $time + * @return $this + */ + public function lastModified($time) + { + $this->header['Last-Modified'] = $time; + + return $this; + } + + /** + * Expires + * @access public + * @param string $time + * @return $this + */ + public function expires($time) + { + $this->header['Expires'] = $time; + + return $this; + } + + /** + * ETag + * @access public + * @param string $eTag + * @return $this + */ + public function eTag($eTag) + { + $this->header['ETag'] = $eTag; + + return $this; + } + + /** + * 页面缓存控制 + * @access public + * @param string $cache 缓存设置 + * @return $this + */ + public function cacheControl($cache) + { + $this->header['Cache-control'] = $cache; + + return $this; + } + + /** + * 设置页面不做任何缓存 + * @access public + * @return $this + */ + public function noCache() + { + $this->header['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'; + $this->header['Pragma'] = 'no-cache'; + + return $this; + } + + /** + * 页面输出类型 + * @access public + * @param string $contentType 输出类型 + * @param string $charset 输出编码 + * @return $this + */ + public function contentType($contentType, $charset = 'utf-8') + { + $this->header['Content-Type'] = $contentType . '; charset=' . $charset; + + return $this; + } + + /** + * 获取头部信息 + * @access public + * @param string $name 头部名称 + * @return mixed + */ + public function getHeader($name = '') + { + if (!empty($name)) { + return isset($this->header[$name]) ? $this->header[$name] : null; + } + + return $this->header; + } + + /** + * 获取原始数据 + * @access public + * @return mixed + */ + public function getData() + { + return $this->data; + } + + /** + * 获取输出数据 + * @access public + * @return mixed + */ + public function getContent() + { + if (null == $this->content) { + $content = $this->output($this->data); + + if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([ + $content, + '__toString', + ]) + ) { + throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content))); + } + + $this->content = (string) $content; + } + + return $this->content; + } + + /** + * 获取状态码 + * @access public + * @return integer + */ + public function getCode() + { + return $this->code; + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app']); + + return $data; + } +} diff --git a/vendor/topthink/framework/library/think/Route.php b/vendor/topthink/framework/library/think/Route.php new file mode 100644 index 0000000..97f6dc7 --- /dev/null +++ b/vendor/topthink/framework/library/think/Route.php @@ -0,0 +1,992 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\RouteNotFoundException; +use think\route\AliasRule; +use think\route\Dispatch; +use think\route\dispatch\Url as UrlDispatch; +use think\route\Domain; +use think\route\Resource; +use think\route\Rule; +use think\route\RuleGroup; +use think\route\RuleItem; + +class Route +{ + /** + * REST定义 + * @var array + */ + protected $rest = [ + 'index' => ['get', '', 'index'], + 'create' => ['get', '/create', 'create'], + 'edit' => ['get', '//edit', 'edit'], + 'read' => ['get', '/', 'read'], + 'save' => ['post', '', 'save'], + 'update' => ['put', '/', 'update'], + 'delete' => ['delete', '/', 'delete'], + ]; + + /** + * 请求方法前缀定义 + * @var array + */ + protected $methodPrefix = [ + 'get' => 'get', + 'post' => 'post', + 'put' => 'put', + 'delete' => 'delete', + 'patch' => 'patch', + ]; + + /** + * 应用对象 + * @var App + */ + protected $app; + + /** + * 请求对象 + * @var Request + */ + protected $request; + + /** + * 当前HOST + * @var string + */ + protected $host; + + /** + * 当前域名 + * @var string + */ + protected $domain; + + /** + * 当前分组对象 + * @var RuleGroup + */ + protected $group; + + /** + * 配置参数 + * @var array + */ + protected $config = []; + + /** + * 路由绑定 + * @var array + */ + protected $bind = []; + + /** + * 域名对象 + * @var array + */ + protected $domains = []; + + /** + * 跨域路由规则 + * @var RuleGroup + */ + protected $cross; + + /** + * 路由别名 + * @var array + */ + protected $alias = []; + + /** + * 路由是否延迟解析 + * @var bool + */ + protected $lazy = true; + + /** + * 路由是否测试模式 + * @var bool + */ + protected $isTest; + + /** + * (分组)路由规则是否合并解析 + * @var bool + */ + protected $mergeRuleRegex = true; + + /** + * 路由解析自动搜索多级控制器 + * @var bool + */ + protected $autoSearchController = true; + + public function __construct(App $app, array $config = []) + { + $this->app = $app; + $this->request = $app['request']; + $this->config = $config; + + $this->host = $this->request->host(true) ?: $config['app_host']; + + $this->setDefaultDomain(); + } + + public function config($name = null) + { + if (is_null($name)) { + return $this->config; + } + + return isset($this->config[$name]) ? $this->config[$name] : null; + } + + /** + * 配置 + * @access public + * @param array $config + * @return void + */ + public function setConfig(array $config = []) + { + $this->config = array_merge($this->config, array_change_key_case($config)); + } + + public static function __make(App $app, Config $config) + { + $config = $config->pull('app'); + $route = new static($app, $config); + + $route->lazy($config['url_lazy_route']) + ->autoSearchController($config['controller_auto_search']) + ->mergeRuleRegex($config['route_rule_merge']); + + return $route; + } + + /** + * 设置路由的请求对象实例 + * @access public + * @param Request $request 请求对象实例 + * @return void + */ + public function setRequest($request) + { + $this->request = $request; + } + + /** + * 设置路由域名及分组(包括资源路由)是否延迟解析 + * @access public + * @param bool $lazy 路由是否延迟解析 + * @return $this + */ + public function lazy($lazy = true) + { + $this->lazy = $lazy; + return $this; + } + + /** + * 设置路由为测试模式 + * @access public + * @param bool $test 路由是否测试模式 + * @return void + */ + public function setTestMode($test) + { + $this->isTest = $test; + } + + /** + * 检查路由是否为测试模式 + * @access public + * @return bool + */ + public function isTest() + { + return $this->isTest; + } + + /** + * 设置路由域名及分组(包括资源路由)是否合并解析 + * @access public + * @param bool $merge 路由是否合并解析 + * @return $this + */ + public function mergeRuleRegex($merge = true) + { + $this->mergeRuleRegex = $merge; + $this->group->mergeRuleRegex($merge); + + return $this; + } + + /** + * 设置路由自动解析是否搜索多级控制器 + * @access public + * @param bool $auto 是否自动搜索多级控制器 + * @return $this + */ + public function autoSearchController($auto = true) + { + $this->autoSearchController = $auto; + return $this; + } + + /** + * 初始化默认域名 + * @access protected + * @return void + */ + protected function setDefaultDomain() + { + // 默认域名 + $this->domain = $this->host; + + // 注册默认域名 + $domain = new Domain($this, $this->host); + + $this->domains[$this->host] = $domain; + + // 默认分组 + $this->group = $domain; + } + + /** + * 设置当前域名 + * @access public + * @param RuleGroup $group 域名 + * @return void + */ + public function setGroup(RuleGroup $group) + { + $this->group = $group; + } + + /** + * 获取当前分组 + * @access public + * @return RuleGroup + */ + public function getGroup() + { + return $this->group; + } + + /** + * 注册变量规则 + * @access public + * @param string|array $name 变量名 + * @param string $rule 变量规则 + * @return $this + */ + public function pattern($name, $rule = '') + { + $this->group->pattern($name, $rule); + + return $this; + } + + /** + * 注册路由参数 + * @access public + * @param string|array $name 参数名 + * @param mixed $value 值 + * @return $this + */ + public function option($name, $value = '') + { + $this->group->option($name, $value); + + return $this; + } + + /** + * 注册域名路由 + * @access public + * @param string|array $name 子域名 + * @param mixed $rule 路由规则 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return Domain + */ + public function domain($name, $rule = '', $option = [], $pattern = []) + { + // 支持多个域名使用相同路由规则 + $domainName = is_array($name) ? array_shift($name) : $name; + + if ('*' != $domainName && false === strpos($domainName, '.')) { + $domainName .= '.' . $this->request->rootDomain(); + } + + if (!isset($this->domains[$domainName])) { + $domain = (new Domain($this, $domainName, $rule, $option, $pattern)) + ->lazy($this->lazy) + ->mergeRuleRegex($this->mergeRuleRegex); + + $this->domains[$domainName] = $domain; + } else { + $domain = $this->domains[$domainName]; + $domain->parseGroupRule($rule); + } + + if (is_array($name) && !empty($name)) { + $root = $this->request->rootDomain(); + foreach ($name as $item) { + if (false === strpos($item, '.')) { + $item .= '.' . $root; + } + + $this->domains[$item] = $domainName; + } + } + + // 返回域名对象 + return $domain; + } + + /** + * 获取域名 + * @access public + * @return array + */ + public function getDomains() + { + return $this->domains; + } + + /** + * 设置路由绑定 + * @access public + * @param string $bind 绑定信息 + * @param string $domain 域名 + * @return $this + */ + public function bind($bind, $domain = null) + { + $domain = is_null($domain) ? $this->domain : $domain; + + $this->bind[$domain] = $bind; + + return $this; + } + + /** + * 读取路由绑定 + * @access public + * @param string $domain 域名 + * @return string|null + */ + public function getBind($domain = null) + { + if (is_null($domain)) { + $domain = $this->domain; + } elseif (true === $domain) { + return $this->bind; + } elseif (false === strpos($domain, '.')) { + $domain .= '.' . $this->request->rootDomain(); + } + + $subDomain = $this->request->subDomain(); + + if (strpos($subDomain, '.')) { + $name = '*' . strstr($subDomain, '.'); + } + + if (isset($this->bind[$domain])) { + $result = $this->bind[$domain]; + } elseif (isset($name) && isset($this->bind[$name])) { + $result = $this->bind[$name]; + } elseif (!empty($subDomain) && isset($this->bind['*'])) { + $result = $this->bind['*']; + } else { + $result = null; + } + + return $result; + } + + /** + * 读取路由标识 + * @access public + * @param string $name 路由标识 + * @param string $domain 域名 + * @return mixed + */ + public function getName($name = null, $domain = null, $method = '*') + { + return $this->app['rule_name']->get($name, $domain, $method); + } + + /** + * 读取路由 + * @access public + * @param string $rule 路由规则 + * @param string $domain 域名 + * @return array + */ + public function getRule($rule, $domain = null) + { + if (is_null($domain)) { + $domain = $this->domain; + } + + return $this->app['rule_name']->getRule($rule, $domain); + } + + /** + * 读取路由 + * @access public + * @param string $domain 域名 + * @return array + */ + public function getRuleList($domain = null) + { + return $this->app['rule_name']->getRuleList($domain); + } + + /** + * 批量导入路由标识 + * @access public + * @param array $name 路由标识 + * @return $this + */ + public function setName($name) + { + $this->app['rule_name']->import($name); + return $this; + } + + /** + * 导入配置文件的路由规则 + * @access public + * @param array $rules 路由规则 + * @param string $type 请求类型 + * @return void + */ + public function import(array $rules, $type = '*') + { + // 检查域名部署 + if (isset($rules['__domain__'])) { + foreach ($rules['__domain__'] as $key => $rule) { + $this->domain($key, $rule); + } + unset($rules['__domain__']); + } + + // 检查变量规则 + if (isset($rules['__pattern__'])) { + $this->pattern($rules['__pattern__']); + unset($rules['__pattern__']); + } + + // 检查路由别名 + if (isset($rules['__alias__'])) { + foreach ($rules['__alias__'] as $key => $val) { + $this->alias($key, $val); + } + unset($rules['__alias__']); + } + + // 检查资源路由 + if (isset($rules['__rest__'])) { + foreach ($rules['__rest__'] as $key => $rule) { + $this->resource($key, $rule); + } + unset($rules['__rest__']); + } + + // 检查路由规则(包含分组) + foreach ($rules as $key => $val) { + if (is_numeric($key)) { + $key = array_shift($val); + } + + if (empty($val)) { + continue; + } + + if (is_string($key) && 0 === strpos($key, '[')) { + $key = substr($key, 1, -1); + $this->group($key, $val); + } elseif (is_array($val)) { + $this->rule($key, $val[0], $type, $val[1], isset($val[2]) ? $val[2] : []); + } else { + $this->rule($key, $val, $type); + } + } + } + + /** + * 注册路由规则 + * @access public + * @param string $rule 路由规则 + * @param mixed $route 路由地址 + * @param string $method 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function rule($rule, $route, $method = '*', array $option = [], array $pattern = []) + { + return $this->group->addRule($rule, $route, $method, $option, $pattern); + } + + /** + * 设置跨域有效路由规则 + * @access public + * @param Rule $rule 路由规则 + * @param string $method 请求类型 + * @return $this + */ + public function setCrossDomainRule($rule, $method = '*') + { + if (!isset($this->cross)) { + $this->cross = (new RuleGroup($this))->mergeRuleRegex($this->mergeRuleRegex); + } + + $this->cross->addRuleItem($rule, $method); + + return $this; + } + + /** + * 批量注册路由规则 + * @access public + * @param array $rules 路由规则 + * @param string $method 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public function rules($rules, $method = '*', array $option = [], array $pattern = []) + { + $this->group->addRules($rules, $method, $option, $pattern); + } + + /** + * 注册路由分组 + * @access public + * @param string|array $name 分组名称或者参数 + * @param array|\Closure $route 分组路由 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleGroup + */ + public function group($name, $route, array $option = [], array $pattern = []) + { + if (is_array($name)) { + $option = $name; + $name = isset($option['name']) ? $option['name'] : ''; + } + + return (new RuleGroup($this, $this->group, $name, $route, $option, $pattern)) + ->lazy($this->lazy) + ->mergeRuleRegex($this->mergeRuleRegex); + } + + /** + * 注册路由 + * @access public + * @param string $rule 路由规则 + * @param mixed $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function any($rule, $route = '', array $option = [], array $pattern = []) + { + return $this->rule($rule, $route, '*', $option, $pattern); + } + + /** + * 注册GET路由 + * @access public + * @param string $rule 路由规则 + * @param mixed $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function get($rule, $route = '', array $option = [], array $pattern = []) + { + return $this->rule($rule, $route, 'GET', $option, $pattern); + } + + /** + * 注册POST路由 + * @access public + * @param string $rule 路由规则 + * @param mixed $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function post($rule, $route = '', array $option = [], array $pattern = []) + { + return $this->rule($rule, $route, 'POST', $option, $pattern); + } + + /** + * 注册PUT路由 + * @access public + * @param string $rule 路由规则 + * @param mixed $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function put($rule, $route = '', array $option = [], array $pattern = []) + { + return $this->rule($rule, $route, 'PUT', $option, $pattern); + } + + /** + * 注册DELETE路由 + * @access public + * @param string $rule 路由规则 + * @param mixed $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function delete($rule, $route = '', array $option = [], array $pattern = []) + { + return $this->rule($rule, $route, 'DELETE', $option, $pattern); + } + + /** + * 注册PATCH路由 + * @access public + * @param string $rule 路由规则 + * @param mixed $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function patch($rule, $route = '', array $option = [], array $pattern = []) + { + return $this->rule($rule, $route, 'PATCH', $option, $pattern); + } + + /** + * 注册资源路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return Resource + */ + public function resource($rule, $route = '', array $option = [], array $pattern = []) + { + return (new Resource($this, $this->group, $rule, $route, $option, $pattern, $this->rest)) + ->lazy($this->lazy); + } + + /** + * 注册控制器路由 操作方法对应不同的请求前缀 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleGroup + */ + public function controller($rule, $route = '', array $option = [], array $pattern = []) + { + $group = new RuleGroup($this, $this->group, $rule, null, $option, $pattern); + + foreach ($this->methodPrefix as $type => $val) { + $group->addRule('', $val . '', $type); + } + + return $group->prefix($route . '/'); + } + + /** + * 注册视图路由 + * @access public + * @param string|array $rule 路由规则 + * @param string $template 路由模板地址 + * @param array $vars 模板变量 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function view($rule, $template = '', array $vars = [], array $option = [], array $pattern = []) + { + return $this->rule($rule, $template, 'GET', $option, $pattern)->view($vars); + } + + /** + * 注册重定向路由 + * @access public + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param array $status 状态码 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return RuleItem + */ + public function redirect($rule, $route = '', $status = 301, array $option = [], array $pattern = []) + { + return $this->rule($rule, $route, '*', $option, $pattern)->redirect()->status($status); + } + + /** + * 注册别名路由 + * @access public + * @param string $rule 路由别名 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @return AliasRule + */ + public function alias($rule, $route, array $option = []) + { + $aliasRule = new AliasRule($this, $this->group, $rule, $route, $option); + + $this->alias[$rule] = $aliasRule; + + return $aliasRule; + } + + /** + * 获取别名路由定义 + * @access public + * @param string $name 路由别名 + * @return string|array|null + */ + public function getAlias($name = null) + { + if (is_null($name)) { + return $this->alias; + } + + return isset($this->alias[$name]) ? $this->alias[$name] : null; + } + + /** + * 设置不同请求类型下面的方法前缀 + * @access public + * @param string|array $method 请求类型 + * @param string $prefix 类型前缀 + * @return $this + */ + public function setMethodPrefix($method, $prefix = '') + { + if (is_array($method)) { + $this->methodPrefix = array_merge($this->methodPrefix, array_change_key_case($method)); + } else { + $this->methodPrefix[strtolower($method)] = $prefix; + } + + return $this; + } + + /** + * 获取请求类型的方法前缀 + * @access public + * @param string $method 请求类型 + * @param string $prefix 类型前缀 + * @return string|null + */ + public function getMethodPrefix($method) + { + $method = strtolower($method); + + return isset($this->methodPrefix[$method]) ? $this->methodPrefix[$method] : null; + } + + /** + * rest方法定义和修改 + * @access public + * @param string $name 方法名称 + * @param array|bool $resource 资源 + * @return $this + */ + public function rest($name, $resource = []) + { + if (is_array($name)) { + $this->rest = $resource ? $name : array_merge($this->rest, $name); + } else { + $this->rest[$name] = $resource; + } + + return $this; + } + + /** + * 获取rest方法定义的参数 + * @access public + * @param string $name 方法名称 + * @return array|null + */ + public function getRest($name = null) + { + if (is_null($name)) { + return $this->rest; + } + + return isset($this->rest[$name]) ? $this->rest[$name] : null; + } + + /** + * 注册未匹配路由规则后的处理 + * @access public + * @param string $route 路由地址 + * @param string $method 请求类型 + * @param array $option 路由参数 + * @return RuleItem + */ + public function miss($route, $method = '*', array $option = []) + { + return $this->group->addMissRule($route, $method, $option); + } + + /** + * 注册一个自动解析的URL路由 + * @access public + * @param string $route 路由地址 + * @return RuleItem + */ + public function auto($route) + { + return $this->group->addAutoRule($route); + } + + /** + * 检测URL路由 + * @access public + * @param string $url URL地址 + * @param bool $must 是否强制路由 + * @return Dispatch + * @throws RouteNotFoundException + */ + public function check($url, $must = false) + { + // 自动检测域名路由 + $domain = $this->checkDomain(); + $url = str_replace($this->config['pathinfo_depr'], '|', $url); + + $completeMatch = $this->config['route_complete_match']; + + $result = $domain->check($this->request, $url, $completeMatch); + + if (false === $result && !empty($this->cross)) { + // 检测跨域路由 + $result = $this->cross->check($this->request, $url, $completeMatch); + } + + if (false !== $result) { + // 路由匹配 + return $result; + } elseif ($must) { + // 强制路由不匹配则抛出异常 + throw new RouteNotFoundException(); + } + + // 默认路由解析 + return new UrlDispatch($this->request, $this->group, $url, [ + 'auto_search' => $this->autoSearchController, + ]); + } + + /** + * 检测域名的路由规则 + * @access protected + * @return Domain + */ + protected function checkDomain() + { + // 获取当前子域名 + $subDomain = $this->request->subDomain(); + + $item = false; + + if ($subDomain && count($this->domains) > 1) { + $domain = explode('.', $subDomain); + $domain2 = array_pop($domain); + + if ($domain) { + // 存在三级域名 + $domain3 = array_pop($domain); + } + + if ($subDomain && isset($this->domains[$subDomain])) { + // 子域名配置 + $item = $this->domains[$subDomain]; + } elseif (isset($this->domains['*.' . $domain2]) && !empty($domain3)) { + // 泛三级域名 + $item = $this->domains['*.' . $domain2]; + $panDomain = $domain3; + } elseif (isset($this->domains['*']) && !empty($domain2)) { + // 泛二级域名 + if ('www' != $domain2) { + $item = $this->domains['*']; + $panDomain = $domain2; + } + } + + if (isset($panDomain)) { + // 保存当前泛域名 + $this->request->setPanDomain($panDomain); + } + } + + if (false === $item) { + // 检测当前完整域名 + $item = $this->domains[$this->host]; + } + + if (is_string($item)) { + $item = $this->domains[$item]; + } + + return $item; + } + + /** + * 清空路由规则 + * @access public + * @return void + */ + public function clear() + { + $this->app['rule_name']->clear(); + $this->group->clear(); + } + + /** + * 设置全局的路由分组参数 + * @access public + * @param string $method 方法名 + * @param array $args 调用参数 + * @return RuleGroup + */ + public function __call($method, $args) + { + return call_user_func_array([$this->group, $method], $args); + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app'], $data['request']); + + return $data; + } +} diff --git a/vendor/topthink/framework/library/think/Session.php b/vendor/topthink/framework/library/think/Session.php new file mode 100644 index 0000000..63ee7a0 --- /dev/null +++ b/vendor/topthink/framework/library/think/Session.php @@ -0,0 +1,579 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ClassNotFoundException; + +class Session +{ + /** + * 配置参数 + * @var array + */ + protected $config = []; + + /** + * 前缀 + * @var string + */ + protected $prefix = ''; + + /** + * 是否初始化 + * @var bool + */ + protected $init = null; + + /** + * 锁驱动 + * @var object + */ + protected $lockDriver = null; + + /** + * 锁key + * @var string + */ + protected $sessKey = 'PHPSESSID'; + + /** + * 锁超时时间 + * @var integer + */ + protected $lockTimeout = 3; + + /** + * 是否启用锁机制 + * @var bool + */ + protected $lock = false; + + public function __construct(array $config = []) + { + $this->config = $config; + } + + /** + * 设置或者获取session作用域(前缀) + * @access public + * @param string $prefix + * @return string|void + */ + public function prefix($prefix = '') + { + empty($this->init) && $this->boot(); + + if (empty($prefix) && null !== $prefix) { + return $this->prefix; + } else { + $this->prefix = $prefix; + } + } + + public static function __make(Config $config) + { + return new static($config->pull('session')); + } + + /** + * 配置 + * @access public + * @param array $config + * @return void + */ + public function setConfig(array $config = []) + { + $this->config = array_merge($this->config, array_change_key_case($config)); + + if (isset($config['prefix'])) { + $this->prefix = $config['prefix']; + } + + if (isset($config['use_lock'])) { + $this->lock = $config['use_lock']; + } + } + + /** + * 设置已经初始化 + * @access public + * @return void + */ + public function inited() + { + $this->init = true; + } + + /** + * session初始化 + * @access public + * @param array $config + * @return void + * @throws \think\Exception + */ + public function init(array $config = []) + { + $config = $config ?: $this->config; + + $isDoStart = false; + if (isset($config['use_trans_sid'])) { + ini_set('session.use_trans_sid', $config['use_trans_sid'] ? 1 : 0); + } + + // 启动session + if (!empty($config['auto_start']) && PHP_SESSION_ACTIVE != session_status()) { + ini_set('session.auto_start', 0); + $isDoStart = true; + } + + if (isset($config['prefix'])) { + $this->prefix = $config['prefix']; + } + + if (isset($config['use_lock'])) { + $this->lock = $config['use_lock']; + } + + if (isset($config['var_session_id']) && isset($_REQUEST[$config['var_session_id']])) { + session_id($_REQUEST[$config['var_session_id']]); + } elseif (isset($config['id']) && !empty($config['id'])) { + session_id($config['id']); + } + + if (isset($config['name'])) { + session_name($config['name']); + } + + if (isset($config['path'])) { + session_save_path($config['path']); + } + + if (isset($config['domain'])) { + ini_set('session.cookie_domain', $config['domain']); + } + + if (isset($config['expire'])) { + ini_set('session.gc_maxlifetime', $config['expire']); + ini_set('session.cookie_lifetime', $config['expire']); + } + + if (isset($config['secure'])) { + ini_set('session.cookie_secure', $config['secure']); + } + + if (isset($config['httponly'])) { + ini_set('session.cookie_httponly', $config['httponly']); + } + + if (isset($config['use_cookies'])) { + ini_set('session.use_cookies', $config['use_cookies'] ? 1 : 0); + } + + if (isset($config['cache_limiter'])) { + session_cache_limiter($config['cache_limiter']); + } + + if (isset($config['cache_expire'])) { + session_cache_expire($config['cache_expire']); + } + + if (!empty($config['type'])) { + // 读取session驱动 + $class = false !== strpos($config['type'], '\\') ? $config['type'] : '\\think\\session\\driver\\' . ucwords($config['type']); + + // 检查驱动类 + if (!class_exists($class) || !session_set_save_handler(new $class($config))) { + throw new ClassNotFoundException('error session handler:' . $class, $class); + } + } + + if ($isDoStart) { + $this->start(); + } else { + $this->init = false; + } + + return $this; + } + + /** + * session自动启动或者初始化 + * @access public + * @return void + */ + public function boot() + { + if (is_null($this->init)) { + $this->init(); + } + + if (false === $this->init) { + if (PHP_SESSION_ACTIVE != session_status()) { + $this->start(); + } + $this->init = true; + } + } + + /** + * session设置 + * @access public + * @param string $name session名称 + * @param mixed $value session值 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public function set($name, $value, $prefix = null) + { + $this->lock(); + + empty($this->init) && $this->boot(); + + $prefix = !is_null($prefix) ? $prefix : $this->prefix; + + if (strpos($name, '.')) { + // 二维数组赋值 + list($name1, $name2) = explode('.', $name); + if ($prefix) { + $_SESSION[$prefix][$name1][$name2] = $value; + } else { + $_SESSION[$name1][$name2] = $value; + } + } elseif ($prefix) { + $_SESSION[$prefix][$name] = $value; + } else { + $_SESSION[$name] = $value; + } + + $this->unlock(); + } + + /** + * session获取 + * @access public + * @param string $name session名称 + * @param string|null $prefix 作用域(前缀) + * @return mixed + */ + public function get($name = '', $prefix = null) + { + $this->lock(); + + empty($this->init) && $this->boot(); + + $prefix = !is_null($prefix) ? $prefix : $this->prefix; + + $value = $prefix ? (!empty($_SESSION[$prefix]) ? $_SESSION[$prefix] : []) : $_SESSION; + + if ('' != $name) { + $name = explode('.', $name); + + foreach ($name as $val) { + if (isset($value[$val])) { + $value = $value[$val]; + } else { + $value = null; + break; + } + } + } + + $this->unlock(); + + return $value; + } + + /** + * session 读写锁驱动实例化 + */ + protected function initDriver() + { + $config = $this->config; + + if (!empty($config['type']) && isset($config['use_lock']) && $config['use_lock']) { + // 读取session驱动 + $class = false !== strpos($config['type'], '\\') ? $config['type'] : '\\think\\session\\driver\\' . ucwords($config['type']); + + // 检查驱动类及类中是否存在 lock 和 unlock 函数 + if (class_exists($class) && method_exists($class, 'lock') && method_exists($class, 'unlock')) { + $this->lockDriver = new $class($config); + } + } + + // 通过cookie获得session_id + if (isset($config['name']) && $config['name']) { + $this->sessKey = $config['name']; + } + + if (isset($config['lock_timeout']) && $config['lock_timeout'] > 0) { + $this->lockTimeout = $config['lock_timeout']; + } + } + + /** + * session 读写加锁 + * @access protected + * @return void + */ + protected function lock() + { + if (empty($this->lock)) { + return; + } + + $this->initDriver(); + + if (null !== $this->lockDriver && method_exists($this->lockDriver, 'lock')) { + $t = time(); + // 使用 session_id 作为互斥条件,即只对同一 session_id 的会话互斥。第一次请求没有 session_id + $sessID = isset($_COOKIE[$this->sessKey]) ? $_COOKIE[$this->sessKey] : ''; + + do { + if (time() - $t > $this->lockTimeout) { + $this->unlock(); + } + } while (!$this->lockDriver->lock($sessID, $this->lockTimeout)); + } + } + + /** + * session 读写解锁 + * @access protected + * @return void + */ + protected function unlock() + { + if (empty($this->lock)) { + return; + } + + $this->pause(); + + if ($this->lockDriver && method_exists($this->lockDriver, 'unlock')) { + $sessID = isset($_COOKIE[$this->sessKey]) ? $_COOKIE[$this->sessKey] : ''; + $this->lockDriver->unlock($sessID); + } + } + + /** + * session获取并删除 + * @access public + * @param string $name session名称 + * @param string|null $prefix 作用域(前缀) + * @return mixed + */ + public function pull($name, $prefix = null) + { + $result = $this->get($name, $prefix); + + if ($result) { + $this->delete($name, $prefix); + return $result; + } else { + return; + } + } + + /** + * session设置 下一次请求有效 + * @access public + * @param string $name session名称 + * @param mixed $value session值 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public function flash($name, $value) + { + $this->set($name, $value); + + if (!$this->has('__flash__.__time__')) { + $this->set('__flash__.__time__', $_SERVER['REQUEST_TIME_FLOAT']); + } + + $this->push('__flash__', $name); + } + + /** + * 清空当前请求的session数据 + * @access public + * @return void + */ + public function flush() + { + if (!$this->init) { + return; + } + + $item = $this->get('__flash__'); + + if (!empty($item)) { + $time = $item['__time__']; + + if ($_SERVER['REQUEST_TIME_FLOAT'] > $time) { + unset($item['__time__']); + $this->delete($item); + $this->set('__flash__', []); + } + } + } + + /** + * 删除session数据 + * @access public + * @param string|array $name session名称 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public function delete($name, $prefix = null) + { + empty($this->init) && $this->boot(); + + $prefix = !is_null($prefix) ? $prefix : $this->prefix; + + if (is_array($name)) { + foreach ($name as $key) { + $this->delete($key, $prefix); + } + } elseif (strpos($name, '.')) { + list($name1, $name2) = explode('.', $name); + if ($prefix) { + unset($_SESSION[$prefix][$name1][$name2]); + } else { + unset($_SESSION[$name1][$name2]); + } + } else { + if ($prefix) { + unset($_SESSION[$prefix][$name]); + } else { + unset($_SESSION[$name]); + } + } + } + + /** + * 清空session数据 + * @access public + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public function clear($prefix = null) + { + empty($this->init) && $this->boot(); + $prefix = !is_null($prefix) ? $prefix : $this->prefix; + + if ($prefix) { + unset($_SESSION[$prefix]); + } else { + $_SESSION = []; + } + } + + /** + * 判断session数据 + * @access public + * @param string $name session名称 + * @param string|null $prefix + * @return bool + */ + public function has($name, $prefix = null) + { + empty($this->init) && $this->boot(); + + $prefix = !is_null($prefix) ? $prefix : $this->prefix; + $value = $prefix ? (!empty($_SESSION[$prefix]) ? $_SESSION[$prefix] : []) : $_SESSION; + + $name = explode('.', $name); + + foreach ($name as $val) { + if (!isset($value[$val])) { + return false; + } else { + $value = $value[$val]; + } + } + + return true; + } + + /** + * 添加数据到一个session数组 + * @access public + * @param string $key + * @param mixed $value + * @return void + */ + public function push($key, $value) + { + $array = $this->get($key); + + if (is_null($array)) { + $array = []; + } + + $array[] = $value; + + $this->set($key, $array); + } + + /** + * 启动session + * @access public + * @return void + */ + public function start() + { + session_start(); + + $this->init = true; + } + + /** + * 销毁session + * @access public + * @return void + */ + public function destroy() + { + if (!empty($_SESSION)) { + $_SESSION = []; + } + + session_unset(); + session_destroy(); + + $this->init = null; + $this->lockDriver = null; + } + + /** + * 重新生成session_id + * @access public + * @param bool $delete 是否删除关联会话文件 + * @return void + */ + public function regenerate($delete = false) + { + session_regenerate_id($delete); + } + + /** + * 暂停session + * @access public + * @return void + */ + public function pause() + { + // 暂停session + session_write_close(); + $this->init = false; + } +} diff --git a/vendor/topthink/framework/library/think/Template.php b/vendor/topthink/framework/library/think/Template.php new file mode 100644 index 0000000..2855cbc --- /dev/null +++ b/vendor/topthink/framework/library/think/Template.php @@ -0,0 +1,1318 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\TemplateNotFoundException; + +/** + * ThinkPHP分离出来的模板引擎 + * 支持XML标签和普通标签的模板解析 + * 编译型模板引擎 支持动态缓存 + */ +class Template +{ + protected $app; + /** + * 模板变量 + * @var array + */ + protected $data = []; + + /** + * 模板配置参数 + * @var array + */ + protected $config = [ + 'view_path' => '', // 模板路径 + 'view_base' => '', + 'view_suffix' => 'html', // 默认模板文件后缀 + 'view_depr' => DIRECTORY_SEPARATOR, + 'cache_suffix' => 'php', // 默认模板缓存后缀 + 'tpl_deny_func_list' => 'echo,exit', // 模板引擎禁用函数 + 'tpl_deny_php' => false, // 默认模板引擎是否禁用PHP原生代码 + 'tpl_begin' => '{', // 模板引擎普通标签开始标记 + 'tpl_end' => '}', // 模板引擎普通标签结束标记 + 'strip_space' => false, // 是否去除模板文件里面的html空格与换行 + 'tpl_cache' => true, // 是否开启模板编译缓存,设为false则每次都会重新编译 + 'compile_type' => 'file', // 模板编译类型 + 'cache_prefix' => '', // 模板缓存前缀标识,可以动态改变 + 'cache_time' => 0, // 模板缓存有效期 0 为永久,(以数字为值,单位:秒) + 'layout_on' => false, // 布局模板开关 + 'layout_name' => 'layout', // 布局模板入口文件 + 'layout_item' => '{__CONTENT__}', // 布局模板的内容替换标识 + 'taglib_begin' => '{', // 标签库标签开始标记 + 'taglib_end' => '}', // 标签库标签结束标记 + 'taglib_load' => true, // 是否使用内置标签库之外的其它标签库,默认自动检测 + 'taglib_build_in' => 'cx', // 内置标签库名称(标签使用不必指定标签库名称),以逗号分隔 注意解析顺序 + 'taglib_pre_load' => '', // 需要额外加载的标签库(须指定标签库名称),多个以逗号分隔 + 'display_cache' => false, // 模板渲染缓存 + 'cache_id' => '', // 模板缓存ID + 'tpl_replace_string' => [], + 'tpl_var_identify' => 'array', // .语法变量识别,array|object|'', 为空时自动识别 + 'default_filter' => 'htmlentities', // 默认过滤方法 用于普通标签输出 + ]; + + /** + * 保留内容信息 + * @var array + */ + private $literal = []; + + /** + * 模板包含信息 + * @var array + */ + private $includeFile = []; + + /** + * 模板存储对象 + * @var object + */ + protected $storage; + + /** + * 架构函数 + * @access public + * @param array $config + */ + public function __construct(App $app, array $config = []) + { + $this->app = $app; + $this->config['cache_path'] = $app->getRuntimePath() . 'temp/'; + $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'; + + $this->storage = Loader::factory($type, '\\think\\template\\driver\\', null); + } + + public static function __make(Config $config) + { + return new static($config->pull('template')); + } + + /** + * 模板变量赋值 + * @access public + * @param mixed $name + * @param mixed $value + * @return void + */ + public function assign($name, $value = '') + { + if (is_array($name)) { + $this->data = array_merge($this->data, $name); + } else { + $this->data[$name] = $value; + } + } + + /** + * 模板引擎参数赋值 + * @access public + * @param mixed $name + * @param mixed $value + */ + public function __set($name, $value) + { + $this->config[$name] = $value; + } + + /** + * 模板引擎配置项 + * @access public + * @param array|string $config + * @return void|array + */ + public function config($config) + { + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } elseif (isset($this->config[$config])) { + return $this->config[$config]; + } + } + + /** + * 模板变量获取 + * @access public + * @param string $name 变量名 + * @return mixed + */ + public function get($name = '') + { + if ('' == $name) { + return $this->data; + } + + $data = $this->data; + + foreach (explode('.', $name) as $key => $val) { + if (isset($data[$val])) { + $data = $data[$val]; + } else { + $data = null; + break; + } + } + + return $data; + } + + /** + * 渲染模板文件 + * @access public + * @param string $template 模板文件 + * @param array $vars 模板变量 + * @param array $config 模板参数 + * @return void + */ + public function fetch($template, $vars = [], $config = []) + { + if ($vars) { + $this->data = $vars; + } + + if ($config) { + $this->config($config); + } + + $cache = $this->app['cache']; + + if (!empty($this->config['cache_id']) && $this->config['display_cache']) { + // 读取渲染缓存 + $cacheContent = $cache->get($this->config['cache_id']); + + if (false !== $cacheContent) { + echo $cacheContent; + return; + } + } + + $template = $this->parseTemplateFile($template); + + if ($template) { + $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($this->config['layout_on'] . $this->config['layout_name'] . $template) . '.' . ltrim($this->config['cache_suffix'], '.'); + + if (!$this->checkCache($cacheFile)) { + // 缓存无效 重新模板编译 + $content = file_get_contents($template); + $this->compiler($content, $cacheFile); + } + + // 页面缓存 + ob_start(); + ob_implicit_flush(0); + + // 读取编译存储 + $this->storage->read($cacheFile, $this->data); + + // 获取并清空缓存 + $content = ob_get_clean(); + + if (!empty($this->config['cache_id']) && $this->config['display_cache']) { + // 缓存页面输出 + $cache->set($this->config['cache_id'], $content, $this->config['cache_time']); + } + + echo $content; + } + } + + /** + * 渲染模板内容 + * @access public + * @param string $content 模板内容 + * @param array $vars 模板变量 + * @param array $config 模板参数 + * @return void + */ + public function display($content, $vars = [], $config = []) + { + if ($vars) { + $this->data = $vars; + } + + if ($config) { + $this->config($config); + } + + $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($content) . '.' . ltrim($this->config['cache_suffix'], '.'); + + if (!$this->checkCache($cacheFile)) { + // 缓存无效 模板编译 + $this->compiler($content, $cacheFile); + } + + // 读取编译存储 + $this->storage->read($cacheFile, $this->data); + } + + /** + * 设置布局 + * @access public + * @param mixed $name 布局模板名称 false 则关闭布局 + * @param string $replace 布局模板内容替换标识 + * @return object + */ + public function layout($name, $replace = '') + { + if (false === $name) { + // 关闭布局 + $this->config['layout_on'] = false; + } else { + // 开启布局 + $this->config['layout_on'] = true; + + // 名称必须为字符串 + if (is_string($name)) { + $this->config['layout_name'] = $name; + } + + if (!empty($replace)) { + $this->config['layout_item'] = $replace; + } + } + + return $this; + } + + /** + * 检查编译缓存是否有效 + * 如果无效则需要重新编译 + * @access private + * @param string $cacheFile 缓存文件名 + * @return boolean + */ + private function checkCache($cacheFile) + { + if (!$this->config['tpl_cache'] || !is_file($cacheFile) || !$handle = @fopen($cacheFile, "r")) { + return false; + } + + // 读取第一行 + preg_match('/\/\*(.+?)\*\//', fgets($handle), $matches); + + if (!isset($matches[1])) { + return false; + } + + $includeFile = unserialize($matches[1]); + + if (!is_array($includeFile)) { + return false; + } + + // 检查模板文件是否有更新 + foreach ($includeFile as $path => $time) { + if (is_file($path) && filemtime($path) > $time) { + // 模板文件如果有更新则缓存需要更新 + return false; + } + } + + // 检查编译存储是否有效 + return $this->storage->check($cacheFile, $this->config['cache_time']); + } + + /** + * 检查编译缓存是否存在 + * @access public + * @param string $cacheId 缓存的id + * @return boolean + */ + public function isCache($cacheId) + { + if ($cacheId && $this->config['display_cache']) { + // 缓存页面输出 + return $this->app['cache']->has($cacheId); + } + + return false; + } + + /** + * 编译模板文件内容 + * @access private + * @param string $content 模板内容 + * @param string $cacheFile 缓存文件名 + * @return void + */ + private function compiler(&$content, $cacheFile) + { + // 判断是否启用布局 + if ($this->config['layout_on']) { + if (false !== strpos($content, '{__NOLAYOUT__}')) { + // 可以单独定义不使用布局 + $content = str_replace('{__NOLAYOUT__}', '', $content); + } else { + // 读取布局模板 + $layoutFile = $this->parseTemplateFile($this->config['layout_name']); + + if ($layoutFile) { + // 替换布局的主体内容 + $content = str_replace($this->config['layout_item'], $content, file_get_contents($layoutFile)); + } + } + } else { + $content = str_replace('{__NOLAYOUT__}', '', $content); + } + + // 模板解析 + $this->parse($content); + + if ($this->config['strip_space']) { + /* 去除html空格与换行 */ + $find = ['~>\s+<~', '~>(\s+\n|\r)~']; + $replace = ['><', '>']; + $content = preg_replace($find, $replace, $content); + } + + // 优化生成的php代码 + $content = preg_replace('/\?>\s*<\?php\s(?!echo\b|\bend)/s', '', $content); + + // 模板过滤输出 + $replace = $this->config['tpl_replace_string']; + $content = str_replace(array_keys($replace), array_values($replace), $content); + + // 添加安全代码及模板引用记录 + $content = 'includeFile) . '*/ ?>' . "\n" . $content; + // 编译存储 + $this->storage->write($cacheFile, $content); + + $this->includeFile = []; + } + + /** + * 模板解析入口 + * 支持普通标签和TagLib解析 支持自定义标签库 + * @access public + * @param string $content 要解析的模板内容 + * @return void + */ + public function parse(&$content) + { + // 内容为空不解析 + if (empty($content)) { + return; + } + + // 替换literal标签内容 + $this->parseLiteral($content); + + // 解析继承 + $this->parseExtend($content); + + // 解析布局 + $this->parseLayout($content); + + // 检查include语法 + $this->parseInclude($content); + + // 替换包含文件中literal标签内容 + $this->parseLiteral($content); + + // 检查PHP语法 + $this->parsePhp($content); + + // 获取需要引入的标签库列表 + // 标签库只需要定义一次,允许引入多个一次 + // 一般放在文件的最前面 + // 格式: + // 当TAGLIB_LOAD配置为true时才会进行检测 + if ($this->config['taglib_load']) { + $tagLibs = $this->getIncludeTagLib($content); + + if (!empty($tagLibs)) { + // 对导入的TagLib进行解析 + foreach ($tagLibs as $tagLibName) { + $this->parseTagLib($tagLibName, $content); + } + } + } + + // 预先加载的标签库 无需在每个模板中使用taglib标签加载 但必须使用标签库XML前缀 + if ($this->config['taglib_pre_load']) { + $tagLibs = explode(',', $this->config['taglib_pre_load']); + + foreach ($tagLibs as $tag) { + $this->parseTagLib($tag, $content); + } + } + + // 内置标签库 无需使用taglib标签导入就可以使用 并且不需使用标签库XML前缀 + $tagLibs = explode(',', $this->config['taglib_build_in']); + + foreach ($tagLibs as $tag) { + $this->parseTagLib($tag, $content, true); + } + + // 解析普通模板标签 {$tagName} + $this->parseTag($content); + + // 还原被替换的Literal标签 + $this->parseLiteral($content, true); + } + + /** + * 检查PHP语法 + * @access private + * @param string $content 要解析的模板内容 + * @return void + * @throws \think\Exception + */ + private function parsePhp(&$content) + { + // 短标签的情况要将' . "\n", $content); + + // PHP语法检查 + if ($this->config['tpl_deny_php'] && false !== strpos($content, 'getRegex('layout'), $content, $matches)) { + // 替换Layout标签 + $content = str_replace($matches[0], '', $content); + // 解析Layout标签 + $array = $this->parseAttr($matches[0]); + + if (!$this->config['layout_on'] || $this->config['layout_name'] != $array['name']) { + // 读取布局模板 + $layoutFile = $this->parseTemplateFile($array['name']); + + if ($layoutFile) { + $replace = isset($array['replace']) ? $array['replace'] : $this->config['layout_item']; + // 替换布局的主体内容 + $content = str_replace($replace, $content, file_get_contents($layoutFile)); + } + } + } else { + $content = str_replace('{__NOLAYOUT__}', '', $content); + } + } + + /** + * 解析模板中的include标签 + * @access private + * @param string $content 要解析的模板内容 + * @return void + */ + private function parseInclude(&$content) + { + $regex = $this->getRegex('include'); + $func = function ($template) use (&$func, &$regex, &$content) { + if (preg_match_all($regex, $template, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $array = $this->parseAttr($match[0]); + $file = $array['file']; + unset($array['file']); + + // 分析模板文件名并读取内容 + $parseStr = $this->parseTemplateName($file); + + foreach ($array as $k => $v) { + // 以$开头字符串转换成模板变量 + if (0 === strpos($v, '$')) { + $v = $this->get(substr($v, 1)); + } + + $parseStr = str_replace('[' . $k . ']', $v, $parseStr); + } + + $content = str_replace($match[0], $parseStr, $content); + // 再次对包含文件进行模板分析 + $func($parseStr); + } + unset($matches); + } + }; + + // 替换模板中的include标签 + $func($content); + } + + /** + * 解析模板中的extend标签 + * @access private + * @param string $content 要解析的模板内容 + * @return void + */ + private function parseExtend(&$content) + { + $regex = $this->getRegex('extend'); + $array = $blocks = $baseBlocks = []; + $extend = ''; + + $func = function ($template) use (&$func, &$regex, &$array, &$extend, &$blocks, &$baseBlocks) { + if (preg_match($regex, $template, $matches)) { + if (!isset($array[$matches['name']])) { + $array[$matches['name']] = 1; + // 读取继承模板 + $extend = $this->parseTemplateName($matches['name']); + + // 递归检查继承 + $func($extend); + + // 取得block标签内容 + $blocks = array_merge($blocks, $this->parseBlock($template)); + + return; + } + } else { + // 取得顶层模板block标签内容 + $baseBlocks = $this->parseBlock($template, true); + + if (empty($extend)) { + // 无extend标签但有block标签的情况 + $extend = $template; + } + } + }; + + $func($content); + + if (!empty($extend)) { + if ($baseBlocks) { + $children = []; + foreach ($baseBlocks as $name => $val) { + $replace = $val['content']; + + if (!empty($children[$name])) { + // 如果包含有子block标签 + foreach ($children[$name] as $key) { + $replace = str_replace($baseBlocks[$key]['begin'] . $baseBlocks[$key]['content'] . $baseBlocks[$key]['end'], $blocks[$key]['content'], $replace); + } + } + + if (isset($blocks[$name])) { + // 带有{__block__}表示与所继承模板的相应标签合并,而不是覆盖 + $replace = str_replace(['{__BLOCK__}', '{__block__}'], $replace, $blocks[$name]['content']); + + if (!empty($val['parent'])) { + // 如果不是最顶层的block标签 + $parent = $val['parent']; + + if (isset($blocks[$parent])) { + $blocks[$parent]['content'] = str_replace($blocks[$name]['begin'] . $blocks[$name]['content'] . $blocks[$name]['end'], $replace, $blocks[$parent]['content']); + } + + $blocks[$name]['content'] = $replace; + $children[$parent][] = $name; + + continue; + } + } elseif (!empty($val['parent'])) { + // 如果子标签没有被继承则用原值 + $children[$val['parent']][] = $name; + $blocks[$name] = $val; + } + + if (!$val['parent']) { + // 替换模板中的顶级block标签 + $extend = str_replace($val['begin'] . $val['content'] . $val['end'], $replace, $extend); + } + } + } + + $content = $extend; + unset($blocks, $baseBlocks); + } + } + + /** + * 替换页面中的literal标签 + * @access private + * @param string $content 模板内容 + * @param boolean $restore 是否为还原 + * @return void + */ + private function parseLiteral(&$content, $restore = false) + { + $regex = $this->getRegex($restore ? 'restoreliteral' : 'literal'); + + if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) { + if (!$restore) { + $count = count($this->literal); + + // 替换literal标签 + foreach ($matches as $match) { + $this->literal[] = substr($match[0], strlen($match[1]), -strlen($match[2])); + $content = str_replace($match[0], "", $content); + $count++; + } + } else { + // 还原literal标签 + foreach ($matches as $match) { + $content = str_replace($match[0], $this->literal[$match[1]], $content); + } + + // 清空literal记录 + $this->literal = []; + } + + unset($matches); + } + } + + /** + * 获取模板中的block标签 + * @access private + * @param string $content 模板内容 + * @param boolean $sort 是否排序 + * @return array + */ + private function parseBlock(&$content, $sort = false) + { + $regex = $this->getRegex('block'); + $result = []; + + if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { + $right = $keys = []; + + foreach ($matches as $match) { + if (empty($match['name'][0])) { + if (count($right) > 0) { + $tag = array_pop($right); + $start = $tag['offset'] + strlen($tag['tag']); + $length = $match[0][1] - $start; + + $result[$tag['name']] = [ + 'begin' => $tag['tag'], + 'content' => substr($content, $start, $length), + 'end' => $match[0][0], + 'parent' => count($right) ? end($right)['name'] : '', + ]; + + $keys[$tag['name']] = $match[0][1]; + } + } else { + // 标签头压入栈 + $right[] = [ + 'name' => $match[2][0], + 'offset' => $match[0][1], + 'tag' => $match[0][0], + ]; + } + } + + unset($right, $matches); + + if ($sort) { + // 按block标签结束符在模板中的位置排序 + array_multisort($keys, $result); + } + } + + return $result; + } + + /** + * 搜索模板页面中包含的TagLib库 + * 并返回列表 + * @access private + * @param string $content 模板内容 + * @return array|null + */ + private function getIncludeTagLib(&$content) + { + // 搜索是否有TagLib标签 + if (preg_match($this->getRegex('taglib'), $content, $matches)) { + // 替换TagLib标签 + $content = str_replace($matches[0], '', $content); + + return explode(',', $matches['name']); + } + } + + /** + * TagLib库解析 + * @access public + * @param string $tagLib 要解析的标签库 + * @param string $content 要解析的模板内容 + * @param boolean $hide 是否隐藏标签库前缀 + * @return void + */ + public function parseTagLib($tagLib, &$content, $hide = false) + { + if (false !== strpos($tagLib, '\\')) { + // 支持指定标签库的命名空间 + $className = $tagLib; + $tagLib = substr($tagLib, strrpos($tagLib, '\\') + 1); + } else { + $className = '\\think\\template\\taglib\\' . ucwords($tagLib); + } + + $tLib = new $className($this); + + $tLib->parseTag($content, $hide ? '' : $tagLib); + } + + /** + * 分析标签属性 + * @access public + * @param string $str 属性字符串 + * @param string $name 不为空时返回指定的属性名 + * @return array + */ + public function parseAttr($str, $name = null) + { + $regex = '/\s+(?>(?P[\w-]+)\s*)=(?>\s*)([\"\'])(?P(?:(?!\\2).)*)\\2/is'; + $array = []; + + if (preg_match_all($regex, $str, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $array[$match['name']] = $match['value']; + } + unset($matches); + } + + if (!empty($name) && isset($array[$name])) { + return $array[$name]; + } + + return $array; + } + + /** + * 模板标签解析 + * 格式: {TagName:args [|content] } + * @access private + * @param string $content 要解析的模板内容 + * @return void + */ + private function parseTag(&$content) + { + $regex = $this->getRegex('tag'); + + if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $str = stripslashes($match[1]); + $flag = substr($str, 0, 1); + + switch ($flag) { + case '$': + // 解析模板变量 格式 {$varName} + // 是否带有?号 + if (false !== $pos = strpos($str, '?')) { + $array = preg_split('/([!=]={1,2}|(?<]={0,1})/', substr($str, 0, $pos), 2, PREG_SPLIT_DELIM_CAPTURE); + $name = $array[0]; + + $this->parseVar($name); + //$this->parseVarFunction($name); + + $str = trim(substr($str, $pos + 1)); + $this->parseVar($str); + $first = substr($str, 0, 1); + + if (strpos($name, ')')) { + // $name为对象或是自动识别,或者含有函数 + if (isset($array[1])) { + $this->parseVar($array[2]); + $name .= $array[1] . $array[2]; + } + + switch ($first) { + case '?': + $this->parseVarFunction($name); + $str = ''; + break; + case '=': + $str = ''; + break; + default: + $str = ''; + } + } else { + if (isset($array[1])) { + $express = true; + $this->parseVar($array[2]); + $express = $name . $array[1] . $array[2]; + } else { + $express = false; + } + + if (in_array($first, ['?', '=', ':'])) { + $str = trim(substr($str, 1)); + if ('$' == substr($str, 0, 1)) { + $str = $this->parseVarFunction($str); + } + } + + // $name为数组 + switch ($first) { + case '?': + // {$varname??'xxx'} $varname有定义则输出$varname,否则输出xxx + $str = 'parseVarFunction($name) . ' : ' . $str . '; ?>'; + break; + case '=': + // {$varname?='xxx'} $varname为真时才输出xxx + $str = ''; + break; + case ':': + // {$varname?:'xxx'} $varname为真时输出$varname,否则输出xxx + $str = 'parseVarFunction($name) . ' : ' . $str . '; ?>'; + break; + default: + if (strpos($str, ':')) { + // {$varname ? 'a' : 'b'} $varname为真时输出a,否则输出b + $array = explode(':', $str, 2); + + $array[0] = '$' == substr(trim($array[0]), 0, 1) ? $this->parseVarFunction($array[0]) : $array[0]; + $array[1] = '$' == substr(trim($array[1]), 0, 1) ? $this->parseVarFunction($array[1]) : $array[1]; + + $str = implode(' : ', $array); + } + $str = ''; + } + } + } else { + $this->parseVar($str); + $this->parseVarFunction($str); + $str = ''; + } + break; + case ':': + // 输出某个函数的结果 + $str = substr($str, 1); + $this->parseVar($str); + $str = ''; + break; + case '~': + // 执行某个函数 + $str = substr($str, 1); + $this->parseVar($str); + $str = ''; + break; + case '-': + case '+': + // 输出计算 + $this->parseVar($str); + $str = ''; + break; + case '/': + // 注释标签 + $flag2 = substr($str, 1, 1); + if ('/' == $flag2 || ('*' == $flag2 && substr(rtrim($str), -2) == '*/')) { + $str = ''; + } + break; + default: + // 未识别的标签直接返回 + $str = $this->config['tpl_begin'] . $str . $this->config['tpl_end']; + break; + } + + $content = str_replace($match[0], $str, $content); + } + + unset($matches); + } + } + + /** + * 模板变量解析,支持使用函数 + * 格式: {$varname|function1|function2=arg1,arg2} + * @access public + * @param string $varStr 变量数据 + * @return void + */ + public function parseVar(&$varStr) + { + $varStr = trim($varStr); + + if (preg_match_all('/\$[a-zA-Z_](?>\w*)(?:[:\.][0-9a-zA-Z_](?>\w*))+/', $varStr, $matches, PREG_OFFSET_CAPTURE)) { + static $_varParseList = []; + + while ($matches[0]) { + $match = array_pop($matches[0]); + + //如果已经解析过该变量字串,则直接返回变量值 + if (isset($_varParseList[$match[0]])) { + $parseStr = $_varParseList[$match[0]]; + } else { + if (strpos($match[0], '.')) { + $vars = explode('.', $match[0]); + $first = array_shift($vars); + + if ('$Think' == $first) { + // 所有以Think.打头的以特殊变量对待 无需模板赋值就可以输出 + $parseStr = $this->parseThinkVar($vars); + } elseif ('$Request' == $first) { + // 获取Request请求对象参数 + $method = array_shift($vars); + if (!empty($vars)) { + $params = implode('.', $vars); + if ('true' != $params) { + $params = '\'' . $params . '\''; + } + } else { + $params = ''; + } + + $parseStr = 'app(\'request\')->' . $method . '(' . $params . ')'; + } else { + switch ($this->config['tpl_var_identify']) { + case 'array': // 识别为数组 + $parseStr = $first . '[\'' . implode('\'][\'', $vars) . '\']'; + break; + case 'obj': // 识别为对象 + $parseStr = $first . '->' . implode('->', $vars); + break; + default: // 自动判断数组或对象 + $parseStr = '(is_array(' . $first . ')?' . $first . '[\'' . implode('\'][\'', $vars) . '\']:' . $first . '->' . implode('->', $vars) . ')'; + } + } + } else { + $parseStr = str_replace(':', '->', $match[0]); + } + + $_varParseList[$match[0]] = $parseStr; + } + + $varStr = substr_replace($varStr, $parseStr, $match[1], strlen($match[0])); + } + unset($matches); + } + } + + /** + * 对模板中使用了函数的变量进行解析 + * 格式 {$varname|function1|function2=arg1,arg2} + * @access public + * @param string $varStr 变量字符串 + * @param bool $autoescape 自动转义 + * @return void + */ + public function parseVarFunction(&$varStr, $autoescape = true) + { + if (!$autoescape && false === strpos($varStr, '|')) { + return $varStr; + } elseif ($autoescape && !preg_match('/\|(\s)?raw(\||\s)?/i', $varStr)) { + $varStr .= '|' . $this->config['default_filter']; + } + + static $_varFunctionList = []; + + $_key = md5($varStr); + + //如果已经解析过该变量字串,则直接返回变量值 + if (isset($_varFunctionList[$_key])) { + $varStr = $_varFunctionList[$_key]; + } else { + $varArray = explode('|', $varStr); + + // 取得变量名称 + $name = trim(array_shift($varArray)); + + // 对变量使用函数 + $length = count($varArray); + + // 取得模板禁止使用函数列表 + $template_deny_funs = explode(',', $this->config['tpl_deny_func_list']); + + for ($i = 0; $i < $length; $i++) { + $args = explode('=', $varArray[$i], 2); + + // 模板函数过滤 + $fun = trim($args[0]); + if (in_array($fun, $template_deny_funs)) { + continue; + } + + switch (strtolower($fun)) { + case 'raw': + break; + case 'date': + $name = 'date(' . $args[1] . ',!is_numeric(' . $name . ')? strtotime(' . $name . ') : ' . $name . ')'; + break; + case 'first': + $name = 'current(' . $name . ')'; + break; + case 'last': + $name = 'end(' . $name . ')'; + break; + case 'upper': + $name = 'strtoupper(' . $name . ')'; + break; + case 'lower': + $name = 'strtolower(' . $name . ')'; + break; + case 'format': + $name = 'sprintf(' . $args[1] . ',' . $name . ')'; + break; + case 'default': // 特殊模板函数 + if (false === strpos($name, '(')) { + $name = '(isset(' . $name . ') && (' . $name . ' !== \'\')?' . $name . ':' . $args[1] . ')'; + } else { + $name = '(' . $name . ' ?: ' . $args[1] . ')'; + } + break; + default: // 通用模板函数 + if (isset($args[1])) { + if (strstr($args[1], '###')) { + $args[1] = str_replace('###', $name, $args[1]); + $name = "$fun($args[1])"; + } else { + $name = "$fun($name,$args[1])"; + } + } else { + if (!empty($args[0])) { + $name = "$fun($name)"; + } + } + } + } + + $_varFunctionList[$_key] = $name; + $varStr = $name; + } + return $varStr; + } + + /** + * 特殊模板变量解析 + * 格式 以 $Think. 打头的变量属于特殊模板变量 + * @access public + * @param array $vars 变量数组 + * @return string + */ + public function parseThinkVar($vars) + { + $type = strtoupper(trim(array_shift($vars))); + $param = implode('.', $vars); + + if ($vars) { + switch ($type) { + case 'SERVER': + $parseStr = 'app(\'request\')->server(\'' . $param . '\')'; + break; + case 'GET': + $parseStr = 'app(\'request\')->get(\'' . $param . '\')'; + break; + case 'POST': + $parseStr = 'app(\'request\')->post(\'' . $param . '\')'; + break; + case 'COOKIE': + $parseStr = 'app(\'cookie\')->get(\'' . $param . '\')'; + break; + case 'SESSION': + $parseStr = 'app(\'session\')->get(\'' . $param . '\')'; + break; + case 'ENV': + $parseStr = 'app(\'request\')->env(\'' . $param . '\')'; + break; + case 'REQUEST': + $parseStr = 'app(\'request\')->request(\'' . $param . '\')'; + break; + case 'CONST': + $parseStr = strtoupper($param); + break; + case 'LANG': + $parseStr = 'app(\'lang\')->get(\'' . $param . '\')'; + break; + case 'CONFIG': + $parseStr = 'app(\'config\')->get(\'' . $param . '\')'; + break; + default: + $parseStr = '\'\''; + break; + } + } else { + switch ($type) { + case 'NOW': + $parseStr = "date('Y-m-d g:i a',time())"; + break; + case 'VERSION': + $parseStr = 'app()->version()'; + break; + case 'LDELIM': + $parseStr = '\'' . ltrim($this->config['tpl_begin'], '\\') . '\''; + break; + case 'RDELIM': + $parseStr = '\'' . ltrim($this->config['tpl_end'], '\\') . '\''; + break; + default: + if (defined($type)) { + $parseStr = $type; + } else { + $parseStr = ''; + } + } + } + + return $parseStr; + } + + /** + * 分析加载的模板文件并读取内容 支持多个模板文件读取 + * @access private + * @param string $templateName 模板文件名 + * @return string + */ + private function parseTemplateName($templateName) + { + $array = explode(',', $templateName); + $parseStr = ''; + + foreach ($array as $templateName) { + if (empty($templateName)) { + continue; + } + + if (0 === strpos($templateName, '$')) { + //支持加载变量文件名 + $templateName = $this->get(substr($templateName, 1)); + } + + $template = $this->parseTemplateFile($templateName); + + if ($template) { + // 获取模板文件内容 + $parseStr .= file_get_contents($template); + } + } + + return $parseStr; + } + + /** + * 解析模板文件名 + * @access private + * @param string $template 文件名 + * @return string|false + */ + private function parseTemplateFile($template) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + if (strpos($template, '@')) { + list($module, $template) = explode('@', $template); + } + + if (0 !== strpos($template, '/')) { + $template = str_replace(['/', ':'], $this->config['view_depr'], $template); + } else { + $template = str_replace(['/', ':'], $this->config['view_depr'], substr($template, 1)); + } + + if ($this->config['view_base']) { + $module = isset($module) ? $module : $this->app['request']->module(); + $path = $this->config['view_base'] . ($module ? $module . DIRECTORY_SEPARATOR : ''); + } else { + $path = isset($module) ? $this->app->getAppPath() . $module . DIRECTORY_SEPARATOR . basename($this->config['view_path']) . DIRECTORY_SEPARATOR : $this->config['view_path']; + } + + $template = $path . $template . '.' . ltrim($this->config['view_suffix'], '.'); + } + + if (is_file($template)) { + // 记录模板文件的更新时间 + $this->includeFile[$template] = filemtime($template); + + return $template; + } + + throw new TemplateNotFoundException('template not exists:' . $template, $template); + } + + /** + * 按标签生成正则 + * @access private + * @param string $tagName 标签名 + * @return string + */ + private function getRegex($tagName) + { + $regex = ''; + if ('tag' == $tagName) { + $begin = $this->config['tpl_begin']; + $end = $this->config['tpl_end']; + + if (strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1) { + $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>[^' . $end . ']*))' . $end; + } else { + $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>(?:(?!' . $end . ').)*))' . $end; + } + } else { + $begin = $this->config['taglib_begin']; + $end = $this->config['taglib_end']; + $single = strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1 ? true : false; + + switch ($tagName) { + case 'block': + if ($single) { + $regex = $begin . '(?:' . $tagName . '\b\s+(?>(?:(?!name=).)*)\bname=([\'\"])(?P[\$\w\-\/\.]+)\\1(?>[^' . $end . ']*)|\/' . $tagName . ')' . $end; + } else { + $regex = $begin . '(?:' . $tagName . '\b\s+(?>(?:(?!name=).)*)\bname=([\'\"])(?P[\$\w\-\/\.]+)\\1(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end; + } + break; + case 'literal': + if ($single) { + $regex = '(' . $begin . $tagName . '\b(?>[^' . $end . ']*)' . $end . ')'; + $regex .= '(?:(?>[^' . $begin . ']*)(?>(?!' . $begin . '(?>' . $tagName . '\b[^' . $end . ']*|\/' . $tagName . ')' . $end . ')' . $begin . '[^' . $begin . ']*)*)'; + $regex .= '(' . $begin . '\/' . $tagName . $end . ')'; + } else { + $regex = '(' . $begin . $tagName . '\b(?>(?:(?!' . $end . ').)*)' . $end . ')'; + $regex .= '(?:(?>(?:(?!' . $begin . ').)*)(?>(?!' . $begin . '(?>' . $tagName . '\b(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end . ')' . $begin . '(?>(?:(?!' . $begin . ').)*))*)'; + $regex .= '(' . $begin . '\/' . $tagName . $end . ')'; + } + break; + case 'restoreliteral': + $regex = ''; + break; + case 'include': + $name = 'file'; + case 'taglib': + case 'layout': + case 'extend': + if (empty($name)) { + $name = 'name'; + } + if ($single) { + $regex = $begin . $tagName . '\b\s+(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P[\$\w\-\/\.\:@,\\\\]+)\\1(?>[^' . $end . ']*)' . $end; + } else { + $regex = $begin . $tagName . '\b\s+(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P[\$\w\-\/\.\:@,\\\\]+)\\1(?>(?:(?!' . $end . ').)*)' . $end; + } + break; + } + } + + return '/' . $regex . '/is'; + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app'], $data['storage']); + + return $data; + } +} diff --git a/vendor/topthink/framework/library/think/Url.php b/vendor/topthink/framework/library/think/Url.php new file mode 100644 index 0000000..acd510a --- /dev/null +++ b/vendor/topthink/framework/library/think/Url.php @@ -0,0 +1,412 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Url +{ + /** + * 配置参数 + * @var array + */ + protected $config = []; + + /** + * ROOT地址 + * @var string + */ + protected $root; + + /** + * 绑定检查 + * @var bool + */ + protected $bindCheck; + + /** + * 应用对象 + * @var App + */ + protected $app; + + public function __construct(App $app, array $config = []) + { + $this->app = $app; + $this->config = $config; + + if (is_file($app->getRuntimePath() . 'route.php')) { + // 读取路由映射文件 + $app['route']->setName(include $app->getRuntimePath() . 'route.php'); + } + } + + /** + * 初始化 + * @access public + * @param array $config + * @return void + */ + public function init(array $config = []) + { + $this->config = array_merge($this->config, array_change_key_case($config)); + } + + public static function __make(App $app, Config $config) + { + return new static($app, $config->pull('app')); + } + + /** + * URL生成 支持路由反射 + * @access public + * @param string $url 路由地址 + * @param string|array $vars 参数(支持数组和字符串)a=val&b=val2... ['a'=>'val1', 'b'=>'val2'] + * @param string|bool $suffix 伪静态后缀,默认为true表示获取配置值 + * @param boolean|string $domain 是否显示域名 或者直接传入域名 + * @return string + */ + public function build($url = '', $vars = '', $suffix = true, $domain = false) + { + // 解析URL + if (0 === strpos($url, '[') && $pos = strpos($url, ']')) { + // [name] 表示使用路由命名标识生成URL + $name = substr($url, 1, $pos - 1); + $url = 'name' . substr($url, $pos + 1); + } + + if (false === strpos($url, '://') && 0 !== strpos($url, '/')) { + $info = parse_url($url); + $url = !empty($info['path']) ? $info['path'] : ''; + + if (isset($info['fragment'])) { + // 解析锚点 + $anchor = $info['fragment']; + + if (false !== strpos($anchor, '?')) { + // 解析参数 + list($anchor, $info['query']) = explode('?', $anchor, 2); + } + + if (false !== strpos($anchor, '@')) { + // 解析域名 + list($anchor, $domain) = explode('@', $anchor, 2); + } + } elseif (strpos($url, '@') && false === strpos($url, '\\')) { + // 解析域名 + list($url, $domain) = explode('@', $url, 2); + } + } + + // 解析参数 + if (is_string($vars)) { + // aaa=1&bbb=2 转换成数组 + parse_str($vars, $vars); + } + + if ($url) { + $checkName = isset($name) ? $name : $url . (isset($info['query']) ? '?' . $info['query'] : ''); + $checkDomain = $domain && is_string($domain) ? $domain : null; + + $rule = $this->app['route']->getName($checkName, $checkDomain); + + if (is_null($rule) && isset($info['query'])) { + $rule = $this->app['route']->getName($url); + // 解析地址里面参数 合并到vars + parse_str($info['query'], $params); + $vars = array_merge($params, $vars); + unset($info['query']); + } + } + + if (!empty($rule) && $match = $this->getRuleUrl($rule, $vars, $domain)) { + // 匹配路由命名标识 + $url = $match[0]; + + if ($domain) { + $domain = $match[1]; + } + + if (!is_null($match[2])) { + $suffix = $match[2]; + } + } elseif (!empty($rule) && isset($name)) { + throw new \InvalidArgumentException('route name not exists:' . $name); + } else { + // 检查别名路由 + $alias = $this->app['route']->getAlias(); + $matchAlias = false; + + if ($alias) { + // 别名路由解析 + foreach ($alias as $key => $item) { + $val = $item->getRoute(); + + if (0 === strpos($url, $val)) { + $url = $key . substr($url, strlen($val)); + $matchAlias = true; + break; + } + } + } + + if (!$matchAlias) { + // 路由标识不存在 直接解析 + $url = $this->parseUrl($url); + } + + // 检测URL绑定 + if (!$this->bindCheck) { + $bind = $this->app['route']->getBind($domain && is_string($domain) ? $domain : null); + + if ($bind && 0 === strpos($url, $bind)) { + $url = substr($url, strlen($bind) + 1); + } else { + $binds = $this->app['route']->getBind(true); + + foreach ($binds as $key => $val) { + if (is_string($val) && 0 === strpos($url, $val) && substr_count($val, '/') > 1) { + $url = substr($url, strlen($val) + 1); + $domain = $key; + break; + } + } + } + } + + if (isset($info['query'])) { + // 解析地址里面参数 合并到vars + parse_str($info['query'], $params); + $vars = array_merge($params, $vars); + } + } + + // 还原URL分隔符 + $depr = $this->config['pathinfo_depr']; + $url = str_replace('/', $depr, $url); + + // URL后缀 + if ('/' == substr($url, -1) || '' == $url) { + $suffix = ''; + } else { + $suffix = $this->parseSuffix($suffix); + } + + // 锚点 + $anchor = !empty($anchor) ? '#' . $anchor : ''; + + // 参数组装 + if (!empty($vars)) { + // 添加参数 + if ($this->config['url_common_param']) { + $vars = http_build_query($vars); + $url .= $suffix . '?' . $vars . $anchor; + } else { + $paramType = $this->config['url_param_type']; + + foreach ($vars as $var => $val) { + if ('' !== trim($val)) { + if ($paramType) { + $url .= $depr . urlencode($val); + } else { + $url .= $depr . $var . $depr . urlencode($val); + } + } + } + + $url .= $suffix . $anchor; + } + } else { + $url .= $suffix . $anchor; + } + + // 检测域名 + $domain = $this->parseDomain($url, $domain); + + // URL组装 + $url = $domain . rtrim($this->root ?: $this->app['request']->root(), '/') . '/' . ltrim($url, '/'); + + $this->bindCheck = false; + + return $url; + } + + // 直接解析URL地址 + protected function parseUrl($url) + { + $request = $this->app['request']; + + if (0 === strpos($url, '/')) { + // 直接作为路由地址解析 + $url = substr($url, 1); + } elseif (false !== strpos($url, '\\')) { + // 解析到类 + $url = ltrim(str_replace('\\', '/', $url), '/'); + } elseif (0 === strpos($url, '@')) { + // 解析到控制器 + $url = substr($url, 1); + } else { + // 解析到 模块/控制器/操作 + $module = $request->module(); + $module = $module ? $module . '/' : ''; + $controller = $request->controller(); + + if ('' == $url) { + $action = $request->action(); + } else { + $path = explode('/', $url); + $action = array_pop($path); + $controller = empty($path) ? $controller : array_pop($path); + $module = empty($path) ? $module : array_pop($path) . '/'; + } + + if ($this->config['url_convert']) { + $action = strtolower($action); + $controller = Loader::parseName($controller); + } + + $url = $module . $controller . '/' . $action; + } + + return $url; + } + + // 检测域名 + protected function parseDomain(&$url, $domain) + { + if (!$domain) { + return ''; + } + + $rootDomain = $this->app['request']->rootDomain(); + if (true === $domain) { + // 自动判断域名 + $domain = $this->config['app_host'] ?: $this->app['request']->host(); + + $domains = $this->app['route']->getDomains(); + + if ($domains) { + $route_domain = array_keys($domains); + foreach ($route_domain as $domain_prefix) { + if (0 === strpos($domain_prefix, '*.') && strpos($domain, ltrim($domain_prefix, '*.')) !== false) { + foreach ($domains as $key => $rule) { + $rule = is_array($rule) ? $rule[0] : $rule; + if (is_string($rule) && false === strpos($key, '*') && 0 === strpos($url, $rule)) { + $url = ltrim($url, $rule); + $domain = $key; + + // 生成对应子域名 + if (!empty($rootDomain)) { + $domain .= $rootDomain; + } + break; + } elseif (false !== strpos($key, '*')) { + if (!empty($rootDomain)) { + $domain .= $rootDomain; + } + + break; + } + } + } + } + } + } elseif (0 !== strpos($domain, $rootDomain) && false === strpos($domain, '.')) { + $domain .= '.' . $rootDomain; + } + + if (false !== strpos($domain, '://')) { + $scheme = ''; + } else { + $scheme = $this->app['request']->isSsl() || $this->config['is_https'] ? 'https://' : 'http://'; + + } + + return $scheme . $domain; + } + + // 解析URL后缀 + protected function parseSuffix($suffix) + { + if ($suffix) { + $suffix = true === $suffix ? $this->config['url_html_suffix'] : $suffix; + + if ($pos = strpos($suffix, '|')) { + $suffix = substr($suffix, 0, $pos); + } + } + + return (empty($suffix) || 0 === strpos($suffix, '.')) ? $suffix : '.' . $suffix; + } + + // 匹配路由地址 + public function getRuleUrl($rule, &$vars = [], $allowDomain = '') + { + $port = $this->app['request']->port(); + foreach ($rule as $item) { + list($url, $pattern, $domain, $suffix, $method) = $item; + + if (is_string($allowDomain) && $domain != $allowDomain) { + continue; + } + + if ($port && !in_array($port, [80, 443])) { + $domain .= ':' . $port; + } + + if (empty($pattern)) { + return [rtrim($url, '?/-'), $domain, $suffix]; + } + + $type = $this->config['url_common_param']; + $keys = []; + + foreach ($pattern as $key => $val) { + if (isset($vars[$key])) { + $url = str_replace(['[:' . $key . ']', '<' . $key . '?>', ':' . $key, '<' . $key . '>'], $type ? $vars[$key] : urlencode($vars[$key]), $url); + $keys[] = $key; + $url = str_replace(['/?', '-?'], ['/', '-'], $url); + $result = [rtrim($url, '?/-'), $domain, $suffix]; + } elseif (2 == $val) { + $url = str_replace(['/[:' . $key . ']', '[:' . $key . ']', '<' . $key . '?>'], '', $url); + $url = str_replace(['/?', '-?'], ['/', '-'], $url); + $result = [rtrim($url, '?/-'), $domain, $suffix]; + } else { + $result = null; + $keys = []; + break; + } + } + + $vars = array_diff_key($vars, array_flip($keys)); + + if (isset($result)) { + return $result; + } + } + + return false; + } + + // 指定当前生成URL地址的root + public function root($root) + { + $this->root = $root; + $this->app['request']->setRoot($root); + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app']); + + return $data; + } +} diff --git a/vendor/topthink/framework/library/think/Validate.php b/vendor/topthink/framework/library/think/Validate.php new file mode 100644 index 0000000..5fde7f3 --- /dev/null +++ b/vendor/topthink/framework/library/think/Validate.php @@ -0,0 +1,1556 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ClassNotFoundException; +use think\validate\ValidateRule; + +class Validate +{ + + /** + * 自定义验证类型 + * @var array + */ + protected static $type = []; + + /** + * 验证类型别名 + * @var array + */ + protected $alias = [ + '>' => 'gt', '>=' => 'egt', '<' => 'lt', '<=' => 'elt', '=' => 'eq', 'same' => 'eq', + ]; + + /** + * 当前验证规则 + * @var array + */ + protected $rule = []; + + /** + * 验证提示信息 + * @var array + */ + protected $message = []; + + /** + * 验证字段描述 + * @var array + */ + protected $field = []; + + /** + * 默认规则提示 + * @var array + */ + protected static $typeMsg = [ + 'require' => ':attribute require', + 'must' => ':attribute must', + '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', + 'mobile' => ':attribute not a valid mobile', + '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', + ]; + + /** + * 当前验证场景 + * @var array + */ + protected $currentScene = null; + + /** + * Filter_var 规则 + * @var array + */ + protected $filter = [ + 'email' => FILTER_VALIDATE_EMAIL, + 'ip' => [FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6], + 'integer' => FILTER_VALIDATE_INT, + 'url' => FILTER_VALIDATE_URL, + 'macAddr' => FILTER_VALIDATE_MAC, + 'float' => FILTER_VALIDATE_FLOAT, + ]; + + /** + * 内置正则验证规则 + * @var array + */ + protected $defaultRegex = [ + 'alphaDash' => '/^[A-Za-z0-9\-\_]+$/', + 'chs' => '/^[\x{4e00}-\x{9fa5}]+$/u', + 'chsAlpha' => '/^[\x{4e00}-\x{9fa5}a-zA-Z]+$/u', + 'chsAlphaNum' => '/^[\x{4e00}-\x{9fa5}a-zA-Z0-9]+$/u', + 'chsDash' => '/^[\x{4e00}-\x{9fa5}a-zA-Z0-9\_\-]+$/u', + 'mobile' => '/^1[3-9][0-9]\d{8}$/', + 'idCard' => '/(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$)/', + 'zip' => '/\d{6}/', + ]; + + /** + * 验证场景定义 + * @var array + */ + protected $scene = []; + + /** + * 验证失败错误信息 + * @var array + */ + protected $error = []; + + /** + * 是否批量验证 + * @var bool + */ + protected $batch = false; + + /** + * 场景需要验证的规则 + * @var array + */ + protected $only = []; + + /** + * 场景需要移除的验证规则 + * @var array + */ + protected $remove = []; + + /** + * 场景需要追加的验证规则 + * @var array + */ + protected $append = []; + + /** + * 验证正则定义 + * @var array + */ + protected $regex = []; + + /** + * 架构函数 + * @access public + * @param array $rules 验证规则 + * @param array $message 验证提示信息 + * @param array $field 验证字段描述信息 + */ + public function __construct(array $rules = [], array $message = [], array $field = []) + { + $this->rule = $rules + $this->rule; + $this->message = array_merge($this->message, $message); + $this->field = array_merge($this->field, $field); + } + + /** + * 创建一个验证器类 + * @access public + * @param array $rules 验证规则 + * @param array $message 验证提示信息 + * @param array $field 验证字段描述信息 + * @return Validate + */ + public static function make(array $rules = [], array $message = [], array $field = []) + { + return new self($rules, $message, $field); + } + + /** + * 添加字段验证规则 + * @access protected + * @param string|array $name 字段名称或者规则数组 + * @param mixed $rule 验证规则或者字段描述信息 + * @return $this + */ + public function rule($name, $rule = '') + { + if (is_array($name)) { + $this->rule = $name + $this->rule; + if (is_array($rule)) { + $this->field = array_merge($this->field, $rule); + } + } else { + $this->rule[$name] = $rule; + } + + return $this; + } + + /** + * 注册扩展验证(类型)规则 + * @access public + * @param string $type 验证规则类型 + * @param mixed $callback callback方法(或闭包) + * @return void + */ + public static function extend($type, $callback = null) + { + if (is_array($type)) { + self::$type = array_merge(self::$type, $type); + } else { + self::$type[$type] = $callback; + } + } + + /** + * 设置验证规则的默认提示信息 + * @access public + * @param string|array $type 验证规则类型名称或者数组 + * @param string $msg 验证提示信息 + * @return void + */ + public static function setTypeMsg($type, $msg = null) + { + if (is_array($type)) { + self::$typeMsg = array_merge(self::$typeMsg, $type); + } else { + self::$typeMsg[$type] = $msg; + } + } + + /** + * 设置提示信息 + * @access public + * @param string|array $name 字段名称 + * @param string $message 提示信息 + * @return Validate + */ + public function message($name, $message = '') + { + if (is_array($name)) { + $this->message = array_merge($this->message, $name); + } else { + $this->message[$name] = $message; + } + + return $this; + } + + /** + * 设置验证场景 + * @access public + * @param string $name 场景名 + * @return $this + */ + public function scene($name) + { + // 设置当前场景 + $this->currentScene = $name; + + return $this; + } + + /** + * 判断是否存在某个验证场景 + * @access public + * @param string $name 场景名 + * @return bool + */ + public function hasScene($name) + { + return isset($this->scene[$name]) || method_exists($this, 'scene' . $name); + } + + /** + * 设置批量验证 + * @access public + * @param bool $batch 是否批量验证 + * @return $this + */ + public function batch($batch = true) + { + $this->batch = $batch; + + return $this; + } + + /** + * 指定需要验证的字段列表 + * @access public + * @param array $fields 字段名 + * @return $this + */ + public function only($fields) + { + $this->only = $fields; + + return $this; + } + + /** + * 移除某个字段的验证规则 + * @access public + * @param string|array $field 字段名 + * @param mixed $rule 验证规则 null 移除所有规则 + * @return $this + */ + public function remove($field, $rule = null) + { + if (is_array($field)) { + foreach ($field as $key => $rule) { + if (is_int($key)) { + $this->remove($rule); + } else { + $this->remove($key, $rule); + } + } + } else { + if (is_string($rule)) { + $rule = explode('|', $rule); + } + + $this->remove[$field] = $rule; + } + + return $this; + } + + /** + * 追加某个字段的验证规则 + * @access public + * @param string|array $field 字段名 + * @param mixed $rule 验证规则 + * @return $this + */ + public function append($field, $rule = null) + { + if (is_array($field)) { + foreach ($field as $key => $rule) { + $this->append($key, $rule); + } + } else { + if (is_string($rule)) { + $rule = explode('|', $rule); + } + + $this->append[$field] = $rule; + } + + return $this; + } + + /** + * 数据自动验证 + * @access public + * @param array $data 数据 + * @param mixed $rules 验证规则 + * @param string $scene 验证场景 + * @return bool + */ + public function check($data, $rules = [], $scene = '') + { + $this->error = []; + + if (empty($rules)) { + // 读取验证规则 + $rules = $this->rule; + } + + // 获取场景定义 + $this->getScene($scene); + + foreach ($this->append as $key => $rule) { + if (!isset($rules[$key])) { + $rules[$key] = $rule; + unset($this->append[$key]); + } + } + + foreach ($rules as $key => $rule) { + // field => 'rule1|rule2...' field => ['rule1','rule2',...] + if (strpos($key, '|')) { + // 字段|描述 用于指定属性名称 + list($key, $title) = explode('|', $key); + } else { + $title = isset($this->field[$key]) ? $this->field[$key] : $key; + } + + // 场景检测 + if (!empty($this->only) && !in_array($key, $this->only)) { + continue; + } + + // 获取数据 支持多维数组 + $value = $this->getDataValue($data, $key); + + // 字段验证 + if ($rule instanceof \Closure) { + $result = call_user_func_array($rule, [$value, $data, $title, $this]); + } elseif ($rule instanceof ValidateRule) { + // 验证因子 + $result = $this->checkItem($key, $value, $rule->getRule(), $data, $rule->getTitle() ?: $title, $rule->getMsg()); + } else { + $result = $this->checkItem($key, $value, $rule, $data, $title); + } + + if (true !== $result) { + // 没有返回true 则表示验证失败 + if (!empty($this->batch)) { + // 批量验证 + if (is_array($result)) { + $this->error = array_merge($this->error, $result); + } else { + $this->error[$key] = $result; + } + } else { + $this->error = $result; + return false; + } + } + } + + return !empty($this->error) ? false : true; + } + + /** + * 根据验证规则验证数据 + * @access public + * @param mixed $value 字段值 + * @param mixed $rules 验证规则 + * @return bool + */ + public function checkRule($value, $rules) + { + if ($rules instanceof \Closure) { + return call_user_func_array($rules, [$value]); + } elseif ($rules instanceof ValidateRule) { + $rules = $rules->getRule(); + } 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 + * @param string $field 字段名 + * @param mixed $value 字段值 + * @param mixed $rules 验证规则 + * @param array $data 数据 + * @param string $title 字段描述 + * @param array $msg 提示信息 + * @return mixed + */ + protected function checkItem($field, $value, $rules, $data, $title = '', $msg = []) + { + if (isset($this->remove[$field]) && true === $this->remove[$field] && empty($this->append[$field])) { + // 字段已经移除 无需验证 + return true; + } + + // 支持多规则验证 require|in:a,b,c|... 或者 ['require','in'=>'a,b,c',...] + if (is_string($rules)) { + $rules = explode('|', $rules); + } + + if (isset($this->append[$field])) { + // 追加额外的验证规则 + $rules = array_unique(array_merge($rules, $this->append[$field]), SORT_REGULAR); + unset($this->append[$field]); + } + + $i = 0; + $result = true; + + foreach ($rules as $key => $rule) { + if ($rule instanceof \Closure) { + $result = call_user_func_array($rule, [$value, $data]); + $info = is_numeric($key) ? '' : $key; + } else { + // 判断验证类型 + list($type, $rule, $info) = $this->getValidateType($key, $rule); + + if (isset($this->append[$field]) && in_array($info, $this->append[$field])) { + + } elseif (array_key_exists($field, $this->remove) && (null === $this->remove[$field] || in_array($info, $this->remove[$field]))) { + // 规则已经移除 + $i++; + continue; + } + + // 验证类型 + if (isset(self::$type[$type])) { + $result = call_user_func_array(self::$type[$type], [$value, $rule, $data, $field, $title]); + } elseif ('must' == $info || 0 === strpos($info, 'require') || (!is_null($value) && '' !== $value)) { + // 验证数据 + $result = call_user_func_array([$this, $type], [$value, $rule, $data, $field, $title]); + } else { + $result = true; + } + } + + if (false === $result) { + // 验证失败 返回错误信息 + if (!empty($msg[$i])) { + $message = $msg[$i]; + if (is_string($message) && strpos($message, '{%') === 0) { + $message = facade\Lang::get(substr($message, 2, -1)); + } + } else { + $message = $this->getRuleMsg($field, $title, $info, $rule); + } + + return $message; + } elseif (true !== $result) { + // 返回自定义错误信息 + if (is_string($result) && false !== strpos($result, ':')) { + $result = str_replace(':attribute', $title, $result); + + if (strpos($result, ':rule') && is_scalar($rule)) { + $result = str_replace(':rule', (string) $rule, $result); + } + } + + return $result; + } + $i++; + } + + 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 public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @param string $field 字段名 + * @return bool + */ + public function confirm($value, $rule, $data = [], $field = '') + { + if ('' == $rule) { + if (strpos($field, '_confirm')) { + $rule = strstr($field, '_confirm', true); + } else { + $rule = $field . '_confirm'; + } + } + + return $this->getDataValue($data, $rule) === $value; + } + + /** + * 验证是否和某个字段的值是否不同 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function different($value, $rule, $data = []) + { + return $this->getDataValue($data, $rule) != $value; + } + + /** + * 验证是否大于等于某个值 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function egt($value, $rule, $data = []) + { + return $value >= $this->getDataValue($data, $rule); + } + + /** + * 验证是否大于某个值 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function gt($value, $rule, $data) + { + return $value > $this->getDataValue($data, $rule); + } + + /** + * 验证是否小于等于某个值 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function elt($value, $rule, $data = []) + { + return $value <= $this->getDataValue($data, $rule); + } + + /** + * 验证是否小于某个值 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function lt($value, $rule, $data = []) + { + return $value < $this->getDataValue($data, $rule); + } + + /** + * 验证是否等于某个值 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function eq($value, $rule) + { + return $value == $rule; + } + + /** + * 必须验证 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function must($value, $rule = null) + { + return !empty($value) || '0' == $value; + } + + /** + * 验证字段值是否为有效格式 + * @access public + * @param mixed $value 字段值 + * @param string $rule 验证规则 + * @param array $data 验证数据 + * @return bool + */ + public function is($value, $rule, $data = []) + { + switch (Loader::parseName($rule, 1, false)) { + case 'require': + // 必须 + $result = !empty($value) || '0' == $value; + break; + case 'accepted': + // 接受 + $result = in_array($value, ['1', 'on', 'yes']); + break; + case 'date': + // 是否是一个有效日期 + $result = false !== strtotime($value); + break; + case 'activeUrl': + // 是否为有效的网址 + $result = checkdnsrr($value); + break; + case 'boolean': + case 'bool': + // 是否为布尔值 + $result = in_array($value, [true, false, 0, 1, '0', '1'], true); + break; + case 'number': + $result = ctype_digit((string) $value); + break; + case 'alphaNum': + $result = ctype_alnum($value); + break; + case 'array': + // 是否为数组 + $result = is_array($value); + break; + case 'file': + $result = $value instanceof File; + break; + case 'image': + $result = $value instanceof File && in_array($this->getImageType($value->getRealPath()), [1, 2, 3, 6]); + break; + case 'token': + $result = $this->token($value, '__token__', $data); + break; + default: + if (isset(self::$type[$rule])) { + // 注册的验证规则 + $result = call_user_func_array(self::$type[$rule], [$value]); + } elseif (function_exists('ctype_' . $rule)) { + // ctype验证规则 + $ctypeFun = 'ctype_' . $rule; + $result = $ctypeFun($value); + } elseif (isset($this->filter[$rule])) { + // Filter_var验证规则 + $result = $this->filter($value, $this->filter[$rule]); + } else { + // 正则验证 + $result = $this->regex($value, $rule); + } + } + + return $result; + } + + // 判断图像类型 + protected function getImageType($image) + { + if (function_exists('exif_imagetype')) { + return exif_imagetype($image); + } + + try { + $info = getimagesize($image); + return $info ? $info[2] : false; + } catch (\Exception $e) { + return false; + } + } + + /** + * 验证是否为合格的域名或者IP 支持A,MX,NS,SOA,PTR,CNAME,AAAA,A6, SRV,NAPTR,TXT 或者 ANY类型 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function activeUrl($value, $rule = 'MX') + { + if (!in_array($rule, ['A', 'MX', 'NS', 'SOA', 'PTR', 'CNAME', 'AAAA', 'A6', 'SRV', 'NAPTR', 'TXT', 'ANY'])) { + $rule = 'MX'; + } + + return checkdnsrr($value, $rule); + } + + /** + * 验证是否有效IP + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 ipv4 ipv6 + * @return bool + */ + public function ip($value, $rule = 'ipv4') + { + if (!in_array($rule, ['ipv4', 'ipv6'])) { + $rule = 'ipv4'; + } + + return $this->filter($value, [FILTER_VALIDATE_IP, 'ipv6' == $rule ? FILTER_FLAG_IPV6 : FILTER_FLAG_IPV4]); + } + + /** + * 验证上传文件后缀 + * @access public + * @param mixed $file 上传文件 + * @param mixed $rule 验证规则 + * @return bool + */ + public function fileExt($file, $rule) + { + if (is_array($file)) { + foreach ($file as $item) { + if (!($item instanceof File) || !$item->checkExt($rule)) { + return false; + } + } + return true; + } elseif ($file instanceof File) { + return $file->checkExt($rule); + } + + return false; + } + + /** + * 验证上传文件类型 + * @access public + * @param mixed $file 上传文件 + * @param mixed $rule 验证规则 + * @return bool + */ + public function fileMime($file, $rule) + { + if (is_array($file)) { + foreach ($file as $item) { + if (!($item instanceof File) || !$item->checkMime($rule)) { + return false; + } + } + return true; + } elseif ($file instanceof File) { + return $file->checkMime($rule); + } + + return false; + } + + /** + * 验证上传文件大小 + * @access public + * @param mixed $file 上传文件 + * @param mixed $rule 验证规则 + * @return bool + */ + public function fileSize($file, $rule) + { + if (is_array($file)) { + foreach ($file as $item) { + if (!($item instanceof File) || !$item->checkSize($rule)) { + return false; + } + } + return true; + } elseif ($file instanceof File) { + return $file->checkSize($rule); + } + + return false; + } + + /** + * 验证图片的宽高及类型 + * @access public + * @param mixed $file 上传文件 + * @param mixed $rule 验证规则 + * @return bool + */ + public function image($file, $rule) + { + if (!($file instanceof File)) { + return false; + } + + if ($rule) { + $rule = explode(',', $rule); + + list($width, $height, $type) = getimagesize($file->getRealPath()); + + if (isset($rule[2])) { + $imageType = strtolower($rule[2]); + + if ('jpg' == $imageType) { + $imageType = 'jpeg'; + } + + if (image_type_to_extension($type, false) != $imageType) { + return false; + } + } + + list($w, $h) = $rule; + + return $w == $width && $h == $height; + } + + return in_array($this->getImageType($file->getRealPath()), [1, 2, 3, 6]); + } + + /** + * 验证请求类型 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function method($value, $rule) + { + $method = Container::get('request')->method(); + return strtoupper($rule) == $method; + } + + /** + * 验证时间和日期是否符合指定格式 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function dateFormat($value, $rule) + { + $info = date_parse_from_format($rule, $value); + return 0 == $info['warning_count'] && 0 == $info['error_count']; + } + + /** + * 验证是否唯一 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 格式:数据表,字段名,排除ID,主键名 + * @param array $data 数据 + * @param string $field 验证字段名 + * @return bool + */ + public function unique($value, $rule, $data, $field) + { + if (is_string($rule)) { + $rule = explode(',', $rule); + } + + if (false !== strpos($rule[0], '\\')) { + // 指定模型类 + $db = new $rule[0]; + } else { + try { + $db = Container::get('app')->model($rule[0]); + } catch (ClassNotFoundException $e) { + $db = Db::name($rule[0]); + } + } + + $key = isset($rule[1]) ? $rule[1] : $field; + + if (strpos($key, '^')) { + // 支持多个字段验证 + $fields = explode('^', $key); + foreach ($fields as $key) { + if (isset($data[$key])) { + $map[] = [$key, '=', $data[$key]]; + } + } + } elseif (strpos($key, '=')) { + parse_str($key, $map); + } elseif (isset($data[$field])) { + $map[] = [$key, '=', $data[$field]]; + } else { + $map = []; + } + + $pk = !empty($rule[3]) ? $rule[3] : $db->getPk(); + + if (is_string($pk)) { + if (isset($rule[2])) { + $map[] = [$pk, '<>', $rule[2]]; + } elseif (isset($data[$pk])) { + $map[] = [$pk, '<>', $data[$pk]]; + } + } + + if ($db->where($map)->field($pk)->find()) { + return false; + } + + return true; + } + + /** + * 使用行为类验证 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return mixed + */ + public function behavior($value, $rule, $data) + { + return Container::get('hook')->exec($rule, $data); + } + + /** + * 使用filter_var方式验证 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function filter($value, $rule) + { + if (is_string($rule) && strpos($rule, ',')) { + list($rule, $param) = explode(',', $rule); + } elseif (is_array($rule)) { + $param = isset($rule[1]) ? $rule[1] : null; + $rule = $rule[0]; + } else { + $param = null; + } + + return false !== filter_var($value, is_int($rule) ? $rule : filter_id($rule), $param); + } + + /** + * 验证某个字段等于某个值的时候必须 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function requireIf($value, $rule, $data) + { + list($field, $val) = explode(',', $rule); + + if ($this->getDataValue($data, $field) == $val) { + return !empty($value) || '0' == $value; + } + + return true; + } + + /** + * 通过回调方法验证某个字段是否必须 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function requireCallback($value, $rule, $data) + { + $result = call_user_func_array([$this, $rule], [$value, $data]); + + if ($result) { + return !empty($value) || '0' == $value; + } + + return true; + } + + /** + * 验证某个字段有值的情况下必须 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function requireWith($value, $rule, $data) + { + $val = $this->getDataValue($data, $rule); + + if (!empty($val)) { + return !empty($value) || '0' == $value; + } + + return true; + } + + /** + * 验证是否在范围内 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function in($value, $rule) + { + return in_array($value, is_array($rule) ? $rule : explode(',', $rule)); + } + + /** + * 验证是否不在某个范围 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function notIn($value, $rule) + { + return !in_array($value, is_array($rule) ? $rule : explode(',', $rule)); + } + + /** + * between验证数据 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function between($value, $rule) + { + if (is_string($rule)) { + $rule = explode(',', $rule); + } + list($min, $max) = $rule; + + return $value >= $min && $value <= $max; + } + + /** + * 使用notbetween验证数据 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function notBetween($value, $rule) + { + if (is_string($rule)) { + $rule = explode(',', $rule); + } + list($min, $max) = $rule; + + return $value < $min || $value > $max; + } + + /** + * 验证数据长度 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function length($value, $rule) + { + if (is_array($value)) { + $length = count($value); + } elseif ($value instanceof File) { + $length = $value->getSize(); + } else { + $length = mb_strlen((string) $value); + } + + if (strpos($rule, ',')) { + // 长度区间 + list($min, $max) = explode(',', $rule); + return $length >= $min && $length <= $max; + } + + // 指定长度 + return $length == $rule; + } + + /** + * 验证数据最大长度 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function max($value, $rule) + { + if (is_array($value)) { + $length = count($value); + } elseif ($value instanceof File) { + $length = $value->getSize(); + } else { + $length = mb_strlen((string) $value); + } + + return $length <= $rule; + } + + /** + * 验证数据最小长度 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function min($value, $rule) + { + if (is_array($value)) { + $length = count($value); + } elseif ($value instanceof File) { + $length = $value->getSize(); + } else { + $length = mb_strlen((string) $value); + } + + return $length >= $rule; + } + + /** + * 验证日期 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function after($value, $rule, $data) + { + return strtotime($value) >= strtotime($rule); + } + + /** + * 验证日期 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public 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 public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + public function expire($value, $rule) + { + if (is_string($rule)) { + $rule = explode(',', $rule); + } + + list($start, $end) = $rule; + + if (!is_numeric($start)) { + $start = strtotime($start); + } + + if (!is_numeric($end)) { + $end = strtotime($end); + } + + return $_SERVER['REQUEST_TIME'] >= $start && $_SERVER['REQUEST_TIME'] <= $end; + } + + /** + * 验证IP许可 + * @access public + * @param string $value 字段值 + * @param mixed $rule 验证规则 + * @return mixed + */ + public function allowIp($value, $rule) + { + return in_array($value, is_array($rule) ? $rule : explode(',', $rule)); + } + + /** + * 验证IP禁用 + * @access public + * @param string $value 字段值 + * @param mixed $rule 验证规则 + * @return mixed + */ + public function denyIp($value, $rule) + { + return !in_array($value, is_array($rule) ? $rule : explode(',', $rule)); + } + + /** + * 使用正则验证数据 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 正则规则或者预定义正则名 + * @return bool + */ + public function regex($value, $rule) + { + if (isset($this->regex[$rule])) { + $rule = $this->regex[$rule]; + } elseif (isset($this->defaultRegex[$rule])) { + $rule = $this->defaultRegex[$rule]; + } + + if (0 !== strpos($rule, '/') && !preg_match('/\/[imsU]{0,4}$/', $rule)) { + // 不是正则表达式则两端补上/ + $rule = '/^' . $rule . '$/'; + } + + return is_scalar($value) && 1 === preg_match($rule, (string) $value); + } + + /** + * 验证表单令牌 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + public function token($value, $rule, $data) + { + $rule = !empty($rule) ? $rule : '__token__'; + $session = Container::get('session'); + + if (!isset($data[$rule]) || !$session->has($rule)) { + // 令牌数据无效 + return false; + } + + // 令牌验证 + if (isset($data[$rule]) && $session->get($rule) === $data[$rule]) { + // 防止重复提交 + $session->delete($rule); // 验证完成销毁session + return true; + } + + // 开启TOKEN重置 + $session->delete($rule); + + return false; + } + + // 获取错误信息 + public function getError() + { + return $this->error; + } + + /** + * 获取数据值 + * @access protected + * @param array $data 数据 + * @param string $key 数据标识 支持多维 + * @return mixed + */ + protected function getDataValue($data, $key) + { + if (is_numeric($key)) { + $value = $key; + } elseif (strpos($key, '.')) { + // 支持多维数组验证 + foreach (explode('.', $key) as $key) { + if (!isset($data[$key])) { + $value = null; + break; + } + $value = $data = $data[$key]; + } + } else { + $value = isset($data[$key]) ? $data[$key] : null; + } + + return $value; + } + + /** + * 获取验证规则的错误提示信息 + * @access protected + * @param string $attribute 字段英文名 + * @param string $title 字段描述名 + * @param string $type 验证规则名称 + * @param mixed $rule 验证规则数据 + * @return string + */ + protected function getRuleMsg($attribute, $title, $type, $rule) + { + $lang = Container::get('lang'); + + if (isset($this->message[$attribute . '.' . $type])) { + $msg = $this->message[$attribute . '.' . $type]; + } elseif (isset($this->message[$attribute][$type])) { + $msg = $this->message[$attribute][$type]; + } elseif (isset($this->message[$attribute])) { + $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 . $lang->get('not conform to the rules'); + } + + if (!is_string($msg)) { + return $msg; + } + + if (0 === strpos($msg, '{%')) { + $msg = $lang->get(substr($msg, 2, -1)); + } elseif ($lang->has($msg)) { + $msg = $lang->get($msg); + } + + if (is_scalar($rule) && false !== strpos($msg, ':')) { + // 变量替换 + if (is_string($rule) && strpos($rule, ',')) { + $array = array_pad(explode(',', $rule), 3, ''); + } else { + $array = array_pad([], 3, ''); + } + $msg = str_replace( + [':attribute', ':1', ':2', ':3'], + [$title, $array[0], $array[1], $array[2]], + $msg); + if (strpos($msg, ':rule')) { + $msg = str_replace(':rule', (string) $rule, $msg); + } + } + + return $msg; + } + + /** + * 获取数据验证的场景 + * @access protected + * @param string $scene 验证场景 + * @return void + */ + protected function getScene($scene = '') + { + if (empty($scene)) { + // 读取指定场景 + $scene = $this->currentScene; + } + + $this->only = $this->append = $this->remove = []; + + if (empty($scene)) { + return; + } + + if (method_exists($this, 'scene' . $scene)) { + call_user_func([$this, 'scene' . $scene]); + } elseif (isset($this->scene[$scene])) { + // 如果设置了验证适用场景 + $scene = $this->scene[$scene]; + + if (is_string($scene)) { + $scene = explode(',', $scene); + } + + $this->only = $scene; + } + } + + /** + * 动态方法 直接调用is方法进行验证 + * @access public + * @param string $method 方法名 + * @param array $args 调用参数 + * @return bool + */ + public function __call($method, $args) + { + if ('is' == strtolower(substr($method, 0, 2))) { + $method = substr($method, 2); + } + + array_push($args, lcfirst($method)); + + return call_user_func_array([$this, 'is'], $args); + } +} diff --git a/vendor/topthink/framework/library/think/View.php b/vendor/topthink/framework/library/think/View.php new file mode 100644 index 0000000..284dd41 --- /dev/null +++ b/vendor/topthink/framework/library/think/View.php @@ -0,0 +1,253 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class View +{ + /** + * 模板引擎实例 + * @var object + */ + public $engine; + + /** + * 模板变量 + * @var array + */ + protected $data = []; + + /** + * 内容过滤 + * @var mixed + */ + protected $filter; + + /** + * 全局模板变量 + * @var array + */ + protected static $var = []; + + /** + * 初始化 + * @access public + * @param mixed $engine 模板引擎参数 + * @return $this + */ + public function init($engine = []) + { + // 初始化模板引擎 + $this->engine($engine); + + return $this; + } + + public static function __make(Config $config) + { + return (new static())->init($config->pull('template')); + } + + /** + * 模板变量静态赋值 + * @access public + * @param mixed $name 变量名 + * @param mixed $value 变量值 + * @return $this + */ + public function share($name, $value = '') + { + if (is_array($name)) { + self::$var = array_merge(self::$var, $name); + } else { + self::$var[$name] = $value; + } + + return $this; + } + + /** + * 清理模板变量 + * @access public + * @return void + */ + public function clear() + { + self::$var = []; + $this->data = []; + } + + /** + * 模板变量赋值 + * @access public + * @param mixed $name 变量名 + * @param mixed $value 变量值 + * @return $this + */ + public function assign($name, $value = '') + { + if (is_array($name)) { + $this->data = array_merge($this->data, $name); + } else { + $this->data[$name] = $value; + } + + return $this; + } + + /** + * 设置当前模板解析的引擎 + * @access public + * @param array|string $options 引擎参数 + * @return $this + */ + public function engine($options = []) + { + if (is_string($options)) { + $type = $options; + $options = []; + } else { + $type = !empty($options['type']) ? $options['type'] : 'Think'; + } + + if (isset($options['type'])) { + unset($options['type']); + } + + $this->engine = Loader::factory($type, '\\think\\view\\driver\\', $options); + + return $this; + } + + /** + * 配置模板引擎 + * @access public + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @return $this + */ + public function config($name, $value = null) + { + $this->engine->config($name, $value); + + return $this; + } + + /** + * 检查模板是否存在 + * @access public + * @param string|array $name 参数名 + * @return bool + */ + public function exists($name) + { + return $this->engine->exists($name); + } + + /** + * 视图过滤 + * @access public + * @param Callable $filter 过滤方法或闭包 + * @return $this + */ + public function filter($filter) + { + if ($filter) { + $this->filter = $filter; + } + + return $this; + } + + /** + * 解析和获取模板内容 用于输出 + * @access public + * @param string $template 模板文件名或者内容 + * @param array $vars 模板输出变量 + * @param array $config 模板参数 + * @param bool $renderContent 是否渲染内容 + * @return string + * @throws \Exception + */ + public function fetch($template = '', $vars = [], $config = [], $renderContent = false) + { + // 模板变量 + $vars = array_merge(self::$var, $this->data, $vars); + + // 页面缓存 + ob_start(); + ob_implicit_flush(0); + + // 渲染输出 + try { + $method = $renderContent ? 'display' : 'fetch'; + $this->engine->$method($template, $vars, $config); + } catch (\Exception $e) { + ob_end_clean(); + throw $e; + } + + // 获取并清空缓存 + $content = ob_get_clean(); + + if ($this->filter) { + $content = call_user_func_array($this->filter, [$content]); + } + + return $content; + } + + /** + * 渲染内容输出 + * @access public + * @param string $content 内容 + * @param array $vars 模板输出变量 + * @param array $config 模板参数 + * @return mixed + */ + public function display($content, $vars = [], $config = []) + { + return $this->fetch($content, $vars, $config, true); + } + + /** + * 模板变量赋值 + * @access public + * @param string $name 变量名 + * @param mixed $value 变量值 + */ + public function __set($name, $value) + { + $this->data[$name] = $value; + } + + /** + * 取得模板显示变量的值 + * @access protected + * @param string $name 模板变量 + * @return mixed + */ + public function __get($name) + { + return $this->data[$name]; + } + + /** + * 检测模板变量是否设置 + * @access public + * @param string $name 模板变量名 + * @return bool + */ + public function __isset($name) + { + return isset($this->data[$name]); + } +} diff --git a/vendor/topthink/framework/library/think/cache/Driver.php b/vendor/topthink/framework/library/think/cache/Driver.php new file mode 100644 index 0000000..6421681 --- /dev/null +++ b/vendor/topthink/framework/library/think/cache/Driver.php @@ -0,0 +1,366 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache; + +use think\Container; + +/** + * 缓存基础类 + */ +abstract class Driver +{ + /** + * 驱动句柄 + * @var object + */ + protected $handler = null; + + /** + * 缓存读取次数 + * @var integer + */ + protected $readTimes = 0; + + /** + * 缓存写入次数 + * @var integer + */ + protected $writeTimes = 0; + + /** + * 缓存参数 + * @var array + */ + protected $options = []; + + /** + * 缓存标签 + * @var string + */ + protected $tag; + + /** + * 序列化方法 + * @var array + */ + protected static $serialize = ['serialize', 'unserialize', 'think_serialize:', 16]; + + /** + * 判断缓存是否存在 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + abstract public function has($name); + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + abstract public function get($name, $default = false); + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 + * @return boolean + */ + abstract public function set($name, $value, $expire = null); + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + abstract public function inc($name, $step = 1); + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + abstract public function dec($name, $step = 1); + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + abstract public function rm($name); + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + abstract public function clear($tag = null); + + /** + * 获取有效期 + * @access protected + * @param integer|\DateTime $expire 有效期 + * @return integer + */ + protected function getExpireTime($expire) + { + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp() - time(); + } + + return $expire; + } + + /** + * 获取实际的缓存标识 + * @access protected + * @param string $name 缓存名 + * @return string + */ + protected function getCacheKey($name) + { + return $this->options['prefix'] . $name; + } + + /** + * 读取缓存并删除 + * @access public + * @param string $name 缓存变量名 + * @return mixed + */ + public function pull($name) + { + $result = $this->get($name, false); + + if ($result) { + $this->rm($name); + return $result; + } else { + return; + } + } + + /** + * 如果不存在则写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 + * @return mixed + */ + public function remember($name, $value, $expire = null) + { + if (!$this->has($name)) { + $time = time(); + while ($time + 5 > time() && $this->has($name . '_lock')) { + // 存在锁定则等待 + usleep(200000); + } + + try { + // 锁定 + $this->set($name . '_lock', true); + + if ($value instanceof \Closure) { + // 获取缓存数据 + $value = Container::getInstance()->invokeFunction($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; + } + } else { + $value = $this->get($name); + } + + return $value; + } + + /** + * 缓存标签 + * @access public + * @param string $name 标签名 + * @param string|array $keys 缓存标识 + * @param bool $overlay 是否覆盖 + * @return $this + */ + public function tag($name, $keys = null, $overlay = false) + { + if (is_null($name)) { + + } elseif (is_null($keys)) { + $this->tag = $name; + } else { + $key = $this->getTagkey($name); + + if (is_string($keys)) { + $keys = explode(',', $keys); + } + + $keys = array_map([$this, 'getCacheKey'], $keys); + + if ($overlay) { + $value = $keys; + } else { + $value = array_unique(array_merge($this->getTagItem($name), $keys)); + } + + $this->set($key, implode(',', $value), 0); + } + + return $this; + } + + /** + * 更新标签 + * @access protected + * @param string $name 缓存标识 + * @return void + */ + protected function setTagItem($name) + { + if ($this->tag) { + $key = $this->getTagkey($this->tag); + $this->tag = null; + + if ($this->has($key)) { + $value = explode(',', $this->get($key)); + $value[] = $name; + + if (count($value) > 1000) { + array_shift($value); + } + + $value = implode(',', array_unique($value)); + } else { + $value = $name; + } + + $this->set($key, $value, 0); + } + } + + /** + * 获取标签包含的缓存标识 + * @access protected + * @param string $tag 缓存标签 + * @return array + */ + protected function getTagItem($tag) + { + $key = $this->getTagkey($tag); + $value = $this->get($key); + + if ($value) { + return array_filter(explode(',', $value)); + } else { + return []; + } + } + + protected function getTagKey($tag) + { + return 'tag_' . md5($tag); + } + + /** + * 序列化数据 + * @access protected + * @param mixed $data + * @return string + */ + protected function serialize($data) + { + if (is_scalar($data) || !$this->options['serialize']) { + return $data; + } + + $serialize = self::$serialize[0]; + + return self::$serialize[2] . $serialize($data); + } + + /** + * 反序列化数据 + * @access protected + * @param string $data + * @return mixed + */ + protected function unserialize($data) + { + if ($this->options['serialize'] && 0 === strpos($data, self::$serialize[2])) { + $unserialize = self::$serialize[1]; + + return $unserialize(substr($data, self::$serialize[3])); + } else { + return $data; + } + } + + /** + * 注册序列化机制 + * @access public + * @param callable $serialize 序列化方法 + * @param callable $unserialize 反序列化方法 + * @param string $prefix 序列化前缀标识 + * @return $this + */ + public static function registerSerialize($serialize, $unserialize, $prefix = 'think_serialize:') + { + self::$serialize = [$serialize, $unserialize, $prefix, strlen($prefix)]; + } + + /** + * 返回句柄对象,可执行其它高级方法 + * + * @access public + * @return object + */ + public function handler() + { + return $this->handler; + } + + public function getReadTimes() + { + return $this->readTimes; + } + + public function getWriteTimes() + { + return $this->writeTimes; + } + + public function __call($method, $args) + { + return call_user_func_array([$this->handler, $method], $args); + } +} diff --git a/vendor/topthink/framework/library/think/cache/driver/File.php b/vendor/topthink/framework/library/think/cache/driver/File.php new file mode 100644 index 0000000..60be08d --- /dev/null +++ b/vendor/topthink/framework/library/think/cache/driver/File.php @@ -0,0 +1,307 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; +use think\Container; + +/** + * 文件类型缓存类 + * @author liu21st + */ +class File extends Driver +{ + protected $options = [ + 'expire' => 0, + 'cache_subdir' => true, + 'prefix' => '', + 'path' => '', + 'hash_type' => 'md5', + 'data_compress' => false, + 'serialize' => true, + ]; + + protected $expire; + + /** + * 架构函数 + * @param array $options + */ + public function __construct($options = []) + { + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + if (empty($this->options['path'])) { + $this->options['path'] = Container::get('app')->getRuntimePath() . 'cache' . DIRECTORY_SEPARATOR; + } elseif (substr($this->options['path'], -1) != DIRECTORY_SEPARATOR) { + $this->options['path'] .= DIRECTORY_SEPARATOR; + } + + $this->init(); + } + + /** + * 初始化检查 + * @access private + * @return boolean + */ + private function init() + { + // 创建项目缓存目录 + try { + if (!is_dir($this->options['path']) && mkdir($this->options['path'], 0755, true)) { + return true; + } + } catch (\Exception $e) { + } + + return false; + } + + /** + * 取得变量的存储文件名 + * @access protected + * @param string $name 缓存变量名 + * @param bool $auto 是否自动创建目录 + * @return string + */ + protected function getCacheKey($name, $auto = false) + { + $name = hash($this->options['hash_type'], $name); + + if ($this->options['cache_subdir']) { + // 使用子目录 + $name = substr($name, 0, 2) . DIRECTORY_SEPARATOR . substr($name, 2); + } + + if ($this->options['prefix']) { + $name = $this->options['prefix'] . DIRECTORY_SEPARATOR . $name; + } + + $filename = $this->options['path'] . $name . '.php'; + $dir = dirname($filename); + + if ($auto && !is_dir($dir)) { + try { + mkdir($dir, 0755, true); + } catch (\Exception $e) { + } + } + + return $filename; + } + + /** + * 判断缓存是否存在 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + return false !== $this->get($name) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $this->readTimes++; + + $filename = $this->getCacheKey($name); + + if (!is_file($filename)) { + return $default; + } + + $content = file_get_contents($filename); + $this->expire = null; + + if (false !== $content) { + $expire = (int) substr($content, 8, 12); + if (0 != $expire && time() > filemtime($filename) + $expire) { + //缓存过期删除缓存文件 + $this->unlink($filename); + return $default; + } + + $this->expire = $expire; + $content = substr($content, 32); + + if ($this->options['data_compress'] && function_exists('gzcompress')) { + //启用数据压缩 + $content = gzuncompress($content); + } + return $this->unserialize($content); + } else { + return $default; + } + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int|\DateTime $expire 有效时间 0为永久 + * @return boolean + */ + public function set($name, $value, $expire = null) + { + $this->writeTimes++; + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + $expire = $this->getExpireTime($expire); + $filename = $this->getCacheKey($name, true); + + if ($this->tag && !is_file($filename)) { + $first = true; + } + + $data = $this->serialize($value); + + if ($this->options['data_compress'] && function_exists('gzcompress')) { + //数据压缩 + $data = gzcompress($data, 3); + } + + $data = "\n" . $data; + $result = file_put_contents($filename, $data); + + if ($result) { + isset($first) && $this->setTagItem($filename); + clearstatcache(); + return true; + } else { + return false; + } + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) + $step; + $expire = $this->expire; + } else { + $value = $step; + $expire = 0; + } + + return $this->set($name, $value, $expire) ? $value : false; + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) - $step; + $expire = $this->expire; + } else { + $value = -$step; + $expire = 0; + } + + return $this->set($name, $value, $expire) ? $value : false; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + $this->writeTimes++; + + try { + return $this->unlink($this->getCacheKey($name)); + } catch (\Exception $e) { + } + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + foreach ($keys as $key) { + $this->unlink($key); + } + $this->rm($this->getTagKey($tag)); + return true; + } + + $this->writeTimes++; + + $files = (array) glob($this->options['path'] . ($this->options['prefix'] ? $this->options['prefix'] . DIRECTORY_SEPARATOR : '') . '*'); + + foreach ($files as $path) { + if (is_dir($path)) { + $matches = glob($path . DIRECTORY_SEPARATOR . '*.php'); + if (is_array($matches)) { + array_map(function ($v) { + $this->unlink($v); + }, $matches); + } + rmdir($path); + } else { + $this->unlink($path); + } + } + + return true; + } + + /** + * 判断文件是否存在后,删除 + * @access private + * @param string $path + * @return bool + * @author byron sampson + * @return boolean + */ + private function unlink($path) + { + return is_file($path) && unlink($path); + } + +} diff --git a/vendor/topthink/framework/library/think/cache/driver/Lite.php b/vendor/topthink/framework/library/think/cache/driver/Lite.php new file mode 100644 index 0000000..0cfe390 --- /dev/null +++ b/vendor/topthink/framework/library/think/cache/driver/Lite.php @@ -0,0 +1,209 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * 文件类型缓存类 + * @author liu21st + */ +class Lite extends Driver +{ + protected $options = [ + 'prefix' => '', + 'path' => '', + 'expire' => 0, // 等于 10*365*24*3600(10年) + ]; + + /** + * 架构函数 + * @access public + * + * @param array $options + */ + public function __construct($options = []) + { + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + if (substr($this->options['path'], -1) != DIRECTORY_SEPARATOR) { + $this->options['path'] .= DIRECTORY_SEPARATOR; + } + + } + + /** + * 取得变量的存储文件名 + * @access protected + * @param string $name 缓存变量名 + * @return string + */ + protected function getCacheKey($name) + { + return $this->options['path'] . $this->options['prefix'] . md5($name) . '.php'; + } + + /** + * 判断缓存是否存在 + * @access public + * @param string $name 缓存变量名 + * @return mixed + */ + public function has($name) + { + return $this->get($name) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $this->readTimes++; + + $filename = $this->getCacheKey($name); + + if (is_file($filename)) { + // 判断是否过期 + $mtime = filemtime($filename); + + if ($mtime < time()) { + // 清除已经过期的文件 + unlink($filename); + return $default; + } + + return include $filename; + } else { + return $default; + } + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int|\DateTime $expire 有效时间 0为永久 + * @return bool + */ + public function set($name, $value, $expire = null) + { + $this->writeTimes++; + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + 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)) { + $first = true; + } + + $ret = file_put_contents($filename, ("setTagItem($filename); + touch($filename, $expire); + } + + return $ret; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) + $step; + } else { + $value = $step; + } + + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) - $step; + } else { + $value = -$step; + } + + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + $this->writeTimes++; + + return unlink($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return bool + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + foreach ($keys as $key) { + unlink($key); + } + + $this->rm($this->getTagKey($tag)); + return true; + } + + $this->writeTimes++; + + array_map("unlink", glob($this->options['path'] . ($this->options['prefix'] ? $this->options['prefix'] . DIRECTORY_SEPARATOR : '') . '*.php')); + } +} diff --git a/vendor/topthink/framework/library/think/cache/driver/Memcache.php b/vendor/topthink/framework/library/think/cache/driver/Memcache.php new file mode 100644 index 0000000..1c53559 --- /dev/null +++ b/vendor/topthink/framework/library/think/cache/driver/Memcache.php @@ -0,0 +1,206 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +class Memcache extends Driver +{ + protected $options = [ + 'host' => '127.0.0.1', + 'port' => 11211, + 'expire' => 0, + 'timeout' => 0, // 超时时间(单位:毫秒) + 'persistent' => true, + 'prefix' => '', + 'serialize' => true, + ]; + + /** + * 架构函数 + * @access public + * @param array $options 缓存参数 + * @throws \BadFunctionCallException + */ + public function __construct($options = []) + { + if (!extension_loaded('memcache')) { + throw new \BadFunctionCallException('not support: memcache'); + } + + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + $this->handler = new \Memcache; + + // 支持集群 + $hosts = explode(',', $this->options['host']); + $ports = explode(',', $this->options['port']); + + if (empty($ports[0])) { + $ports[0] = 11211; + } + + // 建立连接 + foreach ((array) $hosts as $i => $host) { + $port = isset($ports[$i]) ? $ports[$i] : $ports[0]; + $this->options['timeout'] > 0 ? + $this->handler->addServer($host, $port, $this->options['persistent'], 1, $this->options['timeout']) : + $this->handler->addServer($host, $port, $this->options['persistent'], 1); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $key = $this->getCacheKey($name); + + return false !== $this->handler->get($key); + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $this->readTimes++; + + $result = $this->handler->get($this->getCacheKey($name)); + + return false !== $result ? $this->unserialize($result) : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int|DateTime $expire 有效时间(秒) + * @return bool + */ + public function set($name, $value, $expire = null) + { + $this->writeTimes++; + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + if ($this->tag && !$this->has($name)) { + $first = true; + } + + $key = $this->getCacheKey($name); + $expire = $this->getExpireTime($expire); + $value = $this->serialize($value); + + if ($this->handler->set($key, $value, 0, $expire)) { + isset($first) && $this->setTagItem($key); + return true; + } + + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + if ($this->handler->get($key)) { + return $this->handler->increment($key, $step); + } + + return $this->handler->set($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + $value = $this->handler->get($key) - $step; + $res = $this->handler->set($key, $value); + + return !$res ? false : $value; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @param bool|false $ttl + * @return bool + */ + public function rm($name, $ttl = false) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return false === $ttl ? + $this->handler->delete($key) : + $this->handler->delete($key, $ttl); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return bool + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + + foreach ($keys as $key) { + $this->handler->delete($key); + } + + $tagName = $this->getTagKey($tag); + $this->rm($tagName); + return true; + } + + $this->writeTimes++; + + return $this->handler->flush(); + } + +} diff --git a/vendor/topthink/framework/library/think/cache/driver/Memcached.php b/vendor/topthink/framework/library/think/cache/driver/Memcached.php new file mode 100644 index 0000000..4533e78 --- /dev/null +++ b/vendor/topthink/framework/library/think/cache/driver/Memcached.php @@ -0,0 +1,279 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +class Memcached extends Driver +{ + protected $options = [ + 'host' => '127.0.0.1', + 'port' => 11211, + 'expire' => 0, + 'timeout' => 0, // 超时时间(单位:毫秒) + 'prefix' => '', + 'username' => '', //账号 + 'password' => '', //密码 + 'option' => [], + 'serialize' => true, + ]; + + /** + * 架构函数 + * @access public + * @param array $options 缓存参数 + */ + public function __construct($options = []) + { + if (!extension_loaded('memcached')) { + throw new \BadFunctionCallException('not support: memcached'); + } + + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + $this->handler = new \Memcached; + + if (!empty($this->options['option'])) { + $this->handler->setOptions($this->options['option']); + } + + // 设置连接超时时间(单位:毫秒) + if ($this->options['timeout'] > 0) { + $this->handler->setOption(\Memcached::OPT_CONNECT_TIMEOUT, $this->options['timeout']); + } + + // 支持集群 + $hosts = explode(',', $this->options['host']); + $ports = explode(',', $this->options['port']); + if (empty($ports[0])) { + $ports[0] = 11211; + } + + // 建立连接 + $servers = []; + foreach ((array) $hosts as $i => $host) { + $servers[] = [$host, (isset($ports[$i]) ? $ports[$i] : $ports[0]), 1]; + } + + $this->handler->addServers($servers); + $this->handler->setOption(\Memcached::OPT_COMPRESSION, false); + if ('' != $this->options['username']) { + $this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); + $this->handler->setSaslAuthData($this->options['username'], $this->options['password']); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $key = $this->getCacheKey($name); + + return $this->handler->get($key) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $this->readTimes++; + + $result = $this->handler->get($this->getCacheKey($name)); + + return false !== $result ? $this->unserialize($result) : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) + * @return bool + */ + public function set($name, $value, $expire = null) + { + $this->writeTimes++; + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + if ($this->tag && !$this->has($name)) { + $first = true; + } + + $key = $this->getCacheKey($name); + $expire = $this->getExpireTime($expire); + $value = $this->serialize($value); + + if ($this->handler->set($key, $value, $expire)) { + isset($first) && $this->setTagItem($key); + return true; + } + + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + if ($this->handler->get($key)) { + return $this->handler->increment($key, $step); + } + + return $this->handler->set($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + $value = $this->handler->get($key) - $step; + $res = $this->handler->set($key, $value); + + return !$res ? false : $value; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @param bool|false $ttl + * @return bool + */ + public function rm($name, $ttl = false) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return false === $ttl ? + $this->handler->delete($key) : + $this->handler->delete($key, $ttl); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return bool + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + + $this->handler->deleteMulti($keys); + $this->rm($this->getTagKey($tag)); + + return true; + } + + $this->writeTimes++; + + return $this->handler->flush(); + } + + /** + * 缓存标签 + * @access public + * @param string $name 标签名 + * @param string|array $keys 缓存标识 + * @param bool $overlay 是否覆盖 + * @return $this + */ + public function tag($name, $keys = null, $overlay = false) + { + if (is_null($keys)) { + $this->tag = $name; + } else { + $tagName = $this->getTagKey($name); + if ($overlay) { + $this->handler->delete($tagName); + } + + if (!$this->has($tagName)) { + $this->handler->set($tagName, ''); + } + + foreach ($keys as $key) { + $this->handler->append($tagName, ',' . $key); + } + } + + return $this; + } + + /** + * 更新标签 + * @access protected + * @param string $name 缓存标识 + * @return void + */ + protected function setTagItem($name) + { + if ($this->tag) { + $tagName = $this->getTagKey($this->tag); + + if ($this->has($tagName)) { + $this->handler->append($tagName, ',' . $name); + } else { + $this->handler->set($tagName, $name); + } + + $this->tag = null; + } + } + + /** + * 获取标签包含的缓存标识 + * @access public + * @param string $tag 缓存标签 + * @return array + */ + public function getTagItem($tag) + { + $tagName = $this->getTagKey($tag); + return explode(',', trim($this->handler->get($tagName), ',')); + } +} diff --git a/vendor/topthink/framework/library/think/cache/driver/Redis.php b/vendor/topthink/framework/library/think/cache/driver/Redis.php new file mode 100644 index 0000000..4eff2cf --- /dev/null +++ b/vendor/topthink/framework/library/think/cache/driver/Redis.php @@ -0,0 +1,272 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * Redis缓存驱动,适合单机部署、有前端代理实现高可用的场景,性能最好 + * 有需要在业务层实现读写分离、或者使用RedisCluster的需求,请使用Redisd驱动 + * + * 要求安装phpredis扩展:https://github.com/nicolasff/phpredis + * @author 尘缘 <130775@qq.com> + */ +class Redis extends Driver +{ + protected $options = [ + 'host' => '127.0.0.1', + 'port' => 6379, + 'password' => '', + 'select' => 0, + 'timeout' => 0, + 'expire' => 0, + 'persistent' => false, + 'prefix' => '', + 'serialize' => true, + ]; + + /** + * 架构函数 + * @access public + * @param array $options 缓存参数 + */ + public function __construct($options = []) + { + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + if (extension_loaded('redis')) { + $this->handler = new \Redis; + + 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']); + } + + if (0 != $this->options['select']) { + $this->handler->select($this->options['select']); + } + } elseif (class_exists('\Predis\Client')) { + $params = []; + foreach ($this->options as $key => $val) { + if (in_array($key, ['aggregate', 'cluster', 'connections', 'exceptions', 'prefix', 'profile', 'replication', 'parameters'])) { + $params[$key] = $val; + unset($this->options[$key]); + } + } + + if ('' == $this->options['password']) { + unset($this->options['password']); + } + + $this->handler = new \Predis\Client($this->options, $params); + + $this->options['prefix'] = ''; + } else { + throw new \BadFunctionCallException('not support: redis'); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + return $this->handler->exists($this->getCacheKey($name)) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $this->readTimes++; + + $value = $this->handler->get($this->getCacheKey($name)); + + if (is_null($value) || false === $value) { + return $default; + } + + return $this->unserialize($value); + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) + * @return boolean + */ + public function set($name, $value, $expire = null) + { + $this->writeTimes++; + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + if ($this->tag && !$this->has($name)) { + $first = true; + } + + $key = $this->getCacheKey($name); + $expire = $this->getExpireTime($expire); + + $value = $this->serialize($value); + + if ($expire) { + $result = $this->handler->setex($key, $expire, $value); + } else { + $result = $this->handler->set($key, $value); + } + + isset($first) && $this->setTagItem($key); + + return $result; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return $this->handler->incrby($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return $this->handler->decrby($key, $step); + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + $this->writeTimes++; + + return $this->handler->del($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + + $this->handler->del($keys); + + $tagName = $this->getTagKey($tag); + $this->handler->del($tagName); + return true; + } + + $this->writeTimes++; + + return $this->handler->flushDB(); + } + + /** + * 缓存标签 + * @access public + * @param string $name 标签名 + * @param string|array $keys 缓存标识 + * @param bool $overlay 是否覆盖 + * @return $this + */ + public function tag($name, $keys = null, $overlay = false) + { + if (is_null($keys)) { + $this->tag = $name; + } else { + $tagName = $this->getTagKey($name); + if ($overlay) { + $this->handler->del($tagName); + } + + foreach ($keys as $key) { + $this->handler->sAdd($tagName, $key); + } + } + + return $this; + } + + /** + * 更新标签 + * @access protected + * @param string $name 缓存标识 + * @return void + */ + protected function setTagItem($name) + { + if ($this->tag) { + $tagName = $this->getTagKey($this->tag); + $this->handler->sAdd($tagName, $name); + } + } + + /** + * 获取标签包含的缓存标识 + * @access protected + * @param string $tag 缓存标签 + * @return array + */ + protected function getTagItem($tag) + { + $tagName = $this->getTagKey($tag); + return $this->handler->sMembers($tagName); + } +} diff --git a/vendor/topthink/framework/library/think/cache/driver/Sqlite.php b/vendor/topthink/framework/library/think/cache/driver/Sqlite.php new file mode 100644 index 0000000..f57361e --- /dev/null +++ b/vendor/topthink/framework/library/think/cache/driver/Sqlite.php @@ -0,0 +1,233 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * Sqlite缓存驱动 + * @author liu21st + */ +class Sqlite extends Driver +{ + protected $options = [ + 'db' => ':memory:', + 'table' => 'sharedmemory', + 'prefix' => '', + 'expire' => 0, + 'persistent' => false, + 'serialize' => true, + ]; + + /** + * 架构函数 + * @access public + * @param array $options 缓存参数 + * @throws \BadFunctionCallException + */ + public function __construct($options = []) + { + if (!extension_loaded('sqlite')) { + throw new \BadFunctionCallException('not support: sqlite'); + } + + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + $func = $this->options['persistent'] ? 'sqlite_popen' : 'sqlite_open'; + + $this->handler = $func($this->options['db']); + } + + /** + * 获取实际的缓存标识 + * @access public + * @param string $name 缓存名 + * @return string + */ + protected function getCacheKey($name) + { + return $this->options['prefix'] . sqlite_escape_string($name); + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $name = $this->getCacheKey($name); + + $sql = 'SELECT value FROM ' . $this->options['table'] . ' WHERE var=\'' . $name . '\' AND (expire=0 OR expire >' . time() . ') LIMIT 1'; + $result = sqlite_query($this->handler, $sql); + + return sqlite_num_rows($result); + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $this->readTimes++; + + $name = $this->getCacheKey($name); + + $sql = 'SELECT value FROM ' . $this->options['table'] . ' WHERE var=\'' . $name . '\' AND (expire=0 OR expire >' . time() . ') LIMIT 1'; + + $result = sqlite_query($this->handler, $sql); + + if (sqlite_num_rows($result)) { + $content = sqlite_fetch_single($result); + if (function_exists('gzcompress')) { + //启用数据压缩 + $content = gzuncompress($content); + } + + return $this->unserialize($content); + } + + return $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) + * @return boolean + */ + public function set($name, $value, $expire = null) + { + $this->writeTimes++; + + $name = $this->getCacheKey($name); + + $value = sqlite_escape_string($this->serialize($value)); + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp(); + } else { + $expire = (0 == $expire) ? 0 : (time() + $expire); //缓存有效期为0表示永久缓存 + } + + if (function_exists('gzcompress')) { + //数据压缩 + $value = gzcompress($value, 3); + } + + if ($this->tag) { + $tag = $this->tag; + $this->tag = null; + } else { + $tag = ''; + } + + $sql = 'REPLACE INTO ' . $this->options['table'] . ' (var, value, expire, tag) VALUES (\'' . $name . '\', \'' . $value . '\', \'' . $expire . '\', \'' . $tag . '\')'; + + if (sqlite_query($this->handler, $sql)) { + return true; + } + + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) + $step; + } else { + $value = $step; + } + + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) - $step; + } else { + $value = -$step; + } + + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + $this->writeTimes++; + + $name = $this->getCacheKey($name); + + $sql = 'DELETE FROM ' . $this->options['table'] . ' WHERE var=\'' . $name . '\''; + sqlite_query($this->handler, $sql); + + return true; + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + $name = sqlite_escape_string($this->getTagKey($tag)); + $sql = 'DELETE FROM ' . $this->options['table'] . ' WHERE tag=\'' . $name . '\''; + sqlite_query($this->handler, $sql); + return true; + } + + $this->writeTimes++; + + $sql = 'DELETE FROM ' . $this->options['table']; + + sqlite_query($this->handler, $sql); + + return true; + } +} diff --git a/vendor/topthink/framework/library/think/cache/driver/Wincache.php b/vendor/topthink/framework/library/think/cache/driver/Wincache.php new file mode 100644 index 0000000..ef15784 --- /dev/null +++ b/vendor/topthink/framework/library/think/cache/driver/Wincache.php @@ -0,0 +1,175 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * Wincache缓存驱动 + * @author liu21st + */ +class Wincache extends Driver +{ + protected $options = [ + 'prefix' => '', + 'expire' => 0, + 'serialize' => true, + ]; + + /** + * 架构函数 + * @access public + * @param array $options 缓存参数 + * @throws \BadFunctionCallException + */ + public function __construct($options = []) + { + if (!function_exists('wincache_ucache_info')) { + throw new \BadFunctionCallException('not support: WinCache'); + } + + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $this->readTimes++; + + $key = $this->getCacheKey($name); + + return wincache_ucache_exists($key); + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $this->readTimes++; + + $key = $this->getCacheKey($name); + + return wincache_ucache_exists($key) ? $this->unserialize(wincache_ucache_get($key)) : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) + * @return boolean + */ + public function set($name, $value, $expire = null) + { + $this->writeTimes++; + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + $key = $this->getCacheKey($name); + $expire = $this->getExpireTime($expire); + $value = $this->serialize($value); + + if ($this->tag && !$this->has($name)) { + $first = true; + } + + if (wincache_ucache_set($key, $value, $expire)) { + isset($first) && $this->setTagItem($key); + return true; + } + + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return wincache_ucache_inc($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return wincache_ucache_dec($key, $step); + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + $this->writeTimes++; + + return wincache_ucache_delete($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + $keys = $this->getTagItem($tag); + + wincache_ucache_delete($keys); + + $tagName = $this->getTagkey($tag); + $this->rm($tagName); + return true; + } + + $this->writeTimes++; + return wincache_ucache_clear(); + } + +} diff --git a/vendor/topthink/framework/library/think/cache/driver/Xcache.php b/vendor/topthink/framework/library/think/cache/driver/Xcache.php new file mode 100644 index 0000000..4e69859 --- /dev/null +++ b/vendor/topthink/framework/library/think/cache/driver/Xcache.php @@ -0,0 +1,179 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * Xcache缓存驱动 + * @author liu21st + */ +class Xcache extends Driver +{ + protected $options = [ + 'prefix' => '', + 'expire' => 0, + 'serialize' => true, + ]; + + /** + * 架构函数 + * @access public + * @param array $options 缓存参数 + * @throws \BadFunctionCallException + */ + public function __construct($options = []) + { + if (!function_exists('xcache_info')) { + throw new \BadFunctionCallException('not support: Xcache'); + } + + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $key = $this->getCacheKey($name); + + return xcache_isset($key); + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $this->readTimes++; + + $key = $this->getCacheKey($name); + + return xcache_isset($key) ? $this->unserialize(xcache_get($key)) : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) + * @return boolean + */ + public function set($name, $value, $expire = null) + { + $this->writeTimes++; + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + if ($this->tag && !$this->has($name)) { + $first = true; + } + + $key = $this->getCacheKey($name); + $expire = $this->getExpireTime($expire); + $value = $this->serialize($value); + + if (xcache_set($key, $value, $expire)) { + isset($first) && $this->setTagItem($key); + return true; + } + + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return xcache_inc($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return xcache_dec($key, $step); + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + $this->writeTimes++; + + return xcache_unset($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + + foreach ($keys as $key) { + xcache_unset($key); + } + + $this->rm($this->getTagKey($tag)); + return true; + } + + $this->writeTimes++; + + if (function_exists('xcache_unset_by_prefix')) { + return xcache_unset_by_prefix($this->options['prefix']); + } else { + return false; + } + } +} diff --git a/vendor/topthink/framework/library/think/config/driver/Ini.php b/vendor/topthink/framework/library/think/config/driver/Ini.php new file mode 100644 index 0000000..b2a647d --- /dev/null +++ b/vendor/topthink/framework/library/think/config/driver/Ini.php @@ -0,0 +1,31 @@ + +// +---------------------------------------------------------------------- + +namespace think\config\driver; + +class Ini +{ + protected $config; + + public function __construct($config) + { + $this->config = $config; + } + + public function parse() + { + if (is_file($this->config)) { + return parse_ini_file($this->config, true); + } else { + return parse_ini_string($this->config, true); + } + } +} diff --git a/vendor/topthink/framework/library/think/config/driver/Json.php b/vendor/topthink/framework/library/think/config/driver/Json.php new file mode 100644 index 0000000..0d77c8e --- /dev/null +++ b/vendor/topthink/framework/library/think/config/driver/Json.php @@ -0,0 +1,31 @@ + +// +---------------------------------------------------------------------- + +namespace think\config\driver; + +class Json +{ + protected $config; + + public function __construct($config) + { + if (is_file($config)) { + $config = file_get_contents($config); + } + + $this->config = $config; + } + + public function parse() + { + return json_decode($this->config, true); + } +} diff --git a/vendor/topthink/framework/library/think/config/driver/Xml.php b/vendor/topthink/framework/library/think/config/driver/Xml.php new file mode 100644 index 0000000..9d69633 --- /dev/null +++ b/vendor/topthink/framework/library/think/config/driver/Xml.php @@ -0,0 +1,40 @@ + +// +---------------------------------------------------------------------- + +namespace think\config\driver; + +class Xml +{ + protected $config; + + public function __construct($config) + { + $this->config = $config; + } + + public function parse() + { + if (is_file($this->config)) { + $content = simplexml_load_file($this->config); + } else { + $content = simplexml_load_string($this->config); + } + + $result = (array) $content; + foreach ($result as $key => $val) { + if (is_object($val)) { + $result[$key] = (array) $val; + } + } + + return $result; + } +} diff --git a/vendor/topthink/framework/library/think/console/Command.php b/vendor/topthink/framework/library/think/console/Command.php new file mode 100644 index 0000000..a208e7b --- /dev/null +++ b/vendor/topthink/framework/library/think/console/Command.php @@ -0,0 +1,482 @@ + +// +---------------------------------------------------------------------- + +namespace think\console; + +use think\Console; +use think\console\input\Argument; +use think\console\input\Definition; +use think\console\input\Option; + +class Command +{ + + /** @var Console */ + private $console; + private $name; + private $aliases = []; + private $definition; + private $help; + private $description; + private $ignoreValidationErrors = false; + private $consoleDefinitionMerged = false; + private $consoleDefinitionMergedWithArgs = false; + private $code; + private $synopsis = []; + private $usages = []; + + /** @var Input */ + protected $input; + + /** @var Output */ + protected $output; + + /** + * 构造方法 + * @param string|null $name 命令名称,如果没有设置则比如在 configure() 里设置 + * @throws \LogicException + * @api + */ + public function __construct($name = null) + { + $this->definition = new Definition(); + + if (null !== $name) { + $this->setName($name); + } + + $this->configure(); + + if (!$this->name) { + throw new \LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($this))); + } + } + + /** + * 忽略验证错误 + */ + public function ignoreValidationErrors() + { + $this->ignoreValidationErrors = true; + } + + /** + * 设置控制台 + * @param Console $console + */ + public function setConsole(Console $console = null) + { + $this->console = $console; + } + + /** + * 获取控制台 + * @return Console + * @api + */ + public function getConsole() + { + return $this->console; + } + + /** + * 是否有效 + * @return bool + */ + public function isEnabled() + { + return true; + } + + /** + * 配置指令 + */ + protected function configure() + { + } + + /** + * 执行指令 + * @param Input $input + * @param Output $output + * @return null|int + * @throws \LogicException + * @see setCode() + */ + protected function execute(Input $input, Output $output) + { + throw new \LogicException('You must override the execute() method in the concrete command class.'); + } + + /** + * 用户验证 + * @param Input $input + * @param Output $output + */ + protected function interact(Input $input, Output $output) + { + } + + /** + * 初始化 + * @param Input $input An InputInterface instance + * @param Output $output An OutputInterface instance + */ + protected function initialize(Input $input, Output $output) + { + } + + /** + * 执行 + * @param Input $input + * @param Output $output + * @return int + * @throws \Exception + * @see setCode() + * @see execute() + */ + public function run(Input $input, Output $output) + { + $this->input = $input; + $this->output = $output; + + $this->getSynopsis(true); + $this->getSynopsis(false); + + $this->mergeConsoleDefinition(); + + try { + $input->bind($this->definition); + } catch (\Exception $e) { + if (!$this->ignoreValidationErrors) { + throw $e; + } + } + + $this->initialize($input, $output); + + if ($input->isInteractive()) { + $this->interact($input, $output); + } + + $input->validate(); + + if ($this->code) { + $statusCode = call_user_func($this->code, $input, $output); + } else { + $statusCode = $this->execute($input, $output); + } + + return is_numeric($statusCode) ? (int) $statusCode : 0; + } + + /** + * 设置执行代码 + * @param callable $code callable(InputInterface $input, OutputInterface $output) + * @return Command + * @throws \InvalidArgumentException + * @see execute() + */ + public function setCode(callable $code) + { + if (!is_callable($code)) { + throw new \InvalidArgumentException('Invalid callable provided to Command::setCode.'); + } + + if (PHP_VERSION_ID >= 50400 && $code instanceof \Closure) { + $r = new \ReflectionFunction($code); + if (null === $r->getClosureThis()) { + $code = \Closure::bind($code, $this); + } + } + + $this->code = $code; + + return $this; + } + + /** + * 合并参数定义 + * @param bool $mergeArgs + */ + public function mergeConsoleDefinition($mergeArgs = true) + { + if (null === $this->console + || (true === $this->consoleDefinitionMerged + && ($this->consoleDefinitionMergedWithArgs || !$mergeArgs)) + ) { + return; + } + + if ($mergeArgs) { + $currentArguments = $this->definition->getArguments(); + $this->definition->setArguments($this->console->getDefinition()->getArguments()); + $this->definition->addArguments($currentArguments); + } + + $this->definition->addOptions($this->console->getDefinition()->getOptions()); + + $this->consoleDefinitionMerged = true; + if ($mergeArgs) { + $this->consoleDefinitionMergedWithArgs = true; + } + } + + /** + * 设置参数定义 + * @param array|Definition $definition + * @return Command + * @api + */ + public function setDefinition($definition) + { + if ($definition instanceof Definition) { + $this->definition = $definition; + } else { + $this->definition->setDefinition($definition); + } + + $this->consoleDefinitionMerged = false; + + return $this; + } + + /** + * 获取参数定义 + * @return Definition + * @api + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * 获取当前指令的参数定义 + * @return Definition + */ + public function getNativeDefinition() + { + return $this->getDefinition(); + } + + /** + * 添加参数 + * @param string $name 名称 + * @param int $mode 类型 + * @param string $description 描述 + * @param mixed $default 默认值 + * @return Command + */ + public function addArgument($name, $mode = null, $description = '', $default = null) + { + $this->definition->addArgument(new Argument($name, $mode, $description, $default)); + + return $this; + } + + /** + * 添加选项 + * @param string $name 选项名称 + * @param string $shortcut 别名 + * @param int $mode 类型 + * @param string $description 描述 + * @param mixed $default 默认值 + * @return Command + */ + public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null) + { + $this->definition->addOption(new Option($name, $shortcut, $mode, $description, $default)); + + return $this; + } + + /** + * 设置指令名称 + * @param string $name + * @return Command + * @throws \InvalidArgumentException + */ + public function setName($name) + { + $this->validateName($name); + + $this->name = $name; + + return $this; + } + + /** + * 获取指令名称 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 设置描述 + * @param string $description + * @return Command + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + /** + * 获取描述 + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * 设置帮助信息 + * @param string $help + * @return Command + */ + public function setHelp($help) + { + $this->help = $help; + + return $this; + } + + /** + * 获取帮助信息 + * @return string + */ + public function getHelp() + { + return $this->help; + } + + /** + * 描述信息 + * @return string + */ + public function getProcessedHelp() + { + $name = $this->name; + + $placeholders = [ + '%command.name%', + '%command.full_name%', + ]; + $replacements = [ + $name, + $_SERVER['PHP_SELF'] . ' ' . $name, + ]; + + return str_replace($placeholders, $replacements, $this->getHelp()); + } + + /** + * 设置别名 + * @param string[] $aliases + * @return Command + * @throws \InvalidArgumentException + */ + public function setAliases($aliases) + { + if (!is_array($aliases) && !$aliases instanceof \Traversable) { + throw new \InvalidArgumentException('$aliases must be an array or an instance of \Traversable'); + } + + foreach ($aliases as $alias) { + $this->validateName($alias); + } + + $this->aliases = $aliases; + + return $this; + } + + /** + * 获取别名 + * @return array + */ + public function getAliases() + { + return $this->aliases; + } + + /** + * 获取简介 + * @param bool $short 是否简单的 + * @return string + */ + public function getSynopsis($short = false) + { + $key = $short ? 'short' : 'long'; + + if (!isset($this->synopsis[$key])) { + $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short))); + } + + return $this->synopsis[$key]; + } + + /** + * 添加用法介绍 + * @param string $usage + * @return $this + */ + public function addUsage($usage) + { + if (0 !== strpos($usage, $this->name)) { + $usage = sprintf('%s %s', $this->name, $usage); + } + + $this->usages[] = $usage; + + return $this; + } + + /** + * 获取用法介绍 + * @return array + */ + public function getUsages() + { + return $this->usages; + } + + /** + * 验证指令名称 + * @param string $name + * @throws \InvalidArgumentException + */ + private function validateName($name) + { + if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) { + throw new \InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name)); + } + } + + /** + * 输出表格 + * @param Table $table + * @return string + */ + protected function table(Table $table) + { + $content = $table->render(); + $this->output->writeln($content); + return $content; + } +} diff --git a/vendor/topthink/framework/library/think/console/Input.php b/vendor/topthink/framework/library/think/console/Input.php new file mode 100644 index 0000000..2482dfd --- /dev/null +++ b/vendor/topthink/framework/library/think/console/Input.php @@ -0,0 +1,464 @@ + +// +---------------------------------------------------------------------- + +namespace think\console; + +use think\console\input\Argument; +use think\console\input\Definition; +use think\console\input\Option; + +class Input +{ + + /** + * @var Definition + */ + protected $definition; + + /** + * @var Option[] + */ + protected $options = []; + + /** + * @var Argument[] + */ + protected $arguments = []; + + protected $interactive = true; + + private $tokens; + private $parsed; + + public function __construct($argv = null) + { + if (null === $argv) { + $argv = $_SERVER['argv']; + // 去除命令名 + array_shift($argv); + } + + $this->tokens = $argv; + + $this->definition = new Definition(); + } + + protected function setTokens(array $tokens) + { + $this->tokens = $tokens; + } + + /** + * 绑定实例 + * @param Definition $definition A InputDefinition instance + */ + public function bind(Definition $definition) + { + $this->arguments = []; + $this->options = []; + $this->definition = $definition; + + $this->parse(); + } + + /** + * 解析参数 + */ + protected function parse() + { + $parseOptions = true; + $this->parsed = $this->tokens; + while (null !== $token = array_shift($this->parsed)) { + if ($parseOptions && '' == $token) { + $this->parseArgument($token); + } elseif ($parseOptions && '--' == $token) { + $parseOptions = false; + } elseif ($parseOptions && 0 === strpos($token, '--')) { + $this->parseLongOption($token); + } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) { + $this->parseShortOption($token); + } else { + $this->parseArgument($token); + } + } + } + + /** + * 解析短选项 + * @param string $token 当前的指令. + */ + private function parseShortOption($token) + { + $name = substr($token, 1); + + if (strlen($name) > 1) { + if ($this->definition->hasShortcut($name[0]) + && $this->definition->getOptionForShortcut($name[0])->acceptValue() + ) { + $this->addShortOption($name[0], substr($name, 1)); + } else { + $this->parseShortOptionSet($name); + } + } else { + $this->addShortOption($name, null); + } + } + + /** + * 解析短选项 + * @param string $name 当前指令 + * @throws \RuntimeException + */ + private function parseShortOptionSet($name) + { + $len = strlen($name); + for ($i = 0; $i < $len; ++$i) { + if (!$this->definition->hasShortcut($name[$i])) { + throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $name[$i])); + } + + $option = $this->definition->getOptionForShortcut($name[$i]); + if ($option->acceptValue()) { + $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1)); + + break; + } else { + $this->addLongOption($option->getName(), null); + } + } + } + + /** + * 解析完整选项 + * @param string $token 当前指令 + */ + private function parseLongOption($token) + { + $name = substr($token, 2); + + if (false !== $pos = strpos($name, '=')) { + $this->addLongOption(substr($name, 0, $pos), substr($name, $pos + 1)); + } else { + $this->addLongOption($name, null); + } + } + + /** + * 解析参数 + * @param string $token 当前指令 + * @throws \RuntimeException + */ + private function parseArgument($token) + { + $c = count($this->arguments); + + if ($this->definition->hasArgument($c)) { + $arg = $this->definition->getArgument($c); + + $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token; + + } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) { + $arg = $this->definition->getArgument($c - 1); + + $this->arguments[$arg->getName()][] = $token; + } else { + throw new \RuntimeException('Too many arguments.'); + } + } + + /** + * 添加一个短选项的值 + * @param string $shortcut 短名称 + * @param mixed $value 值 + * @throws \RuntimeException + */ + private function addShortOption($shortcut, $value) + { + if (!$this->definition->hasShortcut($shortcut)) { + throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); + } + + /** + * 添加一个完整选项的值 + * @param string $name 选项名 + * @param mixed $value 值 + * @throws \RuntimeException + */ + private function addLongOption($name, $value) + { + if (!$this->definition->hasOption($name)) { + throw new \RuntimeException(sprintf('The "--%s" option does not exist.', $name)); + } + + $option = $this->definition->getOption($name); + + if (false === $value) { + $value = null; + } + + if (null !== $value && !$option->acceptValue()) { + throw new \RuntimeException(sprintf('The "--%s" option does not accept a value.', $name, $value)); + } + + if (null === $value && $option->acceptValue() && count($this->parsed)) { + $next = array_shift($this->parsed); + if (isset($next[0]) && '-' !== $next[0]) { + $value = $next; + } elseif (empty($next)) { + $value = ''; + } else { + array_unshift($this->parsed, $next); + } + } + + if (null === $value) { + if ($option->isValueRequired()) { + throw new \RuntimeException(sprintf('The "--%s" option requires a value.', $name)); + } + + if (!$option->isArray()) { + $value = $option->isValueOptional() ? $option->getDefault() : true; + } + } + + if ($option->isArray()) { + $this->options[$name][] = $value; + } else { + $this->options[$name] = $value; + } + } + + /** + * 获取第一个参数 + * @return string|null + */ + public function getFirstArgument() + { + foreach ($this->tokens as $token) { + if ($token && '-' === $token[0]) { + continue; + } + + return $token; + } + return; + } + + /** + * 检查原始参数是否包含某个值 + * @param string|array $values 需要检查的值 + * @return bool + */ + public function hasParameterOption($values) + { + $values = (array) $values; + + foreach ($this->tokens as $token) { + foreach ($values as $value) { + if ($token === $value || 0 === strpos($token, $value . '=')) { + return true; + } + } + } + + return false; + } + + /** + * 获取原始选项的值 + * @param string|array $values 需要检查的值 + * @param mixed $default 默认值 + * @return mixed The option value + */ + public function getParameterOption($values, $default = false) + { + $values = (array) $values; + $tokens = $this->tokens; + + while (0 < count($tokens)) { + $token = array_shift($tokens); + + foreach ($values as $value) { + if ($token === $value || 0 === strpos($token, $value . '=')) { + if (false !== $pos = strpos($token, '=')) { + return substr($token, $pos + 1); + } + + return array_shift($tokens); + } + } + } + + return $default; + } + + /** + * 验证输入 + * @throws \RuntimeException + */ + public function validate() + { + if (count($this->arguments) < $this->definition->getArgumentRequiredCount()) { + throw new \RuntimeException('Not enough arguments.'); + } + } + + /** + * 检查输入是否是交互的 + * @return bool + */ + public function isInteractive() + { + return $this->interactive; + } + + /** + * 设置输入的交互 + * @param bool + */ + public function setInteractive($interactive) + { + $this->interactive = (bool) $interactive; + } + + /** + * 获取所有的参数 + * @return Argument[] + */ + public function getArguments() + { + return array_merge($this->definition->getArgumentDefaults(), $this->arguments); + } + + /** + * 根据名称获取参数 + * @param string $name 参数名 + * @return mixed + * @throws \InvalidArgumentException + */ + public function getArgument($name) + { + if (!$this->definition->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + return isset($this->arguments[$name]) ? $this->arguments[$name] : $this->definition->getArgument($name) + ->getDefault(); + } + + /** + * 设置参数的值 + * @param string $name 参数名 + * @param string $value 值 + * @throws \InvalidArgumentException + */ + public function setArgument($name, $value) + { + if (!$this->definition->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $this->arguments[$name] = $value; + } + + /** + * 检查是否存在某个参数 + * @param string|int $name 参数名或位置 + * @return bool + */ + public function hasArgument($name) + { + return $this->definition->hasArgument($name); + } + + /** + * 获取所有的选项 + * @return Option[] + */ + public function getOptions() + { + return array_merge($this->definition->getOptionDefaults(), $this->options); + } + + /** + * 获取选项值 + * @param string $name 选项名称 + * @return mixed + * @throws \InvalidArgumentException + */ + public function getOption($name) + { + if (!$this->definition->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + return isset($this->options[$name]) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); + } + + /** + * 设置选项值 + * @param string $name 选项名 + * @param string|bool $value 值 + * @throws \InvalidArgumentException + */ + public function setOption($name, $value) + { + if (!$this->definition->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + $this->options[$name] = $value; + } + + /** + * 是否有某个选项 + * @param string $name 选项名 + * @return bool + */ + public function hasOption($name) + { + return $this->definition->hasOption($name) && isset($this->options[$name]); + } + + /** + * 转义指令 + * @param string $token + * @return string + */ + public function escapeToken($token) + { + return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token); + } + + /** + * 返回传递给命令的参数的字符串 + * @return string + */ + public function __toString() + { + $tokens = array_map(function ($token) { + if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) { + return $match[1] . $this->escapeToken($match[2]); + } + + if ($token && '-' !== $token[0]) { + return $this->escapeToken($token); + } + + return $token; + }, $this->tokens); + + return implode(' ', $tokens); + } +} diff --git a/vendor/topthink/framework/library/think/console/LICENSE b/vendor/topthink/framework/library/think/console/LICENSE new file mode 100644 index 0000000..0abe056 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2016 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/vendor/topthink/framework/library/think/console/Output.php b/vendor/topthink/framework/library/think/console/Output.php new file mode 100644 index 0000000..65dc9fb --- /dev/null +++ b/vendor/topthink/framework/library/think/console/Output.php @@ -0,0 +1,222 @@ + +// +---------------------------------------------------------------------- + +namespace think\console; + +use Exception; +use think\console\output\Ask; +use think\console\output\Descriptor; +use think\console\output\driver\Buffer; +use think\console\output\driver\Console; +use think\console\output\driver\Nothing; +use think\console\output\Question; +use think\console\output\question\Choice; +use think\console\output\question\Confirmation; + +/** + * Class Output + * @package think\console + * + * @see \think\console\output\driver\Console::setDecorated + * @method void setDecorated($decorated) + * + * @see \think\console\output\driver\Buffer::fetch + * @method string fetch() + * + * @method void info($message) + * @method void error($message) + * @method void comment($message) + * @method void warning($message) + * @method void highlight($message) + * @method void question($message) + */ +class Output +{ + const VERBOSITY_QUIET = 0; + const VERBOSITY_NORMAL = 1; + const VERBOSITY_VERBOSE = 2; + const VERBOSITY_VERY_VERBOSE = 3; + const VERBOSITY_DEBUG = 4; + + const OUTPUT_NORMAL = 0; + const OUTPUT_RAW = 1; + const OUTPUT_PLAIN = 2; + + private $verbosity = self::VERBOSITY_NORMAL; + + /** @var Buffer|Console|Nothing */ + private $handle = null; + + protected $styles = [ + 'info', + 'error', + 'comment', + 'question', + 'highlight', + 'warning' + ]; + + public function __construct($driver = 'console') + { + $class = '\\think\\console\\output\\driver\\' . ucwords($driver); + + $this->handle = new $class($this); + } + + public function ask(Input $input, $question, $default = null, $validator = null) + { + $question = new Question($question, $default); + $question->setValidator($validator); + + return $this->askQuestion($input, $question); + } + + public function askHidden(Input $input, $question, $validator = null) + { + $question = new Question($question); + + $question->setHidden(true); + $question->setValidator($validator); + + return $this->askQuestion($input, $question); + } + + public function confirm(Input $input, $question, $default = true) + { + return $this->askQuestion($input, new Confirmation($question, $default)); + } + + /** + * {@inheritdoc} + */ + public function choice(Input $input, $question, array $choices, $default = null) + { + if (null !== $default) { + $values = array_flip($choices); + $default = $values[$default]; + } + + return $this->askQuestion($input, new Choice($question, $choices, $default)); + } + + protected function askQuestion(Input $input, Question $question) + { + $ask = new Ask($input, $this, $question); + $answer = $ask->run(); + + if ($input->isInteractive()) { + $this->newLine(); + } + + return $answer; + } + + protected function block($style, $message) + { + $this->writeln("<{$style}>{$message}"); + } + + /** + * 输出空行 + * @param int $count + */ + public function newLine($count = 1) + { + $this->write(str_repeat(PHP_EOL, $count)); + } + + /** + * 输出信息并换行 + * @param string $messages + * @param int $type + */ + public function writeln($messages, $type = self::OUTPUT_NORMAL) + { + $this->write($messages, true, $type); + } + + /** + * 输出信息 + * @param string $messages + * @param bool $newline + * @param int $type + */ + public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL) + { + $this->handle->write($messages, $newline, $type); + } + + public function renderException(\Exception $e) + { + $this->handle->renderException($e); + } + + /** + * {@inheritdoc} + */ + public function setVerbosity($level) + { + $this->verbosity = (int) $level; + } + + /** + * {@inheritdoc} + */ + public function getVerbosity() + { + return $this->verbosity; + } + + public function isQuiet() + { + return self::VERBOSITY_QUIET === $this->verbosity; + } + + public function isVerbose() + { + return self::VERBOSITY_VERBOSE <= $this->verbosity; + } + + public function isVeryVerbose() + { + return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity; + } + + public function isDebug() + { + return self::VERBOSITY_DEBUG <= $this->verbosity; + } + + public function describe($object, array $options = []) + { + $descriptor = new Descriptor(); + $options = array_merge([ + 'raw_text' => false, + ], $options); + + $descriptor->describe($this, $object, $options); + } + + public function __call($method, $args) + { + if (in_array($method, $this->styles)) { + array_unshift($args, $method); + return call_user_func_array([$this, 'block'], $args); + } + + if ($this->handle && method_exists($this->handle, $method)) { + return call_user_func_array([$this->handle, $method], $args); + } else { + throw new Exception('method not exists:' . __CLASS__ . '->' . $method); + } + } + +} diff --git a/vendor/topthink/framework/library/think/console/Table.php b/vendor/topthink/framework/library/think/console/Table.php new file mode 100644 index 0000000..9e28e26 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/Table.php @@ -0,0 +1,281 @@ + +// +---------------------------------------------------------------------- + +namespace think\console; + +class Table +{ + const ALIGN_LEFT = 1; + const ALIGN_RIGHT = 0; + const ALIGN_CENTER = 2; + + /** + * 头信息数据 + * @var array + */ + protected $header = []; + + /** + * 头部对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER + * @var int + */ + protected $headerAlign = 1; + + /** + * 表格数据(二维数组) + * @var array + */ + protected $rows = []; + + /** + * 单元格对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER + * @var int + */ + protected $cellAlign = 1; + + /** + * 单元格宽度信息 + * @var array + */ + protected $colWidth = []; + + /** + * 表格输出样式 + * @var string + */ + protected $style = 'default'; + + /** + * 表格样式定义 + * @var array + */ + protected $format = [ + 'compact' => [], + 'default' => [ + 'top' => ['+', '-', '+', '+'], + 'cell' => ['|', ' ', '|', '|'], + 'middle' => ['+', '-', '+', '+'], + 'bottom' => ['+', '-', '+', '+'], + 'cross-top' => ['+', '-', '-', '+'], + 'cross-bottom' => ['+', '-', '-', '+'], + ], + 'markdown' => [ + 'top' => [' ', ' ', ' ', ' '], + 'cell' => ['|', ' ', '|', '|'], + 'middle' => ['|', '-', '|', '|'], + 'bottom' => [' ', ' ', ' ', ' '], + 'cross-top' => ['|', ' ', ' ', '|'], + 'cross-bottom' => ['|', ' ', ' ', '|'], + ], + 'borderless' => [ + 'top' => ['=', '=', ' ', '='], + 'cell' => [' ', ' ', ' ', ' '], + 'middle' => ['=', '=', ' ', '='], + 'bottom' => ['=', '=', ' ', '='], + 'cross-top' => ['=', '=', ' ', '='], + 'cross-bottom' => ['=', '=', ' ', '='], + ], + 'box' => [ + 'top' => ['┌', '─', '┬', '┐'], + 'cell' => ['│', ' ', '│', '│'], + 'middle' => ['├', '─', '┼', '┤'], + 'bottom' => ['└', '─', '┴', '┘'], + 'cross-top' => ['├', '─', '┴', '┤'], + 'cross-bottom' => ['├', '─', '┬', '┤'], + ], + 'box-double' => [ + 'top' => ['╔', '═', '╤', '╗'], + 'cell' => ['║', ' ', '│', '║'], + 'middle' => ['╠', '─', '╪', '╣'], + 'bottom' => ['╚', '═', '╧', '╝'], + 'cross-top' => ['╠', '═', '╧', '╣'], + 'cross-bottom' => ['╠', '═', '╤', '╣'], + ], + ]; + + /** + * 设置表格头信息 以及对齐方式 + * @access public + * @param array $header 要输出的Header信息 + * @param int $align 对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER + * @return void + */ + public function setHeader(array $header, $align = self::ALIGN_LEFT) + { + $this->header = $header; + $this->headerAlign = $align; + + $this->checkColWidth($header); + } + + /** + * 设置输出表格数据 及对齐方式 + * @access public + * @param array $rows 要输出的表格数据(二维数组) + * @param int $align 对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER + * @return void + */ + public function setRows(array $rows, $align = self::ALIGN_LEFT) + { + $this->rows = $rows; + $this->cellAlign = $align; + + foreach ($rows as $row) { + $this->checkColWidth($row); + } + } + + /** + * 检查列数据的显示宽度 + * @access public + * @param mixed $row 行数据 + * @return void + */ + protected function checkColWidth($row) + { + if (is_array($row)) { + foreach ($row as $key => $cell) { + if (!isset($this->colWidth[$key]) || strlen($cell) > $this->colWidth[$key]) { + $this->colWidth[$key] = strlen($cell); + } + } + } + } + + /** + * 增加一行表格数据 + * @access public + * @param mixed $row 行数据 + * @param bool $first 是否在开头插入 + * @return void + */ + public function addRow($row, $first = false) + { + if ($first) { + array_unshift($this->rows, $row); + } else { + $this->rows[] = $row; + } + + $this->checkColWidth($row); + } + + /** + * 设置输出表格的样式 + * @access public + * @param string $style 样式名 + * @return void + */ + public function setStyle($style) + { + $this->style = isset($this->format[$style]) ? $style : 'default'; + } + + /** + * 输出分隔行 + * @access public + * @param string $pos 位置 + * @return string + */ + protected function renderSeparator($pos) + { + $style = $this->getStyle($pos); + $array = []; + + foreach ($this->colWidth as $width) { + $array[] = str_repeat($style[1], $width + 2); + } + + return $style[0] . implode($style[2], $array) . $style[3] . PHP_EOL; + } + + /** + * 输出表格头部 + * @access public + * @return string + */ + protected function renderHeader() + { + $style = $this->getStyle('cell'); + $content = $this->renderSeparator('top'); + + foreach ($this->header as $key => $header) { + $array[] = ' ' . str_pad($header, $this->colWidth[$key], $style[1], $this->headerAlign); + } + + if (!empty($array)) { + $content .= $style[0] . implode(' ' . $style[2], $array) . ' ' . $style[3] . PHP_EOL; + + if ($this->rows) { + $content .= $this->renderSeparator('middle'); + } + } + + return $content; + } + + protected function getStyle($style) + { + if ($this->format[$this->style]) { + $style = $this->format[$this->style][$style]; + } else { + $style = [' ', ' ', ' ', ' ']; + } + + return $style; + } + + /** + * 输出表格 + * @access public + * @param array $dataList 表格数据 + * @return string + */ + public function render($dataList = []) + { + if ($dataList) { + $this->setRows($dataList); + } + + // 输出头部 + $content = $this->renderHeader(); + $style = $this->getStyle('cell'); + + if ($this->rows) { + foreach ($this->rows as $row) { + if (is_string($row) && '-' === $row) { + $content .= $this->renderSeparator('middle'); + } elseif (is_scalar($row)) { + $content .= $this->renderSeparator('cross-top'); + $array = str_pad($row, 3 * (count($this->colWidth) - 1) + array_reduce($this->colWidth, function ($a, $b) { + return $a + $b; + })); + + $content .= $style[0] . ' ' . $array . ' ' . $style[3] . PHP_EOL; + $content .= $this->renderSeparator('cross-bottom'); + } else { + $array = []; + + foreach ($row as $key => $val) { + $array[] = ' ' . str_pad($val, $this->colWidth[$key], ' ', $this->cellAlign); + } + + $content .= $style[0] . implode(' ' . $style[2], $array) . ' ' . $style[3] . PHP_EOL; + + } + } + } + + $content .= $this->renderSeparator('bottom'); + + return $content; + } +} diff --git a/vendor/topthink/framework/library/think/console/bin/README.md b/vendor/topthink/framework/library/think/console/bin/README.md new file mode 100644 index 0000000..9acc52f --- /dev/null +++ b/vendor/topthink/framework/library/think/console/bin/README.md @@ -0,0 +1 @@ +console 工具使用 hiddeninput.exe 在 windows 上隐藏密码输入,该二进制文件由第三方提供,相关源码和其他细节可以在 [Hidden Input](https://github.com/Seldaek/hidden-input) 找到。 diff --git a/vendor/topthink/framework/library/think/console/bin/hiddeninput.exe b/vendor/topthink/framework/library/think/console/bin/hiddeninput.exe new file mode 100644 index 0000000..c8cf65e Binary files /dev/null and b/vendor/topthink/framework/library/think/console/bin/hiddeninput.exe differ diff --git a/vendor/topthink/framework/library/think/console/command/Build.php b/vendor/topthink/framework/library/think/console/command/Build.php new file mode 100644 index 0000000..88a5bf8 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/Build.php @@ -0,0 +1,59 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; +use think\facade\App; +use think\facade\Build as AppBuild; + +class Build extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->setName('build') + ->setDefinition([ + new Option('config', null, Option::VALUE_OPTIONAL, "build.php path"), + new Option('module', null, Option::VALUE_OPTIONAL, "module name"), + ]) + ->setDescription('Build Application Dirs'); + } + + protected function execute(Input $input, Output $output) + { + if ($input->hasOption('module')) { + AppBuild::module($input->getOption('module')); + $output->writeln("Successed"); + return; + } + + if ($input->hasOption('config')) { + $build = include $input->getOption('config'); + } else { + $build = include App::getAppPath() . 'build.php'; + } + + if (empty($build)) { + $output->writeln("Build Config Is Empty"); + return; + } + + AppBuild::run($build); + $output->writeln("Successed"); + + } +} diff --git a/vendor/topthink/framework/library/think/console/command/Clear.php b/vendor/topthink/framework/library/think/console/command/Clear.php new file mode 100644 index 0000000..1442575 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/Clear.php @@ -0,0 +1,70 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; +use think\facade\App; +use think\facade\Cache; + +class Clear extends Command +{ + protected function configure() + { + // 指令配置 + $this + ->setName('clear') + ->addOption('path', 'd', Option::VALUE_OPTIONAL, 'path to clear', null) + ->addOption('cache', 'c', Option::VALUE_NONE, 'clear cache file') + ->addOption('route', 'u', Option::VALUE_NONE, 'clear route cache') + ->addOption('log', 'l', Option::VALUE_NONE, 'clear log file') + ->addOption('dir', 'r', Option::VALUE_NONE, 'clear empty dir') + ->setDescription('Clear runtime file'); + } + + protected function execute(Input $input, Output $output) + { + if ($input->getOption('route')) { + Cache::clear('route_cache'); + } else { + if ($input->getOption('cache')) { + $path = App::getRuntimePath() . 'cache'; + } elseif ($input->getOption('log')) { + $path = App::getRuntimePath() . 'log'; + } else { + $path = $input->getOption('path') ?: App::getRuntimePath(); + } + + $rmdir = $input->getOption('dir') ? true : false; + $this->clear(rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, $rmdir); + } + + $output->writeln("Clear Successed"); + } + + protected function clear($path, $rmdir) + { + $files = is_dir($path) ? scandir($path) : []; + + foreach ($files as $file) { + if ('.' != $file && '..' != $file && is_dir($path . $file)) { + array_map('unlink', glob($path . $file . DIRECTORY_SEPARATOR . '*.*')); + if ($rmdir) { + rmdir($path . $file); + } + } elseif ('.gitignore' != $file && is_file($path . $file)) { + unlink($path . $file); + } + } + } +} diff --git a/vendor/topthink/framework/library/think/console/command/Help.php b/vendor/topthink/framework/library/think/console/command/Help.php new file mode 100644 index 0000000..f1b63b4 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/Help.php @@ -0,0 +1,68 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Argument as InputArgument; +use think\console\input\Option as InputOption; +use think\console\Output; + +class Help extends Command +{ + private $command; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->ignoreValidationErrors(); + + $this->setName('help')->setDefinition([ + new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), + ])->setDescription('Displays help for a command')->setHelp(<<%command.name% command displays help for a given command: + + php %command.full_name% list + +To display the list of available commands, please use the list command. +EOF + ); + } + + /** + * Sets the command. + * @param Command $command The command to set + */ + public function setCommand(Command $command) + { + $this->command = $command; + } + + /** + * {@inheritdoc} + */ + protected function execute(Input $input, Output $output) + { + if (null === $this->command) { + $this->command = $this->getConsole()->find($input->getArgument('command_name')); + } + + $output->describe($this->command, [ + 'raw_text' => $input->getOption('raw'), + ]); + + $this->command = null; + } +} diff --git a/vendor/topthink/framework/library/think/console/command/Lists.php b/vendor/topthink/framework/library/think/console/command/Lists.php new file mode 100644 index 0000000..6eb856c --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/Lists.php @@ -0,0 +1,73 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Argument as InputArgument; +use think\console\input\Definition as InputDefinition; +use think\console\input\Option as InputOption; +use think\console\Output; + +class Lists extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->setName('list')->setDefinition($this->createDefinition())->setDescription('Lists commands')->setHelp(<<%command.name% command lists all commands: + + php %command.full_name% + +You can also display the commands for a specific namespace: + + php %command.full_name% test + +It's also possible to get raw list of commands (useful for embedding command runner): + + php %command.full_name% --raw +EOF + ); + } + + /** + * {@inheritdoc} + */ + public function getNativeDefinition() + { + return $this->createDefinition(); + } + + /** + * {@inheritdoc} + */ + protected function execute(Input $input, Output $output) + { + $output->describe($this->getConsole(), [ + 'raw_text' => $input->getOption('raw'), + 'namespace' => $input->getArgument('namespace'), + ]); + } + + /** + * {@inheritdoc} + */ + private function createDefinition() + { + return new InputDefinition([ + new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), + ]); + } +} diff --git a/vendor/topthink/framework/library/think/console/command/Make.php b/vendor/topthink/framework/library/think/console/command/Make.php new file mode 100644 index 0000000..2f20954 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/Make.php @@ -0,0 +1,110 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Argument; +use think\console\Output; +use think\facade\App; +use think\facade\Config; +use think\facade\Env; + +abstract class Make extends Command +{ + protected $type; + + abstract protected function getStub(); + + protected function configure() + { + $this->addArgument('name', Argument::REQUIRED, "The name of the class"); + } + + protected function execute(Input $input, Output $output) + { + + $name = trim($input->getArgument('name')); + + $classname = $this->getClassName($name); + + $pathname = $this->getPathName($classname); + + if (is_file($pathname)) { + $output->writeln('' . $this->type . ' already exists!'); + return false; + } + + if (!is_dir(dirname($pathname))) { + mkdir(dirname($pathname), 0755, true); + } + + file_put_contents($pathname, $this->buildClass($classname)); + + $output->writeln('' . $this->type . ' created successfully.'); + + } + + protected function buildClass($name) + { + $stub = file_get_contents($this->getStub()); + + $namespace = trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\'); + + $class = str_replace($namespace . '\\', '', $name); + + return str_replace(['{%className%}', '{%actionSuffix%}', '{%namespace%}', '{%app_namespace%}'], [ + $class, + Config::get('action_suffix'), + $namespace, + App::getNamespace(), + ], $stub); + } + + protected function getPathName($name) + { + $name = str_replace(App::getNamespace() . '\\', '', $name); + + return Env::get('app_path') . ltrim(str_replace('\\', '/', $name), '/') . '.php'; + } + + protected function getClassName($name) + { + $appNamespace = App::getNamespace(); + + if (strpos($name, $appNamespace . '\\') !== false) { + return $name; + } + + if (Config::get('app_multi_module')) { + if (strpos($name, '/')) { + list($module, $name) = explode('/', $name, 2); + } else { + $module = 'common'; + } + } else { + $module = null; + } + + if (strpos($name, '/') !== false) { + $name = str_replace('/', '\\', $name); + } + + return $this->getNamespace($appNamespace, $module) . '\\' . $name; + } + + protected function getNamespace($appNamespace, $module) + { + return $module ? ($appNamespace . '\\' . $module) : $appNamespace; + } + +} diff --git a/vendor/topthink/framework/library/think/console/command/RouteList.php b/vendor/topthink/framework/library/think/console/command/RouteList.php new file mode 100644 index 0000000..0405c31 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/RouteList.php @@ -0,0 +1,130 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Argument; +use think\console\input\Option; +use think\console\Output; +use think\console\Table; +use think\Container; + +class RouteList extends Command +{ + protected $sortBy = [ + 'rule' => 0, + 'route' => 1, + 'method' => 2, + 'name' => 3, + 'domain' => 4, + ]; + + protected function configure() + { + $this->setName('route:list') + ->addArgument('style', Argument::OPTIONAL, "the style of the table.", 'default') + ->addOption('sort', 's', Option::VALUE_OPTIONAL, 'order by rule name.', 0) + ->addOption('more', 'm', Option::VALUE_NONE, 'show route options.') + ->setDescription('show route list.'); + } + + protected function execute(Input $input, Output $output) + { + $filename = Container::get('app')->getRuntimePath() . 'route_list.php'; + + if (is_file($filename)) { + unlink($filename); + } + + $content = $this->getRouteList(); + file_put_contents($filename, 'Route List' . PHP_EOL . $content); + } + + protected function getRouteList() + { + Container::get('route')->setTestMode(true); + // 路由检测 + $path = Container::get('app')->getRoutePath(); + + $files = is_dir($path) ? scandir($path) : []; + + foreach ($files as $file) { + if (strpos($file, '.php')) { + $filename = $path . DIRECTORY_SEPARATOR . $file; + // 导入路由配置 + $rules = include $filename; + + if (is_array($rules)) { + Container::get('route')->import($rules); + } + } + } + + if (Container::get('config')->get('route_annotation')) { + $suffix = Container::get('config')->get('controller_suffix') || Container::get('config')->get('class_suffix'); + + include Container::get('build')->buildRoute($suffix); + } + + $table = new Table(); + + if ($this->input->hasOption('more')) { + $header = ['Rule', 'Route', 'Method', 'Name', 'Domain', 'Option', 'Pattern']; + } else { + $header = ['Rule', 'Route', 'Method', 'Name', 'Domain']; + } + + $table->setHeader($header); + + $routeList = Container::get('route')->getRuleList(); + $rows = []; + + foreach ($routeList as $domain => $items) { + foreach ($items as $item) { + $item['route'] = $item['route'] instanceof \Closure ? '' : $item['route']; + + if ($this->input->hasOption('more')) { + $item = [$item['rule'], $item['route'], $item['method'], $item['name'], $domain, json_encode($item['option']), json_encode($item['pattern'])]; + } else { + $item = [$item['rule'], $item['route'], $item['method'], $item['name'], $domain]; + } + + $rows[] = $item; + } + } + + if ($this->input->getOption('sort')) { + $sort = $this->input->getOption('sort'); + + if (isset($this->sortBy[$sort])) { + $sort = $this->sortBy[$sort]; + } + + uasort($rows, function ($a, $b) use ($sort) { + $itemA = isset($a[$sort]) ? $a[$sort] : null; + $itemB = isset($b[$sort]) ? $b[$sort] : null; + + return strcasecmp($itemA, $itemB); + }); + } + + $table->setRows($rows); + + if ($this->input->getArgument('style')) { + $style = $this->input->getArgument('style'); + $table->setStyle($style); + } + + return $this->table($table); + } + +} diff --git a/vendor/topthink/framework/library/think/console/command/RunServer.php b/vendor/topthink/framework/library/think/console/command/RunServer.php new file mode 100644 index 0000000..2e028dc --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/RunServer.php @@ -0,0 +1,53 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; +use think\facade\App; + +class RunServer extends Command +{ + public function configure() + { + $this->setName('run') + ->addOption('host', 'H', Option::VALUE_OPTIONAL, + 'The host to server the application on', '127.0.0.1') + ->addOption('port', 'p', Option::VALUE_OPTIONAL, + 'The port to server the application on', 8000) + ->addOption('root', 'r', Option::VALUE_OPTIONAL, + 'The document root of the application', App::getRootPath() . 'public') + ->setDescription('PHP Built-in Server for ThinkPHP'); + } + + public function execute(Input $input, Output $output) + { + $host = $input->getOption('host'); + $port = $input->getOption('port'); + $root = $input->getOption('root'); + + $command = sprintf( + 'php -S %s:%d -t %s %s', + $host, + $port, + escapeshellarg($root), + escapeshellarg($root . DIRECTORY_SEPARATOR . 'router.php') + ); + + $output->writeln(sprintf('ThinkPHP Development server is started On ', $host, $port)); + $output->writeln(sprintf('You can exit with `CTRL-C`')); + $output->writeln(sprintf('Document root is: %s', $root)); + passthru($command); + } + +} diff --git a/vendor/topthink/framework/library/think/console/command/Version.php b/vendor/topthink/framework/library/think/console/command/Version.php new file mode 100644 index 0000000..ee7eca9 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/Version.php @@ -0,0 +1,31 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\Output; +use think\facade\App; + +class Version extends Command +{ + protected function configure() + { + // 指令配置 + $this->setName('version') + ->setDescription('show thinkphp framework version'); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('v' . App::version()); + } +} diff --git a/vendor/topthink/framework/library/think/console/command/make/Command.php b/vendor/topthink/framework/library/think/console/command/make/Command.php new file mode 100644 index 0000000..b539eb2 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/make/Command.php @@ -0,0 +1,56 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\console\command\Make; +use think\console\input\Argument; +use think\facade\App; + +class Command extends Make +{ + protected $type = "Command"; + + protected function configure() + { + parent::configure(); + $this->setName('make:command') + ->addArgument('commandName', Argument::OPTIONAL, "The name of the command") + ->setDescription('Create a new command class'); + } + + protected function buildClass($name) + { + $commandName = $this->input->getArgument('commandName') ?: strtolower(basename($name)); + $namespace = trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\'); + + $class = str_replace($namespace . '\\', '', $name); + $stub = file_get_contents($this->getStub()); + + return str_replace(['{%commandName%}', '{%className%}', '{%namespace%}', '{%app_namespace%}'], [ + $commandName, + $class, + $namespace, + App::getNamespace(), + ], $stub); + } + + protected function getStub() + { + return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'command.stub'; + } + + protected function getNamespace($appNamespace, $module) + { + return $appNamespace . '\\command'; + } + +} diff --git a/vendor/topthink/framework/library/think/console/command/make/Controller.php b/vendor/topthink/framework/library/think/console/command/make/Controller.php new file mode 100644 index 0000000..2a6ab77 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/make/Controller.php @@ -0,0 +1,56 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\console\command\Make; +use think\console\input\Option; +use think\facade\Config; + +class Controller extends Make +{ + protected $type = "Controller"; + + protected function configure() + { + parent::configure(); + $this->setName('make:controller') + ->addOption('api', null, Option::VALUE_NONE, 'Generate an api controller class.') + ->addOption('plain', null, Option::VALUE_NONE, 'Generate an empty controller class.') + ->setDescription('Create a new resource controller class'); + } + + protected function getStub() + { + $stubPath = __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR; + + if ($this->input->getOption('api')) { + return $stubPath . 'controller.api.stub'; + } + + if ($this->input->getOption('plain')) { + return $stubPath . 'controller.plain.stub'; + } + + return $stubPath . 'controller.stub'; + } + + protected function getClassName($name) + { + return parent::getClassName($name) . (Config::get('controller_suffix') ? ucfirst(Config::get('url_controller_layer')) : ''); + } + + protected function getNamespace($appNamespace, $module) + { + return parent::getNamespace($appNamespace, $module) . '\controller'; + } + +} diff --git a/vendor/topthink/framework/library/think/console/command/make/Middleware.php b/vendor/topthink/framework/library/think/console/command/make/Middleware.php new file mode 100644 index 0000000..bfe821b --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/make/Middleware.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\console\command\Make; + +class Middleware extends Make +{ + protected $type = "Middleware"; + + protected function configure() + { + parent::configure(); + $this->setName('make:middleware') + ->setDescription('Create a new middleware class'); + } + + protected function getStub() + { + return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'middleware.stub'; + } + + protected function getNamespace($appNamespace, $module) + { + return parent::getNamespace($appNamespace, 'http') . '\middleware'; + } +} diff --git a/vendor/topthink/framework/library/think/console/command/make/Model.php b/vendor/topthink/framework/library/think/console/command/make/Model.php new file mode 100644 index 0000000..03e6b3f --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/make/Model.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\console\command\Make; + +class Model extends Make +{ + protected $type = "Model"; + + protected function configure() + { + parent::configure(); + $this->setName('make:model') + ->setDescription('Create a new model class'); + } + + protected function getStub() + { + return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'model.stub'; + } + + protected function getNamespace($appNamespace, $module) + { + return parent::getNamespace($appNamespace, $module) . '\model'; + } +} diff --git a/vendor/topthink/framework/library/think/console/command/make/Validate.php b/vendor/topthink/framework/library/think/console/command/make/Validate.php new file mode 100644 index 0000000..89830ad --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/make/Validate.php @@ -0,0 +1,39 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\console\command\Make; + +class Validate extends Make +{ + protected $type = "Validate"; + + protected function configure() + { + parent::configure(); + $this->setName('make:validate') + ->setDescription('Create a validate class'); + } + + protected function getStub() + { + $stubPath = __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR; + + return $stubPath . 'validate.stub'; + } + + protected function getNamespace($appNamespace, $module) + { + return parent::getNamespace($appNamespace, $module) . '\validate'; + } + +} diff --git a/vendor/topthink/framework/library/think/console/command/make/stubs/command.stub b/vendor/topthink/framework/library/think/console/command/make/stubs/command.stub new file mode 100644 index 0000000..d2c7c1e --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/make/stubs/command.stub @@ -0,0 +1,24 @@ +setName('{%commandName%}'); + // 设置参数 + + } + + protected function execute(Input $input, Output $output) + { + // 指令输出 + $output->writeln('{%commandName%}'); + } +} diff --git a/vendor/topthink/framework/library/think/console/command/make/stubs/controller.api.stub b/vendor/topthink/framework/library/think/console/command/make/stubs/controller.api.stub new file mode 100644 index 0000000..54ec059 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/make/stubs/controller.api.stub @@ -0,0 +1,64 @@ + ['规则1','规则2'...] + * + * @var array + */ + protected $rule = []; + + /** + * 定义错误信息 + * 格式:'字段名.规则名' => '错误信息' + * + * @var array + */ + protected $message = []; +} diff --git a/vendor/topthink/framework/library/think/console/command/optimize/Autoload.php b/vendor/topthink/framework/library/think/console/command/optimize/Autoload.php new file mode 100644 index 0000000..b51fd25 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/optimize/Autoload.php @@ -0,0 +1,279 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\console\Command; +use think\console\Input; +use think\console\Output; +use think\Container; + +class Autoload extends Command +{ + protected function configure() + { + $this->setName('optimize:autoload') + ->setDescription('Optimizes PSR0 and PSR4 packages to be loaded with classmaps too, good for production.'); + } + + protected function execute(Input $input, Output $output) + { + + $classmapFile = <<getNamespace() . '\\' => realpath(rtrim($app->getAppPath())), + 'think\\' => $app->getThinkPath() . 'library/think', + 'traits\\' => $app->getThinkPath() . 'library/traits', + '' => realpath(rtrim($app->getRootPath() . 'extend')), + ]; + + krsort($namespacesToScan); + $classMap = []; + foreach ($namespacesToScan as $namespace => $dir) { + + if (!is_dir($dir)) { + continue; + } + + $namespaceFilter = '' === $namespace ? null : $namespace; + $classMap = $this->addClassMapCode($dir, $namespaceFilter, $classMap); + } + + ksort($classMap); + foreach ($classMap as $class => $code) { + $classmapFile .= ' ' . var_export($class, true) . ' => ' . $code; + } + $classmapFile .= "];\n"; + $runtimePath = $app->getRuntimePath(); + if (!is_dir($runtimePath)) { + @mkdir($runtimePath, 0755, true); + } + + file_put_contents($runtimePath . 'classmap.php', $classmapFile); + + $output->writeln('Succeed!'); + } + + protected function addClassMapCode($dir, $namespace, $classMap) + { + foreach ($this->createMap($dir, $namespace) as $class => $path) { + + $pathCode = $this->getPathCode($path) . ",\n"; + + if (!isset($classMap[$class])) { + $classMap[$class] = $pathCode; + } elseif ($classMap[$class] !== $pathCode && !preg_match('{/(test|fixture|example|stub)s?/}i', strtr($classMap[$class] . ' ' . $path, '\\', '/'))) { + $this->output->writeln( + 'Warning: Ambiguous class resolution, "' . $class . '"' . + ' was found in both "' . str_replace(["',\n"], [ + '', + ], $classMap[$class]) . '" and "' . $path . '", the first will be used.' + ); + } + } + return $classMap; + } + + protected function getPathCode($path) + { + $baseDir = ''; + $app = Container::get('app'); + $appPath = $this->normalizePath(realpath($app->getAppPath())); + $libPath = $this->normalizePath(realpath($app->getThinkPath() . 'library')); + $extendPath = $this->normalizePath(realpath($app->getRootPath() . 'extend')); + $path = $this->normalizePath($path); + + if (strpos($path, $libPath . '/') === 0) { + $path = substr($path, strlen($app->getThinkPath() . 'library')); + $baseDir = "'" . $libPath . "/'"; + } elseif (strpos($path, $appPath . '/') === 0) { + $path = substr($path, strlen($appPath) + 1); + $baseDir = "'" . $appPath . "/'"; + } elseif (strpos($path, $extendPath . '/') === 0) { + $path = substr($path, strlen($extendPath) + 1); + $baseDir = "'" . $extendPath . "/'"; + } + + if (false !== $path) { + $baseDir .= " . "; + } + + return $baseDir . ((false !== $path) ? var_export($path, true) : ""); + } + + protected function normalizePath($path) + { + $parts = []; + $path = strtr($path, '\\', '/'); + $prefix = ''; + $absolute = false; + + if (preg_match('{^([0-9a-z]+:(?://(?:[a-z]:)?)?)}i', $path, $match)) { + $prefix = $match[1]; + $path = substr($path, strlen($prefix)); + } + + if (substr($path, 0, 1) === '/') { + $absolute = true; + $path = substr($path, 1); + } + + $up = false; + foreach (explode('/', $path) as $chunk) { + if ('..' === $chunk && ($absolute || $up)) { + array_pop($parts); + $up = !(empty($parts) || '..' === end($parts)); + } elseif ('.' !== $chunk && '' !== $chunk) { + $parts[] = $chunk; + $up = '..' !== $chunk; + } + } + + return $prefix . ($absolute ? '/' : '') . implode('/', $parts); + } + + protected function createMap($path, $namespace = null) + { + if (is_string($path)) { + if (is_file($path)) { + $path = [new \SplFileInfo($path)]; + } elseif (is_dir($path)) { + + $objects = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path), \RecursiveIteratorIterator::SELF_FIRST); + + $path = []; + + /** @var \SplFileInfo $object */ + foreach ($objects as $object) { + if ($object->isFile() && $object->getExtension() == 'php') { + $path[] = $object; + } + } + } else { + throw new \RuntimeException( + 'Could not scan for classes inside "' . $path . + '" which does not appear to be a file nor a folder' + ); + } + } + + $map = []; + + /** @var \SplFileInfo $file */ + foreach ($path as $file) { + $filePath = $file->getRealPath(); + + if (pathinfo($filePath, PATHINFO_EXTENSION) != 'php') { + continue; + } + + $classes = $this->findClasses($filePath); + + foreach ($classes as $class) { + if (null !== $namespace && 0 !== strpos($class, $namespace)) { + continue; + } + + if (!isset($map[$class])) { + $map[$class] = $filePath; + } elseif ($map[$class] !== $filePath && !preg_match('{/(test|fixture|example|stub)s?/}i', strtr($map[$class] . ' ' . $filePath, '\\', '/'))) { + $this->output->writeln( + 'Warning: Ambiguous class resolution, "' . $class . '"' . + ' was found in both "' . $map[$class] . '" and "' . $filePath . '", the first will be used.' + ); + } + } + } + + return $map; + } + + protected function findClasses($path) + { + $extraTypes = '|trait'; + + $contents = @php_strip_whitespace($path); + if (!$contents) { + if (!file_exists($path)) { + $message = 'File at "%s" does not exist, check your classmap definitions'; + } elseif (!is_readable($path)) { + $message = 'File at "%s" is not readable, check its permissions'; + } elseif ('' === trim(file_get_contents($path))) { + return []; + } else { + $message = 'File at "%s" could not be parsed as PHP, it may be binary or corrupted'; + } + $error = error_get_last(); + if (isset($error['message'])) { + $message .= PHP_EOL . 'The following message may be helpful:' . PHP_EOL . $error['message']; + } + throw new \RuntimeException(sprintf($message, $path)); + } + + if (!preg_match('{\b(?:class|interface' . $extraTypes . ')\s}i', $contents)) { + return []; + } + + // strip heredocs/nowdocs + $contents = preg_replace('{<<<\s*(\'?)(\w+)\\1(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)\\2(?=\r\n|\n|\r|;)}s', 'null', $contents); + // strip strings + $contents = preg_replace('{"[^"\\\\]*+(\\\\.[^"\\\\]*+)*+"|\'[^\'\\\\]*+(\\\\.[^\'\\\\]*+)*+\'}s', 'null', $contents); + // strip leading non-php code if needed + if (substr($contents, 0, 2) !== '.+<\?}s', '?>'); + if (false !== $pos && false === strpos(substr($contents, $pos), '])(?Pclass|interface' . $extraTypes . ') \s++ (?P[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+) + | \b(?])(?Pnamespace) (?P\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;] + ) + }ix', $contents, $matches); + + $classes = []; + $namespace = ''; + + for ($i = 0, $len = count($matches['type']); $i < $len; $i++) { + if (!empty($matches['ns'][$i])) { + $namespace = str_replace([' ', "\t", "\r", "\n"], '', $matches['nsname'][$i]) . '\\'; + } else { + $name = $matches['name'][$i]; + if (':' === $name[0]) { + $name = 'xhp' . substr(str_replace(['-', ':'], ['_', '__'], $name), 1); + } elseif ('enum' === $matches['type'][$i]) { + $name = rtrim($name, ':'); + } + $classes[] = ltrim($namespace . $name, '\\'); + } + } + + return $classes; + } + +} diff --git a/vendor/topthink/framework/library/think/console/command/optimize/Config.php b/vendor/topthink/framework/library/think/console/command/optimize/Config.php new file mode 100644 index 0000000..da95556 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/optimize/Config.php @@ -0,0 +1,107 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\console\Command; +use think\console\Input; +use think\console\input\Argument; +use think\console\Output; +use think\Container; +use think\facade\App; + +class Config extends Command +{ + protected function configure() + { + $this->setName('optimize:config') + ->addArgument('module', Argument::OPTIONAL, 'Build module config cache .') + ->setDescription('Build config and common file cache.'); + } + + protected function execute(Input $input, Output $output) + { + if ($input->getArgument('module')) { + $module = $input->getArgument('module') . DIRECTORY_SEPARATOR; + } else { + $module = ''; + } + + $content = 'buildCacheContent($module); + $runtimePath = App::getRuntimePath(); + if (!is_dir($runtimePath . $module)) { + @mkdir($runtimePath . $module, 0755, true); + } + + file_put_contents($runtimePath . $module . 'init.php', $content); + + $output->writeln('Succeed!'); + } + + protected function buildCacheContent($module) + { + $content = '// This cache file is automatically generated at:' . date('Y-m-d H:i:s') . PHP_EOL; + $path = realpath(App::getAppPath() . $module) . DIRECTORY_SEPARATOR; + if ($module) { + $configPath = is_dir($path . 'config') ? $path . 'config' : App::getConfigPath() . $module; + } else { + $configPath = App::getConfigPath(); + } + $ext = App::getConfigExt(); + $config = Container::get('config'); + + $files = is_dir($configPath) ? scandir($configPath) : []; + + foreach ($files as $file) { + if ('.' . pathinfo($file, PATHINFO_EXTENSION) === $ext) { + $filename = $configPath . DIRECTORY_SEPARATOR . $file; + $config->load($filename, pathinfo($file, PATHINFO_FILENAME)); + } + } + + // 加载行为扩展文件 + if (is_file($path . 'tags.php')) { + $tags = include $path . 'tags.php'; + if (is_array($tags)) { + $content .= PHP_EOL . '\think\facade\Hook::import(' . (var_export($tags, true)) . ');' . PHP_EOL; + } + } + + // 加载公共文件 + if (is_file($path . 'common.php')) { + $common = substr(php_strip_whitespace($path . 'common.php'), 6); + if ($common) { + $content .= PHP_EOL . $common . PHP_EOL; + } + } + + if ('' == $module) { + $content .= PHP_EOL . substr(php_strip_whitespace(App::getThinkPath() . 'helper.php'), 6) . PHP_EOL; + + if (is_file($path . 'middleware.php')) { + $middleware = include $path . 'middleware.php'; + if (is_array($middleware)) { + $content .= PHP_EOL . '\think\Container::get("middleware")->import(' . var_export($middleware, true) . ');' . PHP_EOL; + } + } + } + + if (is_file($path . 'provider.php')) { + $provider = include $path . 'provider.php'; + if (is_array($provider)) { + $content .= PHP_EOL . '\think\Container::getInstance()->bindTo(' . var_export($provider, true) . ');' . PHP_EOL; + } + } + + $content .= PHP_EOL . '\think\facade\Config::set(' . var_export($config->get(), true) . ');' . PHP_EOL; + + return $content; + } +} diff --git a/vendor/topthink/framework/library/think/console/command/optimize/Route.php b/vendor/topthink/framework/library/think/console/command/optimize/Route.php new file mode 100644 index 0000000..f6dc632 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/optimize/Route.php @@ -0,0 +1,66 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\console\Command; +use think\console\Input; +use think\console\Output; +use think\Container; + +class Route extends Command +{ + protected function configure() + { + $this->setName('optimize:route') + ->setDescription('Build route cache.'); + } + + protected function execute(Input $input, Output $output) + { + $filename = Container::get('app')->getRuntimePath() . 'route.php'; + if (is_file($filename)) { + unlink($filename); + } + file_put_contents($filename, $this->buildRouteCache()); + $output->writeln('Succeed!'); + } + + protected function buildRouteCache() + { + Container::get('route')->setName([]); + Container::get('route')->setTestMode(true); + // 路由检测 + $path = Container::get('app')->getRoutePath(); + + $files = is_dir($path) ? scandir($path) : []; + + foreach ($files as $file) { + if (strpos($file, '.php')) { + $filename = $path . DIRECTORY_SEPARATOR . $file; + // 导入路由配置 + $rules = include $filename; + if (is_array($rules)) { + Container::get('route')->import($rules); + } + } + } + + if (Container::get('config')->get('route_annotation')) { + $suffix = Container::get('config')->get('controller_suffix') || Container::get('config')->get('class_suffix'); + include Container::get('build')->buildRoute($suffix); + } + + $content = 'getName(), true) . ';'; + return $content; + } + +} diff --git a/vendor/topthink/framework/library/think/console/command/optimize/Schema.php b/vendor/topthink/framework/library/think/console/command/optimize/Schema.php new file mode 100644 index 0000000..16ac83d --- /dev/null +++ b/vendor/topthink/framework/library/think/console/command/optimize/Schema.php @@ -0,0 +1,118 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; +use think\Db; +use think\facade\App; + +class Schema extends Command +{ + protected function configure() + { + $this->setName('optimize:schema') + ->addOption('db', null, Option::VALUE_REQUIRED, 'db name .') + ->addOption('table', null, Option::VALUE_REQUIRED, 'table name .') + ->addOption('module', null, Option::VALUE_REQUIRED, 'module name .') + ->setDescription('Build database schema cache.'); + } + + protected function execute(Input $input, Output $output) + { + if (!is_dir(App::getRuntimePath() . 'schema')) { + @mkdir(App::getRuntimePath() . 'schema', 0755, true); + } + + if ($input->hasOption('module')) { + $module = $input->getOption('module'); + // 读取模型 + $path = App::getAppPath() . $module . DIRECTORY_SEPARATOR . 'model'; + $list = is_dir($path) ? scandir($path) : []; + $namespace = App::getNamespace(); + + foreach ($list as $file) { + if (0 === strpos($file, '.')) { + continue; + } + $class = '\\' . $namespace . '\\' . $module . '\\model\\' . pathinfo($file, PATHINFO_FILENAME); + $this->buildModelSchema($class); + } + + $output->writeln('Succeed!'); + return; + } elseif ($input->hasOption('table')) { + $table = $input->getOption('table'); + if (false === strpos($table, '.')) { + $dbName = Db::getConfig('database'); + } + + $tables[] = $table; + } elseif ($input->hasOption('db')) { + $dbName = $input->getOption('db'); + $tables = Db::getConnection()->getTables($dbName); + } elseif (!\think\facade\Config::get('app_multi_module')) { + $namespace = App::getNamespace(); + $path = App::getAppPath() . 'model'; + $list = is_dir($path) ? scandir($path) : []; + + foreach ($list as $file) { + if (0 === strpos($file, '.')) { + continue; + } + $class = '\\' . $namespace . '\\model\\' . pathinfo($file, PATHINFO_FILENAME); + $this->buildModelSchema($class); + } + + $output->writeln('Succeed!'); + return; + } else { + $tables = Db::getConnection()->getTables(); + } + + $db = isset($dbName) ? $dbName . '.' : ''; + $this->buildDataBaseSchema($tables, $db); + + $output->writeln('Succeed!'); + } + + protected function buildModelSchema($class) + { + $reflect = new \ReflectionClass($class); + if (!$reflect->isAbstract() && $reflect->isSubclassOf('\think\Model')) { + $table = $class::getTable(); + $dbName = $class::getConfig('database'); + $content = 'getFields($table); + $content .= var_export($info, true) . ';'; + + file_put_contents(App::getRuntimePath() . 'schema' . DIRECTORY_SEPARATOR . $dbName . '.' . $table . '.php', $content); + } + } + + protected function buildDataBaseSchema($tables, $db) + { + if ('' == $db) { + $dbName = Db::getConfig('database') . '.'; + } else { + $dbName = $db; + } + + foreach ($tables as $table) { + $content = 'getFields($db . $table); + $content .= var_export($info, true) . ';'; + file_put_contents(App::getRuntimePath() . 'schema' . DIRECTORY_SEPARATOR . $dbName . $table . '.php', $content); + } + } +} diff --git a/vendor/topthink/framework/library/think/console/input/Argument.php b/vendor/topthink/framework/library/think/console/input/Argument.php new file mode 100644 index 0000000..16223bb --- /dev/null +++ b/vendor/topthink/framework/library/think/console/input/Argument.php @@ -0,0 +1,115 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\input; + +class Argument +{ + + const REQUIRED = 1; + const OPTIONAL = 2; + const IS_ARRAY = 4; + + private $name; + private $mode; + private $default; + private $description; + + /** + * 构造方法 + * @param string $name 参数名 + * @param int $mode 参数类型: self::REQUIRED 或者 self::OPTIONAL + * @param string $description 描述 + * @param mixed $default 默认值 (仅 self::OPTIONAL 类型有效) + * @throws \InvalidArgumentException + */ + public function __construct($name, $mode = null, $description = '', $default = null) + { + if (null === $mode) { + $mode = self::OPTIONAL; + } elseif (!is_int($mode) || $mode > 7 || $mode < 1) { + throw new \InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode)); + } + + $this->name = $name; + $this->mode = $mode; + $this->description = $description; + + $this->setDefault($default); + } + + /** + * 获取参数名 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 是否必须 + * @return bool + */ + public function isRequired() + { + return self::REQUIRED === (self::REQUIRED & $this->mode); + } + + /** + * 该参数是否接受数组 + * @return bool + */ + public function isArray() + { + return self::IS_ARRAY === (self::IS_ARRAY & $this->mode); + } + + /** + * 设置默认值 + * @param mixed $default 默认值 + * @throws \LogicException + */ + public function setDefault($default = null) + { + if (self::REQUIRED === $this->mode && null !== $default) { + throw new \LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.'); + } + + if ($this->isArray()) { + if (null === $default) { + $default = []; + } elseif (!is_array($default)) { + throw new \LogicException('A default value for an array argument must be an array.'); + } + } + + $this->default = $default; + } + + /** + * 获取默认值 + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * 获取描述 + * @return string + */ + public function getDescription() + { + return $this->description; + } +} diff --git a/vendor/topthink/framework/library/think/console/input/Definition.php b/vendor/topthink/framework/library/think/console/input/Definition.php new file mode 100644 index 0000000..c71977e --- /dev/null +++ b/vendor/topthink/framework/library/think/console/input/Definition.php @@ -0,0 +1,375 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\input; + +class Definition +{ + + /** + * @var Argument[] + */ + private $arguments; + + private $requiredCount; + private $hasAnArrayArgument = false; + private $hasOptional; + + /** + * @var Option[] + */ + private $options; + private $shortcuts; + + /** + * 构造方法 + * @param array $definition + * @api + */ + public function __construct(array $definition = []) + { + $this->setDefinition($definition); + } + + /** + * 设置指令的定义 + * @param array $definition 定义的数组 + */ + public function setDefinition(array $definition) + { + $arguments = []; + $options = []; + foreach ($definition as $item) { + if ($item instanceof Option) { + $options[] = $item; + } else { + $arguments[] = $item; + } + } + + $this->setArguments($arguments); + $this->setOptions($options); + } + + /** + * 设置参数 + * @param Argument[] $arguments 参数数组 + */ + public function setArguments($arguments = []) + { + $this->arguments = []; + $this->requiredCount = 0; + $this->hasOptional = false; + $this->hasAnArrayArgument = false; + $this->addArguments($arguments); + } + + /** + * 添加参数 + * @param Argument[] $arguments 参数数组 + * @api + */ + public function addArguments($arguments = []) + { + if (null !== $arguments) { + foreach ($arguments as $argument) { + $this->addArgument($argument); + } + } + } + + /** + * 添加一个参数 + * @param Argument $argument 参数 + * @throws \LogicException + */ + public function addArgument(Argument $argument) + { + if (isset($this->arguments[$argument->getName()])) { + throw new \LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName())); + } + + if ($this->hasAnArrayArgument) { + throw new \LogicException('Cannot add an argument after an array argument.'); + } + + if ($argument->isRequired() && $this->hasOptional) { + throw new \LogicException('Cannot add a required argument after an optional one.'); + } + + if ($argument->isArray()) { + $this->hasAnArrayArgument = true; + } + + if ($argument->isRequired()) { + ++$this->requiredCount; + } else { + $this->hasOptional = true; + } + + $this->arguments[$argument->getName()] = $argument; + } + + /** + * 根据名称或者位置获取参数 + * @param string|int $name 参数名或者位置 + * @return Argument 参数 + * @throws \InvalidArgumentException + */ + public function getArgument($name) + { + if (!$this->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments; + + return $arguments[$name]; + } + + /** + * 根据名称或位置检查是否具有某个参数 + * @param string|int $name 参数名或者位置 + * @return bool + * @api + */ + public function hasArgument($name) + { + $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments; + + return isset($arguments[$name]); + } + + /** + * 获取所有的参数 + * @return Argument[] 参数数组 + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * 获取参数数量 + * @return int + */ + public function getArgumentCount() + { + return $this->hasAnArrayArgument ? PHP_INT_MAX : count($this->arguments); + } + + /** + * 获取必填的参数的数量 + * @return int + */ + public function getArgumentRequiredCount() + { + return $this->requiredCount; + } + + /** + * 获取参数默认值 + * @return array + */ + public function getArgumentDefaults() + { + $values = []; + foreach ($this->arguments as $argument) { + $values[$argument->getName()] = $argument->getDefault(); + } + + return $values; + } + + /** + * 设置选项 + * @param Option[] $options 选项数组 + */ + public function setOptions($options = []) + { + $this->options = []; + $this->shortcuts = []; + $this->addOptions($options); + } + + /** + * 添加选项 + * @param Option[] $options 选项数组 + * @api + */ + public function addOptions($options = []) + { + foreach ($options as $option) { + $this->addOption($option); + } + } + + /** + * 添加一个选项 + * @param Option $option 选项 + * @throws \LogicException + * @api + */ + public function addOption(Option $option) + { + if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) { + throw new \LogicException(sprintf('An option named "%s" already exists.', $option->getName())); + } + + if ($option->getShortcut()) { + foreach (explode('|', $option->getShortcut()) as $shortcut) { + if (isset($this->shortcuts[$shortcut]) + && !$option->equals($this->options[$this->shortcuts[$shortcut]]) + ) { + throw new \LogicException(sprintf('An option with shortcut "%s" already exists.', $shortcut)); + } + } + } + + $this->options[$option->getName()] = $option; + if ($option->getShortcut()) { + foreach (explode('|', $option->getShortcut()) as $shortcut) { + $this->shortcuts[$shortcut] = $option->getName(); + } + } + } + + /** + * 根据名称获取选项 + * @param string $name 选项名 + * @return Option + * @throws \InvalidArgumentException + * @api + */ + public function getOption($name) + { + if (!$this->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name)); + } + + return $this->options[$name]; + } + + /** + * 根据名称检查是否有这个选项 + * @param string $name 选项名 + * @return bool + * @api + */ + public function hasOption($name) + { + return isset($this->options[$name]); + } + + /** + * 获取所有选项 + * @return Option[] + * @api + */ + public function getOptions() + { + return $this->options; + } + + /** + * 根据名称检查某个选项是否有短名称 + * @param string $name 短名称 + * @return bool + */ + public function hasShortcut($name) + { + return isset($this->shortcuts[$name]); + } + + /** + * 根据短名称获取选项 + * @param string $shortcut 短名称 + * @return Option + */ + public function getOptionForShortcut($shortcut) + { + return $this->getOption($this->shortcutToName($shortcut)); + } + + /** + * 获取所有选项的默认值 + * @return array + */ + public function getOptionDefaults() + { + $values = []; + foreach ($this->options as $option) { + $values[$option->getName()] = $option->getDefault(); + } + + return $values; + } + + /** + * 根据短名称获取选项名 + * @param string $shortcut 短名称 + * @return string + * @throws \InvalidArgumentException + */ + private function shortcutToName($shortcut) + { + if (!isset($this->shortcuts[$shortcut])) { + throw new \InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + return $this->shortcuts[$shortcut]; + } + + /** + * 获取该指令的介绍 + * @param bool $short 是否简洁介绍 + * @return string + */ + public function getSynopsis($short = false) + { + $elements = []; + + if ($short && $this->getOptions()) { + $elements[] = '[options]'; + } elseif (!$short) { + foreach ($this->getOptions() as $option) { + $value = ''; + if ($option->acceptValue()) { + $value = sprintf(' %s%s%s', $option->isValueOptional() ? '[' : '', strtoupper($option->getName()), $option->isValueOptional() ? ']' : ''); + } + + $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : ''; + $elements[] = sprintf('[%s--%s%s]', $shortcut, $option->getName(), $value); + } + } + + if (count($elements) && $this->getArguments()) { + $elements[] = '[--]'; + } + + foreach ($this->getArguments() as $argument) { + $element = '<' . $argument->getName() . '>'; + if (!$argument->isRequired()) { + $element = '[' . $element . ']'; + } elseif ($argument->isArray()) { + $element .= ' (' . $element . ')'; + } + + if ($argument->isArray()) { + $element .= '...'; + } + + $elements[] = $element; + } + + return implode(' ', $elements); + } +} diff --git a/vendor/topthink/framework/library/think/console/input/Option.php b/vendor/topthink/framework/library/think/console/input/Option.php new file mode 100644 index 0000000..e5707c9 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/input/Option.php @@ -0,0 +1,190 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\input; + +class Option +{ + + const VALUE_NONE = 1; + const VALUE_REQUIRED = 2; + const VALUE_OPTIONAL = 4; + const VALUE_IS_ARRAY = 8; + + private $name; + private $shortcut; + private $mode; + private $default; + private $description; + + /** + * 构造方法 + * @param string $name 选项名 + * @param string|array $shortcut 短名称,多个用|隔开或者使用数组 + * @param int $mode 选项类型(可选类型为 self::VALUE_*) + * @param string $description 描述 + * @param mixed $default 默认值 (类型为 self::VALUE_REQUIRED 或者 self::VALUE_NONE 的时候必须为null) + * @throws \InvalidArgumentException + */ + public function __construct($name, $shortcut = null, $mode = null, $description = '', $default = null) + { + if (0 === strpos($name, '--')) { + $name = substr($name, 2); + } + + if (empty($name)) { + throw new \InvalidArgumentException('An option name cannot be empty.'); + } + + if (empty($shortcut)) { + $shortcut = null; + } + + if (null !== $shortcut) { + if (is_array($shortcut)) { + $shortcut = implode('|', $shortcut); + } + $shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-')); + $shortcuts = array_filter($shortcuts); + $shortcut = implode('|', $shortcuts); + + if (empty($shortcut)) { + throw new \InvalidArgumentException('An option shortcut cannot be empty.'); + } + } + + if (null === $mode) { + $mode = self::VALUE_NONE; + } elseif (!is_int($mode) || $mode > 15 || $mode < 1) { + throw new \InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode)); + } + + $this->name = $name; + $this->shortcut = $shortcut; + $this->mode = $mode; + $this->description = $description; + + if ($this->isArray() && !$this->acceptValue()) { + throw new \InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.'); + } + + $this->setDefault($default); + } + + /** + * 获取短名称 + * @return string + */ + public function getShortcut() + { + return $this->shortcut; + } + + /** + * 获取选项名 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 是否可以设置值 + * @return bool 类型不是 self::VALUE_NONE 的时候返回true,其他均返回false + */ + public function acceptValue() + { + return $this->isValueRequired() || $this->isValueOptional(); + } + + /** + * 是否必须 + * @return bool 类型是 self::VALUE_REQUIRED 的时候返回true,其他均返回false + */ + public function isValueRequired() + { + return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode); + } + + /** + * 是否可选 + * @return bool 类型是 self::VALUE_OPTIONAL 的时候返回true,其他均返回false + */ + public function isValueOptional() + { + return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode); + } + + /** + * 选项值是否接受数组 + * @return bool 类型是 self::VALUE_IS_ARRAY 的时候返回true,其他均返回false + */ + public function isArray() + { + return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode); + } + + /** + * 设置默认值 + * @param mixed $default 默认值 + * @throws \LogicException + */ + public function setDefault($default = null) + { + if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) { + throw new \LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.'); + } + + if ($this->isArray()) { + if (null === $default) { + $default = []; + } elseif (!is_array($default)) { + throw new \LogicException('A default value for an array option must be an array.'); + } + } + + $this->default = $this->acceptValue() ? $default : false; + } + + /** + * 获取默认值 + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * 获取描述文字 + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * 检查所给选项是否是当前这个 + * @param Option $option + * @return bool + */ + public function equals(Option $option) + { + return $option->getName() === $this->getName() + && $option->getShortcut() === $this->getShortcut() + && $option->getDefault() === $this->getDefault() + && $option->isArray() === $this->isArray() + && $option->isValueRequired() === $this->isValueRequired() + && $option->isValueOptional() === $this->isValueOptional(); + } +} diff --git a/vendor/topthink/framework/library/think/console/output/Ask.php b/vendor/topthink/framework/library/think/console/output/Ask.php new file mode 100644 index 0000000..3933eb2 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/output/Ask.php @@ -0,0 +1,340 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output; + +use think\console\Input; +use think\console\Output; +use think\console\output\question\Choice; +use think\console\output\question\Confirmation; + +class Ask +{ + private static $stty; + + private static $shell; + + /** @var Input */ + protected $input; + + /** @var Output */ + protected $output; + + /** @var Question */ + protected $question; + + public function __construct(Input $input, Output $output, Question $question) + { + $this->input = $input; + $this->output = $output; + $this->question = $question; + } + + public function run() + { + if (!$this->input->isInteractive()) { + return $this->question->getDefault(); + } + + if (!$this->question->getValidator()) { + return $this->doAsk(); + } + + $that = $this; + + $interviewer = function () use ($that) { + return $that->doAsk(); + }; + + return $this->validateAttempts($interviewer); + } + + protected function doAsk() + { + $this->writePrompt(); + + $inputStream = STDIN; + $autocomplete = $this->question->getAutocompleterValues(); + + if (null === $autocomplete || !$this->hasSttyAvailable()) { + $ret = false; + if ($this->question->isHidden()) { + try { + $ret = trim($this->getHiddenResponse($inputStream)); + } catch (\RuntimeException $e) { + if (!$this->question->isHiddenFallback()) { + throw $e; + } + } + } + + if (false === $ret) { + $ret = fgets($inputStream, 4096); + if (false === $ret) { + throw new \RuntimeException('Aborted'); + } + $ret = trim($ret); + } + } else { + $ret = trim($this->autocomplete($inputStream)); + } + + $ret = strlen($ret) > 0 ? $ret : $this->question->getDefault(); + + if ($normalizer = $this->question->getNormalizer()) { + return $normalizer($ret); + } + + return $ret; + } + + private function autocomplete($inputStream) + { + $autocomplete = $this->question->getAutocompleterValues(); + $ret = ''; + + $i = 0; + $ofs = -1; + $matches = $autocomplete; + $numMatches = count($matches); + + $sttyMode = shell_exec('stty -g'); + + shell_exec('stty -icanon -echo'); + + while (!feof($inputStream)) { + $c = fread($inputStream, 1); + + if ("\177" === $c) { + if (0 === $numMatches && 0 !== $i) { + --$i; + $this->output->write("\033[1D"); + } + + if ($i === 0) { + $ofs = -1; + $matches = $autocomplete; + $numMatches = count($matches); + } else { + $numMatches = 0; + } + + $ret = substr($ret, 0, $i); + } elseif ("\033" === $c) { + $c .= fread($inputStream, 2); + + if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) { + if ('A' === $c[2] && -1 === $ofs) { + $ofs = 0; + } + + if (0 === $numMatches) { + continue; + } + + $ofs += ('A' === $c[2]) ? -1 : 1; + $ofs = ($numMatches + $ofs) % $numMatches; + } + } elseif (ord($c) < 32) { + if ("\t" === $c || "\n" === $c) { + if ($numMatches > 0 && -1 !== $ofs) { + $ret = $matches[$ofs]; + $this->output->write(substr($ret, $i)); + $i = strlen($ret); + } + + if ("\n" === $c) { + $this->output->write($c); + break; + } + + $numMatches = 0; + } + + continue; + } else { + $this->output->write($c); + $ret .= $c; + ++$i; + + $numMatches = 0; + $ofs = 0; + + foreach ($autocomplete as $value) { + if (0 === strpos($value, $ret) && $i !== strlen($value)) { + $matches[$numMatches++] = $value; + } + } + } + + $this->output->write("\033[K"); + + if ($numMatches > 0 && -1 !== $ofs) { + $this->output->write("\0337"); + $this->output->highlight(substr($matches[$ofs], $i)); + $this->output->write("\0338"); + } + } + + shell_exec(sprintf('stty %s', $sttyMode)); + + return $ret; + } + + protected function getHiddenResponse($inputStream) + { + if ('\\' === DIRECTORY_SEPARATOR) { + $exe = __DIR__ . '/../bin/hiddeninput.exe'; + + $value = rtrim(shell_exec($exe)); + $this->output->writeln(''); + + if (isset($tmpExe)) { + unlink($tmpExe); + } + + return $value; + } + + if ($this->hasSttyAvailable()) { + $sttyMode = shell_exec('stty -g'); + + shell_exec('stty -echo'); + $value = fgets($inputStream, 4096); + shell_exec(sprintf('stty %s', $sttyMode)); + + if (false === $value) { + throw new \RuntimeException('Aborted'); + } + + $value = trim($value); + $this->output->writeln(''); + + return $value; + } + + if (false !== $shell = $this->getShell()) { + $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword'; + $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd); + $value = rtrim(shell_exec($command)); + $this->output->writeln(''); + + return $value; + } + + throw new \RuntimeException('Unable to hide the response.'); + } + + protected function validateAttempts($interviewer) + { + /** @var \Exception $error */ + $error = null; + $attempts = $this->question->getMaxAttempts(); + while (null === $attempts || $attempts--) { + if (null !== $error) { + $this->output->error($error->getMessage()); + } + + try { + return call_user_func($this->question->getValidator(), $interviewer()); + } catch (\Exception $error) { + } + } + + throw $error; + } + + /** + * 显示问题的提示信息 + */ + protected function writePrompt() + { + $text = $this->question->getQuestion(); + $default = $this->question->getDefault(); + + switch (true) { + case null === $default: + $text = sprintf(' %s:', $text); + + break; + + case $this->question instanceof Confirmation: + $text = sprintf(' %s (yes/no) [%s]:', $text, $default ? 'yes' : 'no'); + + break; + + case $this->question instanceof Choice && $this->question->isMultiselect(): + $choices = $this->question->getChoices(); + $default = explode(',', $default); + + foreach ($default as $key => $value) { + $default[$key] = $choices[trim($value)]; + } + + $text = sprintf(' %s [%s]:', $text, implode(', ', $default)); + + break; + + case $this->question instanceof Choice: + $choices = $this->question->getChoices(); + $text = sprintf(' %s [%s]:', $text, $choices[$default]); + + break; + + default: + $text = sprintf(' %s [%s]:', $text, $default); + } + + $this->output->writeln($text); + + if ($this->question instanceof Choice) { + $width = max(array_map('strlen', array_keys($this->question->getChoices()))); + + foreach ($this->question->getChoices() as $key => $value) { + $this->output->writeln(sprintf(" [%-${width}s] %s", $key, $value)); + } + } + + $this->output->write(' > '); + } + + private function getShell() + { + if (null !== self::$shell) { + return self::$shell; + } + + self::$shell = false; + + if (file_exists('/usr/bin/env')) { + $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null"; + foreach (['bash', 'zsh', 'ksh', 'csh'] as $sh) { + if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) { + self::$shell = $sh; + break; + } + } + } + + return self::$shell; + } + + private function hasSttyAvailable() + { + if (null !== self::$stty) { + return self::$stty; + } + + exec('stty 2>&1', $output, $exitcode); + + return self::$stty = $exitcode === 0; + } +} diff --git a/vendor/topthink/framework/library/think/console/output/Descriptor.php b/vendor/topthink/framework/library/think/console/output/Descriptor.php new file mode 100644 index 0000000..6d98d53 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/output/Descriptor.php @@ -0,0 +1,319 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output; + +use think\Console; +use think\console\Command; +use think\console\input\Argument as InputArgument; +use think\console\input\Definition as InputDefinition; +use think\console\input\Option as InputOption; +use think\console\Output; +use think\console\output\descriptor\Console as ConsoleDescription; + +class Descriptor +{ + + /** + * @var Output + */ + protected $output; + + /** + * {@inheritdoc} + */ + public function describe(Output $output, $object, array $options = []) + { + $this->output = $output; + + switch (true) { + case $object instanceof InputArgument: + $this->describeInputArgument($object, $options); + break; + case $object instanceof InputOption: + $this->describeInputOption($object, $options); + break; + case $object instanceof InputDefinition: + $this->describeInputDefinition($object, $options); + break; + case $object instanceof Command: + $this->describeCommand($object, $options); + break; + case $object instanceof Console: + $this->describeConsole($object, $options); + break; + default: + throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object))); + } + } + + /** + * 输出内容 + * @param string $content + * @param bool $decorated + */ + protected function write($content, $decorated = false) + { + $this->output->write($content, false, $decorated ? Output::OUTPUT_NORMAL : Output::OUTPUT_RAW); + } + + /** + * 描述参数 + * @param InputArgument $argument + * @param array $options + * @return string|mixed + */ + protected function describeInputArgument(InputArgument $argument, array $options = []) + { + if (null !== $argument->getDefault() + && (!is_array($argument->getDefault()) + || count($argument->getDefault())) + ) { + $default = sprintf(' [default: %s]', $this->formatDefaultValue($argument->getDefault())); + } else { + $default = ''; + } + + $totalWidth = isset($options['total_width']) ? $options['total_width'] : strlen($argument->getName()); + $spacingWidth = $totalWidth - strlen($argument->getName()) + 2; + + $this->writeText(sprintf(" %s%s%s%s", $argument->getName(), str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + + + 2 spaces + preg_replace('/\s*\R\s*/', PHP_EOL . str_repeat(' ', $totalWidth + 17), $argument->getDescription()), $default), $options); + } + + /** + * 描述选项 + * @param InputOption $option + * @param array $options + * @return string|mixed + */ + protected function describeInputOption(InputOption $option, array $options = []) + { + if ($option->acceptValue() && null !== $option->getDefault() + && (!is_array($option->getDefault()) + || count($option->getDefault())) + ) { + $default = sprintf(' [default: %s]', $this->formatDefaultValue($option->getDefault())); + } else { + $default = ''; + } + + $value = ''; + if ($option->acceptValue()) { + $value = '=' . strtoupper($option->getName()); + + if ($option->isValueOptional()) { + $value = '[' . $value . ']'; + } + } + + $totalWidth = isset($options['total_width']) ? $options['total_width'] : $this->calculateTotalWidthForOptions([$option]); + $synopsis = sprintf('%s%s', $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ', sprintf('--%s%s', $option->getName(), $value)); + + $spacingWidth = $totalWidth - strlen($synopsis) + 2; + + $this->writeText(sprintf(" %s%s%s%s%s", $synopsis, str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + + + 2 spaces + preg_replace('/\s*\R\s*/', "\n" . str_repeat(' ', $totalWidth + 17), $option->getDescription()), $default, $option->isArray() ? ' (multiple values allowed)' : ''), $options); + } + + /** + * 描述输入 + * @param InputDefinition $definition + * @param array $options + * @return string|mixed + */ + protected function describeInputDefinition(InputDefinition $definition, array $options = []) + { + $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions()); + foreach ($definition->getArguments() as $argument) { + $totalWidth = max($totalWidth, strlen($argument->getName())); + } + + if ($definition->getArguments()) { + $this->writeText('Arguments:', $options); + $this->writeText("\n"); + foreach ($definition->getArguments() as $argument) { + $this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth])); + $this->writeText("\n"); + } + } + + if ($definition->getArguments() && $definition->getOptions()) { + $this->writeText("\n"); + } + + if ($definition->getOptions()) { + $laterOptions = []; + + $this->writeText('Options:', $options); + foreach ($definition->getOptions() as $option) { + if (strlen($option->getShortcut()) > 1) { + $laterOptions[] = $option; + continue; + } + $this->writeText("\n"); + $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); + } + foreach ($laterOptions as $option) { + $this->writeText("\n"); + $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); + } + } + } + + /** + * 描述指令 + * @param Command $command + * @param array $options + * @return string|mixed + */ + protected function describeCommand(Command $command, array $options = []) + { + $command->getSynopsis(true); + $command->getSynopsis(false); + $command->mergeConsoleDefinition(false); + + $this->writeText('Usage:', $options); + foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) { + $this->writeText("\n"); + $this->writeText(' ' . $usage, $options); + } + $this->writeText("\n"); + + $definition = $command->getNativeDefinition(); + if ($definition->getOptions() || $definition->getArguments()) { + $this->writeText("\n"); + $this->describeInputDefinition($definition, $options); + $this->writeText("\n"); + } + + if ($help = $command->getProcessedHelp()) { + $this->writeText("\n"); + $this->writeText('Help:', $options); + $this->writeText("\n"); + $this->writeText(' ' . str_replace("\n", "\n ", $help), $options); + $this->writeText("\n"); + } + } + + /** + * 描述控制台 + * @param Console $console + * @param array $options + * @return string|mixed + */ + protected function describeConsole(Console $console, array $options = []) + { + $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null; + $description = new ConsoleDescription($console, $describedNamespace); + + if (isset($options['raw_text']) && $options['raw_text']) { + $width = $this->getColumnWidth($description->getCommands()); + + foreach ($description->getCommands() as $command) { + $this->writeText(sprintf("%-${width}s %s", $command->getName(), $command->getDescription()), $options); + $this->writeText("\n"); + } + } else { + if ('' != $help = $console->getHelp()) { + $this->writeText("$help\n\n", $options); + } + + $this->writeText("Usage:\n", $options); + $this->writeText(" command [options] [arguments]\n\n", $options); + + $this->describeInputDefinition(new InputDefinition($console->getDefinition()->getOptions()), $options); + + $this->writeText("\n"); + $this->writeText("\n"); + + $width = $this->getColumnWidth($description->getCommands()); + + if ($describedNamespace) { + $this->writeText(sprintf('Available commands for the "%s" namespace:', $describedNamespace), $options); + } else { + $this->writeText('Available commands:', $options); + } + + // add commands by namespace + foreach ($description->getNamespaces() as $namespace) { + if (!$describedNamespace && ConsoleDescription::GLOBAL_NAMESPACE !== $namespace['id']) { + $this->writeText("\n"); + $this->writeText(' ' . $namespace['id'] . '', $options); + } + + foreach ($namespace['commands'] as $name) { + $this->writeText("\n"); + $spacingWidth = $width - strlen($name); + $this->writeText(sprintf(" %s%s%s", $name, str_repeat(' ', $spacingWidth), $description->getCommand($name) + ->getDescription()), $options); + } + } + + $this->writeText("\n"); + } + } + + /** + * {@inheritdoc} + */ + private function writeText($content, array $options = []) + { + $this->write(isset($options['raw_text']) + && $options['raw_text'] ? strip_tags($content) : $content, isset($options['raw_output']) ? !$options['raw_output'] : true); + } + + /** + * 格式化 + * @param mixed $default + * @return string + */ + private function formatDefaultValue($default) + { + return json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + + /** + * @param Command[] $commands + * @return int + */ + private function getColumnWidth(array $commands) + { + $width = 0; + foreach ($commands as $command) { + $width = strlen($command->getName()) > $width ? strlen($command->getName()) : $width; + } + + return $width + 2; + } + + /** + * @param InputOption[] $options + * @return int + */ + private function calculateTotalWidthForOptions($options) + { + $totalWidth = 0; + foreach ($options as $option) { + $nameLength = 4 + strlen($option->getName()) + 2; // - + shortcut + , + whitespace + name + -- + + if ($option->acceptValue()) { + $valueLength = 1 + strlen($option->getName()); // = + value + $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ] + + $nameLength += $valueLength; + } + $totalWidth = max($totalWidth, $nameLength); + } + + return $totalWidth; + } +} diff --git a/vendor/topthink/framework/library/think/console/output/Formatter.php b/vendor/topthink/framework/library/think/console/output/Formatter.php new file mode 100644 index 0000000..f8bee55 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/output/Formatter.php @@ -0,0 +1,198 @@ + +// +---------------------------------------------------------------------- +namespace think\console\output; + +use think\console\output\formatter\Stack as StyleStack; +use think\console\output\formatter\Style; + +class Formatter +{ + + private $decorated = false; + private $styles = []; + private $styleStack; + + /** + * 转义 + * @param string $text + * @return string + */ + public static function escape($text) + { + return preg_replace('/([^\\\\]?)setStyle('error', new Style('white', 'red')); + $this->setStyle('info', new Style('green')); + $this->setStyle('comment', new Style('yellow')); + $this->setStyle('question', new Style('black', 'cyan')); + $this->setStyle('highlight', new Style('red')); + $this->setStyle('warning', new Style('black', 'yellow')); + + $this->styleStack = new StyleStack(); + } + + /** + * 设置外观标识 + * @param bool $decorated 是否美化文字 + */ + public function setDecorated($decorated) + { + $this->decorated = (bool) $decorated; + } + + /** + * 获取外观标识 + * @return bool + */ + public function isDecorated() + { + return $this->decorated; + } + + /** + * 添加一个新样式 + * @param string $name 样式名 + * @param Style $style 样式实例 + */ + public function setStyle($name, Style $style) + { + $this->styles[strtolower($name)] = $style; + } + + /** + * 是否有这个样式 + * @param string $name + * @return bool + */ + public function hasStyle($name) + { + return isset($this->styles[strtolower($name)]); + } + + /** + * 获取样式 + * @param string $name + * @return Style + * @throws \InvalidArgumentException + */ + public function getStyle($name) + { + if (!$this->hasStyle($name)) { + throw new \InvalidArgumentException(sprintf('Undefined style: %s', $name)); + } + + return $this->styles[strtolower($name)]; + } + + /** + * 使用所给的样式格式化文字 + * @param string $message 文字 + * @return string + */ + public function format($message) + { + $offset = 0; + $output = ''; + $tagRegex = '[a-z][a-z0-9_=;-]*'; + preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#isx", $message, $matches, PREG_OFFSET_CAPTURE); + foreach ($matches[0] as $i => $match) { + $pos = $match[1]; + $text = $match[0]; + + if (0 != $pos && '\\' == $message[$pos - 1]) { + continue; + } + + $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset)); + $offset = $pos + strlen($text); + + if ($open = '/' != $text[1]) { + $tag = $matches[1][$i][0]; + } else { + $tag = isset($matches[3][$i][0]) ? $matches[3][$i][0] : ''; + } + + if (!$open && !$tag) { + // + $this->styleStack->pop(); + } elseif (false === $style = $this->createStyleFromString(strtolower($tag))) { + $output .= $this->applyCurrentStyle($text); + } elseif ($open) { + $this->styleStack->push($style); + } else { + $this->styleStack->pop($style); + } + } + + $output .= $this->applyCurrentStyle(substr($message, $offset)); + + return str_replace('\\<', '<', $output); + } + + /** + * @return StyleStack + */ + public function getStyleStack() + { + return $this->styleStack; + } + + /** + * 根据字符串创建新的样式实例 + * @param string $string + * @return Style|bool + */ + private function createStyleFromString($string) + { + if (isset($this->styles[$string])) { + return $this->styles[$string]; + } + + if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', strtolower($string), $matches, PREG_SET_ORDER)) { + return false; + } + + $style = new Style(); + foreach ($matches as $match) { + array_shift($match); + + if ('fg' == $match[0]) { + $style->setForeground($match[1]); + } elseif ('bg' == $match[0]) { + $style->setBackground($match[1]); + } else { + try { + $style->setOption($match[1]); + } catch (\InvalidArgumentException $e) { + return false; + } + } + } + + return $style; + } + + /** + * 从堆栈应用样式到文字 + * @param string $text 文字 + * @return string + */ + private function applyCurrentStyle($text) + { + return $this->isDecorated() && strlen($text) > 0 ? $this->styleStack->getCurrent()->apply($text) : $text; + } +} diff --git a/vendor/topthink/framework/library/think/console/output/Question.php b/vendor/topthink/framework/library/think/console/output/Question.php new file mode 100644 index 0000000..03975f2 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/output/Question.php @@ -0,0 +1,211 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output; + +class Question +{ + + private $question; + private $attempts; + private $hidden = false; + private $hiddenFallback = true; + private $autocompleterValues; + private $validator; + private $default; + private $normalizer; + + /** + * 构造方法 + * @param string $question 问题 + * @param mixed $default 默认答案 + */ + public function __construct($question, $default = null) + { + $this->question = $question; + $this->default = $default; + } + + /** + * 获取问题 + * @return string + */ + public function getQuestion() + { + return $this->question; + } + + /** + * 获取默认答案 + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * 是否隐藏答案 + * @return bool + */ + public function isHidden() + { + return $this->hidden; + } + + /** + * 隐藏答案 + * @param bool $hidden + * @return Question + */ + public function setHidden($hidden) + { + if ($this->autocompleterValues) { + throw new \LogicException('A hidden question cannot use the autocompleter.'); + } + + $this->hidden = (bool) $hidden; + + return $this; + } + + /** + * 不能被隐藏是否撤销 + * @return bool + */ + public function isHiddenFallback() + { + return $this->hiddenFallback; + } + + /** + * 设置不能被隐藏的时候的操作 + * @param bool $fallback + * @return Question + */ + public function setHiddenFallback($fallback) + { + $this->hiddenFallback = (bool) $fallback; + + return $this; + } + + /** + * 获取自动完成 + * @return null|array|\Traversable + */ + public function getAutocompleterValues() + { + return $this->autocompleterValues; + } + + /** + * 设置自动完成的值 + * @param null|array|\Traversable $values + * @return Question + * @throws \InvalidArgumentException + * @throws \LogicException + */ + public function setAutocompleterValues($values) + { + if (is_array($values) && $this->isAssoc($values)) { + $values = array_merge(array_keys($values), array_values($values)); + } + + if (null !== $values && !is_array($values)) { + if (!$values instanceof \Traversable || $values instanceof \Countable) { + throw new \InvalidArgumentException('Autocompleter values can be either an array, `null` or an object implementing both `Countable` and `Traversable` interfaces.'); + } + } + + if ($this->hidden) { + throw new \LogicException('A hidden question cannot use the autocompleter.'); + } + + $this->autocompleterValues = $values; + + return $this; + } + + /** + * 设置答案的验证器 + * @param null|callable $validator + * @return Question The current instance + */ + public function setValidator($validator) + { + $this->validator = $validator; + + return $this; + } + + /** + * 获取验证器 + * @return null|callable + */ + public function getValidator() + { + return $this->validator; + } + + /** + * 设置最大重试次数 + * @param null|int $attempts + * @return Question + * @throws \InvalidArgumentException + */ + public function setMaxAttempts($attempts) + { + if (null !== $attempts && $attempts < 1) { + throw new \InvalidArgumentException('Maximum number of attempts must be a positive value.'); + } + + $this->attempts = $attempts; + + return $this; + } + + /** + * 获取最大重试次数 + * @return null|int + */ + public function getMaxAttempts() + { + return $this->attempts; + } + + /** + * 设置响应的回调 + * @param string|\Closure $normalizer + * @return Question + */ + public function setNormalizer($normalizer) + { + $this->normalizer = $normalizer; + + return $this; + } + + /** + * 获取响应回调 + * The normalizer can ba a callable (a string), a closure or a class implementing __invoke. + * @return string|\Closure + */ + public function getNormalizer() + { + return $this->normalizer; + } + + protected function isAssoc($array) + { + return (bool) count(array_filter(array_keys($array), 'is_string')); + } +} diff --git a/vendor/topthink/framework/library/think/console/output/descriptor/Console.php b/vendor/topthink/framework/library/think/console/output/descriptor/Console.php new file mode 100644 index 0000000..8739c53 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/output/descriptor/Console.php @@ -0,0 +1,153 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\descriptor; + +use think\Console as ThinkConsole; +use think\console\Command; + +class Console +{ + + const GLOBAL_NAMESPACE = '_global'; + + /** + * @var ThinkConsole + */ + private $console; + + /** + * @var null|string + */ + private $namespace; + + /** + * @var array + */ + private $namespaces; + + /** + * @var Command[] + */ + private $commands; + + /** + * @var Command[] + */ + private $aliases; + + /** + * 构造方法 + * @param ThinkConsole $console + * @param string|null $namespace + */ + public function __construct(ThinkConsole $console, $namespace = null) + { + $this->console = $console; + $this->namespace = $namespace; + } + + /** + * @return array + */ + public function getNamespaces() + { + if (null === $this->namespaces) { + $this->inspectConsole(); + } + + return $this->namespaces; + } + + /** + * @return Command[] + */ + public function getCommands() + { + if (null === $this->commands) { + $this->inspectConsole(); + } + + return $this->commands; + } + + /** + * @param string $name + * @return Command + * @throws \InvalidArgumentException + */ + public function getCommand($name) + { + if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) { + throw new \InvalidArgumentException(sprintf('Command %s does not exist.', $name)); + } + + return isset($this->commands[$name]) ? $this->commands[$name] : $this->aliases[$name]; + } + + private function inspectConsole() + { + $this->commands = []; + $this->namespaces = []; + + $all = $this->console->all($this->namespace ? $this->console->findNamespace($this->namespace) : null); + foreach ($this->sortCommands($all) as $namespace => $commands) { + $names = []; + + /** @var Command $command */ + foreach ($commands as $name => $command) { + if (is_string($command)) { + $command = new $command(); + } + + if (!$command->getName()) { + continue; + } + + if ($command->getName() === $name) { + $this->commands[$name] = $command; + } else { + $this->aliases[$name] = $command; + } + + $names[] = $name; + } + + $this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names]; + } + } + + /** + * @param array $commands + * @return array + */ + private function sortCommands(array $commands) + { + $namespacedCommands = []; + foreach ($commands as $name => $command) { + $key = $this->console->extractNamespace($name, 1); + if (!$key) { + $key = self::GLOBAL_NAMESPACE; + } + + $namespacedCommands[$key][$name] = $command; + } + ksort($namespacedCommands); + + foreach ($namespacedCommands as &$commandsSet) { + ksort($commandsSet); + } + // unset reference to keep scope clear + unset($commandsSet); + + return $namespacedCommands; + } +} diff --git a/vendor/topthink/framework/library/think/console/output/driver/Buffer.php b/vendor/topthink/framework/library/think/console/output/driver/Buffer.php new file mode 100644 index 0000000..c77a2ec --- /dev/null +++ b/vendor/topthink/framework/library/think/console/output/driver/Buffer.php @@ -0,0 +1,52 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\driver; + +use think\console\Output; + +class Buffer +{ + /** + * @var string + */ + private $buffer = ''; + + public function __construct(Output $output) + { + // do nothing + } + + public function fetch() + { + $content = $this->buffer; + $this->buffer = ''; + return $content; + } + + public function write($messages, $newline = false, $options = Output::OUTPUT_NORMAL) + { + $messages = (array) $messages; + + foreach ($messages as $message) { + $this->buffer .= $message; + } + if ($newline) { + $this->buffer .= "\n"; + } + } + + public function renderException(\Exception $e) + { + // do nothing + } + +} diff --git a/vendor/topthink/framework/library/think/console/output/driver/Console.php b/vendor/topthink/framework/library/think/console/output/driver/Console.php new file mode 100644 index 0000000..e041b52 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/output/driver/Console.php @@ -0,0 +1,368 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\driver; + +use think\console\Output; +use think\console\output\Formatter; + +class Console +{ + + /** @var Resource */ + private $stdout; + + /** @var Formatter */ + private $formatter; + + private $terminalDimensions; + + /** @var Output */ + private $output; + + public function __construct(Output $output) + { + $this->output = $output; + $this->formatter = new Formatter(); + $this->stdout = $this->openOutputStream(); + $decorated = $this->hasColorSupport($this->stdout); + $this->formatter->setDecorated($decorated); + } + + public function setDecorated($decorated) + { + $this->formatter->setDecorated($decorated); + } + + public function write($messages, $newline = false, $type = Output::OUTPUT_NORMAL, $stream = null) + { + if (Output::VERBOSITY_QUIET === $this->output->getVerbosity()) { + return; + } + + $messages = (array) $messages; + + foreach ($messages as $message) { + switch ($type) { + case Output::OUTPUT_NORMAL: + $message = $this->formatter->format($message); + break; + case Output::OUTPUT_RAW: + break; + case Output::OUTPUT_PLAIN: + $message = strip_tags($this->formatter->format($message)); + break; + default: + throw new \InvalidArgumentException(sprintf('Unknown output type given (%s)', $type)); + } + + $this->doWrite($message, $newline, $stream); + } + } + + public function renderException(\Exception $e) + { + $stderr = $this->openErrorStream(); + $decorated = $this->hasColorSupport($stderr); + $this->formatter->setDecorated($decorated); + + do { + $title = sprintf(' [%s] ', get_class($e)); + + $len = $this->stringWidth($title); + + $width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : PHP_INT_MAX; + + if (defined('HHVM_VERSION') && $width > 1 << 31) { + $width = 1 << 31; + } + $lines = []; + foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) { + foreach ($this->splitStringByWidth($line, $width - 4) as $line) { + + $lineLength = $this->stringWidth(preg_replace('/\[[^m]*m/', '', $line)) + 4; + $lines[] = [$line, $lineLength]; + + $len = max($lineLength, $len); + } + } + + $messages = ['', '']; + $messages[] = $emptyLine = sprintf('%s', str_repeat(' ', $len)); + $messages[] = sprintf('%s%s', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title)))); + foreach ($lines as $line) { + $messages[] = sprintf(' %s %s', $line[0], str_repeat(' ', $len - $line[1])); + } + $messages[] = $emptyLine; + $messages[] = ''; + $messages[] = ''; + + $this->write($messages, true, Output::OUTPUT_NORMAL, $stderr); + + if (Output::VERBOSITY_VERBOSE <= $this->output->getVerbosity()) { + $this->write('Exception trace:', true, Output::OUTPUT_NORMAL, $stderr); + + // exception related properties + $trace = $e->getTrace(); + array_unshift($trace, [ + 'function' => '', + 'file' => $e->getFile() !== null ? $e->getFile() : 'n/a', + 'line' => $e->getLine() !== null ? $e->getLine() : 'n/a', + 'args' => [], + ]); + + for ($i = 0, $count = count($trace); $i < $count; ++$i) { + $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : ''; + $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : ''; + $function = $trace[$i]['function']; + $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a'; + $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a'; + + $this->write(sprintf(' %s%s%s() at %s:%s', $class, $type, $function, $file, $line), true, Output::OUTPUT_NORMAL, $stderr); + } + + $this->write('', true, Output::OUTPUT_NORMAL, $stderr); + $this->write('', true, Output::OUTPUT_NORMAL, $stderr); + } + } while ($e = $e->getPrevious()); + + } + + /** + * 获取终端宽度 + * @return int|null + */ + protected function getTerminalWidth() + { + $dimensions = $this->getTerminalDimensions(); + + return $dimensions[0]; + } + + /** + * 获取终端高度 + * @return int|null + */ + protected function getTerminalHeight() + { + $dimensions = $this->getTerminalDimensions(); + + return $dimensions[1]; + } + + /** + * 获取当前终端的尺寸 + * @return array + */ + public function getTerminalDimensions() + { + if ($this->terminalDimensions) { + return $this->terminalDimensions; + } + + if ('\\' === DIRECTORY_SEPARATOR) { + if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) { + return [(int) $matches[1], (int) $matches[2]]; + } + if (preg_match('/^(\d+)x(\d+)$/', $this->getMode(), $matches)) { + return [(int) $matches[1], (int) $matches[2]]; + } + } + + if ($sttyString = $this->getSttyColumns()) { + if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) { + return [(int) $matches[2], (int) $matches[1]]; + } + if (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) { + return [(int) $matches[2], (int) $matches[1]]; + } + } + + return [null, null]; + } + + /** + * 获取stty列数 + * @return string + */ + private function getSttyColumns() + { + if (!function_exists('proc_open')) { + return; + } + + $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; + $process = proc_open('stty -a | grep columns', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]); + if (is_resource($process)) { + $info = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + return $info; + } + return; + } + + /** + * 获取终端模式 + * @return string x 或 null + */ + private function getMode() + { + if (!function_exists('proc_open')) { + return; + } + + $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; + $process = proc_open('mode CON', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]); + if (is_resource($process)) { + $info = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { + return $matches[2] . 'x' . $matches[1]; + } + } + return; + } + + private function stringWidth($string) + { + if (!function_exists('mb_strwidth')) { + return strlen($string); + } + + if (false === $encoding = mb_detect_encoding($string)) { + return strlen($string); + } + + return mb_strwidth($string, $encoding); + } + + private function splitStringByWidth($string, $width) + { + if (!function_exists('mb_strwidth')) { + return str_split($string, $width); + } + + if (false === $encoding = mb_detect_encoding($string)) { + return str_split($string, $width); + } + + $utf8String = mb_convert_encoding($string, 'utf8', $encoding); + $lines = []; + $line = ''; + foreach (preg_split('//u', $utf8String) as $char) { + if (mb_strwidth($line . $char, 'utf8') <= $width) { + $line .= $char; + continue; + } + $lines[] = str_pad($line, $width); + $line = $char; + } + if (strlen($line)) { + $lines[] = count($lines) ? str_pad($line, $width) : $line; + } + + mb_convert_variables($encoding, 'utf8', $lines); + + return $lines; + } + + private function isRunningOS400() + { + $checks = [ + function_exists('php_uname') ? php_uname('s') : '', + getenv('OSTYPE'), + PHP_OS, + ]; + return false !== stripos(implode(';', $checks), 'OS400'); + } + + /** + * 当前环境是否支持写入控制台输出到stdout. + * + * @return bool + */ + protected function hasStdoutSupport() + { + return false === $this->isRunningOS400(); + } + + /** + * 当前环境是否支持写入控制台输出到stderr. + * + * @return bool + */ + protected function hasStderrSupport() + { + return false === $this->isRunningOS400(); + } + + /** + * @return resource + */ + private function openOutputStream() + { + if (!$this->hasStdoutSupport()) { + return fopen('php://output', 'w'); + } + return @fopen('php://stdout', 'w') ?: fopen('php://output', 'w'); + } + + /** + * @return resource + */ + private function openErrorStream() + { + return fopen($this->hasStderrSupport() ? 'php://stderr' : 'php://output', 'w'); + } + + /** + * 将消息写入到输出。 + * @param string $message 消息 + * @param bool $newline 是否另起一行 + * @param null $stream + */ + protected function doWrite($message, $newline, $stream = null) + { + if (null === $stream) { + $stream = $this->stdout; + } + if (false === @fwrite($stream, $message . ($newline ? PHP_EOL : ''))) { + throw new \RuntimeException('Unable to write output.'); + } + + fflush($stream); + } + + /** + * 是否支持着色 + * @param $stream + * @return bool + */ + protected function hasColorSupport($stream) + { + if (DIRECTORY_SEPARATOR === '\\') { + return + '10.0.10586' === PHP_WINDOWS_VERSION_MAJOR . '.' . PHP_WINDOWS_VERSION_MINOR . '.' . PHP_WINDOWS_VERSION_BUILD + || false !== getenv('ANSICON') + || 'ON' === getenv('ConEmuANSI') + || 'xterm' === getenv('TERM'); + } + + return function_exists('posix_isatty') && @posix_isatty($stream); + } + +} diff --git a/vendor/topthink/framework/library/think/console/output/driver/Nothing.php b/vendor/topthink/framework/library/think/console/output/driver/Nothing.php new file mode 100644 index 0000000..9a55f77 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/output/driver/Nothing.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\driver; + +use think\console\Output; + +class Nothing +{ + + public function __construct(Output $output) + { + // do nothing + } + + public function write($messages, $newline = false, $options = Output::OUTPUT_NORMAL) + { + // do nothing + } + + public function renderException(\Exception $e) + { + // do nothing + } +} diff --git a/vendor/topthink/framework/library/think/console/output/formatter/Stack.php b/vendor/topthink/framework/library/think/console/output/formatter/Stack.php new file mode 100644 index 0000000..4864a3f --- /dev/null +++ b/vendor/topthink/framework/library/think/console/output/formatter/Stack.php @@ -0,0 +1,116 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\formatter; + +class Stack +{ + + /** + * @var Style[] + */ + private $styles; + + /** + * @var Style + */ + private $emptyStyle; + + /** + * 构造方法 + * @param Style|null $emptyStyle + */ + public function __construct(Style $emptyStyle = null) + { + $this->emptyStyle = $emptyStyle ?: new Style(); + $this->reset(); + } + + /** + * 重置堆栈 + */ + public function reset() + { + $this->styles = []; + } + + /** + * 推一个样式进入堆栈 + * @param Style $style + */ + public function push(Style $style) + { + $this->styles[] = $style; + } + + /** + * 从堆栈中弹出一个样式 + * @param Style|null $style + * @return Style + * @throws \InvalidArgumentException + */ + public function pop(Style $style = null) + { + if (empty($this->styles)) { + return $this->emptyStyle; + } + + if (null === $style) { + return array_pop($this->styles); + } + + /** + * @var int $index + * @var Style $stackedStyle + */ + foreach (array_reverse($this->styles, true) as $index => $stackedStyle) { + if ($style->apply('') === $stackedStyle->apply('')) { + $this->styles = array_slice($this->styles, 0, $index); + + return $stackedStyle; + } + } + + throw new \InvalidArgumentException('Incorrectly nested style tag found.'); + } + + /** + * 计算堆栈的当前样式。 + * @return Style + */ + public function getCurrent() + { + if (empty($this->styles)) { + return $this->emptyStyle; + } + + return $this->styles[count($this->styles) - 1]; + } + + /** + * @param Style $emptyStyle + * @return Stack + */ + public function setEmptyStyle(Style $emptyStyle) + { + $this->emptyStyle = $emptyStyle; + + return $this; + } + + /** + * @return Style + */ + public function getEmptyStyle() + { + return $this->emptyStyle; + } +} diff --git a/vendor/topthink/framework/library/think/console/output/formatter/Style.php b/vendor/topthink/framework/library/think/console/output/formatter/Style.php new file mode 100644 index 0000000..d9b0999 --- /dev/null +++ b/vendor/topthink/framework/library/think/console/output/formatter/Style.php @@ -0,0 +1,189 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\formatter; + +class Style +{ + + private static $availableForegroundColors = [ + 'black' => ['set' => 30, 'unset' => 39], + 'red' => ['set' => 31, 'unset' => 39], + 'green' => ['set' => 32, 'unset' => 39], + 'yellow' => ['set' => 33, 'unset' => 39], + 'blue' => ['set' => 34, 'unset' => 39], + 'magenta' => ['set' => 35, 'unset' => 39], + 'cyan' => ['set' => 36, 'unset' => 39], + 'white' => ['set' => 37, 'unset' => 39], + ]; + private static $availableBackgroundColors = [ + 'black' => ['set' => 40, 'unset' => 49], + 'red' => ['set' => 41, 'unset' => 49], + 'green' => ['set' => 42, 'unset' => 49], + 'yellow' => ['set' => 43, 'unset' => 49], + 'blue' => ['set' => 44, 'unset' => 49], + 'magenta' => ['set' => 45, 'unset' => 49], + 'cyan' => ['set' => 46, 'unset' => 49], + 'white' => ['set' => 47, 'unset' => 49], + ]; + private static $availableOptions = [ + 'bold' => ['set' => 1, 'unset' => 22], + 'underscore' => ['set' => 4, 'unset' => 24], + 'blink' => ['set' => 5, 'unset' => 25], + 'reverse' => ['set' => 7, 'unset' => 27], + 'conceal' => ['set' => 8, 'unset' => 28], + ]; + + private $foreground; + private $background; + private $options = []; + + /** + * 初始化输出的样式 + * @param string|null $foreground 字体颜色 + * @param string|null $background 背景色 + * @param array $options 格式 + * @api + */ + public function __construct($foreground = null, $background = null, array $options = []) + { + if (null !== $foreground) { + $this->setForeground($foreground); + } + if (null !== $background) { + $this->setBackground($background); + } + if (count($options)) { + $this->setOptions($options); + } + } + + /** + * 设置字体颜色 + * @param string|null $color 颜色名 + * @throws \InvalidArgumentException + * @api + */ + public function setForeground($color = null) + { + if (null === $color) { + $this->foreground = null; + + return; + } + + if (!isset(static::$availableForegroundColors[$color])) { + throw new \InvalidArgumentException(sprintf('Invalid foreground color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableForegroundColors)))); + } + + $this->foreground = static::$availableForegroundColors[$color]; + } + + /** + * 设置背景色 + * @param string|null $color 颜色名 + * @throws \InvalidArgumentException + * @api + */ + public function setBackground($color = null) + { + if (null === $color) { + $this->background = null; + + return; + } + + if (!isset(static::$availableBackgroundColors[$color])) { + throw new \InvalidArgumentException(sprintf('Invalid background color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableBackgroundColors)))); + } + + $this->background = static::$availableBackgroundColors[$color]; + } + + /** + * 设置字体格式 + * @param string $option 格式名 + * @throws \InvalidArgumentException When the option name isn't defined + * @api + */ + public function setOption($option) + { + if (!isset(static::$availableOptions[$option])) { + throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions)))); + } + + if (!in_array(static::$availableOptions[$option], $this->options)) { + $this->options[] = static::$availableOptions[$option]; + } + } + + /** + * 重置字体格式 + * @param string $option 格式名 + * @throws \InvalidArgumentException + */ + public function unsetOption($option) + { + if (!isset(static::$availableOptions[$option])) { + throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions)))); + } + + $pos = array_search(static::$availableOptions[$option], $this->options); + if (false !== $pos) { + unset($this->options[$pos]); + } + } + + /** + * 批量设置字体格式 + * @param array $options + */ + public function setOptions(array $options) + { + $this->options = []; + + foreach ($options as $option) { + $this->setOption($option); + } + } + + /** + * 应用样式到文字 + * @param string $text 文字 + * @return string + */ + public function apply($text) + { + $setCodes = []; + $unsetCodes = []; + + if (null !== $this->foreground) { + $setCodes[] = $this->foreground['set']; + $unsetCodes[] = $this->foreground['unset']; + } + if (null !== $this->background) { + $setCodes[] = $this->background['set']; + $unsetCodes[] = $this->background['unset']; + } + if (count($this->options)) { + foreach ($this->options as $option) { + $setCodes[] = $option['set']; + $unsetCodes[] = $option['unset']; + } + } + + if (0 === count($setCodes)) { + return $text; + } + + return sprintf("\033[%sm%s\033[%sm", implode(';', $setCodes), $text, implode(';', $unsetCodes)); + } +} diff --git a/vendor/topthink/framework/library/think/console/output/question/Choice.php b/vendor/topthink/framework/library/think/console/output/question/Choice.php new file mode 100644 index 0000000..cdc3b4e --- /dev/null +++ b/vendor/topthink/framework/library/think/console/output/question/Choice.php @@ -0,0 +1,163 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\question; + +use think\console\output\Question; + +class Choice extends Question +{ + + private $choices; + private $multiselect = false; + private $prompt = ' > '; + private $errorMessage = 'Value "%s" is invalid'; + + /** + * 构造方法 + * @param string $question 问题 + * @param array $choices 选项 + * @param mixed $default 默认答案 + */ + public function __construct($question, array $choices, $default = null) + { + parent::__construct($question, $default); + + $this->choices = $choices; + $this->setValidator($this->getDefaultValidator()); + $this->setAutocompleterValues($choices); + } + + /** + * 可选项 + * @return array + */ + public function getChoices() + { + return $this->choices; + } + + /** + * 设置可否多选 + * @param bool $multiselect + * @return self + */ + public function setMultiselect($multiselect) + { + $this->multiselect = $multiselect; + $this->setValidator($this->getDefaultValidator()); + + return $this; + } + + public function isMultiselect() + { + return $this->multiselect; + } + + /** + * 获取提示 + * @return string + */ + public function getPrompt() + { + return $this->prompt; + } + + /** + * 设置提示 + * @param string $prompt + * @return self + */ + public function setPrompt($prompt) + { + $this->prompt = $prompt; + + return $this; + } + + /** + * 设置错误提示信息 + * @param string $errorMessage + * @return self + */ + public function setErrorMessage($errorMessage) + { + $this->errorMessage = $errorMessage; + $this->setValidator($this->getDefaultValidator()); + + return $this; + } + + /** + * 获取默认的验证方法 + * @return callable + */ + private function getDefaultValidator() + { + $choices = $this->choices; + $errorMessage = $this->errorMessage; + $multiselect = $this->multiselect; + $isAssoc = $this->isAssoc($choices); + + return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) { + // Collapse all spaces. + $selectedChoices = str_replace(' ', '', $selected); + + if ($multiselect) { + // Check for a separated comma values + if (!preg_match('/^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$/', $selectedChoices, $matches)) { + throw new \InvalidArgumentException(sprintf($errorMessage, $selected)); + } + $selectedChoices = explode(',', $selectedChoices); + } else { + $selectedChoices = [$selected]; + } + + $multiselectChoices = []; + foreach ($selectedChoices as $value) { + $results = []; + foreach ($choices as $key => $choice) { + if ($choice === $value) { + $results[] = $key; + } + } + + if (count($results) > 1) { + throw new \InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of %s.', implode(' or ', $results))); + } + + $result = array_search($value, $choices); + + if (!$isAssoc) { + if (!empty($result)) { + $result = $choices[$result]; + } elseif (isset($choices[$value])) { + $result = $choices[$value]; + } + } elseif (empty($result) && array_key_exists($value, $choices)) { + $result = $value; + } + + if (false === $result) { + throw new \InvalidArgumentException(sprintf($errorMessage, $value)); + } + array_push($multiselectChoices, $result); + } + + if ($multiselect) { + return $multiselectChoices; + } + + return current($multiselectChoices); + }; + } +} diff --git a/vendor/topthink/framework/library/think/console/output/question/Confirmation.php b/vendor/topthink/framework/library/think/console/output/question/Confirmation.php new file mode 100644 index 0000000..6598f9b --- /dev/null +++ b/vendor/topthink/framework/library/think/console/output/question/Confirmation.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\question; + +use think\console\output\Question; + +class Confirmation extends Question +{ + + private $trueAnswerRegex; + + /** + * 构造方法 + * @param string $question 问题 + * @param bool $default 默认答案 + * @param string $trueAnswerRegex 验证正则 + */ + public function __construct($question, $default = true, $trueAnswerRegex = '/^y/i') + { + parent::__construct($question, (bool) $default); + + $this->trueAnswerRegex = $trueAnswerRegex; + $this->setNormalizer($this->getDefaultNormalizer()); + } + + /** + * 获取默认的答案回调 + * @return callable + */ + private function getDefaultNormalizer() + { + $default = $this->getDefault(); + $regex = $this->trueAnswerRegex; + + return function ($answer) use ($default, $regex) { + if (is_bool($answer)) { + return $answer; + } + + $answerIsTrue = (bool) preg_match($regex, $answer); + if (false === $default) { + return $answer && $answerIsTrue; + } + + return !$answer || $answerIsTrue; + }; + } +} diff --git a/vendor/topthink/framework/library/think/db/Builder.php b/vendor/topthink/framework/library/think/db/Builder.php new file mode 100644 index 0000000..60b470e --- /dev/null +++ b/vendor/topthink/framework/library/think/db/Builder.php @@ -0,0 +1,1173 @@ + +// +---------------------------------------------------------------------- + +namespace think\db; + +use PDO; +use think\Exception; + +abstract class Builder +{ + // connection对象实例 + protected $connection; + + // 查询表达式映射 + protected $exp = ['EQ' => '=', 'NEQ' => '<>', 'GT' => '>', 'EGT' => '>=', 'LT' => '<', 'ELT' => '<=', 'NOTLIKE' => 'NOT LIKE', 'NOTIN' => 'NOT IN', 'NOTBETWEEN' => 'NOT BETWEEN', 'NOTEXISTS' => 'NOT EXISTS', 'NOTNULL' => 'NOT NULL', 'NOTBETWEEN TIME' => 'NOT BETWEEN TIME']; + + // 查询表达式解析 + protected $parser = [ + 'parseCompare' => ['=', '<>', '>', '>=', '<', '<='], + 'parseLike' => ['LIKE', 'NOT LIKE'], + 'parseBetween' => ['NOT BETWEEN', 'BETWEEN'], + 'parseIn' => ['NOT IN', 'IN'], + 'parseExp' => ['EXP'], + 'parseNull' => ['NOT NULL', 'NULL'], + 'parseBetweenTime' => ['BETWEEN TIME', 'NOT BETWEEN TIME'], + 'parseTime' => ['< TIME', '> TIME', '<= TIME', '>= TIME'], + 'parseExists' => ['NOT EXISTS', 'EXISTS'], + 'parseColumn' => ['COLUMN'], + ]; + + // SQL表达式 + 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%'; + + protected $deleteSql = 'DELETE FROM %TABLE%%USING%%JOIN%%WHERE%%ORDER%%LIMIT% %LOCK%%COMMENT%'; + + /** + * 架构函数 + * @access public + * @param Connection $connection 数据库连接对象实例 + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + /** + * 获取当前的连接对象实例 + * @access public + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * 注册查询表达式解析 + * @access public + * @param string $name 解析方法 + * @param array $parser 匹配表达式数据 + * @return $this + */ + public function bindParser($name, $parser) + { + $this->parser[$name] = $parser; + return $this; + } + + /** + * 数据分析 + * @access protected + * @param Query $query 查询对象 + * @param array $data 数据 + * @param array $fields 字段信息 + * @param array $bind 参数绑定 + * @return array + */ + protected function parseData(Query $query, $data = [], $fields = [], $bind = []) + { + if (empty($data)) { + return []; + } + + $options = $query->getOptions(); + + // 获取绑定信息 + if (empty($bind)) { + $bind = $this->connection->getFieldsBind($options['table']); + } + + if (empty($fields)) { + if ('*' == $options['field']) { + $fields = array_keys($bind); + } else { + $fields = $options['field']; + } + } + + $result = []; + + foreach ($data as $key => $val) { + if ('*' != $options['field'] && !in_array($key, $fields, true)) { + continue; + } + + $item = $this->parseKey($query, $key, true); + + if ($val instanceof Expression) { + $result[$item] = $val->getValue(); + continue; + } elseif (!is_scalar($val) && (in_array($key, (array) $query->getOptions('json')) || 'json' == $this->connection->getFieldsType($options['table'], $key))) { + $val = json_encode($val, JSON_UNESCAPED_UNICODE); + } elseif (is_object($val) && method_exists($val, '__toString')) { + // 对象数据写入 + $val = $val->__toString(); + } + + if (false !== strpos($key, '->')) { + list($key, $name) = explode('->', $key); + $item = $this->parseKey($query, $key); + $result[$item] = 'json_set(' . $item . ', \'$.' . $name . '\', ' . $this->parseDataBind($query, $key, $val, $bind) . ')'; + } elseif ('*' == $options['field'] && false === strpos($key, '.') && !in_array($key, $fields, true)) { + if ($options['strict']) { + throw new Exception('fields not exists:[' . $key . ']'); + } + } elseif (is_null($val)) { + $result[$item] = 'NULL'; + } elseif (is_array($val) && !empty($val)) { + switch (strtoupper($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)) { + // 过滤非标量数据 + $result[$item] = $this->parseDataBind($query, $key, $val, $bind); + } + } + + return $result; + } + + /** + * 数据绑定处理 + * @access protected + * @param Query $query 查询对象 + * @param string $key 字段名 + * @param mixed $data 数据 + * @param array $bind 绑定数据 + * @return string + */ + protected function parseDataBind(Query $query, $key, $data, $bind = []) + { + if ($data instanceof Expression) { + return $data->getValue(); + } + + $name = $query->bind($data, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR); + + return ':' . $name; + } + + /** + * 字段名分析 + * @access public + * @param Query $query 查询对象 + * @param mixed $key 字段名 + * @param bool $strict 严格检测 + * @return string + */ + public function parseKey(Query $query, $key, $strict = false) + { + return $key instanceof Expression ? $key->getValue() : $key; + } + + /** + * field分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $fields 字段名 + * @return string + */ + protected function parseField(Query $query, $fields) + { + if ('*' == $fields || empty($fields)) { + $fieldsStr = '*'; + } elseif (is_array($fields)) { + // 支持 'field1'=>'field2' 这样的字段别名定义 + $array = []; + + foreach ($fields as $key => $field) { + if (!is_numeric($key)) { + $array[] = $this->parseKey($query, $key) . ' AS ' . $this->parseKey($query, $field, true); + } else { + $array[] = $this->parseKey($query, $field); + } + } + + $fieldsStr = implode(',', $array); + } + + return $fieldsStr; + } + + /** + * table分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $tables 表名 + * @return string + */ + protected function parseTable(Query $query, $tables) + { + $item = []; + $options = $query->getOptions(); + + foreach ((array) $tables as $key => $table) { + if (!is_numeric($key)) { + $key = $this->connection->parseSqlTable($key); + $item[] = $this->parseKey($query, $key) . ' ' . $this->parseKey($query, $table); + } else { + $table = $this->connection->parseSqlTable($table); + + if (isset($options['alias'][$table])) { + $item[] = $this->parseKey($query, $table) . ' ' . $this->parseKey($query, $options['alias'][$table]); + } else { + $item[] = $this->parseKey($query, $table); + } + } + } + + return implode(',', $item); + } + + /** + * where分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $where 查询条件 + * @return string + */ + protected function parseWhere(Query $query, $where) + { + $options = $query->getOptions(); + $whereStr = $this->buildWhere($query, $where); + + if (!empty($options['soft_delete'])) { + // 附加软删除条件 + list($field, $condition) = $options['soft_delete']; + + $binds = $this->connection->getFieldsBind($options['table']); + $whereStr = $whereStr ? '( ' . $whereStr . ' ) AND ' : ''; + $whereStr = $whereStr . $this->parseWhereItem($query, $field, $condition, '', $binds); + } + + return empty($whereStr) ? '' : ' WHERE ' . $whereStr; + } + + /** + * 生成查询条件SQL + * @access public + * @param Query $query 查询对象 + * @param mixed $where 查询条件 + * @return string + */ + public function buildWhere(Query $query, $where) + { + if (empty($where)) { + $where = []; + } + + $whereStr = ''; + $binds = $this->connection->getFieldsBind($query->getOptions('table')); + + foreach ($where as $logic => $val) { + $str = []; + + foreach ($val as $value) { + if ($value instanceof Expression) { + $str[] = ' ' . $logic . ' ( ' . $value->getValue() . ' )'; + continue; + } + + if (is_array($value)) { + if (key($value) !== 0) { + throw new Exception('where express error:' . var_export($value, true)); + } + $field = array_shift($value); + } elseif (!($value instanceof \Closure)) { + throw new Exception('where express error:' . var_export($value, true)); + } + + if ($value instanceof \Closure) { + // 使用闭包查询 + $newQuery = $query->newQuery()->setConnection($this->connection); + $value($newQuery); + $whereClause = $this->buildWhere($newQuery, $newQuery->getOptions('where')); + + if (!empty($whereClause)) { + $query->bind($newQuery->getBind(false)); + $str[] = ' ' . $logic . ' ( ' . $whereClause . ' )'; + } + } elseif (is_array($field)) { + array_unshift($value, $field); + $str2 = []; + foreach ($value as $item) { + $str2[] = $this->parseWhereItem($query, array_shift($item), $item, $logic, $binds); + } + + $str[] = ' ' . $logic . ' ( ' . implode(' AND ', $str2) . ' )'; + } elseif (strpos($field, '|')) { + // 不同字段使用相同查询条件(OR) + $array = explode('|', $field); + $item = []; + + foreach ($array as $k) { + $item[] = $this->parseWhereItem($query, $k, $value, '', $binds); + } + + $str[] = ' ' . $logic . ' ( ' . implode(' OR ', $item) . ' )'; + } elseif (strpos($field, '&')) { + // 不同字段使用相同查询条件(AND) + $array = explode('&', $field); + $item = []; + + foreach ($array as $k) { + $item[] = $this->parseWhereItem($query, $k, $value, '', $binds); + } + + $str[] = ' ' . $logic . ' ( ' . implode(' AND ', $item) . ' )'; + } else { + // 对字段使用表达式查询 + $field = is_string($field) ? $field : ''; + $str[] = ' ' . $logic . ' ' . $this->parseWhereItem($query, $field, $value, $logic, $binds); + } + } + + $whereStr .= empty($whereStr) ? substr(implode(' ', $str), strlen($logic) + 1) : implode(' ', $str); + } + + return $whereStr; + } + + // where子单元分析 + protected function parseWhereItem(Query $query, $field, $val, $rule = '', $binds = []) + { + // 字段分析 + $key = $field ? $this->parseKey($query, $field, true) : ''; + + // 查询规则和条件 + if (!is_array($val)) { + $val = is_null($val) ? ['NULL', ''] : ['=', $val]; + } + + list($exp, $value) = $val; + + // 对一个字段使用多个查询条件 + if (is_array($exp)) { + $item = array_pop($val); + + // 传入 or 或者 and + if (is_string($item) && in_array($item, ['AND', 'and', 'OR', 'or'])) { + $rule = $item; + } else { + array_push($val, $item); + } + + foreach ($val as $k => $item) { + $str[] = $this->parseWhereItem($query, $field, $item, $rule, $binds); + } + + return '( ' . implode(' ' . $rule . ' ', $str) . ' )'; + } + + // 检测操作符 + $exp = strtoupper($exp); + if (isset($this->exp[$exp])) { + $exp = $this->exp[$exp]; + } + + if ($value instanceof Expression) { + + } elseif (is_object($value) && method_exists($value, '__toString')) { + // 对象数据写入 + $value = $value->__toString(); + } + + if (strpos($field, '->')) { + $jsonType = $query->getJsonFieldType($field); + $bindType = $this->connection->getFieldBindType($jsonType); + } else { + $bindType = isset($binds[$field]) && 'LIKE' != $exp ? $binds[$field] : PDO::PARAM_STR; + } + + if (is_scalar($value) && !in_array($exp, ['EXP', 'NOT NULL', 'NULL', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN']) && strpos($exp, 'TIME') === false) { + if (0 === strpos($value, ':') && $query->isBind(substr($value, 1))) { + } else { + $name = $query->bind($value, $bindType); + $value = ':' . $name; + } + } + + // 解析查询表达式 + foreach ($this->parser as $fun => $parse) { + if (in_array($exp, $parse)) { + $whereStr = $this->$fun($query, $key, $exp, $value, $field, $bindType, isset($val[2]) ? $val[2] : 'AND'); + break; + } + } + + if (!isset($whereStr)) { + throw new Exception('where express error:' . $exp); + } + + return $whereStr; + } + + /** + * 模糊查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @param integer $bindType + * @param string $logic + * @return string + */ + protected function parseLike(Query $query, $key, $exp, $value, $field, $bindType, $logic) + { + // 模糊匹配 + if (is_array($value)) { + foreach ($value as $item) { + $name = $query->bind($item, PDO::PARAM_STR); + $array[] = $key . ' ' . $exp . ' :' . $name; + } + + $whereStr = '(' . implode(' ' . strtoupper($logic) . ' ', $array) . ')'; + } else { + $whereStr = $key . ' ' . $exp . ' ' . $value; + } + + return $whereStr; + } + + /** + * 表达式查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param array $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseColumn(Query $query, $key, $exp, array $value, $field, $bindType) + { + // 字段比较查询 + list($op, $field2) = $value; + + if (!in_array($op, ['=', '<>', '>', '>=', '<', '<='])) { + throw new Exception('where express error:' . var_export($value, true)); + } + + return '( ' . $key . ' ' . $op . ' ' . $this->parseKey($query, $field2, true) . ' )'; + } + + /** + * 表达式查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param Expression $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseExp(Query $query, $key, $exp, Expression $value, $field, $bindType) + { + // 表达式查询 + return '( ' . $key . ' ' . $value->getValue() . ' )'; + } + + /** + * Null查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseNull(Query $query, $key, $exp, $value, $field, $bindType) + { + // NULL 查询 + return $key . ' IS ' . $exp; + } + + /** + * 范围查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseBetween(Query $query, $key, $exp, $value, $field, $bindType) + { + // BETWEEN 查询 + $data = is_array($value) ? $value : explode(',', $value); + + $min = $query->bind($data[0], $bindType); + $max = $query->bind($data[1], $bindType); + + return $key . ' ' . $exp . ' :' . $min . ' AND :' . $max . ' '; + } + + /** + * Exists查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseExists(Query $query, $key, $exp, $value, $field, $bindType) + { + // EXISTS 查询 + if ($value instanceof \Closure) { + $value = $this->parseClosure($query, $value, false); + } elseif ($value instanceof Expression) { + $value = $value->getValue(); + } else { + throw new Exception('where express error:' . $value); + } + + return $exp . ' (' . $value . ')'; + } + + /** + * 时间比较查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseTime(Query $query, $key, $exp, $value, $field, $bindType) + { + return $key . ' ' . substr($exp, 0, 2) . ' ' . $this->parseDateTime($query, $value, $field, $bindType); + } + + /** + * 大小比较查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseCompare(Query $query, $key, $exp, $value, $field, $bindType) + { + if (is_array($value)) { + throw new Exception('where express error:' . $exp . var_export($value, true)); + } + + // 比较运算 + if ($value instanceof \Closure) { + $value = $this->parseClosure($query, $value); + } + + if ('=' == $exp && is_null($value)) { + return $key . ' IS NULL'; + } + + return $key . ' ' . $exp . ' ' . $value; + } + + /** + * 时间范围查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseBetweenTime(Query $query, $key, $exp, $value, $field, $bindType) + { + if (is_string($value)) { + $value = explode(',', $value); + } + + return $key . ' ' . substr($exp, 0, -4) + . $this->parseDateTime($query, $value[0], $field, $bindType) + . ' AND ' + . $this->parseDateTime($query, $value[1], $field, $bindType); + + } + + /** + * IN查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @param integer $bindType + * @return string + */ + protected function parseIn(Query $query, $key, $exp, $value, $field, $bindType) + { + // IN 查询 + if ($value instanceof \Closure) { + $value = $this->parseClosure($query, $value, false); + } elseif ($value instanceof Expression) { + $value = $value->getValue(); + } else { + $value = array_unique(is_array($value) ? $value : explode(',', $value)); + $array = []; + + foreach ($value as $k => $v) { + $name = $query->bind($v, $bindType); + $array[] = ':' . $name; + } + + if (count($array) == 1) { + return $key . ('IN' == $exp ? ' = ' : ' <> ') . $array[0]; + } else { + $zone = implode(',', $array); + $value = empty($zone) ? "''" : $zone; + } + } + + return $key . ' ' . $exp . ' (' . $value . ')'; + } + + /** + * 闭包子查询 + * @access protected + * @param Query $query 查询对象 + * @param \Closure $call + * @param bool $show + * @return string + */ + protected function parseClosure(Query $query, $call, $show = true) + { + $newQuery = $query->newQuery()->removeOption(); + $call($newQuery); + + return $newQuery->buildSql($show); + } + + /** + * 日期时间条件解析 + * @access protected + * @param Query $query 查询对象 + * @param string $value + * @param string $key + * @param integer $bindType + * @return string + */ + protected function parseDateTime(Query $query, $value, $key, $bindType = null) + { + $options = $query->getOptions(); + + // 获取时间字段类型 + if (strpos($key, '.')) { + list($table, $key) = explode('.', $key); + + if (isset($options['alias']) && $pos = array_search($table, $options['alias'])) { + $table = $pos; + } + } else { + $table = $options['table']; + } + + $type = $this->connection->getTableInfo($table, 'type'); + + if (isset($type[$key])) { + $info = $type[$key]; + } + + if (isset($info)) { + if (is_string($value)) { + $value = strtotime($value) ?: $value; + } + + if (preg_match('/(datetime|timestamp)/is', $info)) { + // 日期及时间戳类型 + $value = date('Y-m-d H:i:s', $value); + } elseif (preg_match('/(date)/is', $info)) { + // 日期及时间戳类型 + $value = date('Y-m-d', $value); + } + } + + $name = $query->bind($value, $bindType); + + return ':' . $name; + } + + /** + * limit分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $limit + * @return string + */ + protected function parseLimit(Query $query, $limit) + { + return (!empty($limit) && false === strpos($limit, '(')) ? ' LIMIT ' . $limit . ' ' : ''; + } + + /** + * join分析 + * @access protected + * @param Query $query 查询对象 + * @param array $join + * @return string + */ + protected function parseJoin(Query $query, $join) + { + $joinStr = ''; + + if (!empty($join)) { + foreach ($join as $item) { + list($table, $type, $on) = $item; + + $condition = []; + + foreach ((array) $on as $val) { + if ($val instanceof Expression) { + $condition[] = $val->getValue(); + } elseif (strpos($val, '=')) { + list($val1, $val2) = explode('=', $val, 2); + + $condition[] = $this->parseKey($query, $val1) . '=' . $this->parseKey($query, $val2); + } else { + $condition[] = $val; + } + } + + $table = $this->parseTable($query, $table); + + $joinStr .= ' ' . $type . ' JOIN ' . $table . ' ON ' . implode(' AND ', $condition); + } + } + + return $joinStr; + } + + /** + * order分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $order + * @return string + */ + protected function parseOrder(Query $query, $order) + { + foreach ($order as $key => $val) { + if ($val instanceof Expression) { + $array[] = $val->getValue(); + } elseif (is_array($val) && preg_match('/^[\w\.]+$/', $key)) { + $array[] = $this->parseOrderField($query, $key, $val); + } elseif ('[rand]' == $val) { + $array[] = $this->parseRand($query); + } elseif (is_string($val)) { + if (is_numeric($key)) { + list($key, $sort) = explode(' ', strpos($val, ' ') ? $val : $val . ' '); + } else { + $sort = $val; + } + + if (preg_match('/^[\w\.]+$/', $key)) { + $sort = strtoupper($sort); + $sort = in_array($sort, ['ASC', 'DESC'], true) ? ' ' . $sort : ''; + $array[] = $this->parseKey($query, $key, true) . $sort; + } else { + throw new Exception('order express error:' . $key); + } + } + } + + return empty($array) ? '' : ' ORDER BY ' . implode(',', $array); + } + + /** + * orderField分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $key + * @param array $val + * @return string + */ + protected function parseOrderField($query, $key, $val) + { + if (isset($val['sort'])) { + $sort = $val['sort']; + unset($val['sort']); + } else { + $sort = ''; + } + + $sort = strtoupper($sort); + $sort = in_array($sort, ['ASC', 'DESC'], true) ? ' ' . $sort : ''; + + $options = $query->getOptions(); + $bind = $this->connection->getFieldsBind($options['table']); + + foreach ($val as $k => $item) { + $val[$k] = $this->parseDataBind($query, $key, $item, $bind); + } + + return 'field(' . $this->parseKey($query, $key, true) . ',' . implode(',', $val) . ')' . $sort; + } + + /** + * group分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $group + * @return string + */ + protected function parseGroup(Query $query, $group) + { + if (empty($group)) { + return ''; + } + + if (is_string($group)) { + $group = explode(',', $group); + } + + foreach ($group as $key) { + $val[] = $this->parseKey($query, $key); + } + + return ' GROUP BY ' . implode(',', $val); + } + + /** + * having分析 + * @access protected + * @param Query $query 查询对象 + * @param string $having + * @return string + */ + protected function parseHaving(Query $query, $having) + { + return !empty($having) ? ' HAVING ' . $having : ''; + } + + /** + * comment分析 + * @access protected + * @param Query $query 查询对象 + * @param string $comment + * @return string + */ + protected function parseComment(Query $query, $comment) + { + if (false !== strpos($comment, '*/')) { + $comment = strstr($comment, '*/', true); + } + + return !empty($comment) ? ' /* ' . $comment . ' */' : ''; + } + + /** + * distinct分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $distinct + * @return string + */ + protected function parseDistinct(Query $query, $distinct) + { + return !empty($distinct) ? ' DISTINCT ' : ''; + } + + /** + * union分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $union + * @return string + */ + protected function parseUnion(Query $query, $union) + { + if (empty($union)) { + return ''; + } + + $type = $union['type']; + unset($union['type']); + + foreach ($union as $u) { + if ($u instanceof \Closure) { + $sql[] = $type . ' ' . $this->parseClosure($query, $u); + } elseif (is_string($u)) { + $sql[] = $type . ' ( ' . $this->connection->parseSqlTable($u) . ' )'; + } + } + + return ' ' . implode(' ', $sql); + } + + /** + * index分析,可在操作链中指定需要强制使用的索引 + * @access protected + * @param Query $query 查询对象 + * @param mixed $index + * @return string + */ + protected function parseForce(Query $query, $index) + { + if (empty($index)) { + return ''; + } + + return sprintf(" FORCE INDEX ( %s ) ", is_array($index) ? implode(',', $index) : $index); + } + + /** + * 设置锁机制 + * @access protected + * @param Query $query 查询对象 + * @param bool|string $lock + * @return string + */ + protected function parseLock(Query $query, $lock = false) + { + if (is_bool($lock)) { + return $lock ? ' FOR UPDATE ' : ''; + } elseif (is_string($lock) && !empty($lock)) { + return ' ' . trim($lock) . ' '; + } + } + + /** + * 生成查询SQL + * @access public + * @param Query $query 查询对象 + * @return string + */ + public function select(Query $query) + { + $options = $query->getOptions(); + + return str_replace( + ['%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'], + [ + $this->parseTable($query, $options['table']), + $this->parseDistinct($query, $options['distinct']), + $this->parseField($query, $options['field']), + $this->parseJoin($query, $options['join']), + $this->parseWhere($query, $options['where']), + $this->parseGroup($query, $options['group']), + $this->parseHaving($query, $options['having']), + $this->parseOrder($query, $options['order']), + $this->parseLimit($query, $options['limit']), + $this->parseUnion($query, $options['union']), + $this->parseLock($query, $options['lock']), + $this->parseComment($query, $options['comment']), + $this->parseForce($query, $options['force']), + ], + $this->selectSql); + } + + /** + * 生成Insert SQL + * @access public + * @param Query $query 查询对象 + * @param bool $replace 是否replace + * @return string + */ + public function insert(Query $query, $replace = false) + { + $options = $query->getOptions(); + + // 分析并处理数据 + $data = $this->parseData($query, $options['data']); + if (empty($data)) { + return ''; + } + + $fields = array_keys($data); + $values = array_values($data); + + return str_replace( + ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], + [ + $replace ? 'REPLACE' : 'INSERT', + $this->parseTable($query, $options['table']), + implode(' , ', $fields), + implode(' , ', $values), + $this->parseComment($query, $options['comment']), + ], + $this->insertSql); + } + + /** + * 生成insertall SQL + * @access public + * @param Query $query 查询对象 + * @param array $dataSet 数据集 + * @param bool $replace 是否replace + * @return string + */ + public function insertAll(Query $query, $dataSet, $replace = false) + { + $options = $query->getOptions(); + + // 获取合法的字段 + if ('*' == $options['field']) { + $allowFields = $this->connection->getTableFields($options['table']); + } else { + $allowFields = $options['field']; + } + + // 获取绑定信息 + $bind = $this->connection->getFieldsBind($options['table']); + + foreach ($dataSet as $data) { + $data = $this->parseData($query, $data, $allowFields, $bind); + + $values[] = 'SELECT ' . implode(',', array_values($data)); + + if (!isset($insertFields)) { + $insertFields = array_keys($data); + } + } + + $fields = []; + + foreach ($insertFields as $field) { + $fields[] = $this->parseKey($query, $field); + } + + return str_replace( + ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], + [ + $replace ? 'REPLACE' : 'INSERT', + $this->parseTable($query, $options['table']), + implode(' , ', $fields), + implode(' UNION ALL ', $values), + $this->parseComment($query, $options['comment']), + ], + $this->insertAllSql); + } + + /** + * 生成slect insert SQL + * @access public + * @param Query $query 查询对象 + * @param array $fields 数据 + * @param string $table 数据表 + * @return string + */ + public function selectInsert(Query $query, $fields, $table) + { + if (is_string($fields)) { + $fields = explode(',', $fields); + } + + foreach ($fields as &$field) { + $field = $this->parseKey($query, $field, true); + } + + return 'INSERT INTO ' . $this->parseTable($query, $table) . ' (' . implode(',', $fields) . ') ' . $this->select($query); + } + + /** + * 生成update SQL + * @access public + * @param Query $query 查询对象 + * @return string + */ + public function update(Query $query) + { + $options = $query->getOptions(); + + $data = $this->parseData($query, $options['data']); + + if (empty($data)) { + return ''; + } + + foreach ($data as $key => $val) { + $set[] = $key . ' = ' . $val; + } + + return str_replace( + ['%TABLE%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'], + [ + $this->parseTable($query, $options['table']), + implode(' , ', $set), + $this->parseJoin($query, $options['join']), + $this->parseWhere($query, $options['where']), + $this->parseOrder($query, $options['order']), + $this->parseLimit($query, $options['limit']), + $this->parseLock($query, $options['lock']), + $this->parseComment($query, $options['comment']), + ], + $this->updateSql); + } + + /** + * 生成delete SQL + * @access public + * @param Query $query 查询对象 + * @return string + */ + public function delete(Query $query) + { + $options = $query->getOptions(); + + return str_replace( + ['%TABLE%', '%USING%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'], + [ + $this->parseTable($query, $options['table']), + !empty($options['using']) ? ' USING ' . $this->parseTable($query, $options['using']) . ' ' : '', + $this->parseJoin($query, $options['join']), + $this->parseWhere($query, $options['where']), + $this->parseOrder($query, $options['order']), + $this->parseLimit($query, $options['limit']), + $this->parseLock($query, $options['lock']), + $this->parseComment($query, $options['comment']), + ], + $this->deleteSql); + } +} diff --git a/vendor/topthink/framework/library/think/db/Connection.php b/vendor/topthink/framework/library/think/db/Connection.php new file mode 100644 index 0000000..18b4885 --- /dev/null +++ b/vendor/topthink/framework/library/think/db/Connection.php @@ -0,0 +1,2152 @@ + +// +---------------------------------------------------------------------- + +namespace think\db; + +use InvalidArgumentException; +use PDO; +use PDOStatement; +use think\Container; +use think\Db; +use think\db\exception\BindParamException; +use think\Debug; +use think\Exception; +use think\exception\PDOException; +use think\Loader; + +abstract class Connection +{ + const PARAM_FLOAT = 21; + protected static $instance = []; + /** @var PDOStatement PDO操作实例 */ + protected $PDOStatement; + + /** @var string 当前SQL指令 */ + protected $queryStr = ''; + // 返回或者影响记录数 + protected $numRows = 0; + // 事务指令数 + protected $transTimes = 0; + // 错误信息 + protected $error = ''; + + /** @var PDO[] 数据库连接ID 支持多个连接 */ + protected $links = []; + + /** @var PDO 当前连接ID */ + protected $linkID; + protected $linkRead; + protected $linkWrite; + + // 查询结果类型 + protected $fetchType = PDO::FETCH_ASSOC; + // 字段属性大小写 + protected $attrCase = PDO::CASE_LOWER; + // 监听回调 + protected static $event = []; + + // 数据表信息 + protected static $info = []; + + // 使用Builder类 + protected $builderClassName; + // Builder对象 + protected $builder; + // 数据库连接参数配置 + protected $config = [ + // 数据库类型 + 'type' => '', + // 服务器地址 + 'hostname' => '', + // 数据库名 + 'database' => '', + // 用户名 + 'username' => '', + // 密码 + 'password' => '', + // 端口 + 'hostport' => '', + // 连接dsn + 'dsn' => '', + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => '', + // 数据库调试模式 + 'debug' => false, + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 模型写入后自动读取主服务器 + 'read_master' => false, + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 数据集返回类型 + 'resultset_type' => '', + // 自动写入时间戳字段 + 'auto_timestamp' => false, + // 时间字段取出后的默认时间格式 + 'datetime_format' => 'Y-m-d H:i:s', + // 是否需要进行SQL性能分析 + 'sql_explain' => false, + // Builder类 + 'builder' => '', + // Query类 + 'query' => '\\think\\db\\Query', + // 是否需要断线重连 + 'break_reconnect' => false, + // 断线标识字符串 + 'break_match_str' => [], + ]; + + // PDO连接参数 + protected $params = [ + PDO::ATTR_CASE => PDO::CASE_NATURAL, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, + PDO::ATTR_STRINGIFY_FETCHES => false, + PDO::ATTR_EMULATE_PREPARES => false, + ]; + + // 服务器断线标识字符 + protected $breakMatchStr = [ + 'server has gone away', + 'no connection to the server', + 'Lost connection', + 'is dead or not enabled', + 'Error while sending', + 'decryption failed or bad record mac', + 'server closed the connection unexpectedly', + 'SSL connection has been closed unexpectedly', + 'Error writing data to the connection', + 'Resource deadlock avoided', + 'failed with errno', + ]; + + // 绑定参数 + protected $bind = []; + + /** + * 架构函数 读取数据库配置信息 + * @access public + * @param array $config 数据库配置数组 + */ + public function __construct(array $config = []) + { + if (!empty($config)) { + $this->config = array_merge($this->config, $config); + } + + // 创建Builder对象 + $class = $this->getBuilderClass(); + + $this->builder = new $class($this); + + // 执行初始化操作 + $this->initialize(); + } + + /** + * 初始化 + * @access protected + * @return void + */ + protected function initialize() + {} + + /** + * 取得数据库连接类实例 + * @access public + * @param mixed $config 连接配置 + * @param bool|string $name 连接标识 true 强制重新连接 + * @return Connection + * @throws Exception + */ + public static function instance($config = [], $name = false) + { + if (false === $name) { + $name = md5(serialize($config)); + } + + if (true === $name || !isset(self::$instance[$name])) { + if (empty($config['type'])) { + throw new InvalidArgumentException('Undefined db type'); + } + + // 记录初始化信息 + Container::get('app')->log('[ DB ] INIT ' . $config['type']); + + if (true === $name) { + $name = md5(serialize($config)); + } + + self::$instance[$name] = Loader::factory($config['type'], '\\think\\db\\connector\\', $config); + } + + return self::$instance[$name]; + } + + /** + * 获取当前连接器类对应的Builder类 + * @access public + * @return string + */ + public function getBuilderClass() + { + if (!empty($this->builderClassName)) { + return $this->builderClassName; + } + + return $this->getConfig('builder') ?: '\\think\\db\\builder\\' . ucfirst($this->getConfig('type')); + } + + /** + * 设置当前的数据库Builder对象 + * @access protected + * @param Builder $builder + * @return void + */ + protected function setBuilder(Builder $builder) + { + $this->builder = $builder; + + return $this; + } + + /** + * 获取当前的builder实例对象 + * @access public + * @return Builder + */ + public function getBuilder() + { + return $this->builder; + } + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + abstract protected function parseDsn($config); + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + abstract public function getFields($tableName); + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + abstract public function getTables($dbName); + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + abstract protected function getExplain($sql); + + /** + * 对返数据表字段信息进行大小写转换出来 + * @access public + * @param array $info 字段信息 + * @return array + */ + public function fieldCase($info) + { + // 字段大小写转换 + switch ($this->attrCase) { + case PDO::CASE_LOWER: + $info = array_change_key_case($info); + break; + case PDO::CASE_UPPER: + $info = array_change_key_case($info, CASE_UPPER); + break; + case PDO::CASE_NATURAL: + default: + // 不做转换 + } + + return $info; + } + + /** + * 获取字段绑定类型 + * @access public + * @param string $type 字段类型 + * @return integer + */ + public function getFieldBindType($type) + { + if (0 === strpos($type, 'set') || 0 === strpos($type, 'enum')) { + $bind = PDO::PARAM_STR; + } elseif (preg_match('/(double|float|decimal|real|numeric)/is', $type)) { + $bind = self::PARAM_FLOAT; + } elseif (preg_match('/(int|serial|bit)/is', $type)) { + $bind = PDO::PARAM_INT; + } elseif (preg_match('/bool/is', $type)) { + $bind = PDO::PARAM_BOOL; + } else { + $bind = PDO::PARAM_STR; + } + + return $bind; + } + + /** + * 将SQL语句中的__TABLE_NAME__字符串替换成带前缀的表名(小写) + * @access public + * @param string $sql sql语句 + * @return string + */ + public function parseSqlTable($sql) + { + if (false !== strpos($sql, '__')) { + $sql = preg_replace_callback("/__([A-Z0-9_-]+)__/sU", function ($match) { + return $this->getConfig('prefix') . strtolower($match[1]); + }, $sql); + } + + return $sql; + } + + /** + * 获取数据表信息 + * @access public + * @param mixed $tableName 数据表名 留空自动获取 + * @param string $fetch 获取信息类型 包括 fields type bind pk + * @return mixed + */ + public function getTableInfo($tableName, $fetch = '') + { + if (is_array($tableName)) { + $tableName = key($tableName) ?: current($tableName); + } + + if (strpos($tableName, ',')) { + // 多表不获取字段信息 + return false; + } else { + $tableName = $this->parseSqlTable($tableName); + } + + // 修正子查询作为表名的问题 + if (strpos($tableName, ')')) { + return []; + } + + list($tableName) = explode(' ', $tableName); + + if (false === strpos($tableName, '.')) { + $schema = $this->getConfig('database') . '.' . $tableName; + } else { + $schema = $tableName; + } + + if (!isset(self::$info[$schema])) { + // 读取缓存 + $cacheFile = Container::get('app')->getRuntimePath() . 'schema' . DIRECTORY_SEPARATOR . $schema . '.php'; + + if (!$this->config['debug'] && is_file($cacheFile)) { + $info = include $cacheFile; + } else { + $info = $this->getFields($tableName); + } + + $fields = array_keys($info); + $bind = $type = []; + + foreach ($info as $key => $val) { + // 记录字段类型 + $type[$key] = $val['type']; + $bind[$key] = $this->getFieldBindType($val['type']); + + if (!empty($val['primary'])) { + $pk[] = $key; + } + } + + if (isset($pk)) { + // 设置主键 + $pk = count($pk) > 1 ? $pk : $pk[0]; + } else { + $pk = null; + } + + self::$info[$schema] = ['fields' => $fields, 'type' => $type, 'bind' => $bind, 'pk' => $pk]; + } + + return $fetch ? self::$info[$schema][$fetch] : self::$info[$schema]; + } + + /** + * 获取数据表的主键 + * @access public + * @param string $tableName 数据表名 + * @return string|array + */ + public function getPk($tableName) + { + return $this->getTableInfo($tableName, 'pk'); + } + + /** + * 获取数据表字段信息 + * @access public + * @param string $tableName 数据表名 + * @return array + */ + public function getTableFields($tableName) + { + return $this->getTableInfo($tableName, 'fields'); + } + + /** + * 获取数据表字段类型 + * @access public + * @param string $tableName 数据表名 + * @param string $field 字段名 + * @return array|string + */ + public function getFieldsType($tableName, $field = null) + { + $result = $this->getTableInfo($tableName, 'type'); + + if ($field && isset($result[$field])) { + return $result[$field]; + } + + return $result; + } + + /** + * 获取数据表绑定信息 + * @access public + * @param string $tableName 数据表名 + * @return array + */ + public function getFieldsBind($tableName) + { + return $this->getTableInfo($tableName, 'bind'); + } + + /** + * 获取数据库的配置参数 + * @access public + * @param string $config 配置名称 + * @return mixed + */ + public function getConfig($config = '') + { + return $config ? $this->config[$config] : $this->config; + } + + /** + * 设置数据库的配置参数 + * @access public + * @param string|array $config 配置名称 + * @param mixed $value 配置值 + * @return void + */ + public function setConfig($config, $value = '') + { + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } else { + $this->config[$config] = $value; + } + } + + /** + * 连接数据库方法 + * @access public + * @param array $config 连接参数 + * @param integer $linkNum 连接序号 + * @param array|bool $autoConnection 是否自动连接主数据库(用于分布式) + * @return PDO + * @throws Exception + */ + public function connect(array $config = [], $linkNum = 0, $autoConnection = false) + { + if (isset($this->links[$linkNum])) { + return $this->links[$linkNum]; + } + + if (!$config) { + $config = $this->config; + } else { + $config = array_merge($this->config, $config); + } + + // 连接参数 + if (isset($config['params']) && is_array($config['params'])) { + $params = $config['params'] + $this->params; + } else { + $params = $this->params; + } + + // 记录当前字段属性大小写设置 + $this->attrCase = $params[PDO::ATTR_CASE]; + + if (!empty($config['break_match_str'])) { + $this->breakMatchStr = array_merge($this->breakMatchStr, (array) $config['break_match_str']); + } + + try { + if (empty($config['dsn'])) { + $config['dsn'] = $this->parseDsn($config); + } + + if ($config['debug']) { + $startTime = microtime(true); + } + + $this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $params); + + if ($config['debug']) { + // 记录数据库连接信息 + $this->log('[ DB ] CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn']); + } + + return $this->links[$linkNum]; + } catch (\PDOException $e) { + if ($autoConnection) { + $this->log($e->getMessage(), 'error'); + return $this->connect($autoConnection, $linkNum); + } else { + throw $e; + } + } + } + + /** + * 释放查询结果 + * @access public + */ + public function free() + { + $this->PDOStatement = null; + } + + /** + * 获取PDO对象 + * @access public + * @return \PDO|false + */ + public function getPdo() + { + if (!$this->linkID) { + return false; + } + + return $this->linkID; + } + + /** + * 执行查询 使用生成器返回数据 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param bool $master 是否在主服务器读操作 + * @param Model $model 模型对象实例 + * @param array $condition 查询条件 + * @param mixed $relation 关联查询 + * @return \Generator + */ + public function getCursor($sql, $bind = [], $master = false, $model = null, $condition = null, $relation = null) + { + $this->initConnect($master); + + // 记录SQL语句 + $this->queryStr = $sql; + + $this->bind = $bind; + + Db::$queryTimes++; + + // 调试开始 + $this->debug(true); + + // 预处理 + $this->PDOStatement = $this->linkID->prepare($sql); + + // 是否为存储过程调用 + $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']); + + // 参数绑定 + if ($procedure) { + $this->bindParam($bind); + } else { + $this->bindValue($bind); + } + + // 执行查询 + $this->PDOStatement->execute(); + + // 调试结束 + $this->debug(false, '', $master); + + // 返回结果集 + while ($result = $this->PDOStatement->fetch($this->fetchType)) { + if ($model) { + $instance = $model->newInstance($result, $condition); + + if ($relation) { + $instance->relationQuery($relation); + } + + yield $instance; + } else { + yield $result; + } + } + } + + /** + * 执行查询 返回数据集 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param bool $master 是否在主服务器读操作 + * @param bool $pdo 是否返回PDO对象 + * @return array + * @throws BindParamException + * @throws \PDOException + * @throws \Exception + * @throws \Throwable + */ + public function query($sql, $bind = [], $master = false, $pdo = false) + { + $this->initConnect($master); + + if (!$this->linkID) { + return false; + } + + // 记录SQL语句 + $this->queryStr = $sql; + + $this->bind = $bind; + + Db::$queryTimes++; + + try { + // 调试开始 + $this->debug(true); + + // 预处理 + $this->PDOStatement = $this->linkID->prepare($sql); + + // 是否为存储过程调用 + $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']); + + // 参数绑定 + if ($procedure) { + $this->bindParam($bind); + } else { + $this->bindValue($bind); + } + + // 执行查询 + $this->PDOStatement->execute(); + + // 调试结束 + $this->debug(false, '', $master); + + // 返回结果集 + return $this->getResult($pdo, $procedure); + } catch (\PDOException $e) { + if ($this->isBreak($e)) { + return $this->close()->query($sql, $bind, $master, $pdo); + } + + throw new PDOException($e, $this->config, $this->getLastsql()); + } 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); + } + + throw $e; + } + } + + /** + * 执行语句 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param Query $query 查询对象 + * @return int + * @throws BindParamException + * @throws \PDOException + * @throws \Exception + * @throws \Throwable + */ + public function execute($sql, $bind = [], Query $query = null) + { + $this->initConnect(true); + + if (!$this->linkID) { + return false; + } + + // 记录SQL语句 + $this->queryStr = $sql; + + $this->bind = $bind; + + Db::$executeTimes++; + try { + // 调试开始 + $this->debug(true); + + // 预处理 + $this->PDOStatement = $this->linkID->prepare($sql); + + // 是否为存储过程调用 + $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']); + + // 参数绑定 + if ($procedure) { + $this->bindParam($bind); + } else { + $this->bindValue($bind); + } + + // 执行语句 + $this->PDOStatement->execute(); + + // 调试结束 + $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, $query); + } + + throw new PDOException($e, $this->config, $this->getLastsql()); + } catch (\Throwable $e) { + if ($this->isBreak($e)) { + 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; + } + } + + /** + * 查找单条记录 + * @access public + * @param Query $query 查询对象 + * @return array|null|\PDOStatement|string + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function find(Query $query) + { + // 分析查询表达式 + $options = $query->getOptions(); + $pk = $query->getPk($options); + + $data = $options['data']; + $query->setOption('limit', 1); + + if (empty($options['fetch_sql']) && !empty($options['cache'])) { + // 判断查询缓存 + $cache = $options['cache']; + + if (is_string($cache['key'])) { + $key = $cache['key']; + } else { + $key = $this->getCacheKey($query, $data); + } + + $result = Container::get('cache')->get($key); + + if (false !== $result) { + return $result; + } + } + + if (is_string($pk) && !is_array($data)) { + if (isset($key) && strpos($key, '|')) { + list($a, $val) = explode('|', $key); + $item[$pk] = $val; + } else { + $item[$pk] = $data; + } + $data = $item; + } + + $query->setOption('data', $data); + + // 生成查询SQL + $sql = $this->builder->select($query); + + $query->removeOption('limit'); + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + // 事件回调 + $result = $query->trigger('before_find'); + + if (!$result) { + // 执行查询 + $resultSet = $this->query($sql, $bind, $options['master'], $options['fetch_pdo']); + + if ($resultSet instanceof \PDOStatement) { + // 返回PDOStatement对象 + return $resultSet; + } + + $result = isset($resultSet[0]) ? $resultSet[0] : null; + } + + if (isset($cache) && $result) { + // 缓存数据 + $this->cacheData($key, $result, $cache); + } + + return $result; + } + + /** + * 使用游标查询记录 + * @access public + * @param Query $query 查询对象 + * @return \Generator + */ + public function cursor(Query $query) + { + // 分析查询表达式 + $options = $query->getOptions(); + + // 生成查询SQL + $sql = $this->builder->select($query); + + $bind = $query->getBind(); + + $condition = isset($options['where']['AND']) ? $options['where']['AND'] : null; + $relation = isset($options['relaltion']) ? $options['relation'] : null; + + // 执行查询操作 + return $this->getCursor($sql, $bind, $options['master'], $query->getModel(), $condition, $relation); + } + + /** + * 查找记录 + * @access public + * @param Query $query 查询对象 + * @return array|\PDOStatement|string + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function select(Query $query) + { + // 分析查询表达式 + $options = $query->getOptions(); + + if (empty($options['fetch_sql']) && !empty($options['cache'])) { + $resultSet = $this->getCacheData($query, $options['cache'], null, $key); + + if (false !== $resultSet) { + return $resultSet; + } + } + + // 生成查询SQL + $sql = $this->builder->select($query); + + $query->removeOption('limit'); + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + $resultSet = $query->trigger('before_select'); + + if (!$resultSet) { + // 执行查询操作 + $resultSet = $this->query($sql, $bind, $options['master'], $options['fetch_pdo']); + + if ($resultSet instanceof \PDOStatement) { + // 返回PDOStatement对象 + return $resultSet; + } + } + + if (!empty($options['cache']) && false !== $resultSet) { + // 缓存数据集 + $this->cacheData($key, $resultSet, $options['cache']); + } + + return $resultSet; + } + + /** + * 插入记录 + * @access public + * @param Query $query 查询对象 + * @param boolean $replace 是否replace + * @param boolean $getLastInsID 返回自增主键 + * @param string $sequence 自增序列名 + * @return integer|string + */ + public function insert(Query $query, $replace = false, $getLastInsID = false, $sequence = null) + { + // 分析查询表达式 + $options = $query->getOptions(); + + // 生成SQL语句 + $sql = $this->builder->insert($query, $replace); + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + // 执行操作 + $result = '' == $sql ? 0 : $this->execute($sql, $bind, $query); + + if ($result) { + $sequence = $sequence ?: (isset($options['sequence']) ? $options['sequence'] : null); + $lastInsId = $this->getLastInsID($sequence); + + $data = $options['data']; + + if ($lastInsId) { + $pk = $query->getPk($options); + if (is_string($pk)) { + $data[$pk] = $lastInsId; + } + } + + $query->setOption('data', $data); + + $query->trigger('after_insert'); + + if ($getLastInsID) { + return $lastInsId; + } + } + + return $result; + } + + /** + * 批量插入记录 + * @access public + * @param Query $query 查询对象 + * @param mixed $dataSet 数据集 + * @param bool $replace 是否replace + * @param integer $limit 每次写入数据限制 + * @return integer|string + * @throws \Exception + * @throws \Throwable + */ + public function insertAll(Query $query, $dataSet = [], $replace = false, $limit = null) + { + if (!is_array(reset($dataSet))) { + return false; + } + + $options = $query->getOptions(); + + if ($limit) { + // 分批写入 自动启动事务支持 + $this->startTrans(); + + try { + $array = array_chunk($dataSet, $limit, true); + $count = 0; + + foreach ($array as $item) { + $sql = $this->builder->insertAll($query, $item, $replace); + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + $fetchSql[] = $this->getRealSql($sql, $bind); + } else { + $count += $this->execute($sql, $bind, $query); + } + } + + // 提交事务 + $this->commit(); + } catch (\Exception $e) { + $this->rollback(); + throw $e; + } catch (\Throwable $e) { + $this->rollback(); + throw $e; + } + + return isset($fetchSql) ? implode(';', $fetchSql) : $count; + } + + $sql = $this->builder->insertAll($query, $dataSet, $replace); + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + return $this->getRealSql($sql, $bind); + } + + return $this->execute($sql, $bind, $query); + } + + /** + * 通过Select方式插入记录 + * @access public + * @param Query $query 查询对象 + * @param string $fields 要插入的数据表字段名 + * @param string $table 要插入的数据表名 + * @return integer|string + * @throws PDOException + */ + public function selectInsert(Query $query, $fields, $table) + { + // 分析查询表达式 + $options = $query->getOptions(); + + $table = $this->parseSqlTable($table); + + $sql = $this->builder->selectInsert($query, $fields, $table); + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + return $this->getRealSql($sql, $bind); + } + + return $this->execute($sql, $bind, $query); + } + + /** + * 更新记录 + * @access public + * @param Query $query 查询对象 + * @return integer|string + * @throws Exception + * @throws PDOException + */ + public function update(Query $query) + { + $options = $query->getOptions(); + + if (isset($options['cache']) && is_string($options['cache']['key'])) { + $key = $options['cache']['key']; + } + + $pk = $query->getPk($options); + $data = $options['data']; + + if (empty($options['where'])) { + // 如果存在主键数据 则自动作为更新条件 + if (is_string($pk) && isset($data[$pk])) { + $where[$pk] = [$pk, '=', $data[$pk]]; + if (!isset($key)) { + $key = $this->getCacheKey($query, $data[$pk]); + } + unset($data[$pk]); + } elseif (is_array($pk)) { + // 增加复合主键支持 + foreach ($pk as $field) { + if (isset($data[$field])) { + $where[$field] = [$field, '=', $data[$field]]; + } else { + // 如果缺少复合主键数据则不执行 + throw new Exception('miss complex primary data'); + } + unset($data[$field]); + } + } + + if (!isset($where)) { + // 如果没有任何更新条件则不执行 + throw new Exception('miss update condition'); + } else { + $options['where']['AND'] = $where; + $query->setOption('where', ['AND' => $where]); + } + } elseif (!isset($key) && is_string($pk) && isset($options['where']['AND'])) { + foreach ($options['where']['AND'] as $val) { + if (is_array($val) && $val[0] == $pk) { + $key = $this->getCacheKey($query, $val); + } + } + } + + // 更新数据 + $query->setOption('data', $data); + + // 生成UPDATE SQL语句 + $sql = $this->builder->update($query); + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + // 检测缓存 + $cache = Container::get('cache'); + + if (isset($key) && $cache->get($key)) { + // 删除缓存 + $cache->rm($key); + } elseif (!empty($options['cache']['tag'])) { + $cache->clear($options['cache']['tag']); + } + + // 执行操作 + $result = '' == $sql ? 0 : $this->execute($sql, $bind, $query); + + if ($result) { + if (is_string($pk) && isset($where[$pk])) { + $data[$pk] = $where[$pk]; + } elseif (is_string($pk) && isset($key) && strpos($key, '|')) { + list($a, $val) = explode('|', $key); + $data[$pk] = $val; + } + + $query->setOption('data', $data); + $query->trigger('after_update'); + } + + return $result; + } + + /** + * 删除记录 + * @access public + * @param Query $query 查询对象 + * @return int + * @throws Exception + * @throws PDOException + */ + public function delete(Query $query) + { + // 分析查询表达式 + $options = $query->getOptions(); + $pk = $query->getPk($options); + $data = $options['data']; + + if (isset($options['cache']) && is_string($options['cache']['key'])) { + $key = $options['cache']['key']; + } elseif (!is_null($data) && true !== $data && !is_array($data)) { + $key = $this->getCacheKey($query, $data); + } elseif (is_string($pk) && isset($options['where']['AND'])) { + foreach ($options['where']['AND'] as $val) { + if (is_array($val) && $val[0] == $pk) { + $key = $this->getCacheKey($query, $val); + } + } + } + + if (true !== $data && empty($options['where'])) { + // 如果条件为空 不进行删除操作 除非设置 1=1 + throw new Exception('delete without condition'); + } + + // 生成删除SQL语句 + $sql = $this->builder->delete($query); + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + // 检测缓存 + $cache = Container::get('cache'); + + if (isset($key) && $cache->get($key)) { + // 删除缓存 + $cache->rm($key); + } elseif (!empty($options['cache']['tag'])) { + $cache->clear($options['cache']['tag']); + } + + // 执行操作 + $result = $this->execute($sql, $bind, $query); + + if ($result) { + if (!is_array($data) && is_string($pk) && isset($key) && strpos($key, '|')) { + list($a, $val) = explode('|', $key); + $item[$pk] = $val; + $data = $item; + } + + $options['data'] = $data; + + $query->trigger('after_delete'); + } + + return $result; + } + + /** + * 得到某个字段的值 + * @access public + * @param Query $query 查询对象 + * @param string $field 字段名 + * @param mixed $default 默认值 + * @param bool $one 是否返回一个值 + * @return mixed + */ + public function value(Query $query, $field, $default = null, $one = true) + { + $options = $query->getOptions(); + + if (isset($options['field'])) { + $query->removeOption('field'); + } + + if (is_string($field)) { + $field = array_map('trim', explode(',', $field)); + } + + $query->setOption('field', $field); + + if (empty($options['fetch_sql']) && !empty($options['cache'])) { + $cache = $options['cache']; + $result = $this->getCacheData($query, $cache, null, $key); + + if (false !== $result) { + return $result; + } + } + + if ($one) { + $query->setOption('limit', 1); + } + + // 生成查询SQL + $sql = $this->builder->select($query); + + if (isset($options['field'])) { + $query->setOption('field', $options['field']); + } else { + $query->removeOption('field'); + } + + $query->removeOption('limit'); + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + // 执行查询操作 + $pdo = $this->query($sql, $bind, $options['master'], true); + + $result = $pdo->fetchColumn(); + + if (isset($cache) && false !== $result) { + // 缓存数据 + $this->cacheData($key, $result, $cache); + } + + return false !== $result ? $result : $default; + } + + /** + * 得到某个字段的值 + * @access public + * @param Query $query 查询对象 + * @param string $aggregate 聚合方法 + * @param mixed $field 字段名 + * @return mixed + */ + public function aggregate(Query $query, $aggregate, $field) + { + if (is_string($field) && 0 === stripos($field, 'DISTINCT ')) { + list($distinct, $field) = explode(' ', $field); + } + + $field = $aggregate . '(' . (!empty($distinct) ? 'DISTINCT ' : '') . $this->builder->parseKey($query, $field, true) . ') AS tp_' . strtolower($aggregate); + + return $this->value($query, $field, 0, false); + } + + /** + * 得到某个列的数组 + * @access public + * @param Query $query 查询对象 + * @param string $field 字段名 多个字段用逗号分隔 + * @param string $key 索引 + * @return array + */ + public function column(Query $query, $field, $key = '') + { + $options = $query->getOptions(); + + if (isset($options['field'])) { + $query->removeOption('field'); + } + + if (is_null($field)) { + $field = ['*']; + } elseif (is_string($field)) { + $field = array_map('trim', explode(',', $field)); + } + + if ($key && ['*'] != $field) { + array_unshift($field, $key); + $field = array_unique($field); + } + + $query->setOption('field', $field); + + if (empty($options['fetch_sql']) && !empty($options['cache'])) { + // 判断查询缓存 + $cache = $options['cache']; + $result = $this->getCacheData($query, $cache, null, $guid); + + if (false !== $result) { + return $result; + } + } + + // 生成查询SQL + $sql = $this->builder->select($query); + + // 还原field参数 + if (isset($options['field'])) { + $query->setOption('field', $options['field']); + } else { + $query->removeOption('field'); + } + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + // 执行查询操作 + $pdo = $this->query($sql, $bind, $options['master'], true); + + if (1 == $pdo->columnCount()) { + $result = $pdo->fetchAll(PDO::FETCH_COLUMN); + } else { + $resultSet = $pdo->fetchAll(PDO::FETCH_ASSOC); + + if (['*'] == $field && $key) { + $result = array_column($resultSet, null, $key); + } elseif ($resultSet) { + $fields = array_keys($resultSet[0]); + $count = count($fields); + $key1 = array_shift($fields); + $key2 = $fields ? array_shift($fields) : ''; + $key = $key ?: $key1; + + if (strpos($key, '.')) { + list($alias, $key) = explode('.', $key); + } + + if (2 == $count) { + $column = $key2; + } elseif (1 == $count) { + $column = $key1; + } else { + $column = null; + } + + $result = array_column($resultSet, $column, $key); + } else { + $result = []; + } + } + + if (isset($cache) && isset($guid)) { + // 缓存数据 + $this->cacheData($guid, $result, $cache); + } + + return $result; + } + + /** + * 执行查询但只返回PDOStatement对象 + * @access public + * @return \PDOStatement|string + */ + public function pdo(Query $query) + { + // 分析查询表达式 + $options = $query->getOptions(); + + // 生成查询SQL + $sql = $this->builder->select($query); + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + // 执行查询操作 + return $this->query($sql, $bind, $options['master'], true); + } + + /** + * 根据参数绑定组装最终的SQL语句 便于调试 + * @access public + * @param string $sql 带参数绑定的sql语句 + * @param array $bind 参数绑定列表 + * @return string + */ + 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; + + if ((self::PARAM_FLOAT == $type || PDO::PARAM_STR == $type) && is_string($value)) { + $value = '\'' . addslashes($value) . '\''; + } elseif (PDO::PARAM_INT == $type && '' === $value) { + $value = 0; + } + + // 判断占位符 + $sql = is_numeric($key) ? + substr_replace($sql, $value, strpos($sql, '?'), 1) : + substr_replace($sql, $value, strpos($sql, ':' . $key), strlen(':' . $key)); + } + + return rtrim($sql); + } + + /** + * 参数绑定 + * 支持 ['name'=>'value','id'=>123] 对应命名占位符 + * 或者 ['value',123] 对应问号占位符 + * @access public + * @param array $bind 要绑定的参数列表 + * @return void + * @throws BindParamException + */ + protected function bindValue(array $bind = []) + { + foreach ($bind as $key => $val) { + // 占位符 + $param = is_int($key) ? $key + 1 : ':' . $key; + + if (is_array($val)) { + if (PDO::PARAM_INT == $val[1] && '' === $val[0]) { + $val[0] = 0; + } elseif (self::PARAM_FLOAT == $val[1]) { + $val[0] = is_string($val[0]) ? (float) $val[0] : $val[0]; + $val[1] = PDO::PARAM_STR; + } + + $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]); + } else { + $result = $this->PDOStatement->bindValue($param, $val); + } + + if (!$result) { + throw new BindParamException( + "Error occurred when binding parameters '{$param}'", + $this->config, + $this->getLastsql(), + $bind + ); + } + } + } + + /** + * 存储过程的输入输出参数绑定 + * @access public + * @param array $bind 要绑定的参数列表 + * @return void + * @throws BindParamException + */ + protected function bindParam($bind) + { + foreach ($bind as $key => $val) { + $param = is_int($key) ? $key + 1 : ':' . $key; + + if (is_array($val)) { + array_unshift($val, $param); + $result = call_user_func_array([$this->PDOStatement, 'bindParam'], $val); + } else { + $result = $this->PDOStatement->bindValue($param, $val); + } + + if (!$result) { + $param = array_shift($val); + + throw new BindParamException( + "Error occurred when binding parameters '{$param}'", + $this->config, + $this->getLastsql(), + $bind + ); + } + } + } + + /** + * 获得数据集数组 + * @access protected + * @param bool $pdo 是否返回PDOStatement + * @param bool $procedure 是否存储过程 + * @return array + */ + protected function getResult($pdo = false, $procedure = false) + { + if ($pdo) { + // 返回PDOStatement对象处理 + return $this->PDOStatement; + } + + if ($procedure) { + // 存储过程返回结果 + return $this->procedure(); + } + + $result = $this->PDOStatement->fetchAll($this->fetchType); + + $this->numRows = count($result); + + return $result; + } + + /** + * 获得存储过程数据集 + * @access protected + * @return array + */ + protected function procedure() + { + $item = []; + + do { + $result = $this->getResult(); + if ($result) { + $item[] = $result; + } + } while ($this->PDOStatement->nextRowset()); + + $this->numRows = count($item); + + return $item; + } + + /** + * 执行数据库事务 + * @access public + * @param callable $callback 数据操作方法回调 + * @return mixed + * @throws PDOException + * @throws \Exception + * @throws \Throwable + */ + public function transaction($callback) + { + $this->startTrans(); + + try { + $result = null; + if (is_callable($callback)) { + $result = call_user_func_array($callback, [$this]); + } + + $this->commit(); + return $result; + } catch (\Exception $e) { + $this->rollback(); + throw $e; + } catch (\Throwable $e) { + $this->rollback(); + throw $e; + } + } + + /** + * 启动XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function startTransXa($xid) + {} + + /** + * 预编译XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function prepareXa($xid) + {} + + /** + * 提交XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function commitXa($xid) + {} + + /** + * 回滚XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function rollbackXa($xid) + {} + + /** + * 启动事务 + * @access public + * @return void + * @throws \PDOException + * @throws \Exception + */ + public function startTrans() + { + $this->initConnect(true); + if (!$this->linkID) { + return false; + } + + ++$this->transTimes; + + try { + if (1 == $this->transTimes) { + $this->linkID->beginTransaction(); + } elseif ($this->transTimes > 1 && $this->supportSavepoint()) { + $this->linkID->exec( + $this->parseSavepoint('trans' . $this->transTimes) + ); + } + } catch (\Exception $e) { + if ($this->isBreak($e)) { + --$this->transTimes; + return $this->close()->startTrans(); + } + throw $e; + } + } + + /** + * 用于非自动提交状态下面的查询提交 + * @access public + * @return void + * @throws PDOException + */ + public function commit() + { + $this->initConnect(true); + + if (1 == $this->transTimes) { + $this->linkID->commit(); + } + + --$this->transTimes; + } + + /** + * 事务回滚 + * @access public + * @return void + * @throws PDOException + */ + public function rollback() + { + $this->initConnect(true); + + if (1 == $this->transTimes) { + $this->linkID->rollBack(); + } elseif ($this->transTimes > 1 && $this->supportSavepoint()) { + $this->linkID->exec( + $this->parseSavepointRollBack('trans' . $this->transTimes) + ); + } + + $this->transTimes = max(0, $this->transTimes - 1); + } + + /** + * 是否支持事务嵌套 + * @return bool + */ + protected function supportSavepoint() + { + return false; + } + + /** + * 生成定义保存点的SQL + * @access protected + * @param $name + * @return string + */ + protected function parseSavepoint($name) + { + return 'SAVEPOINT ' . $name; + } + + /** + * 生成回滚到保存点的SQL + * @access protected + * @param $name + * @return string + */ + protected function parseSavepointRollBack($name) + { + return 'ROLLBACK TO SAVEPOINT ' . $name; + } + + /** + * 批处理执行SQL语句 + * 批处理的指令都认为是execute操作 + * @access public + * @param array $sqlArray SQL批处理指令 + * @param array $bind 参数绑定 + * @return boolean + */ + public function batchQuery($sqlArray = [], $bind = []) + { + if (!is_array($sqlArray)) { + return false; + } + + // 自动启动事务支持 + $this->startTrans(); + + try { + foreach ($sqlArray as $sql) { + $this->execute($sql, $bind); + } + // 提交事务 + $this->commit(); + } catch (\Exception $e) { + $this->rollback(); + throw $e; + } + + return true; + } + + /** + * 获得查询次数 + * @access public + * @param boolean $execute 是否包含所有查询 + * @return integer + */ + public function getQueryTimes($execute = false) + { + return $execute ? Db::$queryTimes + Db::$executeTimes : Db::$queryTimes; + } + + /** + * 获得执行次数 + * @access public + * @return integer + */ + public function getExecuteTimes() + { + return Db::$executeTimes; + } + + /** + * 关闭数据库(或者重新连接) + * @access public + * @return $this + */ + public function close() + { + $this->linkID = null; + $this->linkWrite = null; + $this->linkRead = null; + $this->links = []; + + // 释放查询 + $this->free(); + + return $this; + } + + /** + * 是否断线 + * @access protected + * @param \PDOException|\Exception $e 异常对象 + * @return bool + */ + protected function isBreak($e) + { + if (!$this->config['break_reconnect']) { + return false; + } + + $error = $e->getMessage(); + + foreach ($this->breakMatchStr as $msg) { + if (false !== stripos($error, $msg)) { + return true; + } + } + return false; + } + + /** + * 获取最近一次查询的sql语句 + * @access public + * @return string + */ + public function getLastSql() + { + return $this->getRealSql($this->queryStr, $this->bind); + } + + /** + * 获取最近插入的ID + * @access public + * @param string $sequence 自增序列名 + * @return string + */ + public function getLastInsID($sequence = null) + { + return $this->linkID->lastInsertId($sequence); + } + + /** + * 获取返回或者影响的记录数 + * @access public + * @return integer + */ + public function getNumRows() + { + return $this->numRows; + } + + /** + * 获取最近的错误信息 + * @access public + * @return string + */ + public function getError() + { + if ($this->PDOStatement) { + $error = $this->PDOStatement->errorInfo(); + $error = $error[1] . ':' . $error[2]; + } else { + $error = ''; + } + + if ('' != $this->queryStr) { + $error .= "\n [ SQL语句 ] : " . $this->getLastsql(); + } + + return $error; + } + + /** + * 数据库调试 记录当前SQL及分析性能 + * @access protected + * @param boolean $start 调试开始标记 true 开始 false 结束 + * @param string $sql 执行的SQL语句 留空自动获取 + * @param bool $master 主从标记 + * @return void + */ + protected function debug($start, $sql = '', $master = false) + { + if (!empty($this->config['debug'])) { + // 开启数据库调试模式 + $debug = Container::get('debug'); + + if ($start) { + $debug->remark('queryStartTime', 'time'); + } else { + // 记录操作结束时间 + $debug->remark('queryEndTime', 'time'); + $runtime = $debug->getRangeTime('queryStartTime', 'queryEndTime'); + $sql = $sql ?: $this->getLastsql(); + $result = []; + + // SQL性能分析 + if ($this->config['sql_explain'] && 0 === stripos(trim($sql), 'select')) { + $result = $this->getExplain($sql); + } + + // SQL监听 + $this->triggerSql($sql, $runtime, $result, $master); + } + } + } + + /** + * 监听SQL执行 + * @access public + * @param callable $callback 回调方法 + * @return void + */ + public function listen($callback) + { + self::$event[] = $callback; + } + + /** + * 触发SQL事件 + * @access protected + * @param string $sql SQL语句 + * @param float $runtime SQL运行时间 + * @param mixed $explain SQL分析 + * @param bool $master 主从标记 + * @return void + */ + protected function triggerSql($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, $master]); + } + } + } else { + if ($this->config['deploy']) { + // 分布式记录当前操作的主从 + $master = $master ? 'master|' : 'slave|'; + } else { + $master = ''; + } + + // 未注册监听则记录到日志中 + $this->log('[ SQL ] ' . $sql . ' [ ' . $master . 'RunTime:' . $runtime . 's ]'); + + if (!empty($explain)) { + $this->log('[ EXPLAIN : ' . var_export($explain, true) . ' ]'); + } + } + } + + public function log($log, $type = 'sql') + { + $this->config['debug'] && Container::get('log')->record($log, $type); + } + + /** + * 初始化数据库连接 + * @access protected + * @param boolean $master 是否主服务器 + * @return void + */ + protected function initConnect($master = true) + { + if (!empty($this->config['deploy'])) { + // 采用分布式数据库 + if ($master || $this->transTimes) { + if (!$this->linkWrite) { + $this->linkWrite = $this->multiConnect(true); + } + + $this->linkID = $this->linkWrite; + } else { + if (!$this->linkRead) { + $this->linkRead = $this->multiConnect(false); + } + + $this->linkID = $this->linkRead; + } + } elseif (!$this->linkID) { + // 默认单数据库 + $this->linkID = $this->connect(); + } + } + + /** + * 连接分布式服务器 + * @access protected + * @param boolean $master 主服务器 + * @return PDO + */ + protected function multiConnect($master = false) + { + $_config = []; + + // 分布式数据库配置解析 + foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { + $_config[$name] = is_string($this->config[$name]) ? explode(',', $this->config[$name]) : $this->config[$name]; + } + + // 主服务器序号 + $m = floor(mt_rand(0, $this->config['master_num'] - 1)); + + if ($this->config['rw_separate']) { + // 主从式采用读写分离 + if ($master) // 主服务器写入 + { + $r = $m; + } elseif (is_numeric($this->config['slave_no'])) { + // 指定服务器读 + $r = $this->config['slave_no']; + } else { + // 读操作连接从服务器 每次随机连接的数据库 + $r = floor(mt_rand($this->config['master_num'], count($_config['hostname']) - 1)); + } + } else { + // 读写操作不区分服务器 每次随机连接的数据库 + $r = floor(mt_rand(0, count($_config['hostname']) - 1)); + } + $dbMaster = false; + + if ($m != $r) { + $dbMaster = []; + foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { + $dbMaster[$name] = isset($_config[$name][$m]) ? $_config[$name][$m] : $_config[$name][0]; + } + } + + $dbConfig = []; + + foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { + $dbConfig[$name] = isset($_config[$name][$r]) ? $_config[$name][$r] : $_config[$name][0]; + } + + return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster); + } + + /** + * 析构方法 + * @access public + */ + public function __destruct() + { + // 关闭连接 + $this->close(); + } + + /** + * 缓存数据 + * @access protected + * @param string $key 缓存标识 + * @param mixed $data 缓存数据 + * @param array $config 缓存参数 + */ + protected function cacheData($key, $data, $config = []) + { + $cache = Container::get('cache'); + + if (isset($config['tag'])) { + $cache->tag($config['tag'])->set($key, $data, $config['expire']); + } else { + $cache->set($key, $data, $config['expire']); + } + } + + /** + * 获取缓存数据 + * @access protected + * @param Query $query 查询对象 + * @param mixed $cache 缓存设置 + * @param array $options 缓存 + * @return mixed + */ + protected function getCacheData(Query $query, $cache, $data, &$key = null) + { + // 判断查询缓存 + $key = is_string($cache['key']) ? $cache['key'] : $this->getCacheKey($query, $data); + + return Container::get('cache')->get($key); + } + + /** + * 生成缓存标识 + * @access protected + * @param Query $query 查询对象 + * @param mixed $value 缓存数据 + * @return string + */ + protected function getCacheKey(Query $query, $value) + { + if (is_scalar($value)) { + $data = $value; + } elseif (is_array($value) && isset($value[1], $value[2]) && in_array($value[1], ['=', 'eq'], true) && is_scalar($value[2])) { + $data = $value[2]; + } + + $prefix = 'think:' . $this->getConfig('database') . '.'; + + if (isset($data)) { + return $prefix . $query->getTable() . '|' . $data; + } + + try { + return md5($prefix . serialize($query->getOptions()) . serialize($query->getBind(false))); + } catch (\Exception $e) { + throw new Exception('closure not support cache(true)'); + } + } + +} diff --git a/vendor/topthink/framework/library/think/db/Expression.php b/vendor/topthink/framework/library/think/db/Expression.php new file mode 100644 index 0000000..f1b92ab --- /dev/null +++ b/vendor/topthink/framework/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/vendor/topthink/framework/library/think/db/Query.php b/vendor/topthink/framework/library/think/db/Query.php new file mode 100644 index 0000000..ba08279 --- /dev/null +++ b/vendor/topthink/framework/library/think/db/Query.php @@ -0,0 +1,3766 @@ + +// +---------------------------------------------------------------------- + +namespace think\db; + +use PDO; +use think\Collection; +use think\Container; +use think\Db; +use think\db\exception\BindParamException; +use think\db\exception\DataNotFoundException; +use think\db\exception\ModelNotFoundException; +use think\Exception; +use think\exception\DbException; +use think\exception\PDOException; +use think\Loader; +use think\Model; +use think\model\Collection as ModelCollection; +use think\model\Relation; +use think\model\relation\OneToOne; +use think\Paginator; + +class Query +{ + /** + * 当前数据库连接对象 + * @var Connection + */ + protected $connection; + + /** + * 当前模型对象 + * @var Model + */ + protected $model; + + /** + * 当前数据表名称(不含前缀) + * @var string + */ + protected $name = ''; + + /** + * 当前数据表主键 + * @var string|array + */ + protected $pk; + + /** + * 当前数据表前缀 + * @var string + */ + protected $prefix = ''; + + /** + * 当前查询参数 + * @var array + */ + protected $options = []; + + /** + * 当前参数绑定 + * @var array + */ + protected $bind = []; + + /** + * 事件回调 + * @var array + */ + private static $event = []; + + /** + * 扩展查询方法 + * @var array + */ + private static $extend = []; + + /** + * 读取主库的表 + * @var array + */ + protected static $readMaster = []; + + /** + * 日期查询表达式 + * @var array + */ + protected $timeRule = [ + 'today' => ['today', 'tomorrow -1second'], + 'yesterday' => ['yesterday', 'today -1second'], + 'week' => ['this week 00:00:00', 'next week 00:00:00 -1second'], + 'last week' => ['last week 00:00:00', 'this week 00:00:00 -1second'], + 'month' => ['first Day of this month 00:00:00', 'first Day of next month 00:00:00 -1second'], + 'last month' => ['first Day of last month 00:00:00', 'first Day of this month 00:00:00 -1second'], + 'year' => ['this year 1/1', 'next year 1/1 -1second'], + 'last year' => ['last year 1/1', 'this year 1/1 -1second'], + ]; + + /** + * 日期查询快捷定义 + * @var array + */ + protected $timeExp = ['d' => 'today', 'w' => 'week', 'm' => 'month', 'y' => 'year']; + + /** + * 架构函数 + * @access public + */ + public function __construct(Connection $connection = null) + { + if (is_null($connection)) { + $this->connection = Db::connect(); + } else { + $this->connection = $connection; + } + + $this->prefix = $this->connection->getConfig('prefix'); + } + + /** + * 创建一个新的查询对象 + * @access public + * @return Query + */ + public function newQuery() + { + $query = new static($this->connection); + + if ($this->model) { + $query->model($this->model); + } + + if (isset($this->options['table'])) { + $query->table($this->options['table']); + } else { + $query->name($this->name); + } + + if (isset($this->options['json'])) { + $query->json($this->options['json'], $this->options['json_assoc']); + } + + if (isset($this->options['field_type'])) { + $query->setJsonFieldType($this->options['field_type']); + } + + return $query; + } + + /** + * 利用__call方法实现一些特殊的Model方法 + * @access public + * @param string $method 方法名称 + * @param array $args 调用参数 + * @return mixed + * @throws DbException + * @throws Exception + */ + public function __call($method, $args) + { + if (isset(self::$extend[strtolower($method)])) { + // 调用扩展查询方法 + array_unshift($args, $this); + + return Container::getInstance() + ->invoke(self::$extend[strtolower($method)], $args); + } elseif (strtolower(substr($method, 0, 5)) == 'getby') { + // 根据某个字段获取记录 + $field = Loader::parseName(substr($method, 5)); + return $this->where($field, '=', $args[0])->find(); + } elseif (strtolower(substr($method, 0, 10)) == 'getfieldby') { + // 根据某个字段获取记录的某个值 + $name = Loader::parseName(substr($method, 10)); + return $this->where($name, '=', $args[0])->value($args[1]); + } elseif (strtolower(substr($method, 0, 7)) == 'whereor') { + $name = Loader::parseName(substr($method, 7)); + array_unshift($args, $name); + return call_user_func_array([$this, 'whereOr'], $args); + } elseif (strtolower(substr($method, 0, 5)) == 'where') { + $name = Loader::parseName(substr($method, 5)); + array_unshift($args, $name); + return call_user_func_array([$this, 'where'], $args); + } 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:' . ($this->model ? get_class($this->model) : static::class) . '->' . $method); + } + } + + /** + * 扩展查询方法 + * @access public + * @param string|array $method 查询方法名 + * @param callable $callback + * @return void + */ + public static function extend($method, $callback = null) + { + if (is_array($method)) { + foreach ($method as $key => $val) { + self::$extend[strtolower($key)] = $val; + } + } else { + self::$extend[strtolower($method)] = $callback; + } + } + + /** + * 设置当前的数据库Connection对象 + * @access public + * @param Connection $connection + * @return $this + */ + public function setConnection(Connection $connection) + { + $this->connection = $connection; + $this->prefix = $this->connection->getConfig('prefix'); + + return $this; + } + + /** + * 获取当前的数据库Connection对象 + * @access public + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * 指定模型 + * @access public + * @param Model $model 模型对象实例 + * @return $this + */ + public function model(Model $model) + { + $this->model = $model; + return $this; + } + + /** + * 获取当前的模型对象 + * @access public + * @return Model|null + */ + public function getModel() + { + return $this->model ? $this->model->setQuery($this) : null; + } + + /** + * 设置从主库读取数据 + * @access public + * @param bool $all 是否所有表有效 + * @return $this + */ + public function readMaster($all = false) + { + $table = $all ? '*' : $this->getTable(); + + static::$readMaster[$table] = true; + + return $this; + } + + /** + * 指定当前数据表名(不含前缀) + * @access public + * @param string $name + * @return $this + */ + public function name($name) + { + $this->name = $name; + return $this; + } + + /** + * 获取当前的数据表名称 + * @access public + * @return string + */ + public function getName() + { + return $this->name ?: $this->model->getName(); + } + + /** + * 得到当前或者指定名称的数据表 + * @access public + * @param string $name + * @return string + */ + public function getTable($name = '') + { + if (empty($name) && isset($this->options['table'])) { + return $this->options['table']; + } + + $name = $name ?: $this->name; + + return $this->prefix . Loader::parseName($name); + } + + /** + * 执行查询 返回数据集 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param boolean $master 是否在主服务器读操作 + * @param bool $pdo 是否返回PDO对象 + * @return mixed + * @throws BindParamException + * @throws PDOException + */ + public function query($sql, $bind = [], $master = false, $pdo = false) + { + return $this->connection->query($sql, $bind, $master, $pdo); + } + + /** + * 执行语句 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @return int + * @throws BindParamException + * @throws PDOException + */ + public function execute($sql, $bind = []) + { + return $this->connection->execute($sql, $bind, $this); + } + + /** + * 监听SQL执行 + * @access public + * @param callable $callback 回调方法 + * @return void + */ + public function listen($callback) + { + $this->connection->listen($callback); + } + + /** + * 获取最近插入的ID + * @access public + * @param string $sequence 自增序列名 + * @return string + */ + public function getLastInsID($sequence = null) + { + return $this->connection->getLastInsID($sequence); + } + + /** + * 获取返回或者影响的记录数 + * @access public + * @return integer + */ + public function getNumRows() + { + return $this->connection->getNumRows(); + } + + /** + * 获取最近一次查询的sql语句 + * @access public + * @return string + */ + public function getLastSql() + { + return $this->connection->getLastSql(); + } + + /** + * 执行数据库Xa事务 + * @access public + * @param callable $callback 数据操作方法回调 + * @param array $dbs 多个查询对象或者连接对象 + * @return mixed + * @throws PDOException + * @throws \Exception + * @throws \Throwable + */ + public function transactionXa($callback, array $dbs = []) + { + $xid = uniqid('xa'); + + if (empty($dbs)) { + $dbs[] = $this->getConnection(); + } + + foreach ($dbs as $key => $db) { + if ($db instanceof Query) { + $db = $db->getConnection(); + + $dbs[$key] = $db; + } + + $db->startTransXa($xid); + } + + try { + $result = null; + if (is_callable($callback)) { + $result = call_user_func_array($callback, [$this]); + } + + foreach ($dbs as $db) { + $db->prepareXa($xid); + } + + foreach ($dbs as $db) { + $db->commitXa($xid); + } + + return $result; + } catch (\Exception $e) { + foreach ($dbs as $db) { + $db->rollbackXa($xid); + } + throw $e; + } catch (\Throwable $e) { + foreach ($dbs as $db) { + $db->rollbackXa($xid); + } + throw $e; + } + } + + /** + * 执行数据库事务 + * @access public + * @param callable $callback 数据操作方法回调 + * @return mixed + */ + public function transaction($callback) + { + return $this->connection->transaction($callback); + } + + /** + * 启动事务 + * @access public + * @return void + */ + public function startTrans() + { + $this->connection->startTrans(); + } + + /** + * 用于非自动提交状态下面的查询提交 + * @access public + * @return void + * @throws PDOException + */ + public function commit() + { + $this->connection->commit(); + } + + /** + * 事务回滚 + * @access public + * @return void + * @throws PDOException + */ + public function rollback() + { + $this->connection->rollback(); + } + + /** + * 批处理执行SQL语句 + * 批处理的指令都认为是execute操作 + * @access public + * @param array $sql SQL批处理指令 + * @return boolean + */ + public function batchQuery($sql = []) + { + return $this->connection->batchQuery($sql); + } + + /** + * 获取数据库的配置参数 + * @access public + * @param string $name 参数名称 + * @return mixed + */ + public function getConfig($name = '') + { + return $this->connection->getConfig($name); + } + + /** + * 获取数据表字段信息 + * @access public + * @param string $tableName 数据表名 + * @return array + */ + public function getTableFields($tableName = '') + { + if ('' == $tableName) { + $tableName = isset($this->options['table']) ? $this->options['table'] : $this->getTable(); + } + + return $this->connection->getTableFields($tableName); + } + + /** + * 获取数据表字段类型 + * @access public + * @param string $tableName 数据表名 + * @param string $field 字段名 + * @return array|string + */ + public function getFieldsType($tableName = '', $field = null) + { + if ('' == $tableName) { + $tableName = isset($this->options['table']) ? $this->options['table'] : $this->getTable(); + } + + return $this->connection->getFieldsType($tableName, $field); + } + + /** + * 得到分表的的数据表名 + * @access public + * @param array $data 操作的数据 + * @param string $field 分表依据的字段 + * @param array $rule 分表规则 + * @return array + */ + public function getPartitionTableName($data, $field, $rule = []) + { + // 对数据表进行分区 + if ($field && isset($data[$field])) { + $value = $data[$field]; + $type = $rule['type']; + switch ($type) { + case 'id': + // 按照id范围分表 + $step = $rule['expr']; + $seq = floor($value / $step) + 1; + break; + case 'year': + // 按照年份分表 + if (!is_numeric($value)) { + $value = strtotime($value); + } + $seq = date('Y', $value) - $rule['expr'] + 1; + break; + case 'mod': + // 按照id的模数分表 + $seq = ($value % $rule['num']) + 1; + break; + case 'md5': + // 按照md5的序列分表 + $seq = (ord(substr(md5($value), 0, 1)) % $rule['num']) + 1; + break; + default: + if (function_exists($type)) { + // 支持指定函数哈希 + $value = $type($value); + } + + $seq = (ord(substr($value, 0, 1)) % $rule['num']) + 1; + } + + return $this->getTable() . '_' . $seq; + } + // 当设置的分表字段不在查询条件或者数据中 + // 进行联合查询,必须设定 partition['num'] + $tableName = []; + for ($i = 0; $i < $rule['num']; $i++) { + $tableName[] = 'SELECT * FROM ' . $this->getTable() . '_' . ($i + 1); + } + + return ['( ' . implode(" UNION ", $tableName) . ' )' => $this->name]; + } + + /** + * 得到某个字段的值 + * @access public + * @param string $field 字段名 + * @param mixed $default 默认值 + * @return mixed + */ + public function value($field, $default = null) + { + $this->parseOptions(); + + return $this->connection->value($this, $field, $default); + } + + /** + * 得到某个列的数组 + * @access public + * @param string $field 字段名 多个字段用逗号分隔 + * @param string $key 索引 + * @return array + */ + public function column($field, $key = '') + { + $this->parseOptions(); + + return $this->connection->column($this, $field, $key); + } + + /** + * 聚合查询 + * @access public + * @param string $aggregate 聚合方法 + * @param string|Expression $field 字段名 + * @param bool $force 强制转为数字类型 + * @return mixed + */ + public function aggregate($aggregate, $field, $force = false) + { + $this->parseOptions(); + + $result = $this->connection->aggregate($this, $aggregate, $field); + + if (!empty($this->options['fetch_sql'])) { + return $result; + } elseif ($force) { + $result = (float) $result; + } + + return $result; + } + + /** + * COUNT查询 + * @access public + * @param string|Expression $field 字段名 + * @return float|string + */ + public function count($field = '*') + { + if (!empty($this->options['group'])) { + // 支持GROUP + $options = $this->getOptions(); + $subSql = $this->options($options) + ->field('count(' . $field . ') AS think_count') + ->bind($this->bind) + ->buildSql(); + + $query = $this->newQuery()->table([$subSql => '_group_count_']); + + if (!empty($options['fetch_sql'])) { + $query->fetchSql(true); + } + + $count = $query->aggregate('COUNT', '*', true); + } else { + $count = $this->aggregate('COUNT', $field, true); + } + + return is_string($count) ? $count : (int) $count; + } + + /** + * SUM查询 + * @access public + * @param string|Expression $field 字段名 + * @return float + */ + public function sum($field) + { + return $this->aggregate('SUM', $field, true); + } + + /** + * MIN查询 + * @access public + * @param string|Expression $field 字段名 + * @param bool $force 强制转为数字类型 + * @return mixed + */ + public function min($field, $force = true) + { + return $this->aggregate('MIN', $field, $force); + } + + /** + * MAX查询 + * @access public + * @param string|Expression $field 字段名 + * @param bool $force 强制转为数字类型 + * @return mixed + */ + public function max($field, $force = true) + { + return $this->aggregate('MAX', $field, $force); + } + + /** + * AVG查询 + * @access public + * @param string|Expression $field 字段名 + * @return float + */ + public function avg($field) + { + return $this->aggregate('AVG', $field, true); + } + + /** + * 设置记录的某个字段值 + * 支持使用数据库字段和方法 + * @access public + * @param string|array $field 字段名 + * @param mixed $value 字段值 + * @return integer + */ + public function setField($field, $value = '') + { + if (is_array($field)) { + $data = $field; + } else { + $data[$field] = $value; + } + + return $this->update($data); + } + + /** + * 字段值(延迟)增长 + * @access public + * @param string $field 字段名 + * @param integer $step 增长值 + * @param integer $lazyTime 延时时间(s) + * @return integer|true + * @throws Exception + */ + public function setInc($field, $step = 1, $lazyTime = 0) + { + $condition = !empty($this->options['where']) ? $this->options['where'] : []; + + if (empty($condition)) { + // 没有条件不做任何更新 + throw new Exception('no data to update'); + } + + if ($lazyTime > 0) { + // 延迟写入 + $guid = md5($this->getTable() . '_' . $field . '_' . serialize($condition)); + $step = $this->lazyWrite('inc', $guid, $step, $lazyTime); + + if (false === $step) { + // 清空查询条件 + $this->options = []; + return true; + } + } + + return $this->setField($field, ['INC', $step]); + } + + /** + * 字段值(延迟)减少 + * @access public + * @param string $field 字段名 + * @param integer $step 减少值 + * @param integer $lazyTime 延时时间(s) + * @return integer|true + * @throws Exception + */ + public function setDec($field, $step = 1, $lazyTime = 0) + { + $condition = !empty($this->options['where']) ? $this->options['where'] : []; + + if (empty($condition)) { + // 没有条件不做任何更新 + throw new Exception('no data to update'); + } + + if ($lazyTime > 0) { + // 延迟写入 + $guid = md5($this->getTable() . '_' . $field . '_' . serialize($condition)); + $step = $this->lazyWrite('dec', $guid, $step, $lazyTime); + + if (false === $step) { + // 清空查询条件 + $this->options = []; + return true; + } + + $value = ['INC', $step]; + } else { + $value = ['DEC', $step]; + } + + return $this->setField($field, $value); + } + + /** + * 延时更新检查 返回false表示需要延时 + * 否则返回实际写入的数值 + * @access protected + * @param string $type 自增或者自减 + * @param string $guid 写入标识 + * @param integer $step 写入步进值 + * @param integer $lazyTime 延时时间(s) + * @return false|integer + */ + protected function lazyWrite($type, $guid, $step, $lazyTime) + { + $cache = Container::get('cache'); + + if (!$cache->has($guid . '_time')) { + // 计时开始 + $cache->set($guid . '_time', time(), 0); + $cache->$type($guid, $step); + } elseif (time() > $cache->get($guid . '_time') + $lazyTime) { + // 删除缓存 + $value = $cache->$type($guid, $step); + $cache->rm($guid); + $cache->rm($guid . '_time'); + return 0 === $value ? false : $value; + } else { + // 更新缓存 + $cache->$type($guid, $step); + } + + return false; + } + + /** + * 查询SQL组装 join + * @access public + * @param mixed $join 关联的表名 + * @param mixed $condition 条件 + * @param string $type JOIN类型 + * @param array $bind 参数绑定 + * @return $this + */ + public function join($join, $condition = null, $type = 'INNER', $bind = []) + { + if (empty($condition)) { + // 如果为组数,则循环调用join + foreach ($join as $key => $value) { + if (is_array($value) && 2 <= count($value)) { + $this->join($value[0], $value[1], isset($value[2]) ? $value[2] : $type); + } + } + } else { + $table = $this->getJoinTable($join); + if ($bind) { + $this->bindParams($condition, $bind); + } + $this->options['join'][] = [$table, strtoupper($type), $condition]; + } + + return $this; + } + + /** + * LEFT JOIN + * @access public + * @param mixed $join 关联的表名 + * @param mixed $condition 条件 + * @param array $bind 参数绑定 + * @return $this + */ + public function leftJoin($join, $condition = null, $bind = []) + { + return $this->join($join, $condition, 'LEFT'); + } + + /** + * RIGHT JOIN + * @access public + * @param mixed $join 关联的表名 + * @param mixed $condition 条件 + * @param array $bind 参数绑定 + * @return $this + */ + public function rightJoin($join, $condition = null, $bind = []) + { + return $this->join($join, $condition, 'RIGHT'); + } + + /** + * FULL JOIN + * @access public + * @param mixed $join 关联的表名 + * @param mixed $condition 条件 + * @param array $bind 参数绑定 + * @return $this + */ + public function fullJoin($join, $condition = null, $bind = []) + { + return $this->join($join, $condition, 'FULL'); + } + + /** + * 获取Join表名及别名 支持 + * ['prefix_table或者子查询'=>'alias'] 'table alias' + * @access protected + * @param array|string $join + * @param string $alias + * @return string + */ + protected function getJoinTable($join, &$alias = null) + { + if (is_array($join)) { + $table = $join; + $alias = array_shift($join); + } else { + $join = trim($join); + + if (false !== strpos($join, '(')) { + // 使用子查询 + $table = $join; + } else { + $prefix = $this->prefix; + if (strpos($join, ' ')) { + // 使用别名 + list($table, $alias) = explode(' ', $join); + } else { + $table = $join; + if (false === strpos($join, '.') && 0 !== strpos($join, '__')) { + $alias = $join; + } + } + + if ($prefix && false === strpos($table, '.') && 0 !== strpos($table, $prefix) && 0 !== strpos($table, '__')) { + $table = $this->getTable($table); + } + } + + if (isset($alias) && $table != $alias) { + $table = [$table => $alias]; + } + } + + return $table; + } + + /** + * 查询SQL组装 union + * @access public + * @param mixed $union + * @param boolean $all + * @return $this + */ + public function union($union, $all = false) + { + if (empty($union)) { + return $this; + } + + $this->options['union']['type'] = $all ? 'UNION ALL' : 'UNION'; + + if (is_array($union)) { + $this->options['union'] = array_merge($this->options['union'], $union); + } else { + $this->options['union'][] = $union; + } + + return $this; + } + + /** + * 查询SQL组装 union all + * @access public + * @param mixed $union + * @return $this + */ + public function unionAll($union) + { + return $this->union($union, true); + } + + /** + * 指定查询字段 支持字段排除和指定数据表 + * @access public + * @param mixed $field + * @param boolean $except 是否排除 + * @param string $tableName 数据表名 + * @param string $prefix 字段前缀 + * @param string $alias 别名前缀 + * @return $this + */ + public function field($field, $except = false, $tableName = '', $prefix = '', $alias = '') + { + 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) { + // 获取全部字段 + $fields = $this->getTableFields($tableName); + $field = $fields ?: ['*']; + } elseif ($except) { + // 字段排除 + $fields = $this->getTableFields($tableName); + $field = $fields ? array_diff($fields, $field) : $field; + } + + if ($tableName) { + // 添加统一的前缀 + $prefix = $prefix ?: $tableName; + foreach ($field as $key => &$val) { + if (is_numeric($key) && $alias) { + $field[$prefix . '.' . $val] = $alias . $val; + unset($field[$key]); + } elseif (is_numeric($key)) { + $val = $prefix . '.' . $val; + } + } + } + + if (isset($this->options['field'])) { + $field = array_merge((array) $this->options['field'], $field); + } + + $this->options['field'] = array_unique($field); + + return $this; + } + + /** + * 表达式方式指定查询字段 + * @access public + * @param string $field 字段名 + * @return $this + */ + public function fieldRaw($field) + { + $this->options['field'][] = $this->raw($field); + + return $this; + } + + /** + * 设置数据 + * @access public + * @param mixed $field 字段名或者数据 + * @param mixed $value 字段值 + * @return $this + */ + public function data($field, $value = null) + { + if (is_array($field)) { + $this->options['data'] = isset($this->options['data']) ? array_merge($this->options['data'], $field) : $field; + } else { + $this->options['data'][$field] = $value; + } + + return $this; + } + + /** + * 字段值增长 + * @access public + * @param string|array $field 字段名 + * @param integer $step 增长值 + * @return $this + */ + public function inc($field, $step = 1, $op = 'INC') + { + $fields = is_string($field) ? explode(',', $field) : $field; + + foreach ($fields as $field => $val) { + if (is_numeric($field)) { + $field = $val; + } else { + $step = $val; + } + + $this->data($field, [$op, $step]); + } + + return $this; + } + + /** + * 字段值减少 + * @access public + * @param string|array $field 字段名 + * @param integer $step 增长值 + * @return $this + */ + public function dec($field, $step = 1) + { + return $this->inc($field, $step, 'DEC'); + } + + /** + * 使用表达式设置数据 + * @access public + * @param string $field 字段名 + * @param string $value 字段值 + * @return $this + */ + public function exp($field, $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 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) { + foreach ($join as $key => $val) { + $this->view($val[0], $val[1], isset($val[2]) ? $val[2] : null, isset($val[3]) ? $val[3] : 'INNER'); + } + } else { + $fields = []; + $table = $this->getJoinTable($join, $alias); + + if (true === $field) { + $fields = $alias . '.*'; + } else { + if (is_string($field)) { + $field = explode(',', $field); + } + + foreach ($field as $key => $val) { + if (is_numeric($key)) { + $fields[] = $alias . '.' . $val; + + $this->options['map'][$val] = $alias . '.' . $val; + } else { + if (preg_match('/[,=\.\'\"\(\s]/', $key)) { + $name = $key; + } else { + $name = $alias . '.' . $key; + } + + $fields[] = $name . ' AS ' . $val; + + $this->options['map'][$val] = $name; + } + } + } + + $this->field($fields); + + if ($on) { + $this->join($table, $on, $type); + } else { + $this->table($table); + } + } + + return $this; + } + + /** + * 设置分表规则 + * @access public + * @param array $data 操作的数据 + * @param string $field 分表依据的字段 + * @param array $rule 分表规则 + * @return $this + */ + public function partition($data, $field, $rule = []) + { + $this->options['table'] = $this->getPartitionTableName($data, $field, $rule); + + return $this; + } + + /** + * 指定AND查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @return $this + */ + public function where($field, $op = null, $condition = null) + { + $param = func_get_args(); + array_shift($param); + return $this->parseWhereExp('AND', $field, $op, $condition, $param); + } + + /** + * 指定OR查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @return $this + */ + public function whereOr($field, $op = null, $condition = null) + { + $param = func_get_args(); + array_shift($param); + return $this->parseWhereExp('OR', $field, $op, $condition, $param); + } + + /** + * 指定XOR查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @return $this + */ + public function whereXor($field, $op = null, $condition = null) + { + $param = func_get_args(); + array_shift($param); + return $this->parseWhereExp('XOR', $field, $op, $condition, $param); + } + + /** + * 指定Null查询条件 + * @access public + * @param mixed $field 查询字段 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNull($field, $logic = 'AND') + { + return $this->parseWhereExp($logic, $field, 'NULL', null, [], true); + } + + /** + * 指定NotNull查询条件 + * @access public + * @param mixed $field 查询字段 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotNull($field, $logic = 'AND') + { + return $this->parseWhereExp($logic, $field, 'NOTNULL', null, [], true); + } + + /** + * 指定Exists查询条件 + * @access public + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereExists($condition, $logic = 'AND') + { + if (is_string($condition)) { + $condition = $this->raw($condition); + } + + $this->options['where'][strtoupper($logic)][] = ['', 'EXISTS', $condition]; + return $this; + } + + /** + * 指定NotExists查询条件 + * @access public + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotExists($condition, $logic = 'AND') + { + if (is_string($condition)) { + $condition = $this->raw($condition); + } + + $this->options['where'][strtoupper($logic)][] = ['', 'NOT EXISTS', $condition]; + return $this; + } + + /** + * 指定In查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereIn($field, $condition, $logic = 'AND') + { + return $this->parseWhereExp($logic, $field, 'IN', $condition, [], true); + } + + /** + * 指定NotIn查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotIn($field, $condition, $logic = 'AND') + { + return $this->parseWhereExp($logic, $field, 'NOT IN', $condition, [], true); + } + + /** + * 指定Like查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereLike($field, $condition, $logic = 'AND') + { + return $this->parseWhereExp($logic, $field, 'LIKE', $condition, [], true); + } + + /** + * 指定NotLike查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotLike($field, $condition, $logic = 'AND') + { + return $this->parseWhereExp($logic, $field, 'NOT LIKE', $condition, [], true); + } + + /** + * 指定Between查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereBetween($field, $condition, $logic = 'AND') + { + return $this->parseWhereExp($logic, $field, 'BETWEEN', $condition, [], true); + } + + /** + * 指定NotBetween查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotBetween($field, $condition, $logic = 'AND') + { + return $this->parseWhereExp($logic, $field, 'NOT BETWEEN', $condition, [], true); + } + + /** + * 比较两个字段 + * @access public + * @param string|array $field1 查询字段 + * @param string $operator 比较操作符 + * @param string $field2 比较字段 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereColumn($field1, $operator = null, $field2 = null, $logic = 'AND') + { + if (is_array($field1)) { + foreach ($field1 as $item) { + $this->whereColumn($item[0], $item[1], isset($item[2]) ? $item[2] : null); + } + return $this; + } + + if (is_null($field2)) { + $field2 = $operator; + $operator = '='; + } + + return $this->parseWhereExp($logic, $field1, 'COLUMN', [$operator, $field2], [], true); + } + + /** + * 设置软删除字段及条件 + * @access public + * @param false|string $field 查询字段 + * @param mixed $condition 查询条件 + * @return $this + */ + public function useSoftDelete($field, $condition = null) + { + if ($field) { + $this->options['soft_delete'] = [$field, $condition]; + } + + return $this; + } + + /** + * 指定Exp查询条件 + * @access public + * @param mixed $field 查询字段 + * @param string $where 查询条件 + * @param array $bind 参数绑定 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereExp($field, $where, $bind = [], $logic = 'AND') + { + if ($bind) { + $this->bindParams($where, $bind); + } + + $this->options['where'][$logic][] = [$field, 'EXP', $this->raw($where)]; + + 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') + { + if ($bind) { + $this->bindParams($where, $bind); + } + + $this->options['where'][$logic][] = $this->raw($where); + + return $this; + } + + /** + * 参数绑定 + * @access public + * @param string $sql 绑定的sql表达式 + * @param array $bind 参数绑定 + * @return void + */ + protected function bindParams(&$sql, array $bind = []) + { + foreach ($bind as $key => $value) { + if (is_array($value)) { + $name = $this->bind($value[0], $value[1], isset($value[2]) ? $value[2] : null); + } else { + $name = $this->bind($value); + } + + if (is_numeric($key)) { + $sql = substr_replace($sql, ':' . $name, strpos($sql, '?'), 1); + } else { + $sql = str_replace(':' . $key, ':' . $name, $sql); + } + } + } + + /** + * 指定表达式查询条件 OR + * @access public + * @param string $where 查询条件 + * @param array $bind 参数绑定 + * @return $this + */ + public function whereOrRaw($where, $bind = []) + { + return $this->whereRaw($where, $bind, 'OR'); + } + + /** + * 分析查询表达式 + * @access protected + * @param string $logic 查询逻辑 and or xor + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @param array $param 查询参数 + * @param bool $strict 严格模式 + * @return $this + */ + protected function parseWhereExp($logic, $field, $op, $condition, array $param = [], $strict = false) + { + if ($field instanceof $this) { + $this->options['where'] = $field->getOptions('where'); + $this->bind($field->getBind(false)); + return $this; + } + + $logic = strtoupper($logic); + + if ($field instanceof Where) { + $this->options['where'][$logic] = $field->parse(); + return $this; + } + + if (is_string($field) && !empty($this->options['via']) && false === strpos($field, '.')) { + $field = $this->options['via'] . '.' . $field; + } + + if ($field instanceof Expression) { + return $this->whereRaw($field, is_array($op) ? $op : [], $logic); + } elseif ($strict) { + // 使用严格模式查询 + $where = [$field, $op, $condition, $logic]; + } elseif (is_array($field)) { + // 解析数组批量查询 + return $this->parseArrayWhereItems($field, $logic); + } elseif ($field instanceof \Closure) { + $where = $field; + } elseif (is_string($field)) { + if (preg_match('/[,=\<\'\"\(\s]/', $field)) { + return $this->whereRaw($field, $op, $logic); + } elseif (is_string($op) && strtolower($op) == 'exp') { + $bind = isset($param[2]) && is_array($param[2]) ? $param[2] : null; + return $this->whereExp($field, $condition, $bind, $logic); + } + + $where = $this->parseWhereItem($logic, $field, $op, $condition, $param); + } + + if (!empty($where)) { + $this->options['where'][$logic][] = $where; + } + + return $this; + } + + /** + * 分析查询表达式 + * @access protected + * @param string $logic 查询逻辑 and or xor + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @param array $param 查询参数 + * @return mixed + */ + protected function parseWhereItem($logic, $field, $op, $condition, $param = []) + { + if (is_array($op)) { + // 同一字段多条件查询 + array_unshift($param, $field); + $where = $param; + } elseif ($field && is_null($condition)) { + if (in_array(strtoupper($op), ['NULL', 'NOTNULL', 'NOT NULL'], true)) { + // null查询 + $where = [$field, $op, '']; + } elseif (in_array($op, ['=', 'eq', 'EQ', null], true)) { + $where = [$field, 'NULL', '']; + } elseif (in_array($op, ['<>', 'neq', 'NEQ'], true)) { + $where = [$field, 'NOTNULL', '']; + } else { + // 字段相等查询 + $where = [$field, '=', $op]; + } + } elseif (in_array(strtoupper($op), ['EXISTS', 'NOT EXISTS', 'NOTEXISTS'], true)) { + $where = [$field, $op, is_string($condition) ? $this->raw($condition) : $condition]; + } else { + $where = $field ? [$field, $op, $condition, isset($param[2]) ? $param[2] : null] : null; + } + + return $where; + } + + /** + * 数组批量查询 + * @access protected + * @param array $field 批量查询 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + protected function parseArrayWhereItems($field, $logic) + { + if (key($field) !== 0) { + $where = []; + foreach ($field as $key => $val) { + if ($val instanceof Expression) { + $where[] = [$key, 'exp', $val]; + } elseif (is_null($val)) { + $where[] = [$key, 'NULL', '']; + } else { + $where[] = [$key, is_array($val) ? 'IN' : '=', $val]; + } + } + } else { + // 数组批量查询 + $where = $field; + } + + if (!empty($where)) { + $this->options['where'][$logic] = isset($this->options['where'][$logic]) ? array_merge($this->options['where'][$logic], $where) : $where; + } + + return $this; + } + + /** + * 去除某个查询条件 + * @access public + * @param string $field 查询字段 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function removeWhereField($field, $logic = 'AND') + { + $logic = strtoupper($logic); + + if (isset($this->options['where'][$logic])) { + foreach ($this->options['where'][$logic] as $key => $val) { + if (is_array($val) && $val[0] == $field) { + unset($this->options['where'][$logic][$key]); + } + } + } + + return $this; + } + + /** + * 去除查询参数 + * @access public + * @param string|bool $option 参数名 true 表示去除所有参数 + * @return $this + */ + public function removeOption($option = true) + { + if (true === $option) { + $this->options = []; + $this->bind = []; + } elseif (is_string($option) && isset($this->options[$option])) { + unset($this->options[$option]); + } + + return $this; + } + + /** + * 条件查询 + * @access public + * @param mixed $condition 满足条件(支持闭包) + * @param \Closure|array $query 满足条件后执行的查询表达式(闭包或数组) + * @param \Closure|array $otherwise 不满足条件后执行 + * @return $this + */ + public function when($condition, $query, $otherwise = null) + { + if ($condition instanceof \Closure) { + $condition = $condition($this); + } + + if ($condition) { + if ($query instanceof \Closure) { + $query($this, $condition); + } elseif (is_array($query)) { + $this->where($query); + } + } elseif ($otherwise) { + if ($otherwise instanceof \Closure) { + $otherwise($this, $condition); + } elseif (is_array($otherwise)) { + $this->where($otherwise); + } + } + + return $this; + } + + /** + * 指定查询数量 + * @access public + * @param mixed $offset 起始位置 + * @param mixed $length 查询数量 + * @return $this + */ + public function limit($offset, $length = null) + { + if (is_null($length) && strpos($offset, ',')) { + list($offset, $length) = explode(',', $offset); + } + + $this->options['limit'] = intval($offset) . ($length ? ',' . intval($length) : ''); + + return $this; + } + + /** + * 指定分页 + * @access public + * @param mixed $page 页数 + * @param mixed $listRows 每页数量 + * @return $this + */ + public function page($page, $listRows = null) + { + if (is_null($listRows) && strpos($page, ',')) { + list($page, $listRows) = explode(',', $page); + } + + $this->options['page'] = [intval($page), intval($listRows)]; + + return $this; + } + + /** + * 分页查询 + * @access public + * @param int|array $listRows 每页数量 数组表示配置参数 + * @param int|bool $simple 是否简洁模式或者总记录数 + * @param array $config 配置参数 + * page:当前页, + * path:url路径, + * query:url额外参数, + * fragment:url锚点, + * var_page:分页变量, + * list_rows:每页数量 + * type:分页类名 + * @return $this[]|\think\Paginator + * @throws DbException + */ + public function paginate($listRows = null, $simple = false, $config = []) + { + if (is_int($simple)) { + $total = $simple; + $simple = false; + } + + $paginate = Container::get('config')->pull('paginate'); + + if (is_array($listRows)) { + $config = array_merge($paginate, $listRows); + $listRows = $config['list_rows']; + } else { + $config = array_merge($paginate, $config); + $listRows = $listRows ?: $config['list_rows']; + } + + /** @var Paginator $class */ + $class = false !== strpos($config['type'], '\\') ? $config['type'] : '\\think\\paginator\\driver\\' . ucwords($config['type']); + $page = isset($config['page']) ? (int) $config['page'] : call_user_func([ + $class, + 'getCurrentPage', + ], $config['var_page']); + + $page = $page < 1 ? 1 : $page; + + $config['path'] = isset($config['path']) ? $config['path'] : call_user_func([$class, 'getCurrentPath']); + + if (!isset($total) && !$simple) { + $options = $this->getOptions(); + + unset($this->options['order'], $this->options['limit'], $this->options['page'], $this->options['field']); + + $bind = $this->bind; + $total = $this->count(); + $results = $this->options($options)->bind($bind)->page($page, $listRows)->select(); + } elseif ($simple) { + $results = $this->limit(($page - 1) * $listRows, $listRows + 1)->select(); + $total = null; + } else { + $results = $this->page($page, $listRows)->select(); + } + + $this->removeOption('limit'); + $this->removeOption('page'); + + return $class::make($results, $listRows, $page, $total, $simple, $config); + } + + /** + * 指定当前操作的数据表 + * @access public + * @param mixed $table 表名 + * @return $this + */ + public function table($table) + { + if (is_string($table)) { + if (strpos($table, ')')) { + // 子查询 + } elseif (strpos($table, ',')) { + $tables = explode(',', $table); + $table = []; + + foreach ($tables as $item) { + list($item, $alias) = explode(' ', trim($item)); + if ($alias) { + $this->alias([$item => $alias]); + $table[$item] = $alias; + } else { + $table[] = $item; + } + } + } elseif (strpos($table, ' ')) { + list($table, $alias) = explode(' ', $table); + + $table = [$table => $alias]; + $this->alias($table); + } + } else { + $tables = $table; + $table = []; + + foreach ($tables as $key => $val) { + if (is_numeric($key)) { + $table[] = $val; + } else { + $this->alias([$key => $val]); + $table[$key] = $val; + } + } + } + + $this->options['table'] = $table; + + return $this; + } + + /** + * USING支持 用于多表删除 + * @access public + * @param mixed $using + * @return $this + */ + public function using($using) + { + $this->options['using'] = $using; + return $this; + } + + /** + * 指定排序 order('id','desc') 或者 order(['id'=>'desc','create_time'=>'desc']) + * @access public + * @param string|array $field 排序字段 + * @param string $order 排序 + * @return $this + */ + public function order($field, $order = null) + { + 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 (strpos($field, ',')) { + $field = array_map('trim', explode(',', $field)); + } else { + $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, $bind = []) + { + if ($bind) { + $this->bindParams($field, $bind); + } + + $this->options['order'][] = $this->raw($field); + + return $this; + } + + /** + * 指定Field排序 order('id',[1,2,3],'desc') + * @access public + * @param string|array $field 排序字段 + * @param array $values 排序值 + * @param string $order + * @return $this + */ + public function orderField($field, array $values, $order = '') + { + if (!empty($values)) { + $values['sort'] = $order; + + $this->options['order'][$field] = $values; + } + + return $this; + } + + /** + * 随机排序 + * @access public + * @return $this + */ + public function orderRand() + { + $this->options['order'][] = '[rand]'; + return $this; + } + + /** + * 查询缓存 + * @access public + * @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 ($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]; + } + + return $this; + } + + /** + * 指定group查询 + * @access public + * @param string|array $group GROUP + * @return $this + */ + public function group($group) + { + $this->options['group'] = $group; + return $this; + } + + /** + * 指定having查询 + * @access public + * @param string $having having + * @return $this + */ + public function having($having) + { + $this->options['having'] = $having; + return $this; + } + + /** + * 指定查询lock + * @access public + * @param bool|string $lock 是否lock + * @return $this + */ + public function lock($lock = false) + { + $this->options['lock'] = $lock; + $this->options['master'] = true; + + return $this; + } + + /** + * 指定distinct查询 + * @access public + * @param string $distinct 是否唯一 + * @return $this + */ + public function distinct($distinct) + { + $this->options['distinct'] = $distinct; + return $this; + } + + /** + * 指定数据表别名 + * @access public + * @param array|string $alias 数据表别名 + * @return $this + */ + public function alias($alias) + { + if (is_array($alias)) { + foreach ($alias as $key => $val) { + if (false !== strpos($key, '__')) { + $table = $this->connection->parseSqlTable($key); + } else { + $table = $key; + } + $this->options['alias'][$table] = $val; + } + } else { + if (isset($this->options['table'])) { + $table = is_array($this->options['table']) ? key($this->options['table']) : $this->options['table']; + if (false !== strpos($table, '__')) { + $table = $this->connection->parseSqlTable($table); + } + } else { + $table = $this->getTable(); + } + + $this->options['alias'][$table] = $alias; + } + + return $this; + } + + /** + * 指定强制索引 + * @access public + * @param string $force 索引名称 + * @return $this + */ + public function force($force) + { + $this->options['force'] = $force; + return $this; + } + + /** + * 查询注释 + * @access public + * @param string $comment 注释 + * @return $this + */ + public function comment($comment) + { + $this->options['comment'] = $comment; + return $this; + } + + /** + * 获取执行的SQL语句 + * @access public + * @param boolean $fetch 是否返回sql + * @return $this + */ + public function fetchSql($fetch = true) + { + $this->options['fetch_sql'] = $fetch; + return $this; + } + + /** + * 不主动获取数据集 + * @access public + * @param bool $pdo 是否返回 PDOStatement 对象 + * @return $this + */ + public function fetchPdo($pdo = true) + { + $this->options['fetch_pdo'] = $pdo; + return $this; + } + + /** + * 设置是否返回数据集对象(支持设置数据集对象类名) + * @access public + * @param bool|string $collection 是否返回数据集对象 + * @return $this + */ + public function fetchCollection($collection = true) + { + $this->options['collection'] = $collection; + + return $this; + } + + /** + * 设置从主服务器读取数据 + * @access public + * @return $this + */ + public function master() + { + $this->options['master'] = true; + return $this; + } + + /** + * 设置是否严格检查字段名 + * @access public + * @param bool $strict 是否严格检查字段 + * @return $this + */ + public function strict($strict = true) + { + $this->options['strict'] = $strict; + return $this; + } + + /** + * 设置查询数据不存在是否抛出异常 + * @access public + * @param bool $fail 数据不存在是否抛出异常 + * @return $this + */ + public function failException($fail = true) + { + $this->options['fail'] = $fail; + return $this; + } + + /** + * 设置自增序列名 + * @access public + * @param string $sequence 自增序列名 + * @return $this + */ + public function sequence($sequence = null) + { + $this->options['sequence'] = $sequence; + return $this; + } + + /** + * 设置需要隐藏的输出属性 + * @access public + * @param mixed $hidden 需要隐藏的字段名 + * @return $this + */ + public function hidden($hidden) + { + if ($this->model) { + $this->options['hidden'] = $hidden; + return $this; + } + + return $this->field($hidden, true); + } + + /** + * 设置需要输出的属性 + * @access public + * @param array $visible 需要输出的属性 + * @return $this + */ + public function visible(array $visible) + { + $this->options['visible'] = $visible; + return $this; + } + + /** + * 设置需要附加的输出属性 + * @access public + * @param array $append 属性列表 + * @return $this + */ + public function append(array $append = []) + { + $this->options['append'] = $append; + return $this; + } + + /** + * 设置数据字段获取器 + * @access public + * @param string|array $name 字段名 + * @param callable $callback 闭包获取器 + * @return $this + */ + public function withAttr($name, $callback = null) + { + if (is_array($name)) { + $this->options['with_attr'] = $name; + } else { + $this->options['with_attr'][$name] = $callback; + } + + return $this; + } + + /** + * 设置JSON字段信息 + * @access public + * @param array $json JSON字段 + * @param bool $assoc 是否取出数组 + * @return $this + */ + public function json(array $json = [], $assoc = false) + { + $this->options['json'] = $json; + $this->options['json_assoc'] = $assoc; + return $this; + } + + /** + * 设置字段类型信息 + * @access public + * @param array $type 字段类型信息 + * @return $this + */ + public function setJsonFieldType(array $type) + { + $this->options['field_type'] = $type; + return $this; + } + + /** + * 获取字段类型信息 + * @access public + * @param string $field 字段名 + * @return string|null + */ + public function getJsonFieldType($field) + { + return isset($this->options['field_type'][$field]) ? $this->options['field_type'][$field] : null; + } + + /** + * 是否允许返回空数据(或空模型) + * @access public + * @param bool $allowEmpty 是否允许为空 + * @return $this + */ + public function allowEmpty($allowEmpty = true) + { + $this->options['allow_empty'] = $allowEmpty; + return $this; + } + + /** + * 添加查询范围 + * @access public + * @param array|string|\Closure $scope 查询范围定义 + * @param array $args 参数 + * @return $this + */ + public function scope($scope, ...$args) + { + // 查询范围的第一个参数始终是当前查询对象 + array_unshift($args, $this); + + if ($scope instanceof \Closure) { + call_user_func_array($scope, $args); + return $this; + } + + if (is_string($scope)) { + $scope = explode(',', $scope); + } + + if ($this->model) { + // 检查模型类的查询范围方法 + foreach ($scope as $name) { + $method = 'scope' . trim($name); + + if (method_exists($this->model, $method)) { + call_user_func_array([$this->model, $method], $args); + } + } + } + + return $this; + } + + /** + * 使用搜索器条件搜索字段 + * @access public + * @param array $fields 搜索字段 + * @param array $data 搜索数据 + * @param string $prefix 字段前缀标识 + * @return $this + */ + public function withSearch(array $fields, array $data = [], $prefix = '') + { + foreach ($fields as $key => $field) { + if ($field instanceof \Closure) { + $field($this, isset($data[$key]) ? $data[$key] : null, $data, $prefix); + } elseif ($this->model) { + // 检测搜索器 + $fieldName = is_numeric($key) ? $field : $key; + $method = 'search' . Loader::parseName($fieldName, 1) . 'Attr'; + + if (method_exists($this->model, $method)) { + $this->model->$method($this, isset($data[$field]) ? $data[$field] : null, $data, $prefix); + } + } + } + + return $this; + } + + /** + * 指定数据表主键 + * @access public + * @param string $pk 主键 + * @return $this + */ + public function pk($pk) + { + $this->pk = $pk; + return $this; + } + + /** + * 查询日期或者时间 + * @access public + * @param string $name 时间表达式 + * @param string|array $rule 时间范围 + * @return $this + */ + public function timeRule($name, $rule) + { + $this->timeRule[$name] = $rule; + return $this; + } + + /** + * 查询日期或者时间 + * @access public + * @param string $field 日期字段名 + * @param string|array $op 比较运算符或者表达式 + * @param string|array $range 比较范围 + * @param string $logic AND OR + * @return $this + */ + public function whereTime($field, $op, $range = null, $logic = 'AND') + { + if (is_null($range)) { + if (is_array($op)) { + $range = $op; + } else { + if (isset($this->timeExp[strtolower($op)])) { + $op = $this->timeExp[strtolower($op)]; + } + + if (isset($this->timeRule[strtolower($op)])) { + $range = $this->timeRule[strtolower($op)]; + } else { + $range = $op; + } + } + + $op = is_array($range) ? 'between' : '>='; + } + + return $this->parseWhereExp($logic, $field, strtolower($op) . ' time', $range, [], true); + } + + /** + * 查询当前时间在两个时间字段范围 + * @access public + * @param string $startField 开始时间字段 + * @param string $endField 结束时间字段 + * @return $this + */ + public function whereBetweenTimeField($startField, $endField) + { + return $this->whereTime($startField, '<=', time()) + ->whereTime($endField, '>=', time()); + } + + /** + * 查询当前时间不在两个时间字段范围 + * @access public + * @param string $startField 开始时间字段 + * @param string $endField 结束时间字段 + * @return $this + */ + public function whereNotBetweenTimeField($startField, $endField) + { + return $this->whereTime($startField, '>', time()) + ->whereTime($endField, '<', time(), 'OR'); + } + + /** + * 查询日期或者时间范围 + * @access public + * @param string $field 日期字段名 + * @param string $startTime 开始时间 + * @param string $endTime 结束时间 + * @param string $logic AND OR + * @return $this + */ + public function whereBetweenTime($field, $startTime, $endTime = null, $logic = 'AND') + { + if (is_null($endTime)) { + $time = is_string($startTime) ? strtotime($startTime) : $startTime; + $endTime = strtotime('+1 day', $time); + } + + return $this->parseWhereExp($logic, $field, 'between time', [$startTime, $endTime], [], true); + } + + /** + * 获取当前数据表的主键 + * @access public + * @param string|array $options 数据表名或者查询参数 + * @return string|array + */ + public function getPk($options = '') + { + if (!empty($this->pk)) { + $pk = $this->pk; + } else { + $pk = $this->connection->getPk(is_array($options) && isset($options['table']) ? $options['table'] : $this->getTable()); + } + + return $pk; + } + + /** + * 参数绑定 + * @access public + * @param mixed $value 绑定变量值 + * @param integer $type 绑定类型 + * @param string $name 绑定名称 + * @return $this|string + */ + public function bind($value, $type = PDO::PARAM_STR, $name = null) + { + if (is_array($value)) { + $this->bind = array_merge($this->bind, $value); + } else { + $name = $name ?: 'ThinkBind_' . (count($this->bind) + 1) . '_' . mt_rand() . '_'; + + $this->bind[$name] = [$value, $type]; + return $name; + } + + return $this; + } + + /** + * 检测参数是否已经绑定 + * @access public + * @param string $key 参数名 + * @return bool + */ + public function isBind($key) + { + return isset($this->bind[$key]); + } + + /** + * 查询参数赋值 + * @access public + * @param string $name 参数名 + * @param mixed $value 值 + * @return $this + */ + public function option($name, $value) + { + $this->options[$name] = $value; + return $this; + } + + /** + * 查询参数赋值 + * @access protected + * @param array $options 表达式参数 + * @return $this + */ + protected function options(array $options) + { + $this->options = $options; + return $this; + } + + /** + * 获取当前的查询参数 + * @access public + * @param string $name 参数名 + * @return mixed + */ + public function getOptions($name = '') + { + if ('' === $name) { + return $this->options; + } + return isset($this->options[$name]) ? $this->options[$name] : null; + } + + /** + * 设置当前的查询参数 + * @access public + * @param string $option 参数名 + * @param mixed $value 参数值 + * @return $this + */ + public function setOption($option, $value) + { + $this->options[$option] = $value; + return $this; + } + + /** + * 设置关联查询JOIN预查询 + * @access public + * @param string|array $with 关联方法名称 + * @return $this + */ + public function with($with) + { + if (empty($with)) { + return $this; + } + + if (is_string($with)) { + $with = explode(',', $with); + } + + $first = true; + + /** @var Model $class */ + $class = $this->model; + foreach ($with as $key => $relation) { + $closure = null; + + if ($relation instanceof \Closure) { + // 支持闭包查询过滤关联条件 + $closure = $relation; + $relation = $key; + } elseif (is_array($relation)) { + $relation = $key; + } elseif (is_string($relation) && strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation, 2); + } + + /** @var Relation $model */ + $relation = Loader::parseName($relation, 1, false); + $model = $class->$relation(); + + if ($model instanceof OneToOne && 0 == $model->getEagerlyType()) { + $table = $model->getTable(); + $model->removeOption() + ->table($table) + ->eagerly($this, $relation, true, '', $closure, $first); + $first = false; + } + } + + $this->via(); + + $this->options['with'] = $with; + + return $this; + } + + /** + * 关联预载入 JOIN方式(不支持嵌套) + * @access protected + * @param string|array $with 关联方法名 + * @param string $joinType JOIN方式 + * @return $this + */ + public function withJoin($with, $joinType = '') + { + if (empty($with)) { + return $this; + } + + if (is_string($with)) { + $with = explode(',', $with); + } + + $first = true; + + /** @var Model $class */ + $class = $this->model; + foreach ($with as $key => $relation) { + $closure = null; + $field = true; + + if ($relation instanceof \Closure) { + // 支持闭包查询过滤关联条件 + $closure = $relation; + $relation = $key; + } elseif (is_array($relation)) { + $field = $relation; + $relation = $key; + } elseif (is_string($relation) && strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation, 2); + } + + /** @var Relation $model */ + $relation = Loader::parseName($relation, 1, false); + $model = $class->$relation(); + + if ($model instanceof OneToOne) { + $model->eagerly($this, $relation, $field, $joinType, $closure, $first); + $first = false; + } else { + // 不支持其它关联 + unset($with[$key]); + } + } + + $this->via(); + + $this->options['with_join'] = $with; + + return $this; + } + + /** + * 关联统计 + * @access protected + * @param string|array $relation 关联方法名 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + protected function withAggregate($relation, $aggregate = 'count', $field = '*', $subQuery = true) + { + $relations = is_string($relation) ? explode(',', $relation) : $relation; + + if (!$subQuery) { + $this->options['with_count'][] = [$relations, $aggregate, $field]; + } else { + if (!isset($this->options['field'])) { + $this->field('*'); + } + + foreach ($relations as $key => $relation) { + $closure = $aggregateField = null; + + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } elseif (!is_int($key)) { + $aggregateField = $relation; + $relation = $key; + } + + $relation = Loader::parseName($relation, 1, false); + + $count = $this->model->$relation()->getRelationCountQuery($closure, $aggregate, $field, $aggregateField); + + if (empty($aggregateField)) { + $aggregateField = Loader::parseName($relation) . '_' . $aggregate; + } + + $this->field(['(' . $count . ')' => $aggregateField]); + } + } + + return $this; + } + + /** + * 关联统计 + * @access public + * @param string|array $relation 关联方法名 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + public function withCount($relation, $subQuery = true) + { + return $this->withAggregate($relation, 'count', '*', $subQuery); + } + + /** + * 关联统计Sum + * @access public + * @param string|array $relation 关联方法名 + * @param string $field 字段 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + public function withSum($relation, $field, $subQuery = true) + { + return $this->withAggregate($relation, 'sum', $field, $subQuery); + } + + /** + * 关联统计Max + * @access public + * @param string|array $relation 关联方法名 + * @param string $field 字段 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + public function withMax($relation, $field, $subQuery = true) + { + return $this->withAggregate($relation, 'max', $field, $subQuery); + } + + /** + * 关联统计Min + * @access public + * @param string|array $relation 关联方法名 + * @param string $field 字段 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + public function withMin($relation, $field, $subQuery = true) + { + return $this->withAggregate($relation, 'min', $field, $subQuery); + } + + /** + * 关联统计Avg + * @access public + * @param string|array $relation 关联方法名 + * @param string $field 字段 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + public function withAvg($relation, $field, $subQuery = true) + { + return $this->withAggregate($relation, 'avg', $field, $subQuery); + } + + /** + * 关联预加载中 获取关联指定字段值 + * example: + * Model::with(['relation' => function($query){ + * $query->withField("id,name"); + * }]) + * + * @access public + * @param string | array $field 指定获取的字段 + * @return $this + */ + public function withField($field) + { + $this->options['with_field'] = $field; + + return $this; + } + + /** + * 设置当前字段添加的表别名 + * @access public + * @param string $via + * @return $this + */ + public function via($via = '') + { + $this->options['via'] = $via; + + return $this; + } + + /** + * 设置关联查询 + * @access public + * @param string|array $relation 关联名称 + * @return $this + */ + public function relation($relation) + { + if (empty($relation)) { + return $this; + } + + if (is_string($relation)) { + $relation = explode(',', $relation); + } + + if (isset($this->options['relation'])) { + $this->options['relation'] = array_merge($this->options['relation'], $relation); + } else { + $this->options['relation'] = $relation; + } + + return $this; + } + + /** + * 插入记录 + * @access public + * @param array $data 数据 + * @param boolean $replace 是否replace + * @param boolean $getLastInsID 返回自增主键 + * @param string $sequence 自增序列名 + * @return integer|string + */ + public function insert(array $data = [], $replace = false, $getLastInsID = false, $sequence = null) + { + $this->parseOptions(); + + $this->options['data'] = array_merge($this->options['data'], $data); + + return $this->connection->insert($this, $replace, $getLastInsID, $sequence); + } + + /** + * 插入记录并获取自增ID + * @access public + * @param array $data 数据 + * @param boolean $replace 是否replace + * @param string $sequence 自增序列名 + * @return integer|string + */ + public function insertGetId(array $data, $replace = false, $sequence = null) + { + return $this->insert($data, $replace, true, $sequence); + } + + /** + * 批量插入记录 + * @access public + * @param array $dataSet 数据集 + * @param boolean $replace 是否replace + * @param integer $limit 每次写入数据限制 + * @return integer|string + */ + public function insertAll(array $dataSet = [], $replace = false, $limit = null) + { + $this->parseOptions(); + + if (empty($dataSet)) { + $dataSet = $this->options['data']; + } + + if (empty($limit) && !empty($this->options['limit'])) { + $limit = $this->options['limit']; + } + + return $this->connection->insertAll($this, $dataSet, $replace, $limit); + } + + /** + * 通过Select方式插入记录 + * @access public + * @param string $fields 要插入的数据表字段名 + * @param string $table 要插入的数据表名 + * @return integer|string + * @throws PDOException + */ + public function selectInsert($fields, $table) + { + $this->parseOptions(); + + return $this->connection->selectInsert($this, $fields, $table); + } + + /** + * 更新记录 + * @access public + * @param mixed $data 数据 + * @return integer|string + * @throws Exception + * @throws PDOException + */ + public function update(array $data = []) + { + $this->parseOptions(); + + $this->options['data'] = array_merge($this->options['data'], $data); + + return $this->connection->update($this); + } + + /** + * 删除记录 + * @access public + * @param mixed $data 表达式 true 表示强制删除 + * @return int + * @throws Exception + * @throws PDOException + */ + public function delete($data = null) + { + $this->parseOptions(); + + if (!is_null($data) && true !== $data) { + // AR模式分析主键条件 + $this->parsePkWhere($data); + } + + if (!empty($this->options['soft_delete'])) { + // 软删除 + list($field, $condition) = $this->options['soft_delete']; + if ($condition) { + unset($this->options['soft_delete']); + $this->options['data'] = [$field => $condition]; + + return $this->connection->update($this); + } + } + + $this->options['data'] = $data; + + return $this->connection->delete($this); + } + + /** + * 执行查询但只返回PDOStatement对象 + * @access public + * @return \PDOStatement|string + */ + public function getPdo() + { + $this->parseOptions(); + + return $this->connection->pdo($this); + } + + /** + * 使用游标查找记录 + * @access public + * @param array|string|Query|\Closure $data + * @return \Generator + */ + public function cursor($data = null) + { + if ($data instanceof \Closure) { + $data($this); + $data = null; + } + + $this->parseOptions(); + + if (!is_null($data)) { + // 主键条件分析 + $this->parsePkWhere($data); + } + + $this->options['data'] = $data; + + $connection = clone $this->connection; + + return $connection->cursor($this); + } + + /** + * 查找记录 + * @access public + * @param array|string|Query|\Closure $data + * @return Collection|array|\PDOStatement|string + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function select($data = null) + { + if ($data instanceof Query) { + return $data->select(); + } elseif ($data instanceof \Closure) { + $data($this); + $data = null; + } + + $this->parseOptions(); + + if (false === $data) { + // 用于子查询 不查询只返回SQL + $this->options['fetch_sql'] = true; + } elseif (!is_null($data)) { + // 主键条件分析 + $this->parsePkWhere($data); + } + + $this->options['data'] = $data; + + $resultSet = $this->connection->select($this); + + if ($this->options['fetch_sql']) { + return $resultSet; + } + + // 返回结果处理 + if (!empty($this->options['fail']) && count($resultSet) == 0) { + $this->throwNotFound($this->options); + } + + // 数据列表读取后的处理 + if (!empty($this->model)) { + // 生成模型对象 + $resultSet = $this->resultSetToModelCollection($resultSet); + } else { + $this->resultSet($resultSet); + } + + return $resultSet; + } + + /** + * 查询数据转换为模型数据集对象 + * @access protected + * @param array $resultSet 数据集 + * @return ModelCollection + */ + protected function resultSetToModelCollection(array $resultSet) + { + if (!empty($this->options['collection']) && is_string($this->options['collection'])) { + $collection = $this->options['collection']; + } + + if (empty($resultSet)) { + return $this->model->toCollection([], isset($collection) ? $collection : null); + } + + // 检查动态获取器 + if (!empty($this->options['with_attr'])) { + foreach ($this->options['with_attr'] as $name => $val) { + if (strpos($name, '.')) { + list($relation, $field) = explode('.', $name); + + $withRelationAttr[$relation][$field] = $val; + unset($this->options['with_attr'][$name]); + } + } + } + + $withRelationAttr = isset($withRelationAttr) ? $withRelationAttr : []; + + foreach ($resultSet as $key => &$result) { + // 数据转换为模型对象 + $this->resultToModel($result, $this->options, true, $withRelationAttr); + } + + if (!empty($this->options['with'])) { + // 预载入 + $result->eagerlyResultSet($resultSet, $this->options['with'], $withRelationAttr); + } + + if (!empty($this->options['with_join'])) { + // JOIN预载入 + $result->eagerlyResultSet($resultSet, $this->options['with_join'], $withRelationAttr, true); + } + + // 模型数据集转换 + return $result->toCollection($resultSet, isset($collection) ? $collection : null); + } + + /** + * 处理数据集 + * @access public + * @param array $resultSet + * @return void + */ + protected function resultSet(&$resultSet) + { + if (!empty($this->options['json'])) { + foreach ($resultSet as &$result) { + $this->jsonResult($result, $this->options['json'], true); + } + } + + if (!empty($this->options['with_attr'])) { + foreach ($resultSet as &$result) { + $this->getResultAttr($result, $this->options['with_attr']); + } + } + + if (!empty($this->options['collection']) || 'collection' == $this->connection->getConfig('resultset_type')) { + // 返回Collection对象 + $resultSet = new Collection($resultSet); + } + } + + /** + * 查找单条记录 + * @access public + * @param array|string|Query|\Closure $data + * @return array|null|\PDOStatement|string|Model + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function find($data = null) + { + if ($data instanceof Query) { + return $data->find(); + } elseif ($data instanceof \Closure) { + $data($this); + $data = null; + } + + $this->parseOptions(); + + if (!is_null($data)) { + // AR模式分析主键条件 + $this->parsePkWhere($data); + } + + $this->options['data'] = $data; + + $result = $this->connection->find($this); + + if ($this->options['fetch_sql']) { + return $result; + } + + // 数据处理 + if (empty($result)) { + return $this->resultToEmpty(); + } + + if (!empty($this->model)) { + // 返回模型对象 + $this->resultToModel($result, $this->options); + } else { + $this->result($result); + } + + return $result; + } + + /** + * 处理空数据 + * @access protected + * @return array|Model|null + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + protected function resultToEmpty() + { + if (!empty($this->options['allow_empty'])) { + return !empty($this->model) ? $this->model->newInstance([], $this->getModelUpdateCondition($this->options)) : []; + } elseif (!empty($this->options['fail'])) { + $this->throwNotFound($this->options); + } + } + + /** + * 查找单条记录 + * @access public + * @param mixed $data 主键值或者查询条件(闭包) + * @param mixed $with 关联预查询 + * @param bool $cache 是否缓存 + * @param bool $failException 是否抛出异常 + * @return static|null + * @throws exception\DbException + */ + public function get($data, $with = [], $cache = false, $failException = false) + { + if (is_null($data)) { + return; + } + + if (true === $with || is_int($with)) { + $cache = $with; + $with = []; + } + + return $this->parseQuery($data, $with, $cache) + ->failException($failException) + ->find($data); + } + + /** + * 查找单条记录 如果不存在直接抛出异常 + * @access public + * @param mixed $data 主键值或者查询条件(闭包) + * @param mixed $with 关联预查询 + * @param bool $cache 是否缓存 + * @return static|null + * @throws exception\DbException + */ + public function getOrFail($data, $with = [], $cache = false) + { + return $this->get($data, $with, $cache, true); + } + + /** + * 查找所有记录 + * @access public + * @param mixed $data 主键列表或者查询条件(闭包) + * @param array|string $with 关联预查询 + * @param bool $cache 是否缓存 + * @return static[]|false + * @throws exception\DbException + */ + public function all($data = null, $with = [], $cache = false) + { + if (true === $with || is_int($with)) { + $cache = $with; + $with = []; + } + + return $this->parseQuery($data, $with, $cache)->select($data); + } + + /** + * 分析查询表达式 + * @access public + * @param mixed $data 主键列表或者查询条件(闭包) + * @param string $with 关联预查询 + * @param bool $cache 是否缓存 + * @return Query + */ + protected function parseQuery(&$data, $with, $cache) + { + $result = $this->with($with)->cache($cache); + + if ((is_array($data) && key($data) !== 0) || $data instanceof Where) { + $result = $result->where($data); + $data = null; + } elseif ($data instanceof \Closure) { + $data($result); + $data = null; + } elseif ($data instanceof Query) { + $result = $data->with($with)->cache($cache); + $data = null; + } + + return $result; + } + + /** + * 处理数据 + * @access protected + * @param array $result 查询数据 + * @return void + */ + protected function result(&$result) + { + if (!empty($this->options['json'])) { + $this->jsonResult($result, $this->options['json'], true); + } + + if (!empty($this->options['with_attr'])) { + $this->getResultAttr($result, $this->options['with_attr']); + } + } + + /** + * 使用获取器处理数据 + * @access protected + * @param array $result 查询数据 + * @param array $withAttr 字段获取器 + * @return void + */ + protected function getResultAttr(&$result, $withAttr = []) + { + foreach ($withAttr as $name => $closure) { + $name = Loader::parseName($name); + + if (strpos($name, '.')) { + // 支持JSON字段 获取器定义 + list($key, $field) = explode('.', $name); + + if (isset($result[$key])) { + $result[$key][$field] = $closure(isset($result[$key][$field]) ? $result[$key][$field] : null, $result[$key]); + } + } else { + $result[$name] = $closure(isset($result[$name]) ? $result[$name] : null, $result); + } + } + } + + /** + * JSON字段数据转换 + * @access protected + * @param array $result 查询数据 + * @param array $json JSON字段 + * @param bool $assoc 是否转换为数组 + * @param array $withRelationAttr 关联获取器 + * @return void + */ + protected function jsonResult(&$result, $json = [], $assoc = false, $withRelationAttr = []) + { + foreach ($json as $name) { + if (isset($result[$name])) { + $result[$name] = json_decode($result[$name], $assoc); + + if (isset($withRelationAttr[$name])) { + foreach ($withRelationAttr[$name] as $key => $closure) { + $data = get_object_vars($result[$name]); + $result[$name]->$key = $closure(isset($result[$name]->$key) ? $result[$name]->$key : null, $data); + } + } + } + } + } + + /** + * 查询数据转换为模型对象 + * @access protected + * @param array $result 查询数据 + * @param array $options 查询参数 + * @param bool $resultSet 是否为数据集查询 + * @param array $withRelationAttr 关联字段获取器 + * @return void + */ + protected function resultToModel(&$result, $options = [], $resultSet = false, $withRelationAttr = []) + { + // 动态获取器 + if (!empty($options['with_attr']) && empty($withRelationAttr)) { + foreach ($options['with_attr'] as $name => $val) { + if (strpos($name, '.')) { + list($relation, $field) = explode('.', $name); + + $withRelationAttr[$relation][$field] = $val; + unset($options['with_attr'][$name]); + } + } + } + + // JSON 数据处理 + if (!empty($options['json'])) { + $this->jsonResult($result, $options['json'], $options['json_assoc'], $withRelationAttr); + } + + $result = $this->model->newInstance($result, $resultSet ? null : $this->getModelUpdateCondition($options)); + + // 动态获取器 + if (!empty($options['with_attr'])) { + $result->withAttribute($options['with_attr']); + } + + // 输出属性控制 + if (!empty($options['visible'])) { + $result->visible($options['visible'], true); + } elseif (!empty($options['hidden'])) { + $result->hidden($options['hidden'], true); + } + + if (!empty($options['append'])) { + $result->append($options['append'], true); + } + + // 关联查询 + if (!empty($options['relation'])) { + $result->relationQuery($options['relation'], $withRelationAttr); + } + + // 预载入查询 + if (!$resultSet && !empty($options['with'])) { + $result->eagerlyResult($result, $options['with'], $withRelationAttr); + } + + // JOIN预载入查询 + if (!$resultSet && !empty($options['with_join'])) { + $result->eagerlyResult($result, $options['with_join'], $withRelationAttr, true); + } + + // 关联统计 + if (!empty($options['with_count'])) { + foreach ($options['with_count'] as $val) { + $result->relationCount($result, $val[0], $val[1], $val[2]); + } + } + } + + /** + * 获取模型的更新条件 + * @access protected + * @param array $options 查询参数 + */ + protected function getModelUpdateCondition(array $options) + { + return isset($options['where']['AND']) ? $options['where']['AND'] : null; + } + + /** + * 查询失败 抛出异常 + * @access protected + * @param array $options 查询参数 + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + protected function throwNotFound($options = []) + { + if (!empty($this->model)) { + $class = get_class($this->model); + throw new ModelNotFoundException('model data Not Found:' . $class, $class, $options); + } + $table = is_array($options['table']) ? key($options['table']) : $options['table']; + throw new DataNotFoundException('table data not Found:' . $table, $table, $options); + } + + /** + * 查找多条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return array|\PDOStatement|string|Model + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function selectOrFail($data = null) + { + return $this->failException(true)->select($data); + } + + /** + * 查找单条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return array|\PDOStatement|string|Model + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function findOrFail($data = null) + { + return $this->failException(true)->find($data); + } + + /** + * 查找单条记录 不存在则返回空模型 + * @access public + * @param array|string|Query|\Closure $data + * @return array|\PDOStatement|string|Model + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function findOrEmpty($data = null) + { + return $this->allowEmpty(true)->find($data); + } + + /** + * 分批数据返回处理 + * @access public + * @param integer $count 每次处理的数据数量 + * @param callable $callback 处理回调方法 + * @param string|array $column 分批处理的字段名 + * @param string $order 字段排序 + * @return boolean + * @throws DbException + */ + public function chunk($count, $callback, $column = null, $order = 'asc') + { + $options = $this->getOptions(); + $column = $column ?: $this->getPk($options); + + if (isset($options['order'])) { + if (Container::get('app')->isDebug()) { + throw new DbException('chunk not support call order'); + } + unset($options['order']); + } + + $bind = $this->bind; + + if (is_array($column)) { + $times = 1; + $query = $this->options($options)->page($times, $count); + } else { + $query = $this->options($options)->limit($count); + + if (strpos($column, '.')) { + list($alias, $key) = explode('.', $column); + } else { + $key = $column; + } + } + + $resultSet = $query->order($column, $order)->select(); + + while (count($resultSet) > 0) { + if ($resultSet instanceof Collection) { + $resultSet = $resultSet->all(); + } + + if (false === call_user_func($callback, $resultSet)) { + return false; + } + + if (isset($times)) { + $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; + } + + /** + * 获取绑定的参数 并清空 + * @access public + * @param bool $clear + * @return array + */ + public function getBind($clear = true) + { + $bind = $this->bind; + if ($clear) { + $this->bind = []; + } + + return $bind; + } + + /** + * 创建子查询SQL + * @access public + * @param bool $sub + * @return string + * @throws DbException + */ + public function buildSql($sub = true) + { + return $sub ? '( ' . $this->select(false) . ' )' : $this->select(false); + } + + /** + * 视图查询处理 + * @access protected + * @param array $options 查询参数 + * @return void + */ + protected function parseView(&$options) + { + if (!isset($options['map'])) { + return; + } + + foreach (['AND', 'OR'] as $logic) { + if (isset($options['where'][$logic])) { + foreach ($options['where'][$logic] as $key => $val) { + if (array_key_exists($key, $options['map'])) { + array_shift($val); + array_unshift($val, $options['map'][$key]); + $options['where'][$logic][$options['map'][$key]] = $val; + unset($options['where'][$logic][$key]); + } + } + } + } + + if (isset($options['order'])) { + // 视图查询排序处理 + if (is_string($options['order'])) { + $options['order'] = explode(',', $options['order']); + } + foreach ($options['order'] as $key => $val) { + if (is_numeric($key) && is_string($val)) { + if (strpos($val, ' ')) { + list($field, $sort) = explode(' ', $val); + if (array_key_exists($field, $options['map'])) { + $options['order'][$options['map'][$field]] = $sort; + unset($options['order'][$key]); + } + } elseif (array_key_exists($val, $options['map'])) { + $options['order'][$options['map'][$val]] = 'asc'; + unset($options['order'][$key]); + } + } elseif (array_key_exists($key, $options['map'])) { + $options['order'][$options['map'][$key]] = $val; + unset($options['order'][$key]); + } + } + } + } + + /** + * 把主键值转换为查询条件 支持复合主键 + * @access public + * @param array|string $data 主键数据 + * @return void + * @throws Exception + */ + public function parsePkWhere($data) + { + $pk = $this->getPk($this->options); + // 获取当前数据表 + $table = is_array($this->options['table']) ? key($this->options['table']) : $this->options['table']; + + if (!empty($this->options['alias'][$table])) { + $alias = $this->options['alias'][$table]; + } + + if (is_string($pk)) { + $key = isset($alias) ? $alias . '.' . $pk : $pk; + // 根据主键查询 + if (is_array($data)) { + $where[$pk] = isset($data[$pk]) ? [$key, '=', $data[$pk]] : [$key, 'in', $data]; + } else { + $where[$pk] = strpos($data, ',') ? [$key, 'IN', $data] : [$key, '=', $data]; + } + } elseif (is_array($pk) && is_array($data) && !empty($data)) { + // 根据复合主键查询 + foreach ($pk as $key) { + if (isset($data[$key])) { + $attr = isset($alias) ? $alias . '.' . $key : $key; + $where[$key] = [$attr, '=', $data[$key]]; + } else { + throw new Exception('miss complex primary data'); + } + } + } + + if (!empty($where)) { + if (isset($this->options['where']['AND'])) { + $this->options['where']['AND'] = array_merge($this->options['where']['AND'], $where); + } else { + $this->options['where']['AND'] = $where; + } + } + + return; + } + + /** + * 分析表达式(可用于查询或者写入操作) + * @access protected + * @return array + */ + protected function parseOptions() + { + $options = $this->getOptions(); + + // 获取数据表 + if (empty($options['table'])) { + $options['table'] = $this->getTable(); + } + + if (!isset($options['where'])) { + $options['where'] = []; + } elseif (isset($options['view'])) { + // 视图查询条件处理 + $this->parseView($options); + } + + if (!isset($options['field'])) { + $options['field'] = '*'; + } + + foreach (['data', 'order', 'join', 'union'] as $name) { + if (!isset($options[$name])) { + $options[$name] = []; + } + } + + if (!isset($options['strict'])) { + $options['strict'] = $this->getConfig('fields_strict'); + } + + foreach (['master', 'lock', 'fetch_pdo', 'fetch_sql', 'distinct'] as $name) { + if (!isset($options[$name])) { + $options[$name] = false; + } + } + + if (isset(static::$readMaster['*']) || (is_string($options['table']) && isset(static::$readMaster[$options['table']]))) { + $options['master'] = true; + } + + foreach (['group', 'having', 'limit', 'force', 'comment'] as $name) { + if (!isset($options[$name])) { + $options[$name] = ''; + } + } + + if (isset($options['page'])) { + // 根据页数计算limit + list($page, $listRows) = $options['page']; + $page = $page > 0 ? $page : 1; + $listRows = $listRows > 0 ? $listRows : (is_numeric($options['limit']) ? $options['limit'] : 20); + $offset = $listRows * ($page - 1); + $options['limit'] = $offset . ',' . $listRows; + } + + $this->options = $options; + + return $options; + } + + /** + * 注册回调方法 + * @access public + * @param string $event 事件名 + * @param callable $callback 回调方法 + * @return void + */ + public static function event($event, $callback) + { + self::$event[$event] = $callback; + } + + /** + * 触发事件 + * @access public + * @param string $event 事件名 + * @return bool + */ + public function trigger($event) + { + $result = false; + + if (isset(self::$event[$event])) { + $result = Container::getInstance()->invoke(self::$event[$event], [$this]); + } + + return $result; + } + +} diff --git a/vendor/topthink/framework/library/think/db/Where.php b/vendor/topthink/framework/library/think/db/Where.php new file mode 100644 index 0000000..9132e54 --- /dev/null +++ b/vendor/topthink/framework/library/think/db/Where.php @@ -0,0 +1,178 @@ + +// +---------------------------------------------------------------------- + +namespace think\db; + +use ArrayAccess; + +class Where implements ArrayAccess +{ + /** + * 查询表达式 + * @var array + */ + protected $where = []; + + /** + * 是否需要增加括号 + * @var bool + */ + protected $enclose = false; + + /** + * 创建一个查询表达式 + * + * @param array $where 查询条件数组 + * @param bool $enclose 是否增加括号 + */ + public function __construct(array $where = [], $enclose = false) + { + $this->where = $where; + $this->enclose = $enclose; + } + + /** + * 设置是否添加括号 + * @access public + * @param bool $enclose + * @return $this + */ + public function enclose($enclose = true) + { + $this->enclose = $enclose; + return $this; + } + + /** + * 解析为Query对象可识别的查询条件数组 + * @access public + * @return array + */ + public function parse() + { + $where = []; + + foreach ($this->where as $key => $val) { + if ($val instanceof Expression) { + $where[] = [$key, 'exp', $val]; + } elseif (is_null($val)) { + $where[] = [$key, 'NULL', '']; + } elseif (is_array($val)) { + $where[] = $this->parseItem($key, $val); + } else { + $where[] = [$key, '=', $val]; + } + } + + return $this->enclose ? [$where] : $where; + } + + /** + * 分析查询表达式 + * @access protected + * @param string $field 查询字段 + * @param array $where 查询条件 + * @return array + */ + protected function parseItem($field, $where = []) + { + $op = $where[0]; + $condition = isset($where[1]) ? $where[1] : null; + + if (is_array($op)) { + // 同一字段多条件查询 + array_unshift($where, $field); + } elseif (is_null($condition)) { + if (in_array(strtoupper($op), ['NULL', 'NOTNULL', 'NOT NULL'], true)) { + // null查询 + $where = [$field, $op, '']; + } elseif (in_array($op, ['=', 'eq', 'EQ', null], true)) { + $where = [$field, 'NULL', '']; + } elseif (in_array($op, ['<>', 'neq', 'NEQ'], true)) { + $where = [$field, 'NOTNULL', '']; + } else { + // 字段相等查询 + $where = [$field, '=', $op]; + } + } else { + $where = [$field, $op, $condition]; + } + + return $where; + } + + /** + * 修改器 设置数据对象的值 + * @access public + * @param string $name 名称 + * @param mixed $value 值 + * @return void + */ + public function __set($name, $value) + { + $this->where[$name] = $value; + } + + /** + * 获取器 获取数据对象的值 + * @access public + * @param string $name 名称 + * @return mixed + */ + public function __get($name) + { + return isset($this->where[$name]) ? $this->where[$name] : null; + } + + /** + * 检测数据对象的值 + * @access public + * @param string $name 名称 + * @return boolean + */ + public function __isset($name) + { + return isset($this->where[$name]); + } + + /** + * 销毁数据对象的值 + * @access public + * @param string $name 名称 + * @return void + */ + public function __unset($name) + { + unset($this->where[$name]); + } + + // ArrayAccess + public function offsetSet($name, $value) + { + $this->__set($name, $value); + } + + public function offsetExists($name) + { + return $this->__isset($name); + } + + public function offsetUnset($name) + { + $this->__unset($name); + } + + public function offsetGet($name) + { + return $this->__get($name); + } + +} diff --git a/vendor/topthink/framework/library/think/db/builder/Mysql.php b/vendor/topthink/framework/library/think/db/builder/Mysql.php new file mode 100644 index 0000000..f7384b3 --- /dev/null +++ b/vendor/topthink/framework/library/think/db/builder/Mysql.php @@ -0,0 +1,184 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\builder; + +use think\db\Builder; +use think\db\Expression; +use think\db\Query; +use think\Exception; + +/** + * mysql数据库驱动 + */ +class Mysql extends Builder +{ + // 查询表达式解析 + protected $parser = [ + 'parseCompare' => ['=', '<>', '>', '>=', '<', '<='], + 'parseLike' => ['LIKE', 'NOT LIKE'], + 'parseBetween' => ['NOT BETWEEN', 'BETWEEN'], + 'parseIn' => ['NOT IN', 'IN'], + 'parseExp' => ['EXP'], + 'parseRegexp' => ['REGEXP', 'NOT REGEXP'], + 'parseNull' => ['NOT NULL', 'NULL'], + 'parseBetweenTime' => ['BETWEEN TIME', 'NOT BETWEEN TIME'], + 'parseTime' => ['< TIME', '> TIME', '<= TIME', '>= TIME'], + 'parseExists' => ['NOT EXISTS', 'EXISTS'], + 'parseColumn' => ['COLUMN'], + ]; + + 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 Query $query 查询对象 + * @param array $dataSet 数据集 + * @param bool $replace 是否replace + * @return string + */ + public function insertAll(Query $query, $dataSet, $replace = false) + { + $options = $query->getOptions(); + + // 获取合法的字段 + if ('*' == $options['field']) { + $allowFields = $this->connection->getTableFields($options['table']); + } else { + $allowFields = $options['field']; + } + + // 获取绑定信息 + $bind = $this->connection->getFieldsBind($options['table']); + + foreach ($dataSet as $k => $data) { + $data = $this->parseData($query, $data, $allowFields, $bind); + + $values[] = '( ' . implode(',', array_values($data)) . ' )'; + + if (!isset($insertFields)) { + $insertFields = array_keys($data); + } + } + + $fields = []; + foreach ($insertFields as $field) { + $fields[] = $this->parseKey($query, $field); + } + + return str_replace( + ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], + [ + $replace ? 'REPLACE' : 'INSERT', + $this->parseTable($query, $options['table']), + implode(' , ', $fields), + implode(' , ', $values), + $this->parseComment($query, $options['comment']), + ], + $this->insertAllSql); + } + + /** + * 正则查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @return string + */ + protected function parseRegexp(Query $query, $key, $exp, $value, $field) + { + if ($value instanceof Expression) { + $value = $value->getValue(); + } + + return $key . ' ' . $exp . ' ' . $value; + } + + /** + * 字段和表名处理 + * @access public + * @param Query $query 查询对象 + * @param mixed $key 字段名 + * @param bool $strict 严格检测 + * @return string + */ + public function parseKey(Query $query, $key, $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, 2); + + return $this->parseKey($query, $field, true) . '->>\'$' . (strpos($name, '[') === 0 ? '' : '.') . str_replace('->>', '.', $name) . '\''; + } + elseif (strpos($key, '->') && false === strpos($key, '(')) { + // JSON字段支持 + list($field, $name) = explode('->', $key, 2); + + return 'json_extract(' . $this->parseKey($query, $field, true) . ', \'$' . (strpos($name, '[') === 0 ? '' : '.') . str_replace('->', '.', $name) . '\')'; + } elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) { + list($table, $key) = explode('.', $key, 2); + + $alias = $query->getOptions('alias'); + + if ('__TABLE__' == $table) { + $table = $query->getOptions('table'); + $table = is_array($table) ? array_shift($table) : $table; + } + + if (isset($alias[$table])) { + $table = $alias[$table]; + } + } + + if ($strict && !preg_match('/^[\w\.\*]+$/', $key)) { + throw new Exception('not support data:' . $key); + } + + if ('*' != $key && !preg_match('/[,\'\"\*\(\)`.\s]/', $key)) { + $key = '`' . $key . '`'; + } + + if (isset($table)) { + if (strpos($table, '.')) { + $table = str_replace('.', '`.`', $table); + } + + $key = '`' . $table . '`.' . $key; + } + + return $key; + } + + /** + * 随机排序 + * @access protected + * @param Query $query 查询对象 + * @return string + */ + protected function parseRand(Query $query) + { + return 'rand()'; + } + +} diff --git a/vendor/topthink/framework/library/think/db/builder/Pgsql.php b/vendor/topthink/framework/library/think/db/builder/Pgsql.php new file mode 100644 index 0000000..742c7db --- /dev/null +++ b/vendor/topthink/framework/library/think/db/builder/Pgsql.php @@ -0,0 +1,104 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\builder; + +use think\db\Builder; +use think\db\Query; + +/** + * Pgsql数据库驱动 + */ +class Pgsql extends Builder +{ + + protected $insertSql = 'INSERT INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%'; + protected $insertAllSql = 'INSERT INTO %TABLE% (%FIELD%) %DATA% %COMMENT%'; + + /** + * limit分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $limit + * @return string + */ + public function parseLimit(Query $query, $limit) + { + $limitStr = ''; + + if (!empty($limit)) { + $limit = explode(',', $limit); + if (count($limit) > 1) { + $limitStr .= ' LIMIT ' . $limit[1] . ' OFFSET ' . $limit[0] . ' '; + } else { + $limitStr .= ' LIMIT ' . $limit[0] . ' '; + } + } + + return $limitStr; + } + + /** + * 字段和表名处理 + * @access public + * @param Query $query 查询对象 + * @param mixed $key 字段名 + * @param bool $strict 严格检测 + * @return string + */ + public function parseKey(Query $query, $key, $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 = $field . '->>\'' . $name . '\''; + } elseif (strpos($key, '.')) { + list($table, $key) = explode('.', $key, 2); + + $alias = $query->getOptions('alias'); + + if ('__TABLE__' == $table) { + $table = $query->getOptions('table'); + $table = is_array($table) ? array_shift($table) : $table; + } + + if (isset($alias[$table])) { + $table = $alias[$table]; + } + } + + if (isset($table)) { + $key = $table . '.' . $key; + } + + return $key; + } + + /** + * 随机排序 + * @access protected + * @param Query $query 查询对象 + * @return string + */ + protected function parseRand(Query $query) + { + return 'RANDOM()'; + } + +} diff --git a/vendor/topthink/framework/library/think/db/builder/Sqlite.php b/vendor/topthink/framework/library/think/db/builder/Sqlite.php new file mode 100644 index 0000000..2b887ca --- /dev/null +++ b/vendor/topthink/framework/library/think/db/builder/Sqlite.php @@ -0,0 +1,96 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\builder; + +use think\db\Builder; +use think\db\Query; + +/** + * Sqlite数据库驱动 + */ +class Sqlite extends Builder +{ + + /** + * limit + * @access public + * @param Query $query 查询对象 + * @param mixed $limit + * @return string + */ + public function parseLimit(Query $query, $limit) + { + $limitStr = ''; + + if (!empty($limit)) { + $limit = explode(',', $limit); + if (count($limit) > 1) { + $limitStr .= ' LIMIT ' . $limit[1] . ' OFFSET ' . $limit[0] . ' '; + } else { + $limitStr .= ' LIMIT ' . $limit[0] . ' '; + } + } + + return $limitStr; + } + + /** + * 随机排序 + * @access protected + * @param Query $query 查询对象 + * @return string + */ + protected function parseRand(Query $query) + { + return 'RANDOM()'; + } + + /** + * 字段和表名处理 + * @access public + * @param Query $query 查询对象 + * @param mixed $key 字段名 + * @param bool $strict 严格检测 + * @return string + */ + public function parseKey(Query $query, $key, $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); + + $alias = $query->getOptions('alias'); + + if ('__TABLE__' == $table) { + $table = $query->getOptions('table'); + $table = is_array($table) ? array_shift($table) : $table; + } + + if (isset($alias[$table])) { + $table = $alias[$table]; + } + } + + if (isset($table)) { + $key = $table . '.' . $key; + } + + return $key; + } +} diff --git a/vendor/topthink/framework/library/think/db/builder/Sqlsrv.php b/vendor/topthink/framework/library/think/db/builder/Sqlsrv.php new file mode 100644 index 0000000..ef27aaf --- /dev/null +++ b/vendor/topthink/framework/library/think/db/builder/Sqlsrv.php @@ -0,0 +1,159 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\builder; + +use think\db\Builder; +use think\db\Expression; +use think\db\Query; +use think\Exception; + +/** + * Sqlsrv数据库驱动 + */ +class Sqlsrv extends Builder +{ + protected $selectSql = 'SELECT T1.* FROM (SELECT thinkphp.*, ROW_NUMBER() OVER (%ORDER%) AS ROW_NUMBER FROM (SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%) AS thinkphp) AS T1 %LIMIT%%COMMENT%'; + 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分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $order + * @return string + */ + protected function parseOrder(Query $query, $order) + { + if (empty($order)) { + return ' ORDER BY rand()'; + } + + foreach ($order as $key => $val) { + if ($val instanceof Expression) { + $array[] = $val->getValue(); + } elseif ('[rand]' == $val) { + $array[] = $this->parseRand($query); + } else { + if (is_numeric($key)) { + list($key, $sort) = explode(' ', strpos($val, ' ') ? $val : $val . ' '); + } else { + $sort = $val; + } + + if (preg_match('/^[\w\.]+$/', $key)) { + $sort = strtoupper($sort); + $sort = in_array($sort, ['ASC', 'DESC'], true) ? ' ' . $sort : ''; + $array[] = $this->parseKey($query, $key, true) . $sort; + } else { + throw new Exception('order express error:' . $key); + } + } + } + + return empty($array) ? '' : ' ORDER BY ' . implode(',', $array); + } + + /** + * 随机排序 + * @access protected + * @param Query $query 查询对象 + * @return string + */ + protected function parseRand(Query $query) + { + return 'rand()'; + } + + /** + * 字段和表名处理 + * @access public + * @param Query $query 查询对象 + * @param mixed $key 字段名 + * @param bool $strict 严格检测 + * @return string + */ + public function parseKey(Query $query, $key, $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); + + $alias = $query->getOptions('alias'); + + if ('__TABLE__' == $table) { + $table = $query->getOptions('table'); + $table = is_array($table) ? array_shift($table) : $table; + } + + if (isset($alias[$table])) { + $table = $alias[$table]; + } + } + + if ($strict && !preg_match('/^[\w\.\*]+$/', $key)) { + throw new Exception('not support data:' . $key); + } + + if ('*' != $key && !preg_match('/[,\'\"\*\(\)\[.\s]/', $key)) { + $key = '[' . $key . ']'; + } + + if (isset($table)) { + $key = '[' . $table . '].' . $key; + } + + return $key; + } + + /** + * limit + * @access protected + * @param Query $query 查询对象 + * @param mixed $limit + * @return string + */ + protected function parseLimit(Query $query, $limit) + { + if (empty($limit)) { + return ''; + } + + $limit = explode(',', $limit); + + if (count($limit) > 1) { + $limitStr = '(T1.ROW_NUMBER BETWEEN ' . $limit[0] . ' + 1 AND ' . $limit[0] . ' + ' . $limit[1] . ')'; + } else { + $limitStr = '(T1.ROW_NUMBER BETWEEN 1 AND ' . $limit[0] . ")"; + } + + return 'WHERE ' . $limitStr; + } + + public function selectInsert(Query $query, $fields, $table) + { + $this->selectSql = $this->selectInsertSql; + + return parent::selectInsert($query, $fields, $table); + } + +} diff --git a/vendor/topthink/framework/library/think/db/connector/Mysql.php b/vendor/topthink/framework/library/think/db/connector/Mysql.php new file mode 100644 index 0000000..cfd2ac7 --- /dev/null +++ b/vendor/topthink/framework/library/think/db/connector/Mysql.php @@ -0,0 +1,229 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\Connection; +use think\db\Query; + +/** + * mysql数据库驱动 + */ +class Mysql extends Connection +{ + + protected $builder = '\\think\\db\\builder\\Mysql'; + + /** + * 初始化 + * @access protected + * @return void + */ + protected function initialize() + { + // Point类型支持 + Query::extend('point', function ($query, $field, $value = null, $fun = 'GeomFromText', $type = 'POINT') { + if (!is_null($value)) { + $query->data($field, ['point', $value, $fun, $type]); + } else { + if (is_string($field)) { + $field = explode(',', $field); + } + $query->setOption('point', $field); + } + + return $query; + }); + } + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn($config) + { + 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']; + } + + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields($tableName) + { + list($tableName) = explode(' ', $tableName); + + if (false === strpos($tableName, '`')) { + if (strpos($tableName, '.')) { + $tableName = str_replace('.', '`.`', $tableName); + } + $tableName = '`' . $tableName . '`'; + } + + $sql = 'SHOW COLUMNS FROM ' . $tableName; + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + if ($result) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + $info[$val['field']] = [ + 'name' => $val['field'], + 'type' => $val['type'], + 'notnull' => 'NO' == $val['null'], + 'default' => $val['default'], + 'primary' => strtolower($val['key']) == 'pri', + 'autoinc' => strtolower($val['extra']) == 'auto_increment', + ]; + } + } + + return $this->fieldCase($info); + } + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables($dbName = '') + { + $sql = !empty($dbName) ? 'SHOW TABLES FROM ' . $dbName : 'SHOW TABLES '; + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + + return $info; + } + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + protected function getExplain($sql) + { + $pdo = $this->linkID->prepare("EXPLAIN " . $this->queryStr); + + foreach ($this->bind as $key => $val) { + // 占位符 + $param = is_int($key) ? $key + 1 : ':' . $key; + + if (is_array($val)) { + if (PDO::PARAM_INT == $val[1] && '' === $val[0]) { + $val[0] = 0; + } elseif (self::PARAM_FLOAT == $val[1]) { + $val[0] = is_string($val[0]) ? (float) $val[0] : $val[0]; + $val[1] = PDO::PARAM_STR; + } + + $result = $pdo->bindValue($param, $val[0], $val[1]); + } else { + $result = $pdo->bindValue($param, $val); + } + } + + $pdo->execute(); + $result = $pdo->fetch(PDO::FETCH_ASSOC); + $result = array_change_key_case($result); + + if (isset($result['extra'])) { + if (strpos($result['extra'], 'filesort') || strpos($result['extra'], 'temporary')) { + $this->log('SQL:' . $this->queryStr . '[' . $result['extra'] . ']', 'warn'); + } + } + + return $result; + } + + protected function supportSavepoint() + { + return true; + } + + /** + * 启动XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function startTransXa($xid) + { + $this->initConnect(true); + if (!$this->linkID) { + return false; + } + + $this->linkID->exec("XA START '$xid'"); + } + + /** + * 预编译XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function prepareXa($xid) + { + $this->initConnect(true); + $this->linkID->exec("XA END '$xid'"); + $this->linkID->exec("XA PREPARE '$xid'"); + } + + /** + * 提交XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function commitXa($xid) + { + $this->initConnect(true); + $this->linkID->exec("XA COMMIT '$xid'"); + } + + /** + * 回滚XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function rollbackXa($xid) + { + $this->initConnect(true); + $this->linkID->exec("XA ROLLBACK '$xid'"); + } +} diff --git a/vendor/topthink/framework/library/think/db/connector/Pgsql.php b/vendor/topthink/framework/library/think/db/connector/Pgsql.php new file mode 100644 index 0000000..ee9fca0 --- /dev/null +++ b/vendor/topthink/framework/library/think/db/connector/Pgsql.php @@ -0,0 +1,116 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\Connection; + +/** + * Pgsql数据库驱动 + */ +class Pgsql extends Connection +{ + protected $builder = '\\think\\db\\builder\\Pgsql'; + + // PDO连接参数 + protected $params = [ + PDO::ATTR_CASE => PDO::CASE_NATURAL, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, + PDO::ATTR_STRINGIFY_FETCHES => false, + ]; + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn($config) + { + $dsn = 'pgsql:dbname=' . $config['database'] . ';host=' . $config['hostname']; + + if (!empty($config['hostport'])) { + $dsn .= ';port=' . $config['hostport']; + } + + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields($tableName) + { + list($tableName) = explode(' ', $tableName); + $sql = 'select fields_name as "field",fields_type as "type",fields_not_null as "null",fields_key_name as "key",fields_default as "default",fields_default as "extra" from table_msg(\'' . $tableName . '\');'; + + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + if ($result) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + $info[$val['field']] = [ + 'name' => $val['field'], + 'type' => $val['type'], + 'notnull' => (bool) ('' !== $val['null']), + 'default' => $val['default'], + 'primary' => !empty($val['key']), + 'autoinc' => (0 === strpos($val['extra'], 'nextval(')), + ]; + } + } + + return $this->fieldCase($info); + } + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables($dbName = '') + { + $sql = "select tablename as Tables_in_test from pg_tables where schemaname ='public'"; + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + + return $info; + } + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + protected function getExplain($sql) + { + return []; + } + + protected function supportSavepoint() + { + return true; + } +} diff --git a/vendor/topthink/framework/library/think/db/connector/Sqlite.php b/vendor/topthink/framework/library/think/db/connector/Sqlite.php new file mode 100644 index 0000000..5b9b3fa --- /dev/null +++ b/vendor/topthink/framework/library/think/db/connector/Sqlite.php @@ -0,0 +1,108 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\Connection; + +/** + * Sqlite数据库驱动 + */ +class Sqlite extends Connection +{ + + protected $builder = '\\think\\db\\builder\\Sqlite'; + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn($config) + { + $dsn = 'sqlite:' . $config['database']; + + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields($tableName) + { + list($tableName) = explode(' ', $tableName); + $sql = 'PRAGMA table_info( ' . $tableName . ' )'; + + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + if ($result) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + $info[$val['name']] = [ + 'name' => $val['name'], + 'type' => $val['type'], + 'notnull' => 1 === $val['notnull'], + 'default' => $val['dflt_value'], + 'primary' => '1' == $val['pk'], + 'autoinc' => '1' == $val['pk'], + ]; + } + } + + return $this->fieldCase($info); + } + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables($dbName = '') + { + $sql = "SELECT name FROM sqlite_master WHERE type='table' " + . "UNION ALL SELECT name FROM sqlite_temp_master " + . "WHERE type='table' ORDER BY name"; + + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + + return $info; + } + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + protected function getExplain($sql) + { + return []; + } + + protected function supportSavepoint() + { + return true; + } +} diff --git a/vendor/topthink/framework/library/think/db/connector/Sqlsrv.php b/vendor/topthink/framework/library/think/db/connector/Sqlsrv.php new file mode 100644 index 0000000..123affb --- /dev/null +++ b/vendor/topthink/framework/library/think/db/connector/Sqlsrv.php @@ -0,0 +1,235 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\Connection; +use think\db\Query; + +/** + * Sqlsrv数据库驱动 + */ +class Sqlsrv extends Connection +{ + // PDO连接参数 + protected $params = [ + PDO::ATTR_CASE => PDO::CASE_NATURAL, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, + PDO::ATTR_STRINGIFY_FETCHES => false, + ]; + + protected $builder = '\\think\\db\\builder\\Sqlsrv'; + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn($config) + { + $dsn = 'sqlsrv:Database=' . $config['database'] . ';Server=' . $config['hostname']; + + if (!empty($config['hostport'])) { + $dsn .= ',' . $config['hostport']; + } + + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields($tableName) + { + list($tableName) = explode(' ', $tableName); + $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 + AND t.table_schema = c.table_schema + AND t.table_name = c.table_name + WHERE t.table_name = '$tableName'"; + + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + if ($result) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + $info[$val['column_name']] = [ + 'name' => $val['column_name'], + 'type' => $val['data_type'], + 'notnull' => (bool) ('' === $val['is_nullable']), // not null is empty, null is yes + 'default' => $val['column_default'], + 'primary' => false, + 'autoinc' => false, + ]; + } + } + + $sql = "SELECT column_name FROM information_schema.key_column_usage WHERE table_name='$tableName'"; + + // 调试开始 + $this->debug(true); + + $pdo = $this->linkID->query($sql); + + // 调试结束 + $this->debug(false, $sql); + + $result = $pdo->fetch(PDO::FETCH_ASSOC); + + if ($result) { + $info[$result['column_name']]['primary'] = true; + } + + return $this->fieldCase($info); + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables($dbName = '') + { + $sql = "SELECT TABLE_NAME + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_TYPE = 'BASE TABLE' + "; + + $pdo = $this->query($sql, [], false, true); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + + return $info; + } + + /** + * 得到某个列的数组 + * @access public + * @param Query $query 查询对象 + * @param string $field 字段名 多个字段用逗号分隔 + * @param string $key 索引 + * @return array + */ + public function column(Query $query, $field, $key = '') + { + $options = $query->getOptions(); + + if (empty($options['fetch_sql']) && !empty($options['cache'])) { + // 判断查询缓存 + $cache = $options['cache']; + + $guid = is_string($cache['key']) ? $cache['key'] : $this->getCacheKey($query, $field); + + $result = Container::get('cache')->get($guid); + + if (false !== $result) { + return $result; + } + } + + if (isset($options['field'])) { + $query->removeOption('field'); + } + + if (is_null($field)) { + $field = '*'; + } elseif ($key && '*' != $field) { + $field = $key . ',' . $field; + } + + if (is_string($field)) { + $field = array_map('trim', explode(',', $field)); + } + + $query->setOption('field', $field); + + // 生成查询SQL + $sql = $this->builder->select($query); + + $bind = $query->getBind(); + + if (!empty($options['fetch_sql'])) { + // 获取实际执行的SQL语句 + return $this->getRealSql($sql, $bind); + } + + // 执行查询操作 + $pdo = $this->query($sql, $bind, $options['master'], true); + + if (1 == $pdo->columnCount()) { + $result = $pdo->fetchAll(PDO::FETCH_COLUMN); + } else { + $resultSet = $pdo->fetchAll(PDO::FETCH_ASSOC); + + if ('*' == $field && $key) { + $result = array_column($resultSet, null, $key); + } elseif ($resultSet) { + $fields = array_keys($resultSet[0]); + $count = count($fields); + $key1 = array_shift($fields); + $key2 = $fields ? array_shift($fields) : ''; + $key = $key ?: $key1; + + if (strpos($key, '.')) { + list($alias, $key) = explode('.', $key); + } + + if (3 == $count) { + $column = $key2; + } elseif ($count < 3) { + $column = $key1; + } else { + $column = null; + } + + $result = array_column($resultSet, $column, $key); + } else { + $result = []; + } + } + + if (isset($cache) && isset($guid)) { + // 缓存数据 + $this->cacheData($guid, $result, $cache); + } + + return $result; + } + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + protected function getExplain($sql) + { + return []; + } +} diff --git a/vendor/topthink/framework/library/think/db/connector/pgsql.sql b/vendor/topthink/framework/library/think/db/connector/pgsql.sql new file mode 100644 index 0000000..5a4442d --- /dev/null +++ b/vendor/topthink/framework/library/think/db/connector/pgsql.sql @@ -0,0 +1,153 @@ +CREATE OR REPLACE FUNCTION pgsql_type(a_type varchar) RETURNS varchar AS +$BODY$ +DECLARE + v_type varchar; +BEGIN + IF a_type='int8' THEN + v_type:='bigint'; + ELSIF a_type='int4' THEN + v_type:='integer'; + ELSIF a_type='int2' THEN + v_type:='smallint'; + ELSIF a_type='bpchar' THEN + v_type:='char'; + ELSE + v_type:=a_type; + END IF; + RETURN v_type; +END; +$BODY$ +LANGUAGE PLPGSQL; + +CREATE TYPE "public"."tablestruct" AS ( + "fields_key_name" varchar(100), + "fields_name" VARCHAR(200), + "fields_type" VARCHAR(20), + "fields_length" BIGINT, + "fields_not_null" VARCHAR(10), + "fields_default" VARCHAR(500), + "fields_comment" VARCHAR(1000) +); + +CREATE OR REPLACE FUNCTION "public"."table_msg" (a_schema_name varchar, a_table_name varchar) RETURNS SETOF "public"."tablestruct" AS +$body$ +DECLARE + v_ret tablestruct; + v_oid oid; + v_sql varchar; + v_rec RECORD; + v_key varchar; + v_conkey smallint[]; + v_pk varchar[]; + v_len smallint; + v_pos smallint := 1; +BEGIN + SELECT + pg_class.oid INTO v_oid + FROM + pg_class + INNER JOIN pg_namespace ON (pg_class.relnamespace = pg_namespace.oid AND lower(pg_namespace.nspname) = a_schema_name) + WHERE + pg_class.relname=a_table_name; + IF NOT FOUND THEN + RETURN; + END IF; + + SELECT + pg_constraint.conkey INTO v_conkey + FROM + pg_constraint + INNER JOIN pg_class ON pg_constraint.conrelid = pg_class.oid + INNER JOIN pg_attribute ON pg_attribute.attrelid = pg_class.oid + INNER JOIN pg_type ON pg_type.oid = pg_attribute.atttypid + WHERE + pg_class.relname = a_table_name + AND pg_constraint.contype = 'p'; + + v_len := array_length(v_conkey,1) + 1; + WHILE v_pos < v_len LOOP + SELECT + pg_attribute.attname INTO v_key + FROM pg_constraint + INNER JOIN pg_class ON pg_constraint.conrelid = pg_class.oid + INNER JOIN pg_attribute ON pg_attribute.attrelid = pg_class.oid AND pg_attribute.attnum = pg_constraint.conkey [ v_conkey[v_pos] ] + INNER JOIN pg_type ON pg_type.oid = pg_attribute.atttypid + WHERE pg_class.relname = a_table_name AND pg_constraint.contype = 'p'; + v_pk := array_append(v_pk,v_key); + + v_pos := v_pos + 1; + END LOOP; + + v_sql=' + SELECT + pg_attribute.attname AS fields_name, + pg_attribute.attnum AS fields_index, + pgsql_type(pg_type.typname::varchar) AS fields_type, + pg_attribute.atttypmod-4 as fields_length, + CASE WHEN pg_attribute.attnotnull THEN ''not null'' + ELSE '''' + END AS fields_not_null, + pg_attrdef.adsrc AS fields_default, + pg_description.description AS fields_comment + FROM + pg_attribute + INNER JOIN pg_class ON pg_attribute.attrelid = pg_class.oid + INNER JOIN pg_type ON pg_attribute.atttypid = pg_type.oid + LEFT OUTER JOIN pg_attrdef ON pg_attrdef.adrelid = pg_class.oid AND pg_attrdef.adnum = pg_attribute.attnum + LEFT OUTER JOIN pg_description ON pg_description.objoid = pg_class.oid AND pg_description.objsubid = pg_attribute.attnum + WHERE + pg_attribute.attnum > 0 + AND attisdropped <> ''t'' + AND pg_class.oid = ' || v_oid || ' + ORDER BY pg_attribute.attnum' ; + + FOR v_rec IN EXECUTE v_sql LOOP + v_ret.fields_name=v_rec.fields_name; + v_ret.fields_type=v_rec.fields_type; + IF v_rec.fields_length > 0 THEN + v_ret.fields_length:=v_rec.fields_length; + ELSE + v_ret.fields_length:=NULL; + END IF; + v_ret.fields_not_null=v_rec.fields_not_null; + v_ret.fields_default=v_rec.fields_default; + v_ret.fields_comment=v_rec.fields_comment; + + v_ret.fields_key_name=''; + + v_len := array_length(v_pk,1) + 1; + v_pos := 1; + WHILE v_pos < v_len LOOP + IF v_rec.fields_name = v_pk[v_pos] THEN + v_ret.fields_key_name=v_pk[v_pos]; + EXIT; + END IF; + v_pos := v_pos + 1; + END LOOP; + + RETURN NEXT v_ret; + END LOOP; + RETURN ; +END; +$body$ +LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER; + +COMMENT ON FUNCTION "public"."table_msg"(a_schema_name varchar, a_table_name varchar) +IS '获得表信息'; + +---重载一个函数 +CREATE OR REPLACE FUNCTION "public"."table_msg" (a_table_name varchar) RETURNS SETOF "public"."tablestruct" AS +$body$ +DECLARE + v_ret tablestruct; +BEGIN + FOR v_ret IN SELECT * FROM table_msg('public',a_table_name) LOOP + RETURN NEXT v_ret; + END LOOP; + RETURN; +END; +$body$ +LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER; + +COMMENT ON FUNCTION "public"."table_msg"(a_table_name varchar) +IS '获得表信息'; \ No newline at end of file diff --git a/vendor/topthink/framework/library/think/db/exception/BindParamException.php b/vendor/topthink/framework/library/think/db/exception/BindParamException.php new file mode 100644 index 0000000..dce0c7b --- /dev/null +++ b/vendor/topthink/framework/library/think/db/exception/BindParamException.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\exception; + +use think\exception\DbException; + +/** + * PDO参数绑定异常 + */ +class BindParamException extends DbException +{ + + /** + * BindParamException constructor. + * @access public + * @param string $message + * @param array $config + * @param string $sql + * @param array $bind + * @param int $code + */ + public function __construct($message, $config, $sql, $bind, $code = 10502) + { + $this->setData('Bind Param', $bind); + parent::__construct($message, $config, $sql, $code); + } +} diff --git a/vendor/topthink/framework/library/think/db/exception/DataNotFoundException.php b/vendor/topthink/framework/library/think/db/exception/DataNotFoundException.php new file mode 100644 index 0000000..883e333 --- /dev/null +++ b/vendor/topthink/framework/library/think/db/exception/DataNotFoundException.php @@ -0,0 +1,44 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\exception; + +use think\exception\DbException; + +class DataNotFoundException extends DbException +{ + protected $table; + + /** + * DbException constructor. + * @access public + * @param string $message + * @param string $table + * @param array $config + */ + public function __construct($message, $table = '', array $config = []) + { + $this->message = $message; + $this->table = $table; + + $this->setData('Database Config', $config); + } + + /** + * 获取数据表名 + * @access public + * @return string + */ + public function getTable() + { + return $this->table; + } +} diff --git a/vendor/topthink/framework/library/think/db/exception/ModelNotFoundException.php b/vendor/topthink/framework/library/think/db/exception/ModelNotFoundException.php new file mode 100644 index 0000000..ae52baf --- /dev/null +++ b/vendor/topthink/framework/library/think/db/exception/ModelNotFoundException.php @@ -0,0 +1,45 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\exception; + +use think\exception\DbException; + +class ModelNotFoundException extends DbException +{ + protected $model; + + /** + * 构造方法 + * @access public + * @param string $message + * @param string $model + * @param array $config + */ + public function __construct($message, $model = '', array $config = []) + { + $this->message = $message; + $this->model = $model; + + $this->setData('Database Config', $config); + } + + /** + * 获取模型类名 + * @access public + * @return string + */ + public function getModel() + { + return $this->model; + } + +} diff --git a/vendor/topthink/framework/library/think/debug/Console.php b/vendor/topthink/framework/library/think/debug/Console.php new file mode 100644 index 0000000..5cbaa0f --- /dev/null +++ b/vendor/topthink/framework/library/think/debug/Console.php @@ -0,0 +1,156 @@ + +// +---------------------------------------------------------------------- + +namespace think\debug; + +use think\Container; +use think\Db; +use think\Response; + +/** + * 浏览器调试输出 + */ +class Console +{ + protected $config = [ + 'tabs' => ['base' => '基本', 'file' => '文件', 'info' => '流程', 'notice|error' => '错误', 'sql' => 'SQL', 'debug|log' => '调试'], + ]; + + // 实例化并传入参数 + public function __construct($config = []) + { + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } + } + + /** + * 调试输出接口 + * @access public + * @param Response $response Response对象 + * @param array $log 日志信息 + * @return bool + */ + public function output(Response $response, array $log = []) + { + $request = Container::get('request'); + $contentType = $response->getHeader('Content-Type'); + $accept = $request->header('accept'); + if (strpos($accept, 'application/json') === 0 || $request->isAjax()) { + return false; + } elseif (!empty($contentType) && strpos($contentType, 'html') === false) { + return false; + } + // 获取基本信息 + $runtime = number_format(microtime(true) - Container::get('app')->getBeginTime(), 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + $mem = number_format((memory_get_usage() - Container::get('app')->getBeginMem()) / 1024, 2); + + if ($request->host()) { + $uri = $request->protocol() . ' ' . $request->method() . ' : ' . $request->url(true); + } else { + $uri = 'cmd:' . implode(' ', $_SERVER['argv']); + } + + // 页面Trace信息 + $base = [ + '请求信息' => date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']) . ' ' . $uri, + '运行时间' => number_format($runtime, 6) . 's [ 吞吐率:' . $reqs . 'req/s ] 内存消耗:' . $mem . 'kb 文件加载:' . count(get_included_files()), + '查询信息' => Db::$queryTimes . ' queries ' . Db::$executeTimes . ' writes ', + '缓存信息' => Container::get('cache')->getReadTimes() . ' reads,' . Container::get('cache')->getWriteTimes() . ' writes', + ]; + + if (session_id()) { + $base['会话信息'] = 'SESSION_ID=' . session_id(); + } + + $info = Container::get('debug')->getFile(true); + + // 页面Trace信息 + $trace = []; + foreach ($this->config['tabs'] as $name => $title) { + $name = strtolower($name); + switch ($name) { + case 'base': // 基本信息 + $trace[$title] = $base; + break; + case 'file': // 文件信息 + $trace[$title] = $info; + break; + default: // 调试信息 + if (strpos($name, '|')) { + // 多组信息 + $names = explode('|', $name); + $result = []; + foreach ($names as $name) { + $result = array_merge($result, isset($log[$name]) ? $log[$name] : []); + } + $trace[$title] = $result; + } else { + $trace[$title] = isset($log[$name]) ? $log[$name] : ''; + } + } + } + + //输出到控制台 + $lines = ''; + foreach ($trace as $type => $msg) { + $lines .= $this->console($type, $msg); + } + $js = << +{$lines} + +JS; + return $js; + } + + protected function console($type, $msg) + { + $type = strtolower($type); + $trace_tabs = array_values($this->config['tabs']); + $line[] = ($type == $trace_tabs[0] || '调试' == $type || '错误' == $type) + ? "console.group('{$type}');" + : "console.groupCollapsed('{$type}');"; + + foreach ((array) $msg as $key => $m) { + switch ($type) { + case '调试': + $var_type = gettype($m); + if (in_array($var_type, ['array', 'string'])) { + $line[] = "console.log(" . json_encode($m) . ");"; + } else { + $line[] = "console.log(" . json_encode(var_export($m, 1)) . ");"; + } + break; + case '错误': + $msg = str_replace("\n", '\n', addslashes(is_scalar($m) ? $m : json_encode($m))); + $style = 'color:#F4006B;font-size:14px;'; + $line[] = "console.error(\"%c{$msg}\", \"{$style}\");"; + break; + case 'sql': + $msg = str_replace("\n", '\n', addslashes($m)); + $style = "color:#009bb4;"; + $line[] = "console.log(\"%c{$msg}\", \"{$style}\");"; + break; + default: + $m = is_string($key) ? $key . ' ' . $m : $key + 1 . ' ' . $m; + $msg = json_encode($m); + $line[] = "console.log({$msg});"; + break; + } + } + $line[] = "console.groupEnd();"; + return implode(PHP_EOL, $line); + } + +} diff --git a/vendor/topthink/framework/library/think/debug/Html.php b/vendor/topthink/framework/library/think/debug/Html.php new file mode 100644 index 0000000..a123762 --- /dev/null +++ b/vendor/topthink/framework/library/think/debug/Html.php @@ -0,0 +1,106 @@ + +// +---------------------------------------------------------------------- + +namespace think\debug; + +use think\Container; +use think\Db; +use think\Response; + +/** + * 页面Trace调试 + */ +class Html +{ + protected $config = [ + 'file' => '', + 'tabs' => ['base' => '基本', 'file' => '文件', 'info' => '流程', 'notice|error' => '错误', 'sql' => 'SQL', 'debug|log' => '调试'], + ]; + + // 实例化并传入参数 + public function __construct(array $config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 调试输出接口 + * @access public + * @param Response $response Response对象 + * @param array $log 日志信息 + * @return bool + */ + public function output(Response $response, array $log = []) + { + $request = Container::get('request'); + $contentType = $response->getHeader('Content-Type'); + $accept = $request->header('accept'); + if (strpos($accept, 'application/json') === 0 || $request->isAjax()) { + return false; + } elseif (!empty($contentType) && strpos($contentType, 'html') === false) { + return false; + } + // 获取基本信息 + $runtime = number_format(microtime(true) - Container::get('app')->getBeginTime(), 10, '.', ''); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + $mem = number_format((memory_get_usage() - Container::get('app')->getBeginMem()) / 1024, 2); + + // 页面Trace信息 + if ($request->host()) { + $uri = $request->protocol() . ' ' . $request->method() . ' : ' . $request->url(true); + } else { + $uri = 'cmd:' . implode(' ', $_SERVER['argv']); + } + $base = [ + '请求信息' => date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']) . ' ' . $uri, + '运行时间' => number_format($runtime, 6) . 's [ 吞吐率:' . $reqs . 'req/s ] 内存消耗:' . $mem . 'kb 文件加载:' . count(get_included_files()), + '查询信息' => Db::$queryTimes . ' queries ' . Db::$executeTimes . ' writes ', + '缓存信息' => Container::get('cache')->getReadTimes() . ' reads,' . Container::get('cache')->getWriteTimes() . ' writes', + ]; + + if (session_id()) { + $base['会话信息'] = 'SESSION_ID=' . session_id(); + } + + $info = Container::get('debug')->getFile(true); + + // 页面Trace信息 + $trace = []; + foreach ($this->config['tabs'] as $name => $title) { + $name = strtolower($name); + switch ($name) { + case 'base': // 基本信息 + $trace[$title] = $base; + break; + case 'file': // 文件信息 + $trace[$title] = $info; + break; + default: // 调试信息 + if (strpos($name, '|')) { + // 多组信息 + $names = explode('|', $name); + $result = []; + foreach ($names as $name) { + $result = array_merge($result, isset($log[$name]) ? $log[$name] : []); + } + $trace[$title] = $result; + } else { + $trace[$title] = isset($log[$name]) ? $log[$name] : ''; + } + } + } + // 调用Trace页面模板 + ob_start(); + include $this->config['file']; + return ob_get_clean(); + } + +} diff --git a/vendor/topthink/framework/library/think/exception/ClassNotFoundException.php b/vendor/topthink/framework/library/think/exception/ClassNotFoundException.php new file mode 100644 index 0000000..eb22e73 --- /dev/null +++ b/vendor/topthink/framework/library/think/exception/ClassNotFoundException.php @@ -0,0 +1,32 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class ClassNotFoundException extends \RuntimeException +{ + protected $class; + public function __construct($message, $class = '') + { + $this->message = $message; + $this->class = $class; + } + + /** + * 获取类名 + * @access public + * @return string + */ + public function getClass() + { + return $this->class; + } +} diff --git a/vendor/topthink/framework/library/think/exception/DbException.php b/vendor/topthink/framework/library/think/exception/DbException.php new file mode 100644 index 0000000..6baafb5 --- /dev/null +++ b/vendor/topthink/framework/library/think/exception/DbException.php @@ -0,0 +1,44 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use think\Exception; + +/** + * Database相关异常处理类 + */ +class DbException extends Exception +{ + /** + * DbException constructor. + * @access public + * @param string $message + * @param array $config + * @param string $sql + * @param int $code + */ + public function __construct($message, array $config = [], $sql = '', $code = 10500) + { + $this->message = $message; + $this->code = $code; + + $this->setData('Database Status', [ + 'Error Code' => $code, + 'Error Message' => $message, + 'Error SQL' => $sql, + ]); + + unset($config['username'], $config['password']); + $this->setData('Database Config', $config); + } + +} diff --git a/vendor/topthink/framework/library/think/exception/ErrorException.php b/vendor/topthink/framework/library/think/exception/ErrorException.php new file mode 100644 index 0000000..3143b8f --- /dev/null +++ b/vendor/topthink/framework/library/think/exception/ErrorException.php @@ -0,0 +1,56 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use think\Exception; + +/** + * ThinkPHP错误异常 + * 主要用于封装 set_error_handler 和 register_shutdown_function 得到的错误 + * 除开从 think\Exception 继承的功能 + * 其他和PHP系统\ErrorException功能基本一样 + */ +class ErrorException extends Exception +{ + /** + * 用于保存错误级别 + * @var integer + */ + protected $severity; + + /** + * 错误异常构造函数 + * @access public + * @param integer $severity 错误级别 + * @param string $message 错误详细信息 + * @param string $file 出错文件路径 + * @param integer $line 出错行号 + */ + public function __construct($severity, $message, $file, $line) + { + $this->severity = $severity; + $this->message = $message; + $this->file = $file; + $this->line = $line; + $this->code = 0; + } + + /** + * 获取错误级别 + * @access public + * @return integer 错误级别 + */ + final public function getSeverity() + { + return $this->severity; + } +} diff --git a/vendor/topthink/framework/library/think/exception/Handle.php b/vendor/topthink/framework/library/think/exception/Handle.php new file mode 100644 index 0000000..02c85ec --- /dev/null +++ b/vendor/topthink/framework/library/think/exception/Handle.php @@ -0,0 +1,306 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use Exception; +use think\console\Output; +use think\Container; +use think\Response; + +class Handle +{ + protected $render; + protected $ignoreReport = [ + '\\think\\exception\\HttpException', + ]; + + public function setRender($render) + { + $this->render = $render; + } + + /** + * Report or log an exception. + * + * @access public + * @param \Exception $exception + * @return void + */ + public function report(Exception $exception) + { + if (!$this->isIgnoreReport($exception)) { + // 收集异常数据 + if (Container::get('app')->isDebug()) { + $data = [ + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'message' => $this->getMessage($exception), + 'code' => $this->getCode($exception), + ]; + $log = "[{$data['code']}]{$data['message']}[{$data['file']}:{$data['line']}]"; + } else { + $data = [ + 'code' => $this->getCode($exception), + 'message' => $this->getMessage($exception), + ]; + $log = "[{$data['code']}]{$data['message']}"; + } + + if (Container::get('app')->config('log.record_trace')) { + $log .= "\r\n" . $exception->getTraceAsString(); + } + + Container::get('log')->record($log, 'error'); + } + } + + protected function isIgnoreReport(Exception $exception) + { + foreach ($this->ignoreReport as $class) { + if ($exception instanceof $class) { + return true; + } + } + + return false; + } + + /** + * Render an exception into an HTTP response. + * + * @access public + * @param \Exception $e + * @return Response + */ + 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 { + return $this->convertExceptionToResponse($e); + } + } + + /** + * @access public + * @param Output $output + * @param Exception $e + */ + public function renderForConsole(Output $output, Exception $e) + { + if (Container::get('app')->isDebug()) { + $output->setVerbosity(Output::VERBOSITY_DEBUG); + } + + $output->renderException($e); + } + + /** + * @access protected + * @param HttpException $e + * @return Response + */ + protected function renderHttpException(HttpException $e) + { + $status = $e->getStatusCode(); + $template = Container::get('app')->config('http_exception_template'); + + if (!Container::get('app')->isDebug() && !empty($template[$status])) { + return Response::create($template[$status], 'view', $status)->assign(['e' => $e]); + } else { + return $this->convertExceptionToResponse($e); + } + } + + /** + * @access protected + * @param Exception $exception + * @return Response + */ + protected function convertExceptionToResponse(Exception $exception) + { + // 收集异常数据 + if (Container::get('app')->isDebug()) { + // 调试模式,获取详细的错误信息 + $data = [ + 'name' => get_class($exception), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'message' => $this->getMessage($exception), + 'trace' => $exception->getTrace(), + 'code' => $this->getCode($exception), + 'source' => $this->getSourceCode($exception), + 'datas' => $this->getExtendData($exception), + 'tables' => [ + 'GET Data' => $_GET, + 'POST Data' => $_POST, + 'Files' => $_FILES, + 'Cookies' => $_COOKIE, + 'Session' => isset($_SESSION) ? $_SESSION : [], + 'Server/Request Data' => $_SERVER, + 'Environment Variables' => $_ENV, + 'ThinkPHP Constants' => $this->getConst(), + ], + ]; + } else { + // 部署模式仅显示 Code 和 Message + $data = [ + 'code' => $this->getCode($exception), + 'message' => $this->getMessage($exception), + ]; + + if (!Container::get('app')->config('show_error_msg')) { + // 不显示详细错误信息 + $data['message'] = Container::get('app')->config('error_message'); + } + } + + //保留一层 + while (ob_get_level() > 1) { + ob_end_clean(); + } + + $data['echo'] = ob_get_clean(); + + ob_start(); + extract($data); + include Container::get('app')->config('exception_tmpl'); + + // 获取并清空缓存 + $content = ob_get_clean(); + $response = Response::create($content, 'html'); + + if ($exception instanceof HttpException) { + $statusCode = $exception->getStatusCode(); + $response->header($exception->getHeaders()); + } + + if (!isset($statusCode)) { + $statusCode = 500; + } + $response->code($statusCode); + + return $response; + } + + /** + * 获取错误编码 + * ErrorException则使用错误级别作为错误编码 + * @access protected + * @param \Exception $exception + * @return integer 错误编码 + */ + protected function getCode(Exception $exception) + { + $code = $exception->getCode(); + + if (!$code && $exception instanceof ErrorException) { + $code = $exception->getSeverity(); + } + + return $code; + } + + /** + * 获取错误信息 + * ErrorException则使用错误级别作为错误编码 + * @access protected + * @param \Exception $exception + * @return string 错误信息 + */ + protected function getMessage(Exception $exception) + { + $message = $exception->getMessage(); + + if (PHP_SAPI == 'cli') { + return $message; + } + + $lang = Container::get('lang'); + + if (strpos($message, ':')) { + $name = strstr($message, ':', true); + $message = $lang->has($name) ? $lang->get($name) . strstr($message, ':') : $message; + } elseif (strpos($message, ',')) { + $name = strstr($message, ',', true); + $message = $lang->has($name) ? $lang->get($name) . ':' . substr(strstr($message, ','), 1) : $message; + } elseif ($lang->has($message)) { + $message = $lang->get($message); + } + + return $message; + } + + /** + * 获取出错文件内容 + * 获取错误的前9行和后9行 + * @access protected + * @param \Exception $exception + * @return array 错误文件内容 + */ + protected function getSourceCode(Exception $exception) + { + // 读取前9行和后9行 + $line = $exception->getLine(); + $first = ($line - 9 > 0) ? $line - 9 : 1; + + try { + $contents = file($exception->getFile()); + $source = [ + 'first' => $first, + 'source' => array_slice($contents, $first - 1, 19), + ]; + } catch (Exception $e) { + $source = []; + } + + return $source; + } + + /** + * 获取异常扩展信息 + * 用于非调试模式html返回类型显示 + * @access protected + * @param \Exception $exception + * @return array 异常类定义的扩展数据 + */ + protected function getExtendData(Exception $exception) + { + $data = []; + + if ($exception instanceof \think\Exception) { + $data = $exception->getData(); + } + + return $data; + } + + /** + * 获取常量列表 + * @access private + * @return array 常量列表 + */ + private static function getConst() + { + $const = get_defined_constants(true); + + return isset($const['user']) ? $const['user'] : []; + } +} diff --git a/vendor/topthink/framework/library/think/exception/HttpException.php b/vendor/topthink/framework/library/think/exception/HttpException.php new file mode 100644 index 0000000..01a27fc --- /dev/null +++ b/vendor/topthink/framework/library/think/exception/HttpException.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class HttpException extends \RuntimeException +{ + private $statusCode; + private $headers; + + public function __construct($statusCode, $message = null, \Exception $previous = null, array $headers = [], $code = 0) + { + $this->statusCode = $statusCode; + $this->headers = $headers; + + parent::__construct($message, $code, $previous); + } + + public function getStatusCode() + { + return $this->statusCode; + } + + public function getHeaders() + { + return $this->headers; + } +} diff --git a/vendor/topthink/framework/library/think/exception/HttpResponseException.php b/vendor/topthink/framework/library/think/exception/HttpResponseException.php new file mode 100644 index 0000000..5297286 --- /dev/null +++ b/vendor/topthink/framework/library/think/exception/HttpResponseException.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use think\Response; + +class HttpResponseException extends \RuntimeException +{ + /** + * @var Response + */ + protected $response; + + public function __construct(Response $response) + { + $this->response = $response; + } + + public function getResponse() + { + return $this->response; + } + +} diff --git a/vendor/topthink/framework/library/think/exception/PDOException.php b/vendor/topthink/framework/library/think/exception/PDOException.php new file mode 100644 index 0000000..25240b6 --- /dev/null +++ b/vendor/topthink/framework/library/think/exception/PDOException.php @@ -0,0 +1,40 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +/** + * PDO异常处理类 + * 重新封装了系统的\PDOException类 + */ +class PDOException extends DbException +{ + /** + * PDOException constructor. + * @access public + * @param \PDOException $exception + * @param array $config + * @param string $sql + * @param int $code + */ + public function __construct(\PDOException $exception, array $config, $sql, $code = 10501) + { + $error = $exception->errorInfo; + + $this->setData('PDO Error Info', [ + 'SQLSTATE' => $error[0], + 'Driver Error Code' => isset($error[1]) ? $error[1] : 0, + 'Driver Error Message' => isset($error[2]) ? $error[2] : '', + ]); + + parent::__construct($exception->getMessage(), $config, $sql, $code); + } +} diff --git a/vendor/topthink/framework/library/think/exception/RouteNotFoundException.php b/vendor/topthink/framework/library/think/exception/RouteNotFoundException.php new file mode 100644 index 0000000..d22e3a6 --- /dev/null +++ b/vendor/topthink/framework/library/think/exception/RouteNotFoundException.php @@ -0,0 +1,22 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class RouteNotFoundException extends HttpException +{ + + public function __construct() + { + parent::__construct(404, 'Route Not Found'); + } + +} diff --git a/vendor/topthink/framework/library/think/exception/TemplateNotFoundException.php b/vendor/topthink/framework/library/think/exception/TemplateNotFoundException.php new file mode 100644 index 0000000..4202069 --- /dev/null +++ b/vendor/topthink/framework/library/think/exception/TemplateNotFoundException.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class TemplateNotFoundException extends \RuntimeException +{ + protected $template; + + public function __construct($message, $template = '') + { + $this->message = $message; + $this->template = $template; + } + + /** + * 获取模板文件 + * @access public + * @return string + */ + public function getTemplate() + { + return $this->template; + } +} diff --git a/vendor/topthink/framework/library/think/exception/ThrowableError.php b/vendor/topthink/framework/library/think/exception/ThrowableError.php new file mode 100644 index 0000000..87b6b9d --- /dev/null +++ b/vendor/topthink/framework/library/think/exception/ThrowableError.php @@ -0,0 +1,47 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class ThrowableError extends \ErrorException +{ + public function __construct(\Throwable $e) + { + + if ($e instanceof \ParseError) { + $message = 'Parse error: ' . $e->getMessage(); + $severity = E_PARSE; + } elseif ($e instanceof \TypeError) { + $message = 'Type error: ' . $e->getMessage(); + $severity = E_RECOVERABLE_ERROR; + } else { + $message = 'Fatal error: ' . $e->getMessage(); + $severity = E_ERROR; + } + + parent::__construct( + $message, + $e->getCode(), + $severity, + $e->getFile(), + $e->getLine() + ); + + $this->setTrace($e->getTrace()); + } + + protected function setTrace($trace) + { + $traceReflector = new \ReflectionProperty('Exception', 'trace'); + $traceReflector->setAccessible(true); + $traceReflector->setValue($this, $trace); + } +} diff --git a/vendor/topthink/framework/library/think/exception/ValidateException.php b/vendor/topthink/framework/library/think/exception/ValidateException.php new file mode 100644 index 0000000..81ddfe2 --- /dev/null +++ b/vendor/topthink/framework/library/think/exception/ValidateException.php @@ -0,0 +1,34 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class ValidateException extends \RuntimeException +{ + protected $error; + + public function __construct($error, $code = 0) + { + $this->error = $error; + $this->message = is_array($error) ? implode(PHP_EOL, $error) : $error; + $this->code = $code; + } + + /** + * 获取验证错误信息 + * @access public + * @return array|string + */ + public function getError() + { + return $this->error; + } +} diff --git a/vendor/topthink/framework/library/think/facade/App.php b/vendor/topthink/framework/library/think/facade/App.php new file mode 100644 index 0000000..b375aa0 --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/App.php @@ -0,0 +1,63 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\App + * @mixin \think\App + * @method \think\App bind(string $bind) static 绑定模块或者控制器 + * @method void initialize() static 初始化应用 + * @method void init(string $module='') static 初始化模块 + * @method \think\Response run() static 执行应用 + * @method \think\App dispatch(\think\route\Dispatch $dispatch) static 设置当前请求的调度信息 + * @method void log(mixed $log, string $type = 'info') static 记录调试信息 + * @method mixed config(string $name='') static 获取配置参数 + * @method \think\route\Dispatch routeCheck() static URL路由检测(根据PATH_INFO) + * @method \think\App routeMust(bool $must = false) static 设置应用的路由检测机制 + * @method \think\Model model(string $name = '', string $layer = 'model', bool $appendSuffix = false, string $common = 'common') static 实例化模型 + * @method object controller(string $name, string $layer = 'controller', bool $appendSuffix = false, string $empty = '') static 实例化控制器 + * @method \think\Validate validate(string $name = '', string $layer = 'validate', bool $appendSuffix = false, string $common = 'common') static 实例化验证器类 + * @method \think\db\Query db(mixed $config = [], mixed $name = false) static 数据库初始化 + * @method mixed action(string $url, $vars = [], $layer = 'controller', $appendSuffix = false) static 调用模块的操作方法 + * @method string parseClass(string $module, string $layer, string $name, bool $appendSuffix = false) static 解析应用类的类名 + * @method string version() static 获取框架版本 + * @method bool isDebug() static 是否为调试模式 + * @method string getModulePath() static 获取当前模块路径 + * @method void setModulePath(string $path) static 设置当前模块路径 + * @method string getRootPath() static 获取应用根目录 + * @method string getAppPath() static 获取应用类库目录 + * @method string getRuntimePath() static 获取应用运行时目录 + * @method string getThinkPath() static 获取核心框架目录 + * @method string getRoutePath() static 获取路由目录 + * @method string getConfigPath() static 获取应用配置目录 + * @method string getConfigExt() static 获取配置后缀 + * @method string setNamespace(string $namespace) static 设置应用类库命名空间 + * @method string getNamespace() static 获取应用类库命名空间 + * @method string getSuffix() static 是否启用类库后缀 + * @method float getBeginTime() static 获取应用开启时间 + * @method integer getBeginMem() static 获取应用初始内存占用 + * @method \think\Container container() static 获取容器实例 + */ +class App extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'app'; + } +} diff --git a/vendor/topthink/framework/library/think/facade/Build.php b/vendor/topthink/framework/library/think/facade/Build.php new file mode 100644 index 0000000..c051bea --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/Build.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Build + * @mixin \think\Build + * @method void run(array $build = [], string $namespace = 'app', bool $suffix = false) static 根据传入的build资料创建目录和文件 + * @method void module(string $module = '', array $list = [], string $namespace = 'app', bool $suffix = false) static 创建模块 + */ +class Build extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'build'; + } +} diff --git a/vendor/topthink/framework/library/think/facade/Cache.php b/vendor/topthink/framework/library/think/facade/Cache.php new file mode 100644 index 0000000..9743486 --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/Cache.php @@ -0,0 +1,45 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Cache + * @mixin \think\Cache + * @method \think\cache\Driver connect(array $options = [], mixed $name = false) static 连接缓存 + * @method \think\cache\Driver init(array $options = []) static 初始化缓存 + * @method \think\cache\Driver store(string $name = '') static 切换缓存类型 + * @method bool has(string $name) static 判断缓存是否存在 + * @method mixed get(string $name, mixed $default = false) static 读取缓存 + * @method mixed pull(string $name) static 读取缓存并删除 + * @method mixed set(string $name, mixed $value, int $expire = null) static 设置缓存 + * @method mixed remember(string $name, mixed $value, int $expire = null) static 如果不存在则写入缓存 + * @method mixed inc(string $name, int $step = 1) static 自增缓存(针对数值缓存) + * @method mixed dec(string $name, int $step = 1) static 自减缓存(针对数值缓存) + * @method bool rm(string $name) static 删除缓存 + * @method bool clear(string $tag = null) static 清除缓存 + * @method mixed tag(string $name, mixed $keys = null, bool $overlay = false) static 缓存标签 + * @method object handler() static 返回句柄对象,可执行其它高级方法 + */ +class Cache extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'cache'; + } +} diff --git a/vendor/topthink/framework/library/think/facade/Config.php b/vendor/topthink/framework/library/think/facade/Config.php new file mode 100644 index 0000000..824d2b6 --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/Config.php @@ -0,0 +1,39 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Config + * @mixin \think\Config + * @method array load(string $file, string $name = '') static 加载配置文件 + * @method bool has(string $name) static 检测配置是否存在 + * @method array pull(string $name) static 获取一级配置参数 + * @method mixed get(string $name,mixed $default = null) static 获取配置参数 + * @method array set(mixed $name, mixed $value = null) static 设置配置参数 + * @method array reset(string $name ='') static 重置配置参数 + * @method void remove(string $name = '') static 移除配置 + * @method void setYaconf(mixed $yaconf) static 设置开启Yaconf 或者指定配置文件名 + */ +class Config extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'config'; + } +} diff --git a/vendor/topthink/framework/library/think/facade/Cookie.php b/vendor/topthink/framework/library/think/facade/Cookie.php new file mode 100644 index 0000000..4d7cea2 --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/Cookie.php @@ -0,0 +1,39 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Cookie + * @mixin \think\Cookie + * @method void init(array $config = []) static 初始化 + * @method bool has(string $name,string $prefix = null) static 判断Cookie数据 + * @method mixed prefix(string $prefix = '') static 设置或者获取cookie作用域(前缀) + * @method mixed get(string $name,string $prefix = null) static Cookie获取 + * @method mixed set(string $name, mixed $value = null, mixed $option = null) static 设置Cookie + * @method void forever(string $name, mixed $value = null, mixed $option = null) static 永久保存Cookie数据 + * @method void delete(string $name, string $prefix = null) static Cookie删除 + * @method void clear($prefix = null) static Cookie清空 + */ +class Cookie extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'cookie'; + } +} diff --git a/vendor/topthink/framework/library/think/facade/Debug.php b/vendor/topthink/framework/library/think/facade/Debug.php new file mode 100644 index 0000000..df20086 --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/Debug.php @@ -0,0 +1,40 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Debug + * @mixin \think\Debug + * @method void remark(string $name, mixed $value = '') static 记录时间(微秒)和内存使用情况 + * @method int getRangeTime(string $start, string $end, mixed $dec = 6) static 统计某个区间的时间(微秒)使用情况 + * @method int getUseTime(int $dec = 6) static 统计从开始到统计时的时间(微秒)使用情况 + * @method string getThroughputRate(string $start, string $end, mixed $dec = 6) static 获取当前访问的吞吐率情况 + * @method string getRangeMem(string $start, string $end, mixed $dec = 2) static 记录区间的内存使用情况 + * @method int getUseMem(int $dec = 2) static 统计从开始到统计时的内存使用情况 + * @method string getMemPeak(string $start, string $end, mixed $dec = 2) static 统计区间的内存峰值情况 + * @method mixed getFile(bool $detail = false) static 获取文件加载信息 + * @method mixed dump(mixed $var, bool $echo = true, string $label = null, int $flags = ENT_SUBSTITUTE) static 浏览器友好的变量输出 + */ +class Debug extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'debug'; + } +} diff --git a/vendor/topthink/framework/library/think/facade/Env.php b/vendor/topthink/framework/library/think/facade/Env.php new file mode 100644 index 0000000..5d04724 --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/Env.php @@ -0,0 +1,34 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Env + * @mixin \think\Env + * @method void load(string $file) static 读取环境变量定义文件 + * @method mixed get(string $name = null, mixed $default = null) static 获取环境变量值 + * @method void set(mixed $env, string $value = null) static 设置环境变量值 + */ +class Env extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'env'; + } +} diff --git a/vendor/topthink/framework/library/think/facade/Hook.php b/vendor/topthink/framework/library/think/facade/Hook.php new file mode 100644 index 0000000..e9e1208 --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/Hook.php @@ -0,0 +1,37 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Hook + * @mixin \think\Hook + * @method \think\Hook alias(mixed $name, mixed $behavior = null) static 指定行为标识 + * @method void add(string $tag, mixed $behavior, bool $first = false) static 动态添加行为扩展到某个标签 + * @method void import(array $tags, bool $recursive = true) static 批量导入插件 + * @method array get(string $tag = '') static 获取插件信息 + * @method mixed listen(string $tag, mixed $params = null, bool $once = false) static 监听标签的行为 + * @method mixed exec(mixed $class, mixed $params = null) static 执行行为 + */ +class Hook extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'hook'; + } +} diff --git a/vendor/topthink/framework/library/think/facade/Lang.php b/vendor/topthink/framework/library/think/facade/Lang.php new file mode 100644 index 0000000..56c4777 --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/Lang.php @@ -0,0 +1,41 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Lang + * @mixin \think\Lang + * @method mixed range($range = '') static 设定当前的语言 + * @method mixed set(mixed $name, string $value = null, string $range = '') static 设置语言定义 + * @method array load(mixed $file, string $range = '') static 加载语言定义 + * @method mixed get(string $name = null, array $vars = [], string $range = '') static 获取语言定义 + * @method mixed has(string $name, string $range = '') static 获取语言定义 + * @method string detect() static 自动侦测设置获取语言选择 + * @method void saveToCookie(string $lang = null) static 设置当前语言到Cookie + * @method void setLangDetectVar(string $var) static 设置语言自动侦测的变量 + * @method void setLangCookieVar(string $var) static 设置语言的cookie保存变量 + * @method void setAllowLangList(array $list) static 设置允许的语言列表 + */ +class Lang extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'lang'; + } +} diff --git a/vendor/topthink/framework/library/think/facade/Log.php b/vendor/topthink/framework/library/think/facade/Log.php new file mode 100644 index 0000000..ae627a5 --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/Log.php @@ -0,0 +1,50 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Log + * @mixin \think\Log + * @method \think\Log init(array $config = []) static 日志初始化 + * @method mixed getLog(string $type = '') static 获取日志信息 + * @method \think\Log record(mixed $msg, string $type = 'info', array $context = []) static 记录日志信息 + * @method \think\Log clear() static 清空日志信息 + * @method \think\Log key(string $key) static 当前日志记录的授权key + * @method \think\Log close() static 关闭本次请求日志写入 + * @method bool check(array $config) static 检查日志写入权限 + * @method bool save() static 保存调试信息 + * @method void write(mixed $msg, string $type = 'info', bool $force = false) static 实时写入日志信息 + * @method void log(string $level,mixed $message, array $context = []) static 记录日志信息 + * @method void emergency(mixed $message, array $context = []) static 记录emergency信息 + * @method void alert(mixed $message, array $context = []) static 记录alert信息 + * @method void critical(mixed $message, array $context = []) static 记录critical信息 + * @method void error(mixed $message, array $context = []) static 记录error信息 + * @method void warning(mixed $message, array $context = []) static 记录warning信息 + * @method void notice(mixed $message, array $context = []) static 记录notice信息 + * @method void info(mixed $message, array $context = []) static 记录info信息 + * @method void debug(mixed $message, array $context = []) static 记录debug信息 + * @method void sql(mixed $message, array $context = []) static 记录sql信息 + */ +class Log extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'log'; + } +} diff --git a/vendor/topthink/framework/library/think/facade/Middleware.php b/vendor/topthink/framework/library/think/facade/Middleware.php new file mode 100644 index 0000000..5e4cac7 --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/Middleware.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Middleware + * @mixin \think\Middleware + * @method void import(array $middlewares = []) static 批量设置中间件 + * @method void add(mixed $middleware) static 添加中间件到队列 + * @method void unshift(mixed $middleware) static 添加中间件到队列开头 + * @method array all() static 获取中间件队列 + * @method \think\Response dispatch(\think\Request $request) static 执行中间件调度 + */ +class Middleware extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'middleware'; + } +} diff --git a/vendor/topthink/framework/library/think/facade/Request.php b/vendor/topthink/framework/library/think/facade/Request.php new file mode 100644 index 0000000..0989253 --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/Request.php @@ -0,0 +1,97 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Request + * @mixin \think\Request + * @method void hook(mixed $method, mixed $callback = null) static Hook 方法注入 + * @method \think\Request create(string $uri, string $method = 'GET', array $params = [], array $cookie = [], array $files = [], array $server = [], string $content = null) static 创建一个URL请求 + * @method mixed domain(bool $port = false) static 获取当前包含协议、端口的域名 + * @method mixed url(bool $domain = false) static 获取当前完整URL + * @method mixed baseUrl(bool $domain = false) static 获取当前URL + * @method mixed baseFile(bool $domain = false) static 获取当前执行的文件 + * @method mixed root(bool $domain = false) static 获取URL访问根地址 + * @method string rootUrl() static 获取URL访问根目录 + * @method string pathinfo() static 获取当前请求URL的pathinfo信息(含URL后缀) + * @method string path() static 获取当前请求URL的pathinfo信息(不含URL后缀) + * @method string ext() static 当前URL的访问后缀 + * @method float time(bool $float = false) static 获取当前请求的时间 + * @method mixed type() static 当前请求的资源类型 + * @method void mimeType(mixed $type, string $val = '') static 设置资源类型 + * @method string method(bool $method = false) static 当前的请求类型 + * @method bool isGet() static 是否为GET请求 + * @method bool isPost() static 是否为POST请求 + * @method bool isPut() static 是否为PUT请求 + * @method bool isDelete() static 是否为DELTE请求 + * @method bool isHead() static 是否为HEAD请求 + * @method bool isPatch() static 是否为PATCH请求 + * @method bool isOptions() static 是否为OPTIONS请求 + * @method bool isCli() static 是否为cli + * @method bool isCgi() static 是否为cgi + * @method mixed param(string $name = '', mixed $default = null, mixed $filter = '') static 获取当前请求的参数 + * @method mixed route(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取路由参数 + * @method mixed get(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取GET参数 + * @method mixed post(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取POST参数 + * @method mixed put(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取PUT参数 + * @method mixed delete(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取DELETE参数 + * @method mixed patch(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取PATCH参数 + * @method mixed request(string $name = '', mixed $default = null, mixed $filter = '') static 获取request变量 + * @method mixed session(string $name = '', mixed $default = null, mixed $filter = '') static 获取session数据 + * @method mixed cookie(string $name = '', mixed $default = null, mixed $filter = '') static 获取cookie参数 + * @method mixed server(string $name = '', mixed $default = null, mixed $filter = '') static 获取server参数 + * @method mixed env(string $name = '', mixed $default = null, mixed $filter = '') static 获取环境变量 + * @method mixed file(string $name = '') static 获取上传的文件信息 + * @method mixed header(string $name = '', mixed $default = null) static 设置或者获取当前的Header + * @method mixed input(array $data,mixed $name = '', mixed $default = null, mixed $filter = '') static 获取变量 支持过滤和默认值 + * @method mixed filter(mixed $filter = null) static 设置或获取当前的过滤规则 + * @method mixed has(string $name, string $type = 'param', bool $checkEmpty = false) static 是否存在某个请求参数 + * @method mixed only(mixed $name, string $type = 'param') static 获取指定的参数 + * @method mixed except(mixed $name, string $type = 'param') static 排除指定参数获取 + * @method bool isSsl() static 当前是否ssl + * @method bool isAjax(bool $ajax = false) static 当前是否Ajax请求 + * @method bool isPjax(bool $pjax = false) static 当前是否Pjax请求 + * @method mixed ip(int $type = 0, bool $adv = true) static 获取客户端IP地址 + * @method bool isMobile() static 检测是否使用手机访问 + * @method string scheme() static 当前URL地址中的scheme参数 + * @method string query() static 当前请求URL地址中的query参数 + * @method string host(bool $stric = false) static 当前请求的host + * @method string port() static 当前请求URL地址中的port参数 + * @method string protocol() static 当前请求 SERVER_PROTOCOL + * @method string remotePort() static 当前请求 REMOTE_PORT + * @method string contentType() static 当前请求 HTTP_CONTENT_TYPE + * @method array routeInfo() static 获取当前请求的路由信息 + * @method array dispatch() static 获取当前请求的调度信息 + * @method string module() static 获取当前的模块名 + * @method string controller(bool $convert = false) static 获取当前的控制器名 + * @method string action(bool $convert = false) static 获取当前的操作名 + * @method string langset() static 获取当前的语言 + * @method string getContent() static 设置或者获取当前请求的content + * @method string getInput() static 获取当前请求的php://input + * @method string token(string $name = '__token__', mixed $type = 'md5') static 生成请求令牌 + * @method string cache(string $key, mixed $expire = null, array $except = [], string $tag = null) static 设置当前地址的请求缓存 + * @method string getCache() static 读取请求缓存设置 + */ +class Request extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'request'; + } +} diff --git a/vendor/topthink/framework/library/think/facade/Response.php b/vendor/topthink/framework/library/think/facade/Response.php new file mode 100644 index 0000000..d7de142 --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/Response.php @@ -0,0 +1,47 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Response + * @mixin \think\Response + * @method \think\response create(mixed $data = '', string $type = '', int $code = 200, array $header = [], array $options = []) static 创建Response对象 + * @method void send() static 发送数据到客户端 + * @method \think\Response options(mixed $options = []) static 输出的参数 + * @method \think\Response data(mixed $data) static 输出数据设置 + * @method \think\Response header(mixed $name, string $value = null) static 设置响应头 + * @method \think\Response content(mixed $content) static 设置页面输出内容 + * @method \think\Response code(int $code) static 发送HTTP状态 + * @method \think\Response lastModified(string $time) static LastModified + * @method \think\Response expires(string $time) static expires + * @method \think\Response eTag(string $eTag) static eTag + * @method \think\Response cacheControl(string $cache) static 页面缓存控制 + * @method \think\Response contentType(string $contentType, string $charset = 'utf-8') static 页面输出类型 + * @method mixed getHeader(string $name) static 获取头部信息 + * @method mixed getData() static 获取原始数据 + * @method mixed getContent() static 获取输出数据 + * @method int getCode() static 获取状态码 + */ +class Response extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'response'; + } +} diff --git a/vendor/topthink/framework/library/think/facade/Route.php b/vendor/topthink/framework/library/think/facade/Route.php new file mode 100644 index 0000000..6457ba4 --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/Route.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Route + * @mixin \think\Route + * @method \think\route\Domain domain(mixed $name, mixed $rule = '', array $option = [], array $pattern = []) static 注册域名路由 + * @method \think\Route pattern(mixed $name, string $rule = '') static 注册变量规则 + * @method \think\Route option(mixed $name, mixed $value = '') static 注册路由参数 + * @method \think\Route bind(string $bind) static 设置路由绑定 + * @method mixed getBind(string $bind) static 读取路由绑定 + * @method \think\Route name(string $name) static 设置当前路由标识 + * @method mixed getName(string $name) static 读取路由标识 + * @method void setName(string $name) static 批量导入路由标识 + * @method void import(array $rules, string $type = '*') static 导入配置文件的路由规则 + * @method \think\route\RuleItem rule(string $rule, mixed $route, string $method = '*', array $option = [], array $pattern = []) static 注册路由规则 + * @method void rules(array $rules, string $method = '*', array $option = [], array $pattern = []) static 批量注册路由规则 + * @method \think\route\RuleGroup group(string|array $name, array|\Closure $route, array $method = '*', array $option = [], array $pattern = []) static 注册路由分组 + * @method \think\route\RuleItem any(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由 + * @method \think\route\RuleItem get(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由 + * @method \think\route\RuleItem post(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由 + * @method \think\route\RuleItem put(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由 + * @method \think\route\RuleItem delete(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由 + * @method \think\route\RuleItem patch(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由 + * @method \think\route\Resource resource(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册资源路由 + * @method \think\Route controller(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册控制器路由 + * @method \think\Route alias(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册别名路由 + * @method \think\Route setMethodPrefix(mixed $method, string $prefix = '') static 设置不同请求类型下面的方法前缀 + * @method \think\Route rest(string $name, array $resource = []) static rest方法定义和修改 + * @method \think\Route\RuleItem miss(string $route, string $method = '*', array $option = []) static 注册未匹配路由规则后的处理 + * @method \think\Route\RuleItem auto(string $route) static 注册一个自动解析的URL路由 + * @method \think\Route\Dispatch check(string $url, string $depr = '/', bool $must = false, bool $completeMatch = false) static 检测URL路由 + */ +class Route extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'route'; + } +} diff --git a/vendor/topthink/framework/library/think/facade/Session.php b/vendor/topthink/framework/library/think/facade/Session.php new file mode 100644 index 0000000..fb9206a --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/Session.php @@ -0,0 +1,46 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Session + * @mixin \think\Session + * @method void init(array $config = []) static session初始化 + * @method bool has(string $name,string $prefix = null) static 判断session数据 + * @method mixed prefix(string $prefix = '') static 设置或者获取session作用域(前缀) + * @method mixed get(string $name = '',string $prefix = null) static session获取 + * @method mixed pull(string $name,string $prefix = null) static session获取并删除 + * @method void push(string $key, mixed $value) static 添加数据到一个session数组 + * @method void set(string $name, mixed $value , string $prefix = null) static 设置session数据 + * @method void flash(string $name, mixed $value = null) static session设置 下一次请求有效 + * @method void flush() static 清空当前请求的session数据 + * @method void delete(string $name, string $prefix = null) static 删除session数据 + * @method void clear($prefix = null) static 清空session数据 + * @method void start() static 启动session + * @method void destroy() static 销毁session + * @method void pause() static 暂停session + * @method void regenerate(bool $delete = false) static 重新生成session_id + */ +class Session extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'session'; + } +} diff --git a/vendor/topthink/framework/library/think/facade/Template.php b/vendor/topthink/framework/library/think/facade/Template.php new file mode 100644 index 0000000..f91b118 --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/Template.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Template + * @mixin \think\Template + * @method void assign(mixed $name, mixed $value = '') static 模板变量赋值 + * @method mixed get(string $name = '') static 获取模板变量 + * @method void fetch(string $template, array $vars = [], array $config = []) static 渲染模板文件 + * @method void display(string $content, array $vars = [], array $config = []) static 渲染模板内容 + * @method mixed layout(string $name, string $replace = '') static 设置模板布局 + */ +class Template extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'template'; + } +} diff --git a/vendor/topthink/framework/library/think/facade/Url.php b/vendor/topthink/framework/library/think/facade/Url.php new file mode 100644 index 0000000..639591a --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/Url.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Url + * @mixin \think\Url + * @method string build(string $url = '', mixed $vars = '', mixed $suffix = true, mixed $domain = false) static URL生成 支持路由反射 + * @method void root(string $root) static 指定当前生成URL地址的root + */ +class Url extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'url'; + } +} diff --git a/vendor/topthink/framework/library/think/facade/Validate.php b/vendor/topthink/framework/library/think/facade/Validate.php new file mode 100644 index 0000000..a6eec23 --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/Validate.php @@ -0,0 +1,75 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Validate + * @mixin \think\Validate + * @method \think\Validate make(array $rules = [], array $message = [], array $field = []) static 创建一个验证器类 + * @method \think\Validate rule(mixed $name, mixed $rule = '') static 添加字段验证规则 + * @method void extend(string $type, mixed $callback = null) static 注册扩展验证(类型)规则 + * @method void setTypeMsg(mixed $type, string $msg = null) static 设置验证规则的默认提示信息 + * @method \think\Validate message(mixed $name, string $message = '') static 设置提示信息 + * @method \think\Validate scene(string $name) static 设置验证场景 + * @method bool hasScene(string $name) static 判断是否存在某个验证场景 + * @method \think\Validate batch(bool $batch = true) static 设置批量验证 + * @method \think\Validate only(array $fields) static 指定需要验证的字段列表 + * @method \think\Validate remove(mixed $field, mixed $rule = true) static 移除某个字段的验证规则 + * @method \think\Validate append(mixed $field, mixed $rule = null) static 追加某个字段的验证规则 + * @method bool confirm(mixed $value, mixed $rule, array $data = [], string $field = '') static 验证是否和某个字段的值一致 + * @method bool different(mixed $value, mixed $rule, array $data = []) static 验证是否和某个字段的值是否不同 + * @method bool egt(mixed $value, mixed $rule, array $data = []) static 验证是否大于等于某个值 + * @method bool gt(mixed $value, mixed $rule, array $data = []) static 验证是否大于某个值 + * @method bool elt(mixed $value, mixed $rule, array $data = []) static 验证是否小于等于某个值 + * @method bool lt(mixed $value, mixed $rule, array $data = []) static 验证是否小于某个值 + * @method bool eq(mixed $value, mixed $rule) static 验证是否等于某个值 + * @method bool must(mixed $value, mixed $rule) static 必须验证 + * @method bool is(mixed $value, mixed $rule, array $data = []) static 验证字段值是否为有效格式 + * @method bool ip(mixed $value, mixed $rule) static 验证是否有效IP + * @method bool requireIf(mixed $value, mixed $rule) static 验证某个字段等于某个值的时候必须 + * @method bool requireCallback(mixed $value, mixed $rule,array $data) static 通过回调方法验证某个字段是否必须 + * @method bool requireWith(mixed $value, mixed $rule, array $data) static 验证某个字段有值的情况下必须 + * @method bool filter(mixed $value, mixed $rule) static 使用filter_var方式验证 + * @method bool in(mixed $value, mixed $rule) static 验证是否在范围内 + * @method bool notIn(mixed $value, mixed $rule) static 验证是否不在范围内 + * @method bool between(mixed $value, mixed $rule) static between验证数据 + * @method bool notBetween(mixed $value, mixed $rule) static 使用notbetween验证数据 + * @method bool length(mixed $value, mixed $rule) static 验证数据长度 + * @method bool max(mixed $value, mixed $rule) static 验证数据最大长度 + * @method bool min(mixed $value, mixed $rule) static 验证数据最小长度 + * @method bool after(mixed $value, mixed $rule) static 验证日期 + * @method bool before(mixed $value, mixed $rule) static 验证日期 + * @method bool expire(mixed $value, mixed $rule) static 验证有效期 + * @method bool allowIp(mixed $value, mixed $rule) static 验证IP许可 + * @method bool denyIp(mixed $value, mixed $rule) static 验证IP禁用 + * @method bool regex(mixed $value, mixed $rule) static 使用正则验证数据 + * @method bool token(mixed $value, mixed $rule) static 验证表单令牌 + * @method bool dateFormat(mixed $value, mixed $rule) static 验证时间和日期是否符合指定格式 + * @method bool unique(mixed $value, mixed $rule, array $data = [], string $field = '') static 验证是否唯一 + * @method bool check(array $data, mixed $rules = [], string $scene = '') static 数据自动验证 + * @method mixed getError(mixed $value, mixed $rule) static 获取错误信息 + */ +class Validate extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'validate'; + } + +} diff --git a/vendor/topthink/framework/library/think/facade/View.php b/vendor/topthink/framework/library/think/facade/View.php new file mode 100644 index 0000000..0843391 --- /dev/null +++ b/vendor/topthink/framework/library/think/facade/View.php @@ -0,0 +1,40 @@ + +// +---------------------------------------------------------------------- + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\View + * @mixin \think\View + * @method \think\View init(mixed $engine = [], array $replace = []) static 初始化 + * @method \think\View share(mixed $name, mixed $value = '') static 模板变量静态赋值 + * @method \think\View assign(mixed $name, mixed $value = '') static 模板变量赋值 + * @method \think\View config(mixed $name, mixed $value = '') static 配置模板引擎 + * @method \think\View exists(mixed $name) static 检查模板是否存在 + * @method \think\View filter(Callable $filter) static 视图内容过滤 + * @method \think\View engine(mixed $engine = []) static 设置当前模板解析的引擎 + * @method string fetch(string $template = '', array $vars = [], array $config = [], bool $renderContent = false) static 解析和获取模板内容 + * @method string display(string $content = '', array $vars = [], array $config = []) static 渲染内容输出 + */ +class View extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'view'; + } +} diff --git a/vendor/topthink/framework/library/think/log/driver/File.php b/vendor/topthink/framework/library/think/log/driver/File.php new file mode 100644 index 0000000..3f6522d --- /dev/null +++ b/vendor/topthink/framework/library/think/log/driver/File.php @@ -0,0 +1,287 @@ + +// +---------------------------------------------------------------------- + +namespace think\log\driver; + +use think\App; + +/** + * 本地化调试输出到文件 + */ +class File +{ + protected $config = [ + 'time_format' => 'c', + 'single' => false, + 'file_size' => 2097152, + 'path' => '', + 'apart_level' => [], + 'max_files' => 0, + 'json' => false, + ]; + + protected $app; + + // 实例化并传入参数 + public function __construct(App $app, $config = []) + { + $this->app = $app; + + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } + + if (empty($this->config['path'])) { + $this->config['path'] = $this->app->getRuntimePath() . 'log' . DIRECTORY_SEPARATOR; + } elseif (substr($this->config['path'], -1) != DIRECTORY_SEPARATOR) { + $this->config['path'] .= DIRECTORY_SEPARATOR; + } + } + + /** + * 日志写入接口 + * @access public + * @param array $log 日志信息 + * @param bool $append 是否追加请求信息 + * @return bool + */ + public function save(array $log = [], $append = false) + { + $destination = $this->getMasterLogFile(); + + $path = dirname($destination); + !is_dir($path) && mkdir($path, 0755, true); + + $info = []; + + foreach ($log as $type => $val) { + + foreach ($val as $msg) { + if (!is_string($msg)) { + $msg = var_export($msg, true); + } + + $info[$type][] = $this->config['json'] ? $msg : '[ ' . $type . ' ] ' . $msg; + } + + if (!$this->config['json'] && (true === $this->config['apart_level'] || in_array($type, $this->config['apart_level']))) { + // 独立记录的日志级别 + $filename = $this->getApartLevelFile($path, $type); + + $this->write($info[$type], $filename, true, $append); + + unset($info[$type]); + } + } + + if ($info) { + return $this->write($info, $destination, false, $append); + } + + return true; + } + + /** + * 日志写入 + * @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) { + $msg = is_array($msg) ? implode(PHP_EOL, $msg) : $msg; + if (PHP_SAPI == 'cli') { + $info['msg'] = $msg; + $info['type'] = $type; + } else { + $info[$type] = $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 public + * @return string + */ + protected function getMasterLogFile() + { + if ($this->config['max_files']) { + $files = glob($this->config['path'] . '*.log'); + + try { + if (count($files) > $this->config['max_files']) { + unlink($files[0]); + } + } catch (\Exception $e) { + } + } + + $cli = PHP_SAPI == 'cli' ? '_cli' : ''; + + if ($this->config['single']) { + $name = is_string($this->config['single']) ? $this->config['single'] : 'single'; + + $destination = $this->config['path'] . $name . $cli . '.log'; + } else { + if ($this->config['max_files']) { + $filename = date('Ymd') . $cli . '.log'; + } 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'; + } elseif ($this->config['max_files']) { + $name = date('Ymd'); + } else { + $name = date('d'); + } + + return $path . DIRECTORY_SEPARATOR . $name . '_' . $type . $cli . '.log'; + } + + /** + * 检查日志文件大小并自动生成备份文件 + * @access protected + * @param string $destination 日志文件 + * @return void + */ + protected function checkLogSize($destination) + { + if (is_file($destination) && floor($this->config['file_size']) <= filesize($destination)) { + try { + rename($destination, dirname($destination) . DIRECTORY_SEPARATOR . time() . '-' . basename($destination)); + } catch (\Exception $e) { + } + } + } + + /** + * 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) . PHP_EOL; + } else { + $now = $info['timestamp']; + unset($info['timestamp']); + + $message = implode(PHP_EOL, $info); + + $message = "[{$now}]" . $message . PHP_EOL; + } + + return $message; + } + + /** + * 解析日志 + * @access protected + * @param array $info 日志信息 + * @return string + */ + protected function parseLog($info) + { + $requestInfo = [ + 'ip' => $this->app['request']->ip(), + 'method' => $this->app['request']->method(), + 'host' => $this->app['request']->host(), + 'uri' => $this->app['request']->url(), + ]; + + if ($this->config['json']) { + $info = $requestInfo + $info; + return json_encode($info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL; + } + + array_unshift($info, "---------------------------------------------------------------" . PHP_EOL . "\r\n[{$info['timestamp']}] {$requestInfo['ip']} {$requestInfo['method']} {$requestInfo['host']}{$requestInfo['uri']}"); + unset($info['timestamp']); + + return implode(PHP_EOL, $info) . PHP_EOL; + } + + protected function getDebugLog(&$info, $append, $apart) + { + if ($this->app->isDebug() && $append) { + + if ($this->config['json']) { + // 获取基本信息 + $runtime = round(microtime(true) - $this->app->getBeginTime(), 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + + $memory_use = number_format((memory_get_usage() - $this->app->getBeginMem()) / 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) - $this->app->getBeginTime(), 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + + $memory_use = number_format((memory_get_usage() - $this->app->getBeginMem()) / 1024, 2); + + $time_str = '[运行时间:' . number_format($runtime, 6) . 's] [吞吐率:' . $reqs . 'req/s]'; + $memory_str = ' [内存消耗:' . $memory_use . 'kb]'; + $file_load = ' [文件加载:' . count(get_included_files()) . ']'; + + array_unshift($info, $time_str . $memory_str . $file_load); + } + } + } +} diff --git a/vendor/topthink/framework/library/think/log/driver/Socket.php b/vendor/topthink/framework/library/think/log/driver/Socket.php new file mode 100644 index 0000000..5e4f8bf --- /dev/null +++ b/vendor/topthink/framework/library/think/log/driver/Socket.php @@ -0,0 +1,279 @@ + +// +---------------------------------------------------------------------- + +namespace think\log\driver; + +use think\App; + +/** + * github: https://github.com/luofei614/SocketLog + * @author luofei614 + */ +class Socket +{ + public $port = 1116; //SocketLog 服务的http的端口号 + + protected $config = [ + // socket服务器地址 + 'host' => 'localhost', + // 是否显示加载的文件列表 + 'show_included_files' => false, + // 日志强制记录到配置的client_id + 'force_client_ids' => [], + // 限制允许读取日志的client_id + 'allow_client_ids' => [], + //输出到浏览器默认展开的日志级别 + 'expand_level' => ['debug'], + ]; + + protected $css = [ + 'sql' => 'color:#009bb4;', + 'sql_warn' => 'color:#009bb4;font-size:14px;', + 'error' => 'color:#f4006b;font-size:14px;', + 'page' => 'color:#40e2ff;background:#171717;', + 'big' => 'font-size:20px;color:red;', + ]; + + protected $allowForceClientIds = []; //配置强制推送且被授权的client_id + protected $app; + + /** + * 架构函数 + * @access public + * @param array $config 缓存参数 + */ + public function __construct(App $app, array $config = []) + { + $this->app = $app; + + if (!empty($config)) { + $this->config = array_merge($this->config, $config); + } + } + + /** + * 调试输出接口 + * @access public + * @param array $log 日志信息 + * @return bool + */ + public function save(array $log = [], $append = false) + { + if (!$this->check()) { + return false; + } + + $trace = []; + + if ($this->app->isDebug()) { + $runtime = round(microtime(true) - $this->app->getBeginTime(), 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() - $this->app->getBeginMem()) / 1024, 2); + $memory_str = ' [内存消耗:' . $memory_use . 'kb]'; + $file_load = ' [文件加载:' . count(get_included_files()) . ']'; + + if (isset($_SERVER['HTTP_HOST'])) { + $current_uri = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + } else { + $current_uri = 'cmd:' . implode(' ', $_SERVER['argv']); + } + + // 基本信息 + $trace[] = [ + 'type' => 'group', + 'msg' => $current_uri . $time_str . $memory_str . $file_load, + 'css' => $this->css['page'], + ]; + } + + foreach ($log as $type => $val) { + $trace[] = [ + 'type' => in_array($type, $this->config['expand_level']) ? 'group' : 'groupCollapsed', + 'msg' => '[ ' . $type . ' ]', + 'css' => isset($this->css[$type]) ? $this->css[$type] : '', + ]; + + foreach ($val as $msg) { + if (!is_string($msg)) { + $msg = var_export($msg, true); + } + $trace[] = [ + 'type' => 'log', + 'msg' => $msg, + 'css' => '', + ]; + } + + $trace[] = [ + 'type' => 'groupEnd', + 'msg' => '', + 'css' => '', + ]; + } + + if ($this->config['show_included_files']) { + $trace[] = [ + 'type' => 'groupCollapsed', + 'msg' => '[ file ]', + 'css' => '', + ]; + + $trace[] = [ + 'type' => 'log', + 'msg' => implode("\n", get_included_files()), + 'css' => '', + ]; + + $trace[] = [ + 'type' => 'groupEnd', + 'msg' => '', + 'css' => '', + ]; + } + + $trace[] = [ + 'type' => 'groupEnd', + 'msg' => '', + 'css' => '', + ]; + + $tabid = $this->getClientArg('tabid'); + + if (!$client_id = $this->getClientArg('client_id')) { + $client_id = ''; + } + + if (!empty($this->allowForceClientIds)) { + //强制推送到多个client_id + foreach ($this->allowForceClientIds as $force_client_id) { + $client_id = $force_client_id; + $this->sendToClient($tabid, $client_id, $trace, $force_client_id); + } + } else { + $this->sendToClient($tabid, $client_id, $trace, ''); + } + + return true; + } + + /** + * 发送给指定客户端 + * @access protected + * @author Zjmainstay + * @param $tabid + * @param $client_id + * @param $logs + * @param $force_client_id + */ + protected function sendToClient($tabid, $client_id, $logs, $force_client_id) + { + $logs = [ + 'tabid' => $tabid, + 'client_id' => $client_id, + 'logs' => $logs, + 'force_client_id' => $force_client_id, + ]; + + $msg = @json_encode($logs); + $address = '/' . $client_id; //将client_id作为地址, server端通过地址判断将日志发布给谁 + + $this->send($this->config['host'], $msg, $address); + } + + protected function check() + { + $tabid = $this->getClientArg('tabid'); + + //是否记录日志的检查 + if (!$tabid && !$this->config['force_client_ids']) { + return false; + } + + //用户认证 + $allow_client_ids = $this->config['allow_client_ids']; + + if (!empty($allow_client_ids)) { + //通过数组交集得出授权强制推送的client_id + $this->allowForceClientIds = array_intersect($allow_client_ids, $this->config['force_client_ids']); + if (!$tabid && count($this->allowForceClientIds)) { + return true; + } + + $client_id = $this->getClientArg('client_id'); + if (!in_array($client_id, $allow_client_ids)) { + return false; + } + } else { + $this->allowForceClientIds = $this->config['force_client_ids']; + } + + return true; + } + + protected function getClientArg($name) + { + static $args = []; + + $key = 'HTTP_USER_AGENT'; + + if (isset($_SERVER['HTTP_SOCKETLOG'])) { + $key = 'HTTP_SOCKETLOG'; + } + + if (!isset($_SERVER[$key])) { + return; + } + + if (empty($args)) { + if (!preg_match('/SocketLog\((.*?)\)/', $_SERVER[$key], $match)) { + $args = ['tabid' => null]; + return; + } + parse_str($match[1], $args); + } + + if (isset($args[$name])) { + return $args[$name]; + } + + return; + } + + /** + * @access protected + * @param string $host - $host of socket server + * @param string $message - 发送的消息 + * @param string $address - 地址 + * @return bool + */ + protected function send($host, $message = '', $address = '/') + { + $url = 'http://' . $host . ':' . $this->port . $address; + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $message); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + + $headers = [ + "Content-Type: application/json;charset=UTF-8", + ]; + + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); //设置header + + return curl_exec($ch); + } + +} diff --git a/vendor/topthink/framework/library/think/model/Collection.php b/vendor/topthink/framework/library/think/model/Collection.php new file mode 100644 index 0000000..fc0967c --- /dev/null +++ b/vendor/topthink/framework/library/think/model/Collection.php @@ -0,0 +1,118 @@ + +// +---------------------------------------------------------------------- + +namespace think\model; + +use think\Collection as BaseCollection; +use think\Model; + +class Collection extends BaseCollection +{ + /** + * 延迟预载入关联查询 + * @access public + * @param mixed $relation 关联 + * @return $this + */ + public function load($relation) + { + if (!$this->isEmpty()) { + $item = current($this->items); + $item->eagerlyResultSet($this->items, $relation); + } + + return $this; + } + + /** + * 绑定(一对一)关联属性到当前模型 + * @access protected + * @param string $relation 关联名称 + * @param array $attrs 绑定属性 + * @return $this + */ + public function bindAttr($relation, array $attrs = []) + { + $this->each(function (Model $model) use ($relation, $attrs) { + $model->bindAttr($relation, $attrs); + }); + + return $this; + } + + /** + * 设置需要隐藏的输出属性 + * @access public + * @param array $hidden 属性列表 + * @param bool $override 是否覆盖 + * @return $this + */ + public function hidden($hidden = [], $override = false) + { + $this->each(function ($model) use ($hidden, $override) { + /** @var Model $model */ + $model->hidden($hidden, $override); + }); + + return $this; + } + + /** + * 设置需要输出的属性 + * @access public + * @param array $visible + * @param bool $override 是否覆盖 + * @return $this + */ + public function visible($visible = [], $override = false) + { + $this->each(function ($model) use ($visible, $override) { + /** @var Model $model */ + $model->visible($visible, $override); + }); + + return $this; + } + + /** + * 设置需要追加的输出属性 + * @access public + * @param array $append 属性列表 + * @param bool $override 是否覆盖 + * @return $this + */ + public function append($append = [], $override = false) + { + $this->each(function ($model) use ($append, $override) { + /** @var Model $model */ + $model && $model->append($append, $override); + }); + + return $this; + } + + /** + * 设置数据字段获取器 + * @access public + * @param string|array $name 字段名 + * @param callable $callback 闭包获取器 + * @return $this + */ + public function withAttr($name, $callback = null) + { + $this->each(function ($model) use ($name, $callback) { + /** @var Model $model */ + $model && $model->withAttribute($name, $callback); + }); + + return $this; + } +} diff --git a/vendor/topthink/framework/library/think/model/Pivot.php b/vendor/topthink/framework/library/think/model/Pivot.php new file mode 100644 index 0000000..a3a395e --- /dev/null +++ b/vendor/topthink/framework/library/think/model/Pivot.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- + +namespace think\model; + +use think\Model; + +class Pivot extends Model +{ + + /** @var Model */ + public $parent; + + protected $autoWriteTimestamp = false; + + /** + * 架构函数 + * @access public + * @param array|object $data 数据 + * @param Model $parent 上级模型 + * @param string $table 中间数据表名 + */ + public function __construct($data = [], Model $parent = null, $table = '') + { + $this->parent = $parent; + + if (is_null($this->name)) { + $this->name = $table; + } + + parent::__construct($data); + } + +} diff --git a/vendor/topthink/framework/library/think/model/Relation.php b/vendor/topthink/framework/library/think/model/Relation.php new file mode 100644 index 0000000..ac6dd4c --- /dev/null +++ b/vendor/topthink/framework/library/think/model/Relation.php @@ -0,0 +1,187 @@ + +// +---------------------------------------------------------------------- + +namespace think\model; + +use think\db\Query; +use think\Exception; +use think\Model; + +/** + * Class Relation + * @package think\model + * + * @mixin Query + */ +abstract class Relation +{ + // 父模型对象 + protected $parent; + /** @var Model 当前关联的模型类 */ + protected $model; + /** @var Query 关联模型查询对象 */ + protected $query; + // 关联表外键 + protected $foreignKey; + // 关联表主键 + protected $localKey; + // 基础查询 + protected $baseQuery; + // 是否为自关联 + protected $selfRelation; + + /** + * 获取关联的所属模型 + * @access public + * @return Model + */ + public function getParent() + { + return $this->parent; + } + + /** + * 获取当前的关联模型类的实例 + * @access public + * @return Model + */ + public function getModel() + { + return $this->query->getModel(); + } + + /** + * 获取当前的关联模型类的实例 + * @access public + * @return Query + */ + public function getQuery() + { + 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 + * @param array $resultSet 数据集 + * @return mixed + */ + protected function resultSetBuild($resultSet) + { + return (new $this->model)->toCollection($resultSet); + } + + protected function getQueryFields($model) + { + $fields = $this->query->getOptions('field'); + return $this->getRelationQueryFields($fields, $model); + } + + protected function getRelationQueryFields($fields, $model) + { + if ($fields) { + + if (is_string($fields)) { + $fields = explode(',', $fields); + } + + foreach ($fields as &$field) { + if (false === strpos($field, '.')) { + $field = $model . '.' . $field; + } + } + } else { + $fields = $model . '.*'; + } + + return $fields; + } + + protected function getQueryWhere(&$where, $relation) + { + foreach ($where as $key => &$val) { + if (is_string($key)) { + $where[] = [false === strpos($key, '.') ? $relation . '.' . $key : $key, '=', $val]; + unset($where[$key]); + } elseif (isset($val[0]) && false === strpos($val[0], '.')) { + $val[0] = $relation . '.' . $val[0]; + } + } + } + + /** + * 更新数据 + * @access public + * @param array $data 更新数据 + * @return integer|string + */ + public function update(array $data = []) + { + return $this->query->update($data); + } + + /** + * 删除记录 + * @access public + * @param mixed $data 表达式 true 表示强制删除 + * @return int + * @throws Exception + * @throws PDOException + */ + public function delete($data = null) + { + return $this->query->delete($data); + } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + {} + + public function __call($method, $args) + { + if ($this->query) { + // 执行基础查询 + $this->baseQuery(); + + $result = call_user_func_array([$this->query->getModel(), $method], $args); + + return $result === $this->query && !in_array(strtolower($method), ['fetchsql', 'fetchpdo']) ? $this : $result; + } else { + throw new Exception('method not exists:' . __CLASS__ . '->' . $method); + } + } +} diff --git a/vendor/topthink/framework/library/think/model/concern/Attribute.php b/vendor/topthink/framework/library/think/model/concern/Attribute.php new file mode 100644 index 0000000..66627b3 --- /dev/null +++ b/vendor/topthink/framework/library/think/model/concern/Attribute.php @@ -0,0 +1,656 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\concern; + +use InvalidArgumentException; +use think\db\Expression; +use think\Exception; +use think\Loader; +use think\model\Relation; + +trait Attribute +{ + /** + * 数据表主键 复合主键使用数组定义 + * @var string|array + */ + protected $pk = 'id'; + + /** + * 数据表字段信息 留空则自动获取 + * @var array + */ + protected $field = []; + + /** + * JSON数据表字段 + * @var array + */ + protected $json = []; + + /** + * JSON数据取出是否需要转换为数组 + * @var bool + */ + protected $jsonAssoc = false; + + /** + * JSON数据表字段类型 + * @var array + */ + protected $jsonType = []; + + /** + * 数据表废弃字段 + * @var array + */ + protected $disuse = []; + + /** + * 数据表只读字段 + * @var array + */ + protected $readonly = []; + + /** + * 数据表字段类型 + * @var array + */ + protected $type = []; + + /** + * 当前模型数据 + * @var array + */ + private $data = []; + + /** + * 修改器执行记录 + * @var array + */ + private $set = []; + + /** + * 原始数据 + * @var array + */ + private $origin = []; + + /** + * 动态获取器 + * @var array + */ + private $withAttr = []; + + /** + * 获取模型对象的主键 + * @access public + * @return string|array + */ + public function getPk() + { + return $this->pk; + } + + /** + * 判断一个字段名是否为主键字段 + * @access public + * @param string $key 名称 + * @return bool + */ + protected function isPk($key) + { + $pk = $this->getPk(); + if (is_string($pk) && $pk == $key) { + return true; + } elseif (is_array($pk) && in_array($key, $pk)) { + return true; + } + + return false; + } + + /** + * 获取模型对象的主键值 + * @access public + * @return integer + */ + public function getKey() + { + $pk = $this->getPk(); + if (is_string($pk) && array_key_exists($pk, $this->data)) { + return $this->data[$pk]; + } + + return; + } + + /** + * 设置允许写入的字段 + * @access public + * @param array|string|true $field 允许写入的字段 如果为true只允许写入数据表字段 + * @return $this + */ + public function allowField($field) + { + if (is_string($field)) { + $field = explode(',', $field); + } + + $this->field = $field; + + return $this; + } + + /** + * 设置只读字段 + * @access public + * @param array|string $field 只读字段 + * @return $this + */ + public function readonly($field) + { + if (is_string($field)) { + $field = explode(',', $field); + } + + $this->readonly = $field; + + return $this; + } + + /** + * 设置数据对象值 + * @access public + * @param mixed $data 数据或者属性名 + * @param mixed $value 值 + * @return $this + */ + public function data($data, $value = null) + { + if (is_string($data)) { + $this->data[$data] = $value; + return $this; + } + + // 清空数据 + $this->data = []; + + if (is_object($data)) { + $data = get_object_vars($data); + } + + if ($this->disuse) { + // 废弃字段 + foreach ((array) $this->disuse as $key) { + if (array_key_exists($key, $data)) { + unset($data[$key]); + } + } + } + + if (true === $value) { + // 数据对象赋值 + foreach ($data as $key => $value) { + $this->setAttr($key, $value, $data); + } + } elseif (is_array($value)) { + foreach ($value as $name) { + if (isset($data[$name])) { + $this->data[$name] = $data[$name]; + } + } + } else { + $this->data = $data; + } + + return $this; + } + + /** + * 批量设置数据对象值 + * @access public + * @param mixed $data 数据 + * @param bool $set 是否需要进行数据处理 + * @return $this + */ + public function appendData($data, $set = false) + { + if ($set) { + // 进行数据处理 + foreach ($data as $key => $value) { + $this->setAttr($key, $value, $data); + } + } else { + if (is_object($data)) { + $data = get_object_vars($data); + } + + $this->data = array_merge($this->data, $data); + } + + return $this; + } + + /** + * 获取对象原始数据 如果不存在指定字段返回null + * @access public + * @param string $name 字段名 留空获取全部 + * @return mixed + */ + public function getOrigin($name = null) + { + if (is_null($name)) { + return $this->origin; + } + return array_key_exists($name, $this->origin) ? $this->origin[$name] : null; + } + + /** + * 获取对象原始数据 如果不存在指定字段返回false + * @access public + * @param string $name 字段名 留空获取全部 + * @return mixed + * @throws InvalidArgumentException + */ + public function getData($name = null) + { + if (is_null($name)) { + return $this->data; + } elseif (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } elseif (array_key_exists($name, $this->relation)) { + return $this->relation[$name]; + } + throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name); + } + + /** + * 获取变化的数据 并排除只读数据 + * @access public + * @return array + */ + public function getChangedData() + { + 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)) { + // 只读字段不允许更新 + foreach ($this->readonly as $key => $field) { + if (isset($data[$field])) { + unset($data[$field]); + } + } + } + + return $data; + } + + /** + * 修改器 设置数据对象值 + * @access public + * @param string $name 属性名 + * @param mixed $value 属性值 + * @param array $data 数据 + * @return void + */ + public function setAttr($name, $value, $data = []) + { + if (isset($this->set[$name])) { + return; + } + + if (is_null($value) && $this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) { + // 自动写入的时间戳字段 + $value = $this->autoWriteTimestamp($name); + } else { + // 检测修改器 + $method = 'set' . Loader::parseName($name, 1) . 'Attr'; + + if (method_exists($this, $method)) { + $origin = $this->data; + $value = $this->$method($value, array_merge($this->data, $data)); + + $this->set[$name] = true; + if (is_null($value) && $origin !== $this->data) { + return; + } + } elseif (isset($this->type[$name])) { + // 类型转换 + $value = $this->writeTransform($value, $this->type[$name]); + } + } + + // 设置数据对象属性 + $this->data[$name] = $value; + } + + /** + * 是否需要自动写入时间字段 + * @access public + * @param bool $auto + * @return $this + */ + public function isAutoWriteTimestamp($auto) + { + $this->autoWriteTimestamp = $auto; + + return $this; + } + + /** + * 自动写入时间戳 + * @access protected + * @param string $name 时间戳字段 + * @return mixed + */ + protected function autoWriteTimestamp($name) + { + if (isset($this->type[$name])) { + $type = $this->type[$name]; + + if (strpos($type, ':')) { + list($type, $param) = explode(':', $type, 2); + } + + switch ($type) { + case 'datetime': + case 'date': + $value = $this->formatDateTime('Y-m-d H:i:s.u'); + break; + case 'timestamp': + case 'integer': + default: + $value = time(); + break; + } + } elseif (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [ + 'datetime', + 'date', + 'timestamp', + ])) { + $value = $this->formatDateTime('Y-m-d H:i:s.u'); + } else { + $value = time(); + } + + return $value; + } + + /** + * 数据写入 类型转换 + * @access protected + * @param mixed $value 值 + * @param string|array $type 要转换的类型 + * @return mixed + */ + protected function writeTransform($value, $type) + { + if (is_null($value)) { + return; + } + + if ($value instanceof Expression) { + return $value; + } + + if (is_array($type)) { + list($type, $param) = $type; + } elseif (strpos($type, ':')) { + list($type, $param) = explode(':', $type, 2); + } + + switch ($type) { + case 'integer': + $value = (int) $value; + break; + case 'float': + if (empty($param)) { + $value = (float) $value; + } else { + $value = (float) number_format($value, $param, '.', ''); + } + break; + case 'boolean': + $value = (bool) $value; + break; + case 'timestamp': + if (!is_numeric($value)) { + $value = strtotime($value); + } + break; + case 'datetime': + $value = is_numeric($value) ? $value : strtotime($value); + $value = $this->formatDateTime('Y-m-d H:i:s.u', $value); + break; + case 'object': + if (is_object($value)) { + $value = json_encode($value, JSON_FORCE_OBJECT); + } + break; + case 'array': + $value = (array) $value; + case 'json': + $option = !empty($param) ? (int) $param : JSON_UNESCAPED_UNICODE; + $value = json_encode($value, $option); + break; + case 'serialize': + $value = serialize($value); + break; + } + + return $value; + } + + /** + * 获取器 获取数据对象的值 + * @access public + * @param string $name 名称 + * @param array $item 数据 + * @return mixed + * @throws InvalidArgumentException + */ + public function getAttr($name, &$item = null) + { + try { + $notFound = false; + $value = $this->getData($name); + } catch (InvalidArgumentException $e) { + $notFound = true; + $value = null; + } + + // 检测属性获取器 + $fieldName = Loader::parseName($name); + $method = 'get' . Loader::parseName($name, 1) . 'Attr'; + + if (isset($this->withAttr[$fieldName])) { + if ($notFound && $relation = $this->isRelationAttr($name)) { + $modelRelation = $this->$relation(); + $value = $this->getRelationData($modelRelation); + } + + $closure = $this->withAttr[$fieldName]; + $value = $closure($value, $this->data); + } elseif (method_exists($this, $method)) { + if ($notFound && $relation = $this->isRelationAttr($name)) { + $modelRelation = $this->$relation(); + $value = $this->getRelationData($modelRelation); + } + + $value = $this->$method($value, $this->data); + } elseif (isset($this->type[$name])) { + // 类型转换 + $value = $this->readTransform($value, $this->type[$name]); + } elseif ($this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) { + if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [ + 'datetime', + 'date', + 'timestamp', + ])) { + $value = $this->formatDateTime($this->dateFormat, $value); + } else { + $value = $this->formatDateTime($this->dateFormat, $value, true); + } + } elseif ($notFound) { + $value = $this->getRelationAttribute($name, $item); + } + + return $value; + } + + /** + * 获取关联属性值 + * @access protected + * @param string $name 属性名 + * @param array $item 数据 + * @return mixed + */ + protected function getRelationAttribute($name, &$item) + { + $relation = $this->isRelationAttr($name); + + if ($relation) { + $modelRelation = $this->$relation(); + if ($modelRelation instanceof Relation) { + $value = $this->getRelationData($modelRelation); + + if ($item && method_exists($modelRelation, 'getBindAttr') && $bindAttr = $modelRelation->getBindAttr()) { + + foreach ($bindAttr as $key => $attr) { + $key = is_numeric($key) ? $attr : $key; + + if (isset($item[$key])) { + throw new Exception('bind attr has exists:' . $key); + } else { + $item[$key] = $value ? $value->getAttr($attr) : null; + } + } + + return false; + } + + // 保存关联对象值 + $this->relation[$name] = $value; + + return $value; + } + } + + throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name); + } + + /** + * 数据读取 类型转换 + * @access protected + * @param mixed $value 值 + * @param string|array $type 要转换的类型 + * @return mixed + */ + protected function readTransform($value, $type) + { + if (is_null($value)) { + return; + } + + if (is_array($type)) { + list($type, $param) = $type; + } elseif (strpos($type, ':')) { + list($type, $param) = explode(':', $type, 2); + } + + switch ($type) { + case 'integer': + $value = (int) $value; + break; + case 'float': + if (empty($param)) { + $value = (float) $value; + } else { + $value = (float) number_format($value, $param, '.', ''); + } + break; + case 'boolean': + $value = (bool) $value; + break; + case 'timestamp': + if (!is_null($value)) { + $format = !empty($param) ? $param : $this->dateFormat; + $value = $this->formatDateTime($format, $value, true); + } + break; + case 'datetime': + if (!is_null($value)) { + $format = !empty($param) ? $param : $this->dateFormat; + $value = $this->formatDateTime($format, $value); + } + break; + case 'json': + $value = json_decode($value, true); + break; + case 'array': + $value = empty($value) ? [] : json_decode($value, true); + break; + case 'object': + $value = empty($value) ? new \stdClass() : json_decode($value); + break; + case 'serialize': + try { + $value = unserialize($value); + } catch (\Exception $e) { + $value = null; + } + break; + default: + if (false !== strpos($type, '\\')) { + // 对象类型 + $value = new $type($value); + } + } + + return $value; + } + + /** + * 设置数据字段获取器 + * @access public + * @param string|array $name 字段名 + * @param callable $callback 闭包获取器 + * @return $this + */ + public function withAttribute($name, $callback = null) + { + if (is_array($name)) { + foreach ($name as $key => $val) { + $key = Loader::parseName($key); + + $this->withAttr[$key] = $val; + } + } else { + $name = Loader::parseName($name); + + $this->withAttr[$name] = $callback; + } + + return $this; + } +} diff --git a/vendor/topthink/framework/library/think/model/concern/Conversion.php b/vendor/topthink/framework/library/think/model/concern/Conversion.php new file mode 100644 index 0000000..de4db93 --- /dev/null +++ b/vendor/topthink/framework/library/think/model/concern/Conversion.php @@ -0,0 +1,273 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\concern; + +use think\Collection; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Collection as ModelCollection; + +/** + * 模型数据转换处理 + */ +trait Conversion +{ + /** + * 数据输出显示的属性 + * @var array + */ + protected $visible = []; + + /** + * 数据输出隐藏的属性 + * @var array + */ + protected $hidden = []; + + /** + * 数据输出需要追加的属性 + * @var array + */ + protected $append = []; + + /** + * 数据集对象名 + * @var string + */ + protected $resultSetType; + + /** + * 设置需要附加的输出属性 + * @access public + * @param array $append 属性列表 + * @param bool $override 是否覆盖 + * @return $this + */ + public function append(array $append = [], $override = false) + { + $this->append = $override ? $append : array_merge($this->append, $append); + + return $this; + } + + /** + * 设置附加关联对象的属性 + * @access public + * @param string $attr 关联属性 + * @param string|array $append 追加属性名 + * @return $this + * @throws Exception + */ + public function appendRelationAttr($attr, $append) + { + if (is_string($append)) { + $append = explode(',', $append); + } + + $relation = Loader::parseName($attr, 1, false); + if (isset($this->relation[$relation])) { + $model = $this->relation[$relation]; + } else { + $model = $this->getRelationData($this->$relation()); + } + + if ($model instanceof Model) { + foreach ($append as $key => $attr) { + $key = is_numeric($key) ? $attr : $key; + if (isset($this->data[$key])) { + throw new Exception('bind attr has exists:' . $key); + } else { + $this->data[$key] = $model->getAttr($attr); + } + } + } + + return $this; + } + + /** + * 设置需要隐藏的输出属性 + * @access public + * @param array $hidden 属性列表 + * @param bool $override 是否覆盖 + * @return $this + */ + public function hidden(array $hidden = [], $override = false) + { + $this->hidden = $override ? $hidden : array_merge($this->hidden, $hidden); + + return $this; + } + + /** + * 设置需要输出的属性 + * @access public + * @param array $visible + * @param bool $override 是否覆盖 + * @return $this + */ + public function visible(array $visible = [], $override = false) + { + $this->visible = $override ? $visible : array_merge($this->visible, $visible); + + return $this; + } + + /** + * 转换当前模型对象为数组 + * @access public + * @return array + */ + public function toArray() + { + $item = []; + $hasVisible = false; + + foreach ($this->visible as $key => $val) { + if (is_string($val)) { + if (strpos($val, '.')) { + list($relation, $name) = explode('.', $val); + $this->visible[$relation][] = $name; + } else { + $this->visible[$val] = true; + $hasVisible = true; + } + unset($this->visible[$key]); + } + } + + foreach ($this->hidden as $key => $val) { + if (is_string($val)) { + if (strpos($val, '.')) { + list($relation, $name) = explode('.', $val); + $this->hidden[$relation][] = $name; + } else { + $this->hidden[$val] = true; + } + unset($this->hidden[$key]); + } + } + + // 合并关联数据 + $data = array_merge($this->data, $this->relation); + + foreach ($data as $key => $val) { + if ($val instanceof Model || $val instanceof ModelCollection) { + // 关联模型对象 + if (isset($this->visible[$key]) && is_array($this->visible[$key])) { + $val->visible($this->visible[$key]); + } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) { + $val->hidden($this->hidden[$key]); + } + // 关联模型对象 + if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) { + $item[$key] = $val->toArray(); + } + } elseif (isset($this->visible[$key])) { + $item[$key] = $this->getAttr($key); + } elseif (!isset($this->hidden[$key]) && !$hasVisible) { + $item[$key] = $this->getAttr($key); + } + } + + // 追加属性(必须定义获取器) + if (!empty($this->append)) { + foreach ($this->append as $key => $name) { + if (is_array($name)) { + // 追加关联对象属性 + $relation = $this->getRelation($key); + + if (!$relation) { + $relation = $this->getAttr($key); + if ($relation) { + $relation->visible($name); + } + } + + $item[$key] = $relation ? $relation->append($name)->toArray() : []; + } elseif (strpos($name, '.')) { + list($key, $attr) = explode('.', $name); + // 追加关联对象属性 + $relation = $this->getRelation($key); + + if (!$relation) { + $relation = $this->getAttr($key); + if ($relation) { + $relation->visible([$attr]); + } + } + + $item[$key] = $relation ? $relation->append([$attr])->toArray() : []; + } else { + $item[$name] = $this->getAttr($name, $item); + } + } + } + + return $item; + } + + /** + * 转换当前模型对象为JSON字符串 + * @access public + * @param integer $options json参数 + * @return string + */ + public function toJson($options = JSON_UNESCAPED_UNICODE) + { + return json_encode($this->toArray(), $options); + } + + /** + * 移除当前模型的关联属性 + * @access public + * @return $this + */ + public function removeRelation() + { + $this->relation = []; + return $this; + } + + public function __toString() + { + return $this->toJson(); + } + + // JsonSerializable + public function jsonSerialize() + { + return $this->toArray(); + } + + /** + * 转换数据集为数据集对象 + * @access public + * @param array|Collection $collection 数据集 + * @param string $resultSetType 数据集类 + * @return Collection + */ + public function toCollection($collection, $resultSetType = null) + { + $resultSetType = $resultSetType ?: $this->resultSetType; + + if ($resultSetType && false !== strpos($resultSetType, '\\')) { + $collection = new $resultSetType($collection); + } else { + $collection = new ModelCollection($collection); + } + + return $collection; + } + +} diff --git a/vendor/topthink/framework/library/think/model/concern/ModelEvent.php b/vendor/topthink/framework/library/think/model/concern/ModelEvent.php new file mode 100644 index 0000000..3a87484 --- /dev/null +++ b/vendor/topthink/framework/library/think/model/concern/ModelEvent.php @@ -0,0 +1,238 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\concern; + +use think\Container; +use think\Loader; + +/** + * 模型事件处理 + */ +trait ModelEvent +{ + /** + * 模型回调 + * @var array + */ + private static $event = []; + + /** + * 模型事件观察 + * @var array + */ + protected static $observe = ['before_write', 'after_write', 'before_insert', 'after_insert', 'before_update', 'after_update', 'before_delete', 'after_delete', 'before_restore', 'after_restore']; + + /** + * 绑定模型事件观察者类 + * @var array + */ + protected $observerClass; + + /** + * 是否需要事件响应 + * @var bool + */ + private $withEvent = true; + + /** + * 注册回调方法 + * @access public + * @param string $event 事件名 + * @param callable $callback 回调方法 + * @param bool $override 是否覆盖 + * @return void + */ + public static function event($event, $callback, $override = false) + { + $class = static::class; + + if ($override) { + self::$event[$class][$event] = []; + } + + self::$event[$class][$event][] = $callback; + } + + /** + * 清除回调方法 + * @access public + * @return void + */ + public static function flushEvent() + { + self::$event[static::class] = []; + } + + /** + * 注册一个模型观察者 + * + * @param object|string $class + * @return void + */ + public static function observe($class) + { + self::flushEvent(); + + foreach (static::$observe as $event) { + $eventFuncName = Loader::parseName($event, 1, false); + + if (method_exists($class, $eventFuncName)) { + static::event($event, [$class, $eventFuncName]); + } + } + } + + /** + * 当前操作的事件响应 + * @access protected + * @param bool $event 是否需要事件响应 + * @return $this + */ + public function withEvent($event) + { + $this->withEvent = $event; + return $this; + } + + /** + * 触发事件 + * @access protected + * @param string $event 事件名 + * @return bool + */ + protected function trigger($event) + { + $class = static::class; + + if ($this->withEvent && isset(self::$event[$class][$event])) { + foreach (self::$event[$class][$event] as $callback) { + $result = Container::getInstance()->invoke($callback, [$this]); + + if (false === $result) { + return false; + } + } + } + + return true; + } + + /** + * 模型before_insert事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function beforeInsert($callback, $override = false) + { + self::event('before_insert', $callback, $override); + } + + /** + * 模型after_insert事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function afterInsert($callback, $override = false) + { + self::event('after_insert', $callback, $override); + } + + /** + * 模型before_update事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function beforeUpdate($callback, $override = false) + { + self::event('before_update', $callback, $override); + } + + /** + * 模型after_update事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function afterUpdate($callback, $override = false) + { + self::event('after_update', $callback, $override); + } + + /** + * 模型before_write事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function beforeWrite($callback, $override = false) + { + self::event('before_write', $callback, $override); + } + + /** + * 模型after_write事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function afterWrite($callback, $override = false) + { + self::event('after_write', $callback, $override); + } + + /** + * 模型before_delete事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function beforeDelete($callback, $override = false) + { + self::event('before_delete', $callback, $override); + } + + /** + * 模型after_delete事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function afterDelete($callback, $override = false) + { + self::event('after_delete', $callback, $override); + } + + /** + * 模型before_restore事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function beforeRestore($callback, $override = false) + { + self::event('before_restore', $callback, $override); + } + + /** + * 模型after_restore事件快捷方法 + * @access protected + * @param callable $callback + * @param bool $override + */ + protected static function afterRestore($callback, $override = false) + { + self::event('after_restore', $callback, $override); + } +} diff --git a/vendor/topthink/framework/library/think/model/concern/RelationShip.php b/vendor/topthink/framework/library/think/model/concern/RelationShip.php new file mode 100644 index 0000000..48579b7 --- /dev/null +++ b/vendor/topthink/framework/library/think/model/concern/RelationShip.php @@ -0,0 +1,697 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\concern; + +use think\Collection; +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; +use think\model\relation\BelongsTo; +use think\model\relation\BelongsToMany; +use think\model\relation\HasMany; +use think\model\relation\HasManyThrough; +use think\model\relation\HasOne; +use think\model\relation\MorphMany; +use think\model\relation\MorphOne; +use think\model\relation\MorphTo; + +/** + * 模型关联处理 + */ +trait RelationShip +{ + /** + * 父关联模型对象 + * @var object + */ + private $parent; + + /** + * 模型关联数据 + * @var array + */ + private $relation = []; + + /** + * 关联写入定义信息 + * @var array + */ + private $together; + + /** + * 关联自动写入信息 + * @var array + */ + protected $relationWrite; + + /** + * 设置父关联对象 + * @access public + * @param Model $model 模型对象 + * @return $this + */ + public function setParent($model) + { + $this->parent = $model; + + return $this; + } + + /** + * 获取父关联对象 + * @access public + * @return Model + */ + public function getParent() + { + return $this->parent; + } + + /** + * 获取当前模型的关联模型数据 + * @access public + * @param string $name 关联方法名 + * @return mixed + */ + public function getRelation($name = null) + { + if (is_null($name)) { + return $this->relation; + } elseif (array_key_exists($name, $this->relation)) { + return $this->relation[$name]; + } + return; + } + + /** + * 设置关联数据对象值 + * @access public + * @param string $name 属性名 + * @param mixed $value 属性值 + * @param array $data 数据 + * @return $this + */ + public function setRelation($name, $value, $data = []) + { + // 检测修改器 + $method = 'set' . Loader::parseName($name, 1) . 'Attr'; + + if (method_exists($this, $method)) { + $value = $this->$method($value, array_merge($this->data, $data)); + } + + $this->relation[$name] = $value; + + return $this; + } + + /** + * 绑定(一对一)关联属性到当前模型 + * @access protected + * @param string $relation 关联名称 + * @param array $attrs 绑定属性 + * @return $this + * @throws Exception + */ + public function bindAttr($relation, array $attrs = []) + { + $relation = $this->getRelation($relation); + + foreach ($attrs as $key => $attr) { + $key = is_numeric($key) ? $attr : $key; + $value = $this->getOrigin($key); + + if (!is_null($value)) { + throw new Exception('bind attr has exists:' . $key); + } + + $this->setAttr($key, $relation ? $relation->getAttr($attr) : null); + } + + return $this; + } + + /** + * 关联数据写入 + * @access public + * @param array|string $relation 关联 + * @return $this + */ + public function together($relation) + { + if (is_string($relation)) { + $relation = explode(',', $relation); + } + + $this->together = $relation; + + $this->checkAutoRelationWrite(); + + return $this; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $relation 关联方法名 + * @param mixed $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public static function has($relation, $operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + $relation = (new static())->$relation(); + + if (is_array($operator) || $operator instanceof \Closure) { + return $relation->hasWhere($operator); + } + + return $relation->has($operator, $count, $id, $joinType); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $relation 关联方法名 + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + public static function hasWhere($relation, $where = [], $fields = '*') + { + return (new static())->$relation()->hasWhere($where, $fields); + } + + /** + * 查询当前模型的关联数据 + * @access public + * @param string|array $relations 关联名 + * @param array $withRelationAttr 关联获取器 + * @return $this + */ + public function relationQuery($relations, $withRelationAttr = []) + { + if (is_string($relations)) { + $relations = explode(',', $relations); + } + + foreach ($relations as $key => $relation) { + $subRelation = ''; + $closure = null; + + if ($relation instanceof \Closure) { + // 支持闭包查询过滤关联条件 + $closure = $relation; + $relation = $key; + } + + if (is_array($relation)) { + $subRelation = $relation; + $relation = $key; + } elseif (strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation, 2); + } + + $method = Loader::parseName($relation, 1, false); + $relationName = Loader::parseName($relation); + + $relationResult = $this->$method(); + + if (isset($withRelationAttr[$relationName])) { + $relationResult->getQuery()->withAttr($withRelationAttr[$relationName]); + } + + $this->relation[$relation] = $relationResult->getRelation($subRelation, $closure); + } + + return $this; + } + + /** + * 预载入关联查询 返回数据集 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 关联名 + * @param array $withRelationAttr 关联获取器 + * @param bool $join 是否为JOIN方式 + * @return array + */ + public function eagerlyResultSet(&$resultSet, $relation, $withRelationAttr = [], $join = false) + { + $relations = is_string($relation) ? explode(',', $relation) : $relation; + + foreach ($relations as $key => $relation) { + $subRelation = ''; + $closure = null; + + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } + + if (is_array($relation)) { + $subRelation = $relation; + $relation = $key; + } elseif (strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation, 2); + } + + $relation = Loader::parseName($relation, 1, false); + $relationName = Loader::parseName($relation); + + $relationResult = $this->$relation(); + + if (isset($withRelationAttr[$relationName])) { + $relationResult->getQuery()->withAttr($withRelationAttr[$relationName]); + } + + $relationResult->eagerlyResultSet($resultSet, $relation, $subRelation, $closure, $join); + } + } + + /** + * 预载入关联查询 返回模型对象 + * @access public + * @param Model $result 数据对象 + * @param string $relation 关联名 + * @param array $withRelationAttr 关联获取器 + * @param bool $join 是否为JOIN方式 + * @return Model + */ + public function eagerlyResult(&$result, $relation, $withRelationAttr = [], $join = false) + { + $relations = is_string($relation) ? explode(',', $relation) : $relation; + + foreach ($relations as $key => $relation) { + $subRelation = ''; + $closure = null; + + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } + + if (is_array($relation)) { + $subRelation = $relation; + $relation = $key; + } elseif (strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation, 2); + } + + $relation = Loader::parseName($relation, 1, false); + $relationName = Loader::parseName($relation); + + $relationResult = $this->$relation(); + + if (isset($withRelationAttr[$relationName])) { + $relationResult->getQuery()->withAttr($withRelationAttr[$relationName]); + } + + $relationResult->eagerlyResult($result, $relation, $subRelation, $closure, $join); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param array $relations 关联名 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @return void + */ + public function relationCount(&$result, $relations, $aggregate = 'sum', $field = '*') + { + foreach ($relations as $key => $relation) { + $closure = $name = null; + + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } elseif (is_string($key)) { + $name = $relation; + $relation = $key; + } + + $relation = Loader::parseName($relation, 1, false); + + $count = $this->$relation()->relationCount($result, $closure, $aggregate, $field, $name); + + if (empty($name)) { + $name = Loader::parseName($relation) . '_' . $aggregate; + } + + $result->setAttr($name, $count); + } + } + + /** + * HAS ONE 关联定义 + * @access public + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前主键 + * @return HasOne + */ + public function hasOne($model, $foreignKey = '', $localKey = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $localKey = $localKey ?: $this->getPk(); + $foreignKey = $foreignKey ?: $this->getForeignKey($this->name); + + return new HasOne($this, $model, $foreignKey, $localKey); + } + + /** + * BELONGS TO 关联定义 + * @access public + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 关联主键 + * @return BelongsTo + */ + public function belongsTo($model, $foreignKey = '', $localKey = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $foreignKey = $foreignKey ?: $this->getForeignKey((new $model)->getName()); + $localKey = $localKey ?: (new $model)->getPk(); + $trace = debug_backtrace(false, 2); + $relation = Loader::parseName($trace[1]['function']); + + return new BelongsTo($this, $model, $foreignKey, $localKey, $relation); + } + + /** + * HAS MANY 关联定义 + * @access public + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前主键 + * @return HasMany + */ + public function hasMany($model, $foreignKey = '', $localKey = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $localKey = $localKey ?: $this->getPk(); + $foreignKey = $foreignKey ?: $this->getForeignKey($this->name); + + return new HasMany($this, $model, $foreignKey, $localKey); + } + + /** + * HAS MANY 远程关联定义 + * @access public + * @param string $model 模型名 + * @param string $through 中间模型名 + * @param string $foreignKey 关联外键 + * @param string $throughKey 关联外键 + * @param string $localKey 当前主键 + * @return HasManyThrough + */ + public function hasManyThrough($model, $through, $foreignKey = '', $throughKey = '', $localKey = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $through = $this->parseModel($through); + $localKey = $localKey ?: $this->getPk(); + $foreignKey = $foreignKey ?: $this->getForeignKey($this->name); + $throughKey = $throughKey ?: $this->getForeignKey((new $through)->getName()); + + return new HasManyThrough($this, $model, $through, $foreignKey, $throughKey, $localKey); + } + + /** + * BELONGS TO MANY 关联定义 + * @access public + * @param string $model 模型名 + * @param string $table 中间表名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前模型关联键 + * @return BelongsToMany + */ + public function belongsToMany($model, $table = '', $foreignKey = '', $localKey = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $name = Loader::parseName(basename(str_replace('\\', '/', $model))); + $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); + } + + /** + * MORPH One 关联定义 + * @access public + * @param string $model 模型名 + * @param string|array $morph 多态字段信息 + * @param string $type 多态类型 + * @return MorphOne + */ + public function morphOne($model, $morph = null, $type = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + + if (is_null($morph)) { + $trace = debug_backtrace(false, 2); + $morph = Loader::parseName($trace[1]['function']); + } + + if (is_array($morph)) { + list($morphType, $foreignKey) = $morph; + } else { + $morphType = $morph . '_type'; + $foreignKey = $morph . '_id'; + } + + $type = $type ?: get_class($this); + + return new MorphOne($this, $model, $foreignKey, $morphType, $type); + } + + /** + * MORPH MANY 关联定义 + * @access public + * @param string $model 模型名 + * @param string|array $morph 多态字段信息 + * @param string $type 多态类型 + * @return MorphMany + */ + public function morphMany($model, $morph = null, $type = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + + if (is_null($morph)) { + $trace = debug_backtrace(false, 2); + $morph = Loader::parseName($trace[1]['function']); + } + + $type = $type ?: get_class($this); + + if (is_array($morph)) { + list($morphType, $foreignKey) = $morph; + } else { + $morphType = $morph . '_type'; + $foreignKey = $morph . '_id'; + } + + return new MorphMany($this, $model, $foreignKey, $morphType, $type); + } + + /** + * MORPH TO 关联定义 + * @access public + * @param string|array $morph 多态字段信息 + * @param array $alias 多态别名定义 + * @return MorphTo + */ + public function morphTo($morph = null, $alias = []) + { + $trace = debug_backtrace(false, 2); + $relation = Loader::parseName($trace[1]['function']); + + if (is_null($morph)) { + $morph = $relation; + } + + // 记录当前关联信息 + if (is_array($morph)) { + list($morphType, $foreignKey) = $morph; + } else { + $morphType = $morph . '_type'; + $foreignKey = $morph . '_id'; + } + + return new MorphTo($this, $morphType, $foreignKey, $alias, $relation); + } + + /** + * 解析模型的完整命名空间 + * @access protected + * @param string $model 模型名(或者完整类名) + * @return string + */ + protected function parseModel($model) + { + if (false === strpos($model, '\\')) { + $path = explode('\\', static::class); + array_pop($path); + array_push($path, Loader::parseName($model, 1)); + $model = implode('\\', $path); + } + + return $model; + } + + /** + * 获取模型的默认外键名 + * @access protected + * @param string $name 模型名 + * @return string + */ + protected function getForeignKey($name) + { + if (strpos($name, '\\')) { + $name = basename(str_replace('\\', '/', $name)); + } + + return Loader::parseName($name) . '_id'; + } + + /** + * 检查属性是否为关联属性 如果是则返回关联方法名 + * @access protected + * @param string $attr 关联属性名 + * @return string|false + */ + protected function isRelationAttr($attr) + { + $relation = Loader::parseName($attr, 1, false); + + if (method_exists($this, $relation) && !method_exists('think\Model', $relation)) { + return $relation; + } + + return false; + } + + /** + * 智能获取关联模型数据 + * @access protected + * @param Relation $modelRelation 模型关联对象 + * @return mixed + */ + protected function getRelationData(Relation $modelRelation) + { + if ($this->parent && !$modelRelation->isSelfRelation() && get_class($this->parent) == get_class($modelRelation->getModel())) { + $value = $this->parent; + } else { + // 获取关联数据 + $value = $modelRelation->getRelation(); + } + + return $value; + } + + /** + * 关联数据自动写入检查 + * @access protected + * @return void + */ + protected function checkAutoRelationWrite() + { + foreach ($this->together as $key => $name) { + if (is_array($name)) { + if (key($name) === 0) { + $this->relationWrite[$key] = []; + // 绑定关联属性 + foreach ((array) $name as $val) { + if (isset($this->data[$val])) { + $this->relationWrite[$key][$val] = $this->data[$val]; + } + } + } else { + // 直接传入关联数据 + $this->relationWrite[$key] = $name; + } + } elseif (isset($this->relation[$name])) { + $this->relationWrite[$name] = $this->relation[$name]; + } elseif (isset($this->data[$name])) { + $this->relationWrite[$name] = $this->data[$name]; + unset($this->data[$name]); + } + } + } + + /** + * 自动关联数据更新(针对一对一关联) + * @access protected + * @return void + */ + protected function autoRelationUpdate() + { + foreach ($this->relationWrite as $name => $val) { + if ($val instanceof Model) { + $val->isUpdate()->save(); + } else { + $model = $this->getRelation($name); + if ($model instanceof Model) { + $model->isUpdate()->save($val); + } + } + } + } + + /** + * 自动关联数据写入(针对一对一关联) + * @access protected + * @return void + */ + protected function autoRelationInsert() + { + foreach ($this->relationWrite as $name => $val) { + $method = Loader::parseName($name, 1, false); + $this->$method()->save($val); + } + } + + /** + * 自动关联数据删除(支持一对一及一对多关联) + * @access protected + * @return void + */ + protected function autoRelationDelete() + { + 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) { + foreach ($result as $model) { + $model->delete(); + } + } + } + } +} diff --git a/vendor/topthink/framework/library/think/model/concern/SoftDelete.php b/vendor/topthink/framework/library/think/model/concern/SoftDelete.php new file mode 100644 index 0000000..ec866ac --- /dev/null +++ b/vendor/topthink/framework/library/think/model/concern/SoftDelete.php @@ -0,0 +1,246 @@ +getDeleteTimeField(); + + if ($field && !empty($this->getOrigin($field))) { + return true; + } + + return false; + } + + /** + * 查询软删除数据 + * @access public + * @return Query + */ + public static function withTrashed() + { + $model = new static(); + + return $model->withTrashedData(true)->db(false); + } + + /** + * 是否包含软删除数据 + * @access protected + * @param bool $withTrashed 是否包含软删除数据 + * @return $this + */ + protected function withTrashedData($withTrashed) + { + $this->withTrashed = $withTrashed; + return $this; + } + + /** + * 只查询软删除数据 + * @access public + * @return Query + */ + public static function onlyTrashed() + { + $model = new static(); + $field = $model->getDeleteTimeField(true); + + if ($field) { + return $model + ->db(false) + ->useSoftDelete($field, $model->getWithTrashedExp()); + } + + return $model->db(false); + } + + /** + * 获取软删除数据的查询条件 + * @access protected + * @return array + */ + protected function getWithTrashedExp() + { + return is_null($this->defaultSoftDelete) ? + ['notnull', ''] : ['<>', $this->defaultSoftDelete]; + } + + /** + * 删除当前的记录 + * @access public + * @return bool + */ + public function delete($force = false) + { + if (!$this->isExists() || false === $this->trigger('before_delete', $this)) { + return false; + } + + $force = $force ?: $this->isForce(); + $name = $this->getDeleteTimeField(); + + if ($name && !$force) { + // 软删除 + $this->data($name, $this->autoWriteTimestamp($name)); + + $result = $this->isUpdate()->withEvent(false)->save(); + + $this->withEvent(true); + } else { + // 读取更新条件 + $where = $this->getWhere(); + + // 删除当前模型数据 + $result = $this->db(false) + ->where($where) + ->removeOption('soft_delete') + ->delete(); + } + + // 关联删除 + if (!empty($this->relationWrite)) { + $this->autoRelationDelete(); + } + + $this->trigger('after_delete', $this); + + $this->exists(false); + + return true; + } + + /** + * 删除记录 + * @access public + * @param mixed $data 主键列表 支持闭包查询条件 + * @param bool $force 是否强制删除 + * @return bool + */ + public static function destroy($data, $force = false) + { + // 传入空不执行删除,但是0可以删除 + if (empty($data) && 0 !== $data) { + return false; + } + // 包含软删除数据 + $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 false; + } + + $resultSet = $query->select($data); + + if ($resultSet) { + foreach ($resultSet as $data) { + $data->force($force)->delete(); + } + } + + return true; + } + + /** + * 恢复被软删除的记录 + * @access public + * @param array $where 更新条件 + * @return bool + */ + public function restore($where = []) + { + $name = $this->getDeleteTimeField(); + + if ($name) { + if (false === $this->trigger('before_restore')) { + return false; + } + + if (empty($where)) { + $pk = $this->getPk(); + + $where[] = [$pk, '=', $this->getData($pk)]; + } + + // 恢复删除 + $this->db(false) + ->where($where) + ->useSoftDelete($name, $this->getWithTrashedExp()) + ->update([$name => $this->defaultSoftDelete]); + + $this->trigger('after_restore'); + + return true; + } + + return false; + } + + /** + * 获取软删除字段 + * @access protected + * @param bool $read 是否查询操作 写操作的时候会自动去掉表别名 + * @return string|false + */ + protected function getDeleteTimeField($read = false) + { + $field = property_exists($this, 'deleteTime') && isset($this->deleteTime) ? $this->deleteTime : 'delete_time'; + + if (false === $field) { + return false; + } + + if (false === strpos($field, '.')) { + $field = '__TABLE__.' . $field; + } + + if (!$read && strpos($field, '.')) { + $array = explode('.', $field); + $field = array_pop($array); + } + + return $field; + } + + /** + * 查询的时候默认排除软删除数据 + * @access protected + * @param Query $query + * @return void + */ + protected function withNoTrashed($query) + { + $field = $this->getDeleteTimeField(true); + + if ($field) { + $condition = is_null($this->defaultSoftDelete) ? ['null', ''] : ['=', $this->defaultSoftDelete]; + $query->useSoftDelete($field, $condition); + } + } +} diff --git a/vendor/topthink/framework/library/think/model/concern/TimeStamp.php b/vendor/topthink/framework/library/think/model/concern/TimeStamp.php new file mode 100644 index 0000000..99a31fa --- /dev/null +++ b/vendor/topthink/framework/library/think/model/concern/TimeStamp.php @@ -0,0 +1,92 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\concern; + +use DateTime; + +/** + * 自动时间戳 + */ +trait TimeStamp +{ + /** + * 是否需要自动写入时间戳 如果设置为字符串 则表示时间字段的类型 + * @var bool|string + */ + protected $autoWriteTimestamp; + + /** + * 创建时间字段 false表示关闭 + * @var false|string + */ + protected $createTime = 'create_time'; + + /** + * 更新时间字段 false表示关闭 + * @var false|string + */ + protected $updateTime = 'update_time'; + + /** + * 时间字段显示格式 + * @var string + */ + protected $dateFormat; + + /** + * 时间日期字段格式化处理 + * @access protected + * @param mixed $format 日期格式 + * @param mixed $time 时间日期表达式 + * @param bool $timestamp 是否进行时间戳转换 + * @return mixed + */ + protected function formatDateTime($format, $time = 'now', $timestamp = false) + { + if (empty($time)) { + return; + } + + if (false === $format) { + return $time; + } elseif (false !== strpos($format, '\\')) { + return new $format($time); + } + + if ($timestamp) { + $dateTime = new DateTime(); + $dateTime->setTimestamp($time); + } else { + $dateTime = new DateTime($time); + } + + return $dateTime->format($format); + } + + /** + * 检查时间字段写入 + * @access protected + * @return void + */ + protected function checkTimeStampWrite() + { + // 自动写入创建时间和更新时间 + if ($this->autoWriteTimestamp) { + if ($this->createTime && !isset($this->data[$this->createTime])) { + $this->data[$this->createTime] = $this->autoWriteTimestamp($this->createTime); + } + if ($this->updateTime && !isset($this->data[$this->updateTime])) { + $this->data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime); + } + } + } +} diff --git a/vendor/topthink/framework/library/think/model/relation/BelongsTo.php b/vendor/topthink/framework/library/think/model/relation/BelongsTo.php new file mode 100644 index 0000000..056c7d7 --- /dev/null +++ b/vendor/topthink/framework/library/think/model/relation/BelongsTo.php @@ -0,0 +1,323 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\Loader; +use think\Model; + +class BelongsTo extends OneToOne +{ + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 关联主键 + * @param string $relation 关联名 + */ + public function __construct(Model $parent, $model, $foreignKey, $localKey, $relation = null) + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + $this->joinType = 'INNER'; + $this->query = (new $model)->db(); + $this->relation = $relation; + + if (get_class($parent) == $model) { + $this->selfRelation = true; + } + } + + /** + * 延迟获取关联数据 + * @access public + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return Model + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure instanceof Closure) { + $closure($this->query); + } + + $foreignKey = $this->foreignKey; + + $relationModel = $this->query + ->removeWhereField($this->localKey) + ->where($this->localKey, $this->parent->$foreignKey) + ->relation($subRelation) + ->find(); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } + + return $relationModel; + } + + /** + * 创建关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $aggregateAlias 聚合字段别名 + * @return string + */ + public function getRelationCountQuery($closure, $aggregate = 'count', $field = '*', &$aggregateAlias = '') + { + if ($closure instanceof Closure) { + $return = $closure($this->query); + + if ($return && is_string($return)) { + $aggregateAlias = $return; + } + } + + return $this->query + ->whereExp($this->localKey, '=' . $this->parent->getTable() . '.' . $this->foreignKey) + ->fetchSql() + ->$aggregate($field); + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount($result, $closure, $aggregate = 'count', $field = '*', &$name = '') + { + $foreignKey = $this->foreignKey; + + if (!isset($result->$foreignKey)) { + return 0; + } + + if ($closure instanceof Closure) { + $return = $closure($this->query); + + if ($return && is_string($return)) { + $name = $return; + } + } + + return $this->query + ->where($this->localKey, '=', $result->$foreignKey) + ->$aggregate($field); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + $table = $this->query->getTable(); + $model = basename(str_replace('\\', '/', get_class($this->parent))); + $relation = basename(str_replace('\\', '/', $this->model)); + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + $softDelete = $this->query->getOptions('soft_delete'); + + return $this->parent->db() + ->alias($model) + ->whereExists(function ($query) use ($table, $model, $relation, $localKey, $foreignKey) { + $query->table([$table => $relation]) + ->field($relation . '.' . $localKey) + ->whereExp($model . '.' . $foreignKey, '=' . $relation . '.' . $localKey) + ->when($softDelete, function ($query) use ($softDelete, $relation) { + $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }); + }); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + 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)) { + $this->getQueryWhere($where, $relation); + } + + $fields = $this->getRelationQueryFields($fields, $model); + $softDelete = $this->query->getOptions('soft_delete'); + + return $this->parent->db() + ->alias($model) + ->field($fields) + ->join([$table => $relation], $model . '.' . $this->foreignKey . '=' . $relation . '.' . $this->localKey, $this->joinType) + ->when($softDelete, function ($query) use ($softDelete, $relation) { + $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }) + ->where($where); + } + + /** + * 预载入关联查询(数据集) + * @access protected + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$foreignKey)) { + $range[] = $result->$foreignKey; + } + } + + if (!empty($range)) { + $this->query->removeWhereField($localKey); + + $data = $this->eagerlyWhere([ + [$localKey, 'in', $range], + ], $localKey, $relation, $subRelation, $closure); + + // 关联属性名 + $attr = Loader::parseName($relation); + + // 关联数据封装 + foreach ($resultSet as $result) { + // 关联模型 + if (!isset($data[$result->$foreignKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$foreignKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($relationModel, $result); + } else { + // 设置关联属性 + $result->setRelation($attr, $relationModel); + } + } + } + } + + /** + * 预载入关联查询(数据) + * @access protected + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + protected function eagerlyOne(&$result, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $this->query->removeWhereField($localKey); + + $data = $this->eagerlyWhere([ + [$localKey, '=', $result->$foreignKey], + ], $localKey, $relation, $subRelation, $closure); + + // 关联模型 + if (!isset($data[$result->$foreignKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$foreignKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($relationModel, $result); + } else { + // 设置关联属性 + $result->setRelation(Loader::parseName($relation), $relationModel); + } + } + + /** + * 添加关联数据 + * @access public + * @param Model $model 关联模型对象 + * @return Model + */ + public function associate($model) + { + $this->parent->setAttr($this->foreignKey, $model->getKey()); + $this->parent->save(); + + return $this->parent->setRelation($this->relation, $model); + } + + /** + * 注销关联数据 + * @access public + * @return Model + */ + public function dissociate() + { + $this->parent->setAttr($this->foreignKey, null); + $this->parent->save(); + + 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/vendor/topthink/framework/library/think/model/relation/BelongsToMany.php b/vendor/topthink/framework/library/think/model/relation/BelongsToMany.php new file mode 100644 index 0000000..6105e23 --- /dev/null +++ b/vendor/topthink/framework/library/think/model/relation/BelongsToMany.php @@ -0,0 +1,712 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\Collection; +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Pivot; +use think\model\Relation; +use think\Paginator; + +class BelongsToMany extends Relation +{ + // 中间表表名 + protected $middle; + // 中间表模型名称 + protected $pivotName; + // 中间表数据名称 + protected $pivotDataName = 'pivot'; + // 中间表模型对象 + protected $pivot; + + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $table 中间表名 + * @param string $foreignKey 关联模型外键 + * @param string $localKey 当前模型关联键 + */ + public function __construct(Model $parent, $model, $table, $foreignKey, $localKey) + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + + if (false !== strpos($table, '\\')) { + $this->pivotName = $table; + $this->middle = basename(str_replace('\\', '/', $table)); + } else { + $this->middle = $table; + } + + $this->query = (new $model)->db(); + $this->pivot = $this->newPivot(); + } + + /** + * 设置中间表模型 + * @access public + * @param $pivot + * @return $this + */ + public function pivot($pivot) + { + $this->pivotName = $pivot; + return $this; + } + + /** + * 设置中间表数据名称 + * @access public + * @param string $name + * @return $this + */ + public function pivotDataName($name) + { + $this->pivotDataName = $name; + return $this; + } + + /** + * 获取中间表更新条件 + * @param $data + * @return array + */ + protected function getUpdateWhere($data) + { + return [ + $this->localKey => $data[$this->localKey], + $this->foreignKey => $data[$this->foreignKey], + ]; + } + + /** + * 实例化中间表模型 + * @access public + * @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; + } + + throw new Exception('pivot model must extends: \think\model\Pivot'); + } + + /** + * 合成中间表模型 + * @access protected + * @param array|Collection|Paginator $models + */ + protected function hydratePivot($models) + { + foreach ($models as $model) { + $pivot = []; + + foreach ($model->getData() as $key => $val) { + if (strpos($key, '__')) { + list($name, $attr) = explode('__', $key, 2); + + if ('pivot' == $name) { + $pivot[$attr] = $val; + unset($model->$key); + } + } + } + + $model->setRelation($this->pivotDataName, $this->newPivot($pivot, true)); + } + } + + /** + * 创建关联查询Query对象 + * @access protected + * @return Query + */ + protected function buildQuery() + { + $foreignKey = $this->foreignKey; + $localKey = $this->localKey; + + // 关联查询 + $pk = $this->parent->getPk(); + + $condition[] = ['pivot.' . $localKey, '=', $this->parent->$pk]; + + return $this->belongsToManyQuery($foreignKey, $localKey, $condition); + } + + /** + * 延迟获取关联数据 + * @access public + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure instanceof Closure) { + $closure($this->query); + } + + $result = $this->buildQuery()->relation($subRelation)->select(); + $this->hydratePivot($result); + + return $result; + } + + /** + * 重载select方法 + * @access public + * @param mixed $data + * @return Collection + */ + public function select($data = null) + { + $result = $this->buildQuery()->select($data); + $this->hydratePivot($result); + + return $result; + } + + /** + * 重载paginate方法 + * @access public + * @param null $listRows + * @param bool $simple + * @param array $config + * @return Paginator + */ + public function paginate($listRows = null, $simple = false, $config = []) + { + $result = $this->buildQuery()->paginate($listRows, $simple, $config); + $this->hydratePivot($result); + + return $result; + } + + /** + * 重载find方法 + * @access public + * @param mixed $data + * @return Model + */ + public function find($data = null) + { + $result = $this->buildQuery()->find($data); + if ($result) { + $this->hydratePivot([$result]); + } + + return $result; + } + + /** + * 查找多条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return Collection + */ + public function selectOrFail($data = null) + { + return $this->failException(true)->select($data); + } + + /** + * 查找单条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return Model + */ + public function findOrFail($data = null) + { + return $this->failException(true)->find($data); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + * @throws Exception + */ + public function hasWhere($where = [], $fields = null) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 设置中间表的查询条件 + * @access public + * @param string $field + * @param string $op + * @param mixed $condition + * @return $this + */ + public function wherePivot($field, $op = null, $condition = null) + { + $this->query->where('pivot.' . $field, $op, $condition); + return $this; + } + + /** + * 预载入关联查询(数据集) + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $pk = $resultSet[0]->getPk(); + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$pk)) { + $range[] = $result->$pk; + } + } + + if (!empty($range)) { + // 查询关联数据 + $data = $this->eagerlyManyToMany([ + ['pivot.' . $localKey, 'in', $range], + ], $relation, $subRelation, $closure); + + // 关联属性名 + $attr = Loader::parseName($relation); + + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$pk])) { + $data[$result->$pk] = []; + } + + $result->setRelation($attr, $this->resultSetBuild($data[$result->$pk])); + } + } + } + + /** + * 预载入关联查询(单个数据) + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $pk = $result->getPk(); + + if (isset($result->$pk)) { + $pk = $result->$pk; + // 查询管理数据 + $data = $this->eagerlyManyToMany([ + ['pivot.' . $this->localKey, '=', $pk], + ], $relation, $subRelation, $closure); + + // 关联数据封装 + if (!isset($data[$pk])) { + $data[$pk] = []; + } + + $result->setRelation(Loader::parseName($relation), $this->resultSetBuild($data[$pk])); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount($result, $closure, $aggregate = 'count', $field = '*', &$name = '') + { + $pk = $result->getPk(); + + if (!isset($result->$pk)) { + return 0; + } + + $pk = $result->$pk; + + if ($closure instanceof Closure) { + $return = $closure($this->query); + + if ($return && is_string($return)) { + $name = $return; + } + } + + return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [ + ['pivot.' . $this->localKey, '=', $pk], + ])->$aggregate($field); + } + + /** + * 获取关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $aggregateAlias 聚合字段别名 + * @return array + */ + public function getRelationCountQuery($closure, $aggregate = 'count', $field = '*', &$aggregateAlias = '') + { + if ($closure instanceof Closure) { + $return = $closure($this->query); + + if ($return && is_string($return)) { + $aggregateAlias = $return; + } + } + + return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [ + [ + 'pivot.' . $this->localKey, 'exp', $this->query->raw('=' . $this->parent->getTable() . '.' . $this->parent->getPk()), + ], + ])->fetchSql()->$aggregate($field); + } + + /** + * 多对多 关联模型预查询 + * @access protected + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param \Closure $closure 闭包 + * @return array + */ + protected function eagerlyManyToMany($where, $relation, $subRelation = '', $closure = null) + { + // 预载入关联查询 支持嵌套预载入 + if ($closure instanceof Closure) { + $closure($this->query); + } + + $list = $this->belongsToManyQuery($this->foreignKey, $this->localKey, $where) + ->with($subRelation) + ->select(); + + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $pivot = []; + foreach ($set->getData() as $key => $val) { + if (strpos($key, '__')) { + list($name, $attr) = explode('__', $key, 2); + if ('pivot' == $name) { + $pivot[$attr] = $val; + unset($set->$key); + } + } + } + + $set->setRelation($this->pivotDataName, $this->newPivot($pivot, true)); + + $data[$pivot[$this->localKey]][] = $set; + } + + return $data; + } + + /** + * BELONGS TO MANY 关联查询 + * @access protected + * @param string $foreignKey 关联模型关联键 + * @param string $localKey 当前模型关联键 + * @param array $condition 关联查询条件 + * @return Query + */ + protected function belongsToManyQuery($foreignKey, $localKey, $condition = []) + { + // 关联查询封装 + $tableName = $this->query->getTable(); + $table = $this->pivot->getTable(); + $fields = $this->getQueryFields($tableName); + + $query = $this->query + ->field($fields) + ->field(true, false, $table, 'pivot', 'pivot__'); + + if (empty($this->baseQuery)) { + $relationFk = $this->query->getPk(); + $query->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk) + ->where($condition); + } + + return $query; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @param array $pivot 中间表额外数据 + * @return array|Pivot + */ + public function save($data, array $pivot = []) + { + // 保存关联表/中间表数据 + return $this->attach($data, $pivot); + } + + /** + * 批量保存当前关联数据对象 + * @access public + * @param array $dataSet 数据集 + * @param array $pivot 中间表额外数据 + * @param bool $samePivot 额外数据是否相同 + * @return array|false + */ + public function saveAll(array $dataSet, array $pivot = [], $samePivot = false) + { + $result = []; + + foreach ($dataSet as $key => $data) { + if (!$samePivot) { + $pivotData = isset($pivot[$key]) ? $pivot[$key] : []; + } else { + $pivotData = $pivot; + } + + $result[] = $this->attach($data, $pivotData); + } + + return empty($result) ? false : $result; + } + + /** + * 附加关联的一个中间表数据 + * @access public + * @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键 + * @param array $pivot 中间表额外数据 + * @return array|Pivot + * @throws Exception + */ + public function attach($data, $pivot = []) + { + if (is_array($data)) { + if (key($data) === 0) { + $id = $data; + } else { + // 保存关联表数据 + $model = new $this->model; + $id = $model->insertGetId($data); + } + } elseif (is_numeric($data) || is_string($data)) { + // 根据关联表主键直接写入中间表 + $id = $data; + } elseif ($data instanceof Model) { + // 根据关联表主键直接写入中间表 + $relationFk = $data->getPk(); + $id = $data->$relationFk; + } + + if ($id) { + // 保存中间表数据 + $pk = $this->parent->getPk(); + $pivot[$this->localKey] = $this->parent->$pk; + $ids = (array) $id; + + foreach ($ids as $id) { + $pivot[$this->foreignKey] = $id; + $this->pivot->replace() + ->exists(false) + ->data([]) + ->save($pivot); + $result[] = $this->newPivot($pivot, true); + } + + if (count($result) == 1) { + // 返回中间表模型对象 + $result = $result[0]; + } + + return $result; + } else { + throw new Exception('miss relation data'); + } + } + + /** + * 判断是否存在关联数据 + * @access public + * @param mixed $data 数据 可以使用关联模型对象 或者 关联对象的主键 + * @return Pivot|false + * @throws Exception + */ + public function attached($data) + { + if ($data instanceof Model) { + $id = $data->getKey(); + } else { + $id = $data; + } + + $pivot = $this->pivot + ->where($this->localKey, $this->parent->getKey()) + ->where($this->foreignKey, $id) + ->find(); + + return $pivot ?: false; + } + + /** + * 解除关联的一个中间表数据 + * @access public + * @param integer|array $data 数据 可以使用关联对象的主键 + * @param bool $relationDel 是否同时删除关联表数据 + * @return integer + */ + public function detach($data = null, $relationDel = false) + { + if (is_array($data)) { + $id = $data; + } elseif (is_numeric($data) || is_string($data)) { + // 根据关联表主键直接写入中间表 + $id = $data; + } elseif ($data instanceof Model) { + // 根据关联表主键直接写入中间表 + $relationFk = $data->getPk(); + $id = $data->$relationFk; + } + + // 删除中间表数据 + $pk = $this->parent->getPk(); + $pivot[] = [$this->localKey, '=', $this->parent->$pk]; + + if (isset($id)) { + $pivot[] = [$this->foreignKey, is_array($id) ? 'in' : '=', $id]; + } + + $result = $this->pivot->where($pivot)->delete(); + + // 删除关联表数据 + if (isset($id) && $relationDel) { + $model = $this->model; + $model::destroy($id); + } + + return $result; + } + + /** + * 数据同步 + * @access public + * @param array $ids + * @param bool $detaching + * @return array + */ + public function sync($ids, $detaching = true) + { + $changes = [ + 'attached' => [], + 'detached' => [], + 'updated' => [], + ]; + + $pk = $this->parent->getPk(); + + $current = $this->pivot + ->where($this->localKey, $this->parent->$pk) + ->column($this->foreignKey); + + $records = []; + + foreach ($ids as $key => $value) { + if (!is_array($value)) { + $records[$value] = []; + } else { + $records[$key] = $value; + } + } + + $detach = array_diff($current, array_keys($records)); + + if ($detaching && count($detach) > 0) { + $this->detach($detach); + $changes['detached'] = $detach; + } + + foreach ($records as $id => $attributes) { + if (!in_array($id, $current)) { + $this->attach($id, $attributes); + $changes['attached'][] = $id; + } elseif (count($attributes) > 0 && $this->attach($id, $attributes)) { + $changes['updated'][] = $id; + } + } + + return $changes; + } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + 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->baseQuery = true; + } + } + +} diff --git a/vendor/topthink/framework/library/think/model/relation/HasMany.php b/vendor/topthink/framework/library/think/model/relation/HasMany.php new file mode 100644 index 0000000..e4df5c4 --- /dev/null +++ b/vendor/topthink/framework/library/think/model/relation/HasMany.php @@ -0,0 +1,360 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\db\Query; +use think\Loader; +use think\Model; +use think\model\Relation; + +class HasMany extends Relation +{ + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前模型主键 + */ + public function __construct(Model $parent, $model, $foreignKey, $localKey) + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + $this->query = (new $model)->db(); + + if (get_class($parent) == $model) { + $this->selfRelation = true; + } + } + + /** + * 延迟获取关联数据 + * @access public + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return \think\Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure instanceof Closure) { + $closure($this->query); + } + + $list = $this->query + ->where($this->foreignKey, $this->parent->{$this->localKey}) + ->relation($subRelation) + ->select(); + + $parent = clone $this->parent; + + foreach ($list as &$model) { + $model->setParent($parent); + } + + return $list; + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $range = []; + + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$localKey)) { + $range[] = $result->$localKey; + } + } + + if (!empty($range)) { + $where = [ + [$this->foreignKey, 'in', $range], + ]; + $data = $this->eagerlyOneToMany($where, $relation, $subRelation, $closure); + + // 关联属性名 + $attr = Loader::parseName($relation); + + // 关联数据封装 + foreach ($resultSet as $result) { + $pk = $result->$localKey; + if (!isset($data[$pk])) { + $data[$pk] = []; + } + + foreach ($data[$pk] as &$relationModel) { + $relationModel->setParent(clone $result); + } + + $result->setRelation($attr, $this->resultSetBuild($data[$pk])); + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + + if (isset($result->$localKey)) { + $pk = $result->$localKey; + $where = [ + [$this->foreignKey, '=', $pk], + ]; + $data = $this->eagerlyOneToMany($where, $relation, $subRelation, $closure); + + // 关联数据封装 + if (!isset($data[$pk])) { + $data[$pk] = []; + } + + foreach ($data[$pk] as &$relationModel) { + $relationModel->setParent(clone $result); + } + + $result->setRelation(Loader::parseName($relation), $this->resultSetBuild($data[$pk])); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount($result, $closure, $aggregate = 'count', $field = '*', &$name = '') + { + $localKey = $this->localKey; + + if (!isset($result->$localKey)) { + return 0; + } + + if ($closure instanceof Closure) { + $return = $closure($this->query); + if ($return && is_string($return)) { + $name = $return; + } + } + + return $this->query + ->where($this->foreignKey, '=', $result->$localKey) + ->$aggregate($field); + } + + /** + * 创建关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $aggregateAlias 聚合字段别名 + * @return string + */ + public function getRelationCountQuery($closure, $aggregate = 'count', $field = '*', &$aggregateAlias = '') + { + if ($closure instanceof Closure) { + $return = $closure($this->query); + + if ($return && is_string($return)) { + $aggregateAlias = $return; + } + } + + return $this->query->alias($aggregate . '_table') + ->whereExp($aggregate . '_table.' . $this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey) + ->fetchSql() + ->$aggregate($field); + } + + /** + * 一对多 关联模型预查询 + * @access public + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param \Closure $closure + * @return array + */ + protected function eagerlyOneToMany($where, $relation, $subRelation = '', $closure = null) + { + $foreignKey = $this->foreignKey; + + $this->query->removeWhereField($this->foreignKey); + + // 预载入关联查询 支持嵌套预载入 + if ($closure instanceof Closure) { + $closure($this->query); + } + + $list = $this->query->where($where)->with($subRelation)->select(); + + // 组装模型数据 + $data = []; + + foreach ($list as $set) { + $data[$set->$foreignKey][] = $set; + } + + return $data; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @param boolean $replace 是否自动识别更新和写入 + * @return Model|false + */ + public function save($data, $replace = true) + { + $model = $this->make(); + + return $model->replace($replace)->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 + * @param array|\think\Collection $dataSet 数据集 + * @param boolean $replace 是否自动识别更新和写入 + * @return array|false + */ + public function saveAll($dataSet, $replace = true) + { + $result = []; + + foreach ($dataSet as $key => $data) { + $result[] = $this->save($data, $replace); + } + + return empty($result) ? false : $result; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + $table = $this->query->getTable(); + $model = basename(str_replace('\\', '/', get_class($this->parent))); + $relation = basename(str_replace('\\', '/', $this->model)); + $softDelete = $this->query->getOptions('soft_delete'); + + return $this->parent->db() + ->alias($model) + ->field($model . '.*') + ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $joinType) + ->when($softDelete, function ($query) use ($softDelete, $relation) { + $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }) + ->group($relation . '.' . $this->foreignKey) + ->having('count(' . $id . ')' . $operator . $count); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + 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)) { + $this->getQueryWhere($where, $relation); + } + + $fields = $this->getRelationQueryFields($fields, $model); + $softDelete = $this->query->getOptions('soft_delete'); + + return $this->parent->db() + ->alias($model) + ->group($model . '.' . $this->localKey) + ->field($fields) + ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey) + ->when($softDelete, function ($query) use ($softDelete, $relation) { + $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }) + ->where($where); + } + + /** + * 执行基础查询(仅执行一次) + * @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/vendor/topthink/framework/library/think/model/relation/HasManyThrough.php b/vendor/topthink/framework/library/think/model/relation/HasManyThrough.php new file mode 100644 index 0000000..be0b0cd --- /dev/null +++ b/vendor/topthink/framework/library/think/model/relation/HasManyThrough.php @@ -0,0 +1,363 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\db\Query; +use think\Loader; +use think\Model; +use think\model\Relation; + +class HasManyThrough extends Relation +{ + // 中间关联表外键 + protected $throughKey; + // 中间表模型 + protected $through; + + /** + * 中间主键 + * @var string + */ + protected $throughPk; + + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $through 中间模型名 + * @param string $foreignKey 关联外键 + * @param string $throughKey 关联外键 + * @param string $localKey 当前主键 + */ + public function __construct(Model $parent, $model, $through, $foreignKey, $throughKey, $localKey) + { + $this->parent = $parent; + $this->model = $model; + $this->through = (new $through)->db(); + $this->foreignKey = $foreignKey; + $this->throughKey = $throughKey; + $this->throughPk = $this->through->getPk(); + $this->localKey = $localKey; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @access public + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return \think\Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure instanceof Closure) { + $closure($this->query); + } + + $this->baseQuery(); + + return $this->query->relation($subRelation)->select(); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + $model = Loader::parseName(basename(str_replace('\\', '/', get_class($this->parent)))); + $throughTable = $this->through->getTable(); + $pk = $this->throughPk; + $throughKey = $this->throughKey; + $relation = (new $this->model)->db(); + $relationTable = $relation->getTable(); + $softDelete = $this->query->getOptions('soft_delete'); + + if ('*' != $id) { + $id = $relationTable . '.' . $relation->getPk(); + } + + return $this->parent->db() + ->alias($model) + ->field($model . '.*') + ->join($throughTable, $throughTable . '.' . $this->foreignKey . '=' . $model . '.' . $this->localKey) + ->join($relationTable, $relationTable . '.' . $throughKey . '=' . $throughTable . '.' . $this->throughPk) + ->when($softDelete, function ($query) use ($softDelete, $relationTable) { + $query->where($relationTable . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }) + ->group($relationTable . '.' . $this->throughKey) + ->having('count(' . $id . ')' . $operator . $count); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + public function hasWhere($where = [], $fields = null) + { + $model = Loader::parseName(basename(str_replace('\\', '/', get_class($this->parent)))); + $throughTable = $this->through->getTable(); + $pk = $this->throughPk; + $throughKey = $this->throughKey; + $modelTable = (new $this->model)->db()->getTable(); + + if (is_array($where)) { + $this->getQueryWhere($where, $modelTable); + } + + $fields = $this->getRelationQueryFields($fields, $model); + $softDelete = $this->query->getOptions('soft_delete'); + + return $this->parent->db() + ->alias($model) + ->join($throughTable, $throughTable . '.' . $this->foreignKey . '=' . $model . '.' . $this->localKey) + ->join($modelTable, $modelTable . '.' . $throughKey . '=' . $throughTable . '.' . $this->throughPk) + ->when($softDelete, function ($query) use ($softDelete, $modelTable) { + $query->where($modelTable . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }) + ->group($modelTable . '.' . $this->throughKey) + ->where($where) + ->field($fields); + } + + /** + * 预载入关联查询(数据集) + * @access protected + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param mixed $subRelation 子关联名 + * @param Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(array &$resultSet, $relation, $subRelation = '', $closure = null) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$localKey)) { + $range[] = $result->$localKey; + } + } + + if (!empty($range)) { + $this->query->removeWhereField($foreignKey); + + $data = $this->eagerlyWhere([ + [$this->foreignKey, 'in', $range], + ], $foreignKey, $relation, $subRelation, $closure); + + // 关联属性名 + $attr = Loader::parseName($relation); + + // 关联数据封装 + foreach ($resultSet as $result) { + $pk = $result->$localKey; + if (!isset($data[$pk])) { + $data[$pk] = []; + } + + foreach ($data[$pk] as &$relationModel) { + $relationModel->setParent(clone $result); + } + + // 设置关联属性 + $result->setRelation($attr, $this->resultSetBuild($data[$pk])); + } + } + } + + /** + * 预载入关联查询(数据) + * @access protected + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param mixed $subRelation 子关联名 + * @param Closure $closure 闭包 + * @return void + */ + public function eagerlyResult($result, $relation, $subRelation = '', $closure = null) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + $pk = $result->$localKey; + + $this->query->removeWhereField($foreignKey); + + $data = $this->eagerlyWhere([ + [$foreignKey, '=', $pk], + ], $foreignKey, $relation, $subRelation, $closure); + + // 关联数据封装 + if (!isset($data[$pk])) { + $data[$pk] = []; + } + + foreach ($data[$pk] as &$relationModel) { + $relationModel->setParent(clone $result); + } + + $result->setRelation(Loader::parseName($relation), $this->resultSetBuild($data[$pk])); + } + + /** + * 关联模型预查询 + * @access public + * @param array $where 关联预查询条件 + * @param string $key 关联键名 + * @param string $relation 关联名 + * @param mixed $subRelation 子关联 + * @param Closure $closure + * @return array + */ + protected function eagerlyWhere(array $where, $key, $relation, $subRelation = '', $closure = null) + { + // 预载入关联查询 支持嵌套预载入 + $throughList = $this->through->where($where)->select(); + $keys = $throughList->column($this->throughPk, $this->throughPk); + + if ($closure instanceof Closure) { + $closure($this->query); + } + + $list = $this->query->where($this->throughKey, 'in', $keys)->select(); + + // 组装模型数据 + $data = []; + $keys = $throughList->column($this->foreignKey, $this->throughPk); + + foreach ($list as $set) { + $data[$keys[$set->{$this->throughKey}]][] = $set; + } + + return $data; + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount($result, $closure, $aggregate = 'count', $field = '*', &$name = null) + { + $localKey = $this->localKey; + + if (!isset($result->$localKey)) { + return 0; + } + + if ($closure instanceof Closure) { + $return = $closure($this->query); + if ($return && is_string($return)) { + $name = $return; + } + } + + $alias = Loader::parseName(basename(str_replace('\\', '/', $this->model))); + $throughTable = $this->through->getTable(); + $pk = $this->throughPk; + $throughKey = $this->throughKey; + $modelTable = $this->parent->getTable(); + + if (false === strpos($field, '.')) { + $field = $alias . '.' . $field; + } + + return $this->query + ->alias($alias) + ->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey) + ->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey) + ->where($throughTable . '.' . $this->foreignKey, $result->$localKey) + ->$aggregate($field); + } + + /** + * 创建关联统计子查询 + * @access public + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return string + */ + public function getRelationCountQuery($closure = null, $aggregate = 'count', $field = '*', &$name = null) + { + if ($closure instanceof Closure) { + $return = $closure($this->query); + if ($return && is_string($return)) { + $name = $return; + } + } + + $alias = Loader::parseName(basename(str_replace('\\', '/', $this->model))); + $throughTable = $this->through->getTable(); + $pk = $this->throughPk; + $throughKey = $this->throughKey; + $modelTable = $this->parent->getTable(); + + if (false === strpos($field, '.')) { + $field = $alias . '.' . $field; + } + + return $this->query + ->alias($alias) + ->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey) + ->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey) + ->whereExp($throughTable . '.' . $this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey) + ->fetchSql() + ->$aggregate($field); + } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $alias = Loader::parseName(basename(str_replace('\\', '/', $this->model))); + $throughTable = $this->through->getTable(); + $pk = $this->throughPk; + $throughKey = $this->throughKey; + $modelTable = $this->parent->getTable(); + $fields = $this->getQueryFields($alias); + + $this->query + ->field($fields) + ->alias($alias) + ->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey) + ->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey) + ->where($throughTable . '.' . $this->foreignKey, $this->parent->{$this->localKey}); + + $this->baseQuery = true; + } + } + +} diff --git a/vendor/topthink/framework/library/think/model/relation/HasOne.php b/vendor/topthink/framework/library/think/model/relation/HasOne.php new file mode 100644 index 0000000..fe09443 --- /dev/null +++ b/vendor/topthink/framework/library/think/model/relation/HasOne.php @@ -0,0 +1,294 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\db\Query; +use think\Loader; +use think\Model; + +class HasOne extends OneToOne +{ + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前模型主键 + */ + public function __construct(Model $parent, $model, $foreignKey, $localKey) + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + $this->joinType = 'INNER'; + $this->query = (new $model)->db(); + + if (get_class($parent) == $model) { + $this->selfRelation = true; + } + } + + /** + * 延迟获取关联数据 + * @access public + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return Model + */ + public function getRelation($subRelation = '', $closure = null) + { + $localKey = $this->localKey; + + if ($closure instanceof Closure) { + $closure($this->query); + } + + // 判断关联类型执行查询 + $relationModel = $this->query + ->removeWhereField($this->foreignKey) + ->where($this->foreignKey, $this->parent->$localKey) + ->relation($subRelation) + ->find(); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } + + return $relationModel; + } + + /** + * 创建关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $aggregateAlias 聚合字段别名 + * @return string + */ + public function getRelationCountQuery($closure, $aggregate = 'count', $field = '*', &$aggregateAlias = '') + { + if ($closure instanceof Closure) { + $return = $closure($this->query); + + if ($return && is_string($return)) { + $aggregateAlias = $return; + } + } + + return $this->query + ->whereExp($this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey) + ->fetchSql() + ->$aggregate($field); + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount($result, $closure, $aggregate = 'count', $field = '*', &$name = '') + { + $localKey = $this->localKey; + + if (!isset($result->$localKey)) { + return 0; + } + + if ($closure instanceof Closure) { + $return = $closure($this->query); + if ($return && is_string($return)) { + $name = $return; + } + } + + return $this->query + ->where($this->foreignKey, '=', $result->$localKey) + ->$aggregate($field); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + $table = $this->query->getTable(); + $model = basename(str_replace('\\', '/', get_class($this->parent))); + $relation = basename(str_replace('\\', '/', $this->model)); + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + $softDelete = $this->query->getOptions('soft_delete'); + + return $this->parent->db() + ->alias($model) + ->whereExists(function ($query) use ($table, $model, $relation, $localKey, $foreignKey, $softDelete) { + $query->table([$table => $relation]) + ->field($relation . '.' . $foreignKey) + ->whereExp($model . '.' . $localKey, '=' . $relation . '.' . $foreignKey) + ->when($softDelete, function ($query) use ($softDelete, $relation) { + $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }); + }); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + 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)) { + $this->getQueryWhere($where, $relation); + } + + $fields = $this->getRelationQueryFields($fields, $model); + $softDelete = $this->query->getOptions('soft_delete'); + + return $this->parent->db() + ->alias($model) + ->field($fields) + ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $this->joinType) + ->when($softDelete, function ($query) use ($softDelete, $relation) { + $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }) + ->where($where); + } + + /** + * 预载入关联查询(数据集) + * @access protected + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$localKey)) { + $range[] = $result->$localKey; + } + } + + if (!empty($range)) { + $this->query->removeWhereField($foreignKey); + + $data = $this->eagerlyWhere([ + [$foreignKey, 'in', $range], + ], $foreignKey, $relation, $subRelation, $closure); + + // 关联属性名 + $attr = Loader::parseName($relation); + + // 关联数据封装 + foreach ($resultSet as $result) { + // 关联模型 + if (!isset($data[$result->$localKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$localKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($relationModel, $result, $this->bindAttr); + } else { + // 设置关联属性 + $result->setRelation($attr, $relationModel); + } + } + } + } + + /** + * 预载入关联查询(数据) + * @access protected + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + protected function eagerlyOne(&$result, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $this->query->removeWhereField($foreignKey); + + $data = $this->eagerlyWhere([ + [$foreignKey, '=', $result->$localKey], + ], $foreignKey, $relation, $subRelation, $closure); + + // 关联模型 + if (!isset($data[$result->$localKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$localKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + 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/vendor/topthink/framework/library/think/model/relation/MorphMany.php b/vendor/topthink/framework/library/think/model/relation/MorphMany.php new file mode 100644 index 0000000..d2af66e --- /dev/null +++ b/vendor/topthink/framework/library/think/model/relation/MorphMany.php @@ -0,0 +1,342 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +class MorphMany extends Relation +{ + // 多态字段 + protected $morphKey; + protected $morphType; + // 多态类型 + protected $type; + + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $morphKey 关联外键 + * @param string $morphType 多态字段名 + * @param string $type 多态类型 + */ + public function __construct(Model $parent, $model, $morphKey, $morphType, $type) + { + $this->parent = $parent; + $this->model = $model; + $this->type = $type; + $this->morphKey = $morphKey; + $this->morphType = $morphType; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @access public + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return \think\Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure instanceof Closure) { + $closure($this->query); + } + + $this->baseQuery(); + + $list = $this->query->relation($subRelation)->select(); + $parent = clone $this->parent; + + foreach ($list as &$model) { + $model->setParent($parent); + } + + return $list; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + throw new Exception('relation not support: has'); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + public function hasWhere($where = [], $fields = null) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $morphType = $this->morphType; + $morphKey = $this->morphKey; + $type = $this->type; + $range = []; + + foreach ($resultSet as $result) { + $pk = $result->getPk(); + // 获取关联外键列表 + if (isset($result->$pk)) { + $range[] = $result->$pk; + } + } + + if (!empty($range)) { + $where = [ + [$morphKey, 'in', $range], + [$morphType, '=', $type], + ]; + $data = $this->eagerlyMorphToMany($where, $relation, $subRelation, $closure); + + // 关联属性名 + $attr = Loader::parseName($relation); + + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$pk])) { + $data[$result->$pk] = []; + } + + foreach ($data[$result->$pk] as &$relationModel) { + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + $result->setRelation($attr, $this->resultSetBuild($data[$result->$pk])); + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $pk = $result->getPk(); + + if (isset($result->$pk)) { + $key = $result->$pk; + $where = [ + [$this->morphKey, '=', $key], + [$this->morphType, '=', $this->type], + ]; + $data = $this->eagerlyMorphToMany($where, $relation, $subRelation, $closure); + + if (!isset($data[$key])) { + $data[$key] = []; + } + + foreach ($data[$key] as &$relationModel) { + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + $result->setRelation(Loader::parseName($relation), $this->resultSetBuild($data[$key])); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount($result, $closure, $aggregate = 'count', $field = '*', &$name = '') + { + $pk = $result->getPk(); + + if (!isset($result->$pk)) { + return 0; + } + + if ($closure instanceof Closure) { + $return = $closure($this->query); + + if ($return && is_string($return)) { + $name = $return; + } + } + + return $this->query + ->where([ + [$this->morphKey, '=', $result->$pk], + [$this->morphType, '=', $this->type], + ]) + ->$aggregate($field); + } + + /** + * 获取关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $aggregateAlias 聚合字段别名 + * @return string + */ + public function getRelationCountQuery($closure, $aggregate = 'count', $field = '*', &$aggregateAlias = '') + { + if ($closure instanceof Closure) { + $return = $closure($this->query); + + if ($return && is_string($return)) { + $aggregateAlias = $return; + } + } + + return $this->query + ->whereExp($this->morphKey, '=' . $this->parent->getTable() . '.' . $this->parent->getPk()) + ->where($this->morphType, '=', $this->type) + ->fetchSql() + ->$aggregate($field); + } + + /** + * 多态一对多 关联模型预查询 + * @access protected + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param \Closure $closure 闭包 + * @return array + */ + protected function eagerlyMorphToMany($where, $relation, $subRelation = '', $closure = null) + { + // 预载入关联查询 支持嵌套预载入 + $this->query->removeOption('where'); + + if ($closure instanceof Closure) { + $closure($this->query); + } + + $list = $this->query->where($where)->with($subRelation)->select(); + $morphKey = $this->morphKey; + + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $data[$set->$morphKey][] = $set; + } + + return $data; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 + * @return Model|false + */ + public function save($data) + { + $model = $this->make(); + + return $model->save($data) ? $model : false; + } + + /** + * 创建关联对象实例 + * @param array $data + * @return Model + */ + public function make($data = []) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + // 保存关联表数据 + $pk = $this->parent->getPk(); + + $data[$this->morphKey] = $this->parent->$pk; + $data[$this->morphType] = $this->type; + + return new $this->model($data); + } + + /** + * 批量保存当前关联数据对象 + * @access public + * @param array $dataSet 数据集 + * @return array|false + */ + public function saveAll(array $dataSet) + { + $result = []; + + foreach ($dataSet as $key => $data) { + $result[] = $this->save($data); + } + + return empty($result) ? false : $result; + } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $pk = $this->parent->getPk(); + + $this->query->where([ + [$this->morphKey, '=', $this->parent->$pk], + [$this->morphType, '=', $this->type], + ]); + + $this->baseQuery = true; + } + } + +} diff --git a/vendor/topthink/framework/library/think/model/relation/MorphOne.php b/vendor/topthink/framework/library/think/model/relation/MorphOne.php new file mode 100644 index 0000000..6bc205c --- /dev/null +++ b/vendor/topthink/framework/library/think/model/relation/MorphOne.php @@ -0,0 +1,257 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +class MorphOne extends Relation +{ + // 多态字段 + protected $morphKey; + protected $morphType; + // 多态类型 + protected $type; + + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $morphKey 关联外键 + * @param string $morphType 多态字段名 + * @param string $type 多态类型 + */ + public function __construct(Model $parent, $model, $morphKey, $morphType, $type) + { + $this->parent = $parent; + $this->model = $model; + $this->type = $type; + $this->morphKey = $morphKey; + $this->morphType = $morphType; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @access public + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return Model + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure instanceof Closure) { + $closure($this->query); + } + + $this->baseQuery(); + + $relationModel = $this->query->relation($subRelation)->find(); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } + + return $relationModel; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + public function hasWhere($where = [], $fields = null) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $morphType = $this->morphType; + $morphKey = $this->morphKey; + $type = $this->type; + $range = []; + + foreach ($resultSet as $result) { + $pk = $result->getPk(); + // 获取关联外键列表 + if (isset($result->$pk)) { + $range[] = $result->$pk; + } + } + + if (!empty($range)) { + $data = $this->eagerlyMorphToOne([ + [$morphKey, 'in', $range], + [$morphType, '=', $type], + ], $relation, $subRelation, $closure); + + // 关联属性名 + $attr = Loader::parseName($relation); + + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$pk])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$pk]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + $result->setRelation($attr, $relationModel); + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $pk = $result->getPk(); + + if (isset($result->$pk)) { + $pk = $result->$pk; + $data = $this->eagerlyMorphToOne([ + [$this->morphKey, '=', $pk], + [$this->morphType, '=', $this->type], + ], $relation, $subRelation, $closure); + + if (isset($data[$pk])) { + $relationModel = $data[$pk]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } else { + $relationModel = null; + } + + $result->setRelation(Loader::parseName($relation), $relationModel); + } + } + + /** + * 多态一对一 关联模型预查询 + * @access protected + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param \Closure $closure 闭包 + * @return array + */ + protected function eagerlyMorphToOne($where, $relation, $subRelation = '', $closure = null) + { + // 预载入关联查询 支持嵌套预载入 + if ($closure instanceof Closure) { + $closure($this->query); + } + + $list = $this->query->where($where)->with($subRelation)->select(); + $morphKey = $this->morphKey; + + // 组装模型数据 + $data = []; + + foreach ($list as $set) { + $data[$set->$morphKey] = $set; + } + + return $data; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 + * @return Model|false + */ + public function save($data) + { + $model = $this->make(); + return $model->save($data) ? $model : false; + } + + /** + * 创建关联对象实例 + * @param array $data + * @return Model + */ + public function make($data = []) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + // 保存关联表数据 + $pk = $this->parent->getPk(); + + $data[$this->morphKey] = $this->parent->$pk; + $data[$this->morphType] = $this->type; + + return new $this->model($data); + } + + /** + * 执行基础查询(进执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $pk = $this->parent->getPk(); + + $this->query->where([ + [$this->morphKey, '=', $this->parent->$pk], + [$this->morphType, '=', $this->type], + ]); + $this->baseQuery = true; + } + } + +} diff --git a/vendor/topthink/framework/library/think/model/relation/MorphTo.php b/vendor/topthink/framework/library/think/model/relation/MorphTo.php new file mode 100644 index 0000000..0786c2f --- /dev/null +++ b/vendor/topthink/framework/library/think/model/relation/MorphTo.php @@ -0,0 +1,316 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +class MorphTo extends Relation +{ + // 多态字段 + protected $morphKey; + protected $morphType; + // 多态别名 + protected $alias; + // 关联名 + protected $relation; + + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $morphType 多态字段名 + * @param string $morphKey 外键名 + * @param array $alias 多态别名定义 + * @param string $relation 关联名 + */ + public function __construct(Model $parent, $morphType, $morphKey, $alias = [], $relation = null) + { + $this->parent = $parent; + $this->morphType = $morphType; + $this->morphKey = $morphKey; + $this->alias = $alias; + $this->relation = $relation; + } + + /** + * 获取当前的关联模型类的实例 + * @access public + * @return Model + */ + public function getModel() + { + $morphType = $this->morphType; + $model = $this->parseModel($this->parent->$morphType); + + return (new $model); + } + + /** + * 延迟获取关联数据 + * @access public + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return Model + */ + public function getRelation($subRelation = '', $closure = null) + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + + // 多态模型 + $model = $this->parseModel($this->parent->$morphType); + + // 主键数据 + $pk = $this->parent->$morphKey; + + $relationModel = (new $model)->relation($subRelation)->find($pk); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } + + return $relationModel; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + public function hasWhere($where = [], $fields = null) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 解析模型的完整命名空间 + * @access protected + * @param string $model 模型名(或者完整类名) + * @return string + */ + protected function parseModel($model) + { + if (isset($this->alias[$model])) { + $model = $this->alias[$model]; + } + + if (false === strpos($model, '\\')) { + $path = explode('\\', get_class($this->parent)); + array_pop($path); + array_push($path, Loader::parseName($model, 1)); + $model = implode('\\', $path); + } + + return $model; + } + + /** + * 设置多态别名 + * @access public + * @param array $alias 别名定义 + * @return $this + */ + public function setAlias($alias) + { + $this->alias = $alias; + + return $this; + } + + /** + * 移除关联查询参数 + * @access public + * @return $this + */ + public function removeOption() + { + return $this; + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + * @throws Exception + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + $range = []; + + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (!empty($result->$morphKey)) { + $range[$result->$morphType][] = $result->$morphKey; + } + } + + if (!empty($range)) { + // 关联属性名 + $attr = Loader::parseName($relation); + + foreach ($range as $key => $val) { + // 多态类型映射 + $model = $this->parseModel($key); + $obj = (new $model)->db(); + $pk = $obj->getPk(); + // 预载入关联查询 支持嵌套预载入 + if ($closure instanceof \Closure) { + $closure($obj); + + if ($field = $obj->getOptions('with_field')) { + $obj->field($field)->removeOption('with_field'); + } + } + $list = $obj->all($val, $subRelation); + $data = []; + + foreach ($list as $k => $vo) { + $data[$vo->$pk] = $vo; + } + + foreach ($resultSet as $result) { + if ($key == $result->$morphType) { + // 关联模型 + if (!isset($data[$result->$morphKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$morphKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + $result->setRelation($attr, $relationModel); + } + } + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + // 多态类型映射 + $model = $this->parseModel($result->{$this->morphType}); + + $this->eagerlyMorphToOne($model, $relation, $result, $subRelation); + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount($result, $closure, $aggregate = 'count', $field = '*', &$name = '') + {} + + /** + * 多态MorphTo 关联模型预查询 + * @access protected + * @param string $model 关联模型对象 + * @param string $relation 关联名 + * @param Model $result + * @param string $subRelation 子关联 + * @return void + */ + protected function eagerlyMorphToOne($model, $relation, &$result, $subRelation = '') + { + // 预载入关联查询 支持嵌套预载入 + $pk = $this->parent->{$this->morphKey}; + $data = (new $model)->with($subRelation)->find($pk); + + if ($data) { + $data->setParent(clone $result); + $data->isUpdate(true); + } + + $result->setRelation(Loader::parseName($relation), $data ?: null); + } + + /** + * 添加关联数据 + * @access public + * @param Model $model 关联模型对象 + * @param string $type 多态类型 + * @return 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, $type ?: get_class($model)); + $this->parent->save(); + + return $this->parent->setRelation($this->relation, $model); + } + + /** + * 注销关联数据 + * @access public + * @return Model + */ + public function dissociate() + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + + $this->parent->setAttr($morphKey, null); + $this->parent->setAttr($morphType, null); + $this->parent->save(); + + return $this->parent->setRelation($this->relation, null); + } + +} diff --git a/vendor/topthink/framework/library/think/model/relation/OneToOne.php b/vendor/topthink/framework/library/think/model/relation/OneToOne.php new file mode 100644 index 0000000..5e22b80 --- /dev/null +++ b/vendor/topthink/framework/library/think/model/relation/OneToOne.php @@ -0,0 +1,337 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +/** + * Class OneToOne + * @package think\model\relation + * + */ +abstract class OneToOne extends Relation +{ + // 预载入方式 0 -JOIN 1 -IN + protected $eagerlyType = 1; + // 当前关联的JOIN类型 + protected $joinType; + // 要绑定的属性 + protected $bindAttr = []; + // 关联名 + protected $relation; + + /** + * 设置join类型 + * @access public + * @param string $type JOIN类型 + * @return $this + */ + public function joinType($type) + { + $this->joinType = $type; + return $this; + } + + /** + * 预载入关联查询(JOIN方式) + * @access public + * @param Query $query 查询对象 + * @param string $relation 关联名 + * @param mixed $field 关联字段 + * @param string $joinType JOIN方式 + * @param \Closure $closure 闭包条件 + * @param bool $first + * @return void + */ + public function eagerly(Query $query, $relation, $field, $joinType, $closure, $first) + { + $name = Loader::parseName(basename(str_replace('\\', '/', get_class($this->parent)))); + + if ($first) { + $table = $query->getTable(); + $query->table([$table => $name]); + + if ($query->getOptions('field')) { + $masterField = $query->getOptions('field'); + $query->removeOption('field'); + } else { + $masterField = true; + } + + $query->field($masterField, false, $table, $name); + } + + // 预载入封装 + $joinTable = $this->query->getTable(); + $joinAlias = $relation; + $joinType = $joinType ?: $this->joinType; + + $query->via($joinAlias); + + if ($this instanceof BelongsTo) { + $joinOn = $name . '.' . $this->foreignKey . '=' . $joinAlias . '.' . $this->localKey; + } else { + $joinOn = $name . '.' . $this->localKey . '=' . $joinAlias . '.' . $this->foreignKey; + } + + if ($closure instanceof Closure) { + // 执行闭包查询 + $closure($query); + // 使用withField指定获取关联的字段,如 + // $query->where(['id'=>1])->withField('id,name'); + if ($query->getOptions('with_field')) { + $field = $query->getOptions('with_field'); + $query->removeOption('with_field'); + } + } + + $query->join([$joinTable => $joinAlias], $joinOn, $joinType) + ->field($field, false, $joinTable, $joinAlias, $relation . '__'); + } + + /** + * 预载入关联查询(数据集) + * @access protected + * @param array $resultSet + * @param string $relation + * @param string $subRelation + * @param \Closure $closure + * @return mixed + */ + abstract protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure); + + /** + * 预载入关联查询(数据) + * @access protected + * @param Model $result + * @param string $relation + * @param string $subRelation + * @param \Closure $closure + * @return mixed + */ + abstract protected function eagerlyOne(&$result, $relation, $subRelation, $closure); + + /** + * 预载入关联查询(数据集) + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @param bool $join 是否为JOIN方式 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure, $join = false) + { + if ($join || 0 == $this->eagerlyType) { + // 模型JOIN关联组装 + foreach ($resultSet as $result) { + $this->match($this->model, $relation, $result); + } + } else { + // IN查询 + $this->eagerlySet($resultSet, $relation, $subRelation, $closure); + } + } + + /** + * 预载入关联查询(数据) + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @param bool $join 是否为JOIN方式 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure, $join = false) + { + if (0 == $this->eagerlyType || $join) { + // 模型JOIN关联组装 + $this->match($this->model, $relation, $result); + } else { + // IN查询 + $this->eagerlyOne($result, $relation, $subRelation, $closure); + } + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @return Model|false + */ + public function save($data) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + $model = new $this->model; + // 保存关联表数据 + $data[$this->foreignKey] = $this->parent->{$this->localKey}; + + return $model->save($data) ? $model : false; + } + + /** + * 设置预载入方式 + * @access public + * @param integer $type 预载入方式 0 JOIN查询 1 IN查询 + * @return $this + */ + public function setEagerlyType($type) + { + $this->eagerlyType = $type; + + return $this; + } + + /** + * 获取预载入方式 + * @access public + * @return integer + */ + public function getEagerlyType() + { + return $this->eagerlyType; + } + + /** + * 绑定关联表的属性到父模型属性 + * @access public + * @param mixed $attr 要绑定的属性列表 + * @return $this + */ + public function bind($attr) + { + if (is_string($attr)) { + $attr = explode(',', $attr); + } + $this->bindAttr = $attr; + + return $this; + } + + /** + * 获取绑定属性 + * @access public + * @return array + */ + public function getBindAttr() + { + return $this->bindAttr; + } + + /** + * 一对一 关联模型预查询拼装 + * @access public + * @param string $model 模型名称 + * @param string $relation 关联名 + * @param Model $result 模型对象实例 + * @return void + */ + protected function match($model, $relation, &$result) + { + // 重新组装模型数据 + foreach ($result->getData() as $key => $val) { + if (strpos($key, '__')) { + list($name, $attr) = explode('__', $key, 2); + if ($name == $relation) { + $list[$name][$attr] = $val; + unset($result->$key); + } + } + } + + if (isset($list[$relation])) { + $array = array_unique($list[$relation]); + + if (count($array) == 1 && null === current($array)) { + $relationModel = null; + } else { + $relationModel = new $model($list[$relation]); + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + if (!empty($this->bindAttr)) { + $this->bindAttr($relationModel, $result, $this->bindAttr); + } + } else { + $relationModel = null; + } + + $result->setRelation(Loader::parseName($relation), $relationModel); + } + + /** + * 绑定关联属性到父模型 + * @access protected + * @param Model $result 关联模型对象 + * @param Model $model 父模型对象 + * @return void + * @throws Exception + */ + protected function bindAttr($model, &$result) + { + foreach ($this->bindAttr as $key => $attr) { + $key = is_numeric($key) ? $attr : $key; + $value = $result->getOrigin($key); + + if (!is_null($value)) { + throw new Exception('bind attr has exists:' . $key); + } + + $result->setAttr($key, $model ? $model->getAttr($attr) : null); + } + } + + /** + * 一对一 关联模型预查询(IN方式) + * @access public + * @param array $where 关联预查询条件 + * @param string $key 关联键名 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param \Closure $closure + * @return array + */ + protected function eagerlyWhere($where, $key, $relation, $subRelation = '', $closure = null) + { + // 预载入关联查询 支持嵌套预载入 + if ($closure instanceof Closure) { + $closure($this->query); + + if ($field = $this->query->getOptions('with_field')) { + $this->query->field($field)->removeOption('with_field'); + } + } + + $list = $this->query->where($where)->with($subRelation)->select(); + + // 组装模型数据 + $data = []; + + foreach ($list as $set) { + $data[$set->$key] = $set; + } + + return $data; + } + +} diff --git a/vendor/topthink/framework/library/think/paginator/driver/Bootstrap.php b/vendor/topthink/framework/library/think/paginator/driver/Bootstrap.php new file mode 100644 index 0000000..ab5315c --- /dev/null +++ b/vendor/topthink/framework/library/think/paginator/driver/Bootstrap.php @@ -0,0 +1,206 @@ + +// +---------------------------------------------------------------------- + +namespace think\paginator\driver; + +use think\Paginator; + +class Bootstrap extends Paginator +{ + + /** + * 上一页按钮 + * @param string $text + * @return string + */ + protected function getPreviousButton($text = "«") + { + + if ($this->currentPage() <= 1) { + return $this->getDisabledTextWrapper($text); + } + + $url = $this->url( + $this->currentPage() - 1 + ); + + return $this->getPageLinkWrapper($url, $text); + } + + /** + * 下一页按钮 + * @param string $text + * @return string + */ + protected function getNextButton($text = '»') + { + if (!$this->hasMore) { + return $this->getDisabledTextWrapper($text); + } + + $url = $this->url($this->currentPage() + 1); + + return $this->getPageLinkWrapper($url, $text); + } + + /** + * 页码按钮 + * @return string + */ + protected function getLinks() + { + if ($this->simple) { + return ''; + } + + $block = [ + 'first' => null, + 'slider' => null, + 'last' => null, + ]; + + $side = 3; + $window = $side * 2; + + if ($this->lastPage < $window + 6) { + $block['first'] = $this->getUrlRange(1, $this->lastPage); + } elseif ($this->currentPage <= $window) { + $block['first'] = $this->getUrlRange(1, $window + 2); + $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage); + } elseif ($this->currentPage > ($this->lastPage - $window)) { + $block['first'] = $this->getUrlRange(1, 2); + $block['last'] = $this->getUrlRange($this->lastPage - ($window + 2), $this->lastPage); + } else { + $block['first'] = $this->getUrlRange(1, 2); + $block['slider'] = $this->getUrlRange($this->currentPage - $side, $this->currentPage + $side); + $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage); + } + + $html = ''; + + if (is_array($block['first'])) { + $html .= $this->getUrlLinks($block['first']); + } + + if (is_array($block['slider'])) { + $html .= $this->getDots(); + $html .= $this->getUrlLinks($block['slider']); + } + + if (is_array($block['last'])) { + $html .= $this->getDots(); + $html .= $this->getUrlLinks($block['last']); + } + + return $html; + } + + /** + * 渲染分页html + * @return mixed + */ + public function render() + { + if ($this->hasPages()) { + if ($this->simple) { + return sprintf( + '
                %s %s
              ', + $this->getPreviousButton(), + $this->getNextButton() + ); + } else { + return sprintf( + '
                %s %s %s
              ', + $this->getPreviousButton(), + $this->getLinks(), + $this->getNextButton() + ); + } + } + } + + /** + * 生成一个可点击的按钮 + * + * @param string $url + * @param int $page + * @return string + */ + protected function getAvailablePageWrapper($url, $page) + { + return '
            • ' . $page . '
            • '; + } + + /** + * 生成一个禁用的按钮 + * + * @param string $text + * @return string + */ + protected function getDisabledTextWrapper($text) + { + return '
            • ' . $text . '
            • '; + } + + /** + * 生成一个激活的按钮 + * + * @param string $text + * @return string + */ + protected function getActivePageWrapper($text) + { + return '
            • ' . $text . '
            • '; + } + + /** + * 生成省略号按钮 + * + * @return string + */ + protected function getDots() + { + return $this->getDisabledTextWrapper('...'); + } + + /** + * 批量生成页码按钮. + * + * @param array $urls + * @return string + */ + protected function getUrlLinks(array $urls) + { + $html = ''; + + foreach ($urls as $page => $url) { + $html .= $this->getPageLinkWrapper($url, $page); + } + + return $html; + } + + /** + * 生成普通页码按钮 + * + * @param string $url + * @param int $page + * @return string + */ + protected function getPageLinkWrapper($url, $page) + { + if ($this->currentPage() == $page) { + return $this->getActivePageWrapper($page); + } + + return $this->getAvailablePageWrapper($url, $page); + } +} diff --git a/vendor/topthink/framework/library/think/process/Builder.php b/vendor/topthink/framework/library/think/process/Builder.php new file mode 100644 index 0000000..da56163 --- /dev/null +++ b/vendor/topthink/framework/library/think/process/Builder.php @@ -0,0 +1,233 @@ + +// +---------------------------------------------------------------------- + +namespace think\process; + +use think\Process; + +class Builder +{ + private $arguments; + private $cwd; + private $env = null; + private $input; + private $timeout = 60; + private $options = []; + private $inheritEnv = true; + private $prefix = []; + private $outputDisabled = false; + + /** + * 构造方法 + * @param string[] $arguments 参数 + */ + public function __construct(array $arguments = []) + { + $this->arguments = $arguments; + } + + /** + * 创建一个实例 + * @param string[] $arguments 参数 + * @return self + */ + public static function create(array $arguments = []) + { + return new static($arguments); + } + + /** + * 添加一个参数 + * @param string $argument 参数 + * @return self + */ + public function add($argument) + { + $this->arguments[] = $argument; + + return $this; + } + + /** + * 添加一个前缀 + * @param string|array $prefix + * @return self + */ + public function setPrefix($prefix) + { + $this->prefix = is_array($prefix) ? $prefix : [$prefix]; + + return $this; + } + + /** + * 设置参数 + * @param string[] $arguments + * @return self + */ + public function setArguments(array $arguments) + { + $this->arguments = $arguments; + + return $this; + } + + /** + * 设置工作目录 + * @param null|string $cwd + * @return self + */ + public function setWorkingDirectory($cwd) + { + $this->cwd = $cwd; + + return $this; + } + + /** + * 是否初始化环境变量 + * @param bool $inheritEnv + * @return self + */ + public function inheritEnvironmentVariables($inheritEnv = true) + { + $this->inheritEnv = $inheritEnv; + + return $this; + } + + /** + * 设置环境变量 + * @param string $name + * @param null|string $value + * @return self + */ + public function setEnv($name, $value) + { + $this->env[$name] = $value; + + return $this; + } + + /** + * 添加环境变量 + * @param array $variables + * @return self + */ + public function addEnvironmentVariables(array $variables) + { + $this->env = array_replace($this->env, $variables); + + return $this; + } + + /** + * 设置输入 + * @param mixed $input + * @return self + */ + public function setInput($input) + { + $this->input = Utils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input); + + return $this; + } + + /** + * 设置超时时间 + * @param float|null $timeout + * @return self + */ + public function setTimeout($timeout) + { + if (null === $timeout) { + $this->timeout = null; + + return $this; + } + + $timeout = (float) $timeout; + + if ($timeout < 0) { + throw new \InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); + } + + $this->timeout = $timeout; + + return $this; + } + + /** + * 设置proc_open选项 + * @param string $name + * @param string $value + * @return self + */ + public function setOption($name, $value) + { + $this->options[$name] = $value; + + return $this; + } + + /** + * 禁止输出 + * @return self + */ + public function disableOutput() + { + $this->outputDisabled = true; + + return $this; + } + + /** + * 开启输出 + * @return self + */ + public function enableOutput() + { + $this->outputDisabled = false; + + return $this; + } + + /** + * 创建一个Process实例 + * @return Process + */ + public function getProcess() + { + if (0 === count($this->prefix) && 0 === count($this->arguments)) { + throw new \LogicException('You must add() command arguments before calling getProcess().'); + } + + $options = $this->options; + + $arguments = array_merge($this->prefix, $this->arguments); + $script = implode(' ', array_map([__NAMESPACE__ . '\\Utils', 'escapeArgument'], $arguments)); + + if ($this->inheritEnv) { + // include $_ENV for BC purposes + $env = array_replace($_ENV, $_SERVER, $this->env); + } else { + $env = $this->env; + } + + $process = new Process($script, $this->cwd, $env, $this->input, $this->timeout, $options); + + if ($this->outputDisabled) { + $process->disableOutput(); + } + + return $process; + } +} diff --git a/vendor/topthink/framework/library/think/process/Utils.php b/vendor/topthink/framework/library/think/process/Utils.php new file mode 100644 index 0000000..f94c648 --- /dev/null +++ b/vendor/topthink/framework/library/think/process/Utils.php @@ -0,0 +1,75 @@ + +// +---------------------------------------------------------------------- + +namespace think\process; + +class Utils +{ + + /** + * 转义字符串 + * @param string $argument + * @return string + */ + public static function escapeArgument($argument) + { + + if ('' === $argument) { + return escapeshellarg($argument); + } + $escapedArgument = ''; + $quote = false; + foreach (preg_split('/(")/i', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) { + if ('"' === $part) { + $escapedArgument .= '\\"'; + } elseif (self::isSurroundedBy($part, '%')) { + // Avoid environment variable expansion + $escapedArgument .= '^%"' . substr($part, 1, -1) . '"^%'; + } else { + // escape trailing backslash + if ('\\' === substr($part, -1)) { + $part .= '\\'; + } + $quote = true; + $escapedArgument .= $part; + } + } + if ($quote) { + $escapedArgument = '"' . $escapedArgument . '"'; + } + return $escapedArgument; + } + + /** + * 验证并进行规范化Process输入。 + * @param string $caller + * @param mixed $input + * @return string + * @throws \InvalidArgumentException + */ + public static function validateInput($caller, $input) + { + if (null !== $input) { + if (is_resource($input)) { + return $input; + } + if (is_scalar($input)) { + return (string) $input; + } + throw new \InvalidArgumentException(sprintf('%s only accepts strings or stream resources.', $caller)); + } + return $input; + } + + private static function isSurroundedBy($arg, $char) + { + return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1]; + } + +} diff --git a/vendor/topthink/framework/library/think/process/exception/Faild.php b/vendor/topthink/framework/library/think/process/exception/Faild.php new file mode 100644 index 0000000..38647bc --- /dev/null +++ b/vendor/topthink/framework/library/think/process/exception/Faild.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\exception; + +use think\Process; + +class Faild extends \RuntimeException +{ + + private $process; + + public function __construct(Process $process) + { + if ($process->isSuccessful()) { + throw new \InvalidArgumentException('Expected a failed process, but the given process was successful.'); + } + + $error = sprintf('The command "%s" failed.' . "\nExit Code: %s(%s)", $process->getCommandLine(), $process->getExitCode(), $process->getExitCodeText()); + + if (!$process->isOutputDisabled()) { + $error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", $process->getOutput(), $process->getErrorOutput()); + } + + parent::__construct($error); + + $this->process = $process; + } + + public function getProcess() + { + return $this->process; + } +} diff --git a/vendor/topthink/framework/library/think/process/exception/Failed.php b/vendor/topthink/framework/library/think/process/exception/Failed.php new file mode 100644 index 0000000..5295082 --- /dev/null +++ b/vendor/topthink/framework/library/think/process/exception/Failed.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\exception; + +use think\Process; + +class Failed extends \RuntimeException +{ + + private $process; + + public function __construct(Process $process) + { + if ($process->isSuccessful()) { + throw new \InvalidArgumentException('Expected a failed process, but the given process was successful.'); + } + + $error = sprintf('The command "%s" failed.' . "\nExit Code: %s(%s)", $process->getCommandLine(), $process->getExitCode(), $process->getExitCodeText()); + + if (!$process->isOutputDisabled()) { + $error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", $process->getOutput(), $process->getErrorOutput()); + } + + parent::__construct($error); + + $this->process = $process; + } + + public function getProcess() + { + return $this->process; + } +} diff --git a/vendor/topthink/framework/library/think/process/exception/Timeout.php b/vendor/topthink/framework/library/think/process/exception/Timeout.php new file mode 100644 index 0000000..d5f1162 --- /dev/null +++ b/vendor/topthink/framework/library/think/process/exception/Timeout.php @@ -0,0 +1,61 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\exception; + +use think\Process; + +class Timeout extends \RuntimeException +{ + + const TYPE_GENERAL = 1; + const TYPE_IDLE = 2; + + private $process; + private $timeoutType; + + public function __construct(Process $process, $timeoutType) + { + $this->process = $process; + $this->timeoutType = $timeoutType; + + parent::__construct(sprintf('The process "%s" exceeded the timeout of %s seconds.', $process->getCommandLine(), $this->getExceededTimeout())); + } + + public function getProcess() + { + return $this->process; + } + + public function isGeneralTimeout() + { + return $this->timeoutType === self::TYPE_GENERAL; + } + + public function isIdleTimeout() + { + return $this->timeoutType === self::TYPE_IDLE; + } + + public function getExceededTimeout() + { + switch ($this->timeoutType) { + case self::TYPE_GENERAL: + return $this->process->getTimeout(); + + case self::TYPE_IDLE: + return $this->process->getIdleTimeout(); + + default: + throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)); + } + } +} diff --git a/vendor/topthink/framework/library/think/process/pipes/Pipes.php b/vendor/topthink/framework/library/think/process/pipes/Pipes.php new file mode 100644 index 0000000..82396b8 --- /dev/null +++ b/vendor/topthink/framework/library/think/process/pipes/Pipes.php @@ -0,0 +1,93 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\pipes; + +abstract class Pipes +{ + + /** @var array */ + public $pipes = []; + + /** @var string */ + protected $inputBuffer = ''; + /** @var resource|null */ + protected $input; + + /** @var bool */ + private $blocked = true; + + const CHUNK_SIZE = 16384; + + /** + * 返回用于 proc_open 描述符的数组 + * @return array + */ + abstract public function getDescriptors(); + + /** + * 返回一个数组的索引由其相关的流,以防这些管道使用的临时文件的文件名。 + * @return string[] + */ + abstract public function getFiles(); + + /** + * 文件句柄和管道中读取数据。 + * @param bool $blocking 是否使用阻塞调用 + * @param bool $close 是否要关闭管道,如果他们已经到达 EOF。 + * @return string[] + */ + abstract public function readAndWrite($blocking, $close = false); + + /** + * 返回当前状态如果有打开的文件句柄或管道。 + * @return bool + */ + abstract public function areOpen(); + + /** + * {@inheritdoc} + */ + public function close() + { + foreach ($this->pipes as $pipe) { + fclose($pipe); + } + $this->pipes = []; + } + + /** + * 检查系统调用已被中断 + * @return bool + */ + protected function hasSystemCallBeenInterrupted() + { + $lastError = error_get_last(); + + return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call'); + } + + protected function unblock() + { + if (!$this->blocked) { + return; + } + + foreach ($this->pipes as $pipe) { + stream_set_blocking($pipe, 0); + } + if (null !== $this->input) { + stream_set_blocking($this->input, 0); + } + + $this->blocked = false; + } +} diff --git a/vendor/topthink/framework/library/think/process/pipes/Unix.php b/vendor/topthink/framework/library/think/process/pipes/Unix.php new file mode 100644 index 0000000..fd99a5d --- /dev/null +++ b/vendor/topthink/framework/library/think/process/pipes/Unix.php @@ -0,0 +1,196 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\pipes; + +use think\Process; + +class Unix extends Pipes +{ + + /** @var bool */ + private $ttyMode; + /** @var bool */ + private $ptyMode; + /** @var bool */ + private $disableOutput; + + public function __construct($ttyMode, $ptyMode, $input, $disableOutput) + { + $this->ttyMode = (bool) $ttyMode; + $this->ptyMode = (bool) $ptyMode; + $this->disableOutput = (bool) $disableOutput; + + if (is_resource($input)) { + $this->input = $input; + } else { + $this->inputBuffer = (string) $input; + } + } + + public function __destruct() + { + $this->close(); + } + + /** + * {@inheritdoc} + */ + public function getDescriptors() + { + if ($this->disableOutput) { + $nullstream = fopen('/dev/null', 'c'); + + return [ + ['pipe', 'r'], + $nullstream, + $nullstream, + ]; + } + + if ($this->ttyMode) { + return [ + ['file', '/dev/tty', 'r'], + ['file', '/dev/tty', 'w'], + ['file', '/dev/tty', 'w'], + ]; + } + + if ($this->ptyMode && Process::isPtySupported()) { + return [ + ['pty'], + ['pty'], + ['pty'], + ]; + } + + return [ + ['pipe', 'r'], + ['pipe', 'w'], // stdout + ['pipe', 'w'], // stderr + ]; + } + + /** + * {@inheritdoc} + */ + public function getFiles() + { + return []; + } + + /** + * {@inheritdoc} + */ + public function readAndWrite($blocking, $close = false) + { + + if (1 === count($this->pipes) && [0] === array_keys($this->pipes)) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + + if (empty($this->pipes)) { + return []; + } + + $this->unblock(); + + $read = []; + + if (null !== $this->input) { + $r = array_merge($this->pipes, ['input' => $this->input]); + } else { + $r = $this->pipes; + } + + unset($r[0]); + + $w = isset($this->pipes[0]) ? [$this->pipes[0]] : null; + $e = null; + + if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = []; + } + + return $read; + } + + if (0 === $n) { + return $read; + } + + foreach ($r as $pipe) { + + $type = (false !== $found = array_search($pipe, $this->pipes)) ? $found : 'input'; + $data = ''; + while ('' !== $dataread = (string) fread($pipe, self::CHUNK_SIZE)) { + $data .= $dataread; + } + + if ('' !== $data) { + if ('input' === $type) { + $this->inputBuffer .= $data; + } else { + $read[$type] = $data; + } + } + + if (false === $data || (true === $close && feof($pipe) && '' === $data)) { + if ('input' === $type) { + $this->input = null; + } else { + fclose($this->pipes[$type]); + unset($this->pipes[$type]); + } + } + } + + if (null !== $w && 0 < count($w)) { + while (strlen($this->inputBuffer)) { + $written = fwrite($w[0], $this->inputBuffer, 2 << 18); // write 512k + if ($written > 0) { + $this->inputBuffer = (string) substr($this->inputBuffer, $written); + } else { + break; + } + } + } + + if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function areOpen() + { + return (bool) $this->pipes; + } + + /** + * 创建一个新的 UnixPipes 实例 + * @param Process $process + * @param string|resource $input + * @return self + */ + public static function create(Process $process, $input) + { + return new static($process->isTty(), $process->isPty(), $input, $process->isOutputDisabled()); + } +} diff --git a/vendor/topthink/framework/library/think/process/pipes/Windows.php b/vendor/topthink/framework/library/think/process/pipes/Windows.php new file mode 100644 index 0000000..1b8b0d4 --- /dev/null +++ b/vendor/topthink/framework/library/think/process/pipes/Windows.php @@ -0,0 +1,228 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\pipes; + +use think\Process; + +class Windows extends Pipes +{ + + /** @var array */ + private $files = []; + /** @var array */ + private $fileHandles = []; + /** @var array */ + private $readBytes = [ + Process::STDOUT => 0, + Process::STDERR => 0, + ]; + /** @var bool */ + private $disableOutput; + + public function __construct($disableOutput, $input) + { + $this->disableOutput = (bool) $disableOutput; + + if (!$this->disableOutput) { + + $this->files = [ + Process::STDOUT => tempnam(sys_get_temp_dir(), 'sf_proc_stdout'), + Process::STDERR => tempnam(sys_get_temp_dir(), 'sf_proc_stderr'), + ]; + foreach ($this->files as $offset => $file) { + $this->fileHandles[$offset] = fopen($this->files[$offset], 'rb'); + if (false === $this->fileHandles[$offset]) { + throw new \RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable'); + } + } + } + + if (is_resource($input)) { + $this->input = $input; + } else { + $this->inputBuffer = $input; + } + } + + public function __destruct() + { + $this->close(); + $this->removeFiles(); + } + + /** + * {@inheritdoc} + */ + public function getDescriptors() + { + if ($this->disableOutput) { + $nullstream = fopen('NUL', 'c'); + + return [ + ['pipe', 'r'], + $nullstream, + $nullstream, + ]; + } + + return [ + ['pipe', 'r'], + ['file', 'NUL', 'w'], + ['file', 'NUL', 'w'], + ]; + } + + /** + * {@inheritdoc} + */ + public function getFiles() + { + return $this->files; + } + + /** + * {@inheritdoc} + */ + public function readAndWrite($blocking, $close = false) + { + $this->write($blocking, $close); + + $read = []; + $fh = $this->fileHandles; + foreach ($fh as $type => $fileHandle) { + if (0 !== fseek($fileHandle, $this->readBytes[$type])) { + continue; + } + $data = ''; + $dataread = null; + while (!feof($fileHandle)) { + if (false !== $dataread = fread($fileHandle, self::CHUNK_SIZE)) { + $data .= $dataread; + } + } + if (0 < $length = strlen($data)) { + $this->readBytes[$type] += $length; + $read[$type] = $data; + } + + if (false === $dataread || (true === $close && feof($fileHandle) && '' === $data)) { + fclose($this->fileHandles[$type]); + unset($this->fileHandles[$type]); + } + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function areOpen() + { + return (bool) $this->pipes && (bool) $this->fileHandles; + } + + /** + * {@inheritdoc} + */ + public function close() + { + parent::close(); + foreach ($this->fileHandles as $handle) { + fclose($handle); + } + $this->fileHandles = []; + } + + /** + * 创建一个新的 WindowsPipes 实例。 + * @param Process $process + * @param $input + * @return self + */ + public static function create(Process $process, $input) + { + return new static($process->isOutputDisabled(), $input); + } + + /** + * 删除临时文件 + */ + private function removeFiles() + { + foreach ($this->files as $filename) { + if (file_exists($filename)) { + @unlink($filename); + } + } + $this->files = []; + } + + /** + * 写入到 stdin 输入 + * @param bool $blocking + * @param bool $close + */ + private function write($blocking, $close) + { + if (empty($this->pipes)) { + return; + } + + $this->unblock(); + + $r = null !== $this->input ? ['input' => $this->input] : null; + $w = isset($this->pipes[0]) ? [$this->pipes[0]] : null; + $e = null; + + if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = []; + } + + return; + } + + if (0 === $n) { + return; + } + + if (null !== $r && 0 < count($r)) { + $data = ''; + while ($dataread = fread($r['input'], self::CHUNK_SIZE)) { + $data .= $dataread; + } + + $this->inputBuffer .= $data; + + if (false === $data || (true === $close && feof($r['input']) && '' === $data)) { + $this->input = null; + } + } + + if (null !== $w && 0 < count($w)) { + while (strlen($this->inputBuffer)) { + $written = fwrite($w[0], $this->inputBuffer, 2 << 18); + if ($written > 0) { + $this->inputBuffer = (string) substr($this->inputBuffer, $written); + } else { + break; + } + } + } + + if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + } +} diff --git a/vendor/topthink/framework/library/think/response/Download.php b/vendor/topthink/framework/library/think/response/Download.php new file mode 100644 index 0000000..5595f9a --- /dev/null +++ b/vendor/topthink/framework/library/think/response/Download.php @@ -0,0 +1,148 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Exception; +use think\Response; + +class Download extends Response +{ + protected $expire = 360; + protected $name; + protected $mimeType; + protected $isContent = false; + protected $openinBrowser = false; + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + * @throws \Exception + */ + protected function output($data) + { + if (!$this->isContent && !is_file($data)) { + throw new Exception('file not exists:' . $data); + } + + ob_end_clean(); + + if (!empty($this->name)) { + $name = $this->name; + } else { + $name = !$this->isContent ? pathinfo($data, PATHINFO_BASENAME) : ''; + } + + if ($this->isContent) { + $mimeType = $this->mimeType; + $size = strlen($data); + } else { + $mimeType = $this->getMimeType($data); + $size = filesize($data); + } + + $this->header['Pragma'] = 'public'; + $this->header['Content-Type'] = $mimeType ?: 'application/octet-stream'; + $this->header['Cache-control'] = 'max-age=' . $this->expire; + $this->header['Content-Disposition'] = $this->openinBrowser ? 'inline' : 'attachment; filename="' . $name . '"'; + $this->header['Content-Length'] = $size; + $this->header['Content-Transfer-Encoding'] = 'binary'; + $this->header['Expires'] = gmdate("D, d M Y H:i:s", time() + $this->expire) . ' GMT'; + + $this->lastModified(gmdate('D, d M Y H:i:s', time()) . ' GMT'); + + $data = $this->isContent ? $data : file_get_contents($data); + return $data; + } + + /** + * 设置是否为内容 必须配合mimeType方法使用 + * @access public + * @param bool $content + * @return $this + */ + public function isContent($content = true) + { + $this->isContent = $content; + return $this; + } + + /** + * 设置有效期 + * @access public + * @param integer $expire 有效期 + * @return $this + */ + public function expire($expire) + { + $this->expire = $expire; + return $this; + } + + /** + * 设置文件类型 + * @access public + * @param string $filename 文件名 + * @return $this + */ + public function mimeType($mimeType) + { + $this->mimeType = $mimeType; + return $this; + } + + /** + * 获取文件类型信息 + * @access public + * @param string $filename 文件名 + * @return string + */ + protected function getMimeType($filename) + { + if (!empty($this->mimeType)) { + return $this->mimeType; + } + + $finfo = finfo_open(FILEINFO_MIME_TYPE); + + return finfo_file($finfo, $filename); + } + + /** + * 设置下载文件的显示名称 + * @access public + * @param string $filename 文件名 + * @param bool $extension 后缀自动识别 + * @return $this + */ + public function name($filename, $extension = true) + { + $this->name = $filename; + + if ($extension && false === strpos($filename, '.')) { + $this->name .= '.' . pathinfo($this->data, PATHINFO_EXTENSION); + } + + return $this; + } + + /** + * 设置是否在浏览器中显示文件 + * @access public + * @param bool $openinBrowser 是否在浏览器中显示文件 + * @return $this + */ + public function openinBrowser($openinBrowser) { + $this->openinBrowser = $openinBrowser; + return $this; + } +} diff --git a/vendor/topthink/framework/library/think/response/Json.php b/vendor/topthink/framework/library/think/response/Json.php new file mode 100644 index 0000000..aa5bbd6 --- /dev/null +++ b/vendor/topthink/framework/library/think/response/Json.php @@ -0,0 +1,51 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Response; + +class Json extends Response +{ + // 输出参数 + protected $options = [ + 'json_encode_param' => JSON_UNESCAPED_UNICODE, + ]; + + protected $contentType = 'application/json'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + * @throws \Exception + */ + protected function output($data) + { + try { + // 返回JSON数据格式到客户端 包含状态信息 + $data = json_encode($data, $this->options['json_encode_param']); + + if (false === $data) { + throw new \InvalidArgumentException(json_last_error_msg()); + } + + return $data; + } catch (\Exception $e) { + if ($e->getPrevious()) { + throw $e->getPrevious(); + } + throw $e; + } + } + +} diff --git a/vendor/topthink/framework/library/think/response/Jsonp.php b/vendor/topthink/framework/library/think/response/Jsonp.php new file mode 100644 index 0000000..f69e88e --- /dev/null +++ b/vendor/topthink/framework/library/think/response/Jsonp.php @@ -0,0 +1,58 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Response; + +class Jsonp extends Response +{ + // 输出参数 + protected $options = [ + 'var_jsonp_handler' => 'callback', + 'default_jsonp_handler' => 'jsonpReturn', + 'json_encode_param' => JSON_UNESCAPED_UNICODE, + ]; + + protected $contentType = 'application/javascript'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + * @throws \Exception + */ + protected function output($data) + { + try { + // 返回JSON数据格式到客户端 包含状态信息 [当url_common_param为false时是无法获取到$_GET的数据的,故使用Request来获取] + $var_jsonp_handler = $this->app['request']->param($this->options['var_jsonp_handler'], ""); + $handler = !empty($var_jsonp_handler) ? $var_jsonp_handler : $this->options['default_jsonp_handler']; + + $data = json_encode($data, $this->options['json_encode_param']); + + if (false === $data) { + throw new \InvalidArgumentException(json_last_error_msg()); + } + + $data = $handler . '(' . $data . ');'; + + return $data; + } catch (\Exception $e) { + if ($e->getPrevious()) { + throw $e->getPrevious(); + } + throw $e; + } + } + +} diff --git a/vendor/topthink/framework/library/think/response/Jump.php b/vendor/topthink/framework/library/think/response/Jump.php new file mode 100644 index 0000000..258448c --- /dev/null +++ b/vendor/topthink/framework/library/think/response/Jump.php @@ -0,0 +1,32 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Response; + +class Jump extends Response +{ + protected $contentType = 'text/html'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + * @throws \Exception + */ + protected function output($data) + { + $data = $this->app['view']->fetch($this->options['jump_template'], $data); + return $data; + } +} diff --git a/vendor/topthink/framework/library/think/response/Redirect.php b/vendor/topthink/framework/library/think/response/Redirect.php new file mode 100644 index 0000000..6b4f118 --- /dev/null +++ b/vendor/topthink/framework/library/think/response/Redirect.php @@ -0,0 +1,119 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Response; + +class Redirect extends Response +{ + + protected $options = []; + + // URL参数 + protected $params = []; + + public function __construct($data = '', $code = 302, array $header = [], array $options = []) + { + parent::__construct($data, $code, $header, $options); + + $this->cacheControl('no-cache,must-revalidate'); + } + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + $this->header['Location'] = $this->getTargetUrl(); + + return; + } + + /** + * 重定向传值(通过Session) + * @access protected + * @param string|array $name 变量名或者数组 + * @param mixed $value 值 + * @return $this + */ + public function with($name, $value = null) + { + $session = $this->app['session']; + + if (is_array($name)) { + foreach ($name as $key => $val) { + $session->flash($key, $val); + } + } else { + $session->flash($name, $value); + } + + return $this; + } + + /** + * 获取跳转地址 + * @access public + * @return string + */ + public function getTargetUrl() + { + if (strpos($this->data, '://') || (0 === strpos($this->data, '/') && empty($this->params))) { + return $this->data; + } else { + return $this->app['url']->build($this->data, $this->params); + } + } + + public function params($params = []) + { + $this->params = $params; + + return $this; + } + + /** + * 记住当前url后跳转 + * @access public + * @param string $url 指定记住的url + * @return $this + */ + public function remember($url = null) + { + $this->app['session']->set('redirect_url', $url ?: $this->app['request']->url()); + + return $this; + } + + /** + * 跳转到上次记住的url + * @access public + * @param string $url 闪存数据不存在时的跳转地址 + * @return $this + */ + public function restore($url = null) + { + $session = $this->app['session']; + + if ($session->has('redirect_url')) { + $this->data = $session->get('redirect_url'); + $session->delete('redirect_url'); + } elseif ($url) { + $this->data = $url; + } + + return $this; + } +} diff --git a/vendor/topthink/framework/library/think/response/View.php b/vendor/topthink/framework/library/think/response/View.php new file mode 100644 index 0000000..3d54c73 --- /dev/null +++ b/vendor/topthink/framework/library/think/response/View.php @@ -0,0 +1,119 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Response; + +class View extends Response +{ + // 输出参数 + protected $options = []; + protected $vars = []; + protected $config = []; + protected $filter; + protected $contentType = 'text/html'; + + /** + * 是否内容渲染 + * @var bool + */ + protected $isContent = false; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + // 渲染模板输出 + return $this->app['view'] + ->filter($this->filter) + ->fetch($data, $this->vars, $this->config, $this->isContent); + } + + /** + * 设置是否为内容渲染 + * @access public + * @param bool $content + * @return $this + */ + public function isContent($content = true) + { + $this->isContent = $content; + return $this; + } + + /** + * 获取视图变量 + * @access public + * @param string $name 模板变量 + * @return mixed + */ + public function getVars($name = null) + { + if (is_null($name)) { + return $this->vars; + } else { + return isset($this->vars[$name]) ? $this->vars[$name] : null; + } + } + + /** + * 模板变量赋值 + * @access public + * @param mixed $name 变量名 + * @param mixed $value 变量值 + * @return $this + */ + public function assign($name, $value = '') + { + if (is_array($name)) { + $this->vars = array_merge($this->vars, $name); + } else { + $this->vars[$name] = $value; + } + + return $this; + } + + public function config($config) + { + $this->config = $config; + return $this; + } + + /** + * 视图内容过滤 + * @access public + * @param callable $filter + * @return $this + */ + public function filter($filter) + { + $this->filter = $filter; + return $this; + } + + /** + * 检查模板是否存在 + * @access private + * @param string|array $name 参数名 + * @return bool + */ + public function exists($name) + { + return $this->app['view']->exists($name); + } + +} diff --git a/vendor/topthink/framework/library/think/response/Xml.php b/vendor/topthink/framework/library/think/response/Xml.php new file mode 100644 index 0000000..9c1681a --- /dev/null +++ b/vendor/topthink/framework/library/think/response/Xml.php @@ -0,0 +1,116 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Collection; +use think\Model; +use think\Response; + +class Xml extends Response +{ + // 输出参数 + protected $options = [ + // 根节点名 + 'root_node' => 'think', + // 根节点属性 + 'root_attr' => '', + //数字索引的子节点名 + 'item_node' => 'item', + // 数字索引子节点key转换的属性名 + 'item_key' => 'id', + // 数据编码 + 'encoding' => 'utf-8', + ]; + + protected $contentType = 'text/xml'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + if (is_string($data)) { + if (0 !== strpos($data, 'options['encoding']; + $xml = ""; + $data = $xml . $data; + } + return $data; + } + + // XML数据转换 + return $this->xmlEncode($data, $this->options['root_node'], $this->options['item_node'], $this->options['root_attr'], $this->options['item_key'], $this->options['encoding']); + } + + /** + * XML编码 + * @access protected + * @param mixed $data 数据 + * @param string $root 根节点名 + * @param string $item 数字索引的子节点名 + * @param string $attr 根节点属性 + * @param string $id 数字索引子节点key转换的属性名 + * @param string $encoding 数据编码 + * @return string + */ + protected function xmlEncode($data, $root, $item, $attr, $id, $encoding) + { + if (is_array($attr)) { + $array = []; + foreach ($attr as $key => $value) { + $array[] = "{$key}=\"{$value}\""; + } + $attr = implode(' ', $array); + } + + $attr = trim($attr); + $attr = empty($attr) ? '' : " {$attr}"; + $xml = ""; + $xml .= "<{$root}{$attr}>"; + $xml .= $this->dataToXml($data, $item, $id); + $xml .= ""; + + return $xml; + } + + /** + * 数据XML编码 + * @access protected + * @param mixed $data 数据 + * @param string $item 数字索引时的节点名称 + * @param string $id 数字索引key转换为的属性名 + * @return string + */ + 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}\""; + $key = $item; + } + $xml .= "<{$key}{$attr}>"; + $xml .= (is_array($val) || is_object($val)) ? $this->dataToXml($val, $item, $id) : $val; + $xml .= ""; + } + + return $xml; + } +} diff --git a/vendor/topthink/framework/library/think/route/AliasRule.php b/vendor/topthink/framework/library/think/route/AliasRule.php new file mode 100644 index 0000000..393cb31 --- /dev/null +++ b/vendor/topthink/framework/library/think/route/AliasRule.php @@ -0,0 +1,119 @@ + +// +---------------------------------------------------------------------- + +namespace think\route; + +use think\Route; + +class AliasRule extends Domain +{ + /** + * 架构函数 + * @access public + * @param Route $router 路由实例 + * @param RuleGroup $parent 上级对象 + * @param string $name 路由别名 + * @param string $route 路由绑定 + * @param array $option 路由参数 + */ + public function __construct(Route $router, RuleGroup $parent, $name, $route, $option = []) + { + $this->router = $router; + $this->parent = $parent; + $this->name = $name; + $this->route = $route; + $this->option = $option; + } + + /** + * 检测路由别名 + * @access public + * @param Request $request 请求对象 + * @param string $url 访问地址 + * @param bool $completeMatch 路由是否完全匹配 + * @return Dispatch|false + */ + public function check($request, $url, $completeMatch = false) + { + if ($dispatch = $this->checkCrossDomain($request)) { + // 允许跨域 + return $dispatch; + } + + // 检查参数有效性 + if (!$this->checkOption($this->option, $request)) { + return false; + } + + list($action, $bind) = array_pad(explode('|', $url, 2), 2, ''); + + if (isset($this->option['allow']) && !in_array($action, $this->option['allow'])) { + // 允许操作 + return false; + } elseif (isset($this->option['except']) && in_array($action, $this->option['except'])) { + // 排除操作 + return false; + } + + if (isset($this->option['method'][$action])) { + $this->option['method'] = $this->option['method'][$action]; + } + + // 匹配后执行的行为 + $this->afterMatchGroup($request); + + if ($this->parent) { + // 合并分组参数 + $this->mergeGroupOptions(); + } + + if (isset($this->option['ext'])) { + // 路由ext参数 优先于系统配置的URL伪静态后缀参数 + $bind = preg_replace('/\.(' . $request->ext() . ')$/i', '', $bind); + } + + $this->parseBindAppendParam($this->route); + + if (0 === strpos($this->route, '\\')) { + // 路由到类 + return $this->bindToClass($request, $bind, substr($this->route, 1)); + } elseif (0 === strpos($this->route, '@')) { + // 路由到控制器类 + return $this->bindToController($request, $bind, substr($this->route, 1)); + } else { + // 路由到模块/控制器 + return $this->bindToModule($request, $bind, $this->route); + } + } + + /** + * 设置允许的操作方法 + * @access public + * @param array $action 操作方法 + * @return $this + */ + public function allow($action = []) + { + return $this->option('allow', $action); + } + + /** + * 设置排除的操作方法 + * @access public + * @param array $action 操作方法 + * @return $this + */ + public function except($action = []) + { + return $this->option('except', $action); + } + +} diff --git a/vendor/topthink/framework/library/think/route/Dispatch.php b/vendor/topthink/framework/library/think/route/Dispatch.php new file mode 100644 index 0000000..7323c98 --- /dev/null +++ b/vendor/topthink/framework/library/think/route/Dispatch.php @@ -0,0 +1,366 @@ + +// +---------------------------------------------------------------------- + +namespace think\route; + +use think\App; +use think\Container; +use think\exception\ValidateException; +use think\Request; +use think\Response; + +abstract class Dispatch +{ + /** + * 应用对象 + * @var App + */ + protected $app; + + /** + * 请求对象 + * @var Request + */ + protected $request; + + /** + * 路由规则 + * @var Rule + */ + protected $rule; + + /** + * 调度信息 + * @var mixed + */ + protected $dispatch; + + /** + * 调度参数 + * @var array + */ + protected $param; + + /** + * 状态码 + * @var string + */ + protected $code; + + /** + * 是否进行大小写转换 + * @var bool + */ + protected $convert; + + public function __construct(Request $request, Rule $rule, $dispatch, $param = [], $code = null) + { + $this->request = $request; + $this->rule = $rule; + $this->app = Container::get('app'); + $this->dispatch = $dispatch; + $this->param = $param; + $this->code = $code; + + if (isset($param['convert'])) { + $this->convert = $param['convert']; + } + } + + public function init() + { + // 执行路由后置操作 + if ($this->rule->doAfter()) { + // 设置请求的路由信息 + + // 设置当前请求的参数 + $this->request->setRouteVars($this->rule->getVars()); + $this->request->routeInfo([ + 'rule' => $this->rule->getRule(), + 'route' => $this->rule->getRoute(), + 'option' => $this->rule->getOption(), + 'var' => $this->rule->getVars(), + ]); + + $this->doRouteAfter(); + } + + return $this; + } + + /** + * 检查路由后置操作 + * @access protected + * @return void + */ + protected function doRouteAfter() + { + // 记录匹配的路由信息 + $option = $this->rule->getOption(); + $matches = $this->rule->getVars(); + + // 添加中间件 + if (!empty($option['middleware'])) { + $this->app['middleware']->import($option['middleware']); + } + + // 绑定模型数据 + if (!empty($option['model'])) { + $this->createBindModel($option['model'], $matches); + } + + // 指定Header数据 + if (!empty($option['header'])) { + $header = $option['header']; + $this->app['hook']->add('response_send', function ($response) use ($header) { + $response->header($header); + }); + } + + // 指定Response响应数据 + if (!empty($option['response'])) { + foreach ($option['response'] as $response) { + $this->app['hook']->add('response_send', $response); + } + } + + // 开启请求缓存 + if (isset($option['cache']) && $this->request->isGet()) { + $this->parseRequestCache($option['cache']); + } + + if (!empty($option['append'])) { + $this->request->setRouteVars($option['append']); + } + } + + /** + * 执行路由调度 + * @access public + * @return mixed + */ + public function run() + { + $option = $this->rule->getOption(); + + // 检测路由after行为 + if (!empty($option['after'])) { + $dispatch = $this->checkAfter($option['after']); + + if ($dispatch instanceof Response) { + return $dispatch; + } + } + + // 数据自动验证 + if (isset($option['validate'])) { + $this->autoValidate($option['validate']); + } + + $data = $this->exec(); + + return $this->autoResponse($data); + } + + protected function autoResponse($data) + { + if ($data instanceof Response) { + $response = $data; + } elseif (!is_null($data)) { + // 默认自动识别响应输出类型 + $isAjax = $this->request->isAjax(); + $type = $isAjax ? $this->rule->getConfig('default_ajax_return') : $this->rule->getConfig('default_return_type'); + + $response = Response::create($data, $type); + } else { + $data = ob_get_clean(); + $content = false === $data ? '' : $data; + $status = '' === $content && $this->request->isJson() ? 204 : 200; + + $response = Response::create($content, '', $status); + } + + return $response; + } + + /** + * 检查路由后置行为 + * @access protected + * @param mixed $after 后置行为 + * @return mixed + */ + protected function checkAfter($after) + { + $this->app['log']->notice('路由后置行为建议使用中间件替代!'); + + $hook = $this->app['hook']; + + $result = null; + + foreach ((array) $after as $behavior) { + $result = $hook->exec($behavior); + + if (!is_null($result)) { + break; + } + } + + // 路由规则重定向 + if ($result instanceof Response) { + return $result; + } + + return false; + } + + /** + * 验证数据 + * @access protected + * @param array $option + * @return void + * @throws ValidateException + */ + protected function autoValidate($option) + { + list($validate, $scene, $message, $batch) = $option; + + if (is_array($validate)) { + // 指定验证规则 + $v = $this->app->validate(); + $v->rule($validate); + } else { + // 调用验证器 + $v = $this->app->validate($validate); + if (!empty($scene)) { + $v->scene($scene); + } + } + + if (!empty($message)) { + $v->message($message); + } + + // 批量验证 + if ($batch) { + $v->batch(true); + } + + if (!$v->check($this->request->param())) { + throw new ValidateException($v->getError()); + } + } + + /** + * 处理路由请求缓存 + * @access protected + * @param Request $request 请求对象 + * @param string|array $cache 路由缓存 + * @return void + */ + protected function parseRequestCache($cache) + { + if (is_array($cache)) { + list($key, $expire, $tag) = array_pad($cache, 3, null); + } else { + $key = str_replace('|', '/', $this->request->url()); + $expire = $cache; + $tag = null; + } + + $cache = $this->request->cache($key, $expire, $tag); + $this->app->setResponseCache($cache); + } + + /** + * 路由绑定模型实例 + * @access protected + * @param array|\Clousre $bindModel 绑定模型 + * @param array $matches 路由变量 + * @return void + */ + protected function createBindModel($bindModel, $matches) + { + foreach ($bindModel as $key => $val) { + if ($val instanceof \Closure) { + $result = $this->app->invokeFunction($val, $matches); + } else { + $fields = explode('&', $key); + + if (is_array($val)) { + list($model, $exception) = $val; + } else { + $model = $val; + $exception = true; + } + + $where = []; + $match = true; + + foreach ($fields as $field) { + if (!isset($matches[$field])) { + $match = false; + break; + } else { + $where[] = [$field, '=', $matches[$field]]; + } + } + + if ($match) { + $query = strpos($model, '\\') ? $model::where($where) : $this->app->model($model)->where($where); + $result = $query->failException($exception)->find(); + } + } + + if (!empty($result)) { + // 注入容器 + $this->app->instance(get_class($result), $result); + } + } + } + + public function convert($convert) + { + $this->convert = $convert; + + return $this; + } + + public function getDispatch() + { + return $this->dispatch; + } + + public function getParam() + { + return $this->param; + } + + abstract public function exec(); + + public function __sleep() + { + return ['rule', 'dispatch', 'convert', 'param', 'code', 'controller', 'actionName']; + } + + public function __wakeup() + { + $this->app = Container::get('app'); + $this->request = $this->app['request']; + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['app'], $data['request'], $data['rule']); + + return $data; + } +} diff --git a/vendor/topthink/framework/library/think/route/Domain.php b/vendor/topthink/framework/library/think/route/Domain.php new file mode 100644 index 0000000..923d9b4 --- /dev/null +++ b/vendor/topthink/framework/library/think/route/Domain.php @@ -0,0 +1,237 @@ + +// +---------------------------------------------------------------------- + +namespace think\route; + +use think\Container; +use think\Loader; +use think\Request; +use think\Route; +use think\route\dispatch\Callback as CallbackDispatch; +use think\route\dispatch\Controller as ControllerDispatch; +use think\route\dispatch\Module as ModuleDispatch; + +class Domain extends RuleGroup +{ + /** + * 架构函数 + * @access public + * @param Route $router 路由对象 + * @param string $name 路由域名 + * @param mixed $rule 域名路由 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + */ + public function __construct(Route $router, $name = '', $rule = null, $option = [], $pattern = []) + { + $this->router = $router; + $this->domain = $name; + $this->option = $option; + $this->rule = $rule; + $this->pattern = $pattern; + } + + /** + * 检测域名路由 + * @access public + * @param Request $request 请求对象 + * @param string $url 访问地址 + * @param bool $completeMatch 路由是否完全匹配 + * @return Dispatch|false + */ + public function check($request, $url, $completeMatch = false) + { + // 检测别名路由 + $result = $this->checkRouteAlias($request, $url); + + if (false !== $result) { + return $result; + } + + // 检测URL绑定 + $result = $this->checkUrlBind($request, $url); + + if (!empty($this->option['append'])) { + $request->setRouteVars($this->option['append']); + unset($this->option['append']); + } + + if (false !== $result) { + return $result; + } + + // 添加域名中间件 + if (!empty($this->option['middleware'])) { + Container::get('middleware')->import($this->option['middleware']); + unset($this->option['middleware']); + } + + return parent::check($request, $url, $completeMatch); + } + + /** + * 设置路由绑定 + * @access public + * @param string $bind 绑定信息 + * @return $this + */ + public function bind($bind) + { + $this->router->bind($bind, $this->domain); + return $this; + } + + /** + * 检测路由别名 + * @access private + * @param Request $request + * @param string $url URL地址 + * @return Dispatch|false + */ + private function checkRouteAlias($request, $url) + { + $alias = strpos($url, '|') ? strstr($url, '|', true) : $url; + + $item = $this->router->getAlias($alias); + + return $item ? $item->check($request, $url) : false; + } + + /** + * 检测URL绑定 + * @access private + * @param Request $request + * @param string $url URL地址 + * @return Dispatch|false + */ + private function checkUrlBind($request, $url) + { + $bind = $this->router->getBind($this->domain); + + if (!empty($bind)) { + $this->parseBindAppendParam($bind); + + // 记录绑定信息 + Container::get('app')->log('[ BIND ] ' . var_export($bind, true)); + + // 如果有URL绑定 则进行绑定检测 + $type = substr($bind, 0, 1); + $bind = substr($bind, 1); + + $bindTo = [ + '\\' => 'bindToClass', + '@' => 'bindToController', + ':' => 'bindToNamespace', + ]; + + if (isset($bindTo[$type])) { + return $this->{$bindTo[$type]}($request, $url, $bind); + } + } + + return false; + } + + protected function parseBindAppendParam(&$bind) + { + if (false !== strpos($bind, '?')) { + list($bind, $query) = explode('?', $bind); + parse_str($query, $vars); + $this->append($vars); + } + } + + /** + * 绑定到类 + * @access protected + * @param Request $request + * @param string $url URL地址 + * @param string $class 类名(带命名空间) + * @return CallbackDispatch + */ + protected function bindToClass($request, $url, $class) + { + $array = explode('|', $url, 2); + $action = !empty($array[0]) ? $array[0] : $this->router->config('default_action'); + $param = []; + + if (!empty($array[1])) { + $this->parseUrlParams($request, $array[1], $param); + } + + return new CallbackDispatch($request, $this, [$class, $action], $param); + } + + /** + * 绑定到命名空间 + * @access protected + * @param Request $request + * @param string $url URL地址 + * @param string $namespace 命名空间 + * @return CallbackDispatch + */ + protected function bindToNamespace($request, $url, $namespace) + { + $array = explode('|', $url, 3); + $class = !empty($array[0]) ? $array[0] : $this->router->config('default_controller'); + $method = !empty($array[1]) ? $array[1] : $this->router->config('default_action'); + $param = []; + + if (!empty($array[2])) { + $this->parseUrlParams($request, $array[2], $param); + } + + return new CallbackDispatch($request, $this, [$namespace . '\\' . Loader::parseName($class, 1), $method], $param); + } + + /** + * 绑定到控制器类 + * @access protected + * @param Request $request + * @param string $url URL地址 + * @param string $controller 控制器名 (支持带模块名 index/user ) + * @return ControllerDispatch + */ + protected function bindToController($request, $url, $controller) + { + $array = explode('|', $url, 2); + $action = !empty($array[0]) ? $array[0] : $this->router->config('default_action'); + $param = []; + + if (!empty($array[1])) { + $this->parseUrlParams($request, $array[1], $param); + } + + return new ControllerDispatch($request, $this, $controller . '/' . $action, $param); + } + + /** + * 绑定到模块/控制器 + * @access protected + * @param Request $request + * @param string $url URL地址 + * @param string $controller 控制器类名(带命名空间) + * @return ModuleDispatch + */ + protected function bindToModule($request, $url, $controller) + { + $array = explode('|', $url, 2); + $action = !empty($array[0]) ? $array[0] : $this->router->config('default_action'); + $param = []; + + if (!empty($array[1])) { + $this->parseUrlParams($request, $array[1], $param); + } + + return new ModuleDispatch($request, $this, $controller . '/' . $action, $param); + } + +} diff --git a/vendor/topthink/framework/library/think/route/Resource.php b/vendor/topthink/framework/library/think/route/Resource.php new file mode 100644 index 0000000..ff13928 --- /dev/null +++ b/vendor/topthink/framework/library/think/route/Resource.php @@ -0,0 +1,126 @@ + +// +---------------------------------------------------------------------- + +namespace think\route; + +use think\Route; + +class Resource extends RuleGroup +{ + // 资源路由名称 + protected $resource; + + // REST路由方法定义 + protected $rest = []; + + /** + * 架构函数 + * @access public + * @param Route $router 路由对象 + * @param RuleGroup $parent 上级对象 + * @param string $name 资源名称 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @param array $rest 资源定义 + */ + public function __construct(Route $router, RuleGroup $parent = null, $name = '', $route = '', $option = [], $pattern = [], $rest = []) + { + $this->router = $router; + $this->parent = $parent; + $this->resource = $name; + $this->route = $route; + $this->name = strpos($name, '.') ? strstr($name, '.', true) : $name; + + $this->setFullName(); + + // 资源路由默认为完整匹配 + $option['complete_match'] = true; + + $this->pattern = $pattern; + $this->option = $option; + $this->rest = $rest; + + if ($this->parent) { + $this->domain = $this->parent->getDomain(); + $this->parent->addRuleItem($this); + } + + if ($router->isTest()) { + $this->buildResourceRule(); + } + } + + /** + * 生成资源路由规则 + * @access protected + * @return void + */ + protected function buildResourceRule() + { + $origin = $this->router->getGroup(); + $this->router->setGroup($this); + + $rule = $this->resource; + $option = $this->option; + + if (strpos($rule, '.')) { + // 注册嵌套资源路由 + $array = explode('.', $rule); + $last = array_pop($array); + $item = []; + + foreach ($array as $val) { + $item[] = $val . '/<' . (isset($option['var'][$val]) ? $option['var'][$val] : $val . '_id') . '>'; + } + + $rule = implode('/', $item) . '/' . $last; + } + + $prefix = substr($rule, strlen($this->name) + 1); + + // 注册资源路由 + foreach ($this->rest as $key => $val) { + if ((isset($option['only']) && !in_array($key, $option['only'])) + || (isset($option['except']) && in_array($key, $option['except']))) { + continue; + } + + if (isset($last) && strpos($val[1], '') && isset($option['var'][$last])) { + $val[1] = str_replace('', '<' . $option['var'][$last] . '>', $val[1]); + } elseif (strpos($val[1], '') && isset($option['var'][$rule])) { + $val[1] = str_replace('', '<' . $option['var'][$rule] . '>', $val[1]); + } + + $this->addRule(trim($prefix . $val[1], '/'), $this->route . '/' . $val[2], $val[0]); + } + + $this->router->setGroup($origin); + } + + /** + * rest方法定义和修改 + * @access public + * @param string $name 方法名称 + * @param array|bool $resource 资源 + * @return $this + */ + public function rest($name, $resource = []) + { + if (is_array($name)) { + $this->rest = $resource ? $name : array_merge($this->rest, $name); + } else { + $this->rest[$name] = $resource; + } + + return $this; + } +} diff --git a/vendor/topthink/framework/library/think/route/Rule.php b/vendor/topthink/framework/library/think/route/Rule.php new file mode 100644 index 0000000..996305f --- /dev/null +++ b/vendor/topthink/framework/library/think/route/Rule.php @@ -0,0 +1,1130 @@ + +// +---------------------------------------------------------------------- + +namespace think\route; + +use think\Container; +use think\Request; +use think\Response; +use think\route\dispatch\Callback as CallbackDispatch; +use think\route\dispatch\Controller as ControllerDispatch; +use think\route\dispatch\Module as ModuleDispatch; +use think\route\dispatch\Redirect as RedirectDispatch; +use think\route\dispatch\Response as ResponseDispatch; +use think\route\dispatch\View as ViewDispatch; + +abstract class Rule +{ + /** + * 路由标识 + * @var string + */ + protected $name; + + /** + * 路由对象 + * @var Route + */ + protected $router; + + /** + * 路由所属分组 + * @var RuleGroup + */ + protected $parent; + + /** + * 路由规则 + * @var mixed + */ + protected $rule; + + /** + * 路由地址 + * @var string|\Closure + */ + protected $route; + + /** + * 请求类型 + * @var string + */ + protected $method; + + /** + * 路由变量 + * @var array + */ + protected $vars = []; + + /** + * 路由参数 + * @var array + */ + protected $option = []; + + /** + * 路由变量规则 + * @var array + */ + protected $pattern = []; + + /** + * 需要和分组合并的路由参数 + * @var array + */ + protected $mergeOptions = ['after', 'model', 'header', 'response', 'append', 'middleware']; + + /** + * 是否需要后置操作 + * @var bool + */ + protected $doAfter; + + /** + * 是否锁定参数 + * @var bool + */ + protected $lockOption = false; + + abstract public function check($request, $url, $completeMatch = false); + + /** + * 获取Name + * @access public + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 获取当前路由规则 + * @access public + * @return string + */ + public function getRule() + { + return $this->rule; + } + + /** + * 获取当前路由地址 + * @access public + * @return mixed + */ + public function getRoute() + { + return $this->route; + } + + /** + * 获取当前路由的请求类型 + * @access public + * @return string + */ + public function getMethod() + { + return strtolower($this->method); + } + + /** + * 获取当前路由的变量 + * @access public + * @return array + */ + public function getVars() + { + return $this->vars; + } + + /** + * 获取路由对象 + * @access public + * @return Route + */ + public function getRouter() + { + return $this->router; + } + + /** + * 路由是否有后置操作 + * @access public + * @return bool + */ + public function doAfter() + { + return $this->doAfter; + } + + /** + * 获取路由分组 + * @access public + * @return RuleGroup|null + */ + public function getParent() + { + return $this->parent; + } + + /** + * 获取路由所在域名 + * @access public + * @return string + */ + public function getDomain() + { + return $this->parent->getDomain(); + } + + /** + * 获取变量规则定义 + * @access public + * @param string $name 变量名 + * @return mixed + */ + public function getPattern($name = '') + { + if ('' === $name) { + return $this->pattern; + } + + return isset($this->pattern[$name]) ? $this->pattern[$name] : null; + } + + /** + * 获取路由参数 + * @access public + * @param string $name 变量名 + * @return mixed + */ + public function getConfig($name = '') + { + return $this->router->config($name); + } + + /** + * 获取路由参数定义 + * @access public + * @param string $name 参数名 + * @return mixed + */ + public function getOption($name = '') + { + if ('' === $name) { + return $this->option; + } + + return isset($this->option[$name]) ? $this->option[$name] : null; + } + + /** + * 注册路由参数 + * @access public + * @param string|array $name 参数名 + * @param mixed $value 值 + * @return $this + */ + public function option($name, $value = '') + { + if (is_array($name)) { + $this->option = array_merge($this->option, $name); + } else { + $this->option[$name] = $value; + } + + return $this; + } + + /** + * 注册变量规则 + * @access public + * @param string|array $name 变量名 + * @param string $rule 变量规则 + * @return $this + */ + public function pattern($name, $rule = '') + { + if (is_array($name)) { + $this->pattern = array_merge($this->pattern, $name); + } else { + $this->pattern[$name] = $rule; + } + + return $this; + } + + /** + * 设置标识 + * @access public + * @param string $name 标识名 + * @return $this + */ + public function name($name) + { + $this->name = $name; + + return $this; + } + + /** + * 设置变量 + * @access public + * @param array $vars 变量 + * @return $this + */ + public function vars($vars) + { + $this->vars = $vars; + + return $this; + } + + /** + * 设置路由请求类型 + * @access public + * @param string $method + * @return $this + */ + public function method($method) + { + return $this->option('method', strtolower($method)); + } + + /** + * 设置路由前置行为 + * @access public + * @param array|\Closure $before + * @return $this + */ + public function before($before) + { + return $this->option('before', $before); + } + + /** + * 设置路由后置行为 + * @access public + * @param array|\Closure $after + * @return $this + */ + public function after($after) + { + return $this->option('after', $after); + } + + /** + * 检查后缀 + * @access public + * @param string $ext + * @return $this + */ + public function ext($ext = '') + { + return $this->option('ext', $ext); + } + + /** + * 检查禁止后缀 + * @access public + * @param string $ext + * @return $this + */ + public function denyExt($ext = '') + { + return $this->option('deny_ext', $ext); + } + + /** + * 检查域名 + * @access public + * @param string $domain + * @return $this + */ + public function domain($domain) + { + return $this->option('domain', $domain); + } + + /** + * 设置参数过滤检查 + * @access public + * @param string|array $name + * @param mixed $value + * @return $this + */ + public function filter($name, $value = null) + { + if (is_array($name)) { + $this->option['filter'] = $name; + } else { + $this->option['filter'][$name] = $value; + } + + return $this; + } + + /** + * 绑定模型 + * @access public + * @param array|string $var 路由变量名 多个使用 & 分割 + * @param string|\Closure $model 绑定模型类 + * @param bool $exception 是否抛出异常 + * @return $this + */ + public function model($var, $model = null, $exception = true) + { + if ($var instanceof \Closure) { + $this->option['model'][] = $var; + } elseif (is_array($var)) { + $this->option['model'] = $var; + } elseif (is_null($model)) { + $this->option['model']['id'] = [$var, true]; + } else { + $this->option['model'][$var] = [$model, $exception]; + } + + return $this; + } + + /** + * 附加路由隐式参数 + * @access public + * @param array $append + * @return $this + */ + public function append(array $append = []) + { + if (isset($this->option['append'])) { + $this->option['append'] = array_merge($this->option['append'], $append); + } else { + $this->option['append'] = $append; + } + + return $this; + } + + /** + * 绑定验证 + * @access public + * @param mixed $validate 验证器类 + * @param string $scene 验证场景 + * @param array $message 验证提示 + * @param bool $batch 批量验证 + * @return $this + */ + public function validate($validate, $scene = null, $message = [], $batch = false) + { + $this->option['validate'] = [$validate, $scene, $message, $batch]; + + return $this; + } + + /** + * 绑定Response对象 + * @access public + * @param mixed $response + * @return $this + */ + public function response($response) + { + $this->option['response'][] = $response; + return $this; + } + + /** + * 设置Response Header信息 + * @access public + * @param string|array $name 参数名 + * @param string $value 参数值 + * @return $this + */ + public function header($header, $value = null) + { + if (is_array($header)) { + $this->option['header'] = $header; + } else { + $this->option['header'][$header] = $value; + } + + return $this; + } + + /** + * 指定路由中间件 + * @access public + * @param string|array|\Closure $middleware + * @param mixed $param + * @return $this + */ + public function middleware($middleware, $param = null) + { + if (is_null($param) && is_array($middleware)) { + $this->option['middleware'] = $middleware; + } else { + foreach ((array) $middleware as $item) { + $this->option['middleware'][] = [$item, $param]; + } + } + + return $this; + } + + /** + * 设置路由缓存 + * @access public + * @param array|string $cache + * @return $this + */ + public function cache($cache) + { + return $this->option('cache', $cache); + } + + /** + * 检查URL分隔符 + * @access public + * @param bool $depr + * @return $this + */ + public function depr($depr) + { + return $this->option('param_depr', $depr); + } + + /** + * 是否合并额外参数 + * @access public + * @param bool $merge + * @return $this + */ + public function mergeExtraVars($merge = true) + { + return $this->option('merge_extra_vars', $merge); + } + + /** + * 设置需要合并的路由参数 + * @access public + * @param array $option + * @return $this + */ + public function mergeOptions($option = []) + { + $this->mergeOptions = array_merge($this->mergeOptions, $option); + return $this; + } + + /** + * 检查是否为HTTPS请求 + * @access public + * @param bool $https + * @return $this + */ + public function https($https = true) + { + return $this->option('https', $https); + } + + /** + * 检查是否为AJAX请求 + * @access public + * @param bool $ajax + * @return $this + */ + public function ajax($ajax = true) + { + return $this->option('ajax', $ajax); + } + + /** + * 检查是否为PJAX请求 + * @access public + * @param bool $pjax + * @return $this + */ + public function pjax($pjax = true) + { + return $this->option('pjax', $pjax); + } + + /** + * 检查是否为手机访问 + * @access public + * @param bool $mobile + * @return $this + */ + public function mobile($mobile = true) + { + return $this->option('mobile', $mobile); + } + + /** + * 当前路由到一个模板地址 当使用数组的时候可以传入模板变量 + * @access public + * @param bool|array $view + * @return $this + */ + public function view($view = true) + { + return $this->option('view', $view); + } + + /** + * 当前路由为重定向 + * @access public + * @param bool $redirect 是否为重定向 + * @return $this + */ + public function redirect($redirect = true) + { + return $this->option('redirect', $redirect); + } + + /** + * 设置路由完整匹配 + * @access public + * @param bool $match + * @return $this + */ + public function completeMatch($match = true) + { + return $this->option('complete_match', $match); + } + + /** + * 是否去除URL最后的斜线 + * @access public + * @param bool $remove + * @return $this + */ + public function removeSlash($remove = true) + { + return $this->option('remove_slash', $remove); + } + + /** + * 设置是否允许跨域 + * @access public + * @param bool $allow + * @param array $header + * @return $this + */ + public function allowCrossDomain($allow = true, $header = []) + { + if (!empty($header)) { + $this->header($header); + } + + if ($allow && $this->parent) { + $this->parent->addRuleItem($this, 'options'); + } + + return $this->option('cross_domain', $allow); + } + + /** + * 检查OPTIONS请求 + * @access public + * @param Request $request + * @return Dispatch|void + */ + protected function checkCrossDomain($request) + { + if (!empty($this->option['cross_domain'])) { + $header = [ + 'Access-Control-Allow-Credentials' => 'true', + 'Access-Control-Allow-Methods' => 'GET, POST, PATCH, PUT, DELETE', + 'Access-Control-Allow-Headers' => 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With', + ]; + + if (!empty($this->option['header'])) { + $header = array_merge($header, $this->option['header']); + } + + if (!isset($header['Access-Control-Allow-Origin'])) { + $httpOrigin = $request->header('origin'); + + if ($httpOrigin && strpos(config('cookie.domain'), $httpOrigin)) { + $header['Access-Control-Allow-Origin'] = $httpOrigin; + } else { + $header['Access-Control-Allow-Origin'] = '*'; + } + } + + $this->option['header'] = $header; + + if ($request->method(true) == 'OPTIONS') { + return new ResponseDispatch($request, $this, Response::create()->code(204)->header($header)); + } + } + } + + /** + * 设置路由规则全局有效 + * @access public + * @return $this + */ + public function crossDomainRule() + { + if ($this instanceof RuleGroup) { + $method = '*'; + } else { + $method = $this->method; + } + + $this->router->setCrossDomainRule($this, $method); + + return $this; + } + + /** + * 合并分组参数 + * @access public + * @return array + */ + public function mergeGroupOptions() + { + if (!$this->lockOption) { + $parentOption = $this->parent->getOption(); + // 合并分组参数 + foreach ($this->mergeOptions as $item) { + if (isset($parentOption[$item]) && isset($this->option[$item])) { + $this->option[$item] = array_merge($parentOption[$item], $this->option[$item]); + } + } + + $this->option = array_merge($parentOption, $this->option); + $this->lockOption = true; + } + + return $this->option; + } + + /** + * 解析匹配到的规则路由 + * @access public + * @param Request $request 请求对象 + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $url URL地址 + * @param array $option 路由参数 + * @param array $matches 匹配的变量 + * @return Dispatch + */ + public function parseRule($request, $rule, $route, $url, $option = [], $matches = []) + { + if (is_string($route) && isset($option['prefix'])) { + // 路由地址前缀 + $route = $option['prefix'] . $route; + } + + // 替换路由地址中的变量 + if (is_string($route) && !empty($matches)) { + $search = $replace = []; + + foreach ($matches as $key => $value) { + $search[] = '<' . $key . '>'; + $replace[] = $value; + + $search[] = ':' . $key; + $replace[] = $value; + } + + $route = str_replace($search, $replace, $route); + } + + // 解析额外参数 + $count = substr_count($rule, '/'); + $url = array_slice(explode('|', $url), $count + 1); + $this->parseUrlParams($request, implode('|', $url), $matches); + + $this->vars = $matches; + $this->option = $option; + $this->doAfter = true; + + // 发起路由调度 + return $this->dispatch($request, $route, $option); + } + + /** + * 检查路由前置行为 + * @access protected + * @param mixed $before 前置行为 + * @return mixed + */ + protected function checkBefore($before) + { + $hook = Container::get('hook'); + + foreach ((array) $before as $behavior) { + $result = $hook->exec($behavior); + + if (false === $result) { + return false; + } + } + } + + /** + * 发起路由调度 + * @access protected + * @param Request $request Request对象 + * @param mixed $route 路由地址 + * @param array $option 路由参数 + * @return Dispatch + */ + protected function dispatch($request, $route, $option) + { + if ($route instanceof \Closure) { + // 执行闭包 + $result = new CallbackDispatch($request, $this, $route); + } elseif ($route instanceof Response) { + $result = new ResponseDispatch($request, $this, $route); + } elseif (isset($option['view']) && false !== $option['view']) { + $result = new ViewDispatch($request, $this, $route, is_array($option['view']) ? $option['view'] : []); + } elseif (!empty($option['redirect']) || 0 === strpos($route, '/') || strpos($route, '://')) { + // 路由到重定向地址 + $result = new RedirectDispatch($request, $this, $route, [], isset($option['status']) ? $option['status'] : 301); + } elseif (false !== strpos($route, '\\')) { + // 路由到方法 + $result = $this->dispatchMethod($request, $route); + } elseif (0 === strpos($route, '@')) { + // 路由到控制器 + $result = $this->dispatchController($request, substr($route, 1)); + } else { + // 路由到模块/控制器/操作 + $result = $this->dispatchModule($request, $route); + } + + return $result; + } + + /** + * 解析URL地址为 模块/控制器/操作 + * @access protected + * @param Request $request Request对象 + * @param string $route 路由地址 + * @return CallbackDispatch + */ + protected function dispatchMethod($request, $route) + { + list($path, $var) = $this->parseUrlPath($route); + + $route = str_replace('/', '@', implode('/', $path)); + $method = strpos($route, '@') ? explode('@', $route) : $route; + + return new CallbackDispatch($request, $this, $method, $var); + } + + /** + * 解析URL地址为 模块/控制器/操作 + * @access protected + * @param Request $request Request对象 + * @param string $route 路由地址 + * @return ControllerDispatch + */ + protected function dispatchController($request, $route) + { + list($route, $var) = $this->parseUrlPath($route); + + $result = new ControllerDispatch($request, $this, implode('/', $route), $var); + + $request->setAction(array_pop($route)); + $request->setController($route ? array_pop($route) : $this->getConfig('default_controller')); + $request->setModule($route ? array_pop($route) : $this->getConfig('default_module')); + + return $result; + } + + /** + * 解析URL地址为 模块/控制器/操作 + * @access protected + * @param Request $request Request对象 + * @param string $route 路由地址 + * @return ModuleDispatch + */ + protected function dispatchModule($request, $route) + { + list($path, $var) = $this->parseUrlPath($route); + + $action = array_pop($path); + $controller = !empty($path) ? array_pop($path) : null; + $module = $this->getConfig('app_multi_module') && !empty($path) ? array_pop($path) : null; + $method = $request->method(); + + if ($this->getConfig('use_action_prefix') && $this->router->getMethodPrefix($method)) { + $prefix = $this->router->getMethodPrefix($method); + // 操作方法前缀支持 + $action = 0 !== strpos($action, $prefix) ? $prefix . $action : $action; + } + + // 设置当前请求的路由变量 + $request->setRouteVars($var); + + // 路由到模块/控制器/操作 + return new ModuleDispatch($request, $this, [$module, $controller, $action], ['convert' => false]); + } + + /** + * 路由检查 + * @access protected + * @param array $option 路由参数 + * @param Request $request Request对象 + * @return bool + */ + protected function checkOption($option, Request $request) + { + // 请求类型检测 + if (!empty($option['method'])) { + if (is_string($option['method']) && false === stripos($option['method'], $request->method())) { + return false; + } + } + + // AJAX PJAX 请求检查 + foreach (['ajax', 'pjax', 'mobile'] as $item) { + if (isset($option[$item])) { + $call = 'is' . $item; + if ($option[$item] && !$request->$call() || !$option[$item] && $request->$call()) { + return false; + } + } + } + + // 伪静态后缀检测 + if ($request->url() != '/' && ((isset($option['ext']) && false === stripos('|' . $option['ext'] . '|', '|' . $request->ext() . '|')) + || (isset($option['deny_ext']) && false !== stripos('|' . $option['deny_ext'] . '|', '|' . $request->ext() . '|')))) { + return false; + } + + // 域名检查 + if ((isset($option['domain']) && !in_array($option['domain'], [$request->host(true), $request->subDomain()]))) { + return false; + } + + // HTTPS检查 + if ((isset($option['https']) && $option['https'] && !$request->isSsl()) + || (isset($option['https']) && !$option['https'] && $request->isSsl())) { + return false; + } + + // 请求参数检查 + if (isset($option['filter'])) { + foreach ($option['filter'] as $name => $value) { + if ($request->param($name, '', null) != $value) { + return false; + } + } + } + return true; + } + + /** + * 解析URL地址中的参数Request对象 + * @access protected + * @param Request $request + * @param string $rule 路由规则 + * @param array $var 变量 + * @return void + */ + protected function parseUrlParams($request, $url, &$var = []) + { + if ($url) { + if ($this->getConfig('url_param_type')) { + $var += explode('|', $url); + } else { + preg_replace_callback('/(\w+)\|([^\|]+)/', function ($match) use (&$var) { + $var[$match[1]] = strip_tags($match[2]); + }, $url); + } + } + } + + /** + * 解析URL的pathinfo参数和变量 + * @access public + * @param string $url URL地址 + * @return array + */ + public function parseUrlPath($url) + { + // 分隔符替换 确保路由定义使用统一的分隔符 + $url = str_replace('|', '/', $url); + $url = trim($url, '/'); + $var = []; + + if (false !== strpos($url, '?')) { + // [模块/控制器/操作?]参数1=值1&参数2=值2... + $info = parse_url($url); + $path = explode('/', $info['path']); + parse_str($info['query'], $var); + } elseif (strpos($url, '/')) { + // [模块/控制器/操作] + $path = explode('/', $url); + } elseif (false !== strpos($url, '=')) { + // 参数1=值1&参数2=值2... + $path = []; + parse_str($url, $var); + } else { + $path = [$url]; + } + + return [$path, $var]; + } + + /** + * 生成路由的正则规则 + * @access protected + * @param string $rule 路由规则 + * @param array $match 匹配的变量 + * @param array $pattern 路由变量规则 + * @param array $option 路由参数 + * @param bool $completeMatch 路由是否完全匹配 + * @param string $suffix 路由正则变量后缀 + * @return string + */ + protected function buildRuleRegex($rule, $match, $pattern = [], $option = [], $completeMatch = false, $suffix = '') + { + foreach ($match as $name) { + $replace[] = $this->buildNameRegex($name, $pattern, $suffix); + } + + // 是否区分 / 地址访问 + if ('/' != $rule) { + if (!empty($option['remove_slash'])) { + $rule = rtrim($rule, '/'); + } elseif (substr($rule, -1) == '/') { + $rule = rtrim($rule, '/'); + $hasSlash = true; + } + } + + $regex = str_replace(array_unique($match), array_unique($replace), $rule); + $regex = str_replace([')?/', ')/', ')?-', ')-', '\\\\/'], [')\/', ')\/', ')\-', ')\-', '\/'], $regex); + + if (isset($hasSlash)) { + $regex .= '\/'; + } + + return $regex . ($completeMatch ? '$' : ''); + } + + /** + * 生成路由变量的正则规则 + * @access protected + * @param string $name 路由变量 + * @param string $pattern 变量规则 + * @param string $suffix 路由正则变量后缀 + * @return string + */ + protected function buildNameRegex($name, $pattern, $suffix) + { + $optional = ''; + $slash = substr($name, 0, 1); + + if (in_array($slash, ['/', '-'])) { + $prefix = '\\' . $slash; + $name = substr($name, 1); + $slash = substr($name, 0, 1); + } else { + $prefix = ''; + } + + if ('<' != $slash) { + return $prefix . preg_quote($name, '/'); + } + + if (strpos($name, '?')) { + $name = substr($name, 1, -2); + $optional = '?'; + } elseif (strpos($name, '>')) { + $name = substr($name, 1, -1); + } + + if (isset($pattern[$name])) { + $nameRule = $pattern[$name]; + if (0 === strpos($nameRule, '/') && '/' == substr($nameRule, -1)) { + $nameRule = substr($nameRule, 1, -1); + } + } else { + $nameRule = $this->getConfig('default_route_pattern'); + } + + return '(' . $prefix . '(?<' . $name . $suffix . '>' . $nameRule . '))' . $optional; + } + + /** + * 分析路由规则中的变量 + * @access protected + * @param string $rule 路由规则 + * @return array + */ + protected function parseVar($rule) + { + // 提取路由规则中的变量 + $var = []; + + if (preg_match_all('/<\w+\??>/', $rule, $matches)) { + foreach ($matches[0] as $name) { + $optional = false; + + if (strpos($name, '?')) { + $name = substr($name, 1, -2); + $optional = true; + } else { + $name = substr($name, 1, -1); + } + + $var[$name] = $optional ? 2 : 1; + } + } + + return $var; + } + + /** + * 设置路由参数 + * @access public + * @param string $method 方法名 + * @param array $args 调用参数 + * @return $this + */ + public function __call($method, $args) + { + if (count($args) > 1) { + $args[0] = $args; + } + array_unshift($args, $method); + + return call_user_func_array([$this, 'option'], $args); + } + + public function __sleep() + { + return ['name', 'rule', 'route', 'method', 'vars', 'option', 'pattern', 'doAfter']; + } + + public function __wakeup() + { + $this->router = Container::get('route'); + } + + public function __debugInfo() + { + $data = get_object_vars($this); + unset($data['parent'], $data['router'], $data['route']); + + return $data; + } +} diff --git a/vendor/topthink/framework/library/think/route/RuleGroup.php b/vendor/topthink/framework/library/think/route/RuleGroup.php new file mode 100644 index 0000000..5781d8c --- /dev/null +++ b/vendor/topthink/framework/library/think/route/RuleGroup.php @@ -0,0 +1,601 @@ + +// +---------------------------------------------------------------------- + +namespace think\route; + +use think\Container; +use think\Exception; +use think\Request; +use think\Response; +use think\Route; +use think\route\dispatch\Response as ResponseDispatch; +use think\route\dispatch\Url as UrlDispatch; + +class RuleGroup extends Rule +{ + // 分组路由(包括子分组) + protected $rules = [ + '*' => [], + 'get' => [], + 'post' => [], + 'put' => [], + 'patch' => [], + 'delete' => [], + 'head' => [], + 'options' => [], + ]; + + // MISS路由 + protected $miss; + + // 自动路由 + protected $auto; + + // 完整名称 + protected $fullName; + + // 所在域名 + protected $domain; + + /** + * 架构函数 + * @access public + * @param Route $router 路由对象 + * @param RuleGroup $parent 上级对象 + * @param string $name 分组名称 + * @param mixed $rule 分组路由 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + */ + public function __construct(Route $router, RuleGroup $parent = null, $name = '', $rule = [], $option = [], $pattern = []) + { + $this->router = $router; + $this->parent = $parent; + $this->rule = $rule; + $this->name = trim($name, '/'); + $this->option = $option; + $this->pattern = $pattern; + + $this->setFullName(); + + if ($this->parent) { + $this->domain = $this->parent->getDomain(); + $this->parent->addRuleItem($this); + } + + if (!empty($option['cross_domain'])) { + $this->router->setCrossDomainRule($this); + } + + if ($router->isTest()) { + $this->lazy(false); + } + } + + /** + * 设置分组的路由规则 + * @access public + * @return void + */ + protected function setFullName() + { + if (false !== strpos($this->name, ':')) { + $this->name = preg_replace(['/\[\:(\w+)\]/', '/\:(\w+)/'], ['<\1?>', '<\1>'], $this->name); + } + + if ($this->parent && $this->parent->getFullName()) { + $this->fullName = $this->parent->getFullName() . ($this->name ? '/' . $this->name : ''); + } else { + $this->fullName = $this->name; + } + } + + /** + * 获取所属域名 + * @access public + * @return string + */ + public function getDomain() + { + return $this->domain; + } + + /** + * 检测分组路由 + * @access public + * @param Request $request 请求对象 + * @param string $url 访问地址 + * @param bool $completeMatch 路由是否完全匹配 + * @return Dispatch|false + */ + public function check($request, $url, $completeMatch = false) + { + // 跨域OPTIONS请求 + if ($dispatch = $this->checkCrossDomain($request)) { + return $dispatch; + } + + // 检查分组有效性 + if (!$this->checkOption($this->option, $request) || !$this->checkUrl($url)) { + return false; + } + + // 检查前置行为 + if (isset($this->option['before'])) { + if (false === $this->checkBefore($this->option['before'])) { + return false; + } + unset($this->option['before']); + } + + // 解析分组路由 + if ($this instanceof Resource) { + $this->buildResourceRule(); + } elseif ($this->rule) { + if ($this->rule instanceof Response) { + return new ResponseDispatch($request, $this, $this->rule); + } + + $this->parseGroupRule($this->rule); + } + + // 获取当前路由规则 + $method = strtolower($request->method()); + $rules = $this->getMethodRules($method); + + if ($this->parent) { + // 合并分组参数 + $this->mergeGroupOptions(); + // 合并分组变量规则 + $this->pattern = array_merge($this->parent->getPattern(), $this->pattern); + } + + if (isset($this->option['complete_match'])) { + $completeMatch = $this->option['complete_match']; + } + + if (!empty($this->option['merge_rule_regex'])) { + // 合并路由正则规则进行路由匹配检查 + $result = $this->checkMergeRuleRegex($request, $rules, $url, $completeMatch); + + if (false !== $result) { + return $result; + } + } + + // 检查分组路由 + foreach ($rules as $key => $item) { + $result = $item->check($request, $url, $completeMatch); + + if (false !== $result) { + return $result; + } + } + + if ($this->auto) { + // 自动解析URL地址 + $result = new UrlDispatch($request, $this, $this->auto . '/' . $url, ['auto_search' => false]); + } elseif ($this->miss && in_array($this->miss->getMethod(), ['*', $method])) { + // 未匹配所有路由的路由规则处理 + $result = $this->miss->parseRule($request, '', $this->miss->getRoute(), $url, $this->miss->mergeGroupOptions()); + } else { + $result = false; + } + + return $result; + } + + /** + * 获取当前请求的路由规则(包括子分组、资源路由) + * @access protected + * @param string $method + * @return array + */ + protected function getMethodRules($method) + { + return array_merge($this->rules[$method], $this->rules['*']); + } + + /** + * 分组URL匹配检查 + * @access protected + * @param string $url + * @return bool + */ + protected function checkUrl($url) + { + if ($this->fullName) { + $pos = strpos($this->fullName, '<'); + + if (false !== $pos) { + $str = substr($this->fullName, 0, $pos); + } else { + $str = $this->fullName; + } + + if ($str && 0 !== stripos(str_replace('|', '/', $url), $str)) { + return false; + } + } + + return true; + } + + /** + * 延迟解析分组的路由规则 + * @access public + * @param bool $lazy 路由是否延迟解析 + * @return $this + */ + public function lazy($lazy = true) + { + if (!$lazy) { + $this->parseGroupRule($this->rule); + $this->rule = null; + } + + return $this; + } + + /** + * 解析分组和域名的路由规则及绑定 + * @access public + * @param mixed $rule 路由规则 + * @return void + */ + public function parseGroupRule($rule) + { + $origin = $this->router->getGroup(); + $this->router->setGroup($this); + + if ($rule instanceof \Closure) { + Container::getInstance()->invokeFunction($rule); + } elseif (is_array($rule)) { + $this->addRules($rule); + } elseif (is_string($rule) && $rule) { + $this->router->bind($rule, $this->domain); + } + + $this->router->setGroup($origin); + } + + /** + * 检测分组路由 + * @access public + * @param Request $request 请求对象 + * @param array $rules 路由规则 + * @param string $url 访问地址 + * @param bool $completeMatch 路由是否完全匹配 + * @return Dispatch|false + */ + protected function checkMergeRuleRegex($request, &$rules, $url, $completeMatch) + { + $depr = $this->router->config('pathinfo_depr'); + $url = $depr . str_replace('|', $depr, $url); + + foreach ($rules as $key => $item) { + if ($item instanceof RuleItem) { + $rule = $depr . str_replace('/', $depr, $item->getRule()); + if ($depr == $rule && $depr != $url) { + unset($rules[$key]); + continue; + } + + $complete = null !== $item->getOption('complete_match') ? $item->getOption('complete_match') : $completeMatch; + + if (false === strpos($rule, '<')) { + if (0 === strcasecmp($rule, $url) || (!$complete && 0 === strncasecmp($rule, $url, strlen($rule)))) { + return $item->checkRule($request, $url, []); + } + + unset($rules[$key]); + continue; + } + + $slash = preg_quote('/-' . $depr, '/'); + + if ($matchRule = preg_split('/[' . $slash . ']<\w+\??>/', $rule, 2)) { + if ($matchRule[0] && 0 !== strncasecmp($rule, $url, strlen($matchRule[0]))) { + unset($rules[$key]); + continue; + } + } + + if (preg_match_all('/[' . $slash . ']??/', $rule, $matches)) { + unset($rules[$key]); + $pattern = array_merge($this->getPattern(), $item->getPattern()); + $option = array_merge($this->getOption(), $item->getOption()); + + $regex[$key] = $this->buildRuleRegex($rule, $matches[0], $pattern, $option, $complete, '_THINK_' . $key); + $items[$key] = $item; + } + } + } + + if (empty($regex)) { + return false; + } + + try { + $result = preg_match('/^(?:' . implode('|', $regex) . ')/u', $url, $match); + } catch (\Exception $e) { + throw new Exception('route pattern error'); + } + + if ($result) { + $var = []; + foreach ($match as $key => $val) { + if (is_string($key) && '' !== $val) { + list($name, $pos) = explode('_THINK_', $key); + + $var[$name] = $val; + } + } + + if (!isset($pos)) { + foreach ($regex as $key => $item) { + if (0 === strpos(str_replace(['\/', '\-', '\\' . $depr], ['/', '-', $depr], $item), $match[0])) { + $pos = $key; + break; + } + } + } + + $rule = $items[$pos]->getRule(); + $array = $this->router->getRule($rule); + + foreach ($array as $item) { + if (in_array($item->getMethod(), ['*', strtolower($request->method())])) { + $result = $item->checkRule($request, $url, $var); + + if (false !== $result) { + return $result; + } + } + } + } + + return false; + } + + /** + * 获取分组的MISS路由 + * @access public + * @return RuleItem|null + */ + public function getMissRule() + { + return $this->miss; + } + + /** + * 获取分组的自动路由 + * @access public + * @return string + */ + public function getAutoRule() + { + return $this->auto; + } + + /** + * 注册自动路由 + * @access public + * @param string $route 路由规则 + * @return void + */ + public function addAutoRule($route) + { + $this->auto = $route; + } + + /** + * 注册MISS路由 + * @access public + * @param string $route 路由地址 + * @param string $method 请求类型 + * @param array $option 路由参数 + * @return RuleItem + */ + public function addMissRule($route, $method = '*', $option = []) + { + // 创建路由规则实例 + $ruleItem = new RuleItem($this->router, $this, null, '', $route, strtolower($method), $option); + + $this->miss = $ruleItem; + + return $ruleItem; + } + + /** + * 添加分组下的路由规则或者子分组 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $method 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return $this + */ + public function addRule($rule, $route, $method = '*', $option = [], $pattern = []) + { + // 读取路由标识 + if (is_array($rule)) { + $name = $rule[0]; + $rule = $rule[1]; + } elseif (is_string($route)) { + $name = $route; + } else { + $name = null; + } + + $method = strtolower($method); + + if ('/' === $rule || '' === $rule) { + // 首页自动完整匹配 + $rule .= '$'; + } + + // 创建路由规则实例 + $ruleItem = new RuleItem($this->router, $this, $name, $rule, $route, $method, $option, $pattern); + + if (!empty($option['cross_domain'])) { + $this->router->setCrossDomainRule($ruleItem, $method); + } + + $this->addRuleItem($ruleItem, $method); + + return $ruleItem; + } + + /** + * 批量注册路由规则 + * @access public + * @param array $rules 路由规则 + * @param string $method 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public function addRules($rules, $method = '*', $option = [], $pattern = []) + { + foreach ($rules as $key => $val) { + if (is_numeric($key)) { + $key = array_shift($val); + } + + if (is_array($val)) { + $route = array_shift($val); + $option = $val ? array_shift($val) : []; + $pattern = $val ? array_shift($val) : []; + } else { + $route = $val; + } + + $this->addRule($key, $route, $method, $option, $pattern); + } + } + + public function addRuleItem($rule, $method = '*') + { + if (strpos($method, '|')) { + $rule->method($method); + $method = '*'; + } + + $this->rules[$method][] = $rule; + + return $this; + } + + /** + * 设置分组的路由前缀 + * @access public + * @param string $prefix + * @return $this + */ + public function prefix($prefix) + { + if ($this->parent && $this->parent->getOption('prefix')) { + $prefix = $this->parent->getOption('prefix') . $prefix; + } + + return $this->option('prefix', $prefix); + } + + /** + * 设置资源允许 + * @access public + * @param array $only + * @return $this + */ + public function only($only) + { + return $this->option('only', $only); + } + + /** + * 设置资源排除 + * @access public + * @param array $except + * @return $this + */ + public function except($except) + { + return $this->option('except', $except); + } + + /** + * 设置资源路由的变量 + * @access public + * @param array $vars + * @return $this + */ + public function vars($vars) + { + return $this->option('var', $vars); + } + + /** + * 合并分组的路由规则正则 + * @access public + * @param bool $merge + * @return $this + */ + public function mergeRuleRegex($merge = true) + { + return $this->option('merge_rule_regex', $merge); + } + + /** + * 获取完整分组Name + * @access public + * @return string + */ + public function getFullName() + { + return $this->fullName; + } + + /** + * 获取分组的路由规则 + * @access public + * @param string $method + * @return array + */ + public function getRules($method = '') + { + if ('' === $method) { + return $this->rules; + } + + return isset($this->rules[strtolower($method)]) ? $this->rules[strtolower($method)] : []; + } + + /** + * 清空分组下的路由规则 + * @access public + * @return void + */ + public function clear() + { + $this->rules = [ + '*' => [], + 'get' => [], + 'post' => [], + 'put' => [], + 'patch' => [], + 'delete' => [], + 'head' => [], + 'options' => [], + ]; + } +} diff --git a/vendor/topthink/framework/library/think/route/RuleItem.php b/vendor/topthink/framework/library/think/route/RuleItem.php new file mode 100644 index 0000000..a05d2de --- /dev/null +++ b/vendor/topthink/framework/library/think/route/RuleItem.php @@ -0,0 +1,292 @@ + +// +---------------------------------------------------------------------- + +namespace think\route; + +use think\Container; +use think\Exception; +use think\Route; + +class RuleItem extends Rule +{ + protected $hasSetRule; + + /** + * 架构函数 + * @access public + * @param Route $router 路由实例 + * @param RuleGroup $parent 上级对象 + * @param string $name 路由标识 + * @param string|array $rule 路由规则 + * @param string|\Closure $route 路由地址 + * @param string $method 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + */ + public function __construct(Route $router, RuleGroup $parent, $name, $rule, $route, $method = '*', $option = [], $pattern = []) + { + $this->router = $router; + $this->parent = $parent; + $this->name = $name; + $this->route = $route; + $this->method = $method; + $this->option = $option; + $this->pattern = $pattern; + + $this->setRule($rule); + + if (!empty($option['cross_domain'])) { + $this->router->setCrossDomainRule($this, $method); + } + } + + /** + * 路由规则预处理 + * @access public + * @param string $rule 路由规则 + * @return void + */ + public function setRule($rule) + { + if ('$' == substr($rule, -1, 1)) { + // 是否完整匹配 + $rule = substr($rule, 0, -1); + + $this->option['complete_match'] = true; + } + + $rule = '/' != $rule ? ltrim($rule, '/') : ''; + + if ($this->parent && $prefix = $this->parent->getFullName()) { + $rule = $prefix . ($rule ? '/' . ltrim($rule, '/') : ''); + } + + if (false !== strpos($rule, ':')) { + $this->rule = preg_replace(['/\[\:(\w+)\]/', '/\:(\w+)/'], ['<\1?>', '<\1>'], $rule); + } else { + $this->rule = $rule; + } + + // 生成路由标识的快捷访问 + $this->setRuleName(); + } + + /** + * 检查后缀 + * @access public + * @param string $ext + * @return $this + */ + public function ext($ext = '') + { + $this->option('ext', $ext); + $this->setRuleName(true); + + return $this; + } + + /** + * 设置别名 + * @access public + * @param string $name + * @return $this + */ + public function name($name) + { + $this->name = $name; + $this->setRuleName(true); + + return $this; + } + + /** + * 设置路由标识 用于URL反解生成 + * @access protected + * @param bool $first 是否插入开头 + * @return void + */ + protected function setRuleName($first = false) + { + if ($this->name) { + $vars = $this->parseVar($this->rule); + $name = strtolower($this->name); + + if (isset($this->option['ext'])) { + $suffix = $this->option['ext']; + } elseif ($this->parent->getOption('ext')) { + $suffix = $this->parent->getOption('ext'); + } else { + $suffix = null; + } + + $value = [$this->rule, $vars, $this->parent->getDomain(), $suffix, $this->method]; + + Container::get('rule_name')->set($name, $value, $first); + } + + if (!$this->hasSetRule) { + Container::get('rule_name')->setRule($this->rule, $this); + $this->hasSetRule = true; + } + } + + /** + * 检测路由 + * @access public + * @param Request $request 请求对象 + * @param string $url 访问地址 + * @param array $match 匹配路由变量 + * @param bool $completeMatch 路由是否完全匹配 + * @return Dispatch|false + */ + public function checkRule($request, $url, $match = null, $completeMatch = false) + { + // 检查参数有效性 + if (!$this->checkOption($this->option, $request)) { + return false; + } + + // 合并分组参数 + $option = $this->mergeGroupOptions(); + + $url = $this->urlSuffixCheck($request, $url, $option); + + if (is_null($match)) { + $match = $this->match($url, $option, $completeMatch); + } + + if (false !== $match) { + if (!empty($option['cross_domain'])) { + if ($dispatch = $this->checkCrossDomain($request)) { + // 允许跨域 + return $dispatch; + } + + $option['header'] = $this->option['header']; + } + + // 检查前置行为 + if (isset($option['before']) && false === $this->checkBefore($option['before'])) { + return false; + } + + return $this->parseRule($request, $this->rule, $this->route, $url, $option, $match); + } + + return false; + } + + /** + * 检测路由(含路由匹配) + * @access public + * @param Request $request 请求对象 + * @param string $url 访问地址 + * @param string $depr 路径分隔符 + * @param bool $completeMatch 路由是否完全匹配 + * @return Dispatch|false + */ + public function check($request, $url, $completeMatch = false) + { + return $this->checkRule($request, $url, null, $completeMatch); + } + + /** + * URL后缀及Slash检查 + * @access protected + * @param Request $request 请求对象 + * @param string $url 访问地址 + * @param array $option 路由参数 + * @return string + */ + protected function urlSuffixCheck($request, $url, $option = []) + { + // 是否区分 / 地址访问 + if (!empty($option['remove_slash']) && '/' != $this->rule) { + $this->rule = rtrim($this->rule, '/'); + $url = rtrim($url, '|'); + } + + if (isset($option['ext'])) { + // 路由ext参数 优先于系统配置的URL伪静态后缀参数 + $url = preg_replace('/\.(' . $request->ext() . ')$/i', '', $url); + } + + return $url; + } + + /** + * 检测URL和规则路由是否匹配 + * @access private + * @param string $url URL地址 + * @param array $option 路由参数 + * @param bool $completeMatch 路由是否完全匹配 + * @return array|false + */ + private function match($url, $option, $completeMatch) + { + if (isset($option['complete_match'])) { + $completeMatch = $option['complete_match']; + } + + $pattern = array_merge($this->parent->getPattern(), $this->pattern); + $depr = $this->router->config('pathinfo_depr'); + + // 检查完整规则定义 + if (isset($pattern['__url__']) && !preg_match(0 === strpos($pattern['__url__'], '/') ? $pattern['__url__'] : '/^' . $pattern['__url__'] . '/', str_replace('|', $depr, $url))) { + return false; + } + + $var = []; + $url = $depr . str_replace('|', $depr, $url); + $rule = $depr . str_replace('/', $depr, $this->rule); + + if ($depr == $rule && $depr != $url) { + return false; + } + + if (false === strpos($rule, '<')) { + if (0 === strcasecmp($rule, $url) || (!$completeMatch && 0 === strncasecmp($rule . $depr, $url . $depr, strlen($rule . $depr)))) { + return $var; + } + return false; + } + + $slash = preg_quote('/-' . $depr, '/'); + + if ($matchRule = preg_split('/[' . $slash . ']?<\w+\??>/', $rule, 2)) { + if ($matchRule[0] && 0 !== strncasecmp($rule, $url, strlen($matchRule[0]))) { + return false; + } + } + + if (preg_match_all('/[' . $slash . ']??/', $rule, $matches)) { + $regex = $this->buildRuleRegex($rule, $matches[0], $pattern, $option, $completeMatch); + + try { + if (!preg_match('/^' . $regex . ($completeMatch ? '$' : '') . '/u', $url, $match)) { + return false; + } + } catch (\Exception $e) { + throw new Exception('route pattern error'); + } + + foreach ($match as $key => $val) { + if (is_string($key)) { + $var[$key] = $val; + } + } + } + + // 成功匹配后返回URL中的动态变量数组 + return $var; + } + +} diff --git a/vendor/topthink/framework/library/think/route/RuleName.php b/vendor/topthink/framework/library/think/route/RuleName.php new file mode 100644 index 0000000..202fb0e --- /dev/null +++ b/vendor/topthink/framework/library/think/route/RuleName.php @@ -0,0 +1,147 @@ + +// +---------------------------------------------------------------------- + +namespace think\route; + +class RuleName +{ + protected $item = []; + protected $rule = []; + + /** + * 注册路由标识 + * @access public + * @param string $name 路由标识 + * @param array $value 路由规则 + * @param bool $first 是否置顶 + * @return void + */ + public function set($name, $value, $first = false) + { + if ($first && isset($this->item[$name])) { + array_unshift($this->item[$name], $value); + } else { + $this->item[$name][] = $value; + } + } + + /** + * 注册路由规则 + * @access public + * @param string $rule 路由规则 + * @param RuleItem $route 路由 + * @return void + */ + public function setRule($rule, $route) + { + $this->rule[$route->getDomain()][$rule][$route->getMethod()] = $route; + } + + /** + * 根据路由规则获取路由对象(列表) + * @access public + * @param string $name 路由标识 + * @param string $domain 域名 + * @return array + */ + public function getRule($rule, $domain = null) + { + return isset($this->rule[$domain][$rule]) ? $this->rule[$domain][$rule] : []; + } + + /** + * 获取全部路由列表 + * @access public + * @param string $domain 域名 + * @return array + */ + public function getRuleList($domain = null) + { + $list = []; + + foreach ($this->rule as $ruleDomain => $rules) { + foreach ($rules as $rule => $items) { + foreach ($items as $item) { + $val['domain'] = $ruleDomain; + + foreach (['method', 'rule', 'name', 'route', 'pattern', 'option'] as $param) { + $call = 'get' . $param; + $val[$param] = $item->$call(); + } + + $list[$ruleDomain][] = $val; + } + } + } + + if ($domain) { + return isset($list[$domain]) ? $list[$domain] : []; + } + + return $list; + } + + /** + * 导入路由标识 + * @access public + * @param array $name 路由标识 + * @return void + */ + public function import($item) + { + $this->item = $item; + } + + /** + * 根据路由标识获取路由信息(用于URL生成) + * @access public + * @param string $name 路由标识 + * @param string $domain 域名 + * @return array|null + */ + public function get($name = null, $domain = null, $method = '*') + { + if (is_null($name)) { + return $this->item; + } + + $name = strtolower($name); + $method = strtolower($method); + + if (isset($this->item[$name])) { + if (is_null($domain)) { + $result = $this->item[$name]; + } else { + $result = []; + foreach ($this->item[$name] as $item) { + if ($item[2] == $domain && ('*' == $item[4] || $method == $item[4])) { + $result[] = $item; + } + } + } + } else { + $result = null; + } + + return $result; + } + + /** + * 清空路由规则 + * @access public + * @return void + */ + public function clear() + { + $this->item = []; + $this->rule = []; + } +} diff --git a/vendor/topthink/framework/library/think/route/dispatch/Callback.php b/vendor/topthink/framework/library/think/route/dispatch/Callback.php new file mode 100644 index 0000000..ca76fc9 --- /dev/null +++ b/vendor/topthink/framework/library/think/route/dispatch/Callback.php @@ -0,0 +1,26 @@ + +// +---------------------------------------------------------------------- + +namespace think\route\dispatch; + +use think\route\Dispatch; + +class Callback extends Dispatch +{ + public function exec() + { + // 执行回调方法 + $vars = array_merge($this->request->param(), $this->param); + + return $this->app->invoke($this->dispatch, $vars); + } + +} diff --git a/vendor/topthink/framework/library/think/route/dispatch/Controller.php b/vendor/topthink/framework/library/think/route/dispatch/Controller.php new file mode 100644 index 0000000..1de8299 --- /dev/null +++ b/vendor/topthink/framework/library/think/route/dispatch/Controller.php @@ -0,0 +1,30 @@ + +// +---------------------------------------------------------------------- + +namespace think\route\dispatch; + +use think\route\Dispatch; + +class Controller extends Dispatch +{ + public function exec() + { + // 执行控制器的操作方法 + $vars = array_merge($this->request->param(), $this->param); + + return $this->app->action( + $this->dispatch, $vars, + $this->rule->getConfig('url_controller_layer'), + $this->rule->getConfig('controller_suffix') + ); + } + +} diff --git a/vendor/topthink/framework/library/think/route/dispatch/Module.php b/vendor/topthink/framework/library/think/route/dispatch/Module.php new file mode 100644 index 0000000..40bd775 --- /dev/null +++ b/vendor/topthink/framework/library/think/route/dispatch/Module.php @@ -0,0 +1,138 @@ + +// +---------------------------------------------------------------------- + +namespace think\route\dispatch; + +use ReflectionMethod; +use think\exception\ClassNotFoundException; +use think\exception\HttpException; +use think\Loader; +use think\Request; +use think\route\Dispatch; + +class Module extends Dispatch +{ + protected $controller; + protected $actionName; + + public function init() + { + parent::init(); + + $result = $this->dispatch; + + if (is_string($result)) { + $result = explode('/', $result); + } + + if ($this->rule->getConfig('app_multi_module')) { + // 多模块部署 + $module = strip_tags(strtolower($result[0] ?: $this->rule->getConfig('default_module'))); + $bind = $this->rule->getRouter()->getBind(); + $available = false; + + if ($bind && preg_match('/^[a-z]/is', $bind)) { + // 绑定模块 + list($bindModule) = explode('/', $bind); + if (empty($result[0])) { + $module = $bindModule; + } + $available = true; + } elseif (!in_array($module, $this->rule->getConfig('deny_module_list')) && is_dir($this->app->getAppPath() . $module)) { + $available = true; + } elseif ($this->rule->getConfig('empty_module')) { + $module = $this->rule->getConfig('empty_module'); + $available = true; + } + + // 模块初始化 + if ($module && $available) { + // 初始化模块 + $this->request->setModule($module); + $this->app->init($module); + } else { + throw new HttpException(404, 'module not exists:' . $module); + } + } + + // 是否自动转换控制器和操作名 + $convert = is_bool($this->convert) ? $this->convert : $this->rule->getConfig('url_convert'); + // 获取控制器名 + $controller = strip_tags($result[1] ?: $this->rule->getConfig('default_controller')); + + $this->controller = $convert ? strtolower($controller) : $controller; + + // 获取操作名 + $this->actionName = strip_tags($result[2] ?: $this->rule->getConfig('default_action')); + + // 设置当前请求的控制器、操作 + $this->request + ->setController(Loader::parseName($this->controller, 1)) + ->setAction($this->actionName); + + return $this; + } + + public function exec() + { + // 监听module_init + $this->app['hook']->listen('module_init'); + + try { + // 实例化控制器 + $instance = $this->app->controller($this->controller, + $this->rule->getConfig('url_controller_layer'), + $this->rule->getConfig('controller_suffix'), + $this->rule->getConfig('empty_controller')); + } catch (ClassNotFoundException $e) { + throw new HttpException(404, 'controller not exists:' . $e->getClass()); + } + + $this->app['middleware']->controller(function (Request $request, $next) use ($instance) { + // 获取当前操作名 + $action = $this->actionName . $this->rule->getConfig('action_suffix'); + + if (is_callable([$instance, $action])) { + // 执行操作方法 + $call = [$instance, $action]; + + // 严格获取当前操作方法名 + $reflect = new ReflectionMethod($instance, $action); + $methodName = $reflect->getName(); + $suffix = $this->rule->getConfig('action_suffix'); + $actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName; + $this->request->setAction($actionName); + + // 自动获取请求变量 + $vars = $this->rule->getConfig('url_param_type') + ? $this->request->route() + : $this->request->param(); + $vars = array_merge($vars, $this->param); + } elseif (is_callable([$instance, '_empty'])) { + // 空操作 + $call = [$instance, '_empty']; + $vars = [$this->actionName]; + $reflect = new ReflectionMethod($instance, '_empty'); + } else { + // 操作不存在 + throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()'); + } + + $this->app['hook']->listen('action_begin', $call); + + $data = $this->app->invokeReflectMethod($instance, $reflect, $vars); + + return $this->autoResponse($data); + }); + + return $this->app['middleware']->dispatch($this->request, 'controller'); + } +} diff --git a/vendor/topthink/framework/library/think/route/dispatch/Redirect.php b/vendor/topthink/framework/library/think/route/dispatch/Redirect.php new file mode 100644 index 0000000..fae2c9a --- /dev/null +++ b/vendor/topthink/framework/library/think/route/dispatch/Redirect.php @@ -0,0 +1,23 @@ + +// +---------------------------------------------------------------------- + +namespace think\route\dispatch; + +use think\Response; +use think\route\Dispatch; + +class Redirect extends Dispatch +{ + public function exec() + { + return Response::create($this->dispatch, 'redirect')->code($this->code); + } +} diff --git a/vendor/topthink/framework/library/think/route/dispatch/Response.php b/vendor/topthink/framework/library/think/route/dispatch/Response.php new file mode 100644 index 0000000..66f4e5a --- /dev/null +++ b/vendor/topthink/framework/library/think/route/dispatch/Response.php @@ -0,0 +1,23 @@ + +// +---------------------------------------------------------------------- + +namespace think\route\dispatch; + +use think\route\Dispatch; + +class Response extends Dispatch +{ + public function exec() + { + return $this->dispatch; + } + +} diff --git a/vendor/topthink/framework/library/think/route/dispatch/Url.php b/vendor/topthink/framework/library/think/route/dispatch/Url.php new file mode 100644 index 0000000..acc524e --- /dev/null +++ b/vendor/topthink/framework/library/think/route/dispatch/Url.php @@ -0,0 +1,169 @@ + +// +---------------------------------------------------------------------- + +namespace think\route\dispatch; + +use think\exception\HttpException; +use think\Loader; +use think\route\Dispatch; + +class Url extends Dispatch +{ + public function init() + { + // 解析默认的URL规则 + $result = $this->parseUrl($this->dispatch); + + return (new Module($this->request, $this->rule, $result))->init(); + } + + public function exec() + {} + + /** + * 解析URL地址 + * @access protected + * @param string $url URL + * @return array + */ + protected function parseUrl($url) + { + $depr = $this->rule->getConfig('pathinfo_depr'); + $bind = $this->rule->getRouter()->getBind(); + + if (!empty($bind) && preg_match('/^[a-z]/is', $bind)) { + $bind = str_replace('/', $depr, $bind); + // 如果有模块/控制器绑定 + $url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr); + } + + list($path, $var) = $this->rule->parseUrlPath($url); + if (empty($path)) { + return [null, null, null]; + } + + // 解析模块 + $module = $this->rule->getConfig('app_multi_module') ? array_shift($path) : null; + + if ($this->param['auto_search']) { + $controller = $this->autoFindController($module, $path); + } else { + // 解析控制器 + $controller = !empty($path) ? array_shift($path) : null; + } + + if ($controller && !preg_match('/^[A-Za-z0-9][\w|\.]*$/', $controller)) { + throw new HttpException(404, 'controller not exists:' . $controller); + } + + // 解析操作 + $action = !empty($path) ? array_shift($path) : null; + + // 解析额外参数 + if ($path) { + if ($this->rule->getConfig('url_param_type')) { + $var += $path; + } else { + preg_replace_callback('/(\w+)\|([^\|]+)/', function ($match) use (&$var) { + $var[$match[1]] = strip_tags($match[2]); + }, implode('|', $path)); + } + } + + $panDomain = $this->request->panDomain(); + + if ($panDomain && $key = array_search('*', $var)) { + // 泛域名赋值 + $var[$key] = $panDomain; + } + + // 设置当前请求的参数 + $this->request->setRouteVars($var); + + // 封装路由 + $route = [$module, $controller, $action]; + + if ($this->hasDefinedRoute($route, $bind)) { + throw new HttpException(404, 'invalid request:' . str_replace('|', $depr, $url)); + } + + return $route; + } + + /** + * 检查URL是否已经定义过路由 + * @access protected + * @param string $route 路由信息 + * @param string $bind 绑定信息 + * @return bool + */ + protected function hasDefinedRoute($route, $bind) + { + list($module, $controller, $action) = $route; + + // 检查地址是否被定义过路由 + $name = strtolower($module . '/' . Loader::parseName($controller, 1) . '/' . $action); + + $name2 = ''; + + if (empty($module) || $module == $bind) { + $name2 = strtolower(Loader::parseName($controller, 1) . '/' . $action); + } + + $host = $this->request->host(true); + + $method = $this->request->method(); + + if ($this->rule->getRouter()->getName($name, $host, $method) || $this->rule->getRouter()->getName($name2, $host, $method)) { + return true; + } + + return false; + } + + /** + * 自动定位控制器类 + * @access protected + * @param string $module 模块名 + * @param array $path URL + * @return string + */ + protected function autoFindController($module, &$path) + { + $dir = $this->app->getAppPath() . ($module ? $module . '/' : '') . $this->rule->getConfig('url_controller_layer'); + $suffix = $this->app->getSuffix() || $this->rule->getConfig('controller_suffix') ? ucfirst($this->rule->getConfig('url_controller_layer')) : ''; + + $item = []; + $find = false; + + foreach ($path as $val) { + $item[] = $val; + $file = $dir . '/' . str_replace('.', '/', $val) . $suffix . '.php'; + $file = pathinfo($file, PATHINFO_DIRNAME) . '/' . Loader::parseName(pathinfo($file, PATHINFO_FILENAME), 1) . '.php'; + if (is_file($file)) { + $find = true; + break; + } else { + $dir .= '/' . Loader::parseName($val); + } + } + + if ($find) { + $controller = implode('.', $item); + $path = array_slice($path, count($item)); + } else { + $controller = array_shift($path); + } + + return $controller; + } + +} diff --git a/vendor/topthink/framework/library/think/route/dispatch/View.php b/vendor/topthink/framework/library/think/route/dispatch/View.php new file mode 100644 index 0000000..ea3ef11 --- /dev/null +++ b/vendor/topthink/framework/library/think/route/dispatch/View.php @@ -0,0 +1,26 @@ + +// +---------------------------------------------------------------------- + +namespace think\route\dispatch; + +use think\Response; +use think\route\Dispatch; + +class View extends Dispatch +{ + public function exec() + { + // 渲染模板输出 + $vars = array_merge($this->request->param(), $this->param); + + return Response::create($this->dispatch, 'view')->assign($vars); + } +} diff --git a/vendor/topthink/framework/library/think/session/driver/Memcache.php b/vendor/topthink/framework/library/think/session/driver/Memcache.php new file mode 100644 index 0000000..40d7bb8 --- /dev/null +++ b/vendor/topthink/framework/library/think/session/driver/Memcache.php @@ -0,0 +1,124 @@ + +// +---------------------------------------------------------------------- + +namespace think\session\driver; + +use SessionHandlerInterface; +use think\Exception; + +class Memcache implements SessionHandlerInterface +{ + protected $handler = null; + protected $config = [ + 'host' => '127.0.0.1', // memcache主机 + 'port' => 11211, // memcache端口 + 'expire' => 3600, // session有效期 + 'timeout' => 0, // 连接超时时间(单位:毫秒) + 'persistent' => true, // 长连接 + 'session_name' => '', // memcache key前缀 + ]; + + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 打开Session + * @access public + * @param string $savePath + * @param mixed $sessName + */ + public function open($savePath, $sessName) + { + // 检测php环境 + if (!extension_loaded('memcache')) { + throw new Exception('not support:memcache'); + } + + $this->handler = new \Memcache; + + // 支持集群 + $hosts = explode(',', $this->config['host']); + $ports = explode(',', $this->config['port']); + + if (empty($ports[0])) { + $ports[0] = 11211; + } + + // 建立连接 + foreach ((array) $hosts as $i => $host) { + $port = isset($ports[$i]) ? $ports[$i] : $ports[0]; + $this->config['timeout'] > 0 ? + $this->handler->addServer($host, $port, $this->config['persistent'], 1, $this->config['timeout']) : + $this->handler->addServer($host, $port, $this->config['persistent'], 1); + } + + return true; + } + + /** + * 关闭Session + * @access public + */ + public function close() + { + $this->gc(ini_get('session.gc_maxlifetime')); + $this->handler->close(); + $this->handler = null; + + return true; + } + + /** + * 读取Session + * @access public + * @param string $sessID + */ + public function read($sessID) + { + return (string) $this->handler->get($this->config['session_name'] . $sessID); + } + + /** + * 写入Session + * @access public + * @param string $sessID + * @param string $sessData + * @return bool + */ + public function write($sessID, $sessData) + { + return $this->handler->set($this->config['session_name'] . $sessID, $sessData, 0, $this->config['expire']); + } + + /** + * 删除Session + * @access public + * @param string $sessID + * @return bool + */ + public function destroy($sessID) + { + return $this->handler->delete($this->config['session_name'] . $sessID); + } + + /** + * Session 垃圾回收 + * @access public + * @param string $sessMaxLifeTime + * @return true + */ + public function gc($sessMaxLifeTime) + { + return true; + } +} diff --git a/vendor/topthink/framework/library/think/session/driver/Memcached.php b/vendor/topthink/framework/library/think/session/driver/Memcached.php new file mode 100644 index 0000000..074b2ff --- /dev/null +++ b/vendor/topthink/framework/library/think/session/driver/Memcached.php @@ -0,0 +1,135 @@ + +// +---------------------------------------------------------------------- + +namespace think\session\driver; + +use SessionHandlerInterface; +use think\Exception; + +class Memcached implements SessionHandlerInterface +{ + protected $handler = null; + protected $config = [ + 'host' => '127.0.0.1', // memcache主机 + 'port' => 11211, // memcache端口 + 'expire' => 3600, // session有效期 + 'timeout' => 0, // 连接超时时间(单位:毫秒) + 'session_name' => '', // memcache key前缀 + 'username' => '', //账号 + 'password' => '', //密码 + ]; + + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 打开Session + * @access public + * @param string $savePath + * @param mixed $sessName + */ + public function open($savePath, $sessName) + { + // 检测php环境 + if (!extension_loaded('memcached')) { + throw new Exception('not support:memcached'); + } + + $this->handler = new \Memcached; + + // 设置连接超时时间(单位:毫秒) + if ($this->config['timeout'] > 0) { + $this->handler->setOption(\Memcached::OPT_CONNECT_TIMEOUT, $this->config['timeout']); + } + + // 支持集群 + $hosts = explode(',', $this->config['host']); + $ports = explode(',', $this->config['port']); + + if (empty($ports[0])) { + $ports[0] = 11211; + } + + // 建立连接 + $servers = []; + foreach ((array) $hosts as $i => $host) { + $servers[] = [$host, (isset($ports[$i]) ? $ports[$i] : $ports[0]), 1]; + } + + $this->handler->addServers($servers); + + if ('' != $this->config['username']) { + $this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); + $this->handler->setSaslAuthData($this->config['username'], $this->config['password']); + } + + return true; + } + + /** + * 关闭Session + * @access public + */ + public function close() + { + $this->gc(ini_get('session.gc_maxlifetime')); + $this->handler->quit(); + $this->handler = null; + + return true; + } + + /** + * 读取Session + * @access public + * @param string $sessID + */ + public function read($sessID) + { + return (string) $this->handler->get($this->config['session_name'] . $sessID); + } + + /** + * 写入Session + * @access public + * @param string $sessID + * @param string $sessData + * @return bool + */ + public function write($sessID, $sessData) + { + return $this->handler->set($this->config['session_name'] . $sessID, $sessData, $this->config['expire']); + } + + /** + * 删除Session + * @access public + * @param string $sessID + * @return bool + */ + public function destroy($sessID) + { + return $this->handler->delete($this->config['session_name'] . $sessID); + } + + /** + * Session 垃圾回收 + * @access public + * @param string $sessMaxLifeTime + * @return true + */ + public function gc($sessMaxLifeTime) + { + return true; + } +} diff --git a/vendor/topthink/framework/library/think/session/driver/Redis.php b/vendor/topthink/framework/library/think/session/driver/Redis.php new file mode 100644 index 0000000..5a0e7bc --- /dev/null +++ b/vendor/topthink/framework/library/think/session/driver/Redis.php @@ -0,0 +1,179 @@ + +// +---------------------------------------------------------------------- + +namespace think\session\driver; + +use SessionHandlerInterface; +use think\Exception; + +class Redis implements SessionHandlerInterface +{ + /** @var \Redis */ + protected $handler = null; + protected $config = [ + 'host' => '127.0.0.1', // redis主机 + 'port' => 6379, // redis端口 + 'password' => '', // 密码 + 'select' => 0, // 操作库 + 'expire' => 3600, // 有效期(秒) + 'timeout' => 0, // 超时时间(秒) + 'persistent' => true, // 是否长连接 + 'session_name' => '', // sessionkey前缀 + ]; + + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 打开Session + * @access public + * @param string $savePath + * @param mixed $sessName + * @return bool + * @throws Exception + */ + public function open($savePath, $sessName) + { + if (extension_loaded('redis')) { + $this->handler = new \Redis; + + // 建立连接 + $func = $this->config['persistent'] ? 'pconnect' : 'connect'; + $this->handler->$func($this->config['host'], $this->config['port'], $this->config['timeout']); + + if ('' != $this->config['password']) { + $this->handler->auth($this->config['password']); + } + + if (0 != $this->config['select']) { + $this->handler->select($this->config['select']); + } + } elseif (class_exists('\Predis\Client')) { + $params = []; + foreach ($this->config as $key => $val) { + if (in_array($key, ['aggregate', 'cluster', 'connections', 'exceptions', 'prefix', 'profile', 'replication'])) { + $params[$key] = $val; + unset($this->config[$key]); + } + } + $this->handler = new \Predis\Client($this->config, $params); + } else { + throw new \BadFunctionCallException('not support: redis'); + } + + return true; + } + + /** + * 关闭Session + * @access public + */ + public function close() + { + $this->gc(ini_get('session.gc_maxlifetime')); + $this->handler->close(); + $this->handler = null; + + return true; + } + + /** + * 读取Session + * @access public + * @param string $sessID + * @return string + */ + public function read($sessID) + { + return (string) $this->handler->get($this->config['session_name'] . $sessID); + } + + /** + * 写入Session + * @access public + * @param string $sessID + * @param string $sessData + * @return bool + */ + public function write($sessID, $sessData) + { + if ($this->config['expire'] > 0) { + $result = $this->handler->setex($this->config['session_name'] . $sessID, $this->config['expire'], $sessData); + } else { + $result = $this->handler->set($this->config['session_name'] . $sessID, $sessData); + } + + return $result ? true : false; + } + + /** + * 删除Session + * @access public + * @param string $sessID + * @return bool + */ + public function destroy($sessID) + { + return $this->handler->del($this->config['session_name'] . $sessID) > 0; + } + + /** + * Session 垃圾回收 + * @access public + * @param string $sessMaxLifeTime + * @return bool + */ + public function gc($sessMaxLifeTime) + { + return true; + } + + /** + * Redis Session 驱动的加锁机制 + * @access public + * @param string $sessID 用于加锁的sessID + * @param integer $timeout 默认过期时间 + * @return bool + */ + public function lock($sessID, $timeout = 10) + { + if (null == $this->handler) { + $this->open('', ''); + } + + $lockKey = 'LOCK_PREFIX_' . $sessID; + // 使用setnx操作加锁 + $isLock = $this->handler->setnx($lockKey, 1); + if ($isLock) { + // 设置过期时间,防止死任务的出现 + $this->handler->expire($lockKey, $timeout); + return true; + } + + return false; + } + + /** + * Redis Session 驱动的解锁机制 + * @access public + * @param string $sessID 用于解锁的sessID + */ + public function unlock($sessID) + { + if (null == $this->handler) { + $this->open('', ''); + } + + $this->handler->del('LOCK_PREFIX_' . $sessID); + } +} diff --git a/vendor/topthink/framework/library/think/template/TagLib.php b/vendor/topthink/framework/library/think/template/TagLib.php new file mode 100644 index 0000000..bbbb2c0 --- /dev/null +++ b/vendor/topthink/framework/library/think/template/TagLib.php @@ -0,0 +1,351 @@ + +// +---------------------------------------------------------------------- + +namespace think\template; + +use think\Exception; + +/** + * ThinkPHP标签库TagLib解析基类 + * @category Think + * @package Think + * @subpackage Template + * @author liu21st + */ +class TagLib +{ + + /** + * 标签库定义XML文件 + * @var string + * @access protected + */ + protected $xml = ''; + protected $tags = []; // 标签定义 + /** + * 标签库名称 + * @var string + * @access protected + */ + protected $tagLib = ''; + + /** + * 标签库标签列表 + * @var array + * @access protected + */ + protected $tagList = []; + + /** + * 标签库分析数组 + * @var array + * @access protected + */ + protected $parse = []; + + /** + * 标签库是否有效 + * @var bool + * @access protected + */ + protected $valid = false; + + /** + * 当前模板对象 + * @var object + * @access protected + */ + protected $tpl; + + protected $comparison = [' nheq ' => ' !== ', ' heq ' => ' === ', ' neq ' => ' != ', ' eq ' => ' == ', ' egt ' => ' >= ', ' gt ' => ' > ', ' elt ' => ' <= ', ' lt ' => ' < ']; + + /** + * 架构函数 + * @access public + * @param \stdClass $template 模板引擎对象 + */ + public function __construct($template) + { + $this->tpl = $template; + } + + /** + * 按签标库替换页面中的标签 + * @access public + * @param string $content 模板内容 + * @param string $lib 标签库名 + * @return void + */ + public function parseTag(&$content, $lib = '') + { + $tags = []; + $lib = $lib ? strtolower($lib) . ':' : ''; + + foreach ($this->tags as $name => $val) { + $close = !isset($val['close']) || $val['close'] ? 1 : 0; + $tags[$close][$lib . $name] = $name; + if (isset($val['alias'])) { + // 别名设置 + $array = (array) $val['alias']; + foreach (explode(',', $array[0]) as $v) { + $tags[$close][$lib . $v] = $name; + } + } + } + + // 闭合标签 + if (!empty($tags[1])) { + $nodes = []; + $regex = $this->getRegex(array_keys($tags[1]), 1); + if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { + $right = []; + foreach ($matches as $match) { + if ('' == $match[1][0]) { + $name = strtolower($match[2][0]); + // 如果有没闭合的标签头则取出最后一个 + if (!empty($right[$name])) { + // $match[0][1]为标签结束符在模板中的位置 + $nodes[$match[0][1]] = [ + 'name' => $name, + 'begin' => array_pop($right[$name]), // 标签开始符 + 'end' => $match[0], // 标签结束符 + ]; + } + } else { + // 标签头压入栈 + $right[strtolower($match[1][0])][] = $match[0]; + } + } + unset($right, $matches); + // 按标签在模板中的位置从后向前排序 + krsort($nodes); + } + + $break = ''; + if ($nodes) { + $beginArray = []; + // 标签替换 从后向前 + foreach ($nodes as $pos => $node) { + // 对应的标签名 + $name = $tags[1][$node['name']]; + $alias = $lib . $name != $node['name'] ? ($lib ? strstr($node['name'], $lib) : $node['name']) : ''; + + // 解析标签属性 + $attrs = $this->parseAttr($node['begin'][0], $name, $alias); + $method = 'tag' . $name; + + // 读取标签库中对应的标签内容 replace[0]用来替换标签头,replace[1]用来替换标签尾 + $replace = explode($break, $this->$method($attrs, $break)); + + if (count($replace) > 1) { + while ($beginArray) { + $begin = end($beginArray); + // 判断当前标签尾的位置是否在栈中最后一个标签头的后面,是则为子标签 + if ($node['end'][1] > $begin['pos']) { + break; + } else { + // 不为子标签时,取出栈中最后一个标签头 + $begin = array_pop($beginArray); + // 替换标签头部 + $content = substr_replace($content, $begin['str'], $begin['pos'], $begin['len']); + } + } + // 替换标签尾部 + $content = substr_replace($content, $replace[1], $node['end'][1], strlen($node['end'][0])); + // 把标签头压入栈 + $beginArray[] = ['pos' => $node['begin'][1], 'len' => strlen($node['begin'][0]), 'str' => $replace[0]]; + } + } + + while ($beginArray) { + $begin = array_pop($beginArray); + // 替换标签头部 + $content = substr_replace($content, $begin['str'], $begin['pos'], $begin['len']); + } + } + } + // 自闭合标签 + if (!empty($tags[0])) { + $regex = $this->getRegex(array_keys($tags[0]), 0); + $content = preg_replace_callback($regex, function ($matches) use (&$tags, &$lib) { + // 对应的标签名 + $name = $tags[0][strtolower($matches[1])]; + $alias = $lib . $name != $matches[1] ? ($lib ? strstr($matches[1], $lib) : $matches[1]) : ''; + // 解析标签属性 + $attrs = $this->parseAttr($matches[0], $name, $alias); + $method = 'tag' . $name; + return $this->$method($attrs, ''); + }, $content); + } + + return; + } + + /** + * 按标签生成正则 + * @access public + * @param array|string $tags 标签名 + * @param boolean $close 是否为闭合标签 + * @return string + */ + public function getRegex($tags, $close) + { + $begin = $this->tpl->config('taglib_begin'); + $end = $this->tpl->config('taglib_end'); + $single = strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1 ? true : false; + $tagName = is_array($tags) ? implode('|', $tags) : $tags; + + if ($single) { + if ($close) { + // 如果是闭合标签 + $regex = $begin . '(?:(' . $tagName . ')\b(?>[^' . $end . ']*)|\/(' . $tagName . '))' . $end; + } else { + $regex = $begin . '(' . $tagName . ')\b(?>[^' . $end . ']*)' . $end; + } + } else { + if ($close) { + // 如果是闭合标签 + $regex = $begin . '(?:(' . $tagName . ')\b(?>(?:(?!' . $end . ').)*)|\/(' . $tagName . '))' . $end; + } else { + $regex = $begin . '(' . $tagName . ')\b(?>(?:(?!' . $end . ').)*)' . $end; + } + } + + return '/' . $regex . '/is'; + } + + /** + * 分析标签属性 正则方式 + * @access public + * @param string $str 标签属性字符串 + * @param string $name 标签名 + * @param string $alias 别名 + * @return array + */ + public function parseAttr($str, $name, $alias = '') + { + $regex = '/\s+(?>(?P[\w-]+)\s*)=(?>\s*)([\"\'])(?P(?:(?!\\2).)*)\\2/is'; + $result = []; + + if (preg_match_all($regex, $str, $matches)) { + foreach ($matches['name'] as $key => $val) { + $result[$val] = $matches['value'][$key]; + } + + if (!isset($this->tags[$name])) { + // 检测是否存在别名定义 + foreach ($this->tags as $key => $val) { + if (isset($val['alias'])) { + $array = (array) $val['alias']; + if (in_array($name, explode(',', $array[0]))) { + $tag = $val; + $type = !empty($array[1]) ? $array[1] : 'type'; + $result[$type] = $name; + break; + } + } + } + } else { + $tag = $this->tags[$name]; + // 设置了标签别名 + if (!empty($alias) && isset($tag['alias'])) { + $type = !empty($tag['alias'][1]) ? $tag['alias'][1] : 'type'; + $result[$type] = $alias; + } + } + + if (!empty($tag['must'])) { + $must = explode(',', $tag['must']); + foreach ($must as $name) { + if (!isset($result[$name])) { + throw new Exception('tag attr must:' . $name); + } + } + } + } else { + // 允许直接使用表达式的标签 + if (!empty($this->tags[$name]['expression'])) { + static $_taglibs; + if (!isset($_taglibs[$name])) { + $_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]); + // 清除自闭合标签尾部/ + $result['expression'] = rtrim($result['expression'], '/'); + $result['expression'] = trim($result['expression']); + } elseif (empty($this->tags[$name]) || !empty($this->tags[$name]['attr'])) { + throw new Exception('tag error:' . $name); + } + } + + return $result; + } + + /** + * 解析条件表达式 + * @access public + * @param string $condition 表达式标签内容 + * @return string + */ + public function parseCondition($condition) + { + if (!strpos($condition, '::') && strpos($condition, ':')) { + $condition = ' ' . substr(strstr($condition, ':'), 1); + } + + $condition = str_ireplace(array_keys($this->comparison), array_values($this->comparison), $condition); + $this->tpl->parseVar($condition); + + // $this->tpl->parseVarFunction($condition); // XXX: 此句能解析表达式中用|分隔的函数,但表达式中如果有|、||这样的逻辑运算就产生了歧异 + return $condition; + } + + /** + * 自动识别构建变量 + * @access public + * @param string $name 变量描述 + * @return string + */ + public function autoBuildVar(&$name) + { + $flag = substr($name, 0, 1); + + if (':' == $flag) { + // 以:开头为函数调用,解析前去掉: + $name = substr($name, 1); + } elseif ('$' != $flag && preg_match('/[a-zA-Z_]/', $flag)) { + // XXX: 这句的写法可能还需要改进 + // 常量不需要解析 + if (defined($name)) { + return $name; + } + + // 不以$开头并且也不是常量,自动补上$前缀 + $name = '$' . $name; + } + + $this->tpl->parseVar($name); + $this->tpl->parseVarFunction($name, false); + + return $name; + } + + /** + * 获取标签列表 + * @access public + * @return array + */ + public function getTags() + { + return $this->tags; + } +} diff --git a/vendor/topthink/framework/library/think/template/driver/File.php b/vendor/topthink/framework/library/think/template/driver/File.php new file mode 100644 index 0000000..3b96a0f --- /dev/null +++ b/vendor/topthink/framework/library/think/template/driver/File.php @@ -0,0 +1,83 @@ + +// +---------------------------------------------------------------------- + +namespace think\template\driver; + +use think\Exception; + +class File +{ + protected $cacheFile; + + /** + * 写入编译缓存 + * @access public + * @param string $cacheFile 缓存的文件名 + * @param string $content 缓存的内容 + * @return void|array + */ + public function write($cacheFile, $content) + { + // 检测模板目录 + $dir = dirname($cacheFile); + + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + + // 生成模板缓存文件 + if (false === file_put_contents($cacheFile, $content)) { + throw new Exception('cache write error:' . $cacheFile, 11602); + } + } + + /** + * 读取编译编译 + * @access public + * @param string $cacheFile 缓存的文件名 + * @param array $vars 变量数组 + * @return void + */ + public function read($cacheFile, $vars = []) + { + $this->cacheFile = $cacheFile; + + if (!empty($vars) && is_array($vars)) { + // 模板阵列变量分解成为独立变量 + extract($vars, EXTR_OVERWRITE); + } + + //载入模版缓存文件 + include $this->cacheFile; + } + + /** + * 检查编译缓存是否有效 + * @access public + * @param string $cacheFile 缓存的文件名 + * @param int $cacheTime 缓存时间 + * @return boolean + */ + public function check($cacheFile, $cacheTime) + { + // 缓存文件不存在, 直接返回false + if (!file_exists($cacheFile)) { + return false; + } + + if (0 != $cacheTime && time() > filemtime($cacheFile) + $cacheTime) { + // 缓存是否在有效期 + return false; + } + + return true; + } +} diff --git a/vendor/topthink/framework/library/think/template/taglib/Cx.php b/vendor/topthink/framework/library/think/template/taglib/Cx.php new file mode 100644 index 0000000..ad741f2 --- /dev/null +++ b/vendor/topthink/framework/library/think/template/taglib/Cx.php @@ -0,0 +1,724 @@ + +// +---------------------------------------------------------------------- + +namespace think\template\taglib; + +use think\template\TagLib; + +/** + * CX标签库解析类 + * @category Think + * @package Think + * @subpackage Driver.Taglib + * @author liu21st + */ +class Cx extends Taglib +{ + + // 标签定义 + protected $tags = [ + // 标签定义: attr 属性列表 close 是否闭合(0 或者1 默认1) alias 标签别名 level 嵌套层次 + 'php' => ['attr' => ''], + 'volist' => ['attr' => 'name,id,offset,length,key,mod', 'alias' => 'iterate'], + 'foreach' => ['attr' => 'name,id,item,key,offset,length,mod', 'expression' => true], + 'if' => ['attr' => 'condition', 'expression' => true], + 'elseif' => ['attr' => 'condition', 'close' => 0, 'expression' => true], + 'else' => ['attr' => '', 'close' => 0], + 'switch' => ['attr' => 'name', 'expression' => true], + 'case' => ['attr' => 'value,break', 'expression' => true], + 'default' => ['attr' => '', 'close' => 0], + 'compare' => ['attr' => 'name,value,type', 'alias' => ['eq,equal,notequal,neq,gt,lt,egt,elt,heq,nheq', 'type']], + 'range' => ['attr' => 'name,value,type', 'alias' => ['in,notin,between,notbetween', 'type']], + 'empty' => ['attr' => 'name'], + 'notempty' => ['attr' => 'name'], + 'present' => ['attr' => 'name'], + 'notpresent' => ['attr' => 'name'], + 'defined' => ['attr' => 'name'], + 'notdefined' => ['attr' => 'name'], + 'load' => ['attr' => 'file,href,type,value,basepath', 'close' => 0, 'alias' => ['import,css,js', 'type']], + 'assign' => ['attr' => 'name,value', 'close' => 0], + 'define' => ['attr' => 'name,value', 'close' => 0], + 'for' => ['attr' => 'start,end,name,comparison,step'], + 'url' => ['attr' => 'link,vars,suffix,domain', 'close' => 0, 'expression' => true], + 'function' => ['attr' => 'name,vars,use,call'], + ]; + + /** + * php标签解析 + * 格式: + * {php}echo $name{/php} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagPhp($tag, $content) + { + $parseStr = ''; + return $parseStr; + } + + /** + * volist标签解析 循环输出数据集 + * 格式: + * {volist name="userList" id="user" empty=""} + * {user.username} + * {user.email} + * {/volist} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string|void + */ + public function tagVolist($tag, $content) + { + $name = $tag['name']; + $id = $tag['id']; + $empty = isset($tag['empty']) ? $tag['empty'] : ''; + $key = !empty($tag['key']) ? $tag['key'] : 'i'; + $mod = isset($tag['mod']) ? $tag['mod'] : '2'; + $offset = !empty($tag['offset']) && is_numeric($tag['offset']) ? intval($tag['offset']) : 0; + $length = !empty($tag['length']) && is_numeric($tag['length']) ? intval($tag['length']) : 'null'; + // 允许使用函数设定数据集 {$vo.name} + $parseStr = 'autoBuildVar($name); + $parseStr .= '$_result=' . $name . ';'; + $name = '$_result'; + } else { + $name = $this->autoBuildVar($name); + } + + $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection || ' . $name . ' instanceof \think\Paginator): $' . $key . ' = 0;'; + + // 设置了输出数组长度 + if (0 != $offset || 'null' != $length) { + $parseStr .= '$__LIST__ = is_array(' . $name . ') ? array_slice(' . $name . ',' . $offset . ',' . $length . ', true) : ' . $name . '->slice(' . $offset . ',' . $length . ', true); '; + } else { + $parseStr .= ' $__LIST__ = ' . $name . ';'; + } + + $parseStr .= 'if( count($__LIST__)==0 ) : echo "' . $empty . '" ;'; + $parseStr .= 'else: '; + $parseStr .= 'foreach($__LIST__ as $key=>$' . $id . '): '; + $parseStr .= '$mod = ($' . $key . ' % ' . $mod . ' );'; + $parseStr .= '++$' . $key . ';?>'; + $parseStr .= $content; + $parseStr .= ''; + + if (!empty($parseStr)) { + return $parseStr; + } + + return; + } + + /** + * foreach标签解析 循环输出数据集 + * 格式: + * {foreach name="userList" id="user" key="key" index="i" mod="2" offset="3" length="5" empty=""} + * {user.username} + * {/foreach} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string|void + */ + public function tagForeach($tag, $content) + { + // 直接使用表达式 + if (!empty($tag['expression'])) { + $expression = ltrim(rtrim($tag['expression'], ')'), '('); + $expression = $this->autoBuildVar($expression); + $parseStr = ''; + $parseStr .= $content; + $parseStr .= ''; + return $parseStr; + } + + $name = $tag['name']; + $key = !empty($tag['key']) ? $tag['key'] : 'key'; + $item = !empty($tag['id']) ? $tag['id'] : $tag['item']; + $empty = isset($tag['empty']) ? $tag['empty'] : ''; + $offset = !empty($tag['offset']) && is_numeric($tag['offset']) ? intval($tag['offset']) : 0; + $length = !empty($tag['length']) && is_numeric($tag['length']) ? intval($tag['length']) : 'null'; + + $parseStr = 'autoBuildVar($name); + $parseStr .= $var . '=' . $name . '; '; + $name = $var; + } else { + $name = $this->autoBuildVar($name); + } + + $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection || ' . $name . ' instanceof \think\Paginator): '; + + // 设置了输出数组长度 + if (0 != $offset || 'null' != $length) { + if (!isset($var)) { + $var = '$_' . uniqid(); + } + $parseStr .= $var . ' = is_array(' . $name . ') ? array_slice(' . $name . ',' . $offset . ',' . $length . ', true) : ' . $name . '->slice(' . $offset . ',' . $length . ', true); '; + } else { + $var = &$name; + } + + $parseStr .= 'if( count(' . $var . ')==0 ) : echo "' . $empty . '" ;'; + $parseStr .= 'else: '; + + // 设置了索引项 + if (isset($tag['index'])) { + $index = $tag['index']; + $parseStr .= '$' . $index . '=0; '; + } + + $parseStr .= 'foreach(' . $var . ' as $' . $key . '=>$' . $item . '): '; + + // 设置了索引项 + if (isset($tag['index'])) { + $index = $tag['index']; + if (isset($tag['mod'])) { + $mod = (int) $tag['mod']; + $parseStr .= '$mod = ($' . $index . ' % ' . $mod . '); '; + } + $parseStr .= '++$' . $index . '; '; + } + + $parseStr .= '?>'; + // 循环体中的内容 + $parseStr .= $content; + $parseStr .= ''; + + if (!empty($parseStr)) { + return $parseStr; + } + + return; + } + + /** + * if标签解析 + * 格式: + * {if condition=" $a eq 1"} + * {elseif condition="$a eq 2" /} + * {else /} + * {/if} + * 表达式支持 eq neq gt egt lt elt == > >= < <= or and || && + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagIf($tag, $content) + { + $condition = !empty($tag['expression']) ? $tag['expression'] : $tag['condition']; + $condition = $this->parseCondition($condition); + $parseStr = '' . $content . ''; + + return $parseStr; + } + + /** + * elseif标签解析 + * 格式:见if标签 + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagElseif($tag, $content) + { + $condition = !empty($tag['expression']) ? $tag['expression'] : $tag['condition']; + $condition = $this->parseCondition($condition); + $parseStr = ''; + + return $parseStr; + } + + /** + * else标签解析 + * 格式:见if标签 + * @access public + * @param array $tag 标签属性 + * @return string + */ + public function tagElse($tag) + { + $parseStr = ''; + + return $parseStr; + } + + /** + * switch标签解析 + * 格式: + * {switch name="a.name"} + * {case value="1" break="false"}1{/case} + * {case value="2" }2{/case} + * {default /}other + * {/switch} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagSwitch($tag, $content) + { + $name = !empty($tag['expression']) ? $tag['expression'] : $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = '' . $content . ''; + + return $parseStr; + } + + /** + * case标签解析 需要配合switch才有效 + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagCase($tag, $content) + { + $value = isset($tag['expression']) ? $tag['expression'] : $tag['value']; + $flag = substr($value, 0, 1); + + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($value); + $value = 'case ' . $value . ':'; + } elseif (strpos($value, '|')) { + $values = explode('|', $value); + $value = ''; + foreach ($values as $val) { + $value .= 'case "' . addslashes($val) . '":'; + } + } else { + $value = 'case "' . $value . '":'; + } + + $parseStr = '' . $content; + $isBreak = isset($tag['break']) ? $tag['break'] : ''; + + if ('' == $isBreak || $isBreak) { + $parseStr .= ''; + } + + return $parseStr; + } + + /** + * default标签解析 需要配合switch才有效 + * 使用: {default /}ddfdf + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagDefault($tag) + { + $parseStr = ''; + + return $parseStr; + } + + /** + * compare标签解析 + * 用于值的比较 支持 eq neq gt lt egt elt heq nheq 默认是eq + * 格式: {compare name="" type="eq" value="" }content{/compare} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagCompare($tag, $content) + { + $name = $tag['name']; + $value = $tag['value']; + $type = isset($tag['type']) ? $tag['type'] : 'eq'; // 比较类型 + $name = $this->autoBuildVar($name); + $flag = substr($value, 0, 1); + + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($value); + } else { + $value = '\'' . $value . '\''; + } + + switch ($type) { + case 'equal': + $type = 'eq'; + break; + case 'notequal': + $type = 'neq'; + break; + } + $type = $this->parseCondition(' ' . $type . ' '); + $parseStr = '' . $content . ''; + + return $parseStr; + } + + /** + * range标签解析 + * 如果某个变量存在于某个范围 则输出内容 type= in 表示在范围内 否则表示在范围外 + * 格式: {range name="var|function" value="val" type='in|notin' }content{/range} + * example: {range name="a" value="1,2,3" type='in' }content{/range} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagRange($tag, $content) + { + $name = $tag['name']; + $value = $tag['value']; + $type = isset($tag['type']) ? $tag['type'] : 'in'; // 比较类型 + + $name = $this->autoBuildVar($name); + $flag = substr($value, 0, 1); + + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($value); + $str = 'is_array(' . $value . ')?' . $value . ':explode(\',\',' . $value . ')'; + } else { + $value = '"' . $value . '"'; + $str = 'explode(\',\',' . $value . ')'; + } + + if ('between' == $type) { + $parseStr = '= $_RANGE_VAR_[0] && ' . $name . '<= $_RANGE_VAR_[1]):?>' . $content . ''; + } elseif ('notbetween' == $type) { + $parseStr = '$_RANGE_VAR_[1]):?>' . $content . ''; + } else { + $fun = ('in' == $type) ? 'in_array' : '!in_array'; + $parseStr = '' . $content . ''; + } + + return $parseStr; + } + + /** + * present标签解析 + * 如果某个变量已经设置 则输出内容 + * 格式: {present name="" }content{/present} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagPresent($tag, $content) + { + $name = $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = '' . $content . ''; + + return $parseStr; + } + + /** + * notpresent标签解析 + * 如果某个变量没有设置,则输出内容 + * 格式: {notpresent name="" }content{/notpresent} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagNotpresent($tag, $content) + { + $name = $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = '' . $content . ''; + + return $parseStr; + } + + /** + * empty标签解析 + * 如果某个变量为empty 则输出内容 + * 格式: {empty name="" }content{/empty} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagEmpty($tag, $content) + { + $name = $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = 'isEmpty())): ?>' . $content . ''; + + return $parseStr; + } + + /** + * notempty标签解析 + * 如果某个变量不为empty 则输出内容 + * 格式: {notempty name="" }content{/notempty} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagNotempty($tag, $content) + { + $name = $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = 'isEmpty()))): ?>' . $content . ''; + + return $parseStr; + } + + /** + * 判断是否已经定义了该常量 + * {defined name='TXT'}已定义{/defined} + * @access public + * @param array $tag + * @param string $content + * @return string + */ + public function tagDefined($tag, $content) + { + $name = $tag['name']; + $parseStr = '' . $content . ''; + + return $parseStr; + } + + /** + * 判断是否没有定义了该常量 + * {notdefined name='TXT'}已定义{/notdefined} + * @access public + * @param array $tag + * @param string $content + * @return string + */ + public function tagNotdefined($tag, $content) + { + $name = $tag['name']; + $parseStr = '' . $content . ''; + + return $parseStr; + } + + /** + * load 标签解析 {load file="/static/js/base.js" /} + * 格式:{load file="/static/css/base.css" /} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagLoad($tag, $content) + { + $file = isset($tag['file']) ? $tag['file'] : $tag['href']; + $type = isset($tag['type']) ? strtolower($tag['type']) : ''; + + $parseStr = ''; + $endStr = ''; + + // 判断是否存在加载条件 允许使用函数判断(默认为isset) + if (isset($tag['value'])) { + $name = $tag['value']; + $name = $this->autoBuildVar($name); + $name = 'isset(' . $name . ')'; + $parseStr .= ''; + $endStr = ''; + } + + // 文件方式导入 + $array = explode(',', $file); + + foreach ($array as $val) { + $type = strtolower(substr(strrchr($val, '.'), 1)); + switch ($type) { + case 'js': + $parseStr .= ''; + break; + case 'css': + $parseStr .= ''; + break; + case 'php': + $parseStr .= ''; + break; + } + } + + return $parseStr . $endStr; + } + + /** + * assign标签解析 + * 在模板中给某个变量赋值 支持变量赋值 + * 格式: {assign name="" value="" /} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagAssign($tag, $content) + { + $name = $this->autoBuildVar($tag['name']); + $flag = substr($tag['value'], 0, 1); + + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($tag['value']); + } else { + $value = '\'' . $tag['value'] . '\''; + } + + $parseStr = ''; + + return $parseStr; + } + + /** + * define标签解析 + * 在模板中定义常量 支持变量赋值 + * 格式: {define name="" value="" /} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagDefine($tag, $content) + { + $name = '\'' . $tag['name'] . '\''; + $flag = substr($tag['value'], 0, 1); + + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($tag['value']); + } else { + $value = '\'' . $tag['value'] . '\''; + } + + $parseStr = ''; + + return $parseStr; + } + + /** + * for标签解析 + * 格式: + * {for start="" end="" comparison="" step="" name=""} + * content + * {/for} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagFor($tag, $content) + { + //设置默认值 + $start = 0; + $end = 0; + $step = 1; + $comparison = 'lt'; + $name = 'i'; + $rand = rand(); //添加随机数,防止嵌套变量冲突 + + //获取属性 + foreach ($tag as $key => $value) { + $value = trim($value); + $flag = substr($value, 0, 1); + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($value); + } + + switch ($key) { + case 'start': + $start = $value; + break; + case 'end': + $end = $value; + break; + case 'step': + $step = $value; + break; + case 'comparison': + $comparison = $value; + break; + case 'name': + $name = $value; + break; + } + } + + $parseStr = 'parseCondition('$' . $name . ' ' . $comparison . ' $__FOR_END_' . $rand . '__') . ';$' . $name . '+=' . $step . '){ ?>'; + $parseStr .= $content; + $parseStr .= ''; + + return $parseStr; + } + + /** + * url函数的tag标签 + * 格式:{url link="模块/控制器/方法" vars="参数" suffix="true或者false 是否带有后缀" domain="true或者false 是否携带域名" /} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagUrl($tag, $content) + { + $url = isset($tag['link']) ? $tag['link'] : ''; + $vars = isset($tag['vars']) ? $tag['vars'] : ''; + $suffix = isset($tag['suffix']) ? $tag['suffix'] : 'true'; + $domain = isset($tag['domain']) ? $tag['domain'] : 'false'; + + return ''; + } + + /** + * function标签解析 匿名函数,可实现递归 + * 使用: + * {function name="func" vars="$data" call="$list" use="&$a,&$b"} + * {if is_array($data)} + * {foreach $data as $val} + * {~func($val) /} + * {/foreach} + * {else /} + * {$data} + * {/if} + * {/function} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagFunction($tag, $content) + { + $name = !empty($tag['name']) ? $tag['name'] : 'func'; + $vars = !empty($tag['vars']) ? $tag['vars'] : ''; + $call = !empty($tag['call']) ? $tag['call'] : ''; + $use = ['&$' . $name]; + + if (!empty($tag['use'])) { + foreach (explode(',', $tag['use']) as $val) { + $use[] = '&' . ltrim(trim($val), '&'); + } + } + + $parseStr = '' . $content . '' : '?>'; + + return $parseStr; + } +} diff --git a/vendor/topthink/framework/library/think/validate/ValidateRule.php b/vendor/topthink/framework/library/think/validate/ValidateRule.php new file mode 100644 index 0000000..7cd7017 --- /dev/null +++ b/vendor/topthink/framework/library/think/validate/ValidateRule.php @@ -0,0 +1,171 @@ + +// +---------------------------------------------------------------------- + +namespace think\validate; + +/** + * Class ValidateRule + * @package think\validate + * @method ValidateRule confirm(mixed $rule, string $msg = '') static 验证是否和某个字段的值一致 + * @method ValidateRule different(mixed $rule, string $msg = '') static 验证是否和某个字段的值是否不同 + * @method ValidateRule egt(mixed $rule, string $msg = '') static 验证是否大于等于某个值 + * @method ValidateRule gt(mixed $rule, string $msg = '') static 验证是否大于某个值 + * @method ValidateRule elt(mixed $rule, string $msg = '') static 验证是否小于等于某个值 + * @method ValidateRule lt(mixed $rule, string $msg = '') static 验证是否小于某个值 + * @method ValidateRule eg(mixed $rule, string $msg = '') static 验证是否等于某个值 + * @method ValidateRule in(mixed $rule, string $msg = '') static 验证是否在范围内 + * @method ValidateRule notIn(mixed $rule, string $msg = '') static 验证是否不在某个范围 + * @method ValidateRule between(mixed $rule, string $msg = '') static 验证是否在某个区间 + * @method ValidateRule notBetween(mixed $rule, string $msg = '') static 验证是否不在某个区间 + * @method ValidateRule length(mixed $rule, string $msg = '') static 验证数据长度 + * @method ValidateRule max(mixed $rule, string $msg = '') static 验证数据最大长度 + * @method ValidateRule min(mixed $rule, string $msg = '') static 验证数据最小长度 + * @method ValidateRule after(mixed $rule, string $msg = '') static 验证日期 + * @method ValidateRule before(mixed $rule, string $msg = '') static 验证日期 + * @method ValidateRule expire(mixed $rule, string $msg = '') static 验证有效期 + * @method ValidateRule allowIp(mixed $rule, string $msg = '') static 验证IP许可 + * @method ValidateRule denyIp(mixed $rule, string $msg = '') static 验证IP禁用 + * @method ValidateRule regex(mixed $rule, string $msg = '') static 使用正则验证数据 + * @method ValidateRule token(mixed $rule='__token__', string $msg = '') static 验证表单令牌 + * @method ValidateRule is(mixed $rule, string $msg = '') static 验证字段值是否为有效格式 + * @method ValidateRule isRequire(mixed $rule = null, string $msg = '') static 验证字段必须 + * @method ValidateRule isNumber(mixed $rule = null, string $msg = '') static 验证字段值是否为数字 + * @method ValidateRule isArray(mixed $rule = null, string $msg = '') static 验证字段值是否为数组 + * @method ValidateRule isInteger(mixed $rule = null, string $msg = '') static 验证字段值是否为整形 + * @method ValidateRule isFloat(mixed $rule = null, string $msg = '') static 验证字段值是否为浮点数 + * @method ValidateRule isMobile(mixed $rule = null, string $msg = '') static 验证字段值是否为手机 + * @method ValidateRule isIdCard(mixed $rule = null, string $msg = '') static 验证字段值是否为身份证号码 + * @method ValidateRule isChs(mixed $rule = null, string $msg = '') static 验证字段值是否为中文 + * @method ValidateRule isChsDash(mixed $rule = null, string $msg = '') static 验证字段值是否为中文字母及下划线 + * @method ValidateRule isChsAlpha(mixed $rule = null, string $msg = '') static 验证字段值是否为中文和字母 + * @method ValidateRule isChsAlphaNum(mixed $rule = null, string $msg = '') static 验证字段值是否为中文字母和数字 + * @method ValidateRule isDate(mixed $rule = null, string $msg = '') static 验证字段值是否为有效格式 + * @method ValidateRule isBool(mixed $rule = null, string $msg = '') static 验证字段值是否为布尔值 + * @method ValidateRule isAlpha(mixed $rule = null, string $msg = '') static 验证字段值是否为字母 + * @method ValidateRule isAlphaDash(mixed $rule = null, string $msg = '') static 验证字段值是否为字母和下划线 + * @method ValidateRule isAlphaNum(mixed $rule = null, string $msg = '') static 验证字段值是否为字母和数字 + * @method ValidateRule isAccepted(mixed $rule = null, string $msg = '') static 验证字段值是否为yes, on, 或是 1 + * @method ValidateRule isEmail(mixed $rule = null, string $msg = '') static 验证字段值是否为有效邮箱格式 + * @method ValidateRule isUrl(mixed $rule = null, string $msg = '') static 验证字段值是否为有效URL地址 + * @method ValidateRule activeUrl(mixed $rule, string $msg = '') static 验证是否为合格的域名或者IP + * @method ValidateRule ip(mixed $rule, string $msg = '') static 验证是否有效IP + * @method ValidateRule fileExt(mixed $rule, string $msg = '') static 验证文件后缀 + * @method ValidateRule fileMime(mixed $rule, string $msg = '') static 验证文件类型 + * @method ValidateRule fileSize(mixed $rule, string $msg = '') static 验证文件大小 + * @method ValidateRule image(mixed $rule, string $msg = '') static 验证图像文件 + * @method ValidateRule method(mixed $rule, string $msg = '') static 验证请求类型 + * @method ValidateRule dateFormat(mixed $rule, string $msg = '') static 验证时间和日期是否符合指定格式 + * @method ValidateRule unique(mixed $rule, string $msg = '') static 验证是否唯一 + * @method ValidateRule behavior(mixed $rule, string $msg = '') static 使用行为类验证 + * @method ValidateRule filter(mixed $rule, string $msg = '') static 使用filter_var方式验证 + * @method ValidateRule requireIf(mixed $rule, string $msg = '') static 验证某个字段等于某个值的时候必须 + * @method ValidateRule requireCallback(mixed $rule, string $msg = '') static 通过回调方法验证某个字段是否必须 + * @method ValidateRule requireWith(mixed $rule, string $msg = '') static 验证某个字段有值的情况下必须 + * @method ValidateRule must(mixed $rule = null, string $msg = '') static 必须验证 + */ +class ValidateRule +{ + // 验证字段的名称 + protected $title; + + // 当前验证规则 + protected $rule = []; + + // 验证提示信息 + protected $message = []; + + /** + * 添加验证因子 + * @access protected + * @param string $name 验证名称 + * @param mixed $rule 验证规则 + * @param string $msg 提示信息 + * @return $this + */ + protected function addItem($name, $rule = null, $msg = '') + { + if ($rule || 0 === $rule) { + $this->rule[$name] = $rule; + } else { + $this->rule[] = $name; + } + + $this->message[] = $msg; + + return $this; + } + + /** + * 获取验证规则 + * @access public + * @return array + */ + public function getRule() + { + return $this->rule; + } + + /** + * 获取验证字段名称 + * @access public + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * 获取验证提示 + * @access public + * @return array + */ + public function getMsg() + { + return $this->message; + } + + /** + * 设置验证字段名称 + * @access public + * @return $this + */ + public function title($title) + { + $this->title = $title; + + return $this; + } + + public function __call($method, $args) + { + if ('is' == strtolower(substr($method, 0, 2))) { + $method = substr($method, 2); + } + + array_unshift($args, lcfirst($method)); + + return call_user_func_array([$this, 'addItem'], $args); + } + + public static function __callStatic($method, $args) + { + $rule = new static(); + + if ('is' == strtolower(substr($method, 0, 2))) { + $method = substr($method, 2); + } + + array_unshift($args, lcfirst($method)); + + return call_user_func_array([$rule, 'addItem'], $args); + } +} diff --git a/vendor/topthink/framework/library/think/view/driver/Php.php b/vendor/topthink/framework/library/think/view/driver/Php.php new file mode 100644 index 0000000..7948dc0 --- /dev/null +++ b/vendor/topthink/framework/library/think/view/driver/Php.php @@ -0,0 +1,183 @@ + +// +---------------------------------------------------------------------- + +namespace think\view\driver; + +use think\App; +use think\exception\TemplateNotFoundException; +use think\Loader; + +class Php +{ + // 模板引擎参数 + protected $config = [ + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 + 'auto_rule' => 1, + // 视图基础目录(集中式) + 'view_base' => '', + // 模板起始路径 + 'view_path' => '', + // 模板文件后缀 + 'view_suffix' => 'php', + // 模板文件名分隔符 + 'view_depr' => DIRECTORY_SEPARATOR, + ]; + + protected $template; + protected $app; + protected $content; + + public function __construct(App $app, $config = []) + { + $this->app = $app; + $this->config = array_merge($this->config, (array) $config); + } + + /** + * 检测是否存在模板文件 + * @access public + * @param string $template 模板文件或者模板规则 + * @return bool + */ + public function exists($template) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + + return is_file($template); + } + + /** + * 渲染模板文件 + * @access public + * @param string $template 模板文件 + * @param array $data 模板变量 + * @return void + */ + public function fetch($template, $data = []) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + + // 模板不存在 抛出异常 + if (!is_file($template)) { + throw new TemplateNotFoundException('template not exists:' . $template, $template); + } + + $this->template = $template; + + // 记录视图信息 + $this->app + ->log('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]'); + + extract($data, EXTR_OVERWRITE); + include $this->template; + } + + /** + * 渲染模板内容 + * @access public + * @param string $content 模板内容 + * @param array $data 模板变量 + * @return void + */ + public function display($content, $data = []) + { + $this->content = $content; + + extract($data, EXTR_OVERWRITE); + eval('?>' . $this->content); + } + + /** + * 自动定位模板文件 + * @access private + * @param string $template 模板文件规则 + * @return string + */ + private function parseTemplate($template) + { + if (empty($this->config['view_path'])) { + $this->config['view_path'] = $this->app->getModulePath() . 'view' . DIRECTORY_SEPARATOR; + } + + $request = $this->app['request']; + + // 获取视图根目录 + if (strpos($template, '@')) { + // 跨模块调用 + list($module, $template) = explode('@', $template); + } + + if ($this->config['view_base']) { + // 基础视图目录 + $module = isset($module) ? $module : $request->module(); + $path = $this->config['view_base'] . ($module ? $module . DIRECTORY_SEPARATOR : ''); + } else { + $path = isset($module) ? $this->app->getAppPath() . $module . DIRECTORY_SEPARATOR . 'view' . DIRECTORY_SEPARATOR : $this->config['view_path']; + } + + $depr = $this->config['view_depr']; + + if (0 !== strpos($template, '/')) { + $template = str_replace(['/', ':'], $depr, $template); + $controller = Loader::parseName($request->controller()); + + if ($controller) { + if ('' == $template) { + // 如果模板文件名为空 按照默认规则定位 + $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $this->getActionTemplate($request); + } elseif (false === strpos($template, $depr)) { + $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $template; + } + } + } else { + $template = str_replace(['/', ':'], $depr, substr($template, 1)); + } + + return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.'); + } + + protected function getActionTemplate($request) + { + $rule = [$request->action(true), Loader::parseName($request->action(true)), $request->action()]; + $type = $this->config['auto_rule']; + + return isset($rule[$type]) ? $rule[$type] : $rule[0]; + } + + /** + * 配置模板引擎 + * @access private + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @return void + */ + public function config($name, $value = null) + { + if (is_array($name)) { + $this->config = array_merge($this->config, $name); + } elseif (is_null($value)) { + return isset($this->config[$name]) ? $this->config[$name] : null; + } else { + $this->config[$name] = $value; + } + } + + public function __debugInfo() + { + return ['config' => $this->config]; + } +} diff --git a/vendor/topthink/framework/library/think/view/driver/Think.php b/vendor/topthink/framework/library/think/view/driver/Think.php new file mode 100644 index 0000000..877aee8 --- /dev/null +++ b/vendor/topthink/framework/library/think/view/driver/Think.php @@ -0,0 +1,192 @@ + +// +---------------------------------------------------------------------- + +namespace think\view\driver; + +use think\App; +use think\exception\TemplateNotFoundException; +use think\Loader; +use think\Template; + +class Think +{ + // 模板引擎实例 + private $template; + private $app; + + // 模板引擎参数 + protected $config = [ + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 + 'auto_rule' => 1, + // 视图基础目录(集中式) + 'view_base' => '', + // 模板起始路径 + 'view_path' => '', + // 模板文件后缀 + 'view_suffix' => 'html', + // 模板文件名分隔符 + 'view_depr' => DIRECTORY_SEPARATOR, + // 是否开启模板编译缓存,设为false则每次都会重新编译 + 'tpl_cache' => true, + ]; + + public function __construct(App $app, $config = []) + { + $this->app = $app; + $this->config = array_merge($this->config, (array) $config); + + if (empty($this->config['view_path'])) { + $this->config['view_path'] = $app->getModulePath() . 'view' . DIRECTORY_SEPARATOR; + } + + $this->template = new Template($app, $this->config); + } + + /** + * 检测是否存在模板文件 + * @access public + * @param string $template 模板文件或者模板规则 + * @return bool + */ + public function exists($template) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + + return is_file($template); + } + + /** + * 渲染模板文件 + * @access public + * @param string $template 模板文件 + * @param array $data 模板变量 + * @param array $config 模板参数 + * @return void + */ + public function fetch($template, $data = [], $config = []) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + + // 模板不存在 抛出异常 + if (!is_file($template)) { + throw new TemplateNotFoundException('template not exists:' . $template, $template); + } + + // 记录视图信息 + $this->app + ->log('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]'); + + $this->template->fetch($template, $data, $config); + } + + /** + * 渲染模板内容 + * @access public + * @param string $template 模板内容 + * @param array $data 模板变量 + * @param array $config 模板参数 + * @return void + */ + public function display($template, $data = [], $config = []) + { + $this->template->display($template, $data, $config); + } + + /** + * 自动定位模板文件 + * @access private + * @param string $template 模板文件规则 + * @return string + */ + private function parseTemplate($template) + { + // 分析模板文件规则 + $request = $this->app['request']; + + // 获取视图根目录 + if (strpos($template, '@')) { + // 跨模块调用 + list($module, $template) = explode('@', $template); + } + + if ($this->config['view_base']) { + // 基础视图目录 + $module = isset($module) ? $module : $request->module(); + $path = $this->config['view_base'] . ($module ? $module . DIRECTORY_SEPARATOR : ''); + } else { + $path = isset($module) ? $this->app->getAppPath() . $module . DIRECTORY_SEPARATOR . 'view' . DIRECTORY_SEPARATOR : $this->config['view_path']; + } + + $depr = $this->config['view_depr']; + + if (0 !== strpos($template, '/')) { + $template = str_replace(['/', ':'], $depr, $template); + $controller = Loader::parseName($request->controller()); + + if ($controller) { + if ('' == $template) { + // 如果模板文件名为空 按照默认规则定位 + $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $this->getActionTemplate($request); + } elseif (false === strpos($template, $depr)) { + $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $template; + } + } + } else { + $template = str_replace(['/', ':'], $depr, substr($template, 1)); + } + + return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.'); + } + + protected function getActionTemplate($request) + { + $rule = [$request->action(true), Loader::parseName($request->action(true)), $request->action()]; + $type = $this->config['auto_rule']; + + return isset($rule[$type]) ? $rule[$type] : $rule[0]; + } + + /** + * 配置或者获取模板引擎参数 + * @access private + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @return mixed + */ + public function config($name, $value = null) + { + if (is_array($name)) { + $this->template->config($name); + $this->config = array_merge($this->config, $name); + } elseif (is_null($value)) { + return $this->template->config($name); + } else { + $this->template->$name = $value; + $this->config[$name] = $value; + } + } + + public function __call($method, $params) + { + return call_user_func_array([$this->template, $method], $params); + } + + public function __debugInfo() + { + return ['config' => $this->config]; + } +} diff --git a/vendor/topthink/framework/library/traits/controller/Jump.php b/vendor/topthink/framework/library/traits/controller/Jump.php new file mode 100644 index 0000000..41f7e93 --- /dev/null +++ b/vendor/topthink/framework/library/traits/controller/Jump.php @@ -0,0 +1,168 @@ +error(); + * $this->redirect(); + * } + * } + */ +namespace traits\controller; + +use think\Container; +use think\exception\HttpResponseException; +use think\Response; +use think\response\Redirect; + +trait Jump +{ + /** + * 应用实例 + * @var \think\App + */ + protected $app; + + /** + * 操作成功跳转的快捷方法 + * @access protected + * @param mixed $msg 提示信息 + * @param string $url 跳转的URL地址 + * @param mixed $data 返回的数据 + * @param integer $wait 跳转等待时间 + * @param array $header 发送的Header信息 + * @return void + */ + protected function success($msg = '', $url = null, $data = '', $wait = 3, array $header = []) + { + if (is_null($url) && isset($_SERVER["HTTP_REFERER"])) { + $url = $_SERVER["HTTP_REFERER"]; + } elseif ('' !== $url) { + $url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : Container::get('url')->build($url); + } + + $result = [ + 'code' => 1, + 'msg' => $msg, + 'data' => $data, + 'url' => $url, + 'wait' => $wait, + ]; + + $type = $this->getResponseType(); + // 把跳转模板的渲染下沉,这样在 response_send 行为里通过getData()获得的数据是一致性的格式 + if ('html' == strtolower($type)) { + $type = 'jump'; + } + + $response = Response::create($result, $type)->header($header)->options(['jump_template' => $this->app['config']->get('dispatch_success_tmpl')]); + + throw new HttpResponseException($response); + } + + /** + * 操作错误跳转的快捷方法 + * @access protected + * @param mixed $msg 提示信息 + * @param string $url 跳转的URL地址 + * @param mixed $data 返回的数据 + * @param integer $wait 跳转等待时间 + * @param array $header 发送的Header信息 + * @return void + */ + protected function error($msg = '', $url = null, $data = '', $wait = 3, array $header = []) + { + $type = $this->getResponseType(); + if (is_null($url)) { + $url = $this->app['request']->isAjax() ? '' : 'javascript:history.back(-1);'; + } elseif ('' !== $url) { + $url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : $this->app['url']->build($url); + } + + $result = [ + 'code' => 0, + 'msg' => $msg, + 'data' => $data, + 'url' => $url, + 'wait' => $wait, + ]; + + if ('html' == strtolower($type)) { + $type = 'jump'; + } + + $response = Response::create($result, $type)->header($header)->options(['jump_template' => $this->app['config']->get('dispatch_error_tmpl')]); + + throw new HttpResponseException($response); + } + + /** + * 返回封装后的API数据到客户端 + * @access protected + * @param mixed $data 要返回的数据 + * @param integer $code 返回的code + * @param mixed $msg 提示信息 + * @param string $type 返回数据格式 + * @param array $header 发送的Header信息 + * @return void + */ + protected function result($data, $code = 0, $msg = '', $type = '', array $header = []) + { + $result = [ + 'code' => $code, + 'msg' => $msg, + 'time' => time(), + 'data' => $data, + ]; + + $type = $type ?: $this->getResponseType(); + $response = Response::create($result, $type)->header($header); + + throw new HttpResponseException($response); + } + + /** + * URL重定向 + * @access protected + * @param string $url 跳转的URL表达式 + * @param array|integer $params 其它URL参数 + * @param integer $code http code + * @param array $with 隐式传参 + * @return void + */ + protected function redirect($url, $params = [], $code = 302, $with = []) + { + $response = new Redirect($url); + + if (is_integer($params)) { + $code = $params; + $params = []; + } + + $response->code($code)->params($params)->with($with); + + throw new HttpResponseException($response); + } + + /** + * 获取当前的response 输出类型 + * @access protected + * @return string + */ + protected function getResponseType() + { + if (!$this->app) { + $this->app = Container::get('app'); + } + + $isAjax = $this->app['request']->isAjax(); + $config = $this->app['config']; + + return $isAjax + ? $config->get('default_ajax_return') + : $config->get('default_return_type'); + } +} diff --git a/vendor/topthink/framework/logo.png b/vendor/topthink/framework/logo.png new file mode 100644 index 0000000..25fd059 Binary files /dev/null and b/vendor/topthink/framework/logo.png differ diff --git a/vendor/topthink/framework/phpunit.xml.dist b/vendor/topthink/framework/phpunit.xml.dist new file mode 100644 index 0000000..37c3d2b --- /dev/null +++ b/vendor/topthink/framework/phpunit.xml.dist @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + ./library/think/*/tests/ + + + + + + ./library/ + + ./library/think/*/tests + ./library/think/*/assets + ./library/think/*/resources + ./library/think/*/vendor + + + + \ No newline at end of file diff --git a/vendor/topthink/framework/tpl/default_index.tpl b/vendor/topthink/framework/tpl/default_index.tpl new file mode 100644 index 0000000..e5c1363 --- /dev/null +++ b/vendor/topthink/framework/tpl/default_index.tpl @@ -0,0 +1,10 @@ +*{ 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 }

              :)

              ThinkPHP V5.1
              12载初心不改(2006-2018) - 你值得信赖的PHP框架

              '; + } +} diff --git a/vendor/topthink/framework/tpl/dispatch_jump.tpl b/vendor/topthink/framework/tpl/dispatch_jump.tpl new file mode 100644 index 0000000..583376b --- /dev/null +++ b/vendor/topthink/framework/tpl/dispatch_jump.tpl @@ -0,0 +1,49 @@ +{__NOLAYOUT__} + + + + + 跳转提示 + + + +
              + + +

              :)

              +

              + + +

              :(

              +

              + + +

              +

              + 页面自动 跳转 等待时间: +

              +
              + + + diff --git a/vendor/topthink/framework/tpl/page_trace.tpl b/vendor/topthink/framework/tpl/page_trace.tpl new file mode 100644 index 0000000..2e5afba --- /dev/null +++ b/vendor/topthink/framework/tpl/page_trace.tpl @@ -0,0 +1,71 @@ +
              + + +
              +
              +
              getUseTime().'s ';?>
              + +
              + + diff --git a/vendor/topthink/framework/tpl/think_exception.tpl b/vendor/topthink/framework/tpl/think_exception.tpl new file mode 100644 index 0000000..bd2e2cc --- /dev/null +++ b/vendor/topthink/framework/tpl/think_exception.tpl @@ -0,0 +1,507 @@ +'.end($names).''; + } + } + + if(!function_exists('parse_file')){ + function parse_file($file, $line) + { + return ''.basename($file)." line {$line}".''; + } + } + + if(!function_exists('parse_args')){ + function parse_args($args) + { + $result = []; + + foreach ($args as $key => $item) { + switch (true) { + case is_object($item): + $value = sprintf('object(%s)', parse_class(get_class($item))); + break; + case is_array($item): + if(count($item) > 3){ + $value = sprintf('[%s, ...]', parse_args(array_slice($item, 0, 3))); + } else { + $value = sprintf('[%s]', parse_args($item)); + } + break; + case is_string($item): + if(strlen($item) > 20){ + $value = sprintf( + '\'%s...\'', + htmlentities($item), + htmlentities(substr($item, 0, 20)) + ); + } else { + $value = sprintf("'%s'", htmlentities($item)); + } + break; + case is_int($item): + case is_float($item): + $value = $item; + break; + case is_null($item): + $value = 'null'; + break; + case is_bool($item): + $value = '' . ($item ? 'true' : 'false') . ''; + break; + case is_resource($item): + $value = 'resource'; + break; + default: + $value = htmlentities(str_replace("\n", '', var_export(strval($item), true))); + break; + } + + $result[] = is_int($key) ? $value : "'{$key}' => {$value}"; + } + + return implode(', ', $result); + } + } +?> + + + + + 系统发生错误 + + + + +
              + +
              + +
              +
              + +
              +
              +

              [

              +
              +

              +
              + +
              + +
              +
                $value) { ?>
              +
              + +
              +

              Call Stack

              +
                +
              1. + +
              2. + +
              3. + +
              +
              +
              + +
              + +

              + +
              + + + +
              +

              Exception Datas

              + $value) { ?> + + + + + + + $val) { ?> + + + + + + + +
              empty
              + +
              + +
              + + + +
              +

              Environment Variables

              + $value) { ?> + + + + + + + $val) { ?> + + + + + + + +
              empty
              + +
              + +
              + + + + + + + + diff --git a/vendor/topthink/think-captcha/.gitignore b/vendor/topthink/think-captcha/.gitignore new file mode 100644 index 0000000..85d49cb --- /dev/null +++ b/vendor/topthink/think-captcha/.gitignore @@ -0,0 +1,3 @@ +/vendor/ +/composer.lock +.idea \ No newline at end of file diff --git a/vendor/topthink/think-captcha/LICENSE b/vendor/topthink/think-captcha/LICENSE new file mode 100644 index 0000000..835ce60 --- /dev/null +++ b/vendor/topthink/think-captcha/LICENSE @@ -0,0 +1,32 @@ + +ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 +版权所有Copyright © 2006-2016 by ThinkPHP (http://thinkphp.cn) +All rights reserved。 +ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 + +Apache Licence是著名的非盈利开源组织Apache采用的协议。 +该协议和BSD类似,鼓励代码共享和尊重原作者的著作权, +允许代码修改,再作为开源或商业软件发布。需要满足 +的条件: +1. 需要给代码的用户一份Apache Licence ; +2. 如果你修改了代码,需要在被修改的文件中说明; +3. 在延伸的代码中(修改和有源代码衍生的代码中)需要 +带有原来代码中的协议,商标,专利声明和其他原来作者规 +定需要包含的说明; +4. 如果再发布的产品中包含一个Notice文件,则在Notice文 +件中需要带有本协议内容。你可以在Notice中增加自己的 +许可,但不可以表现为对Apache Licence构成更改。 +具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0 + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/topthink/think-captcha/README.md b/vendor/topthink/think-captcha/README.md new file mode 100644 index 0000000..9ad2cd7 --- /dev/null +++ b/vendor/topthink/think-captcha/README.md @@ -0,0 +1,33 @@ +# think-captcha +thinkphp5.1 验证码类库 + +## 安装 +> composer require topthink/think-captcha + + +##使用 + +###模板里输出验证码 + +~~~ +
              {:captcha_img()}
              +~~~ +或者 +~~~ +
              captcha
              +~~~ +> 上面两种的最终效果是一样的 + +### 控制器里验证 +使用TP5的内置验证功能即可 +~~~ +$this->validate($data,[ + 'captcha|验证码'=>'require|captcha' +]); +~~~ +或者手动验证 +~~~ +if(!captcha_check($captcha)){ + //验证失败 +}; +~~~ \ No newline at end of file diff --git a/vendor/topthink/think-captcha/assets/bgs/1.jpg b/vendor/topthink/think-captcha/assets/bgs/1.jpg new file mode 100644 index 0000000..d417136 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/1.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/2.jpg b/vendor/topthink/think-captcha/assets/bgs/2.jpg new file mode 100644 index 0000000..56640bd Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/2.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/3.jpg b/vendor/topthink/think-captcha/assets/bgs/3.jpg new file mode 100644 index 0000000..83e5bd9 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/3.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/4.jpg b/vendor/topthink/think-captcha/assets/bgs/4.jpg new file mode 100644 index 0000000..97a3721 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/4.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/5.jpg b/vendor/topthink/think-captcha/assets/bgs/5.jpg new file mode 100644 index 0000000..220a17a Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/5.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/6.jpg b/vendor/topthink/think-captcha/assets/bgs/6.jpg new file mode 100644 index 0000000..be53ea0 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/6.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/7.jpg b/vendor/topthink/think-captcha/assets/bgs/7.jpg new file mode 100644 index 0000000..fbf537f Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/7.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/8.jpg b/vendor/topthink/think-captcha/assets/bgs/8.jpg new file mode 100644 index 0000000..e10cf28 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/8.jpg differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/1.ttf b/vendor/topthink/think-captcha/assets/ttfs/1.ttf new file mode 100644 index 0000000..d4ee155 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/1.ttf differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/2.ttf b/vendor/topthink/think-captcha/assets/ttfs/2.ttf new file mode 100644 index 0000000..3a452b6 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/2.ttf differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/3.ttf b/vendor/topthink/think-captcha/assets/ttfs/3.ttf new file mode 100644 index 0000000..d07a4d9 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/3.ttf differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/4.ttf b/vendor/topthink/think-captcha/assets/ttfs/4.ttf new file mode 100644 index 0000000..54a14ed Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/4.ttf differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/5.ttf b/vendor/topthink/think-captcha/assets/ttfs/5.ttf new file mode 100644 index 0000000..d672876 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/5.ttf differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/6.ttf b/vendor/topthink/think-captcha/assets/ttfs/6.ttf new file mode 100644 index 0000000..7f183e2 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/6.ttf differ diff --git a/vendor/topthink/think-captcha/assets/zhttfs/1.ttf b/vendor/topthink/think-captcha/assets/zhttfs/1.ttf new file mode 100644 index 0000000..1c14f7f Binary files /dev/null and b/vendor/topthink/think-captcha/assets/zhttfs/1.ttf differ diff --git a/vendor/topthink/think-captcha/composer.json b/vendor/topthink/think-captcha/composer.json new file mode 100644 index 0000000..172fe09 --- /dev/null +++ b/vendor/topthink/think-captcha/composer.json @@ -0,0 +1,22 @@ +{ + "name": "topthink/think-captcha", + "description": "captcha package for thinkphp5", + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "license": "Apache-2.0", + "require": { + "topthink/framework": "5.1.*" + }, + "autoload": { + "psr-4": { + "think\\captcha\\": "src/" + }, + "files": [ + "src/helper.php" + ] + } +} diff --git a/vendor/topthink/think-captcha/src/Captcha.php b/vendor/topthink/think-captcha/src/Captcha.php new file mode 100644 index 0000000..d57653f --- /dev/null +++ b/vendor/topthink/think-captcha/src/Captcha.php @@ -0,0 +1,320 @@ + +// +---------------------------------------------------------------------- + +namespace think\captcha; + +use think\facade\Session; + +class Captcha +{ + protected $config = [ + 'seKey' => 'ThinkPHP.CN', + // 验证码加密密钥 + 'codeSet' => '2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY', + // 验证码字符集合 + 'expire' => 1800, + // 验证码过期时间(s) + 'useZh' => false, + // 使用中文验证码 + 'zhSet' => '们以我到他会作时要动国产的一是工就年阶义发成部民可出能方进在了不和有大这主中人上为来分生对于学下级地个用同行面说种过命度革而多子后自社加小机也经力线本电高量长党得实家定深法表着水理化争现所二起政三好十战无农使性前等反体合斗路图把结第里正新开论之物从当两些还天资事队批点育重其思与间内去因件日利相由压员气业代全组数果期导平各基或月毛然如应形想制心样干都向变关问比展那它最及外没看治提五解系林者米群头意只明四道马认次文通但条较克又公孔领军流入接席位情运器并飞原油放立题质指建区验活众很教决特此常石强极土少已根共直团统式转别造切九你取西持总料连任志观调七么山程百报更见必真保热委手改管处己将修支识病象几先老光专什六型具示复安带每东增则完风回南广劳轮科北打积车计给节做务被整联步类集号列温装即毫知轴研单色坚据速防史拉世设达尔场织历花受求传口断况采精金界品判参层止边清至万确究书术状厂须离再目海交权且儿青才证低越际八试规斯近注办布门铁需走议县兵固除般引齿千胜细影济白格效置推空配刀叶率述今选养德话查差半敌始片施响收华觉备名红续均药标记难存测士身紧液派准斤角降维板许破述技消底床田势端感往神便贺村构照容非搞亚磨族火段算适讲按值美态黄易彪服早班麦削信排台声该击素张密害侯草何树肥继右属市严径螺检左页抗苏显苦英快称坏移约巴材省黑武培著河帝仅针怎植京助升王眼她抓含苗副杂普谈围食射源例致酸旧却充足短划剂宣环落首尺波承粉践府鱼随考刻靠够满夫失包住促枝局菌杆周护岩师举曲春元超负砂封换太模贫减阳扬江析亩木言球朝医校古呢稻宋听唯输滑站另卫字鼓刚写刘微略范供阿块某功套友限项余倒卷创律雨让骨远帮初皮播优占死毒圈伟季训控激找叫云互跟裂粮粒母练塞钢顶策双留误础吸阻故寸盾晚丝女散焊功株亲院冷彻弹错散商视艺灭版烈零室轻血倍缺厘泵察绝富城冲喷壤简否柱李望盘磁雄似困巩益洲脱投送奴侧润盖挥距触星松送获兴独官混纪依未突架宽冬章湿偏纹吃执阀矿寨责熟稳夺硬价努翻奇甲预职评读背协损棉侵灰虽矛厚罗泥辟告卵箱掌氧恩爱停曾溶营终纲孟钱待尽俄缩沙退陈讨奋械载胞幼哪剥迫旋征槽倒握担仍呀鲜吧卡粗介钻逐弱脚怕盐末阴丰雾冠丙街莱贝辐肠付吉渗瑞惊顿挤秒悬姆烂森糖圣凹陶词迟蚕亿矩康遵牧遭幅园腔订香肉弟屋敏恢忘编印蜂急拿扩伤飞露核缘游振操央伍域甚迅辉异序免纸夜乡久隶缸夹念兰映沟乙吗儒杀汽磷艰晶插埃燃欢铁补咱芽永瓦倾阵碳演威附牙芽永瓦斜灌欧献顺猪洋腐请透司危括脉宜笑若尾束壮暴企菜穗楚汉愈绿拖牛份染既秋遍锻玉夏疗尖殖井费州访吹荣铜沿替滚客召旱悟刺脑措贯藏敢令隙炉壳硫煤迎铸粘探临薄旬善福纵择礼愿伏残雷延烟句纯渐耕跑泽慢栽鲁赤繁境潮横掉锥希池败船假亮谓托伙哲怀割摆贡呈劲财仪沉炼麻罪祖息车穿货销齐鼠抽画饲龙库守筑房歌寒喜哥洗蚀废纳腹乎录镜妇恶脂庄擦险赞钟摇典柄辩竹谷卖乱虚桥奥伯赶垂途额壁网截野遗静谋弄挂课镇妄盛耐援扎虑键归符庆聚绕摩忙舞遇索顾胶羊湖钉仁音迹碎伸灯避泛亡答勇频皇柳哈揭甘诺概宪浓岛袭谁洪谢炮浇斑讯懂灵蛋闭孩释乳巨徒私银伊景坦累匀霉杜乐勒隔弯绩招绍胡呼痛峰零柴簧午跳居尚丁秦稍追梁折耗碱殊岗挖氏刃剧堆赫荷胸衡勤膜篇登驻案刊秧缓凸役剪川雪链渔啦脸户洛孢勃盟买杨宗焦赛旗滤硅炭股坐蒸凝竟陷枪黎救冒暗洞犯筒您宋弧爆谬涂味津臂障褐陆啊健尊豆拔莫抵桑坡缝警挑污冰柬嘴啥饭塑寄赵喊垫丹渡耳刨虎笔稀昆浪萨茶滴浅拥穴覆伦娘吨浸袖珠雌妈紫戏塔锤震岁貌洁剖牢锋疑霸闪埔猛诉刷狠忽灾闹乔唐漏闻沈熔氯荒茎男凡抢像浆旁玻亦忠唱蒙予纷捕锁尤乘乌智淡允叛畜俘摸锈扫毕璃宝芯爷鉴秘净蒋钙肩腾枯抛轨堂拌爸循诱祝励肯酒绳穷塘燥泡袋朗喂铝软渠颗惯贸粪综墙趋彼届墨碍启逆卸航衣孙龄岭骗休借', + // 中文验证码字符串 + 'useImgBg' => false, + // 使用背景图片 + 'fontSize' => 25, + // 验证码字体大小(px) + 'useCurve' => true, + // 是否画混淆曲线 + 'useNoise' => true, + // 是否添加杂点 + 'imageH' => 0, + // 验证码图片高度 + 'imageW' => 0, + // 验证码图片宽度 + 'length' => 5, + // 验证码位数 + 'fontttf' => '', + // 验证码字体,不设置随机获取 + 'bg' => [243, 251, 254], + // 背景颜色 + 'reset' => true, + // 验证成功后是否重置 + ]; + + private $im = null; // 验证码图片实例 + private $color = null; // 验证码字体颜色 + + /** + * 架构方法 设置参数 + * @access public + * @param array $config 配置参数 + */ + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 使用 $this->name 获取配置 + * @access public + * @param string $name 配置名称 + * @return mixed 配置值 + */ + public function __get($name) + { + return $this->config[$name]; + } + + /** + * 设置验证码配置 + * @access public + * @param string $name 配置名称 + * @param string $value 配置值 + * @return void + */ + public function __set($name, $value) + { + if (isset($this->config[$name])) { + $this->config[$name] = $value; + } + } + + /** + * 检查配置 + * @access public + * @param string $name 配置名称 + * @return bool + */ + public function __isset($name) + { + return isset($this->config[$name]); + } + + /** + * 验证验证码是否正确 + * @access public + * @param string $code 用户验证码 + * @param string $id 验证码标识 + * @return bool 用户验证码是否正确 + */ + public function check($code, $id = '') + { + $key = $this->authcode($this->seKey) . $id; + // 验证码不能为空 + $secode = Session::get($key, ''); + if (empty($code) || empty($secode)) { + return false; + } + // session 过期 + if (time() - $secode['verify_time'] > $this->expire) { + Session::delete($key, ''); + return false; + } + + if ($this->authcode(strtoupper($code)) == $secode['verify_code']) { + $this->reset && Session::delete($key, ''); + return true; + } + + return false; + } + + /** + * 输出验证码并把验证码的值保存的session中 + * 验证码保存到session的格式为: array('verify_code' => '验证码值', 'verify_time' => '验证码创建时间'); + * @access public + * @param string $id 要生成验证码的标识 + * @return \think\Response + */ + public function entry($id = '') + { + // 图片宽(px) + $this->imageW || $this->imageW = $this->length * $this->fontSize * 1.5 + $this->length * $this->fontSize / 2; + // 图片高(px) + $this->imageH || $this->imageH = $this->fontSize * 2.5; + // 建立一幅 $this->imageW x $this->imageH 的图像 + $this->im = imagecreate($this->imageW, $this->imageH); + // 设置背景 + imagecolorallocate($this->im, $this->bg[0], $this->bg[1], $this->bg[2]); + + // 验证码字体随机颜色 + $this->color = imagecolorallocate($this->im, mt_rand(1, 150), mt_rand(1, 150), mt_rand(1, 150)); + // 验证码使用随机字体 + $ttfPath = __DIR__ . '/../assets/' . ($this->useZh ? 'zhttfs' : 'ttfs') . '/'; + + if (empty($this->fontttf)) { + $dir = dir($ttfPath); + $ttfs = []; + while (false !== ($file = $dir->read())) { + if ('.' != $file[0] && substr($file, -4) == '.ttf') { + $ttfs[] = $file; + } + } + $dir->close(); + $this->fontttf = $ttfs[array_rand($ttfs)]; + } + $this->fontttf = $ttfPath . $this->fontttf; + + if ($this->useImgBg) { + $this->background(); + } + + if ($this->useNoise) { + // 绘杂点 + $this->writeNoise(); + } + if ($this->useCurve) { + // 绘干扰线 + $this->writeCurve(); + } + + // 绘验证码 + $code = []; // 验证码 + $codeNX = 0; // 验证码第N个字符的左边距 + if ($this->useZh) { + // 中文验证码 + for ($i = 0; $i < $this->length; $i++) { + $code[$i] = iconv_substr($this->zhSet, floor(mt_rand(0, mb_strlen($this->zhSet, 'utf-8') - 1)), 1, 'utf-8'); + imagettftext($this->im, $this->fontSize, mt_rand(-40, 40), $this->fontSize * ($i + 1) * 1.5, $this->fontSize + mt_rand(10, 20), $this->color, $this->fontttf, $code[$i]); + } + } else { + for ($i = 0; $i < $this->length; $i++) { + $code[$i] = $this->codeSet[mt_rand(0, strlen($this->codeSet) - 1)]; + $codeNX += mt_rand($this->fontSize * 1.2, $this->fontSize * 1.6); + imagettftext($this->im, $this->fontSize, mt_rand(-40, 40), $codeNX, $this->fontSize * 1.6, $this->color, $this->fontttf, $code[$i]); + } + } + + // 保存验证码 + $key = $this->authcode($this->seKey); + $code = $this->authcode(strtoupper(implode('', $code))); + $secode = []; + $secode['verify_code'] = $code; // 把校验码保存到session + $secode['verify_time'] = time(); // 验证码创建时间 + Session::set($key . $id, $secode, ''); + + ob_start(); + // 输出图像 + imagepng($this->im); + $content = ob_get_clean(); + imagedestroy($this->im); + + return response($content, 200, ['Content-Length' => strlen($content)])->contentType('image/png'); + } + + /** + * 画一条由两条连在一起构成的随机正弦函数曲线作干扰线(你可以改成更帅的曲线函数) + * + * 高中的数学公式咋都忘了涅,写出来 + * 正弦型函数解析式:y=Asin(ωx+φ)+b + * 各常数值对函数图像的影响: + * A:决定峰值(即纵向拉伸压缩的倍数) + * b:表示波形在Y轴的位置关系或纵向移动距离(上加下减) + * φ:决定波形与X轴位置关系或横向移动距离(左加右减) + * ω:决定周期(最小正周期T=2π/∣ω∣) + * + */ + private function writeCurve() + { + $px = $py = 0; + + // 曲线前部分 + $A = mt_rand(1, $this->imageH / 2); // 振幅 + $b = mt_rand(-$this->imageH / 4, $this->imageH / 4); // Y轴方向偏移量 + $f = mt_rand(-$this->imageH / 4, $this->imageH / 4); // X轴方向偏移量 + $T = mt_rand($this->imageH, $this->imageW * 2); // 周期 + $w = (2 * M_PI) / $T; + + $px1 = 0; // 曲线横坐标起始位置 + $px2 = mt_rand($this->imageW / 2, $this->imageW * 0.8); // 曲线横坐标结束位置 + + for ($px = $px1; $px <= $px2; $px = $px + 1) { + if (0 != $w) { + $py = $A * sin($w * $px + $f) + $b + $this->imageH / 2; // y = Asin(ωx+φ) + b + $i = (int) ($this->fontSize / 5); + while ($i > 0) { + imagesetpixel($this->im, $px + $i, $py + $i, $this->color); // 这里(while)循环画像素点比imagettftext和imagestring用字体大小一次画出(不用这while循环)性能要好很多 + $i--; + } + } + } + + // 曲线后部分 + $A = mt_rand(1, $this->imageH / 2); // 振幅 + $f = mt_rand(-$this->imageH / 4, $this->imageH / 4); // X轴方向偏移量 + $T = mt_rand($this->imageH, $this->imageW * 2); // 周期 + $w = (2 * M_PI) / $T; + $b = $py - $A * sin($w * $px + $f) - $this->imageH / 2; + $px1 = $px2; + $px2 = $this->imageW; + + for ($px = $px1; $px <= $px2; $px = $px + 1) { + if (0 != $w) { + $py = $A * sin($w * $px + $f) + $b + $this->imageH / 2; // y = Asin(ωx+φ) + b + $i = (int) ($this->fontSize / 5); + while ($i > 0) { + imagesetpixel($this->im, $px + $i, $py + $i, $this->color); + $i--; + } + } + } + } + + /** + * 画杂点 + * 往图片上写不同颜色的字母或数字 + */ + private function writeNoise() + { + $codeSet = '2345678abcdefhijkmnpqrstuvwxyz'; + for ($i = 0; $i < 10; $i++) { + //杂点颜色 + $noiseColor = imagecolorallocate($this->im, mt_rand(150, 225), mt_rand(150, 225), mt_rand(150, 225)); + for ($j = 0; $j < 5; $j++) { + // 绘杂点 + imagestring($this->im, 5, mt_rand(-10, $this->imageW), mt_rand(-10, $this->imageH), $codeSet[mt_rand(0, 29)], $noiseColor); + } + } + } + + /** + * 绘制背景图片 + * 注:如果验证码输出图片比较大,将占用比较多的系统资源 + */ + private function background() + { + $path = __DIR__ . '/../assets/bgs/'; + $dir = dir($path); + + $bgs = []; + while (false !== ($file = $dir->read())) { + if ('.' != $file[0] && substr($file, -4) == '.jpg') { + $bgs[] = $path . $file; + } + } + $dir->close(); + + $gb = $bgs[array_rand($bgs)]; + + list($width, $height) = @getimagesize($gb); + // Resample + $bgImage = @imagecreatefromjpeg($gb); + @imagecopyresampled($this->im, $bgImage, 0, 0, 0, 0, $this->imageW, $this->imageH, $width, $height); + @imagedestroy($bgImage); + } + + /* 加密验证码 */ + private function authcode($str) + { + $key = substr(md5($this->seKey), 5, 8); + $str = substr(md5($str), 8, 10); + return md5($key . $str); + } +} diff --git a/vendor/topthink/think-captcha/src/CaptchaController.php b/vendor/topthink/think-captcha/src/CaptchaController.php new file mode 100644 index 0000000..5452db6 --- /dev/null +++ b/vendor/topthink/think-captcha/src/CaptchaController.php @@ -0,0 +1,23 @@ + +// +---------------------------------------------------------------------- + +namespace think\captcha; + +use think\facade\Config; + +class CaptchaController +{ + public function index($id = "") + { + $captcha = new Captcha((array) Config::pull('captcha')); + return $captcha->entry($id); + } +} diff --git a/vendor/topthink/think-captcha/src/helper.php b/vendor/topthink/think-captcha/src/helper.php new file mode 100644 index 0000000..6049807 --- /dev/null +++ b/vendor/topthink/think-captcha/src/helper.php @@ -0,0 +1,61 @@ + +// +---------------------------------------------------------------------- + +Route::get('captcha/[:id]', "\\think\\captcha\\CaptchaController@index"); + +Validate::extend('captcha', function ($value, $id = '') { + return captcha_check($value, $id); +}); + +Validate::setTypeMsg('captcha', ':attribute错误!'); + +/** + * @param string $id + * @param array $config + * @return \think\Response + */ +function captcha($id = '', $config = []) +{ + $captcha = new \think\captcha\Captcha($config); + return $captcha->entry($id); +} + +/** + * @param $id + * @return string + */ +function captcha_src($id = '') +{ +// return Url::build('/captcha' . ($id ? "/{$id}" : '')); + $base_file = str_replace(ADMIN_FILE, 'index.php', request()->baseFile()); + return $base_file.'/captcha'.($id ? "/{$id}" : '').'.html'; +} + +/** + * @param $id + * @return mixed + */ +function captcha_img($id = '') +{ + return 'captcha'; +} + +/** + * @param $value + * @param string $id + * @param array $config + * @return bool + */ +function captcha_check($value, $id = '') +{ + $captcha = new \think\captcha\Captcha((array) Config::pull('captcha')); + return $captcha->check($value, $id); +} diff --git a/vendor/topthink/think-helper/.gitignore b/vendor/topthink/think-helper/.gitignore new file mode 100644 index 0000000..e244eda --- /dev/null +++ b/vendor/topthink/think-helper/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +/.idea/ \ No newline at end of file diff --git a/vendor/topthink/think-helper/LICENSE b/vendor/topthink/think-helper/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/vendor/topthink/think-helper/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/topthink/think-helper/README.md b/vendor/topthink/think-helper/README.md new file mode 100644 index 0000000..27ef670 --- /dev/null +++ b/vendor/topthink/think-helper/README.md @@ -0,0 +1,92 @@ +# thinkphp5 常用的一些扩展类库 + +> 更新完善中 + +> 以下类库都在`\\think\\helper`命名空间下 + +## Str +> 字符串操作 + +``` +// 检查字符串中是否包含某些字符串 +Str::contains($haystack, $needles) + +// 检查字符串是否以某些字符串结尾 +Str::endsWith($haystack, $needles) + +// 获取指定长度的随机字母数字组合的字符串 +Str::random($length = 16) + +// 字符串转小写 +Str::lower($value) + +// 字符串转大写 +Str::upper($value) + +// 获取字符串的长度 +Str::length($value) + +// 截取字符串 +Str::substr($string, $start, $length = null) + +``` + +## Hash +> 创建密码的哈希 + +``` +// 创建 +Hash::make($value, $type = null, array $options = []) + +// 检查 +Hash::check($value, $hashedValue, $type = null, array $options = []) + +``` + +## Time +> 时间戳操作 + +``` +// 今日开始和结束的时间戳 +Time::today(); + +// 昨日开始和结束的时间戳 +Time::yesterday(); + +// 本周开始和结束的时间戳 +Time::week(); + +// 上周开始和结束的时间戳 +Time::lastWeek(); + +// 本月开始和结束的时间戳 +Time::month(); + +// 上月开始和结束的时间戳 +Time::lastMonth(); + +// 今年开始和结束的时间戳 +Time::year(); + +// 去年开始和结束的时间戳 +Time::lastYear(); + +// 获取7天前零点到现在的时间戳 +Time::dayToNow(7) + +// 获取7天前零点到昨日结束的时间戳 +Time::dayToNow(7, true) + +// 获取7天前的时间戳 +Time::daysAgo(7) + +// 获取7天后的时间戳 +Time::daysAfter(7) + +// 天数转换成秒数 +Time::daysToSecond(5) + +// 周数转换成秒数 +Time::weekToSecond(5) + +``` \ No newline at end of file diff --git a/vendor/topthink/think-helper/composer.json b/vendor/topthink/think-helper/composer.json new file mode 100644 index 0000000..d246fa3 --- /dev/null +++ b/vendor/topthink/think-helper/composer.json @@ -0,0 +1,19 @@ +{ + "name": "topthink/think-helper", + "description": "The ThinkPHP5 Helper Package", + "license": "Apache-2.0", + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "autoload": { + "psr-4": { + "think\\helper\\": "src" + }, + "files": [ + "src/helper.php" + ] + } +} diff --git a/vendor/topthink/think-helper/src/Arr.php b/vendor/topthink/think-helper/src/Arr.php new file mode 100644 index 0000000..7350bb2 --- /dev/null +++ b/vendor/topthink/think-helper/src/Arr.php @@ -0,0 +1,41 @@ + +// +---------------------------------------------------------------------- + +namespace think\helper; + + +class Arr +{ + + public static function isAssoc(array $array) + { + $keys = array_keys($array); + + return array_keys($keys) !== $keys; + } + + public static function sortRecursive($array) + { + foreach ($array as &$value) { + if (is_array($value)) { + $value = static::sortRecursive($value); + } + } + + if (static::isAssoc($array)) { + ksort($array); + } else { + sort($array); + } + + return $array; + } +} \ No newline at end of file diff --git a/vendor/topthink/think-helper/src/Hash.php b/vendor/topthink/think-helper/src/Hash.php new file mode 100644 index 0000000..b56543e --- /dev/null +++ b/vendor/topthink/think-helper/src/Hash.php @@ -0,0 +1,48 @@ + +// +---------------------------------------------------------------------- + +namespace think\helper; + + +class Hash +{ + protected static $handle = []; + + public static function make($value, $type = null, array $options = []) + { + return self::handle($type)->make($value, $options); + } + + public static function check($value, $hashedValue, $type = null, array $options = []) + { + return self::handle($type)->check($value, $hashedValue, $options); + } + + public static function handle($type) + { + if (is_null($type)) { + if (PHP_VERSION_ID >= 50500) { + $type = 'bcrypt'; + } else { + $type = 'md5'; + } + } + if (empty(self::$handle[$type])) { + $class = "\\think\\helper\\hash\\" . ucfirst($type); + if (!class_exists($class)) { + throw new \ErrorException("Not found {$type} hash type!"); + } + self::$handle[$type] = new $class(); + } + return self::$handle[$type]; + } + +} \ No newline at end of file diff --git a/vendor/topthink/think-helper/src/Str.php b/vendor/topthink/think-helper/src/Str.php new file mode 100644 index 0000000..ba56cb4 --- /dev/null +++ b/vendor/topthink/think-helper/src/Str.php @@ -0,0 +1,202 @@ + +// +---------------------------------------------------------------------- +namespace think\helper; + +class Str +{ + + protected static $snakeCache = []; + + protected static $camelCache = []; + + protected static $studlyCache = []; + + /** + * 检查字符串中是否包含某些字符串 + * @param string $haystack + * @param string|array $needles + * @return bool + */ + public static function contains($haystack, $needles) + { + foreach ((array) $needles as $needle) { + if ($needle != '' && mb_strpos($haystack, $needle) !== false) { + return true; + } + } + + return false; + } + + /** + * 检查字符串是否以某些字符串结尾 + * + * @param string $haystack + * @param string|array $needles + * @return bool + */ + public static function endsWith($haystack, $needles) + { + foreach ((array) $needles as $needle) { + if ((string) $needle === static::substr($haystack, -static::length($needle))) { + return true; + } + } + + return false; + } + + /** + * 检查字符串是否以某些字符串开头 + * + * @param string $haystack + * @param string|array $needles + * @return bool + */ + public static function startsWith($haystack, $needles) + { + foreach ((array) $needles as $needle) { + if ($needle != '' && mb_strpos($haystack, $needle) === 0) { + return true; + } + } + + return false; + } + + /** + * 获取指定长度的随机字母数字组合的字符串 + * + * @param int $length + * @return string + */ + public static function random($length = 16) + { + $pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + + return static::substr(str_shuffle(str_repeat($pool, $length)), 0, $length); + } + + /** + * 字符串转小写 + * + * @param string $value + * @return string + */ + public static function lower($value) + { + return mb_strtolower($value, 'UTF-8'); + } + + /** + * 字符串转大写 + * + * @param string $value + * @return string + */ + public static function upper($value) + { + return mb_strtoupper($value, 'UTF-8'); + } + + /** + * 获取字符串的长度 + * + * @param string $value + * @return int + */ + public static function length($value) + { + return mb_strlen($value); + } + + /** + * 截取字符串 + * + * @param string $string + * @param int $start + * @param int|null $length + * @return string + */ + public static function substr($string, $start, $length = null) + { + return mb_substr($string, $start, $length, 'UTF-8'); + } + + /** + * 驼峰转下划线 + * + * @param string $value + * @param string $delimiter + * @return string + */ + public static function snake($value, $delimiter = '_') + { + $key = $value; + + if (isset(static::$snakeCache[$key][$delimiter])) { + return static::$snakeCache[$key][$delimiter]; + } + + if (!ctype_lower($value)) { + $value = preg_replace('/\s+/u', '', $value); + + $value = static::lower(preg_replace('/(.)(?=[A-Z])/u', '$1' . $delimiter, $value)); + } + + return static::$snakeCache[$key][$delimiter] = $value; + } + + /** + * 下划线转驼峰(首字母小写) + * + * @param string $value + * @return string + */ + public static function camel($value) + { + if (isset(static::$camelCache[$value])) { + return static::$camelCache[$value]; + } + + return static::$camelCache[$value] = lcfirst(static::studly($value)); + } + + /** + * 下划线转驼峰(首字母大写) + * + * @param string $value + * @return string + */ + public static function studly($value) + { + $key = $value; + + if (isset(static::$studlyCache[$key])) { + return static::$studlyCache[$key]; + } + + $value = ucwords(str_replace(['-', '_'], ' ', $value)); + + return static::$studlyCache[$key] = str_replace(' ', '', $value); + } + + /** + * 转为首字母大写的标题格式 + * + * @param string $value + * @return string + */ + public static function title($value) + { + return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8'); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-helper/src/Time.php b/vendor/topthink/think-helper/src/Time.php new file mode 100644 index 0000000..aede75f --- /dev/null +++ b/vendor/topthink/think-helper/src/Time.php @@ -0,0 +1,203 @@ + +// +---------------------------------------------------------------------- +namespace think\helper; + +class Time +{ + /** + * 返回今日开始和结束的时间戳 + * + * @return array + */ + public static function today() + { + list($y, $m, $d) = explode('-', date('Y-m-d')); + return [ + mktime(0, 0, 0, $m, $d, $y), + mktime(23, 59, 59, $m, $d, $y) + ]; + } + + /** + * 返回昨日开始和结束的时间戳 + * + * @return array + */ + public static function yesterday() + { + $yesterday = date('d') - 1; + return [ + mktime(0, 0, 0, date('m'), $yesterday, date('Y')), + mktime(23, 59, 59, date('m'), $yesterday, date('Y')) + ]; + } + + /** + * 返回本周开始和结束的时间戳 + * + * @return array + */ + public static function week() + { + list($y, $m, $d, $w) = explode('-', date('Y-m-d-w')); + if($w == 0) $w = 7; //修正周日的问题 + return [ + mktime(0, 0, 0, $m, $d - $w + 1, $y), mktime(23, 59, 59, $m, $d - $w + 7, $y) + ]; + } + + /** + * 返回上周开始和结束的时间戳 + * + * @return array + */ + public static function lastWeek() + { + $timestamp = time(); + return [ + strtotime(date('Y-m-d', strtotime("last week Monday", $timestamp))), + strtotime(date('Y-m-d', strtotime("last week Sunday", $timestamp))) + 24 * 3600 - 1 + ]; + } + + /** + * 返回本月开始和结束的时间戳 + * + * @return array + */ + public static function month($everyDay = false) + { + list($y, $m, $t) = explode('-', date('Y-m-t')); + return [ + mktime(0, 0, 0, $m, 1, $y), + mktime(23, 59, 59, $m, $t, $y) + ]; + } + + /** + * 返回上个月开始和结束的时间戳 + * + * @return array + */ + public static function lastMonth() + { + $y = date('Y'); + $m = date('m'); + $begin = mktime(0, 0, 0, $m - 1, 1, $y); + $end = mktime(23, 59, 59, $m - 1, date('t', $begin), $y); + + return [$begin, $end]; + } + + /** + * 返回今年开始和结束的时间戳 + * + * @return array + */ + public static function year() + { + $y = date('Y'); + return [ + mktime(0, 0, 0, 1, 1, $y), + mktime(23, 59, 59, 12, 31, $y) + ]; + } + + /** + * 返回去年开始和结束的时间戳 + * + * @return array + */ + public static function lastYear() + { + $year = date('Y') - 1; + return [ + mktime(0, 0, 0, 1, 1, $year), + mktime(23, 59, 59, 12, 31, $year) + ]; + } + + public static function dayOf() + { + + } + + /** + * 获取几天前零点到现在/昨日结束的时间戳 + * + * @param int $day 天数 + * @param bool $now 返回现在或者昨天结束时间戳 + * @return array + */ + public static function dayToNow($day = 1, $now = true) + { + $end = time(); + if (!$now) { + list($foo, $end) = self::yesterday(); + } + + return [ + mktime(0, 0, 0, date('m'), date('d') - $day, date('Y')), + $end + ]; + } + + /** + * 返回几天前的时间戳 + * + * @param int $day + * @return int + */ + public static function daysAgo($day = 1) + { + $nowTime = time(); + return $nowTime - self::daysToSecond($day); + } + + /** + * 返回几天后的时间戳 + * + * @param int $day + * @return int + */ + public static function daysAfter($day = 1) + { + $nowTime = time(); + return $nowTime + self::daysToSecond($day); + } + + /** + * 天数转换成秒数 + * + * @param int $day + * @return int + */ + public static function daysToSecond($day = 1) + { + return $day * 86400; + } + + /** + * 周数转换成秒数 + * + * @param int $week + * @return int + */ + public static function weekToSecond($week = 1) + { + return self::daysToSecond() * 7 * $week; + } + + private static function startTimeToEndTime() + { + + } +} diff --git a/vendor/topthink/think-helper/src/hash/Bcrypt.php b/vendor/topthink/think-helper/src/hash/Bcrypt.php new file mode 100644 index 0000000..8f55a77 --- /dev/null +++ b/vendor/topthink/think-helper/src/hash/Bcrypt.php @@ -0,0 +1,51 @@ + +// +---------------------------------------------------------------------- +namespace think\helper\hash; + +class Bcrypt +{ + + /** + * Default crypt cost factor. + * + * @var int + */ + protected $rounds = 10; + + public function make($value, array $options = []) + { + $cost = isset($options['rounds']) ? $options['rounds'] : $this->rounds; + + $hash = password_hash($value, PASSWORD_BCRYPT, ['cost' => $cost]); + + if ($hash === false) { + throw new \RuntimeException('Bcrypt hashing not supported.'); + } + + return $hash; + } + + public function check($value, $hashedValue, array $options = []) + { + if (strlen($hashedValue) === 0) { + return false; + } + + return password_verify($value, $hashedValue); + } + + public function setRounds($rounds) + { + $this->rounds = (int)$rounds; + + return $this; + } +} \ No newline at end of file diff --git a/vendor/topthink/think-helper/src/hash/Md5.php b/vendor/topthink/think-helper/src/hash/Md5.php new file mode 100644 index 0000000..5ec07ca --- /dev/null +++ b/vendor/topthink/think-helper/src/hash/Md5.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- +namespace think\helper\hash; + +class Md5 +{ + + protected $salt = 'think'; + + public function make($value, array $options = []) + { + $salt = isset($options['salt']) ? $options['salt'] : $this->salt; + + return md5(md5($value) . $salt); + } + + public function check($value, $hashedValue, array $options = []) + { + if (strlen($hashedValue) === 0) { + return false; + } + + $salt = isset($options['salt']) ? $options['salt'] : $this->salt; + + return md5(md5($value) . $salt) == $hashedValue; + } + + public function setSalt($salt) + { + $this->salt = (string)$salt; + + return $this; + } +} \ No newline at end of file diff --git a/vendor/topthink/think-helper/src/helper.php b/vendor/topthink/think-helper/src/helper.php new file mode 100644 index 0000000..f6a5523 --- /dev/null +++ b/vendor/topthink/think-helper/src/helper.php @@ -0,0 +1,93 @@ + +// +---------------------------------------------------------------------- + +if (!function_exists('class_basename')) { + /** + * 获取类名(不包含命名空间) + * + * @param string|object $class + * @return string + */ + function class_basename($class) + { + $class = is_object($class) ? get_class($class) : $class; + + return basename(str_replace('\\', '/', $class)); + } +} + +if (!function_exists('class_uses_recursive')) { + /** + *获取一个类里所有用到的trait,包括父类的 + * + * @param $class + * @return array + */ + function class_uses_recursive($class) + { + if (is_object($class)) { + $class = get_class($class); + } + + $results = []; + + foreach (array_merge([$class => $class], class_parents($class)) as $class) { + $results += trait_uses_recursive($class); + } + + return array_unique($results); + } +} + +if (!function_exists('trait_uses_recursive')) { + /** + * 获取一个trait里所有引用到的trait + * + * @param string $trait + * @return array + */ + function trait_uses_recursive($trait) + { + $traits = class_uses($trait); + + foreach ($traits as $trait) { + $traits += trait_uses_recursive($trait); + } + + return $traits; + } +} +if (!function_exists('classnames')) { + /** + * css样式名生成器 + * classnames("foo", "bar"); // => "foo bar" + * classnames("foo", [ "bar"=> true ]); // => "foo bar" + * classnames([ "foo-bar"=> true ]); // => "foo-bar" + * classnames([ "foo-bar"=> false ]); // => " + * classnames([ "foo" => true ], [ "bar"=> true ]); // => "foo bar" + * classnames([ "foo" => true, "bar"=> true ]); // => "foo bar" + * classnames("foo", [ "bar"=> true, "duck"=> false ], "baz", [ "quux"=> true ]); // => "foo bar baz quux" + * classnames(null, false, "bar", 0, 1, [ "baz"=> null ]); // => "bar 1" + */ + function classnames() + { + $args = func_get_args(); + $classes = array_map(function ($arg) { + if (is_array($arg)) { + return implode(" ", array_filter(array_map(function ($expression, $class) { + return $expression ? $class : false; + }, $arg, array_keys($arg)))); + } + return $arg; + }, $args); + return implode(" ", array_filter($classes)); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/.gitignore b/vendor/topthink/think-image/.gitignore new file mode 100644 index 0000000..e2974aa --- /dev/null +++ b/vendor/topthink/think-image/.gitignore @@ -0,0 +1,4 @@ +/vendor/ +/thinkphp/ +/composer.lock +/.idea/ \ No newline at end of file diff --git a/vendor/topthink/think-image/.travis.yml b/vendor/topthink/think-image/.travis.yml new file mode 100644 index 0000000..75b4c97 --- /dev/null +++ b/vendor/topthink/think-image/.travis.yml @@ -0,0 +1,22 @@ +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +matrix: + allow_failures: + - php: 7.0 + - php: hhvm + +before_script: + - composer self-update + - composer install --prefer-source --no-interaction --dev + +script: phpunit --coverage-clover=coverage.xml --configuration=phpunit.xml + +after_success: + - bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/vendor/topthink/think-image/LICENSE b/vendor/topthink/think-image/LICENSE new file mode 100644 index 0000000..c0ee812 --- /dev/null +++ b/vendor/topthink/think-image/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/topthink/think-image/README.md b/vendor/topthink/think-image/README.md new file mode 100644 index 0000000..68eb804 --- /dev/null +++ b/vendor/topthink/think-image/README.md @@ -0,0 +1,29 @@ +# The ThinkPHP5 Image Package + +[![Build Status](https://img.shields.io/travis/top-think/think-image.svg)](https://travis-ci.org/top-think/think-image) +[![Coverage Status](https://img.shields.io/codecov/c/github/top-think/think-image.svg)](https://codecov.io/github/top-think/think-image) +[![Downloads](https://img.shields.io/github/downloads/top-think/think-image/total.svg)](https://github.com/top-think/think-image/releases) +[![Releases](https://img.shields.io/github/release/top-think/think-image.svg)](https://github.com/top-think/think-image/releases/latest) +[![Releases Downloads](https://img.shields.io/github/downloads/top-think/think-image/latest/total.svg)](https://github.com/top-think/think-image/releases/latest) +[![Packagist Status](https://img.shields.io/packagist/v/top-think/think-image.svg)](https://packagist.org/packages/topthink/think-image) +[![Packagist Downloads](https://img.shields.io/packagist/dt/top-think/think-image.svg)](https://packagist.org/packages/topthink/think-image) + +## 安装 + +> composer require topthink/think-image + +## 使用 + +~~~ +$image = \think\Image::open('./image.jpg'); +或者 +$image = \think\Image::open(request()->file('image')); + + +$image->crop(...) + ->thumb(...) + ->water(...) + ->text(....) + ->save(..); + +~~~ \ No newline at end of file diff --git a/vendor/topthink/think-image/composer.json b/vendor/topthink/think-image/composer.json new file mode 100644 index 0000000..b06cdd5 --- /dev/null +++ b/vendor/topthink/think-image/composer.json @@ -0,0 +1,26 @@ +{ + "name": "topthink/think-image", + "description": "The ThinkPHP5 Image Package", + "license": "Apache-2.0", + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "require": { + "ext-gd": "*" + }, + "require-dev": { + "topthink/framework": "^5.0", + "phpunit/phpunit": "4.8.*" + }, + "config": { + "preferred-install": "dist" + }, + "autoload": { + "psr-4": { + "think\\": "src" + } + } +} diff --git a/vendor/topthink/think-image/phpunit.xml b/vendor/topthink/think-image/phpunit.xml new file mode 100644 index 0000000..ecbff6c --- /dev/null +++ b/vendor/topthink/think-image/phpunit.xml @@ -0,0 +1,20 @@ + + + + + ./tests/ + + + + + + diff --git a/vendor/topthink/think-image/src/Image.php b/vendor/topthink/think-image/src/Image.php new file mode 100644 index 0000000..6f5814d --- /dev/null +++ b/vendor/topthink/think-image/src/Image.php @@ -0,0 +1,610 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\image\Exception as ImageException; +use think\image\gif\Gif; + +class Image +{ + + /* 缩略图相关常量定义 */ + const THUMB_SCALING = 1; //常量,标识缩略图等比例缩放类型 + const THUMB_FILLED = 2; //常量,标识缩略图缩放后填充类型 + const THUMB_CENTER = 3; //常量,标识缩略图居中裁剪类型 + const THUMB_NORTHWEST = 4; //常量,标识缩略图左上角裁剪类型 + const THUMB_SOUTHEAST = 5; //常量,标识缩略图右下角裁剪类型 + const THUMB_FIXED = 6; //常量,标识缩略图固定尺寸缩放类型 + /* 水印相关常量定义 */ + const WATER_NORTHWEST = 1; //常量,标识左上角水印 + const WATER_NORTH = 2; //常量,标识上居中水印 + const WATER_NORTHEAST = 3; //常量,标识右上角水印 + const WATER_WEST = 4; //常量,标识左居中水印 + const WATER_CENTER = 5; //常量,标识居中水印 + const WATER_EAST = 6; //常量,标识右居中水印 + const WATER_SOUTHWEST = 7; //常量,标识左下角水印 + const WATER_SOUTH = 8; //常量,标识下居中水印 + const WATER_SOUTHEAST = 9; //常量,标识右下角水印 + /* 翻转相关常量定义 */ + const FLIP_X = 1; //X轴翻转 + const FLIP_Y = 2; //Y轴翻转 + + /** + * 图像资源对象 + * + * @var resource + */ + protected $im; + + /** @var Gif */ + protected $gif; + + /** + * 图像信息,包括 width, height, type, mime, size + * + * @var array + */ + protected $info; + + protected function __construct(\SplFileInfo $file) + { + //获取图像信息 + $info = @getimagesize($file->getPathname()); + + //检测图像合法性 + if (false === $info || (IMAGETYPE_GIF === $info[2] && empty($info['bits']))) { + throw new ImageException('Illegal image file'); + } + + //设置图像信息 + $this->info = [ + 'width' => $info[0], + 'height' => $info[1], + 'type' => image_type_to_extension($info[2], false), + 'mime' => $info['mime'], + ]; + + //打开图像 + if ('gif' == $this->info['type']) { + $this->gif = new Gif($file->getPathname()); + $this->im = @imagecreatefromstring($this->gif->image()); + } else { + $fun = "imagecreatefrom{$this->info['type']}"; + $this->im = @$fun($file->getPathname()); + } + + if (empty($this->im)) { + throw new ImageException('Failed to create image resources!'); + } + + } + + /** + * 打开一个图片文件 + * @param \SplFileInfo|string $file + * @return Image + */ + public static function open($file) + { + if (is_string($file)) { + $file = new \SplFileInfo($file); + } + if (!$file->isFile()) { + throw new ImageException('image file not exist'); + } + return new self($file); + } + + /** + * 保存图像 + * @param string $pathname 图像保存路径名称 + * @param null|string $type 图像类型 + * @param int $quality 图像质量 + * @param bool $interlace 是否对JPEG类型图像设置隔行扫描 + * @return $this + */ + public function save($pathname, $type = null, $quality = 80, $interlace = true) + { + //自动获取图像类型 + if (is_null($type)) { + $type = $this->info['type']; + } else { + $type = strtolower($type); + } + //保存图像 + if ('jpeg' == $type || 'jpg' == $type) { + //JPEG图像设置隔行扫描 + imageinterlace($this->im, $interlace); + imagejpeg($this->im, $pathname, $quality); + } elseif ('gif' == $type && !empty($this->gif)) { + $this->gif->save($pathname); + } elseif ('png' == $type) { + //设定保存完整的 alpha 通道信息 + imagesavealpha($this->im, true); + //ImagePNG生成图像的质量范围从0到9的 + imagepng($this->im, $pathname, min((int) ($quality / 10), 9)); + } else { + $fun = 'image' . $type; + $fun($this->im, $pathname); + } + + return $this; + } + + /** + * 返回图像宽度 + * @return int 图像宽度 + */ + public function width() + { + return $this->info['width']; + } + + /** + * 返回图像高度 + * @return int 图像高度 + */ + public function height() + { + return $this->info['height']; + } + + /** + * 返回图像类型 + * @return string 图像类型 + */ + public function type() + { + return $this->info['type']; + } + + /** + * 返回图像MIME类型 + * @return string 图像MIME类型 + */ + public function mime() + { + return $this->info['mime']; + } + + /** + * 返回图像尺寸数组 0 - 图像宽度,1 - 图像高度 + * @return array 图像尺寸 + */ + public function size() + { + return [$this->info['width'], $this->info['height']]; + } + + /** + * 旋转图像 + * @param int $degrees 顺时针旋转的度数 + * @return $this + */ + public function rotate($degrees = 90) + { + do { + $img = imagerotate($this->im, -$degrees, imagecolorallocatealpha($this->im, 0, 0, 0, 127)); + imagedestroy($this->im); + $this->im = $img; + } while (!empty($this->gif) && $this->gifNext()); + + $this->info['width'] = imagesx($this->im); + $this->info['height'] = imagesy($this->im); + + return $this; + } + + /** + * 翻转图像 + * @param integer $direction 翻转轴,X或者Y + * @return $this + */ + public function flip($direction = self::FLIP_X) + { + //原图宽度和高度 + $w = $this->info['width']; + $h = $this->info['height']; + + do { + + $img = imagecreatetruecolor($w, $h); + + switch ($direction) { + case self::FLIP_X: + for ($y = 0; $y < $h; $y++) { + imagecopy($img, $this->im, 0, $h - $y - 1, 0, $y, $w, 1); + } + break; + case self::FLIP_Y: + for ($x = 0; $x < $w; $x++) { + imagecopy($img, $this->im, $w - $x - 1, 0, $x, 0, 1, $h); + } + break; + default: + throw new ImageException('不支持的翻转类型'); + } + + imagedestroy($this->im); + $this->im = $img; + + } while (!empty($this->gif) && $this->gifNext()); + + return $this; + } + + /** + * 裁剪图像 + * + * @param integer $w 裁剪区域宽度 + * @param integer $h 裁剪区域高度 + * @param integer $x 裁剪区域x坐标 + * @param integer $y 裁剪区域y坐标 + * @param integer $width 图像保存宽度 + * @param integer $height 图像保存高度 + * + * @return $this + */ + public function crop($w, $h, $x = 0, $y = 0, $width = null, $height = null) + { + //设置保存尺寸 + empty($width) && $width = $w; + empty($height) && $height = $h; + do { + //创建新图像 + $img = imagecreatetruecolor($width, $height); + // 调整默认颜色 + $color = imagecolorallocate($img, 255, 255, 255); + imagefill($img, 0, 0, $color); + //裁剪 + imagecopyresampled($img, $this->im, 0, 0, $x, $y, $width, $height, $w, $h); + imagedestroy($this->im); //销毁原图 + //设置新图像 + $this->im = $img; + } while (!empty($this->gif) && $this->gifNext()); + $this->info['width'] = (int) $width; + $this->info['height'] = (int) $height; + return $this; + } + + /** + * 生成缩略图 + * + * @param integer $width 缩略图最大宽度 + * @param integer $height 缩略图最大高度 + * @param int $type 缩略图裁剪类型 + * + * @return $this + */ + public function thumb($width, $height, $type = self::THUMB_SCALING) + { + //原图宽度和高度 + $w = $this->info['width']; + $h = $this->info['height']; + /* 计算缩略图生成的必要参数 */ + switch ($type) { + /* 等比例缩放 */ + case self::THUMB_SCALING: + //原图尺寸小于缩略图尺寸则不进行缩略 + if ($w < $width && $h < $height) { + return $this; + } + //计算缩放比例 + $scale = min($width / $w, $height / $h); + //设置缩略图的坐标及宽度和高度 + $x = $y = 0; + $width = $w * $scale; + $height = $h * $scale; + break; + /* 居中裁剪 */ + case self::THUMB_CENTER: + //计算缩放比例 + $scale = max($width / $w, $height / $h); + //设置缩略图的坐标及宽度和高度 + $w = $width / $scale; + $h = $height / $scale; + $x = ($this->info['width'] - $w) / 2; + $y = ($this->info['height'] - $h) / 2; + break; + /* 左上角裁剪 */ + case self::THUMB_NORTHWEST: + //计算缩放比例 + $scale = max($width / $w, $height / $h); + //设置缩略图的坐标及宽度和高度 + $x = $y = 0; + $w = $width / $scale; + $h = $height / $scale; + break; + /* 右下角裁剪 */ + case self::THUMB_SOUTHEAST: + //计算缩放比例 + $scale = max($width / $w, $height / $h); + //设置缩略图的坐标及宽度和高度 + $w = $width / $scale; + $h = $height / $scale; + $x = $this->info['width'] - $w; + $y = $this->info['height'] - $h; + break; + /* 填充 */ + case self::THUMB_FILLED: + //计算缩放比例 + if ($w < $width && $h < $height) { + $scale = 1; + } else { + $scale = min($width / $w, $height / $h); + } + //设置缩略图的坐标及宽度和高度 + $neww = $w * $scale; + $newh = $h * $scale; + $x = $this->info['width'] - $w; + $y = $this->info['height'] - $h; + $posx = ($width - $w * $scale) / 2; + $posy = ($height - $h * $scale) / 2; + do { + //创建新图像 + $img = imagecreatetruecolor($width, $height); + // 调整默认颜色 + $color = imagecolorallocate($img, 255, 255, 255); + imagefill($img, 0, 0, $color); + //裁剪 + imagecopyresampled($img, $this->im, $posx, $posy, $x, $y, $neww, $newh, $w, $h); + imagedestroy($this->im); //销毁原图 + $this->im = $img; + } while (!empty($this->gif) && $this->gifNext()); + $this->info['width'] = (int) $width; + $this->info['height'] = (int) $height; + return $this; + /* 固定 */ + case self::THUMB_FIXED: + $x = $y = 0; + break; + default: + throw new ImageException('不支持的缩略图裁剪类型'); + } + /* 裁剪图像 */ + return $this->crop($w, $h, $x, $y, $width, $height); + } + + /** + * 添加水印 + * + * @param string $source 水印图片路径 + * @param int $locate 水印位置 + * @param int $alpha 透明度 + * @return $this + */ + public function water($source, $locate = self::WATER_SOUTHEAST, $alpha = 100) + { + if (!is_file($source)) { + throw new ImageException('水印图像不存在'); + } + //获取水印图像信息 + $info = getimagesize($source); + if (false === $info || (IMAGETYPE_GIF === $info[2] && empty($info['bits']))) { + throw new ImageException('非法水印文件'); + } + //创建水印图像资源 + $fun = 'imagecreatefrom' . image_type_to_extension($info[2], false); + $water = $fun($source); + //设定水印图像的混色模式 + imagealphablending($water, true); + /* 设定水印位置 */ + switch ($locate) { + /* 右下角水印 */ + case self::WATER_SOUTHEAST: + $x = $this->info['width'] - $info[0]; + $y = $this->info['height'] - $info[1]; + break; + /* 左下角水印 */ + case self::WATER_SOUTHWEST: + $x = 0; + $y = $this->info['height'] - $info[1]; + break; + /* 左上角水印 */ + case self::WATER_NORTHWEST: + $x = $y = 0; + break; + /* 右上角水印 */ + case self::WATER_NORTHEAST: + $x = $this->info['width'] - $info[0]; + $y = 0; + break; + /* 居中水印 */ + case self::WATER_CENTER: + $x = ($this->info['width'] - $info[0]) / 2; + $y = ($this->info['height'] - $info[1]) / 2; + break; + /* 下居中水印 */ + case self::WATER_SOUTH: + $x = ($this->info['width'] - $info[0]) / 2; + $y = $this->info['height'] - $info[1]; + break; + /* 右居中水印 */ + case self::WATER_EAST: + $x = $this->info['width'] - $info[0]; + $y = ($this->info['height'] - $info[1]) / 2; + break; + /* 上居中水印 */ + case self::WATER_NORTH: + $x = ($this->info['width'] - $info[0]) / 2; + $y = 0; + break; + /* 左居中水印 */ + case self::WATER_WEST: + $x = 0; + $y = ($this->info['height'] - $info[1]) / 2; + break; + default: + /* 自定义水印坐标 */ + if (is_array($locate)) { + list($x, $y) = $locate; + } else { + throw new ImageException('不支持的水印位置类型'); + } + } + do { + //添加水印 + $src = imagecreatetruecolor($info[0], $info[1]); + // 调整默认颜色 + $color = imagecolorallocate($src, 255, 255, 255); + imagefill($src, 0, 0, $color); + imagecopy($src, $this->im, 0, 0, $x, $y, $info[0], $info[1]); + imagecopy($src, $water, 0, 0, 0, 0, $info[0], $info[1]); + imagecopymerge($this->im, $src, $x, $y, 0, 0, $info[0], $info[1], $alpha); + //销毁零时图片资源 + imagedestroy($src); + } while (!empty($this->gif) && $this->gifNext()); + //销毁水印资源 + imagedestroy($water); + return $this; + } + + /** + * 图像添加文字 + * + * @param string $text 添加的文字 + * @param string $font 字体路径 + * @param integer $size 字号 + * @param string $color 文字颜色 + * @param int $locate 文字写入位置 + * @param integer $offset 文字相对当前位置的偏移量 + * @param integer $angle 文字倾斜角度 + * + * @return $this + * @throws ImageException + */ + public function text($text, $font, $size, $color = '#00000000', + $locate = self::WATER_SOUTHEAST, $offset = 0, $angle = 0) { + + if (!is_file($font)) { + throw new ImageException("不存在的字体文件:{$font}"); + } + //获取文字信息 + $info = imagettfbbox($size, $angle, $font, $text); + $minx = min($info[0], $info[2], $info[4], $info[6]); + $maxx = max($info[0], $info[2], $info[4], $info[6]); + $miny = min($info[1], $info[3], $info[5], $info[7]); + $maxy = max($info[1], $info[3], $info[5], $info[7]); + /* 计算文字初始坐标和尺寸 */ + $x = $minx; + $y = abs($miny); + $w = $maxx - $minx; + $h = $maxy - $miny; + /* 设定文字位置 */ + switch ($locate) { + /* 右下角文字 */ + case self::WATER_SOUTHEAST: + $x += $this->info['width'] - $w; + $y += $this->info['height'] - $h; + break; + /* 左下角文字 */ + case self::WATER_SOUTHWEST: + $y += $this->info['height'] - $h; + break; + /* 左上角文字 */ + case self::WATER_NORTHWEST: + // 起始坐标即为左上角坐标,无需调整 + break; + /* 右上角文字 */ + case self::WATER_NORTHEAST: + $x += $this->info['width'] - $w; + break; + /* 居中文字 */ + case self::WATER_CENTER: + $x += ($this->info['width'] - $w) / 2; + $y += ($this->info['height'] - $h) / 2; + break; + /* 下居中文字 */ + case self::WATER_SOUTH: + $x += ($this->info['width'] - $w) / 2; + $y += $this->info['height'] - $h; + break; + /* 右居中文字 */ + case self::WATER_EAST: + $x += $this->info['width'] - $w; + $y += ($this->info['height'] - $h) / 2; + break; + /* 上居中文字 */ + case self::WATER_NORTH: + $x += ($this->info['width'] - $w) / 2; + break; + /* 左居中文字 */ + case self::WATER_WEST: + $y += ($this->info['height'] - $h) / 2; + break; + default: + /* 自定义文字坐标 */ + if (is_array($locate)) { + list($posx, $posy) = $locate; + $x += $posx; + $y += $posy; + } else { + throw new ImageException('不支持的文字位置类型'); + } + } + /* 设置偏移量 */ + if (is_array($offset)) { + $offset = array_map('intval', $offset); + list($ox, $oy) = $offset; + } else { + $offset = intval($offset); + $ox = $oy = $offset; + } + /* 设置颜色 */ + if (is_string($color) && 0 === strpos($color, '#')) { + $color = str_split(substr($color, 1), 2); + $color = array_map('hexdec', $color); + if (empty($color[3]) || $color[3] > 127) { + $color[3] = 0; + } + } elseif (!is_array($color)) { + throw new ImageException('错误的颜色值'); + } + do { + /* 写入文字 */ + $col = imagecolorallocatealpha($this->im, $color[0], $color[1], $color[2], $color[3]); + imagettftext($this->im, $size, $angle, $x + $ox, $y + $oy, $col, $font, $text); + } while (!empty($this->gif) && $this->gifNext()); + return $this; + } + + /** + * 切换到GIF的下一帧并保存当前帧 + */ + protected function gifNext() + { + ob_start(); + ob_implicit_flush(0); + imagegif($this->im); + $img = ob_get_clean(); + $this->gif->image($img); + $next = $this->gif->nextImage(); + if ($next) { + imagedestroy($this->im); + $this->im = imagecreatefromstring($next); + return $next; + } else { + imagedestroy($this->im); + $this->im = imagecreatefromstring($this->gif->image()); + return false; + } + } + + /** + * 析构方法,用于销毁图像资源 + */ + public function __destruct() + { + empty($this->im) || imagedestroy($this->im); + } + +} diff --git a/vendor/topthink/think-image/src/image/Exception.php b/vendor/topthink/think-image/src/image/Exception.php new file mode 100644 index 0000000..58ca2ee --- /dev/null +++ b/vendor/topthink/think-image/src/image/Exception.php @@ -0,0 +1,18 @@ + +// +---------------------------------------------------------------------- + +namespace think\image; + + +class Exception extends \RuntimeException +{ + +} \ No newline at end of file diff --git a/vendor/topthink/think-image/src/image/gif/Decoder.php b/vendor/topthink/think-image/src/image/gif/Decoder.php new file mode 100644 index 0000000..2c8c233 --- /dev/null +++ b/vendor/topthink/think-image/src/image/gif/Decoder.php @@ -0,0 +1,207 @@ + +// +---------------------------------------------------------------------- + +namespace think\image\gif; + + +class Decoder +{ + public $GIF_buffer = []; + public $GIF_arrays = []; + public $GIF_delays = []; + public $GIF_stream = ""; + public $GIF_string = ""; + public $GIF_bfseek = 0; + public $GIF_screen = []; + public $GIF_global = []; + public $GIF_sorted; + public $GIF_colorS; + public $GIF_colorC; + public $GIF_colorF; + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFDecoder ( $GIF_pointer ) + :: + */ + public function __construct($GIF_pointer) + { + $this->GIF_stream = $GIF_pointer; + $this->getByte(6); // GIF89a + $this->getByte(7); // Logical Screen Descriptor + $this->GIF_screen = $this->GIF_buffer; + $this->GIF_colorF = $this->GIF_buffer[4] & 0x80 ? 1 : 0; + $this->GIF_sorted = $this->GIF_buffer[4] & 0x08 ? 1 : 0; + $this->GIF_colorC = $this->GIF_buffer[4] & 0x07; + $this->GIF_colorS = 2 << $this->GIF_colorC; + if (1 == $this->GIF_colorF) { + $this->getByte(3 * $this->GIF_colorS); + $this->GIF_global = $this->GIF_buffer; + } + + for ($cycle = 1; $cycle;) { + if ($this->getByte(1)) { + switch ($this->GIF_buffer[0]) { + case 0x21: + $this->readExtensions(); + break; + case 0x2C: + $this->readDescriptor(); + break; + case 0x3B: + $cycle = 0; + break; + } + } else { + $cycle = 0; + } + } + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFReadExtension ( ) + :: + */ + public function readExtensions() + { + $this->getByte(1); + for (; ;) { + $this->getByte(1); + if (($u = $this->GIF_buffer[0]) == 0x00) { + break; + } + $this->getByte($u); + /* + * 07.05.2007. + * Implemented a new line for a new function + * to determine the originaly delays between + * frames. + * + */ + if (4 == $u) { + $this->GIF_delays[] = ($this->GIF_buffer[1] | $this->GIF_buffer[2] << 8); + } + } + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFReadExtension ( ) + :: + */ + public function readDescriptor() + { + $this->getByte(9); + $GIF_screen = $this->GIF_buffer; + $GIF_colorF = $this->GIF_buffer[8] & 0x80 ? 1 : 0; + if ($GIF_colorF) { + $GIF_code = $this->GIF_buffer[8] & 0x07; + $GIF_sort = $this->GIF_buffer[8] & 0x20 ? 1 : 0; + } else { + $GIF_code = $this->GIF_colorC; + $GIF_sort = $this->GIF_sorted; + } + $GIF_size = 2 << $GIF_code; + $this->GIF_screen[4] &= 0x70; + $this->GIF_screen[4] |= 0x80; + $this->GIF_screen[4] |= $GIF_code; + if ($GIF_sort) { + $this->GIF_screen[4] |= 0x08; + } + $this->GIF_string = "GIF87a"; + $this->putByte($this->GIF_screen); + if (1 == $GIF_colorF) { + $this->getByte(3 * $GIF_size); + $this->putByte($this->GIF_buffer); + } else { + $this->putByte($this->GIF_global); + } + $this->GIF_string .= chr(0x2C); + $GIF_screen[8] &= 0x40; + $this->putByte($GIF_screen); + $this->getByte(1); + $this->putByte($this->GIF_buffer); + for (; ;) { + $this->getByte(1); + $this->putByte($this->GIF_buffer); + if (($u = $this->GIF_buffer[0]) == 0x00) { + break; + } + $this->getByte($u); + $this->putByte($this->GIF_buffer); + } + $this->GIF_string .= chr(0x3B); + /* + Add frames into $GIF_stream array... + */ + $this->GIF_arrays[] = $this->GIF_string; + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFGetByte ( $len ) + :: + */ + public function getByte($len) + { + $this->GIF_buffer = []; + for ($i = 0; $i < $len; $i++) { + if ($this->GIF_bfseek > strlen($this->GIF_stream)) { + return 0; + } + $this->GIF_buffer[] = ord($this->GIF_stream{$this->GIF_bfseek++}); + } + return 1; + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFPutByte ( $bytes ) + :: + */ + public function putByte($bytes) + { + for ($i = 0; $i < count($bytes); $i++) { + $this->GIF_string .= chr($bytes[$i]); + } + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: PUBLIC FUNCTIONS + :: + :: + :: GIFGetFrames ( ) + :: + */ + public function getFrames() + { + return ($this->GIF_arrays); + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFGetDelays ( ) + :: + */ + public function getDelays() + { + return ($this->GIF_delays); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/src/image/gif/Encoder.php b/vendor/topthink/think-image/src/image/gif/Encoder.php new file mode 100644 index 0000000..9d898db --- /dev/null +++ b/vendor/topthink/think-image/src/image/gif/Encoder.php @@ -0,0 +1,222 @@ + +// +---------------------------------------------------------------------- +namespace think\image\gif; + +class Encoder +{ + public $GIF = "GIF89a"; /* GIF header 6 bytes */ + public $VER = "GIFEncoder V2.05"; /* Encoder version */ + public $BUF = []; + public $LOP = 0; + public $DIS = 2; + public $COL = -1; + public $IMG = -1; + public $ERR = [ + 'ERR00' => "Does not supported function for only one image!", + 'ERR01' => "Source is not a GIF image!", + 'ERR02' => "Unintelligible flag ", + 'ERR03' => "Does not make animation from animated GIF source", + ]; + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFEncoder... + :: + */ + public function __construct( + $GIF_src, $GIF_dly, $GIF_lop, $GIF_dis, + $GIF_red, $GIF_grn, $GIF_blu, $GIF_mod + ) + { + if (!is_array($GIF_src)) { + printf("%s: %s", $this->VER, $this->ERR['ERR00']); + exit(0); + } + $this->LOP = ($GIF_lop > -1) ? $GIF_lop : 0; + $this->DIS = ($GIF_dis > -1) ? (($GIF_dis < 3) ? $GIF_dis : 3) : 2; + $this->COL = ($GIF_red > -1 && $GIF_grn > -1 && $GIF_blu > -1) ? + ($GIF_red | ($GIF_grn << 8) | ($GIF_blu << 16)) : -1; + for ($i = 0; $i < count($GIF_src); $i++) { + if (strtolower($GIF_mod) == "url") { + $this->BUF[] = fread(fopen($GIF_src[$i], "rb"), filesize($GIF_src[$i])); + } else if (strtolower($GIF_mod) == "bin") { + $this->BUF[] = $GIF_src[$i]; + } else { + printf("%s: %s ( %s )!", $this->VER, $this->ERR['ERR02'], $GIF_mod); + exit(0); + } + if (substr($this->BUF[$i], 0, 6) != "GIF87a" && substr($this->BUF[$i], 0, 6) != "GIF89a") { + printf("%s: %d %s", $this->VER, $i, $this->ERR['ERR01']); + exit(0); + } + for ($j = (13 + 3 * (2 << (ord($this->BUF[$i]{10}) & 0x07))), $k = true; $k; $j++) { + switch ($this->BUF[$i]{$j}) { + case "!": + if ((substr($this->BUF[$i], ($j + 3), 8)) == "NETSCAPE") { + printf("%s: %s ( %s source )!", $this->VER, $this->ERR['ERR03'], ($i + 1)); + exit(0); + } + break; + case ";": + $k = false; + break; + } + } + } + $this->addHeader(); + for ($i = 0; $i < count($this->BUF); $i++) { + $this->addFrames($i, $GIF_dly[$i]); + } + $this->addFooter(); + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFAddHeader... + :: + */ + public function addHeader() + { + if (ord($this->BUF[0]{10}) & 0x80) { + $cmap = 3 * (2 << (ord($this->BUF[0]{10}) & 0x07)); + $this->GIF .= substr($this->BUF[0], 6, 7); + $this->GIF .= substr($this->BUF[0], 13, $cmap); + $this->GIF .= "!\377\13NETSCAPE2.0\3\1" . $this->word($this->LOP) . "\0"; + } + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFAddFrames... + :: + */ + public function addFrames($i, $d) + { + $Locals_img = ''; + $Locals_str = 13 + 3 * (2 << (ord($this->BUF[$i]{10}) & 0x07)); + $Locals_end = strlen($this->BUF[$i]) - $Locals_str - 1; + $Locals_tmp = substr($this->BUF[$i], $Locals_str, $Locals_end); + $Global_len = 2 << (ord($this->BUF[0]{10}) & 0x07); + $Locals_len = 2 << (ord($this->BUF[$i]{10}) & 0x07); + $Global_rgb = substr($this->BUF[0], 13, + 3 * (2 << (ord($this->BUF[0]{10}) & 0x07))); + $Locals_rgb = substr($this->BUF[$i], 13, + 3 * (2 << (ord($this->BUF[$i]{10}) & 0x07))); + $Locals_ext = "!\xF9\x04" . chr(($this->DIS << 2) + 0) . + chr(($d >> 0) & 0xFF) . chr(($d >> 8) & 0xFF) . "\x0\x0"; + if ($this->COL > -1 && ord($this->BUF[$i]{10}) & 0x80) { + for ($j = 0; $j < (2 << (ord($this->BUF[$i]{10}) & 0x07)); $j++) { + if ( + ord($Locals_rgb{3 * $j + 0}) == (($this->COL >> 16) & 0xFF) && + ord($Locals_rgb{3 * $j + 1}) == (($this->COL >> 8) & 0xFF) && + ord($Locals_rgb{3 * $j + 2}) == (($this->COL >> 0) & 0xFF) + ) { + $Locals_ext = "!\xF9\x04" . chr(($this->DIS << 2) + 1) . + chr(($d >> 0) & 0xFF) . chr(($d >> 8) & 0xFF) . chr($j) . "\x0"; + break; + } + } + } + switch ($Locals_tmp{0}) { + case "!": + /** + * @var string $Locals_img ; + */ + $Locals_img = substr($Locals_tmp, 8, 10); + $Locals_tmp = substr($Locals_tmp, 18, strlen($Locals_tmp) - 18); + break; + case ",": + $Locals_img = substr($Locals_tmp, 0, 10); + $Locals_tmp = substr($Locals_tmp, 10, strlen($Locals_tmp) - 10); + break; + } + if (ord($this->BUF[$i]{10}) & 0x80 && $this->IMG > -1) { + if ($Global_len == $Locals_len) { + if ($this->blockCompare($Global_rgb, $Locals_rgb, $Global_len)) { + $this->GIF .= ($Locals_ext . $Locals_img . $Locals_tmp); + } else { + $byte = ord($Locals_img{9}); + $byte |= 0x80; + $byte &= 0xF8; + $byte |= (ord($this->BUF[0]{10}) & 0x07); + $Locals_img{9} = chr($byte); + $this->GIF .= ($Locals_ext . $Locals_img . $Locals_rgb . $Locals_tmp); + } + } else { + $byte = ord($Locals_img{9}); + $byte |= 0x80; + $byte &= 0xF8; + $byte |= (ord($this->BUF[$i]{10}) & 0x07); + $Locals_img{9} = chr($byte); + $this->GIF .= ($Locals_ext . $Locals_img . $Locals_rgb . $Locals_tmp); + } + } else { + $this->GIF .= ($Locals_ext . $Locals_img . $Locals_tmp); + } + $this->IMG = 1; + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFAddFooter... + :: + */ + public function addFooter() + { + $this->GIF .= ";"; + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFBlockCompare... + :: + */ + public function blockCompare($GlobalBlock, $LocalBlock, $Len) + { + for ($i = 0; $i < $Len; $i++) { + if ( + $GlobalBlock{3 * $i + 0} != $LocalBlock{3 * $i + 0} || + $GlobalBlock{3 * $i + 1} != $LocalBlock{3 * $i + 1} || + $GlobalBlock{3 * $i + 2} != $LocalBlock{3 * $i + 2} + ) { + return (0); + } + } + return (1); + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFWord... + :: + */ + public function word($int) + { + return (chr($int & 0xFF) . chr(($int >> 8) & 0xFF)); + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GetAnimation... + :: + */ + public function getAnimation() + { + return ($this->GIF); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/src/image/gif/Gif.php b/vendor/topthink/think-image/src/image/gif/Gif.php new file mode 100644 index 0000000..46fc9bf --- /dev/null +++ b/vendor/topthink/think-image/src/image/gif/Gif.php @@ -0,0 +1,88 @@ + +// +---------------------------------------------------------------------- + +namespace think\image\gif; + +class Gif +{ + /** + * GIF帧列表 + * + * @var array + */ + private $frames = []; + /** + * 每帧等待时间列表 + * + * @var array + */ + private $delays = []; + + /** + * 构造方法,用于解码GIF图片 + * + * @param string $src GIF图片数据 + * @param string $mod 图片数据类型 + * @throws \Exception + */ + public function __construct($src = null, $mod = 'url') + { + if (!is_null($src)) { + if ('url' == $mod && is_file($src)) { + $src = file_get_contents($src); + } + /* 解码GIF图片 */ + try { + $de = new Decoder($src); + $this->frames = $de->getFrames(); + $this->delays = $de->getDelays(); + } catch (\Exception $e) { + throw new \Exception("解码GIF图片出错"); + } + } + } + + /** + * 设置或获取当前帧的数据 + * + * @param string $stream 二进制数据流 + * @return mixed 获取到的数据 + */ + public function image($stream = null) + { + if (is_null($stream)) { + $current = current($this->frames); + return false === $current ? reset($this->frames) : $current; + } + $this->frames[key($this->frames)] = $stream; + } + + /** + * 将当前帧移动到下一帧 + * + * @return string 当前帧数据 + */ + public function nextImage() + { + return next($this->frames); + } + + /** + * 编码并保存当前GIF图片 + * + * @param string $pathname 图片名称 + */ + public function save($pathname) + { + $gif = new Encoder($this->frames, $this->delays, 0, 2, 0, 0, 0, 'bin'); + file_put_contents($pathname, $gif->getAnimation()); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/CropTest.php b/vendor/topthink/think-image/tests/CropTest.php new file mode 100644 index 0000000..48d6c73 --- /dev/null +++ b/vendor/topthink/think-image/tests/CropTest.php @@ -0,0 +1,67 @@ + +// +---------------------------------------------------------------------- +namespace tests; + +use think\Image; + +class CropTest extends TestCase +{ + public function testJpeg() + { + $pathname = TEST_PATH . 'tmp/crop.jpg'; + $image = Image::open($this->getJpeg()); + + $image->crop(200, 200, 100, 100, 300, 300)->save($pathname); + + $this->assertEquals(300, $image->width()); + $this->assertEquals(300, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + public function testPng() + { + $pathname = TEST_PATH . 'tmp/crop.png'; + $image = Image::open($this->getPng()); + + $image->crop(200, 200, 100, 100, 300, 300)->save($pathname); + + $this->assertEquals(300, $image->width()); + $this->assertEquals(300, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + public function testGif() + { + $pathname = TEST_PATH . 'tmp/crop.gif'; + $image = Image::open($this->getGif()); + + $image->crop(200, 200, 100, 100, 300, 300)->save($pathname); + + $this->assertEquals(300, $image->width()); + $this->assertEquals(300, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/FlipTest.php b/vendor/topthink/think-image/tests/FlipTest.php new file mode 100644 index 0000000..21b80b9 --- /dev/null +++ b/vendor/topthink/think-image/tests/FlipTest.php @@ -0,0 +1,43 @@ + +// +---------------------------------------------------------------------- +namespace tests; + +use think\Image; + +class FlipTest extends TestCase +{ + public function testJpeg() + { + $pathname = TEST_PATH . 'tmp/flip.jpg'; + $image = Image::open($this->getJpeg()); + $image->flip()->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + + public function testGif() + { + $pathname = TEST_PATH . 'tmp/flip.gif'; + $image = Image::open($this->getGif()); + $image->flip(Image::FLIP_Y)->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/InfoTest.php b/vendor/topthink/think-image/tests/InfoTest.php new file mode 100644 index 0000000..9f7cb3c --- /dev/null +++ b/vendor/topthink/think-image/tests/InfoTest.php @@ -0,0 +1,60 @@ + +// +---------------------------------------------------------------------- +namespace tests; + +use think\Image; + +class InfoTest extends TestCase +{ + + public function testOpen() + { + $this->setExpectedException("\\think\\image\\Exception"); + Image::open(''); + } + + public function testIllegal() + { + $this->setExpectedException("\\think\\image\\Exception", 'Illegal image file'); + Image::open(TEST_PATH . 'images/test.bmp'); + } + + public function testJpeg() + { + $image = Image::open($this->getJpeg()); + $this->assertEquals(800, $image->width()); + $this->assertEquals(600, $image->height()); + $this->assertEquals('jpeg', $image->type()); + $this->assertEquals('image/jpeg', $image->mime()); + $this->assertEquals([800, 600], $image->size()); + } + + + public function testPng() + { + $image = Image::open($this->getPng()); + $this->assertEquals(800, $image->width()); + $this->assertEquals(600, $image->height()); + $this->assertEquals('png', $image->type()); + $this->assertEquals('image/png', $image->mime()); + $this->assertEquals([800, 600], $image->size()); + } + + public function testGif() + { + $image = Image::open($this->getGif()); + $this->assertEquals(380, $image->width()); + $this->assertEquals(216, $image->height()); + $this->assertEquals('gif', $image->type()); + $this->assertEquals('image/gif', $image->mime()); + $this->assertEquals([380, 216], $image->size()); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/RotateTest.php b/vendor/topthink/think-image/tests/RotateTest.php new file mode 100644 index 0000000..85ecd79 --- /dev/null +++ b/vendor/topthink/think-image/tests/RotateTest.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- +namespace tests; + +use think\Image; + +class RotateTest extends TestCase +{ + public function testJpeg() + { + $pathname = TEST_PATH . 'tmp/rotate.jpg'; + $image = Image::open($this->getJpeg()); + $image->rotate(90)->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + public function testGif() + { + $pathname = TEST_PATH . 'tmp/rotate.gif'; + $image = Image::open($this->getGif()); + $image->rotate(90)->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/TestCase.php b/vendor/topthink/think-image/tests/TestCase.php new file mode 100644 index 0000000..888ace8 --- /dev/null +++ b/vendor/topthink/think-image/tests/TestCase.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace tests; + +use think\File; + +abstract class TestCase extends \PHPUnit_Framework_TestCase +{ + + protected function getJpeg() + { + return new File(TEST_PATH . 'images/test.jpg'); + } + + protected function getPng() + { + return new File(TEST_PATH . 'images/test.png'); + } + + protected function getGif() + { + return new File(TEST_PATH . 'images/test.gif'); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/TextTest.php b/vendor/topthink/think-image/tests/TextTest.php new file mode 100644 index 0000000..992c427 --- /dev/null +++ b/vendor/topthink/think-image/tests/TextTest.php @@ -0,0 +1,58 @@ + +// +---------------------------------------------------------------------- +namespace tests; + +use think\Image; + +class TextTest extends TestCase +{ + public function testJpeg() + { + $pathname = TEST_PATH . 'tmp/text.jpg'; + $image = Image::open($this->getJpeg()); + + $image->text('test', TEST_PATH . 'images/test.ttf', 12)->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + public function testPng() + { + $pathname = TEST_PATH . 'tmp/text.png'; + $image = Image::open($this->getPng()); + + $image->text('test', TEST_PATH . 'images/test.ttf', 12)->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + public function testGif() + { + $pathname = TEST_PATH . 'tmp/text.gif'; + $image = Image::open($this->getGif()); + + $image->text('test', TEST_PATH . 'images/test.ttf', 12)->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/ThumbTest.php b/vendor/topthink/think-image/tests/ThumbTest.php new file mode 100644 index 0000000..7f461db --- /dev/null +++ b/vendor/topthink/think-image/tests/ThumbTest.php @@ -0,0 +1,284 @@ + +// +---------------------------------------------------------------------- +namespace tests; + +use think\Image; + +class ThumbTest extends TestCase +{ + public function testJpeg() + { + $pathname = TEST_PATH . 'tmp/thumb.jpg'; + + //1 + $image = Image::open($this->getJpeg()); + + $image->thumb(200, 200, Image::THUMB_CENTER)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //2 + $image = Image::open($this->getJpeg()); + + $image->thumb(200, 200, Image::THUMB_SCALING)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(150, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //3 + $image = Image::open($this->getJpeg()); + + $image->thumb(200, 200, Image::THUMB_FILLED)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //4 + $image = Image::open($this->getJpeg()); + + $image->thumb(200, 200, Image::THUMB_NORTHWEST)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //5 + $image = Image::open($this->getJpeg()); + + $image->thumb(200, 200, Image::THUMB_SOUTHEAST)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //6 + $image = Image::open($this->getJpeg()); + + $image->thumb(200, 200, Image::THUMB_FIXED)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + + public function testPng() + { + $pathname = TEST_PATH . 'tmp/thumb.png'; + + //1 + $image = Image::open($this->getPng()); + + $image->thumb(200, 200, Image::THUMB_CENTER)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //2 + $image = Image::open($this->getPng()); + + $image->thumb(200, 200, Image::THUMB_SCALING)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(150, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //3 + $image = Image::open($this->getPng()); + + $image->thumb(200, 200, Image::THUMB_FILLED)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //4 + $image = Image::open($this->getPng()); + + $image->thumb(200, 200, Image::THUMB_NORTHWEST)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //5 + $image = Image::open($this->getPng()); + + $image->thumb(200, 200, Image::THUMB_SOUTHEAST)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //6 + $image = Image::open($this->getPng()); + + $image->thumb(200, 200, Image::THUMB_FIXED)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + public function testGif() + { + $pathname = TEST_PATH . 'tmp/thumb.gif'; + + //1 + $image = Image::open($this->getGif()); + + $image->thumb(200, 200, Image::THUMB_CENTER)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //2 + $image = Image::open($this->getGif()); + + $image->thumb(200, 200, Image::THUMB_SCALING)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(113, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //3 + $image = Image::open($this->getGif()); + + $image->thumb(200, 200, Image::THUMB_FILLED)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //4 + $image = Image::open($this->getGif()); + + $image->thumb(200, 200, Image::THUMB_NORTHWEST)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //5 + $image = Image::open($this->getGif()); + + $image->thumb(200, 200, Image::THUMB_SOUTHEAST)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //6 + $image = Image::open($this->getGif()); + + $image->thumb(200, 200, Image::THUMB_FIXED)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/WaterTest.php b/vendor/topthink/think-image/tests/WaterTest.php new file mode 100644 index 0000000..3daa614 --- /dev/null +++ b/vendor/topthink/think-image/tests/WaterTest.php @@ -0,0 +1,58 @@ + +// +---------------------------------------------------------------------- +namespace tests; + +use think\Image; + +class WaterTest extends TestCase +{ + public function testJpeg() + { + $pathname = TEST_PATH . 'tmp/water.jpg'; + $image = Image::open($this->getJpeg()); + + $image->water(TEST_PATH . 'images/test.gif')->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + public function testPng() + { + $pathname = TEST_PATH . 'tmp/water.png'; + $image = Image::open($this->getPng()); + + $image->water(TEST_PATH . 'images/test.gif')->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + public function testGif() + { + $pathname = TEST_PATH . 'tmp/water.gif'; + $image = Image::open($this->getGif()); + + $image->water(TEST_PATH . 'images/test.jpg')->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/autoload.php b/vendor/topthink/think-image/tests/autoload.php new file mode 100644 index 0000000..1110027 --- /dev/null +++ b/vendor/topthink/think-image/tests/autoload.php @@ -0,0 +1,15 @@ + +// +---------------------------------------------------------------------- +define('TEST_PATH', __DIR__ . '/'); +// 加载框架基础文件 +require __DIR__ . '/../thinkphp/base.php'; +\think\Loader::addNamespace('tests', TEST_PATH); +\think\Loader::addNamespace('think', __DIR__ . '/../src/'); \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/images/test.bmp b/vendor/topthink/think-image/tests/images/test.bmp new file mode 100644 index 0000000..e69de29 diff --git a/vendor/topthink/think-image/tests/images/test.gif b/vendor/topthink/think-image/tests/images/test.gif new file mode 100644 index 0000000..c6d5472 Binary files /dev/null and b/vendor/topthink/think-image/tests/images/test.gif differ diff --git a/vendor/topthink/think-image/tests/images/test.jpg b/vendor/topthink/think-image/tests/images/test.jpg new file mode 100644 index 0000000..4bb6549 Binary files /dev/null and b/vendor/topthink/think-image/tests/images/test.jpg differ diff --git a/vendor/topthink/think-image/tests/images/test.png b/vendor/topthink/think-image/tests/images/test.png new file mode 100644 index 0000000..f4830e3 Binary files /dev/null and b/vendor/topthink/think-image/tests/images/test.png differ diff --git a/vendor/topthink/think-image/tests/images/test.ttf b/vendor/topthink/think-image/tests/images/test.ttf new file mode 100644 index 0000000..4f985c8 Binary files /dev/null and b/vendor/topthink/think-image/tests/images/test.ttf differ diff --git a/vendor/topthink/think-image/tests/tmp/.gitignore b/vendor/topthink/think-image/tests/tmp/.gitignore new file mode 100644 index 0000000..a3a0c8b --- /dev/null +++ b/vendor/topthink/think-image/tests/tmp/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/vendor/topthink/think-installer/.gitignore b/vendor/topthink/think-installer/.gitignore new file mode 100644 index 0000000..8f4c02d --- /dev/null +++ b/vendor/topthink/think-installer/.gitignore @@ -0,0 +1,3 @@ +/.idea +composer.lock +/vendor \ No newline at end of file diff --git a/vendor/topthink/think-installer/composer.json b/vendor/topthink/think-installer/composer.json new file mode 100644 index 0000000..08bc72b --- /dev/null +++ b/vendor/topthink/think-installer/composer.json @@ -0,0 +1,25 @@ +{ + "name": "topthink/think-installer", + "type": "composer-plugin", + "require": { + "composer-plugin-api": "^1.0||^2.0" + }, + "require-dev": { + "composer/composer": "^1.0||^2.0" + }, + "license": "Apache-2.0", + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "autoload": { + "psr-4": { + "think\\composer\\": "src" + } + }, + "extra": { + "class": "think\\composer\\Plugin" + } +} diff --git a/vendor/topthink/think-installer/src/LibraryInstaller.php b/vendor/topthink/think-installer/src/LibraryInstaller.php new file mode 100644 index 0000000..45c05bc --- /dev/null +++ b/vendor/topthink/think-installer/src/LibraryInstaller.php @@ -0,0 +1,28 @@ +makePromise(parent::install($repo, $package)); + } + + public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) + { + return $this->makePromise(parent::update($repo, $initial, $target)); + } + + protected function makePromise($promise) + { + if ($promise instanceof PromiseInterface) { + return $promise; + } + return new Promise(); + } +} diff --git a/vendor/topthink/think-installer/src/Plugin.php b/vendor/topthink/think-installer/src/Plugin.php new file mode 100644 index 0000000..4415232 --- /dev/null +++ b/vendor/topthink/think-installer/src/Plugin.php @@ -0,0 +1,34 @@ +getInstallationManager(); + + //框架核心 + $manager->addInstaller(new ThinkFramework($io, $composer)); + + //单元测试 + $manager->addInstaller(new ThinkTesting($io, $composer)); + + //扩展 + $manager->addInstaller(new ThinkExtend($io, $composer)); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + + } + + public function uninstall(Composer $composer, IOInterface $io) + { + + } +} diff --git a/vendor/topthink/think-installer/src/Promise.php b/vendor/topthink/think-installer/src/Promise.php new file mode 100644 index 0000000..6bfed5d --- /dev/null +++ b/vendor/topthink/think-installer/src/Promise.php @@ -0,0 +1,11 @@ + +// +---------------------------------------------------------------------- + +namespace think\composer; + +use Composer\Package\PackageInterface; +use Composer\Repository\InstalledRepositoryInterface; + +class ThinkExtend extends LibraryInstaller +{ + public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + { + return parent::install($repo, $package) + ->then(function () use ($package) { + $this->copyExtraFiles($package); + }); + } + + public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) + { + return parent::update($repo, $initial, $target) + ->then(function () use ($target) { + $this->copyExtraFiles($target); + }); + } + + protected function copyExtraFiles(PackageInterface $package) + { + if ($this->composer->getPackage()->getType() == 'project') { + + $extra = $package->getExtra(); + + if (!empty($extra['think-config'])) { + + $configDir = 'config'; + + $this->filesystem->ensureDirectoryExists($configDir); + + //配置文件 + foreach ((array) $extra['think-config'] as $name => $config) { + $target = $configDir . DIRECTORY_SEPARATOR . $name . '.php'; + $source = $this->getInstallPath($package) . DIRECTORY_SEPARATOR . $config; + + if (is_file($target)) { + $this->io->write("File {$target} exist!"); + continue; + } + + if (!is_file($source)) { + $this->io->write("File {$target} not exist!"); + continue; + } + + copy($source, $target); + } + } + } + } + + public function supports($packageType) + { + return 'think-extend' === $packageType; + } +} diff --git a/vendor/topthink/think-installer/src/ThinkFramework.php b/vendor/topthink/think-installer/src/ThinkFramework.php new file mode 100644 index 0000000..2a7cc03 --- /dev/null +++ b/vendor/topthink/think-installer/src/ThinkFramework.php @@ -0,0 +1,66 @@ +getPrettyName()) { + throw new InvalidArgumentException('Unable to install this library!'); + } + + if ($this->composer->getPackage()->getType() !== 'project') { + return parent::getInstallPath($package); + } + + if ($this->composer->getPackage()) { + $extra = $this->composer->getPackage()->getExtra(); + if (!empty($extra['think-path'])) { + return $extra['think-path']; + } + } + + return 'thinkphp'; + } + + public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + { + return parent::install($repo, $package) + ->then(function () use ($package) { + $this->removeTestDir($package); + }); + } + + public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) + { + return parent::update($repo, $initial, $target) + ->then(function () use ($target) { + $this->removeTestDir($target); + }); + } + + protected function removeTestDir(PackageInterface $target) + { + if ($this->composer->getPackage()->getType() == 'project' && $target->getInstallationSource() != 'source') { + //remove tests dir + $this->filesystem->removeDirectory($this->getInstallPath($target) . DIRECTORY_SEPARATOR . 'tests'); + } + } + + /** + * {@inheritDoc} + */ + public function supports($packageType) + { + return 'think-framework' === $packageType; + } +} diff --git a/vendor/topthink/think-installer/src/ThinkTesting.php b/vendor/topthink/think-installer/src/ThinkTesting.php new file mode 100644 index 0000000..af74d20 --- /dev/null +++ b/vendor/topthink/think-installer/src/ThinkTesting.php @@ -0,0 +1,66 @@ + +// +---------------------------------------------------------------------- + +namespace think\composer; + +use Composer\Package\PackageInterface; +use Composer\Repository\InstalledRepositoryInterface; +use InvalidArgumentException; + +class ThinkTesting extends LibraryInstaller +{ + /** + * {@inheritDoc} + */ + public function getInstallPath(PackageInterface $package) + { + if ('topthink/think-testing' !== $package->getPrettyName()) { + throw new InvalidArgumentException('Unable to install this library!'); + } + + return parent::getInstallPath($package); + } + + public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + { + return parent::install($repo, $package) + ->then(function () use ($package) { + $this->copyTestDir($package); + }); + } + + public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) + { + return parent::update($repo, $initial, $target) + ->then(function () use ($target) { + $this->copyTestDir($target); + }); + } + + private function copyTestDir(PackageInterface $package) + { + $appDir = dirname($this->vendorDir); + $source = $this->getInstallPath($package) . DIRECTORY_SEPARATOR . 'example'; + if (!is_file($appDir . DIRECTORY_SEPARATOR . 'phpunit.xml')) { + $this->filesystem->copyThenRemove($source, $appDir); + } else { + $this->filesystem->removeDirectoryPhp($source); + } + } + + /** + * {@inheritDoc} + */ + public function supports($packageType) + { + return 'think-testing' === $packageType; + } +}